manim 0.18.1__py3-none-any.whl → 0.19.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of manim might be problematic. Click here for more details.

Files changed (129) hide show
  1. manim/__main__.py +45 -12
  2. manim/_config/__init__.py +2 -2
  3. manim/_config/cli_colors.py +8 -4
  4. manim/_config/default.cfg +0 -2
  5. manim/_config/logger_utils.py +5 -0
  6. manim/_config/utils.py +29 -38
  7. manim/animation/animation.py +148 -8
  8. manim/animation/composition.py +16 -13
  9. manim/animation/creation.py +184 -8
  10. manim/animation/fading.py +5 -8
  11. manim/animation/indication.py +93 -26
  12. manim/animation/movement.py +21 -3
  13. manim/animation/rotation.py +2 -1
  14. manim/animation/specialized.py +3 -5
  15. manim/animation/speedmodifier.py +3 -3
  16. manim/animation/transform.py +4 -5
  17. manim/animation/updaters/mobject_update_utils.py +17 -14
  18. manim/camera/camera.py +2 -2
  19. manim/cli/__init__.py +17 -0
  20. manim/cli/cfg/group.py +52 -36
  21. manim/cli/checkhealth/checks.py +92 -76
  22. manim/cli/checkhealth/commands.py +12 -5
  23. manim/cli/default_group.py +148 -24
  24. manim/cli/init/commands.py +28 -23
  25. manim/cli/plugins/commands.py +13 -3
  26. manim/cli/render/commands.py +47 -42
  27. manim/cli/render/global_options.py +43 -9
  28. manim/cli/render/render_options.py +84 -19
  29. manim/constants.py +11 -4
  30. manim/mobject/frame.py +0 -1
  31. manim/mobject/geometry/arc.py +109 -75
  32. manim/mobject/geometry/boolean_ops.py +20 -17
  33. manim/mobject/geometry/labeled.py +300 -77
  34. manim/mobject/geometry/line.py +120 -60
  35. manim/mobject/geometry/polygram.py +109 -25
  36. manim/mobject/geometry/shape_matchers.py +35 -15
  37. manim/mobject/geometry/tips.py +36 -27
  38. manim/mobject/graph.py +48 -40
  39. manim/mobject/graphing/coordinate_systems.py +110 -45
  40. manim/mobject/graphing/functions.py +16 -10
  41. manim/mobject/graphing/number_line.py +23 -9
  42. manim/mobject/graphing/probability.py +2 -10
  43. manim/mobject/graphing/scale.py +6 -5
  44. manim/mobject/matrix.py +17 -19
  45. manim/mobject/mobject.py +149 -103
  46. manim/mobject/opengl/opengl_geometry.py +4 -8
  47. manim/mobject/opengl/opengl_mobject.py +506 -343
  48. manim/mobject/opengl/opengl_point_cloud_mobject.py +3 -7
  49. manim/mobject/opengl/opengl_surface.py +1 -2
  50. manim/mobject/opengl/opengl_vectorized_mobject.py +27 -65
  51. manim/mobject/svg/brace.py +61 -13
  52. manim/mobject/svg/svg_mobject.py +2 -1
  53. manim/mobject/table.py +11 -12
  54. manim/mobject/text/code_mobject.py +186 -550
  55. manim/mobject/text/numbers.py +7 -7
  56. manim/mobject/text/tex_mobject.py +22 -13
  57. manim/mobject/text/text_mobject.py +29 -20
  58. manim/mobject/three_d/polyhedra.py +98 -1
  59. manim/mobject/three_d/three_dimensions.py +59 -31
  60. manim/mobject/types/image_mobject.py +37 -23
  61. manim/mobject/types/point_cloud_mobject.py +103 -67
  62. manim/mobject/types/vectorized_mobject.py +387 -214
  63. manim/mobject/value_tracker.py +2 -1
  64. manim/mobject/vector_field.py +2 -4
  65. manim/opengl/__init__.py +3 -3
  66. manim/plugins/__init__.py +2 -3
  67. manim/plugins/plugins_flags.py +3 -3
  68. manim/renderer/cairo_renderer.py +11 -11
  69. manim/renderer/opengl_renderer.py +19 -20
  70. manim/renderer/shader.py +2 -3
  71. manim/renderer/shader_wrapper.py +3 -2
  72. manim/scene/moving_camera_scene.py +23 -0
  73. manim/scene/scene.py +72 -41
  74. manim/scene/scene_file_writer.py +313 -164
  75. manim/scene/section.py +15 -15
  76. manim/scene/three_d_scene.py +8 -15
  77. manim/scene/vector_space_scene.py +3 -6
  78. manim/typing.py +326 -66
  79. manim/utils/bezier.py +1658 -381
  80. manim/utils/caching.py +11 -5
  81. manim/utils/color/AS2700.py +2 -0
  82. manim/utils/color/BS381.py +2 -0
  83. manim/utils/color/DVIPSNAMES.py +96 -0
  84. manim/utils/color/SVGNAMES.py +179 -0
  85. manim/utils/color/X11.py +3 -0
  86. manim/utils/color/XKCD.py +2 -0
  87. manim/utils/color/__init__.py +8 -5
  88. manim/utils/color/core.py +818 -301
  89. manim/utils/color/manim_colors.py +7 -9
  90. manim/utils/commands.py +40 -19
  91. manim/utils/config_ops.py +18 -13
  92. manim/utils/debug.py +8 -6
  93. manim/utils/deprecation.py +92 -43
  94. manim/utils/docbuild/autoaliasattr_directive.py +45 -8
  95. manim/utils/docbuild/autocolor_directive.py +12 -13
  96. manim/utils/docbuild/manim_directive.py +35 -29
  97. manim/utils/docbuild/module_parsing.py +74 -27
  98. manim/utils/family.py +3 -3
  99. manim/utils/family_ops.py +12 -4
  100. manim/utils/file_ops.py +22 -16
  101. manim/utils/hashing.py +7 -7
  102. manim/utils/images.py +10 -4
  103. manim/utils/ipython_magic.py +12 -8
  104. manim/utils/iterables.py +161 -119
  105. manim/utils/module_ops.py +55 -19
  106. manim/utils/opengl.py +68 -23
  107. manim/utils/parameter_parsing.py +3 -2
  108. manim/utils/paths.py +11 -5
  109. manim/utils/polylabel.py +168 -0
  110. manim/utils/qhull.py +218 -0
  111. manim/utils/rate_functions.py +69 -32
  112. manim/utils/simple_functions.py +24 -15
  113. manim/utils/sounds.py +7 -1
  114. manim/utils/space_ops.py +48 -37
  115. manim/utils/testing/_frames_testers.py +13 -8
  116. manim/utils/testing/_show_diff.py +5 -3
  117. manim/utils/testing/_test_class_makers.py +33 -18
  118. manim/utils/testing/frames_comparison.py +20 -14
  119. manim/utils/tex.py +4 -2
  120. manim/utils/tex_file_writing.py +45 -45
  121. manim/utils/tex_templates.py +1 -1
  122. manim/utils/unit.py +6 -5
  123. {manim-0.18.1.dist-info → manim-0.19.0.dist-info}/METADATA +16 -9
  124. manim-0.19.0.dist-info/RECORD +221 -0
  125. {manim-0.18.1.dist-info → manim-0.19.0.dist-info}/WHEEL +1 -1
  126. manim-0.18.1.dist-info/RECORD +0 -217
  127. {manim-0.18.1.dist-info → manim-0.19.0.dist-info}/LICENSE +0 -0
  128. {manim-0.18.1.dist-info → manim-0.19.0.dist-info}/LICENSE.community +0 -0
  129. {manim-0.18.1.dist-info → manim-0.19.0.dist-info}/entry_points.txt +0 -0
