Merge branch 'experimental' of https://github.com/ManimCommunity/manim into experimental

This commit is contained in:
Francisco Manríquez Novoa 2026-01-29 15:22:33 -03:00
commit dcc6f4b901
19 changed files with 187 additions and 110 deletions

View file

@ -13,8 +13,8 @@ scene.render()
```
should be changed to:
```py
manager = Manager(SceneClass)
manager.render()
with Manager(SceneClass) as manager:
manager.render()
```
If you are a plugin author that subclasses `Scene` and changed `Scene.render`, you should migrate

View file

@ -43,13 +43,16 @@ class Test(Scene):
if __name__ == "__main__":
with tempconfig(
{
"preview": True,
"write_to_movie": False,
"disable_caching": True,
"frame_rate": 60,
"disable_caching_warning": True,
}
with (
tempconfig(
{
"preview": True,
"write_to_movie": False,
"disable_caching": True,
"frame_rate": 60,
"disable_caching_warning": True,
}
),
Manager(Test) as manager,
):
Manager(Test).render()
manager.render()

View file

@ -84,8 +84,8 @@ def checkhealth() -> None:
self.execution_time = timeit.timeit(self._inner_construct, number=1)
with mn.tempconfig({"preview": True, "disable_caching": True}):
manager = mn.Manager(CheckHealthDemo)
manager.render()
with mn.Manager(CheckHealthDemo) as manager:
manager.render()
click.echo(
f"Scene rendered in {manager.scene.execution_time:.2f} seconds."

View file

@ -89,8 +89,7 @@ def render(**kwargs: Any) -> ClickArgs | dict[str, Any]:
file = Path(config.input_file)
try:
for SceneClass in scene_classes_from_file(file):
with tempconfig({}):
manager = Manager(SceneClass)
with tempconfig({}), Manager(SceneClass) as manager:
manager.render()
except Exception:
error_console.print_exception()

View file

@ -27,6 +27,7 @@ from manim.utils.progressbar import (
)
if TYPE_CHECKING:
from types import TracebackType
from typing import Any
import numpy.typing as npt
@ -58,7 +59,10 @@ class Manager(Generic[Scene_co]):
self.play(FadeIn(Circle()))
Manager(Manimation).render()
# make sure to use it as a context manager
# to ensure proper resource cleanup
with Manager(Manimation) as manager:
manager.render()
"""
def __init__(self, scene_cls: type[Scene_co]) -> None:
@ -82,6 +86,20 @@ class Manager(Generic[Scene_co]):
# internal state
self._skipping = False
def __str__(self) -> str:
return f"{self.__class__.__name__}({self.scene!r}) at time {self.time:.2f}s"
def __enter__(self) -> Manager[Scene_co]:
return self
def __exit__(
self,
exc_type: type[BaseException] | None,
exc_value: BaseException | None,
traceback: TracebackType | None,
) -> None:
self.release()
# keep these as instance methods so subclasses
# have access to everything
def create_renderer(self) -> RendererProtocol:
@ -148,13 +166,12 @@ class Manager(Generic[Scene_co]):
self.play(Create(Circle()))
with tempconfig({"preview": True}):
Manager(MyScene).render()
with tempconfig({"preview": True}), Manager(MyScene) as manager:
manager.render()
"""
config._warn_about_config_options()
self._render_first_pass()
self._render_second_pass()
self.release()
def _render_first_pass(self) -> None:
"""
@ -165,7 +182,7 @@ class Manager(Generic[Scene_co]):
with contextlib.suppress(EndSceneEarlyException):
self.construct()
self.post_contruct()
self.post_construct()
self._interact()
self.tear_down()
@ -192,7 +209,7 @@ class Manager(Generic[Scene_co]):
def release(self) -> None:
self.renderer.release()
def post_contruct(self) -> None:
def post_construct(self) -> None:
"""Run post-construct hooks, and clean up the file writer."""
if self.file_writer.num_plays:
self.file_writer.finish()
@ -407,6 +424,7 @@ class Manager(Generic[Scene_co]):
break
else:
self.time += dt
self.scene.time = self.time
self.renderer.render(state)
if self.window is not None and self.window.is_closing:
raise EndSceneEarlyException()

View file

@ -452,7 +452,9 @@ class OpenGLRenderer(Renderer, RendererProtocol):
def release(self) -> None:
self.ctx.release()
# print("self.output_fbo.mglo: ", self.output_fbo.mglo)
self.output_fbo.release()
# print("self.output_fbo.mglo: ", self.output_fbo.mglo)
class GLVMobjectManager:

View file

@ -58,7 +58,7 @@ class Window(PygletWindow, WindowProtocol):
)
super().__init__(size=size)
self.pressed_keys = set()
self.pressed_keys: set = set()
self.title = f"Manim Community {__version__}"
self.size = size

View file

@ -26,7 +26,7 @@ class Renderer(ABC):
to :meth:`render_image`, etc.
"""
def __init__(self):
def __init__(self) -> None:
self.capabilities = [
(OpenGLVMobject, self.render_vmobject),
(ImageMobject, self.render_image),
@ -42,7 +42,7 @@ class Renderer(ABC):
def render_mobject(self, mob: OpenGLMobject) -> None:
for mob_cls, render_func in self.capabilities:
if isinstance(mob, mob_cls):
render_func(mob)
render_func(mob) # type: ignore[operator]
break
else:
if not isinstance(mob, InvisibleMobject):

View file

@ -230,7 +230,11 @@ class Scene:
index = self.mobjects.index(mobject)
self.mobjects = [
*self.mobjects[:index],
*replacements,
*[
replacement
for replacement in replacements
if replacement not in self.mobjects
],
*self.mobjects[index + 1 :],
]
return self

View file

@ -209,13 +209,13 @@ def _make_test_comparing_frames(
base_scene=base_scene,
construct_test=construct,
)
manager = Manager(scene_tested)
manager.file_writer = file_writer_class(
manager.scene.get_default_scene_name()
)
manager.render()
if last_frame:
frames_tester.check_frame(-1, manager.renderer.get_pixels())
with Manager(scene_tested) as manager:
manager.file_writer = file_writer_class(
manager.scene.get_default_scene_name()
)
manager.render()
if last_frame:
frames_tester.check_frame(-1, manager.renderer.get_pixels())
return real_test

View file

@ -64,12 +64,30 @@ ignore_errors = True
[mypy-manim.animation.animation]
ignore_errors = True
[mypy-manim.animation.changing]
ignore_errors = True
[mypy-manim.animation.composition]
ignore_errors = True
[mypy-manim.animation.creation]
ignore_errors = True
[mypy-manim.animation.fading]
ignore_errors = True
[mypy-manim.animation.growing]
ignore_errors = True
[mypy-manim.animation.indication]
ignore_errors = True
[mypy-manim.animation.movement]
ignore_errors = True
[mypy-manim.animation.numbers]
ignore_errors = True
[mypy-manim.animation.specialized]
ignore_errors = True
@ -82,6 +100,9 @@ ignore_errors = True
[mypy-manim.animation.transform]
ignore_errors = True
[mypy-manim.animation.updaters.update]
ignore_errors = True
[mypy-manim.animation.updaters.mobject_update_utils]
ignore_errors = True
@ -91,15 +112,33 @@ ignore_errors = True
[mypy-manim.camera.mapping_camera]
ignore_errors = True
[mypy-manim.cli.checkhealth.commands]
ignore_errors = True
[mypy-manim.cli.default_group]
ignore_errors = True
[mypy-manim.mobject.frame]
ignore_errors = True
[mypy-manim.mobject.geometry.arc]
ignore_errors = True
[mypy-manim.mobject.geometry.boolean_ops]
ignore_errors = True
[mypy-manim.mobject.geometry.labeled]
ignore_errors = True
[mypy-manim.mobject.geometry.line]
ignore_errors = True
[mypy-manim.mobject.geometry.polygram]
ignore_errors = True
[mypy-manim.mobject.geometry.shape_matchers]
ignore_errors = True
[mypy-manim.mobject.graphing.coordinate_systems]
ignore_errors = True
@ -112,6 +151,18 @@ ignore_errors = True
[mypy-manim.mobject.mobject]
ignore_errors = True
[mypy-manim.mobject.text.tex_mobject]
ignore_errors = True
[mypy-manim.mobject.text.text_mobject]
ignore_errors = True
[mypy-manim.mobject.opengl.dot_cloud]
ignore_errors = True
[mypy-manim.mobject.opengl.shader]
ignore_errors = True
[mypy-manim.mobject.opengl.opengl_compatibility]
ignore_errors = True
@ -142,18 +193,39 @@ ignore_errors = True
[mypy-manim.mobject.vector_field]
ignore_errors = True
[mypy-manim.renderer.buffers.buffer]
ignore_errors = True
[mypy-manim.renderer.cairo_renderer]
ignore_errors = True
[mypy-manim.renderer.opengl_renderer]
ignore_errors = True
[mypy-manim.renderer.opengl_shader_program]
ignore_errors = True
[mypy-manim.renderer.shader_wrapper]
ignore_errors = True
[mypy-manim.scene.three_d_scene]
ignore_errors = True
[mypy-manim.utils.caching]
ignore_errors = True
[mypy-manim.utils.directories]
ignore_errors = True
[mypy-manim.utils.family_ops]
ignore_errors = True
[mypy-manim.utils.hashing]
ignore_errors = True
[mypy-manim.utils.testing.frames_comparison]
ignore_errors = True
# Added temporarily due to current mypy failures
[mypy-manim.camera.three_d_camera]
ignore_errors = True
@ -221,18 +293,27 @@ ignore_errors = True
[mypy-manim.utils.commands]
ignore_errors = True
[mypy-manim.utils.debug]
ignore_errors = True
[mypy-manim.utils.images]
ignore_errors = True
[mypy-manim.utils.iterables]
ignore_errors = True
[mypy-manim.utils.module_ops]
ignore_errors = True
[mypy-manim.utils.opengl]
ignore_errors = True
[mypy-manim.utils.paths]
ignore_errors = True
[mypy-manim.utils.progressbar]
ignore_errors = True
[mypy-manim.utils.space_ops]
ignore_errors = True

View file

@ -43,9 +43,9 @@ def set_test_scene(scene_object: type[Scene], module_name: str, config):
temp_path = Path(tmpdir)
config["text_dir"] = temp_path / "text"
config["tex_dir"] = temp_path / "tex"
manager = Manager(scene_object)
manager.render()
data = manager.renderer.get_pixels()
with Manager(scene_object) as manager:
manager.render()
data = manager.renderer.get_pixels()
assert not np.all(
data == np.array([0, 0, 0, 255]),

View file

@ -40,22 +40,13 @@ def test_wait_invalid_duration(duration):
test_scene.wait(duration)
@pytest.mark.parametrize("frozen_frame", [False, True])
def test_wait_duration_shorter_than_frame_rate(manim_caplog, frozen_frame):
def test_wait_duration_shorter_than_frame_rate(manim_caplog):
manager = Manager(Scene)
test_scene = manager.scene
test_scene.wait(1e-9, frozen_frame=frozen_frame)
test_scene.wait(1e-9)
assert "too short for the current frame rate" in manim_caplog.text
@pytest.mark.parametrize("duration", [0, -1])
def test_pause_invalid_duration(duration):
manager = Manager(Scene)
test_scene = manager.scene
with pytest.raises(ValueError, match="The duration must be a positive number."):
test_scene.pause(duration)
@pytest.mark.parametrize("max_time", [0, -1])
def test_wait_until_invalid_max_time(max_time):
manager = Manager(Scene)

View file

@ -189,7 +189,8 @@ def test_animationgroup_calls_finish():
def finish(self):
self.finished = True
scene = Scene()
manager = Manager(Scene)
scene = manager.scene
sqr_animation = MyAnimation(Square())
circ_animation = MyAnimation(Circle())
animation_group = AnimationGroup(sqr_animation, circ_animation)

View file

@ -1,10 +1,11 @@
from __future__ import annotations
from manim import Circle, ReplacementTransform, Scene, Square, VGroup
from manim import Circle, Manager, ReplacementTransform, Scene, Square, VGroup
def test_no_duplicate_references():
scene = Scene()
manager = Manager(Scene)
scene = manager.scene
c = Circle()
sq = Square()
scene.add(c, sq)
@ -15,7 +16,8 @@ def test_no_duplicate_references():
def test_duplicate_references_in_group():
scene = Scene()
manager = Manager(Scene)
scene = manager.scene
c = Circle()
sq = Square()
vg = VGroup(c, sq)

View file

@ -1,23 +0,0 @@
from __future__ import annotations
from manim import *
def test_zoom():
s1 = Square()
s1.set_x(-10)
s2 = Square()
s2.set_x(10)
with tempconfig({"dry_run": True, "quality": "low_quality"}):
manager = Manager(MovingCameraScene)
scene = manager.scene
scene.add(s1, s2)
scene.play(scene.camera.auto_zoom([s1, s2]))
assert scene.camera.frame_width == abs(
s1.get_left()[0] - s2.get_right()[0],
)
assert scene.camera.frame.get_center()[0] == (
abs(s1.get_center()[0] + s2.get_center()[0]) / 2
)

View file

@ -4,7 +4,7 @@ import datetime
import pytest
from manim import Circle, FadeIn, Group, Manager, Mobject, Scene, Square
from manim import Circle, FadeIn, Group, Manager, OpenGLMobject, Scene, Square
from manim.animation.animation import Wait
@ -12,27 +12,26 @@ def test_scene_add_remove(dry_run):
manager = Manager(Scene)
scene = manager.scene
assert len(scene.mobjects) == 0
scene.add(Mobject())
scene.add(OpenGLMobject())
assert len(scene.mobjects) == 1
scene.add(*(Mobject() for _ in range(10)))
scene.add(*(OpenGLMobject() for _ in range(10)))
assert len(scene.mobjects) == 11
# Check that adding a mobject twice does not actually add it twice
repeated = Mobject()
repeated = OpenGLMobject()
scene.add(repeated)
assert len(scene.mobjects) == 12
scene.add(repeated)
assert len(scene.mobjects) == 12
# Check that Scene.add() returns the Scene (for chained calls)
assert scene.add(Mobject()) is scene
assert scene.add(OpenGLMobject()) is scene
manager = Manager(Scene)
scene = manager.scene
to_remove = Mobject()
scene = Scene()
to_remove = OpenGLMobject()
scene.add(to_remove)
scene.add(*(Mobject() for _ in range(10)))
scene.add(*(OpenGLMobject() for _ in range(10)))
assert len(scene.mobjects) == 11
scene.remove(to_remove)
assert len(scene.mobjects) == 10
@ -40,7 +39,7 @@ def test_scene_add_remove(dry_run):
assert len(scene.mobjects) == 10
# Check that Scene.remove() returns the instance (for chained calls)
assert scene.add(Mobject()) is scene
assert scene.add(OpenGLMobject()) is scene
def test_scene_time(dry_run):
@ -87,10 +86,10 @@ def test_replace(dry_run):
manager = Manager(Scene)
scene = manager.scene
first = Mobject(name="first")
second = Mobject(name="second")
third = Mobject(name="third")
fourth = Mobject(name="fourth")
first = OpenGLMobject(name="first")
second = OpenGLMobject(name="second")
third = OpenGLMobject(name="third")
fourth = OpenGLMobject(name="fourth")
scene.add(first)
scene.add(Group(second, third, name="group"))
@ -98,8 +97,8 @@ def test_replace(dry_run):
assert_names(scene.mobjects, ["first", "group", "fourth"])
assert_names(scene.mobjects[1], ["second", "third"])
alpha = Mobject(name="alpha")
beta = Mobject(name="beta")
alpha = OpenGLMobject(name="alpha")
beta = OpenGLMobject(name="beta")
scene.replace(first, alpha)
assert_names(scene.mobjects, ["alpha", "group", "fourth"])

View file

@ -61,16 +61,16 @@ def test_transparent(config):
config.verbosity = "ERROR"
config.dry_run = True
manager = Manager(MyScene)
manager.render()
frame = manager.renderer.get_pixels()
with Manager(MyScene) as manager:
manager.render()
frame = manager.renderer.get_pixels()
np.testing.assert_allclose(frame[0, 0], [0, 0, 0, 255])
config.transparent = True
manager = Manager(MyScene)
manager.render()
frame = manager.renderer.get_pixels()
with Manager(MyScene) as manager:
manager.render()
frame = manager.renderer.get_pixels()
np.testing.assert_allclose(frame[0, 0], [0, 0, 0, 0])
@ -78,9 +78,9 @@ def test_transparent_by_background_opacity(config, dry_run):
config.background_opacity = 0.5
assert config.transparent is True
manager = Manager(MyScene)
manager.render()
frame = manager.renderer.get_pixels()
with Manager(MyScene) as manager:
manager.render()
frame = manager.renderer.get_pixels()
np.testing.assert_allclose(frame[0, 0], [0, 0, 0, 127])
assert config.movie_file_extension == ".mov"
assert config.transparent is True
@ -92,9 +92,9 @@ def test_background_color(config):
config.verbosity = "ERROR"
config.dry_run = True
manager = Manager(MyScene)
manager.render()
frame = manager.renderer.get_pixels()
with Manager(MyScene) as manager:
manager.render()
frame = manager.renderer.get_pixels()
np.testing.assert_allclose(frame[0, 0], [255, 255, 255, 255])
@ -134,8 +134,8 @@ def test_custom_dirs(tmp_path, config):
config.tex_dir = "{media_dir}/test_tex"
config.log_dir = "{media_dir}/test_log"
manager = Manager(MyScene)
manager.render()
with Manager(MyScene) as manager:
manager.render()
tmp_path = Path(tmp_path)
assert_dir_filled(tmp_path / "test_sections")
assert_file_exists(tmp_path / "test_sections/MyScene.json")
@ -215,8 +215,8 @@ def test_dry_run_with_png_format(config, dry_run):
config.write_to_movie = False
config.disable_caching = True
assert config.dry_run is True
manager = Manager(MyScene)
manager.render()
with Manager(MyScene) as manager:
manager.render()
def test_dry_run_with_png_format_skipped_animations(config, dry_run):
@ -224,8 +224,8 @@ def test_dry_run_with_png_format_skipped_animations(config, dry_run):
config.write_to_movie = False
config.disable_caching = True
assert config["dry_run"] is True
manager = Manager(MyScene)
manager.render()
with Manager(MyScene) as manager:
manager.render()
def test_tex_template_file(tmp_path):

View file

@ -28,10 +28,10 @@ from .simple_scenes import (
def test_t_values(config, using_temp_config, disabling_caching, frame_rate):
"""Test that the framerate corresponds to the number of times animations are updated"""
config.frame_rate = frame_rate
manager = Manager(SquareToCircle)
scene = manager.scene
scene._update_animations = Mock()
manager.render()
with Manager(SquareToCircle) as manager:
scene = manager.scene
scene._update_animations = Mock()
manager.render()
assert scene._update_animations.call_count == config["frame_rate"]
np.testing.assert_allclose(
([call.args[1] for call in scene._update_animations.call_args_list]),