Skip to content

Commit d5b8b41

Browse files
Implemented VGroup parsing iterables (#3966)
1 parent 39382e6 commit d5b8b41

File tree

3 files changed

+191
-19
lines changed

3 files changed

+191
-19
lines changed

manim/mobject/types/vectorized_mobject.py

Lines changed: 74 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from manim.constants import *
2525
from manim.mobject.mobject import Mobject
2626
from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
27+
from manim.mobject.opengl.opengl_mobject import OpenGLMobject
2728
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject
2829
from manim.mobject.three_d.three_d_utils import (
2930
get_3d_vmob_gradient_start_and_end_points,
@@ -2056,7 +2057,11 @@ def construct(self):
20562057
20572058
"""
20582059

2059-
def __init__(self, *vmobjects, **kwargs):
2060+
def __init__(
2061+
self,
2062+
*vmobjects: VMobject | Iterable[VMobject],
2063+
**kwargs,
2064+
):
20602065
super().__init__(**kwargs)
20612066
self.add(*vmobjects)
20622067

@@ -2069,13 +2074,16 @@ def __str__(self) -> str:
20692074
f"submobject{'s' if len(self.submobjects) > 0 else ''}"
20702075
)
20712076

2072-
def add(self, *vmobjects: VMobject) -> Self:
2073-
"""Checks if all passed elements are an instance of VMobject and then add them to submobjects
2077+
def add(
2078+
self,
2079+
*vmobjects: VMobject | Iterable[VMobject],
2080+
) -> Self:
2081+
"""Checks if all passed elements are an instance, or iterables of VMobject and then adds them to submobjects
20742082
20752083
Parameters
20762084
----------
20772085
vmobjects
2078-
List of VMobject to add
2086+
List or iterable of VMobjects to add
20792087
20802088
Returns
20812089
-------
@@ -2084,10 +2092,13 @@ def add(self, *vmobjects: VMobject) -> Self:
20842092
Raises
20852093
------
20862094
TypeError
2087-
If one element of the list is not an instance of VMobject
2095+
If one element of the list, or iterable is not an instance of VMobject
20882096
20892097
Examples
20902098
--------
2099+
The following example shows how to add individual or multiple `VMobject` instances through the `VGroup`
2100+
constructor and its `.add()` method.
2101+
20912102
.. manim:: AddToVGroup
20922103
20932104
class AddToVGroup(Scene):
@@ -2116,8 +2127,65 @@ def construct(self):
21162127
self.play( # Animate group without component
21172128
(gr-circle_red).animate.shift(RIGHT)
21182129
)
2130+
2131+
A `VGroup` can be created using iterables as well. Keep in mind that all generated values from an
2132+
iterable must be an instance of `VMobject`. This is demonstrated below:
2133+
2134+
.. manim:: AddIterableToVGroupExample
2135+
:save_last_frame:
2136+
2137+
class AddIterableToVGroupExample(Scene):
2138+
def construct(self):
2139+
v = VGroup(
2140+
Square(), # Singular VMobject instance
2141+
[Circle(), Triangle()], # List of VMobject instances
2142+
Dot(),
2143+
(Dot() for _ in range(2)), # Iterable that generates VMobjects
2144+
)
2145+
v.arrange()
2146+
self.add(v)
2147+
2148+
To facilitate this, the iterable is unpacked before its individual instances are added to the `VGroup`.
2149+
As a result, when you index a `VGroup`, you will never get back an iterable.
2150+
Instead, you will always receive `VMobject` instances, including those
2151+
that were part of the iterable/s that you originally added to the `VGroup`.
21192152
"""
2120-
return super().add(*vmobjects)
2153+
2154+
def get_type_error_message(invalid_obj, invalid_indices):
2155+
return (
2156+
f"Only values of type {vmobject_render_type.__name__} can be added "
2157+
"as submobjects of VGroup, but the value "
2158+
f"{repr(invalid_obj)} (at index {invalid_indices[1]} of "
2159+
f"parameter {invalid_indices[0]}) is of type "
2160+
f"{type(invalid_obj).__name__}."
2161+
)
2162+
2163+
vmobject_render_type = (
2164+
OpenGLVMobject if config.renderer == RendererType.OPENGL else VMobject
2165+
)
2166+
valid_vmobjects = []
2167+
2168+
for i, vmobject in enumerate(vmobjects):
2169+
if isinstance(vmobject, vmobject_render_type):
2170+
valid_vmobjects.append(vmobject)
2171+
elif isinstance(vmobject, Iterable) and not isinstance(
2172+
vmobject, (Mobject, OpenGLMobject)
2173+
):
2174+
for j, subvmobject in enumerate(vmobject):
2175+
if not isinstance(subvmobject, vmobject_render_type):
2176+
raise TypeError(get_type_error_message(subvmobject, (i, j)))
2177+
valid_vmobjects.append(subvmobject)
2178+
elif isinstance(vmobject, Iterable) and isinstance(
2179+
vmobject, (Mobject, OpenGLMobject)
2180+
):
2181+
raise TypeError(
2182+
f"{get_type_error_message(vmobject, (i, 0))} "
2183+
"You can try adding this value into a Group instead."
2184+
)
2185+
else:
2186+
raise TypeError(get_type_error_message(vmobject, (i, 0)))
2187+
2188+
return super().add(*valid_vmobjects)
21212189

21222190
def __add__(self, vmobject: VMobject) -> Self:
21232191
return VGroup(*self.submobjects, vmobject)

tests/module/mobject/types/vectorized_mobject/test_vectorized_mobject.py

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -132,26 +132,72 @@ def test_vgroup_init():
132132
VGroup(3.0)
133133
assert str(init_with_float_info.value) == (
134134
"Only values of type VMobject can be added as submobjects of VGroup, "
135-
"but the value 3.0 (at index 0) is of type float."
135+
"but the value 3.0 (at index 0 of parameter 0) is of type float."
136136
)
137137

138138
with pytest.raises(TypeError) as init_with_mob_info:
139139
VGroup(Mobject())
140140
assert str(init_with_mob_info.value) == (
141141
"Only values of type VMobject can be added as submobjects of VGroup, "
142-
"but the value Mobject (at index 0) is of type Mobject. You can try "
142+
"but the value Mobject (at index 0 of parameter 0) is of type Mobject. You can try "
143143
"adding this value into a Group instead."
144144
)
145145

146146
with pytest.raises(TypeError) as init_with_vmob_and_mob_info:
147147
VGroup(VMobject(), Mobject())
148148
assert str(init_with_vmob_and_mob_info.value) == (
149149
"Only values of type VMobject can be added as submobjects of VGroup, "
150-
"but the value Mobject (at index 1) is of type Mobject. You can try "
150+
"but the value Mobject (at index 0 of parameter 1) is of type Mobject. You can try "
151151
"adding this value into a Group instead."
152152
)
153153

154154

155+
def test_vgroup_init_with_iterable():
156+
"""Test VGroup instantiation with an iterable type."""
157+
158+
def type_generator(type_to_generate, n):
159+
return (type_to_generate() for _ in range(n))
160+
161+
def mixed_type_generator(major_type, minor_type, minor_type_positions, n):
162+
return (
163+
minor_type() if i in minor_type_positions else major_type()
164+
for i in range(n)
165+
)
166+
167+
obj = VGroup(VMobject())
168+
assert len(obj.submobjects) == 1
169+
170+
obj = VGroup(type_generator(VMobject, 38))
171+
assert len(obj.submobjects) == 38
172+
173+
obj = VGroup(VMobject(), [VMobject(), VMobject()], type_generator(VMobject, 38))
174+
assert len(obj.submobjects) == 41
175+
176+
# A VGroup cannot be initialised with an iterable containing a Mobject
177+
with pytest.raises(TypeError) as init_with_mob_iterable:
178+
VGroup(type_generator(Mobject, 5))
179+
assert str(init_with_mob_iterable.value) == (
180+
"Only values of type VMobject can be added as submobjects of VGroup, "
181+
"but the value Mobject (at index 0 of parameter 0) is of type Mobject."
182+
)
183+
184+
# A VGroup cannot be initialised with an iterable containing a Mobject in any position
185+
with pytest.raises(TypeError) as init_with_mobs_and_vmobs_iterable:
186+
VGroup(mixed_type_generator(VMobject, Mobject, [3, 5], 7))
187+
assert str(init_with_mobs_and_vmobs_iterable.value) == (
188+
"Only values of type VMobject can be added as submobjects of VGroup, "
189+
"but the value Mobject (at index 3 of parameter 0) is of type Mobject."
190+
)
191+
192+
# A VGroup cannot be initialised with an iterable containing non VMobject's in any position
193+
with pytest.raises(TypeError) as init_with_float_and_vmobs_iterable:
194+
VGroup(mixed_type_generator(VMobject, float, [6, 7], 9))
195+
assert str(init_with_float_and_vmobs_iterable.value) == (
196+
"Only values of type VMobject can be added as submobjects of VGroup, "
197+
"but the value 0.0 (at index 6 of parameter 0) is of type float."
198+
)
199+
200+
155201
def test_vgroup_add():
156202
"""Test the VGroup add method."""
157203
obj = VGroup()
@@ -165,7 +211,7 @@ def test_vgroup_add():
165211
obj.add(3)
166212
assert str(add_int_info.value) == (
167213
"Only values of type VMobject can be added as submobjects of VGroup, "
168-
"but the value 3 (at index 0) is of type int."
214+
"but the value 3 (at index 0 of parameter 0) is of type int."
169215
)
170216
assert len(obj.submobjects) == 1
171217

@@ -175,7 +221,7 @@ def test_vgroup_add():
175221
obj.add(Mobject())
176222
assert str(add_mob_info.value) == (
177223
"Only values of type VMobject can be added as submobjects of VGroup, "
178-
"but the value Mobject (at index 0) is of type Mobject. You can try "
224+
"but the value Mobject (at index 0 of parameter 0) is of type Mobject. You can try "
179225
"adding this value into a Group instead."
180226
)
181227
assert len(obj.submobjects) == 1
@@ -185,7 +231,7 @@ def test_vgroup_add():
185231
obj.add(VMobject(), Mobject())
186232
assert str(add_vmob_and_mob_info.value) == (
187233
"Only values of type VMobject can be added as submobjects of VGroup, "
188-
"but the value Mobject (at index 1) is of type Mobject. You can try "
234+
"but the value Mobject (at index 0 of parameter 1) is of type Mobject. You can try "
189235
"adding this value into a Group instead."
190236
)
191237
assert len(obj.submobjects) == 1

tests/opengl/test_opengl_vectorized_mobject.py

Lines changed: 65 additions & 7 deletions
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 Circle, Line, Square, VDict, VGroup
6+
from manim import Circle, Line, Square, VDict, VGroup, VMobject
77
from manim.mobject.opengl.opengl_mobject import OpenGLMobject
88
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject
99

@@ -90,26 +90,84 @@ def test_vgroup_init(using_opengl_renderer):
9090
VGroup(3.0)
9191
assert str(init_with_float_info.value) == (
9292
"Only values of type OpenGLVMobject can be added as submobjects of "
93-
"VGroup, but the value 3.0 (at index 0) is of type float."
93+
"VGroup, but the value 3.0 (at index 0 of parameter 0) is of type float."
9494
)
9595

9696
with pytest.raises(TypeError) as init_with_mob_info:
9797
VGroup(OpenGLMobject())
9898
assert str(init_with_mob_info.value) == (
9999
"Only values of type OpenGLVMobject can be added as submobjects of "
100-
"VGroup, but the value OpenGLMobject (at index 0) is of type "
100+
"VGroup, but the value OpenGLMobject (at index 0 of parameter 0) is of type "
101101
"OpenGLMobject. You can try adding this value into a Group instead."
102102
)
103103

104104
with pytest.raises(TypeError) as init_with_vmob_and_mob_info:
105105
VGroup(OpenGLVMobject(), OpenGLMobject())
106106
assert str(init_with_vmob_and_mob_info.value) == (
107107
"Only values of type OpenGLVMobject can be added as submobjects of "
108-
"VGroup, but the value OpenGLMobject (at index 1) is of type "
108+
"VGroup, but the value OpenGLMobject (at index 0 of parameter 1) is of type "
109109
"OpenGLMobject. You can try adding this value into a Group instead."
110110
)
111111

112112

113+
def test_vgroup_init_with_iterable(using_opengl_renderer):
114+
"""Test VGroup instantiation with an iterable type."""
115+
116+
def type_generator(type_to_generate, n):
117+
return (type_to_generate() for _ in range(n))
118+
119+
def mixed_type_generator(major_type, minor_type, minor_type_positions, n):
120+
return (
121+
minor_type() if i in minor_type_positions else major_type()
122+
for i in range(n)
123+
)
124+
125+
obj = VGroup(OpenGLVMobject())
126+
assert len(obj.submobjects) == 1
127+
128+
obj = VGroup(type_generator(OpenGLVMobject, 38))
129+
assert len(obj.submobjects) == 38
130+
131+
obj = VGroup(
132+
OpenGLVMobject(),
133+
[OpenGLVMobject(), OpenGLVMobject()],
134+
type_generator(OpenGLVMobject, 38),
135+
)
136+
assert len(obj.submobjects) == 41
137+
138+
# A VGroup cannot be initialised with an iterable containing a OpenGLMobject
139+
with pytest.raises(TypeError) as init_with_mob_iterable:
140+
VGroup(type_generator(OpenGLMobject, 5))
141+
assert str(init_with_mob_iterable.value) == (
142+
"Only values of type OpenGLVMobject can be added as submobjects of VGroup, "
143+
"but the value OpenGLMobject (at index 0 of parameter 0) is of type OpenGLMobject."
144+
)
145+
146+
# A VGroup cannot be initialised with an iterable containing a OpenGLMobject in any position
147+
with pytest.raises(TypeError) as init_with_mobs_and_vmobs_iterable:
148+
VGroup(mixed_type_generator(OpenGLVMobject, OpenGLMobject, [3, 5], 7))
149+
assert str(init_with_mobs_and_vmobs_iterable.value) == (
150+
"Only values of type OpenGLVMobject can be added as submobjects of VGroup, "
151+
"but the value OpenGLMobject (at index 3 of parameter 0) is of type OpenGLMobject."
152+
)
153+
154+
# A VGroup cannot be initialised with an iterable containing non OpenGLVMobject's in any position
155+
with pytest.raises(TypeError) as init_with_float_and_vmobs_iterable:
156+
VGroup(mixed_type_generator(OpenGLVMobject, float, [6, 7], 9))
157+
assert str(init_with_float_and_vmobs_iterable.value) == (
158+
"Only values of type OpenGLVMobject can be added as submobjects of VGroup, "
159+
"but the value 0.0 (at index 6 of parameter 0) is of type float."
160+
)
161+
162+
# A VGroup cannot be initialised with an iterable containing both OpenGLVMobject's and VMobject's
163+
with pytest.raises(TypeError) as init_with_mobs_and_vmobs_iterable:
164+
VGroup(mixed_type_generator(OpenGLVMobject, VMobject, [3, 5], 7))
165+
assert str(init_with_mobs_and_vmobs_iterable.value) == (
166+
"Only values of type OpenGLVMobject can be added as submobjects of VGroup, "
167+
"but the value VMobject (at index 3 of parameter 0) is of type VMobject."
168+
)
169+
170+
113171
def test_vgroup_add(using_opengl_renderer):
114172
"""Test the VGroup add method."""
115173
obj = VGroup()
@@ -123,7 +181,7 @@ def test_vgroup_add(using_opengl_renderer):
123181
obj.add(3)
124182
assert str(add_int_info.value) == (
125183
"Only values of type OpenGLVMobject can be added as submobjects of "
126-
"VGroup, but the value 3 (at index 0) is of type int."
184+
"VGroup, but the value 3 (at index 0 of parameter 0) is of type int."
127185
)
128186
assert len(obj.submobjects) == 1
129187

@@ -133,7 +191,7 @@ def test_vgroup_add(using_opengl_renderer):
133191
obj.add(OpenGLMobject())
134192
assert str(add_mob_info.value) == (
135193
"Only values of type OpenGLVMobject can be added as submobjects of "
136-
"VGroup, but the value OpenGLMobject (at index 0) is of type "
194+
"VGroup, but the value OpenGLMobject (at index 0 of parameter 0) is of type "
137195
"OpenGLMobject. You can try adding this value into a Group instead."
138196
)
139197
assert len(obj.submobjects) == 1
@@ -143,7 +201,7 @@ def test_vgroup_add(using_opengl_renderer):
143201
obj.add(OpenGLVMobject(), OpenGLMobject())
144202
assert str(add_vmob_and_mob_info.value) == (
145203
"Only values of type OpenGLVMobject can be added as submobjects of "
146-
"VGroup, but the value OpenGLMobject (at index 1) is of type "
204+
"VGroup, but the value OpenGLMobject (at index 0 of parameter 1) is of type "
147205
"OpenGLMobject. You can try adding this value into a Group instead."
148206
)
149207
assert len(obj.submobjects) == 1

0 commit comments

Comments
 (0)