mirror of
https://github.com/ManimCommunity/manim.git
synced 2026-06-29 10:02:11 +00:00
* test * revert test changes * first logging changes * fix unrelated typo * change to logging * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Save logs to csv file for later reading it * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix makefile issues * remove unnecessary comments * clean up manim_directive * fix local var bug * Show log only if examples have been rendered * Move controll from makefile to sphinx events * remove added blank line * remove rendering_summary file * fix slight regex problem * comment out code in question * change encoding * re-add code * switch to build-finished event * fix indentation bug * use csv module * Update manim/mobject/mobject.py Co-authored-by: Jason Villanueva <a@jsonvillanueva.com> * Update manim/mobject/mobject.py Co-authored-by: Jason Villanueva <a@jsonvillanueva.com> * Use Pathlib Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Jason Villanueva <a@jsonvillanueva.com> Co-authored-by: Jan-Hendrik Müller <44469195+kolibril13@users.noreply.github.com>
378 lines
11 KiB
Python
378 lines
11 KiB
Python
r"""
|
|
A directive for including Manim videos in a Sphinx document
|
|
===========================================================
|
|
|
|
When rendering the HTML documentation, the ``.. manim::`` directive
|
|
implemented here allows to include rendered videos.
|
|
|
|
Its basic usage that allows processing **inline content**
|
|
looks as follows::
|
|
|
|
.. manim:: MyScene
|
|
|
|
class MyScene(Scene):
|
|
def construct(self):
|
|
...
|
|
|
|
It is required to pass the name of the class representing the
|
|
scene to be rendered to the directive.
|
|
|
|
As a second application, the directive can also be used to
|
|
render scenes that are defined within doctests, for example::
|
|
|
|
.. manim:: DirectiveDoctestExample
|
|
:ref_classes: Dot
|
|
|
|
>>> dot = Dot(color=RED)
|
|
>>> dot.color
|
|
<Color #fc6255>
|
|
>>> class DirectiveDoctestExample(Scene):
|
|
... def construct(self):
|
|
... self.play(Create(dot))
|
|
|
|
|
|
Options
|
|
-------
|
|
|
|
Options can be passed as follows::
|
|
|
|
.. manim:: <Class name>
|
|
:<option name>: <value>
|
|
|
|
The following configuration options are supported by the
|
|
directive:
|
|
|
|
hide_source
|
|
If this flag is present without argument,
|
|
the source code is not displayed above the rendered video.
|
|
|
|
quality : {'low', 'medium', 'high', 'fourk'}
|
|
Controls render quality of the video, in analogy to
|
|
the corresponding command line flags.
|
|
|
|
save_as_gif
|
|
If this flag is present without argument,
|
|
the scene is rendered as a gif.
|
|
|
|
save_last_frame
|
|
If this flag is present without argument,
|
|
an image representing the last frame of the scene will
|
|
be rendered and displayed, instead of a video.
|
|
|
|
ref_classes
|
|
A list of classes, separated by spaces, that is
|
|
rendered in a reference block after the source code.
|
|
|
|
ref_functions
|
|
A list of functions, separated by spaces,
|
|
that is rendered in a reference block after the source code.
|
|
|
|
ref_methods
|
|
A list of methods, separated by spaces,
|
|
that is rendered in a reference block after the source code.
|
|
|
|
"""
|
|
import csv
|
|
import itertools as it
|
|
import os
|
|
import re
|
|
import shutil
|
|
import sys
|
|
from pathlib import Path
|
|
from timeit import timeit
|
|
from typing import Callable, List
|
|
|
|
import jinja2
|
|
from docutils import nodes
|
|
from docutils.parsers.rst import Directive, directives
|
|
from docutils.statemachine import StringList
|
|
|
|
from manim import QUALITIES
|
|
|
|
classnamedict = {}
|
|
|
|
|
|
class skip_manim_node(nodes.Admonition, nodes.Element):
|
|
pass
|
|
|
|
|
|
def visit(self, node, name=""):
|
|
self.visit_admonition(node, name)
|
|
|
|
|
|
def depart(self, node):
|
|
self.depart_admonition(node)
|
|
|
|
|
|
def process_name_list(option_input: str, reference_type: str) -> List[str]:
|
|
r"""Reformats a string of space separated class names
|
|
as a list of strings containing valid Sphinx references.
|
|
|
|
Tests
|
|
-----
|
|
|
|
::
|
|
|
|
>>> process_name_list("Tex TexTemplate", "class")
|
|
[":class:`~.Tex`", ":class:`~.TexTemplate`"]
|
|
>>> process_name_list("Scene.play Mobject.rotate", "func")
|
|
[":func:`~.Scene.play`", ":func:`~.Mobject.rotate`"]
|
|
"""
|
|
return [f":{reference_type}:`~.{name}`" for name in option_input.split()]
|
|
|
|
|
|
class ManimDirective(Directive):
|
|
r"""The manim directive, rendering videos while building
|
|
the documentation.
|
|
|
|
See the module docstring for documentation.
|
|
"""
|
|
has_content = True
|
|
required_arguments = 1
|
|
optional_arguments = 0
|
|
option_spec = {
|
|
"hide_source": bool,
|
|
"quality": lambda arg: directives.choice(
|
|
arg, ("low", "medium", "high", "fourk")
|
|
),
|
|
"save_as_gif": bool,
|
|
"save_last_frame": bool,
|
|
"ref_modules": lambda arg: process_name_list(arg, "mod"),
|
|
"ref_classes": lambda arg: process_name_list(arg, "class"),
|
|
"ref_functions": lambda arg: process_name_list(arg, "func"),
|
|
"ref_methods": lambda arg: process_name_list(arg, "meth"),
|
|
}
|
|
final_argument_whitespace = True
|
|
|
|
def run(self):
|
|
if "skip-manim" in self.state.document.settings.env.app.builder.tags.tags:
|
|
node = skip_manim_node()
|
|
self.state.nested_parse(
|
|
StringList(self.content[0]), self.content_offset, node
|
|
)
|
|
return [node]
|
|
|
|
from manim import config
|
|
|
|
global classnamedict
|
|
|
|
clsname = self.arguments[0]
|
|
if clsname not in classnamedict:
|
|
classnamedict[clsname] = 1
|
|
else:
|
|
classnamedict[clsname] += 1
|
|
|
|
hide_source = "hide_source" in self.options
|
|
save_as_gif = "save_as_gif" in self.options
|
|
save_last_frame = "save_last_frame" in self.options
|
|
assert not (save_as_gif and save_last_frame)
|
|
|
|
ref_content = (
|
|
self.options.get("ref_modules", [])
|
|
+ self.options.get("ref_classes", [])
|
|
+ self.options.get("ref_functions", [])
|
|
+ self.options.get("ref_methods", [])
|
|
)
|
|
if ref_content:
|
|
ref_block = "References: " + " ".join(ref_content)
|
|
|
|
else:
|
|
ref_block = ""
|
|
|
|
if "quality" in self.options:
|
|
quality = f'{self.options["quality"]}_quality'
|
|
else:
|
|
quality = "example_quality"
|
|
frame_rate = QUALITIES[quality]["frame_rate"]
|
|
pixel_height = QUALITIES[quality]["pixel_height"]
|
|
pixel_width = QUALITIES[quality]["pixel_width"]
|
|
qualitydir = f"{pixel_height}p{frame_rate}"
|
|
|
|
state_machine = self.state_machine
|
|
document = state_machine.document
|
|
|
|
source_file_name = Path(document.attributes["source"])
|
|
source_rel_name = source_file_name.relative_to(setup.confdir)
|
|
source_rel_dir = source_rel_name.parents[0]
|
|
dest_dir = Path(setup.app.builder.outdir, source_rel_dir).absolute()
|
|
if not dest_dir.exists():
|
|
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
source_block = [
|
|
".. code-block:: python",
|
|
"",
|
|
" from manim import *\n",
|
|
*[" " + line for line in self.content],
|
|
]
|
|
source_block = "\n".join(source_block)
|
|
|
|
config.media_dir = (Path(setup.confdir) / "media").absolute()
|
|
config.images_dir = "{media_dir}/images"
|
|
config.video_dir = "{media_dir}/videos/{quality}"
|
|
output_file = f"{clsname}-{classnamedict[clsname]}"
|
|
config.assets_dir = Path("_static")
|
|
config.progress_bar = "none"
|
|
config.verbosity = "WARNING"
|
|
|
|
config_code = [
|
|
f'config["frame_rate"] = {frame_rate}',
|
|
f'config["pixel_height"] = {pixel_height}',
|
|
f'config["pixel_width"] = {pixel_width}',
|
|
f'config["save_last_frame"] = {save_last_frame}',
|
|
f'config["write_to_movie"] = {not save_last_frame}',
|
|
f'config["output_file"] = r"{output_file}"',
|
|
]
|
|
if save_last_frame:
|
|
config_code.append('config["format"] = None')
|
|
if save_as_gif:
|
|
config_code.append('config["format"] = "gif"')
|
|
|
|
user_code = self.content
|
|
if user_code[0].startswith(">>> "): # check whether block comes from doctest
|
|
user_code = [
|
|
line[4:] for line in user_code if line.startswith((">>> ", "... "))
|
|
]
|
|
|
|
code = [
|
|
"from manim import *",
|
|
*config_code,
|
|
*user_code,
|
|
f"{clsname}().render()",
|
|
]
|
|
run_time = timeit(lambda: exec("\n".join(code), globals()), number=1)
|
|
|
|
_write_rendering_stats(
|
|
clsname,
|
|
run_time,
|
|
self.state.document.settings.env.docname,
|
|
)
|
|
|
|
# copy video file to output directory
|
|
if not (save_as_gif or save_last_frame):
|
|
filename = f"{output_file}.mp4"
|
|
filesrc = config.get_dir("video_dir") / filename
|
|
destfile = Path(dest_dir, filename)
|
|
shutil.copyfile(filesrc, destfile)
|
|
elif save_as_gif:
|
|
filename = f"{output_file}.gif"
|
|
filesrc = config.get_dir("video_dir") / filename
|
|
elif save_last_frame:
|
|
filename = f"{output_file}.png"
|
|
filesrc = config.get_dir("images_dir") / filename
|
|
else:
|
|
raise ValueError("Invalid combination of render flags received.")
|
|
rendered_template = jinja2.Template(TEMPLATE).render(
|
|
clsname=clsname,
|
|
clsname_lowercase=clsname.lower(),
|
|
hide_source=hide_source,
|
|
filesrc_rel=Path(filesrc).relative_to(setup.confdir).as_posix(),
|
|
output_file=output_file,
|
|
save_last_frame=save_last_frame,
|
|
save_as_gif=save_as_gif,
|
|
source_block=source_block,
|
|
ref_block=ref_block,
|
|
)
|
|
state_machine.insert_input(
|
|
rendered_template.split("\n"), source=document.attributes["source"]
|
|
)
|
|
|
|
return []
|
|
|
|
|
|
rendering_times_file_path = Path("../rendering_times.csv")
|
|
|
|
|
|
def _write_rendering_stats(scene_name, run_time, file_name):
|
|
with open(rendering_times_file_path, "a") as file:
|
|
csv.writer(file).writerow(
|
|
[
|
|
re.sub("^(reference\/)|(manim\.)", "", file_name),
|
|
scene_name,
|
|
"%.3f" % run_time,
|
|
]
|
|
)
|
|
|
|
|
|
def _log_rendering_times(*args):
|
|
if rendering_times_file_path.exists():
|
|
with open(rendering_times_file_path) as file:
|
|
data = list(csv.reader(file))
|
|
if len(data) == 0:
|
|
sys.exit()
|
|
|
|
print("\nRendering Summary\n-----------------\n")
|
|
|
|
max_file_length = max([len(row[0]) for row in data])
|
|
for key, group in it.groupby(data, key=lambda row: row[0]):
|
|
key = key.ljust(max_file_length + 1, ".")
|
|
group = list(group)
|
|
if len(group) == 1:
|
|
row = group[0]
|
|
print(f"{key}{row[2].rjust(7, '.')}s {row[1]}")
|
|
continue
|
|
time_sum = sum([float(row[2]) for row in group])
|
|
print(
|
|
f"{key}{f'{time_sum:.3f}'.rjust(7, '.')}s => {len(group)} EXAMPLES"
|
|
)
|
|
for row in group:
|
|
print(f"{' '*(max_file_length)} {row[2].rjust(7)}s {row[1]}")
|
|
print("")
|
|
|
|
|
|
def _delete_rendering_times(*args):
|
|
if rendering_times_file_path.exists():
|
|
os.remove(rendering_times_file_path)
|
|
|
|
|
|
def setup(app):
|
|
import manim
|
|
|
|
app.add_node(skip_manim_node, html=(visit, depart))
|
|
|
|
setup.app = app
|
|
setup.config = app.config
|
|
setup.confdir = app.confdir
|
|
|
|
app.add_directive("manim", ManimDirective)
|
|
|
|
app.connect("builder-inited", _delete_rendering_times)
|
|
app.connect("build-finished", _log_rendering_times)
|
|
|
|
metadata = {"parallel_read_safe": False, "parallel_write_safe": True}
|
|
return metadata
|
|
|
|
|
|
TEMPLATE = r"""
|
|
{% if not hide_source %}
|
|
.. raw:: html
|
|
|
|
<div id="{{ clsname_lowercase }}" class="admonition admonition-manim-example">
|
|
<p class="admonition-title">Example: {{ clsname }} <a class="headerlink" href="#{{ clsname_lowercase }}">¶</a></p>
|
|
|
|
{% endif %}
|
|
|
|
{% if not (save_as_gif or save_last_frame) %}
|
|
.. raw:: html
|
|
|
|
<video class="manim-video" controls loop autoplay src="./{{ output_file }}.mp4"></video>
|
|
|
|
{% elif save_as_gif %}
|
|
.. image:: /{{ filesrc_rel }}
|
|
:align: center
|
|
|
|
{% elif save_last_frame %}
|
|
.. image:: /{{ filesrc_rel }}
|
|
:align: center
|
|
|
|
{% endif %}
|
|
{% if not hide_source %}
|
|
{{ source_block }}
|
|
|
|
{{ ref_block }}
|
|
|
|
{% endif %}
|
|
|
|
.. raw:: html
|
|
|
|
</div>
|
|
"""
|