Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions tests/module/mobject/types/vectorized_mobject/test_stroke.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -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)
Expand Down
Loading