Fix all tests in test_vectorized_mobject.py and allow iterables in OpenGLVGroup

This commit is contained in:
Francisco Manríquez Novoa 2026-02-13 02:47:14 -03:00
commit c76c868213
3 changed files with 504 additions and 205 deletions

View file

@ -912,7 +912,7 @@ class OpenGLMobject:
error_message = (
f"Only values of type {mob_class.__name__} can be added "
f"as submobjects of {type(self).__name__}, but the value "
f"{submob} (at index {i}) is of type "
f"{repr(submob)} (at index {i}) is of type "
f"{type(submob).__name__}."
)
# Intended for subclasses such as VMobject, which

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,5 @@
from math import cos, sin
from typing import TYPE_CHECKING, Any, TypeVar
import numpy as np
import pytest
@ -21,15 +22,37 @@ from manim.mobject.opengl.opengl_vectorized_mobject import (
OpenGLVMobject as VMobject,
)
if TYPE_CHECKING:
from collections.abc import Iterable
T1 = TypeVar("T1", Any)
T2 = TypeVar("T2", Any)
def get_add_type_error_message(
parent_mob: OpenGLVMobject,
invalid_obj: Any,
index: int,
subindex: int | None = None,
suggest_using_group: bool = False,
) -> str:
# TODO: when OpenGLVMobject is renamed to VMobject, also modify this message.
if subindex is None:
clarification = f"at index {index}"
else:
clarification = f"at subindex {subindex} of iterable at index {index}"
message = (
f"Only values of type OpenGLVMobject can be added as submobjects of "
f"{type(parent_mob).__name__}, but the value {repr(invalid_obj)} "
f"({clarification}) is of type {type(invalid_obj).__name__}."
)
if suggest_using_group:
message += " You can try adding this value into a Group instead."
return message
def test_vmobject_add():
def get_type_error_message(invalid_obj, invalid_indices):
return (
f"Only values of type VMobject can be added as submobjects of VMobject, but "
f"the value {repr(invalid_obj)} (at index {invalid_indices[1]}) is of type "
f"{type(invalid_obj).__name__}."
)
"""Test the VMobject add method."""
obj = VMobject()
assert len(obj.submobjects) == 0
@ -40,21 +63,25 @@ def test_vmobject_add():
# Can't add non-VMobject values to a VMobject.
with pytest.raises(TypeError) as add_int_info:
obj.add(3)
assert str(add_int_info.value) == (get_type_error_message(3, [0, 0]))
assert str(add_int_info.value) == get_add_type_error_message(
parent_mob=obj, invalid_obj=3, index=0
)
assert len(obj.submobjects) == 1
# Plain Mobjects can't be added to a VMobject if they're not
# VMobjects. Suggest adding them into a Group instead.
with pytest.raises(TypeError) as add_mob_info:
obj.add(Mobject())
assert str(add_mob_info.value) == (get_type_error_message(Mobject(), [0, 0]))
assert str(add_mob_info.value) == get_add_type_error_message(
parent_mob=obj, invalid_obj=Mobject(), index=0, suggest_using_group=True
)
assert len(obj.submobjects) == 1
with pytest.raises(TypeError) as add_vmob_and_mob_info:
# If only one of the added objects is not an instance of VMobject, none of them should be added
obj.add(VMobject(), Mobject())
assert str(add_vmob_and_mob_info.value) == (
get_type_error_message(Mobject(), [0, 1])
assert str(add_vmob_and_mob_info.value) == get_add_type_error_message(
parent_mob=obj, invalid_obj=Mobject(), index=1, suggest_using_group=True
)
assert len(obj.submobjects) == 1
@ -62,7 +89,8 @@ def test_vmobject_add():
with pytest.raises(ValueError) as add_self_info:
obj.add(obj)
assert str(add_self_info.value) == (
"Cannot add VMobject as a submobject of itself (at index 0)."
# TODO: when OpenGLVMobject is renamed to VMobject, also modify this message
"Cannot add OpenGLVMobject as a submobject of itself (at index 0)."
)
assert len(obj.submobjects) == 1
@ -163,35 +191,35 @@ def test_vgroup_init():
# A VGroup cannot contain non-VMobject values.
with pytest.raises(TypeError) as init_with_float_info:
VGroup(3.0)
assert str(init_with_float_info.value) == (
"Only values of type VMobject can be added as submobjects of VGroup, "
"but the value 3.0 (at index 0 of parameter 0) is of type float."
assert str(init_with_float_info.value) == get_add_type_error_message(
parent_mob=VGroup(), invalid_obj=3.0, index=0
)
with pytest.raises(TypeError) as init_with_mob_info:
VGroup(Mobject())
assert str(init_with_mob_info.value) == (
"Only values of type VMobject can be added as submobjects of VGroup, "
"but the value Mobject (at index 0 of parameter 0) is of type Mobject. You can try "
"adding this value into a Group instead."
assert str(init_with_mob_info.value) == get_add_type_error_message(
parent_mob=VGroup(), invalid_obj=Mobject(), index=0, suggest_using_group=True
)
with pytest.raises(TypeError) as init_with_vmob_and_mob_info:
VGroup(VMobject(), Mobject())
assert str(init_with_vmob_and_mob_info.value) == (
"Only values of type VMobject can be added as submobjects of VGroup, "
"but the value Mobject (at index 0 of parameter 1) is of type Mobject. You can try "
"adding this value into a Group instead."
assert str(init_with_vmob_and_mob_info.value) == get_add_type_error_message(
parent_mob=VGroup(), invalid_obj=Mobject(), index=1, suggest_using_group=True
)
def test_vgroup_init_with_iterable():
"""Test VGroup instantiation with an iterable type."""
def type_generator(type_to_generate, n):
def type_generator(type_to_generate: type[T1], n: int) -> Iterable[T1]:
return (type_to_generate() for _ in range(n))
def mixed_type_generator(major_type, minor_type, minor_type_positions, n):
def mixed_type_generator(
major_type: type[T1],
minor_type: type[T2],
minor_type_positions: list[int],
n: int,
) -> Iterable[T1 | T2]:
return (
minor_type() if i in minor_type_positions else major_type()
for i in range(n)
@ -213,25 +241,33 @@ def test_vgroup_init_with_iterable():
# A VGroup cannot be initialised with an iterable containing a Mobject
with pytest.raises(TypeError) as init_with_mob_iterable:
VGroup(type_generator(Mobject, 5))
assert str(init_with_mob_iterable.value) == (
"Only values of type VMobject can be added as submobjects of VGroup, "
"but the value Mobject (at index 0 of parameter 0) is of type Mobject."
assert str(init_with_mob_iterable.value) == get_add_type_error_message(
parent_mob=VGroup(),
invalid_obj=Mobject(),
index=0,
subindex=0,
suggest_using_group=True,
)
# A VGroup cannot be initialised with an iterable containing a Mobject in any position
with pytest.raises(TypeError) as init_with_mobs_and_vmobs_iterable:
VGroup(mixed_type_generator(VMobject, Mobject, [3, 5], 7))
assert str(init_with_mobs_and_vmobs_iterable.value) == (
"Only values of type VMobject can be added as submobjects of VGroup, "
"but the value Mobject (at index 3 of parameter 0) is of type Mobject."
assert str(init_with_mobs_and_vmobs_iterable.value) == get_add_type_error_message(
parent_mob=VGroup(),
invalid_obj=Mobject(),
index=0,
subindex=3,
suggest_using_group=True,
)
# A VGroup cannot be initialised with an iterable containing non VMobject's in any position
with pytest.raises(TypeError) as init_with_float_and_vmobs_iterable:
VGroup(mixed_type_generator(VMobject, float, [6, 7], 9))
assert str(init_with_float_and_vmobs_iterable.value) == (
"Only values of type VMobject can be added as submobjects of VGroup, "
"but the value 0.0 (at index 6 of parameter 0) is of type float."
assert str(init_with_float_and_vmobs_iterable.value) == get_add_type_error_message(
parent_mob=VGroup(),
invalid_obj=0.0,
index=0,
subindex=6,
)
@ -246,9 +282,10 @@ def test_vgroup_add():
# Can't add non-VMobject values to a VMobject or VGroup.
with pytest.raises(TypeError) as add_int_info:
obj.add(3)
assert str(add_int_info.value) == (
"Only values of type VMobject can be added as submobjects of VGroup, "
"but the value 3 (at index 0 of parameter 0) is of type int."
assert str(add_int_info.value) == get_add_type_error_message(
parent_mob=obj,
invalid_obj=3,
index=0,
)
assert len(obj.submobjects) == 1
@ -256,20 +293,16 @@ def test_vgroup_add():
# VMobjects. Suggest adding them into a Group instead.
with pytest.raises(TypeError) as add_mob_info:
obj.add(Mobject())
assert str(add_mob_info.value) == (
"Only values of type VMobject can be added as submobjects of VGroup, "
"but the value Mobject (at index 0 of parameter 0) is of type Mobject. You can try "
"adding this value into a Group instead."
assert str(add_mob_info.value) == get_add_type_error_message(
parent_mob=obj, invalid_obj=Mobject(), index=0, suggest_using_group=True
)
assert len(obj.submobjects) == 1
with pytest.raises(TypeError) as add_vmob_and_mob_info:
# If only one of the added objects is not an instance of VMobject, none of them should be added
obj.add(VMobject(), Mobject())
assert str(add_vmob_and_mob_info.value) == (
"Only values of type VMobject can be added as submobjects of VGroup, "
"but the value Mobject (at index 0 of parameter 1) is of type Mobject. You can try "
"adding this value into a Group instead."
assert str(add_vmob_and_mob_info.value) == get_add_type_error_message(
parent_mob=obj, invalid_obj=Mobject(), index=1, suggest_using_group=True
)
assert len(obj.submobjects) == 1
@ -277,7 +310,7 @@ def test_vgroup_add():
with pytest.raises(ValueError) as add_self_info:
obj.add(obj)
assert str(add_self_info.value) == (
"Cannot add VGroup as a submobject of itself (at index 0)."
"Cannot add OpenGLVGroup as a submobject of itself (at index 0)."
)
assert len(obj.submobjects) == 1
@ -435,9 +468,8 @@ def test_vgroup_item_assignment_only_allows_vmobjects():
vgroup = VGroup(VMobject())
with pytest.raises(TypeError) as assign_str_info:
vgroup[0] = "invalid object"
assert str(assign_str_info.value) == (
"Only values of type VMobject can be added as submobjects of VGroup, "
"but the value invalid object (at index 0) is of type str."
assert str(assign_str_info.value) == get_add_type_error_message(
parent_mob=vgroup, invalid_obj="invalid object", index=0
)
@ -541,22 +573,43 @@ def test_proportion_from_point():
def test_pointwise_become_partial_where_vmobject_is_self():
sq = Square()
sq.pointwise_become_partial(vmobject=sq, a=0.2, b=0.7)
expected_points = np.array(
# remap=True forces the square to retain its initial amount of 4 Bézier curves after
# becoming partial, so it has 4 * 3 = 12 points.
square_1 = Square()
square_1.pointwise_become_partial(vmobject=square_1, a=0.2, b=0.7, remap=True)
expected_points_1 = np.array(
[
[-0.6, 1.0, 0.0],
[-0.73333333, 1.0, 0.0],
[-0.86666667, 1.0, 0.0],
[-0.7, 1.0, 0.0],
[-0.8, 1.0, 0.0],
[-0.8, 1.0, 0.0],
[-0.9, 1.0, 0.0],
[-1.0, 1.0, 0.0],
[-1.0, 1.0, 0.0],
[-1.0, 0.33333333, 0.0],
[-1.0, -0.33333333, 0.0],
[-1.0, 0.0, 0.0],
[-1.0, -1.0, 0.0],
[-1.0, -1.0, 0.0],
[-0.46666667, -1.0, 0.0],
[0.06666667, -1.0, 0.0],
[-0.2, -1.0, 0.0],
[0.6, -1.0, 0.0],
]
)
np.testing.assert_allclose(sq.points, expected_points)
np.testing.assert_allclose(square_1.points, expected_points_1)
# remap=False allow the square to end up with less Bézier curves after becoming
# partial. In this case, it ends up with 3 curves, so 3 * 3 = 9 points.
square_2 = Square()
square_2.pointwise_become_partial(vmobject=square_2, a=0.2, b=0.7, remap=False)
expected_points_2 = np.array(
[
[-0.6, 1.0, 0.0],
[-0.8, 1.0, 0.0],
[-1.0, 1.0, 0.0],
[-1.0, 1.0, 0.0],
[-1.0, 0.0, 0.0],
[-1.0, -1.0, 0.0],
[-1.0, -1.0, 0.0],
[-0.2, -1.0, 0.0],
[0.6, -1.0, 0.0],
]
)
np.testing.assert_allclose(square_2.points, expected_points_2)