mirror of
https://github.com/3b1b/manim.git
synced 2025-09-01 00:48:45 +00:00
Merge pull request #1796 from 3b1b/video-work
Improved embed, fixes to Mobject.copy
This commit is contained in:
commit
753ef3b74a
8 changed files with 588 additions and 310 deletions
|
@ -117,16 +117,19 @@ def parse_cli():
|
|||
)
|
||||
parser.add_argument(
|
||||
"-n", "--start_at_animation_number",
|
||||
help="Start rendering not from the first animation, but"
|
||||
"from another, specified by its index. If you pass"
|
||||
"in two comma separated values, e.g. \"3,6\", it will end"
|
||||
help="Start rendering not from the first animation, but "
|
||||
"from another, specified by its index. If you pass "
|
||||
"in two comma separated values, e.g. \"3,6\", it will end "
|
||||
"the rendering at the second value",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-e", "--embed", metavar="LINENO",
|
||||
help="Takes a line number as an argument, and results"
|
||||
"in the scene being called as if the line `self.embed()`"
|
||||
"was inserted into the scene code at that line number."
|
||||
"-e", "--embed",
|
||||
nargs="?",
|
||||
const="",
|
||||
help="Creates a new file where the line `self.embed` is inserted "
|
||||
"into the Scenes construct method. "
|
||||
"If a string is passed in, the line will be inserted below the "
|
||||
"last line of code including that string."
|
||||
)
|
||||
parser.add_argument(
|
||||
"-r", "--resolution",
|
||||
|
@ -185,14 +188,62 @@ def get_module(file_name):
|
|||
return module
|
||||
|
||||
|
||||
def get_indent(line: str):
|
||||
return len(line) - len(line.lstrip())
|
||||
|
||||
|
||||
@contextmanager
|
||||
def insert_embed_line(file_name, lineno):
|
||||
def insert_embed_line(file_name: str, scene_name: str, line_marker: str):
|
||||
"""
|
||||
This is hacky, but convenient. When user includes the argument "-e", it will try
|
||||
to recreate a file that inserts the line `self.embed()` into the end of the scene's
|
||||
construct method. If there is an argument passed in, it will insert the line after
|
||||
the last line in the sourcefile which includes that string.
|
||||
"""
|
||||
with open(file_name, 'r') as fp:
|
||||
lines = fp.readlines()
|
||||
line = lines[lineno - 1]
|
||||
n_spaces = len(line) - len(line.lstrip())
|
||||
lines.insert(lineno, " " * n_spaces + "self.embed()\n")
|
||||
|
||||
try:
|
||||
scene_line_number = next(
|
||||
i for i, line in enumerate(lines)
|
||||
if line.startswith(f"class {scene_name}")
|
||||
)
|
||||
except StopIteration:
|
||||
log.error(f"No scene {scene_name}")
|
||||
|
||||
prev_line_num = None
|
||||
n_spaces = None
|
||||
if len(line_marker) == 0:
|
||||
# Find the end of the construct method
|
||||
in_construct = False
|
||||
for index in range(scene_line_number, len(lines) - 1):
|
||||
line = lines[index]
|
||||
if line.lstrip().startswith("def construct"):
|
||||
in_construct = True
|
||||
n_spaces = get_indent(line) + 4
|
||||
elif in_construct:
|
||||
if len(line.strip()) > 0 and get_indent(line) < n_spaces:
|
||||
prev_line_num = index - 2
|
||||
break
|
||||
elif line_marker.isdigit():
|
||||
# Treat the argument as a line number
|
||||
prev_line_num = int(line_marker) - 1
|
||||
elif len(line_marker) > 0:
|
||||
# Treat the argument as a string
|
||||
try:
|
||||
prev_line_num = next(
|
||||
i
|
||||
for i in range(len(lines) - 1, scene_line_number, -1)
|
||||
if line_marker in lines[i]
|
||||
)
|
||||
except StopIteration:
|
||||
log.error(f"No lines matching {line_marker}")
|
||||
sys.exit(2)
|
||||
|
||||
# Insert and write new file
|
||||
if n_spaces is None:
|
||||
n_spaces = get_indent(lines[prev_line_num])
|
||||
lines.insert(prev_line_num + 1, " " * n_spaces + "self.embed()\n")
|
||||
alt_file = file_name.replace(".py", "_inserted_embed.py")
|
||||
with open(alt_file, 'w') as fp:
|
||||
fp.writelines(lines)
|
||||
|
@ -296,10 +347,10 @@ def get_configuration(args):
|
|||
"quiet": args.quiet,
|
||||
}
|
||||
|
||||
if args.embed is None:
|
||||
module = get_module(args.file)
|
||||
else:
|
||||
with insert_embed_line(args.file, int(args.embed)) as alt_file:
|
||||
module = get_module(args.file)
|
||||
|
||||
if args.embed is not None:
|
||||
with insert_embed_line(args.file, args.scene_names[0], args.embed) as alt_file:
|
||||
module = get_module(alt_file)
|
||||
|
||||
config = {
|
||||
|
|
|
@ -75,6 +75,7 @@ DEFAULT_STROKE_WIDTH = 4
|
|||
# For keyboard interactions
|
||||
CTRL_SYMBOL = 65508
|
||||
SHIFT_SYMBOL = 65505
|
||||
COMMAND_SYMBOL = 65517
|
||||
DELETE_SYMBOL = 65288
|
||||
ARROW_SYMBOLS = list(range(65361, 65365))
|
||||
|
||||
|
|
|
@ -4,10 +4,11 @@ import sys
|
|||
|
||||
from manimlib.config import get_custom_config
|
||||
from manimlib.logger import log
|
||||
from manimlib.scene.interactive_scene import InteractiveScene
|
||||
from manimlib.scene.scene import Scene
|
||||
|
||||
|
||||
class BlankScene(Scene):
|
||||
class BlankScene(InteractiveScene):
|
||||
def construct(self):
|
||||
exec(get_custom_config()["universal_import_line"])
|
||||
self.embed()
|
||||
|
|
|
@ -33,7 +33,6 @@ from manimlib.utils.config_ops import digest_config
|
|||
from manimlib.utils.iterables import batch_by_property
|
||||
from manimlib.utils.iterables import list_update
|
||||
from manimlib.utils.iterables import listify
|
||||
from manimlib.utils.iterables import make_even
|
||||
from manimlib.utils.iterables import resize_array
|
||||
from manimlib.utils.iterables import resize_preserving_order
|
||||
from manimlib.utils.iterables import resize_with_interpolation
|
||||
|
@ -96,7 +95,8 @@ class Mobject(object):
|
|||
self.locked_data_keys: set[str] = set()
|
||||
self.needs_new_bounding_box: bool = True
|
||||
self._is_animating: bool = False
|
||||
self._is_movable: bool = False
|
||||
self.saved_state = None
|
||||
self.target = None
|
||||
|
||||
self.init_data()
|
||||
self.init_uniforms()
|
||||
|
@ -148,8 +148,10 @@ class Mobject(object):
|
|||
return self
|
||||
|
||||
def set_uniforms(self, uniforms: dict):
|
||||
for key in uniforms:
|
||||
self.uniforms[key] = uniforms[key] # Copy?
|
||||
for key, value in uniforms.items():
|
||||
if isinstance(value, np.ndarray):
|
||||
value = value.copy()
|
||||
self.uniforms[key] = value
|
||||
return self
|
||||
|
||||
@property
|
||||
|
@ -472,66 +474,91 @@ class Mobject(object):
|
|||
self.assemble_family()
|
||||
return self
|
||||
|
||||
# Creating new Mobjects from this one
|
||||
# Copying and serialization
|
||||
|
||||
def replicate(self, n: int) -> Group:
|
||||
return self.get_group_class()(
|
||||
*(self.copy() for x in range(n))
|
||||
)
|
||||
def stash_mobject_pointers(func):
|
||||
@wraps(func)
|
||||
def wrapper(self, *args, **kwargs):
|
||||
uncopied_attrs = ["parents", "target", "saved_state"]
|
||||
stash = dict()
|
||||
for attr in uncopied_attrs:
|
||||
if hasattr(self, attr):
|
||||
value = getattr(self, attr)
|
||||
stash[attr] = value
|
||||
null_value = [] if isinstance(value, list) else None
|
||||
setattr(self, attr, null_value)
|
||||
result = func(self, *args, **kwargs)
|
||||
self.__dict__.update(stash)
|
||||
return result
|
||||
return wrapper
|
||||
|
||||
def get_grid(self, n_rows: int, n_cols: int, height: float | None = None, **kwargs):
|
||||
"""
|
||||
Returns a new mobject containing multiple copies of this one
|
||||
arranged in a grid
|
||||
"""
|
||||
grid = self.replicate(n_rows * n_cols)
|
||||
grid.arrange_in_grid(n_rows, n_cols, **kwargs)
|
||||
if height is not None:
|
||||
grid.set_height(height)
|
||||
return grid
|
||||
@stash_mobject_pointers
|
||||
def serialize(self):
|
||||
return pickle.dumps(self)
|
||||
|
||||
# Copying
|
||||
def deserialize(self, data: bytes):
|
||||
self.become(pickle.loads(data))
|
||||
return self
|
||||
|
||||
def copy(self):
|
||||
self.parents = []
|
||||
try:
|
||||
return pickle.loads(pickle.dumps(self))
|
||||
except AttributeError:
|
||||
return copy.deepcopy(self)
|
||||
@stash_mobject_pointers
|
||||
def copy(self, deep: bool = False):
|
||||
if deep:
|
||||
try:
|
||||
# Often faster than deepcopy
|
||||
return pickle.loads(pickle.dumps(self))
|
||||
except AttributeError:
|
||||
return copy.deepcopy(self)
|
||||
|
||||
result = copy.copy(self)
|
||||
|
||||
# The line above is only a shallow copy, so the internal
|
||||
# data which are numpyu arrays or other mobjects still
|
||||
# need to be further copied.
|
||||
result.data = dict(self.data)
|
||||
for key in result.data:
|
||||
result.data[key] = result.data[key].copy()
|
||||
|
||||
result.uniforms = dict(self.uniforms)
|
||||
for key in result.uniforms:
|
||||
if isinstance(result.uniforms[key], np.ndarray):
|
||||
result.uniforms[key] = result.uniforms[key].copy()
|
||||
|
||||
result.submobjects = []
|
||||
result.add(*(sm.copy() for sm in self.submobjects))
|
||||
result.match_updaters(self)
|
||||
|
||||
family = self.get_family()
|
||||
for attr, value in list(self.__dict__.items()):
|
||||
if isinstance(value, Mobject) and value in family and value is not self:
|
||||
setattr(result, attr, result.family[self.family.index(value)])
|
||||
if isinstance(value, np.ndarray):
|
||||
setattr(result, attr, value.copy())
|
||||
if isinstance(value, ShaderWrapper):
|
||||
setattr(result, attr, value.copy())
|
||||
return result
|
||||
|
||||
def deepcopy(self):
|
||||
# This used to be different from copy, so is now just here for backward compatibility
|
||||
return self.copy()
|
||||
return self.copy(deep=True)
|
||||
|
||||
def generate_target(self, use_deepcopy: bool = False):
|
||||
# TODO, remove now pointless use_deepcopy arg
|
||||
self.target = None # Prevent exponential explosion
|
||||
self.target = self.copy()
|
||||
self.target = self.copy(deep=use_deepcopy)
|
||||
self.target.saved_state = self.saved_state
|
||||
return self.target
|
||||
|
||||
def save_state(self, use_deepcopy: bool = False):
|
||||
# TODO, remove now pointless use_deepcopy arg
|
||||
if hasattr(self, "saved_state"):
|
||||
# Prevent exponential growth of data
|
||||
self.saved_state = None
|
||||
self.saved_state = self.copy()
|
||||
self.saved_state = self.copy(deep=use_deepcopy)
|
||||
self.saved_state.target = self.target
|
||||
return self
|
||||
|
||||
def restore(self):
|
||||
if not hasattr(self, "saved_state") or self.save_state is None:
|
||||
if not hasattr(self, "saved_state") or self.saved_state is None:
|
||||
raise Exception("Trying to restore without having saved")
|
||||
self.become(self.saved_state)
|
||||
return self
|
||||
|
||||
def save_to_file(self, file_path):
|
||||
if not file_path.endswith(".mob"):
|
||||
file_path += ".mob"
|
||||
if os.path.exists(file_path):
|
||||
cont = input(f"{file_path} already exists. Overwrite (y/n)? ")
|
||||
if cont != "y":
|
||||
return
|
||||
def save_to_file(self, file_path: str, supress_overwrite_warning: bool = False):
|
||||
with open(file_path, "wb") as fp:
|
||||
pickle.dump(self, fp)
|
||||
fp.write(self.serialize())
|
||||
log.info(f"Saved mobject to {file_path}")
|
||||
return self
|
||||
|
||||
|
@ -544,6 +571,39 @@ class Mobject(object):
|
|||
mobject = pickle.load(fp)
|
||||
return mobject
|
||||
|
||||
def become(self, mobject: Mobject):
|
||||
"""
|
||||
Edit all data and submobjects to be idential
|
||||
to another mobject
|
||||
"""
|
||||
self.align_family(mobject)
|
||||
for sm1, sm2 in zip(self.get_family(), mobject.get_family()):
|
||||
sm1.set_data(sm2.data)
|
||||
sm1.set_uniforms(sm2.uniforms)
|
||||
sm1.shader_folder = sm2.shader_folder
|
||||
sm1.texture_paths = sm2.texture_paths
|
||||
sm1.depth_test = sm2.depth_test
|
||||
sm1.render_primitive = sm2.render_primitive
|
||||
self.refresh_bounding_box(recurse_down=True)
|
||||
return self
|
||||
|
||||
# Creating new Mobjects from this one
|
||||
|
||||
def replicate(self, n: int) -> Group:
|
||||
group_class = self.get_group_class()
|
||||
return group_class(*(self.copy() for _ in range(n)))
|
||||
|
||||
def get_grid(self, n_rows: int, n_cols: int, height: float | None = None, **kwargs) -> Group:
|
||||
"""
|
||||
Returns a new mobject containing multiple copies of this one
|
||||
arranged in a grid
|
||||
"""
|
||||
grid = self.replicate(n_rows * n_cols)
|
||||
grid.arrange_in_grid(n_rows, n_cols, **kwargs)
|
||||
if height is not None:
|
||||
grid.set_height(height)
|
||||
return grid
|
||||
|
||||
# Updating
|
||||
|
||||
def init_updaters(self):
|
||||
|
@ -646,21 +706,13 @@ class Mobject(object):
|
|||
# Check if mark as static or not for camera
|
||||
|
||||
def is_changing(self) -> bool:
|
||||
return self._is_animating or self.has_updaters or self._is_movable
|
||||
return self._is_animating or self.has_updaters
|
||||
|
||||
def set_animating_status(self, is_animating: bool, recurse: bool = True) -> None:
|
||||
for mob in self.get_family(recurse):
|
||||
mob._is_animating = is_animating
|
||||
return self
|
||||
|
||||
def make_movable(self, value: bool = True, recurse: bool = True) -> None:
|
||||
for mob in self.get_family(recurse):
|
||||
mob._is_movable = value
|
||||
return self
|
||||
|
||||
def is_movable(self) -> bool:
|
||||
return self._is_movable
|
||||
|
||||
# Transforming operations
|
||||
|
||||
def shift(self, vector: np.ndarray):
|
||||
|
@ -1540,18 +1592,6 @@ class Mobject(object):
|
|||
"""
|
||||
pass # To implement in subclass
|
||||
|
||||
def become(self, mobject: Mobject):
|
||||
"""
|
||||
Edit all data and submobjects to be idential
|
||||
to another mobject
|
||||
"""
|
||||
self.align_family(mobject)
|
||||
for sm1, sm2 in zip(self.get_family(), mobject.get_family()):
|
||||
sm1.set_data(sm2.data)
|
||||
sm1.set_uniforms(sm2.uniforms)
|
||||
self.refresh_bounding_box(recurse_down=True)
|
||||
return self
|
||||
|
||||
# Locking data
|
||||
|
||||
def lock_data(self, keys: Iterable[str]):
|
||||
|
|
|
@ -8,6 +8,8 @@ import operator as op
|
|||
import moderngl
|
||||
import numpy as np
|
||||
|
||||
from manimlib.constants import GREY_C
|
||||
from manimlib.constants import GREY_E
|
||||
from manimlib.constants import BLACK, WHITE
|
||||
from manimlib.constants import DEFAULT_STROKE_WIDTH
|
||||
from manimlib.constants import DEGREES
|
||||
|
@ -23,6 +25,7 @@ from manimlib.utils.bezier import integer_interpolate
|
|||
from manimlib.utils.bezier import interpolate
|
||||
from manimlib.utils.bezier import inverse_interpolate
|
||||
from manimlib.utils.bezier import partial_quadratic_bezier_points
|
||||
from manimlib.utils.color import color_gradient
|
||||
from manimlib.utils.color import rgb_to_hex
|
||||
from manimlib.utils.iterables import listify
|
||||
from manimlib.utils.iterables import make_even
|
||||
|
@ -1193,3 +1196,24 @@ class DashedVMobject(VMobject):
|
|||
# Family is already taken care of by get_subcurve
|
||||
# implementation
|
||||
self.match_style(vmobject, recurse=False)
|
||||
|
||||
|
||||
class VHighlight(VGroup):
|
||||
def __init__(
|
||||
self,
|
||||
vmobject: VMobject,
|
||||
n_layers: int = 3,
|
||||
color_bounds: tuple[ManimColor] = (GREY_C, GREY_E),
|
||||
max_stroke_width: float = 10.0,
|
||||
):
|
||||
outline = vmobject.replicate(n_layers)
|
||||
outline.set_fill(opacity=0)
|
||||
added_widths = np.linspace(0, max_stroke_width, n_layers + 1)[1:]
|
||||
colors = color_gradient(color_bounds, n_layers)
|
||||
for part, added_width, color in zip(reversed(outline), added_widths, colors):
|
||||
for sm in part.family_members_with_points():
|
||||
part.set_stroke(
|
||||
width=sm.get_stroke_width() + added_width,
|
||||
color=color,
|
||||
)
|
||||
super().__init__(*outline)
|
||||
|
|
|
@ -1,24 +1,23 @@
|
|||
import itertools as it
|
||||
import numpy as np
|
||||
import os
|
||||
import platform
|
||||
import pyperclip
|
||||
|
||||
from manimlib.animation.fading import FadeIn
|
||||
from manimlib.constants import ARROW_SYMBOLS, DELETE_SYMBOL, SHIFT_SYMBOL
|
||||
from manimlib.constants import ARROW_SYMBOLS, CTRL_SYMBOL, DELETE_SYMBOL, SHIFT_SYMBOL
|
||||
from manimlib.constants import COMMAND_MODIFIER, SHIFT_MODIFIER
|
||||
from manimlib.constants import DL, DOWN, DR, LEFT, ORIGIN, RIGHT, UL, UP, UR
|
||||
from manimlib.constants import FRAME_WIDTH, SMALL_BUFF
|
||||
from manimlib.constants import MANIM_COLORS, WHITE, YELLOW
|
||||
from manimlib.logger import log
|
||||
from manimlib.constants import MANIM_COLORS, WHITE, GREY_C
|
||||
from manimlib.mobject.geometry import Rectangle
|
||||
from manimlib.mobject.geometry import Square
|
||||
from manimlib.mobject.mobject import Group
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.mobject.numbers import DecimalNumber
|
||||
from manimlib.mobject.svg.tex_mobject import Tex
|
||||
from manimlib.mobject.svg.text_mobject import Text
|
||||
from manimlib.mobject.types.dot_cloud import DotCloud
|
||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
from manimlib.mobject.types.vectorized_mobject import VHighlight
|
||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
from manimlib.scene.scene import Scene
|
||||
from manimlib.utils.family_ops import extract_mobject_family_members
|
||||
|
@ -28,14 +27,17 @@ from manimlib.utils.tex_file_writing import LatexError
|
|||
|
||||
SELECT_KEY = 's'
|
||||
GRAB_KEY = 'g'
|
||||
HORIZONTAL_GRAB_KEY = 'h'
|
||||
VERTICAL_GRAB_KEY = 'v'
|
||||
X_GRAB_KEY = 'h'
|
||||
Y_GRAB_KEY = 'v'
|
||||
GRAB_KEYS = [GRAB_KEY, X_GRAB_KEY, Y_GRAB_KEY]
|
||||
RESIZE_KEY = 't'
|
||||
COLOR_KEY = 'c'
|
||||
CURSOR_LOCATION_KEY = 'l'
|
||||
|
||||
|
||||
# Note, a lot of the functionality here is still buggy and very much a work in progress.
|
||||
|
||||
|
||||
class InteractiveScene(Scene):
|
||||
"""
|
||||
To select mobjects on screen, hold ctrl and move the mouse to highlight a region,
|
||||
|
@ -66,36 +68,102 @@ class InteractiveScene(Scene):
|
|||
selection_rectangle_stroke_width = 1.0
|
||||
colors = MANIM_COLORS
|
||||
selection_nudge_size = 0.05
|
||||
cursor_location_config = dict(
|
||||
font_size=14,
|
||||
fill_color=GREY_C,
|
||||
num_decimal_places=3,
|
||||
)
|
||||
|
||||
def setup(self):
|
||||
self.selection = Group()
|
||||
self.selection_highlight = Group()
|
||||
self.selection_rectangle = self.get_selection_rectangle()
|
||||
self.color_palette = self.get_color_palette()
|
||||
self.cursor_location_label = self.get_cursor_location_label()
|
||||
self.unselectables = [
|
||||
self.selection,
|
||||
self.selection_highlight,
|
||||
self.selection_rectangle,
|
||||
self.cursor_location_label,
|
||||
self.camera.frame
|
||||
]
|
||||
self.saved_selection_state = []
|
||||
self.select_top_level_mobs = True
|
||||
self.regenerate_selection_search_set()
|
||||
|
||||
self.is_selecting = False
|
||||
self.is_grabbing = False
|
||||
self.add(self.selection_highlight)
|
||||
|
||||
def get_selection_rectangle(self):
|
||||
rect = Rectangle(
|
||||
stroke_color=self.selection_rectangle_stroke_color,
|
||||
stroke_width=self.selection_rectangle_stroke_width,
|
||||
)
|
||||
rect.fix_in_frame()
|
||||
rect.fixed_corner = ORIGIN
|
||||
rect.add_updater(self.update_selection_rectangle)
|
||||
return rect
|
||||
|
||||
def update_selection_rectangle(self, rect):
|
||||
p1 = rect.fixed_corner
|
||||
p2 = self.mouse_point.get_center()
|
||||
rect.set_points_as_corners([
|
||||
p1, [p2[0], p1[1], 0],
|
||||
p2, [p1[0], p2[1], 0],
|
||||
p1,
|
||||
])
|
||||
return rect
|
||||
|
||||
def get_color_palette(self):
|
||||
palette = VGroup(*(
|
||||
Square(fill_color=color, fill_opacity=1, side_length=1)
|
||||
for color in self.colors
|
||||
))
|
||||
palette.set_stroke(width=0)
|
||||
palette.arrange(RIGHT, buff=0.5)
|
||||
palette.set_width(FRAME_WIDTH - 0.5)
|
||||
palette.to_edge(DOWN, buff=SMALL_BUFF)
|
||||
palette.fix_in_frame()
|
||||
return palette
|
||||
|
||||
def get_cursor_location_label(self):
|
||||
decimals = VGroup(*(
|
||||
DecimalNumber(**self.cursor_location_config)
|
||||
for n in range(3)
|
||||
))
|
||||
|
||||
def update_coords(decimals):
|
||||
for mob, coord in zip(decimals, self.mouse_point.get_location()):
|
||||
mob.set_value(coord)
|
||||
decimals.arrange(RIGHT, buff=decimals.get_height())
|
||||
decimals.to_corner(DR, buff=SMALL_BUFF)
|
||||
decimals.fix_in_frame()
|
||||
return decimals
|
||||
|
||||
decimals.add_updater(update_coords)
|
||||
return decimals
|
||||
|
||||
# Related to selection
|
||||
|
||||
def toggle_selection_mode(self):
|
||||
self.select_top_level_mobs = not self.select_top_level_mobs
|
||||
self.refresh_selection_scope()
|
||||
self.regenerate_selection_search_set()
|
||||
|
||||
def get_selection_search_set(self):
|
||||
mobs = [m for m in self.mobjects if m not in self.unselectables]
|
||||
def get_selection_search_set(self) -> list[Mobject]:
|
||||
return self.selection_search_set
|
||||
|
||||
def regenerate_selection_search_set(self):
|
||||
selectable = list(filter(
|
||||
lambda m: m not in self.unselectables,
|
||||
self.mobjects
|
||||
))
|
||||
if self.select_top_level_mobs:
|
||||
return mobs
|
||||
self.selection_search_set = selectable
|
||||
else:
|
||||
return [
|
||||
self.selection_search_set = [
|
||||
submob
|
||||
for mob in mobs
|
||||
for mob in selectable
|
||||
for submob in mob.family_members_with_points()
|
||||
]
|
||||
|
||||
|
@ -116,37 +184,7 @@ class InteractiveScene(Scene):
|
|||
)
|
||||
self.refresh_selection_highlight()
|
||||
|
||||
def get_selection_rectangle(self):
|
||||
rect = Rectangle(
|
||||
stroke_color=self.selection_rectangle_stroke_color,
|
||||
stroke_width=self.selection_rectangle_stroke_width,
|
||||
)
|
||||
rect.fix_in_frame()
|
||||
rect.fixed_corner = ORIGIN
|
||||
rect.add_updater(self.update_selection_rectangle)
|
||||
return rect
|
||||
|
||||
def get_color_palette(self):
|
||||
palette = VGroup(*(
|
||||
Square(fill_color=color, fill_opacity=1, side_length=1)
|
||||
for color in self.colors
|
||||
))
|
||||
palette.set_stroke(width=0)
|
||||
palette.arrange(RIGHT, buff=0.5)
|
||||
palette.set_width(FRAME_WIDTH - 0.5)
|
||||
palette.to_edge(DOWN, buff=SMALL_BUFF)
|
||||
palette.fix_in_frame()
|
||||
return palette
|
||||
|
||||
def get_stroke_highlight(self, vmobject):
|
||||
outline = vmobject.copy()
|
||||
for sm, osm in zip(vmobject.get_family(), outline.get_family()):
|
||||
osm.set_fill(opacity=0)
|
||||
osm.set_stroke(YELLOW, width=sm.get_stroke_width() + 1.5)
|
||||
outline.add_updater(lambda o: o.replace(vmobject))
|
||||
return outline
|
||||
|
||||
def get_corner_dots(self, mobject):
|
||||
def get_corner_dots(self, mobject: Mobject) -> Mobject:
|
||||
dots = DotCloud(**self.corner_dot_config)
|
||||
radius = self.corner_dot_config["radius"]
|
||||
if mobject.get_depth() < 1e-2:
|
||||
|
@ -159,9 +197,11 @@ class InteractiveScene(Scene):
|
|||
]))
|
||||
return dots
|
||||
|
||||
def get_highlight(self, mobject):
|
||||
if isinstance(mobject, VMobject) and mobject.has_points():
|
||||
return self.get_stroke_highlight(mobject)
|
||||
def get_highlight(self, mobject: Mobject) -> Mobject:
|
||||
if isinstance(mobject, VMobject) and mobject.has_points() and not self.select_top_level_mobs:
|
||||
result = VHighlight(mobject)
|
||||
result.add_updater(lambda m: m.replace(mobject))
|
||||
return result
|
||||
else:
|
||||
return self.get_corner_dots(mobject)
|
||||
|
||||
|
@ -171,40 +211,54 @@ class InteractiveScene(Scene):
|
|||
for mob in self.selection
|
||||
])
|
||||
|
||||
def update_selection_rectangle(self, rect):
|
||||
p1 = rect.fixed_corner
|
||||
p2 = self.mouse_point.get_center()
|
||||
rect.set_points_as_corners([
|
||||
p1, [p2[0], p1[1], 0],
|
||||
p2, [p1[0], p2[1], 0],
|
||||
p1,
|
||||
])
|
||||
return rect
|
||||
|
||||
def add_to_selection(self, *mobjects):
|
||||
mobs = list(filter(lambda m: m not in self.unselectables, mobjects))
|
||||
self.selection.add(*mobjects)
|
||||
mobs = list(filter(
|
||||
lambda m: m not in self.unselectables and m not in self.selection,
|
||||
mobjects
|
||||
))
|
||||
if len(mobs) == 0:
|
||||
return
|
||||
self.selection.add(*mobs)
|
||||
self.selection_highlight.add(*map(self.get_highlight, mobs))
|
||||
self.saved_selection_state = [(mob, mob.copy()) for mob in self.selection]
|
||||
for mob in mobs:
|
||||
mob.set_animating_status(True)
|
||||
self.refresh_static_mobjects()
|
||||
|
||||
def toggle_from_selection(self, *mobjects):
|
||||
for mob in mobjects:
|
||||
if mob in self.selection:
|
||||
self.selection.remove(mob)
|
||||
mob.set_animating_status(False)
|
||||
else:
|
||||
self.add_to_selection(mob)
|
||||
self.refresh_selection_highlight()
|
||||
|
||||
def clear_selection(self):
|
||||
for mob in self.selection:
|
||||
mob.set_animating_status(False)
|
||||
self.selection.set_submobjects([])
|
||||
self.selection_highlight.set_submobjects([])
|
||||
self.refresh_static_mobjects()
|
||||
|
||||
def add(self, *new_mobjects: Mobject):
|
||||
for mob in new_mobjects:
|
||||
mob.make_movable()
|
||||
super().add(*new_mobjects)
|
||||
self.regenerate_selection_search_set()
|
||||
|
||||
# Selection operations
|
||||
def remove(self, *mobjects: Mobject):
|
||||
super().remove(*mobjects)
|
||||
self.regenerate_selection_search_set()
|
||||
|
||||
def disable_interaction(self, *mobjects: Mobject):
|
||||
for mob in mobjects:
|
||||
self.unselectables.append(mob)
|
||||
self.regenerate_selection_search_set()
|
||||
|
||||
def enable_interaction(self, *mobjects: Mobject):
|
||||
for mob in mobjects:
|
||||
if mob in self.unselectables:
|
||||
self.unselectables.remove(mob)
|
||||
|
||||
# Functions for keyboard actions
|
||||
|
||||
def copy_selection(self):
|
||||
ids = map(id, self.selection)
|
||||
|
@ -218,11 +272,11 @@ class InteractiveScene(Scene):
|
|||
mobs = map(self.id_to_mobject, ids)
|
||||
mob_copies = [m.copy() for m in mobs if m is not None]
|
||||
self.clear_selection()
|
||||
self.add_to_selection(*mob_copies)
|
||||
self.play(*(
|
||||
FadeIn(mc, run_time=0.5, scale=1.5)
|
||||
for mc in mob_copies
|
||||
))
|
||||
self.add_to_selection(*mob_copies)
|
||||
return
|
||||
except ValueError:
|
||||
pass
|
||||
|
@ -242,41 +296,27 @@ class InteractiveScene(Scene):
|
|||
self.remove(*self.selection)
|
||||
self.clear_selection()
|
||||
|
||||
def saved_selection_to_file(self):
|
||||
directory = self.file_writer.get_saved_mobject_directory()
|
||||
files = os.listdir(directory)
|
||||
for mob in self.selection:
|
||||
file_name = str(mob) + "_0.mob"
|
||||
index = 0
|
||||
while file_name in files:
|
||||
file_name = file_name.replace(str(index), str(index + 1))
|
||||
index += 1
|
||||
if platform.system() == 'Darwin':
|
||||
user_name = os.popen(f"""
|
||||
osascript -e '
|
||||
set chosenfile to (choose file name default name "{file_name}" default location "{directory}")
|
||||
POSIX path of chosenfile'
|
||||
""").read()
|
||||
user_name = user_name.replace("\n", "")
|
||||
else:
|
||||
user_name = input(
|
||||
f"Enter mobject file name (default is {file_name}): "
|
||||
)
|
||||
if user_name:
|
||||
file_name = user_name
|
||||
files.append(file_name)
|
||||
self.save_mobect(mob, file_name)
|
||||
|
||||
def undo(self):
|
||||
mobs = []
|
||||
for mob, state in self.saved_selection_state:
|
||||
mob.become(state)
|
||||
mobs.append(mob)
|
||||
if mob not in self.mobjects:
|
||||
self.add(mob)
|
||||
self.selection.set_submobjects(mobs)
|
||||
def restore_state(self, mobject_states: list[tuple[Mobject, Mobject]]):
|
||||
super().restore_state(mobject_states)
|
||||
self.refresh_selection_highlight()
|
||||
|
||||
def enable_selection(self):
|
||||
self.is_selecting = True
|
||||
self.add(self.selection_rectangle)
|
||||
self.selection_rectangle.fixed_corner = self.mouse_point.get_center().copy()
|
||||
|
||||
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)
|
||||
|
||||
def prepare_grab(self):
|
||||
mp = self.mouse_point.get_center()
|
||||
self.mouse_to_selection = mp - self.selection.get_center()
|
||||
self.is_grabbing = True
|
||||
|
||||
def prepare_resizing(self, about_corner=False):
|
||||
center = self.selection.get_center()
|
||||
mp = self.mouse_point.get_center()
|
||||
|
@ -286,136 +326,177 @@ class InteractiveScene(Scene):
|
|||
self.scale_about_point = center
|
||||
self.scale_ref_vect = mp - self.scale_about_point
|
||||
self.scale_ref_width = self.selection.get_width()
|
||||
self.scale_ref_height = self.selection.get_height()
|
||||
|
||||
# Event handlers
|
||||
def toggle_color_palette(self):
|
||||
if len(self.selection) == 0:
|
||||
return
|
||||
if self.color_palette not in self.mobjects:
|
||||
self.save_state()
|
||||
self.add(self.color_palette)
|
||||
else:
|
||||
self.remove(self.color_palette)
|
||||
|
||||
def group_selection(self):
|
||||
group = self.get_group(*self.selection)
|
||||
self.add(group)
|
||||
self.clear_selection()
|
||||
self.add_to_selection(group)
|
||||
|
||||
def ungroup_selection(self):
|
||||
pieces = []
|
||||
for mob in list(self.selection):
|
||||
self.remove(mob)
|
||||
pieces.extend(list(mob))
|
||||
self.clear_selection()
|
||||
self.add(*pieces)
|
||||
self.add_to_selection(*pieces)
|
||||
|
||||
def nudge_selection(self, vect: np.ndarray, large: bool = False):
|
||||
nudge = self.selection_nudge_size
|
||||
if large:
|
||||
nudge *= 10
|
||||
self.selection.shift(nudge * vect)
|
||||
|
||||
def save_selection_to_file(self):
|
||||
if len(self.selection) == 1:
|
||||
self.save_mobject_to_file(self.selection[0])
|
||||
else:
|
||||
self.save_mobject_to_file(self.selection)
|
||||
|
||||
def on_key_press(self, symbol: int, modifiers: int) -> None:
|
||||
super().on_key_press(symbol, modifiers)
|
||||
char = chr(symbol)
|
||||
# Enable selection
|
||||
if char == SELECT_KEY and modifiers == 0:
|
||||
self.is_selecting = True
|
||||
self.add(self.selection_rectangle)
|
||||
self.selection_rectangle.fixed_corner = self.mouse_point.get_center().copy()
|
||||
# Prepare for move
|
||||
elif char in [GRAB_KEY, HORIZONTAL_GRAB_KEY, VERTICAL_GRAB_KEY] and modifiers == 0:
|
||||
mp = self.mouse_point.get_center()
|
||||
self.mouse_to_selection = mp - self.selection.get_center()
|
||||
# Prepare for resizing
|
||||
self.enable_selection()
|
||||
elif char in GRAB_KEYS and modifiers == 0:
|
||||
self.prepare_grab()
|
||||
elif char == RESIZE_KEY and modifiers in [0, SHIFT_MODIFIER]:
|
||||
self.prepare_resizing(about_corner=(modifiers == SHIFT_MODIFIER))
|
||||
elif symbol == SHIFT_SYMBOL:
|
||||
if self.window.is_key_pressed(ord("t")):
|
||||
self.prepare_resizing(about_corner=True)
|
||||
# Show color palette
|
||||
elif char == COLOR_KEY and modifiers == 0:
|
||||
if len(self.selection) == 0:
|
||||
return
|
||||
if self.color_palette not in self.mobjects:
|
||||
self.add(self.color_palette)
|
||||
else:
|
||||
self.remove(self.color_palette)
|
||||
# Command + c -> Copy mobject ids to clipboard
|
||||
self.toggle_color_palette()
|
||||
elif char == CURSOR_LOCATION_KEY and modifiers == 0:
|
||||
self.add(self.cursor_location_label)
|
||||
elif char == "c" and modifiers == COMMAND_MODIFIER:
|
||||
self.copy_selection()
|
||||
# Command + v -> Paste
|
||||
elif char == "v" and modifiers == COMMAND_MODIFIER:
|
||||
self.paste_selection()
|
||||
# Command + x -> Cut
|
||||
elif char == "x" and modifiers == COMMAND_MODIFIER:
|
||||
# TODO, this copy won't work, because once the objects are removed,
|
||||
# they're not searched for in the pasting.
|
||||
self.copy_selection()
|
||||
self.delete_selection()
|
||||
# Delete
|
||||
elif symbol == DELETE_SYMBOL:
|
||||
self.delete_selection()
|
||||
# Command + a -> Select all
|
||||
elif char == "a" and modifiers == COMMAND_MODIFIER:
|
||||
self.clear_selection()
|
||||
self.add_to_selection(*self.mobjects)
|
||||
# Command + g -> Group selection
|
||||
elif char == "g" and modifiers == COMMAND_MODIFIER:
|
||||
group = self.get_group(*self.selection)
|
||||
self.add(group)
|
||||
self.clear_selection()
|
||||
self.add_to_selection(group)
|
||||
# Command + shift + g -> Ungroup the selection
|
||||
self.group_selection()
|
||||
elif char == "g" and modifiers == COMMAND_MODIFIER | SHIFT_MODIFIER:
|
||||
pieces = []
|
||||
for mob in list(self.selection):
|
||||
self.remove(mob)
|
||||
pieces.extend(list(mob))
|
||||
self.clear_selection()
|
||||
self.add(*pieces)
|
||||
self.add_to_selection(*pieces)
|
||||
# Command + t -> Toggle selection mode
|
||||
self.ungroup_selection()
|
||||
elif char == "t" and modifiers == COMMAND_MODIFIER:
|
||||
self.toggle_selection_mode()
|
||||
# Command + z -> Restore selection to original state
|
||||
elif char == "z" and modifiers == COMMAND_MODIFIER:
|
||||
self.undo()
|
||||
# Command + s -> Save selections to file
|
||||
elif char == "z" and modifiers == COMMAND_MODIFIER | SHIFT_MODIFIER:
|
||||
self.redo()
|
||||
elif char == "s" and modifiers == COMMAND_MODIFIER:
|
||||
self.saved_selection_to_file()
|
||||
# Keyboard movements
|
||||
self.save_selection_to_file()
|
||||
elif symbol in ARROW_SYMBOLS:
|
||||
nudge = self.selection_nudge_size
|
||||
if (modifiers & SHIFT_MODIFIER):
|
||||
nudge *= 10
|
||||
vect = [LEFT, UP, RIGHT, DOWN][ARROW_SYMBOLS.index(symbol)]
|
||||
self.selection.shift(nudge * vect)
|
||||
self.nudge_selection(
|
||||
vect=[LEFT, UP, RIGHT, DOWN][ARROW_SYMBOLS.index(symbol)],
|
||||
large=(modifiers & SHIFT_MODIFIER),
|
||||
)
|
||||
|
||||
# Conditions for saving state
|
||||
if char in [GRAB_KEY, X_GRAB_KEY, Y_GRAB_KEY, RESIZE_KEY]:
|
||||
self.save_state()
|
||||
|
||||
def on_key_release(self, symbol: int, modifiers: int) -> None:
|
||||
super().on_key_release(symbol, modifiers)
|
||||
if chr(symbol) == SELECT_KEY:
|
||||
self.is_selecting = False
|
||||
self.remove(self.selection_rectangle)
|
||||
for mob in reversed(self.get_selection_search_set()):
|
||||
if mob.is_movable() and self.selection_rectangle.is_touching(mob):
|
||||
self.add_to_selection(mob)
|
||||
self.gather_new_selection()
|
||||
if chr(symbol) in GRAB_KEYS:
|
||||
self.is_grabbing = False
|
||||
elif chr(symbol) == CURSOR_LOCATION_KEY:
|
||||
self.remove(self.cursor_location_label)
|
||||
elif symbol == SHIFT_SYMBOL and self.window.is_key_pressed(ord(RESIZE_KEY)):
|
||||
self.prepare_resizing(about_corner=False)
|
||||
|
||||
elif symbol == SHIFT_SYMBOL:
|
||||
if self.window.is_key_pressed(ord(RESIZE_KEY)):
|
||||
self.prepare_resizing(about_corner=False)
|
||||
# Mouse actions
|
||||
def handle_grabbing(self, point: np.ndarray):
|
||||
diff = point - self.mouse_to_selection
|
||||
if self.window.is_key_pressed(ord(GRAB_KEY)):
|
||||
self.selection.move_to(diff)
|
||||
elif self.window.is_key_pressed(ord(X_GRAB_KEY)):
|
||||
self.selection.set_x(diff[0])
|
||||
elif self.window.is_key_pressed(ord(Y_GRAB_KEY)):
|
||||
self.selection.set_y(diff[1])
|
||||
|
||||
def on_mouse_motion(self, point: np.ndarray, d_point: np.ndarray) -> None:
|
||||
super().on_mouse_motion(point, d_point)
|
||||
# Move selection
|
||||
if self.window.is_key_pressed(ord("g")):
|
||||
self.selection.move_to(point - self.mouse_to_selection)
|
||||
# Move selection restricted to horizontal
|
||||
elif self.window.is_key_pressed(ord("h")):
|
||||
self.selection.set_x((point - self.mouse_to_selection)[0])
|
||||
# Move selection restricted to vertical
|
||||
elif self.window.is_key_pressed(ord("v")):
|
||||
self.selection.set_y((point - self.mouse_to_selection)[1])
|
||||
# Scale selection
|
||||
elif self.window.is_key_pressed(ord("t")):
|
||||
# TODO, allow for scaling about the opposite corner
|
||||
vect = point - self.scale_about_point
|
||||
def handle_resizing(self, point: np.ndarray):
|
||||
vect = point - self.scale_about_point
|
||||
if self.window.is_key_pressed(CTRL_SYMBOL):
|
||||
for i in (0, 1):
|
||||
scalar = vect[i] / self.scale_ref_vect[i]
|
||||
self.selection.rescale_to_fit(
|
||||
scalar * [self.scale_ref_width, self.scale_ref_height][i],
|
||||
dim=i,
|
||||
about_point=self.scale_about_point,
|
||||
stretch=True,
|
||||
)
|
||||
else:
|
||||
scalar = get_norm(vect) / get_norm(self.scale_ref_vect)
|
||||
self.selection.set_width(
|
||||
scalar * self.scale_ref_width,
|
||||
about_point=self.scale_about_point
|
||||
)
|
||||
|
||||
def handle_sweeping_selection(self, point: np.ndarray):
|
||||
mob = self.point_to_mobject(
|
||||
point, search_set=self.get_selection_search_set(),
|
||||
buff=SMALL_BUFF
|
||||
)
|
||||
if mob is not None:
|
||||
self.add_to_selection(mob)
|
||||
|
||||
def choose_color(self, point: np.ndarray):
|
||||
# Search through all mobject on the screen, not just the palette
|
||||
to_search = [
|
||||
sm
|
||||
for mobject in self.mobjects
|
||||
for sm in mobject.family_members_with_points()
|
||||
if mobject not in self.unselectables
|
||||
]
|
||||
mob = self.point_to_mobject(point, to_search)
|
||||
if mob is not None:
|
||||
self.selection.set_color(mob.get_color())
|
||||
self.remove(self.color_palette)
|
||||
|
||||
def toggle_clicked_mobject_from_selection(self, point: np.ndarray):
|
||||
mob = self.point_to_mobject(
|
||||
point,
|
||||
search_set=self.get_selection_search_set(),
|
||||
buff=SMALL_BUFF
|
||||
)
|
||||
if mob is not None:
|
||||
self.toggle_from_selection(mob)
|
||||
|
||||
def on_mouse_motion(self, point: np.ndarray, d_point: np.ndarray) -> None:
|
||||
super().on_mouse_motion(point, d_point)
|
||||
if self.is_grabbing:
|
||||
self.handle_grabbing(point)
|
||||
elif self.window.is_key_pressed(ord(RESIZE_KEY)):
|
||||
self.handle_resizing(point)
|
||||
elif self.window.is_key_pressed(ord(SELECT_KEY)) and self.window.is_key_pressed(SHIFT_SYMBOL):
|
||||
self.handle_sweeping_selection(point)
|
||||
|
||||
def on_mouse_release(self, point: np.ndarray, button: int, mods: int) -> None:
|
||||
super().on_mouse_release(point, button, mods)
|
||||
if self.color_palette in self.mobjects:
|
||||
# Search through all mobject on the screne, not just the palette
|
||||
to_search = list(it.chain(*(
|
||||
mobject.family_members_with_points()
|
||||
for mobject in self.mobjects
|
||||
if mobject not in self.unselectables
|
||||
)))
|
||||
mob = self.point_to_mobject(point, to_search)
|
||||
if mob is not None:
|
||||
self.selection.set_color(mob.get_fill_color())
|
||||
self.remove(self.color_palette)
|
||||
self.choose_color(point)
|
||||
elif self.window.is_key_pressed(SHIFT_SYMBOL):
|
||||
mob = self.point_to_mobject(point)
|
||||
if mob is not None:
|
||||
self.toggle_from_selection(mob)
|
||||
self.toggle_clicked_mobject_from_selection(point)
|
||||
else:
|
||||
self.clear_selection()
|
||||
|
|
|
@ -14,10 +14,9 @@ from tqdm import tqdm as ProgressDisplay
|
|||
from manimlib.animation.animation import prepare_animation
|
||||
from manimlib.animation.transform import MoveToTarget
|
||||
from manimlib.camera.camera import Camera
|
||||
from manimlib.config import get_custom_config
|
||||
from manimlib.constants import ARROW_SYMBOLS
|
||||
from manimlib.constants import DEFAULT_WAIT_TIME
|
||||
from manimlib.constants import COMMAND_MODIFIER, CTRL_MODIFIER, SHIFT_MODIFIER
|
||||
from manimlib.constants import COMMAND_MODIFIER
|
||||
from manimlib.event_handler import EVENT_DISPATCHER
|
||||
from manimlib.event_handler.event_type import EventType
|
||||
from manimlib.logger import log
|
||||
|
@ -46,7 +45,6 @@ FRAME_SHIFT_KEY = 'f'
|
|||
ZOOM_KEY = 'z'
|
||||
RESET_FRAME_KEY = 'r'
|
||||
QUIT_KEY = 'q'
|
||||
EMBED_KEY = 'e'
|
||||
|
||||
|
||||
class Scene(object):
|
||||
|
@ -65,6 +63,7 @@ class Scene(object):
|
|||
"presenter_mode": False,
|
||||
"linger_after_completion": True,
|
||||
"pan_sensitivity": 3,
|
||||
"max_num_saved_states": 20,
|
||||
}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
|
@ -74,12 +73,15 @@ class Scene(object):
|
|||
self.window = Window(scene=self, **self.window_config)
|
||||
self.camera_config["ctx"] = self.window.ctx
|
||||
self.camera_config["frame_rate"] = 30 # Where's that 30 from?
|
||||
self.undo_stack = []
|
||||
self.redo_stack = []
|
||||
else:
|
||||
self.window = None
|
||||
|
||||
self.camera: Camera = self.camera_class(**self.camera_config)
|
||||
self.file_writer = SceneFileWriter(self, **self.file_writer_config)
|
||||
self.mobjects: list[Mobject] = [self.camera.frame]
|
||||
self.id_to_mobject_map: dict[int, Mobject] = dict()
|
||||
self.num_plays: int = 0
|
||||
self.time: float = 0
|
||||
self.skip_time: float = 0
|
||||
|
@ -91,12 +93,16 @@ class Scene(object):
|
|||
self.mouse_point = Point()
|
||||
self.mouse_drag_point = Point()
|
||||
self.hold_on_wait = self.presenter_mode
|
||||
self.inside_embed = False
|
||||
|
||||
# Much nicer to work with deterministic scenes
|
||||
if self.random_seed is not None:
|
||||
random.seed(self.random_seed)
|
||||
np.random.seed(self.random_seed)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.__class__.__name__
|
||||
|
||||
def run(self) -> None:
|
||||
self.virtual_animation_start_time: float = 0
|
||||
self.real_animation_start_time: float = time.time()
|
||||
|
@ -146,40 +152,57 @@ class Scene(object):
|
|||
|
||||
def embed(self, close_scene_on_exit: bool = True) -> None:
|
||||
if not self.preview:
|
||||
# If the scene is just being
|
||||
# written, ignore embed calls
|
||||
# Ignore embed calls when there is no preview
|
||||
return
|
||||
self.inside_embed = True
|
||||
self.stop_skipping()
|
||||
self.linger_after_completion = False
|
||||
self.update_frame()
|
||||
|
||||
# Save scene state at the point of embedding
|
||||
self.save_state()
|
||||
|
||||
from IPython.terminal.embed import InteractiveShellEmbed
|
||||
shell = InteractiveShellEmbed()
|
||||
# Have the frame update after each command
|
||||
shell.events.register('post_run_cell', lambda *a, **kw: self.refresh_static_mobjects())
|
||||
shell.events.register('post_run_cell', lambda *a, **kw: self.update_frame())
|
||||
# Use the locals of the caller as the local namespace
|
||||
# once embedded, and add a few custom shortcuts
|
||||
# Configure and launch embedded IPython terminal
|
||||
from IPython.terminal import embed, pt_inputhooks
|
||||
shell = embed.InteractiveShellEmbed.instance()
|
||||
|
||||
# Use the locals namespace of the caller
|
||||
local_ns = inspect.currentframe().f_back.f_locals
|
||||
local_ns["touch"] = self.interact
|
||||
local_ns["i2g"] = self.ids_to_group
|
||||
for term in ("play", "wait", "add", "remove", "clear", "save_state", "restore"):
|
||||
local_ns[term] = getattr(self, term)
|
||||
log.info("Tips: Now the embed iPython terminal is open. But you can't interact with"
|
||||
" the window directly. To do so, you need to type `touch()` or `self.interact()`")
|
||||
exec(get_custom_config()["universal_import_line"])
|
||||
# Add a few custom shortcuts
|
||||
local_ns.update({
|
||||
name: getattr(self, name)
|
||||
for name in [
|
||||
"play", "wait", "add", "remove", "clear",
|
||||
"save_state", "undo", "redo", "i2g", "i2m"
|
||||
]
|
||||
})
|
||||
|
||||
# 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:
|
||||
self.update_frame(dt=0)
|
||||
|
||||
pt_inputhooks.register("manim", inputhook)
|
||||
shell.enable_gui("manim")
|
||||
|
||||
# Operation to run after each ipython command
|
||||
def post_cell_func(*args, **kwargs):
|
||||
self.refresh_static_mobjects()
|
||||
|
||||
shell.events.register("post_run_cell", post_cell_func)
|
||||
|
||||
# Launch shell, with stack_depth=2 indicating we should use caller globals/locals
|
||||
shell(local_ns=local_ns, stack_depth=2)
|
||||
|
||||
self.inside_embed = False
|
||||
# End scene when exiting an embed
|
||||
if close_scene_on_exit:
|
||||
raise EndSceneEarlyException()
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.__class__.__name__
|
||||
|
||||
# Only these methods should touch the camera
|
||||
|
||||
def get_image(self) -> Image:
|
||||
return self.camera.get_image()
|
||||
|
||||
|
@ -210,6 +233,7 @@ class Scene(object):
|
|||
self.file_writer.write_frame(self.camera)
|
||||
|
||||
# Related to updating
|
||||
|
||||
def update_mobjects(self, dt: float) -> None:
|
||||
for mobject in self.mobjects:
|
||||
mobject.update(dt)
|
||||
|
@ -228,6 +252,7 @@ class Scene(object):
|
|||
])
|
||||
|
||||
# Related to time
|
||||
|
||||
def get_time(self) -> float:
|
||||
return self.time
|
||||
|
||||
|
@ -235,6 +260,7 @@ class Scene(object):
|
|||
self.time += dt
|
||||
|
||||
# Related to internal mobject organization
|
||||
|
||||
def get_top_level_mobjects(self) -> list[Mobject]:
|
||||
# Return only those which are not in the family
|
||||
# of another mobject from the scene
|
||||
|
@ -259,6 +285,11 @@ class Scene(object):
|
|||
"""
|
||||
self.remove(*new_mobjects)
|
||||
self.mobjects += new_mobjects
|
||||
self.id_to_mobject_map.update({
|
||||
id(sm): sm
|
||||
for m in new_mobjects
|
||||
for sm in m.get_family()
|
||||
})
|
||||
return self
|
||||
|
||||
def add_mobjects_among(self, values: Iterable):
|
||||
|
@ -322,11 +353,7 @@ class Scene(object):
|
|||
return Group(*mobjects)
|
||||
|
||||
def id_to_mobject(self, id_value):
|
||||
for mob in self.mobjects:
|
||||
for sm in mob.get_family():
|
||||
if id(sm) == id_value:
|
||||
return sm
|
||||
return None
|
||||
return self.id_to_mobject_map[id_value]
|
||||
|
||||
def ids_to_group(self, *id_values):
|
||||
return self.get_group(*filter(
|
||||
|
@ -334,7 +361,14 @@ class Scene(object):
|
|||
map(self.id_to_mobject, id_values)
|
||||
))
|
||||
|
||||
def i2g(self, *id_values):
|
||||
return self.ids_to_group(*id_values)
|
||||
|
||||
def i2m(self, id_value):
|
||||
return self.id_to_mobject(id_value)
|
||||
|
||||
# Related to skipping
|
||||
|
||||
def update_skipping_status(self) -> None:
|
||||
if self.start_at_animation_number is not None:
|
||||
if self.num_plays == self.start_at_animation_number:
|
||||
|
@ -350,6 +384,7 @@ class Scene(object):
|
|||
self.skip_animations = False
|
||||
|
||||
# Methods associated with running animations
|
||||
|
||||
def get_time_progression(
|
||||
self,
|
||||
run_time: float,
|
||||
|
@ -473,6 +508,8 @@ class Scene(object):
|
|||
def handle_play_like_call(func):
|
||||
@wraps(func)
|
||||
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:
|
||||
|
@ -594,24 +631,39 @@ class Scene(object):
|
|||
self.file_writer.add_sound(sound_file, time, gain, gain_to_background)
|
||||
|
||||
# Helpers for interactive development
|
||||
|
||||
def get_state(self) -> list[tuple[Mobject, Mobject]]:
|
||||
return [(mob, mob.copy()) for mob in self.mobjects]
|
||||
|
||||
def restore_state(self, mobject_states: list[tuple[Mobject, Mobject]]):
|
||||
self.mobjects = [mob.become(mob_copy) for mob, mob_copy in mobject_states]
|
||||
|
||||
def save_state(self) -> None:
|
||||
self.saved_state = [
|
||||
(mob, mob.copy())
|
||||
for mob in self.mobjects
|
||||
]
|
||||
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)
|
||||
|
||||
def restore(self) -> None:
|
||||
if not hasattr(self, "saved_state"):
|
||||
raise Exception("Trying to restore scene without having saved")
|
||||
self.mobjects = []
|
||||
for mob, mob_state in self.saved_state:
|
||||
mob.become(mob_state)
|
||||
self.mobjects.append(mob)
|
||||
def undo(self):
|
||||
if self.undo_stack:
|
||||
self.redo_stack.append(self.get_state())
|
||||
self.restore_state(self.undo_stack.pop())
|
||||
self.refresh_static_mobjects()
|
||||
|
||||
def save_mobect(self, mobject: Mobject, file_name: str):
|
||||
directory = self.file_writer.get_saved_mobject_directory()
|
||||
path = os.path.join(directory, file_name)
|
||||
mobject.save_to_file(path)
|
||||
def redo(self):
|
||||
if self.redo_stack:
|
||||
self.undo_stack.append(self.get_state())
|
||||
self.restore_state(self.redo_stack.pop())
|
||||
self.refresh_static_mobjects()
|
||||
|
||||
def save_mobject_to_file(self, mobject: Mobject, file_path: str | None = None) -> None:
|
||||
if file_path is None:
|
||||
file_path = self.file_writer.get_saved_mobject_path(mobject)
|
||||
if file_path is None:
|
||||
return
|
||||
mobject.save_to_file(file_path)
|
||||
|
||||
def load_mobject(self, file_name):
|
||||
if os.path.exists(file_name):
|
||||
|
@ -739,9 +791,6 @@ class Scene(object):
|
|||
# Space or right arrow
|
||||
elif char == " " or symbol == ARROW_SYMBOLS[2]:
|
||||
self.hold_on_wait = False
|
||||
# ctrl + shift + e
|
||||
elif char == EMBED_KEY and modifiers == CTRL_MODIFIER | SHIFT_MODIFIER:
|
||||
self.embed(close_scene_on_exit=False)
|
||||
|
||||
def on_resize(self, width: int, height: int) -> None:
|
||||
self.camera.reset_pixel_shape(width, height)
|
||||
|
|
|
@ -12,6 +12,7 @@ from tqdm import tqdm as ProgressDisplay
|
|||
|
||||
from manimlib.constants import FFMPEG_BIN
|
||||
from manimlib.logger import log
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.utils.config_ops import digest_config
|
||||
from manimlib.utils.file_ops import add_extension_if_not_present
|
||||
from manimlib.utils.file_ops import get_sorted_integer_files
|
||||
|
@ -61,7 +62,7 @@ class SceneFileWriter(object):
|
|||
|
||||
# Output directories and files
|
||||
def init_output_directories(self) -> None:
|
||||
out_dir = self.output_directory
|
||||
out_dir = self.output_directory or ""
|
||||
if self.mirror_module_path:
|
||||
module_dir = self.get_default_module_directory()
|
||||
out_dir = os.path.join(out_dir, module_dir)
|
||||
|
@ -128,6 +129,36 @@ class SceneFileWriter(object):
|
|||
str(self.scene),
|
||||
))
|
||||
|
||||
def get_saved_mobject_path(self, mobject: Mobject) -> str | None:
|
||||
directory = self.get_saved_mobject_directory()
|
||||
files = os.listdir(directory)
|
||||
default_name = str(mobject) + "_0.mob"
|
||||
index = 0
|
||||
while default_name in files:
|
||||
default_name = default_name.replace(str(index), str(index + 1))
|
||||
index += 1
|
||||
if platform.system() == 'Darwin':
|
||||
cmds = [
|
||||
"osascript", "-e",
|
||||
f"""
|
||||
set chosenfile to (choose file name default name "{default_name}" default location "{directory}")
|
||||
POSIX path of chosenfile
|
||||
""",
|
||||
]
|
||||
process = sp.Popen(cmds, stdout=sp.PIPE)
|
||||
file_path = process.stdout.read().decode("utf-8").split("\n")[0]
|
||||
if not file_path:
|
||||
return
|
||||
else:
|
||||
user_name = input(f"Enter mobject file name (default is {default_name}): ")
|
||||
file_path = os.path.join(directory, user_name or default_name)
|
||||
if os.path.exists(file_path) or os.path.exists(file_path + ".mob"):
|
||||
if input(f"{file_path} already exists. Overwrite (y/n)? ") != "y":
|
||||
return
|
||||
if not file_path.endswith(".mob"):
|
||||
file_path = file_path + ".mob"
|
||||
return file_path
|
||||
|
||||
# Sound
|
||||
def init_audio(self) -> None:
|
||||
self.includes_sound: bool = False
|
||||
|
|
Loading…
Add table
Reference in a new issue