manim 0.18.0.post0__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 (146) hide show
  1. manim/__init__.py +3 -6
  2. manim/__main__.py +61 -20
  3. manim/_config/__init__.py +6 -3
  4. manim/_config/cli_colors.py +16 -8
  5. manim/_config/default.cfg +1 -3
  6. manim/_config/logger_utils.py +14 -8
  7. manim/_config/utils.py +651 -472
  8. manim/animation/animation.py +152 -5
  9. manim/animation/composition.py +80 -39
  10. manim/animation/creation.py +196 -14
  11. manim/animation/fading.py +5 -9
  12. manim/animation/indication.py +103 -47
  13. manim/animation/movement.py +22 -5
  14. manim/animation/rotation.py +3 -2
  15. manim/animation/specialized.py +4 -6
  16. manim/animation/speedmodifier.py +10 -5
  17. manim/animation/transform.py +4 -5
  18. manim/animation/transform_matching_parts.py +1 -1
  19. manim/animation/updaters/mobject_update_utils.py +17 -14
  20. manim/camera/camera.py +15 -6
  21. manim/cli/__init__.py +17 -0
  22. manim/cli/cfg/group.py +70 -44
  23. manim/cli/checkhealth/checks.py +93 -75
  24. manim/cli/checkhealth/commands.py +14 -5
  25. manim/cli/default_group.py +157 -25
  26. manim/cli/init/commands.py +32 -24
  27. manim/cli/plugins/commands.py +16 -3
  28. manim/cli/render/commands.py +72 -60
  29. manim/cli/render/ease_of_access_options.py +4 -3
  30. manim/cli/render/global_options.py +51 -15
  31. manim/cli/render/output_options.py +6 -5
  32. manim/cli/render/render_options.py +97 -32
  33. manim/constants.py +65 -19
  34. manim/gui/gui.py +2 -0
  35. manim/mobject/frame.py +0 -1
  36. manim/mobject/geometry/arc.py +112 -78
  37. manim/mobject/geometry/boolean_ops.py +32 -25
  38. manim/mobject/geometry/labeled.py +300 -77
  39. manim/mobject/geometry/line.py +132 -64
  40. manim/mobject/geometry/polygram.py +126 -30
  41. manim/mobject/geometry/shape_matchers.py +35 -15
  42. manim/mobject/geometry/tips.py +38 -29
  43. manim/mobject/graph.py +414 -133
  44. manim/mobject/graphing/coordinate_systems.py +126 -64
  45. manim/mobject/graphing/functions.py +25 -15
  46. manim/mobject/graphing/number_line.py +24 -10
  47. manim/mobject/graphing/probability.py +2 -10
  48. manim/mobject/graphing/scale.py +6 -5
  49. manim/mobject/matrix.py +17 -19
  50. manim/mobject/mobject.py +314 -165
  51. manim/mobject/opengl/opengl_compatibility.py +2 -0
  52. manim/mobject/opengl/opengl_geometry.py +30 -9
  53. manim/mobject/opengl/opengl_image_mobject.py +2 -0
  54. manim/mobject/opengl/opengl_mobject.py +509 -343
  55. manim/mobject/opengl/opengl_point_cloud_mobject.py +5 -7
  56. manim/mobject/opengl/opengl_surface.py +3 -2
  57. manim/mobject/opengl/opengl_three_dimensions.py +2 -0
  58. manim/mobject/opengl/opengl_vectorized_mobject.py +46 -79
  59. manim/mobject/svg/brace.py +63 -13
  60. manim/mobject/svg/svg_mobject.py +4 -3
  61. manim/mobject/table.py +11 -13
  62. manim/mobject/text/code_mobject.py +186 -548
  63. manim/mobject/text/numbers.py +9 -7
  64. manim/mobject/text/tex_mobject.py +23 -14
  65. manim/mobject/text/text_mobject.py +70 -24
  66. manim/mobject/three_d/polyhedra.py +98 -1
  67. manim/mobject/three_d/three_d_utils.py +4 -4
  68. manim/mobject/three_d/three_dimensions.py +62 -34
  69. manim/mobject/types/image_mobject.py +42 -24
  70. manim/mobject/types/point_cloud_mobject.py +105 -67
  71. manim/mobject/types/vectorized_mobject.py +496 -228
  72. manim/mobject/value_tracker.py +5 -4
  73. manim/mobject/vector_field.py +5 -5
  74. manim/opengl/__init__.py +3 -3
  75. manim/plugins/__init__.py +14 -1
  76. manim/plugins/plugins_flags.py +14 -8
  77. manim/renderer/cairo_renderer.py +20 -10
  78. manim/renderer/opengl_renderer.py +21 -23
  79. manim/renderer/opengl_renderer_window.py +2 -0
  80. manim/renderer/shader.py +2 -3
  81. manim/renderer/shader_wrapper.py +5 -2
  82. manim/renderer/vectorized_mobject_rendering.py +5 -0
  83. manim/scene/moving_camera_scene.py +23 -0
  84. manim/scene/scene.py +90 -43
  85. manim/scene/scene_file_writer.py +316 -165
  86. manim/scene/section.py +17 -15
  87. manim/scene/three_d_scene.py +13 -21
  88. manim/scene/vector_space_scene.py +22 -9
  89. manim/typing.py +830 -70
  90. manim/utils/bezier.py +1667 -399
  91. manim/utils/caching.py +13 -5
  92. manim/utils/color/AS2700.py +2 -0
  93. manim/utils/color/BS381.py +3 -0
  94. manim/utils/color/DVIPSNAMES.py +96 -0
  95. manim/utils/color/SVGNAMES.py +179 -0
  96. manim/utils/color/X11.py +3 -0
  97. manim/utils/color/XKCD.py +3 -0
  98. manim/utils/color/__init__.py +8 -5
  99. manim/utils/color/core.py +844 -309
  100. manim/utils/color/manim_colors.py +7 -9
  101. manim/utils/commands.py +48 -20
  102. manim/utils/config_ops.py +18 -13
  103. manim/utils/debug.py +8 -7
  104. manim/utils/deprecation.py +90 -40
  105. manim/utils/docbuild/__init__.py +17 -0
  106. manim/utils/docbuild/autoaliasattr_directive.py +234 -0
  107. manim/utils/docbuild/autocolor_directive.py +21 -17
  108. manim/utils/docbuild/manim_directive.py +50 -35
  109. manim/utils/docbuild/module_parsing.py +245 -0
  110. manim/utils/exceptions.py +6 -0
  111. manim/utils/family.py +5 -3
  112. manim/utils/family_ops.py +17 -4
  113. manim/utils/file_ops.py +26 -16
  114. manim/utils/hashing.py +9 -7
  115. manim/utils/images.py +10 -4
  116. manim/utils/ipython_magic.py +14 -8
  117. manim/utils/iterables.py +161 -119
  118. manim/utils/module_ops.py +57 -19
  119. manim/utils/opengl.py +83 -24
  120. manim/utils/parameter_parsing.py +32 -0
  121. manim/utils/paths.py +21 -23
  122. manim/utils/polylabel.py +168 -0
  123. manim/utils/qhull.py +218 -0
  124. manim/utils/rate_functions.py +74 -39
  125. manim/utils/simple_functions.py +24 -15
  126. manim/utils/sounds.py +7 -1
  127. manim/utils/space_ops.py +125 -69
  128. manim/utils/testing/__init__.py +17 -0
  129. manim/utils/testing/_frames_testers.py +13 -8
  130. manim/utils/testing/_show_diff.py +5 -3
  131. manim/utils/testing/_test_class_makers.py +33 -18
  132. manim/utils/testing/frames_comparison.py +27 -19
  133. manim/utils/tex.py +127 -197
  134. manim/utils/tex_file_writing.py +47 -45
  135. manim/utils/tex_templates.py +2 -1
  136. manim/utils/unit.py +6 -5
  137. {manim-0.18.0.post0.dist-info → manim-0.19.0.dist-info}/LICENSE.community +1 -1
  138. {manim-0.18.0.post0.dist-info → manim-0.19.0.dist-info}/METADATA +40 -39
  139. manim-0.19.0.dist-info/RECORD +221 -0
  140. {manim-0.18.0.post0.dist-info → manim-0.19.0.dist-info}/WHEEL +1 -1
  141. manim/cli/new/__init__.py +0 -0
  142. manim/cli/new/group.py +0 -189
  143. manim/plugins/import_plugins.py +0 -43
  144. manim-0.18.0.post0.dist-info/RECORD +0 -217
  145. {manim-0.18.0.post0.dist-info → manim-0.19.0.dist-info}/LICENSE +0 -0
  146. {manim-0.18.0.post0.dist-info → manim-0.19.0.dist-info}/entry_points.txt +0 -0
