diff --git a/manim/mobject/mobject.py b/manim/mobject/mobject.py index ee487a7fc5..ffcbd0e29a 100644 --- a/manim/mobject/mobject.py +++ b/manim/mobject/mobject.py @@ -14,18 +14,30 @@ import sys import types import warnings -from collections.abc import Iterable +from collections.abc import Iterable, Sequence from functools import partialmethod, reduce from pathlib import Path from typing import TYPE_CHECKING, Callable, Literal import numpy as np +from manim import config, logger +from manim.constants import ( + DEFAULT_MOBJECT_TO_EDGE_BUFFER, + DEFAULT_MOBJECT_TO_MOBJECT_BUFFER, + DL, + DOWN, + IN, + LEFT, + MED_SMALL_BUFF, + ORIGIN, + OUT, + RIGHT, + TAU, + UP, +) from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL - -from .. import config, logger -from ..constants import * -from ..utils.color import ( +from manim.utils.color import ( BLACK, WHITE, YELLOW_C, @@ -34,14 +46,15 @@ color_gradient, interpolate_color, ) -from ..utils.exceptions import MultiAnimationOverrideException -from ..utils.iterables import list_update, remove_list_redundancies -from ..utils.paths import straight_path -from ..utils.space_ops import angle_between_vectors, normalize, rotation_matrix +from manim.utils.exceptions import MultiAnimationOverrideException +from manim.utils.iterables import list_update, remove_list_redundancies +from manim.utils.paths import straight_path +from manim.utils.space_ops import angle_between_vectors, normalize, rotation_matrix if TYPE_CHECKING: from typing_extensions import Self, TypeAlias + from manim.animation.animation import Animation from manim.typing import ( FunctionOverride, ManimFloat, @@ -54,8 +67,6 @@ Vector3D, ) - from ..animation.animation import Animation - TimeBasedUpdater: TypeAlias = Callable[["Mobject", float], object] NonTimeBasedUpdater: TypeAlias = Callable[["Mobject"], object] Updater: TypeAlias = NonTimeBasedUpdater | TimeBasedUpdater @@ -71,7 +82,16 @@ class Mobject: Attributes ---------- submobjects : List[:class:`Mobject`] - The contained objects. + The contained objects, or "children" of this Mobject. + parents : List[:class:`Mobject`] + The Mobjects which contain this as part of their submobjects. It is + important to have a backreference to the parents in case this Mobject's + family changes in order to notify them of that change, because this + Mobject is also a part of their families. + family : List[:class:`Mobject`] | None + An optional, memoized list containing this Mobject, its submobjects, + those submobjects' submobjects, and so on. If the family must be + recalculated for any reason, this attribute is set to None. points : :class:`numpy.ndarray` The points of the objects. @@ -96,7 +116,7 @@ def __init_subclass__(cls, **kwargs) -> None: def __init__( self, - color: ParsableManimColor | list[ParsableManimColor] = WHITE, + color: ParsableManimColor | Sequence[ParsableManimColor] = WHITE, name: str | None = None, dim: int = 3, target=None, @@ -107,7 +127,12 @@ def __init__( self.target = target self.z_index = z_index self.point_hash = None - self.submobjects = [] + # NOTE: the _parents, _submobjects and _family attributes MUST BE + # IN THIS ORDER! Otherwise, Mobject.__deepcopy__() will break or + # generate a Mobject with a different hash! + self._parents: list[Mobject] = [] + self._submobjects: list[Mobject] = [] + self._family: list[Mobject] | None = None self.updaters: list[Updater] = [] self.updating_suspended = False self.color = ManimColor.parse(color) @@ -149,7 +174,7 @@ def _assert_valid_submobjects(self, submobjects: Iterable[Mobject]) -> Self: return self._assert_valid_submobjects_internal(submobjects, Mobject) def _assert_valid_submobjects_internal( - self, submobjects: list[Mobject], mob_class: type[Mobject] + self, submobjects: Iterable[Mobject], mob_class: type[Mobject] ) -> Self: for i, submob in enumerate(submobjects): if not isinstance(submob, mob_class): @@ -173,6 +198,30 @@ def _assert_valid_submobjects_internal( ) return self + @property + def submobjects(self) -> Sequence[Mobject]: + return self._submobjects + + # TODO: remove or modify this to prevent users from directly setting submobjects + @submobjects.setter + def submobjects(self, new_submobjects: Iterable[Mobject]) -> None: + self.set_submobjects(new_submobjects) + + def set_submobjects(self, new_submobjects: Iterable[Mobject]) -> Self: + self._assert_valid_submobjects(new_submobjects) + self.remove(*self._submobjects) + self.add(*new_submobjects) + self.note_changed_family() + return self + + @property + def parents(self) -> Sequence[Mobject]: + return self._parents + + @property + def family(self) -> Sequence[Mobject]: + return self.get_family() + @classmethod def animation_override_for( cls, @@ -395,7 +444,19 @@ def __deepcopy__(self, clone_from_id) -> Self: result = cls.__new__(cls) clone_from_id[id(self)] = result for k, v in self.__dict__.items(): - setattr(result, k, copy.deepcopy(v, clone_from_id)) + # This must be set manually because result has no attributes, + # and specifically INSIDE the loop to preserve attribute order, + # or test_hash_consistency() will fail! + if k == "_parents": + result._parents = [] + continue + if k == "_submobjects": + result._submobjects = copy.deepcopy(v, clone_from_id) + result._add_as_parent_of_submobs(result._submobjects) + elif k == "_family": + result._family = None + else: + setattr(result, k, copy.deepcopy(v, clone_from_id)) result.original_id = str(id(self)) return result @@ -420,6 +481,12 @@ def generate_points(self) -> None: subclasses. """ + def _add_as_parent_of_submobs(self, mobjects: Iterable[Mobject]) -> Self: + for mobject in mobjects: + if self not in mobject._parents: + mobject._parents.append(self) + return self + def add(self, *mobjects: Mobject) -> Self: """Add mobjects as submobjects. @@ -505,10 +572,12 @@ def add(self, *mobjects: Mobject) -> Self: "this is not possible. Repetitions are ignored.", ) - self.submobjects = list_update(self.submobjects, unique_mobjects) + self._submobjects = list_update(self._submobjects, unique_mobjects) + self._add_as_parent_of_submobs(unique_mobjects) + self.note_changed_family() return self - def insert(self, index: int, mobject: Mobject) -> None: + def insert(self, index: int, mobject: Mobject) -> Self: """Inserts a mobject at a specific position into self.submobjects Effectively just calls ``self.submobjects.insert(index, mobject)``, @@ -524,7 +593,11 @@ def insert(self, index: int, mobject: Mobject) -> None: The mobject to be inserted. """ self._assert_valid_submobjects([mobject]) - self.submobjects.insert(index, mobject) + # TODO: should verify that subsequent submobjects are not repeated + self._submobjects.insert(index, mobject) + self._add_as_parent_of_submobs([mobject]) + self.note_changed_family() + return self def __add__(self, mobject: Mobject): raise NotImplementedError @@ -579,7 +652,9 @@ def add_to_back(self, *mobjects: Mobject) -> Self: self._assert_valid_submobjects(mobjects) self.remove(*mobjects) # dict.fromkeys() removes duplicates while maintaining order - self.submobjects = list(dict.fromkeys(mobjects)) + self.submobjects + self._submobjects = list(dict.fromkeys(mobjects)) + self._submobjects + self._add_as_parent_of_submobs(mobjects) + self.note_changed_family() return self def remove(self, *mobjects: Mobject) -> Self: @@ -605,8 +680,11 @@ def remove(self, *mobjects: Mobject) -> Self: """ for mobject in mobjects: - if mobject in self.submobjects: - self.submobjects.remove(mobject) + if mobject in self._submobjects: + self._submobjects.remove(mobject) + if self in mobject._parents: + mobject._parents.remove(self) + self.note_changed_family() return self def __sub__(self, other): @@ -815,7 +893,7 @@ def depth(self, value: float): self.scale_to_fit_depth(value) # Can't be staticmethod because of point_cloud_mobject.py - def get_array_attrs(self) -> list[Literal["points"]]: + def get_array_attrs(self) -> Sequence[Literal["points"]]: return ["points"] def apply_over_attr_arrays(self, func: MappingFunction) -> Self: @@ -900,11 +978,11 @@ def update(self, dt: float = 0, recursive: bool = True) -> Self: else: updater(self) if recursive: - for submob in self.submobjects: + for submob in self._submobjects: submob.update(dt, recursive) return self - def get_time_based_updaters(self) -> list[TimeBasedUpdater]: + def get_time_based_updaters(self) -> Sequence[TimeBasedUpdater]: """Return all updaters using the ``dt`` parameter. The updaters use this parameter as the input for difference in time. @@ -944,7 +1022,7 @@ def has_time_based_updater(self) -> bool: "dt" in inspect.signature(updater).parameters for updater in self.updaters ) - def get_updaters(self) -> list[Updater]: + def get_updaters(self) -> Sequence[Updater]: """Return all updaters. Returns @@ -960,7 +1038,7 @@ def get_updaters(self) -> list[Updater]: """ return self.updaters - def get_family_updaters(self) -> list[Updater]: + def get_family_updaters(self) -> Sequence[Updater]: return list(it.chain(*(sm.get_updaters() for sm in self.get_family()))) def add_updater( @@ -1091,7 +1169,7 @@ def clear_updaters(self, recursive: bool = True) -> Self: """ self.updaters = [] if recursive: - for submob in self.submobjects: + for submob in self._submobjects: submob.clear_updaters() return self @@ -1148,7 +1226,7 @@ def suspend_updating(self, recursive: bool = True) -> Self: self.updating_suspended = True if recursive: - for submob in self.submobjects: + for submob in self._submobjects: submob.suspend_updating(recursive) return self @@ -1173,7 +1251,7 @@ def resume_updating(self, recursive: bool = True) -> Self: """ self.updating_suspended = False if recursive: - for submob in self.submobjects: + for submob in self._submobjects: submob.resume_updating(recursive) self.update(dt=0, recursive=recursive) return self @@ -1333,7 +1411,7 @@ def apply_function_to_position(self, function: MappingFunction) -> Self: return self def apply_function_to_submobject_positions(self, function: MappingFunction) -> Self: - for submob in self.submobjects: + for submob in self._submobjects: submob.apply_function_to_position(function) return self @@ -1733,7 +1811,7 @@ def set_z(self, z: float, direction: Vector3D = ORIGIN) -> Self: def space_out_submobjects(self, factor: float = 1.5, **kwargs) -> Self: self.scale(factor, **kwargs) - for submob in self.submobjects: + for submob in self._submobjects: submob.scale(1.0 / factor) return self @@ -1755,7 +1833,7 @@ def move_to( def replace( self, mobject: Mobject, dim_to_match: int = 0, stretch: bool = False ) -> Self: - if not mobject.get_num_points() and not mobject.submobjects: + if not mobject.get_num_points() and not mobject._submobjects: raise Warning("Attempting to replace mobject with no points") if stretch: self.stretch_to_fit_width(mobject.width) @@ -1848,7 +1926,7 @@ def add_background_rectangle( return self def add_background_rectangle_to_submobjects(self, **kwargs) -> Self: - for submobject in self.submobjects: + for submobject in self._submobjects: submobject.add_background_rectangle(**kwargs) return self @@ -1868,7 +1946,7 @@ def set_color( of color """ if family: - for submob in self.submobjects: + for submob in self._submobjects: submob.set_color(color, family=family) self.color = ManimColor.parse(color) @@ -1944,13 +2022,13 @@ def fade_to( new_color = interpolate_color(self.get_color(), color, alpha) self.set_color(new_color, family=False) if family: - for submob in self.submobjects: + for submob in self._submobjects: submob.fade_to(color, alpha) return self def fade(self, darkness: float = 0.5, family: bool = True) -> Self: if family: - for submob in self.submobjects: + for submob in self._submobjects: submob.fade(darkness, family) return self @@ -1989,7 +2067,7 @@ def restore(self) -> Self: def reduce_across_dimension(self, reduce_func: Callable, dim: int): """Find the min or max value from a dimension across all points in this and submobjects.""" assert dim >= 0 and dim <= 2 - if len(self.submobjects) == 0 and len(self.points) == 0: + if len(self._submobjects) == 0 and len(self.points) == 0: # If we have no points and no submobjects, return 0 (e.g. center) return 0 @@ -2002,7 +2080,7 @@ def reduce_across_dimension(self, reduce_func: Callable, dim: int): rv = reduce_func(self.points[:, dim]) # Recursively ask submobjects (if any) for the biggest/ # smallest dimension they have and compare it to the return value. - for mobj in self.submobjects: + for mobj in self._submobjects: value = mobj.reduce_across_dimension(reduce_func, dim) if rv is None: rv = value @@ -2010,11 +2088,11 @@ def reduce_across_dimension(self, reduce_func: Callable, dim: int): rv = reduce_func([value, rv]) return rv - def nonempty_submobjects(self) -> list[Self]: + def nonempty_submobjects(self) -> Sequence[Mobject]: return [ submob - for submob in self.submobjects - if len(submob.submobjects) != 0 or len(submob.points) != 0 + for submob in self._submobjects + if len(submob._submobjects) != 0 or len(submob.points) != 0 ] def get_merged_array(self, array_attr: str) -> np.ndarray: @@ -2024,7 +2102,7 @@ def get_merged_array(self, array_attr: str) -> np.ndarray: traversal of the submobjects. """ result = getattr(self, array_attr) - for submob in self.submobjects: + for submob in self._submobjects: result = np.append(result, submob.get_merged_array(array_attr), axis=0) return result @@ -2198,7 +2276,7 @@ def proportion_from_point(self, point: Point3D) -> float: def get_pieces(self, n_pieces: float) -> Group: template = self.copy() - template.submobjects = [] + template.set_submobjects([]) alphas = np.linspace(0, 1, n_pieces + 1) return Group( *( @@ -2308,18 +2386,106 @@ def get_mobject_type_class() -> type[Mobject]: """Return the base class of this mobject type.""" return Mobject - def split(self) -> list[Self]: + def split(self) -> Sequence[Mobject]: result = [self] if len(self.points) > 0 else [] - return result + self.submobjects + return result + self._submobjects - def get_family(self, recurse: bool = True) -> list[Self]: - sub_families = [x.get_family() for x in self.submobjects] - all_mobjects = [self] + list(it.chain(*sub_families)) - return remove_list_redundancies(all_mobjects) + def note_changed_family(self, *, only_changed_order=False) -> Self: + """Indicates that this Mobject's family should be recalculated, by + setting it to None to void the previous computation. If this Mobject + has parents, it is also part of their respective families, so they must + be notified as well. - def family_members_with_points(self) -> list[Self]: + This method must be called after any change which involves modifying + some Mobject's submobjects, such as a call to Mobject.add. + + Parameters + ---------- + only_changed_order + If True, indicate that the family still contains the same Mobjects, + only in a different order. This prevents recalculating bounding + boxes and updater statuses, because they remain the same. If False, + indicate that some Mobjects were added or removed to the family, + and trigger the aforementioned recalculations. Default is False. + + Returns + ------- + :class:`Mobject` + The Mobject itself. + """ + self._family = None + # TODO: Implement when bounding boxes and updater statuses are implemented + # if not only_changed_order: + # self.refresh_has_updater_status() + # self.refresh_bounding_box() + for parent in self._parents: + parent.note_changed_family(only_changed_order=only_changed_order) + return self + + def get_family(self, *, recurse: bool = True) -> Sequence[Mobject]: + """Obtain the family of this Mobject, consisting of itself, its + submobjects, the submobjects' submobjects, and so on. If the + family was calculated previously and memoized into the :attr:`family` + attribute as a list, return that list. Otherwise, if the attribute is + None, calculate and memoize it now. + + Parameters + ---------- + recurse + If True, explore this Mobject's submobjects and so on, to + compute the full family. Otherwise, stop at this Mobject and + return a list containing only this one. Default is True. + + Returns + ------- + list[:class:`Mobject`] + The family of this Mobject. + """ + if not recurse: + return [self] + if self._family is None: + # Reconstruct and save + sub_families = (sm.get_family() for sm in self._submobjects) + family = [self, *it.chain(*sub_families)] + self._family = remove_list_redundancies(family) + return self._family + + def family_members_with_points(self) -> Sequence[Mobject]: return [m for m in self.get_family() if m.get_num_points() > 0] + def get_ancestors(self, *, extended: bool = False) -> Sequence[Mobject]: + """Returns the parents, grandparents, etc. of this :class:`Mobject`. + The order of the result is top-down: from the highest members of the + hierarchy, down to the parents of this Mobjects. + + If ``extended`` is set to ``True``, it includes the ancestors of all + family members, e.g. any other parents of a submobject. + + Parameters + ---------- + extended + Whether to also include the ancestors of all the family members + of this Mobject or not. Defaults to ``False``. + + Returns + ------- + list[:class:`Mobject`] + The ancestors of this Mobject, and possibly also the ancestors + of this Mobject's family members. + """ + ancestors = [] + to_process = self.get_family(recurse=extended).copy() + excluded = set(to_process) + while to_process: + for p in to_process.pop().parents: + if p not in excluded: + ancestors.append(p) + to_process.append(p) + # Ensure mobjects highest in the hierarchy show up first + ancestors.reverse() + # Remove list redundancies while preserving order + return list(dict.fromkeys(ancestors)) + def arrange( self, direction: Vector3D = RIGHT, @@ -2344,7 +2510,7 @@ def construct(self): x = VGroup(s1, s2, s3, s4).set_x(0).arrange(buff=1.0) self.add(x) """ - for m1, m2 in zip(self.submobjects, self.submobjects[1:]): + for m1, m2 in zip(self._submobjects, self._submobjects[1:]): m2.next_to(m1, direction, buff, **kwargs) if center: self.center() @@ -2451,7 +2617,7 @@ def construct(self): """ from manim.mobject.geometry.line import Line - mobs = self.submobjects.copy() + mobs = self._submobjects.copy() start_pos = self.get_center() # get cols / rows values if given (implicitly) @@ -2602,17 +2768,20 @@ def sort( def submob_func(m: Mobject): return point_to_num_func(m.get_center()) - self.submobjects.sort(key=submob_func) + self._submobjects.sort(key=submob_func) + self.note_changed_family(only_changed_order=True) return self - def shuffle(self, recursive: bool = False) -> None: + def shuffle(self, recursive: bool = False) -> Self: """Shuffles the list of :attr:`submobjects`.""" if recursive: - for submob in self.submobjects: + for submob in self._submobjects: submob.shuffle(recursive=True) - random.shuffle(self.submobjects) + random.shuffle(self._submobjects) + self.note_changed_family(only_changed_order=True) + return self - def invert(self, recursive: bool = False) -> None: + def invert(self, recursive: bool = False) -> Self: """Inverts the list of :attr:`submobjects`. Parameters @@ -2634,9 +2803,11 @@ def construct(self): self.play(Write(s), Write(s2)) """ if recursive: - for submob in self.submobjects: + for submob in self._submobjects: submob.invert(recursive=True) - self.submobjects.reverse() + self._submobjects.reverse() + self.note_changed_family(only_changed_order=True) + return self # Just here to keep from breaking old scenes. def arrange_submobjects(self, *args, **kwargs) -> Self: @@ -2664,7 +2835,7 @@ def sort_submobjects(self, *args, **kwargs) -> Self: """Sort the :attr:`submobjects`""" return self.sort(*args, **kwargs) - def shuffle_submobjects(self, *args, **kwargs) -> None: + def shuffle_submobjects(self, *args, **kwargs) -> Self: """Shuffles the order of :attr:`submobjects` Examples @@ -2704,7 +2875,7 @@ def align_data(self, mobject: Mobject, skip_point_alignment: bool = False) -> No if not skip_point_alignment: self.align_points(mobject) # Recurse - for m1, m2 in zip(self.submobjects, mobject.submobjects): + for m1, m2 in zip(self._submobjects, mobject._submobjects): m1.align_data(m2) def get_point_mobject(self, center=None): @@ -2729,8 +2900,8 @@ def align_points_with_larger(self, larger_mobject: Mobject): def align_submobjects(self, mobject: Mobject) -> Self: mob1 = self mob2 = mobject - n1 = len(mob1.submobjects) - n2 = len(mob2.submobjects) + n1 = len(mob1._submobjects) + n2 = len(mob2._submobjects) mob1.add_n_more_submobjects(max(0, n2 - n1)) mob2.add_n_more_submobjects(max(0, n1 - n2)) return self @@ -2753,20 +2924,23 @@ def null_point_align(self, mobject: Mobject): def push_self_into_submobjects(self) -> Self: copy = self.copy() - copy.submobjects = [] + copy._submobjects = [] self.reset_points() self.add(copy) return self - def add_n_more_submobjects(self, n: int) -> Self | None: + def add_n_more_submobjects(self, n: int) -> Self: if n == 0: - return None + return self - curr = len(self.submobjects) + curr = len(self._submobjects) if curr == 0: # If empty, simply add n point mobjects - self.submobjects = [self.get_point_mobject() for k in range(n)] - return None + self._submobjects = [self.get_point_mobject() for _ in range(n)] + for submob in self._submobjects: + submob._parents.append(self) + self.note_changed_family() + return self target = curr + n # TODO, factor this out to utils so as to reuse @@ -2774,11 +2948,14 @@ def add_n_more_submobjects(self, n: int) -> Self | None: repeat_indices = (np.arange(target) * curr) // target split_factors = [sum(repeat_indices == i) for i in range(curr)] new_submobs = [] - for submob, sf in zip(self.submobjects, split_factors): + for submob, sf in zip(self._submobjects, split_factors): new_submobs.append(submob) for _ in range(1, sf): - new_submobs.append(submob.copy().fade(1)) - self.submobjects = new_submobs + submob_copy = submob.copy().fade(1) + submob_copy._parents.append(self) + new_submobs.append(submob_copy) + self._submobjects = new_submobs + self.note_changed_family() return self def repeat_submobject(self, submob: Mobject) -> Self: @@ -3013,7 +3190,7 @@ def construct(self): self.add(circle) """ if family: - for submob in self.submobjects: + for submob in self._submobjects: submob.set_z_index(z_index_value, family=family) self.z_index = z_index_value return self diff --git a/manim/mobject/opengl/opengl_mobject.py b/manim/mobject/opengl/opengl_mobject.py index c907c4c2e0..cefa5e9edb 100644 --- a/manim/mobject/opengl/opengl_mobject.py +++ b/manim/mobject/opengl/opengl_mobject.py @@ -723,7 +723,7 @@ def compute_bounding_box(self) -> npt.NDArray[float]: def refresh_bounding_box( self, recurse_down: bool = False, recurse_up: bool = True ) -> Self: - for mob in self.get_family(recurse_down): + for mob in self.get_family(recurse=recurse_down): mob.needs_new_bounding_box = True if recurse_up: for parent in self.parents: @@ -762,7 +762,7 @@ def assemble_family(self) -> Self: parent.assemble_family() return self - def get_family(self, recurse: bool = True) -> Sequence[OpenGLMobject]: + def get_family(self, *, recurse: bool = True) -> Sequence[OpenGLMobject]: if recurse and hasattr(self, "family"): return self.family else: @@ -2071,7 +2071,7 @@ def set_rgba_array( # Color only if color is not None and opacity is None: - for mob in self.get_family(recurse): + for mob in self.get_family(recurse=recurse): mob.data[name] = resize_array( mob.data[name] if name in mob.data else np.empty((1, 3)), len(rgbs) ) @@ -2079,7 +2079,7 @@ def set_rgba_array( # Opacity only if color is None and opacity is not None: - for mob in self.get_family(recurse): + for mob in self.get_family(recurse=recurse): mob.data[name] = resize_array( mob.data[name] if name in mob.data else np.empty((1, 3)), len(opacities), @@ -2089,7 +2089,7 @@ def set_rgba_array( # Color and opacity if color is not None and opacity is not None: rgbas = np.array([[*rgb, o] for rgb, o in zip(*make_even(rgbs, opacities))]) - for mob in self.get_family(recurse): + for mob in self.get_family(recurse=recurse): mob.data[name] = rgbas.copy() return self @@ -2112,7 +2112,7 @@ def set_rgba_array_direct( recurse set to true to recursively apply this method to submobjects """ - for mob in self.get_family(recurse): + for mob in self.get_family(recurse=recurse): mob.data[name] = rgbas.copy() def set_color( @@ -2172,7 +2172,7 @@ def get_gloss(self) -> float: return self.gloss def set_gloss(self, gloss: float, recurse: bool = True) -> Self: - for mob in self.get_family(recurse): + for mob in self.get_family(recurse=recurse): mob.gloss = gloss return self @@ -2180,7 +2180,7 @@ def get_shadow(self) -> float: return self.shadow def set_shadow(self, shadow: float, recurse: bool = True) -> Self: - for mob in self.get_family(recurse): + for mob in self.get_family(recurse=recurse): mob.shadow = shadow return self diff --git a/manim/mobject/opengl/opengl_surface.py b/manim/mobject/opengl/opengl_surface.py index 565b8c71cf..59504819f3 100644 --- a/manim/mobject/opengl/opengl_surface.py +++ b/manim/mobject/opengl/opengl_surface.py @@ -437,7 +437,7 @@ def init_colors(self): self.opacity = np.array([self.uv_surface.rgbas[:, 3]]) def set_opacity(self, opacity, recurse=True): - for mob in self.get_family(recurse): + for mob in self.get_family(recurse=recurse): mob.opacity = np.array([[o] for o in listify(opacity)]) return self diff --git a/manim/mobject/opengl/opengl_vectorized_mobject.py b/manim/mobject/opengl/opengl_vectorized_mobject.py index 8037760c4a..f7b2dffda2 100644 --- a/manim/mobject/opengl/opengl_vectorized_mobject.py +++ b/manim/mobject/opengl/opengl_vectorized_mobject.py @@ -268,11 +268,11 @@ def set_stroke( self.set_rgba_array(color, opacity, "stroke_rgba", recurse) if width is not None: - for mob in self.get_family(recurse): + for mob in self.get_family(recurse=recurse): mob.stroke_width = np.array([[width] for width in tuplify(width)]) if background is not None: - for mob in self.get_family(recurse): + for mob in self.get_family(recurse=recurse): mob.draw_stroke_behind_fill = background return self @@ -439,7 +439,7 @@ def get_opacity(self): return self.get_stroke_opacity() def set_flat_stroke(self, flat_stroke=True, recurse=True): - for mob in self.get_family(recurse): + for mob in self.get_family(recurse=recurse): mob.flat_stroke = flat_stroke return self @@ -549,7 +549,7 @@ def is_closed(self): return self.consider_points_equals(self.points[0], self.points[-1]) def subdivide_sharp_curves(self, angle_threshold=30 * DEGREES, recurse=True): - vmobs = [vm for vm in self.get_family(recurse) if vm.has_points()] + vmobs = [vm for vm in self.get_family(recurse=recurse) if vm.has_points()] for vmob in vmobs: new_points = [] for tup in vmob.get_bezier_tuples(): @@ -1249,7 +1249,7 @@ def insert_n_curves(self, n: int, recurse=True) -> OpenGLVMobject: OpenGLVMobject for chaining. """ - for mob in self.get_family(recurse): + for mob in self.get_family(recurse=recurse): if mob.get_num_curves() > 0: new_points = mob.insert_n_curves_to_point_list(n, mob.points) # TODO, this should happen in insert_n_curves_to_point_list diff --git a/manim/mobject/types/vectorized_mobject.py b/manim/mobject/types/vectorized_mobject.py index 86b5cd1386..0944499636 100644 --- a/manim/mobject/types/vectorized_mobject.py +++ b/manim/mobject/types/vectorized_mobject.py @@ -164,7 +164,8 @@ def __init__( ) self.cap_style: CapStyleType = cap_style super().__init__(**kwargs) - self.submobjects: list[VMobject] + self._submobjects: list[VMobject] + self._family: list[VMobject] | None # TODO: Find where color overwrites are happening and remove the color doubling # if "color" in kwargs: diff --git a/manim/utils/hashing.py b/manim/utils/hashing.py index 9557ba18f1..c9a20b1b40 100644 --- a/manim/utils/hashing.py +++ b/manim/utils/hashing.py @@ -301,6 +301,13 @@ def encode(self, obj: Any): The object encoder with the standard json process. """ _Memoizer.mark_as_processed(obj) + if isinstance(obj, Mobject): + # Skip the _family attribute when encoding + family = obj._family + obj._family = None + encoded = super().encode(obj) + obj._family = family + return encoded if isinstance(obj, (dict, list, tuple)): return super().encode(self._cleaned_iterable(obj)) return super().encode(obj)