Add regression tests for four recent fixes in vectorized_mobject.py (#4750)

* Add regression tests for four recent fixes in vectorized_mobject.py

Adds four regression tests for fixes that landed without tests in the same PR:

  * 21cf9998 (PR #4630, fixes #3569 + #4629) -- IndexError in
    `get_nth_subpath` when `path_list` is empty; ensure it always returns
    a NumPy array.

  * f6cdb547 (PR #4219) -- `add_points_as_corners` silently dropped a
    single new point when called on a VMobject whose last subpath was
    complete.

  * 3d029c12 (PR #4320, fixes #4255) -- `pointwise_become_partial` cleared
    the target's points when the source had no cubic curves, surfacing as
    `Arrow3D.get_start()` / `get_end()` returning the origin after a
    `Create` animation.

  * 429f25328 (PR #4694) -- `scale(scale_stroke=True)` on a compound
    VMobject propagated the parent's scaled stroke width to every
    submobject, overwriting submobjects with their own (e.g. zero) stroke.

Each test reproduces the original failing condition at the unit level
and asserts the post-fix behavior. Validation: every test was confirmed
to fail when the corresponding fix is reverted on the source file, and
pass when the fix is restored.

4 tests, ~0.2s runtime. Tests go in the existing files
`tests/module/mobject/types/vectorized_mobject/test_vectorized_mobject.py`
(three tests, adjacent to their topically-related siblings) and
`tests/module/mobject/types/vectorized_mobject/test_stroke.py` (one test,
next to the existing `test_stroke_scale`).

Co-authored-by: LetMarq <LetMarq@users.noreply.github.com>

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

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

---------

Co-authored-by: THE-RAF <THE-RAF@users.noreply.github.com>
Co-authored-by: LetMarq <LetMarq@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Francisco Manríquez Novoa <49853152+chopan050@users.noreply.github.com>
This commit is contained in:
Rafael Brusiquesi Martins 2026-06-02 18:53:49 -03:00 committed by GitHub
commit 56f7eb2a1f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 110 additions and 0 deletions

View file

@ -61,3 +61,33 @@ def test_background_stroke_scale():
b.scale(0.5, scale_stroke=True)
assert a.get_stroke_width(background=True) == 50
assert b.get_stroke_width(background=True) == 25
def test_stroke_scale_preserves_relative_widths_in_compound_mobjects():
"""Regression test for fix 429f25328 (PR #4694).
When ``scale(..., scale_stroke=True)`` is called on a compound VMobject
whose submobjects have different stroke widths, the buggy version called
``self.set_stroke(width=abs(scale_factor) * self.get_stroke_width())``,
which uses the *parent's* stroke width and then propagates that single
scaled value to the whole family overwriting each submobject's own
width. In particular, a submobject with zero stroke would gain non-zero
stroke after scaling.
The fix iterates over ``self.get_family()`` and scales each submobject's
stroke individually with ``family=False`` so the relative widths are
preserved.
"""
from manim import VGroup
inner_with_stroke = VMobject()
inner_with_stroke.set_stroke(width=4)
inner_zero_stroke = VMobject()
inner_zero_stroke.set_stroke(width=0)
compound = VGroup(inner_with_stroke, inner_zero_stroke)
compound.scale(0.5, scale_stroke=True)
# Post-fix: each submob's width is scaled by 0.5 of its OWN value.
assert inner_with_stroke.get_stroke_width() == 2
assert inner_zero_stroke.get_stroke_width() == 0

View file

@ -96,6 +96,29 @@ def test_vmobject_add_points_as_corners():
np.testing.assert_allclose(obj1.points, obj3.points)
def test_add_points_as_corners_single_point_connects_to_existing_path():
"""Regression test for #4218 / fix f6cdb547 (PR #4219).
When ``add_points_as_corners`` is called with a single new point on a
VMobject whose last subpath is complete (so ``has_new_path_started()``
returns False), the buggy version silently dropped the new point the
``else`` branch computed ``start_corners = points[:-1]`` which is empty
for a one-point input. The fix unifies the two branches so the existing
path's last point is always used as the start corner.
"""
v = VMobject()
v.start_new_path(np.array([0.0, 0.0, 0.0]))
v.add_line_to(np.array([1.0, 0.0, 0.0]))
assert not v.has_new_path_started()
n_before = len(v.points)
v.add_points_as_corners([[2.0, 0.0, 0.0]])
# Post-fix: a cubic from [1, 0, 0] to [2, 0, 0] is appended.
assert len(v.points) > n_before
np.testing.assert_array_equal(v.points[-1], [2.0, 0.0, 0.0])
def test_vmobject_point_from_proportion():
obj = VMobject()
@ -528,6 +551,63 @@ def test_proportion_from_point():
np.testing.assert_allclose(props, [0, 1 / 3, 2 / 3])
def test_align_points_handles_vmobject_with_no_complete_cubic_curves():
"""Regression test for #3569 / #4629 (fix 21cf9998 / PR #4630).
When ``align_points`` encounters a VMobject whose points array is
non-empty but holds fewer than ``n_points_per_cubic_curve`` points,
``get_subpaths()`` returns ``[]`` while ``has_no_points()`` returns
``False`` so the pre-loop sanitization that would normally add a
null curve is skipped. The buggy ``get_nth_subpath`` closure then
indexed ``path_list[-1]`` on the empty list and raised
``IndexError: list index out of range``.
The fix returns a zero-valued null path in that case and ensures the
closure always returns a NumPy array (the previous list return type
broke downstream ``reshape`` calls).
"""
target = VMobject()
target.set_points(
np.array([[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [2.0, 0.0, 0.0], [3.0, 0.0, 0.0]])
)
sub_cubic = VMobject()
sub_cubic.set_points(np.array([[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [2.0, 0.0, 0.0]]))
assert sub_cubic.get_subpaths() == []
assert not sub_cubic.has_no_points()
# Pre-fix: raises IndexError. Post-fix: completes; points are ndarray.
target.align_points(sub_cubic)
assert isinstance(target.points, np.ndarray)
assert isinstance(sub_cubic.points, np.ndarray)
def test_pointwise_become_partial_preserves_target_when_source_has_no_curves():
"""Regression test for #4255 / fix 3d029c12 (PR #4320).
When ``pointwise_become_partial`` is called with a source ``VMobject`` that
has zero cubic curves (e.g. an empty ``VMobject`` or a ``VectorizedPoint``
holding a single point), the buggy version called ``self.clear_points()``
on the *target*, zeroing out its data. The fix removes that call.
This bug surfaced as ``Arrow3D.get_start()`` / ``get_end()`` returning
``[0, 0, 0]`` after a ``Create`` animation, because the arrow's
``end_point`` sub-mobject has 1 point but no cubic curves.
"""
target = VMobject()
original_points = np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]])
target.set_points(original_points)
empty_source = VMobject()
assert empty_source.get_num_curves() == 0
# Choose a, b so the `(a <= 0 and b >= 1)` early-return is skipped
# and the `num_curves == 0` branch is exercised.
target.pointwise_become_partial(empty_source, 0.0, 0.5)
np.testing.assert_array_equal(target.points, original_points)
def test_pointwise_become_partial_where_vmobject_is_self():
sq = Square()
sq.pointwise_become_partial(vmobject=sq, a=0.2, b=0.7)