Merge pull request #1943 from 3b1b/string-mobject-refactor

StringMobject refactor
This commit is contained in:
Grant Sanderson 2022-12-23 17:11:30 -08:00 committed by GitHub
commit fcff44a66b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 1027 additions and 749 deletions

View file

@ -70,7 +70,7 @@ AnimatingMethods
class AnimatingMethods(Scene):
def construct(self):
grid = Tex(r"\pi").get_grid(10, 10, height=4)
grid = OldTex(r"\pi").get_grid(10, 10, height=4)
self.add(grid)
# You can animate the application of mobject methods with the
@ -192,16 +192,16 @@ TexTransformExample
# each of these strings. For example, the Tex mobject
# below will have 5 subjects, corresponding to the
# expressions [A^2, +, B^2, =, C^2]
Tex("A^2", "+", "B^2", "=", "C^2"),
OldTex("A^2", "+", "B^2", "=", "C^2"),
# Likewise here
Tex("A^2", "=", "C^2", "-", "B^2"),
OldTex("A^2", "=", "C^2", "-", "B^2"),
# Alternatively, you can pass in the keyword argument
# "isolate" with a list of strings that should be out as
# their own submobject. So the line below is equivalent
# to the commented out line below it.
Tex("A^2 = (C + B)(C - B)", isolate=["A^2", *to_isolate]),
# Tex("A^2", "=", "(", "C", "+", "B", ")", "(", "C", "-", "B", ")"),
Tex("A = \\sqrt{(C + B)(C - B)}", isolate=["A", *to_isolate])
OldTex("A^2 = (C + B)(C - B)", isolate=["A^2", *to_isolate]),
# OldTex("A^2", "=", "(", "C", "+", "B", ")", "(", "C", "-", "B", ")"),
OldTex("A = \\sqrt{(C + B)(C - B)}", isolate=["A", *to_isolate])
)
lines.arrange(DOWN, buff=LARGE_BUFF)
for line in lines:
@ -260,7 +260,7 @@ TexTransformExample
# new_line2 and the "\sqrt" from the final line. By passing in,
# transform_mismatches=True, it will transform this "^2" part into
# the "\sqrt" part.
new_line2 = Tex("A^2 = (C + B)(C - B)", isolate=["A", *to_isolate])
new_line2 = OldTex("A^2 = (C + B)(C - B)", isolate=["A", *to_isolate])
new_line2.replace(lines[2])
new_line2.match_style(lines[2])

View file

@ -70,7 +70,7 @@ class OpeningManimExample(Scene):
class AnimatingMethods(Scene):
def construct(self):
grid = Tex(r"\pi").get_grid(10, 10, height=4)
grid = Tex(R"\pi").get_grid(10, 10, height=4)
self.add(grid)
# You can animate the application of mobject methods with the
@ -165,16 +165,16 @@ class TexTransformExample(Scene):
# each of these strings. For example, the Tex mobject
# below will have 5 subjects, corresponding to the
# expressions [A^2, +, B^2, =, C^2]
Tex("A^2", "+", "B^2", "=", "C^2"),
OldTex("A^2", "+", "B^2", "=", "C^2"),
# Likewise here
Tex("A^2", "=", "C^2", "-", "B^2"),
OldTex("A^2", "=", "C^2", "-", "B^2"),
# Alternatively, you can pass in the keyword argument
# "isolate" with a list of strings that should be out as
# their own submobject. So the line below is equivalent
# to the commented out line below it.
Tex("A^2 = (C + B)(C - B)", isolate=["A^2", *to_isolate]),
# Tex("A^2", "=", "(", "C", "+", "B", ")", "(", "C", "-", "B", ")"),
Tex("A = \\sqrt{(C + B)(C - B)}", isolate=["A", *to_isolate])
OldTex("A^2 = (C + B)(C - B)", isolate=["A^2", *to_isolate]),
# OldTex("A^2", "=", "(", "C", "+", "B", ")", "(", "C", "-", "B", ")"),
OldTex("A = \\sqrt{(C + B)(C - B)}", isolate=["A", *to_isolate])
)
lines.arrange(DOWN, buff=LARGE_BUFF)
for line in lines:
@ -233,7 +233,7 @@ class TexTransformExample(Scene):
# new_line2 and the "\sqrt" from the final line. By passing in,
# transform_mismatches=True, it will transform this "^2" part into
# the "\sqrt" part.
new_line2 = Tex("A^2 = (C + B)(C - B)", isolate=["A", *to_isolate])
new_line2 = OldTex("A^2 = (C + B)(C - B)", isolate=["A", *to_isolate])
new_line2.replace(lines[2])
new_line2.match_style(lines[2])

View file

@ -43,9 +43,10 @@ from manimlib.mobject.probability import *
from manimlib.mobject.shape_matchers import *
from manimlib.mobject.svg.brace import *
from manimlib.mobject.svg.drawings import *
from manimlib.mobject.svg.mtex_mobject import *
from manimlib.mobject.svg.tex_mobject import *
from manimlib.mobject.svg.string_mobject import *
from manimlib.mobject.svg.svg_mobject import *
from manimlib.mobject.svg.special_tex import *
from manimlib.mobject.svg.tex_mobject import *
from manimlib.mobject.svg.text_mobject import *
from manimlib.mobject.three_dimensions import *
@ -74,3 +75,4 @@ from manimlib.utils.rate_functions import *
from manimlib.utils.simple_functions import *
from manimlib.utils.sounds import *
from manimlib.utils.space_ops import *
from manimlib.utils.tex import *

View file

@ -407,7 +407,6 @@ class TurnInsideOut(Transform):
def create_target(self) -> Mobject:
result = self.mobject.copy().reverse_points()
if isinstance(result, VMobject):
result.refresh_unit_normal()
result.refresh_triangulation()
return result

View file

@ -13,15 +13,14 @@ from manimlib.animation.transform import Transform
from manimlib.mobject.mobject import Mobject
from manimlib.mobject.mobject import Group
from manimlib.mobject.svg.string_mobject import StringMobject
from manimlib.mobject.svg.tex_mobject import Tex
from manimlib.mobject.svg.old_tex_mobject import OldTex
from manimlib.mobject.types.vectorized_mobject import VGroup
from manimlib.mobject.types.vectorized_mobject import VMobject
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from manimlib.mobject.svg.tex_mobject import SingleStringTex
from manimlib.mobject.svg.tex_mobject import Tex
from manimlib.mobject.svg.old_tex_mobject import SingleStringTex
from manimlib.scene.scene import Scene
@ -140,15 +139,15 @@ class TransformMatchingShapes(TransformMatchingParts):
class TransformMatchingTex(TransformMatchingParts):
mobject_type: type = Tex
mobject_type: type = OldTex
group_type: type = VGroup
@staticmethod
def get_mobject_parts(mobject: Tex) -> list[SingleStringTex]:
def get_mobject_parts(mobject: OldTex) -> list[SingleStringTex]:
return mobject.submobjects
@staticmethod
def get_mobject_key(mobject: Tex) -> str:
def get_mobject_key(mobject: OldTex) -> str:
return mobject.get_tex()

View file

@ -697,6 +697,7 @@ class ComplexPlane(NumberPlane):
self,
numbers: list[complex] | None = None,
skip_first: bool = True,
font_size: int = 36,
**kwargs
):
if numbers is None:
@ -712,7 +713,7 @@ class ComplexPlane(NumberPlane):
else:
axis = self.get_x_axis()
value = z.real
number_mob = axis.get_number_mobject(value, **kwargs)
number_mob = axis.get_number_mobject(value, font_size=font_size, **kwargs)
# For -i, remove the "1"
if z.imag == -1:
number_mob.remove(number_mob[1])

View file

