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.
- manim/__init__.py +11 -6
- manim/__main__.py +62 -19
- manim/_config/__init__.py +10 -9
- manim/_config/cli_colors.py +26 -9
- manim/_config/default.cfg +1 -3
- manim/_config/logger_utils.py +23 -13
- manim/_config/utils.py +662 -468
- manim/animation/animation.py +164 -18
- manim/animation/changing.py +34 -23
- manim/animation/composition.py +265 -67
- manim/animation/creation.py +208 -26
- manim/animation/fading.py +16 -18
- manim/animation/growing.py +35 -15
- manim/animation/indication.py +150 -76
- manim/animation/movement.py +56 -22
- manim/animation/numbers.py +64 -6
- manim/animation/rotation.py +78 -7
- manim/animation/specialized.py +6 -7
- manim/animation/speedmodifier.py +13 -10
- manim/animation/transform.py +14 -11
- manim/animation/transform_matching_parts.py +3 -4
- manim/animation/updaters/mobject_update_utils.py +152 -30
- manim/animation/updaters/update.py +10 -7
- manim/camera/camera.py +182 -118
- manim/camera/mapping_camera.py +34 -3
- manim/camera/moving_camera.py +95 -74
- manim/camera/multi_camera.py +23 -15
- manim/camera/three_d_camera.py +70 -52
- manim/cli/__init__.py +17 -0
- manim/cli/cfg/group.py +76 -44
- manim/cli/checkhealth/checks.py +192 -0
- manim/cli/checkhealth/commands.py +90 -0
- manim/cli/default_group.py +158 -25
- manim/cli/init/commands.py +33 -25
- manim/cli/plugins/commands.py +16 -3
- manim/cli/render/commands.py +72 -60
- manim/cli/render/ease_of_access_options.py +4 -3
- manim/cli/render/global_options.py +59 -17
- manim/cli/render/output_options.py +6 -5
- manim/cli/render/render_options.py +98 -33
- manim/constants.py +109 -59
- manim/data_structures.py +31 -0
- manim/mobject/frame.py +8 -5
- manim/mobject/geometry/__init__.py +1 -0
- manim/mobject/geometry/arc.py +277 -135
- manim/mobject/geometry/boolean_ops.py +32 -31
- manim/mobject/geometry/labeled.py +376 -0
- manim/mobject/geometry/line.py +192 -87
- manim/mobject/geometry/polygram.py +224 -58
- manim/mobject/geometry/shape_matchers.py +61 -25
- manim/mobject/geometry/tips.py +122 -48
- manim/mobject/graph.py +1027 -419
- manim/mobject/graphing/coordinate_systems.py +533 -278
- manim/mobject/graphing/functions.py +53 -32
- manim/mobject/graphing/number_line.py +123 -65
- manim/mobject/graphing/probability.py +88 -62
- manim/mobject/graphing/scale.py +33 -19
- manim/mobject/logo.py +118 -28
- manim/mobject/matrix.py +87 -83
- manim/mobject/mobject.py +912 -442
- manim/mobject/opengl/dot_cloud.py +16 -5
- manim/mobject/opengl/opengl_compatibility.py +4 -2
- manim/mobject/opengl/opengl_geometry.py +254 -153
- manim/mobject/opengl/opengl_image_mobject.py +3 -1
- manim/mobject/opengl/opengl_mobject.py +779 -482
- manim/mobject/opengl/opengl_point_cloud_mobject.py +41 -14
- manim/mobject/opengl/opengl_surface.py +14 -92
- manim/mobject/opengl/opengl_three_dimensions.py +12 -8
- manim/mobject/opengl/opengl_vectorized_mobject.py +98 -100
- manim/mobject/svg/brace.py +173 -41
- manim/mobject/svg/svg_mobject.py +139 -53
- manim/mobject/table.py +61 -68
- manim/mobject/text/code_mobject.py +193 -539
- manim/mobject/text/numbers.py +81 -34
- manim/mobject/text/tex_mobject.py +130 -78
- manim/mobject/text/text_mobject.py +288 -164
- manim/mobject/three_d/polyhedra.py +111 -13
- manim/mobject/three_d/three_d_utils.py +17 -8
- manim/mobject/three_d/three_dimensions.py +239 -106
- manim/mobject/types/image_mobject.py +50 -30
- manim/mobject/types/point_cloud_mobject.py +120 -75
- manim/mobject/types/vectorized_mobject.py +841 -408
- manim/mobject/value_tracker.py +105 -38
- manim/mobject/vector_field.py +50 -31
- manim/opengl/__init__.py +3 -3
- manim/plugins/__init__.py +14 -1
- manim/plugins/plugins_flags.py +10 -14
- manim/renderer/cairo_renderer.py +65 -50
- manim/renderer/opengl_renderer.py +89 -69
- manim/renderer/opengl_renderer_window.py +39 -18
- manim/renderer/shader.py +123 -87
- manim/renderer/shader_wrapper.py +44 -28
- manim/renderer/vectorized_mobject_rendering.py +38 -10
- manim/scene/moving_camera_scene.py +32 -3
- manim/scene/scene.py +507 -242
- manim/scene/scene_file_writer.py +371 -220
- manim/scene/section.py +20 -16
- manim/scene/three_d_scene.py +14 -22
- manim/scene/vector_space_scene.py +223 -129
- manim/scene/zoomed_scene.py +46 -41
- manim/typing.py +990 -0
- manim/utils/bezier.py +1823 -371
- manim/utils/caching.py +12 -5
- manim/utils/color/AS2700.py +236 -0
- manim/utils/color/BS381.py +318 -0
- manim/utils/color/DVIPSNAMES.py +96 -0
- manim/utils/color/SVGNAMES.py +179 -0
- manim/utils/color/X11.py +533 -0
- manim/utils/color/XKCD.py +952 -0
- manim/utils/color/__init__.py +61 -0
- manim/utils/color/core.py +1667 -0
- manim/utils/color/manim_colors.py +218 -0
- manim/utils/commands.py +48 -20
- manim/utils/config_ops.py +39 -19
- manim/utils/debug.py +8 -7
- manim/utils/deprecation.py +86 -39
- manim/utils/docbuild/__init__.py +17 -0
- manim/utils/docbuild/autoaliasattr_directive.py +236 -0
- manim/utils/docbuild/autocolor_directive.py +99 -0
- manim/utils/docbuild/manim_directive.py +94 -41
- manim/utils/docbuild/module_parsing.py +245 -0
- manim/utils/exceptions.py +6 -0
- manim/utils/family.py +5 -3
- manim/utils/family_ops.py +17 -4
- manim/utils/file_ops.py +27 -17
- manim/utils/hashing.py +55 -45
- manim/utils/images.py +13 -7
- manim/utils/ipython_magic.py +13 -7
- manim/utils/iterables.py +163 -120
- manim/utils/module_ops.py +66 -24
- manim/utils/opengl.py +77 -24
- manim/utils/parameter_parsing.py +32 -0
- manim/utils/paths.py +30 -33
- manim/utils/polylabel.py +235 -0
- manim/utils/qhull.py +218 -0
- manim/utils/rate_functions.py +98 -32
- manim/utils/simple_functions.py +25 -33
- manim/utils/sounds.py +7 -1
- manim/utils/space_ops.py +188 -115
- manim/utils/testing/__init__.py +17 -0
- manim/utils/testing/_frames_testers.py +13 -8
- manim/utils/testing/_show_diff.py +5 -3
- manim/utils/testing/_test_class_makers.py +34 -18
- manim/utils/testing/frames_comparison.py +37 -19
- manim/utils/tex.py +130 -198
- manim/utils/tex_file_writing.py +77 -47
- manim/utils/tex_templates.py +2 -1
- manim/utils/unit.py +6 -5
- {manim-0.17.0.dist-info → manim-0.19.1.dist-info}/METADATA +64 -65
- manim-0.19.1.dist-info/RECORD +220 -0
- {manim-0.17.0.dist-info → manim-0.19.1.dist-info}/WHEEL +1 -1
- manim-0.19.1.dist-info/entry_points.txt +3 -0
- {manim-0.17.0.dist-info → manim-0.19.1.dist-info/licenses}/LICENSE.community +1 -1
- manim/cli/new/group.py +0 -189
- manim/communitycolors.py +0 -9
- manim/gui/__init__.py +0 -0
- manim/gui/gui.py +0 -82
- manim/plugins/import_plugins.py +0 -43
- manim/utils/color.py +0 -552
- manim-0.17.0.dist-info/RECORD +0 -206
- manim-0.17.0.dist-info/entry_points.txt +0 -4
- /manim/cli/{new → checkhealth}/__init__.py +0 -0
- {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
|
|
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(("
|
|
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 =
|
|
113
|
-
self.last_t =
|
|
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.
|
|
122
|
-
self.
|
|
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
|
-
|
|
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
|
|
263
|
+
except RerunSceneException:
|
|
227
264
|
self.remove(*self.mobjects)
|
|
228
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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,
|
|
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
|
|
342
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
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=
|
|
518
|
+
to_remove=new_and_foreground_mobjects,
|
|
473
519
|
mobject_list_name="moving_mobjects",
|
|
474
520
|
)
|
|
475
|
-
self.moving_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(
|
|
565
|
+
filter(lambda_function, self.meshes),
|
|
515
566
|
)
|
|
516
567
|
return self
|
|
517
|
-
|
|
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(
|
|
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
|
-
|
|
632
|
-
|
|
633
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
977
|
-
|
|
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
|
-
#
|
|
1015
|
-
if
|
|
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.
|
|
1177
|
+
start_time = self.time
|
|
1033
1178
|
self.renderer.play(self, *args, **kwargs)
|
|
1034
|
-
run_time = self.
|
|
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.
|
|
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
|
|
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(
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(("
|
|
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(("
|
|
1424
|
+
self.queue.put(SceneInteractContinue("keyboard"))
|
|
1265
1425
|
|
|
1266
|
-
def get_embedded_method(method_name):
|
|
1267
|
-
|
|
1426
|
+
def get_embedded_method(method_name: str) -> Callable[..., None]:
|
|
1427
|
+
method = getattr(self, method_name)
|
|
1268
1428
|
|
|
1269
|
-
|
|
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
|
|
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
|
-
|
|
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=
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
1320
|
-
if
|
|
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
|
|
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
|
-
|
|
1330
|
-
|
|
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
|
|
1521
|
+
elif isinstance(action, SceneInteractContinue):
|
|
1345
1522
|
# Intentionally skip calling join() on the file thread to save time.
|
|
1346
|
-
if
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
1481
|
-
end=datetime.timedelta(seconds=self.
|
|
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.
|
|
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(
|
|
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(
|
|
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()
|