manim 0.17.0__py3-none-any.whl → 0.19.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. manim/__init__.py +11 -6
  2. manim/__main__.py +62 -19
  3. manim/_config/__init__.py +10 -9
  4. manim/_config/cli_colors.py +26 -9
  5. manim/_config/default.cfg +1 -3
  6. manim/_config/logger_utils.py +23 -13
  7. manim/_config/utils.py +662 -468
  8. manim/animation/animation.py +164 -18
  9. manim/animation/changing.py +34 -23
  10. manim/animation/composition.py +265 -67
  11. manim/animation/creation.py +208 -26
  12. manim/animation/fading.py +16 -18
  13. manim/animation/growing.py +35 -15
  14. manim/animation/indication.py +150 -76
  15. manim/animation/movement.py +56 -22
  16. manim/animation/numbers.py +64 -6
  17. manim/animation/rotation.py +78 -7
  18. manim/animation/specialized.py +6 -7
  19. manim/animation/speedmodifier.py +13 -10
  20. manim/animation/transform.py +14 -11
  21. manim/animation/transform_matching_parts.py +3 -4
  22. manim/animation/updaters/mobject_update_utils.py +152 -30
  23. manim/animation/updaters/update.py +10 -7
  24. manim/camera/camera.py +182 -118
  25. manim/camera/mapping_camera.py +34 -3
  26. manim/camera/moving_camera.py +95 -74
  27. manim/camera/multi_camera.py +23 -15
  28. manim/camera/three_d_camera.py +70 -52
  29. manim/cli/__init__.py +17 -0
  30. manim/cli/cfg/group.py +76 -44
  31. manim/cli/checkhealth/checks.py +192 -0
  32. manim/cli/checkhealth/commands.py +90 -0
  33. manim/cli/default_group.py +158 -25
  34. manim/cli/init/commands.py +33 -25
  35. manim/cli/plugins/commands.py +16 -3
  36. manim/cli/render/commands.py +72 -60
  37. manim/cli/render/ease_of_access_options.py +4 -3
  38. manim/cli/render/global_options.py +59 -17
  39. manim/cli/render/output_options.py +6 -5
  40. manim/cli/render/render_options.py +98 -33
  41. manim/constants.py +109 -59
  42. manim/data_structures.py +31 -0
  43. manim/mobject/frame.py +8 -5
  44. manim/mobject/geometry/__init__.py +1 -0
  45. manim/mobject/geometry/arc.py +277 -135
  46. manim/mobject/geometry/boolean_ops.py +32 -31
  47. manim/mobject/geometry/labeled.py +376 -0
  48. manim/mobject/geometry/line.py +192 -87
  49. manim/mobject/geometry/polygram.py +224 -58
  50. manim/mobject/geometry/shape_matchers.py +61 -25
  51. manim/mobject/geometry/tips.py +122 -48
  52. manim/mobject/graph.py +1027 -419
  53. manim/mobject/graphing/coordinate_systems.py +533 -278
  54. manim/mobject/graphing/functions.py +53 -32
  55. manim/mobject/graphing/number_line.py +123 -65
  56. manim/mobject/graphing/probability.py +88 -62
  57. manim/mobject/graphing/scale.py +33 -19
  58. manim/mobject/logo.py +118 -28
  59. manim/mobject/matrix.py +87 -83
  60. manim/mobject/mobject.py +912 -442
  61. manim/mobject/opengl/dot_cloud.py +16 -5
  62. manim/mobject/opengl/opengl_compatibility.py +4 -2
  63. manim/mobject/opengl/opengl_geometry.py +254 -153
  64. manim/mobject/opengl/opengl_image_mobject.py +3 -1
  65. manim/mobject/opengl/opengl_mobject.py +779 -482
  66. manim/mobject/opengl/opengl_point_cloud_mobject.py +41 -14
  67. manim/mobject/opengl/opengl_surface.py +14 -92
  68. manim/mobject/opengl/opengl_three_dimensions.py +12 -8
  69. manim/mobject/opengl/opengl_vectorized_mobject.py +98 -100
  70. manim/mobject/svg/brace.py +173 -41
  71. manim/mobject/svg/svg_mobject.py +139 -53
  72. manim/mobject/table.py +61 -68
  73. manim/mobject/text/code_mobject.py +193 -539
  74. manim/mobject/text/numbers.py +81 -34
  75. manim/mobject/text/tex_mobject.py +130 -78
  76. manim/mobject/text/text_mobject.py +288 -164
  77. manim/mobject/three_d/polyhedra.py +111 -13
  78. manim/mobject/three_d/three_d_utils.py +17 -8
  79. manim/mobject/three_d/three_dimensions.py +239 -106
  80. manim/mobject/types/image_mobject.py +50 -30
  81. manim/mobject/types/point_cloud_mobject.py +120 -75
  82. manim/mobject/types/vectorized_mobject.py +841 -408
  83. manim/mobject/value_tracker.py +105 -38
  84. manim/mobject/vector_field.py +50 -31
  85. manim/opengl/__init__.py +3 -3
  86. manim/plugins/__init__.py +14 -1
  87. manim/plugins/plugins_flags.py +10 -14
  88. manim/renderer/cairo_renderer.py +65 -50
  89. manim/renderer/opengl_renderer.py +89 -69
  90. manim/renderer/opengl_renderer_window.py +39 -18
  91. manim/renderer/shader.py +123 -87
  92. manim/renderer/shader_wrapper.py +44 -28
  93. manim/renderer/vectorized_mobject_rendering.py +38 -10
  94. manim/scene/moving_camera_scene.py +32 -3
  95. manim/scene/scene.py +507 -242
  96. manim/scene/scene_file_writer.py +371 -220
  97. manim/scene/section.py +20 -16
  98. manim/scene/three_d_scene.py +14 -22
  99. manim/scene/vector_space_scene.py +223 -129
  100. manim/scene/zoomed_scene.py +46 -41
  101. manim/typing.py +990 -0
  102. manim/utils/bezier.py +1823 -371
  103. manim/utils/caching.py +12 -5
  104. manim/utils/color/AS2700.py +236 -0
  105. manim/utils/color/BS381.py +318 -0
  106. manim/utils/color/DVIPSNAMES.py +96 -0
  107. manim/utils/color/SVGNAMES.py +179 -0
  108. manim/utils/color/X11.py +533 -0
  109. manim/utils/color/XKCD.py +952 -0
  110. manim/utils/color/__init__.py +61 -0
  111. manim/utils/color/core.py +1667 -0
  112. manim/utils/color/manim_colors.py +218 -0
  113. manim/utils/commands.py +48 -20
  114. manim/utils/config_ops.py +39 -19
  115. manim/utils/debug.py +8 -7
  116. manim/utils/deprecation.py +86 -39
  117. manim/utils/docbuild/__init__.py +17 -0
  118. manim/utils/docbuild/autoaliasattr_directive.py +236 -0
  119. manim/utils/docbuild/autocolor_directive.py +99 -0
  120. manim/utils/docbuild/manim_directive.py +94 -41
  121. manim/utils/docbuild/module_parsing.py +245 -0
  122. manim/utils/exceptions.py +6 -0
  123. manim/utils/family.py +5 -3
  124. manim/utils/family_ops.py +17 -4
  125. manim/utils/file_ops.py +27 -17
  126. manim/utils/hashing.py +55 -45
  127. manim/utils/images.py +13 -7
  128. manim/utils/ipython_magic.py +13 -7
  129. manim/utils/iterables.py +163 -120
  130. manim/utils/module_ops.py +66 -24
  131. manim/utils/opengl.py +77 -24
  132. manim/utils/parameter_parsing.py +32 -0
  133. manim/utils/paths.py +30 -33
  134. manim/utils/polylabel.py +235 -0
  135. manim/utils/qhull.py +218 -0
  136. manim/utils/rate_functions.py +98 -32
  137. manim/utils/simple_functions.py +25 -33
  138. manim/utils/sounds.py +7 -1
  139. manim/utils/space_ops.py +188 -115
  140. manim/utils/testing/__init__.py +17 -0
  141. manim/utils/testing/_frames_testers.py +13 -8
  142. manim/utils/testing/_show_diff.py +5 -3
  143. manim/utils/testing/_test_class_makers.py +34 -18
  144. manim/utils/testing/frames_comparison.py +37 -19
  145. manim/utils/tex.py +130 -198
  146. manim/utils/tex_file_writing.py +77 -47
  147. manim/utils/tex_templates.py +2 -1
  148. manim/utils/unit.py +6 -5
  149. {manim-0.17.0.dist-info → manim-0.19.1.dist-info}/METADATA +64 -65
  150. manim-0.19.1.dist-info/RECORD +220 -0
  151. {manim-0.17.0.dist-info → manim-0.19.1.dist-info}/WHEEL +1 -1
  152. manim-0.19.1.dist-info/entry_points.txt +3 -0
  153. {manim-0.17.0.dist-info → manim-0.19.1.dist-info/licenses}/LICENSE.community +1 -1
  154. manim/cli/new/group.py +0 -189
  155. manim/communitycolors.py +0 -9
  156. manim/gui/__init__.py +0 -0
  157. manim/gui/gui.py +0 -82
  158. manim/plugins/import_plugins.py +0 -43
  159. manim/utils/color.py +0 -552
  160. manim-0.17.0.dist-info/RECORD +0 -206
  161. manim-0.17.0.dist-info/entry_points.txt +0 -4
  162. /manim/cli/{new → checkhealth}/__init__.py +0 -0
  163. {manim-0.17.0.dist-info → manim-0.19.1.dist-info/licenses}/LICENSE +0 -0
