Refactor`Mobject.put_start_and_end_on() to shift Mobject to start when it's a closed curve (#4658)

* fix: preserve geometry in put_start_and_end_on for zero-vector mobjects

* fix: preserve geometry in put_start_and_end_on for zero-vector mobjects

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* fixed asser_array_equal line

* removed commets in mobject.py and opengl_mobject.py as suggested

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* added stacklevel=2 suggestion

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Francisco Manríquez Novoa <49853152+chopan050@users.noreply.github.com>
This commit is contained in:
GoThrones 2026-04-15 01:27:06 +05:30 committed by GitHub
commit c45724989d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 41 additions and 28 deletions

View file

@ -1941,30 +1941,33 @@ class Mobject:
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):
# 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)
current_start, current_end = self.get_start_and_end()
current_vector = current_end - current_start
if np.all(current_vector == 0):
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.",
stacklevel=2,
)
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

View file

@ -6,6 +6,7 @@ import itertools as it
import random
import sys
import types
import warnings
from collections.abc import Callable, Iterable, Iterator, Sequence
from functools import partialmethod, wraps
from math import ceil
@ -2134,26 +2135,33 @@ class OpenGLMobject:
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):
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.",
stacklevel=2,
)
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

View file

@ -129,4 +129,6 @@ def test_start_and_end_at_same_point():
line = DashedLine(np.zeros(3), np.zeros(3))
line.put_start_and_end_on(np.zeros(3), np.array([0, 0, 0]))
np.testing.assert_array_equal(np.round(np.zeros(3), 4), np.round(line.points, 4))
np.testing.assert_array_equal(
np.round(line.points, 4), np.round(np.zeros((4, 3)), 4)
)