@@ -14,16 +14,8 @@ __all__ = [
14
14
 
15
15
  import itertools as it
16
16
  import sys
17
- from typing import (
18
- TYPE_CHECKING,
19
- Callable,
20
- Generator,
21
- Hashable,
22
- Iterable,
23
- Literal,
24
- Mapping,
25
- Sequence,
26
- )
17
+ from collections.abc import Hashable, Iterable, Mapping, Sequence
18
+ from typing import TYPE_CHECKING, Callable, Literal
27
19
 
28
20
  import numpy as np
29
21
  from PIL.Image import Image
@@ -32,13 +24,15 @@ from manim import config
32
24
  from manim.constants import *
33
25
  from manim.mobject.mobject import Mobject
34
26
  from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
27
+ from manim.mobject.opengl.opengl_mobject import OpenGLMobject
35
28
  from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject
36
29
  from manim.mobject.three_d.three_d_utils import (
37
30
  get_3d_vmob_gradient_start_and_end_points,
38
31
  )
39
32
  from manim.utils.bezier import (
40
33
  bezier,
41
- get_smooth_handle_points,
34
+ bezier_remap,
35
+ get_smooth_cubic_bezier_handle_points,
42
36
  integer_interpolate,
43
37
  interpolate,
44
38
  partial_bezier_points,
@@ -54,18 +48,22 @@ from manim.utils.iterables import (
54
48
  from manim.utils.space_ops import rotate_vector, shoelace_direction
55
49
 
56
50
  if TYPE_CHECKING:
51
+ from typing import Any
52
+
57
53
  import numpy.typing as npt
58
54
  from typing_extensions import Self
59
55
 
60
56
  from manim.typing import (
61
- BezierPoints,
62
- CubicBezierPoints,
57
+ CubicBezierPath,
58
+ CubicBezierPointsLike,
59
+ CubicSpline,
63
60
  ManimFloat,
64
61
  MappingFunction,
65
- Point2D,
62
+ Point2DLike,
66
63
  Point3D,
67
64
  Point3D_Array,
68
- QuadraticBezierPoints,
65
+ Point3DLike,
66
+ Point3DLike_Array,
69
67
  RGBA_Array_Float,
70
68
  Vector3D,
71
69
  Zeros,
@@ -79,16 +77,6 @@ if TYPE_CHECKING:
79
77
  # - Think about length of self.points. Always 0 or 1 mod 4?
80
78
  # That's kind of weird.
81
79
 
82
- __all__ = [
83
- "VMobject",
84
- "VGroup",
85
- "VDict",
86
- "VectorizedPoint",
87
- "CurvesAsSubmobjects",
88
- "VectorizedPoint",
89
- "DashedVMobject",
90
- ]
91
-
92
80
 
93
81
  class VMobject(Mobject):
94
82
  """A vectorized mobject.
@@ -139,7 +127,7 @@ class VMobject(Mobject):
139
127
  tolerance_for_point_equality: float = 1e-6,
140
128
  n_points_per_cubic_curve: int = 4,
141
129
  cap_style: CapStyleType = CapStyleType.AUTO,
142
- **kwargs,
130
+ **kwargs: Any,
143
131
  ):
144
132
  self.fill_opacity = fill_opacity
145
133
  self.stroke_opacity = stroke_opacity
@@ -166,6 +154,9 @@ class VMobject(Mobject):
166
154
  self.shade_in_3d: bool = shade_in_3d
167
155
  self.tolerance_for_point_equality: float = tolerance_for_point_equality
168
156
  self.n_points_per_cubic_curve: int = n_points_per_cubic_curve
157
+ self._bezier_t_values: npt.NDArray[float] = np.linspace(
158
+ 0, 1, n_points_per_cubic_curve
159
+ )
169
160
  self.cap_style: CapStyleType = cap_style
170
161
  super().__init__(**kwargs)
171
162
  self.submobjects: list[VMobject]
@@ -179,6 +170,9 @@ class VMobject(Mobject):
179
170
  if stroke_color is not None:
180
171
  self.stroke_color = ManimColor.parse(stroke_color)
181
172
 
173
+ def _assert_valid_submobjects(self, submobjects: Iterable[VMobject]) -> Self:
174
+ return self._assert_valid_submobjects_internal(submobjects, VMobject)
175
+
182
176
  # OpenGL compatibility
183
177
  @property
184
178
  def n_points_per_curve(self) -> int:
@@ -478,6 +472,64 @@ class VMobject(Mobject):
478
472
  self.set_stroke(opacity=opacity, family=family, background=True)
479
473
  return self
480
474
 
475
+ def scale(self, scale_factor: float, scale_stroke: bool = False, **kwargs) -> Self:
476
+ r"""Scale the size by a factor.
477
+
478
+ Default behavior is to scale about the center of the vmobject.
479
+
480
+ Parameters
481
+ ----------
482
+ scale_factor
483
+ The scaling factor :math:`\alpha`. If :math:`0 < |\alpha| < 1`, the mobject
484
+ will shrink, and for :math:`|\alpha| > 1` it will grow. Furthermore,
485
+ if :math:`\alpha < 0`, the mobject is also flipped.
486
+ scale_stroke
487
+ Boolean determining if the object's outline is scaled when the object is scaled.
488
+ If enabled, and object with 2px outline is scaled by a factor of .5, it will have an outline of 1px.
489
+ kwargs
490
+ Additional keyword arguments passed to
491
+ :meth:`~.Mobject.scale`.
492
+
493
+ Returns
494
+ -------
495
+ :class:`VMobject`
496
+ ``self``
497
+
498
+ Examples
499
+ --------
500
+
501
+ .. manim:: MobjectScaleExample
502
+ :save_last_frame:
503
+
504
+ class MobjectScaleExample(Scene):
505
+ def construct(self):
506
+ c1 = Circle(1, RED).set_x(-1)
507
+ c2 = Circle(1, GREEN).set_x(1)
508
+
509
+ vg = VGroup(c1, c2)
510
+ vg.set_stroke(width=50)
511
+ self.add(vg)
512
+
513
+ self.play(
514
+ c1.animate.scale(.25),
515
+ c2.animate.scale(.25,
516
+ scale_stroke=True)
517
+ )
518
+
519
+ See also
520
+ --------
521
+ :meth:`move_to`
522
+
523
+ """
524
+ if scale_stroke:
525
+ self.set_stroke(width=abs(scale_factor) * self.get_stroke_width())
526
+ self.set_stroke(
527
+ width=abs(scale_factor) * self.get_stroke_width(background=True),
528
+ background=True,
529
+ )
530
+ super().scale(scale_factor, **kwargs)
531
+ return self
532
+
481
533
  def fade(self, darkness: float = 0.5, family: bool = True) -> Self:
482
534
  factor = 1.0 - darkness
483
535
  self.set_fill(opacity=factor * self.get_fill_opacity(), family=False)
@@ -587,7 +639,6 @@ class VMobject(Mobject):
587
639
  :meth:`~.VMobject.set_sheen`
588
640
  :meth:`~.VMobject.rotate_sheen_direction`
589
641
  """
590
-
591
642
  direction = np.array(direction)
592
643
  if family:
593
644
  for submob in self.get_family():
@@ -653,7 +704,6 @@ class VMobject(Mobject):
653
704
  circle = Circle(fill_opacity=1).set_sheen(-0.3, DR)
654
705
  self.add(circle)
655
706
  """
656
-
657
707
  if family:
658
708
  for submob in self.submobjects:
659
709
  submob.set_sheen(factor, direction, family)
@@ -709,14 +759,14 @@ class VMobject(Mobject):
709
759
  submob.z_index_group = self
710
760
  return self
711
761
 
712
- def set_points(self, points: Point3D_Array) -> Self:
762
+ def set_points(self, points: Point3DLike_Array) -> Self:
713
763
  self.points: Point3D_Array = np.array(points)
714
764
  return self
715
765
 
716
766
  def resize_points(
717
767
  self,
718
768
  new_length: int,
719
- resize_func: Callable[[Point3D, int], Point3D] = resize_array,
769
+ resize_func: Callable[[Point3D_Array, int], Point3D_Array] = resize_array,
720
770
  ) -> Self:
721
771
  """Resize the array of anchor points and handles to have
722
772
  the specified size.
@@ -736,10 +786,10 @@ class VMobject(Mobject):
736
786
 
737
787
  def set_anchors_and_handles(
738
788
  self,
739
- anchors1: CubicBezierPoints,
740
- handles1: CubicBezierPoints,
741
- handles2: CubicBezierPoints,
742
- anchors2: CubicBezierPoints,
789
+ anchors1: Point3DLike_Array,
790
+ handles1: Point3DLike_Array,
791
+ handles2: Point3DLike_Array,
792
+ anchors2: Point3DLike_Array,
743
793
  ) -> Self:
744
794
  """Given two sets of anchors and handles, process them to set them as anchors
745
795
  and handles of the VMobject.
@@ -757,7 +807,7 @@ class VMobject(Mobject):
757
807
  assert len(anchors1) == len(handles1) == len(handles2) == len(anchors2)
758
808
  nppcc = self.n_points_per_cubic_curve # 4
759
809
  total_len = nppcc * len(anchors1)
760
- self.points = np.zeros((total_len, self.dim))
810
+ self.points = np.empty((total_len, self.dim))
761
811
  # the following will, from the four sets, dispatch them in points such that
762
812
  # self.points = [
763
813
  # anchors1[0], handles1[0], handles2[0], anchors1[0], anchors1[1],
@@ -769,31 +819,69 @@ class VMobject(Mobject):
769
819
  return self
770
820
 
771
821
  def clear_points(self) -> None:
822
+ # TODO: shouldn't this return self instead of None?
772
823
  self.points = np.zeros((0, self.dim))
773
824
 
774
- def append_points(self, new_points: Point3D_Array) -> Self:
825
+ def append_points(self, new_points: Point3DLike_Array) -> Self:
826
+ """Append the given ``new_points`` to the end of
827
+ :attr:`VMobject.points`.
828
+
829
+ Parameters
830
+ ----------
831
+ new_points
832
+ An array of 3D points to append.
833
+
834
+ Returns
835
+ -------
836
+ :class:`VMobject`
837
+ The VMobject itself, after appending ``new_points``.
838
+ """
775
839
  # TODO, check that number new points is a multiple of 4?
776
840
  # or else that if len(self.points) % 4 == 1, then
777
841
  # len(new_points) % 4 == 3?
778
- self.points = np.append(self.points, new_points, axis=0)
842
+ n = len(self.points)
843
+ points = np.empty((n + len(new_points), self.dim))
844
+ points[:n] = self.points
845
+ points[n:] = new_points
846
+ self.points = points
779
847
  return self
780
848
 
781
- def start_new_path(self, point: Point3D) -> Self:
782
- if len(self.points) % 4 != 0:
849
+ def start_new_path(self, point: Point3DLike) -> Self:
850
+ """Append a ``point`` to the :attr:`VMobject.points`, which will be the
851
+ beginning of a new Bézier curve in the path given by the points. If
852
+ there's an unfinished curve at the end of :attr:`VMobject.points`,
853
+ complete it by appending the last Bézier curve's start anchor as many
854
+ times as needed.
855
+
856
+ Parameters
857
+ ----------
858
+ point
859
+ A 3D point to append to :attr:`VMobject.points`.
860
+
861
+ Returns
862
+ -------
863
+ :class:`VMobject`
864
+ The VMobject itself, after appending ``point`` and starting a new
865
+ curve.
866
+ """
867
+ n_points = len(self.points)
868
+ nppc = self.n_points_per_curve
869
+ if n_points % nppc != 0:
783
870
  # close the open path by appending the last
784
871
  # start anchor sufficiently often
785
872
  last_anchor = self.get_start_anchors()[-1]
786
- for _ in range(4 - (len(self.points) % 4)):
787
- self.append_points([last_anchor])
788
- self.append_points([point])
873
+ closure = [last_anchor] * (nppc - (n_points % nppc))
874
+ self.append_points(closure + [point])
875
+ else:
876
+ self.append_points([point])
789
877
  return self
790
878
 
791
879
  def add_cubic_bezier_curve(
792
880
  self,
793
- anchor1: CubicBezierPoints,
794
- handle1: CubicBezierPoints,
795
- handle2: CubicBezierPoints,
796
- anchor2: CubicBezierPoints,
881
+ anchor1: Point3DLike,
882
+ handle1: Point3DLike,
883
+ handle2: Point3DLike,
884
+ anchor2: Point3DLike,
797
885
  ) -> None:
798
886
  # TODO, check the len(self.points) % 4 == 0?
799
887
  self.append_points([anchor1, handle1, handle2, anchor2])
@@ -804,9 +892,9 @@ class VMobject(Mobject):
804
892
 
805
893
  def add_cubic_bezier_curve_to(
806
894
  self,
807
- handle1: CubicBezierPoints,
808
- handle2: CubicBezierPoints,
809
- anchor: CubicBezierPoints,
895
+ handle1: Point3DLike,
896
+ handle2: Point3DLike,
897
+ anchor: Point3DLike,
810
898
  ) -> Self:
811
899
  """Add cubic bezier curve to the path.
812
900
 
@@ -836,8 +924,8 @@ class VMobject(Mobject):
836
924
 
837
925
  def add_quadratic_bezier_curve_to(
838
926
  self,
839
- handle: QuadraticBezierPoints,
840
- anchor: QuadraticBezierPoints,
927
+ handle: Point3DLike,
928
+ anchor: Point3DLike,
841
929
  ) -> Self:
842
930
  """Add Quadratic bezier curve to the path.
843
931
 
@@ -860,30 +948,29 @@ class VMobject(Mobject):
860
948
  )
861
949
  return self
862
950
 
863
- def add_line_to(self, point: Point3D) -> Self:
951
+ def add_line_to(self, point: Point3DLike) -> Self:
864
952
  """Add a straight line from the last point of VMobject to the given point.
865
953
 
866
954
  Parameters
867
955
  ----------
868
956
 
869
957
  point
870
- end of the straight line.
958
+ The end of the straight line.
871
959
 
872
960
  Returns
873
961
  -------
874
962
  :class:`VMobject`
875
963
  ``self``
876
964
  """
877
- nppcc = self.n_points_per_cubic_curve
878
965
  self.add_cubic_bezier_curve_to(
879
966
  *(
880
- interpolate(self.get_last_point(), point, a)
881
- for a in np.linspace(0, 1, nppcc)[1:]
967
+ interpolate(self.get_last_point(), point, t)
968
+ for t in self._bezier_t_values[1:]
882
969
  )
883
970
  )
884
971
  return self
885
972
 
886
- def add_smooth_curve_to(self, *points: Point3D) -> Self:
973
+ def add_smooth_curve_to(self, *points: Point3DLike) -> Self:
887
974
  """Creates a smooth curve from given points and add it to the VMobject. If two points are passed in, the first is interpreted
888
975
  as a handle, the second as an anchor.
889
976
 
@@ -942,16 +1029,58 @@ class VMobject(Mobject):
942
1029
  if not self.is_closed():
943
1030
  self.add_line_to(self.get_subpaths()[-1][0])
944
1031
 
945
- def add_points_as_corners(self, points: Iterable[Point3D]) -> Iterable[Point3D]:
946
- for point in points:
947
- self.add_line_to(point)
948
- return points
1032
+ def add_points_as_corners(self, points: Point3DLike_Array) -> Self:
1033
+ """Append multiple straight lines at the end of
1034
+ :attr:`VMobject.points`, which connect the given ``points`` in order
1035
+ starting from the end of the current path. These ``points`` would be
1036
+ therefore the corners of the new polyline appended to the path.
1037
+
1038
+ Parameters
1039
+ ----------
1040
+ points
1041
+ An array of 3D points representing the corners of the polyline to
1042
+ append to :attr:`VMobject.points`.
1043
+
1044
+ Returns
1045
+ -------
1046
+ :class:`VMobject`
1047
+ The VMobject itself, after appending the straight lines to its
1048
+ path.
1049
+ """
1050
+ points = np.asarray(points).reshape(-1, self.dim)
1051
+ num_points = points.shape[0]
1052
+ if num_points == 0:
1053
+ return self
1054
+
1055
+ if self.has_new_path_started():
1056
+ # Pop the last point from self.points and
1057
+ # add it to start_corners
1058
+ start_corners = np.empty((num_points, self.dim))
1059
+ start_corners[0] = self.points[-1]
1060
+ start_corners[1:] = points[:-1]
1061
+ end_corners = points
1062
+ self.points = self.points[:-1]
1063
+ else:
1064
+ start_corners = points[:-1]
1065
+ end_corners = points[1:]
1066
+
1067
+ nppcc = self.n_points_per_cubic_curve
1068
+ new_points = np.empty((nppcc * start_corners.shape[0], self.dim))
1069
+ new_points[::nppcc] = start_corners
1070
+ new_points[nppcc - 1 :: nppcc] = end_corners
1071
+ for i, t in enumerate(self._bezier_t_values):
1072
+ new_points[i::nppcc] = interpolate(start_corners, end_corners, t)
1073
+
1074
+ self.append_points(new_points)
1075
+ return self
949
1076
 
950
- def set_points_as_corners(self, points: Point3D_Array) -> Self:
951
- """Given an array of points, set them as corner of the vmobject.
1077
+ def set_points_as_corners(self, points: Point3DLike_Array) -> Self:
1078
+ """Given an array of points, set them as corners of the
1079
+ :class:`VMobject`.
952
1080
 
953
- To achieve that, this algorithm sets handles aligned with the anchors such that the resultant bezier curve will be the segment
954
- between the two anchors.
1081
+ To achieve that, this algorithm sets handles aligned with the anchors
1082
+ such that the resultant Bézier curve will be the segment between the
1083
+ two anchors.
955
1084
 
956
1085
  Parameters
957
1086
  ----------
@@ -961,7 +1090,7 @@ class VMobject(Mobject):
961
1090
  Returns
962
1091
  -------
963
1092
  :class:`VMobject`
964
- ``self``
1093
+ The VMobject itself, after setting the new points as corners.
965
1094
 
966
1095
 
967
1096
  Examples
@@ -984,16 +1113,15 @@ class VMobject(Mobject):
984
1113
  vmob.set_points_as_corners(corners).scale(2)
985
1114
  self.add(vmob)
986
1115
  """
987
- nppcc = self.n_points_per_cubic_curve
988
1116
  points = np.array(points)
989
1117
  # This will set the handles aligned with the anchors.
990
1118
  # Id est, a bezier curve will be the segment from the two anchors such that the handles belongs to this segment.
991
1119
  self.set_anchors_and_handles(
992
- *(interpolate(points[:-1], points[1:], a) for a in np.linspace(0, 1, nppcc))
1120
+ *(interpolate(points[:-1], points[1:], t) for t in self._bezier_t_values)
993
1121
  )
994
1122
  return self
995
1123
 
996
- def set_points_smoothly(self, points: Point3D_Array) -> Self:
1124
+ def set_points_smoothly(self, points: Point3DLike_Array) -> Self:
997
1125
  self.set_points_as_corners(points)
998
1126
  self.make_smooth()
999
1127
  return self
@@ -1019,7 +1147,7 @@ class VMobject(Mobject):
1019
1147
  # The append is needed as the last element is not reached when slicing with numpy.
1020
1148
  anchors = np.append(subpath[::nppcc], subpath[-1:], 0)
1021
1149
  if mode == "smooth":
1022
- h1, h2 = get_smooth_handle_points(anchors)
1150
+ h1, h2 = get_smooth_cubic_bezier_handle_points(anchors)
1023
1151
  else: # mode == "jagged"
1024
1152
  # The following will make the handles aligned with the anchors, thus making the bezier curve a segment
1025
1153
  a1 = anchors[:-1]
@@ -1038,19 +1166,17 @@ class VMobject(Mobject):
1038
1166
  def make_jagged(self) -> Self:
1039
1167
  return self.change_anchor_mode("jagged")
1040
1168
 
1041
- def add_subpath(self, points: Point3D_Array) -> Self:
1169
+ def add_subpath(self, points: CubicBezierPathLike) -> Self:
1042
1170
  assert len(points) % 4 == 0
1043
- self.points: Point3D_Array = np.append(self.points, points, axis=0)
1171
+ self.append_points(points)
1044
1172
  return self
1045
1173
 
1046
1174
  def append_vectorized_mobject(self, vectorized_mobject: VMobject) -> None:
1047
- new_points = list(vectorized_mobject.points)
1048
-
1049
1175
  if self.has_new_path_started():
1050
1176
  # Remove last point, which is starting
1051
1177
  # a new path
1052
1178
  self.points = self.points[:-1]
1053
- self.append_points(new_points)
1179
+ self.append_points(vectorized_mobject.points)
1054
1180
 
1055
1181
  def apply_function(self, function: MappingFunction) -> Self:
1056
1182
  factor = self.pre_function_handle_to_anchor_scale_factor
@@ -1065,7 +1191,7 @@ class VMobject(Mobject):
1065
1191
  self,
1066
1192
  angle: float,
1067
1193
  axis: Vector3D = OUT,
1068
- about_point: Point3D | None = None,
1194
+ about_point: Point3DLike | None = None,
1069
1195
  **kwargs,
1070
1196
  ) -> Self:
1071
1197
  self.rotate_sheen_direction(angle, axis)
@@ -1104,10 +1230,10 @@ class VMobject(Mobject):
1104
1230
  return self
1105
1231
 
1106
1232
  #
1107
- def consider_points_equals(self, p0: Point3D, p1: Point3D) -> bool:
1233
+ def consider_points_equals(self, p0: Point3DLike, p1: Point3DLike) -> bool:
1108
1234
  return np.allclose(p0, p1, atol=self.tolerance_for_point_equality)
1109
1235
 
1110
- def consider_points_equals_2d(self, p0: Point2D, p1: Point2D) -> bool:
1236
+ def consider_points_equals_2d(self, p0: Point2DLike, p1: Point2DLike) -> bool:
1111
1237
  """Determine if two points are close enough to be considered equal.
1112
1238
 
1113
1239
  This uses the algorithm from np.isclose(), but expanded here for the
@@ -1128,19 +1254,17 @@ class VMobject(Mobject):
1128
1254
  atol = self.tolerance_for_point_equality
1129
1255
  if abs(p0[0] - p1[0]) > atol + rtol * abs(p1[0]):
1130
1256
  return False
1131
- if abs(p0[1] - p1[1]) > atol + rtol * abs(p1[1]):
1132
- return False
1133
- return True
1257
+ return abs(p0[1] - p1[1]) <= atol + rtol * abs(p1[1])
1134
1258
 
1135
1259
  # Information about line
1136
1260
  def get_cubic_bezier_tuples_from_points(
1137
- self, points: Point3D_Array
1138
- ) -> npt.NDArray[Point3D_Array]:
1261
+ self, points: CubicBezierPathLike
1262
+ ) -> CubicBezierPoints_Array:
1139
1263
  return np.array(self.gen_cubic_bezier_tuples_from_points(points))
1140
1264
 
1141
1265
  def gen_cubic_bezier_tuples_from_points(
1142
- self, points: Point3D_Array
1143
- ) -> tuple[Point3D_Array]:
1266
+ self, points: CubicBezierPathLike
1267
+ ) -> tuple[CubicBezierPointsLike, ...]:
1144
1268
  """Returns the bezier tuples from an array of points.
1145
1269
 
1146
1270
  self.points is a list of the anchors and handles of the bezier curves of the mobject (ie [anchor1, handle1, handle2, anchor2, anchor3 ..])
@@ -1164,14 +1288,14 @@ class VMobject(Mobject):
1164
1288
  # Basically take every nppcc element.
1165
1289
  return tuple(points[i : i + nppcc] for i in range(0, len(points), nppcc))
1166
1290
 
1167
- def get_cubic_bezier_tuples(self) -> npt.NDArray[Point3D_Array]:
1291
+ def get_cubic_bezier_tuples(self) -> CubicBezierPoints_Array:
1168
1292
  return self.get_cubic_bezier_tuples_from_points(self.points)
1169
1293
 
1170
1294
  def _gen_subpaths_from_points(
1171
1295
  self,
1172
- points: Point3D_Array,
1296
+ points: CubicBezierPath,
1173
1297
  filter_func: Callable[[int], bool],
1174
- ) -> Generator[Point3D_Array]:
1298
+ ) -> Iterable[CubicSpline]:
1175
1299
  """Given an array of points defining the bezier curves of the vmobject, return subpaths formed by these points.
1176
1300
  Here, Two bezier curves form a path if at least two of their anchors are evaluated True by the relation defined by filter_func.
1177
1301
 
@@ -1189,7 +1313,7 @@ class VMobject(Mobject):
1189
1313
 
1190
1314
  Returns
1191
1315
  -------
1192
- Generator[Point3D_Array]
1316
+ Iterable[CubicSpline]
1193
1317
  subpaths formed by the points.
1194
1318
  """
1195
1319
  nppcc = self.n_points_per_cubic_curve
@@ -1201,7 +1325,7 @@ class VMobject(Mobject):
1201
1325
  if (i2 - i1) >= nppcc
1202
1326
  )
1203
1327
 
1204
- def get_subpaths_from_points(self, points: Point3D_Array) -> list[Point3D_Array]:
1328
+ def get_subpaths_from_points(self, points: CubicBezierPath) -> list[CubicSpline]:
1205
1329
  return list(
1206
1330
  self._gen_subpaths_from_points(
1207
1331
  points,
@@ -1210,26 +1334,26 @@ class VMobject(Mobject):
1210
1334
  )
1211
1335
 
1212
1336
  def gen_subpaths_from_points_2d(
1213
- self, points: Point3D_Array
1214
- ) -> Generator[Point3D_Array]:
1337
+ self, points: CubicBezierPath
1338
+ ) -> Iterable[CubicSpline]:
1215
1339
  return self._gen_subpaths_from_points(
1216
1340
  points,
1217
1341
  lambda n: not self.consider_points_equals_2d(points[n - 1], points[n]),
1218
1342
  )
1219
1343
 
1220
- def get_subpaths(self) -> list[Point3D_Array]:
1344
+ def get_subpaths(self) -> list[CubicSpline]:
1221
1345
  """Returns subpaths formed by the curves of the VMobject.
1222
1346
 
1223
1347
  Subpaths are ranges of curves with each pair of consecutive curves having their end/start points coincident.
1224
1348
 
1225
1349
  Returns
1226
1350
  -------
1227
- list[Point3D_Array]
1351
+ list[CubicSpline]
1228
1352
  subpaths.
1229
1353
  """
1230
1354
  return self.get_subpaths_from_points(self.points)
1231
1355
 
1232
- def get_nth_curve_points(self, n: int) -> Point3D_Array:
1356
+ def get_nth_curve_points(self, n: int) -> CubicBezierPoints:
1233
1357
  """Returns the points defining the nth curve of the vmobject.
1234
1358
 
1235
1359
  Parameters
@@ -1239,7 +1363,7 @@ class VMobject(Mobject):
1239
1363
 
1240
1364
  Returns
1241
1365
  -------
1242
- Point3D_Array
1366
+ CubicBezierPoints
1243
1367
  points defining the nth bezier curve (anchors, handles)
1244
1368
  """
1245
1369
  assert n < self.get_num_curves()
@@ -1308,7 +1432,6 @@ class VMobject(Mobject):
1308
1432
  length : :class:`float`
1309
1433
  The length of the nth curve.
1310
1434
  """
1311
-
1312
1435
  _, length = self.get_nth_curve_function_with_length(n, sample_points)
1313
1436
 
1314
1437
  return length
@@ -1334,7 +1457,6 @@ class VMobject(Mobject):
1334
1457
  length : :class:`float`
1335
1458
  The length of the nth curve.
1336
1459
  """
1337
-
1338
1460
  curve = self.get_nth_curve_function(n)
1339
1461
  norms = self.get_nth_curve_length_pieces(n, sample_points=sample_points)
1340
1462
  length = np.sum(norms)
@@ -1354,15 +1476,14 @@ class VMobject(Mobject):
1354
1476
 
1355
1477
  def get_curve_functions(
1356
1478
  self,
1357
- ) -> Generator[Callable[[float], Point3D]]:
1479
+ ) -> Iterable[Callable[[float], Point3D]]:
1358
1480
  """Gets the functions for the curves of the mobject.
1359
1481
 
1360
1482
  Returns
1361
1483
  -------
1362
- Generator[Callable[[float], Point3D]]
1484
+ Iterable[Callable[[float], Point3D]]
1363
1485
  The functions for the curves.
1364
1486
  """
1365
-
1366
1487
  num_curves = self.get_num_curves()
1367
1488
 
1368
1489
  for n in range(num_curves):
@@ -1370,7 +1491,7 @@ class VMobject(Mobject):
1370
1491
 
1371
1492
  def get_curve_functions_with_lengths(
1372
1493
  self, **kwargs
1373
- ) -> Generator[tuple[Callable[[float], Point3D], float]]:
1494
+ ) -> Iterable[tuple[Callable[[float], Point3D], float]]:
1374
1495
  """Gets the functions and lengths of the curves for the mobject.
1375
1496
 
1376
1497
  Parameters
@@ -1380,10 +1501,9 @@ class VMobject(Mobject):
1380
1501
 
1381
1502
  Returns
1382
1503
  -------
1383
- Generator[tuple[Callable[[float], Point3D], float]]
1504
+ Iterable[tuple[Callable[[float], Point3D], float]]
1384
1505
  The functions and lengths of the curves.
1385
1506
  """
1386
-
1387
1507
  num_curves = self.get_num_curves()
1388
1508
 
1389
1509
  for n in range(num_curves):
@@ -1425,7 +1545,6 @@ class VMobject(Mobject):
1425
1545
  line.point_from_proportion(proportion)
1426
1546
  ))
1427
1547
  """
1428
-
1429
1548
  if alpha < 0 or alpha > 1:
1430
1549
  raise ValueError(f"Alpha {alpha} not between 0 and 1.")
1431
1550
 
@@ -1454,7 +1573,7 @@ class VMobject(Mobject):
1454
1573
 
1455
1574
  def proportion_from_point(
1456
1575
  self,
1457
- point: Iterable[float | int],
1576
+ point: Point3DLike,
1458
1577
  ) -> float:
1459
1578
  """Returns the proportion along the path of the :class:`VMobject`
1460
1579
  a particular given point is at.
@@ -1543,7 +1662,7 @@ class VMobject(Mobject):
1543
1662
  nppcc = self.n_points_per_cubic_curve
1544
1663
  return self.points[nppcc - 1 :: nppcc]
1545
1664
 
1546
- def get_anchors(self) -> Point3D_Array:
1665
+ def get_anchors(self) -> list[Point3D]:
1547
1666
  """Returns the anchors of the curves forming the VMobject.
1548
1667
 
1549
1668
  Returns
@@ -1577,7 +1696,6 @@ class VMobject(Mobject):
1577
1696
  float
1578
1697
  The length of the :class:`VMobject`.
1579
1698
  """
1580
-
1581
1699
  return sum(
1582
1700
  length
1583
1701
  for _, length in self.get_curve_functions_with_lengths(
@@ -1682,8 +1800,8 @@ class VMobject(Mobject):
1682
1800
  return self
1683
1801
 
1684
1802
  def insert_n_curves_to_point_list(
1685
- self, n: int, points: Point3D_Array
1686
- ) -> npt.NDArray[BezierPoints]:
1803
+ self, n: int, points: BezierPathLike
1804
+ ) -> BezierPath:
1687
1805
  """Given an array of k points defining a bezier curves (anchors and handles), returns points defining exactly k + n bezier curves.
1688
1806
 
1689
1807
  Parameters
@@ -1697,44 +1815,14 @@ class VMobject(Mobject):
1697
1815
  -------
1698
1816
  Points generated.
1699
1817
  """
1700
-
1701
1818
  if len(points) == 1:
1702
1819
  nppcc = self.n_points_per_cubic_curve
1703
1820
  return np.repeat(points, nppcc * n, 0)
1704
- bezier_quads = self.get_cubic_bezier_tuples_from_points(points)
1705
- curr_num = len(bezier_quads)
1706
- target_num = curr_num + n
1707
- # This is an array with values ranging from 0
1708
- # up to curr_num, with repeats such that
1709
- # it's total length is target_num. For example,
1710
- # with curr_num = 10, target_num = 15, this would
1711
- # be [0, 0, 1, 2, 2, 3, 4, 4, 5, 6, 6, 7, 8, 8, 9]
1712
- repeat_indices = (np.arange(target_num, dtype="i") * curr_num) // target_num
1713
-
1714
- # If the nth term of this list is k, it means
1715
- # that the nth curve of our path should be split
1716
- # into k pieces.
1717
- # In the above example our array had the following elements
1718
- # [0, 0, 1, 2, 2, 3, 4, 4, 5, 6, 6, 7, 8, 8, 9]
1719
- # We have two 0s, one 1, two 2s and so on.
1720
- # The split factors array would hence be:
1721
- # [2, 1, 2, 1, 2, 1, 2, 1, 2, 1]
1722
- split_factors = np.zeros(curr_num, dtype="i")
1723
- for val in repeat_indices:
1724
- split_factors[val] += 1
1725
-
1726
- new_points = np.zeros((0, self.dim))
1727
- for quad, sf in zip(bezier_quads, split_factors):
1728
- # What was once a single cubic curve defined
1729
- # by "quad" will now be broken into sf
1730
- # smaller cubic curves
1731
- alphas = np.linspace(0, 1, sf + 1)
1732
- for a1, a2 in zip(alphas, alphas[1:]):
1733
- new_points = np.append(
1734
- new_points,
1735
- partial_bezier_points(quad, a1, a2),
1736
- axis=0,
1737
- )
1821
+ bezier_tuples = self.get_cubic_bezier_tuples_from_points(points)
1822
+ current_number_of_curves = len(bezier_tuples)
1823
+ new_number_of_curves = current_number_of_curves + n
1824
+ new_bezier_tuples = bezier_remap(bezier_tuples, new_number_of_curves)
1825
+ new_points = new_bezier_tuples.reshape(-1, 3)
1738
1826
  return new_points
1739
1827
 
1740
1828
  def align_rgbas(self, vmobject: VMobject) -> Self:
@@ -1750,7 +1838,7 @@ class VMobject(Mobject):
1750
1838
  setattr(self, attr, new_a1)
1751
1839
  return self
1752
1840
 
1753
- def get_point_mobject(self, center: Point3D | None = None) -> VectorizedPoint:
1841
+ def get_point_mobject(self, center: Point3DLike | None = None) -> VectorizedPoint:
1754
1842
  if center is None:
1755
1843
  center = self.get_center()
1756
1844
  point = VectorizedPoint(center)
@@ -1787,60 +1875,91 @@ class VMobject(Mobject):
1787
1875
  a: float,
1788
1876
  b: float,
1789
1877
  ) -> Self:
1790
- """Given two bounds a and b, transforms the points of the self vmobject into the points of the vmobject
1791
- passed as parameter with respect to the bounds. Points here stand for control points of the bezier curves (anchors and handles)
1878
+ """Given a 2nd :class:`.VMobject` ``vmobject``, a lower bound ``a`` and
1879
+ an upper bound ``b``, modify this :class:`.VMobject`'s points to
1880
+ match the portion of the Bézier spline described by ``vmobject.points``
1881
+ with the parameter ``t`` between ``a`` and ``b``.
1792
1882
 
1793
1883
  Parameters
1794
1884
  ----------
1795
1885
  vmobject
1796
- The vmobject that will serve as a model.
1886
+ The :class:`.VMobject` that will serve as a model.
1797
1887
  a
1798
- upper-bound.
1888
+ The lower bound for ``t``.
1799
1889
  b
1800
- lower-bound
1890
+ The upper bound for ``t``
1801
1891
 
1802
1892
  Returns
1803
1893
  -------
1804
- :class:`VMobject`
1805
- ``self``
1894
+ :class:`.VMobject`
1895
+ The :class:`.VMobject` itself, after the transformation.
1896
+
1897
+ Raises
1898
+ ------
1899
+ TypeError
1900
+ If ``vmobject`` is not an instance of :class:`VMobject`.
1806
1901
  """
1807
- assert isinstance(vmobject, VMobject)
1902
+ if not isinstance(vmobject, VMobject):
1903
+ raise TypeError(
1904
+ f"Expected a VMobject, got value {vmobject} of type "
1905
+ f"{type(vmobject).__name__}."
1906
+ )
1808
1907
  # Partial curve includes three portions:
1809
- # - A middle section, which matches the curve exactly
1810
- # - A start, which is some ending portion of an inner cubic
1811
- # - An end, which is the starting portion of a later inner cubic
1908
+ # - A middle section, which matches the curve exactly.
1909
+ # - A start, which is some ending portion of an inner cubic.
1910
+ # - An end, which is the starting portion of a later inner cubic.
1812
1911
  if a <= 0 and b >= 1:
1813
1912
  self.set_points(vmobject.points)
1814
1913
  return self
1815
- bezier_quads = vmobject.get_cubic_bezier_tuples()
1816
- num_cubics = len(bezier_quads)
1817
-
1818
- # The following two lines will compute which bezier curves of the given mobject need to be processed.
1819
- # The residue basically indicates de proportion of the selected bezier curve that have to be selected.
1820
- # Ex : if lower_index is 3, and lower_residue is 0.4, then the algorithm will append to the points 0.4 of the third bezier curve
1821
- lower_index, lower_residue = integer_interpolate(0, num_cubics, a)
1822
- upper_index, upper_residue = integer_interpolate(0, num_cubics, b)
1823
-
1824
- self.clear_points()
1825
- if num_cubics == 0:
1914
+ num_curves = vmobject.get_num_curves()
1915
+ if num_curves == 0:
1916
+ self.clear_points()
1826
1917
  return self
1918
+
1919
+ # The following two lines will compute which Bézier curves of the given Mobject must be processed.
1920
+ # The residue indicates the proportion of the selected Bézier curve which must be selected.
1921
+ #
1922
+ # Example: if num_curves is 10, a is 0.34 and b is 0.78, then:
1923
+ # - lower_index is 3 and lower_residue is 0.4, which means the algorithm will look at the 3rd Bézier
1924
+ # and select its part which ranges from t=0.4 to t=1.
1925
+ # - upper_index is 7 and upper_residue is 0.8, which means the algorithm will look at the 7th Bézier
1926
+ # and select its part which ranges from t=0 to t=0.8.
1927
+ lower_index, lower_residue = integer_interpolate(0, num_curves, a)
1928
+ upper_index, upper_residue = integer_interpolate(0, num_curves, b)
1929
+
1930
+ nppc = self.n_points_per_curve
1931
+ # If both indices coincide, get a part of a single Bézier curve.
1827
1932
  if lower_index == upper_index:
1828
- self.append_points(
1829
- partial_bezier_points(
1830
- bezier_quads[lower_index],
1831
- lower_residue,
1832
- upper_residue,
1833
- ),
1933
+ # Look at the "lower_index"-th Bézier curve and select its part from
1934
+ # t=lower_residue to t=upper_residue.
1935
+ self.points = partial_bezier_points(
1936
+ vmobject.points[nppc * lower_index : nppc * (lower_index + 1)],
1937
+ lower_residue,
1938
+ upper_residue,
1834
1939
  )
1835
1940
  else:
1836
- self.append_points(
1837
- partial_bezier_points(bezier_quads[lower_index], lower_residue, 1),
1941
+ # Allocate space for (upper_index-lower_index+1) Bézier curves.
1942
+ self.points = np.empty((nppc * (upper_index - lower_index + 1), self.dim))
1943
+ # Look at the "lower_index"-th Bezier curve and select its part from
1944
+ # t=lower_residue to t=1. This is the first curve in self.points.
1945
+ self.points[:nppc] = partial_bezier_points(
1946
+ vmobject.points[nppc * lower_index : nppc * (lower_index + 1)],
1947
+ lower_residue,
1948
+ 1,
1838
1949
  )
1839
- for quad in bezier_quads[lower_index + 1 : upper_index]:
1840
- self.append_points(quad)
1841
- self.append_points(
1842
- partial_bezier_points(bezier_quads[upper_index], 0, upper_residue),
1950
+ # If there are more curves between the "lower_index"-th and the
1951
+ # "upper_index"-th Béziers, add them all to self.points.
1952
+ self.points[nppc:-nppc] = vmobject.points[
1953
+ nppc * (lower_index + 1) : nppc * upper_index
1954
+ ]
1955
+ # Look at the "upper_index"-th Bézier curve and select its part from
1956
+ # t=0 to t=upper_residue. This is the last curve in self.points.
1957
+ self.points[-nppc:] = partial_bezier_points(
1958
+ vmobject.points[nppc * upper_index : nppc * (upper_index + 1)],
1959
+ 0,
1960
+ upper_residue,
1843
1961
  )
1962
+
1844
1963
  return self
1845
1964
 
1846
1965
  def get_subcurve(self, a: float, b: float) -> Self:
@@ -1959,19 +2078,21 @@ class VGroup(VMobject, metaclass=ConvertToOpenGL):
1959
2078
  >>> triangle, square = Triangle(), Square()
1960
2079
  >>> vg.add(triangle)
1961
2080
  VGroup(Triangle)
1962
- >>> vg + square # a new VGroup is constructed
2081
+ >>> vg + square # a new VGroup is constructed
1963
2082
  VGroup(Triangle, Square)
1964
- >>> vg # not modified
2083
+ >>> vg # not modified
1965
2084
  VGroup(Triangle)
1966
- >>> vg += square; vg # modifies vg
2085
+ >>> vg += square
2086
+ >>> vg # modifies vg
1967
2087
  VGroup(Triangle, Square)
1968
2088
  >>> vg.remove(triangle)
1969
2089
  VGroup(Square)
1970
- >>> vg - square; # a new VGroup is constructed
2090
+ >>> vg - square # a new VGroup is constructed
1971
2091
  VGroup()
1972
- >>> vg # not modified
2092
+ >>> vg # not modified
1973
2093
  VGroup(Square)
1974
- >>> vg -= square; vg # modifies vg
2094
+ >>> vg -= square
2095
+ >>> vg # modifies vg
1975
2096
  VGroup()
1976
2097
 
1977
2098
  .. manim:: ArcShapeIris
@@ -1991,12 +2112,14 @@ class VGroup(VMobject, metaclass=ConvertToOpenGL):
1991
2112
 
1992
2113
  """
1993
2114
 
1994
- def __init__(self, *vmobjects, **kwargs):
2115
+ def __init__(
2116
+ self, *vmobjects: VMobject | Iterable[VMobject], **kwargs: Any
2117
+ ) -> None:
1995
2118
  super().__init__(**kwargs)
1996
2119
  self.add(*vmobjects)
1997
2120
 
1998
2121
  def __repr__(self) -> str:
1999
- return f'{self.__class__.__name__}({", ".join(str(mob) for mob in self.submobjects)})'
2122
+ return f"{self.__class__.__name__}({', '.join(str(mob) for mob in self.submobjects)})"
2000
2123
 
2001
2124
  def __str__(self) -> str:
2002
2125
  return (
@@ -2004,13 +2127,16 @@ class VGroup(VMobject, metaclass=ConvertToOpenGL):
2004
2127
  f"submobject{'s' if len(self.submobjects) > 0 else ''}"
2005
2128
  )
2006
2129
 
2007
- def add(self, *vmobjects: VMobject) -> Self:
2008
- """Checks if all passed elements are an instance of VMobject and then add them to submobjects
2130
+ def add(
2131
+ self,
2132
+ *vmobjects: VMobject | Iterable[VMobject],
2133
+ ) -> Self:
2134
+ """Checks if all passed elements are an instance, or iterables of VMobject and then adds them to submobjects
2009
2135
 
2010
2136
  Parameters
2011
2137
  ----------
2012
2138
  vmobjects
2013
- List of VMobject to add
2139
+ List or iterable of VMobjects to add
2014
2140
 
2015
2141
  Returns
2016
2142
  -------
@@ -2019,10 +2145,13 @@ class VGroup(VMobject, metaclass=ConvertToOpenGL):
2019
2145
  Raises
2020
2146
  ------
2021
2147
  TypeError
2022
- If one element of the list is not an instance of VMobject
2148
+ If one element of the list, or iterable is not an instance of VMobject
2023
2149
 
2024
2150
  Examples
2025
2151
  --------
2152
+ The following example shows how to add individual or multiple `VMobject` instances through the `VGroup`
2153
+ constructor and its `.add()` method.
2154
+
2026
2155
  .. manim:: AddToVGroup
2027
2156
 
2028
2157
  class AddToVGroup(Scene):
@@ -2051,16 +2180,65 @@ class VGroup(VMobject, metaclass=ConvertToOpenGL):
2051
2180
  self.play( # Animate group without component
2052
2181
  (gr-circle_red).animate.shift(RIGHT)
2053
2182
  )
2183
+
2184
+ A `VGroup` can be created using iterables as well. Keep in mind that all generated values from an
2185
+ iterable must be an instance of `VMobject`. This is demonstrated below:
2186
+
2187
+ .. manim:: AddIterableToVGroupExample
2188
+ :save_last_frame:
2189
+
2190
+ class AddIterableToVGroupExample(Scene):
2191
+ def construct(self):
2192
+ v = VGroup(
2193
+ Square(), # Singular VMobject instance
2194
+ [Circle(), Triangle()], # List of VMobject instances
2195
+ Dot(),
2196
+ (Dot() for _ in range(2)), # Iterable that generates VMobjects
2197
+ )
2198
+ v.arrange()
2199
+ self.add(v)
2200
+
2201
+ To facilitate this, the iterable is unpacked before its individual instances are added to the `VGroup`.
2202
+ As a result, when you index a `VGroup`, you will never get back an iterable.
2203
+ Instead, you will always receive `VMobject` instances, including those
2204
+ that were part of the iterable/s that you originally added to the `VGroup`.
2054
2205
  """
2055
- for m in vmobjects:
2056
- if not isinstance(m, (VMobject, OpenGLVMobject)):
2206
+
2207
+ def get_type_error_message(invalid_obj, invalid_indices):
2208
+ return (
2209
+ f"Only values of type {vmobject_render_type.__name__} can be added "
2210
+ "as submobjects of VGroup, but the value "
2211
+ f"{repr(invalid_obj)} (at index {invalid_indices[1]} of "
2212
+ f"parameter {invalid_indices[0]}) is of type "
2213
+ f"{type(invalid_obj).__name__}."
2214
+ )
2215
+
2216
+ vmobject_render_type = (
2217
+ OpenGLVMobject if config.renderer == RendererType.OPENGL else VMobject
2218
+ )
2219
+ valid_vmobjects = []
2220
+
2221
+ for i, vmobject in enumerate(vmobjects):
2222
+ if isinstance(vmobject, vmobject_render_type):
2223
+ valid_vmobjects.append(vmobject)
2224
+ elif isinstance(vmobject, Iterable) and not isinstance(
2225
+ vmobject, (Mobject, OpenGLMobject)
2226
+ ):
2227
+ for j, subvmobject in enumerate(vmobject):
2228
+ if not isinstance(subvmobject, vmobject_render_type):
2229
+ raise TypeError(get_type_error_message(subvmobject, (i, j)))
2230
+ valid_vmobjects.append(subvmobject)
2231
+ elif isinstance(vmobject, Iterable) and isinstance(
2232
+ vmobject, (Mobject, OpenGLMobject)
2233
+ ):
2057
2234
  raise TypeError(
2058
- f"All submobjects of {self.__class__.__name__} must be of type VMobject. "
2059
- f"Got {repr(m)} ({type(m).__name__}) instead. "
2060
- "You can try using `Group` instead."
2235
+ f"{get_type_error_message(vmobject, (i, 0))} "
2236
+ "You can try adding this value into a Group instead."
2061
2237
  )
2238
+ else:
2239
+ raise TypeError(get_type_error_message(vmobject, (i, 0)))
2062
2240
 
2063
- return super().add(*vmobjects)
2241
+ return super().add(*valid_vmobjects)
2064
2242
 
2065
2243
  def __add__(self, vmobject: VMobject) -> Self:
2066
2244
  return VGroup(*self.submobjects, vmobject)
@@ -2097,8 +2275,7 @@ class VGroup(VMobject, metaclass=ConvertToOpenGL):
2097
2275
  >>> new_obj = VMobject()
2098
2276
  >>> vgroup[0] = new_obj
2099
2277
  """
2100
- if not all(isinstance(m, (VMobject, OpenGLVMobject)) for m in value):
2101
- raise TypeError("All submobjects must be of type VMobject")
2278
+ self._assert_valid_submobjects(tuplify(value))
2102
2279
  self.submobjects[key] = value
2103
2280
 
2104
2281
 
@@ -2242,7 +2419,7 @@ class VDict(VMobject, metaclass=ConvertToOpenGL):
2242
2419
  Normal usage::
2243
2420
 
2244
2421
  square_obj = Square()
2245
- my_dict.add([('s', square_obj)])
2422
+ my_dict.add([("s", square_obj)])
2246
2423
  """
2247
2424
  for key, value in dict(mapping_or_iterable).items():
2248
2425
  self.add_key_value_pair(key, value)
@@ -2269,10 +2446,10 @@ class VDict(VMobject, metaclass=ConvertToOpenGL):
2269
2446
  --------
2270
2447
  Normal usage::
2271
2448
 
2272
- my_dict.remove('square')
2449
+ my_dict.remove("square")
2273
2450
  """
2274
2451
  if key not in self.submob_dict:
2275
- raise KeyError("The given key '%s' is not present in the VDict" % str(key))
2452
+ raise KeyError(f"The given key '{key!s}' is not present in the VDict")
2276
2453
  super().remove(self.submob_dict[key])
2277
2454
  del self.submob_dict[key]
2278
2455
  return self
@@ -2294,7 +2471,7 @@ class VDict(VMobject, metaclass=ConvertToOpenGL):
2294
2471
  --------
2295
2472
  Normal usage::
2296
2473
 
2297
- self.play(Create(my_dict['s']))
2474
+ self.play(Create(my_dict["s"]))
2298
2475
  """
2299
2476
  submob = self.submob_dict[key]
2300
2477
  return submob
@@ -2318,7 +2495,7 @@ class VDict(VMobject, metaclass=ConvertToOpenGL):
2318
2495
  Normal usage::
2319
2496
 
2320
2497
  square_obj = Square()
2321
- my_dict['sq'] = square_obj
2498
+ my_dict["sq"] = square_obj
2322
2499
  """
2323
2500
  if key in self.submob_dict:
2324
2501
  self.remove(key)
@@ -2423,11 +2600,10 @@ class VDict(VMobject, metaclass=ConvertToOpenGL):
2423
2600
  Normal usage::
2424
2601
 
2425
2602
  square_obj = Square()
2426
- self.add_key_value_pair('s', square_obj)
2603
+ self.add_key_value_pair("s", square_obj)
2427
2604
 
2428
2605
  """
2429
- if not isinstance(value, (VMobject, OpenGLVMobject)):
2430
- raise TypeError("All submobjects must be of type VMobject")
2606
+ self._assert_valid_submobjects([value])
2431
2607
  mob = value
2432
2608
  if self.show_keys:
2433
2609
  # This import is here and not at the top to avoid circular import
@@ -2443,7 +2619,7 @@ class VDict(VMobject, metaclass=ConvertToOpenGL):
2443
2619
  class VectorizedPoint(VMobject, metaclass=ConvertToOpenGL):
2444
2620
  def __init__(
2445
2621
  self,
2446
- location: Point3D = ORIGIN,
2622
+ location: Point3DLike = ORIGIN,
2447
2623
  color: ManimColor = BLACK,
2448
2624
  fill_opacity: float = 0,
2449
2625
  stroke_width: float = 0,
@@ -2643,15 +2819,12 @@ class DashedVMobject(VMobject, metaclass=ConvertToOpenGL):
2643
2819
  if vmobject.is_closed():
2644
2820
  void_len = (1 - r) / n
2645
2821
  else:
2646
- if n == 1:
2647
- void_len = 1 - r
2648
- else:
2649
- void_len = (1 - r) / (n - 1)
2822
+ void_len = 1 - r if n == 1 else (1 - r) / (n - 1)
2650
2823
 
2651
2824
  period = dash_len + void_len
2652
2825
  phase_shift = (dash_offset % 1) * period
2653
2826
 
2654
- if vmobject.is_closed():
2827
+ if vmobject.is_closed(): # noqa: SIM108
2655
2828
  # closed curves have equal amount of dashes and voids
2656
2829
  pattern_len = 1
2657
2830
  else: