Skip to content

Commit 7e950b3

Browse files
Prevent mutation of about_point in apply_points_function_about_point - Fixes #4445 (#4478)
* Add regression tests for about_point view mutation issue This adds regression tests for issue #4445 where using get_vertices()[0] as about_point in transformation methods would cause incorrect results due to numpy array view mutation. Tests added: - test_rotate_about_vertex_view - test_scale_about_vertex_view - test_stretch_about_vertex_view - test_apply_matrix_about_vertex_view - test_opengl_rotate_about_vertex_view (OpenGL was not affected by the bug) Related to #4445 * Fix about_point view mutation in apply_points_function_about_point When about_point parameter receives a numpy array view (e.g., from get_vertices()[0]), the in-place operation `mob.points -= about_point` would mutate the view, corrupting the transformation calculation. This fix copies about_point before using it to prevent view mutation. The OpenGL renderer was not affected by this bug because it uses `arr - about_point` (creates temporary) instead of `arr -= about_point` (mutates in-place). Fixes #4445 * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 9528337 commit 7e950b3

File tree

3 files changed

+104
-1
lines changed

3 files changed

+104
-1
lines changed

manim/mobject/mobject.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1474,6 +1474,8 @@ def apply_points_function_about_point(
14741474
if about_edge is None:
14751475
about_edge = ORIGIN
14761476
about_point = self.get_critical_point(about_edge)
1477+
# Make a copy to prevent mutation of the original array if about_point is a view
1478+
about_point = np.array(about_point, copy=True)
14771479
for mob in self.family_members_with_points():
14781480
mob.points -= about_point
14791481
mob.points = func(mob.points)

tests/module/mobject/mobject/test_mobject.py

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import numpy as np
44
import pytest
55

6-
from manim import DL, UR, Circle, Mobject, Rectangle, Square, VGroup
6+
from manim import DL, PI, UR, Circle, Mobject, Rectangle, Square, Triangle, VGroup
77

88

99
def test_mobject_add():
@@ -168,3 +168,78 @@ def test_mobject_dimensions_has_points_and_children():
168168
assert inner_rect.width == 2
169169
assert inner_rect.height == 1
170170
assert inner_rect.depth == 0
171+
172+
173+
def test_rotate_about_vertex_view():
174+
"""Test that rotating about a vertex obtained from get_vertices() works correctly.
175+
176+
This is a regression test for an issue where get_vertices() returns a view of the points array,
177+
and using it as about_point in rotate() would cause the view to be mutated.
178+
"""
179+
triangle = Triangle()
180+
original_vertices = triangle.get_vertices().copy()
181+
first_vertex = original_vertices[0].copy()
182+
183+
# This should rotate about the first vertex without corrupting it
184+
triangle.rotate(PI / 2, about_point=triangle.get_vertices()[0])
185+
186+
# The first vertex should remain in the same position (within numerical precision)
187+
rotated_vertices = triangle.get_vertices()
188+
np.testing.assert_allclose(rotated_vertices[0], first_vertex, atol=1e-6)
189+
190+
191+
def test_scale_about_vertex_view():
192+
"""Test that scaling about a vertex obtained from get_vertices() works correctly.
193+
194+
This is a regression test for an issue where get_vertices() returns a view of the points array,
195+
and using it as about_point in scale() would cause the view to be mutated.
196+
"""
197+
triangle = Triangle()
198+
original_vertices = triangle.get_vertices().copy()
199+
first_vertex = original_vertices[0].copy()
200+
201+
# This should scale about the first vertex without corrupting it
202+
triangle.scale(2, about_point=triangle.get_vertices()[0])
203+
204+
# The first vertex should remain in the same position (within numerical precision)
205+
scaled_vertices = triangle.get_vertices()
206+
np.testing.assert_allclose(scaled_vertices[0], first_vertex, atol=1e-6)
207+
208+
209+
def test_stretch_about_vertex_view():
210+
"""Test that stretching about a vertex obtained from get_vertices() works correctly.
211+
212+
This is a regression test for an issue where get_vertices() returns a view of the points array,
213+
and using it as about_point in stretch() would cause the view to be mutated.
214+
"""
215+
triangle = Triangle()
216+
original_vertices = triangle.get_vertices().copy()
217+
first_vertex = original_vertices[0].copy()
218+
219+
# This should stretch about the first vertex without corrupting it
220+
triangle.stretch(2, 0, about_point=triangle.get_vertices()[0])
221+
222+
# The first vertex should remain in the same position (within numerical precision)
223+
stretched_vertices = triangle.get_vertices()
224+
np.testing.assert_allclose(stretched_vertices[0], first_vertex, atol=1e-6)
225+
226+
227+
def test_apply_matrix_about_vertex_view():
228+
"""Test that apply_matrix about a vertex obtained from get_vertices() works correctly.
229+
230+
This is a regression test for an issue where get_vertices() returns a view of the points array,
231+
and using it as about_point in apply_matrix() would cause the view to be mutated.
232+
"""
233+
triangle = Triangle()
234+
original_vertices = triangle.get_vertices().copy()
235+
first_vertex = original_vertices[0].copy()
236+
237+
# Define a rotation matrix (90 degrees rotation around z-axis)
238+
rotation_matrix = np.array([[0, -1, 0], [1, 0, 0], [0, 0, 1]])
239+
240+
# This should apply the matrix about the first vertex without corrupting it
241+
triangle.apply_matrix(rotation_matrix, about_point=triangle.get_vertices()[0])
242+
243+
# The first vertex should remain in the same position (within numerical precision)
244+
transformed_vertices = triangle.get_vertices()
245+
np.testing.assert_allclose(transformed_vertices[0], first_vertex, atol=1e-6)

tests/opengl/test_opengl_mobject.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
from __future__ import annotations
22

3+
import numpy as np
34
import pytest
45

6+
from manim import PI
7+
from manim.mobject.opengl.opengl_geometry import OpenGLTriangle
58
from manim.mobject.opengl.opengl_mobject import OpenGLMobject
69

710

@@ -60,3 +63,26 @@ def test_opengl_mobject_remove(using_opengl_renderer):
6063
assert len(obj.submobjects) == 10
6164

6265
assert obj.remove(OpenGLMobject()) is obj
66+
67+
68+
def test_opengl_rotate_about_vertex_view(using_opengl_renderer):
69+
"""Test that rotating about a vertex obtained from get_vertices() works correctly.
70+
71+
This is a regression test for an issue in the non-OpenGL (Cairo) renderer where
72+
get_vertices() returns a view of the points array, and using it as about_point
73+
in rotate() would cause the view to be mutated. The OpenGL renderer was not affected
74+
by this bug due to its different implementation (using `arr - about_point` which
75+
creates a temporary array rather than `arr -= about_point` which mutates in-place).
76+
77+
This test verifies that the OpenGL renderer continues to handle vertex views correctly.
78+
"""
79+
triangle = OpenGLTriangle()
80+
original_vertices = triangle.get_vertices().copy()
81+
first_vertex = original_vertices[0].copy()
82+
83+
# This should rotate about the first vertex without corrupting it
84+
triangle.rotate(PI / 2, about_point=triangle.get_vertices()[0])
85+
86+
# The first vertex should remain in the same position (within numerical precision)
87+
rotated_vertices = triangle.get_vertices()
88+
np.testing.assert_allclose(rotated_vertices[0], first_vertex, atol=1e-6)

0 commit comments

Comments
 (0)