mirror of
https://github.com/ManimCommunity/manim.git
synced 2026-06-22 10:01:47 +00:00
Merge branch 'main' of https://github.com/ManimCommunity/manim into experimental
This commit is contained in:
commit
d6ea8412d6
38 changed files with 1587 additions and 470 deletions
|
|
@ -36,9 +36,9 @@ RUN pip install -r docs/requirements.txt
|
|||
|
||||
ARG NB_USER=manimuser
|
||||
ARG NB_UID=1000
|
||||
ENV USER ${NB_USER}
|
||||
ENV NB_UID ${NB_UID}
|
||||
ENV HOME /manim
|
||||
ENV USER=${NB_USER}
|
||||
ENV NB_UID=${NB_UID}
|
||||
ENV HOME=/manim
|
||||
|
||||
RUN adduser --disabled-password \
|
||||
--gecos "Default user" \
|
||||
|
|
|
|||
|
|
@ -759,7 +759,6 @@ its :class:`.FileWriter` to open an output container. The process
|
|||
is started by a call to ``libav`` and opens a container to which rendered
|
||||
raw frames can be written. As long as the output is open, the container
|
||||
can be accessed via the ``output_container`` attribute of the file writer.
|
||||
|
||||
With the writing process in place, the renderer then asks the scene
|
||||
to "begin" the animations.
|
||||
|
||||
|
|
@ -904,7 +903,6 @@ method is called.
|
|||
would be slightly longer than 1 second. We decided against this at some point.
|
||||
|
||||
In the end, the time progression is closed (which completes the displayed progress bar)
|
||||
in the terminal.
|
||||
|
||||
This pretty much concludes the walkthrough of a :class:`.Scene.play` call,
|
||||
and actually there is not too much more to say for our toy example either: at
|
||||
|
|
|
|||
|
|
@ -99,6 +99,11 @@ def make_logger(
|
|||
logger = logging.getLogger("manim")
|
||||
logger.addHandler(rich_handler)
|
||||
logger.setLevel(verbosity)
|
||||
logger.propagate = False
|
||||
|
||||
if not (libav_logger := logging.getLogger()).hasHandlers():
|
||||
libav_logger.addHandler(rich_handler)
|
||||
libav_logger.setLevel(verbosity)
|
||||
|
||||
if not (libav_logger := logging.getLogger()).hasHandlers():
|
||||
libav_logger.addHandler(rich_handler)
|
||||
|
|
|
|||
|
|
@ -40,6 +40,8 @@ if TYPE_CHECKING:
|
|||
|
||||
__all__ = ["config_file_paths", "make_config_parser", "ManimConfig", "ManimFrame"]
|
||||
|
||||
logger = logging.getLogger("manim")
|
||||
|
||||
|
||||
def config_file_paths() -> list[Path]:
|
||||
"""The paths where ``.cfg`` files will be searched for.
|
||||
|
|
@ -812,7 +814,7 @@ class ManimConfig(MutableMapping):
|
|||
try:
|
||||
self.upto_animation_number = nflag[1]
|
||||
except Exception:
|
||||
logging.getLogger("manim").info(
|
||||
logger.info(
|
||||
f"No end scene number specified in -n option. Rendering from {nflag[0]} onwards...",
|
||||
)
|
||||
|
||||
|
|
@ -1052,7 +1054,7 @@ class ManimConfig(MutableMapping):
|
|||
val,
|
||||
["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
|
||||
)
|
||||
logging.getLogger("manim").setLevel(val)
|
||||
logger.setLevel(val)
|
||||
|
||||
@property
|
||||
def format(self) -> str:
|
||||
|
|
@ -1282,6 +1284,8 @@ class ManimConfig(MutableMapping):
|
|||
@background_opacity.setter
|
||||
def background_opacity(self, value: float) -> None:
|
||||
self._set_between("background_opacity", value, 0, 1)
|
||||
if self.background_opacity < 1:
|
||||
self.resolve_movie_file_extension(is_transparent=True)
|
||||
|
||||
@property
|
||||
def frame_size(self) -> tuple[int, int]:
|
||||
|
|
@ -1316,8 +1320,8 @@ class ManimConfig(MutableMapping):
|
|||
|
||||
@property
|
||||
def transparent(self) -> bool:
|
||||
"""Whether the background opacity is 0.0 (-t)."""
|
||||
return self._d["background_opacity"] == 0.0
|
||||
"""Whether the background opacity is less than 1.0 (-t)."""
|
||||
return self._d["background_opacity"] < 1.0
|
||||
|
||||
@transparent.setter
|
||||
def transparent(self, value: bool) -> None:
|
||||
|
|
@ -1435,6 +1439,7 @@ class ManimConfig(MutableMapping):
|
|||
self._d.__setitem__("window_size", value)
|
||||
|
||||
def resolve_movie_file_extension(self, is_transparent: bool) -> None:
|
||||
prev_file_extension = self.movie_file_extension
|
||||
if is_transparent:
|
||||
self.movie_file_extension = ".webm" if self.format == "webm" else ".mov"
|
||||
elif self.format == "webm":
|
||||
|
|
@ -1443,6 +1448,11 @@ class ManimConfig(MutableMapping):
|
|||
self.movie_file_extension = ".mov"
|
||||
else:
|
||||
self.movie_file_extension = ".mp4"
|
||||
if self.movie_file_extension != prev_file_extension:
|
||||
logger.warning(
|
||||
f"Output format changed to '{self.movie_file_extension}' "
|
||||
"to support transparency",
|
||||
)
|
||||
|
||||
@property
|
||||
def enable_gui(self) -> bool:
|
||||
|
|
@ -1788,7 +1798,7 @@ class ManimConfig(MutableMapping):
|
|||
def tex_template_file(self, val: str) -> None:
|
||||
if val:
|
||||
if not os.access(val, os.R_OK):
|
||||
logging.getLogger("manim").warning(
|
||||
logger.warning(
|
||||
f"Custom TeX template {val} not found or not readable.",
|
||||
)
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ if TYPE_CHECKING:
|
|||
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVGroup
|
||||
from manim.mobject.types.vectorized_mobject import VGroup
|
||||
|
||||
|
||||
__all__ = ["AnimationGroup", "Succession", "LaggedStart", "LaggedStartMap"]
|
||||
|
||||
|
||||
|
|
@ -81,15 +80,16 @@ class AnimationGroup(Animation):
|
|||
return list(self.group)
|
||||
|
||||
def begin(self) -> None:
|
||||
for anim in self.animations:
|
||||
anim.begin()
|
||||
self.process_subanimation_buffer(anim.buffer)
|
||||
if not self.animations:
|
||||
raise ValueError(
|
||||
f"Trying to play {self} without animations, this is not supported. "
|
||||
"Please add at least one subanimation."
|
||||
)
|
||||
|
||||
for anim in self.animations:
|
||||
anim.begin()
|
||||
self.process_subanimation_buffer(anim.buffer)
|
||||
|
||||
self.anim_group_time = 0.0
|
||||
if self.suspend_mobject_updating:
|
||||
self.group.suspend_updating()
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ from manim.mobject.geometry.arc import Circle, Dot
|
|||
from manim.mobject.geometry.line import Line
|
||||
from manim.mobject.geometry.polygram import Rectangle
|
||||
from manim.mobject.geometry.shape_matchers import SurroundingRectangle
|
||||
from manim.scene.scene import Scene
|
||||
|
||||
from .. import config
|
||||
from ..animation.animation import Animation
|
||||
|
|
|
|||
|
|
@ -41,9 +41,7 @@ def always_redraw(func: Callable[[], Mobject]) -> Mobject:
|
|||
A function without (required) input arguments that returns
|
||||
a mobject.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
Examples --------
|
||||
.. manim:: TangentAnimation
|
||||
|
||||
class TangentAnimation(Scene):
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
"""A camera that controls the FOV, orientation, and position of the scene."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ from manim.utils.module_ops import scene_classes_from_file
|
|||
|
||||
__all__ = ["render"]
|
||||
|
||||
__all__ = ["render"]
|
||||
|
||||
|
||||
@cloup.command(
|
||||
context_settings=None,
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import re
|
||||
|
||||
from cloup import Choice, option, option_group
|
||||
|
||||
from ... import logger
|
||||
|
||||
__all__ = ["global_options"]
|
||||
|
||||
|
||||
logger = logging.getLogger("manim")
|
||||
|
||||
def validate_gui_location(ctx, param, value):
|
||||
if value:
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import re
|
||||
|
||||
from cloup import Choice, option, option_group
|
||||
|
||||
from manim.constants import QUALITIES, RendererType
|
||||
|
||||
from ... import logger
|
||||
__all__ = ["render_options"]
|
||||
|
||||
logger = logging.getLogger("manim")
|
||||
|
||||
__all__ = ["render_options"]
|
||||
|
||||
|
|
|
|||
|
|
@ -58,7 +58,6 @@ if TYPE_CHECKING:
|
|||
NonTimeBasedUpdater: TypeAlias = Callable[["Mobject"], object]
|
||||
Updater: TypeAlias = NonTimeBasedUpdater | TimeBasedUpdater
|
||||
|
||||
|
||||
class Mobject:
|
||||
"""Mathematical Object: base class for objects that can be displayed on screen.
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ from __future__ import annotations
|
|||
import copy
|
||||
import inspect
|
||||
import itertools as it
|
||||
import logging
|
||||
import numbers
|
||||
import os
|
||||
import pickle
|
||||
|
|
@ -17,7 +18,7 @@ import moderngl
|
|||
import numpy as np
|
||||
from typing_extensions import TypeVar
|
||||
|
||||
from manim import config, logger
|
||||
from manim import config
|
||||
from manim.constants import *
|
||||
from manim.event_handler import EVENT_DISPATCHER
|
||||
from manim.event_handler.event_listener import EventListener
|
||||
|
|
@ -72,6 +73,8 @@ if TYPE_CHECKING:
|
|||
R = TypeVar("R", bound="RendererData")
|
||||
T_co = TypeVar("T_co", covariant=True, bound="OpenGLMobject")
|
||||
|
||||
logger = logging.getLogger("manim")
|
||||
|
||||
UNIFORM_DTYPE = np.float64
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -41,11 +41,19 @@ from manim.utils.space_ops import (
|
|||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Iterable, Sequence
|
||||
from typing import Callable
|
||||
from collections.abc import Callable, Iterable, Sequence
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
__all__ = [
|
||||
"OpenGLVMobject",
|
||||
"OpenGLVGroup",
|
||||
"OpenGLVectorizedPoint",
|
||||
"OpenGLCurvesAsSubmobjects",
|
||||
"OpenGLDashedVMobject",
|
||||
]
|
||||
|
||||
|
||||
DEFAULT_STROKE_COLOR = GREY_A
|
||||
DEFAULT_FILL_COLOR = GREY_C
|
||||
|
||||
|
|
@ -117,6 +125,10 @@ class OpenGLVMobject(OpenGLMobject):
|
|||
super().__init__(**kwargs)
|
||||
# self.refresh_unit_normal()
|
||||
|
||||
def _assert_valid_submobjects(self, submobjects: Iterable[OpenGLVMobject]) -> Self:
|
||||
return self._assert_valid_submobjects_internal(submobjects, OpenGLVMobject)
|
||||
|
||||
|
||||
def get_group_class(self) -> type[OpenGLVGroup]: # type: ignore
|
||||
return OpenGLVGroup
|
||||
|
||||
|
|
@ -982,6 +994,52 @@ class OpenGLVMobject(OpenGLMobject):
|
|||
for n in range(num_curves):
|
||||
yield self.get_nth_curve_function_with_length(n, **kwargs)
|
||||
|
||||
def point_from_proportion(self, alpha: float) -> np.ndarray:
|
||||
"""Gets the point at a proportion along the path of the :class:`OpenGLVMobject`.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
alpha
|
||||
The proportion along the the path of the :class:`OpenGLVMobject`.
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`numpy.ndarray`
|
||||
The point on the :class:`OpenGLVMobject`.
|
||||
|
||||
Raises
|
||||
------
|
||||
:exc:`ValueError`
|
||||
If ``alpha`` is not between 0 and 1.
|
||||
:exc:`Exception`
|
||||
If the :class:`OpenGLVMobject` has no points.
|
||||
"""
|
||||
|
||||
if alpha < 0 or alpha > 1:
|
||||
raise ValueError(f"Alpha {alpha} not between 0 and 1.")
|
||||
|
||||
self.throw_error_if_no_points()
|
||||
if alpha == 1:
|
||||
return self.points[-1]
|
||||
|
||||
curves_and_lengths = tuple(self.get_curve_functions_with_lengths())
|
||||
|
||||
target_length = alpha * np.sum(
|
||||
np.fromiter((length for _, length in curves_and_lengths), dtype=np.float64)
|
||||
)
|
||||
current_length = 0
|
||||
|
||||
for curve, length in curves_and_lengths:
|
||||
if current_length + length >= target_length:
|
||||
if length != 0:
|
||||
residue = (target_length - current_length) / length
|
||||
else:
|
||||
residue = 0
|
||||
|
||||
return curve(residue)
|
||||
|
||||
current_length += length
|
||||
|
||||
def proportion_from_point(
|
||||
self,
|
||||
point: Iterable[float | int],
|
||||
|
|
@ -1038,7 +1096,7 @@ class OpenGLVMobject(OpenGLMobject):
|
|||
|
||||
return alpha
|
||||
|
||||
def get_anchors_and_handles(self):
|
||||
def get_anchors_and_handles(self) -> Iterable[np.ndarray]:
|
||||
"""
|
||||
Returns anchors1, handles, anchors2,
|
||||
where (anchors1[i], handles[i], anchors2[i])
|
||||
|
|
@ -1070,12 +1128,12 @@ class OpenGLVMobject(OpenGLMobject):
|
|||
nppc = self.n_points_per_curve
|
||||
return self.points[nppc - 1 :: nppc]
|
||||
|
||||
def get_anchors(self) -> np.ndarray:
|
||||
def get_anchors(self) -> Iterable[np.ndarray]:
|
||||
"""Returns the anchors of the curves forming the OpenGLVMobject.
|
||||
|
||||
Returns
|
||||
-------
|
||||
np.ndarray
|
||||
Iterable[np.ndarray]
|
||||
The anchors.
|
||||
"""
|
||||
points = self.points
|
||||
|
|
@ -1239,8 +1297,8 @@ class OpenGLVMobject(OpenGLMobject):
|
|||
return path
|
||||
|
||||
for n in range(n_subpaths):
|
||||
sp1 = get_nth_subpath(subpaths1, n)
|
||||
sp2 = get_nth_subpath(subpaths2, n)
|
||||
sp1 = np.asarray(get_nth_subpath(subpaths1, n))
|
||||
sp2 = np.asarray(get_nth_subpath(subpaths2, n))
|
||||
diff1 = max(0, (len(sp2) - len(sp1)) // nppc)
|
||||
diff2 = max(0, (len(sp1) - len(sp2)) // nppc)
|
||||
sp1 = self.insert_n_curves_to_point_list(diff1, sp1)
|
||||
|
|
@ -1300,6 +1358,7 @@ class OpenGLVMobject(OpenGLMobject):
|
|||
new_points = new_bezier_tuples.reshape(-1, 3)
|
||||
return new_points
|
||||
|
||||
|
||||
def interpolate_color(self, mobject1, mobject2, alpha):
|
||||
attrs = [
|
||||
"fill_color",
|
||||
|
|
@ -1328,6 +1387,7 @@ class OpenGLVMobject(OpenGLMobject):
|
|||
setattr(self, attr, getattr(mobject2, attr))
|
||||
continue
|
||||
|
||||
|
||||
attr1 = getattr(mobject1, attr)
|
||||
attr2 = getattr(mobject2, attr)
|
||||
if isinstance(attr1, list) or isinstance(attr2, list):
|
||||
|
|
@ -1503,8 +1563,6 @@ class OpenGLVGroup(OpenGLVMobject):
|
|||
"""
|
||||
|
||||
def __init__(self, *vmobjects, **kwargs):
|
||||
if not all(isinstance(m, OpenGLVMobject) for m in vmobjects):
|
||||
raise Exception("All submobjects must be of type OpenGLVMobject")
|
||||
super().__init__(**kwargs)
|
||||
self.add(*vmobjects)
|
||||
|
||||
|
|
@ -1582,8 +1640,6 @@ class OpenGLVGroup(OpenGLVMobject):
|
|||
(gr-circle_red).animate.shift(RIGHT)
|
||||
)
|
||||
"""
|
||||
if not all(isinstance(m, OpenGLVMobject) for m in vmobjects):
|
||||
raise TypeError("All submobjects must be of type OpenGLVMobject")
|
||||
return super().add(*vmobjects)
|
||||
|
||||
def __add__(self, vmobject):
|
||||
|
|
@ -1629,8 +1685,7 @@ class OpenGLVGroup(OpenGLVMobject):
|
|||
|
||||
>>> config.renderer = original_renderer
|
||||
"""
|
||||
if not all(isinstance(m, OpenGLVMobject) for m in value):
|
||||
raise TypeError("All submobjects must be of type OpenGLVMobject")
|
||||
self._assert_valid_submobjects(tuplify(value))
|
||||
self.submobjects[key] = value # type: ignore
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -215,7 +215,7 @@ class Surface(VGroup):
|
|||
|
||||
def set_fill_by_value(
|
||||
self,
|
||||
axes: Mobject,
|
||||
axes: OpenGLMobject,
|
||||
colorscale: list[ParsableManimColor] | ParsableManimColor | None = None,
|
||||
axis: int = 2,
|
||||
**kwargs,
|
||||
|
|
@ -962,8 +962,8 @@ class Line3D(Cylinder):
|
|||
|
||||
def pointify(
|
||||
self,
|
||||
mob_or_point: Mobject | Point3D,
|
||||
direction: Vector3D = None,
|
||||
mob_or_point: OpenGLMobject | Point3D,
|
||||
direction: Vector3D | None = None,
|
||||
) -> np.ndarray:
|
||||
"""Gets a point representing the center of the :class:`Mobjects <.Mobject>`.
|
||||
|
||||
|
|
@ -979,7 +979,7 @@ class Line3D(Cylinder):
|
|||
:class:`numpy.array`
|
||||
Center of the :class:`Mobjects <.Mobject>` or point, or edge if direction is given.
|
||||
"""
|
||||
if isinstance(mob_or_point, (Mobject, OpenGLMobject)):
|
||||
if isinstance(mob_or_point, OpenGLMobject):
|
||||
mob = mob_or_point
|
||||
if direction is None:
|
||||
return mob.get_center()
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -8,8 +8,8 @@ from .plugins_flags import get_plugins, list_plugins
|
|||
__all__ = [
|
||||
"plugins",
|
||||
"Hooks",
|
||||
"get_plugins",
|
||||
"list_plugins",
|
||||
"get_plugins",
|
||||
]
|
||||
|
||||
requested_plugins: set[str] = set(config["plugins"])
|
||||
|
|
@ -17,4 +17,4 @@ missing_plugins = requested_plugins - set(get_plugins().keys())
|
|||
|
||||
|
||||
if missing_plugins:
|
||||
logger.warning("Missing Plugins: %s", missing_plugins)
|
||||
logger.warning(f"Missing Plugins: {missing_plugins}")
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ from manim.event_handler.window import WindowABC
|
|||
|
||||
__all__ = ["Window"]
|
||||
|
||||
__all__ = ["Window"]
|
||||
|
||||
|
||||
class Window(PygletWindow, WindowABC):
|
||||
name = "Manim Community"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import logging
|
||||
import re
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
|
|
@ -10,8 +11,6 @@ import numpy as np
|
|||
|
||||
from manim.utils.iterables import resize_array
|
||||
|
||||
from .. import logger
|
||||
|
||||
# Mobjects that should be rendered with
|
||||
# the same shader will be organized and
|
||||
# clumped together based on keeping track
|
||||
|
|
@ -20,6 +19,8 @@ from .. import logger
|
|||
|
||||
__all__ = ["ShaderWrapper"]
|
||||
|
||||
logger = logging.getLogger("manim")
|
||||
|
||||
|
||||
def get_shader_dir():
|
||||
return Path(__file__).parent / "shaders"
|
||||
|
|
|
|||
292
manim/renderer/vectorized_mobject_rendering.py
Normal file
292
manim/renderer/vectorized_mobject_rendering.py
Normal file
|
|
@ -0,0 +1,292 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import collections
|
||||
|
||||
import numpy as np
|
||||
|
||||
from ..utils import opengl
|
||||
from ..utils.space_ops import cross2d, earclip_triangulation
|
||||
from .shader import Shader
|
||||
|
||||
__all__ = [
|
||||
"render_opengl_vectorized_mobject_fill",
|
||||
"render_opengl_vectorized_mobject_stroke",
|
||||
]
|
||||
|
||||
|
||||
def build_matrix_lists(mob):
|
||||
root_hierarchical_matrix = mob.hierarchical_model_matrix()
|
||||
matrix_to_mobject_list = collections.defaultdict(list)
|
||||
if mob.has_points():
|
||||
matrix_to_mobject_list[tuple(root_hierarchical_matrix.ravel())].append(mob)
|
||||
mobject_to_hierarchical_matrix = {mob: root_hierarchical_matrix}
|
||||
dfs = [mob]
|
||||
while dfs:
|
||||
parent = dfs.pop()
|
||||
for child in parent.submobjects:
|
||||
child_hierarchical_matrix = (
|
||||
mobject_to_hierarchical_matrix[parent] @ child.model_matrix
|
||||
)
|
||||
mobject_to_hierarchical_matrix[child] = child_hierarchical_matrix
|
||||
if child.has_points():
|
||||
matrix_to_mobject_list[tuple(child_hierarchical_matrix.ravel())].append(
|
||||
child,
|
||||
)
|
||||
dfs.append(child)
|
||||
return matrix_to_mobject_list
|
||||
|
||||
|
||||
def render_opengl_vectorized_mobject_fill(renderer, mobject):
|
||||
matrix_to_mobject_list = build_matrix_lists(mobject)
|
||||
|
||||
for matrix_tuple, mobject_list in matrix_to_mobject_list.items():
|
||||
model_matrix = np.array(matrix_tuple).reshape((4, 4))
|
||||
render_mobject_fills_with_matrix(renderer, model_matrix, mobject_list)
|
||||
|
||||
|
||||
def render_mobject_fills_with_matrix(renderer, model_matrix, mobjects):
|
||||
# Precompute the total number of vertices for which to reserve space.
|
||||
# Note that triangulate_mobject() will cache its results.
|
||||
total_size = 0
|
||||
for submob in mobjects:
|
||||
total_size += triangulate_mobject(submob).shape[0]
|
||||
|
||||
attributes = np.empty(
|
||||
total_size,
|
||||
dtype=[
|
||||
("in_vert", np.float32, (3,)),
|
||||
("in_color", np.float32, (4,)),
|
||||
("texture_coords", np.float32, (2,)),
|
||||
("texture_mode", np.int32),
|
||||
],
|
||||
)
|
||||
|
||||
write_offset = 0
|
||||
for submob in mobjects:
|
||||
if not submob.has_points():
|
||||
continue
|
||||
mobject_triangulation = triangulate_mobject(submob)
|
||||
end_offset = write_offset + mobject_triangulation.shape[0]
|
||||
attributes[write_offset:end_offset] = mobject_triangulation
|
||||
attributes["in_color"][write_offset:end_offset] = np.repeat(
|
||||
submob.fill_rgba,
|
||||
mobject_triangulation.shape[0],
|
||||
axis=0,
|
||||
)
|
||||
write_offset = end_offset
|
||||
|
||||
fill_shader = Shader(renderer.context, name="vectorized_mobject_fill")
|
||||
fill_shader.set_uniform(
|
||||
"u_model_view_matrix",
|
||||
opengl.matrix_to_shader_input(
|
||||
renderer.camera.unformatted_view_matrix @ model_matrix,
|
||||
),
|
||||
)
|
||||
fill_shader.set_uniform(
|
||||
"u_projection_matrix",
|
||||
renderer.scene.camera.projection_matrix,
|
||||
)
|
||||
|
||||
vbo = renderer.context.buffer(attributes.tobytes())
|
||||
vao = renderer.context.simple_vertex_array(
|
||||
fill_shader.shader_program,
|
||||
vbo,
|
||||
*attributes.dtype.names,
|
||||
)
|
||||
vao.render()
|
||||
vao.release()
|
||||
vbo.release()
|
||||
|
||||
|
||||
def triangulate_mobject(mob):
|
||||
if not mob.needs_new_triangulation:
|
||||
return mob.triangulation
|
||||
|
||||
# Figure out how to triangulate the interior to know
|
||||
# how to send the points as to the vertex shader.
|
||||
# First triangles come directly from the points
|
||||
# normal_vector = mob.get_unit_normal()
|
||||
points = mob.points
|
||||
|
||||
b0s = points[0::3]
|
||||
b1s = points[1::3]
|
||||
b2s = points[2::3]
|
||||
v01s = b1s - b0s
|
||||
v12s = b2s - b1s
|
||||
|
||||
crosses = cross2d(v01s, v12s)
|
||||
convexities = np.sign(crosses)
|
||||
if mob.orientation == 1:
|
||||
concave_parts = convexities > 0
|
||||
convex_parts = convexities <= 0
|
||||
else:
|
||||
concave_parts = convexities < 0
|
||||
convex_parts = convexities >= 0
|
||||
|
||||
# These are the vertices to which we'll apply a polygon triangulation
|
||||
atol = mob.tolerance_for_point_equality
|
||||
end_of_loop = np.zeros(len(b0s), dtype=bool)
|
||||
end_of_loop[:-1] = (np.abs(b2s[:-1] - b0s[1:]) > atol).any(1)
|
||||
end_of_loop[-1] = True
|
||||
|
||||
indices = np.arange(len(points), dtype=int)
|
||||
inner_vert_indices = np.hstack(
|
||||
[
|
||||
indices[0::3],
|
||||
indices[1::3][concave_parts],
|
||||
indices[2::3][end_of_loop],
|
||||
],
|
||||
)
|
||||
inner_vert_indices.sort()
|
||||
rings = np.arange(1, len(inner_vert_indices) + 1)[inner_vert_indices % 3 == 2]
|
||||
|
||||
# Triangulate
|
||||
inner_verts = points[inner_vert_indices]
|
||||
inner_tri_indices = inner_vert_indices[earclip_triangulation(inner_verts, rings)]
|
||||
|
||||
bezier_triangle_indices = np.reshape(indices, (-1, 3))
|
||||
concave_triangle_indices = np.reshape(bezier_triangle_indices[concave_parts], (-1))
|
||||
convex_triangle_indices = np.reshape(bezier_triangle_indices[convex_parts], (-1))
|
||||
|
||||
points = points[
|
||||
np.hstack(
|
||||
[
|
||||
concave_triangle_indices,
|
||||
convex_triangle_indices,
|
||||
inner_tri_indices,
|
||||
],
|
||||
)
|
||||
]
|
||||
texture_coords = np.tile(
|
||||
[
|
||||
[0.0, 0.0],
|
||||
[0.5, 0.0],
|
||||
[1.0, 1.0],
|
||||
],
|
||||
(points.shape[0] // 3, 1),
|
||||
)
|
||||
texture_mode = np.hstack(
|
||||
(
|
||||
np.ones(concave_triangle_indices.shape[0]),
|
||||
-1 * np.ones(convex_triangle_indices.shape[0]),
|
||||
np.zeros(inner_tri_indices.shape[0]),
|
||||
),
|
||||
)
|
||||
|
||||
attributes = np.zeros(
|
||||
points.shape[0],
|
||||
dtype=[
|
||||
("in_vert", np.float32, (3,)),
|
||||
("in_color", np.float32, (4,)),
|
||||
("texture_coords", np.float32, (2,)),
|
||||
("texture_mode", np.int32),
|
||||
],
|
||||
)
|
||||
attributes["in_vert"] = points
|
||||
attributes["texture_coords"] = texture_coords
|
||||
attributes["texture_mode"] = texture_mode
|
||||
|
||||
mob.triangulation = attributes
|
||||
mob.needs_new_triangulation = False
|
||||
|
||||
return attributes
|
||||
|
||||
|
||||
def render_opengl_vectorized_mobject_stroke(renderer, mobject):
|
||||
matrix_to_mobject_list = build_matrix_lists(mobject)
|
||||
for matrix_tuple, mobject_list in matrix_to_mobject_list.items():
|
||||
model_matrix = np.array(matrix_tuple).reshape((4, 4))
|
||||
render_mobject_strokes_with_matrix(renderer, model_matrix, mobject_list)
|
||||
|
||||
|
||||
def render_mobject_strokes_with_matrix(renderer, model_matrix, mobjects):
|
||||
# Precompute the total number of vertices for which to reserve space.
|
||||
total_size = 0
|
||||
for submob in mobjects:
|
||||
total_size += submob.points.shape[0]
|
||||
|
||||
points = np.empty((total_size, 3))
|
||||
colors = np.empty((total_size, 4))
|
||||
widths = np.empty(total_size)
|
||||
|
||||
write_offset = 0
|
||||
for submob in mobjects:
|
||||
if not submob.has_points():
|
||||
continue
|
||||
end_offset = write_offset + submob.points.shape[0]
|
||||
|
||||
points[write_offset:end_offset] = submob.points
|
||||
if submob.stroke_rgba.shape[0] == points[write_offset:end_offset].shape[0]:
|
||||
colors[write_offset:end_offset] = submob.stroke_rgba
|
||||
else:
|
||||
colors[write_offset:end_offset] = np.repeat(
|
||||
submob.stroke_rgba,
|
||||
submob.points.shape[0],
|
||||
axis=0,
|
||||
)
|
||||
widths[write_offset:end_offset] = np.repeat(
|
||||
submob.stroke_width,
|
||||
submob.points.shape[0],
|
||||
)
|
||||
write_offset = end_offset
|
||||
|
||||
stroke_data = np.zeros(
|
||||
len(points),
|
||||
dtype=[
|
||||
# ("previous_curve", np.float32, (3, 3)),
|
||||
("current_curve", np.float32, (3, 3)),
|
||||
# ("next_curve", np.float32, (3, 3)),
|
||||
("tile_coordinate", np.float32, (2,)),
|
||||
("in_color", np.float32, (4,)),
|
||||
("in_width", np.float32),
|
||||
],
|
||||
)
|
||||
|
||||
stroke_data["in_color"] = colors
|
||||
stroke_data["in_width"] = widths
|
||||
curves = np.reshape(points, (-1, 3, 3))
|
||||
# stroke_data["previous_curve"] = np.repeat(np.roll(curves, 1, axis=0), 3, axis=0)
|
||||
stroke_data["current_curve"] = np.repeat(curves, 3, axis=0)
|
||||
# stroke_data["next_curve"] = np.repeat(np.roll(curves, -1, axis=0), 3, axis=0)
|
||||
|
||||
# Repeat each vertex in order to make a tile.
|
||||
stroke_data = np.tile(stroke_data, 2)
|
||||
stroke_data["tile_coordinate"] = np.vstack(
|
||||
(
|
||||
np.tile(
|
||||
[
|
||||
[0.0, 0.0],
|
||||
[0.0, 1.0],
|
||||
[1.0, 1.0],
|
||||
],
|
||||
(len(points) // 3, 1),
|
||||
),
|
||||
np.tile(
|
||||
[
|
||||
[0.0, 0.0],
|
||||
[1.0, 0.0],
|
||||
[1.0, 1.0],
|
||||
],
|
||||
(len(points) // 3, 1),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
shader = Shader(renderer.context, "vectorized_mobject_stroke")
|
||||
shader.set_uniform(
|
||||
"u_model_view_matrix",
|
||||
opengl.matrix_to_shader_input(
|
||||
renderer.camera.unformatted_view_matrix @ model_matrix,
|
||||
),
|
||||
)
|
||||
shader.set_uniform("u_projection_matrix", renderer.scene.camera.projection_matrix)
|
||||
shader.set_uniform("manim_unit_normal", tuple(-mobjects[0].unit_normal[0]))
|
||||
|
||||
vbo = renderer.context.buffer(stroke_data.tobytes())
|
||||
vao = renderer.context.simple_vertex_array(
|
||||
shader.shader_program, vbo, *stroke_data.dtype.names
|
||||
)
|
||||
renderer.frame_buffer_object.use()
|
||||
vao.render()
|
||||
vao.release()
|
||||
vbo.release()
|
||||
551
manim/scene/three_d_scene.py
Normal file
551
manim/scene/three_d_scene.py
Normal file
|
|
@ -0,0 +1,551 @@
|
|||
"""A scene suitable for rendering three-dimensional objects and animations."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
__all__ = ["ThreeDScene", "SpecialThreeDScene"]
|
||||
|
||||
|
||||
import warnings
|
||||
from collections.abc import Iterable, Sequence
|
||||
|
||||
import numpy as np
|
||||
|
||||
from manim.mobject.geometry.line import Line
|
||||
from manim.mobject.graphing.coordinate_systems import ThreeDAxes
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject
|
||||
from manim.mobject.three_d.three_dimensions import Sphere
|
||||
from manim.mobject.value_tracker import ValueTracker
|
||||
|
||||
from .. import config
|
||||
from ..animation.animation import Animation
|
||||
from ..animation.transform import Transform
|
||||
from ..camera.three_d_camera import ThreeDCamera
|
||||
from ..constants import DEGREES, RendererType
|
||||
from ..mobject.mobject import Mobject
|
||||
from ..mobject.types.vectorized_mobject import VectorizedPoint, VGroup
|
||||
from ..renderer.opengl_renderer import OpenGLCamera
|
||||
from ..scene.scene import Scene
|
||||
from ..utils.config_ops import merge_dicts_recursively
|
||||
|
||||
|
||||
class ThreeDScene(Scene):
|
||||
"""
|
||||
This is a Scene, with special configurations and properties that
|
||||
make it suitable for Three Dimensional Scenes.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
camera_class=ThreeDCamera,
|
||||
ambient_camera_rotation=None,
|
||||
default_angled_camera_orientation_kwargs=None,
|
||||
**kwargs,
|
||||
):
|
||||
self.ambient_camera_rotation = ambient_camera_rotation
|
||||
if default_angled_camera_orientation_kwargs is None:
|
||||
default_angled_camera_orientation_kwargs = {
|
||||
"phi": 70 * DEGREES,
|
||||
"theta": -135 * DEGREES,
|
||||
}
|
||||
self.default_angled_camera_orientation_kwargs = (
|
||||
default_angled_camera_orientation_kwargs
|
||||
)
|
||||
super().__init__(camera_class=camera_class, **kwargs)
|
||||
|
||||
def set_camera_orientation(
|
||||
self,
|
||||
phi: float | None = None,
|
||||
theta: float | None = None,
|
||||
gamma: float | None = None,
|
||||
zoom: float | None = None,
|
||||
focal_distance: float | None = None,
|
||||
frame_center: Mobject | Sequence[float] | None = None,
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
This method sets the orientation of the camera in the scene.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
phi
|
||||
The polar angle i.e the angle between Z_AXIS and Camera through ORIGIN in radians.
|
||||
|
||||
theta
|
||||
The azimuthal angle i.e the angle that spins the camera around the Z_AXIS.
|
||||
|
||||
focal_distance
|
||||
The focal_distance of the Camera.
|
||||
|
||||
gamma
|
||||
The rotation of the camera about the vector from the ORIGIN to the Camera.
|
||||
|
||||
zoom
|
||||
The zoom factor of the scene.
|
||||
|
||||
frame_center
|
||||
The new center of the camera frame in cartesian coordinates.
|
||||
|
||||
"""
|
||||
|
||||
if phi is not None:
|
||||
self.renderer.camera.set_phi(phi)
|
||||
if theta is not None:
|
||||
self.renderer.camera.set_theta(theta)
|
||||
if focal_distance is not None:
|
||||
self.renderer.camera.set_focal_distance(focal_distance)
|
||||
if gamma is not None:
|
||||
self.renderer.camera.set_gamma(gamma)
|
||||
if zoom is not None:
|
||||
self.renderer.camera.set_zoom(zoom)
|
||||
if frame_center is not None:
|
||||
self.renderer.camera._frame_center.move_to(frame_center)
|
||||
|
||||
def begin_ambient_camera_rotation(self, rate: float = 0.02, about: str = "theta"):
|
||||
"""
|
||||
This method begins an ambient rotation of the camera about the Z_AXIS,
|
||||
in the anticlockwise direction
|
||||
|
||||
Parameters
|
||||
----------
|
||||
rate
|
||||
The rate at which the camera should rotate about the Z_AXIS.
|
||||
Negative rate means clockwise rotation.
|
||||
about
|
||||
one of 3 options: ["theta", "phi", "gamma"]. defaults to theta.
|
||||
"""
|
||||
# TODO, use a ValueTracker for rate, so that it
|
||||
# can begin and end smoothly
|
||||
about: str = about.lower()
|
||||
try:
|
||||
if config.renderer == RendererType.CAIRO:
|
||||
trackers = {
|
||||
"theta": self.camera.theta_tracker,
|
||||
"phi": self.camera.phi_tracker,
|
||||
"gamma": self.camera.gamma_tracker,
|
||||
}
|
||||
x: ValueTracker = trackers[about]
|
||||
x.add_updater(lambda m, dt: x.increment_value(rate * dt))
|
||||
self.add(x)
|
||||
elif config.renderer == RendererType.OPENGL:
|
||||
cam: OpenGLCamera = self.camera
|
||||
methods = {
|
||||
"theta": cam.increment_theta,
|
||||
"phi": cam.increment_phi,
|
||||
"gamma": cam.increment_gamma,
|
||||
}
|
||||
cam.add_updater(lambda m, dt: methods[about](rate * dt))
|
||||
self.add(self.camera)
|
||||
except Exception:
|
||||
raise ValueError("Invalid ambient rotation angle.")
|
||||
|
||||
def stop_ambient_camera_rotation(self, about="theta"):
|
||||
"""
|
||||
This method stops all ambient camera rotation.
|
||||
"""
|
||||
about: str = about.lower()
|
||||
try:
|
||||
if config.renderer == RendererType.CAIRO:
|
||||
trackers = {
|
||||
"theta": self.camera.theta_tracker,
|
||||
"phi": self.camera.phi_tracker,
|
||||
"gamma": self.camera.gamma_tracker,
|
||||
}
|
||||
x: ValueTracker = trackers[about]
|
||||
x.clear_updaters()
|
||||
self.remove(x)
|
||||
elif config.renderer == RendererType.OPENGL:
|
||||
self.camera.clear_updaters()
|
||||
except Exception:
|
||||
raise ValueError("Invalid ambient rotation angle.")
|
||||
|
||||
def begin_3dillusion_camera_rotation(
|
||||
self,
|
||||
rate: float = 1,
|
||||
origin_phi: float | None = None,
|
||||
origin_theta: float | None = None,
|
||||
):
|
||||
"""
|
||||
This method creates a 3D camera rotation illusion around
|
||||
the current camera orientation.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
rate
|
||||
The rate at which the camera rotation illusion should operate.
|
||||
origin_phi
|
||||
The polar angle the camera should move around. Defaults
|
||||
to the current phi angle.
|
||||
origin_theta
|
||||
The azimutal angle the camera should move around. Defaults
|
||||
to the current theta angle.
|
||||
"""
|
||||
if origin_theta is None:
|
||||
origin_theta = self.renderer.camera.theta_tracker.get_value()
|
||||
if origin_phi is None:
|
||||
origin_phi = self.renderer.camera.phi_tracker.get_value()
|
||||
|
||||
val_tracker_theta = ValueTracker(0)
|
||||
|
||||
def update_theta(m, dt):
|
||||
val_tracker_theta.increment_value(dt * rate)
|
||||
val_for_left_right = 0.2 * np.sin(val_tracker_theta.get_value())
|
||||
return m.set_value(origin_theta + val_for_left_right)
|
||||
|
||||
self.renderer.camera.theta_tracker.add_updater(update_theta)
|
||||
self.add(self.renderer.camera.theta_tracker)
|
||||
|
||||
val_tracker_phi = ValueTracker(0)
|
||||
|
||||
def update_phi(m, dt):
|
||||
val_tracker_phi.increment_value(dt * rate)
|
||||
val_for_up_down = 0.1 * np.cos(val_tracker_phi.get_value()) - 0.1
|
||||
return m.set_value(origin_phi + val_for_up_down)
|
||||
|
||||
self.renderer.camera.phi_tracker.add_updater(update_phi)
|
||||
self.add(self.renderer.camera.phi_tracker)
|
||||
|
||||
def stop_3dillusion_camera_rotation(self):
|
||||
"""
|
||||
This method stops all illusion camera rotations.
|
||||
"""
|
||||
self.renderer.camera.theta_tracker.clear_updaters()
|
||||
self.remove(self.renderer.camera.theta_tracker)
|
||||
self.renderer.camera.phi_tracker.clear_updaters()
|
||||
self.remove(self.renderer.camera.phi_tracker)
|
||||
|
||||
def move_camera(
|
||||
self,
|
||||
phi: float | None = None,
|
||||
theta: float | None = None,
|
||||
gamma: float | None = None,
|
||||
zoom: float | None = None,
|
||||
focal_distance: float | None = None,
|
||||
frame_center: Mobject | Sequence[float] | None = None,
|
||||
added_anims: Iterable[Animation] = [],
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
This method animates the movement of the camera
|
||||
to the given spherical coordinates.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
phi
|
||||
The polar angle i.e the angle between Z_AXIS and Camera through ORIGIN in radians.
|
||||
|
||||
theta
|
||||
The azimuthal angle i.e the angle that spins the camera around the Z_AXIS.
|
||||
|
||||
focal_distance
|
||||
The radial focal_distance between ORIGIN and Camera.
|
||||
|
||||
gamma
|
||||
The rotation of the camera about the vector from the ORIGIN to the Camera.
|
||||
|
||||
zoom
|
||||
The zoom factor of the camera.
|
||||
|
||||
frame_center
|
||||
The new center of the camera frame in cartesian coordinates.
|
||||
|
||||
added_anims
|
||||
Any other animations to be played at the same time.
|
||||
|
||||
"""
|
||||
anims = []
|
||||
|
||||
if config.renderer == RendererType.CAIRO:
|
||||
self.camera: ThreeDCamera
|
||||
value_tracker_pairs = [
|
||||
(phi, self.camera.phi_tracker),
|
||||
(theta, self.camera.theta_tracker),
|
||||
(focal_distance, self.camera.focal_distance_tracker),
|
||||
(gamma, self.camera.gamma_tracker),
|
||||
(zoom, self.camera.zoom_tracker),
|
||||
]
|
||||
for value, tracker in value_tracker_pairs:
|
||||
if value is not None:
|
||||
anims.append(tracker.animate.set_value(value))
|
||||
if frame_center is not None:
|
||||
anims.append(self.camera._frame_center.animate.move_to(frame_center))
|
||||
elif config.renderer == RendererType.OPENGL:
|
||||
cam: OpenGLCamera = self.camera
|
||||
cam2 = cam.copy()
|
||||
methods = {
|
||||
"theta": cam2.set_theta,
|
||||
"phi": cam2.set_phi,
|
||||
"gamma": cam2.set_gamma,
|
||||
"zoom": cam2.scale,
|
||||
"frame_center": cam2.move_to,
|
||||
}
|
||||
if frame_center is not None:
|
||||
if isinstance(frame_center, OpenGLMobject):
|
||||
frame_center = frame_center.get_center()
|
||||
frame_center = list(frame_center)
|
||||
|
||||
zoom_value = None
|
||||
if zoom is not None:
|
||||
zoom_value = config.frame_height / (zoom * cam.height)
|
||||
|
||||
for value, method in [
|
||||
[theta, "theta"],
|
||||
[phi, "phi"],
|
||||
[gamma, "gamma"],
|
||||
[zoom_value, "zoom"],
|
||||
[frame_center, "frame_center"],
|
||||
]:
|
||||
if value is not None:
|
||||
methods[method](value)
|
||||
|
||||
if focal_distance is not None:
|
||||
warnings.warn(
|
||||
"focal distance of OpenGLCamera can not be adjusted.",
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
anims += [Transform(cam, cam2)]
|
||||
|
||||
self.play(*anims + added_anims, **kwargs)
|
||||
|
||||
# These lines are added to improve performance. If manim thinks that frame_center is moving,
|
||||
# it is required to redraw every object. These lines remove frame_center from the Scene once
|
||||
# its animation is done, ensuring that manim does not think that it is moving. Since the
|
||||
# frame_center is never actually drawn, this shouldn't break anything.
|
||||
if frame_center is not None and config.renderer == RendererType.CAIRO:
|
||||
self.remove(self.camera._frame_center)
|
||||
|
||||
def get_moving_mobjects(self, *animations: Animation):
|
||||
"""
|
||||
This method returns a list of all of the Mobjects in the Scene that
|
||||
are moving, that are also in the animations passed.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
*animations
|
||||
The animations whose mobjects will be checked.
|
||||
"""
|
||||
moving_mobjects = super().get_moving_mobjects(*animations)
|
||||
camera_mobjects = self.renderer.camera.get_value_trackers() + [
|
||||
self.renderer.camera._frame_center,
|
||||
]
|
||||
if any(cm in moving_mobjects for cm in camera_mobjects):
|
||||
return self.mobjects
|
||||
return moving_mobjects
|
||||
|
||||
def add_fixed_orientation_mobjects(self, *mobjects: Mobject, **kwargs):
|
||||
"""
|
||||
This method is used to prevent the rotation and tilting
|
||||
of mobjects as the camera moves around. The mobject can
|
||||
still move in the x,y,z directions, but will always be
|
||||
at the angle (relative to the camera) that it was at
|
||||
when it was passed through this method.)
|
||||
|
||||
Parameters
|
||||
----------
|
||||
*mobjects
|
||||
The Mobject(s) whose orientation must be fixed.
|
||||
|
||||
**kwargs
|
||||
Some valid kwargs are
|
||||
use_static_center_func : bool
|
||||
center_func : function
|
||||
"""
|
||||
if config.renderer == RendererType.CAIRO:
|
||||
self.add(*mobjects)
|
||||
self.renderer.camera.add_fixed_orientation_mobjects(*mobjects, **kwargs)
|
||||
elif config.renderer == RendererType.OPENGL:
|
||||
for mob in mobjects:
|
||||
mob: OpenGLMobject
|
||||
mob.fix_orientation()
|
||||
self.add(mob)
|
||||
|
||||
def add_fixed_in_frame_mobjects(self, *mobjects: Mobject):
|
||||
"""
|
||||
This method is used to prevent the rotation and movement
|
||||
of mobjects as the camera moves around. The mobject is
|
||||
essentially overlaid, and is not impacted by the camera's
|
||||
movement in any way.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
*mobjects
|
||||
The Mobjects whose orientation must be fixed.
|
||||
"""
|
||||
if config.renderer == RendererType.CAIRO:
|
||||
self.add(*mobjects)
|
||||
self.camera: ThreeDCamera
|
||||
self.camera.add_fixed_in_frame_mobjects(*mobjects)
|
||||
elif config.renderer == RendererType.OPENGL:
|
||||
for mob in mobjects:
|
||||
mob: OpenGLMobject
|
||||
mob.fix_in_frame()
|
||||
self.add(mob)
|
||||
|
||||
def remove_fixed_orientation_mobjects(self, *mobjects: Mobject):
|
||||
"""
|
||||
This method "unfixes" the orientation of the mobjects
|
||||
passed, meaning they will no longer be at the same angle
|
||||
relative to the camera. This only makes sense if the
|
||||
mobject was passed through add_fixed_orientation_mobjects first.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
*mobjects
|
||||
The Mobjects whose orientation must be unfixed.
|
||||
"""
|
||||
if config.renderer == RendererType.CAIRO:
|
||||
self.renderer.camera.remove_fixed_orientation_mobjects(*mobjects)
|
||||
elif config.renderer == RendererType.OPENGL:
|
||||
for mob in mobjects:
|
||||
mob: OpenGLMobject
|
||||
mob.unfix_orientation()
|
||||
self.remove(mob)
|
||||
|
||||
def remove_fixed_in_frame_mobjects(self, *mobjects: Mobject):
|
||||
"""
|
||||
This method undoes what add_fixed_in_frame_mobjects does.
|
||||
It allows the mobject to be affected by the movement of
|
||||
the camera.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
*mobjects
|
||||
The Mobjects whose position and orientation must be unfixed.
|
||||
"""
|
||||
if config.renderer == RendererType.CAIRO:
|
||||
self.renderer.camera.remove_fixed_in_frame_mobjects(*mobjects)
|
||||
elif config.renderer == RendererType.OPENGL:
|
||||
for mob in mobjects:
|
||||
mob: OpenGLMobject
|
||||
mob.unfix_from_frame()
|
||||
self.remove(mob)
|
||||
|
||||
##
|
||||
def set_to_default_angled_camera_orientation(self, **kwargs):
|
||||
"""
|
||||
This method sets the default_angled_camera_orientation to the
|
||||
keyword arguments passed, and sets the camera to that orientation.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
**kwargs
|
||||
Some recognised kwargs are phi, theta, focal_distance, gamma,
|
||||
which have the same meaning as the parameters in set_camera_orientation.
|
||||
"""
|
||||
config = dict(
|
||||
self.default_camera_orientation_kwargs,
|
||||
) # Where doe this come from?
|
||||
config.update(kwargs)
|
||||
self.set_camera_orientation(**config)
|
||||
|
||||
|
||||
class SpecialThreeDScene(ThreeDScene):
|
||||
"""An extension of :class:`ThreeDScene` with more settings.
|
||||
|
||||
It has some extra configuration for axes, spheres,
|
||||
and an override for low quality rendering. Further key differences
|
||||
are:
|
||||
|
||||
* The camera shades applicable 3DMobjects by default,
|
||||
except if rendering in low quality.
|
||||
* Some default params for Spheres and Axes have been added.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
cut_axes_at_radius=True,
|
||||
camera_config={"should_apply_shading": True, "exponential_projection": True},
|
||||
three_d_axes_config={
|
||||
"num_axis_pieces": 1,
|
||||
"axis_config": {
|
||||
"unit_size": 2,
|
||||
"tick_frequency": 1,
|
||||
"numbers_with_elongated_ticks": [0, 1, 2],
|
||||
"stroke_width": 2,
|
||||
},
|
||||
},
|
||||
sphere_config={"radius": 2, "resolution": (24, 48)},
|
||||
default_angled_camera_position={
|
||||
"phi": 70 * DEGREES,
|
||||
"theta": -110 * DEGREES,
|
||||
},
|
||||
# When scene is extracted with -l flag, this
|
||||
# configuration will override the above configuration.
|
||||
low_quality_config={
|
||||
"camera_config": {"should_apply_shading": False},
|
||||
"three_d_axes_config": {"num_axis_pieces": 1},
|
||||
"sphere_config": {"resolution": (12, 24)},
|
||||
},
|
||||
**kwargs,
|
||||
):
|
||||
self.cut_axes_at_radius = cut_axes_at_radius
|
||||
self.camera_config = camera_config
|
||||
self.three_d_axes_config = three_d_axes_config
|
||||
self.sphere_config = sphere_config
|
||||
self.default_angled_camera_position = default_angled_camera_position
|
||||
self.low_quality_config = low_quality_config
|
||||
if self.renderer.camera_config["pixel_width"] == config["pixel_width"]:
|
||||
_config = {}
|
||||
else:
|
||||
_config = self.low_quality_config
|
||||
_config = merge_dicts_recursively(_config, kwargs)
|
||||
super().__init__(**_config)
|
||||
|
||||
def get_axes(self):
|
||||
"""Return a set of 3D axes.
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`.ThreeDAxes`
|
||||
A set of 3D axes.
|
||||
"""
|
||||
axes = ThreeDAxes(**self.three_d_axes_config)
|
||||
for axis in axes:
|
||||
if self.cut_axes_at_radius:
|
||||
p0 = axis.get_start()
|
||||
p1 = axis.number_to_point(-1)
|
||||
p2 = axis.number_to_point(1)
|
||||
p3 = axis.get_end()
|
||||
new_pieces = VGroup(Line(p0, p1), Line(p1, p2), Line(p2, p3))
|
||||
for piece in new_pieces:
|
||||
piece.shade_in_3d = True
|
||||
new_pieces.match_style(axis.pieces)
|
||||
axis.pieces.submobjects = new_pieces.submobjects
|
||||
for tick in axis.tick_marks:
|
||||
tick.add(VectorizedPoint(1.5 * tick.get_center()))
|
||||
return axes
|
||||
|
||||
def get_sphere(self, **kwargs):
|
||||
"""
|
||||
Returns a sphere with the passed keyword arguments as properties.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
**kwargs
|
||||
Any valid parameter of :class:`~.Sphere` or :class:`~.Surface`.
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`~.Sphere`
|
||||
The sphere object.
|
||||
"""
|
||||
config = merge_dicts_recursively(self.sphere_config, kwargs)
|
||||
return Sphere(**config)
|
||||
|
||||
def get_default_camera_position(self):
|
||||
"""
|
||||
Returns the default_angled_camera position.
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict
|
||||
Dictionary of phi, theta, focal_distance, and gamma.
|
||||
"""
|
||||
return self.default_angled_camera_position
|
||||
|
||||
def set_camera_to_default_position(self):
|
||||
"""
|
||||
Sets the camera to its default position.
|
||||
"""
|
||||
self.set_camera_orientation(**self.default_angled_camera_position)
|
||||
|
|
@ -1148,6 +1148,7 @@ def get_smooth_quadratic_bezier_handle_points(points: Point3D_Array) -> Point3D_
|
|||
return handles
|
||||
|
||||
|
||||
# Figuring out which Bézier curves most smoothly connect a sequence of points
|
||||
def get_smooth_cubic_bezier_handle_points(
|
||||
anchors: Point3D_Array,
|
||||
) -> tuple[Point3D_Array, Point3D_Array]:
|
||||
|
|
|
|||
|
|
@ -6,13 +6,14 @@ __all__ = ["deprecated", "deprecated_params"]
|
|||
|
||||
|
||||
import inspect
|
||||
import logging
|
||||
import re
|
||||
from collections.abc import Iterable
|
||||
from typing import Any, Callable
|
||||
|
||||
from decorator import decorate, decorator
|
||||
|
||||
from .. import logger
|
||||
logger = logging.getLogger("manim")
|
||||
|
||||
|
||||
def _get_callable_info(callable: Callable) -> tuple[str, str]:
|
||||
|
|
|
|||
|
|
@ -82,7 +82,6 @@ from __future__ import annotations
|
|||
|
||||
import csv
|
||||
import itertools as it
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
|
|
|
|||
|
|
@ -17,16 +17,11 @@ import numpy as np
|
|||
from .. import config, logger
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from typing_extensions import TypeVar
|
||||
|
||||
from manim.animation.protocol import AnimationProtocol
|
||||
from manim.camera.camera import Camera
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject
|
||||
from manim.scene.scene import Scene
|
||||
|
||||
T = TypeVar("T")
|
||||
S = TypeVar("S", default=str)
|
||||
|
||||
__all__ = ["KEYS_TO_FILTER_OUT", "get_hash_from_play_call", "get_json"]
|
||||
|
||||
# Sometimes there are elements that are not suitable for hashing (too long or
|
||||
|
|
@ -37,8 +32,6 @@ KEYS_TO_FILTER_OUT = {
|
|||
"pixel_array",
|
||||
"pixel_array_to_cairo_context",
|
||||
}
|
||||
|
||||
|
||||
class _Memoizer:
|
||||
"""Implements the memoization logic to optimize the hashing procedure and prevent
|
||||
the circular references within iterable processed.
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ from manim.renderer.shader import shader_program_cache
|
|||
|
||||
__all__ = ["ManimMagic"]
|
||||
|
||||
__all__ = ["ManimMagic"]
|
||||
|
||||
try:
|
||||
from IPython import get_ipython
|
||||
from IPython.core.interactiveshell import InteractiveShell
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ if TYPE_CHECKING:
|
|||
|
||||
from manim.scene.scene import Scene
|
||||
|
||||
__all__ = ["scene_classes_from_file"]
|
||||
|
||||
|
||||
__all__ = ["scene_classes_from_file"]
|
||||
|
||||
|
|
|
|||
|
|
@ -114,17 +114,6 @@ def clip(a, min_a, max_a):
|
|||
return a
|
||||
|
||||
|
||||
def fdiv(a: float, b: float, zero_over_zero_value: float | None = None) -> float:
|
||||
if zero_over_zero_value is not None:
|
||||
out = np.full_like(a, zero_over_zero_value)
|
||||
where = np.logical_or(a != 0, b != 0)
|
||||
else:
|
||||
out = None
|
||||
where = True
|
||||
|
||||
return np.true_divide(a, b, out=out, where=where)
|
||||
|
||||
|
||||
def sigmoid(x: float) -> float:
|
||||
r"""Returns the output of the logistic function.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import logging
|
||||
import warnings
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
|
||||
from manim import logger
|
||||
from manim.typing import PixelArray
|
||||
|
||||
from ._show_diff import show_diff_helper
|
||||
|
|
@ -16,6 +16,8 @@ __all__ = ["_FramesTester", "_ControlDataWriter"]
|
|||
FRAME_ABSOLUTE_TOLERANCE = 1.01
|
||||
FRAME_MISMATCH_RATIO_TOLERANCE = 1e-5
|
||||
|
||||
logger = logging.getLogger("manim")
|
||||
|
||||
|
||||
class _FramesTester:
|
||||
def __init__(self, file_path: Path, show_diff=False) -> None:
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ __all__ = ["frames_comparison"]
|
|||
SCENE_PARAMETER_NAME = "scene"
|
||||
_tests_root_dir_path = Path(__file__).absolute().parents[2]
|
||||
PATH_CONTROL_DATA = _tests_root_dir_path / Path("control_data", "graphical_units_data")
|
||||
MIN_CAIRO_VERSION = 11800
|
||||
|
||||
|
||||
def frames_comparison(
|
||||
|
|
|
|||
58
poetry.lock
generated
58
poetry.lock
generated
|
|
@ -1,4 +1,4 @@
|
|||
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "alabaster"
|
||||
|
|
@ -978,13 +978,13 @@ files = [
|
|||
|
||||
[[package]]
|
||||
name = "exceptiongroup"
|
||||
version = "1.2.1"
|
||||
version = "1.2.2"
|
||||
description = "Backport of PEP 654 (exception groups)"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"},
|
||||
{file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"},
|
||||
{file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"},
|
||||
{file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
|
|
@ -1825,13 +1825,13 @@ jupyter-server = ">=1.1.2"
|
|||
|
||||
[[package]]
|
||||
name = "jupyter-server"
|
||||
version = "2.14.1"
|
||||
version = "2.14.2"
|
||||
description = "The backend—i.e. core services, APIs, and REST endpoints—to Jupyter web applications."
|
||||
optional = true
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "jupyter_server-2.14.1-py3-none-any.whl", hash = "sha256:16f7177c3a4ea8fe37784e2d31271981a812f0b2874af17339031dc3510cc2a5"},
|
||||
{file = "jupyter_server-2.14.1.tar.gz", hash = "sha256:12558d158ec7a0653bf96cc272bc7ad79e0127d503b982ed144399346694f726"},
|
||||
{file = "jupyter_server-2.14.2-py3-none-any.whl", hash = "sha256:47ff506127c2f7851a17bf4713434208fc490955d0e8632e95014a9a9afbeefd"},
|
||||
{file = "jupyter_server-2.14.2.tar.gz", hash = "sha256:66095021aa9638ced276c248b1d81862e4c50f292d575920bbe960de1c56b12b"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
|
@ -3281,13 +3281,13 @@ urllib3 = ">=1.26.0"
|
|||
|
||||
[[package]]
|
||||
name = "pyglet"
|
||||
version = "2.0.15"
|
||||
version = "2.0.16"
|
||||
description = "pyglet is a cross-platform games and multimedia package."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pyglet-2.0.15-py3-none-any.whl", hash = "sha256:9e4cc16efc308106fd3a9ff8f04e7a6f4f6a807c6ac8a331375efbbac8be85af"},
|
||||
{file = "pyglet-2.0.15.tar.gz", hash = "sha256:42085567cece0c7f1c14e36eef799938cbf528cfbb0150c484b984f3ff1aa771"},
|
||||
{file = "pyglet-2.0.16-py3-none-any.whl", hash = "sha256:332593c8c14fa2c545a0da3da2f99b8c75c6e822a90cd4ea8e239fad658ff5a1"},
|
||||
{file = "pyglet-2.0.16.tar.gz", hash = "sha256:af007b22ff5f302edeb2a06d749cfef53f52e67f1e52f0b2babd840d37193482"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -3928,29 +3928,29 @@ files = [
|
|||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.5.1"
|
||||
version = "0.5.2"
|
||||
description = "An extremely fast Python linter and code formatter, written in Rust."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "ruff-0.5.1-py3-none-linux_armv6l.whl", hash = "sha256:6ecf968fcf94d942d42b700af18ede94b07521bd188aaf2cd7bc898dd8cb63b6"},
|
||||
{file = "ruff-0.5.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:204fb0a472f00f2e6280a7c8c7c066e11e20e23a37557d63045bf27a616ba61c"},
|
||||
{file = "ruff-0.5.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d235968460e8758d1e1297e1de59a38d94102f60cafb4d5382033c324404ee9d"},
|
||||
{file = "ruff-0.5.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38beace10b8d5f9b6bdc91619310af6d63dd2019f3fb2d17a2da26360d7962fa"},
|
||||
{file = "ruff-0.5.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e478d2f09cf06add143cf8c4540ef77b6599191e0c50ed976582f06e588c994"},
|
||||
{file = "ruff-0.5.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f0368d765eec8247b8550251c49ebb20554cc4e812f383ff9f5bf0d5d94190b0"},
|
||||
{file = "ruff-0.5.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:3a9a9a1b582e37669b0138b7c1d9d60b9edac880b80eb2baba6d0e566bdeca4d"},
|
||||
{file = "ruff-0.5.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bdd9f723e16003623423affabcc0a807a66552ee6a29f90eddad87a40c750b78"},
|
||||
{file = "ruff-0.5.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:be9fd62c1e99539da05fcdc1e90d20f74aec1b7a1613463ed77870057cd6bd96"},
|
||||
{file = "ruff-0.5.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e216fc75a80ea1fbd96af94a6233d90190d5b65cc3d5dfacf2bd48c3e067d3e1"},
|
||||
{file = "ruff-0.5.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c4c2112e9883a40967827d5c24803525145e7dab315497fae149764979ac7929"},
|
||||
{file = "ruff-0.5.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dfaf11c8a116394da3b65cd4b36de30d8552fa45b8119b9ef5ca6638ab964fa3"},
|
||||
{file = "ruff-0.5.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d7ceb9b2fe700ee09a0c6b192c5ef03c56eb82a0514218d8ff700f6ade004108"},
|
||||
{file = "ruff-0.5.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:bac6288e82f6296f82ed5285f597713acb2a6ae26618ffc6b429c597b392535c"},
|
||||
{file = "ruff-0.5.1-py3-none-win32.whl", hash = "sha256:5c441d9c24ec09e1cb190a04535c5379b36b73c4bc20aa180c54812c27d1cca4"},
|
||||
{file = "ruff-0.5.1-py3-none-win_amd64.whl", hash = "sha256:b1789bf2cd3d1b5a7d38397cac1398ddf3ad7f73f4de01b1e913e2abc7dfc51d"},
|
||||
{file = "ruff-0.5.1-py3-none-win_arm64.whl", hash = "sha256:2875b7596a740cbbd492f32d24be73e545a4ce0a3daf51e4f4e609962bfd3cd2"},
|
||||
{file = "ruff-0.5.1.tar.gz", hash = "sha256:3164488aebd89b1745b47fd00604fb4358d774465f20d1fcd907f9c0fc1b0655"},
|
||||
{file = "ruff-0.5.2-py3-none-linux_armv6l.whl", hash = "sha256:7bab8345df60f9368d5f4594bfb8b71157496b44c30ff035d1d01972e764d3be"},
|
||||
{file = "ruff-0.5.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:1aa7acad382ada0189dbe76095cf0a36cd0036779607c397ffdea16517f535b1"},
|
||||
{file = "ruff-0.5.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:aec618d5a0cdba5592c60c2dee7d9c865180627f1a4a691257dea14ac1aa264d"},
|
||||
{file = "ruff-0.5.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0b62adc5ce81780ff04077e88bac0986363e4a3260ad3ef11ae9c14aa0e67ef"},
|
||||
{file = "ruff-0.5.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dc42ebf56ede83cb080a50eba35a06e636775649a1ffd03dc986533f878702a3"},
|
||||
{file = "ruff-0.5.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c15c6e9f88c67ffa442681365d11df38afb11059fc44238e71a9d9f1fd51de70"},
|
||||
{file = "ruff-0.5.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d3de9a5960f72c335ef00763d861fc5005ef0644cb260ba1b5a115a102157251"},
|
||||
{file = "ruff-0.5.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fe5a968ae933e8f7627a7b2fc8893336ac2be0eb0aace762d3421f6e8f7b7f83"},
|
||||
{file = "ruff-0.5.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a04f54a9018f75615ae52f36ea1c5515e356e5d5e214b22609ddb546baef7132"},
|
||||
{file = "ruff-0.5.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ed02fb52e3741f0738db5f93e10ae0fb5c71eb33a4f2ba87c9a2fa97462a649"},
|
||||
{file = "ruff-0.5.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3cf8fe659f6362530435d97d738eb413e9f090e7e993f88711b0377fbdc99f60"},
|
||||
{file = "ruff-0.5.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:237a37e673e9f3cbfff0d2243e797c4862a44c93d2f52a52021c1a1b0899f846"},
|
||||
{file = "ruff-0.5.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2a2949ce7c1cbd8317432ada80fe32156df825b2fd611688814c8557824ef060"},
|
||||
{file = "ruff-0.5.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:481af57c8e99da92ad168924fd82220266043c8255942a1cb87958b108ac9335"},
|
||||
{file = "ruff-0.5.2-py3-none-win32.whl", hash = "sha256:f1aea290c56d913e363066d83d3fc26848814a1fed3d72144ff9c930e8c7c718"},
|
||||
{file = "ruff-0.5.2-py3-none-win_amd64.whl", hash = "sha256:8532660b72b5d94d2a0a7a27ae7b9b40053662d00357bb2a6864dd7e38819084"},
|
||||
{file = "ruff-0.5.2-py3-none-win_arm64.whl", hash = "sha256:73439805c5cb68f364d826a5c5c4b6c798ded6b7ebaa4011f01ce6c94e4d5583"},
|
||||
{file = "ruff-0.5.2.tar.gz", hash = "sha256:2c0df2d2de685433794a14d8d2e240df619b748fbe3367346baa519d8e6f1ca2"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
|
@ -45,6 +46,15 @@ def pytest_collection_modifyitems(config, items):
|
|||
item.add_marker(slow_skip)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def manim_caplog(caplog):
|
||||
logger = logging.getLogger("manim")
|
||||
logger.propagate = True
|
||||
caplog.set_level(logging.INFO, logger="manim")
|
||||
yield caplog
|
||||
logger.propagate = False
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def config():
|
||||
saved = manim.config.copy()
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
|
|
@ -10,6 +11,7 @@ import numpy as np
|
|||
from manim import Manager, logger
|
||||
from manim.scene.scene import Scene
|
||||
|
||||
logger = logging.getLogger("manim")
|
||||
|
||||
def set_test_scene(scene_object: type[Scene], module_name: str, config):
|
||||
"""Function used to set up the test data for a new feature. This will basically set up a pre-rendered frame for a scene. This is meant to be used only
|
||||
|
|
|
|||
|
|
@ -16,22 +16,22 @@ def test_animation_forbidden_run_time(run_time):
|
|||
test_scene.play(FadeIn(None, run_time=run_time))
|
||||
|
||||
|
||||
def test_animation_run_time_shorter_than_frame_rate(caplog, config):
|
||||
def test_animation_run_time_shorter_than_frame_rate(manim_caplog, config):
|
||||
manager = Manager(Scene)
|
||||
test_scene = manager.scene
|
||||
test_scene.play(FadeIn(None, run_time=1 / (config.frame_rate + 1)))
|
||||
assert (
|
||||
"Original run time of FadeIn(Mobject) is shorter than current frame rate"
|
||||
in caplog.text
|
||||
in manim_caplog.text
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("frozen_frame", [False, True])
|
||||
def test_wait_run_time_shorter_than_frame_rate(caplog, frozen_frame):
|
||||
def test_wait_run_time_shorter_than_frame_rate(manim_caplog, frozen_frame):
|
||||
manager = Manager(Scene)
|
||||
test_scene = manager.scene
|
||||
test_scene.wait(1e-9, frozen_frame=frozen_frame)
|
||||
assert (
|
||||
"Original run time of Wait(Mobject) is shorter than current frame rate"
|
||||
in caplog.text
|
||||
in manim_caplog.text
|
||||
)
|
||||
|
|
|
|||
|
|
@ -15,8 +15,7 @@ def test_get_arc_center():
|
|||
)
|
||||
|
||||
|
||||
def test_BackgroundRectangle(caplog):
|
||||
caplog.set_level(logging.INFO)
|
||||
def test_BackgroundRectangle(manim_caplog):
|
||||
c = Circle()
|
||||
bg = BackgroundRectangle(c)
|
||||
bg.set_style(fill_opacity=0.42)
|
||||
|
|
@ -24,5 +23,5 @@ def test_BackgroundRectangle(caplog):
|
|||
bg.set_style(fill_opacity=1, hello="world")
|
||||
assert (
|
||||
"Argument {'hello': 'world'} is ignored in BackgroundRectangle.set_style."
|
||||
in caplog.text
|
||||
in manim_caplog.text
|
||||
)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,11 @@ import datetime
|
|||
|
||||
import pytest
|
||||
|
||||
<<<<<<< HEAD
|
||||
from manim import Circle, FadeIn, Group, Manager, Mobject, Scene, Square
|
||||
=======
|
||||
from manim import Circle, FadeIn, Group, Mobject, Scene, Square
|
||||
>>>>>>> f1ce5122253882a20b6714adf728eccfa41162e9
|
||||
from manim.animation.animation import Wait
|
||||
|
||||
|
||||
|
|
@ -26,30 +30,14 @@ def test_scene_add_remove(dry_run):
|
|||
|
||||
# Check that Scene.add() returns the Scene (for chained calls)
|
||||
assert scene.add(Mobject()) is scene
|
||||
to_remove = Mobject()
|
||||
manager = Manager(Scene)
|
||||
scene = manager.scene
|
||||
scene.add(to_remove)
|
||||
scene.add(*(Mobject() for _ in range(10)))
|
||||
assert len(scene.mobjects) == 11
|
||||
scene.remove(to_remove)
|
||||
assert len(scene.mobjects) == 10
|
||||
scene.remove(to_remove)
|
||||
assert len(scene.mobjects) == 10
|
||||
|
||||
# Check that Scene.remove() returns the instance (for chained calls)
|
||||
assert scene.add(Mobject()) is scene
|
||||
|
||||
|
||||
def test_scene_time(dry_run):
|
||||
manager = Manager(Scene)
|
||||
scene = manager.scene
|
||||
assert scene.renderer.time == 0
|
||||
assert scene.time == 0
|
||||
scene.wait(2)
|
||||
assert scene.renderer.time == 2
|
||||
assert scene.time == 2
|
||||
scene.play(FadeIn(Circle()), run_time=0.5)
|
||||
assert pytest.approx(scene.renderer.time) == 2.5
|
||||
scene.renderer._original_skipping_status = True
|
||||
assert pytest.approx(scene.time) == 2.5
|
||||
scene._original_skipping_status = True
|
||||
scene.play(FadeIn(Square()), run_time=5) # this animation gets skipped.
|
||||
assert pytest.approx(scene.renderer.time) == 7.5
|
||||
|
||||
|
|
|
|||
|
|
@ -1,23 +1,13 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
|
||||
from manim.utils.deprecation import deprecated, deprecated_params
|
||||
|
||||
|
||||
def _get_caplog_record_msg(warn_caplog_manim):
|
||||
logger_name, level, message = warn_caplog_manim.record_tuples[0]
|
||||
def _get_caplog_record_msg(manim_caplog):
|
||||
logger_name, level, message = manim_caplog.record_tuples[0]
|
||||
return message
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def warn_caplog_manim(caplog):
|
||||
caplog.set_level(logging.WARNING, logger="manim")
|
||||
yield caplog
|
||||
|
||||
|
||||
@deprecated
|
||||
class Foo:
|
||||
def __init__(self):
|
||||
|
|
@ -77,11 +67,11 @@ class QuuzAll:
|
|||
doc_admonition = "\n\n.. attention:: Deprecated\n "
|
||||
|
||||
|
||||
def test_deprecate_class_no_args(warn_caplog_manim):
|
||||
def test_deprecate_class_no_args(manim_caplog):
|
||||
"""Test the deprecation of a class (decorator with no arguments)."""
|
||||
f = Foo()
|
||||
assert len(warn_caplog_manim.record_tuples) == 1
|
||||
msg = _get_caplog_record_msg(warn_caplog_manim)
|
||||
assert len(manim_caplog.record_tuples) == 1
|
||||
msg = _get_caplog_record_msg(manim_caplog)
|
||||
assert (
|
||||
msg
|
||||
== "The class Foo has been deprecated and may be removed in a later version."
|
||||
|
|
@ -89,11 +79,11 @@ def test_deprecate_class_no_args(warn_caplog_manim):
|
|||
assert f.__doc__ == f"{doc_admonition}{msg}"
|
||||
|
||||
|
||||
def test_deprecate_class_since(warn_caplog_manim):
|
||||
def test_deprecate_class_since(manim_caplog):
|
||||
"""Test the deprecation of a class (decorator with since argument)."""
|
||||
b = Bar()
|
||||
assert len(warn_caplog_manim.record_tuples) == 1
|
||||
msg = _get_caplog_record_msg(warn_caplog_manim)
|
||||
assert len(manim_caplog.record_tuples) == 1
|
||||
msg = _get_caplog_record_msg(manim_caplog)
|
||||
assert (
|
||||
msg
|
||||
== "The class Bar has been deprecated since v0.6.0 and may be removed in a later version."
|
||||
|
|
@ -101,11 +91,11 @@ def test_deprecate_class_since(warn_caplog_manim):
|
|||
assert b.__doc__ == f"The Bar class.{doc_admonition}{msg}"
|
||||
|
||||
|
||||
def test_deprecate_class_until(warn_caplog_manim):
|
||||
def test_deprecate_class_until(manim_caplog):
|
||||
"""Test the deprecation of a class (decorator with until argument)."""
|
||||
bz = Baz()
|
||||
assert len(warn_caplog_manim.record_tuples) == 1
|
||||
msg = _get_caplog_record_msg(warn_caplog_manim)
|
||||
assert len(manim_caplog.record_tuples) == 1
|
||||
msg = _get_caplog_record_msg(manim_caplog)
|
||||
assert (
|
||||
msg
|
||||
== "The class Baz has been deprecated and is expected to be removed after 06/01/2021."
|
||||
|
|
@ -113,11 +103,11 @@ def test_deprecate_class_until(warn_caplog_manim):
|
|||
assert bz.__doc__ == f"The Baz class.{doc_admonition}{msg}"
|
||||
|
||||
|
||||
def test_deprecate_class_since_and_until(warn_caplog_manim):
|
||||
def test_deprecate_class_since_and_until(manim_caplog):
|
||||
"""Test the deprecation of a class (decorator with since and until arguments)."""
|
||||
qx = Qux()
|
||||
assert len(warn_caplog_manim.record_tuples) == 1
|
||||
msg = _get_caplog_record_msg(warn_caplog_manim)
|
||||
assert len(manim_caplog.record_tuples) == 1
|
||||
msg = _get_caplog_record_msg(manim_caplog)
|
||||
assert (
|
||||
msg
|
||||
== "The class Qux has been deprecated since 0.7.0 and is expected to be removed after 0.9.0-rc2."
|
||||
|
|
@ -125,11 +115,11 @@ def test_deprecate_class_since_and_until(warn_caplog_manim):
|
|||
assert qx.__doc__ == f"{doc_admonition}{msg}"
|
||||
|
||||
|
||||
def test_deprecate_class_msg(warn_caplog_manim):
|
||||
def test_deprecate_class_msg(manim_caplog):
|
||||
"""Test the deprecation of a class (decorator with msg argument)."""
|
||||
qu = Quux()
|
||||
assert len(warn_caplog_manim.record_tuples) == 1
|
||||
msg = _get_caplog_record_msg(warn_caplog_manim)
|
||||
assert len(manim_caplog.record_tuples) == 1
|
||||
msg = _get_caplog_record_msg(manim_caplog)
|
||||
assert (
|
||||
msg
|
||||
== "The class Quux has been deprecated and may be removed in a later version. Use something else."
|
||||
|
|
@ -137,11 +127,11 @@ def test_deprecate_class_msg(warn_caplog_manim):
|
|||
assert qu.__doc__ == f"{doc_admonition}{msg}"
|
||||
|
||||
|
||||
def test_deprecate_class_replacement(warn_caplog_manim):
|
||||
def test_deprecate_class_replacement(manim_caplog):
|
||||
"""Test the deprecation of a class (decorator with replacement argument)."""
|
||||
qz = Quuz()
|
||||
assert len(warn_caplog_manim.record_tuples) == 1
|
||||
msg = _get_caplog_record_msg(warn_caplog_manim)
|
||||
assert len(manim_caplog.record_tuples) == 1
|
||||
msg = _get_caplog_record_msg(manim_caplog)
|
||||
assert (
|
||||
msg
|
||||
== "The class Quuz has been deprecated and may be removed in a later version. Use ReplaceQuuz instead."
|
||||
|
|
@ -150,11 +140,11 @@ def test_deprecate_class_replacement(warn_caplog_manim):
|
|||
assert qz.__doc__ == f"{doc_admonition}{doc_msg}"
|
||||
|
||||
|
||||
def test_deprecate_class_all(warn_caplog_manim):
|
||||
def test_deprecate_class_all(manim_caplog):
|
||||
"""Test the deprecation of a class (decorator with all arguments)."""
|
||||
qza = QuuzAll()
|
||||
assert len(warn_caplog_manim.record_tuples) == 1
|
||||
msg = _get_caplog_record_msg(warn_caplog_manim)
|
||||
assert len(manim_caplog.record_tuples) == 1
|
||||
msg = _get_caplog_record_msg(manim_caplog)
|
||||
assert (
|
||||
msg
|
||||
== "The class QuuzAll has been deprecated since 0.7.0 and is expected to be removed after 1.2.1. Use ReplaceQuuz instead. Don't use this please."
|
||||
|
|
@ -243,11 +233,11 @@ class Top:
|
|||
return kwargs
|
||||
|
||||
|
||||
def test_deprecate_func_no_args(warn_caplog_manim):
|
||||
def test_deprecate_func_no_args(manim_caplog):
|
||||
"""Test the deprecation of a method (decorator with no arguments)."""
|
||||
useless()
|
||||
assert len(warn_caplog_manim.record_tuples) == 1
|
||||
msg = _get_caplog_record_msg(warn_caplog_manim)
|
||||
assert len(manim_caplog.record_tuples) == 1
|
||||
msg = _get_caplog_record_msg(manim_caplog)
|
||||
assert (
|
||||
msg
|
||||
== "The function useless has been deprecated and may be removed in a later version."
|
||||
|
|
@ -255,12 +245,12 @@ def test_deprecate_func_no_args(warn_caplog_manim):
|
|||
assert useless.__doc__ == f"{doc_admonition}{msg}"
|
||||
|
||||
|
||||
def test_deprecate_func_in_class_since_and_message(warn_caplog_manim):
|
||||
def test_deprecate_func_in_class_since_and_message(manim_caplog):
|
||||
"""Test the deprecation of a method within a class (decorator with since and message arguments)."""
|
||||
t = Top()
|
||||
t.mid_func()
|
||||
assert len(warn_caplog_manim.record_tuples) == 1
|
||||
msg = _get_caplog_record_msg(warn_caplog_manim)
|
||||
assert len(manim_caplog.record_tuples) == 1
|
||||
msg = _get_caplog_record_msg(manim_caplog)
|
||||
assert (
|
||||
msg
|
||||
== "The method Top.mid_func has been deprecated since 0.8.0 and may be removed in a later version. This method is useless."
|
||||
|
|
@ -268,11 +258,11 @@ def test_deprecate_func_in_class_since_and_message(warn_caplog_manim):
|
|||
assert t.mid_func.__doc__ == f"Middle function in Top.{doc_admonition}{msg}"
|
||||
|
||||
|
||||
def test_deprecate_nested_class_until_and_replacement(warn_caplog_manim):
|
||||
def test_deprecate_nested_class_until_and_replacement(manim_caplog):
|
||||
"""Test the deprecation of a nested class (decorator with until and replacement arguments)."""
|
||||
n = Top().Nested()
|
||||
assert len(warn_caplog_manim.record_tuples) == 1
|
||||
msg = _get_caplog_record_msg(warn_caplog_manim)
|
||||
assert len(manim_caplog.record_tuples) == 1
|
||||
msg = _get_caplog_record_msg(manim_caplog)
|
||||
assert (
|
||||
msg
|
||||
== "The class Top.Nested has been deprecated and is expected to be removed after 1.4.0. Use Top.NewNested instead."
|
||||
|
|
@ -281,12 +271,12 @@ def test_deprecate_nested_class_until_and_replacement(warn_caplog_manim):
|
|||
assert n.__doc__ == f"{doc_admonition}{doc_msg}"
|
||||
|
||||
|
||||
def test_deprecate_nested_class_func_since_and_until(warn_caplog_manim):
|
||||
def test_deprecate_nested_class_func_since_and_until(manim_caplog):
|
||||
"""Test the deprecation of a method within a nested class (decorator with since and until arguments)."""
|
||||
n = Top().NewNested()
|
||||
n.nested_func()
|
||||
assert len(warn_caplog_manim.record_tuples) == 1
|
||||
msg = _get_caplog_record_msg(warn_caplog_manim)
|
||||
assert len(manim_caplog.record_tuples) == 1
|
||||
msg = _get_caplog_record_msg(manim_caplog)
|
||||
assert (
|
||||
msg
|
||||
== "The method Top.NewNested.nested_func has been deprecated since 1.0.0 and is expected to be removed after 12/25/2025."
|
||||
|
|
@ -297,13 +287,13 @@ def test_deprecate_nested_class_func_since_and_until(warn_caplog_manim):
|
|||
)
|
||||
|
||||
|
||||
def test_deprecate_nested_func(warn_caplog_manim):
|
||||
def test_deprecate_nested_func(manim_caplog):
|
||||
"""Test the deprecation of a nested method (decorator with no arguments)."""
|
||||
b = Top().Bottom()
|
||||
answer = b.normal_func()
|
||||
answer(1)
|
||||
assert len(warn_caplog_manim.record_tuples) == 1
|
||||
msg = _get_caplog_record_msg(warn_caplog_manim)
|
||||
assert len(manim_caplog.record_tuples) == 1
|
||||
msg = _get_caplog_record_msg(manim_caplog)
|
||||
assert (
|
||||
msg
|
||||
== "The method Top.Bottom.normal_func.<locals>.nested_func has been deprecated and may be removed in a later version."
|
||||
|
|
@ -311,36 +301,36 @@ def test_deprecate_nested_func(warn_caplog_manim):
|
|||
assert answer.__doc__ == f"{doc_admonition}{msg}"
|
||||
|
||||
|
||||
def test_deprecate_func_params(warn_caplog_manim):
|
||||
def test_deprecate_func_params(manim_caplog):
|
||||
"""Test the deprecation of method parameters (decorator with params argument)."""
|
||||
t = Top()
|
||||
t.foo(a=2, b=3, z=4)
|
||||
assert len(warn_caplog_manim.record_tuples) == 1
|
||||
msg = _get_caplog_record_msg(warn_caplog_manim)
|
||||
assert len(manim_caplog.record_tuples) == 1
|
||||
msg = _get_caplog_record_msg(manim_caplog)
|
||||
assert (
|
||||
msg
|
||||
== "The parameters a and b of method Top.foo have been deprecated and may be removed in a later version. Use something else."
|
||||
)
|
||||
|
||||
|
||||
def test_deprecate_func_single_param_since_and_until(warn_caplog_manim):
|
||||
def test_deprecate_func_single_param_since_and_until(manim_caplog):
|
||||
"""Test the deprecation of a single method parameter (decorator with since and until arguments)."""
|
||||
t = Top()
|
||||
t.bar(a=1, b=2)
|
||||
assert len(warn_caplog_manim.record_tuples) == 1
|
||||
msg = _get_caplog_record_msg(warn_caplog_manim)
|
||||
assert len(manim_caplog.record_tuples) == 1
|
||||
msg = _get_caplog_record_msg(manim_caplog)
|
||||
assert (
|
||||
msg
|
||||
== "The parameter a of method Top.bar has been deprecated since v0.2 and is expected to be removed after v0.4."
|
||||
)
|
||||
|
||||
|
||||
def test_deprecate_func_param_redirect_tuple(warn_caplog_manim):
|
||||
def test_deprecate_func_param_redirect_tuple(manim_caplog):
|
||||
"""Test the deprecation of a method parameter and redirecting it to a new one using tuple."""
|
||||
t = Top()
|
||||
obj = t.baz(x=1, old_param=2)
|
||||
assert len(warn_caplog_manim.record_tuples) == 1
|
||||
msg = _get_caplog_record_msg(warn_caplog_manim)
|
||||
assert len(manim_caplog.record_tuples) == 1
|
||||
msg = _get_caplog_record_msg(manim_caplog)
|
||||
assert (
|
||||
msg
|
||||
== "The parameter old_param of method Top.baz has been deprecated and may be removed in a later version."
|
||||
|
|
@ -348,12 +338,12 @@ def test_deprecate_func_param_redirect_tuple(warn_caplog_manim):
|
|||
assert obj == {"x": 1, "new_param": 2}
|
||||
|
||||
|
||||
def test_deprecate_func_param_redirect_lambda(warn_caplog_manim):
|
||||
def test_deprecate_func_param_redirect_lambda(manim_caplog):
|
||||
"""Test the deprecation of a method parameter and redirecting it to a new one using lambda function."""
|
||||
t = Top()
|
||||
obj = t.qux(runtime_in_ms=500)
|
||||
assert len(warn_caplog_manim.record_tuples) == 1
|
||||
msg = _get_caplog_record_msg(warn_caplog_manim)
|
||||
assert len(manim_caplog.record_tuples) == 1
|
||||
msg = _get_caplog_record_msg(manim_caplog)
|
||||
assert (
|
||||
msg
|
||||
== "The parameter runtime_in_ms of method Top.qux has been deprecated and may be removed in a later version."
|
||||
|
|
@ -361,12 +351,12 @@ def test_deprecate_func_param_redirect_lambda(warn_caplog_manim):
|
|||
assert obj == {"run_time": 0.5}
|
||||
|
||||
|
||||
def test_deprecate_func_param_redirect_many_to_one(warn_caplog_manim):
|
||||
def test_deprecate_func_param_redirect_many_to_one(manim_caplog):
|
||||
"""Test the deprecation of multiple method parameters and redirecting them to one."""
|
||||
t = Top()
|
||||
obj = t.quux(point2D_x=3, point2D_y=5)
|
||||
assert len(warn_caplog_manim.record_tuples) == 1
|
||||
msg = _get_caplog_record_msg(warn_caplog_manim)
|
||||
assert len(manim_caplog.record_tuples) == 1
|
||||
msg = _get_caplog_record_msg(manim_caplog)
|
||||
assert (
|
||||
msg
|
||||
== "The parameters point2D_x and point2D_y of method Top.quux have been deprecated and may be removed in a later version."
|
||||
|
|
@ -374,23 +364,23 @@ def test_deprecate_func_param_redirect_many_to_one(warn_caplog_manim):
|
|||
assert obj == {"point2D": (3, 5)}
|
||||
|
||||
|
||||
def test_deprecate_func_param_redirect_one_to_many(warn_caplog_manim):
|
||||
def test_deprecate_func_param_redirect_one_to_many(manim_caplog):
|
||||
"""Test the deprecation of one method parameter and redirecting it to many."""
|
||||
t = Top()
|
||||
obj1 = t.quuz(point2D=0)
|
||||
assert len(warn_caplog_manim.record_tuples) == 1
|
||||
msg = _get_caplog_record_msg(warn_caplog_manim)
|
||||
assert len(manim_caplog.record_tuples) == 1
|
||||
msg = _get_caplog_record_msg(manim_caplog)
|
||||
assert (
|
||||
msg
|
||||
== "The parameter point2D of method Top.quuz has been deprecated and may be removed in a later version."
|
||||
)
|
||||
assert obj1 == {"x": 0, "y": 0}
|
||||
|
||||
warn_caplog_manim.clear()
|
||||
manim_caplog.clear()
|
||||
|
||||
obj2 = t.quuz(point2D=(2, 3))
|
||||
assert len(warn_caplog_manim.record_tuples) == 1
|
||||
msg = _get_caplog_record_msg(warn_caplog_manim)
|
||||
assert len(manim_caplog.record_tuples) == 1
|
||||
msg = _get_caplog_record_msg(manim_caplog)
|
||||
assert (
|
||||
msg
|
||||
== "The parameter point2D of method Top.quuz has been deprecated and may be removed in a later version."
|
||||
|
|
|
|||
|
|
@ -75,6 +75,18 @@ def test_transparent(config):
|
|||
np.testing.assert_allclose(frame[0, 0], [0, 0, 0, 0])
|
||||
|
||||
|
||||
def test_transparent_by_background_opacity(config, dry_run):
|
||||
config.background_opacity = 0.5
|
||||
assert config.transparent is True
|
||||
|
||||
manager = Manager(MyScene)
|
||||
manager.render()
|
||||
frame = manager.renderer.get_pixels()
|
||||
np.testing.assert_allclose(frame[0, 0], [0, 0, 0, 127])
|
||||
assert config.movie_file_extension == ".mov"
|
||||
assert config.transparent is True
|
||||
|
||||
|
||||
def test_background_color(config):
|
||||
"""Test the 'background_color' config option."""
|
||||
|
||||
|
|
@ -221,7 +233,6 @@ def test_dry_run_with_png_format_skipped_animations(config, dry_run):
|
|||
manager = Manager(MyScene)
|
||||
manager.render()
|
||||
|
||||
|
||||
def test_tex_template_file(tmp_path):
|
||||
"""Test that a custom tex template file can be set from a config file."""
|
||||
tex_file = Path(tmp_path / "my_template.tex")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue