From 1feae235669fe1e52278406afea53428a035dfe6 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 29 Dec 2022 10:37:46 -0800 Subject: [PATCH 01/16] Improve num_tex_symbols --- manimlib/utils/tex.py | 68 ++--- manimlib/utils/tex_to_symbol_count.py | 359 +++++++++++++------------- 2 files changed, 205 insertions(+), 222 deletions(-) diff --git a/manimlib/utils/tex.py b/manimlib/utils/tex.py index e3ea53bb..1a70e429 100644 --- a/manimlib/utils/tex.py +++ b/manimlib/utils/tex.py @@ -1,57 +1,41 @@ from __future__ import annotations import re -from functools import lru_cache -from typing import TYPE_CHECKING -if TYPE_CHECKING: - from typing import List, Tuple - - -@lru_cache(maxsize=1) -def get_pattern_symbol_count_pairs() -> List[Tuple[str, int]]: - from manimlib.utils.tex_to_symbol_count import TEX_TO_SYMBOL_COUNT - - # Gather all keys of previous map, grouped by common value - count_to_tex_list = dict() - for command, num in TEX_TO_SYMBOL_COUNT.items(): - if num not in count_to_tex_list: - count_to_tex_list[num] = [] - count_to_tex_list[num].append(command) - - # Create a list associating each count with a regular expression - # that will find any tex commands matching that list - pattern_symbol_count_pairs = list() - - # Account for patterns like \begin{align} and \phantom{thing} - # which, together with the bracketed content account for zero paths. - # Deliberately put this first in the list - tex_list = ["begin", "end", "phantom"] - pattern_symbol_count_pairs.append( - ("|".join(r"\\" + s + r"\{[^\\}]+\}" for s in tex_list), 0) - ) - - for count, tex_list in count_to_tex_list.items(): - pattern = "|".join(r"\\" + s 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 +from manimlib.utils.tex_to_symbol_count import TEX_TO_SYMBOL_COUNT def num_tex_symbols(tex: str) -> int: """ This function attempts to estimate the number of symbols that a given string of tex would produce. + + Warning, it may not behave perfectly """ + # First, remove patterns like \begin{align}, \phantom{thing}, + # \begin{array}{cc}, etc. + pattern = "|".join( + r"(\\" + s + ")" + r"(\{\w+\})?(\{\w+\})?(\[\w+\])?" + for s in ["begin", "end", "phantom"] + ) + for tup in re.findall(pattern, tex): + tex = tex.replace("".join(tup), " ") + + # Progressively count the symbols associated with certain tex commands, + # and remove those commands from the string, adding the number of symbols + # that command creates 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 + + # Start with the special case \sqrt[number] + for substr in re.findall(r"\\sqrt\[[0-9]+\]", tex): + total += len(substr) - 5 # e.g. \sqrt[3] is 3 symbols + tex = tex.replace(substr, " ") + + general_command = r"\\[a-zA-Z!,-/:;<>]+" + for substr in re.findall(general_command, tex): + total += TEX_TO_SYMBOL_COUNT.get(substr, 1) + tex = tex.replace(substr, " ") # Count remaining characters - total += sum(map(lambda c: c not in "^{} \n\t_$", tex)) + 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 index e858b572..c314b5dd 100644 --- a/manimlib/utils/tex_to_symbol_count.py +++ b/manimlib/utils/tex_to_symbol_count.py @@ -1,182 +1,181 @@ 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 + R"\!": 0, + R"\,": 0, + R"\-": 0, + R"\/": 0, + R"\:": 0, + R"\;": 0, + R"\>": 0, + R"\aa": 0, + R"\AA": 0, + R"\ae": 0, + R"\AE": 0, + R"\arccos": 6, + R"\arcsin": 6, + R"\arctan": 6, + R"\arg": 3, + R"\author": 0, + R"\bf": 0, + R"\bibliography": 0, + R"\bibliographystyle": 0, + R"\big": 0, + R"\Big": 0, + R"\bigodot": 4, + R"\bigoplus": 5, + R"\bigskip": 0, + R"\bmod": 3, + R"\boldmath": 0, + R"\bottomfraction": 2, + R"\bowtie": 2, + R"\cal": 0, + R"\cdots": 3, + R"\centering": 0, + R"\cite": 2, + R"\cong": 2, + R"\contentsline": 0, + R"\cos": 3, + R"\cosh": 4, + R"\cot": 3, + R"\coth": 4, + R"\csc": 3, + R"\date": 0, + R"\dblfloatpagefraction": 2, + R"\dbltopfraction": 2, + R"\ddots": 3, + R"\deg": 3, + R"\det": 3, + R"\dim": 3, + R"\displaystyle": 0, + R"\div": 2, + R"\doteq": 2, + R"\dotfill": 0, + R"\emph": 0, + R"\exp": 3, + R"\fbox": 4, + R"\floatpagefraction": 2, + R"\flushbottom": 0, + R"\footnotesize": 0, + R"\footnotetext": 0, + R"\frame": 2, + R"\framebox": 4, + R"\fussy": 0, + R"\gcd": 3, + R"\ghost": 0, + R"\glossary": 0, + R"\hfill": 0, + R"\hom": 3, + R"\hookleftarrow": 2, + R"\hookrightarrow": 2, + R"\hrulefill": 0, + R"\huge": 0, + R"\Huge": 0, + R"\hyphenation": 0, + R"\iff": 2, + R"\Im": 2, + R"\index": 0, + R"\inf": 3, + R"\it": 0, + R"\ker": 3, + R"\l": 0, + R"\L": 0, + R"\label": 0, + R"\large": 0, + R"\Large": 0, + R"\LARGE": 0, + R"\ldots": 3, + R"\lefteqn": 0, + R"\left": 0, + R"\lg": 2, + R"\lim": 3, + R"\liminf": 6, + R"\limsup": 6, + R"\linebreak": 0, + R"\ln": 2, + R"\log": 3, + R"\longleftarrow": 2, + R"\Longleftarrow": 2, + R"\longleftrightarrow": 2, + R"\Longleftrightarrow": 2, + R"\longmapsto": 3, + R"\longrightarrow": 2, + R"\Longrightarrow": 2, + R"\makebox": 0, + R"\mapsto": 2, + R"\markright": 0, + R"\max": 3, + R"\mbox": 0, + R"\medskip": 0, + R"\min": 3, + R"\mit": 0, + R"\models": 2, + R"\ne": 2, + R"\neq": 2, + R"\newline": 0, + R"\noindent": 0, + R"\nolinebreak": 0, + R"\nonumber": 0, + R"\nopagebreak": 0, + R"\normalmarginpar": 0, + R"\normalsize": 0, + R"\notin": 2, + R"\o": 0, + R"\O": 0, + R"\obeycr": 0, + R"\oe": 0, + R"\OE": 0, + R"\overbrace": 4, + R"\pagebreak": 0, + R"\pagenumbering": 0, + R"\pageref": 2, + R"\pmod": 5, + R"\Pr": 2, + R"\protect": 0, + R"\qquad": 0, + R"\quad": 0, + R"\raggedbottom": 0, + R"\raggedleft": 0, + R"\raggedright": 0, + R"\Re": 2, + R"\ref": 2, + R"\restorecr": 0, + R"\reversemarginpar": 0, + R"\right": 0, + R"\rm": 0, + R"\sc": 0, + R"\scriptscriptstyle": 0, + R"\scriptsize": 0, + R"\scriptstyle": 0, + R"\sec": 3, + R"\sf": 0, + R"\shortstack": 0, + R"\sin": 3, + R"\sinh": 4, + R"\sl": 0, + R"\sloppy": 0, + R"\small": 0, + R"\Small": 0, + R"\smallskip": 0, + R"\sqrt": 2, + R"\ss": 0, + R"\sup": 3, + R"\tan": 3, + R"\tanh": 4, + R"\textbf": 0, + R"\textfraction": 2, + R"\textstyle": 0, + R"\thicklines": 0, + R"\thinlines": 0, + R"\thinspace": 0, + R"\tiny": 0, + R"\title": 0, + R"\today": 15, + R"\topfraction": 2, + R"\tt": 0, + R"\typeout": 0, + R"\unboldmath": 0, + R"\underbrace": 6, + R"\underline": 0, + R"\value": 0, + R"\vdots": 3, + R"\vline": 0 } \ No newline at end of file From 4330f78ed6aeb5495f066bc949987d86996186c6 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 29 Dec 2022 10:44:52 -0800 Subject: [PATCH 02/16] Enable lag_ratio for TransformMatchingStrings --- manimlib/animation/transform_matching_parts.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/manimlib/animation/transform_matching_parts.py b/manimlib/animation/transform_matching_parts.py index de32bf89..3bd369d8 100644 --- a/manimlib/animation/transform_matching_parts.py +++ b/manimlib/animation/transform_matching_parts.py @@ -151,13 +151,14 @@ class TransformMatchingStrings(AnimationGroup): match_animation: type = Transform, mismatch_animation: type = Transform, run_time=2, + lag_ratio=0, **kwargs, ): self.source = source self.target = target matched_keys = matched_keys or list() key_map = key_map or dict() - self.anim_config = dict(run_time=run_time, **kwargs) + self.anim_config = dict(**kwargs) # We will progressively build up a list of transforms # from characters in source to those in target. These @@ -208,7 +209,11 @@ class TransformMatchingStrings(AnimationGroup): target_char, source.get_center(), **self.anim_config )) - super().__init__(*self.anims) + super().__init__( + *self.anims, + run_time=run_time, + lag_ratio=lag_ratio, + ) def add_transform( self, From 9018357d20134289740a1a9b7e2dc0dd33c267e1 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 29 Dec 2022 12:02:20 -0800 Subject: [PATCH 03/16] Fix Mobject.looks_identical --- manimlib/mobject/mobject.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/manimlib/mobject/mobject.py b/manimlib/mobject/mobject.py index 368a3d11..dc33efcb 100644 --- a/manimlib/mobject/mobject.py +++ b/manimlib/mobject/mobject.py @@ -657,7 +657,7 @@ class Mobject(object): self.match_updaters(mobject) return self - def looks_identical(self, mobject: Mobject): + def looks_identical(self, mobject: Mobject) -> bool: fam1 = self.family_members_with_points() fam2 = mobject.family_members_with_points() if len(fam1) != len(fam2): @@ -667,13 +667,8 @@ class Mobject(object): if set(d1).difference(d2): return False for key in d1: - eq = (d1[key] == d2[key]) - if isinstance(eq, bool): - if not eq: - return False - else: - if not eq.all(): - return False + if not np.isclose(d1[key], d2[key]).all(): + return False return True # Creating new Mobjects from this one From 3738f0a48e24b173f66d78822e331393c31dfe16 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 29 Dec 2022 12:02:28 -0800 Subject: [PATCH 04/16] Add Mobject.has_same_shape_as --- manimlib/mobject/mobject.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/manimlib/mobject/mobject.py b/manimlib/mobject/mobject.py index dc33efcb..c9fdef77 100644 --- a/manimlib/mobject/mobject.py +++ b/manimlib/mobject/mobject.py @@ -671,6 +671,16 @@ class Mobject(object): return False return True + def has_same_shape_as(self, mobject: Mobject) -> bool: + # Normalize both point sets by centering and making height 1 + points1, points2 = ( + (m.get_all_points() - m.get_center()) / m.get_height() + for m in (self, mobject) + ) + if len(points1) != len(points2): + return False + return bool(np.isclose(points1, points2).all()) + # Creating new Mobjects from this one def replicate(self, n: int) -> Group: From ef09d6fce2ab714cf2338263e7bb317ad815909d Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 29 Dec 2022 12:02:48 -0800 Subject: [PATCH 05/16] Allow for AnimationGroup to specify that parts belong to a VGroup --- manimlib/animation/composition.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/manimlib/animation/composition.py b/manimlib/animation/composition.py index e82ca79e..0acdd49c 100644 --- a/manimlib/animation/composition.py +++ b/manimlib/animation/composition.py @@ -29,6 +29,7 @@ class AnimationGroup(Animation): run_time: float = -1, # If negative, default to sum of inputed animation runtimes lag_ratio: float = 0.0, group: Mobject | None = None, + group_type: type = Group, **kwargs ): self.animations = [prepare_animation(anim) for anim in animations] @@ -38,7 +39,7 @@ class AnimationGroup(Animation): self.lag_ratio = lag_ratio self.group = group if self.group is None: - self.group = Group(*remove_list_redundancies( + self.group = group_type(*remove_list_redundancies( [anim.mobject for anim in self.animations] )) @@ -49,7 +50,7 @@ class AnimationGroup(Animation): **kwargs ) - def get_all_mobjects(self) -> Group: + def get_all_mobjects(self) -> Mobject: return self.group def begin(self) -> None: From 25ac5f350781463295e5f9f09fb14dbb1e251b85 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 29 Dec 2022 12:03:17 -0800 Subject: [PATCH 06/16] Use Mobject.1.has_same_shape_as in TransformMatchingStrings --- manimlib/animation/transform_matching_parts.py | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/manimlib/animation/transform_matching_parts.py b/manimlib/animation/transform_matching_parts.py index 3bd369d8..6c608cae 100644 --- a/manimlib/animation/transform_matching_parts.py +++ b/manimlib/animation/transform_matching_parts.py @@ -4,26 +4,20 @@ import itertools as it import numpy as np -from manimlib.animation.animation import Animation from manimlib.animation.composition import AnimationGroup from manimlib.animation.fading import FadeInFromPoint from manimlib.animation.fading import FadeOutToPoint from manimlib.animation.fading import FadeTransformPieces -from manimlib.animation.fading import FadeTransform -from manimlib.animation.transform import ReplacementTransform 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.old_tex_mobject import OldTex -from manimlib.mobject.svg.tex_mobject import Tex 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.old_tex_mobject import SingleStringTex from manimlib.scene.scene import Scene @@ -213,6 +207,7 @@ class TransformMatchingStrings(AnimationGroup): *self.anims, run_time=run_time, lag_ratio=lag_ratio, + group_type=VGroup, ) def add_transform( @@ -235,18 +230,10 @@ class TransformMatchingStrings(AnimationGroup): self.target_chars.remove(char) def find_pairs_with_matching_shapes(self, chars1, chars2) -> list[tuple[VMobject, VMobject]]: - for char in (*chars1, *chars2): - char.save_state() - char.set_height(1) - char.center() result = [] for char1, char2 in it.product(chars1, chars2): - p1 = char1.get_points() - p2 = char2.get_points() - if len(p1) == len(p2) and np.isclose(p1, p2 , atol=1e-1).all(): + if char1.has_same_shape_as(char2): result.append((char1, char2)) - for char in (*chars1, *chars2): - char.restore() return result def clean_up_from_scene(self, scene: Scene) -> None: From 3165a28cd0fe370829b2e27773ff415035b45316 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 29 Dec 2022 12:04:01 -0800 Subject: [PATCH 07/16] Clean up and fix VMobject.get_shader_wrapper_list --- manimlib/mobject/types/vectorized_mobject.py | 47 ++++++++------------ 1 file changed, 19 insertions(+), 28 deletions(-) diff --git a/manimlib/mobject/types/vectorized_mobject.py b/manimlib/mobject/types/vectorized_mobject.py index f7c703b6..87df2601 100644 --- a/manimlib/mobject/types/vectorized_mobject.py +++ b/manimlib/mobject/types/vectorized_mobject.py @@ -148,11 +148,6 @@ class VMobject(Mobject): raise Exception("All submobjects must be of type VMobject") super().add(*vmobjects) - def copy(self, deep: bool = False) -> VMobject: - result = super().copy(deep) - result.shader_wrapper_list = [sw.copy() for sw in self.shader_wrapper_list] - return result - # Colors def init_colors(self): self.set_fill( @@ -809,6 +804,11 @@ class VMobject(Mobject): # Alignment def align_points(self, vmobject: VMobject): if self.get_num_points() == len(vmobject.get_points()): + # If both have fill, and they have the same shape, just + # give them the same triangulation so that it's not recalculated + # needlessly throughout an animation + if self.has_fill() and vmobject.has_fill() and self.has_same_shape_as(vmobject): + vmobject.triangulation = self.triangulation return for mob in self, vmobject: @@ -1077,14 +1077,6 @@ class VMobject(Mobject): render_primitive=self.render_primitive, ) - self.shader_wrapper_list = [ - self.stroke_shader_wrapper.copy(), # Use for back stroke - self.fill_shader_wrapper.copy(), - self.stroke_shader_wrapper.copy(), - ] - for sw in self.shader_wrapper_list: - sw.uniforms = self.uniforms - def refresh_shader_wrapper_id(self): for wrapper in [self.fill_shader_wrapper, self.stroke_shader_wrapper]: wrapper.refresh_id() @@ -1107,30 +1099,29 @@ class VMobject(Mobject): # Build up data lists fill_shader_wrappers = [] stroke_shader_wrappers = [] - back_stroke_shader_wrappers = [] for submob in self.family_members_with_points(): if submob.has_fill(): fill_shader_wrappers.append(submob.get_fill_shader_wrapper()) if submob.has_stroke(): - ssw = submob.get_stroke_shader_wrapper() - if submob.draw_stroke_behind_fill: - back_stroke_shader_wrappers.append(ssw) - else: - stroke_shader_wrappers.append(ssw) + stroke_shader_wrappers.append(submob.get_stroke_shader_wrapper()) + if submob.draw_stroke_behind_fill: + self.draw_stroke_behind_fill = True - # Combine data lists - sw_lists = [ - back_stroke_shader_wrappers, - fill_shader_wrappers, - stroke_shader_wrappers, - ] - for sw, sw_list in zip(self.shader_wrapper_list, sw_lists): + self_sws = [self.fill_shader_wrapper, self.stroke_shader_wrapper] + sw_lists = [fill_shader_wrappers, stroke_shader_wrappers] + for sw, sw_list in zip(self_sws, sw_lists): if not sw_list: + sw.vert_data = resize_array(sw.vert_data, 0) continue - sw.read_in(*sw_list) + if sw is sw_list[0]: + sw.combine_with(*sw_list[1:]) + else: + sw.read_in(*sw_list) sw.depth_test = any(sw.depth_test for sw in sw_list) sw.uniforms.update(sw_list[0].uniforms) - return list(filter(lambda sw: len(sw.vert_data) > 0, self.shader_wrapper_list)) + if self.draw_stroke_behind_fill: + self_sws.reverse() + return [sw for sw in self_sws if len(sw.vert_data) > 0] def get_stroke_shader_data(self) -> np.ndarray: points = self.get_points() From a54a81744d8c3abecfb65b693d969c65883b8bf8 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 29 Dec 2022 12:04:35 -0800 Subject: [PATCH 08/16] Have ShaderWrapper.combine_with do nothing for empty arg list --- manimlib/shader_wrapper.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/manimlib/shader_wrapper.py b/manimlib/shader_wrapper.py index a053d9ca..13fb3a28 100644 --- a/manimlib/shader_wrapper.py +++ b/manimlib/shader_wrapper.py @@ -131,7 +131,8 @@ class ShaderWrapper(object): self.refresh_id() def combine_with(self, *shader_wrappers: ShaderWrapper) -> ShaderWrapper: - self.read_in(self.copy(), *shader_wrappers) + if len(shader_wrappers) > 0: + self.read_in(self.copy(), *shader_wrappers) return self def read_in(self, *shader_wrappers: ShaderWrapper) -> ShaderWrapper: From 13fc8daba91f92c34a5ac5a937bc08f9660a2093 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 29 Dec 2022 14:18:46 -0800 Subject: [PATCH 09/16] Extend StringMobject.select_unisolated_substring to work for regular expressions --- manimlib/mobject/svg/string_mobject.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/manimlib/mobject/svg/string_mobject.py b/manimlib/mobject/svg/string_mobject.py index 08a3c73d..3c81ccc1 100644 --- a/manimlib/mobject/svg/string_mobject.py +++ b/manimlib/mobject/svg/string_mobject.py @@ -547,7 +547,7 @@ class StringMobject(SVGMobject, ABC): def select_parts(self, selector: Selector) -> VGroup: specified_substrings = self.get_specified_substrings() - if isinstance(selector, str) and selector not in specified_substrings: + if isinstance(selector, (str, re.Pattern)) and selector not in specified_substrings: return self.select_unisolated_substring(selector) indices_list = self.get_submob_indices_lists_by_selector(selector) return self.build_parts_from_indices_lists(indices_list) @@ -563,11 +563,14 @@ class StringMobject(SVGMobject, ABC): def substr_to_path_count(self, substr: str) -> int: return len(re.sub(R"\s", "", substr)) - def select_unisolated_substring(self, substr: str) -> VGroup: + def select_unisolated_substring(self, pattern: str | re.Pattern) -> VGroup: + if isinstance(pattern, str): + pattern = re.compile(re.escape(pattern)) result = [] - for match in re.finditer(re.escape(substr), self.string): + for match in re.finditer(pattern, self.string): index = match.start() start = self.substr_to_path_count(self.string[:index]) + substr = match.group() end = start + self.substr_to_path_count(substr) result.append(self[start:end]) return VGroup(*result) From 4e014d7a8f5b91486a521188cbc5ec9db1cf89a1 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 29 Dec 2022 14:36:50 -0800 Subject: [PATCH 10/16] Have get_scene_config automatically find intersection of Scene parameters and config keys Previously it required duplicating any new parameter --- manimlib/extract_scene.py | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/manimlib/extract_scene.py b/manimlib/extract_scene.py index 64a19db4..efda54a0 100644 --- a/manimlib/extract_scene.py +++ b/manimlib/extract_scene.py @@ -54,22 +54,11 @@ def prompt_user_for_choice(scene_classes): def get_scene_config(config): - return dict([ - (key, config[key]) - for key in [ - "window_config", - "camera_config", - "file_writer_config", - "skip_animations", - "start_at_animation_number", - "end_at_animation_number", - "leave_progress_bars", - "show_animation_progress", - "preview", - "presenter_mode", - ] - ]) - + scene_parameters = inspect.signature(Scene).parameters.keys() + return { + key: config[key] + for key in set(scene_parameters).intersection(config.keys()) + } def compute_total_frames(scene_class, scene_config): """ From cba101995fb96f70f278aeedc6530e36f8ab52c2 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 29 Dec 2022 14:38:25 -0800 Subject: [PATCH 11/16] Allow for customizable exception display mode with Scene.embed --- manimlib/config.py | 1 + manimlib/default_config.yml | 1 + manimlib/scene/scene.py | 4 ++++ 3 files changed, 6 insertions(+) diff --git a/manimlib/config.py b/manimlib/config.py index 1f8e54f2..e2538777 100644 --- a/manimlib/config.py +++ b/manimlib/config.py @@ -491,4 +491,5 @@ def get_configuration(args: Namespace) -> dict: "presenter_mode": args.presenter_mode, "leave_progress_bars": args.leave_progress_bars, "show_animation_progress": args.show_animation_progress, + "embed_exception_mode": custom_config["embed_exception_mode"], } diff --git a/manimlib/default_config.yml b/manimlib/default_config.yml index 65001983..50023006 100644 --- a/manimlib/default_config.yml +++ b/manimlib/default_config.yml @@ -42,3 +42,4 @@ camera_resolutions: 4k: "3840x2160" default_resolution: "high" fps: 30 +embed_exception_mode: "Verbose" diff --git a/manimlib/scene/scene.py b/manimlib/scene/scene.py index 3be11916..398fd164 100644 --- a/manimlib/scene/scene.py +++ b/manimlib/scene/scene.py @@ -74,6 +74,7 @@ class Scene(object): preview: bool = True, presenter_mode: bool = False, show_animation_progress: bool = False, + embed_exception_mode = "", ): self.skip_animations = skip_animations self.always_update_mobjects = always_update_mobjects @@ -83,6 +84,7 @@ class Scene(object): self.preview = preview self.presenter_mode = presenter_mode self.show_animation_progress = show_animation_progress + self.embed_exception_mode = embed_exception_mode self.camera_config = {**self.default_camera_config, **camera_config} self.window_config = {**self.default_window_config, **window_config} @@ -249,6 +251,8 @@ class Scene(object): shell.events.register("post_run_cell", post_cell_func) + shell.magic(f"xmode {self.embed_exception_mode}") + # Launch shell shell( local_ns=local_ns, From 71815fd7de23188545d0073352324b4a2e9cfcad Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 29 Dec 2022 14:58:40 -0800 Subject: [PATCH 12/16] Add (optional) error sound for exceptions in Scene.embed --- manimlib/config.py | 1 + manimlib/default_config.yml | 1 + manimlib/scene/scene.py | 14 +++++++++++++- 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/manimlib/config.py b/manimlib/config.py index e2538777..17e203ab 100644 --- a/manimlib/config.py +++ b/manimlib/config.py @@ -492,4 +492,5 @@ def get_configuration(args: Namespace) -> dict: "leave_progress_bars": args.leave_progress_bars, "show_animation_progress": args.show_animation_progress, "embed_exception_mode": custom_config["embed_exception_mode"], + "embed_error_sound": custom_config["embed_error_sound"], } diff --git a/manimlib/default_config.yml b/manimlib/default_config.yml index 50023006..bfee5293 100644 --- a/manimlib/default_config.yml +++ b/manimlib/default_config.yml @@ -43,3 +43,4 @@ camera_resolutions: default_resolution: "high" fps: 30 embed_exception_mode: "Verbose" +embed_error_sound: False diff --git a/manimlib/scene/scene.py b/manimlib/scene/scene.py index 398fd164..66d1d529 100644 --- a/manimlib/scene/scene.py +++ b/manimlib/scene/scene.py @@ -74,7 +74,8 @@ class Scene(object): preview: bool = True, presenter_mode: bool = False, show_animation_progress: bool = False, - embed_exception_mode = "", + embed_exception_mode: str = "", + embed_error_sound: bool = False, ): self.skip_animations = skip_animations self.always_update_mobjects = always_update_mobjects @@ -85,6 +86,7 @@ class Scene(object): self.presenter_mode = presenter_mode self.show_animation_progress = show_animation_progress self.embed_exception_mode = embed_exception_mode + self.embed_error_sound = embed_error_sound self.camera_config = {**self.default_camera_config, **camera_config} self.window_config = {**self.default_window_config, **window_config} @@ -251,6 +253,16 @@ class Scene(object): shell.events.register("post_run_cell", post_cell_func) + if self.embed_error_sound: + # Play sound after any Exceptions + def custom_exc(shell, etype, evalue, tb, tb_offset=None): + # still show the error don't just swallow it + shell.showtraceback((etype, evalue, tb), tb_offset=tb_offset) + os.system("printf '\a'") + + shell.set_custom_exc((Exception,), custom_exc) + + # Set desired exception mode shell.magic(f"xmode {self.embed_exception_mode}") # Launch shell From ba68505c18b80b2e229098effa678171b802b258 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 29 Dec 2022 15:08:57 -0800 Subject: [PATCH 13/16] Flash around border on exceptions in Scene.embed --- manimlib/scene/scene.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/manimlib/scene/scene.py b/manimlib/scene/scene.py index 66d1d529..55b8c37d 100644 --- a/manimlib/scene/scene.py +++ b/manimlib/scene/scene.py @@ -16,15 +16,18 @@ import numpy as np from tqdm import tqdm as ProgressDisplay from manimlib.animation.animation import prepare_animation +from manimlib.animation.fading import VFadeInThenOut from manimlib.camera.camera import Camera from manimlib.config import get_module from manimlib.constants import ARROW_SYMBOLS from manimlib.constants import DEFAULT_WAIT_TIME from manimlib.constants import COMMAND_MODIFIER from manimlib.constants import SHIFT_MODIFIER +from manimlib.constants import RED from manimlib.event_handler import EVENT_DISPATCHER from manimlib.event_handler.event_type import EventType from manimlib.logger import log +from manimlib.mobject.frame import FullScreenRectangle from manimlib.mobject.mobject import _AnimationBuilder from manimlib.mobject.mobject import Group from manimlib.mobject.mobject import Mobject @@ -34,7 +37,6 @@ from manimlib.mobject.types.vectorized_mobject import VMobject from manimlib.scene.scene_file_writer import SceneFileWriter from manimlib.utils.family_ops import extract_mobject_family_members from manimlib.utils.family_ops import recursive_mobject_remove -from manimlib.utils.iterables import list_difference_update from typing import TYPE_CHECKING @@ -253,14 +255,18 @@ class Scene(object): shell.events.register("post_run_cell", post_cell_func) - if self.embed_error_sound: - # Play sound after any Exceptions - def custom_exc(shell, etype, evalue, tb, tb_offset=None): - # still show the error don't just swallow it - shell.showtraceback((etype, evalue, tb), tb_offset=tb_offset) + # Flash border, and potentially play sound, on exceptions + def custom_exc(shell, etype, evalue, tb, tb_offset=None): + # still show the error don't just swallow it + shell.showtraceback((etype, evalue, tb), tb_offset=tb_offset) + if self.embed_error_sound: os.system("printf '\a'") + self.play(VFadeInThenOut( + FullScreenRectangle().set_stroke(RED, 30).set_fill(opacity=0), + run_time=0.5, + )) - shell.set_custom_exc((Exception,), custom_exc) + shell.set_custom_exc((Exception,), custom_exc) # Set desired exception mode shell.magic(f"xmode {self.embed_exception_mode}") From 4bc7e3a8f2408bb3c0039b1f06180c62898034db Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 29 Dec 2022 15:40:18 -0800 Subject: [PATCH 14/16] Remove redundant long_line specification --- manimlib/mobject/svg/svg_mobject.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/manimlib/mobject/svg/svg_mobject.py b/manimlib/mobject/svg/svg_mobject.py index e8ac932b..099a90cc 100644 --- a/manimlib/mobject/svg/svg_mobject.py +++ b/manimlib/mobject/svg/svg_mobject.py @@ -292,15 +292,12 @@ class VMobjectFromSVGPath(VMobject): def __init__( self, path_obj: se.Path, - long_lines: bool = False, should_subdivide_sharp_curves: bool = False, - should_remove_null_curves: bool = False, **kwargs ): # Get rid of arcs path_obj.approximate_arcs_with_quads() self.path_obj = path_obj - self.long_lines = long_lines self.should_subdivide_sharp_curves = should_subdivide_sharp_curves self.should_remove_null_curves = should_remove_null_curves super().__init__(**kwargs) From 2dbb9367c4c82c574ffe205ad896b8820167bc65 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 29 Dec 2022 15:40:32 -0800 Subject: [PATCH 15/16] Default to removing null curves --- manimlib/mobject/svg/svg_mobject.py | 1 + 1 file changed, 1 insertion(+) diff --git a/manimlib/mobject/svg/svg_mobject.py b/manimlib/mobject/svg/svg_mobject.py index 099a90cc..f8d9bc30 100644 --- a/manimlib/mobject/svg/svg_mobject.py +++ b/manimlib/mobject/svg/svg_mobject.py @@ -293,6 +293,7 @@ class VMobjectFromSVGPath(VMobject): self, path_obj: se.Path, should_subdivide_sharp_curves: bool = False, + should_remove_null_curves: bool = True, **kwargs ): # Get rid of arcs From d6d75d8f9afbff713ba82a13479b46966e81f9dd Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 29 Dec 2022 15:50:35 -0800 Subject: [PATCH 16/16] Change from np.all(arr) to arr.all() --- manimlib/mobject/mobject.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manimlib/mobject/mobject.py b/manimlib/mobject/mobject.py index c9fdef77..9fa6ebee 100644 --- a/manimlib/mobject/mobject.py +++ b/manimlib/mobject/mobject.py @@ -1728,7 +1728,7 @@ class Mobject(object): for sm, sm1, sm2 in zip(self.get_family(), mobject1.get_family(), mobject2.get_family()): keys = sm.data.keys() & sm1.data.keys() & sm2.data.keys() sm.lock_data(list(filter( - lambda key: np.all(sm1.data[key] == sm2.data[key]), + lambda key: (sm1.data[key] == sm2.data[key]).all(), keys, ))) return self