@@ -363,74 +363,113 @@ def _break_up_by_substrings(self) -> Self:
363363 curr_index = new_index
364364 i += 1
365365 elif tex_string .strip ().startswith (("^" , "_" )):
366- # Handle consecutive scripts as a group
367- script_group = [tex_string ]
368- j = i + 1
369- while j < len (self .tex_strings ) and self .tex_strings [
370- j
371- ].strip ().startswith (("^" , "_" )):
372- script_group .append (self .tex_strings [j ])
373- j += 1
374-
375- # Calculate total submobjects needed for all scripts
376- total_script_submobs = sum (
377- len (
378- SingleStringMathTex (
379- s ,
380- tex_environment = self .tex_environment ,
381- tex_template = self .tex_template ,
382- ).submobjects
383- )
384- for s in script_group
385- )
366+ # Handle consecutive scripts as a group, matching by Y-position
367+ script_group , j = self ._group_consecutive_scripts (i )
368+ total_script_submobs = self ._total_submobs_for_scripts (script_group )
369+ script_pool = self .submobjects [curr_index :curr_index + total_script_submobs ]
370+
371+ self ._assign_script_group (script_group , script_pool , new_submobjects )
386372
387- # Get the pool of available submobjects for all scripts
388- script_pool = self .submobjects [
389- curr_index : curr_index + total_script_submobs
390- ]
391-
392- # Process each script in the group
393- for script_tex in script_group :
394- script_mob = SingleStringMathTex (
395- script_tex ,
396- tex_environment = self .tex_environment ,
397- tex_template = self .tex_template ,
398- )
399- script_num_submobs = len (script_mob .submobjects )
400-
401- if script_num_submobs > 0 and len (script_pool ) > 0 :
402- # Select submobjects by Y position
403- is_superscript = script_tex .strip ().startswith ("^" )
404- sorted_pool = sorted (
405- script_pool ,
406- key = lambda mob : mob .get_center ()[1 ],
407- reverse = is_superscript , # highest first for ^, lowest first for _
408- )
409-
410- # Take the first script_num_submobs from sorted pool
411- selected = sorted_pool [:script_num_submobs ]
412- script_mob .submobjects = selected
413-
414- # Remove selected submobjects from pool
415- for sel in selected :
416- if sel in script_pool :
417- script_pool .remove (sel )
418-
419- new_submobjects .append (script_mob )
420-
421- # Update indices
422373 curr_index += total_script_submobs
423- i = j # Skip past all processed scripts
374+ i = j
424375 else :
425- # Normal (non-script) processing
426- sub_tex_mob .submobjects = self .submobjects [curr_index :new_index ]
427- new_submobjects .append (sub_tex_mob )
428- curr_index = new_index
429- i += 1
376+ # Base element processing: check if followed by scripts
377+ next_is_script = (i + 1 < len (self .tex_strings ) and
378+ self .tex_strings [i + 1 ].strip ().startswith (("^" , "_" )))
379+
380+ if next_is_script and num_submobs > 0 :
381+ script_group , j = self ._group_consecutive_scripts (i + 1 )
382+ total_script_submobs = self ._total_submobs_for_scripts (script_group )
383+ total_needed = num_submobs + total_script_submobs
384+
385+ all_submobs = self .submobjects [curr_index :curr_index + total_needed ]
386+
387+ # Only use special handling if scripts have content (non-empty)
388+ if total_script_submobs > 0 and len (all_submobs ) == total_needed :
389+ # LaTeX may render base+scripts in unexpected order
390+ # Find base by Y-position: closest to baseline (Y=0)
391+ base_submob = min (all_submobs , key = lambda m : abs (m .get_center ()[1 ]))
392+ sub_tex_mob .submobjects = [base_submob ]
393+
394+ script_pool = [m for m in all_submobs if m != base_submob ]
395+ new_submobjects .append (sub_tex_mob )
396+
397+ self ._assign_script_group (script_group , script_pool , new_submobjects )
398+
399+ curr_index += total_needed
400+ i = j
401+ else :
402+ # Fallback if counts don't match
403+ sub_tex_mob .submobjects = self .submobjects [curr_index :new_index ]
404+ new_submobjects .append (sub_tex_mob )
405+ curr_index = new_index
406+ i += 1
407+ else :
408+ sub_tex_mob .submobjects = self .submobjects [curr_index :new_index ]
409+ new_submobjects .append (sub_tex_mob )
410+ curr_index = new_index
411+ i += 1
430412
431413 self .submobjects = new_submobjects
432414 return self
433415
416+ def _group_consecutive_scripts (self , start_index : int ) -> tuple [list [str ], int ]:
417+ """Collect consecutive script tex_strings starting at ``start_index``.
418+
419+ Returns the list of scripts and the index just after the group.
420+ Scripts are tex strings starting with '^' or '_'.
421+ """
422+ script_group = [self .tex_strings [start_index ]]
423+ j = start_index + 1
424+ while j < len (self .tex_strings ) and self .tex_strings [j ].strip ().startswith (("^" , "_" )):
425+ script_group .append (self .tex_strings [j ])
426+ j += 1
427+ return script_group , j
428+
429+ def _total_submobs_for_scripts (self , script_group : list [str ]) -> int :
430+ """Calculate total submobject count for a group of script strings.
431+
432+ Creates temporary SingleStringMathTex instances to inspect counts.
433+ """
434+ total = 0
435+ for s in script_group :
436+ total += len (
437+ SingleStringMathTex (
438+ s , tex_environment = self .tex_environment , tex_template = self .tex_template
439+ ).submobjects
440+ )
441+ return total
442+
443+ def _assign_script_group (self , script_group : list [str ], script_pool : list [VMobject ], new_submobjects : list [VMobject ]) -> None :
444+ """Assign submobjects from ``script_pool`` to scripts in ``script_group``.
445+
446+ Selection strategy:
447+ - Superscripts ('^'): Pick highest Y-position items (above baseline)
448+ - Subscripts ('_'): Pick lowest Y-position items (below baseline)
449+
450+ Selected submobjects are removed from pool to prevent reuse.
451+ """
452+ for script_tex in script_group :
453+ script_mob = SingleStringMathTex (
454+ script_tex , tex_environment = self .tex_environment , tex_template = self .tex_template
455+ )
456+ script_num_submobs = len (script_mob .submobjects )
457+
458+ if script_num_submobs > 0 and len (script_pool ) > 0 :
459+ is_superscript = script_tex .strip ().startswith ("^" )
460+ # Sort by Y-position: reverse=True for superscripts (highest first)
461+ sorted_pool = sorted (script_pool , key = lambda mob : mob .get_center ()[1 ], reverse = is_superscript )
462+
463+ selected = sorted_pool [:script_num_submobs ]
464+ script_mob .submobjects = selected
465+
466+ # Remove selected items from pool
467+ for sel in selected :
468+ if sel in script_pool :
469+ script_pool .remove (sel )
470+
471+ new_submobjects .append (script_mob )
472+
434473 def get_parts_by_tex (
435474 self , tex : str , substring : bool = True , case_sensitive : bool = True
436475 ) -> VGroup :
0 commit comments