mirror of
https://github.com/jeertmans/manim-slides.git
synced 2025-07-17 16:28:21 +08:00
Compare commits
10 Commits
reorganize
...
pre-commit
Author | SHA1 | Date | |
---|---|---|---|
4e483525e4 | |||
ba733eeb97 | |||
14813f7af1 | |||
2f62915ad6 | |||
c81e117bb8 | |||
6255f644ab | |||
9e12feb275 | |||
d2d5cc10b7 | |||
3c6e2db7db | |||
04b0eb5685 |
@ -13,7 +13,7 @@ repos:
|
|||||||
- id: end-of-file-fixer
|
- id: end-of-file-fixer
|
||||||
- id: trailing-whitespace
|
- id: trailing-whitespace
|
||||||
- repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks
|
- repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks
|
||||||
rev: v2.14.0
|
rev: v2.15.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: pretty-format-yaml
|
- id: pretty-format-yaml
|
||||||
args: [--autofix]
|
args: [--autofix]
|
||||||
@ -21,13 +21,13 @@ repos:
|
|||||||
exclude: poetry.lock
|
exclude: poetry.lock
|
||||||
args: [--autofix, --trailing-commas]
|
args: [--autofix, --trailing-commas]
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: v0.11.5
|
rev: v0.12.3
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
args: [--fix]
|
args: [--fix]
|
||||||
- id: ruff-format
|
- id: ruff-format
|
||||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||||
rev: v1.15.0
|
rev: v1.16.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: mypy
|
- id: mypy
|
||||||
additional_dependencies: [types-requests, types-setuptools]
|
additional_dependencies: [types-requests, types-setuptools]
|
||||||
|
16
CHANGELOG.md
16
CHANGELOG.md
@ -10,6 +10,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
(unreleased)=
|
(unreleased)=
|
||||||
## [Unreleased](https://github.com/jeertmans/manim-slides/compare/v5.5.1...HEAD)
|
## [Unreleased](https://github.com/jeertmans/manim-slides/compare/v5.5.1...HEAD)
|
||||||
|
|
||||||
|
(unreleased-added)=
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `manim-slides render` now exits with the same return code as the one returned by `manim render` or `manimgl`.
|
||||||
|
[@chrjabs](https://github.com/chrjabs) [#545](https://github.com/jeertmans/manim-slides/pull/545)
|
||||||
|
|
||||||
(unreleased-chore)=
|
(unreleased-chore)=
|
||||||
### Chore
|
### Chore
|
||||||
|
|
||||||
@ -18,10 +24,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
when using one of those extras, but as they were not part of the public API,
|
when using one of those extras, but as they were not part of the public API,
|
||||||
we do not consider this to be a **breaking change**.
|
we do not consider this to be a **breaking change**.
|
||||||
[#542](https://github.com/jeertmans/manim-slides/pull/542)
|
[#542](https://github.com/jeertmans/manim-slides/pull/542)
|
||||||
- Moved `manim_slides.docs.manim_slides_directive` to `manim_slides.sphinxext.manim_slides_directive`.
|
- Added example in the research section of the gallery.
|
||||||
This is a **breaking change** because documentation configs have
|
[@mmcilree](https://github.com/mmcilree) [#552](https://github.com/jeertmans/manim-slides/pull/552)
|
||||||
to be updated.
|
- Added example in the school work section of the gallery.
|
||||||
[#242](https://github.com/jeertmans/manim-slides/pull/242)
|
[@casperalgera](https://github.com/casperalgera) [#556](https://github.com/jeertmans/manim-slides/pull/556)
|
||||||
|
- Added example in the school work section of the gallery.
|
||||||
|
[@amstrdm](https://github.com/amstrdm) [#557](https://github.com/jeertmans/manim-slides/pull/557)
|
||||||
|
|
||||||
(v5.5.1)=
|
(v5.5.1)=
|
||||||
## [v5.5.1](https://github.com/jeertmans/manim-slides/compare/v5.5.0...v5.5.1)
|
## [v5.5.1](https://github.com/jeertmans/manim-slides/compare/v5.5.0...v5.5.1)
|
||||||
|
20
README.md
20
README.md
@ -154,21 +154,23 @@ Below is a comparison of the most used ones with Manim Slides:
|
|||||||
|
|
||||||
## Citing
|
## Citing
|
||||||
|
|
||||||
If you use this project, please cite it using the following reference:
|
If you use this software, please cite it using as:
|
||||||
|
|
||||||
```bibtex
|
```bibtex
|
||||||
@article{Jerome_Eertmans_Manim_Slides_A_2023,
|
@article{Jerome_Eertmans_Manim_Slides_A_2023,
|
||||||
title = {{Manim Slides: A Python package for presenting Manim content anywhere}},
|
title = {{Manim Slides: A Python package for presenting Manim content anywhere}},
|
||||||
author = {{Jérome Eertmans}},
|
author = {{Jérome Eertmans}},
|
||||||
year = 2023,
|
year = 2023,
|
||||||
month = aug,
|
month = aug,
|
||||||
journal = {Journal of Open Source Education},
|
journal = {Journal of Open Source Education},
|
||||||
volume = 6,
|
volume = 6,
|
||||||
doi = {10.21105/jose.00206}
|
doi = {10.21105/jose.00206}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
or by linking this GitHub repository at the end of the presentation.
|
or by linking this GitHub repository at the end of your presentation.
|
||||||
|
|
||||||
|
Other citation formats can be obtained by clicking on the *Cite this repository* button on this page.
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
|
@ -37,7 +37,7 @@ extensions = [
|
|||||||
"sphinx_copybutton",
|
"sphinx_copybutton",
|
||||||
"sphinx_design",
|
"sphinx_design",
|
||||||
# Custom
|
# Custom
|
||||||
"manim_slides.sphinxext.manim_slides_directive",
|
"manim_slides.docs.manim_slides_directive",
|
||||||
]
|
]
|
||||||
|
|
||||||
autodoc_typehints = "both"
|
autodoc_typehints = "both"
|
||||||
|
@ -92,7 +92,7 @@ This will also show the default value for each option.
|
|||||||
If you want to create your own template, the best is to start from the default one.
|
If you want to create your own template, the best is to start from the default one.
|
||||||
|
|
||||||
You can either download it from the
|
You can either download it from the
|
||||||
[template folder](https://github.com/jeertmans/manim-slides/tree/main/manim_slides/cli/convert/templates)
|
[template folder](https://github.com/jeertmans/manim-slides/tree/main/manim_slides/templates)
|
||||||
or use the `manim-slides convert --to=FORMAT --show-template` command,
|
or use the `manim-slides convert --to=FORMAT --show-template` command,
|
||||||
where `FORMAT` is one of the supported formats.
|
where `FORMAT` is one of the supported formats.
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ If you too have created content with Manim Slides that is available online
|
|||||||
(e.g., a YouTube video or website),
|
(e.g., a YouTube video or website),
|
||||||
don't hesitate to contact us so that we can share your content on this page!
|
don't hesitate to contact us so that we can share your content on this page!
|
||||||
|
|
||||||
## Scientif Research
|
## Scientific Research
|
||||||
|
|
||||||
Below are people that dissimenate their research results
|
Below are people that dissimenate their research results
|
||||||
using Manim Slides presentations.
|
using Manim Slides presentations.
|
||||||
@ -23,7 +23,7 @@ using Manim Slides presentations.
|
|||||||
|
|
||||||
Daniel publishes his presentations on *Cosmology, String Theory and related*
|
Daniel publishes his presentations on *Cosmology, String Theory and related*
|
||||||
topics on his
|
topics on his
|
||||||
[personal website](https://panopepino.github.io/web_page/main_page/slides.html). https://panopepino.github.io/web_page/main_page/slides.html
|
[personal website](https://panopepino.github.io/web_page/main_page/slides.html).
|
||||||
|
|
||||||
For example, below are the slides of a seminar he gave titled
|
For example, below are the slides of a seminar he gave titled
|
||||||
[Our Universe on a (Dark) Bubble](https://panopepino.github.io/web_page/main_page/presentations/2023_11_long/LS.html).
|
[Our Universe on a (Dark) Bubble](https://panopepino.github.io/web_page/main_page/presentations/2023_11_long/LS.html).
|
||||||
@ -67,9 +67,66 @@ For example, below are the slides of his
|
|||||||
</iframe>
|
</iframe>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
### Matthew McIlree
|
||||||
|
|
||||||
|
Matthew is a Computer Science researcher from Scotland and has used Manim Slides
|
||||||
|
to present his work on *Proof Logging for Constraint Programming*. He also publishes
|
||||||
|
his presentation slides on his [personal website](https://matthewmcilree.com).
|
||||||
|
|
||||||
|
Here are the slides from a 25-minute talk he presented at the 39th Annual AAAI Conference on Artificial Intelligence titled
|
||||||
|
[Certifying Bounds Propagation for Integer Multiplication Constraints](https://matthewmcilree.com/files/slides/mcilree_aaai2025.html).
|
||||||
|
|
||||||
|
<div style="position:relative;padding-bottom:56.25%;">
|
||||||
|
<iframe
|
||||||
|
loading="lazy"
|
||||||
|
style="width:100%;height:100%;position:absolute;left:0px;top:0px;"
|
||||||
|
frameborder="1"
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
allowfullscreen
|
||||||
|
allow="autoplay"
|
||||||
|
src="https://matthewmcilree.com/files/slides/mcilree_aaai2025.html">
|
||||||
|
</iframe>
|
||||||
|
</div>
|
||||||
|
|
||||||
## School Work
|
## School Work
|
||||||
|
|
||||||
Below are people that used Manim Slides for school presentations.
|
Below are people that used Manim Slides for school presentations.
|
||||||
|
|
||||||
*This list is currently empty. Please reach out to us if you have examples
|
### Antonio Caserta
|
||||||
to share!*
|
|
||||||
|
Antonio is a 17-year-old high school student from Germany who used Manim Slides to present his final project, *Episteme*, an AI financial terminal that uses crowdsourced data from social networks to gather stock insights.
|
||||||
|
|
||||||
|
The slides from his 30-minute presentation to the school board can be found below and on his [Github repository](https://github.com/amstrdm/episteme-manim-slides)
|
||||||
|
|
||||||
|
<div style="position:relative;padding-bottom:56.25%;">
|
||||||
|
<iframe
|
||||||
|
loading="lazy"
|
||||||
|
style="width:100%;height:100%;position:absolute;left:0px;top:0px;"
|
||||||
|
frameborder="1"
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
allowfullscreen
|
||||||
|
allow="autoplay"
|
||||||
|
src="https://amstrdm.github.io/episteme-manim-slides/">
|
||||||
|
</iframe>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
### Casper Algera
|
||||||
|
|
||||||
|
Casper, a mathematics student from the Netherlands, used Manim Slides to present his bachelor's thesis.
|
||||||
|
In his presentation, he illustrates a probabilistic coupling argument related to the [contact process](https://en.wikipedia.org/wiki/Contact_process_(mathematics)).
|
||||||
|
His slides are available below, and his full presentation can be viewed on [YouTube](https://www.youtube.com/watch?v=ZJhvfCL5MWE).
|
||||||
|
|
||||||
|
<div style="position:relative;padding-bottom:56.25%;">
|
||||||
|
<iframe
|
||||||
|
loading="lazy"
|
||||||
|
style="width:100%;height:100%;position:absolute;left:0px;top:0px;"
|
||||||
|
frameborder="1"
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
allowfullscreen
|
||||||
|
allow="autoplay"
|
||||||
|
src="https://casperalgera.github.io/criticalvalueCP/">
|
||||||
|
</iframe>
|
||||||
|
</div>
|
||||||
|
@ -4,7 +4,7 @@ This page contains an exhaustive list of all the commands available with `manim-
|
|||||||
|
|
||||||
|
|
||||||
```{eval-rst}
|
```{eval-rst}
|
||||||
.. click:: manim_slides.cli.commands:main
|
.. click:: manim_slides.__main__:cli
|
||||||
:prog: manim-slides
|
:prog: manim-slides
|
||||||
:nested: full
|
:nested: full
|
||||||
```
|
```
|
||||||
|
@ -4,7 +4,7 @@ One of the benefits of the `convert` command is the use of template files.
|
|||||||
|
|
||||||
Currently, only the HTML export uses one. If not specified, the template
|
Currently, only the HTML export uses one. If not specified, the template
|
||||||
will be the one shipped with Manim Slides, see
|
will be the one shipped with Manim Slides, see
|
||||||
[`manim_slides/cli/convert/templates/revealjs.html`](https://github.com/jeertmans/manim-slides/blob/main/manim_slides/cli/convert/templates/revealjs.html).
|
[`manim_slides/templates/revealjs.html`](https://github.com/jeertmans/manim-slides/blob/main/manim_slides/templates/revealjs.html).
|
||||||
|
|
||||||
Because you can actually use your own template with the `--use-template`
|
Because you can actually use your own template with the `--use-template`
|
||||||
option, possibilities are infinite!
|
option, possibilities are infinite!
|
||||||
|
@ -30,7 +30,7 @@ manim-slides convert --show-config
|
|||||||
## Using a Custom Template
|
## Using a Custom Template
|
||||||
|
|
||||||
The default template used for HTML conversion can be found on
|
The default template used for HTML conversion can be found on
|
||||||
[GitHub](https://github.com/jeertmans/manim-slides/blob/main/manim_slides/cli/convert/templates/revealjs.html)
|
[GitHub](https://github.com/jeertmans/manim-slides/blob/main/manim_slides/templates/revealjs.html)
|
||||||
or printed with the `--show-template` option.
|
or printed with the `--show-template` option.
|
||||||
If you wish to use another template, you can do so with the
|
If you wish to use another template, you can do so with the
|
||||||
`--use-template FILE` option.
|
`--use-template FILE` option.
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# Manim Slides' Sphinx directive
|
# Manim Slides' Sphinx directive
|
||||||
|
|
||||||
```{eval-rst}
|
```{eval-rst}
|
||||||
.. automodule:: manim_slides.sphinxext.manim_slides_directive
|
.. automodule:: manim_slides.docs.manim_slides_directive
|
||||||
:members: ManimSlidesDirective
|
:members: ManimSlidesDirective
|
||||||
```
|
```
|
||||||
|
@ -1,10 +1,3 @@
|
|||||||
"""
|
|
||||||
Manim Slides module.
|
|
||||||
|
|
||||||
Submodules are lazily imported, in order to provide a faster import experience
|
|
||||||
in some cases.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
from types import ModuleType
|
from types import ModuleType
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@ -15,7 +8,9 @@ from .__version__ import __version__
|
|||||||
class Module(ModuleType):
|
class Module(ModuleType):
|
||||||
def __getattr__(self, name: str) -> Any:
|
def __getattr__(self, name: str) -> Any:
|
||||||
if name == "Slide" or name == "ThreeDSlide":
|
if name == "Slide" or name == "ThreeDSlide":
|
||||||
module = __import__("manim_slides.slide", None, None, [name])
|
module = __import__(
|
||||||
|
"manim_slides.slide", None, None, ["Slide", "ThreeDSlide"]
|
||||||
|
)
|
||||||
return getattr(module, name)
|
return getattr(module, name)
|
||||||
elif name == "ManimSlidesMagic":
|
elif name == "ManimSlidesMagic":
|
||||||
module = __import__(
|
module = __import__(
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
"""Manim Slides' main entrypoint."""
|
import json
|
||||||
|
|
||||||
|
import click
|
||||||
|
import requests
|
||||||
|
from click_default_group import DefaultGroup
|
||||||
|
|
||||||
from .__version__ import __version__
|
from .__version__ import __version__
|
||||||
from .checkhealth import checkhealth
|
from .checkhealth import checkhealth
|
||||||
from .cli.commands import main
|
|
||||||
from .convert import convert
|
from .convert import convert
|
||||||
from .logger import logger
|
from .logger import logger
|
||||||
from .present import list_scenes, present
|
from .present import list_scenes, present
|
||||||
@ -69,4 +72,4 @@ cli.add_command(render)
|
|||||||
cli.add_command(wizard)
|
cli.add_command(wizard)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
cli()
|
||||||
|
@ -1,3 +1 @@
|
|||||||
"""Manim Slides' version."""
|
__version__ = "5.5.1"
|
||||||
|
|
||||||
__version__ = "5.4.2"
|
|
||||||
|
@ -1,72 +0,0 @@
|
|||||||
"""Manim Slides' CLI."""
|
|
||||||
|
|
||||||
import json
|
|
||||||
|
|
||||||
import click
|
|
||||||
import requests
|
|
||||||
from click_default_group import DefaultGroup
|
|
||||||
|
|
||||||
from ..__version__ import __version__
|
|
||||||
from ..core.logger import logger
|
|
||||||
from .convert.commands import convert
|
|
||||||
from .present.commands import list_scenes, present
|
|
||||||
from .render.commands import render
|
|
||||||
from .wizard.commands import init, wizard
|
|
||||||
|
|
||||||
|
|
||||||
@click.group(cls=DefaultGroup, default="present", default_if_no_args=True)
|
|
||||||
@click.option(
|
|
||||||
"--notify-outdated-version/--silent",
|
|
||||||
" /-S",
|
|
||||||
is_flag=True,
|
|
||||||
default=True,
|
|
||||||
help="Check if a new version of Manim Slides is available.",
|
|
||||||
)
|
|
||||||
@click.version_option(__version__, "-v", "--version")
|
|
||||||
@click.help_option("-h", "--help")
|
|
||||||
def main(notify_outdated_version: bool) -> None:
|
|
||||||
"""
|
|
||||||
Manim Slides command-line utilities.
|
|
||||||
|
|
||||||
If no command is specified, defaults to `present`.
|
|
||||||
"""
|
|
||||||
# Code below is mostly a copy from:
|
|
||||||
# https://github.com/ManimCommunity/manim/blob/main/manim/cli/render/commands.py
|
|
||||||
if notify_outdated_version:
|
|
||||||
manim_info_url = "https://pypi.org/pypi/manim-slides/json"
|
|
||||||
warn_prompt = "Cannot check if latest release of Manim Slides is installed"
|
|
||||||
try:
|
|
||||||
req_info: requests.models.Response = requests.get(manim_info_url, timeout=2)
|
|
||||||
req_info.raise_for_status()
|
|
||||||
stable = req_info.json()["info"]["version"]
|
|
||||||
if stable != __version__:
|
|
||||||
click.echo(
|
|
||||||
"You are using Manim Slides version "
|
|
||||||
+ click.style(f"v{__version__}", fg="red")
|
|
||||||
+ ", but version "
|
|
||||||
+ click.style(f"v{stable}", fg="green")
|
|
||||||
+ " is available."
|
|
||||||
)
|
|
||||||
click.echo(
|
|
||||||
"You should consider upgrading via "
|
|
||||||
+ click.style("pip install -U manim-slides", fg="yellow")
|
|
||||||
)
|
|
||||||
except requests.exceptions.HTTPError:
|
|
||||||
logger.debug(f"HTTP Error: {warn_prompt}")
|
|
||||||
except requests.exceptions.ConnectionError:
|
|
||||||
logger.debug(f"Connection Error: {warn_prompt}")
|
|
||||||
except requests.exceptions.Timeout:
|
|
||||||
logger.debug(f"Timed Out: {warn_prompt}")
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
logger.debug(warn_prompt)
|
|
||||||
logger.debug(f"Error decoding JSON from {manim_info_url}")
|
|
||||||
except Exception:
|
|
||||||
logger.debug(f"Something went wrong: {warn_prompt}")
|
|
||||||
|
|
||||||
|
|
||||||
main.add_command(convert)
|
|
||||||
main.add_command(init)
|
|
||||||
main.add_command(list_scenes)
|
|
||||||
main.add_command(present)
|
|
||||||
main.add_command(render)
|
|
||||||
main.add_command(wizard)
|
|
@ -1 +0,0 @@
|
|||||||
"""Manim Slides conversion templates."""
|
|
@ -1 +0,0 @@
|
|||||||
"""Manim Slides' presentation commands."""
|
|
@ -1 +0,0 @@
|
|||||||
"""Manim Slides' wizard."""
|
|
@ -4,9 +4,8 @@ from typing import Any, Callable
|
|||||||
import click
|
import click
|
||||||
from click import Context, Parameter
|
from click import Context, Parameter
|
||||||
|
|
||||||
from ..core.config import list_presentation_configs
|
from .defaults import CONFIG_PATH, FOLDER_PATH
|
||||||
from ..core.defaults import CONFIG_PATH, FOLDER_PATH
|
from .logger import logger
|
||||||
from ..core.logger import logger
|
|
||||||
|
|
||||||
F = Callable[..., Any]
|
F = Callable[..., Any]
|
||||||
Wrapper = Callable[[F], F]
|
Wrapper = Callable[[F], F]
|
||||||
@ -89,68 +88,6 @@ def folder_path_option(function: F) -> F:
|
|||||||
callback=callback,
|
callback=callback,
|
||||||
help="Set slides folder.",
|
help="Set slides folder.",
|
||||||
show_default=True,
|
show_default=True,
|
||||||
is_eager=True, # Needed to expose its value to other callbacks
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return wrapper(function)
|
return wrapper(function)
|
||||||
|
|
||||||
|
|
||||||
def scenes_argument(function: F) -> F:
|
|
||||||
"""
|
|
||||||
Wrap a function to add a scenes arguments.
|
|
||||||
|
|
||||||
This function assumes that :func:`folder_path_option` is also used
|
|
||||||
on the same decorated function.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def callback(ctx: Context, param: Parameter, value: tuple[str]) -> list[Path]:
|
|
||||||
folder: Path = ctx.params.get("folder")
|
|
||||||
|
|
||||||
presentation_config_paths = list_presentation_configs(folder)
|
|
||||||
scene_names = [path.stem for path in presentation_config_paths]
|
|
||||||
num_scenes = len(scene_names)
|
|
||||||
num_digits = len(str(num_scenes))
|
|
||||||
|
|
||||||
if num_scenes == 0:
|
|
||||||
raise click.UsageError(
|
|
||||||
f"Folder {folder} does not contain "
|
|
||||||
"any valid config file, did you render the animations first?"
|
|
||||||
)
|
|
||||||
|
|
||||||
paths = []
|
|
||||||
|
|
||||||
if value:
|
|
||||||
for scene_name in value:
|
|
||||||
try:
|
|
||||||
i = scene_names.index(scene_name)
|
|
||||||
paths.append(presentation_config_paths[i])
|
|
||||||
except ValueError:
|
|
||||||
raise click.UsageError(
|
|
||||||
f"Could not find scene `{scene_name}` in: "
|
|
||||||
+ ", ".join(scene_names)
|
|
||||||
+ ". Did you make a typo or forgot to render the animations first?"
|
|
||||||
) from None
|
|
||||||
else:
|
|
||||||
click.echo(
|
|
||||||
"Choose at least one or more scenes from "
|
|
||||||
"(enter the corresponding number):\n"
|
|
||||||
+ "\n".join(
|
|
||||||
f"- {i:{num_digits}d}: {name}"
|
|
||||||
for i, name in enumerate(scene_names, start=1)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
continue_prompt = True
|
|
||||||
while continue_prompt:
|
|
||||||
index = click.prompt(
|
|
||||||
"Please enter a value", type=click.IntRange(1, num_scenes)
|
|
||||||
)
|
|
||||||
paths.append(presentation_config_paths[index - 1])
|
|
||||||
continue_prompt = click.confirm(
|
|
||||||
"Do you want to enter an additional scene?"
|
|
||||||
)
|
|
||||||
|
|
||||||
return paths
|
|
||||||
|
|
||||||
wrapper: Wrapper = click.argument("scenes", nargs=-1, callback=callback)
|
|
||||||
|
|
||||||
return wrapper(function)
|
|
@ -1,5 +1,3 @@
|
|||||||
"""Manim Slides' configuration tools."""
|
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import shutil
|
import shutil
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
@ -15,7 +13,6 @@ from pydantic import (
|
|||||||
FilePath,
|
FilePath,
|
||||||
PositiveInt,
|
PositiveInt,
|
||||||
PrivateAttr,
|
PrivateAttr,
|
||||||
ValidationError,
|
|
||||||
conset,
|
conset,
|
||||||
field_serializer,
|
field_serializer,
|
||||||
field_validator,
|
field_validator,
|
||||||
@ -29,54 +26,28 @@ Receiver = Callable[..., Any]
|
|||||||
|
|
||||||
|
|
||||||
class Signal(BaseModel): # type: ignore[misc]
|
class Signal(BaseModel): # type: ignore[misc]
|
||||||
"""Signal that notifies a list of receivers when it is emitted."""
|
__receivers: list[Receiver] = PrivateAttr(default_factory=list)
|
||||||
|
|
||||||
__receivers: set[Receiver] = PrivateAttr(default_factory=set)
|
|
||||||
|
|
||||||
def connect(self, receiver: Receiver) -> None:
|
def connect(self, receiver: Receiver) -> None:
|
||||||
"""
|
self.__receivers.append(receiver)
|
||||||
Connect a receiver to this signal.
|
|
||||||
|
|
||||||
This is a no-op if the receiver was already connected to this signal.
|
|
||||||
|
|
||||||
:param receiver: The receiver to connect.
|
|
||||||
"""
|
|
||||||
self.__receivers.add(receiver)
|
|
||||||
|
|
||||||
def disconnect(self, receiver: Receiver) -> None:
|
def disconnect(self, receiver: Receiver) -> None:
|
||||||
"""
|
self.__receivers.remove(receiver)
|
||||||
Disconnect a receiver from this signal.
|
|
||||||
|
|
||||||
This is a no-op if the receiver was not connected to this signal.
|
|
||||||
|
|
||||||
:param receiver: The receiver to disconnect.
|
|
||||||
"""
|
|
||||||
self.__receivers.discard(receiver)
|
|
||||||
|
|
||||||
def emit(self, *args: Any) -> None:
|
def emit(self, *args: Any) -> None:
|
||||||
"""
|
|
||||||
Emit this signal and call each of the attached receivers.
|
|
||||||
|
|
||||||
:param args: Positional arguments passed to each receiver.
|
|
||||||
"""
|
|
||||||
for receiver in self.__receivers:
|
for receiver in self.__receivers:
|
||||||
receiver(*args)
|
receiver(*args)
|
||||||
|
|
||||||
|
|
||||||
def key_id(name: str) -> PositiveInt:
|
def key_id(name: str) -> PositiveInt:
|
||||||
"""
|
"""Avoid importing Qt too early."""
|
||||||
Return the id corresponding to the given key name.
|
from qtpy.QtCore import Qt
|
||||||
|
|
||||||
:param str: The name of the key, e.g., 'Q'.
|
|
||||||
:return: The corresponding id.
|
|
||||||
"""
|
|
||||||
from qtpy.QtCore import Qt # Avoid importing Qt too early."""
|
|
||||||
|
|
||||||
return getattr(Qt, f"Key_{name}")
|
return getattr(Qt, f"Key_{name}")
|
||||||
|
|
||||||
|
|
||||||
class Key(BaseModel): # type: ignore[misc]
|
class Key(BaseModel): # type: ignore[misc]
|
||||||
"""Represent a list of key codes, with optionally a name."""
|
"""Represents a list of key codes, with optionally a name."""
|
||||||
|
|
||||||
ids: conset(PositiveInt, min_length=1) # type: ignore[valid-type]
|
ids: conset(PositiveInt, min_length=1) # type: ignore[valid-type]
|
||||||
name: Optional[str] = None
|
name: Optional[str] = None
|
||||||
@ -87,7 +58,6 @@ class Key(BaseModel): # type: ignore[misc]
|
|||||||
self.ids = set(ids)
|
self.ids = set(ids)
|
||||||
|
|
||||||
def match(self, key_id: int) -> bool:
|
def match(self, key_id: int) -> bool:
|
||||||
"""Return whether a given key id matches this key."""
|
|
||||||
m = key_id in self.ids
|
m = key_id in self.ids
|
||||||
|
|
||||||
if m:
|
if m:
|
||||||
@ -165,7 +135,6 @@ class Config(BaseModel): # type: ignore[misc]
|
|||||||
"""General Manim Slides config."""
|
"""General Manim Slides config."""
|
||||||
|
|
||||||
keys: Keys = Field(default_factory=Keys)
|
keys: Keys = Field(default_factory=Keys)
|
||||||
"""The key mapping."""
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_file(cls, path: Path) -> "Config":
|
def from_file(cls, path: Path) -> "Config":
|
||||||
@ -173,16 +142,11 @@ class Config(BaseModel): # type: ignore[misc]
|
|||||||
return cls.model_validate(rtoml.load(path)) # type: ignore
|
return cls.model_validate(rtoml.load(path)) # type: ignore
|
||||||
|
|
||||||
def to_file(self, path: Path) -> None:
|
def to_file(self, path: Path) -> None:
|
||||||
"""Dump this configuration to a file."""
|
"""Dump the configuration to a file."""
|
||||||
rtoml.dump(self.model_dump(), path, pretty=True)
|
rtoml.dump(self.model_dump(), path, pretty=True)
|
||||||
|
|
||||||
def merge_with(self, other: "Config") -> "Config":
|
def merge_with(self, other: "Config") -> "Config":
|
||||||
"""
|
"""Merge with another config."""
|
||||||
Merge with another config.
|
|
||||||
|
|
||||||
:param other: The other config to be merged with.
|
|
||||||
:return: This config, updated.
|
|
||||||
"""
|
|
||||||
self.keys = self.keys.merge_with(other.keys)
|
self.keys = self.keys.merge_with(other.keys)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
@ -191,17 +155,11 @@ class BaseSlideConfig(BaseModel): # type: ignore
|
|||||||
"""Base class for slide config."""
|
"""Base class for slide config."""
|
||||||
|
|
||||||
loop: bool = False
|
loop: bool = False
|
||||||
"""Whether this slide should loop."""
|
|
||||||
auto_next: bool = False
|
auto_next: bool = False
|
||||||
"""Whether this slide is skipped upon completion."""
|
|
||||||
playback_rate: float = 1.0
|
playback_rate: float = 1.0
|
||||||
"""The speed at which the animation is played (1.0 is normal)."""
|
|
||||||
reversed_playback_rate: float = 1.0
|
reversed_playback_rate: float = 1.0
|
||||||
"""The speed at which the reversed animation is played."""
|
|
||||||
notes: str = ""
|
notes: str = ""
|
||||||
"""The notes attached to this slide."""
|
|
||||||
dedent_notes: bool = True
|
dedent_notes: bool = True
|
||||||
"""Whether to automatically remove any leading indentation in the notes."""
|
|
||||||
skip_animations: bool = False
|
skip_animations: bool = False
|
||||||
src: Optional[FilePath] = None
|
src: Optional[FilePath] = None
|
||||||
|
|
||||||
@ -215,11 +173,7 @@ class BaseSlideConfig(BaseModel): # type: ignore
|
|||||||
The wrapped function must follow two criteria:
|
The wrapped function must follow two criteria:
|
||||||
- its last parameter must be ``**kwargs`` (or equivalent);
|
- its last parameter must be ``**kwargs`` (or equivalent);
|
||||||
- and its second last parameter must be ``<arg_name>``.
|
- and its second last parameter must be ``<arg_name>``.
|
||||||
|
|
||||||
:param arg_name: The name of the argument.
|
|
||||||
:return: The wrapped function.
|
|
||||||
"""
|
"""
|
||||||
# TODO: improve docs and (maybe) type-hints too
|
|
||||||
|
|
||||||
def _wrapper_(fun: Callable[..., Any]) -> Callable[..., Any]:
|
def _wrapper_(fun: Callable[..., Any]) -> Callable[..., Any]:
|
||||||
@wraps(fun)
|
@wraps(fun)
|
||||||
@ -255,11 +209,6 @@ class BaseSlideConfig(BaseModel): # type: ignore
|
|||||||
def apply_dedent_notes(
|
def apply_dedent_notes(
|
||||||
self,
|
self,
|
||||||
) -> "BaseSlideConfig":
|
) -> "BaseSlideConfig":
|
||||||
"""
|
|
||||||
Remove indentation from notes, if specified.
|
|
||||||
|
|
||||||
:return: The config, optionally modified.
|
|
||||||
"""
|
|
||||||
if self.dedent_notes:
|
if self.dedent_notes:
|
||||||
self.notes = dedent(self.notes)
|
self.notes = dedent(self.notes)
|
||||||
|
|
||||||
@ -270,9 +219,7 @@ class PreSlideConfig(BaseSlideConfig):
|
|||||||
"""Slide config to be used prior to rendering."""
|
"""Slide config to be used prior to rendering."""
|
||||||
|
|
||||||
start_animation: int
|
start_animation: int
|
||||||
"""The index of the first animation."""
|
|
||||||
end_animation: int
|
end_animation: int
|
||||||
"""The index after the last animation."""
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_base_slide_config_and_animation_indices(
|
def from_base_slide_config_and_animation_indices(
|
||||||
@ -281,13 +228,6 @@ class PreSlideConfig(BaseSlideConfig):
|
|||||||
start_animation: int,
|
start_animation: int,
|
||||||
end_animation: int,
|
end_animation: int,
|
||||||
) -> "PreSlideConfig":
|
) -> "PreSlideConfig":
|
||||||
"""
|
|
||||||
Create a config from a base config and animation indices.
|
|
||||||
|
|
||||||
:param base_slide_config: The base config.
|
|
||||||
:param start_animation: The index of the first animation.
|
|
||||||
:param end_animation: The index after the last animation.
|
|
||||||
"""
|
|
||||||
return cls(
|
return cls(
|
||||||
start_animation=start_animation,
|
start_animation=start_animation,
|
||||||
end_animation=end_animation,
|
end_animation=end_animation,
|
||||||
@ -297,12 +237,6 @@ class PreSlideConfig(BaseSlideConfig):
|
|||||||
@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:
|
||||||
"""
|
|
||||||
Validate that animation indices are positive integers.
|
|
||||||
|
|
||||||
:param v: An animation index.
|
|
||||||
:return: The animation index, if valid.
|
|
||||||
"""
|
|
||||||
if v < 0:
|
if v < 0:
|
||||||
raise ValueError("Animation index (start or end) cannot be negative")
|
raise ValueError("Animation index (start or end) cannot be negative")
|
||||||
return v
|
return v
|
||||||
@ -311,11 +245,7 @@ class PreSlideConfig(BaseSlideConfig):
|
|||||||
def start_animation_is_before_end(
|
def start_animation_is_before_end(
|
||||||
self,
|
self,
|
||||||
) -> "PreSlideConfig":
|
) -> "PreSlideConfig":
|
||||||
"""
|
if self.start_animation > self.end_animation:
|
||||||
Validate that start and end animation indices satisfy 'start < end'.
|
|
||||||
|
|
||||||
:return: The config, if indices are valid.
|
|
||||||
"""
|
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"Start animation index must be strictly lower than end animation index"
|
"Start animation index must be strictly lower than end animation index"
|
||||||
)
|
)
|
||||||
@ -349,9 +279,7 @@ class SlideConfig(BaseSlideConfig):
|
|||||||
"""Slide config to be used after rendering."""
|
"""Slide config to be used after rendering."""
|
||||||
|
|
||||||
file: FilePath
|
file: FilePath
|
||||||
"""The file containing the animation."""
|
|
||||||
rev_file: FilePath
|
rev_file: FilePath
|
||||||
"""The file containing the reversed animation."""
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_pre_slide_config_and_files(
|
def from_pre_slide_config_and_files(
|
||||||
@ -361,22 +289,13 @@ class SlideConfig(BaseSlideConfig):
|
|||||||
|
|
||||||
|
|
||||||
class PresentationConfig(BaseModel): # type: ignore[misc]
|
class PresentationConfig(BaseModel): # type: ignore[misc]
|
||||||
"""Presentation config that contains all necessary information for a presentation."""
|
|
||||||
|
|
||||||
slides: list[SlideConfig] = Field(min_length=1)
|
slides: list[SlideConfig] = Field(min_length=1)
|
||||||
"""The non-empty list of slide configs."""
|
|
||||||
resolution: tuple[PositiveInt, PositiveInt] = (1920, 1080)
|
resolution: tuple[PositiveInt, PositiveInt] = (1920, 1080)
|
||||||
"""The resolution of the animation files."""
|
|
||||||
background_color: Color = "black"
|
background_color: Color = "black"
|
||||||
"""The background color of the animation files."""
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_file(cls, path: Path) -> "PresentationConfig":
|
def from_file(cls, path: Path) -> "PresentationConfig":
|
||||||
"""
|
"""Read a presentation configuration from a file."""
|
||||||
Read a presentation configuration from a file.
|
|
||||||
|
|
||||||
:param path: The path where the config is read from.
|
|
||||||
"""
|
|
||||||
with open(path) as f:
|
with open(path) as f:
|
||||||
obj = json.load(f)
|
obj = json.load(f)
|
||||||
|
|
||||||
@ -393,11 +312,7 @@ class PresentationConfig(BaseModel): # type: ignore[misc]
|
|||||||
return cls.model_validate(obj) # type: ignore
|
return cls.model_validate(obj) # type: ignore
|
||||||
|
|
||||||
def to_file(self, path: Path) -> None:
|
def to_file(self, path: Path) -> None:
|
||||||
"""
|
"""Dump the presentation configuration to a file."""
|
||||||
Dump the presentation configuration to a file.
|
|
||||||
|
|
||||||
:param path: The path to save this config.
|
|
||||||
"""
|
|
||||||
with open(path, "w") as f:
|
with open(path, "w") as f:
|
||||||
f.write(self.model_dump_json(indent=2))
|
f.write(self.model_dump_json(indent=2))
|
||||||
|
|
||||||
@ -408,14 +323,7 @@ class PresentationConfig(BaseModel): # type: ignore[misc]
|
|||||||
include_reversed: bool = True,
|
include_reversed: bool = True,
|
||||||
prefix: str = "",
|
prefix: str = "",
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""Copy the files to a given directory."""
|
||||||
Copy the files to a given directory and return the corresponding configuration.
|
|
||||||
|
|
||||||
:param folder: The folder that will contain the animation files.
|
|
||||||
:param use_cached: Whether caching should be used to avoid copies when possible.
|
|
||||||
:param include_reversed: Whether to also copy reversed animation to the folder.
|
|
||||||
:param prefix: Optional prefix added to each file name.
|
|
||||||
"""
|
|
||||||
for slide_config in self.slides:
|
for slide_config in self.slides:
|
||||||
file = slide_config.file
|
file = slide_config.file
|
||||||
rev_file = slide_config.rev_file
|
rev_file = slide_config.rev_file
|
||||||
@ -427,6 +335,4 @@ class PresentationConfig(BaseModel): # type: ignore[misc]
|
|||||||
shutil.copy(file, dest)
|
shutil.copy(file, dest)
|
||||||
|
|
||||||
if include_reversed and (not use_cached or not rev_dest.exists()):
|
if include_reversed and (not use_cached or not rev_dest.exists()):
|
||||||
# TODO: if include_reversed is False, then rev_dev will likely not exist
|
|
||||||
# and this will cause an issue when decoding.
|
|
||||||
shutil.copy(rev_file, rev_dest)
|
shutil.copy(rev_file, rev_dest)
|
@ -37,18 +37,14 @@ from pydantic_core import CoreSchema, core_schema
|
|||||||
from pydantic_extra_types.color import Color
|
from pydantic_extra_types.color import Color
|
||||||
from tqdm import tqdm
|
from tqdm import tqdm
|
||||||
|
|
||||||
from ...core.config import PresentationConfig
|
|
||||||
from ...core.logger import logger
|
|
||||||
from ..commons import folder_path_option, scenes_argument, verbosity_option
|
|
||||||
from . import templates
|
from . import templates
|
||||||
|
from .commons import folder_path_option, verbosity_option
|
||||||
|
from .config import PresentationConfig
|
||||||
|
from .logger import logger
|
||||||
|
from .present import get_scenes_presentation_config
|
||||||
|
|
||||||
|
|
||||||
def open_with_default(file: Path) -> None:
|
def open_with_default(file: Path) -> None:
|
||||||
"""
|
|
||||||
Open a file with the default application.
|
|
||||||
|
|
||||||
:param file: The file to open.
|
|
||||||
"""
|
|
||||||
system = platform.system()
|
system = platform.system()
|
||||||
if system == "Darwin":
|
if system == "Darwin":
|
||||||
subprocess.call(("open", str(file)))
|
subprocess.call(("open", str(file)))
|
||||||
@ -146,7 +142,6 @@ class Str(str):
|
|||||||
|
|
||||||
# This fixes pickling issue on Python 3.8
|
# This fixes pickling issue on Python 3.8
|
||||||
__reduce_ex__ = str.__reduce_ex__
|
__reduce_ex__ = str.__reduce_ex__
|
||||||
# TODO: do we still need this?
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def __get_pydantic_core_schema__(
|
def __get_pydantic_core_schema__(
|
||||||
@ -552,11 +547,6 @@ class RevealJS(Converter):
|
|||||||
return resources.files(templates).joinpath("revealjs.html").read_text()
|
return resources.files(templates).joinpath("revealjs.html").read_text()
|
||||||
|
|
||||||
def open(self, file: Path) -> None:
|
def open(self, file: Path) -> None:
|
||||||
"""
|
|
||||||
Open the HTML file inside a web browser.
|
|
||||||
|
|
||||||
:param path: The path to the HTML file.
|
|
||||||
"""
|
|
||||||
webbrowser.open(file.absolute().as_uri())
|
webbrowser.open(file.absolute().as_uri())
|
||||||
|
|
||||||
def convert_to(self, dest: Path) -> None: # noqa: C901
|
def convert_to(self, dest: Path) -> None: # noqa: C901
|
||||||
@ -920,7 +910,7 @@ def show_template_option(function: Callable[..., Any]) -> Callable[..., Any]:
|
|||||||
|
|
||||||
|
|
||||||
@click.command()
|
@click.command()
|
||||||
@scenes_argument
|
@click.argument("scenes", nargs=-1)
|
||||||
@folder_path_option
|
@folder_path_option
|
||||||
@click.argument("dest", type=click.Path(dir_okay=False, path_type=Path))
|
@click.argument("dest", type=click.Path(dir_okay=False, path_type=Path))
|
||||||
@click.option(
|
@click.option(
|
||||||
@ -970,7 +960,7 @@ def show_template_option(function: Callable[..., Any]) -> Callable[..., Any]:
|
|||||||
@show_config_options
|
@show_config_options
|
||||||
@verbosity_option
|
@verbosity_option
|
||||||
def convert(
|
def convert(
|
||||||
scenes: list[Path],
|
scenes: list[str],
|
||||||
folder: Path,
|
folder: Path,
|
||||||
dest: Path,
|
dest: Path,
|
||||||
to: str,
|
to: str,
|
||||||
@ -981,7 +971,7 @@ def convert(
|
|||||||
one_file: bool,
|
one_file: bool,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Convert SCENE(s) into a given format and writes the result in DEST."""
|
"""Convert SCENE(s) into a given format and writes the result in DEST."""
|
||||||
presentation_configs = [PresentationConfig.from_file(scene) for scene in scenes]
|
presentation_configs = get_scenes_presentation_config(scenes, folder)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if to == "auto":
|
if to == "auto":
|
@ -1,8 +1,4 @@
|
|||||||
"""Manim Slides' defaults."""
|
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
FOLDER_PATH: Path = Path("./slides")
|
FOLDER_PATH: Path = Path("./slides")
|
||||||
"""Folder where slides are stored."""
|
|
||||||
CONFIG_PATH: Path = Path(".manim-slides.toml")
|
CONFIG_PATH: Path = Path(".manim-slides.toml")
|
||||||
"""Path to local Manim Slides config."""
|
|
@ -40,7 +40,7 @@ First, you must include the directive in the Sphinx configuration file:
|
|||||||
|
|
||||||
extensions = [
|
extensions = [
|
||||||
# ...
|
# ...
|
||||||
"manim_slides.sphinxext.manim_slides_directive",
|
"manim_slides.docs.manim_slides_directive",
|
||||||
]
|
]
|
||||||
|
|
||||||
Its basic usage that allows processing **inline content**
|
Its basic usage that allows processing **inline content**
|
@ -116,7 +116,7 @@ class ManimSlidesMagic(Magics): # type: ignore
|
|||||||
file) will be moved relative to the video locations. Use-cases include building
|
file) will be moved relative to the video locations. Use-cases include building
|
||||||
documentation with Sphinx and JupyterBook. See also the
|
documentation with Sphinx and JupyterBook. See also the
|
||||||
:mod:`Manim Slides directive for Sphinx
|
:mod:`Manim Slides directive for Sphinx
|
||||||
<manim_slides.sphinxext.manim_slides_directive>`.
|
<manim_slides.docs.manim_slides_directive>`.
|
||||||
|
|
||||||
Examples
|
Examples
|
||||||
--------
|
--------
|
||||||
|
@ -31,11 +31,7 @@ HIGHLIGHTED_KEYWORDS = [ # these keywords are highlighted specially
|
|||||||
|
|
||||||
|
|
||||||
def make_logger() -> logging.Logger:
|
def make_logger() -> logging.Logger:
|
||||||
"""
|
"""Make a logger similar to the one used by Manim."""
|
||||||
Make a logger similar to the one used by Manim.
|
|
||||||
|
|
||||||
:return: The logger instance.
|
|
||||||
"""
|
|
||||||
RichHandler.KEYWORDS = HIGHLIGHTED_KEYWORDS
|
RichHandler.KEYWORDS = HIGHLIGHTED_KEYWORDS
|
||||||
rich_handler = RichHandler(
|
rich_handler = RichHandler(
|
||||||
show_time=True,
|
show_time=True,
|
||||||
@ -51,5 +47,6 @@ def make_logger() -> logging.Logger:
|
|||||||
return logger
|
return logger
|
||||||
|
|
||||||
|
|
||||||
logger = make_logger()
|
make_logger()
|
||||||
"""The logger instance used across this project."""
|
|
||||||
|
logger = logging.getLogger("manim-slides")
|
@ -7,14 +7,9 @@ import click
|
|||||||
from click import Context, Parameter
|
from click import Context, Parameter
|
||||||
from pydantic import ValidationError
|
from pydantic import ValidationError
|
||||||
|
|
||||||
from ...core.config import Config, PresentationConfig, list_presentation_configs
|
from ..commons import config_path_option, folder_path_option, verbosity_option
|
||||||
from ...core.logger import logger
|
from ..config import Config, PresentationConfig
|
||||||
from ..commons import (
|
from ..logger import logger
|
||||||
config_path_option,
|
|
||||||
folder_path_option,
|
|
||||||
scenes_argument,
|
|
||||||
verbosity_option,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@click.command()
|
@click.command()
|
||||||
@ -23,10 +18,8 @@ from ..commons import (
|
|||||||
@verbosity_option
|
@verbosity_option
|
||||||
def list_scenes(folder: Path) -> None:
|
def list_scenes(folder: Path) -> None:
|
||||||
"""List available scenes."""
|
"""List available scenes."""
|
||||||
scene_names = [path.stem for path in list_presentation_configs(folder)]
|
for i, scene in enumerate(_list_scenes(folder), start=1):
|
||||||
num_digits = len(str(len(scene_names)))
|
click.secho(f"{i}: {scene}", fg="green")
|
||||||
for i, scene_name in enumerate(scene_names, start=1):
|
|
||||||
click.secho(f"{i:{num_digits}d}: {scene_name}", fg="green")
|
|
||||||
|
|
||||||
|
|
||||||
def _list_scenes(folder: Path) -> list[str]:
|
def _list_scenes(folder: Path) -> list[str]:
|
||||||
@ -137,7 +130,7 @@ def start_at_callback(
|
|||||||
|
|
||||||
|
|
||||||
@click.command()
|
@click.command()
|
||||||
@scenes_argument
|
@click.argument("scenes", nargs=-1)
|
||||||
@config_path_option
|
@config_path_option
|
||||||
@folder_path_option
|
@folder_path_option
|
||||||
@click.option("--start-paused", is_flag=True, help="Start paused.")
|
@click.option("--start-paused", is_flag=True, help="Start paused.")
|
||||||
@ -283,7 +276,7 @@ def present( # noqa: C901
|
|||||||
if skip_all:
|
if skip_all:
|
||||||
exit_after_last_slide = True
|
exit_after_last_slide = True
|
||||||
|
|
||||||
presentation_configs = [PresentationConfig.from_file(path) for path in scenes]
|
presentation_configs = get_scenes_presentation_config(scenes, folder)
|
||||||
|
|
||||||
if config_path.exists():
|
if config_path.exists():
|
||||||
try:
|
try:
|
@ -1,4 +1,14 @@
|
|||||||
"""Manim Slides' rendering commands."""
|
"""
|
||||||
|
Alias command to either
|
||||||
|
``manim render [OPTIONS] [ARGS]...`` or
|
||||||
|
``manimgl -w [OPTIONS] [ARGS]...``.
|
||||||
|
|
||||||
|
This is especially useful for two reasons:
|
||||||
|
|
||||||
|
1. You can are sure to execute the rendering command with the same Python environment
|
||||||
|
as for ``manim-slides``.
|
||||||
|
2. You can pass options to the config.
|
||||||
|
"""
|
||||||
|
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
@ -34,22 +44,11 @@ def render(ce: bool, gl: bool, args: tuple[str, ...]) -> None:
|
|||||||
|
|
||||||
Use ``manim-slides render --help`` to see help information for
|
Use ``manim-slides render --help`` to see help information for
|
||||||
a specific renderer.
|
a specific renderer.
|
||||||
|
|
||||||
Alias command to either
|
|
||||||
``manim render [OPTIONS] [ARGS]...`` or
|
|
||||||
``manimgl [OPTIONS] [ARGS]...``.
|
|
||||||
|
|
||||||
This is especially useful for two reasons:
|
|
||||||
|
|
||||||
1. You can are sure to execute the rendering command with the same Python environment
|
|
||||||
as for ``manim-slides``.
|
|
||||||
2. You can pass options to the config.
|
|
||||||
"""
|
"""
|
||||||
if ce and gl:
|
if ce and gl:
|
||||||
raise click.UsageError("You cannot specify both --CE and --GL renderers.")
|
raise click.UsageError("You cannot specify both --CE and --GL renderers.")
|
||||||
if gl:
|
if gl:
|
||||||
subprocess.run([sys.executable, "-m", "manimlib", "-w", *args])
|
completed = subprocess.run([sys.executable, "-m", "manimlib", "-w", *args])
|
||||||
else:
|
else:
|
||||||
from manim.cli.render.commands import render as render_ce
|
completed = subprocess.run([sys.executable, "-m", "manim", "render", *args])
|
||||||
|
sys.exit(completed.returncode)
|
||||||
render_ce(args, standalone_mode=False)
|
|
@ -1,12 +1,10 @@
|
|||||||
"""Slides module with logic to either import ManimCE or ManimGL."""
|
__all__ = [
|
||||||
|
|
||||||
__all__ = (
|
|
||||||
"API_NAME",
|
"API_NAME",
|
||||||
"MANIM",
|
"MANIM",
|
||||||
"MANIMGL",
|
"MANIMGL",
|
||||||
"Slide",
|
"Slide",
|
||||||
"ThreeDSlide",
|
"ThreeDSlide",
|
||||||
)
|
]
|
||||||
|
|
||||||
|
|
||||||
import os
|
import os
|
||||||
@ -16,10 +14,10 @@ import sys
|
|||||||
class ManimApiNotFoundError(ImportError):
|
class ManimApiNotFoundError(ImportError):
|
||||||
"""Error raised if specified manim API could be imported."""
|
"""Error raised if specified manim API could be imported."""
|
||||||
|
|
||||||
_msg = "Could not import the specified manim API: `{api}`."
|
_msg = "Could not import the specified manim API"
|
||||||
|
|
||||||
def __init__(self, api: str) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__(self._msg.format(api=api))
|
super().__init__(self._msg)
|
||||||
|
|
||||||
|
|
||||||
API_NAMES = {
|
API_NAMES = {
|
||||||
@ -28,12 +26,9 @@ API_NAMES = {
|
|||||||
"manimlib": "manimlib",
|
"manimlib": "manimlib",
|
||||||
"manimgl": "manimlib",
|
"manimgl": "manimlib",
|
||||||
}
|
}
|
||||||
"""Allowed values for API."""
|
|
||||||
|
|
||||||
MANIM_API: str = "MANIM_API"
|
MANIM_API: str = "MANIM_API"
|
||||||
"""API environ variable name."""
|
|
||||||
FORCE_MANIM_API: str = "FORCE_" + MANIM_API
|
FORCE_MANIM_API: str = "FORCE_" + MANIM_API
|
||||||
"""FORCE API environ variable name."""
|
|
||||||
|
|
||||||
API: str = os.environ.get(MANIM_API, "manim").lower()
|
API: str = os.environ.get(MANIM_API, "manim").lower()
|
||||||
|
|
||||||
@ -58,14 +53,11 @@ if MANIM:
|
|||||||
try:
|
try:
|
||||||
from .manim import Slide, ThreeDSlide
|
from .manim import Slide, ThreeDSlide
|
||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
raise ManimApiNotFoundError("manim") from e
|
raise ManimApiNotFoundError from e
|
||||||
elif MANIMGL:
|
elif MANIMGL:
|
||||||
try:
|
try:
|
||||||
from .manimlib import Slide, ThreeDSlide
|
from .manimlib import Slide, ThreeDSlide
|
||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
raise ManimApiNotFoundError("manimlib") from e
|
raise ManimApiNotFoundError from e
|
||||||
else:
|
else:
|
||||||
raise ValueError(
|
raise ManimApiNotFoundError
|
||||||
"This error should never occur. "
|
|
||||||
"Please report an issue on GitHub if you encounter it."
|
|
||||||
)
|
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
"""Base class for the Slide class."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
__all__ = ("BaseSlide",)
|
__all__ = ["BaseSlide"]
|
||||||
|
|
||||||
import platform
|
import platform
|
||||||
import shutil
|
import shutil
|
||||||
@ -17,15 +15,10 @@ from typing import (
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
from tqdm import tqdm
|
from tqdm import tqdm
|
||||||
|
|
||||||
from ..core.config import (
|
from ..config import BaseSlideConfig, PresentationConfig, PreSlideConfig, SlideConfig
|
||||||
BaseSlideConfig,
|
from ..defaults import FOLDER_PATH
|
||||||
PresentationConfig,
|
from ..logger import logger
|
||||||
PreSlideConfig,
|
from ..utils import concatenate_video_files, merge_basenames, reverse_video_file
|
||||||
SlideConfig,
|
|
||||||
)
|
|
||||||
from ..core.defaults import FOLDER_PATH
|
|
||||||
from ..core.logger import logger
|
|
||||||
from ..core.utils import concatenate_video_files, merge_basenames, reverse_video_file
|
|
||||||
from . import MANIM
|
from . import MANIM
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
"""Manim's implementation of the Slide class."""
|
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
@ -7,7 +5,7 @@ from manim import Scene, ThreeDScene, config
|
|||||||
from manim.renderer.opengl_renderer import OpenGLRenderer
|
from manim.renderer.opengl_renderer import OpenGLRenderer
|
||||||
from manim.utils.color import rgba_to_color
|
from manim.utils.color import rgba_to_color
|
||||||
|
|
||||||
from ..core.config import BaseSlideConfig
|
from ..config import BaseSlideConfig
|
||||||
from .base import BaseSlide
|
from .base import BaseSlide
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
"""ManimGL's implementation of the Slide class."""
|
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, ClassVar, Optional
|
from typing import Any, ClassVar, Optional
|
||||||
|
|
||||||
|
@ -3,9 +3,10 @@ from pathlib import Path
|
|||||||
|
|
||||||
import click
|
import click
|
||||||
|
|
||||||
from ...core.config import Config
|
|
||||||
from ...core.logger import logger
|
|
||||||
from ..commons import config_options, verbosity_option
|
from ..commons import config_options, verbosity_option
|
||||||
|
from ..config import Config
|
||||||
|
from ..defaults import CONFIG_PATH
|
||||||
|
from ..logger import logger
|
||||||
|
|
||||||
|
|
||||||
@click.command()
|
@click.command()
|
||||||
@ -36,7 +37,7 @@ def _init(
|
|||||||
mode.
|
mode.
|
||||||
"""
|
"""
|
||||||
if config_path.exists():
|
if config_path.exists():
|
||||||
logger.debug(f"The `{config_path}` configuration file exists")
|
click.secho(f"The `{CONFIG_PATH}` configuration file exists")
|
||||||
|
|
||||||
if not force and not merge:
|
if not force and not merge:
|
||||||
choice = click.prompt(
|
choice = click.prompt(
|
||||||
@ -56,7 +57,7 @@ def _init(
|
|||||||
if force:
|
if force:
|
||||||
logger.debug(f"Overwriting `{config_path}` if exists")
|
logger.debug(f"Overwriting `{config_path}` if exists")
|
||||||
elif merge:
|
elif merge:
|
||||||
logger.debug(f"Merging new config into `{config_path}`")
|
logger.debug("Merging new config into `{config_path}`")
|
||||||
|
|
||||||
if not skip_interactive:
|
if not skip_interactive:
|
||||||
if config_path.exists():
|
if config_path.exists():
|
||||||
@ -81,4 +82,4 @@ def _init(
|
|||||||
|
|
||||||
config.to_file(config_path)
|
config.to_file(config_path)
|
||||||
|
|
||||||
logger.debug(f"Configuration file successfully saved to `{config_path}`")
|
click.secho(f"Configuration file successfully saved to `{config_path}`")
|
@ -88,7 +88,7 @@ pyside6-full = ["manim-slides[full,pyside6]"]
|
|||||||
sphinx-directive = ["docutils>=0.20.1", "manim-slides[manim]"]
|
sphinx-directive = ["docutils>=0.20.1", "manim-slides[manim]"]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
manim-slides = "manim_slides.cli.commands:main"
|
manim-slides = "manim_slides.__main__:cli"
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
Changelog = "https://github.com/jeertmans/manim-slides/releases"
|
Changelog = "https://github.com/jeertmans/manim-slides/releases"
|
||||||
@ -208,9 +208,10 @@ filterwarnings = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
extend-exclude = ["manim_slides/cli/resources.py"]
|
extend-exclude = ["manim_slides/resources.py"]
|
||||||
extend-include = ["*.ipynb"]
|
extend-include = ["*.ipynb"]
|
||||||
line-length = 88
|
line-length = 88
|
||||||
|
target-version = "py39"
|
||||||
|
|
||||||
[tool.ruff.lint]
|
[tool.ruff.lint]
|
||||||
extend-ignore = [
|
extend-ignore = [
|
||||||
|
@ -42,3 +42,8 @@ class BasicSlide(Slide):
|
|||||||
|
|
||||||
class BasicSlideSkipReversing(BasicSlide):
|
class BasicSlideSkipReversing(BasicSlide):
|
||||||
skip_reversing = True
|
skip_reversing = True
|
||||||
|
|
||||||
|
|
||||||
|
class FailingSlide(Slide):
|
||||||
|
def construct(self):
|
||||||
|
self.play("this fails to render")
|
||||||
|
@ -113,6 +113,37 @@ def test_render_basic_slide(
|
|||||||
assert local_presentation_config.resolution == presentation_config.resolution
|
assert local_presentation_config.resolution == presentation_config.resolution
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"renderer",
|
||||||
|
[
|
||||||
|
"--CE",
|
||||||
|
pytest.param(
|
||||||
|
"--GL",
|
||||||
|
marks=pytest.mark.skipif(
|
||||||
|
sys.version_info < (3, 10),
|
||||||
|
reason="See https://github.com/3b1b/manim/issues/2263.",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
"--CE --renderer=opengl",
|
||||||
|
],
|
||||||
|
ids=("CE", "GL", "CE(GL)"),
|
||||||
|
)
|
||||||
|
def test_render_failing_slide(
|
||||||
|
renderer: str,
|
||||||
|
slides_file: Path,
|
||||||
|
manimgl_config: Path,
|
||||||
|
) -> None:
|
||||||
|
runner = CliRunner()
|
||||||
|
|
||||||
|
with runner.isolated_filesystem() as tmp_dir:
|
||||||
|
shutil.copy(manimgl_config, tmp_dir)
|
||||||
|
results = runner.invoke(
|
||||||
|
render, [*renderer.split(" "), str(slides_file), "FailingSlide", "-ql"]
|
||||||
|
)
|
||||||
|
|
||||||
|
assert results.exit_code != 0, results
|
||||||
|
|
||||||
|
|
||||||
def test_clear_cache(
|
def test_clear_cache(
|
||||||
slides_file: Path,
|
slides_file: Path,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
Reference in New Issue
Block a user