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.
- manim/__init__.py +3 -6
- manim/__main__.py +61 -20
- manim/_config/__init__.py +6 -3
- manim/_config/cli_colors.py +16 -8
- manim/_config/default.cfg +1 -3
- manim/_config/logger_utils.py +14 -8
- manim/_config/utils.py +651 -472
- manim/animation/animation.py +152 -5
- manim/animation/composition.py +80 -39
- manim/animation/creation.py +196 -14
- manim/animation/fading.py +5 -9
- manim/animation/indication.py +103 -47
- manim/animation/movement.py +22 -5
- manim/animation/rotation.py +3 -2
- manim/animation/specialized.py +4 -6
- manim/animation/speedmodifier.py +10 -5
- manim/animation/transform.py +4 -5
- manim/animation/transform_matching_parts.py +1 -1
- manim/animation/updaters/mobject_update_utils.py +17 -14
- manim/camera/camera.py +15 -6
- manim/cli/__init__.py +17 -0
- manim/cli/cfg/group.py +70 -44
- manim/cli/checkhealth/checks.py +93 -75
- manim/cli/checkhealth/commands.py +14 -5
- manim/cli/default_group.py +157 -25
- manim/cli/init/commands.py +32 -24
- manim/cli/plugins/commands.py +16 -3
- manim/cli/render/commands.py +72 -60
- manim/cli/render/ease_of_access_options.py +4 -3
- manim/cli/render/global_options.py +51 -15
- manim/cli/render/output_options.py +6 -5
- manim/cli/render/render_options.py +97 -32
- manim/constants.py +65 -19
- manim/gui/gui.py +2 -0
- manim/mobject/frame.py +0 -1
- manim/mobject/geometry/arc.py +112 -78
- manim/mobject/geometry/boolean_ops.py +32 -25
- manim/mobject/geometry/labeled.py +300 -77
- manim/mobject/geometry/line.py +132 -64
- manim/mobject/geometry/polygram.py +126 -30
- manim/mobject/geometry/shape_matchers.py +35 -15
- manim/mobject/geometry/tips.py +38 -29
- manim/mobject/graph.py +414 -133
- manim/mobject/graphing/coordinate_systems.py +126 -64
- manim/mobject/graphing/functions.py +25 -15
- manim/mobject/graphing/number_line.py +24 -10
- manim/mobject/graphing/probability.py +2 -10
- manim/mobject/graphing/scale.py +6 -5
- manim/mobject/matrix.py +17 -19
- manim/mobject/mobject.py +314 -165
- manim/mobject/opengl/opengl_compatibility.py +2 -0
- manim/mobject/opengl/opengl_geometry.py +30 -9
- manim/mobject/opengl/opengl_image_mobject.py +2 -0
- manim/mobject/opengl/opengl_mobject.py +509 -343
- manim/mobject/opengl/opengl_point_cloud_mobject.py +5 -7
- manim/mobject/opengl/opengl_surface.py +3 -2
- manim/mobject/opengl/opengl_three_dimensions.py +2 -0
- manim/mobject/opengl/opengl_vectorized_mobject.py +46 -79
- manim/mobject/svg/brace.py +63 -13
- manim/mobject/svg/svg_mobject.py +4 -3
- manim/mobject/table.py +11 -13
- manim/mobject/text/code_mobject.py +186 -548
- manim/mobject/text/numbers.py +9 -7
- manim/mobject/text/tex_mobject.py +23 -14
- manim/mobject/text/text_mobject.py +70 -24
- manim/mobject/three_d/polyhedra.py +98 -1
- manim/mobject/three_d/three_d_utils.py +4 -4
- manim/mobject/three_d/three_dimensions.py +62 -34
- manim/mobject/types/image_mobject.py +42 -24
- manim/mobject/types/point_cloud_mobject.py +105 -67
- manim/mobject/types/vectorized_mobject.py +496 -228
- manim/mobject/value_tracker.py +5 -4
- manim/mobject/vector_field.py +5 -5
- manim/opengl/__init__.py +3 -3
- manim/plugins/__init__.py +14 -1
- manim/plugins/plugins_flags.py +14 -8
- manim/renderer/cairo_renderer.py +20 -10
- manim/renderer/opengl_renderer.py +21 -23
- manim/renderer/opengl_renderer_window.py +2 -0
- manim/renderer/shader.py +2 -3
- manim/renderer/shader_wrapper.py +5 -2
- manim/renderer/vectorized_mobject_rendering.py +5 -0
- manim/scene/moving_camera_scene.py +23 -0
- manim/scene/scene.py +90 -43
- manim/scene/scene_file_writer.py +316 -165
- manim/scene/section.py +17 -15
- manim/scene/three_d_scene.py +13 -21
- manim/scene/vector_space_scene.py +22 -9
- manim/typing.py +830 -70
- manim/utils/bezier.py +1667 -399
- manim/utils/caching.py +13 -5
- manim/utils/color/AS2700.py +2 -0
- manim/utils/color/BS381.py +3 -0
- manim/utils/color/DVIPSNAMES.py +96 -0
- manim/utils/color/SVGNAMES.py +179 -0
- manim/utils/color/X11.py +3 -0
- manim/utils/color/XKCD.py +3 -0
- manim/utils/color/__init__.py +8 -5
- manim/utils/color/core.py +844 -309
- manim/utils/color/manim_colors.py +7 -9
- manim/utils/commands.py +48 -20
- manim/utils/config_ops.py +18 -13
- manim/utils/debug.py +8 -7
- manim/utils/deprecation.py +90 -40
- manim/utils/docbuild/__init__.py +17 -0
- manim/utils/docbuild/autoaliasattr_directive.py +234 -0
- manim/utils/docbuild/autocolor_directive.py +21 -17
- manim/utils/docbuild/manim_directive.py +50 -35
- manim/utils/docbuild/module_parsing.py +245 -0
- manim/utils/exceptions.py +6 -0
- manim/utils/family.py +5 -3
- manim/utils/family_ops.py +17 -4
- manim/utils/file_ops.py +26 -16
- manim/utils/hashing.py +9 -7
- manim/utils/images.py +10 -4
- manim/utils/ipython_magic.py +14 -8
- manim/utils/iterables.py +161 -119
- manim/utils/module_ops.py +57 -19
- manim/utils/opengl.py +83 -24
- manim/utils/parameter_parsing.py +32 -0
- manim/utils/paths.py +21 -23
- manim/utils/polylabel.py +168 -0
- manim/utils/qhull.py +218 -0
- manim/utils/rate_functions.py +74 -39
- manim/utils/simple_functions.py +24 -15
- manim/utils/sounds.py +7 -1
- manim/utils/space_ops.py +125 -69
- manim/utils/testing/__init__.py +17 -0
- manim/utils/testing/_frames_testers.py +13 -8
- manim/utils/testing/_show_diff.py +5 -3
- manim/utils/testing/_test_class_makers.py +33 -18
- manim/utils/testing/frames_comparison.py +27 -19
- manim/utils/tex.py +127 -197
- manim/utils/tex_file_writing.py +47 -45
- manim/utils/tex_templates.py +2 -1
- manim/utils/unit.py +6 -5
- {manim-0.18.0.post0.dist-info → manim-0.19.0.dist-info}/LICENSE.community +1 -1
- {manim-0.18.0.post0.dist-info → manim-0.19.0.dist-info}/METADATA +40 -39
- manim-0.19.0.dist-info/RECORD +221 -0
- {manim-0.18.0.post0.dist-info → manim-0.19.0.dist-info}/WHEEL +1 -1
- manim/cli/new/__init__.py +0 -0
- manim/cli/new/group.py +0 -189
- manim/plugins/import_plugins.py +0 -43
- manim-0.18.0.post0.dist-info/RECORD +0 -217
- {manim-0.18.0.post0.dist-info → manim-0.19.0.dist-info}/LICENSE +0 -0
- {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
|
-
|
|
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
|
-
|
|
42
|
-
|
|
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
|
|
53
|
-
return np.
|
|
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
|
-
|
|
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) ->
|
|
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(
|
|
277
|
-
axis_x =
|
|
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(
|
|
281
|
-
axis_y = -
|
|
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
|
-
|
|
306
|
-
|
|
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(
|
|
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:
|
|
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
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
if
|
|
383
|
-
|
|
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
|
-
|
|
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(
|
|
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:
|
|
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 = (
|
|
533
|
-
x, y, z =
|
|
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:
|
|
545
|
-
v0s:
|
|
546
|
-
p1s:
|
|
547
|
-
v1s:
|
|
592
|
+
p0s: Point3DLike_Array,
|
|
593
|
+
v0s: Vector3D_Array,
|
|
594
|
+
p1s: Point3DLike_Array,
|
|
595
|
+
v1s: Vector3D_Array,
|
|
548
596
|
threshold: float = 1e-5,
|
|
549
|
-
) ->
|
|
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 =
|
|
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
|
-
|
|
643
|
+
val: float = total_angle / TAU
|
|
644
|
+
return val
|
|
596
645
|
|
|
597
646
|
|
|
598
|
-
def shoelace(x_y:
|
|
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
|
-
|
|
657
|
+
val: float = np.trapz(y, x)
|
|
658
|
+
return val
|
|
609
659
|
|
|
610
660
|
|
|
611
|
-
def shoelace_direction(x_y:
|
|
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:
|
|
627
|
-
|
|
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(
|
|
719
|
-
|
|
720
|
-
|
|
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 =
|
|
873
|
+
direction = cross(p1 - p2, norm_vector)
|
|
818
874
|
m = midpoint(p1, p2)
|
|
819
875
|
return [m + direction, m - direction]
|
manim/utils/testing/__init__.py
CHANGED
|
@@ -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
|
|
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:
|
|
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:
|
|
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:
|
|
12
|
-
expected_frame_data:
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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__(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|