Skip to content

Conversation

@henrikmidtiby
Copy link
Contributor

@henrikmidtiby henrikmidtiby commented Dec 17, 2025

Overview: What does this pull request change?

This PR changes a large part of the implementation of the MathTex class.
The reason is to deal with the issues related to splitting the tex string and then try to match the generated objects with parts of the original tex string.
The idea for the PR is based on this discussion on discord.
https://discord.com/channels/581738731934056449/1406977902788218892

Motivation and Explanation: Why and how do your changes improve the library?

This PR changes how MathTex objects are created / rendered.
To illustrate the changes in the process, please look at the following example
of calling MathTex.

exp1 = MathTex("{a}^2+{b}^2", 
               "=", 
               "{c}^2", 
               substrings_to_isolate=["a", "b", "c"])

The first step in rendering the MathTex equation, is to join the three separate strings to a single string and add some markup to the string that will allow manim to extract certain parts of the equation afterwards. Each of the three input strings are put in the following markup.

\special{dvisvgm:raw <g id='unique000'>} < input string > \special{dvisvgm:raw </g>}

This ends up looking like the following:

\special{dvisvgm:raw <g id='unique000'>}{a}^2+{b}^2 \special{dvisvgm:raw </g>}
\special{dvisvgm:raw <g id='unique001'>}= \special{dvisvgm:raw </g>}
\special{dvisvgm:raw <g id='unique002'>}{c}^2\special{dvisvgm:raw </g>}

This allows us to access a named group in the formed svg file corresponding to each of the three tex strings.
For each string, the elements of substrings_to_isolate is also located, and marked up with a similar markup.
E.g. the a in the equation ends up like this, where the element is placed in the named group 'unique000ss'.

\special{dvisvgm:raw <g id='unique000ss'>}a\special{dvisvgm:raw </g>}

All there marked up strings are joined to the following string (newlines were added to increase readability).

\special{dvisvgm:raw <g id='unique000'>}{
\special{dvisvgm:raw <g id='unique000ss'>}a
\special{dvisvgm:raw </g>}}^2+{
\special{dvisvgm:raw <g id='unique001ss'>}b
\special{dvisvgm:raw </g>}}^2 
\special{dvisvgm:raw </g>}
\special{dvisvgm:raw <g id='unique001'>}= 
\special{dvisvgm:raw </g>}
\special{dvisvgm:raw <g id='unique002'>}{
\special{dvisvgm:raw <g id='unique002ss'>}c
\special{dvisvgm:raw </g>}}^2
\special{dvisvgm:raw </g>}

This string is then saved to a tex file and converted to first a .dvi and then a .svg file using the latex and dvisvgm command line tools.
Finally the svg file is loaded by a SVGMobject from which the named groups can be accessed as demonstrated here.

exp1.scale(2)
exp1.set_color_by_tex("a", BLUE)
exp1[2].set_color(ORANGE)
self.add(exp1)

The full code for this example is provided here

class Scene27(Scene):
    def construct(self):
        exp1 = MathTex("{a}^2+{b}^2", 
                       "=", 
                       "{c}^2", 
                       substrings_to_isolate=["a", "b", "c"])
        exp1.scale(2)
        exp1.set_color_by_tex("a", BLUE)
        exp1[2].set_color(ORANGE)
        self.add(exp1)

and it renders as follows
Scene27_ManimCE_v0 19 1

Links to added or changed documentation pages

Further Information and Comments

This PR is an extension of #4473 "Maintain groups when importing svg files"

Reviewer Checklist

  • The PR title is descriptive enough for the changelog, and the PR is labeled correctly
  • If applicable: newly added non-private functions and classes have a docstring including a short summary and a PARAMETERS section
  • If applicable: newly added functions and classes are tested

@henrikmidtiby
Copy link
Contributor Author

And here are some example code and the generated output.

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)

        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)
ExampleScene8_ManimCE_v0 19 1
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)
ExampleScene11_ManimCE_v0 19 1

@henrikmidtiby
Copy link
Contributor Author

I have located a minor issue related to the "Using text" guide.
https://docs.manim.community/en/stable/guides/using_text.html#substrings-and-parts

In the example IncorrectLaTeXSubstringColoring, the equation is no longer colored as in the current documentation.

@henrikmidtiby henrikmidtiby marked this pull request as ready for review January 13, 2026 08:55
@henrikmidtiby henrikmidtiby linked an issue Jan 13, 2026 that may be closed by this pull request
@henrikmidtiby henrikmidtiby added pr:bugfix Bug fix for use in PRs solving a specific issue:bug refactor Refactor or redesign of existing code maintenance refactoring, typos, removing clutter/dead code, and other code quality improvements issue:bug Something isn't working... For use in issues labels Jan 13, 2026
Copy link
Member

@behackl behackl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is great and I love the much more robust implementation. Kudos and thank you!

Here are some more suggestions:

  • It might be nice to have some sort of validation in SVGMobject.__init__ that checks whether all ids from the SVG are contained in the id map at the end of initialising (and log warnings if some are missing?)
  • The PR description should mention removal/renaming of get_parts_by_tex, index_of_part_by_tex

Additionally, my review clanker suggests adding two properties to MathTex:

@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("ss")]

@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("ss")]

in order to make break_up_by_substring more readable,

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)
    new_submobjects.append(mtp)

Not sure whether that is worth it, and/or whether 'ss' should perhaps be made more explicit (I'd like that) -- but other than these I don't really have any substantial feedback. I like it.

Left a few more inline comments, please do take a look!

: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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we attempt to strengthen the parser to make this not necessary? Perhaps not at first.

for parent_name in vgroup_stack[:-1]:
vgroups[parent_name].add(mob)
except Exception as e:
print(e)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logger.error with a bit more context?

mtp.add(*self.id_to_vgroup_dict[tex_string_id].submobjects)
new_submobjects.append(mtp)
except KeyError as e:
print(f"KeyError: {e}")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, more context + logger?

Suggested change
print(f"KeyError: {e}")
logger.error(f"MathTex: Could not find SVG group for tex part '{tex_string}' (id: {tex_string_id}). Using fallback to root group.")

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

issue:bug Something isn't working... For use in issues maintenance refactoring, typos, removing clutter/dead code, and other code quality improvements pr:bugfix Bug fix for use in PRs solving a specific issue:bug refactor Refactor or redesign of existing code

Projects

Status: 🆕 New

2 participants