manim 0.17.0__py3-none-any.whl → 0.19.1__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.
Files changed (163) hide show
  1. manim/__init__.py +11 -6
  2. manim/__main__.py +62 -19
  3. manim/_config/__init__.py +10 -9
  4. manim/_config/cli_colors.py +26 -9
  5. manim/_config/default.cfg +1 -3
  6. manim/_config/logger_utils.py +23 -13
  7. manim/_config/utils.py +662 -468
  8. manim/animation/animation.py +164 -18
  9. manim/animation/changing.py +34 -23
  10. manim/animation/composition.py +265 -67
  11. manim/animation/creation.py +208 -26
  12. manim/animation/fading.py +16 -18
  13. manim/animation/growing.py +35 -15
  14. manim/animation/indication.py +150 -76
  15. manim/animation/movement.py +56 -22
  16. manim/animation/numbers.py +64 -6
  17. manim/animation/rotation.py +78 -7
  18. manim/animation/specialized.py +6 -7
  19. manim/animation/speedmodifier.py +13 -10
  20. manim/animation/transform.py +14 -11
  21. manim/animation/transform_matching_parts.py +3 -4
  22. manim/animation/updaters/mobject_update_utils.py +152 -30
  23. manim/animation/updaters/update.py +10 -7
  24. manim/camera/camera.py +182 -118
  25. manim/camera/mapping_camera.py +34 -3
  26. manim/camera/moving_camera.py +95 -74
  27. manim/camera/multi_camera.py +23 -15
  28. manim/camera/three_d_camera.py +70 -52
  29. manim/cli/__init__.py +17 -0
  30. manim/cli/cfg/group.py +76 -44
  31. manim/cli/checkhealth/checks.py +192 -0
  32. manim/cli/checkhealth/commands.py +90 -0
  33. manim/cli/default_group.py +158 -25
  34. manim/cli/init/commands.py +33 -25
  35. manim/cli/plugins/commands.py +16 -3
  36. manim/cli/render/commands.py +72 -60
  37. manim/cli/render/ease_of_access_options.py +4 -3
  38. manim/cli/render/global_options.py +59 -17
  39. manim/cli/render/output_options.py +6 -5
  40. manim/cli/render/render_options.py +98 -33
  41. manim/constants.py +109 -59
  42. manim/data_structures.py +31 -0
  43. manim/mobject/frame.py +8 -5
  44. manim/mobject/geometry/__init__.py +1 -0
  45. manim/mobject/geometry/arc.py +277 -135
  46. manim/mobject/geometry/boolean_ops.py +32 -31
  47. manim/mobject/geometry/labeled.py +376 -0
  48. manim/mobject/geometry/line.py +192 -87
  49. manim/mobject/geometry/polygram.py +224 -58
  50. manim/mobject/geometry/shape_matchers.py +61 -25
  51. manim/mobject/geometry/tips.py +122 -48
  52. manim/mobject/graph.py +1027 -419
  53. manim/mobject/graphing/coordinate_systems.py +533 -278
  54. manim/mobject/graphing/functions.py +53 -32
  55. manim/mobject/graphing/number_line.py +123 -65
  56. manim/mobject/graphing/probability.py +88 -62
  57. manim/mobject/graphing/scale.py +33 -19
  58. manim/mobject/logo.py +118 -28
  59. manim/mobject/matrix.py +87 -83
  60. manim/mobject/mobject.py +912 -442
  61. manim/mobject/opengl/dot_cloud.py +16 -5
  62. manim/mobject/opengl/opengl_compatibility.py +4 -2
  63. manim/mobject/opengl/opengl_geometry.py +254 -153
  64. manim/mobject/opengl/opengl_image_mobject.py +3 -1
  65. manim/mobject/opengl/opengl_mobject.py +779 -482
  66. manim/mobject/opengl/opengl_point_cloud_mobject.py +41 -14
  67. manim/mobject/opengl/opengl_surface.py +14 -92
  68. manim/mobject/opengl/opengl_three_dimensions.py +12 -8
  69. manim/mobject/opengl/opengl_vectorized_mobject.py +98 -100
  70. manim/mobject/svg/brace.py +173 -41
  71. manim/mobject/svg/svg_mobject.py +139 -53
  72. manim/mobject/table.py +61 -68
  73. manim/mobject/text/code_mobject.py +193 -539
  74. manim/mobject/text/numbers.py +81 -34
  75. manim/mobject/text/tex_mobject.py +130 -78
  76. manim/mobject/text/text_mobject.py +288 -164
  77. manim/mobject/three_d/polyhedra.py +111 -13
  78. manim/mobject/three_d/three_d_utils.py +17 -8
  79. manim/mobject/three_d/three_dimensions.py +239 -106
  80. manim/mobject/types/image_mobject.py +50 -30
  81. manim/mobject/types/point_cloud_mobject.py +120 -75
  82. manim/mobject/types/vectorized_mobject.py +841 -408
  83. manim/mobject/value_tracker.py +105 -38
  84. manim/mobject/vector_field.py +50 -31
  85. manim/opengl/__init__.py +3 -3
  86. manim/plugins/__init__.py +14 -1
  87. manim/plugins/plugins_flags.py +10 -14
  88. manim/renderer/cairo_renderer.py +65 -50
  89. manim/renderer/opengl_renderer.py +89 -69
  90. manim/renderer/opengl_renderer_window.py +39 -18
  91. manim/renderer/shader.py +123 -87
  92. manim/renderer/shader_wrapper.py +44 -28
  93. manim/renderer/vectorized_mobject_rendering.py +38 -10
  94. manim/scene/moving_camera_scene.py +32 -3
  95. manim/scene/scene.py +507 -242
  96. manim/scene/scene_file_writer.py +371 -220
  97. manim/scene/section.py +20 -16
  98. manim/scene/three_d_scene.py +14 -22
  99. manim/scene/vector_space_scene.py +223 -129
  100. manim/scene/zoomed_scene.py +46 -41
  101. manim/typing.py +990 -0
  102. manim/utils/bezier.py +1823 -371
  103. manim/utils/caching.py +12 -5
  104. manim/utils/color/AS2700.py +236 -0
  105. manim/utils/color/BS381.py +318 -0
  106. manim/utils/color/DVIPSNAMES.py +96 -0
  107. manim/utils/color/SVGNAMES.py +179 -0
  108. manim/utils/color/X11.py +533 -0
  109. manim/utils/color/XKCD.py +952 -0
  110. manim/utils/color/__init__.py +61 -0
  111. manim/utils/color/core.py +1667 -0
  112. manim/utils/color/manim_colors.py +218 -0
  113. manim/utils/commands.py +48 -20
  114. manim/utils/config_ops.py +39 -19
  115. manim/utils/debug.py +8 -7
  116. manim/utils/deprecation.py +86 -39
  117. manim/utils/docbuild/__init__.py +17 -0
  118. manim/utils/docbuild/autoaliasattr_directive.py +236 -0
  119. manim/utils/docbuild/autocolor_directive.py +99 -0
  120. manim/utils/docbuild/manim_directive.py +94 -41
  121. manim/utils/docbuild/module_parsing.py +245 -0
  122. manim/utils/exceptions.py +6 -0
  123. manim/utils/family.py +5 -3
  124. manim/utils/family_ops.py +17 -4
  125. manim/utils/file_ops.py +27 -17
  126. manim/utils/hashing.py +55 -45
  127. manim/utils/images.py +13 -7
  128. manim/utils/ipython_magic.py +13 -7
  129. manim/utils/iterables.py +163 -120
  130. manim/utils/module_ops.py +66 -24
  131. manim/utils/opengl.py +77 -24
  132. manim/utils/parameter_parsing.py +32 -0
  133. manim/utils/paths.py +30 -33
  134. manim/utils/polylabel.py +235 -0
  135. manim/utils/qhull.py +218 -0
  136. manim/utils/rate_functions.py +98 -32
  137. manim/utils/simple_functions.py +25 -33
  138. manim/utils/sounds.py +7 -1
  139. manim/utils/space_ops.py +188 -115
  140. manim/utils/testing/__init__.py +17 -0
  141. manim/utils/testing/_frames_testers.py +13 -8
  142. manim/utils/testing/_show_diff.py +5 -3
  143. manim/utils/testing/_test_class_makers.py +34 -18
  144. manim/utils/testing/frames_comparison.py +37 -19
  145. manim/utils/tex.py +130 -198
  146. manim/utils/tex_file_writing.py +77 -47
  147. manim/utils/tex_templates.py +2 -1
  148. manim/utils/unit.py +6 -5
  149. {manim-0.17.0.dist-info → manim-0.19.1.dist-info}/METADATA +64 -65
  150. manim-0.19.1.dist-info/RECORD +220 -0
  151. {manim-0.17.0.dist-info → manim-0.19.1.dist-info}/WHEEL +1 -1
  152. manim-0.19.1.dist-info/entry_points.txt +3 -0
  153. {manim-0.17.0.dist-info → manim-0.19.1.dist-info/licenses}/LICENSE.community +1 -1
  154. manim/cli/new/group.py +0 -189
  155. manim/communitycolors.py +0 -9
  156. manim/gui/__init__.py +0 -0
  157. manim/gui/gui.py +0 -82
  158. manim/plugins/import_plugins.py +0 -43
  159. manim/utils/color.py +0 -552
  160. manim-0.17.0.dist-info/RECORD +0 -206
  161. manim-0.17.0.dist-info/entry_points.txt +0 -4
  162. /manim/cli/{new → checkhealth}/__init__.py +0 -0
  163. {manim-0.17.0.dist-info → manim-0.19.1.dist-info/licenses}/LICENSE +0 -0
