manim 0.17.0__py3-none-any.whl → 0.19.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. manim/__init__.py +11 -6
  2. manim/__main__.py +62 -19
  3. manim/_config/__init__.py +10 -9
  4. manim/_config/cli_colors.py +26 -9
  5. manim/_config/default.cfg +1 -3
  6. manim/_config/logger_utils.py +23 -13
  7. manim/_config/utils.py +662 -468
  8. manim/animation/animation.py +164 -18
  9. manim/animation/changing.py +34 -23
  10. manim/animation/composition.py +265 -67
  11. manim/animation/creation.py +208 -26
  12. manim/animation/fading.py +16 -18
  13. manim/animation/growing.py +35 -15
  14. manim/animation/indication.py +150 -76
  15. manim/animation/movement.py +56 -22
  16. manim/animation/numbers.py +64 -6
  17. manim/animation/rotation.py +78 -7
  18. manim/animation/specialized.py +6 -7
  19. manim/animation/speedmodifier.py +13 -10
  20. manim/animation/transform.py +14 -11
  21. manim/animation/transform_matching_parts.py +3 -4
  22. manim/animation/updaters/mobject_update_utils.py +152 -30
  23. manim/animation/updaters/update.py +10 -7
  24. manim/camera/camera.py +182 -118
  25. manim/camera/mapping_camera.py +34 -3
  26. manim/camera/moving_camera.py +95 -74
  27. manim/camera/multi_camera.py +23 -15
  28. manim/camera/three_d_camera.py +70 -52
  29. manim/cli/__init__.py +17 -0
  30. manim/cli/cfg/group.py +76 -44
  31. manim/cli/checkhealth/checks.py +192 -0
  32. manim/cli/checkhealth/commands.py +90 -0
  33. manim/cli/default_group.py +158 -25
  34. manim/cli/init/commands.py +33 -25
  35. manim/cli/plugins/commands.py +16 -3
  36. manim/cli/render/commands.py +72 -60
  37. manim/cli/render/ease_of_access_options.py +4 -3
  38. manim/cli/render/global_options.py +59 -17
  39. manim/cli/render/output_options.py +6 -5
  40. manim/cli/render/render_options.py +98 -33
  41. manim/constants.py +109 -59
  42. manim/data_structures.py +31 -0
  43. manim/mobject/frame.py +8 -5
  44. manim/mobject/geometry/__init__.py +1 -0
  45. manim/mobject/geometry/arc.py +277 -135
  46. manim/mobject/geometry/boolean_ops.py +32 -31
  47. manim/mobject/geometry/labeled.py +376 -0
  48. manim/mobject/geometry/line.py +192 -87
  49. manim/mobject/geometry/polygram.py +224 -58
  50. manim/mobject/geometry/shape_matchers.py +61 -25
  51. manim/mobject/geometry/tips.py +122 -48
  52. manim/mobject/graph.py +1027 -419
  53. manim/mobject/graphing/coordinate_systems.py +533 -278
  54. manim/mobject/graphing/functions.py +53 -32
  55. manim/mobject/graphing/number_line.py +123 -65
  56. manim/mobject/graphing/probability.py +88 -62
  57. manim/mobject/graphing/scale.py +33 -19
  58. manim/mobject/logo.py +118 -28
  59. manim/mobject/matrix.py +87 -83
  60. manim/mobject/mobject.py +912 -442
  61. manim/mobject/opengl/dot_cloud.py +16 -5
  62. manim/mobject/opengl/opengl_compatibility.py +4 -2
  63. manim/mobject/opengl/opengl_geometry.py +254 -153
  64. manim/mobject/opengl/opengl_image_mobject.py +3 -1
  65. manim/mobject/opengl/opengl_mobject.py +779 -482
  66. manim/mobject/opengl/opengl_point_cloud_mobject.py +41 -14
  67. manim/mobject/opengl/opengl_surface.py +14 -92
  68. manim/mobject/opengl/opengl_three_dimensions.py +12 -8
  69. manim/mobject/opengl/opengl_vectorized_mobject.py +98 -100
  70. manim/mobject/svg/brace.py +173 -41
  71. manim/mobject/svg/svg_mobject.py +139 -53
  72. manim/mobject/table.py +61 -68
  73. manim/mobject/text/code_mobject.py +193 -539
  74. manim/mobject/text/numbers.py +81 -34
  75. manim/mobject/text/tex_mobject.py +130 -78
  76. manim/mobject/text/text_mobject.py +288 -164
  77. manim/mobject/three_d/polyhedra.py +111 -13
  78. manim/mobject/three_d/three_d_utils.py +17 -8
  79. manim/mobject/three_d/three_dimensions.py +239 -106
  80. manim/mobject/types/image_mobject.py +50 -30
  81. manim/mobject/types/point_cloud_mobject.py +120 -75
  82. manim/mobject/types/vectorized_mobject.py +841 -408
  83. manim/mobject/value_tracker.py +105 -38
  84. manim/mobject/vector_field.py +50 -31
  85. manim/opengl/__init__.py +3 -3
  86. manim/plugins/__init__.py +14 -1
  87. manim/plugins/plugins_flags.py +10 -14
  88. manim/renderer/cairo_renderer.py +65 -50
  89. manim/renderer/opengl_renderer.py +89 -69
  90. manim/renderer/opengl_renderer_window.py +39 -18
  91. manim/renderer/shader.py +123 -87
  92. manim/renderer/shader_wrapper.py +44 -28
  93. manim/renderer/vectorized_mobject_rendering.py +38 -10
  94. manim/scene/moving_camera_scene.py +32 -3
  95. manim/scene/scene.py +507 -242
  96. manim/scene/scene_file_writer.py +371 -220
  97. manim/scene/section.py +20 -16
  98. manim/scene/three_d_scene.py +14 -22
  99. manim/scene/vector_space_scene.py +223 -129
  100. manim/scene/zoomed_scene.py +46 -41
  101. manim/typing.py +990 -0
  102. manim/utils/bezier.py +1823 -371
  103. manim/utils/caching.py +12 -5
  104. manim/utils/color/AS2700.py +236 -0
  105. manim/utils/color/BS381.py +318 -0
  106. manim/utils/color/DVIPSNAMES.py +96 -0
  107. manim/utils/color/SVGNAMES.py +179 -0
  108. manim/utils/color/X11.py +533 -0
  109. manim/utils/color/XKCD.py +952 -0
  110. manim/utils/color/__init__.py +61 -0
  111. manim/utils/color/core.py +1667 -0
  112. manim/utils/color/manim_colors.py +218 -0
  113. manim/utils/commands.py +48 -20
  114. manim/utils/config_ops.py +39 -19
  115. manim/utils/debug.py +8 -7
  116. manim/utils/deprecation.py +86 -39
  117. manim/utils/docbuild/__init__.py +17 -0
  118. manim/utils/docbuild/autoaliasattr_directive.py +236 -0
  119. manim/utils/docbuild/autocolor_directive.py +99 -0
  120. manim/utils/docbuild/manim_directive.py +94 -41
  121. manim/utils/docbuild/module_parsing.py +245 -0
  122. manim/utils/exceptions.py +6 -0
  123. manim/utils/family.py +5 -3
  124. manim/utils/family_ops.py +17 -4
  125. manim/utils/file_ops.py +27 -17
  126. manim/utils/hashing.py +55 -45
  127. manim/utils/images.py +13 -7
  128. manim/utils/ipython_magic.py +13 -7
  129. manim/utils/iterables.py +163 -120
  130. manim/utils/module_ops.py +66 -24
  131. manim/utils/opengl.py +77 -24
  132. manim/utils/parameter_parsing.py +32 -0
  133. manim/utils/paths.py +30 -33
  134. manim/utils/polylabel.py +235 -0
  135. manim/utils/qhull.py +218 -0
  136. manim/utils/rate_functions.py +98 -32
  137. manim/utils/simple_functions.py +25 -33
  138. manim/utils/sounds.py +7 -1
  139. manim/utils/space_ops.py +188 -115
  140. manim/utils/testing/__init__.py +17 -0
  141. manim/utils/testing/_frames_testers.py +13 -8
  142. manim/utils/testing/_show_diff.py +5 -3
  143. manim/utils/testing/_test_class_makers.py +34 -18
  144. manim/utils/testing/frames_comparison.py +37 -19
  145. manim/utils/tex.py +130 -198
  146. manim/utils/tex_file_writing.py +77 -47
  147. manim/utils/tex_templates.py +2 -1
  148. manim/utils/unit.py +6 -5
  149. {manim-0.17.0.dist-info → manim-0.19.1.dist-info}/METADATA +64 -65
  150. manim-0.19.1.dist-info/RECORD +220 -0
  151. {manim-0.17.0.dist-info → manim-0.19.1.dist-info}/WHEEL +1 -1
  152. manim-0.19.1.dist-info/entry_points.txt +3 -0
  153. {manim-0.17.0.dist-info → manim-0.19.1.dist-info/licenses}/LICENSE.community +1 -1
  154. manim/cli/new/group.py +0 -189
  155. manim/communitycolors.py +0 -9
  156. manim/gui/__init__.py +0 -0
  157. manim/gui/gui.py +0 -82
  158. manim/plugins/import_plugins.py +0 -43
  159. manim/utils/color.py +0 -552
  160. manim-0.17.0.dist-info/RECORD +0 -206
  161. manim-0.17.0.dist-info/entry_points.txt +0 -4
  162. /manim/cli/{new → checkhealth}/__init__.py +0 -0
  163. {manim-0.17.0.dist-info → manim-0.19.1.dist-info/licenses}/LICENSE +0 -0