manim/scene/scene.py CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ from manim.utils.parameter_parsing import flatten_iterable_parameters
6
+
7
+ from ..mobject.mobject import _AnimationBuilder
8
+
5
9
  __all__ = ["Scene"]
6
10
 
7
11
  import copy
@@ -11,9 +15,9 @@ import platform
11
15
  import random
12
16
  import threading
13
17
  import time
14
- import types
18
+ from dataclasses import dataclass
19
+ from pathlib import Path
15
20
  from queue import Queue
16
- from typing import Callable
17
21
 
18
22
  import srt
19
23
 
@@ -23,13 +27,21 @@ try:
23
27
  import dearpygui.dearpygui as dpg
24
28
 
25
29
  dearpygui_imported = True
30
+ dpg.create_context()
31
+ window = dpg.generate_uuid()
26
32
  except ImportError:
27
33
  dearpygui_imported = False
34
+
35
+ from collections.abc import Callable, Iterable, Sequence
36
+ from typing import TYPE_CHECKING, Any, Union
37
+
28
38
  import numpy as np
29
39
  from tqdm import tqdm
30
- from watchdog.events import FileSystemEventHandler
40
+ from watchdog.events import DirModifiedEvent, FileModifiedEvent, FileSystemEventHandler
31
41
  from watchdog.observers import Observer
32
42
 
43
+ from manim import __version__
44
+ from manim.data_structures import MethodWithArgs
33
45
  from manim.mobject.mobject import Mobject
34
46
  from manim.mobject.opengl.opengl_mobject import OpenGLPoint
35
47
 
@@ -37,9 +49,8 @@ from .. import config, logger
37
49
  from ..animation.animation import Animation, Wait, prepare_animation
38
50
  from ..camera.camera import Camera
39
51
  from ..constants import *
40
- from ..gui.gui import configure_pygui
41
52
  from ..renderer.cairo_renderer import CairoRenderer
42
- from ..renderer.opengl_renderer import OpenGLRenderer
53
+ from ..renderer.opengl_renderer import OpenGLCamera, OpenGLMobject, OpenGLRenderer
43
54
  from ..renderer.shader import Object3D
44
55
  from ..utils import opengl, space_ops
45
56
  from ..utils.exceptions import EndSceneEarlyException, RerunSceneException
@@ -47,17 +58,83 @@ from ..utils.family import extract_mobject_family_members
47
58
  from ..utils.family_ops import restructure_list_to_exclude_certain_family_members
48
59
  from ..utils.file_ops import open_media_file
49
60
  from ..utils.iterables import list_difference_update, list_update
61
+ from ..utils.module_ops import scene_classes_from_file
62
+
63
+ if TYPE_CHECKING:
64
+ from types import FrameType
65
+
66
+ from typing_extensions import Self, TypeAlias
67
+
68
+ from manim.typing import Point3D
69
+
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.
84
+ """
85
+
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"``.
99
+ """
100
+
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
50
127
 
51
128
 
52
129
  class RerunSceneHandler(FileSystemEventHandler):
53
130
  """A class to handle rerunning a Scene after the input file is modified."""
54
131
 
55
- def __init__(self, queue):
132
+ def __init__(self, queue: Queue[SceneInteractAction]) -> None:
56
133
  super().__init__()
57
134
  self.queue = queue
58
135
 
59
- def on_modified(self, event):
60
- self.queue.put(("rerun_file", [], {}))
136
+ def on_modified(self, event: DirModifiedEvent | FileModifiedEvent) -> None:
137
+ self.queue.put(SceneInteractRerun("file"))
61
138
 
62
139
 
63
140
  class Scene:
@@ -93,35 +170,33 @@ class Scene:
93
170
 
94
171
  def __init__(
95
172
  self,
96
- renderer=None,
97
- camera_class=Camera,
98
- always_update_mobjects=False,
99
- random_seed=None,
100
- skip_animations=False,
101
- ):
173
+ renderer: CairoRenderer | OpenGLRenderer | None = None,
174
+ camera_class: type[Camera] = Camera,
175
+ always_update_mobjects: bool = False,
176
+ random_seed: int | None = None,
177
+ skip_animations: bool = False,
178
+ ) -> None:
102
179
  self.camera_class = camera_class
103
180
  self.always_update_mobjects = always_update_mobjects
104
181
  self.random_seed = random_seed
105
182
  self.skip_animations = skip_animations
106
183
 
107
- self.animations = None
108
- self.stop_condition = None
109
- self.moving_mobjects = []
110
- self.static_mobjects = []
111
- self.time_progression = None
112
- self.duration = None
113
- self.last_t = None
114
- self.queue = Queue()
184
+ self.animations: list[Animation] | None = None
185
+ self.stop_condition: Callable[[], bool] | None = None
186
+ self.moving_mobjects: list[Mobject] = []
187
+ self.static_mobjects: list[Mobject] = []
188
+ self.time_progression: tqdm[float] | None = None
189
+ self.duration: float = 0.0
190
+ self.last_t = 0.0
191
+ self.queue: Queue[SceneInteractAction] = Queue()
115
192
  self.skip_animation_preview = False
116
- self.meshes = []
193
+ self.meshes: list[Object3D] = []
117
194
  self.camera_target = ORIGIN
118
- self.widgets = []
195
+ self.widgets: list[dict[str, Any]] = []
119
196
  self.dearpygui_imported = dearpygui_imported
120
- self.updaters = []
121
- self.point_lights = []
122
- self.ambient_light = None
123
- self.key_to_function_map = {}
124
- self.mouse_press_callbacks = []
197
+ self.updaters: list[Callable[[float], None]] = []
198
+ self.key_to_function_map: dict[str, Callable[[], None]] = {}
199
+ self.mouse_press_callbacks: list[Callable[[], None]] = []
125
200
  self.interactive_mode = False
126
201
 
127
202
  if config.renderer == RendererType.OPENGL:
@@ -132,7 +207,9 @@ class Scene:
132
207
  renderer = OpenGLRenderer()
133
208
 
134
209
  if renderer is None:
135
- self.renderer = CairoRenderer(
210
+ self.renderer: CairoRenderer | OpenGLRenderer = CairoRenderer(
211
+ # TODO: Is it a suitable approach to make an instance of
212
+ # the self.camera_class here?
136
213
  camera_class=self.camera_class,
137
214
  skip_animations=self.skip_animations,
138
215
  )
@@ -140,18 +217,23 @@ class Scene:
140
217
  self.renderer = renderer
141
218
  self.renderer.init_scene(self)
142
219
 
143
- self.mobjects = []
220
+ self.mobjects: list[Mobject] = []
144
221
  # TODO, remove need for foreground mobjects
145
- self.foreground_mobjects = []
222
+ self.foreground_mobjects: list[Mobject] = []
146
223
  if self.random_seed is not None:
147
224
  random.seed(self.random_seed)
148
225
  np.random.seed(self.random_seed)
149
226
 
150
227
  @property
151
- def camera(self):
228
+ def camera(self) -> Camera | OpenGLCamera:
152
229
  return self.renderer.camera
153
230
 
154
- def __deepcopy__(self, clone_from_id):
231
+ @property
232
+ def time(self) -> float:
233
+ """The time since the start of the scene."""
234
+ return self.renderer.time
235
+
236
+ def __deepcopy__(self, clone_from_id: dict[int, Any]) -> Scene:
155
237
  cls = self.__class__
156
238
  result = cls.__new__(cls)
157
239
  clone_from_id[id(self)] = result
@@ -161,55 +243,10 @@ class Scene:
161
243
  if k == "camera_class":
162
244
  setattr(result, k, v)
163
245
  setattr(result, k, copy.deepcopy(v, clone_from_id))
164
- result.mobject_updater_lists = []
165
-
166
- # Update updaters
167
- for mobject in self.mobjects:
168
- cloned_updaters = []
169
- for updater in mobject.updaters:
170
- # Make the cloned updater use the cloned Mobjects as free variables
171
- # rather than the original ones. Analyzing function bytecode with the
172
- # dis module will help in understanding this.
173
- # https://docs.python.org/3/library/dis.html
174
- # TODO: Do the same for function calls recursively.
175
- free_variable_map = inspect.getclosurevars(updater).nonlocals
176
- cloned_co_freevars = []
177
- cloned_closure = []
178
- for free_variable_name in updater.__code__.co_freevars:
179
- free_variable_value = free_variable_map[free_variable_name]
180
-
181
- # If the referenced variable has not been cloned, raise.
182
- if id(free_variable_value) not in clone_from_id:
183
- raise Exception(
184
- f"{free_variable_name} is referenced from an updater "
185
- "but is not an attribute of the Scene, which isn't "
186
- "allowed.",
187
- )
188
-
189
- # Add the cloned object's name to the free variable list.
190
- cloned_co_freevars.append(free_variable_name)
191
-
192
- # Add a cell containing the cloned object's reference to the
193
- # closure list.
194
- cloned_closure.append(
195
- types.CellType(clone_from_id[id(free_variable_value)]),
196
- )
197
246
 
198
- cloned_updater = types.FunctionType(
199
- updater.__code__.replace(co_freevars=tuple(cloned_co_freevars)),
200
- updater.__globals__,
201
- updater.__name__,
202
- updater.__defaults__,
203
- tuple(cloned_closure),
204
- )
205
- cloned_updaters.append(cloned_updater)
206
- mobject_clone = clone_from_id[id(mobject)]
207
- mobject_clone.updaters = cloned_updaters
208
- if len(cloned_updaters) > 0:
209
- result.mobject_updater_lists.append((mobject_clone, cloned_updaters))
210
247
  return result
211
248
 
212
- def render(self, preview: bool = False):
249
+ def render(self, preview: bool = False) -> bool:
213
250
  """
214
251
  Renders this Scene.
215
252
 
@@ -223,9 +260,10 @@ class Scene:
223
260
  self.construct()
224
261
  except EndSceneEarlyException:
225
262
  pass
226
- except RerunSceneException as e:
263
+ except RerunSceneException:
227
264
  self.remove(*self.mobjects)
228
- self.renderer.clear_screen()
265
+ # TODO: The CairoRenderer does not have the method clear_screen()
266
+ self.renderer.clear_screen() # type: ignore[union-attr]
229
267
  self.renderer.num_plays = 0
230
268
  return True
231
269
  self.tear_down()
@@ -249,7 +287,9 @@ class Scene:
249
287
  if config["preview"] or config["show_in_file_browser"]:
250
288
  open_media_file(self.renderer.file_writer)
251
289
 
252
- def setup(self):
290
+ return False
291
+
292
+ def setup(self) -> None:
253
293
  """
254
294
  This is meant to be implemented by any scenes which
255
295
  are commonly subclassed, and have some common setup
@@ -257,7 +297,7 @@ class Scene:
257
297
  """
258
298
  pass
259
299
 
260
- def tear_down(self):
300
+ def tear_down(self) -> None:
261
301
  """
262
302
  This is meant to be implemented by any scenes which
263
303
  are commonly subclassed, and have some common method
@@ -265,7 +305,7 @@ class Scene:
265
305
  """
266
306
  pass
267
307
 
268
- def construct(self):
308
+ def construct(self) -> None:
269
309
  """Add content to the Scene.
270
310
 
271
311
  From within :meth:`Scene.construct`, display mobjects on screen by calling
@@ -281,7 +321,7 @@ class Scene:
281
321
  Examples
282
322
  --------
283
323
  A typical manim script includes a class derived from :class:`Scene` with an
284
- overridden :meth:`Scene.contruct` method:
324
+ overridden :meth:`Scene.construct` method:
285
325
 
286
326
  .. code-block:: python
287
327
 
@@ -301,19 +341,19 @@ class Scene:
301
341
  def next_section(
302
342
  self,
303
343
  name: str = "unnamed",
304
- type: str = DefaultSectionType.NORMAL,
344
+ section_type: str = DefaultSectionType.NORMAL,
305
345
  skip_animations: bool = False,
306
346
  ) -> None:
307
347
  """Create separation here; the last section gets finished and a new one gets created.
308
348
  ``skip_animations`` skips the rendering of all animations in this section.
309
349
  Refer to :doc:`the documentation</tutorials/output_and_config>` on how to use sections.
310
350
  """
311
- self.renderer.file_writer.next_section(name, type, skip_animations)
351
+ self.renderer.file_writer.next_section(name, section_type, skip_animations)
312
352
 
313
- def __str__(self):
353
+ def __str__(self) -> str:
314
354
  return self.__class__.__name__
315
355
 
316
- def get_attrs(self, *keys: str):
356
+ def get_attrs(self, *keys: str) -> list[Any]:
317
357
  """
318
358
  Gets attributes of a scene given the attribute's identifier/name.
319
359
 
@@ -329,7 +369,7 @@ class Scene:
329
369
  """
330
370
  return [getattr(self, key) for key in keys]
331
371
 
332
- def update_mobjects(self, dt: float):
372
+ def update_mobjects(self, dt: float) -> None:
333
373
  """
334
374
  Begins updating all mobjects in the Scene.
335
375
 
@@ -338,15 +378,15 @@ class Scene:
338
378
  dt
339
379
  Change in time between updates. Defaults (mostly) to 1/frames_per_second
340
380
  """
341
- for mobject in self.mobjects:
342
- mobject.update(dt)
381
+ for mobj in self.mobjects:
382
+ mobj.update(dt)
343
383
 
344
- def update_meshes(self, dt):
384
+ def update_meshes(self, dt: float) -> None:
345
385
  for obj in self.meshes:
346
386
  for mesh in obj.get_family():
347
387
  mesh.update(dt)
348
388
 
349
- def update_self(self, dt: float):
389
+ def update_self(self, dt: float) -> None:
350
390
  """Run all scene updater functions.
351
391
 
352
392
  Among all types of update functions (mobject updaters, mesh updaters,
@@ -378,22 +418,23 @@ class Scene:
378
418
 
379
419
  This is only called when a single Wait animation is played.
380
420
  """
421
+ assert self.animations is not None
381
422
  wait_animation = self.animations[0]
423
+ assert isinstance(wait_animation, Wait)
382
424
  if wait_animation.is_static_wait is None:
383
425
  should_update = (
384
426
  self.always_update_mobjects
385
427
  or self.updaters
428
+ or wait_animation.stop_condition is not None
386
429
  or any(
387
- [
388
- mob.has_time_based_updater()
389
- for mob in self.get_mobject_family_members()
390
- ],
430
+ mob.has_time_based_updater()
431
+ for mob in self.get_mobject_family_members()
391
432
  )
392
433
  )
393
434
  wait_animation.is_static_wait = not should_update
394
435
  return not wait_animation.is_static_wait
395
436
 
396
- def get_top_level_mobjects(self):
437
+ def get_top_level_mobjects(self) -> list[Mobject]:
397
438
  """
398
439
  Returns all mobjects which are not submobjects.
399
440
 
@@ -406,13 +447,13 @@ class Scene:
406
447
  # of another mobject from the scene
407
448
  families = [m.get_family() for m in self.mobjects]
408
449
 
409
- def is_top_level(mobject):
450
+ def is_top_level(mobject: Mobject) -> bool:
410
451
  num_families = sum((mobject in family) for family in families)
411
452
  return num_families == 1
412
453
 
413
454
  return list(filter(is_top_level, self.mobjects))
414
455
 
415
- def get_mobject_family_members(self):
456
+ def get_mobject_family_members(self) -> list[Mobject]:
416
457
  """
417
458
  Returns list of family-members of all mobjects in scene.
418
459
  If a Circle() and a VGroup(Rectangle(),Triangle()) were added,
@@ -429,13 +470,14 @@ class Scene:
429
470
  for mob in self.mobjects:
430
471
  family_members.extend(mob.get_family())
431
472
  return family_members
432
- elif config.renderer == RendererType.CAIRO:
473
+ else:
474
+ assert config.renderer == RendererType.CAIRO
433
475
  return extract_mobject_family_members(
434
476
  self.mobjects,
435
477
  use_z_index=self.renderer.camera.use_z_index,
436
478
  )
437
479
 
438
- def add(self, *mobjects: Mobject):
480
+ def add(self, *mobjects: Mobject | OpenGLMobject) -> Self:
439
481
  """
440
482
  Mobjects will be displayed, from background to
441
483
  foreground in the order with which they are added.
@@ -453,29 +495,33 @@ class Scene:
453
495
  """
454
496
  if config.renderer == RendererType.OPENGL:
455
497
  new_mobjects = []
456
- new_meshes = []
498
+ new_meshes: list[Object3D] = []
457
499
  for mobject_or_mesh in mobjects:
458
500
  if isinstance(mobject_or_mesh, Object3D):
459
501
  new_meshes.append(mobject_or_mesh)
460
502
  else:
461
503
  new_mobjects.append(mobject_or_mesh)
462
- self.remove(*new_mobjects)
463
- self.mobjects += new_mobjects
464
- self.remove(*new_meshes)
504
+ self.remove(*new_mobjects) # type: ignore[arg-type]
505
+ self.mobjects += new_mobjects # type: ignore[arg-type]
506
+ self.remove(*new_meshes) # type: ignore[arg-type]
465
507
  self.meshes += new_meshes
466
- elif config.renderer == RendererType.CAIRO:
467
- mobjects = [*mobjects, *self.foreground_mobjects]
468
- self.restructure_mobjects(to_remove=mobjects)
469
- self.mobjects += mobjects
508
+ else:
509
+ assert config.renderer == RendererType.CAIRO
510
+ new_and_foreground_mobjects: list[Mobject] = [
511
+ *mobjects, # type: ignore[list-item]
512
+ *self.foreground_mobjects,
513
+ ]
514
+ self.restructure_mobjects(to_remove=new_and_foreground_mobjects)
515
+ self.mobjects += new_and_foreground_mobjects
470
516
  if self.moving_mobjects:
471
517
  self.restructure_mobjects(
472
- to_remove=mobjects,
518
+ to_remove=new_and_foreground_mobjects,
473
519
  mobject_list_name="moving_mobjects",
474
520
  )
475
- self.moving_mobjects += mobjects
521
+ self.moving_mobjects += new_and_foreground_mobjects
476
522
  return self
477
523
 
478
- def add_mobjects_from_animations(self, animations):
524
+ def add_mobjects_from_animations(self, animations: list[Animation]) -> None:
479
525
  curr_mobjects = self.get_mobject_family_members()
480
526
  for animation in animations:
481
527
  if animation.is_introducer():
@@ -485,9 +531,9 @@ class Scene:
485
531
  mob = animation.mobject
486
532
  if mob is not None and mob not in curr_mobjects:
487
533
  self.add(mob)
488
- curr_mobjects += mob.get_family()
534
+ curr_mobjects += mob.get_family() # type: ignore[arg-type]
489
535
 
490
- def remove(self, *mobjects: Mobject):
536
+ def remove(self, *mobjects: Mobject) -> Self:
491
537
  """
492
538
  Removes mobjects in the passed list of mobjects
493
539
  from the scene and the foreground, by removing them
@@ -500,7 +546,8 @@ class Scene:
500
546
  """
501
547
  if config.renderer == RendererType.OPENGL:
502
548
  mobjects_to_remove = []
503
- meshes_to_remove = set()
549
+ meshes_to_remove: set[Object3D] = set()
550
+ mobject_or_mesh: Mobject
504
551
  for mobject_or_mesh in mobjects:
505
552
  if isinstance(mobject_or_mesh, Object3D):
506
553
  meshes_to_remove.add(mobject_or_mesh)
@@ -510,15 +557,80 @@ class Scene:
510
557
  self.mobjects,
511
558
  mobjects_to_remove,
512
559
  )
