mirror of
https://github.com/3b1b/manim.git
synced 2025-11-14 15:47:44 +00:00
Merge branch '3b1b:master' into refactor
This commit is contained in:
commit
69db53d612
7 changed files with 168 additions and 73 deletions
|
|
@ -245,13 +245,13 @@ def insert_embed_line(file_name: str, scene_name: str, line_marker: str):
|
||||||
n_spaces = get_indent(lines[prev_line_num])
|
n_spaces = get_indent(lines[prev_line_num])
|
||||||
new_lines = list(lines)
|
new_lines = list(lines)
|
||||||
new_lines.insert(prev_line_num + 1, " " * n_spaces + "self.embed()\n")
|
new_lines.insert(prev_line_num + 1, " " * n_spaces + "self.embed()\n")
|
||||||
with open(file_name, 'w') as fp:
|
alt_file = file_name.replace(".py", "_insert_embed.py")
|
||||||
|
with open(alt_file, 'w') as fp:
|
||||||
fp.writelines(new_lines)
|
fp.writelines(new_lines)
|
||||||
try:
|
try:
|
||||||
yield file_name
|
yield alt_file
|
||||||
finally:
|
finally:
|
||||||
with open(file_name, 'w') as fp:
|
os.remove(alt_file)
|
||||||
fp.writelines(lines)
|
|
||||||
|
|
||||||
|
|
||||||
def get_custom_config():
|
def get_custom_config():
|
||||||
|
|
|
||||||
|
|
@ -895,7 +895,15 @@ class Polygon(VMobject):
|
||||||
def get_vertices(self) -> list[np.ndarray]:
|
def get_vertices(self) -> list[np.ndarray]:
|
||||||
return self.get_start_anchors()
|
return self.get_start_anchors()
|
||||||
|
|
||||||
def round_corners(self, radius: float = 0.5):
|
def round_corners(self, radius: float | None = None):
|
||||||
|
if radius is None:
|
||||||
|
verts = self.get_vertices()
|
||||||
|
min_edge_length = min(
|
||||||
|
get_norm(v1 - v2)
|
||||||
|
for v1, v2 in zip(verts, verts[1:])
|
||||||
|
if not np.isclose(v1, v2).all()
|
||||||
|
)
|
||||||
|
radius = 0.25 * min_edge_length
|
||||||
vertices = self.get_vertices()
|
vertices = self.get_vertices()
|
||||||
arcs = []
|
arcs = []
|
||||||
for v1, v2, v3 in adjacent_n_tuples(vertices, 3):
|
for v1, v2, v3 in adjacent_n_tuples(vertices, 3):
|
||||||
|
|
|
||||||
|
|
@ -343,6 +343,26 @@ class Mobject(object):
|
||||||
def family_members_with_points(self):
|
def family_members_with_points(self):
|
||||||
return [m for m in self.get_family() if m.has_points()]
|
return [m for m in self.get_family() if m.has_points()]
|
||||||
|
|
||||||
|
def get_ancestors(self, extended: bool = False) -> list[Mobject]:
|
||||||
|
"""
|
||||||
|
Returns parents, grandparents, etc.
|
||||||
|
Order of result should be from higher members of the hierarchy down.
|
||||||
|
|
||||||
|
If extended is set to true, it includes the ancestors of all family members,
|
||||||
|
e.g. any other parents of a submobject
|
||||||
|
"""
|
||||||
|
ancestors = []
|
||||||
|
to_process = list(self.get_family(recurse=extended))
|
||||||
|
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)
|
||||||
|
# Remove redundancies while preserving order
|
||||||
|
ancestors.reverse()
|
||||||
|
return list(dict.fromkeys(ancestors))
|
||||||
|
|
||||||
def add(self, *mobjects: Mobject):
|
def add(self, *mobjects: Mobject):
|
||||||
if self in mobjects:
|
if self in mobjects:
|
||||||
raise Exception("Mobject cannot contain self")
|
raise Exception("Mobject cannot contain self")
|
||||||
|
|
@ -354,12 +374,13 @@ class Mobject(object):
|
||||||
self.assemble_family()
|
self.assemble_family()
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def remove(self, *mobjects: Mobject):
|
def remove(self, *mobjects: Mobject, reassemble: bool = True):
|
||||||
for mobject in mobjects:
|
for mobject in mobjects:
|
||||||
if mobject in self.submobjects:
|
if mobject in self.submobjects:
|
||||||
self.submobjects.remove(mobject)
|
self.submobjects.remove(mobject)
|
||||||
if self in mobject.parents:
|
if self in mobject.parents:
|
||||||
mobject.parents.remove(self)
|
mobject.parents.remove(self)
|
||||||
|
if reassemble:
|
||||||
self.assemble_family()
|
self.assemble_family()
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|
@ -381,7 +402,7 @@ class Mobject(object):
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def set_submobjects(self, submobject_list: list[Mobject]):
|
def set_submobjects(self, submobject_list: list[Mobject]):
|
||||||
self.remove(*self.submobjects)
|
self.remove(*self.submobjects, reassemble=False)
|
||||||
self.add(*submobject_list)
|
self.add(*submobject_list)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|
@ -526,17 +547,24 @@ class Mobject(object):
|
||||||
for key, value in self.uniforms.items()
|
for key, value in self.uniforms.items()
|
||||||
}
|
}
|
||||||
|
|
||||||
result.submobjects = []
|
# Instead of adding using result.add, which does some checks for updating
|
||||||
result.add(*(sm.copy() for sm in self.submobjects))
|
# updater statues and bounding box, just directly modify the family-related
|
||||||
result.match_updaters(self)
|
# lists
|
||||||
|
result.submobjects = [sm.copy() for sm in self.submobjects]
|
||||||
|
for sm in result.submobjects:
|
||||||
|
sm.parents = [result]
|
||||||
|
result.family = [result, *it.chain(*(sm.get_family() for sm in result.submobjects))]
|
||||||
|
|
||||||
|
# Similarly, instead of calling match_updaters, since we know the status
|
||||||
|
# won't have changed, just directly match.
|
||||||
|
result.non_time_updaters = list(self.non_time_updaters)
|
||||||
|
result.time_based_updaters = list(self.time_based_updaters)
|
||||||
|
|
||||||
family = self.get_family()
|
family = self.get_family()
|
||||||
for attr, value in list(self.__dict__.items()):
|
for attr, value in list(self.__dict__.items()):
|
||||||
if isinstance(value, Mobject) and value is not self:
|
if isinstance(value, Mobject) and value is not self:
|
||||||
if value in family:
|
if value in family:
|
||||||
setattr(result, attr, result.family[self.family.index(value)])
|
setattr(result, attr, result.family[self.family.index(value)])
|
||||||
else:
|
|
||||||
setattr(result, attr, value.copy())
|
|
||||||
if isinstance(value, np.ndarray):
|
if isinstance(value, np.ndarray):
|
||||||
setattr(result, attr, value.copy())
|
setattr(result, attr, value.copy())
|
||||||
if isinstance(value, ShaderWrapper):
|
if isinstance(value, ShaderWrapper):
|
||||||
|
|
@ -590,6 +618,23 @@ class Mobject(object):
|
||||||
self.refresh_bounding_box(recurse_down=True)
|
self.refresh_bounding_box(recurse_down=True)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
def looks_identical(self, mobject: Mobject):
|
||||||
|
fam1 = self.get_family()
|
||||||
|
fam2 = mobject.get_family()
|
||||||
|
if len(fam1) != len(fam2):
|
||||||
|
return False
|
||||||
|
for m1, m2 in zip(fam1, fam2):
|
||||||
|
for d1, d2 in [(m1.data, m2.data), (m1.uniforms, m2.uniforms)]:
|
||||||
|
if set(d1).difference(d2):
|
||||||
|
return False
|
||||||
|
for key in d1:
|
||||||
|
if isinstance(d1[key], np.ndarray):
|
||||||
|
if not np.all(d1[key] == d2[key]):
|
||||||
|
return False
|
||||||
|
elif d1[key] != d2[key]:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
# Creating new Mobjects from this one
|
# Creating new Mobjects from this one
|
||||||
|
|
||||||
def replicate(self, n: int) -> Group:
|
def replicate(self, n: int) -> Group:
|
||||||
|
|
|
||||||
|
|
@ -206,10 +206,18 @@ class InteractiveScene(Scene):
|
||||||
return self.get_corner_dots(mobject)
|
return self.get_corner_dots(mobject)
|
||||||
|
|
||||||
def refresh_selection_highlight(self):
|
def refresh_selection_highlight(self):
|
||||||
|
if len(self.selection) > 0:
|
||||||
|
self.remove(self.selection_highlight)
|
||||||
self.selection_highlight.set_submobjects([
|
self.selection_highlight.set_submobjects([
|
||||||
self.get_highlight(mob)
|
self.get_highlight(mob)
|
||||||
for mob in self.selection
|
for mob in self.selection
|
||||||
])
|
])
|
||||||
|
index = min((
|
||||||
|
i for i, mob in enumerate(self.mobjects)
|
||||||
|
for sm in self.selection
|
||||||
|
if sm in mob.get_family()
|
||||||
|
))
|
||||||
|
self.mobjects.insert(index, self.selection_highlight)
|
||||||
|
|
||||||
def add_to_selection(self, *mobjects):
|
def add_to_selection(self, *mobjects):
|
||||||
mobs = list(filter(
|
mobs = list(filter(
|
||||||
|
|
@ -219,8 +227,10 @@ class InteractiveScene(Scene):
|
||||||
if len(mobs) == 0:
|
if len(mobs) == 0:
|
||||||
return
|
return
|
||||||
self.selection.add(*mobs)
|
self.selection.add(*mobs)
|
||||||
self.selection_highlight.add(*map(self.get_highlight, mobs))
|
self.refresh_selection_highlight()
|
||||||
for mob in mobs:
|
for sm in mobs:
|
||||||
|
for mob in self.mobjects:
|
||||||
|
if sm in mob.get_family():
|
||||||
mob.set_animating_status(True)
|
mob.set_animating_status(True)
|
||||||
self.refresh_static_mobjects()
|
self.refresh_static_mobjects()
|
||||||
|
|
||||||
|
|
@ -307,10 +317,13 @@ class InteractiveScene(Scene):
|
||||||
|
|
||||||
def gather_new_selection(self):
|
def gather_new_selection(self):
|
||||||
self.is_selecting = False
|
self.is_selecting = False
|
||||||
|
if self.selection_rectangle in self.mobjects:
|
||||||
self.remove(self.selection_rectangle)
|
self.remove(self.selection_rectangle)
|
||||||
|
additions = []
|
||||||
for mob in reversed(self.get_selection_search_set()):
|
for mob in reversed(self.get_selection_search_set()):
|
||||||
if self.selection_rectangle.is_touching(mob):
|
if self.selection_rectangle.is_touching(mob):
|
||||||
self.add_to_selection(mob)
|
additions.append(mob)
|
||||||
|
self.add_to_selection(*additions)
|
||||||
|
|
||||||
def prepare_grab(self):
|
def prepare_grab(self):
|
||||||
mp = self.mouse_point.get_center()
|
mp = self.mouse_point.get_center()
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||||
from manimlib.scene.scene_file_writer import SceneFileWriter
|
from manimlib.scene.scene_file_writer import SceneFileWriter
|
||||||
from manimlib.utils.config_ops import digest_config
|
from manimlib.utils.config_ops import digest_config
|
||||||
from manimlib.utils.family_ops import extract_mobject_family_members
|
from manimlib.utils.family_ops import extract_mobject_family_members
|
||||||
from manimlib.utils.family_ops import restructure_list_to_exclude_certain_family_members
|
from manimlib.utils.iterables import list_difference_update
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
|
@ -63,7 +63,7 @@ class Scene(object):
|
||||||
"presenter_mode": False,
|
"presenter_mode": False,
|
||||||
"linger_after_completion": True,
|
"linger_after_completion": True,
|
||||||
"pan_sensitivity": 3,
|
"pan_sensitivity": 3,
|
||||||
"max_num_saved_states": 20,
|
"max_num_saved_states": 50,
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
|
|
@ -178,10 +178,7 @@ class Scene(object):
|
||||||
# Enables gui interactions during the embed
|
# Enables gui interactions during the embed
|
||||||
def inputhook(context):
|
def inputhook(context):
|
||||||
while not context.input_is_ready():
|
while not context.input_is_ready():
|
||||||
if self.window.is_closing:
|
if not self.window.is_closing:
|
||||||
pass
|
|
||||||
# self.window.destroy()
|
|
||||||
else:
|
|
||||||
self.update_frame(dt=0)
|
self.update_frame(dt=0)
|
||||||
|
|
||||||
pt_inputhooks.register("manim", inputhook)
|
pt_inputhooks.register("manim", inputhook)
|
||||||
|
|
@ -190,6 +187,7 @@ class Scene(object):
|
||||||
# Operation to run after each ipython command
|
# Operation to run after each ipython command
|
||||||
def post_cell_func(*args, **kwargs):
|
def post_cell_func(*args, **kwargs):
|
||||||
self.refresh_static_mobjects()
|
self.refresh_static_mobjects()
|
||||||
|
self.save_state()
|
||||||
|
|
||||||
shell.events.register("post_run_cell", post_cell_func)
|
shell.events.register("post_run_cell", post_cell_func)
|
||||||
|
|
||||||
|
|
@ -304,10 +302,31 @@ class Scene(object):
|
||||||
))
|
))
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def remove(self, *mobjects_to_remove: Mobject):
|
def replace(self, mobject: Mobject, *replacements: Mobject):
|
||||||
self.mobjects = restructure_list_to_exclude_certain_family_members(
|
if mobject in self.mobjects:
|
||||||
self.mobjects, mobjects_to_remove
|
index = self.mobjects.index(mobject)
|
||||||
)
|
self.mobjects = [
|
||||||
|
*self.mobjects[:index],
|
||||||
|
*replacements,
|
||||||
|
*self.mobjects[index + 1:]
|
||||||
|
]
|
||||||
|
return self
|
||||||
|
|
||||||
|
def remove(self, *mobjects: Mobject):
|
||||||
|
"""
|
||||||
|
Removes anything in mobjects from scenes mobject list, but in the event that one
|
||||||
|
of the items to be removed is a member of the family of an item in mobject_list,
|
||||||
|
the other family members are added back into the list.
|
||||||
|
|
||||||
|
For example, if the scene includes Group(m1, m2, m3), and we call scene.remove(m1),
|
||||||
|
the desired behavior is for the scene to then include m2 and m3 (ungrouped).
|
||||||
|
"""
|
||||||
|
for mob in mobjects:
|
||||||
|
# First restructure self.mobjects so that parents/grandparents/etc. are replaced
|
||||||
|
# with their children, likewise for all ancestors in the extended family.
|
||||||
|
for ancestor in mob.get_ancestors(extended=True):
|
||||||
|
self.replace(ancestor, *ancestor.submobjects)
|
||||||
|
self.mobjects = list_difference_update(self.mobjects, mob.get_family())
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def bring_to_front(self, *mobjects: Mobject):
|
def bring_to_front(self, *mobjects: Mobject):
|
||||||
|
|
@ -510,6 +529,7 @@ class Scene(object):
|
||||||
def wrapper(self, *args, **kwargs):
|
def wrapper(self, *args, **kwargs):
|
||||||
if self.inside_embed:
|
if self.inside_embed:
|
||||||
self.save_state()
|
self.save_state()
|
||||||
|
|
||||||
self.update_skipping_status()
|
self.update_skipping_status()
|
||||||
should_write = not self.skip_animations
|
should_write = not self.skip_animations
|
||||||
if should_write:
|
if should_write:
|
||||||
|
|
@ -525,6 +545,9 @@ class Scene(object):
|
||||||
if should_write:
|
if should_write:
|
||||||
self.file_writer.end_animation()
|
self.file_writer.end_animation()
|
||||||
|
|
||||||
|
if self.inside_embed:
|
||||||
|
self.save_state()
|
||||||
|
|
||||||
self.num_plays += 1
|
self.num_plays += 1
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
|
@ -581,10 +604,10 @@ class Scene(object):
|
||||||
note: str = None,
|
note: str = None,
|
||||||
ignore_presenter_mode: bool = False
|
ignore_presenter_mode: bool = False
|
||||||
):
|
):
|
||||||
if note:
|
|
||||||
log.info(note)
|
|
||||||
self.update_mobjects(dt=0) # Any problems with this?
|
self.update_mobjects(dt=0) # Any problems with this?
|
||||||
if self.presenter_mode and not self.skip_animations and not ignore_presenter_mode:
|
if self.presenter_mode and not self.skip_animations and not ignore_presenter_mode:
|
||||||
|
if note:
|
||||||
|
log.info(note)
|
||||||
while self.hold_on_wait:
|
while self.hold_on_wait:
|
||||||
self.update_frame(dt=1 / self.camera.frame_rate)
|
self.update_frame(dt=1 / self.camera.frame_rate)
|
||||||
self.hold_on_wait = True
|
self.hold_on_wait = True
|
||||||
|
|
@ -632,8 +655,22 @@ class Scene(object):
|
||||||
|
|
||||||
# Helpers for interactive development
|
# Helpers for interactive development
|
||||||
|
|
||||||
def get_state(self) -> list[tuple[Mobject, Mobject]]:
|
def get_state(self) -> tuple[list[tuple[Mobject, Mobject]], int]:
|
||||||
return [(mob, mob.copy()) for mob in self.mobjects]
|
if self.undo_stack:
|
||||||
|
last_state = dict(self.undo_stack[-1])
|
||||||
|
else:
|
||||||
|
last_state = {}
|
||||||
|
result = []
|
||||||
|
n_changes = 0
|
||||||
|
for mob in self.mobjects:
|
||||||
|
# If it hasn't changed since the last state, just point to the
|
||||||
|
# same copy as before
|
||||||
|
if mob in last_state and last_state[mob].looks_identical(mob):
|
||||||
|
result.append((mob, last_state[mob]))
|
||||||
|
else:
|
||||||
|
result.append((mob, mob.copy()))
|
||||||
|
n_changes += 1
|
||||||
|
return result, n_changes
|
||||||
|
|
||||||
def restore_state(self, mobject_states: list[tuple[Mobject, Mobject]]):
|
def restore_state(self, mobject_states: list[tuple[Mobject, Mobject]]):
|
||||||
self.mobjects = [mob.become(mob_copy) for mob, mob_copy in mobject_states]
|
self.mobjects = [mob.become(mob_copy) for mob, mob_copy in mobject_states]
|
||||||
|
|
@ -642,19 +679,23 @@ class Scene(object):
|
||||||
if not self.preview:
|
if not self.preview:
|
||||||
return
|
return
|
||||||
self.redo_stack = []
|
self.redo_stack = []
|
||||||
self.undo_stack.append(self.get_state())
|
state, n_changes = self.get_state()
|
||||||
|
if n_changes > 0:
|
||||||
|
self.undo_stack.append(state)
|
||||||
if len(self.undo_stack) > self.max_num_saved_states:
|
if len(self.undo_stack) > self.max_num_saved_states:
|
||||||
self.undo_stack.pop(0)
|
self.undo_stack.pop(0)
|
||||||
|
|
||||||
def undo(self):
|
def undo(self):
|
||||||
if self.undo_stack:
|
if self.undo_stack:
|
||||||
self.redo_stack.append(self.get_state())
|
state, n_changes = self.get_state()
|
||||||
|
self.redo_stack.append(state)
|
||||||
self.restore_state(self.undo_stack.pop())
|
self.restore_state(self.undo_stack.pop())
|
||||||
self.refresh_static_mobjects()
|
self.refresh_static_mobjects()
|
||||||
|
|
||||||
def redo(self):
|
def redo(self):
|
||||||
if self.redo_stack:
|
if self.redo_stack:
|
||||||
self.undo_stack.append(self.get_state())
|
state, n_changes = self.get_state()
|
||||||
|
self.undo_stack.append(state)
|
||||||
self.restore_state(self.redo_stack.pop())
|
self.restore_state(self.redo_stack.pop())
|
||||||
self.refresh_static_mobjects()
|
self.refresh_static_mobjects()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -45,13 +45,30 @@ class ShaderWrapper(object):
|
||||||
self.init_program_code()
|
self.init_program_code()
|
||||||
self.refresh_id()
|
self.refresh_id()
|
||||||
|
|
||||||
|
def __eq__(self, shader_wrapper: ShaderWrapper):
|
||||||
|
return all((
|
||||||
|
np.all(self.vert_data == shader_wrapper.vert_data),
|
||||||
|
np.all(self.vert_indices == shader_wrapper.vert_indices),
|
||||||
|
self.shader_folder == shader_wrapper.shader_folder,
|
||||||
|
all(
|
||||||
|
np.all(self.uniforms[key] == shader_wrapper.uniforms[key])
|
||||||
|
for key in self.uniforms
|
||||||
|
),
|
||||||
|
all(
|
||||||
|
self.texture_paths[key] == shader_wrapper.texture_paths[key]
|
||||||
|
for key in self.texture_paths
|
||||||
|
),
|
||||||
|
self.depth_test == shader_wrapper.depth_test,
|
||||||
|
self.render_primitive == shader_wrapper.render_primitive,
|
||||||
|
))
|
||||||
|
|
||||||
def copy(self):
|
def copy(self):
|
||||||
result = copy.copy(self)
|
result = copy.copy(self)
|
||||||
result.vert_data = np.array(self.vert_data)
|
result.vert_data = np.array(self.vert_data)
|
||||||
if result.vert_indices is not None:
|
if result.vert_indices is not None:
|
||||||
result.vert_indices = np.array(self.vert_indices)
|
result.vert_indices = np.array(self.vert_indices)
|
||||||
if self.uniforms:
|
if self.uniforms:
|
||||||
result.uniforms = dict(self.uniforms)
|
result.uniforms = {key: np.array(value) for key, value in self.uniforms.items()}
|
||||||
if self.texture_paths:
|
if self.texture_paths:
|
||||||
result.texture_paths = dict(self.texture_paths)
|
result.texture_paths = dict(self.texture_paths)
|
||||||
return result
|
return result
|
||||||
|
|
|
||||||
|
|
@ -18,32 +18,3 @@ def extract_mobject_family_members(
|
||||||
for sm in mob.get_family()
|
for sm in mob.get_family()
|
||||||
if (not exclude_pointless) or sm.has_points()
|
if (not exclude_pointless) or sm.has_points()
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def restructure_list_to_exclude_certain_family_members(
|
|
||||||
mobject_list: list[Mobject],
|
|
||||||
to_remove: list[Mobject]
|
|
||||||
) -> list[Mobject]:
|
|
||||||
"""
|
|
||||||
Removes anything in to_remove from mobject_list, but in the event that one of
|
|
||||||
the items to be removed is a member of the family of an item in mobject_list,
|
|
||||||
the other family members are added back into the list.
|
|
||||||
|
|
||||||
This is useful in cases where a scene contains a group, e.g. Group(m1, m2, m3),
|
|
||||||
but one of its submobjects is removed, e.g. scene.remove(m1), it's useful
|
|
||||||
for the list of mobject_list to be edited to contain other submobjects, but not m1.
|
|
||||||
"""
|
|
||||||
new_list = []
|
|
||||||
to_remove = extract_mobject_family_members(to_remove)
|
|
||||||
|
|
||||||
def add_safe_mobjects_from_list(list_to_examine, set_to_remove):
|
|
||||||
for mob in list_to_examine:
|
|
||||||
if mob in set_to_remove:
|
|
||||||
continue
|
|
||||||
intersect = set_to_remove.intersection(mob.get_family())
|
|
||||||
if intersect:
|
|
||||||
add_safe_mobjects_from_list(mob.submobjects, intersect)
|
|
||||||
else:
|
|
||||||
new_list.append(mob)
|
|
||||||
add_safe_mobjects_from_list(mobject_list, set(to_remove))
|
|
||||||
return new_list
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue