From e6a31aad843abf11b1da864bab2fd6f38fdf4ce0 Mon Sep 17 00:00:00 2001 From: GoThrones Date: Thu, 26 Mar 2026 16:11:13 +0530 Subject: [PATCH 1/3] fix: preserve geometry in put_start_and_end_on for zero-vector mobjects --- manim/mobject/mobject.py | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/manim/mobject/mobject.py b/manim/mobject/mobject.py index 9f3818c188..20a3679823 100644 --- a/manim/mobject/mobject.py +++ b/manim/mobject/mobject.py @@ -1941,30 +1941,35 @@ def surround( return self def put_start_and_end_on(self, start: Point3DLike, end: Point3DLike) -> Self: - curr_start, curr_end = self.get_start_and_end() - curr_vect = curr_end - curr_start - if np.all(curr_vect == 0): + current_start, current_end = self.get_start_and_end() + current_vector = current_end - current_start + if np.all(current_vector == 0): # TODO: this looks broken. It makes self.points a Point3D instead # of a Point3D_Array. However, modifying this breaks some tests # where this is currently expected. - self.points = np.array(start) + #self.points = np.array(start) + + # Previously broken: self.points = np.array(start) would collapse + # all points to a single Point3D instead of shifting the mobject. + # Fixed by using shift instead. + self.shift(np.asarray(start) - current_start) return self - target_vect = np.asarray(end) - np.asarray(start) + target_vector = np.asarray(end) - np.asarray(start) axis = ( - normalize(np.cross(curr_vect, target_vect)) - if np.linalg.norm(np.cross(curr_vect, target_vect)) != 0 + normalize(np.cross(current_vector, target_vector)) + if np.linalg.norm(np.cross(current_vector, target_vector)) != 0 else OUT ) self.scale( - np.linalg.norm(target_vect) / np.linalg.norm(curr_vect), - about_point=curr_start, + np.linalg.norm(target_vector) / np.linalg.norm(current_vector), + about_point=current_start, ) self.rotate( - angle_between_vectors(curr_vect, target_vect), - about_point=curr_start, + angle_between_vectors(current_vector, target_vector), + about_point=current_start, axis=axis, ) - self.shift(start - curr_start) + self.shift(np.asarray(start) - current_start) return self # Background rectangle From a9dd8fb5409776527cf969f1e97eedc5e5c39387 Mon Sep 17 00:00:00 2001 From: GoThrones Date: Thu, 26 Mar 2026 16:27:31 +0530 Subject: [PATCH 2/3] fix: preserve geometry in put_start_and_end_on for zero-vector mobjects --- manim/mobject/mobject.py | 12 ++++----- manim/mobject/opengl/opengl_mobject.py | 34 +++++++++++++++++--------- 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/manim/mobject/mobject.py b/manim/mobject/mobject.py index 20a3679823..8b27fc16f7 100644 --- a/manim/mobject/mobject.py +++ b/manim/mobject/mobject.py @@ -1944,16 +1944,16 @@ def put_start_and_end_on(self, start: Point3DLike, end: Point3DLike) -> Self: current_start, current_end = self.get_start_and_end() current_vector = current_end - current_start if np.all(current_vector == 0): - # TODO: this looks broken. It makes self.points a Point3D instead - # of a Point3D_Array. However, modifying this breaks some tests - # where this is currently expected. - #self.points = np.array(start) - - # Previously broken: self.points = np.array(start) would collapse + # Previously self.points = np.array(start) was used here, but it would have collapsed # all points to a single Point3D instead of shifting the mobject. # Fixed by using shift instead. + warnings.warn( + "put_start_and_end_on has been called on a closed loop or zero-length mobject. " + f"{type(self).__name__} will be shifted to start point instead." + ) self.shift(np.asarray(start) - current_start) return self + target_vector = np.asarray(end) - np.asarray(start) axis = ( normalize(np.cross(current_vector, target_vector)) diff --git a/manim/mobject/opengl/opengl_mobject.py b/manim/mobject/opengl/opengl_mobject.py index 52cb3f3ac2..777f3df7e0 100644 --- a/manim/mobject/opengl/opengl_mobject.py +++ b/manim/mobject/opengl/opengl_mobject.py @@ -2134,28 +2134,38 @@ def surround( return self def put_start_and_end_on(self, start: Point3DLike, end: Point3DLike) -> Self: - curr_start, curr_end = self.get_start_and_end() - curr_vect = curr_end - curr_start - if np.all(curr_vect == 0): - raise Exception("Cannot position endpoints of closed loop") - target_vect = np.array(end) - np.array(start) + current_start, current_end = self.get_start_and_end() + current_vector = current_end - current_start + if np.all(current_vector == 0): + # Previously self.points = np.array(start) was used here, but it would have collapsed + # all points to a single Point3D instead of shifting the mobject. + # Fixed by using shift instead. + warnings.warn( + "put_start_and_end_on has been called on a closed loop or zero-length mobject. " + f"{type(self).__name__} will be shifted to start point instead." + ) + self.shift(np.asarray(start) - current_start) + return self + + target_vector = np.asarray(end) - np.asarray(start) axis = ( - normalize(np.cross(curr_vect, target_vect)) - if np.linalg.norm(np.cross(curr_vect, target_vect)) != 0 + normalize(np.cross(current_vector, target_vector)) + if np.linalg.norm(np.cross(current_vector, target_vector)) != 0 else OUT ) self.scale( - float(np.linalg.norm(target_vect) / np.linalg.norm(curr_vect)), - about_point=curr_start, + np.linalg.norm(target_vector) / np.linalg.norm(current_vector), + about_point=current_start, ) self.rotate( - angle_between_vectors(curr_vect, target_vect), - about_point=curr_start, + angle_between_vectors(current_vector, target_vector), + about_point=current_start, axis=axis, ) - self.shift(start - curr_start) + self.shift(np.asarray(start) - current_start) return self + # Color functions def set_rgba_array( From 41e5da2deec6120151173c2ccefbee91a9e1174b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 26 Mar 2026 11:08:48 +0000 Subject: [PATCH 3/3] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- manim/mobject/mobject.py | 2 +- manim/mobject/opengl/opengl_mobject.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/manim/mobject/mobject.py b/manim/mobject/mobject.py index 8b27fc16f7..b3ad97432a 100644 --- a/manim/mobject/mobject.py +++ b/manim/mobject/mobject.py @@ -1953,7 +1953,7 @@ def put_start_and_end_on(self, start: Point3DLike, end: Point3DLike) -> Self: ) self.shift(np.asarray(start) - current_start) return self - + target_vector = np.asarray(end) - np.asarray(start) axis = ( normalize(np.cross(current_vector, target_vector)) diff --git a/manim/mobject/opengl/opengl_mobject.py b/manim/mobject/opengl/opengl_mobject.py index 777f3df7e0..8575df0192 100644 --- a/manim/mobject/opengl/opengl_mobject.py +++ b/manim/mobject/opengl/opengl_mobject.py @@ -2146,7 +2146,7 @@ def put_start_and_end_on(self, start: Point3DLike, end: Point3DLike) -> Self: ) self.shift(np.asarray(start) - current_start) return self - + target_vector = np.asarray(end) - np.asarray(start) axis = ( normalize(np.cross(current_vector, target_vector)) @@ -2165,7 +2165,6 @@ def put_start_and_end_on(self, start: Point3DLike, end: Point3DLike) -> Self: self.shift(np.asarray(start) - current_start) return self - # Color functions def set_rgba_array(