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
manim/utils/space_ops.py CHANGED
@@ -2,7 +2,34 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from manim.typing import Point3D_Array, Vector
5
+ import itertools as it
6
+ from collections.abc import Sequence
7
+ from typing import TYPE_CHECKING, Callable
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
+ Vector3D_Array,
32
+ )
6
33
 
7
34
  __all__ = [
8
35
  "quaternion_mult",
@@ -38,19 +65,19 @@ __all__ = [
38
65
  ]
39
66
 
40
67
 
41
- import itertools as it
42
- from typing import Sequence
43
-
44
- import numpy as np
45
- from mapbox_earcut import triangulate_float32 as earcut
46
- from scipy.spatial.transform import Rotation
47
-
48
- from ..constants import DOWN, OUT, PI, RIGHT, TAU, UP, RendererType
49
- from ..utils.iterables import adjacent_pairs
68
+ def norm_squared(v: float) -> float:
69
+ val: float = np.dot(v, v)
70
+ return val
50
71
 
51
72
 
52
- def norm_squared(v: float) -> float:
53
- return np.dot(v, v)
73
+ def cross(v1: Vector3D, v2: Vector3D) -> Vector3D:
74
+ return np.array(
75
+ [
76
+ v1[1] * v2[2] - v1[2] * v2[1],
77
+ v1[2] * v2[0] - v1[0] * v2[2],
78
+ v1[0] * v2[1] - v1[1] * v2[0],
79
+ ]
80
+ )
54
81
 
55
82
 
56
83
  # Quaternions
@@ -104,7 +131,7 @@ def quaternion_from_angle_axis(
104
131
 
105
132
  Returns
106
133
  -------
107
- List[float]
134
+ list[float]
108
135
  Gives back a quaternion from the angle and axis
109
136
  """
110
137
  if not axis_normalized:
@@ -174,7 +201,6 @@ def rotate_vector(
174
201
  ValueError
175
202
  If vector is not of dimension 2 or 3.
176
203
  """
177
-
178
204
  if len(vector) > 3:
179
205
  raise ValueError("Vector must have the correct dimensions.")
180
206
  if len(vector) == 2:
@@ -182,7 +208,7 @@ def rotate_vector(
182
208
  return rotation_matrix(angle, axis) @ vector
183
209
 
184
210
 
185
- def thick_diagonal(dim: int, thickness=2) -> np.ndarray:
211
+ def thick_diagonal(dim: int, thickness: int = 2) -> MatrixMN:
186
212
  row_indices = np.arange(dim).repeat(dim).reshape((dim, dim))
187
213
  col_indices = np.transpose(row_indices)
188
214
  return (np.abs(row_indices - col_indices) < thickness).astype("uint8")
@@ -230,9 +256,7 @@ def rotation_matrix(
230
256
  axis: np.ndarray,
231
257
  homogeneous: bool = False,
232
258
  ) -> np.ndarray:
233
- """
234
- Rotation in R^3 about a specified axis of rotation.
235
- """
259
+ """Rotation in R^3 about a specified axis of rotation."""
236
260
  inhomogeneous_rotation_matrix = Rotation.from_rotvec(
237
261
  angle * normalize(np.array(axis))
238
262
  ).as_matrix()
@@ -273,12 +297,12 @@ def z_to_vector(vector: np.ndarray) -> np.ndarray:
273
297
  (normalized) vector provided as an argument
274
298
  """
275
299
  axis_z = normalize(vector)
276
- axis_y = normalize(np.cross(axis_z, RIGHT))
277
- axis_x = np.cross(axis_y, axis_z)
300
+ axis_y = normalize(cross(axis_z, RIGHT))
301
+ axis_x = cross(axis_y, axis_z)
278
302
  if np.linalg.norm(axis_y) == 0:
279
303
  # the vector passed just so happened to be in the x direction.
280
- axis_x = normalize(np.cross(UP, axis_z))
281
- axis_y = -np.cross(axis_x, axis_z)
304
+ axis_x = normalize(cross(UP, axis_z))
305
+ axis_y = -cross(axis_x, axis_z)
282
306
 
283
307
  return np.array([axis_x, axis_y, axis_z]).T
284
308
 
@@ -302,8 +326,10 @@ def angle_of_vector(vector: Sequence[float] | np.ndarray) -> float:
302
326
  c_vec = np.empty(vector.shape[1], dtype=np.complex128)
303
327
  c_vec.real = vector[0]
304
328
  c_vec.imag = vector[1]
305
- return np.angle(c_vec)
306
- return np.angle(complex(*vector[:2]))
329
+ val1: float = np.angle(c_vec)
330
+ return val1
331
+ val: float = np.angle(complex(*vector[:2]))
332
+ return val
307
333
 
308
334
 
309
335
  def angle_between_vectors(v1: np.ndarray, v2: np.ndarray) -> float:
@@ -322,14 +348,17 @@ def angle_between_vectors(v1: np.ndarray, v2: np.ndarray) -> float:
322
348
  float
323
349
  The angle between the vectors.
324
350
  """
325
-
326
- return 2 * np.arctan2(
351
+ val: float = 2 * np.arctan2(
327
352
  np.linalg.norm(normalize(v1) - normalize(v2)),
328
353
  np.linalg.norm(normalize(v1) + normalize(v2)),
329
354
  )
330
355
 
356
+ return val
357
+
331
358
 
332
- def normalize(vect: np.ndarray | tuple[float], fall_back=None) -> np.ndarray:
359
+ def normalize(
360
+ vect: np.ndarray | tuple[float], fall_back: np.ndarray | None = None
361
+ ) -> np.ndarray:
333
362
  norm = np.linalg.norm(vect)
334
363
  if norm > 0:
335
364
  return np.array(vect) / norm
@@ -359,7 +388,7 @@ def normalize_along_axis(array: np.ndarray, axis: np.ndarray) -> np.ndarray:
359
388
  return array
360
389
 
361
390
 
362
- def get_unit_normal(v1: np.ndarray, v2: np.ndarray, tol: float = 1e-6) -> np.ndarray:
391
+ def get_unit_normal(v1: Vector3D, v2: Vector3D, tol: float = 1e-6) -> Vector3D:
363
392
  """Gets the unit normal of the vectors.
364
393
 
365
394
  Parameters
@@ -376,16 +405,37 @@ def get_unit_normal(v1: np.ndarray, v2: np.ndarray, tol: float = 1e-6) -> np.nda
376
405
  np.ndarray
377
406
  The normal of the two vectors.
378
407
  """
379
- v1, v2 = (normalize(i) for i in (v1, v2))
380
- cp = np.cross(v1, v2)
381
- cp_norm = np.linalg.norm(cp)
382
- if cp_norm < tol:
383
- # Vectors align, so find a normal to them in the plane shared with the z-axis
384
- cp = np.cross(np.cross(v1, OUT), v1)
385
- cp_norm = np.linalg.norm(cp)
386
- if cp_norm < tol:
408
+ # Instead of normalizing v1 and v2, just divide by the greatest
409
+ # of all their absolute components, which is just enough
410
+ div1, div2 = max(np.abs(v1)), max(np.abs(v2))
411
+ if div1 == 0.0:
412
+ if div2 == 0.0:
387
413
  return DOWN
388
- return normalize(cp)
414
+ u = v2 / div2
415
+ elif div2 == 0.0:
416
+ u = v1 / div1
417
+ else:
418
+ # Normal scenario: v1 and v2 are both non-null
419
+ u1, u2 = v1 / div1, v2 / div2
420
+ cp = cross(u1, u2)
421
+ cp_norm = np.sqrt(norm_squared(cp))
422
+ if cp_norm > tol:
423
+ return cp / cp_norm
424
+ # Otherwise, v1 and v2 were aligned
425
+ u = u1
426
+
427
+ # If you are here, you have an "unique", non-zero, unit-ish vector u
428
+ # If it's also too aligned to the Z axis, just return DOWN
429
+ if abs(u[0]) < tol and abs(u[1]) < tol:
430
+ return DOWN
431
+ # Otherwise rotate u in the plane it shares with the Z axis,
432
+ # 90° TOWARDS the Z axis. This is done via (u x [0, 0, 1]) x u,
433
+ # which gives [-xz, -yz, x²+y²] (slightly scaled as well)
434
+ cp = np.array([-u[0] * u[2], -u[1] * u[2], u[0] * u[0] + u[1] * u[1]])
435
+ cp_norm = np.sqrt(norm_squared(cp))
436
+ # Because the norm(u) == 0 case was filtered in the beginning,
437
+ # there is no need to check if the norm of cp is 0
438
+ return cp / cp_norm
389
439
 
390
440
 
391
441
  ###
@@ -434,12 +484,8 @@ def regular_vertices(
434
484
  start_angle : :class:`float`
435
485
  The angle the vertices start at.
436
486
  """
437
-
438
487
  if start_angle is None:
439
- if n % 2 == 0:
440
- start_angle = 0
441
- else:
442
- start_angle = TAU / 4
488
+ start_angle = 0 if n % 2 == 0 else TAU / 4
443
489
 
444
490
  start_vector = rotate_vector(RIGHT * radius, start_angle)
445
491
  vertices = compass_directions(n, start_vector)
@@ -455,11 +501,13 @@ def R3_to_complex(point: Sequence[float]) -> np.ndarray:
455
501
  return complex(*point[:2])
456
502
 
457
503
 
458
- def complex_func_to_R3_func(complex_func):
504
+ def complex_func_to_R3_func(
505
+ complex_func: Callable[[complex], complex],
506
+ ) -> Callable[[Point3DLike], Point3D]:
459
507
  return lambda p: complex_to_R3(complex_func(R3_to_complex(p)))
460
508
 
461
509
 
462
- def center_of_mass(points: Sequence[float]) -> np.ndarray:
510
+ def center_of_mass(points: PointNDLike_Array) -> PointND:
463
511
  """Gets the center of mass of the points in space.
464
512
 
465
513
  Parameters
@@ -529,8 +577,8 @@ def line_intersection(
529
577
  np.pad(np.array(i)[:, :2], ((0, 0), (0, 1)), constant_values=1)
530
578
  for i in (line1, line2)
531
579
  )
532
- line1, line2 = (np.cross(*i) for i in padded)
533
- x, y, z = np.cross(line1, line2)
580
+ line1, line2 = (cross(*i) for i in padded)
581
+ x, y, z = cross(line1, line2)
534
582
 
535
583
  if z == 0:
536
584
  raise ValueError(
@@ -541,12 +589,12 @@ def line_intersection(
541
589
 
542
590
 
543
591
  def find_intersection(
544
- p0s: Sequence[np.ndarray] | Point3D_Array,
545
- v0s: Sequence[np.ndarray] | Point3D_Array,
546
- p1s: Sequence[np.ndarray] | Point3D_Array,
547
- v1s: Sequence[np.ndarray] | Point3D_Array,
592
+ p0s: Point3DLike_Array,
593
+ v0s: Vector3D_Array,
594
+ p1s: Point3DLike_Array,
595
+ v1s: Vector3D_Array,
548
596
  threshold: float = 1e-5,
549
- ) -> Sequence[np.ndarray]:
597
+ ) -> list[Point3D]:
550
598
  """
551
599
  Return the intersection of a line passing through p0 in direction v0
552
600
  with one passing through p1 in direction v1 (or array of intersections
@@ -558,7 +606,7 @@ def find_intersection(
558
606
  result = []
559
607
 
560
608
  for p0, v0, p1, v1 in zip(*[p0s, v0s, p1s, v1s]):
561
- normal = np.cross(v1, np.cross(v0, v1))
609
+ normal = cross(v1, cross(v0, v1))
562
610
  denom = max(np.dot(v0, normal), threshold)
563
611
  result += [p0 + np.dot(p1 - p0, normal) / denom * v0]
564
612
  return result
@@ -581,21 +629,22 @@ def get_winding_number(points: Sequence[np.ndarray]) -> float:
581
629
  >>> from manim import Square, get_winding_number
582
630
  >>> polygon = Square()
583
631
  >>> get_winding_number(polygon.get_vertices())
584
- 1.0
585
- >>> polygon.shift(2*UP)
632
+ np.float64(1.0)
633
+ >>> polygon.shift(2 * UP)
586
634
  Square
587
635
  >>> get_winding_number(polygon.get_vertices())
588
- 0.0
636
+ np.float64(0.0)
589
637
  """
590
- total_angle = 0
638
+ total_angle: float = 0
591
639
  for p1, p2 in adjacent_pairs(points):
592
640
  d_angle = angle_of_vector(p2) - angle_of_vector(p1)
593
641
  d_angle = ((d_angle + PI) % TAU) - PI
594
642
  total_angle += d_angle
595
- return total_angle / TAU
643
+ val: float = total_angle / TAU
644
+ return val
596
645
 
597
646
 
598
- def shoelace(x_y: np.ndarray) -> float:
647
+ def shoelace(x_y: Point2D_Array) -> float:
599
648
  """2D implementation of the shoelace formula.
600
649
 
601
650
  Returns
@@ -605,10 +654,11 @@ def shoelace(x_y: np.ndarray) -> float:
605
654
  """
606
655
  x = x_y[:, 0]
607
656
  y = x_y[:, 1]
608
- return np.trapz(y, x)
657
+ val: float = np.trapz(y, x)
658
+ return val
609
659
 
610
660
 
611
- def shoelace_direction(x_y: np.ndarray) -> str:
661
+ def shoelace_direction(x_y: Point2D_Array) -> str:
612
662
  """
613
663
  Uses the area determined by the shoelace method to determine whether
614
664
  the input set of points is directed clockwise or counterclockwise.
@@ -623,8 +673,9 @@ def shoelace_direction(x_y: np.ndarray) -> str:
623
673
 
624
674
 
625
675
  def cross2d(
626
- a: Sequence[Vector] | Vector, b: Sequence[Vector] | Vector
627
- ) -> Sequence[float] | float:
676
+ a: Vector2D | Vector2D_Array,
677
+ b: Vector2D | Vector2D_Array,
678
+ ) -> ManimFloat | npt.NDArray[ManimFloat]:
628
679
  """Compute the determinant(s) of the passed
629
680
  vector (sequences).
630
681
 
@@ -647,7 +698,7 @@ def cross2d(
647
698
  .. code-block:: pycon
648
699
 
649
700
  >>> cross2d(np.array([1, 2]), np.array([3, 4]))
650
- -2
701
+ np.int64(-2)
651
702
  >>> cross2d(
652
703
  ... np.array([[1, 2, 0], [1, 0, 0]]),
653
704
  ... np.array([[3, 4, 0], [0, 1, 0]]),
@@ -715,12 +766,17 @@ def earclip_triangulation(verts: np.ndarray, ring_ends: list) -> list:
715
766
 
716
767
  # Move the ring which j belongs to from the
717
768
  # attached list to the detached list
718
- new_ring = next(filter(lambda ring: ring[0] <= j < ring[-1], detached_rings))
719
- detached_rings.remove(new_ring)
720
- attached_rings.append(new_ring)
769
+ new_ring = next(
770
+ (ring for ring in detached_rings if ring[0] <= j < ring[-1]), None
771
+ )
772
+ if new_ring is not None:
773
+ detached_rings.remove(new_ring)
774
+ attached_rings.append(new_ring)
775
+ else:
776
+ raise Exception("Could not find a ring to attach")
721
777
 
722
778
  # Setup linked list
723
- after = []
779
+ after: list[int] = []
724
780
  end0 = 0
725
781
  for end1 in ring_ends:
726
782
  after.extend(range(end0 + 1, end1))
@@ -791,7 +847,7 @@ def spherical_to_cartesian(spherical: Sequence[float]) -> np.ndarray:
791
847
 
792
848
  def perpendicular_bisector(
793
849
  line: Sequence[np.ndarray],
794
- norm_vector=OUT,
850
+ norm_vector: Vector3D = OUT,
795
851
  ) -> Sequence[np.ndarray]:
796
852
  """Returns a list of two points that correspond
797
853
  to the ends of the perpendicular bisector of the
@@ -814,6 +870,6 @@ def perpendicular_bisector(
814
870
  """
815
871
  p1 = line[0]
816
872
  p2 = line[1]
817
- direction = np.cross(p1 - p2, norm_vector)
873
+ direction = cross(p1 - p2, norm_vector)
818
874
  m = midpoint(p1, p2)
819
875
  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
@@ -1,9 +1,12 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import Callable
3
+ from typing import Any, Callable
4
4
 
5
+ from manim.renderer.cairo_renderer import CairoRenderer
6
+ from manim.renderer.opengl_renderer import OpenGLRenderer
5
7
  from manim.scene.scene import Scene
6
8
  from manim.scene.scene_file_writer import SceneFileWriter
9
+ from manim.typing import PixelArray, StrPath
7
10
 
8
11
  from ._frames_testers import _FramesTester
9
12
 
@@ -11,13 +14,14 @@ from ._frames_testers import _FramesTester
11
14
  def _make_test_scene_class(
12
15
  base_scene: type[Scene],
13
16
  construct_test: Callable[[Scene], None],
14
- test_renderer,
17
+ test_renderer: CairoRenderer | OpenGLRenderer | None,
15
18
  ) -> type[Scene]:
16
- class _TestedScene(base_scene):
17
- def __init__(self, *args, **kwargs):
18
- super().__init__(renderer=test_renderer, *args, **kwargs)
19
+ # TODO: Get the type annotation right for the base_scene argument.
20
+ class _TestedScene(base_scene): # type: ignore[valid-type, misc]
21
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
22
+ super().__init__(*args, renderer=test_renderer, **kwargs)
19
23
 
20
- def construct(self):
24
+ def construct(self) -> None:
21
25
  construct_test(self)
22
26
 
23
27
  # Manim hack to render the very last frame (normally the last frame is not the very end of the animation)
@@ -28,7 +32,7 @@ def _make_test_scene_class(
28
32
  return _TestedScene
29
33
 
30
34
 
31
- def _make_test_renderer_class(from_renderer):
35
+ def _make_test_renderer_class(from_renderer: type) -> Any:
32
36
  # Just for inheritance.
33
37
  class _TestRenderer(from_renderer):
34
38
  pass
@@ -39,39 +43,50 @@ def _make_test_renderer_class(from_renderer):
39
43
  class DummySceneFileWriter(SceneFileWriter):
40
44
  """Delegate of SceneFileWriter used to test the frames."""
41
45
 
42
- def __init__(self, renderer, scene_name, **kwargs):
46
+ def __init__(
47
+ self,
48
+ renderer: CairoRenderer | OpenGLRenderer,
49
+ scene_name: StrPath,
50
+ **kwargs: Any,
51
+ ) -> None:
43
52
  super().__init__(renderer, scene_name, **kwargs)
44
53
  self.i = 0
45
54
 
46
- def init_output_directories(self, scene_name):
55
+ def init_output_directories(self, scene_name: StrPath) -> None:
47
56
  pass
48
57
 
49
- def add_partial_movie_file(self, hash_animation):
58
+ def add_partial_movie_file(self, hash_animation: str) -> None:
50
59
  pass
51
60
 
52
- def begin_animation(self, allow_write=True):
61
+ def begin_animation(
62
+ self, allow_write: bool = True, file_path: StrPath | None = None
63
+ ) -> Any:
53
64
  pass
54
65
 
55
- def end_animation(self, allow_write):
66
+ def end_animation(self, allow_write: bool = False) -> None:
56
67
  pass
57
68
 
58
- def combine_to_movie(self):
69
+ def combine_to_movie(self) -> None:
59
70
  pass
60
71
 
61
- def combine_to_section_videos(self):
72
+ def combine_to_section_videos(self) -> None:
62
73
  pass
63
74
 
64
- def clean_cache(self):
75
+ def clean_cache(self) -> None:
65
76
  pass
66
77
 
67
- def write_frame(self, frame_or_renderer):
78
+ def write_frame(
79
+ self, frame_or_renderer: PixelArray | OpenGLRenderer, num_frames: int = 1
80
+ ) -> None:
68
81
  self.i += 1
69
82
 
70
83
 
71
84
  def _make_scene_file_writer_class(tester: _FramesTester) -> type[SceneFileWriter]:
72
85
  class TestSceneFileWriter(DummySceneFileWriter):
73
- def write_frame(self, frame_or_renderer):
86
+ def write_frame(
87
+ self, frame_or_renderer: PixelArray | OpenGLRenderer, num_frames: int = 1
88
+ ) -> None:
74
89
  tester.check_frame(self.i, frame_or_renderer)
75
- super().write_frame(frame_or_renderer)
90
+ super().write_frame(frame_or_renderer, num_frames=num_frames)
76
91
 
77
92
  return TestSceneFileWriter