diff --git a/manim/mobject/types/vectorized_mobject.py b/manim/mobject/types/vectorized_mobject.py index d529be779b..b5a51811a4 100644 --- a/manim/mobject/types/vectorized_mobject.py +++ b/manim/mobject/types/vectorized_mobject.py @@ -2832,6 +2832,21 @@ def __init__( self.dashed_ratio = dashed_ratio self.num_dashes = num_dashes super().__init__(color=color, **kwargs) + + # Work on a copy to avoid mutating the caller's mobject (e.g. removing tips). + base_vmobject = vmobject + vmobject = base_vmobject.copy() + + # TipableVMobject instances (Arrow, Vector, etc.) carry tips as submobjects. + # When dashing such objects, each subcurve would otherwise include its own + # tip, leading to many overlapping arrowheads. Pop tips from the working + # copy and re-attach them only once after the dashes are created. + tips = None + if hasattr(vmobject, "pop_tips"): + popped_tips = vmobject.pop_tips() + if len(popped_tips.submobjects) > 0: + tips = popped_tips + r = self.dashed_ratio n = self.num_dashes if n > 0: @@ -2917,6 +2932,9 @@ def __init__( # Family is already taken care of by get_subcurve # implementation if config.renderer == RendererType.OPENGL: - self.match_style(vmobject, recurse=False) + self.match_style(base_vmobject, recurse=False) else: - self.match_style(vmobject, family=False) + self.match_style(base_vmobject, family=False) + + if tips is not None: + self.add(*tips.submobjects) diff --git a/tests/module/mobject/types/vectorized_mobject/test_dashed_vmobject.py b/tests/module/mobject/types/vectorized_mobject/test_dashed_vmobject.py new file mode 100644 index 0000000000..809c9c47f7 --- /dev/null +++ b/tests/module/mobject/types/vectorized_mobject/test_dashed_vmobject.py @@ -0,0 +1,43 @@ +from manim import ORIGIN, UR, Arrow, DashedVMobject, VGroup +from manim.mobject.geometry.tips import ArrowTip, StealthTip + + +def _collect_tips(mobject): + return [mob for mob in mobject.get_family() if isinstance(mob, ArrowTip)] + + +def test_dashed_arrow_has_single_tip(): + dashed = DashedVMobject(Arrow(ORIGIN, 2 * UR)) + tips = _collect_tips(dashed) + + assert len(tips) == 1 + + +def test_dashed_arrow_tip_not_duplicated_in_group_opacity(): + base_arrow = Arrow(ORIGIN, 2 * UR) + faded_arrow = base_arrow.copy().set_fill(opacity=0.4).set_stroke(opacity=0.4) + + dashed_group = ( + VGroup(DashedVMobject(faded_arrow)) + .set_fill(opacity=0.4, family=True) + .set_stroke(opacity=0.4, family=True) + ) + + tips = _collect_tips(dashed_group) + + assert len(tips) == 1 + + +def test_dashed_arrow_custom_tip_shape_has_single_tip(): + dashed = DashedVMobject(Arrow(ORIGIN, 2 * UR, tip_shape=StealthTip)) + tips = _collect_tips(dashed) + + assert len(tips) == 1 + assert isinstance(tips[0], StealthTip) + + +def test_dashed_arrow_with_start_tip_has_two_tips(): + dashed = DashedVMobject(Arrow(ORIGIN, 2 * UR).add_tip(at_start=True)) + tips = _collect_tips(dashed) + + assert len(tips) == 2