@@ -14,55 +14,58 @@ __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
- import numpy.typing as npt
30
21
  from PIL.Image import Image
31
- from typing_extensions import Self
32
22
 
23
+ from manim import config
24
+ from manim.constants import *
25
+ from manim.mobject.mobject import Mobject
33
26
  from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
27
+ from manim.mobject.opengl.opengl_mobject import OpenGLMobject
34
28
  from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject
35
29
  from manim.mobject.three_d.three_d_utils import (
36
30
  get_3d_vmob_gradient_start_and_end_points,
37
31
  )
38
-
39
- from ... import config
40
- from ...constants import *
41
- from ...mobject.mobject import Mobject
42
- from ...utils.bezier import (
32
+ from manim.utils.bezier import (
43
33
  bezier,
44
- get_smooth_handle_points,
34
+ bezier_remap,
35
+ get_smooth_cubic_bezier_handle_points,
45
36
  integer_interpolate,
46
37
  interpolate,
47
38
  partial_bezier_points,
48
39
  proportions_along_bezier_curve_for_point,
49
40
  )
50
- from ...utils.color import BLACK, WHITE, ManimColor, ParsableManimColor
51
- from ...utils.iterables import make_even, resize_array, stretch_array_to_length, tuplify
52
- from ...utils.space_ops import rotate_vector, shoelace_direction
41
+ from manim.utils.color import BLACK, WHITE, ManimColor, ParsableManimColor
42
+ from manim.utils.iterables import (
43
+ make_even,
44
+ resize_array,
45
+ stretch_array_to_length,
46
+ tuplify,
47
+ )
48
+ from manim.utils.space_ops import rotate_vector, shoelace_direction
53
49
 
54
50
  if TYPE_CHECKING:
51
+ from typing import Any
52
+
53
+ import numpy.typing as npt
54
+ from typing_extensions import Self
55
+
55
56
  from manim.typing import (
56
- BezierPoints,
57
- CubicBezierPoints,
57
+ CubicBezierPath,
58
+ CubicBezierPointsLike,
59
+ CubicSpline,
58
60
  ManimFloat,
59
61
  MappingFunction,
60
- Point2D,
62
+ Point2DLike,
61
63
  Point3D,
62
64
  Point3D_Array,
63
- QuadraticBezierPoints,
65
+ Point3DLike,
66
+ Point3DLike_Array,
64
67
  RGBA_Array_Float,
65
- Vector3,
68
+ Vector3D,
66
69
  Zeros,
67
70
  )
68
71
 
@@ -114,7 +117,7 @@ class VMobject(Mobject):
114
117
  background_stroke_width: float = 0,
115
118
  sheen_factor: float = 0.0,
116
119
  joint_type: LineJointType | None = None,
117
- sheen_direction: Vector3 = UL,
120
+ sheen_direction: Vector3D = UL,
118
121
  close_new_points: bool = False,
119
122
  pre_function_handle_to_anchor_scale_factor: float = 0.01,
120
123
  make_smooth_after_applying_functions: bool = False,
@@ -123,7 +126,8 @@ class VMobject(Mobject):
123
126
  # TODO, do we care about accounting for varying zoom levels?
124
127
  tolerance_for_point_equality: float = 1e-6,
125
128
  n_points_per_cubic_curve: int = 4,
126
- **kwargs,
129
+ cap_style: CapStyleType = CapStyleType.AUTO,
130
+ **kwargs: Any,
127
131
  ):
128
132
  self.fill_opacity = fill_opacity
129
133
  self.stroke_opacity = stroke_opacity
@@ -138,7 +142,7 @@ class VMobject(Mobject):
138
142
  self.joint_type: LineJointType = (
139
143
  LineJointType.AUTO if joint_type is None else joint_type
140
144
  )
141
- self.sheen_direction: Vector3 = sheen_direction
145
+ self.sheen_direction: Vector3D = sheen_direction
142
146
  self.close_new_points: bool = close_new_points
143
147
  self.pre_function_handle_to_anchor_scale_factor: float = (
144
148
  pre_function_handle_to_anchor_scale_factor
@@ -150,6 +154,10 @@ class VMobject(Mobject):
150
154
  self.shade_in_3d: bool = shade_in_3d
151
155
  self.tolerance_for_point_equality: float = tolerance_for_point_equality
152
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
+ )
160
+ self.cap_style: CapStyleType = cap_style
153
161
  super().__init__(**kwargs)
154
162
  self.submobjects: list[VMobject]
155
163
 
@@ -162,6 +170,9 @@ class VMobject(Mobject):
162
170
  if stroke_color is not None:
163
171
  self.stroke_color = ManimColor.parse(stroke_color)
164
172
 
173
+ def _assert_valid_submobjects(self, submobjects: Iterable[VMobject]) -> Self:
174
+ return self._assert_valid_submobjects_internal(submobjects, VMobject)
175
+
165
176
  # OpenGL compatibility
166
177
  @property
167
178
  def n_points_per_curve(self) -> int:
@@ -335,11 +346,39 @@ class VMobject(Mobject):
335
346
  setattr(self, opacity_name, opacity)
336
347
  if color is not None and background:
337
348
  if isinstance(color, (list, tuple)):
338
- self.background_stroke_color = color
349
+ self.background_stroke_color = ManimColor.parse(color)
339
350
  else:
340
351
  self.background_stroke_color = ManimColor(color)
341
352
  return self
342
353
 
