mirror of
https://github.com/ManimCommunity/manim.git
synced 2026-06-22 10:01:47 +00:00
Rewrite MathTex to make it more robust regarding splitting (#4515)
* Extracted the method get_mob_from_shape_element * Moved more functionality to get_mob_from_shape_element * More cleanup * Parse the svg file while maintaining the group structure. * Make the svg groups available * Handle PERF401 issue * [pre-commit.ci] pre-commit autoupdate (#4506) 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> * Added an example of the issue * Experimenting with coloring elements from the latex equation * ... * Regular expression can now match more than one object * Process the string by applying the substrings in the order they match * Code refactoring and added type annotations * ... * Added a lot of test cases * More examples * More examples * Use matched_strings_and_ids to simplify existing methods * Remove unused code * Update get_part_by_tex to use matched_strings_and_ids * This is required for test_MathTable to pass * Ensure that self.texstring is set. * Added more examples from exising issues in the github repo * Ensure that latex groups are maintained by adding an additional pair of curly braces around the extracted part * ExampleScene -> Scene * Added comment * _break_up_by_substrings * Refactor code * Added comment to example * Handle integer inputs well. * Expose the original tex_string * Do not treat the content of substrings_to_isolate as regular expressions. * Updated examples * Update examples * Fix SVMobject caching issue. * Remove traces from brace_notation_split_occurred * Simplify MathTex::_break_up_by_substrings * Fix small issue in tex that in some cases moved elements a tiny bit around * No use of regular expressions for locate substrings. * Updated notes to the set of test cases * Handle issues with the center environment. * Add example * Fix issue with rectangles (e.g. from sqrt) * ConvertToOpenGL * Reduce the number of nesting levels. * Use the specified arg_seperator * Deal with the double curly brace markup * Code cleanup * Code cleanup * Rollback a few changes * Code cleanup * Adjust paths the generated artefacts in tests that rely on MathTex * Added a remark to the using text guide on enclosing snippets in curly braces for substrings_to_isolate to work * 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. * Log errors properly and display some information about the errors and their context. * Code refactoring as suggested by Benjamin --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Benjamin Hackl <devel@benjamin-hackl.at>
This commit is contained in:
parent
6e9ec60b6f
commit
357bb3fbba
7 changed files with 248 additions and 138 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -21,12 +21,12 @@ from ..geometry.arc import Circle
|
|||
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"]
|
||||
|
||||
|
||||
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:
|
||||
|
|
@ -127,6 +127,7 @@ class SVGMobject(VMobject, metaclass=ConvertToOpenGL):
|
|||
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
|
||||
|
||||
|
|
@ -170,6 +171,7 @@ class SVGMobject(VMobject, metaclass=ConvertToOpenGL):
|
|||
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()
|
||||
|
|
@ -203,8 +205,9 @@ class SVGMobject(VMobject, metaclass=ConvertToOpenGL):
|
|||
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 +261,9 @@ class SVGMobject(VMobject, metaclass=ConvertToOpenGL):
|
|||
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
|
||||
|
|
@ -267,36 +272,77 @@ class SVGMobject(VMobject, metaclass=ConvertToOpenGL):
|
|||
The parsed SVG file.
|
||||
"""
|
||||
result: list[VMobject] = []
|
||||
for shape in svg.elements():
|
||||
# can we combine the two continue cases into one?
|
||||
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)
|
||||
if isinstance(shape, se.Transformable) and shape.apply:
|
||||
self.handle_transform(mob, shape.transform)
|
||||
result.append(mob)
|
||||
return result
|
||||
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
|
||||
vg = VGroup()
|
||||
vgroup_names.append(group_name)
|
||||
vgroup_stack.append(group_name)
|
||||
vgroups[group_name] = vg
|
||||
|
||||
if isinstance(element, (se.Group, se.Use)):
|
||||
stack.extend((subelement, depth + 1) for subelement in element[::-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)
|
||||
for parent_name in vgroup_stack[:-1]:
|
||||
vgroups[parent_name].add(mob)
|
||||
except Exception as e:
|
||||
logger.error(f"Exception occurred in 'get_mobjects_from'. Details: {e}")
|
||||
|
||||
return result, vgroups
|
||||
|
||||
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
|
||||
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
|
||||
def handle_transform(mob: VMobject, matrix: se.Matrix) -> VMobject:
|
||||
|
|
|
|||
|
|
@ -1078,11 +1078,11 @@ class IntegerTable(Table):
|
|||
[[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}"})
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ r"""Mobjects representing text rendered using LaTeX.
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
from manim.utils.color import BLACK, ManimColor, ParsableManimColor
|
||||
from manim.utils.color import BLACK, ParsableManimColor
|
||||
|
||||
__all__ = [
|
||||
"SingleStringMathTex",
|
||||
|
|
@ -23,10 +23,9 @@ __all__ = [
|
|||
]
|
||||
|
||||
|
||||
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, Self
|
||||
|
|
@ -39,6 +38,10 @@ from manim.mobject.types.vectorized_mobject import VGroup, VMobject
|
|||
from manim.utils.tex import TexTemplate
|
||||
from manim.utils.tex_file_writing import tex_to_svg_file
|
||||
|
||||
from ..opengl.opengl_compatibility import ConvertToOpenGL
|
||||
|
||||
MATHTEX_SUBSTRING = "substring"
|
||||
|
||||
|
||||
class SingleStringMathTex(SVGMobject):
|
||||
"""Elementary building block for rendering text with LaTeX.
|
||||
|
|
@ -264,22 +267,30 @@ class MathTex(SingleStringMathTex):
|
|||
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 = self._break_up_tex_strings(tex_strings)
|
||||
self.tex_strings = self._prepare_tex_strings(tex_strings)
|
||||
self.matched_strings_and_ids: list[tuple[str, str]] = []
|
||||
|
||||
try:
|
||||
joined_string = self._join_tex_strings_with_unique_deliminters(
|
||||
self.tex_strings, self.substrings_to_isolate
|
||||
)
|
||||
super().__init__(
|
||||
self.arg_separator.join(self.tex_strings),
|
||||
joined_string,
|
||||
tex_environment=self.tex_environment,
|
||||
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:
|
||||
|
|
@ -301,36 +312,109 @@ class MathTex(SingleStringMathTex):
|
|||
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
|
||||
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
|
||||
]
|
||||
tex_strings_combined = sum(tex_strings_brace_splitted, [])
|
||||
if len(tex_strings_combined) > pre_split_length:
|
||||
# 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)
|
||||
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]
|
||||
|
||||
# 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(),
|
||||
def _join_tex_strings_with_unique_deliminters(
|
||||
self, tex_strings: list[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 <g id='unique{idx:03d}'>}}"
|
||||
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
|
||||
if idx < len(tex_strings) - 1:
|
||||
string_part += self.arg_separator
|
||||
string_part += r"\special{dvisvgm:raw </g>}"
|
||||
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"(.*?)({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))
|
||||
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 <g id='unique{ssIdx:03d}{MATHTEX_SUBSTRING}'>}}"
|
||||
)
|
||||
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]
|
||||
post_string = r"\special{dvisvgm:raw </g>}"
|
||||
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:
|
||||
"""
|
||||
|
|
@ -339,51 +423,32 @@ class MathTex(SingleStringMathTex):
|
|||
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,
|
||||
try:
|
||||
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)
|
||||
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."
|
||||
)
|
||||
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
|
||||
new_submobjects.append(self.id_to_vgroup_dict["root"])
|
||||
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
|
||||
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
|
||||
) -> 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(
|
||||
|
|
@ -409,22 +474,18 @@ class MathTex(SingleStringMathTex):
|
|||
"""
|
||||
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:
|
||||
|
|
@ -433,16 +494,17 @@ class MathTex(SingleStringMathTex):
|
|||
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())
|
||||
|
||||
|
||||
class MathTexPart(VMobject, metaclass=ConvertToOpenGL):
|
||||
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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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 <g id='unique000'>}\\frac{1}{0}\\special{dvisvgm:raw </g>}\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 <g id='unique000'>}\\frac{1}{0}\\special{dvisvgm:raw </g>}\n"}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ from manim import MathTex, SingleStringMathTex, Tex, TexTemplate, tempconfig
|
|||
|
||||
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(exist_ok=True)
|
||||
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()
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ from manim import MathTex, SingleStringMathTex, Tex
|
|||
|
||||
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):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue