diff --git a/manim/mobject/mobject.py b/manim/mobject/mobject.py index ec1e83079c..1b9a342a34 100644 --- a/manim/mobject/mobject.py +++ b/manim/mobject/mobject.py @@ -1474,6 +1474,8 @@ def apply_points_function_about_point( if about_edge is None: about_edge = ORIGIN about_point = self.get_critical_point(about_edge) + # Make a copy to prevent mutation of the original array if about_point is a view + about_point = np.array(about_point, copy=True) for mob in self.family_members_with_points(): mob.points -= about_point mob.points = func(mob.points) diff --git a/tests/module/mobject/mobject/test_mobject.py b/tests/module/mobject/mobject/test_mobject.py index 89deea93c9..203d312627 100644 --- a/tests/module/mobject/mobject/test_mobject.py +++ b/tests/module/mobject/mobject/test_mobject.py @@ -3,7 +3,7 @@ import numpy as np import pytest -from manim import DL, UR, Circle, Mobject, Rectangle, Square, VGroup +from manim import DL, PI, UR, Circle, Mobject, Rectangle, Square, Triangle, VGroup def test_mobject_add(): @@ -168,3 +168,78 @@ def test_mobject_dimensions_has_points_and_children(): assert inner_rect.width == 2 assert inner_rect.height == 1 assert inner_rect.depth == 0 + + +def test_rotate_about_vertex_view(): + """Test that rotating about a vertex obtained from get_vertices() works correctly. + + This is a regression test for an issue where get_vertices() returns a view of the points array, + and using it as about_point in rotate() would cause the view to be mutated. + """ + triangle = Triangle() + original_vertices = triangle.get_vertices().copy() + first_vertex = original_vertices[0].copy() + + # This should rotate about the first vertex without corrupting it + triangle.rotate(PI / 2, about_point=triangle.get_vertices()[0]) + + # The first vertex should remain in the same position (within numerical precision) + rotated_vertices = triangle.get_vertices() + np.testing.assert_allclose(rotated_vertices[0], first_vertex, atol=1e-6) + + +def test_scale_about_vertex_view(): + """Test that scaling about a vertex obtained from get_vertices() works correctly. + + This is a regression test for an issue where get_vertices() returns a view of the points array, + and using it as about_point in scale() would cause the view to be mutated. + """ + triangle = Triangle() + original_vertices = triangle.get_vertices().copy() + first_vertex = original_vertices[0].copy() + + # This should scale about the first vertex without corrupting it + triangle.scale(2, about_point=triangle.get_vertices()[0]) + + # The first vertex should remain in the same position (within numerical precision) + scaled_vertices = triangle.get_vertices() + np.testing.assert_allclose(scaled_vertices[0], first_vertex, atol=1e-6) + + +def test_stretch_about_vertex_view(): + """Test that stretching about a vertex obtained from get_vertices() works correctly. + + This is a regression test for an issue where get_vertices() returns a view of the points array, + and using it as about_point in stretch() would cause the view to be mutated. + """ + triangle = Triangle() + original_vertices = triangle.get_vertices().copy() + first_vertex = original_vertices[0].copy() + + # This should stretch about the first vertex without corrupting it + triangle.stretch(2, 0, about_point=triangle.get_vertices()[0]) + + # The first vertex should remain in the same position (within numerical precision) + stretched_vertices = triangle.get_vertices() + np.testing.assert_allclose(stretched_vertices[0], first_vertex, atol=1e-6) + + +def test_apply_matrix_about_vertex_view(): + """Test that apply_matrix about a vertex obtained from get_vertices() works correctly. + + This is a regression test for an issue where get_vertices() returns a view of the points array, + and using it as about_point in apply_matrix() would cause the view to be mutated. + """ + triangle = Triangle() + original_vertices = triangle.get_vertices().copy() + first_vertex = original_vertices[0].copy() + + # Define a rotation matrix (90 degrees rotation around z-axis) + rotation_matrix = np.array([[0, -1, 0], [1, 0, 0], [0, 0, 1]]) + + # This should apply the matrix about the first vertex without corrupting it + triangle.apply_matrix(rotation_matrix, about_point=triangle.get_vertices()[0]) + + # The first vertex should remain in the same position (within numerical precision) + transformed_vertices = triangle.get_vertices() + np.testing.assert_allclose(transformed_vertices[0], first_vertex, atol=1e-6) diff --git a/tests/opengl/test_opengl_mobject.py b/tests/opengl/test_opengl_mobject.py index d5bfac97f9..faea6ddd5c 100644 --- a/tests/opengl/test_opengl_mobject.py +++ b/tests/opengl/test_opengl_mobject.py @@ -1,7 +1,10 @@ from __future__ import annotations +import numpy as np import pytest +from manim import PI +from manim.mobject.opengl.opengl_geometry import OpenGLTriangle from manim.mobject.opengl.opengl_mobject import OpenGLMobject @@ -60,3 +63,26 @@ def test_opengl_mobject_remove(using_opengl_renderer): assert len(obj.submobjects) == 10 assert obj.remove(OpenGLMobject()) is obj + + +def test_opengl_rotate_about_vertex_view(using_opengl_renderer): + """Test that rotating about a vertex obtained from get_vertices() works correctly. + + This is a regression test for an issue in the non-OpenGL (Cairo) renderer where + get_vertices() returns a view of the points array, and using it as about_point + in rotate() would cause the view to be mutated. The OpenGL renderer was not affected + by this bug due to its different implementation (using `arr - about_point` which + creates a temporary array rather than `arr -= about_point` which mutates in-place). + + This test verifies that the OpenGL renderer continues to handle vertex views correctly. + """ + triangle = OpenGLTriangle() + original_vertices = triangle.get_vertices().copy() + first_vertex = original_vertices[0].copy() + + # This should rotate about the first vertex without corrupting it + triangle.rotate(PI / 2, about_point=triangle.get_vertices()[0]) + + # The first vertex should remain in the same position (within numerical precision) + rotated_vertices = triangle.get_vertices() + np.testing.assert_allclose(rotated_vertices[0], first_vertex, atol=1e-6)