560
+
561
+ def lambda_function(mesh: Object3D) -> bool:
562
+ return mesh not in set(meshes_to_remove)
563
+
513
564
  self.meshes = list(
514
- filter(lambda mesh: mesh not in set(meshes_to_remove), self.meshes),
565
+ filter(lambda_function, self.meshes),
515
566
  )
516
567
  return self
517
- elif config.renderer == RendererType.CAIRO:
568
+ else:
569
+ assert config.renderer == RendererType.CAIRO
518
570
  for list_name in "mobjects", "foreground_mobjects":
519
571
  self.restructure_mobjects(mobjects, list_name, False)
520
572
  return self
521
573
 
574
+ def replace(self, old_mobject: Mobject, new_mobject: Mobject) -> None:
575
+ """Replace one mobject in the scene with another, preserving draw order.
576
+
577
+ If ``old_mobject`` is a submobject of some other Mobject (e.g. a
578
+ :class:`.Group`), the new_mobject will replace it inside the group,
579
+ without otherwise changing the parent mobject.
580
+
581
+ Parameters
582
+ ----------
583
+ old_mobject
584
+ The mobject to be replaced. Must be present in the scene.
585
+ new_mobject
586
+ A mobject which must not already be in the scene.
587
+
588
+ """
589
+ if old_mobject is None or new_mobject is None:
590
+ raise ValueError("Specified mobjects cannot be None")
591
+
592
+ def replace_in_list(
593
+ mobj_list: list[Mobject], old_m: Mobject, new_m: Mobject
594
+ ) -> bool:
595
+ # Avoid duplicate references to the same object in self.mobjects
596
+ if new_m in mobj_list:
597
+ if old_m is new_m:
598
+ # In this case, one could say that the old Mobject was already found.
599
+ # No replacement is needed, since old_m is new_m, so no action is required.
600
+ # This might be unexpected, so raise a warning.
601
+ logger.warning(
602
+ f"Attempted to replace {type(old_m).__name__} "
603
+ "with itself in Scene.mobjects."
604
+ )
605
+ return True
606
+ mobj_list.remove(new_m)
607
+
608
+ # We use breadth-first search because some Mobjects get very deep and
609
+ # we expect top-level elements to be the most common targets for replace.
610
+ for i in range(0, len(mobj_list)):
611
+ # Is this the old mobject?
612
+ if mobj_list[i] == old_m:
613
+ # If so, write the new object to the same spot and stop looking.
614
+ mobj_list[i] = new_m
615
+ return True
616
+ # Now check all the children of all these mobs.
617
+ for mob in mobj_list: # noqa: SIM110
618
+ if replace_in_list(mob.submobjects, old_m, new_m):
619
+ # If we found it in a submobject, stop looking.
620
+ return True
621
+ # If we did not find the mobject in the mobject list or any submobjects,
622
+ # (or the list was empty), indicate we did not make the replacement.
623
+ return False
624
+
625
+ # Make use of short-circuiting conditionals to check mobjects and then
626
+ # foreground_mobjects
627
+ replaced = replace_in_list(
628
+ self.mobjects, old_mobject, new_mobject
629
+ ) or replace_in_list(self.foreground_mobjects, old_mobject, new_mobject)
630
+
631
+ if not replaced:
632
+ raise ValueError(f"Could not find {old_mobject} in scene")
633
+
522
634
  def add_updater(self, func: Callable[[float], None]) -> None:
523
635
  """Add an update function to the scene.
524
636
 
@@ -567,10 +679,10 @@ class Scene:
567
679
 
568
680
  def restructure_mobjects(
569
681
  self,
570
- to_remove: Mobject,
682
+ to_remove: Sequence[Mobject],
571
683
  mobject_list_name: str = "mobjects",
572
684
  extract_families: bool = True,
573
- ):
685
+ ) -> Scene:
574
686
  """
575
687
  tl:wr
576
688
  If your scene has a Group(), and you removed a mobject from the Group,
@@ -608,7 +720,9 @@ class Scene:
608
720
  setattr(self, mobject_list_name, new_list)
609
721
  return self
610
722
 
611
- def get_restructured_mobject_list(self, mobjects: list, to_remove: list):
723
+ def get_restructured_mobject_list(
724
+ self, mobjects: Iterable[Mobject], to_remove: Iterable[Mobject]
725
+ ) -> list[Mobject]:
612
726
  """
613
727
  Given a list of mobjects and a list of mobjects to be removed, this
614
728
  filters out the removable mobjects from the list of mobjects.
@@ -627,10 +741,11 @@ class Scene:
627
741
  list
628
742
  The list of mobjects with the mobjects to remove removed.
629
743
  """
744
+ new_mobjects: list[Mobject] = []
630
745
 
631
- new_mobjects = []
632
-
633
- def add_safe_mobjects_from_list(list_to_examine, set_to_remove):
746
+ def add_safe_mobjects_from_list(
747
+ list_to_examine: Iterable[Mobject], set_to_remove: set[Mobject]
748
+ ) -> None:
634
749
  for mob in list_to_examine:
635
750
  if mob in set_to_remove:
636
751
  continue
@@ -644,7 +759,7 @@ class Scene:
644
759
  return new_mobjects
645
760
 
646
761
  # TODO, remove this, and calls to this
647
- def add_foreground_mobjects(self, *mobjects: Mobject):
762
+ def add_foreground_mobjects(self, *mobjects: Mobject) -> Scene:
648
763
  """
649
764
  Adds mobjects to the foreground, and internally to the list
650
765
  foreground_mobjects, and mobjects.
@@ -663,7 +778,7 @@ class Scene:
663
778
  self.add(*mobjects)
664
779
  return self
665
780
 
666
- def add_foreground_mobject(self, mobject: Mobject):
781
+ def add_foreground_mobject(self, mobject: Mobject) -> Scene:
667
782
  """
668
783
  Adds a single mobject to the foreground, and internally to the list
669
784
  foreground_mobjects, and mobjects.
@@ -680,7 +795,7 @@ class Scene:
680
795
  """
681
796
  return self.add_foreground_mobjects(mobject)
682
797
 
683
- def remove_foreground_mobjects(self, *to_remove: Mobject):
798
+ def remove_foreground_mobjects(self, *to_remove: Mobject) -> Scene:
684
799
  """
685
800
  Removes mobjects from the foreground, and internally from the list
686
801
  foreground_mobjects.
@@ -698,7 +813,7 @@ class Scene:
698
813
  self.restructure_mobjects(to_remove, "foreground_mobjects")
