mirror of
https://github.com/jeertmans/manim-slides.git
synced 2025-05-23 21:46:49 +08:00
chore(lib): simplify how to add config options (#321)
* chore(lib): simplify how to add config options * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * chore(lint): some fixes --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
@ -22,6 +22,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
to slide config.
|
||||
[#320](https://github.com/jeertmans/manim-slides/pull/320)
|
||||
|
||||
(v5.1-modified)=
|
||||
### Modified
|
||||
|
||||
- Modified the internal logic to simplify adding configuration options.
|
||||
[#321](https://github.com/jeertmans/manim-slides/pull/321)
|
||||
|
||||
## [v5](https://github.com/jeertmans/manim-slides/compare/v4.16.0...v5.0.0)
|
||||
|
||||
Prior to v5, there was no real CHANGELOG other than the GitHub releases,
|
||||
|
@ -1,5 +1,7 @@
|
||||
import json
|
||||
import shutil
|
||||
from functools import wraps
|
||||
from inspect import Parameter, signature
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, Dict, List, Optional, Set, Tuple
|
||||
|
||||
@ -135,14 +137,76 @@ class Config(BaseModel): # type: ignore[misc]
|
||||
return self
|
||||
|
||||
|
||||
class PreSlideConfig(BaseModel): # type: ignore
|
||||
start_animation: int
|
||||
end_animation: int
|
||||
class BaseSlideConfig(BaseModel): # type: ignore
|
||||
"""Base class for slide config."""
|
||||
|
||||
loop: bool = False
|
||||
auto_next: bool = False
|
||||
playback_rate: float = 1.0
|
||||
reversed_playback_rate: float = 1.0
|
||||
|
||||
@classmethod
|
||||
def wrapper(cls, arg_name: str) -> Callable[..., Any]:
|
||||
"""
|
||||
Wrap a function to transform keyword argument into an instance of this class.
|
||||
|
||||
The function signature is updated to reflect the new keyword-only arguments.
|
||||
|
||||
The wrapped function must follow two criteria:
|
||||
- its last parameter must be ``**kwargs`` (or equivalent);
|
||||
- and its second last parameter must be ``<arg_name>``.
|
||||
"""
|
||||
|
||||
def _wrapper_(fun: Callable[..., Any]) -> Callable[..., Any]:
|
||||
@wraps(fun)
|
||||
def __wrapper__(*args: Any, **kwargs: Any) -> Any: # noqa: N807
|
||||
fun_kwargs = {
|
||||
key: value
|
||||
for key, value in kwargs.items()
|
||||
if key not in cls.__fields__
|
||||
}
|
||||
fun_kwargs[arg_name] = cls(**kwargs)
|
||||
return fun(*args, **fun_kwargs)
|
||||
|
||||
sig = signature(fun)
|
||||
parameters = list(sig.parameters.values())
|
||||
parameters[-2:-1] = [
|
||||
Parameter(
|
||||
field_name,
|
||||
Parameter.KEYWORD_ONLY,
|
||||
default=field_info.default,
|
||||
annotation=field_info.annotation,
|
||||
)
|
||||
for field_name, field_info in cls.__fields__.items()
|
||||
]
|
||||
|
||||
sig = sig.replace(parameters=parameters)
|
||||
__wrapper__.__signature__ = sig # type: ignore[attr-defined]
|
||||
|
||||
return __wrapper__
|
||||
|
||||
return _wrapper_
|
||||
|
||||
|
||||
class PreSlideConfig(BaseSlideConfig):
|
||||
"""Slide config to be used prior to rendering."""
|
||||
|
||||
start_animation: int
|
||||
end_animation: int
|
||||
|
||||
@classmethod
|
||||
def from_base_slide_config_and_animation_indices(
|
||||
cls,
|
||||
base_slide_config: BaseSlideConfig,
|
||||
start_animation: int,
|
||||
end_animation: int,
|
||||
) -> "PreSlideConfig":
|
||||
return cls(
|
||||
start_animation=start_animation,
|
||||
end_animation=end_animation,
|
||||
**base_slide_config.dict(),
|
||||
)
|
||||
|
||||
@field_validator("start_animation", "end_animation")
|
||||
@classmethod
|
||||
def index_is_posint(cls, v: int) -> int:
|
||||
@ -187,26 +251,17 @@ class PreSlideConfig(BaseModel): # type: ignore
|
||||
return slice(self.start_animation, self.end_animation)
|
||||
|
||||
|
||||
class SlideConfig(BaseModel): # type: ignore[misc]
|
||||
class SlideConfig(BaseSlideConfig):
|
||||
"""Slide config to be used after rendering."""
|
||||
|
||||
file: FilePath
|
||||
rev_file: FilePath
|
||||
loop: bool = False
|
||||
auto_next: bool = False
|
||||
playback_rate: float = 1.0
|
||||
reversed_playback_rate: float = 1.0
|
||||
|
||||
@classmethod
|
||||
def from_pre_slide_config_and_files(
|
||||
cls, pre_slide_config: PreSlideConfig, file: Path, rev_file: Path
|
||||
) -> "SlideConfig":
|
||||
return cls(
|
||||
file=file,
|
||||
rev_file=rev_file,
|
||||
loop=pre_slide_config.loop,
|
||||
auto_next=pre_slide_config.auto_next,
|
||||
playback_rate=pre_slide_config.playback_rate,
|
||||
reversed_playback_rate=pre_slide_config.reversed_playback_rate,
|
||||
)
|
||||
return cls(file=file, rev_file=rev_file, **pre_slide_config.dict())
|
||||
|
||||
|
||||
class PresentationConfig(BaseModel): # type: ignore[misc]
|
||||
|
@ -217,7 +217,9 @@ def start_at_callback(
|
||||
metavar="RATE",
|
||||
type=float,
|
||||
default=1.0,
|
||||
help="Playback rate of the video slides, see PySide6 docs for details.",
|
||||
help="Playback rate of the video slides, see PySide6 docs for details. "
|
||||
" The playback rate of each slide is defined as the product of its default "
|
||||
" playback rate and the provided value.",
|
||||
)
|
||||
@click.option(
|
||||
"--next-terminates-loop",
|
||||
|
@ -104,7 +104,7 @@ class Player(QMainWindow): # type: ignore[misc]
|
||||
|
||||
self.media_player = QMediaPlayer(self)
|
||||
self.media_player.setVideoOutput(self.video_widget)
|
||||
self.media_player.setPlaybackRate(playback_rate)
|
||||
self.playback_rate = playback_rate
|
||||
|
||||
self.presentation_changed.connect(self.presentation_changed_callback)
|
||||
self.slide_changed.connect(self.slide_changed_callback)
|
||||
@ -238,10 +238,12 @@ class Player(QMainWindow): # type: ignore[misc]
|
||||
|
||||
if self.playing_reversed_slide:
|
||||
self.media_player.setPlaybackRate(
|
||||
self.current_slide_config.reversed_playback_rate
|
||||
self.current_slide_config.reversed_playback_rate * self.playback_rate
|
||||
)
|
||||
else:
|
||||
self.media_player.setPlaybackRate(self.current_slide_config.playback_rate)
|
||||
self.media_player.setPlaybackRate(
|
||||
self.current_slide_config.playback_rate * self.playback_rate
|
||||
)
|
||||
|
||||
if start_paused:
|
||||
self.media_player.pause()
|
||||
|
@ -8,7 +8,7 @@ from typing import Any, List, MutableMapping, Optional, Sequence, Tuple, ValuesV
|
||||
import numpy as np
|
||||
from tqdm import tqdm
|
||||
|
||||
from ..config import PresentationConfig, PreSlideConfig, SlideConfig
|
||||
from ..config import BaseSlideConfig, PresentationConfig, PreSlideConfig, SlideConfig
|
||||
from ..defaults import FFMPEG_BIN, FOLDER_PATH
|
||||
from ..logger import logger
|
||||
from ..utils import concatenate_video_files, merge_basenames, reverse_video_file
|
||||
@ -29,7 +29,7 @@ class BaseSlide:
|
||||
super().__init__(*args, **kwargs)
|
||||
self._output_folder: Path = output_folder
|
||||
self._slides: List[PreSlideConfig] = []
|
||||
self._pre_slide_config_kwargs: MutableMapping[str, Any] = {}
|
||||
self._base_slide_config: BaseSlideConfig = BaseSlideConfig()
|
||||
self._current_slide = 1
|
||||
self._current_animation = 0
|
||||
self._start_animation = 0
|
||||
@ -254,13 +254,11 @@ class BaseSlide:
|
||||
super().play(*args, **kwargs) # type: ignore[misc]
|
||||
self._current_animation += 1
|
||||
|
||||
@BaseSlideConfig.wrapper("base_slide_config")
|
||||
def next_slide(
|
||||
self,
|
||||
*,
|
||||
loop: bool = False,
|
||||
auto_next: bool = False,
|
||||
playback_rate: float = 1.0,
|
||||
reversed_playback_rate: float = 1.0,
|
||||
base_slide_config: BaseSlideConfig,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""
|
||||
@ -380,21 +378,16 @@ class BaseSlide:
|
||||
self.wait(self.wait_time_between_slides) # type: ignore[attr-defined]
|
||||
|
||||
self._slides.append(
|
||||
PreSlideConfig(
|
||||
start_animation=self._start_animation,
|
||||
end_animation=self._current_animation,
|
||||
**self._pre_slide_config_kwargs,
|
||||
PreSlideConfig.from_base_slide_config_and_animation_indices(
|
||||
self._base_slide_config,
|
||||
self._start_animation,
|
||||
self._current_animation,
|
||||
)
|
||||
)
|
||||
|
||||
self._current_slide += 1
|
||||
|
||||
self._pre_slide_config_kwargs = dict(
|
||||
loop=loop,
|
||||
auto_next=auto_next,
|
||||
playback_rate=playback_rate,
|
||||
reversed_playback_rate=reversed_playback_rate,
|
||||
)
|
||||
self._base_slide_config = base_slide_config
|
||||
self._start_animation = self._current_animation
|
||||
|
||||
def _add_last_slide(self) -> None:
|
||||
@ -406,10 +399,10 @@ class BaseSlide:
|
||||
return
|
||||
|
||||
self._slides.append(
|
||||
PreSlideConfig(
|
||||
start_animation=self._start_animation,
|
||||
end_animation=self._current_animation,
|
||||
**self._pre_slide_config_kwargs,
|
||||
PreSlideConfig.from_base_slide_config_and_animation_indices(
|
||||
self._base_slide_config,
|
||||
self._start_animation,
|
||||
self._current_animation,
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -3,6 +3,7 @@ from typing import Any, List, Optional, Tuple
|
||||
|
||||
from manim import Scene, ThreeDScene, config
|
||||
|
||||
from ..config import BaseSlideConfig
|
||||
from .base import BaseSlide
|
||||
|
||||
|
||||
@ -79,22 +80,17 @@ class Slide(BaseSlide, Scene): # type: ignore[misc]
|
||||
"""
|
||||
self.next_slide(*args, **kwargs)
|
||||
|
||||
@BaseSlideConfig.wrapper("base_slide_config")
|
||||
def next_slide(
|
||||
self,
|
||||
*args: Any,
|
||||
loop: bool = False,
|
||||
auto_next: bool = False,
|
||||
playback_rate: float = 1.0,
|
||||
reversed_playback_rate: float = 1.0,
|
||||
base_slide_config: BaseSlideConfig,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
Scene.next_section(self, *args, **kwargs)
|
||||
BaseSlide.next_slide(
|
||||
BaseSlide.next_slide.__wrapped__(
|
||||
self,
|
||||
loop=loop,
|
||||
auto_next=auto_next,
|
||||
playback_rate=playback_rate,
|
||||
reversed_playback_rate=reversed_playback_rate,
|
||||
base_slide_config=base_slide_config,
|
||||
)
|
||||
|
||||
def render(self, *args: Any, **kwargs: Any) -> None:
|
||||
|
@ -140,16 +140,16 @@ class TestSlide:
|
||||
|
||||
self.add(text)
|
||||
|
||||
assert "loop" not in self._pre_slide_config_kwargs
|
||||
assert not self._base_slide_config.loop
|
||||
|
||||
self.next_slide(loop=True)
|
||||
self.play(text.animate.scale(2))
|
||||
|
||||
assert self._pre_slide_config_kwargs["loop"]
|
||||
assert self._base_slide_config.loop
|
||||
|
||||
self.next_slide(loop=False)
|
||||
|
||||
assert not self._pre_slide_config_kwargs["loop"]
|
||||
assert not self._base_slide_config.loop
|
||||
|
||||
@assert_constructs
|
||||
class TestAutoNext(Slide):
|
||||
@ -158,16 +158,16 @@ class TestSlide:
|
||||
|
||||
self.add(text)
|
||||
|
||||
assert "auto_next" not in self._pre_slide_config_kwargs
|
||||
assert not self._base_slide_config.auto_next
|
||||
|
||||
self.next_slide(auto_next=True)
|
||||
self.play(text.animate.scale(2))
|
||||
|
||||
assert self._pre_slide_config_kwargs["auto_next"]
|
||||
assert self._base_slide_config.auto_next
|
||||
|
||||
self.next_slide(auto_next=False)
|
||||
|
||||
assert not self._pre_slide_config_kwargs["auto_next"]
|
||||
assert not self._base_slide_config.auto_next
|
||||
|
||||
@assert_constructs
|
||||
class TestLoopAndAutoNextFails(Slide):
|
||||
@ -189,12 +189,12 @@ class TestSlide:
|
||||
|
||||
self.add(text)
|
||||
|
||||
assert "playback_rate" not in self._pre_slide_config_kwargs
|
||||
assert self._base_slide_config.playback_rate == 1.0
|
||||
|
||||
self.next_slide(playback_rate=2.0)
|
||||
self.play(text.animate.scale(2))
|
||||
|
||||
assert self._pre_slide_config_kwargs["playback_rate"] == 2.0
|
||||
assert self._base_slide_config.playback_rate == 2.0
|
||||
|
||||
@assert_constructs
|
||||
class TestWipe(Slide):
|
||||
|
Reference in New Issue
Block a user