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(
|
parser.add_argument(
|
||||||
"-n", "--start_at_animation_number",
|
"-n", "--start_at_animation_number",
|
||||||
help="Start rendering not from the first animation, but"
|
help="Start rendering not from the first animation, but "
|
||||||
"from another, specified by its index. If you pass"
|
"from another, specified by its index. If you pass "
|
||||||
"in two comma separated values, e.g. \"3,6\", it will end"
|
"in two comma separated values, e.g. \"3,6\", it will end "
|
||||||
"the rendering at the second value",
|
"the rendering at the second value",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-e", "--embed", metavar="LINENO",
|
"-e", "--embed",
|
||||||
help="Takes a line number as an argument, and results"
|
nargs="?",
|
||||||
"in the scene being called as if the line `self.embed()`"
|
const="",
|
||||||
"was inserted into the scene code at that line number."
|
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(
|
parser.add_argument(
|
||||||
"-r", "--resolution",
|
"-r", "--resolution",
|
||||||
|
@ -185,14 +188,62 @@ def get_module(file_name):
|
||||||
return module
|
return module
|
||||||
|
|
||||||
|
|
||||||
|
def get_indent(line: str):
|
||||||
|
return len(line) - len(line.lstrip())
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
@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:
|
with open(file_name, 'r') as fp:
|
||||||
lines = fp.readlines()
|
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")
|
alt_file = file_name.replace(".py", "_inserted_embed.py")
|
||||||
with open(alt_file, 'w') as fp:
|
with open(alt_file, 'w') as fp:
|
||||||
fp.writelines(lines)
|
fp.writelines(lines)
|
||||||
|
@ -296,10 +347,10 @@ def get_configuration(args):
|
||||||
"quiet": args.quiet,
|
"quiet": args.quiet,
|
||||||
}
|
}
|
||||||
|
|
||||||
if args.embed is None:
|
module = get_module(args.file)
|
||||||
module = get_module(args.file)
|
|
||||||
else:
|
if args.embed is not None:
|
||||||
with insert_embed_line(args.file, int(args.embed)) as alt_file:
|
with insert_embed_line(args.file, args.scene_names[0], args.embed) as alt_file:
|
||||||
module = get_module(alt_file)
|
module = get_module(alt_file)
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
|
|
|
@ -75,6 +75,7 @@ DEFAULT_STROKE_WIDTH = 4
|
||||||
# For keyboard interactions
|
# For keyboard interactions
|
||||||
CTRL_SYMBOL = 65508
|
CTRL_SYMBOL = 65508
|
||||||
SHIFT_SYMBOL = 65505
|
SHIFT_SYMBOL = 65505
|
||||||
|
COMMAND_SYMBOL = 65517
|
||||||
DELETE_SYMBOL = 65288
|
DELETE_SYMBOL = 65288
|
||||||
ARROW_SYMBOLS = list(range(65361, 65365))
|
ARROW_SYMBOLS = list(range(65361, 65365))
|
||||||
|
|
||||||
|
|
|
@ -4,10 +4,11 @@ import sys
|
||||||
|
|
||||||
from manimlib.config import get_custom_config
|
from manimlib.config import get_custom_config
|
||||||
from manimlib.logger import log
|
from manimlib.logger import log
|
||||||
|
from manimlib.scene.interactive_scene import InteractiveScene
|
||||||
from manimlib.scene.scene import Scene
|
from manimlib.scene.scene import Scene
|
||||||
|
|
||||||
|
|
||||||
class BlankScene(Scene):
|
class BlankScene(InteractiveScene):
|
||||||
def construct(self):
|
def construct(self):
|
||||||
exec(get_custom_config()["universal_import_line"])
|
exec(get_custom_config()["universal_import_line"])
|
||||||
self.embed()
|
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 batch_by_property
|
||||||
from manimlib.utils.iterables import list_update
|
from manimlib.utils.iterables import list_update
|
||||||
from manimlib.utils.iterables import listify
|
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_array
|
||||||
from manimlib.utils.iterables import resize_preserving_order
|
from manimlib.utils.iterables import resize_preserving_order
|
||||||
from manimlib.utils.iterables import resize_with_interpolation
|
from manimlib.utils.iterables import resize_with_interpolation
|
||||||
|
@ -96,7 +95,8 @@ class Mobject(object):
|
||||||
self.locked_data_keys: set[str] = set()
|
self.locked_data_keys: set[str] = set()
|
||||||
self.needs_new_bounding_box: bool = True
|
self.needs_new_bounding_box: bool = True
|
||||||
self._is_animating: bool = False
|
self._is_animating: bool = False
|
||||||
self._is_movable: bool = False
|
self.saved_state = None
|
||||||
|
self.target = None
|
||||||
|
|
||||||
self.init_data()
|
self.init_data()
|
||||||
self.init_uniforms()
|
self.init_uniforms()
|
||||||
|
@ -148,8 +148,10 @@ class Mobject(object):
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def set_uniforms(self, uniforms: dict):
|
def set_uniforms(self, uniforms: dict):
|
||||||
for key in uniforms:
|
for key, value in uniforms.items():
|
||||||
self.uniforms[key] = uniforms[key] # Copy?
|
if isinstance(value, np.ndarray):
|
||||||
|
value = value.copy()
|
||||||
|
self.uniforms[key] = value
|
||||||
return self
|
return self
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -472,66 +474,91 @@ class Mobject(object):
|
||||||
self.assemble_family()
|
self.assemble_family()
|
||||||
return self
|
return self
|
||||||
|
|
||||||
# Creating new Mobjects from this one
|
# Copying and serialization
|
||||||
|
|
||||||
def replicate(self, n: int) -> Group:
|
def stash_mobject_pointers(func):
|
||||||
return self.get_group_class()(
|
@wraps(func)
|
||||||
*(self.copy() for x in range(n))
|
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):
|
@stash_mobject_pointers
|
||||||
"""
|
def serialize(self):
|
||||||
Returns a new mobject containing multiple copies of this one
|
return pickle.dumps(self)
|
||||||
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
|
|
||||||
|
|
||||||
# Copying
|
def deserialize(self, data: bytes):
|
||||||
|
self.become(pickle.loads(data))
|
||||||
|
return self
|
||||||
|
|
||||||
def copy(self):
|
@stash_mobject_pointers
|
||||||
self.parents = []
|
def copy(self, deep: bool = False):
|
||||||
try:
|
if deep:
|
||||||
return pickle.loads(pickle.dumps(self))
|
try:
|
||||||
except AttributeError:
|
# Often faster than deepcopy
|
||||||
return copy.deepcopy(self)
|
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):
|
def deepcopy(self):
|
||||||
# This used to be different from copy, so is now just here for backward compatibility
|
return self.copy(deep=True)
|
||||||
return self.copy()
|
|
||||||
|
|
||||||
def generate_target(self, use_deepcopy: bool = False):
|
def generate_target(self, use_deepcopy: bool = False):
|
||||||
# TODO, remove now pointless use_deepcopy arg
|
self.target = self.copy(deep=use_deepcopy)
|
||||||
self.target = None # Prevent exponential explosion
|
self.target.saved_state = self.saved_state
|
||||||
self.target = self.copy()
|
|
||||||
return self.target
|
return self.target
|
||||||
|
|
||||||
def save_state(self, use_deepcopy: bool = False):
|
def save_state(self, use_deepcopy: bool = False):
|
||||||
# TODO, remove now pointless use_deepcopy arg
|
self.saved_state = self.copy(deep=use_deepcopy)
|
||||||
if hasattr(self, "saved_state"):
|
self.saved_state.target = self.target
|
||||||
# Prevent exponential growth of data
|
|
||||||
self.saved_state = None
|
|
||||||
self.saved_state = self.copy()
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def restore(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")
|
raise Exception("Trying to restore without having saved")
|
||||||
self.become(self.saved_state)
|
self.become(self.saved_state)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def save_to_file(self, file_path):
|
def save_to_file(self, file_path: str, supress_overwrite_warning: bool = False):
|
||||||
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
|
|
||||||
with open(file_path, "wb") as fp:
|
with open(file_path, "wb") as fp:
|
||||||
pickle.dump(self, fp)
|
fp.write(self.serialize())
|
||||||
log.info(f"Saved mobject to {file_path}")
|
log.info(f"Saved mobject to {file_path}")
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
@ -544,6 +571,39 @@ class Mobject(object):
|
||||||
mobject = pickle.load(fp)
|
mobject = pickle.load(fp)
|
||||||
return mobject
|
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
|
# Updating
|
||||||
|
|
||||||
def init_updaters(self):
|
def init_updaters(self):
|
||||||
|
@ -646,21 +706,13 @@ class Mobject(object):
|
||||||
# Check if mark as static or not for camera
|
# Check if mark as static or not for camera
|
||||||
|
|
||||||
def is_changing(self) -> bool:
|
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:
|
def set_animating_status(self, is_animating: bool, recurse: bool = True) -> None:
|
||||||
for mob in self.get_family(recurse):
|
for mob in self.get_family(recurse):
|
||||||
mob._is_animating = is_animating
|
mob._is_animating = is_animating
|
||||||
return self
|
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
|
# Transforming operations
|
||||||
|
|
||||||
def shift(self, vector: np.ndarray):
|
def shift(self, vector: np.ndarray):
|
||||||
|
@ -1540,18 +1592,6 @@ class Mobject(object):
|
||||||
"""
|
"""
|
||||||
pass # To implement in subclass
|
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
|
# Locking data
|
||||||
|
|
||||||
def lock_data(self, keys: Iterable[str]):
|
def lock_data(self, keys: Iterable[str]):
|
||||||
|
|
|
@ -8,6 +8,8 @@ import operator as op
|
||||||
import moderngl
|
import moderngl
|
||||||
import numpy as np
|
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 BLACK, WHITE
|
||||||
from manimlib.constants import DEFAULT_STROKE_WIDTH
|
from manimlib.constants import DEFAULT_STROKE_WIDTH
|
||||||
from manimlib.constants import DEGREES
|
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 interpolate
|
||||||
from manimlib.utils.bezier import inverse_interpolate
|
from manimlib.utils.bezier import inverse_interpolate
|
||||||
from manimlib.utils.bezier import partial_quadratic_bezier_points
|
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.color import rgb_to_hex
|
||||||
from manimlib.utils.iterables import listify
|
from manimlib.utils.iterables import listify
|
||||||
from manimlib.utils.iterables import make_even
|
from manimlib.utils.iterables import make_even
|
||||||
|
@ -1193,3 +1196,24 @@ class DashedVMobject(VMobject):
|
||||||
# Family is already taken care of by get_subcurve
|
# Family is already taken care of by get_subcurve
|
||||||
# implementation
|
# implementation
|
||||||
self.match_style(vmobject, recurse=False)
|
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 itertools as it
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import os
|
|
||||||
import platform
|
|
||||||
import pyperclip
|
import pyperclip
|
||||||
|
|
||||||
from manimlib.animation.fading import FadeIn
|
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 COMMAND_MODIFIER, SHIFT_MODIFIER
|
||||||
from manimlib.constants import DL, DOWN, DR, LEFT, ORIGIN, RIGHT, UL, UP, UR
|
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 FRAME_WIDTH, SMALL_BUFF
|
||||||
from manimlib.constants import MANIM_COLORS, WHITE, YELLOW
|
from manimlib.constants import MANIM_COLORS, WHITE, GREY_C
|
||||||
from manimlib.logger import log
|
|
||||||
from manimlib.mobject.geometry import Rectangle
|
from manimlib.mobject.geometry import Rectangle
|
||||||
from manimlib.mobject.geometry import Square
|
from manimlib.mobject.geometry import Square
|
||||||
from manimlib.mobject.mobject import Group
|
from manimlib.mobject.mobject import Group
|
||||||
from manimlib.mobject.mobject import Mobject
|
from manimlib.mobject.mobject import Mobject
|
||||||
|
from manimlib.mobject.numbers import DecimalNumber
|
||||||
from manimlib.mobject.svg.tex_mobject import Tex
|
from manimlib.mobject.svg.tex_mobject import Tex
|
||||||
from manimlib.mobject.svg.text_mobject import Text
|
from manimlib.mobject.svg.text_mobject import Text
|
||||||
from manimlib.mobject.types.dot_cloud import DotCloud
|
from manimlib.mobject.types.dot_cloud import DotCloud
|
||||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
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.mobject.types.vectorized_mobject import VMobject
|
||||||
from manimlib.scene.scene import Scene
|
from manimlib.scene.scene import Scene
|
||||||
from manimlib.utils.family_ops import extract_mobject_family_members
|
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'
|
SELECT_KEY = 's'
|
||||||
GRAB_KEY = 'g'
|
GRAB_KEY = 'g'
|
||||||
HORIZONTAL_GRAB_KEY = 'h'
|
X_GRAB_KEY = 'h'
|
||||||
VERTICAL_GRAB_KEY = 'v'
|
Y_GRAB_KEY = 'v'
|
||||||
|
GRAB_KEYS = [GRAB_KEY, X_GRAB_KEY, Y_GRAB_KEY]
|
||||||
RESIZE_KEY = 't'
|
RESIZE_KEY = 't'
|
||||||
COLOR_KEY = 'c'
|
COLOR_KEY = 'c'
|
||||||
|
CURSOR_LOCATION_KEY = 'l'
|
||||||
|
|
||||||
|
|
||||||
# Note, a lot of the functionality here is still buggy and very much a work in progress.
|
# Note, a lot of the functionality here is still buggy and very much a work in progress.
|
||||||
|
|
||||||
|
|
||||||
class InteractiveScene(Scene):
|
class InteractiveScene(Scene):
|
||||||
"""
|
"""
|
||||||
To select mobjects on screen, hold ctrl and move the mouse to highlight a region,
|
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
|
selection_rectangle_stroke_width = 1.0
|
||||||
colors = MANIM_COLORS
|
colors = MANIM_COLORS
|
||||||
selection_nudge_size = 0.05
|
selection_nudge_size = 0.05
|
||||||
|
cursor_location_config = dict(
|
||||||
|
font_size=14,
|
||||||
|
fill_color=GREY_C,
|
||||||
|
num_decimal_places=3,
|
||||||
|
)
|
||||||
|
|
||||||
def setup(self):
|
def setup(self):
|
||||||
self.selection = Group()
|
self.selection = Group()
|
||||||
self.selection_highlight = Group()
|
self.selection_highlight = Group()
|
||||||
self.selection_rectangle = self.get_selection_rectangle()
|
self.selection_rectangle = self.get_selection_rectangle()
|
||||||
self.color_palette = self.get_color_palette()
|
self.color_palette = self.get_color_palette()
|
||||||
|
self.cursor_location_label = self.get_cursor_location_label()
|
||||||
self.unselectables = [
|
self.unselectables = [
|
||||||
self.selection,
|
self.selection,
|
||||||
self.selection_highlight,
|
self.selection_highlight,
|
||||||
self.selection_rectangle,
|
self.selection_rectangle,
|
||||||
|
self.cursor_location_label,
|
||||||
self.camera.frame
|
self.camera.frame
|
||||||
]
|
]
|
||||||
self.saved_selection_state = []
|
|
||||||
self.select_top_level_mobs = True
|
self.select_top_level_mobs = True
|
||||||
|
self.regenerate_selection_search_set()
|
||||||
|
|
||||||
self.is_selecting = False
|
self.is_selecting = False
|
||||||
|
self.is_grabbing = False
|
||||||
self.add(self.selection_highlight)
|
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):
|
def toggle_selection_mode(self):
|
||||||
self.select_top_level_mobs = not self.select_top_level_mobs
|
self.select_top_level_mobs = not self.select_top_level_mobs
|
||||||
self.refresh_selection_scope()
|
self.refresh_selection_scope()
|
||||||
|
self.regenerate_selection_search_set()
|
||||||
|
|
||||||
def get_selection_search_set(self):
|
def get_selection_search_set(self) -> list[Mobject]:
|
||||||
mobs = [m for m in self.mobjects if m not in self.unselectables]
|
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:
|
if self.select_top_level_mobs:
|
||||||
return mobs
|
self.selection_search_set = selectable
|
||||||
else:
|
else:
|
||||||
return [
|
self.selection_search_set = [
|
||||||
submob
|
submob
|
||||||
for mob in mobs
|
for mob in selectable
|
||||||
for submob in mob.family_members_with_points()
|
for submob in mob.family_members_with_points()
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -116,37 +184,7 @@ class InteractiveScene(Scene):
|
||||||
)
|
)
|
||||||
self.refresh_selection_highlight()
|
self.refresh_selection_highlight()
|
||||||
|
|
||||||
def get_selection_rectangle(self):
|
def get_corner_dots(self, mobject: Mobject) -> Mobject:
|
||||||
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):
|
|
||||||
dots = DotCloud(**self.corner_dot_config)
|
dots = DotCloud(**self.corner_dot_config)
|
||||||
radius = self.corner_dot_config["radius"]
|
radius = self.corner_dot_config["radius"]
|
||||||
if mobject.get_depth() < 1e-2:
|
if mobject.get_depth() < 1e-2:
|
||||||
|
@ -159,9 +197,11 @@ class InteractiveScene(Scene):
|
||||||
]))
|
]))
|
||||||
return dots
|
return dots
|
||||||
|
|
||||||
def get_highlight(self, mobject):
|
def get_highlight(self, mobject: Mobject) -> Mobject:
|
||||||
if isinstance(mobject, VMobject) and mobject.has_points():
|
if isinstance(mobject, VMobject) and mobject.has_points() and not self.select_top_level_mobs:
|
||||||
return self.get_stroke_highlight(mobject)
|
result = VHighlight(mobject)
|
||||||
|
result.add_updater(lambda m: m.replace(mobject))
|
||||||
|
return result
|
||||||
else:
|
else:
|
||||||
return self.get_corner_dots(mobject)
|
return self.get_corner_dots(mobject)
|
||||||
|
|
||||||
|
@ -171,40 +211,54 @@ class InteractiveScene(Scene):
|
||||||
for mob in self.selection
|
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):
|
def add_to_selection(self, *mobjects):
|
||||||
mobs = list(filter(lambda m: m not in self.unselectables, mobjects))
|
mobs = list(filter(
|
||||||
self.selection.add(*mobjects)
|
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.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):
|
def toggle_from_selection(self, *mobjects):
|
||||||
for mob in mobjects:
|
for mob in mobjects:
|
||||||
if mob in self.selection:
|
if mob in self.selection:
|
||||||
self.selection.remove(mob)
|
self.selection.remove(mob)
|
||||||
|
mob.set_animating_status(False)
|
||||||
else:
|
else:
|
||||||
self.add_to_selection(mob)
|
self.add_to_selection(mob)
|
||||||
self.refresh_selection_highlight()
|
self.refresh_selection_highlight()
|
||||||
|
|
||||||
def clear_selection(self):
|
def clear_selection(self):
|
||||||
|
for mob in self.selection:
|
||||||
|
mob.set_animating_status(False)
|
||||||
self.selection.set_submobjects([])
|
self.selection.set_submobjects([])
|
||||||
self.selection_highlight.set_submobjects([])
|
self.selection_highlight.set_submobjects([])
|
||||||
|
self.refresh_static_mobjects()
|
||||||
|
|
||||||
def add(self, *new_mobjects: Mobject):
|
def add(self, *new_mobjects: Mobject):
|
||||||
for mob in new_mobjects:
|
|
||||||
mob.make_movable()
|
|
||||||
super().add(*new_mobjects)
|
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):
|
def copy_selection(self):
|
||||||
ids = map(id, self.selection)
|
ids = map(id, self.selection)
|
||||||
|
@ -218,11 +272,11 @@ class InteractiveScene(Scene):
|
||||||
mobs = map(self.id_to_mobject, ids)
|
mobs = map(self.id_to_mobject, ids)
|
||||||
mob_copies = [m.copy() for m in mobs if m is not None]
|
mob_copies = [m.copy() for m in mobs if m is not None]
|
||||||
self.clear_selection()
|
self.clear_selection()
|
||||||
self.add_to_selection(*mob_copies)
|
|
||||||
self.play(*(
|
self.play(*(
|
||||||
FadeIn(mc, run_time=0.5, scale=1.5)
|
FadeIn(mc, run_time=0.5, scale=1.5)
|
||||||
for mc in mob_copies
|
for mc in mob_copies
|
||||||
))
|
))
|
||||||
|
self.add_to_selection(*mob_copies)
|
||||||
return
|
return
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
@ -242,41 +296,27 @@ class InteractiveScene(Scene):
|
||||||
self.remove(*self.selection)
|
self.remove(*self.selection)
|
||||||
self.clear_selection()
|
self.clear_selection()
|
||||||
|
|
||||||
def saved_selection_to_file(self):
|
def restore_state(self, mobject_states: list[tuple[Mobject, Mobject]]):
|
||||||
directory = self.file_writer.get_saved_mobject_directory()
|
super().restore_state(mobject_states)
|
||||||
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)
|
|
||||||
self.refresh_selection_highlight()
|
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):
|
def prepare_resizing(self, about_corner=False):
|
||||||
center = self.selection.get_center()
|
center = self.selection.get_center()
|
||||||
mp = self.mouse_point.get_center()
|
mp = self.mouse_point.get_center()
|
||||||
|
@ -286,136 +326,177 @@ class InteractiveScene(Scene):
|
||||||
self.scale_about_point = center
|
self.scale_about_point = center
|
||||||
self.scale_ref_vect = mp - self.scale_about_point
|
self.scale_ref_vect = mp - self.scale_about_point
|
||||||
self.scale_ref_width = self.selection.get_width()
|
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:
|
def on_key_press(self, symbol: int, modifiers: int) -> None:
|
||||||
super().on_key_press(symbol, modifiers)
|
super().on_key_press(symbol, modifiers)
|
||||||
char = chr(symbol)
|
char = chr(symbol)
|
||||||
# Enable selection
|
|
||||||
if char == SELECT_KEY and modifiers == 0:
|
if char == SELECT_KEY and modifiers == 0:
|
||||||
self.is_selecting = True
|
self.enable_selection()
|
||||||
self.add(self.selection_rectangle)
|
elif char in GRAB_KEYS and modifiers == 0:
|
||||||
self.selection_rectangle.fixed_corner = self.mouse_point.get_center().copy()
|
self.prepare_grab()
|
||||||
# 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
|
|
||||||
elif char == RESIZE_KEY and modifiers in [0, SHIFT_MODIFIER]:
|
elif char == RESIZE_KEY and modifiers in [0, SHIFT_MODIFIER]:
|
||||||
self.prepare_resizing(about_corner=(modifiers == SHIFT_MODIFIER))
|
self.prepare_resizing(about_corner=(modifiers == SHIFT_MODIFIER))
|
||||||
elif symbol == SHIFT_SYMBOL:
|
elif symbol == SHIFT_SYMBOL:
|
||||||
if self.window.is_key_pressed(ord("t")):
|
if self.window.is_key_pressed(ord("t")):
|
||||||
self.prepare_resizing(about_corner=True)
|
self.prepare_resizing(about_corner=True)
|
||||||
# Show color palette
|
|
||||||
elif char == COLOR_KEY and modifiers == 0:
|
elif char == COLOR_KEY and modifiers == 0:
|
||||||
if len(self.selection) == 0:
|
self.toggle_color_palette()
|
||||||
return
|
elif char == CURSOR_LOCATION_KEY and modifiers == 0:
|
||||||
if self.color_palette not in self.mobjects:
|
self.add(self.cursor_location_label)
|
||||||
self.add(self.color_palette)
|
|
||||||
else:
|
|
||||||
self.remove(self.color_palette)
|
|
||||||
# Command + c -> Copy mobject ids to clipboard
|
|
||||||
elif char == "c" and modifiers == COMMAND_MODIFIER:
|
elif char == "c" and modifiers == COMMAND_MODIFIER:
|
||||||
self.copy_selection()
|
self.copy_selection()
|
||||||
# Command + v -> Paste
|
|
||||||
elif char == "v" and modifiers == COMMAND_MODIFIER:
|
elif char == "v" and modifiers == COMMAND_MODIFIER:
|
||||||
self.paste_selection()
|
self.paste_selection()
|
||||||
# Command + x -> Cut
|
|
||||||
elif char == "x" and modifiers == COMMAND_MODIFIER:
|
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.copy_selection()
|
||||||
self.delete_selection()
|
self.delete_selection()
|
||||||
# Delete
|
|
||||||
elif symbol == DELETE_SYMBOL:
|
elif symbol == DELETE_SYMBOL:
|
||||||
self.delete_selection()
|
self.delete_selection()
|
||||||
# Command + a -> Select all
|
|
||||||
elif char == "a" and modifiers == COMMAND_MODIFIER:
|
elif char == "a" and modifiers == COMMAND_MODIFIER:
|
||||||
self.clear_selection()
|
self.clear_selection()
|
||||||
self.add_to_selection(*self.mobjects)
|
self.add_to_selection(*self.mobjects)
|
||||||
# Command + g -> Group selection
|
|
||||||
elif char == "g" and modifiers == COMMAND_MODIFIER:
|
elif char == "g" and modifiers == COMMAND_MODIFIER:
|
||||||
group = self.get_group(*self.selection)
|
self.group_selection()
|
||||||
self.add(group)
|
|
||||||
self.clear_selection()
|
|
||||||
self.add_to_selection(group)
|
|
||||||
# Command + shift + g -> Ungroup the selection
|
|
||||||
elif char == "g" and modifiers == COMMAND_MODIFIER | SHIFT_MODIFIER:
|
elif char == "g" and modifiers == COMMAND_MODIFIER | SHIFT_MODIFIER:
|
||||||
pieces = []
|
self.ungroup_selection()
|
||||||
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
|
|
||||||
elif char == "t" and modifiers == COMMAND_MODIFIER:
|
elif char == "t" and modifiers == COMMAND_MODIFIER:
|
||||||
self.toggle_selection_mode()
|
self.toggle_selection_mode()
|
||||||
# Command + z -> Restore selection to original state
|
|
||||||
elif char == "z" and modifiers == COMMAND_MODIFIER:
|
elif char == "z" and modifiers == COMMAND_MODIFIER:
|
||||||
self.undo()
|
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:
|
elif char == "s" and modifiers == COMMAND_MODIFIER:
|
||||||
self.saved_selection_to_file()
|
self.save_selection_to_file()
|
||||||
# Keyboard movements
|
|
||||||
elif symbol in ARROW_SYMBOLS:
|
elif symbol in ARROW_SYMBOLS:
|
||||||
nudge = self.selection_nudge_size
|
self.nudge_selection(
|
||||||
if (modifiers & SHIFT_MODIFIER):
|
vect=[LEFT, UP, RIGHT, DOWN][ARROW_SYMBOLS.index(symbol)],
|
||||||
nudge *= 10
|
large=(modifiers & SHIFT_MODIFIER),
|
||||||
vect = [LEFT, UP, RIGHT, DOWN][ARROW_SYMBOLS.index(symbol)]
|
)
|
||||||
self.selection.shift(nudge * vect)
|
|
||||||
|
# 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:
|
def on_key_release(self, symbol: int, modifiers: int) -> None:
|
||||||
super().on_key_release(symbol, modifiers)
|
super().on_key_release(symbol, modifiers)
|
||||||
if chr(symbol) == SELECT_KEY:
|
if chr(symbol) == SELECT_KEY:
|
||||||
self.is_selecting = False
|
self.gather_new_selection()
|
||||||
self.remove(self.selection_rectangle)
|
if chr(symbol) in GRAB_KEYS:
|
||||||
for mob in reversed(self.get_selection_search_set()):
|
self.is_grabbing = False
|
||||||
if mob.is_movable() and self.selection_rectangle.is_touching(mob):
|
elif chr(symbol) == CURSOR_LOCATION_KEY:
|
||||||
self.add_to_selection(mob)
|
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:
|
# Mouse actions
|
||||||
if self.window.is_key_pressed(ord(RESIZE_KEY)):
|
def handle_grabbing(self, point: np.ndarray):
|
||||||
self.prepare_resizing(about_corner=False)
|
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:
|
def handle_resizing(self, point: np.ndarray):
|
||||||
super().on_mouse_motion(point, d_point)
|
vect = point - self.scale_about_point
|
||||||
# Move selection
|
if self.window.is_key_pressed(CTRL_SYMBOL):
|
||||||
if self.window.is_key_pressed(ord("g")):
|
for i in (0, 1):
|
||||||
self.selection.move_to(point - self.mouse_to_selection)
|
scalar = vect[i] / self.scale_ref_vect[i]
|
||||||
# Move selection restricted to horizontal
|
self.selection.rescale_to_fit(
|
||||||
elif self.window.is_key_pressed(ord("h")):
|
scalar * [self.scale_ref_width, self.scale_ref_height][i],
|
||||||
self.selection.set_x((point - self.mouse_to_selection)[0])
|
dim=i,
|
||||||
# Move selection restricted to vertical
|
about_point=self.scale_about_point,
|
||||||
elif self.window.is_key_pressed(ord("v")):
|
stretch=True,
|
||||||
self.selection.set_y((point - self.mouse_to_selection)[1])
|
)
|
||||||
# Scale selection
|
else:
|
||||||
elif self.window.is_key_pressed(ord("t")):
|
|
||||||
# TODO, allow for scaling about the opposite corner
|
|
||||||
vect = point - self.scale_about_point
|
|
||||||
scalar = get_norm(vect) / get_norm(self.scale_ref_vect)
|
scalar = get_norm(vect) / get_norm(self.scale_ref_vect)
|
||||||
self.selection.set_width(
|
self.selection.set_width(
|
||||||
scalar * self.scale_ref_width,
|
scalar * self.scale_ref_width,
|
||||||
about_point=self.scale_about_point
|
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:
|
def on_mouse_release(self, point: np.ndarray, button: int, mods: int) -> None:
|
||||||
super().on_mouse_release(point, button, mods)
|
super().on_mouse_release(point, button, mods)
|
||||||
if self.color_palette in self.mobjects:
|
if self.color_palette in self.mobjects:
|
||||||
# Search through all mobject on the screne, not just the palette
|
self.choose_color(point)
|
||||||
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)
|
|
||||||
elif self.window.is_key_pressed(SHIFT_SYMBOL):
|
elif self.window.is_key_pressed(SHIFT_SYMBOL):
|
||||||
mob = self.point_to_mobject(point)
|
self.toggle_clicked_mobject_from_selection(point)
|
||||||
if mob is not None:
|
|
||||||
self.toggle_from_selection(mob)
|
|
||||||
else:
|
else:
|
||||||
self.clear_selection()
|
self.clear_selection()
|
||||||
|
|
|
@ -14,10 +14,9 @@ from tqdm import tqdm as ProgressDisplay
|
||||||
from manimlib.animation.animation import prepare_animation
|
from manimlib.animation.animation import prepare_animation
|
||||||
from manimlib.animation.transform import MoveToTarget
|
from manimlib.animation.transform import MoveToTarget
|
||||||
from manimlib.camera.camera import Camera
|
from manimlib.camera.camera import Camera
|
||||||
from manimlib.config import get_custom_config
|
|
||||||
from manimlib.constants import ARROW_SYMBOLS
|
from manimlib.constants import ARROW_SYMBOLS
|
||||||
from manimlib.constants import DEFAULT_WAIT_TIME
|
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 import EVENT_DISPATCHER
|
||||||
from manimlib.event_handler.event_type import EventType
|
from manimlib.event_handler.event_type import EventType
|
||||||
from manimlib.logger import log
|
from manimlib.logger import log
|
||||||
|
@ -46,7 +45,6 @@ FRAME_SHIFT_KEY = 'f'
|
||||||
ZOOM_KEY = 'z'
|
ZOOM_KEY = 'z'
|
||||||
RESET_FRAME_KEY = 'r'
|
RESET_FRAME_KEY = 'r'
|
||||||
QUIT_KEY = 'q'
|
QUIT_KEY = 'q'
|
||||||
EMBED_KEY = 'e'
|
|
||||||
|
|
||||||
|
|
||||||
class Scene(object):
|
class Scene(object):
|
||||||
|
@ -65,6 +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,
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
|
@ -74,12 +73,15 @@ class Scene(object):
|
||||||
self.window = Window(scene=self, **self.window_config)
|
self.window = Window(scene=self, **self.window_config)
|
||||||
self.camera_config["ctx"] = self.window.ctx
|
self.camera_config["ctx"] = self.window.ctx
|
||||||
self.camera_config["frame_rate"] = 30 # Where's that 30 from?
|
self.camera_config["frame_rate"] = 30 # Where's that 30 from?
|
||||||
|
self.undo_stack = []
|
||||||
|
self.redo_stack = []
|
||||||
else:
|
else:
|
||||||
self.window = None
|
self.window = None
|
||||||
|
|
||||||
self.camera: Camera = self.camera_class(**self.camera_config)
|
self.camera: Camera = self.camera_class(**self.camera_config)
|
||||||
self.file_writer = SceneFileWriter(self, **self.file_writer_config)
|
self.file_writer = SceneFileWriter(self, **self.file_writer_config)
|
||||||
self.mobjects: list[Mobject] = [self.camera.frame]
|
self.mobjects: list[Mobject] = [self.camera.frame]
|
||||||
|
self.id_to_mobject_map: dict[int, Mobject] = dict()
|
||||||
self.num_plays: int = 0
|
self.num_plays: int = 0
|
||||||
self.time: float = 0
|
self.time: float = 0
|
||||||
self.skip_time: float = 0
|
self.skip_time: float = 0
|
||||||
|
@ -91,12 +93,16 @@ class Scene(object):
|
||||||
self.mouse_point = Point()
|
self.mouse_point = Point()
|
||||||
self.mouse_drag_point = Point()
|
self.mouse_drag_point = Point()
|
||||||
self.hold_on_wait = self.presenter_mode
|
self.hold_on_wait = self.presenter_mode
|
||||||
|
self.inside_embed = False
|
||||||
|
|
||||||
# Much nicer to work with deterministic scenes
|
# Much nicer to work with deterministic scenes
|
||||||
if self.random_seed is not None:
|
if self.random_seed is not None:
|
||||||
random.seed(self.random_seed)
|
random.seed(self.random_seed)
|
||||||
np.random.seed(self.random_seed)
|
np.random.seed(self.random_seed)
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return self.__class__.__name__
|
||||||
|
|
||||||
def run(self) -> None:
|
def run(self) -> None:
|
||||||
self.virtual_animation_start_time: float = 0
|
self.virtual_animation_start_time: float = 0
|
||||||
self.real_animation_start_time: float = time.time()
|
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:
|
def embed(self, close_scene_on_exit: bool = True) -> None:
|
||||||
if not self.preview:
|
if not self.preview:
|
||||||
# If the scene is just being
|
# Ignore embed calls when there is no preview
|
||||||
# written, ignore embed calls
|
|
||||||
return
|
return
|
||||||
|
self.inside_embed = True
|
||||||
self.stop_skipping()
|
self.stop_skipping()
|
||||||
self.linger_after_completion = False
|
self.linger_after_completion = False
|
||||||
self.update_frame()
|
self.update_frame()
|
||||||
|
|
||||||
# Save scene state at the point of embedding
|
|
||||||
self.save_state()
|
self.save_state()
|
||||||
|
|
||||||
from IPython.terminal.embed import InteractiveShellEmbed
|
# Configure and launch embedded IPython terminal
|
||||||
shell = InteractiveShellEmbed()
|
from IPython.terminal import embed, pt_inputhooks
|
||||||
# Have the frame update after each command
|
shell = embed.InteractiveShellEmbed.instance()
|
||||||
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 namespace of the caller
|
||||||
# Use the locals of the caller as the local namespace
|
|
||||||
# once embedded, and add a few custom shortcuts
|
|
||||||
local_ns = inspect.currentframe().f_back.f_locals
|
local_ns = inspect.currentframe().f_back.f_locals
|
||||||
local_ns["touch"] = self.interact
|
# Add a few custom shortcuts
|
||||||
local_ns["i2g"] = self.ids_to_group
|
local_ns.update({
|
||||||
for term in ("play", "wait", "add", "remove", "clear", "save_state", "restore"):
|
name: getattr(self, name)
|
||||||
local_ns[term] = getattr(self, term)
|
for name in [
|
||||||
log.info("Tips: Now the embed iPython terminal is open. But you can't interact with"
|
"play", "wait", "add", "remove", "clear",
|
||||||
" the window directly. To do so, you need to type `touch()` or `self.interact()`")
|
"save_state", "undo", "redo", "i2g", "i2m"
|
||||||
exec(get_custom_config()["universal_import_line"])
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
# 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)
|
shell(local_ns=local_ns, stack_depth=2)
|
||||||
|
|
||||||
|
self.inside_embed = False
|
||||||
# End scene when exiting an embed
|
# End scene when exiting an embed
|
||||||
if close_scene_on_exit:
|
if close_scene_on_exit:
|
||||||
raise EndSceneEarlyException()
|
raise EndSceneEarlyException()
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return self.__class__.__name__
|
|
||||||
|
|
||||||
# Only these methods should touch the camera
|
# Only these methods should touch the camera
|
||||||
|
|
||||||
def get_image(self) -> Image:
|
def get_image(self) -> Image:
|
||||||
return self.camera.get_image()
|
return self.camera.get_image()
|
||||||
|
|
||||||
|
@ -210,6 +233,7 @@ class Scene(object):
|
||||||
self.file_writer.write_frame(self.camera)
|
self.file_writer.write_frame(self.camera)
|
||||||
|
|
||||||
# Related to updating
|
# Related to updating
|
||||||
|
|
||||||
def update_mobjects(self, dt: float) -> None:
|
def update_mobjects(self, dt: float) -> None:
|
||||||
for mobject in self.mobjects:
|
for mobject in self.mobjects:
|
||||||
mobject.update(dt)
|
mobject.update(dt)
|
||||||
|
@ -228,6 +252,7 @@ class Scene(object):
|
||||||
])
|
])
|
||||||
|
|
||||||
# Related to time
|
# Related to time
|
||||||
|
|
||||||
def get_time(self) -> float:
|
def get_time(self) -> float:
|
||||||
return self.time
|
return self.time
|
||||||
|
|
||||||
|
@ -235,6 +260,7 @@ class Scene(object):
|
||||||
self.time += dt
|
self.time += dt
|
||||||
|
|
||||||
# Related to internal mobject organization
|
# Related to internal mobject organization
|
||||||
|
|
||||||
def get_top_level_mobjects(self) -> list[Mobject]:
|
def get_top_level_mobjects(self) -> list[Mobject]:
|
||||||
# Return only those which are not in the family
|
# Return only those which are not in the family
|
||||||
# of another mobject from the scene
|
# of another mobject from the scene
|
||||||
|
@ -259,6 +285,11 @@ class Scene(object):
|
||||||
"""
|
"""
|
||||||
self.remove(*new_mobjects)
|
self.remove(*new_mobjects)
|
||||||
self.mobjects += 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
|
return self
|
||||||
|
|
||||||
def add_mobjects_among(self, values: Iterable):
|
def add_mobjects_among(self, values: Iterable):
|
||||||
|
@ -322,11 +353,7 @@ class Scene(object):
|
||||||
return Group(*mobjects)
|
return Group(*mobjects)
|
||||||
|
|
||||||
def id_to_mobject(self, id_value):
|
def id_to_mobject(self, id_value):
|
||||||
for mob in self.mobjects:
|
return self.id_to_mobject_map[id_value]
|
||||||
for sm in mob.get_family():
|
|
||||||
if id(sm) == id_value:
|
|
||||||
return sm
|
|
||||||
return None
|
|
||||||
|
|
||||||
def ids_to_group(self, *id_values):
|
def ids_to_group(self, *id_values):
|
||||||
return self.get_group(*filter(
|
return self.get_group(*filter(
|
||||||
|
@ -334,7 +361,14 @@ class Scene(object):
|
||||||
map(self.id_to_mobject, id_values)
|
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
|
# Related to skipping
|
||||||
|
|
||||||
def update_skipping_status(self) -> None:
|
def update_skipping_status(self) -> None:
|
||||||
if self.start_at_animation_number is not None:
|
if self.start_at_animation_number is not None:
|
||||||
if self.num_plays == self.start_at_animation_number:
|
if self.num_plays == self.start_at_animation_number:
|
||||||
|
@ -350,6 +384,7 @@ class Scene(object):
|
||||||
self.skip_animations = False
|
self.skip_animations = False
|
||||||
|
|
||||||
# Methods associated with running animations
|
# Methods associated with running animations
|
||||||
|
|
||||||
def get_time_progression(
|
def get_time_progression(
|
||||||
self,
|
self,
|
||||||
run_time: float,
|
run_time: float,
|
||||||
|
@ -473,6 +508,8 @@ class Scene(object):
|
||||||
def handle_play_like_call(func):
|
def handle_play_like_call(func):
|
||||||
@wraps(func)
|
@wraps(func)
|
||||||
def wrapper(self, *args, **kwargs):
|
def wrapper(self, *args, **kwargs):
|
||||||
|
if self.inside_embed:
|
||||||
|
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:
|
||||||
|
@ -594,24 +631,39 @@ class Scene(object):
|
||||||
self.file_writer.add_sound(sound_file, time, gain, gain_to_background)
|
self.file_writer.add_sound(sound_file, time, gain, gain_to_background)
|
||||||
|
|
||||||
# Helpers for interactive development
|
# 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:
|
def save_state(self) -> None:
|
||||||
self.saved_state = [
|
if not self.preview:
|
||||||
(mob, mob.copy())
|
return
|
||||||
for mob in self.mobjects
|
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:
|
def undo(self):
|
||||||
if not hasattr(self, "saved_state"):
|
if self.undo_stack:
|
||||||
raise Exception("Trying to restore scene without having saved")
|
self.redo_stack.append(self.get_state())
|
||||||
self.mobjects = []
|
self.restore_state(self.undo_stack.pop())
|
||||||
for mob, mob_state in self.saved_state:
|
self.refresh_static_mobjects()
|
||||||
mob.become(mob_state)
|
|
||||||
self.mobjects.append(mob)
|
|
||||||
|
|
||||||
def save_mobect(self, mobject: Mobject, file_name: str):
|
def redo(self):
|
||||||
directory = self.file_writer.get_saved_mobject_directory()
|
if self.redo_stack:
|
||||||
path = os.path.join(directory, file_name)
|
self.undo_stack.append(self.get_state())
|
||||||
mobject.save_to_file(path)
|
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):
|
def load_mobject(self, file_name):
|
||||||
if os.path.exists(file_name):
|
if os.path.exists(file_name):
|
||||||
|
@ -739,9 +791,6 @@ class Scene(object):
|
||||||
# Space or right arrow
|
# Space or right arrow
|
||||||
elif char == " " or symbol == ARROW_SYMBOLS[2]:
|
elif char == " " or symbol == ARROW_SYMBOLS[2]:
|
||||||
self.hold_on_wait = False
|
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:
|
def on_resize(self, width: int, height: int) -> None:
|
||||||
self.camera.reset_pixel_shape(width, height)
|
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.constants import FFMPEG_BIN
|
||||||
from manimlib.logger import log
|
from manimlib.logger import log
|
||||||
|
from manimlib.mobject.mobject import Mobject
|
||||||
from manimlib.utils.config_ops import digest_config
|
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 add_extension_if_not_present
|
||||||
from manimlib.utils.file_ops import get_sorted_integer_files
|
from manimlib.utils.file_ops import get_sorted_integer_files
|
||||||
|
@ -61,7 +62,7 @@ class SceneFileWriter(object):
|
||||||
|
|
||||||
# Output directories and files
|
# Output directories and files
|
||||||
def init_output_directories(self) -> None:
|
def init_output_directories(self) -> None:
|
||||||
out_dir = self.output_directory
|
out_dir = self.output_directory or ""
|
||||||
if self.mirror_module_path:
|
if self.mirror_module_path:
|
||||||
module_dir = self.get_default_module_directory()
|
module_dir = self.get_default_module_directory()
|
||||||
out_dir = os.path.join(out_dir, module_dir)
|
out_dir = os.path.join(out_dir, module_dir)
|
||||||
|
@ -128,6 +129,36 @@ class SceneFileWriter(object):
|
||||||
str(self.scene),
|
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
|
# Sound
|
||||||
def init_audio(self) -> None:
|
def init_audio(self) -> None:
|
||||||
self.includes_sound: bool = False
|
self.includes_sound: bool = False
|
||||||
|
|
Loading…
Add table
Reference in a new issue