699
814
  return self
700
815
 
701
- def remove_foreground_mobject(self, mobject: Mobject):
816
+ def remove_foreground_mobject(self, mobject: Mobject) -> Scene:
702
817
  """
703
818
  Removes a single mobject from the foreground, and internally from the list
704
819
  foreground_mobjects.
@@ -715,7 +830,7 @@ class Scene:
715
830
  """
716
831
  return self.remove_foreground_mobjects(mobject)
717
832
 
718
- def bring_to_front(self, *mobjects: Mobject):
833
+ def bring_to_front(self, *mobjects: Mobject) -> Scene:
719
834
  """
720
835
  Adds the passed mobjects to the scene again,
721
836
  pushing them to he front of the scene.
@@ -734,7 +849,7 @@ class Scene:
734
849
  self.add(*mobjects)
735
850
  return self
736
851
 
737
- def bring_to_back(self, *mobjects: Mobject):
852
+ def bring_to_back(self, *mobjects: Mobject) -> Scene:
738
853
  """
739
854
  Removes the mobject from the scene and
740
855
  adds them to the back of the scene.
@@ -754,7 +869,7 @@ class Scene:
754
869
  self.mobjects = list(mobjects) + self.mobjects
755
870
  return self
756
871
 
757
- def clear(self):
872
+ def clear(self) -> Self:
758
873
  """
759
874
  Removes all mobjects present in self.mobjects
760
875
  and self.foreground_mobjects from the scene.
@@ -770,7 +885,7 @@ class Scene:
770
885
  self.foreground_mobjects = []
771
886
  return self
772
887
 
773
- def get_moving_mobjects(self, *animations: Animation):
888
+ def get_moving_mobjects(self, *animations: Animation) -> list[Mobject]:
774
889
  """
775
890
  Gets all moving mobjects in the passed animation(s).
776
891
 
@@ -801,7 +916,9 @@ class Scene:
801
916
  return mobjects[i:]
802
917
  return []
803
918
 
804
- def get_moving_and_static_mobjects(self, animations):
919
+ def get_moving_and_static_mobjects(
920
+ self, animations: Iterable[Animation]
921
+ ) -> tuple[list[Mobject], list[Mobject]]:
805
922
  all_mobjects = list_update(self.mobjects, self.foreground_mobjects)
806
923
  all_mobject_families = extract_mobject_family_members(
807
924
  all_mobjects,
@@ -819,7 +936,11 @@ class Scene:
819
936
  )
820
937
  return all_moving_mobject_families, static_mobjects
821
938
 
822
- def compile_animations(self, *args: Animation, **kwargs):
939
+ def compile_animations(
940
+ self,
941
+ *args: Animation | Mobject | _AnimationBuilder,
942
+ **kwargs: Any,
943
+ ) -> list[Animation]:
823
944
  """
824
945
  Creates _MethodAnimations from any _AnimationBuilders and updates animation
825
946
  kwargs with kwargs passed to play().
@@ -837,19 +958,21 @@ class Scene:
837
958
  Animations to be played.
838
959
  """
839
960
  animations = []
840
- for arg in args:
961
+ arg_anims = flatten_iterable_parameters(args)
962
+ # Allow passing a generator to self.play instead of comma separated arguments
963
+ for arg in arg_anims:
841
964
  try:
842
- animations.append(prepare_animation(arg))
843
- except TypeError:
965
+ animations.append(prepare_animation(arg)) # type: ignore[arg-type]
966
+ except TypeError as e:
844
967
  if inspect.ismethod(arg):
845
968
  raise TypeError(
846
969
  "Passing Mobject methods to Scene.play is no longer"
847
970
  " supported. Use Mobject.animate instead.",
848
- )
971
+ ) from e
849
972
  else:
850
973
  raise TypeError(
851
974
  f"Unexpected argument {arg} passed to Scene.play().",
852
- )
975
+ ) from e
853
976
 
854
977
  for animation in animations:
855
978
  for k, v in kwargs.items():
@@ -859,7 +982,7 @@ class Scene:
859
982
 
860
983
  def _get_animation_time_progression(
861
984
  self, animations: list[Animation], duration: float
862
- ):
985
+ ) -> tqdm[float]:
863
986
  """
864
987
  You will hardly use this when making your own animations.
865
988
  This method is for Manim's internal use.
@@ -912,10 +1035,10 @@ class Scene:
912
1035
  def get_time_progression(
913
1036
  self,
914
1037
  run_time: float,
915
- description,
1038
+ description: str,
916
1039
  n_iterations: int | None = None,
917
1040
  override_skip_animations: bool = False,
918
- ):
1041
+ ) -> tqdm[float]:
919
1042
  """
920
1043
  You will hardly use this when making your own animations.
921
1044
  This method is for Manim's internal use.
@@ -943,7 +1066,7 @@ class Scene:
943
1066
  The CommandLine Progress Bar.
944
1067
  """
945
1068
  if self.renderer.skip_animations and not override_skip_animations:
946
- times = [run_time]
1069
+ times: Iterable[float] = [run_time]
947
1070
  else:
948
1071
  step = 1 / config["frame_rate"]
949
1072
  times = np.arange(0, run_time, step)
@@ -957,7 +1080,36 @@ class Scene:
957
1080
  )
958
1081
  return time_progression
959
1082
 
960
- def get_run_time(self, animations: list[Animation]):
1083
+ @classmethod
1084
+ def validate_run_time(
1085
+ cls,
1086
+ run_time: float,
1087
+ method: Callable[[Any], Any],
1088
+ parameter_name: str = "run_time",
1089
+ ) -> float:
1090
+ method_name = f"{cls.__name__}.{method.__name__}()"
1091
+ if run_time <= 0:
1092
+ raise ValueError(
1093
+ f"{method_name} has a {parameter_name} of "
1094
+ f"{run_time:g} <= 0 seconds which Manim cannot render. "
1095
+ f"The {parameter_name} must be a positive number."
1096
+ )
1097
+
1098
+ # config.frame_rate holds the number of frames per second
1099
+ fps = config.frame_rate
1100
+ seconds_per_frame = 1 / fps
1101
+ if run_time < seconds_per_frame:
1102
+ logger.warning(
1103
+ f"The original {parameter_name} of {method_name}, "
1104
+ f"{run_time:g} seconds, is too short for the current frame "
1105
+ f"rate of {fps:g} FPS. Rendering with the shortest possible "
1106
+ f"{parameter_name} of {seconds_per_frame:g} seconds instead."
1107
+ )
1108
+ run_time = seconds_per_frame
1109
+
1110
+ return run_time
1111
+
1112
+ def get_run_time(self, animations: list[Animation]) -> float:
961
1113
  """
962
1114
  Gets the total run time for a list of animations.
963
1115
 
@@ -972,24 +1124,18 @@ class Scene:
972
1124
  float
973
1125
  The total ``run_time`` of all of the animations in the list.
974
1126
  """
975
-
976
- if len(animations) == 1 and isinstance(animations[0], Wait):
977
- if animations[0].stop_condition is not None:
978
- return 0
979
- else:
980
- return animations[0].duration
981
-
982
- else:
983
- return np.max([animation.run_time for animation in animations])
1127
+ run_time = max(animation.run_time for animation in animations)
1128
+ run_time = self.validate_run_time(run_time, self.play, "total run_time")
1129
+ return run_time
984
1130
 
985
1131
  def play(
986
1132
  self,
987
- *args,
988
- subcaption=None,
989
- subcaption_duration=None,
990
- subcaption_offset=0,
991
- **kwargs,
992
- ):
1133
+ *args: Animation | Mobject | _AnimationBuilder,
1134
+ subcaption: str | None = None,
1135
+ subcaption_duration: float | None = None,
1136
+ subcaption_offset: float = 0,
1137
+ **kwargs: Any,
1138
+ ) -> None:
993
1139
  r"""Plays an animation in this scene.
994
1140
 
995
1141
  Parameters
@@ -1011,8 +1157,13 @@ class Scene:
1011
1157
  All other keywords are passed to the renderer.
1012
1158
 
