mirror of
https://github.com/3b1b/manim.git
synced 2025-11-14 07:27:44 +00:00
Merge pull request #1943 from 3b1b/string-mobject-refactor
StringMobject refactor
This commit is contained in:
commit
fcff44a66b
29 changed files with 1027 additions and 749 deletions
|
|
@ -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])
|
||||
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
|
||||
|
|
|
|||
|
|
@ -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 *
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 = ""
|
||||
339
manimlib/mobject/svg/old_tex_mobject.py
Normal file
339
manimlib/mobject/svg/old_tex_mobject.py
Normal 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
|
||||
)
|
||||
80
manimlib/mobject/svg/special_tex.py
Normal file
80
manimlib/mobject/svg/special_tex.py
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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 = ""
|
||||
|
|
|
|||
|
|
@ -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]:
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
#
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
57
manimlib/utils/tex.py
Normal 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
|
||||
182
manimlib/utils/tex_to_symbol_count.py
Normal file
182
manimlib/utils/tex_to_symbol_count.py
Normal 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
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue