From 8b90f4da96743b581a6a967e41bd0302b5b53e43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francisco=20Manr=C3=ADquez?= Date: Sun, 15 Dec 2024 19:41:30 -0300 Subject: [PATCH 01/11] Add ThreeDScene and rewrite Camera internals --- manim/__init__.py | 2 + manim/camera/camera.py | 553 ++++++++++++++++++---- manim/mobject/three_d/three_dimensions.py | 5 +- manim/renderer/opengl_renderer.py | 6 +- manim/renderer/renderer.py | 2 + manim/scene/three_d_scene.py | 491 +++++++------------ 6 files changed, 648 insertions(+), 411 deletions(-) diff --git a/manim/__init__.py b/manim/__init__.py index f9bded2b2f..1ea06d1638 100644 --- a/manim/__init__.py +++ b/manim/__init__.py @@ -56,6 +56,7 @@ from manim.mobject.mobject import * from manim.mobject.opengl.dot_cloud import * from manim.mobject.opengl.opengl_point_cloud_mobject import * +from manim.mobject.opengl.opengl_vectorized_mobject import * from manim.mobject.svg.brace import * from manim.mobject.svg.svg_mobject import * from manim.mobject.table import * @@ -73,6 +74,7 @@ from manim.mobject.vector_field import * from manim.scene.scene import * from manim.scene.sections import * +from manim.scene.three_d_scene import * from manim.scene.vector_space_scene import * from manim.utils import color, rate_functions, unit from manim.utils.bezier import * diff --git a/manim/camera/camera.py b/manim/camera/camera.py index 4a26f98ab6..b249eaf424 100644 --- a/manim/camera/camera.py +++ b/manim/camera/camera.py @@ -3,123 +3,375 @@ from __future__ import annotations import math +from typing import TYPE_CHECKING, Any, TypedDict import numpy as np -from scipy.spatial.transform import Rotation from manim._config import config +from manim.constants import * from manim.mobject.opengl.opengl_mobject import OpenGLMobject +from manim.utils.paths import straight_path +from manim.utils.space_ops import rotation_matrix -from ..constants import * -from ..utils.space_ops import normalize +if TYPE_CHECKING: + from typing_extensions import Self + + from manim.typing import MatrixMN, Point3D, Vector3D + + +class CameraOrientationConfig(TypedDict, total=False): + theta: float | None + phi: float | None + gamma: float | None + zoom: float | None + focal_distance: float | None + frame_center: OpenGLMobject | Sequence[float] | None class Camera(OpenGLMobject): def __init__( self, frame_shape: tuple[float, float] = (config.frame_width, config.frame_height), - center_point: np.ndarray = ORIGIN, - focal_dist_to_height: float = 2.0, + center_point: Point3D = ORIGIN, # TODO: use Point3DLike + focal_distance: float = 16.0, **kwargs, ): - self.frame_shape = frame_shape + self.initial_frame_shape = frame_shape self.center_point = center_point - self.focal_dist_to_height = focal_dist_to_height - self.orientation = Rotation.identity().as_quat() + self.focal_distance = focal_distance + self.set_euler_angles(theta=-TAU / 4, phi=0.0, gamma=0.0) + self.ambient_rotation_updaters_dict: dict[Updater | None] = { + "theta": None, + "gamma": None, + "phi": None, + } + self.precession_updater: Updater | None = None super().__init__(**kwargs) def init_points(self) -> None: self.set_points([ORIGIN, LEFT, RIGHT, DOWN, UP]) - self.set_width(self.frame_shape[0], stretch=True) - self.set_height(self.frame_shape[1], stretch=True) + self.set_width(self.initial_frame_shape[0], stretch=True) + self.set_height(self.initial_frame_shape[1], stretch=True) self.move_to(self.center_point) - def set_orientation(self, rotation: Rotation): - self.orientation = rotation.as_quat() + def interpolate( + self, + mobject1: OpenGLMobject, + mobject2: OpenGLMobject, + alpha: float, + path_func: PathFuncType = straight_path(), + ) -> Self: + """Interpolate the orientation of two cameras.""" + cam1: Camera = mobject1 + cam2: Camera = mobject2 + + orientation1 = cam1.get_orientation() + orientation2 = cam2.get_orientation() + new_orientation = { + key: path_func(orientation1[key], orientation2[key], alpha) + for key in orientation1 + } + return self.set_orientation(**new_orientation) + + def get_orientation(self) -> CameraOrientationConfig: + return { + "theta": self.get_theta(), + "phi": self.get_phi(), + "gamma": self.get_gamma(), + "zoom": self.get_zoom(), + "focal_distance": self.focal_distance, + "frame_center": self.get_center(), + } + + def set_orientation( + self, + theta: float | None = None, + phi: float | None = None, + gamma: float | None = None, + zoom: float | None = None, + focal_distance: float | None = None, + frame_center: OpenGLMobject | Point3D | None = None, # TODO: use Point3DLike + ) -> Self: + """This method sets the orientation of the camera in the scene. + + Parameters + ---------- + theta + The azimuthal angle i.e the angle that spins the camera around the Z_AXIS. + phi + The polar angle i.e the angle between Z_AXIS and Camera through ORIGIN in radians. + gamma + The rotation of the camera about the vector from the ORIGIN to the Camera. + zoom + The zoom factor of the scene. + focal_distance + The focal_distance of the Camera. + frame_center + The new center of the camera frame in cartesian coordinates. + + Returns + ------- + :class:`Camera` + The camera after applying all changes. + """ + self.set_euler_angles(theta=theta, phi=phi, gamma=gamma) + if focal_distance is not None: + self.focal_distance = focal_distance + if zoom is not None: + self.set_zoom(zoom) + if frame_center is not None: + self.move_to(frame_center) return self - def get_orientation(self): - return Rotation.from_quat(self.orientation) - - def get_euler_angles(self): - return self.get_orientation().as_euler("zxz")[::-1] - - def get_theta(self): - return self.get_euler_angles()[0] - - def get_phi(self): - return self.get_euler_angles()[1] - - def get_gamma(self): - return self.get_euler_angles()[2] - - def get_inverse_camera_rotation_matrix(self): - return self.get_orientation().as_matrix().T - - def rotate(self, angle: float, axis: np.ndarray = OUT, **kwargs): - rot = Rotation.from_rotvec(axis * normalize(axis)) - self.set_orientation(rot * self.get_orientation()) + def get_euler_angles(self) -> np.ndarray: + return np.array([self._theta, self._phi, self._gamma]) def set_euler_angles( self, theta: float | None = None, phi: float | None = None, gamma: float | None = None, - units: float = RADIANS, - ): - eulers = self.get_euler_angles() # theta, phi, gamma - for i, var in enumerate([theta, phi, gamma]): - if var is not None: - eulers[i] = var * units - self.set_orientation(Rotation.from_euler("zxz", eulers[::-1])) + ) -> Self: + if theta is not None: + self.set_theta(theta) + if phi is not None: + self.set_phi(phi) + if gamma is not None: + self.set_gamma(gamma) return self - def reorient( - self, - theta_degrees: float | None = None, - phi_degrees: float | None = None, - gamma_degrees: float | None = None, - ): - """ - Shortcut for set_euler_angles, defaulting to taking - in angles in degrees - """ - self.set_euler_angles(theta_degrees, phi_degrees, gamma_degrees, units=DEGREES) + def get_theta(self) -> float: + return self._theta + + def set_theta(self, theta: float) -> Self: + self._theta = theta + self._rotation_matrix = None + # If we don't add TAU/4 (90°) to theta, the camera will be positioned + # over the negative Y axis instead of the positive X axis. + cos = np.cos(theta + TAU / 4) + sin = np.sin(theta + TAU / 4) + self._theta_z_matrix = np.array( + [ + [cos, -sin, 0], + [sin, cos, 0], + [0, 0, 1], + ] + ) + return self + + def increment_theta(self, dtheta: float) -> Self: + return self.set_theta(self._theta + dtheta) + + def get_phi(self) -> float: + return self._phi + + def set_phi(self, phi: float) -> Self: + self._phi = phi + self._rotation_matrix = None + cos = np.cos(phi) + sin = np.sin(phi) + self._phi_x_matrix = np.array( + [ + [1, 0, 0], + [0, cos, -sin], + [0, sin, cos], + ] + ) return self - def set_theta(self, theta: float): - return self.set_euler_angles(theta=theta) + def increment_phi(self, dphi: float) -> Self: + return self.set_phi(self._phi + dgamma) + + def get_gamma(self) -> float: + return self._gamma + + def set_gamma(self, gamma: float) -> Self: + self._gamma = gamma + self._rotation_matrix = None + cos = np.cos(gamma) + sin = np.sin(gamma) + self._gamma_z_matrix = np.array( + [ + [cos, -sin, 0], + [sin, cos, 0], + [0, 0, 1], + ] + ) + return self - def set_phi(self, phi: float): - return self.set_euler_angles(phi=phi) + def increment_gamma(self, dgamma: float) -> Self: + return self.set_gamma(self._gamma + dgamma) - def set_gamma(self, gamma: float): - return self.set_euler_angles(gamma=gamma) + def get_rotation_matrix(self) -> MatrixMN: + r"""Get the current rotation matrix. - def increment_theta(self, dtheta: float): - self.rotate(dtheta, OUT) - return self + In order to get the current rotation using the Euler angles: - def increment_phi(self, dphi: float): - self.rotate(dphi, self.get_inverse_camera_rotation_matrix()[0]) - return self + 1. Rotate :math:`\gamma` along the Z axis (XY plane). + 2. Rotate :math:`\varphi` along the X axis (YZ plane). + 3. Rotate :math:`\theta` along the Z axis again (XY plane). - def increment_gamma(self, dgamma: float): - self.rotate(dgamma, self.get_inverse_camera_rotation_matrix()[2]) - return self + See :meth:`Camera.rotate()` for more information. - def set_focal_distance(self, focal_distance: float): - self.focal_dist_to_height = focal_distance / self.get_height() + Returns + ------- + MatrixMN + The current 3x3 rotation matrix. + """ + if self._rotation_matrix is None: + self._rotation_matrix = ( + self._theta_z_matrix @ self._phi_x_matrix @ self._gamma_z_matrix + ) + return self._rotation_matrix + + def get_inverse_rotation_matrix(self) -> MatrixMN: + return self.get_rotation_matrix().T + + # TODO: rotate is still unreliable. The Euler angles are automatically + # standardized to (-TAU/2, TAU/2), leading to potentially unwanted behavior + # when animating. Plus, if the camera is on the Z axis, which occurs when + # phi is a multiple of TAU/2, the current implementation can only determine + # theta + gamma, but not exactly theta or gamma yet. + def rotate(self, angle: float, axis: Vector3D = OUT, **kwargs: Any) -> Self: + r"""Rotate the camera in a given ``angle`` along the given ``axis``. + + After rotating the camera, the Euler angles must be recalculated, Given + the Euler angles :math:`\theta`, :math:`\varphi` and :math:`\gamma`, + the current rotation matrix is obtained in this way: + + 1. Rotate :math:`\gamma` along the Z axis (XY plane). This + corresponds to multiplying by the following matrix: + + .. math:: + + R_z(\gamma) = \begin{pmatrix} + \cos(\gamma) & -\sin(\gamma) & 0 \\ + \sin(\gamma) & \cos(\gamma) & 0 \\ + 0 & 0 & 1 + \end{pmatrix} + + 2. Rotate :math:`\varphi` along the X axis (YZ plane). This + corresponds to multiplying by the following matrix: + + .. math:: + + R_x(\varphi) = \begin{pmatrix} + 1 & 0 & 0 \\ + 0 & \cos(\varphi) & -\sin(\gamma) \\ + 0 & \sin(\varphi) & \cos(\varphi) + \end{pmatrix} + + 3. Rotate :math:`\theta` along the Z axis again (XY plane). This + corresponds to multiplying by the following matrix: + + .. math:: + + R_z(\theta) = \begin{pmatrix} + \cos(\theta) & -\sin(\theta) & 0 \\ + \sin(\theta) & \cos(\theta) & 0 \\ + 0 & 0 & 1 + \end{pmatrix} + + Applying these matrices in order, the final rotation matrix is: + + .. math:: + + R = R_z(\theta) R_x(\varphi) R_z(\gamma) = \begin{pmatrix} + \cos(\theta)\cos(\gamma) - \sin(\theta)\cos(\varphi)\cos(\gamma) & -\sin(\theta)\cos(varphi)\cos(\gamma) - \cos(\theta)\sin(\gamma) & \sin(\theta)\sin(\varphi) \\ + \cos(\theta)\cos(\varphi)\sin(\gamma) + \sin(\theta)\cos(\gamma) & \cos(\theta)\cos(varphi)\cos(\gamma) - \sin(\theta)\sin(\gamma) & -\cos(\theta)\sin(\varphi) \\ + \sin(\varphi)\sin(\gamma) & \sin(\varphi)\cos(\gamma) & \cos(\varphi) + \end{pmatrix} + + From this matrix, if :math:`\sin(\varphi) \neq 0`, then it is possible + to retrieve the Euler angles in the following way: + + .. math:: + + \frac{R_{1,3}}{-R_{2,3}} = \frac{\sin(\theta)\sin(\varphi)}{\cos(\theta)\sin(\varphi)} = \tan(\theta) \quad &\Longrightarrow \quad \theta = \text{atan2}(-R_{2,3}, R_{1,3}) \\ + \frac{\sqrt{R_{1,3}^2 + R_{2,3}^2}}{R_{3,3}} = \frac{\sqrt{\sin^2(\theta)\sin^2(\varphi) + \cos^2(\theta)\sin^2(\varphi)}}{\cos(\varphi)} = \tan(\varphi) \quad &\Longrightarrow \quad \theta = \text{atan2}(\sqrt{R_{1,3}^2 + R_{2,3}^2}, R_{3,3}) \\ + \frac{R_{3,1}}{R_{3,2}} = \frac{\sin(\varphi)\sin(\gamma)}{\sin(\varphi)\cos(\gamma)} = \tan(\gamma) \quad &\Longrightarrow \quad \theta = \text{atan2}(R_{3,2}, R_{3,1}) \\ + + However, if :math:`\sin(\varphi) = 0`, then: + + .. math:: + \frac{R_{2,2}}{R_{1,1}} = \frac{\sin(\theta \pm \gamma)}{\cos(\theta \pm \gamma)} = \tan(\theta \pm \gamma) \quad \Longrightarrow \quad \theta \pm \gamma = \text{atan2}(R_{1,1}, R_{2,2}) + + and currently there's no implemented way to exactly find :math:`\theta` + and :math:`\gamma`, so this function sets :math:`\gamma = 0`. + + .. warning:: + + This method is still unreliable. The Euler angles are automatically + standardized to (-TAU/2, TAU/2), leading to potentially unwanted behavior + when using :attr:`OpenGLMobject.animate`. Plus, if the camera is on + the Z axis, which occurs when phi is a multiple of TAU/2, the current + implementation can only determine theta +- gamma, but not exactly + theta or gamma yet. + + Parameters + ---------- + angle + Angle of rotation. + axis + Axis of rotation. + **kwargs + Additional parameters which are required by + :meth:`OpenGLMobject.rotate`. + + Returns + ------- + :class:`Camera` + The camera after the rotation. + """ + new_rot = rotation_matrix(angle, axis) @ self.get_rotation_matrix() + + # Recalculate theta, phi and gamma. + cos_phi = new_rot[2, 2] + # If phi is 0 or TAU/2, there's a gimbal lock and it's not trivial to + # determine theta and gamma, only theta +- gamma. + if cos_phi in [-1, 1]: + cos_theta_pm_gamma = new_rot[0, 0] + sin_theta_pm_gamma = new_rot[1, 0] + theta_pm_gamma = np.arctan2(cos_theta_pm_gamma, sin_theta_pm_gamma) + + # TODO: based on the axis, maybe there is a way to recover theta and gamma. + theta = theta_pm_gamma + phi = 0.0 if cos_phi == 1 else TAU / 2 + gamma = 0.0 + else: + sin_theta_sin_phi = new_rot[0, 2] + cos_theta_sin_phi = -new_rot[1, 2] + theta = np.arctan2(sin_theta_sin_phi, cos_theta_sin_phi) + + sin_phi = np.sqrt(sin_theta_sin_phi**2 + cos_theta_sin_phi**2) + phi = np.arctan2(cos_phi, sin_phi) + + sin_phi_sin_gamma = new_rot[2, 0] + sin_phi_cos_gamma = new_rot[2, 1] + gamma = np.arctan2(sin_phi_cos_gamma, sin_phi_sin_gamma) + + self.set_euler_angles(theta=theta, phi=phi, gamma=gamma) + self._rotation_matrix = new_rot return self - def set_field_of_view(self, field_of_view: float): - self.focal_dist_to_height = 2 * math.tan(field_of_view / 2) + def get_zoom(self) -> float: + return config.frame_height / self.height + + def set_zoom(self, zoom: float) -> Self: + scale_factor = config.frame_height / (zoom * self.height) + return self.scale(scale_factor) + + def get_field_of_view(self) -> float: + return 2 * math.atan(self.focal_distance / (2 * self.height)) + + def set_field_of_view(self, field_of_view: float) -> Self: + self.focal_distance = 2 * math.tan(field_of_view / 2) * self.height return self - def get_shape(self): + def get_frame_shape(self) -> tuple[float, float]: return (self.get_width(), self.get_height()) - def get_center(self) -> np.ndarray: + def get_center(self) -> Point3D: # Assumes first point is at the center return self.points[0] @@ -131,13 +383,154 @@ def get_height(self) -> float: points = self.points return points[4, 1] - points[3, 1] - def get_focal_distance(self) -> float: - return self.focal_dist_to_height * self.get_height() # type: ignore - - def get_field_of_view(self) -> float: - return 2 * math.atan(self.focal_dist_to_height / 2) - - def get_implied_camera_location(self) -> np.ndarray: - to_camera = self.get_inverse_camera_rotation_matrix()[2] + def get_implied_camera_location(self) -> Point3D: + to_camera = self.get_rotation_matrix()[:, 2] dist = self.get_focal_distance() return self.get_center() + dist * to_camera + + # Movement methods + + def begin_ambient_rotation(self, rate: float = 0.02, about: str = "theta") -> Self: + """Apply an updater to rotate the camera on every frame by modifying + one of three Euler angles: "theta" (rotate about the Z axis), "phi" + (modify the angle between the camera and the Z axis) or "gamma" (rotate + the camera in its position while it's looking at the same point). + + Parameters + ---------- + rate + The rate at which the camera should rotate about the specified + angle. A positive rate means counterclockwise rotation, and a + negative rate means clockwise rotation. + about + One of 3 options: ["theta", "phi", "gamma"]. Defaults to "theta". + + Returns + ------- + :class:`Camera` + The camera after applying the rotation updater. + """ + # TODO, use a ValueTracker for rate, so that it + # can begin and end smoothly + about: str = about.lower() + self.stop_ambient_rotation(about=about) + + methods = { + "theta": self.increment_theta, + "phi": self.increment_phi, + "gamma": self.increment_gamma, + } + if about not in methods: + raise ValueError(f"Invalid ambient rotation angle '{about}'.") + + def ambient_rotation(mob: Camera, dt: float) -> Camera: + methods[about](rate * dt) + return mob + + self.add_updater(ambient_rotation) + self.ambient_rotation_updaters_dict[about] = ambient_rotation + return self + + def stop_ambient_rotation(self, about: str = "theta") -> Self: + """Stop ambient camera rotation on the specified angle. If there's a + corresponding ambient rotation updater applied on the camera, remove + it. + + Parameters + ---------- + about + The Euler angle for which the rotation should stop. This angle can + be "theta", "phi" or "gamma". Defaults to "theta". + + Returns + ------- + :class:`Camera` + The camera after applying the rotation updater. + """ + about: str = about.lower() + if about not in self.ambient_rotation_updaters_dict: + raise ValueError(f"Invalid ambient rotation angle '{about}'.") + + updater = self.ambient_rotation_updaters_dict[about] + if updater is not None: + self.remove_updater(updater) + self.ambient_rotation_updaters_dict[about] = None + + return self + + def begin_precession( + self, + rate: float = 1.0, + radius: float = 0.2, + origin_theta: float | None = None, + origin_phi: float | None = None, + ) -> Self: + """Begin a camera precession by adding an updater. This precession + consists of moving around the point given by ``origin_phi`` and + ``origin_theta``, keeping the ``gamma`` Euler angle constant. + + Parameters + ---------- + rate + The rate at which the camera precession should operate. + radius + The precession radius. + origin_phi + The polar angle the camera should move around. If ``None``, + defaults to the current ``phi`` angle. + origin_theta + The azimutal angle the camera should move around. If ``None``, + defaults to the current ``theta`` angle. + + Returns + ------- + :class:`Camera` + The camera after applying the precession updater. + """ + self.stop_precession() + + if origin_theta is None: + origin_theta = self.get_theta() + if origin_phi is None: + origin_phi = self.get_phi() + + precession_angle = 0.0 + + def precession(mob: Camera, dt: float) -> Camera: + nonlocal precession_angle + precession_angle += rate * dt + dtheta = radius * np.sin(precession_angle) + dphi = radius * np.cos(precession_angle) + return mob.set_theta(origin_theta + dtheta).set_phi(origin_phi + dphi) + + self.add_updater(precession) + self.precession_updater = precession + return self + + def stop_precession(self) -> Self: + """Remove the precession camera updater, if any. + + Returns + ------- + :class:`Camera` + The camera after removing the precession updater. + """ + updater = self.precession_updater + if updater is not None: + self.remove_updater(updater) + self.precession_updater = None + return self + + def begin_3dillusion_rotation( + self, + rate: float = 1.0, + radius: float = 0.2, + origin_theta: float | None = None, + origin_phi: float | None = None, + ) -> Self: + """Alias for :meth:`begin_precession`, which is preferred.""" + return self.begin_precession(rate, radius, origin_theta, origin_phi) + + def stop_3dillusion_rotation(self) -> Self: + """Alias for :meth:`stop_precession`, which is preferred.""" + return self.stop_precession() diff --git a/manim/mobject/three_d/three_dimensions.py b/manim/mobject/three_d/three_dimensions.py index d3e9ca123a..014b4b3b8e 100644 --- a/manim/mobject/three_d/three_dimensions.py +++ b/manim/mobject/three_d/three_dimensions.py @@ -29,7 +29,6 @@ from manim.constants import * from manim.mobject.geometry.arc import Circle from manim.mobject.geometry.polygram import Square -from manim.mobject.mobject import * from manim.mobject.opengl.opengl_mobject import OpenGLMobject from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject from manim.mobject.types.vectorized_mobject import VectorizedPoint, VGroup @@ -110,6 +109,7 @@ def __init__( checkerboard_colors: Sequence[ParsableManimColor] | bool = [BLUE_D, BLUE_E], stroke_color: ParsableManimColor = LIGHT_GREY, stroke_width: float = 0.5, + stroke_opacity: float = 1.0, # TODO: placed temporarily to have a stroke_opacity should_make_jagged: bool = False, pre_function_handle_to_anchor_scale_factor: float = 0.00001, **kwargs: Any, @@ -120,7 +120,7 @@ def __init__( self.resolution = resolution self.surface_piece_config = surface_piece_config self.fill_color: ManimColor = ManimColor(fill_color) - self.fill_opacity = fill_opacity + self.fill_opacity = fill_opacity # TODO: remove if checkerboard_colors: self.checkerboard_colors: list[ManimColor] = [ ManimColor(x) for x in checkerboard_colors @@ -129,6 +129,7 @@ def __init__( self.checkerboard_colors = checkerboard_colors self.stroke_color: ManimColor = ManimColor(stroke_color) self.stroke_width = stroke_width + self.stroke_opacity = stroke_opacity # TODO: remove self.should_make_jagged = should_make_jagged self.pre_function_handle_to_anchor_scale_factor = ( pre_function_handle_to_anchor_scale_factor diff --git a/manim/renderer/opengl_renderer.py b/manim/renderer/opengl_renderer.py index 5decf8d859..eef8e12290 100644 --- a/manim/renderer/opengl_renderer.py +++ b/manim/renderer/opengl_renderer.py @@ -256,10 +256,10 @@ def use_window(self) -> None: # TODO this should also be done with the update decorators because if the camera doesn't change this is pretty rough def init_camera(self, camera: Camera): camera_data = { - "frame_shape": (config.frame_width, config.frame_height), + "frame_shape": camera.get_frame_shape(), "camera_center": camera.get_center(), - "camera_rotation": camera.get_inverse_camera_rotation_matrix().T, - "focal_distance": camera.get_focal_distance(), + "camera_rotation": camera.get_rotation_matrix(), + "focal_distance": camera.focal_distance, } ubo_camera.write(camera_data) ubo_camera.bind() diff --git a/manim/renderer/renderer.py b/manim/renderer/renderer.py index f9464daa91..f94bf85971 100644 --- a/manim/renderer/renderer.py +++ b/manim/renderer/renderer.py @@ -4,6 +4,7 @@ from typing import TYPE_CHECKING, Protocol, runtime_checkable from manim._config import logger +from manim.mobject.opengl.opengl_mobject import OpenGLMobject from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject from manim.mobject.types.image_mobject import ImageMobject @@ -28,6 +29,7 @@ def __init__(self): self.capabilities = [ (OpenGLVMobject, self.render_vmobject), (ImageMobject, self.render_image), + (OpenGLMobject, lambda mob: None), ] def render(self, state: SceneState) -> None: diff --git a/manim/scene/three_d_scene.py b/manim/scene/three_d_scene.py index 4a4b19bb88..c897c36f0d 100644 --- a/manim/scene/three_d_scene.py +++ b/manim/scene/three_d_scene.py @@ -5,311 +5,206 @@ __all__ = ["ThreeDScene", "SpecialThreeDScene"] -import warnings from collections.abc import Iterable, Sequence +from typing import TYPE_CHECKING, Any -import numpy as np - +from manim._config import config +from manim.animation.animation import Animation 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.opengl.opengl_vectorized_mobject import ( + OpenGLVectorizedPoint, + OpenGLVGroup, +) from manim.mobject.three_d.three_dimensions import Sphere -from manim.mobject.value_tracker import ValueTracker +from manim.scene.scene import Scene +from manim.utils.config_ops import merge_dicts_recursively +from manim.utils.deprecation import deprecated -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 +if TYPE_CHECKING: + pass class ThreeDScene(Scene): + """A :class:`Scene` with special configurations and properties that make it + suitable for 3D scenes. """ - 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) + @deprecated( + replacement="Camera.set_orientation", + message="Use self.camera.set_orientation() instead.", + ) def set_camera_orientation( self, - phi: float | None = None, theta: float | None = None, + phi: 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. + frame_center: OpenGLMobject | Sequence[float] | None = None, + ) -> None: + """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. - + phi + The polar angle i.e the angle between Z_AXIS and Camera through ORIGIN in radians. gamma The rotation of the camera about the vector from the ORIGIN to the Camera. - zoom The zoom factor of the scene. - + focal_distance + The focal_distance of the Camera. 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 + self.camera.set_orientation( + theta, phi, gamma, zoom, focal_distance, frame_center + ) + + @deprecated( + replacement="Camera.begin_ambient_rotation", + message="Use self.camera.begin_ambient_rotation() followed by self.add(self.camera) instead.", + ) + def begin_ambient_camera_rotation( + self, rate: float = 0.02, about: str = "theta" + ) -> None: + """Apply an updater to rotate the camera on every frame by modifying + one of three Euler angles: "theta" (rotate about the Z axis), "phi" + (modify the angle between the camera and the Z axis) or "gamma" (rotate + the camera in its position while it's looking at the same point). Parameters ---------- rate - The rate at which the camera should rotate about the Z_AXIS. - Negative rate means clockwise rotation. + The rate at which the camera should rotate for the specified + angle. A positive rate means counterclockwise rotation, and a + negative rate means clockwise rotation. about - one of 3 options: ["theta", "phi", "gamma"]. defaults to theta. + The Euler angle to modify, which can be "theta", "phi" or "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 as e: - raise ValueError("Invalid ambient rotation angle.") from e - - 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 as e: - raise ValueError("Invalid ambient rotation angle.") from e + self.camera.begin_ambient_rotation(rate, about) + self.add(self.camera) + + @deprecated( + replacement="Camera.stop_ambient_rotation", + message="Use self.camera.stop_ambient_rotation() instead.", + ) + def stop_ambient_camera_rotation(self, about: str = "theta") -> None: + """Stop ambient camera rotation on the specified angle. If there's a + corresponding ambient rotation updater applied on the camera, remove + it. - def begin_3dillusion_camera_rotation( + Parameters + ---------- + about + The Euler angle for which the rotation should stop. This angle can + be "theta", "phi" or "gamma". Defaults to "theta". + """ + self.camera.stop_ambient_rotation(about) + + @deprecated( + replacement="Camera.begin_precession", + message="Use self.camera.begin_precession() followed by self.add(self.camera) instead.", + ) + def begin_camera_precession( self, - rate: float = 1, + rate: float = 1.0, + radius: float = 0.2, origin_phi: float | None = None, origin_theta: float | None = None, - ): - """ - This method creates a 3D camera rotation illusion around - the current camera orientation. + ) -> None: + """Begin a camera precession by adding an updater. This precession + consists of moving around the point given by ``origin_phi`` and + ``origin_theta``, keeping the ``gamma`` Euler angle constant. Parameters ---------- rate - The rate at which the camera rotation illusion should operate. + The rate at which the camera precession should operate. + radius + The precession radius. origin_phi - The polar angle the camera should move around. Defaults - to the current phi angle. + The polar angle the camera should move around. If ``None``, + defaults to the current ``phi`` angle. origin_theta - The azimutal angle the camera should move around. Defaults - to the current theta angle. + The azimutal angle the camera should move around. If ``None``, + 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) - + self.camera.begin_precession(rate, radius, origin_phi, origin_theta) + self.add(self.camera) + + @deprecated( + replacement="Camera.stop_precession", + message="Use self.camera.stop_precession() instead.", + ) + def stop_camera_precession(self): + """Remove the precession camera updater, if any.""" + self.camera.stop_precession() + + @deprecated( + replacement="Camera.begin_precession", + message="Use self.camera.begin_precession() followed by self.add(self.camera) instead.", + ) + def begin_3dillusion_camera_rotation( + self, + rate: float = 1.0, + radius: float = 0.2, + origin_phi: float | None = None, + origin_theta: float | None = None, + ) -> None: + """Alias of :meth:`begin_camera_precession`.""" + self.begin_camera_precession(rate, radius, origin_phi, origin_theta) + + @deprecated( + replacement="Camera.stop_precession", + message="Use self.camera.stop_precession() instead.", + ) 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) + """Alias of :meth:`stop_camera_precession`.""" + self.stop_camera_precession() def move_camera( self, - phi: float | None = None, theta: float | None = None, + phi: float | None = None, gamma: float | None = None, zoom: float | None = None, focal_distance: float | None = None, - frame_center: Mobject | Sequence[float] | None = None, + frame_center: OpenGLMobject | Sequence[float] | None = None, added_anims: Iterable[Animation] = [], - **kwargs, - ): - """ - This method animates the movement of the camera - to the given spherical coordinates. + **kwargs: Any, + ) -> None: + """Animate 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. - + phi + The polar angle i.e the angle between Z_AXIS and Camera through ORIGIN in radians. gamma The rotation of the camera about the vector from the ORIGIN to the Camera. - + focal_distance + The radial focal_distance between ORIGIN and 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): + animation = self.camera.animate.set_orientation( + theta, phi, gamma, zoom, focal_distance, frame_center + ) + + self.play(animation, *added_anims, **kwargs) + + def get_moving_mobjects(self, *animations: Animation) -> list[OpenGLMobject]: """ This method returns a list of all of the Mobjects in the Scene that are moving, that are also in the animations passed. @@ -320,14 +215,12 @@ def get_moving_mobjects(self, *animations: Animation): 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, - ] + camera_mobjects = self.camera.get_value_trackers() 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): + def add_fixed_orientation_mobjects(self, *mobjects: OpenGLMobject) -> None: """ This method is used to prevent the rotation and tilting of mobjects as the camera moves around. The mobject can @@ -345,16 +238,11 @@ def add_fixed_orientation_mobjects(self, *mobjects: Mobject, **kwargs): 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): + for mob in mobjects: + mob.fix_orientation() + self.add(mob) + + def add_fixed_in_frame_mobjects(self, *mobjects: OpenGLMobject) -> None: """ This method is used to prevent the rotation and movement of mobjects as the camera moves around. The mobject is @@ -366,17 +254,11 @@ def add_fixed_in_frame_mobjects(self, *mobjects: Mobject): *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): + for mob in mobjects: + mob.fix_in_frame() + self.add(mob) + + def remove_fixed_orientation_mobjects(self, *mobjects: OpenGLMobject) -> None: """ This method "unfixes" the orientation of the mobjects passed, meaning they will no longer be at the same angle @@ -388,15 +270,11 @@ def remove_fixed_orientation_mobjects(self, *mobjects: Mobject): *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): + for mob in mobjects: + mob.unfix_orientation() + self.remove(mob) + + def remove_fixed_in_frame_mobjects(self, *mobjects: OpenGLMobject) -> None: """ This method undoes what add_fixed_in_frame_mobjects does. It allows the mobject to be affected by the movement of @@ -407,31 +285,9 @@ def remove_fixed_in_frame_mobjects(self, *mobjects: Mobject): *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) + for mob in mobjects: + mob.unfix_from_frame() + self.remove(mob) class SpecialThreeDScene(ThreeDScene): @@ -449,9 +305,12 @@ class SpecialThreeDScene(ThreeDScene): def __init__( self, - cut_axes_at_radius=True, - camera_config={"should_apply_shading": True, "exponential_projection": True}, - three_d_axes_config={ + cut_axes_at_radius: bool = True, + camera_config: dict = { + "should_apply_shading": True, + "exponential_projection": True, + }, + three_d_axes_config: dict = { "num_axis_pieces": 1, "axis_config": { "unit_size": 2, @@ -460,34 +319,29 @@ def __init__( "stroke_width": 2, }, }, - sphere_config={"radius": 2, "resolution": (24, 48)}, - default_angled_camera_position={ - "phi": 70 * DEGREES, - "theta": -110 * DEGREES, - }, + sphere_config: dict = {"radius": 2, "resolution": (24, 48)}, # When scene is extracted with -l flag, this # configuration will override the above configuration. - low_quality_config={ + low_quality_config: dict = { "camera_config": {"should_apply_shading": False}, "three_d_axes_config": {"num_axis_pieces": 1}, "sphere_config": {"resolution": (12, 24)}, }, - **kwargs, - ): + **kwargs: Any, + ) -> None: 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"]: + if self.manager.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): + def get_axes(self) -> ThreeDAxes: """Return a set of 3D axes. Returns @@ -502,17 +356,17 @@ def get_axes(self): 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)) + new_pieces = OpenGLVGroup(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 axis.pieces.note_changed_family() for tick in axis.tick_marks: - tick.add(VectorizedPoint(1.5 * tick.get_center())) + tick.add(OpenGLVectorizedPoint(1.5 * tick.get_center())) return axes - def get_sphere(self, **kwargs): + def get_sphere(self, **kwargs: Any) -> Sphere: """ Returns a sphere with the passed keyword arguments as properties. @@ -528,18 +382,3 @@ def get_sphere(self, **kwargs): """ 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) From ae04878b9e25daac461c7c26e48a3c47b9c231ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francisco=20Manr=C3=ADquez?= Date: Sun, 15 Dec 2024 20:17:38 -0300 Subject: [PATCH 02/11] Log warning when using Camera.rotate() and add docstrings to Euler angle methods --- manim/camera/camera.py | 153 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 147 insertions(+), 6 deletions(-) diff --git a/manim/camera/camera.py b/manim/camera/camera.py index b249eaf424..bada1b07aa 100644 --- a/manim/camera/camera.py +++ b/manim/camera/camera.py @@ -6,17 +6,19 @@ from typing import TYPE_CHECKING, Any, TypedDict import numpy as np +import numpy.typing as npt -from manim._config import config +from manim._config import config, logger from manim.constants import * from manim.mobject.opengl.opengl_mobject import OpenGLMobject +from manim.utils.deprecation import deprecated from manim.utils.paths import straight_path from manim.utils.space_ops import rotation_matrix if TYPE_CHECKING: from typing_extensions import Self - from manim.typing import MatrixMN, Point3D, Vector3D + from manim.typing import ManimFloat, MatrixMN, Point3D, Vector3D class CameraOrientationConfig(TypedDict, total=False): @@ -123,7 +125,7 @@ def set_orientation( self.move_to(frame_center) return self - def get_euler_angles(self) -> np.ndarray: + def get_euler_angles(self) -> npt.NDArray[ManimFloat]: return np.array([self._theta, self._phi, self._gamma]) def set_euler_angles( @@ -141,9 +143,30 @@ def set_euler_angles( return self def get_theta(self) -> float: + """Get the angle theta along which the camera is rotated about the Z + axis. + + Returns + ------- + float + The theta angle. + """ return self._theta def set_theta(self, theta: float) -> Self: + """Set the angle theta by which the camera is rotated about the Z + axis. + + Parameters + ---------- + theta + The new theta angle. + + Returns + ------- + :class:`Camera` + The camera after setting its theta angle. + """ self._theta = theta self._rotation_matrix = None # If we don't add TAU/4 (90°) to theta, the camera will be positioned @@ -160,12 +183,44 @@ def set_theta(self, theta: float) -> Self: return self def increment_theta(self, dtheta: float) -> Self: + """Incremeet the angle theta by which the camera is rotated about the Z + axis, by a given ``dtheta``. + + Parameters + ---------- + dtheta + The increment in the angle theta. + + Returns + ------- + :class:`Camera` + The camera after incrementing its theta angle. + """ return self.set_theta(self._theta + dtheta) def get_phi(self) -> float: + """Get the angle phi between the camera and the Z axis. + + Returns + ------- + float + The phi angle. + """ return self._phi def set_phi(self, phi: float) -> Self: + """Set the angle phi between the camera and the Z axis. + + Parameters + ---------- + phi + The new phi angle. + + Returns + ------- + :class:`Camera` + The camera after setting its phi angle. + """ self._phi = phi self._rotation_matrix = None cos = np.cos(phi) @@ -180,12 +235,46 @@ def set_phi(self, phi: float) -> Self: return self def increment_phi(self, dphi: float) -> Self: + """Increment the angle phi between the camera and the Z axis by a given + ``dphi``. + + Parameters + ---------- + dphi + The increment in the angle phi. + + Returns + ------- + :class:`Camera` + The camera after incrementing its phi angle. + """ return self.set_phi(self._phi + dgamma) def get_gamma(self) -> float: + """Get the angle gamma by which the camera is rotated while standing on + its current position. + + Returns + ------- + float + The gamma angle. + """ return self._gamma def set_gamma(self, gamma: float) -> Self: + """Set the angle gamma by which the camera is rotated while standing on + its current position. + + Parameters + ---------- + gamma + The new gamma angle. + + Returns + ------- + :class:`Camera` + The camera after setting its gamma angle. + """ self._gamma = gamma self._rotation_matrix = None cos = np.cos(gamma) @@ -200,6 +289,19 @@ def set_gamma(self, gamma: float) -> Self: return self def increment_gamma(self, dgamma: float) -> Self: + """Increment the angle gamma by which the camera is rotated while + standing on its current position, by an angle ``dgamma``. + + Parameters + ---------- + dgamma + The increment in the angle gamma. + + Returns + ------- + :class:`Camera` + The camera after incrementing its gamma angle. + """ return self.set_gamma(self._gamma + dgamma) def get_rotation_matrix(self) -> MatrixMN: @@ -323,6 +425,15 @@ def rotate(self, angle: float, axis: Vector3D = OUT, **kwargs: Any) -> Self: :class:`Camera` The camera after the rotation. """ + logger.warning( + "Using this method automatically standardizes the Euler angles " + "theta, phi and gamma, which might result in unexpected behavior " + "when animating the camera. If phi is 0° or 180°, this method " + "is not able to determine exactly theta and gamma, because their " + "axes are aligned. Therefore, in that case, gamma will be set to " + "0°." + ) + new_rot = rotation_matrix(angle, axis) @ self.get_rotation_matrix() # Recalculate theta, phi and gamma. @@ -383,10 +494,32 @@ def get_height(self) -> float: points = self.points return points[4, 1] - points[3, 1] + def get_implied_camera_direction(self) -> Vector3D: + """Use the rotation matrix given by the Euler angles theta, phi and + gamma to calculate the direction along which the camera would be + positioned if it had a physical position. + + Returns + ------- + :class:`Vector3D` + The direction along which the camera would be positioned if it had + a physical position. + """ + return self.get_rotation_matrix()[:, 2] + def get_implied_camera_location(self) -> Point3D: - to_camera = self.get_rotation_matrix()[:, 2] - dist = self.get_focal_distance() - return self.get_center() + dist * to_camera + """Use the Euler angles theta, phi and gamma, as well as the frame + center and the focal distance, to calculate the point in which the + camera would be positioned if it had a physical position. + + Returns + ------- + :class:`Point3D` + The point in which the camera would be positioned if it had a + physical position. + """ + to_camera = self.get_implied_camera_direction() + return self.get_center() + self.focal_distance * to_camera # Movement methods @@ -521,6 +654,10 @@ def stop_precession(self) -> Self: self.precession_updater = None return self + @deprecated( + replacement="Camera.begin_precession", + message="Use Camera.begin_precession() instead.", + ) def begin_3dillusion_rotation( self, rate: float = 1.0, @@ -531,6 +668,10 @@ def begin_3dillusion_rotation( """Alias for :meth:`begin_precession`, which is preferred.""" return self.begin_precession(rate, radius, origin_theta, origin_phi) + @deprecated( + replacement="Camera.stop_precession", + message="Use Camera.stop_precession() instead.", + ) def stop_3dillusion_rotation(self) -> Self: """Alias for :meth:`stop_precession`, which is preferred.""" return self.stop_precession() From 5bfa6dcfb6eb484d245a8e6665942a7c2870380c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francisco=20Manr=C3=ADquez?= Date: Sun, 15 Dec 2024 20:43:31 -0300 Subject: [PATCH 03/11] Add Camera.set_focal_distance() --- manim/camera/camera.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/manim/camera/camera.py b/manim/camera/camera.py index bada1b07aa..fa49d151c8 100644 --- a/manim/camera/camera.py +++ b/manim/camera/camera.py @@ -329,6 +329,10 @@ def get_rotation_matrix(self) -> MatrixMN: def get_inverse_rotation_matrix(self) -> MatrixMN: return self.get_rotation_matrix().T + def set_focal_distance(self, focal_distance: float) -> Self: + self.focal_distance = focal_distance + return self + # TODO: rotate is still unreliable. The Euler angles are automatically # standardized to (-TAU/2, TAU/2), leading to potentially unwanted behavior # when animating. Plus, if the camera is on the Z axis, which occurs when From 7b5a1bd36b9ab77c4310d5d6fcc090978ded7d6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francisco=20Manr=C3=ADquez?= Date: Sun, 15 Dec 2024 21:38:29 -0300 Subject: [PATCH 04/11] Delete ThreeDScene --- manim/__init__.py | 1 - manim/camera/camera.py | 23 -- manim/mobject/graphing/coordinate_systems.py | 16 +- manim/mobject/graphing/functions.py | 4 +- manim/mobject/three_d/polyhedra.py | 28 +- manim/mobject/three_d/three_dimensions.py | 56 +-- manim/scene/three_d_scene.py | 384 ------------------ manim/utils/testing/frames_comparison.py | 2 +- tests/module/scene/test_threed_scene.py | 18 - tests/test_graphical_units/test_axes.py | 2 +- .../test_coordinate_systems.py | 4 +- tests/test_graphical_units/test_mobjects.py | 2 +- 12 files changed, 58 insertions(+), 482 deletions(-) delete mode 100644 manim/scene/three_d_scene.py delete mode 100644 tests/module/scene/test_threed_scene.py diff --git a/manim/__init__.py b/manim/__init__.py index 1ea06d1638..64d315c370 100644 --- a/manim/__init__.py +++ b/manim/__init__.py @@ -74,7 +74,6 @@ from manim.mobject.vector_field import * from manim.scene.scene import * from manim.scene.sections import * -from manim.scene.three_d_scene import * from manim.scene.vector_space_scene import * from manim.utils import color, rate_functions, unit from manim.utils.bezier import * diff --git a/manim/camera/camera.py b/manim/camera/camera.py index fa49d151c8..c700e457a8 100644 --- a/manim/camera/camera.py +++ b/manim/camera/camera.py @@ -11,7 +11,6 @@ from manim._config import config, logger from manim.constants import * from manim.mobject.opengl.opengl_mobject import OpenGLMobject -from manim.utils.deprecation import deprecated from manim.utils.paths import straight_path from manim.utils.space_ops import rotation_matrix @@ -657,25 +656,3 @@ def stop_precession(self) -> Self: self.remove_updater(updater) self.precession_updater = None return self - - @deprecated( - replacement="Camera.begin_precession", - message="Use Camera.begin_precession() instead.", - ) - def begin_3dillusion_rotation( - self, - rate: float = 1.0, - radius: float = 0.2, - origin_theta: float | None = None, - origin_phi: float | None = None, - ) -> Self: - """Alias for :meth:`begin_precession`, which is preferred.""" - return self.begin_precession(rate, radius, origin_theta, origin_phi) - - @deprecated( - replacement="Camera.stop_precession", - message="Use Camera.stop_precession() instead.", - ) - def stop_3dillusion_rotation(self) -> Self: - """Alias for :meth:`stop_precession`, which is preferred.""" - return self.stop_precession() diff --git a/manim/mobject/graphing/coordinate_systems.py b/manim/mobject/graphing/coordinate_systems.py index 0dc7d6c4d6..0cd20c7194 100644 --- a/manim/mobject/graphing/coordinate_systems.py +++ b/manim/mobject/graphing/coordinate_systems.py @@ -954,10 +954,10 @@ def plot_surface( .. manim:: PlotSurfaceExample :save_last_frame: - class PlotSurfaceExample(ThreeDScene): + class PlotSurfaceExample(Scene): def construct(self): resolution_fa = 16 - self.set_camera_orientation(phi=75 * DEGREES, theta=-60 * DEGREES) + self.camera.set_orientation(phi=75 * DEGREES, theta=-60 * DEGREES) axes = ThreeDAxes(x_range=(-3, 3, 1), y_range=(-3, 3, 1), z_range=(-5, 5, 1)) def param_trig(u, v): x = u @@ -2519,11 +2519,11 @@ def get_y_axis_label( .. manim:: GetYAxisLabelExample :save_last_frame: - class GetYAxisLabelExample(ThreeDScene): + class GetYAxisLabelExample(Scene): def construct(self): ax = ThreeDAxes() lab = ax.get_y_axis_label(Tex("$y$-label")) - self.set_camera_orientation(phi=2*PI/5, theta=PI/5) + self.camera.set_orientation(phi=2*PI/5, theta=PI/5) self.add(ax, lab) """ positioned_label = self._get_axis_label( @@ -2569,11 +2569,11 @@ def get_z_axis_label( .. manim:: GetZAxisLabelExample :save_last_frame: - class GetZAxisLabelExample(ThreeDScene): + class GetZAxisLabelExample(Scene): def construct(self): ax = ThreeDAxes() lab = ax.get_z_axis_label(Tex("$z$-label")) - self.set_camera_orientation(phi=2*PI/5, theta=PI/5) + self.camera.set_orientation(phi=2*PI/5, theta=PI/5) self.add(ax, lab) """ positioned_label = self._get_axis_label( @@ -2620,9 +2620,9 @@ def get_axis_labels( .. manim:: GetAxisLabelsExample :save_last_frame: - class GetAxisLabelsExample(ThreeDScene): + class GetAxisLabelsExample(Scene): def construct(self): - self.set_camera_orientation(phi=2*PI/5, theta=PI/5) + self.camera.set_orientation(phi=2*PI/5, theta=PI/5) axes = ThreeDAxes() labels = axes.get_axis_labels( Text("x-axis").scale(0.7), Text("y-axis").scale(0.45), Text("z-axis").scale(0.45) diff --git a/manim/mobject/graphing/functions.py b/manim/mobject/graphing/functions.py index 1cd660b894..4c16718876 100644 --- a/manim/mobject/graphing/functions.py +++ b/manim/mobject/graphing/functions.py @@ -65,7 +65,7 @@ def construct(self): .. manim:: ThreeDParametricSpring :save_last_frame: - class ThreeDParametricSpring(ThreeDScene): + class ThreeDParametricSpring(Scene): def construct(self): curve1 = ParametricFunction( lambda u: ( @@ -76,7 +76,7 @@ def construct(self): ).set_shade_in_3d(True) axes = ThreeDAxes() self.add(axes, curve1) - self.set_camera_orientation(phi=80 * DEGREES, theta=-60 * DEGREES) + self.camera.set_orientation(phi=80 * DEGREES, theta=-60 * DEGREES) self.wait() .. attention:: diff --git a/manim/mobject/three_d/polyhedra.py b/manim/mobject/three_d/polyhedra.py index 8046f6066c..3b3f054845 100644 --- a/manim/mobject/three_d/polyhedra.py +++ b/manim/mobject/three_d/polyhedra.py @@ -51,9 +51,9 @@ class Polyhedron(VGroup): .. manim:: SquarePyramidScene :save_last_frame: - class SquarePyramidScene(ThreeDScene): + class SquarePyramidScene(Scene): def construct(self): - self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES) + self.camera.set_orientation(phi=75 * DEGREES, theta=30 * DEGREES) vertex_coords = [ [1, 1, 0], [1, -1, 0], @@ -85,9 +85,9 @@ def construct(self): .. manim:: PolyhedronSubMobjects :save_last_frame: - class PolyhedronSubMobjects(ThreeDScene): + class PolyhedronSubMobjects(Scene): def construct(self): - self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES) + self.camera.set_orientation(phi=75 * DEGREES, theta=30 * DEGREES) octahedron = Octahedron(edge_length = 3) octahedron.graph[0].set_color(RED) octahedron.faces[2].set_color(YELLOW) @@ -174,9 +174,9 @@ class Tetrahedron(Polyhedron): .. manim:: TetrahedronScene :save_last_frame: - class TetrahedronScene(ThreeDScene): + class TetrahedronScene(Scene): def construct(self): - self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES) + self.camera.set_orientation(phi=75 * DEGREES, theta=30 * DEGREES) obj = Tetrahedron() self.add(obj) """ @@ -209,9 +209,9 @@ class Octahedron(Polyhedron): .. manim:: OctahedronScene :save_last_frame: - class OctahedronScene(ThreeDScene): + class OctahedronScene(Scene): def construct(self): - self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES) + self.camera.set_orientation(phi=75 * DEGREES, theta=30 * DEGREES) obj = Octahedron() self.add(obj) """ @@ -255,9 +255,9 @@ class Icosahedron(Polyhedron): .. manim:: IcosahedronScene :save_last_frame: - class IcosahedronScene(ThreeDScene): + class IcosahedronScene(Scene): def construct(self): - self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES) + self.camera.set_orientation(phi=75 * DEGREES, theta=30 * DEGREES) obj = Icosahedron() self.add(obj) """ @@ -320,9 +320,9 @@ class Dodecahedron(Polyhedron): .. manim:: DodecahedronScene :save_last_frame: - class DodecahedronScene(ThreeDScene): + class DodecahedronScene(Scene): def construct(self): - self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES) + self.camera.set_orientation(phi=75 * DEGREES, theta=30 * DEGREES) obj = Dodecahedron() self.add(obj) """ @@ -390,9 +390,9 @@ class ConvexHull3D(Polyhedron): :save_last_frame: :quality: high - class ConvexHull3DExample(ThreeDScene): + class ConvexHull3DExample(Scene): def construct(self): - self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES) + self.camera.set_orientation(phi=75 * DEGREES, theta=30 * DEGREES) points = [ [ 1.93192757, 0.44134585, -1.52407061], [-0.93302521, 1.23206983, 0.64117067], diff --git a/manim/mobject/three_d/three_dimensions.py b/manim/mobject/three_d/three_dimensions.py index 014b4b3b8e..6684ea1967 100644 --- a/manim/mobject/three_d/three_dimensions.py +++ b/manim/mobject/three_d/three_dimensions.py @@ -81,7 +81,7 @@ class Surface(VGroup): .. manim:: ParaSurface :save_last_frame: - class ParaSurface(ThreeDScene): + class ParaSurface(Scene): def func(self, u, v): return np.array([np.cos(u) * np.cos(v), np.cos(u) * np.sin(v), u]) @@ -93,7 +93,7 @@ def construct(self): v_range=[0, TAU], resolution=8, ) - self.set_camera_orientation(theta=70 * DEGREES, phi=75 * DEGREES) + self.camera.set_orientation(theta=70 * DEGREES, phi=75 * DEGREES) self.add(axes, surface) """ @@ -246,16 +246,18 @@ def set_fill_by_value( .. manim:: FillByValueExample :save_last_frame: - class FillByValueExample(ThreeDScene): + class FillByValueExample(Scene): def construct(self): resolution_fa = 8 - self.set_camera_orientation(phi=75 * DEGREES, theta=-160 * DEGREES) + self.camera.set_orientation(phi=75 * DEGREES, theta=-160 * DEGREES) axes = ThreeDAxes(x_range=(0, 5, 1), y_range=(0, 5, 1), z_range=(-1, 1, 0.5)) + def param_surface(u, v): x = u y = v z = np.sin(x) * np.cos(y) return z + surface_plane = Surface( lambda u, v: axes.c2p(u, v, param_surface(u, v)), resolution=(resolution_fa, resolution_fa), @@ -349,9 +351,9 @@ class Sphere(Surface): .. manim:: ExampleSphere :save_last_frame: - class ExampleSphere(ThreeDScene): + class ExampleSphere(Scene): def construct(self): - self.set_camera_orientation(phi=PI / 6, theta=PI / 6) + self.camera.set_orientation(phi=PI / 6, theta=PI / 6) sphere1 = Sphere( center=(3, 0, 0), radius=1, @@ -433,9 +435,9 @@ class Dot3D(Sphere): .. manim:: Dot3DExample :save_last_frame: - class Dot3DExample(ThreeDScene): + class Dot3DExample(Scene): def construct(self): - self.set_camera_orientation(phi=75*DEGREES, theta=-45*DEGREES) + self.camera.set_orientation(phi=75*DEGREES, theta=-45*DEGREES) axes = ThreeDAxes() dot_1 = Dot3D(point=axes.coords_to_point(0, 0, 1), color=RED) @@ -477,9 +479,9 @@ class Cube(VGroup): .. manim:: CubeExample :save_last_frame: - class CubeExample(ThreeDScene): + class CubeExample(Scene): def construct(self): - self.set_camera_orientation(phi=75*DEGREES, theta=-45*DEGREES) + self.camera.set_orientation(phi=75*DEGREES, theta=-45*DEGREES) axes = ThreeDAxes() cube = Cube(side_length=3, fill_opacity=0.7, fill_color=BLUE) @@ -533,9 +535,9 @@ class Prism(Cube): .. manim:: ExamplePrism :save_last_frame: - class ExamplePrism(ThreeDScene): + class ExamplePrism(Scene): def construct(self): - self.set_camera_orientation(phi=60 * DEGREES, theta=150 * DEGREES) + self.camera.set_orientation(phi=60 * DEGREES, theta=150 * DEGREES) prismSmall = Prism(dimensions=[1, 2, 3]).rotate(PI / 2) prismLarge = Prism(dimensions=[1.5, 3, 4.5]).move_to([2, 0, 0]) self.add(prismSmall, prismLarge) @@ -584,11 +586,11 @@ class Cone(Surface): .. manim:: ExampleCone :save_last_frame: - class ExampleCone(ThreeDScene): + class ExampleCone(Scene): def construct(self): axes = ThreeDAxes() cone = Cone(direction=X_AXIS+Y_AXIS+2*Z_AXIS, resolution=8) - self.set_camera_orientation(phi=5*PI/11, theta=PI/9) + self.camera.set_orientation(phi=5*PI/11, theta=PI/9) self.add(axes, cone) """ @@ -745,11 +747,11 @@ class Cylinder(Surface): .. manim:: ExampleCylinder :save_last_frame: - class ExampleCylinder(ThreeDScene): + class ExampleCylinder(Scene): def construct(self): axes = ThreeDAxes() cylinder = Cylinder(radius=2, height=3) - self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES) + self.camera.set_orientation(phi=75 * DEGREES, theta=30 * DEGREES) self.add(axes, cylinder) """ @@ -882,7 +884,7 @@ def get_direction(self) -> np.ndarray: class Line3D(Cylinder): - """A cylindrical line, for use in ThreeDScene. + """A cylindrical line. Parameters ---------- @@ -906,11 +908,11 @@ class Line3D(Cylinder): .. manim:: ExampleLine3D :save_last_frame: - class ExampleLine3D(ThreeDScene): + class ExampleLine3D(Scene): def construct(self): axes = ThreeDAxes() line = Line3D(start=np.array([0, 0, 0]), end=np.array([2, 2, 2])) - self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES) + self.camera.set_orientation(phi=75 * DEGREES, theta=30 * DEGREES) self.add(axes, line) """ @@ -1042,9 +1044,9 @@ def parallel_to( .. manim:: ParallelLineExample :save_last_frame: - class ParallelLineExample(ThreeDScene): + class ParallelLineExample(Scene): def construct(self): - self.set_camera_orientation(PI / 3, -PI / 4) + self.camera.set_orientation(PI / 3, -PI / 4) ax = ThreeDAxes((-5, 5), (-5, 5), (-5, 5), 10, 10, 10) line1 = Line3D(RIGHT * 2, UP + OUT, color=RED) line2 = Line3D.parallel_to(line1, color=YELLOW) @@ -1090,9 +1092,9 @@ def perpendicular_to( .. manim:: PerpLineExample :save_last_frame: - class PerpLineExample(ThreeDScene): + class PerpLineExample(Scene): def construct(self): - self.set_camera_orientation(PI / 3, -PI / 4) + self.camera.set_orientation(PI / 3, -PI / 4) ax = ThreeDAxes((-5, 5), (-5, 5), (-5, 5), 10, 10, 10) line1 = Line3D(RIGHT * 2, UP + OUT, color=RED) line2 = Line3D.perpendicular_to(line1, color=BLUE) @@ -1138,7 +1140,7 @@ class Arrow3D(Line3D): .. manim:: ExampleArrow3D :save_last_frame: - class ExampleArrow3D(ThreeDScene): + class ExampleArrow3D(Scene): def construct(self): axes = ThreeDAxes() arrow = Arrow3D( @@ -1146,7 +1148,7 @@ def construct(self): end=np.array([2, 2, 2]), resolution=8 ) - self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES) + self.camera.set_orientation(phi=75 * DEGREES, theta=30 * DEGREES) self.add(axes, arrow) """ @@ -1213,11 +1215,11 @@ class Torus(Surface): .. manim :: ExampleTorus :save_last_frame: - class ExampleTorus(ThreeDScene): + class ExampleTorus(Scene): def construct(self): axes = ThreeDAxes() torus = Torus() - self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES) + self.camera.set_orientation(phi=75 * DEGREES, theta=30 * DEGREES) self.add(axes, torus) """ diff --git a/manim/scene/three_d_scene.py b/manim/scene/three_d_scene.py deleted file mode 100644 index c897c36f0d..0000000000 --- a/manim/scene/three_d_scene.py +++ /dev/null @@ -1,384 +0,0 @@ -"""A scene suitable for rendering three-dimensional objects and animations.""" - -from __future__ import annotations - -__all__ = ["ThreeDScene", "SpecialThreeDScene"] - - -from collections.abc import Iterable, Sequence -from typing import TYPE_CHECKING, Any - -from manim._config import config -from manim.animation.animation import Animation -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.opengl.opengl_vectorized_mobject import ( - OpenGLVectorizedPoint, - OpenGLVGroup, -) -from manim.mobject.three_d.three_dimensions import Sphere -from manim.scene.scene import Scene -from manim.utils.config_ops import merge_dicts_recursively -from manim.utils.deprecation import deprecated - -if TYPE_CHECKING: - pass - - -class ThreeDScene(Scene): - """A :class:`Scene` with special configurations and properties that make it - suitable for 3D scenes. - """ - - @deprecated( - replacement="Camera.set_orientation", - message="Use self.camera.set_orientation() instead.", - ) - def set_camera_orientation( - self, - theta: float | None = None, - phi: float | None = None, - gamma: float | None = None, - zoom: float | None = None, - focal_distance: float | None = None, - frame_center: OpenGLMobject | Sequence[float] | None = None, - ) -> None: - """This method sets the orientation of the camera in the scene. - - Parameters - ---------- - theta - The azimuthal angle i.e the angle that spins the camera around the Z_AXIS. - phi - The polar angle i.e the angle between Z_AXIS and Camera through ORIGIN in radians. - gamma - The rotation of the camera about the vector from the ORIGIN to the Camera. - zoom - The zoom factor of the scene. - focal_distance - The focal_distance of the Camera. - frame_center - The new center of the camera frame in cartesian coordinates. - """ - self.camera.set_orientation( - theta, phi, gamma, zoom, focal_distance, frame_center - ) - - @deprecated( - replacement="Camera.begin_ambient_rotation", - message="Use self.camera.begin_ambient_rotation() followed by self.add(self.camera) instead.", - ) - def begin_ambient_camera_rotation( - self, rate: float = 0.02, about: str = "theta" - ) -> None: - """Apply an updater to rotate the camera on every frame by modifying - one of three Euler angles: "theta" (rotate about the Z axis), "phi" - (modify the angle between the camera and the Z axis) or "gamma" (rotate - the camera in its position while it's looking at the same point). - - Parameters - ---------- - rate - The rate at which the camera should rotate for the specified - angle. A positive rate means counterclockwise rotation, and a - negative rate means clockwise rotation. - about - The Euler angle to modify, which can be "theta", "phi" or "gamma". - Defaults to "theta". - """ - self.camera.begin_ambient_rotation(rate, about) - self.add(self.camera) - - @deprecated( - replacement="Camera.stop_ambient_rotation", - message="Use self.camera.stop_ambient_rotation() instead.", - ) - def stop_ambient_camera_rotation(self, about: str = "theta") -> None: - """Stop ambient camera rotation on the specified angle. If there's a - corresponding ambient rotation updater applied on the camera, remove - it. - - Parameters - ---------- - about - The Euler angle for which the rotation should stop. This angle can - be "theta", "phi" or "gamma". Defaults to "theta". - """ - self.camera.stop_ambient_rotation(about) - - @deprecated( - replacement="Camera.begin_precession", - message="Use self.camera.begin_precession() followed by self.add(self.camera) instead.", - ) - def begin_camera_precession( - self, - rate: float = 1.0, - radius: float = 0.2, - origin_phi: float | None = None, - origin_theta: float | None = None, - ) -> None: - """Begin a camera precession by adding an updater. This precession - consists of moving around the point given by ``origin_phi`` and - ``origin_theta``, keeping the ``gamma`` Euler angle constant. - - Parameters - ---------- - rate - The rate at which the camera precession should operate. - radius - The precession radius. - origin_phi - The polar angle the camera should move around. If ``None``, - defaults to the current ``phi`` angle. - origin_theta - The azimutal angle the camera should move around. If ``None``, - defaults to the current ``theta`` angle. - """ - self.camera.begin_precession(rate, radius, origin_phi, origin_theta) - self.add(self.camera) - - @deprecated( - replacement="Camera.stop_precession", - message="Use self.camera.stop_precession() instead.", - ) - def stop_camera_precession(self): - """Remove the precession camera updater, if any.""" - self.camera.stop_precession() - - @deprecated( - replacement="Camera.begin_precession", - message="Use self.camera.begin_precession() followed by self.add(self.camera) instead.", - ) - def begin_3dillusion_camera_rotation( - self, - rate: float = 1.0, - radius: float = 0.2, - origin_phi: float | None = None, - origin_theta: float | None = None, - ) -> None: - """Alias of :meth:`begin_camera_precession`.""" - self.begin_camera_precession(rate, radius, origin_phi, origin_theta) - - @deprecated( - replacement="Camera.stop_precession", - message="Use self.camera.stop_precession() instead.", - ) - def stop_3dillusion_camera_rotation(self): - """Alias of :meth:`stop_camera_precession`.""" - self.stop_camera_precession() - - def move_camera( - self, - theta: float | None = None, - phi: float | None = None, - gamma: float | None = None, - zoom: float | None = None, - focal_distance: float | None = None, - frame_center: OpenGLMobject | Sequence[float] | None = None, - added_anims: Iterable[Animation] = [], - **kwargs: Any, - ) -> None: - """Animate the movement of the camera to the given spherical coordinates. - - Parameters - ---------- - theta - The azimuthal angle i.e the angle that spins the camera around the Z_AXIS. - phi - The polar angle i.e the angle between Z_AXIS and Camera through ORIGIN in radians. - gamma - The rotation of the camera about the vector from the ORIGIN to the Camera. - focal_distance - The radial focal_distance between ORIGIN and 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. - """ - animation = self.camera.animate.set_orientation( - theta, phi, gamma, zoom, focal_distance, frame_center - ) - - self.play(animation, *added_anims, **kwargs) - - def get_moving_mobjects(self, *animations: Animation) -> list[OpenGLMobject]: - """ - 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.camera.get_value_trackers() - if any(cm in moving_mobjects for cm in camera_mobjects): - return self.mobjects - return moving_mobjects - - def add_fixed_orientation_mobjects(self, *mobjects: OpenGLMobject) -> None: - """ - 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 - """ - for mob in mobjects: - mob.fix_orientation() - self.add(mob) - - def add_fixed_in_frame_mobjects(self, *mobjects: OpenGLMobject) -> None: - """ - 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. - """ - for mob in mobjects: - mob.fix_in_frame() - self.add(mob) - - def remove_fixed_orientation_mobjects(self, *mobjects: OpenGLMobject) -> None: - """ - 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. - """ - for mob in mobjects: - mob.unfix_orientation() - self.remove(mob) - - def remove_fixed_in_frame_mobjects(self, *mobjects: OpenGLMobject) -> None: - """ - 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. - """ - for mob in mobjects: - mob.unfix_from_frame() - self.remove(mob) - - -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: bool = True, - camera_config: dict = { - "should_apply_shading": True, - "exponential_projection": True, - }, - three_d_axes_config: dict = { - "num_axis_pieces": 1, - "axis_config": { - "unit_size": 2, - "tick_frequency": 1, - "numbers_with_elongated_ticks": [0, 1, 2], - "stroke_width": 2, - }, - }, - sphere_config: dict = {"radius": 2, "resolution": (24, 48)}, - # When scene is extracted with -l flag, this - # configuration will override the above configuration. - low_quality_config: dict = { - "camera_config": {"should_apply_shading": False}, - "three_d_axes_config": {"num_axis_pieces": 1}, - "sphere_config": {"resolution": (12, 24)}, - }, - **kwargs: Any, - ) -> None: - 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.low_quality_config = low_quality_config - if self.manager.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) -> ThreeDAxes: - """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 = OpenGLVGroup(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 - axis.pieces.note_changed_family() - for tick in axis.tick_marks: - tick.add(OpenGLVectorizedPoint(1.5 * tick.get_center())) - return axes - - def get_sphere(self, **kwargs: Any) -> Sphere: - """ - 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) diff --git a/manim/utils/testing/frames_comparison.py b/manim/utils/testing/frames_comparison.py index 6caa6837c9..767cf149dc 100644 --- a/manim/utils/testing/frames_comparison.py +++ b/manim/utils/testing/frames_comparison.py @@ -53,7 +53,7 @@ def frames_comparison( last_frame whether the test should test the last frame, by default True. base_scene - The base class for the scene (ThreeDScene, etc.), by default Scene + The base class for the scene (VectorSpaceScene, etc.), by default Scene .. warning:: By default, last_frame is True, which means that only the last frame is tested. diff --git a/tests/module/scene/test_threed_scene.py b/tests/module/scene/test_threed_scene.py deleted file mode 100644 index 7e055b65cc..0000000000 --- a/tests/module/scene/test_threed_scene.py +++ /dev/null @@ -1,18 +0,0 @@ -from manim import Circle, Manager, Square, ThreeDScene - - -def test_fixed_mobjects(): - manager = Manager(ThreeDScene) - scene = manager.scene - s = Square() - c = Circle() - scene.add_fixed_in_frame_mobjects(s, c) - assert set(scene.mobjects) == {s, c} - assert set(scene.camera.fixed_in_frame_mobjects) == {s, c} - scene.remove_fixed_in_frame_mobjects(s) - assert set(scene.mobjects) == {s, c} - assert set(scene.camera.fixed_in_frame_mobjects) == {c} - scene.add_fixed_orientation_mobjects(s) - assert set(scene.camera.fixed_orientation_mobjects) == {s} - scene.remove_fixed_orientation_mobjects(s) - assert len(scene.camera.fixed_orientation_mobjects) == 0 diff --git a/tests/test_graphical_units/test_axes.py b/tests/test_graphical_units/test_axes.py index 2e06300e46..ee6f47ed0e 100644 --- a/tests/test_graphical_units/test_axes.py +++ b/tests/test_graphical_units/test_axes.py @@ -282,7 +282,7 @@ def test_get_riemann_rectangles(scene, use_vectorized): scene.add(ax, bounding_line, quadratic, rects_right, rects_left, bounded_rects) -@frames_comparison(base_scene=ThreeDScene) +@frames_comparison def test_get_z_axis_label(scene): ax = ThreeDAxes() lab = ax.get_z_axis_label(Tex("$z$-label")) diff --git a/tests/test_graphical_units/test_coordinate_systems.py b/tests/test_graphical_units/test_coordinate_systems.py index 7d6dad67af..b0220a4532 100644 --- a/tests/test_graphical_units/test_coordinate_systems.py +++ b/tests/test_graphical_units/test_coordinate_systems.py @@ -37,7 +37,7 @@ def test_line_graph(scene): scene.add(plane, first_line, second_line) -@frames_comparison(base_scene=ThreeDScene) +@frames_comparison def test_plot_surface(scene): axes = ThreeDAxes(x_range=(-5, 5, 1), y_range=(-5, 5, 1), z_range=(-5, 5, 1)) @@ -57,7 +57,7 @@ def param_trig(u, v): scene.add(axes, trig_plane) -@frames_comparison(base_scene=ThreeDScene) +@frames_comparison def test_plot_surface_colorscale(scene): axes = ThreeDAxes(x_range=(-3, 3, 1), y_range=(-3, 3, 1), z_range=(-5, 5, 1)) diff --git a/tests/test_graphical_units/test_mobjects.py b/tests/test_graphical_units/test_mobjects.py index c76c8599d7..612535d7ec 100644 --- a/tests/test_graphical_units/test_mobjects.py +++ b/tests/test_graphical_units/test_mobjects.py @@ -6,7 +6,7 @@ __module_test__ = "mobjects" -@frames_comparison(base_scene=ThreeDScene) +@frames_comparison def test_PointCloudDot(scene): p = PointCloudDot() scene.add(p) From de6ffc7560c6acbc0c8ab0fa01e2aa4bbb007418 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francisco=20Manr=C3=ADquez?= Date: Sun, 15 Dec 2024 23:08:15 -0300 Subject: [PATCH 05/11] Modify references to Camera.set_orientation() and similar stuff --- ...object.three_d.three_dimensions.Line3D.pot | 2 +- ...manim.mobject.three_d.three_dimensions.pot | 2 +- ...scene.three_d_scene.SpecialThreeDScene.pot | 97 -------- .../manim.scene.three_d_scene.ThreeDScene.pot | 210 ------------------ docs/source/examples.rst | 66 +++--- manim/mobject/graphing/coordinate_systems.py | 8 +- manim/mobject/graphing/functions.py | 2 +- manim/mobject/three_d/polyhedra.py | 14 +- manim/mobject/three_d/three_dimensions.py | 24 +- tests/test_graphical_units/test_axes.py | 42 ++-- tests/test_graphical_units/test_threed.py | 60 ++--- 11 files changed, 118 insertions(+), 409 deletions(-) delete mode 100644 docs/i18n/gettext/reference/manim.scene.three_d_scene.SpecialThreeDScene.pot delete mode 100644 docs/i18n/gettext/reference/manim.scene.three_d_scene.ThreeDScene.pot diff --git a/docs/i18n/gettext/reference/manim.mobject.three_d.three_dimensions.Line3D.pot b/docs/i18n/gettext/reference/manim.mobject.three_d.three_dimensions.Line3D.pot index 45444fe0a6..7ba794a3d0 100644 --- a/docs/i18n/gettext/reference/manim.mobject.three_d.three_dimensions.Line3D.pot +++ b/docs/i18n/gettext/reference/manim.mobject.three_d.three_dimensions.Line3D.pot @@ -19,7 +19,7 @@ msgid "Bases: :py:class:`manim.mobject.three_d.three_dimensions.Cylinder`" msgstr "" #: ../../../manim/mobject/three_d/three_dimensions.py:docstring of manim.mobject.three_d.three_dimensions.Line3D:1 -msgid "A cylindrical line, for use in ThreeDScene." +msgid "A cylindrical line." msgstr "" #: ../../../manim/mobject/three_d/three_dimensions.py:docstring of manim.mobject.three_d.three_dimensions.Line3D:4 diff --git a/docs/i18n/gettext/reference/manim.mobject.three_d.three_dimensions.pot b/docs/i18n/gettext/reference/manim.mobject.three_d.three_dimensions.pot index 66e4bc3e93..3ee6e0a815 100644 --- a/docs/i18n/gettext/reference/manim.mobject.three_d.three_dimensions.pot +++ b/docs/i18n/gettext/reference/manim.mobject.three_d.three_dimensions.pot @@ -35,7 +35,7 @@ msgid "A spherical dot." msgstr "" #: ../../source/reference/manim.mobject.three_d.three_dimensions.rst:40::1 -msgid "A cylindrical line, for use in ThreeDScene." +msgid "A cylindrical line." msgstr "" #: ../../source/reference/manim.mobject.three_d.three_dimensions.rst:40::1 diff --git a/docs/i18n/gettext/reference/manim.scene.three_d_scene.SpecialThreeDScene.pot b/docs/i18n/gettext/reference/manim.scene.three_d_scene.SpecialThreeDScene.pot deleted file mode 100644 index 3c9400b6c9..0000000000 --- a/docs/i18n/gettext/reference/manim.scene.three_d_scene.SpecialThreeDScene.pot +++ /dev/null @@ -1,97 +0,0 @@ - -msgid "" -msgstr "" -"Project-Id-Version: Manim \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" - -#: ../../source/reference/manim.scene.three_d_scene.SpecialThreeDScene.rst:2 -msgid "SpecialThreeDScene" -msgstr "" - -#: ../../source/reference/manim.scene.three_d_scene.SpecialThreeDScene.rst:4 -msgid "Qualified name: ``manim.scene.three\\_d\\_scene.SpecialThreeDScene``" -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene:1 -msgid "Bases: :py:class:`manim.scene.three_d_scene.ThreeDScene`" -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene:1 -msgid "An extension of :class:`ThreeDScene` with more settings." -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene:3 -msgid "It has some extra configuration for axes, spheres, and an override for low quality rendering. Further key differences are:" -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene:7 -msgid "The camera shades applicable 3DMobjects by default, except if rendering in low quality." -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene:9 -msgid "Some default params for Spheres and Axes have been added." -msgstr "" - -#: ../../source/reference/manim.scene.three_d_scene.SpecialThreeDScene.rst:14 -msgid "Methods" -msgstr "" - -#: ../../source/reference/manim.scene.three_d_scene.SpecialThreeDScene.rst:23::1 -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene.get_axes:1 -msgid "Return a set of 3D axes." -msgstr "" - -#: ../../source/reference/manim.scene.three_d_scene.SpecialThreeDScene.rst:23::1 -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene.get_default_camera_position:1 -msgid "Returns the default_angled_camera position." -msgstr "" - -#: ../../source/reference/manim.scene.three_d_scene.SpecialThreeDScene.rst:23::1 -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene.get_sphere:1 -msgid "Returns a sphere with the passed keyword arguments as properties." -msgstr "" - -#: ../../source/reference/manim.scene.three_d_scene.SpecialThreeDScene.rst:23::1 -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene.set_camera_to_default_position:1 -msgid "Sets the camera to its default position." -msgstr "" - -#: ../../source/reference/manim.scene.three_d_scene.SpecialThreeDScene.rst:25 -msgid "Attributes" -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene.get_axes:0 -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene.get_default_camera_position:0 -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene.get_sphere:0 -msgid "Returns" -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene.get_axes:3 -msgid "A set of 3D axes." -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene.get_axes:0 -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene.get_default_camera_position:0 -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene.get_sphere:0 -msgid "Return type" -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene.get_default_camera_position:3 -msgid "Dictionary of phi, theta, focal_distance, and gamma." -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene.get_sphere:0 -msgid "Parameters" -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene.get_sphere:3 -msgid "Any valid parameter of :class:`~.Sphere` or :class:`~.Surface`." -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene.get_sphere:5 -msgid "The sphere object." -msgstr "" - - diff --git a/docs/i18n/gettext/reference/manim.scene.three_d_scene.ThreeDScene.pot b/docs/i18n/gettext/reference/manim.scene.three_d_scene.ThreeDScene.pot deleted file mode 100644 index 2a7d73eb70..0000000000 --- a/docs/i18n/gettext/reference/manim.scene.three_d_scene.ThreeDScene.pot +++ /dev/null @@ -1,210 +0,0 @@ - -msgid "" -msgstr "" -"Project-Id-Version: Manim \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" - -#: ../../source/reference/manim.scene.three_d_scene.ThreeDScene.rst:2 -msgid "ThreeDScene" -msgstr "" - -#: ../../source/reference/manim.scene.three_d_scene.ThreeDScene.rst:4 -msgid "Qualified name: ``manim.scene.three\\_d\\_scene.ThreeDScene``" -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene:1 -msgid "Bases: :py:class:`manim.scene.scene.Scene`" -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene:1 -msgid "This is a Scene, with special configurations and properties that make it suitable for Three Dimensional Scenes." -msgstr "" - -#: ../../source/reference/manim.scene.three_d_scene.ThreeDScene.rst:14 -msgid "Methods" -msgstr "" - -#: ../../source/reference/manim.scene.three_d_scene.ThreeDScene.rst:31::1 -msgid "This method is used to prevent the rotation and movement of mobjects as the camera moves around." -msgstr "" - -#: ../../source/reference/manim.scene.three_d_scene.ThreeDScene.rst:31::1 -msgid "This method is used to prevent the rotation and tilting of mobjects as the camera moves around." -msgstr "" - -#: ../../source/reference/manim.scene.three_d_scene.ThreeDScene.rst:31::1 -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.begin_3dillusion_camera_rotation:1 -msgid "This method creates a 3D camera rotation illusion around the current camera orientation." -msgstr "" - -#: ../../source/reference/manim.scene.three_d_scene.ThreeDScene.rst:31::1 -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.begin_ambient_camera_rotation:1 -msgid "This method begins an ambient rotation of the camera about the Z_AXIS, in the anticlockwise direction" -msgstr "" - -#: ../../source/reference/manim.scene.three_d_scene.ThreeDScene.rst:31::1 -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.get_moving_mobjects:1 -msgid "This method returns a list of all of the Mobjects in the Scene that are moving, that are also in the animations passed." -msgstr "" - -#: ../../source/reference/manim.scene.three_d_scene.ThreeDScene.rst:31::1 -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.move_camera:1 -msgid "This method animates the movement of the camera to the given spherical coordinates." -msgstr "" - -#: ../../source/reference/manim.scene.three_d_scene.ThreeDScene.rst:31::1 -msgid "This method undoes what add_fixed_in_frame_mobjects does." -msgstr "" - -#: ../../source/reference/manim.scene.three_d_scene.ThreeDScene.rst:31::1 -msgid "This method \"unfixes\" the orientation of the mobjects passed, meaning they will no longer be at the same angle relative to the camera." -msgstr "" - -#: ../../source/reference/manim.scene.three_d_scene.ThreeDScene.rst:31::1 -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.set_camera_orientation:1 -msgid "This method sets the orientation of the camera in the scene." -msgstr "" - -#: ../../source/reference/manim.scene.three_d_scene.ThreeDScene.rst:31::1 -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.set_to_default_angled_camera_orientation:1 -msgid "This method sets the default_angled_camera_orientation to the keyword arguments passed, and sets the camera to that orientation." -msgstr "" - -#: ../../source/reference/manim.scene.three_d_scene.ThreeDScene.rst:31::1 -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.stop_3dillusion_camera_rotation:1 -msgid "This method stops all illusion camera rotations." -msgstr "" - -#: ../../source/reference/manim.scene.three_d_scene.ThreeDScene.rst:31::1 -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.stop_ambient_camera_rotation:1 -msgid "This method stops all ambient camera rotation." -msgstr "" - -#: ../../source/reference/manim.scene.three_d_scene.ThreeDScene.rst:33 -msgid "Attributes" -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.add_fixed_in_frame_mobjects:1 -msgid "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." -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.add_fixed_in_frame_mobjects:0 -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.add_fixed_orientation_mobjects:0 -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.begin_3dillusion_camera_rotation:0 -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.begin_ambient_camera_rotation:0 -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.get_moving_mobjects:0 -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.move_camera:0 -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.remove_fixed_in_frame_mobjects:0 -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.remove_fixed_orientation_mobjects:0 -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.set_camera_orientation:0 -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.set_to_default_angled_camera_orientation:0 -msgid "Parameters" -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.add_fixed_in_frame_mobjects:6 -msgid "The Mobjects whose orientation must be fixed." -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.add_fixed_orientation_mobjects:1 -msgid "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.)" -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.add_fixed_orientation_mobjects:7 -msgid "The Mobject(s) whose orientation must be fixed." -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.add_fixed_orientation_mobjects:9 -msgid "Some valid kwargs are use_static_center_func : bool center_func : function" -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.add_fixed_orientation_mobjects:11 -msgid "Some valid kwargs are" -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.add_fixed_orientation_mobjects:11 -msgid "use_static_center_func : bool center_func : function" -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.begin_3dillusion_camera_rotation:4 -msgid "The rate at which the camera rotation illusion should operate." -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.begin_3dillusion_camera_rotation:5 -msgid "The polar angle the camera should move around. Defaults to the current phi angle." -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.begin_3dillusion_camera_rotation:7 -msgid "The azimutal angle the camera should move around. Defaults to the current theta angle." -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.begin_ambient_camera_rotation:4 -msgid "The rate at which the camera should rotate about the Z_AXIS. Negative rate means clockwise rotation." -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.begin_ambient_camera_rotation:7 -msgid "one of 3 options: [\"theta\", \"phi\", \"gamma\"]. defaults to theta." -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.get_moving_mobjects:4 -msgid "The animations whose mobjects will be checked." -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.move_camera:4 -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.set_camera_orientation:3 -msgid "The polar angle i.e the angle between Z_AXIS and Camera through ORIGIN in radians." -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.move_camera:6 -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.set_camera_orientation:5 -msgid "The azimuthal angle i.e the angle that spins the camera around the Z_AXIS." -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.move_camera:8 -msgid "The radial focal_distance between ORIGIN and Camera." -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.move_camera:10 -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.set_camera_orientation:9 -msgid "The rotation of the camera about the vector from the ORIGIN to the Camera." -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.move_camera:12 -msgid "The zoom factor of the camera." -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.move_camera:14 -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.set_camera_orientation:13 -msgid "The new center of the camera frame in cartesian coordinates." -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.move_camera:16 -msgid "Any other animations to be played at the same time." -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.remove_fixed_in_frame_mobjects:1 -msgid "This method undoes what add_fixed_in_frame_mobjects does. It allows the mobject to be affected by the movement of the camera." -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.remove_fixed_in_frame_mobjects:5 -msgid "The Mobjects whose position and orientation must be unfixed." -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.remove_fixed_orientation_mobjects:1 -msgid "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." -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.remove_fixed_orientation_mobjects:6 -msgid "The Mobjects whose orientation must be unfixed." -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.set_camera_orientation:7 -msgid "The focal_distance of the Camera." -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.set_camera_orientation:11 -msgid "The zoom factor of the scene." -msgstr "" - - diff --git a/docs/source/examples.rst b/docs/source/examples.rst index 3213c35160..00c4de9b6a 100644 --- a/docs/source/examples.rst +++ b/docs/source/examples.rst @@ -597,25 +597,25 @@ Special Camera Settings .. manim:: FixedInFrameMObjectTest :save_last_frame: - :ref_classes: ThreeDScene - :ref_methods: ThreeDScene.set_camera_orientation ThreeDScene.add_fixed_in_frame_mobjects + :ref_classes: Scene + :ref_methods: Camera.set_orientation OpenGLMobject.fix_in_frame - class FixedInFrameMObjectTest(ThreeDScene): + class FixedInFrameMObjectTest(Scene): def construct(self): axes = ThreeDAxes() - self.set_camera_orientation(phi=75 * DEGREES, theta=-45 * DEGREES) + self.camera.set_orientation(theta=-45 * DEGREES, phi=75 * DEGREES) text3d = Text("This is a 3D text") - self.add_fixed_in_frame_mobjects(text3d) + text3d.fix_in_frame() text3d.to_corner(UL) self.add(axes) self.wait() .. manim:: ThreeDLightSourcePosition :save_last_frame: - :ref_classes: ThreeDScene ThreeDAxes Surface - :ref_methods: ThreeDScene.set_camera_orientation + :ref_classes: Scene ThreeDAxes Surface + :ref_methods: Camera.set_orientation - class ThreeDLightSourcePosition(ThreeDScene): + class ThreeDLightSourcePosition(Scene): def construct(self): axes = ThreeDAxes() sphere = Surface( @@ -626,49 +626,57 @@ Special Camera Settings ]), v_range=[0, TAU], u_range=[-PI / 2, PI / 2], checkerboard_colors=[RED_D, RED_E], resolution=(15, 32) ) - self.renderer.camera.light_source.move_to(3*IN) # changes the source of the light - self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES) + # TODO: implement light source + self.camera.light_source.move_to(3*IN) # changes the source of the light + self.camera.set_orientation(theta=30 * DEGREES, phi=75 * DEGREES) self.add(axes, sphere) .. manim:: ThreeDCameraRotation - :ref_classes: ThreeDScene ThreeDAxes - :ref_methods: ThreeDScene.begin_ambient_camera_rotation ThreeDScene.stop_ambient_camera_rotation + :ref_classes: Circle Scene ThreeDAxes + :ref_methods: Camera.begin_ambient_rotation Camera.stop_ambient_rotation - class ThreeDCameraRotation(ThreeDScene): + class ThreeDCameraRotation(Scene): def construct(self): axes = ThreeDAxes() - circle=Circle() - self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES) + circle = Circle() + self.camera.set_orientation(theta=30 * DEGREES, phi=75 * DEGREES) self.add(circle,axes) - self.begin_ambient_camera_rotation(rate=0.1) + self.camera.begin_ambient_rotation(rate=0.1) + self.add(self.camera) self.wait() - self.stop_ambient_camera_rotation() - self.move_camera(phi=75 * DEGREES, theta=30 * DEGREES) + self.camera.stop_ambient_rotation() + self.play( + self.camera.animate.set_orientation( + theta=30 * DEGREES, phi=75 * DEGREES + ), + ) self.wait() -.. manim:: ThreeDCameraIllusionRotation - :ref_classes: ThreeDScene ThreeDAxes - :ref_methods: ThreeDScene.begin_3dillusion_camera_rotation ThreeDScene.stop_3dillusion_camera_rotation +.. manim:: ThreeDCameraPrecession + :ref_classes: Circle Scene ThreeDAxes + :ref_methods: Camera.begin_precession Camera.stop_precession - class ThreeDCameraIllusionRotation(ThreeDScene): + class ThreeDCameraPrecession(Scene): def construct(self): axes = ThreeDAxes() - circle=Circle() - self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES) + circle = Circle() + self.camera.set_orientation(theta=30 * DEGREES, phi=75 * DEGREES) self.add(circle,axes) - self.begin_3dillusion_camera_rotation(rate=2) + self.camera.begin_precession(rate=2) + self.add(self.camera) self.wait(PI/2) - self.stop_3dillusion_camera_rotation() + self.camera.stop_precession() .. manim:: ThreeDSurfacePlot :save_last_frame: - :ref_classes: ThreeDScene Surface + :ref_classes: Scene Surface + :ref_methods: Camera.set_orientation - class ThreeDSurfacePlot(ThreeDScene): + class ThreeDSurfacePlot(Scene): def construct(self): resolution_fa = 24 - self.set_camera_orientation(phi=75 * DEGREES, theta=-30 * DEGREES) + self.camera.set_orientation(theta=-30 * DEGREES, phi=75 * DEGREES) def param_gauss(u, v): x = u diff --git a/manim/mobject/graphing/coordinate_systems.py b/manim/mobject/graphing/coordinate_systems.py index 0cd20c7194..9d47106e82 100644 --- a/manim/mobject/graphing/coordinate_systems.py +++ b/manim/mobject/graphing/coordinate_systems.py @@ -957,7 +957,7 @@ def plot_surface( class PlotSurfaceExample(Scene): def construct(self): resolution_fa = 16 - self.camera.set_orientation(phi=75 * DEGREES, theta=-60 * DEGREES) + self.camera.set_orientation(theta=-60 * DEGREES, phi=75 * DEGREES) axes = ThreeDAxes(x_range=(-3, 3, 1), y_range=(-3, 3, 1), z_range=(-5, 5, 1)) def param_trig(u, v): x = u @@ -2523,7 +2523,7 @@ class GetYAxisLabelExample(Scene): def construct(self): ax = ThreeDAxes() lab = ax.get_y_axis_label(Tex("$y$-label")) - self.camera.set_orientation(phi=2*PI/5, theta=PI/5) + self.camera.set_orientation(theta=PI/5, phi=2*PI/5) self.add(ax, lab) """ positioned_label = self._get_axis_label( @@ -2573,7 +2573,7 @@ class GetZAxisLabelExample(Scene): def construct(self): ax = ThreeDAxes() lab = ax.get_z_axis_label(Tex("$z$-label")) - self.camera.set_orientation(phi=2*PI/5, theta=PI/5) + self.camera.set_orientation(theta=PI/5, phi=2*PI/5) self.add(ax, lab) """ positioned_label = self._get_axis_label( @@ -2622,7 +2622,7 @@ def get_axis_labels( class GetAxisLabelsExample(Scene): def construct(self): - self.camera.set_orientation(phi=2*PI/5, theta=PI/5) + self.camera.set_orientation(theta=PI/5, phi=2*PI/5) axes = ThreeDAxes() labels = axes.get_axis_labels( Text("x-axis").scale(0.7), Text("y-axis").scale(0.45), Text("z-axis").scale(0.45) diff --git a/manim/mobject/graphing/functions.py b/manim/mobject/graphing/functions.py index 4c16718876..8eb19fdd3c 100644 --- a/manim/mobject/graphing/functions.py +++ b/manim/mobject/graphing/functions.py @@ -76,7 +76,7 @@ def construct(self): ).set_shade_in_3d(True) axes = ThreeDAxes() self.add(axes, curve1) - self.camera.set_orientation(phi=80 * DEGREES, theta=-60 * DEGREES) + self.camera.set_orientation(theta=-60 * DEGREES, phi=80 * DEGREES) self.wait() .. attention:: diff --git a/manim/mobject/three_d/polyhedra.py b/manim/mobject/three_d/polyhedra.py index 3b3f054845..30a78a9c78 100644 --- a/manim/mobject/three_d/polyhedra.py +++ b/manim/mobject/three_d/polyhedra.py @@ -53,7 +53,7 @@ class Polyhedron(VGroup): class SquarePyramidScene(Scene): def construct(self): - self.camera.set_orientation(phi=75 * DEGREES, theta=30 * DEGREES) + self.camera.set_orientation(theta=30 * DEGREES, phi=75 * DEGREES) vertex_coords = [ [1, 1, 0], [1, -1, 0], @@ -87,7 +87,7 @@ def construct(self): class PolyhedronSubMobjects(Scene): def construct(self): - self.camera.set_orientation(phi=75 * DEGREES, theta=30 * DEGREES) + self.camera.set_orientation(theta=30 * DEGREES, phi=75 * DEGREES) octahedron = Octahedron(edge_length = 3) octahedron.graph[0].set_color(RED) octahedron.faces[2].set_color(YELLOW) @@ -176,7 +176,7 @@ class Tetrahedron(Polyhedron): class TetrahedronScene(Scene): def construct(self): - self.camera.set_orientation(phi=75 * DEGREES, theta=30 * DEGREES) + self.camera.set_orientation(theta=30 * DEGREES, phi=75 * DEGREES) obj = Tetrahedron() self.add(obj) """ @@ -211,7 +211,7 @@ class Octahedron(Polyhedron): class OctahedronScene(Scene): def construct(self): - self.camera.set_orientation(phi=75 * DEGREES, theta=30 * DEGREES) + self.camera.set_orientation(theta=30 * DEGREES, phi=75 * DEGREES) obj = Octahedron() self.add(obj) """ @@ -257,7 +257,7 @@ class Icosahedron(Polyhedron): class IcosahedronScene(Scene): def construct(self): - self.camera.set_orientation(phi=75 * DEGREES, theta=30 * DEGREES) + self.camera.set_orientation(theta=30 * DEGREES, phi=75 * DEGREES) obj = Icosahedron() self.add(obj) """ @@ -322,7 +322,7 @@ class Dodecahedron(Polyhedron): class DodecahedronScene(Scene): def construct(self): - self.camera.set_orientation(phi=75 * DEGREES, theta=30 * DEGREES) + self.camera.set_orientation(theta=30 * DEGREES, phi=75 * DEGREES) obj = Dodecahedron() self.add(obj) """ @@ -392,7 +392,7 @@ class ConvexHull3D(Polyhedron): class ConvexHull3DExample(Scene): def construct(self): - self.camera.set_orientation(phi=75 * DEGREES, theta=30 * DEGREES) + self.camera.set_orientation(theta=30 * DEGREES, phi=75 * DEGREES) points = [ [ 1.93192757, 0.44134585, -1.52407061], [-0.93302521, 1.23206983, 0.64117067], diff --git a/manim/mobject/three_d/three_dimensions.py b/manim/mobject/three_d/three_dimensions.py index 6684ea1967..9badb91ded 100644 --- a/manim/mobject/three_d/three_dimensions.py +++ b/manim/mobject/three_d/three_dimensions.py @@ -249,7 +249,7 @@ def set_fill_by_value( class FillByValueExample(Scene): def construct(self): resolution_fa = 8 - self.camera.set_orientation(phi=75 * DEGREES, theta=-160 * DEGREES) + self.camera.set_orientation(theta=-160 * DEGREES, phi=75 * DEGREES) axes = ThreeDAxes(x_range=(0, 5, 1), y_range=(0, 5, 1), z_range=(-1, 1, 0.5)) def param_surface(u, v): @@ -353,7 +353,7 @@ class Sphere(Surface): class ExampleSphere(Scene): def construct(self): - self.camera.set_orientation(phi=PI / 6, theta=PI / 6) + self.camera.set_orientation(theta=PI / 6, phi=PI / 6) sphere1 = Sphere( center=(3, 0, 0), radius=1, @@ -437,7 +437,7 @@ class Dot3D(Sphere): class Dot3DExample(Scene): def construct(self): - self.camera.set_orientation(phi=75*DEGREES, theta=-45*DEGREES) + self.camera.set_orientation(theta=-45*DEGREES, phi=75*DEGREES) axes = ThreeDAxes() dot_1 = Dot3D(point=axes.coords_to_point(0, 0, 1), color=RED) @@ -481,7 +481,7 @@ class Cube(VGroup): class CubeExample(Scene): def construct(self): - self.camera.set_orientation(phi=75*DEGREES, theta=-45*DEGREES) + self.camera.set_orientation(theta=-45*DEGREES, phi=75*DEGREES) axes = ThreeDAxes() cube = Cube(side_length=3, fill_opacity=0.7, fill_color=BLUE) @@ -537,7 +537,7 @@ class Prism(Cube): class ExamplePrism(Scene): def construct(self): - self.camera.set_orientation(phi=60 * DEGREES, theta=150 * DEGREES) + self.camera.set_orientation(theta=150 * DEGREES, phi=60 * DEGREES) prismSmall = Prism(dimensions=[1, 2, 3]).rotate(PI / 2) prismLarge = Prism(dimensions=[1.5, 3, 4.5]).move_to([2, 0, 0]) self.add(prismSmall, prismLarge) @@ -590,7 +590,7 @@ class ExampleCone(Scene): def construct(self): axes = ThreeDAxes() cone = Cone(direction=X_AXIS+Y_AXIS+2*Z_AXIS, resolution=8) - self.camera.set_orientation(phi=5*PI/11, theta=PI/9) + self.camera.set_orientation(theta=PI/9, phi=5*PI/11) self.add(axes, cone) """ @@ -751,7 +751,7 @@ class ExampleCylinder(Scene): def construct(self): axes = ThreeDAxes() cylinder = Cylinder(radius=2, height=3) - self.camera.set_orientation(phi=75 * DEGREES, theta=30 * DEGREES) + self.camera.set_orientation(theta=30 * DEGREES, phi=75 * DEGREES) self.add(axes, cylinder) """ @@ -912,7 +912,7 @@ class ExampleLine3D(Scene): def construct(self): axes = ThreeDAxes() line = Line3D(start=np.array([0, 0, 0]), end=np.array([2, 2, 2])) - self.camera.set_orientation(phi=75 * DEGREES, theta=30 * DEGREES) + self.camera.set_orientation(theta=30 * DEGREES, phi=75 * DEGREES) self.add(axes, line) """ @@ -1046,7 +1046,7 @@ def parallel_to( class ParallelLineExample(Scene): def construct(self): - self.camera.set_orientation(PI / 3, -PI / 4) + self.camera.set_orientation(theta=-PI / 4, phi=PI / 3) ax = ThreeDAxes((-5, 5), (-5, 5), (-5, 5), 10, 10, 10) line1 = Line3D(RIGHT * 2, UP + OUT, color=RED) line2 = Line3D.parallel_to(line1, color=YELLOW) @@ -1094,7 +1094,7 @@ def perpendicular_to( class PerpLineExample(Scene): def construct(self): - self.camera.set_orientation(PI / 3, -PI / 4) + self.camera.set_orientation(theta=-PI / 4, phi=PI / 3) ax = ThreeDAxes((-5, 5), (-5, 5), (-5, 5), 10, 10, 10) line1 = Line3D(RIGHT * 2, UP + OUT, color=RED) line2 = Line3D.perpendicular_to(line1, color=BLUE) @@ -1148,7 +1148,7 @@ def construct(self): end=np.array([2, 2, 2]), resolution=8 ) - self.camera.set_orientation(phi=75 * DEGREES, theta=30 * DEGREES) + self.camera.set_orientation(theta=30 * DEGREES, phi=75 * DEGREES) self.add(axes, arrow) """ @@ -1219,7 +1219,7 @@ class ExampleTorus(Scene): def construct(self): axes = ThreeDAxes() torus = Torus() - self.camera.set_orientation(phi=75 * DEGREES, theta=30 * DEGREES) + self.camera.set_orientation(theta=30 * DEGREES, phi=75 * DEGREES) self.add(axes, torus) """ diff --git a/tests/test_graphical_units/test_axes.py b/tests/test_graphical_units/test_axes.py index ee6f47ed0e..2337918e90 100644 --- a/tests/test_graphical_units/test_axes.py +++ b/tests/test_graphical_units/test_axes.py @@ -7,7 +7,7 @@ @frames_comparison -def test_axes(scene): +def test_axes(scene: Scene) -> None: graph = Axes( x_range=[-10, 10, 1], y_range=[-10, 10, 1], @@ -21,14 +21,14 @@ def test_axes(scene): @frames_comparison -def test_plot_functions(scene, use_vectorized): +def test_plot_functions(scene: Scene, use_vectorized: bool) -> None: ax = Axes(x_range=(-10, 10.3), y_range=(-1.5, 1.5)) graph = ax.plot(lambda x: x**2, use_vectorized=use_vectorized) scene.add(ax, graph) @frames_comparison -def test_custom_coordinates(scene): +def test_custom_coordinates(scene: Scene) -> None: ax = Axes(x_range=[0, 10]) ax.add_coordinates( @@ -38,14 +38,14 @@ def test_custom_coordinates(scene): @frames_comparison -def test_get_axis_labels(scene): +def test_get_axis_labels(scene: Scene) -> None: ax = Axes() labels = ax.get_axis_labels(Tex("$x$-axis").scale(0.7), Tex("$y$-axis").scale(0.45)) scene.add(ax, labels) @frames_comparison -def test_get_x_axis_label(scene): +def test_get_x_axis_label(scene: Scene) -> None: ax = Axes(x_range=(0, 8), y_range=(0, 5), x_length=8, y_length=5) x_label = ax.get_x_axis_label( Tex("$x$-values").scale(0.65), @@ -57,7 +57,7 @@ def test_get_x_axis_label(scene): @frames_comparison -def test_get_y_axis_label(scene): +def test_get_y_axis_label(scene: Scene) -> None: ax = Axes(x_range=(0, 8), y_range=(0, 5), x_length=8, y_length=5) y_label = ax.get_y_axis_label( Tex("$y$-values").scale(0.65).rotate(90 * DEGREES), @@ -69,7 +69,7 @@ def test_get_y_axis_label(scene): @frames_comparison -def test_axis_tip_default_width_height(scene): +def test_axis_tip_default_width_height(scene: Scene) -> None: ax = Axes( x_range=(0, 4), y_range=(0, 4), @@ -80,7 +80,7 @@ def test_axis_tip_default_width_height(scene): @frames_comparison -def test_axis_tip_custom_width_height(scene): +def test_axis_tip_custom_width_height(scene: Scene) -> None: ax = Axes( x_range=(0, 4), y_range=(0, 4), @@ -93,7 +93,7 @@ def test_axis_tip_custom_width_height(scene): @frames_comparison -def test_plot_derivative_graph(scene, use_vectorized): +def test_plot_derivative_graph(scene: Scene, use_vectorized: bool) -> None: ax = NumberPlane(y_range=[-1, 7], background_line_style={"stroke_opacity": 0.4}) curve_1 = ax.plot(lambda x: x**2, color=PURPLE_B, use_vectorized=use_vectorized) @@ -104,7 +104,7 @@ def test_plot_derivative_graph(scene, use_vectorized): @frames_comparison -def test_plot(scene, use_vectorized): +def test_plot(scene: Scene, use_vectorized: bool) -> None: # construct the axes ax_1 = Axes( x_range=[0.001, 6], @@ -150,7 +150,7 @@ def log_func(x): @frames_comparison -def test_get_graph_label(scene): +def test_get_graph_label(scene: Scene) -> None: ax = Axes() sin = ax.plot(lambda x: np.sin(x), color=PURPLE_B) label = ax.get_graph_label( @@ -166,7 +166,7 @@ def test_get_graph_label(scene): @frames_comparison -def test_get_lines_to_point(scene): +def test_get_lines_to_point(scene: Scene) -> None: ax = Axes() circ = Circle(radius=0.5).move_to([-4, -1.5, 0]) @@ -176,7 +176,7 @@ def test_get_lines_to_point(scene): @frames_comparison -def test_plot_line_graph(scene): +def test_plot_line_graph(scene: Scene) -> None: plane = NumberPlane( x_range=(0, 7), y_range=(0, 5), @@ -199,7 +199,7 @@ def test_plot_line_graph(scene): @frames_comparison -def test_t_label(scene): +def test_t_label(scene: Scene) -> None: # defines the axes and linear function axes = Axes(x_range=[-1, 10], y_range=[-1, 10], x_length=9, y_length=6) func = axes.plot(lambda x: x, color=BLUE) @@ -209,7 +209,7 @@ def test_t_label(scene): @frames_comparison -def test_get_area(scene): +def test_get_area(scene: Scene) -> None: ax = Axes().add_coordinates() curve1 = ax.plot( lambda x: 2 * np.sin(x), @@ -235,7 +235,7 @@ def test_get_area(scene): @frames_comparison -def test_get_area_with_boundary_and_few_plot_points(scene): +def test_get_area_with_boundary_and_few_plot_points(scene: Scene) -> None: ax = Axes(x_range=[-2, 2], y_range=[-2, 2], color=WHITE) f1 = ax.plot(lambda t: t, [-1, 1, 0.5]) f2 = ax.plot(lambda t: 1, [-1, 1, 0.5]) @@ -246,7 +246,7 @@ def test_get_area_with_boundary_and_few_plot_points(scene): @frames_comparison -def test_get_riemann_rectangles(scene, use_vectorized): +def test_get_riemann_rectangles(scene: Scene, use_vectorized: bool) -> None: ax = Axes(y_range=[-2, 10]) quadratic = ax.plot(lambda x: 0.5 * x**2 - 0.5, use_vectorized=use_vectorized) @@ -283,15 +283,15 @@ def test_get_riemann_rectangles(scene, use_vectorized): @frames_comparison -def test_get_z_axis_label(scene): +def test_get_z_axis_label(scene: Scene) -> None: ax = ThreeDAxes() lab = ax.get_z_axis_label(Tex("$z$-label")) - scene.set_camera_orientation(phi=2 * PI / 5, theta=PI / 5) + scene.camera.set_orientation(theta=PI / 5, phi=2 * PI / 5) scene.add(ax, lab) @frames_comparison -def test_polar_graph(scene): +def test_polar_graph(scene: Scene) -> None: polar = PolarPlane() def r(theta): @@ -302,7 +302,7 @@ def r(theta): @frames_comparison -def test_log_scaling_graph(scene): +def test_log_scaling_graph(scene: Scene) -> None: ax = Axes( x_range=[0, 8], y_range=[-2, 4], diff --git a/tests/test_graphical_units/test_threed.py b/tests/test_graphical_units/test_threed.py index dbf8945835..d45a4fa1f0 100644 --- a/tests/test_graphical_units/test_threed.py +++ b/tests/test_graphical_units/test_threed.py @@ -7,33 +7,33 @@ @frames_comparison -def test_AddFixedInFrameMobjects(scene: Scene): - scene.camera.set_euler_angles(phi=75 * DEGREES, theta=-45 * DEGREES) +def test_AddFixedInFrameMobjects(scene: Scene) -> None: + scene.camera.set_orientation(theta=-45 * DEGREES, phi=75 * DEGREES) text = Tex("This is a 3D tex") scene.add(text) @frames_comparison -def test_Cube(scene): +def test_Cube(scene: Scene) -> None: scene.add(Cube()) @frames_comparison -def test_Sphere(scene): +def test_Sphere(scene: Scene) -> None: scene.add(Sphere()) @frames_comparison -def test_Dot3D(scene): +def test_Dot3D(scene: Scene) -> None: scene.add(Dot3D()) @frames_comparison -def test_Cone(scene): +def test_Cone(scene: Scene) -> None: scene.add(Cone(resolution=16)) -def test_Cone_get_start_and_get_end(): +def test_Cone_get_start_and_get_end() -> None: cone = Cone().shift(RIGHT).rotate(PI / 4, about_point=ORIGIN, about_edge=OUT) start = [0.70710678, 0.70710678, -1.0] end = [0.70710678, 0.70710678, 0.0] @@ -46,12 +46,12 @@ def test_Cone_get_start_and_get_end(): @frames_comparison -def test_Cylinder(scene): +def test_Cylinder(scene: Scene) -> None: scene.add(Cylinder()) @frames_comparison -def test_Line3D(scene): +def test_Line3D(scene: Scene) -> None: line1 = Line3D(resolution=16).shift(LEFT * 2) line2 = Line3D(resolution=16).shift(RIGHT * 2) perp_line = Line3D.perpendicular_to(line1, UP + OUT, resolution=16) @@ -60,47 +60,55 @@ def test_Line3D(scene): @frames_comparison -def test_Arrow3D(scene): +def test_Arrow3D(scene: Scene) -> None: scene.add(Arrow3D(resolution=16)) @frames_comparison -def test_Torus(scene): +def test_Torus(scene: Scene) -> None: scene.add(Torus()) @frames_comparison -def test_Axes(scene): +def test_Axes(scene: Scene) -> None: scene.add(ThreeDAxes()) @frames_comparison -def test_CameraMoveAxes(scene): +def test_CameraMoveAxes(scene: Scene) -> None: """Tests camera movement to explore varied views of a static scene.""" axes = ThreeDAxes() scene.add(axes) scene.add(Dot([1, 2, 3])) - scene.move_camera(phi=PI / 8, theta=-PI / 8, frame_center=[1, 2, 3], zoom=2) + scene.play( + scene.camera.animate.set_orientation( + theta=-PI / 8, phi=PI / 8, frame_center=[1, 2, 3], zoom=2 + ) + ) @frames_comparison -def test_CameraMove(scene): +def test_CameraMove(scene: Scene) -> None: cube = Cube() scene.add(cube) - scene.move_camera(phi=PI / 4, theta=PI / 4, frame_center=[0, 0, -1], zoom=0.5) + scene.play( + scene.camera.animate.set_orientation( + theta=PI / 4, phi=PI / 4, frame_center=[0, 0, -1], zoom=0.5 + ) + ) @frames_comparison -def test_AmbientCameraMove(scene): +def test_AmbientCameraMove(scene: Scene) -> None: cube = Cube() - scene.begin_ambient_camera_rotation(rate=0.5) - scene.add(cube) + scene.camera.begin_ambient_rotation(rate=0.5) + scene.add(cube, scene.camera) scene.wait() @frames_comparison -def test_MovingVertices(scene): - scene.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES) +def test_MovingVertices(scene: Scene) -> None: + scene.camera.set_orientation(theta=30 * DEGREES, phi=75 * DEGREES) vertices = [1, 2, 3, 4] edges = [(1, 2), (2, 3), (3, 4), (1, 3), (1, 4)] g = Graph(vertices, edges) @@ -115,9 +123,9 @@ def test_MovingVertices(scene): @frames_comparison -def test_SurfaceColorscale(scene): +def test_SurfaceColorscale(scene: Scene) -> None: resolution_fa = 16 - scene.set_camera_orientation(phi=75 * DEGREES, theta=-30 * DEGREES) + scene.camera.set_orientation(theta=-30 * DEGREES, phi=75 * DEGREES) axes = ThreeDAxes(x_range=(-3, 3, 1), y_range=(-3, 3, 1), z_range=(-4, 4, 1)) def param_trig(u, v): @@ -139,9 +147,9 @@ def param_trig(u, v): @frames_comparison -def test_Y_Direction(scene): +def test_Y_Direction(scene: Scene) -> None: resolution_fa = 16 - scene.set_camera_orientation(phi=75 * DEGREES, theta=-120 * DEGREES) + scene.camera.set_orientation(theta=-120 * DEGREES, phi=75 * DEGREES) axes = ThreeDAxes(x_range=(0, 5, 1), y_range=(0, 5, 1), z_range=(-1, 1, 0.5)) def param_surface(u, v): @@ -163,7 +171,7 @@ def param_surface(u, v): scene.add(axes, surface_plane) -def test_get_start_and_end_Arrow3d(): +def test_get_start_and_end_Arrow3d() -> None: start, end = ORIGIN, np.array([2, 1, 0]) arrow = Arrow3D(start, end) assert np.allclose( From f8496e44cfefc47c2ca0fd5f26fd3c090bc5e0fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francisco=20Manr=C3=ADquez?= Date: Mon, 16 Dec 2024 17:17:59 -0300 Subject: [PATCH 06/11] Add InvisibleMobject and allow rendering submobjects of Group or OpenGLMobject --- manim/camera/camera.py | 4 +-- manim/mobject/mobject.py | 5 ++-- manim/mobject/opengl/opengl_mobject.py | 15 ++++++++-- manim/renderer/renderer.py | 38 +++++++++++++++++--------- 4 files changed, 43 insertions(+), 19 deletions(-) diff --git a/manim/camera/camera.py b/manim/camera/camera.py index c700e457a8..1bfd3e1527 100644 --- a/manim/camera/camera.py +++ b/manim/camera/camera.py @@ -10,7 +10,7 @@ from manim._config import config, logger from manim.constants import * -from manim.mobject.opengl.opengl_mobject import OpenGLMobject +from manim.mobject.opengl.opengl_mobject import InvisibleMobject, OpenGLMobject from manim.utils.paths import straight_path from manim.utils.space_ops import rotation_matrix @@ -29,7 +29,7 @@ class CameraOrientationConfig(TypedDict, total=False): frame_center: OpenGLMobject | Sequence[float] | None -class Camera(OpenGLMobject): +class Camera(OpenGLMobject, InvisibleMobject): def __init__( self, frame_shape: tuple[float, float] = (config.frame_width, config.frame_height), diff --git a/manim/mobject/mobject.py b/manim/mobject/mobject.py index 74c3357bd3..3bd6ccfcab 100644 --- a/manim/mobject/mobject.py +++ b/manim/mobject/mobject.py @@ -24,6 +24,7 @@ from manim import config, logger from manim.constants import * from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL +from manim.mobject.opengl.opengl_mobject import InvisibleMobject from manim.utils.color import ( BLACK, WHITE, @@ -3012,7 +3013,7 @@ def set_z_index_by_z_Point3D(self) -> Self: return self -class Group(Mobject, metaclass=ConvertToOpenGL): +class Group(Mobject, InvisibleMobject, metaclass=ConvertToOpenGL): """Groups together multiple :class:`Mobjects <.Mobject>`. Notes @@ -3027,7 +3028,7 @@ def __init__(self, *mobjects, **kwargs) -> None: self.add(*mobjects) -class Point(Mobject, metaclass=ConvertToOpenGL): +class Point(Mobject, InvisibleMobject, metaclass=ConvertToOpenGL): def __init__( self, location: np.ndarray = ORIGIN, diff --git a/manim/mobject/opengl/opengl_mobject.py b/manim/mobject/opengl/opengl_mobject.py index 77c712f5de..7f8edf428d 100644 --- a/manim/mobject/opengl/opengl_mobject.py +++ b/manim/mobject/opengl/opengl_mobject.py @@ -41,7 +41,7 @@ rotation_matrix_transpose, ) -__all__ = ["OpenGLMobject", "MobjectKwargs"] +__all__ = ["InvisibleMobject", "OpenGLMobject", "MobjectKwargs"] if TYPE_CHECKING: from collections.abc import Iterable, Sequence @@ -72,6 +72,17 @@ logger = logging.getLogger("manim") +class InvisibleMobject: + """By default, if an :class:`OpenGLMobject` can't be rendered, the + :class:`Renderer` raises a warning before attempting to render its + submobjects. However, any subclass of :class:`OpenGLMobject` which is also + marked as a subclass of :class:`InvisibleMobject` raises no warning, + because it's explicitly marked as not intended to be rendered. + """ + + pass + + def stash_mobject_pointers( func: Callable[Concatenate[M, P], T], ) -> Callable[Concatenate[M, P], T]: @@ -2908,7 +2919,7 @@ def set(self, **kwargs) -> Self: return self -class OpenGLGroup(OpenGLMobject): +class OpenGLGroup(OpenGLMobject, InvisibleMobject): def __init__(self, *mobjects, **kwargs): if not all(isinstance(m, OpenGLMobject) for m in mobjects): raise Exception("All submobjects must be of type OpenGLMobject") diff --git a/manim/renderer/renderer.py b/manim/renderer/renderer.py index f94bf85971..56394117c9 100644 --- a/manim/renderer/renderer.py +++ b/manim/renderer/renderer.py @@ -4,6 +4,7 @@ from typing import TYPE_CHECKING, Protocol, runtime_checkable from manim._config import logger +from manim.mobject.mobject import InvisibleMobject from manim.mobject.opengl.opengl_mobject import OpenGLMobject from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject from manim.mobject.types.image_mobject import ImageMobject @@ -29,36 +30,47 @@ def __init__(self): self.capabilities = [ (OpenGLVMobject, self.render_vmobject), (ImageMobject, self.render_image), - (OpenGLMobject, lambda mob: None), ] + self._unsupported_mob_warnings: set[str] = set() def render(self, state: SceneState) -> None: self.pre_render(state.camera) for mob in state.mobjects: - for type_, render_func in self.capabilities: - if isinstance(mob, type_): - render_func(mob) - break - else: - logger.warn( - f"The type{type(mob)} is not supported in Renderer: {self.__class__}" - ) + self.render_mobject(mob) self.post_render() + def render_mobject(self, mob: OpenGLMobject) -> None: + for MobClass, render_func in self.capabilities: + if isinstance(mob, MobClass): + render_func(mob) + break + else: + if not isinstance(mob, InvisibleMobject): + warning_message = ( + f"{type(mob).__name__} cannot be rendered by {type(self).__name__}. " + "Attempting to render its submobjects..." + ) + # This prevents spamming the console. + if warning_message not in self._unsupported_mob_warnings: + logger.warning(warning_message) + self._unsupported_mob_warnings.add(warning_message) + for submob in mob.submobjects: + self.render_mobject(submob) + @abstractmethod - def pre_render(self, camera: Camera): + def pre_render(self, camera: Camera) -> None: """Actions before rendering any :class:`.OpenGLMobject`""" @abstractmethod - def post_render(self): + def post_render(self) -> None: """Actions before rendering any :class:`.OpenGLMobject`""" @abstractmethod - def render_vmobject(self, mob: OpenGLVMobject): + def render_vmobject(self, mob: OpenGLVMobject) -> None: raise NotImplementedError @abstractmethod - def render_image(self, mob: ImageMobject): + def render_image(self, mob: ImageMobject) -> None: raise NotImplementedError From 898419de77e6be8a29b84e06da1890867139dd13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francisco=20Manr=C3=ADquez=20Novoa?= <49853152+chopan050@users.noreply.github.com> Date: Mon, 16 Dec 2024 20:19:38 -0300 Subject: [PATCH 07/11] MobClass -> mob_cls Co-authored-by: Aarush Deshpande <110117391+JasonGrace2282@users.noreply.github.com> --- manim/renderer/renderer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/manim/renderer/renderer.py b/manim/renderer/renderer.py index 56394117c9..b17f38f1e9 100644 --- a/manim/renderer/renderer.py +++ b/manim/renderer/renderer.py @@ -40,8 +40,8 @@ def render(self, state: SceneState) -> None: self.post_render() def render_mobject(self, mob: OpenGLMobject) -> None: - for MobClass, render_func in self.capabilities: - if isinstance(mob, MobClass): + for mob_cls, render_func in self.capabilities: + if isinstance(mob, mob_cls): render_func(mob) break else: From e46e079a1fc72d439a8082e686375798264f9186 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francisco=20Manr=C3=ADquez=20Novoa?= <49853152+chopan050@users.noreply.github.com> Date: Mon, 16 Dec 2024 20:20:15 -0300 Subject: [PATCH 08/11] Add oneline summary to InvisibleMobject Co-authored-by: Aarush Deshpande <110117391+JasonGrace2282@users.noreply.github.com> --- manim/mobject/opengl/opengl_mobject.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/manim/mobject/opengl/opengl_mobject.py b/manim/mobject/opengl/opengl_mobject.py index 7f8edf428d..cb17ec55ef 100644 --- a/manim/mobject/opengl/opengl_mobject.py +++ b/manim/mobject/opengl/opengl_mobject.py @@ -73,7 +73,9 @@ class InvisibleMobject: - """By default, if an :class:`OpenGLMobject` can't be rendered, the + """Marker class for rendering a mobject's submobjects, and not the mobject itself. + + By default, if an :class:`OpenGLMobject` can't be rendered, the :class:`Renderer` raises a warning before attempting to render its submobjects. However, any subclass of :class:`OpenGLMobject` which is also marked as a subclass of :class:`InvisibleMobject` raises no warning, From 5f2edcdf61298cf6f4b5f0f44e1ca03325e51ccf Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 16 Dec 2024 23:20:27 +0000 Subject: [PATCH 09/11] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- manim/mobject/opengl/opengl_mobject.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manim/mobject/opengl/opengl_mobject.py b/manim/mobject/opengl/opengl_mobject.py index cb17ec55ef..fae0e89114 100644 --- a/manim/mobject/opengl/opengl_mobject.py +++ b/manim/mobject/opengl/opengl_mobject.py @@ -74,7 +74,7 @@ class InvisibleMobject: """Marker class for rendering a mobject's submobjects, and not the mobject itself. - + By default, if an :class:`OpenGLMobject` can't be rendered, the :class:`Renderer` raises a warning before attempting to render its submobjects. However, any subclass of :class:`OpenGLMobject` which is also From 78adf3082364c8967d02b21fe89a5771c8dceedd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francisco=20Manr=C3=ADquez?= Date: Mon, 16 Dec 2024 22:16:41 -0300 Subject: [PATCH 10/11] Final removal of ThreeDScene in docs --- docs/source/contributing/testing.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/contributing/testing.rst b/docs/source/contributing/testing.rst index dfc6a225f8..1386afbfe2 100644 --- a/docs/source/contributing/testing.rst +++ b/docs/source/contributing/testing.rst @@ -213,11 +213,11 @@ The decorator can be used with or without parentheses. **By default, the test on circle = Circle() scene.play(Animation(circle)) -You can also specify, when needed, which base scene you need (ThreeDScene, for example) : +You can also specify, when needed, which base scene you need (VectorSpaceScene, for example) : .. code:: python - @frames_comparison(last_frame=False, base_scene=ThreeDScene) + @frames_comparison(last_frame=False, base_scene=VectorSpaceScene) def test_circle(scene): circle = Circle() scene.play(Animation(circle)) From 3c8197321141b3c2e0d2d82737b8573ee130bd1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francisco=20Manr=C3=ADquez?= Date: Mon, 16 Dec 2024 22:18:16 -0300 Subject: [PATCH 11/11] VectorSpaceScene -> VectorScene --- docs/source/contributing/testing.rst | 4 ++-- manim/utils/testing/frames_comparison.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/contributing/testing.rst b/docs/source/contributing/testing.rst index 1386afbfe2..caddc5c463 100644 --- a/docs/source/contributing/testing.rst +++ b/docs/source/contributing/testing.rst @@ -213,11 +213,11 @@ The decorator can be used with or without parentheses. **By default, the test on circle = Circle() scene.play(Animation(circle)) -You can also specify, when needed, which base scene you need (VectorSpaceScene, for example) : +You can also specify, when needed, which base scene you need (VectorScene, for example) : .. code:: python - @frames_comparison(last_frame=False, base_scene=VectorSpaceScene) + @frames_comparison(last_frame=False, base_scene=VectorScene) def test_circle(scene): circle = Circle() scene.play(Animation(circle)) diff --git a/manim/utils/testing/frames_comparison.py b/manim/utils/testing/frames_comparison.py index 767cf149dc..b287639cf4 100644 --- a/manim/utils/testing/frames_comparison.py +++ b/manim/utils/testing/frames_comparison.py @@ -53,7 +53,7 @@ def frames_comparison( last_frame whether the test should test the last frame, by default True. base_scene - The base class for the scene (VectorSpaceScene, etc.), by default Scene + The base class for the scene (VectorScene, etc.), by default Scene .. warning:: By default, last_frame is True, which means that only the last frame is tested.