manim 0.18.1__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/__main__.py +45 -12
- manim/_config/__init__.py +2 -2
- manim/_config/cli_colors.py +8 -4
- manim/_config/default.cfg +0 -2
- manim/_config/logger_utils.py +5 -0
- manim/_config/utils.py +29 -38
- manim/animation/animation.py +148 -8
- manim/animation/composition.py +16 -13
- manim/animation/creation.py +184 -8
- manim/animation/fading.py +5 -8
- manim/animation/indication.py +93 -26
- manim/animation/movement.py +21 -3
- manim/animation/rotation.py +2 -1
- manim/animation/specialized.py +3 -5
- manim/animation/speedmodifier.py +3 -3
- manim/animation/transform.py +4 -5
- manim/animation/updaters/mobject_update_utils.py +17 -14
- manim/camera/camera.py +2 -2
- manim/cli/__init__.py +17 -0
- manim/cli/cfg/group.py +52 -36
- manim/cli/checkhealth/checks.py +92 -76
- manim/cli/checkhealth/commands.py +12 -5
- manim/cli/default_group.py +148 -24
- manim/cli/init/commands.py +28 -23
- manim/cli/plugins/commands.py +13 -3
- manim/cli/render/commands.py +47 -42
- manim/cli/render/global_options.py +43 -9
- manim/cli/render/render_options.py +84 -19
- manim/constants.py +11 -4
- manim/mobject/frame.py +0 -1
- manim/mobject/geometry/arc.py +109 -75
- manim/mobject/geometry/boolean_ops.py +20 -17
- manim/mobject/geometry/labeled.py +300 -77
- manim/mobject/geometry/line.py +120 -60
- manim/mobject/geometry/polygram.py +109 -25
- manim/mobject/geometry/shape_matchers.py +35 -15
- manim/mobject/geometry/tips.py +36 -27
- manim/mobject/graph.py +48 -40
- manim/mobject/graphing/coordinate_systems.py +110 -45
- manim/mobject/graphing/functions.py +16 -10
- manim/mobject/graphing/number_line.py +23 -9
- manim/mobject/graphing/probability.py +2 -10
- manim/mobject/graphing/scale.py +6 -5
- manim/mobject/matrix.py +17 -19
- manim/mobject/mobject.py +149 -103
- manim/mobject/opengl/opengl_geometry.py +4 -8
- manim/mobject/opengl/opengl_mobject.py +506 -343
- manim/mobject/opengl/opengl_point_cloud_mobject.py +3 -7
- manim/mobject/opengl/opengl_surface.py +1 -2
- manim/mobject/opengl/opengl_vectorized_mobject.py +27 -65
- manim/mobject/svg/brace.py +61 -13
- manim/mobject/svg/svg_mobject.py +2 -1
- manim/mobject/table.py +11 -12
- manim/mobject/text/code_mobject.py +186 -550
- manim/mobject/text/numbers.py +7 -7
- manim/mobject/text/tex_mobject.py +22 -13
- manim/mobject/text/text_mobject.py +29 -20
- manim/mobject/three_d/polyhedra.py +98 -1
- manim/mobject/three_d/three_dimensions.py +59 -31
- manim/mobject/types/image_mobject.py +37 -23
- manim/mobject/types/point_cloud_mobject.py +103 -67
- manim/mobject/types/vectorized_mobject.py +387 -214
- manim/mobject/value_tracker.py +2 -1
- manim/mobject/vector_field.py +2 -4
- manim/opengl/__init__.py +3 -3
- manim/plugins/__init__.py +2 -3
- manim/plugins/plugins_flags.py +3 -3
- manim/renderer/cairo_renderer.py +11 -11
- manim/renderer/opengl_renderer.py +19 -20
- manim/renderer/shader.py +2 -3
- manim/renderer/shader_wrapper.py +3 -2
- manim/scene/moving_camera_scene.py +23 -0
- manim/scene/scene.py +72 -41
- manim/scene/scene_file_writer.py +313 -164
- manim/scene/section.py +15 -15
- manim/scene/three_d_scene.py +8 -15
- manim/scene/vector_space_scene.py +3 -6
- manim/typing.py +326 -66
- manim/utils/bezier.py +1658 -381
- manim/utils/caching.py +11 -5
- manim/utils/color/AS2700.py +2 -0
- manim/utils/color/BS381.py +2 -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 +2 -0
- manim/utils/color/__init__.py +8 -5
- manim/utils/color/core.py +818 -301
- manim/utils/color/manim_colors.py +7 -9
- manim/utils/commands.py +40 -19
- manim/utils/config_ops.py +18 -13
- manim/utils/debug.py +8 -6
- manim/utils/deprecation.py +92 -43
- manim/utils/docbuild/autoaliasattr_directive.py +45 -8
- manim/utils/docbuild/autocolor_directive.py +12 -13
- manim/utils/docbuild/manim_directive.py +35 -29
- manim/utils/docbuild/module_parsing.py +74 -27
- manim/utils/family.py +3 -3
- manim/utils/family_ops.py +12 -4
- manim/utils/file_ops.py +22 -16
- manim/utils/hashing.py +7 -7
- manim/utils/images.py +10 -4
- manim/utils/ipython_magic.py +12 -8
- manim/utils/iterables.py +161 -119
- manim/utils/module_ops.py +55 -19
- manim/utils/opengl.py +68 -23
- manim/utils/parameter_parsing.py +3 -2
- manim/utils/paths.py +11 -5
- manim/utils/polylabel.py +168 -0
- manim/utils/qhull.py +218 -0
- manim/utils/rate_functions.py +69 -32
- manim/utils/simple_functions.py +24 -15
- manim/utils/sounds.py +7 -1
- manim/utils/space_ops.py +48 -37
- 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 +20 -14
- manim/utils/tex.py +4 -2
- manim/utils/tex_file_writing.py +45 -45
- manim/utils/tex_templates.py +1 -1
- manim/utils/unit.py +6 -5
- {manim-0.18.1.dist-info → manim-0.19.0.dist-info}/METADATA +16 -9
- manim-0.19.0.dist-info/RECORD +221 -0
- {manim-0.18.1.dist-info → manim-0.19.0.dist-info}/WHEEL +1 -1
- manim-0.18.1.dist-info/RECORD +0 -217
- {manim-0.18.1.dist-info → manim-0.19.0.dist-info}/LICENSE +0 -0
- {manim-0.18.1.dist-info → manim-0.19.0.dist-info}/LICENSE.community +0 -0
- {manim-0.18.1.dist-info → manim-0.19.0.dist-info}/entry_points.txt +0 -0
manim/utils/opengl.py
CHANGED
|
@@ -1,9 +1,25 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
3
5
|
import numpy as np
|
|
4
6
|
import numpy.linalg as linalg
|
|
5
7
|
|
|
6
|
-
from
|
|
8
|
+
from manim._config import config
|
|
9
|
+
from manim.typing import ManimFloat
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
import numpy.typing as npt
|
|
13
|
+
from typing_extensions import TypeAlias
|
|
14
|
+
|
|
15
|
+
from manim.typing import MatrixMN, Point3D
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from typing_extensions import TypeAlias
|
|
20
|
+
|
|
21
|
+
from manim.typing import MatrixMN
|
|
22
|
+
|
|
7
23
|
|
|
8
24
|
depth = 20
|
|
9
25
|
|
|
@@ -21,18 +37,37 @@ __all__ = [
|
|
|
21
37
|
"view_matrix",
|
|
22
38
|
]
|
|
23
39
|
|
|
40
|
+
FlattenedMatrix4x4: TypeAlias = tuple[
|
|
41
|
+
float,
|
|
42
|
+
float,
|
|
43
|
+
float,
|
|
44
|
+
float,
|
|
45
|
+
float,
|
|
46
|
+
float,
|
|
47
|
+
float,
|
|
48
|
+
float,
|
|
49
|
+
float,
|
|
50
|
+
float,
|
|
51
|
+
float,
|
|
52
|
+
float,
|
|
53
|
+
float,
|
|
54
|
+
float,
|
|
55
|
+
float,
|
|
56
|
+
float,
|
|
57
|
+
]
|
|
58
|
+
|
|
24
59
|
|
|
25
|
-
def matrix_to_shader_input(matrix):
|
|
60
|
+
def matrix_to_shader_input(matrix: MatrixMN) -> FlattenedMatrix4x4:
|
|
26
61
|
return tuple(matrix.T.ravel())
|
|
27
62
|
|
|
28
63
|
|
|
29
64
|
def orthographic_projection_matrix(
|
|
30
|
-
width=None,
|
|
31
|
-
height=None,
|
|
32
|
-
near=1,
|
|
33
|
-
far=depth + 1,
|
|
34
|
-
|
|
35
|
-
):
|
|
65
|
+
width: float | None = None,
|
|
66
|
+
height: float | None = None,
|
|
67
|
+
near: float = 1,
|
|
68
|
+
far: float = depth + 1,
|
|
69
|
+
format_: bool = True,
|
|
70
|
+
) -> MatrixMN | FlattenedMatrix4x4:
|
|
36
71
|
if width is None:
|
|
37
72
|
width = config["frame_width"]
|
|
38
73
|
if height is None:
|
|
@@ -45,13 +80,19 @@ def orthographic_projection_matrix(
|
|
|
45
80
|
[0, 0, 0, 1],
|
|
46
81
|
],
|
|
47
82
|
)
|
|
48
|
-
if
|
|
83
|
+
if format_:
|
|
49
84
|
return matrix_to_shader_input(projection_matrix)
|
|
50
85
|
else:
|
|
51
86
|
return projection_matrix
|
|
52
87
|
|
|
53
88
|
|
|
54
|
-
def perspective_projection_matrix(
|
|
89
|
+
def perspective_projection_matrix(
|
|
90
|
+
width: float | None = None,
|
|
91
|
+
height: float | None = None,
|
|
92
|
+
near: float = 2,
|
|
93
|
+
far: float = 50,
|
|
94
|
+
format_: bool = True,
|
|
95
|
+
) -> MatrixMN | FlattenedMatrix4x4:
|
|
55
96
|
if width is None:
|
|
56
97
|
width = config["frame_width"] / 6
|
|
57
98
|
if height is None:
|
|
@@ -64,13 +105,13 @@ def perspective_projection_matrix(width=None, height=None, near=2, far=50, forma
|
|
|
64
105
|
[0, 0, -1, 0],
|
|
65
106
|
],
|
|
66
107
|
)
|
|
67
|
-
if
|
|
108
|
+
if format_:
|
|
68
109
|
return matrix_to_shader_input(projection_matrix)
|
|
69
110
|
else:
|
|
70
111
|
return projection_matrix
|
|
71
112
|
|
|
72
113
|
|
|
73
|
-
def translation_matrix(x=0, y=0, z=0):
|
|
114
|
+
def translation_matrix(x: float = 0, y: float = 0, z: float = 0) -> MatrixMN:
|
|
74
115
|
return np.array(
|
|
75
116
|
[
|
|
76
117
|
[1, 0, 0, x],
|
|
@@ -78,10 +119,11 @@ def translation_matrix(x=0, y=0, z=0):
|
|
|
78
119
|
[0, 0, 1, z],
|
|
79
120
|
[0, 0, 0, 1],
|
|
80
121
|
],
|
|
122
|
+
dtype=ManimFloat,
|
|
81
123
|
)
|
|
82
124
|
|
|
83
125
|
|
|
84
|
-
def x_rotation_matrix(x=0):
|
|
126
|
+
def x_rotation_matrix(x: float = 0) -> MatrixMN:
|
|
85
127
|
return np.array(
|
|
86
128
|
[
|
|
87
129
|
[1, 0, 0, 0],
|
|
@@ -92,7 +134,7 @@ def x_rotation_matrix(x=0):
|
|
|
92
134
|
)
|
|
93
135
|
|
|
94
136
|
|
|
95
|
-
def y_rotation_matrix(y=0):
|
|
137
|
+
def y_rotation_matrix(y: float = 0) -> MatrixMN:
|
|
96
138
|
return np.array(
|
|
97
139
|
[
|
|
98
140
|
[np.cos(y), 0, np.sin(y), 0],
|
|
@@ -103,7 +145,7 @@ def y_rotation_matrix(y=0):
|
|
|
103
145
|
)
|
|
104
146
|
|
|
105
147
|
|
|
106
|
-
def z_rotation_matrix(z=0):
|
|
148
|
+
def z_rotation_matrix(z: float = 0) -> MatrixMN:
|
|
107
149
|
return np.array(
|
|
108
150
|
[
|
|
109
151
|
[np.cos(z), -np.sin(z), 0, 0],
|
|
@@ -115,7 +157,9 @@ def z_rotation_matrix(z=0):
|
|
|
115
157
|
|
|
116
158
|
|
|
117
159
|
# TODO: When rotating around the x axis, rotation eventually stops.
|
|
118
|
-
def rotate_in_place_matrix(
|
|
160
|
+
def rotate_in_place_matrix(
|
|
161
|
+
initial_position: Point3D, x: float = 0, y: float = 0, z: float = 0
|
|
162
|
+
) -> MatrixMN:
|
|
119
163
|
return np.matmul(
|
|
120
164
|
translation_matrix(*-initial_position),
|
|
121
165
|
np.matmul(
|
|
@@ -125,14 +169,14 @@ def rotate_in_place_matrix(initial_position, x=0, y=0, z=0):
|
|
|
125
169
|
)
|
|
126
170
|
|
|
127
171
|
|
|
128
|
-
def rotation_matrix(x=0, y=0, z=0):
|
|
172
|
+
def rotation_matrix(x: float = 0, y: float = 0, z: float = 0) -> MatrixMN:
|
|
129
173
|
return np.matmul(
|
|
130
174
|
np.matmul(x_rotation_matrix(x), y_rotation_matrix(y)),
|
|
131
175
|
z_rotation_matrix(z),
|
|
132
176
|
)
|
|
133
177
|
|
|
134
178
|
|
|
135
|
-
def scale_matrix(scale_factor=1):
|
|
179
|
+
def scale_matrix(scale_factor: float = 1) -> npt.NDArray:
|
|
136
180
|
return np.array(
|
|
137
181
|
[
|
|
138
182
|
[scale_factor, 0, 0, 0],
|
|
@@ -140,15 +184,16 @@ def scale_matrix(scale_factor=1):
|
|
|
140
184
|
[0, 0, scale_factor, 0],
|
|
141
185
|
[0, 0, 0, 1],
|
|
142
186
|
],
|
|
187
|
+
dtype=ManimFloat,
|
|
143
188
|
)
|
|
144
189
|
|
|
145
190
|
|
|
146
191
|
def view_matrix(
|
|
147
|
-
translation=None,
|
|
148
|
-
x_rotation=0,
|
|
149
|
-
y_rotation=0,
|
|
150
|
-
z_rotation=0,
|
|
151
|
-
):
|
|
192
|
+
translation: Point3D | None = None,
|
|
193
|
+
x_rotation: float = 0,
|
|
194
|
+
y_rotation: float = 0,
|
|
195
|
+
z_rotation: float = 0,
|
|
196
|
+
) -> MatrixMN:
|
|
152
197
|
if translation is None:
|
|
153
198
|
translation = np.array([0, 0, depth / 2 + 1])
|
|
154
199
|
model_matrix = np.matmul(
|
manim/utils/parameter_parsing.py
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from collections.abc import Iterable
|
|
3
4
|
from types import GeneratorType
|
|
4
|
-
from typing import
|
|
5
|
+
from typing import TypeVar
|
|
5
6
|
|
|
6
7
|
T = TypeVar("T")
|
|
7
8
|
|
|
@@ -22,7 +23,7 @@ def flatten_iterable_parameters(
|
|
|
22
23
|
:class:`list`
|
|
23
24
|
The flattened list of parameters.
|
|
24
25
|
"""
|
|
25
|
-
flattened_parameters = []
|
|
26
|
+
flattened_parameters: list[T] = []
|
|
26
27
|
for arg in args:
|
|
27
28
|
if isinstance(arg, (Iterable, GeneratorType)):
|
|
28
29
|
flattened_parameters.extend(arg)
|
manim/utils/paths.py
CHANGED
|
@@ -19,13 +19,13 @@ from ..utils.bezier import interpolate
|
|
|
19
19
|
from ..utils.space_ops import rotation_matrix
|
|
20
20
|
|
|
21
21
|
if TYPE_CHECKING:
|
|
22
|
-
from manim.typing import PathFuncType, Vector3D
|
|
22
|
+
from manim.typing import PathFuncType, Point3D_Array, Vector3D
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
STRAIGHT_PATH_THRESHOLD = 0.01
|
|
26
26
|
|
|
27
27
|
|
|
28
|
-
def straight_path():
|
|
28
|
+
def straight_path() -> PathFuncType:
|
|
29
29
|
"""Simplest path function. Each point in a set goes in a straight path toward its destination.
|
|
30
30
|
|
|
31
31
|
Examples
|
|
@@ -136,7 +136,9 @@ def path_along_circles(
|
|
|
136
136
|
axis = OUT
|
|
137
137
|
unit_axis = axis / np.linalg.norm(axis)
|
|
138
138
|
|
|
139
|
-
def path(
|
|
139
|
+
def path(
|
|
140
|
+
start_points: Point3D_Array, end_points: Point3D_Array, alpha: float
|
|
141
|
+
) -> Point3D_Array:
|
|
140
142
|
detransformed_end_points = circles_centers + np.dot(
|
|
141
143
|
end_points - circles_centers, rotation_matrix(-arc_angle, unit_axis).T
|
|
142
144
|
)
|
|
@@ -206,7 +208,9 @@ def path_along_arc(arc_angle: float, axis: Vector3D = OUT) -> PathFuncType:
|
|
|
206
208
|
axis = OUT
|
|
207
209
|
unit_axis = axis / np.linalg.norm(axis)
|
|
208
210
|
|
|
209
|
-
def path(
|
|
211
|
+
def path(
|
|
212
|
+
start_points: Point3D_Array, end_points: Point3D_Array, alpha: float
|
|
213
|
+
) -> Point3D_Array:
|
|
210
214
|
vects = end_points - start_points
|
|
211
215
|
centers = start_points + 0.5 * vects
|
|
212
216
|
if arc_angle != np.pi:
|
|
@@ -365,7 +369,9 @@ def spiral_path(angle: float, axis: Vector3D = OUT) -> PathFuncType:
|
|
|
365
369
|
axis = OUT
|
|
366
370
|
unit_axis = axis / np.linalg.norm(axis)
|
|
367
371
|
|
|
368
|
-
def path(
|
|
372
|
+
def path(
|
|
373
|
+
start_points: Point3D_Array, end_points: Point3D_Array, alpha: float
|
|
374
|
+
) -> Point3D_Array:
|
|
369
375
|
rot_matrix = rotation_matrix((alpha - 1) * angle, unit_axis)
|
|
370
376
|
return start_points + alpha * np.dot(end_points - start_points, rot_matrix.T)
|
|
371
377
|
|
manim/utils/polylabel.py
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from queue import PriorityQueue
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from collections.abc import Sequence
|
|
11
|
+
|
|
12
|
+
from manim.typing import (
|
|
13
|
+
Point2D,
|
|
14
|
+
Point2D_Array,
|
|
15
|
+
Point2DLike,
|
|
16
|
+
Point2DLike_Array,
|
|
17
|
+
Point3DLike_Array,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Polygon:
|
|
22
|
+
"""
|
|
23
|
+
Initializes the Polygon with the given rings.
|
|
24
|
+
|
|
25
|
+
Parameters
|
|
26
|
+
----------
|
|
27
|
+
rings
|
|
28
|
+
A collection of closed polygonal ring.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(self, rings: Sequence[Point2DLike_Array]) -> None:
|
|
32
|
+
np_rings: list[Point2D_Array] = [np.asarray(ring) for ring in rings]
|
|
33
|
+
# Flatten Array
|
|
34
|
+
csum = np.cumsum([ring.shape[0] for ring in np_rings])
|
|
35
|
+
self.array: Point2D_Array = np.concatenate(np_rings, axis=0)
|
|
36
|
+
|
|
37
|
+
# Compute Boundary
|
|
38
|
+
self.start: Point2D_Array = np.delete(self.array, csum - 1, axis=0)
|
|
39
|
+
self.stop: Point2D_Array = np.delete(self.array, csum % csum[-1], axis=0)
|
|
40
|
+
self.diff: Point2D_Array = np.delete(
|
|
41
|
+
np.diff(self.array, axis=0), csum[:-1] - 1, axis=0
|
|
42
|
+
)
|
|
43
|
+
self.norm: Point2D_Array = self.diff / np.einsum(
|
|
44
|
+
"ij,ij->i", self.diff, self.diff
|
|
45
|
+
).reshape(-1, 1)
|
|
46
|
+
|
|
47
|
+
# Compute Centroid
|
|
48
|
+
x, y = self.start[:, 0], self.start[:, 1]
|
|
49
|
+
xr, yr = self.stop[:, 0], self.stop[:, 1]
|
|
50
|
+
self.area: float = 0.5 * (np.dot(x, yr) - np.dot(xr, y))
|
|
51
|
+
if self.area:
|
|
52
|
+
factor = x * yr - xr * y
|
|
53
|
+
cx = np.sum((x + xr) * factor) / (6.0 * self.area)
|
|
54
|
+
cy = np.sum((y + yr) * factor) / (6.0 * self.area)
|
|
55
|
+
self.centroid = np.array([cx, cy])
|
|
56
|
+
|
|
57
|
+
def compute_distance(self, point: Point2DLike) -> float:
|
|
58
|
+
"""Compute the minimum distance from a point to the polygon."""
|
|
59
|
+
scalars = np.einsum("ij,ij->i", self.norm, point - self.start)
|
|
60
|
+
clips = np.clip(scalars, 0, 1).reshape(-1, 1)
|
|
61
|
+
d: float = np.min(
|
|
62
|
+
np.linalg.norm(self.start + self.diff * clips - point, axis=1)
|
|
63
|
+
)
|
|
64
|
+
return d if self.inside(point) else -d
|
|
65
|
+
|
|
66
|
+
def inside(self, point: Point2DLike) -> bool:
|
|
67
|
+
"""Check if a point is inside the polygon."""
|
|
68
|
+
# Views
|
|
69
|
+
px, py = point
|
|
70
|
+
x, y = self.start[:, 0], self.start[:, 1]
|
|
71
|
+
xr, yr = self.stop[:, 0], self.stop[:, 1]
|
|
72
|
+
|
|
73
|
+
# Count Crossings (enforce short-circuit)
|
|
74
|
+
c = (y > py) != (yr > py)
|
|
75
|
+
c = px < x[c] + (py - y[c]) * (xr[c] - x[c]) / (yr[c] - y[c])
|
|
76
|
+
c_sum: int = np.sum(c)
|
|
77
|
+
return c_sum % 2 == 1
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class Cell:
|
|
81
|
+
"""
|
|
82
|
+
A square in a mesh covering the :class:`~.Polygon` passed as an argument.
|
|
83
|
+
|
|
84
|
+
Parameters
|
|
85
|
+
----------
|
|
86
|
+
c
|
|
87
|
+
Center coordinates of the Cell.
|
|
88
|
+
h
|
|
89
|
+
Half-Size of the Cell.
|
|
90
|
+
polygon
|
|
91
|
+
:class:`~.Polygon` object for which the distance is computed.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
def __init__(self, c: Point2DLike, h: float, polygon: Polygon) -> None:
|
|
95
|
+
self.c: Point2D = np.asarray(c)
|
|
96
|
+
self.h = h
|
|
97
|
+
self.d = polygon.compute_distance(self.c)
|
|
98
|
+
self.p = self.d + self.h * np.sqrt(2)
|
|
99
|
+
|
|
100
|
+
def __lt__(self, other: Cell) -> bool:
|
|
101
|
+
return self.d < other.d
|
|
102
|
+
|
|
103
|
+
def __gt__(self, other: Cell) -> bool:
|
|
104
|
+
return self.d > other.d
|
|
105
|
+
|
|
106
|
+
def __le__(self, other: Cell) -> bool:
|
|
107
|
+
return self.d <= other.d
|
|
108
|
+
|
|
109
|
+
def __ge__(self, other: Cell) -> bool:
|
|
110
|
+
return self.d >= other.d
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def polylabel(rings: Sequence[Point3DLike_Array], precision: float = 0.01) -> Cell:
|
|
114
|
+
"""
|
|
115
|
+
Finds the pole of inaccessibility (the point that is farthest from the edges of the polygon)
|
|
116
|
+
using an iterative grid-based approach.
|
|
117
|
+
|
|
118
|
+
Parameters
|
|
119
|
+
----------
|
|
120
|
+
rings
|
|
121
|
+
A list of lists, where each list is a sequence of points representing the rings of the polygon.
|
|
122
|
+
Typically, multiple rings indicate holes in the polygon.
|
|
123
|
+
precision
|
|
124
|
+
The precision of the result (default is 0.01).
|
|
125
|
+
|
|
126
|
+
Returns
|
|
127
|
+
-------
|
|
128
|
+
Cell
|
|
129
|
+
A Cell containing the pole of inaccessibility to a given precision.
|
|
130
|
+
"""
|
|
131
|
+
# Precompute Polygon Data
|
|
132
|
+
np_rings: list[Point2D_Array] = [np.asarray(ring)[:, :2] for ring in rings]
|
|
133
|
+
polygon = Polygon(np_rings)
|
|
134
|
+
|
|
135
|
+
# Bounding Box
|
|
136
|
+
mins = np.min(polygon.array, axis=0)
|
|
137
|
+
maxs = np.max(polygon.array, axis=0)
|
|
138
|
+
dims = maxs - mins
|
|
139
|
+
s = np.min(dims)
|
|
140
|
+
h = s / 2.0
|
|
141
|
+
|
|
142
|
+
# Initial Grid
|
|
143
|
+
queue: PriorityQueue[Cell] = PriorityQueue()
|
|
144
|
+
xv, yv = np.meshgrid(np.arange(mins[0], maxs[0], s), np.arange(mins[1], maxs[1], s))
|
|
145
|
+
for corner in np.vstack([xv.ravel(), yv.ravel()]).T:
|
|
146
|
+
queue.put(Cell(corner + h, h, polygon))
|
|
147
|
+
|
|
148
|
+
# Initial Guess
|
|
149
|
+
best = Cell(polygon.centroid, 0, polygon)
|
|
150
|
+
bbox = Cell(mins + (dims / 2), 0, polygon)
|
|
151
|
+
if bbox.d > best.d:
|
|
152
|
+
best = bbox
|
|
153
|
+
|
|
154
|
+
# While there are cells to consider...
|
|
155
|
+
directions = np.array([[-1, -1], [1, -1], [-1, 1], [1, 1]])
|
|
156
|
+
while not queue.empty():
|
|
157
|
+
cell = queue.get()
|
|
158
|
+
if cell > best:
|
|
159
|
+
best = cell
|
|
160
|
+
# If a cell is promising, subdivide!
|
|
161
|
+
if cell.p - best.d > precision:
|
|
162
|
+
h = cell.h / 2.0
|
|
163
|
+
offsets = cell.c + directions * h
|
|
164
|
+
queue.put(Cell(offsets[0], h, polygon))
|
|
165
|
+
queue.put(Cell(offsets[1], h, polygon))
|
|
166
|
+
queue.put(Cell(offsets[2], h, polygon))
|
|
167
|
+
queue.put(Cell(offsets[3], h, polygon))
|
|
168
|
+
return best
|
manim/utils/qhull.py
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from manim.typing import PointND, PointND_Array
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class QuickHullPoint:
|
|
13
|
+
def __init__(self, coordinates: PointND_Array) -> None:
|
|
14
|
+
self.coordinates = coordinates
|
|
15
|
+
|
|
16
|
+
def __hash__(self) -> int:
|
|
17
|
+
return hash(self.coordinates.tobytes())
|
|
18
|
+
|
|
19
|
+
def __eq__(self, other: object) -> bool:
|
|
20
|
+
if not isinstance(other, QuickHullPoint):
|
|
21
|
+
raise ValueError
|
|
22
|
+
are_coordinates_equal: bool = np.array_equal(
|
|
23
|
+
self.coordinates, other.coordinates
|
|
24
|
+
)
|
|
25
|
+
return are_coordinates_equal
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class SubFacet:
|
|
29
|
+
def __init__(self, coordinates: PointND_Array) -> None:
|
|
30
|
+
self.coordinates = coordinates
|
|
31
|
+
self.points = frozenset(QuickHullPoint(c) for c in coordinates)
|
|
32
|
+
|
|
33
|
+
def __hash__(self) -> int:
|
|
34
|
+
return hash(self.points)
|
|
35
|
+
|
|
36
|
+
def __eq__(self, other: object) -> bool:
|
|
37
|
+
if not isinstance(other, SubFacet):
|
|
38
|
+
raise ValueError
|
|
39
|
+
return self.points == other.points
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class Facet:
|
|
43
|
+
def __init__(self, coordinates: PointND_Array, internal: PointND) -> None:
|
|
44
|
+
self.coordinates = coordinates
|
|
45
|
+
self.center: PointND = np.mean(coordinates, axis=0)
|
|
46
|
+
self.normal = self.compute_normal(internal)
|
|
47
|
+
self.subfacets = frozenset(
|
|
48
|
+
SubFacet(np.delete(self.coordinates, i, axis=0))
|
|
49
|
+
for i in range(self.coordinates.shape[0])
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
def compute_normal(self, internal: PointND) -> PointND:
|
|
53
|
+
centered = self.coordinates - self.center
|
|
54
|
+
_, _, vh = np.linalg.svd(centered)
|
|
55
|
+
normal: PointND = vh[-1, :]
|
|
56
|
+
normal /= np.linalg.norm(normal)
|
|
57
|
+
|
|
58
|
+
# If the normal points towards the internal point, flip it!
|
|
59
|
+
if np.dot(normal, self.center - internal) < 0:
|
|
60
|
+
normal *= -1
|
|
61
|
+
|
|
62
|
+
return normal
|
|
63
|
+
|
|
64
|
+
def __hash__(self) -> int:
|
|
65
|
+
return hash(self.subfacets)
|
|
66
|
+
|
|
67
|
+
def __eq__(self, other: object) -> bool:
|
|
68
|
+
if not isinstance(other, Facet):
|
|
69
|
+
raise ValueError
|
|
70
|
+
return self.subfacets == other.subfacets
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class Horizon:
|
|
74
|
+
def __init__(self) -> None:
|
|
75
|
+
self.facets: set[Facet] = set()
|
|
76
|
+
self.boundary: list[SubFacet] = []
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class QuickHull:
|
|
80
|
+
"""
|
|
81
|
+
QuickHull algorithm for constructing a convex hull from a set of points.
|
|
82
|
+
|
|
83
|
+
Parameters
|
|
84
|
+
----------
|
|
85
|
+
tolerance
|
|
86
|
+
A tolerance threshold for determining when points lie on the convex hull (default is 1e-5).
|
|
87
|
+
|
|
88
|
+
Attributes
|
|
89
|
+
----------
|
|
90
|
+
facets
|
|
91
|
+
List of facets considered.
|
|
92
|
+
removed
|
|
93
|
+
Set of internal facets that have been removed from the hull during the construction process.
|
|
94
|
+
outside
|
|
95
|
+
Dictionary mapping each facet to its outside points and eye point.
|
|
96
|
+
neighbors
|
|
97
|
+
Mapping of subfacets to their neighboring facets. Each subfacet links precisely two neighbors.
|
|
98
|
+
unclaimed
|
|
99
|
+
Points that have not yet been classified as inside or outside the current hull.
|
|
100
|
+
internal
|
|
101
|
+
An internal point (i.e., the center of the initial simplex) used as a reference during hull construction.
|
|
102
|
+
tolerance
|
|
103
|
+
The tolerance used to determine if points are considered outside the current hull.
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
def __init__(self, tolerance: float = 1e-5) -> None:
|
|
107
|
+
self.facets: list[Facet] = []
|
|
108
|
+
self.removed: set[Facet] = set()
|
|
109
|
+
self.outside: dict[Facet, tuple[PointND_Array | None, PointND | None]] = {}
|
|
110
|
+
self.neighbors: dict[SubFacet, set[Facet]] = {}
|
|
111
|
+
self.unclaimed: PointND_Array | None = None
|
|
112
|
+
self.internal: PointND | None = None
|
|
113
|
+
self.tolerance = tolerance
|
|
114
|
+
|
|
115
|
+
def initialize(self, points: PointND_Array) -> None:
|
|
116
|
+
# Sample Points
|
|
117
|
+
simplex = points[
|
|
118
|
+
np.random.choice(points.shape[0], points.shape[1] + 1, replace=False)
|
|
119
|
+
]
|
|
120
|
+
self.unclaimed = points
|
|
121
|
+
new_internal: PointND = np.mean(simplex, axis=0)
|
|
122
|
+
self.internal = new_internal
|
|
123
|
+
|
|
124
|
+
# Build Simplex
|
|
125
|
+
for c in range(simplex.shape[0]):
|
|
126
|
+
facet = Facet(np.delete(simplex, c, axis=0), internal=new_internal)
|
|
127
|
+
self.classify(facet)
|
|
128
|
+
self.facets.append(facet)
|
|
129
|
+
|
|
130
|
+
# Attach Neighbors
|
|
131
|
+
for f in self.facets:
|
|
132
|
+
for sf in f.subfacets:
|
|
133
|
+
self.neighbors.setdefault(sf, set()).add(f)
|
|
134
|
+
|
|
135
|
+
def classify(self, facet: Facet) -> None:
|
|
136
|
+
assert self.unclaimed is not None, (
|
|
137
|
+
"Call .initialize() before using .classify()."
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
if not self.unclaimed.size:
|
|
141
|
+
self.outside[facet] = (None, None)
|
|
142
|
+
return
|
|
143
|
+
|
|
144
|
+
# Compute Projections
|
|
145
|
+
projections = (self.unclaimed - facet.center) @ facet.normal
|
|
146
|
+
arg = np.argmax(projections)
|
|
147
|
+
mask = projections > self.tolerance
|
|
148
|
+
|
|
149
|
+
# Identify Eye and Outside Set
|
|
150
|
+
eye = self.unclaimed[arg] if projections[arg] > self.tolerance else None
|
|
151
|
+
outside = self.unclaimed[mask]
|
|
152
|
+
self.outside[facet] = (outside, eye)
|
|
153
|
+
self.unclaimed = self.unclaimed[~mask]
|
|
154
|
+
|
|
155
|
+
def compute_horizon(self, eye: PointND, start_facet: Facet) -> Horizon:
|
|
156
|
+
horizon = Horizon()
|
|
157
|
+
self._recursive_horizon(eye, start_facet, horizon)
|
|
158
|
+
return horizon
|
|
159
|
+
|
|
160
|
+
def _recursive_horizon(self, eye: PointND, facet: Facet, horizon: Horizon) -> bool:
|
|
161
|
+
visible = np.dot(facet.normal, eye - facet.center) > 0
|
|
162
|
+
if not visible:
|
|
163
|
+
return False
|
|
164
|
+
|
|
165
|
+
# If the eye is visible from the facet:
|
|
166
|
+
# Label the facet as visible and cross each edge
|
|
167
|
+
horizon.facets.add(facet)
|
|
168
|
+
for subfacet in facet.subfacets:
|
|
169
|
+
neighbor = (self.neighbors[subfacet] - {facet}).pop()
|
|
170
|
+
# If the neighbor is not visible, then the edge shared must be on the boundary
|
|
171
|
+
if neighbor not in horizon.facets and not self._recursive_horizon(
|
|
172
|
+
eye, neighbor, horizon
|
|
173
|
+
):
|
|
174
|
+
horizon.boundary.append(subfacet)
|
|
175
|
+
return True
|
|
176
|
+
|
|
177
|
+
def build(self, points: PointND_Array) -> None:
|
|
178
|
+
num, dim = points.shape
|
|
179
|
+
if (dim == 0) or (num < dim + 1):
|
|
180
|
+
raise ValueError("Not enough points supplied to build Convex Hull!")
|
|
181
|
+
if dim == 1:
|
|
182
|
+
raise ValueError("The Convex Hull of 1D data is its min-max!")
|
|
183
|
+
|
|
184
|
+
self.initialize(points)
|
|
185
|
+
|
|
186
|
+
# This helps the type checker.
|
|
187
|
+
assert self.unclaimed is not None
|
|
188
|
+
assert self.internal is not None
|
|
189
|
+
|
|
190
|
+
while True:
|
|
191
|
+
updated = False
|
|
192
|
+
for facet in self.facets:
|
|
193
|
+
if facet in self.removed:
|
|
194
|
+
continue
|
|
195
|
+
outside, eye = self.outside[facet]
|
|
196
|
+
if eye is not None:
|
|
197
|
+
updated = True
|
|
198
|
+
horizon = self.compute_horizon(eye, facet)
|
|
199
|
+
for f in horizon.facets:
|
|
200
|
+
points_to_append = self.outside[f][0]
|
|
201
|
+
# TODO: is this always true?
|
|
202
|
+
assert points_to_append is not None
|
|
203
|
+
self.unclaimed = np.vstack((self.unclaimed, points_to_append))
|
|
204
|
+
self.removed.add(f)
|
|
205
|
+
for sf in f.subfacets:
|
|
206
|
+
self.neighbors[sf].discard(f)
|
|
207
|
+
if self.neighbors[sf] == set():
|
|
208
|
+
del self.neighbors[sf]
|
|
209
|
+
for sf in horizon.boundary:
|
|
210
|
+
nf = Facet(
|
|
211
|
+
np.vstack((sf.coordinates, eye)), internal=self.internal
|
|
212
|
+
)
|
|
213
|
+
self.classify(nf)
|
|
214
|
+
self.facets.append(nf)
|
|
215
|
+
for nsf in nf.subfacets:
|
|
216
|
+
self.neighbors.setdefault(nsf, set()).add(nf)
|
|
217
|
+
if not updated:
|
|
218
|
+
break
|