diff --git a/manim/animation/growing.py b/manim/animation/growing.py index 889de79fc0..279de26f58 100644 --- a/manim/animation/growing.py +++ b/manim/animation/growing.py @@ -33,12 +33,13 @@ def construct(self): from typing import TYPE_CHECKING, Any +from manim.mobject.geometry.line import Arrow + from ..animation.transform import Transform from ..constants import PI from ..utils.paths import spiral_path if TYPE_CHECKING: - from manim.mobject.geometry.line import Arrow from manim.mobject.opengl.opengl_mobject import OpenGLMobject from manim.typing import Point3DLike, Vector3DLike from manim.utils.color import ParsableManimColor @@ -205,6 +206,7 @@ def __init__( def create_starting_mobject(self) -> Mobject | OpenGLMobject: start_arrow = self.mobject.copy() + assert isinstance(start_arrow, Arrow) start_arrow.scale(0, scale_tips=True, about_point=self.point) if self.point_color: start_arrow.set_color(self.point_color) diff --git a/manim/mobject/abstract/__init__.py b/manim/mobject/abstract/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/manim/mobject/abstract/positionable.py b/manim/mobject/abstract/positionable.py new file mode 100644 index 0000000000..96f978deb3 --- /dev/null +++ b/manim/mobject/abstract/positionable.py @@ -0,0 +1,2295 @@ +from __future__ import annotations + +import operator as op +from collections.abc import Callable, Iterable +from functools import reduce +from typing import Any, Self + +import numpy as np + +from manim._config import config +from manim.constants import ( + DEFAULT_MOBJECT_TO_EDGE_BUFFER, + DL, + DOWN, + IN, + LEFT, + ORIGIN, + OUT, + RIGHT, + TAU, + UP, +) +from manim.typing import ( + MatrixMN, + Point3D, + Point3D_Array, + Point3DLike, + Point3DLike_Array, + Vector3DLike, +) +from manim.utils.space_ops import rotation_matrix + + +class Positionable: + """A positionable object.""" + + ### FUNDAMENTALS ### + points: Point3D_Array = np.array([]) + + def get_all_points(self) -> Point3D_Array: + """Returns all points. + + Returns + ------- + Point3D_Array + All points. + + See also + -------- + :meth:`set_points` + """ + result = self.points + for mob in self.get_family(): + if mob is self: + continue + if len(mob.points) > 0: + result = np.append(result, mob.points, axis=0) + return result + + def set_points( + self, + points: Point3DLike_Array | Positionable, + ) -> Self: + """Sets the points. + + When another object is passed, the points of per family member is matched. + + Parameters + ---------- + points : Point3DLike_Array | Positionable + The points. + + Returns + ------- + Self + The object itself. + + Examples + -------- + .. manim:: MatchPointsScene + + class MatchPointsScene(Scene): + def construct(self): + circ = Circle(fill_color=RED, fill_opacity=0.8) + square = Square(fill_color=BLUE, fill_opacity=0.2) + self.add(circ) + self.wait(0.5) + self.play(circ.animate.set_points(square)) + self.wait(0.5) + + See also + -------- + :meth:`get_points` + """ + if isinstance(points, Positionable): + for mob1, mob2 in zip(self.get_family(), points.get_family(), strict=False): + mob1.set_points(mob2.points.copy()) + else: + self.points = np.asarray(points, dtype=float) + return self + + def get_points_defining_boundary(self) -> Point3D_Array: + """Returns all points defining the boundary. + + Returns + ------- + Point3D_Array + The points defining the boundary. + + See also + -------- + :meth:`get_points` + """ + return self.get_all_points() + + ### APPLYING FUNCTIONS ### + + def get_family(self) -> Iterable[Positionable]: + """Returns all family members recursively.""" + yield self + + def apply_to_family( + self, + function: Callable[[Positionable], Any], + *, + only_with_points: bool = True, + ) -> Self: + """Applies a function to every family member. + + Parameters + ---------- + function : Callable[[Positionable], Any] + The function to apply. + only_with_points : bool, optional + Whether to apply the function only to members with points., by default True + + Returns + ------- + Self + The object itself. + + See also + -------- + :meth:`get_family` + """ + for mob in self.get_family(): + if only_with_points and len(mob.points) == 0: + continue + function(mob) + return self + + def apply_array_function( + self, + function: Callable[[Point3D_Array], Point3D_Array], + *, + about_point: Point3DLike | None = None, + about_edge: Vector3DLike | None = None, + ) -> Self: + """Applies a function to the points array. + + Parameters + ---------- + function : Callable[[Point3D_Array], Point3D_Array] + The function to apply. + about_point : Point3DLike | None, optional + The point about which to apply the function., by default None + about_edge : Vector3DLike | None, optional + The edge about which to apply the function., by default None + + Returns + ------- + Self + The object itself. + + See also + -------- + :meth:`apply_to_family` + """ + if about_point is None: + if about_edge is None: + about_edge = ORIGIN + about_point = self.get_position(direction=about_edge) + + # TODO: Is this necessary? + # Make a copy to prevent mutation of the original array if about_point is a view + about_point = np.array(about_point, copy=True) + + if (about_point == ORIGIN).all(): + + def apply(mob: Positionable) -> None: + mob.points = function(mob.points) + else: + + def apply(mob: Positionable) -> None: + mob.points -= about_point + mob.points = function(mob.points) + mob.points += about_point + + return self.apply_to_family(function=apply) + + def apply_function( + self, + function: Callable[[Point3D], Point3D], + *, + about_point: Point3DLike | None = None, + about_edge: Vector3DLike | None = None, + ) -> Self: + """Applies a function to every point. + + Parameters + ---------- + function : Callable[[Point3D], Point3D] + The function to apply. + about_point : Point3DLike | None, optional + The point about which to apply the function., by default None + about_edge : Vector3DLike | None, optional + The edge about which to apply the function., by default None + + Returns + ------- + Self + The object itself. + + See also + -------- + :meth:`apply_array_function` + """ + if about_point is None and about_edge is None: + about_point = ORIGIN + + def apply(points: Point3D_Array) -> Point3D_Array: + return np.apply_along_axis(func1d=function, axis=1, arr=points) + + return self.apply_array_function( + function=apply, + about_point=about_point, + about_edge=about_edge, + ) + + def apply_complex_function( + self, + function: Callable[[complex], complex], + *, + about_point: Point3DLike | None = None, + about_edge: Vector3DLike | None = None, + ) -> Self: + """Applies a complex function to every point. + + Parameters + ---------- + function : Callable[[complex], complex] + The function to apply. + about_point : Point3DLike | None, optional + The point about which to apply the function., by default None + about_edge : Vector3DLike | None, optional + The edge about which to apply the function., by default None + + Returns + ------- + Self + The object itself. + + Example + ------- + + .. manim:: ApplyFuncExample + + class ApplyFuncExample(Scene): + def construct(self): + circ = Circle().scale(1.5) + circ_ref = circ.copy() + circ.apply_complex_function( + lambda x: np.exp(x*1j) + ) + t = ValueTracker(0) + circ.add_updater( + lambda x: x.become(circ_ref.copy().apply_complex_function( + lambda x: np.exp(x+t.get_value()*1j) + )).set_color(BLUE) + ) + self.add(circ_ref) + self.play(TransformFromCopy(circ_ref, circ)) + self.play(t.animate.set_value(TAU), run_time=3) + + See also + -------- + :meth:`apply_function` + """ + + def apply(point: Point3D) -> Point3D: + x, y, z = point + xy_complex = function(complex(x, y)) + return np.array([xy_complex.real, xy_complex.imag, z]) + + return self.apply_function( + function=apply, + about_point=about_point, + about_edge=about_edge, + ) + + ### TRANSFORMATIONS ### + + def translate(self, vector: Vector3DLike) -> Self: + """Applies a translation. + + Parameters + ---------- + vector : Vector3DLike + The vector. + + Returns + ------- + Self + The object itself. + """ + return self.apply_to_family(function=lambda mob: mob.points.__iadd__(vector)) + + def rotate( + self, + angle: float, + axis: Vector3DLike = OUT, + *, + about_point: Point3DLike | None = None, + about_edge: Vector3DLike | None = None, + ) -> Self: + """Applies a rotation. + + Parameters + ---------- + angle : float + The angle. + axis : Vector3DLike, optional + The axis about which to apply the rotation., by default OUT + about_point : Point3DLike | None, optional + The point about which to apply the rotation., by default None + about_edge : Vector3DLike | None, optional + The edge about which to apply the rotation., by default None + + .. note:: + To animate a rotation, use :class:`~.Rotating` or :class:`~.Rotate` + instead of ``.animate.rotate(...)``. + The ``.animate.rotate(...)`` syntax only applies a transformation + from the initial state to the final rotated state + (interpolation between the two states), without showing proper rotational motion + based on the angle (from 0 to the given angle). + + Examples + -------- + + .. manim:: RotateMethodExample + :save_last_frame: + + class RotateMethodExample(Scene): + def construct(self): + circle = Circle(radius=1, color=BLUE) + line = Line(start=ORIGIN, end=RIGHT) + arrow1 = Arrow(start=ORIGIN, end=RIGHT, buff=0, color=GOLD) + group1 = VGroup(circle, line, arrow1) + + group2 = group1.copy() + arrow2 = group2[2] + arrow2.rotate(angle=PI / 4, about_point=arrow2.get_start()) + + group3 = group1.copy() + arrow3 = group3[2] + arrow3.rotate(angle=120 * DEGREES, about_point=arrow3.get_start()) + + self.add(VGroup(group1, group2, group3).arrange(RIGHT, buff=1)) + + Returns + ------- + Self + The object itself. + """ + if about_point is None and about_edge is None: + about_edge = ORIGIN + return self.apply_matrix( + matrix=rotation_matrix(angle, axis), + about_point=about_point, + about_edge=about_edge, + ) + + def scale( + self, + factor: float, + scale_stroke: bool = False, + *, + about_point: Point3DLike | None = None, + about_edge: Vector3DLike | None = None, + ) -> Self: + """Applies a uniform scaling. + + Parameters + ---------- + factor : float + The factor. + scale_stroke : bool, optional + Whether to scale the stroke width., by default False + about_point : Point3DLike | None, optional + The point about which to apply the scaling., by default None + about_edge : Vector3DLike | None, optional + The edge about which to apply the scaling., by default None + + Returns + ------- + Self + The object itself. + + Examples + -------- + + .. manim:: MobjectScaleExample + :save_last_frame: + + class MobjectScaleExample(Scene): + def construct(self): + f1 = Text("F") + f2 = Text("F").scale(2) + f3 = Text("F").scale(0.5) + f4 = Text("F").scale(-1) + + vgroup = VGroup(f1, f2, f3, f4).arrange(6 * RIGHT) + self.add(vgroup) + + See also + -------- + :meth:`stretch` + """ + return self.apply_array_function( + function=lambda points: points.__imul__(factor), + about_point=about_point, + about_edge=about_edge, + ) + + def stretch( + self, + factor: float, + dim: int, + *, + about_point: Point3DLike | None = None, + about_edge: Vector3DLike | None = None, + ) -> Self: + """Applies a non-uniform scaling. + + Parameters + ---------- + factor : float + The factor. + dim : int + The dimension to scale. + about_point : Point3DLike | None, optional + The point about which to apply the stretching., by default None + about_edge : Vector3DLike | None, optional + The edge about which to apply the stretching., by default None + + Returns + ------- + Self + The object itself. + + See also + -------- + :meth:`scale` + """ + + def function(points: Point3D_Array) -> Point3D_Array: + points[:, dim] *= factor + return points + + return self.apply_array_function( + function=function, + about_point=about_point, + about_edge=about_edge, + ) + + def apply_matrix( + self, + matrix: MatrixMN, + *, + about_point: Point3DLike | None = None, + about_edge: Vector3DLike | None = None, + ) -> Self: + """Applies a matrix to every point. + + Parameters + ---------- + matrix : MatrixMN + The matrix to apply. + about_point : Point3DLike | None, optional + The point about which to apply the matrix., by default None + about_edge : Vector3DLike | None, optional + The edge about which to apply the matrix., by default None + + Returns + ------- + Self + The object itself. + """ + if about_point is None and about_edge is None: + about_point = ORIGIN + matrix = np.asarray(matrix) + full_matrix = np.identity(3) + full_matrix[: matrix.shape[0], : matrix.shape[1]] = matrix + + return self.apply_array_function( + function=lambda points: points.dot(full_matrix.T, out=points), + about_point=about_point, + about_edge=about_edge, + ) + + ### GENERAL ### + + def get_position( + self, + direction: Vector3DLike = ORIGIN, + ) -> Point3D: + """The position. + + Parameters + ---------- + direction : Vector3DLike, optional + The direction., by default ORIGIN + + Returns + ------- + Point3D + The position. + + See also + -------- + :meth:`set_position` + """ + points = self.get_points_defining_boundary() + return np.array( + [ + self._get_extremum_along_dim( + points=points, + dim=dim, + key=key, + ) + for dim, key in enumerate(direction) + ] + ) + + def set_position( + self, + point: Point3DLike | Positionable, + *, + aligned_edge: Vector3DLike = ORIGIN, + ) -> Self: + """Sets the position. + + Parameters + ---------- + point : Point3DLike | Positionable + The point. + aligned_edge : Vector3DLike, optional + Which edge to position., by default ORIGIN + + Returns + ------- + Self + The object itself. + + See also + -------- + :meth:`get_position` + """ + if isinstance(point, Positionable): + point = point.get_position(direction=aligned_edge) + current = self.get_position(direction=aligned_edge) + vector = point - current + return self.translate(vector=vector) + + def get_center(self) -> Point3D: + """Returns the center position. + + Returns + ------- + Point3D + The center position. + + See also + -------- + :meth:`set_center`, :meth:`get_position` + """ + return self.get_position(direction=ORIGIN) + + def set_center( + self, + center: Point3DLike | Positionable, + ) -> Self: + """Sets the center position. + + Parameters + ---------- + center : Point3DLike | Positionable + The center position. + + Returns + ------- + Self + The object itself. + + See also + -------- + :meth:`get_center`, :meth:`set_position` + """ + return self.set_position(point=center, aligned_edge=ORIGIN) + + def get_left(self) -> Point3D: + """Returns the left position. + + Returns + ------- + Point3D + The left position. + + See also + -------- + :meth:`set_left`, :meth:`get_position` + """ + return self.get_position(direction=LEFT) + + def set_left( + self, + left: Point3DLike | Positionable, + ) -> Self: + """Sets the left position. + + Parameters + ---------- + left : Point3DLike | Positionable + The left position. + + Returns + ------- + Self + The object itself. + + See also + -------- + :meth:`get_left`, :meth:`set_position` + """ + return self.set_position(point=left, aligned_edge=LEFT) + + def get_right(self) -> Point3D: + """Returns the right position. + + Returns + ------- + Point3D + The right position. + + See also + -------- + :meth:`set_right`, :meth:`get_position` + """ + return self.get_position(direction=RIGHT) + + def set_right( + self, + right: Point3DLike | Positionable, + ) -> Self: + """Sets the right position. + + Parameters + ---------- + right : Point3DLike | Positionable + The right position. + + Returns + ------- + Self + The object itself. + + See also + -------- + :meth:`get_right`, :meth:`set_position` + """ + return self.set_position(point=right, aligned_edge=RIGHT) + + def get_bottom(self) -> Point3D: + """Returns the bottom position. + + Returns + ------- + Point3D + The bottom position. + + See also + -------- + :meth:`set_bottom`, :meth:`get_position` + """ + return self.get_position(direction=DOWN) + + def set_bottom( + self, + bottom: Point3DLike | Positionable, + ) -> Self: + """Sets the bottom position. + + Parameters + ---------- + bottom : Point3DLike | Positionable + The bottom position. + + Returns + ------- + Self + The object itself. + + See also + -------- + :meth:`get_bottom`, :meth:`set_position` + """ + return self.set_position(point=bottom, aligned_edge=DOWN) + + def get_top(self) -> Point3D: + """Returns the top position. + + Returns + ------- + Point3D + The top position. + + See also + -------- + :meth:`set_top`, :meth:`get_position` + """ + return self.get_position(direction=UP) + + def set_top( + self, + top: Point3DLike | Positionable, + ) -> Self: + """Sets the top position. + + Parameters + ---------- + top : Point3DLike | Positionable + The top position. + + Returns + ------- + Self + The object itself. + + See also + -------- + :meth:`get_top`, :meth:`set_position` + """ + return self.set_position(point=top, aligned_edge=UP) + + def get_nadir(self) -> Point3D: + """Returns the nadir position. + + Returns + ------- + Point3D + The nadir position. + + See also + -------- + :meth:`set_nadir`, :meth:`get_position` + """ + return self.get_position(direction=IN) + + def set_nadir( + self, + nadir: Point3DLike | Positionable, + ) -> Self: + """Sets the nadir position. + + Parameters + ---------- + nadir : Point3DLike | Positionable + The nadir position. + + Returns + ------- + Self + The object itself. + + See also + -------- + :meth:`get_nadir`, :meth:`set_position` + """ + return self.set_position(point=nadir, aligned_edge=IN) + + def get_zenith(self) -> Point3D: + """Returns the zenith position. + + Returns + ------- + Point3D + The zenith position. + + See also + -------- + :meth:`set_zenith`, :meth:`get_position` + """ + return self.get_position(direction=OUT) + + def set_zenith( + self, + zenith: Point3DLike | Positionable, + ) -> Self: + """Sets the zenith position. + + Parameters + ---------- + zenith : Point3DLike | Positionable + The zenith position. + + Returns + ------- + Self + The object itself. + + See also + -------- + :meth:`get_zenith`, :meth:`set_position` + """ + return self.set_position(point=zenith, aligned_edge=OUT) + + def get_coordinate( + self, + dim: int, + direction: Vector3DLike = ORIGIN, + ) -> float: + """Returns the coordinate of a dimension. + + Parameters + ---------- + dim : int + The dimension. + direction : Vector3DLike, optional + The direction., by default ORIGIN + + Returns + ------- + float + The coordinate. + + See also + -------- + :meth:`set_coordinate` + """ + return self._get_extremum_along_dim( + points=self.get_points_defining_boundary(), + dim=dim, + key=np.sign(direction[dim]), + ) + + def set_coordinate( + self, + coordinate: float | Positionable, + dim: int, + direction: Vector3DLike = ORIGIN, + ) -> Self: + """Sets the coordinate of a dimension. + + Parameters + ---------- + value : float | Positionable + The coordinate. + dim : int + The dimension. + direction : Vector3DLike, optional + The direction., by default ORIGIN + + Returns + ------- + Self + The object itself. + + See also + -------- + :meth:`get_coordinate` + """ + if isinstance(coordinate, Positionable): + coordinate = coordinate.get_coordinate(dim=dim, direction=direction) + current = self.get_coordinate(dim=dim, direction=direction) + vector = np.zeros(3) + vector[dim] = coordinate - current + return self.translate(vector=vector) + + def get_x(self, direction: Vector3DLike = ORIGIN) -> float: + """Returns the x coordinate. + + Parameters + ---------- + direction : Vector3DLike, optional + The direction., by default ORIGIN + + Returns + ------- + float + The x coordinate. + + See also + -------- + :meth:`set_x`, :meth:`get_coordinate` + """ + return self.get_coordinate(dim=0, direction=direction) + + def set_x(self, x: float | Positionable, direction: Vector3DLike = ORIGIN) -> Self: + """Sets the x coordinate. + + Parameters + ---------- + x : float | Positionable + The x coordinate. + direction : Vector3DLike, optional + The direction., by default ORIGIN + + Returns + ------- + Self + The object itself. + + See also + -------- + :meth:`get_x`, :meth:`set_coordinate` + """ + return self.set_coordinate(coordinate=x, dim=0, direction=direction) + + def get_y(self, direction: Vector3DLike = ORIGIN) -> float: + """Returns the y coordinate. + + Parameters + ---------- + direction : Vector3DLike, optional + The direction., by default ORIGIN + + Returns + ------- + float + The y coordinate. + + See also + -------- + :meth:`set_y`, :meth:`get_coordinate` + """ + return self.get_coordinate(dim=1, direction=direction) + + def set_y(self, y: float | Positionable, direction: Vector3DLike = ORIGIN) -> Self: + """Sets the y coordinate. + + Parameters + ---------- + y : float | Positionable + The y coordinate. + direction : Vector3DLike, optional + The direction., by default ORIGIN + + Returns + ------- + Self + The object itself. + + See also + -------- + :meth:`get_y`, :meth:`set_coordinate` + """ + return self.set_coordinate(coordinate=y, dim=1, direction=direction) + + def get_z(self, direction: Vector3DLike = ORIGIN) -> float: + """Returns the z coordinate. + + Parameters + ---------- + direction : Vector3DLike, optional + The direction., by default ORIGIN + + Returns + ------- + float + The z coordinate. + + See also + -------- + :meth:`set_z`, :meth:`get_coordinate` + """ + return self.get_coordinate(dim=2, direction=direction) + + def set_z(self, z: float | Positionable, direction: Vector3DLike = ORIGIN) -> Self: + """Sets the z coordinate. + + Parameters + ---------- + z : float | Positionable + The z coordinate. + direction : Vector3DLike, optional + The direction., by default ORIGIN + + Returns + ------- + Self + The object itself. + + See also + -------- + :meth:`get_z`, :meth:`get_coordinate` + """ + return self.set_coordinate(coordinate=z, dim=2, direction=direction) + + def get_dim_size(self, dim: int) -> float: + """Returns the size of a dimension. + + Parameters + ---------- + dim : int + The dimension. + + Returns + ------- + float + The size of the dimension. + + See also + -------- + :meth:`set_dim_size` + """ + # TODO: Changing this to `get_boundary_points` breaks the `test_img_and_svg.py`` tests + points = self.get_all_points() + if len(points) == 0: + return 0 + return np.ptp(points[:, dim]) # type: ignore[no-any-return] + + def set_dim_size( + self, + size: float | Positionable, + dim: int, + *, + stretch: bool = False, + about_point: Point3DLike | None = None, + about_edge: Vector3DLike | None = None, + ) -> Self: + """Sets the size of a dimension. + + Parameters + ---------- + size : float | Positionable + The size. + dim : int + The dimension. + stretch : bool, optional + Whether to use non-uniform or uniform scaling., by default False + about_point : Point3DLike | None, optional + The point about which the scaling is applied., by default None + about_edge : Vector3DLike | None, optional + The edge about which the scaling is applied., by default None + + Returns + ------- + Self + The object itself. + + See also + -------- + :meth:`get_dim_size`, :meth:`scale`, :meth:`stretch` + """ + if isinstance(size, Positionable): + size = size.get_dim_size(dim=dim) + + current_size = self.get_dim_size(dim=dim) + if current_size == 0: + return self + + factor = size / current_size + if stretch: + return self.stretch( + factor=factor, + dim=dim, + about_point=about_point, + about_edge=about_edge, + ) + else: + return self.scale( + factor=factor, + about_point=about_point, + about_edge=about_edge, + ) + + def get_width(self) -> float: + """Returns the width. + + Returns + ------- + float + The width. + + See also + -------- + :meth:`set_width`, :meth:`get_dim_size` + """ + return self.get_dim_size(dim=0) + + def set_width( + self, + width: float | Positionable, + *, + stretch: bool = False, + about_point: Point3DLike | None = None, + about_edge: Vector3DLike | None = None, + ) -> Self: + """Sets the width. + + Parameters + ---------- + width : float | Positionable + The width. + stretch : bool, optional + Whether to use non-uniform or uniform scaling., by default False + about_point : Point3DLike | None, optional + The point about which the scaling is applied., by default None + about_edge : Vector3DLike | None, optional + The edge about which the scaling is applied., by default None + + Returns + ------- + Self + The object itself. + + See also + -------- + :meth:`get_width`, :meth:`set_dim_size` + """ + return self.set_dim_size( + size=width, + dim=0, + stretch=stretch, + about_point=about_point, + about_edge=about_edge, + ) + + def get_height(self) -> float: + """Returns the height. + + Returns + ------- + float + The height. + + See also + -------- + :meth:`set_height`, :meth:`get_dim_size` + """ + return self.get_dim_size(dim=1) + + def set_height( + self, + height: float | Positionable, + *, + stretch: bool = False, + about_point: Point3DLike | None = None, + about_edge: Vector3DLike | None = None, + ) -> Self: + """Sets the height. + + Parameters + ---------- + height : float | Positionable + The height. + stretch : bool, optional + Whether to use non-uniform or uniform scaling., by default False + about_point : Point3DLike | None, optional + The point about which the scaling is applied., by default None + about_edge : Vector3DLike | None, optional + The edge about which the scaling is applied., by default None + + Returns + ------- + Self + The object itself. + + + See also + -------- + :meth:`get_height`, :meth:`set_dim_size` + """ + return self.set_dim_size( + size=height, + dim=1, + stretch=stretch, + about_point=about_point, + about_edge=about_edge, + ) + + def get_depth(self) -> float: + """Returns the depth. + + Returns + ------- + float + The depth. + + + See also + -------- + :meth:`set_depth`, :meth:`get_dim_size` + """ + return self.get_dim_size(dim=2) + + def set_depth( + self, + depth: float | Positionable, + *, + stretch: bool = False, + about_point: Point3DLike | None = None, + about_edge: Vector3DLike | None = None, + ) -> Self: + """Sets the depth. + + Parameters + ---------- + depth : float | Positionable + The depth. + stretch : bool, optional + Whether to use non-uniform or uniform scaling., by default False + about_point : Point3DLike | None, optional + The point about which the scaling is applied., by default None + about_edge : Vector3DLike | None, optional + The edge about which the scaling is applied., by default None + + Returns + ------- + Self + The object itself. + + + See also + -------- + :meth:`get_depth`, :meth:`set_dim_size` + """ + return self.set_dim_size( + size=depth, + dim=2, + stretch=stretch, + about_point=about_point, + about_edge=about_edge, + ) + + ### SPECIALIZED ### + + def align_on_border( + self, + direction: Vector3DLike, + *, + buff: float = DEFAULT_MOBJECT_TO_EDGE_BUFFER, + ) -> Self: + """Aligns the object on a border. + + Parameters + ---------- + direction : Vector3DLike + Which border to align to. + buff : float, optional + The buff., by default DEFAULT_MOBJECT_TO_EDGE_BUFFER + frame : Point3DLike | None, optional + The frame., by default None + + Returns + ------- + Self + The object itself. + + See also + -------- + :meth:`align_to` + """ + frame = (config.frame_x_radius, config.frame_y_radius, 0) + target = np.sign(direction) * frame - buff * np.asarray(direction) + return self.align_to(target, direction=direction) + + def align_to( + self, + point: Point3DLike | Positionable, + direction: Vector3DLike = ORIGIN, + ) -> Self: + """Aligns the object onto a point. + + Parameters + ---------- + mobject_or_point : Point3DLike | Positionable + The point. + direction : Vector3DLike, optional + The direction., by default ORIGIN + + Returns + ------- + Self + The object itself. + + Examples: + mob1.align_to(mob2, UP) moves mob1 vertically so that its + top edge lines ups with mob2's top edge. + """ + if isinstance(point, Positionable): + point = point.get_position(direction=direction) + source = self.get_position(direction=direction) + target = np.where(direction == 0, source, point) + return self.translate(target - source) + + def center(self) -> Self: + """Centers the object. + + Returns + ------- + Self + The object itself. + + See also + -------- + :meth:`set_center` + """ + return self.set_center(center=ORIGIN) + + def flip( + self, + axis: Vector3DLike = UP, + *, + about_point: Point3DLike | None = None, + about_edge: Vector3DLike | None = None, + ) -> Self: + """Flips. + + Parameters + ---------- + axis : Vector3DLike, optional + The axis about which to flip., by default UP + about_point : Point3DLike | None, optional + The point about which to flip., by default None + about_edge : Vector3DLike | None, optional + The edge about which to flip., by default None + + Returns + ------- + Self + The object itself. + + Examples + -------- + + .. manim:: FlipExample + :save_last_frame: + + class FlipExample(Scene): + def construct(self): + s= Line(LEFT, RIGHT+UP).shift(4*LEFT) + self.add(s) + s2= s.copy().flip() + self.add(s2) + + See also + -------- + :meth:`rotate` + """ + return self.rotate( + TAU / 2, + axis, + about_point=about_point, + about_edge=about_edge, + ) + + def get_bounding_box(self) -> tuple[Point3D, Point3D]: + """Returns the bounding box. + + Returns + ------- + tuple[Point3D, Point3D] + The bottom-left and top-right points. + """ + points = self.get_points_defining_boundary() + if len(points) == 0: + return (np.zeros(3), np.zeros(3)) + mins = points.min(axis=0) + maxs = points.max(axis=0) + return (mins, maxs) + + def get_center_of_mass(self) -> Point3D: + """Returns the center of mass. + + Returns + ------- + Point3D + The center of mass. + """ + points = self.get_all_points() + if len(points) == 0: + return ORIGIN + return points.mean(axis=0) + + def get_boundary_point(self, direction: Vector3DLike) -> Point3D: + """Returns a boundary point. + + Parameters + ---------- + direction : Vector3DLike + The direction. + + Returns + ------- + Point3D + The boundary point. + """ + points = self.get_points_defining_boundary() + index = np.argmax(points.dot(direction)) + return points[index] + + def get_extremum_along_dim( + self, + dim: int = 0, + key: int = 0, + ) -> float: + """Returns the extremum value along a dimension. + + Parameters + ---------- + dim, optional + The dimension, by default 0 + key, optional + The key., by default 0 + + Returns + ------- + The value. + """ + return self._get_extremum_along_dim( + self.get_points_defining_boundary(), + dim=dim, + key=key, + ) + + def _get_extremum_along_dim( + self, + points: Point3D_Array, + dim: int = 0, + key: int = 0, + ) -> float: + if len(points) == 0: + return 0 + values = points[:, dim] + return ( # type: ignore[no-any-return] + values.min() + if key < 0 + else (values.min() + values.max()) / 2 + if key == 0 + else values.max() + ) + + def is_off_screen(self) -> bool: + """Returns whether this is off screen. + + Returns + ------- + bool + Is off screen. + """ + points = self.get_points_defining_boundary() + return ( + # left is too right + ( + self._get_extremum_along_dim(points=points, dim=0, key=-1) + > config.frame_x_radius + ) + # right is too left + or ( + self._get_extremum_along_dim(points=points, dim=0, key=1) + < -config.frame_x_radius + ) + # bottom is too high + or ( + self._get_extremum_along_dim(points=points, dim=1, key=-1) + > config.frame_y_radius + ) + # top is too low + or ( + self._get_extremum_along_dim(points=points, dim=1, key=1) + < -config.frame_y_radius + ) + ) + + def pose_at_angle( + self, + *, + about_point: Point3DLike | None = None, + about_edge: Vector3DLike | None = None, + ) -> Self: + """Poses at angle. + + Parameters + ---------- + about_point : Point3DLike | None, optional + The point about which to pose., by default None + about_edge : Vector3DLike | None, optional + The edge about which to pose., by default None + + Returns + ------- + Self + The object itself. + + See also + -------- + :meth:`rotate` + """ + return self.rotate( + angle=TAU / 14, + axis=RIGHT + UP, + about_point=about_point, + about_edge=about_edge, + ) + + def reduce_across_dimension( + self, + function: Callable[[Iterable[float]], float], + dim: int, + ) -> float | None: + points = self.get_points_defining_boundary() + if len(points) == 0: + return None + + return function(points[:, dim]) + + def shift_onto_screen( + self, + *, + buff: float = DEFAULT_MOBJECT_TO_EDGE_BUFFER, + ) -> Self: + """Shifts onto screen. + + Parameters + ---------- + buff : float, optional + The buff., by default DEFAULT_MOBJECT_TO_EDGE_BUFFER + + Returns + ------- + Self + The object itself. + """ + # TODO: Simplify/Optimize implementation + frame = (config.frame_x_radius, config.frame_y_radius) + for edge in UP, DOWN, LEFT, RIGHT: + dim = np.argmax(np.abs(edge)) + max_val = frame[dim] - buff + edge_center = self.get_position(edge) + if np.dot(edge_center, edge) > max_val: + self.to_edge(edge, buff=buff) + return self + + def scale_to_fit( + self, + size: float, + dim: int, + *, + about_point: Point3DLike | None = None, + about_edge: Vector3DLike | None = None, + ) -> Self: + """Scales to fit a size for a dimension. + + Parameters + ---------- + size : float + The size. + dim : int + The dimension. + about_point : Point3DLike | None, optional + The point about which to scale., by default None + about_edge : Vector3DLike | None, optional + The edge about which to scale., by default None + + Returns + ------- + Self + The object itself. + + See also + -------- + :meth:`set_dim_size`, :meth:`scale` + """ + return self.set_dim_size( + size=size, + dim=dim, + stretch=False, + about_point=about_point, + about_edge=about_edge, + ) + + def scale_to_fit_width( + self, + width: float, + *, + about_point: Point3DLike | None = None, + about_edge: Vector3DLike | None = None, + ) -> Self: + """Scales to fit a width. + + Parameters + ---------- + width : float + The width. + about_point : Point3DLike | None, optional + The point about which scale., by default None + about_edge : Vector3DLike | None, optional + The point about which to scale., by default None + + Returns + ------- + Self + The object itself. + + Examples + -------- + :: + + >>> from manim import * + >>> sq = Square() + >>> sq.height + np.float64(2.0) + >>> sq.scale_to_fit_width(5) + Square + >>> sq.width + np.float64(5.0) + >>> sq.height + np.float64(5.0) + + See also + -------- + :meth:`scale_to_fit`, :meth:`scale`, :meth:`set_width` + """ + return self.scale_to_fit( + size=width, + dim=0, + about_point=about_point, + about_edge=about_edge, + ) + + def scale_to_fit_height( + self, + height: float, + *, + about_point: Point3DLike | None = None, + about_edge: Vector3DLike | None = None, + ) -> Self: + """Scales to fit a jeight. + + Parameters + ---------- + height : float + The height. + about_point : Point3DLike | None, optional + The point about which scale., by default None + about_edge : Vector3DLike | None, optional + The point about which to scale., by default None + + Returns + ------- + Self + The object itself. + + Examples + -------- + :: + + >>> from manim import * + >>> sq = Square() + >>> sq.width + np.float64(2.0) + >>> sq.scale_to_fit_height(5) + Square + >>> sq.height + np.float64(5.0) + >>> sq.width + np.float64(5.0) + + See also + -------- + :meth:`scale_to_fit`, :meth:`scale`, :meth:`set_height` + """ + return self.scale_to_fit( + size=height, + dim=1, + about_point=about_point, + about_edge=about_edge, + ) + + def scale_to_fit_depth( + self, + depth: float, + *, + about_point: Point3DLike | None = None, + about_edge: Vector3DLike | None = None, + ) -> Self: + """Scales to fit a depth. + + Parameters + ---------- + depth : float + The depth. + about_point : Point3DLike | None, optional + The point about which scale., by default None + about_edge : Vector3DLike | None, optional + The point about which to scale., by default None + + Returns + ------- + Self + The object itself. + + See also + -------- + :meth:`scale_to_fit`, :meth:`scale`, :meth:`set_depth` + """ + return self.scale_to_fit( + size=depth, + dim=2, + about_point=about_point, + about_edge=about_edge, + ) + + def stretch_to_fit( + self, + size: float, + dim: int, + about_point: Point3DLike | None = None, + about_edge: Vector3DLike | None = None, + ) -> Self: + """Stretches to fit the size of a dimension. + + Parameters + ---------- + size : float + The size. + dim : int + The dimension. + about_point : Point3DLike | None, optional + The point about which to stretch., by default None + about_edge : Vector3DLike | None, optional + The edge about which to stretch., by default None + + Returns + ------- + Self + The object itself. + + See also + -------- + :meth:`set_dim_size`, :meth:`stretch` + """ + return self.set_dim_size( + size=size, + dim=dim, + stretch=True, + about_point=about_point, + about_edge=about_edge, + ) + + def stretch_to_fit_width( + self, + width: float, + *, + about_point: Point3DLike | None = None, + about_edge: Vector3DLike | None = None, + ) -> Self: + """Stretches to fit a width. + + Parameters + ---------- + width : float + The width. + about_point : Point3DLike | None, optional + The point about which to stretch., by default None + about_edge : Vector3DLike | None, optional + The edge about which to stretch., by default None + + Returns + ------- + Self + The object itself. + + Examples + -------- + :: + + >>> from manim import * + >>> sq = Square() + >>> sq.height + np.float64(2.0) + >>> sq.stretch_to_fit_width(5) + Square + >>> sq.width + np.float64(5.0) + >>> sq.height + np.float64(2.0) + + See also + -------- + :meth:`stretch_to_fit`, :meth:`stretch`, :meth:`set_width` + """ + return self.stretch_to_fit( + size=width, + dim=0, + about_point=about_point, + about_edge=about_edge, + ) + + def stretch_to_fit_height( + self, + height: float, + *, + about_point: Point3DLike | None = None, + about_edge: Vector3DLike | None = None, + ) -> Self: + """Stretches to fit a height. + + Parameters + ---------- + height : float + The height. + about_point : Point3DLike | None, optional + The point about which to stretch., by default None + about_edge : Vector3DLike | None, optional + The edge about which to stretch., by default None + + Returns + ------- + Self + The object itself. + + Examples + -------- + :: + + >>> from manim import * + >>> sq = Square() + >>> sq.width + np.float64(2.0) + >>> sq.stretch_to_fit_height(5) + Square + >>> sq.height + np.float64(5.0) + >>> sq.width + np.float64(2.0) + + See also + -------- + :meth:`stretch_to_fit`, :meth:`stretch`, :meth:`set_height` + """ + return self.stretch_to_fit( + size=height, + dim=1, + about_point=about_point, + about_edge=about_edge, + ) + + def stretch_to_fit_depth( + self, + depth: float, + *, + about_point: Point3DLike | None = None, + about_edge: Vector3DLike | None = None, + ) -> Self: + """Stretches to fit a depth. + + Parameters + ---------- + depth : float + The depth. + about_point : Point3DLike | None, optional + The point about which to stretch., by default None + about_edge : Vector3DLike | None, optional + The edge about which to stretch., by default None + + Returns + ------- + Self + The object itself. + + See also + -------- + :meth:`stretch_to_fit`, :meth:`stretch`, :meth:`set_depth` + """ + return self.stretch_to_fit( + size=depth, + dim=2, + about_point=about_point, + about_edge=about_edge, + ) + + ### DEPRECATIONS ### + # @deprecated(replacement="set_position(function(self.get_position()))") + def apply_function_to_position( + self, + function: Callable[[Point3D], Point3DLike], + ) -> Self: + return self.move_to(function(self.get_center())) + + # @deprecated(replacement="apply_array_function") + def apply_points_function_about_point( + self, + function: Callable[[Point3D_Array], Point3D_Array], + *, + about_point: Point3DLike | None = None, + about_edge: Vector3DLike | None = None, + ) -> Self: + return self.apply_array_function( + function=function, + about_point=about_point, + about_edge=about_edge, + ) + + @property + # @deprecated(replacement="(get|set)_depth") + def depth(self) -> float: + """The width. + + See also + -------- + :meth:`get_depth`, :meth:`set_depth` + """ + return self.get_depth() + + @depth.setter + # @deprecated(replacement="(get|set)_depth") + def depth(self, value: float) -> None: + self.set_depth(depth=value) + + # @deprecated(replacement="get_position") + def get_corner(self: Self, direction: Vector3DLike = ORIGIN) -> Point3D: + return self.get_position(direction=direction) + + # @deprecated(replacement="get_coordinate") + def get_coord(self, dim: int, direction: Vector3DLike = ORIGIN) -> float: + return self.get_coordinate(dim=dim, direction=direction) + + # @deprecated(replacement="get_position") + def get_critical_point(self: Self, direction: Vector3DLike = ORIGIN) -> Point3D: + return self.get_position(direction=direction) + + # @deprecated(replacement="get_position") + def get_edge_center(self: Self, direction: Vector3DLike = ORIGIN) -> Point3D: + return self.get_position(direction=direction) + + @property + # @deprecated(replacement="(get|set)_height") + def height(self) -> float: + """The height. + + Examples + -------- + .. manim:: HeightExample + + class HeightExample(Scene): + def construct(self): + decimal = DecimalNumber().to_edge(UP) + rect = Rectangle(color=BLUE) + rect_copy = rect.copy().set_stroke(GRAY, opacity=0.5) + + decimal.add_updater(lambda d: d.set_value(rect.height)) + + self.add(rect_copy, rect, decimal) + self.play(rect.animate.set(height=5)) + self.wait() + + See also + -------- + :meth:`get_height`, :meth:`set_height` + """ + return self.get_height() + + @height.setter + # @deprecated(replacement="(get|set)_height") + def height(self, value: float) -> None: + self.set_height(height=value) + + # @deprecated(replacement="get_dim_size") + def length_over_dim(self, dim: int) -> float: + return self.get_dim_size(dim=dim) + + # @deprecated(replacement="set_coord") + def match_coord( + self, + mobject: Positionable, + dim: int, + direction: Vector3DLike = ORIGIN, + ) -> Self: + return self.set_coordinate( + coordinate=mobject, + dim=dim, + direction=direction, + ) + + # @deprecated(replacement="set_dim_size") + def match_dim_size( + self, + mobject: Positionable, + dim: int, + *, + stretch: bool = False, + about_point: Point3DLike | None = None, + about_edge: Vector3DLike | None = None, + ) -> Self: + return self.set_dim_size( + size=mobject, + dim=dim, + stretch=stretch, + about_point=about_point, + about_edge=about_edge, + ) + + # @deprecated(replacement="set_depth") + def match_depth( + self, + mobject: Positionable, + *, + stretch: bool = False, + about_point: Point3DLike | None = None, + about_edge: Vector3DLike | None = None, + ) -> Self: + return self.set_depth( + depth=mobject, + stretch=stretch, + about_point=about_point, + about_edge=about_edge, + ) + + # @deprecated(replacement="set_points") + def match_points(self: Self, mobject: Point3DLike_Array | Positionable) -> Self: + return self.set_points(points=mobject) + + # @deprecated(replacement="set_x") + def match_x( + self, + mobject: float | Positionable, + direction: Vector3DLike = ORIGIN, + ) -> Self: + return self.set_x(x=mobject, direction=direction) + + # @deprecated(replacement="set_y") + def match_y( + self, + mobject: float | Positionable, + direction: Vector3DLike = ORIGIN, + ) -> Self: + return self.set_y(y=mobject, direction=direction) + + # @deprecated(replacement="set_z") + def match_z( + self, + mobject: float | Positionable, + direction: Vector3DLike = ORIGIN, + ) -> Self: + return self.set_z(z=mobject, direction=direction) + + # @deprecated(replacement="set_width") + def match_width( + self, + mobject: Positionable, + *, + stretch: bool = False, + about_point: Point3DLike | None = None, + about_edge: Vector3DLike | None = None, + ) -> Self: + return self.set_width( + width=mobject, + stretch=stretch, + about_point=about_point, + about_edge=about_edge, + ) + + # @deprecated(replacement="set_height") + def match_height( + self, + mobject: Positionable, + *, + stretch: bool = False, + about_point: Point3DLike | None = None, + about_edge: Vector3DLike | None = None, + ) -> Self: + return self.set_height( + height=mobject, + stretch=stretch, + about_point=about_point, + about_edge=about_edge, + ) + + # @deprecated(replacement="set_position") + def move_to( + self, + point: Point3DLike | Positionable, + aligned_edge: Vector3DLike = ORIGIN, + ) -> Self: + """Moves to a position. + + Parameters + ---------- + point_or_mobject : Point3DLike | Positionable + The point. + aligned_edge : Vector3DLike, optional + Which edge to position., by default ORIGIN + + Returns + ------- + Self + The object itself. + + See also + -------- + :meth:`set_position` + """ + return self.set_position( + point=point, + aligned_edge=aligned_edge, + ) + + # @deprecated(replacement="rotate") + def rotate_about_origin( + self, + angle: float, + axis: Vector3DLike = OUT, + ) -> Self: + return self.rotate( + angle=angle, + axis=axis, + about_point=ORIGIN, + ) + + # @deprecated(replacement="set_dim_size") + def rescale_to_fit( + self, + length: float | Positionable, + dim: int, + *, + stretch: bool = False, + about_point: Point3DLike | None = None, + about_edge: Vector3DLike | None = None, + ) -> Self: + return self.set_dim_size( + size=length, + dim=dim, + stretch=stretch, + about_point=about_point, + about_edge=about_edge, + ) + + # @deprecated(replacement="set_coordinate") + def set_coord( + self, + value: float | Positionable, + dim: int, + direction: Vector3DLike = ORIGIN, + ) -> Self: + return self.set_coordinate(coordinate=value, dim=dim, direction=direction) + + # @deprecated(replacement="translate") + def shift(self, *vectors: Vector3DLike) -> Self: + """_summary_ + + Parameters + ---------- + vectors: *Vector3DLike + The vectors. + + Returns + ------- + Self + The object itself. + """ + return self.translate(vector=reduce(op.add, vectors)) + + # @deprecated(replacement="stretch") + def stretch_about_point(self, factor: float, dim: int, point: Point3DLike) -> Self: + return self.stretch(factor=factor, dim=dim, about_point=point) + + # @deprecated(replacement="align_on_border") + def to_corner( + self, + corner: Vector3DLike = DL, + *, + buff: float = DEFAULT_MOBJECT_TO_EDGE_BUFFER, + ) -> Self: + """Aligns to a corner. + + Parameters + ---------- + corner : Vector3DLike, optional + The corner., by default DL + buff : float, optional + The buff., by default DEFAULT_MOBJECT_TO_EDGE_BUFFER + + Returns + ------- + Self + The object itself. + + Examples + -------- + + .. manim:: ToCornerExample + :save_last_frame: + + class ToCornerExample(Scene): + def construct(self): + c = Circle() + c.to_corner(UR) + t = Tex("To the corner!") + t2 = MathTex("x^3").shift(DOWN) + self.add(c,t,t2) + t.to_corner(DL, buff=0) + t2.to_corner(UL, buff=1.5) + + See also + -------- + :meth:`align_on_border` + """ + return self.align_on_border(direction=corner, buff=buff) + + # @deprecated(replacement="align_on_border") + def to_edge( + self, + edge: Vector3DLike = LEFT, + *, + buff: float = DEFAULT_MOBJECT_TO_EDGE_BUFFER, + ) -> Self: + """Aligns to an edge. + + Parameters + ---------- + edge : Vector3DLike, optional + The edge., by default LEFT + buff : float, optional + The buff., by default DEFAULT_MOBJECT_TO_EDGE_BUFFER + + Returns + ------- + Self + The object itself. + + Examples + -------- + + .. manim:: ToEdgeExample + :save_last_frame: + + class ToEdgeExample(Scene): + def construct(self): + tex_top = Tex("I am at the top!") + tex_top.to_edge(UP) + tex_side = Tex("I am moving to the side!") + c = Circle().shift(2*DOWN) + self.add(tex_top, tex_side, c) + tex_side.to_edge(LEFT) + c.to_edge(RIGHT, buff=0) + + See also + -------- + :meth:`align_on_border` + """ + return self.align_on_border(direction=edge, buff=buff) + + @property + # @deprecated(replacement="(get|set)_width") + def width(self) -> float: + """The width. + + Examples + -------- + .. manim:: WidthExample + + class WidthExample(Scene): + def construct(self): + decimal = DecimalNumber().to_edge(UP) + rect = Rectangle(color=BLUE) + rect_copy = rect.copy().set_stroke(GRAY, opacity=0.5) + + decimal.add_updater(lambda d: d.set_value(rect.width)) + + self.add(rect_copy, rect, decimal) + self.play(rect.animate.set(width=7)) + self.wait() + + See also + -------- + :meth:`get_width`, :meth:`set_width` + """ + return self.get_width() + + @width.setter + # @deprecated(replacement="(get|set)_width") + def width(self, value: float) -> None: + self.set_width(width=value) diff --git a/manim/mobject/geometry/line.py b/manim/mobject/geometry/line.py index b085cdb5a4..bf01c53697 100644 --- a/manim/mobject/geometry/line.py +++ b/manim/mobject/geometry/line.py @@ -607,7 +607,14 @@ def __init__( self.add_tip(tip_shape=tip_shape) self._set_stroke_width_from_length() - def scale(self, factor: float, scale_tips: bool = False, **kwargs: Any) -> Self: # type: ignore[override] + def scale( + self, + factor: float, + scale_tips: bool = False, + *, + about_point: Point3DLike | None = None, + about_edge: Vector3DLike | None = None, + ) -> Self: r"""Scale an arrow, but keep stroke width and arrow tip size fixed. @@ -639,7 +646,12 @@ def scale(self, factor: float, scale_tips: bool = False, **kwargs: Any) -> Self: return self if scale_tips: - super().scale(factor, **kwargs) + super().scale( + factor, + scale_tips, + about_point=about_point, + about_edge=about_edge, + ) self._set_stroke_width_from_length() return self @@ -648,7 +660,12 @@ def scale(self, factor: float, scale_tips: bool = False, **kwargs: Any) -> Self: if has_tip or has_start_tip: old_tips = self.pop_tips() - super().scale(factor, **kwargs) + super().scale( + factor, + scale_tips, + about_point=about_point, + about_edge=about_edge, + ) self._set_stroke_width_from_length() if has_tip: diff --git a/manim/mobject/logo.py b/manim/mobject/logo.py index 505ac5f4f7..d40a3bded2 100644 --- a/manim/mobject/logo.py +++ b/manim/mobject/logo.py @@ -4,7 +4,7 @@ __all__ = ["ManimBanner"] -from typing import Any, Self +from typing import Self import svgelements as se @@ -12,7 +12,7 @@ from manim.mobject.geometry.arc import Circle from manim.mobject.geometry.polygram import Square, Triangle from manim.mobject.mobject import Mobject -from manim.typing import Vector3D +from manim.typing import Point3DLike, Vector3D, Vector3DLike from .. import constants as cst from ..animation.animation import override_animation @@ -184,7 +184,14 @@ def __init__(self, dark_theme: bool = True): # and thus not yet added to the submobjects of self. self.anim = anim - def scale(self, scale_factor: float, **kwargs: Any) -> Self: + def scale( + self, + factor: float, + scale_stroke: bool = False, + *, + about_point: Point3DLike | None = None, + about_edge: Vector3DLike | None = None, + ) -> Self: """Scale the banner by the specified scale factor. Parameters @@ -197,11 +204,18 @@ def scale(self, scale_factor: float, **kwargs: Any) -> Self: :class:`~.ManimBanner` The scaled banner. """ - self.scale_factor *= scale_factor + self.scale_factor *= factor # Note: self.anim is only added to self after expand() if self.anim not in self.submobjects: - self.anim.scale(scale_factor, **kwargs) - return super().scale(scale_factor, **kwargs) + self.anim.scale( + factor, + scale_stroke, + about_point=about_point, + about_edge=about_edge, + ) + return super().scale( + factor, scale_stroke, about_point=about_point, about_edge=about_edge + ) @override_animation(Create) def create(self, run_time: float = 2) -> AnimationGroup: diff --git a/manim/mobject/mobject.py b/manim/mobject/mobject.py index 5d52899c7a..a2f7cc1ee7 100644 --- a/manim/mobject/mobject.py +++ b/manim/mobject/mobject.py @@ -2,14 +2,10 @@ from __future__ import annotations -__all__ = ["Mobject", "Group", "override_animate"] - - import copy import inspect import itertools as it import math -import operator as op import random import sys import types @@ -22,6 +18,7 @@ import numpy as np from manim.data_structures import MethodWithArgs +from manim.mobject.abstract.positionable import Positionable from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL from .. import config, logger @@ -38,7 +35,7 @@ from ..utils.exceptions import MultiAnimationOverrideException from ..utils.iterables import list_update, remove_list_redundancies from ..utils.paths import straight_path -from ..utils.space_ops import angle_between_vectors, normalize, rotation_matrix +from ..utils.space_ops import angle_between_vectors, normalize if TYPE_CHECKING: from typing import Self, TypeAlias @@ -49,13 +46,11 @@ from manim.typing import ( FunctionOverride, MappingFunction, - MatrixMN, MultiMappingFunction, PathFuncType, Point3D, Point3D_Array, Point3DLike, - Point3DLike_Array, Vector3D, Vector3DLike, ) @@ -64,12 +59,15 @@ from ..camera.camera import Camera +__all__ = ["Mobject", "Group", "override_animate"] + + _TimeBasedUpdater: TypeAlias = Callable[["Mobject", float], object] _NonTimeBasedUpdater: TypeAlias = Callable[["Mobject"], object] _Updater: TypeAlias = _NonTimeBasedUpdater | _TimeBasedUpdater -class Mobject: +class Mobject(Positionable): """Mathematical Object: base class for objects that can be displayed on screen. There is a compatibility layer that allows for @@ -773,98 +771,6 @@ def setter(self: Mobject, value: Any) -> Mobject: # Unhandled attribute, therefore error raise AttributeError(f"{type(self).__name__} object has no attribute '{attr}'") - @property - def width(self) -> float: - """The width of the mobject. - - Returns - ------- - :class:`float` - - Examples - -------- - .. manim:: WidthExample - - class WidthExample(Scene): - def construct(self): - decimal = DecimalNumber().to_edge(UP) - rect = Rectangle(color=BLUE) - rect_copy = rect.copy().set_stroke(GRAY, opacity=0.5) - - decimal.add_updater(lambda d: d.set_value(rect.width)) - - self.add(rect_copy, rect, decimal) - self.play(rect.animate.set(width=7)) - self.wait() - - See also - -------- - :meth:`length_over_dim` - - """ - # Get the length across the X dimension - return self.length_over_dim(0) - - @width.setter - def width(self, value: float) -> None: - self.scale_to_fit_width(value) - - @property - def height(self) -> float: - """The height of the mobject. - - Returns - ------- - :class:`float` - - Examples - -------- - .. manim:: HeightExample - - class HeightExample(Scene): - def construct(self): - decimal = DecimalNumber().to_edge(UP) - rect = Rectangle(color=BLUE) - rect_copy = rect.copy().set_stroke(GRAY, opacity=0.5) - - decimal.add_updater(lambda d: d.set_value(rect.height)) - - self.add(rect_copy, rect, decimal) - self.play(rect.animate.set(height=5)) - self.wait() - - See also - -------- - :meth:`length_over_dim` - - """ - # Get the length across the Y dimension - return self.length_over_dim(1) - - @height.setter - def height(self, value: float) -> None: - self.scale_to_fit_height(value) - - @property - def depth(self) -> float: - """The depth of the mobject. - - Returns - ------- - :class:`float` - - See also - -------- - :meth:`length_over_dim` - - """ - # Get the length across the Z dimension - return self.length_over_dim(2) - - @depth.setter - def depth(self, value: float) -> None: - self.scale_to_fit_depth(value) - # Can't be staticmethod because of point_cloud_mobject.py def get_array_attrs(self) -> list[str]: return ["points"] @@ -1235,317 +1141,11 @@ def resume_updating(self, recursive: bool = True) -> Self: # Transforming operations - def apply_to_family(self, func: Callable[[Mobject], None]) -> Self: - """Apply a function to ``self`` and every submobject with points recursively. - - Parameters - ---------- - func - The function to apply to each mobject. ``func`` gets passed the respective - (sub)mobject as parameter. - - Returns - ------- - :class:`Mobject` - ``self`` - - See also - -------- - :meth:`family_members_with_points` - - """ - for mob in self.family_members_with_points(): - func(mob) - - return self - - def shift(self, *vectors: Vector3DLike) -> Self: - """Shift by the given vectors. - - Parameters - ---------- - vectors - Vectors to shift by. If multiple vectors are given, they are added - together. - - Returns - ------- - :class:`Mobject` - ``self`` - - See also - -------- - :meth:`move_to` - """ - total_vector = reduce(op.add, vectors) - for mob in self.family_members_with_points(): - mob.points = mob.points.astype("float") - mob.points += total_vector - - return self - - def scale( - self, - scale_factor: float, - *, - about_point: Point3DLike | None = None, - about_edge: Vector3DLike | None = None, - ) -> Self: - r"""Scale the size by a factor. - - Default behavior is to scale about the center of the mobject. - - Parameters - ---------- - scale_factor - The scaling factor :math:`\alpha`. If :math:`0 < |\alpha| < 1`, the mobject - will shrink, and for :math:`|\alpha| > 1` it will grow. Furthermore, - if :math:`\alpha < 0`, the mobject is also flipped. - about_point - The point about which to apply the scaling. - about_edge - The edge about which to apply the scaling. - - Returns - ------- - :class:`Mobject` - ``self`` - - Examples - -------- - - .. manim:: MobjectScaleExample - :save_last_frame: - - class MobjectScaleExample(Scene): - def construct(self): - f1 = Text("F") - f2 = Text("F").scale(2) - f3 = Text("F").scale(0.5) - f4 = Text("F").scale(-1) - - vgroup = VGroup(f1, f2, f3, f4).arrange(6 * RIGHT) - self.add(vgroup) - - See also - -------- - :meth:`move_to` - - """ - self.apply_points_function_about_point( - lambda points: scale_factor * points, about_point, about_edge - ) - return self - - def rotate_about_origin(self, angle: float, axis: Vector3DLike = OUT) -> Self: - """Rotates the :class:`~.Mobject` about the ORIGIN, which is at [0,0,0].""" - return self.rotate(angle, axis, about_point=ORIGIN) - - def rotate( - self, - angle: float, - axis: Vector3DLike = OUT, - *, - about_point: Point3DLike | None = None, - about_edge: Vector3DLike | None = None, - **kwargs: Any, - ) -> Self: - """Rotates the :class:`~.Mobject` around a specified axis and point. - - Parameters - ---------- - angle - The angle of rotation in radians. Predefined constants such as ``DEGREES`` - can also be used to specify the angle in degrees. - axis - The rotation axis (see :class:`~.Rotating` for more). - about_point - The point about which the mobject rotates. If ``None``, rotation occurs around - the center of the mobject. - about_edge - The edge about which to apply the scaling. - - Returns - ------- - :class:`Mobject` - ``self`` (for method chaining) - - - .. note:: - To animate a rotation, use :class:`~.Rotating` or :class:`~.Rotate` - instead of ``.animate.rotate(...)``. - The ``.animate.rotate(...)`` syntax only applies a transformation - from the initial state to the final rotated state - (interpolation between the two states), without showing proper rotational motion - based on the angle (from 0 to the given angle). - - Examples - -------- - - .. manim:: RotateMethodExample - :save_last_frame: - - class RotateMethodExample(Scene): - def construct(self): - circle = Circle(radius=1, color=BLUE) - line = Line(start=ORIGIN, end=RIGHT) - arrow1 = Arrow(start=ORIGIN, end=RIGHT, buff=0, color=GOLD) - group1 = VGroup(circle, line, arrow1) - - group2 = group1.copy() - arrow2 = group2[2] - arrow2.rotate(angle=PI / 4, about_point=arrow2.get_start()) - - group3 = group1.copy() - arrow3 = group3[2] - arrow3.rotate(angle=120 * DEGREES, about_point=arrow3.get_start()) - - self.add(VGroup(group1, group2, group3).arrange(RIGHT, buff=1)) - - See also - -------- - :class:`~.Rotating`, :class:`~.Rotate`, :attr:`~.Mobject.animate`, :meth:`apply_points_function_about_point` - - """ - rot_matrix = rotation_matrix(angle, axis) - self.apply_points_function_about_point( - lambda points: np.dot(points, rot_matrix.T), about_point, about_edge - ) - return self - - def flip( - self, - axis: Vector3DLike = UP, - *, - about_point: Point3DLike | None = None, - about_edge: Vector3DLike | None = None, - ) -> Self: - """Flips/Mirrors an mobject about its center. - - Examples - -------- - - .. manim:: FlipExample - :save_last_frame: - - class FlipExample(Scene): - def construct(self): - s= Line(LEFT, RIGHT+UP).shift(4*LEFT) - self.add(s) - s2= s.copy().flip() - self.add(s2) - - """ - return self.rotate( - TAU / 2, axis, about_point=about_point, about_edge=about_edge - ) - - def stretch( - self, - factor: float, - dim: int, - *, - about_point: Point3DLike | None = None, - about_edge: Vector3DLike | None = None, - ) -> Self: - def func(points: Point3D_Array) -> Point3D_Array: - points[:, dim] *= factor - return points - - self.apply_points_function_about_point(func, about_point, about_edge) - return self - - def apply_function( - self, - function: MappingFunction, - *, - about_point: Point3DLike | None = None, - about_edge: Vector3DLike | None = None, - ) -> Self: - # Default to applying matrix about the origin, not mobjects center - if about_point is None and about_edge is None: - about_point = ORIGIN - - def multi_mapping_function(points: Point3D_Array) -> Point3D_Array: - result: Point3D_Array = np.apply_along_axis(function, 1, points) - return result - - self.apply_points_function_about_point( - multi_mapping_function, - about_point, - about_edge, - ) - return self - - def apply_function_to_position(self, function: MappingFunction) -> Self: - self.move_to(function(self.get_center())) - return self - def apply_function_to_submobject_positions(self, function: MappingFunction) -> Self: for submob in self.submobjects: submob.apply_function_to_position(function) return self - def apply_matrix( - self, - matrix: MatrixMN, - *, - about_point: Point3DLike | None = None, - about_edge: Vector3DLike | None = None, - ) -> Self: - # Default to applying matrix about the origin, not mobjects center - if about_point is None and about_edge is None: - about_point = ORIGIN - full_matrix = np.identity(self.dim) - matrix = np.array(matrix) - full_matrix[: matrix.shape[0], : matrix.shape[1]] = matrix - self.apply_points_function_about_point( - lambda points: np.dot(points, full_matrix.T), about_point, about_edge - ) - return self - - def apply_complex_function( - self, - function: Callable[[complex], complex], - *, - about_point: Point3DLike | None = None, - about_edge: Vector3DLike | None = None, - ) -> Self: - """Applies a complex function to a :class:`Mobject`. - The x and y Point3Ds correspond to the real and imaginary parts respectively. - - Example - ------- - - .. manim:: ApplyFuncExample - - class ApplyFuncExample(Scene): - def construct(self): - circ = Circle().scale(1.5) - circ_ref = circ.copy() - circ.apply_complex_function( - lambda x: np.exp(x*1j) - ) - t = ValueTracker(0) - circ.add_updater( - lambda x: x.become(circ_ref.copy().apply_complex_function( - lambda x: np.exp(x+t.get_value()*1j) - )).set_color(BLUE) - ) - self.add(circ_ref) - self.play(TransformFromCopy(circ_ref, circ)) - self.play(t.animate.set_value(TAU), run_time=3) - """ - - def R3_func(point: Point3D) -> Point3D: - x, y, z = point - xy_complex = function(complex(x, y)) - return np.array([xy_complex.real, xy_complex.imag, z]) - - return self.apply_function( - R3_func, about_point=about_point, about_edge=about_edge - ) - def reverse_points(self) -> Self: for mob in self.family_members_with_points(): mob.apply_over_attr_arrays(lambda arr: np.array(list(reversed(arr)))) @@ -1565,117 +1165,8 @@ def repeat_array(array: Point3D_Array) -> Point3D_Array: # Note, much of these are now redundant with default behavior of # above methods - # TODO: name is inconsistent with OpenGLMobject.apply_points_function() - def apply_points_function_about_point( - self, - func: MultiMappingFunction, - about_point: Point3DLike | None = None, - about_edge: Vector3DLike | None = None, - ) -> Self: - if about_point is None: - if about_edge is None: - about_edge = ORIGIN - about_point = self.get_critical_point(about_edge) - # Make a copy to prevent mutation of the original array if about_point is a view - about_point = np.array(about_point, copy=True) - for mob in self.family_members_with_points(): - mob.points -= about_point - mob.points = func(mob.points) - mob.points += about_point - return self - - def pose_at_angle(self, **kwargs: Any) -> Self: - self.rotate(TAU / 14, RIGHT + UP, **kwargs) - return self - # Positioning methods - def center(self) -> Self: - """Moves the center of the mobject to the center of the scene. - - Returns - ------- - :class:`.Mobject` - The centered mobject. - """ - self.shift(-self.get_center()) - return self - - def align_on_border( - self, direction: Vector3DLike, buff: float = DEFAULT_MOBJECT_TO_EDGE_BUFFER - ) -> Self: - """Direction just needs to be a vector pointing towards side or - corner in the 2d plane. - """ - target_point = np.sign(direction) * ( - config["frame_x_radius"], - config["frame_y_radius"], - 0, - ) - point_to_align = self.get_critical_point(direction) - shift_val = target_point - point_to_align - buff * np.array(direction) - shift_val = shift_val * abs(np.sign(direction)) - self.shift(shift_val) - return self - - def to_corner( - self, corner: Vector3DLike = DL, buff: float = DEFAULT_MOBJECT_TO_EDGE_BUFFER - ) -> Self: - """Moves this :class:`~.Mobject` to the given corner of the screen. - - Returns - ------- - :class:`.Mobject` - The newly positioned mobject. - - Examples - -------- - - .. manim:: ToCornerExample - :save_last_frame: - - class ToCornerExample(Scene): - def construct(self): - c = Circle() - c.to_corner(UR) - t = Tex("To the corner!") - t2 = MathTex("x^3").shift(DOWN) - self.add(c,t,t2) - t.to_corner(DL, buff=0) - t2.to_corner(UL, buff=1.5) - """ - return self.align_on_border(corner, buff) - - def to_edge( - self, edge: Vector3DLike = LEFT, buff: float = DEFAULT_MOBJECT_TO_EDGE_BUFFER - ) -> Self: - """Moves this :class:`~.Mobject` to the given edge of the screen, - without affecting its position in the other dimension. - - Returns - ------- - :class:`.Mobject` - The newly positioned mobject. - - Examples - -------- - - .. manim:: ToEdgeExample - :save_last_frame: - - class ToEdgeExample(Scene): - def construct(self): - tex_top = Tex("I am at the top!") - tex_top.to_edge(UP) - tex_side = Tex("I am moving to the side!") - c = Circle().shift(2*DOWN) - self.add(tex_top, tex_side, c) - tex_side.to_edge(LEFT) - c.to_edge(RIGHT, buff=0) - - """ - return self.align_on_border(edge, buff) - def next_to( self, mobject_or_point: Mobject | Point3DLike, @@ -1730,192 +1221,12 @@ def construct(self): self.shift((target_point - point_to_align + buff * np_direction) * coor_mask) return self - def shift_onto_screen(self, **kwargs: Any) -> Self: - space_lengths = [config["frame_x_radius"], config["frame_y_radius"]] - for vect in UP, DOWN, LEFT, RIGHT: - dim = np.argmax(np.abs(vect)) - buff = kwargs.get("buff", DEFAULT_MOBJECT_TO_EDGE_BUFFER) - max_val = space_lengths[dim] - buff - edge_center = self.get_edge_center(vect) - if np.dot(edge_center, vect) > max_val: - self.to_edge(vect, **kwargs) - return self - - def is_off_screen(self) -> bool: - if self.get_left()[0] > config["frame_x_radius"]: - return True - if self.get_right()[0] < -config["frame_x_radius"]: - return True - if self.get_bottom()[1] > config["frame_y_radius"]: - return True - rv: bool = self.get_top()[1] < -config["frame_y_radius"] - return rv - - def stretch_about_point(self, factor: float, dim: int, point: Point3DLike) -> Self: - return self.stretch(factor, dim, about_point=point) - - def rescale_to_fit( - self, length: float, dim: int, stretch: bool = False, **kwargs: Any - ) -> Self: - old_length = self.length_over_dim(dim) - if old_length == 0: - return self - if stretch: - self.stretch(length / old_length, dim, **kwargs) - else: - self.scale(length / old_length, **kwargs) - return self - - def scale_to_fit_width(self, width: float, **kwargs: Any) -> Self: - """Scales the :class:`~.Mobject` to fit a width while keeping height/depth proportional. - - Returns - ------- - :class:`Mobject` - ``self`` - - Examples - -------- - :: - - >>> from manim import * - >>> sq = Square() - >>> sq.height - np.float64(2.0) - >>> sq.scale_to_fit_width(5) - Square - >>> sq.width - np.float64(5.0) - >>> sq.height - np.float64(5.0) - """ - return self.rescale_to_fit(width, 0, stretch=False, **kwargs) - - def stretch_to_fit_width(self, width: float, **kwargs: Any) -> Self: - """Stretches the :class:`~.Mobject` to fit a width, not keeping height/depth proportional. - - Returns - ------- - :class:`Mobject` - ``self`` - - Examples - -------- - :: - - >>> from manim import * - >>> sq = Square() - >>> sq.height - np.float64(2.0) - >>> sq.stretch_to_fit_width(5) - Square - >>> sq.width - np.float64(5.0) - >>> sq.height - np.float64(2.0) - """ - return self.rescale_to_fit(width, 0, stretch=True, **kwargs) - - def scale_to_fit_height(self, height: float, **kwargs: Any) -> Self: - """Scales the :class:`~.Mobject` to fit a height while keeping width/depth proportional. - - Returns - ------- - :class:`Mobject` - ``self`` - - Examples - -------- - :: - - >>> from manim import * - >>> sq = Square() - >>> sq.width - np.float64(2.0) - >>> sq.scale_to_fit_height(5) - Square - >>> sq.height - np.float64(5.0) - >>> sq.width - np.float64(5.0) - """ - return self.rescale_to_fit(height, 1, stretch=False, **kwargs) - - def stretch_to_fit_height(self, height: float, **kwargs: Any) -> Self: - """Stretches the :class:`~.Mobject` to fit a height, not keeping width/depth proportional. - - Returns - ------- - :class:`Mobject` - ``self`` - - Examples - -------- - :: - - >>> from manim import * - >>> sq = Square() - >>> sq.width - np.float64(2.0) - >>> sq.stretch_to_fit_height(5) - Square - >>> sq.height - np.float64(5.0) - >>> sq.width - np.float64(2.0) - """ - return self.rescale_to_fit(height, 1, stretch=True, **kwargs) - - def scale_to_fit_depth(self, depth: float, **kwargs: Any) -> Self: - """Scales the :class:`~.Mobject` to fit a depth while keeping width/height proportional.""" - return self.rescale_to_fit(depth, 2, stretch=False, **kwargs) - - def stretch_to_fit_depth(self, depth: float, **kwargs: Any) -> Self: - """Stretches the :class:`~.Mobject` to fit a depth, not keeping width/height proportional.""" - return self.rescale_to_fit(depth, 2, stretch=True, **kwargs) - - def set_coord( - self, value: float, dim: int, direction: Vector3DLike = ORIGIN - ) -> Self: - curr = self.get_coord(dim, direction) - shift_vect = np.zeros(self.dim) - shift_vect[dim] = value - curr - self.shift(shift_vect) - return self - - def set_x(self, x: float, direction: Vector3DLike = ORIGIN) -> Self: - """Set x value of the center of the :class:`~.Mobject` (``int`` or ``float``)""" - return self.set_coord(x, 0, direction) - - def set_y(self, y: float, direction: Vector3DLike = ORIGIN) -> Self: - """Set y value of the center of the :class:`~.Mobject` (``int`` or ``float``)""" - return self.set_coord(y, 1, direction) - - def set_z(self, z: float, direction: Vector3DLike = ORIGIN) -> Self: - """Set z value of the center of the :class:`~.Mobject` (``int`` or ``float``)""" - return self.set_coord(z, 2, direction) - def space_out_submobjects(self, factor: float = 1.5, **kwargs: Any) -> Self: self.scale(factor, **kwargs) for submob in self.submobjects: submob.scale(1.0 / factor) return self - def move_to( - self, - point_or_mobject: Point3DLike | Mobject, - aligned_edge: Vector3DLike = ORIGIN, - coor_mask: Vector3DLike = np.array([1, 1, 1]), - ) -> Self: - """Move center of the :class:`~.Mobject` to certain Point3D.""" - if isinstance(point_or_mobject, Mobject): - target = point_or_mobject.get_critical_point(aligned_edge) - else: - target = point_or_mobject - point_to_align = self.get_critical_point(aligned_edge) - self.shift((target - point_to_align) * coor_mask) - return self - def replace( self, mobject: Mobject, dim_to_match: int = 0, stretch: bool = False ) -> Self: @@ -2165,51 +1476,6 @@ def restore(self) -> Self: self.become(self.saved_state) return self - def reduce_across_dimension( - self, reduce_func: Callable[[Iterable[float]], float], dim: int - ) -> float | None: - """Find the min or max value from a dimension across all points in this Mobject and its - submobjects. This allows for using :meth:`~.length_over_dim` to calculate its length over - a dimension, i.e. its height, width or depth. If this Mobject is empty, return ``None``, - since this Mobject should not be taken into account when calculating lengths. - - Parameters - ---------- - reduce_func - The reducer function to use in order to calculate a value over a dimension. - dim - The dimension to use. It should be 0, 1 or 2, representing the X, Y or Z coordinate, - respectively. - - Returns - ------- - float | None - The min or max value over the dimension specified by ``dim``, or ``None`` if this - Mobject is empty. - """ - assert dim >= 0 - assert dim <= 2 - if len(self.submobjects) == 0 and len(self.points) == 0: - # If we have no points and no submobjects, return None - return None - - # If we do not have points (but do have submobjects) - # use only the points from those. - if len(self.points) == 0: # noqa: SIM108 - rv = None - else: - # Otherwise, be sure to include our own points - rv = reduce_func(self.points[:, dim]) - # Recursively ask submobjects (if any) for the biggest/ - # smallest dimension they have and compare it to the return value. - for mobj in self.submobjects: - value = mobj.reduce_across_dimension(reduce_func, dim) - if rv is None: - rv = value - elif value is not None: - rv = reduce_func([value, rv]) - return rv - def nonempty_submobjects(self) -> Sequence[Mobject]: return [ submob @@ -2238,80 +1504,11 @@ def get_all_points(self) -> Point3D_Array: # Getters - def get_points_defining_boundary(self) -> Point3D_Array: - return self.get_all_points() - def get_num_points(self) -> int: return len(self.points) - def get_extremum_along_dim( - self, points: Point3DLike_Array | None = None, dim: int = 0, key: int = 0 - ) -> float: - np_points: Point3D_Array = ( - self.get_points_defining_boundary() - if points is None - else np.asarray(points) - ) - values = np_points[:, dim] - if key < 0: - rv: float = np.min(values) - return rv - elif key == 0: - rv = (np.min(values) + np.max(values)) / 2 - return rv - else: - rv = np.max(values) - return rv - - def get_critical_point(self, direction: Vector3DLike) -> Point3D: - """Picture a box bounding the :class:`~.Mobject`. Such a box has - 9 'critical points': 4 corners, 4 edge center, the - center. This returns one of them, along the given direction. - - :: - - sample = Arc(start_angle=PI / 7, angle=PI / 5) - - # These are all equivalent - max_y_1 = sample.get_top()[1] - max_y_2 = sample.get_critical_point(UP)[1] - max_y_3 = sample.get_extremum_along_dim(dim=1, key=1) - - """ - result = np.zeros(self.dim) - all_points = self.get_points_defining_boundary() - if len(all_points) == 0: - return result - for dim in range(self.dim): - result[dim] = self.get_extremum_along_dim( - all_points, - dim=dim, - key=np.array(direction[dim]), - ) - return result - # Pseudonyms for more general get_critical_point method - def get_edge_center(self, direction: Vector3DLike) -> Point3D: - """Get edge Point3Ds for certain direction.""" - return self.get_critical_point(direction) - - def get_corner(self, direction: Vector3DLike) -> Point3D: - """Get corner Point3Ds for certain direction.""" - return self.get_critical_point(direction) - - def get_center(self) -> Point3D: - """Get center Point3Ds""" - return self.get_critical_point(np.zeros(self.dim)) - - def get_center_of_mass(self) -> Point3D: - return np.apply_along_axis(np.mean, 0, self.get_all_points()) - - def get_boundary_point(self, direction: Vector3DLike) -> Point3D: - all_points = self.get_points_defining_boundary() - index = np.argmax(np.dot(all_points, direction)) - return all_points[index] - def get_midpoint(self) -> Point3D: """Get Point3Ds of the middle of the path that forms the :class:`~.Mobject`. @@ -2335,54 +1532,6 @@ def construct(self): """ return self.point_from_proportion(0.5) - def get_top(self) -> Point3D: - """Get top Point3Ds of a box bounding the :class:`~.Mobject`""" - return self.get_edge_center(UP) - - def get_bottom(self) -> Point3D: - """Get bottom Point3Ds of a box bounding the :class:`~.Mobject`""" - return self.get_edge_center(DOWN) - - def get_right(self) -> Point3D: - """Get right Point3Ds of a box bounding the :class:`~.Mobject`""" - return self.get_edge_center(RIGHT) - - def get_left(self) -> Point3D: - """Get left Point3Ds of a box bounding the :class:`~.Mobject`""" - return self.get_edge_center(LEFT) - - def get_zenith(self) -> Point3D: - """Get zenith Point3Ds of a box bounding a 3D :class:`~.Mobject`.""" - return self.get_edge_center(OUT) - - def get_nadir(self) -> Point3D: - """Get nadir (opposite the zenith) Point3Ds of a box bounding a 3D :class:`~.Mobject`.""" - return self.get_edge_center(IN) - - def length_over_dim(self, dim: int) -> float: - """Measure the length of an :class:`~.Mobject` in a certain direction.""" - max_coord = self.reduce_across_dimension(max, dim) - min_coord = self.reduce_across_dimension(min, dim) - if max_coord is None or min_coord is None: - return 0 - return max_coord - min_coord - - def get_coord(self, dim: int, direction: Vector3DLike = ORIGIN) -> float: - """Meant to generalize ``get_x``, ``get_y`` and ``get_z``""" - return self.get_extremum_along_dim(dim=dim, key=np.array(direction)[dim]) - - def get_x(self, direction: Vector3DLike = ORIGIN) -> float: - """Returns x Point3D of the center of the :class:`~.Mobject` as ``float``""" - return self.get_coord(0, direction) - - def get_y(self, direction: Vector3DLike = ORIGIN) -> float: - """Returns y Point3D of the center of the :class:`~.Mobject` as ``float``""" - return self.get_coord(1, direction) - - def get_z(self, direction: Vector3DLike = ORIGIN) -> float: - """Returns z Point3D of the center of the :class:`~.Mobject` as ``float``""" - return self.get_coord(2, direction) - def get_start(self) -> Point3D: """Returns the point, where the stroke that surrounds the :class:`~.Mobject` starts.""" self.throw_error_if_no_points() @@ -2433,65 +1582,6 @@ def match_color(self, mobject: Mobject) -> Self: """Match the color with the color of another :class:`~.Mobject`.""" return self.set_color(mobject.get_color()) - def match_dim_size(self, mobject: Mobject, dim: int, **kwargs: Any) -> Self: - """Match the specified dimension with the dimension of another :class:`~.Mobject`.""" - return self.rescale_to_fit(mobject.length_over_dim(dim), dim, **kwargs) - - def match_width(self, mobject: Mobject, **kwargs: Any) -> Self: - """Match the width with the width of another :class:`~.Mobject`.""" - return self.match_dim_size(mobject, 0, **kwargs) - - def match_height(self, mobject: Mobject, **kwargs: Any) -> Self: - """Match the height with the height of another :class:`~.Mobject`.""" - return self.match_dim_size(mobject, 1, **kwargs) - - def match_depth(self, mobject: Mobject, **kwargs: Any) -> Self: - """Match the depth with the depth of another :class:`~.Mobject`.""" - return self.match_dim_size(mobject, 2, **kwargs) - - def match_coord( - self, mobject: Mobject, dim: int, direction: Vector3DLike = ORIGIN - ) -> Self: - """Match the Point3Ds with the Point3Ds of another :class:`~.Mobject`.""" - return self.set_coord( - mobject.get_coord(dim, direction), - dim=dim, - direction=direction, - ) - - def match_x(self, mobject: Mobject, direction: Vector3DLike = ORIGIN) -> Self: - """Match x coord. to the x coord. of another :class:`~.Mobject`.""" - return self.match_coord(mobject, 0, direction) - - def match_y(self, mobject: Mobject, direction: Vector3DLike = ORIGIN) -> Self: - """Match y coord. to the x coord. of another :class:`~.Mobject`.""" - return self.match_coord(mobject, 1, direction) - - def match_z(self, mobject: Mobject, direction: Vector3DLike = ORIGIN) -> Self: - """Match z coord. to the x coord. of another :class:`~.Mobject`.""" - return self.match_coord(mobject, 2, direction) - - def align_to( - self, - mobject_or_point: Mobject | Point3DLike, - direction: Vector3DLike = ORIGIN, - ) -> Self: - """Aligns mobject to another :class:`~.Mobject` in a certain direction. - - Examples: - mob1.align_to(mob2, UP) moves mob1 vertically so that its - top edge lines ups with mob2's top edge. - """ - if isinstance(mobject_or_point, Mobject): - point = mobject_or_point.get_critical_point(direction) - else: - point = mobject_or_point - - for dim in range(self.dim): - if direction[dim] != 0: - self.set_coord(point[dim], dim, direction) - return self - # Family matters def __getitem__(self, value: Any) -> Mobject: @@ -3321,27 +2411,6 @@ def construct(self): sm1.interpolate_color(sm1, sm2, 1) return self - def match_points(self, mobject: Mobject, copy_submobjects: bool = True) -> Self: - """Edit points, positions, and submobjects to be identical - to another :class:`~.Mobject`, while keeping the style unchanged. - - Examples - -------- - .. manim:: MatchPointsScene - - class MatchPointsScene(Scene): - def construct(self): - circ = Circle(fill_color=RED, fill_opacity=0.8) - square = Square(fill_color=BLUE, fill_opacity=0.2) - self.add(circ) - self.wait(0.5) - self.play(circ.animate.match_points(square)) - self.wait(0.5) - """ - for sm1, sm2 in zip(self.get_family(), mobject.get_family(), strict=False): - sm1.points = np.array(sm2.points) - return self - # Errors def throw_error_if_no_points(self) -> None: if self.has_no_points(): diff --git a/manim/mobject/table.py b/manim/mobject/table.py index ca4c96a7f8..fc3b27bb32 100644 --- a/manim/mobject/table.py +++ b/manim/mobject/table.py @@ -57,6 +57,8 @@ def construct(self): from typing import Self +from manim.typing import Point3DLike, Vector3DLike + __all__ = [ "Table", "MathTable", @@ -996,13 +998,23 @@ def construct(self): return AnimationGroup(*animations, lag_ratio=lag_ratio) def scale( - self, scale_factor: float, scale_stroke: bool = False, **kwargs: Any + self, + factor: float, + scale_stroke: bool = False, + *, + about_point: Point3DLike | None = None, + about_edge: Vector3DLike | None = None, ) -> Self: # h_buff and v_buff must be adjusted so that Table.get_cell # can construct an accurate polygon for a cell. - self.h_buff *= scale_factor - self.v_buff *= scale_factor - super().scale(scale_factor, scale_stroke=scale_stroke, **kwargs) + self.h_buff *= factor + self.v_buff *= factor + super().scale( + factor, + scale_stroke=scale_stroke, + about_point=about_point, + about_edge=about_edge, + ) return self diff --git a/manim/mobject/text/typst_mobject.py b/manim/mobject/text/typst_mobject.py index b2e5aeb793..18076a892d 100644 --- a/manim/mobject/text/typst_mobject.py +++ b/manim/mobject/text/typst_mobject.py @@ -117,6 +117,8 @@ def construct(self): from __future__ import annotations +from manim.typing import Point3DLike, Vector3DLike + __all__ = [ "Typst", "MathTypst", @@ -292,14 +294,14 @@ def font_size(self, val: float) -> None: def scale( self, - scale_factor: float, + factor: float, scale_stroke: bool = False, *, - about_point: np.ndarray | None = None, - about_edge: np.ndarray | None = None, + about_point: Point3DLike | None = None, + about_edge: Vector3DLike | None = None, ) -> Self: result = super().scale( - scale_factor, + factor, scale_stroke=scale_stroke, about_point=about_point, about_edge=about_edge, diff --git a/manim/mobject/types/vectorized_mobject.py b/manim/mobject/types/vectorized_mobject.py index 8d05268539..a7c74551cf 100644 --- a/manim/mobject/types/vectorized_mobject.py +++ b/manim/mobject/types/vectorized_mobject.py @@ -483,7 +483,7 @@ def set_opacity(self, opacity: float, family: bool = True) -> Self: def scale( self, - scale_factor: float, + factor: float, scale_stroke: bool = False, *, about_point: Point3DLike | None = None, @@ -543,15 +543,17 @@ def construct(self): for mob in self.get_family(): if isinstance(mob, VMobject): mob.set_stroke( - width=abs(scale_factor) * mob.get_stroke_width(), + width=abs(factor) * mob.get_stroke_width(), family=False, ) mob.set_stroke( - width=abs(scale_factor) * mob.get_stroke_width(background=True), + width=abs(factor) * mob.get_stroke_width(background=True), background=True, family=False, ) - super().scale(scale_factor, about_point=about_point, about_edge=about_edge) + super().scale( + factor, scale_stroke, about_point=about_point, about_edge=about_edge + ) return self def fade(self, darkness: float = 0.5, family: bool = True) -> Self: @@ -793,10 +795,6 @@ def set_shade_in_3d( submob.z_index_group = self return self - def set_points(self, points: Point3DLike_Array) -> Self: - self.points: Point3D_Array = np.array(points) - return self - def resize_points( self, new_length: int, diff --git a/tests/module/mobject/test_positioning.py b/tests/module/mobject/test_positioning.py new file mode 100644 index 0000000000..92bbe14510 --- /dev/null +++ b/tests/module/mobject/test_positioning.py @@ -0,0 +1,336 @@ +import numpy as np + +from manim.constants import DEGREES, DOWN, IN, LEFT, ORIGIN, OUT, RIGHT, UP +from manim.mobject.abstract.positionable import Positionable + + +def test_translate() -> None: + p = Positionable().set_points([(1, 2, 3)]) + p.translate((-40, -50, -60)) + np.testing.assert_allclose(p.points, [(1 - 40, 2 - 50, 3 - 60)]) + + p = Positionable().set_points( + [ + (-1, -1, -1), + (-1, -1, +1), + (-1, +1, -1), + (-1, +1, +1), + (+1, -1, -1), + (+1, -1, +1), + (+1, +1, -1), + (+1, +1, +1), + ] + ) + p.translate((2, -3, 4)) + np.testing.assert_allclose( + p.points, + [ + (-1 + 2, -1 - 3, -1 + 4), + (-1 + 2, -1 - 3, +1 + 4), + (-1 + 2, +1 - 3, -1 + 4), + (-1 + 2, +1 - 3, +1 + 4), + (+1 + 2, -1 - 3, -1 + 4), + (+1 + 2, -1 - 3, +1 + 4), + (+1 + 2, +1 - 3, -1 + 4), + (+1 + 2, +1 - 3, +1 + 4), + ], + ) + + +def test_rotate() -> None: + p = Positionable().set_points([(1, 1, 1)]) + p.rotate(90 * DEGREES, axis=(1, 0, 0), about_point=ORIGIN) + np.testing.assert_allclose(p.points, [(1, -1, 1)]) + + p = Positionable().set_points([(1, 1, 1)]) + p.rotate(90 * DEGREES, axis=(0, 1, 0), about_point=ORIGIN) + np.testing.assert_allclose(p.points, [(1, 1, -1)]) + + p = Positionable().set_points([(1, 1, 1)]) + p.rotate(90 * DEGREES, axis=(0, 0, 1), about_point=ORIGIN) + np.testing.assert_allclose(p.points, [(-1, 1, 1)]) + + p = Positionable().set_points( + [ + (0, 0, 0), + (0, 0, 2), + (0, 2, 0), + (0, 2, 2), + (2, 0, 0), + (2, 0, 2), + (2, 2, 0), + (2, 2, 2), + ] + ) + p.rotate(90 * DEGREES, axis=(1, 0, 0)) + np.testing.assert_allclose( + p.points, + [ + (0, 2, 0), + (0, 0, 0), + (0, 2, 2), + (0, 0, 2), + (2, 2, 0), + (2, 0, 0), + (2, 2, 2), + (2, 0, 2), + ], + atol=1e-8, + ) + + +def test_scale() -> None: + p = Positionable().set_points( + [ + (0, 0, 0), + (0, 0, 2), + (0, 2, 0), + (0, 2, 2), + (2, 0, 0), + (2, 0, 2), + (2, 2, 0), + (2, 2, 2), + ] + ) + p.scale(2) + np.testing.assert_allclose( + p.points, + [ + (-1, -1, -1), + (-1, -1, +3), + (-1, +3, -1), + (-1, +3, +3), + (+3, -1, -1), + (+3, -1, +3), + (+3, +3, -1), + (+3, +3, +3), + ], + ) + + +def test_stretch() -> None: + p = Positionable().set_points( + [ + (0, 0, 0), + (0, 0, 2), + (0, 2, 0), + (0, 2, 2), + (2, 0, 0), + (2, 0, 2), + (2, 2, 0), + (2, 2, 2), + ] + ) + p.stretch(2, dim=1) + np.testing.assert_allclose( + p.points, + [ + (0, -1, 0), + (0, -1, 2), + (0, +3, 0), + (0, +3, 2), + (2, -1, 0), + (2, -1, 2), + (2, +3, 0), + (2, +3, 2), + ], + ) + + +def test_get_position() -> None: + p = Positionable() + np.testing.assert_allclose(p.get_position(), (0, 0, 0)) + + p = Positionable().set_points([(1, 2, 3)]) + np.testing.assert_allclose(p.get_position(), (1, 2, 3)) + + p = Positionable().set_points( + [ + (0, 0, 0), + (0, 0, 2), + (0, 2, 0), + (0, 2, 2), + (2, 0, 0), + (2, 0, 2), + (2, 2, 0), + (2, 2, 2), + ] + ) + np.testing.assert_allclose(p.get_position((-1, -1, -1)), (0, 0, 0)) + np.testing.assert_allclose(p.get_position((-1, -1, +0)), (0, 0, 1)) + np.testing.assert_allclose(p.get_position((-1, -1, +1)), (0, 0, 2)) + np.testing.assert_allclose(p.get_position((-1, +0, -1)), (0, 1, 0)) + np.testing.assert_allclose(p.get_position((-1, +0, +0)), (0, 1, 1)) + np.testing.assert_allclose(p.get_position((-1, +0, +1)), (0, 1, 2)) + np.testing.assert_allclose(p.get_position((-1, +1, -1)), (0, 2, 0)) + np.testing.assert_allclose(p.get_position((-1, +1, +0)), (0, 2, 1)) + np.testing.assert_allclose(p.get_position((-1, +1, +1)), (0, 2, 2)) + np.testing.assert_allclose(p.get_position((+0, -1, -1)), (1, 0, 0)) + np.testing.assert_allclose(p.get_position((+0, -1, +0)), (1, 0, 1)) + np.testing.assert_allclose(p.get_position((+0, -1, +1)), (1, 0, 2)) + np.testing.assert_allclose(p.get_position((+0, +0, -1)), (1, 1, 0)) + np.testing.assert_allclose(p.get_position((+0, +0, +0)), (1, 1, 1)) + np.testing.assert_allclose(p.get_position((+0, +0, +1)), (1, 1, 2)) + np.testing.assert_allclose(p.get_position((+0, +1, -1)), (1, 2, 0)) + np.testing.assert_allclose(p.get_position((+0, +1, +0)), (1, 2, 1)) + np.testing.assert_allclose(p.get_position((+0, +1, +1)), (1, 2, 2)) + np.testing.assert_allclose(p.get_position((+1, -1, -1)), (2, 0, 0)) + np.testing.assert_allclose(p.get_position((+1, -1, +0)), (2, 0, 1)) + np.testing.assert_allclose(p.get_position((+1, -1, +1)), (2, 0, 2)) + np.testing.assert_allclose(p.get_position((+1, +0, -1)), (2, 1, 0)) + np.testing.assert_allclose(p.get_position((+1, +0, +0)), (2, 1, 1)) + np.testing.assert_allclose(p.get_position((+1, +0, +1)), (2, 1, 2)) + np.testing.assert_allclose(p.get_position((+1, +1, -1)), (2, 2, 0)) + np.testing.assert_allclose(p.get_position((+1, +1, +0)), (2, 2, 1)) + np.testing.assert_allclose(p.get_position((+1, +1, +1)), (2, 2, 2)) + + +def test_set_position() -> None: + p = Positionable().set_points([(0, 0, 0)]) + p.set_position((1, 2, 3)) + np.testing.assert_allclose(p.points, [(1, 2, 3)]) + + p = Positionable().set_points( + [ + (-1, -1, -1), + (-1, -1, +1), + (-1, +1, -1), + (-1, +1, +1), + (+1, -1, -1), + (+1, -1, +1), + (+1, +1, -1), + (+1, +1, +1), + ] + ) + p.set_position((3, 2, 1)) + np.testing.assert_allclose( + p.points, + [ + (-1 + 3, -1 + 2, -1 + 1), + (-1 + 3, -1 + 2, +1 + 1), + (-1 + 3, +1 + 2, -1 + 1), + (-1 + 3, +1 + 2, +1 + 1), + (+1 + 3, -1 + 2, -1 + 1), + (+1 + 3, -1 + 2, +1 + 1), + (+1 + 3, +1 + 2, -1 + 1), + (+1 + 3, +1 + 2, +1 + 1), + ], + ) + + +def test_get_coordinate() -> None: + p = Positionable() + np.testing.assert_allclose(p.get_coordinate(0), 0) + np.testing.assert_allclose(p.get_coordinate(1), 0) + np.testing.assert_allclose(p.get_coordinate(2), 0) + + p = Positionable().set_points([(1, 2, 3)]) + np.testing.assert_allclose(p.get_coordinate(0), 1) + np.testing.assert_allclose(p.get_coordinate(1), 2) + np.testing.assert_allclose(p.get_coordinate(2), 3) + + p = Positionable().set_points( + [ + (-1, -2, -3), + (-1, -2, +3), + (-1, +2, -3), + (-1, +2, +3), + (+1, -2, -3), + (+1, -2, +3), + (+1, +2, -3), + (+1, +2, +3), + ] + ) + np.testing.assert_allclose(p.get_coordinate(0, LEFT), -1) + np.testing.assert_allclose(p.get_coordinate(0, RIGHT), 1) + np.testing.assert_allclose(p.get_coordinate(1, DOWN), -2) + np.testing.assert_allclose(p.get_coordinate(1, UP), 2) + np.testing.assert_allclose(p.get_coordinate(2, IN), -3) + np.testing.assert_allclose(p.get_coordinate(2, OUT), 3) + + +def test_set_coordinate_x() -> None: + p = Positionable().set_points( + [ + (-1, -2, -3), + (-1, -2, +3), + (-1, +2, -3), + (-1, +2, +3), + (+1, -2, -3), + (+1, -2, +3), + (+1, +2, -3), + (+1, +2, +3), + ] + ) + p.set_coordinate(coordinate=4, dim=0) + np.testing.assert_allclose( + p.points, + [ + (-1 + 4, -2, -3), + (-1 + 4, -2, +3), + (-1 + 4, +2, -3), + (-1 + 4, +2, +3), + (+1 + 4, -2, -3), + (+1 + 4, -2, +3), + (+1 + 4, +2, -3), + (+1 + 4, +2, +3), + ], + ) + + +def test_set_coordinate_y() -> None: + p = Positionable().set_points( + [ + (-1, -2, -3), + (-1, -2, +3), + (-1, +2, -3), + (-1, +2, +3), + (+1, -2, -3), + (+1, -2, +3), + (+1, +2, -3), + (+1, +2, +3), + ] + ) + p.set_coordinate(coordinate=5, dim=1) + np.testing.assert_allclose( + p.points, + [ + (-1, -2 + 5, -3), + (-1, -2 + 5, +3), + (-1, +2 + 5, -3), + (-1, +2 + 5, +3), + (+1, -2 + 5, -3), + (+1, -2 + 5, +3), + (+1, +2 + 5, -3), + (+1, +2 + 5, +3), + ], + ) + + +def test_set_coordinate_z() -> None: + p = Positionable().set_points( + [ + (-1, -2, -3), + (-1, -2, +3), + (-1, +2, -3), + (-1, +2, +3), + (+1, -2, -3), + (+1, -2, +3), + (+1, +2, -3), + (+1, +2, +3), + ] + ) + p.set_coordinate(coordinate=6, dim=2) + np.testing.assert_allclose( + p.points, + [ + (-1, -2, -3 + 6), + (-1, -2, +3 + 6), + (-1, +2, -3 + 6), + (-1, +2, +3 + 6), + (+1, -2, -3 + 6), + (+1, -2, +3 + 6), + (+1, +2, -3 + 6), + (+1, +2, +3 + 6), + ], + )