diff --git a/manim/animation/transform.py b/manim/animation/transform.py index 667208d88a..cbf7d39640 100644 --- a/manim/animation/transform.py +++ b/manim/animation/transform.py @@ -33,6 +33,7 @@ import numpy as np +from manim.data_structures import MethodWithArgs from manim.mobject.opengl.opengl_mobject import OpenGLGroup, OpenGLMobject from .. import config @@ -438,13 +439,13 @@ def check_validity_of_input(self, mobject: Mobject) -> None: class _MethodAnimation(MoveToTarget): - def __init__(self, mobject, methods): + def __init__(self, mobject: Mobject, methods: list[MethodWithArgs]) -> None: self.methods = methods super().__init__(mobject) def finish(self) -> None: - for method, method_args, method_kwargs in self.methods: - method.__func__(self.mobject, *method_args, **method_kwargs) + for item in self.methods: + item.method.__func__(self.mobject, *item.args, **item.kwargs) super().finish() diff --git a/manim/data_structures.py b/manim/data_structures.py new file mode 100644 index 0000000000..f8ec7ffce4 --- /dev/null +++ b/manim/data_structures.py @@ -0,0 +1,71 @@ +from __future__ import annotations + +from collections.abc import Iterable +from dataclasses import dataclass +from types import MethodType +from typing import Any + + +@dataclass +class MethodWithArgs: + """Object containing a :attr:`method` which is intended to be called later + with the positional arguments :attr:`args` and the keyword arguments + :attr:`kwargs`. + + Attributes + ---------- + method : MethodType + A callable representing a method of some class. + args : Iterable[Any] + Positional arguments for :attr:`method`. + kwargs : dict[str, Any] + Keyword arguments for :attr:`method`. + """ + + __slots__ = ["method", "args", "kwargs"] + + method: MethodType + args: Iterable[Any] + kwargs: dict[str, Any] + + +@dataclass +class SceneInteractContinue: + """Object which, when encountered in :meth:`~.Scene.interact`, triggers + the end of the scene interaction, continuing with the rest of the + animations, if any. This object can be queued in :attr:`~.Scene.queue` + for later use in :meth:`~.Scene.interact`. + + Attributes + ---------- + sender : str + The name of the entity which issued the end of the scene interaction, + such as "gui" or "keyboard". + """ + + __slots__ = ["sender"] + + sender: str + + +class SceneInteractRerun: + """Object which, when encountered in :meth:`~.Scene.interact`, triggers + the rerun of the scene. This object can be queued in :attr:`~.Scene.queue` + for later use in :meth:`~.Scene.interact`. + + Attributes + ---------- + sender : str + The name of the entity which issued the rerun of the scene, such as + "gui", "keyboard", "play" or "file". + kwargs : dict[str, Any] + Additional keyword arguments when rerunning the scene. Currently, + only `"from_animation_number"` is being used, which determines the + animation from which to start rerunning the scene. + """ + + __slots__ = ["sender", "kwargs"] + + def __init__(self, sender: str, **kwargs: Any) -> None: + self.sender = sender + self.kwargs = kwargs diff --git a/manim/gui/gui.py b/manim/gui/gui.py index f173c1bbc1..35fed9cf4f 100644 --- a/manim/gui/gui.py +++ b/manim/gui/gui.py @@ -13,11 +13,13 @@ from collections.abc import Sequence from typing import TYPE_CHECKING, Any -from .. import __version__, config -from ..utils.module_ops import scene_classes_from_file +from manim import __version__ +from manim._config import config +from manim.data_structures import SceneInteractContinue, SceneInteractRerun +from manim.utils.module_ops import scene_classes_from_file if TYPE_CHECKING: - from ..renderer.opengl_renderer import OpenGLRenderer + from manim.renderer.opengl_renderer import OpenGLRenderer __all__ = ["configure_pygui"] @@ -44,14 +46,14 @@ def configure_pygui( dpg.set_viewport_height(540) def rerun_callback(sender, data): - renderer.scene.queue.put(("rerun_gui", [], {})) + renderer.scene.queue.put(SceneInteractRerun("gui")) def continue_callback(sender, data): - renderer.scene.queue.put(("exit_gui", [], {})) + renderer.scene.queue.put(SceneInteractContinue("gui")) def scene_selection_callback(sender, data): config["scene_names"] = (dpg.get_value(sender),) - renderer.scene.queue.put(("rerun_gui", [], {})) + renderer.scene.queue.put(SceneInteractRerun("gui")) scene_classes = scene_classes_from_file(Path(config["input_file"]), full_list=True) scene_names = [scene_class.__name__ for scene_class in scene_classes] diff --git a/manim/mobject/mobject.py b/manim/mobject/mobject.py index 15df06dee2..027ea215ba 100644 --- a/manim/mobject/mobject.py +++ b/manim/mobject/mobject.py @@ -21,6 +21,7 @@ import numpy as np +from manim.data_structures import MethodWithArgs from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL from .. import config, logger @@ -3232,7 +3233,7 @@ def __init__(self, mobject) -> None: self.overridden_animation = None self.is_chaining = False - self.methods = [] + self.methods: list[MethodWithArgs] = [] # Whether animation args can be passed self.cannot_pass_args = False @@ -3267,7 +3268,7 @@ def update_target(*method_args, **method_kwargs): **method_kwargs, ) else: - self.methods.append([method, method_args, method_kwargs]) + self.methods.append(MethodWithArgs(method, method_args, method_kwargs)) method(*method_args, **method_kwargs) return self diff --git a/manim/mobject/opengl/opengl_mobject.py b/manim/mobject/opengl/opengl_mobject.py index 6428995cd5..55995a84b8 100644 --- a/manim/mobject/opengl/opengl_mobject.py +++ b/manim/mobject/opengl/opengl_mobject.py @@ -16,6 +16,7 @@ from manim import config, logger from manim.constants import * +from manim.data_structures import MethodWithArgs from manim.renderer.shader_wrapper import get_colormap_code from manim.utils.bezier import integer_interpolate, interpolate from manim.utils.color import ( @@ -2938,7 +2939,7 @@ def __init__(self, mobject: OpenGLMobject): self.overridden_animation = None self.is_chaining = False - self.methods = [] + self.methods: list[MethodWithArgs] = [] # Whether animation args can be passed self.cannot_pass_args = False @@ -2973,7 +2974,7 @@ def update_target(*method_args, **method_kwargs): **method_kwargs, ) else: - self.methods.append([method, method_args, method_kwargs]) + self.methods.append(MethodWithArgs(method, method_args, method_kwargs)) method(*method_args, **method_kwargs) return self diff --git a/manim/scene/scene.py b/manim/scene/scene.py index 2c6ef3cdc9..47d2e73c7b 100644 --- a/manim/scene/scene.py +++ b/manim/scene/scene.py @@ -34,6 +34,11 @@ from watchdog.events import DirModifiedEvent, FileModifiedEvent, FileSystemEventHandler from watchdog.observers import Observer +from manim.data_structures import ( + MethodWithArgs, + SceneInteractContinue, + SceneInteractRerun, +) from manim.mobject.mobject import Mobject from manim.mobject.opengl.opengl_mobject import OpenGLPoint @@ -55,26 +60,26 @@ if TYPE_CHECKING: from collections.abc import Iterable, Sequence from types import FrameType - from typing import Any, Callable, TypeAlias + from typing import Any, Callable, TypeAlias, Union from typing_extensions import Self from manim.typing import Point3D - SceneInteractAction: TypeAlias = tuple[str, Iterable[Any], dict[str, Any]] - """ - The SceneInteractAction type alias is used for elements in the queue + SceneInteractAction: TypeAlias = Union[ + MethodWithArgs, SceneInteractContinue, SceneInteractRerun + ] + """The SceneInteractAction type alias is used for elements in the queue used by Scene.interact(). - The elements consist consist of: - - a string, which is either the name of a Scene method or some special keyword - starting with "rerun" or "exit", - - a list of args for the Scene method (only used if the first string actually - corresponds to a method) and - - a dict of kwargs for the Scene method (if the first string corresponds to one. - Otherwise, currently Scene.interact() extracts a possible "from_animation_number" from it if the first string starts with "rerun"), - as seen around the source code where it's common to use self.queue.put((method_name, [], {})) and similar items. + The elements can be one of the following three: + - a :class:`~.MethodWithArgs` object, which represents a :class:`Scene` + method to be called along with its args and kwargs, + - a :class:`~.SceneInteractContinue` object, indicating that the scene + interaction is over and the scene will continue rendering after that, or + - a :class:`~.SceneInteractRerun` object, indicating that the scene should + render again. """ @@ -86,7 +91,7 @@ def __init__(self, queue: Queue[SceneInteractAction]) -> None: self.queue = queue def on_modified(self, event: DirModifiedEvent | FileModifiedEvent) -> None: - self.queue.put(("rerun_file", [], {})) + self.queue.put(SceneInteractRerun("file")) class Scene: @@ -1102,6 +1107,7 @@ def play( and config.renderer == RendererType.OPENGL and threading.current_thread().name != "MainThread" ): + # TODO: are these actually being used? kwargs.update( { "subcaption": subcaption, @@ -1109,13 +1115,7 @@ def play( "subcaption_offset": subcaption_offset, } ) - self.queue.put( - ( - "play", - args, - kwargs, - ) - ) + self.queue.put(SceneInteractRerun("play", **kwargs)) return start_time = self.time @@ -1359,17 +1359,19 @@ def load_module_into_namespace( load_module_into_namespace(manim.opengl, namespace) def embedded_rerun(*args: Any, **kwargs: Any) -> None: - self.queue.put(("rerun_keyboard", args, kwargs)) + self.queue.put(SceneInteractRerun("keyboard")) shell.exiter() namespace["rerun"] = embedded_rerun shell(local_ns=namespace) - self.queue.put(("exit_keyboard", [], {})) + self.queue.put(SceneInteractContinue("keyboard")) def get_embedded_method(method_name: str) -> Callable[..., None]: + method = getattr(self, method_name) + def embedded_method(*args: Any, **kwargs: Any) -> None: - self.queue.put((method_name, args, kwargs)) + self.queue.put(MethodWithArgs(method, args, kwargs)) return embedded_method @@ -1434,34 +1436,33 @@ def interact(self, shell: Any, keyboard_thread: threading.Thread) -> None: last_time = time.time() while not (self.renderer.window.is_closing or self.quit_interaction): if not self.queue.empty(): - tup = self.queue.get_nowait() - if tup[0].startswith("rerun"): + action = self.queue.get_nowait() + if isinstance(action, SceneInteractRerun): # Intentionally skip calling join() on the file thread to save time. - if not tup[0].endswith("keyboard"): + if action.sender != "keyboard": if shell.pt_app: shell.pt_app.app.exit(exception=EOFError) file_observer.unschedule_all() raise RerunSceneException keyboard_thread.join() - kwargs = tup[2] - if "from_animation_number" in kwargs: - config["from_animation_number"] = kwargs[ + if "from_animation_number" in action.kwargs: + config["from_animation_number"] = action.kwargs[ "from_animation_number" ] # # TODO: This option only makes sense if interactive_embed() is run at the # # end of a scene by default. - # if "upto_animation_number" in kwargs: - # config["upto_animation_number"] = kwargs[ + # if "upto_animation_number" in action.kwargs: + # config["upto_animation_number"] = action.kwargs[ # "upto_animation_number" # ] keyboard_thread.join() file_observer.unschedule_all() raise RerunSceneException - elif tup[0].startswith("exit"): + elif isinstance(action, SceneInteractContinue): # Intentionally skip calling join() on the file thread to save time. - if not tup[0].endswith("keyboard") and shell.pt_app: + if action.sender != "keyboard" and shell.pt_app: shell.pt_app.app.exit(exception=EOFError) keyboard_thread.join() # Remove exit_keyboard from the queue if necessary. @@ -1470,8 +1471,7 @@ def interact(self, shell: Any, keyboard_thread: threading.Thread) -> None: keyboard_thread_needs_join = False break else: - method, args, kwargs = tup - getattr(self, method)(*args, **kwargs) + action.method(*action.args, **action.kwargs) else: self.renderer.animation_start_time = 0 dt = time.time() - last_time