manim/utils/opengl.py CHANGED
@@ -1,24 +1,67 @@
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 .. import config
8
+ from manim._config import config
9
+ from manim.typing import ManimFloat
7
10
 
8
- depth = 20
11
+ if TYPE_CHECKING:
12
+ import numpy.typing as npt
13
+ from typing_extensions import TypeAlias
14
+
15
+ from manim.typing import MatrixMN, Point3D
9
16
 
10
17
 
11
- def matrix_to_shader_input(matrix):
18
+ depth = 20
19
+
20
+ __all__ = [
21
+ "matrix_to_shader_input",
22
+ "orthographic_projection_matrix",
23
+ "perspective_projection_matrix",
24
+ "translation_matrix",
25
+ "x_rotation_matrix",
26
+ "y_rotation_matrix",
27
+ "z_rotation_matrix",
28
+ "rotate_in_place_matrix",
29
+ "rotation_matrix",
30
+ "scale_matrix",
31
+ "view_matrix",
32
+ ]
33
+
34
+ FlattenedMatrix4x4: TypeAlias = tuple[
35
+ float,
36
+ float,
37
+ float,
38
+ float,
39
+ float,
40
+ float,
41
+ float,
42
+ float,
43
+ float,
44
+ float,
45
+ float,
46
+ float,
47
+ float,
48
+ float,
49
+ float,
50
+ float,
51
+ ]
52
+
53
+
54
+ def matrix_to_shader_input(matrix: MatrixMN) -> FlattenedMatrix4x4:
12
55
  return tuple(matrix.T.ravel())
