From 73009e82797910d7c41c3b08984f9fa7d0f200f8 Mon Sep 17 00:00:00 2001 From: GniLudio <50866361+GniLudio@users.noreply.github.com> Date: Fri, 14 Aug 2026 08:18:03 +0200 Subject: [PATCH 01/66] Create positionable.py --- manim/mobject/abstract/positionable.py | 130 +++++++++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 manim/mobject/abstract/positionable.py diff --git a/manim/mobject/abstract/positionable.py b/manim/mobject/abstract/positionable.py new file mode 100644 index 0000000000..27f4669a36 --- /dev/null +++ b/manim/mobject/abstract/positionable.py @@ -0,0 +1,130 @@ +from typing import Self + +import numpy as np + +from manim import ORIGIN +from manim.typing import Point3D, Point3D_Array, Point3DLike, Point3DLike_Array + + +class Positionable: + def __init__(self, *points: Point3DLike): + self.points: Point3D_Array = np.asarray(points) + + def get_points(self) -> Point3D_Array: + if len(self.points) == 0: + return np.array([ORIGIN]) + return self.points + + def set_points(self, points: Point3DLike_Array) -> Self: + self.points = np.asarray(points) + return self + + # Getter + def length_over_dim(self, dim: int) -> float: + points = self.get_points() + values = points[:, dim] + return values.max() - values.min() + + @property + def width(self) -> float: + return self.length_over_dim(0) + + @property + def height(self) -> float: + return self.length_over_dim(1) + + @property + def depth(self) -> float: + return self.length_over_dim(2) + + def get_bottom(self) -> Point3D: + points = self.get_points() + x = (points[:, 0].min() + points[:, 0].max()) / 2 + y = points[:, 1].min() + z = (points[:, 2].min() + points[:, 2].max()) / 2 + return np.array([x, y, z]) + + # Helper Methods + # align_on_border + # align_to + # apply_complex_function + # apply_function + # apply_function_to_position + # apply_matrix + # apply_over_attr_arrays + # apply_points_function + # apply_points_function_about_point + # center + # flip + # get_array_attrs + # get_bottom + # get_boundary_point + # get_bounding_box + # get_bounding_box_point + # get_center + # get_center_of_mass + # get_continuous_bounding_box_point + # get_coord + # get_corner + # get_critical_point + # get_depth + # get_edge_center + # get_extremum_along_dim + # get_height + # get_left + # get_midpoint + # get_nadir + # get_points_defining_boundary + # get_right + # get_top + # get_width + # get_x + # get_y + # get_z + # get_zenith + # is_off_screen + # is_point_touching + # length_over_dim + # match_coord + # match_depth + # match_dim_size + # match_height + # match_width + # match_x + # match_y + # match_z + # move_to + # next_to + # pfp + # point_from_proportion + # pose_at_angle + # proportion_from_point + # reduce_across_dimension + # rescale_to_fit + # rotate + # rotate_about_origin + # scale + # scale_to_fit_depth + # scale_to_fit_height + # scale_to_fit_width + # set_coord + # set_depth + # set_height + # set_width + # set_x + # set_y + # set_z + # shift + # shift_onto_screen + # stretch + # stretch_about_point + # stretch_to_fit_depth + # stretch_to_fit_height + # stretch_to_fit_width + # to_corner + # to_edge + + +if __name__ == "__main__": + mob = Positionable(*[(0, -1, 0), (0, -2, 1)]) + print(mob.get_bottom()) From 4b0583e8bd8eb7c05d1fa1e7b1dfecbfa5e14e19 Mon Sep 17 00:00:00 2001 From: GniLudio Date: Mon, 17 Aug 2026 15:27:00 +0200 Subject: [PATCH 02/66] Update --- manim/mobject/abstract/__init__.py | 0 manim/mobject/abstract/logs.txt | Bin 0 -> 2142 bytes manim/mobject/abstract/positionable.md | 20 + manim/mobject/abstract/positionable.py | 716 +++++++++++++++++++++---- manim/mobject/abstract/ruff.toml | 1 + 5 files changed, 635 insertions(+), 102 deletions(-) create mode 100644 manim/mobject/abstract/__init__.py create mode 100644 manim/mobject/abstract/logs.txt create mode 100644 manim/mobject/abstract/positionable.md create mode 100644 manim/mobject/abstract/ruff.toml 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/logs.txt b/manim/mobject/abstract/logs.txt new file mode 100644 index 0000000000000000000000000000000000000000..5c25b3e19a4ed891746eb065db0bdec4208deb40 GIT binary patch literal 2142 zcmbW1&2HO3421VwpzqLok!KPT*>VKivV413{Q{ugFL_kdC>>f95FwGy`3t!uIxsorEjYW$H~u_@1VSJGV4?0T;##zTPIgl zllK`JV3!&6!aX*5a^JnM`Hxm==E94<%No!WliaKRo4(BEsCv&r?BqW-SUk#mP*jO^ zRxA;rZ_iCH2hFaInPp=Z&rYa`+Pk&8x(8-m>mZcrReV+ze>E&H2=k`27v1G|l->MZg_(>?iEujLpgaT%1e|&5UtHv!hE#sGtV4jHt zBet0qoyBcr`godk%XDbGjZw1IVs?w3Prp+Y+f88oJd3CqHbnR>cfXzZHuny;2>$?c CHfd`B literal 0 HcmV?d00001 diff --git a/manim/mobject/abstract/positionable.md b/manim/mobject/abstract/positionable.md new file mode 100644 index 0000000000..f8da101f68 --- /dev/null +++ b/manim/mobject/abstract/positionable.md @@ -0,0 +1,20 @@ +# Positionable + +## Notes +* How should mobject with 0 points be handled? + * Currently: Treats behavior as undefined. + * Advantage: Makes calculations simpler and more efficient. +* Should properties be dropped in favor of setter/getter methods? + * E.g. `width`, `height` and `depth`. + * Advantages: + * More in line with "manim-code-style". + * Would allow method chaining. + * Would allow additional optional parameters. + * Alternative: + * Support both. + * Disadvantage: + * Isn't really an actual value behind the scenes. + * Other indirect attributes have setter methods. + + +## Progress \ No newline at end of file diff --git a/manim/mobject/abstract/positionable.py b/manim/mobject/abstract/positionable.py index 27f4669a36..4e71508cd3 100644 --- a/manim/mobject/abstract/positionable.py +++ b/manim/mobject/abstract/positionable.py @@ -1,130 +1,642 @@ -from typing import Self +import contextlib +from collections.abc import Callable, Iterable +from typing import Any, Self import numpy as np -from manim import ORIGIN -from manim.typing import Point3D, Point3D_Array, Point3DLike, Point3DLike_Array +from manim._config import config +from manim.constants import ( + DEFAULT_MOBJECT_TO_EDGE_BUFFER, + DEFAULT_MOBJECT_TO_MOBJECT_BUFFER, + DL, + DOWN, + IN, + LEFT, + ORIGIN, + OUT, + RIGHT, + TAU, + UP, +) +from manim.mobject.mobject import Mobject +from manim.typing import ( + MatrixMN, + Point3D, + Point3D_Array, + Point3DLike, + Point3DLike_Array, + Vector3D, + Vector3DLike, +) +from manim.utils.space_ops import rotation_matrix class Positionable: - def __init__(self, *points: Point3DLike): - self.points: Point3D_Array = np.asarray(points) + # FUNDAMENTALS + points: Point3D_Array def get_points(self) -> Point3D_Array: - if len(self.points) == 0: - return np.array([ORIGIN]) return self.points def set_points(self, points: Point3DLike_Array) -> Self: self.points = np.asarray(points) return self - # Getter + # METHODS + + def align_on_border( + self, + direction: Vector3DLike, + buff: float = DEFAULT_MOBJECT_TO_EDGE_BUFFER, + ) -> Self: + # TODO: Add parameter for this? + frame = (config.frame_x_radius, config.frame_y_radius, 0) + target = np.sign(direction) * frame - buff * np.array(direction) + self.move_to(point_or_mobject=target, aligned_edge=direction) + return self + + def align_to( + self, + mobject_or_point: "Positionable | Point3DLike", + direction: Vector3DLike = ORIGIN, + ) -> Self: + target = mobject_or_point.get_critical_point(direction=direction) if isinstance(mobject_or_point, Positionable) else mobject_or_point + self.move_to(point_or_mobject=target, aligned_edge=direction) + return self + + def apply_complex_function( + self, + function: Callable[[complex], complex], + about_point: Point3DLike | None = None, + about_edge: Vector3DLike | None = None, + ) -> Self: + 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 apply_function( + self, + function: Callable[[Point3D], Point3D], + *, + about_point: Point3DLike | None = None, + about_edge: Vector3DLike | None = None, + ) -> Self: + 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 + + # TODO: Do we really need this? + def apply_function_to_position( + self, + function: Callable[[Point3D], Point3D], + ) -> Self: + return self.move_to(function(self.get_center())) + + def apply_matrix( + self, + matrix: MatrixMN, + *, + about_point: Point3DLike | None = None, + about_edge: Vector3DLike | None = None, + ) -> Self: + 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_points_function( + lambda points: np.dot(points, full_matrix.T), + about_point=about_point, + about_edge=about_edge, + ) + + def apply_points_function( + self, + func: Callable[[Point3D], Point3D], + about_point: Point3DLike | None = None, + about_edge: Vector3DLike | None = None, + ) -> Self: + if about_point is None: + about_point = self.get_critical_point(direction=about_edge if about_edge is not None else ORIGIN) + points = self.get_points() + points -= about_point + points = func(points) + points += about_point + return self.set_points(points) + + # @deprecated(message="Use apply_points_function() instead.") + def apply_points_function_about_point( + self, + func: Callable[[Point3D], Point3D], + about_point: Point3DLike | None = None, + about_edge: Vector3DLike | None = None, + ) -> Self: + return self.apply_points_function( + func=func, + about_point=about_point, + about_edge=about_edge, + ) + + def center(self) -> Self: + return self.move_to(point_or_mobject=ORIGIN) + + @property + def depth(self) -> float: + return self.length_over_dim(dim=2) + + def flip( + self, + axis: Vector3DLike = UP, + about_point: Point3DLike | None = None, + about_edge: Vector3DLike | None = None, + ) -> Self: + return self.rotate( + angle=TAU / 2, + axis=axis, + about_point=about_point, + about_edge=about_edge, + ) + + def get_bottom(self) -> Point3D: + return self.get_critical_point(DOWN) + + # TODO: Should this function be dropped? + def get_boundary_point(self, direction: Vector3DLike) -> Point3D: + all_points = self.get_points() + index = np.argmax(np.dot(all_points, direction)) + return all_points[index] + + def get_bounding_box(self) -> Point3D_Array: + points = self.get_points() + mins = points.min(axis=0) + maxs = points.max(axis=0) + mids = (mins + maxs) / 2 + return np.array([mins, mids, maxs]) + + def get_center(self) -> Point3D: + return self.get_critical_point(ORIGIN) + + def get_center_of_mass(self) -> Point3D: + return self.get_points().mean(axis=0) + + def get_coord(self, dim: int, direction: Vector3DLike = ORIGIN) -> float: + return self.get_critical_point(direction=direction)[dim] + + # @deprecated(message="Use get_critical_point() instead") + def get_corner(self, direction: Vector3DLike) -> Point3D: + return self.get_critical_point(direction) + + # TODO: Should the `np.sign(direction)` restriction be dropped? + # Advantage: Would allow in-between values + # Disadvantage: Would alter behavior + # Alternative: Declare an additional method. (which this method then would use) + def get_critical_point(self, direction: Vector3DLike) -> Point3D: + direction = np.sign(direction) + _, mids, maxs = self.get_bounding_box() + return mids + (maxs - mids) * direction + + def get_edge_center(self, direction: Vector3DLike) -> Point3D: + return self.get_critical_point(direction=direction) + + def get_end(self) -> Point3D: + return self.points[-1] + + def get_extremum_along_dim( + self, + dim: int = 0, + key: int = 0, + ) -> float: + direction = np.zeros(3) + direction[dim] = np.sign(key) + critical_pt = self.get_critical_point(direction) + return critical_pt[dim] + + def get_left(self) -> Point3D: + return self.get_critical_point(LEFT) + + def get_nadir(self) -> Point3D: + """Get nadir (opposite the zenith) Point3Ds of a box bounding a 3D :class:`~.Mobject`.""" + return self.get_critical_point(IN) + + def get_right(self) -> Point3D: + return self.get_critical_point(RIGHT) + + def get_start(self) -> Point3D: + return self.points[0] + + def get_start_and_end(self) -> tuple[Point3D, Point3D]: + return self.get_start(), self.get_end() + + def get_top(self) -> Point3D: + return self.get_critical_point(UP) + + def get_x(self, direction: Vector3DLike = ORIGIN) -> float: + return self.get_coord(dim=0, direction=direction) + + def get_y(self, direction: Vector3DLike = ORIGIN) -> float: + return self.get_coord(dim=1, direction=direction) + + def get_z(self, direction: Vector3DLike = ORIGIN) -> float: + return self.get_coord(dim=2, direction=direction) + + def get_zenith(self) -> Point3D: + return self.get_critical_point(direction=OUT) + + @property + def height(self) -> float: + return self.length_over_dim(dim=1) + + @height.setter + def height(self, value: float) -> None: + raise NotImplementedError + def length_over_dim(self, dim: int) -> float: points = self.get_points() values = points[:, dim] return values.max() - values.min() + def match_coord( + self, + mobject: Mobject, + dim: int, + direction: Vector3DLike = ORIGIN, + ) -> Self: + return self.set_coord( + mobject.get_coord(dim=dim, direction=direction), + dim=dim, + direction=direction, + ) + + # def match_depth(self) -> Self: + # return self.set_depth() + + # def match_dim_size(self) -> Self: + # return self.set_dim_size() + + # def match_height(self) -> Self: + # return self.set_height() + + def match_points(self, mobject: "Positionable") -> Self: + return self.set_points(mobject.get_points()) + + # def match_width(self) -> Self: + # return self.set_width() + + def match_x( + self, + mobject: "Positionable", + direction: Vector3DLike = ORIGIN, + ) -> Self: + return self.set_x( + x=mobject.get_x(direction=direction), + direction=direction, + ) + + def match_y( + self, + mobject: "Positionable", + direction: Vector3DLike = ORIGIN, + ) -> Self: + return self.set_y( + y=mobject.get_y(direction=direction), + direction=direction, + ) + + def match_z( + self, + mobject: "Positionable", + direction: Vector3DLike = ORIGIN, + ) -> Self: + return self.set_z( + z=mobject.get_z(direction=direction), + direction=direction, + ) + + def move_to( + self, + point_or_mobject: "Point3DLike | Positionable", + aligned_edge: Vector3DLike = ORIGIN, + coor_mask: Vector3DLike = np.array([1, 1, 1]), + ) -> Self: + source = self.get_critical_point(aligned_edge) + target = point_or_mobject.get_critical_point(aligned_edge) if isinstance(point_or_mobject, Positionable) else point_or_mobject + self.shift((target - source) * coor_mask) + return self + + def next_to( + self, + mobject_or_point: "Positionable | Point3DLike", + direction: Vector3DLike = RIGHT, + buff: float = DEFAULT_MOBJECT_TO_MOBJECT_BUFFER, + aligned_edge: Vector3DLike = ORIGIN, + coor_mask: Vector3DLike = np.array([1, 1, 1]), + ) -> Self: + np_direction = np.asarray(direction) + np_aligned_edge = np.asarray(aligned_edge) + source = self.get_critical_point(np_aligned_edge - np_direction) + target = mobject_or_point.get_critical_point(np_aligned_edge + np_direction) if isinstance(mobject_or_point, Positionable) else mobject_or_point + return self.shift((target - source + buff * np_direction) * coor_mask) + + def pose_at_angle(self, **kwargs: Any) -> Self: + raise NotImplementedError + + def put_start_and_end_on(self, start: Point3DLike, end: Point3DLike) -> Self: + raise NotImplementedError + + def reduce_across_dimension( + self, + reduce_func: Callable[[Iterable[float]], float], + dim: int, + ) -> float | None: + raise NotImplementedError + + def rescale_to_fit( + self, + length: float, + dim: int, + stretch: bool = False, + about_point: Point3DLike | None = None, + about_edge: Vector3DLike | None = None, + ) -> Self: + old_length = self.length_over_dim(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 rotate( + self, + angle: float, + axis: Vector3DLike = OUT, + *, + about_point: Point3DLike | None = None, + about_edge: Vector3DLike | None = None, + ) -> Self: + return self.apply_matrix( + matrix=rotation_matrix(angle, axis), + about_point=about_point, + about_edge=about_edge, + ) + + def rotate_about_origin( + self, + angle: float, + axis: Vector3DLike = OUT, + ) -> Self: + return self.rotate( + angle=angle, + axis=axis, + about_point=ORIGIN, + ) + + def scale( + self, + scale_factor: float, + about_point: Point3DLike | None = None, + about_edge: Vector3DLike | None = None, + ) -> Self: + raise NotImplementedError + + def scale_to_fit_depth(self) -> Self: + raise NotImplementedError + + def scale_to_fit_height(self) -> Self: + raise NotImplementedError + + def scale_to_fit_width(self) -> Self: + raise NotImplementedError + + def set_coord( + self, + value: float, + dim: int, + direction: Vector3DLike = ORIGIN, + ) -> Self: + target = self.get_critical_point(direction=direction) + target[dim] = value + return self.move_to(point_or_mobject=target, aligned_edge=direction) + + def set_x( + self, + x: float, + direction: Vector3DLike = ORIGIN, + ) -> Self: + return self.set_coord(value=x, dim=0, direction=direction) + + def set_y(self, y: float, direction: Vector3DLike = ORIGIN) -> Self: + return self.set_coord(value=y, dim=1, direction=direction) + + def set_z(self, z: float, direction: Vector3DLike = ORIGIN) -> Self: + return self.set_coord(value=z, dim=2, direction=direction) + + def shift(self, vector: Vector3DLike) -> Self: + points = self.get_points() + self.set_points(points + vector) + return self + + def shift_onto_screen(self) -> Self: + raise NotImplementedError + + 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 + + return self.apply_points_function( + func=func, + about_point=about_point, + about_edge=about_edge, + ) + + def stretch_about_point(self) -> Self: + raise NotImplementedError + + def stretch_to_fit_depth(self) -> Self: + raise NotImplementedError + + def stretch_to_fit_height(self) -> Self: + raise NotImplementedError + + def stretch_to_fit_width(self) -> Self: + raise NotImplementedError + + def to_corner( + self, + corner: Vector3DLike = DL, + buff: float = DEFAULT_MOBJECT_TO_EDGE_BUFFER, + ) -> Self: + return self.align_on_border(direction=corner, buff=buff) + + def to_edge( + self, + edge: Vector3DLike = LEFT, + buff: float = DEFAULT_MOBJECT_TO_EDGE_BUFFER, + ) -> Self: + return self.align_on_border(direction=edge, buff=buff) + @property def width(self) -> float: return self.length_over_dim(0) - @property - def height(self) -> float: - return self.length_over_dim(1) - @property - def depth(self) -> float: - return self.length_over_dim(2) +def main() -> None: + mob_1 = Mobject() + mob_2 = Positionable() + rng = np.random.default_rng(seed=1) - def get_bottom(self) -> Point3D: - points = self.get_points() - x = (points[:, 0].min() + points[:, 0].max()) / 2 - y = points[:, 1].min() - z = (points[:, 2].min() + points[:, 2].max()) / 2 - return np.array([x, y, z]) - - # Helper Methods - # align_on_border - # align_to - # apply_complex_function - # apply_function - # apply_function_to_position - # apply_matrix - # apply_over_attr_arrays - # apply_points_function - # apply_points_function_about_point - # center - # flip - # get_array_attrs - # get_bottom - # get_boundary_point - # get_bounding_box - # get_bounding_box_point - # get_center - # get_center_of_mass - # get_continuous_bounding_box_point - # get_coord - # get_corner - # get_critical_point - # get_depth - # get_edge_center - # get_extremum_along_dim - # get_height - # get_left - # get_midpoint - # get_nadir - # get_points_defining_boundary - # get_right - # get_top - # get_width - # get_x - # get_y - # get_z - # get_zenith - # is_off_screen - # is_point_touching - # length_over_dim - # match_coord - # match_depth - # match_dim_size - # match_height - # match_width - # match_x - # match_y - # match_z - # move_to - # next_to - # pfp - # point_from_proportion - # pose_at_angle - # proportion_from_point - # reduce_across_dimension - # rescale_to_fit - # rotate - # rotate_about_origin - # scale - # scale_to_fit_depth - # scale_to_fit_height - # scale_to_fit_width - # set_coord - # set_depth - # set_height - # set_width - # set_x - # set_y - # set_z - # shift - # shift_onto_screen - # stretch - # stretch_about_point - # stretch_to_fit_depth - # stretch_to_fit_height - # stretch_to_fit_width - # to_corner - # to_edge + def validate_getter(getter: Callable[[Mobject | Positionable], np.typing.ArrayLike]) -> None: + expected: np.typing.ArrayLike | None = None + with contextlib.suppress(Exception): + expected = getter(mob_1) + if expected is not None: + got = getter(mob_2) + assert np.allclose(got, expected), f"'{got}' - '{expected}'\n" + + def validate_setter(setter: Callable[[Mobject | Positionable], Any]): + points_1 = np.asarray(mob_1.points).copy() + points_2 = np.asarray(mob_2.points).copy() + setter(mob_1) + setter(mob_2) + assert np.allclose(mob_1.points, mob_2.points) + + mob_1.points = points_1 + mob_2.points = points_2 + + def random_number(low: float = -10, high: float = 10) -> float: + return rng.uniform(low=low, high=high) + + def random_point(low: float = -10, high: float = 10) -> Point3D: + return rng.uniform(low=low, high=high, size=3) + + def random_points(low: float = -10, high: float = 10, size: int = 1) -> np.ndarray: + return rng.uniform(low=low, high=high, size=(size, 3)) + + def random_vector(low: float = -3, high: float = 3) -> Vector3D: + return rng.uniform(low=low, high=high, size=3) + + def create_another[T: Mobject | Positionable](mob: T, points: Point3D_Array) -> T: + another = mob.__class__() + another.points = points + return another + + # Try from 1 to a 100 points + point_counts = list(range(1, 101)) + rng.shuffle(point_counts) + for point_count in point_counts: + # Validate every point count 100 times + for _ in range(100): + # Generate random points + points = random_points(size=point_count) + mob_1.points = mob_2.points = points.copy() + + validate_setter(lambda mob, d=random_vector(), b=random_number(): mob.align_on_border(direction=d, buff=b)) + validate_setter(lambda mob, p=random_point(), d=random_vector(): mob.align_to(mobject_or_point=p, direction=d)) + # validate_setter(lambda mob: mob.apply_complex_function(...)) + # validate_setter(lambda mob: mob.apply_function(...)) + # validate_setter(lambda mob: mob.apply_function_to_position(...)) + # validate_setter(lambda mob: mob.apply_matrix(...)) + # validate_setter(lambda mob: mob.apply_points_function_about_point(...)) + validate_setter(lambda mob: mob.center()) + validate_getter(lambda mob: mob.depth) + # validate_setter(lambda mob, v=random_number(): setattr(mob, "depth", v)) + validate_setter(lambda mob, a=random_vector(), p=random_point(), e=random_vector(): mob.flip(axis=a, about_point=p, about_edge=e)) + validate_getter(lambda mob: mob.get_bottom()) + validate_getter(lambda mob, d=random_vector(): mob.get_boundary_point(direction=d)) + validate_getter(lambda mob: mob.get_center()) + validate_getter(lambda mob: mob.get_center_of_mass()) + for dim in [0, 1, 2]: + validate_getter(lambda mob, d=random_vector(): mob.get_coord(dim=dim, direction=d)) + validate_getter(lambda mob, d=random_vector(): mob.get_corner(direction=d)) + validate_getter(lambda mob, d=random_vector(): mob.get_critical_point(direction=d)) + validate_getter(lambda mob, d=random_vector(): mob.get_edge_center(direction=d)) + validate_getter(lambda mob: mob.get_end()) + for dim in [0, 1, 2]: + for key in [0, 1, 2]: + validate_getter(lambda mob: mob.get_extremum_along_dim(dim=dim, key=key)) + validate_getter(lambda mob: mob.get_left()) + validate_getter(lambda mob: mob.get_nadir()) + validate_getter(lambda mob: mob.get_right()) + validate_getter(lambda mob: mob.get_start()) + validate_getter(lambda mob: mob.get_start_and_end()) + validate_getter(lambda mob: mob.get_top()) + validate_getter(lambda mob: mob.get_x()) + validate_getter(lambda mob: mob.get_y()) + validate_getter(lambda mob: mob.get_z()) + validate_getter(lambda mob: mob.get_zenith()) + validate_getter(lambda mob: mob.height) + # validate_setter(lambda mob, h=random_number(): setattr(mob, "height", h)) + for dim in [0, 1, 2]: + validate_getter(lambda mob: mob.length_over_dim(dim=dim)) + + points = random_point() + + for dim in [0, 1, 2]: + validate_setter(lambda mob, p=random_points(size=int(random_number(1, 100))): mob.match_coord(mobject=create_another(mob=mob, points=p), dim=dim)) + # validate_setter(lambda mob: mob.match_depth()) + # validate_setter(lambda mob: mob.match_dim_size()) + # validate_setter(lambda mob: mob.match_height()) + validate_setter(lambda mob, p=random_points(size=int(random_number(1, 100))): mob.match_points(mobject=create_another(mob=mob, points=p))) + # validate_setter(lambda mob: mob.match_width()) + validate_setter(lambda mob, p=random_points(size=int(random_number(1, 100))): mob.match_x(mobject=create_another(mob=mob, points=p))) + validate_setter(lambda mob, p=random_points(size=int(random_number(1, 100))): mob.match_y(mobject=create_another(mob=mob, points=p))) + validate_setter(lambda mob, p=random_points(size=int(random_number(1, 100))): mob.match_z(mobject=create_another(mob=mob, points=p))) + validate_setter(lambda mob, p=random_point(), e=random_vector(), m=random_vector(): mob.move_to(point_or_mobject=p, aligned_edge=e, coor_mask=m)) + validate_setter( + lambda mob, p=random_point(), d=random_vector(), b=random_number(), e=random_vector(), m=random_vector(): mob.next_to( + mobject_or_point=p, direction=d, buff=b, aligned_edge=e, coor_mask=m + ) + ) + # validate_setter(lambda mob: mob.pose_at_angle()) + # validate_setter(lambda mob: mob.put_start_and_end_on()) + # validate_getter(lambda mob: mob.reduce_across_dimension()) + # validate_setter(lambda mob: mob.rescale_to_fit()) + validate_setter(lambda mob, a=random_number(), ax=random_vector(), e=random_vector(): mob.rotate(angle=a, axis=ax, about_edge=e)) + # validate_setter(lambda mob, a=random_number(), ax=random_vector(),: mob.rotate_about_origin(angle=a, axis=ax)) + # validate_setter(lambda mob: mob.scale()) + # validate_setter(lambda mob: mob.scale_to_fit_depth()) + # validate_setter(lambda mob: mob.scale_to_fit_height()) + # validate_setter(lambda mob: mob.scale_to_fit_width()) + for dim in [0, 1, 2]: + validate_setter(lambda mob, v=random_number(), d=random_vector(): mob.set_coord(value=v, dim=dim, direction=d)) + validate_setter(lambda mob, x=random_number(), d=random_vector(): mob.set_x(x=x, direction=d)) + validate_setter(lambda mob, y=random_number(), d=random_vector(): mob.set_y(y=y, direction=d)) + validate_setter(lambda mob, z=random_number(), d=random_vector(): mob.set_z(z=z, direction=d)) + validate_setter(lambda mob, v=random_vector(): mob.shift(v)) + # validate_setter(lambda mob, v=random_vector(): mob.shift_onto_screen()) + for dim in [0, 1, 2]: + validate_setter(lambda mob, f=random_number(), p=random_point(), e=random_vector(): mob.stretch(factor=f, dim=dim, about_point=p, about_edge=e)) + # validate_setter(lambda mob: mob.stretch_about_point()) + # validate_setter(lambda mob: mob.stretch_to_fit_depth()) + # validate_setter(lambda mob: mob.stretch_to_fit_height()) + # validate_setter(lambda mob: mob.stretch_to_fit_width()) + validate_setter(lambda mob, c=random_vector(), b=random_number(): mob.to_corner(corner=c, buff=b)) + validate_setter(lambda mob, c=random_vector(), b=random_number(): mob.to_edge(edge=c, buff=b)) + validate_getter(lambda mob: mob.width) + # validate_setter(lambda mob, w=random_number(): setattr(mob, "width", w)) if __name__ == "__main__": - mob = Positionable(*[(0, -1, 0), (0, -2, 1)]) - print(mob.get_bottom()) + main() diff --git a/manim/mobject/abstract/ruff.toml b/manim/mobject/abstract/ruff.toml new file mode 100644 index 0000000000..2cfeee68dd --- /dev/null +++ b/manim/mobject/abstract/ruff.toml @@ -0,0 +1 @@ +line-length = 200 \ No newline at end of file From c7eb89a1f64a27b90df9d50222cdc1c570315285 Mon Sep 17 00:00:00 2001 From: GniLudio <50866361+GniLudio@users.noreply.github.com> Date: Mon, 17 Aug 2026 23:31:43 +0200 Subject: [PATCH 03/66] Update --- manim/mobject/abstract/positionable.py | 247 +++++-------------------- manim/mobject/abstract/test.py | 201 ++++++++++++++++++++ 2 files changed, 251 insertions(+), 197 deletions(-) create mode 100644 manim/mobject/abstract/test.py diff --git a/manim/mobject/abstract/positionable.py b/manim/mobject/abstract/positionable.py index 4e71508cd3..474b91396e 100644 --- a/manim/mobject/abstract/positionable.py +++ b/manim/mobject/abstract/positionable.py @@ -1,4 +1,3 @@ -import contextlib from collections.abc import Callable, Iterable from typing import Any, Self @@ -24,8 +23,6 @@ Point3D, Point3D_Array, Point3DLike, - Point3DLike_Array, - Vector3D, Vector3DLike, ) from manim.utils.space_ops import rotation_matrix @@ -35,23 +32,16 @@ class Positionable: # FUNDAMENTALS points: Point3D_Array - def get_points(self) -> Point3D_Array: - return self.points - - def set_points(self, points: Point3DLike_Array) -> Self: - self.points = np.asarray(points) - return self - # METHODS + # TODO: Add a parameter for the frame? def align_on_border( self, direction: Vector3DLike, buff: float = DEFAULT_MOBJECT_TO_EDGE_BUFFER, ) -> Self: - # TODO: Add parameter for this? frame = (config.frame_x_radius, config.frame_y_radius, 0) - target = np.sign(direction) * frame - buff * np.array(direction) + target = np.sign(direction) * frame - buff * np.asarray(direction) self.move_to(point_or_mobject=target, aligned_edge=direction) return self @@ -75,7 +65,11 @@ def R3_func(point: Point3D) -> Point3D: 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) + return self.apply_function( + function=R3_func, + about_point=about_point, + about_edge=about_edge, + ) def apply_function( self, @@ -88,17 +82,14 @@ def apply_function( about_point = ORIGIN def multi_mapping_function(points: Point3D_Array) -> Point3D_Array: - result: Point3D_Array = np.apply_along_axis(function, 1, points) - return result + return np.apply_along_axis(func1d=function, axis=1, arr=points) - self.apply_points_function_about_point( - multi_mapping_function, - about_point, - about_edge, + return self.apply_points_function_about_point( + function=multi_mapping_function, + about_point=about_point, + about_edge=about_edge, ) - return self - # TODO: Do we really need this? def apply_function_to_position( self, function: Callable[[Point3D], Point3D], @@ -108,44 +99,49 @@ def apply_function_to_position( def apply_matrix( self, matrix: MatrixMN, - *, about_point: Point3DLike | None = None, about_edge: Vector3DLike | None = None, ) -> Self: 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 + + # Fast path for standard 3x3 matrices + if matrix.shape == (3, 3): + full_matrix = matrix + else: + full_matrix = np.identity(3) + full_matrix[: matrix.shape[0], : matrix.shape[1]] = matrix + return self.apply_points_function( - lambda points: np.dot(points, full_matrix.T), + lambda points: points.dot(full_matrix.T), about_point=about_point, about_edge=about_edge, ) def apply_points_function( self, - func: Callable[[Point3D], Point3D], + function: Callable[[Point3D], Point3D], about_point: Point3DLike | None = None, about_edge: Vector3DLike | None = None, ) -> Self: if about_point is None: about_point = self.get_critical_point(direction=about_edge if about_edge is not None else ORIGIN) - points = self.get_points() - points -= about_point - points = func(points) - points += about_point - return self.set_points(points) + self.points -= about_point + self.points = function(self.points) + self.points += about_point + return self # @deprecated(message="Use apply_points_function() instead.") def apply_points_function_about_point( self, - func: Callable[[Point3D], Point3D], + function: Callable[[Point3D], Point3D], about_point: Point3DLike | None = None, about_edge: Vector3DLike | None = None, ) -> Self: return self.apply_points_function( - func=func, + function=function, about_point=about_point, about_edge=about_edge, ) @@ -175,14 +171,12 @@ def get_bottom(self) -> Point3D: # TODO: Should this function be dropped? def get_boundary_point(self, direction: Vector3DLike) -> Point3D: - all_points = self.get_points() - index = np.argmax(np.dot(all_points, direction)) - return all_points[index] + index = np.argmax(np.dot(self.points, direction)) + return self.points[index] def get_bounding_box(self) -> Point3D_Array: - points = self.get_points() - mins = points.min(axis=0) - maxs = points.max(axis=0) + mins = self.points.min(axis=0) + maxs = self.points.max(axis=0) mids = (mins + maxs) / 2 return np.array([mins, mids, maxs]) @@ -190,7 +184,7 @@ def get_center(self) -> Point3D: return self.get_critical_point(ORIGIN) def get_center_of_mass(self) -> Point3D: - return self.get_points().mean(axis=0) + return self.points.mean(axis=0) def get_coord(self, dim: int, direction: Vector3DLike = ORIGIN) -> float: return self.get_critical_point(direction=direction)[dim] @@ -264,8 +258,7 @@ def height(self, value: float) -> None: raise NotImplementedError def length_over_dim(self, dim: int) -> float: - points = self.get_points() - values = points[:, dim] + values = self.points[:, dim] return values.max() - values.min() def match_coord( @@ -290,7 +283,8 @@ def match_coord( # return self.set_height() def match_points(self, mobject: "Positionable") -> Self: - return self.set_points(mobject.get_points()) + self.points = mobject.points.copy() + return self # def match_width(self) -> Self: # return self.set_width() @@ -371,14 +365,15 @@ def rescale_to_fit( about_point: Point3DLike | None = None, about_edge: Vector3DLike | None = None, ) -> Self: - old_length = self.length_over_dim(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 + raise NotImplementedError + # old_length = self.length_over_dim(dim=dim) + # if old_length == 0: + # return self + # if stretch: + # self.stretch(length / old_length, dim, ...) + # else: + # self.scale(length / old_length, ...) + # return self def rotate( self, @@ -446,8 +441,7 @@ def set_z(self, z: float, direction: Vector3DLike = ORIGIN) -> Self: return self.set_coord(value=z, dim=2, direction=direction) def shift(self, vector: Vector3DLike) -> Self: - points = self.get_points() - self.set_points(points + vector) + self.points += vector return self def shift_onto_screen(self) -> Self: @@ -457,16 +451,15 @@ 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: + def function(points: Point3D_Array) -> Point3D_Array: points[:, dim] *= factor return points return self.apply_points_function( - func=func, + function=function, about_point=about_point, about_edge=about_edge, ) @@ -499,144 +492,4 @@ def to_edge( @property def width(self) -> float: - return self.length_over_dim(0) - - -def main() -> None: - mob_1 = Mobject() - mob_2 = Positionable() - rng = np.random.default_rng(seed=1) - - def validate_getter(getter: Callable[[Mobject | Positionable], np.typing.ArrayLike]) -> None: - expected: np.typing.ArrayLike | None = None - with contextlib.suppress(Exception): - expected = getter(mob_1) - if expected is not None: - got = getter(mob_2) - assert np.allclose(got, expected), f"'{got}' - '{expected}'\n" - - def validate_setter(setter: Callable[[Mobject | Positionable], Any]): - points_1 = np.asarray(mob_1.points).copy() - points_2 = np.asarray(mob_2.points).copy() - setter(mob_1) - setter(mob_2) - assert np.allclose(mob_1.points, mob_2.points) - - mob_1.points = points_1 - mob_2.points = points_2 - - def random_number(low: float = -10, high: float = 10) -> float: - return rng.uniform(low=low, high=high) - - def random_point(low: float = -10, high: float = 10) -> Point3D: - return rng.uniform(low=low, high=high, size=3) - - def random_points(low: float = -10, high: float = 10, size: int = 1) -> np.ndarray: - return rng.uniform(low=low, high=high, size=(size, 3)) - - def random_vector(low: float = -3, high: float = 3) -> Vector3D: - return rng.uniform(low=low, high=high, size=3) - - def create_another[T: Mobject | Positionable](mob: T, points: Point3D_Array) -> T: - another = mob.__class__() - another.points = points - return another - - # Try from 1 to a 100 points - point_counts = list(range(1, 101)) - rng.shuffle(point_counts) - for point_count in point_counts: - # Validate every point count 100 times - for _ in range(100): - # Generate random points - points = random_points(size=point_count) - mob_1.points = mob_2.points = points.copy() - - validate_setter(lambda mob, d=random_vector(), b=random_number(): mob.align_on_border(direction=d, buff=b)) - validate_setter(lambda mob, p=random_point(), d=random_vector(): mob.align_to(mobject_or_point=p, direction=d)) - # validate_setter(lambda mob: mob.apply_complex_function(...)) - # validate_setter(lambda mob: mob.apply_function(...)) - # validate_setter(lambda mob: mob.apply_function_to_position(...)) - # validate_setter(lambda mob: mob.apply_matrix(...)) - # validate_setter(lambda mob: mob.apply_points_function_about_point(...)) - validate_setter(lambda mob: mob.center()) - validate_getter(lambda mob: mob.depth) - # validate_setter(lambda mob, v=random_number(): setattr(mob, "depth", v)) - validate_setter(lambda mob, a=random_vector(), p=random_point(), e=random_vector(): mob.flip(axis=a, about_point=p, about_edge=e)) - validate_getter(lambda mob: mob.get_bottom()) - validate_getter(lambda mob, d=random_vector(): mob.get_boundary_point(direction=d)) - validate_getter(lambda mob: mob.get_center()) - validate_getter(lambda mob: mob.get_center_of_mass()) - for dim in [0, 1, 2]: - validate_getter(lambda mob, d=random_vector(): mob.get_coord(dim=dim, direction=d)) - validate_getter(lambda mob, d=random_vector(): mob.get_corner(direction=d)) - validate_getter(lambda mob, d=random_vector(): mob.get_critical_point(direction=d)) - validate_getter(lambda mob, d=random_vector(): mob.get_edge_center(direction=d)) - validate_getter(lambda mob: mob.get_end()) - for dim in [0, 1, 2]: - for key in [0, 1, 2]: - validate_getter(lambda mob: mob.get_extremum_along_dim(dim=dim, key=key)) - validate_getter(lambda mob: mob.get_left()) - validate_getter(lambda mob: mob.get_nadir()) - validate_getter(lambda mob: mob.get_right()) - validate_getter(lambda mob: mob.get_start()) - validate_getter(lambda mob: mob.get_start_and_end()) - validate_getter(lambda mob: mob.get_top()) - validate_getter(lambda mob: mob.get_x()) - validate_getter(lambda mob: mob.get_y()) - validate_getter(lambda mob: mob.get_z()) - validate_getter(lambda mob: mob.get_zenith()) - validate_getter(lambda mob: mob.height) - # validate_setter(lambda mob, h=random_number(): setattr(mob, "height", h)) - for dim in [0, 1, 2]: - validate_getter(lambda mob: mob.length_over_dim(dim=dim)) - - points = random_point() - - for dim in [0, 1, 2]: - validate_setter(lambda mob, p=random_points(size=int(random_number(1, 100))): mob.match_coord(mobject=create_another(mob=mob, points=p), dim=dim)) - # validate_setter(lambda mob: mob.match_depth()) - # validate_setter(lambda mob: mob.match_dim_size()) - # validate_setter(lambda mob: mob.match_height()) - validate_setter(lambda mob, p=random_points(size=int(random_number(1, 100))): mob.match_points(mobject=create_another(mob=mob, points=p))) - # validate_setter(lambda mob: mob.match_width()) - validate_setter(lambda mob, p=random_points(size=int(random_number(1, 100))): mob.match_x(mobject=create_another(mob=mob, points=p))) - validate_setter(lambda mob, p=random_points(size=int(random_number(1, 100))): mob.match_y(mobject=create_another(mob=mob, points=p))) - validate_setter(lambda mob, p=random_points(size=int(random_number(1, 100))): mob.match_z(mobject=create_another(mob=mob, points=p))) - validate_setter(lambda mob, p=random_point(), e=random_vector(), m=random_vector(): mob.move_to(point_or_mobject=p, aligned_edge=e, coor_mask=m)) - validate_setter( - lambda mob, p=random_point(), d=random_vector(), b=random_number(), e=random_vector(), m=random_vector(): mob.next_to( - mobject_or_point=p, direction=d, buff=b, aligned_edge=e, coor_mask=m - ) - ) - # validate_setter(lambda mob: mob.pose_at_angle()) - # validate_setter(lambda mob: mob.put_start_and_end_on()) - # validate_getter(lambda mob: mob.reduce_across_dimension()) - # validate_setter(lambda mob: mob.rescale_to_fit()) - validate_setter(lambda mob, a=random_number(), ax=random_vector(), e=random_vector(): mob.rotate(angle=a, axis=ax, about_edge=e)) - # validate_setter(lambda mob, a=random_number(), ax=random_vector(),: mob.rotate_about_origin(angle=a, axis=ax)) - # validate_setter(lambda mob: mob.scale()) - # validate_setter(lambda mob: mob.scale_to_fit_depth()) - # validate_setter(lambda mob: mob.scale_to_fit_height()) - # validate_setter(lambda mob: mob.scale_to_fit_width()) - for dim in [0, 1, 2]: - validate_setter(lambda mob, v=random_number(), d=random_vector(): mob.set_coord(value=v, dim=dim, direction=d)) - validate_setter(lambda mob, x=random_number(), d=random_vector(): mob.set_x(x=x, direction=d)) - validate_setter(lambda mob, y=random_number(), d=random_vector(): mob.set_y(y=y, direction=d)) - validate_setter(lambda mob, z=random_number(), d=random_vector(): mob.set_z(z=z, direction=d)) - validate_setter(lambda mob, v=random_vector(): mob.shift(v)) - # validate_setter(lambda mob, v=random_vector(): mob.shift_onto_screen()) - for dim in [0, 1, 2]: - validate_setter(lambda mob, f=random_number(), p=random_point(), e=random_vector(): mob.stretch(factor=f, dim=dim, about_point=p, about_edge=e)) - # validate_setter(lambda mob: mob.stretch_about_point()) - # validate_setter(lambda mob: mob.stretch_to_fit_depth()) - # validate_setter(lambda mob: mob.stretch_to_fit_height()) - # validate_setter(lambda mob: mob.stretch_to_fit_width()) - validate_setter(lambda mob, c=random_vector(), b=random_number(): mob.to_corner(corner=c, buff=b)) - validate_setter(lambda mob, c=random_vector(), b=random_number(): mob.to_edge(edge=c, buff=b)) - validate_getter(lambda mob: mob.width) - # validate_setter(lambda mob, w=random_number(): setattr(mob, "width", w)) - - -if __name__ == "__main__": - main() + return self.length_over_dim(dim=0) diff --git a/manim/mobject/abstract/test.py b/manim/mobject/abstract/test.py new file mode 100644 index 0000000000..c1657ae73a --- /dev/null +++ b/manim/mobject/abstract/test.py @@ -0,0 +1,201 @@ +from collections.abc import Callable +import contextlib +import time +from typing import Any + +import numpy as np + +from manim.mobject.abstract.positionable import Positionable +from manim.mobject.mobject import Mobject +from manim.typing import Point3D, Point3D_Array, Vector3D + + +_RNG = np.random.default_rng(seed=1) + + +def random_number(low: float = -10, high: float = 10) -> float: + return _RNG.uniform(low=low, high=high) + + +def random_point(low: float = -10, high: float = 10) -> Point3D: + return _RNG.uniform(low=low, high=high, size=3) + + +def random_points(low: float = -10, high: float = 10, size: int = 1) -> np.ndarray: + return _RNG.uniform(low=low, high=high, size=(size, 3)) + + +def random_vector(low: float = -3, high: float = 3) -> Vector3D: + return _RNG.uniform(low=low, high=high, size=3) + + +def create_another[T: Mobject | Positionable](mob: T, points: Point3D_Array) -> T: + another = mob.__class__() + another.points = points + return another + + +def create_mobs(point_count: int) -> tuple[Mobject, Positionable]: + points = random_points(size=point_count) + mob_1 = Mobject() + mob_2 = Positionable() + mob_1.points = points.copy() + mob_2.points = points.copy() + return mob_1, mob_2 + + +def validate_function( + name: str, + function: Callable[[Mobject | Positionable, dict], Any], + validate: Callable[[Any, Any], None], + create_kwargs: Callable[[], dict], + point_counts: list[int] = list(range(1, 101)), + loop_count: int = 100, +) -> None: + point_counts = point_counts.copy() + _RNG.shuffle(point_counts) + + time_0, time_1 = 0, 0 + + for point_count in point_counts: + for _ in range(loop_count): + mob_1, mob_2 = create_mobs(point_count=point_count) + kwargs = create_kwargs() + + result_0: np.typing.ArrayLike | None = None + with contextlib.suppress(Exception): + start = time.perf_counter_ns() + result_0 = function(mob_1, kwargs) + time_0 += time.perf_counter_ns() - start + + if result_0 is not None: + start = time.perf_counter_ns() + result_1 = function(mob_2, kwargs) + time_1 += time.perf_counter_ns() - start + + validate(result_0, result_1) + + print(name, f"{time_0 / time_1:2.4f}") + + +def validate_setter( + name: str, + function: Callable[[Mobject | Positionable, dict], Any], + create_kwargs: Callable[[], Any] = lambda: {}, +): + def validate(result_1: Any, result_2: Any) -> None: + assert isinstance(result_1, Positionable | Mobject) + assert isinstance(result_2, Positionable | Mobject) + assert np.allclose(result_1.points, result_2.points) + + validate_function( + name=name, + function=function, + validate=validate, + create_kwargs=create_kwargs, + ) + + +def validate_getter( + name: str, + function: Callable[[Mobject | Positionable, dict], Any], + create_kwargs: Callable[[], Any] = lambda: {}, +): + def validate(result_1: Any, result_2: Any) -> None: + assert np.allclose(result_1, result_2) + + validate_function( + name=name, + function=function, + validate=validate, + create_kwargs=create_kwargs, + ) + + +def main() -> None: + validate_setter("align_on_border", lambda mob, kwargs: mob.align_on_border(**kwargs), lambda: {"direction": random_vector(), "buff": random_number()}) + validate_setter("align_to", lambda mob, kwargs: mob.align_to(**kwargs), lambda: {"mobject_or_point": random_point(), "direction": random_vector()}) + # validate_setter("apply_complex_function", lambda mob: mob.apply_complex_function(...)) + # validate_setter("apply_function", lambda mob: mob.apply_function(...)) + # validate_setter("apply_function_to_position", lambda mob: mob.apply_function_to_position(...)) + # validate_setter("apply_matrix", lambda mob: mob.apply_matrix(...)) + # validate_setter("apply_points_function_about_point", lambda mob: mob.apply_points_function_about_point(...)) + validate_setter("center", lambda mob, _: mob.center()) + validate_getter("depth", lambda mob, _: mob.depth) + # validate_setter("depth", lambda mob, v=random_number(): setattr(mob, "depth", v)) + validate_setter("flip", lambda mob, kwargs: mob.flip(**kwargs), lambda: {"axis": random_vector(), "about_point": random_point(), "about_edge": random_vector()}) + validate_getter("get_bottom", lambda mob, _: mob.get_bottom()) + validate_getter("get_boundary_point", lambda mob, kwargs: mob.get_boundary_point(**kwargs), lambda: {"direction": random_vector()}) + validate_getter("get_center", lambda mob, _: mob.get_center()) + validate_getter("get_center_of_mass", lambda mob, _: mob.get_center_of_mass()) + for dim in [0, 1, 2]: + validate_getter("get_coord", lambda mob, kwargs: mob.get_coord(**kwargs), lambda: {"dim": dim, "direction": random_vector()}) + validate_getter("get_corner", lambda mob, kwargs: mob.get_corner(**kwargs), lambda: {"direction": random_vector()}) + validate_getter("get_critical_point", lambda mob, kwargs: mob.get_critical_point(**kwargs), lambda: {"direction": random_vector()}) + validate_getter("get_edge_center", lambda mob, kwargs: mob.get_edge_center(**kwargs), lambda: {"direction": random_vector()}) + validate_getter("get_end", lambda mob, _: mob.get_end()) + for dim in [0, 1, 2]: + for key in [0, 1, 2]: + validate_getter("get_extremum_along_dim", lambda mob, kwargs: mob.get_extremum_along_dim(**kwargs), lambda: {"dim": dim, "key": key}) + validate_getter("get_left", lambda mob, _: mob.get_left()) + validate_getter("get_nadir", lambda mob, _: mob.get_nadir()) + validate_getter("get_right", lambda mob, _: mob.get_right()) + validate_getter("get_start", lambda mob, _: mob.get_start()) + validate_getter("get_start_and_end", lambda mob, _: mob.get_start_and_end()) + validate_getter("get_top", lambda mob, _: mob.get_top()) + validate_getter("get_x", lambda mob, _: mob.get_x()) + validate_getter("get_y", lambda mob, _: mob.get_y()) + validate_getter("get_z", lambda mob, _: mob.get_z()) + validate_getter("get_zenith", lambda mob, _: mob.get_zenith()) + validate_getter("height", lambda mob, _: mob.height) + # validate_setter("height", lambda mob, h=random_number(): setattr(mob, "height", h)) + for dim in [0, 1, 2]: + validate_getter("length_over_dim", lambda mob, _: mob.length_over_dim(dim=dim)) + + for dim in [0, 1, 2]: + validate_setter("match_coord", lambda mob, kwargs: mob.match_coord(mobject=create_another(mob=mob, **kwargs), dim=dim), lambda: {"points": random_points(size=int(random_number(1, 100)))}) + # validate_setter("match_depth", lambda mob: mob.match_depth()) + # validate_setter("match_dim_size", lambda mob: mob.match_dim_size()) + # validate_setter("match_height", lambda mob: mob.match_height()) + validate_setter("match_points", lambda mob, kwargs: mob.match_points(mobject=create_another(mob=mob, **kwargs)), lambda: {"points": random_points(size=int(random_number(1, 100)))}) + # validate_setter("match_width", lambda mob: mob.match_width()) + validate_setter("match_x", lambda mob, kwargs: mob.match_x(mobject=create_another(mob=mob, **kwargs)), lambda: {"points": random_points(size=int(random_number(1, 100)))}) + validate_setter("match_y", lambda mob, kwargs: mob.match_y(mobject=create_another(mob=mob, **kwargs)), lambda: {"points": random_points(size=int(random_number(1, 100)))}) + validate_setter("match_z", lambda mob, kwargs: mob.match_z(mobject=create_another(mob=mob, **kwargs)), lambda: {"points": random_points(size=int(random_number(1, 100)))}) + validate_setter("move_to", lambda mob, kwargs: mob.move_to(**kwargs), lambda: {"point_or_mobject": random_point(), "aligned_edge": random_vector(), "coor_mask": random_vector()}) + validate_setter( + "next_to", + lambda mob, kwargs: mob.next_to(**kwargs), + lambda: {"mobject_or_point": random_point(), "direction": random_vector(), "buff": random_number(), "aligned_edge": random_vector(), "coor_mask": random_vector()}, + ) + # validate_setter("pose_at_angle", lambda mob: mob.pose_at_angle()) + # validate_setter("put_start_and_end_on", lambda mob: mob.put_start_and_end_on()) + # validate_getter("reduce_across_dimension", lambda mob: mob.reduce_across_dimension()) + # validate_setter("rescale_to_fit", lambda mob: mob.rescale_to_fit()) + validate_setter("rotate", lambda mob, kwargs: mob.rotate(**kwargs), lambda: {"angle": random_number(), "axis": random_vector(), "about_edge": random_vector()}) + # validate_setter("rotate_about_origin", lambda mob, a=random_number(), ax=random_vector(),: mob.rotate_about_origin(angle=a, axis=ax)) + # validate_setter("scale", lambda mob: mob.scale()) + # validate_setter("scale_to_fit_depth", lambda mob: mob.scale_to_fit_depth()) + # validate_setter("scale_to_fit_height", lambda mob: mob.scale_to_fit_height()) + # validate_setter("scale_to_fit_width", lambda mob: mob.scale_to_fit_width()) + for dim in [0, 1, 2]: + validate_setter("set_coord", lambda mob, kwargs: mob.set_coord(**kwargs), lambda: {"value": random_number(), "dim": dim, "direction": random_vector()}) + validate_setter("set_x", lambda mob, kwargs: mob.set_x(**kwargs), lambda: {"x": random_number(), "direction": random_vector()}) + validate_setter("set_y", lambda mob, kwargs: mob.set_y(**kwargs), lambda: {"y": random_number(), "direction": random_vector()}) + validate_setter("set_z", lambda mob, kwargs: mob.set_z(**kwargs), lambda: {"z": random_number(), "direction": random_vector()}) + validate_setter("shift", lambda mob, kwargs: mob.shift(kwargs["value"]), lambda: {"value": random_vector()}) + # validate_setter("shift_onto_screen", lambda mob, v=random_vector(): mob.shift_onto_screen()) + for dim in [0, 1, 2]: + validate_setter("stretch", lambda mob, kwargs: mob.stretch(**kwargs), lambda: {"factor": random_number(), "dim": dim, "about_point": random_point(), "about_edge": random_vector()}) + # validate_setter("stretch_about_point", lambda mob: mob.stretch_about_point()) + # validate_setter("stretch_to_fit_depth", lambda mob: mob.stretch_to_fit_depth()) + # validate_setter("stretch_to_fit_height", lambda mob: mob.stretch_to_fit_height()) + # validate_setter("stretch_to_fit_width", lambda mob: mob.stretch_to_fit_width()) + validate_setter("to_corner", lambda mob, kwargs: mob.to_corner(**kwargs), lambda: {"corner": random_vector(), "buff": random_number()}) + validate_setter("to_edge", lambda mob, kwargs: mob.to_edge(**kwargs), lambda: {"edge": random_vector(), "buff": random_number()}) + validate_getter("width", lambda mob, _: mob.width) + # validate_setter("width", lambda mob, w=random_number(): setattr(mob, "width", w)) + + +if __name__ == "__main__": + main() From 9193aed7a39cdfb28b3c69b75258ea144f1d28db Mon Sep 17 00:00:00 2001 From: GniLudio Date: Tue, 18 Aug 2026 15:26:47 +0200 Subject: [PATCH 04/66] Update --- manim/mobject/abstract/logs.txt | Bin 2142 -> 0 bytes manim/mobject/abstract/methods.txt | Bin 0 -> 14296 bytes manim/mobject/abstract/positionable.md | 40 ++- manim/mobject/abstract/positionable.py | 408 +++++++++++++------------ manim/mobject/abstract/ruff.toml | 1 - manim/mobject/abstract/test.py | 325 +++++++++++++------- 6 files changed, 468 insertions(+), 306 deletions(-) delete mode 100644 manim/mobject/abstract/logs.txt create mode 100644 manim/mobject/abstract/methods.txt delete mode 100644 manim/mobject/abstract/ruff.toml diff --git a/manim/mobject/abstract/logs.txt b/manim/mobject/abstract/logs.txt deleted file mode 100644 index 5c25b3e19a4ed891746eb065db0bdec4208deb40..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2142 zcmbW1&2HO3421VwpzqLok!KPT*>VKivV413{Q{ugFL_kdC>>f95FwGy`3t!uIxsorEjYW$H~u_@1VSJGV4?0T;##zTPIgl zllK`JV3!&6!aX*5a^JnM`Hxm==E94<%No!WliaKRo4(BEsCv&r?BqW-SUk#mP*jO^ zRxA;rZ_iCH2hFaInPp=Z&rYa`+Pk&8x(8-m>mZcrReV+ze>E&H2=k`27v1G|l->MZg_(>?iEujLpgaT%1e|&5UtHv!hE#sGtV4jHt zBet0qoyBcr`godk%XDbGjZw1IVs?w3Prp+Y+f88oJd3CqHbnR>cfXzZHuny;2>$?c CHfd`B diff --git a/manim/mobject/abstract/methods.txt b/manim/mobject/abstract/methods.txt new file mode 100644 index 0000000000000000000000000000000000000000..93c6495124cf6e1251b8a6d97a56b7b157f2304a GIT binary patch literal 14296 zcmbW8+in~;5{COW3*==sudqOF5@fITb|6cmL!!g14wmJI-%Wk}>sJ-o?4FTf*ytgP z#X9~~WViqSzu%{a>C5!zw4ctW=jrF^AJb3MF8-WN7x9OuI(9$Z2mN!*K2JCCbU!_H zG#9~XKOKVGL;U-;n7x_zY586R?RiIe8T=oE`}1@gPxrx-o!fO+F)CS}gLX7`6Z8A& zuA93VYR4|nw|sK+X?h7uorgSUAy=>WO^h(#uV(*Vw%b{omhvvNy9f@UVQO1zGE-!boahHF|Z5SzXp%T zSpp*9=_6`nE9S85*I0!=i7ZKk-Wh*CzHS#>k(Hh9=Uo)H(xg70ra`;nOK?07OI-!` zhp_PdtO0f-Yp}@G?71IXwF@4^_GNIo3q5YH1fqvgUES0(#n-V2? z0;^gN<%%{=dRwrXCQkGS=He58n|-1%0(28kc^wU6Es8tb(ic)IX$B)fdq6x8B;Cg(kqs+&QM{g z&_p`DN6+ISv`L&u4iEFF#E}#MHJDBFUUu-gUCD<4diUa7IJos zfVPn0A_g7s!g_9fpJ~+X$tKscX1iEPpIH6S&acm$VkPt1 zcb<}uu@Ut^NaN^Y4x6X{7*ThJG(<7K#Ctqf`6iuC6pjkcS3*H>@F^t0lGsu!bf&tB zz37m$=2t{fpKv}?q$XP5&w-}Kt~%G(z;=#EM7o;^A1*mx$7q`cb^X$Nb#@$|m4Xw( z#cYA)&W_(o?-;IQj$@Ds57cT-#Q?qAF|Zpnr!%0^ZFMD}R9|^j0o4CX?z98FDkm9M|4Z;Aj^KJbS6)Rf*|(+NA@oCnb?hQG9kWC* zXMUjpCrmWd-Gbtl_$Hd#HTI39g^Nde6iW)K3f5VUj5Vrs>#P1VqOv7_W?O0 zzb2lg9n52F7gfvJJLgJelP$Q5xa%=uowm<-EMqU|d5NvdaVjU7OOK@Pg1B4f0}^Y6 z>4}9?)>l-u9O@&^W@PC19OYWoC?(mJ{d7K`(a4x;O^m1hZ7j{-wI^`IC{MnJ{fI^P z3$ibx|Bg%%`usBe>mydLV=Q}8&`Q6BFFJV=H{@7WN$4KpCdqqWht)6R51Bj~8)Jhr z`DxDK^qG+P*qTGcm=Mor=#h7F4{k9MTgX6kdJY~whqhyFo+nCJzq-E9y}r8loMl&G z-8fUXxG%HyC3MLfMN3Ha0k75bj?s`lhn24pA#`6nbB-qp*84=sPw&&7|1|D1o#fCM zl*UPAdEYsue2$@3&;Lxn$A0~p0<=IwM+;GbPGoeg`Hp?Z1M%HUq|6~k^5)!kk>?TN zUqS=cXos_5Pq&d;icI`5qo9o&09NL4-degL(vF0U|w{_R^ zylSDfSW2fz&e4u6a>93X(hO?%|BeIOaXbePp{(NQ;$5SjANJbKb9kE!X({}!!?#T0 z*?ZqIoMzQ_htt+k)0e zMijjyNyAx^oTYA16HcRakGI@2BeV5f-J0ciU%zp-3PKJX<7<;_8z-? zpM8GCnvaijs&_JL-jV}Xm2rn zThVr8xvF*>Jqo3K8NY1G&>sa%`KlI?az3+ntigIt6iXEuD64y)w#N&_c%J&Y~|Ou zA5SWx39G4tCr@-wgspWK$jL;~lD)ovZFkD~dW*DHnaCuvSZ^C^-mk5ww=}hzu*Yg` z6VhNCjb`=nT|uQ*|G(a+c+J>5>|$Jy4NT8n74O4(A9>!jvRyU83a?0?8J$KmTc?>Z zeT_=KX=o+FJDhxo5VB`C{qm7<=9eB^u04kS$z#ouOABUAS=I^p-5Ai~hNa7$mX)EN zxVJjuKjz%JpMITwTRDTv_nbrgogbN`yLe)h_&Df z8?lH&f2&d>XxW{5PwvR|b(HR)mpkXX$ULDve0(25MW)LC6*^TdtvmRVH=sfs-R%p7 z{0-QMH+rfwxS79q6N0R3&rJPdQabWpKvj+`%^PQOlJ|*+c zJJIM&J*Fe#v=(HRJg&vtc0EQ_5xM1yjj=d-y~HzTYw&(->2*G7=axU;=jq)_KhABE z>!9Ud-obljgnpi@wTEm;Tq472oQ=<|WBOL!mWHz%H-7l>F814^Rn?>a!NycE^-No9 zUg!AI-Ax|}tt2gF-;>xvUXypwo>_K`t-rY6);$1caenDdOWyPF211<&9`)Tky6R@o z_LsgnUJ<9Ct4PaNn4Z_|1-mGZ;HI;8%|>-%$MdanO=;9O@OBVw^F0`LX^~6ob3FSm zt;^UGTdXwJ!VEnl^7Pwn2hwX!>BZkVb}b)PCp z$~oSprS0Q1b06tIT(BFIZ`XlOU`y`N(sDu>-B9-DCHE5W!($+W-u1LQtkZpq{)hKM zKG{nm)3;HcqwSY_9`8}n<4gSWjP+UGzHL$9BfeSE$-0%;c1}z%r!l&Dwnq%}(P@J3 zAoMMjcXN-3#X-#5*`G!=udY%oed>5bGS}L?`|PW6r5aaS$p&3!Z|gz6Bi0-6+RcsM zsraoz?Ur@I^c#>oZ7Qd=Kbq*SxVKu)*zf%c+f7zq+ST~ZNex`#{{Scou!y*srhPwC zZ{_1{gi%g#Z8m literal 0 HcmV?d00001 diff --git a/manim/mobject/abstract/positionable.md b/manim/mobject/abstract/positionable.md index f8da101f68..824fc47776 100644 --- a/manim/mobject/abstract/positionable.md +++ b/manim/mobject/abstract/positionable.md @@ -3,7 +3,9 @@ ## Notes * How should mobject with 0 points be handled? * Currently: Treats behavior as undefined. - * Advantage: Makes calculations simpler and more efficient. + * Advantage: Makes some calculations a lot simpler. + * Disadvantage: Results in some breaking changes. + * Consideration: Simplicity/Efficiency vs Guarding Exceptions. * Should properties be dropped in favor of setter/getter methods? * E.g. `width`, `height` and `depth`. * Advantages: @@ -17,4 +19,38 @@ * Other indirect attributes have setter methods. -## Progress \ No newline at end of file +## Hierarchy + +* shift + * move_to + * align_on_border + * to_corner + * to_edge + * align_to + * center + * set_coord + * set_(x|y|z) + * next_to (TODO: Implement using `move_to`) +* apply_array_function + * apply_function + * apply_complex_function + * apply_matrix + * rotate + * flip + * pose_at_angle + * scale + * scale_to_fit + * scale_to_fit_(width|height|depth) + * stretch + * stretch_to_fit + * stretch_to_fit_(width|height|depth) +* length_over_dim + * get_(width|height|depth) +* get_bounding_box + * get_critical_point (or get_corner, get_edge_center) + * get_(center|bottom|top|left|right|nadir|zenith) + * get_coord + * get_(x|y|z) + +# Deprecated +* width|height|depth = (set|get)_(width|height|depth) \ No newline at end of file diff --git a/manim/mobject/abstract/positionable.py b/manim/mobject/abstract/positionable.py index 474b91396e..6ac9e23b42 100644 --- a/manim/mobject/abstract/positionable.py +++ b/manim/mobject/abstract/positionable.py @@ -1,5 +1,5 @@ -from collections.abc import Callable, Iterable -from typing import Any, Self +from collections.abc import Callable +from typing import Self import numpy as np @@ -18,6 +18,9 @@ UP, ) from manim.mobject.mobject import Mobject +from manim.mobject.opengl.opengl_mobject import OpenGLMobject +from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject +from manim.mobject.types.vectorized_mobject import VMobject from manim.typing import ( MatrixMN, Point3D, @@ -34,29 +37,35 @@ class Positionable: # METHODS - # TODO: Add a parameter for the frame? + # TODO: Keep/Remove frame parameter? + # TODO: Should the default of the frame parameter be handled inside the method to allow config changes? def align_on_border( self, direction: Vector3DLike, + *, buff: float = DEFAULT_MOBJECT_TO_EDGE_BUFFER, + frame: Point3DLike | None = (config.frame_x_radius, config.frame_y_radius, 0), ) -> Self: - frame = (config.frame_x_radius, config.frame_y_radius, 0) target = np.sign(direction) * frame - buff * np.asarray(direction) - self.move_to(point_or_mobject=target, aligned_edge=direction) - return self + return self.move_to(point_or_mobject=target, aligned_edge=direction) def align_to( self, mobject_or_point: "Positionable | Point3DLike", + *, direction: Vector3DLike = ORIGIN, ) -> Self: - target = mobject_or_point.get_critical_point(direction=direction) if isinstance(mobject_or_point, Positionable) else mobject_or_point - self.move_to(point_or_mobject=target, aligned_edge=direction) - return self + target = ( + mobject_or_point.get_critical_point(direction=direction) + if isinstance(mobject_or_point, Positionable) + else mobject_or_point + ) + return self.move_to(point_or_mobject=target, aligned_edge=direction) def apply_complex_function( self, function: Callable[[complex], complex], + *, about_point: Point3DLike | None = None, about_edge: Vector3DLike | None = None, ) -> Self: @@ -81,24 +90,19 @@ def apply_function( if about_point is None and about_edge is None: about_point = ORIGIN - def multi_mapping_function(points: Point3D_Array) -> Point3D_Array: + def mapping_function(points: Point3D_Array) -> Point3D_Array: return np.apply_along_axis(func1d=function, axis=1, arr=points) - return self.apply_points_function_about_point( - function=multi_mapping_function, + return self.apply_array_function( + function=mapping_function, about_point=about_point, about_edge=about_edge, ) - def apply_function_to_position( - self, - function: Callable[[Point3D], Point3D], - ) -> Self: - return self.move_to(function(self.get_center())) - def apply_matrix( self, matrix: MatrixMN, + *, about_point: Point3DLike | None = None, about_edge: Vector3DLike | None = None, ) -> Self: @@ -114,48 +118,35 @@ def apply_matrix( full_matrix = np.identity(3) full_matrix[: matrix.shape[0], : matrix.shape[1]] = matrix - return self.apply_points_function( - lambda points: points.dot(full_matrix.T), + return self.apply_array_function( + function=lambda points: points.dot(full_matrix.T), about_point=about_point, about_edge=about_edge, ) - def apply_points_function( + def apply_array_function( self, - function: Callable[[Point3D], Point3D], + function: Callable[[Point3D_Array], Point3D_Array], + *, about_point: Point3DLike | None = None, about_edge: Vector3DLike | None = None, ) -> Self: if about_point is None: - about_point = self.get_critical_point(direction=about_edge if about_edge is not None else ORIGIN) + about_point = self.get_critical_point( + direction=about_edge if about_edge is not None else ORIGIN + ) self.points -= about_point self.points = function(self.points) self.points += about_point return self - # @deprecated(message="Use apply_points_function() instead.") - def apply_points_function_about_point( - self, - function: Callable[[Point3D], Point3D], - about_point: Point3DLike | None = None, - about_edge: Vector3DLike | None = None, - ) -> Self: - return self.apply_points_function( - function=function, - about_point=about_point, - about_edge=about_edge, - ) - def center(self) -> Self: return self.move_to(point_or_mobject=ORIGIN) - @property - def depth(self) -> float: - return self.length_over_dim(dim=2) - def flip( self, axis: Vector3DLike = UP, + *, about_point: Point3DLike | None = None, about_edge: Vector3DLike | None = None, ) -> Self: @@ -167,12 +158,7 @@ def flip( ) def get_bottom(self) -> Point3D: - return self.get_critical_point(DOWN) - - # TODO: Should this function be dropped? - def get_boundary_point(self, direction: Vector3DLike) -> Point3D: - index = np.argmax(np.dot(self.points, direction)) - return self.points[index] + return self.get_critical_point(direction=DOWN) def get_bounding_box(self) -> Point3D_Array: mins = self.points.min(axis=0) @@ -181,7 +167,7 @@ def get_bounding_box(self) -> Point3D_Array: return np.array([mins, mids, maxs]) def get_center(self) -> Point3D: - return self.get_critical_point(ORIGIN) + return self.get_critical_point(direction=ORIGIN) def get_center_of_mass(self) -> Point3D: return self.points.mean(axis=0) @@ -189,44 +175,34 @@ def get_center_of_mass(self) -> Point3D: def get_coord(self, dim: int, direction: Vector3DLike = ORIGIN) -> float: return self.get_critical_point(direction=direction)[dim] - # @deprecated(message="Use get_critical_point() instead") def get_corner(self, direction: Vector3DLike) -> Point3D: - return self.get_critical_point(direction) + return self.get_critical_point(direction=direction) - # TODO: Should the `np.sign(direction)` restriction be dropped? - # Advantage: Would allow in-between values - # Disadvantage: Would alter behavior - # Alternative: Declare an additional method. (which this method then would use) def get_critical_point(self, direction: Vector3DLike) -> Point3D: direction = np.sign(direction) _, mids, maxs = self.get_bounding_box() return mids + (maxs - mids) * direction + def get_depth(self) -> float: + return self.length_over_dim(dim=2) + def get_edge_center(self, direction: Vector3DLike) -> Point3D: return self.get_critical_point(direction=direction) def get_end(self) -> Point3D: return self.points[-1] - def get_extremum_along_dim( - self, - dim: int = 0, - key: int = 0, - ) -> float: - direction = np.zeros(3) - direction[dim] = np.sign(key) - critical_pt = self.get_critical_point(direction) - return critical_pt[dim] + def get_height(self) -> float: + return self.length_over_dim(dim=1) def get_left(self) -> Point3D: - return self.get_critical_point(LEFT) + return self.get_critical_point(direction=LEFT) def get_nadir(self) -> Point3D: - """Get nadir (opposite the zenith) Point3Ds of a box bounding a 3D :class:`~.Mobject`.""" - return self.get_critical_point(IN) + return self.get_critical_point(direction=IN) def get_right(self) -> Point3D: - return self.get_critical_point(RIGHT) + return self.get_critical_point(direction=RIGHT) def get_start(self) -> Point3D: return self.points[0] @@ -237,6 +213,9 @@ def get_start_and_end(self) -> tuple[Point3D, Point3D]: def get_top(self) -> Point3D: return self.get_critical_point(UP) + def get_width(self) -> float: + return self.length_over_dim(dim=0) + def get_x(self, direction: Vector3DLike = ORIGIN) -> float: return self.get_coord(dim=0, direction=direction) @@ -249,90 +228,29 @@ def get_z(self, direction: Vector3DLike = ORIGIN) -> float: def get_zenith(self) -> Point3D: return self.get_critical_point(direction=OUT) - @property - def height(self) -> float: - return self.length_over_dim(dim=1) - - @height.setter - def height(self, value: float) -> None: - raise NotImplementedError - def length_over_dim(self, dim: int) -> float: values = self.points[:, dim] return values.max() - values.min() - def match_coord( - self, - mobject: Mobject, - dim: int, - direction: Vector3DLike = ORIGIN, - ) -> Self: - return self.set_coord( - mobject.get_coord(dim=dim, direction=direction), - dim=dim, - direction=direction, - ) - - # def match_depth(self) -> Self: - # return self.set_depth() - - # def match_dim_size(self) -> Self: - # return self.set_dim_size() - - # def match_height(self) -> Self: - # return self.set_height() - - def match_points(self, mobject: "Positionable") -> Self: - self.points = mobject.points.copy() - return self - - # def match_width(self) -> Self: - # return self.set_width() - - def match_x( - self, - mobject: "Positionable", - direction: Vector3DLike = ORIGIN, - ) -> Self: - return self.set_x( - x=mobject.get_x(direction=direction), - direction=direction, - ) - - def match_y( - self, - mobject: "Positionable", - direction: Vector3DLike = ORIGIN, - ) -> Self: - return self.set_y( - y=mobject.get_y(direction=direction), - direction=direction, - ) - - def match_z( - self, - mobject: "Positionable", - direction: Vector3DLike = ORIGIN, - ) -> Self: - return self.set_z( - z=mobject.get_z(direction=direction), - direction=direction, - ) - def move_to( self, point_or_mobject: "Point3DLike | Positionable", + *, aligned_edge: Vector3DLike = ORIGIN, coor_mask: Vector3DLike = np.array([1, 1, 1]), ) -> Self: - source = self.get_critical_point(aligned_edge) - target = point_or_mobject.get_critical_point(aligned_edge) if isinstance(point_or_mobject, Positionable) else point_or_mobject - self.shift((target - source) * coor_mask) - return self + source = self.get_critical_point(direction=aligned_edge) + target = ( + point_or_mobject.get_critical_point(direction=aligned_edge) + if isinstance(point_or_mobject, Positionable) + else point_or_mobject + ) + return self.shift(vector=(target - source) * coor_mask) def next_to( self, mobject_or_point: "Positionable | Point3DLike", + *, direction: Vector3DLike = RIGHT, buff: float = DEFAULT_MOBJECT_TO_MOBJECT_BUFFER, aligned_edge: Vector3DLike = ORIGIN, @@ -340,40 +258,26 @@ def next_to( ) -> Self: np_direction = np.asarray(direction) np_aligned_edge = np.asarray(aligned_edge) - source = self.get_critical_point(np_aligned_edge - np_direction) - target = mobject_or_point.get_critical_point(np_aligned_edge + np_direction) if isinstance(mobject_or_point, Positionable) else mobject_or_point + source = self.get_critical_point(direction=np_aligned_edge - np_direction) + target = ( + mobject_or_point.get_critical_point(np_aligned_edge + np_direction) + if isinstance(mobject_or_point, Positionable) + else mobject_or_point + ) return self.shift((target - source + buff * np_direction) * coor_mask) - def pose_at_angle(self, **kwargs: Any) -> Self: - raise NotImplementedError - - def put_start_and_end_on(self, start: Point3DLike, end: Point3DLike) -> Self: - raise NotImplementedError - - def reduce_across_dimension( + def pose_at_angle( self, - reduce_func: Callable[[Iterable[float]], float], - dim: int, - ) -> float | None: - raise NotImplementedError - - def rescale_to_fit( - self, - length: float, - dim: int, - stretch: bool = False, + *, about_point: Point3DLike | None = None, about_edge: Vector3DLike | None = None, ) -> Self: - raise NotImplementedError - # old_length = self.length_over_dim(dim=dim) - # if old_length == 0: - # return self - # if stretch: - # self.stretch(length / old_length, dim, ...) - # else: - # self.scale(length / old_length, ...) - # return self + return self.rotate( + angle=TAU / 14, + axis=RIGHT + UP, + about_point=about_point, + about_edge=about_edge, + ) def rotate( self, @@ -389,38 +293,84 @@ def rotate( about_edge=about_edge, ) - def rotate_about_origin( + def scale( self, - angle: float, - axis: Vector3DLike = OUT, + # TODO: Rename to 'factor' + scale_factor: float | Vector3DLike, + about_point: Point3DLike | None = None, + about_edge: Vector3DLike | None = None, ) -> Self: - return self.rotate( - angle=angle, - axis=axis, - about_point=ORIGIN, + return self.apply_array_function( + function=lambda points: scale_factor * points, + about_point=about_point, + about_edge=about_edge, ) - def scale( + def scale_to_fit( self, - scale_factor: float, + length: float, + dim: int, + *, about_point: Point3DLike | None = None, about_edge: Vector3DLike | None = None, ) -> Self: - raise NotImplementedError + actual_length = self.length_over_dim(dim=dim) + if actual_length == 0: + return self - def scale_to_fit_depth(self) -> Self: - raise NotImplementedError + return self.scale( + scale_factor=length / actual_length, + about_point=about_point, + about_edge=about_edge, + ) - def scale_to_fit_height(self) -> Self: - raise NotImplementedError + def scale_to_fit_depth( + self, + depth: float, + *, + about_point: Point3DLike | None = None, + about_edge: Vector3DLike | None = None, + ) -> Self: + return self.scale_to_fit( + length=depth, + dim=2, + about_point=about_point, + about_edge=about_edge, + ) - def scale_to_fit_width(self) -> Self: - raise NotImplementedError + def scale_to_fit_height( + self, + height: float, + *, + about_point: Point3DLike | None = None, + about_edge: Vector3DLike | None = None, + ) -> Self: + return self.scale_to_fit( + length=height, + dim=1, + 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: + return self.scale_to_fit( + length=width, + dim=0, + about_point=about_point, + about_edge=about_edge, + ) def set_coord( self, value: float, dim: int, + *, direction: Vector3DLike = ORIGIN, ) -> Self: target = self.get_critical_point(direction=direction) @@ -444,37 +394,80 @@ def shift(self, vector: Vector3DLike) -> Self: self.points += vector return self - def shift_onto_screen(self) -> Self: - raise NotImplementedError - def stretch( self, factor: float, dim: int, + *, about_point: Point3DLike | None = None, about_edge: Vector3DLike | None = None, ) -> Self: - def function(points: Point3D_Array) -> Point3D_Array: - points[:, dim] *= factor - return points - - return self.apply_points_function( - function=function, + return self.scale( + scale_factor=np.array([factor if i == dim else 1.0 for i in range(3)]), about_point=about_point, about_edge=about_edge, ) - def stretch_about_point(self) -> Self: - raise NotImplementedError + def stretch_to_fit( + self, + length: float, + dim: int, + *, + about_point: Point3DLike | None = None, + about_edge: Vector3DLike | None = None, + ) -> Self: + actual_length = self.length_over_dim(dim=dim) + if actual_length == 0: + return self - def stretch_to_fit_depth(self) -> Self: - raise NotImplementedError + return self.stretch( + factor=length / actual_length, + dim=dim, + about_point=about_point, + about_edge=about_edge, + ) - def stretch_to_fit_height(self) -> Self: - raise NotImplementedError + def stretch_to_fit_depth( + self, + depth: float, + *, + about_point: Point3DLike | None = None, + about_edge: Vector3DLike | None = None, + ) -> Self: + return self.stretch_to_fit( + length=depth, + dim=2, + 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: + return self.stretch_to_fit( + length=height, + dim=1, + about_point=about_point, + about_edge=about_edge, + ) - def stretch_to_fit_width(self) -> Self: - raise NotImplementedError + def stretch_to_fit_width( + self, + width: float, + *, + about_point: Point3DLike | None = None, + about_edge: Vector3DLike | None = None, + ) -> Self: + return self.stretch_to_fit( + length=width, + dim=0, + about_point=about_point, + about_edge=about_edge, + ) def to_corner( self, @@ -490,6 +483,21 @@ def to_edge( ) -> Self: return self.align_on_border(direction=edge, buff=buff) - @property - def width(self) -> float: - return self.length_over_dim(dim=0) + # Deprecated + + +def dump_methods() -> None: + seen: set[str] = set() + + for cls in [Mobject, VMobject, OpenGLMobject, OpenGLVMobject]: + assert isinstance(cls, type) + print(cls.__name__) + for name in sorted(cls.__dict__): + if name in seen: + continue + print(f"\t{name}") + seen |= cls.__dict__.keys() + + +if __name__ == "__main__": + dump_methods() diff --git a/manim/mobject/abstract/ruff.toml b/manim/mobject/abstract/ruff.toml deleted file mode 100644 index 2cfeee68dd..0000000000 --- a/manim/mobject/abstract/ruff.toml +++ /dev/null @@ -1 +0,0 @@ -line-length = 200 \ No newline at end of file diff --git a/manim/mobject/abstract/test.py b/manim/mobject/abstract/test.py index c1657ae73a..9954805e65 100644 --- a/manim/mobject/abstract/test.py +++ b/manim/mobject/abstract/test.py @@ -1,86 +1,82 @@ -from collections.abc import Callable -import contextlib import time +from collections.abc import Callable from typing import Any import numpy as np from manim.mobject.abstract.positionable import Positionable from manim.mobject.mobject import Mobject -from manim.typing import Point3D, Point3D_Array, Vector3D - +from manim.typing import Point3D, Vector3D _RNG = np.random.default_rng(seed=1) -def random_number(low: float = -10, high: float = 10) -> float: +def _random_number(low: float = -10, high: float = 10) -> float: return _RNG.uniform(low=low, high=high) -def random_point(low: float = -10, high: float = 10) -> Point3D: +def _random_point(low: float = -10, high: float = 10) -> Point3D: return _RNG.uniform(low=low, high=high, size=3) -def random_points(low: float = -10, high: float = 10, size: int = 1) -> np.ndarray: +def _random_points(low: float = -10, high: float = 10, size: int = 1) -> np.ndarray: return _RNG.uniform(low=low, high=high, size=(size, 3)) -def random_vector(low: float = -3, high: float = 3) -> Vector3D: +def _random_vector(low: float = -3, high: float = 3) -> Vector3D: return _RNG.uniform(low=low, high=high, size=3) -def create_another[T: Mobject | Positionable](mob: T, points: Point3D_Array) -> T: - another = mob.__class__() - another.points = points - return another +def _random_choice(a: list[Any]) -> Any: + return _RNG.choice(a=a) -def create_mobs(point_count: int) -> tuple[Mobject, Positionable]: - points = random_points(size=point_count) - mob_1 = Mobject() - mob_2 = Positionable() - mob_1.points = points.copy() - mob_2.points = points.copy() - return mob_1, mob_2 +# def _create_another( +# mob: Mobject | Positionable, points: Point3D_Array +# ) -> Mobject | Positionable: +# another = type(mob)() +# another.points = points +# return another def validate_function( name: str, - function: Callable[[Mobject | Positionable, dict], Any], + function: Callable[[Mobject | Positionable, dict[Any, Any]], Any], validate: Callable[[Any, Any], None], - create_kwargs: Callable[[], dict], + create_kwargs: Callable[[], dict[Any, Any]], + *, point_counts: list[int] = list(range(1, 101)), loop_count: int = 100, ) -> None: - point_counts = point_counts.copy() - _RNG.shuffle(point_counts) - - time_0, time_1 = 0, 0 + time_old, time_new = 0, 0 for point_count in point_counts: for _ in range(loop_count): - mob_1, mob_2 = create_mobs(point_count=point_count) + points = _random_points(size=point_count) + + mob_old = Mobject() + mob_old.points = points.copy() + mob_new = Positionable() + mob_new.points = points.copy() + kwargs = create_kwargs() - result_0: np.typing.ArrayLike | None = None - with contextlib.suppress(Exception): - start = time.perf_counter_ns() - result_0 = function(mob_1, kwargs) - time_0 += time.perf_counter_ns() - start + start = time.perf_counter_ns() + result_old = function(mob_old, kwargs) + time_old += time.perf_counter_ns() - start - if result_0 is not None: - start = time.perf_counter_ns() - result_1 = function(mob_2, kwargs) - time_1 += time.perf_counter_ns() - start + start = time.perf_counter_ns() + result_new = function(mob_new, kwargs) + time_new += time.perf_counter_ns() - start - validate(result_0, result_1) + validate(result_old, result_new) - print(name, f"{time_0 / time_1:2.4f}") + print(name.ljust(25), f"{time_old / time_new:1.2f}x".ljust(6)) def validate_setter( name: str, - function: Callable[[Mobject | Positionable, dict], Any], + function: Callable[[Mobject | Positionable, dict[Any, Any]], Any], create_kwargs: Callable[[], Any] = lambda: {}, ): def validate(result_1: Any, result_2: Any) -> None: @@ -98,7 +94,7 @@ def validate(result_1: Any, result_2: Any) -> None: def validate_getter( name: str, - function: Callable[[Mobject | Positionable, dict], Any], + function: Callable[[Mobject | Positionable, dict[Any, Any]], Any], create_kwargs: Callable[[], Any] = lambda: {}, ): def validate(result_1: Any, result_2: Any) -> None: @@ -113,30 +109,50 @@ def validate(result_1: Any, result_2: Any) -> None: def main() -> None: - validate_setter("align_on_border", lambda mob, kwargs: mob.align_on_border(**kwargs), lambda: {"direction": random_vector(), "buff": random_number()}) - validate_setter("align_to", lambda mob, kwargs: mob.align_to(**kwargs), lambda: {"mobject_or_point": random_point(), "direction": random_vector()}) - # validate_setter("apply_complex_function", lambda mob: mob.apply_complex_function(...)) - # validate_setter("apply_function", lambda mob: mob.apply_function(...)) - # validate_setter("apply_function_to_position", lambda mob: mob.apply_function_to_position(...)) - # validate_setter("apply_matrix", lambda mob: mob.apply_matrix(...)) - # validate_setter("apply_points_function_about_point", lambda mob: mob.apply_points_function_about_point(...)) + validate_setter( + "align_on_border", + lambda mob, kwargs: mob.align_on_border(**kwargs), + lambda: {"direction": _random_vector(), "buff": _random_number()}, + ) + validate_setter( + "align_to", + lambda mob, kwargs: mob.align_to(**kwargs), + lambda: {"mobject_or_point": _random_point(), "direction": _random_vector()}, + ) validate_setter("center", lambda mob, _: mob.center()) - validate_getter("depth", lambda mob, _: mob.depth) - # validate_setter("depth", lambda mob, v=random_number(): setattr(mob, "depth", v)) - validate_setter("flip", lambda mob, kwargs: mob.flip(**kwargs), lambda: {"axis": random_vector(), "about_point": random_point(), "about_edge": random_vector()}) + validate_setter( + "flip", + lambda mob, kwargs: mob.flip(**kwargs), + lambda: { + "axis": _random_vector(), + "about_point": _random_point(), + "about_edge": _random_vector(), + }, + ) validate_getter("get_bottom", lambda mob, _: mob.get_bottom()) - validate_getter("get_boundary_point", lambda mob, kwargs: mob.get_boundary_point(**kwargs), lambda: {"direction": random_vector()}) validate_getter("get_center", lambda mob, _: mob.get_center()) validate_getter("get_center_of_mass", lambda mob, _: mob.get_center_of_mass()) - for dim in [0, 1, 2]: - validate_getter("get_coord", lambda mob, kwargs: mob.get_coord(**kwargs), lambda: {"dim": dim, "direction": random_vector()}) - validate_getter("get_corner", lambda mob, kwargs: mob.get_corner(**kwargs), lambda: {"direction": random_vector()}) - validate_getter("get_critical_point", lambda mob, kwargs: mob.get_critical_point(**kwargs), lambda: {"direction": random_vector()}) - validate_getter("get_edge_center", lambda mob, kwargs: mob.get_edge_center(**kwargs), lambda: {"direction": random_vector()}) + validate_getter( + "get_coord", + lambda mob, kwargs: mob.get_coord(**kwargs), + lambda: {"dim": _random_choice([0, 1, 2]), "direction": _random_vector()}, + ) + validate_getter( + "get_corner", + lambda mob, kwargs: mob.get_corner(**kwargs), + lambda: {"direction": _random_vector()}, + ) + validate_getter( + "get_critical_point", + lambda mob, kwargs: mob.get_critical_point(**kwargs), + lambda: {"direction": _random_vector()}, + ) + validate_getter( + "get_edge_center", + lambda mob, kwargs: mob.get_edge_center(**kwargs), + lambda: {"direction": _random_vector()}, + ) validate_getter("get_end", lambda mob, _: mob.get_end()) - for dim in [0, 1, 2]: - for key in [0, 1, 2]: - validate_getter("get_extremum_along_dim", lambda mob, kwargs: mob.get_extremum_along_dim(**kwargs), lambda: {"dim": dim, "key": key}) validate_getter("get_left", lambda mob, _: mob.get_left()) validate_getter("get_nadir", lambda mob, _: mob.get_nadir()) validate_getter("get_right", lambda mob, _: mob.get_right()) @@ -147,54 +163,157 @@ def main() -> None: validate_getter("get_y", lambda mob, _: mob.get_y()) validate_getter("get_z", lambda mob, _: mob.get_z()) validate_getter("get_zenith", lambda mob, _: mob.get_zenith()) - validate_getter("height", lambda mob, _: mob.height) - # validate_setter("height", lambda mob, h=random_number(): setattr(mob, "height", h)) - for dim in [0, 1, 2]: - validate_getter("length_over_dim", lambda mob, _: mob.length_over_dim(dim=dim)) - - for dim in [0, 1, 2]: - validate_setter("match_coord", lambda mob, kwargs: mob.match_coord(mobject=create_another(mob=mob, **kwargs), dim=dim), lambda: {"points": random_points(size=int(random_number(1, 100)))}) - # validate_setter("match_depth", lambda mob: mob.match_depth()) - # validate_setter("match_dim_size", lambda mob: mob.match_dim_size()) - # validate_setter("match_height", lambda mob: mob.match_height()) - validate_setter("match_points", lambda mob, kwargs: mob.match_points(mobject=create_another(mob=mob, **kwargs)), lambda: {"points": random_points(size=int(random_number(1, 100)))}) - # validate_setter("match_width", lambda mob: mob.match_width()) - validate_setter("match_x", lambda mob, kwargs: mob.match_x(mobject=create_another(mob=mob, **kwargs)), lambda: {"points": random_points(size=int(random_number(1, 100)))}) - validate_setter("match_y", lambda mob, kwargs: mob.match_y(mobject=create_another(mob=mob, **kwargs)), lambda: {"points": random_points(size=int(random_number(1, 100)))}) - validate_setter("match_z", lambda mob, kwargs: mob.match_z(mobject=create_another(mob=mob, **kwargs)), lambda: {"points": random_points(size=int(random_number(1, 100)))}) - validate_setter("move_to", lambda mob, kwargs: mob.move_to(**kwargs), lambda: {"point_or_mobject": random_point(), "aligned_edge": random_vector(), "coor_mask": random_vector()}) + validate_getter( + "length_over_dim", + lambda mob, kwargs: mob.length_over_dim(**kwargs), + lambda: {"dim": _random_choice([0, 1, 2])}, + ) + validate_setter( + "move_to", + lambda mob, kwargs: mob.move_to(**kwargs), + lambda: { + "point_or_mobject": _random_point(), + "aligned_edge": _random_vector(), + "coor_mask": _random_vector(), + }, + ) validate_setter( "next_to", lambda mob, kwargs: mob.next_to(**kwargs), - lambda: {"mobject_or_point": random_point(), "direction": random_vector(), "buff": random_number(), "aligned_edge": random_vector(), "coor_mask": random_vector()}, - ) - # validate_setter("pose_at_angle", lambda mob: mob.pose_at_angle()) - # validate_setter("put_start_and_end_on", lambda mob: mob.put_start_and_end_on()) - # validate_getter("reduce_across_dimension", lambda mob: mob.reduce_across_dimension()) - # validate_setter("rescale_to_fit", lambda mob: mob.rescale_to_fit()) - validate_setter("rotate", lambda mob, kwargs: mob.rotate(**kwargs), lambda: {"angle": random_number(), "axis": random_vector(), "about_edge": random_vector()}) - # validate_setter("rotate_about_origin", lambda mob, a=random_number(), ax=random_vector(),: mob.rotate_about_origin(angle=a, axis=ax)) - # validate_setter("scale", lambda mob: mob.scale()) - # validate_setter("scale_to_fit_depth", lambda mob: mob.scale_to_fit_depth()) - # validate_setter("scale_to_fit_height", lambda mob: mob.scale_to_fit_height()) - # validate_setter("scale_to_fit_width", lambda mob: mob.scale_to_fit_width()) - for dim in [0, 1, 2]: - validate_setter("set_coord", lambda mob, kwargs: mob.set_coord(**kwargs), lambda: {"value": random_number(), "dim": dim, "direction": random_vector()}) - validate_setter("set_x", lambda mob, kwargs: mob.set_x(**kwargs), lambda: {"x": random_number(), "direction": random_vector()}) - validate_setter("set_y", lambda mob, kwargs: mob.set_y(**kwargs), lambda: {"y": random_number(), "direction": random_vector()}) - validate_setter("set_z", lambda mob, kwargs: mob.set_z(**kwargs), lambda: {"z": random_number(), "direction": random_vector()}) - validate_setter("shift", lambda mob, kwargs: mob.shift(kwargs["value"]), lambda: {"value": random_vector()}) - # validate_setter("shift_onto_screen", lambda mob, v=random_vector(): mob.shift_onto_screen()) - for dim in [0, 1, 2]: - validate_setter("stretch", lambda mob, kwargs: mob.stretch(**kwargs), lambda: {"factor": random_number(), "dim": dim, "about_point": random_point(), "about_edge": random_vector()}) - # validate_setter("stretch_about_point", lambda mob: mob.stretch_about_point()) - # validate_setter("stretch_to_fit_depth", lambda mob: mob.stretch_to_fit_depth()) - # validate_setter("stretch_to_fit_height", lambda mob: mob.stretch_to_fit_height()) - # validate_setter("stretch_to_fit_width", lambda mob: mob.stretch_to_fit_width()) - validate_setter("to_corner", lambda mob, kwargs: mob.to_corner(**kwargs), lambda: {"corner": random_vector(), "buff": random_number()}) - validate_setter("to_edge", lambda mob, kwargs: mob.to_edge(**kwargs), lambda: {"edge": random_vector(), "buff": random_number()}) - validate_getter("width", lambda mob, _: mob.width) - # validate_setter("width", lambda mob, w=random_number(): setattr(mob, "width", w)) + lambda: { + "mobject_or_point": _random_point(), + "direction": _random_vector(), + "buff": _random_number(), + "aligned_edge": _random_vector(), + "coor_mask": _random_vector(), + }, + ) + validate_setter( + "pose_at_angle", + lambda mob, kwargs: mob.pose_at_angle(**kwargs), + lambda: {"about_point": _random_point(), "about_edge": _random_vector()}, + ) + validate_setter( + "rotate", + lambda mob, kwargs: mob.rotate(**kwargs), + lambda: { + "angle": _random_number(), + "axis": _random_vector(), + "about_edge": _random_vector(), + }, + ) + validate_setter( + "scale", + lambda mob, kwargs: mob.scale(**kwargs), + lambda: { + "scale_factor": _random_number(), + "about_point": _random_point(), + "about_edge": _random_vector(), + }, + ) + validate_setter( + "scale_to_fit_depth", + lambda mob, kwargs: mob.scale_to_fit_depth(**kwargs), + lambda: { + "depth": _random_number(), + "about_point": _random_point(), + "about_edge": _random_vector(), + }, + ) + validate_setter( + "scale_to_fit_height", + lambda mob, kwargs: mob.scale_to_fit_height(**kwargs), + lambda: { + "height": _random_number(), + "about_point": _random_point(), + "about_edge": _random_vector(), + }, + ) + validate_setter( + "scale_to_fit_width", + lambda mob, kwargs: mob.scale_to_fit_width(**kwargs), + lambda: { + "width": _random_number(), + "about_point": _random_point(), + "about_edge": _random_vector(), + }, + ) + validate_setter( + "set_coord", + lambda mob, kwargs: mob.set_coord(**kwargs), + lambda: { + "value": _random_number(), + "dim": _random_choice([0, 1, 2]), + "direction": _random_vector(), + }, + ) + validate_setter( + "set_x", + lambda mob, kwargs: mob.set_x(**kwargs), + lambda: {"x": _random_number(), "direction": _random_vector()}, + ) + validate_setter( + "set_y", + lambda mob, kwargs: mob.set_y(**kwargs), + lambda: {"y": _random_number(), "direction": _random_vector()}, + ) + validate_setter( + "set_z", + lambda mob, kwargs: mob.set_z(**kwargs), + lambda: {"z": _random_number(), "direction": _random_vector()}, + ) + validate_setter( + "shift", + lambda mob, kwargs: mob.shift(kwargs["value"]), + lambda: {"value": _random_vector()}, + ) + validate_setter( + "stretch", + lambda mob, kwargs: mob.stretch(**kwargs), + lambda: { + "factor": _random_number(), + "dim": _random_choice([0, 1, 2]), + "about_point": _random_point(), + "about_edge": _random_vector(), + }, + ) + validate_setter( + "stretch_to_fit_depth", + lambda mob, kwargs: mob.stretch_to_fit_depth(**kwargs), + lambda: { + "depth": _random_number(), + "about_point": _random_point(), + "about_edge": _random_vector(), + }, + ) + validate_setter( + "stretch_to_fit_height", + lambda mob, kwargs: mob.stretch_to_fit_height(**kwargs), + lambda: { + "height": _random_number(), + "about_point": _random_point(), + "about_edge": _random_vector(), + }, + ) + validate_setter( + "stretch_to_fit_width", + lambda mob, kwargs: mob.stretch_to_fit_width(**kwargs), + lambda: { + "width": _random_number(), + "about_point": _random_point(), + "about_edge": _random_vector(), + }, + ) + validate_setter( + "to_corner", + lambda mob, kwargs: mob.to_corner(**kwargs), + lambda: {"corner": _random_vector(), "buff": _random_number()}, + ) + validate_setter( + "to_edge", + lambda mob, kwargs: mob.to_edge(**kwargs), + lambda: {"edge": _random_vector(), "buff": _random_number()}, + ) if __name__ == "__main__": From a25ce2c6b54615045b26359e33bc14a47b70127c Mon Sep 17 00:00:00 2001 From: GniLudio <50866361+GniLudio@users.noreply.github.com> Date: Tue, 18 Aug 2026 23:43:38 +0200 Subject: [PATCH 05/66] Update --- manim/mobject/abstract/attributes.py | 34 ++ manim/mobject/abstract/attributes.txt | Bin 0 -> 18038 bytes manim/mobject/abstract/methods.txt | Bin 14296 -> 0 bytes manim/mobject/abstract/positionable.md | 44 +-- manim/mobject/abstract/positionable.py | 436 ++++++++++++++++++++++--- manim/mobject/abstract/test.py | 224 ++++++++----- 6 files changed, 572 insertions(+), 166 deletions(-) create mode 100644 manim/mobject/abstract/attributes.py create mode 100644 manim/mobject/abstract/attributes.txt delete mode 100644 manim/mobject/abstract/methods.txt diff --git a/manim/mobject/abstract/attributes.py b/manim/mobject/abstract/attributes.py new file mode 100644 index 0000000000..e966fb173d --- /dev/null +++ b/manim/mobject/abstract/attributes.py @@ -0,0 +1,34 @@ +from manim.mobject.abstract.positionable import Positionable +from manim.mobject.mobject import Mobject +from manim.mobject.opengl.opengl_mobject import OpenGLMobject +from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject +from manim.mobject.types.vectorized_mobject import VMobject + + +def main() -> None: + seen: set[str] = set() + + for cls in [Mobject, VMobject, OpenGLMobject, OpenGLVMobject]: + assert isinstance(cls, type) + print(cls.__name__) + for name, attr in sorted(cls.__dict__.items()): + if ( + name in seen + or name.startswith("__") + or attr is getattr(cls.__base__, name, None) + ): + continue + print( + f"\t{'-+'[getattr(Positionable, name, None) is not getattr(Positionable.__base__, name, None)]} {name}" + ) + seen |= cls.__dict__.keys() + + print(Positionable.__name__) + for name, attr in Positionable.__dict__.items(): + if name.startswith("__") or attr is getattr(Positionable.__base__, name, None): + continue + print(f"\t* {name}", "(new)" if name not in seen else "") + + +if __name__ == "__main__": + main() diff --git a/manim/mobject/abstract/attributes.txt b/manim/mobject/abstract/attributes.txt new file mode 100644 index 0000000000000000000000000000000000000000..54d3bbe255316e9d3dc98d710d937efb09a29372 GIT binary patch literal 18038 zcmbuG%WfUX4TgK00C|U5=7MZ8uMi-c3^J>14{XVjJfmB5@$KQ0@ax|M$!b>ju?K?e zqb?SU#mi3?yUzdq`_uBUyj^}--Ypl)%kqci_sj2=zb-#4r{&FZX+Njs{c>+3PwW5B z_V2|yc5k2VjONAue=v%Z{rkbVJgtq@aN?P~m+-5&2~JL|2@yI6i*u5G0U+sFO7qo=Rhgy*&G^Ig(}YxUfjC)3x1 zY3a&Vx-*U4E}!kMJL7q0T4RUL_V-5{d0B4k)1y(#Vp)gZyXui`oG}D~=k;-nZ2v9Pr>*Bp>nB*!hJ$C8N}cxma-$U7c*FIqow@xIJ1tgl7@ekH+mL$q82SJa+C+p&z22`o6KZ99<$Y}FURFQhdCtQ68xOd(pFM8)MtB;Z<$t~^@;W!X ziBY0AG?_=0$>2w}3IE(uZ>^bE%A-^WSH_Jfq86xHLw>A#Yrl?9mD9sI;{G^}k?F&u z>L`8;URn_Mpy}nh!F>7mAs+KF;$yOR_4`n>6xA3`+68spwC-1vfeMIMHqnGxp z+}WbkbZFB3By+s~s`Xg4^R!2s4K~#()%6#1Rn>I$@s^f44Ai+Y-sDtST*!^DD4MpK z#yhVKzdBt%+f(N4k6{H>p4lt2R`7o8b}~%H8}Ra(CuL285hK14sXapHnVuo@vy9uW zzb}o4eX$kLAKIj@Aeyf1m;diqtL?||mz7Qxad_p;y7QB9pq>sJXa8`T{n2x@8Fs!g zX|@)Yk5tslk}&S)rPu>cl{t}+2;r}z@h!6A8}QPJ&Akg-y|-0`)L`|qX&bGh_t+8G zfOqS}5jD?o^{c#P1jEAJS&3tu01Az2!5Hx$kEx zPGpy>hI&bIeB^mKM)Gry6Ui%`=W2%MWI(b{evYN_c*dyJLD=IH8Ynte9Li)mcZJ7= zzNlO|1!INB^%G-Y*LWJ>Ilc`?r0veh9f!JtpL4S;uf~^CFx+oV9`6JY@kiC_qwN`K z0&_=PB`$It5E+*?i#!4?vBJ)j352*rao#`nU>=!|=nLud(-C1S6&VuE^-ueqjRlw2<{O@;L!CzCtQ+`Y*&%A)UX-< z#-~^$&P_;z@3LCR^lUV&g`W^@Q43+G7?n>1*0NUUDs&jns{T|N5JIr|Rn#)-4&m8Bo25LQ&y;s_+<8A|<{%l%R&G6&$_pg30z2DgL zsPF4D;hf?hpCgW=0+3`k#`$B3q#Lu)mHpIyCwLssdu+<+@x1y$wX?FwIddP)SB1vj zFX=2=*P~|?7$;5W1ovXRysxl*UhySRDx*Q>GsbvV(SS2(TPu$fYTO~Ty z*`vsyZ`58Rq(sO1wh6`CbEDM=E z`><-&!@rlGZ2j(B2U@Xv_clC=y@DdSzVW!qzC_%N5~>2hzfSfOJMtd*g+<3(+ZAtB z=$C!5&*&2b!%DSA_?B7np6^>R@5AZR8Sqi;n+!)^;b$Z;VuWty!40TI7PDg%;bdEY16*^w~(cn7o zsP?(h1q1`gW8|mvsXHHy^~0;T79Oj0GO~9=!*5+PJs$gx)k{W`LAPCz^Uat;Mql{8 zSu=k)j~l4t9`x-q*z;buyW4eWsk5~A*dqoyDk^`S8}#0Tx^{dwoRKTj^Txw7=XS@V zAMD|sD(VNjqd`{fa)kTHjI!SwHMes;Cps$kJfn+{O1&2)m9kd3y2+FG_j$$dPNURB zVb2+bXEI{Z&rPauK{4{|JjWgJ?Dd>$wR3oT2cJrnvq_fN(StfHq@l{;^sSpoRQ{od zEYm)ns?Cfq(qHV;^_tQ9@m%r7K61DC{5+gzA?xTN=p&;6 zf6dP)L5U}#Rn_yvo%(**Lp@dJE=g554#tdbhmu?d<3@o_n{mXPD$O!?rmpx>0t? zUa@+2`s?RpujYQyRzj@EX%Vj?8c)lQC6b+QSi9$OPF4zDf%F}>ZN$XtRG(cQlg~Kb zRGi9d#L9i<%AJnA6T-jNn7v>AvHWw|_aGYeZU?{S)bYmj>)x7gAcTZ@&Ws{%CPlxC zXd+rNGUnS`+0)P=w^6#vLH8PT6OtN9`u6)gA!odUj}z@fxx+;TL3Q%0@yNS(oq%1n z`z=J%J0qc--~XJg!T#k9@2#uj=em8ZFv|TVR*QR2R872hQU)gK*evq9L@XQ3G^!(LzQGkptk zPH&AZuPkfWV!ICac=c|t8>8#zxjJ*)c5HLSJ40OGTh58(jKDuSE7?!y{eXVXb`O<~ zh@+E>&F^g8BYI_J&R5tWYQdvddVk zNQ5Pz2Ls7CobG4X5ORz&{7g};p*joH1as!Ivs5ATx%8vY2hRB3$O7aQvQS+MY{X0J zT9uyj)vP1kojbiQf;W~2iS*8QyV{)glht>;pY+@w68fAPHF!geY;XMf*oe}3UXR5- z)AvfvvqjTADs0MKM0Gc|J8_!?7GXK(*duM0yF)AIsgoG@jC5}@ukN)n76Qp{S6|2| z>s&_3wmy;l8;QMsh&N(gm60j?>LKKXb`MGT9)wUQSF`i?c>dNZpDJ{+_qT0v$`^iw zEbLD26gthusQXMe({(Eg_4@a0g~x$ud@o8~gtmgK)7AHb`HjT4&?-mxJ+1CNrmq(< zp6&0Wp@+KH8oBuT(Tww}huNAHCAZp$d40T9DRr+>`0ic4Ezg{tqQC3+4&nw~_*th# zeal+C4OX?d=I8;@+7qOKD8kcj-0ds5Y%{tO&oqMd)N*{>UX@tGJx1CdEa0{_J zm$*l1Wi-ScM{3pDRr1X3_7C0pNR}){qe46KZB|s*PA6FG@X6#?y+ehl`j(R#mT~`n zvg%NHt~(Ohla6$f>p0NR(Z0$&i-9`3?%{~T&*lSkIsK%~4Mk#KJN~m_Wk!`B((~MF z;l!&tmiy9Vm>DH^59(O-W1`N9I?UzzVfmx2L1*euMTVG7ZrJzI*LD?%4RfK*IOM>3F@ps)C6tjmU|!hIabka&-~QCFP5J{k+8rpNcp5+Ou7BMF1wB768rR#Ma zqhK+pq3;Fsp6%3}r1@OCTAh^Ve9xBxRc3KAEE0D34S-<&kPDsuMIQl+Ai33&eLy@(8k1U4WAy zq$)P-lBuXy&i{_YI5WSG(;ZNny780^}L7TL08@kjv2J$ly__ z@ysqK%;{(TGoR6WidTTW@B8RXI^8Ox0y{=g=QWpH-1gz>% literal 0 HcmV?d00001 diff --git a/manim/mobject/abstract/methods.txt b/manim/mobject/abstract/methods.txt deleted file mode 100644 index 93c6495124cf6e1251b8a6d97a56b7b157f2304a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14296 zcmbW8+in~;5{COW3*==sudqOF5@fITb|6cmL!!g14wmJI-%Wk}>sJ-o?4FTf*ytgP z#X9~~WViqSzu%{a>C5!zw4ctW=jrF^AJb3MF8-WN7x9OuI(9$Z2mN!*K2JCCbU!_H zG#9~XKOKVGL;U-;n7x_zY586R?RiIe8T=oE`}1@gPxrx-o!fO+F)CS}gLX7`6Z8A& zuA93VYR4|nw|sK+X?h7uorgSUAy=>WO^h(#uV(*Vw%b{omhvvNy9f@UVQO1zGE-!boahHF|Z5SzXp%T zSpp*9=_6`nE9S85*I0!=i7ZKk-Wh*CzHS#>k(Hh9=Uo)H(xg70ra`;nOK?07OI-!` zhp_PdtO0f-Yp}@G?71IXwF@4^_GNIo3q5YH1fqvgUES0(#n-V2? z0;^gN<%%{=dRwrXCQkGS=He58n|-1%0(28kc^wU6Es8tb(ic)IX$B)fdq6x8B;Cg(kqs+&QM{g z&_p`DN6+ISv`L&u4iEFF#E}#MHJDBFUUu-gUCD<4diUa7IJos zfVPn0A_g7s!g_9fpJ~+X$tKscX1iEPpIH6S&acm$VkPt1 zcb<}uu@Ut^NaN^Y4x6X{7*ThJG(<7K#Ctqf`6iuC6pjkcS3*H>@F^t0lGsu!bf&tB zz37m$=2t{fpKv}?q$XP5&w-}Kt~%G(z;=#EM7o;^A1*mx$7q`cb^X$Nb#@$|m4Xw( z#cYA)&W_(o?-;IQj$@Ds57cT-#Q?qAF|Zpnr!%0^ZFMD}R9|^j0o4CX?z98FDkm9M|4Z;Aj^KJbS6)Rf*|(+NA@oCnb?hQG9kWC* zXMUjpCrmWd-Gbtl_$Hd#HTI39g^Nde6iW)K3f5VUj5Vrs>#P1VqOv7_W?O0 zzb2lg9n52F7gfvJJLgJelP$Q5xa%=uowm<-EMqU|d5NvdaVjU7OOK@Pg1B4f0}^Y6 z>4}9?)>l-u9O@&^W@PC19OYWoC?(mJ{d7K`(a4x;O^m1hZ7j{-wI^`IC{MnJ{fI^P z3$ibx|Bg%%`usBe>mydLV=Q}8&`Q6BFFJV=H{@7WN$4KpCdqqWht)6R51Bj~8)Jhr z`DxDK^qG+P*qTGcm=Mor=#h7F4{k9MTgX6kdJY~whqhyFo+nCJzq-E9y}r8loMl&G z-8fUXxG%HyC3MLfMN3Ha0k75bj?s`lhn24pA#`6nbB-qp*84=sPw&&7|1|D1o#fCM zl*UPAdEYsue2$@3&;Lxn$A0~p0<=IwM+;GbPGoeg`Hp?Z1M%HUq|6~k^5)!kk>?TN zUqS=cXos_5Pq&d;icI`5qo9o&09NL4-degL(vF0U|w{_R^ zylSDfSW2fz&e4u6a>93X(hO?%|BeIOaXbePp{(NQ;$5SjANJbKb9kE!X({}!!?#T0 z*?ZqIoMzQ_htt+k)0e zMijjyNyAx^oTYA16HcRakGI@2BeV5f-J0ciU%zp-3PKJX<7<;_8z-? zpM8GCnvaijs&_JL-jV}Xm2rn zThVr8xvF*>Jqo3K8NY1G&>sa%`KlI?az3+ntigIt6iXEuD64y)w#N&_c%J&Y~|Ou zA5SWx39G4tCr@-wgspWK$jL;~lD)ovZFkD~dW*DHnaCuvSZ^C^-mk5ww=}hzu*Yg` z6VhNCjb`=nT|uQ*|G(a+c+J>5>|$Jy4NT8n74O4(A9>!jvRyU83a?0?8J$KmTc?>Z zeT_=KX=o+FJDhxo5VB`C{qm7<=9eB^u04kS$z#ouOABUAS=I^p-5Ai~hNa7$mX)EN zxVJjuKjz%JpMITwTRDTv_nbrgogbN`yLe)h_&Df z8?lH&f2&d>XxW{5PwvR|b(HR)mpkXX$ULDve0(25MW)LC6*^TdtvmRVH=sfs-R%p7 z{0-QMH+rfwxS79q6N0R3&rJPdQabWpKvj+`%^PQOlJ|*+c zJJIM&J*Fe#v=(HRJg&vtc0EQ_5xM1yjj=d-y~HzTYw&(->2*G7=axU;=jq)_KhABE z>!9Ud-obljgnpi@wTEm;Tq472oQ=<|WBOL!mWHz%H-7l>F814^Rn?>a!NycE^-No9 zUg!AI-Ax|}tt2gF-;>xvUXypwo>_K`t-rY6);$1caenDdOWyPF211<&9`)Tky6R@o z_LsgnUJ<9Ct4PaNn4Z_|1-mGZ;HI;8%|>-%$MdanO=;9O@OBVw^F0`LX^~6ob3FSm zt;^UGTdXwJ!VEnl^7Pwn2hwX!>BZkVb}b)PCp z$~oSprS0Q1b06tIT(BFIZ`XlOU`y`N(sDu>-B9-DCHE5W!($+W-u1LQtkZpq{)hKM zKG{nm)3;HcqwSY_9`8}n<4gSWjP+UGzHL$9BfeSE$-0%;c1}z%r!l&Dwnq%}(P@J3 zAoMMjcXN-3#X-#5*`G!=udY%oed>5bGS}L?`|PW6r5aaS$p&3!Z|gz6Bi0-6+RcsM zsraoz?Ur@I^c#>oZ7Qd=Kbq*SxVKu)*zf%c+f7zq+ST~ZNex`#{{Scou!y*srhPwC zZ{_1{gi%g#Z8m diff --git a/manim/mobject/abstract/positionable.md b/manim/mobject/abstract/positionable.md index 824fc47776..06a1df89a4 100644 --- a/manim/mobject/abstract/positionable.md +++ b/manim/mobject/abstract/positionable.md @@ -1,5 +1,13 @@ # Positionable +## Changes +> TODO + +## TODO +* Handling for 0 points +* Documentation +* Helpful error messages + ## Notes * How should mobject with 0 points be handled? * Currently: Treats behavior as undefined. @@ -19,38 +27,6 @@ * Other indirect attributes have setter methods. -## Hierarchy - -* shift - * move_to - * align_on_border - * to_corner - * to_edge - * align_to - * center - * set_coord - * set_(x|y|z) - * next_to (TODO: Implement using `move_to`) -* apply_array_function - * apply_function - * apply_complex_function - * apply_matrix - * rotate - * flip - * pose_at_angle - * scale - * scale_to_fit - * scale_to_fit_(width|height|depth) - * stretch - * stretch_to_fit - * stretch_to_fit_(width|height|depth) -* length_over_dim - * get_(width|height|depth) -* get_bounding_box - * get_critical_point (or get_corner, get_edge_center) - * get_(center|bottom|top|left|right|nadir|zenith) - * get_coord - * get_(x|y|z) +# Testing -# Deprecated -* width|height|depth = (set|get)_(width|height|depth) \ No newline at end of file +Tries to ensure that the behavior for mobjects with at least 1 point stays the same through randomized testing. diff --git a/manim/mobject/abstract/positionable.py b/manim/mobject/abstract/positionable.py index 6ac9e23b42..5b8549772f 100644 --- a/manim/mobject/abstract/positionable.py +++ b/manim/mobject/abstract/positionable.py @@ -1,4 +1,4 @@ -from collections.abc import Callable +from collections.abc import Callable, Iterable from typing import Self import numpy as np @@ -28,12 +28,13 @@ Point3DLike, Vector3DLike, ) +from manim.utils.deprecation import deprecated from manim.utils.space_ops import rotation_matrix class Positionable: # FUNDAMENTALS - points: Point3D_Array + points: Point3D_Array = np.array([(0.0, 0.0, 0.0)]) # METHODS @@ -44,10 +45,15 @@ def align_on_border( direction: Vector3DLike, *, buff: float = DEFAULT_MOBJECT_TO_EDGE_BUFFER, - frame: Point3DLike | None = (config.frame_x_radius, config.frame_y_radius, 0), + frame: Point3DLike | None = None, ) -> Self: - target = np.sign(direction) * frame - buff * np.asarray(direction) - return self.move_to(point_or_mobject=target, aligned_edge=direction) + if frame is None: + frame = (config.frame_x_radius, config.frame_y_radius, 0) + target_point = np.sign(direction) * frame + point_to_align = self.get_critical_point(direction=direction) + shift_val = target_point - point_to_align - buff * np.asarray(direction) + shift_val = shift_val * abs(np.sign(direction)) + return self.shift(shift_val) def align_to( self, @@ -55,13 +61,33 @@ def align_to( *, direction: Vector3DLike = ORIGIN, ) -> Self: - target = ( + source = self.get_critical_point(direction=direction) + target = np.array( mobject_or_point.get_critical_point(direction=direction) if isinstance(mobject_or_point, Positionable) else mobject_or_point ) + for i, v in enumerate(np.sign(direction)): + if v == 0: + target[i] = source[i] return self.move_to(point_or_mobject=target, aligned_edge=direction) + def apply_array_function( + self, + function: Callable[[Point3D_Array], Point3D_Array], + *, + about_point: Point3DLike | None = None, + about_edge: Vector3DLike | None = None, + ) -> Self: + if about_point is None: + about_point = self.get_critical_point( + direction=about_edge if about_edge is not None else ORIGIN + ) + self.points -= about_point + self.points = function(self.points) + self.points += about_point + return self + def apply_complex_function( self, function: Callable[[complex], complex], @@ -99,6 +125,13 @@ def mapping_function(points: Point3D_Array) -> Point3D_Array: about_edge=about_edge, ) + @deprecated(replacement="move_to(function(self.get_center()))") + def apply_function_to_position( + self, + function: Callable[[Point3D], Point3D], + ) -> Self: + return self.move_to(function(self.get_center())) + def apply_matrix( self, matrix: MatrixMN, @@ -124,25 +157,32 @@ def apply_matrix( about_edge=about_edge, ) - def apply_array_function( + @deprecated(replacement="apply_array_function") + def apply_points_function_about_point( self, - function: Callable[[Point3D_Array], Point3D_Array], - *, + func: Callable[[Point3D_Array], Point3D_Array], about_point: Point3DLike | None = None, about_edge: Vector3DLike | None = None, ) -> Self: - if about_point is None: - about_point = self.get_critical_point( - direction=about_edge if about_edge is not None else ORIGIN - ) - self.points -= about_point - self.points = function(self.points) - self.points += about_point - return self + return self.apply_array_function( + function=func, + about_point=about_point, + about_edge=about_edge, + ) def center(self) -> Self: return self.move_to(point_or_mobject=ORIGIN) + @property + @deprecated(replacement="get_depth") + def depth(self) -> float: + return self.get_depth() + + @depth.setter + @deprecated(replacement="set_depth") + def depth(self, value: float) -> Self: + return self.set_depth(depth=value, stretch=False) + def flip( self, axis: Vector3DLike = UP, @@ -160,6 +200,9 @@ def flip( def get_bottom(self) -> Point3D: return self.get_critical_point(direction=DOWN) + def get_boundary_point(self, direction: Vector3DLike) -> Point3D: + return self.get_critical_point(direction=direction) + def get_bounding_box(self) -> Point3D_Array: mins = self.points.min(axis=0) maxs = self.points.max(axis=0) @@ -173,6 +216,7 @@ def get_center_of_mass(self) -> Point3D: return self.points.mean(axis=0) def get_coord(self, dim: int, direction: Vector3DLike = ORIGIN) -> float: + # TODO: Optimize by only calculating dim return self.get_critical_point(direction=direction)[dim] def get_corner(self, direction: Vector3DLike) -> Point3D: @@ -184,16 +228,33 @@ def get_critical_point(self, direction: Vector3DLike) -> Point3D: return mids + (maxs - mids) * direction def get_depth(self) -> float: - return self.length_over_dim(dim=2) + return self.get_dim_size(dim=2) + + def get_dim_size(self, dim: int) -> float: + values = self.points[:, dim] + return values.max() - values.min() def get_edge_center(self, direction: Vector3DLike) -> Point3D: return self.get_critical_point(direction=direction) - def get_end(self) -> Point3D: - return self.points[-1] + def get_extremum_along_dim( + self, + dim: int = 0, + key: int = 0, + ) -> float: + values = self.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_height(self) -> float: - return self.length_over_dim(dim=1) + return self.get_dim_size(dim=1) def get_left(self) -> Point3D: return self.get_critical_point(direction=LEFT) @@ -204,17 +265,11 @@ def get_nadir(self) -> Point3D: def get_right(self) -> Point3D: return self.get_critical_point(direction=RIGHT) - def get_start(self) -> Point3D: - return self.points[0] - - def get_start_and_end(self) -> tuple[Point3D, Point3D]: - return self.get_start(), self.get_end() - def get_top(self) -> Point3D: return self.get_critical_point(UP) def get_width(self) -> float: - return self.length_over_dim(dim=0) + return self.get_dim_size(dim=0) def get_x(self, direction: Vector3DLike = ORIGIN) -> float: return self.get_coord(dim=0, direction=direction) @@ -228,9 +283,138 @@ def get_z(self, direction: Vector3DLike = ORIGIN) -> float: def get_zenith(self) -> Point3D: return self.get_critical_point(direction=OUT) + @property + @deprecated(replacement="get_height") + def height(self) -> float: + return self.get_height() + + @height.setter + @deprecated(replacement="set_height") + def height(self, value: float) -> Self: + return self.set_height(height=value, stretch=False) + + def is_off_screen(self) -> bool: + # TODO: Optimize using the bounding box + 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 + return self.get_top()[1] < -config.frame_y_radius + + @deprecated(replacement="get_dim_size") def length_over_dim(self, dim: int) -> float: - values = self.points[:, dim] - return values.max() - values.min() + return self.get_dim_size(dim=dim) + + def match_coord( + self, + mobject: "Positionable", + dim: int, + *, + direction: Vector3DLike = ORIGIN, + ) -> Self: + return self.set_coord( + mobject.get_coord(dim=dim, direction=direction), + dim=dim, + direction=direction, + ) + + def match_depth( + self, + mobject: "Positionable", + stretch: bool, + *, + about_point: Point3DLike | None = None, + about_edge: Vector3DLike | None = None, + ) -> Self: + return self.set_depth( + mobject.get_depth(), + stretch=stretch, + about_point=about_point, + about_edge=about_edge, + ) + + def match_dim_size( + self, + mobject: "Positionable", + dim: int, + stretch: bool, + *, + about_point: Point3DLike | None = None, + about_edge: Vector3DLike | None = None, + ) -> Self: + return self.set_dim_size( + mobject.get_dim_size(dim=dim), + dim=dim, + stretch=stretch, + about_point=about_point, + about_edge=about_edge, + ) + + def match_height( + self, + mobject: "Positionable", + stretch: bool, + *, + about_point: Point3DLike | None = None, + about_edge: Vector3DLike | None = None, + ) -> Self: + return self.set_height( + mobject.get_height(), + stretch=stretch, + about_point=about_point, + about_edge=about_edge, + ) + + def match_points(self, mobject: "Positionable") -> Self: + self.points = mobject.points.copy() + return self + + def match_width( + self, + mobject: "Positionable", + stretch: bool, + *, + about_point: Point3DLike | None = None, + about_edge: Vector3DLike | None = None, + ) -> Self: + return self.set_width( + mobject.get_width(), + stretch=stretch, + about_point=about_point, + about_edge=about_edge, + ) + + def match_x( + self, + mobject: "Positionable", + direction: Vector3DLike = ORIGIN, + ) -> Self: + return self.set_x( + mobject.get_x(direction=direction), + direction=direction, + ) + + def match_y( + self, + mobject: "Positionable", + direction: Vector3DLike = ORIGIN, + ) -> Self: + return self.set_y( + mobject.get_y(direction=direction), + direction=direction, + ) + + def match_z( + self, + mobject: "Positionable", + direction: Vector3DLike = ORIGIN, + ) -> Self: + return self.set_z( + mobject.get_z(direction=direction), + direction=direction, + ) def move_to( self, @@ -279,6 +463,35 @@ def pose_at_angle( about_edge=about_edge, ) + @deprecated() + def reduce_across_dimension( + self, + reduce_func: Callable[[Iterable[float]], float], + dim: int, + ) -> float | None: + if len(self.points) == 0: + return None + + return reduce_func(self.points[:, dim]) + + @deprecated(replacement="set_dim_size") + def rescale_to_fit( + self, + length: float, + dim: int, + stretch: bool, + *, + 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, + ) + def rotate( self, angle: float, @@ -287,12 +500,29 @@ def rotate( about_point: Point3DLike | None = None, about_edge: Vector3DLike | None = None, ) -> Self: + 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, ) + @deprecated(replacement="rotate") + def rotate_about_origin( + self, + angle: float, + axis: Vector3DLike = OUT, + *, + about_edge: Vector3DLike | None = None, + ) -> Self: + return self.rotate( + angle=angle, + axis=axis, + about_point=ORIGIN, + about_edge=about_edge, + ) + def scale( self, # TODO: Rename to 'factor' @@ -314,7 +544,7 @@ def scale_to_fit( about_point: Point3DLike | None = None, about_edge: Vector3DLike | None = None, ) -> Self: - actual_length = self.length_over_dim(dim=dim) + actual_length = self.get_dim_size(dim=dim) if actual_length == 0: return self @@ -373,10 +603,86 @@ def set_coord( *, direction: Vector3DLike = ORIGIN, ) -> Self: + # TODO: Optimize by only calculating dim and using shift target = self.get_critical_point(direction=direction) target[dim] = value return self.move_to(point_or_mobject=target, aligned_edge=direction) + def set_depth( + self, + depth: float, + stretch: bool, + *, + about_point: Point3DLike | None = None, + about_edge: Vector3DLike | None = None, + ) -> Self: + return self.set_dim_size( + size=depth, + dim=2, + stretch=stretch, + about_point=about_point, + about_edge=about_edge, + ) + + def set_dim_size( + self, + size: float, + dim: int, + stretch: bool, + *, + about_point: Point3DLike | None = None, + about_edge: Vector3DLike | None = None, + ) -> Self: + old_length = self.get_dim_size(dim=dim) + if old_length == 0: + return self + factor = size / old_length + if stretch: + return self.stretch( + factor=factor, + dim=dim, + about_point=about_point, + about_edge=about_edge, + ) + else: + return self.scale( + scale_factor=factor, + about_point=about_point, + about_edge=about_edge, + ) + + def set_height( + self, + height: float, + stretch: bool, + *, + about_point: Point3DLike | None = None, + about_edge: Vector3DLike | None = None, + ) -> Self: + return self.set_dim_size( + size=height, + dim=1, + stretch=stretch, + about_point=about_point, + about_edge=about_edge, + ) + + def set_width( + self, + width: float, + stretch: bool, + *, + about_point: Point3DLike | None = None, + about_edge: Vector3DLike | None = None, + ) -> Self: + return self.set_dim_size( + size=width, + dim=0, + stretch=stretch, + about_point=about_point, + about_edge=about_edge, + ) + def set_x( self, x: float, @@ -384,16 +690,38 @@ def set_x( ) -> Self: return self.set_coord(value=x, dim=0, direction=direction) - def set_y(self, y: float, direction: Vector3DLike = ORIGIN) -> Self: + def set_y( + self, + y: float, + direction: Vector3DLike = ORIGIN, + ) -> Self: return self.set_coord(value=y, dim=1, direction=direction) - def set_z(self, z: float, direction: Vector3DLike = ORIGIN) -> Self: + def set_z( + self, + z: float, + direction: Vector3DLike = ORIGIN, + ) -> Self: return self.set_coord(value=z, dim=2, direction=direction) def shift(self, vector: Vector3DLike) -> Self: self.points += vector return self + def shift_onto_screen( + self, + buff: float = DEFAULT_MOBJECT_TO_EDGE_BUFFER, + ) -> Self: + # TODO: Simplify implementation + space_lengths = [config["frame_x_radius"], config["frame_y_radius"]] + for edge in UP, DOWN, LEFT, RIGHT: + dim = np.argmax(np.abs(edge)) + max_val = space_lengths[dim] - buff + edge_center = self.get_edge_center(direction=edge) + if np.dot(edge_center, edge) > max_val: + self.to_edge(edge=edge, buff=buff) + return self + def stretch( self, factor: float, @@ -408,6 +736,22 @@ def stretch( about_edge=about_edge, ) + @deprecated(replacement="stretch") + def stretch_about_point( + self, + factor: float, + dim: int, + point: Point3DLike, + *, + about_edge: Vector3DLike | None = None, + ) -> Self: + return self.stretch( + factor=factor, + dim=dim, + about_point=point, + about_edge=about_edge, + ) + def stretch_to_fit( self, length: float, @@ -416,7 +760,7 @@ def stretch_to_fit( about_point: Point3DLike | None = None, about_edge: Vector3DLike | None = None, ) -> Self: - actual_length = self.length_over_dim(dim=dim) + actual_length = self.get_dim_size(dim=dim) if actual_length == 0: return self @@ -483,21 +827,11 @@ def to_edge( ) -> Self: return self.align_on_border(direction=edge, buff=buff) - # Deprecated - - -def dump_methods() -> None: - seen: set[str] = set() - - for cls in [Mobject, VMobject, OpenGLMobject, OpenGLVMobject]: - assert isinstance(cls, type) - print(cls.__name__) - for name in sorted(cls.__dict__): - if name in seen: - continue - print(f"\t{name}") - seen |= cls.__dict__.keys() - + @property + @deprecated(replacement="get_width") + def width(self) -> float: + return self.get_width() -if __name__ == "__main__": - dump_methods() + @width.setter + def width(self, value: float) -> Self: + return self.set_width(width=value, stretch=False) diff --git a/manim/mobject/abstract/test.py b/manim/mobject/abstract/test.py index 9954805e65..46bccd9124 100644 --- a/manim/mobject/abstract/test.py +++ b/manim/mobject/abstract/test.py @@ -1,42 +1,56 @@ import time from collections.abc import Callable +from logging import getLogger from typing import Any import numpy as np from manim.mobject.abstract.positionable import Positionable from manim.mobject.mobject import Mobject -from manim.typing import Point3D, Vector3D +from manim.typing import Point3D, Point3D_Array, Vector3D -_RNG = np.random.default_rng(seed=1) +_RNG = np.random.default_rng() +POINT_COUNTS = list(range(1, 101)) +LOOPS_PER_POINT_COUNT: int = 10 +UNTESTED = [ + name + for name, attr in Positionable.__dict__.items() + if not (name.startswith("__") or attr is getattr(Positionable.__base__, name, None)) +] -def _random_number(low: float = -10, high: float = 10) -> float: +def optional(value: Any, a: float = 0.9) -> Any | None: + return value if _RNG.uniform() < a else None + + +def random_number(low: float = -10, high: float = 10) -> float: return _RNG.uniform(low=low, high=high) -def _random_point(low: float = -10, high: float = 10) -> Point3D: +def random_point(low: float = -10, high: float = 10) -> Point3D: return _RNG.uniform(low=low, high=high, size=3) -def _random_points(low: float = -10, high: float = 10, size: int = 1) -> np.ndarray: +def random_points(low: float = -10, high: float = 10, size: int = 1) -> np.ndarray: return _RNG.uniform(low=low, high=high, size=(size, 3)) -def _random_vector(low: float = -3, high: float = 3) -> Vector3D: - return _RNG.uniform(low=low, high=high, size=3) +def random_vector(low: float = -3, high: float = 3) -> Vector3D: + dtype = random_choice([int, float]) + return _RNG.uniform(low=low, high=high, size=3).astype(dtype=dtype) -def _random_choice(a: list[Any]) -> Any: +def random_choice(a: list[Any]) -> Any: return _RNG.choice(a=a) -# def _create_another( -# mob: Mobject | Positionable, points: Point3D_Array -# ) -> Mobject | Positionable: -# another = type(mob)() -# another.points = points -# return another +def create_another( + mob: Mobject | Positionable, + points: Point3D_Array, +) -> Mobject | Positionable: + another = type(mob)() + another.points = points + return another def validate_function( @@ -44,15 +58,13 @@ def validate_function( function: Callable[[Mobject | Positionable, dict[Any, Any]], Any], validate: Callable[[Any, Any], None], create_kwargs: Callable[[], dict[Any, Any]], - *, - point_counts: list[int] = list(range(1, 101)), - loop_count: int = 100, ) -> None: + global POINT_COUNTS, LOOPS_PER_POINT_COUNT time_old, time_new = 0, 0 - for point_count in point_counts: - for _ in range(loop_count): - points = _random_points(size=point_count) + for point_count in POINT_COUNTS: + for _ in range(LOOPS_PER_POINT_COUNT): + points = random_points(size=point_count) mob_old = Mobject() mob_old.points = points.copy() @@ -60,6 +72,7 @@ def validate_function( mob_new.points = points.copy() kwargs = create_kwargs() + kwargs = {key: value for key, value in kwargs.items() if value is not None} start = time.perf_counter_ns() result_old = function(mob_old, kwargs) @@ -69,9 +82,15 @@ def validate_function( result_new = function(mob_new, kwargs) time_new += time.perf_counter_ns() - start - validate(result_old, result_new) + try: + validate(result_old, result_new) + except AssertionError as e: + raise ValueError( + f"\nPoint Count: {point_count}\nKwargs: {kwargs}\nPoints: {points}\nOld Result: {result_old}\nNew Result: {result_new}" + ) from e # noqa: B904 - print(name.ljust(25), f"{time_old / time_new:1.2f}x".ljust(6)) + print(f"\t{name.ljust(25)}\t{time_old / time_new:1.2f}x".ljust(6)) + UNTESTED.remove(name) def validate_setter( @@ -109,24 +128,43 @@ def validate(result_1: Any, result_2: Any) -> None: def main() -> None: + getLogger("manim").addFilter(lambda x: "deprecated" not in x.getMessage()) + validate_setter( "align_on_border", lambda mob, kwargs: mob.align_on_border(**kwargs), - lambda: {"direction": _random_vector(), "buff": _random_number()}, + lambda: { + "direction": random_vector(), + "buff": optional(random_number()), + }, ) validate_setter( "align_to", lambda mob, kwargs: mob.align_to(**kwargs), - lambda: {"mobject_or_point": _random_point(), "direction": _random_vector()}, + lambda: { + "mobject_or_point": random_point(), + "direction": optional(random_vector()), + }, ) + # TODO: apply_complex_function + # TODO: apply_function + # TODO: apply_function_to_position + # TODO: apply_matrix + # TODO: apply_points_function_about_point validate_setter("center", lambda mob, _: mob.center()) + validate_getter("depth", lambda mob, _: mob.depth) + validate_setter( + "depth", + lambda mob, kwargs: setattr(mob, "depth", kwargs["value"]), + lambda: {"value": random_number()}, + ) validate_setter( "flip", lambda mob, kwargs: mob.flip(**kwargs), lambda: { - "axis": _random_vector(), - "about_point": _random_point(), - "about_edge": _random_vector(), + "axis": optional(random_vector()), + "about_point": optional(random_point()), + "about_edge": optional(random_vector()), }, ) validate_getter("get_bottom", lambda mob, _: mob.get_bottom()) @@ -135,29 +173,29 @@ def main() -> None: validate_getter( "get_coord", lambda mob, kwargs: mob.get_coord(**kwargs), - lambda: {"dim": _random_choice([0, 1, 2]), "direction": _random_vector()}, + lambda: { + "dim": random_choice([0, 1, 2]), + "direction": optional(random_vector()), + }, ) validate_getter( "get_corner", lambda mob, kwargs: mob.get_corner(**kwargs), - lambda: {"direction": _random_vector()}, + lambda: {"direction": random_vector()}, ) validate_getter( "get_critical_point", lambda mob, kwargs: mob.get_critical_point(**kwargs), - lambda: {"direction": _random_vector()}, + lambda: {"direction": random_vector()}, ) validate_getter( "get_edge_center", lambda mob, kwargs: mob.get_edge_center(**kwargs), - lambda: {"direction": _random_vector()}, + lambda: {"direction": random_vector()}, ) - validate_getter("get_end", lambda mob, _: mob.get_end()) validate_getter("get_left", lambda mob, _: mob.get_left()) validate_getter("get_nadir", lambda mob, _: mob.get_nadir()) validate_getter("get_right", lambda mob, _: mob.get_right()) - validate_getter("get_start", lambda mob, _: mob.get_start()) - validate_getter("get_start_and_end", lambda mob, _: mob.get_start_and_end()) validate_getter("get_top", lambda mob, _: mob.get_top()) validate_getter("get_x", lambda mob, _: mob.get_x()) validate_getter("get_y", lambda mob, _: mob.get_y()) @@ -166,155 +204,179 @@ def main() -> None: validate_getter( "length_over_dim", lambda mob, kwargs: mob.length_over_dim(**kwargs), - lambda: {"dim": _random_choice([0, 1, 2])}, + lambda: {"dim": random_choice([0, 1, 2])}, ) validate_setter( "move_to", lambda mob, kwargs: mob.move_to(**kwargs), lambda: { - "point_or_mobject": _random_point(), - "aligned_edge": _random_vector(), - "coor_mask": _random_vector(), + "point_or_mobject": random_point(), + "aligned_edge": optional(random_vector()), + "coor_mask": optional(random_vector()), }, ) validate_setter( "next_to", lambda mob, kwargs: mob.next_to(**kwargs), lambda: { - "mobject_or_point": _random_point(), - "direction": _random_vector(), - "buff": _random_number(), - "aligned_edge": _random_vector(), - "coor_mask": _random_vector(), + "mobject_or_point": random_point(), + "direction": optional(random_vector()), + "buff": optional(random_number()), + "aligned_edge": optional(random_vector()), + "coor_mask": optional(random_vector()), }, ) validate_setter( "pose_at_angle", lambda mob, kwargs: mob.pose_at_angle(**kwargs), - lambda: {"about_point": _random_point(), "about_edge": _random_vector()}, + lambda: { + "about_point": optional(random_point()), + "about_edge": optional(random_vector()), + }, ) validate_setter( "rotate", lambda mob, kwargs: mob.rotate(**kwargs), lambda: { - "angle": _random_number(), - "axis": _random_vector(), - "about_edge": _random_vector(), + "angle": random_number(), + "axis": optional(random_vector()), + "about_edge": optional(random_vector()), }, ) validate_setter( "scale", lambda mob, kwargs: mob.scale(**kwargs), lambda: { - "scale_factor": _random_number(), - "about_point": _random_point(), - "about_edge": _random_vector(), + "scale_factor": random_number(), + "about_point": optional(random_point()), + "about_edge": optional(random_vector()), }, ) validate_setter( "scale_to_fit_depth", lambda mob, kwargs: mob.scale_to_fit_depth(**kwargs), lambda: { - "depth": _random_number(), - "about_point": _random_point(), - "about_edge": _random_vector(), + "depth": random_number(), + "about_point": optional(random_point()), + "about_edge": optional(random_vector()), }, ) validate_setter( "scale_to_fit_height", lambda mob, kwargs: mob.scale_to_fit_height(**kwargs), lambda: { - "height": _random_number(), - "about_point": _random_point(), - "about_edge": _random_vector(), + "height": random_number(), + "about_point": optional(random_point()), + "about_edge": optional(random_vector()), }, ) validate_setter( "scale_to_fit_width", lambda mob, kwargs: mob.scale_to_fit_width(**kwargs), lambda: { - "width": _random_number(), - "about_point": _random_point(), - "about_edge": _random_vector(), + "width": random_number(), + "about_point": optional(random_point()), + "about_edge": optional(random_vector()), }, ) validate_setter( "set_coord", lambda mob, kwargs: mob.set_coord(**kwargs), lambda: { - "value": _random_number(), - "dim": _random_choice([0, 1, 2]), - "direction": _random_vector(), + "value": random_number(), + "dim": random_choice([0, 1, 2]), + "direction": optional(random_vector()), }, ) validate_setter( "set_x", lambda mob, kwargs: mob.set_x(**kwargs), - lambda: {"x": _random_number(), "direction": _random_vector()}, + lambda: { + "x": random_number(), + "direction": optional(random_vector()), + }, ) validate_setter( "set_y", lambda mob, kwargs: mob.set_y(**kwargs), - lambda: {"y": _random_number(), "direction": _random_vector()}, + lambda: { + "y": random_number(), + "direction": optional(random_vector()), + }, ) validate_setter( "set_z", lambda mob, kwargs: mob.set_z(**kwargs), - lambda: {"z": _random_number(), "direction": _random_vector()}, + lambda: { + "z": random_number(), + "direction": optional(random_vector()), + }, ) validate_setter( "shift", lambda mob, kwargs: mob.shift(kwargs["value"]), - lambda: {"value": _random_vector()}, + lambda: { + "value": random_vector(), + }, ) validate_setter( "stretch", lambda mob, kwargs: mob.stretch(**kwargs), lambda: { - "factor": _random_number(), - "dim": _random_choice([0, 1, 2]), - "about_point": _random_point(), - "about_edge": _random_vector(), + "factor": random_number(), + "dim": random_choice([0, 1, 2]), + "about_point": optional(random_point()), + "about_edge": optional(random_vector()), }, ) validate_setter( "stretch_to_fit_depth", lambda mob, kwargs: mob.stretch_to_fit_depth(**kwargs), lambda: { - "depth": _random_number(), - "about_point": _random_point(), - "about_edge": _random_vector(), + "depth": random_number(), + "about_point": optional(random_point()), + "about_edge": optional(random_vector()), }, ) validate_setter( "stretch_to_fit_height", lambda mob, kwargs: mob.stretch_to_fit_height(**kwargs), lambda: { - "height": _random_number(), - "about_point": _random_point(), - "about_edge": _random_vector(), + "height": random_number(), + "about_point": optional(random_point()), + "about_edge": optional(random_vector()), }, ) validate_setter( "stretch_to_fit_width", lambda mob, kwargs: mob.stretch_to_fit_width(**kwargs), lambda: { - "width": _random_number(), - "about_point": _random_point(), - "about_edge": _random_vector(), + "width": random_number(), + "about_point": optional(random_point()), + "about_edge": optional(random_vector()), }, ) validate_setter( "to_corner", lambda mob, kwargs: mob.to_corner(**kwargs), - lambda: {"corner": _random_vector(), "buff": _random_number()}, + lambda: { + "corner": random_vector(), + "buff": random_number(), + }, ) validate_setter( "to_edge", lambda mob, kwargs: mob.to_edge(**kwargs), - lambda: {"edge": _random_vector(), "buff": _random_number()}, + lambda: { + "edge": optional(random_vector()), + "buff": optional(random_number()), + }, ) + print("Untested") + for name in UNTESTED: + print(f"\t{name}") + if __name__ == "__main__": main() From 69de5680b65ec8562f28e888118f1124f5bd9164 Mon Sep 17 00:00:00 2001 From: GniLudio Date: Wed, 19 Aug 2026 11:11:09 +0200 Subject: [PATCH 06/66] Update --- manim/mobject/abstract/positionable.py | 40 ++- manim/mobject/abstract/test.py | 405 ++++++++++++++++++------- 2 files changed, 312 insertions(+), 133 deletions(-) diff --git a/manim/mobject/abstract/positionable.py b/manim/mobject/abstract/positionable.py index 5b8549772f..8e562ae8b9 100644 --- a/manim/mobject/abstract/positionable.py +++ b/manim/mobject/abstract/positionable.py @@ -201,7 +201,8 @@ def get_bottom(self) -> Point3D: return self.get_critical_point(direction=DOWN) def get_boundary_point(self, direction: Vector3DLike) -> Point3D: - return self.get_critical_point(direction=direction) + index = np.argmax(np.dot(self.points, direction)) + return self.points[index] def get_bounding_box(self) -> Point3D_Array: mins = self.points.min(axis=0) @@ -295,13 +296,12 @@ def height(self, value: float) -> Self: def is_off_screen(self) -> bool: # TODO: Optimize using the bounding box - 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 - return self.get_top()[1] < -config.frame_y_radius + return ( + self.get_left()[0] > config["frame_x_radius"] + or self.get_right()[0] < -config["frame_x_radius"] + or self.get_bottom()[1] > config["frame_y_radius"] + or self.get_top()[1] < -config["frame_y_radius"] + ) @deprecated(replacement="get_dim_size") def length_over_dim(self, dim: int) -> float: @@ -323,8 +323,8 @@ def match_coord( def match_depth( self, mobject: "Positionable", - stretch: bool, *, + stretch: bool = False, about_point: Point3DLike | None = None, about_edge: Vector3DLike | None = None, ) -> Self: @@ -339,8 +339,8 @@ def match_dim_size( self, mobject: "Positionable", dim: int, - stretch: bool, *, + stretch: bool = False, about_point: Point3DLike | None = None, about_edge: Vector3DLike | None = None, ) -> Self: @@ -355,8 +355,8 @@ def match_dim_size( def match_height( self, mobject: "Positionable", - stretch: bool, *, + stretch: bool = False, about_point: Point3DLike | None = None, about_edge: Vector3DLike | None = None, ) -> Self: @@ -374,8 +374,8 @@ def match_points(self, mobject: "Positionable") -> Self: def match_width( self, mobject: "Positionable", - stretch: bool, *, + stretch: bool = False, about_point: Point3DLike | None = None, about_edge: Vector3DLike | None = None, ) -> Self: @@ -479,8 +479,8 @@ def rescale_to_fit( self, length: float, dim: int, - stretch: bool, *, + stretch: bool = False, about_point: Point3DLike | None = None, about_edge: Vector3DLike | None = None, ) -> Self: @@ -513,14 +513,11 @@ def rotate_about_origin( self, angle: float, axis: Vector3DLike = OUT, - *, - about_edge: Vector3DLike | None = None, ) -> Self: return self.rotate( angle=angle, axis=axis, about_point=ORIGIN, - about_edge=about_edge, ) def scale( @@ -611,8 +608,8 @@ def set_coord( def set_depth( self, depth: float, - stretch: bool, *, + stretch: bool = False, about_point: Point3DLike | None = None, about_edge: Vector3DLike | None = None, ) -> Self: @@ -628,8 +625,8 @@ def set_dim_size( self, size: float, dim: int, - stretch: bool, *, + stretch: bool = False, about_point: Point3DLike | None = None, about_edge: Vector3DLike | None = None, ) -> Self: @@ -654,8 +651,8 @@ def set_dim_size( def set_height( self, height: float, - stretch: bool, *, + stretch: bool = False, about_point: Point3DLike | None = None, about_edge: Vector3DLike | None = None, ) -> Self: @@ -670,8 +667,8 @@ def set_height( def set_width( self, width: float, - stretch: bool, *, + stretch: bool = False, about_point: Point3DLike | None = None, about_edge: Vector3DLike | None = None, ) -> Self: @@ -742,14 +739,11 @@ def stretch_about_point( factor: float, dim: int, point: Point3DLike, - *, - about_edge: Vector3DLike | None = None, ) -> Self: return self.stretch( factor=factor, dim=dim, about_point=point, - about_edge=about_edge, ) def stretch_to_fit( diff --git a/manim/mobject/abstract/test.py b/manim/mobject/abstract/test.py index 46bccd9124..d855836552 100644 --- a/manim/mobject/abstract/test.py +++ b/manim/mobject/abstract/test.py @@ -11,7 +11,7 @@ _RNG = np.random.default_rng() POINT_COUNTS = list(range(1, 101)) -LOOPS_PER_POINT_COUNT: int = 10 +LOOPS_PER_POINT_COUNT: int = 100 UNTESTED = [ name for name, attr in Positionable.__dict__.items() @@ -19,114 +19,6 @@ ] -def optional(value: Any, a: float = 0.9) -> Any | None: - return value if _RNG.uniform() < a else None - - -def random_number(low: float = -10, high: float = 10) -> float: - return _RNG.uniform(low=low, high=high) - - -def random_point(low: float = -10, high: float = 10) -> Point3D: - return _RNG.uniform(low=low, high=high, size=3) - - -def random_points(low: float = -10, high: float = 10, size: int = 1) -> np.ndarray: - return _RNG.uniform(low=low, high=high, size=(size, 3)) - - -def random_vector(low: float = -3, high: float = 3) -> Vector3D: - dtype = random_choice([int, float]) - return _RNG.uniform(low=low, high=high, size=3).astype(dtype=dtype) - - -def random_choice(a: list[Any]) -> Any: - return _RNG.choice(a=a) - - -def create_another( - mob: Mobject | Positionable, - points: Point3D_Array, -) -> Mobject | Positionable: - another = type(mob)() - another.points = points - return another - - -def validate_function( - name: str, - function: Callable[[Mobject | Positionable, dict[Any, Any]], Any], - validate: Callable[[Any, Any], None], - create_kwargs: Callable[[], dict[Any, Any]], -) -> None: - global POINT_COUNTS, LOOPS_PER_POINT_COUNT - time_old, time_new = 0, 0 - - for point_count in POINT_COUNTS: - for _ in range(LOOPS_PER_POINT_COUNT): - points = random_points(size=point_count) - - mob_old = Mobject() - mob_old.points = points.copy() - mob_new = Positionable() - mob_new.points = points.copy() - - kwargs = create_kwargs() - kwargs = {key: value for key, value in kwargs.items() if value is not None} - - start = time.perf_counter_ns() - result_old = function(mob_old, kwargs) - time_old += time.perf_counter_ns() - start - - start = time.perf_counter_ns() - result_new = function(mob_new, kwargs) - time_new += time.perf_counter_ns() - start - - try: - validate(result_old, result_new) - except AssertionError as e: - raise ValueError( - f"\nPoint Count: {point_count}\nKwargs: {kwargs}\nPoints: {points}\nOld Result: {result_old}\nNew Result: {result_new}" - ) from e # noqa: B904 - - print(f"\t{name.ljust(25)}\t{time_old / time_new:1.2f}x".ljust(6)) - UNTESTED.remove(name) - - -def validate_setter( - name: str, - function: Callable[[Mobject | Positionable, dict[Any, Any]], Any], - create_kwargs: Callable[[], Any] = lambda: {}, -): - def validate(result_1: Any, result_2: Any) -> None: - assert isinstance(result_1, Positionable | Mobject) - assert isinstance(result_2, Positionable | Mobject) - assert np.allclose(result_1.points, result_2.points) - - validate_function( - name=name, - function=function, - validate=validate, - create_kwargs=create_kwargs, - ) - - -def validate_getter( - name: str, - function: Callable[[Mobject | Positionable, dict[Any, Any]], Any], - create_kwargs: Callable[[], Any] = lambda: {}, -): - def validate(result_1: Any, result_2: Any) -> None: - assert np.allclose(result_1, result_2) - - validate_function( - name=name, - function=function, - validate=validate, - create_kwargs=create_kwargs, - ) - - def main() -> None: getLogger("manim").addFilter(lambda x: "deprecated" not in x.getMessage()) @@ -168,6 +60,11 @@ def main() -> None: }, ) validate_getter("get_bottom", lambda mob, _: mob.get_bottom()) + validate_getter( + "get_boundary_point", + lambda mob, kwargs: mob.get_boundary_point(**kwargs), + lambda: {"direction": random_vector()}, + ) validate_getter("get_center", lambda mob, _: mob.get_center()) validate_getter("get_center_of_mass", lambda mob, _: mob.get_center_of_mass()) validate_getter( @@ -193,6 +90,14 @@ def main() -> None: lambda mob, kwargs: mob.get_edge_center(**kwargs), lambda: {"direction": random_vector()}, ) + validate_getter( + "get_extremum_along_dim", + lambda mob, kwargs: mob.get_extremum_along_dim(**kwargs), + lambda: { + "dim": random_choice([0, 1, 2]), + "key": random_choice([0, 1, 2]), + }, + ) validate_getter("get_left", lambda mob, _: mob.get_left()) validate_getter("get_nadir", lambda mob, _: mob.get_nadir()) validate_getter("get_right", lambda mob, _: mob.get_right()) @@ -201,11 +106,119 @@ def main() -> None: validate_getter("get_y", lambda mob, _: mob.get_y()) validate_getter("get_z", lambda mob, _: mob.get_z()) validate_getter("get_zenith", lambda mob, _: mob.get_zenith()) + validate_getter("height", lambda mob, _: mob.height) + validate_setter( + "height", + lambda mob, kwargs: setattr(mob, "height", kwargs["value"]), + lambda: { + "value": random_number(), + }, + ) + validate_getter("is_off_screen", lambda mob, _: mob.is_off_screen()) validate_getter( "length_over_dim", lambda mob, kwargs: mob.length_over_dim(**kwargs), lambda: {"dim": random_choice([0, 1, 2])}, ) + validate_setter( + "match_coord", + lambda mob, kwargs: mob.match_coord( + create_another(mob, kwargs.pop("points")), **kwargs + ), + lambda: { + "points": random_points(size=int(random_number(1, 100))), + "dim": random_choice([0, 1, 2]), + "direction": optional(random_vector()), + }, + ) + validate_setter( + "match_depth", + lambda mob, kwargs: mob.match_depth( + create_another(mob, kwargs.pop("points")), **kwargs + ), + lambda: { + "points": random_points(size=int(random_number(1, 100))), + "stretch": optional(random_choice([True, False])), + "about_point": optional(random_point()), + "about_edge": optional(random_vector()), + }, + ) + validate_setter( + "match_dim_size", + lambda mob, kwargs: mob.match_dim_size( + create_another(mob, kwargs.pop("points")), **kwargs + ), + lambda: { + "points": random_points(size=int(random_number(1, 100))), + "dim": random_choice([0, 1, 2]), + "stretch": optional(random_choice([True, False])), + "about_point": optional(random_point()), + "about_edge": optional(random_vector()), + }, + ) + validate_setter( + "match_height", + lambda mob, kwargs: mob.match_height( + create_another(mob, kwargs.pop("points")), **kwargs + ), + lambda: { + "points": random_points(size=int(random_number(1, 100))), + "stretch": optional(random_choice([True, False])), + "about_point": optional(random_point()), + "about_edge": optional(random_vector()), + }, + ) + validate_setter( + "match_points", + lambda mob, kwargs: mob.match_points( + create_another(mob, kwargs.pop("points")), **kwargs + ), + lambda: { + "points": random_points(size=int(random_number(1, 100))), + }, + ) + validate_setter( + "match_width", + lambda mob, kwargs: mob.match_width( + create_another(mob, kwargs.pop("points")), **kwargs + ), + lambda: { + "points": random_points(size=int(random_number(1, 100))), + "stretch": optional(random_choice([True, False])), + "about_point": optional(random_point()), + "about_edge": optional(random_vector()), + }, + ) + validate_setter( + "match_x", + lambda mob, kwargs: mob.match_x( + create_another(mob, kwargs.pop("points")), **kwargs + ), + lambda: { + "points": random_points(size=int(random_number(1, 100))), + "direction": optional(random_vector()), + }, + ) + validate_setter( + "match_y", + lambda mob, kwargs: mob.match_y( + create_another(mob, kwargs.pop("points")), **kwargs + ), + lambda: { + "points": random_points(size=int(random_number(1, 100))), + "direction": optional(random_vector()), + }, + ) + validate_setter( + "match_z", + lambda mob, kwargs: mob.match_z( + create_another(mob, kwargs.pop("points")), **kwargs + ), + lambda: { + "points": random_points(size=int(random_number(1, 100))), + "direction": optional(random_vector()), + }, + ) validate_setter( "move_to", lambda mob, kwargs: mob.move_to(**kwargs), @@ -234,6 +247,18 @@ def main() -> None: "about_edge": optional(random_vector()), }, ) + # TODO: reduce_across_dimension + validate_setter( + "rescale_to_fit", + lambda mob, kwargs: mob.rescale_to_fit(**kwargs), + lambda: { + "length": random_number(), + "dim": random_choice([0, 1, 2]), + "stretch": optional(random_choice([True, False])), + "about_point": optional(random_point()), + "about_edge": optional(random_vector()), + }, + ) validate_setter( "rotate", lambda mob, kwargs: mob.rotate(**kwargs), @@ -243,6 +268,14 @@ def main() -> None: "about_edge": optional(random_vector()), }, ) + validate_setter( + "rotate_about_origin", + lambda mob, kwargs: mob.rotate_about_origin(**kwargs), + lambda: { + "angle": random_number(), + "axis": optional(random_vector()), + }, + ) validate_setter( "scale", lambda mob, kwargs: mob.scale(**kwargs), @@ -319,6 +352,13 @@ def main() -> None: "value": random_vector(), }, ) + validate_setter( + "shift_onto_screen", + lambda mob, kwargs: mob.shift_onto_screen(**kwargs), + lambda: { + "buff": random_number(), + }, + ) validate_setter( "stretch", lambda mob, kwargs: mob.stretch(**kwargs), @@ -329,6 +369,15 @@ def main() -> None: "about_edge": optional(random_vector()), }, ) + validate_setter( + "stretch_about_point", + lambda mob, kwargs: mob.stretch_about_point(**kwargs), + lambda: { + "factor": random_number(), + "dim": random_choice([0, 1, 2]), + "point": random_point(), + }, + ) validate_setter( "stretch_to_fit_depth", lambda mob, kwargs: mob.stretch_to_fit_depth(**kwargs), @@ -373,9 +422,145 @@ def main() -> None: }, ) + validate_getter("width", lambda mob, _: mob.width) + validate_setter( + "width", + lambda mob, kwargs: setattr(mob, "width", kwargs["value"]), + lambda: { + "value": random_number(), + }, + ) + print("Untested") for name in UNTESTED: - print(f"\t{name}") + if hasattr(Mobject, name): + print(f"\t{name}") + + +def validate_function( + name: str, + function: Callable[[Mobject | Positionable, dict[Any, Any]], Any], + validate: Callable[[Mobject, Positionable, Any, Any], None], + create_kwargs: Callable[[], dict[Any, Any]], +) -> None: + global POINT_COUNTS, LOOPS_PER_POINT_COUNT + time_old, time_new = 0, 0 + + for point_count in POINT_COUNTS: + for _ in range(LOOPS_PER_POINT_COUNT): + points = random_points(size=point_count) + + mob_old = Mobject() + mob_old.points = points.copy() + mob_new = Positionable() + mob_new.points = points.copy() + + kwargs = create_kwargs() + kwargs = {key: value for key, value in kwargs.items() if value is not None} + + start = time.perf_counter_ns() + result_old = function(mob_old, kwargs.copy()) + time_old += time.perf_counter_ns() - start + + start = time.perf_counter_ns() + result_new = function(mob_new, kwargs.copy()) + time_new += time.perf_counter_ns() - start + + try: + validate(mob_old, mob_new, result_old, result_new) + except AssertionError as e: + raise ValueError( + f""" + Point Count: {point_count} + Kwargs: {kwargs} + Points: {points} + Old Result: {result_old} + New Result: {result_new} + Old Points: {mob_old.points} + New Points: {mob_new.points} + """.replace(" ", "") + ) from e + + print(f"\t{name.ljust(25)}\t{time_old / time_new:1.2f}x\t{time_old / 1e9:.0f}s") + if name in UNTESTED: + UNTESTED.remove(name) + + +def validate_setter( + name: str, + function: Callable[[Mobject | Positionable, dict[Any, Any]], Any], + create_kwargs: Callable[[], Any] = lambda: {}, +): + def validate( + mob_old: Mobject, + mob_new: Positionable, + result_old: Any, + result_new: Any, + ) -> None: + assert (result_old is None) == (result_new is None) + assert np.allclose(mob_old.points, mob_new.points) + + validate_function( + name=name, + function=function, + validate=validate, + create_kwargs=create_kwargs, + ) + + +def validate_getter( + name: str, + function: Callable[[Mobject | Positionable, dict[Any, Any]], Any], + create_kwargs: Callable[[], Any] = lambda: {}, +): + def validate( + mob_old: Mobject, + mob_new: Positionable, + result_old: Any, + result_new: Any, + ) -> None: + assert np.allclose(result_old, result_new) + + validate_function( + name=name, + function=function, + validate=validate, + create_kwargs=create_kwargs, + ) + + +def optional(value: Any, a: float = 0.9) -> Any | None: + return value if _RNG.uniform() < a else None + + +def random_number(low: float = -10, high: float = 10) -> float: + return _RNG.uniform(low=low, high=high) + + +def random_point(low: float = -10, high: float = 10) -> Point3D: + return _RNG.uniform(low=low, high=high, size=3) + + +def random_points(low: float = -10, high: float = 10, size: int = 1) -> np.ndarray: + return _RNG.uniform(low=low, high=high, size=(size, 3)) + + +def random_vector(low: float = -3, high: float = 3) -> Vector3D: + dtype = random_choice([int, float]) + return _RNG.uniform(low=low, high=high, size=3).astype(dtype=dtype) + + +def random_choice(a: list[Any]) -> Any: + return _RNG.choice(a=a) + + +def create_another( + mob: Mobject | Positionable, + points: Point3D_Array, +) -> Mobject | Positionable: + another = type(mob)() + another.points = points + return another if __name__ == "__main__": From 4be12517ea937c011dbf38292c72fe60598094dd Mon Sep 17 00:00:00 2001 From: GniLudio Date: Wed, 19 Aug 2026 11:23:28 +0200 Subject: [PATCH 07/66] Update --- manim/mobject/abstract/positionable.md | 18 +++++++++++++++++- manim/mobject/abstract/positionable.py | 4 ---- manim/mobject/abstract/test.py | 8 ++++---- 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/manim/mobject/abstract/positionable.md b/manim/mobject/abstract/positionable.md index 06a1df89a4..1fa6433df8 100644 --- a/manim/mobject/abstract/positionable.md +++ b/manim/mobject/abstract/positionable.md @@ -29,4 +29,20 @@ # Testing -Tries to ensure that the behavior for mobjects with at least 1 point stays the same through randomized testing. +Tries to ensure the same behavior for mobjects with at least 1 point by randomized testing. +Run `python test.py` to run the randomized tests. + +## Pseudo Code +```py +function = lambda mob, kwargs: mob.some_function(**kwargs) # function that you want to test + +for point_count in (1, 100): # tests different point counts + for _ in range(100): # test every point count many times + points = random_points(point_count) # generate points + mob_old = Mobject().set_points(points) # create old implementation + mob_new = Positionable().set_points(points) # create new implementation + kwargs = random_parameters() # randomize parameters + result_old = function(mob_old, kwargs) # apply old implementation + result_new = function(mob_new, kwargs) # apply new implementation + validate(mob_old, mob_new, result_old, result_new) # compare results +``` \ No newline at end of file diff --git a/manim/mobject/abstract/positionable.py b/manim/mobject/abstract/positionable.py index 8e562ae8b9..6c848826dc 100644 --- a/manim/mobject/abstract/positionable.py +++ b/manim/mobject/abstract/positionable.py @@ -17,10 +17,6 @@ TAU, UP, ) -from manim.mobject.mobject import Mobject -from manim.mobject.opengl.opengl_mobject import OpenGLMobject -from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject -from manim.mobject.types.vectorized_mobject import VMobject from manim.typing import ( MatrixMN, Point3D, diff --git a/manim/mobject/abstract/test.py b/manim/mobject/abstract/test.py index d855836552..38c6c00171 100644 --- a/manim/mobject/abstract/test.py +++ b/manim/mobject/abstract/test.py @@ -441,7 +441,7 @@ def validate_function( name: str, function: Callable[[Mobject | Positionable, dict[Any, Any]], Any], validate: Callable[[Mobject, Positionable, Any, Any], None], - create_kwargs: Callable[[], dict[Any, Any]], + generate_random_parameters: Callable[[], dict[Any, Any]], ) -> None: global POINT_COUNTS, LOOPS_PER_POINT_COUNT time_old, time_new = 0, 0 @@ -455,7 +455,7 @@ def validate_function( mob_new = Positionable() mob_new.points = points.copy() - kwargs = create_kwargs() + kwargs = generate_random_parameters() kwargs = {key: value for key, value in kwargs.items() if value is not None} start = time.perf_counter_ns() @@ -504,7 +504,7 @@ def validate( name=name, function=function, validate=validate, - create_kwargs=create_kwargs, + generate_random_parameters=create_kwargs, ) @@ -525,7 +525,7 @@ def validate( name=name, function=function, validate=validate, - create_kwargs=create_kwargs, + generate_random_parameters=create_kwargs, ) From f7f3ab1ddabf012a10c9a9cd5d4e99ffa36d6dd3 Mon Sep 17 00:00:00 2001 From: GniLudio Date: Wed, 19 Aug 2026 18:11:02 +0200 Subject: [PATCH 08/66] Update --- manim/mobject/abstract/positionable.md | 122 +++++++++++++++++++------ manim/mobject/abstract/positionable.py | 3 + manim/mobject/abstract/test.py | 12 ++- manim/mobject/abstract/test.txt | Bin 0 -> 5930 bytes 4 files changed, 103 insertions(+), 34 deletions(-) create mode 100644 manim/mobject/abstract/test.txt diff --git a/manim/mobject/abstract/positionable.md b/manim/mobject/abstract/positionable.md index 1fa6433df8..4ac2e54d27 100644 --- a/manim/mobject/abstract/positionable.md +++ b/manim/mobject/abstract/positionable.md @@ -1,38 +1,102 @@ # Positionable +## TODO + +- Handling for 0 points? +- Documentation + ## Changes -> TODO -## TODO -* Handling for 0 points -* Documentation -* Helpful error messages - -## Notes -* How should mobject with 0 points be handled? - * Currently: Treats behavior as undefined. - * Advantage: Makes some calculations a lot simpler. - * Disadvantage: Results in some breaking changes. - * Consideration: Simplicity/Efficiency vs Guarding Exceptions. -* Should properties be dropped in favor of setter/getter methods? - * E.g. `width`, `height` and `depth`. - * Advantages: - * More in line with "manim-code-style". - * Would allow method chaining. - * Would allow additional optional parameters. - * Alternative: - * Support both. - * Disadvantage: - * Isn't really an actual value behind the scenes. - * Other indirect attributes have setter methods. - - -# Testing +| Attribute | Description | Speed | +| :---------------------------------: | :--------------------------------------------------------------: | :---: | +| `align_on_border` | made `buff` keyword-only
added `frame` parameter | 1.51x | +| `align_to` | made `direction` keyword-only | 0.72x | +| `apply_array_function` | new
renamed from `apply_points_function_about_point` | - | +| `apply_complex_function` | - | - | +| `apply_function` | - | - | +| `apply_function_to_position` | deprecated
use `move_to(function(self.get_center()))` instead | - | +| `apply_matrix` | - | - | +| `apply_points_function_about_point` | deprecated - use `apply_array_function` instead | - | +| `center` | - | 2.08x | +| `depth` | deprecated - use `get_depth` instead | 0.37x | +| `depth` | deprecated - use `set_depth` instead | 1.01x | +| `flip` | - | 1.35x | +| `get_bottom` | - | | +| `get_boundary_point` | - | | +| `get_bounding_box` | new | - | +| `get_center` | - | | +| `get_center_of_mass` | - | | +| `get_coord` | - | | +| `get_corner` | deprecated - use `get_critical_point` instead | | +| `get_critical_point` | - | | +| `get_depth` | new - replacement for `depth` | | +| `get_dim_size` | new | | +| `get_edge_center` | deprecated - use `get_critical_point` instead | | +| `get_extremum_along_dim` | deprecated | | +| `get_height` | new - replacement for `height` | | +| `get_left` | - | | +| `get_nadir` | - | | +| `get_right` | - | | +| `get_top` | - | | +| `get_width` | new - replacement for `width` | | +| `get_x` | - | | +| `get_y` | - | | +| `get_z` | - | | +| `get_zenith` | - | | +| `height` | deprecated - use `get_height` instead | | +| `height` | deprecated - use `set_height` instead | | +| `is_off_screen` | - | | +| `length_over_dim` | deprecated - use `get_dim_size` instead | | +| `match_coord` | made `direction` keyword-only | | +| `match_depth` | made kwargs explicit | | +| `match_dim_size` | made kwargs explicit | | +| `match_height` | made kwargs explicit | | +| `match_points` | removed `copy_submobjects` parameter | | +| `match_width` | | | +| `match_x` | | | +| `match_y` | | | +| `match_z` | | | +| `move_to` | | | +| `next_to` | | | +| `pose_at_angle` | | | +| `reduce_across_dimension` | | | +| `rescale_to_fit` | | | +| `rotate` | | | +| `rotate_about_origin` | | | +| `scale` | | | +| `scale_to_fit` | (new) | | +| `scale_to_fit_depth` | | | +| `scale_to_fit_height` | | | +| `scale_to_fit_width` | | | +| `set_coord` | | | +| `set_depth` | | | +| `set_dim_size` | (new) | | +| `set_height` | | | +| `set_width` | | | +| `set_x` | | | +| `set_y` | | | +| `set_z` | | | +| `shift` | | | +| `shift_onto_screen` | | | +| `stretch` | | | +| `stretch_about_point` | | | +| `stretch_to_fit` | (new) | | +| `stretch_to_fit_depth` | | | +| `stretch_to_fit_height` | | | +| `stretch_to_fit_width` | | | +| `to_corner` | | | +| `to_edge` | | | +| `width` | | | + +## Hierarchy + +## Testing Tries to ensure the same behavior for mobjects with at least 1 point by randomized testing. -Run `python test.py` to run the randomized tests. +Run`python test.py` to run the randomized tests. + +### Pseudo Code -## Pseudo Code ```py function = lambda mob, kwargs: mob.some_function(**kwargs) # function that you want to test @@ -45,4 +109,4 @@ for point_count in (1, 100): # tests different po result_old = function(mob_old, kwargs) # apply old implementation result_new = function(mob_new, kwargs) # apply new implementation validate(mob_old, mob_new, result_old, result_new) # compare results -``` \ No newline at end of file +``` diff --git a/manim/mobject/abstract/positionable.py b/manim/mobject/abstract/positionable.py index 6c848826dc..48dd4739b7 100644 --- a/manim/mobject/abstract/positionable.py +++ b/manim/mobject/abstract/positionable.py @@ -216,6 +216,7 @@ def get_coord(self, dim: int, direction: Vector3DLike = ORIGIN) -> float: # TODO: Optimize by only calculating dim return self.get_critical_point(direction=direction)[dim] + @deprecated(replacement="get_critical_point") def get_corner(self, direction: Vector3DLike) -> Point3D: return self.get_critical_point(direction=direction) @@ -231,9 +232,11 @@ def get_dim_size(self, dim: int) -> float: values = self.points[:, dim] return values.max() - values.min() + @deprecated(replacement="get_critical_point") def get_edge_center(self, direction: Vector3DLike) -> Point3D: return self.get_critical_point(direction=direction) + @deprecated() def get_extremum_along_dim( self, dim: int = 0, diff --git a/manim/mobject/abstract/test.py b/manim/mobject/abstract/test.py index 38c6c00171..ff822dc0ef 100644 --- a/manim/mobject/abstract/test.py +++ b/manim/mobject/abstract/test.py @@ -441,7 +441,7 @@ def validate_function( name: str, function: Callable[[Mobject | Positionable, dict[Any, Any]], Any], validate: Callable[[Mobject, Positionable, Any, Any], None], - generate_random_parameters: Callable[[], dict[Any, Any]], + random_parameters: Callable[[], dict[Any, Any]], ) -> None: global POINT_COUNTS, LOOPS_PER_POINT_COUNT time_old, time_new = 0, 0 @@ -455,7 +455,7 @@ def validate_function( mob_new = Positionable() mob_new.points = points.copy() - kwargs = generate_random_parameters() + kwargs = random_parameters() kwargs = {key: value for key, value in kwargs.items() if value is not None} start = time.perf_counter_ns() @@ -481,7 +481,9 @@ def validate_function( """.replace(" ", "") ) from e - print(f"\t{name.ljust(25)}\t{time_old / time_new:1.2f}x\t{time_old / 1e9:.0f}s") + print( + f"\t{name.ljust(25)}\t{time_old / time_new:1.2f}x\t{time_old / 1e9:.2f}s\t{time_new / 1e9:.2f}s" + ) if name in UNTESTED: UNTESTED.remove(name) @@ -504,7 +506,7 @@ def validate( name=name, function=function, validate=validate, - generate_random_parameters=create_kwargs, + random_parameters=create_kwargs, ) @@ -525,7 +527,7 @@ def validate( name=name, function=function, validate=validate, - generate_random_parameters=create_kwargs, + random_parameters=create_kwargs, ) diff --git a/manim/mobject/abstract/test.txt b/manim/mobject/abstract/test.txt new file mode 100644 index 0000000000000000000000000000000000000000..d592debca1aeb8025023088f5d5ef4351cdd83c5 GIT binary patch literal 5930 zcmcIo(NY>g5ZqT%m4C<=Slj_BAChOv03ub26kwu$e3I^+%+2iGEw@;vtl*x}-oBoh zp4t8Ldlc@%W7vjG*o8%Sj-Nm9{Sa1Rjn8jAU!zckt1!doYZ!$w)-~40f9ontvHlvq z;MrrJfZB(a{!H+0fT-Alr~8Z>>?Qc^!YRu##pTlos8`^7+hC`>4V&-22lhh&W7$Xe zO~OrsJ%&Q2N!{@N2sZT|CwkvD*friq^}dg>+!wQ^Cd8hAtLk0j$#~y1*c)haLXI#y zPWXBbPXqf1_8iO=+sBe|P~IN<1yNlga}MDRyX~>hHpV%Pr(oB}HN_qyy2@MDl7os} zo-dLob&`YBL1Ns;+hene?!o*C?{LIdoA(rY&%(UnUAa6NNA26ZpMl7FOp&@z!fy%; z(}cCCyysYVu@6u;SLwEg= z8tR?EeT@jd;NK#$r!#J_Ug3Q-?*+R`YTjz6FP4u$y$8DKnp+9>Z8lH3dhfuK?D{?1 z@1xk}gF|)6NN(VU!^{PH?lejByUUwSQs!8fW4{(CEy_rZ%v^66$3CXD)8+m4AFc^XDQjU!dt1{DK?#ys&4~xGSxz# z6x(>$5$`;9S*J%-&sn9GwgFYLp%n$KaEdEq74Qi}`dTyi4O8TmRh4_2<`IZ1JPxDi&OZygukOpSg(oY% z>=r_MgYTE=JZLkrVCzkRk6CW5yshSQ*nMxELzSOvo>))wwd2n_AHin5%-vm>oN?3# zRg>@Ng}g6iwon(z%R8LD_vrh)ZDS;!;uGf>P5``(TjKY(_@7 Date: Wed, 19 Aug 2026 18:54:55 +0200 Subject: [PATCH 09/66] Update --- manim/mobject/abstract/positionable.md | 161 +++++++++++++------------ manim/mobject/abstract/positionable.py | 9 ++ 2 files changed, 90 insertions(+), 80 deletions(-) diff --git a/manim/mobject/abstract/positionable.md b/manim/mobject/abstract/positionable.md index 4ac2e54d27..56841d9c95 100644 --- a/manim/mobject/abstract/positionable.md +++ b/manim/mobject/abstract/positionable.md @@ -7,86 +7,87 @@ ## Changes -| Attribute | Description | Speed | -| :---------------------------------: | :--------------------------------------------------------------: | :---: | -| `align_on_border` | made `buff` keyword-only
added `frame` parameter | 1.51x | -| `align_to` | made `direction` keyword-only | 0.72x | -| `apply_array_function` | new
renamed from `apply_points_function_about_point` | - | -| `apply_complex_function` | - | - | -| `apply_function` | - | - | -| `apply_function_to_position` | deprecated
use `move_to(function(self.get_center()))` instead | - | -| `apply_matrix` | - | - | -| `apply_points_function_about_point` | deprecated - use `apply_array_function` instead | - | -| `center` | - | 2.08x | -| `depth` | deprecated - use `get_depth` instead | 0.37x | -| `depth` | deprecated - use `set_depth` instead | 1.01x | -| `flip` | - | 1.35x | -| `get_bottom` | - | | -| `get_boundary_point` | - | | -| `get_bounding_box` | new | - | -| `get_center` | - | | -| `get_center_of_mass` | - | | -| `get_coord` | - | | -| `get_corner` | deprecated - use `get_critical_point` instead | | -| `get_critical_point` | - | | -| `get_depth` | new - replacement for `depth` | | -| `get_dim_size` | new | | -| `get_edge_center` | deprecated - use `get_critical_point` instead | | -| `get_extremum_along_dim` | deprecated | | -| `get_height` | new - replacement for `height` | | -| `get_left` | - | | -| `get_nadir` | - | | -| `get_right` | - | | -| `get_top` | - | | -| `get_width` | new - replacement for `width` | | -| `get_x` | - | | -| `get_y` | - | | -| `get_z` | - | | -| `get_zenith` | - | | -| `height` | deprecated - use `get_height` instead | | -| `height` | deprecated - use `set_height` instead | | -| `is_off_screen` | - | | -| `length_over_dim` | deprecated - use `get_dim_size` instead | | -| `match_coord` | made `direction` keyword-only | | -| `match_depth` | made kwargs explicit | | -| `match_dim_size` | made kwargs explicit | | -| `match_height` | made kwargs explicit | | -| `match_points` | removed `copy_submobjects` parameter | | -| `match_width` | | | -| `match_x` | | | -| `match_y` | | | -| `match_z` | | | -| `move_to` | | | -| `next_to` | | | -| `pose_at_angle` | | | -| `reduce_across_dimension` | | | -| `rescale_to_fit` | | | -| `rotate` | | | -| `rotate_about_origin` | | | -| `scale` | | | -| `scale_to_fit` | (new) | | -| `scale_to_fit_depth` | | | -| `scale_to_fit_height` | | | -| `scale_to_fit_width` | | | -| `set_coord` | | | -| `set_depth` | | | -| `set_dim_size` | (new) | | -| `set_height` | | | -| `set_width` | | | -| `set_x` | | | -| `set_y` | | | -| `set_z` | | | -| `shift` | | | -| `shift_onto_screen` | | | -| `stretch` | | | -| `stretch_about_point` | | | -| `stretch_to_fit` | (new) | | -| `stretch_to_fit_depth` | | | -| `stretch_to_fit_height` | | | -| `stretch_to_fit_width` | | | -| `to_corner` | | | -| `to_edge` | | | -| `width` | | | +| Attribute | Description | +|:-----------------------------------:|:----------------------------------------------------------------------------------------------------------:| +| `align_on_border` | made `buff` keyword-only
added `frame` parameter | +| `align_to` | made `direction` keyword-only | +| `apply_array_function` | new
renamed from `apply_points_function_about_point` | +| `apply_complex_function` | - | +| `apply_function` | - | +| `apply_function_to_position` | deprecated
use `move_to(function(self.get_center()))` instead | +| `apply_matrix` | - | +| `apply_points_function_about_point` | deprecated - use `apply_array_function` instead | +| `center` | - | +| `depth` | deprecated - use `get_depth` instead | +| `depth` | deprecated - use `set_depth` instead | +| `flip` | - | +| `get_bottom` | - | +| `get_boundary_point` | - | +| `get_bounding_box` | new | +| `get_center` | - | +| `get_center_of_mass` | - | +| `get_coord` | - | +| `get_corner` | deprecated - use `get_critical_point` instead | +| `get_critical_point` | - | +| `get_depth` | new - replacement for `depth` | +| `get_dim_size` | new | +| `get_edge_center` | deprecated - use `get_critical_point` instead | +| `get_extremum_along_dim` | deprecated | +| `get_height` | new - replacement for `height` | +| `get_left` | - | +| `get_nadir` | - | +| `get_right` | - | +| `get_top` | - | +| `get_width` | new - replacement for `width` | +| `get_x` | - | +| `get_y` | - | +| `get_z` | - | +| `get_zenith` | - | +| `height` | deprecated - use `get_height` instead | +| `height` | deprecated - use `set_height` instead | +| `is_off_screen` | - | +| `length_over_dim` | deprecated - use `get_dim_size` instead | +| `match_coord` | made `direction` keyword-only | +| `match_depth` | made kwargs explicit | +| `match_dim_size` | made kwargs explicit | +| `match_height` | made kwargs explicit | +| `match_points` | removed `copy_submobjects` parameter | +| `match_width` | made kwargs explicit | +| `match_x` | made `direction` keyword-only | +| `match_y` | made `direction` keyword-only | +| `match_z` | made `direction` keyword-only | +| `move_to` | made `aligned_edge` and `coor_mask` keyword-only | +| `next_to` | implementation without submobject logic
made `direction`,`buff`,`aligned_edge`,`coor_mask` keyword-only | +| `pose_at_angle` | deprecated
made kwargs explicit | +| `reduce_across_dimension` | - | +| `rescale_to_fit` | made `stretch` keyword-only
made kwargs explicit | +| `rotate` | removed unused kwargs parameter | +| `rotate_about_origin` | deprecated - use `rotate` instead | +| `scale` | supports scaling by a 3D vector | +| `scale_to_fit` | new | +| `scale_to_fit_depth` | made kwargs explicit | +| `scale_to_fit_height` | made kwargs explicit | +| `scale_to_fit_width` | made kwargs explicit | +| `set_coord` | made `direction` explicit | +| `set_depth` | new - replacement for `depth` | +| `set_dim_size` | new - replacement for `rescale_to_fit` | +| `set_height` | new - replacement for `height` | +| `set_width` | new - replacement for `width` | +| `set_x` | made `direction` keyword-only | +| `set_y` | made `direction` keyword-only | +| `set_z` | made `direction` keyword-only | +| `shift` | changed vararg `*vectors` to `vector` | +| `shift_onto_screen` | made kwargs explicit | +| `stretch` | - | +| `stretch_about_point` | deprecated - use `stretch` instead | +| `stretch_to_fit` | new | +| `stretch_to_fit_depth` | made kwargs explicit | +| `stretch_to_fit_height` | made kwargs explicit | +| `stretch_to_fit_width` | made kwargs explicit | +| `to_corner` | made `buff` keyword-only | +| `to_edge` | made `buff` keyword-only | +| `width` | deprecated - use `get_width` instead | +| `width` | deprecated - use `set_width` instead | ## Hierarchy diff --git a/manim/mobject/abstract/positionable.py b/manim/mobject/abstract/positionable.py index 48dd4739b7..41d4d7012c 100644 --- a/manim/mobject/abstract/positionable.py +++ b/manim/mobject/abstract/positionable.py @@ -388,6 +388,7 @@ def match_width( def match_x( self, mobject: "Positionable", + *, direction: Vector3DLike = ORIGIN, ) -> Self: return self.set_x( @@ -398,6 +399,7 @@ def match_x( def match_y( self, mobject: "Positionable", + *, direction: Vector3DLike = ORIGIN, ) -> Self: return self.set_y( @@ -408,6 +410,7 @@ def match_y( def match_z( self, mobject: "Positionable", + *, direction: Vector3DLike = ORIGIN, ) -> Self: return self.set_z( @@ -449,6 +452,7 @@ def next_to( ) return self.shift((target - source + buff * np_direction) * coor_mask) + @deprecated() def pose_at_angle( self, *, @@ -682,6 +686,7 @@ def set_width( def set_x( self, x: float, + *, direction: Vector3DLike = ORIGIN, ) -> Self: return self.set_coord(value=x, dim=0, direction=direction) @@ -689,6 +694,7 @@ def set_x( def set_y( self, y: float, + *, direction: Vector3DLike = ORIGIN, ) -> Self: return self.set_coord(value=y, dim=1, direction=direction) @@ -696,6 +702,7 @@ def set_y( def set_z( self, z: float, + *, direction: Vector3DLike = ORIGIN, ) -> Self: return self.set_coord(value=z, dim=2, direction=direction) @@ -809,6 +816,7 @@ def stretch_to_fit_width( def to_corner( self, corner: Vector3DLike = DL, + *, buff: float = DEFAULT_MOBJECT_TO_EDGE_BUFFER, ) -> Self: return self.align_on_border(direction=corner, buff=buff) @@ -816,6 +824,7 @@ def to_corner( def to_edge( self, edge: Vector3DLike = LEFT, + *, buff: float = DEFAULT_MOBJECT_TO_EDGE_BUFFER, ) -> Self: return self.align_on_border(direction=edge, buff=buff) From a2602918cd986923c2587a36ea6543f8442cfa53 Mon Sep 17 00:00:00 2001 From: GniLudio <50866361+GniLudio@users.noreply.github.com> Date: Wed, 19 Aug 2026 23:46:18 +0200 Subject: [PATCH 10/66] Update --- manim/mobject/abstract/attributes.py | 34 -------- manim/mobject/abstract/attributes.txt | Bin 18038 -> 0 bytes manim/mobject/abstract/positionable.md | 113 ------------------------- manim/mobject/abstract/positionable.py | 104 ++++++++++------------- manim/mobject/abstract/test.py | 45 ++++++++-- manim/mobject/abstract/test.txt | Bin 5930 -> 0 bytes 6 files changed, 83 insertions(+), 213 deletions(-) delete mode 100644 manim/mobject/abstract/attributes.py delete mode 100644 manim/mobject/abstract/attributes.txt delete mode 100644 manim/mobject/abstract/positionable.md delete mode 100644 manim/mobject/abstract/test.txt diff --git a/manim/mobject/abstract/attributes.py b/manim/mobject/abstract/attributes.py deleted file mode 100644 index e966fb173d..0000000000 --- a/manim/mobject/abstract/attributes.py +++ /dev/null @@ -1,34 +0,0 @@ -from manim.mobject.abstract.positionable import Positionable -from manim.mobject.mobject import Mobject -from manim.mobject.opengl.opengl_mobject import OpenGLMobject -from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject -from manim.mobject.types.vectorized_mobject import VMobject - - -def main() -> None: - seen: set[str] = set() - - for cls in [Mobject, VMobject, OpenGLMobject, OpenGLVMobject]: - assert isinstance(cls, type) - print(cls.__name__) - for name, attr in sorted(cls.__dict__.items()): - if ( - name in seen - or name.startswith("__") - or attr is getattr(cls.__base__, name, None) - ): - continue - print( - f"\t{'-+'[getattr(Positionable, name, None) is not getattr(Positionable.__base__, name, None)]} {name}" - ) - seen |= cls.__dict__.keys() - - print(Positionable.__name__) - for name, attr in Positionable.__dict__.items(): - if name.startswith("__") or attr is getattr(Positionable.__base__, name, None): - continue - print(f"\t* {name}", "(new)" if name not in seen else "") - - -if __name__ == "__main__": - main() diff --git a/manim/mobject/abstract/attributes.txt b/manim/mobject/abstract/attributes.txt deleted file mode 100644 index 54d3bbe255316e9d3dc98d710d937efb09a29372..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18038 zcmbuG%WfUX4TgK00C|U5=7MZ8uMi-c3^J>14{XVjJfmB5@$KQ0@ax|M$!b>ju?K?e zqb?SU#mi3?yUzdq`_uBUyj^}--Ypl)%kqci_sj2=zb-#4r{&FZX+Njs{c>+3PwW5B z_V2|yc5k2VjONAue=v%Z{rkbVJgtq@aN?P~m+-5&2~JL|2@yI6i*u5G0U+sFO7qo=Rhgy*&G^Ig(}YxUfjC)3x1 zY3a&Vx-*U4E}!kMJL7q0T4RUL_V-5{d0B4k)1y(#Vp)gZyXui`oG}D~=k;-nZ2v9Pr>*Bp>nB*!hJ$C8N}cxma-$U7c*FIqow@xIJ1tgl7@ekH+mL$q82SJa+C+p&z22`o6KZ99<$Y}FURFQhdCtQ68xOd(pFM8)MtB;Z<$t~^@;W!X ziBY0AG?_=0$>2w}3IE(uZ>^bE%A-^WSH_Jfq86xHLw>A#Yrl?9mD9sI;{G^}k?F&u z>L`8;URn_Mpy}nh!F>7mAs+KF;$yOR_4`n>6xA3`+68spwC-1vfeMIMHqnGxp z+}WbkbZFB3By+s~s`Xg4^R!2s4K~#()%6#1Rn>I$@s^f44Ai+Y-sDtST*!^DD4MpK z#yhVKzdBt%+f(N4k6{H>p4lt2R`7o8b}~%H8}Ra(CuL285hK14sXapHnVuo@vy9uW zzb}o4eX$kLAKIj@Aeyf1m;diqtL?||mz7Qxad_p;y7QB9pq>sJXa8`T{n2x@8Fs!g zX|@)Yk5tslk}&S)rPu>cl{t}+2;r}z@h!6A8}QPJ&Akg-y|-0`)L`|qX&bGh_t+8G zfOqS}5jD?o^{c#P1jEAJS&3tu01Az2!5Hx$kEx zPGpy>hI&bIeB^mKM)Gry6Ui%`=W2%MWI(b{evYN_c*dyJLD=IH8Ynte9Li)mcZJ7= zzNlO|1!INB^%G-Y*LWJ>Ilc`?r0veh9f!JtpL4S;uf~^CFx+oV9`6JY@kiC_qwN`K z0&_=PB`$It5E+*?i#!4?vBJ)j352*rao#`nU>=!|=nLud(-C1S6&VuE^-ueqjRlw2<{O@;L!CzCtQ+`Y*&%A)UX-< z#-~^$&P_;z@3LCR^lUV&g`W^@Q43+G7?n>1*0NUUDs&jns{T|N5JIr|Rn#)-4&m8Bo25LQ&y;s_+<8A|<{%l%R&G6&$_pg30z2DgL zsPF4D;hf?hpCgW=0+3`k#`$B3q#Lu)mHpIyCwLssdu+<+@x1y$wX?FwIddP)SB1vj zFX=2=*P~|?7$;5W1ovXRysxl*UhySRDx*Q>GsbvV(SS2(TPu$fYTO~Ty z*`vsyZ`58Rq(sO1wh6`CbEDM=E z`><-&!@rlGZ2j(B2U@Xv_clC=y@DdSzVW!qzC_%N5~>2hzfSfOJMtd*g+<3(+ZAtB z=$C!5&*&2b!%DSA_?B7np6^>R@5AZR8Sqi;n+!)^;b$Z;VuWty!40TI7PDg%;bdEY16*^w~(cn7o zsP?(h1q1`gW8|mvsXHHy^~0;T79Oj0GO~9=!*5+PJs$gx)k{W`LAPCz^Uat;Mql{8 zSu=k)j~l4t9`x-q*z;buyW4eWsk5~A*dqoyDk^`S8}#0Tx^{dwoRKTj^Txw7=XS@V zAMD|sD(VNjqd`{fa)kTHjI!SwHMes;Cps$kJfn+{O1&2)m9kd3y2+FG_j$$dPNURB zVb2+bXEI{Z&rPauK{4{|JjWgJ?Dd>$wR3oT2cJrnvq_fN(StfHq@l{;^sSpoRQ{od zEYm)ns?Cfq(qHV;^_tQ9@m%r7K61DC{5+gzA?xTN=p&;6 zf6dP)L5U}#Rn_yvo%(**Lp@dJE=g554#tdbhmu?d<3@o_n{mXPD$O!?rmpx>0t? zUa@+2`s?RpujYQyRzj@EX%Vj?8c)lQC6b+QSi9$OPF4zDf%F}>ZN$XtRG(cQlg~Kb zRGi9d#L9i<%AJnA6T-jNn7v>AvHWw|_aGYeZU?{S)bYmj>)x7gAcTZ@&Ws{%CPlxC zXd+rNGUnS`+0)P=w^6#vLH8PT6OtN9`u6)gA!odUj}z@fxx+;TL3Q%0@yNS(oq%1n z`z=J%J0qc--~XJg!T#k9@2#uj=em8ZFv|TVR*QR2R872hQU)gK*evq9L@XQ3G^!(LzQGkptk zPH&AZuPkfWV!ICac=c|t8>8#zxjJ*)c5HLSJ40OGTh58(jKDuSE7?!y{eXVXb`O<~ zh@+E>&F^g8BYI_J&R5tWYQdvddVk zNQ5Pz2Ls7CobG4X5ORz&{7g};p*joH1as!Ivs5ATx%8vY2hRB3$O7aQvQS+MY{X0J zT9uyj)vP1kojbiQf;W~2iS*8QyV{)glht>;pY+@w68fAPHF!geY;XMf*oe}3UXR5- z)AvfvvqjTADs0MKM0Gc|J8_!?7GXK(*duM0yF)AIsgoG@jC5}@ukN)n76Qp{S6|2| z>s&_3wmy;l8;QMsh&N(gm60j?>LKKXb`MGT9)wUQSF`i?c>dNZpDJ{+_qT0v$`^iw zEbLD26gthusQXMe({(Eg_4@a0g~x$ud@o8~gtmgK)7AHb`HjT4&?-mxJ+1CNrmq(< zp6&0Wp@+KH8oBuT(Tww}huNAHCAZp$d40T9DRr+>`0ic4Ezg{tqQC3+4&nw~_*th# zeal+C4OX?d=I8;@+7qOKD8kcj-0ds5Y%{tO&oqMd)N*{>UX@tGJx1CdEa0{_J zm$*l1Wi-ScM{3pDRr1X3_7C0pNR}){qe46KZB|s*PA6FG@X6#?y+ehl`j(R#mT~`n zvg%NHt~(Ohla6$f>p0NR(Z0$&i-9`3?%{~T&*lSkIsK%~4Mk#KJN~m_Wk!`B((~MF z;l!&tmiy9Vm>DH^59(O-W1`N9I?UzzVfmx2L1*euMTVG7ZrJzI*LD?%4RfK*IOM>3F@ps)C6tjmU|!hIabka&-~QCFP5J{k+8rpNcp5+Ou7BMF1wB768rR#Ma zqhK+pq3;Fsp6%3}r1@OCTAh^Ve9xBxRc3KAEE0D34S-<&kPDsuMIQl+Ai33&eLy@(8k1U4WAy zq$)P-lBuXy&i{_YI5WSG(;ZNny780^}L7TL08@kjv2J$ly__ z@ysqK%;{(TGoR6WidTTW@B8RXI^8Ox0y{=g=QWpH-1gz>% diff --git a/manim/mobject/abstract/positionable.md b/manim/mobject/abstract/positionable.md deleted file mode 100644 index 56841d9c95..0000000000 --- a/manim/mobject/abstract/positionable.md +++ /dev/null @@ -1,113 +0,0 @@ -# Positionable - -## TODO - -- Handling for 0 points? -- Documentation - -## Changes - -| Attribute | Description | -|:-----------------------------------:|:----------------------------------------------------------------------------------------------------------:| -| `align_on_border` | made `buff` keyword-only
added `frame` parameter | -| `align_to` | made `direction` keyword-only | -| `apply_array_function` | new
renamed from `apply_points_function_about_point` | -| `apply_complex_function` | - | -| `apply_function` | - | -| `apply_function_to_position` | deprecated
use `move_to(function(self.get_center()))` instead | -| `apply_matrix` | - | -| `apply_points_function_about_point` | deprecated - use `apply_array_function` instead | -| `center` | - | -| `depth` | deprecated - use `get_depth` instead | -| `depth` | deprecated - use `set_depth` instead | -| `flip` | - | -| `get_bottom` | - | -| `get_boundary_point` | - | -| `get_bounding_box` | new | -| `get_center` | - | -| `get_center_of_mass` | - | -| `get_coord` | - | -| `get_corner` | deprecated - use `get_critical_point` instead | -| `get_critical_point` | - | -| `get_depth` | new - replacement for `depth` | -| `get_dim_size` | new | -| `get_edge_center` | deprecated - use `get_critical_point` instead | -| `get_extremum_along_dim` | deprecated | -| `get_height` | new - replacement for `height` | -| `get_left` | - | -| `get_nadir` | - | -| `get_right` | - | -| `get_top` | - | -| `get_width` | new - replacement for `width` | -| `get_x` | - | -| `get_y` | - | -| `get_z` | - | -| `get_zenith` | - | -| `height` | deprecated - use `get_height` instead | -| `height` | deprecated - use `set_height` instead | -| `is_off_screen` | - | -| `length_over_dim` | deprecated - use `get_dim_size` instead | -| `match_coord` | made `direction` keyword-only | -| `match_depth` | made kwargs explicit | -| `match_dim_size` | made kwargs explicit | -| `match_height` | made kwargs explicit | -| `match_points` | removed `copy_submobjects` parameter | -| `match_width` | made kwargs explicit | -| `match_x` | made `direction` keyword-only | -| `match_y` | made `direction` keyword-only | -| `match_z` | made `direction` keyword-only | -| `move_to` | made `aligned_edge` and `coor_mask` keyword-only | -| `next_to` | implementation without submobject logic
made `direction`,`buff`,`aligned_edge`,`coor_mask` keyword-only | -| `pose_at_angle` | deprecated
made kwargs explicit | -| `reduce_across_dimension` | - | -| `rescale_to_fit` | made `stretch` keyword-only
made kwargs explicit | -| `rotate` | removed unused kwargs parameter | -| `rotate_about_origin` | deprecated - use `rotate` instead | -| `scale` | supports scaling by a 3D vector | -| `scale_to_fit` | new | -| `scale_to_fit_depth` | made kwargs explicit | -| `scale_to_fit_height` | made kwargs explicit | -| `scale_to_fit_width` | made kwargs explicit | -| `set_coord` | made `direction` explicit | -| `set_depth` | new - replacement for `depth` | -| `set_dim_size` | new - replacement for `rescale_to_fit` | -| `set_height` | new - replacement for `height` | -| `set_width` | new - replacement for `width` | -| `set_x` | made `direction` keyword-only | -| `set_y` | made `direction` keyword-only | -| `set_z` | made `direction` keyword-only | -| `shift` | changed vararg `*vectors` to `vector` | -| `shift_onto_screen` | made kwargs explicit | -| `stretch` | - | -| `stretch_about_point` | deprecated - use `stretch` instead | -| `stretch_to_fit` | new | -| `stretch_to_fit_depth` | made kwargs explicit | -| `stretch_to_fit_height` | made kwargs explicit | -| `stretch_to_fit_width` | made kwargs explicit | -| `to_corner` | made `buff` keyword-only | -| `to_edge` | made `buff` keyword-only | -| `width` | deprecated - use `get_width` instead | -| `width` | deprecated - use `set_width` instead | - -## Hierarchy - -## Testing - -Tries to ensure the same behavior for mobjects with at least 1 point by randomized testing. -Run`python test.py` to run the randomized tests. - -### Pseudo Code - -```py -function = lambda mob, kwargs: mob.some_function(**kwargs) # function that you want to test - -for point_count in (1, 100): # tests different point counts - for _ in range(100): # test every point count many times - points = random_points(point_count) # generate points - mob_old = Mobject().set_points(points) # create old implementation - mob_new = Positionable().set_points(points) # create new implementation - kwargs = random_parameters() # randomize parameters - result_old = function(mob_old, kwargs) # apply old implementation - result_new = function(mob_new, kwargs) # apply new implementation - validate(mob_old, mob_new, result_old, result_new) # compare results -``` diff --git a/manim/mobject/abstract/positionable.py b/manim/mobject/abstract/positionable.py index 41d4d7012c..388dcb4866 100644 --- a/manim/mobject/abstract/positionable.py +++ b/manim/mobject/abstract/positionable.py @@ -24,23 +24,19 @@ Point3DLike, Vector3DLike, ) -from manim.utils.deprecation import deprecated + +# from manim.utils.deprecation import deprecated from manim.utils.space_ops import rotation_matrix class Positionable: - # FUNDAMENTALS points: Point3D_Array = np.array([(0.0, 0.0, 0.0)]) - # METHODS - - # TODO: Keep/Remove frame parameter? - # TODO: Should the default of the frame parameter be handled inside the method to allow config changes? def align_on_border( self, direction: Vector3DLike, - *, buff: float = DEFAULT_MOBJECT_TO_EDGE_BUFFER, + *, frame: Point3DLike | None = None, ) -> Self: if frame is None: @@ -54,7 +50,6 @@ def align_on_border( def align_to( self, mobject_or_point: "Positionable | Point3DLike", - *, direction: Vector3DLike = ORIGIN, ) -> Self: source = self.get_critical_point(direction=direction) @@ -63,10 +58,8 @@ def align_to( if isinstance(mobject_or_point, Positionable) else mobject_or_point ) - for i, v in enumerate(np.sign(direction)): - if v == 0: - target[i] = source[i] - return self.move_to(point_or_mobject=target, aligned_edge=direction) + target = np.where(direction == 0, source, target) + return self.shift(target - source) def apply_array_function( self, @@ -121,7 +114,7 @@ def mapping_function(points: Point3D_Array) -> Point3D_Array: about_edge=about_edge, ) - @deprecated(replacement="move_to(function(self.get_center()))") + # @deprecated(replacement="move_to(function(self.get_center()))") def apply_function_to_position( self, function: Callable[[Point3D], Point3D], @@ -153,7 +146,7 @@ def apply_matrix( about_edge=about_edge, ) - @deprecated(replacement="apply_array_function") + # @deprecated(replacement="apply_array_function") def apply_points_function_about_point( self, func: Callable[[Point3D_Array], Point3D_Array], @@ -170,12 +163,12 @@ def center(self) -> Self: return self.move_to(point_or_mobject=ORIGIN) @property - @deprecated(replacement="get_depth") + # @deprecated(replacement="get_depth") def depth(self) -> float: return self.get_depth() @depth.setter - @deprecated(replacement="set_depth") + # @deprecated(replacement="set_depth") def depth(self, value: float) -> Self: return self.set_depth(depth=value, stretch=False) @@ -213,10 +206,16 @@ def get_center_of_mass(self) -> Point3D: return self.points.mean(axis=0) def get_coord(self, dim: int, direction: Vector3DLike = ORIGIN) -> float: - # TODO: Optimize by only calculating dim - return self.get_critical_point(direction=direction)[dim] + key = np.sign(direction[dim]) + return ( + self.points[:, dim].min() + if key == -1 + else (self.points[:, dim].min() + self.points[:, dim].max()) / 2 + if key == 0 + else self.points[:, dim].max() + ) - @deprecated(replacement="get_critical_point") + # @deprecated(replacement="get_critical_point") def get_corner(self, direction: Vector3DLike) -> Point3D: return self.get_critical_point(direction=direction) @@ -232,11 +231,11 @@ def get_dim_size(self, dim: int) -> float: values = self.points[:, dim] return values.max() - values.min() - @deprecated(replacement="get_critical_point") + # @deprecated(replacement="get_critical_point") def get_edge_center(self, direction: Vector3DLike) -> Point3D: return self.get_critical_point(direction=direction) - @deprecated() + # @deprecated() def get_extremum_along_dim( self, dim: int = 0, @@ -284,25 +283,25 @@ def get_zenith(self) -> Point3D: return self.get_critical_point(direction=OUT) @property - @deprecated(replacement="get_height") + # @deprecated(replacement="get_height") def height(self) -> float: return self.get_height() @height.setter - @deprecated(replacement="set_height") + # @deprecated(replacement="set_height") def height(self, value: float) -> Self: return self.set_height(height=value, stretch=False) def is_off_screen(self) -> bool: - # TODO: Optimize using the bounding box + mins, _, maxs = self.get_bounding_box() return ( - self.get_left()[0] > config["frame_x_radius"] - or self.get_right()[0] < -config["frame_x_radius"] - or self.get_bottom()[1] > config["frame_y_radius"] - or self.get_top()[1] < -config["frame_y_radius"] + mins[0] > config.frame_x_radius + or maxs[0] < -config.frame_x_radius + or mins[1] > config.frame_y_radius + or maxs[1] < -config.frame_y_radius ) - @deprecated(replacement="get_dim_size") + # @deprecated(replacement="get_dim_size") def length_over_dim(self, dim: int) -> float: return self.get_dim_size(dim=dim) @@ -310,7 +309,6 @@ def match_coord( self, mobject: "Positionable", dim: int, - *, direction: Vector3DLike = ORIGIN, ) -> Self: return self.set_coord( @@ -388,7 +386,6 @@ def match_width( def match_x( self, mobject: "Positionable", - *, direction: Vector3DLike = ORIGIN, ) -> Self: return self.set_x( @@ -399,7 +396,6 @@ def match_x( def match_y( self, mobject: "Positionable", - *, direction: Vector3DLike = ORIGIN, ) -> Self: return self.set_y( @@ -410,7 +406,6 @@ def match_y( def match_z( self, mobject: "Positionable", - *, direction: Vector3DLike = ORIGIN, ) -> Self: return self.set_z( @@ -421,22 +416,22 @@ def match_z( def move_to( self, point_or_mobject: "Point3DLike | Positionable", - *, aligned_edge: Vector3DLike = ORIGIN, coor_mask: Vector3DLike = np.array([1, 1, 1]), ) -> Self: + if isinstance(point_or_mobject, Positionable): + return self.move_to( + point_or_mobject.get_critical_point(direction=aligned_edge), + aligned_edge=aligned_edge, + coor_mask=coor_mask, + ) source = self.get_critical_point(direction=aligned_edge) - target = ( - point_or_mobject.get_critical_point(direction=aligned_edge) - if isinstance(point_or_mobject, Positionable) - else point_or_mobject - ) + target = point_or_mobject return self.shift(vector=(target - source) * coor_mask) def next_to( self, mobject_or_point: "Positionable | Point3DLike", - *, direction: Vector3DLike = RIGHT, buff: float = DEFAULT_MOBJECT_TO_MOBJECT_BUFFER, aligned_edge: Vector3DLike = ORIGIN, @@ -452,7 +447,7 @@ def next_to( ) return self.shift((target - source + buff * np_direction) * coor_mask) - @deprecated() + # @deprecated() def pose_at_angle( self, *, @@ -466,7 +461,7 @@ def pose_at_angle( about_edge=about_edge, ) - @deprecated() + # @deprecated() def reduce_across_dimension( self, reduce_func: Callable[[Iterable[float]], float], @@ -477,13 +472,13 @@ def reduce_across_dimension( return reduce_func(self.points[:, dim]) - @deprecated(replacement="set_dim_size") + # @deprecated(replacement="set_dim_size") def rescale_to_fit( self, length: float, dim: int, - *, stretch: bool = False, + *, about_point: Point3DLike | None = None, about_edge: Vector3DLike | None = None, ) -> Self: @@ -511,7 +506,7 @@ def rotate( about_edge=about_edge, ) - @deprecated(replacement="rotate") + # @deprecated(replacement="rotate") def rotate_about_origin( self, angle: float, @@ -600,13 +595,11 @@ def set_coord( self, value: float, dim: int, - *, direction: Vector3DLike = ORIGIN, ) -> Self: - # TODO: Optimize by only calculating dim and using shift - target = self.get_critical_point(direction=direction) - target[dim] = value - return self.move_to(point_or_mobject=target, aligned_edge=direction) + source = self.get_coord(dim, direction=direction) + self.points[:, dim] += value - source + return self def set_depth( self, @@ -686,7 +679,6 @@ def set_width( def set_x( self, x: float, - *, direction: Vector3DLike = ORIGIN, ) -> Self: return self.set_coord(value=x, dim=0, direction=direction) @@ -694,7 +686,6 @@ def set_x( def set_y( self, y: float, - *, direction: Vector3DLike = ORIGIN, ) -> Self: return self.set_coord(value=y, dim=1, direction=direction) @@ -702,7 +693,6 @@ def set_y( def set_z( self, z: float, - *, direction: Vector3DLike = ORIGIN, ) -> Self: return self.set_coord(value=z, dim=2, direction=direction) @@ -720,7 +710,7 @@ def shift_onto_screen( for edge in UP, DOWN, LEFT, RIGHT: dim = np.argmax(np.abs(edge)) max_val = space_lengths[dim] - buff - edge_center = self.get_edge_center(direction=edge) + edge_center = self.get_critical_point(direction=edge) if np.dot(edge_center, edge) > max_val: self.to_edge(edge=edge, buff=buff) return self @@ -739,7 +729,7 @@ def stretch( about_edge=about_edge, ) - @deprecated(replacement="stretch") + # @deprecated(replacement="stretch") def stretch_about_point( self, factor: float, @@ -816,7 +806,6 @@ def stretch_to_fit_width( def to_corner( self, corner: Vector3DLike = DL, - *, buff: float = DEFAULT_MOBJECT_TO_EDGE_BUFFER, ) -> Self: return self.align_on_border(direction=corner, buff=buff) @@ -824,13 +813,12 @@ def to_corner( def to_edge( self, edge: Vector3DLike = LEFT, - *, buff: float = DEFAULT_MOBJECT_TO_EDGE_BUFFER, ) -> Self: return self.align_on_border(direction=edge, buff=buff) @property - @deprecated(replacement="get_width") + # @deprecated(replacement="get_width") def width(self) -> float: return self.get_width() diff --git a/manim/mobject/abstract/test.py b/manim/mobject/abstract/test.py index ff822dc0ef..76347727b5 100644 --- a/manim/mobject/abstract/test.py +++ b/manim/mobject/abstract/test.py @@ -1,16 +1,20 @@ +"Tests whether the methods of Positionable behave the exact same as the Mobject methods." + import time from collections.abc import Callable -from logging import getLogger from typing import Any import numpy as np from manim.mobject.abstract.positionable import Positionable from manim.mobject.mobject import Mobject +from manim.mobject.opengl.opengl_mobject import OpenGLMobject +from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject +from manim.mobject.types.vectorized_mobject import VMobject from manim.typing import Point3D, Point3D_Array, Vector3D _RNG = np.random.default_rng() -POINT_COUNTS = list(range(1, 101)) +POINT_COUNTS = list(range(1, 100 + 1, 1)) LOOPS_PER_POINT_COUNT: int = 100 UNTESTED = [ name @@ -20,8 +24,6 @@ def main() -> None: - getLogger("manim").addFilter(lambda x: "deprecated" not in x.getMessage()) - validate_setter( "align_on_border", lambda mob, kwargs: mob.align_on_border(**kwargs), @@ -539,17 +541,19 @@ def random_number(low: float = -10, high: float = 10) -> float: return _RNG.uniform(low=low, high=high) -def random_point(low: float = -10, high: float = 10) -> Point3D: +def random_point(low: float = -25, high: float = 25) -> Point3D: return _RNG.uniform(low=low, high=high, size=3) -def random_points(low: float = -10, high: float = 10, size: int = 1) -> np.ndarray: +def random_points(low: float = -25, high: float = 25, size: int = 1) -> np.ndarray: return _RNG.uniform(low=low, high=high, size=(size, 3)) def random_vector(low: float = -3, high: float = 3) -> Vector3D: - dtype = random_choice([int, float]) - return _RNG.uniform(low=low, high=high, size=3).astype(dtype=dtype) + v = _RNG.uniform(low=low, high=high, size=3) + if random_number(0, 1) <= 0.5: + v = np.round(v) + return v def random_choice(a: list[Any]) -> Any: @@ -565,5 +569,30 @@ def create_another( return another +def dump_attributes() -> None: + seen: set[str] = set() + + for cls in [Mobject, VMobject, OpenGLMobject, OpenGLVMobject]: + assert isinstance(cls, type) + print(cls.__name__) + for name, attr in sorted(cls.__dict__.items()): + if ( + name in seen + or name.startswith("__") + or attr is getattr(cls.__base__, name, None) + ): + continue + print( + f"\t{'-+'[getattr(Positionable, name, None) is not getattr(Positionable.__base__, name, None)]} {name}" + ) + seen |= cls.__dict__.keys() + + print(Positionable.__name__) + for name, attr in Positionable.__dict__.items(): + if name.startswith("__") or attr is getattr(Positionable.__base__, name, None): + continue + print(f"\t* {name}", "(new)" if name not in seen else "") + + if __name__ == "__main__": main() diff --git a/manim/mobject/abstract/test.txt b/manim/mobject/abstract/test.txt deleted file mode 100644 index d592debca1aeb8025023088f5d5ef4351cdd83c5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5930 zcmcIo(NY>g5ZqT%m4C<=Slj_BAChOv03ub26kwu$e3I^+%+2iGEw@;vtl*x}-oBoh zp4t8Ldlc@%W7vjG*o8%Sj-Nm9{Sa1Rjn8jAU!zckt1!doYZ!$w)-~40f9ontvHlvq z;MrrJfZB(a{!H+0fT-Alr~8Z>>?Qc^!YRu##pTlos8`^7+hC`>4V&-22lhh&W7$Xe zO~OrsJ%&Q2N!{@N2sZT|CwkvD*friq^}dg>+!wQ^Cd8hAtLk0j$#~y1*c)haLXI#y zPWXBbPXqf1_8iO=+sBe|P~IN<1yNlga}MDRyX~>hHpV%Pr(oB}HN_qyy2@MDl7os} zo-dLob&`YBL1Ns;+hene?!o*C?{LIdoA(rY&%(UnUAa6NNA26ZpMl7FOp&@z!fy%; z(}cCCyysYVu@6u;SLwEg= z8tR?EeT@jd;NK#$r!#J_Ug3Q-?*+R`YTjz6FP4u$y$8DKnp+9>Z8lH3dhfuK?D{?1 z@1xk}gF|)6NN(VU!^{PH?lejByUUwSQs!8fW4{(CEy_rZ%v^66$3CXD)8+m4AFc^XDQjU!dt1{DK?#ys&4~xGSxz# z6x(>$5$`;9S*J%-&sn9GwgFYLp%n$KaEdEq74Qi}`dTyi4O8TmRh4_2<`IZ1JPxDi&OZygukOpSg(oY% z>=r_MgYTE=JZLkrVCzkRk6CW5yshSQ*nMxELzSOvo>))wwd2n_AHin5%-vm>oN?3# zRg>@Ng}g6iwon(z%R8LD_vrh)ZDS;!;uGf>P5``(TjKY(_@7 Date: Thu, 20 Aug 2026 00:00:45 +0200 Subject: [PATCH 11/66] Update --- manim/mobject/abstract/positionable.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/manim/mobject/abstract/positionable.py b/manim/mobject/abstract/positionable.py index 388dcb4866..9a5437869f 100644 --- a/manim/mobject/abstract/positionable.py +++ b/manim/mobject/abstract/positionable.py @@ -1,5 +1,6 @@ from collections.abc import Callable, Iterable from typing import Self +from warnings import deprecated import numpy as np @@ -701,6 +702,7 @@ def shift(self, vector: Vector3DLike) -> Self: self.points += vector return self + # @deprecated() def shift_onto_screen( self, buff: float = DEFAULT_MOBJECT_TO_EDGE_BUFFER, From 81a5681ef27638a91b6b4df1d9db9c1a015a5861 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 19 Aug 2026 22:29:12 +0000 Subject: [PATCH 12/66] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- manim/mobject/abstract/positionable.py | 1 - 1 file changed, 1 deletion(-) diff --git a/manim/mobject/abstract/positionable.py b/manim/mobject/abstract/positionable.py index 9a5437869f..8af1b964d0 100644 --- a/manim/mobject/abstract/positionable.py +++ b/manim/mobject/abstract/positionable.py @@ -1,6 +1,5 @@ from collections.abc import Callable, Iterable from typing import Self -from warnings import deprecated import numpy as np From cdffedea61f994dc9cde86a21351e35e16127ef9 Mon Sep 17 00:00:00 2001 From: GniLudio <50866361+GniLudio@users.noreply.github.com> Date: Thu, 20 Aug 2026 00:42:10 +0200 Subject: [PATCH 13/66] Pre-commit fixes --- manim/mobject/abstract/positionable.py | 14 ++++++++------ manim/mobject/abstract/test.py | 10 +++++----- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/manim/mobject/abstract/positionable.py b/manim/mobject/abstract/positionable.py index 8af1b964d0..a7aeddb313 100644 --- a/manim/mobject/abstract/positionable.py +++ b/manim/mobject/abstract/positionable.py @@ -1,5 +1,5 @@ from collections.abc import Callable, Iterable -from typing import Self +from typing import Self, cast import numpy as np @@ -207,12 +207,13 @@ def get_center_of_mass(self) -> Point3D: def get_coord(self, dim: int, direction: Vector3DLike = ORIGIN) -> float: key = np.sign(direction[dim]) - return ( + return cast( + float, self.points[:, dim].min() if key == -1 else (self.points[:, dim].min() + self.points[:, dim].max()) / 2 if key == 0 - else self.points[:, dim].max() + else self.points[:, dim].max(), ) # @deprecated(replacement="get_critical_point") @@ -229,7 +230,7 @@ def get_depth(self) -> float: def get_dim_size(self, dim: int) -> float: values = self.points[:, dim] - return values.max() - values.min() + return cast(float, values.max() - values.min()) # @deprecated(replacement="get_critical_point") def get_edge_center(self, direction: Vector3DLike) -> Point3D: @@ -294,11 +295,12 @@ def height(self, value: float) -> Self: def is_off_screen(self) -> bool: mins, _, maxs = self.get_bounding_box() - return ( + return cast( + bool, mins[0] > config.frame_x_radius or maxs[0] < -config.frame_x_radius or mins[1] > config.frame_y_radius - or maxs[1] < -config.frame_y_radius + or maxs[1] < -config.frame_y_radius, ) # @deprecated(replacement="get_dim_size") diff --git a/manim/mobject/abstract/test.py b/manim/mobject/abstract/test.py index 76347727b5..ad280f20c5 100644 --- a/manim/mobject/abstract/test.py +++ b/manim/mobject/abstract/test.py @@ -2,7 +2,7 @@ import time from collections.abc import Callable -from typing import Any +from typing import Any, cast import numpy as np @@ -494,7 +494,7 @@ def validate_setter( name: str, function: Callable[[Mobject | Positionable, dict[Any, Any]], Any], create_kwargs: Callable[[], Any] = lambda: {}, -): +) -> None: def validate( mob_old: Mobject, mob_new: Positionable, @@ -516,7 +516,7 @@ def validate_getter( name: str, function: Callable[[Mobject | Positionable, dict[Any, Any]], Any], create_kwargs: Callable[[], Any] = lambda: {}, -): +) -> None: def validate( mob_old: Mobject, mob_new: Positionable, @@ -538,7 +538,7 @@ def optional(value: Any, a: float = 0.9) -> Any | None: def random_number(low: float = -10, high: float = 10) -> float: - return _RNG.uniform(low=low, high=high) + return cast(float, _RNG.uniform(low=low, high=high)) def random_point(low: float = -25, high: float = 25) -> Point3D: @@ -563,7 +563,7 @@ def random_choice(a: list[Any]) -> Any: def create_another( mob: Mobject | Positionable, points: Point3D_Array, -) -> Mobject | Positionable: +) -> Any: another = type(mob)() another.points = points return another From c1b28b60b300f3c615c29e5c0ffb89ea49162975 Mon Sep 17 00:00:00 2001 From: GniLudio <50866361+GniLudio@users.noreply.github.com> Date: Fri, 21 Aug 2026 23:17:19 +0200 Subject: [PATCH 14/66] Restructuring and add inheritance to Mobject --- manim/mobject/abstract/positionable.py | 1018 ++++++++++++------------ manim/mobject/abstract/test.py | 598 -------------- manim/mobject/mobject.py | 943 +--------------------- 3 files changed, 498 insertions(+), 2061 deletions(-) delete mode 100644 manim/mobject/abstract/test.py diff --git a/manim/mobject/abstract/positionable.py b/manim/mobject/abstract/positionable.py index a7aeddb313..d05a371ac6 100644 --- a/manim/mobject/abstract/positionable.py +++ b/manim/mobject/abstract/positionable.py @@ -1,12 +1,11 @@ from collections.abc import Callable, Iterable -from typing import Self, cast +from typing import Any, Self import numpy as np from manim._config import config from manim.constants import ( DEFAULT_MOBJECT_TO_EDGE_BUFFER, - DEFAULT_MOBJECT_TO_MOBJECT_BUFFER, DL, DOWN, IN, @@ -22,60 +21,125 @@ Point3D, Point3D_Array, Point3DLike, + Point3DLike_Array, Vector3DLike, ) - -# from manim.utils.deprecation import deprecated from manim.utils.space_ops import rotation_matrix class Positionable: - points: Point3D_Array = np.array([(0.0, 0.0, 0.0)]) + """A positionable object. + + ### Applying Functions + * get_family + * apply_to_family + * apply_array_function + * apply_function + * apply_complex_function + ### Transformations + * apply_matrix + * translate + * rotate + * scale + * stretch + ### General + * get_bounding_box + * (get|set)_position + * (get|set)_(center|left|right|bottom|top|nadir|zenith) + * (get|set)_coord + * (get|set)_(x|y|z) + * (get|set)_dim_size + * (get|set)_(width|height|depth) + ### Specialized + * align_on_border + * align_to + * center + * flip + * is_off_screen + * get_center_of_mass + * get_boundary_point + * next_to (TODO) + * shift_onto_screen + * scale_to_fit + * scale_to_fit_(width|height|depth) + * stretch_to_fit + * stretch_to_fit_(width|height|depth) + * to_corner + * to_edge + ### Aliases & Combability + + """ + + ### FUNDAMENTALS ### + points: Point3D_Array = np.array([]) + + def get_points(self) -> Point3D_Array: + return np.concat([mob.points for mob in self.get_family()]) + + def set_points(self, points: "Point3DLike_Array | Positionable") -> Self: + 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) + return self - def align_on_border( - self, - direction: Vector3DLike, - buff: float = DEFAULT_MOBJECT_TO_EDGE_BUFFER, - *, - frame: Point3DLike | None = None, - ) -> Self: - if frame is None: - frame = (config.frame_x_radius, config.frame_y_radius, 0) - target_point = np.sign(direction) * frame - point_to_align = self.get_critical_point(direction=direction) - shift_val = target_point - point_to_align - buff * np.asarray(direction) - shift_val = shift_val * abs(np.sign(direction)) - return self.shift(shift_val) + def get_points_defining_boundary(self) -> Point3D_Array: + return self.get_points() - def align_to( + ### APPLYING FUNCTIONS ### + + def get_family(self) -> Iterable["Positionable"]: + yield self + + def apply_to_family( self, - mobject_or_point: "Positionable | Point3DLike", - direction: Vector3DLike = ORIGIN, + function: Callable[["Positionable"], Any], + only_with_points: bool = True, ) -> Self: - source = self.get_critical_point(direction=direction) - target = np.array( - mobject_or_point.get_critical_point(direction=direction) - if isinstance(mobject_or_point, Positionable) - else mobject_or_point - ) - target = np.where(direction == 0, source, target) - return self.shift(target - source) + 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: if about_point is None: - about_point = self.get_critical_point( - direction=about_edge if about_edge is not None else ORIGIN - ) - self.points -= about_point - self.points = function(self.points) - self.points += about_point - return self + if about_edge is None: + about_edge = ORIGIN + about_point = self.get_position(direction=about_edge) + + about_point = np.array(about_point, copy=True) + + 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: + 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, @@ -84,483 +148,424 @@ def apply_complex_function( about_point: Point3DLike | None = None, about_edge: Vector3DLike | None = None, ) -> Self: - def R3_func(point: Point3D) -> Point3D: + 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=R3_func, + function=apply, about_point=about_point, about_edge=about_edge, ) - def apply_function( + ### TRANSFORMATIONS ### + + def apply_matrix( self, - function: Callable[[Point3D], Point3D], - *, + matrix: MatrixMN, about_point: Point3DLike | None = None, about_edge: Vector3DLike | None = None, ) -> Self: 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 - def mapping_function(points: Point3D_Array) -> Point3D_Array: - return np.apply_along_axis(func1d=function, axis=1, arr=points) - - return self.apply_array_function( - function=mapping_function, + self.apply_array_function( + function=lambda points: points.dot(full_matrix.T, out=points), about_point=about_point, about_edge=about_edge, ) + return self - # @deprecated(replacement="move_to(function(self.get_center()))") - def apply_function_to_position( - self, - function: Callable[[Point3D], Point3D], - ) -> Self: - return self.move_to(function(self.get_center())) + def translate(self, vector: Vector3DLike) -> Self: + def function(mob: Positionable) -> None: + mob.points += vector - def apply_matrix( + return self.apply_to_family(function=function) + + def rotate( self, - matrix: MatrixMN, - *, + angle: float, + axis: Vector3DLike = OUT, about_point: Point3DLike | None = None, about_edge: Vector3DLike | None = None, ) -> Self: if about_point is None and about_edge is None: - about_point = ORIGIN - - matrix = np.asarray(matrix) - - # Fast path for standard 3x3 matrices - if matrix.shape == (3, 3): - full_matrix = matrix - else: - 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), + about_edge = ORIGIN + return self.apply_matrix( + matrix=rotation_matrix(angle, axis), about_point=about_point, about_edge=about_edge, ) - # @deprecated(replacement="apply_array_function") - def apply_points_function_about_point( + def scale( self, - func: Callable[[Point3D_Array], Point3D_Array], + # TODO: Rename to `factor` + scale_factor: float, + *, about_point: Point3DLike | None = None, about_edge: Vector3DLike | None = None, ) -> Self: return self.apply_array_function( - function=func, + function=lambda points: points.__imul__(scale_factor), about_point=about_point, about_edge=about_edge, ) - def center(self) -> Self: - return self.move_to(point_or_mobject=ORIGIN) - - @property - # @deprecated(replacement="get_depth") - def depth(self) -> float: - return self.get_depth() - - @depth.setter - # @deprecated(replacement="set_depth") - def depth(self, value: float) -> Self: - return self.set_depth(depth=value, stretch=False) - - def flip( + def stretch( self, - axis: Vector3DLike = UP, + factor: float, + dim: int, *, about_point: Point3DLike | None = None, about_edge: Vector3DLike | None = None, ) -> Self: - return self.rotate( - angle=TAU / 2, - axis=axis, + 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 get_bottom(self) -> Point3D: - return self.get_critical_point(direction=DOWN) + ### GENERAL ### - def get_boundary_point(self, direction: Vector3DLike) -> Point3D: - index = np.argmax(np.dot(self.points, direction)) - return self.points[index] + def get_bounding_box(self) -> tuple[Point3D, Point3D]: + 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_bounding_box(self) -> Point3D_Array: - mins = self.points.min(axis=0) - maxs = self.points.max(axis=0) + def get_position(self, direction: Vector3DLike = ORIGIN) -> Point3D: + direction = np.sign(direction) + mins, maxs = self.get_bounding_box() mids = (mins + maxs) / 2 - return np.array([mins, mids, maxs]) + return mids + (maxs - mids) * direction + + def set_position( + self, + point: "Point3DLike | Positionable", + aligned_edge: Vector3DLike = ORIGIN, + coor_mask: Vector3DLike = np.array([1, 1, 1]), + ) -> Self: + if isinstance(point, Positionable): + point = point.get_position(direction=aligned_edge) + current = self.get_position(direction=aligned_edge) + vector = (point - current) * coor_mask + return self.translate(vector=vector) def get_center(self) -> Point3D: - return self.get_critical_point(direction=ORIGIN) + return self.get_position(direction=ORIGIN) - def get_center_of_mass(self) -> Point3D: - return self.points.mean(axis=0) - - def get_coord(self, dim: int, direction: Vector3DLike = ORIGIN) -> float: - key = np.sign(direction[dim]) - return cast( - float, - self.points[:, dim].min() - if key == -1 - else (self.points[:, dim].min() + self.points[:, dim].max()) / 2 - if key == 0 - else self.points[:, dim].max(), - ) + def set_center( + self, + center: "Point3DLike | Positionable", + coor_mask: Vector3DLike = np.array([1, 1, 1]), + ) -> Self: + return self.set_position(point=center, aligned_edge=ORIGIN, coor_mask=coor_mask) - # @deprecated(replacement="get_critical_point") - def get_corner(self, direction: Vector3DLike) -> Point3D: - return self.get_critical_point(direction=direction) + def get_left(self) -> Point3D: + return self.get_position(direction=LEFT) - def get_critical_point(self, direction: Vector3DLike) -> Point3D: - direction = np.sign(direction) - _, mids, maxs = self.get_bounding_box() - return mids + (maxs - mids) * direction + def set_left( + self, + left: "Point3DLike | Positionable", + coor_mask: Vector3DLike = np.array([1, 1, 1]), + ) -> Self: + return self.set_position(point=left, aligned_edge=LEFT, coor_mask=coor_mask) - def get_depth(self) -> float: - return self.get_dim_size(dim=2) + def get_right(self) -> Point3D: + return self.get_position(direction=RIGHT) - def get_dim_size(self, dim: int) -> float: - values = self.points[:, dim] - return cast(float, values.max() - values.min()) + def set_right( + self, + right: "Point3DLike | Positionable", + coor_mask: Vector3DLike = np.array([1, 1, 1]), + ) -> Self: + return self.set_position(point=right, aligned_edge=RIGHT, coor_mask=coor_mask) - # @deprecated(replacement="get_critical_point") - def get_edge_center(self, direction: Vector3DLike) -> Point3D: - return self.get_critical_point(direction=direction) + def get_bottom(self) -> Point3D: + return self.get_position(direction=DOWN) - # @deprecated() - def get_extremum_along_dim( + def set_bottom( self, - dim: int = 0, - key: int = 0, - ) -> float: - values = self.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 + bottom: "Point3DLike | Positionable", + coor_mask: Vector3DLike = np.array([1, 1, 1]), + ) -> Self: + return self.set_position(point=bottom, aligned_edge=DOWN, coor_mask=coor_mask) - def get_height(self) -> float: - return self.get_dim_size(dim=1) + def get_top(self) -> Point3D: + return self.get_position(direction=UP) - def get_left(self) -> Point3D: - return self.get_critical_point(direction=LEFT) + def set_top( + self, + top: "Point3DLike | Positionable", + coor_mask: Vector3DLike = np.array([1, 1, 1]), + ) -> Self: + return self.set_position(point=top, aligned_edge=UP, coor_mask=coor_mask) def get_nadir(self) -> Point3D: - return self.get_critical_point(direction=IN) + return self.get_position(direction=IN) - def get_right(self) -> Point3D: - return self.get_critical_point(direction=RIGHT) + def set_nadir( + self, + nadir: "Point3DLike | Positionable", + coor_mask: Vector3DLike = np.array([1, 1, 1]), + ) -> Self: + return self.set_position(point=nadir, aligned_edge=IN, coor_mask=coor_mask) - def get_top(self) -> Point3D: - return self.get_critical_point(UP) + def get_zenith(self) -> Point3D: + return self.get_position(direction=OUT) - def get_width(self) -> float: - return self.get_dim_size(dim=0) + def set_zenith( + self, + zenith: "Point3DLike | Positionable", + coor_mask: Vector3DLike = np.array([1, 1, 1]), + ) -> Self: + return self.set_position(point=zenith, aligned_edge=OUT, coor_mask=coor_mask) + + def get_coord( + self, + dim: int, + direction: Vector3DLike = ORIGIN, + ) -> float: + points = self.get_points() + if len(points) == 0: + return 0 + + key = direction[dim] + 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 set_coord( + self, + value: "float | Positionable", + dim: int, + direction: Vector3DLike = ORIGIN, + ) -> Self: + if isinstance(value, Positionable): + value = value.get_coord(dim=dim, direction=direction) + current = self.get_coord(dim=dim, direction=direction) + vector = np.zeros(3) + vector[dim] = value - current + return self.translate(vector=vector) def get_x(self, direction: Vector3DLike = ORIGIN) -> float: return self.get_coord(dim=0, direction=direction) + def set_x(self, x: float, direction: Vector3DLike = ORIGIN) -> Self: + return self.set_coord(value=x, dim=0, direction=direction) + def get_y(self, direction: Vector3DLike = ORIGIN) -> float: return self.get_coord(dim=1, direction=direction) + def set_y(self, y: float, direction: Vector3DLike = ORIGIN) -> Self: + return self.set_coord(value=y, dim=1, direction=direction) + def get_z(self, direction: Vector3DLike = ORIGIN) -> float: return self.get_coord(dim=2, direction=direction) - def get_zenith(self) -> Point3D: - return self.get_critical_point(direction=OUT) - - @property - # @deprecated(replacement="get_height") - def height(self) -> float: - return self.get_height() - - @height.setter - # @deprecated(replacement="set_height") - def height(self, value: float) -> Self: - return self.set_height(height=value, stretch=False) - - def is_off_screen(self) -> bool: - mins, _, maxs = self.get_bounding_box() - return cast( - bool, - mins[0] > config.frame_x_radius - or maxs[0] < -config.frame_x_radius - or mins[1] > config.frame_y_radius - or maxs[1] < -config.frame_y_radius, - ) + def set_z(self, z: float, direction: Vector3DLike = ORIGIN) -> Self: + return self.set_coord(value=z, dim=2, direction=direction) - # @deprecated(replacement="get_dim_size") - def length_over_dim(self, dim: int) -> float: - return self.get_dim_size(dim=dim) + def get_dim_size(self, dim: int) -> float: + points = self.get_points() + if len(points) == 0: + return 0 + return np.ptp(points[:, dim]) # type: ignore[no-any-return] - def match_coord( + def set_dim_size( self, - mobject: "Positionable", + size: "float | Positionable", dim: int, - direction: Vector3DLike = ORIGIN, - ) -> Self: - return self.set_coord( - mobject.get_coord(dim=dim, direction=direction), - dim=dim, - direction=direction, - ) - - def match_depth( - self, - mobject: "Positionable", - *, stretch: bool = False, about_point: Point3DLike | None = None, about_edge: Vector3DLike | None = None, ) -> Self: - return self.set_depth( - mobject.get_depth(), - stretch=stretch, - about_point=about_point, - about_edge=about_edge, - ) + 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( + scale_factor=factor, + about_point=about_point, + about_edge=about_edge, + ) - def match_dim_size( + def get_width(self) -> float: + return self.get_dim_size(dim=0) + + def set_width( self, - mobject: "Positionable", - dim: int, - *, + width: "float | Positionable", stretch: bool = False, about_point: Point3DLike | None = None, about_edge: Vector3DLike | None = None, ) -> Self: return self.set_dim_size( - mobject.get_dim_size(dim=dim), - dim=dim, + size=width, + dim=0, stretch=stretch, about_point=about_point, about_edge=about_edge, ) - def match_height( + def get_height(self) -> float: + return self.get_dim_size(dim=1) + + def set_height( self, - mobject: "Positionable", - *, + height: "float | Positionable", stretch: bool = False, about_point: Point3DLike | None = None, about_edge: Vector3DLike | None = None, ) -> Self: - return self.set_height( - mobject.get_height(), + return self.set_dim_size( + size=height, + dim=1, stretch=stretch, about_point=about_point, about_edge=about_edge, ) - def match_points(self, mobject: "Positionable") -> Self: - self.points = mobject.points.copy() - return self + def get_depth(self) -> float: + return self.get_dim_size(dim=2) - def match_width( + def set_depth( self, - mobject: "Positionable", - *, + depth: "float | Positionable", stretch: bool = False, about_point: Point3DLike | None = None, about_edge: Vector3DLike | None = None, ) -> Self: - return self.set_width( - mobject.get_width(), + return self.set_dim_size( + size=depth, + dim=2, stretch=stretch, about_point=about_point, about_edge=about_edge, ) - def match_x( + ### SPECIALIZED ### + def align_on_border( self, - mobject: "Positionable", - direction: Vector3DLike = ORIGIN, + direction: Vector3DLike, + buff: float = DEFAULT_MOBJECT_TO_EDGE_BUFFER, ) -> Self: - return self.set_x( - mobject.get_x(direction=direction), - direction=direction, - ) + frame = (config.frame_x_radius, config.frame_y_radius, 0) + target_point = np.sign(direction) * frame + point_to_align = self.get_critical_point(direction=direction) + shift_val = target_point - point_to_align - buff * np.asarray(direction) + shift_val = shift_val * abs(np.sign(direction)) + return self.shift(shift_val) - def match_y( + def align_to( self, - mobject: "Positionable", - direction: Vector3DLike = ORIGIN, - ) -> Self: - return self.set_y( - mobject.get_y(direction=direction), - direction=direction, - ) - - def match_z( - self, - mobject: "Positionable", + # TODO: Rename to point + mobject_or_point: "Positionable | Point3DLike", direction: Vector3DLike = ORIGIN, ) -> Self: - return self.set_z( - mobject.get_z(direction=direction), - direction=direction, - ) - - def move_to( - self, - point_or_mobject: "Point3DLike | Positionable", - aligned_edge: Vector3DLike = ORIGIN, - coor_mask: Vector3DLike = np.array([1, 1, 1]), - ) -> Self: - if isinstance(point_or_mobject, Positionable): - return self.move_to( - point_or_mobject.get_critical_point(direction=aligned_edge), - aligned_edge=aligned_edge, - coor_mask=coor_mask, - ) - source = self.get_critical_point(direction=aligned_edge) - target = point_or_mobject - return self.shift(vector=(target - source) * coor_mask) + if isinstance(mobject_or_point, Positionable): + mobject_or_point = mobject_or_point.get_position(direction=direction) + source = self.get_critical_point(direction=direction) + target = np.where(direction == 0, source, mobject_or_point) + return self.shift(target - source) - def next_to( - self, - mobject_or_point: "Positionable | Point3DLike", - direction: Vector3DLike = RIGHT, - buff: float = DEFAULT_MOBJECT_TO_MOBJECT_BUFFER, - aligned_edge: Vector3DLike = ORIGIN, - coor_mask: Vector3DLike = np.array([1, 1, 1]), - ) -> Self: - np_direction = np.asarray(direction) - np_aligned_edge = np.asarray(aligned_edge) - source = self.get_critical_point(direction=np_aligned_edge - np_direction) - target = ( - mobject_or_point.get_critical_point(np_aligned_edge + np_direction) - if isinstance(mobject_or_point, Positionable) - else mobject_or_point - ) - return self.shift((target - source + buff * np_direction) * coor_mask) + def center(self) -> Self: + return self.set_center(ORIGIN) - # @deprecated() - def pose_at_angle( + def flip( self, - *, + axis: Vector3DLike = UP, about_point: Point3DLike | None = None, about_edge: Vector3DLike | None = None, ) -> Self: return self.rotate( - angle=TAU / 14, - axis=RIGHT + UP, + TAU / 2, + axis, about_point=about_point, about_edge=about_edge, ) - # @deprecated() - def reduce_across_dimension( - self, - reduce_func: Callable[[Iterable[float]], float], - dim: int, - ) -> float | None: - if len(self.points) == 0: - return None - - return reduce_func(self.points[:, dim]) - - # @deprecated(replacement="set_dim_size") - def rescale_to_fit( - self, - length: float, - 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, + def is_off_screen(self) -> bool: + mins, maxs = self.get_bounding_box() + return ( # type: ignore[return-value] + mins[0] > config.frame_x_radius + or maxs[0] < -config.frame_x_radius + or mins[1] > config.frame_y_radius + or maxs[1] < -config.frame_y_radius, ) - def rotate( - self, - angle: float, - axis: Vector3DLike = OUT, - *, - about_point: Point3DLike | None = None, - about_edge: Vector3DLike | None = None, - ) -> Self: - 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 get_center_of_mass(self) -> Point3D: + points = self.get_points() + if len(points) == 0: + return ORIGIN + return points.mean(axis=0) - # @deprecated(replacement="rotate") - def rotate_about_origin( - self, - angle: float, - axis: Vector3DLike = OUT, - ) -> Self: - return self.rotate( - angle=angle, - axis=axis, - about_point=ORIGIN, - ) + def get_boundary_point(self, direction: Vector3DLike) -> Point3D: + points = self.get_points_defining_boundary() + index = np.argmax(points.dot(direction)) + return points[index] - def scale( - self, - # TODO: Rename to 'factor' - scale_factor: float | Vector3DLike, - about_point: Point3DLike | None = None, - about_edge: Vector3DLike | None = None, - ) -> Self: - return self.apply_array_function( - function=lambda points: scale_factor * points, - about_point=about_point, - about_edge=about_edge, - ) + def shift_onto_screen(self, buff: float = DEFAULT_MOBJECT_TO_EDGE_BUFFER) -> Self: + space_lengths = [config["frame_x_radius"], config["frame_y_radius"]] + for vect in UP, DOWN, LEFT, RIGHT: + dim = np.argmax(np.abs(vect)) + 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, buff=buff) + return self def scale_to_fit( self, - length: float, + size: float, dim: int, - *, about_point: Point3DLike | None = None, about_edge: Vector3DLike | None = None, ) -> Self: - actual_length = self.get_dim_size(dim=dim) - if actual_length == 0: - return self - - return self.scale( - scale_factor=length / actual_length, + return self.set_dim_size( + size=size, + dim=dim, + stretch=False, about_point=about_point, about_edge=about_edge, ) - def scale_to_fit_depth( + def scale_to_fit_width( self, - depth: float, - *, + width: float, about_point: Point3DLike | None = None, about_edge: Vector3DLike | None = None, ) -> Self: return self.scale_to_fit( - length=depth, - dim=2, + size=width, + dim=0, about_point=about_point, about_edge=about_edge, ) @@ -568,263 +573,224 @@ def scale_to_fit_depth( def scale_to_fit_height( self, height: float, - *, about_point: Point3DLike | None = None, about_edge: Vector3DLike | None = None, ) -> Self: return self.scale_to_fit( - length=height, + size=height, dim=1, about_point=about_point, about_edge=about_edge, ) - def scale_to_fit_width( + def scale_to_fit_depth( self, - width: float, - *, + depth: float, about_point: Point3DLike | None = None, about_edge: Vector3DLike | None = None, ) -> Self: return self.scale_to_fit( - length=width, - dim=0, + size=depth, + dim=2, about_point=about_point, about_edge=about_edge, ) - def set_coord( + def stretch_to_fit( self, - value: float, + size: float, dim: int, - direction: Vector3DLike = ORIGIN, - ) -> Self: - source = self.get_coord(dim, direction=direction) - self.points[:, dim] += value - source - return self - - def set_depth( - self, - depth: float, - *, - stretch: bool = False, about_point: Point3DLike | None = None, about_edge: Vector3DLike | None = None, ) -> Self: return self.set_dim_size( - size=depth, - dim=2, - stretch=stretch, + size=size, + dim=dim, + stretch=True, about_point=about_point, about_edge=about_edge, ) - def set_dim_size( + def stretch_to_fit_width( self, - size: float, - dim: int, - *, - stretch: bool = False, + width: float, about_point: Point3DLike | None = None, about_edge: Vector3DLike | None = None, ) -> Self: - old_length = self.get_dim_size(dim=dim) - if old_length == 0: - return self - factor = size / old_length - if stretch: - return self.stretch( - factor=factor, - dim=dim, - about_point=about_point, - about_edge=about_edge, - ) - else: - return self.scale( - scale_factor=factor, - about_point=about_point, - about_edge=about_edge, - ) + return self.stretch_to_fit( + size=width, + dim=0, + about_point=about_point, + about_edge=about_edge, + ) - def set_height( + def stretch_to_fit_height( self, height: float, - *, - stretch: bool = False, about_point: Point3DLike | None = None, about_edge: Vector3DLike | None = None, ) -> Self: - return self.set_dim_size( + return self.stretch_to_fit( size=height, dim=1, - stretch=stretch, about_point=about_point, about_edge=about_edge, ) - def set_width( + def stretch_to_fit_depth( self, - width: float, - *, - stretch: bool = False, + depth: float, about_point: Point3DLike | None = None, about_edge: Vector3DLike | None = None, ) -> Self: - return self.set_dim_size( - size=width, - dim=0, - stretch=stretch, + return self.stretch_to_fit( + size=depth, + dim=2, about_point=about_point, about_edge=about_edge, ) - def set_x( - self, - x: float, - direction: Vector3DLike = ORIGIN, - ) -> Self: - return self.set_coord(value=x, dim=0, direction=direction) - - def set_y( - self, - y: float, - direction: Vector3DLike = ORIGIN, - ) -> Self: - return self.set_coord(value=y, dim=1, direction=direction) - - def set_z( + def to_corner( self, - z: float, - direction: Vector3DLike = ORIGIN, + corner: Vector3DLike = DL, + buff: float = DEFAULT_MOBJECT_TO_EDGE_BUFFER, ) -> Self: - return self.set_coord(value=z, dim=2, direction=direction) - - def shift(self, vector: Vector3DLike) -> Self: - self.points += vector - return self + return self.align_on_border(direction=corner, buff=buff) - # @deprecated() - def shift_onto_screen( + def to_edge( self, + edge: Vector3DLike = LEFT, buff: float = DEFAULT_MOBJECT_TO_EDGE_BUFFER, ) -> Self: - # TODO: Simplify implementation - space_lengths = [config["frame_x_radius"], config["frame_y_radius"]] - for edge in UP, DOWN, LEFT, RIGHT: - dim = np.argmax(np.abs(edge)) - max_val = space_lengths[dim] - buff - edge_center = self.get_critical_point(direction=edge) - if np.dot(edge_center, edge) > max_val: - self.to_edge(edge=edge, buff=buff) - return self + return self.align_on_border(direction=edge, buff=buff) - def stretch( - self, - factor: float, - dim: int, - *, - about_point: Point3DLike | None = None, - about_edge: Vector3DLike | None = None, - ) -> Self: - return self.scale( - scale_factor=np.array([factor if i == dim else 1.0 for i in range(3)]), - about_point=about_point, - about_edge=about_edge, - ) + ### ALIASES & COMBABILITY ### + match_points = set_points + apply_points_function_about_point = apply_array_function + shift = translate + get_critical_point = get_position + match_coord = set_coord + match_x = set_x + match_y = set_y + match_z = set_z + match_dim_size = set_dim_size + match_width = set_width + match_height = set_height + match_depth = set_depth + length_over_dim = get_dim_size + get_edge_center = get_position + get_corner = get_position - # @deprecated(replacement="stretch") - def stretch_about_point( + def move_to( self, - factor: float, - dim: int, - point: Point3DLike, + point_or_mobject: "Point3DLike | Positionable", + aligned_edge: Vector3DLike = ORIGIN, + coor_mask: Vector3DLike = np.array([1, 1, 1]), ) -> Self: - return self.stretch( - factor=factor, - dim=dim, - about_point=point, + return self.set_position( + point=point_or_mobject, + aligned_edge=aligned_edge, + coor_mask=coor_mask, ) - def stretch_to_fit( + def stretch_about_point(self, factor: float, dim: int, point: Point3DLike) -> Self: + return self.stretch(factor=factor, dim=dim, about_point=point) + + def rescale_to_fit( self, - length: float, + length: "float | Positionable", dim: int, - *, + stretch: bool = False, about_point: Point3DLike | None = None, about_edge: Vector3DLike | None = None, ) -> Self: - actual_length = self.get_dim_size(dim=dim) - if actual_length == 0: - return self - - return self.stretch( - factor=length / actual_length, + return self.set_dim_size( + size=length, dim=dim, + stretch=stretch, 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: - return self.stretch_to_fit( - length=depth, - dim=2, - about_point=about_point, - about_edge=about_edge, - ) + @property + def width(self) -> float: + return self.get_width() - def stretch_to_fit_height( + @width.setter + def width(self, value: float) -> None: + self.set_width(width=value) + + @property + def height(self) -> float: + return self.get_height() + + @height.setter + def height(self, value: float) -> None: + self.set_height(height=value) + + @property + def depth(self) -> float: + return self.get_depth() + + @depth.setter + def depth(self, value: float) -> None: + self.set_depth(depth=value) + + def pose_at_angle( self, - height: float, - *, about_point: Point3DLike | None = None, about_edge: Vector3DLike | None = None, ) -> Self: - return self.stretch_to_fit( - length=height, - dim=1, + return self.rotate( + angle=TAU / 14, + axis=RIGHT + UP, about_point=about_point, about_edge=about_edge, ) - def stretch_to_fit_width( + def rotate_about_origin( self, - width: float, - *, - about_point: Point3DLike | None = None, - about_edge: Vector3DLike | None = None, + angle: float, + axis: Vector3DLike = OUT, ) -> Self: - return self.stretch_to_fit( - length=width, - dim=0, - about_point=about_point, - about_edge=about_edge, + return self.rotate( + angle=angle, + axis=axis, + about_point=ORIGIN, ) - def to_corner( + def get_extremum_along_dim( self, - corner: Vector3DLike = DL, - buff: float = DEFAULT_MOBJECT_TO_EDGE_BUFFER, - ) -> Self: - return self.align_on_border(direction=corner, buff=buff) + dim: int = 0, + key: int = 0, + ) -> float: + points = self.get_points() + if len(points) == 0: + return 0 + values = 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 to_edge( + def apply_function_to_position( self, - edge: Vector3DLike = LEFT, - buff: float = DEFAULT_MOBJECT_TO_EDGE_BUFFER, + function: Callable[[Point3D], Point3DLike], ) -> Self: - return self.align_on_border(direction=edge, buff=buff) + return self.move_to(function(self.get_center())) - @property - # @deprecated(replacement="get_width") - def width(self) -> float: - return self.get_width() + def reduce_across_dimension( + self, + reduce_func: Callable[[Iterable[float]], float], + dim: int, + ) -> float | None: + points = self.get_points() + if len(points) == 0: + return None - @width.setter - def width(self, value: float) -> Self: - return self.set_width(width=value, stretch=False) + return reduce_func(points[:, dim]) diff --git a/manim/mobject/abstract/test.py b/manim/mobject/abstract/test.py deleted file mode 100644 index ad280f20c5..0000000000 --- a/manim/mobject/abstract/test.py +++ /dev/null @@ -1,598 +0,0 @@ -"Tests whether the methods of Positionable behave the exact same as the Mobject methods." - -import time -from collections.abc import Callable -from typing import Any, cast - -import numpy as np - -from manim.mobject.abstract.positionable import Positionable -from manim.mobject.mobject import Mobject -from manim.mobject.opengl.opengl_mobject import OpenGLMobject -from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject -from manim.mobject.types.vectorized_mobject import VMobject -from manim.typing import Point3D, Point3D_Array, Vector3D - -_RNG = np.random.default_rng() -POINT_COUNTS = list(range(1, 100 + 1, 1)) -LOOPS_PER_POINT_COUNT: int = 100 -UNTESTED = [ - name - for name, attr in Positionable.__dict__.items() - if not (name.startswith("__") or attr is getattr(Positionable.__base__, name, None)) -] - - -def main() -> None: - validate_setter( - "align_on_border", - lambda mob, kwargs: mob.align_on_border(**kwargs), - lambda: { - "direction": random_vector(), - "buff": optional(random_number()), - }, - ) - validate_setter( - "align_to", - lambda mob, kwargs: mob.align_to(**kwargs), - lambda: { - "mobject_or_point": random_point(), - "direction": optional(random_vector()), - }, - ) - # TODO: apply_complex_function - # TODO: apply_function - # TODO: apply_function_to_position - # TODO: apply_matrix - # TODO: apply_points_function_about_point - validate_setter("center", lambda mob, _: mob.center()) - validate_getter("depth", lambda mob, _: mob.depth) - validate_setter( - "depth", - lambda mob, kwargs: setattr(mob, "depth", kwargs["value"]), - lambda: {"value": random_number()}, - ) - validate_setter( - "flip", - lambda mob, kwargs: mob.flip(**kwargs), - lambda: { - "axis": optional(random_vector()), - "about_point": optional(random_point()), - "about_edge": optional(random_vector()), - }, - ) - validate_getter("get_bottom", lambda mob, _: mob.get_bottom()) - validate_getter( - "get_boundary_point", - lambda mob, kwargs: mob.get_boundary_point(**kwargs), - lambda: {"direction": random_vector()}, - ) - validate_getter("get_center", lambda mob, _: mob.get_center()) - validate_getter("get_center_of_mass", lambda mob, _: mob.get_center_of_mass()) - validate_getter( - "get_coord", - lambda mob, kwargs: mob.get_coord(**kwargs), - lambda: { - "dim": random_choice([0, 1, 2]), - "direction": optional(random_vector()), - }, - ) - validate_getter( - "get_corner", - lambda mob, kwargs: mob.get_corner(**kwargs), - lambda: {"direction": random_vector()}, - ) - validate_getter( - "get_critical_point", - lambda mob, kwargs: mob.get_critical_point(**kwargs), - lambda: {"direction": random_vector()}, - ) - validate_getter( - "get_edge_center", - lambda mob, kwargs: mob.get_edge_center(**kwargs), - lambda: {"direction": random_vector()}, - ) - validate_getter( - "get_extremum_along_dim", - lambda mob, kwargs: mob.get_extremum_along_dim(**kwargs), - lambda: { - "dim": random_choice([0, 1, 2]), - "key": random_choice([0, 1, 2]), - }, - ) - validate_getter("get_left", lambda mob, _: mob.get_left()) - validate_getter("get_nadir", lambda mob, _: mob.get_nadir()) - validate_getter("get_right", lambda mob, _: mob.get_right()) - validate_getter("get_top", lambda mob, _: mob.get_top()) - validate_getter("get_x", lambda mob, _: mob.get_x()) - validate_getter("get_y", lambda mob, _: mob.get_y()) - validate_getter("get_z", lambda mob, _: mob.get_z()) - validate_getter("get_zenith", lambda mob, _: mob.get_zenith()) - validate_getter("height", lambda mob, _: mob.height) - validate_setter( - "height", - lambda mob, kwargs: setattr(mob, "height", kwargs["value"]), - lambda: { - "value": random_number(), - }, - ) - validate_getter("is_off_screen", lambda mob, _: mob.is_off_screen()) - validate_getter( - "length_over_dim", - lambda mob, kwargs: mob.length_over_dim(**kwargs), - lambda: {"dim": random_choice([0, 1, 2])}, - ) - validate_setter( - "match_coord", - lambda mob, kwargs: mob.match_coord( - create_another(mob, kwargs.pop("points")), **kwargs - ), - lambda: { - "points": random_points(size=int(random_number(1, 100))), - "dim": random_choice([0, 1, 2]), - "direction": optional(random_vector()), - }, - ) - validate_setter( - "match_depth", - lambda mob, kwargs: mob.match_depth( - create_another(mob, kwargs.pop("points")), **kwargs - ), - lambda: { - "points": random_points(size=int(random_number(1, 100))), - "stretch": optional(random_choice([True, False])), - "about_point": optional(random_point()), - "about_edge": optional(random_vector()), - }, - ) - validate_setter( - "match_dim_size", - lambda mob, kwargs: mob.match_dim_size( - create_another(mob, kwargs.pop("points")), **kwargs - ), - lambda: { - "points": random_points(size=int(random_number(1, 100))), - "dim": random_choice([0, 1, 2]), - "stretch": optional(random_choice([True, False])), - "about_point": optional(random_point()), - "about_edge": optional(random_vector()), - }, - ) - validate_setter( - "match_height", - lambda mob, kwargs: mob.match_height( - create_another(mob, kwargs.pop("points")), **kwargs - ), - lambda: { - "points": random_points(size=int(random_number(1, 100))), - "stretch": optional(random_choice([True, False])), - "about_point": optional(random_point()), - "about_edge": optional(random_vector()), - }, - ) - validate_setter( - "match_points", - lambda mob, kwargs: mob.match_points( - create_another(mob, kwargs.pop("points")), **kwargs - ), - lambda: { - "points": random_points(size=int(random_number(1, 100))), - }, - ) - validate_setter( - "match_width", - lambda mob, kwargs: mob.match_width( - create_another(mob, kwargs.pop("points")), **kwargs - ), - lambda: { - "points": random_points(size=int(random_number(1, 100))), - "stretch": optional(random_choice([True, False])), - "about_point": optional(random_point()), - "about_edge": optional(random_vector()), - }, - ) - validate_setter( - "match_x", - lambda mob, kwargs: mob.match_x( - create_another(mob, kwargs.pop("points")), **kwargs - ), - lambda: { - "points": random_points(size=int(random_number(1, 100))), - "direction": optional(random_vector()), - }, - ) - validate_setter( - "match_y", - lambda mob, kwargs: mob.match_y( - create_another(mob, kwargs.pop("points")), **kwargs - ), - lambda: { - "points": random_points(size=int(random_number(1, 100))), - "direction": optional(random_vector()), - }, - ) - validate_setter( - "match_z", - lambda mob, kwargs: mob.match_z( - create_another(mob, kwargs.pop("points")), **kwargs - ), - lambda: { - "points": random_points(size=int(random_number(1, 100))), - "direction": optional(random_vector()), - }, - ) - validate_setter( - "move_to", - lambda mob, kwargs: mob.move_to(**kwargs), - lambda: { - "point_or_mobject": random_point(), - "aligned_edge": optional(random_vector()), - "coor_mask": optional(random_vector()), - }, - ) - validate_setter( - "next_to", - lambda mob, kwargs: mob.next_to(**kwargs), - lambda: { - "mobject_or_point": random_point(), - "direction": optional(random_vector()), - "buff": optional(random_number()), - "aligned_edge": optional(random_vector()), - "coor_mask": optional(random_vector()), - }, - ) - validate_setter( - "pose_at_angle", - lambda mob, kwargs: mob.pose_at_angle(**kwargs), - lambda: { - "about_point": optional(random_point()), - "about_edge": optional(random_vector()), - }, - ) - # TODO: reduce_across_dimension - validate_setter( - "rescale_to_fit", - lambda mob, kwargs: mob.rescale_to_fit(**kwargs), - lambda: { - "length": random_number(), - "dim": random_choice([0, 1, 2]), - "stretch": optional(random_choice([True, False])), - "about_point": optional(random_point()), - "about_edge": optional(random_vector()), - }, - ) - validate_setter( - "rotate", - lambda mob, kwargs: mob.rotate(**kwargs), - lambda: { - "angle": random_number(), - "axis": optional(random_vector()), - "about_edge": optional(random_vector()), - }, - ) - validate_setter( - "rotate_about_origin", - lambda mob, kwargs: mob.rotate_about_origin(**kwargs), - lambda: { - "angle": random_number(), - "axis": optional(random_vector()), - }, - ) - validate_setter( - "scale", - lambda mob, kwargs: mob.scale(**kwargs), - lambda: { - "scale_factor": random_number(), - "about_point": optional(random_point()), - "about_edge": optional(random_vector()), - }, - ) - validate_setter( - "scale_to_fit_depth", - lambda mob, kwargs: mob.scale_to_fit_depth(**kwargs), - lambda: { - "depth": random_number(), - "about_point": optional(random_point()), - "about_edge": optional(random_vector()), - }, - ) - validate_setter( - "scale_to_fit_height", - lambda mob, kwargs: mob.scale_to_fit_height(**kwargs), - lambda: { - "height": random_number(), - "about_point": optional(random_point()), - "about_edge": optional(random_vector()), - }, - ) - validate_setter( - "scale_to_fit_width", - lambda mob, kwargs: mob.scale_to_fit_width(**kwargs), - lambda: { - "width": random_number(), - "about_point": optional(random_point()), - "about_edge": optional(random_vector()), - }, - ) - validate_setter( - "set_coord", - lambda mob, kwargs: mob.set_coord(**kwargs), - lambda: { - "value": random_number(), - "dim": random_choice([0, 1, 2]), - "direction": optional(random_vector()), - }, - ) - validate_setter( - "set_x", - lambda mob, kwargs: mob.set_x(**kwargs), - lambda: { - "x": random_number(), - "direction": optional(random_vector()), - }, - ) - validate_setter( - "set_y", - lambda mob, kwargs: mob.set_y(**kwargs), - lambda: { - "y": random_number(), - "direction": optional(random_vector()), - }, - ) - validate_setter( - "set_z", - lambda mob, kwargs: mob.set_z(**kwargs), - lambda: { - "z": random_number(), - "direction": optional(random_vector()), - }, - ) - validate_setter( - "shift", - lambda mob, kwargs: mob.shift(kwargs["value"]), - lambda: { - "value": random_vector(), - }, - ) - validate_setter( - "shift_onto_screen", - lambda mob, kwargs: mob.shift_onto_screen(**kwargs), - lambda: { - "buff": random_number(), - }, - ) - validate_setter( - "stretch", - lambda mob, kwargs: mob.stretch(**kwargs), - lambda: { - "factor": random_number(), - "dim": random_choice([0, 1, 2]), - "about_point": optional(random_point()), - "about_edge": optional(random_vector()), - }, - ) - validate_setter( - "stretch_about_point", - lambda mob, kwargs: mob.stretch_about_point(**kwargs), - lambda: { - "factor": random_number(), - "dim": random_choice([0, 1, 2]), - "point": random_point(), - }, - ) - validate_setter( - "stretch_to_fit_depth", - lambda mob, kwargs: mob.stretch_to_fit_depth(**kwargs), - lambda: { - "depth": random_number(), - "about_point": optional(random_point()), - "about_edge": optional(random_vector()), - }, - ) - validate_setter( - "stretch_to_fit_height", - lambda mob, kwargs: mob.stretch_to_fit_height(**kwargs), - lambda: { - "height": random_number(), - "about_point": optional(random_point()), - "about_edge": optional(random_vector()), - }, - ) - validate_setter( - "stretch_to_fit_width", - lambda mob, kwargs: mob.stretch_to_fit_width(**kwargs), - lambda: { - "width": random_number(), - "about_point": optional(random_point()), - "about_edge": optional(random_vector()), - }, - ) - validate_setter( - "to_corner", - lambda mob, kwargs: mob.to_corner(**kwargs), - lambda: { - "corner": random_vector(), - "buff": random_number(), - }, - ) - validate_setter( - "to_edge", - lambda mob, kwargs: mob.to_edge(**kwargs), - lambda: { - "edge": optional(random_vector()), - "buff": optional(random_number()), - }, - ) - - validate_getter("width", lambda mob, _: mob.width) - validate_setter( - "width", - lambda mob, kwargs: setattr(mob, "width", kwargs["value"]), - lambda: { - "value": random_number(), - }, - ) - - print("Untested") - for name in UNTESTED: - if hasattr(Mobject, name): - print(f"\t{name}") - - -def validate_function( - name: str, - function: Callable[[Mobject | Positionable, dict[Any, Any]], Any], - validate: Callable[[Mobject, Positionable, Any, Any], None], - random_parameters: Callable[[], dict[Any, Any]], -) -> None: - global POINT_COUNTS, LOOPS_PER_POINT_COUNT - time_old, time_new = 0, 0 - - for point_count in POINT_COUNTS: - for _ in range(LOOPS_PER_POINT_COUNT): - points = random_points(size=point_count) - - mob_old = Mobject() - mob_old.points = points.copy() - mob_new = Positionable() - mob_new.points = points.copy() - - kwargs = random_parameters() - kwargs = {key: value for key, value in kwargs.items() if value is not None} - - start = time.perf_counter_ns() - result_old = function(mob_old, kwargs.copy()) - time_old += time.perf_counter_ns() - start - - start = time.perf_counter_ns() - result_new = function(mob_new, kwargs.copy()) - time_new += time.perf_counter_ns() - start - - try: - validate(mob_old, mob_new, result_old, result_new) - except AssertionError as e: - raise ValueError( - f""" - Point Count: {point_count} - Kwargs: {kwargs} - Points: {points} - Old Result: {result_old} - New Result: {result_new} - Old Points: {mob_old.points} - New Points: {mob_new.points} - """.replace(" ", "") - ) from e - - print( - f"\t{name.ljust(25)}\t{time_old / time_new:1.2f}x\t{time_old / 1e9:.2f}s\t{time_new / 1e9:.2f}s" - ) - if name in UNTESTED: - UNTESTED.remove(name) - - -def validate_setter( - name: str, - function: Callable[[Mobject | Positionable, dict[Any, Any]], Any], - create_kwargs: Callable[[], Any] = lambda: {}, -) -> None: - def validate( - mob_old: Mobject, - mob_new: Positionable, - result_old: Any, - result_new: Any, - ) -> None: - assert (result_old is None) == (result_new is None) - assert np.allclose(mob_old.points, mob_new.points) - - validate_function( - name=name, - function=function, - validate=validate, - random_parameters=create_kwargs, - ) - - -def validate_getter( - name: str, - function: Callable[[Mobject | Positionable, dict[Any, Any]], Any], - create_kwargs: Callable[[], Any] = lambda: {}, -) -> None: - def validate( - mob_old: Mobject, - mob_new: Positionable, - result_old: Any, - result_new: Any, - ) -> None: - assert np.allclose(result_old, result_new) - - validate_function( - name=name, - function=function, - validate=validate, - random_parameters=create_kwargs, - ) - - -def optional(value: Any, a: float = 0.9) -> Any | None: - return value if _RNG.uniform() < a else None - - -def random_number(low: float = -10, high: float = 10) -> float: - return cast(float, _RNG.uniform(low=low, high=high)) - - -def random_point(low: float = -25, high: float = 25) -> Point3D: - return _RNG.uniform(low=low, high=high, size=3) - - -def random_points(low: float = -25, high: float = 25, size: int = 1) -> np.ndarray: - return _RNG.uniform(low=low, high=high, size=(size, 3)) - - -def random_vector(low: float = -3, high: float = 3) -> Vector3D: - v = _RNG.uniform(low=low, high=high, size=3) - if random_number(0, 1) <= 0.5: - v = np.round(v) - return v - - -def random_choice(a: list[Any]) -> Any: - return _RNG.choice(a=a) - - -def create_another( - mob: Mobject | Positionable, - points: Point3D_Array, -) -> Any: - another = type(mob)() - another.points = points - return another - - -def dump_attributes() -> None: - seen: set[str] = set() - - for cls in [Mobject, VMobject, OpenGLMobject, OpenGLVMobject]: - assert isinstance(cls, type) - print(cls.__name__) - for name, attr in sorted(cls.__dict__.items()): - if ( - name in seen - or name.startswith("__") - or attr is getattr(cls.__base__, name, None) - ): - continue - print( - f"\t{'-+'[getattr(Positionable, name, None) is not getattr(Positionable.__base__, name, None)]} {name}" - ) - seen |= cls.__dict__.keys() - - print(Positionable.__name__) - for name, attr in Positionable.__dict__.items(): - if name.startswith("__") or attr is getattr(Positionable.__base__, name, None): - continue - print(f"\t* {name}", "(new)" if name not in seen else "") - - -if __name__ == "__main__": - main() diff --git a/manim/mobject/mobject.py b/manim/mobject/mobject.py index 6b61aadf91..54d81a2619 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: @@ -3311,27 +2401,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(): From 67209c3fe453cacd4be6227961813ac277b1e500 Mon Sep 17 00:00:00 2001 From: GniLudio <50866361+GniLudio@users.noreply.github.com> Date: Fri, 21 Aug 2026 23:58:03 +0200 Subject: [PATCH 15/66] Fix scale overrides --- manim/animation/growing.py | 1 + manim/mobject/abstract/positionable.py | 2 +- manim/mobject/geometry/line.py | 22 +++++++++++++++++++--- manim/mobject/logo.py | 23 ++++++++++++++++++----- manim/mobject/opengl/opengl_geometry.py | 12 ++++++++++-- manim/mobject/opengl/opengl_mobject.py | 2 +- manim/mobject/table.py | 15 +++++++++++++-- manim/mobject/text/typst_mobject.py | 1 - manim/mobject/types/vectorized_mobject.py | 5 +++-- 9 files changed, 66 insertions(+), 17 deletions(-) diff --git a/manim/animation/growing.py b/manim/animation/growing.py index 889de79fc0..f5e61e2f90 100644 --- a/manim/animation/growing.py +++ b/manim/animation/growing.py @@ -205,6 +205,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/positionable.py b/manim/mobject/abstract/positionable.py index d05a371ac6..a5832d782c 100644 --- a/manim/mobject/abstract/positionable.py +++ b/manim/mobject/abstract/positionable.py @@ -205,7 +205,7 @@ def scale( self, # TODO: Rename to `factor` scale_factor: float, - *, + scale_stroke: bool = False, about_point: Point3DLike | None = None, about_edge: Vector3DLike | None = None, ) -> Self: diff --git a/manim/mobject/geometry/line.py b/manim/mobject/geometry/line.py index b085cdb5a4..f5a5eca37a 100644 --- a/manim/mobject/geometry/line.py +++ b/manim/mobject/geometry/line.py @@ -607,7 +607,13 @@ 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( # pyright: ignore[reportIncompatibleMethodOverride] + 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 +645,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 +659,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..1068ba7f8c 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,13 @@ 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, + scale_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 @@ -200,8 +206,15 @@ def scale(self, scale_factor: float, **kwargs: Any) -> Self: self.scale_factor *= scale_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( + scale_factor, + scale_stroke, + about_point=about_point, + about_edge=about_edge, + ) + return super().scale( + 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/opengl/opengl_geometry.py b/manim/mobject/opengl/opengl_geometry.py index 6028de1266..3ceac285b4 100644 --- a/manim/mobject/opengl/opengl_geometry.py +++ b/manim/mobject/opengl/opengl_geometry.py @@ -785,8 +785,16 @@ def put_start_and_end_on(self, start: Point3DLike, end: Point3DLike) -> Self: self.set_points_by_ends(start, end, buff=0, path_arc=self.path_arc) return self - def scale(self, *args: Any, **kwargs: Any) -> Self: - super().scale(*args, **kwargs) + def scale( + self, + scale_factor: float, + scale_stroke: bool = False, + about_point: Point3DLike | None = None, + about_edge: Point3DLike | None = ORIGIN, + ) -> Self: + super().scale( + scale_factor, scale_stroke, about_point=about_edge, about_edge=about_edge + ) self.reset_points_around_ends() return self diff --git a/manim/mobject/opengl/opengl_mobject.py b/manim/mobject/opengl/opengl_mobject.py index 8a8714bba7..a161bd5963 100644 --- a/manim/mobject/opengl/opengl_mobject.py +++ b/manim/mobject/opengl/opengl_mobject.py @@ -1630,9 +1630,9 @@ def shift(self, vector: Vector3DLike) -> Self: def scale( self, scale_factor: float, + scale_stroke: bool = False, about_point: Point3DLike | None = None, about_edge: Point3DLike | None = ORIGIN, - **_kwargs: object, ) -> Self: r"""Scale the size by a factor. diff --git a/manim/mobject/table.py b/manim/mobject/table.py index ca4c96a7f8..2a5f324140 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,22 @@ def construct(self): return AnimationGroup(*animations, lag_ratio=lag_ratio) def scale( - self, scale_factor: float, scale_stroke: bool = False, **kwargs: Any + self, + scale_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) + super().scale( + 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..d4d0c4ddaa 100644 --- a/manim/mobject/text/typst_mobject.py +++ b/manim/mobject/text/typst_mobject.py @@ -294,7 +294,6 @@ def scale( self, scale_factor: float, scale_stroke: bool = False, - *, about_point: np.ndarray | None = None, about_edge: np.ndarray | None = None, ) -> Self: diff --git a/manim/mobject/types/vectorized_mobject.py b/manim/mobject/types/vectorized_mobject.py index 791b28bf5a..03a9cdd4f8 100644 --- a/manim/mobject/types/vectorized_mobject.py +++ b/manim/mobject/types/vectorized_mobject.py @@ -485,7 +485,6 @@ def scale( self, scale_factor: float, scale_stroke: bool = False, - *, about_point: Point3DLike | None = None, about_edge: Vector3DLike | None = None, ) -> Self: @@ -551,7 +550,9 @@ def construct(self): background=True, family=False, ) - super().scale(scale_factor, about_point=about_point, about_edge=about_edge) + super().scale( + 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: From 51669898577d6ebb5bdb6914bdbab6b4bb6d45bf Mon Sep 17 00:00:00 2001 From: GniLudio <50866361+GniLudio@users.noreply.github.com> Date: Sat, 22 Aug 2026 00:01:06 +0200 Subject: [PATCH 16/66] Remove type: ignore --- manim/mobject/geometry/line.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manim/mobject/geometry/line.py b/manim/mobject/geometry/line.py index f5a5eca37a..66d68a4459 100644 --- a/manim/mobject/geometry/line.py +++ b/manim/mobject/geometry/line.py @@ -607,7 +607,7 @@ def __init__( self.add_tip(tip_shape=tip_shape) self._set_stroke_width_from_length() - def scale( # pyright: ignore[reportIncompatibleMethodOverride] + def scale( self, factor: float, scale_tips: bool = False, From 1c4392f55174f38faf8699136997f895a18880ef Mon Sep 17 00:00:00 2001 From: GniLudio <50866361+GniLudio@users.noreply.github.com> Date: Sat, 22 Aug 2026 00:01:53 +0200 Subject: [PATCH 17/66] Fix codeql --- manim/animation/growing.py | 3 +-- manim/mobject/geometry/line.py | 14 +++++++------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/manim/animation/growing.py b/manim/animation/growing.py index f5e61e2f90..b3de6fa622 100644 --- a/manim/animation/growing.py +++ b/manim/animation/growing.py @@ -205,8 +205,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) + start_arrow.scale(0, scale_stroke=True, about_point=self.point) if self.point_color: start_arrow.set_color(self.point_color) return start_arrow diff --git a/manim/mobject/geometry/line.py b/manim/mobject/geometry/line.py index 66d68a4459..614554fe3d 100644 --- a/manim/mobject/geometry/line.py +++ b/manim/mobject/geometry/line.py @@ -609,8 +609,8 @@ def __init__( def scale( self, - factor: float, - scale_tips: bool = False, + scale_factor: float, + scale_stroke: bool = False, about_point: Point3DLike | None = None, about_edge: Vector3DLike | None = None, ) -> Self: @@ -644,10 +644,10 @@ def scale( if self.get_length() == 0: return self - if scale_tips: + if scale_stroke: super().scale( - factor, - scale_tips, + scale_factor, + scale_stroke, about_point=about_point, about_edge=about_edge, ) @@ -660,8 +660,8 @@ def scale( old_tips = self.pop_tips() super().scale( - factor, - scale_tips, + scale_factor, + scale_stroke, about_point=about_point, about_edge=about_edge, ) From 6a1d198411bae6324d3c36c4e722ab3d4279394f Mon Sep 17 00:00:00 2001 From: GniLudio <50866361+GniLudio@users.noreply.github.com> Date: Sat, 22 Aug 2026 10:31:21 +0200 Subject: [PATCH 18/66] Keyword-only args and categorize methods --- manim/mobject/abstract/positionable.py | 354 +++++++++++++++---------- 1 file changed, 220 insertions(+), 134 deletions(-) diff --git a/manim/mobject/abstract/positionable.py b/manim/mobject/abstract/positionable.py index a5832d782c..4e6b3f1879 100644 --- a/manim/mobject/abstract/positionable.py +++ b/manim/mobject/abstract/positionable.py @@ -24,49 +24,83 @@ Point3DLike_Array, Vector3DLike, ) +from manim.utils.deprecation import deprecated from manim.utils.space_ops import rotation_matrix +if False: + # DISABLES DEPRECATED WARNING FOR TESTING + def deprecated(*args: Any, **kwargs: Any) -> Callable[..., Any]: # type: ignore[no-redef] + def wrapper(func: Callable[..., Any]) -> Callable[..., Any]: + return func + + return wrapper + class Positionable: """A positionable object. + ### Basics + - points + - (get|set)_points + - get_points_defining_boundary ### Applying Functions - * get_family - * apply_to_family - * apply_array_function - * apply_function - * apply_complex_function + - get_family + - apply_to_family + - apply_array_function + - apply_function + - apply_complex_function ### Transformations - * apply_matrix - * translate - * rotate - * scale - * stretch + - apply_matrix + - translate + - rotate + - scale + - stretch ### General - * get_bounding_box - * (get|set)_position - * (get|set)_(center|left|right|bottom|top|nadir|zenith) - * (get|set)_coord - * (get|set)_(x|y|z) - * (get|set)_dim_size - * (get|set)_(width|height|depth) + - get_bounding_box + - (get|set)_position + - (get|set)_(center|left|right|bottom|top|nadir|zenith) + - (get|set)_coord + - (get|set)_(x|y|z) + - (get|set)_dim_size + - (get|set)_(width|height|depth) ### Specialized - * align_on_border - * align_to - * center - * flip - * is_off_screen - * get_center_of_mass - * get_boundary_point - * next_to (TODO) - * shift_onto_screen - * scale_to_fit - * scale_to_fit_(width|height|depth) - * stretch_to_fit - * stretch_to_fit_(width|height|depth) - * to_corner - * to_edge - ### Aliases & Combability + - align_on_border + - align_to + - is_off_screen + - get_center_of_mass + - get_boundary_point + - next_to (TODO) + - shift_onto_screen + ### Aliases + - center = set_center(ORIGIN) + - move_to = set_position + - scale_to_fit = set_dim_size(stretch=False) + - scale_to_fit_(width|height|depth) + - stretch_to_fit = set_dim_size(stretch=True) + - stretch_to_fit_(width|height|depth) + - get_critical_point = get_position + - get_edge_center = get_position + - get_corner = get_position + - shift = translate + - to_corner = align_on_border + - to_edge = align_on_border + ### Deprecated + - apply_points_function_about_point + - apply_function_to_position + - flip + - length_over_dim + - get_extremum_along_dim + - match_points + - match_coord + - match_(x|y|z) + - match_dim_size + - match_(width|height|depth) + - pose_at_angle + - reduce_across_dimension + - rescale_to_fit + - rotate_about_origin + - stretch_about_point + - (width|height|depth) """ @@ -76,7 +110,10 @@ class Positionable: def get_points(self) -> Point3D_Array: return np.concat([mob.points for mob in self.get_family()]) - def set_points(self, points: "Point3DLike_Array | Positionable") -> Self: + def set_points( + self, + points: "Point3DLike_Array | Positionable", + ) -> Self: if isinstance(points, Positionable): for mob1, mob2 in zip(self.get_family(), points.get_family(), strict=False): mob1.set_points(mob2.points.copy()) @@ -95,6 +132,7 @@ def get_family(self) -> Iterable["Positionable"]: def apply_to_family( self, function: Callable[["Positionable"], Any], + *, only_with_points: bool = True, ) -> Self: for mob in self.get_family(): @@ -106,6 +144,7 @@ def apply_to_family( def apply_array_function( self, function: Callable[[Point3D_Array], Point3D_Array], + *, about_point: Point3DLike | None = None, about_edge: Vector3DLike | None = None, ) -> Self: @@ -126,6 +165,7 @@ def apply(mob: Positionable) -> None: def apply_function( self, function: Callable[[Point3D], Point3D], + *, about_point: Point3DLike | None = None, about_edge: Vector3DLike | None = None, ) -> Self: @@ -164,6 +204,7 @@ def apply(point: Point3D) -> Point3D: def apply_matrix( self, matrix: MatrixMN, + *, about_point: Point3DLike | None = None, about_edge: Vector3DLike | None = None, ) -> Self: @@ -190,6 +231,7 @@ def rotate( self, angle: float, axis: Vector3DLike = OUT, + *, about_point: Point3DLike | None = None, about_edge: Vector3DLike | None = None, ) -> Self: @@ -205,7 +247,7 @@ def scale( self, # TODO: Rename to `factor` scale_factor: float, - scale_stroke: bool = False, + *, about_point: Point3DLike | None = None, about_edge: Vector3DLike | None = None, ) -> Self: @@ -243,7 +285,10 @@ def get_bounding_box(self) -> tuple[Point3D, Point3D]: maxs = points.max(axis=0) return (mins, maxs) - def get_position(self, direction: Vector3DLike = ORIGIN) -> Point3D: + def get_position( + self, + direction: Vector3DLike = ORIGIN, + ) -> Point3D: direction = np.sign(direction) mins, maxs = self.get_bounding_box() mids = (mins + maxs) / 2 @@ -252,6 +297,7 @@ def get_position(self, direction: Vector3DLike = ORIGIN) -> Point3D: def set_position( self, point: "Point3DLike | Positionable", + *, aligned_edge: Vector3DLike = ORIGIN, coor_mask: Vector3DLike = np.array([1, 1, 1]), ) -> Self: @@ -267,6 +313,7 @@ def get_center(self) -> Point3D: def set_center( self, center: "Point3DLike | Positionable", + *, coor_mask: Vector3DLike = np.array([1, 1, 1]), ) -> Self: return self.set_position(point=center, aligned_edge=ORIGIN, coor_mask=coor_mask) @@ -277,6 +324,7 @@ def get_left(self) -> Point3D: def set_left( self, left: "Point3DLike | Positionable", + *, coor_mask: Vector3DLike = np.array([1, 1, 1]), ) -> Self: return self.set_position(point=left, aligned_edge=LEFT, coor_mask=coor_mask) @@ -287,6 +335,7 @@ def get_right(self) -> Point3D: def set_right( self, right: "Point3DLike | Positionable", + *, coor_mask: Vector3DLike = np.array([1, 1, 1]), ) -> Self: return self.set_position(point=right, aligned_edge=RIGHT, coor_mask=coor_mask) @@ -297,6 +346,7 @@ def get_bottom(self) -> Point3D: def set_bottom( self, bottom: "Point3DLike | Positionable", + *, coor_mask: Vector3DLike = np.array([1, 1, 1]), ) -> Self: return self.set_position(point=bottom, aligned_edge=DOWN, coor_mask=coor_mask) @@ -307,6 +357,7 @@ def get_top(self) -> Point3D: def set_top( self, top: "Point3DLike | Positionable", + *, coor_mask: Vector3DLike = np.array([1, 1, 1]), ) -> Self: return self.set_position(point=top, aligned_edge=UP, coor_mask=coor_mask) @@ -317,6 +368,7 @@ def get_nadir(self) -> Point3D: def set_nadir( self, nadir: "Point3DLike | Positionable", + *, coor_mask: Vector3DLike = np.array([1, 1, 1]), ) -> Self: return self.set_position(point=nadir, aligned_edge=IN, coor_mask=coor_mask) @@ -327,6 +379,7 @@ def get_zenith(self) -> Point3D: def set_zenith( self, zenith: "Point3DLike | Positionable", + *, coor_mask: Vector3DLike = np.array([1, 1, 1]), ) -> Self: return self.set_position(point=zenith, aligned_edge=OUT, coor_mask=coor_mask) @@ -391,6 +444,7 @@ def set_dim_size( self, size: "float | Positionable", dim: int, + *, stretch: bool = False, about_point: Point3DLike | None = None, about_edge: Vector3DLike | None = None, @@ -423,6 +477,7 @@ def get_width(self) -> float: def set_width( self, width: "float | Positionable", + *, stretch: bool = False, about_point: Point3DLike | None = None, about_edge: Vector3DLike | None = None, @@ -441,6 +496,7 @@ def get_height(self) -> float: def set_height( self, height: "float | Positionable", + *, stretch: bool = False, about_point: Point3DLike | None = None, about_edge: Vector3DLike | None = None, @@ -459,6 +515,7 @@ def get_depth(self) -> float: def set_depth( self, depth: "float | Positionable", + *, stretch: bool = False, about_point: Point3DLike | None = None, about_edge: Vector3DLike | None = None, @@ -472,9 +529,11 @@ def set_depth( ) ### SPECIALIZED ### + def align_on_border( self, direction: Vector3DLike, + *, buff: float = DEFAULT_MOBJECT_TO_EDGE_BUFFER, ) -> Self: frame = (config.frame_x_radius, config.frame_y_radius, 0) @@ -496,22 +555,6 @@ def align_to( target = np.where(direction == 0, source, mobject_or_point) return self.shift(target - source) - def center(self) -> Self: - return self.set_center(ORIGIN) - - def flip( - self, - axis: Vector3DLike = UP, - about_point: Point3DLike | None = None, - about_edge: Vector3DLike | None = None, - ) -> Self: - return self.rotate( - TAU / 2, - axis, - about_point=about_point, - about_edge=about_edge, - ) - def is_off_screen(self) -> bool: mins, maxs = self.get_bounding_box() return ( # type: ignore[return-value] @@ -532,8 +575,13 @@ def get_boundary_point(self, direction: Vector3DLike) -> Point3D: index = np.argmax(points.dot(direction)) return points[index] - def shift_onto_screen(self, buff: float = DEFAULT_MOBJECT_TO_EDGE_BUFFER) -> Self: - space_lengths = [config["frame_x_radius"], config["frame_y_radius"]] + def shift_onto_screen( + self, + *, + buff: float = DEFAULT_MOBJECT_TO_EDGE_BUFFER, + ) -> Self: + # TODO: Simplify implementation + space_lengths = [config.frame_x_radius, config.frame_y_radius] for vect in UP, DOWN, LEFT, RIGHT: dim = np.argmax(np.abs(vect)) max_val = space_lengths[dim] - buff @@ -542,10 +590,32 @@ def shift_onto_screen(self, buff: float = DEFAULT_MOBJECT_TO_EDGE_BUFFER) -> Sel self.to_edge(vect, buff=buff) return self + ### ALIASES ### + shift = translate + get_critical_point = get_position + get_edge_center = get_position + get_corner = get_position + + def center(self) -> Self: + return self.set_center(ORIGIN) + + def move_to( + self, + point_or_mobject: "Point3DLike | Positionable", + aligned_edge: Vector3DLike = ORIGIN, + coor_mask: Vector3DLike = np.array([1, 1, 1]), + ) -> Self: + return self.set_position( + point=point_or_mobject, + aligned_edge=aligned_edge, + coor_mask=coor_mask, + ) + def scale_to_fit( self, size: float, dim: int, + *, about_point: Point3DLike | None = None, about_edge: Vector3DLike | None = None, ) -> Self: @@ -560,6 +630,7 @@ def scale_to_fit( def scale_to_fit_width( self, width: float, + *, about_point: Point3DLike | None = None, about_edge: Vector3DLike | None = None, ) -> Self: @@ -573,6 +644,7 @@ def scale_to_fit_width( def scale_to_fit_height( self, height: float, + *, about_point: Point3DLike | None = None, about_edge: Vector3DLike | None = None, ) -> Self: @@ -586,6 +658,7 @@ def scale_to_fit_height( def scale_to_fit_depth( self, depth: float, + *, about_point: Point3DLike | None = None, about_edge: Vector3DLike | None = None, ) -> Self: @@ -664,133 +737,146 @@ def to_edge( ) -> Self: return self.align_on_border(direction=edge, buff=buff) - ### ALIASES & COMBABILITY ### - match_points = set_points - apply_points_function_about_point = apply_array_function - shift = translate - get_critical_point = get_position - match_coord = set_coord - match_x = set_x - match_y = set_y - match_z = set_z - match_dim_size = set_dim_size - match_width = set_width - match_height = set_height - match_depth = set_depth - length_over_dim = get_dim_size - get_edge_center = get_position - get_corner = get_position - - def move_to( + ### DEPRECATED ### + + apply_points_function_about_point = deprecated(apply_array_function) + length_over_dim = deprecated(get_dim_size) + match_points = deprecated(set_points, replacement="set_points") + match_coord = deprecated(set_coord, message="set_coord") + match_x = deprecated(set_x, message="set_x") + match_y = deprecated(set_y, message="set_y") + match_z = deprecated(set_z, message="set_z") + match_dim_size = deprecated(set_dim_size, message="set_dim_size") + match_width = deprecated(set_width, message="set_width") + match_height = deprecated(set_height, message="set_height") + match_depth = deprecated(set_depth, message="set_depth") + + @deprecated(replacement="move_to(function(self.get_center()))") + def apply_function_to_position( self, - point_or_mobject: "Point3DLike | Positionable", - aligned_edge: Vector3DLike = ORIGIN, - coor_mask: Vector3DLike = np.array([1, 1, 1]), + function: Callable[[Point3D], Point3DLike], ) -> Self: - return self.set_position( - point=point_or_mobject, - aligned_edge=aligned_edge, - coor_mask=coor_mask, - ) + return self.move_to(function(self.get_center())) - def stretch_about_point(self, factor: float, dim: int, point: Point3DLike) -> Self: - return self.stretch(factor=factor, dim=dim, about_point=point) + @deprecated() + def get_extremum_along_dim( + self, + dim: int = 0, + key: int = 0, + ) -> float: + points = self.get_points() + if len(points) == 0: + return 0 + values = 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 rescale_to_fit( + @deprecated() + def reduce_across_dimension( self, - length: "float | Positionable", + reduce_func: Callable[[Iterable[float]], float], dim: int, - stretch: bool = False, + ) -> float | None: + points = self.get_points() + if len(points) == 0: + return None + + return reduce_func(points[:, dim]) + + @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="rotate") + def pose_at_angle( + self, about_point: Point3DLike | None = None, about_edge: Vector3DLike | None = None, ) -> Self: - return self.set_dim_size( - size=length, - dim=dim, - stretch=stretch, + return self.rotate( + angle=TAU / 14, + axis=RIGHT + UP, about_point=about_point, about_edge=about_edge, ) @property + @deprecated(replacement="get_width") def width(self) -> float: return self.get_width() @width.setter + @deprecated(replacement="set_width") def width(self, value: float) -> None: self.set_width(width=value) @property + @deprecated(replacement="get_height") def height(self) -> float: return self.get_height() @height.setter + @deprecated(replacement="set_height") def height(self, value: float) -> None: self.set_height(height=value) @property + @deprecated(replacement="get_depth") def depth(self) -> float: return self.get_depth() @depth.setter + @deprecated(replacement="set_depth") def depth(self, value: float) -> None: self.set_depth(depth=value) - def pose_at_angle( + @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.rotate( - angle=TAU / 14, - axis=RIGHT + UP, + return self.set_dim_size( + size=length, + dim=dim, + stretch=stretch, about_point=about_point, about_edge=about_edge, ) - def rotate_about_origin( + @deprecated(replacement="rotate") + def flip( self, - angle: float, - axis: Vector3DLike = OUT, + axis: Vector3DLike = UP, + *, + about_point: Point3DLike | None = None, + about_edge: Vector3DLike | None = None, ) -> Self: return self.rotate( - angle=angle, - axis=axis, - about_point=ORIGIN, + TAU / 2, + axis, + about_point=about_point, + about_edge=about_edge, ) - def get_extremum_along_dim( - self, - dim: int = 0, - key: int = 0, - ) -> float: - points = self.get_points() - if len(points) == 0: - return 0 - values = 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 apply_function_to_position( - self, - function: Callable[[Point3D], Point3DLike], - ) -> Self: - return self.move_to(function(self.get_center())) - - def reduce_across_dimension( - self, - reduce_func: Callable[[Iterable[float]], float], - dim: int, - ) -> float | None: - points = self.get_points() - if len(points) == 0: - return None - - return reduce_func(points[:, dim]) + @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) From 4da832e6dd312a54945b9ce74200f43b977dd943 Mon Sep 17 00:00:00 2001 From: GniLudio <50866361+GniLudio@users.noreply.github.com> Date: Sat, 22 Aug 2026 10:34:56 +0200 Subject: [PATCH 19/66] Fix scale parameters --- manim/mobject/abstract/positionable.py | 1 + 1 file changed, 1 insertion(+) diff --git a/manim/mobject/abstract/positionable.py b/manim/mobject/abstract/positionable.py index 4e6b3f1879..e23568a740 100644 --- a/manim/mobject/abstract/positionable.py +++ b/manim/mobject/abstract/positionable.py @@ -247,6 +247,7 @@ def scale( self, # TODO: Rename to `factor` scale_factor: float, + scale_stroke: bool = False, *, about_point: Point3DLike | None = None, about_edge: Vector3DLike | None = None, From 0631d2018b5aa465e731dd8e243e89be35972953 Mon Sep 17 00:00:00 2001 From: GniLudio <50866361+GniLudio@users.noreply.github.com> Date: Sat, 22 Aug 2026 10:35:31 +0200 Subject: [PATCH 20/66] remove unreachable code --- manim/mobject/abstract/positionable.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/manim/mobject/abstract/positionable.py b/manim/mobject/abstract/positionable.py index e23568a740..2e894f78bd 100644 --- a/manim/mobject/abstract/positionable.py +++ b/manim/mobject/abstract/positionable.py @@ -27,14 +27,6 @@ from manim.utils.deprecation import deprecated from manim.utils.space_ops import rotation_matrix -if False: - # DISABLES DEPRECATED WARNING FOR TESTING - def deprecated(*args: Any, **kwargs: Any) -> Callable[..., Any]: # type: ignore[no-redef] - def wrapper(func: Callable[..., Any]) -> Callable[..., Any]: - return func - - return wrapper - class Positionable: """A positionable object. From da605ca9576836206539d5b560af3a1dfee786c1 Mon Sep 17 00:00:00 2001 From: GniLudio <50866361+GniLudio@users.noreply.github.com> Date: Sat, 22 Aug 2026 15:23:12 +0200 Subject: [PATCH 21/66] Fix scale parameters --- manim/mobject/geometry/line.py | 1 + manim/mobject/logo.py | 1 + manim/mobject/opengl/opengl_geometry.py | 1 + manim/mobject/opengl/opengl_mobject.py | 1 + manim/mobject/table.py | 1 + manim/mobject/text/typst_mobject.py | 1 + manim/mobject/types/vectorized_mobject.py | 1 + 7 files changed, 7 insertions(+) diff --git a/manim/mobject/geometry/line.py b/manim/mobject/geometry/line.py index 614554fe3d..9dff97e283 100644 --- a/manim/mobject/geometry/line.py +++ b/manim/mobject/geometry/line.py @@ -611,6 +611,7 @@ def scale( self, scale_factor: float, scale_stroke: bool = False, + *, about_point: Point3DLike | None = None, about_edge: Vector3DLike | None = None, ) -> Self: diff --git a/manim/mobject/logo.py b/manim/mobject/logo.py index 1068ba7f8c..beab9cc278 100644 --- a/manim/mobject/logo.py +++ b/manim/mobject/logo.py @@ -188,6 +188,7 @@ def scale( self, scale_factor: float, scale_stroke: bool = False, + *, about_point: Point3DLike | None = None, about_edge: Vector3DLike | None = None, ) -> Self: diff --git a/manim/mobject/opengl/opengl_geometry.py b/manim/mobject/opengl/opengl_geometry.py index 3ceac285b4..b6c788a834 100644 --- a/manim/mobject/opengl/opengl_geometry.py +++ b/manim/mobject/opengl/opengl_geometry.py @@ -789,6 +789,7 @@ def scale( self, scale_factor: float, scale_stroke: bool = False, + *, about_point: Point3DLike | None = None, about_edge: Point3DLike | None = ORIGIN, ) -> Self: diff --git a/manim/mobject/opengl/opengl_mobject.py b/manim/mobject/opengl/opengl_mobject.py index a161bd5963..4368680b9e 100644 --- a/manim/mobject/opengl/opengl_mobject.py +++ b/manim/mobject/opengl/opengl_mobject.py @@ -1631,6 +1631,7 @@ def scale( self, scale_factor: float, scale_stroke: bool = False, + *, about_point: Point3DLike | None = None, about_edge: Point3DLike | None = ORIGIN, ) -> Self: diff --git a/manim/mobject/table.py b/manim/mobject/table.py index 2a5f324140..dd6701df60 100644 --- a/manim/mobject/table.py +++ b/manim/mobject/table.py @@ -1001,6 +1001,7 @@ def scale( self, scale_factor: float, scale_stroke: bool = False, + *, about_point: Point3DLike | None = None, about_edge: Vector3DLike | None = None, ) -> Self: diff --git a/manim/mobject/text/typst_mobject.py b/manim/mobject/text/typst_mobject.py index d4d0c4ddaa..b2e5aeb793 100644 --- a/manim/mobject/text/typst_mobject.py +++ b/manim/mobject/text/typst_mobject.py @@ -294,6 +294,7 @@ def scale( self, scale_factor: float, scale_stroke: bool = False, + *, about_point: np.ndarray | None = None, about_edge: np.ndarray | None = None, ) -> Self: diff --git a/manim/mobject/types/vectorized_mobject.py b/manim/mobject/types/vectorized_mobject.py index 03a9cdd4f8..c4df3e5239 100644 --- a/manim/mobject/types/vectorized_mobject.py +++ b/manim/mobject/types/vectorized_mobject.py @@ -485,6 +485,7 @@ def scale( self, scale_factor: float, scale_stroke: bool = False, + *, about_point: Point3DLike | None = None, about_edge: Vector3DLike | None = None, ) -> Self: From 5c1e7501239b02aa4bb4bfbe694f2623b6853f00 Mon Sep 17 00:00:00 2001 From: GniLudio <50866361+GniLudio@users.noreply.github.com> Date: Sat, 22 Aug 2026 16:28:26 +0200 Subject: [PATCH 22/66] Update positionable.py --- manim/mobject/abstract/positionable.py | 154 ++++++++++++------------- 1 file changed, 73 insertions(+), 81 deletions(-) diff --git a/manim/mobject/abstract/positionable.py b/manim/mobject/abstract/positionable.py index 2e894f78bd..114a696668 100644 --- a/manim/mobject/abstract/positionable.py +++ b/manim/mobject/abstract/positionable.py @@ -24,7 +24,6 @@ Point3DLike_Array, Vector3DLike, ) -from manim.utils.deprecation import deprecated from manim.utils.space_ops import rotation_matrix @@ -65,6 +64,8 @@ class Positionable: - shift_onto_screen ### Aliases - center = set_center(ORIGIN) + - flip + - length_over_dim - move_to = set_position - scale_to_fit = set_dim_size(stretch=False) - scale_to_fit_(width|height|depth) @@ -73,26 +74,24 @@ class Positionable: - get_critical_point = get_position - get_edge_center = get_position - get_corner = get_position + - pose_at_angle - shift = translate - to_corner = align_on_border - to_edge = align_on_border + - (width|height|depth) ### Deprecated - apply_points_function_about_point - apply_function_to_position - - flip - - length_over_dim - get_extremum_along_dim - match_points - match_coord - match_(x|y|z) - match_dim_size - match_(width|height|depth) - - pose_at_angle - reduce_across_dimension - rescale_to_fit - rotate_about_origin - stretch_about_point - - (width|height|depth) """ @@ -588,10 +587,25 @@ def shift_onto_screen( get_critical_point = get_position get_edge_center = get_position get_corner = get_position + length_over_dim = get_dim_size def center(self) -> Self: return self.set_center(ORIGIN) + def flip( + self, + axis: Vector3DLike = UP, + *, + about_point: Point3DLike | None = None, + about_edge: Vector3DLike | None = None, + ) -> Self: + return self.rotate( + TAU / 2, + axis, + about_point=about_point, + about_edge=about_edge, + ) + def move_to( self, point_or_mobject: "Point3DLike | Positionable", @@ -604,6 +618,18 @@ def move_to( coor_mask=coor_mask, ) + def pose_at_angle( + self, + about_point: Point3DLike | None = None, + about_edge: Vector3DLike | None = None, + ) -> Self: + return self.rotate( + angle=TAU / 14, + axis=RIGHT + UP, + about_point=about_point, + about_edge=about_edge, + ) + def scale_to_fit( self, size: float, @@ -730,28 +756,52 @@ def to_edge( ) -> Self: return self.align_on_border(direction=edge, buff=buff) + @property + def width(self) -> float: + return self.get_width() + + @width.setter + def width(self, value: float) -> None: + self.set_width(width=value) + + @property + def height(self) -> float: + return self.get_height() + + @height.setter + def height(self, value: float) -> None: + self.set_height(height=value) + + @property + def depth(self) -> float: + return self.get_depth() + + @depth.setter + def depth(self, value: float) -> None: + self.set_depth(depth=value) + ### DEPRECATED ### - apply_points_function_about_point = deprecated(apply_array_function) - length_over_dim = deprecated(get_dim_size) - match_points = deprecated(set_points, replacement="set_points") - match_coord = deprecated(set_coord, message="set_coord") - match_x = deprecated(set_x, message="set_x") - match_y = deprecated(set_y, message="set_y") - match_z = deprecated(set_z, message="set_z") - match_dim_size = deprecated(set_dim_size, message="set_dim_size") - match_width = deprecated(set_width, message="set_width") - match_height = deprecated(set_height, message="set_height") - match_depth = deprecated(set_depth, message="set_depth") - - @deprecated(replacement="move_to(function(self.get_center()))") + apply_points_function_about_point = apply_array_function + length_over_dim = get_dim_size + match_points = set_points + match_coord = set_coord + match_x = set_x + match_y = set_y + match_z = set_z + match_dim_size = set_dim_size + match_width = set_width + match_height = set_height + match_depth = set_depth + + # @deprecated(replacement="move_to(function(self.get_center()))") def apply_function_to_position( self, function: Callable[[Point3D], Point3DLike], ) -> Self: return self.move_to(function(self.get_center())) - @deprecated() + # @deprecated() def get_extremum_along_dim( self, dim: int = 0, @@ -771,7 +821,7 @@ def get_extremum_along_dim( rv = np.max(values) return rv - @deprecated() + # @deprecated() def reduce_across_dimension( self, reduce_func: Callable[[Iterable[float]], float], @@ -783,7 +833,7 @@ def reduce_across_dimension( return reduce_func(points[:, dim]) - @deprecated(replacement="rotate") + # @deprecated(replacement="rotate") def rotate_about_origin( self, angle: float, @@ -795,50 +845,7 @@ def rotate_about_origin( about_point=ORIGIN, ) - @deprecated(replacement="rotate") - def pose_at_angle( - self, - about_point: Point3DLike | None = None, - about_edge: Vector3DLike | None = None, - ) -> Self: - return self.rotate( - angle=TAU / 14, - axis=RIGHT + UP, - about_point=about_point, - about_edge=about_edge, - ) - - @property - @deprecated(replacement="get_width") - def width(self) -> float: - return self.get_width() - - @width.setter - @deprecated(replacement="set_width") - def width(self, value: float) -> None: - self.set_width(width=value) - - @property - @deprecated(replacement="get_height") - def height(self) -> float: - return self.get_height() - - @height.setter - @deprecated(replacement="set_height") - def height(self, value: float) -> None: - self.set_height(height=value) - - @property - @deprecated(replacement="get_depth") - def depth(self) -> float: - return self.get_depth() - - @depth.setter - @deprecated(replacement="set_depth") - def depth(self, value: float) -> None: - self.set_depth(depth=value) - - @deprecated(replacement="set_dim_size") + # @deprecated(replacement="set_dim_size") def rescale_to_fit( self, length: "float | Positionable", @@ -855,21 +862,6 @@ def rescale_to_fit( about_edge=about_edge, ) - @deprecated(replacement="rotate") - def flip( - self, - axis: Vector3DLike = UP, - *, - about_point: Point3DLike | None = None, - about_edge: Vector3DLike | None = None, - ) -> Self: - return self.rotate( - TAU / 2, - axis, - about_point=about_point, - about_edge=about_edge, - ) - - @deprecated(replacement="stretch") + # @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) From a9fd91dcfce4681426976c3a487e4aace4d0ff1b Mon Sep 17 00:00:00 2001 From: GniLudio <50866361+GniLudio@users.noreply.github.com> Date: Sat, 22 Aug 2026 16:34:34 +0200 Subject: [PATCH 23/66] Remove redefined variable --- manim/mobject/abstract/positionable.py | 1 - 1 file changed, 1 deletion(-) diff --git a/manim/mobject/abstract/positionable.py b/manim/mobject/abstract/positionable.py index 114a696668..c1570eb9ea 100644 --- a/manim/mobject/abstract/positionable.py +++ b/manim/mobject/abstract/positionable.py @@ -783,7 +783,6 @@ def depth(self, value: float) -> None: ### DEPRECATED ### apply_points_function_about_point = apply_array_function - length_over_dim = get_dim_size match_points = set_points match_coord = set_coord match_x = set_x From d93595863116ef9a55526a100b9f29ba98b28532 Mon Sep 17 00:00:00 2001 From: GniLudio <50866361+GniLudio@users.noreply.github.com> Date: Sat, 22 Aug 2026 16:51:01 +0200 Subject: [PATCH 24/66] Rever line.py parameter name --- manim/mobject/geometry/line.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/manim/mobject/geometry/line.py b/manim/mobject/geometry/line.py index 9dff97e283..c9c745f0c3 100644 --- a/manim/mobject/geometry/line.py +++ b/manim/mobject/geometry/line.py @@ -610,7 +610,7 @@ def __init__( def scale( self, scale_factor: float, - scale_stroke: bool = False, + scale_tips: bool = False, *, about_point: Point3DLike | None = None, about_edge: Vector3DLike | None = None, @@ -645,10 +645,10 @@ def scale( if self.get_length() == 0: return self - if scale_stroke: + if scale_tips: super().scale( scale_factor, - scale_stroke, + scale_tips, about_point=about_point, about_edge=about_edge, ) @@ -662,7 +662,7 @@ def scale( super().scale( scale_factor, - scale_stroke, + scale_tips, about_point=about_point, about_edge=about_edge, ) From b7cb6dbb4a15d44327e155ffb658c817c32c2dae Mon Sep 17 00:00:00 2001 From: GniLudio <50866361+GniLudio@users.noreply.github.com> Date: Sat, 22 Aug 2026 16:54:17 +0200 Subject: [PATCH 25/66] Update growing.py --- manim/animation/growing.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/manim/animation/growing.py b/manim/animation/growing.py index b3de6fa622..f5e61e2f90 100644 --- a/manim/animation/growing.py +++ b/manim/animation/growing.py @@ -205,7 +205,8 @@ def __init__( def create_starting_mobject(self) -> Mobject | OpenGLMobject: start_arrow = self.mobject.copy() - start_arrow.scale(0, scale_stroke=True, about_point=self.point) + 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) return start_arrow From c467f4e6aba7331424e12c1e7ee93545739f0a0d Mon Sep 17 00:00:00 2001 From: GniLudio <50866361+GniLudio@users.noreply.github.com> Date: Sat, 22 Aug 2026 17:29:27 +0200 Subject: [PATCH 26/66] Fix import --- manim/animation/growing.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/manim/animation/growing.py b/manim/animation/growing.py index f5e61e2f90..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 From c06945ccb0fa981fac2e19c39f375d2fe8d8eb49 Mon Sep 17 00:00:00 2001 From: GniLudio <50866361+GniLudio@users.noreply.github.com> Date: Sat, 22 Aug 2026 18:08:19 +0200 Subject: [PATCH 27/66] Deprecate methods --- manim/mobject/abstract/positionable.py | 35 +++++++++++++------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/manim/mobject/abstract/positionable.py b/manim/mobject/abstract/positionable.py index c1570eb9ea..a70cc48512 100644 --- a/manim/mobject/abstract/positionable.py +++ b/manim/mobject/abstract/positionable.py @@ -24,6 +24,7 @@ Point3DLike_Array, Vector3DLike, ) +from manim.utils.deprecation import deprecated from manim.utils.space_ops import rotation_matrix @@ -782,25 +783,25 @@ def depth(self, value: float) -> None: ### DEPRECATED ### - apply_points_function_about_point = apply_array_function - match_points = set_points - match_coord = set_coord - match_x = set_x - match_y = set_y - match_z = set_z - match_dim_size = set_dim_size - match_width = set_width - match_height = set_height - match_depth = set_depth - - # @deprecated(replacement="move_to(function(self.get_center()))") + apply_points_function_about_point = deprecated(apply_array_function) + match_points = deprecated(set_points) + match_coord = deprecated(set_coord) + match_x = deprecated(set_x) + match_y = deprecated(set_y) + match_z = deprecated(set_z) + match_dim_size = deprecated(set_dim_size) + match_width = deprecated(set_width) + match_height = deprecated(set_height) + match_depth = deprecated(set_depth) + + @deprecated(replacement="move_to(function(self.get_center()))") def apply_function_to_position( self, function: Callable[[Point3D], Point3DLike], ) -> Self: return self.move_to(function(self.get_center())) - # @deprecated() + @deprecated() def get_extremum_along_dim( self, dim: int = 0, @@ -820,7 +821,7 @@ def get_extremum_along_dim( rv = np.max(values) return rv - # @deprecated() + @deprecated() def reduce_across_dimension( self, reduce_func: Callable[[Iterable[float]], float], @@ -832,7 +833,7 @@ def reduce_across_dimension( return reduce_func(points[:, dim]) - # @deprecated(replacement="rotate") + @deprecated(replacement="rotate") def rotate_about_origin( self, angle: float, @@ -844,7 +845,7 @@ def rotate_about_origin( about_point=ORIGIN, ) - # @deprecated(replacement="set_dim_size") + @deprecated(replacement="set_dim_size") def rescale_to_fit( self, length: "float | Positionable", @@ -861,6 +862,6 @@ def rescale_to_fit( about_edge=about_edge, ) - # @deprecated(replacement="stretch") + @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) From d7ba706497775d45ac59978f6581428246a4676c Mon Sep 17 00:00:00 2001 From: GniLudio <50866361+GniLudio@users.noreply.github.com> Date: Sat, 22 Aug 2026 18:30:34 +0200 Subject: [PATCH 28/66] Revert "Deprecate methods" This reverts commit c06945ccb0fa981fac2e19c39f375d2fe8d8eb49. --- manim/mobject/abstract/positionable.py | 35 +++++++++++++------------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/manim/mobject/abstract/positionable.py b/manim/mobject/abstract/positionable.py index a70cc48512..c1570eb9ea 100644 --- a/manim/mobject/abstract/positionable.py +++ b/manim/mobject/abstract/positionable.py @@ -24,7 +24,6 @@ Point3DLike_Array, Vector3DLike, ) -from manim.utils.deprecation import deprecated from manim.utils.space_ops import rotation_matrix @@ -783,25 +782,25 @@ def depth(self, value: float) -> None: ### DEPRECATED ### - apply_points_function_about_point = deprecated(apply_array_function) - match_points = deprecated(set_points) - match_coord = deprecated(set_coord) - match_x = deprecated(set_x) - match_y = deprecated(set_y) - match_z = deprecated(set_z) - match_dim_size = deprecated(set_dim_size) - match_width = deprecated(set_width) - match_height = deprecated(set_height) - match_depth = deprecated(set_depth) - - @deprecated(replacement="move_to(function(self.get_center()))") + apply_points_function_about_point = apply_array_function + match_points = set_points + match_coord = set_coord + match_x = set_x + match_y = set_y + match_z = set_z + match_dim_size = set_dim_size + match_width = set_width + match_height = set_height + match_depth = set_depth + + # @deprecated(replacement="move_to(function(self.get_center()))") def apply_function_to_position( self, function: Callable[[Point3D], Point3DLike], ) -> Self: return self.move_to(function(self.get_center())) - @deprecated() + # @deprecated() def get_extremum_along_dim( self, dim: int = 0, @@ -821,7 +820,7 @@ def get_extremum_along_dim( rv = np.max(values) return rv - @deprecated() + # @deprecated() def reduce_across_dimension( self, reduce_func: Callable[[Iterable[float]], float], @@ -833,7 +832,7 @@ def reduce_across_dimension( return reduce_func(points[:, dim]) - @deprecated(replacement="rotate") + # @deprecated(replacement="rotate") def rotate_about_origin( self, angle: float, @@ -845,7 +844,7 @@ def rotate_about_origin( about_point=ORIGIN, ) - @deprecated(replacement="set_dim_size") + # @deprecated(replacement="set_dim_size") def rescale_to_fit( self, length: "float | Positionable", @@ -862,6 +861,6 @@ def rescale_to_fit( about_edge=about_edge, ) - @deprecated(replacement="stretch") + # @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) From 0836fb3dac3771648c5667479fd060c192f8796b Mon Sep 17 00:00:00 2001 From: GniLudio <50866361+GniLudio@users.noreply.github.com> Date: Sat, 22 Aug 2026 18:30:50 +0200 Subject: [PATCH 29/66] Remove set_points override --- manim/mobject/types/vectorized_mobject.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/manim/mobject/types/vectorized_mobject.py b/manim/mobject/types/vectorized_mobject.py index c4df3e5239..11a2a21a41 100644 --- a/manim/mobject/types/vectorized_mobject.py +++ b/manim/mobject/types/vectorized_mobject.py @@ -795,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, From b72417d6e09b4240b0b3e212e2763e6fd764f080 Mon Sep 17 00:00:00 2001 From: GniLudio <50866361+GniLudio@users.noreply.github.com> Date: Sat, 22 Aug 2026 19:20:18 +0200 Subject: [PATCH 30/66] Simplify align_on_border --- manim/mobject/abstract/positionable.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/manim/mobject/abstract/positionable.py b/manim/mobject/abstract/positionable.py index c1570eb9ea..b536aaa3d8 100644 --- a/manim/mobject/abstract/positionable.py +++ b/manim/mobject/abstract/positionable.py @@ -55,12 +55,12 @@ class Positionable: - (get|set)_dim_size - (get|set)_(width|height|depth) ### Specialized - - align_on_border - align_to + - align_on_border + - next_to (TODO) - is_off_screen - get_center_of_mass - get_boundary_point - - next_to (TODO) - shift_onto_screen ### Aliases - center = set_center(ORIGIN) @@ -529,11 +529,8 @@ def align_on_border( buff: float = DEFAULT_MOBJECT_TO_EDGE_BUFFER, ) -> Self: frame = (config.frame_x_radius, config.frame_y_radius, 0) - target_point = np.sign(direction) * frame - point_to_align = self.get_critical_point(direction=direction) - shift_val = target_point - point_to_align - buff * np.asarray(direction) - shift_val = shift_val * abs(np.sign(direction)) - return self.shift(shift_val) + target = np.sign(direction) * frame - buff * np.asarray(direction) + return self.align_to(target, direction=direction) def align_to( self, From b5df56a2f2c74bd5f84e8d4e78c92b8cfa1aa7af Mon Sep 17 00:00:00 2001 From: GniLudio <50866361+GniLudio@users.noreply.github.com> Date: Sat, 22 Aug 2026 19:28:14 +0200 Subject: [PATCH 31/66] Adjust keyword-only args --- manim/mobject/abstract/positionable.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/manim/mobject/abstract/positionable.py b/manim/mobject/abstract/positionable.py index b536aaa3d8..4aaaed74ce 100644 --- a/manim/mobject/abstract/positionable.py +++ b/manim/mobject/abstract/positionable.py @@ -742,6 +742,7 @@ def stretch_to_fit_depth( def to_corner( self, corner: Vector3DLike = DL, + *, buff: float = DEFAULT_MOBJECT_TO_EDGE_BUFFER, ) -> Self: return self.align_on_border(direction=corner, buff=buff) @@ -749,6 +750,7 @@ def to_corner( def to_edge( self, edge: Vector3DLike = LEFT, + *, buff: float = DEFAULT_MOBJECT_TO_EDGE_BUFFER, ) -> Self: return self.align_on_border(direction=edge, buff=buff) From cace5f916c6dca5c2b7dae89d488b47b094b964f Mon Sep 17 00:00:00 2001 From: GniLudio Date: Mon, 24 Aug 2026 12:03:28 +0200 Subject: [PATCH 32/66] Add docstrings --- manim/mobject/abstract/positionable.py | 876 ++++++++++++++++++++++++- 1 file changed, 864 insertions(+), 12 deletions(-) diff --git a/manim/mobject/abstract/positionable.py b/manim/mobject/abstract/positionable.py index 4aaaed74ce..9372fec9bc 100644 --- a/manim/mobject/abstract/positionable.py +++ b/manim/mobject/abstract/positionable.py @@ -50,7 +50,7 @@ class Positionable: - get_bounding_box - (get|set)_position - (get|set)_(center|left|right|bottom|top|nadir|zenith) - - (get|set)_coord + - (get|set)_coordinate - (get|set)_(x|y|z) - (get|set)_dim_size - (get|set)_(width|height|depth) @@ -71,10 +71,12 @@ class Positionable: - scale_to_fit_(width|height|depth) - stretch_to_fit = set_dim_size(stretch=True) - stretch_to_fit_(width|height|depth) + - get_coord = get_coordinate - get_critical_point = get_position - get_edge_center = get_position - get_corner = get_position - pose_at_angle + - set_coordinate = set_coord - shift = translate - to_corner = align_on_border - to_edge = align_on_border @@ -99,12 +101,33 @@ class Positionable: points: Point3D_Array = np.array([]) def get_points(self) -> Point3D_Array: + """Returns all points. + + Returns + ------- + Point3D_Array + All points. + """ return np.concat([mob.points for mob in self.get_family()]) 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. + """ if isinstance(points, Positionable): for mob1, mob2 in zip(self.get_family(), points.get_family(), strict=False): mob1.set_points(mob2.points.copy()) @@ -113,11 +136,19 @@ def set_points( return self def get_points_defining_boundary(self) -> Point3D_Array: + """Returns all points defining the boundary. + + Returns + ------- + Point3D_Array + The points defining the boundary. + """ return self.get_points() ### APPLYING FUNCTIONS ### def get_family(self) -> Iterable["Positionable"]: + """Returns all family members recursively.""" yield self def apply_to_family( @@ -126,6 +157,20 @@ def apply_to_family( *, 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. + """ for mob in self.get_family(): if only_with_points and len(mob.points) == 0: continue @@ -139,6 +184,22 @@ def apply_array_function( 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. + """ if about_point is None: if about_edge is None: about_edge = ORIGIN @@ -160,6 +221,22 @@ def apply_function( 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. + """ if about_point is None and about_edge is None: about_point = ORIGIN @@ -179,6 +256,23 @@ def apply_complex_function( 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. + """ + def apply(point: Point3D) -> Point3D: x, y, z = point xy_complex = function(complex(x, y)) @@ -199,6 +293,22 @@ def apply_matrix( 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) @@ -213,6 +323,19 @@ def apply_matrix( return self def translate(self, vector: Vector3DLike) -> Self: + """Applies a translation. + + Parameters + ---------- + vector : Vector3DLike + The vector. + + Returns + ------- + Self + The object itself. + """ + def function(mob: Positionable) -> None: mob.points += vector @@ -226,6 +349,24 @@ def rotate( 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 + + Returns + ------- + Self + _description_ + """ if about_point is None and about_edge is None: about_edge = ORIGIN return self.apply_matrix( @@ -238,11 +379,30 @@ def scale( self, # TODO: Rename to `factor` scale_factor: float, + # TODO: Remove this? scale_stroke: bool = False, *, about_point: Point3DLike | None = None, about_edge: Vector3DLike | None = None, ) -> Self: + """Applies a uniform scaling. + + Parameters + ---------- + scale_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. + """ return self.apply_array_function( function=lambda points: points.__imul__(scale_factor), about_point=about_point, @@ -257,6 +417,25 @@ def stretch( 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. + """ + def function(points: Point3D_Array) -> Point3D_Array: points[:, dim] *= factor return points @@ -270,6 +449,13 @@ def function(points: Point3D_Array) -> Point3D_Array: ### GENERAL ### 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)) @@ -281,6 +467,18 @@ def get_position( self, direction: Vector3DLike = ORIGIN, ) -> Point3D: + """The position. + + Parameters + ---------- + direction : Vector3DLike, optional + TODO, by default ORIGIN + + Returns + ------- + Point3D + The position. + """ direction = np.sign(direction) mins, maxs = self.get_bounding_box() mids = (mins + maxs) / 2 @@ -291,8 +489,25 @@ def set_position( point: "Point3DLike | Positionable", *, aligned_edge: Vector3DLike = ORIGIN, + # TODO: Remove this? coor_mask: Vector3DLike = np.array([1, 1, 1]), ) -> Self: + """Sets the position. + + Parameters + ---------- + point : Point3DLike | Positionable + The point. + aligned_edge : Vector3DLike, optional + Which edge to position., by default ORIGIN + coor_mask : Vector3DLike, optional + TODO, by default [1, 1, 1] + + Returns + ------- + Self + The object itself. + """ if isinstance(point, Positionable): point = point.get_position(direction=aligned_edge) current = self.get_position(direction=aligned_edge) @@ -300,6 +515,13 @@ def set_position( return self.translate(vector=vector) def get_center(self) -> Point3D: + """Returns the center position. + + Returns + ------- + Point3D + The center position. + """ return self.get_position(direction=ORIGIN) def set_center( @@ -308,9 +530,30 @@ def set_center( *, coor_mask: Vector3DLike = np.array([1, 1, 1]), ) -> Self: + """Sets the center position. + + Parameters + ---------- + center : Point3DLike | Positionable + The center position. + coor_mask : Vector3DLike, optional + TODO, by default np.array([1, 1, 1]) + + Returns + ------- + Self + The object itself. + """ return self.set_position(point=center, aligned_edge=ORIGIN, coor_mask=coor_mask) def get_left(self) -> Point3D: + """Returns the left position. + + Returns + ------- + Point3D + The left position. + """ return self.get_position(direction=LEFT) def set_left( @@ -319,9 +562,24 @@ def set_left( *, coor_mask: Vector3DLike = np.array([1, 1, 1]), ) -> Self: + """Sets the left position. + + Parameters + ---------- + left : Point3DLike | Positionable + The left position. + coor_mask : Vector3DLike, optional + TODO, by default np.array([1, 1, 1]) + + Returns + ------- + Self + The object itself. + """ return self.set_position(point=left, aligned_edge=LEFT, coor_mask=coor_mask) def get_right(self) -> Point3D: + """Returns the right position.""" return self.get_position(direction=RIGHT) def set_right( @@ -330,9 +588,30 @@ def set_right( *, coor_mask: Vector3DLike = np.array([1, 1, 1]), ) -> Self: + """Sets the right position. + + Parameters + ---------- + right : Point3DLike | Positionable + The right position. + coor_mask : Vector3DLike, optional + TODO, by default np.array([1, 1, 1]) + + Returns + ------- + Self + The object itself. + """ return self.set_position(point=right, aligned_edge=RIGHT, coor_mask=coor_mask) def get_bottom(self) -> Point3D: + """Returns the bottom position. + + Returns + ------- + Point3D + The bottom position. + """ return self.get_position(direction=DOWN) def set_bottom( @@ -341,9 +620,30 @@ def set_bottom( *, coor_mask: Vector3DLike = np.array([1, 1, 1]), ) -> Self: + """Sets the bottom position. + + Parameters + ---------- + bottom : Point3DLike | Positionable + The bottom position. + coor_mask : Vector3DLike, optional + TODO, by default np.array([1, 1, 1]) + + Returns + ------- + Self + The object itself. + """ return self.set_position(point=bottom, aligned_edge=DOWN, coor_mask=coor_mask) def get_top(self) -> Point3D: + """Returns the top position. + + Returns + ------- + Point3D + The top position. + """ return self.get_position(direction=UP) def set_top( @@ -352,9 +652,30 @@ def set_top( *, coor_mask: Vector3DLike = np.array([1, 1, 1]), ) -> Self: + """Sets the top position. + + Parameters + ---------- + top : Point3DLike | Positionable + The top position. + coor_mask : Vector3DLike, optional + TODO, by default np.array([1, 1, 1]) + + Returns + ------- + Self + The object itself. + """ return self.set_position(point=top, aligned_edge=UP, coor_mask=coor_mask) def get_nadir(self) -> Point3D: + """Returns the nadir position. + + Returns + ------- + Point3D + The nadir position. + """ return self.get_position(direction=IN) def set_nadir( @@ -363,9 +684,30 @@ def set_nadir( *, coor_mask: Vector3DLike = np.array([1, 1, 1]), ) -> Self: + """Sets the nadir position. + + Parameters + ---------- + nadir : Point3DLike | Positionable + The nadir position. + coor_mask : Vector3DLike, optional + TODO, by default np.array([1, 1, 1]) + + Returns + ------- + Self + The object itself. + """ return self.set_position(point=nadir, aligned_edge=IN, coor_mask=coor_mask) def get_zenith(self) -> Point3D: + """Returns the zenith position. + + Returns + ------- + Point3D + The zenith position. + """ return self.get_position(direction=OUT) def set_zenith( @@ -374,13 +716,41 @@ def set_zenith( *, coor_mask: Vector3DLike = np.array([1, 1, 1]), ) -> Self: + """Sets the zenith position. + + Parameters + ---------- + zenith : Point3DLike | Positionable + The zenith position. + coor_mask : Vector3DLike, optional + TODO, by default np.array([1, 1, 1]) + + Returns + ------- + Self + The object itself. + """ return self.set_position(point=zenith, aligned_edge=OUT, coor_mask=coor_mask) - def get_coord( + def get_coordinate( self, dim: int, direction: Vector3DLike = ORIGIN, ) -> float: + """Returns the coordinate of a dimension. + + Parameters + ---------- + dim : int + The dimension. + direction : Vector3DLike, optional + TODO, by default ORIGIN + + Returns + ------- + float + The coordinate. + """ points = self.get_points() if len(points) == 0: return 0 @@ -395,38 +765,144 @@ def get_coord( else values.max() ) - def set_coord( + def set_coordinate( self, value: "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 + TODO, by default ORIGIN + + Returns + ------- + Self + The object itself. + """ if isinstance(value, Positionable): - value = value.get_coord(dim=dim, direction=direction) - current = self.get_coord(dim=dim, direction=direction) + value = value.get_coordinate(dim=dim, direction=direction) + current = self.get_coordinate(dim=dim, direction=direction) vector = np.zeros(3) vector[dim] = value - current return self.translate(vector=vector) def get_x(self, direction: Vector3DLike = ORIGIN) -> float: - return self.get_coord(dim=0, direction=direction) + """Returns the x coordinate. + + Parameters + ---------- + direction : Vector3DLike, optional + TODO, by default ORIGIN + + Returns + ------- + float + The x coordinate. + """ + return self.get_coordinate(dim=0, direction=direction) def set_x(self, x: float, direction: Vector3DLike = ORIGIN) -> Self: - return self.set_coord(value=x, dim=0, direction=direction) + """Sets the x coordinate. + + Parameters + ---------- + x : float + The x coordinate. + direction : Vector3DLike, optional + TODO, by default ORIGIN + + Returns + ------- + Self + The object itself. + """ + return self.set_coordinate(value=x, dim=0, direction=direction) def get_y(self, direction: Vector3DLike = ORIGIN) -> float: - return self.get_coord(dim=1, direction=direction) + """Returns the y coordinate. + + Parameters + ---------- + direction : Vector3DLike, optional + TODO, by default ORIGIN + + Returns + ------- + float + The y coordinate. + """ + return self.get_coordinate(dim=1, direction=direction) def set_y(self, y: float, direction: Vector3DLike = ORIGIN) -> Self: - return self.set_coord(value=y, dim=1, direction=direction) + """Sets the y coordinate. + + Parameters + ---------- + y : float + The y coordinate. + direction : Vector3DLike, optional + TODO, by default ORIGIN + + Returns + ------- + Self + The object itself. + """ + return self.set_coordinate(value=y, dim=1, direction=direction) def get_z(self, direction: Vector3DLike = ORIGIN) -> float: - return self.get_coord(dim=2, direction=direction) + """Returns the z coordinate. + + Parameters + ---------- + direction : Vector3DLike, optional + TODO, by default ORIGIN + + Returns + ------- + float + The z coordinate. + """ + return self.get_coordinate(dim=2, direction=direction) def set_z(self, z: float, direction: Vector3DLike = ORIGIN) -> Self: - return self.set_coord(value=z, dim=2, direction=direction) + """Sets the z coordinate. + + Parameters + ---------- + z : float + The z coordinate. + direction : Vector3DLike, optional + TODO, by default ORIGIN + + Returns + ------- + Self + The object itself. + """ + return self.set_coordinate(value=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. + """ points = self.get_points() if len(points) == 0: return 0 @@ -441,6 +917,26 @@ def set_dim_size( 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 + _description_ + """ if isinstance(size, Positionable): size = size.get_dim_size(dim=dim) @@ -464,6 +960,13 @@ def set_dim_size( ) def get_width(self) -> float: + """Returns the width. + + Returns + ------- + float + The width. + """ return self.get_dim_size(dim=0) def set_width( @@ -474,6 +977,24 @@ def set_width( 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. + """ return self.set_dim_size( size=width, dim=0, @@ -483,6 +1004,13 @@ def set_width( ) def get_height(self) -> float: + """Returns the height. + + Returns + ------- + float + The height. + """ return self.get_dim_size(dim=1) def set_height( @@ -493,6 +1021,24 @@ def set_height( 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. + """ return self.set_dim_size( size=height, dim=1, @@ -502,6 +1048,13 @@ def set_height( ) def get_depth(self) -> float: + """Returns the depth. + + Returns + ------- + float + The depth. + """ return self.get_dim_size(dim=2) def set_depth( @@ -512,6 +1065,24 @@ def set_depth( 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. + """ return self.set_dim_size( size=depth, dim=2, @@ -528,6 +1099,21 @@ def align_on_border( *, 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 + + Returns + ------- + Self + The object itself. + """ + # TODO: Make frame a parameter? 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) @@ -538,6 +1124,20 @@ def align_to( mobject_or_point: "Positionable | Point3DLike", direction: Vector3DLike = ORIGIN, ) -> Self: + """Aligns the object onto a point. + + Parameters + ---------- + mobject_or_point : Positionable | Point3DLike + The point. + direction : Vector3DLike, optional + TODO, by default ORIGIN + + Returns + ------- + Self + The object itself. + """ if isinstance(mobject_or_point, Positionable): mobject_or_point = mobject_or_point.get_position(direction=direction) source = self.get_critical_point(direction=direction) @@ -545,6 +1145,13 @@ def align_to( return self.shift(target - source) def is_off_screen(self) -> bool: + """Returns whether this is off screen. + + Returns + ------- + bool + Is off screen. + """ mins, maxs = self.get_bounding_box() return ( # type: ignore[return-value] mins[0] > config.frame_x_radius @@ -554,12 +1161,31 @@ def is_off_screen(self) -> bool: ) def get_center_of_mass(self) -> Point3D: + """Returns the center of mass. + + Returns + ------- + Point3D + The center of mass. + """ points = self.get_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 + TODO + + Returns + ------- + Point3D + The boundary point. + """ points = self.get_points_defining_boundary() index = np.argmax(points.dot(direction)) return points[index] @@ -569,6 +1195,18 @@ def shift_onto_screen( *, 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 implementation space_lengths = [config.frame_x_radius, config.frame_y_radius] for vect in UP, DOWN, LEFT, RIGHT: @@ -585,6 +1223,8 @@ def shift_onto_screen( get_edge_center = get_position get_corner = get_position length_over_dim = get_dim_size + get_coord = get_coordinate + set_coord = set_coordinate def center(self) -> Self: return self.set_center(ORIGIN) @@ -596,6 +1236,22 @@ def flip( 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. + """ return self.rotate( TAU / 2, axis, @@ -609,6 +1265,22 @@ def move_to( aligned_edge: Vector3DLike = ORIGIN, coor_mask: Vector3DLike = np.array([1, 1, 1]), ) -> Self: + """Moves to a position. + + Parameters + ---------- + point_or_mobject : Point3DLike | Positionable + The point. + aligned_edge : Vector3DLike, optional + Which edge to position., by default ORIGIN + coor_mask : Vector3DLike, optional + TODO, by default [1, 1, 1] + + Returns + ------- + Self + The object itself. + """ return self.set_position( point=point_or_mobject, aligned_edge=aligned_edge, @@ -620,6 +1292,20 @@ def pose_at_angle( about_point: Point3DLike | None = None, about_edge: Vector3DLike | None = None, ) -> Self: + """TODO + + 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 + _description_ + """ return self.rotate( angle=TAU / 14, axis=RIGHT + UP, @@ -635,6 +1321,24 @@ def scale_to_fit( 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. + """ return self.set_dim_size( size=size, dim=dim, @@ -650,6 +1354,22 @@ def scale_to_fit_width( 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. + """ return self.scale_to_fit( size=width, dim=0, @@ -664,6 +1384,22 @@ def scale_to_fit_height( 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. + """ return self.scale_to_fit( size=height, dim=1, @@ -678,6 +1414,22 @@ def scale_to_fit_depth( 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. + """ return self.scale_to_fit( size=depth, dim=2, @@ -692,6 +1444,24 @@ def stretch_to_fit( 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. + """ return self.set_dim_size( size=size, dim=dim, @@ -706,6 +1476,22 @@ def stretch_to_fit_width( 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. + """ return self.stretch_to_fit( size=width, dim=0, @@ -719,6 +1505,22 @@ def stretch_to_fit_height( 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. + """ return self.stretch_to_fit( size=height, dim=1, @@ -732,6 +1534,22 @@ def stretch_to_fit_depth( 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. + """ return self.stretch_to_fit( size=depth, dim=2, @@ -745,6 +1563,20 @@ def to_corner( *, 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. + """ return self.align_on_border(direction=corner, buff=buff) def to_edge( @@ -753,37 +1585,57 @@ def to_edge( *, 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. + """ return self.align_on_border(direction=edge, buff=buff) @property def width(self) -> float: + """The width.""" return self.get_width() @width.setter def width(self, value: float) -> None: + """The width.""" self.set_width(width=value) @property def height(self) -> float: + """The height.""" return self.get_height() @height.setter def height(self, value: float) -> None: + """The height.""" self.set_height(height=value) @property def depth(self) -> float: + """The depth.""" return self.get_depth() @depth.setter def depth(self, value: float) -> None: + """The depth.""" self.set_depth(depth=value) ### DEPRECATED ### apply_points_function_about_point = apply_array_function match_points = set_points - match_coord = set_coord + match_coord = set_coordinate match_x = set_x match_y = set_y match_z = set_z From 05305980e840435fdc61765ff5230382ebcdadca Mon Sep 17 00:00:00 2001 From: GniLudio Date: Mon, 24 Aug 2026 12:59:20 +0200 Subject: [PATCH 33/66] Make shift backwards compatible --- manim/mobject/abstract/positionable.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/manim/mobject/abstract/positionable.py b/manim/mobject/abstract/positionable.py index 9372fec9bc..d5b53875f7 100644 --- a/manim/mobject/abstract/positionable.py +++ b/manim/mobject/abstract/positionable.py @@ -23,6 +23,7 @@ Point3DLike, Point3DLike_Array, Vector3DLike, + Vector3DLike_Array, ) from manim.utils.space_ops import rotation_matrix @@ -1218,7 +1219,6 @@ def shift_onto_screen( return self ### ALIASES ### - shift = translate get_critical_point = get_position get_edge_center = get_position get_corner = get_position @@ -1226,6 +1226,9 @@ def shift_onto_screen( get_coord = get_coordinate set_coord = set_coordinate + def shift(self, *vectors: Vector3DLike_Array) -> Self: + return self.translate(np.sum(vectors, axis=0)) + def center(self) -> Self: return self.set_center(ORIGIN) From 3cbdc20570e9a00b6f3b4528705b008e761fc00a Mon Sep 17 00:00:00 2001 From: GniLudio Date: Mon, 24 Aug 2026 13:02:21 +0200 Subject: [PATCH 34/66] Revert opengl scale signatures --- manim/mobject/opengl/opengl_geometry.py | 13 ++----------- manim/mobject/opengl/opengl_mobject.py | 2 +- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/manim/mobject/opengl/opengl_geometry.py b/manim/mobject/opengl/opengl_geometry.py index b6c788a834..6028de1266 100644 --- a/manim/mobject/opengl/opengl_geometry.py +++ b/manim/mobject/opengl/opengl_geometry.py @@ -785,17 +785,8 @@ def put_start_and_end_on(self, start: Point3DLike, end: Point3DLike) -> Self: self.set_points_by_ends(start, end, buff=0, path_arc=self.path_arc) return self - def scale( - self, - scale_factor: float, - scale_stroke: bool = False, - *, - about_point: Point3DLike | None = None, - about_edge: Point3DLike | None = ORIGIN, - ) -> Self: - super().scale( - scale_factor, scale_stroke, about_point=about_edge, about_edge=about_edge - ) + def scale(self, *args: Any, **kwargs: Any) -> Self: + super().scale(*args, **kwargs) self.reset_points_around_ends() return self diff --git a/manim/mobject/opengl/opengl_mobject.py b/manim/mobject/opengl/opengl_mobject.py index 4368680b9e..6f8d8c624d 100644 --- a/manim/mobject/opengl/opengl_mobject.py +++ b/manim/mobject/opengl/opengl_mobject.py @@ -1630,10 +1630,10 @@ def shift(self, vector: Vector3DLike) -> Self: def scale( self, scale_factor: float, - scale_stroke: bool = False, *, about_point: Point3DLike | None = None, about_edge: Point3DLike | None = ORIGIN, + **_kwargs: object, ) -> Self: r"""Scale the size by a factor. From 2c10fe217997354dba444653518e65c2a1fcb6da Mon Sep 17 00:00:00 2001 From: GniLudio Date: Mon, 24 Aug 2026 13:02:54 +0200 Subject: [PATCH 35/66] Revert opengl scale signatures --- manim/mobject/opengl/opengl_mobject.py | 1 - 1 file changed, 1 deletion(-) diff --git a/manim/mobject/opengl/opengl_mobject.py b/manim/mobject/opengl/opengl_mobject.py index 6f8d8c624d..8a8714bba7 100644 --- a/manim/mobject/opengl/opengl_mobject.py +++ b/manim/mobject/opengl/opengl_mobject.py @@ -1630,7 +1630,6 @@ def shift(self, vector: Vector3DLike) -> Self: def scale( self, scale_factor: float, - *, about_point: Point3DLike | None = None, about_edge: Point3DLike | None = ORIGIN, **_kwargs: object, From 0a98cddb5320dfdc1a3e213128347be30314a40e Mon Sep 17 00:00:00 2001 From: GniLudio Date: Mon, 24 Aug 2026 13:21:28 +0200 Subject: [PATCH 36/66] Adjust type annotation --- manim/mobject/abstract/positionable.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/manim/mobject/abstract/positionable.py b/manim/mobject/abstract/positionable.py index d5b53875f7..0ebeea5435 100644 --- a/manim/mobject/abstract/positionable.py +++ b/manim/mobject/abstract/positionable.py @@ -1122,14 +1122,14 @@ def align_on_border( def align_to( self, # TODO: Rename to point - mobject_or_point: "Positionable | Point3DLike", + mobject_or_point: "Point3DLike | Positionable", direction: Vector3DLike = ORIGIN, ) -> Self: """Aligns the object onto a point. Parameters ---------- - mobject_or_point : Positionable | Point3DLike + mobject_or_point : Point3DLike | Positionable The point. direction : Vector3DLike, optional TODO, by default ORIGIN From 0323f5ab958d68ac5cdc3b19bff0072c15fb7524 Mon Sep 17 00:00:00 2001 From: GniLudio Date: Mon, 24 Aug 2026 13:25:58 +0200 Subject: [PATCH 37/66] Rename scale_factor to factor --- manim/mobject/abstract/positionable.py | 15 +++++++-------- manim/mobject/geometry/line.py | 6 +++--- manim/mobject/logo.py | 8 ++++---- manim/mobject/table.py | 8 ++++---- manim/mobject/text/typst_mobject.py | 4 ++-- manim/mobject/types/vectorized_mobject.py | 8 ++++---- 6 files changed, 24 insertions(+), 25 deletions(-) diff --git a/manim/mobject/abstract/positionable.py b/manim/mobject/abstract/positionable.py index 0ebeea5435..38d31faa5b 100644 --- a/manim/mobject/abstract/positionable.py +++ b/manim/mobject/abstract/positionable.py @@ -378,8 +378,7 @@ def rotate( def scale( self, - # TODO: Rename to `factor` - scale_factor: float, + factor: float, # TODO: Remove this? scale_stroke: bool = False, *, @@ -405,7 +404,7 @@ def scale( The object itself. """ return self.apply_array_function( - function=lambda points: points.__imul__(scale_factor), + function=lambda points: points.__imul__(factor), about_point=about_point, about_edge=about_edge, ) @@ -955,7 +954,7 @@ def set_dim_size( ) else: return self.scale( - scale_factor=factor, + factor=factor, about_point=about_point, about_edge=about_edge, ) @@ -1122,7 +1121,7 @@ def align_on_border( def align_to( self, # TODO: Rename to point - mobject_or_point: "Point3DLike | Positionable", + point: "Point3DLike | Positionable", direction: Vector3DLike = ORIGIN, ) -> Self: """Aligns the object onto a point. @@ -1139,10 +1138,10 @@ def align_to( Self The object itself. """ - if isinstance(mobject_or_point, Positionable): - mobject_or_point = mobject_or_point.get_position(direction=direction) + if isinstance(point, Positionable): + point = point.get_position(direction=direction) source = self.get_critical_point(direction=direction) - target = np.where(direction == 0, source, mobject_or_point) + target = np.where(direction == 0, source, point) return self.shift(target - source) def is_off_screen(self) -> bool: diff --git a/manim/mobject/geometry/line.py b/manim/mobject/geometry/line.py index c9c745f0c3..bf01c53697 100644 --- a/manim/mobject/geometry/line.py +++ b/manim/mobject/geometry/line.py @@ -609,7 +609,7 @@ def __init__( def scale( self, - scale_factor: float, + factor: float, scale_tips: bool = False, *, about_point: Point3DLike | None = None, @@ -647,7 +647,7 @@ def scale( if scale_tips: super().scale( - scale_factor, + factor, scale_tips, about_point=about_point, about_edge=about_edge, @@ -661,7 +661,7 @@ def scale( old_tips = self.pop_tips() super().scale( - scale_factor, + factor, scale_tips, about_point=about_point, about_edge=about_edge, diff --git a/manim/mobject/logo.py b/manim/mobject/logo.py index beab9cc278..d40a3bded2 100644 --- a/manim/mobject/logo.py +++ b/manim/mobject/logo.py @@ -186,7 +186,7 @@ def __init__(self, dark_theme: bool = True): def scale( self, - scale_factor: float, + factor: float, scale_stroke: bool = False, *, about_point: Point3DLike | None = None, @@ -204,17 +204,17 @@ def scale( :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, + factor, scale_stroke, about_point=about_point, about_edge=about_edge, ) return super().scale( - scale_factor, scale_stroke, about_point=about_point, about_edge=about_edge + factor, scale_stroke, about_point=about_point, about_edge=about_edge ) @override_animation(Create) diff --git a/manim/mobject/table.py b/manim/mobject/table.py index dd6701df60..fc3b27bb32 100644 --- a/manim/mobject/table.py +++ b/manim/mobject/table.py @@ -999,7 +999,7 @@ def construct(self): def scale( self, - scale_factor: float, + factor: float, scale_stroke: bool = False, *, about_point: Point3DLike | None = None, @@ -1007,10 +1007,10 @@ def scale( ) -> 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 + self.h_buff *= factor + self.v_buff *= factor super().scale( - scale_factor, + factor, scale_stroke=scale_stroke, about_point=about_point, about_edge=about_edge, diff --git a/manim/mobject/text/typst_mobject.py b/manim/mobject/text/typst_mobject.py index b2e5aeb793..f921a46ffe 100644 --- a/manim/mobject/text/typst_mobject.py +++ b/manim/mobject/text/typst_mobject.py @@ -292,14 +292,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, ) -> 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 3088da524b..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,16 +543,16 @@ 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, scale_stroke, about_point=about_point, about_edge=about_edge + factor, scale_stroke, about_point=about_point, about_edge=about_edge ) return self From 4ca0bbd9e9cf47ee2799fce28fc5f8d72d69f906 Mon Sep 17 00:00:00 2001 From: GniLudio Date: Mon, 24 Aug 2026 13:33:16 +0200 Subject: [PATCH 38/66] Add missing docstrings --- manim/mobject/abstract/positionable.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/manim/mobject/abstract/positionable.py b/manim/mobject/abstract/positionable.py index 38d31faa5b..613f861e8b 100644 --- a/manim/mobject/abstract/positionable.py +++ b/manim/mobject/abstract/positionable.py @@ -366,7 +366,7 @@ def rotate( Returns ------- Self - _description_ + The object itself. """ if about_point is None and about_edge is None: about_edge = ORIGIN @@ -935,7 +935,7 @@ def set_dim_size( Returns ------- Self - _description_ + The object itself. """ if isinstance(size, Positionable): size = size.get_dim_size(dim=dim) @@ -1120,7 +1120,6 @@ def align_on_border( def align_to( self, - # TODO: Rename to point point: "Point3DLike | Positionable", direction: Vector3DLike = ORIGIN, ) -> Self: @@ -1294,7 +1293,7 @@ def pose_at_angle( about_point: Point3DLike | None = None, about_edge: Vector3DLike | None = None, ) -> Self: - """TODO + """Poses at angle. Parameters ---------- @@ -1306,7 +1305,7 @@ def pose_at_angle( Returns ------- Self - _description_ + The object itself. """ return self.rotate( angle=TAU / 14, From 163127407717524e37ea82ed86b36c6580e9ca2e Mon Sep 17 00:00:00 2001 From: GniLudio Date: Mon, 24 Aug 2026 13:36:33 +0200 Subject: [PATCH 39/66] Remove scale.scale_stroke and add align_on_border.frame parameters --- manim/mobject/abstract/positionable.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/manim/mobject/abstract/positionable.py b/manim/mobject/abstract/positionable.py index 613f861e8b..b2d48054ca 100644 --- a/manim/mobject/abstract/positionable.py +++ b/manim/mobject/abstract/positionable.py @@ -379,8 +379,6 @@ def rotate( def scale( self, factor: float, - # TODO: Remove this? - scale_stroke: bool = False, *, about_point: Point3DLike | None = None, about_edge: Vector3DLike | None = None, @@ -1098,6 +1096,7 @@ def align_on_border( direction: Vector3DLike, *, buff: float = DEFAULT_MOBJECT_TO_EDGE_BUFFER, + frame: Point3DLike | None = None, ) -> Self: """Aligns the object on a border. @@ -1114,7 +1113,8 @@ def align_on_border( The object itself. """ # TODO: Make frame a parameter? - frame = (config.frame_x_radius, config.frame_y_radius, 0) + if frame is None: + 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) From 6edef704883e623dae5c36d3ecbe7e00f4a23d77 Mon Sep 17 00:00:00 2001 From: GniLudio Date: Mon, 24 Aug 2026 13:39:53 +0200 Subject: [PATCH 40/66] Readd scale.scale_stroke --- manim/mobject/abstract/positionable.py | 1 + 1 file changed, 1 insertion(+) diff --git a/manim/mobject/abstract/positionable.py b/manim/mobject/abstract/positionable.py index b2d48054ca..08af8f1587 100644 --- a/manim/mobject/abstract/positionable.py +++ b/manim/mobject/abstract/positionable.py @@ -379,6 +379,7 @@ def rotate( def scale( self, factor: float, + scale_stroke: bool = False, *, about_point: Point3DLike | None = None, about_edge: Vector3DLike | None = None, From c4206efd0ba57971cfd319fd8a651fa4a4899bdf Mon Sep 17 00:00:00 2001 From: GniLudio Date: Mon, 24 Aug 2026 13:40:41 +0200 Subject: [PATCH 41/66] Fix type annotations --- manim/mobject/text/typst_mobject.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/manim/mobject/text/typst_mobject.py b/manim/mobject/text/typst_mobject.py index f921a46ffe..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", @@ -295,8 +297,8 @@ def scale( 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( factor, From 5580edb7440c754210f160508196c96f6a5ecaf5 Mon Sep 17 00:00:00 2001 From: GniLudio Date: Mon, 24 Aug 2026 13:44:08 +0200 Subject: [PATCH 42/66] Remove unnecessary todo --- manim/mobject/abstract/positionable.py | 1 - 1 file changed, 1 deletion(-) diff --git a/manim/mobject/abstract/positionable.py b/manim/mobject/abstract/positionable.py index 08af8f1587..fed0540583 100644 --- a/manim/mobject/abstract/positionable.py +++ b/manim/mobject/abstract/positionable.py @@ -1113,7 +1113,6 @@ def align_on_border( Self The object itself. """ - # TODO: Make frame a parameter? if frame is None: frame = (config.frame_x_radius, config.frame_y_radius, 0) target = np.sign(direction) * frame - buff * np.asarray(direction) From 42f9e1901d6edc0c569ab2da1211f5fed7c62f07 Mon Sep 17 00:00:00 2001 From: GniLudio Date: Mon, 24 Aug 2026 13:46:40 +0200 Subject: [PATCH 43/66] Add missing docstring --- manim/mobject/abstract/positionable.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/manim/mobject/abstract/positionable.py b/manim/mobject/abstract/positionable.py index fed0540583..bcd9f12cdc 100644 --- a/manim/mobject/abstract/positionable.py +++ b/manim/mobject/abstract/positionable.py @@ -1107,6 +1107,8 @@ def align_on_border( 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 ------- From 4be8a10e94b696c912e5685748b894867ab779df Mon Sep 17 00:00:00 2001 From: GniLudio Date: Mon, 24 Aug 2026 14:29:00 +0200 Subject: [PATCH 44/66] Add "See also" to docstrings --- manim/mobject/abstract/positionable.py | 281 ++++++++++++++++++++++++- 1 file changed, 272 insertions(+), 9 deletions(-) diff --git a/manim/mobject/abstract/positionable.py b/manim/mobject/abstract/positionable.py index bcd9f12cdc..2b210dafb2 100644 --- a/manim/mobject/abstract/positionable.py +++ b/manim/mobject/abstract/positionable.py @@ -23,7 +23,6 @@ Point3DLike, Point3DLike_Array, Vector3DLike, - Vector3DLike_Array, ) from manim.utils.space_ops import rotation_matrix @@ -108,6 +107,10 @@ def get_points(self) -> Point3D_Array: ------- Point3D_Array All points. + + See also + -------- + :meth:`set_points` """ return np.concat([mob.points for mob in self.get_family()]) @@ -128,6 +131,10 @@ def set_points( ------- Self The object itself. + + See also + -------- + :meth:`get_points` """ if isinstance(points, Positionable): for mob1, mob2 in zip(self.get_family(), points.get_family(), strict=False): @@ -143,6 +150,10 @@ def get_points_defining_boundary(self) -> Point3D_Array: ------- Point3D_Array The points defining the boundary. + + See also + -------- + :meth:`get_points` """ return self.get_points() @@ -171,6 +182,10 @@ def apply_to_family( ------- Self The object itself. + + See also + -------- + :meth:`get_family` """ for mob in self.get_family(): if only_with_points and len(mob.points) == 0: @@ -200,6 +215,10 @@ def apply_array_function( ------- Self The object itself. + + See also + -------- + :meth:`apply_to_family` """ if about_point is None: if about_edge is None: @@ -237,6 +256,10 @@ def apply_function( ------- Self The object itself. + + See also + -------- + :meth:`apply_array_function` """ if about_point is None and about_edge is None: about_point = ORIGIN @@ -272,6 +295,10 @@ def apply_complex_function( ------- Self The object itself. + + See also + -------- + :meth:`apply_function` """ def apply(point: Point3D) -> Point3D: @@ -401,6 +428,10 @@ def scale( ------- Self The object itself. + + See also + -------- + :meth:`stretch` """ return self.apply_array_function( function=lambda points: points.__imul__(factor), @@ -433,6 +464,10 @@ def stretch( ------- Self The object itself. + + See also + -------- + :meth:`scale` """ def function(points: Point3D_Array) -> Point3D_Array: @@ -477,6 +512,10 @@ def get_position( ------- Point3D The position. + + See also + -------- + :meth:`set_position` """ direction = np.sign(direction) mins, maxs = self.get_bounding_box() @@ -506,6 +545,10 @@ def set_position( ------- Self The object itself. + + See also + -------- + :meth:`get_position` """ if isinstance(point, Positionable): point = point.get_position(direction=aligned_edge) @@ -520,6 +563,10 @@ def get_center(self) -> Point3D: ------- Point3D The center position. + + See also + -------- + :meth:`set_center`, :meth:`get_position` """ return self.get_position(direction=ORIGIN) @@ -542,6 +589,10 @@ def set_center( ------- Self The object itself. + + See also + -------- + :meth:`get_center`, :meth:`set_position` """ return self.set_position(point=center, aligned_edge=ORIGIN, coor_mask=coor_mask) @@ -552,6 +603,10 @@ def get_left(self) -> Point3D: ------- Point3D The left position. + + See also + -------- + :meth:`set_left`, :meth:`get_position` """ return self.get_position(direction=LEFT) @@ -574,11 +629,25 @@ def set_left( ------- Self The object itself. + + See also + -------- + :meth:`get_left`, :meth:`set_position` """ return self.set_position(point=left, aligned_edge=LEFT, coor_mask=coor_mask) def get_right(self) -> Point3D: - """Returns the right position.""" + """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( @@ -600,6 +669,10 @@ def set_right( ------- Self The object itself. + + See also + -------- + :meth:`get_right`, :meth:`set_position` """ return self.set_position(point=right, aligned_edge=RIGHT, coor_mask=coor_mask) @@ -610,6 +683,10 @@ def get_bottom(self) -> Point3D: ------- Point3D The bottom position. + + See also + -------- + :meth:`set_bottom`, :meth:`get_position` """ return self.get_position(direction=DOWN) @@ -632,6 +709,10 @@ def set_bottom( ------- Self The object itself. + + See also + -------- + :meth:`get_bottom`, :meth:`set_position` """ return self.set_position(point=bottom, aligned_edge=DOWN, coor_mask=coor_mask) @@ -642,6 +723,10 @@ def get_top(self) -> Point3D: ------- Point3D The top position. + + See also + -------- + :meth:`set_top`, :meth:`get_position` """ return self.get_position(direction=UP) @@ -664,6 +749,10 @@ def set_top( ------- Self The object itself. + + See also + -------- + :meth:`get_top`, :meth:`set_position` """ return self.set_position(point=top, aligned_edge=UP, coor_mask=coor_mask) @@ -674,6 +763,10 @@ def get_nadir(self) -> Point3D: ------- Point3D The nadir position. + + See also + -------- + :meth:`set_nadir`, :meth:`get_position` """ return self.get_position(direction=IN) @@ -696,6 +789,10 @@ def set_nadir( ------- Self The object itself. + + See also + -------- + :meth:`get_nadir`, :meth:`set_position` """ return self.set_position(point=nadir, aligned_edge=IN, coor_mask=coor_mask) @@ -706,6 +803,10 @@ def get_zenith(self) -> Point3D: ------- Point3D The zenith position. + + See also + -------- + :meth:`set_zenith`, :meth:`get_position` """ return self.get_position(direction=OUT) @@ -728,6 +829,10 @@ def set_zenith( ------- Self The object itself. + + See also + -------- + :meth:`get_zenith`, :meth:`set_position` """ return self.set_position(point=zenith, aligned_edge=OUT, coor_mask=coor_mask) @@ -749,6 +854,10 @@ def get_coordinate( ------- float The coordinate. + + See also + -------- + :meth:`set_coordinate` """ points = self.get_points() if len(points) == 0: @@ -785,6 +894,10 @@ def set_coordinate( ------- Self The object itself. + + See also + -------- + :meth:`get_coordinate` """ if isinstance(value, Positionable): value = value.get_coordinate(dim=dim, direction=direction) @@ -805,6 +918,10 @@ def get_x(self, direction: Vector3DLike = ORIGIN) -> float: ------- float The x coordinate. + + See also + -------- + :meth:`set_x`, :meth:`get_coordinate` """ return self.get_coordinate(dim=0, direction=direction) @@ -822,6 +939,10 @@ def set_x(self, x: float, direction: Vector3DLike = ORIGIN) -> Self: ------- Self The object itself. + + See also + -------- + :meth:`get_x`, :meth:`set_coordinate` """ return self.set_coordinate(value=x, dim=0, direction=direction) @@ -837,6 +958,10 @@ def get_y(self, direction: Vector3DLike = ORIGIN) -> float: ------- float The y coordinate. + + See also + -------- + :meth:`set_y`, :meth:`get_coordinate` """ return self.get_coordinate(dim=1, direction=direction) @@ -854,6 +979,10 @@ def set_y(self, y: float, direction: Vector3DLike = ORIGIN) -> Self: ------- Self The object itself. + + See also + -------- + :meth:`get_y`, :meth:`set_coordinate` """ return self.set_coordinate(value=y, dim=1, direction=direction) @@ -869,6 +998,10 @@ def get_z(self, direction: Vector3DLike = ORIGIN) -> float: ------- float The z coordinate. + + See also + -------- + :meth:`set_z`, :meth:`get_coordinate` """ return self.get_coordinate(dim=2, direction=direction) @@ -886,6 +1019,10 @@ def set_z(self, z: float, direction: Vector3DLike = ORIGIN) -> Self: ------- Self The object itself. + + See also + -------- + :meth:`get_z`, :meth:`get_coordinate` """ return self.set_coordinate(value=z, dim=2, direction=direction) @@ -901,6 +1038,10 @@ def get_dim_size(self, dim: int) -> float: ------- float The size of the dimension. + + See also + -------- + :meth:`set_dim_size` """ points = self.get_points() if len(points) == 0: @@ -935,6 +1076,10 @@ def set_dim_size( ------- 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) @@ -965,6 +1110,10 @@ def get_width(self) -> float: ------- float The width. + + See also + -------- + :meth:`set_width`, :meth:`get_dim_size` """ return self.get_dim_size(dim=0) @@ -993,6 +1142,10 @@ def set_width( ------- Self The object itself. + + See also + -------- + :meth:`get_width`, :meth:`set_dim_size` """ return self.set_dim_size( size=width, @@ -1009,6 +1162,10 @@ def get_height(self) -> float: ------- float The height. + + See also + -------- + :meth:`set_height`, :meth:`get_dim_size` """ return self.get_dim_size(dim=1) @@ -1037,6 +1194,11 @@ def set_height( ------- Self The object itself. + + + See also + -------- + :meth:`get_height`, :meth:`set_dim_size` """ return self.set_dim_size( size=height, @@ -1053,6 +1215,11 @@ def get_depth(self) -> float: ------- float The depth. + + + See also + -------- + :meth:`set_depth`, :meth:`get_dim_size` """ return self.get_dim_size(dim=2) @@ -1081,6 +1248,11 @@ def set_depth( ------- Self The object itself. + + + See also + -------- + :meth:`get_depth`, :meth:`set_dim_size` """ return self.set_dim_size( size=depth, @@ -1114,6 +1286,10 @@ def align_on_border( ------- Self The object itself. + + See also + -------- + :meth:`align_to` """ if frame is None: frame = (config.frame_x_radius, config.frame_y_radius, 0) @@ -1226,10 +1402,33 @@ def shift_onto_screen( get_coord = get_coordinate set_coord = set_coordinate - def shift(self, *vectors: Vector3DLike_Array) -> Self: + def shift(self, *vectors: Vector3DLike) -> Self: + """_summary_ + + Parameters + ---------- + vectors: *Vector3DLike + The vectors. + + Returns + ------- + Self + The object itself. + """ return self.translate(np.sum(vectors, axis=0)) def center(self) -> Self: + """Moves to the ORIGIN. + + Returns + ------- + Self + The object itself. + + See also + -------- + :meth:`set_center` + """ return self.set_center(ORIGIN) def flip( @@ -1254,6 +1453,10 @@ def flip( ------- Self The object itself. + + See also + -------- + :meth:`rotate` """ return self.rotate( TAU / 2, @@ -1283,6 +1486,10 @@ def move_to( ------- Self The object itself. + + See also + -------- + :meth:`set_position` """ return self.set_position( point=point_or_mobject, @@ -1308,6 +1515,10 @@ def pose_at_angle( ------- Self The object itself. + + See also + -------- + :meth:`rotate` """ return self.rotate( angle=TAU / 14, @@ -1341,6 +1552,10 @@ def scale_to_fit( ------- Self The object itself. + + See also + -------- + :meth:`set_dim_size`, :meth:`scale` """ return self.set_dim_size( size=size, @@ -1372,6 +1587,10 @@ def scale_to_fit_width( ------- Self The object itself. + + See also + -------- + :meth:`scale_to_fit`, :meth:`scale`, :meth:`set_width` """ return self.scale_to_fit( size=width, @@ -1402,6 +1621,10 @@ def scale_to_fit_height( ------- Self The object itself. + + See also + -------- + :meth:`scale_to_fit`, :meth:`scale`, :meth:`set_height` """ return self.scale_to_fit( size=height, @@ -1432,6 +1655,10 @@ def scale_to_fit_depth( ------- Self The object itself. + + See also + -------- + :meth:`scale_to_fit`, :meth:`scale`, :meth:`set_depth` """ return self.scale_to_fit( size=depth, @@ -1464,6 +1691,10 @@ def stretch_to_fit( ------- Self The object itself. + + See also + -------- + :meth:`set_dim_size`, :meth:`stretch` """ return self.set_dim_size( size=size, @@ -1494,6 +1725,10 @@ def stretch_to_fit_width( ------- Self The object itself. + + See also + -------- + :meth:`stretch_to_fit`, :meth:`stretch`, :meth:`set_width` """ return self.stretch_to_fit( size=width, @@ -1523,6 +1758,10 @@ def stretch_to_fit_height( ------- Self The object itself. + + See also + -------- + :meth:`stretch_to_fit`, :meth:`stretch`, :meth:`set_height` """ return self.stretch_to_fit( size=height, @@ -1552,6 +1791,10 @@ def stretch_to_fit_depth( ------- Self The object itself. + + See also + -------- + :meth:`stretch_to_fit`, :meth:`stretch`, :meth:`set_depth` """ return self.stretch_to_fit( size=depth, @@ -1579,6 +1822,10 @@ def to_corner( ------- Self The object itself. + + See also + -------- + :meth:`align_on_border` """ return self.align_on_border(direction=corner, buff=buff) @@ -1601,37 +1848,53 @@ def to_edge( ------- Self The object itself. + + See also + -------- + :meth:`align_on_border` """ return self.align_on_border(direction=edge, buff=buff) @property def width(self) -> float: - """The width.""" + """The width. + + See also + -------- + :meth:`get_width`, :meth:`set_width` + """ return self.get_width() @width.setter def width(self, value: float) -> None: - """The width.""" self.set_width(width=value) @property def height(self) -> float: - """The height.""" + """The height. + + See also + -------- + :meth:`get_height`, :meth:`set_height` + """ return self.get_height() @height.setter def height(self, value: float) -> None: - """The height.""" self.set_height(height=value) @property def depth(self) -> float: - """The depth.""" + """The width. + + See also + -------- + :meth:`get_depth`, :meth:`set_depth` + """ return self.get_depth() @depth.setter def depth(self, value: float) -> None: - """The depth.""" self.set_depth(depth=value) ### DEPRECATED ### From 80d21289e09132aeaf79b924e565c5cc92e093bb Mon Sep 17 00:00:00 2001 From: GniLudio Date: Mon, 24 Aug 2026 14:41:56 +0200 Subject: [PATCH 45/66] Clean up class docstring --- manim/mobject/abstract/positionable.py | 69 +------------------------- 1 file changed, 1 insertion(+), 68 deletions(-) diff --git a/manim/mobject/abstract/positionable.py b/manim/mobject/abstract/positionable.py index 2b210dafb2..9c059db6f1 100644 --- a/manim/mobject/abstract/positionable.py +++ b/manim/mobject/abstract/positionable.py @@ -28,74 +28,7 @@ class Positionable: - """A positionable object. - - ### Basics - - points - - (get|set)_points - - get_points_defining_boundary - ### Applying Functions - - get_family - - apply_to_family - - apply_array_function - - apply_function - - apply_complex_function - ### Transformations - - apply_matrix - - translate - - rotate - - scale - - stretch - ### General - - get_bounding_box - - (get|set)_position - - (get|set)_(center|left|right|bottom|top|nadir|zenith) - - (get|set)_coordinate - - (get|set)_(x|y|z) - - (get|set)_dim_size - - (get|set)_(width|height|depth) - ### Specialized - - align_to - - align_on_border - - next_to (TODO) - - is_off_screen - - get_center_of_mass - - get_boundary_point - - shift_onto_screen - ### Aliases - - center = set_center(ORIGIN) - - flip - - length_over_dim - - move_to = set_position - - scale_to_fit = set_dim_size(stretch=False) - - scale_to_fit_(width|height|depth) - - stretch_to_fit = set_dim_size(stretch=True) - - stretch_to_fit_(width|height|depth) - - get_coord = get_coordinate - - get_critical_point = get_position - - get_edge_center = get_position - - get_corner = get_position - - pose_at_angle - - set_coordinate = set_coord - - shift = translate - - to_corner = align_on_border - - to_edge = align_on_border - - (width|height|depth) - ### Deprecated - - apply_points_function_about_point - - apply_function_to_position - - get_extremum_along_dim - - match_points - - match_coord - - match_(x|y|z) - - match_dim_size - - match_(width|height|depth) - - reduce_across_dimension - - rescale_to_fit - - rotate_about_origin - - stretch_about_point - - """ + """A positionable object.""" ### FUNDAMENTALS ### points: Point3D_Array = np.array([]) From a5db5939b0092aa9310a46263654813ff0bb1b61 Mon Sep 17 00:00:00 2001 From: GniLudio Date: Mon, 24 Aug 2026 14:54:21 +0200 Subject: [PATCH 46/66] Add examples --- manim/mobject/abstract/positionable.py | 225 +++++++++++++++++++++++++ 1 file changed, 225 insertions(+) diff --git a/manim/mobject/abstract/positionable.py b/manim/mobject/abstract/positionable.py index 9c059db6f1..1b6745c3ba 100644 --- a/manim/mobject/abstract/positionable.py +++ b/manim/mobject/abstract/positionable.py @@ -65,6 +65,19 @@ def set_points( 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` @@ -158,6 +171,8 @@ def apply_array_function( 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) def apply(mob: Positionable) -> None: @@ -229,6 +244,28 @@ def apply_complex_function( 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` @@ -323,6 +360,37 @@ def rotate( 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 @@ -362,6 +430,22 @@ def scale( 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` @@ -1247,6 +1331,10 @@ def align_to( ------- 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) @@ -1387,6 +1475,19 @@ def flip( 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` @@ -1521,6 +1622,21 @@ def scale_to_fit_width( 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` @@ -1555,6 +1671,21 @@ def scale_to_fit_height( 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` @@ -1659,6 +1790,21 @@ def stretch_to_fit_width( 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` @@ -1692,6 +1838,21 @@ def stretch_to_fit_height( 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` @@ -1756,6 +1917,22 @@ def to_corner( 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` @@ -1782,6 +1959,22 @@ def to_edge( 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` @@ -1792,6 +1985,22 @@ def to_edge( 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` @@ -1806,6 +2015,22 @@ def width(self, value: float) -> None: 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` From bc719dce52242598b154fe1596fcd8dedaa43472 Mon Sep 17 00:00:00 2001 From: GniLudio <50866361+GniLudio@users.noreply.github.com> Date: Mon, 24 Aug 2026 17:48:38 +0200 Subject: [PATCH 47/66] Drop coor_mask parameter from added methods --- manim/mobject/abstract/positionable.py | 42 +++++--------------------- 1 file changed, 7 insertions(+), 35 deletions(-) diff --git a/manim/mobject/abstract/positionable.py b/manim/mobject/abstract/positionable.py index 1b6745c3ba..e7292a43ec 100644 --- a/manim/mobject/abstract/positionable.py +++ b/manim/mobject/abstract/positionable.py @@ -590,8 +590,6 @@ def get_center(self) -> Point3D: def set_center( self, center: "Point3DLike | Positionable", - *, - coor_mask: Vector3DLike = np.array([1, 1, 1]), ) -> Self: """Sets the center position. @@ -599,8 +597,6 @@ def set_center( ---------- center : Point3DLike | Positionable The center position. - coor_mask : Vector3DLike, optional - TODO, by default np.array([1, 1, 1]) Returns ------- @@ -611,7 +607,7 @@ def set_center( -------- :meth:`get_center`, :meth:`set_position` """ - return self.set_position(point=center, aligned_edge=ORIGIN, coor_mask=coor_mask) + return self.set_position(point=center, aligned_edge=ORIGIN) def get_left(self) -> Point3D: """Returns the left position. @@ -630,8 +626,6 @@ def get_left(self) -> Point3D: def set_left( self, left: "Point3DLike | Positionable", - *, - coor_mask: Vector3DLike = np.array([1, 1, 1]), ) -> Self: """Sets the left position. @@ -639,8 +633,6 @@ def set_left( ---------- left : Point3DLike | Positionable The left position. - coor_mask : Vector3DLike, optional - TODO, by default np.array([1, 1, 1]) Returns ------- @@ -651,7 +643,7 @@ def set_left( -------- :meth:`get_left`, :meth:`set_position` """ - return self.set_position(point=left, aligned_edge=LEFT, coor_mask=coor_mask) + return self.set_position(point=left, aligned_edge=LEFT) def get_right(self) -> Point3D: """Returns the right position. @@ -670,8 +662,6 @@ def get_right(self) -> Point3D: def set_right( self, right: "Point3DLike | Positionable", - *, - coor_mask: Vector3DLike = np.array([1, 1, 1]), ) -> Self: """Sets the right position. @@ -679,8 +669,6 @@ def set_right( ---------- right : Point3DLike | Positionable The right position. - coor_mask : Vector3DLike, optional - TODO, by default np.array([1, 1, 1]) Returns ------- @@ -691,7 +679,7 @@ def set_right( -------- :meth:`get_right`, :meth:`set_position` """ - return self.set_position(point=right, aligned_edge=RIGHT, coor_mask=coor_mask) + return self.set_position(point=right, aligned_edge=RIGHT) def get_bottom(self) -> Point3D: """Returns the bottom position. @@ -710,8 +698,6 @@ def get_bottom(self) -> Point3D: def set_bottom( self, bottom: "Point3DLike | Positionable", - *, - coor_mask: Vector3DLike = np.array([1, 1, 1]), ) -> Self: """Sets the bottom position. @@ -719,8 +705,6 @@ def set_bottom( ---------- bottom : Point3DLike | Positionable The bottom position. - coor_mask : Vector3DLike, optional - TODO, by default np.array([1, 1, 1]) Returns ------- @@ -731,7 +715,7 @@ def set_bottom( -------- :meth:`get_bottom`, :meth:`set_position` """ - return self.set_position(point=bottom, aligned_edge=DOWN, coor_mask=coor_mask) + return self.set_position(point=bottom, aligned_edge=DOWN) def get_top(self) -> Point3D: """Returns the top position. @@ -750,8 +734,6 @@ def get_top(self) -> Point3D: def set_top( self, top: "Point3DLike | Positionable", - *, - coor_mask: Vector3DLike = np.array([1, 1, 1]), ) -> Self: """Sets the top position. @@ -759,8 +741,6 @@ def set_top( ---------- top : Point3DLike | Positionable The top position. - coor_mask : Vector3DLike, optional - TODO, by default np.array([1, 1, 1]) Returns ------- @@ -771,7 +751,7 @@ def set_top( -------- :meth:`get_top`, :meth:`set_position` """ - return self.set_position(point=top, aligned_edge=UP, coor_mask=coor_mask) + return self.set_position(point=top, aligned_edge=UP) def get_nadir(self) -> Point3D: """Returns the nadir position. @@ -790,8 +770,6 @@ def get_nadir(self) -> Point3D: def set_nadir( self, nadir: "Point3DLike | Positionable", - *, - coor_mask: Vector3DLike = np.array([1, 1, 1]), ) -> Self: """Sets the nadir position. @@ -799,8 +777,6 @@ def set_nadir( ---------- nadir : Point3DLike | Positionable The nadir position. - coor_mask : Vector3DLike, optional - TODO, by default np.array([1, 1, 1]) Returns ------- @@ -811,7 +787,7 @@ def set_nadir( -------- :meth:`get_nadir`, :meth:`set_position` """ - return self.set_position(point=nadir, aligned_edge=IN, coor_mask=coor_mask) + return self.set_position(point=nadir, aligned_edge=IN) def get_zenith(self) -> Point3D: """Returns the zenith position. @@ -830,8 +806,6 @@ def get_zenith(self) -> Point3D: def set_zenith( self, zenith: "Point3DLike | Positionable", - *, - coor_mask: Vector3DLike = np.array([1, 1, 1]), ) -> Self: """Sets the zenith position. @@ -839,8 +813,6 @@ def set_zenith( ---------- zenith : Point3DLike | Positionable The zenith position. - coor_mask : Vector3DLike, optional - TODO, by default np.array([1, 1, 1]) Returns ------- @@ -851,7 +823,7 @@ def set_zenith( -------- :meth:`get_zenith`, :meth:`set_position` """ - return self.set_position(point=zenith, aligned_edge=OUT, coor_mask=coor_mask) + return self.set_position(point=zenith, aligned_edge=OUT) def get_coordinate( self, From 058870e00168f48c0ed4ddb2f34e8a3671966768 Mon Sep 17 00:00:00 2001 From: GniLudio <50866361+GniLudio@users.noreply.github.com> Date: Mon, 24 Aug 2026 22:18:26 +0200 Subject: [PATCH 48/66] Readd some optimizations --- manim/mobject/abstract/positionable.py | 69 +++++++++++++++----------- 1 file changed, 40 insertions(+), 29 deletions(-) diff --git a/manim/mobject/abstract/positionable.py b/manim/mobject/abstract/positionable.py index e7292a43ec..1a2e9517c7 100644 --- a/manim/mobject/abstract/positionable.py +++ b/manim/mobject/abstract/positionable.py @@ -534,10 +534,13 @@ def get_position( -------- :meth:`set_position` """ - direction = np.sign(direction) - mins, maxs = self.get_bounding_box() - mids = (mins + maxs) / 2 - return mids + (maxs - mids) * direction + points = self.get_points() + return np.array( + [ + self._get_extremum(values=points[:, dim], key=key) + for dim, key in enumerate(direction) + ] + ) def set_position( self, @@ -1310,10 +1313,41 @@ def align_to( """ if isinstance(point, Positionable): point = point.get_position(direction=direction) - source = self.get_critical_point(direction=direction) + source = self.get_position(direction=direction) target = np.where(direction == 0, source, point) return self.shift(target - source) + def get_extremum_along_dim( + self, + dim: int = 0, + key: int = 0, + ) -> float: + """Returns the extremum along a dimension. + + Parameters + ---------- + dim, optional + The dimension., by default 0 + key, optional + Whether to get the minimum (key<0), center (key=0) or maximum value (key>0)., by default 0 + + Returns + ------- + _description_ + """ + return self._get_extremum(self.get_points()[:, dim], key=key) + + def _get_extremum(self, values: np.ndarray, key: int) -> float: + if len(values) == 0: + return 0 + 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. @@ -1338,10 +1372,7 @@ def get_center_of_mass(self) -> Point3D: Point3D The center of mass. """ - points = self.get_points() - if len(points) == 0: - return ORIGIN - return points.mean(axis=0) + return self.get_points().mean(axis=0) def get_boundary_point(self, direction: Vector3DLike) -> Point3D: """Returns a boundary point. @@ -2047,26 +2078,6 @@ def apply_function_to_position( ) -> Self: return self.move_to(function(self.get_center())) - # @deprecated() - def get_extremum_along_dim( - self, - dim: int = 0, - key: int = 0, - ) -> float: - points = self.get_points() - if len(points) == 0: - return 0 - values = 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 - # @deprecated() def reduce_across_dimension( self, From 81a895d7cd01b040f8a6a03ed7074b9780ec91d4 Mon Sep 17 00:00:00 2001 From: GniLudio Date: Wed, 26 Aug 2026 10:09:27 +0200 Subject: [PATCH 49/66] Fix and some refactoring --- manim/mobject/abstract/positionable.py | 60 +++++++++++--------------- 1 file changed, 25 insertions(+), 35 deletions(-) diff --git a/manim/mobject/abstract/positionable.py b/manim/mobject/abstract/positionable.py index 1a2e9517c7..986c9ee319 100644 --- a/manim/mobject/abstract/positionable.py +++ b/manim/mobject/abstract/positionable.py @@ -1,4 +1,6 @@ +import operator as op from collections.abc import Callable, Iterable +from functools import reduce from typing import Any, Self import numpy as np @@ -416,7 +418,7 @@ def scale( Parameters ---------- - scale_factor : float + factor : float The factor. scale_stroke : bool, optional Whether to scale the stroke width., by default False @@ -534,10 +536,14 @@ def get_position( -------- :meth:`set_position` """ - points = self.get_points() + points = self.get_points_defining_boundary() return np.array( [ - self._get_extremum(values=points[:, dim], key=key) + self.get_extremum_along_dim( + points=points, + dim=dim, + key=key, + ) for dim, key in enumerate(direction) ] ) @@ -851,18 +857,10 @@ def get_coordinate( -------- :meth:`set_coordinate` """ - points = self.get_points() - if len(points) == 0: - return 0 - - key = direction[dim] - 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() + return self.get_extremum_along_dim( + points=self.get_points(), + dim=dim, + key=np.sign(direction[dim]), ) def set_coordinate( @@ -1315,31 +1313,19 @@ def align_to( point = point.get_position(direction=direction) source = self.get_position(direction=direction) target = np.where(direction == 0, source, point) - return self.shift(target - source) + return self.translate(target - source) def get_extremum_along_dim( self, + points: Point3D_Array | None = None, dim: int = 0, key: int = 0, ) -> float: - """Returns the extremum along a dimension. - - Parameters - ---------- - dim, optional - The dimension., by default 0 - key, optional - Whether to get the minimum (key<0), center (key=0) or maximum value (key>0)., by default 0 - - Returns - ------- - _description_ - """ - return self._get_extremum(self.get_points()[:, dim], key=key) - - def _get_extremum(self, values: np.ndarray, key: int) -> float: - if len(values) == 0: + if points is None: + points = self.get_points() + if len(points) == 0: return 0 + values = points[:, dim] return ( # type: ignore[no-any-return] values.min() if key < 0 @@ -1372,7 +1358,10 @@ def get_center_of_mass(self) -> Point3D: Point3D The center of mass. """ - return self.get_points().mean(axis=0) + points = self.get_points() + if len(points) == 0: + return ORIGIN + return points.mean(axis=0) def get_boundary_point(self, direction: Vector3DLike) -> Point3D: """Returns a boundary point. @@ -1439,7 +1428,8 @@ def shift(self, *vectors: Vector3DLike) -> Self: Self The object itself. """ - return self.translate(np.sum(vectors, axis=0)) + vector = reduce(op.add, vectors) + return self.translate(vector=vector) def center(self) -> Self: """Moves to the ORIGIN. From e87efc69ea1006de858cf2bfb932ae4cc6447030 Mon Sep 17 00:00:00 2001 From: GniLudio <50866361+GniLudio@users.noreply.github.com> Date: Thu, 27 Aug 2026 17:51:05 +0200 Subject: [PATCH 50/66] Update positionable.py --- manim/mobject/abstract/positionable.py | 119 +++++++++++++------------ 1 file changed, 60 insertions(+), 59 deletions(-) diff --git a/manim/mobject/abstract/positionable.py b/manim/mobject/abstract/positionable.py index 986c9ee319..ca38c13f5a 100644 --- a/manim/mobject/abstract/positionable.py +++ b/manim/mobject/abstract/positionable.py @@ -35,7 +35,7 @@ class Positionable: ### FUNDAMENTALS ### points: Point3D_Array = np.array([]) - def get_points(self) -> Point3D_Array: + def get_all_points(self) -> Point3D_Array: """Returns all points. Returns @@ -103,7 +103,7 @@ def get_points_defining_boundary(self) -> Point3D_Array: -------- :meth:`get_points` """ - return self.get_points() + return self.get_all_points() ### APPLYING FUNCTIONS ### @@ -335,11 +335,16 @@ def translate(self, vector: Vector3DLike) -> Self: Self The object itself. """ + return self.apply_array_function( + function=lambda points: points.__iadd__(vector) + ) - def function(mob: Positionable) -> None: - mob.points += vector + def translate_dim(self, length: float, dim: int) -> Self: + def function(points: Point3D_Array) -> Point3D_Array: + points[:, dim] += length + return points - return self.apply_to_family(function=function) + return self.apply_array_function(function=function) def rotate( self, @@ -501,21 +506,6 @@ def function(points: Point3D_Array) -> Point3D_Array: ### GENERAL ### - 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_position( self, direction: Vector3DLike = ORIGIN, @@ -539,7 +529,7 @@ def get_position( points = self.get_points_defining_boundary() return np.array( [ - self.get_extremum_along_dim( + self._get_extremum_along_dim( points=points, dim=dim, key=key, @@ -553,8 +543,6 @@ def set_position( point: "Point3DLike | Positionable", *, aligned_edge: Vector3DLike = ORIGIN, - # TODO: Remove this? - coor_mask: Vector3DLike = np.array([1, 1, 1]), ) -> Self: """Sets the position. @@ -564,8 +552,6 @@ def set_position( The point. aligned_edge : Vector3DLike, optional Which edge to position., by default ORIGIN - coor_mask : Vector3DLike, optional - TODO, by default [1, 1, 1] Returns ------- @@ -579,7 +565,7 @@ def set_position( if isinstance(point, Positionable): point = point.get_position(direction=aligned_edge) current = self.get_position(direction=aligned_edge) - vector = (point - current) * coor_mask + vector = point - current return self.translate(vector=vector) def get_center(self) -> Point3D: @@ -857,8 +843,8 @@ def get_coordinate( -------- :meth:`set_coordinate` """ - return self.get_extremum_along_dim( - points=self.get_points(), + return self._get_extremum_along_dim( + points=self.get_points_defining_boundary(), dim=dim, key=np.sign(direction[dim]), ) @@ -892,9 +878,8 @@ def set_coordinate( if isinstance(value, Positionable): value = value.get_coordinate(dim=dim, direction=direction) current = self.get_coordinate(dim=dim, direction=direction) - vector = np.zeros(3) - vector[dim] = value - current - return self.translate(vector=vector) + vector = value - current + return self.translate_dim(length=vector, dim=dim) def get_x(self, direction: Vector3DLike = ORIGIN) -> float: """Returns the x coordinate. @@ -1033,7 +1018,7 @@ def get_dim_size(self, dim: int) -> float: -------- :meth:`set_dim_size` """ - points = self.get_points() + points = self.get_all_points() if len(points) == 0: return 0 return np.ptp(points[:, dim]) # type: ignore[no-any-return] @@ -1315,14 +1300,34 @@ def align_to( target = np.where(direction == 0, source, point) return self.translate(target - source) + 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_extremum_along_dim( self, - points: Point3D_Array | None = None, dim: int = 0, key: int = 0, ) -> float: - if points is None: - points = self.get_points() + return self._get_extremum_along_dim(self.get_all_points(), 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] @@ -1334,22 +1339,6 @@ def get_extremum_along_dim( else values.max() ) - def is_off_screen(self) -> bool: - """Returns whether this is off screen. - - Returns - ------- - bool - Is off screen. - """ - mins, maxs = self.get_bounding_box() - return ( # type: ignore[return-value] - mins[0] > config.frame_x_radius - or maxs[0] < -config.frame_x_radius - or mins[1] > config.frame_y_radius - or maxs[1] < -config.frame_y_radius, - ) - def get_center_of_mass(self) -> Point3D: """Returns the center of mass. @@ -1358,7 +1347,7 @@ def get_center_of_mass(self) -> Point3D: Point3D The center of mass. """ - points = self.get_points() + points = self.get_all_points() if len(points) == 0: return ORIGIN return points.mean(axis=0) @@ -1380,6 +1369,22 @@ def get_boundary_point(self, direction: Vector3DLike) -> Point3D: index = np.argmax(points.dot(direction)) return points[index] + def is_off_screen(self) -> bool: + """Returns whether this is off screen. + + Returns + ------- + bool + Is off screen. + """ + mins, maxs = self.get_bounding_box() + return ( # type: ignore[return-value] + mins[0] > config.frame_x_radius + or maxs[0] < -config.frame_x_radius + or mins[1] > config.frame_y_radius + or maxs[1] < -config.frame_y_radius, + ) + def shift_onto_screen( self, *, @@ -1397,12 +1402,12 @@ def shift_onto_screen( Self The object itself. """ - # TODO: Simplify implementation + # TODO: Simplify/Optimize implementation space_lengths = [config.frame_x_radius, config.frame_y_radius] for vect in UP, DOWN, LEFT, RIGHT: dim = np.argmax(np.abs(vect)) max_val = space_lengths[dim] - buff - edge_center = self.get_edge_center(vect) + edge_center = self.get_position(vect) if np.dot(edge_center, vect) > max_val: self.to_edge(vect, buff=buff) return self @@ -1496,7 +1501,6 @@ def move_to( self, point_or_mobject: "Point3DLike | Positionable", aligned_edge: Vector3DLike = ORIGIN, - coor_mask: Vector3DLike = np.array([1, 1, 1]), ) -> Self: """Moves to a position. @@ -1506,8 +1510,6 @@ def move_to( The point. aligned_edge : Vector3DLike, optional Which edge to position., by default ORIGIN - coor_mask : Vector3DLike, optional - TODO, by default [1, 1, 1] Returns ------- @@ -1521,7 +1523,6 @@ def move_to( return self.set_position( point=point_or_mobject, aligned_edge=aligned_edge, - coor_mask=coor_mask, ) def pose_at_angle( @@ -2074,7 +2075,7 @@ def reduce_across_dimension( reduce_func: Callable[[Iterable[float]], float], dim: int, ) -> float | None: - points = self.get_points() + points = self.get_all_points() if len(points) == 0: return None From 2405b1d5260080e5f38b922889b50f3ae16337c4 Mon Sep 17 00:00:00 2001 From: GniLudio <50866361+GniLudio@users.noreply.github.com> Date: Thu, 27 Aug 2026 18:25:35 +0200 Subject: [PATCH 51/66] Some optimizations --- manim/mobject/abstract/positionable.py | 30 ++++++++++++-------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/manim/mobject/abstract/positionable.py b/manim/mobject/abstract/positionable.py index ca38c13f5a..17b1ea8f36 100644 --- a/manim/mobject/abstract/positionable.py +++ b/manim/mobject/abstract/positionable.py @@ -177,10 +177,16 @@ def apply_array_function( # Make a copy to prevent mutation of the original array if about_point is a view about_point = np.array(about_point, copy=True) - def apply(mob: Positionable) -> None: - mob.points -= about_point - mob.points = function(mob.points) - mob.points += about_point + 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) @@ -335,16 +341,7 @@ def translate(self, vector: Vector3DLike) -> Self: Self The object itself. """ - return self.apply_array_function( - function=lambda points: points.__iadd__(vector) - ) - - def translate_dim(self, length: float, dim: int) -> Self: - def function(points: Point3D_Array) -> Point3D_Array: - points[:, dim] += length - return points - - return self.apply_array_function(function=function) + return self.apply_to_family(function=lambda mob: mob.points.__iadd__(vector)) def rotate( self, @@ -878,8 +875,9 @@ def set_coordinate( if isinstance(value, Positionable): value = value.get_coordinate(dim=dim, direction=direction) current = self.get_coordinate(dim=dim, direction=direction) - vector = value - current - return self.translate_dim(length=vector, dim=dim) + vector = np.zeros(3) + vector[dim] = value - current + return self.translate(vector=vector) def get_x(self, direction: Vector3DLike = ORIGIN) -> float: """Returns the x coordinate. From d6afd294c1ce0d29fa986cc91c616a3d8ad55924 Mon Sep 17 00:00:00 2001 From: GniLudio <50866361+GniLudio@users.noreply.github.com> Date: Thu, 27 Aug 2026 20:23:39 +0200 Subject: [PATCH 52/66] Optimize is_off_screen --- manim/mobject/abstract/positionable.py | 42 +++++++++++++++++++------- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/manim/mobject/abstract/positionable.py b/manim/mobject/abstract/positionable.py index 17b1ea8f36..d1e64d8ad7 100644 --- a/manim/mobject/abstract/positionable.py +++ b/manim/mobject/abstract/positionable.py @@ -47,7 +47,13 @@ def get_all_points(self) -> Point3D_Array: -------- :meth:`set_points` """ - return np.concat([mob.points for mob in self.get_family()]) + 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, @@ -321,12 +327,11 @@ def apply_matrix( full_matrix = np.identity(3) full_matrix[: matrix.shape[0], : matrix.shape[1]] = matrix - self.apply_array_function( + return self.apply_array_function( function=lambda points: points.dot(full_matrix.T, out=points), about_point=about_point, about_edge=about_edge, ) - return self def translate(self, vector: Vector3DLike) -> Self: """Applies a translation. @@ -1375,12 +1380,28 @@ def is_off_screen(self) -> bool: bool Is off screen. """ - mins, maxs = self.get_bounding_box() - return ( # type: ignore[return-value] - mins[0] > config.frame_x_radius - or maxs[0] < -config.frame_x_radius - or mins[1] > config.frame_y_radius - or maxs[1] < -config.frame_y_radius, + 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 shift_onto_screen( @@ -1431,8 +1452,7 @@ def shift(self, *vectors: Vector3DLike) -> Self: Self The object itself. """ - vector = reduce(op.add, vectors) - return self.translate(vector=vector) + return self.translate(vector=reduce(op.add, vectors)) def center(self) -> Self: """Moves to the ORIGIN. From 311524fbb0423e22e24697a1df843c04dadb24d3 Mon Sep 17 00:00:00 2001 From: GniLudio <50866361+GniLudio@users.noreply.github.com> Date: Thu, 27 Aug 2026 20:48:27 +0200 Subject: [PATCH 53/66] Replace get_all_points with get_points_defining_boundary --- manim/mobject/abstract/positionable.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/manim/mobject/abstract/positionable.py b/manim/mobject/abstract/positionable.py index d1e64d8ad7..ae81b050e1 100644 --- a/manim/mobject/abstract/positionable.py +++ b/manim/mobject/abstract/positionable.py @@ -1021,6 +1021,7 @@ def get_dim_size(self, dim: int) -> float: -------- :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 @@ -1323,7 +1324,11 @@ def get_extremum_along_dim( dim: int = 0, key: int = 0, ) -> float: - return self._get_extremum_along_dim(self.get_all_points(), dim=dim, key=key) + return self._get_extremum_along_dim( + self.get_points_defining_boundary(), + dim=dim, + key=key, + ) def _get_extremum_along_dim( self, @@ -2093,7 +2098,7 @@ def reduce_across_dimension( reduce_func: Callable[[Iterable[float]], float], dim: int, ) -> float | None: - points = self.get_all_points() + points = self.get_points_defining_boundary() if len(points) == 0: return None From a79048370797db84c1bd3e6808289d23f93a9822 Mon Sep 17 00:00:00 2001 From: GniLudio <50866361+GniLudio@users.noreply.github.com> Date: Thu, 27 Aug 2026 22:50:58 +0200 Subject: [PATCH 54/66] Add todo --- manim/mobject/abstract/positionable.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/manim/mobject/abstract/positionable.py b/manim/mobject/abstract/positionable.py index ae81b050e1..43ab31f732 100644 --- a/manim/mobject/abstract/positionable.py +++ b/manim/mobject/abstract/positionable.py @@ -1358,6 +1358,9 @@ def get_center_of_mass(self) -> Point3D: points = self.get_all_points() if len(points) == 0: return ORIGIN + # TODO: Performs better with over ~2.5k points + # return np.apply_along_axis(func1d=np.mean, axis=0, arr=points) + # TODO: Performs better with under ~2.5k points return points.mean(axis=0) def get_boundary_point(self, direction: Vector3DLike) -> Point3D: From d84de49df759620ea5a881a231dfe9cbbb18c939 Mon Sep 17 00:00:00 2001 From: GniLudio <50866361+GniLudio@users.noreply.github.com> Date: Fri, 28 Aug 2026 09:14:03 +0200 Subject: [PATCH 55/66] Remove TODO --- manim/mobject/abstract/positionable.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/manim/mobject/abstract/positionable.py b/manim/mobject/abstract/positionable.py index 43ab31f732..ae81b050e1 100644 --- a/manim/mobject/abstract/positionable.py +++ b/manim/mobject/abstract/positionable.py @@ -1358,9 +1358,6 @@ def get_center_of_mass(self) -> Point3D: points = self.get_all_points() if len(points) == 0: return ORIGIN - # TODO: Performs better with over ~2.5k points - # return np.apply_along_axis(func1d=np.mean, axis=0, arr=points) - # TODO: Performs better with under ~2.5k points return points.mean(axis=0) def get_boundary_point(self, direction: Vector3DLike) -> Point3D: From e169d206356787c948db452f0c7606beb29fb5d7 Mon Sep 17 00:00:00 2001 From: GniLudio <50866361+GniLudio@users.noreply.github.com> Date: Fri, 28 Aug 2026 12:48:59 +0200 Subject: [PATCH 56/66] Update positionable.py --- manim/mobject/abstract/positionable.py | 33 ++++++++++++++------------ 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/manim/mobject/abstract/positionable.py b/manim/mobject/abstract/positionable.py index ae81b050e1..6344dc1891 100644 --- a/manim/mobject/abstract/positionable.py +++ b/manim/mobject/abstract/positionable.py @@ -1248,7 +1248,6 @@ def align_on_border( direction: Vector3DLike, *, buff: float = DEFAULT_MOBJECT_TO_EDGE_BUFFER, - frame: Point3DLike | None = None, ) -> Self: """Aligns the object on a border. @@ -1270,8 +1269,7 @@ def align_on_border( -------- :meth:`align_to` """ - if frame is None: - frame = (config.frame_x_radius, config.frame_y_radius, 0) + 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) @@ -1427,13 +1425,13 @@ def shift_onto_screen( The object itself. """ # TODO: Simplify/Optimize implementation - space_lengths = [config.frame_x_radius, config.frame_y_radius] - for vect in UP, DOWN, LEFT, RIGHT: - dim = np.argmax(np.abs(vect)) - max_val = space_lengths[dim] - buff - edge_center = self.get_position(vect) - if np.dot(edge_center, vect) > max_val: - self.to_edge(vect, buff=buff) + 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 ### ALIASES ### @@ -1471,7 +1469,7 @@ def center(self) -> Self: -------- :meth:`set_center` """ - return self.set_center(ORIGIN) + return self.set_center(center=ORIGIN) def flip( self, @@ -1522,7 +1520,7 @@ def construct(self): def move_to( self, - point_or_mobject: "Point3DLike | Positionable", + point: "Point3DLike | Positionable", aligned_edge: Vector3DLike = ORIGIN, ) -> Self: """Moves to a position. @@ -1544,12 +1542,13 @@ def move_to( :meth:`set_position` """ return self.set_position( - point=point_or_mobject, + point=point, aligned_edge=aligned_edge, ) def pose_at_angle( self, + *, about_point: Point3DLike | None = None, about_edge: Vector3DLike | None = None, ) -> Self: @@ -1788,6 +1787,7 @@ def stretch_to_fit( def stretch_to_fit_width( self, width: float, + *, about_point: Point3DLike | None = None, about_edge: Vector3DLike | None = None, ) -> Self: @@ -1836,6 +1836,7 @@ def stretch_to_fit_width( def stretch_to_fit_height( self, height: float, + *, about_point: Point3DLike | None = None, about_edge: Vector3DLike | None = None, ) -> Self: @@ -1884,6 +1885,7 @@ def stretch_to_fit_height( def stretch_to_fit_depth( self, depth: float, + *, about_point: Point3DLike | None = None, about_edge: Vector3DLike | None = None, ) -> Self: @@ -2095,14 +2097,14 @@ def apply_function_to_position( # @deprecated() def reduce_across_dimension( self, - reduce_func: Callable[[Iterable[float]], float], + function: Callable[[Iterable[float]], float], dim: int, ) -> float | None: points = self.get_points_defining_boundary() if len(points) == 0: return None - return reduce_func(points[:, dim]) + return function(points[:, dim]) # @deprecated(replacement="rotate") def rotate_about_origin( @@ -2121,6 +2123,7 @@ def rescale_to_fit( self, length: "float | Positionable", dim: int, + *, stretch: bool = False, about_point: Point3DLike | None = None, about_edge: Vector3DLike | None = None, From fed9dbf8eb3bf914e9ee8430924bbd4245bac584 Mon Sep 17 00:00:00 2001 From: GniLudio <50866361+GniLudio@users.noreply.github.com> Date: Fri, 28 Aug 2026 17:47:30 +0200 Subject: [PATCH 57/66] Use new union syntax --- manim/mobject/abstract/positionable.py | 42 ++++++++++++++------------ 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/manim/mobject/abstract/positionable.py b/manim/mobject/abstract/positionable.py index 6344dc1891..86ab6c1bc2 100644 --- a/manim/mobject/abstract/positionable.py +++ b/manim/mobject/abstract/positionable.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import operator as op from collections.abc import Callable, Iterable from functools import reduce @@ -57,7 +59,7 @@ def get_all_points(self) -> Point3D_Array: def set_points( self, - points: "Point3DLike_Array | Positionable", + points: Point3DLike_Array | Positionable, ) -> Self: """Sets the points. @@ -113,13 +115,13 @@ def get_points_defining_boundary(self) -> Point3D_Array: ### APPLYING FUNCTIONS ### - def get_family(self) -> Iterable["Positionable"]: + def get_family(self) -> Iterable[Positionable]: """Returns all family members recursively.""" yield self def apply_to_family( self, - function: Callable[["Positionable"], Any], + function: Callable[[Positionable], Any], *, only_with_points: bool = True, ) -> Self: @@ -542,7 +544,7 @@ def get_position( def set_position( self, - point: "Point3DLike | Positionable", + point: Point3DLike | Positionable, *, aligned_edge: Vector3DLike = ORIGIN, ) -> Self: @@ -586,7 +588,7 @@ def get_center(self) -> Point3D: def set_center( self, - center: "Point3DLike | Positionable", + center: Point3DLike | Positionable, ) -> Self: """Sets the center position. @@ -622,7 +624,7 @@ def get_left(self) -> Point3D: def set_left( self, - left: "Point3DLike | Positionable", + left: Point3DLike | Positionable, ) -> Self: """Sets the left position. @@ -658,7 +660,7 @@ def get_right(self) -> Point3D: def set_right( self, - right: "Point3DLike | Positionable", + right: Point3DLike | Positionable, ) -> Self: """Sets the right position. @@ -694,7 +696,7 @@ def get_bottom(self) -> Point3D: def set_bottom( self, - bottom: "Point3DLike | Positionable", + bottom: Point3DLike | Positionable, ) -> Self: """Sets the bottom position. @@ -730,7 +732,7 @@ def get_top(self) -> Point3D: def set_top( self, - top: "Point3DLike | Positionable", + top: Point3DLike | Positionable, ) -> Self: """Sets the top position. @@ -756,7 +758,7 @@ def get_nadir(self) -> Point3D: Returns ------- Point3D - The nadir position. + The nadir position. See also -------- @@ -766,7 +768,7 @@ def get_nadir(self) -> Point3D: def set_nadir( self, - nadir: "Point3DLike | Positionable", + nadir: Point3DLike | Positionable, ) -> Self: """Sets the nadir position. @@ -802,7 +804,7 @@ def get_zenith(self) -> Point3D: def set_zenith( self, - zenith: "Point3DLike | Positionable", + zenith: Point3DLike | Positionable, ) -> Self: """Sets the zenith position. @@ -853,7 +855,7 @@ def get_coordinate( def set_coordinate( self, - value: "float | Positionable", + value: float | Positionable, dim: int, direction: Vector3DLike = ORIGIN, ) -> Self: @@ -1029,7 +1031,7 @@ def get_dim_size(self, dim: int) -> float: def set_dim_size( self, - size: "float | Positionable", + size: float | Positionable, dim: int, *, stretch: bool = False, @@ -1098,7 +1100,7 @@ def get_width(self) -> float: def set_width( self, - width: "float | Positionable", + width: float | Positionable, *, stretch: bool = False, about_point: Point3DLike | None = None, @@ -1150,7 +1152,7 @@ def get_height(self) -> float: def set_height( self, - height: "float | Positionable", + height: float | Positionable, *, stretch: bool = False, about_point: Point3DLike | None = None, @@ -1204,7 +1206,7 @@ def get_depth(self) -> float: def set_depth( self, - depth: "float | Positionable", + depth: float | Positionable, *, stretch: bool = False, about_point: Point3DLike | None = None, @@ -1275,7 +1277,7 @@ def align_on_border( def align_to( self, - point: "Point3DLike | Positionable", + point: Point3DLike | Positionable, direction: Vector3DLike = ORIGIN, ) -> Self: """Aligns the object onto a point. @@ -1520,7 +1522,7 @@ def construct(self): def move_to( self, - point: "Point3DLike | Positionable", + point: Point3DLike | Positionable, aligned_edge: Vector3DLike = ORIGIN, ) -> Self: """Moves to a position. @@ -2121,7 +2123,7 @@ def rotate_about_origin( # @deprecated(replacement="set_dim_size") def rescale_to_fit( self, - length: "float | Positionable", + length: float | Positionable, dim: int, *, stretch: bool = False, From c729f602cdad760d22f189368f565a4ad3b4ae91 Mon Sep 17 00:00:00 2001 From: GniLudio <50866361+GniLudio@users.noreply.github.com> Date: Fri, 28 Aug 2026 18:35:43 +0200 Subject: [PATCH 58/66] Force dtype float for points --- manim/mobject/abstract/positionable.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manim/mobject/abstract/positionable.py b/manim/mobject/abstract/positionable.py index 86ab6c1bc2..0a91a64141 100644 --- a/manim/mobject/abstract/positionable.py +++ b/manim/mobject/abstract/positionable.py @@ -96,7 +96,7 @@ def construct(self): 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) + self.points = np.asarray(points, dtype=float) return self def get_points_defining_boundary(self) -> Point3D_Array: From 8e714e1e8c6ac9ae0091a44a37cacd984b4ae793 Mon Sep 17 00:00:00 2001 From: GniLudio <50866361+GniLudio@users.noreply.github.com> Date: Sat, 29 Aug 2026 12:30:42 +0200 Subject: [PATCH 59/66] Move apply_matrix --- manim/mobject/abstract/positionable.py | 70 +++++++++++++------------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/manim/mobject/abstract/positionable.py b/manim/mobject/abstract/positionable.py index 0a91a64141..a7442a658b 100644 --- a/manim/mobject/abstract/positionable.py +++ b/manim/mobject/abstract/positionable.py @@ -300,41 +300,6 @@ def apply(point: Point3D) -> Point3D: ### TRANSFORMATIONS ### - 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, - ) - def translate(self, vector: Vector3DLike) -> Self: """Applies a translation. @@ -508,6 +473,41 @@ def function(points: Point3D_Array) -> Point3D_Array: 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( From 6fe8b3a06153b41f0b1ce7ec0026574e238c0568 Mon Sep 17 00:00:00 2001 From: GniLudio <50866361+GniLudio@users.noreply.github.com> Date: Sat, 29 Aug 2026 16:54:24 +0200 Subject: [PATCH 60/66] Add tests --- tests/module/mobject/test_positioning.py | 336 +++++++++++++++++++++++ 1 file changed, 336 insertions(+) create mode 100644 tests/module/mobject/test_positioning.py diff --git a/tests/module/mobject/test_positioning.py b/tests/module/mobject/test_positioning.py new file mode 100644 index 0000000000..f1e8d2fb28 --- /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(value=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(value=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(value=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), + ], + ) From b0e64e9d43f7eb9f0c4ca623e19b6256a5933e4f Mon Sep 17 00:00:00 2001 From: GniLudio <50866361+GniLudio@users.noreply.github.com> Date: Sat, 29 Aug 2026 20:17:58 +0200 Subject: [PATCH 61/66] Deprecates more methods, sort methods and add missing docs --- manim/mobject/abstract/positionable.py | 858 +++++++++++++---------- tests/module/mobject/test_positioning.py | 6 +- 2 files changed, 508 insertions(+), 356 deletions(-) diff --git a/manim/mobject/abstract/positionable.py b/manim/mobject/abstract/positionable.py index a7442a658b..b5b8e13458 100644 --- a/manim/mobject/abstract/positionable.py +++ b/manim/mobject/abstract/positionable.py @@ -519,7 +519,7 @@ def get_position( Parameters ---------- direction : Vector3DLike, optional - TODO, by default ORIGIN + The direction., by default ORIGIN Returns ------- @@ -836,7 +836,7 @@ def get_coordinate( dim : int The dimension. direction : Vector3DLike, optional - TODO, by default ORIGIN + The direction., by default ORIGIN Returns ------- @@ -855,7 +855,7 @@ def get_coordinate( def set_coordinate( self, - value: float | Positionable, + coordinate: float | Positionable, dim: int, direction: Vector3DLike = ORIGIN, ) -> Self: @@ -868,7 +868,7 @@ def set_coordinate( dim : int The dimension. direction : Vector3DLike, optional - TODO, by default ORIGIN + The direction., by default ORIGIN Returns ------- @@ -879,11 +879,11 @@ def set_coordinate( -------- :meth:`get_coordinate` """ - if isinstance(value, Positionable): - value = value.get_coordinate(dim=dim, direction=direction) + 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] = value - current + vector[dim] = coordinate - current return self.translate(vector=vector) def get_x(self, direction: Vector3DLike = ORIGIN) -> float: @@ -892,7 +892,7 @@ def get_x(self, direction: Vector3DLike = ORIGIN) -> float: Parameters ---------- direction : Vector3DLike, optional - TODO, by default ORIGIN + The direction., by default ORIGIN Returns ------- @@ -905,15 +905,15 @@ def get_x(self, direction: Vector3DLike = ORIGIN) -> float: """ return self.get_coordinate(dim=0, direction=direction) - def set_x(self, x: float, direction: Vector3DLike = ORIGIN) -> Self: + def set_x(self, x: float | Positionable, direction: Vector3DLike = ORIGIN) -> Self: """Sets the x coordinate. Parameters ---------- - x : float + x : float | Positionable The x coordinate. direction : Vector3DLike, optional - TODO, by default ORIGIN + The direction., by default ORIGIN Returns ------- @@ -924,7 +924,7 @@ def set_x(self, x: float, direction: Vector3DLike = ORIGIN) -> Self: -------- :meth:`get_x`, :meth:`set_coordinate` """ - return self.set_coordinate(value=x, dim=0, direction=direction) + return self.set_coordinate(coordinate=x, dim=0, direction=direction) def get_y(self, direction: Vector3DLike = ORIGIN) -> float: """Returns the y coordinate. @@ -932,7 +932,7 @@ def get_y(self, direction: Vector3DLike = ORIGIN) -> float: Parameters ---------- direction : Vector3DLike, optional - TODO, by default ORIGIN + The direction., by default ORIGIN Returns ------- @@ -945,15 +945,15 @@ def get_y(self, direction: Vector3DLike = ORIGIN) -> float: """ return self.get_coordinate(dim=1, direction=direction) - def set_y(self, y: float, direction: Vector3DLike = ORIGIN) -> Self: + def set_y(self, y: float | Positionable, direction: Vector3DLike = ORIGIN) -> Self: """Sets the y coordinate. Parameters ---------- - y : float + y : float | Positionable The y coordinate. direction : Vector3DLike, optional - TODO, by default ORIGIN + The direction., by default ORIGIN Returns ------- @@ -964,7 +964,7 @@ def set_y(self, y: float, direction: Vector3DLike = ORIGIN) -> Self: -------- :meth:`get_y`, :meth:`set_coordinate` """ - return self.set_coordinate(value=y, dim=1, direction=direction) + return self.set_coordinate(coordinate=y, dim=1, direction=direction) def get_z(self, direction: Vector3DLike = ORIGIN) -> float: """Returns the z coordinate. @@ -972,7 +972,7 @@ def get_z(self, direction: Vector3DLike = ORIGIN) -> float: Parameters ---------- direction : Vector3DLike, optional - TODO, by default ORIGIN + The direction., by default ORIGIN Returns ------- @@ -985,15 +985,15 @@ def get_z(self, direction: Vector3DLike = ORIGIN) -> float: """ return self.get_coordinate(dim=2, direction=direction) - def set_z(self, z: float, direction: Vector3DLike = ORIGIN) -> Self: + def set_z(self, z: float | Positionable, direction: Vector3DLike = ORIGIN) -> Self: """Sets the z coordinate. Parameters ---------- - z : float + z : float | Positionable The z coordinate. direction : Vector3DLike, optional - TODO, by default ORIGIN + The direction., by default ORIGIN Returns ------- @@ -1004,7 +1004,7 @@ def set_z(self, z: float, direction: Vector3DLike = ORIGIN) -> Self: -------- :meth:`get_z`, :meth:`get_coordinate` """ - return self.set_coordinate(value=z, dim=2, direction=direction) + return self.set_coordinate(coordinate=z, dim=2, direction=direction) def get_dim_size(self, dim: int) -> float: """Returns the size of a dimension. @@ -1287,7 +1287,7 @@ def align_to( mobject_or_point : Point3DLike | Positionable The point. direction : Vector3DLike, optional - TODO, by default ORIGIN + The direction., by default ORIGIN Returns ------- @@ -1304,6 +1304,67 @@ def align_to( 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. @@ -1319,34 +1380,6 @@ def get_bounding_box(self) -> tuple[Point3D, Point3D]: maxs = points.max(axis=0) return (mins, maxs) - def get_extremum_along_dim( - self, - dim: int = 0, - key: int = 0, - ) -> float: - 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 get_center_of_mass(self) -> Point3D: """Returns the center of mass. @@ -1366,7 +1399,7 @@ def get_boundary_point(self, direction: Vector3DLike) -> Point3D: Parameters ---------- direction : Vector3DLike - TODO + The direction. Returns ------- @@ -1377,6 +1410,47 @@ def get_boundary_point(self, direction: Vector3DLike) -> Point3D: 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. @@ -1409,6 +1483,48 @@ def is_off_screen(self) -> bool: ) ) + 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, *, @@ -1436,31 +1552,26 @@ def shift_onto_screen( self.to_edge(edge, buff=buff) return self - ### ALIASES ### - get_critical_point = get_position - get_edge_center = get_position - get_corner = get_position - length_over_dim = get_dim_size - get_coord = get_coordinate - set_coord = set_coordinate - - def shift(self, *vectors: Vector3DLike) -> Self: - """_summary_ + 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 ---------- - vectors: *Vector3DLike - The vectors. - - Returns - ------- - Self - The object itself. - """ - return self.translate(vector=reduce(op.add, vectors)) - - def center(self) -> Self: - """Moves to the ORIGIN. + 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 ------- @@ -1469,27 +1580,33 @@ def center(self) -> Self: See also -------- - :meth:`set_center` + :meth:`set_dim_size`, :meth:`scale` """ - return self.set_center(center=ORIGIN) + return self.set_dim_size( + size=size, + dim=dim, + stretch=False, + about_point=about_point, + about_edge=about_edge, + ) - def flip( + def scale_to_fit_width( self, - axis: Vector3DLike = UP, + width: float, *, about_point: Point3DLike | None = None, about_edge: Vector3DLike | None = None, ) -> Self: - """Flips. + """Scales to fit a width. Parameters ---------- - axis : Vector3DLike, optional - The axis about which to flip., by default UP + width : float + The width. about_point : Point3DLike | None, optional - The point about which to flip., by default None + The point about which scale., by default None about_edge : Vector3DLike | None, optional - The edge about which to flip., by default None + The point about which to scale., by default None Returns ------- @@ -1498,175 +1615,31 @@ def flip( 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) + >>> 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:`rotate` + :meth:`scale_to_fit`, :meth:`scale`, :meth:`set_width` """ - return self.rotate( - TAU / 2, - axis, + return self.scale_to_fit( + size=width, + dim=0, about_point=about_point, about_edge=about_edge, ) - 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, - ) - - 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 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( + def scale_to_fit_height( self, height: float, *, @@ -1918,6 +1891,292 @@ def stretch_to_fit_depth( 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, + ) -> 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, @@ -1960,6 +2219,7 @@ def construct(self): """ return self.align_on_border(direction=corner, buff=buff) + # @deprecated(replacement="align_on_border") def to_edge( self, edge: Vector3DLike = LEFT, @@ -2003,6 +2263,7 @@ def construct(self): return self.align_on_border(direction=edge, buff=buff) @property + # @deprecated(replacement="(get|set)_width") def width(self) -> float: """The width. @@ -2029,115 +2290,6 @@ def construct(self): return self.get_width() @width.setter + # @deprecated(replacement="(get|set)_width") def width(self, value: float) -> None: self.set_width(width=value) - - @property - 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 - def height(self, value: float) -> None: - self.set_height(height=value) - - @property - def depth(self) -> float: - """The width. - - See also - -------- - :meth:`get_depth`, :meth:`set_depth` - """ - return self.get_depth() - - @depth.setter - def depth(self, value: float) -> None: - self.set_depth(depth=value) - - ### DEPRECATED ### - - apply_points_function_about_point = apply_array_function - match_points = set_points - match_coord = set_coordinate - match_x = set_x - match_y = set_y - match_z = set_z - match_dim_size = set_dim_size - match_width = set_width - match_height = set_height - match_depth = set_depth - - # @deprecated(replacement="move_to(function(self.get_center()))") - def apply_function_to_position( - self, - function: Callable[[Point3D], Point3DLike], - ) -> Self: - return self.move_to(function(self.get_center())) - - # @deprecated() - 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]) - - # @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="stretch") - def stretch_about_point(self, factor: float, dim: int, point: Point3DLike) -> Self: - return self.stretch(factor=factor, dim=dim, about_point=point) diff --git a/tests/module/mobject/test_positioning.py b/tests/module/mobject/test_positioning.py index f1e8d2fb28..92bbe14510 100644 --- a/tests/module/mobject/test_positioning.py +++ b/tests/module/mobject/test_positioning.py @@ -262,7 +262,7 @@ def test_set_coordinate_x() -> None: (+1, +2, +3), ] ) - p.set_coordinate(value=4, dim=0) + p.set_coordinate(coordinate=4, dim=0) np.testing.assert_allclose( p.points, [ @@ -291,7 +291,7 @@ def test_set_coordinate_y() -> None: (+1, +2, +3), ] ) - p.set_coordinate(value=5, dim=1) + p.set_coordinate(coordinate=5, dim=1) np.testing.assert_allclose( p.points, [ @@ -320,7 +320,7 @@ def test_set_coordinate_z() -> None: (+1, +2, +3), ] ) - p.set_coordinate(value=6, dim=2) + p.set_coordinate(coordinate=6, dim=2) np.testing.assert_allclose( p.points, [ From 286d08d0fb1086ad6249ccfab5501de6a52862e5 Mon Sep 17 00:00:00 2001 From: GniLudio <50866361+GniLudio@users.noreply.github.com> Date: Sat, 29 Aug 2026 21:16:16 +0200 Subject: [PATCH 62/66] Add default parameter --- manim/mobject/abstract/positionable.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manim/mobject/abstract/positionable.py b/manim/mobject/abstract/positionable.py index b5b8e13458..96f978deb3 100644 --- a/manim/mobject/abstract/positionable.py +++ b/manim/mobject/abstract/positionable.py @@ -2152,7 +2152,7 @@ def set_coord( self, value: float | Positionable, dim: int, - direction: Vector3DLike, + direction: Vector3DLike = ORIGIN, ) -> Self: return self.set_coordinate(coordinate=value, dim=dim, direction=direction) From 0cadfeb9e766080986e57b38ff4b5ec718b675be Mon Sep 17 00:00:00 2001 From: GniLudio <50866361+GniLudio@users.noreply.github.com> Date: Sun, 30 Aug 2026 11:02:55 +0200 Subject: [PATCH 63/66] Skip point aggregation --- manim/mobject/abstract/positionable.py | 109 ++++++++++++++----------- 1 file changed, 62 insertions(+), 47 deletions(-) diff --git a/manim/mobject/abstract/positionable.py b/manim/mobject/abstract/positionable.py index 96f978deb3..4b473dade9 100644 --- a/manim/mobject/abstract/positionable.py +++ b/manim/mobject/abstract/positionable.py @@ -3,7 +3,7 @@ import operator as op from collections.abc import Callable, Iterable from functools import reduce -from typing import Any, Self +from typing import Any, Self, TypeVar import numpy as np @@ -30,6 +30,8 @@ ) from manim.utils.space_ops import rotation_matrix +T = TypeVar("T") + class Positionable: """A positionable object.""" @@ -298,6 +300,43 @@ def apply(point: Point3D) -> Point3D: about_edge=about_edge, ) + def reduce_points( + self, + function: Callable[[Point3D_Array], T], + aggregator: Callable[[Iterable[T]], T], + *, + default: T | None = None, + only_with_points: bool = True, + ) -> T: + """Reduces the points to a value. + + Parameters + ---------- + function + The reduce function. + aggregator + The aggregator function. + default, optional + The default value., by default None + only_with_points, optional + Whether to only use members with points., by default True + + Returns + ------- + The value. + """ + if only_with_points: + values = [ + function(mob.points) for mob in self.get_family() if len(mob.points) > 0 + ] + else: + values = [function(mob.points) for mob in self.get_family()] + + if len(values) == 0 and default is not None: + return default + + return aggregator(values) + ### TRANSFORMATIONS ### def translate(self, vector: Vector3DLike) -> Self: @@ -530,11 +569,9 @@ def get_position( -------- :meth:`set_position` """ - points = self.get_points_defining_boundary() return np.array( [ - self._get_extremum_along_dim( - points=points, + self.get_extremum_along_dim( dim=dim, key=key, ) @@ -847,11 +884,7 @@ def get_coordinate( -------- :meth:`set_coordinate` """ - return self._get_extremum_along_dim( - points=self.get_points_defining_boundary(), - dim=dim, - key=np.sign(direction[dim]), - ) + return self.get_extremum_along_dim(dim=dim, key=np.sign(direction[dim])) def set_coordinate( self, @@ -1428,28 +1461,23 @@ def get_extremum_along_dim( ------- 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 reduce(values: np.ndarray) -> float: + return ( # type: ignore[no-any-return] + values.min() + if key < 0 + else (values.min() + values.max()) / 2 + if key == 0 + else values.max() + ) + + def function(points: Point3D_Array) -> float: + return reduce(points[:, dim]) + + def aggregator(values: Iterable[float]) -> float: + return reduce(np.array(values)) + + return self.reduce_points(function=function, aggregator=aggregator, default=0.0) def is_off_screen(self) -> bool: """Returns whether this is off screen. @@ -1459,28 +1487,15 @@ def is_off_screen(self) -> bool: 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 - ) + (self.get_extremum_along_dim(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 - ) + or (self.get_extremum_along_dim(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 - ) + or (self.get_extremum_along_dim(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 - ) + or (self.get_extremum_along_dim(dim=1, key=1) < -config.frame_y_radius) ) def pose_at_angle( From 6adf786a8df2149dd92ca769155a95789511e6e2 Mon Sep 17 00:00:00 2001 From: GniLudio <50866361+GniLudio@users.noreply.github.com> Date: Sun, 30 Aug 2026 14:14:58 +0200 Subject: [PATCH 64/66] Fix and use reduce_points --- manim/mobject/abstract/positionable.py | 194 +++++++++++++++++-------- 1 file changed, 135 insertions(+), 59 deletions(-) diff --git a/manim/mobject/abstract/positionable.py b/manim/mobject/abstract/positionable.py index 4b473dade9..b74c828242 100644 --- a/manim/mobject/abstract/positionable.py +++ b/manim/mobject/abstract/positionable.py @@ -3,7 +3,7 @@ import operator as op from collections.abc import Callable, Iterable from functools import reduce -from typing import Any, Self, TypeVar +from typing import Any, Literal, Self, TypeVar import numpy as np @@ -31,6 +31,7 @@ from manim.utils.space_ops import rotation_matrix T = TypeVar("T") +R = TypeVar("R") class Positionable: @@ -51,13 +52,17 @@ def get_all_points(self) -> Point3D_Array: -------- :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 aggregate(values: Iterable[Point3D_Array]) -> Point3D_Array: + values = list(values) + if len(values) == 1: + return values[0] + return np.concat(values, axis=0) + + return self.reduce_points( + function=lambda points: points, + aggregate=aggregate, + ) def set_points( self, @@ -115,6 +120,9 @@ def get_points_defining_boundary(self) -> Point3D_Array: """ return self.get_all_points() + def get_anchors(self) -> Iterable[Point3D]: + return self.points # type: ignore[no-any-return] + ### APPLYING FUNCTIONS ### def get_family(self) -> Iterable[Positionable]: @@ -303,11 +311,12 @@ def apply(point: Point3D) -> Point3D: def reduce_points( self, function: Callable[[Point3D_Array], T], - aggregator: Callable[[Iterable[T]], T], + aggregate: Callable[[Iterable[T]], R], *, - default: T | None = None, + default: R | None = None, only_with_points: bool = True, - ) -> T: + which_points: Literal["all", "boundary", "anchors", "points"] = "points", + ) -> R: """Reduces the points to a value. Parameters @@ -320,22 +329,39 @@ def reduce_points( The default value., by default None only_with_points, optional Whether to only use members with points., by default True + which_points, optional + Which points to use., by default "anchors" + """ + if which_points == "all" or which_points == "boundary": + points = ( + self.get_all_points() + if which_points == "all" + else self.get_points_defining_boundary() + ) + if len(points) == 0 and default is not None: + return default + return aggregate([function(points)]) - Returns - ------- - The value. - """ - if only_with_points: - values = [ - function(mob.points) for mob in self.get_family() if len(mob.points) > 0 - ] - else: - values = [function(mob.points) for mob in self.get_family()] + def get_points(mob: Positionable) -> Point3D_Array | None: + match which_points: + case "anchors": + return np.array(mob.get_anchors()) + case "points": + return mob.points + + return None + + values = [ + function(points) + for mob in self.get_family() + if (points := get_points(mob)) is not None + and (not only_with_points or len(points) > 0) + ] if len(values) == 0 and default is not None: return default - return aggregator(values) + return aggregate(values) ### TRANSFORMATIONS ### @@ -1056,11 +1082,18 @@ def get_dim_size(self, dim: int) -> float: -------- :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] + # FIXME: Changing which_points to `anchors` causes some of the `tests/test_graphical_units/test_img_and_svg.py` tests to fail + low = self.reduce_points( + function=lambda points: points[:, dim].min(), + aggregate=min, + default=0.0, + ) + high = self.reduce_points( + function=lambda points: points[:, dim].max(), + aggregate=max, + default=0.0, + ) + return high - low def set_dim_size( self, @@ -1406,11 +1439,18 @@ def get_bounding_box(self) -> tuple[Point3D, Point3D]: 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) + mins = self.reduce_points( + function=lambda points: points.min(), + aggregate=lambda values: np.array(values).min(), + default=np.zeros(3), + which_points="anchors", + ) + maxs = self.reduce_points( + function=lambda points: points.min(), + aggregate=lambda values: np.array(values).min(), + default=np.zeros(3), + which_points="anchors", + ) return (mins, maxs) def get_center_of_mass(self) -> Point3D: @@ -1421,10 +1461,20 @@ def get_center_of_mass(self) -> Point3D: Point3D The center of mass. """ - points = self.get_all_points() - if len(points) == 0: - return ORIGIN - return points.mean(axis=0) + + def function(points: Point3D_Array) -> tuple[int, Point3D]: + return len(points), points.mean(axis=0) + + def aggregate(values: Iterable[tuple[int, Point3D]]) -> Point3D: + counts = np.array([n for n, _ in values]) + points = np.array([p for _, p in values]) + return counts / counts.sum() * points + + return self.reduce_points( + function=function, + aggregate=aggregate, + default=ORIGIN, + ) def get_boundary_point(self, direction: Vector3DLike) -> Point3D: """Returns a boundary point. @@ -1439,9 +1489,18 @@ def get_boundary_point(self, direction: Vector3DLike) -> Point3D: Point3D The boundary point. """ - points = self.get_points_defining_boundary() - index = np.argmax(points.dot(direction)) - return points[index] + + def function(points: Point3D_Array) -> Point3D: + return points[np.argmax(points.dot(direction))] + + def aggregate(values: Iterable[Point3D]) -> Point3D: + return function(np.array(values)) + + return self.reduce_points( + function=function, + aggregate=aggregate, + default=ORIGIN, + ) def get_extremum_along_dim( self, @@ -1461,23 +1520,34 @@ def get_extremum_along_dim( ------- The value. """ - - def reduce(values: np.ndarray) -> float: - return ( # type: ignore[no-any-return] - values.min() - if key < 0 - else (values.min() + values.max()) / 2 - if key == 0 - else values.max() + if key < 0: + return self.reduce_points( + function=lambda points: points[:, dim].min(), + aggregate=min, + default=0.0, + which_points="anchors", ) - - def function(points: Point3D_Array) -> float: - return reduce(points[:, dim]) - - def aggregator(values: Iterable[float]) -> float: - return reduce(np.array(values)) - - return self.reduce_points(function=function, aggregator=aggregator, default=0.0) + elif key > 0: + return self.reduce_points( + function=lambda points: points[:, dim].max(), + aggregate=max, + default=0.0, + which_points="anchors", + ) + else: + low = self.reduce_points( + function=lambda points: points[:, dim].min(), + aggregate=min, + default=0.0, + which_points="anchors", + ) + high = self.reduce_points( + function=lambda points: points[:, dim].max(), + aggregate=max, + default=0.0, + which_points="anchors", + ) + return (low + high) / 2 def is_off_screen(self) -> bool: """Returns whether this is off screen. @@ -1534,11 +1604,17 @@ def reduce_across_dimension( function: Callable[[Iterable[float]], float], dim: int, ) -> float | None: - points = self.get_points_defining_boundary() - if len(points) == 0: - return None + def aggregate(values: Iterable[float]) -> float | None: + values = list(values) + if len(values) == 0: + return None - return function(points[:, dim]) + return function(values) + + return self.reduce_points( + function=lambda points: function(points[:, dim]), + aggregate=aggregate, + ) def shift_onto_screen( self, From 8758685e8d3b1891670a6906d939a4f3542407b7 Mon Sep 17 00:00:00 2001 From: GniLudio <50866361+GniLudio@users.noreply.github.com> Date: Sun, 30 Aug 2026 17:57:13 +0200 Subject: [PATCH 65/66] Revert "Fix and use reduce_points" This reverts commit 6adf786a8df2149dd92ca769155a95789511e6e2. --- manim/mobject/abstract/positionable.py | 194 ++++++++----------------- 1 file changed, 59 insertions(+), 135 deletions(-) diff --git a/manim/mobject/abstract/positionable.py b/manim/mobject/abstract/positionable.py index b74c828242..4b473dade9 100644 --- a/manim/mobject/abstract/positionable.py +++ b/manim/mobject/abstract/positionable.py @@ -3,7 +3,7 @@ import operator as op from collections.abc import Callable, Iterable from functools import reduce -from typing import Any, Literal, Self, TypeVar +from typing import Any, Self, TypeVar import numpy as np @@ -31,7 +31,6 @@ from manim.utils.space_ops import rotation_matrix T = TypeVar("T") -R = TypeVar("R") class Positionable: @@ -52,17 +51,13 @@ def get_all_points(self) -> Point3D_Array: -------- :meth:`set_points` """ - - def aggregate(values: Iterable[Point3D_Array]) -> Point3D_Array: - values = list(values) - if len(values) == 1: - return values[0] - return np.concat(values, axis=0) - - return self.reduce_points( - function=lambda points: points, - aggregate=aggregate, - ) + 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, @@ -120,9 +115,6 @@ def get_points_defining_boundary(self) -> Point3D_Array: """ return self.get_all_points() - def get_anchors(self) -> Iterable[Point3D]: - return self.points # type: ignore[no-any-return] - ### APPLYING FUNCTIONS ### def get_family(self) -> Iterable[Positionable]: @@ -311,12 +303,11 @@ def apply(point: Point3D) -> Point3D: def reduce_points( self, function: Callable[[Point3D_Array], T], - aggregate: Callable[[Iterable[T]], R], + aggregator: Callable[[Iterable[T]], T], *, - default: R | None = None, + default: T | None = None, only_with_points: bool = True, - which_points: Literal["all", "boundary", "anchors", "points"] = "points", - ) -> R: + ) -> T: """Reduces the points to a value. Parameters @@ -329,39 +320,22 @@ def reduce_points( The default value., by default None only_with_points, optional Whether to only use members with points., by default True - which_points, optional - Which points to use., by default "anchors" - """ - if which_points == "all" or which_points == "boundary": - points = ( - self.get_all_points() - if which_points == "all" - else self.get_points_defining_boundary() - ) - if len(points) == 0 and default is not None: - return default - return aggregate([function(points)]) - - def get_points(mob: Positionable) -> Point3D_Array | None: - match which_points: - case "anchors": - return np.array(mob.get_anchors()) - case "points": - return mob.points - return None - - values = [ - function(points) - for mob in self.get_family() - if (points := get_points(mob)) is not None - and (not only_with_points or len(points) > 0) - ] + Returns + ------- + The value. + """ + if only_with_points: + values = [ + function(mob.points) for mob in self.get_family() if len(mob.points) > 0 + ] + else: + values = [function(mob.points) for mob in self.get_family()] if len(values) == 0 and default is not None: return default - return aggregate(values) + return aggregator(values) ### TRANSFORMATIONS ### @@ -1082,18 +1056,11 @@ def get_dim_size(self, dim: int) -> float: -------- :meth:`set_dim_size` """ - # FIXME: Changing which_points to `anchors` causes some of the `tests/test_graphical_units/test_img_and_svg.py` tests to fail - low = self.reduce_points( - function=lambda points: points[:, dim].min(), - aggregate=min, - default=0.0, - ) - high = self.reduce_points( - function=lambda points: points[:, dim].max(), - aggregate=max, - default=0.0, - ) - return high - low + # 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, @@ -1439,18 +1406,11 @@ def get_bounding_box(self) -> tuple[Point3D, Point3D]: tuple[Point3D, Point3D] The bottom-left and top-right points. """ - mins = self.reduce_points( - function=lambda points: points.min(), - aggregate=lambda values: np.array(values).min(), - default=np.zeros(3), - which_points="anchors", - ) - maxs = self.reduce_points( - function=lambda points: points.min(), - aggregate=lambda values: np.array(values).min(), - default=np.zeros(3), - which_points="anchors", - ) + 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: @@ -1461,20 +1421,10 @@ def get_center_of_mass(self) -> Point3D: Point3D The center of mass. """ - - def function(points: Point3D_Array) -> tuple[int, Point3D]: - return len(points), points.mean(axis=0) - - def aggregate(values: Iterable[tuple[int, Point3D]]) -> Point3D: - counts = np.array([n for n, _ in values]) - points = np.array([p for _, p in values]) - return counts / counts.sum() * points - - return self.reduce_points( - function=function, - aggregate=aggregate, - default=ORIGIN, - ) + 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. @@ -1489,18 +1439,9 @@ def get_boundary_point(self, direction: Vector3DLike) -> Point3D: Point3D The boundary point. """ - - def function(points: Point3D_Array) -> Point3D: - return points[np.argmax(points.dot(direction))] - - def aggregate(values: Iterable[Point3D]) -> Point3D: - return function(np.array(values)) - - return self.reduce_points( - function=function, - aggregate=aggregate, - default=ORIGIN, - ) + points = self.get_points_defining_boundary() + index = np.argmax(points.dot(direction)) + return points[index] def get_extremum_along_dim( self, @@ -1520,34 +1461,23 @@ def get_extremum_along_dim( ------- The value. """ - if key < 0: - return self.reduce_points( - function=lambda points: points[:, dim].min(), - aggregate=min, - default=0.0, - which_points="anchors", - ) - elif key > 0: - return self.reduce_points( - function=lambda points: points[:, dim].max(), - aggregate=max, - default=0.0, - which_points="anchors", - ) - else: - low = self.reduce_points( - function=lambda points: points[:, dim].min(), - aggregate=min, - default=0.0, - which_points="anchors", - ) - high = self.reduce_points( - function=lambda points: points[:, dim].max(), - aggregate=max, - default=0.0, - which_points="anchors", + + def reduce(values: np.ndarray) -> float: + return ( # type: ignore[no-any-return] + values.min() + if key < 0 + else (values.min() + values.max()) / 2 + if key == 0 + else values.max() ) - return (low + high) / 2 + + def function(points: Point3D_Array) -> float: + return reduce(points[:, dim]) + + def aggregator(values: Iterable[float]) -> float: + return reduce(np.array(values)) + + return self.reduce_points(function=function, aggregator=aggregator, default=0.0) def is_off_screen(self) -> bool: """Returns whether this is off screen. @@ -1604,17 +1534,11 @@ def reduce_across_dimension( function: Callable[[Iterable[float]], float], dim: int, ) -> float | None: - def aggregate(values: Iterable[float]) -> float | None: - values = list(values) - if len(values) == 0: - return None - - return function(values) + points = self.get_points_defining_boundary() + if len(points) == 0: + return None - return self.reduce_points( - function=lambda points: function(points[:, dim]), - aggregate=aggregate, - ) + return function(points[:, dim]) def shift_onto_screen( self, From caaa490a54add3efd8c450e0971b4386361be92c Mon Sep 17 00:00:00 2001 From: GniLudio <50866361+GniLudio@users.noreply.github.com> Date: Sun, 30 Aug 2026 17:57:17 +0200 Subject: [PATCH 66/66] Revert "Skip point aggregation" This reverts commit 0cadfeb9e766080986e57b38ff4b5ec718b675be. --- manim/mobject/abstract/positionable.py | 109 +++++++++++-------------- 1 file changed, 47 insertions(+), 62 deletions(-) diff --git a/manim/mobject/abstract/positionable.py b/manim/mobject/abstract/positionable.py index 4b473dade9..96f978deb3 100644 --- a/manim/mobject/abstract/positionable.py +++ b/manim/mobject/abstract/positionable.py @@ -3,7 +3,7 @@ import operator as op from collections.abc import Callable, Iterable from functools import reduce -from typing import Any, Self, TypeVar +from typing import Any, Self import numpy as np @@ -30,8 +30,6 @@ ) from manim.utils.space_ops import rotation_matrix -T = TypeVar("T") - class Positionable: """A positionable object.""" @@ -300,43 +298,6 @@ def apply(point: Point3D) -> Point3D: about_edge=about_edge, ) - def reduce_points( - self, - function: Callable[[Point3D_Array], T], - aggregator: Callable[[Iterable[T]], T], - *, - default: T | None = None, - only_with_points: bool = True, - ) -> T: - """Reduces the points to a value. - - Parameters - ---------- - function - The reduce function. - aggregator - The aggregator function. - default, optional - The default value., by default None - only_with_points, optional - Whether to only use members with points., by default True - - Returns - ------- - The value. - """ - if only_with_points: - values = [ - function(mob.points) for mob in self.get_family() if len(mob.points) > 0 - ] - else: - values = [function(mob.points) for mob in self.get_family()] - - if len(values) == 0 and default is not None: - return default - - return aggregator(values) - ### TRANSFORMATIONS ### def translate(self, vector: Vector3DLike) -> Self: @@ -569,9 +530,11 @@ def get_position( -------- :meth:`set_position` """ + points = self.get_points_defining_boundary() return np.array( [ - self.get_extremum_along_dim( + self._get_extremum_along_dim( + points=points, dim=dim, key=key, ) @@ -884,7 +847,11 @@ def get_coordinate( -------- :meth:`set_coordinate` """ - return self.get_extremum_along_dim(dim=dim, key=np.sign(direction[dim])) + return self._get_extremum_along_dim( + points=self.get_points_defining_boundary(), + dim=dim, + key=np.sign(direction[dim]), + ) def set_coordinate( self, @@ -1461,23 +1428,28 @@ def get_extremum_along_dim( ------- The value. """ + return self._get_extremum_along_dim( + self.get_points_defining_boundary(), + dim=dim, + key=key, + ) - def reduce(values: np.ndarray) -> float: - return ( # type: ignore[no-any-return] - values.min() - if key < 0 - else (values.min() + values.max()) / 2 - if key == 0 - else values.max() - ) - - def function(points: Point3D_Array) -> float: - return reduce(points[:, dim]) - - def aggregator(values: Iterable[float]) -> float: - return reduce(np.array(values)) - - return self.reduce_points(function=function, aggregator=aggregator, default=0.0) + 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. @@ -1487,15 +1459,28 @@ def is_off_screen(self) -> bool: bool Is off screen. """ + points = self.get_points_defining_boundary() return ( # left is too right - (self.get_extremum_along_dim(dim=0, key=-1) > config.frame_x_radius) + ( + 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(dim=0, key=1) < -config.frame_x_radius) + 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(dim=1, key=-1) > config.frame_y_radius) + 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(dim=1, key=1) < -config.frame_y_radius) + or ( + self._get_extremum_along_dim(points=points, dim=1, key=1) + < -config.frame_y_radius + ) ) def pose_at_angle(