2022-02-14 21:22:18 +08:00
|
|
|
from __future__ import annotations
|
|
|
|
|
2022-05-02 11:11:18 -07:00
|
|
|
from collections import OrderedDict
|
2022-02-14 21:22:18 +08:00
|
|
|
import inspect
|
2022-04-20 21:50:37 -07:00
|
|
|
import os
|
2022-04-12 19:19:59 +08:00
|
|
|
import platform
|
2022-04-27 09:54:29 -07:00
|
|
|
import pyperclip
|
2022-04-12 19:19:59 +08:00
|
|
|
import random
|
|
|
|
import time
|
2018-03-31 15:11:35 -07:00
|
|
|
|
2022-12-19 10:51:26 -08:00
|
|
|
from IPython.terminal import pt_inputhooks
|
|
|
|
from IPython.terminal.embed import InteractiveShellEmbed
|
|
|
|
from IPython.core.getipython import get_ipython
|
|
|
|
|
2018-12-24 12:37:51 -08:00
|
|
|
import numpy as np
|
2023-01-12 12:03:14 +01:00
|
|
|
from tqdm.auto import tqdm as ProgressDisplay
|
2015-03-22 16:15:29 -06:00
|
|
|
|
2021-02-10 07:43:46 -06:00
|
|
|
from manimlib.animation.animation import prepare_animation
|
2022-12-29 15:08:57 -08:00
|
|
|
from manimlib.animation.fading import VFadeInThenOut
|
2018-12-24 12:37:51 -08:00
|
|
|
from manimlib.camera.camera import Camera
|
2023-01-28 12:02:19 -08:00
|
|
|
from manimlib.camera.camera_frame import CameraFrame
|
2022-12-18 11:11:08 -08:00
|
|
|
from manimlib.config import get_module
|
2022-04-20 21:50:37 -07:00
|
|
|
from manimlib.constants import ARROW_SYMBOLS
|
2021-01-05 23:14:16 -08:00
|
|
|
from manimlib.constants import DEFAULT_WAIT_TIME
|
2022-04-22 11:46:18 -07:00
|
|
|
from manimlib.constants import COMMAND_MODIFIER
|
2022-04-23 09:20:44 -07:00
|
|
|
from manimlib.constants import SHIFT_MODIFIER
|
2022-12-29 15:08:57 -08:00
|
|
|
from manimlib.constants import RED
|
2022-04-12 19:19:59 +08:00
|
|
|
from manimlib.event_handler import EVENT_DISPATCHER
|
|
|
|
from manimlib.event_handler.event_type import EventType
|
|
|
|
from manimlib.logger import log
|
2022-12-29 15:08:57 -08:00
|
|
|
from manimlib.mobject.frame import FullScreenRectangle
|
2022-05-02 11:10:57 -07:00
|
|
|
from manimlib.mobject.mobject import _AnimationBuilder
|
2022-04-22 16:42:45 +08:00
|
|
|
from manimlib.mobject.mobject import Group
|
2018-12-24 12:37:51 -08:00
|
|
|
from manimlib.mobject.mobject import Mobject
|
2021-10-16 13:04:52 +08:00
|
|
|
from manimlib.mobject.mobject import Point
|
2022-04-20 21:50:37 -07:00
|
|
|
from manimlib.mobject.types.vectorized_mobject import VGroup
|
2022-04-22 16:42:45 +08:00
|
|
|
from manimlib.mobject.types.vectorized_mobject import VMobject
|
2019-01-24 21:47:40 -08:00
|
|
|
from manimlib.scene.scene_file_writer import SceneFileWriter
|
2020-01-15 18:30:58 -08:00
|
|
|
from manimlib.utils.family_ops import extract_mobject_family_members
|
2022-12-26 07:46:40 -07:00
|
|
|
from manimlib.utils.family_ops import recursive_mobject_remove
|
2018-04-06 13:58:59 -07:00
|
|
|
|
2022-02-14 21:22:18 +08:00
|
|
|
from typing import TYPE_CHECKING
|
2022-02-16 21:08:25 +08:00
|
|
|
|
2022-02-14 21:22:18 +08:00
|
|
|
if TYPE_CHECKING:
|
2022-04-12 19:19:59 +08:00
|
|
|
from typing import Callable, Iterable
|
2023-01-28 22:40:57 -08:00
|
|
|
from manimlib.typing import Vect3
|
2022-04-12 19:19:59 +08:00
|
|
|
|
2022-02-14 21:22:18 +08:00
|
|
|
from PIL.Image import Image
|
2022-04-12 19:19:59 +08:00
|
|
|
|
2022-02-14 21:22:18 +08:00
|
|
|
from manimlib.animation.animation import Animation
|
|
|
|
|
2018-12-27 09:41:41 -08:00
|
|
|
|
2022-04-20 21:50:37 -07:00
|
|
|
PAN_3D_KEY = 'd'
|
|
|
|
FRAME_SHIFT_KEY = 'f'
|
|
|
|
RESET_FRAME_KEY = 'r'
|
|
|
|
QUIT_KEY = 'q'
|
|
|
|
|
|
|
|
|
2021-01-03 12:29:05 -08:00
|
|
|
class Scene(object):
|
2022-12-14 17:01:46 -08:00
|
|
|
random_seed: int = 0
|
2023-01-28 22:30:46 -08:00
|
|
|
pan_sensitivity: float = 0.5
|
2023-01-30 15:03:05 -08:00
|
|
|
scroll_sensitivity: float = 20
|
2022-12-14 17:01:46 -08:00
|
|
|
max_num_saved_states: int = 50
|
2022-12-19 11:35:43 -08:00
|
|
|
default_camera_config: dict = dict()
|
|
|
|
default_window_config: dict = dict()
|
|
|
|
default_file_writer_config: dict = dict()
|
2023-01-20 16:30:39 -08:00
|
|
|
samples = 0
|
2023-01-28 12:02:19 -08:00
|
|
|
# Euler angles, in degrees
|
|
|
|
default_frame_orientation = (0, 0)
|
2018-04-06 13:58:59 -07:00
|
|
|
|
2022-12-14 17:01:46 -08:00
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
window_config: dict = dict(),
|
|
|
|
camera_config: dict = dict(),
|
|
|
|
file_writer_config: dict = dict(),
|
|
|
|
skip_animations: bool = False,
|
|
|
|
always_update_mobjects: bool = False,
|
|
|
|
start_at_animation_number: int | None = None,
|
|
|
|
end_at_animation_number: int | None = None,
|
|
|
|
leave_progress_bars: bool = False,
|
|
|
|
preview: bool = True,
|
|
|
|
presenter_mode: bool = False,
|
|
|
|
show_animation_progress: bool = False,
|
2022-12-29 14:58:40 -08:00
|
|
|
embed_exception_mode: str = "",
|
|
|
|
embed_error_sound: bool = False,
|
2022-12-14 17:01:46 -08:00
|
|
|
):
|
|
|
|
self.skip_animations = skip_animations
|
|
|
|
self.always_update_mobjects = always_update_mobjects
|
|
|
|
self.start_at_animation_number = start_at_animation_number
|
|
|
|
self.end_at_animation_number = end_at_animation_number
|
|
|
|
self.leave_progress_bars = leave_progress_bars
|
|
|
|
self.preview = preview
|
|
|
|
self.presenter_mode = presenter_mode
|
|
|
|
self.show_animation_progress = show_animation_progress
|
2022-12-29 14:38:25 -08:00
|
|
|
self.embed_exception_mode = embed_exception_mode
|
2022-12-29 14:58:40 -08:00
|
|
|
self.embed_error_sound = embed_error_sound
|
2022-12-14 17:01:46 -08:00
|
|
|
|
2022-12-19 11:35:43 -08:00
|
|
|
self.camera_config = {**self.default_camera_config, **camera_config}
|
|
|
|
self.window_config = {**self.default_window_config, **window_config}
|
2023-01-20 16:30:39 -08:00
|
|
|
for config in self.camera_config, self.window_config:
|
|
|
|
config["samples"] = self.samples
|
2022-12-19 11:35:43 -08:00
|
|
|
self.file_writer_config = {**self.default_file_writer_config, **file_writer_config}
|
|
|
|
|
2022-12-14 17:01:46 -08:00
|
|
|
# Initialize window, if applicable
|
2020-02-11 19:51:19 -08:00
|
|
|
if self.preview:
|
2021-02-11 12:21:06 -08:00
|
|
|
from manimlib.window import Window
|
2022-12-19 11:35:43 -08:00
|
|
|
self.window = Window(scene=self, **self.window_config)
|
2023-01-23 17:03:46 -08:00
|
|
|
self.camera_config["window"] = self.window
|
2022-12-14 17:14:53 -08:00
|
|
|
self.camera_config["fps"] = 30 # Where's that 30 from?
|
2020-02-13 10:42:07 -08:00
|
|
|
else:
|
|
|
|
self.window = None
|
2020-02-11 19:51:19 -08:00
|
|
|
|
2022-12-14 17:01:46 -08:00
|
|
|
# Core state of the scene
|
2022-12-19 11:35:43 -08:00
|
|
|
self.camera: Camera = Camera(**self.camera_config)
|
2023-01-28 12:02:19 -08:00
|
|
|
self.frame: CameraFrame = self.camera.frame
|
|
|
|
self.frame.reorient(*self.default_frame_orientation)
|
|
|
|
self.frame.make_orientation_default()
|
|
|
|
|
2022-12-19 11:35:43 -08:00
|
|
|
self.file_writer = SceneFileWriter(self, **self.file_writer_config)
|
2022-02-14 21:22:18 +08:00
|
|
|
self.mobjects: list[Mobject] = [self.camera.frame]
|
2022-04-22 08:33:57 -07:00
|
|
|
self.id_to_mobject_map: dict[int, Mobject] = dict()
|
2022-02-14 21:22:18 +08:00
|
|
|
self.num_plays: int = 0
|
|
|
|
self.time: float = 0
|
|
|
|
self.skip_time: float = 0
|
|
|
|
self.original_skipping_status: bool = self.skip_animations
|
2022-04-27 09:53:56 -07:00
|
|
|
self.checkpoint_states: dict[str, list[tuple[Mobject, Mobject]]] = dict()
|
2022-12-19 11:35:43 -08:00
|
|
|
self.undo_stack = []
|
|
|
|
self.redo_stack = []
|
2022-04-27 11:19:20 -07:00
|
|
|
|
2021-12-07 10:03:10 -08:00
|
|
|
if self.start_at_animation_number is not None:
|
|
|
|
self.skip_animations = True
|
2022-11-18 09:12:40 -08:00
|
|
|
if self.file_writer.has_progress_display():
|
2022-04-27 11:19:20 -07:00
|
|
|
self.show_animation_progress = False
|
2020-02-14 15:30:44 -08:00
|
|
|
|
2020-02-14 16:26:49 -08:00
|
|
|
# Items associated with interaction
|
2020-02-14 15:30:44 -08:00
|
|
|
self.mouse_point = Point()
|
|
|
|
self.mouse_drag_point = Point()
|
2022-02-14 07:52:06 -08:00
|
|
|
self.hold_on_wait = self.presenter_mode
|
2022-04-27 11:19:44 -07:00
|
|
|
self.quit_interaction = False
|
2020-02-14 15:30:44 -08:00
|
|
|
|
2021-01-05 22:37:28 -08:00
|
|
|
# Much nicer to work with deterministic scenes
|
2017-12-06 15:17:59 -08:00
|
|
|
if self.random_seed is not None:
|
|
|
|
random.seed(self.random_seed)
|
|
|
|
np.random.seed(self.random_seed)
|
2015-10-29 13:45:28 -07:00
|
|
|
|
2022-04-22 11:44:28 -07:00
|
|
|
def __str__(self) -> str:
|
|
|
|
return self.__class__.__name__
|
|
|
|
|
2022-02-14 21:22:18 +08:00
|
|
|
def run(self) -> None:
|
|
|
|
self.virtual_animation_start_time: float = 0
|
|
|
|
self.real_animation_start_time: float = time.time()
|
2021-01-23 11:02:22 -08:00
|
|
|
self.file_writer.begin()
|
2021-01-05 22:37:28 -08:00
|
|
|
|
2016-08-10 10:26:07 -07:00
|
|
|
self.setup()
|
2018-02-19 17:24:17 -08:00
|
|
|
try:
|
2019-01-24 21:47:40 -08:00
|
|
|
self.construct()
|
2022-05-03 12:41:44 -07:00
|
|
|
self.interact()
|
|
|
|
except EndScene:
|
|
|
|
pass
|
|
|
|
except KeyboardInterrupt:
|
|
|
|
# Get rid keyboard interupt symbols
|
|
|
|
print("", end="\r")
|
2022-05-11 12:48:08 -07:00
|
|
|
self.file_writer.ended_with_interrupt = True
|
2019-01-04 12:47:48 -08:00
|
|
|
self.tear_down()
|
2020-02-11 19:51:19 -08:00
|
|
|
|
2022-02-14 21:22:18 +08:00
|
|
|
def setup(self) -> None:
|
2017-04-21 17:40:49 -07:00
|
|
|
"""
|
|
|
|
This is meant to be implement by any scenes which
|
2018-02-26 19:07:57 -08:00
|
|
|
are comonly subclassed, and have some common setup
|
2017-04-21 17:40:49 -07:00
|
|
|
involved before the construct method is called.
|
|
|
|
"""
|
|
|
|
pass
|
2016-08-10 10:26:07 -07:00
|
|
|
|
2022-02-14 21:22:18 +08:00
|
|
|
def construct(self) -> None:
|
2020-02-22 13:20:22 -08:00
|
|
|
# Where all the animation happens
|
2020-02-11 19:51:19 -08:00
|
|
|
# To be implemented in subclasses
|
|
|
|
pass
|
2015-03-22 16:15:29 -06:00
|
|
|
|
2022-02-14 21:22:18 +08:00
|
|
|
def tear_down(self) -> None:
|
2020-02-14 15:30:44 -08:00
|
|
|
self.stop_skipping()
|
2020-02-11 19:51:19 -08:00
|
|
|
self.file_writer.finish()
|
2022-04-27 11:19:44 -07:00
|
|
|
if self.window:
|
2022-05-03 12:41:44 -07:00
|
|
|
self.window.destroy()
|
|
|
|
self.window = None
|
2020-02-13 10:42:07 -08:00
|
|
|
|
2022-02-14 21:22:18 +08:00
|
|
|
def interact(self) -> None:
|
2022-12-18 12:01:33 -08:00
|
|
|
"""
|
|
|
|
If there is a window, enter a loop
|
|
|
|
which updates the frame while under
|
|
|
|
the hood calling the pyglet event loop
|
|
|
|
"""
|
2022-05-03 12:41:44 -07:00
|
|
|
if self.window is None:
|
|
|
|
return
|
2022-12-18 12:01:33 -08:00
|
|
|
log.info(
|
|
|
|
"\nTips: Using the keys `d`, `f`, or `z` " +
|
|
|
|
"you can interact with the scene. " +
|
|
|
|
"Press `command + q` or `esc` to quit"
|
|
|
|
)
|
2022-05-03 12:41:44 -07:00
|
|
|
self.skip_animations = False
|
|
|
|
while not self.is_window_closing():
|
2022-05-14 17:47:31 -07:00
|
|
|
self.update_frame(1 / self.camera.fps)
|
2020-02-04 15:28:50 -08:00
|
|
|
|
2022-12-19 10:51:26 -08:00
|
|
|
def embed(
|
|
|
|
self,
|
|
|
|
close_scene_on_exit: bool = True,
|
2023-01-26 15:28:10 -08:00
|
|
|
show_animation_progress: bool = False,
|
2022-12-19 10:51:26 -08:00
|
|
|
) -> None:
|
2020-02-23 22:59:29 +00:00
|
|
|
if not self.preview:
|
2022-05-03 12:41:44 -07:00
|
|
|
return # Embed is only relevant with a preview
|
2020-02-14 15:30:44 -08:00
|
|
|
self.stop_skipping()
|
|
|
|
self.update_frame()
|
2021-10-11 06:22:41 -07:00
|
|
|
self.save_state()
|
2022-12-19 10:51:26 -08:00
|
|
|
self.show_animation_progress = show_animation_progress
|
2021-10-11 06:22:41 -07:00
|
|
|
|
2022-12-19 10:51:26 -08:00
|
|
|
# Create embedded IPython terminal to be configured
|
|
|
|
shell = InteractiveShellEmbed.instance()
|
2022-04-22 08:16:17 -07:00
|
|
|
|
|
|
|
# Use the locals namespace of the caller
|
2022-12-18 11:11:08 -08:00
|
|
|
caller_frame = inspect.currentframe().f_back
|
|
|
|
local_ns = dict(caller_frame.f_locals)
|
|
|
|
|
2022-04-22 08:16:17 -07:00
|
|
|
# Add a few custom shortcuts
|
2022-12-19 10:51:26 -08:00
|
|
|
local_ns.update(
|
|
|
|
play=self.play,
|
|
|
|
wait=self.wait,
|
|
|
|
add=self.add,
|
|
|
|
remove=self.remove,
|
|
|
|
clear=self.clear,
|
|
|
|
save_state=self.save_state,
|
|
|
|
undo=self.undo,
|
|
|
|
redo=self.redo,
|
|
|
|
i2g=self.i2g,
|
|
|
|
i2m=self.i2m,
|
|
|
|
checkpoint_paste=self.checkpoint_paste,
|
|
|
|
)
|
2022-04-27 09:53:56 -07:00
|
|
|
|
2022-04-22 08:16:17 -07:00
|
|
|
# Enables gui interactions during the embed
|
|
|
|
def inputhook(context):
|
|
|
|
while not context.input_is_ready():
|
2022-05-03 12:41:44 -07:00
|
|
|
if not self.is_window_closing():
|
2022-04-22 10:16:43 -07:00
|
|
|
self.update_frame(dt=0)
|
2022-05-03 12:41:44 -07:00
|
|
|
if self.is_window_closing():
|
|
|
|
shell.ask_exit()
|
2022-04-22 08:16:17 -07:00
|
|
|
|
|
|
|
pt_inputhooks.register("manim", inputhook)
|
|
|
|
shell.enable_gui("manim")
|
|
|
|
|
2022-04-27 09:54:29 -07:00
|
|
|
# This is hacky, but there's an issue with ipython which is that
|
|
|
|
# when you define lambda's or list comprehensions during a shell session,
|
|
|
|
# they are not aware of local variables in the surrounding scope. Because
|
|
|
|
# That comes up a fair bit during scene construction, to get around this,
|
|
|
|
# we (admittedly sketchily) update the global namespace to match the local
|
|
|
|
# namespace, since this is just a shell session anyway.
|
2022-05-03 12:41:44 -07:00
|
|
|
shell.events.register(
|
|
|
|
"pre_run_cell",
|
|
|
|
lambda: shell.user_global_ns.update(shell.user_ns)
|
|
|
|
)
|
2022-04-27 09:54:29 -07:00
|
|
|
|
2022-04-22 11:44:28 -07:00
|
|
|
# Operation to run after each ipython command
|
2022-04-27 09:54:29 -07:00
|
|
|
def post_cell_func():
|
2022-05-14 17:28:11 -07:00
|
|
|
if not self.is_window_closing():
|
|
|
|
self.update_frame(dt=0, ignore_skipping=True)
|
2022-04-23 18:52:26 -07:00
|
|
|
self.save_state()
|
2022-04-22 08:16:17 -07:00
|
|
|
|
|
|
|
shell.events.register("post_run_cell", post_cell_func)
|
|
|
|
|
2022-12-29 15:08:57 -08:00
|
|
|
# Flash border, and potentially play sound, on exceptions
|
|
|
|
def custom_exc(shell, etype, evalue, tb, tb_offset=None):
|
|
|
|
# still show the error don't just swallow it
|
|
|
|
shell.showtraceback((etype, evalue, tb), tb_offset=tb_offset)
|
|
|
|
if self.embed_error_sound:
|
2022-12-29 14:58:40 -08:00
|
|
|
os.system("printf '\a'")
|
2022-12-29 20:18:19 -08:00
|
|
|
rect = FullScreenRectangle().set_stroke(RED, 30).set_fill(opacity=0)
|
|
|
|
rect.fix_in_frame()
|
|
|
|
self.play(VFadeInThenOut(rect, run_time=0.5))
|
2022-12-29 14:58:40 -08:00
|
|
|
|
2022-12-29 15:08:57 -08:00
|
|
|
shell.set_custom_exc((Exception,), custom_exc)
|
2022-12-29 14:58:40 -08:00
|
|
|
|
|
|
|
# Set desired exception mode
|
2022-12-29 14:38:25 -08:00
|
|
|
shell.magic(f"xmode {self.embed_exception_mode}")
|
|
|
|
|
2022-12-19 10:51:26 -08:00
|
|
|
# Launch shell
|
|
|
|
shell(
|
|
|
|
local_ns=local_ns,
|
|
|
|
# Pretend like we're embeding in the caller function, not here
|
|
|
|
stack_depth=2,
|
|
|
|
# Specify that the present module is the caller's, not here
|
|
|
|
module=get_module(caller_frame.f_globals["__file__"])
|
|
|
|
)
|
2022-04-22 08:16:17 -07:00
|
|
|
|
2022-02-13 15:16:16 -08:00
|
|
|
# End scene when exiting an embed
|
|
|
|
if close_scene_on_exit:
|
2022-05-03 12:41:44 -07:00
|
|
|
raise EndScene()
|
2020-02-14 15:30:44 -08:00
|
|
|
|
2018-04-06 13:58:59 -07:00
|
|
|
# Only these methods should touch the camera
|
2022-04-22 11:44:28 -07:00
|
|
|
|
2022-02-14 21:22:18 +08:00
|
|
|
def get_image(self) -> Image:
|
2023-01-24 12:05:08 -08:00
|
|
|
if self.window is not None:
|
2023-01-26 20:02:50 -08:00
|
|
|
self.camera.use_window_fbo(False)
|
|
|
|
self.camera.capture(*self.mobjects)
|
2023-01-24 12:05:08 -08:00
|
|
|
image = self.camera.get_image()
|
|
|
|
if self.window is not None:
|
2023-01-26 20:02:50 -08:00
|
|
|
self.camera.use_window_fbo(True)
|
2023-01-24 12:05:08 -08:00
|
|
|
return image
|
2015-10-29 13:45:28 -07:00
|
|
|
|
2022-02-14 21:22:18 +08:00
|
|
|
def show(self) -> None:
|
2021-01-05 22:37:28 -08:00
|
|
|
self.update_frame(ignore_skipping=True)
|
|
|
|
self.get_image().show()
|
|
|
|
|
2022-02-14 21:22:18 +08:00
|
|
|
def update_frame(self, dt: float = 0, ignore_skipping: bool = False) -> None:
|
2020-02-14 11:55:07 -08:00
|
|
|
self.increment_time(dt)
|
2020-02-17 12:14:40 -08:00
|
|
|
self.update_mobjects(dt)
|
2019-01-24 21:47:40 -08:00
|
|
|
if self.skip_animations and not ignore_skipping:
|
2018-02-19 17:24:17 -08:00
|
|
|
return
|
2020-02-11 19:51:19 -08:00
|
|
|
|
2022-05-03 12:41:44 -07:00
|
|
|
if self.is_window_closing():
|
|
|
|
raise EndScene()
|
2022-04-27 11:19:44 -07:00
|
|
|
|
2020-02-13 10:42:07 -08:00
|
|
|
if self.window:
|
|
|
|
self.window.clear()
|
2020-02-17 12:14:40 -08:00
|
|
|
self.camera.capture(*self.mobjects)
|
2020-02-14 11:55:07 -08:00
|
|
|
|
2020-02-13 10:42:07 -08:00
|
|
|
if self.window:
|
|
|
|
self.window.swap_buffers()
|
2020-02-18 22:30:43 -08:00
|
|
|
vt = self.time - self.virtual_animation_start_time
|
|
|
|
rt = time.time() - self.real_animation_start_time
|
|
|
|
if rt < vt:
|
|
|
|
self.update_frame(0)
|
2020-02-14 11:55:07 -08:00
|
|
|
|
2022-02-14 21:22:18 +08:00
|
|
|
def emit_frame(self) -> None:
|
2020-02-11 19:51:19 -08:00
|
|
|
if not self.skip_animations:
|
|
|
|
self.file_writer.write_frame(self.camera)
|
|
|
|
|
2021-01-05 22:37:28 -08:00
|
|
|
# Related to updating
|
2022-04-22 11:44:28 -07:00
|
|
|
|
2022-02-14 21:22:18 +08:00
|
|
|
def update_mobjects(self, dt: float) -> None:
|
2019-01-29 14:40:44 -08:00
|
|
|
for mobject in self.mobjects:
|
2018-08-12 12:17:32 -07:00
|
|
|
mobject.update(dt)
|
2017-08-24 11:43:38 -07:00
|
|
|
|
2022-02-14 21:22:18 +08:00
|
|
|
def should_update_mobjects(self) -> bool:
|
2019-02-15 20:05:16 -08:00
|
|
|
return self.always_update_mobjects or any([
|
2020-02-14 15:30:44 -08:00
|
|
|
len(mob.get_family_updaters()) > 0
|
|
|
|
for mob in self.mobjects
|
2018-08-12 12:17:32 -07:00
|
|
|
])
|
2017-08-24 19:05:04 -07:00
|
|
|
|
2022-02-14 21:22:18 +08:00
|
|
|
def has_time_based_updaters(self) -> bool:
|
2021-12-07 10:05:33 -08:00
|
|
|
return any([
|
|
|
|
sm.has_time_based_updater()
|
|
|
|
for mob in self.mobjects()
|
|
|
|
for sm in mob.get_family()
|
|
|
|
])
|
|
|
|
|
2021-01-05 22:37:28 -08:00
|
|
|
# Related to time
|
2022-04-22 11:44:28 -07:00
|
|
|
|
2022-02-14 21:22:18 +08:00
|
|
|
def get_time(self) -> float:
|
2019-01-10 17:06:22 -08:00
|
|
|
return self.time
|
|
|
|
|
2022-02-14 21:22:18 +08:00
|
|
|
def increment_time(self, dt: float) -> None:
|
2020-02-14 11:55:07 -08:00
|
|
|
self.time += dt
|
2019-01-10 17:06:22 -08:00
|
|
|
|
2021-01-05 22:37:28 -08:00
|
|
|
# Related to internal mobject organization
|
2022-04-22 11:44:28 -07:00
|
|
|
|
2022-02-14 21:22:18 +08:00
|
|
|
def get_top_level_mobjects(self) -> list[Mobject]:
|
2017-02-09 21:09:51 -08:00
|
|
|
# Return only those which are not in the family
|
|
|
|
# of another mobject from the scene
|
|
|
|
mobjects = self.get_mobjects()
|
2018-08-21 19:15:16 -07:00
|
|
|
families = [m.get_family() for m in mobjects]
|
2018-04-06 13:58:59 -07:00
|
|
|
|
2017-02-09 21:09:51 -08:00
|
|
|
def is_top_level(mobject):
|
|
|
|
num_families = sum([
|
2017-10-05 21:03:30 -05:00
|
|
|
(mobject in family)
|
2017-02-09 21:09:51 -08:00
|
|
|
for family in families
|
|
|
|
])
|
|
|
|
return num_families == 1
|
2018-08-09 17:56:05 -07:00
|
|
|
return list(filter(is_top_level, mobjects))
|
2017-10-05 21:03:30 -05:00
|
|
|
|
2022-02-14 21:22:18 +08:00
|
|
|
def get_mobject_family_members(self) -> list[Mobject]:
|
2020-01-15 18:30:58 -08:00
|
|
|
return extract_mobject_family_members(self.mobjects)
|
2018-08-13 14:13:30 -07:00
|
|
|
|
2022-02-14 21:22:18 +08:00
|
|
|
def add(self, *new_mobjects: Mobject):
|
2015-03-26 22:49:22 -06:00
|
|
|
"""
|
2019-02-15 20:05:16 -08:00
|
|
|
Mobjects will be displayed, from background to
|
|
|
|
foreground in the order with which they are added.
|
2015-03-26 22:49:22 -06:00
|
|
|
"""
|
2020-01-15 18:30:58 -08:00
|
|
|
self.remove(*new_mobjects)
|
|
|
|
self.mobjects += new_mobjects
|
2022-04-22 08:33:57 -07:00
|
|
|
self.id_to_mobject_map.update({
|
|
|
|
id(sm): sm
|
|
|
|
for m in new_mobjects
|
|
|
|
for sm in m.get_family()
|
|
|
|
})
|
2015-06-09 11:26:12 -07:00
|
|
|
return self
|
2015-03-22 16:15:29 -06:00
|
|
|
|
2022-02-14 21:22:18 +08:00
|
|
|
def add_mobjects_among(self, values: Iterable):
|
2015-10-09 19:53:38 -07:00
|
|
|
"""
|
2019-02-15 20:05:16 -08:00
|
|
|
This is meant mostly for quick prototyping,
|
|
|
|
e.g. to add all mobjects defined up to a point,
|
|
|
|
call self.add_mobjects_among(locals().values())
|
2015-10-09 19:53:38 -07:00
|
|
|
"""
|
2019-02-15 20:05:16 -08:00
|
|
|
self.add(*filter(
|
|
|
|
lambda m: isinstance(m, Mobject),
|
|
|
|
values
|
|
|
|
))
|
2015-11-02 13:03:01 -08:00
|
|
|
return self
|
2015-10-09 19:53:38 -07:00
|
|
|
|
2022-04-24 10:29:31 -07:00
|
|
|
def replace(self, mobject: Mobject, *replacements: Mobject):
|
|
|
|
if mobject in self.mobjects:
|
|
|
|
index = self.mobjects.index(mobject)
|
|
|
|
self.mobjects = [
|
|
|
|
*self.mobjects[:index],
|
|
|
|
*replacements,
|
|
|
|
*self.mobjects[index + 1:]
|
|
|
|
]
|
|
|
|
return self
|
|
|
|
|
2022-12-26 07:46:40 -07:00
|
|
|
def remove(self, *mobjects_to_remove: Mobject):
|
2022-04-24 10:29:31 -07:00
|
|
|
"""
|
|
|
|
Removes anything in mobjects from scenes mobject list, but in the event that one
|
|
|
|
of the items to be removed is a member of the family of an item in mobject_list,
|
|
|
|
the other family members are added back into the list.
|
|
|
|
|
|
|
|
For example, if the scene includes Group(m1, m2, m3), and we call scene.remove(m1),
|
|
|
|
the desired behavior is for the scene to then include m2 and m3 (ungrouped).
|
|
|
|
"""
|
2022-12-26 07:46:40 -07:00
|
|
|
to_remove = set(extract_mobject_family_members(mobjects_to_remove))
|
|
|
|
new_mobjects, _ = recursive_mobject_remove(self.mobjects, to_remove)
|
|
|
|
self.mobjects = new_mobjects
|
2018-01-18 16:23:31 -08:00
|
|
|
|
2022-02-14 21:22:18 +08:00
|
|
|
def bring_to_front(self, *mobjects: Mobject):
|
2016-08-02 12:26:15 -07:00
|
|
|
self.add(*mobjects)
|
2015-10-12 19:39:46 -07:00
|
|
|
return self
|
|
|
|
|
2022-02-14 21:22:18 +08:00
|
|
|
def bring_to_back(self, *mobjects: Mobject):
|
2016-08-02 12:26:15 -07:00
|
|
|
self.remove(*mobjects)
|
2018-01-17 21:32:50 -08:00
|
|
|
self.mobjects = list(mobjects) + self.mobjects
|
2015-10-12 19:39:46 -07:00
|
|
|
return self
|
|
|
|
|
2015-06-10 22:00:35 -07:00
|
|
|
def clear(self):
|
2015-10-29 13:45:28 -07:00
|
|
|
self.mobjects = []
|
2015-06-10 22:00:35 -07:00
|
|
|
return self
|
2015-03-30 17:51:26 -07:00
|
|
|
|
2022-02-14 21:22:18 +08:00
|
|
|
def get_mobjects(self) -> list[Mobject]:
|
2016-07-18 11:50:26 -07:00
|
|
|
return list(self.mobjects)
|
|
|
|
|
2022-02-14 21:22:18 +08:00
|
|
|
def get_mobject_copies(self) -> list[Mobject]:
|
2016-07-18 11:50:26 -07:00
|
|
|
return [m.copy() for m in self.mobjects]
|
|
|
|
|
2022-02-14 21:22:18 +08:00
|
|
|
def point_to_mobject(
|
|
|
|
self,
|
|
|
|
point: np.ndarray,
|
|
|
|
search_set: Iterable[Mobject] | None = None,
|
|
|
|
buff: float = 0
|
|
|
|
) -> Mobject | None:
|
2021-08-22 14:57:32 -07:00
|
|
|
"""
|
|
|
|
E.g. if clicking on the scene, this returns the top layer mobject
|
|
|
|
under a given point
|
|
|
|
"""
|
|
|
|
if search_set is None:
|
|
|
|
search_set = self.mobjects
|
|
|
|
for mobject in reversed(search_set):
|
|
|
|
if mobject.is_point_touching(point, buff=buff):
|
|
|
|
return mobject
|
|
|
|
return None
|
|
|
|
|
2022-04-20 21:48:58 -07:00
|
|
|
def get_group(self, *mobjects):
|
|
|
|
if all(isinstance(m, VMobject) for m in mobjects):
|
|
|
|
return VGroup(*mobjects)
|
|
|
|
else:
|
|
|
|
return Group(*mobjects)
|
|
|
|
|
|
|
|
def id_to_mobject(self, id_value):
|
2022-04-22 08:33:57 -07:00
|
|
|
return self.id_to_mobject_map[id_value]
|
2022-04-20 21:48:58 -07:00
|
|
|
|
|
|
|
def ids_to_group(self, *id_values):
|
|
|
|
return self.get_group(*filter(
|
|
|
|
lambda x: x is not None,
|
|
|
|
map(self.id_to_mobject, id_values)
|
|
|
|
))
|
|
|
|
|
2022-04-22 08:16:17 -07:00
|
|
|
def i2g(self, *id_values):
|
|
|
|
return self.ids_to_group(*id_values)
|
|
|
|
|
|
|
|
def i2m(self, id_value):
|
|
|
|
return self.id_to_mobject(id_value)
|
|
|
|
|
2021-01-05 22:37:28 -08:00
|
|
|
# Related to skipping
|
2022-04-22 11:44:28 -07:00
|
|
|
|
2022-02-14 21:22:18 +08:00
|
|
|
def update_skipping_status(self) -> None:
|
2021-01-05 22:37:28 -08:00
|
|
|
if self.start_at_animation_number is not None:
|
|
|
|
if self.num_plays == self.start_at_animation_number:
|
2021-12-07 10:04:28 -08:00
|
|
|
self.skip_time = self.time
|
|
|
|
if not self.original_skipping_status:
|
|
|
|
self.stop_skipping()
|
2021-01-05 22:37:28 -08:00
|
|
|
if self.end_at_animation_number is not None:
|
|
|
|
if self.num_plays >= self.end_at_animation_number:
|
2022-05-03 12:41:44 -07:00
|
|
|
raise EndScene()
|
2021-01-05 22:37:28 -08:00
|
|
|
|
2022-02-14 21:22:18 +08:00
|
|
|
def stop_skipping(self) -> None:
|
2021-12-07 10:03:10 -08:00
|
|
|
self.virtual_animation_start_time = self.time
|
|
|
|
self.skip_animations = False
|
2021-01-05 22:37:28 -08:00
|
|
|
|
|
|
|
# Methods associated with running animations
|
2022-04-22 11:44:28 -07:00
|
|
|
|
2022-02-14 21:22:18 +08:00
|
|
|
def get_time_progression(
|
|
|
|
self,
|
|
|
|
run_time: float,
|
|
|
|
n_iterations: int | None = None,
|
|
|
|
desc: str = "",
|
|
|
|
override_skip_animations: bool = False
|
|
|
|
) -> list[float] | np.ndarray | ProgressDisplay:
|
2019-01-15 12:19:09 -08:00
|
|
|
if self.skip_animations and not override_skip_animations:
|
2021-11-30 11:41:33 -08:00
|
|
|
return [run_time]
|
2022-04-27 11:19:20 -07:00
|
|
|
|
2022-05-14 17:47:31 -07:00
|
|
|
times = np.arange(0, run_time, 1 / self.camera.fps)
|
2021-11-30 11:41:33 -08:00
|
|
|
|
2022-11-18 09:12:40 -08:00
|
|
|
self.file_writer.set_progress_display_description(sub_desc=desc)
|
2021-11-30 11:41:33 -08:00
|
|
|
|
2022-04-27 11:19:20 -07:00
|
|
|
if self.show_animation_progress:
|
|
|
|
return ProgressDisplay(
|
|
|
|
times,
|
|
|
|
total=n_iterations,
|
|
|
|
leave=self.leave_progress_bars,
|
|
|
|
ascii=True if platform.system() == 'Windows' else None,
|
|
|
|
desc=desc,
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
return times
|
2017-08-24 11:43:38 -07:00
|
|
|
|
2022-02-14 21:22:18 +08:00
|
|
|
def get_run_time(self, animations: Iterable[Animation]) -> float:
|
2022-05-02 11:40:42 -07:00
|
|
|
return np.max([animation.get_run_time() for animation in animations])
|
2019-01-16 11:10:43 -08:00
|
|
|
|
2022-02-14 21:22:18 +08:00
|
|
|
def get_animation_time_progression(
|
|
|
|
self,
|
|
|
|
animations: Iterable[Animation]
|
|
|
|
) -> list[float] | np.ndarray | ProgressDisplay:
|
2022-12-16 15:21:31 -08:00
|
|
|
animations = list(animations)
|
2019-01-16 11:10:43 -08:00
|
|
|
run_time = self.get_run_time(animations)
|
2021-11-30 11:41:33 -08:00
|
|
|
description = f"{self.num_plays} {animations[0]}"
|
|
|
|
if len(animations) > 1:
|
|
|
|
description += ", etc."
|
|
|
|
time_progression = self.get_time_progression(run_time, desc=description)
|
2016-02-27 12:44:52 -08:00
|
|
|
return time_progression
|
|
|
|
|
2022-02-14 21:22:18 +08:00
|
|
|
def get_wait_time_progression(
|
|
|
|
self,
|
|
|
|
duration: float,
|
|
|
|
stop_condition: Callable[[], bool] | None = None
|
|
|
|
) -> list[float] | np.ndarray | ProgressDisplay:
|
2021-11-30 11:41:33 -08:00
|
|
|
kw = {"desc": f"{self.num_plays} Waiting"}
|
2021-01-05 22:37:28 -08:00
|
|
|
if stop_condition is not None:
|
2021-11-30 11:41:33 -08:00
|
|
|
kw["n_iterations"] = -1 # So it doesn't show % progress
|
|
|
|
kw["override_skip_animations"] = True
|
|
|
|
return self.get_time_progression(duration, **kw)
|
2021-01-05 22:37:28 -08:00
|
|
|
|
2022-12-17 18:59:05 -08:00
|
|
|
def pre_play(self):
|
|
|
|
if self.presenter_mode and self.num_plays == 0:
|
|
|
|
self.hold_loop()
|
2020-02-18 22:30:43 -08:00
|
|
|
|
2022-12-17 18:59:05 -08:00
|
|
|
self.update_skipping_status()
|
2020-02-18 22:30:43 -08:00
|
|
|
|
2022-12-17 18:59:05 -08:00
|
|
|
if not self.skip_animations:
|
|
|
|
self.file_writer.begin_animation()
|
2020-02-18 22:30:43 -08:00
|
|
|
|
2022-12-17 18:59:05 -08:00
|
|
|
if self.window:
|
|
|
|
self.real_animation_start_time = time.time()
|
|
|
|
self.virtual_animation_start_time = self.time
|
2022-04-23 18:52:26 -07:00
|
|
|
|
2022-12-17 18:59:05 -08:00
|
|
|
def post_play(self):
|
|
|
|
if not self.skip_animations:
|
|
|
|
self.file_writer.end_animation()
|
|
|
|
|
|
|
|
if self.skip_animations and self.window is not None:
|
|
|
|
# Show some quick frames along the way
|
|
|
|
self.update_frame(dt=0, ignore_skipping=True)
|
2022-05-04 21:22:48 -07:00
|
|
|
|
2022-12-17 18:59:05 -08:00
|
|
|
self.num_plays += 1
|
2019-01-24 22:24:01 -08:00
|
|
|
|
2022-02-14 21:22:18 +08:00
|
|
|
def begin_animations(self, animations: Iterable[Animation]) -> None:
|
2018-02-19 16:50:58 -08:00
|
|
|
for animation in animations:
|
2019-02-11 20:53:20 -08:00
|
|
|
animation.begin()
|
2018-08-12 12:17:32 -07:00
|
|
|
# Anything animated that's not already in the
|
2020-02-21 10:56:40 -08:00
|
|
|
# scene gets added to the scene. Note, for
|
|
|
|
# animated mobjects that are in the family of
|
|
|
|
# those on screen, this can result in a restructuring
|
|
|
|
# of the scene.mobjects list, which is usually desired.
|
2021-01-05 22:37:28 -08:00
|
|
|
if animation.mobject not in self.mobjects:
|
|
|
|
self.add(animation.mobject)
|
2019-02-08 11:00:04 -08:00
|
|
|
|
2022-02-14 21:22:18 +08:00
|
|
|
def progress_through_animations(self, animations: Iterable[Animation]) -> None:
|
2019-02-08 12:51:21 -08:00
|
|
|
last_t = 0
|
2017-08-24 11:43:38 -07:00
|
|
|
for t in self.get_animation_time_progression(animations):
|
2019-02-08 12:51:21 -08:00
|
|
|
dt = t - last_t
|
|
|
|
last_t = t
|
2015-04-03 16:41:25 -07:00
|
|
|
for animation in animations:
|
2019-02-08 12:32:24 -08:00
|
|
|
animation.update_mobjects(dt)
|
2019-02-08 12:51:21 -08:00
|
|
|
alpha = t / animation.run_time
|
|
|
|
animation.interpolate(alpha)
|
2020-02-14 11:55:07 -08:00
|
|
|
self.update_frame(dt)
|
|
|
|
self.emit_frame()
|
2019-02-08 11:00:04 -08:00
|
|
|
|
2022-02-14 21:22:18 +08:00
|
|
|
def finish_animations(self, animations: Iterable[Animation]) -> None:
|
2019-02-08 11:00:04 -08:00
|
|
|
for animation in animations:
|
|
|
|
animation.finish()
|
2019-02-08 11:57:27 -08:00
|
|
|
animation.clean_up_from_scene(self)
|
2018-02-19 17:24:17 -08:00
|
|
|
if self.skip_animations:
|
2019-02-15 20:05:16 -08:00
|
|
|
self.update_mobjects(self.get_run_time(animations))
|
2018-02-19 17:24:17 -08:00
|
|
|
else:
|
2019-02-15 20:05:16 -08:00
|
|
|
self.update_mobjects(0)
|
2018-11-01 11:23:34 +03:00
|
|
|
|
2022-12-16 15:21:31 -08:00
|
|
|
def play(
|
|
|
|
self,
|
|
|
|
*proto_animations: Animation | _AnimationBuilder,
|
|
|
|
run_time: float | None = None,
|
|
|
|
rate_func: Callable[[float], float] | None = None,
|
|
|
|
lag_ratio: float | None = None,
|
|
|
|
) -> None:
|
2022-05-02 11:10:57 -07:00
|
|
|
if len(proto_animations) == 0:
|
2021-10-16 13:04:52 +08:00
|
|
|
log.warning("Called Scene.play with no animations")
|
2019-02-08 12:51:21 -08:00
|
|
|
return
|
2022-12-16 15:21:31 -08:00
|
|
|
animations = list(map(prepare_animation, proto_animations))
|
|
|
|
for anim in animations:
|
|
|
|
anim.update_rate_info(run_time, rate_func, lag_ratio)
|
2022-12-17 18:59:05 -08:00
|
|
|
self.pre_play()
|
2019-02-08 15:26:30 -08:00
|
|
|
self.begin_animations(animations)
|
2019-02-08 12:51:21 -08:00
|
|
|
self.progress_through_animations(animations)
|
|
|
|
self.finish_animations(animations)
|
2022-12-17 18:59:05 -08:00
|
|
|
self.post_play()
|
2016-07-19 11:07:26 -07:00
|
|
|
|
2022-02-14 21:22:18 +08:00
|
|
|
def wait(
|
|
|
|
self,
|
|
|
|
duration: float = DEFAULT_WAIT_TIME,
|
|
|
|
stop_condition: Callable[[], bool] = None,
|
|
|
|
note: str = None,
|
|
|
|
ignore_presenter_mode: bool = False
|
|
|
|
):
|
2022-12-17 18:59:05 -08:00
|
|
|
self.pre_play()
|
2019-02-15 20:05:16 -08:00
|
|
|
self.update_mobjects(dt=0) # Any problems with this?
|
2022-02-13 15:16:16 -08:00
|
|
|
if self.presenter_mode and not self.skip_animations and not ignore_presenter_mode:
|
2022-04-25 09:55:49 -07:00
|
|
|
if note:
|
|
|
|
log.info(note)
|
2022-05-03 12:45:16 -07:00
|
|
|
self.hold_loop()
|
2022-02-13 15:16:16 -08:00
|
|
|
else:
|
|
|
|
time_progression = self.get_wait_time_progression(duration, stop_condition)
|
|
|
|
last_t = 0
|
|
|
|
for t in time_progression:
|
|
|
|
dt = t - last_t
|
|
|
|
last_t = t
|
|
|
|
self.update_frame(dt)
|
|
|
|
self.emit_frame()
|
|
|
|
if stop_condition is not None and stop_condition():
|
|
|
|
break
|
2022-12-17 18:59:05 -08:00
|
|
|
self.post_play()
|
2015-03-22 16:15:29 -06:00
|
|
|
|
2022-05-03 12:45:16 -07:00
|
|
|
def hold_loop(self):
|
|
|
|
while self.hold_on_wait:
|
2022-05-14 17:47:31 -07:00
|
|
|
self.update_frame(dt=1 / self.camera.fps)
|
2022-05-03 12:45:16 -07:00
|
|
|
self.hold_on_wait = True
|
|
|
|
|
2022-02-14 21:22:18 +08:00
|
|
|
def wait_until(
|
|
|
|
self,
|
|
|
|
stop_condition: Callable[[], bool],
|
|
|
|
max_time: float = 60
|
|
|
|
):
|
2019-01-14 13:26:58 -08:00
|
|
|
self.wait(max_time, stop_condition=stop_condition)
|
2018-01-29 21:29:36 -08:00
|
|
|
|
2017-03-20 14:37:51 -07:00
|
|
|
def force_skipping(self):
|
|
|
|
self.original_skipping_status = self.skip_animations
|
|
|
|
self.skip_animations = True
|
|
|
|
return self
|
|
|
|
|
|
|
|
def revert_to_original_skipping_status(self):
|
|
|
|
if hasattr(self, "original_skipping_status"):
|
|
|
|
self.skip_animations = self.original_skipping_status
|
|
|
|
return self
|
|
|
|
|
2022-02-14 21:22:18 +08:00
|
|
|
def add_sound(
|
|
|
|
self,
|
|
|
|
sound_file: str,
|
|
|
|
time_offset: float = 0,
|
|
|
|
gain: float | None = None,
|
|
|
|
gain_to_background: float | None = None
|
|
|
|
):
|
2019-09-10 13:26:30 -07:00
|
|
|
if self.skip_animations:
|
|
|
|
return
|
2019-01-24 22:24:01 -08:00
|
|
|
time = self.get_time() + time_offset
|
2022-02-14 21:22:18 +08:00
|
|
|
self.file_writer.add_sound(sound_file, time, gain, gain_to_background)
|
2019-01-24 22:24:01 -08:00
|
|
|
|
2020-02-22 13:20:22 -08:00
|
|
|
# Helpers for interactive development
|
|
|
|
|
2022-05-02 11:11:18 -07:00
|
|
|
def get_state(self) -> SceneState:
|
|
|
|
return SceneState(self)
|
2022-04-22 11:44:28 -07:00
|
|
|
|
2022-05-02 11:11:18 -07:00
|
|
|
def restore_state(self, scene_state: SceneState):
|
|
|
|
scene_state.restore_scene(self)
|
2020-02-22 13:20:22 -08:00
|
|
|
|
2022-04-22 11:44:28 -07:00
|
|
|
def save_state(self) -> None:
|
|
|
|
if not self.preview:
|
|
|
|
return
|
2022-05-02 11:11:18 -07:00
|
|
|
state = self.get_state()
|
|
|
|
if self.undo_stack and state.mobjects_match(self.undo_stack[-1]):
|
|
|
|
return
|
2022-04-22 11:44:28 -07:00
|
|
|
self.redo_stack = []
|
2022-05-02 11:11:18 -07:00
|
|
|
self.undo_stack.append(state)
|
|
|
|
if len(self.undo_stack) > self.max_num_saved_states:
|
|
|
|
self.undo_stack.pop(0)
|
2022-04-22 11:44:28 -07:00
|
|
|
|
|
|
|
def undo(self):
|
|
|
|
if self.undo_stack:
|
2022-05-02 11:11:18 -07:00
|
|
|
self.redo_stack.append(self.get_state())
|
2022-04-22 11:44:28 -07:00
|
|
|
self.restore_state(self.undo_stack.pop())
|
|
|
|
|
|
|
|
def redo(self):
|
|
|
|
if self.redo_stack:
|
2022-05-02 11:11:18 -07:00
|
|
|
self.undo_stack.append(self.get_state())
|
2022-04-22 11:44:28 -07:00
|
|
|
self.restore_state(self.redo_stack.pop())
|
|
|
|
|
2023-01-26 15:28:10 -08:00
|
|
|
def checkpoint_paste(
|
|
|
|
self,
|
|
|
|
skip: bool = False,
|
|
|
|
record: bool = False,
|
|
|
|
progress_bar: bool = True
|
|
|
|
):
|
2022-12-19 10:51:26 -08:00
|
|
|
"""
|
|
|
|
Used during interactive development to run (or re-run)
|
|
|
|
a block of scene code.
|
|
|
|
|
|
|
|
If the copied selection starts with a comment, this will
|
|
|
|
revert to the state of the scene the first time this function
|
|
|
|
was called on a block of code starting with that comment.
|
|
|
|
"""
|
2022-12-19 11:36:28 -08:00
|
|
|
shell = get_ipython()
|
2023-01-23 17:10:18 -08:00
|
|
|
if shell is None or self.window is None:
|
2022-12-19 11:36:28 -08:00
|
|
|
raise Exception(
|
|
|
|
"Scene.checkpoint_paste cannot be called outside of " +
|
|
|
|
"an ipython shell"
|
|
|
|
)
|
|
|
|
|
2022-12-19 10:51:26 -08:00
|
|
|
pasted = pyperclip.paste()
|
|
|
|
line0 = pasted.lstrip().split("\n")[0]
|
|
|
|
if line0.startswith("#"):
|
|
|
|
if line0 not in self.checkpoint_states:
|
|
|
|
self.checkpoint(line0)
|
|
|
|
else:
|
|
|
|
self.revert_to_checkpoint(line0)
|
|
|
|
|
|
|
|
prev_skipping = self.skip_animations
|
|
|
|
self.skip_animations = skip
|
|
|
|
|
2023-01-26 15:28:10 -08:00
|
|
|
prev_progress = self.show_animation_progress
|
|
|
|
self.show_animation_progress = progress_bar
|
|
|
|
|
2023-01-23 17:10:18 -08:00
|
|
|
if record:
|
2023-01-25 22:34:11 -08:00
|
|
|
self.camera.use_window_fbo(False)
|
2023-01-23 17:10:18 -08:00
|
|
|
self.file_writer.begin_insert()
|
|
|
|
|
2022-12-19 10:51:26 -08:00
|
|
|
shell.run_cell(pasted)
|
|
|
|
|
2023-01-23 17:10:18 -08:00
|
|
|
if record:
|
|
|
|
self.file_writer.end_insert()
|
2023-01-25 22:34:11 -08:00
|
|
|
self.camera.use_window_fbo(True)
|
2023-01-23 17:10:18 -08:00
|
|
|
|
2022-12-19 10:51:26 -08:00
|
|
|
self.skip_animations = prev_skipping
|
2023-01-26 15:28:10 -08:00
|
|
|
self.show_animation_progress = prev_progress
|
2022-12-19 10:51:26 -08:00
|
|
|
|
2022-04-27 09:53:56 -07:00
|
|
|
def checkpoint(self, key: str):
|
2022-05-02 11:11:18 -07:00
|
|
|
self.checkpoint_states[key] = self.get_state()
|
2022-04-27 09:53:56 -07:00
|
|
|
|
|
|
|
def revert_to_checkpoint(self, key: str):
|
|
|
|
if key not in self.checkpoint_states:
|
|
|
|
log.error(f"No checkpoint at {key}")
|
|
|
|
return
|
2022-05-01 15:31:42 -04:00
|
|
|
all_keys = list(self.checkpoint_states.keys())
|
|
|
|
index = all_keys.index(key)
|
|
|
|
for later_key in all_keys[index + 1:]:
|
|
|
|
self.checkpoint_states.pop(later_key)
|
|
|
|
|
2022-04-27 09:53:56 -07:00
|
|
|
self.restore_state(self.checkpoint_states[key])
|
|
|
|
|
|
|
|
def clear_checkpoints(self):
|
|
|
|
self.checkpoint_states = dict()
|
|
|
|
|
2022-04-22 11:44:28 -07:00
|
|
|
def save_mobject_to_file(self, mobject: Mobject, file_path: str | None = None) -> None:
|
2022-04-21 15:02:11 -07:00
|
|
|
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)
|
2022-04-20 21:49:57 -07:00
|
|
|
|
|
|
|
def load_mobject(self, file_name):
|
|
|
|
if os.path.exists(file_name):
|
|
|
|
path = file_name
|
|
|
|
else:
|
|
|
|
directory = self.file_writer.get_saved_mobject_directory()
|
|
|
|
path = os.path.join(directory, file_name)
|
|
|
|
return Mobject.load(path)
|
|
|
|
|
2022-05-03 12:41:44 -07:00
|
|
|
def is_window_closing(self):
|
|
|
|
return self.window and (self.window.is_closing or self.quit_interaction)
|
|
|
|
|
2020-02-11 19:51:19 -08:00
|
|
|
# Event handling
|
2021-01-28 14:02:43 +05:30
|
|
|
|
2022-02-14 21:22:18 +08:00
|
|
|
def on_mouse_motion(
|
|
|
|
self,
|
2023-01-28 22:40:57 -08:00
|
|
|
point: Vect3,
|
|
|
|
d_point: Vect3
|
2022-02-14 21:22:18 +08:00
|
|
|
) -> None:
|
2023-01-30 14:15:39 -08:00
|
|
|
assert(self.window is not None)
|
2020-02-14 15:30:44 -08:00
|
|
|
self.mouse_point.move_to(point)
|
2021-01-28 14:02:43 +05:30
|
|
|
|
2021-02-02 16:04:50 +05:30
|
|
|
event_data = {"point": point, "d_point": d_point}
|
|
|
|
propagate_event = EVENT_DISPATCHER.dispatch(EventType.MouseMotionEvent, **event_data)
|
|
|
|
if propagate_event is not None and propagate_event is False:
|
|
|
|
return
|
2021-01-28 14:02:43 +05:30
|
|
|
|
2021-01-06 12:48:58 -08:00
|
|
|
frame = self.camera.frame
|
2022-04-14 14:36:17 -07:00
|
|
|
# Handle perspective changes
|
2022-04-20 21:50:37 -07:00
|
|
|
if self.window.is_key_pressed(ord(PAN_3D_KEY)):
|
2023-01-30 14:15:39 -08:00
|
|
|
ff_d_point = frame.to_fixed_frame_point(d_point, relative=True)
|
2023-01-28 22:30:46 -08:00
|
|
|
ff_d_point *= self.pan_sensitivity
|
|
|
|
frame.increment_theta(-ff_d_point[0])
|
|
|
|
frame.increment_phi(ff_d_point[1])
|
2022-04-14 14:36:17 -07:00
|
|
|
# Handle frame movements
|
2022-04-20 21:50:37 -07:00
|
|
|
elif self.window.is_key_pressed(ord(FRAME_SHIFT_KEY)):
|
2023-01-30 15:02:04 -08:00
|
|
|
frame.shift(-d_point)
|
2021-01-18 16:39:29 -08:00
|
|
|
|
2022-02-14 21:22:18 +08:00
|
|
|
def on_mouse_drag(
|
|
|
|
self,
|
2023-01-28 22:40:57 -08:00
|
|
|
point: Vect3,
|
|
|
|
d_point: Vect3,
|
2022-02-14 21:22:18 +08:00
|
|
|
buttons: int,
|
|
|
|
modifiers: int
|
|
|
|
) -> None:
|
2021-01-18 16:39:29 -08:00
|
|
|
self.mouse_drag_point.move_to(point)
|
2023-01-30 15:02:04 -08:00
|
|
|
self.frame.shift(-d_point)
|
2020-02-11 19:51:19 -08:00
|
|
|
|
2021-02-02 16:04:50 +05:30
|
|
|
event_data = {"point": point, "d_point": d_point, "buttons": buttons, "modifiers": modifiers}
|
|
|
|
propagate_event = EVENT_DISPATCHER.dispatch(EventType.MouseDragEvent, **event_data)
|
|
|
|
if propagate_event is not None and propagate_event is False:
|
|
|
|
return
|
2021-01-31 16:05:55 +05:30
|
|
|
|
2022-02-14 21:22:18 +08:00
|
|
|
def on_mouse_press(
|
|
|
|
self,
|
2023-01-28 22:40:57 -08:00
|
|
|
point: Vect3,
|
2022-02-14 21:22:18 +08:00
|
|
|
button: int,
|
|
|
|
mods: int
|
|
|
|
) -> None:
|
2022-04-20 21:50:37 -07:00
|
|
|
self.mouse_drag_point.move_to(point)
|
2021-02-02 16:04:50 +05:30
|
|
|
event_data = {"point": point, "button": button, "mods": mods}
|
|
|
|
propagate_event = EVENT_DISPATCHER.dispatch(EventType.MousePressEvent, **event_data)
|
|
|
|
if propagate_event is not None and propagate_event is False:
|
|
|
|
return
|
2020-02-11 19:51:19 -08:00
|
|
|
|
2022-02-14 21:22:18 +08:00
|
|
|
def on_mouse_release(
|
|
|
|
self,
|
2023-01-28 22:40:57 -08:00
|
|
|
point: Vect3,
|
2022-02-14 21:22:18 +08:00
|
|
|
button: int,
|
|
|
|
mods: int
|
|
|
|
) -> None:
|
2021-02-02 16:04:50 +05:30
|
|
|
event_data = {"point": point, "button": button, "mods": mods}
|
|
|
|
propagate_event = EVENT_DISPATCHER.dispatch(EventType.MouseReleaseEvent, **event_data)
|
|
|
|
if propagate_event is not None and propagate_event is False:
|
|
|
|
return
|
2020-02-11 19:51:19 -08:00
|
|
|
|
2022-02-14 21:22:18 +08:00
|
|
|
def on_mouse_scroll(
|
|
|
|
self,
|
2023-01-28 22:40:57 -08:00
|
|
|
point: Vect3,
|
2023-01-30 15:03:05 -08:00
|
|
|
offset: Vect3,
|
|
|
|
x_pixel_offset: float,
|
|
|
|
y_pixel_offset: float
|
2022-02-14 21:22:18 +08:00
|
|
|
) -> None:
|
2021-02-02 16:04:50 +05:30
|
|
|
event_data = {"point": point, "offset": offset}
|
|
|
|
propagate_event = EVENT_DISPATCHER.dispatch(EventType.MouseScrollEvent, **event_data)
|
|
|
|
if propagate_event is not None and propagate_event is False:
|
|
|
|
return
|
2021-01-28 14:02:43 +05:30
|
|
|
|
2023-01-30 15:03:05 -08:00
|
|
|
rel_offset = y_pixel_offset / self.camera.get_pixel_height()
|
|
|
|
self.frame.scale(
|
|
|
|
1 - self.scroll_sensitivity * rel_offset,
|
|
|
|
about_point=point
|
|
|
|
)
|
2020-02-11 19:51:19 -08:00
|
|
|
|
2022-02-14 21:22:18 +08:00
|
|
|
def on_key_release(
|
|
|
|
self,
|
|
|
|
symbol: int,
|
|
|
|
modifiers: int
|
|
|
|
) -> None:
|
2021-02-02 16:04:50 +05:30
|
|
|
event_data = {"symbol": symbol, "modifiers": modifiers}
|
|
|
|
propagate_event = EVENT_DISPATCHER.dispatch(EventType.KeyReleaseEvent, **event_data)
|
|
|
|
if propagate_event is not None and propagate_event is False:
|
|
|
|
return
|
2020-02-11 19:51:19 -08:00
|
|
|
|
2022-02-14 21:22:18 +08:00
|
|
|
def on_key_press(
|
|
|
|
self,
|
|
|
|
symbol: int,
|
|
|
|
modifiers: int
|
|
|
|
) -> None:
|
2021-01-16 10:21:42 +08:00
|
|
|
try:
|
|
|
|
char = chr(symbol)
|
|
|
|
except OverflowError:
|
2021-10-07 17:37:10 +08:00
|
|
|
log.warning("The value of the pressed key is too large.")
|
2021-01-16 10:21:42 +08:00
|
|
|
return
|
2021-01-28 14:02:43 +05:30
|
|
|
|
2021-02-02 16:04:50 +05:30
|
|
|
event_data = {"symbol": symbol, "modifiers": modifiers}
|
|
|
|
propagate_event = EVENT_DISPATCHER.dispatch(EventType.KeyPressEvent, **event_data)
|
|
|
|
if propagate_event is not None and propagate_event is False:
|
|
|
|
return
|
2021-01-28 14:02:43 +05:30
|
|
|
|
2022-04-20 21:50:37 -07:00
|
|
|
if char == RESET_FRAME_KEY:
|
2022-07-19 12:38:02 -07:00
|
|
|
self.play(self.camera.frame.animate.to_default_state())
|
2022-04-23 09:20:44 -07:00
|
|
|
elif char == "z" and modifiers == COMMAND_MODIFIER:
|
|
|
|
self.undo()
|
|
|
|
elif char == "z" and modifiers == COMMAND_MODIFIER | SHIFT_MODIFIER:
|
|
|
|
self.redo()
|
2022-04-20 21:50:37 -07:00
|
|
|
# command + q
|
|
|
|
elif char == QUIT_KEY and modifiers == COMMAND_MODIFIER:
|
2020-02-14 16:26:49 -08:00
|
|
|
self.quit_interaction = True
|
2022-04-20 21:50:37 -07:00
|
|
|
# Space or right arrow
|
|
|
|
elif char == " " or symbol == ARROW_SYMBOLS[2]:
|
2022-02-13 15:16:16 -08:00
|
|
|
self.hold_on_wait = False
|
2020-02-11 19:51:19 -08:00
|
|
|
|
2022-02-14 21:22:18 +08:00
|
|
|
def on_resize(self, width: int, height: int) -> None:
|
2023-01-23 14:02:06 -08:00
|
|
|
pass
|
2020-02-11 19:51:19 -08:00
|
|
|
|
2022-02-14 21:22:18 +08:00
|
|
|
def on_show(self) -> None:
|
2020-02-11 19:51:19 -08:00
|
|
|
pass
|
|
|
|
|
2022-02-14 21:22:18 +08:00
|
|
|
def on_hide(self) -> None:
|
2020-02-11 19:51:19 -08:00
|
|
|
pass
|
|
|
|
|
2022-02-14 21:22:18 +08:00
|
|
|
def on_close(self) -> None:
|
2020-02-11 19:51:19 -08:00
|
|
|
pass
|
|
|
|
|
2018-04-06 13:58:59 -07:00
|
|
|
|
2022-05-02 11:11:18 -07:00
|
|
|
class SceneState():
|
|
|
|
def __init__(self, scene: Scene, ignore: list[Mobject] | None = None):
|
|
|
|
self.time = scene.time
|
|
|
|
self.num_plays = scene.num_plays
|
|
|
|
self.mobjects_to_copies = OrderedDict.fromkeys(scene.mobjects)
|
|
|
|
if ignore:
|
|
|
|
for mob in ignore:
|
|
|
|
self.mobjects_to_copies.pop(mob, None)
|
|
|
|
|
|
|
|
last_m2c = scene.undo_stack[-1].mobjects_to_copies if scene.undo_stack else dict()
|
|
|
|
for mob in self.mobjects_to_copies:
|
|
|
|
# If it hasn't changed since the last state, just point to the
|
|
|
|
# same copy as before
|
|
|
|
if mob in last_m2c and last_m2c[mob].looks_identical(mob):
|
|
|
|
self.mobjects_to_copies[mob] = last_m2c[mob]
|
|
|
|
else:
|
|
|
|
self.mobjects_to_copies[mob] = mob.copy()
|
|
|
|
|
|
|
|
def __eq__(self, state: SceneState):
|
|
|
|
return all((
|
|
|
|
self.time == state.time,
|
|
|
|
self.num_plays == state.num_plays,
|
|
|
|
self.mobjects_to_copies == state.mobjects_to_copies
|
|
|
|
))
|
|
|
|
|
|
|
|
def mobjects_match(self, state: SceneState):
|
|
|
|
return self.mobjects_to_copies == state.mobjects_to_copies
|
|
|
|
|
|
|
|
def n_changes(self, state: SceneState):
|
|
|
|
m2c = state.mobjects_to_copies
|
|
|
|
return sum(
|
|
|
|
1 - int(mob in m2c and mob.looks_identical(m2c[mob]))
|
|
|
|
for mob in self.mobjects_to_copies
|
|
|
|
)
|
|
|
|
|
|
|
|
def restore_scene(self, scene: Scene):
|
|
|
|
scene.time = self.time
|
|
|
|
scene.num_plays = self.num_plays
|
|
|
|
scene.mobjects = [
|
|
|
|
mob.become(mob_copy)
|
|
|
|
for mob, mob_copy in self.mobjects_to_copies.items()
|
|
|
|
]
|
|
|
|
|
|
|
|
|
2022-05-03 12:41:44 -07:00
|
|
|
class EndScene(Exception):
|
2018-02-19 17:24:17 -08:00
|
|
|
pass
|
2023-01-28 12:02:19 -08:00
|
|
|
|
|
|
|
|
|
|
|
class ThreeDScene(Scene):
|
|
|
|
samples = 4
|
|
|
|
default_frame_orientation = (-30, 70)
|
2023-01-28 15:02:00 -08:00
|
|
|
|
|
|
|
def add(self, *mobjects, set_depth_test: bool = True):
|
|
|
|
for mob in mobjects:
|
|
|
|
if set_depth_test and not mob.is_fixed_in_frame():
|
|
|
|
mob.apply_depth_test()
|
|
|
|
super().add(*mobjects)
|