354
+ def set_cap_style(self, cap_style: CapStyleType) -> Self:
355
+ """
356
+ Sets the cap style of the :class:`VMobject`.
357
+
358
+ Parameters
359
+ ----------
360
+ cap_style
361
+ The cap style to be set. See :class:`.CapStyleType` for options.
362
+
363
+ Returns
364
+ -------
365
+ :class:`VMobject`
366
+ ``self``
367
+
368
+ Examples
369
+ --------
370
+ .. manim:: CapStyleExample
371
+ :save_last_frame:
372
+
373
+ class CapStyleExample(Scene):
374
+ def construct(self):
375
+ line = Line(LEFT, RIGHT, color=YELLOW, stroke_width=20)
376
+ line.set_cap_style(CapStyleType.ROUND)
377
+ self.add(line)
378
+ """
379
+ self.cap_style = cap_style
380
+ return self
381
+
343
382
  def set_background_stroke(self, **kwargs) -> Self:
344
383
  kwargs["background"] = True
345
384
  self.set_stroke(**kwargs)
@@ -356,7 +395,7 @@ class VMobject(Mobject):
356
395
  background_stroke_width: float | None = None,
357
396
  background_stroke_opacity: float | None = None,
358
397
  sheen_factor: float | None = None,
359
- sheen_direction: Vector3 | None = None,
398
+ sheen_direction: Vector3D | None = None,
360
399
  background_image: Image | str | None = None,
361
400
  family: bool = True,
362
401
  ) -> Self:
@@ -433,6 +472,64 @@ class VMobject(Mobject):
433
472
  self.set_stroke(opacity=opacity, family=family, background=True)
434
473
  return self
435
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
+
436
533
  def fade(self, darkness: float = 0.5, family: bool = True) -> Self:
437
534
  factor = 1.0 - darkness
438
535
  self.set_fill(opacity=factor * self.get_fill_opacity(), family=False)
@@ -523,7 +620,7 @@ class VMobject(Mobject):
523
620
 
524
621
  color = property(get_color, set_color)
525
622
 
526
- def set_sheen_direction(self, direction: Vector3, family: bool = True) -> Self:
623
+ def set_sheen_direction(self, direction: Vector3D, family: bool = True) -> Self:
527
624
  """Sets the direction of the applied sheen.
528
625
 
529
626
  Parameters
@@ -542,17 +639,16 @@ class VMobject(Mobject):
542
639
  :meth:`~.VMobject.set_sheen`
543
640
  :meth:`~.VMobject.rotate_sheen_direction`
544
641
  """
545
-
546
642
  direction = np.array(direction)
547
643
  if family:
548
644
  for submob in self.get_family():
549
645
  submob.sheen_direction = direction
550
646
  else:
551
- self.sheen_direction: Vector3 = direction
647
+ self.sheen_direction: Vector3D = direction
552
648
  return self
553
649
 
554
650
  def rotate_sheen_direction(
555
- self, angle: float, axis: Vector3 = OUT, family: bool = True
651
+ self, angle: float, axis: Vector3D = OUT, family: bool = True
556
652
  ) -> Self:
557
653
  """Rotates the direction of the applied sheen.
558
654
 
@@ -585,7 +681,7 @@ class VMobject(Mobject):
585
681
  return self
586
682
 
587
683
  def set_sheen(
588
- self, factor: float, direction: Vector3 | None = None, family: bool = True
684
+ self, factor: float, direction: Vector3D | None = None, family: bool = True
589
685
  ) -> Self:
590
686
  """Applies a color gradient from a direction.
591
687
 
@@ -608,7 +704,6 @@ class VMobject(Mobject):
608
704
  circle = Circle(fill_opacity=1).set_sheen(-0.3, DR)
609
705
  self.add(circle)
610
706
  """
611
-
612
707
  if family:
613
708
  for submob in self.submobjects:
614
709
  submob.set_sheen(factor, direction, family)
@@ -623,7 +718,7 @@ class VMobject(Mobject):
623
718
  self.set_fill(self.get_fill_color(), family=family)
624
719
  return self
625
720
 
626
- def get_sheen_direction(self) -> Vector3:
721
+ def get_sheen_direction(self) -> Vector3D:
627
722
  return np.array(self.sheen_direction)
628
723
 
629
724
  def get_sheen_factor(self) -> float:
@@ -664,14 +759,14 @@ class VMobject(Mobject):
664
759
  submob.z_index_group = self
665
760
  return self
666
761
 
667
- def set_points(self, points: Point3D_Array) -> Self:
762
+ def set_points(self, points: Point3DLike_Array) -> Self:
668
763
  self.points: Point3D_Array = np.array(points)
669
764
  return self
670
765
 
671
766
  def resize_points(
672
767
  self,
673
768
  new_length: int,
674
- resize_func: Callable[[Point3D, int], Point3D] = resize_array,
769
+ resize_func: Callable[[Point3D_Array, int], Point3D_Array] = resize_array,
675
770
  ) -> Self:
676
771
  """Resize the array of anchor points and handles to have
677
772
  the specified size.
@@ -691,10 +786,10 @@ class VMobject(Mobject):
691
786
 
692
787
  def set_anchors_and_handles(
693
788
  self,
694
- anchors1: CubicBezierPoints,
695
- handles1: CubicBezierPoints,
696
- handles2: CubicBezierPoints,
697
- anchors2: CubicBezierPoints,
789
+ anchors1: Point3DLike_Array,
790
+ handles1: Point3DLike_Array,
791
+ handles2: Point3DLike_Array,
792
+ anchors2: Point3DLike_Array,
698
793
  ) -> Self:
699
794
  """Given two sets of anchors and handles, process them to set them as anchors
700
795
  and handles of the VMobject.
@@ -712,7 +807,7 @@ class VMobject(Mobject):
712
807
  assert len(anchors1) == len(handles1) == len(handles2) == len(anchors2)
713
808
  nppcc = self.n_points_per_cubic_curve # 4
714
809
  total_len = nppcc * len(anchors1)
715
- self.points = np.zeros((total_len, self.dim))
810
+ self.points = np.empty((total_len, self.dim))
716
811
  # the following will, from the four sets, dispatch them in points such that
717
812
  # self.points = [
718
813
  # anchors1[0], handles1[0], handles2[0], anchors1[0], anchors1[1],
@@ -724,31 +819,69 @@ class VMobject(Mobject):
724
819
  return self
725
820
 
726
821
  def clear_points(self) -> None:
822
+ # TODO: shouldn't this return self instead of None?
727
823
  self.points = np.zeros((0, self.dim))
728
824
 
729
- 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
+ """
730
839
  # TODO, check that number new points is a multiple of 4?
731
840
  # or else that if len(self.points) % 4 == 1, then
732
841
  # len(new_points) % 4 == 3?
733
- 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
734
847
  return self
735
848
 
736
- def start_new_path(self, point: Point3D) -> Self:
737
- 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:
738
870
  # close the open path by appending the last
739
871
  # start anchor sufficiently often
740
872
  last_anchor = self.get_start_anchors()[-1]
