Add type annotations to scene.py and vector_space_scene.py (#4260)

* Stop ignoring errors from manim/scene.py

Count: 307 errors

Type annotations on scene/vector_space_scene.py

Add type annotations to scene/vector_space_scene.py

Mypy count: 210

Reverting two changes that triggers an error in the automatic testing.

Further work on type hinting.

Avoid forwarding positional arguments from Arrow to Line in the constructor.

Revert "Avoid forwarding positional arguments from Arrow to Line in the constructor."

This reverts commit 80ae8576c1.

Removed several type ignore statements and addressed comments from JasonGrace2282

Revert "Activate mypy check of mobject.geometry.*"

This reverts commit d477c9a994.

Revert "Removed several type ignore statements and addressed comments from JasonGrace2282"

This reverts commit 07bbe3f220.

Added type annotations to zoomed_scene.py

Error count: 308 -> 303

Adding type annotations to all methods in vector_space_scene.py

Error count: 303 -> 272

Get rid of no-untyped-call errors from my in the vector_space_scene.py file

Error count: 272 -> 343

Handle type issues related to ManimColor in vector_space_scene.py

Handle var-annotated issues in vector_space_scene.py

Error count: 332 -> 330

Handling has-type type errors in vector_space_scene.py

Error count: 330 -> 285

Handled name-defined type issues in vector_space_scene.py

Error count: 285 -> 282

Address type issue with calling an untyped method.

Error count: 282 -> 281

Fix some typing issues in transform_mathcing_parts.py

Change stroke_width to float in vector_space_scene.py

Handled a few type errors.

Error count: 267

Handled several typing issues in three_d_scene.py

Error count: 267 -> 248

Dealing with type errors in scene_file_writer.py

Error count: 248 -> 216

Ensured that all methods in scene.py have type declarations.

Error count: 216 -> 225

Handle type issues related to interactivity by asserting that the camera is the OpenGLCamera

Error count: 225 -> 182

Handle type issues in scene.py

Error count: 182 -> 167

Asserting that the renderer or camera is of the proper type to use certain methods.

This is mainly related to interactive elements and the 3D camera used in the ThreeDScene

Error count: 167 -> 143

Avoid cyclic import of dependencies

Error count: 143 -> 143

Handling no-untyped-call type errors in manim/scene/scene.py

Error count: 143 -> 131

Handling assignment type errors in manim/scene/*.py

Error count: 131 -> 121

Handling arg-type type errors in manim/scene/*.py

Error count: 121 -> 116

Handling arg-type type errors in manim/scene/*.py

Error count: 116 -> 112

Fixing various type errors

Error count: 112 -> 102

Fixing various type errors

Error count: 102 -> 97

Fixing various type errors

Error count: 97 -> 90

Some aggressive changes to silence a significant number of type errors.

Error count: 90 -> 66

Commented out an import (IPython) that makes the CI tests fail.

Fix various type errors.

More type annotations.

Code cleanup.

Remove the property mobject_updater_lists of the Scene class as it is not used anywhere.

Handle import-untyped typing issues.

More work on type annotations in manin/scene/.*

More work on scenes/*.py looking at the dependency opengl_renderer.py

More work on types in scenes/*.py

Ignored an old bunch of type ignore statements.

More work on dependencies for scene.py

More work on dependencies for scene.py

* Stop ignoring errors from manim/scene/scene.py

mypy error count: 307

* Adding type annotations to scene.py (C1)

* Adding type annotations to scene.py (C2.1)

* Adding type annotations to scene.py (C2.2)

* Adding type annotations to scene.py (C2.3)

* Adding type annotations to scene.py (C3)

* Adding type annotations to scene.py (C4)

* Adding type annotations to scene.py (C5)

* Adding type annotations to scene.py (C6)

* Adding type annotations to scene.py (C7)

* ...

* Focus on scene.py

* Adding type annotations to vector_space_scene.py

* Added types to opengl_renderer.py

* Added types to cairo_renderer.py

* Fixed the last mypy errors in scene.py - many was ignored

* Fixed the last mypy errors in vector_space_scene.py - many was ignored

* Got rid of the last mypy errors.

* Activate mypy check of vector_space_scene.py

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Code cleanup.

* Update manim/gui/gui.py

Co-authored-by: Francisco Manríquez Novoa <49853152+chopan050@users.noreply.github.com>

* Update manim/renderer/opengl_renderer.py

Co-authored-by: Francisco Manríquez Novoa <49853152+chopan050@users.noreply.github.com>

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Update manim/scene/scene.py

Co-authored-by: Francisco Manríquez Novoa <49853152+chopan050@users.noreply.github.com>

* Update manim/scene/scene.py

Co-authored-by: Francisco Manríquez Novoa <49853152+chopan050@users.noreply.github.com>

* Update manim/scene/scene.py

Co-authored-by: Francisco Manríquez Novoa <49853152+chopan050@users.noreply.github.com>

* Update manim/scene/scene.py

Co-authored-by: Francisco Manríquez Novoa <49853152+chopan050@users.noreply.github.com>

* Update manim/scene/scene.py

Co-authored-by: Francisco Manríquez Novoa <49853152+chopan050@users.noreply.github.com>

* Implementing suggestions from chopan50

* Explicitly mention all files where type errors are ignored in mypy.ini

# Conflicts:
#	mypy.ini

* Fix issue

* Updates based on comments from chopan50

* Updates suggested by Chopan50

* Addressed more comments from chopan50

* Addressing more comments from chopan50

* Added docstring to SceneInteractAction

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Differentiate between _AnimationBuilder from an mobject and an opengl_mobject

* Avoid a nameclash with the mobject module and variable name

* Last touches.

* Rolled back some changes related to _AnimationBuilder

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Francisco Manríquez Novoa <49853152+chopan050@users.noreply.github.com>
This commit is contained in:
Henrik Skov Midtiby 2025-06-24 19:50:40 +02:00 committed by GitHub
commit ea16d22735
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 453 additions and 270 deletions

View file

@ -540,7 +540,7 @@ class Animation:
def prepare_animation(
anim: Animation | mobject._AnimationBuilder,
anim: Animation | mobject._AnimationBuilder | opengl_mobject._AnimationBuilder,
) -> Animation:
r"""Returns either an unchanged animation, or the animation built
from a passed animation factory.

View file

@ -10,9 +10,16 @@ except ImportError:
dearpygui_imported = False
from collections.abc import Sequence
from typing import TYPE_CHECKING, Any
from .. import __version__, config
from ..utils.module_ops import scene_classes_from_file
if TYPE_CHECKING:
from ..renderer.opengl_renderer import OpenGLRenderer
__all__ = ["configure_pygui"]
if dearpygui_imported:
@ -20,7 +27,9 @@ if dearpygui_imported:
window = dpg.generate_uuid()
def configure_pygui(renderer, widgets, update=True):
def configure_pygui(
renderer: OpenGLRenderer, widgets: Sequence[dict[str, Any]], update: bool = True
) -> None:
if not dearpygui_imported:
raise RuntimeError("Attempted to use DearPyGUI when it isn't imported.")
if update:

View file

@ -400,7 +400,7 @@ class Matrix(VMobject, metaclass=ConvertToOpenGL):
mob.add_background_rectangle()
return self
def get_mob_matrix(self):
def get_mob_matrix(self) -> list[list[MathTex]]:
"""Return the underlying mob matrix mobjects.
Returns
@ -435,13 +435,13 @@ class Matrix(VMobject, metaclass=ConvertToOpenGL):
"""
return self.elements
def get_brackets(self):
def get_brackets(self) -> VGroup:
r"""Return the bracket mobjects.
Returns
--------
List[:class:`~.VGroup`]
Each VGroup contains a bracket
:class:`~.VGroup`
A VGroup containing the left and right bracket.
Examples
--------

View file

@ -29,6 +29,7 @@ import re
from collections.abc import Iterable
from functools import reduce
from textwrap import dedent
from typing import Any
from manim import config, logger
from manim.constants import *
@ -447,7 +448,11 @@ class Tex(MathTex):
"""
def __init__(
self, *tex_strings, arg_separator="", tex_environment="center", **kwargs
self,
*tex_strings: str,
arg_separator: str = "",
tex_environment: str = "center",
**kwargs: Any,
):
super().__init__(
*tex_strings,

View file

@ -33,11 +33,11 @@ class CairoRenderer:
def __init__(
self,
file_writer_class=SceneFileWriter,
camera_class=None,
skip_animations=False,
**kwargs,
):
file_writer_class: type[SceneFileWriter] = SceneFileWriter,
camera_class: type[Camera] | None = None,
skip_animations: bool = False,
**kwargs: Any,
) -> None:
# All of the following are set to EITHER the value passed via kwargs,
# OR the value stored in the global config dict at the time of
# _instance construction_.
@ -51,7 +51,7 @@ class CairoRenderer:
self.time = 0
self.static_image = None
def init_scene(self, scene):
def init_scene(self, scene: Scene) -> None:
self.file_writer: Any = self._file_writer_class(
self,
scene.__class__.__name__,
@ -119,12 +119,12 @@ class CairoRenderer:
def update_frame( # TODO Description in Docstring
self,
scene,
scene: Scene,
mobjects: typing.Iterable[Mobject] | None = None,
include_submobjects: bool = True,
ignore_skipping: bool = True,
**kwargs,
):
**kwargs: Any,
) -> None:
"""Update the frame.
Parameters
@ -263,7 +263,7 @@ class CairoRenderer:
self.skip_animations = True
raise EndSceneEarlyException()
def scene_finished(self, scene):
def scene_finished(self, scene: Scene) -> None:
# If no animations in scene, render an image instead
if self.num_plays:
self.file_writer.finish()

View file

@ -4,14 +4,18 @@ import contextlib
import itertools as it
import time
from functools import cached_property
from typing import Any
from typing import TYPE_CHECKING, Any
import moderngl
import numpy as np
from PIL import Image
from manim import config, logger
from manim.mobject.opengl.opengl_mobject import OpenGLMobject, OpenGLPoint
from manim.mobject.opengl.opengl_mobject import (
OpenGLMobject,
OpenGLPoint,
_AnimationBuilder,
)
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject
from manim.utils.caching import handle_caching_play
from manim.utils.color import color_to_rgba
@ -35,6 +39,14 @@ from .vectorized_mobject_rendering import (
render_opengl_vectorized_mobject_stroke,
)
if TYPE_CHECKING:
from typing_extensions import Self
from manim.animation.animation import Animation
from manim.mobject.mobject import Mobject
from manim.scene.scene import Scene
__all__ = ["OpenGLCamera", "OpenGLRenderer"]
@ -102,7 +114,7 @@ class OpenGLCamera(OpenGLMobject):
self.euler_angles = euler_angles
self.refresh_rotation_matrix()
def get_position(self):
def get_position(self) -> Point3D:
return self.model_matrix[:, 3][:3]
def set_position(self, position):
@ -123,7 +135,7 @@ class OpenGLCamera(OpenGLMobject):
self.set_height(self.frame_shape[1], stretch=True)
self.move_to(self.center_point)
def to_default_state(self):
def to_default_state(self) -> Self:
self.center()
self.set_height(config["frame_height"])
self.set_width(config["frame_width"])
@ -166,28 +178,28 @@ class OpenGLCamera(OpenGLMobject):
self.refresh_rotation_matrix()
return self
def set_theta(self, theta):
def set_theta(self, theta: float) -> Self:
return self.set_euler_angles(theta=theta)
def set_phi(self, phi):
def set_phi(self, phi: float) -> Self:
return self.set_euler_angles(phi=phi)
def set_gamma(self, gamma):
def set_gamma(self, gamma: float) -> Self:
return self.set_euler_angles(gamma=gamma)
def increment_theta(self, dtheta):
def increment_theta(self, dtheta: float) -> Self:
self.euler_angles[0] += dtheta
self.refresh_rotation_matrix()
return self
def increment_phi(self, dphi):
def increment_phi(self, dphi: float) -> Self:
phi = self.euler_angles[1]
new_phi = clip(phi + dphi, -PI / 2, PI / 2)
self.euler_angles[1] = new_phi
self.refresh_rotation_matrix()
return self
def increment_gamma(self, dgamma):
def increment_gamma(self, dgamma: float) -> Self:
self.euler_angles[2] += dgamma
self.refresh_rotation_matrix()
return self
@ -199,15 +211,15 @@ class OpenGLCamera(OpenGLMobject):
# Assumes first point is at the center
return self.points[0]
def get_width(self):
def get_width(self) -> float:
points = self.points
return points[2, 0] - points[1, 0]
def get_height(self):
def get_height(self) -> float:
points = self.points
return points[4, 1] - points[3, 1]
def get_focal_distance(self):
def get_focal_distance(self) -> float:
return self.focal_distance * self.get_height()
def interpolate(self, *args, **kwargs):
@ -412,7 +424,12 @@ class OpenGLRenderer:
raise EndSceneEarlyException()
@handle_caching_play
def play(self, scene, *args, **kwargs):
def play(
self,
scene: Scene,
*args: Animation | Mobject | _AnimationBuilder,
**kwargs: Any,
) -> None:
# TODO: Handle data locking / unlocking.
self.animation_start_time = time.time()
self.file_writer.begin_animation(not self.skip_animations)
@ -440,11 +457,13 @@ class OpenGLRenderer:
self.time += scene.duration
self.num_plays += 1
def clear_screen(self):
def clear_screen(self) -> None:
self.frame_buffer_object.clear(*self.background_color)
self.window.swap_buffers()
def render(self, scene, frame_offset, moving_mobjects):
def render(
self, scene: Scene, frame_offset, moving_mobjects: list[Mobject]
) -> None:
self.update_frame(scene)
if self.skip_animations:

View file

@ -4,6 +4,8 @@ from __future__ import annotations
from manim.utils.parameter_parsing import flatten_iterable_parameters
from ..mobject.mobject import _AnimationBuilder
__all__ = ["Scene"]
import copy
@ -29,7 +31,7 @@ from typing import TYPE_CHECKING
import numpy as np
from tqdm import tqdm
from watchdog.events import FileSystemEventHandler
from watchdog.events import DirModifiedEvent, FileModifiedEvent, FileSystemEventHandler
from watchdog.observers import Observer
from manim.mobject.mobject import Mobject
@ -41,7 +43,7 @@ from ..camera.camera import Camera
from ..constants import *
from ..gui.gui import configure_pygui
from ..renderer.cairo_renderer import CairoRenderer
from ..renderer.opengl_renderer import OpenGLRenderer
from ..renderer.opengl_renderer import OpenGLCamera, OpenGLMobject, OpenGLRenderer
from ..renderer.shader import Object3D
from ..utils import opengl, space_ops
from ..utils.exceptions import EndSceneEarlyException, RerunSceneException
@ -51,20 +53,39 @@ from ..utils.file_ops import open_media_file
from ..utils.iterables import list_difference_update, list_update
if TYPE_CHECKING:
from collections.abc import Sequence
from typing import Callable
from collections.abc import Iterable, Sequence
from types import FrameType
from typing import Any, Callable, TypeAlias
from manim.mobject.mobject import _AnimationBuilder
from typing_extensions import Self
from manim.typing import Point3D
SceneInteractAction: TypeAlias = tuple[str, Iterable[Any], dict[str, Any]]
"""
The SceneInteractAction type alias is used for elements in the queue
used by Scene.interact().
The elements consist consist of:
- a string, which is either the name of a Scene method or some special keyword
starting with "rerun" or "exit",
- a list of args for the Scene method (only used if the first string actually
corresponds to a method) and
- a dict of kwargs for the Scene method (if the first string corresponds to one.
Otherwise, currently Scene.interact() extracts a possible "from_animation_number" from it if the first string starts with "rerun"),
as seen around the source code where it's common to use self.queue.put((method_name, [], {})) and similar items.
"""
class RerunSceneHandler(FileSystemEventHandler):
"""A class to handle rerunning a Scene after the input file is modified."""
def __init__(self, queue):
def __init__(self, queue: Queue[SceneInteractAction]) -> None:
super().__init__()
self.queue = queue
def on_modified(self, event):
def on_modified(self, event: DirModifiedEvent | FileModifiedEvent) -> None:
self.queue.put(("rerun_file", [], {}))
@ -112,22 +133,22 @@ class Scene:
self.random_seed = random_seed
self.skip_animations = skip_animations
self.animations = None
self.stop_condition = None
self.moving_mobjects = []
self.static_mobjects = []
self.time_progression = None
self.duration = None
self.last_t = None
self.queue = Queue()
self.animations: list[Animation] | None = None
self.stop_condition: Callable[[], bool] | None = None
self.moving_mobjects: list[Mobject] = []
self.static_mobjects: list[Mobject] = []
self.time_progression: tqdm[float] | None = None
self.duration: float | None = None
self.last_t = 0.0
self.queue: Queue[SceneInteractAction] = Queue()
self.skip_animation_preview = False
self.meshes = []
self.meshes: list[Object3D] = []
self.camera_target = ORIGIN
self.widgets = []
self.widgets: list[Any] = []
self.dearpygui_imported = dearpygui_imported
self.updaters = []
self.key_to_function_map = {}
self.mouse_press_callbacks = []
self.updaters: list[Callable[[float], None]] = []
self.key_to_function_map: dict[str, Callable[[], None]] = {}
self.mouse_press_callbacks: list[Callable[[], None]] = []
self.interactive_mode = False
if config.renderer == RendererType.OPENGL:
@ -138,7 +159,9 @@ class Scene:
renderer = OpenGLRenderer()
if renderer is None:
self.renderer = CairoRenderer(
self.renderer: CairoRenderer | OpenGLRenderer = CairoRenderer(
# TODO: Is it a suitable approach to make an instance of
# the self.camera_class here?
camera_class=self.camera_class,
skip_animations=self.skip_animations,
)
@ -146,15 +169,15 @@ class Scene:
self.renderer = renderer
self.renderer.init_scene(self)
self.mobjects = []
self.mobjects: list[Mobject] = []
# TODO, remove need for foreground mobjects
self.foreground_mobjects = []
self.foreground_mobjects: list[Mobject] = []
if self.random_seed is not None:
random.seed(self.random_seed)
np.random.seed(self.random_seed)
@property
def camera(self):
def camera(self) -> Camera | OpenGLCamera:
return self.renderer.camera
@property
@ -162,7 +185,7 @@ class Scene:
"""The time since the start of the scene."""
return self.renderer.time
def __deepcopy__(self, clone_from_id):
def __deepcopy__(self, clone_from_id: dict[int, Any]) -> Scene:
cls = self.__class__
result = cls.__new__(cls)
clone_from_id[id(self)] = result
@ -175,7 +198,7 @@ class Scene:
return result
def render(self, preview: bool = False):
def render(self, preview: bool = False) -> bool:
"""
Renders this Scene.
@ -191,7 +214,8 @@ class Scene:
pass
except RerunSceneException:
self.remove(*self.mobjects)
self.renderer.clear_screen()
# TODO: The CairoRenderer does not have the method clear_screen()
self.renderer.clear_screen() # type: ignore[union-attr]
self.renderer.num_plays = 0
return True
self.tear_down()
@ -215,7 +239,9 @@ class Scene:
if config["preview"] or config["show_in_file_browser"]:
open_media_file(self.renderer.file_writer)
def setup(self):
return False
def setup(self) -> None:
"""
This is meant to be implemented by any scenes which
are commonly subclassed, and have some common setup
@ -223,7 +249,7 @@ class Scene:
"""
pass
def tear_down(self):
def tear_down(self) -> None:
"""
This is meant to be implemented by any scenes which
are commonly subclassed, and have some common method
@ -231,7 +257,7 @@ class Scene:
"""
pass
def construct(self):
def construct(self) -> None:
"""Add content to the Scene.
From within :meth:`Scene.construct`, display mobjects on screen by calling
@ -276,10 +302,10 @@ class Scene:
"""
self.renderer.file_writer.next_section(name, section_type, skip_animations)
def __str__(self):
def __str__(self) -> str:
return self.__class__.__name__
def get_attrs(self, *keys: str):
def get_attrs(self, *keys: str) -> list[Any]:
"""
Gets attributes of a scene given the attribute's identifier/name.
@ -295,7 +321,7 @@ class Scene:
"""
return [getattr(self, key) for key in keys]
def update_mobjects(self, dt: float):
def update_mobjects(self, dt: float) -> None:
"""
Begins updating all mobjects in the Scene.
@ -304,15 +330,15 @@ class Scene:
dt
Change in time between updates. Defaults (mostly) to 1/frames_per_second
"""
for mobject in self.mobjects:
mobject.update(dt)
for mobj in self.mobjects:
mobj.update(dt)
def update_meshes(self, dt):
def update_meshes(self, dt: float) -> None:
for obj in self.meshes:
for mesh in obj.get_family():
for mesh in obj.get_family(): # type: ignore[no-untyped-call]
mesh.update(dt)
def update_self(self, dt: float):
def update_self(self, dt: float) -> None:
"""Run all scene updater functions.
Among all types of update functions (mobject updaters, mesh updaters,
@ -344,7 +370,9 @@ class Scene:
This is only called when a single Wait animation is played.
"""
assert self.animations is not None
wait_animation = self.animations[0]
assert isinstance(wait_animation, Wait)
if wait_animation.is_static_wait is None:
should_update = (
self.always_update_mobjects
@ -358,7 +386,7 @@ class Scene:
wait_animation.is_static_wait = not should_update
return not wait_animation.is_static_wait
def get_top_level_mobjects(self):
def get_top_level_mobjects(self) -> list[Mobject]:
"""
Returns all mobjects which are not submobjects.
@ -371,13 +399,13 @@ class Scene:
# of another mobject from the scene
families = [m.get_family() for m in self.mobjects]
def is_top_level(mobject):
def is_top_level(mobject: Mobject) -> bool:
num_families = sum((mobject in family) for family in families)
return num_families == 1
return list(filter(is_top_level, self.mobjects))
def get_mobject_family_members(self):
def get_mobject_family_members(self) -> list[Mobject]:
"""
Returns list of family-members of all mobjects in scene.
If a Circle() and a VGroup(Rectangle(),Triangle()) were added,
@ -394,13 +422,14 @@ class Scene:
for mob in self.mobjects:
family_members.extend(mob.get_family())
return family_members
elif config.renderer == RendererType.CAIRO:
else:
assert config.renderer == RendererType.CAIRO
return extract_mobject_family_members(
self.mobjects,
use_z_index=self.renderer.camera.use_z_index,
)
def add(self, *mobjects: Mobject):
def add(self, *mobjects: Mobject | OpenGLMobject) -> Self:
"""
Mobjects will be displayed, from background to
foreground in the order with which they are added.
@ -418,26 +447,30 @@ class Scene:
"""
if config.renderer == RendererType.OPENGL:
new_mobjects = []
new_meshes = []
new_meshes: list[Object3D] = []
for mobject_or_mesh in mobjects:
if isinstance(mobject_or_mesh, Object3D):
new_meshes.append(mobject_or_mesh)
else:
new_mobjects.append(mobject_or_mesh)
self.remove(*new_mobjects)
self.mobjects += new_mobjects
self.remove(*new_meshes)
self.remove(*new_mobjects) # type: ignore[arg-type]
self.mobjects += new_mobjects # type: ignore[arg-type]
self.remove(*new_meshes) # type: ignore[arg-type]
self.meshes += new_meshes
elif config.renderer == RendererType.CAIRO:
mobjects = [*mobjects, *self.foreground_mobjects]
self.restructure_mobjects(to_remove=mobjects)
self.mobjects += mobjects
else:
assert config.renderer == RendererType.CAIRO
new_and_foreground_mobjects: list[Mobject] = [
*mobjects, # type: ignore[list-item]
*self.foreground_mobjects,
]
self.restructure_mobjects(to_remove=new_and_foreground_mobjects)
self.mobjects += new_and_foreground_mobjects
if self.moving_mobjects:
self.restructure_mobjects(
to_remove=mobjects,
to_remove=new_and_foreground_mobjects,
mobject_list_name="moving_mobjects",
)
self.moving_mobjects += mobjects
self.moving_mobjects += new_and_foreground_mobjects
return self
def add_mobjects_from_animations(self, animations: list[Animation]) -> None:
@ -450,9 +483,9 @@ class Scene:
mob = animation.mobject
if mob is not None and mob not in curr_mobjects:
self.add(mob)
curr_mobjects += mob.get_family()
curr_mobjects += mob.get_family() # type: ignore[arg-type]
def remove(self, *mobjects: Mobject):
def remove(self, *mobjects: Mobject) -> Self:
"""
Removes mobjects in the passed list of mobjects
from the scene and the foreground, by removing them
@ -465,7 +498,8 @@ class Scene:
"""
if config.renderer == RendererType.OPENGL:
mobjects_to_remove = []
meshes_to_remove = set()
meshes_to_remove: set[Object3D] = set()
mobject_or_mesh: Mobject
for mobject_or_mesh in mobjects:
if isinstance(mobject_or_mesh, Object3D):
meshes_to_remove.add(mobject_or_mesh)
@ -475,11 +509,16 @@ class Scene:
self.mobjects,
mobjects_to_remove,
)
def lambda_function(mesh: Object3D) -> bool:
return mesh not in set(meshes_to_remove)
self.meshes = list(
filter(lambda mesh: mesh not in set(meshes_to_remove), self.meshes),
filter(lambda_function, self.meshes),
)
return self
elif config.renderer == RendererType.CAIRO:
else:
assert config.renderer == RendererType.CAIRO
for list_name in "mobjects", "foreground_mobjects":
self.restructure_mobjects(mobjects, list_name, False)
return self
@ -582,7 +621,7 @@ class Scene:
to_remove: Sequence[Mobject],
mobject_list_name: str = "mobjects",
extract_families: bool = True,
):
) -> Scene:
"""
tl:wr
If your scene has a Group(), and you removed a mobject from the Group,
@ -620,7 +659,9 @@ class Scene:
setattr(self, mobject_list_name, new_list)
return self
def get_restructured_mobject_list(self, mobjects: list, to_remove: list):
def get_restructured_mobject_list(
self, mobjects: Iterable[Mobject], to_remove: Iterable[Mobject]
) -> list[Mobject]:
"""
Given a list of mobjects and a list of mobjects to be removed, this
filters out the removable mobjects from the list of mobjects.
@ -639,9 +680,11 @@ class Scene:
list
The list of mobjects with the mobjects to remove removed.
"""
new_mobjects = []
new_mobjects: list[Mobject] = []
def add_safe_mobjects_from_list(list_to_examine, set_to_remove):
def add_safe_mobjects_from_list(
list_to_examine: Iterable[Mobject], set_to_remove: set[Mobject]
) -> None:
for mob in list_to_examine:
if mob in set_to_remove:
continue
@ -655,7 +698,7 @@ class Scene:
return new_mobjects
# TODO, remove this, and calls to this
def add_foreground_mobjects(self, *mobjects: Mobject):
def add_foreground_mobjects(self, *mobjects: Mobject) -> Scene:
"""
Adds mobjects to the foreground, and internally to the list
foreground_mobjects, and mobjects.
@ -674,7 +717,7 @@ class Scene:
self.add(*mobjects)
return self
def add_foreground_mobject(self, mobject: Mobject):
def add_foreground_mobject(self, mobject: Mobject) -> Scene:
"""
Adds a single mobject to the foreground, and internally to the list
foreground_mobjects, and mobjects.
@ -691,7 +734,7 @@ class Scene:
"""
return self.add_foreground_mobjects(mobject)
def remove_foreground_mobjects(self, *to_remove: Mobject):
def remove_foreground_mobjects(self, *to_remove: Mobject) -> Scene:
"""
Removes mobjects from the foreground, and internally from the list
foreground_mobjects.
@ -709,7 +752,7 @@ class Scene:
self.restructure_mobjects(to_remove, "foreground_mobjects")
return self
def remove_foreground_mobject(self, mobject: Mobject):
def remove_foreground_mobject(self, mobject: Mobject) -> Scene:
"""
Removes a single mobject from the foreground, and internally from the list
foreground_mobjects.
@ -726,7 +769,7 @@ class Scene:
"""
return self.remove_foreground_mobjects(mobject)
def bring_to_front(self, *mobjects: Mobject):
def bring_to_front(self, *mobjects: Mobject) -> Scene:
"""
Adds the passed mobjects to the scene again,
pushing them to he front of the scene.
@ -745,7 +788,7 @@ class Scene:
self.add(*mobjects)
return self
def bring_to_back(self, *mobjects: Mobject):
def bring_to_back(self, *mobjects: Mobject) -> Scene:
"""
Removes the mobject from the scene and
adds them to the back of the scene.
@ -765,7 +808,7 @@ class Scene:
self.mobjects = list(mobjects) + self.mobjects
return self
def clear(self):
def clear(self) -> Self:
"""
Removes all mobjects present in self.mobjects
and self.foreground_mobjects from the scene.
@ -812,7 +855,9 @@ class Scene:
return mobjects[i:]
return []
def get_moving_and_static_mobjects(self, animations):
def get_moving_and_static_mobjects(
self, animations: Iterable[Animation]
) -> tuple[list[Mobject], list[Mobject]]:
all_mobjects = list_update(self.mobjects, self.foreground_mobjects)
all_mobject_families = extract_mobject_family_members(
all_mobjects,
@ -833,8 +878,8 @@ class Scene:
def compile_animations(
self,
*args: Animation | Mobject | _AnimationBuilder,
**kwargs,
):
**kwargs: Any,
) -> list[Animation]:
"""
Creates _MethodAnimations from any _AnimationBuilders and updates animation
kwargs with kwargs passed to play().
@ -856,7 +901,7 @@ class Scene:
# Allow passing a generator to self.play instead of comma separated arguments
for arg in arg_anims:
try:
animations.append(prepare_animation(arg))
animations.append(prepare_animation(arg)) # type: ignore[arg-type]
except TypeError as e:
if inspect.ismethod(arg):
raise TypeError(
@ -876,7 +921,7 @@ class Scene:
def _get_animation_time_progression(
self, animations: list[Animation], duration: float
):
) -> tqdm[float]:
"""
You will hardly use this when making your own animations.
This method is for Manim's internal use.
@ -929,10 +974,10 @@ class Scene:
def get_time_progression(
self,
run_time: float,
description,
description: str,
n_iterations: int | None = None,
override_skip_animations: bool = False,
):
) -> tqdm[float]:
"""
You will hardly use this when making your own animations.
This method is for Manim's internal use.
@ -960,7 +1005,7 @@ class Scene:
The CommandLine Progress Bar.
"""
if self.renderer.skip_animations and not override_skip_animations:
times = [run_time]
times: Iterable[float] = [run_time]
else:
step = 1 / config["frame_rate"]
times = np.arange(0, run_time, step)
@ -978,7 +1023,7 @@ class Scene:
def validate_run_time(
cls,
run_time: float,
method: Callable[[Any, ...], Any],
method: Callable[[Any], Any],
parameter_name: str = "run_time",
) -> float:
method_name = f"{cls.__name__}.{method.__name__}()"
@ -1003,7 +1048,7 @@ class Scene:
return run_time
def get_run_time(self, animations: list[Animation]):
def get_run_time(self, animations: list[Animation]) -> float:
"""
Gets the total run time for a list of animations.
@ -1025,11 +1070,11 @@ class Scene:
def play(
self,
*args: Animation | Mobject | _AnimationBuilder,
subcaption=None,
subcaption_duration=None,
subcaption_offset=0,
**kwargs,
):
subcaption: str | None = None,
subcaption_duration: float | None = None,
subcaption_offset: float = 0,
**kwargs: Any,
) -> None:
r"""Plays an animation in this scene.
Parameters
@ -1094,7 +1139,7 @@ class Scene:
duration: float = DEFAULT_WAIT_TIME,
stop_condition: Callable[[], bool] | None = None,
frozen_frame: bool | None = None,
):
) -> None:
"""Plays a "no operation" animation.
Parameters
@ -1125,7 +1170,7 @@ class Scene:
)
)
def pause(self, duration: float = DEFAULT_WAIT_TIME):
def pause(self, duration: float = DEFAULT_WAIT_TIME) -> None:
"""Pauses the scene (i.e., displays a frozen frame).
This is an alias for :meth:`.wait` with ``frozen_frame``
@ -1143,7 +1188,9 @@ class Scene:
duration = self.validate_run_time(duration, self.pause, "duration")
self.wait(duration=duration, frozen_frame=True)
def wait_until(self, stop_condition: Callable[[], bool], max_time: float = 60):
def wait_until(
self, stop_condition: Callable[[], bool], max_time: float = 60
) -> None:
"""Wait until a condition is satisfied, up to a given maximum duration.
Parameters
@ -1160,8 +1207,8 @@ class Scene:
def compile_animation_data(
self,
*animations: Animation | Mobject | _AnimationBuilder,
**play_kwargs,
):
**play_kwargs: Any,
) -> Self | None:
"""Given a list of animations, compile the corresponding
static and moving mobjects, and gather the animation durations.
@ -1207,6 +1254,7 @@ class Scene:
def begin_animations(self) -> None:
"""Start the animations of the scene."""
assert self.animations is not None
for animation in self.animations:
animation._setup_scene(self)
animation.begin()
@ -1221,13 +1269,14 @@ class Scene:
def is_current_animation_frozen_frame(self) -> bool:
"""Returns whether the current animation produces a static frame (generally a Wait)."""
assert self.animations is not None
return (
isinstance(self.animations[0], Wait)
and len(self.animations) == 1
and self.animations[0].is_static_wait
)
def play_internal(self, skip_rendering: bool = False):
def play_internal(self, skip_rendering: bool = False) -> None:
"""
This method is used to prep the animations for rendering,
apply the arguments and parameters required to them,
@ -1238,6 +1287,7 @@ class Scene:
skip_rendering
Whether the rendering should be skipped, by default False
"""
assert self.animations is not None
self.duration = self.get_run_time(self.animations)
self.time_progression = self._get_animation_time_progression(
self.animations,
@ -1256,11 +1306,13 @@ class Scene:
animation.clean_up_from_scene(self)
if not self.renderer.skip_animations:
self.update_mobjects(0)
self.renderer.static_image = None
# TODO: The OpenGLRenderer does not have the property static.image.
self.renderer.static_image = None # type: ignore[union-attr]
# Closing the progress bar at the end of the play.
self.time_progression.close()
def check_interactive_embed_is_valid(self):
def check_interactive_embed_is_valid(self) -> bool:
assert isinstance(self.renderer, OpenGLRenderer)
if config["force_window"]:
return True
if self.skip_animation_preview:
@ -1285,23 +1337,28 @@ class Scene:
return False
return True
def interactive_embed(self):
def interactive_embed(self) -> None:
"""Like embed(), but allows for screen interaction."""
assert isinstance(self.camera, OpenGLCamera)
assert isinstance(self.renderer, OpenGLRenderer)
if not self.check_interactive_embed_is_valid():
return
self.interactive_mode = True
from IPython.terminal.embed import InteractiveShellEmbed
def ipython(shell, namespace):
def ipython(shell: InteractiveShellEmbed, namespace: dict[str, Any]) -> None:
import manim.opengl
def load_module_into_namespace(module, namespace):
def load_module_into_namespace(
module: Any, namespace: dict[str, Any]
) -> None:
for name in dir(module):
namespace[name] = getattr(module, name)
load_module_into_namespace(manim, namespace)
load_module_into_namespace(manim.opengl, namespace)
def embedded_rerun(*args, **kwargs):
def embedded_rerun(*args: Any, **kwargs: Any) -> None:
self.queue.put(("rerun_keyboard", args, kwargs))
shell.exiter()
@ -1310,10 +1367,14 @@ class Scene:
shell(local_ns=namespace)
self.queue.put(("exit_keyboard", [], {}))
def get_embedded_method(method_name):
return lambda *args, **kwargs: self.queue.put((method_name, args, kwargs))
def get_embedded_method(method_name: str) -> Callable[..., None]:
def embedded_method(*args: Any, **kwargs: Any) -> None:
self.queue.put((method_name, args, kwargs))
local_namespace = inspect.currentframe().f_back.f_locals
return embedded_method
currentframe: FrameType = inspect.currentframe() # type: ignore[assignment]
local_namespace = currentframe.f_back.f_locals # type: ignore[union-attr]
for method in ("play", "wait", "add", "remove"):
embedded_method = get_embedded_method(method)
# Allow for calling scene methods without prepending 'self.'.
@ -1322,7 +1383,6 @@ class Scene:
from sqlite3 import connect
from IPython.core.getipython import get_ipython
from IPython.terminal.embed import InteractiveShellEmbed
from traitlets.config import Config
cfg = Config()
@ -1358,7 +1418,10 @@ class Scene:
self.interact(shell, keyboard_thread)
def interact(self, shell, keyboard_thread):
# from IPython.terminal.embed import InteractiveShellEmbed
def interact(self, shell: Any, keyboard_thread: threading.Thread) -> None:
assert isinstance(self.renderer, OpenGLRenderer)
event_handler = RerunSceneHandler(self.queue)
file_observer = Observer()
file_observer.schedule(event_handler, config["input_file"], recursive=True)
@ -1435,7 +1498,8 @@ class Scene:
if self.renderer.window.is_closing:
self.renderer.window.destroy()
def embed(self):
def embed(self) -> None:
assert isinstance(self.renderer, OpenGLRenderer)
if not config["preview"]:
logger.warning("Called embed() while no preview window is available.")
return
@ -1459,7 +1523,9 @@ class Scene:
# Use the locals of the caller as the local namespace
# once embedded, and add a few custom shortcuts.
local_ns = inspect.currentframe().f_back.f_locals
current_frame = inspect.currentframe()
assert isinstance(current_frame, FrameType)
local_ns = current_frame.f_back.f_locals # type: ignore[union-attr]
# local_ns["touch"] = self.interact
for method in (
"play",
@ -1477,9 +1543,10 @@ class Scene:
# End scene when exiting an embed.
raise Exception("Exiting scene.")
def update_to_time(self, t):
def update_to_time(self, t: float) -> None:
dt = t - self.last_t
self.last_t = t
assert self.animations is not None
for animation in self.animations:
animation.update_mobjects(dt)
alpha = t / animation.run_time
@ -1541,8 +1608,8 @@ class Scene:
sound_file: str,
time_offset: float = 0,
gain: float | None = None,
**kwargs,
):
**kwargs: Any,
) -> None:
"""
This method is used to add a sound to the animation.
@ -1583,7 +1650,9 @@ class Scene:
time = self.time + time_offset
self.renderer.file_writer.add_sound(sound_file, time, gain, **kwargs)
def on_mouse_motion(self, point, d_point):
def on_mouse_motion(self, point: Point3D, d_point: Point3D) -> None:
assert isinstance(self.camera, OpenGLCamera)
assert isinstance(self.renderer, OpenGLRenderer)
self.mouse_point.move_to(point)
if SHIFT_VALUE in self.renderer.pressed_keys:
shift = -d_point
@ -1593,13 +1662,15 @@ class Scene:
shift = np.dot(np.transpose(transform), shift)
self.camera.shift(shift)
def on_mouse_scroll(self, point, offset):
def on_mouse_scroll(self, point: Point3D, offset: Point3D) -> None:
assert isinstance(self.camera, OpenGLCamera)
if not config.use_projection_stroke_shaders:
factor = 1 + np.arctan(-2.1 * offset[1])
self.camera.scale(factor, about_point=self.camera_target)
self.mouse_scroll_orbit_controls(point, offset)
def on_key_press(self, symbol, modifiers):
def on_key_press(self, symbol: int, modifiers: int) -> None:
assert isinstance(self.camera, OpenGLCamera)
try:
char = chr(symbol)
except OverflowError:
@ -1615,10 +1686,17 @@ class Scene:
if char in self.key_to_function_map:
self.key_to_function_map[char]()
def on_key_release(self, symbol, modifiers):
def on_key_release(self, symbol: int, modifiers: int) -> None:
pass
def on_mouse_drag(self, point, d_point, buttons, modifiers):
def on_mouse_drag(
self,
point: Point3D,
d_point: Point3D,
buttons: int,
modifiers: int,
) -> None:
assert isinstance(self.camera, OpenGLCamera)
self.mouse_drag_point.move_to(point)
if buttons == 1:
self.camera.increment_theta(-d_point[0])
@ -1632,7 +1710,8 @@ class Scene:
self.mouse_drag_orbit_controls(point, d_point, buttons, modifiers)
def mouse_scroll_orbit_controls(self, point, offset):
def mouse_scroll_orbit_controls(self, point: Point3D, offset: Point3D) -> None:
assert isinstance(self.camera, OpenGLCamera)
camera_to_target = self.camera_target - self.camera.get_position()
camera_to_target *= np.sign(offset[1])
shift_vector = 0.01 * camera_to_target
@ -1640,7 +1719,14 @@ class Scene:
opengl.translation_matrix(*shift_vector) @ self.camera.model_matrix
)
def mouse_drag_orbit_controls(self, point, d_point, buttons, modifiers):
def mouse_drag_orbit_controls(
self,
point: Point3D,
d_point: Point3D,
buttons: int,
modifiers: int,
) -> None:
assert isinstance(self.camera, OpenGLCamera)
# Left click drag.
if buttons == 1:
# Translate to target the origin and rotate around the z axis.
@ -1713,9 +1799,9 @@ class Scene:
)
self.camera_target += total_shift_vector
def set_key_function(self, char, func):
def set_key_function(self, char: str, func: Callable[[], Any]) -> None:
self.key_to_function_map[char] = func
def on_mouse_press(self, point, button, modifiers):
def on_mouse_press(self, point: Point3D, button: int, modifiers: int) -> None:
for func in self.mouse_press_callbacks:
func()

View file

@ -4,10 +4,13 @@ from __future__ import annotations
__all__ = ["VectorScene", "LinearTransformationScene"]
from typing import Callable
from collections.abc import Iterable
from typing import TYPE_CHECKING, Any, Callable, cast
import numpy as np
from manim.animation.creation import DrawBorderThenFill, Group
from manim.camera.camera import Camera
from manim.mobject.geometry.arc import Dot
from manim.mobject.geometry.line import Arrow, Line, Vector
from manim.mobject.geometry.polygram import Rectangle
@ -41,6 +44,14 @@ from ..utils.color import (
from ..utils.rate_functions import rush_from, rush_into
from ..utils.space_ops import angle_of_vector
if TYPE_CHECKING:
from typing import Any
from typing_extensions import Self
from manim.typing import MappingFunction, Point2DLike, Point3D, Point3DLike
X_COLOR = GREEN_C
Y_COLOR = RED_C
Z_COLOR = BLUE_D
@ -53,11 +64,11 @@ Z_COLOR = BLUE_D
# Also, methods I would have thought of as getters, like coords_to_vector, are
# actually doing a lot of animating.
class VectorScene(Scene):
def __init__(self, basis_vector_stroke_width=6, **kwargs):
def __init__(self, basis_vector_stroke_width: float = 6.0, **kwargs: Any) -> None:
super().__init__(**kwargs)
self.basis_vector_stroke_width = basis_vector_stroke_width
def add_plane(self, animate: bool = False, **kwargs):
def add_plane(self, animate: bool = False, **kwargs: Any) -> NumberPlane:
"""
Adds a NumberPlane object to the background.
@ -79,7 +90,11 @@ class VectorScene(Scene):
self.add(plane)
return plane
def add_axes(self, animate: bool = False, color: bool = WHITE):
def add_axes(
self,
animate: bool = False,
color: ParsableManimColor | Iterable[ParsableManimColor] = WHITE,
) -> Axes:
"""
Adds a pair of Axes to the Scene.
@ -96,7 +111,9 @@ class VectorScene(Scene):
self.add(axes)
return axes
def lock_in_faded_grid(self, dimness: float = 0.7, axes_dimness: float = 0.5):
def lock_in_faded_grid(
self, dimness: float = 0.7, axes_dimness: float = 0.5
) -> None:
"""
This method freezes the NumberPlane and Axes that were already
in the background, and adds new, manipulatable ones to the foreground.
@ -116,11 +133,13 @@ class VectorScene(Scene):
axes.fade(axes_dimness)
self.add(axes)
self.renderer.update_frame()
# TODO
# error: Missing positional argument "scene" in call to "update_frame" of "CairoRenderer" [call-arg]
self.renderer.update_frame() # type: ignore[call-arg]
self.renderer.camera = Camera(self.renderer.get_frame())
self.clear()
def get_vector(self, numerical_vector: np.ndarray | list | tuple, **kwargs):
def get_vector(self, numerical_vector: Point3DLike, **kwargs: Any) -> Arrow:
"""
Returns an arrow on the Plane given an input numerical vector.
@ -137,19 +156,21 @@ class VectorScene(Scene):
The Arrow representing the Vector.
"""
return Arrow(
self.plane.coords_to_point(0, 0),
self.plane.coords_to_point(*numerical_vector[:2]),
# TODO
# error: "VectorScene" has no attribute "plane" [attr-defined]
self.plane.coords_to_point(0, 0), # type: ignore[attr-defined]
self.plane.coords_to_point(*numerical_vector[:2]), # type: ignore[attr-defined]
buff=0,
**kwargs,
)
def add_vector(
self,
vector: Arrow | list | tuple | np.ndarray,
color: str = YELLOW,
vector: Arrow | Point3DLike,
color: ParsableManimColor | Iterable[ParsableManimColor] = YELLOW,
animate: bool = True,
**kwargs,
):
**kwargs: Any,
) -> Arrow:
"""
Returns the Vector after adding it to the Plane.
@ -179,13 +200,13 @@ class VectorScene(Scene):
The arrow representing the vector.
"""
if not isinstance(vector, Arrow):
vector = Vector(vector, color=color, **kwargs)
vector = Vector(np.asarray(vector), color=color, **kwargs)
if animate:
self.play(GrowArrow(vector))
self.add(vector)
return vector
def write_vector_coordinates(self, vector: Arrow, **kwargs):
def write_vector_coordinates(self, vector: Vector, **kwargs: Any) -> Matrix:
"""
Returns a column matrix indicating the vector coordinates,
after writing them to the screen.
@ -203,11 +224,15 @@ class VectorScene(Scene):
:class:`.Matrix`
The column matrix representing the vector.
"""
coords = vector.coordinate_label(**kwargs)
coords: Matrix = vector.coordinate_label(**kwargs)
self.play(Write(coords))
return coords
def get_basis_vectors(self, i_hat_color: str = X_COLOR, j_hat_color: str = Y_COLOR):
def get_basis_vectors(
self,
i_hat_color: ParsableManimColor | Iterable[ParsableManimColor] = X_COLOR,
j_hat_color: ParsableManimColor | Iterable[ParsableManimColor] = Y_COLOR,
) -> VGroup:
"""
Returns a VGroup of the Basis Vectors (1,0) and (0,1)
@ -226,12 +251,16 @@ class VectorScene(Scene):
"""
return VGroup(
*(
Vector(vect, color=color, stroke_width=self.basis_vector_stroke_width)
Vector(
np.asarray(vect),
color=color,
stroke_width=self.basis_vector_stroke_width,
)
for vect, color in [([1, 0], i_hat_color), ([0, 1], j_hat_color)]
)
)
def get_basis_vector_labels(self, **kwargs):
def get_basis_vector_labels(self, **kwargs: Any) -> VGroup:
"""
Returns naming labels for the basis vectors.
@ -263,13 +292,13 @@ class VectorScene(Scene):
def get_vector_label(
self,
vector: Vector,
label,
label: MathTex | str,
at_tip: bool = False,
direction: str = "left",
rotate: bool = False,
color: str | None = None,
color: ParsableManimColor | None = None,
label_scale_factor: float = LARGE_BUFF - 0.2,
):
) -> MathTex:
"""
Returns naming labels for the passed vector.
@ -300,8 +329,11 @@ class VectorScene(Scene):
label = "\\vec{\\textbf{%s}}" % label # noqa: UP031
label = MathTex(label)
if color is None:
color = vector.get_color()
label.set_color(color)
prepared_color: ParsableManimColor = vector.get_color()
else:
prepared_color = color
label.set_color(prepared_color)
assert isinstance(label, MathTex)
label.scale(label_scale_factor)
label.add_background_rectangle()
@ -314,16 +346,18 @@ class VectorScene(Scene):
if not rotate:
label.rotate(-angle, about_point=ORIGIN)
if direction == "left":
label.shift(-label.get_bottom() + 0.1 * UP)
temp_shift_1: Point3D = np.asarray(label.get_bottom())
label.shift(-temp_shift_1 + 0.1 * UP)
else:
label.shift(-label.get_top() + 0.1 * DOWN)
temp_shift_2: Point3D = np.asarray(label.get_top())
label.shift(-temp_shift_2 + 0.1 * DOWN)
label.rotate(angle, about_point=ORIGIN)
label.shift((vector.get_end() - vector.get_start()) / 2)
return label
def label_vector(
self, vector: Vector, label: MathTex | str, animate: bool = True, **kwargs
):
self, vector: Vector, label: MathTex | str, animate: bool = True, **kwargs: Any
) -> MathTex:
"""
Shortcut method for creating, and animating the addition of
a label for the vector.
@ -347,38 +381,38 @@ class VectorScene(Scene):
:class:`~.MathTex`
The MathTex of the label.
"""
label = self.get_vector_label(vector, label, **kwargs)
mathtex_label = self.get_vector_label(vector, label, **kwargs)
if animate:
self.play(Write(label, run_time=1))
self.add(label)
return label
self.play(Write(mathtex_label, run_time=1))
self.add(mathtex_label)
return mathtex_label
def position_x_coordinate(
self,
x_coord,
x_line,
vector,
): # TODO Write DocStrings for this.
x_coord: MathTex,
x_line: Line,
vector: Point3DLike,
) -> MathTex: # TODO Write DocStrings for this.
x_coord.next_to(x_line, -np.sign(vector[1]) * UP)
x_coord.set_color(X_COLOR)
return x_coord
def position_y_coordinate(
self,
y_coord,
y_line,
vector,
): # TODO Write DocStrings for this.
y_coord: MathTex,
y_line: Line,
vector: Point3DLike,
) -> MathTex: # TODO Write DocStrings for this.
y_coord.next_to(y_line, np.sign(vector[0]) * RIGHT)
y_coord.set_color(Y_COLOR)
return y_coord
def coords_to_vector(
self,
vector: np.ndarray | list | tuple,
coords_start: np.ndarray | list | tuple = 2 * RIGHT + 2 * UP,
vector: Point2DLike,
coords_start: Point3DLike = 2 * RIGHT + 2 * UP,
clean_up: bool = True,
):
) -> None:
"""
This method writes the vector as a column matrix (henceforth called the label),
takes the values in it one by one, and form the corresponding
@ -409,26 +443,29 @@ class VectorScene(Scene):
y_line = Line(x_line.get_end(), arrow.get_end())
x_line.set_color(X_COLOR)
y_line.set_color(Y_COLOR)
x_coord, y_coord = array.get_mob_matrix().flatten()
mob_matrix = array.get_mob_matrix()
x_coord = mob_matrix[0][0]
y_coord = mob_matrix[1][0]
self.play(Write(array, run_time=1))
self.wait()
self.play(
ApplyFunction(
lambda x: self.position_x_coordinate(x, x_line, vector),
lambda x: self.position_x_coordinate(x, x_line, vector), # type: ignore[arg-type]
x_coord,
),
)
self.play(Create(x_line))
animations = [
ApplyFunction(
lambda y: self.position_y_coordinate(y, y_line, vector),
lambda y: self.position_y_coordinate(y, y_line, vector), # type: ignore[arg-type]
y_coord,
),
FadeOut(array.get_brackets()),
]
self.play(*animations)
y_coord, _ = (anim.mobject for anim in animations)
# TODO: Can we delete the line below? I don't think it have any purpose.
# y_coord, _ = (anim.mobject for anim in animations)
self.play(Create(y_line))
self.play(Create(arrow))
self.wait()
@ -438,10 +475,10 @@ class VectorScene(Scene):
def vector_to_coords(
self,
vector: np.ndarray | list | tuple,
vector: Point3DLike,
integer_labels: bool = True,
clean_up: bool = True,
):
) -> tuple[Matrix, Line, Line]:
"""
This method displays vector as a Vector() based vector, and then shows
the corresponding lines that make up the x and y components of the vector.
@ -475,7 +512,7 @@ class VectorScene(Scene):
y_line = Line(x_line.get_end(), arrow.get_end())
x_line.set_color(X_COLOR)
y_line.set_color(Y_COLOR)
x_coord, y_coord = array.get_entries()
x_coord, y_coord = cast(VGroup, array.get_entries())
x_coord_start = self.position_x_coordinate(x_coord.copy(), x_line, vector)
y_coord_start = self.position_y_coordinate(y_coord.copy(), y_line, vector)
brackets = array.get_brackets()
@ -499,7 +536,7 @@ class VectorScene(Scene):
self.add(*starting_mobjects)
return array, x_line, y_line
def show_ghost_movement(self, vector: Arrow | list | tuple | np.ndarray):
def show_ghost_movement(self, vector: Arrow | Point2DLike | Point3DLike) -> None:
"""
This method plays an animation that partially shows the entire plane moving
in the direction of a particular vector. This is useful when you wish to
@ -513,20 +550,26 @@ class VectorScene(Scene):
"""
if isinstance(vector, Arrow):
vector = vector.get_end() - vector.get_start()
elif len(vector) == 2:
vector = np.append(np.array(vector), 0.0)
x_max = int(config["frame_x_radius"] + abs(vector[0]))
y_max = int(config["frame_y_radius"] + abs(vector[1]))
else:
vector = np.asarray(vector)
if len(vector) == 2:
vector = np.append(np.array(vector), 0.0)
vector_cleaned: Point3D = vector
x_max = int(config["frame_x_radius"] + abs(vector_cleaned[0]))
y_max = int(config["frame_y_radius"] + abs(vector_cleaned[1]))
# TODO:
# I think that this should be a VGroup instead of a VMobject.
dots = VMobject(
*(
*( # type: ignore[arg-type]
Dot(x * RIGHT + y * UP)
for x in range(-x_max, x_max)
for y in range(-y_max, y_max)
)
)
dots.set_fill(BLACK, opacity=0)
dots_halfway = dots.copy().shift(vector / 2).set_fill(WHITE, 1)
dots_end = dots.copy().shift(vector)
dots_halfway = dots.copy().shift(vector_cleaned / 2).set_fill(WHITE, 1)
dots_end = dots.copy().shift(vector_cleaned)
self.play(Transform(dots, dots_halfway, rate_func=rush_into))
self.play(Transform(dots, dots_end, rate_func=rush_from))
@ -585,16 +628,16 @@ class LinearTransformationScene(VectorScene):
self,
include_background_plane: bool = True,
include_foreground_plane: bool = True,
background_plane_kwargs: dict | None = None,
foreground_plane_kwargs: dict | None = None,
background_plane_kwargs: dict[str, Any] | None = None,
foreground_plane_kwargs: dict[str, Any] | None = None,
show_coordinates: bool = False,
show_basis_vectors: bool = True,
basis_vector_stroke_width: float = 6,
i_hat_color: ParsableManimColor = X_COLOR,
j_hat_color: ParsableManimColor = Y_COLOR,
leave_ghost_vectors: bool = False,
**kwargs,
):
**kwargs: Any,
) -> None:
super().__init__(**kwargs)
self.include_background_plane = include_background_plane
@ -605,7 +648,7 @@ class LinearTransformationScene(VectorScene):
self.i_hat_color = ManimColor(i_hat_color)
self.j_hat_color = ManimColor(j_hat_color)
self.leave_ghost_vectors = leave_ghost_vectors
self.background_plane_kwargs = {
self.background_plane_kwargs: dict[str, Any] = {
"color": GREY,
"axis_config": {
"color": GREY,
@ -618,7 +661,7 @@ class LinearTransformationScene(VectorScene):
self.ghost_vectors = VGroup()
self.foreground_plane_kwargs = {
self.foreground_plane_kwargs: dict[str, Any] = {
"x_range": np.array([-config["frame_width"], config["frame_width"], 1.0]),
"y_range": np.array([-config["frame_width"], config["frame_width"], 1.0]),
"faded_line_ratio": 1,
@ -630,22 +673,25 @@ class LinearTransformationScene(VectorScene):
)
@staticmethod
def update_default_configs(default_configs, passed_configs):
def update_default_configs(
default_configs: Iterable[dict[str, Any]],
passed_configs: Iterable[dict[str, Any] | None],
) -> None:
for default_config, passed_config in zip(default_configs, passed_configs):
if passed_config is not None:
update_dict_recursively(default_config, passed_config)
def setup(self):
def setup(self) -> None:
# The has_already_setup attr is to not break all the old Scenes
if hasattr(self, "has_already_setup"):
return
self.has_already_setup = True
self.background_mobjects = []
self.foreground_mobjects = []
self.transformable_mobjects = []
self.moving_vectors = []
self.transformable_labels = []
self.moving_mobjects = []
self.background_mobjects: list[Mobject] = []
self.foreground_mobjects: list[Mobject] = []
self.transformable_mobjects: list[Mobject] = []
self.moving_vectors: list[Mobject] = []
self.transformable_labels: list[MathTex] = []
self.moving_mobjects: list[Mobject] = []
self.background_plane = NumberPlane(**self.background_plane_kwargs)
@ -665,7 +711,9 @@ class LinearTransformationScene(VectorScene):
self.i_hat, self.j_hat = self.basis_vectors
self.add(self.basis_vectors)
def add_special_mobjects(self, mob_list: list, *mobs_to_add: Mobject):
def add_special_mobjects(
self, mob_list: list[Mobject], *mobs_to_add: Mobject
) -> None:
"""
Adds mobjects to a separate list that can be tracked,
if these mobjects have some extra importance.
@ -685,7 +733,7 @@ class LinearTransformationScene(VectorScene):
mob_list.append(mobject)
self.add(mobject)
def add_background_mobject(self, *mobjects: Mobject):
def add_background_mobject(self, *mobjects: Mobject) -> None:
"""
Adds the mobjects to the special list
self.background_mobjects.
@ -697,8 +745,9 @@ class LinearTransformationScene(VectorScene):
"""
self.add_special_mobjects(self.background_mobjects, *mobjects)
# TODO, this conflicts with Scene.add_fore
def add_foreground_mobject(self, *mobjects: Mobject):
# TODO, this conflicts with Scene.add_foreground_mobject
# Please be aware that there is also the method Scene.add_foreground_mobjects.
def add_foreground_mobject(self, *mobjects: Mobject) -> None: # type: ignore[override]
"""
Adds the mobjects to the special list
self.foreground_mobjects.
@ -710,7 +759,7 @@ class LinearTransformationScene(VectorScene):
"""
self.add_special_mobjects(self.foreground_mobjects, *mobjects)
def add_transformable_mobject(self, *mobjects: Mobject):
def add_transformable_mobject(self, *mobjects: Mobject) -> None:
"""
Adds the mobjects to the special list
self.transformable_mobjects.
@ -724,7 +773,7 @@ class LinearTransformationScene(VectorScene):
def add_moving_mobject(
self, mobject: Mobject, target_mobject: Mobject | None = None
):
) -> None:
"""
Adds the mobject to the special list
self.moving_mobject, and adds a property
@ -751,8 +800,11 @@ class LinearTransformationScene(VectorScene):
return self.ghost_vectors
def get_unit_square(
self, color: str = YELLOW, opacity: float = 0.3, stroke_width: float = 3
):
self,
color: ParsableManimColor | Iterable[ParsableManimColor] = YELLOW,
opacity: float = 0.3,
stroke_width: float = 3,
) -> Rectangle:
"""
Returns a unit square for the current NumberPlane.
@ -783,7 +835,7 @@ class LinearTransformationScene(VectorScene):
square.move_to(self.plane.coords_to_point(0, 0), DL)
return square
def add_unit_square(self, animate: bool = False, **kwargs):
def add_unit_square(self, animate: bool = False, **kwargs: Any) -> Self:
"""
Adds a unit square to the scene via
self.get_unit_square.
@ -814,8 +866,12 @@ class LinearTransformationScene(VectorScene):
return self
def add_vector(
self, vector: Arrow | list | tuple | np.ndarray, color: str = YELLOW, **kwargs
):
self,
vector: Arrow | list | tuple | np.ndarray,
color: ParsableManimColor = YELLOW,
animate: bool = False,
**kwargs: Any,
) -> Arrow:
"""
Adds a vector to the scene, and puts it in the special
list self.moving_vectors.
@ -839,11 +895,11 @@ class LinearTransformationScene(VectorScene):
Arrow
The arrow representing the vector.
"""
vector = super().add_vector(vector, color=color, **kwargs)
vector = super().add_vector(vector, color=color, animate=animate, **kwargs)
self.moving_vectors.append(vector)
return vector
def write_vector_coordinates(self, vector: Arrow, **kwargs):
def write_vector_coordinates(self, vector: Vector, **kwargs: Any) -> Matrix:
"""
Returns a column matrix indicating the vector coordinates,
after writing them to the screen, and adding them to the
@ -872,8 +928,8 @@ class LinearTransformationScene(VectorScene):
label: MathTex | str,
transformation_name: str | MathTex = "L",
new_label: str | MathTex | None = None,
**kwargs,
):
**kwargs: Any,
) -> MathTex:
"""
Method for creating, and animating the addition of
a transformable label for the vector.
@ -900,26 +956,27 @@ class LinearTransformationScene(VectorScene):
:class:`~.MathTex`
The MathTex of the label.
"""
# TODO: Clear up types in this function. This is currently a mess.
label_mob = self.label_vector(vector, label, **kwargs)
if new_label:
label_mob.target_text = new_label
label_mob.target_text = new_label # type: ignore[attr-defined]
else:
label_mob.target_text = (
f"{transformation_name}({label_mob.get_tex_string()})"
label_mob.target_text = ( # type: ignore[attr-defined]
f"{transformation_name}({label_mob.get_tex_string()})" # type: ignore[no-untyped-call]
)
label_mob.vector = vector
label_mob.kwargs = kwargs
if "animate" in label_mob.kwargs:
label_mob.kwargs.pop("animate")
label_mob.vector = vector # type: ignore[attr-defined]
label_mob.kwargs = kwargs # type: ignore[attr-defined]
if "animate" in label_mob.kwargs: # type: ignore[attr-defined]
label_mob.kwargs.pop("animate") # type: ignore[attr-defined]
self.transformable_labels.append(label_mob)
return label_mob
return cast(MathTex, label_mob)
def add_title(
self,
title: str | MathTex | Tex,
scale_factor: float = 1.5,
animate: bool = False,
):
) -> Self:
"""
Adds a title, after scaling it, adding a background rectangle,
moving it to the top and adding it to foreground_mobjects adding
@ -951,7 +1008,9 @@ class LinearTransformationScene(VectorScene):
self.title = title
return self
def get_matrix_transformation(self, matrix: np.ndarray | list | tuple):
def get_matrix_transformation(
self, matrix: np.ndarray | list | tuple
) -> Callable[[Point3D], Point3D]:
"""
Returns a function corresponding to the linear
transformation represented by the matrix passed.
@ -965,7 +1024,7 @@ class LinearTransformationScene(VectorScene):
def get_transposed_matrix_transformation(
self, transposed_matrix: np.ndarray | list | tuple
):
) -> Callable[[Point3D], Point3D]:
"""
Returns a function corresponding to the linear
transformation represented by the transposed
@ -985,7 +1044,7 @@ class LinearTransformationScene(VectorScene):
raise ValueError("Matrix has bad dimensions")
return lambda point: np.dot(point, transposed_matrix)
def get_piece_movement(self, pieces: list | tuple | np.ndarray):
def get_piece_movement(self, pieces: Iterable[Mobject]) -> Transform:
"""
This method returns an animation that moves an arbitrary
mobject in "pieces" to its corresponding .target value.
@ -1013,7 +1072,7 @@ class LinearTransformationScene(VectorScene):
self.add(self.ghost_vectors[-1])
return Transform(start, target, lag_ratio=0)
def get_moving_mobject_movement(self, func: Callable[[np.ndarray], np.ndarray]):
def get_moving_mobject_movement(self, func: MappingFunction) -> Transform:
"""
This method returns an animation that moves a mobject
in "self.moving_mobjects" to its corresponding .target value.
@ -1034,11 +1093,12 @@ class LinearTransformationScene(VectorScene):
for m in self.moving_mobjects:
if m.target is None:
m.target = m.copy()
target_point = func(m.get_center())
temp: Point3D = m.get_center()
target_point = func(temp)
m.target.move_to(target_point)
return self.get_piece_movement(self.moving_mobjects)
def get_vector_movement(self, func: Callable[[np.ndarray], np.ndarray]):
def get_vector_movement(self, func: MappingFunction) -> Transform:
"""
This method returns an animation that moves a mobject
in "self.moving_vectors" to its corresponding .target value.
@ -1058,12 +1118,12 @@ class LinearTransformationScene(VectorScene):
"""
for v in self.moving_vectors:
v.target = Vector(func(v.get_end()), color=v.get_color())
norm = np.linalg.norm(v.target.get_end())
norm = float(np.linalg.norm(v.target.get_end()))
if norm < 0.1:
v.target.get_tip().scale(norm)
return self.get_piece_movement(self.moving_vectors)
def get_transformable_label_movement(self):
def get_transformable_label_movement(self) -> Transform:
"""
This method returns an animation that moves all labels
in "self.transformable_labels" to its corresponding .target .
@ -1074,12 +1134,17 @@ class LinearTransformationScene(VectorScene):
The animation of the movement.
"""
for label in self.transformable_labels:
# TODO: This location and lines 933 and 335 are the only locations in
# the code where the target_text property is referenced.
target_text: MathTex | str = label.target_text # type: ignore[assignment]
label.target = self.get_vector_label(
label.vector.target, label.target_text, **label.kwargs
label.vector.target, # type: ignore[attr-defined]
target_text,
**label.kwargs, # type: ignore[arg-type]
)
return self.get_piece_movement(self.transformable_labels)
def apply_matrix(self, matrix: np.ndarray | list | tuple, **kwargs):
def apply_matrix(self, matrix: np.ndarray | list | tuple, **kwargs: Any) -> None:
"""
Applies the transformation represented by the
given matrix to the number plane, and each vector/similar
@ -1094,7 +1159,7 @@ class LinearTransformationScene(VectorScene):
"""
self.apply_transposed_matrix(np.array(matrix).T, **kwargs)
def apply_inverse(self, matrix: np.ndarray | list | tuple, **kwargs):
def apply_inverse(self, matrix: np.ndarray | list | tuple, **kwargs: Any) -> None:
"""
This method applies the linear transformation
represented by the inverse of the passed matrix
@ -1110,8 +1175,8 @@ class LinearTransformationScene(VectorScene):
self.apply_matrix(np.linalg.inv(matrix), **kwargs)
def apply_transposed_matrix(
self, transposed_matrix: np.ndarray | list | tuple, **kwargs
):
self, transposed_matrix: np.ndarray | list | tuple, **kwargs: Any
) -> None:
"""
Applies the transformation represented by the
given transposed matrix to the number plane,
@ -1132,7 +1197,9 @@ class LinearTransformationScene(VectorScene):
kwargs["path_arc"] = net_rotation
self.apply_function(func, **kwargs)
def apply_inverse_transpose(self, t_matrix: np.ndarray | list | tuple, **kwargs):
def apply_inverse_transpose(
self, t_matrix: np.ndarray | list | tuple, **kwargs: Any
) -> None:
"""
Applies the inverse of the transformation represented
by the given transposed matrix to the number plane and each
@ -1149,8 +1216,8 @@ class LinearTransformationScene(VectorScene):
self.apply_transposed_matrix(t_inv, **kwargs)
def apply_nonlinear_transformation(
self, function: Callable[[np.ndarray], np.ndarray], **kwargs
):
self, function: Callable[[np.ndarray], np.ndarray], **kwargs: Any
) -> None:
"""
Applies the non-linear transformation represented
by the given function to the number plane and each
@ -1168,10 +1235,10 @@ class LinearTransformationScene(VectorScene):
def apply_function(
self,
function: Callable[[np.ndarray], np.ndarray],
added_anims: list = [],
**kwargs,
):
function: MappingFunction,
added_anims: list[Animation] = [],
**kwargs: Any,
) -> None:
"""
Applies the given function to each of the mobjects in
self.transformable_mobjects, and plays the animation showing
@ -1194,7 +1261,7 @@ class LinearTransformationScene(VectorScene):
kwargs["run_time"] = 3
anims = (
[
ApplyPointwiseFunction(function, t_mob)
ApplyPointwiseFunction(function, t_mob) # type: ignore[arg-type]
for t_mob in self.transformable_mobjects
]
+ [

View file

@ -210,18 +210,15 @@ ignore_errors = True
[mypy-manim.renderer.vectorized_mobject_rendering]
ignore_errors = True
[mypy-manim.scene.moving_camera_scene]
ignore_errors = True
[mypy-manim.scene.scene_file_writer]
ignore_errors = True
[mypy-manim.scene.scene]
ignore_errors = True
[mypy-manim.scene.three_d_scene]
ignore_errors = True
[mypy-manim.scene.vector_space_scene]
ignore_errors = True
[mypy-manim.scene.zoomed_scene]
ignore_errors = True