From 52a326e26a37a2774be01ffcf1ac30a2b6af31f2 Mon Sep 17 00:00:00 2001 From: Deividas Morkunas Date: Sun, 9 Nov 2025 01:09:04 +0000 Subject: [PATCH 1/3] 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 --- tests/module/mobject/mobject/test_mobject.py | 77 +++++++++++++++++++- tests/opengl/test_opengl_mobject.py | 26 +++++++ 2 files changed, 102 insertions(+), 1 deletion(-) diff --git a/tests/module/mobject/mobject/test_mobject.py b/tests/module/mobject/mobject/test_mobject.py index 89deea93c9..58fe6e211a 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, UR, PI, 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) From de40f2e079c0be840c6124600a5947707945ed82 Mon Sep 17 00:00:00 2001 From: Deividas Morkunas Date: Sun, 9 Nov 2025 01:09:39 +0000 Subject: [PATCH 2/3] 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 --- manim/mobject/mobject.py | 2 ++ 1 file changed, 2 insertions(+) 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) From 7fda70241317b0e0dfdd2698690882f5f8c23d9e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 15 Nov 2025 18:15:23 +0000 Subject: [PATCH 3/3] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/module/mobject/mobject/test_mobject.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/module/mobject/mobject/test_mobject.py b/tests/module/mobject/mobject/test_mobject.py index 58fe6e211a..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, PI, Circle, Mobject, Rectangle, Square, Triangle, VGroup +from manim import DL, PI, UR, Circle, Mobject, Rectangle, Square, Triangle, VGroup def test_mobject_add():