741
- for _ in range(4 - (len(self.points) % 4)):
742
- self.append_points([last_anchor])
743
- 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])
744
877
  return self
745
878
 
746
879
  def add_cubic_bezier_curve(
747
880
  self,
748
- anchor1: CubicBezierPoints,
749
- handle1: CubicBezierPoints,
750
- handle2: CubicBezierPoints,
751
- anchor2: CubicBezierPoints,
881
+ anchor1: Point3DLike,
882
+ handle1: Point3DLike,
883
+ handle2: Point3DLike,
884
+ anchor2: Point3DLike,
752
885
  ) -> None:
753
886
  # TODO, check the len(self.points) % 4 == 0?
754
887
  self.append_points([anchor1, handle1, handle2, anchor2])
@@ -759,9 +892,9 @@ class VMobject(Mobject):
759
892
 
760
893
  def add_cubic_bezier_curve_to(
761
894
  self,
762
- handle1: CubicBezierPoints,
763
- handle2: CubicBezierPoints,
764
- anchor: CubicBezierPoints,
895
+ handle1: Point3DLike,
896
+ handle2: Point3DLike,
897
+ anchor: Point3DLike,
765
898
  ) -> Self:
766
899
  """Add cubic bezier curve to the path.
767
900
 
@@ -791,8 +924,8 @@ class VMobject(Mobject):
791
924
 
792
925
  def add_quadratic_bezier_curve_to(
793
926
  self,
794
- handle: QuadraticBezierPoints,
795
- anchor: QuadraticBezierPoints,
927
+ handle: Point3DLike,
928
+ anchor: Point3DLike,
796
929
  ) -> Self:
797
930
  """Add Quadratic bezier curve to the path.
798
931
 
@@ -815,30 +948,29 @@ class VMobject(Mobject):
815
948
  )
816
949
  return self
817
950
 
818
- def add_line_to(self, point: Point3D) -> Self:
951
+ def add_line_to(self, point: Point3DLike) -> Self:
819
952
  """Add a straight line from the last point of VMobject to the given point.
820
953
 
821
954
  Parameters
822
955
  ----------
823
956
 
824
957
  point
825
- end of the straight line.
958
+ The end of the straight line.
826
959
 
827
960
  Returns
828
961
  -------
829
962
  :class:`VMobject`
830
963
  ``self``
831
964
  """
832
- nppcc = self.n_points_per_cubic_curve
833
965
  self.add_cubic_bezier_curve_to(
834
966
  *(
835
- interpolate(self.get_last_point(), point, a)
836
- 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:]
837
969
  )
838
970
  )
839
971
  return self
840
972
 
841
- def add_smooth_curve_to(self, *points: Point3D) -> Self:
973
+ def add_smooth_curve_to(self, *points: Point3DLike) -> Self:
842
974
  """Creates a smooth curve from given points and add it to the VMobject. If two points are passed in, the first is interpreted
843
975
  as a handle, the second as an anchor.
844
976
 
@@ -897,16 +1029,58 @@ class VMobject(Mobject):
897
1029
  if not self.is_closed():
898
1030
  self.add_line_to(self.get_subpaths()[-1][0])
899
1031
 
900
- def add_points_as_corners(self, points: Iterable[Point3D]) -> Iterable[Point3D]:
901
- for point in points:
902
- self.add_line_to(point)
903
- 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)
904
1073
 
905
- def set_points_as_corners(self, points: Point3D_Array) -> Self:
906
- """Given an array of points, set them as corner of the vmobject.
1074
+ self.append_points(new_points)
1075
+ return self
907
1076
 