manim/utils/space_ops.py CHANGED
@@ -2,6 +2,36 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import itertools as it
6
+ from collections.abc import Callable, Sequence
7
+ from typing import TYPE_CHECKING
8
+
9
+ import numpy as np
10
+ from mapbox_earcut import triangulate_float32 as earcut
11
+ from scipy.spatial.transform import Rotation
12
+
13
+ from manim.constants import DOWN, OUT, PI, RIGHT, TAU, UP
14
+ from manim.utils.iterables import adjacent_pairs
15
+
16
+ if TYPE_CHECKING:
17
+ import numpy.typing as npt
18
+
19
+ from manim.typing import (
20
+ ManimFloat,
21
+ MatrixMN,
22
+ Point2D_Array,
23
+ Point3D,
24
+ Point3DLike,
25
+ Point3DLike_Array,
26
+ PointND,
27
+ PointNDLike_Array,
28
+ Vector2D,
29
+ Vector2D_Array,
30
+ Vector3D,
31
+ Vector3DLike,
32
+ Vector3DLike_Array,
33
+ )
34
+
5
35
  __all__ = [
6
36
  "quaternion_mult",
7
37
  "quaternion_from_angle_axis",
@@ -36,21 +66,19 @@ __all__ = [
36
66
  ]
37
67
 
38
68
 
39
- import itertools as it
40
- import math
41
- from typing import Sequence
42
-
43
- import numpy as np
44
- from mapbox_earcut import triangulate_float32 as earcut
45
- from scipy.spatial.transform import Rotation
46
-
47
- from .. import config
48
- from ..constants import DOWN, OUT, PI, RIGHT, TAU, UP, RendererType
49
- from ..utils.iterables import adjacent_pairs
69
+ def norm_squared(v: float) -> float:
70
+ val: float = np.dot(v, v)
71
+ return val
50
72
 
51
73
 
52
- def norm_squared(v: float) -> float:
53
- return np.dot(v, v)
74
+ def cross(v1: Vector3DLike, v2: Vector3DLike) -> Vector3D:
75
+ return np.array(
76
+ [
77
+ v1[1] * v2[2] - v1[2] * v2[1],
78
+ v1[2] * v2[0] - v1[0] * v2[2],
79
+ v1[0] * v2[1] - v1[1] * v2[0],
80
+ ]
81
+ )
54
82
 
55
83
 
56
84
  # Quaternions
@@ -69,34 +97,19 @@ def quaternion_mult(
69
97
  Union[np.ndarray, List[Union[float, np.ndarray]]]
70
98
  Returns a list of product of two quaternions.
71
99
  """
72
- if config.renderer == RendererType.OPENGL:
73
- if len(quats) == 0:
74
- return [1, 0, 0, 0]
75
- result = quats[0]
76
- for next_quat in quats[1:]:
77
- w1, x1, y1, z1 = result
78
- w2, x2, y2, z2 = next_quat
79
- result = [
80
- w1 * w2 - x1 * x2 - y1 * y2 - z1 * z2,
81
- w1 * x2 + x1 * w2 + y1 * z2 - z1 * y2,
82
- w1 * y2 + y1 * w2 + z1 * x2 - x1 * z2,
83
- w1 * z2 + z1 * w2 + x1 * y2 - y1 * x2,
84
- ]
85
- return result
86
- elif config.renderer == RendererType.CAIRO:
87
- q1 = quats[0]
88
- q2 = quats[1]
89
-
90
- w1, x1, y1, z1 = q1
91
- w2, x2, y2, z2 = q2
92
- return np.array(
93
- [
94
- w1 * w2 - x1 * x2 - y1 * y2 - z1 * z2,
95
- w1 * x2 + x1 * w2 + y1 * z2 - z1 * y2,
96
- w1 * y2 + y1 * w2 + z1 * x2 - x1 * z2,
97
- w1 * z2 + z1 * w2 + x1 * y2 - y1 * x2,
98
- ],
99
- )
100
+ if len(quats) == 0:
101
+ return [1, 0, 0, 0]
102
+ result = quats[0]
103
+ for next_quat in quats[1:]:
104
+ w1, x1, y1, z1 = result
105
+ w2, x2, y2, z2 = next_quat
106
+ result = [
107
+ w1 * w2 - x1 * x2 - y1 * y2 - z1 * z2,
108
+ w1 * x2 + x1 * w2 + y1 * z2 - z1 * y2,
109
+ w1 * y2 + y1 * w2 + z1 * x2 - x1 * z2,
110
+ w1 * z2 + z1 * w2 + x1 * y2 - y1 * x2,
111
+ ]
112
+ return result
100
113
 
101
114
 
102
115
  def quaternion_from_angle_axis(
@@ -119,15 +132,12 @@ def quaternion_from_angle_axis(
119
132
 
120
133
  Returns
121
134
  -------
122
- List[float]
135
+ list[float]
123
136
  Gives back a quaternion from the angle and axis
124
137
  """
125
- if config.renderer == RendererType.OPENGL:
126
- if not axis_normalized:
127
- axis = normalize(axis)
128
- return [math.cos(angle / 2), *(math.sin(angle / 2) * axis)]
129
- elif config.renderer == RendererType.CAIRO:
130
- return np.append(np.cos(angle / 2), np.sin(angle / 2) * normalize(axis))
138
+ if not axis_normalized:
139
+ axis = normalize(axis)
140
+ return [np.cos(angle / 2), *(np.sin(angle / 2) * axis)]
131
141
 
132
142
 
133
143
  def angle_axis_from_quaternion(quaternion: Sequence[float]) -> Sequence[float]:
@@ -169,8 +179,8 @@ def quaternion_conjugate(quaternion: Sequence[float]) -> np.ndarray:
169
179
 
170
180
 
171
181
  def rotate_vector(
172
- vector: np.ndarray, angle: float, axis: np.ndarray = OUT
173
- ) -> np.ndarray:
182
+ vector: Vector3DLike, angle: float, axis: Vector3DLike = OUT
183
+ ) -> Vector3D:
174
184
  """Function for rotating a vector.
175
185
 
176
186
  Parameters
@@ -192,7 +202,6 @@ def rotate_vector(
192
202
  ValueError
193
203
  If vector is not of dimension 2 or 3.
194
204
  """
195
-
196
205
  if len(vector) > 3:
197
206
  raise ValueError("Vector must have the correct dimensions.")
198
207
  if len(vector) == 2:
@@ -200,7 +209,7 @@ def rotate_vector(
200
209
  return rotation_matrix(angle, axis) @ vector
201
210
 
202
211
 
203
- def thick_diagonal(dim: int, thickness=2) -> np.ndarray:
212
+ def thick_diagonal(dim: int, thickness: int = 2) -> MatrixMN:
204
213
  row_indices = np.arange(dim).repeat(dim).reshape((dim, dim))
205
214
  col_indices = np.transpose(row_indices)
206
215
  return (np.abs(row_indices - col_indices) < thickness).astype("uint8")
@@ -237,7 +246,7 @@ def rotation_matrix_from_quaternion(quat: np.ndarray) -> np.ndarray:
237
246
  return np.transpose(rotation_matrix_transpose_from_quaternion(quat))
238
247
 
239
248
 
240
- def rotation_matrix_transpose(angle: float, axis: np.ndarray) -> np.ndarray:
249
+ def rotation_matrix_transpose(angle: float, axis: Vector3DLike) -> np.ndarray:
241
250
  if all(np.array(axis)[:2] == np.zeros(2)):
242
251
  return rotation_about_z(angle * np.sign(axis[2])).T
243
252
  return rotation_matrix(angle, axis).T
@@ -245,14 +254,12 @@ def rotation_matrix_transpose(angle: float, axis: np.ndarray) -> np.ndarray:
245
254
 
246
255
  def rotation_matrix(
247
256
  angle: float,
248
- axis: np.ndarray,
257
+ axis: Vector3DLike,
249
258
  homogeneous: bool = False,
250
259
  ) -> np.ndarray:
251
- """
252
- Rotation in R^3 about a specified axis of rotation.
253
- """
260
+ """Rotation in R^3 about a specified axis of rotation."""
254
261
  inhomogeneous_rotation_matrix = Rotation.from_rotvec(
255
- angle * normalize(np.array(axis))
262
+ angle * normalize(axis)
256
263
  ).as_matrix()
257
264
  if not homogeneous:
258
265
  return inhomogeneous_rotation_matrix
@@ -275,7 +282,7 @@ def rotation_about_z(angle: float) -> np.ndarray:
275
282
  np.ndarray
276
283
  Gives back the rotated matrix.
277
284
  """
278
- c, s = math.cos(angle), math.sin(angle)
285
+ c, s = np.cos(angle), np.sin(angle)
279
286
  return np.array(
280
287
  [
281
288
  [c, -s, 0],
@@ -291,12 +298,12 @@ def z_to_vector(vector: np.ndarray) -> np.ndarray:
291
298
  (normalized) vector provided as an argument
292
299
  """
293
300
  axis_z = normalize(vector)
294
- axis_y = normalize(np.cross(axis_z, RIGHT))
295
- axis_x = np.cross(axis_y, axis_z)
301
+ axis_y = normalize(cross(axis_z, RIGHT))
302
+ axis_x = cross(axis_y, axis_z)
296
303
  if np.linalg.norm(axis_y) == 0:
297
304
  # the vector passed just so happened to be in the x direction.
298
- axis_x = normalize(np.cross(UP, axis_z))
299
- axis_y = -np.cross(axis_x, axis_z)
305
+ axis_x = normalize(cross(UP, axis_z))
306
+ axis_y = -cross(axis_x, axis_z)
300
307
 
301
308
  return np.array([axis_x, axis_y, axis_z]).T
302
309
 
@@ -320,11 +327,13 @@ def angle_of_vector(vector: Sequence[float] | np.ndarray) -> float:
320
327
  c_vec = np.empty(vector.shape[1], dtype=np.complex128)
321
328
  c_vec.real = vector[0]
322
329
  c_vec.imag = vector[1]
323
- return np.angle(c_vec)
324
- return np.angle(complex(*vector[:2]))
330
+ val1: float = np.angle(c_vec)
331
+ return val1
332
+ val: float = np.angle(complex(*vector[:2]))
333
+ return val
325
334
 
326
335
 
327
- def angle_between_vectors(v1: np.ndarray, v2: np.ndarray) -> np.ndarray:
336
+ def angle_between_vectors(v1: np.ndarray, v2: np.ndarray) -> float:
328
337
  """Returns the angle between two vectors.
329
338
  This angle will always be between 0 and pi
330
339
 
@@ -337,17 +346,20 @@ def angle_between_vectors(v1: np.ndarray, v2: np.ndarray) -> np.ndarray:
337
346
 
338
347
  Returns
339
348
  -------
340
- np.ndarray
349
+ float
341
350
  The angle between the vectors.
342
351
  """
343
-
344
- return 2 * np.arctan2(
352
+ val: float = 2 * np.arctan2(
345
353
  np.linalg.norm(normalize(v1) - normalize(v2)),
346
354
  np.linalg.norm(normalize(v1) + normalize(v2)),
347
355
  )
348
356
 
357
+ return val
349
358
 
350
- def normalize(vect: np.ndarray | tuple[float], fall_back=None) -> np.ndarray:
359
+
360
+ def normalize(
361
+ vect: np.ndarray | tuple[float], fall_back: np.ndarray | None = None
362
+ ) -> np.ndarray:
351
363
  norm = np.linalg.norm(vect)
352
364
  if norm > 0:
353
365
  return np.array(vect) / norm
@@ -377,7 +389,7 @@ def normalize_along_axis(array: np.ndarray, axis: np.ndarray) -> np.ndarray:
377
389
  return array
378
390
 
379
391
 
380
- def get_unit_normal(v1: np.ndarray, v2: np.ndarray, tol: float = 1e-6) -> np.ndarray:
392
+ def get_unit_normal(v1: Vector3DLike, v2: Vector3DLike, tol: float = 1e-6) -> Vector3D:
381
393
  """Gets the unit normal of the vectors.
382
394
 
383
395
  Parameters
@@ -394,16 +406,40 @@ def get_unit_normal(v1: np.ndarray, v2: np.ndarray, tol: float = 1e-6) -> np.nda
394
406
  np.ndarray
395
407
  The normal of the two vectors.
396
408
  """
397
- v1, v2 = (normalize(i) for i in (v1, v2))
398
- cp = np.cross(v1, v2)
399
- cp_norm = np.linalg.norm(cp)
400
- if cp_norm < tol:
401
- # Vectors align, so find a normal to them in the plane shared with the z-axis
402
- cp = np.cross(np.cross(v1, OUT), v1)
403
- cp_norm = np.linalg.norm(cp)
404
- if cp_norm < tol:
409
+ np_v1 = np.asarray(v1)
410
+ np_v2 = np.asarray(v2)
411
+
412
+ # Instead of normalizing v1 and v2, just divide by the greatest
413
+ # of all their absolute components, which is just enough
414
+ div1, div2 = max(np.abs(np_v1)), max(np.abs(np_v2))
415
+ if div1 == 0.0:
416
+ if div2 == 0.0:
405
417
  return DOWN
406
- return normalize(cp)
418
+ u = np_v2 / div2
419
+ elif div2 == 0.0:
420
+ u = np_v1 / div1
421
+ else:
422
+ # Normal scenario: v1 and v2 are both non-null
423
+ u1, u2 = np_v1 / div1, np_v2 / div2
424
+ cp = cross(u1, u2)
425
+ cp_norm = np.sqrt(norm_squared(cp))
426
+ if cp_norm > tol:
427
+ return cp / cp_norm
428
+ # Otherwise, v1 and v2 were aligned
429
+ u = u1
430
+
431
+ # If you are here, you have an "unique", non-zero, unit-ish vector u
432
+ # If it's also too aligned to the Z axis, just return DOWN
433
+ if abs(u[0]) < tol and abs(u[1]) < tol:
434
+ return DOWN
435
+ # Otherwise rotate u in the plane it shares with the Z axis,
436
+ # 90° TOWARDS the Z axis. This is done via (u x [0, 0, 1]) x u,
437
+ # which gives [-xz, -yz, x²+y²] (slightly scaled as well)
438
+ cp = np.array([-u[0] * u[2], -u[1] * u[2], u[0] * u[0] + u[1] * u[1]])
439
+ cp_norm = np.sqrt(norm_squared(cp))
440
+ # Because the norm(u) == 0 case was filtered in the beginning,
441
+ # there is no need to check if the norm of cp is 0
442
+ return cp / cp_norm
407
443
 
408
444
 
409
445
  ###
@@ -452,12 +488,8 @@ def regular_vertices(
452
488
  start_angle : :class:`float`
453
489
  The angle the vertices start at.
454
490
  """
455
-
456
491
  if start_angle is None:
457
- if n % 2 == 0:
458
- start_angle = 0
459
- else:
460
- start_angle = TAU / 4
492
+ start_angle = 0 if n % 2 == 0 else TAU / 4
461
493
 
462
494
  start_vector = rotate_vector(RIGHT * radius, start_angle)
463
495
  vertices = compass_directions(n, start_vector)
@@ -473,11 +505,13 @@ def R3_to_complex(point: Sequence[float]) -> np.ndarray:
473
505
  return complex(*point[:2])
474
506
 
475
507
 
476
- def complex_func_to_R3_func(complex_func):
508
+ def complex_func_to_R3_func(
509
+ complex_func: Callable[[complex], complex],
510
+ ) -> Callable[[Point3DLike], Point3D]:
477
511
  return lambda p: complex_to_R3(complex_func(R3_to_complex(p)))
478
512
 
479
513
 
480
- def center_of_mass(points: Sequence[float]) -> np.ndarray:
514
+ def center_of_mass(points: PointNDLike_Array) -> PointND:
481
515
  """Gets the center of mass of the points in space.
482
516
 
483
517
  Parameters
@@ -547,8 +581,8 @@ def line_intersection(
547
581
  np.pad(np.array(i)[:, :2], ((0, 0), (0, 1)), constant_values=1)
548
582
  for i in (line1, line2)
549
583
  )
550
- line1, line2 = (np.cross(*i) for i in padded)
551
- x, y, z = np.cross(line1, line2)
584
+ line1, line2 = (cross(*i) for i in padded)
585
+ x, y, z = cross(line1, line2)
552
586
 
553
587
  if z == 0:
554
588
  raise ValueError(
@@ -559,12 +593,12 @@ def line_intersection(
559
593
 
560
594
 
561
595
  def find_intersection(
562
- p0s: Sequence[np.ndarray],
563
- v0s: Sequence[np.ndarray],
564
- p1s: Sequence[np.ndarray],
565
- v1s: Sequence[np.ndarray],
596
+ p0s: Point3DLike_Array,
597
+ v0s: Vector3DLike_Array,
598
+ p1s: Point3DLike_Array,
599
+ v1s: Vector3DLike_Array,
566
600
  threshold: float = 1e-5,
567
- ) -> Sequence[np.ndarray]:
601
+ ) -> list[Point3D]:
568
602
  """
569
603
  Return the intersection of a line passing through p0 in direction v0
570
604
  with one passing through p1 in direction v1 (or array of intersections
@@ -575,8 +609,8 @@ def find_intersection(
575
609
  # algorithm from https://en.wikipedia.org/wiki/Skew_lines#Nearest_points
576
610
  result = []
577
611
 
578
- for p0, v0, p1, v1 in zip(*[p0s, v0s, p1s, v1s]):
579
- normal = np.cross(v1, np.cross(v0, v1))
612
+ for p0, v0, p1, v1 in zip(p0s, v0s, p1s, v1s):
613
+ normal = cross(v1, cross(v0, v1))
580
614
  denom = max(np.dot(v0, normal), threshold)
581
615
  result += [p0 + np.dot(p1 - p0, normal) / denom * v0]
582
616
  return result
@@ -599,21 +633,22 @@ def get_winding_number(points: Sequence[np.ndarray]) -> float:
599
633
  >>> from manim import Square, get_winding_number
600
634
  >>> polygon = Square()
601
635
  >>> get_winding_number(polygon.get_vertices())
602
- 1.0
603
- >>> polygon.shift(2*UP)
636
+ np.float64(1.0)
637
+ >>> polygon.shift(2 * UP)
604
638
  Square
605
639
  >>> get_winding_number(polygon.get_vertices())
606
- 0.0
640
+ np.float64(0.0)
607
641
  """
608
- total_angle = 0
642
+ total_angle: float = 0
609
643
  for p1, p2 in adjacent_pairs(points):
610
644
  d_angle = angle_of_vector(p2) - angle_of_vector(p1)
611
645
  d_angle = ((d_angle + PI) % TAU) - PI
612
646
  total_angle += d_angle
613
- return total_angle / TAU
647
+ val: float = total_angle / TAU
648
+ return val
614
649
 
615
650
 
616
- def shoelace(x_y: np.ndarray) -> float:
651
+ def shoelace(x_y: Point2D_Array) -> float:
617
652
  """2D implementation of the shoelace formula.
618
653
 
619
654
  Returns
@@ -623,10 +658,11 @@ def shoelace(x_y: np.ndarray) -> float:
623
658
  """
624
659
  x = x_y[:, 0]
625
660
  y = x_y[:, 1]
626
- return np.trapz(y, x)
661
+ val: float = np.trapz(y, x)
662
+ return val
627
663
 
628
664
 
629
- def shoelace_direction(x_y: np.ndarray) -> str:
665
+ def shoelace_direction(x_y: Point2D_Array) -> str:
630
666
  """
631
667
  Uses the area determined by the shoelace method to determine whether
632
668
  the input set of points is directed clockwise or counterclockwise.
@@ -640,7 +676,39 @@ def shoelace_direction(x_y: np.ndarray) -> str:
640
676
  return "CW" if area > 0 else "CCW"
641
677
 
642
678
 
643
- def cross2d(a, b):
679
+ def cross2d(
680
+ a: Vector2D | Vector2D_Array,
681
+ b: Vector2D | Vector2D_Array,
682
+ ) -> ManimFloat | npt.NDArray[ManimFloat]:
683
+ """Compute the determinant(s) of the passed
684
+ vector (sequences).
685
+
686
+ Parameters
687
+ ----------
688
+ a
689
+ A vector or a sequence of vectors.
690
+ b
691
+ A vector or a sequence of vectors.
692
+
693
+ Returns
694
+ -------
695
+ Sequence[float] | float
696
+ The determinant or sequence of determinants
697
+ of the first two components of the specified
698
+ vectors.
699
+
700
+ Examples
701
+ --------
702
+ .. code-block:: pycon
703
+
704
+ >>> cross2d(np.array([1, 2]), np.array([3, 4]))
705
+ np.int64(-2)
706
+ >>> cross2d(
707
+ ... np.array([[1, 2, 0], [1, 0, 0]]),
708
+ ... np.array([[3, 4, 0], [0, 1, 0]]),
709
+ ... )
710
+ array([-2, 1])
711
+ """
644
712
  if len(a.shape) == 2:
645
713
  return a[:, 0] * b[:, 1] - a[:, 1] * b[:, 0]
646
714
  else:
@@ -702,12 +770,17 @@ def earclip_triangulation(verts: np.ndarray, ring_ends: list) -> list:
702
770
 
703
771
  # Move the ring which j belongs to from the
704
772
  # attached list to the detached list
705
- new_ring = next(filter(lambda ring: ring[0] <= j < ring[-1], detached_rings))
706
- detached_rings.remove(new_ring)
707
- attached_rings.append(new_ring)
773
+ new_ring = next(
774
+ (ring for ring in detached_rings if ring[0] <= j < ring[-1]), None
775
+ )
776
+ if new_ring is not None:
777
+ detached_rings.remove(new_ring)
778
+ attached_rings.append(new_ring)
779
+ else:
780
+ raise Exception("Could not find a ring to attach")
708
781
 
709
782
  # Setup linked list
710
- after = []
783
+ after: list[int] = []
711
784
  end0 = 0
712
785
  for end1 in ring_ends:
713
786
  after.extend(range(end0 + 1, end1))
@@ -729,22 +802,22 @@ def earclip_triangulation(verts: np.ndarray, ring_ends: list) -> list:
729
802
  if i == 0:
730
803
  break
731
804
 
732
- meta_indices = earcut(verts[indices, :2], [len(indices)])
805
+ meta_indices = earcut(verts[indices, :2], np.array([len(indices)], dtype=np.uint32))
733
806
  return [indices[mi] for mi in meta_indices]
734
807
 
735
808
 
736
- def cartesian_to_spherical(vec: Sequence[float]) -> np.ndarray:
809
+ def cartesian_to_spherical(vec: Vector3DLike) -> np.ndarray:
737
810
  """Returns an array of numbers corresponding to each
738
811
  polar coordinate value (distance, phi, theta).
739
812
 
740
813
  Parameters
741
814
  ----------
742
815
  vec
743
- A numpy array ``[x, y, z]``.
816
+ A numpy array or a sequence of floats ``[x, y, z]``.
744
817
  """
745
818
  norm = np.linalg.norm(vec)
746
819
  if norm == 0:
747
- return 0, 0, 0
820
+ return np.zeros(3)
748
821
  r = norm
749
822
  phi = np.arccos(vec[2] / r)
750
823
  theta = np.arctan2(vec[1], vec[0])
@@ -778,7 +851,7 @@ def spherical_to_cartesian(spherical: Sequence[float]) -> np.ndarray:
778
851
 
779
852
  def perpendicular_bisector(
780
853
  line: Sequence[np.ndarray],
781
- norm_vector=OUT,
854
+ norm_vector: Vector3D = OUT,
782
855
  ) -> Sequence[np.ndarray]:
783
856
  """Returns a list of two points that correspond
784
857
  to the ends of the perpendicular bisector of the
@@ -801,6 +874,6 @@ def perpendicular_bisector(
801
874
  """
802
875
  p1 = line[0]
803
876
  p2 = line[1]
804
- direction = np.cross(p1 - p2, norm_vector)
877
+ direction = cross(p1 - p2, norm_vector)
805
878
  m = midpoint(p1, p2)
806
879
  return [m + direction, m - direction]
@@ -0,0 +1,17 @@
1
+ """Utilities for Manim tests using `pytest <https://pytest.org>`_.
2
+
3
+ For more information about Manim testing, see:
4
+
5
+ - :doc:`/contributing/development`, specifically the ``Tests`` bullet
6
+ point under :ref:`polishing-changes-and-submitting-a-pull-request`
7
+ - :doc:`/contributing/testing`
8
+
9
+ .. autosummary::
10
+ :toctree: ../reference
11
+
12
+ frames_comparison
13
+ _frames_testers
14
+ _show_diff
15
+ _test_class_makers
16
+
17
+ """
@@ -1,21 +1,25 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import contextlib
4
+ import logging
4
5
  import warnings
6
+ from collections.abc import Generator
5
7
  from pathlib import Path
6
8
 
7
9
  import numpy as np
8
10
 
9
- from manim import logger
11
+ from manim.typing import PixelArray
10
12
 
11
13
  from ._show_diff import show_diff_helper
12
14
 
13
15
  FRAME_ABSOLUTE_TOLERANCE = 1.01
14
16
  FRAME_MISMATCH_RATIO_TOLERANCE = 1e-5
15
17
 
18
+ logger = logging.getLogger("manim")
19
+
16
20
 
17
21
  class _FramesTester:
18
- def __init__(self, file_path: Path, show_diff=False) -> None:
22
+ def __init__(self, file_path: Path, show_diff: bool = False) -> None:
19
23
  self._file_path = file_path
20
24
  self._show_diff = show_diff
21
25
  self._frames: np.ndarray
@@ -23,7 +27,7 @@ class _FramesTester:
23
27
  self._frames_compared = 0
24
28
 
25
29
  @contextlib.contextmanager
26
- def testing(self):
30
+ def testing(self) -> Generator[None, None, None]:
27
31
  with np.load(self._file_path) as data:
28
32
  self._frames = data["frame_data"]
29
33
  # For backward compatibility, when the control data contains only one frame (<= v0.8.0)
@@ -37,7 +41,7 @@ class _FramesTester:
37
41
  f"when there are {self._number_frames} control frames for this test."
38
42
  )
39
43
 
40
- def check_frame(self, frame_number: int, frame: np.ndarray):
44
+ def check_frame(self, frame_number: int, frame: PixelArray) -> None:
41
45
  assert frame_number < self._number_frames, (
42
46
  f"The tested scene is at frame number {frame_number} "
43
47
  f"when there are {self._number_frames} control frames."
@@ -63,7 +67,8 @@ class _FramesTester:
63
67
  warnings.warn(
64
68
  f"Mismatch of {number_of_mismatches} pixel values in frame {frame_number} "
65
69
  f"against control data in {self._file_path}. Below error threshold, "
66
- "continuing..."
70
+ "continuing...",
71
+ stacklevel=1,
67
72
  )
68
73
  return
69
74
 
@@ -84,17 +89,17 @@ class _ControlDataWriter(_FramesTester):
84
89
  self._number_frames_written: int = 0
85
90
 
86
91
  # Actually write a frame.
87
- def check_frame(self, index: int, frame: np.ndarray):
92
+ def check_frame(self, index: int, frame: PixelArray) -> None:
88
93
  frame = frame[np.newaxis, ...]
89
94
  self.frames = np.concatenate((self.frames, frame))
90
95
  self._number_frames_written += 1
91
96
 
92
97
  @contextlib.contextmanager
93
- def testing(self):
98
+ def testing(self) -> Generator[None, None, None]:
94
99
  yield
95
100
  self.save_contol_data()
96
101
 
97
- def save_contol_data(self):
102
+ def save_contol_data(self) -> None:
98
103
  self.frames = self.frames.astype("uint8")
99
104
  np.savez_compressed(self.file_path, frame_data=self.frames)
100
105
  logger.info(
@@ -5,13 +5,15 @@ import warnings
5
5
 
6
6
  import numpy as np
7
7
 
8
+ from manim.typing import PixelArray
9
+
8
10
 
9
11
  def show_diff_helper(
10
12
  frame_number: int,
11
- frame_data: np.ndarray,
12
- expected_frame_data: np.ndarray,
13
+ frame_data: PixelArray,
14
+ expected_frame_data: PixelArray,
13
15
  control_data_filename: str,
14
- ):
16
+ ) -> None:
15
17
  """Will visually display with matplotlib differences between frame generated and the one expected."""
16
18
  import matplotlib.gridspec as gridspec
17
19
  import matplotlib.pyplot as plt