mirror of
https://github.com/3b1b/manim.git
synced 2025-11-14 11:37:45 +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])
|
||||
new_lines = list(lines)
|
||||
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)
|
||||
try:
|
||||
yield file_name
|
||||
yield alt_file
|
||||
finally:
|
||||
with open(file_name, 'w') as fp:
|
||||
fp.writelines(lines)
|
||||
os.remove(alt_file)
|
||||
|
||||
|
||||
def get_custom_config():
|
||||
|
|
|
|||
|
|
@ -895,7 +895,15 @@ class Polygon(VMobject):
|
|||
def get_vertices(self) -> list[np.ndarray]:
|
||||
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()
|
||||
arcs = []
|
||||
for v1, v2, v3 in adjacent_n_tuples(vertices, 3):
|
||||
|
|
|
|||
|
|
@ -343,6 +343,26 @@ class Mobject(object):
|
|||
def family_members_with_points(self):
|
||||
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):
|
||||
if self in mobjects:
|
||||
raise Exception("Mobject cannot contain self")
|
||||
|
|
@ -354,13 +374,14 @@ class Mobject(object):
|
|||
self.assemble_family()
|
||||
return self
|
||||
|
||||
def remove(self, *mobjects: Mobject):
|
||||
def remove(self, *mobjects: Mobject, reassemble: bool = True):
|
||||
for mobject in mobjects:
|
||||
if mobject in self.submobjects:
|
||||
self.submobjects.remove(mobject)
|
||||
if self in mobject.parents:
|
||||
mobject.parents.remove(self)
|
||||
self.assemble_family()
|
||||
if reassemble:
|
||||
self.assemble_family()
|
||||
return self
|
||||
|
||||
def add_to_back(self, *mobjects: Mobject):
|
||||
|
|
@ -381,7 +402,7 @@ class Mobject(object):
|
|||
return self
|
||||
|
||||
def set_submobjects(self, submobject_list: list[Mobject]):
|
||||
self.remove(*self.submobjects)
|
||||
self.remove(*self.submobjects, reassemble=False)
|
||||
self.add(*submobject_list)
|
||||
return self
|
||||
|
||||
|
|
@ -526,17 +547,24 @@ class Mobject(object):
|
|||
for key, value in self.uniforms.items()
|
||||
}
|
||||
|
||||
result.submobjects = []
|
||||
result.add(*(sm.copy() for sm in self.submobjects))
|
||||
result.match_updaters(self)
|
||||
# Instead of adding using result.add, which does some checks for updating
|
||||
# updater statues and bounding box, just directly modify the family-related
|
||||
# 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()
|
||||
for attr, value in list(self.__dict__.items()):
|
||||
if isinstance(value, Mobject) and value is not self:
|
||||
if value in family:
|
||||
setattr(result, attr, result.family[self.family.index(value)])
|
||||
else:
|
||||
setattr(result, attr, value.copy())
|
||||
if isinstance(value, np.ndarray):
|
||||
setattr(result, attr, value.copy())
|
||||
if isinstance(value, ShaderWrapper):
|
||||
|
|
@ -590,6 +618,23 @@ class Mobject(object):
|
|||
self.refresh_bounding_box(recurse_down=True)
|
||||
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
|
||||
|
||||
def replicate(self, n: int) -> Group:
|
||||
|
|
|
|||
|
|
@ -206,10 +206,18 @@ class InteractiveScene(Scene):
|
|||
return self.get_corner_dots(mobject)
|
||||
|
||||
def refresh_selection_highlight(self):
|
||||
self.selection_highlight.set_submobjects([
|
||||
self.get_highlight(mob)
|
||||
for mob in self.selection
|
||||
])
|
||||
if len(self.selection) > 0:
|
||||
self.remove(self.selection_highlight)
|
||||
self.selection_highlight.set_submobjects([
|
||||
self.get_highlight(mob)
|
||||
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):
|
||||
mobs = list(filter(
|
||||
|
|
@ -219,9 +227,11 @@ class InteractiveScene(Scene):
|
|||
if len(mobs) == 0:
|
||||
return
|
||||
self.selection.add(*mobs)
|
||||
self.selection_highlight.add(*map(self.get_highlight, mobs))
|
||||
for mob in mobs:
|
||||
mob.set_animating_status(True)
|
||||
self.refresh_selection_highlight()
|
||||
for sm in mobs:
|
||||
for mob in self.mobjects:
|
||||
if sm in mob.get_family():
|
||||
mob.set_animating_status(True)
|
||||
self.refresh_static_mobjects()
|
||||
|
||||
def toggle_from_selection(self, *mobjects):
|
||||
|
|
@ -307,10 +317,13 @@ class InteractiveScene(Scene):
|
|||
|
||||
def gather_new_selection(self):
|
||||
self.is_selecting = False
|
||||
self.remove(self.selection_rectangle)
|
||||
for mob in reversed(self.get_selection_search_set()):
|
||||
if self.selection_rectangle.is_touching(mob):
|
||||
self.add_to_selection(mob)
|
||||
if self.selection_rectangle in self.mobjects:
|
||||
self.remove(self.selection_rectangle)
|
||||
additions = []
|
||||
for mob in reversed(self.get_selection_search_set()):
|
||||
if self.selection_rectangle.is_touching(mob):
|
||||
additions.append(mob)
|
||||
self.add_to_selection(*additions)
|
||||
|
||||
def prepare_grab(self):
|
||||
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.utils.config_ops import digest_config
|
||||
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
|
||||
|
||||
|
|
@ -63,7 +63,7 @@ class Scene(object):
|
|||
"presenter_mode": False,
|
||||
"linger_after_completion": True,
|
||||
"pan_sensitivity": 3,
|
||||
"max_num_saved_states": 20,
|
||||
"max_num_saved_states": 50,
|
||||
}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
|
|
@ -178,10 +178,7 @@ class Scene(object):
|
|||
# Enables gui interactions during the embed
|
||||
def inputhook(context):
|
||||
while not context.input_is_ready():
|
||||
if self.window.is_closing:
|
||||
pass
|
||||
# self.window.destroy()
|
||||
else:
|
||||
if not self.window.is_closing:
|
||||
self.update_frame(dt=0)
|
||||
|
||||
pt_inputhooks.register("manim", inputhook)
|
||||
|
|
@ -190,6 +187,7 @@ class Scene(object):
|
|||
# Operation to run after each ipython command
|
||||
def post_cell_func(*args, **kwargs):
|
||||
self.refresh_static_mobjects()
|
||||
self.save_state()
|
||||
|
||||
shell.events.register("post_run_cell", post_cell_func)
|
||||
|
||||
|
|
@ -304,10 +302,31 @@ class Scene(object):
|
|||
))
|
||||
return self
|
||||
|
||||
def remove(self, *mobjects_to_remove: Mobject):
|
||||
self.mobjects = restructure_list_to_exclude_certain_family_members(
|
||||
self.mobjects, mobjects_to_remove
|
||||
)
|
||||
def replace(self, mobject: Mobject, *replacements: Mobject):
|
||||
if mobject in self.mobjects:
|
||||
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
|
||||
|
||||
def bring_to_front(self, *mobjects: Mobject):
|
||||
|
|
@ -510,6 +529,7 @@ class Scene(object):
|
|||
def wrapper(self, *args, **kwargs):
|
||||
if self.inside_embed:
|
||||
self.save_state()
|
||||
|
||||
self.update_skipping_status()
|
||||
should_write = not self.skip_animations
|
||||
if should_write:
|
||||
|
|
@ -525,6 +545,9 @@ class Scene(object):
|
|||
if should_write:
|
||||
self.file_writer.end_animation()
|
||||
|
||||
if self.inside_embed:
|
||||
self.save_state()
|
||||
|
||||
self.num_plays += 1
|
||||
return wrapper
|
||||
|
||||
|
|
@ -581,10 +604,10 @@ class Scene(object):
|
|||
note: str = None,
|
||||
ignore_presenter_mode: bool = False
|
||||
):
|
||||
if note:
|
||||
log.info(note)
|
||||
self.update_mobjects(dt=0) # Any problems with this?
|
||||
if self.presenter_mode and not self.skip_animations and not ignore_presenter_mode:
|
||||
if note:
|
||||
log.info(note)
|
||||
while self.hold_on_wait:
|
||||
self.update_frame(dt=1 / self.camera.frame_rate)
|
||||
self.hold_on_wait = True
|
||||
|
|
@ -632,8 +655,22 @@ class Scene(object):
|
|||
|
||||
# Helpers for interactive development
|
||||
|
||||
def get_state(self) -> list[tuple[Mobject, Mobject]]:
|
||||
return [(mob, mob.copy()) for mob in self.mobjects]
|
||||
def get_state(self) -> tuple[list[tuple[Mobject, Mobject]], int]:
|
||||
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]]):
|
||||
self.mobjects = [mob.become(mob_copy) for mob, mob_copy in mobject_states]
|
||||
|
|
@ -642,19 +679,23 @@ class Scene(object):
|
|||
if not self.preview:
|
||||
return
|
||||
self.redo_stack = []
|
||||
self.undo_stack.append(self.get_state())
|
||||
if len(self.undo_stack) > self.max_num_saved_states:
|
||||
self.undo_stack.pop(0)
|
||||
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:
|
||||
self.undo_stack.pop(0)
|
||||
|
||||
def undo(self):
|
||||
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.refresh_static_mobjects()
|
||||
|
||||
def redo(self):
|
||||
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.refresh_static_mobjects()
|
||||
|
||||
|
|
|
|||
|
|
@ -45,13 +45,30 @@ class ShaderWrapper(object):
|
|||
self.init_program_code()
|
||||
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):
|
||||
result = copy.copy(self)
|
||||
result.vert_data = np.array(self.vert_data)
|
||||
if result.vert_indices is not None:
|
||||
result.vert_indices = np.array(self.vert_indices)
|
||||
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:
|
||||
result.texture_paths = dict(self.texture_paths)
|
||||
return result
|
||||
|
|
|
|||
|
|
@ -18,32 +18,3 @@ def extract_mobject_family_members(
|
|||
for sm in mob.get_family()
|
||||
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