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.config
|
||||||
import manimlib.logger
|
import manimlib.logger
|
||||||
import manimlib.utils.init_config
|
import manimlib.utils.init_config
|
||||||
from manimlib.reload_manager import reload_manager
|
from manimlib.reload_manager import ReloadManager
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
@ -22,7 +22,7 @@ def main():
|
||||||
manimlib.utils.init_config.init_customization()
|
manimlib.utils.init_config.init_customization()
|
||||||
return
|
return
|
||||||
|
|
||||||
reload_manager.args = args
|
reload_manager = ReloadManager(args)
|
||||||
reload_manager.run()
|
reload_manager.run()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
from argparse import Namespace
|
|
||||||
import colour
|
import colour
|
||||||
import importlib
|
import importlib
|
||||||
import inspect
|
import inspect
|
||||||
|
@ -13,21 +12,15 @@ import yaml
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
|
|
||||||
from manimlib.logger import log
|
from manimlib.logger import log
|
||||||
from manimlib.module_loader import ModuleLoader
|
|
||||||
from manimlib.utils.dict_ops import merge_dicts_recursively
|
from manimlib.utils.dict_ops import merge_dicts_recursively
|
||||||
from manimlib.utils.init_config import init_customization
|
from manimlib.utils.init_config import init_customization
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
Module = importlib.util.types.ModuleType
|
from argparse import Namespace
|
||||||
from typing import Optional
|
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():
|
def parse_cli():
|
||||||
try:
|
try:
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
|
@ -144,12 +137,8 @@ def parse_cli():
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-e", "--embed",
|
"-e", "--embed",
|
||||||
nargs="?",
|
|
||||||
const="",
|
|
||||||
help="Creates a new file where the line `self.embed` is inserted " + \
|
help="Creates a new file where the line `self.embed` is inserted " + \
|
||||||
"into the Scenes construct method. " + \
|
"at the corresponding line number"
|
||||||
"If a string is passed in, the line will be inserted below the " + \
|
|
||||||
"last line of code including that string."
|
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-r", "--resolution",
|
"-r", "--resolution",
|
||||||
|
@ -210,93 +199,6 @@ def get_manim_dir():
|
||||||
return os.path.abspath(os.path.join(manimlib_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):
|
def load_yaml(file_path: str):
|
||||||
try:
|
try:
|
||||||
with open(file_path, "r") as file:
|
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
|
return int(stan), None
|
||||||
|
|
||||||
|
|
||||||
def get_output_directory(args: Namespace, custom_config: dict) -> str:
|
def get_output_directory(args: Namespace, global_config: dict) -> str:
|
||||||
dir_config = custom_config["directories"]
|
dir_config = global_config["directories"]
|
||||||
output_directory = args.video_dir or dir_config["output"]
|
output_directory = args.video_dir or dir_config["output"]
|
||||||
if dir_config["mirror_module_path"] and args.file:
|
if dir_config["mirror_module_path"] and args.file:
|
||||||
to_cut = dir_config["removed_mirror_prefix"]
|
to_cut = dir_config["removed_mirror_prefix"]
|
||||||
|
@ -356,7 +258,7 @@ def get_output_directory(args: Namespace, custom_config: dict) -> str:
|
||||||
return output_directory
|
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 = {
|
result = {
|
||||||
"write_to_movie": not args.skip_animations and args.write_file,
|
"write_to_movie": not args.skip_animations and args.write_file,
|
||||||
"save_last_frame": 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
|
# If -t is passed in (for transparent), this will be RGBA
|
||||||
"png_mode": "RGBA" if args.transparent else "RGB",
|
"png_mode": "RGBA" if args.transparent else "RGB",
|
||||||
"movie_file_extension": get_file_ext(args),
|
"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,
|
"file_name": args.file_name,
|
||||||
"input_file_path": args.file or "",
|
"input_file_path": args.file or "",
|
||||||
"open_file_upon_completion": args.open,
|
"open_file_upon_completion": args.open,
|
||||||
"show_file_location_upon_completion": args.finder,
|
"show_file_location_upon_completion": args.finder,
|
||||||
"quiet": args.quiet,
|
"quiet": args.quiet,
|
||||||
**custom_config["file_writer_config"],
|
**global_config["file_writer_config"],
|
||||||
}
|
}
|
||||||
|
|
||||||
if args.vcodec:
|
if args.vcodec:
|
||||||
|
@ -387,32 +289,11 @@ def get_file_writer_config(args: Namespace, custom_config: dict) -> dict:
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def get_window_config(args: Namespace, custom_config: dict, camera_config: dict) -> dict:
|
def get_resolution(args: Optional[Namespace] = None, global_config: Optional[dict] = None):
|
||||||
# Default to making window half the screen size
|
args = args or parse_cli()
|
||||||
# but make it full screen if -f is passed in
|
global_config = global_config or get_global_config()
|
||||||
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))
|
|
||||||
|
|
||||||
|
camera_resolutions = global_config["camera_resolutions"]
|
||||||
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"]
|
|
||||||
if args.resolution:
|
if args.resolution:
|
||||||
resolution = args.resolution
|
resolution = args.resolution
|
||||||
elif args.low_quality:
|
elif args.low_quality:
|
||||||
|
@ -426,33 +307,53 @@ def get_camera_config(args: Optional[Namespace] = None, custom_config: Optional[
|
||||||
else:
|
else:
|
||||||
resolution = camera_resolutions[camera_resolutions["default_resolution"]]
|
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_str, height_str = resolution.split("x")
|
||||||
width = int(width_str)
|
return int(width_str), int(height_str)
|
||||||
height = 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_width": width,
|
||||||
"pixel_height": height,
|
"pixel_height": height,
|
||||||
"frame_config": {
|
|
||||||
"frame_shape": ((width / height) * FRAME_HEIGHT, FRAME_HEIGHT),
|
|
||||||
},
|
|
||||||
"fps": fps,
|
"fps": fps,
|
||||||
})
|
}
|
||||||
|
|
||||||
try:
|
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)
|
camera_config["background_color"] = colour.Color(bg_color)
|
||||||
except ValueError as err:
|
except ValueError as err:
|
||||||
log.error("Please use a valid color")
|
log.error("Please use a valid color")
|
||||||
log.error(err)
|
log.error(err)
|
||||||
sys.exit(2)
|
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
|
# scene has a background opacity of 0
|
||||||
if args.transparent:
|
if args.transparent:
|
||||||
camera_config["background_opacity"] = 0
|
camera_config["background_opacity"] = 0
|
||||||
|
@ -466,17 +367,15 @@ def get_scene_config(args: Namespace) -> dict:
|
||||||
"""
|
"""
|
||||||
global_config = get_global_config()
|
global_config = get_global_config()
|
||||||
camera_config = get_camera_config(args, 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)
|
start, end = get_animations_numbers(args)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"file_writer_config": get_file_writer_config(args, global_config),
|
"file_writer_config": file_writer_config,
|
||||||
"camera_config": camera_config,
|
"camera_config": camera_config,
|
||||||
"window_config": window_config,
|
|
||||||
"skip_animations": args.skip_animations,
|
"skip_animations": args.skip_animations,
|
||||||
"start_at_animation_number": start,
|
"start_at_animation_number": start,
|
||||||
"end_at_animation_number": end,
|
"end_at_animation_number": end,
|
||||||
"preview": not args.write_file,
|
|
||||||
"presenter_mode": args.presenter_mode,
|
"presenter_mode": args.presenter_mode,
|
||||||
"leave_progress_bars": args.leave_progress_bars,
|
"leave_progress_bars": args.leave_progress_bars,
|
||||||
"show_animation_progress": args.show_animation_progress,
|
"show_animation_progress": args.show_animation_progress,
|
||||||
|
@ -486,10 +385,15 @@ def get_scene_config(args: Namespace) -> dict:
|
||||||
|
|
||||||
|
|
||||||
def get_run_config(args: Namespace):
|
def get_run_config(args: Namespace):
|
||||||
|
window_config = get_window_config(args, get_global_config())
|
||||||
return {
|
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,
|
"prerun": args.prerun,
|
||||||
"scene_names": args.scene_names,
|
"scene_names": args.scene_names,
|
||||||
"quiet": args.quiet or args.write_all,
|
"quiet": args.quiet or args.write_all,
|
||||||
"write_all": 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
|
from __future__ import annotations
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from manimlib.config import get_camera_config
|
from manimlib.config import get_resolution
|
||||||
from manimlib.config import FRAME_HEIGHT
|
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
@ -13,18 +12,19 @@ if TYPE_CHECKING:
|
||||||
# TODO, it feels a bit unprincipled to have some global constants
|
# TODO, it feels a bit unprincipled to have some global constants
|
||||||
# depend on the output of this function, and for all that configuration
|
# depend on the output of this function, and for all that configuration
|
||||||
# code to be run merely upon importing from this file.
|
# 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
|
# 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_WIDTH: float = FRAME_HEIGHT * ASPECT_RATIO
|
||||||
FRAME_SHAPE: tuple[float, float] = (FRAME_WIDTH, FRAME_HEIGHT)
|
FRAME_SHAPE: tuple[float, float] = (FRAME_WIDTH, FRAME_HEIGHT)
|
||||||
FRAME_Y_RADIUS: float = FRAME_HEIGHT / 2
|
FRAME_Y_RADIUS: float = FRAME_HEIGHT / 2
|
||||||
FRAME_X_RADIUS: float = FRAME_WIDTH / 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
|
SMALL_BUFF: float = 0.1
|
||||||
MED_SMALL_BUFF: float = 0.25
|
MED_SMALL_BUFF: float = 0.25
|
||||||
|
|
|
@ -58,3 +58,4 @@ camera_resolutions:
|
||||||
fps: 30
|
fps: 30
|
||||||
embed_exception_mode: "Verbose"
|
embed_exception_mode: "Verbose"
|
||||||
embed_error_sound: False
|
embed_error_sound: False
|
||||||
|
ignore_manimlib_modules_on_reload: True
|
||||||
|
|
|
@ -1,12 +1,21 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import copy
|
import copy
|
||||||
import inspect
|
import inspect
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
from manimlib.module_loader import ModuleLoader
|
||||||
|
|
||||||
from manimlib.config import get_global_config
|
from manimlib.config import get_global_config
|
||||||
from manimlib.logger import log
|
from manimlib.logger import log
|
||||||
from manimlib.scene.interactive_scene import InteractiveScene
|
from manimlib.scene.interactive_scene import InteractiveScene
|
||||||
from manimlib.scene.scene import Scene
|
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):
|
class BlankScene(InteractiveScene):
|
||||||
def construct(self):
|
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):
|
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
|
# If no module was passed in, just play the blank scene
|
||||||
return [BlankScene(**scene_config)]
|
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)
|
return get_scenes_to_render(all_scene_classes, scene_config, run_config)
|
||||||
|
|
|
@ -11,4 +11,4 @@ logging.basicConfig(
|
||||||
)
|
)
|
||||||
|
|
||||||
log = logging.getLogger("manimgl")
|
log = logging.getLogger("manimgl")
|
||||||
log.setLevel("DEBUG")
|
log.setLevel("WARNING")
|
||||||
|
|
|
@ -715,21 +715,6 @@ class Mobject(object):
|
||||||
self.become(self.saved_state)
|
self.become(self.saved_state)
|
||||||
return self
|
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:
|
def become(self, mobject: Mobject, match_updaters=False) -> Self:
|
||||||
"""
|
"""
|
||||||
Edit all data and submobjects to be idential
|
Edit all data and submobjects to be idential
|
||||||
|
|
|
@ -6,12 +6,11 @@ import os
|
||||||
import sys
|
import sys
|
||||||
import sysconfig
|
import sysconfig
|
||||||
|
|
||||||
|
from manimlib.config import get_global_config
|
||||||
from manimlib.logger import log
|
from manimlib.logger import log
|
||||||
|
|
||||||
Module = importlib.util.types.ModuleType
|
Module = importlib.util.types.ModuleType
|
||||||
|
|
||||||
IGNORE_MANIMLIB_MODULES = True
|
|
||||||
|
|
||||||
|
|
||||||
class ModuleLoader:
|
class ModuleLoader:
|
||||||
"""
|
"""
|
||||||
|
@ -76,10 +75,7 @@ class ModuleLoader:
|
||||||
builtins.__import__ = tracked_import
|
builtins.__import__ = tracked_import
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Remove the "_insert_embed" suffix from the module name
|
|
||||||
module_name = module.__name__
|
module_name = module.__name__
|
||||||
if module.__name__.endswith("_insert_embed"):
|
|
||||||
module_name = module_name[:-13]
|
|
||||||
log.debug('Reloading module "%s"', module_name)
|
log.debug('Reloading module "%s"', module_name)
|
||||||
|
|
||||||
spec.loader.exec_module(module)
|
spec.loader.exec_module(module)
|
||||||
|
@ -146,7 +142,8 @@ class ModuleLoader:
|
||||||
|
|
||||||
Only user-defined modules are reloaded, see `is_user_defined_module()`.
|
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
|
return
|
||||||
|
|
||||||
if not hasattr(module, "__dict__"):
|
if not hasattr(module, "__dict__"):
|
||||||
|
|
|
@ -1,7 +1,20 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from IPython.terminal.embed import KillEmbedded
|
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:
|
class ReloadManager:
|
||||||
"""
|
"""
|
||||||
Manages the loading and running of scenes and is called directly from the
|
Manages the loading and running of scenes and is called directly from the
|
||||||
|
@ -12,20 +25,17 @@ class ReloadManager:
|
||||||
command in the IPython shell.
|
command in the IPython shell.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
args: Any = None
|
|
||||||
scenes: list[Any] = []
|
|
||||||
window = None
|
window = None
|
||||||
|
|
||||||
# The line number to load the scene from when reloading
|
|
||||||
start_at_line = None
|
|
||||||
|
|
||||||
is_reload = False
|
is_reload = False
|
||||||
|
|
||||||
|
def __init__(self, cli_args: Namespace):
|
||||||
|
self.args = cli_args
|
||||||
|
|
||||||
def set_new_start_at_line(self, start_at_line):
|
def set_new_start_at_line(self, start_at_line):
|
||||||
"""
|
"""
|
||||||
Sets/Updates the line number to load the scene from when reloading.
|
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):
|
def run(self):
|
||||||
"""
|
"""
|
||||||
|
@ -34,61 +44,43 @@ class ReloadManager:
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
# blocking call since a scene will init an IPython shell()
|
# 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
|
return
|
||||||
except KillEmbedded:
|
except KillEmbedded:
|
||||||
# Requested via the `exit_raise` IPython runline magic
|
# Requested via the `exit_raise` IPython runline magic
|
||||||
# by means of our scene.reload() command
|
# by means of our scene.reload() command
|
||||||
for scene in self.scenes:
|
self.note_reload()
|
||||||
scene.tear_down()
|
|
||||||
|
|
||||||
self.scenes = []
|
|
||||||
self.is_reload = True
|
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
break
|
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.
|
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
|
# Args to Config
|
||||||
self.args.is_reload = self.is_reload
|
|
||||||
scene_config = manimlib.config.get_scene_config(self.args)
|
scene_config = manimlib.config.get_scene_config(self.args)
|
||||||
if self.window:
|
scene_config.update(reload_manager=self)
|
||||||
scene_config["existing_window"] = self.window # see scene initialization
|
|
||||||
|
|
||||||
run_config = manimlib.config.get_run_config(self.args)
|
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
|
# Scenes
|
||||||
self.scenes = manimlib.extract_scene.main(scene_config, run_config)
|
scenes = manimlib.extract_scene.main(scene_config, run_config)
|
||||||
if len(self.scenes) == 0:
|
if len(scenes) == 0:
|
||||||
print("No scenes found to run")
|
print("No scenes found to run")
|
||||||
return
|
|
||||||
|
|
||||||
# Find first available window
|
for scene in scenes:
|
||||||
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"
|
|
||||||
]))
|
|
||||||
scene.run()
|
scene.run()
|
||||||
|
|
||||||
reload_manager = ReloadManager()
|
|
||||||
|
|
|
@ -460,12 +460,6 @@ class InteractiveScene(Scene):
|
||||||
nudge *= 10
|
nudge *= 10
|
||||||
self.selection.shift(nudge * vect)
|
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
|
# Key actions
|
||||||
def on_key_press(self, symbol: int, modifiers: int) -> None:
|
def on_key_press(self, symbol: int, modifiers: int) -> None:
|
||||||
super().on_key_press(symbol, modifiers)
|
super().on_key_press(symbol, modifiers)
|
||||||
|
@ -503,8 +497,6 @@ class InteractiveScene(Scene):
|
||||||
self.ungroup_selection()
|
self.ungroup_selection()
|
||||||
elif char == "t" and (modifiers & (PygletWindowKeys.MOD_COMMAND | PygletWindowKeys.MOD_CTRL)):
|
elif char == "t" and (modifiers & (PygletWindowKeys.MOD_COMMAND | PygletWindowKeys.MOD_CTRL)):
|
||||||
self.toggle_selection_mode()
|
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):
|
elif char == "d" and (modifiers & PygletWindowKeys.MOD_SHIFT):
|
||||||
self.copy_frame_positioning()
|
self.copy_frame_positioning()
|
||||||
elif char == "c" and (modifiers & PygletWindowKeys.MOD_SHIFT):
|
elif char == "c" and (modifiers & PygletWindowKeys.MOD_SHIFT):
|
||||||
|
|
|
@ -1,41 +1,33 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
import inspect
|
|
||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
import pyperclip
|
|
||||||
import random
|
import random
|
||||||
import time
|
import time
|
||||||
import re
|
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
|
||||||
from IPython.terminal import pt_inputhooks
|
|
||||||
from IPython.terminal.embed import InteractiveShellEmbed
|
|
||||||
from pyglet.window import key as PygletWindowKeys
|
from pyglet.window import key as PygletWindowKeys
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from tqdm.auto import tqdm as ProgressDisplay
|
from tqdm.auto import tqdm as ProgressDisplay
|
||||||
|
|
||||||
from manimlib.animation.animation import prepare_animation
|
from manimlib.animation.animation import prepare_animation
|
||||||
from manimlib.animation.fading import VFadeInThenOut
|
|
||||||
from manimlib.camera.camera import Camera
|
from manimlib.camera.camera import Camera
|
||||||
from manimlib.camera.camera_frame import CameraFrame
|
from manimlib.camera.camera_frame import CameraFrame
|
||||||
from manimlib.module_loader import ModuleLoader
|
|
||||||
from manimlib.constants import ARROW_SYMBOLS
|
from manimlib.constants import ARROW_SYMBOLS
|
||||||
from manimlib.constants import DEFAULT_WAIT_TIME
|
from manimlib.constants import DEFAULT_WAIT_TIME
|
||||||
from manimlib.constants import RED
|
|
||||||
from manimlib.event_handler import EVENT_DISPATCHER
|
from manimlib.event_handler import EVENT_DISPATCHER
|
||||||
from manimlib.event_handler.event_type import EventType
|
from manimlib.event_handler.event_type import EventType
|
||||||
from manimlib.logger import log
|
from manimlib.logger import log
|
||||||
from manimlib.reload_manager import reload_manager
|
|
||||||
from manimlib.mobject.frame import FullScreenRectangle
|
|
||||||
from manimlib.mobject.mobject import _AnimationBuilder
|
from manimlib.mobject.mobject import _AnimationBuilder
|
||||||
from manimlib.mobject.mobject import Group
|
from manimlib.mobject.mobject import Group
|
||||||
from manimlib.mobject.mobject import Mobject
|
from manimlib.mobject.mobject import Mobject
|
||||||
from manimlib.mobject.mobject import Point
|
from manimlib.mobject.mobject import Point
|
||||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
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.scene.scene_file_writer import SceneFileWriter
|
||||||
from manimlib.utils.family_ops import extract_mobject_family_members
|
from manimlib.utils.family_ops import extract_mobject_family_members
|
||||||
from manimlib.utils.family_ops import recursive_mobject_remove
|
from manimlib.utils.family_ops import recursive_mobject_remove
|
||||||
|
@ -45,13 +37,14 @@ from manimlib.window import Window
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from typing import Callable, Iterable, TypeVar
|
from typing import Callable, Iterable, TypeVar, Optional
|
||||||
from manimlib.typing import Vect3
|
from manimlib.typing import Vect3
|
||||||
|
|
||||||
T = TypeVar('T')
|
T = TypeVar('T')
|
||||||
|
|
||||||
from PIL.Image import Image
|
from PIL.Image import Image
|
||||||
|
|
||||||
|
from manimlib.reload_manager import ReloadManager
|
||||||
from manimlib.animation.animation import Animation
|
from manimlib.animation.animation import Animation
|
||||||
|
|
||||||
|
|
||||||
|
@ -68,7 +61,6 @@ class Scene(object):
|
||||||
drag_to_pan: bool = True
|
drag_to_pan: bool = True
|
||||||
max_num_saved_states: int = 50
|
max_num_saved_states: int = 50
|
||||||
default_camera_config: dict = dict()
|
default_camera_config: dict = dict()
|
||||||
default_window_config: dict = dict()
|
|
||||||
default_file_writer_config: dict = dict()
|
default_file_writer_config: dict = dict()
|
||||||
samples = 0
|
samples = 0
|
||||||
# Euler angles, in degrees
|
# Euler angles, in degrees
|
||||||
|
@ -76,7 +68,6 @@ class Scene(object):
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
window_config: dict = dict(),
|
|
||||||
camera_config: dict = dict(),
|
camera_config: dict = dict(),
|
||||||
file_writer_config: dict = dict(),
|
file_writer_config: dict = dict(),
|
||||||
skip_animations: bool = False,
|
skip_animations: bool = False,
|
||||||
|
@ -84,45 +75,39 @@ class Scene(object):
|
||||||
start_at_animation_number: int | None = None,
|
start_at_animation_number: int | None = None,
|
||||||
end_at_animation_number: int | None = None,
|
end_at_animation_number: int | None = None,
|
||||||
leave_progress_bars: bool = False,
|
leave_progress_bars: bool = False,
|
||||||
preview: bool = True,
|
window: Optional[Window] = None,
|
||||||
|
reload_manager: Optional[ReloadManager] = None,
|
||||||
presenter_mode: bool = False,
|
presenter_mode: bool = False,
|
||||||
show_animation_progress: bool = False,
|
show_animation_progress: bool = False,
|
||||||
embed_exception_mode: str = "",
|
embed_exception_mode: str = "",
|
||||||
embed_error_sound: bool = False,
|
embed_error_sound: bool = False,
|
||||||
existing_window: Window | None = None,
|
|
||||||
):
|
):
|
||||||
self.skip_animations = skip_animations
|
self.skip_animations = skip_animations
|
||||||
self.always_update_mobjects = always_update_mobjects
|
self.always_update_mobjects = always_update_mobjects
|
||||||
self.start_at_animation_number = start_at_animation_number
|
self.start_at_animation_number = start_at_animation_number
|
||||||
self.end_at_animation_number = end_at_animation_number
|
self.end_at_animation_number = end_at_animation_number
|
||||||
self.leave_progress_bars = leave_progress_bars
|
self.leave_progress_bars = leave_progress_bars
|
||||||
self.preview = preview
|
|
||||||
self.presenter_mode = presenter_mode
|
self.presenter_mode = presenter_mode
|
||||||
self.show_animation_progress = show_animation_progress
|
self.show_animation_progress = show_animation_progress
|
||||||
self.embed_exception_mode = embed_exception_mode
|
self.embed_exception_mode = embed_exception_mode
|
||||||
self.embed_error_sound = embed_error_sound
|
self.embed_error_sound = embed_error_sound
|
||||||
|
self.reload_manager = reload_manager
|
||||||
|
|
||||||
self.camera_config = {**self.default_camera_config, **camera_config}
|
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}
|
self.file_writer_config = {**self.default_file_writer_config, **file_writer_config}
|
||||||
|
|
||||||
# Initialize window, if applicable (and reuse window if provided during
|
self.window = window
|
||||||
# reload by means of the ReloadManager)
|
if self.window:
|
||||||
if self.preview:
|
self.window.init_for_scene(self)
|
||||||
if existing_window:
|
# Make sure camera and Pyglet window sync
|
||||||
self.window = existing_window
|
self.camera_config["fps"] = 30
|
||||||
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
|
|
||||||
|
|
||||||
# Core state of the scene
|
# 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: CameraFrame = self.camera.frame
|
||||||
self.frame.reorient(*self.default_frame_orientation)
|
self.frame.reorient(*self.default_frame_orientation)
|
||||||
self.frame.make_orientation_default()
|
self.frame.make_orientation_default()
|
||||||
|
@ -136,6 +121,7 @@ class Scene(object):
|
||||||
self.skip_time: float = 0
|
self.skip_time: float = 0
|
||||||
self.original_skipping_status: bool = self.skip_animations
|
self.original_skipping_status: bool = self.skip_animations
|
||||||
self.checkpoint_states: dict[str, list[tuple[Mobject, Mobject]]] = dict()
|
self.checkpoint_states: dict[str, list[tuple[Mobject, Mobject]]] = dict()
|
||||||
|
self.checkpoint_manager: CheckpointManager = CheckpointManager()
|
||||||
self.undo_stack = []
|
self.undo_stack = []
|
||||||
self.redo_stack = []
|
self.redo_stack = []
|
||||||
|
|
||||||
|
@ -220,80 +206,12 @@ class Scene(object):
|
||||||
close_scene_on_exit: bool = True,
|
close_scene_on_exit: bool = True,
|
||||||
show_animation_progress: bool = False,
|
show_animation_progress: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
if not self.preview:
|
if not self.window:
|
||||||
# Embed is only relevant with a preview
|
# Embed is only relevant for interactive development with a Window
|
||||||
return
|
return
|
||||||
self.stop_skipping()
|
|
||||||
self.update_frame(force_draw=True)
|
|
||||||
self.save_state()
|
|
||||||
self.show_animation_progress = show_animation_progress
|
self.show_animation_progress = show_animation_progress
|
||||||
|
|
||||||
# Create embedded IPython terminal configured to have access to
|
interactive_scene_embed(self)
|
||||||
# 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()
|
|
||||||
|
|
||||||
# End scene when exiting an embed
|
# End scene when exiting an embed
|
||||||
if close_scene_on_exit:
|
if close_scene_on_exit:
|
||||||
|
@ -616,6 +534,7 @@ class Scene(object):
|
||||||
self.num_plays += 1
|
self.num_plays += 1
|
||||||
|
|
||||||
def begin_animations(self, animations: Iterable[Animation]) -> None:
|
def begin_animations(self, animations: Iterable[Animation]) -> None:
|
||||||
|
all_mobjects = set(self.get_mobject_family_members())
|
||||||
for animation in animations:
|
for animation in animations:
|
||||||
animation.begin()
|
animation.begin()
|
||||||
# Anything animated that's not already in the
|
# Anything animated that's not already in the
|
||||||
|
@ -623,8 +542,9 @@ class Scene(object):
|
||||||
# animated mobjects that are in the family of
|
# animated mobjects that are in the family of
|
||||||
# those on screen, this can result in a restructuring
|
# those on screen, this can result in a restructuring
|
||||||
# of the scene.mobjects list, which is usually desired.
|
# 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)
|
self.add(animation.mobject)
|
||||||
|
all_mobjects = all_mobjects.union(animation.mobject.get_family())
|
||||||
|
|
||||||
def progress_through_animations(self, animations: Iterable[Animation]) -> None:
|
def progress_through_animations(self, animations: Iterable[Animation]) -> None:
|
||||||
last_t = 0
|
last_t = 0
|
||||||
|
@ -736,8 +656,6 @@ class Scene(object):
|
||||||
scene_state.restore_scene(self)
|
scene_state.restore_scene(self)
|
||||||
|
|
||||||
def save_state(self) -> None:
|
def save_state(self) -> None:
|
||||||
if not self.preview:
|
|
||||||
return
|
|
||||||
state = self.get_state()
|
state = self.get_state()
|
||||||
if self.undo_stack and state.mobjects_match(self.undo_stack[-1]):
|
if self.undo_stack and state.mobjects_match(self.undo_stack[-1]):
|
||||||
return
|
return
|
||||||
|
@ -770,37 +688,6 @@ class Scene(object):
|
||||||
revert to the state of the scene the first time this function
|
revert to the state of the scene the first time this function
|
||||||
was called on a block of code starting with that comment.
|
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
|
# Keep track of skipping and progress bar status
|
||||||
self.skip_animations = skip
|
self.skip_animations = skip
|
||||||
|
|
||||||
|
@ -811,7 +698,7 @@ class Scene(object):
|
||||||
self.camera.use_window_fbo(False)
|
self.camera.use_window_fbo(False)
|
||||||
self.file_writer.begin_insert()
|
self.file_writer.begin_insert()
|
||||||
|
|
||||||
self.shell.run_cell(pasted)
|
self.checkpoint_manager.checkpoint_paste(self)
|
||||||
|
|
||||||
if record:
|
if record:
|
||||||
self.file_writer.end_insert()
|
self.file_writer.end_insert()
|
||||||
|
@ -820,37 +707,8 @@ class Scene(object):
|
||||||
self.stop_skipping()
|
self.stop_skipping()
|
||||||
self.show_animation_progress = prev_progress
|
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):
|
def clear_checkpoints(self):
|
||||||
self.checkpoint_states = dict()
|
self.checkpoint_manager.clear_checkpoints()
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
def is_window_closing(self):
|
def is_window_closing(self):
|
||||||
return self.window and (self.window.is_closing or self.quit_interaction)
|
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
|
If `start_at_line` is provided, the scene will be reloaded at that line
|
||||||
number. This corresponds to the `linemarker` param of the
|
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
|
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
|
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`
|
ReloadManager, which will catch the error raised by the `exit_raise`
|
||||||
magic command that we invoke here.
|
magic command that we invoke here.
|
||||||
|
|
||||||
Note that we cannot define a custom exception class for this error,
|
Note that we cannot define a custom exception class for this error,
|
||||||
since the IPython kernel will swallow any exception. While we can catch
|
since the IPython kernel will swallow any exception. While we can catch
|
||||||
such an exception in our custom exception handler registered with the
|
such an exception in our custom exception handler registered with the
|
||||||
`set_custom_exc` method, we cannot break out of the IPython shell by
|
`set_custom_exc` method, we cannot break out of the IPython shell by
|
||||||
this means.
|
this means.
|
||||||
"""
|
"""
|
||||||
reload_manager.set_new_start_at_line(start_at_line)
|
self.reload_manager.set_new_start_at_line(start_at_line)
|
||||||
self.shell.run_line_magic("exit_raise", "")
|
shell = get_ipython()
|
||||||
|
if shell:
|
||||||
|
shell.run_line_magic("exit_raise", "")
|
||||||
|
|
||||||
def focus(self) -> None:
|
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:
|
def get_movie_file_path(self) -> str:
|
||||||
return self.movie_file_path
|
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
|
# Sound
|
||||||
def init_audio(self) -> None:
|
def init_audio(self) -> None:
|
||||||
self.includes_sound: bool = False
|
self.includes_sound: bool = False
|
||||||
|
|
|
@ -14,7 +14,7 @@ from manimlib.constants import FRAME_SHAPE
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from typing import Callable, TypeVar
|
from typing import Callable, TypeVar, Optional
|
||||||
from manimlib.scene.scene import Scene
|
from manimlib.scene.scene import Scene
|
||||||
|
|
||||||
T = TypeVar("T")
|
T = TypeVar("T")
|
||||||
|
@ -29,21 +29,22 @@ class Window(PygletWindow):
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
scene: Scene,
|
scene: Optional[Scene] = None,
|
||||||
size: tuple[int, int] = (1280, 720),
|
size: tuple[int, int] = (1280, 720),
|
||||||
samples: int = 0
|
samples: int = 0
|
||||||
):
|
):
|
||||||
scene.window = self
|
|
||||||
super().__init__(size=size, samples=samples)
|
super().__init__(size=size, samples=samples)
|
||||||
|
|
||||||
|
self.scene = scene
|
||||||
self.default_size = size
|
self.default_size = size
|
||||||
self.default_position = self.find_initial_position(size)
|
self.default_position = self.find_initial_position(size)
|
||||||
self.pressed_keys = set()
|
self.pressed_keys = set()
|
||||||
self.size = size
|
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.
|
Resets the state and updates the scene associated to this window.
|
||||||
|
|
||||||
|
@ -114,7 +115,7 @@ class Window(PygletWindow):
|
||||||
py: int,
|
py: int,
|
||||||
relative: bool = False
|
relative: bool = False
|
||||||
) -> np.ndarray:
|
) -> np.ndarray:
|
||||||
if not hasattr(self.scene, "frame"):
|
if self.scene is None or not hasattr(self.scene, "frame"):
|
||||||
return np.zeros(3)
|
return np.zeros(3)
|
||||||
|
|
||||||
pixel_shape = np.array(self.size)
|
pixel_shape = np.array(self.size)
|
||||||
|
@ -145,6 +146,8 @@ class Window(PygletWindow):
|
||||||
@note_undrawn_event
|
@note_undrawn_event
|
||||||
def on_mouse_motion(self, x: int, y: int, dx: int, dy: int) -> None:
|
def on_mouse_motion(self, x: int, y: int, dx: int, dy: int) -> None:
|
||||||
super().on_mouse_motion(x, y, dx, dy)
|
super().on_mouse_motion(x, y, dx, dy)
|
||||||
|
if not self.scene:
|
||||||
|
return
|
||||||
point = self.pixel_coords_to_space_coords(x, y)
|
point = self.pixel_coords_to_space_coords(x, y)
|
||||||
d_point = self.pixel_coords_to_space_coords(dx, dy, relative=True)
|
d_point = self.pixel_coords_to_space_coords(dx, dy, relative=True)
|
||||||
self.scene.on_mouse_motion(point, d_point)
|
self.scene.on_mouse_motion(point, d_point)
|
||||||
|
@ -152,6 +155,8 @@ class Window(PygletWindow):
|
||||||
@note_undrawn_event
|
@note_undrawn_event
|
||||||
def on_mouse_drag(self, x: int, y: int, dx: int, dy: int, buttons: int, modifiers: int) -> None:
|
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)
|
super().on_mouse_drag(x, y, dx, dy, buttons, modifiers)
|
||||||
|
if not self.scene:
|
||||||
|
return
|
||||||
point = self.pixel_coords_to_space_coords(x, y)
|
point = self.pixel_coords_to_space_coords(x, y)
|
||||||
d_point = self.pixel_coords_to_space_coords(dx, dy, relative=True)
|
d_point = self.pixel_coords_to_space_coords(dx, dy, relative=True)
|
||||||
self.scene.on_mouse_drag(point, d_point, buttons, modifiers)
|
self.scene.on_mouse_drag(point, d_point, buttons, modifiers)
|
||||||
|
@ -159,18 +164,24 @@ class Window(PygletWindow):
|
||||||
@note_undrawn_event
|
@note_undrawn_event
|
||||||
def on_mouse_press(self, x: int, y: int, button: int, mods: int) -> None:
|
def on_mouse_press(self, x: int, y: int, button: int, mods: int) -> None:
|
||||||
super().on_mouse_press(x, y, button, mods)
|
super().on_mouse_press(x, y, button, mods)
|
||||||
|
if not self.scene:
|
||||||
|
return
|
||||||
point = self.pixel_coords_to_space_coords(x, y)
|
point = self.pixel_coords_to_space_coords(x, y)
|
||||||
self.scene.on_mouse_press(point, button, mods)
|
self.scene.on_mouse_press(point, button, mods)
|
||||||
|
|
||||||
@note_undrawn_event
|
@note_undrawn_event
|
||||||
def on_mouse_release(self, x: int, y: int, button: int, mods: int) -> None:
|
def on_mouse_release(self, x: int, y: int, button: int, mods: int) -> None:
|
||||||
super().on_mouse_release(x, y, button, mods)
|
super().on_mouse_release(x, y, button, mods)
|
||||||
|
if not self.scene:
|
||||||
|
return
|
||||||
point = self.pixel_coords_to_space_coords(x, y)
|
point = self.pixel_coords_to_space_coords(x, y)
|
||||||
self.scene.on_mouse_release(point, button, mods)
|
self.scene.on_mouse_release(point, button, mods)
|
||||||
|
|
||||||
@note_undrawn_event
|
@note_undrawn_event
|
||||||
def on_mouse_scroll(self, x: int, y: int, x_offset: float, y_offset: float) -> None:
|
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)
|
super().on_mouse_scroll(x, y, x_offset, y_offset)
|
||||||
|
if not self.scene:
|
||||||
|
return
|
||||||
point = self.pixel_coords_to_space_coords(x, y)
|
point = self.pixel_coords_to_space_coords(x, y)
|
||||||
offset = self.pixel_coords_to_space_coords(x_offset, y_offset, relative=True)
|
offset = self.pixel_coords_to_space_coords(x_offset, y_offset, relative=True)
|
||||||
self.scene.on_mouse_scroll(point, offset, x_offset, y_offset)
|
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:
|
def on_key_press(self, symbol: int, modifiers: int) -> None:
|
||||||
self.pressed_keys.add(symbol) # Modifiers?
|
self.pressed_keys.add(symbol) # Modifiers?
|
||||||
super().on_key_press(symbol, modifiers)
|
super().on_key_press(symbol, modifiers)
|
||||||
|
if not self.scene:
|
||||||
|
return
|
||||||
self.scene.on_key_press(symbol, modifiers)
|
self.scene.on_key_press(symbol, modifiers)
|
||||||
|
|
||||||
@note_undrawn_event
|
@note_undrawn_event
|
||||||
def on_key_release(self, symbol: int, modifiers: int) -> None:
|
def on_key_release(self, symbol: int, modifiers: int) -> None:
|
||||||
self.pressed_keys.difference_update({symbol}) # Modifiers?
|
self.pressed_keys.difference_update({symbol}) # Modifiers?
|
||||||
super().on_key_release(symbol, modifiers)
|
super().on_key_release(symbol, modifiers)
|
||||||
|
if not self.scene:
|
||||||
|
return
|
||||||
self.scene.on_key_release(symbol, modifiers)
|
self.scene.on_key_release(symbol, modifiers)
|
||||||
|
|
||||||
@note_undrawn_event
|
@note_undrawn_event
|
||||||
def on_resize(self, width: int, height: int) -> None:
|
def on_resize(self, width: int, height: int) -> None:
|
||||||
super().on_resize(width, height)
|
super().on_resize(width, height)
|
||||||
|
if not self.scene:
|
||||||
|
return
|
||||||
self.scene.on_resize(width, height)
|
self.scene.on_resize(width, height)
|
||||||
|
|
||||||
@note_undrawn_event
|
@note_undrawn_event
|
||||||
def on_show(self) -> None:
|
def on_show(self) -> None:
|
||||||
super().on_show()
|
super().on_show()
|
||||||
|
if not self.scene:
|
||||||
|
return
|
||||||
self.scene.on_show()
|
self.scene.on_show()
|
||||||
|
|
||||||
@note_undrawn_event
|
@note_undrawn_event
|
||||||
def on_hide(self) -> None:
|
def on_hide(self) -> None:
|
||||||
super().on_hide()
|
super().on_hide()
|
||||||
|
if not self.scene:
|
||||||
|
return
|
||||||
self.scene.on_hide()
|
self.scene.on_hide()
|
||||||
|
|
||||||
@note_undrawn_event
|
@note_undrawn_event
|
||||||
def on_close(self) -> None:
|
def on_close(self) -> None:
|
||||||
super().on_close()
|
super().on_close()
|
||||||
|
if not self.scene:
|
||||||
|
return
|
||||||
self.scene.on_close()
|
self.scene.on_close()
|
||||||
|
|
||||||
def is_key_pressed(self, symbol: int) -> bool:
|
def is_key_pressed(self, symbol: int) -> bool:
|
||||||
|
|
Loading…
Add table
Reference in a new issue