908
- To achieve that, this algorithm sets handles aligned with the anchors such that the resultant bezier curve will be the segment
909
- between the two anchors.
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`.
1080
+
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.
910
1084
 
911
1085
  Parameters
912
1086
  ----------
@@ -916,18 +1090,38 @@ class VMobject(Mobject):
916
1090
  Returns
917
1091
  -------
918
1092
  :class:`VMobject`
919
- ``self``
1093
+ The VMobject itself, after setting the new points as corners.
1094
+
1095
+
1096
+ Examples
1097
+ --------
1098
+ .. manim:: PointsAsCornersExample
1099
+ :save_last_frame:
1100
+
1101
+ class PointsAsCornersExample(Scene):
1102
+ def construct(self):
1103
+ corners = (
1104
+ # create square
1105
+ UR, UL,
1106
+ DL, DR,
1107
+ UR,
1108
+ # create crosses
1109
+ DL, UL,
1110
+ DR
1111
+ )
1112
+ vmob = VMobject(stroke_color=RED)
1113
+ vmob.set_points_as_corners(corners).scale(2)
1114
+ self.add(vmob)
920
1115
  """
921
- nppcc = self.n_points_per_cubic_curve
922
1116
  points = np.array(points)
923
1117
  # This will set the handles aligned with the anchors.
924
1118
  # Id est, a bezier curve will be the segment from the two anchors such that the handles belongs to this segment.
925
1119
  self.set_anchors_and_handles(
926
- *(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)
927
1121
  )
928
1122
  return self
929
1123
 
930
- def set_points_smoothly(self, points: Point3D_Array) -> Self:
1124
+ def set_points_smoothly(self, points: Point3DLike_Array) -> Self:
931
1125
  self.set_points_as_corners(points)
932
1126
  self.make_smooth()
933
1127
  return self
@@ -953,7 +1147,7 @@ class VMobject(Mobject):
953
1147
  # The append is needed as the last element is not reached when slicing with numpy.
954
1148
  anchors = np.append(subpath[::nppcc], subpath[-1:], 0)
955
1149
  if mode == "smooth":
956
- h1, h2 = get_smooth_handle_points(anchors)
1150
+ h1, h2 = get_smooth_cubic_bezier_handle_points(anchors)
957
1151
  else: # mode == "jagged"
958
1152
  # The following will make the handles aligned with the anchors, thus making the bezier curve a segment
959
1153
  a1 = anchors[:-1]
@@ -972,19 +1166,17 @@ class VMobject(Mobject):
972
1166
  def make_jagged(self) -> Self:
973
1167
  return self.change_anchor_mode("jagged")
974
1168
 
975
- def add_subpath(self, points: Point3D_Array) -> Self:
1169
+ def add_subpath(self, points: CubicBezierPathLike) -> Self:
976
1170
  assert len(points) % 4 == 0
977
- self.points: Point3D_Array = np.append(self.points, points, axis=0)
1171
+ self.append_points(points)
978
1172
  return self
979
1173
 
980
1174
  def append_vectorized_mobject(self, vectorized_mobject: VMobject) -> None:
981
- new_points = list(vectorized_mobject.points)
982
-
983
1175
  if self.has_new_path_started():
984
1176
  # Remove last point, which is starting
985
1177
  # a new path
986
1178
  self.points = self.points[:-1]
987
- self.append_points(new_points)
1179
+ self.append_points(vectorized_mobject.points)
988
1180
 
989
1181
  def apply_function(self, function: MappingFunction) -> Self:
990
1182
  factor = self.pre_function_handle_to_anchor_scale_factor
@@ -998,8 +1190,8 @@ class VMobject(Mobject):
998
1190
  def rotate(
999
1191
  self,
1000
1192
  angle: float,
1001
- axis: Vector3 = OUT,
1002
- about_point: Point3D | None = None,
1193
+ axis: Vector3D = OUT,
1194
+ about_point: Point3DLike | None = None,
1003
1195
  **kwargs,
1004
1196
  ) -> Self:
1005
1197
  self.rotate_sheen_direction(angle, axis)
@@ -1038,10 +1230,10 @@ class VMobject(Mobject):
1038
1230
  return self
1039
1231
 
1040
1232
  #
1041
- def consider_points_equals(self, p0: Point3D, p1: Point3D) -> bool:
1233
+ def consider_points_equals(self, p0: Point3DLike, p1: Point3DLike) -> bool:
1042
1234
  return np.allclose(p0, p1, atol=self.tolerance_for_point_equality)
1043
1235
 
1044
- def consider_points_equals_2d(self, p0: Point2D, p1: Point2D) -> bool:
1236
+ def consider_points_equals_2d(self, p0: Point2DLike, p1: Point2DLike) -> bool:
1045
1237
  """Determine if two points are close enough to be considered equal.
1046
1238
 
1047
1239
  This uses the algorithm from np.isclose(), but expanded here for the
@@ -1062,19 +1254,17 @@ class VMobject(Mobject):
1062
1254
  atol = self.tolerance_for_point_equality
1063
1255
  if abs(p0[0] - p1[0]) > atol + rtol * abs(p1[0]):
1064
1256
  return False
1065
- if abs(p0[1] - p1[1]) > atol + rtol * abs(p1[1]):
1066
- return False
1067
- return True
1257
+ return abs(p0[1] - p1[1]) <= atol + rtol * abs(p1[1])
1068
1258
 
1069
1259
  # Information about line
1070
1260
  def get_cubic_bezier_tuples_from_points(
1071
- self, points: Point3D_Array
1072
- ) -> npt.NDArray[Point3D_Array]:
1261
+ self, points: CubicBezierPathLike
1262
+ ) -> CubicBezierPoints_Array:
1073
1263
  return np.array(self.gen_cubic_bezier_tuples_from_points(points))
1074
1264
 
1075
1265
  def gen_cubic_bezier_tuples_from_points(
1076
- self, points: Point3D_Array
1077
- ) -> tuple[Point3D_Array]:
1266
+ self, points: CubicBezierPathLike
1267
+ ) -> tuple[CubicBezierPointsLike, ...]:
1078
1268
  """Returns the bezier tuples from an array of points.
1079
1269
 
1080
1270
  self.points is a list of the anchors and handles of the bezier curves of the mobject (ie [anchor1, handle1, handle2, anchor2, anchor3 ..])
@@ -1098,14 +1288,14 @@ class VMobject(Mobject):
1098
1288
  # Basically take every nppcc element.
1099
1289
  return tuple(points[i : i + nppcc] for i in range(0, len(points), nppcc))
1100
1290
 
1101
- def get_cubic_bezier_tuples(self) -> npt.NDArray[Point3D_Array]:
1291
+ def get_cubic_bezier_tuples(self) -> CubicBezierPoints_Array:
1102
1292
  return self.get_cubic_bezier_tuples_from_points(self.points)
1103
1293
 
1104
1294
  def _gen_subpaths_from_points(
1105
1295
  self,
1106
- points: Point3D_Array,
1296
+ points: CubicBezierPath,
1107
1297
  filter_func: Callable[[int], bool],
1108
- ) -> Generator[Point3D_Array]:
1298
+ ) -> Iterable[CubicSpline]:
1109
1299
  """Given an array of points defining the bezier curves of the vmobject, return subpaths formed by these points.
1110
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.
1111
1301
 
@@ -1123,7 +1313,7 @@ class VMobject(Mobject):
1123
1313
 
1124
1314
  Returns
1125
1315
  -------
1126
- Generator[Point3D_Array]
1316
+ Iterable[CubicSpline]
1127
1317
  subpaths formed by the points.
1128
1318
  """
1129
1319
  nppcc = self.n_points_per_cubic_curve
@@ -1135,7 +1325,7 @@ class VMobject(Mobject):
1135
1325
  if (i2 - i1) >= nppcc
1136
1326
  )
1137
1327
 
1138
- def get_subpaths_from_points(self, points: Point3D_Array) -> list[Point3D_Array]:
1328
+ def get_subpaths_from_points(self, points: CubicBezierPath) -> list[CubicSpline]:
1139
1329
  return list(
1140
1330
  self._gen_subpaths_from_points(
1141
1331
  points,
@@ -1144,26 +1334,26 @@ class VMobject(Mobject):
1144
1334
  )
1145
1335
 
1146
1336
  def gen_subpaths_from_points_2d(
1147
- self, points: Point3D_Array
1148
- ) -> Generator[Point3D_Array]:
1337
+ self, points: CubicBezierPath
1338
+ ) -> Iterable[CubicSpline]:
1149
1339
  return self._gen_subpaths_from_points(
1150
1340
  points,
1151
1341
  lambda n: not self.consider_points_equals_2d(points[n - 1], points[n]),
1152
1342
  )
1153
1343
 
1154
- def get_subpaths(self) -> list[Point3D_Array]:
1344
+ def get_subpaths(self) -> list[CubicSpline]:
1155
1345
  """Returns subpaths formed by the curves of the VMobject.
1156
1346
 
1157
1347
  Subpaths are ranges of curves with each pair of consecutive curves having their end/start points coincident.
1158
1348
 
1159
1349
  Returns
1160
1350
  -------
1161
- list[Point3D_Array]
1351
+ list[CubicSpline]
1162
1352
  subpaths.
1163
1353
  """
1164
1354
  return self.get_subpaths_from_points(self.points)
1165
1355
 
1166
- def get_nth_curve_points(self, n: int) -> Point3D_Array:
1356
+ def get_nth_curve_points(self, n: int) -> CubicBezierPoints:
1167
1357
  """Returns the points defining the nth curve of the vmobject.
1168
1358
 
1169
1359
  Parameters
@@ -1173,7 +1363,7 @@ class VMobject(Mobject):
1173
1363
 
1174
1364
  Returns
1175
1365
  -------
1176
- Point3D_Array
1366
+ CubicBezierPoints
1177
1367
  points defining the nth bezier curve (anchors, handles)
1178
1368
  """
1179
1369
  assert n < self.get_num_curves()
@@ -1242,7 +1432,6 @@ class VMobject(Mobject):
1242
1432
  length : :class:`float`
1243
1433
  The length of the nth curve.
1244
1434
  """
1245
-
1246
1435
  _, length = self.get_nth_curve_function_with_length(n, sample_points)
1247
1436
 
1248
1437
  return length
@@ -1268,7 +1457,6 @@ class VMobject(Mobject):
1268
1457
  length : :class:`float`
1269
1458
  The length of the nth curve.
1270
1459
  """
1271
-
1272
1460
  curve = self.get_nth_curve_function(n)
1273
1461
  norms = self.get_nth_curve_length_pieces(n, sample_points=sample_points)
1274
1462
  length = np.sum(norms)
@@ -1288,15 +1476,14 @@ class VMobject(Mobject):
1288
1476
 
1289
1477
  def get_curve_functions(
1290
1478
  self,
1291
- ) -> Generator[Callable[[float], Point3D]]:
1479
+ ) -> Iterable[Callable[[float], Point3D]]:
1292
1480
  """Gets the functions for the curves of the mobject.
1293
1481
 
1294
1482
  Returns
1295
1483
  -------
1296
- Generator[Callable[[float], Point3D]]
1484
+ Iterable[Callable[[float], Point3D]]
1297
1485
  The functions for the curves.
1298
1486
  """
1299
-
1300
1487
  num_curves = self.get_num_curves()
1301
1488
 
1302
1489
  for n in range(num_curves):
@@ -1304,7 +1491,7 @@ class VMobject(Mobject):
1304
1491
 
1305
1492
  def get_curve_functions_with_lengths(
1306
1493
  self, **kwargs
1307
- ) -> Generator[tuple[Callable[[float], Point3D], float]]:
1494
+ ) -> Iterable[tuple[Callable[[float], Point3D], float]]:
1308
1495
  """Gets the functions and lengths of the curves for the mobject.
1309
1496
 
1310
1497
  Parameters
@@ -1314,10 +1501,9 @@ class VMobject(Mobject):
1314
1501
 
1315
1502
  Returns
1316
1503
  -------
1317
- Generator[tuple[Callable[[float], Point3D], float]]
1504
+ Iterable[tuple[Callable[[float], Point3D], float]]
1318
1505
  The functions and lengths of the curves.
1319
1506
  """
1320
-
1321
1507
  num_curves = self.get_num_curves()
1322
1508
 
1323
1509
  for n in range(num_curves):
@@ -1342,8 +1528,23 @@ class VMobject(Mobject):
1342
1528
  If ``alpha`` is not between 0 and 1.
1343
1529
  :exc:`Exception`
1344
1530
  If the :class:`VMobject` has no points.
1345
- """
1346
1531
 
1532
+ Example
1533
+ -------
1534
+ .. manim:: PointFromProportion
1535
+ :save_last_frame:
1536
+
1537
+ class PointFromProportion(Scene):
1538
+ def construct(self):
1539
+ line = Line(2*DL, 2*UR)
1540
+ self.add(line)
1541
+ colors = (RED, BLUE, YELLOW)
1542
+ proportions = (1/4, 1/2, 3/4)
1543
+ for color, proportion in zip(colors, proportions):
1544
+ self.add(Dot(color=color).move_to(
1545
+ line.point_from_proportion(proportion)
1546
+ ))
1547
+ """
1347
1548
  if alpha < 0 or alpha > 1:
1348
1549
  raise ValueError(f"Alpha {alpha} not between 0 and 1.")
1349
1550
 
@@ -1366,10 +1567,13 @@ class VMobject(Mobject):
1366
1567
  return curve(residue)
1367
1568
 
1368
1569
  current_length += length
1570
+ raise Exception(
1571
+ "Not sure how you reached here, please file a bug report at https://github.com/ManimCommunity/manim/issues/new/choose"
1572
+ )
1369
1573
 
1370
1574
  def proportion_from_point(
1371
1575
  self,
1372
- point: Iterable[float | int],
1576
+ point: Point3DLike,
1373
1577
  ) -> float:
1374
1578
  """Returns the proportion along the path of the :class:`VMobject`
1375
1579
  a particular given point is at.
@@ -1458,7 +1662,7 @@ class VMobject(Mobject):
1458
1662
  nppcc = self.n_points_per_cubic_curve
1459
1663
  return self.points[nppcc - 1 :: nppcc]
1460
1664
 
1461
- def get_anchors(self) -> Point3D_Array:
1665
+ def get_anchors(self) -> list[Point3D]:
1462
1666
  """Returns the anchors of the curves forming the VMobject.
1463
1667
 
1464
1668
  Returns
@@ -1468,9 +1672,10 @@ class VMobject(Mobject):
1468
1672
  """
1469
1673
  if self.points.shape[0] == 1:
1470
1674
  return self.points
1471
- return np.array(
1472
- tuple(it.chain(*zip(self.get_start_anchors(), self.get_end_anchors()))),
1473
- )
1675
+
1676
+ s = self.get_start_anchors()
1677
+ e = self.get_end_anchors()
1678
+ return list(it.chain.from_iterable(zip(s, e)))
1474
1679
 
1475
1680
  def get_points_defining_boundary(self) -> Point3D_Array:
1476
1681
  # Probably returns all anchors, but this is weird regarding the name of the method.
@@ -1491,7 +1696,6 @@ class VMobject(Mobject):
1491
1696
  float
1492
1697
  The length of the :class:`VMobject`.
1493
1698
  """
1494
-
1495
1699
  return sum(
1496
1700
  length
1497
1701
  for _, length in self.get_curve_functions_with_lengths(
@@ -1596,8 +1800,8 @@ class VMobject(Mobject):
1596
1800
  return self
1597
1801
 
1598
1802
  def insert_n_curves_to_point_list(
1599
- self, n: int, points: Point3D_Array
1600
- ) -> npt.NDArray[BezierPoints]:
1803
+ self, n: int, points: BezierPathLike
1804
+ ) -> BezierPath:
1601
1805
  """Given an array of k points defining a bezier curves (anchors and handles), returns points defining exactly k + n bezier curves.
1602
1806
 
1603
1807
  Parameters
@@ -1611,44 +1815,14 @@ class VMobject(Mobject):
1611
1815
  -------
1612
1816
  Points generated.
1613
1817
  """
1614
-
1615
1818
  if len(points) == 1:
1616
1819
  nppcc = self.n_points_per_cubic_curve
1617
1820
  return np.repeat(points, nppcc * n, 0)
1618
- bezier_quads = self.get_cubic_bezier_tuples_from_points(points)
1619
- curr_num = len(bezier_quads)
1620
- target_num = curr_num + n
1621
- # This is an array with values ranging from 0
1622
- # up to curr_num, with repeats such that
1623
- # it's total length is target_num. For example,
1624
- # with curr_num = 10, target_num = 15, this would
1625
- # be [0, 0, 1, 2, 2, 3, 4, 4, 5, 6, 6, 7, 8, 8, 9]
1626
- repeat_indices = (np.arange(target_num, dtype="i") * curr_num) // target_num
1627
-
1628
- # If the nth term of this list is k, it means
1629
- # that the nth curve of our path should be split
1630
- # into k pieces.
1631
- # In the above example our array had the following elements
1632
- # [0, 0, 1, 2, 2, 3, 4, 4, 5, 6, 6, 7, 8, 8, 9]
1633
- # We have two 0s, one 1, two 2s and so on.
1634
- # The split factors array would hence be:
1635
- # [2, 1, 2, 1, 2, 1, 2, 1, 2, 1]
1636
- split_factors = np.zeros(curr_num, dtype="i")
1637
- for val in repeat_indices:
1638
- split_factors[val] += 1
1639
-
1640
- new_points = np.zeros((0, self.dim))
1641
- for quad, sf in zip(bezier_quads, split_factors):
1642
- # What was once a single cubic curve defined
1643
- # by "quad" will now be broken into sf
1644
- # smaller cubic curves
1645
- alphas = np.linspace(0, 1, sf + 1)
1646
- for a1, a2 in zip(alphas, alphas[1:]):
1647
- new_points = np.append(
1648
- new_points,
1649
- partial_bezier_points(quad, a1, a2),
1650
- axis=0,
1651
- )
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)
1652
1826
  return new_points
1653
1827
 
1654
1828
  def align_rgbas(self, vmobject: VMobject) -> Self:
@@ -1664,7 +1838,7 @@ class VMobject(Mobject):
1664
1838
  setattr(self, attr, new_a1)
1665
1839
  return self
1666
1840
 
1667
- def get_point_mobject(self, center: Point3D | None = None) -> VectorizedPoint:
1841
+ def get_point_mobject(self, center: Point3DLike | None = None) -> VectorizedPoint:
1668
1842
  if center is None:
1669
1843
  center = self.get_center()
1670
1844
  point = VectorizedPoint(center)
@@ -1690,7 +1864,10 @@ class VMobject(Mobject):
1690
1864
  interpolate(getattr(mobject1, attr), getattr(mobject2, attr), alpha),
1691
1865
  )
1692
1866
  if alpha == 1.0:
1693
- setattr(self, attr, getattr(mobject2, attr))
1867
+ val = getattr(mobject2, attr)
1868
+ if isinstance(val, np.ndarray):
1869
+ val = val.copy()
1870
+ setattr(self, attr, val)
1694
1871
 
1695
1872
  def pointwise_become_partial(
1696
1873
  self,
@@ -1698,60 +1875,91 @@ class VMobject(Mobject):
1698
1875
  a: float,
1699
1876
  b: float,
1700
1877
  ) -> Self:
1701
- """Given two bounds a and b, transforms the points of the self vmobject into the points of the vmobject
1702
- 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``.
1703
1882
 
1704
1883
  Parameters
1705
1884
  ----------
1706
1885
  vmobject
1707
- The vmobject that will serve as a model.
1886
+ The :class:`.VMobject` that will serve as a model.
1708
1887
  a
1709
- upper-bound.
1888
+ The lower bound for ``t``.
1710
1889
  b
1711
- lower-bound
1890
+ The upper bound for ``t``
1712
1891
 
1713
1892
  Returns
1714
1893
  -------
1715
- :class:`VMobject`
1716
- ``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`.
1717
1901
  """
1718
- 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
+ )
1719
1907
  # Partial curve includes three portions:
1720
- # - A middle section, which matches the curve exactly
1721
- # - A start, which is some ending portion of an inner cubic
1722
- # - 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.
1723
1911
  if a <= 0 and b >= 1:
1724
1912
  self.set_points(vmobject.points)
1725
1913
  return self
1726
- bezier_quads = vmobject.get_cubic_bezier_tuples()
1727
- num_cubics = len(bezier_quads)
1728
-
1729
- # The following two lines will compute which bezier curves of the given mobject need to be processed.
1730
- # The residue basically indicates de proportion of the selected bezier curve that have to be selected.
1731
- # 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
1732
- lower_index, lower_residue = integer_interpolate(0, num_cubics, a)
1733
- upper_index, upper_residue = integer_interpolate(0, num_cubics, b)
1734
-
1735
- self.clear_points()
1736
- if num_cubics == 0:
1914
+ num_curves = vmobject.get_num_curves()
1915
+ if num_curves == 0:
1916
+ self.clear_points()
1737
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.
1738
1932
  if lower_index == upper_index:
1739
- self.append_points(
1740
- partial_bezier_points(
1741
- bezier_quads[lower_index],
1742
- lower_residue,
1743
- upper_residue,
1744
- ),
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,
1745
1939
  )
1746
1940
  else:
1747
- self.append_points(
1748
- 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,
1749
1949
  )
1750
- for quad in bezier_quads[lower_index + 1 : upper_index]:
1751
- self.append_points(quad)
1752
- self.append_points(
1753
- 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,
1754
1961
  )
1962
+
1755
1963
  return self
1756
1964
 
1757
1965
  def get_subcurve(self, a: float, b: float) -> Self:
@@ -1870,19 +2078,21 @@ class VGroup(VMobject, metaclass=ConvertToOpenGL):
1870
2078
  >>> triangle, square = Triangle(), Square()
1871
2079
  >>> vg.add(triangle)
1872
2080
  VGroup(Triangle)
1873
- >>> vg + square # a new VGroup is constructed
2081
+ >>> vg + square # a new VGroup is constructed
1874
2082
  VGroup(Triangle, Square)
1875
- >>> vg # not modified
2083
+ >>> vg # not modified
1876
2084
  VGroup(Triangle)
1877
- >>> vg += square; vg # modifies vg
2085
+ >>> vg += square
2086
+ >>> vg # modifies vg
1878
2087
  VGroup(Triangle, Square)
1879
2088
  >>> vg.remove(triangle)
1880
2089
  VGroup(Square)
1881
- >>> vg - square; # a new VGroup is constructed
2090
+ >>> vg - square # a new VGroup is constructed
1882
2091
  VGroup()
1883
- >>> vg # not modified
2092
+ >>> vg # not modified
1884
2093
  VGroup(Square)
1885
- >>> vg -= square; vg # modifies vg
2094
+ >>> vg -= square
2095
+ >>> vg # modifies vg
1886
2096
  VGroup()
1887
2097
 
1888
2098
  .. manim:: ArcShapeIris
@@ -1902,12 +2112,14 @@ class VGroup(VMobject, metaclass=ConvertToOpenGL):
1902
2112
 
1903
2113
  """
