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.
- manim/__init__.py +11 -6
- manim/__main__.py +62 -19
- manim/_config/__init__.py +10 -9
- manim/_config/cli_colors.py +26 -9
- manim/_config/default.cfg +1 -3
- manim/_config/logger_utils.py +23 -13
- manim/_config/utils.py +662 -468
- manim/animation/animation.py +164 -18
- manim/animation/changing.py +34 -23
- manim/animation/composition.py +265 -67
- manim/animation/creation.py +208 -26
- manim/animation/fading.py +16 -18
- manim/animation/growing.py +35 -15
- manim/animation/indication.py +150 -76
- manim/animation/movement.py +56 -22
- manim/animation/numbers.py +64 -6
- manim/animation/rotation.py +78 -7
- manim/animation/specialized.py +6 -7
- manim/animation/speedmodifier.py +13 -10
- manim/animation/transform.py +14 -11
- manim/animation/transform_matching_parts.py +3 -4
- manim/animation/updaters/mobject_update_utils.py +152 -30
- manim/animation/updaters/update.py +10 -7
- manim/camera/camera.py +182 -118
- manim/camera/mapping_camera.py +34 -3
- manim/camera/moving_camera.py +95 -74
- manim/camera/multi_camera.py +23 -15
- manim/camera/three_d_camera.py +70 -52
- manim/cli/__init__.py +17 -0
- manim/cli/cfg/group.py +76 -44
- manim/cli/checkhealth/checks.py +192 -0
- manim/cli/checkhealth/commands.py +90 -0
- manim/cli/default_group.py +158 -25
- manim/cli/init/commands.py +33 -25
- 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 +59 -17
- manim/cli/render/output_options.py +6 -5
- manim/cli/render/render_options.py +98 -33
- manim/constants.py +109 -59
- manim/data_structures.py +31 -0
- manim/mobject/frame.py +8 -5
- manim/mobject/geometry/__init__.py +1 -0
- manim/mobject/geometry/arc.py +277 -135
- manim/mobject/geometry/boolean_ops.py +32 -31
- manim/mobject/geometry/labeled.py +376 -0
- manim/mobject/geometry/line.py +192 -87
- manim/mobject/geometry/polygram.py +224 -58
- manim/mobject/geometry/shape_matchers.py +61 -25
- manim/mobject/geometry/tips.py +122 -48
- manim/mobject/graph.py +1027 -419
- manim/mobject/graphing/coordinate_systems.py +533 -278
- manim/mobject/graphing/functions.py +53 -32
- manim/mobject/graphing/number_line.py +123 -65
- manim/mobject/graphing/probability.py +88 -62
- manim/mobject/graphing/scale.py +33 -19
- manim/mobject/logo.py +118 -28
- manim/mobject/matrix.py +87 -83
- manim/mobject/mobject.py +912 -442
- manim/mobject/opengl/dot_cloud.py +16 -5
- manim/mobject/opengl/opengl_compatibility.py +4 -2
- manim/mobject/opengl/opengl_geometry.py +254 -153
- manim/mobject/opengl/opengl_image_mobject.py +3 -1
- manim/mobject/opengl/opengl_mobject.py +779 -482
- manim/mobject/opengl/opengl_point_cloud_mobject.py +41 -14
- manim/mobject/opengl/opengl_surface.py +14 -92
- manim/mobject/opengl/opengl_three_dimensions.py +12 -8
- manim/mobject/opengl/opengl_vectorized_mobject.py +98 -100
- manim/mobject/svg/brace.py +173 -41
- manim/mobject/svg/svg_mobject.py +139 -53
- manim/mobject/table.py +61 -68
- manim/mobject/text/code_mobject.py +193 -539
- manim/mobject/text/numbers.py +81 -34
- manim/mobject/text/tex_mobject.py +130 -78
- manim/mobject/text/text_mobject.py +288 -164
- manim/mobject/three_d/polyhedra.py +111 -13
- manim/mobject/three_d/three_d_utils.py +17 -8
- manim/mobject/three_d/three_dimensions.py +239 -106
- manim/mobject/types/image_mobject.py +50 -30
- manim/mobject/types/point_cloud_mobject.py +120 -75
- manim/mobject/types/vectorized_mobject.py +841 -408
- manim/mobject/value_tracker.py +105 -38
- manim/mobject/vector_field.py +50 -31
- manim/opengl/__init__.py +3 -3
- manim/plugins/__init__.py +14 -1
- manim/plugins/plugins_flags.py +10 -14
- manim/renderer/cairo_renderer.py +65 -50
- manim/renderer/opengl_renderer.py +89 -69
- manim/renderer/opengl_renderer_window.py +39 -18
- manim/renderer/shader.py +123 -87
- manim/renderer/shader_wrapper.py +44 -28
- manim/renderer/vectorized_mobject_rendering.py +38 -10
- manim/scene/moving_camera_scene.py +32 -3
- manim/scene/scene.py +507 -242
- manim/scene/scene_file_writer.py +371 -220
- manim/scene/section.py +20 -16
- manim/scene/three_d_scene.py +14 -22
- manim/scene/vector_space_scene.py +223 -129
- manim/scene/zoomed_scene.py +46 -41
- manim/typing.py +990 -0
- manim/utils/bezier.py +1823 -371
- manim/utils/caching.py +12 -5
- manim/utils/color/AS2700.py +236 -0
- manim/utils/color/BS381.py +318 -0
- manim/utils/color/DVIPSNAMES.py +96 -0
- manim/utils/color/SVGNAMES.py +179 -0
- manim/utils/color/X11.py +533 -0
- manim/utils/color/XKCD.py +952 -0
- manim/utils/color/__init__.py +61 -0
- manim/utils/color/core.py +1667 -0
- manim/utils/color/manim_colors.py +218 -0
- manim/utils/commands.py +48 -20
- manim/utils/config_ops.py +39 -19
- manim/utils/debug.py +8 -7
- manim/utils/deprecation.py +86 -39
- manim/utils/docbuild/__init__.py +17 -0
- manim/utils/docbuild/autoaliasattr_directive.py +236 -0
- manim/utils/docbuild/autocolor_directive.py +99 -0
- manim/utils/docbuild/manim_directive.py +94 -41
- 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 +27 -17
- manim/utils/hashing.py +55 -45
- manim/utils/images.py +13 -7
- manim/utils/ipython_magic.py +13 -7
- manim/utils/iterables.py +163 -120
- manim/utils/module_ops.py +66 -24
- manim/utils/opengl.py +77 -24
- manim/utils/parameter_parsing.py +32 -0
- manim/utils/paths.py +30 -33
- manim/utils/polylabel.py +235 -0
- manim/utils/qhull.py +218 -0
- manim/utils/rate_functions.py +98 -32
- manim/utils/simple_functions.py +25 -33
- manim/utils/sounds.py +7 -1
- manim/utils/space_ops.py +188 -115
- 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 +34 -18
- manim/utils/testing/frames_comparison.py +37 -19
- manim/utils/tex.py +130 -198
- manim/utils/tex_file_writing.py +77 -47
- manim/utils/tex_templates.py +2 -1
- manim/utils/unit.py +6 -5
- {manim-0.17.0.dist-info → manim-0.19.1.dist-info}/METADATA +64 -65
- manim-0.19.1.dist-info/RECORD +220 -0
- {manim-0.17.0.dist-info → manim-0.19.1.dist-info}/WHEEL +1 -1
- manim-0.19.1.dist-info/entry_points.txt +3 -0
- {manim-0.17.0.dist-info → manim-0.19.1.dist-info/licenses}/LICENSE.community +1 -1
- manim/cli/new/group.py +0 -189
- manim/communitycolors.py +0 -9
- manim/gui/__init__.py +0 -0
- manim/gui/gui.py +0 -82
- manim/plugins/import_plugins.py +0 -43
- manim/utils/color.py +0 -552
- manim-0.17.0.dist-info/RECORD +0 -206
- manim-0.17.0.dist-info/entry_points.txt +0 -4
- /manim/cli/{new → checkhealth}/__init__.py +0 -0
- {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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
|
53
|
-
return np.
|
|
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
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
135
|
+
list[float]
|
|
123
136
|
Gives back a quaternion from the angle and axis
|
|
124
137
|
"""
|
|
125
|
-
if
|
|
126
|
-
|
|
127
|
-
|
|
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:
|
|
173
|
-
) ->
|
|
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) ->
|
|
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:
|
|
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:
|
|
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(
|
|
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 =
|
|
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(
|
|
295
|
-
axis_x =
|
|
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(
|
|
299
|
-
axis_y = -
|
|
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
|
-
|
|
324
|
-
|
|
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) ->
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
if
|
|
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
|
-
|
|
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(
|
|
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:
|
|
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 = (
|
|
551
|
-
x, y, z =
|
|
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:
|
|
563
|
-
v0s:
|
|
564
|
-
p1s:
|
|
565
|
-
v1s:
|
|
596
|
+
p0s: Point3DLike_Array,
|
|
597
|
+
v0s: Vector3DLike_Array,
|
|
598
|
+
p1s: Point3DLike_Array,
|
|
599
|
+
v1s: Vector3DLike_Array,
|
|
566
600
|
threshold: float = 1e-5,
|
|
567
|
-
) ->
|
|
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(
|
|
579
|
-
normal =
|
|
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
|
-
|
|
647
|
+
val: float = total_angle / TAU
|
|
648
|
+
return val
|
|
614
649
|
|
|
615
650
|
|
|
616
|
-
def shoelace(x_y:
|
|
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
|
-
|
|
661
|
+
val: float = np.trapz(y, x)
|
|
662
|
+
return val
|
|
627
663
|
|
|
628
664
|
|
|
629
|
-
def shoelace_direction(x_y:
|
|
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(
|
|
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(
|
|
706
|
-
|
|
707
|
-
|
|
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:
|
|
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
|
|
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 =
|
|
877
|
+
direction = cross(p1 - p2, norm_vector)
|
|
805
878
|
m = midpoint(p1, p2)
|
|
806
879
|
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
|