mirror of
https://github.com/3b1b/manim.git
synced 2025-09-01 00:48:45 +00:00
commit
1a14a6bd0d
14 changed files with 378 additions and 446 deletions
|
@ -3,7 +3,7 @@ from manimlib import __version__
|
|||
import manimlib.config
|
||||
import manimlib.logger
|
||||
import manimlib.utils.init_config
|
||||
from manimlib.reload_manager import reload_manager
|
||||
from manimlib.reload_manager import ReloadManager
|
||||
|
||||
|
||||
def main():
|
||||
|
@ -22,7 +22,7 @@ def main():
|
|||
manimlib.utils.init_config.init_customization()
|
||||
return
|
||||
|
||||
reload_manager.args = args
|
||||
reload_manager = ReloadManager(args)
|
||||
reload_manager.run()
|
||||
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from argparse import Namespace
|
||||
import colour
|
||||
import importlib
|
||||
import inspect
|
||||
|
@ -13,21 +12,15 @@ import yaml
|
|||
from functools import lru_cache
|
||||
|
||||
from manimlib.logger import log
|
||||
from manimlib.module_loader import ModuleLoader
|
||||
from manimlib.utils.dict_ops import merge_dicts_recursively
|
||||
from manimlib.utils.init_config import init_customization
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
if TYPE_CHECKING:
|
||||
Module = importlib.util.types.ModuleType
|
||||
from argparse import Namespace
|
||||
from typing import Optional
|
||||
|
||||
|
||||
# This has to be here instead of in constants.py
|
||||
# due to its use in creating the camera configuration
|
||||
FRAME_HEIGHT: float = 8.0
|
||||
|
||||
|
||||
def parse_cli():
|
||||
try:
|
||||
parser = argparse.ArgumentParser()
|
||||
|
@ -144,12 +137,8 @@ def parse_cli():
|
|||
)
|
||||
parser.add_argument(
|
||||
"-e", "--embed",
|
||||
nargs="?",
|
||||
const="",
|
||||
help="Creates a new file where the line `self.embed` is inserted " + \
|
||||
"into the Scenes construct method. " + \
|
||||
"If a string is passed in, the line will be inserted below the " + \
|
||||
"last line of code including that string."
|
||||
"at the corresponding line number"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-r", "--resolution",
|
||||
|
@ -210,93 +199,6 @@ def get_manim_dir():
|
|||
return os.path.abspath(os.path.join(manimlib_dir, ".."))
|
||||
|
||||
|
||||
def get_indent(line: str):
|
||||
return len(line) - len(line.lstrip())
|
||||
|
||||
|
||||
def get_module_with_inserted_embed_line(
|
||||
file_name: str, scene_name: str, line_marker: str, is_during_reload
|
||||
):
|
||||
"""
|
||||
This is hacky, but convenient. When user includes the argument "-e", it will try
|
||||
to recreate a file that inserts the line `self.embed()` into the end of the scene's
|
||||
construct method. If there is an argument passed in, it will insert the line after
|
||||
the last line in the sourcefile which includes that string.
|
||||
"""
|
||||
with open(file_name, 'r') as fp:
|
||||
lines = fp.readlines()
|
||||
|
||||
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}")
|
||||
return
|
||||
|
||||
prev_line_num = -1
|
||||
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 or 0):
|
||||
prev_line_num = index - 1
|
||||
break
|
||||
if prev_line_num < 0:
|
||||
prev_line_num = len(lines) - 1
|
||||
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(scene_line_number, len(lines) - 1)
|
||||
if line_marker in lines[i]
|
||||
)
|
||||
except StopIteration:
|
||||
log.error(f"No lines matching {line_marker}")
|
||||
sys.exit(2)
|
||||
|
||||
# Insert the embed line, rewrite file, then write it back when done
|
||||
if n_spaces is None:
|
||||
n_spaces = get_indent(lines[prev_line_num])
|
||||
inserted_line = " " * n_spaces + "self.embed()\n"
|
||||
new_lines = list(lines)
|
||||
new_lines.insert(prev_line_num + 1, inserted_line)
|
||||
new_file = file_name.replace(".py", "_insert_embed.py")
|
||||
|
||||
with open(new_file, 'w') as fp:
|
||||
fp.writelines(new_lines)
|
||||
|
||||
module = ModuleLoader.get_module(new_file, is_during_reload)
|
||||
# This is to pretend the module imported from the edited lines
|
||||
# of code actually comes from the original file.
|
||||
module.__file__ = file_name
|
||||
|
||||
os.remove(new_file)
|
||||
|
||||
return module
|
||||
|
||||
|
||||
def get_scene_module(args: Namespace) -> Module:
|
||||
if args.embed is None:
|
||||
return ModuleLoader.get_module(args.file)
|
||||
else:
|
||||
is_reload = args.is_reload if hasattr(args, "is_reload") else False
|
||||
return get_module_with_inserted_embed_line(
|
||||
args.file, args.scene_names[0], args.embed, is_reload
|
||||
)
|
||||
|
||||
|
||||
def load_yaml(file_path: str):
|
||||
try:
|
||||
with open(file_path, "r") as file:
|
||||
|
@ -343,8 +245,8 @@ def get_animations_numbers(args: Namespace) -> tuple[int | None, int | None]:
|
|||
return int(stan), None
|
||||
|
||||
|
||||
def get_output_directory(args: Namespace, custom_config: dict) -> str:
|
||||
dir_config = custom_config["directories"]
|
||||
def get_output_directory(args: Namespace, global_config: dict) -> str:
|
||||
dir_config = global_config["directories"]
|
||||
output_directory = args.video_dir or dir_config["output"]
|
||||
if dir_config["mirror_module_path"] and args.file:
|
||||
to_cut = dir_config["removed_mirror_prefix"]
|
||||
|
@ -356,7 +258,7 @@ def get_output_directory(args: Namespace, custom_config: dict) -> str:
|
|||
return output_directory
|
||||
|
||||
|
||||
def get_file_writer_config(args: Namespace, custom_config: dict) -> dict:
|
||||
def get_file_writer_config(args: Namespace, global_config: dict) -> dict:
|
||||
result = {
|
||||
"write_to_movie": not args.skip_animations and args.write_file,
|
||||
"save_last_frame": args.skip_animations and args.write_file,
|
||||
|
@ -364,13 +266,13 @@ def get_file_writer_config(args: Namespace, custom_config: dict) -> dict:
|
|||
# If -t is passed in (for transparent), this will be RGBA
|
||||
"png_mode": "RGBA" if args.transparent else "RGB",
|
||||
"movie_file_extension": get_file_ext(args),
|
||||
"output_directory": get_output_directory(args, custom_config),
|
||||
"output_directory": get_output_directory(args, global_config),
|
||||
"file_name": args.file_name,
|
||||
"input_file_path": args.file or "",
|
||||
"open_file_upon_completion": args.open,
|
||||
"show_file_location_upon_completion": args.finder,
|
||||
"quiet": args.quiet,
|
||||
**custom_config["file_writer_config"],
|
||||
**global_config["file_writer_config"],
|
||||
}
|
||||
|
||||
if args.vcodec:
|
||||
|
@ -387,32 +289,11 @@ def get_file_writer_config(args: Namespace, custom_config: dict) -> dict:
|
|||
return result
|
||||
|
||||
|
||||
def get_window_config(args: Namespace, custom_config: dict, camera_config: dict) -> dict:
|
||||
# Default to making window half the screen size
|
||||
# but make it full screen if -f is passed in
|
||||
try:
|
||||
monitors = screeninfo.get_monitors()
|
||||
except screeninfo.ScreenInfoError:
|
||||
# Default fallback
|
||||
monitors = [screeninfo.Monitor(width=1920, height=1080)]
|
||||
mon_index = custom_config["window_monitor"]
|
||||
monitor = monitors[min(mon_index, len(monitors) - 1)]
|
||||
aspect_ratio = camera_config["pixel_width"] / camera_config["pixel_height"]
|
||||
window_width = monitor.width
|
||||
if not (args.full_screen or custom_config["full_screen"]):
|
||||
window_width //= 2
|
||||
window_height = int(window_width / aspect_ratio)
|
||||
return dict(size=(window_width, window_height))
|
||||
def get_resolution(args: Optional[Namespace] = None, global_config: Optional[dict] = None):
|
||||
args = args or parse_cli()
|
||||
global_config = global_config or get_global_config()
|
||||
|
||||
|
||||
def get_camera_config(args: Optional[Namespace] = None, custom_config: Optional[dict] = None) -> dict:
|
||||
if args is None:
|
||||
args = parse_cli()
|
||||
if custom_config is None:
|
||||
custom_config = get_global_config()
|
||||
|
||||
camera_config = dict()
|
||||
camera_resolutions = custom_config["camera_resolutions"]
|
||||
camera_resolutions = global_config["camera_resolutions"]
|
||||
if args.resolution:
|
||||
resolution = args.resolution
|
||||
elif args.low_quality:
|
||||
|
@ -426,33 +307,53 @@ def get_camera_config(args: Optional[Namespace] = None, custom_config: Optional[
|
|||
else:
|
||||
resolution = camera_resolutions[camera_resolutions["default_resolution"]]
|
||||
|
||||
if args.fps:
|
||||
fps = int(args.fps)
|
||||
else:
|
||||
fps = custom_config["fps"]
|
||||
|
||||
width_str, height_str = resolution.split("x")
|
||||
width = int(width_str)
|
||||
height = int(height_str)
|
||||
return int(width_str), int(height_str)
|
||||
|
||||
camera_config.update({
|
||||
|
||||
def get_window_config(args: Namespace, global_config: dict) -> dict:
|
||||
# Default to making window half the screen size
|
||||
# but make it full screen if -f is passed in
|
||||
try:
|
||||
monitors = screeninfo.get_monitors()
|
||||
except screeninfo.ScreenInfoError:
|
||||
# Default fallback
|
||||
monitors = [screeninfo.Monitor(width=1920, height=1080)]
|
||||
mon_index = global_config["window_monitor"]
|
||||
monitor = monitors[min(mon_index, len(monitors) - 1)]
|
||||
|
||||
width, height = get_resolution(args, global_config)
|
||||
|
||||
aspect_ratio = width / height
|
||||
window_width = monitor.width
|
||||
if not (args.full_screen or global_config["full_screen"]):
|
||||
window_width //= 2
|
||||
window_height = int(window_width / aspect_ratio)
|
||||
return dict(size=(window_width, window_height))
|
||||
|
||||
|
||||
def get_camera_config(args: Optional[Namespace] = None, global_config: Optional[dict] = None) -> dict:
|
||||
args = args or parse_cli()
|
||||
global_config = global_config or get_global_config()
|
||||
|
||||
width, height = get_resolution(args, global_config)
|
||||
fps = int(args.fps or global_config["fps"])
|
||||
|
||||
camera_config = {
|
||||
"pixel_width": width,
|
||||
"pixel_height": height,
|
||||
"frame_config": {
|
||||
"frame_shape": ((width / height) * FRAME_HEIGHT, FRAME_HEIGHT),
|
||||
},
|
||||
"fps": fps,
|
||||
})
|
||||
}
|
||||
|
||||
try:
|
||||
bg_color = args.color or custom_config["style"]["background_color"]
|
||||
bg_color = args.color or global_config["style"]["background_color"]
|
||||
camera_config["background_color"] = colour.Color(bg_color)
|
||||
except ValueError as err:
|
||||
log.error("Please use a valid color")
|
||||
log.error(err)
|
||||
sys.exit(2)
|
||||
|
||||
# If rendering a transparent image/move, make sure the
|
||||
# If rendering a transparent image/movie, make sure the
|
||||
# scene has a background opacity of 0
|
||||
if args.transparent:
|
||||
camera_config["background_opacity"] = 0
|
||||
|
@ -466,17 +367,15 @@ def get_scene_config(args: Namespace) -> dict:
|
|||
"""
|
||||
global_config = get_global_config()
|
||||
camera_config = get_camera_config(args, global_config)
|
||||
window_config = get_window_config(args, global_config, camera_config)
|
||||
file_writer_config = get_file_writer_config(args, global_config)
|
||||
start, end = get_animations_numbers(args)
|
||||
|
||||
return {
|
||||
"file_writer_config": get_file_writer_config(args, global_config),
|
||||
"file_writer_config": file_writer_config,
|
||||
"camera_config": camera_config,
|
||||
"window_config": window_config,
|
||||
"skip_animations": args.skip_animations,
|
||||
"start_at_animation_number": start,
|
||||
"end_at_animation_number": end,
|
||||
"preview": not args.write_file,
|
||||
"presenter_mode": args.presenter_mode,
|
||||
"leave_progress_bars": args.leave_progress_bars,
|
||||
"show_animation_progress": args.show_animation_progress,
|
||||
|
@ -486,10 +385,15 @@ def get_scene_config(args: Namespace) -> dict:
|
|||
|
||||
|
||||
def get_run_config(args: Namespace):
|
||||
window_config = get_window_config(args, get_global_config())
|
||||
return {
|
||||
"module": get_scene_module(args),
|
||||
"file_name": args.file,
|
||||
"embed_line": int(args.embed) if args.embed is not None else None,
|
||||
"is_reload": False,
|
||||
"prerun": args.prerun,
|
||||
"scene_names": args.scene_names,
|
||||
"quiet": args.quiet or args.write_all,
|
||||
"write_all": args.write_all,
|
||||
"window_config": window_config,
|
||||
"show_in_window": not args.write_file
|
||||
}
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
from __future__ import annotations
|
||||
import numpy as np
|
||||
|
||||
from manimlib.config import get_camera_config
|
||||
from manimlib.config import FRAME_HEIGHT
|
||||
from manimlib.config import get_resolution
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
if TYPE_CHECKING:
|
||||
|
@ -13,18 +12,19 @@ if TYPE_CHECKING:
|
|||
# TODO, it feels a bit unprincipled to have some global constants
|
||||
# depend on the output of this function, and for all that configuration
|
||||
# code to be run merely upon importing from this file.
|
||||
CAMERA_CONFIG = get_camera_config()
|
||||
DEFAULT_RESOLUTION: tuple[int, int] = get_resolution()
|
||||
DEFAULT_PIXEL_WIDTH = DEFAULT_RESOLUTION[0]
|
||||
DEFAULT_PIXEL_HEIGHT = DEFAULT_RESOLUTION[1]
|
||||
DEFAULT_FPS: int = 30
|
||||
|
||||
# Sizes relevant to default camera frame
|
||||
ASPECT_RATIO: float = CAMERA_CONFIG['pixel_width'] / CAMERA_CONFIG['pixel_height']
|
||||
ASPECT_RATIO: float = DEFAULT_PIXEL_WIDTH / DEFAULT_PIXEL_HEIGHT
|
||||
FRAME_HEIGHT: float = 8.0
|
||||
FRAME_WIDTH: float = FRAME_HEIGHT * ASPECT_RATIO
|
||||
FRAME_SHAPE: tuple[float, float] = (FRAME_WIDTH, FRAME_HEIGHT)
|
||||
FRAME_Y_RADIUS: float = FRAME_HEIGHT / 2
|
||||
FRAME_X_RADIUS: float = FRAME_WIDTH / 2
|
||||
|
||||
DEFAULT_PIXEL_HEIGHT: int = CAMERA_CONFIG['pixel_height']
|
||||
DEFAULT_PIXEL_WIDTH: int = CAMERA_CONFIG['pixel_width']
|
||||
DEFAULT_FPS: int = 30
|
||||
|
||||
SMALL_BUFF: float = 0.1
|
||||
MED_SMALL_BUFF: float = 0.25
|
||||
|
|
|
@ -58,3 +58,4 @@ camera_resolutions:
|
|||
fps: 30
|
||||
embed_exception_mode: "Verbose"
|
||||
embed_error_sound: False
|
||||
ignore_manimlib_modules_on_reload: True
|
||||
|
|
|
@ -1,12 +1,21 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import inspect
|
||||
import sys
|
||||
|
||||
from manimlib.module_loader import ModuleLoader
|
||||
|
||||
from manimlib.config import get_global_config
|
||||
from manimlib.logger import log
|
||||
from manimlib.scene.interactive_scene import InteractiveScene
|
||||
from manimlib.scene.scene import Scene
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
if TYPE_CHECKING:
|
||||
Module = importlib.util.types.ModuleType
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class BlankScene(InteractiveScene):
|
||||
def construct(self):
|
||||
|
@ -115,10 +124,60 @@ def get_scene_classes_from_module(module):
|
|||
]
|
||||
|
||||
|
||||
def get_indent(code_lines: list[str], line_number: int) -> str:
|
||||
"""
|
||||
Find the indent associated with a given line of python code,
|
||||
as a string of spaces
|
||||
"""
|
||||
# Find most recent non-empty line
|
||||
try:
|
||||
line = next(filter(lambda line: line.strip(), code_lines[line_number - 1::-1]))
|
||||
except StopIteration:
|
||||
return ""
|
||||
|
||||
# Either return its leading spaces, or add for if it ends with colon
|
||||
n_spaces = len(line) - len(line.lstrip())
|
||||
if line.endswith(":"):
|
||||
n_spaces += 4
|
||||
return n_spaces * " "
|
||||
|
||||
|
||||
def insert_embed_line_to_module(module: Module, line_number: int):
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
lines = inspect.getsource(module).splitlines()
|
||||
|
||||
# Add the relevant embed line to the code
|
||||
indent = get_indent(lines, line_number)
|
||||
lines.insert(line_number, indent + "self.embed()")
|
||||
new_code = "\n".join(lines)
|
||||
|
||||
# Execute the code, which presumably redefines the user's
|
||||
# scene to include this embed line, within the relevant module.
|
||||
code_object = compile(new_code, module.__name__, 'exec')
|
||||
exec(code_object, module.__dict__)
|
||||
|
||||
|
||||
def get_scene_module(file_name: Optional[str], embed_line: Optional[int], is_reload: bool = False) -> Module:
|
||||
module = ModuleLoader.get_module(file_name, is_reload)
|
||||
if embed_line:
|
||||
insert_embed_line_to_module(module, embed_line)
|
||||
return module
|
||||
|
||||
|
||||
def main(scene_config, run_config):
|
||||
if run_config["module"] is None:
|
||||
module = get_scene_module(
|
||||
run_config["file_name"],
|
||||
run_config["embed_line"],
|
||||
run_config["is_reload"]
|
||||
)
|
||||
if module is None:
|
||||
# If no module was passed in, just play the blank scene
|
||||
return [BlankScene(**scene_config)]
|
||||
|
||||
all_scene_classes = get_scene_classes_from_module(run_config["module"])
|
||||
all_scene_classes = get_scene_classes_from_module(module)
|
||||
return get_scenes_to_render(all_scene_classes, scene_config, run_config)
|
||||
|
|
|
@ -11,4 +11,4 @@ logging.basicConfig(
|
|||
)
|
||||
|
||||
log = logging.getLogger("manimgl")
|
||||
log.setLevel("DEBUG")
|
||||
log.setLevel("WARNING")
|
||||
|
|
|
@ -715,21 +715,6 @@ class Mobject(object):
|
|||
self.become(self.saved_state)
|
||||
return self
|
||||
|
||||
def save_to_file(self, file_path: str) -> Self:
|
||||
with open(file_path, "wb") as fp:
|
||||
fp.write(self.serialize())
|
||||
log.info(f"Saved mobject to {file_path}")
|
||||
return self
|
||||
|
||||
@staticmethod
|
||||
def load(file_path) -> Mobject:
|
||||
if not os.path.exists(file_path):
|
||||
log.error(f"No file found at {file_path}")
|
||||
sys.exit(2)
|
||||
with open(file_path, "rb") as fp:
|
||||
mobject = pickle.load(fp)
|
||||
return mobject
|
||||
|
||||
def become(self, mobject: Mobject, match_updaters=False) -> Self:
|
||||
"""
|
||||
Edit all data and submobjects to be idential
|
||||
|
|
|
@ -6,12 +6,11 @@ import os
|
|||
import sys
|
||||
import sysconfig
|
||||
|
||||
from manimlib.config import get_global_config
|
||||
from manimlib.logger import log
|
||||
|
||||
Module = importlib.util.types.ModuleType
|
||||
|
||||
IGNORE_MANIMLIB_MODULES = True
|
||||
|
||||
|
||||
class ModuleLoader:
|
||||
"""
|
||||
|
@ -76,10 +75,7 @@ class ModuleLoader:
|
|||
builtins.__import__ = tracked_import
|
||||
|
||||
try:
|
||||
# Remove the "_insert_embed" suffix from the module name
|
||||
module_name = module.__name__
|
||||
if module.__name__.endswith("_insert_embed"):
|
||||
module_name = module_name[:-13]
|
||||
log.debug('Reloading module "%s"', module_name)
|
||||
|
||||
spec.loader.exec_module(module)
|
||||
|
@ -146,7 +142,8 @@ class ModuleLoader:
|
|||
|
||||
Only user-defined modules are reloaded, see `is_user_defined_module()`.
|
||||
"""
|
||||
if IGNORE_MANIMLIB_MODULES and module.__name__.startswith("manimlib"):
|
||||
ignore_manimlib_modules = get_global_config()["ignore_manimlib_modules_on_reload"]
|
||||
if ignore_manimlib_modules and module.__name__.startswith("manimlib"):
|
||||
return
|
||||
|
||||
if not hasattr(module, "__dict__"):
|
||||
|
|
|
@ -1,7 +1,20 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
from IPython.terminal.embed import KillEmbedded
|
||||
|
||||
|
||||
import manimlib.config
|
||||
import manimlib.extract_scene
|
||||
|
||||
from manimlib.window import Window
|
||||
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
if TYPE_CHECKING:
|
||||
from argparse import Namespace
|
||||
|
||||
|
||||
class ReloadManager:
|
||||
"""
|
||||
Manages the loading and running of scenes and is called directly from the
|
||||
|
@ -12,20 +25,17 @@ class ReloadManager:
|
|||
command in the IPython shell.
|
||||
"""
|
||||
|
||||
args: Any = None
|
||||
scenes: list[Any] = []
|
||||
window = None
|
||||
|
||||
# The line number to load the scene from when reloading
|
||||
start_at_line = None
|
||||
|
||||
is_reload = False
|
||||
|
||||
def __init__(self, cli_args: Namespace):
|
||||
self.args = cli_args
|
||||
|
||||
def set_new_start_at_line(self, start_at_line):
|
||||
"""
|
||||
Sets/Updates the line number to load the scene from when reloading.
|
||||
"""
|
||||
self.start_at_line = start_at_line
|
||||
self.args.embed = str(start_at_line)
|
||||
|
||||
def run(self):
|
||||
"""
|
||||
|
@ -34,61 +44,43 @@ class ReloadManager:
|
|||
while True:
|
||||
try:
|
||||
# blocking call since a scene will init an IPython shell()
|
||||
self.retrieve_scenes_and_run(self.start_at_line)
|
||||
self.retrieve_scenes_and_run()
|
||||
return
|
||||
except KillEmbedded:
|
||||
# Requested via the `exit_raise` IPython runline magic
|
||||
# by means of our scene.reload() command
|
||||
for scene in self.scenes:
|
||||
scene.tear_down()
|
||||
|
||||
self.scenes = []
|
||||
self.is_reload = True
|
||||
|
||||
self.note_reload()
|
||||
except KeyboardInterrupt:
|
||||
break
|
||||
|
||||
def retrieve_scenes_and_run(self, overwrite_start_at_line: int | None = None):
|
||||
def note_reload(self):
|
||||
self.is_reload = True
|
||||
print(" ".join([
|
||||
"Reloading interactive session for",
|
||||
f"\033[96m{self.args.scene_names[0]}\033[0m",
|
||||
f"at line \033[96m{self.args.embed}\033[0m"
|
||||
]))
|
||||
|
||||
def retrieve_scenes_and_run(self):
|
||||
"""
|
||||
Creates a new configuration based on the CLI args and runs the scenes.
|
||||
"""
|
||||
import manimlib.config
|
||||
import manimlib.extract_scene
|
||||
|
||||
# Args
|
||||
if self.args is None:
|
||||
raise RuntimeError("Fatal error: No args were passed to the ReloadManager")
|
||||
if overwrite_start_at_line is not None:
|
||||
self.args.embed = str(overwrite_start_at_line)
|
||||
|
||||
# Args to Config
|
||||
self.args.is_reload = self.is_reload
|
||||
scene_config = manimlib.config.get_scene_config(self.args)
|
||||
if self.window:
|
||||
scene_config["existing_window"] = self.window # see scene initialization
|
||||
scene_config.update(reload_manager=self)
|
||||
|
||||
run_config = manimlib.config.get_run_config(self.args)
|
||||
run_config.update(is_reload=self.is_reload)
|
||||
|
||||
# Create or reuse window
|
||||
if run_config["show_in_window"] and not self.window:
|
||||
self.window = Window(**run_config["window_config"])
|
||||
scene_config.update(window=self.window)
|
||||
|
||||
# Scenes
|
||||
self.scenes = manimlib.extract_scene.main(scene_config, run_config)
|
||||
if len(self.scenes) == 0:
|
||||
scenes = manimlib.extract_scene.main(scene_config, run_config)
|
||||
if len(scenes) == 0:
|
||||
print("No scenes found to run")
|
||||
return
|
||||
|
||||
# Find first available window
|
||||
for scene in self.scenes:
|
||||
if scene.window is not None:
|
||||
self.window = scene.window
|
||||
break
|
||||
|
||||
for scene in self.scenes:
|
||||
if self.args.embed:
|
||||
print(" ".join([
|
||||
"Loading interactive session for",
|
||||
f"\033[96m{self.args.scene_names[0]}\033[0m",
|
||||
f"in \033[96m{self.args.file}\033[0m",
|
||||
f"at line \033[96m{self.args.embed}\033[0m"
|
||||
]))
|
||||
for scene in scenes:
|
||||
scene.run()
|
||||
|
||||
reload_manager = ReloadManager()
|
||||
|
|
|
@ -460,12 +460,6 @@ class InteractiveScene(Scene):
|
|||
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)
|
||||
|
||||
# Key actions
|
||||
def on_key_press(self, symbol: int, modifiers: int) -> None:
|
||||
super().on_key_press(symbol, modifiers)
|
||||
|
@ -503,8 +497,6 @@ class InteractiveScene(Scene):
|
|||
self.ungroup_selection()
|
||||
elif char == "t" and (modifiers & (PygletWindowKeys.MOD_COMMAND | PygletWindowKeys.MOD_CTRL)):
|
||||
self.toggle_selection_mode()
|
||||
elif char == "s" and (modifiers & (PygletWindowKeys.MOD_COMMAND | PygletWindowKeys.MOD_CTRL)):
|
||||
self.save_selection_to_file()
|
||||
elif char == "d" and (modifiers & PygletWindowKeys.MOD_SHIFT):
|
||||
self.copy_frame_positioning()
|
||||
elif char == "c" and (modifiers & PygletWindowKeys.MOD_SHIFT):
|
||||
|
|
|
@ -1,41 +1,33 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from collections import OrderedDict
|
||||
import inspect
|
||||
import os
|
||||
import platform
|
||||
import pyperclip
|
||||
import random
|
||||
import time
|
||||
import re
|
||||
from functools import wraps
|
||||
|
||||
from IPython.terminal import pt_inputhooks
|
||||
from IPython.terminal.embed import InteractiveShellEmbed
|
||||
from pyglet.window import key as PygletWindowKeys
|
||||
|
||||
import numpy as np
|
||||
from tqdm.auto import tqdm as ProgressDisplay
|
||||
|
||||
from manimlib.animation.animation import prepare_animation
|
||||
from manimlib.animation.fading import VFadeInThenOut
|
||||
from manimlib.camera.camera import Camera
|
||||
from manimlib.camera.camera_frame import CameraFrame
|
||||
from manimlib.module_loader import ModuleLoader
|
||||
from manimlib.constants import ARROW_SYMBOLS
|
||||
from manimlib.constants import DEFAULT_WAIT_TIME
|
||||
from manimlib.constants import RED
|
||||
from manimlib.event_handler import EVENT_DISPATCHER
|
||||
from manimlib.event_handler.event_type import EventType
|
||||
from manimlib.logger import log
|
||||
from manimlib.reload_manager import reload_manager
|
||||
from manimlib.mobject.frame import FullScreenRectangle
|
||||
from manimlib.mobject.mobject import _AnimationBuilder
|
||||
from manimlib.mobject.mobject import Group
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.mobject.mobject import Point
|
||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
from manimlib.scene.scene_embed import interactive_scene_embed
|
||||
from manimlib.scene.scene_embed import CheckpointManager
|
||||
from manimlib.scene.scene_file_writer import SceneFileWriter
|
||||
from manimlib.utils.family_ops import extract_mobject_family_members
|
||||
from manimlib.utils.family_ops import recursive_mobject_remove
|
||||
|
@ -45,13 +37,14 @@ from manimlib.window import Window
|
|||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Callable, Iterable, TypeVar
|
||||
from typing import Callable, Iterable, TypeVar, Optional
|
||||
from manimlib.typing import Vect3
|
||||
|
||||
T = TypeVar('T')
|
||||
|
||||
from PIL.Image import Image
|
||||
|
||||
from manimlib.reload_manager import ReloadManager
|
||||
from manimlib.animation.animation import Animation
|
||||
|
||||
|
||||
|
@ -68,7 +61,6 @@ class Scene(object):
|
|||
drag_to_pan: bool = True
|
||||
max_num_saved_states: int = 50
|
||||
default_camera_config: dict = dict()
|
||||
default_window_config: dict = dict()
|
||||
default_file_writer_config: dict = dict()
|
||||
samples = 0
|
||||
# Euler angles, in degrees
|
||||
|
@ -76,7 +68,6 @@ class Scene(object):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
window_config: dict = dict(),
|
||||
camera_config: dict = dict(),
|
||||
file_writer_config: dict = dict(),
|
||||
skip_animations: bool = False,
|
||||
|
@ -84,45 +75,39 @@ class Scene(object):
|
|||
start_at_animation_number: int | None = None,
|
||||
end_at_animation_number: int | None = None,
|
||||
leave_progress_bars: bool = False,
|
||||
preview: bool = True,
|
||||
window: Optional[Window] = None,
|
||||
reload_manager: Optional[ReloadManager] = None,
|
||||
presenter_mode: bool = False,
|
||||
show_animation_progress: bool = False,
|
||||
embed_exception_mode: str = "",
|
||||
embed_error_sound: bool = False,
|
||||
existing_window: Window | None = None,
|
||||
):
|
||||
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
|
||||
self.embed_exception_mode = embed_exception_mode
|
||||
self.embed_error_sound = embed_error_sound
|
||||
self.reload_manager = reload_manager
|
||||
|
||||
self.camera_config = {**self.default_camera_config, **camera_config}
|
||||
self.window_config = {**self.default_window_config, **window_config}
|
||||
for config in self.camera_config, self.window_config:
|
||||
config["samples"] = self.samples
|
||||
self.file_writer_config = {**self.default_file_writer_config, **file_writer_config}
|
||||
|
||||
# Initialize window, if applicable (and reuse window if provided during
|
||||
# reload by means of the ReloadManager)
|
||||
if self.preview:
|
||||
if existing_window:
|
||||
self.window = existing_window
|
||||
self.window.update_scene(self)
|
||||
else:
|
||||
self.window = Window(scene=self, **self.window_config)
|
||||
self.camera_config["fps"] = 30 # Where's that 30 from?
|
||||
self.camera_config["window"] = self.window
|
||||
else:
|
||||
self.window = None
|
||||
self.window = window
|
||||
if self.window:
|
||||
self.window.init_for_scene(self)
|
||||
# Make sure camera and Pyglet window sync
|
||||
self.camera_config["fps"] = 30
|
||||
|
||||
# Core state of the scene
|
||||
self.camera: Camera = Camera(**self.camera_config)
|
||||
self.camera: Camera = Camera(
|
||||
window=self.window,
|
||||
samples=self.samples,
|
||||
**self.camera_config
|
||||
)
|
||||
self.frame: CameraFrame = self.camera.frame
|
||||
self.frame.reorient(*self.default_frame_orientation)
|
||||
self.frame.make_orientation_default()
|
||||
|
@ -136,6 +121,7 @@ class Scene(object):
|
|||
self.skip_time: float = 0
|
||||
self.original_skipping_status: bool = self.skip_animations
|
||||
self.checkpoint_states: dict[str, list[tuple[Mobject, Mobject]]] = dict()
|
||||
self.checkpoint_manager: CheckpointManager = CheckpointManager()
|
||||
self.undo_stack = []
|
||||
self.redo_stack = []
|
||||
|
||||
|
@ -220,80 +206,12 @@ class Scene(object):
|
|||
close_scene_on_exit: bool = True,
|
||||
show_animation_progress: bool = False,
|
||||
) -> None:
|
||||
if not self.preview:
|
||||
# Embed is only relevant with a preview
|
||||
if not self.window:
|
||||
# Embed is only relevant for interactive development with a Window
|
||||
return
|
||||
self.stop_skipping()
|
||||
self.update_frame(force_draw=True)
|
||||
self.save_state()
|
||||
self.show_animation_progress = show_animation_progress
|
||||
|
||||
# Create embedded IPython terminal configured to have access to
|
||||
# the local namespace of the caller
|
||||
caller_frame = inspect.currentframe().f_back
|
||||
module = ModuleLoader.get_module(caller_frame.f_globals["__file__"])
|
||||
shell = InteractiveShellEmbed(
|
||||
user_module=module,
|
||||
display_banner=False,
|
||||
xmode=self.embed_exception_mode
|
||||
)
|
||||
self.shell = shell
|
||||
|
||||
# Add a few custom shortcuts to that local namespace
|
||||
local_ns = dict(caller_frame.f_locals)
|
||||
local_ns.update(
|
||||
play=self.play,
|
||||
wait=self.wait,
|
||||
add=self.add,
|
||||
remove=self.remove,
|
||||
clear=self.clear,
|
||||
focus=self.focus,
|
||||
save_state=self.save_state,
|
||||
reload=self.reload,
|
||||
undo=self.undo,
|
||||
redo=self.redo,
|
||||
i2g=self.i2g,
|
||||
i2m=self.i2m,
|
||||
checkpoint_paste=self.checkpoint_paste,
|
||||
touch=lambda: shell.enable_gui("manim"),
|
||||
notouch=lambda: shell.enable_gui(None),
|
||||
)
|
||||
|
||||
# Update the shell module with the caller's locals + shortcuts
|
||||
module.__dict__.update(local_ns)
|
||||
|
||||
# Enables gui interactions during the embed
|
||||
def inputhook(context):
|
||||
while not context.input_is_ready():
|
||||
if not self.is_window_closing():
|
||||
self.update_frame(dt=0)
|
||||
if self.is_window_closing():
|
||||
shell.ask_exit()
|
||||
|
||||
pt_inputhooks.register("manim", inputhook)
|
||||
shell.enable_gui("manim")
|
||||
|
||||
# Operation to run after each ipython command
|
||||
def post_cell_func(*args, **kwargs):
|
||||
if not self.is_window_closing():
|
||||
self.update_frame(dt=0, force_draw=True)
|
||||
|
||||
shell.events.register("post_run_cell", post_cell_func)
|
||||
|
||||
# Flash border, and potentially play sound, on exceptions
|
||||
def custom_exc(shell, etype, evalue, tb, tb_offset=None):
|
||||
# Show the error don't just swallow it
|
||||
shell.showtraceback((etype, evalue, tb), tb_offset=tb_offset)
|
||||
if self.embed_error_sound:
|
||||
os.system("printf '\a'")
|
||||
rect = FullScreenRectangle().set_stroke(RED, 30).set_fill(opacity=0)
|
||||
rect.fix_in_frame()
|
||||
self.play(VFadeInThenOut(rect, run_time=0.5))
|
||||
|
||||
shell.set_custom_exc((Exception,), custom_exc)
|
||||
|
||||
# Launch shell
|
||||
shell()
|
||||
interactive_scene_embed(self)
|
||||
|
||||
# End scene when exiting an embed
|
||||
if close_scene_on_exit:
|
||||
|
@ -616,6 +534,7 @@ class Scene(object):
|
|||
self.num_plays += 1
|
||||
|
||||
def begin_animations(self, animations: Iterable[Animation]) -> None:
|
||||
all_mobjects = set(self.get_mobject_family_members())
|
||||
for animation in animations:
|
||||
animation.begin()
|
||||
# Anything animated that's not already in the
|
||||
|
@ -623,8 +542,9 @@ class Scene(object):
|
|||
# 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.
|
||||
if animation.mobject not in self.get_mobject_family_members():
|
||||
if animation.mobject not in all_mobjects:
|
||||
self.add(animation.mobject)
|
||||
all_mobjects = all_mobjects.union(animation.mobject.get_family())
|
||||
|
||||
def progress_through_animations(self, animations: Iterable[Animation]) -> None:
|
||||
last_t = 0
|
||||
|
@ -736,8 +656,6 @@ class Scene(object):
|
|||
scene_state.restore_scene(self)
|
||||
|
||||
def save_state(self) -> None:
|
||||
if not self.preview:
|
||||
return
|
||||
state = self.get_state()
|
||||
if self.undo_stack and state.mobjects_match(self.undo_stack[-1]):
|
||||
return
|
||||
|
@ -770,37 +688,6 @@ class Scene(object):
|
|||
revert to the state of the scene the first time this function
|
||||
was called on a block of code starting with that comment.
|
||||
"""
|
||||
if self.shell is None or self.window is None:
|
||||
raise Exception(
|
||||
"Scene.checkpoint_paste cannot be called outside of " +
|
||||
"an ipython shell"
|
||||
)
|
||||
|
||||
pasted = pyperclip.paste()
|
||||
lines = pasted.split("\n")
|
||||
|
||||
# Commented lines trigger saved checkpoints
|
||||
if lines[0].lstrip().startswith("#"):
|
||||
if lines[0] not in self.checkpoint_states:
|
||||
self.checkpoint(lines[0])
|
||||
else:
|
||||
self.revert_to_checkpoint(lines[0])
|
||||
|
||||
# Copied methods of a scene are handled specially
|
||||
# A bit hacky, yes, but convenient
|
||||
method_pattern = r"^def\s+([a-zA-Z_]\w*)\s*\(self.*\):"
|
||||
method_names = re.findall(method_pattern ,lines[0].strip())
|
||||
if method_names:
|
||||
method_name = method_names[0]
|
||||
indent = " " * lines[0].index(lines[0].strip())
|
||||
pasted = "\n".join([
|
||||
# Remove self from function signature
|
||||
re.sub(r"self(,\s*)?", "", lines[0]),
|
||||
*lines[1:],
|
||||
# Attach to scene via self.func_name = func_name
|
||||
f"{indent}self.{method_name} = {method_name}"
|
||||
])
|
||||
|
||||
# Keep track of skipping and progress bar status
|
||||
self.skip_animations = skip
|
||||
|
||||
|
@ -811,7 +698,7 @@ class Scene(object):
|
|||
self.camera.use_window_fbo(False)
|
||||
self.file_writer.begin_insert()
|
||||
|
||||
self.shell.run_cell(pasted)
|
||||
self.checkpoint_manager.checkpoint_paste(self)
|
||||
|
||||
if record:
|
||||
self.file_writer.end_insert()
|
||||
|
@ -820,37 +707,8 @@ class Scene(object):
|
|||
self.stop_skipping()
|
||||
self.show_animation_progress = prev_progress
|
||||
|
||||
def checkpoint(self, key: str):
|
||||
self.checkpoint_states[key] = self.get_state()
|
||||
|
||||
def revert_to_checkpoint(self, key: str):
|
||||
if key not in self.checkpoint_states:
|
||||
log.error(f"No checkpoint at {key}")
|
||||
return
|
||||
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)
|
||||
|
||||
self.restore_state(self.checkpoint_states[key])
|
||||
|
||||
def clear_checkpoints(self):
|
||||
self.checkpoint_states = dict()
|
||||
|
||||
def save_mobject_to_file(self, mobject: Mobject, file_path: str | None = None) -> None:
|
||||
if file_path is None:
|
||||
file_path = self.file_writer.get_saved_mobject_path(mobject)
|
||||
if file_path is None:
|
||||
return
|
||||
mobject.save_to_file(file_path)
|
||||
|
||||
def load_mobject(self, file_name):
|
||||
if os.path.exists(file_name):
|
||||
path = file_name
|
||||
else:
|
||||
directory = self.file_writer.get_saved_mobject_directory()
|
||||
path = os.path.join(directory, file_name)
|
||||
return Mobject.load(path)
|
||||
self.checkpoint_manager.clear_checkpoints()
|
||||
|
||||
def is_window_closing(self):
|
||||
return self.window and (self.window.is_closing or self.quit_interaction)
|
||||
|
@ -1006,20 +864,23 @@ class Scene(object):
|
|||
|
||||
If `start_at_line` is provided, the scene will be reloaded at that line
|
||||
number. This corresponds to the `linemarker` param of the
|
||||
`config.get_module_with_inserted_embed_line()` method.
|
||||
`extract_scene.insert_embed_line_to_module()` method.
|
||||
|
||||
Before reload, the scene is cleared and the entire state is reset, such
|
||||
that we can start from a clean slate. This is taken care of by the
|
||||
ReloadManager, which will catch the error raised by the `exit_raise`
|
||||
magic command that we invoke here.
|
||||
|
||||
Note that we cannot define a custom exception class for this error,
|
||||
since the IPython kernel will swallow any exception. While we can catch
|
||||
such an exception in our custom exception handler registered with the
|
||||
`set_custom_exc` method, we cannot break out of the IPython shell by
|
||||
this means.
|
||||
"""
|
||||
reload_manager.set_new_start_at_line(start_at_line)
|
||||
self.shell.run_line_magic("exit_raise", "")
|
||||
self.reload_manager.set_new_start_at_line(start_at_line)
|
||||
shell = get_ipython()
|
||||
if shell:
|
||||
shell.run_line_magic("exit_raise", "")
|
||||
|
||||
def focus(self) -> None:
|
||||
"""
|
||||
|
|
151
manimlib/scene/scene_embed.py
Normal file
151
manimlib/scene/scene_embed.py
Normal file
|
@ -0,0 +1,151 @@
|
|||
import inspect
|
||||
import pyperclip
|
||||
import re
|
||||
|
||||
from IPython.terminal import pt_inputhooks
|
||||
from IPython.terminal.embed import InteractiveShellEmbed
|
||||
|
||||
from manimlib.animation.fading import VFadeInThenOut
|
||||
from manimlib.constants import RED
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.mobject.frame import FullScreenRectangle
|
||||
from manimlib.module_loader import ModuleLoader
|
||||
|
||||
|
||||
def interactive_scene_embed(scene):
|
||||
scene.stop_skipping()
|
||||
scene.update_frame(force_draw=True)
|
||||
|
||||
shell = get_ipython_shell_for_embedded_scene(scene)
|
||||
enable_gui(shell, scene)
|
||||
ensure_frame_update_post_cell(shell, scene)
|
||||
ensure_flash_on_error(shell, scene)
|
||||
|
||||
# Launch shell
|
||||
shell()
|
||||
|
||||
|
||||
def get_ipython_shell_for_embedded_scene(scene):
|
||||
"""
|
||||
Create embedded IPython terminal configured to have access to
|
||||
the local namespace of the caller
|
||||
"""
|
||||
# Triple back should take us to the context in a user's scene definition
|
||||
# which is calling "self.embed"
|
||||
caller_frame = inspect.currentframe().f_back.f_back.f_back
|
||||
|
||||
# Update the module's namespace to include local variables
|
||||
module = ModuleLoader.get_module(caller_frame.f_globals["__file__"])
|
||||
module.__dict__.update(caller_frame.f_locals)
|
||||
module.__dict__.update(get_shortcuts(scene))
|
||||
|
||||
return InteractiveShellEmbed(
|
||||
user_module=module,
|
||||
display_banner=False,
|
||||
xmode=scene.embed_exception_mode
|
||||
)
|
||||
|
||||
|
||||
def get_shortcuts(scene):
|
||||
"""
|
||||
A few custom shortcuts useful to have in the interactive shell namespace
|
||||
"""
|
||||
return dict(
|
||||
play=scene.play,
|
||||
wait=scene.wait,
|
||||
add=scene.add,
|
||||
remove=scene.remove,
|
||||
clear=scene.clear,
|
||||
focus=scene.focus,
|
||||
save_state=scene.save_state,
|
||||
reload=scene.reload,
|
||||
undo=scene.undo,
|
||||
redo=scene.redo,
|
||||
i2g=scene.i2g,
|
||||
i2m=scene.i2m,
|
||||
checkpoint_paste=scene.checkpoint_paste,
|
||||
)
|
||||
|
||||
|
||||
def enable_gui(shell, scene):
|
||||
"""Enables gui interactions during the embed"""
|
||||
def inputhook(context):
|
||||
while not context.input_is_ready():
|
||||
if not scene.is_window_closing():
|
||||
scene.update_frame(dt=0)
|
||||
if scene.is_window_closing():
|
||||
shell.ask_exit()
|
||||
|
||||
pt_inputhooks.register("manim", inputhook)
|
||||
shell.enable_gui("manim")
|
||||
|
||||
|
||||
def ensure_frame_update_post_cell(shell, scene):
|
||||
"""Ensure the scene updates its frame after each ipython cell"""
|
||||
def post_cell_func(*args, **kwargs):
|
||||
if not scene.is_window_closing():
|
||||
scene.update_frame(dt=0, force_draw=True)
|
||||
|
||||
shell.events.register("post_run_cell", post_cell_func)
|
||||
|
||||
|
||||
def ensure_flash_on_error(shell, scene):
|
||||
"""Flash border, and potentially play sound, on exceptions"""
|
||||
def custom_exc(shell, etype, evalue, tb, tb_offset=None):
|
||||
# Show the error don't just swallow it
|
||||
shell.showtraceback((etype, evalue, tb), tb_offset=tb_offset)
|
||||
if scene.embed_error_sound:
|
||||
os.system("printf '\a'")
|
||||
rect = FullScreenRectangle().set_stroke(RED, 30).set_fill(opacity=0)
|
||||
rect.fix_in_frame()
|
||||
scene.play(VFadeInThenOut(rect, run_time=0.5))
|
||||
|
||||
shell.set_custom_exc((Exception,), custom_exc)
|
||||
|
||||
|
||||
class CheckpointManager:
|
||||
checkpoint_states: dict[str, list[tuple[Mobject, Mobject]]] = dict()
|
||||
|
||||
def checkpoint_paste(self, scene):
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
shell = get_ipython()
|
||||
if shell is None:
|
||||
return
|
||||
|
||||
code_string = pyperclip.paste()
|
||||
|
||||
checkpoint_key = self.get_leading_comment(code_string)
|
||||
self.handle_checkpoint_key(scene, checkpoint_key)
|
||||
shell.run_cell(code_string)
|
||||
|
||||
@staticmethod
|
||||
def get_leading_comment(code_string: str):
|
||||
leading_line = code_string.partition("\n")[0].lstrip()
|
||||
if leading_line.startswith("#"):
|
||||
return leading_line
|
||||
return None
|
||||
|
||||
def handle_checkpoint_key(self, scene, key: str):
|
||||
if key is None:
|
||||
return
|
||||
elif key in self.checkpoint_states:
|
||||
# Revert to checkpoint
|
||||
scene.restore_state(self.checkpoint_states[key])
|
||||
|
||||
# Clear out any saved states that show up later
|
||||
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)
|
||||
else:
|
||||
self.checkpoint_states[key] = scene.get_state()
|
||||
|
||||
def clear_checkpoints(self):
|
||||
self.checkpoint_states = dict()
|
|
@ -146,39 +146,6 @@ class SceneFileWriter(object):
|
|||
def get_movie_file_path(self) -> str:
|
||||
return self.movie_file_path
|
||||
|
||||
def get_saved_mobject_directory(self) -> str:
|
||||
return guarantee_existence(self.saved_mobject_directory)
|
||||
|
||||
def get_saved_mobject_path(self, mobject: Mobject) -> str | None:
|
||||
directory = self.get_saved_mobject_directory()
|
||||
files = os.listdir(directory)
|
||||
default_name = str(mobject) + "_0.mob"
|
||||
index = 0
|
||||
while default_name in files:
|
||||
default_name = default_name.replace(str(index), str(index + 1))
|
||||
index += 1
|
||||
if platform.system() == 'Darwin':
|
||||
cmds = [
|
||||
"osascript", "-e",
|
||||
f"""
|
||||
set chosenfile to (choose file name default name "{default_name}" default location "{directory}")
|
||||
POSIX path of chosenfile
|
||||
""",
|
||||
]
|
||||
process = sp.Popen(cmds, stdout=sp.PIPE)
|
||||
file_path = process.stdout.read().decode("utf-8").split("\n")[0]
|
||||
if not file_path:
|
||||
return
|
||||
else:
|
||||
user_name = input(f"Enter mobject file name (default is {default_name}): ")
|
||||
file_path = os.path.join(directory, user_name or default_name)
|
||||
if os.path.exists(file_path) or os.path.exists(file_path + ".mob"):
|
||||
if input(f"{file_path} already exists. Overwrite (y/n)? ") != "y":
|
||||
return
|
||||
if not file_path.endswith(".mob"):
|
||||
file_path = file_path + ".mob"
|
||||
return file_path
|
||||
|
||||
# Sound
|
||||
def init_audio(self) -> None:
|
||||
self.includes_sound: bool = False
|
||||
|
|
|
@ -14,7 +14,7 @@ from manimlib.constants import FRAME_SHAPE
|
|||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Callable, TypeVar
|
||||
from typing import Callable, TypeVar, Optional
|
||||
from manimlib.scene.scene import Scene
|
||||
|
||||
T = TypeVar("T")
|
||||
|
@ -29,21 +29,22 @@ class Window(PygletWindow):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
scene: Scene,
|
||||
scene: Optional[Scene] = None,
|
||||
size: tuple[int, int] = (1280, 720),
|
||||
samples: int = 0
|
||||
):
|
||||
scene.window = self
|
||||
super().__init__(size=size, samples=samples)
|
||||
|
||||
self.scene = scene
|
||||
self.default_size = size
|
||||
self.default_position = self.find_initial_position(size)
|
||||
self.pressed_keys = set()
|
||||
self.size = size
|
||||
|
||||
self.update_scene(scene)
|
||||
if self.scene:
|
||||
self.init_for_scene(scene)
|
||||
|
||||
def update_scene(self, scene: Scene):
|
||||
def init_for_scene(self, scene: Scene):
|
||||
"""
|
||||
Resets the state and updates the scene associated to this window.
|
||||
|
||||
|
@ -114,7 +115,7 @@ class Window(PygletWindow):
|
|||
py: int,
|
||||
relative: bool = False
|
||||
) -> np.ndarray:
|
||||
if not hasattr(self.scene, "frame"):
|
||||
if self.scene is None or not hasattr(self.scene, "frame"):
|
||||
return np.zeros(3)
|
||||
|
||||
pixel_shape = np.array(self.size)
|
||||
|
@ -145,6 +146,8 @@ class Window(PygletWindow):
|
|||
@note_undrawn_event
|
||||
def on_mouse_motion(self, x: int, y: int, dx: int, dy: int) -> None:
|
||||
super().on_mouse_motion(x, y, dx, dy)
|
||||
if not self.scene:
|
||||
return
|
||||
point = self.pixel_coords_to_space_coords(x, y)
|
||||
d_point = self.pixel_coords_to_space_coords(dx, dy, relative=True)
|
||||
self.scene.on_mouse_motion(point, d_point)
|
||||
|
@ -152,6 +155,8 @@ class Window(PygletWindow):
|
|||
@note_undrawn_event
|
||||
def on_mouse_drag(self, x: int, y: int, dx: int, dy: int, buttons: int, modifiers: int) -> None:
|
||||
super().on_mouse_drag(x, y, dx, dy, buttons, modifiers)
|
||||
if not self.scene:
|
||||
return
|
||||
point = self.pixel_coords_to_space_coords(x, y)
|
||||
d_point = self.pixel_coords_to_space_coords(dx, dy, relative=True)
|
||||
self.scene.on_mouse_drag(point, d_point, buttons, modifiers)
|
||||
|
@ -159,18 +164,24 @@ class Window(PygletWindow):
|
|||
@note_undrawn_event
|
||||
def on_mouse_press(self, x: int, y: int, button: int, mods: int) -> None:
|
||||
super().on_mouse_press(x, y, button, mods)
|
||||
if not self.scene:
|
||||
return
|
||||
point = self.pixel_coords_to_space_coords(x, y)
|
||||
self.scene.on_mouse_press(point, button, mods)
|
||||
|
||||
@note_undrawn_event
|
||||
def on_mouse_release(self, x: int, y: int, button: int, mods: int) -> None:
|
||||
super().on_mouse_release(x, y, button, mods)
|
||||
if not self.scene:
|
||||
return
|
||||
point = self.pixel_coords_to_space_coords(x, y)
|
||||
self.scene.on_mouse_release(point, button, mods)
|
||||
|
||||
@note_undrawn_event
|
||||
def on_mouse_scroll(self, x: int, y: int, x_offset: float, y_offset: float) -> None:
|
||||
super().on_mouse_scroll(x, y, x_offset, y_offset)
|
||||
if not self.scene:
|
||||
return
|
||||
point = self.pixel_coords_to_space_coords(x, y)
|
||||
offset = self.pixel_coords_to_space_coords(x_offset, y_offset, relative=True)
|
||||
self.scene.on_mouse_scroll(point, offset, x_offset, y_offset)
|
||||
|
@ -179,32 +190,44 @@ class Window(PygletWindow):
|
|||
def on_key_press(self, symbol: int, modifiers: int) -> None:
|
||||
self.pressed_keys.add(symbol) # Modifiers?
|
||||
super().on_key_press(symbol, modifiers)
|
||||
if not self.scene:
|
||||
return
|
||||
self.scene.on_key_press(symbol, modifiers)
|
||||
|
||||
@note_undrawn_event
|
||||
def on_key_release(self, symbol: int, modifiers: int) -> None:
|
||||
self.pressed_keys.difference_update({symbol}) # Modifiers?
|
||||
super().on_key_release(symbol, modifiers)
|
||||
if not self.scene:
|
||||
return
|
||||
self.scene.on_key_release(symbol, modifiers)
|
||||
|
||||
@note_undrawn_event
|
||||
def on_resize(self, width: int, height: int) -> None:
|
||||
super().on_resize(width, height)
|
||||
if not self.scene:
|
||||
return
|
||||
self.scene.on_resize(width, height)
|
||||
|
||||
@note_undrawn_event
|
||||
def on_show(self) -> None:
|
||||
super().on_show()
|
||||
if not self.scene:
|
||||
return
|
||||
self.scene.on_show()
|
||||
|
||||
@note_undrawn_event
|
||||
def on_hide(self) -> None:
|
||||
super().on_hide()
|
||||
if not self.scene:
|
||||
return
|
||||
self.scene.on_hide()
|
||||
|
||||
@note_undrawn_event
|
||||
def on_close(self) -> None:
|
||||
super().on_close()
|
||||
if not self.scene:
|
||||
return
|
||||
self.scene.on_close()
|
||||
|
||||
def is_key_pressed(self, symbol: int) -> bool:
|
||||
|
|
Loading…
Add table
Reference in a new issue