mirror of
https://github.com/jeertmans/manim-slides.git
synced 2025-05-24 05:57:14 +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.
|
to slide config.
|
||||||
[#320](https://github.com/jeertmans/manim-slides/pull/320)
|
[#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)
|
## [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,
|
Prior to v5, there was no real CHANGELOG other than the GitHub releases,
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import json
|
import json
|
||||||
import shutil
|
import shutil
|
||||||
|
from functools import wraps
|
||||||
|
from inspect import Parameter, signature
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Callable, Dict, List, Optional, Set, Tuple
|
from typing import Any, Callable, Dict, List, Optional, Set, Tuple
|
||||||
|
|
||||||
@ -135,14 +137,76 @@ class Config(BaseModel): # type: ignore[misc]
|
|||||||
return self
|
return self
|
||||||
|
|
||||||
|
|
||||||
class PreSlideConfig(BaseModel): # type: ignore
|
class BaseSlideConfig(BaseModel): # type: ignore
|
||||||
start_animation: int
|
"""Base class for slide config."""
|
||||||
end_animation: int
|
|
||||||
loop: bool = False
|
loop: bool = False
|
||||||
auto_next: bool = False
|
auto_next: bool = False
|
||||||
playback_rate: float = 1.0
|
playback_rate: float = 1.0
|
||||||
reversed_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")
|
@field_validator("start_animation", "end_animation")
|
||||||
@classmethod
|
@classmethod
|
||||||
def index_is_posint(cls, v: int) -> int:
|
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)
|
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
|
file: FilePath
|
||||||
rev_file: FilePath
|
rev_file: FilePath
|
||||||
loop: bool = False
|
|
||||||
auto_next: bool = False
|
|
||||||
playback_rate: float = 1.0
|
|
||||||
reversed_playback_rate: float = 1.0
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_pre_slide_config_and_files(
|
def from_pre_slide_config_and_files(
|
||||||
cls, pre_slide_config: PreSlideConfig, file: Path, rev_file: Path
|
cls, pre_slide_config: PreSlideConfig, file: Path, rev_file: Path
|
||||||
) -> "SlideConfig":
|
) -> "SlideConfig":
|
||||||
return cls(
|
return cls(file=file, rev_file=rev_file, **pre_slide_config.dict())
|
||||||
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,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class PresentationConfig(BaseModel): # type: ignore[misc]
|
class PresentationConfig(BaseModel): # type: ignore[misc]
|
||||||
|
@ -217,7 +217,9 @@ def start_at_callback(
|
|||||||
metavar="RATE",
|
metavar="RATE",
|
||||||
type=float,
|
type=float,
|
||||||
default=1.0,
|
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(
|
@click.option(
|
||||||
"--next-terminates-loop",
|
"--next-terminates-loop",
|
||||||
|
@ -104,7 +104,7 @@ class Player(QMainWindow): # type: ignore[misc]
|
|||||||
|
|
||||||
self.media_player = QMediaPlayer(self)
|
self.media_player = QMediaPlayer(self)
|
||||||
self.media_player.setVideoOutput(self.video_widget)
|
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.presentation_changed.connect(self.presentation_changed_callback)
|
||||||
self.slide_changed.connect(self.slide_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:
|
if self.playing_reversed_slide:
|
||||||
self.media_player.setPlaybackRate(
|
self.media_player.setPlaybackRate(
|
||||||
self.current_slide_config.reversed_playback_rate
|
self.current_slide_config.reversed_playback_rate * self.playback_rate
|
||||||
)
|
)
|
||||||
else:
|
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:
|
if start_paused:
|
||||||
self.media_player.pause()
|
self.media_player.pause()
|
||||||
|
@ -8,7 +8,7 @@ from typing import Any, List, MutableMapping, Optional, Sequence, Tuple, ValuesV
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
from tqdm import tqdm
|
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 ..defaults import FFMPEG_BIN, FOLDER_PATH
|
||||||
from ..logger import logger
|
from ..logger import logger
|
||||||
from ..utils import concatenate_video_files, merge_basenames, reverse_video_file
|
from ..utils import concatenate_video_files, merge_basenames, reverse_video_file
|
||||||
@ -29,7 +29,7 @@ class BaseSlide:
|
|||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self._output_folder: Path = output_folder
|
self._output_folder: Path = output_folder
|
||||||
self._slides: List[PreSlideConfig] = []
|
self._slides: List[PreSlideConfig] = []
|
||||||
self._pre_slide_config_kwargs: MutableMapping[str, Any] = {}
|
self._base_slide_config: BaseSlideConfig = BaseSlideConfig()
|
||||||
self._current_slide = 1
|
self._current_slide = 1
|
||||||
self._current_animation = 0
|
self._current_animation = 0
|
||||||
self._start_animation = 0
|
self._start_animation = 0
|
||||||
@ -254,13 +254,11 @@ class BaseSlide:
|
|||||||
super().play(*args, **kwargs) # type: ignore[misc]
|
super().play(*args, **kwargs) # type: ignore[misc]
|
||||||
self._current_animation += 1
|
self._current_animation += 1
|
||||||
|
|
||||||
|
@BaseSlideConfig.wrapper("base_slide_config")
|
||||||
def next_slide(
|
def next_slide(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
loop: bool = False,
|
base_slide_config: BaseSlideConfig,
|
||||||
auto_next: bool = False,
|
|
||||||
playback_rate: float = 1.0,
|
|
||||||
reversed_playback_rate: float = 1.0,
|
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
@ -380,21 +378,16 @@ class BaseSlide:
|
|||||||
self.wait(self.wait_time_between_slides) # type: ignore[attr-defined]
|
self.wait(self.wait_time_between_slides) # type: ignore[attr-defined]
|
||||||
|
|
||||||
self._slides.append(
|
self._slides.append(
|
||||||
PreSlideConfig(
|
PreSlideConfig.from_base_slide_config_and_animation_indices(
|
||||||
start_animation=self._start_animation,
|
self._base_slide_config,
|
||||||
end_animation=self._current_animation,
|
self._start_animation,
|
||||||
**self._pre_slide_config_kwargs,
|
self._current_animation,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
self._current_slide += 1
|
self._current_slide += 1
|
||||||
|
|
||||||
self._pre_slide_config_kwargs = dict(
|
self._base_slide_config = base_slide_config
|
||||||
loop=loop,
|
|
||||||
auto_next=auto_next,
|
|
||||||
playback_rate=playback_rate,
|
|
||||||
reversed_playback_rate=reversed_playback_rate,
|
|
||||||
)
|
|
||||||
self._start_animation = self._current_animation
|
self._start_animation = self._current_animation
|
||||||
|
|
||||||
def _add_last_slide(self) -> None:
|
def _add_last_slide(self) -> None:
|
||||||
@ -406,10 +399,10 @@ class BaseSlide:
|
|||||||
return
|
return
|
||||||
|
|
||||||
self._slides.append(
|
self._slides.append(
|
||||||
PreSlideConfig(
|
PreSlideConfig.from_base_slide_config_and_animation_indices(
|
||||||
start_animation=self._start_animation,
|
self._base_slide_config,
|
||||||
end_animation=self._current_animation,
|
self._start_animation,
|
||||||
**self._pre_slide_config_kwargs,
|
self._current_animation,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ from typing import Any, List, Optional, Tuple
|
|||||||
|
|
||||||
from manim import Scene, ThreeDScene, config
|
from manim import Scene, ThreeDScene, config
|
||||||
|
|
||||||
|
from ..config import BaseSlideConfig
|
||||||
from .base import BaseSlide
|
from .base import BaseSlide
|
||||||
|
|
||||||
|
|
||||||
@ -79,22 +80,17 @@ class Slide(BaseSlide, Scene): # type: ignore[misc]
|
|||||||
"""
|
"""
|
||||||
self.next_slide(*args, **kwargs)
|
self.next_slide(*args, **kwargs)
|
||||||
|
|
||||||
|
@BaseSlideConfig.wrapper("base_slide_config")
|
||||||
def next_slide(
|
def next_slide(
|
||||||
self,
|
self,
|
||||||
*args: Any,
|
*args: Any,
|
||||||
loop: bool = False,
|
base_slide_config: BaseSlideConfig,
|
||||||
auto_next: bool = False,
|
|
||||||
playback_rate: float = 1.0,
|
|
||||||
reversed_playback_rate: float = 1.0,
|
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> None:
|
) -> None:
|
||||||
Scene.next_section(self, *args, **kwargs)
|
Scene.next_section(self, *args, **kwargs)
|
||||||
BaseSlide.next_slide(
|
BaseSlide.next_slide.__wrapped__(
|
||||||
self,
|
self,
|
||||||
loop=loop,
|
base_slide_config=base_slide_config,
|
||||||
auto_next=auto_next,
|
|
||||||
playback_rate=playback_rate,
|
|
||||||
reversed_playback_rate=reversed_playback_rate,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def render(self, *args: Any, **kwargs: Any) -> None:
|
def render(self, *args: Any, **kwargs: Any) -> None:
|
||||||
|
@ -140,16 +140,16 @@ class TestSlide:
|
|||||||
|
|
||||||
self.add(text)
|
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.next_slide(loop=True)
|
||||||
self.play(text.animate.scale(2))
|
self.play(text.animate.scale(2))
|
||||||
|
|
||||||
assert self._pre_slide_config_kwargs["loop"]
|
assert self._base_slide_config.loop
|
||||||
|
|
||||||
self.next_slide(loop=False)
|
self.next_slide(loop=False)
|
||||||
|
|
||||||
assert not self._pre_slide_config_kwargs["loop"]
|
assert not self._base_slide_config.loop
|
||||||
|
|
||||||
@assert_constructs
|
@assert_constructs
|
||||||
class TestAutoNext(Slide):
|
class TestAutoNext(Slide):
|
||||||
@ -158,16 +158,16 @@ class TestSlide:
|
|||||||
|
|
||||||
self.add(text)
|
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.next_slide(auto_next=True)
|
||||||
self.play(text.animate.scale(2))
|
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)
|
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
|
@assert_constructs
|
||||||
class TestLoopAndAutoNextFails(Slide):
|
class TestLoopAndAutoNextFails(Slide):
|
||||||
@ -189,12 +189,12 @@ class TestSlide:
|
|||||||
|
|
||||||
self.add(text)
|
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.next_slide(playback_rate=2.0)
|
||||||
self.play(text.animate.scale(2))
|
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
|
@assert_constructs
|
||||||
class TestWipe(Slide):
|
class TestWipe(Slide):
|
||||||
|
Reference in New Issue
Block a user