@ -146,7 +146,7 @@ class Matrix(VMobject):
R"\left[\begin{array}{c}",
*height * [R"\quad \\"],
R"\end{array}\right]",
)))[0]
)))
brackets.set_height(self.get_height() + v_buff)
l_bracket = brackets[:len(brackets) // 2]
r_bracket = brackets[len(brackets) // 2:]
@ -219,7 +219,7 @@ def get_det_text(
background_rect: bool = False,
initial_scale_factor: int = 2
) -> VGroup:
parens = Tex("(", ")")
parens = Tex("()")
parens.scale(initial_scale_factor)
parens.stretch_to_fit_height(matrix.get_height())
l_paren, r_paren = parens.split()

View file

@ -534,7 +534,8 @@ class Mobject(object):
# Copying and serialization
def stash_mobject_pointers(func):
@staticmethod
def stash_mobject_pointers(func: Callable):
@wraps(func)
def wrapper(self, *args, **kwargs):
uncopied_attrs = ["parents", "target", "saved_state"]
@ -1740,7 +1741,8 @@ class Mobject(object):
# Operations touching shader uniforms
def affects_shader_info_id(func):
@staticmethod
def affects_shader_info_id(func: Callable):
@wraps(func)
def wrapper(self):
for mob in self.get_family():

View file

@ -4,7 +4,7 @@ import numpy as np
from manimlib.constants import DOWN, LEFT, RIGHT, UP
from manimlib.constants import WHITE
from manimlib.mobject.svg.tex_mobject import SingleStringTex
from manimlib.mobject.svg.tex_mobject import Tex
from manimlib.mobject.svg.text_mobject import Text
from manimlib.mobject.types.vectorized_mobject import VMobject
@ -73,7 +73,7 @@ class DecimalNumber(VMobject):
dots.arrange(RIGHT, buff=2 * dots[0].get_width())
self.add(dots)
if self.unit is not None:
self.unit_sign = SingleStringTex(self.unit, font_size=self.get_font_size())
self.unit_sign = Tex(self.unit, font_size=self.get_font_size())
self.add(self.unit_sign)
self.arrange(

View file

@ -11,7 +11,6 @@ from manimlib.constants import PI
from manimlib.animation.composition import AnimationGroup
from manimlib.animation.fading import FadeIn
from manimlib.animation.growing import GrowFromCenter
from manimlib.mobject.svg.tex_mobject import SingleStringTex
from manimlib.mobject.svg.tex_mobject import Tex
from manimlib.mobject.svg.tex_mobject import TexText
from manimlib.mobject.svg.text_mobject import Text
@ -30,7 +29,7 @@ if TYPE_CHECKING:
from manimlib.typing import Vect3
class Brace(SingleStringTex):
class Brace(Tex):
def __init__(
self,
mobject: Mobject,
@ -52,7 +51,6 @@ class Brace(SingleStringTex):
self.shift(left - self.get_corner(UL) + buff * DOWN)
for mob in mobject, self:
mob.rotate(angle, about_point=ORIGIN)
self.refresh_unit_normal()
def set_initial_width(self, width: float):
width_diff = width - self.get_width()

View file

@ -42,10 +42,11 @@ from manimlib.mobject.geometry import Polygon
from manimlib.mobject.geometry import Rectangle
from manimlib.mobject.geometry import Square
from manimlib.mobject.mobject import Mobject
from manimlib.mobject.numbers import Integer
from manimlib.mobject.svg.svg_mobject import SVGMobject
from manimlib.mobject.svg.tex_mobject import Tex
from manimlib.mobject.svg.tex_mobject import TexText
from manimlib.mobject.svg.tex_mobject import TexTextFromPresetString
from manimlib.mobject.svg.special_tex import TexTextFromPresetString
from manimlib.mobject.three_dimensions import Prismify
from manimlib.mobject.three_dimensions import VCube
from manimlib.mobject.types.vectorized_mobject import VGroup
@ -125,7 +126,7 @@ class Speedometer(VMobject):
for index, angle in enumerate(tick_angle_range):
vect = rotate_vector(RIGHT, angle)
tick = Line((1 - tick_length) * vect, vect)
label = Tex(str(10 * index))
label = Integer(10 * index)
label.set_height(tick_length)
label.shift((1 + tick_length) * vect)
self.add(tick, label)
@ -397,9 +398,7 @@ class Bubble(SVGMobject):
return self
def flip(self, axis=UP):
Mobject.flip(self, axis=axis)
self.refresh_unit_normal()
self.refresh_triangulation()
super().flip(axis=axis)
if abs(axis[1]) > 0:
self.direction = -np.array(self.direction)
return self

View file

@ -1,219 +0,0 @@
from __future__ import annotations
import re
from manimlib.mobject.svg.string_mobject import StringMobject
from manimlib.utils.color import color_to_hex
from manimlib.utils.color import hex_to_int
from manimlib.utils.tex_file_writing import tex_content_to_svg_file
from typing import TYPE_CHECKING
if TYPE_CHECKING:
import re
from typing import Iterable, Union
from manimlib.mobject.types.vectorized_mobject import VGroup
from manimlib.typing import ManimColor
Span = tuple[int, int]
Selector = Union[
str,
re.Pattern,
tuple[Union[int, None], Union[int, None]],
Iterable[Union[
str,
re.Pattern,
tuple[Union[int, None], Union[int, None]]
]]
]
SCALE_FACTOR_PER_FONT_POINT = 0.001
class MTex(StringMobject):
tex_environment: str = "align*"
def __init__(
self,
tex_string: str,
font_size: int = 48,
alignment: str = R"\centering",
template: str = "",
additional_preamble: str = "",
tex_to_color_map: dict = dict(),
**kwargs
):
# Prevent from passing an empty string.
if not tex_string.strip():
tex_string = R"\\"
self.tex_string = tex_string
self.alignment = alignment
self.template = template
self.additional_preamble = additional_preamble
self.tex_to_color_map = dict(tex_to_color_map)
super().__init__(
tex_string,
**kwargs
)
self.set_color_by_tex_to_color_map(tex_to_color_map)
self.scale(SCALE_FACTOR_PER_FONT_POINT * font_size)
@property
def hash_seed(self) -> tuple:
return (
self.__class__.__name__,
self.svg_default,
self.path_string_config,
self.base_color,
self.isolate,
self.protect,
self.tex_string,
self.alignment,
self.tex_environment,
self.tex_to_color_map,
self.template,
self.additional_preamble
)
def get_file_path_by_content(self, content: str) -> str:
file_path = tex_content_to_svg_file(
content, self.template, self.additional_preamble, self.tex_string
)
return file_path
# Parsing
@staticmethod
def get_command_matches(string: str) -> list[re.Match]:
# Lump together adjacent brace pairs
pattern = re.compile(r"""
(?P<command>\\(?:[a-zA-Z]+|.))
|(?P<open>{+)
|(?P<close>}+)
""", flags=re.X | re.S)
result = []
open_stack = []
for match_obj in pattern.finditer(string):
if match_obj.group("open"):
open_stack.append((match_obj.span(), len(result)))
elif match_obj.group("close"):
close_start, close_end = match_obj.span()
while True:
if not open_stack:
raise ValueError("Missing '{' inserted")
(open_start, open_end), index = open_stack.pop()
n = min(open_end - open_start, close_end - close_start)
result.insert(index, pattern.fullmatch(
string, pos=open_end - n, endpos=open_end
))
result.append(pattern.fullmatch(
string, pos=close_start, endpos=close_start + n
))
close_start += n
if close_start < close_end:
continue
open_end -= n
if open_start < open_end:
open_stack.append(((open_start, open_end), index))
break
else:
result.append(match_obj)
if open_stack:
raise ValueError("Missing '}' inserted")
return result
@staticmethod
def get_command_flag(match_obj: re.Match) -> int:
if match_obj.group("open"):
return 1
if match_obj.group("close"):
return -1
return 0
@staticmethod
def replace_for_content(match_obj: re.Match) -> str:
return match_obj.group()
@staticmethod
def replace_for_matching(match_obj: re.Match) -> str:
if match_obj.group("command"):
return match_obj.group()
return ""
@staticmethod
def get_attr_dict_from_command_pair(
open_command: re.Match, close_command: re.Match
) -> dict[str, str] | None:
if len(open_command.group()) >= 2:
return {}
return None
def get_configured_items(self) -> list[tuple[Span, dict[str, str]]]:
return [
(span, {})
for selector in self.tex_to_color_map
for span in self.find_spans_by_selector(selector)
]
@staticmethod
def get_color_command(rgb_hex: str) -> str:
rgb = hex_to_int(rgb_hex)
rg, b = divmod(rgb, 256)
r, g = divmod(rg, 256)
return f"\\color[RGB]{{{r}, {g}, {b}}}"
@staticmethod
def get_command_string(
attr_dict: dict[str, str], is_end: bool, label_hex: str | None
) -> str:
if label_hex is None:
return ""
if is_end:
return "}}"
return "{{" + MTex.get_color_command(label_hex)
def get_content_prefix_and_suffix(
self, is_labelled: bool
) -> tuple[str, str]:
prefix_lines = []
suffix_lines = []
if not is_labelled:
prefix_lines.append(self.get_color_command(
color_to_hex(self.base_color)
))
if self.alignment:
prefix_lines.append(self.alignment)
if self.tex_environment:
prefix_lines.append(f"\\begin{{{self.tex_environment}}}")
suffix_lines.append(f"\\end{{{self.tex_environment}}}")
return (
"".join([line + "\n" for line in prefix_lines]),
"".join(["\n" + line for line in suffix_lines])
)
# Method alias
def get_parts_by_tex(self, selector: Selector) -> VGroup:
return self.select_parts(selector)
def get_part_by_tex(self, selector: Selector, **kwargs) -> VGroup:
return self.select_part(selector, **kwargs)
def set_color_by_tex(self, selector: Selector, color: ManimColor):
return self.set_parts_color(selector, color)
def set_color_by_tex_to_color_map(
self, color_map: dict[Selector, ManimColor]
):
return self.set_parts_color_by_dict(color_map)
def get_tex(self) -> str:
return self.get_string()
class MTexText(MTex):
tex_environment: str = ""

View file

@ -0,0 +1,339 @@
from __future__ import annotations
from functools import reduce
import operator as op
import re
from manimlib.constants import BLACK, WHITE
from manimlib.mobject.svg.svg_mobject import SVGMobject
from manimlib.mobject.types.vectorized_mobject import VGroup
from manimlib.utils.tex_file_writing import tex_content_to_svg_file
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Iterable, List, Dict
from manimlib.typing import ManimColor
SCALE_FACTOR_PER_FONT_POINT = 0.001
class SingleStringTex(SVGMobject):
height: float | None = None
def __init__(
self,
tex_string: str,
height: float | None = None,
fill_color: ManimColor = WHITE,
fill_opacity: float = 1.0,
stroke_width: float = 0,
svg_default: dict = dict(fill_color=WHITE),
path_string_config: dict = dict(
should_subdivide_sharp_curves=True,
should_remove_null_curves=True,
),
font_size: int = 48,
alignment: str = R"\centering",
math_mode: bool = True,
organize_left_to_right: bool = False,
template: str = "",
additional_preamble: str = "",
**kwargs
):
self.tex_string = tex_string
self.svg_default = dict(svg_default)
self.path_string_config = dict(path_string_config)
self.font_size = font_size
self.alignment = alignment
self.math_mode = math_mode
self.organize_left_to_right = organize_left_to_right
self.template = template
self.additional_preamble = additional_preamble
super().__init__(
height=height,
fill_color=fill_color,
fill_opacity=fill_opacity,
stroke_width=stroke_width,
path_string_config=path_string_config,
**kwargs
)
if self.height is None:
self.scale(SCALE_FACTOR_PER_FONT_POINT * self.font_size)
if self.organize_left_to_right:
self.organize_submobjects_left_to_right()
@property
def hash_seed(self) -> tuple:
return (
self.__class__.__name__,
self.svg_default,
self.path_string_config,
self.tex_string,
self.alignment,
self.math_mode,
self.template,
self.additional_preamble
)
def get_file_path(self) -> str:
content = self.get_tex_file_body(self.tex_string)
file_path = tex_content_to_svg_file(
content, self.template, self.additional_preamble, self.tex_string
)
return file_path
def get_tex_file_body(self, tex_string: str) -> str:
new_tex = self.get_modified_expression(tex_string)
if self.math_mode:
new_tex = "\\begin{align*}\n" + new_tex + "\n\\end{align*}"
return self.alignment + "\n" + new_tex
def get_modified_expression(self, tex_string: str) -> str:
return self.modify_special_strings(tex_string.strip())
def modify_special_strings(self, tex: str) -> str:
tex = tex.strip()
should_add_filler = reduce(op.or_, [
# Fraction line needs something to be over
tex == "\\over",
tex == "\\overline",
# Makesure sqrt has overbar
tex == "\\sqrt",
tex == "\\sqrt{",
# Need to add blank subscript or superscript
tex.endswith("_"),
tex.endswith("^"),
tex.endswith("dot"),
])
if should_add_filler:
filler = "{\\quad}"
tex += filler
should_add_double_filler = reduce(op.or_, [
tex == "\\overset",
# TODO: these can't be used since they change
# the latex draw order.
# tex == "\\frac", # you can use \\over as a alternative
# tex == "\\dfrac",
# tex == "\\binom",
])
if should_add_double_filler:
filler = "{\\quad}{\\quad}"
tex += filler
if tex == "\\substack":
tex = "\\quad"
if tex == "":
tex = "\\quad"
# To keep files from starting with a line break
if tex.startswith("\\\\"):
tex = tex.replace("\\\\", "\\quad\\\\")
tex = self.balance_braces(tex)
# Handle imbalanced \left and \right
num_lefts, num_rights = [
len([
s for s in tex.split(substr)[1:]
if s and s[0] in "(){}[]|.\\"
])
for substr in ("\\left", "\\right")
]
if num_lefts != num_rights:
tex = tex.replace("\\left", "\\big")
tex = tex.replace("\\right", "\\big")
for context in ["array"]:
begin_in = ("\\begin{%s}" % context) in tex
end_in = ("\\end{%s}" % context) in tex
if begin_in ^ end_in:
# Just turn this into a blank string,
# which means caller should leave a
# stray \\begin{...} with other symbols
tex = ""
return tex
def balance_braces(self, tex: str) -> str:
"""
Makes Tex resiliant to unmatched braces
"""
num_unclosed_brackets = 0
for i in range(len(tex)):
if i > 0 and tex[i - 1] == "\\":
# So as to not count '\{' type expressions
continue
char = tex[i]
if char == "{":
num_unclosed_brackets += 1
elif char == "}":
if num_unclosed_brackets == 0:
tex = "{" + tex
else:
num_unclosed_brackets -= 1
tex += num_unclosed_brackets * "}"
return tex
def get_tex(self) -> str:
return self.tex_string
def organize_submobjects_left_to_right(self):
self.sort(lambda p: p[0])
return self
class OldTex(SingleStringTex):
def __init__(
self,
*tex_strings: str,
arg_separator: str = "",
isolate: List[str] = [],
tex_to_color_map: Dict[str, ManimColor] = {},
**kwargs
):
self.tex_strings = self.break_up_tex_strings(
tex_strings,
substrings_to_isolate=[*isolate, *tex_to_color_map.keys()]
)
full_string = arg_separator.join(self.tex_strings)
super().__init__(full_string, **kwargs)
self.break_up_by_substrings(self.tex_strings)
self.set_color_by_tex_to_color_map(tex_to_color_map)
if self.organize_left_to_right:
self.organize_submobjects_left_to_right()
def break_up_tex_strings(self, tex_strings: Iterable[str], substrings_to_isolate: List[str] = []) -> Iterable[str]:
# Separate out any strings specified in the isolate
# or tex_to_color_map lists.
if len(substrings_to_isolate) == 0:
return tex_strings
patterns = (
"({})".format(re.escape(ss))
for ss in substrings_to_isolate
)
pattern = "|".join(patterns)
pieces = []
for s in tex_strings:
if pattern:
pieces.extend(re.split(pattern, s))
else:
pieces.append(s)
return list(filter(lambda s: s, pieces))
def break_up_by_substrings(self, tex_strings: Iterable[str]):
"""
Reorganize existing submojects one layer
deeper based on the structure of tex_strings (as a list
of tex_strings)
"""
if len(list(tex_strings)) == 1:
submob = self.copy()
self.set_submobjects([submob])
return self
new_submobjects = []
curr_index = 0
for tex_string in tex_strings:
tex_string = tex_string.strip()
if len(tex_string) == 0:
continue
sub_tex_mob = SingleStringTex(tex_string, math_mode=self.math_mode)
num_submobs = len(sub_tex_mob)
if num_submobs == 0:
continue
new_index = curr_index + num_submobs
sub_tex_mob.set_submobjects(self.submobjects[curr_index:new_index])
new_submobjects.append(sub_tex_mob)
curr_index = new_index
self.set_submobjects(new_submobjects)
return self
def get_parts_by_tex(
self,
tex: str,
substring: bool = True,
case_sensitive: bool = True
) -> VGroup:
def test(tex1, tex2):
if not case_sensitive:
tex1 = tex1.lower()
tex2 = tex2.lower()
if substring:
return tex1 in tex2
else:
return tex1 == tex2
return VGroup(*filter(
lambda m: isinstance(m, SingleStringTex) and test(tex, m.get_tex()),
self.submobjects
))
def get_part_by_tex(self, tex: str, **kwargs) -> SingleStringTex | None:
all_parts = self.get_parts_by_tex(tex, **kwargs)
return all_parts[0] if all_parts else None
def set_color_by_tex(self, tex: str, color: ManimColor, **kwargs):
self.get_parts_by_tex(tex, **kwargs).set_color(color)
return self
def set_color_by_tex_to_color_map(
self,
tex_to_color_map: dict[str, ManimColor],
**kwargs
):
for tex, color in list(tex_to_color_map.items()):
self.set_color_by_tex(tex, color, **kwargs)
return self
def index_of_part(self, part: SingleStringTex, start: int = 0) -> int:
return self.submobjects.index(part, start)
def index_of_part_by_tex(self, tex: str, start: int = 0, **kwargs) -> int:
part = self.get_part_by_tex(tex, **kwargs)
return self.index_of_part(part, start)
def slice_by_tex(
self,
start_tex: str | None = None,
stop_tex: str | None = None,
**kwargs
) -> VGroup:
if start_tex is None:
start_index = 0
else:
start_index = self.index_of_part_by_tex(start_tex, **kwargs)
if stop_tex is None:
return self[start_index:]
else:
stop_index = self.index_of_part_by_tex(stop_tex, start=start_index, **kwargs)
return self[start_index:stop_index]
def sort_alphabetically(self) -> None:
self.submobjects.sort(key=lambda m: m.get_tex())
def set_bstroke(self, color: ManimColor = BLACK, width: float = 4):
self.set_stroke(color, width, background=True)
return self
class OldTexText(OldTex):
def __init__(
self,
*tex_strings: str,
math_mode: bool = False,
arg_separator: str = "",
**kwargs
):
super().__init__(
*tex_strings,
math_mode=math_mode,
arg_separator=arg_separator,
**kwargs
)

View file

@ -0,0 +1,80 @@
from __future__ import annotations
from manimlib.constants import MED_SMALL_BUFF, WHITE, GREY_C
from manimlib.constants import DOWN, LEFT, RIGHT, UP
from manimlib.constants import FRAME_WIDTH
from manimlib.constants import MED_LARGE_BUFF, SMALL_BUFF
from manimlib.mobject.geometry import Line
from manimlib.mobject.types.vectorized_mobject import VGroup
from manimlib.mobject.svg.tex_mobject import TexText
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from manimlib.typing import ManimColor, Vect3
class BulletedList(VGroup):
def __init__(
self,
*items: str,
buff: float = MED_LARGE_BUFF,
aligned_edge: Vect3 = LEFT,
**kwargs
):
labelled_content = [R"\item " + item for item in items]
tex_string = "\n".join([
R"\begin{itemize}",
*labelled_content,
R"\end{itemize}"
])
tex_text = TexText(tex_string, isolate=labelled_content, **kwargs)
lines = (tex_text.select_part(part) for part in labelled_content)
super().__init__(*lines)
self.arrange(DOWN, buff=buff, aligned_edge=aligned_edge)
def fade_all_but(self, index: int, opacity: float = 0.25) -> None:
for i, part in enumerate(self.submobjects):
part.set_fill(opacity=(1.0 if i == index else opacity))
class TexTextFromPresetString(TexText):
tex: str = ""
default_color: ManimColor = WHITE
def __init__(self, **kwargs):
super().__init__(
self.tex,
color=kwargs.pop("color", self.default_color),
**kwargs
)
class Title(TexText):
def __init__(
self,
*text_parts: str,
font_size: int = 72,
include_underline: bool = True,
underline_width: float = FRAME_WIDTH - 2,
# This will override underline_width
match_underline_width_to_text: bool = False,
underline_buff: float = SMALL_BUFF,
underline_style: dict = dict(stroke_width=2, stroke_color=GREY_C),
**kwargs
):
super().__init__(*text_parts, font_size=font_size, **kwargs)
self.to_edge(UP, buff=MED_SMALL_BUFF)
if include_underline:
underline = Line(LEFT, RIGHT, **underline_style)
underline.next_to(self, DOWN, buff=underline_buff)
if match_underline_width_to_text:
underline.match_width(self)
else:
underline.set_width(underline_width)
self.add(underline)
self.underline = underline

View file

@ -9,6 +9,7 @@ from scipy.spatial.distance import cdist
from manimlib.constants import WHITE
from manimlib.logger import log
from manimlib.mobject.svg.svg_mobject import SVGMobject
from manimlib.mobject.types.vectorized_mobject import VMobject
from manimlib.mobject.types.vectorized_mobject import VGroup
from manimlib.utils.color import color_to_hex
from manimlib.utils.color import hex_to_int
@ -17,25 +18,13 @@ from manimlib.utils.color import int_to_hex
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Callable, Iterable, Union
from manimlib.typing import ManimColor
Span = tuple[int, int]
Selector = Union[
str,
re.Pattern,
tuple[Union[int, None], Union[int, None]],
Iterable[Union[
str,
re.Pattern,
tuple[Union[int, None], Union[int, None]]
]]
]
from typing import Callable
from manimlib.typing import ManimColor, Span, Selector
class StringMobject(SVGMobject, ABC):
"""
An abstract base class for `MTex` and `MarkupText`
An abstract base class for `Tex` and `MarkupText`
This class aims to optimize the logic of "slicing submobjects
via substrings". This could be much clearer and more user-friendly
@ -67,12 +56,17 @@ class StringMobject(SVGMobject, ABC):
base_color: ManimColor = WHITE,
isolate: Selector = (),
protect: Selector = (),
# When set to true, only the labelled svg is
# rendered, and its contents are used directly
# for the body of this String Mobject
use_labelled_svg: bool = False,
**kwargs
):
self.string = string
self.base_color = base_color or WHITE
self.isolate = isolate
self.protect = protect
self.use_labelled_svg = use_labelled_svg
self.parse()
super().__init__(
@ -84,47 +78,34 @@ class StringMobject(SVGMobject, ABC):
)
self.labels = [submob.label for submob in self.submobjects]
def get_file_path(self) -> str:
original_content = self.get_content(is_labelled=False)
return self.get_file_path_by_content(original_content)
def get_file_path(self, is_labelled: bool = False) -> str:
is_labelled = is_labelled or self.use_labelled_svg
return self.get_file_path_by_content(self.get_content(is_labelled))
@abstractmethod
def get_file_path_by_content(self, content: str) -> str:
return ""
def generate_mobject(self) -> None:
super().generate_mobject()
def assign_labels_by_color(self, mobjects: list[VMobject]) -> None:
"""
Assuming each mobject in the list `mobjects` has a fill color
meant to represent a numerical label, this assigns those
those numerical labels to each mobject as an attribute
"""
labels_count = len(self.labelled_spans)
if labels_count == 1:
for submob in self.submobjects:
submob.label = 0
for mob in mobjects:
mob.label = 0
return
labelled_content = self.get_content(is_labelled=True)
file_path = self.get_file_path_by_content(labelled_content)
labelled_svg = SVGMobject(file_path)
if len(self.submobjects) != len(labelled_svg.submobjects):
log.warning(
"Cannot align submobjects of the labelled svg " + \
"to the original svg. Skip the labelling process."
)
for submob in self.submobjects:
submob.label = 0
return
self.rearrange_submobjects_by_positions(labelled_svg)
unrecognizable_colors = []
for submob, labelled_svg_submob in zip(
self.submobjects, labelled_svg.submobjects
):
label = hex_to_int(color_to_hex(
labelled_svg_submob.get_fill_color()
))
for mob in mobjects:
label = hex_to_int(color_to_hex(mob.get_fill_color()))
if label >= labels_count:
unrecognizable_colors.append(label)
label = 0
submob.label = label
mob.label = label
if unrecognizable_colors:
log.warning(
"Unrecognizable color labels detected (%s). " + \
@ -135,26 +116,59 @@ class StringMobject(SVGMobject, ABC):
)
)
def mobjects_from_file(self, file_path: str) -> list[VMobject]:
submobs = super().mobjects_from_file(file_path)
if self.use_labelled_svg:
# This means submobjects are colored according to spans
self.assign_labels_by_color(submobs)
return submobs
# Otherwise, submobs are not colored, so generate a new list
# of submobject which are and use those for labels
unlabelled_submobs = submobs
labelled_content = self.get_content(is_labelled=True)
labelled_file = self.get_file_path_by_content(labelled_content)
labelled_submobs = super().mobjects_from_file(labelled_file)
self.labelled_submobs = labelled_submobs
self.unlabelled_submobs = unlabelled_submobs
self.assign_labels_by_color(labelled_submobs)
self.rearrange_submobjects_by_positions(labelled_submobs, unlabelled_submobs)
for usm, lsm in zip(unlabelled_submobs, labelled_submobs):
usm.label = lsm.label
if len(unlabelled_submobs) != len(labelled_submobs):
log.warning(
"Cannot align submobjects of the labelled svg " + \
"to the original svg. Skip the labelling process."
)
for usm in unlabelled_submobs:
usm.label = 0
return unlabelled_submobs
return unlabelled_submobs
def rearrange_submobjects_by_positions(
self, labelled_svg: SVGMobject
self, labelled_submobs: list[VMobject], unlabelled_submobs: list[VMobject],
) -> None:
# Rearrange submobjects of `labelled_svg` so that
# each submobject is labelled by the nearest one of `labelled_svg`.
# The correctness cannot be ensured, since the svg may
# change significantly after inserting color commands.
if not labelled_svg.submobjects:
"""
Rearrange `labeleled_submobjects` so that each submobject
is labelled by the nearest one of `unlabelled_submobs`.
The correctness cannot be ensured, since the svg may
change significantly after inserting color commands.
"""
if len(labelled_submobs) == 0:
return
labelled_svg.replace(self)
labelled_svg = VGroup(*labelled_submobs)
labelled_svg.replace(VGroup(*unlabelled_submobs))
distance_matrix = cdist(
[submob.get_center() for submob in self.submobjects],
[submob.get_center() for submob in labelled_svg.submobjects]
[submob.get_center() for submob in unlabelled_submobs],
[submob.get_center() for submob in labelled_submobs]
)
_, indices = linear_sum_assignment(distance_matrix)
labelled_svg.set_submobjects([
labelled_svg.submobjects[index]
for index in indices
])
labelled_submobs[:] = [labelled_submobs[index] for index in indices]
# Toolkits
@ -524,13 +538,36 @@ class StringMobject(SVGMobject, ABC):
])
def select_parts(self, selector: Selector) -> VGroup:
return self.build_parts_from_indices_lists(
self.get_submob_indices_lists_by_selector(selector)
)
indices_list = self.get_submob_indices_lists_by_selector(selector)
if indices_list:
return self.build_parts_from_indices_lists(indices_list)
elif isinstance(selector, str):
# Otherwise, try finding substrings which weren't specifically isolated
log.warning("Accessing unisolated substring, results may not be as expected")
return self.select_unisolated_substring(selector)
else:
return VGroup()
def select_part(self, selector: Selector, index: int = 0) -> VGroup:
def __getitem__(self, value: int | slice | Selector) -> VMobject:
if isinstance(value, (int, slice)):
return super().__getitem__(value)
return self.select_parts(value)
def select_part(self, selector: Selector, index: int = 0) -> VMobject:
return self.select_parts(selector)[index]
def substr_to_path_count(self, substr: str) -> int:
return len(re.sub(R"\s", "", substr))
def select_unisolated_substring(self, substr: str) -> VGroup:
result = []
for match in re.finditer(substr.replace("\\", r"\\"), self.string):
index = match.start()
start = self.substr_to_path_count(self.string[:index])
end = start + self.substr_to_path_count(substr)
result.append(self[start:end])
return VGroup(*result)
def set_parts_color(self, selector: Selector, color: ManimColor):
self.select_parts(selector).set_color(color)
return self

View file

@ -27,7 +27,7 @@ if TYPE_CHECKING:
SVG_HASH_TO_MOB_MAP: dict[int, VMobject] = {}
SVG_HASH_TO_MOB_MAP: dict[int, list[VMobject]] = {}
def _convert_point_to_3d(x: float, y: float) -> np.ndarray:
@ -97,12 +97,13 @@ class SVGMobject(VMobject):
def init_svg_mobject(self) -> None:
hash_val = hash_obj(self.hash_seed)
if hash_val in SVG_HASH_TO_MOB_MAP:
mob = SVG_HASH_TO_MOB_MAP[hash_val].copy()
self.add(*mob)
return
submobs = [sm.copy() for sm in SVG_HASH_TO_MOB_MAP[hash_val]]
else:
submobs = self.mobjects_from_file(self.get_file_path())
SVG_HASH_TO_MOB_MAP[hash_val] = [sm.copy() for sm in submobs]
self.generate_mobject()
SVG_HASH_TO_MOB_MAP[hash_val] = self.copy()
self.add(*submobs)
self.flip(RIGHT) # Flip y
@property
def hash_seed(self) -> tuple:
@ -115,8 +116,7 @@ class SVGMobject(VMobject):
self.file_name
)
def generate_mobject(self) -> None:
file_path = self.get_file_path()
def mobjects_from_file(self, file_path: str) -> list[VMobject]:
element_tree = ET.parse(file_path)
new_tree = self.modify_xml_tree(element_tree)
@ -127,9 +127,7 @@ class SVGMobject(VMobject):
svg = se.SVG.parse(data_stream)
data_stream.close()
mobjects = self.get_mobjects_from(svg)
self.add(*mobjects)
self.flip(RIGHT) # Flip y
return self.mobjects_from_svg(svg)
def get_file_path(self) -> str:
if self.file_name is None:
@ -178,7 +176,7 @@ class SVGMobject(VMobject):
result[svg_key] = str(svg_default_dict[style_key])
return result
def get_mobjects_from(self, svg: se.SVG) -> list[VMobject]:
def mobjects_from_svg(self, svg: se.SVG) -> list[VMobject]:
result = []
for shape in svg.elements():
if isinstance(shape, (se.Group, se.Use)):
@ -314,12 +312,9 @@ class VMobjectFromSVGPath(VMobject):
path_string = self.path_obj.d()
path_hash = hash_string(path_string)
points_filepath = os.path.join(get_mobject_data_dir(), f"{path_hash}_points.npy")
tris_filepath = os.path.join(get_mobject_data_dir(), f"{path_hash}_tris.npy")
if os.path.exists(points_filepath) and os.path.exists(tris_filepath):
if os.path.exists(points_filepath):
self.set_points(np.load(points_filepath))
self.triangulation = np.load(tris_filepath)
self.needs_new_triangulation = False
else:
self.handle_commands()
if self.should_subdivide_sharp_curves:
@ -330,7 +325,7 @@ class VMobjectFromSVGPath(VMobject):
self.set_points(self.get_points_without_null_curves())
# Save to a file for future use
np.save(points_filepath, self.get_points())
np.save(tris_filepath, self.get_triangulation())
self.get_triangulation()
def handle_commands(self) -> None:
segment_class_to_func_map = {

View file

@ -1,74 +1,68 @@
from __future__ import annotations
from functools import reduce
import operator as op
import re
from manimlib.constants import BLACK, WHITE, GREY_C
from manimlib.constants import DOWN, LEFT, RIGHT, UP
from manimlib.constants import FRAME_WIDTH
from manimlib.constants import MED_LARGE_BUFF, SMALL_BUFF
from manimlib.mobject.geometry import Line
from manimlib.mobject.svg.svg_mobject import SVGMobject
from manimlib.mobject.svg.string_mobject import StringMobject
from manimlib.mobject.types.vectorized_mobject import VGroup
from manimlib.mobject.types.vectorized_mobject import VMobject
from manimlib.utils.color import color_to_hex
from manimlib.utils.color import hex_to_int
from manimlib.utils.tex_file_writing import tex_content_to_svg_file
from manimlib.utils.tex import num_tex_symbols
from manimlib.logger import log
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Iterable, List, Dict
from manimlib.typing import ManimColor
from manimlib.typing import ManimColor, Span, Selector
SCALE_FACTOR_PER_FONT_POINT = 0.001
class SingleStringTex(SVGMobject):
height: float | None = None
class Tex(StringMobject):
tex_environment: str = "align*"
def __init__(
self,
tex_string: str,
height: float | None = None,
fill_color: ManimColor = WHITE,
fill_opacity: float = 1.0,
stroke_width: float = 0,
svg_default: dict = dict(fill_color=WHITE),
path_string_config: dict = dict(
should_subdivide_sharp_curves=True,
should_remove_null_curves=True,
),
*tex_strings: str,
font_size: int = 48,
alignment: str = R"\centering",
math_mode: bool = True,
organize_left_to_right: bool = False,
template: str = "",
additional_preamble: str = "",
tex_to_color_map: dict = dict(),
t2c: dict = dict(),
isolate: Selector = [],
use_labelled_svg: bool = True,
**kwargs
):
# Combine multi-string arg, but mark them to isolate
if len(tex_strings) > 1:
if isinstance(isolate, (str, re.Pattern, tuple)):
isolate = [isolate]
isolate = [*isolate, *tex_strings]
tex_string = " ".join(tex_strings)
# Prevent from passing an empty string.
if not tex_string.strip():
tex_string = R"\\"
self.tex_string = tex_string
self.svg_default = dict(svg_default)
self.path_string_config = dict(path_string_config)
self.font_size = font_size
self.alignment = alignment
self.math_mode = math_mode
self.organize_left_to_right = organize_left_to_right
self.template = template
self.additional_preamble = additional_preamble
self.tex_to_color_map = dict(**t2c, **tex_to_color_map)
super().__init__(
height=height,
fill_color=fill_color,
fill_opacity=fill_opacity,
stroke_width=stroke_width,
path_string_config=path_string_config,
tex_string,
use_labelled_svg=use_labelled_svg,
isolate=isolate,
**kwargs
)
if self.height is None:
self.scale(SCALE_FACTOR_PER_FONT_POINT * self.font_size)
if self.organize_left_to_right:
self.organize_submobjects_left_to_right()
self.set_color_by_tex_to_color_map(self.tex_to_color_map)
self.scale(SCALE_FACTOR_PER_FONT_POINT * font_size)
@property
def hash_seed(self) -> tuple:
@ -76,346 +70,159 @@ class SingleStringTex(SVGMobject):
self.__class__.__name__,
self.svg_default,
self.path_string_config,
self.base_color,
self.isolate,
self.protect,
self.tex_string,
self.alignment,
self.math_mode,
self.tex_environment,
self.tex_to_color_map,
self.template,
self.additional_preamble
)
def get_file_path(self) -> str:
content = self.get_tex_file_body(self.tex_string)
file_path = tex_content_to_svg_file(
def get_file_path_by_content(self, content: str) -> str:
return tex_content_to_svg_file(
content, self.template, self.additional_preamble, self.tex_string
)
return file_path
def get_tex_file_body(self, tex_string: str) -> str:
new_tex = self.get_modified_expression(tex_string)
if self.math_mode:
new_tex = "\\begin{align*}\n" + new_tex + "\n\\end{align*}"
return self.alignment + "\n" + new_tex
# Parsing
def get_modified_expression(self, tex_string: str) -> str:
return self.modify_special_strings(tex_string.strip())
@staticmethod
def get_command_matches(string: str) -> list[re.Match]:
# Lump together adjacent brace pairs
pattern = re.compile(r"""
(?P<command>\\(?:[a-zA-Z]+|.))
|(?P<open>{+)
|(?P<close>}+)
""", flags=re.X | re.S)
result = []
open_stack = []
for match_obj in pattern.finditer(string):
if match_obj.group("open"):
open_stack.append((match_obj.span(), len(result)))
elif match_obj.group("close"):
close_start, close_end = match_obj.span()
while True:
if not open_stack:
raise ValueError("Missing '{' inserted")
(open_start, open_end), index = open_stack.pop()
n = min(open_end - open_start, close_end - close_start)
result.insert(index, pattern.fullmatch(
string, pos=open_end - n, endpos=open_end
))
result.append(pattern.fullmatch(
string, pos=close_start, endpos=close_start + n
))
close_start += n
if close_start < close_end:
continue
open_end -= n
if open_start < open_end:
open_stack.append(((open_start, open_end), index))
break
else:
result.append(match_obj)
if open_stack:
raise ValueError("Missing '}' inserted")
return result
def modify_special_strings(self, tex: str) -> str:
tex = tex.strip()
should_add_filler = reduce(op.or_, [
# Fraction line needs something to be over
tex == "\\over",
tex == "\\overline",
# Makesure sqrt has overbar
tex == "\\sqrt",
tex == "\\sqrt{",
# Need to add blank subscript or superscript
tex.endswith("_"),
tex.endswith("^"),
tex.endswith("dot"),
])
if should_add_filler:
filler = "{\\quad}"
tex += filler
@staticmethod
def get_command_flag(match_obj: re.Match) -> int:
if match_obj.group("open"):
return 1
if match_obj.group("close"):
return -1
return 0
should_add_double_filler = reduce(op.or_, [
tex == "\\overset",
# TODO: these can't be used since they change
# the latex draw order.
# tex == "\\frac", # you can use \\over as a alternative
# tex == "\\dfrac",
# tex == "\\binom",
])
if should_add_double_filler:
filler = "{\\quad}{\\quad}"
tex += filler
@staticmethod
def replace_for_content(match_obj: re.Match) -> str:
return match_obj.group()
if tex == "\\substack":
tex = "\\quad"
@staticmethod
def replace_for_matching(match_obj: re.Match) -> str:
if match_obj.group("command"):
return match_obj.group()
return ""
if tex == "":
tex = "\\quad"
@staticmethod
def get_attr_dict_from_command_pair(
open_command: re.Match, close_command: re.Match
) -> dict[str, str] | None:
if len(open_command.group()) >= 2:
return {}
return None
# To keep files from starting with a line break
if tex.startswith("\\\\"):
tex = tex.replace("\\\\", "\\quad\\\\")
tex = self.balance_braces(tex)
# Handle imbalanced \left and \right
num_lefts, num_rights = [
len([
s for s in tex.split(substr)[1:]
if s and s[0] in "(){}[]|.\\"
])
for substr in ("\\left", "\\right")
def get_configured_items(self) -> list[tuple[Span, dict[str, str]]]:
return [
(span, {})
for selector in self.tex_to_color_map
for span in self.find_spans_by_selector(selector)
]
if num_lefts != num_rights:
tex = tex.replace("\\left", "\\big")
tex = tex.replace("\\right", "\\big")
for context in ["array"]:
begin_in = ("\\begin{%s}" % context) in tex
end_in = ("\\end{%s}" % context) in tex
if begin_in ^ end_in:
# Just turn this into a blank string,
# which means caller should leave a
# stray \\begin{...} with other symbols
tex = ""
return tex
@staticmethod
def get_color_command(rgb_hex: str) -> str:
rgb = hex_to_int(rgb_hex)
rg, b = divmod(rgb, 256)
r, g = divmod(rg, 256)
return f"\\color[RGB]{{{r}, {g}, {b}}}"
def balance_braces(self, tex: str) -> str:
"""
Makes Tex resiliant to unmatched braces
"""
num_unclosed_brackets = 0
for i in range(len(tex)):
if i > 0 and tex[i - 1] == "\\":
# So as to not count '\{' type expressions
continue
char = tex[i]
if char == "{":
num_unclosed_brackets += 1
elif char == "}":
if num_unclosed_brackets == 0:
tex = "{" + tex
else:
num_unclosed_brackets -= 1
tex += num_unclosed_brackets * "}"
return tex
@staticmethod
def get_command_string(
attr_dict: dict[str, str], is_end: bool, label_hex: str | None
) -> str:
if label_hex is None:
return ""
if is_end:
return "}}"
return "{{" + Tex.get_color_command(label_hex)
def get_tex(self) -> str:
return self.tex_string
def organize_submobjects_left_to_right(self):
self.sort(lambda p: p[0])
return self
class Tex(SingleStringTex):
def __init__(
self,
*tex_strings: str,
arg_separator: str = "",
isolate: List[str] = [],
tex_to_color_map: Dict[str, ManimColor] = {},
**kwargs
):
self.tex_strings = self.break_up_tex_strings(
tex_strings,
substrings_to_isolate=[*isolate, *tex_to_color_map.keys()]
def get_content_prefix_and_suffix(
self, is_labelled: bool
) -> tuple[str, str]:
prefix_lines = []
suffix_lines = []
if not is_labelled:
prefix_lines.append(self.get_color_command(
color_to_hex(self.base_color)
))
if self.alignment:
prefix_lines.append(self.alignment)
if self.tex_environment:
prefix_lines.append(f"\\begin{{{self.tex_environment}}}")
suffix_lines.append(f"\\end{{{self.tex_environment}}}")
return (
"".join([line + "\n" for line in prefix_lines]),
"".join(["\n" + line for line in suffix_lines])
)
full_string = arg_separator.join(self.tex_strings)
super().__init__(full_string, **kwargs)
self.break_up_by_substrings(self.tex_strings)
self.set_color_by_tex_to_color_map(tex_to_color_map)
# Method alias
if self.organize_left_to_right:
self.organize_submobjects_left_to_right()
def get_parts_by_tex(self, selector: Selector) -> VGroup:
return self.select_parts(selector)
def break_up_tex_strings(self, tex_strings: Iterable[str], substrings_to_isolate: List[str] = []) -> Iterable[str]:
# Separate out any strings specified in the isolate
# or tex_to_color_map lists.
if len(substrings_to_isolate) == 0:
return tex_strings
patterns = (
"({})".format(re.escape(ss))
for ss in substrings_to_isolate
)
pattern = "|".join(patterns)
pieces = []
for s in tex_strings:
if pattern:
pieces.extend(re.split(pattern, s))
else:
pieces.append(s)
return list(filter(lambda s: s, pieces))
def get_part_by_tex(self, selector: Selector, index: int = 0) -> VMobject:
return self.select_part(selector, index)
def break_up_by_substrings(self, tex_strings: Iterable[str]):
"""
Reorganize existing submojects one layer
deeper based on the structure of tex_strings (as a list
of tex_strings)
"""
if len(list(tex_strings)) == 1:
submob = self.copy()
self.set_submobjects([submob])
return self
new_submobjects = []
curr_index = 0
for tex_string in tex_strings:
tex_string = tex_string.strip()
if len(tex_string) == 0:
continue
sub_tex_mob = SingleStringTex(tex_string, math_mode=self.math_mode)
num_submobs = len(sub_tex_mob)
if num_submobs == 0:
continue
new_index = curr_index + num_submobs
sub_tex_mob.set_submobjects(self.submobjects[curr_index:new_index])
new_submobjects.append(sub_tex_mob)
curr_index = new_index
self.set_submobjects(new_submobjects)
return self
def get_parts_by_tex(
self,
tex: str,
substring: bool = True,
case_sensitive: bool = True
) -> VGroup:
def test(tex1, tex2):
if not case_sensitive:
tex1 = tex1.lower()
tex2 = tex2.lower()
if substring:
return tex1 in tex2
else:
return tex1 == tex2
return VGroup(*filter(
lambda m: isinstance(m, SingleStringTex) and test(tex, m.get_tex()),
self.submobjects
))
def get_part_by_tex(self, tex: str, **kwargs) -> SingleStringTex | None:
all_parts = self.get_parts_by_tex(tex, **kwargs)
return all_parts[0] if all_parts else None
def set_color_by_tex(self, tex: str, color: ManimColor, **kwargs):
self.get_parts_by_tex(tex, **kwargs).set_color(color)
return self
def set_color_by_tex(self, selector: Selector, color: ManimColor):
return self.set_parts_color(selector, color)
def set_color_by_tex_to_color_map(
self,
tex_to_color_map: dict[str, ManimColor],
**kwargs
self, color_map: dict[Selector, ManimColor]
):
for tex, color in list(tex_to_color_map.items()):
self.set_color_by_tex(tex, color, **kwargs)
return self
return self.set_parts_color_by_dict(color_map)
def index_of_part(self, part: SingleStringTex, start: int = 0) -> int:
return self.submobjects.index(part, start)
def substr_to_path_count(self, substr: str) -> int:
tex = self.get_tex()
if len(self) != num_tex_symbols(tex):
log.warning(
f"Estimated size of {tex} does not match true size",
)
return num_tex_symbols(substr)
def index_of_part_by_tex(self, tex: str, start: int = 0, **kwargs) -> int:
part = self.get_part_by_tex(tex, **kwargs)
return self.index_of_part(part, start)
def slice_by_tex(
self,
start_tex: str | None = None,
stop_tex: str | None = None,
**kwargs
) -> VGroup:
if start_tex is None:
start_index = 0
else:
start_index = self.index_of_part_by_tex(start_tex, **kwargs)
if stop_tex is None:
return self[start_index:]
else:
stop_index = self.index_of_part_by_tex(stop_tex, start=start_index, **kwargs)
return self[start_index:stop_index]
def sort_alphabetically(self) -> None:
self.submobjects.sort(key=lambda m: m.get_tex())
def set_bstroke(self, color: ManimColor = BLACK, width: float = 4):
self.set_stroke(color, width, background=True)
return self
def get_tex(self) -> str:
return self.get_string()
class TexText(Tex):
def __init__(
self,
*tex_strings: str,
math_mode: bool = False,
arg_separator: str = "",
**kwargs
):
super().__init__(
*tex_strings,
math_mode=math_mode,
arg_separator=arg_separator,
**kwargs
)
class BulletedList(TexText):
def __init__(
self,
*items: str,
buff: float = MED_LARGE_BUFF,
dot_scale_factor: float = 2.0,
alignment: str = "",
**kwargs
):
super().__init__(
*(s + R"\\" for s in items),
alignment=alignment,
**kwargs
)
for part in self:
dot = Tex(R"\cdot").scale(dot_scale_factor)
dot.next_to(part[0], LEFT, SMALL_BUFF)
part.add_to_back(dot)
self.arrange(
DOWN,
aligned_edge=LEFT,
buff=buff
)
def fade_all_but(self, index_or_string: int | str, opacity: float = 0.5) -> None:
arg = index_or_string
if isinstance(arg, str):
part = self.get_part_by_tex(arg)
elif isinstance(arg, int):
part = self.submobjects[arg]
else:
raise Exception("Expected int or string, got {0}".format(arg))
for other_part in self.submobjects:
if other_part is part:
other_part.set_fill(opacity=1)
else:
other_part.set_fill(opacity=opacity)
class TexTextFromPresetString(TexText):
tex: str = ""
default_color: ManimColor = WHITE
def __init__(self, **kwargs):
super().__init__(
self.tex,
color=kwargs.pop("color", self.default_color),
**kwargs
)
class Title(TexText):
def __init__(
self,
*text_parts: str,
scale_factor: float = 1.0,
include_underline: bool = True,
underline_width: float = FRAME_WIDTH - 2,
# This will override underline_width
match_underline_width_to_text: bool = False,
underline_buff: float = SMALL_BUFF,
underline_style: dict = dict(stroke_width=2, stroke_color=GREY_C),
**kwargs
):
super().__init__(*text_parts, **kwargs)
self.scale(scale_factor)
self.to_edge(UP)
if include_underline:
underline = Line(LEFT, RIGHT, **underline_style)
underline.next_to(self, DOWN, buff=underline_buff)
if match_underline_width_to_text:
underline.match_width(self)
else:
underline.set_width(underline_width)
self.add(underline)
self.underline = underline
tex_environment: str = ""

View file

@ -24,21 +24,10 @@ from manimlib.utils.simple_functions import hash_string
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Iterable, Union
from typing import Iterable
from manimlib.mobject.types.vectorized_mobject import VGroup
from manimlib.typing import ManimColor
Span = tuple[int, int]
Selector = Union[
str,
re.Pattern,
tuple[Union[int, None], Union[int, None]],
Iterable[Union[
str,
re.Pattern,
tuple[Union[int, None], Union[int, None]]
]]
]
from manimlib.typing import ManimColor, Span, Selector
TEXT_MOB_SCALE_FACTOR = 0.0076
@ -427,9 +416,10 @@ class Text(MarkupText):
text: str,
# For backward compatibility
isolate: Selector = (re.compile(r"\w+", re.U), re.compile(r"\S+", re.U)),
use_labelled_svg: bool = True,
**kwargs
):
super().__init__(text, isolate=isolate, **kwargs)
super().__init__(text, isolate=isolate, use_labelled_svg=use_labelled_svg, **kwargs)
@staticmethod
def get_command_matches(string: str) -> list[re.Match]:

View file

@ -320,7 +320,6 @@ class VCube(VGroup3D):
)
face = Square(side_length=side_length, **style)
super().__init__(*square_to_cube_faces(face), **style)
self.refresh_unit_normal()
class VPrism(VCube):

View file

@ -53,7 +53,7 @@ class VMobject(Mobject):
fill_shader_folder: str = "quadratic_bezier_fill"
fill_dtype: Sequence[Tuple[str, type, Tuple[int]]] = [
('point', np.float32, (3,)),
('unit_normal', np.float32, (3,)),
('orientation', np.float32, (1,)),
('color', np.float32, (4,)),
('vert_index', np.float32, (1,)),
]
@ -61,7 +61,6 @@ class VMobject(Mobject):
("point", np.float32, (3,)),
("prev_point", np.float32, (3,)),
("next_point", np.float32, (3,)),
('unit_normal', np.float32, (3,)),
("stroke_width", np.float32, (1,)),
("color", np.float32, (4,)),
]
@ -114,7 +113,7 @@ class VMobject(Mobject):
"fill_rgba": np.zeros((1, 4)),
"stroke_rgba": np.zeros((1, 4)),
"stroke_width": np.zeros((1, 1)),
"unit_normal": np.array(OUT, ndmin=2),
"orientation": np.zeros((1, 1)),
})
# These are here just to make type checkers happy
@ -758,9 +757,15 @@ class VMobject(Mobject):
if not self.has_points():
return np.zeros(3)
nppc = self.n_points_per_curve
points = self.get_points()
p0 = points[:-1]
p1 = points[1:]
p0 = points[0::nppc]
p1 = points[nppc - 1::nppc]
if len(p0) != len(p1):
m = min(len(p0), len(p1))
p0 = p0[:m]
p1 = p1[:m]
# Each term goes through all edges [(x1, y1, z1), (x2, y2, z2)]
return 0.5 * np.array([
@ -769,10 +774,7 @@ class VMobject(Mobject):
sum((p0[:, 0] + p1[:, 0]) * (p1[:, 1] - p0[:, 1])), # Add up (x1 + x2)*(y2 - y1)
])
def get_unit_normal(self, recompute: bool = False) -> Vect3:
if not recompute:
return self.data["unit_normal"][0]
def get_unit_normal(self) -> Vect3:
if self.get_num_points() < 3:
return OUT
@ -786,14 +788,8 @@ class VMobject(Mobject):
points[1] - points[0],
points[2] - points[1],
)
self.data["unit_normal"][:] = normal
return normal
def refresh_unit_normal(self):
for mob in self.get_family():
mob.get_unit_normal(recompute=True)
return self
# Alignment
def align_points(self, vmobject: VMobject):
if self.get_num_points() == len(vmobject.get_points()):
@ -948,15 +944,16 @@ class VMobject(Mobject):
def refresh_triangulation(self):
for mob in self.get_family():
mob.needs_new_triangulation = True
mob.data["orientation"] = resize_array(
mob.data["orientation"],
mob.get_num_points()
)
return self
def get_triangulation(self, normal_vector: Vect3 | None = None):
def get_triangulation(self):
# Figure out how to triangulate the interior to know
# how to send the points as to the vertex shader.
# First triangles come directly from the points
if normal_vector is None:
normal_vector = self.get_unit_normal(recompute=True)
if not self.needs_new_triangulation:
return self.triangulation
@ -967,6 +964,7 @@ class VMobject(Mobject):
self.needs_new_triangulation = False
return self.triangulation
normal_vector = self.get_unit_normal()
if not np.isclose(normal_vector, OUT).all():
# Rotate points such that unit normal vector is OUT
points = np.dot(points, z_to_vector(normal_vector))
@ -980,6 +978,8 @@ class VMobject(Mobject):
crosses = cross2d(v01s, v12s)
convexities = np.sign(crosses)
orientations = np.sign(convexities.repeat(3))
self.data["orientation"] = orientations.reshape((len(orientations), 1))
atol = self.tolerance_for_point_equality
end_of_loop = np.zeros(len(b0s), dtype=bool)
@ -1008,13 +1008,13 @@ class VMobject(Mobject):
self.needs_new_triangulation = False
return tri_indices
def triggers_refreshed_triangulation(func):
@staticmethod
def triggers_refreshed_triangulation(func: Callable):
@wraps(func)
def wrapper(self, *args, **kwargs):
old_points = self.get_points().copy()
func(self, *args, **kwargs)
if not np.all(self.get_points() == old_points):
self.refresh_unit_normal()
self.refresh_triangulation()
return wrapper
@ -1023,10 +1023,9 @@ class VMobject(Mobject):
super().set_points(points)
return self
@triggers_refreshed_triangulation
def append_points(self, points: Vect3Array):
super().append_points(points)
self.refresh_unit_normal()
self.refresh_triangulation()
return self
@triggers_refreshed_triangulation
@ -1052,12 +1051,6 @@ class VMobject(Mobject):
self.make_approximately_smooth()
return self
def flip(self, axis: Vect3 = UP, **kwargs):
super().flip(axis, **kwargs)
self.refresh_unit_normal()
self.refresh_triangulation()
return self
# For shaders
def init_shader_data(self):
self.fill_data = np.zeros(0, dtype=self.fill_dtype)
@ -1142,7 +1135,6 @@ class VMobject(Mobject):
self.read_data_to_shader(self.stroke_data, "color", "stroke_rgba")
self.read_data_to_shader(self.stroke_data, "stroke_width", "stroke_width")
self.read_data_to_shader(self.stroke_data, "unit_normal", "unit_normal")
return self.stroke_data
@ -1154,7 +1146,7 @@ class VMobject(Mobject):
self.read_data_to_shader(self.fill_data, "point", "points")
self.read_data_to_shader(self.fill_data, "color", "fill_rgba")
self.read_data_to_shader(self.fill_data, "unit_normal", "unit_normal")
self.read_data_to_shader(self.fill_data, "orientation", "orientation")
return self.fill_data

View file

@ -17,7 +17,7 @@ uniform float gloss;
uniform float shadow;
in vec3 bp[3];
in vec3 v_global_unit_normal[3];
in float v_orientation[3];
in vec4 v_color[3];
in float v_vert_index[3];
@ -32,6 +32,8 @@ out vec2 uv_coords;
out vec2 uv_b2;
out float bezier_degree;
vec3 local_unit_normal;
// Analog of import for manim only
#INSERT quadratic_bezier_geometry_functions.glsl
@ -44,7 +46,7 @@ void emit_vertex_wrapper(vec3 point, int index){
color = finalize_color(
v_color[index],
point,
v_global_unit_normal[index],
local_unit_normal,
light_source_position,
camera_position,
reflectiveness,
@ -128,7 +130,7 @@ void main(){
vec3 new_bp[3];
bezier_degree = get_reduced_control_points(vec3[3](bp[0], bp[1], bp[2]), new_bp);
vec3 local_unit_normal = get_unit_normal(new_bp);
orientation = sign(dot(v_global_unit_normal[0], local_unit_normal));
orientation = v_orientation[0];
if(bezier_degree >= 1){
emit_pentagon(new_bp, local_unit_normal);

View file

@ -3,12 +3,12 @@
#INSERT camera_uniform_declarations.glsl
in vec3 point;
in vec3 unit_normal;
in float orientation;
in vec4 color;
in float vert_index;
out vec3 bp; // Bezier control point
out vec3 v_global_unit_normal;
out float v_orientation;
out vec4 v_color;
out float v_vert_index;
@ -17,7 +17,7 @@ out float v_vert_index;
void main(){
bp = position_point_into_frame(point);
v_global_unit_normal = rotate_point_into_frame(unit_normal);
v_orientation = orientation;
v_color = color;
v_vert_index = vert_index;
}

View file

@ -22,7 +22,6 @@ uniform float shadow;
in vec3 bp[3];
in vec3 prev_bp[3];
in vec3 next_bp[3];
in vec3 v_global_unit_normal[3];
in vec4 v_color[3];
in float v_stroke_width[3];
@ -43,6 +42,8 @@ out float bezier_degree;
out vec2 uv_coords;
out vec2 uv_b2;
vec3 unit_normal;
// Codes for joint types
const float AUTO_JOINT = 0;
const float ROUND_JOINT = 1;
@ -206,6 +207,7 @@ void main() {
vec3 controls[3];
vec3 prev[3];
vec3 next[3];
unit_normal = get_unit_normal(controls);
bezier_degree = get_reduced_control_points(vec3[3](bp[0], bp[1], bp[2]), controls);
if(bezier_degree == 0.0) return; // Null curve
int degree = int(bezier_degree);
@ -219,7 +221,7 @@ void main() {
float sf = perspective_scale_factor(controls[i].z, focal_distance);
if(bool(flat_stroke)){
vec3 to_cam = normalize(vec3(0.0, 0.0, focal_distance) - controls[i]);
sf *= abs(dot(v_global_unit_normal[i], to_cam));
sf *= abs(dot(unit_normal, to_cam));
}
scaled_strokes[i] = v_stroke_width[i] * sf;
}
@ -259,7 +261,7 @@ void main() {
color = finalize_color(
v_color[index_map[i]],
xyz_coords,
v_global_unit_normal[index_map[i]],
unit_normal,
light_source_position,
camera_position,
reflectiveness,

View file

@ -14,7 +14,6 @@ in vec4 color;
out vec3 bp;
out vec3 prev_bp;
out vec3 next_bp;
out vec3 v_global_unit_normal;
out float v_stroke_width;
out vec4 v_color;
@ -27,7 +26,6 @@ void main(){
bp = position_point_into_frame(point);
prev_bp = position_point_into_frame(prev_point);
next_bp = position_point_into_frame(next_point);
v_global_unit_normal = rotate_point_into_frame(unit_normal);
v_stroke_width = STROKE_WIDTH_CONVERSION * stroke_width * frame_shape[1] / 8.0;
v_color = color;

View file

@ -1,14 +1,24 @@
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Union, Tuple, Annotated, Literal
from typing import Union, Tuple, Annotated, Literal, Iterable
from colour import Color
import numpy as np
import re
# Abbreviations for a common types
ManimColor = Union[str, Color, None]
RangeSpecifier = Tuple[float, float, float] | Tuple[float, float]
Span = tuple[int, int]
SingleSelector = Union[
str,
re.Pattern,
tuple[Union[int, None], Union[int, None]],
]
Selector = Union[SingleSelector, Iterable[SingleSelector]]
# These are various alternate names for np.ndarray meant to specify
# certain shapes.
#

View file

@ -63,7 +63,7 @@ def color_to_int_rgba(color: ManimColor, opacity: float = 1.0) -> np.ndarray[int
def color_to_hex(color: ManimColor) -> str:
return Color(color).hex.upper()
return Color(color).get_hex_l().upper()
def hex_to_int(rgb_hex: str) -> int:

View file

@ -10,7 +10,7 @@ import numpy as np
from scipy.spatial.transform import Rotation
from tqdm import tqdm as ProgressDisplay
from manimlib.constants import DOWN, OUT, RIGHT
from manimlib.constants import DOWN, OUT, RIGHT, UP
from manimlib.constants import PI, TAU
from manimlib.utils.iterables import adjacent_pairs
from manimlib.utils.simple_functions import clip
@ -134,11 +134,18 @@ def rotation_about_z(angle: float) -> Matrix3x3:
def rotation_between_vectors(v1: Vect3, v2: Vect3) -> Matrix3x3:
if np.all(np.isclose(v1, v2)):
if np.isclose(v1, v2).all():
return np.identity(3)
axis = np.cross(v1, v2)
if np.isclose(axis, [0, 0, 0]).all():
# v1 and v2 align
axis = np.cross(v1, RIGHT)
if np.isclose(axis, [0, 0, 0]).all():
# v1 and v2 _and_ RIGHT all align
axis = np.cross(v1, UP)
return rotation_matrix(
angle=angle_between_vectors(v1, v2),
axis=np.cross(v1, v2)
axis=axis,
)
@ -178,8 +185,7 @@ def normalize_along_axis(
norms = np.sqrt((array * array).sum(axis))
norms[norms == 0] = 1
buffed_norms = np.repeat(norms, array.shape[axis]).reshape(array.shape)
array /= buffed_norms
return array
return array / buffed_norms
def get_unit_normal(
@ -384,9 +390,10 @@ def earclip_triangulation(verts: Vect3Array | Vect2Array, ring_ends: list[int])
list(range(e0, e1))
for e0, e1 in zip([0, *ring_ends], ring_ends)
]
epsilon = 1e-6
def is_in(point, ring_id):
return abs(abs(get_winding_number([i - point for i in verts[rings[ring_id]]])) - 1) < 1e-5
return abs(abs(get_winding_number([i - point for i in verts[rings[ring_id]]])) - 1) < epsilon
def ring_area(ring_id):
ring = rings[ring_id]
@ -397,8 +404,8 @@ def earclip_triangulation(verts: Vect3Array | Vect2Array, ring_ends: list[int])
# Points at the same position may cause problems
for i in rings:
verts[i[0]] += (verts[i[1]] - verts[i[0]]) * 1e-6
verts[i[-1]] += (verts[i[-2]] - verts[i[-1]]) * 1e-6
verts[i[0]] += (verts[i[1]] - verts[i[0]]) * epsilon
verts[i[-1]] += (verts[i[-2]] - verts[i[-1]]) * epsilon
# First, we should know which rings are directly contained in it for each ring

57
manimlib/utils/tex.py Normal file
View file

@ -0,0 +1,57 @@
from __future__ import annotations
import re
from functools import lru_cache
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import List, Tuple
@lru_cache(maxsize=1)
def get_pattern_symbol_count_pairs() -> List[Tuple[str, int]]:
from manimlib.utils.tex_to_symbol_count import TEX_TO_SYMBOL_COUNT
# Gather all keys of previous map, grouped by common value
count_to_tex_list = dict()
for command, num in TEX_TO_SYMBOL_COUNT.items():
if num not in count_to_tex_list:
count_to_tex_list[num] = []
count_to_tex_list[num].append(command)
# Create a list associating each count with a regular expression
# that will find any tex commands matching that list
pattern_symbol_count_pairs = list()
# Account for patterns like \begin{align} and \phantom{thing}
# which, together with the bracketed content account for zero paths.
# Deliberately put this first in the list
tex_list = ["begin", "end", "phantom"]
pattern_symbol_count_pairs.append(
("|".join(r"\\" + s + r"\{[^\\}]+\}" for s in tex_list), 0)
)
for count, tex_list in count_to_tex_list.items():
pattern = "|".join(r"\\" + s + r"(\s|\\)" + s for s in tex_list)
pattern_symbol_count_pairs.append((pattern, count))
# Assume all other expressions of the form \thing are drawn with one path
# Deliberately put this last in the list
pattern_symbol_count_pairs.append((r"\\[a-zA-Z]+", 1))
return pattern_symbol_count_pairs
def num_tex_symbols(tex: str) -> int:
"""
This function attempts to estimate the number of symbols that
a given string of tex would produce.
"""
total = 0
for pattern, count in get_pattern_symbol_count_pairs():
total += count * len(re.findall(pattern, tex))
tex = re.sub(pattern, " ", tex) # Remove that pattern
# Count remaining characters
total += sum(map(lambda c: c not in "^{} \n\t_$", tex))
return total

View file

@ -0,0 +1,182 @@
TEX_TO_SYMBOL_COUNT = {
"!": 0,
",": 0,
",": 0,
"-": 0,
"-": 0,
"/": 0,
":": 0,
";": 0,
";": 0,
">": 0,
"aa": 0,
"AA": 0,
"ae": 0,
"AE": 0,
"arccos": 6,
"arcsin": 6,
"arctan": 6,
"arg": 3,
"author": 0,
"bf": 0,
"bibliography": 0,
"bibliographystyle": 0,
"big": 0,
"Big": 0,
"bigodot": 4,
"bigoplus": 5,
"bigskip": 0,
"bmod": 3,
"boldmath": 0,
"bottomfraction": 2,
"bowtie": 2,
"cal": 0,
"cdots": 3,
"centering": 0,
"cite": 2,
"cong": 2,
"contentsline": 0,
"cos": 3,
"cosh": 4,
"cot": 3,
"coth": 4,
"csc": 3,
"date": 0,
"dblfloatpagefraction": 2,
"dbltopfraction": 2,
"ddots": 3,
"deg": 3,
"det": 3,
"dim": 3,
"displaystyle": 0,
"div": 2,
"doteq": 2,
"dotfill": 0,
"emph": 0,
"exp": 3,
"fbox": 4,
"floatpagefraction": 2,
"flushbottom": 0,
"footnotesize": 0,
"footnotetext": 0,
"frame": 2,
"framebox": 4,
"fussy": 0,
"gcd": 3,
"ghost": 0,
"glossary": 0,
"hfill": 0,
"hom": 3,
"hookleftarrow": 2,
"hookrightarrow": 2,
"hrulefill": 0,
"huge": 0,
"Huge": 0,
"hyphenation": 0,
"iff": 2,
"Im": 2,
"index": 0,
"inf": 3,
"it": 0,
"ker": 3,
"l": 0,
"L": 0,
"label": 0,
"large": 0,
"Large": 0,
"LARGE": 0,
"ldots": 3,
"lefteqn": 0,
"lg": 2,
"lim": 3,
"liminf": 6,
"limsup": 6,
"linebreak": 0,
"ln": 2,
"log": 3,
"longleftarrow": 2,
"Longleftarrow": 2,
"longleftrightarrow": 2,
"Longleftrightarrow": 2,
"longmapsto": 3,
"longrightarrow": 2,
"Longrightarrow": 2,
"makebox": 0,
"mapsto": 2,
"markright": 0,
"max": 3,
"mbox": 0,
"medskip": 0,
"min": 3,
"mit": 0,
"models": 2,
"ne": 2,
"neq": 2,
"newline": 0,
"noindent": 0,
"nolinebreak": 0,
"nonumber": 0,
"nopagebreak": 0,
"normalmarginpar": 0,
"normalsize": 0,
"notin": 2,
"o": 0,
"O": 0,
"obeycr": 0,
"oe": 0,
"OE": 0,
"overbrace": 4,
"pagebreak": 0,
"pagenumbering": 0,
"pageref": 2,
"pmod": 5,
"Pr": 2,
"protect": 0,
"qquad": 0,
"quad": 0,
"raggedbottom": 0,
"raggedleft": 0,
"raggedright": 0,
"Re": 2,
"ref": 2,
"restorecr": 0,
"reversemarginpar": 0,
"rm": 0,
"sc": 0,
"scriptscriptstyle": 0,
"scriptsize": 0,
"scriptstyle": 0,
"sec": 3,
"sf": 0,
"shortstack": 0,
"sin": 3,
"sinh": 4,
"sl": 0,
"sloppy": 0,
"small": 0,
"Small": 0,
"smallskip": 0,
"sqrt": 2,
"ss": 0,
"sup": 3,
"tan": 3,
"tanh": 4,
"textbf": 0,
"textfraction": 2,
"textstyle": 0,
"thicklines": 0,
"thinlines": 0,
"thinspace": 0,
"tiny": 0,
"title": 0,
"today": 15,
"topfraction": 2,
"tt": 0,
"typeout": 0,
"unboldmath": 0,
"underbrace": 6,
"underline": 0,
"value": 0,
"vdots": 3,
"vline": 0
}