Fix polyline distortion when adding tips to Line paths

This commit is contained in:
Brighton 2026-03-21 03:50:24 +08:00
commit a41aba21ca
3 changed files with 128 additions and 0 deletions

View file

@ -26,6 +26,7 @@ from manim.mobject.mobject import Mobject
from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
from manim.mobject.opengl.opengl_mobject import OpenGLMobject
from manim.mobject.types.vectorized_mobject import DashedVMobject, VGroup, VMobject
from manim.utils.bezier import partial_bezier_points
from manim.utils.color import WHITE
from manim.utils.space_ops import angle_of_vector, line_intersection, normalize
@ -234,6 +235,49 @@ class Line(TipableVMobject):
self.generate_points()
return super().put_start_and_end_on(start, end)
def _trim_path_with_tip_base(
self,
point: Point3DLike,
at_start: bool,
) -> bool:
if self.path_arc != 0 or self.get_num_curves() <= 1:
return False
curve_index = 0 if at_start else self.get_num_curves() - 1
if curve_index < 0:
return False
curve_points = self.get_nth_curve_points(curve_index)
start_anchor = curve_points[0]
end_anchor = curve_points[-1]
segment = end_anchor - start_anchor
segment_length_sq = float(np.dot(segment, segment))
if segment_length_sq == 0:
return False
residue = float(
np.clip(
np.dot(np.asarray(point) - start_anchor, segment) / segment_length_sq,
0,
1,
)
)
nppc = self.n_points_per_curve
if at_start:
self.points[:nppc] = partial_bezier_points(curve_points, residue, 1)
else:
self.points[-nppc:] = partial_bezier_points(curve_points, 0, residue)
return True
def reset_endpoints_based_on_tip(self, tip: ArrowTip, at_start: bool) -> Self:
if self.get_length() == 0:
return self
if self._trim_path_with_tip_base(tip.base, at_start):
return self
return super().reset_endpoints_based_on_tip(tip, at_start)
def get_vector(self) -> Vector3D:
return self.get_end() - self.get_start()

View file

@ -21,6 +21,7 @@ from manim.typing import (
Vector3D,
Vector3DLike,
)
from manim.utils.bezier import partial_bezier_points
from manim.utils.color import *
from manim.utils.iterables import adjacent_n_tuples, adjacent_pairs
from manim.utils.simple_functions import clip
@ -553,6 +554,45 @@ class OpenGLLine(OpenGLTipableVMobject):
self.set_points_by_ends(start, end, self.path_arc)
return super().put_start_and_end_on(start, end)
def _trim_path_with_tip_base(self, point: Point3DLike, at_start: bool) -> bool:
if self.path_arc != 0 or self.get_num_curves() <= 1:
return False
curve_index = 0 if at_start else self.get_num_curves() - 1
if curve_index < 0:
return False
curve_points = self.get_nth_curve_points(curve_index)
start_anchor = curve_points[0]
end_anchor = curve_points[-1]
segment = end_anchor - start_anchor
segment_length_sq = float(np.dot(segment, segment))
if segment_length_sq == 0:
return False
residue = float(
np.clip(
np.dot(np.asarray(point) - start_anchor, segment) / segment_length_sq,
0,
1,
)
)
nppc = self.n_points_per_curve
if at_start:
self.points[:nppc] = partial_bezier_points(curve_points, residue, 1)
else:
self.points[-nppc:] = partial_bezier_points(curve_points, 0, residue)
return True
def reset_endpoints_based_on_tip(self, tip: OpenGLArrowTip, at_start: bool) -> Self:
if self.get_length() == 0:
return self
if self._trim_path_with_tip_base(tip.get_base(), at_start):
return self
return super().reset_endpoints_based_on_tip(tip, at_start)
def get_vector(self) -> Vector3D:
return self.get_end() - self.get_start()

View file

@ -217,6 +217,50 @@ def test_line_with_buff_and_path_arc():
np.testing.assert_allclose(line.points, expected_points)
def test_add_tip_preserves_polyline_corner():
line = Line()
line.set_points_as_corners(
[
np.array([0.0, 0.0, 0.0]),
np.array([0.0, 2.0, 0.0]),
np.array([3.0, 2.0, 0.0]),
]
)
original_first_curve = line.points[:4].copy()
line.add_tip(tip_length=0.5)
np.testing.assert_allclose(line.points[:4], original_first_curve)
np.testing.assert_allclose(line.points[3], np.array([0.0, 2.0, 0.0]))
np.testing.assert_allclose(line.points[4], np.array([0.0, 2.0, 0.0]))
np.testing.assert_allclose(line.points[-1], line.tip.base)
np.testing.assert_allclose(line.tip.base, np.array([2.5, 2.0, 0.0]))
def test_add_start_tip_preserves_polyline_corner():
line = Line()
line.set_points_as_corners(
[
np.array([0.0, 0.0, 0.0]),
np.array([0.0, 2.0, 0.0]),
np.array([3.0, 2.0, 0.0]),
]
)
original_last_curve = line.points[-4:].copy()
line.add_tip(at_start=True, tip_length=0.5)
np.testing.assert_allclose(line.points[-4:], original_last_curve)
np.testing.assert_allclose(line.points[3], np.array([0.0, 2.0, 0.0]))
np.testing.assert_allclose(line.points[4], np.array([0.0, 2.0, 0.0]))
np.testing.assert_allclose(line.points[0], line.start_tip.base, atol=1e-12)
np.testing.assert_allclose(
line.start_tip.base,
np.array([0.0, 0.5, 0.0]),
atol=1e-12,
)
def test_Circle_point_at_angle():
from manim import TAU