13
56
 
14
57
 
15
58
  def orthographic_projection_matrix(
16
- width=None,
17
- height=None,
18
- near=1,
19
- far=depth + 1,
20
- format=True,
21
- ):
59
+ width: float | None = None,
60
+ height: float | None = None,
61
+ near: float = 1,
62
+ far: float = depth + 1,
63
+ format_: bool = True,
64
+ ) -> MatrixMN | FlattenedMatrix4x4:
22
65
  if width is None:
23
66
  width = config["frame_width"]
24
67
  if height is None:
@@ -31,13 +74,19 @@ def orthographic_projection_matrix(
31
74
  [0, 0, 0, 1],
32
75
  ],
33
76
  )
34
- if format:
77
+ if format_:
35
78
  return matrix_to_shader_input(projection_matrix)
36
79
  else:
37
80
  return projection_matrix
38
81
 
39
82
 
40
- def perspective_projection_matrix(width=None, height=None, near=2, far=50, format=True):
83
+ def perspective_projection_matrix(
84
+ width: float | None = None,
85
+ height: float | None = None,
86
+ near: float = 2,
87
+ far: float = 50,
88
+ format_: bool = True,
89
+ ) -> MatrixMN | FlattenedMatrix4x4:
41
90
  if width is None:
42
91
  width = config["frame_width"] / 6