1904
2114
 
1905
- def __init__(self, *vmobjects, **kwargs):
2115
+ def __init__(
2116
+ self, *vmobjects: VMobject | Iterable[VMobject], **kwargs: Any
2117
+ ) -> None:
1906
2118
  super().__init__(**kwargs)
1907
2119
  self.add(*vmobjects)
1908
2120
 
1909
2121
  def __repr__(self) -> str:
1910
- 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)})"
1911
2123
 
1912
2124
  def __str__(self) -> str:
1913
2125
  return (
@@ -1915,13 +2127,16 @@ class VGroup(VMobject, metaclass=ConvertToOpenGL):
1915
2127
  f"submobject{'s' if len(self.submobjects) > 0 else ''}"
1916
2128
  )
1917
2129
 
1918
- def add(self, *vmobjects: VMobject) -> Self:
1919
- """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
1920
2135
 
1921
2136
  Parameters
1922
2137
  ----------
1923
2138
  vmobjects
1924
- List of VMobject to add
2139
+ List or iterable of VMobjects to add
1925
2140
 
1926
2141
  Returns
1927
2142
  -------
@@ -1930,10 +2145,13 @@ class VGroup(VMobject, metaclass=ConvertToOpenGL):
1930
2145
  Raises
1931
2146
  ------
1932
2147
  TypeError
1933
- 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
1934
2149
 
1935
2150
  Examples
1936
2151
  --------
2152
+ The following example shows how to add individual or multiple `VMobject` instances through the `VGroup`
2153
+ constructor and its `.add()` method.
2154
+
1937
2155
  .. manim:: AddToVGroup
1938
2156
 
1939
2157
  class AddToVGroup(Scene):
@@ -1962,10 +2180,65 @@ class VGroup(VMobject, metaclass=ConvertToOpenGL):
1962
2180
  self.play( # Animate group without component
1963
2181
  (gr-circle_red).animate.shift(RIGHT)
1964
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`.
1965
2205
  """
