1515import random
1616import threading
1717import time
18+ from dataclasses import dataclass
1819from pathlib import Path
1920from queue import Queue
2021
3839from watchdog .observers import Observer
3940
4041from manim import __version__
42+ from manim .data_structures import MethodWithArgs
4143from manim .mobject .mobject import Mobject
4244from manim .mobject .opengl .opengl_mobject import OpenGLPoint
4345
5961if TYPE_CHECKING :
6062 from collections .abc import Iterable , Sequence
6163 from types import FrameType
62- from typing import Any , Callable , TypeAlias
64+ from typing import Any , Callable , TypeAlias , Union
6365
6466 from typing_extensions import Self
6567
6668 from manim .typing import Point3D
6769
68- SceneInteractAction : TypeAlias = tuple [str , Iterable [Any ], dict [str , Any ]]
70+ SceneInteractAction : TypeAlias = Union [
71+ MethodWithArgs , "SceneInteractContinue" , "SceneInteractRerun"
72+ ]
73+ """The SceneInteractAction type alias is used for elements in the queue
74+ used by :meth:`.Scene.interact()`.
75+
76+ The elements can be one of the following three:
77+
78+ - a :class:`~.MethodWithArgs` object, which represents a :class:`Scene`
79+ method to be called along with its args and kwargs,
80+ - a :class:`~.SceneInteractContinue` object, indicating that the scene
81+ interaction is over and the scene will continue rendering after that, or
82+ - a :class:`~.SceneInteractRerun` object, indicating that the scene should
83+ render again.
6984 """
70- The SceneInteractAction type alias is used for elements in the queue
71- used by Scene.interact().
72- The elements consist consist of:
73-
74- - a string, which is either the name of a Scene method or some special keyword
75- starting with "rerun" or "exit",
76- - a list of args for the Scene method (only used if the first string actually
77- corresponds to a method) and
78- - a dict of kwargs for the Scene method (if the first string corresponds to one.
79- Otherwise, currently Scene.interact() extracts a possible "from_animation_number" from it if the first string starts with "rerun"),
80- as seen around the source code where it's common to use self.queue.put((method_name, [], {})) and similar items.
8185
86+
87+ @dataclass
88+ class SceneInteractContinue :
89+ """Object which, when encountered in :meth:`.Scene.interact`, triggers
90+ the end of the scene interaction, continuing with the rest of the
91+ animations, if any. This object can be queued in :attr:`.Scene.queue`
92+ for later use in :meth:`.Scene.interact`.
93+
94+ Attributes
95+ ----------
96+ sender : str
97+ The name of the entity which issued the end of the scene interaction,
98+ such as ``"gui"`` or ``"keyboard"``.
8299 """
83100
101+ __slots__ = ["sender" ]
102+
103+ sender : str
104+
105+
106+ class SceneInteractRerun :
107+ """Object which, when encountered in :meth:`.Scene.interact`, triggers
108+ the rerun of the scene. This object can be queued in :attr:`.Scene.queue`
109+ for later use in :meth:`.Scene.interact`.
110+
111+ Attributes
112+ ----------
113+ sender : str
114+ The name of the entity which issued the rerun of the scene, such as
115+ ``"gui"``, ``"keyboard"``, ``"play"`` or ``"file"``.
116+ kwargs : dict[str, Any]
117+ Additional keyword arguments when rerunning the scene. Currently,
118+ only ``"from_animation_number"`` is being used, which determines the
119+ animation from which to start rerunning the scene.
120+ """
121+
122+ __slots__ = ["sender" , "kwargs" ]
123+
124+ def __init__ (self , sender : str , ** kwargs : Any ) -> None :
125+ self .sender = sender
126+ self .kwargs = kwargs
127+
84128
85129class RerunSceneHandler (FileSystemEventHandler ):
86130 """A class to handle rerunning a Scene after the input file is modified."""
@@ -90,7 +134,7 @@ def __init__(self, queue: Queue[SceneInteractAction]) -> None:
90134 self .queue = queue
91135
92136 def on_modified (self , event : DirModifiedEvent | FileModifiedEvent ) -> None :
93- self .queue .put (( "rerun_file" , [], {} ))
137+ self .queue .put (SceneInteractRerun ( "file" ))
94138
95139
96140class Scene :
@@ -1106,20 +1150,15 @@ def play(
11061150 and config .renderer == RendererType .OPENGL
11071151 and threading .current_thread ().name != "MainThread"
11081152 ):
1153+ # TODO: are these actually being used?
11091154 kwargs .update (
11101155 {
11111156 "subcaption" : subcaption ,
11121157 "subcaption_duration" : subcaption_duration ,
11131158 "subcaption_offset" : subcaption_offset ,
11141159 }
11151160 )
1116- self .queue .put (
1117- (
1118- "play" ,
1119- args ,
1120- kwargs ,
1121- )
1122- )
1161+ self .queue .put (SceneInteractRerun ("play" , ** kwargs ))
11231162 return
11241163
11251164 start_time = self .time
@@ -1363,17 +1402,19 @@ def load_module_into_namespace(
13631402 load_module_into_namespace (manim .opengl , namespace )
13641403
13651404 def embedded_rerun (* args : Any , ** kwargs : Any ) -> None :
1366- self .queue .put (( "rerun_keyboard" , args , kwargs ))
1405+ self .queue .put (SceneInteractRerun ( "keyboard" ))
13671406 shell .exiter ()
13681407
13691408 namespace ["rerun" ] = embedded_rerun
13701409
13711410 shell (local_ns = namespace )
1372- self .queue .put (( "exit_keyboard" , [], {} ))
1411+ self .queue .put (SceneInteractContinue ( "keyboard" ))
13731412
13741413 def get_embedded_method (method_name : str ) -> Callable [..., None ]:
1414+ method = getattr (self , method_name )
1415+
13751416 def embedded_method (* args : Any , ** kwargs : Any ) -> None :
1376- self .queue .put (( method_name , args , kwargs ))
1417+ self .queue .put (MethodWithArgs ( method , args , kwargs ))
13771418
13781419 return embedded_method
13791420
@@ -1437,34 +1478,33 @@ def interact(self, shell: Any, keyboard_thread: threading.Thread) -> None:
14371478 last_time = time .time ()
14381479 while not (self .renderer .window .is_closing or self .quit_interaction ):
14391480 if not self .queue .empty ():
1440- tup = self .queue .get_nowait ()
1441- if tup [ 0 ]. startswith ( "rerun" ):
1481+ action = self .queue .get_nowait ()
1482+ if isinstance ( action , SceneInteractRerun ):
14421483 # Intentionally skip calling join() on the file thread to save time.
1443- if not tup [ 0 ]. endswith ( "keyboard" ) :
1484+ if action . sender != "keyboard" :
14441485 if shell .pt_app :
14451486 shell .pt_app .app .exit (exception = EOFError )
14461487 file_observer .unschedule_all ()
14471488 raise RerunSceneException
14481489 keyboard_thread .join ()
14491490
1450- kwargs = tup [2 ]
1451- if "from_animation_number" in kwargs :
1452- config ["from_animation_number" ] = kwargs [
1491+ if "from_animation_number" in action .kwargs :
1492+ config ["from_animation_number" ] = action .kwargs [
14531493 "from_animation_number"
14541494 ]
14551495 # # TODO: This option only makes sense if interactive_embed() is run at the
14561496 # # end of a scene by default.
1457- # if "upto_animation_number" in kwargs:
1458- # config["upto_animation_number"] = kwargs[
1497+ # if "upto_animation_number" in action. kwargs:
1498+ # config["upto_animation_number"] = action. kwargs[
14591499 # "upto_animation_number"
14601500 # ]
14611501
14621502 keyboard_thread .join ()
14631503 file_observer .unschedule_all ()
14641504 raise RerunSceneException
1465- elif tup [ 0 ]. startswith ( "exit" ):
1505+ elif isinstance ( action , SceneInteractContinue ):
14661506 # Intentionally skip calling join() on the file thread to save time.
1467- if not tup [ 0 ]. endswith ( "keyboard" ) and shell .pt_app :
1507+ if action . sender != "keyboard" and shell .pt_app :
14681508 shell .pt_app .app .exit (exception = EOFError )
14691509 keyboard_thread .join ()
14701510 # Remove exit_keyboard from the queue if necessary.
@@ -1473,8 +1513,7 @@ def interact(self, shell: Any, keyboard_thread: threading.Thread) -> None:
14731513 keyboard_thread_needs_join = False
14741514 break
14751515 else :
1476- method , args , kwargs = tup
1477- getattr (self , method )(* args , ** kwargs )
1516+ action .method (* action .args , ** action .kwargs )
14781517 else :
14791518 self .renderer .animation_start_time = 0
14801519 dt = time .time () - last_time
@@ -1561,14 +1600,14 @@ def _configure_pygui(self, update: bool = True) -> None:
15611600 dpg .set_viewport_height (540 )
15621601
15631602 def rerun_callback (sender : Any , data : Any ) -> None :
1564- self .queue .put (( "rerun_gui" , [], {} ))
1603+ self .queue .put (SceneInteractRerun ( "gui" ))
15651604
15661605 def continue_callback (sender : Any , data : Any ) -> None :
1567- self .queue .put (( "exit_gui" , [], {} ))
1606+ self .queue .put (SceneInteractContinue ( "gui" ))
15681607
15691608 def scene_selection_callback (sender : Any , data : Any ) -> None :
15701609 config ["scene_names" ] = (dpg .get_value (sender ),)
1571- self .queue .put (( "rerun_gui" , [], {} ))
1610+ self .queue .put (SceneInteractRerun ( "gui" ))
15721611
15731612 scene_classes = scene_classes_from_file (
15741613 Path (config ["input_file" ]), full_list = True
0 commit comments