43
92
  if height is None:
@@ -50,13 +99,13 @@ def perspective_projection_matrix(width=None, height=None, near=2, far=50, forma
50
99
  [0, 0, -1, 0],
51
100
  ],
52
101
  )
53
- if format:
102
+ if format_:
54
103
  return matrix_to_shader_input(projection_matrix)
55
104
  else:
56
105
  return projection_matrix
57
106
 
58
107
 
59
- def translation_matrix(x=0, y=0, z=0):
108
+ def translation_matrix(x: float = 0, y: float = 0, z: float = 0) -> MatrixMN:
60
109
  return np.array(
61
110
  [
62
111
  [1, 0, 0, x],
@@ -64,10 +113,11 @@ def translation_matrix(x=0, y=0, z=0):
64
113
  [0, 0, 1, z],
65
114
  [0, 0, 0, 1],
66
115
  ],
116
+ dtype=ManimFloat,
67
117
  )
68
118
 
69
119
 
70
- def x_rotation_matrix(x=0):
120
+ def x_rotation_matrix(x: float = 0) -> MatrixMN:
71
121
  return np.array(
72
122
  [
73
123
  [1, 0, 0, 0],
@@ -78,7 +128,7 @@ def x_rotation_matrix(x=0):
78
128
  )
79
129
 
80
130
 
81
- def y_rotation_matrix(y=0):
131
+ def y_rotation_matrix(y: float = 0) -> MatrixMN:
82
132
  return np.array(
83
133
  [
84
134
  [np.cos(y), 0, np.sin(y), 0],
@@ -89,7 +139,7 @@ def y_rotation_matrix(y=0):
89
139
  )
90
140
 
91
141
 
92
- def z_rotation_matrix(z=0):
142
+ def z_rotation_matrix(z: float = 0) -> MatrixMN:
93
143
  return np.array(
94
144
  [
95
145
  [np.cos(z), -np.sin(z), 0, 0],
@@ -101,7 +151,9 @@ def z_rotation_matrix(z=0):
101
151
 
102
152
 
103
153
  # TODO: When rotating around the x axis, rotation eventually stops.
104
- def rotate_in_place_matrix(initial_position, x=0, y=0, z=0):
154
+ def rotate_in_place_matrix(
155
+ initial_position: Point3D, x: float = 0, y: float = 0, z: float = 0
156
+ ) -> MatrixMN:
105
157
  return np.matmul(
106
158
  translation_matrix(*-initial_position),
107
159
  np.matmul(
@@ -111,14 +163,14 @@ def rotate_in_place_matrix(initial_position, x=0, y=0, z=0):
111
163
  )
112
164
 
113
165
 
114
- def rotation_matrix(x=0, y=0, z=0):
166
+ def rotation_matrix(x: float = 0, y: float = 0, z: float = 0) -> MatrixMN:
115
167
  return np.matmul(
116
168
  np.matmul(x_rotation_matrix(x), y_rotation_matrix(y)),
117
169
  z_rotation_matrix(z),
118
170
  )
119
171
 
120
172
 
121
- def scale_matrix(scale_factor=1):
173
+ def scale_matrix(scale_factor: float = 1) -> npt.NDArray:
122
174
  return np.array(
123
175
  [
124
176
  [scale_factor, 0, 0, 0],
@@ -126,15 +178,16 @@ def scale_matrix(scale_factor=1):
126
178
  [0, 0, scale_factor, 0],
127
179
  [0, 0, 0, 1],
128
180
  ],
181
+ dtype=ManimFloat,
129
182
  )
130
183
 
131
184
 
132
185
  def view_matrix(
133
- translation=None,
134
- x_rotation=0,
135
- y_rotation=0,
136
- z_rotation=0,
137
- ):
186
+ translation: Point3D | None = None,
187
+ x_rotation: float = 0,
188
+ y_rotation: float = 0,
189
+ z_rotation: float = 0,
190
+ ) -> MatrixMN:
138
191
  if translation is None:
139
192
  translation = np.array([0, 0, depth / 2 + 1])
140
193
  model_matrix = np.matmul(
@@ -0,0 +1,32 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterable
4
+ from types import GeneratorType
5
+ from typing import TypeVar
6
+
7
+ T = TypeVar("T")
8
+
9
+
10
+ def flatten_iterable_parameters(
11
+ args: Iterable[T | Iterable[T] | GeneratorType],
12
+ ) -> list[T]:
13
+ """Flattens an iterable of parameters into a list of parameters.
14
+
15
+ Parameters
16
+ ----------
17
+ args
18
+ The iterable of parameters to flatten.
19
+ [(generator), [], (), ...]
20
+
21
+ Returns
22
+ -------
23
+ :class:`list`
24
+ The flattened list of parameters.
25
+ """
26
+ flattened_parameters: list[T] = []
27
+ for arg in args:
28
+ if isinstance(arg, (Iterable, GeneratorType)):
29
+ flattened_parameters.extend(arg)
30
+ else:
31
+ flattened_parameters.append(arg)
32
+ return flattened_parameters
manim/utils/paths.py CHANGED
@@ -10,28 +10,27 @@ __all__ = [
10
10
  ]
11
11
 
12
12
 
13
- from typing import Callable
13
+ from typing import TYPE_CHECKING
14
14
 
15
15
  import numpy as np
16
16
 
17
17
  from ..constants import OUT
18
18
  from ..utils.bezier import interpolate
19
- from ..utils.deprecation import deprecated_params
20
- from ..utils.space_ops import rotation_matrix
19
+ from ..utils.space_ops import normalize, rotation_matrix
20
+
21
+ if TYPE_CHECKING:
22
+ from manim.typing import (
23
+ PathFuncType,
24
+ Point3D_Array,
25
+ Point3DLike_Array,
26
+ Vector3DLike,
27
+ )
21
28
 
22
- STRAIGHT_PATH_THRESHOLD = 0.01
23
29
 
24
- PATH_FUNC_TYPE = Callable[[np.ndarray, np.ndarray, float], np.ndarray]
30
+ STRAIGHT_PATH_THRESHOLD = 0.01
25
31
 
26
32
 
27
- # Remove `*args` and the `if` inside the functions when removing deprecation
28
- @deprecated_params(
29
- params="start_points, end_points, alpha",
30
- since="v0.14",
31
- until="v0.15",
32
- message="Straight path is now returning interpolating function to make it consistent with other path functions. Use straight_path()(a,b,c) instead of straight_path(a,b,c).",
33
- )
34
- def straight_path(*args) -> PATH_FUNC_TYPE:
33
+ def straight_path() -> PathFuncType:
35
34
  """Simplest path function. Each point in a set goes in a straight path toward its destination.
36
35
 
37
36
  Examples
@@ -74,14 +73,12 @@ def straight_path(*args) -> PATH_FUNC_TYPE:
74
73
  self.wait()
75
74
 
76
75
  """
77
- if len(args) > 0:
78
- return interpolate(*args)
79
76
  return interpolate
80
77
 
81
78
 
82
79
  def path_along_circles(
83
- arc_angle: float, circles_centers: np.ndarray, axis: np.ndarray = OUT
84
- ) -> PATH_FUNC_TYPE:
80
+ arc_angle: float, circles_centers: Point3DLike_Array, axis: Vector3DLike = OUT
81
+ ) -> PathFuncType:
85
82
  """This function transforms each point by moving it roughly along a circle, each with its own specified center.
86
83
 
87
84
  The path may be seen as each point smoothly changing its orbit from its starting position to its destination.
@@ -140,11 +137,11 @@ def path_along_circles(
140
137
  self.wait()
141
138
 
142
139
  """
143
- if np.linalg.norm(axis) == 0:
144
- axis = OUT
145
- unit_axis = axis / np.linalg.norm(axis)
140
+ unit_axis = normalize(axis, fall_back=OUT)
146
141
 
147
- def path(start_points: np.ndarray, end_points: np.ndarray, alpha: float):
142
+ def path(
143
+ start_points: Point3D_Array, end_points: Point3D_Array, alpha: float
144
+ ) -> Point3D_Array:
148
145
  detransformed_end_points = circles_centers + np.dot(
149
146
  end_points - circles_centers, rotation_matrix(-arc_angle, unit_axis).T
150
147
  )
@@ -158,7 +155,7 @@ def path_along_circles(
158
155
  return path
159
156
 
160
157
 
161
- def path_along_arc(arc_angle: float, axis: np.ndarray = OUT) -> PATH_FUNC_TYPE:
158
+ def path_along_arc(arc_angle: float, axis: Vector3DLike = OUT) -> PathFuncType:
162
159
  """This function transforms each point by moving it along a circular arc.
163
160
 
164
161
  Parameters
@@ -210,11 +207,11 @@ def path_along_arc(arc_angle: float, axis: np.ndarray = OUT) -> PATH_FUNC_TYPE:
210
207
  """
211
208
  if abs(arc_angle) < STRAIGHT_PATH_THRESHOLD:
212
209
  return straight_path()
213
- if np.linalg.norm(axis) == 0:
214
- axis = OUT
215
- unit_axis = axis / np.linalg.norm(axis)
210
+ unit_axis = normalize(axis, fall_back=OUT)
216
211
 
217
- def path(start_points: np.ndarray, end_points: np.ndarray, alpha: float):
212
+ def path(
213
+ start_points: Point3D_Array, end_points: Point3D_Array, alpha: float
214
+ ) -> Point3D_Array:
218
215
  vects = end_points - start_points
219
216
  centers = start_points + 0.5 * vects
220
217
  if arc_angle != np.pi:
@@ -225,7 +222,7 @@ def path_along_arc(arc_angle: float, axis: np.ndarray = OUT) -> PATH_FUNC_TYPE:
225
222
  return path
226
223
 
227
224
 
228
- def clockwise_path() -> PATH_FUNC_TYPE:
225
+ def clockwise_path() -> PathFuncType:
229
226
  """This function transforms each point by moving clockwise around a half circle.
230
227
 
231
228
  Examples
@@ -271,7 +268,7 @@ def clockwise_path() -> PATH_FUNC_TYPE:
271
268
  return path_along_arc(-np.pi)
272
269
 
273
270
 
274
- def counterclockwise_path() -> PATH_FUNC_TYPE:
271
+ def counterclockwise_path() -> PathFuncType:
275
272
  """This function transforms each point by moving counterclockwise around a half circle.
276
273
 
277
274
  Examples
@@ -317,7 +314,7 @@ def counterclockwise_path() -> PATH_FUNC_TYPE:
317
314
  return path_along_arc(np.pi)
318
315
 
319
316
 
320
- def spiral_path(angle: float, axis: np.ndarray = OUT) -> PATH_FUNC_TYPE:
317
+ def spiral_path(angle: float, axis: Vector3DLike = OUT) -> PathFuncType:
321
318
  """This function transforms each point by moving along a spiral to its destination.
322
319
 
323
320
  Parameters
@@ -369,11 +366,11 @@ def spiral_path(angle: float, axis: np.ndarray = OUT) -> PATH_FUNC_TYPE:
369
366
  """
370
367
  if abs(angle) < STRAIGHT_PATH_THRESHOLD:
371
368
  return straight_path()
372
- if np.linalg.norm(axis) == 0:
373
- axis = OUT
374
- unit_axis = axis / np.linalg.norm(axis)
369
+ unit_axis = normalize(axis, fall_back=OUT)
375
370
 
376
- def path(start_points: np.ndarray, end_points: np.ndarray, alpha: float):
371
+ def path(
372
+ start_points: Point3D_Array, end_points: Point3D_Array, alpha: float
373
+ ) -> Point3D_Array:
377
374
  rot_matrix = rotation_matrix((alpha - 1) * angle, unit_axis)
378
375
  return start_points + alpha * np.dot(end_points - start_points, rot_matrix.T)
379
376
 
@@ -0,0 +1,235 @@
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 sequence of points, where each sequence represents the rings of the polygon.
29
+ Typically, multiple rings indicate holes in the polygon.
30
+ """
31
+
32
+ def __init__(self, rings: Sequence[Point2DLike_Array]) -> None:
33
+ np_rings: list[Point2D_Array] = [np.asarray(ring) for ring in rings]
34
+ # Flatten Array
35
+ csum = np.cumsum([ring.shape[0] for ring in np_rings])
36
+ self.array: Point2D_Array = np.concatenate(np_rings, axis=0)
37
+
38
+ # Compute Boundary
39
+ self.start: Point2D_Array = np.delete(self.array, csum - 1, axis=0)
40
+ self.stop: Point2D_Array = np.delete(self.array, csum % csum[-1], axis=0)
41
+ self.diff: Point2D_Array = np.delete(
42
+ np.diff(self.array, axis=0), csum[:-1] - 1, axis=0
43
+ )
44
+ self.norm: Point2D_Array = self.diff / np.einsum(
45
+ "ij,ij->i", self.diff, self.diff
46
+ ).reshape(-1, 1)
47
+
48
+ # Compute Centroid
49
+ x, y = self.start[:, 0], self.start[:, 1]
50
+ xr, yr = self.stop[:, 0], self.stop[:, 1]
51
+ self.area: float = 0.5 * (np.dot(x, yr) - np.dot(xr, y))
52
+ if self.area:
53
+ factor = x * yr - xr * y
54
+ cx = np.sum((x + xr) * factor) / (6.0 * self.area)
55
+ cy = np.sum((y + yr) * factor) / (6.0 * self.area)
56
+ self.centroid = np.array([cx, cy])
57
+
58
+ def compute_distance(self, point: Point2DLike) -> float:
59
+ """Compute the minimum distance from a point to the polygon."""
60
+ scalars = np.einsum("ij,ij->i", self.norm, point - self.start)
61
+ clips = np.clip(scalars, 0, 1).reshape(-1, 1)
62
+ d: float = np.min(
63
+ np.linalg.norm(self.start + self.diff * clips - point, axis=1)
64
+ )
65
+ return d if self.inside(point) else -d
66
+
67
+ def _is_point_on_segment(
68
+ self,
69
+ x_point: float,
70
+ y_point: float,
71
+ x0: float,
72
+ y0: float,
73
+ x1: float,
74
+ y1: float,
75
+ ) -> bool:
76
+ """
77
+ Check if a point is on the segment.
78
+
79
+ The segment is defined by (x0, y0) to (x1, y1).
80
+ """
81
+ if min(x0, x1) <= x_point <= max(x0, x1) and min(y0, y1) <= y_point <= max(
82
+ y0, y1
83
+ ):
84
+ dx = x1 - x0
85
+ dy = y1 - y0
86
+ cross = dx * (y_point - y0) - dy * (x_point - x0)
87
+ return bool(np.isclose(cross, 0.0))
88
+ return False
89
+
90
+ def _ray_crosses_segment(
91
+ self,
92
+ x_point: float,
93
+ y_point: float,
94
+ x0: float,
95
+ y0: float,
96
+ x1: float,
97
+ y1: float,
98
+ ) -> bool:
99
+ """
100
+ Check if a horizontal ray to the right from point (x_point, y_point) crosses the segment.
101
+
102
+ The segment is defined by (x0, y0) to (x1, y1).
103
+ """
104
+ if (y0 > y_point) != (y1 > y_point):
105
+ slope = (x1 - x0) / (y1 - y0)
106
+ x_intersect = slope * (y_point - y0) + x0
107
+ return bool(x_point < x_intersect)
108
+ return False
109
+
110
+ def inside(self, point: Point2DLike) -> bool:
111
+ """
112
+ Check if a point is inside the polygon.
113
+
114
+ Uses ray casting algorithm and checks boundary points consistently.
115
+ """
116
+ point_x, point_y = point
117
+ start_x, start_y = self.start[:, 0], self.start[:, 1]
118
+ stop_x, stop_y = self.stop[:, 0], self.stop[:, 1]
119
+ segment_count = len(start_x)
120
+
121
+ for i in range(segment_count):
122
+ if self._is_point_on_segment(
123
+ point_x,
124
+ point_y,
125
+ start_x[i],
126
+ start_y[i],
127
+ stop_x[i],
128
+ stop_y[i],
129
+ ):
130
+ return True
131
+
132
+ crossings = 0
133
+ for i in range(segment_count):
134
+ if self._ray_crosses_segment(
135
+ point_x,
136
+ point_y,
137
+ start_x[i],
138
+ start_y[i],
139
+ stop_x[i],
140
+ stop_y[i],
141
+ ):
142
+ crossings += 1
143
+
144
+ return crossings % 2 == 1
145
+
146
+
147
+ class Cell:
148
+ """
149
+ A square in a mesh covering the :class:`~.Polygon` passed as an argument.
150
+
151
+ Parameters
152
+ ----------
153
+ c
154
+ Center coordinates of the Cell.
155
+ h
156
+ Half-Size of the Cell.
157
+ polygon
158
+ :class:`~.Polygon` object for which the distance is computed.
159
+ """
160
+
161
+ def __init__(self, c: Point2DLike, h: float, polygon: Polygon) -> None:
162
+ self.c: Point2D = np.asarray(c)
163
+ self.h = h
164
+ self.d = polygon.compute_distance(self.c)
165
+ self.p = self.d + self.h * np.sqrt(2)
166
+
167
+ def __lt__(self, other: Cell) -> bool:
168
+ return self.d < other.d
169
+
170
+ def __gt__(self, other: Cell) -> bool:
171
+ return self.d > other.d
172
+
173
+ def __le__(self, other: Cell) -> bool:
174
+ return self.d <= other.d
175
+
176
+ def __ge__(self, other: Cell) -> bool:
177
+ return self.d >= other.d
178
+
179
+
180
+ def polylabel(rings: Sequence[Point3DLike_Array], precision: float = 0.01) -> Cell:
181
+ """
182
+ Finds the pole of inaccessibility (the point that is farthest from the edges of the polygon)
183
+ using an iterative grid-based approach.
184
+
185
+ Parameters
186
+ ----------
187
+ rings
188
+ A list of lists, where each list is a sequence of points representing the rings of the polygon.
189
+ Typically, multiple rings indicate holes in the polygon.
190
+ precision
191
+ The precision of the result (default is 0.01).
192
+
193
+ Returns
194
+ -------
195
+ Cell
196
+ A Cell containing the pole of inaccessibility to a given precision.
197
+ """
198
+ # Precompute Polygon Data
199
+ np_rings: list[Point2D_Array] = [np.asarray(ring)[:, :2] for ring in rings]
200
+ polygon = Polygon(np_rings)
201
+
202
+ # Bounding Box
203
+ mins = np.min(polygon.array, axis=0)
204
+ maxs = np.max(polygon.array, axis=0)
205
+ dims = maxs - mins
206
+ s = np.min(dims)
207
+ h = s / 2.0
208
+
209
+ # Initial Grid
210
+ queue: PriorityQueue[Cell] = PriorityQueue()
211
+ xv, yv = np.meshgrid(np.arange(mins[0], maxs[0], s), np.arange(mins[1], maxs[1], s))
212
+ for corner in np.vstack([xv.ravel(), yv.ravel()]).T:
213
+ queue.put(Cell(corner + h, h, polygon))
214
+
215
+ # Initial Guess
216
+ best = Cell(polygon.centroid, 0, polygon)
217
+ bbox = Cell(mins + (dims / 2), 0, polygon)
218
+ if bbox.d > best.d:
219
+ best = bbox
220
+
221
+ # While there are cells to consider...
222
+ directions = np.array([[-1, -1], [1, -1], [-1, 1], [1, 1]])
223
+ while not queue.empty():
224
+ cell = queue.get()
225
+ if cell > best:
226
+ best = cell
227
+ # If a cell is promising, subdivide!
228
+ if cell.p - best.d > precision:
229
+ h = cell.h / 2.0
230
+ offsets = cell.c + directions * h
231
+ queue.put(Cell(offsets[0], h, polygon))
232
+ queue.put(Cell(offsets[1], h, polygon))
233
+ queue.put(Cell(offsets[2], h, polygon))
234
+ queue.put(Cell(offsets[3], h, polygon))
235
+ return best