1013
1159
  """
1014
- # Make sure this is running on the main thread
1015
- if threading.current_thread().name != "MainThread":
1160
+ # If we are in interactive embedded mode, make sure this is running on the main thread (required for OpenGL)
1161
+ if (
1162
+ self.interactive_mode
1163
+ and config.renderer == RendererType.OPENGL
1164
+ and threading.current_thread().name != "MainThread"
1165
+ ):
1166
+ # TODO: are these actually being used?
1016
1167
  kwargs.update(
1017
1168
  {
1018
1169
  "subcaption": subcaption,
@@ -1020,24 +1171,18 @@ class Scene:
1020
1171
  "subcaption_offset": subcaption_offset,
1021
1172
  }
1022
1173
  )
1023
- self.queue.put(
1024
- (
1025
- "play",
1026
- args,
1027
- kwargs,
1028
- )
1029
- )
1174
+ self.queue.put(SceneInteractRerun("play", **kwargs))
1030
1175
  return
1031
1176
 
1032
- start_time = self.renderer.time
1177
+ start_time = self.time
1033
1178
  self.renderer.play(self, *args, **kwargs)
1034
- run_time = self.renderer.time - start_time
1179
+ run_time = self.time - start_time
1035
1180
  if subcaption:
1036
1181
  if subcaption_duration is None:
1037
1182
  subcaption_duration = run_time
1038
1183
  # The start of the subcaption needs to be offset by the
1039
1184
  # run_time of the animation because it is added after
1040
- # the animation has already been played (and Scene.renderer.time
1185
+ # the animation has already been played (and Scene.time
1041
1186
  # has already been updated).
1042
1187
  self.add_subcaption(
1043
1188
  content=subcaption,
@@ -1050,7 +1195,7 @@ class Scene:
1050
1195
  duration: float = DEFAULT_WAIT_TIME,
1051
1196
  stop_condition: Callable[[], bool] | None = None,
1052
1197
  frozen_frame: bool | None = None,
1053
- ):
1198
+ ) -> None:
1054
1199
  """Plays a "no operation" animation.
1055
1200
 
1056
1201
  Parameters
@@ -1060,7 +1205,8 @@ class Scene:
1060
1205
  stop_condition
1061
1206
  A function without positional arguments that is evaluated every time
1062
1207
  a frame is rendered. The animation only stops when the return value
1063
- of the function is truthy. Overrides any value passed to ``duration``.
1208
+ of the function is truthy, or when the time specified in ``duration``
1209
+ passes.
1064
1210
  frozen_frame
1065
1211
  If True, updater functions are not evaluated, and the animation outputs
1066
1212
  a frozen frame. If False, updater functions are called and frames
@@ -1071,6 +1217,7 @@ class Scene:
1071
1217
  --------
1072
1218
  :class:`.Wait`, :meth:`.should_mobjects_update`
1073
1219
  """
1220
+ duration = self.validate_run_time(duration, self.wait, "duration")
1074
1221
  self.play(
1075
1222
  Wait(
1076
1223
  run_time=duration,
@@ -1079,7 +1226,7 @@ class Scene:
1079
1226
  )
1080
1227
  )
1081
1228
 
1082
- def pause(self, duration: float = DEFAULT_WAIT_TIME):
1229
+ def pause(self, duration: float = DEFAULT_WAIT_TIME) -> None:
1083
1230
  """Pauses the scene (i.e., displays a frozen frame).
1084
1231
 
1085
1232
  This is an alias for :meth:`.wait` with ``frozen_frame``
@@ -1094,25 +1241,30 @@ class Scene:
1094
1241
  --------
1095
1242
  :meth:`.wait`, :class:`.Wait`
1096
1243
  """
1244
+ duration = self.validate_run_time(duration, self.pause, "duration")
1097
1245
  self.wait(duration=duration, frozen_frame=True)
1098
1246
 
1099
- def wait_until(self, stop_condition: Callable[[], bool], max_time: float = 60):
1100
- """
1101
- Like a wrapper for wait().
1102
- You pass a function that determines whether to continue waiting,
1103
- and a max wait time if that is never fulfilled.
1247
+ def wait_until(
1248
+ self, stop_condition: Callable[[], bool], max_time: float = 60
1249
+ ) -> None:
1250
+ """Wait until a condition is satisfied, up to a given maximum duration.
1104
1251
 
1105
1252
  Parameters
1106
1253
  ----------
1107
1254
  stop_condition
1108
- The function whose boolean return value determines whether to continue waiting
1109
-
1255
+ A function with no arguments that determines whether or not the
1256
+ scene should keep waiting.
1110
1257
  max_time
1111
- The maximum wait time in seconds, if the stop_condition is never fulfilled.
1258
+ The maximum wait time in seconds.
1112
1259
  """
1260
+ max_time = self.validate_run_time(max_time, self.wait_until, "max_time")
1113
1261
  self.wait(max_time, stop_condition=stop_condition)
1114
1262
 
1115
- def compile_animation_data(self, *animations: Animation, **play_kwargs):
1263
+ def compile_animation_data(
1264
+ self,
1265
+ *animations: Animation | Mobject | _AnimationBuilder,
1266
+ **play_kwargs: Any,
1267
+ ) -> Self | None:
1116
1268
  """Given a list of animations, compile the corresponding
1117
1269
  static and moving mobjects, and gather the animation durations.
1118
1270
 
@@ -1144,20 +1296,21 @@ class Scene:
1144
1296
  self.moving_mobjects = []
1145
1297
  self.static_mobjects = []
1146
1298
 
1299
+ self.duration = self.get_run_time(self.animations)
1147
1300
  if len(self.animations) == 1 and isinstance(self.animations[0], Wait):
1148
1301
  if self.should_update_mobjects():
1149
1302
  self.update_mobjects(dt=0) # Any problems with this?
1150
1303
  self.stop_condition = self.animations[0].stop_condition
1151
1304
  else:
1152
- self.duration = self.animations[0].duration
1153
1305
  # Static image logic when the wait is static is done by the renderer, not here.
1154
1306
  self.animations[0].is_static_wait = True
1155
1307
  return None
1156
- self.duration = self.get_run_time(self.animations)
1308
+
1157
1309
  return self
1158
1310
 
1159
1311
  def begin_animations(self) -> None:
1160
1312
  """Start the animations of the scene."""
1313
+ assert self.animations is not None
1161
1314
  for animation in self.animations:
1162
1315
  animation._setup_scene(self)
1163
1316
  animation.begin()
@@ -1172,13 +1325,14 @@ class Scene:
1172
1325
 
1173
1326
  def is_current_animation_frozen_frame(self) -> bool:
1174
1327
  """Returns whether the current animation produces a static frame (generally a Wait)."""
1328
+ assert self.animations is not None
1175
1329
  return (
1176
1330
  isinstance(self.animations[0], Wait)
1177
1331
  and len(self.animations) == 1
1178
1332
  and self.animations[0].is_static_wait
1179
1333
  )
1180
1334
 
1181
- def play_internal(self, skip_rendering: bool = False):
1335
+ def play_internal(self, skip_rendering: bool = False) -> None:
1182
1336
  """
1183
1337
  This method is used to prep the animations for rendering,
1184
1338
  apply the arguments and parameters required to them,
@@ -1189,6 +1343,7 @@ class Scene:
1189
1343
  skip_rendering
1190
1344
  Whether the rendering should be skipped, by default False
