Skip to content

Commit a41aba2

Browse files
BrightonBrighton
authored andcommitted
Fix polyline distortion when adding tips to Line paths
1 parent 21cf999 commit a41aba2

3 files changed

Lines changed: 128 additions & 0 deletions

File tree

manim/mobject/geometry/line.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
2727
from manim.mobject.opengl.opengl_mobject import OpenGLMobject
2828
from manim.mobject.types.vectorized_mobject import DashedVMobject, VGroup, VMobject
29+
from manim.utils.bezier import partial_bezier_points
2930
from manim.utils.color import WHITE
3031
from manim.utils.space_ops import angle_of_vector, line_intersection, normalize
3132

@@ -234,6 +235,49 @@ def construct(self):
234235
self.generate_points()
235236
return super().put_start_and_end_on(start, end)
236237

238+
def _trim_path_with_tip_base(
239+
self,
240+
point: Point3DLike,
241+
at_start: bool,
242+
) -> bool:
243+
if self.path_arc != 0 or self.get_num_curves() <= 1:
244+
return False
245+
246+
curve_index = 0 if at_start else self.get_num_curves() - 1
247+
if curve_index < 0:
248+
return False
249+
250+
curve_points = self.get_nth_curve_points(curve_index)
251+
start_anchor = curve_points[0]
252+
end_anchor = curve_points[-1]
253+
segment = end_anchor - start_anchor
254+
segment_length_sq = float(np.dot(segment, segment))
255+
if segment_length_sq == 0:
256+
return False
257+
258+
residue = float(
259+
np.clip(
260+
np.dot(np.asarray(point) - start_anchor, segment) / segment_length_sq,
261+
0,
262+
1,
263+
)
264+
)
265+
nppc = self.n_points_per_curve
266+
if at_start:
267+
self.points[:nppc] = partial_bezier_points(curve_points, residue, 1)
268+
else:
269+
self.points[-nppc:] = partial_bezier_points(curve_points, 0, residue)
270+
return True
271+
272+
def reset_endpoints_based_on_tip(self, tip: ArrowTip, at_start: bool) -> Self:
273+
if self.get_length() == 0:
274+
return self
275+
276+
if self._trim_path_with_tip_base(tip.base, at_start):
277+
return self
278+
279+
return super().reset_endpoints_based_on_tip(tip, at_start)
280+
237281
def get_vector(self) -> Vector3D:
238282
return self.get_end() - self.get_start()
239283

manim/mobject/opengl/opengl_geometry.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
Vector3D,
2222
Vector3DLike,
2323
)
24+
from manim.utils.bezier import partial_bezier_points
2425
from manim.utils.color import *
2526
from manim.utils.iterables import adjacent_n_tuples, adjacent_pairs
2627
from manim.utils.simple_functions import clip
@@ -553,6 +554,45 @@ def put_start_and_end_on(self, start: Point3DLike, end: Point3DLike) -> Self:
553554
self.set_points_by_ends(start, end, self.path_arc)
554555
return super().put_start_and_end_on(start, end)
555556

557+
def _trim_path_with_tip_base(self, point: Point3DLike, at_start: bool) -> bool:
558+
if self.path_arc != 0 or self.get_num_curves() <= 1:
559+
return False
560+
561+
curve_index = 0 if at_start else self.get_num_curves() - 1
562+
if curve_index < 0:
563+
return False
564+
565+
curve_points = self.get_nth_curve_points(curve_index)
566+
start_anchor = curve_points[0]
567+
end_anchor = curve_points[-1]
568+
segment = end_anchor - start_anchor
569+
segment_length_sq = float(np.dot(segment, segment))
570+
if segment_length_sq == 0:
571+
return False
572+
573+
residue = float(
574+
np.clip(
575+
np.dot(np.asarray(point) - start_anchor, segment) / segment_length_sq,
576+
0,
577+
1,
578+
)
579+
)
580+
nppc = self.n_points_per_curve
581+
if at_start:
582+
self.points[:nppc] = partial_bezier_points(curve_points, residue, 1)
583+
else:
584+
self.points[-nppc:] = partial_bezier_points(curve_points, 0, residue)
585+
return True
586+
587+
def reset_endpoints_based_on_tip(self, tip: OpenGLArrowTip, at_start: bool) -> Self:
588+
if self.get_length() == 0:
589+
return self
590+
591+
if self._trim_path_with_tip_base(tip.get_base(), at_start):
592+
return self
593+
594+
return super().reset_endpoints_based_on_tip(tip, at_start)
595+
556596
def get_vector(self) -> Vector3D:
557597
return self.get_end() - self.get_start()
558598

tests/module/mobject/geometry/test_unit_geometry.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,50 @@ def test_line_with_buff_and_path_arc():
217217
np.testing.assert_allclose(line.points, expected_points)
218218

219219

220+
def test_add_tip_preserves_polyline_corner():
221+
line = Line()
222+
line.set_points_as_corners(
223+
[
224+
np.array([0.0, 0.0, 0.0]),
225+
np.array([0.0, 2.0, 0.0]),
226+
np.array([3.0, 2.0, 0.0]),
227+
]
228+
)
229+
original_first_curve = line.points[:4].copy()
230+
231+
line.add_tip(tip_length=0.5)
232+
233+
np.testing.assert_allclose(line.points[:4], original_first_curve)
234+
np.testing.assert_allclose(line.points[3], np.array([0.0, 2.0, 0.0]))
235+
np.testing.assert_allclose(line.points[4], np.array([0.0, 2.0, 0.0]))
236+
np.testing.assert_allclose(line.points[-1], line.tip.base)
237+
np.testing.assert_allclose(line.tip.base, np.array([2.5, 2.0, 0.0]))
238+
239+
240+
def test_add_start_tip_preserves_polyline_corner():
241+
line = Line()
242+
line.set_points_as_corners(
243+
[
244+
np.array([0.0, 0.0, 0.0]),
245+
np.array([0.0, 2.0, 0.0]),
246+
np.array([3.0, 2.0, 0.0]),
247+
]
248+
)
249+
original_last_curve = line.points[-4:].copy()
250+
251+
line.add_tip(at_start=True, tip_length=0.5)
252+
253+
np.testing.assert_allclose(line.points[-4:], original_last_curve)
254+
np.testing.assert_allclose(line.points[3], np.array([0.0, 2.0, 0.0]))
255+
np.testing.assert_allclose(line.points[4], np.array([0.0, 2.0, 0.0]))
256+
np.testing.assert_allclose(line.points[0], line.start_tip.base, atol=1e-12)
257+
np.testing.assert_allclose(
258+
line.start_tip.base,
259+
np.array([0.0, 0.5, 0.0]),
260+
atol=1e-12,
261+
)
262+
263+
220264
def test_Circle_point_at_angle():
221265
from manim import TAU
222266

0 commit comments

Comments
 (0)