1966
- if not all(isinstance(m, (VMobject, OpenGLVMobject)) for m in vmobjects):
1967
- raise TypeError("All submobjects must be of type VMobject")
1968
- return super().add(*vmobjects)
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
+ ):
2234
+ raise TypeError(
2235
+ f"{get_type_error_message(vmobject, (i, 0))} "
2236
+ "You can try adding this value into a Group instead."
2237
+ )
2238
+ else:
2239
+ raise TypeError(get_type_error_message(vmobject, (i, 0)))
2240
+
2241
+ return super().add(*valid_vmobjects)
1969
2242
 
1970
2243
  def __add__(self, vmobject: VMobject) -> Self:
1971
2244
  return VGroup(*self.submobjects, vmobject)
@@ -2002,8 +2275,7 @@ class VGroup(VMobject, metaclass=ConvertToOpenGL):
2002
2275
  >>> new_obj = VMobject()
2003
2276
  >>> vgroup[0] = new_obj
2004
2277
  """
2005
- if not all(isinstance(m, (VMobject, OpenGLVMobject)) for m in value):
2006
- raise TypeError("All submobjects must be of type VMobject")
2278
+ self._assert_valid_submobjects(tuplify(value))
2007
2279
  self.submobjects[key] = value
2008
2280
 
2009
2281
 
@@ -2147,7 +2419,7 @@ class VDict(VMobject, metaclass=ConvertToOpenGL):
2147
2419
  Normal usage::
2148
2420
 
2149
2421
  square_obj = Square()
2150
- my_dict.add([('s', square_obj)])
2422
+ my_dict.add([("s", square_obj)])
2151
2423
  """
2152
2424
  for key, value in dict(mapping_or_iterable).items():
2153
2425
  self.add_key_value_pair(key, value)
@@ -2174,10 +2446,10 @@ class VDict(VMobject, metaclass=ConvertToOpenGL):
2174
2446
  --------
2175
2447
  Normal usage::
2176
2448
 
2177
- my_dict.remove('square')
2449
+ my_dict.remove("square")
2178
2450
  """
2179
2451
  if key not in self.submob_dict:
2180
- 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")
2181
2453
  super().remove(self.submob_dict[key])
2182
2454
  del self.submob_dict[key]
2183
2455
  return self
@@ -2199,7 +2471,7 @@ class VDict(VMobject, metaclass=ConvertToOpenGL):
2199
2471
  --------
2200
2472
  Normal usage::
2201
2473
 
2202
- self.play(Create(my_dict['s']))
2474
+ self.play(Create(my_dict["s"]))
2203
2475
  """
2204
2476
  submob = self.submob_dict[key]
2205
2477
  return submob
@@ -2223,7 +2495,7 @@ class VDict(VMobject, metaclass=ConvertToOpenGL):
2223
2495
  Normal usage::
2224
2496
 
2225
2497
  square_obj = Square()
2226
- my_dict['sq'] = square_obj
2498
+ my_dict["sq"] = square_obj
2227
2499
  """
2228
2500
  if key in self.submob_dict:
2229
2501
  self.remove(key)
@@ -2328,11 +2600,10 @@ class VDict(VMobject, metaclass=ConvertToOpenGL):
2328
2600
  Normal usage::
2329
2601
 
2330
2602
  square_obj = Square()
2331
- self.add_key_value_pair('s', square_obj)
2603
+ self.add_key_value_pair("s", square_obj)
2332
2604
 
2333
2605
  """
2334
- if not isinstance(value, (VMobject, OpenGLVMobject)):
2335
- raise TypeError("All submobjects must be of type VMobject")
2606
+ self._assert_valid_submobjects([value])
2336
2607
  mob = value
2337
2608
  if self.show_keys:
2338
2609
  # This import is here and not at the top to avoid circular import
@@ -2348,7 +2619,7 @@ class VDict(VMobject, metaclass=ConvertToOpenGL):
2348
2619
  class VectorizedPoint(VMobject, metaclass=ConvertToOpenGL):
2349
2620
  def __init__(
2350
2621
  self,
2351
- location: Point3D = ORIGIN,
2622
+ location: Point3DLike = ORIGIN,
2352
2623
  color: ManimColor = BLACK,
2353
2624
  fill_opacity: float = 0,
2354
2625
  stroke_width: float = 0,
@@ -2458,7 +2729,7 @@ class CurvesAsSubmobjects(VGroup):
2458
2729
  if len(self.submobjects) == 0:
2459
2730
  caller_name = sys._getframe(1).f_code.co_name
2460
2731
  raise Exception(
2461
- f"Cannot call CurvesAsSubmobjects.{caller_name} for a CurvesAsSubmobject with no submobjects"
2732
+ f"Cannot call CurvesAsSubmobjects. {caller_name} for a CurvesAsSubmobject with no submobjects"
2462
2733
  )
2463
2734
 
2464
2735
  def _get_submobjects_with_points(self):
@@ -2468,7 +2739,7 @@ class CurvesAsSubmobjects(VGroup):
2468
2739
  if len(submobjs_with_pts) == 0:
2469
2740
  caller_name = sys._getframe(1).f_code.co_name
2470
2741
  raise Exception(
2471
- f"Cannot call CurvesAsSubmobjects.{caller_name} for a CurvesAsSubmobject whose submobjects have no points"
2742
+ f"Cannot call CurvesAsSubmobjects. {caller_name} for a CurvesAsSubmobject whose submobjects have no points"
2472
2743
  )
2473
2744
  return submobjs_with_pts
2474
2745
 
@@ -2548,15 +2819,12 @@ class DashedVMobject(VMobject, metaclass=ConvertToOpenGL):
2548
2819
  if vmobject.is_closed():
2549
2820
  void_len = (1 - r) / n
2550
2821
  else:
2551
- if n == 1:
2552
- void_len = 1 - r
2553
- else:
2554
- void_len = (1 - r) / (n - 1)
2822
+ void_len = 1 - r if n == 1 else (1 - r) / (n - 1)
2555
2823
 
2556
2824
  period = dash_len + void_len
2557
2825
  phase_shift = (dash_offset % 1) * period
2558
2826
 
2559
- if vmobject.is_closed():
2827
+ if vmobject.is_closed(): # noqa: SIM108
2560
2828
  # closed curves have equal amount of dashes and voids
2561
2829
  pattern_len = 1
2562
2830
  else: