From 32ab690932b4328cd7bdc2a2fc58ab5d59143195 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Mart=C3=ADn?= Date: Tue, 21 Jan 2025 11:27:21 +0100 Subject: [PATCH] feat(lib): add `skip_animations` compatibility (#516) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Add skip_animations compatibility * Add tests, config and changelog * chore(fmt): auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Update manim_slides/slide/base.py Co-authored-by: Jérome Eertmans * chore(fmt): auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * chore(tests): implement tests --------- Co-authored-by: Jérome Eertmans Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- CHANGELOG.md | 6 +++ manim_slides/config.py | 1 + manim_slides/slide/base.py | 16 +++++++- manim_slides/slide/manim.py | 13 ++++++- tests/test_slide.py | 73 ++++++++++++++++++++++++++++++++++++- uv.lock | 3 +- 6 files changed, 107 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index af79510..b21b668 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 (unreleased)= ## [Unreleased](https://github.com/jeertmans/manim-slides/compare/v5.3.1...HEAD) +(unreleased-added)= +### Added + +- Added `skip_animations` compatibility with ManimCE. + [@Rapsssito](https://github.com/Rapsssito) [#516](https://github.com/jeertmans/manim-slides/pull/516) + (v5.3.1)= ## [v5.3.1](https://github.com/jeertmans/manim-slides/compare/v5.3.0...v5.3.1) diff --git a/manim_slides/config.py b/manim_slides/config.py index 987a1e0..6f6a8d2 100644 --- a/manim_slides/config.py +++ b/manim_slides/config.py @@ -160,6 +160,7 @@ class BaseSlideConfig(BaseModel): # type: ignore reversed_playback_rate: float = 1.0 notes: str = "" dedent_notes: bool = True + skip_animations: bool = False @classmethod def wrapper(cls, arg_name: str) -> Callable[..., Any]: diff --git a/manim_slides/slide/base.py b/manim_slides/slide/base.py index 9f47635..cc728de 100644 --- a/manim_slides/slide/base.py +++ b/manim_slides/slide/base.py @@ -277,7 +277,7 @@ class BaseSlide: self._wait_time_between_slides = max(wait_time, 0.0) def play(self, *args: Any, **kwargs: Any) -> None: - """Overload `self.play` and increment animation count.""" + """Overload 'self.play' and increment animation count.""" super().play(*args, **kwargs) # type: ignore[misc] self._current_animation += 1 @@ -299,6 +299,11 @@ class BaseSlide: Positional arguments passed to :meth:`Scene.next_section`, or ignored if `manimlib` API is used. + :param skip_animations: + Exclude the next slide from the output. + + If `manim` is used, this is also passed to `:meth:`Scene.next_section`, + which will avoid rendering the corresponding animations. :param loop: If set, next slide will be looping. :param auto_next: @@ -521,9 +526,16 @@ class BaseSlide: ascii=True if platform.system() == "Windows" else None, disable=not self._show_progress_bar, ): + if pre_slide_config.skip_animations: + continue slide_files = files[pre_slide_config.slides_slice] - file = merge_basenames(slide_files) + try: + file = merge_basenames(slide_files) + except ValueError as e: + raise ValueError( + f"Failed to merge basenames of files for slide: {pre_slide_config!r}" + ) from e dst_file = scene_files_folder / file.name rev_file = scene_files_folder / f"{file.stem}_reversed{file.suffix}" diff --git a/manim_slides/slide/manim.py b/manim_slides/slide/manim.py index 21e5083..2d5ef07 100644 --- a/manim_slides/slide/manim.py +++ b/manim_slides/slide/manim.py @@ -89,6 +89,15 @@ class Slide(BaseSlide, Scene): # type: ignore[misc] def _start_at_animation_number(self) -> Optional[int]: return config["from_animation_number"] # type: ignore + def play(self, *args: Any, **kwargs: Any) -> None: + """Overload 'self.play' and increment animation count.""" + super().play(*args, **kwargs) + + if self._base_slide_config.skip_animations: + # Manim will not render the animations, so we reset the animation + # counter to the previous value + self._current_animation -= 1 + def next_section(self, *args: Any, **kwargs: Any) -> None: """ Alias to :meth:`next_slide`. @@ -111,7 +120,9 @@ class Slide(BaseSlide, Scene): # type: ignore[misc] base_slide_config: BaseSlideConfig, **kwargs: Any, ) -> None: - Scene.next_section(self, *args, **kwargs) + Scene.next_section( + self, *args, skip_animations=base_slide_config.skip_animations, **kwargs + ) BaseSlide.next_slide.__wrapped__( self, base_slide_config=base_slide_config, diff --git a/tests/test_slide.py b/tests/test_slide.py index acf68f9..95a7252 100644 --- a/tests/test_slide.py +++ b/tests/test_slide.py @@ -1,6 +1,11 @@ +import contextlib +import os import random import shutil import sys +import tempfile +import threading +from collections.abc import Iterator from pathlib import Path from typing import Any, Union @@ -17,6 +22,7 @@ from manim import ( Dot, FadeIn, GrowFromCenter, + Square, Text, ) from manim.renderer.opengl_renderer import OpenGLRenderer @@ -229,8 +235,26 @@ def assert_constructs(cls: SlideType) -> None: init_slide(cls).construct() +_L = ( + threading.Lock() +) # We cannot change directory multiple times at once (in the same thread) + + +@contextlib.contextmanager +def tmp_cwd() -> Iterator[str]: + old_cwd = os.getcwd() + + with tempfile.TemporaryDirectory() as tmp_dir, _L: + try: + os.chdir(tmp_dir) + yield tmp_dir + finally: + os.chdir(old_cwd) + + def assert_renders(cls: SlideType) -> None: - init_slide(cls).render() + with tmp_cwd(): + init_slide(cls).render() class TestSlide: @@ -479,6 +503,53 @@ class TestSlide: self.next_slide() assert self._current_slide == 2 + def test_next_slide_skip_animations(self) -> None: + class Foo(CESlide): + def construct(self) -> None: + circle = Circle(color=BLUE) + self.play(GrowFromCenter(circle)) + assert not self._base_slide_config.skip_animations + self.next_slide(skip_animations=True) + square = Square(color=BLUE) + self.play(GrowFromCenter(square)) + assert self._base_slide_config.skip_animations + self.next_slide() + assert not self._base_slide_config.skip_animations + self.play(GrowFromCenter(square)) + + class Bar(CESlide): + def construct(self) -> None: + circle = Circle(color=BLUE) + self.play(GrowFromCenter(circle)) + assert not self._base_slide_config.skip_animations + self.next_slide(skip_animations=False) + square = Square(color=BLUE) + self.play(GrowFromCenter(square)) + assert not self._base_slide_config.skip_animations + self.next_slide() + assert not self._base_slide_config.skip_animations + self.play(GrowFromCenter(square)) + + with tmp_cwd() as tmp_dir: + init_slide(Foo).render() + init_slide(Bar).render() + + slides_folder = Path(tmp_dir) / "slides" + + assert slides_folder.exists() + + slide_file = slides_folder / "Foo.json" + + config = PresentationConfig.from_file(slide_file) + + assert len(config.slides) == 2 + + slide_file = slides_folder / "Bar.json" + + config = PresentationConfig.from_file(slide_file) + + assert len(config.slides) == 3 + def test_canvas(self) -> None: @assert_constructs class _(CESlide): diff --git a/uv.lock b/uv.lock index 319a811..cfbc770 100644 --- a/uv.lock +++ b/uv.lock @@ -1482,7 +1482,6 @@ wheels = [ [[package]] name = "manim-slides" -version = "5.3.1" source = { editable = "." } dependencies = [ { name = "av" }, @@ -2762,6 +2761,8 @@ name = "pyqt6-qt6" version = "6.8.1" source = { registry = "https://pypi.org/simple" } wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/19/b89eb6cecbdf1e65a44658a083693a967e9d428370026711b624e928a8ca/PyQt6_Qt6-6.8.1-1-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:2f4b8b55b1414b93f340f22e8c88d25550efcdebc4b65a3927dd947b73bd4358", size = 80877444 }, + { url = "https://files.pythonhosted.org/packages/87/1b/94d3710ee7ef93ee99c1dac512f631de5e310f6b21e43f474ef269f840b6/PyQt6_Qt6-6.8.1-1-py3-none-manylinux_2_39_aarch64.whl", hash = "sha256:98aa99fe38ae68c5318284cd28f3479ba538c40bf6ece293980abae0925c1b24", size = 79473622 }, { url = "https://files.pythonhosted.org/packages/df/0a/c47a1cc272b418faff8af79b121f0cecd32b09d634253254e3a990432220/PyQt6_Qt6-6.8.1-py3-none-macosx_10_14_x86_64.whl", hash = "sha256:1eb8460a1fdb38d0b2458c2974c01d471c1e59e4eb19ea63fc447aaba3ad530e", size = 65754939 }, { url = "https://files.pythonhosted.org/packages/b1/e6/cc4fbc97a7d0955185e33add3ce00480f0023424d17ac6f864a504f60251/PyQt6_Qt6-6.8.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:9f3790c4ce4dc576e48b8718d55fb8743057e6cbd53a6ca1dd253ffbac9b7287", size = 59956028 }, { url = "https://files.pythonhosted.org/packages/01/22/c2997fe76d765d9ba960e9a099238cb419a316362bdde50fedacc23e7c7d/PyQt6_Qt6-6.8.1-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:d6ca5d2b9d2ec0ee4a814b2175f641a5c4299cb80b45e0f5f8356632663f89b3", size = 72561636 },