Fix empty submobjects distorting width/height of parent Mobject (#4088)

* Issue-4087 Add bug test

* Issue-4087 Fix bug

* Issue-4087 Fix comment and add type hint

* Issue-4087 Fix comment

* Issue-4087 Improve previous test instead of adding a new one

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Update tests/module/mobject/mobject/test_mobject.py

* Enhance docstring for reduce_across_dimension method

Updated docstring for reduce_across_dimension method to clarify its purpose and parameters.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Remove float typehints for coords in length_over_dim

* Modify test for empty VMobject dimensions

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: Francisco Manríquez Novoa <49853152+chopan050@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
Guillaume Vauvert 2026-06-10 22:21:52 +02:00 committed by GitHub
commit 852ebd1c60
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 60 additions and 26 deletions

View file

@ -2162,13 +2162,31 @@ class Mobject:
def reduce_across_dimension(
self, reduce_func: Callable[[Iterable[float]], float], dim: int
) -> float:
"""Find the min or max value from a dimension across all points in this and submobjects."""
) -> 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 0 (e.g. center)
return 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.
@ -2181,8 +2199,10 @@ class Mobject:
# smallest dimension they have and compare it to the return value.
for mobj in self.submobjects:
value = mobj.reduce_across_dimension(reduce_func, dim)
rv = value if rv is None else reduce_func([value, rv])
assert rv is not None
if rv is None:
rv = value
elif value is not None:
rv = reduce_func([value, rv])
return rv
def nonempty_submobjects(self) -> Sequence[Mobject]:
@ -2336,11 +2356,10 @@ class Mobject:
def length_over_dim(self, dim: int) -> float:
"""Measure the length of an :class:`~.Mobject` in a certain direction."""
max_coord: float = self.reduce_across_dimension(
max,
dim,
)
min_coord: float = self.reduce_across_dimension(min, dim)
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:

View file

@ -3,7 +3,20 @@ from __future__ import annotations
import numpy as np
import pytest
from manim import DL, PI, UR, Circle, Mobject, Rectangle, Square, Triangle, VGroup
from manim import (
DL,
DR,
PI,
UL,
UR,
Circle,
Mobject,
Rectangle,
Square,
Triangle,
VGroup,
VMobject,
)
def test_mobject_add():
@ -135,22 +148,24 @@ def test_mobject_dimensions_nested_mobjects():
assert is_close(vg.depth, 0.775), vg.depth
def test_mobject_dimensions_mobjects_with_no_points_are_at_origin():
rect = Rectangle(width=2, height=3)
rect.move_to([-4, -5, 0])
outer_group = VGroup(rect)
def test_mobject_dimensions_mobjects_with_no_points():
empty_mob = VMobject()
assert empty_mob.width == 0
assert empty_mob.height == 0
# This is as one would expect
assert outer_group.width == 2
assert outer_group.height == 3
for direction in [DL, DR, UL, UR]:
rect = Rectangle(width=2, height=3)
rect.move_to(direction * 10)
outer_group = VGroup(rect)
# Adding a mobject with no points has a quirk of adding a "point"
# to [0, 0, 0] (the origin). This changes the size of the outer
# group because now the bottom left corner is at [-5, -6.5, 0]
# but the upper right corner is [0, 0, 0] instead of [-3, -3.5, 0]
outer_group.add(VGroup())
assert outer_group.width == 5
assert outer_group.height == 6.5
# This is as one would expect
assert outer_group.width == 2
assert outer_group.height == 3
# Adding a submobject with no points does not change the group size
outer_group.add(empty_mob)
assert outer_group.width == 2
assert outer_group.height == 3
def test_mobject_dimensions_has_points_and_children():