From b88b358c79120409314ee1f34a1ee3a81d5e18ef Mon Sep 17 00:00:00 2001 From: Henrik Skov Midtiby Date: Thu, 13 Nov 2025 16:25:11 +0100 Subject: [PATCH 01/56] Extracted the method get_mob_from_shape_element --- manim/mobject/svg/svg_mobject.py | 38 ++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/manim/mobject/svg/svg_mobject.py b/manim/mobject/svg/svg_mobject.py index bd494c0211..c73d7334fc 100644 --- a/manim/mobject/svg/svg_mobject.py +++ b/manim/mobject/svg/svg_mobject.py @@ -269,27 +269,11 @@ def get_mobjects_from(self, svg: se.SVG) -> list[VMobject]: result: list[VMobject] = [] for shape in svg.elements(): # can we combine the two continue cases into one? + mob = self.get_mob_from_shape_element(shape) if isinstance(shape, se.Group): # noqa: SIM114 continue - elif isinstance(shape, se.Path): - mob: VMobject = self.path_to_mobject(shape) - elif isinstance(shape, se.SimpleLine): - mob = self.line_to_mobject(shape) - elif isinstance(shape, se.Rect): - mob = self.rect_to_mobject(shape) - elif isinstance(shape, (se.Circle, se.Ellipse)): - mob = self.ellipse_to_mobject(shape) - elif isinstance(shape, se.Polygon): - mob = self.polygon_to_mobject(shape) - elif isinstance(shape, se.Polyline): - mob = self.polyline_to_mobject(shape) - elif isinstance(shape, se.Text): - mob = self.text_to_mobject(shape) elif isinstance(shape, se.Use) or type(shape) is se.SVGElement: continue - else: - logger.warning(f"Unsupported element type: {type(shape)}") - continue if mob is None or not mob.has_points(): continue self.apply_style_to_mobject(mob, shape) @@ -298,6 +282,26 @@ def get_mobjects_from(self, svg: se.SVG) -> list[VMobject]: result.append(mob) return result + def get_mob_from_shape_element(self, shape: se.SVGElement) -> VMobject | None: + if isinstance(shape, se.Path): + mob: VMobject | None = self.path_to_mobject(shape) + elif isinstance(shape, se.SimpleLine): + mob = self.line_to_mobject(shape) + elif isinstance(shape, se.Rect): + mob = self.rect_to_mobject(shape) + elif isinstance(shape, (se.Circle, se.Ellipse)): + mob = self.ellipse_to_mobject(shape) + elif isinstance(shape, se.Polygon): + mob = self.polygon_to_mobject(shape) + elif isinstance(shape, se.Polyline): + mob = self.polyline_to_mobject(shape) + elif isinstance(shape, se.Text): + mob = self.text_to_mobject(shape) + else: + logger.warning(f"Unsupported element type: {type(shape)}") + mob = None + return mob + @staticmethod def handle_transform(mob: VMobject, matrix: se.Matrix) -> VMobject: """Apply SVG transformations to the converted mobject. From e8e003ac203cc3bbab5081711fb4fa4b3ff79f5e Mon Sep 17 00:00:00 2001 From: Henrik Skov Midtiby Date: Thu, 13 Nov 2025 16:31:26 +0100 Subject: [PATCH 02/56] Moved more functionality to get_mob_from_shape_element --- manim/mobject/svg/svg_mobject.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/manim/mobject/svg/svg_mobject.py b/manim/mobject/svg/svg_mobject.py index c73d7334fc..78062b14e4 100644 --- a/manim/mobject/svg/svg_mobject.py +++ b/manim/mobject/svg/svg_mobject.py @@ -268,18 +268,13 @@ def get_mobjects_from(self, svg: se.SVG) -> list[VMobject]: """ result: list[VMobject] = [] for shape in svg.elements(): - # can we combine the two continue cases into one? mob = self.get_mob_from_shape_element(shape) if isinstance(shape, se.Group): # noqa: SIM114 continue elif isinstance(shape, se.Use) or type(shape) is se.SVGElement: continue - if mob is None or not mob.has_points(): - continue - self.apply_style_to_mobject(mob, shape) - if isinstance(shape, se.Transformable) and shape.apply: - self.handle_transform(mob, shape.transform) - result.append(mob) + if mob is not None: + result.append(mob) return result def get_mob_from_shape_element(self, shape: se.SVGElement) -> VMobject | None: @@ -300,6 +295,11 @@ def get_mob_from_shape_element(self, shape: se.SVGElement) -> VMobject | None: else: logger.warning(f"Unsupported element type: {type(shape)}") mob = None + if mob is None or not mob.has_points(): + return mob + self.apply_style_to_mobject(mob, shape) + if isinstance(shape, se.Transformable) and shape.apply: + self.handle_transform(mob, shape.transform) return mob @staticmethod From f9f71387db302794b0f80983969344d10c8fd650 Mon Sep 17 00:00:00 2001 From: Henrik Skov Midtiby Date: Thu, 13 Nov 2025 16:38:21 +0100 Subject: [PATCH 03/56] More cleanup --- manim/mobject/svg/svg_mobject.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manim/mobject/svg/svg_mobject.py b/manim/mobject/svg/svg_mobject.py index 78062b14e4..3b1aaefbdc 100644 --- a/manim/mobject/svg/svg_mobject.py +++ b/manim/mobject/svg/svg_mobject.py @@ -268,11 +268,11 @@ def get_mobjects_from(self, svg: se.SVG) -> list[VMobject]: """ result: list[VMobject] = [] for shape in svg.elements(): - mob = self.get_mob_from_shape_element(shape) if isinstance(shape, se.Group): # noqa: SIM114 continue elif isinstance(shape, se.Use) or type(shape) is se.SVGElement: continue + mob = self.get_mob_from_shape_element(shape) if mob is not None: result.append(mob) return result From 1a9212b0a1729ba44f51e9050ab4c68e7a12d6e6 Mon Sep 17 00:00:00 2001 From: Henrik Skov Midtiby Date: Thu, 13 Nov 2025 22:49:55 +0100 Subject: [PATCH 04/56] Parse the svg file while maintaining the group structure. --- manim/mobject/svg/svg_mobject.py | 57 +++++++++++++++++++++++++++----- 1 file changed, 48 insertions(+), 9 deletions(-) diff --git a/manim/mobject/svg/svg_mobject.py b/manim/mobject/svg/svg_mobject.py index 3b1aaefbdc..cfb2921059 100644 --- a/manim/mobject/svg/svg_mobject.py +++ b/manim/mobject/svg/svg_mobject.py @@ -21,7 +21,7 @@ from ..geometry.line import Line from ..geometry.polygram import Polygon, Rectangle, RoundedRectangle from ..opengl.opengl_compatibility import ConvertToOpenGL -from ..types.vectorized_mobject import VMobject +from ..types.vectorized_mobject import VGroup, VMobject __all__ = ["SVGMobject", "VMobjectFromSVGPath"] @@ -267,14 +267,53 @@ def get_mobjects_from(self, svg: se.SVG) -> list[VMobject]: The parsed SVG file. """ result: list[VMobject] = [] - for shape in svg.elements(): - if isinstance(shape, se.Group): # noqa: SIM114 - continue - elif isinstance(shape, se.Use) or type(shape) is se.SVGElement: - continue - mob = self.get_mob_from_shape_element(shape) - if mob is not None: - result.append(mob) + stack: list[tuple[se.SVGElement, int]] = [] + stack.append((svg, 1)) + group_id_number = 0 + vgroup_stack: list[str] = ["root"] + vgroup_names: list[str] = ["root"] + vgroups: dict[str, VGroup] = {"root": VGroup()} + while len(stack) > 0: + element, depth = stack.pop() + # Reduce stack heights + vgroup_stack = vgroup_stack[0:(depth)] + try: + group_name = str(element.values["id"]) + except Exception: + group_name = f"numbered_group_{group_id_number}" + group_id_number += 1 + if isinstance(element, se.Group): + vg = VGroup() + vgroup_names.append(group_name) + vgroup_stack.append(group_name) + vgroups[group_name] = vg + + if isinstance(element, (se.Group, se.Use)): + for subelement in element[::-1]: + stack.append((subelement, depth + 1)) + # Add element to the parent vgroup + try: + if isinstance( + element, + ( + se.Path, + se.SimpleLine, + se.Rect, + se.Circle, + se.Ellipse, + se.Polygon, + se.Polyline, + se.Text, + ), + ): + mob = self.get_mob_from_shape_element(element) + if mob is not None: + result.append(mob) + parent_name = vgroup_stack[-2] + vgroups[parent_name].add(vgroups[group_name]) + except Exception as e: + print(e) + return result def get_mob_from_shape_element(self, shape: se.SVGElement) -> VMobject | None: From bd78c4387c0db866591b9166a7d413ad4fe3e683 Mon Sep 17 00:00:00 2001 From: Henrik Skov Midtiby Date: Thu, 13 Nov 2025 23:13:10 +0100 Subject: [PATCH 05/56] Make the svg groups available --- manim/mobject/svg/svg_mobject.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/manim/mobject/svg/svg_mobject.py b/manim/mobject/svg/svg_mobject.py index cfb2921059..50c8bfcb9d 100644 --- a/manim/mobject/svg/svg_mobject.py +++ b/manim/mobject/svg/svg_mobject.py @@ -127,6 +127,7 @@ def __init__( self.stroke_color = stroke_color self.stroke_opacity = stroke_opacity # type: ignore[assignment] self.stroke_width = stroke_width # type: ignore[assignment] + self.id_to_vgroup_dict: dict[str, VGroup] = {} if self.stroke_width is None: self.stroke_width = 0 @@ -203,8 +204,9 @@ def generate_mobject(self) -> None: svg = se.SVG.parse(modified_file_path) modified_file_path.unlink() - mobjects = self.get_mobjects_from(svg) + mobjects, mobject_dict = self.get_mobjects_from(svg) self.add(*mobjects) + self.id_to_vgroup_dict = mobject_dict self.flip(RIGHT) # Flip y def get_file_path(self) -> Path: @@ -258,7 +260,9 @@ def generate_config_style_dict(self) -> dict[str, str]: result[svg_key] = str(svg_default_dict[style_key]) return result - def get_mobjects_from(self, svg: se.SVG) -> list[VMobject]: + def get_mobjects_from( + self, svg: se.SVG + ) -> tuple[list[VMobject], dict[str, VGroup]]: """Convert the elements of the SVG to a list of mobjects. Parameters @@ -282,11 +286,10 @@ def get_mobjects_from(self, svg: se.SVG) -> list[VMobject]: except Exception: group_name = f"numbered_group_{group_id_number}" group_id_number += 1 - if isinstance(element, se.Group): - vg = VGroup() - vgroup_names.append(group_name) - vgroup_stack.append(group_name) - vgroups[group_name] = vg + vg = VGroup() + vgroup_names.append(group_name) + vgroup_stack.append(group_name) + vgroups[group_name] = vg if isinstance(element, (se.Group, se.Use)): for subelement in element[::-1]: @@ -310,11 +313,12 @@ def get_mobjects_from(self, svg: se.SVG) -> list[VMobject]: if mob is not None: result.append(mob) parent_name = vgroup_stack[-2] - vgroups[parent_name].add(vgroups[group_name]) + for parent_name in vgroup_stack[:-2]: + vgroups[parent_name].add(mob) except Exception as e: print(e) - return result + return result, vgroups def get_mob_from_shape_element(self, shape: se.SVGElement) -> VMobject | None: if isinstance(shape, se.Path): From 9e364e340697f961f66dd4312dcd1413611ed34c Mon Sep 17 00:00:00 2001 From: Henrik Skov Midtiby Date: Mon, 8 Dec 2025 18:00:48 +0100 Subject: [PATCH 06/56] Handle PERF401 issue --- manim/mobject/svg/svg_mobject.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/manim/mobject/svg/svg_mobject.py b/manim/mobject/svg/svg_mobject.py index 50c8bfcb9d..f54b251a64 100644 --- a/manim/mobject/svg/svg_mobject.py +++ b/manim/mobject/svg/svg_mobject.py @@ -292,8 +292,7 @@ def get_mobjects_from( vgroups[group_name] = vg if isinstance(element, (se.Group, se.Use)): - for subelement in element[::-1]: - stack.append((subelement, depth + 1)) + stack.extend((subelement, depth + 1) for subelement in element[::-1]) # Add element to the parent vgroup try: if isinstance( From 90594790788893e57b31630edab89b221cf2c410 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 15 Dec 2025 12:10:32 +0100 Subject: [PATCH 07/56] [pre-commit.ci] pre-commit autoupdate (#4506) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.14.7 → v0.14.8](https://github.com/astral-sh/ruff-pre-commit/compare/v0.14.7...v0.14.8) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5d191c3851..6f2abb3848 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,7 +21,7 @@ repos: args: ["-L", "medias,nam"] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.7 + rev: v0.14.8 hooks: - id: ruff name: ruff lint From 66f456eaf6b4f563812f5a68ff82c3817bb4c95a Mon Sep 17 00:00:00 2001 From: Henrik Skov Midtiby Date: Mon, 15 Dec 2025 16:35:33 +0100 Subject: [PATCH 08/56] Added an example of the issue --- issue/issue3492.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 issue/issue3492.py diff --git a/issue/issue3492.py b/issue/issue3492.py new file mode 100644 index 0000000000..058ccb1ce6 --- /dev/null +++ b/issue/issue3492.py @@ -0,0 +1,15 @@ +from __future__ import annotations + +from manim import * + + +class ExampleScene(Scene): + def construct(self): + formula = MathTex( + r"P(X=k) = ", + "\\binom{12}{k} ", + r"0.5^k", + r"(1-0.5)^{12-k}", + substrings_to_isolate=["k"], + ).scale(1.3) + self.play(formula.animate.set_color_by_tex("k", ORANGE)) From d6c6f5d21d8f7d4b99f7c7601d61832a913cba56 Mon Sep 17 00:00:00 2001 From: Henrik Skov Midtiby Date: Mon, 15 Dec 2025 17:57:54 +0100 Subject: [PATCH 09/56] Experimenting with coloring elements from the latex equation --- issue/issue3492.py | 34 +++++++++++++++++++++++++++++++ manim/mobject/text/tex_mobject.py | 32 +++++++++++++++++++++++++++-- 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/issue/issue3492.py b/issue/issue3492.py index 058ccb1ce6..00bc5d28e3 100644 --- a/issue/issue3492.py +++ b/issue/issue3492.py @@ -13,3 +13,37 @@ def construct(self): substrings_to_isolate=["k"], ).scale(1.3) self.play(formula.animate.set_color_by_tex("k", ORANGE)) + + +class ExampleScene2(Scene): + def construct(self): + formula = MathTex( + r"P(X=k) = 0.5^k (1-0.5)^{12-k}", + ).scale(1.3) + print(formula.id_to_vgroup_dict) + # formula.id_to_vgroup_dict['unique002'].set_color(RED) + # formula.set_color_by_tex("k", ORANGE) + self.add(formula) + + +class ExampleScene3(Scene): + def construct(self): + formula = MathTex( + r"P(X=k) =", + r"\binom{12}{k}", + r"0.5^{k}", + r"(1-0.5)^{12-k}", + substrings_to_isolate=["k"], + ).scale(1.3) + for k in formula.id_to_vgroup_dict: + print(k) + for key in formula.id_to_vgroup_dict: + if key[-2:] == "ss": + formula.id_to_vgroup_dict[key].set_color(GREEN) + + # formula.id_to_vgroup_dict['unique000ss'].set_color(RED) + # formula.id_to_vgroup_dict['unique001ss'].set_color(GREEN) + # formula.id_to_vgroup_dict['unique002ss'].set_color(BLUE) + # formula.id_to_vgroup_dict['unique003ss'].set_color(YELLOW) + # formula.set_color_by_tex("k", ORANGE) + self.add(formula) diff --git a/manim/mobject/text/tex_mobject.py b/manim/mobject/text/tex_mobject.py index 03bc285e79..00a4b2b335 100644 --- a/manim/mobject/text/tex_mobject.py +++ b/manim/mobject/text/tex_mobject.py @@ -274,10 +274,38 @@ def __init__( self.tex_to_color_map = tex_to_color_map self.tex_environment = tex_environment self.brace_notation_split_occurred = False - self.tex_strings = self._break_up_tex_strings(tex_strings) + self.tex_strings = tex_strings + + def join_tex_strings_with_unique_deliminters( + tex_strings: Iterable[str], substrings_to_isolate: Iterable[str] + ) -> str: + joined_string = "" + for idx, tex_string in enumerate(tex_strings): + string_part = rf"\special{{dvisvgm:raw }}" + print("tex_string: '", tex_string, "'") + for substring in substrings_to_isolate: + pre_string = rf"\special{{dvisvgm:raw }}" + post_string = r"\special{dvisvgm:raw }" + replacement_string = pre_string + substring + post_string + tex_string = tex_string.replace(substring, replacement_string, 1) + string_part += tex_string + string_part += r"\special{dvisvgm:raw }" + # string_part = f"{tex_string} " + joined_string = joined_string + string_part + return joined_string + + # self.tex_strings = self._break_up_tex_strings(tex_strings) try: + joined_string = join_tex_strings_with_unique_deliminters( + self.tex_strings, self.substrings_to_isolate + ) + print("joined_string") + print("'" + joined_string + "'") + # print("self.arg_separator.join(self.tex_strings)") + # print("'" + self.arg_separator.join(self.tex_strings) + "'") super().__init__( - self.arg_separator.join(self.tex_strings), + # self.arg_separator.join(self.tex_strings), + joined_string, tex_environment=self.tex_environment, tex_template=self.tex_template, **kwargs, From 96db1b0ba4c7fe677f9544d2fc644775be2b6200 Mon Sep 17 00:00:00 2001 From: Henrik Skov Midtiby Date: Tue, 16 Dec 2025 08:31:51 +0100 Subject: [PATCH 10/56] ... --- manim/mobject/text/tex_mobject.py | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/manim/mobject/text/tex_mobject.py b/manim/mobject/text/tex_mobject.py index 00a4b2b335..676e01878a 100644 --- a/manim/mobject/text/tex_mobject.py +++ b/manim/mobject/text/tex_mobject.py @@ -280,14 +280,29 @@ def join_tex_strings_with_unique_deliminters( tex_strings: Iterable[str], substrings_to_isolate: Iterable[str] ) -> str: joined_string = "" + ssIdx = 0 for idx, tex_string in enumerate(tex_strings): string_part = rf"\special{{dvisvgm:raw }}" print("tex_string: '", tex_string, "'") for substring in substrings_to_isolate: - pre_string = rf"\special{{dvisvgm:raw }}" - post_string = r"\special{dvisvgm:raw }" - replacement_string = pre_string + substring + post_string - tex_string = tex_string.replace(substring, replacement_string, 1) + remaining_string = tex_string + match = re.match(f"(.*)({substring})(.*)", remaining_string) + if match: + pre_match = match.group(1) + matched_string = match.group(2) + post_match = match.group(3) + pre_string = ( + rf"\special{{dvisvgm:raw }}" + ) + post_string = r"\special{dvisvgm:raw }" + ssIdx += 1 + tex_string = ( + pre_match + + pre_string + + matched_string + + post_string + + post_match + ) string_part += tex_string string_part += r"\special{dvisvgm:raw }" # string_part = f"{tex_string} " From fe411af5a96d0be3b191868d9148e3c0657a32ed Mon Sep 17 00:00:00 2001 From: Henrik Skov Midtiby Date: Tue, 16 Dec 2025 09:57:04 +0100 Subject: [PATCH 11/56] Regular expression can now match more than one object --- manim/mobject/text/tex_mobject.py | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/manim/mobject/text/tex_mobject.py b/manim/mobject/text/tex_mobject.py index 676e01878a..8d0ab4b066 100644 --- a/manim/mobject/text/tex_mobject.py +++ b/manim/mobject/text/tex_mobject.py @@ -281,13 +281,19 @@ def join_tex_strings_with_unique_deliminters( ) -> str: joined_string = "" ssIdx = 0 + matched_strings_and_ids = [] for idx, tex_string in enumerate(tex_strings): string_part = rf"\special{{dvisvgm:raw }}" + matched_strings_and_ids.append((tex_string, f"unique{idx:03d}")) print("tex_string: '", tex_string, "'") + # Repeat replace until stable + processed_string = tex_string for substring in substrings_to_isolate: remaining_string = tex_string - match = re.match(f"(.*)({substring})(.*)", remaining_string) - if match: + processed_string = "" + while match := re.match( + f"(.*?)({substring})(.*)", remaining_string + ): pre_match = match.group(1) matched_string = match.group(2) post_match = match.group(3) @@ -295,18 +301,26 @@ def join_tex_strings_with_unique_deliminters( rf"\special{{dvisvgm:raw }}" ) post_string = r"\special{dvisvgm:raw }" + matched_strings_and_ids.append( + (matched_string, f"unique{ssIdx:03d}") + ) ssIdx += 1 - tex_string = ( - pre_match + processed_string = ( + processed_string + + pre_match + pre_string + matched_string + post_string - + post_match ) - string_part += tex_string + remaining_string = post_match + processed_string = processed_string + remaining_string + + string_part += processed_string string_part += r"\special{dvisvgm:raw }" # string_part = f"{tex_string} " joined_string = joined_string + string_part + print("matched_strings_and_ids") + print(matched_strings_and_ids) return joined_string # self.tex_strings = self._break_up_tex_strings(tex_strings) From 68395dfbfd70e146947047f002776701dd8a20de Mon Sep 17 00:00:00 2001 From: Henrik Skov Midtiby Date: Tue, 16 Dec 2025 10:14:43 +0100 Subject: [PATCH 12/56] Process the string by applying the substrings in the order they match --- manim/mobject/text/tex_mobject.py | 37 +++++++++++++++++++------------ 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/manim/mobject/text/tex_mobject.py b/manim/mobject/text/tex_mobject.py index 8d0ab4b066..f0171a210a 100644 --- a/manim/mobject/text/tex_mobject.py +++ b/manim/mobject/text/tex_mobject.py @@ -286,23 +286,30 @@ def join_tex_strings_with_unique_deliminters( string_part = rf"\special{{dvisvgm:raw }}" matched_strings_and_ids.append((tex_string, f"unique{idx:03d}")) print("tex_string: '", tex_string, "'") - # Repeat replace until stable - processed_string = tex_string - for substring in substrings_to_isolate: - remaining_string = tex_string - processed_string = "" - while match := re.match( - f"(.*?)({substring})(.*)", remaining_string - ): - pre_match = match.group(1) - matched_string = match.group(2) - post_match = match.group(3) + # Try to match with all substrings_to_isolate and apply the first match + # then match again (on the rest of the string) and continue until no + # characters are left in the string + unprocessed_string = tex_string + processed_string = "" + while len(unprocessed_string) > 0: + first_match_start = len(unprocessed_string) + first_match = None + for substring in substrings_to_isolate: + match = re.match(f"(.*?)({substring})(.*)", unprocessed_string) + if match and len(match.group(1)) < first_match_start: + first_match = match + first_match_start = len(match.group(1)) + + if first_match: + pre_match = first_match.group(1) + matched_string = first_match.group(2) + post_match = first_match.group(3) pre_string = ( rf"\special{{dvisvgm:raw }}" ) post_string = r"\special{dvisvgm:raw }" matched_strings_and_ids.append( - (matched_string, f"unique{ssIdx:03d}") + (matched_string, f"unique{ssIdx:03d}ss") ) ssIdx += 1 processed_string = ( @@ -312,8 +319,10 @@ def join_tex_strings_with_unique_deliminters( + matched_string + post_string ) - remaining_string = post_match - processed_string = processed_string + remaining_string + unprocessed_string = post_match + else: + processed_string = processed_string + unprocessed_string + unprocessed_string = "" string_part += processed_string string_part += r"\special{dvisvgm:raw }" From 4499c73ce674c0544d247d3e21b816f25b735cc5 Mon Sep 17 00:00:00 2001 From: Henrik Skov Midtiby Date: Tue, 16 Dec 2025 10:37:49 +0100 Subject: [PATCH 13/56] Code refactoring and added type annotations --- manim/mobject/text/tex_mobject.py | 66 ++++++++++++++++--------------- 1 file changed, 34 insertions(+), 32 deletions(-) diff --git a/manim/mobject/text/tex_mobject.py b/manim/mobject/text/tex_mobject.py index f0171a210a..e0d54d607d 100644 --- a/manim/mobject/text/tex_mobject.py +++ b/manim/mobject/text/tex_mobject.py @@ -275,61 +275,63 @@ def __init__( self.tex_environment = tex_environment self.brace_notation_split_occurred = False self.tex_strings = tex_strings + self.matched_strings_and_ids: list[tuple[str, str]] = [] + + def locate_first_match( + substrings_to_isolate: Iterable[str], unprocessed_string: str + ) -> re.Match | None: + first_match_start = len(unprocessed_string) + first_match = None + for substring in substrings_to_isolate: + match = re.match(f"(.*?)({substring})(.*)", unprocessed_string) + if match and len(match.group(1)) < first_match_start: + first_match = match + first_match_start = len(match.group(1)) + return first_match + + def handle_match(ssIdx: int, first_match: re.Match) -> tuple[str, str]: + pre_match = first_match.group(1) + matched_string = first_match.group(2) + post_match = first_match.group(3) + pre_string = rf"\special{{dvisvgm:raw }}" + post_string = r"\special{dvisvgm:raw }" + self.matched_strings_and_ids.append( + (matched_string, f"unique{ssIdx:03d}ss") + ) + processed_string = pre_match + pre_string + matched_string + post_string + unprocessed_string = post_match + return processed_string, unprocessed_string def join_tex_strings_with_unique_deliminters( tex_strings: Iterable[str], substrings_to_isolate: Iterable[str] ) -> str: joined_string = "" ssIdx = 0 - matched_strings_and_ids = [] for idx, tex_string in enumerate(tex_strings): string_part = rf"\special{{dvisvgm:raw }}" - matched_strings_and_ids.append((tex_string, f"unique{idx:03d}")) - print("tex_string: '", tex_string, "'") + self.matched_strings_and_ids.append((tex_string, f"unique{idx:03d}")) + # Try to match with all substrings_to_isolate and apply the first match # then match again (on the rest of the string) and continue until no # characters are left in the string unprocessed_string = tex_string processed_string = "" while len(unprocessed_string) > 0: - first_match_start = len(unprocessed_string) - first_match = None - for substring in substrings_to_isolate: - match = re.match(f"(.*?)({substring})(.*)", unprocessed_string) - if match and len(match.group(1)) < first_match_start: - first_match = match - first_match_start = len(match.group(1)) + first_match = locate_first_match( + substrings_to_isolate, unprocessed_string + ) if first_match: - pre_match = first_match.group(1) - matched_string = first_match.group(2) - post_match = first_match.group(3) - pre_string = ( - rf"\special{{dvisvgm:raw }}" - ) - post_string = r"\special{dvisvgm:raw }" - matched_strings_and_ids.append( - (matched_string, f"unique{ssIdx:03d}ss") - ) + processed, unprocessed_string = handle_match(ssIdx, first_match) + processed_string = processed_string + processed ssIdx += 1 - processed_string = ( - processed_string - + pre_match - + pre_string - + matched_string - + post_string - ) - unprocessed_string = post_match else: processed_string = processed_string + unprocessed_string unprocessed_string = "" string_part += processed_string string_part += r"\special{dvisvgm:raw }" - # string_part = f"{tex_string} " joined_string = joined_string + string_part - print("matched_strings_and_ids") - print(matched_strings_and_ids) return joined_string # self.tex_strings = self._break_up_tex_strings(tex_strings) @@ -348,7 +350,7 @@ def join_tex_strings_with_unique_deliminters( tex_template=self.tex_template, **kwargs, ) - self._break_up_by_substrings() + # self._break_up_by_substrings() except ValueError as compilation_error: if self.brace_notation_split_occurred: logger.error( From 751c71812a40fa5db2736d7c59b50ea4848100bf Mon Sep 17 00:00:00 2001 From: Henrik Skov Midtiby Date: Tue, 16 Dec 2025 10:59:57 +0100 Subject: [PATCH 14/56] ... --- manim/mobject/text/tex_mobject.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/manim/mobject/text/tex_mobject.py b/manim/mobject/text/tex_mobject.py index e0d54d607d..f4875ff21a 100644 --- a/manim/mobject/text/tex_mobject.py +++ b/manim/mobject/text/tex_mobject.py @@ -281,12 +281,20 @@ def locate_first_match( substrings_to_isolate: Iterable[str], unprocessed_string: str ) -> re.Match | None: first_match_start = len(unprocessed_string) + first_match_length = 0 first_match = None for substring in substrings_to_isolate: match = re.match(f"(.*?)({substring})(.*)", unprocessed_string) if match and len(match.group(1)) < first_match_start: first_match = match first_match_start = len(match.group(1)) + first_match_length = len(match.group(2)) + elif match and len(match.group(1)) == first_match_start: + # Break ties by looking at length of matches. + if first_match_length < len(match.group(2)): + first_match = match + first_match_start = len(match.group(1)) + first_match_length = len(match.group(2)) return first_match def handle_match(ssIdx: int, first_match: re.Match) -> tuple[str, str]: @@ -339,8 +347,6 @@ def join_tex_strings_with_unique_deliminters( joined_string = join_tex_strings_with_unique_deliminters( self.tex_strings, self.substrings_to_isolate ) - print("joined_string") - print("'" + joined_string + "'") # print("self.arg_separator.join(self.tex_strings)") # print("'" + self.arg_separator.join(self.tex_strings) + "'") super().__init__( From 73a08e4a8e2a0ff1b92d458df8e0a86868ff44d1 Mon Sep 17 00:00:00 2001 From: Henrik Skov Midtiby Date: Tue, 16 Dec 2025 11:03:16 +0100 Subject: [PATCH 15/56] Added a lot of test cases --- issue/MathTexExamples.py | 159 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 issue/MathTexExamples.py diff --git a/issue/MathTexExamples.py b/issue/MathTexExamples.py new file mode 100644 index 0000000000..bed86750e6 --- /dev/null +++ b/issue/MathTexExamples.py @@ -0,0 +1,159 @@ +from __future__ import annotations + +from manim import * + + +class ExampleScene2(Scene): + def construct(self): + formula = MathTex( + r"P(X=k) = 0.5^k (1-0.5)^{12-k}", + ).scale(1.3) + print(formula.id_to_vgroup_dict.keys()) + # formula.id_to_vgroup_dict['unique002'].set_color(RED) + # formula.set_color_by_tex("k", ORANGE) + self.add(formula) + + +class ExampleScene3(Scene): + def construct(self): + formula = MathTex( + r"P(X=k) =", + r"\binom{12}{k}", + r"0.5^{k}", + r"(1-0.5)^{12-k}", + substrings_to_isolate=["k"], + ).scale(1.3) + for k in formula.id_to_vgroup_dict: + print(k) + for key in formula.id_to_vgroup_dict: + if key[-2:] == "ss": + formula.id_to_vgroup_dict[key].set_color(GREEN) + + # formula.id_to_vgroup_dict['unique000ss'].set_color(RED) + # formula.id_to_vgroup_dict['unique001ss'].set_color(GREEN) + # formula.id_to_vgroup_dict['unique002ss'].set_color(BLUE) + # formula.id_to_vgroup_dict['unique003ss'].set_color(YELLOW) + # formula.set_color_by_tex("k", ORANGE) + self.add(formula) + + +class ExampleScene4a(Scene): + def construct(self): + formula = MathTex( + r"a^2 + b^2 = c^2 + a^2", + substrings_to_isolate=["a", "b"], + ).scale(1.3) + for k in formula.id_to_vgroup_dict: + print(k) + for key in formula.id_to_vgroup_dict: + if key[-2:] == "ss": + formula.id_to_vgroup_dict[key].set_color(GREEN) + + self.add(formula) + + +class ExampleScene4b(Scene): + def construct(self): + formula = MathTex( + r"a^2 + b^2 = c^2 + a^2", + substrings_to_isolate=["c", "a"], + ).scale(1.3) + print("Hejsa") + for k in formula.id_to_vgroup_dict: + print(k) + for key in formula.id_to_vgroup_dict: + if key[-2:] == "ss": + formula.id_to_vgroup_dict[key].set_color(GREEN) + + self.add(formula) + + +class ExampleScene5(Scene): + def construct(self): + formula = MathTex( + r"a^2 + b^2 = c^2 + d^2 - a^2", + substrings_to_isolate=["[acd]"], + ).scale(1.3) + for k in formula.id_to_vgroup_dict: + print(k) + for key in formula.id_to_vgroup_dict: + if key[-2:] == "ss": + formula.id_to_vgroup_dict[key].set_color(GREEN) + + self.add(formula) + + +# TODO: +# When all scenes are rendered with a single command line call +# uv run manim render MathTexExamples.py --write_all +# ExampleScene6 fails with the following error +# KeyError: 'unique001ss' +# I think it is related to a caching issue, because the error vanishes +# when the scene is rendered by itself. +# uv run manim render MathTexExamples.py ExampleScene6 +class ExampleScene6(Scene): + def construct(self): + formula = MathTex( + r"a^2 + b^2 = c^2 + d^2 - a^2", + substrings_to_isolate=["[acd]"], + ).scale(1.3) + + for k in formula.id_to_vgroup_dict: + print(k) + + def set_color_by_tex(mathtex, tex, color): + print(mathtex.matched_strings_and_ids) + for match in mathtex.matched_strings_and_ids: + if match[0] == tex: + mathtex.id_to_vgroup_dict[match[1]].set_color(color) + + set_color_by_tex(formula, "c", ORANGE) + set_color_by_tex(formula, "a", RED) + + self.add(formula) + + +class ExampleScene7(Scene): + def construct(self): + formula = MathTex( + r"a^2 + b^2 = c^2 + d^2 - 2 a^2", + substrings_to_isolate=["[acd]"], + ).scale(1.3) + + for k in formula.id_to_vgroup_dict: + print(k) + + def set_color_by_tex(mathtex, tex, color): + print(mathtex.matched_strings_and_ids) + for match in mathtex.matched_strings_and_ids: + if match[0] == tex: + mathtex.id_to_vgroup_dict[match[1]].set_color(color) + + set_color_by_tex(formula, "c", GREEN) + set_color_by_tex(formula, "a", RED) + + self.add(formula) + + +class ExampleScene8(Scene): + def construct(self): + formula = MathTex( + r"P(X=k) =", + r"\binom{12}{k}", + r"0.5^{k}", + r"(1-0.5)^{12-k}", + substrings_to_isolate=["k", "1", "12", "0.5"], + ).scale(1.3) + + def set_color_by_tex( + mathtex: MathTex, tex: str, color: ParsableManimColor + ) -> None: + for match in mathtex.matched_strings_and_ids: + if match[0] == tex: + mathtex.id_to_vgroup_dict[match[1]].set_color(color) + + set_color_by_tex(formula, "k", GREEN) + set_color_by_tex(formula, "12", RED) + set_color_by_tex(formula, "1", YELLOW) + set_color_by_tex(formula, "0.5", BLUE_D) + self.add(formula) From b9e313e028cb3f9349299b667cb3d8214776b295 Mon Sep 17 00:00:00 2001 From: Henrik Skov Midtiby Date: Tue, 16 Dec 2025 14:34:05 +0100 Subject: [PATCH 16/56] More examples --- issue/MathTexExamples.py | 60 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/issue/MathTexExamples.py b/issue/MathTexExamples.py index bed86750e6..3cd6d3d6e6 100644 --- a/issue/MathTexExamples.py +++ b/issue/MathTexExamples.py @@ -157,3 +157,63 @@ def set_color_by_tex( set_color_by_tex(formula, "1", YELLOW) set_color_by_tex(formula, "0.5", BLUE_D) self.add(formula) + + +class ExampleScene9(Scene): + def construct(self): + t2cm = {r"\sum": BLUE, "^{n}": RED, "_{1}": GREEN, "x": YELLOW} + eq1 = MathTex(r"\sum", "^{n}", "_{1}", "x").scale(1.3) + eq2 = MathTex(r"\sum", "_{1}", "^{n}", "x").scale(1.3) + + def set_color_by_tex( + mathtex: MathTex, tex: str, color: ParsableManimColor + ) -> None: + for match in mathtex.matched_strings_and_ids: + if match[0] == tex: + mathtex.id_to_vgroup_dict[match[1]].set_color(color) + + for k, v in t2cm.items(): + set_color_by_tex(eq1, k, v) + set_color_by_tex(eq2, k, v) + + grp = VGroup(eq1, eq2).arrange_in_grid(2, 1) + self.add(grp) + + +class ExampleScene10(Scene): + def construct(self): + # TODO: This approach to highlighting \sum does not work right now. + # It changes the shape of the rendered equation. + t2cm1 = {r"\\sum": BLUE, "n": RED, "1": GREEN, "x": YELLOW} + t2cm2 = {r"\sum": BLUE, "n": RED, "1": GREEN, "x": YELLOW} + eq1 = MathTex( + r"\sum^{n}_{1} x", substrings_to_isolate=list(t2cm1.keys()) + ).scale(1.3) + eq2 = MathTex( + r"\sum_{1}^{n} x", substrings_to_isolate=list(t2cm2.keys()) + ).scale(1.3) + + def set_color_by_tex( + mathtex: MathTex, tex: str, color: ParsableManimColor + ) -> None: + for match in mathtex.matched_strings_and_ids: + if match[0] == tex: + mathtex.id_to_vgroup_dict[match[1]].set_color(color) + + for k, v in t2cm1.items(): + set_color_by_tex(eq1, k, v) + for k, v in t2cm2.items(): + set_color_by_tex(eq2, k, v) + + grp = VGroup(eq1, eq2).arrange_in_grid(2, 1) + self.add(grp) + + # This workaround based on index_labels still work + # labels = index_labels(eq2) + # self.add(labels) + # eq1[0].set_color(BLUE) + # eq2[1].set_color(BLUE) + + +# Get inspiration from +# https://docs.manim.community/en/stable/guides/using_text.html#text-with-latex From 63a4c1927fbdffe0a2b5283ef008498fb433af3f Mon Sep 17 00:00:00 2001 From: Henrik Skov Midtiby Date: Wed, 17 Dec 2025 08:43:57 +0100 Subject: [PATCH 17/56] More examples --- issue/MathTexExamples.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/issue/MathTexExamples.py b/issue/MathTexExamples.py index 3cd6d3d6e6..4afbddb9f3 100644 --- a/issue/MathTexExamples.py +++ b/issue/MathTexExamples.py @@ -215,5 +215,25 @@ def set_color_by_tex( # eq2[1].set_color(BLUE) +class ExampleScene11(Scene): + def construct(self): + t2cm = {"n": RED, "1": GREEN, "x": YELLOW} + eq = MathTex(r"\sum_{1}^{n} x", tex_to_color_map=t2cm).scale(1.3) + + self.add(eq) + + +class ExampleScene12(Scene): + def construct(self): + eq = MathTex(r"\sum_{1}^{n} x", substrings_to_isolate=["1", "n", "x"]).scale( + 1.3 + ) + eq.set_color_by_tex("1", YELLOW) + eq.set_color_by_tex("x", RED) + eq.set_opacity_by_tex("n", 0.5) + + self.add(eq) + + # Get inspiration from # https://docs.manim.community/en/stable/guides/using_text.html#text-with-latex From a651d954676d2f20cd88e613e6212137d9080baa Mon Sep 17 00:00:00 2001 From: Henrik Skov Midtiby Date: Wed, 17 Dec 2025 08:48:51 +0100 Subject: [PATCH 18/56] Use matched_strings_and_ids to simplify existing methods --- manim/mobject/text/tex_mobject.py | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/manim/mobject/text/tex_mobject.py b/manim/mobject/text/tex_mobject.py index f4875ff21a..bb7df6982f 100644 --- a/manim/mobject/text/tex_mobject.py +++ b/manim/mobject/text/tex_mobject.py @@ -12,7 +12,7 @@ from __future__ import annotations -from manim.utils.color import BLACK, ManimColor, ParsableManimColor +from manim.utils.color import BLACK, ParsableManimColor __all__ = [ "SingleStringMathTex", @@ -266,12 +266,13 @@ def __init__( self.tex_template = kwargs.pop("tex_template", config["tex_template"]) self.arg_separator = arg_separator self.substrings_to_isolate = ( - [] if substrings_to_isolate is None else substrings_to_isolate + [] if substrings_to_isolate is None else list(substrings_to_isolate) ) if tex_to_color_map is None: self.tex_to_color_map: dict[str, ParsableManimColor] = {} else: self.tex_to_color_map = tex_to_color_map + self.substrings_to_isolate.extend(self.tex_to_color_map.keys()) self.tex_environment = tex_environment self.brace_notation_split_occurred = False self.tex_strings = tex_strings @@ -457,9 +458,9 @@ def get_part_by_tex(self, tex: str, **kwargs: Any) -> MathTex | None: def set_color_by_tex( self, tex: str, color: ParsableManimColor, **kwargs: Any ) -> Self: - parts_to_color = self.get_parts_by_tex(tex, **kwargs) - for part in parts_to_color: - part.set_color(color) + for tex_str, match_id in self.matched_strings_and_ids: + if tex_str == tex: + self.id_to_vgroup_dict[match_id].set_color(color) return self def set_opacity_by_tex( @@ -485,22 +486,18 @@ def set_opacity_by_tex( """ if remaining_opacity is not None: self.set_opacity(opacity=remaining_opacity) - for part in self.get_parts_by_tex(tex): - part.set_opacity(opacity) + for tex_str, match_id in self.matched_strings_and_ids: + if tex_str == tex: + self.id_to_vgroup_dict[match_id].set_opacity(opacity) return self def set_color_by_tex_to_color_map( self, texs_to_color_map: dict[str, ParsableManimColor], **kwargs: Any ) -> Self: for texs, color in list(texs_to_color_map.items()): - try: - # If the given key behaves like tex_strings - texs + "" - self.set_color_by_tex(texs, ManimColor(color), **kwargs) - except TypeError: - # If the given key is a tuple - for tex in texs: - self.set_color_by_tex(tex, ManimColor(color), **kwargs) + for match in self.matched_strings_and_ids: + if match[0] == texs: + self.id_to_vgroup_dict[match[1]].set_color(color) return self def index_of_part(self, part: MathTex) -> int: From b8c912306f4425eeed548349412ea8536b0ce007 Mon Sep 17 00:00:00 2001 From: Henrik Skov Midtiby Date: Wed, 17 Dec 2025 08:50:34 +0100 Subject: [PATCH 19/56] Remove unused code --- manim/mobject/text/tex_mobject.py | 87 +------------------------------ 1 file changed, 1 insertion(+), 86 deletions(-) diff --git a/manim/mobject/text/tex_mobject.py b/manim/mobject/text/tex_mobject.py index bb7df6982f..5503b25a27 100644 --- a/manim/mobject/text/tex_mobject.py +++ b/manim/mobject/text/tex_mobject.py @@ -23,10 +23,9 @@ ] -import itertools as it import operator as op import re -from collections.abc import Iterable, Sequence +from collections.abc import Iterable from functools import reduce from textwrap import dedent from typing import Any @@ -343,21 +342,16 @@ def join_tex_strings_with_unique_deliminters( joined_string = joined_string + string_part return joined_string - # self.tex_strings = self._break_up_tex_strings(tex_strings) try: joined_string = join_tex_strings_with_unique_deliminters( self.tex_strings, self.substrings_to_isolate ) - # print("self.arg_separator.join(self.tex_strings)") - # print("'" + self.arg_separator.join(self.tex_strings) + "'") super().__init__( - # self.arg_separator.join(self.tex_strings), joined_string, tex_environment=self.tex_environment, tex_template=self.tex_template, **kwargs, ) - # self._break_up_by_substrings() except ValueError as compilation_error: if self.brace_notation_split_occurred: logger.error( @@ -378,79 +372,6 @@ def join_tex_strings_with_unique_deliminters( if self.organize_left_to_right: self._organize_submobjects_left_to_right() - def _break_up_tex_strings(self, tex_strings: Sequence[str]) -> list[str]: - # Separate out anything surrounded in double braces - pre_split_length = len(tex_strings) - tex_strings_brace_splitted = [ - re.split("{{(.*?)}}", str(t)) for t in tex_strings - ] - tex_strings_combined = sum(tex_strings_brace_splitted, []) - if len(tex_strings_combined) > pre_split_length: - self.brace_notation_split_occurred = True - - # Separate out any strings specified in the isolate - # or tex_to_color_map lists. - patterns = [] - patterns.extend( - [ - f"({re.escape(ss)})" - for ss in it.chain( - self.substrings_to_isolate, - self.tex_to_color_map.keys(), - ) - ], - ) - pattern = "|".join(patterns) - if pattern: - pieces = [] - for s in tex_strings_combined: - pieces.extend(re.split(pattern, s)) - else: - pieces = tex_strings_combined - return [p for p in pieces if p] - - def _break_up_by_substrings(self) -> Self: - """ - Reorganize existing submobjects one layer - deeper based on the structure of tex_strings (as a list - of tex_strings) - """ - new_submobjects: list[VMobject] = [] - curr_index = 0 - for tex_string in self.tex_strings: - sub_tex_mob = SingleStringMathTex( - tex_string, - tex_environment=self.tex_environment, - tex_template=self.tex_template, - ) - num_submobs = len(sub_tex_mob.submobjects) - 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) - else: - sub_tex_mob.submobjects = self.submobjects[curr_index:new_index] - new_submobjects.append(sub_tex_mob) - curr_index = new_index - self.submobjects = new_submobjects - return self - - def get_parts_by_tex( - self, tex: str, substring: bool = True, case_sensitive: bool = True - ) -> VGroup: - def test(tex1: str, tex2: str) -> bool: - if not case_sensitive: - tex1 = tex1.lower() - tex2 = tex2.lower() - if substring: - return tex1 in tex2 - else: - return tex1 == tex2 - - return VGroup(*(m for m in self.submobjects if test(tex, m.get_tex_string()))) - def get_part_by_tex(self, tex: str, **kwargs: Any) -> MathTex | None: all_parts = self.get_parts_by_tex(tex, **kwargs) return all_parts[0] if all_parts else None @@ -506,12 +427,6 @@ def index_of_part(self, part: MathTex) -> int: raise ValueError("Trying to get index of part not in MathTex") return split_self.index(part) - def index_of_part_by_tex(self, tex: str, **kwargs: Any) -> int: - part = self.get_part_by_tex(tex, **kwargs) - if part is None: - return -1 - return self.index_of_part(part) - def sort_alphabetically(self) -> None: self.submobjects.sort(key=lambda m: m.get_tex_string()) From 86a1b466b30d3b21f282cd590027198f6e059d41 Mon Sep 17 00:00:00 2001 From: Henrik Skov Midtiby Date: Wed, 17 Dec 2025 08:51:59 +0100 Subject: [PATCH 20/56] Update get_part_by_tex to use matched_strings_and_ids --- manim/mobject/text/tex_mobject.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/manim/mobject/text/tex_mobject.py b/manim/mobject/text/tex_mobject.py index 5503b25a27..dff7b91b86 100644 --- a/manim/mobject/text/tex_mobject.py +++ b/manim/mobject/text/tex_mobject.py @@ -372,9 +372,11 @@ def join_tex_strings_with_unique_deliminters( if self.organize_left_to_right: self._organize_submobjects_left_to_right() - def get_part_by_tex(self, tex: str, **kwargs: Any) -> MathTex | None: - all_parts = self.get_parts_by_tex(tex, **kwargs) - return all_parts[0] if all_parts else None + def get_part_by_tex(self, tex: str, **kwargs: Any) -> VGroup | None: + for tex_str, match_id in self.matched_strings_and_ids: + if tex_str == tex: + return self.id_to_vgroup_dict[match_id] + return None def set_color_by_tex( self, tex: str, color: ParsableManimColor, **kwargs: Any From 1c2d11309d2e43e30f82a16f27b92c2b18625f89 Mon Sep 17 00:00:00 2001 From: Henrik Skov Midtiby Date: Wed, 17 Dec 2025 08:58:14 +0100 Subject: [PATCH 21/56] This is required for test_MathTable to pass --- issue/MathTexExamples.py | 9 +++++++++ manim/mobject/text/tex_mobject.py | 5 ++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/issue/MathTexExamples.py b/issue/MathTexExamples.py index 4afbddb9f3..f7f6a87e93 100644 --- a/issue/MathTexExamples.py +++ b/issue/MathTexExamples.py @@ -235,5 +235,14 @@ def construct(self): self.add(eq) +class ExampleScene13(Scene): + def construct(self): + matrix_elements = [[1, 2, 3]] + row = 0 + column = 2 + matrix = Matrix(matrix_elements) + print(matrix.get_columns()[column][row].tex_string) + + # Get inspiration from # https://docs.manim.community/en/stable/guides/using_text.html#text-with-latex diff --git a/manim/mobject/text/tex_mobject.py b/manim/mobject/text/tex_mobject.py index dff7b91b86..bad99b1cfe 100644 --- a/manim/mobject/text/tex_mobject.py +++ b/manim/mobject/text/tex_mobject.py @@ -322,7 +322,7 @@ def join_tex_strings_with_unique_deliminters( # Try to match with all substrings_to_isolate and apply the first match # then match again (on the rest of the string) and continue until no # characters are left in the string - unprocessed_string = tex_string + unprocessed_string = str(tex_string) processed_string = "" while len(unprocessed_string) > 0: first_match = locate_first_match( @@ -352,6 +352,9 @@ def join_tex_strings_with_unique_deliminters( tex_template=self.tex_template, **kwargs, ) + self.tex_string = self.arg_separator.join( + [str(s) for s in self.tex_strings] + ) except ValueError as compilation_error: if self.brace_notation_split_occurred: logger.error( From d9bc03f359e58678b3af4c758988f84b4ab811b8 Mon Sep 17 00:00:00 2001 From: Henrik Skov Midtiby Date: Wed, 17 Dec 2025 09:31:39 +0100 Subject: [PATCH 22/56] Ensure that self.texstring is set. --- issue/MathTexExamples.py | 9 +++++++++ manim/animation/transform_matching_parts.py | 7 ++++++- manim/mobject/text/tex_mobject.py | 1 + 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/issue/MathTexExamples.py b/issue/MathTexExamples.py index f7f6a87e93..7db451bfc7 100644 --- a/issue/MathTexExamples.py +++ b/issue/MathTexExamples.py @@ -244,5 +244,14 @@ def construct(self): print(matrix.get_columns()[column][row].tex_string) +class ExampleScene14(Scene): + def construct(self): + start = MathTex("A", r"\to", "B") + end = MathTex("B", r"\to", "A") + + self.add(start) + self.play(TransformMatchingTex(start, end, fade_transform_mismatches=True)) + + # Get inspiration from # https://docs.manim.community/en/stable/guides/using_text.html#text-with-latex diff --git a/manim/animation/transform_matching_parts.py b/manim/animation/transform_matching_parts.py index 03305201f1..74093f128f 100644 --- a/manim/animation/transform_matching_parts.py +++ b/manim/animation/transform_matching_parts.py @@ -294,4 +294,9 @@ def get_mobject_parts(mobject: Mobject) -> list[Mobject]: @staticmethod def get_mobject_key(mobject: Mobject) -> str: - return mobject.tex_string + # Ugly hack to make the following test pass + # test_TransformMatchingTex_FadeTransformMismatches_NothingToFade + try: + return mobject.tex_string + except Exception: + return "" diff --git a/manim/mobject/text/tex_mobject.py b/manim/mobject/text/tex_mobject.py index bad99b1cfe..5c66522d2f 100644 --- a/manim/mobject/text/tex_mobject.py +++ b/manim/mobject/text/tex_mobject.py @@ -262,6 +262,7 @@ def __init__( tex_environment: str | None = "align*", **kwargs: Any, ): + self.texstring = "" self.tex_template = kwargs.pop("tex_template", config["tex_template"]) self.arg_separator = arg_separator self.substrings_to_isolate = ( From d12bde7a3b4494e495a3b2e5da3f3a9888530d67 Mon Sep 17 00:00:00 2001 From: Henrik Skov Midtiby Date: Thu, 18 Dec 2025 11:22:37 +0100 Subject: [PATCH 23/56] Added more examples from exising issues in the github repo --- issue/MathTexExamples.py | 281 ++++++++++++++++++++++++++++++++++----- 1 file changed, 247 insertions(+), 34 deletions(-) diff --git a/issue/MathTexExamples.py b/issue/MathTexExamples.py index 7db451bfc7..38813c1986 100644 --- a/issue/MathTexExamples.py +++ b/issue/MathTexExamples.py @@ -58,7 +58,6 @@ def construct(self): r"a^2 + b^2 = c^2 + a^2", substrings_to_isolate=["c", "a"], ).scale(1.3) - print("Hejsa") for k in formula.id_to_vgroup_dict: print(k) for key in formula.id_to_vgroup_dict: @@ -98,17 +97,8 @@ def construct(self): substrings_to_isolate=["[acd]"], ).scale(1.3) - for k in formula.id_to_vgroup_dict: - print(k) - - def set_color_by_tex(mathtex, tex, color): - print(mathtex.matched_strings_and_ids) - for match in mathtex.matched_strings_and_ids: - if match[0] == tex: - mathtex.id_to_vgroup_dict[match[1]].set_color(color) - - set_color_by_tex(formula, "c", ORANGE) - set_color_by_tex(formula, "a", RED) + formula.set_color_by_tex("c", ORANGE) + formula.set_color_by_tex("a", RED) self.add(formula) @@ -120,22 +110,19 @@ def construct(self): substrings_to_isolate=["[acd]"], ).scale(1.3) - for k in formula.id_to_vgroup_dict: - print(k) - - def set_color_by_tex(mathtex, tex, color): - print(mathtex.matched_strings_and_ids) - for match in mathtex.matched_strings_and_ids: - if match[0] == tex: - mathtex.id_to_vgroup_dict[match[1]].set_color(color) - - set_color_by_tex(formula, "c", GREEN) - set_color_by_tex(formula, "a", RED) + formula.set_color_by_tex("c", GREEN) + formula.set_color_by_tex("a", RED) self.add(formula) class ExampleScene8(Scene): + """ + Example based on this issue: + set_color_by_tex selects wrong substring in certain contexts + https://github.com/ManimCommunity/manim/issues/3492 + """ + def construct(self): formula = MathTex( r"P(X=k) =", @@ -145,17 +132,10 @@ def construct(self): substrings_to_isolate=["k", "1", "12", "0.5"], ).scale(1.3) - def set_color_by_tex( - mathtex: MathTex, tex: str, color: ParsableManimColor - ) -> None: - for match in mathtex.matched_strings_and_ids: - if match[0] == tex: - mathtex.id_to_vgroup_dict[match[1]].set_color(color) - - set_color_by_tex(formula, "k", GREEN) - set_color_by_tex(formula, "12", RED) - set_color_by_tex(formula, "1", YELLOW) - set_color_by_tex(formula, "0.5", BLUE_D) + formula.set_color_by_tex("k", GREEN) + formula.set_color_by_tex("12", RED) + formula.set_color_by_tex("1", YELLOW) + formula.set_color_by_tex("0.5", BLUE_D) self.add(formula) @@ -253,5 +233,238 @@ def construct(self): self.play(TransformMatchingTex(start, end, fade_transform_mismatches=True)) +class ExampleScene15(Scene): + """ + Example taken from + TeX splitting can cause the last parts of an equation to not be displayed + https://github.com/ManimCommunity/manim/issues/2970 + + This example seems to work well. + """ + + def construct(self): + template = TexTemplate() + template.add_to_preamble(r"""\usepackage[english]{babel} + \usepackage{csquotes}\usepackage{cancel}""") + lpc_implies_polynomial = ( + MathTex( + r"s[t] = a_1 s[t-1] + a_2 s[t-2] + a_3 s[t-3] + \dots\\", + r"{{\Downarrow}}\\", + r"\text{Polynomial function}", + tex_template=template, + tex_environment="gather*", + ) + .scale(0.9) + .shift(UP * 1.5) + ) + lpc_implies_not_polynomial = ( + MathTex( + r"s[t] = a_1 s[t-1] + a_2 s[t-2] + a_3 s[t-3] + \dots\\", + r"\xcancel{ {{\Downarrow}} }\\", + r"\text{Polynomial function}", + tex_template=template, + tex_environment="gather*", + ) + .scale(0.9) + .shift(DOWN * 1.5) + ) + self.add(lpc_implies_not_polynomial, lpc_implies_polynomial) + + +class ExampleScene16(Scene): + """ + LaTeX rendering incorrectly + https://github.com/ManimCommunity/manim/issues/2912 + + Problem is still present. + I think it is related to the fraction line being a line object in the + svg and not an object defined by bezier curves. + """ + + def construct(self): + preamble = r""" + %\usepackage[mathrm=sym]{unicode-math} + %\setmathfont{Fira Math} + """ + + template = TexTemplate( + preamble=preamble, tex_compiler="lualatex", output_format=".pdf" + ) + + self.add(Tex(r"$\frac{a}{b}$", tex_template=template)) + + +class ExampleScene17(Scene): + """ + Tex splitting issue with frac numerator + https://github.com/ManimCommunity/manim/issues/2884 + + Problem is not solved at the moment. + Please examine TransformMatchingTex::get_mobject_key() to handle this. + """ + + def construct(self): + n = MathTex("n").shift(LEFT) + denominator = MathTex(r"\frac{ 2 }{ n + 1 }", substrings_to_isolate="n").shift( + 2 * UP + ) + self.add(n) + self.wait(2) + # This works + self.play(TransformMatchingTex(n, denominator)) + self.wait(2) + + # This does not work + numerator = MathTex(r"\frac{ n + 1 }{ 2 }", substrings_to_isolate="n").shift( + 2 * DOWN + ) + self.play(TransformMatchingTex(n, numerator)) + self.wait(2) + + +class ExampleScene18(Scene): + """ + Transforming to similar MathTex object distorts sometimes + https://github.com/ManimCommunity/manim/issues/2544 + + Seems to work ok. + """ + + def construct(self): + var = "a" + a1 = MathTex("2").shift(UP) + a2 = MathTex(var) + a3 = MathTex(var).shift(DOWN) + + self.play(Write(a1)) + self.wait() + self.play(Transform(a1, a2)) + self.wait() + self.play(Transform(a1, a3)) + self.wait() + + +class ExampleScene19(Scene): + """ + Docstring for ExampleScene19 + Manim's unexpected colouring behaviour under the radical sign + https://github.com/ManimCommunity/manim/issues/1996 + + The code does not run at the moment. + AttributeError: VMobjectFromSVGPath object has no attribute 'tex_string' + """ + + def construct(self): + val_a = 3 + val_b = 2 + color_a = "#0470cf" + color_b = "#cf0492" + tex_a = Tex(str(val_a).format(int), color=color_a).move_to([-1, 2, 0]).scale(2) + tex_b = Tex(str(val_b).format(int), color=color_b).move_to([1, 2, 0]).scale(2) + + self.play(FadeIn(tex_a, tex_b)) + self.wait() + + # using \over because \frac fails here + form = MathTex( + r"\hat{u}= { 3 \hat{i}+ 2\hat{j} \over{ \sqrt{ {2^{2}+3^{2} } } } }", + substrings_to_isolate=["2", "3"], + ).scale(1.25) + + # This line triggers the error + idx_a = np.array( + [ + i + for i, character in enumerate(form) + if character.get_tex_string() == tex_a[0].get_tex_string() + ], + dtype="int", + ) + idx_b = np.array( + [ + i + for i, character in enumerate(form) + if character.get_tex_string() == tex_b[0].get_tex_string() + ], + dtype="int", + ) + + get_pos_a = form[idx_a[0]].get_center() + get_pos_b = form[idx_b[0]].get_center() + + # using the set_color methods of MathTex object + form.set_color_by_tex(str(val_a).format(int), color_a) + b1_copy = form[idx_b[0]].set_color(color_b) + print(b1_copy) + + self.play(FadeIn(form)) + self.play(tex_a.animate.move_to(get_pos_a).match_height(form[idx_a[0]])) + self.play(tex_b.animate.move_to(get_pos_b).match_height(form[idx_b[0]])) + self.wait(1) + + +class ExampleScene20(Scene): + """ + LaTex Error in combination of MathTex and SurroundingRectangle + https://github.com/ManimCommunity/manim/issues/1907 + + The example seems to be working fine. + """ + + def construct(self): + if False: # try to write ... = \frac { u'v - uv' } {v^2} + # for boxes: └─3─┘ └─5─┘ + + text = MathTex( + r"\left( \frac{u}{v} \right)'", # 0 + "=", # 1 + r"\frac {", # 2 ⇐ error-stop at opening brace + "u' v", # 3 * + "-", # 4 + "u v'", # 5 * + "} {v^2}", # 6 ⇐ closing brace + ) # ⇑ ⇐ is here! + + else: # instead use unwanted ... = { u'v - uv' } \frac {1} {v^2} + # for boxes: └─3─┘ └─5─┘ + + text = MathTex( + r"\left( \frac{u}{v} \right)'", # 0 + "=", # 1 + r"\left( \, ", # 2 + "u' v", # 3 * + "-", # 4 + "u v'", # 5 * + r"\, \right) \frac{1}{v^2}", # 6 + ) + + box1 = SurroundingRectangle(text[3], buff=0.1) + box2 = SurroundingRectangle(text[5], buff=0.1, color=RED) + + self.play(Write(text)) + self.play(Create(box1)) + self.play(TransformFromCopy(box1, box2)) + + self.wait(5) + + +class ExampleScene21(Scene): + """ + LaTeX compilation error when breaking up a MathTex string by subscripts + https://github.com/ManimCommunity/manim/issues/1865 + + If I add {} around each subscript, the code works as expected. + """ + + def construct(self): + # Original string which fails to compile + # reqeq2 = MathTex(r"Y_{ij} = \mu_i + \gamma_i X_{ij} + e_{ij}", substrings_to_isolate = ["i"]) + reqeq2 = MathTex( + r"Y_{ij} = \mu_{i} + \gamma_{i} X_{ij} + e_{ij}", + substrings_to_isolate=["i"], + ) + self.add(reqeq2) + + # Get inspiration from # https://docs.manim.community/en/stable/guides/using_text.html#text-with-latex From 63f786fad5873322c1d90bc518015347e28ad3e9 Mon Sep 17 00:00:00 2001 From: Henrik Skov Midtiby Date: Thu, 18 Dec 2025 11:26:29 +0100 Subject: [PATCH 24/56] Ensure that latex groups are maintained by adding an additional pair of curly braces around the extracted part --- issue/MathTexExamples.py | 2 +- manim/mobject/text/tex_mobject.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/issue/MathTexExamples.py b/issue/MathTexExamples.py index 38813c1986..60c4a4c279 100644 --- a/issue/MathTexExamples.py +++ b/issue/MathTexExamples.py @@ -460,7 +460,7 @@ def construct(self): # Original string which fails to compile # reqeq2 = MathTex(r"Y_{ij} = \mu_i + \gamma_i X_{ij} + e_{ij}", substrings_to_isolate = ["i"]) reqeq2 = MathTex( - r"Y_{ij} = \mu_{i} + \gamma_{i} X_{ij} + e_{ij}", + r"Y_{ij} = \mu_i + \gamma_i X_{ij} + e_{ij}", substrings_to_isolate=["i"], ) self.add(reqeq2) diff --git a/manim/mobject/text/tex_mobject.py b/manim/mobject/text/tex_mobject.py index 5c66522d2f..c252990038 100644 --- a/manim/mobject/text/tex_mobject.py +++ b/manim/mobject/text/tex_mobject.py @@ -302,8 +302,8 @@ def handle_match(ssIdx: int, first_match: re.Match) -> tuple[str, str]: pre_match = first_match.group(1) matched_string = first_match.group(2) post_match = first_match.group(3) - pre_string = rf"\special{{dvisvgm:raw }}" - post_string = r"\special{dvisvgm:raw }" + pre_string = "{" + rf"\special{{dvisvgm:raw }}" + post_string = r"\special{dvisvgm:raw }}" self.matched_strings_and_ids.append( (matched_string, f"unique{ssIdx:03d}ss") ) From d8a4b4348051312e18c15711e6970e51bf06f761 Mon Sep 17 00:00:00 2001 From: Henrik Skov Midtiby Date: Fri, 19 Dec 2025 15:36:28 +0100 Subject: [PATCH 25/56] ExampleScene -> Scene --- issue/MathTexExamples.py | 138 ++++++++++++++++++++++----------------- 1 file changed, 78 insertions(+), 60 deletions(-) diff --git a/issue/MathTexExamples.py b/issue/MathTexExamples.py index 60c4a4c279..5d1276dd31 100644 --- a/issue/MathTexExamples.py +++ b/issue/MathTexExamples.py @@ -3,7 +3,7 @@ from manim import * -class ExampleScene2(Scene): +class Scene2(Scene): def construct(self): formula = MathTex( r"P(X=k) = 0.5^k (1-0.5)^{12-k}", @@ -14,7 +14,7 @@ def construct(self): self.add(formula) -class ExampleScene3(Scene): +class Scene3(Scene): def construct(self): formula = MathTex( r"P(X=k) =", @@ -37,7 +37,7 @@ def construct(self): self.add(formula) -class ExampleScene4a(Scene): +class Scene4a(Scene): def construct(self): formula = MathTex( r"a^2 + b^2 = c^2 + a^2", @@ -52,7 +52,7 @@ def construct(self): self.add(formula) -class ExampleScene4b(Scene): +class Scene4b(Scene): def construct(self): formula = MathTex( r"a^2 + b^2 = c^2 + a^2", @@ -67,7 +67,7 @@ def construct(self): self.add(formula) -class ExampleScene5(Scene): +class Scene5(Scene): def construct(self): formula = MathTex( r"a^2 + b^2 = c^2 + d^2 - a^2", @@ -85,12 +85,12 @@ def construct(self): # TODO: # When all scenes are rendered with a single command line call # uv run manim render MathTexExamples.py --write_all -# ExampleScene6 fails with the following error +# Scene6 fails with the following error # KeyError: 'unique001ss' # I think it is related to a caching issue, because the error vanishes # when the scene is rendered by itself. -# uv run manim render MathTexExamples.py ExampleScene6 -class ExampleScene6(Scene): +# uv run manim render MathTexExamples.py Scene6 +class Scene6(Scene): def construct(self): formula = MathTex( r"a^2 + b^2 = c^2 + d^2 - a^2", @@ -103,7 +103,7 @@ def construct(self): self.add(formula) -class ExampleScene7(Scene): +class Scene7(Scene): def construct(self): formula = MathTex( r"a^2 + b^2 = c^2 + d^2 - 2 a^2", @@ -116,7 +116,7 @@ def construct(self): self.add(formula) -class ExampleScene8(Scene): +class Scene8(Scene): """ Example based on this issue: set_color_by_tex selects wrong substring in certain contexts @@ -139,7 +139,7 @@ def construct(self): self.add(formula) -class ExampleScene9(Scene): +class Scene9(Scene): def construct(self): t2cm = {r"\sum": BLUE, "^{n}": RED, "_{1}": GREEN, "x": YELLOW} eq1 = MathTex(r"\sum", "^{n}", "_{1}", "x").scale(1.3) @@ -160,7 +160,7 @@ def set_color_by_tex( self.add(grp) -class ExampleScene10(Scene): +class Scene10(Scene): def construct(self): # TODO: This approach to highlighting \sum does not work right now. # It changes the shape of the rendered equation. @@ -195,7 +195,7 @@ def set_color_by_tex( # eq2[1].set_color(BLUE) -class ExampleScene11(Scene): +class Scene11(Scene): def construct(self): t2cm = {"n": RED, "1": GREEN, "x": YELLOW} eq = MathTex(r"\sum_{1}^{n} x", tex_to_color_map=t2cm).scale(1.3) @@ -203,7 +203,7 @@ def construct(self): self.add(eq) -class ExampleScene12(Scene): +class Scene12(Scene): def construct(self): eq = MathTex(r"\sum_{1}^{n} x", substrings_to_isolate=["1", "n", "x"]).scale( 1.3 @@ -215,7 +215,7 @@ def construct(self): self.add(eq) -class ExampleScene13(Scene): +class Scene13(Scene): def construct(self): matrix_elements = [[1, 2, 3]] row = 0 @@ -224,7 +224,7 @@ def construct(self): print(matrix.get_columns()[column][row].tex_string) -class ExampleScene14(Scene): +class Scene14(Scene): def construct(self): start = MathTex("A", r"\to", "B") end = MathTex("B", r"\to", "A") @@ -233,7 +233,7 @@ def construct(self): self.play(TransformMatchingTex(start, end, fade_transform_mismatches=True)) -class ExampleScene15(Scene): +class Scene15(Scene): """ Example taken from TeX splitting can cause the last parts of an equation to not be displayed @@ -271,7 +271,7 @@ def construct(self): self.add(lpc_implies_not_polynomial, lpc_implies_polynomial) -class ExampleScene16(Scene): +class Scene16(Scene): """ LaTeX rendering incorrectly https://github.com/ManimCommunity/manim/issues/2912 @@ -294,7 +294,7 @@ def construct(self): self.add(Tex(r"$\frac{a}{b}$", tex_template=template)) -class ExampleScene17(Scene): +class Scene17(Scene): """ Tex splitting issue with frac numerator https://github.com/ManimCommunity/manim/issues/2884 @@ -322,7 +322,7 @@ def construct(self): self.wait(2) -class ExampleScene18(Scene): +class Scene18(Scene): """ Transforming to similar MathTex object distorts sometimes https://github.com/ManimCommunity/manim/issues/2544 @@ -344,14 +344,15 @@ def construct(self): self.wait() -class ExampleScene19(Scene): +class Scene19(Scene): """ - Docstring for ExampleScene19 Manim's unexpected colouring behaviour under the radical sign https://github.com/ManimCommunity/manim/issues/1996 - The code does not run at the moment. + The code from the issue does not run at the moment. AttributeError: VMobjectFromSVGPath object has no attribute 'tex_string' + + The code have been adjusted to be able to run. """ def construct(self): @@ -359,51 +360,47 @@ def construct(self): val_b = 2 color_a = "#0470cf" color_b = "#cf0492" - tex_a = Tex(str(val_a).format(int), color=color_a).move_to([-1, 2, 0]).scale(2) - tex_b = Tex(str(val_b).format(int), color=color_b).move_to([1, 2, 0]).scale(2) + tex_a = Tex(str(val_a), color=color_a).move_to([-1, 2, 0]).scale(2) + tex_b = Tex(str(val_b), color=color_b).move_to([1, 2, 0]).scale(2) self.play(FadeIn(tex_a, tex_b)) self.wait() - # using \over because \frac fails here form = MathTex( - r"\hat{u}= { 3 \hat{i}+ 2\hat{j} \over{ \sqrt{ {2^{2}+3^{2} } } } }", + # unique000 + r"\hat{u}= \frac{ 3 \hat{i}+ 2\hat{j}}{\sqrt{ {", + # unique001 + r"2", + # unique002 + r"^{2}+", + # unique003 + r"3", + # unique004 + r"^{2} } } } } }", substrings_to_isolate=["2", "3"], ).scale(1.25) - # This line triggers the error - idx_a = np.array( - [ - i - for i, character in enumerate(form) - if character.get_tex_string() == tex_a[0].get_tex_string() - ], - dtype="int", - ) - idx_b = np.array( - [ - i - for i, character in enumerate(form) - if character.get_tex_string() == tex_b[0].get_tex_string() - ], - dtype="int", - ) - - get_pos_a = form[idx_a[0]].get_center() - get_pos_b = form[idx_b[0]].get_center() + get_pos_a = form.id_to_vgroup_dict["unique003"].get_center() + get_pos_b = form.id_to_vgroup_dict["unique001"].get_center() - # using the set_color methods of MathTex object - form.set_color_by_tex(str(val_a).format(int), color_a) - b1_copy = form[idx_b[0]].set_color(color_b) - print(b1_copy) + form.id_to_vgroup_dict["unique003"].set_color(color_a) + form.id_to_vgroup_dict["unique001"].set_color(color_b) self.play(FadeIn(form)) - self.play(tex_a.animate.move_to(get_pos_a).match_height(form[idx_a[0]])) - self.play(tex_b.animate.move_to(get_pos_b).match_height(form[idx_b[0]])) + self.play( + tex_a.animate.move_to(get_pos_a).match_height( + form.id_to_vgroup_dict["unique003"] + ) + ) + self.play( + tex_b.animate.move_to(get_pos_b).match_height( + form.id_to_vgroup_dict["unique001"] + ) + ) self.wait(1) -class ExampleScene20(Scene): +class Scene20(Scene): """ LaTex Error in combination of MathTex and SurroundingRectangle https://github.com/ManimCommunity/manim/issues/1907 @@ -448,23 +445,44 @@ def construct(self): self.wait(5) -class ExampleScene21(Scene): +class Scene21(Scene): """ LaTeX compilation error when breaking up a MathTex string by subscripts https://github.com/ManimCommunity/manim/issues/1865 - If I add {} around each subscript, the code works as expected. + This seems to work well. """ def construct(self): - # Original string which fails to compile - # reqeq2 = MathTex(r"Y_{ij} = \mu_i + \gamma_i X_{ij} + e_{ij}", substrings_to_isolate = ["i"]) reqeq2 = MathTex( - r"Y_{ij} = \mu_i + \gamma_i X_{ij} + e_{ij}", - substrings_to_isolate=["i"], + r"Y_{ij} = \mu_i + \gamma_i X_{ij} + e_{ij}", substrings_to_isolate=["i"] ) self.add(reqeq2) +class Scene22(Scene): + """ + uwezi on discord 2025-05-27 Kerning (tex vs text) + https://discord.com/channels/581738731934056449/1376977419269050460/1376998448724967454 + + In the thread it is explained that the tex_environment should + be given without the opening curly brace. + tex_environment="minipage}{20em}" + The code below seems to work fine. + """ + + def construct(self): + tex = Tex( + r""" +Lorem ipsum dolor sit amet, consectetur adipiscing elit. +Donec rhoncus eros turpis, quis ullamcorper augue pretium eget. +Nullam hendrerit massa at mauris lacinia, eget rhoncus +enim vestibulum. +""", + tex_environment="{minipage}{20em}", + ) + self.add(tex) + + # Get inspiration from # https://docs.manim.community/en/stable/guides/using_text.html#text-with-latex From bb41f5c570c035f4e86a62d2d5ed01aa028e73fb Mon Sep 17 00:00:00 2001 From: Henrik Skov Midtiby Date: Fri, 19 Dec 2025 15:41:30 +0100 Subject: [PATCH 26/56] Added comment --- issue/MathTexExamples.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/issue/MathTexExamples.py b/issue/MathTexExamples.py index 5d1276dd31..0b73222065 100644 --- a/issue/MathTexExamples.py +++ b/issue/MathTexExamples.py @@ -225,6 +225,11 @@ def construct(self): class Scene14(Scene): + """ + Triggers this exception + Exception in TransformMatchingTex::get_mobject_key + """ + def construct(self): start = MathTex("A", r"\to", "B") end = MathTex("B", r"\to", "A") From c9b6c5291809b7c2a7f4e3d914b1da6376d9697b Mon Sep 17 00:00:00 2001 From: Henrik Skov Midtiby Date: Fri, 19 Dec 2025 21:46:59 +0100 Subject: [PATCH 27/56] _break_up_by_substrings --- manim/mobject/text/tex_mobject.py | 32 ++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/manim/mobject/text/tex_mobject.py b/manim/mobject/text/tex_mobject.py index c252990038..75f3bb3163 100644 --- a/manim/mobject/text/tex_mobject.py +++ b/manim/mobject/text/tex_mobject.py @@ -353,9 +353,7 @@ def join_tex_strings_with_unique_deliminters( tex_template=self.tex_template, **kwargs, ) - self.tex_string = self.arg_separator.join( - [str(s) for s in self.tex_strings] - ) + self._break_up_by_substrings() except ValueError as compilation_error: if self.brace_notation_split_occurred: logger.error( @@ -376,6 +374,34 @@ def join_tex_strings_with_unique_deliminters( if self.organize_left_to_right: self._organize_submobjects_left_to_right() + def _break_up_by_substrings(self) -> Self: + """ + Reorganize existing submobjects one layer + deeper based on the structure of tex_strings (as a list + of tex_strings) + """ + new_submobjects: list[VMobject] = [] + curr_index = 0 + for tex_string in self.tex_strings: + sub_tex_mob = SingleStringMathTex( + tex_string, + tex_environment=self.tex_environment, + tex_template=self.tex_template, + ) + num_submobs = len(sub_tex_mob.submobjects) + 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) + else: + sub_tex_mob.submobjects = self.submobjects[curr_index:new_index] + new_submobjects.append(sub_tex_mob) + curr_index = new_index + self.submobjects = new_submobjects + return self + def get_part_by_tex(self, tex: str, **kwargs: Any) -> VGroup | None: for tex_str, match_id in self.matched_strings_and_ids: if tex_str == tex: From 0afc9b887902c1c263839eccfa0d23b48df0cc34 Mon Sep 17 00:00:00 2001 From: Henrik Skov Midtiby Date: Fri, 19 Dec 2025 21:48:57 +0100 Subject: [PATCH 28/56] Refactor code --- manim/mobject/text/tex_mobject.py | 132 +++++++++++++++--------------- 1 file changed, 66 insertions(+), 66 deletions(-) diff --git a/manim/mobject/text/tex_mobject.py b/manim/mobject/text/tex_mobject.py index 75f3bb3163..0c28a956bc 100644 --- a/manim/mobject/text/tex_mobject.py +++ b/manim/mobject/text/tex_mobject.py @@ -278,73 +278,8 @@ def __init__( self.tex_strings = tex_strings self.matched_strings_and_ids: list[tuple[str, str]] = [] - def locate_first_match( - substrings_to_isolate: Iterable[str], unprocessed_string: str - ) -> re.Match | None: - first_match_start = len(unprocessed_string) - first_match_length = 0 - first_match = None - for substring in substrings_to_isolate: - match = re.match(f"(.*?)({substring})(.*)", unprocessed_string) - if match and len(match.group(1)) < first_match_start: - first_match = match - first_match_start = len(match.group(1)) - first_match_length = len(match.group(2)) - elif match and len(match.group(1)) == first_match_start: - # Break ties by looking at length of matches. - if first_match_length < len(match.group(2)): - first_match = match - first_match_start = len(match.group(1)) - first_match_length = len(match.group(2)) - return first_match - - def handle_match(ssIdx: int, first_match: re.Match) -> tuple[str, str]: - pre_match = first_match.group(1) - matched_string = first_match.group(2) - post_match = first_match.group(3) - pre_string = "{" + rf"\special{{dvisvgm:raw }}" - post_string = r"\special{dvisvgm:raw }}" - self.matched_strings_and_ids.append( - (matched_string, f"unique{ssIdx:03d}ss") - ) - processed_string = pre_match + pre_string + matched_string + post_string - unprocessed_string = post_match - return processed_string, unprocessed_string - - def join_tex_strings_with_unique_deliminters( - tex_strings: Iterable[str], substrings_to_isolate: Iterable[str] - ) -> str: - joined_string = "" - ssIdx = 0 - for idx, tex_string in enumerate(tex_strings): - string_part = rf"\special{{dvisvgm:raw }}" - self.matched_strings_and_ids.append((tex_string, f"unique{idx:03d}")) - - # Try to match with all substrings_to_isolate and apply the first match - # then match again (on the rest of the string) and continue until no - # characters are left in the string - unprocessed_string = str(tex_string) - processed_string = "" - while len(unprocessed_string) > 0: - first_match = locate_first_match( - substrings_to_isolate, unprocessed_string - ) - - if first_match: - processed, unprocessed_string = handle_match(ssIdx, first_match) - processed_string = processed_string + processed - ssIdx += 1 - else: - processed_string = processed_string + unprocessed_string - unprocessed_string = "" - - string_part += processed_string - string_part += r"\special{dvisvgm:raw }" - joined_string = joined_string + string_part - return joined_string - try: - joined_string = join_tex_strings_with_unique_deliminters( + joined_string = self._join_tex_strings_with_unique_deliminters( self.tex_strings, self.substrings_to_isolate ) super().__init__( @@ -374,6 +309,71 @@ def join_tex_strings_with_unique_deliminters( if self.organize_left_to_right: self._organize_submobjects_left_to_right() + def _join_tex_strings_with_unique_deliminters( + self, tex_strings: Iterable[str], substrings_to_isolate: Iterable[str] + ) -> str: + joined_string = "" + ssIdx = 0 + for idx, tex_string in enumerate(tex_strings): + string_part = rf"\special{{dvisvgm:raw }}" + self.matched_strings_and_ids.append((tex_string, f"unique{idx:03d}")) + + # Try to match with all substrings_to_isolate and apply the first match + # then match again (on the rest of the string) and continue until no + # characters are left in the string + unprocessed_string = str(tex_string) + processed_string = "" + while len(unprocessed_string) > 0: + first_match = self._locate_first_match( + substrings_to_isolate, unprocessed_string + ) + + if first_match: + processed, unprocessed_string = self._handle_match( + ssIdx, first_match + ) + processed_string = processed_string + processed + ssIdx += 1 + else: + processed_string = processed_string + unprocessed_string + unprocessed_string = "" + + string_part += processed_string + string_part += r"\special{dvisvgm:raw }" + joined_string = joined_string + string_part + return joined_string + + def _locate_first_match( + self, substrings_to_isolate: Iterable[str], unprocessed_string: str + ) -> re.Match | None: + first_match_start = len(unprocessed_string) + first_match_length = 0 + first_match = None + for substring in substrings_to_isolate: + match = re.match(f"(.*?)({substring})(.*)", unprocessed_string) + if match and len(match.group(1)) < first_match_start: + first_match = match + first_match_start = len(match.group(1)) + first_match_length = len(match.group(2)) + elif match and len(match.group(1)) == first_match_start: + # Break ties by looking at length of matches. + if first_match_length < len(match.group(2)): + first_match = match + first_match_start = len(match.group(1)) + first_match_length = len(match.group(2)) + return first_match + + def _handle_match(self, ssIdx: int, first_match: re.Match) -> tuple[str, str]: + pre_match = first_match.group(1) + matched_string = first_match.group(2) + post_match = first_match.group(3) + pre_string = "{" + rf"\special{{dvisvgm:raw }}" + post_string = r"\special{dvisvgm:raw }}" + self.matched_strings_and_ids.append((matched_string, f"unique{ssIdx:03d}ss")) + processed_string = pre_match + pre_string + matched_string + post_string + unprocessed_string = post_match + return processed_string, unprocessed_string + def _break_up_by_substrings(self) -> Self: """ Reorganize existing submobjects one layer From e2f320326d3aa367da52856685a42643474def45 Mon Sep 17 00:00:00 2001 From: Henrik Skov Midtiby Date: Fri, 19 Dec 2025 21:49:27 +0100 Subject: [PATCH 29/56] Added comment to example --- issue/MathTexExamples.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/issue/MathTexExamples.py b/issue/MathTexExamples.py index 0b73222065..1f690e7036 100644 --- a/issue/MathTexExamples.py +++ b/issue/MathTexExamples.py @@ -305,7 +305,10 @@ class Scene17(Scene): https://github.com/ManimCommunity/manim/issues/2884 Problem is not solved at the moment. - Please examine TransformMatchingTex::get_mobject_key() to handle this. + + I don't think it is possible to make this + type of animation / transform with the current implementation, as it + requires the expression to be able to compile for each segment. """ def construct(self): @@ -315,17 +318,20 @@ def construct(self): ) self.add(n) self.wait(2) - # This works self.play(TransformMatchingTex(n, denominator)) self.wait(2) - # This does not work numerator = MathTex(r"\frac{ n + 1 }{ 2 }", substrings_to_isolate="n").shift( 2 * DOWN ) self.play(TransformMatchingTex(n, numerator)) self.wait(2) + # This approach works fine. + fraction_right = MathTex(r"{ ", "n", r" + 1 } \over { 2 }").shift(2 * RIGHT) + self.play(TransformMatchingTex(n, fraction_right)) + self.wait(2) + class Scene18(Scene): """ From f0a4bf94d3c979c3a83ae6330e67e4fd022a428f Mon Sep 17 00:00:00 2001 From: Henrik Skov Midtiby Date: Fri, 19 Dec 2025 22:02:59 +0100 Subject: [PATCH 30/56] Handle integer inputs well. --- manim/mobject/text/tex_mobject.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manim/mobject/text/tex_mobject.py b/manim/mobject/text/tex_mobject.py index 0c28a956bc..e65ae862f8 100644 --- a/manim/mobject/text/tex_mobject.py +++ b/manim/mobject/text/tex_mobject.py @@ -384,7 +384,7 @@ def _break_up_by_substrings(self) -> Self: curr_index = 0 for tex_string in self.tex_strings: sub_tex_mob = SingleStringMathTex( - tex_string, + str(tex_string), tex_environment=self.tex_environment, tex_template=self.tex_template, ) From 3fd3adcfef1a90a195267064af1a1b5544bb6def Mon Sep 17 00:00:00 2001 From: Henrik Skov Midtiby Date: Fri, 19 Dec 2025 22:40:50 +0100 Subject: [PATCH 31/56] Expose the original tex_string --- manim/mobject/text/tex_mobject.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/manim/mobject/text/tex_mobject.py b/manim/mobject/text/tex_mobject.py index e65ae862f8..a46efc7f88 100644 --- a/manim/mobject/text/tex_mobject.py +++ b/manim/mobject/text/tex_mobject.py @@ -262,7 +262,6 @@ def __init__( tex_environment: str | None = "align*", **kwargs: Any, ): - self.texstring = "" self.tex_template = kwargs.pop("tex_template", config["tex_template"]) self.arg_separator = arg_separator self.substrings_to_isolate = ( @@ -275,7 +274,12 @@ def __init__( self.substrings_to_isolate.extend(self.tex_to_color_map.keys()) self.tex_environment = tex_environment self.brace_notation_split_occurred = False - self.tex_strings = tex_strings + # Deal with the case where tex_strings contains integers instead + # of strings. + tex_strings_validated = [ + string if isinstance(string, str) else str(string) for string in tex_strings + ] + self.tex_strings = tex_strings_validated self.matched_strings_and_ids: list[tuple[str, str]] = [] try: @@ -288,6 +292,8 @@ def __init__( tex_template=self.tex_template, **kwargs, ) + # Save the original tex_string + self.tex_string = self.arg_separator.join(self.tex_strings) self._break_up_by_substrings() except ValueError as compilation_error: if self.brace_notation_split_occurred: From f06991e2fe717f8f2df6c32aa80923f9dfa68978 Mon Sep 17 00:00:00 2001 From: Henrik Skov Midtiby Date: Fri, 19 Dec 2025 22:54:08 +0100 Subject: [PATCH 32/56] Do not treat the content of substrings_to_isolate as regular expressions. --- manim/mobject/text/tex_mobject.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manim/mobject/text/tex_mobject.py b/manim/mobject/text/tex_mobject.py index a46efc7f88..768cd60903 100644 --- a/manim/mobject/text/tex_mobject.py +++ b/manim/mobject/text/tex_mobject.py @@ -356,7 +356,7 @@ def _locate_first_match( first_match_length = 0 first_match = None for substring in substrings_to_isolate: - match = re.match(f"(.*?)({substring})(.*)", unprocessed_string) + match = re.match(f"(.*?)({re.escape(substring)})(.*)", unprocessed_string) if match and len(match.group(1)) < first_match_start: first_match = match first_match_start = len(match.group(1)) From c11dcc32910ec0908ebab53466e09ff1fb9cb3ab Mon Sep 17 00:00:00 2001 From: Henrik Skov Midtiby Date: Fri, 19 Dec 2025 22:54:26 +0100 Subject: [PATCH 33/56] Updated examples --- issue/MathTexExamples.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/issue/MathTexExamples.py b/issue/MathTexExamples.py index 1f690e7036..7e33e537fa 100644 --- a/issue/MathTexExamples.py +++ b/issue/MathTexExamples.py @@ -221,6 +221,7 @@ def construct(self): row = 0 column = 2 matrix = Matrix(matrix_elements) + print("matrix.get_columns()[column][row].tex_string") print(matrix.get_columns()[column][row].tex_string) @@ -495,5 +496,14 @@ def construct(self): self.add(tex) +class Scene23(Scene): + def construct(self): + """Test that set_opacity_by_tex works correctly.""" + tex = MathTex("f(x) = y", substrings_to_isolate=["f(x)"]) + print(tex.matched_strings_and_ids) + tex.set_opacity_by_tex("f(x)", 0.2, 0.5) + self.add(tex) + + # Get inspiration from # https://docs.manim.community/en/stable/guides/using_text.html#text-with-latex From c11799a574189c61482aef061171367abadc357a Mon Sep 17 00:00:00 2001 From: Henrik Skov Midtiby Date: Fri, 19 Dec 2025 23:05:45 +0100 Subject: [PATCH 34/56] Update examples --- issue/MathTexExamples.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/issue/MathTexExamples.py b/issue/MathTexExamples.py index 7e33e537fa..1666f59c18 100644 --- a/issue/MathTexExamples.py +++ b/issue/MathTexExamples.py @@ -94,10 +94,10 @@ class Scene6(Scene): def construct(self): formula = MathTex( r"a^2 + b^2 = c^2 + d^2 - a^2", - substrings_to_isolate=["[acd]"], + substrings_to_isolate=["a", "c", "d"], ).scale(1.3) - formula.set_color_by_tex("c", ORANGE) + formula.set_color_by_tex("c", YELLOW) formula.set_color_by_tex("a", RED) self.add(formula) @@ -107,7 +107,7 @@ class Scene7(Scene): def construct(self): formula = MathTex( r"a^2 + b^2 = c^2 + d^2 - 2 a^2", - substrings_to_isolate=["[acd]"], + substrings_to_isolate=["a", "c", "d"], ).scale(1.3) formula.set_color_by_tex("c", GREEN) From ada86f1a7461a0e4ea475dc2dbfc4db262793569 Mon Sep 17 00:00:00 2001 From: Henrik Skov Midtiby Date: Mon, 22 Dec 2025 22:06:36 +0100 Subject: [PATCH 35/56] Fix SVMobject caching issue. --- issue/MathTexExamples.py | 17 ++++++++++++++--- manim/mobject/svg/svg_mobject.py | 3 ++- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/issue/MathTexExamples.py b/issue/MathTexExamples.py index 1666f59c18..051763bf87 100644 --- a/issue/MathTexExamples.py +++ b/issue/MathTexExamples.py @@ -204,10 +204,21 @@ def construct(self): class Scene12(Scene): + """ + The following command + uv run manim MathTexExamples.py Scene11 Scene12 + triggers what I believe is a caching error. + If the parameter "use_svg_cache=False" is included in the call + to MathTex the issue disappears. + KeyError: 'unique000' + The issue is that the tex string is the exact same in this + scene as in Scene11. This also means that the id_to_vgroup_dict is empty. + """ + def construct(self): - eq = MathTex(r"\sum_{1}^{n} x", substrings_to_isolate=["1", "n", "x"]).scale( - 1.3 - ) + eq = MathTex( + r"\sum_{1}^{n} x", substrings_to_isolate=["1", "n", "x"], use_svg_cache=True + ).scale(1.3) eq.set_color_by_tex("1", YELLOW) eq.set_color_by_tex("x", RED) eq.set_opacity_by_tex("n", 0.5) diff --git a/manim/mobject/svg/svg_mobject.py b/manim/mobject/svg/svg_mobject.py index f54b251a64..6aaa484b9c 100644 --- a/manim/mobject/svg/svg_mobject.py +++ b/manim/mobject/svg/svg_mobject.py @@ -26,7 +26,7 @@ __all__ = ["SVGMobject", "VMobjectFromSVGPath"] -SVG_HASH_TO_MOB_MAP: dict[int, VMobject] = {} +SVG_HASH_TO_MOB_MAP: dict[int, SVGMobject] = {} def _convert_point_to_3d(x: float, y: float) -> np.ndarray: @@ -171,6 +171,7 @@ def init_svg_mobject(self, use_svg_cache: bool) -> None: if hash_val in SVG_HASH_TO_MOB_MAP: mob = SVG_HASH_TO_MOB_MAP[hash_val].copy() self.add(*mob) + self.id_to_vgroup_dict = mob.id_to_vgroup_dict return self.generate_mobject() From 919bfee4328121ae651e3c7dcdd2fa04f936da10 Mon Sep 17 00:00:00 2001 From: Henrik Skov Midtiby Date: Mon, 22 Dec 2025 22:07:16 +0100 Subject: [PATCH 36/56] Remove traces from brace_notation_split_occurred --- manim/mobject/text/tex_mobject.py | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/manim/mobject/text/tex_mobject.py b/manim/mobject/text/tex_mobject.py index 768cd60903..011a09d0bd 100644 --- a/manim/mobject/text/tex_mobject.py +++ b/manim/mobject/text/tex_mobject.py @@ -27,12 +27,11 @@ import re from collections.abc import Iterable from functools import reduce -from textwrap import dedent from typing import Any from typing_extensions import Self -from manim import config, logger +from manim import config from manim.constants import * from manim.mobject.geometry.line import Line from manim.mobject.svg.svg_mobject import SVGMobject @@ -273,7 +272,6 @@ def __init__( self.tex_to_color_map = tex_to_color_map self.substrings_to_isolate.extend(self.tex_to_color_map.keys()) self.tex_environment = tex_environment - self.brace_notation_split_occurred = False # Deal with the case where tex_strings contains integers instead # of strings. tex_strings_validated = [ @@ -296,19 +294,6 @@ def __init__( self.tex_string = self.arg_separator.join(self.tex_strings) self._break_up_by_substrings() except ValueError as compilation_error: - if self.brace_notation_split_occurred: - logger.error( - dedent( - """\ - A group of double braces, {{ ... }}, was detected in - your string. Manim splits TeX strings at the double - braces, which might have caused the current - compilation error. If you didn't use the double brace - split intentionally, add spaces between the braces to - avoid the automatic splitting: {{ ... }} --> { { ... } }. - """, - ), - ) raise compilation_error self.set_color_by_tex_to_color_map(self.tex_to_color_map) From b7d1c83ffdf9f9c18324622aa94649853e4d6fab Mon Sep 17 00:00:00 2001 From: Henrik Skov Midtiby Date: Mon, 22 Dec 2025 22:08:07 +0100 Subject: [PATCH 37/56] Simplify MathTex::_break_up_by_substrings --- manim/mobject/text/tex_mobject.py | 33 ++++++++++++++----------------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/manim/mobject/text/tex_mobject.py b/manim/mobject/text/tex_mobject.py index 011a09d0bd..81c6206f0f 100644 --- a/manim/mobject/text/tex_mobject.py +++ b/manim/mobject/text/tex_mobject.py @@ -372,24 +372,14 @@ def _break_up_by_substrings(self) -> Self: of tex_strings) """ new_submobjects: list[VMobject] = [] - curr_index = 0 - for tex_string in self.tex_strings: - sub_tex_mob = SingleStringMathTex( - str(tex_string), - tex_environment=self.tex_environment, - tex_template=self.tex_template, - ) - num_submobs = len(sub_tex_mob.submobjects) - 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) - else: - sub_tex_mob.submobjects = self.submobjects[curr_index:new_index] - new_submobjects.append(sub_tex_mob) - curr_index = new_index + for tex_string, tex_string_id in self.matched_strings_and_ids: + if tex_string_id[-2:] == "ss": + continue + mtp = MathTexPart() + mtp.tex_string = tex_string + mtp.submobjects.append(self.id_to_vgroup_dict[tex_string_id]) + new_submobjects.append(mtp) + self.old_submobjects = self.submobjects self.submobjects = new_submobjects return self @@ -454,6 +444,13 @@ def sort_alphabetically(self) -> None: self.submobjects.sort(key=lambda m: m.get_tex_string()) +class MathTexPart(VMobject): + tex_string: str + + def __repr__(self) -> str: + return f"{type(self).__name__}({repr(self.tex_string)})" + + class Tex(MathTex): r"""A string compiled with LaTeX in normal mode. From ca6f0c1760cada2aa1dde0e553643c9bd5bc6de5 Mon Sep 17 00:00:00 2001 From: Henrik Skov Midtiby Date: Mon, 22 Dec 2025 22:10:42 +0100 Subject: [PATCH 38/56] Fix small issue in tex that in some cases moved elements a tiny bit around --- issue/MathTexExamples.py | 19 +++++++++++++++++++ manim/mobject/text/tex_mobject.py | 4 ++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/issue/MathTexExamples.py b/issue/MathTexExamples.py index 051763bf87..532c97a0e2 100644 --- a/issue/MathTexExamples.py +++ b/issue/MathTexExamples.py @@ -37,7 +37,26 @@ def construct(self): self.add(formula) +class Scene4(Scene): + def construct(self): + formula = MathTex( + r"a^2 + b^2 = c^2 + a^2", + ).scale(1.3) + + self.add(formula) + + class Scene4a(Scene): + r""" + One small issue here is that the power 2 that b is raised to + is moved a bit upwards than in Scene4 and Scene4b. + This is related to adding a pair of curly braces around the + detected substrings in MathTex::_handle_match + pre_string = "{" + rf"\special{{dvisvgm:raw }}" + post_string = r"\special{dvisvgm:raw }}" + If the curly braces are not added, the issue disappears. + """ + def construct(self): formula = MathTex( r"a^2 + b^2 = c^2 + a^2", diff --git a/manim/mobject/text/tex_mobject.py b/manim/mobject/text/tex_mobject.py index 81c6206f0f..5bca9421a0 100644 --- a/manim/mobject/text/tex_mobject.py +++ b/manim/mobject/text/tex_mobject.py @@ -358,8 +358,8 @@ def _handle_match(self, ssIdx: int, first_match: re.Match) -> tuple[str, str]: pre_match = first_match.group(1) matched_string = first_match.group(2) post_match = first_match.group(3) - pre_string = "{" + rf"\special{{dvisvgm:raw }}" - post_string = r"\special{dvisvgm:raw }}" + pre_string = rf"\special{{dvisvgm:raw }}" + post_string = r"\special{dvisvgm:raw }" self.matched_strings_and_ids.append((matched_string, f"unique{ssIdx:03d}ss")) processed_string = pre_match + pre_string + matched_string + post_string unprocessed_string = post_match From d7ddee877285ddf6e1b19e4207d22233a8ef4f70 Mon Sep 17 00:00:00 2001 From: Henrik Skov Midtiby Date: Mon, 22 Dec 2025 22:11:14 +0100 Subject: [PATCH 39/56] No use of regular expressions for locate substrings. --- issue/MathTexExamples.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/issue/MathTexExamples.py b/issue/MathTexExamples.py index 532c97a0e2..74c18b3c14 100644 --- a/issue/MathTexExamples.py +++ b/issue/MathTexExamples.py @@ -87,6 +87,11 @@ def construct(self): class Scene5(Scene): + """ + At an earlier stage, I experimented with using a regular expression to + locate substrings to isolate. I don't think this is actually needed. + """ + def construct(self): formula = MathTex( r"a^2 + b^2 = c^2 + d^2 - a^2", From 654f877954fe0fd6bce558d383b26e097a008116 Mon Sep 17 00:00:00 2001 From: Henrik Skov Midtiby Date: Mon, 22 Dec 2025 23:24:03 +0100 Subject: [PATCH 40/56] Updated notes to the set of test cases --- issue/MathTexExamples.py | 210 +++++++++++++++++++++++++++++---------- 1 file changed, 159 insertions(+), 51 deletions(-) diff --git a/issue/MathTexExamples.py b/issue/MathTexExamples.py index 74c18b3c14..48cea8ebc9 100644 --- a/issue/MathTexExamples.py +++ b/issue/MathTexExamples.py @@ -3,6 +3,17 @@ from manim import * +class Scene1(Scene): + def construct(self): + formula = MathTex( + r"P(X=k)", + r" = 0.5^k (1-0.5)^{12-k}", + ) + formula.id_to_vgroup_dict["unique001"].set_color(RED) + self.add(formula) + # self.add(formula.id_to_vgroup_dict['unique001']) + + class Scene2(Scene): def construct(self): formula = MathTex( @@ -106,14 +117,6 @@ def construct(self): self.add(formula) -# TODO: -# When all scenes are rendered with a single command line call -# uv run manim render MathTexExamples.py --write_all -# Scene6 fails with the following error -# KeyError: 'unique001ss' -# I think it is related to a caching issue, because the error vanishes -# when the scene is rendered by itself. -# uv run manim render MathTexExamples.py Scene6 class Scene6(Scene): def construct(self): formula = MathTex( @@ -169,22 +172,21 @@ def construct(self): eq1 = MathTex(r"\sum", "^{n}", "_{1}", "x").scale(1.3) eq2 = MathTex(r"\sum", "_{1}", "^{n}", "x").scale(1.3) - def set_color_by_tex( - mathtex: MathTex, tex: str, color: ParsableManimColor - ) -> None: - for match in mathtex.matched_strings_and_ids: - if match[0] == tex: - mathtex.id_to_vgroup_dict[match[1]].set_color(color) - - for k, v in t2cm.items(): - set_color_by_tex(eq1, k, v) - set_color_by_tex(eq2, k, v) + for texstring, color in t2cm.items(): + eq1.set_color_by_tex(texstring, color) + eq2.set_color_by_tex(texstring, color) grp = VGroup(eq1, eq2).arrange_in_grid(2, 1) self.add(grp) class Scene10(Scene): + """ + This scene show an example of when the current implementation + of substrings_to_isolate fails, as it changes the layout of the + rendered latex equation. + """ + def construct(self): # TODO: This approach to highlighting \sum does not work right now. # It changes the shape of the rendered equation. @@ -197,26 +199,42 @@ def construct(self): r"\sum_{1}^{n} x", substrings_to_isolate=list(t2cm2.keys()) ).scale(1.3) - def set_color_by_tex( - mathtex: MathTex, tex: str, color: ParsableManimColor - ) -> None: - for match in mathtex.matched_strings_and_ids: - if match[0] == tex: - mathtex.id_to_vgroup_dict[match[1]].set_color(color) + for texstring, color in t2cm1.items(): + eq1.set_color_by_tex(texstring, color) + for texstring, color in t2cm2.items(): + eq2.set_color_by_tex(texstring, color) + + grp = VGroup(eq1, eq2).arrange_in_grid(2, 1) + self.add(grp) + + +class Scene10a(Scene): + """ + This scene shows a workaround to the issue in Scene10, based on + index_labels, it works, but currently the levelling is a bit different + the previous implementation of MathTex. + """ + + def construct(self): + t2cm = {"n": RED, "1": GREEN, "x": YELLOW} + eq1 = MathTex(r"\sum^{n}_{1} x", substrings_to_isolate=list(t2cm.keys())).scale( + 1.3 + ) + eq2 = MathTex(r"\sum_{1}^{n} x", substrings_to_isolate=list(t2cm.keys())).scale( + 1.3 + ) - for k, v in t2cm1.items(): - set_color_by_tex(eq1, k, v) - for k, v in t2cm2.items(): - set_color_by_tex(eq2, k, v) + for texstring, color in t2cm.items(): + eq1.set_color_by_tex(texstring, color) + eq2.set_color_by_tex(texstring, color) grp = VGroup(eq1, eq2).arrange_in_grid(2, 1) self.add(grp) # This workaround based on index_labels still work - # labels = index_labels(eq2) + # labels = index_labels(eq2[0][0]) # self.add(labels) - # eq1[0].set_color(BLUE) - # eq2[1].set_color(BLUE) + eq2[0][0][1].set_color(BLUE) class Scene11(Scene): @@ -228,17 +246,6 @@ def construct(self): class Scene12(Scene): - """ - The following command - uv run manim MathTexExamples.py Scene11 Scene12 - triggers what I believe is a caching error. - If the parameter "use_svg_cache=False" is included in the call - to MathTex the issue disappears. - KeyError: 'unique000' - The issue is that the tex string is the exact same in this - scene as in Scene11. This also means that the id_to_vgroup_dict is empty. - """ - def construct(self): eq = MathTex( r"\sum_{1}^{n} x", substrings_to_isolate=["1", "n", "x"], use_svg_cache=True @@ -258,17 +265,13 @@ def construct(self): matrix = Matrix(matrix_elements) print("matrix.get_columns()[column][row].tex_string") print(matrix.get_columns()[column][row].tex_string) + self.add(matrix) class Scene14(Scene): - """ - Triggers this exception - Exception in TransformMatchingTex::get_mobject_key - """ - def construct(self): - start = MathTex("A", r"\to", "B") - end = MathTex("B", r"\to", "A") + start = MathTex("2", "A", r"\to", "B").shift(UP) + end = MathTex("2", "B", r"\to", "A") self.add(start) self.play(TransformMatchingTex(start, end, fade_transform_mismatches=True)) @@ -369,6 +372,22 @@ def construct(self): self.wait(2) +class Scene17a(Scene): + """This seems to work fine right now.""" + + def construct(self): + n = MathTex("n").shift(LEFT) + denominator = MathTex(r"\frac{ 2 }{ ", "n", " + 1 }").shift(2 * UP) + self.add(n) + self.wait(2) + self.play(TransformMatchingTex(n, denominator)) + self.wait(2) + + numerator = MathTex(r"\frac{", "n", " + 1 }{ 2 }").shift(2 * DOWN) + self.play(TransformMatchingTex(n, numerator)) + self.wait(2) + + class Scene18(Scene): """ Transforming to similar MathTex object distorts sometimes @@ -447,6 +466,73 @@ def construct(self): self.wait(1) +class Scene19a(Scene): + """ + This seems to work fine now. + I have split the input string manually. + + Minor issue is that the horizontal line in the fraction is not displayed + and neither is the horizontal line above the squareroot. + """ + + def construct(self): + val_a = 3 + val_b = 2 + color_a = "#f1f514" + color_b = "#cf0492" + tex_a = ( + MathTex(str(val_a).format(int), color=color_a).move_to([-1, 2, 0]).scale(2) + ) + tex_b = ( + MathTex(str(val_b).format(int), color=color_b).move_to([1, 2, 0]).scale(2) + ) + + self.play(FadeIn(tex_a, tex_b)) + self.wait() + + form = MathTex( + r"\hat{u}= \frac{ ", + "3", + r" \hat{i} + ", + "2", + r"\hat{j}}{ \sqrt{ ", + "2", + "^{2} + ", + "3", + "^{2} } } }", + ).scale(1.25) + + idx_a = [ + int(i) + for i, character in enumerate(form) + if character.tex_string == tex_a[0].tex_string + ] + idx_b = [ + int(i) + for i, character in enumerate(form) + if character.tex_string == tex_b[0].tex_string + ] + + print(idx_a) + print(idx_b) + + get_pos_a = form[idx_a[0]].get_center() + get_pos_b = form[idx_b[0]].get_center() + a1_copy = [] + b1_copy = [] + + # here I force colouring of two's and three's on the MathTex object + for i in range(len(idx_a)): + a1_copy += form[idx_a[i]].set_color(color_a) + for i in range(len(idx_b)): + b1_copy += form[idx_b[i]].set_color(color_b) + + self.play(FadeIn(form)) + self.play(tex_a.animate.move_to(get_pos_a).match_height(form[idx_a[0]])) + self.play(tex_b.animate.move_to(get_pos_b).match_height(form[idx_b[0]])) + self.wait(1) + + class Scene20(Scene): """ LaTex Error in combination of MathTex and SurroundingRectangle @@ -493,11 +579,16 @@ def construct(self): class Scene21(Scene): - """ + r""" LaTeX compilation error when breaking up a MathTex string by subscripts https://github.com/ManimCommunity/manim/issues/1865 - This seems to work well. + This currently fails. + It can be solved by adding a pair of curly braces around the + detected substrings in MathTex::_handle_match + pre_string = "{" + rf"\special{{dvisvgm:raw }}" + post_string = r"\special{dvisvgm:raw }}" + But doing that triggers and issue in Scene4a """ def construct(self): @@ -507,6 +598,23 @@ def construct(self): self.add(reqeq2) +class Scene21a(Scene): + """ + LaTeX compilation error when breaking up a MathTex string by subscripts + https://github.com/ManimCommunity/manim/issues/1865 + + By inserting curly braces around the objects to isolate, + the error vanishes. + """ + + def construct(self): + reqeq2 = MathTex( + r"Y_{ij} = \mu_{i} + \gamma_{i} X_{ij} + e_{ij}", + substrings_to_isolate=["i"], + ) + self.add(reqeq2) + + class Scene22(Scene): """ uwezi on discord 2025-05-27 Kerning (tex vs text) From ee93305435f3c13192255bc0316dad34672ee485 Mon Sep 17 00:00:00 2001 From: Henrik Skov Midtiby Date: Mon, 22 Dec 2025 23:25:19 +0100 Subject: [PATCH 41/56] Handle issues with the center environment. --- issue/MathTexExamples.py | 28 ++++++++++++++++++++++++++++ manim/mobject/text/tex_mobject.py | 19 +++++++++++-------- 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/issue/MathTexExamples.py b/issue/MathTexExamples.py index 48cea8ebc9..aaf00635a8 100644 --- a/issue/MathTexExamples.py +++ b/issue/MathTexExamples.py @@ -323,6 +323,11 @@ class Scene16(Scene): Problem is still present. I think it is related to the fraction line being a line object in the svg and not an object defined by bezier curves. + + Currently this fails to run as expected. + When inspecting the generated svg file, the group "unique000" that was inserted in the + .tex file is not present... + https://dvisvgm.de/Manpage/#specials """ def construct(self): @@ -338,6 +343,29 @@ def construct(self): self.add(Tex(r"$\frac{a}{b}$", tex_template=template)) +class Scene16a(Scene): + """ + Currently this fails to run as expected. + When inspecting the generated svg file, the group "unique000" that was inserted in the + .tex file is not present... + KeyError: unique000 + + By adding this to MathTex::_break_up_by_substrings + new_submobjects.append(self.id_to_vgroup_dict['root']) + the code still runs, even if the expected group was not found. + """ + + def construct(self): + self.add(Tex(r"$\frac{a}{b}$", tex_environment="center")) + + +class Scene16b(Scene): + """This works fine.""" + + def construct(self): + self.add(Tex(r"$\frac{a}{b}$", tex_environment=None)) + + class Scene17(Scene): """ Tex splitting issue with frac numerator diff --git a/manim/mobject/text/tex_mobject.py b/manim/mobject/text/tex_mobject.py index 5bca9421a0..16c642e72d 100644 --- a/manim/mobject/text/tex_mobject.py +++ b/manim/mobject/text/tex_mobject.py @@ -372,14 +372,17 @@ def _break_up_by_substrings(self) -> Self: of tex_strings) """ new_submobjects: list[VMobject] = [] - for tex_string, tex_string_id in self.matched_strings_and_ids: - if tex_string_id[-2:] == "ss": - continue - mtp = MathTexPart() - mtp.tex_string = tex_string - mtp.submobjects.append(self.id_to_vgroup_dict[tex_string_id]) - new_submobjects.append(mtp) - self.old_submobjects = self.submobjects + try: + for tex_string, tex_string_id in self.matched_strings_and_ids: + if tex_string_id[-2:] == "ss": + continue + mtp = MathTexPart() + mtp.tex_string = tex_string + mtp.submobjects.append(self.id_to_vgroup_dict[tex_string_id]) + new_submobjects.append(mtp) + except KeyError as e: + print(f"KeyError: {e}") + new_submobjects.append(self.id_to_vgroup_dict["root"]) self.submobjects = new_submobjects return self From 01e1f251c9aea59d2c722845164ad797f22f6218 Mon Sep 17 00:00:00 2001 From: Henrik Skov Midtiby Date: Mon, 22 Dec 2025 23:31:36 +0100 Subject: [PATCH 42/56] Add example --- issue/MathTexExamples.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/issue/MathTexExamples.py b/issue/MathTexExamples.py index aaf00635a8..d05ca2cf82 100644 --- a/issue/MathTexExamples.py +++ b/issue/MathTexExamples.py @@ -676,5 +676,17 @@ def construct(self): self.add(tex) +class Scene24(Scene): + def construct(self): + exp1 = MathTex("a^2", "+", "b^2", "=", "c^2").shift(2 * UP) + exp2 = MathTex("a^2", "=", "c^2", "-", "b^2") + exp3 = MathTex("a", "=", r"\sqrt{", "c^2", "-", "b^2", "}").shift(2 * DOWN) + self.add(exp1) + self.wait(2) + self.play(TransformMatchingTex(exp1, exp2), run_time=5) + self.play(TransformMatchingTex(exp2, exp3), run_time=5) + self.wait(2) + + # Get inspiration from # https://docs.manim.community/en/stable/guides/using_text.html#text-with-latex From 084745dbfb87f5abed33ce10cda0d0ec7c72fb27 Mon Sep 17 00:00:00 2001 From: Henrik Skov Midtiby Date: Tue, 23 Dec 2025 00:03:59 +0100 Subject: [PATCH 43/56] Fix issue with rectangles (e.g. from sqrt) --- manim/mobject/svg/svg_mobject.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manim/mobject/svg/svg_mobject.py b/manim/mobject/svg/svg_mobject.py index 6aaa484b9c..6318db79d0 100644 --- a/manim/mobject/svg/svg_mobject.py +++ b/manim/mobject/svg/svg_mobject.py @@ -313,7 +313,7 @@ def get_mobjects_from( if mob is not None: result.append(mob) parent_name = vgroup_stack[-2] - for parent_name in vgroup_stack[:-2]: + for parent_name in vgroup_stack[:-1]: vgroups[parent_name].add(mob) except Exception as e: print(e) From e4d19cb32182bfbf22d55b4ccba0ce941744b796 Mon Sep 17 00:00:00 2001 From: Henrik Skov Midtiby Date: Tue, 23 Dec 2025 00:12:24 +0100 Subject: [PATCH 44/56] ConvertToOpenGL --- manim/mobject/text/tex_mobject.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/manim/mobject/text/tex_mobject.py b/manim/mobject/text/tex_mobject.py index 16c642e72d..e7291cd8b6 100644 --- a/manim/mobject/text/tex_mobject.py +++ b/manim/mobject/text/tex_mobject.py @@ -39,6 +39,8 @@ from manim.utils.tex import TexTemplate from manim.utils.tex_file_writing import tex_to_svg_file +from ..opengl.opengl_compatibility import ConvertToOpenGL + class SingleStringMathTex(SVGMobject): """Elementary building block for rendering text with LaTeX. @@ -447,7 +449,7 @@ def sort_alphabetically(self) -> None: self.submobjects.sort(key=lambda m: m.get_tex_string()) -class MathTexPart(VMobject): +class MathTexPart(VMobject, metaclass=ConvertToOpenGL): tex_string: str def __repr__(self) -> str: From 3b7da3be06bc0287b5144171457f496cce3bbb1a Mon Sep 17 00:00:00 2001 From: Henrik Skov Midtiby Date: Tue, 23 Dec 2025 20:27:14 +0100 Subject: [PATCH 45/56] Reduce the number of nesting levels. --- manim/mobject/text/tex_mobject.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manim/mobject/text/tex_mobject.py b/manim/mobject/text/tex_mobject.py index e7291cd8b6..c82ef8f423 100644 --- a/manim/mobject/text/tex_mobject.py +++ b/manim/mobject/text/tex_mobject.py @@ -380,7 +380,7 @@ def _break_up_by_substrings(self) -> Self: continue mtp = MathTexPart() mtp.tex_string = tex_string - mtp.submobjects.append(self.id_to_vgroup_dict[tex_string_id]) + mtp.add(*self.id_to_vgroup_dict[tex_string_id].submobjects) new_submobjects.append(mtp) except KeyError as e: print(f"KeyError: {e}") From d2ea3f9a21757f913e6ecdba94dd72e2cf005006 Mon Sep 17 00:00:00 2001 From: Henrik Skov Midtiby Date: Tue, 23 Dec 2025 20:29:32 +0100 Subject: [PATCH 46/56] Use the specified arg_seperator --- manim/mobject/text/tex_mobject.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/manim/mobject/text/tex_mobject.py b/manim/mobject/text/tex_mobject.py index c82ef8f423..5fbcf8cdaa 100644 --- a/manim/mobject/text/tex_mobject.py +++ b/manim/mobject/text/tex_mobject.py @@ -303,7 +303,7 @@ def __init__( self._organize_submobjects_left_to_right() def _join_tex_strings_with_unique_deliminters( - self, tex_strings: Iterable[str], substrings_to_isolate: Iterable[str] + self, tex_strings: list[str], substrings_to_isolate: Iterable[str] ) -> str: joined_string = "" ssIdx = 0 @@ -332,6 +332,8 @@ def _join_tex_strings_with_unique_deliminters( unprocessed_string = "" string_part += processed_string + if idx < len(tex_strings) - 1: + string_part += self.arg_separator string_part += r"\special{dvisvgm:raw }" joined_string = joined_string + string_part return joined_string From e98c5149c9094262fc4d3cd95d88b315db2a1d38 Mon Sep 17 00:00:00 2001 From: Henrik Skov Midtiby Date: Tue, 23 Dec 2025 22:06:29 +0100 Subject: [PATCH 47/56] Deal with the double curly brace markup --- issue/MathTexExamples.py | 41 ++++++++++++++++++++++++++----- manim/mobject/text/tex_mobject.py | 20 ++++++++++----- 2 files changed, 49 insertions(+), 12 deletions(-) diff --git a/issue/MathTexExamples.py b/issue/MathTexExamples.py index d05ca2cf82..c21b9564e9 100644 --- a/issue/MathTexExamples.py +++ b/issue/MathTexExamples.py @@ -232,9 +232,9 @@ def construct(self): self.add(grp) # This workaround based on index_labels still work - # labels = index_labels(eq2[0][0]) + # labels = index_labels(eq2[0]) # self.add(labels) - eq2[0][0][1].set_color(BLUE) + eq2[0][1].set_color(BLUE) class Scene11(Scene): @@ -498,9 +498,6 @@ class Scene19a(Scene): """ This seems to work fine now. I have split the input string manually. - - Minor issue is that the horizontal line in the fraction is not displayed - and neither is the horizontal line above the squareroot. """ def construct(self): @@ -620,8 +617,12 @@ class Scene21(Scene): """ def construct(self): + # reqeq2 = MathTex( + # r"Y_{ij} = \mu_i + \gamma_i X_{ij} + e_{ij}", substrings_to_isolate=["i"] + # ) reqeq2 = MathTex( - r"Y_{ij} = \mu_i + \gamma_i X_{ij} + e_{ij}", substrings_to_isolate=["i"] + r"Y_{ij} = \mu_{i} + \gamma_{i} X_{ij} + e_{ij}", + substrings_to_isolate=["i"], ) self.add(reqeq2) @@ -688,5 +689,33 @@ def construct(self): self.wait(2) +class Scene25(Scene): + """test_tex_white_space_and_non_whitespace_args(using_opengl_renderer):""" + + def construct(self): + """Check that correct number of submobjects are created per string when mixing characters with whitespace""" + separator = ", \n . \t\t" + str_part_1 = "Hello" + str_part_2 = "world" + str_part_3 = "It is" + str_part_4 = "me!" + tex = Tex( + str_part_1, str_part_2, str_part_3, str_part_4, arg_separator=separator + ) + assert len(tex) == 4 + print(len(tex[0])) + print(tex[0]) + print(len("".join((str_part_1 + separator).split()))) + print("".join((str_part_1 + separator).split())) + assert len(tex[0]) == len("".join((str_part_1 + separator).split())) + + +class Scene26(Scene): + def construct(self): + eq = MathTex("{{ a }} + {{ b }} = {{ c }}") + self.add(eq) + eq.set_color_by_tex("a", YELLOW) + + # Get inspiration from # https://docs.manim.community/en/stable/guides/using_text.html#text-with-latex diff --git a/manim/mobject/text/tex_mobject.py b/manim/mobject/text/tex_mobject.py index 5fbcf8cdaa..8a49ba2b7f 100644 --- a/manim/mobject/text/tex_mobject.py +++ b/manim/mobject/text/tex_mobject.py @@ -274,12 +274,7 @@ def __init__( self.tex_to_color_map = tex_to_color_map self.substrings_to_isolate.extend(self.tex_to_color_map.keys()) self.tex_environment = tex_environment - # Deal with the case where tex_strings contains integers instead - # of strings. - tex_strings_validated = [ - string if isinstance(string, str) else str(string) for string in tex_strings - ] - self.tex_strings = tex_strings_validated + self.tex_strings = self._prepare_tex_strings(tex_strings) self.matched_strings_and_ids: list[tuple[str, str]] = [] try: @@ -302,6 +297,19 @@ def __init__( if self.organize_left_to_right: self._organize_submobjects_left_to_right() + def _prepare_tex_strings(self, tex_strings: Iterable[str]) -> list[str]: + # Deal with the case where tex_strings contains integers instead + # of strings. + tex_strings_validated = [ + string if isinstance(string, str) else str(string) for string in tex_strings + ] + # Locate double curly bracers + tex_strings_validated_two = [] + for tex_string in tex_strings_validated: + split = re.split(r"{{|}}", tex_string) + tex_strings_validated_two.extend(split) + return [string for string in tex_strings_validated_two if len(string) > 0] + def _join_tex_strings_with_unique_deliminters( self, tex_strings: list[str], substrings_to_isolate: Iterable[str] ) -> str: From 2d6bfee66726f6331eb54fd4e609449a6ede7317 Mon Sep 17 00:00:00 2001 From: Henrik Skov Midtiby Date: Tue, 23 Dec 2025 22:08:54 +0100 Subject: [PATCH 48/56] Code cleanup --- issue/MathTexExamples.py | 721 --------------------------------------- 1 file changed, 721 deletions(-) delete mode 100644 issue/MathTexExamples.py diff --git a/issue/MathTexExamples.py b/issue/MathTexExamples.py deleted file mode 100644 index c21b9564e9..0000000000 --- a/issue/MathTexExamples.py +++ /dev/null @@ -1,721 +0,0 @@ -from __future__ import annotations - -from manim import * - - -class Scene1(Scene): - def construct(self): - formula = MathTex( - r"P(X=k)", - r" = 0.5^k (1-0.5)^{12-k}", - ) - formula.id_to_vgroup_dict["unique001"].set_color(RED) - self.add(formula) - # self.add(formula.id_to_vgroup_dict['unique001']) - - -class Scene2(Scene): - def construct(self): - formula = MathTex( - r"P(X=k) = 0.5^k (1-0.5)^{12-k}", - ).scale(1.3) - print(formula.id_to_vgroup_dict.keys()) - # formula.id_to_vgroup_dict['unique002'].set_color(RED) - # formula.set_color_by_tex("k", ORANGE) - self.add(formula) - - -class Scene3(Scene): - def construct(self): - formula = MathTex( - r"P(X=k) =", - r"\binom{12}{k}", - r"0.5^{k}", - r"(1-0.5)^{12-k}", - substrings_to_isolate=["k"], - ).scale(1.3) - for k in formula.id_to_vgroup_dict: - print(k) - for key in formula.id_to_vgroup_dict: - if key[-2:] == "ss": - formula.id_to_vgroup_dict[key].set_color(GREEN) - - # formula.id_to_vgroup_dict['unique000ss'].set_color(RED) - # formula.id_to_vgroup_dict['unique001ss'].set_color(GREEN) - # formula.id_to_vgroup_dict['unique002ss'].set_color(BLUE) - # formula.id_to_vgroup_dict['unique003ss'].set_color(YELLOW) - # formula.set_color_by_tex("k", ORANGE) - self.add(formula) - - -class Scene4(Scene): - def construct(self): - formula = MathTex( - r"a^2 + b^2 = c^2 + a^2", - ).scale(1.3) - - self.add(formula) - - -class Scene4a(Scene): - r""" - One small issue here is that the power 2 that b is raised to - is moved a bit upwards than in Scene4 and Scene4b. - This is related to adding a pair of curly braces around the - detected substrings in MathTex::_handle_match - pre_string = "{" + rf"\special{{dvisvgm:raw }}" - post_string = r"\special{dvisvgm:raw }}" - If the curly braces are not added, the issue disappears. - """ - - def construct(self): - formula = MathTex( - r"a^2 + b^2 = c^2 + a^2", - substrings_to_isolate=["a", "b"], - ).scale(1.3) - for k in formula.id_to_vgroup_dict: - print(k) - for key in formula.id_to_vgroup_dict: - if key[-2:] == "ss": - formula.id_to_vgroup_dict[key].set_color(GREEN) - - self.add(formula) - - -class Scene4b(Scene): - def construct(self): - formula = MathTex( - r"a^2 + b^2 = c^2 + a^2", - substrings_to_isolate=["c", "a"], - ).scale(1.3) - for k in formula.id_to_vgroup_dict: - print(k) - for key in formula.id_to_vgroup_dict: - if key[-2:] == "ss": - formula.id_to_vgroup_dict[key].set_color(GREEN) - - self.add(formula) - - -class Scene5(Scene): - """ - At an earlier stage, I experimented with using a regular expression to - locate substrings to isolate. I don't think this is actually needed. - """ - - def construct(self): - formula = MathTex( - r"a^2 + b^2 = c^2 + d^2 - a^2", - substrings_to_isolate=["[acd]"], - ).scale(1.3) - for k in formula.id_to_vgroup_dict: - print(k) - for key in formula.id_to_vgroup_dict: - if key[-2:] == "ss": - formula.id_to_vgroup_dict[key].set_color(GREEN) - - self.add(formula) - - -class Scene6(Scene): - def construct(self): - formula = MathTex( - r"a^2 + b^2 = c^2 + d^2 - a^2", - substrings_to_isolate=["a", "c", "d"], - ).scale(1.3) - - formula.set_color_by_tex("c", YELLOW) - formula.set_color_by_tex("a", RED) - - self.add(formula) - - -class Scene7(Scene): - def construct(self): - formula = MathTex( - r"a^2 + b^2 = c^2 + d^2 - 2 a^2", - substrings_to_isolate=["a", "c", "d"], - ).scale(1.3) - - formula.set_color_by_tex("c", GREEN) - formula.set_color_by_tex("a", RED) - - self.add(formula) - - -class Scene8(Scene): - """ - Example based on this issue: - set_color_by_tex selects wrong substring in certain contexts - https://github.com/ManimCommunity/manim/issues/3492 - """ - - def construct(self): - formula = MathTex( - r"P(X=k) =", - r"\binom{12}{k}", - r"0.5^{k}", - r"(1-0.5)^{12-k}", - substrings_to_isolate=["k", "1", "12", "0.5"], - ).scale(1.3) - - formula.set_color_by_tex("k", GREEN) - formula.set_color_by_tex("12", RED) - formula.set_color_by_tex("1", YELLOW) - formula.set_color_by_tex("0.5", BLUE_D) - self.add(formula) - - -class Scene9(Scene): - def construct(self): - t2cm = {r"\sum": BLUE, "^{n}": RED, "_{1}": GREEN, "x": YELLOW} - eq1 = MathTex(r"\sum", "^{n}", "_{1}", "x").scale(1.3) - eq2 = MathTex(r"\sum", "_{1}", "^{n}", "x").scale(1.3) - - for texstring, color in t2cm.items(): - eq1.set_color_by_tex(texstring, color) - eq2.set_color_by_tex(texstring, color) - - grp = VGroup(eq1, eq2).arrange_in_grid(2, 1) - self.add(grp) - - -class Scene10(Scene): - """ - This scene show an example of when the current implementation - of substrings_to_isolate fails, as it changes the layout of the - rendered latex equation. - """ - - def construct(self): - # TODO: This approach to highlighting \sum does not work right now. - # It changes the shape of the rendered equation. - t2cm1 = {r"\\sum": BLUE, "n": RED, "1": GREEN, "x": YELLOW} - t2cm2 = {r"\sum": BLUE, "n": RED, "1": GREEN, "x": YELLOW} - eq1 = MathTex( - r"\sum^{n}_{1} x", substrings_to_isolate=list(t2cm1.keys()) - ).scale(1.3) - eq2 = MathTex( - r"\sum_{1}^{n} x", substrings_to_isolate=list(t2cm2.keys()) - ).scale(1.3) - - for texstring, color in t2cm1.items(): - eq1.set_color_by_tex(texstring, color) - for texstring, color in t2cm2.items(): - eq2.set_color_by_tex(texstring, color) - - grp = VGroup(eq1, eq2).arrange_in_grid(2, 1) - self.add(grp) - - -class Scene10a(Scene): - """ - This scene shows a workaround to the issue in Scene10, based on - index_labels, it works, but currently the levelling is a bit different - the previous implementation of MathTex. - """ - - def construct(self): - t2cm = {"n": RED, "1": GREEN, "x": YELLOW} - eq1 = MathTex(r"\sum^{n}_{1} x", substrings_to_isolate=list(t2cm.keys())).scale( - 1.3 - ) - eq2 = MathTex(r"\sum_{1}^{n} x", substrings_to_isolate=list(t2cm.keys())).scale( - 1.3 - ) - - for texstring, color in t2cm.items(): - eq1.set_color_by_tex(texstring, color) - eq2.set_color_by_tex(texstring, color) - - grp = VGroup(eq1, eq2).arrange_in_grid(2, 1) - self.add(grp) - - # This workaround based on index_labels still work - # labels = index_labels(eq2[0]) - # self.add(labels) - eq2[0][1].set_color(BLUE) - - -class Scene11(Scene): - def construct(self): - t2cm = {"n": RED, "1": GREEN, "x": YELLOW} - eq = MathTex(r"\sum_{1}^{n} x", tex_to_color_map=t2cm).scale(1.3) - - self.add(eq) - - -class Scene12(Scene): - def construct(self): - eq = MathTex( - r"\sum_{1}^{n} x", substrings_to_isolate=["1", "n", "x"], use_svg_cache=True - ).scale(1.3) - eq.set_color_by_tex("1", YELLOW) - eq.set_color_by_tex("x", RED) - eq.set_opacity_by_tex("n", 0.5) - - self.add(eq) - - -class Scene13(Scene): - def construct(self): - matrix_elements = [[1, 2, 3]] - row = 0 - column = 2 - matrix = Matrix(matrix_elements) - print("matrix.get_columns()[column][row].tex_string") - print(matrix.get_columns()[column][row].tex_string) - self.add(matrix) - - -class Scene14(Scene): - def construct(self): - start = MathTex("2", "A", r"\to", "B").shift(UP) - end = MathTex("2", "B", r"\to", "A") - - self.add(start) - self.play(TransformMatchingTex(start, end, fade_transform_mismatches=True)) - - -class Scene15(Scene): - """ - Example taken from - TeX splitting can cause the last parts of an equation to not be displayed - https://github.com/ManimCommunity/manim/issues/2970 - - This example seems to work well. - """ - - def construct(self): - template = TexTemplate() - template.add_to_preamble(r"""\usepackage[english]{babel} - \usepackage{csquotes}\usepackage{cancel}""") - lpc_implies_polynomial = ( - MathTex( - r"s[t] = a_1 s[t-1] + a_2 s[t-2] + a_3 s[t-3] + \dots\\", - r"{{\Downarrow}}\\", - r"\text{Polynomial function}", - tex_template=template, - tex_environment="gather*", - ) - .scale(0.9) - .shift(UP * 1.5) - ) - lpc_implies_not_polynomial = ( - MathTex( - r"s[t] = a_1 s[t-1] + a_2 s[t-2] + a_3 s[t-3] + \dots\\", - r"\xcancel{ {{\Downarrow}} }\\", - r"\text{Polynomial function}", - tex_template=template, - tex_environment="gather*", - ) - .scale(0.9) - .shift(DOWN * 1.5) - ) - self.add(lpc_implies_not_polynomial, lpc_implies_polynomial) - - -class Scene16(Scene): - """ - LaTeX rendering incorrectly - https://github.com/ManimCommunity/manim/issues/2912 - - Problem is still present. - I think it is related to the fraction line being a line object in the - svg and not an object defined by bezier curves. - - Currently this fails to run as expected. - When inspecting the generated svg file, the group "unique000" that was inserted in the - .tex file is not present... - https://dvisvgm.de/Manpage/#specials - """ - - def construct(self): - preamble = r""" - %\usepackage[mathrm=sym]{unicode-math} - %\setmathfont{Fira Math} - """ - - template = TexTemplate( - preamble=preamble, tex_compiler="lualatex", output_format=".pdf" - ) - - self.add(Tex(r"$\frac{a}{b}$", tex_template=template)) - - -class Scene16a(Scene): - """ - Currently this fails to run as expected. - When inspecting the generated svg file, the group "unique000" that was inserted in the - .tex file is not present... - KeyError: unique000 - - By adding this to MathTex::_break_up_by_substrings - new_submobjects.append(self.id_to_vgroup_dict['root']) - the code still runs, even if the expected group was not found. - """ - - def construct(self): - self.add(Tex(r"$\frac{a}{b}$", tex_environment="center")) - - -class Scene16b(Scene): - """This works fine.""" - - def construct(self): - self.add(Tex(r"$\frac{a}{b}$", tex_environment=None)) - - -class Scene17(Scene): - """ - Tex splitting issue with frac numerator - https://github.com/ManimCommunity/manim/issues/2884 - - Problem is not solved at the moment. - - I don't think it is possible to make this - type of animation / transform with the current implementation, as it - requires the expression to be able to compile for each segment. - """ - - def construct(self): - n = MathTex("n").shift(LEFT) - denominator = MathTex(r"\frac{ 2 }{ n + 1 }", substrings_to_isolate="n").shift( - 2 * UP - ) - self.add(n) - self.wait(2) - self.play(TransformMatchingTex(n, denominator)) - self.wait(2) - - numerator = MathTex(r"\frac{ n + 1 }{ 2 }", substrings_to_isolate="n").shift( - 2 * DOWN - ) - self.play(TransformMatchingTex(n, numerator)) - self.wait(2) - - # This approach works fine. - fraction_right = MathTex(r"{ ", "n", r" + 1 } \over { 2 }").shift(2 * RIGHT) - self.play(TransformMatchingTex(n, fraction_right)) - self.wait(2) - - -class Scene17a(Scene): - """This seems to work fine right now.""" - - def construct(self): - n = MathTex("n").shift(LEFT) - denominator = MathTex(r"\frac{ 2 }{ ", "n", " + 1 }").shift(2 * UP) - self.add(n) - self.wait(2) - self.play(TransformMatchingTex(n, denominator)) - self.wait(2) - - numerator = MathTex(r"\frac{", "n", " + 1 }{ 2 }").shift(2 * DOWN) - self.play(TransformMatchingTex(n, numerator)) - self.wait(2) - - -class Scene18(Scene): - """ - Transforming to similar MathTex object distorts sometimes - https://github.com/ManimCommunity/manim/issues/2544 - - Seems to work ok. - """ - - def construct(self): - var = "a" - a1 = MathTex("2").shift(UP) - a2 = MathTex(var) - a3 = MathTex(var).shift(DOWN) - - self.play(Write(a1)) - self.wait() - self.play(Transform(a1, a2)) - self.wait() - self.play(Transform(a1, a3)) - self.wait() - - -class Scene19(Scene): - """ - Manim's unexpected colouring behaviour under the radical sign - https://github.com/ManimCommunity/manim/issues/1996 - - The code from the issue does not run at the moment. - AttributeError: VMobjectFromSVGPath object has no attribute 'tex_string' - - The code have been adjusted to be able to run. - """ - - def construct(self): - val_a = 3 - val_b = 2 - color_a = "#0470cf" - color_b = "#cf0492" - tex_a = Tex(str(val_a), color=color_a).move_to([-1, 2, 0]).scale(2) - tex_b = Tex(str(val_b), color=color_b).move_to([1, 2, 0]).scale(2) - - self.play(FadeIn(tex_a, tex_b)) - self.wait() - - form = MathTex( - # unique000 - r"\hat{u}= \frac{ 3 \hat{i}+ 2\hat{j}}{\sqrt{ {", - # unique001 - r"2", - # unique002 - r"^{2}+", - # unique003 - r"3", - # unique004 - r"^{2} } } } } }", - substrings_to_isolate=["2", "3"], - ).scale(1.25) - - get_pos_a = form.id_to_vgroup_dict["unique003"].get_center() - get_pos_b = form.id_to_vgroup_dict["unique001"].get_center() - - form.id_to_vgroup_dict["unique003"].set_color(color_a) - form.id_to_vgroup_dict["unique001"].set_color(color_b) - - self.play(FadeIn(form)) - self.play( - tex_a.animate.move_to(get_pos_a).match_height( - form.id_to_vgroup_dict["unique003"] - ) - ) - self.play( - tex_b.animate.move_to(get_pos_b).match_height( - form.id_to_vgroup_dict["unique001"] - ) - ) - self.wait(1) - - -class Scene19a(Scene): - """ - This seems to work fine now. - I have split the input string manually. - """ - - def construct(self): - val_a = 3 - val_b = 2 - color_a = "#f1f514" - color_b = "#cf0492" - tex_a = ( - MathTex(str(val_a).format(int), color=color_a).move_to([-1, 2, 0]).scale(2) - ) - tex_b = ( - MathTex(str(val_b).format(int), color=color_b).move_to([1, 2, 0]).scale(2) - ) - - self.play(FadeIn(tex_a, tex_b)) - self.wait() - - form = MathTex( - r"\hat{u}= \frac{ ", - "3", - r" \hat{i} + ", - "2", - r"\hat{j}}{ \sqrt{ ", - "2", - "^{2} + ", - "3", - "^{2} } } }", - ).scale(1.25) - - idx_a = [ - int(i) - for i, character in enumerate(form) - if character.tex_string == tex_a[0].tex_string - ] - idx_b = [ - int(i) - for i, character in enumerate(form) - if character.tex_string == tex_b[0].tex_string - ] - - print(idx_a) - print(idx_b) - - get_pos_a = form[idx_a[0]].get_center() - get_pos_b = form[idx_b[0]].get_center() - a1_copy = [] - b1_copy = [] - - # here I force colouring of two's and three's on the MathTex object - for i in range(len(idx_a)): - a1_copy += form[idx_a[i]].set_color(color_a) - for i in range(len(idx_b)): - b1_copy += form[idx_b[i]].set_color(color_b) - - self.play(FadeIn(form)) - self.play(tex_a.animate.move_to(get_pos_a).match_height(form[idx_a[0]])) - self.play(tex_b.animate.move_to(get_pos_b).match_height(form[idx_b[0]])) - self.wait(1) - - -class Scene20(Scene): - """ - LaTex Error in combination of MathTex and SurroundingRectangle - https://github.com/ManimCommunity/manim/issues/1907 - - The example seems to be working fine. - """ - - def construct(self): - if False: # try to write ... = \frac { u'v - uv' } {v^2} - # for boxes: └─3─┘ └─5─┘ - - text = MathTex( - r"\left( \frac{u}{v} \right)'", # 0 - "=", # 1 - r"\frac {", # 2 ⇐ error-stop at opening brace - "u' v", # 3 * - "-", # 4 - "u v'", # 5 * - "} {v^2}", # 6 ⇐ closing brace - ) # ⇑ ⇐ is here! - - else: # instead use unwanted ... = { u'v - uv' } \frac {1} {v^2} - # for boxes: └─3─┘ └─5─┘ - - text = MathTex( - r"\left( \frac{u}{v} \right)'", # 0 - "=", # 1 - r"\left( \, ", # 2 - "u' v", # 3 * - "-", # 4 - "u v'", # 5 * - r"\, \right) \frac{1}{v^2}", # 6 - ) - - box1 = SurroundingRectangle(text[3], buff=0.1) - box2 = SurroundingRectangle(text[5], buff=0.1, color=RED) - - self.play(Write(text)) - self.play(Create(box1)) - self.play(TransformFromCopy(box1, box2)) - - self.wait(5) - - -class Scene21(Scene): - r""" - LaTeX compilation error when breaking up a MathTex string by subscripts - https://github.com/ManimCommunity/manim/issues/1865 - - This currently fails. - It can be solved by adding a pair of curly braces around the - detected substrings in MathTex::_handle_match - pre_string = "{" + rf"\special{{dvisvgm:raw }}" - post_string = r"\special{dvisvgm:raw }}" - But doing that triggers and issue in Scene4a - """ - - def construct(self): - # reqeq2 = MathTex( - # r"Y_{ij} = \mu_i + \gamma_i X_{ij} + e_{ij}", substrings_to_isolate=["i"] - # ) - reqeq2 = MathTex( - r"Y_{ij} = \mu_{i} + \gamma_{i} X_{ij} + e_{ij}", - substrings_to_isolate=["i"], - ) - self.add(reqeq2) - - -class Scene21a(Scene): - """ - LaTeX compilation error when breaking up a MathTex string by subscripts - https://github.com/ManimCommunity/manim/issues/1865 - - By inserting curly braces around the objects to isolate, - the error vanishes. - """ - - def construct(self): - reqeq2 = MathTex( - r"Y_{ij} = \mu_{i} + \gamma_{i} X_{ij} + e_{ij}", - substrings_to_isolate=["i"], - ) - self.add(reqeq2) - - -class Scene22(Scene): - """ - uwezi on discord 2025-05-27 Kerning (tex vs text) - https://discord.com/channels/581738731934056449/1376977419269050460/1376998448724967454 - - In the thread it is explained that the tex_environment should - be given without the opening curly brace. - tex_environment="minipage}{20em}" - The code below seems to work fine. - """ - - def construct(self): - tex = Tex( - r""" -Lorem ipsum dolor sit amet, consectetur adipiscing elit. -Donec rhoncus eros turpis, quis ullamcorper augue pretium eget. -Nullam hendrerit massa at mauris lacinia, eget rhoncus -enim vestibulum. -""", - tex_environment="{minipage}{20em}", - ) - self.add(tex) - - -class Scene23(Scene): - def construct(self): - """Test that set_opacity_by_tex works correctly.""" - tex = MathTex("f(x) = y", substrings_to_isolate=["f(x)"]) - print(tex.matched_strings_and_ids) - tex.set_opacity_by_tex("f(x)", 0.2, 0.5) - self.add(tex) - - -class Scene24(Scene): - def construct(self): - exp1 = MathTex("a^2", "+", "b^2", "=", "c^2").shift(2 * UP) - exp2 = MathTex("a^2", "=", "c^2", "-", "b^2") - exp3 = MathTex("a", "=", r"\sqrt{", "c^2", "-", "b^2", "}").shift(2 * DOWN) - self.add(exp1) - self.wait(2) - self.play(TransformMatchingTex(exp1, exp2), run_time=5) - self.play(TransformMatchingTex(exp2, exp3), run_time=5) - self.wait(2) - - -class Scene25(Scene): - """test_tex_white_space_and_non_whitespace_args(using_opengl_renderer):""" - - def construct(self): - """Check that correct number of submobjects are created per string when mixing characters with whitespace""" - separator = ", \n . \t\t" - str_part_1 = "Hello" - str_part_2 = "world" - str_part_3 = "It is" - str_part_4 = "me!" - tex = Tex( - str_part_1, str_part_2, str_part_3, str_part_4, arg_separator=separator - ) - assert len(tex) == 4 - print(len(tex[0])) - print(tex[0]) - print(len("".join((str_part_1 + separator).split()))) - print("".join((str_part_1 + separator).split())) - assert len(tex[0]) == len("".join((str_part_1 + separator).split())) - - -class Scene26(Scene): - def construct(self): - eq = MathTex("{{ a }} + {{ b }} = {{ c }}") - self.add(eq) - eq.set_color_by_tex("a", YELLOW) - - -# Get inspiration from -# https://docs.manim.community/en/stable/guides/using_text.html#text-with-latex From 3e99987e56eeee15a80145856b96b52c54c8f005 Mon Sep 17 00:00:00 2001 From: Henrik Skov Midtiby Date: Tue, 23 Dec 2025 22:10:04 +0100 Subject: [PATCH 49/56] Code cleanup --- issue/issue3492.py | 49 ---------------------------------------------- 1 file changed, 49 deletions(-) delete mode 100644 issue/issue3492.py diff --git a/issue/issue3492.py b/issue/issue3492.py deleted file mode 100644 index 00bc5d28e3..0000000000 --- a/issue/issue3492.py +++ /dev/null @@ -1,49 +0,0 @@ -from __future__ import annotations - -from manim import * - - -class ExampleScene(Scene): - def construct(self): - formula = MathTex( - r"P(X=k) = ", - "\\binom{12}{k} ", - r"0.5^k", - r"(1-0.5)^{12-k}", - substrings_to_isolate=["k"], - ).scale(1.3) - self.play(formula.animate.set_color_by_tex("k", ORANGE)) - - -class ExampleScene2(Scene): - def construct(self): - formula = MathTex( - r"P(X=k) = 0.5^k (1-0.5)^{12-k}", - ).scale(1.3) - print(formula.id_to_vgroup_dict) - # formula.id_to_vgroup_dict['unique002'].set_color(RED) - # formula.set_color_by_tex("k", ORANGE) - self.add(formula) - - -class ExampleScene3(Scene): - def construct(self): - formula = MathTex( - r"P(X=k) =", - r"\binom{12}{k}", - r"0.5^{k}", - r"(1-0.5)^{12-k}", - substrings_to_isolate=["k"], - ).scale(1.3) - for k in formula.id_to_vgroup_dict: - print(k) - for key in formula.id_to_vgroup_dict: - if key[-2:] == "ss": - formula.id_to_vgroup_dict[key].set_color(GREEN) - - # formula.id_to_vgroup_dict['unique000ss'].set_color(RED) - # formula.id_to_vgroup_dict['unique001ss'].set_color(GREEN) - # formula.id_to_vgroup_dict['unique002ss'].set_color(BLUE) - # formula.id_to_vgroup_dict['unique003ss'].set_color(YELLOW) - # formula.set_color_by_tex("k", ORANGE) - self.add(formula) From ba58cd0f5a609b80ee06a70554f3cc920ff29941 Mon Sep 17 00:00:00 2001 From: Henrik Skov Midtiby Date: Tue, 23 Dec 2025 22:18:28 +0100 Subject: [PATCH 50/56] Rollback a few changes --- manim/animation/transform_matching_parts.py | 7 +------ manim/mobject/text/tex_mobject.py | 19 ++++++++++++++++++- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/manim/animation/transform_matching_parts.py b/manim/animation/transform_matching_parts.py index 74093f128f..03305201f1 100644 --- a/manim/animation/transform_matching_parts.py +++ b/manim/animation/transform_matching_parts.py @@ -294,9 +294,4 @@ def get_mobject_parts(mobject: Mobject) -> list[Mobject]: @staticmethod def get_mobject_key(mobject: Mobject) -> str: - # Ugly hack to make the following test pass - # test_TransformMatchingTex_FadeTransformMismatches_NothingToFade - try: - return mobject.tex_string - except Exception: - return "" + return mobject.tex_string diff --git a/manim/mobject/text/tex_mobject.py b/manim/mobject/text/tex_mobject.py index 8a49ba2b7f..1237ca4ec7 100644 --- a/manim/mobject/text/tex_mobject.py +++ b/manim/mobject/text/tex_mobject.py @@ -27,11 +27,12 @@ import re from collections.abc import Iterable from functools import reduce +from textwrap import dedent from typing import Any from typing_extensions import Self -from manim import config +from manim import config, logger from manim.constants import * from manim.mobject.geometry.line import Line from manim.mobject.svg.svg_mobject import SVGMobject @@ -274,6 +275,7 @@ def __init__( self.tex_to_color_map = tex_to_color_map self.substrings_to_isolate.extend(self.tex_to_color_map.keys()) self.tex_environment = tex_environment + self.brace_notation_split_occurred = False self.tex_strings = self._prepare_tex_strings(tex_strings) self.matched_strings_and_ids: list[tuple[str, str]] = [] @@ -291,6 +293,19 @@ def __init__( self.tex_string = self.arg_separator.join(self.tex_strings) self._break_up_by_substrings() except ValueError as compilation_error: + if self.brace_notation_split_occurred: + logger.error( + dedent( + """\ + A group of double braces, {{ ... }}, was detected in + your string. Manim splits TeX strings at the double + braces, which might have caused the current + compilation error. If you didn't use the double brace + split intentionally, add spaces between the braces to + avoid the automatic splitting: {{ ... }} --> { { ... } }. + """, + ), + ) raise compilation_error self.set_color_by_tex_to_color_map(self.tex_to_color_map) @@ -308,6 +323,8 @@ def _prepare_tex_strings(self, tex_strings: Iterable[str]) -> list[str]: for tex_string in tex_strings_validated: split = re.split(r"{{|}}", tex_string) tex_strings_validated_two.extend(split) + if len(tex_strings_validated_two) > len(tex_strings_validated): + self.brace_notation_split_occurred = True return [string for string in tex_strings_validated_two if len(string) > 0] def _join_tex_strings_with_unique_deliminters( From 4025934c3750d1b38b922682322cf7ab567d2b50 Mon Sep 17 00:00:00 2001 From: Henrik Skov Midtiby Date: Tue, 23 Dec 2025 22:19:19 +0100 Subject: [PATCH 51/56] Code cleanup --- manim/mobject/svg/svg_mobject.py | 1 - 1 file changed, 1 deletion(-) diff --git a/manim/mobject/svg/svg_mobject.py b/manim/mobject/svg/svg_mobject.py index 6318db79d0..62ea4ddc92 100644 --- a/manim/mobject/svg/svg_mobject.py +++ b/manim/mobject/svg/svg_mobject.py @@ -312,7 +312,6 @@ def get_mobjects_from( mob = self.get_mob_from_shape_element(element) if mob is not None: result.append(mob) - parent_name = vgroup_stack[-2] for parent_name in vgroup_stack[:-1]: vgroups[parent_name].add(mob) except Exception as e: From e928a0cde42a92d4ac6b5841ffd8fee8ae49c74a Mon Sep 17 00:00:00 2001 From: Henrik Skov Midtiby Date: Tue, 13 Jan 2026 09:12:40 +0100 Subject: [PATCH 52/56] Adjust paths the generated artefacts in tests that rely on MathTex --- .../logs_data/bad_tex_scene_BadTex.txt | 4 ++-- tests/module/mobject/text/test_texmobject.py | 18 +++++++++--------- tests/opengl/test_texmobject_opengl.py | 4 ++-- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/tests/control_data/logs_data/bad_tex_scene_BadTex.txt b/tests/control_data/logs_data/bad_tex_scene_BadTex.txt index 02c8813969..06a36fb5a1 100644 --- a/tests/control_data/logs_data/bad_tex_scene_BadTex.txt +++ b/tests/control_data/logs_data/bad_tex_scene_BadTex.txt @@ -1,8 +1,8 @@ {"levelname": "INFO", "module": "logger_utils", "message": "Log file will be saved in <>"} {"levelname": "INFO", "module": "tex_file_writing", "message": "Writing <> to <>"} {"levelname": "ERROR", "module": "tex_file_writing", "message": "LaTeX compilation error: LaTeX Error: File `notapackage.sty' not found.\n"} -{"levelname": "ERROR", "module": "tex_file_writing", "message": "Context of error: \n\\documentclass[preview]{standalone}\n-> \\usepackage{notapackage}\n\\begin{document}\n\\begin{center}\n\\frac{1}{0}\n"} +{"levelname": "ERROR", "module": "tex_file_writing", "message": "Context of error: \n\\documentclass[preview]{standalone}\n-> \\usepackage{notapackage}\n\\begin{document}\n\\begin{center}\n\\special{dvisvgm:raw }\\frac{1}{0}\\special{dvisvgm:raw }\n"} {"levelname": "INFO", "module": "tex_file_writing", "message": "You do not have package notapackage.sty installed."} {"levelname": "INFO", "module": "tex_file_writing", "message": "Install notapackage.sty it using your LaTeX package manager, or check for typos."} {"levelname": "ERROR", "module": "tex_file_writing", "message": "LaTeX compilation error: Emergency stop.\n"} -{"levelname": "ERROR", "module": "tex_file_writing", "message": "Context of error: \n\\documentclass[preview]{standalone}\n-> \\usepackage{notapackage}\n\\begin{document}\n\\begin{center}\n\\frac{1}{0}\n"} +{"levelname": "ERROR", "module": "tex_file_writing", "message": "Context of error: \n\\documentclass[preview]{standalone}\n-> \\usepackage{notapackage}\n\\begin{document}\n\\begin{center}\n\\special{dvisvgm:raw }\\frac{1}{0}\\special{dvisvgm:raw }\n"} diff --git a/tests/module/mobject/text/test_texmobject.py b/tests/module/mobject/text/test_texmobject.py index c47b30c101..f78bb5212d 100644 --- a/tests/module/mobject/text/test_texmobject.py +++ b/tests/module/mobject/text/test_texmobject.py @@ -10,7 +10,7 @@ def test_MathTex(config): MathTex("a^2 + b^2 = c^2") - assert Path(config.media_dir, "Tex", "e4be163a00cf424f.svg").exists() + assert Path(config.media_dir, "Tex", "05bb0a41ed575f00.svg").exists() def test_SingleStringMathTex(config): @@ -29,7 +29,7 @@ def test_double_braces_testing(text_input, length_sub): def test_tex(config): Tex("The horse does not eat cucumber salad.") - assert Path(config.media_dir, "Tex", "c3945e23e546c95a.svg").exists() + assert Path(config.media_dir, "Tex", "5384b41741a246bd.svg").exists() def test_tex_temp_directory(tmpdir, monkeypatch): @@ -42,12 +42,12 @@ def test_tex_temp_directory(tmpdir, monkeypatch): with tempconfig({"media_dir": "media"}): Tex("The horse does not eat cucumber salad.") assert Path("media", "Tex").exists() - assert Path("media", "Tex", "c3945e23e546c95a.svg").exists() + assert Path("media", "Tex", "5384b41741a246bd.svg").exists() def test_percent_char_rendering(config): Tex(r"\%") - assert Path(config.media_dir, "Tex", "4a583af4d19a3adf.tex").exists() + assert Path(config.media_dir, "Tex", "32509dd0ea993961.tex").exists() def test_tex_whitespace_arg(): @@ -218,11 +218,11 @@ def test_tex_garbage_collection(tmpdir, monkeypatch, config): Path(tmpdir, "media").mkdir() config.media_dir = "media" - tex_without_log = Tex("Hello World!") # d771330b76d29ffb.tex - assert Path("media", "Tex", "d771330b76d29ffb.tex").exists() - assert not Path("media", "Tex", "d771330b76d29ffb.log").exists() + tex_without_log = Tex("Hello World!") # 058a4e242c57db6d.tex + assert Path("media", "Tex", "058a4e242c57db6d.tex").exists() + assert not Path("media", "Tex", "058a4e242c57db6d.log").exists() config.no_latex_cleanup = True - tex_with_log = Tex("Hello World, again!") # da27670a37b08799.tex - assert Path("media", "Tex", "da27670a37b08799.log").exists() + tex_with_log = Tex("Hello World, again!") # 45b4e7819cc20cb1.tex + assert Path("media", "Tex", "45b4e7819cc20cb1.log").exists() diff --git a/tests/opengl/test_texmobject_opengl.py b/tests/opengl/test_texmobject_opengl.py index e9826f9d8f..618bdb4a4f 100644 --- a/tests/opengl/test_texmobject_opengl.py +++ b/tests/opengl/test_texmobject_opengl.py @@ -9,7 +9,7 @@ def test_MathTex(config, using_opengl_renderer): MathTex("a^2 + b^2 = c^2") - assert Path(config.media_dir, "Tex", "e4be163a00cf424f.svg").exists() + assert Path(config.media_dir, "Tex", "05bb0a41ed575f00.svg").exists() def test_SingleStringMathTex(config, using_opengl_renderer): @@ -28,7 +28,7 @@ def test_double_braces_testing(using_opengl_renderer, text_input, length_sub): def test_tex(config, using_opengl_renderer): Tex("The horse does not eat cucumber salad.") - assert Path(config.media_dir, "Tex", "c3945e23e546c95a.svg").exists() + assert Path(config.media_dir, "Tex", "5384b41741a246bd.svg").exists() def test_tex_whitespace_arg(using_opengl_renderer): From bbdd0bbea798dccb216ed1c6b76cb0f06ab25b0a Mon Sep 17 00:00:00 2001 From: Henrik Skov Midtiby Date: Tue, 13 Jan 2026 09:39:35 +0100 Subject: [PATCH 53/56] Added a remark to the using text guide on enclosing snippets in curly braces for substrings_to_isolate to work --- docs/source/guides/using_text.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/source/guides/using_text.rst b/docs/source/guides/using_text.rst index a71b29ec81..739510621c 100644 --- a/docs/source/guides/using_text.rst +++ b/docs/source/guides/using_text.rst @@ -424,7 +424,7 @@ may be expected. To color only ``x`` yellow, we have to do the following: class CorrectLaTeXSubstringColoring(Scene): def construct(self): equation = MathTex( - r"e^x = x^0 + x^1 + \frac{1}{2} x^2 + \frac{1}{6} x^3 + \cdots + \frac{1}{n!} x^n + \cdots", + r"e^{x} = x^0 + x^1 + \frac{1}{2} x^2 + \frac{1}{6} x^3 + \cdots + \frac{1}{n!} x^n + \cdots", substrings_to_isolate="x" ) equation.set_color_by_tex("x", YELLOW) @@ -434,6 +434,8 @@ By setting ``substrings_to_isolate`` to ``x``, we split up the :class:`~.MathTex` into substrings automatically and isolate the ``x`` components into individual substrings. Only then can :meth:`~.set_color_by_tex` be used to achieve the desired result. +If one of the ``substrings_to_isolate`` is in a sub or superscript, it needs +to be enclosed by curly brackets. Note that Manim also supports a custom syntax that allows splitting a TeX string into substrings easily: simply enclose parts of your formula From 800cfa6b23ced93cbe348ef42ed4631d3a649961 Mon Sep 17 00:00:00 2001 From: Henrik Skov Midtiby Date: Tue, 13 Jan 2026 09:40:54 +0100 Subject: [PATCH 54/56] Added space around the numerator argument to frac to avoid having double curly braces in the example. This would otherwise trigger MathTex to split the string at that location. --- manim/mobject/table.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/manim/mobject/table.py b/manim/mobject/table.py index 6aa806277f..14cb7db98f 100644 --- a/manim/mobject/table.py +++ b/manim/mobject/table.py @@ -1078,11 +1078,11 @@ def construct(self): [[0,30,45,60,90], [90,60,45,30,0]], col_labels=[ - MathTex(r"\frac{\sqrt{0}}{2}"), - MathTex(r"\frac{\sqrt{1}}{2}"), - MathTex(r"\frac{\sqrt{2}}{2}"), - MathTex(r"\frac{\sqrt{3}}{2}"), - MathTex(r"\frac{\sqrt{4}}{2}")], + MathTex(r"\frac{ \sqrt{0} }{2}"), + MathTex(r"\frac{ \sqrt{1} }{2}"), + MathTex(r"\frac{ \sqrt{2} }{2}"), + MathTex(r"\frac{ \sqrt{3} }{2}"), + MathTex(r"\frac{ \sqrt{4} }{2}")], row_labels=[MathTex(r"\sin"), MathTex(r"\cos")], h_buff=1, element_to_mobject_config={"unit": r"^{\circ}"}) From 09b6cd06680be5103a8e6f8bc981925f30bc36df Mon Sep 17 00:00:00 2001 From: Henrik Skov Midtiby Date: Mon, 26 Jan 2026 08:30:35 +0100 Subject: [PATCH 55/56] Log errors properly and display some information about the errors and their context. --- manim/mobject/svg/svg_mobject.py | 2 +- manim/mobject/text/tex_mobject.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/manim/mobject/svg/svg_mobject.py b/manim/mobject/svg/svg_mobject.py index 62ea4ddc92..c296130a27 100644 --- a/manim/mobject/svg/svg_mobject.py +++ b/manim/mobject/svg/svg_mobject.py @@ -315,7 +315,7 @@ def get_mobjects_from( for parent_name in vgroup_stack[:-1]: vgroups[parent_name].add(mob) except Exception as e: - print(e) + logger.error(f"Exception occurred in 'get_mobjects_from'. Details: {e}") return result, vgroups diff --git a/manim/mobject/text/tex_mobject.py b/manim/mobject/text/tex_mobject.py index 96358d4c68..cec7a697ef 100644 --- a/manim/mobject/text/tex_mobject.py +++ b/manim/mobject/text/tex_mobject.py @@ -407,8 +407,10 @@ def _break_up_by_substrings(self) -> Self: mtp.tex_string = tex_string mtp.add(*self.id_to_vgroup_dict[tex_string_id].submobjects) new_submobjects.append(mtp) - except KeyError as e: - print(f"KeyError: {e}") + except KeyError: + logger.error( + f"MathTex: Could not find SVG group for tex part '{tex_string}' (id: {tex_string_id}). Using fallback to root group." + ) new_submobjects.append(self.id_to_vgroup_dict["root"]) self.submobjects = new_submobjects return self From 114c013cab0b6e27fbc6059933bbc8c45eaa1ec9 Mon Sep 17 00:00:00 2001 From: Henrik Skov Midtiby Date: Mon, 26 Jan 2026 08:31:37 +0100 Subject: [PATCH 56/56] Code refactoring as suggested by Benjamin --- manim/mobject/text/tex_mobject.py | 32 ++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/manim/mobject/text/tex_mobject.py b/manim/mobject/text/tex_mobject.py index cec7a697ef..18546bce89 100644 --- a/manim/mobject/text/tex_mobject.py +++ b/manim/mobject/text/tex_mobject.py @@ -40,6 +40,8 @@ from ..opengl.opengl_compatibility import ConvertToOpenGL +MATHTEX_SUBSTRING = "substring" + class SingleStringMathTex(SVGMobject): """Elementary building block for rendering text with LaTeX. @@ -385,13 +387,35 @@ def _handle_match(self, ssIdx: int, first_match: re.Match) -> tuple[str, str]: pre_match = first_match.group(1) matched_string = first_match.group(2) post_match = first_match.group(3) - pre_string = rf"\special{{dvisvgm:raw }}" + pre_string = ( + rf"\special{{dvisvgm:raw }}" + ) post_string = r"\special{dvisvgm:raw }" - self.matched_strings_and_ids.append((matched_string, f"unique{ssIdx:03d}ss")) + self.matched_strings_and_ids.append( + (matched_string, f"unique{ssIdx:03d}{MATHTEX_SUBSTRING}") + ) processed_string = pre_match + pre_string + matched_string + post_string unprocessed_string = post_match return processed_string, unprocessed_string + @property + def _substring_matches(self) -> list[tuple[str, str]]: + """Return only the 'ss' (substring_to_isolate) matches.""" + return [ + (tex, id_) + for tex, id_ in self.matched_strings_and_ids + if id_.endswith(MATHTEX_SUBSTRING) + ] + + @property + def _main_matches(self) -> list[tuple[str, str]]: + """Return only the main tex_string matches.""" + return [ + (tex, id_) + for tex, id_ in self.matched_strings_and_ids + if not id_.endswith(MATHTEX_SUBSTRING) + ] + def _break_up_by_substrings(self) -> Self: """ Reorganize existing submobjects one layer @@ -400,9 +424,7 @@ def _break_up_by_substrings(self) -> Self: """ new_submobjects: list[VMobject] = [] try: - for tex_string, tex_string_id in self.matched_strings_and_ids: - if tex_string_id[-2:] == "ss": - continue + for tex_string, tex_string_id in self._main_matches: mtp = MathTexPart() mtp.tex_string = tex_string mtp.add(*self.id_to_vgroup_dict[tex_string_id].submobjects)