From dd662b0d1265f2852b933a14e6fe57e76fc715dc Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 20 Dec 2022 09:52:24 -0800 Subject: [PATCH 01/36] Move Selector and Span to manimlib.typing --- manimlib/mobject/svg/mtex_mobject.py | 17 +---------------- manimlib/mobject/svg/string_mobject.py | 16 ++-------------- manimlib/mobject/svg/text_mobject.py | 15 ++------------- manimlib/typing.py | 12 +++++++++++- 4 files changed, 16 insertions(+), 44 deletions(-) diff --git a/manimlib/mobject/svg/mtex_mobject.py b/manimlib/mobject/svg/mtex_mobject.py index 1a545666..c6c0102a 100644 --- a/manimlib/mobject/svg/mtex_mobject.py +++ b/manimlib/mobject/svg/mtex_mobject.py @@ -10,23 +10,8 @@ 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]] - ]] - ] + from manimlib.typing import ManimColor, Span, Selector SCALE_FACTOR_PER_FONT_POINT = 0.001 diff --git a/manimlib/mobject/svg/string_mobject.py b/manimlib/mobject/svg/string_mobject.py index a21ed9b4..b97da8df 100644 --- a/manimlib/mobject/svg/string_mobject.py +++ b/manimlib/mobject/svg/string_mobject.py @@ -17,20 +17,8 @@ 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): diff --git a/manimlib/mobject/svg/text_mobject.py b/manimlib/mobject/svg/text_mobject.py index f504cc01..7fea568f 100644 --- a/manimlib/mobject/svg/text_mobject.py +++ b/manimlib/mobject/svg/text_mobject.py @@ -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 diff --git a/manimlib/typing.py b/manimlib/typing.py index 6764b412..5e383791 100644 --- a/manimlib/typing.py +++ b/manimlib/typing.py @@ -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. # From d4417d3d07e3ab511ae87f7365680dc8ddae0421 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 20 Dec 2022 09:52:40 -0800 Subject: [PATCH 02/36] Add t2c option to MTex --- manimlib/mobject/svg/mtex_mobject.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/manimlib/mobject/svg/mtex_mobject.py b/manimlib/mobject/svg/mtex_mobject.py index c6c0102a..bcb0e867 100644 --- a/manimlib/mobject/svg/mtex_mobject.py +++ b/manimlib/mobject/svg/mtex_mobject.py @@ -28,6 +28,7 @@ class MTex(StringMobject): template: str = "", additional_preamble: str = "", tex_to_color_map: dict = dict(), + t2c: dict = dict(), **kwargs ): # Prevent from passing an empty string. @@ -37,14 +38,11 @@ class MTex(StringMobject): self.alignment = alignment self.template = template self.additional_preamble = additional_preamble - self.tex_to_color_map = dict(tex_to_color_map) + self.tex_to_color_map = dict(**t2c, **tex_to_color_map) - super().__init__( - tex_string, - **kwargs - ) + super().__init__(tex_string, **kwargs) - self.set_color_by_tex_to_color_map(tex_to_color_map) + self.set_color_by_tex_to_color_map(self.tex_to_color_map) self.scale(SCALE_FACTOR_PER_FONT_POINT * font_size) @property @@ -65,10 +63,9 @@ class MTex(StringMobject): ) def get_file_path_by_content(self, content: str) -> str: - file_path = tex_content_to_svg_file( + return tex_content_to_svg_file( content, self.template, self.additional_preamble, self.tex_string ) - return file_path # Parsing From 1a485ddd1919b5ab57adf9971041efe1324518af Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 20 Dec 2022 12:21:09 -0800 Subject: [PATCH 03/36] Ensure color_to_hex always returns 6 character hex codes --- manimlib/utils/color.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manimlib/utils/color.py b/manimlib/utils/color.py index f6b18e2e..d35a40ce 100644 --- a/manimlib/utils/color.py +++ b/manimlib/utils/color.py @@ -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: From 9c106eb8732ad7ff1414981542bc8751478b684a Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 20 Dec 2022 12:22:25 -0800 Subject: [PATCH 04/36] Add option for StringMobject to only render a single svg Then set as the default behavior for MTex and Text --- manimlib/mobject/svg/mtex_mobject.py | 3 ++- manimlib/mobject/svg/svg_mobject.py | 22 ++++++++++------------ manimlib/mobject/svg/text_mobject.py | 3 ++- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/manimlib/mobject/svg/mtex_mobject.py b/manimlib/mobject/svg/mtex_mobject.py index bcb0e867..0f66e5af 100644 --- a/manimlib/mobject/svg/mtex_mobject.py +++ b/manimlib/mobject/svg/mtex_mobject.py @@ -29,6 +29,7 @@ class MTex(StringMobject): additional_preamble: str = "", tex_to_color_map: dict = dict(), t2c: dict = dict(), + use_labelled_svg: bool = True, **kwargs ): # Prevent from passing an empty string. @@ -40,7 +41,7 @@ class MTex(StringMobject): self.additional_preamble = additional_preamble self.tex_to_color_map = dict(**t2c, **tex_to_color_map) - super().__init__(tex_string, **kwargs) + super().__init__(tex_string, use_labelled_svg=use_labelled_svg, **kwargs) self.set_color_by_tex_to_color_map(self.tex_to_color_map) self.scale(SCALE_FACTOR_PER_FONT_POINT * font_size) diff --git a/manimlib/mobject/svg/svg_mobject.py b/manimlib/mobject/svg/svg_mobject.py index fa2286cf..97cccdbd 100644 --- a/manimlib/mobject/svg/svg_mobject.py +++ b/manimlib/mobject/svg/svg_mobject.py @@ -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)): diff --git a/manimlib/mobject/svg/text_mobject.py b/manimlib/mobject/svg/text_mobject.py index 7fea568f..b89081bf 100644 --- a/manimlib/mobject/svg/text_mobject.py +++ b/manimlib/mobject/svg/text_mobject.py @@ -416,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]: From 6176bcd45a9d1fc06d4f9478d9bb9fe91f699f2c Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 20 Dec 2022 12:23:19 -0800 Subject: [PATCH 05/36] Add option for StringMobject to only render one svg --- manimlib/mobject/svg/string_mobject.py | 108 +++++++++++++++---------- 1 file changed, 67 insertions(+), 41 deletions(-) diff --git a/manimlib/mobject/svg/string_mobject.py b/manimlib/mobject/svg/string_mobject.py index b97da8df..adc160a6 100644 --- a/manimlib/mobject/svg/string_mobject.py +++ b/manimlib/mobject/svg/string_mobject.py @@ -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 @@ -55,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__( @@ -72,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). " + \ @@ -123,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 From e0725c111e82fe739bd2bb1fae2dc4dd7edef987 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 20 Dec 2022 14:09:53 -0800 Subject: [PATCH 06/36] Allow MTex to accept multiple strings as an argument --- manimlib/mobject/svg/mtex_mobject.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/manimlib/mobject/svg/mtex_mobject.py b/manimlib/mobject/svg/mtex_mobject.py index 0f66e5af..4cacdc93 100644 --- a/manimlib/mobject/svg/mtex_mobject.py +++ b/manimlib/mobject/svg/mtex_mobject.py @@ -22,26 +22,41 @@ class MTex(StringMobject): def __init__( self, - tex_string: str, + *tex_strings: str, font_size: int = 48, alignment: str = R"\centering", 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.alignment = alignment self.template = template self.additional_preamble = additional_preamble self.tex_to_color_map = dict(**t2c, **tex_to_color_map) - super().__init__(tex_string, use_labelled_svg=use_labelled_svg, **kwargs) + super().__init__( + tex_string, + use_labelled_svg=use_labelled_svg, + isolate=isolate, + **kwargs + ) self.set_color_by_tex_to_color_map(self.tex_to_color_map) self.scale(SCALE_FACTOR_PER_FONT_POINT * font_size) From 3293f72adc904f7599a8815f29be21441214f0e1 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 20 Dec 2022 14:48:54 -0800 Subject: [PATCH 07/36] Move BulletedList, TexTextFromPresetString and Title to their own file --- manimlib/__init__.py | 1 + manimlib/mobject/svg/drawings.py | 2 +- manimlib/mobject/svg/special_tex.py | 94 +++++++++++++++++++++++++++++ manimlib/mobject/svg/tex_mobject.py | 84 +------------------------- 4 files changed, 97 insertions(+), 84 deletions(-) create mode 100644 manimlib/mobject/svg/special_tex.py diff --git a/manimlib/__init__.py b/manimlib/__init__.py index 17d3da72..7704b352 100644 --- a/manimlib/__init__.py +++ b/manimlib/__init__.py @@ -46,6 +46,7 @@ from manimlib.mobject.svg.drawings import * from manimlib.mobject.svg.mtex_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 * diff --git a/manimlib/mobject/svg/drawings.py b/manimlib/mobject/svg/drawings.py index 8603bfa4..b26179ec 100644 --- a/manimlib/mobject/svg/drawings.py +++ b/manimlib/mobject/svg/drawings.py @@ -45,7 +45,7 @@ from manimlib.mobject.mobject import Mobject 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 diff --git a/manimlib/mobject/svg/special_tex.py b/manimlib/mobject/svg/special_tex.py new file mode 100644 index 00000000..e4a3539f --- /dev/null +++ b/manimlib/mobject/svg/special_tex.py @@ -0,0 +1,94 @@ +from __future__ import annotations + +from manimlib.constants import 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.tex_mobject import Tex, TexText +from manimlib.mobject.svg.mtex_mobject import MTex, MTexText + + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from manimlib.typing import ManimColor + + + +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 diff --git a/manimlib/mobject/svg/tex_mobject.py b/manimlib/mobject/svg/tex_mobject.py index 2b5b38bc..cfcd1c7b 100644 --- a/manimlib/mobject/svg/tex_mobject.py +++ b/manimlib/mobject/svg/tex_mobject.py @@ -4,11 +4,7 @@ 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.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 @@ -341,81 +337,3 @@ class TexText(Tex): 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 From 0d525baf29f50c24a16629ee7b3bd2245770bd8b Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 20 Dec 2022 15:08:54 -0800 Subject: [PATCH 08/36] Clean up special_tex contents, make each class use MTexText instead of TexText --- manimlib/mobject/svg/special_tex.py | 68 ++++++++++++----------------- 1 file changed, 27 insertions(+), 41 deletions(-) diff --git a/manimlib/mobject/svg/special_tex.py b/manimlib/mobject/svg/special_tex.py index e4a3539f..9929b589 100644 --- a/manimlib/mobject/svg/special_tex.py +++ b/manimlib/mobject/svg/special_tex.py @@ -1,61 +1,48 @@ from __future__ import annotations -from manimlib.constants import WHITE, GREY_C +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.svg.tex_mobject import Tex, TexText -from manimlib.mobject.svg.mtex_mobject import MTex, MTexText +from manimlib.mobject.types.vectorized_mobject import VGroup +from manimlib.mobject.svg.mtex_mobject import MTexText from typing import TYPE_CHECKING if TYPE_CHECKING: - from manimlib.typing import ManimColor + from manimlib.typing import ManimColor, Vect3 -class BulletedList(TexText): +class BulletedList(VGroup): def __init__( self, *items: str, buff: float = MED_LARGE_BUFF, - dot_scale_factor: float = 2.0, - alignment: str = "", + aligned_edge: Vect3 = LEFT, **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 - ) + ): + labelled_content = [R"\item " + item for item in items] + tex_string = "\n".join([ + R"\begin{itemize}", + *labelled_content, + R"\end{itemize}" + ]) + tex_text = MTexText(tex_string, isolate=labelled_content, **kwargs) + lines = (tex_text.select_part(part) for part in labelled_content) - 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) + 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): +class TexTextFromPresetString(MTexText): tex: str = "" default_color: ManimColor = WHITE @@ -67,11 +54,11 @@ class TexTextFromPresetString(TexText): ) -class Title(TexText): +class Title(MTexText): def __init__( self, *text_parts: str, - scale_factor: float = 1.0, + font_size: int = 72, include_underline: bool = True, underline_width: float = FRAME_WIDTH - 2, # This will override underline_width @@ -80,9 +67,8 @@ class Title(TexText): 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) + 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) From 6d5b980d4a35cb7846e54f56ac1ab4b82576f5d5 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 20 Dec 2022 15:22:34 -0800 Subject: [PATCH 09/36] Replace Tex and MTex throughout library --- logo/logo.py | 8 ++++---- manimlib/mobject/coordinate_systems.py | 19 ++++++++++--------- manimlib/mobject/matrix.py | 22 +++++++++++----------- manimlib/mobject/probability.py | 8 ++++---- manimlib/mobject/svg/brace.py | 8 ++++---- manimlib/mobject/svg/drawings.py | 9 +++++---- manimlib/scene/interactive_scene.py | 6 +++--- 7 files changed, 41 insertions(+), 39 deletions(-) diff --git a/logo/logo.py b/logo/logo.py index f44da45d..109d9c9e 100644 --- a/logo/logo.py +++ b/logo/logo.py @@ -60,10 +60,10 @@ class Thumbnail(GraphScene): triangle.scale(0.1) # - x_label_p1 = Tex("a") - output_label_p1 = Tex("f(a)") - x_label_p2 = Tex("b") - output_label_p2 = Tex("f(b)") + x_label_p1 = MTex("a") + output_label_p1 = MTex("f(a)") + x_label_p2 = MTex("b") + output_label_p2 = MTex("f(b)") v_line_p1 = get_v_line(input_tracker_p1) v_line_p2 = get_v_line(input_tracker_p2) h_line_p1 = get_h_line(input_tracker_p1) diff --git a/manimlib/mobject/coordinate_systems.py b/manimlib/mobject/coordinate_systems.py index c2620c47..49e5754d 100644 --- a/manimlib/mobject/coordinate_systems.py +++ b/manimlib/mobject/coordinate_systems.py @@ -18,7 +18,7 @@ from manimlib.mobject.geometry import DashedLine from manimlib.mobject.geometry import Line from manimlib.mobject.geometry import Rectangle from manimlib.mobject.number_line import NumberLine -from manimlib.mobject.svg.tex_mobject import Tex +from manimlib.mobject.svg.mtex_mobject import MTex from manimlib.mobject.types.dot_cloud import DotCloud from manimlib.mobject.types.surface import ParametricSurface from manimlib.mobject.types.vectorized_mobject import VGroup @@ -105,7 +105,7 @@ class CoordinateSystem(ABC): edge: Vect3 = RIGHT, direction: Vect3 = DL, **kwargs - ) -> Tex: + ) -> MTex: return self.get_axis_label( label_tex, self.get_x_axis(), edge, direction, **kwargs @@ -117,7 +117,7 @@ class CoordinateSystem(ABC): edge: Vect3 = UP, direction: Vect3 = DR, **kwargs - ) -> Tex: + ) -> MTex: return self.get_axis_label( label_tex, self.get_y_axis(), edge, direction, **kwargs @@ -130,8 +130,8 @@ class CoordinateSystem(ABC): edge: Vect3, direction: Vect3, buff: float = MED_SMALL_BUFF - ) -> Tex: - label = Tex(label_tex) + ) -> MTex: + label = MTex(label_tex) label.next_to( axis.get_edge_center(edge), direction, buff=buff @@ -268,9 +268,9 @@ class CoordinateSystem(ABC): direction: Vect3 = RIGHT, buff: float = MED_SMALL_BUFF, color: ManimColor | None = None - ) -> Tex | Mobject: + ) -> MTex | Mobject: if isinstance(label, str): - label = Tex(label) + label = MTex(label) if color is None: label.match_color(graph) if x is None: @@ -537,7 +537,7 @@ class ThreeDAxes(Axes): def add_axis_labels(self, x_tex="x", y_tex="y", z_tex="z", font_size=24, buff=0.2): x_label, y_label, z_label = labels = VGroup(*( - Tex(tex, font_size=font_size) + MTex(tex, font_size=font_size) for tex in [x_tex, y_tex, z_tex] )) z_label.rotate(PI / 2, RIGHT) @@ -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]) diff --git a/manimlib/mobject/matrix.py b/manimlib/mobject/matrix.py index 0dc8e7ec..c827cf85 100644 --- a/manimlib/mobject/matrix.py +++ b/manimlib/mobject/matrix.py @@ -10,8 +10,8 @@ from manimlib.constants import WHITE from manimlib.mobject.numbers import DecimalNumber from manimlib.mobject.numbers import Integer from manimlib.mobject.shape_matchers import BackgroundRectangle -from manimlib.mobject.svg.tex_mobject import Tex -from manimlib.mobject.svg.tex_mobject import TexText +from manimlib.mobject.svg.mtex_mobject import MTex +from manimlib.mobject.svg.mtex_mobject import MTexText from manimlib.mobject.types.vectorized_mobject import VGroup from manimlib.mobject.types.vectorized_mobject import VMobject @@ -41,8 +41,8 @@ def matrix_to_tex_string(matrix: npt.ArrayLike) -> str: return prefix + R" \\ ".join(rows) + suffix -def matrix_to_mobject(matrix: npt.ArrayLike) -> Tex: - return Tex(matrix_to_tex_string(matrix)) +def matrix_to_mobject(matrix: npt.ArrayLike) -> MTex: + return MTex(matrix_to_tex_string(matrix)) def vector_coordinate_label( @@ -109,7 +109,7 @@ class Matrix(VMobject): def element_to_mobject(self, element: str | float | VMobject, **config) -> VMobject: if isinstance(element, VMobject): return element - return Tex(str(element), **config) + return MTex(str(element), **config) def matrix_to_mob_matrix( self, @@ -142,11 +142,11 @@ class Matrix(VMobject): def add_brackets(self, v_buff: float, h_buff: float): height = len(self.mob_matrix) - brackets = Tex("".join(( + brackets = MTex("".join(( 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,22 +219,22 @@ def get_det_text( background_rect: bool = False, initial_scale_factor: int = 2 ) -> VGroup: - parens = Tex("(", ")") + parens = MTex("()") parens.scale(initial_scale_factor) parens.stretch_to_fit_height(matrix.get_height()) l_paren, r_paren = parens.split() l_paren.next_to(matrix, LEFT, buff=0.1) r_paren.next_to(matrix, RIGHT, buff=0.1) - det = TexText("det") + det = MTexText("det") det.scale(initial_scale_factor) det.next_to(l_paren, LEFT, buff=0.1) if background_rect: det.add_background_rectangle() det_text = VGroup(det, l_paren, r_paren) if determinant is not None: - eq = Tex("=") + eq = MTex("=") eq.next_to(r_paren, RIGHT, buff=0.1) - result = Tex(str(determinant)) + result = MTex(str(determinant)) result.next_to(eq, RIGHT, buff=0.2) det_text.add(eq, result) return det_text diff --git a/manimlib/mobject/probability.py b/manimlib/mobject/probability.py index 44a3fbf5..b519e26c 100644 --- a/manimlib/mobject/probability.py +++ b/manimlib/mobject/probability.py @@ -9,7 +9,7 @@ from manimlib.mobject.geometry import Line from manimlib.mobject.geometry import Rectangle from manimlib.mobject.mobject import Mobject from manimlib.mobject.svg.brace import Brace -from manimlib.mobject.svg.tex_mobject import Tex +from manimlib.mobject.svg.mtex_mobject import MTex from manimlib.mobject.svg.tex_mobject import TexText from manimlib.mobject.types.vectorized_mobject import VGroup from manimlib.utils.color import color_gradient @@ -132,7 +132,7 @@ class SampleSpace(Rectangle): if isinstance(label, Mobject): label_mob = label else: - label_mob = Tex(label) + label_mob = MTex(label) label_mob.scale(self.default_label_scale_val) label_mob.next_to(brace, direction, buff) @@ -266,7 +266,7 @@ class BarChart(VGroup): if self.label_y_axis: labels = VGroup() for y_tick, value in zip(y_ticks, values): - label = Tex(str(np.round(value, 2))) + label = MTex(str(np.round(value, 2))) label.set_height(self.y_axis_label_height) label.next_to(y_tick, LEFT, SMALL_BUFF) labels.add(label) @@ -289,7 +289,7 @@ class BarChart(VGroup): bar_labels = VGroup() for bar, name in zip(bars, self.bar_names): - label = Tex(str(name)) + label = MTex(str(name)) label.scale(self.bar_label_scale_val) label.next_to(bar, DOWN, SMALL_BUFF) bar_labels.add(label) diff --git a/manimlib/mobject/svg/brace.py b/manimlib/mobject/svg/brace.py index 53c84a7e..1ad27377 100644 --- a/manimlib/mobject/svg/brace.py +++ b/manimlib/mobject/svg/brace.py @@ -12,7 +12,7 @@ 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.mtex_mobject import MTex from manimlib.mobject.svg.tex_mobject import TexText from manimlib.mobject.svg.text_mobject import Text from manimlib.mobject.types.vectorized_mobject import VGroup @@ -92,8 +92,8 @@ class Brace(SingleStringTex): self.put_at_tip(text_mob, buff=buff) return text_mob - def get_tex(self, *tex: str, **kwargs) -> Tex: - tex_mob = Tex(*tex) + def get_tex(self, *tex: str, **kwargs) -> MTex: + tex_mob = MTex(*tex) self.put_at_tip(tex_mob, **kwargs) return tex_mob @@ -109,7 +109,7 @@ class Brace(SingleStringTex): class BraceLabel(VMobject): - label_constructor: type = Tex + label_constructor: type = MTex def __init__( self, diff --git a/manimlib/mobject/svg/drawings.py b/manimlib/mobject/svg/drawings.py index b26179ec..ed7920a6 100644 --- a/manimlib/mobject/svg/drawings.py +++ b/manimlib/mobject/svg/drawings.py @@ -42,9 +42,10 @@ 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.mtex_mobject import MTex +from manimlib.mobject.svg.mtex_mobject import MTexText from manimlib.mobject.svg.special_tex import TexTextFromPresetString from manimlib.mobject.three_dimensions import Prismify from manimlib.mobject.three_dimensions import VCube @@ -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) @@ -426,7 +427,7 @@ class Bubble(SVGMobject): return self.content def write(self, *text): - self.add_content(TexText(*text)) + self.add_content(MTexText(*text)) return self def resize_to_content(self, buff=0.75): diff --git a/manimlib/scene/interactive_scene.py b/manimlib/scene/interactive_scene.py index b3eb0fb2..f9a29c1e 100644 --- a/manimlib/scene/interactive_scene.py +++ b/manimlib/scene/interactive_scene.py @@ -17,7 +17,7 @@ from manimlib.mobject.geometry import Square from manimlib.mobject.mobject import Group from manimlib.mobject.mobject import Mobject from manimlib.mobject.numbers import DecimalNumber -from manimlib.mobject.svg.tex_mobject import Tex +from manimlib.mobject.svg.mtex_mobject import MTex from manimlib.mobject.svg.text_mobject import Text from manimlib.mobject.types.dot_cloud import DotCloud from manimlib.mobject.types.vectorized_mobject import VGroup @@ -61,7 +61,7 @@ class InteractiveScene(Scene): Command + 'c' copies the ids of selections to clipboard Command + 'v' will paste either: - The copied mobject - - A Tex mobject based on copied LaTeX + - A MTex mobject based on copied LaTeX - A Text mobject based on copied Text Command + 'z' restores selection back to its original state Command + 's' saves the selected mobjects to file @@ -358,7 +358,7 @@ class InteractiveScene(Scene): # Otherwise, treat as tex or text if set("\\^=+").intersection(clipboard_str): # Proxy to text for LaTeX try: - new_mob = Tex(clipboard_str) + new_mob = MTex(clipboard_str) except LatexError: return else: From e357885da020e67f8dbc505a49f648c785dc4cdf Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 20 Dec 2022 15:24:38 -0800 Subject: [PATCH 10/36] Replace TexText with MTexText --- docs/source/getting_started/example_scenes.rst | 2 +- example_scenes.py | 2 +- logo/logo.py | 2 +- manimlib/mobject/probability.py | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/source/getting_started/example_scenes.rst b/docs/source/getting_started/example_scenes.rst index e51621c5..9d09e0c2 100644 --- a/docs/source/getting_started/example_scenes.rst +++ b/docs/source/getting_started/example_scenes.rst @@ -700,7 +700,7 @@ OpeningManimExample moving_c_grid.prepare_for_nonlinear_transform() c_grid.set_stroke(BLUE_E, 1) c_grid.add_coordinate_labels(font_size=24) - complex_map_words = TexText(""" + complex_map_words = MTexText(""" Or thinking of the plane as $\\mathds{C}$,\\\\ this is the map $z \\rightarrow z^2$ """) diff --git a/example_scenes.py b/example_scenes.py index 5dfa4756..cce59dbb 100644 --- a/example_scenes.py +++ b/example_scenes.py @@ -47,7 +47,7 @@ class OpeningManimExample(Scene): moving_c_grid.prepare_for_nonlinear_transform() c_grid.set_stroke(BLUE_E, 1) c_grid.add_coordinate_labels(font_size=24) - complex_map_words = TexText(""" + complex_map_words = MTexText(""" Or thinking of the plane as $\\mathds{C}$,\\\\ this is the map $z \\rightarrow z^2$ """) diff --git a/logo/logo.py b/logo/logo.py index 109d9c9e..a79791ce 100644 --- a/logo/logo.py +++ b/logo/logo.py @@ -170,7 +170,7 @@ class Thumbnail(GraphScene): # adding manim picture = Group(*self.mobjects) picture.scale(0.6).to_edge(LEFT, buff=SMALL_BUFF) - manim = TexText("Manim").set_height(1.5) \ + manim = MTexText("Manim").set_height(1.5) \ .next_to(picture, RIGHT) \ .shift(DOWN * 0.7) self.add(manim) diff --git a/manimlib/mobject/probability.py b/manimlib/mobject/probability.py index b519e26c..26ee9200 100644 --- a/manimlib/mobject/probability.py +++ b/manimlib/mobject/probability.py @@ -10,7 +10,7 @@ from manimlib.mobject.geometry import Rectangle from manimlib.mobject.mobject import Mobject from manimlib.mobject.svg.brace import Brace from manimlib.mobject.svg.mtex_mobject import MTex -from manimlib.mobject.svg.tex_mobject import TexText +from manimlib.mobject.svg.mtex_mobject import MTexText from manimlib.mobject.types.vectorized_mobject import VGroup from manimlib.utils.color import color_gradient from manimlib.utils.iterables import listify @@ -52,7 +52,7 @@ class SampleSpace(Rectangle): buff: float = MED_SMALL_BUFF ) -> None: # TODO, should this really exist in SampleSpaceScene - title_mob = TexText(title) + title_mob = MTexText(title) if title_mob.get_width() > self.get_width(): title_mob.set_width(self.get_width()) title_mob.next_to(self, UP, buff=buff) From 81e6ab5b1debe22dd31ad33fde71dd44e28bf883 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 20 Dec 2022 17:02:29 -0800 Subject: [PATCH 11/36] Add MTex.dirty_select A more general way to select subparts by tex. Not as reliable as select_parts, but useful in many cases. --- manimlib/mobject/svg/mtex_mobject.py | 33 ++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/manimlib/mobject/svg/mtex_mobject.py b/manimlib/mobject/svg/mtex_mobject.py index 4cacdc93..b7adf2c4 100644 --- a/manimlib/mobject/svg/mtex_mobject.py +++ b/manimlib/mobject/svg/mtex_mobject.py @@ -209,6 +209,39 @@ class MTex(StringMobject): ): return self.set_parts_color_by_dict(color_map) + @staticmethod + def n_symbols(tex) -> int: + """ + This function attempts to estimate the number of symbols that + a given string of tex would produce. + + No guarantees this is accurate. + """ + count_to_subtrs = [ + (0, ["emph", "textbf", "big", "Big", "small", "Small"]), + (2, ["sqrt", "ne"]), + (6, ["underbrace"]), + # Replace all other \expressions (like "\pi") with a single character + # Deliberately put this last. + (1, ["[a-zA-Z]+"]) + ] + for count, substrs in count_to_subtrs: + # Replace occurances of the given substrings with `count` characters + pattern = "|".join((R"\\" + s for s in substrs )) + tex = re.sub(pattern, "X" * count, tex) + # Ignore various control characters + return len(list(filter(lambda c: c not in "^{} \n\t_", tex))) + + def dirty_select(self, substr: str) -> VGroup: + tex = self.get_tex() + result = [] + for match in re.finditer(substr.replace("\\", R"\\"), tex): + index = match.start() + start = self.n_symbols(tex[:index]) + end = start + self.n_symbols(substr) + result.append(self[start:end]) + return VGroup(*result) + def get_tex(self) -> str: return self.get_string() From ef941b4040d91866f9d847ed097ae01e06ed5cd9 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 20 Dec 2022 22:35:41 -0800 Subject: [PATCH 12/36] Factor out num_tex_symbols --- manimlib/__init__.py | 1 + manimlib/mobject/svg/mtex_mobject.py | 44 ++++++++++++---------------- manimlib/utils/tex.py | 27 +++++++++++++++++ 3 files changed, 47 insertions(+), 25 deletions(-) create mode 100644 manimlib/utils/tex.py diff --git a/manimlib/__init__.py b/manimlib/__init__.py index 7704b352..520d4825 100644 --- a/manimlib/__init__.py +++ b/manimlib/__init__.py @@ -75,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 * diff --git a/manimlib/mobject/svg/mtex_mobject.py b/manimlib/mobject/svg/mtex_mobject.py index b7adf2c4..5698dcfa 100644 --- a/manimlib/mobject/svg/mtex_mobject.py +++ b/manimlib/mobject/svg/mtex_mobject.py @@ -3,14 +3,17 @@ from __future__ import annotations import re 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 manimlib.mobject.types.vectorized_mobject import VGroup from manimlib.typing import ManimColor, Span, Selector @@ -209,36 +212,27 @@ class MTex(StringMobject): ): return self.set_parts_color_by_dict(color_map) - @staticmethod - def n_symbols(tex) -> int: - """ - This function attempts to estimate the number of symbols that - a given string of tex would produce. - - No guarantees this is accurate. - """ - count_to_subtrs = [ - (0, ["emph", "textbf", "big", "Big", "small", "Small"]), - (2, ["sqrt", "ne"]), - (6, ["underbrace"]), - # Replace all other \expressions (like "\pi") with a single character - # Deliberately put this last. - (1, ["[a-zA-Z]+"]) - ] - for count, substrs in count_to_subtrs: - # Replace occurances of the given substrings with `count` characters - pattern = "|".join((R"\\" + s for s in substrs )) - tex = re.sub(pattern, "X" * count, tex) - # Ignore various control characters - return len(list(filter(lambda c: c not in "^{} \n\t_", tex))) def dirty_select(self, substr: str) -> VGroup: + """ + Tries to pull out substrings based on guessing how + many symbols are associated with a given tex string. + + This can fail in cases where the order of symbols does + not match the order in which they're drawn by latex. + For example, `\\underbrace{text}' orders the brace + after the text. + """ tex = self.get_tex() result = [] + if len(self) != num_tex_symbols(tex): + log.warning( + f"Estimated size of {tex} does not match true size", + ) for match in re.finditer(substr.replace("\\", R"\\"), tex): index = match.start() - start = self.n_symbols(tex[:index]) - end = start + self.n_symbols(substr) + start = num_tex_symbols(tex[:index]) + end = start + num_tex_symbols(substr) result.append(self[start:end]) return VGroup(*result) diff --git a/manimlib/utils/tex.py b/manimlib/utils/tex.py new file mode 100644 index 00000000..2d454731 --- /dev/null +++ b/manimlib/utils/tex.py @@ -0,0 +1,27 @@ +import re + +def num_tex_symbols(tex: str) -> int: + """ + This function attempts to estimate the number of symbols that + a given string of tex would produce. + + No guarantees this is accurate. + """ + count_to_subtrs = [ + (0, [ + "emph", "textbf", "big", "Big", "small", "Small", + "quad", "qquad", ",", ";", "ghost", + *"^{} \n\t_", + ]), + (2, ["sqrt", "ne"]), + (6, ["underbrace"]), + # Replace all other \expressions (like "\pi") with a single character + # Deliberately put this last. + (1, ["[a-zA-Z]+"]) + ] + for count, substrs in count_to_subtrs: + # Replace occurances of the given substrings with `count` characters + pattern = "|".join((R"\\" + s for s in substrs )) + tex = re.sub(pattern, "X" * count, tex) + # Ignore various control characters + return sum(map(lambda c: c not in "^{} \n\t_", tex)) From 4b731404354dae0b6f2bb0c4030e3fc5c86b0499 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 20 Dec 2022 22:37:01 -0800 Subject: [PATCH 13/36] Have MTex.select_parts fall back on dirty_select with a warning --- manimlib/mobject/svg/mtex_mobject.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/manimlib/mobject/svg/mtex_mobject.py b/manimlib/mobject/svg/mtex_mobject.py index 5698dcfa..a94e6bce 100644 --- a/manimlib/mobject/svg/mtex_mobject.py +++ b/manimlib/mobject/svg/mtex_mobject.py @@ -201,8 +201,8 @@ class MTex(StringMobject): 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 get_part_by_tex(self, selector: Selector, index: int = 0) -> VGroup: + return self.select_part(selector, index) def set_color_by_tex(self, selector: Selector, color: ManimColor): return self.set_parts_color(selector, color) @@ -212,6 +212,12 @@ class MTex(StringMobject): ): return self.set_parts_color_by_dict(color_map) + def select_parts(self, selector: Selector) -> VGroup: + result = super().select_parts(selector) + if len(result) == 0 and isinstance(selector, str): + log.warning(f"Accessing unisolated substring: {selector}") + return self.dirty_select(selector) + return result def dirty_select(self, substr: str) -> VGroup: """ From 6277e283731fdabff8001885943d0cfabe734d1a Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 20 Dec 2022 22:37:23 -0800 Subject: [PATCH 14/36] Have MTex.__getitem__ call MTex.select_parts --- manimlib/mobject/svg/mtex_mobject.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/manimlib/mobject/svg/mtex_mobject.py b/manimlib/mobject/svg/mtex_mobject.py index a94e6bce..90182c80 100644 --- a/manimlib/mobject/svg/mtex_mobject.py +++ b/manimlib/mobject/svg/mtex_mobject.py @@ -242,6 +242,12 @@ class MTex(StringMobject): result.append(self[start:end]) return VGroup(*result) + def __getitem__(self, value: int | slice | str | re.Pattern) -> VMobject: + # TODO, maybe move this functionality up to StringMobject + if isinstance(value, (str, re.Pattern)): + return self.select_parts(value) + return super().__getitem__(value) + def get_tex(self) -> str: return self.get_string() From 10d53c82e1bf354ef042570d7fb340be304c7739 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 21 Dec 2022 12:47:18 -0800 Subject: [PATCH 15/36] Improved num_tex_symbols Based on data gathered for tex commands --- manimlib/utils/tex.py | 71 +++++++--- manimlib/utils/tex_to_symbol_count.py | 182 ++++++++++++++++++++++++++ 2 files changed, 233 insertions(+), 20 deletions(-) create mode 100644 manimlib/utils/tex_to_symbol_count.py diff --git a/manimlib/utils/tex.py b/manimlib/utils/tex.py index 2d454731..cbabc2ec 100644 --- a/manimlib/utils/tex.py +++ b/manimlib/utils/tex.py @@ -1,27 +1,58 @@ +from __future__ import annotations + +import os 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. - - No guarantees this is accurate. """ - count_to_subtrs = [ - (0, [ - "emph", "textbf", "big", "Big", "small", "Small", - "quad", "qquad", ",", ";", "ghost", - *"^{} \n\t_", - ]), - (2, ["sqrt", "ne"]), - (6, ["underbrace"]), - # Replace all other \expressions (like "\pi") with a single character - # Deliberately put this last. - (1, ["[a-zA-Z]+"]) - ] - for count, substrs in count_to_subtrs: - # Replace occurances of the given substrings with `count` characters - pattern = "|".join((R"\\" + s for s in substrs )) - tex = re.sub(pattern, "X" * count, tex) - # Ignore various control characters - return sum(map(lambda c: c not in "^{} \n\t_", tex)) + 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 diff --git a/manimlib/utils/tex_to_symbol_count.py b/manimlib/utils/tex_to_symbol_count.py new file mode 100644 index 00000000..e858b572 --- /dev/null +++ b/manimlib/utils/tex_to_symbol_count.py @@ -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 +} \ No newline at end of file From 958c34c70580438ff1fd56478dc69718926a9154 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 21 Dec 2022 12:47:48 -0800 Subject: [PATCH 16/36] Push functionality for selecting unisolated substrings up into StringMobject --- manimlib/mobject/svg/mtex_mobject.py | 34 +++----------------------- manimlib/mobject/svg/string_mobject.py | 31 ++++++++++++++++++++--- 2 files changed, 30 insertions(+), 35 deletions(-) diff --git a/manimlib/mobject/svg/mtex_mobject.py b/manimlib/mobject/svg/mtex_mobject.py index 90182c80..1860fa40 100644 --- a/manimlib/mobject/svg/mtex_mobject.py +++ b/manimlib/mobject/svg/mtex_mobject.py @@ -201,7 +201,7 @@ class MTex(StringMobject): def get_parts_by_tex(self, selector: Selector) -> VGroup: return self.select_parts(selector) - def get_part_by_tex(self, selector: Selector, index: int = 0) -> VGroup: + def get_part_by_tex(self, selector: Selector, index: int = 0) -> VMobject: return self.select_part(selector, index) def set_color_by_tex(self, selector: Selector, color: ManimColor): @@ -212,41 +212,13 @@ class MTex(StringMobject): ): return self.set_parts_color_by_dict(color_map) - def select_parts(self, selector: Selector) -> VGroup: - result = super().select_parts(selector) - if len(result) == 0 and isinstance(selector, str): - log.warning(f"Accessing unisolated substring: {selector}") - return self.dirty_select(selector) - return result - - def dirty_select(self, substr: str) -> VGroup: - """ - Tries to pull out substrings based on guessing how - many symbols are associated with a given tex string. - - This can fail in cases where the order of symbols does - not match the order in which they're drawn by latex. - For example, `\\underbrace{text}' orders the brace - after the text. - """ + def substr_to_path_count(self, substr: str) -> int: tex = self.get_tex() - result = [] if len(self) != num_tex_symbols(tex): log.warning( f"Estimated size of {tex} does not match true size", ) - for match in re.finditer(substr.replace("\\", R"\\"), tex): - index = match.start() - start = num_tex_symbols(tex[:index]) - end = start + num_tex_symbols(substr) - result.append(self[start:end]) - return VGroup(*result) - - def __getitem__(self, value: int | slice | str | re.Pattern) -> VMobject: - # TODO, maybe move this functionality up to StringMobject - if isinstance(value, (str, re.Pattern)): - return self.select_parts(value) - return super().__getitem__(value) + return num_tex_symbols(substr) def get_tex(self) -> str: return self.get_string() diff --git a/manimlib/mobject/svg/string_mobject.py b/manimlib/mobject/svg/string_mobject.py index adc160a6..9aa71cf0 100644 --- a/manimlib/mobject/svg/string_mobject.py +++ b/manimlib/mobject/svg/string_mobject.py @@ -538,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 From 805236337e90cbacc5bec0aa7f94c72981fd646e Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 21 Dec 2022 12:52:32 -0800 Subject: [PATCH 17/36] Remove stray import --- manimlib/utils/tex.py | 1 - 1 file changed, 1 deletion(-) diff --git a/manimlib/utils/tex.py b/manimlib/utils/tex.py index cbabc2ec..b8d45b42 100644 --- a/manimlib/utils/tex.py +++ b/manimlib/utils/tex.py @@ -1,6 +1,5 @@ from __future__ import annotations -import os import re from functools import lru_cache From 33682b7199756b2600cd8148678e6669279411e5 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 21 Dec 2022 13:18:20 -0800 Subject: [PATCH 18/36] MTex is the new Tex, Tex is now OldTex Global replace Tex -> OldTex TexText -> OldTexText MTex -> Tex MTexText -> TexText --- .../source/getting_started/example_scenes.rst | 16 +- example_scenes.py | 16 +- logo/logo.py | 10 +- manimlib/__init__.py | 2 +- .../animation/transform_matching_parts.py | 11 +- manimlib/mobject/coordinate_systems.py | 16 +- manimlib/mobject/matrix.py | 20 +- manimlib/mobject/numbers.py | 4 +- manimlib/mobject/probability.py | 12 +- manimlib/mobject/svg/brace.py | 11 +- manimlib/mobject/svg/drawings.py | 6 +- manimlib/mobject/svg/mtex_mobject.py | 228 ---------- manimlib/mobject/svg/old_tex_mobject.py | 339 ++++++++++++++ manimlib/mobject/svg/special_tex.py | 8 +- manimlib/mobject/svg/string_mobject.py | 2 +- manimlib/mobject/svg/tex_mobject.py | 423 +++++++----------- manimlib/scene/interactive_scene.py | 6 +- 17 files changed, 564 insertions(+), 566 deletions(-) delete mode 100644 manimlib/mobject/svg/mtex_mobject.py create mode 100644 manimlib/mobject/svg/old_tex_mobject.py diff --git a/docs/source/getting_started/example_scenes.rst b/docs/source/getting_started/example_scenes.rst index 9d09e0c2..1caeb889 100644 --- a/docs/source/getting_started/example_scenes.rst +++ b/docs/source/getting_started/example_scenes.rst @@ -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]) @@ -700,7 +700,7 @@ OpeningManimExample moving_c_grid.prepare_for_nonlinear_transform() c_grid.set_stroke(BLUE_E, 1) c_grid.add_coordinate_labels(font_size=24) - complex_map_words = MTexText(""" + complex_map_words = TexText(""" Or thinking of the plane as $\\mathds{C}$,\\\\ this is the map $z \\rightarrow z^2$ """) diff --git a/example_scenes.py b/example_scenes.py index cce59dbb..a1ca9c69 100644 --- a/example_scenes.py +++ b/example_scenes.py @@ -47,7 +47,7 @@ class OpeningManimExample(Scene): moving_c_grid.prepare_for_nonlinear_transform() c_grid.set_stroke(BLUE_E, 1) c_grid.add_coordinate_labels(font_size=24) - complex_map_words = MTexText(""" + complex_map_words = TexText(""" Or thinking of the plane as $\\mathds{C}$,\\\\ this is the map $z \\rightarrow z^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]) diff --git a/logo/logo.py b/logo/logo.py index a79791ce..f44da45d 100644 --- a/logo/logo.py +++ b/logo/logo.py @@ -60,10 +60,10 @@ class Thumbnail(GraphScene): triangle.scale(0.1) # - x_label_p1 = MTex("a") - output_label_p1 = MTex("f(a)") - x_label_p2 = MTex("b") - output_label_p2 = MTex("f(b)") + x_label_p1 = Tex("a") + output_label_p1 = Tex("f(a)") + x_label_p2 = Tex("b") + output_label_p2 = Tex("f(b)") v_line_p1 = get_v_line(input_tracker_p1) v_line_p2 = get_v_line(input_tracker_p2) h_line_p1 = get_h_line(input_tracker_p1) @@ -170,7 +170,7 @@ class Thumbnail(GraphScene): # adding manim picture = Group(*self.mobjects) picture.scale(0.6).to_edge(LEFT, buff=SMALL_BUFF) - manim = MTexText("Manim").set_height(1.5) \ + manim = TexText("Manim").set_height(1.5) \ .next_to(picture, RIGHT) \ .shift(DOWN * 0.7) self.add(manim) diff --git a/manimlib/__init__.py b/manimlib/__init__.py index 520d4825..063cfa62 100644 --- a/manimlib/__init__.py +++ b/manimlib/__init__.py @@ -43,7 +43,7 @@ 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 * diff --git a/manimlib/animation/transform_matching_parts.py b/manimlib/animation/transform_matching_parts.py index 16b0534f..d82c8874 100644 --- a/manimlib/animation/transform_matching_parts.py +++ b/manimlib/animation/transform_matching_parts.py @@ -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() diff --git a/manimlib/mobject/coordinate_systems.py b/manimlib/mobject/coordinate_systems.py index 49e5754d..b670d8ee 100644 --- a/manimlib/mobject/coordinate_systems.py +++ b/manimlib/mobject/coordinate_systems.py @@ -18,7 +18,7 @@ from manimlib.mobject.geometry import DashedLine from manimlib.mobject.geometry import Line from manimlib.mobject.geometry import Rectangle from manimlib.mobject.number_line import NumberLine -from manimlib.mobject.svg.mtex_mobject import MTex +from manimlib.mobject.svg.tex_mobject import Tex from manimlib.mobject.types.dot_cloud import DotCloud from manimlib.mobject.types.surface import ParametricSurface from manimlib.mobject.types.vectorized_mobject import VGroup @@ -105,7 +105,7 @@ class CoordinateSystem(ABC): edge: Vect3 = RIGHT, direction: Vect3 = DL, **kwargs - ) -> MTex: + ) -> Tex: return self.get_axis_label( label_tex, self.get_x_axis(), edge, direction, **kwargs @@ -117,7 +117,7 @@ class CoordinateSystem(ABC): edge: Vect3 = UP, direction: Vect3 = DR, **kwargs - ) -> MTex: + ) -> Tex: return self.get_axis_label( label_tex, self.get_y_axis(), edge, direction, **kwargs @@ -130,8 +130,8 @@ class CoordinateSystem(ABC): edge: Vect3, direction: Vect3, buff: float = MED_SMALL_BUFF - ) -> MTex: - label = MTex(label_tex) + ) -> Tex: + label = Tex(label_tex) label.next_to( axis.get_edge_center(edge), direction, buff=buff @@ -268,9 +268,9 @@ class CoordinateSystem(ABC): direction: Vect3 = RIGHT, buff: float = MED_SMALL_BUFF, color: ManimColor | None = None - ) -> MTex | Mobject: + ) -> Tex | Mobject: if isinstance(label, str): - label = MTex(label) + label = Tex(label) if color is None: label.match_color(graph) if x is None: @@ -537,7 +537,7 @@ class ThreeDAxes(Axes): def add_axis_labels(self, x_tex="x", y_tex="y", z_tex="z", font_size=24, buff=0.2): x_label, y_label, z_label = labels = VGroup(*( - MTex(tex, font_size=font_size) + Tex(tex, font_size=font_size) for tex in [x_tex, y_tex, z_tex] )) z_label.rotate(PI / 2, RIGHT) diff --git a/manimlib/mobject/matrix.py b/manimlib/mobject/matrix.py index c827cf85..1cb132fd 100644 --- a/manimlib/mobject/matrix.py +++ b/manimlib/mobject/matrix.py @@ -10,8 +10,8 @@ from manimlib.constants import WHITE from manimlib.mobject.numbers import DecimalNumber from manimlib.mobject.numbers import Integer from manimlib.mobject.shape_matchers import BackgroundRectangle -from manimlib.mobject.svg.mtex_mobject import MTex -from manimlib.mobject.svg.mtex_mobject import MTexText +from manimlib.mobject.svg.tex_mobject import Tex +from manimlib.mobject.svg.tex_mobject import TexText from manimlib.mobject.types.vectorized_mobject import VGroup from manimlib.mobject.types.vectorized_mobject import VMobject @@ -41,8 +41,8 @@ def matrix_to_tex_string(matrix: npt.ArrayLike) -> str: return prefix + R" \\ ".join(rows) + suffix -def matrix_to_mobject(matrix: npt.ArrayLike) -> MTex: - return MTex(matrix_to_tex_string(matrix)) +def matrix_to_mobject(matrix: npt.ArrayLike) -> Tex: + return Tex(matrix_to_tex_string(matrix)) def vector_coordinate_label( @@ -109,7 +109,7 @@ class Matrix(VMobject): def element_to_mobject(self, element: str | float | VMobject, **config) -> VMobject: if isinstance(element, VMobject): return element - return MTex(str(element), **config) + return Tex(str(element), **config) def matrix_to_mob_matrix( self, @@ -142,7 +142,7 @@ class Matrix(VMobject): def add_brackets(self, v_buff: float, h_buff: float): height = len(self.mob_matrix) - brackets = MTex("".join(( + brackets = Tex("".join(( R"\left[\begin{array}{c}", *height * [R"\quad \\"], R"\end{array}\right]", @@ -219,22 +219,22 @@ def get_det_text( background_rect: bool = False, initial_scale_factor: int = 2 ) -> VGroup: - parens = MTex("()") + parens = Tex("()") parens.scale(initial_scale_factor) parens.stretch_to_fit_height(matrix.get_height()) l_paren, r_paren = parens.split() l_paren.next_to(matrix, LEFT, buff=0.1) r_paren.next_to(matrix, RIGHT, buff=0.1) - det = MTexText("det") + det = TexText("det") det.scale(initial_scale_factor) det.next_to(l_paren, LEFT, buff=0.1) if background_rect: det.add_background_rectangle() det_text = VGroup(det, l_paren, r_paren) if determinant is not None: - eq = MTex("=") + eq = Tex("=") eq.next_to(r_paren, RIGHT, buff=0.1) - result = MTex(str(determinant)) + result = Tex(str(determinant)) result.next_to(eq, RIGHT, buff=0.2) det_text.add(eq, result) return det_text diff --git a/manimlib/mobject/numbers.py b/manimlib/mobject/numbers.py index 4c7f365c..7effabea 100644 --- a/manimlib/mobject/numbers.py +++ b/manimlib/mobject/numbers.py @@ -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( diff --git a/manimlib/mobject/probability.py b/manimlib/mobject/probability.py index 26ee9200..44a3fbf5 100644 --- a/manimlib/mobject/probability.py +++ b/manimlib/mobject/probability.py @@ -9,8 +9,8 @@ from manimlib.mobject.geometry import Line from manimlib.mobject.geometry import Rectangle from manimlib.mobject.mobject import Mobject from manimlib.mobject.svg.brace import Brace -from manimlib.mobject.svg.mtex_mobject import MTex -from manimlib.mobject.svg.mtex_mobject import MTexText +from manimlib.mobject.svg.tex_mobject import Tex +from manimlib.mobject.svg.tex_mobject import TexText from manimlib.mobject.types.vectorized_mobject import VGroup from manimlib.utils.color import color_gradient from manimlib.utils.iterables import listify @@ -52,7 +52,7 @@ class SampleSpace(Rectangle): buff: float = MED_SMALL_BUFF ) -> None: # TODO, should this really exist in SampleSpaceScene - title_mob = MTexText(title) + title_mob = TexText(title) if title_mob.get_width() > self.get_width(): title_mob.set_width(self.get_width()) title_mob.next_to(self, UP, buff=buff) @@ -132,7 +132,7 @@ class SampleSpace(Rectangle): if isinstance(label, Mobject): label_mob = label else: - label_mob = MTex(label) + label_mob = Tex(label) label_mob.scale(self.default_label_scale_val) label_mob.next_to(brace, direction, buff) @@ -266,7 +266,7 @@ class BarChart(VGroup): if self.label_y_axis: labels = VGroup() for y_tick, value in zip(y_ticks, values): - label = MTex(str(np.round(value, 2))) + label = Tex(str(np.round(value, 2))) label.set_height(self.y_axis_label_height) label.next_to(y_tick, LEFT, SMALL_BUFF) labels.add(label) @@ -289,7 +289,7 @@ class BarChart(VGroup): bar_labels = VGroup() for bar, name in zip(bars, self.bar_names): - label = MTex(str(name)) + label = Tex(str(name)) label.scale(self.bar_label_scale_val) label.next_to(bar, DOWN, SMALL_BUFF) bar_labels.add(label) diff --git a/manimlib/mobject/svg/brace.py b/manimlib/mobject/svg/brace.py index 1ad27377..65cd3740 100644 --- a/manimlib/mobject/svg/brace.py +++ b/manimlib/mobject/svg/brace.py @@ -11,8 +11,7 @@ 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.mtex_mobject import MTex +from manimlib.mobject.svg.tex_mobject import Tex from manimlib.mobject.svg.tex_mobject import TexText from manimlib.mobject.svg.text_mobject import Text from manimlib.mobject.types.vectorized_mobject import VGroup @@ -30,7 +29,7 @@ if TYPE_CHECKING: from manimlib.typing import Vect3 -class Brace(SingleStringTex): +class Brace(Tex): def __init__( self, mobject: Mobject, @@ -92,8 +91,8 @@ class Brace(SingleStringTex): self.put_at_tip(text_mob, buff=buff) return text_mob - def get_tex(self, *tex: str, **kwargs) -> MTex: - tex_mob = MTex(*tex) + def get_tex(self, *tex: str, **kwargs) -> Tex: + tex_mob = Tex(*tex) self.put_at_tip(tex_mob, **kwargs) return tex_mob @@ -109,7 +108,7 @@ class Brace(SingleStringTex): class BraceLabel(VMobject): - label_constructor: type = MTex + label_constructor: type = Tex def __init__( self, diff --git a/manimlib/mobject/svg/drawings.py b/manimlib/mobject/svg/drawings.py index ed7920a6..5a413ee5 100644 --- a/manimlib/mobject/svg/drawings.py +++ b/manimlib/mobject/svg/drawings.py @@ -44,8 +44,8 @@ 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.mtex_mobject import MTex -from manimlib.mobject.svg.mtex_mobject import MTexText +from manimlib.mobject.svg.tex_mobject import Tex +from manimlib.mobject.svg.tex_mobject import TexText from manimlib.mobject.svg.special_tex import TexTextFromPresetString from manimlib.mobject.three_dimensions import Prismify from manimlib.mobject.three_dimensions import VCube @@ -427,7 +427,7 @@ class Bubble(SVGMobject): return self.content def write(self, *text): - self.add_content(MTexText(*text)) + self.add_content(TexText(*text)) return self def resize_to_content(self, buff=0.75): diff --git a/manimlib/mobject/svg/mtex_mobject.py b/manimlib/mobject/svg/mtex_mobject.py deleted file mode 100644 index 1860fa40..00000000 --- a/manimlib/mobject/svg/mtex_mobject.py +++ /dev/null @@ -1,228 +0,0 @@ -from __future__ import annotations - -import re - -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 manimlib.typing import ManimColor, Span, Selector - - -SCALE_FACTOR_PER_FONT_POINT = 0.001 - - -class MTex(StringMobject): - tex_environment: str = "align*" - - def __init__( - self, - *tex_strings: str, - font_size: int = 48, - alignment: str = R"\centering", - 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.alignment = alignment - self.template = template - self.additional_preamble = additional_preamble - self.tex_to_color_map = dict(**t2c, **tex_to_color_map) - - super().__init__( - tex_string, - use_labelled_svg=use_labelled_svg, - isolate=isolate, - **kwargs - ) - - 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: - 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: - return tex_content_to_svg_file( - content, self.template, self.additional_preamble, self.tex_string - ) - - # Parsing - - @staticmethod - def get_command_matches(string: str) -> list[re.Match]: - # Lump together adjacent brace pairs - pattern = re.compile(r""" - (?P\\(?:[a-zA-Z]+|.)) - |(?P{+) - |(?P}+) - """, 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, index: int = 0) -> VMobject: - return self.select_part(selector, index) - - 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 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 get_tex(self) -> str: - return self.get_string() - - -class MTexText(MTex): - tex_environment: str = "" diff --git a/manimlib/mobject/svg/old_tex_mobject.py b/manimlib/mobject/svg/old_tex_mobject.py new file mode 100644 index 00000000..d0b46f88 --- /dev/null +++ b/manimlib/mobject/svg/old_tex_mobject.py @@ -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 + ) diff --git a/manimlib/mobject/svg/special_tex.py b/manimlib/mobject/svg/special_tex.py index 9929b589..578c00e2 100644 --- a/manimlib/mobject/svg/special_tex.py +++ b/manimlib/mobject/svg/special_tex.py @@ -6,7 +6,7 @@ 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.mtex_mobject import MTexText +from manimlib.mobject.svg.tex_mobject import TexText from typing import TYPE_CHECKING @@ -30,7 +30,7 @@ class BulletedList(VGroup): *labelled_content, R"\end{itemize}" ]) - tex_text = MTexText(tex_string, isolate=labelled_content, **kwargs) + tex_text = TexText(tex_string, isolate=labelled_content, **kwargs) lines = (tex_text.select_part(part) for part in labelled_content) super().__init__(*lines) @@ -42,7 +42,7 @@ class BulletedList(VGroup): part.set_fill(opacity=(1.0 if i == index else opacity)) -class TexTextFromPresetString(MTexText): +class TexTextFromPresetString(TexText): tex: str = "" default_color: ManimColor = WHITE @@ -54,7 +54,7 @@ class TexTextFromPresetString(MTexText): ) -class Title(MTexText): +class Title(TexText): def __init__( self, *text_parts: str, diff --git a/manimlib/mobject/svg/string_mobject.py b/manimlib/mobject/svg/string_mobject.py index 9aa71cf0..71f2b6b9 100644 --- a/manimlib/mobject/svg/string_mobject.py +++ b/manimlib/mobject/svg/string_mobject.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: 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 diff --git a/manimlib/mobject/svg/tex_mobject.py b/manimlib/mobject/svg/tex_mobject.py index cfcd1c7b..77a438b4 100644 --- a/manimlib/mobject/svg/tex_mobject.py +++ b/manimlib/mobject/svg/tex_mobject.py @@ -1,70 +1,68 @@ 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.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: @@ -72,268 +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\\(?:[a-zA-Z]+|.)) + |(?P{+) + |(?P}+) + """, 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 - ) + tex_environment: str = "" diff --git a/manimlib/scene/interactive_scene.py b/manimlib/scene/interactive_scene.py index f9a29c1e..b3eb0fb2 100644 --- a/manimlib/scene/interactive_scene.py +++ b/manimlib/scene/interactive_scene.py @@ -17,7 +17,7 @@ from manimlib.mobject.geometry import Square from manimlib.mobject.mobject import Group from manimlib.mobject.mobject import Mobject from manimlib.mobject.numbers import DecimalNumber -from manimlib.mobject.svg.mtex_mobject import MTex +from manimlib.mobject.svg.tex_mobject import Tex from manimlib.mobject.svg.text_mobject import Text from manimlib.mobject.types.dot_cloud import DotCloud from manimlib.mobject.types.vectorized_mobject import VGroup @@ -61,7 +61,7 @@ class InteractiveScene(Scene): Command + 'c' copies the ids of selections to clipboard Command + 'v' will paste either: - The copied mobject - - A MTex mobject based on copied LaTeX + - A Tex mobject based on copied LaTeX - A Text mobject based on copied Text Command + 'z' restores selection back to its original state Command + 's' saves the selected mobjects to file @@ -358,7 +358,7 @@ class InteractiveScene(Scene): # Otherwise, treat as tex or text if set("\\^=+").intersection(clipboard_str): # Proxy to text for LaTeX try: - new_mob = MTex(clipboard_str) + new_mob = Tex(clipboard_str) except LatexError: return else: From ff090c016f320bafba498bff996bdd980057e980 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 22 Dec 2022 11:46:51 -0700 Subject: [PATCH 19/36] Use only anchors to compute area vector --- manimlib/mobject/types/vectorized_mobject.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/manimlib/mobject/types/vectorized_mobject.py b/manimlib/mobject/types/vectorized_mobject.py index 3221beaf..c3ea4709 100644 --- a/manimlib/mobject/types/vectorized_mobject.py +++ b/manimlib/mobject/types/vectorized_mobject.py @@ -758,9 +758,10 @@ 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] # Each term goes through all edges [(x1, y1, z1), (x2, y2, z2)] return 0.5 * np.array([ @@ -1023,10 +1024,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 From 5af4b9cc653f41a36da80ba8fb1af70ad05d6a49 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 22 Dec 2022 11:52:30 -0700 Subject: [PATCH 20/36] No need to refresh triangulation in flip --- manimlib/mobject/types/vectorized_mobject.py | 1 - 1 file changed, 1 deletion(-) diff --git a/manimlib/mobject/types/vectorized_mobject.py b/manimlib/mobject/types/vectorized_mobject.py index c3ea4709..5fc5c416 100644 --- a/manimlib/mobject/types/vectorized_mobject.py +++ b/manimlib/mobject/types/vectorized_mobject.py @@ -1055,7 +1055,6 @@ class VMobject(Mobject): def flip(self, axis: Vect3 = UP, **kwargs): super().flip(axis, **kwargs) self.refresh_unit_normal() - self.refresh_triangulation() return self # For shaders From a4d47f64b00642ed9dd43454a664911c20d37c1e Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 22 Dec 2022 11:52:55 -0700 Subject: [PATCH 21/36] Fix normalize_along_axis --- manimlib/utils/space_ops.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/manimlib/utils/space_ops.py b/manimlib/utils/space_ops.py index 1c0a9572..75ce94fb 100644 --- a/manimlib/utils/space_ops.py +++ b/manimlib/utils/space_ops.py @@ -178,8 +178,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( From fca7c0609a24553c00b3bdf65a6f059879bb893a Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 22 Dec 2022 11:53:14 -0700 Subject: [PATCH 22/36] Factor out epsilon in earclip_triangulation --- manimlib/utils/space_ops.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/manimlib/utils/space_ops.py b/manimlib/utils/space_ops.py index 75ce94fb..509d8545 100644 --- a/manimlib/utils/space_ops.py +++ b/manimlib/utils/space_ops.py @@ -383,9 +383,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] @@ -396,8 +397,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 From b7d473ff43708793cae6fce93d4dc7742f047f3c Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 22 Dec 2022 12:03:33 -0700 Subject: [PATCH 23/36] Revert "Revert back from the needs_new_unit_normal change in favor of recomputation" This reverts commit d8deec8f813166b9bf42960acb0f6da4349712e7. --- manimlib/mobject/types/vectorized_mobject.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/manimlib/mobject/types/vectorized_mobject.py b/manimlib/mobject/types/vectorized_mobject.py index 5fc5c416..0a991930 100644 --- a/manimlib/mobject/types/vectorized_mobject.py +++ b/manimlib/mobject/types/vectorized_mobject.py @@ -100,6 +100,7 @@ class VMobject(Mobject): self.flat_stroke = flat_stroke self.needs_new_triangulation = True + self.needs_new_unit_normal = True self.triangulation = np.zeros(0, dtype='i4') super().__init__(**kwargs) @@ -771,7 +772,7 @@ class VMobject(Mobject): ]) def get_unit_normal(self, recompute: bool = False) -> Vect3: - if not recompute: + if not self.needs_new_unit_normal and not recompute: return self.data["unit_normal"][0] if self.get_num_points() < 3: @@ -788,11 +789,12 @@ class VMobject(Mobject): points[2] - points[1], ) self.data["unit_normal"][:] = normal + self.needs_new_unit_normal = False return normal def refresh_unit_normal(self): for mob in self.get_family(): - mob.get_unit_normal(recompute=True) + mob.needs_new_unit_normal = True return self # Alignment From 8c5d4db411d8200941ed1af19db3f6975468cd54 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 22 Dec 2022 12:09:54 -0700 Subject: [PATCH 24/36] Ensure VMobject.get_area_vector works with uneven number of points --- manimlib/mobject/types/vectorized_mobject.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/manimlib/mobject/types/vectorized_mobject.py b/manimlib/mobject/types/vectorized_mobject.py index 0a991930..e7936e7b 100644 --- a/manimlib/mobject/types/vectorized_mobject.py +++ b/manimlib/mobject/types/vectorized_mobject.py @@ -764,6 +764,11 @@ class VMobject(Mobject): 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([ sum((p0[:, 1] + p1[:, 1]) * (p1[:, 2] - p0[:, 2])), # Add up (y1 + y2)*(z2 - z1) From 96d391d9fda4d692ba51cebc4ba4e802ec2d2ea0 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 22 Dec 2022 17:51:22 -0700 Subject: [PATCH 25/36] Simplify get_unit_normal to always recompute --- manimlib/mobject/types/vectorized_mobject.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/manimlib/mobject/types/vectorized_mobject.py b/manimlib/mobject/types/vectorized_mobject.py index e7936e7b..3c3e1dd4 100644 --- a/manimlib/mobject/types/vectorized_mobject.py +++ b/manimlib/mobject/types/vectorized_mobject.py @@ -100,7 +100,6 @@ class VMobject(Mobject): self.flat_stroke = flat_stroke self.needs_new_triangulation = True - self.needs_new_unit_normal = True self.triangulation = np.zeros(0, dtype='i4') super().__init__(**kwargs) @@ -776,10 +775,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 self.needs_new_unit_normal and not recompute: - return self.data["unit_normal"][0] - + def get_unit_normal(self) -> Vect3: if self.get_num_points() < 3: return OUT @@ -794,12 +790,11 @@ class VMobject(Mobject): points[2] - points[1], ) self.data["unit_normal"][:] = normal - self.needs_new_unit_normal = False return normal def refresh_unit_normal(self): for mob in self.get_family(): - mob.needs_new_unit_normal = True + mob.get_unit_normal() return self # Alignment @@ -963,7 +958,7 @@ class VMobject(Mobject): # 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) + normal_vector = self.get_unit_normal() if not self.needs_new_triangulation: return self.triangulation @@ -977,6 +972,7 @@ class VMobject(Mobject): if not np.isclose(normal_vector, OUT).all(): # Rotate points such that unit normal vector is OUT + # Note, transpose of z_to_vector is its inverse points = np.dot(points, z_to_vector(normal_vector)) indices = np.arange(len(points), dtype=int) From c36d1788565e6c7a95462fd97003b323e3b02975 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 22 Dec 2022 17:51:59 -0700 Subject: [PATCH 26/36] Trigger unit normal refresh for all bounding_box refreshes --- manimlib/mobject/types/vectorized_mobject.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/manimlib/mobject/types/vectorized_mobject.py b/manimlib/mobject/types/vectorized_mobject.py index 3c3e1dd4..e1fa0b60 100644 --- a/manimlib/mobject/types/vectorized_mobject.py +++ b/manimlib/mobject/types/vectorized_mobject.py @@ -1055,9 +1055,16 @@ class VMobject(Mobject): self.make_approximately_smooth() return self - def flip(self, axis: Vect3 = UP, **kwargs): - super().flip(axis, **kwargs) - self.refresh_unit_normal() + def refresh_bounding_box( + self, + recurse_down: bool = False, + recurse_up: bool = True + ): + super().refresh_bounding_box(recurse_down, recurse_up) + # Anything which calls for refreshing the bounding box + # shoudl also trigger a recomputation of the unit normal + for mob in self.get_family(recurse_down): + mob.refresh_unit_normal() return self # For shaders From baf2690d77c9144ad3c1ec93a9c4a2c69ae3b006 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 22 Dec 2022 17:57:47 -0700 Subject: [PATCH 27/36] In rotation_between_vectors, account for the case where vectors align --- manimlib/utils/space_ops.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/manimlib/utils/space_ops.py b/manimlib/utils/space_ops.py index 509d8545..a5112bda 100644 --- a/manimlib/utils/space_ops.py +++ b/manimlib/utils/space_ops.py @@ -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,8 +134,15 @@ 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) From ef04b9eb013fb376b4d7678a2b5e43094ad97861 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 22 Dec 2022 18:07:46 -0700 Subject: [PATCH 28/36] Revert "Simplify get_unit_normal to always recompute" This reverts commit 96d391d9fda4d692ba51cebc4ba4e802ec2d2ea0. --- manimlib/mobject/types/vectorized_mobject.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/manimlib/mobject/types/vectorized_mobject.py b/manimlib/mobject/types/vectorized_mobject.py index e1fa0b60..88a08393 100644 --- a/manimlib/mobject/types/vectorized_mobject.py +++ b/manimlib/mobject/types/vectorized_mobject.py @@ -100,6 +100,7 @@ class VMobject(Mobject): self.flat_stroke = flat_stroke self.needs_new_triangulation = True + self.needs_new_unit_normal = True self.triangulation = np.zeros(0, dtype='i4') super().__init__(**kwargs) @@ -775,7 +776,10 @@ class VMobject(Mobject): sum((p0[:, 0] + p1[:, 0]) * (p1[:, 1] - p0[:, 1])), # Add up (x1 + x2)*(y2 - y1) ]) - def get_unit_normal(self) -> Vect3: + def get_unit_normal(self, recompute: bool = False) -> Vect3: + if not self.needs_new_unit_normal and not recompute: + return self.data["unit_normal"][0] + if self.get_num_points() < 3: return OUT @@ -790,11 +794,12 @@ class VMobject(Mobject): points[2] - points[1], ) self.data["unit_normal"][:] = normal + self.needs_new_unit_normal = False return normal def refresh_unit_normal(self): for mob in self.get_family(): - mob.get_unit_normal() + mob.needs_new_unit_normal = True return self # Alignment @@ -958,7 +963,7 @@ class VMobject(Mobject): # 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() + normal_vector = self.get_unit_normal(recompute=True) if not self.needs_new_triangulation: return self.triangulation @@ -972,7 +977,6 @@ class VMobject(Mobject): if not np.isclose(normal_vector, OUT).all(): # Rotate points such that unit normal vector is OUT - # Note, transpose of z_to_vector is its inverse points = np.dot(points, z_to_vector(normal_vector)) indices = np.arange(len(points), dtype=int) From 3878b8c077cd5054fcea314641d917f32c2ac4dd Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Fri, 23 Dec 2022 10:04:27 -0700 Subject: [PATCH 29/36] Fix rotation_between_vectors --- manimlib/utils/space_ops.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manimlib/utils/space_ops.py b/manimlib/utils/space_ops.py index a5112bda..1bde3d1a 100644 --- a/manimlib/utils/space_ops.py +++ b/manimlib/utils/space_ops.py @@ -145,7 +145,7 @@ def rotation_between_vectors(v1: Vect3, v2: Vect3) -> Matrix3x3: axis = np.cross(v1, UP) return rotation_matrix( angle=angle_between_vectors(v1, v2), - axis=np.cross(v1, v2) + axis=axis, ) From 40bf1fd6a98c5f5e51c21bdb958936ef9b513b6e Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Fri, 23 Dec 2022 10:08:23 -0700 Subject: [PATCH 30/36] Stop tracking unit_normal in data Track orientation of each curve instead --- manimlib/mobject/types/vectorized_mobject.py | 28 ++++++------------- .../shaders/quadratic_bezier_fill/geom.glsl | 8 ++++-- .../shaders/quadratic_bezier_fill/vert.glsl | 6 ++-- .../shaders/quadratic_bezier_stroke/geom.glsl | 8 ++++-- .../shaders/quadratic_bezier_stroke/vert.glsl | 2 -- 5 files changed, 22 insertions(+), 30 deletions(-) diff --git a/manimlib/mobject/types/vectorized_mobject.py b/manimlib/mobject/types/vectorized_mobject.py index 88a08393..01553af4 100644 --- a/manimlib/mobject/types/vectorized_mobject.py +++ b/manimlib/mobject/types/vectorized_mobject.py @@ -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,)), ] @@ -100,7 +99,6 @@ class VMobject(Mobject): self.flat_stroke = flat_stroke self.needs_new_triangulation = True - self.needs_new_unit_normal = True self.triangulation = np.zeros(0, dtype='i4') super().__init__(**kwargs) @@ -115,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 @@ -776,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 self.needs_new_unit_normal and not recompute: - return self.data["unit_normal"][0] - + def get_unit_normal(self) -> Vect3: if self.get_num_points() < 3: return OUT @@ -793,13 +788,9 @@ class VMobject(Mobject): points[1] - points[0], points[2] - points[1], ) - self.data["unit_normal"][:] = normal - self.needs_new_unit_normal = False return normal - def refresh_unit_normal(self): - for mob in self.get_family(): - mob.needs_new_unit_normal = True + def refresh_unit_normal(self): # TODO, Delete return self # Alignment @@ -958,13 +949,10 @@ class VMobject(Mobject): mob.needs_new_triangulation = True 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 @@ -975,6 +963,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)) @@ -988,6 +977,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) @@ -1155,7 +1146,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 @@ -1167,7 +1157,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 diff --git a/manimlib/shaders/quadratic_bezier_fill/geom.glsl b/manimlib/shaders/quadratic_bezier_fill/geom.glsl index 7bc48d3e..81d5c19a 100644 --- a/manimlib/shaders/quadratic_bezier_fill/geom.glsl +++ b/manimlib/shaders/quadratic_bezier_fill/geom.glsl @@ -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); diff --git a/manimlib/shaders/quadratic_bezier_fill/vert.glsl b/manimlib/shaders/quadratic_bezier_fill/vert.glsl index dab9d256..2a0fc1a2 100644 --- a/manimlib/shaders/quadratic_bezier_fill/vert.glsl +++ b/manimlib/shaders/quadratic_bezier_fill/vert.glsl @@ -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; } \ No newline at end of file diff --git a/manimlib/shaders/quadratic_bezier_stroke/geom.glsl b/manimlib/shaders/quadratic_bezier_stroke/geom.glsl index 08d35891..dd28e171 100644 --- a/manimlib/shaders/quadratic_bezier_stroke/geom.glsl +++ b/manimlib/shaders/quadratic_bezier_stroke/geom.glsl @@ -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, diff --git a/manimlib/shaders/quadratic_bezier_stroke/vert.glsl b/manimlib/shaders/quadratic_bezier_stroke/vert.glsl index e8eab203..a4beb755 100644 --- a/manimlib/shaders/quadratic_bezier_stroke/vert.glsl +++ b/manimlib/shaders/quadratic_bezier_stroke/vert.glsl @@ -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; From dd2fb6ae74d4de045ce5b2ee12b74cf16856df82 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Fri, 23 Dec 2022 10:23:57 -0700 Subject: [PATCH 31/36] Delete refresh_unit_normal --- manimlib/animation/indication.py | 1 - manimlib/mobject/svg/brace.py | 1 - manimlib/mobject/svg/drawings.py | 4 +--- manimlib/mobject/three_dimensions.py | 1 - manimlib/mobject/types/vectorized_mobject.py | 16 ---------------- 5 files changed, 1 insertion(+), 22 deletions(-) diff --git a/manimlib/animation/indication.py b/manimlib/animation/indication.py index b7d2d6c0..aa0bc3f8 100644 --- a/manimlib/animation/indication.py +++ b/manimlib/animation/indication.py @@ -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 diff --git a/manimlib/mobject/svg/brace.py b/manimlib/mobject/svg/brace.py index 65cd3740..0e2aa66d 100644 --- a/manimlib/mobject/svg/brace.py +++ b/manimlib/mobject/svg/brace.py @@ -51,7 +51,6 @@ class Brace(Tex): 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() diff --git a/manimlib/mobject/svg/drawings.py b/manimlib/mobject/svg/drawings.py index 5a413ee5..370460b9 100644 --- a/manimlib/mobject/svg/drawings.py +++ b/manimlib/mobject/svg/drawings.py @@ -398,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 diff --git a/manimlib/mobject/three_dimensions.py b/manimlib/mobject/three_dimensions.py index de4a446c..49cba1e3 100644 --- a/manimlib/mobject/three_dimensions.py +++ b/manimlib/mobject/three_dimensions.py @@ -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): diff --git a/manimlib/mobject/types/vectorized_mobject.py b/manimlib/mobject/types/vectorized_mobject.py index 01553af4..8f29cf1e 100644 --- a/manimlib/mobject/types/vectorized_mobject.py +++ b/manimlib/mobject/types/vectorized_mobject.py @@ -790,9 +790,6 @@ class VMobject(Mobject): ) return normal - def refresh_unit_normal(self): # TODO, Delete - return self - # Alignment def align_points(self, vmobject: VMobject): if self.get_num_points() == len(vmobject.get_points()): @@ -1013,7 +1010,6 @@ class VMobject(Mobject): 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 @@ -1050,18 +1046,6 @@ class VMobject(Mobject): self.make_approximately_smooth() return self - def refresh_bounding_box( - self, - recurse_down: bool = False, - recurse_up: bool = True - ): - super().refresh_bounding_box(recurse_down, recurse_up) - # Anything which calls for refreshing the bounding box - # shoudl also trigger a recomputation of the unit normal - for mob in self.get_family(recurse_down): - mob.refresh_unit_normal() - return self - # For shaders def init_shader_data(self): self.fill_data = np.zeros(0, dtype=self.fill_dtype) From 7f203d16119836886708cd8e2871fbc8a874a3b2 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Fri, 23 Dec 2022 13:33:07 -0700 Subject: [PATCH 32/36] Remove triangulation cachine --- manimlib/mobject/svg/svg_mobject.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/manimlib/mobject/svg/svg_mobject.py b/manimlib/mobject/svg/svg_mobject.py index 97cccdbd..e8ac932b 100644 --- a/manimlib/mobject/svg/svg_mobject.py +++ b/manimlib/mobject/svg/svg_mobject.py @@ -312,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: @@ -328,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 = { From 453b863738aecf3852ce86e7fcc0a51cfa7f5e71 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Fri, 23 Dec 2022 17:24:33 -0700 Subject: [PATCH 33/36] Be sure to align data in FadeTransform --- manimlib/animation/fading.py | 1 + 1 file changed, 1 insertion(+) diff --git a/manimlib/animation/fading.py b/manimlib/animation/fading.py index 1ed2996f..c8103cbd 100644 --- a/manimlib/animation/fading.py +++ b/manimlib/animation/fading.py @@ -113,6 +113,7 @@ class FadeTransform(Transform): start, end = self.starting_mobject, self.ending_mobject for m0, m1 in ((start[1], start[0]), (end[0], end[1])): self.ghost_to(m0, m1) + self.ending_mobject.align_data(self.mobject) def ghost_to(self, source: Mobject, target: Mobject) -> None: source.replace(target, stretch=self.stretch, dim_to_match=self.dim_to_match) From 580d57a45c28ac99ed5acdbd2a223804287cd76c Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Fri, 23 Dec 2022 17:44:00 -0700 Subject: [PATCH 34/36] Add type hints and @staticmethod decorators to wraps functions --- manimlib/mobject/mobject.py | 3 ++- manimlib/mobject/types/vectorized_mobject.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/manimlib/mobject/mobject.py b/manimlib/mobject/mobject.py index f74d1475..720d6971 100644 --- a/manimlib/mobject/mobject.py +++ b/manimlib/mobject/mobject.py @@ -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"] diff --git a/manimlib/mobject/types/vectorized_mobject.py b/manimlib/mobject/types/vectorized_mobject.py index 8f29cf1e..2629552c 100644 --- a/manimlib/mobject/types/vectorized_mobject.py +++ b/manimlib/mobject/types/vectorized_mobject.py @@ -1004,7 +1004,8 @@ 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() From db52d0a73fee96c71fbcf5e99320c5933a769d2d Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Fri, 23 Dec 2022 17:45:35 -0700 Subject: [PATCH 35/36] Add type hints and @staticmethod decorators to wraps functions --- manimlib/mobject/mobject.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/manimlib/mobject/mobject.py b/manimlib/mobject/mobject.py index 720d6971..61cd07ce 100644 --- a/manimlib/mobject/mobject.py +++ b/manimlib/mobject/mobject.py @@ -1741,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(): From 62c9e2b58ff971f0529a6a066f62f246cb0c31d5 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Fri, 23 Dec 2022 17:57:24 -0700 Subject: [PATCH 36/36] Fix data["orientation"] alignment issue a separate way --- manimlib/animation/fading.py | 1 - manimlib/mobject/types/vectorized_mobject.py | 4 ++++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/manimlib/animation/fading.py b/manimlib/animation/fading.py index c8103cbd..1ed2996f 100644 --- a/manimlib/animation/fading.py +++ b/manimlib/animation/fading.py @@ -113,7 +113,6 @@ class FadeTransform(Transform): start, end = self.starting_mobject, self.ending_mobject for m0, m1 in ((start[1], start[0]), (end[0], end[1])): self.ghost_to(m0, m1) - self.ending_mobject.align_data(self.mobject) def ghost_to(self, source: Mobject, target: Mobject) -> None: source.replace(target, stretch=self.stretch, dim_to_match=self.dim_to_match) diff --git a/manimlib/mobject/types/vectorized_mobject.py b/manimlib/mobject/types/vectorized_mobject.py index 2629552c..26360254 100644 --- a/manimlib/mobject/types/vectorized_mobject.py +++ b/manimlib/mobject/types/vectorized_mobject.py @@ -944,6 +944,10 @@ 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):