1191
1345
  """
1346
+ assert self.animations is not None
1192
1347
  self.duration = self.get_run_time(self.animations)
1193
1348
  self.time_progression = self._get_animation_time_progression(
1194
1349
  self.animations,
@@ -1207,11 +1362,13 @@ class Scene:
1207
1362
  animation.clean_up_from_scene(self)
1208
1363
  if not self.renderer.skip_animations:
1209
1364
  self.update_mobjects(0)
1210
- self.renderer.static_image = None
1365
+ # TODO: The OpenGLRenderer does not have the property static.image.
1366
+ self.renderer.static_image = None # type: ignore[union-attr]
1211
1367
  # Closing the progress bar at the end of the play.
1212
1368
  self.time_progression.close()
1213
1369
 
1214
- def check_interactive_embed_is_valid(self):
1370
+ def check_interactive_embed_is_valid(self) -> bool:
1371
+ assert isinstance(self.renderer, OpenGLRenderer)
1215
1372
  if config["force_window"]:
1216
1373
  return True
1217
1374
  if self.skip_animation_preview:
@@ -1236,48 +1393,64 @@ class Scene:
1236
1393
  return False
1237
1394
  return True
1238
1395
 
1239
- def interactive_embed(self):
1240
- """
1241
- Like embed(), but allows for screen interaction.
1242
- """
1396
+ def interactive_embed(self) -> None:
1397
+ """Like embed(), but allows for screen interaction."""
1398
+ assert isinstance(self.camera, OpenGLCamera)
1399
+ assert isinstance(self.renderer, OpenGLRenderer)
1243
1400
  if not self.check_interactive_embed_is_valid():
1244
1401
  return
1245
1402
  self.interactive_mode = True
1403
+ from IPython.terminal.embed import InteractiveShellEmbed
1246
1404
 
1247
- def ipython(shell, namespace):
1405
+ def ipython(shell: InteractiveShellEmbed, namespace: dict[str, Any]) -> None:
1248
1406
  import manim.opengl
1249
1407
 
1250
- def load_module_into_namespace(module, namespace):
1408
+ def load_module_into_namespace(
1409
+ module: Any, namespace: dict[str, Any]
1410
+ ) -> None:
1251
1411
  for name in dir(module):
1252
1412
  namespace[name] = getattr(module, name)
1253
1413
 
1254
1414
  load_module_into_namespace(manim, namespace)
1255
1415
  load_module_into_namespace(manim.opengl, namespace)
1256
1416
 
1257
- def embedded_rerun(*args, **kwargs):
1258
- self.queue.put(("rerun_keyboard", args, kwargs))
1417
+ def embedded_rerun(*args: Any, **kwargs: Any) -> None:
1418
+ self.queue.put(SceneInteractRerun("keyboard"))
1259
1419
  shell.exiter()
1260
1420
 
1261
1421
  namespace["rerun"] = embedded_rerun
1262
1422
 
1263
1423
  shell(local_ns=namespace)
1264
- self.queue.put(("exit_keyboard", [], {}))
1424
+ self.queue.put(SceneInteractContinue("keyboard"))
1265
1425
 
1266
- def get_embedded_method(method_name):
1267
- return lambda *args, **kwargs: self.queue.put((method_name, args, kwargs))
1426
+ def get_embedded_method(method_name: str) -> Callable[..., None]:
1427
+ method = getattr(self, method_name)
1268
1428
 
1269
- local_namespace = inspect.currentframe().f_back.f_locals
1429
+ def embedded_method(*args: Any, **kwargs: Any) -> None:
1430
+ self.queue.put(MethodWithArgs(method, args, kwargs))
1431
+
1432
+ return embedded_method
1433
+
1434
+ currentframe: FrameType = inspect.currentframe() # type: ignore[assignment]
1435
+ local_namespace = currentframe.f_back.f_locals # type: ignore[union-attr]
1270
1436
  for method in ("play", "wait", "add", "remove"):
1271
1437
  embedded_method = get_embedded_method(method)
1272
1438
  # Allow for calling scene methods without prepending 'self.'.
1273
1439
  local_namespace[method] = embedded_method
1274
1440
 
1275
- from IPython.terminal.embed import InteractiveShellEmbed
1441
+ from sqlite3 import connect
1442
+
1443
+ from IPython.core.getipython import get_ipython
1276
1444
  from traitlets.config import Config
1277
1445
 
1278
1446
  cfg = Config()
1279
1447
  cfg.TerminalInteractiveShell.confirm_exit = False
1280
- shell = InteractiveShellEmbed(config=cfg)
1448
+ if get_ipython() is None:
1449
+ shell = InteractiveShellEmbed.instance(config=cfg)
1450
+ else:
1451
+ shell = InteractiveShellEmbed(config=cfg)
1452
+ hist = get_ipython().history_manager
1453
+ hist.db = connect(hist.hist_file, check_same_thread=False)
1281
1454
 
1282
1455
  keyboard_thread = threading.Thread(
1283
1456
  target=ipython,
@@ -1291,19 +1464,21 @@ class Scene:
1291
1464
  if self.dearpygui_imported and config["enable_gui"]:
1292
1465
  if not dpg.is_dearpygui_running():
1293
1466
  gui_thread = threading.Thread(
1294
- target=configure_pygui,
1295
- args=(self.renderer, self.widgets),
1467
+ target=self._configure_pygui,
1296
1468
  kwargs={"update": False},
1297
1469
  )
1298
1470
  gui_thread.start()
1299
1471
  else:
1300
- configure_pygui(self.renderer, self.widgets, update=True)
1472
+ self._configure_pygui(update=True)
1301
1473
 
1302
1474
  self.camera.model_matrix = self.camera.default_model_matrix
1303
1475
 
1304
1476
  self.interact(shell, keyboard_thread)
1305
1477
 
1306
- def interact(self, shell, keyboard_thread):
1478
+ # from IPython.terminal.embed import InteractiveShellEmbed
1479
+
1480
+ def interact(self, shell: Any, keyboard_thread: threading.Thread) -> None:
1481
+ assert isinstance(self.renderer, OpenGLRenderer)
1307
1482
  event_handler = RerunSceneHandler(self.queue)
1308
1483
  file_observer = Observer()
1309
1484
  file_observer.schedule(event_handler, config["input_file"], recursive=True)
@@ -1314,36 +1489,38 @@ class Scene:
1314
1489
  assert self.queue.qsize() == 0
1315
1490
 
1316
1491
  last_time = time.time()
1317
- while not (self.renderer.window.is_closing or self.quit_interaction):
1492
+ while not (
1493
+ (self.renderer.window is not None and self.renderer.window.is_closing)
1494
+ or self.quit_interaction
1495
+ ):
1318
1496
  if not self.queue.empty():
1319
- tup = self.queue.get_nowait()
1320
- if tup[0].startswith("rerun"):
1497
+ action = self.queue.get_nowait()
1498
+ if isinstance(action, SceneInteractRerun):
1321
1499
  # Intentionally skip calling join() on the file thread to save time.
1322
- if not tup[0].endswith("keyboard"):
1500
+ if action.sender != "keyboard":
1323
1501
  if shell.pt_app:
1324
1502
  shell.pt_app.app.exit(exception=EOFError)
1325
1503
  file_observer.unschedule_all()
1326
1504
  raise RerunSceneException
1327
1505
  keyboard_thread.join()
1328
1506
 
1329
- kwargs = tup[2]
1330
- if "from_animation_number" in kwargs:
1331
- config["from_animation_number"] = kwargs[
1507
+ if "from_animation_number" in action.kwargs:
1508
+ config["from_animation_number"] = action.kwargs[
1332
1509
  "from_animation_number"
1333
1510
  ]
1334
1511
  # # TODO: This option only makes sense if interactive_embed() is run at the
1335
1512
  # # end of a scene by default.
1336
- # if "upto_animation_number" in kwargs:
1337
- # config["upto_animation_number"] = kwargs[
1513
+ # if "upto_animation_number" in action.kwargs:
1514
+ # config["upto_animation_number"] = action.kwargs[
1338
1515
  # "upto_animation_number"
1339
1516
  # ]
1340
1517
 
1341
1518
  keyboard_thread.join()
1342
1519
  file_observer.unschedule_all()
1343
1520
  raise RerunSceneException
1344
- elif tup[0].startswith("exit"):
1521
+ elif isinstance(action, SceneInteractContinue):
1345
1522
  # Intentionally skip calling join() on the file thread to save time.
1346
- if not tup[0].endswith("keyboard") and shell.pt_app:
1523
+ if action.sender != "keyboard" and shell.pt_app:
1347
1524
  shell.pt_app.app.exit(exception=EOFError)
1348
1525
  keyboard_thread.join()
1349
1526
  # Remove exit_keyboard from the queue if necessary.
@@ -1352,8 +1529,7 @@ class Scene:
1352
1529
  keyboard_thread_needs_join = False
1353
1530
  break
1354
1531
  else:
1355
- method, args, kwargs = tup
1356
- getattr(self, method)(*args, **kwargs)
1532
+ action.method(*action.args, **action.kwargs)
1357
1533
  else:
1358
1534
  self.renderer.animation_start_time = 0
1359
1535
  dt = time.time() - last_time
@@ -1377,10 +1553,11 @@ class Scene:
1377
1553
  if self.dearpygui_imported and config["enable_gui"]:
1378
1554
  dpg.stop_dearpygui()
1379
1555
 
1380
- if self.renderer.window.is_closing:
1556
+ if self.renderer.window is not None and self.renderer.window.is_closing:
1381
1557
  self.renderer.window.destroy()
1382
1558
 
1383
- def embed(self):
1559
+ def embed(self) -> None:
1560
+ assert isinstance(self.renderer, OpenGLRenderer)
1384
1561
  if not config["preview"]:
1385
1562
  logger.warning("Called embed() while no preview window is available.")
1386
1563
  return
@@ -1404,7 +1581,9 @@ class Scene:
1404
1581
 
1405
1582
  # Use the locals of the caller as the local namespace
1406
1583
  # once embedded, and add a few custom shortcuts.
1407
- local_ns = inspect.currentframe().f_back.f_locals
1584
+ current_frame = inspect.currentframe()
1585
+ assert isinstance(current_frame, FrameType)
1586
+ local_ns = current_frame.f_back.f_locals # type: ignore[union-attr]
1408
1587
  # local_ns["touch"] = self.interact
1409
1588
  for method in (
1410
1589
  "play",
@@ -1422,9 +1601,77 @@ class Scene:
1422
1601
  # End scene when exiting an embed.
1423
1602
  raise Exception("Exiting scene.")
1424
1603
 
1425
- def update_to_time(self, t):
1604
+ def _configure_pygui(self, update: bool = True) -> None:
1605
+ if not self.dearpygui_imported:
1606
+ raise RuntimeError("Attempted to use DearPyGUI when it isn't imported.")
1607
+ if update:
1608
+ dpg.delete_item(window)
1609
+ else:
1610
+ dpg.create_viewport()
1611
+ dpg.setup_dearpygui()
1612
+ dpg.show_viewport()
1613
+
1614
+ dpg.set_viewport_title(title=f"Manim Community v{__version__}")
1615
+ dpg.set_viewport_width(1015)
1616
+ dpg.set_viewport_height(540)
1617
+
1618
+ def rerun_callback(sender: Any, data: Any) -> None:
1619
+ self.queue.put(SceneInteractRerun("gui"))
1620
+
1621
+ def continue_callback(sender: Any, data: Any) -> None:
1622
+ self.queue.put(SceneInteractContinue("gui"))
1623
+
1624
+ def scene_selection_callback(sender: Any, data: Any) -> None:
1625
+ config["scene_names"] = (dpg.get_value(sender),)
1626
+ self.queue.put(SceneInteractRerun("gui"))
1627
+
1628
+ scene_classes = scene_classes_from_file(
1629
+ Path(config["input_file"]), full_list=True
1630
+ ) # type: ignore[call-overload]
1631
+ scene_names = [scene_class.__name__ for scene_class in scene_classes]
1632
+
1633
+ with dpg.window(
1634
+ id=window,
1635
+ label="Manim GUI",
1636
+ pos=[config["gui_location"][0], config["gui_location"][1]],
1637
+ width=1000,
1638
+ height=500,
1639
+ ):
1640
+ dpg.set_global_font_scale(2)
1641
+ dpg.add_button(label="Rerun", callback=rerun_callback)
1642
+ dpg.add_button(label="Continue", callback=continue_callback)
1643
+ dpg.add_combo(
1644
+ label="Selected scene",
1645
+ items=scene_names,
1646
+ callback=scene_selection_callback,
1647
+ default_value=config["scene_names"][0],
1648
+ )
1649
+ dpg.add_separator()
1650
+ if len(self.widgets) != 0:
1651
+ with dpg.collapsing_header(
1652
+ label=f"{config['scene_names'][0]} widgets",
1653
+ default_open=True,
1654
+ ):
1655
+ for widget_config in self.widgets:
1656
+ widget_config_copy = widget_config.copy()
1657
+ name = widget_config_copy["name"]
1658
+ widget = widget_config_copy["widget"]
1659
+ if widget != "separator":
1660
+ del widget_config_copy["name"]
1661
+ del widget_config_copy["widget"]
1662
+ getattr(dpg, f"add_{widget}")(
1663
+ label=name, **widget_config_copy
1664
+ )
1665
+ else:
1666
+ dpg.add_separator()
1667
+
1668
+ if not update:
1669
+ dpg.start_dearpygui()
1670
+
1671
+ def update_to_time(self, t: float) -> None:
1426
1672
  dt = t - self.last_t
1427
1673
  self.last_t = t
1674
+ assert self.animations is not None
1428
1675
  for animation in self.animations:
1429
1676
  animation.update_mobjects(dt)
1430
1677
  alpha = t / animation.run_time
@@ -1439,7 +1686,7 @@ class Scene:
1439
1686
  r"""Adds an entry in the corresponding subcaption file
