diff --git a/manim/mobject/text/tex_mobject.py b/manim/mobject/text/tex_mobject.py index 03bc285e79..345ab6fc4a 100644 --- a/manim/mobject/text/tex_mobject.py +++ b/manim/mobject/text/tex_mobject.py @@ -342,7 +342,10 @@ def _break_up_by_substrings(self) -> Self: """ new_submobjects: list[VMobject] = [] curr_index = 0 - for tex_string in self.tex_strings: + i = 0 + + while i < len(self.tex_strings): + tex_string = self.tex_strings[i] sub_tex_mob = SingleStringMathTex( tex_string, tex_environment=self.tex_environment, @@ -352,13 +355,79 @@ def _break_up_by_substrings(self) -> Self: new_index = ( curr_index + num_submobs + len("".join(self.arg_separator.split())) ) + if num_submobs == 0: last_submob_index = min(curr_index, len(self.submobjects) - 1) sub_tex_mob.move_to(self.submobjects[last_submob_index], RIGHT) + new_submobjects.append(sub_tex_mob) + curr_index = new_index + i += 1 + elif tex_string.strip().startswith(("^", "_")): + # Handle consecutive scripts as a group + script_group = [tex_string] + j = i + 1 + while j < len(self.tex_strings) and self.tex_strings[ + j + ].strip().startswith(("^", "_")): + script_group.append(self.tex_strings[j]) + j += 1 + + # Calculate total submobjects needed for all scripts + total_script_submobs = sum( + len( + SingleStringMathTex( + s, + tex_environment=self.tex_environment, + tex_template=self.tex_template, + ).submobjects + ) + for s in script_group + ) + + # Get the pool of available submobjects for all scripts + script_pool = self.submobjects[ + curr_index : curr_index + total_script_submobs + ] + + # Process each script in the group + for script_tex in script_group: + script_mob = SingleStringMathTex( + script_tex, + tex_environment=self.tex_environment, + tex_template=self.tex_template, + ) + script_num_submobs = len(script_mob.submobjects) + + if script_num_submobs > 0 and len(script_pool) > 0: + # Select submobjects by Y position + is_superscript = script_tex.strip().startswith("^") + sorted_pool = sorted( + script_pool, + key=lambda mob: mob.get_center()[1], + reverse=is_superscript, # highest first for ^, lowest first for _ + ) + + # Take the first script_num_submobs from sorted pool + selected = sorted_pool[:script_num_submobs] + script_mob.submobjects = selected + + # Remove selected submobjects from pool + for sel in selected: + if sel in script_pool: + script_pool.remove(sel) + + new_submobjects.append(script_mob) + + # Update indices + curr_index += total_script_submobs + i = j # Skip past all processed scripts else: + # Normal (non-script) processing sub_tex_mob.submobjects = self.submobjects[curr_index:new_index] - new_submobjects.append(sub_tex_mob) - curr_index = new_index + new_submobjects.append(sub_tex_mob) + curr_index = new_index + i += 1 + self.submobjects = new_submobjects return self diff --git a/tests/module/mobject/text/test_texmobject.py b/tests/module/mobject/text/test_texmobject.py index ca8e635ea6..b6addf8aa4 100644 --- a/tests/module/mobject/text/test_texmobject.py +++ b/tests/module/mobject/text/test_texmobject.py @@ -225,3 +225,31 @@ def test_tex_garbage_collection(tmpdir, monkeypatch, config): tex_with_log = Tex("Hello World, again!") # da27670a37b08799.tex assert Path("media", "Tex", "da27670a37b08799.log").exists() + + +def test_tex_strings_with_subscripts_and_superscripts(): + """Test that submobjects match tex_strings and positions when LaTeX reorders scripts. + + Regression test for issue #3548. + """ + eq1 = MathTex("A", "^n", "_1") + assert eq1.submobjects[0].get_tex_string() == "A" + assert eq1.submobjects[1].get_tex_string() == "^n" + assert eq1.submobjects[2].get_tex_string() == "_1" + assert eq1.submobjects[1].get_center()[1] > 0 + assert eq1.submobjects[2].get_center()[1] < 0 + + eq2 = MathTex("A", "_1", "^n") + assert eq2.submobjects[0].get_tex_string() == "A" + assert eq2.submobjects[1].get_tex_string() == "_1" + assert eq2.submobjects[2].get_tex_string() == "^n" + assert eq2.submobjects[1].get_center()[1] < 0 + assert eq2.submobjects[2].get_center()[1] > 0 + + eq3 = MathTex("\\sum", "^n", "_1", "x") + assert eq3.submobjects[0].get_tex_string() == "\\sum" + assert eq3.submobjects[1].get_tex_string() == "^n" + assert eq3.submobjects[2].get_tex_string() == "_1" + assert eq3.submobjects[3].get_tex_string() == "x" + assert eq3.submobjects[1].get_center()[1] > 0 + assert eq3.submobjects[2].get_center()[1] < 0