1440
1687
  at the current time stamp.
1441
1688
 
1442
- The current time stamp is obtained from ``Scene.renderer.time``.
1689
+ The current time stamp is obtained from ``Scene.time``.
1443
1690
 
1444
1691
  Parameters
1445
1692
  ----------
@@ -1469,16 +1716,15 @@ class Scene:
1469
1716
 
1470
1717
  # second option: within the call to Scene.play
1471
1718
  self.play(
1472
- Transform(square, circle),
1473
- subcaption="The square transforms."
1719
+ Transform(square, circle), subcaption="The square transforms."
1474
1720
  )
1475
1721
 
1476
1722
  """
1477
1723
  subtitle = srt.Subtitle(
1478
1724
  index=len(self.renderer.file_writer.subcaptions),
1479
1725
  content=content,
1480
- start=datetime.timedelta(seconds=self.renderer.time + offset),
1481
- end=datetime.timedelta(seconds=self.renderer.time + offset + duration),
1726
+ start=datetime.timedelta(seconds=float(self.time + offset)),
1727
+ end=datetime.timedelta(seconds=float(self.time + offset + duration)),
1482
1728
  )
1483
1729
  self.renderer.file_writer.subcaptions.append(subtitle)
1484
1730
 
@@ -1487,8 +1733,8 @@ class Scene:
1487
1733
  sound_file: str,
1488
1734
  time_offset: float = 0,
1489
1735
  gain: float | None = None,
1490
- **kwargs,
1491
- ):
1736
+ **kwargs: Any,
1737
+ ) -> None:
1492
1738
  """
1493
1739
  This method is used to add a sound to the animation.
1494
1740
 
@@ -1526,10 +1772,12 @@ class Scene:
1526
1772
  """
1527
1773
  if self.renderer.skip_animations:
1528
1774
  return
1529
- time = self.renderer.time + time_offset
1775
+ time = self.time + time_offset
1530
1776
  self.renderer.file_writer.add_sound(sound_file, time, gain, **kwargs)
1531
1777
 
1532
- def on_mouse_motion(self, point, d_point):
1778
+ def on_mouse_motion(self, point: Point3D, d_point: Point3D) -> None:
1779
+ assert isinstance(self.camera, OpenGLCamera)
1780
+ assert isinstance(self.renderer, OpenGLRenderer)
1533
1781
  self.mouse_point.move_to(point)
1534
1782
  if SHIFT_VALUE in self.renderer.pressed_keys:
1535
1783
  shift = -d_point
@@ -1539,13 +1787,15 @@ class Scene:
1539
1787
  shift = np.dot(np.transpose(transform), shift)
1540
1788
  self.camera.shift(shift)
1541
1789
 
1542
- def on_mouse_scroll(self, point, offset):
1790
+ def on_mouse_scroll(self, point: Point3D, offset: Point3D) -> None:
1791
+ assert isinstance(self.camera, OpenGLCamera)
1543
1792
  if not config.use_projection_stroke_shaders:
1544
1793
  factor = 1 + np.arctan(-2.1 * offset[1])
1545
1794
  self.camera.scale(factor, about_point=self.camera_target)
1546
1795
  self.mouse_scroll_orbit_controls(point, offset)
1547
1796
 
1548
- def on_key_press(self, symbol, modifiers):
1797
+ def on_key_press(self, symbol: int, modifiers: int) -> None:
1798
+ assert isinstance(self.camera, OpenGLCamera)
1549
1799
  try:
1550
1800
  char = chr(symbol)
1551
1801
  except OverflowError:
@@ -1561,10 +1811,17 @@ class Scene:
1561
1811
  if char in self.key_to_function_map:
1562
1812
  self.key_to_function_map[char]()
1563
1813
 
1564
- def on_key_release(self, symbol, modifiers):
1814
+ def on_key_release(self, symbol: int, modifiers: int) -> None:
1565
1815
  pass
1566
1816
 
1567
- def on_mouse_drag(self, point, d_point, buttons, modifiers):
1817
+ def on_mouse_drag(
1818
+ self,
1819
+ point: Point3D,
1820
+ d_point: Point3D,
1821
+ buttons: int,
1822
+ modifiers: int,
1823
+ ) -> None:
1824
+ assert isinstance(self.camera, OpenGLCamera)
1568
1825
  self.mouse_drag_point.move_to(point)
1569
1826
  if buttons == 1:
1570
1827
  self.camera.increment_theta(-d_point[0])
@@ -1578,7 +1835,8 @@ class Scene:
1578
1835
 
1579
1836
  self.mouse_drag_orbit_controls(point, d_point, buttons, modifiers)
1580
1837
 
1581
- def mouse_scroll_orbit_controls(self, point, offset):
1838
+ def mouse_scroll_orbit_controls(self, point: Point3D, offset: Point3D) -> None:
1839
+ assert isinstance(self.camera, OpenGLCamera)
1582
1840
  camera_to_target = self.camera_target - self.camera.get_position()
1583
1841
  camera_to_target *= np.sign(offset[1])
1584
1842
  shift_vector = 0.01 * camera_to_target
@@ -1586,7 +1844,14 @@ class Scene:
1586
1844
  opengl.translation_matrix(*shift_vector) @ self.camera.model_matrix
1587
1845
  )
1588
1846
 
1589
- def mouse_drag_orbit_controls(self, point, d_point, buttons, modifiers):
1847
+ def mouse_drag_orbit_controls(
1848
+ self,
1849
+ point: Point3D,
1850
+ d_point: Point3D,
1851
+ buttons: int,
1852
+ modifiers: int,
1853
+ ) -> None:
1854
+ assert isinstance(self.camera, OpenGLCamera)
1590
1855
  # Left click drag.
1591
1856
  if buttons == 1:
1592
1857
  # Translate to target the origin and rotate around the z axis.
@@ -1659,9 +1924,9 @@ class Scene:
1659
1924
  )
1660
1925
  self.camera_target += total_shift_vector
1661
1926
 
1662
- def set_key_function(self, char, func):
1927
+ def set_key_function(self, char: str, func: Callable[[], Any]) -> None:
1663
1928
  self.key_to_function_map[char] = func
1664
1929
 
1665
- def on_mouse_press(self, point, button, modifiers):
1930
+ def on_mouse_press(self, point: Point3D, button: str, modifiers: int) -> None:
1666
1931
  for func in self.mouse_press_callbacks:
1667
1932
  func()