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/bezier.py CHANGED
@@ -5,76 +5,291 @@ from __future__ import annotations
5
5
  __all__ = [
6
6
  "bezier",
7
7
  "partial_bezier_points",
8
- "partial_quadratic_bezier_points",
8
+ "split_bezier",
9
+ "subdivide_bezier",
10
+ "bezier_remap",
9
11
  "interpolate",
10
12
  "integer_interpolate",
11
13
  "mid",
12
14
  "inverse_interpolate",
13
15
  "match_interpolate",
14
- "get_smooth_handle_points",
15
16
  "get_smooth_cubic_bezier_handle_points",
16
- "diag_to_matrix",
17
17
  "is_closed",
18
18
  "proportions_along_bezier_curve_for_point",
19
19
  "point_lies_on_bezier",
20
20
  ]
21
21
 
22
22
 
23
- import typing
23
+ from collections.abc import Callable, Sequence
24
24
  from functools import reduce
25
- from typing import Iterable
25
+ from typing import TYPE_CHECKING, overload
26
26
 
27
27
  import numpy as np
28
- from scipy import linalg
29
28
 
30
- from ..utils.simple_functions import choose
31
- from ..utils.space_ops import cross2d, find_intersection
29
+ from manim.utils.simple_functions import choose
30
+
31
+ if TYPE_CHECKING:
32
+ from manim.typing import (
33
+ BezierPoints,
34
+ BezierPoints_Array,
35
+ BezierPointsLike,
36
+ BezierPointsLike_Array,
37
+ ColVector,
38
+ MatrixMN,
39
+ Point3D,
40
+ Point3D_Array,
41
+ Point3DLike,
42
+ Point3DLike_Array,
43
+ QuadraticBezierPath,
44
+ QuadraticSpline,
45
+ Spline,
46
+ )
47
+
48
+ # l is a commonly used name in linear algebra
49
+ # ruff: noqa: E741
50
+
51
+
52
+ @overload
53
+ def bezier(
54
+ points: BezierPointsLike,
55
+ ) -> Callable[[float | ColVector], Point3D | Point3D_Array]: ...
56
+
57
+
58
+ @overload
59
+ def bezier(
60
+ points: Sequence[Point3DLike_Array],
61
+ ) -> Callable[[float | ColVector], Point3D_Array]: ...
32
62
 
33
63
 
34
64
  def bezier(
35
- points: np.ndarray,
36
- ) -> typing.Callable[[float], int | typing.Iterable]:
37
- """Classic implementation of a bezier curve.
65
+ points: Point3D_Array | Sequence[Point3D_Array],
66
+ ) -> Callable[[float | ColVector], Point3D_Array]:
67
+ """Classic implementation of a Bézier curve.
38
68
 
39
69
  Parameters
40
70
  ----------
41
71
  points
42
- points defining the desired bezier curve.
72
+ :math:`(d+1, 3)`-shaped array of :math:`d+1` control points defining a single Bézier
73
+ curve of degree :math:`d`. Alternatively, for vectorization purposes, ``points`` can
74
+ also be a :math:`(d+1, M, 3)`-shaped sequence of :math:`d+1` arrays of :math:`M`
75
+ control points each, which define `M` Bézier curves instead.
43
76
 
44
77
  Returns
45
78
  -------
46
- typing.Callable[[float], typing.Union[int, typing.Iterable]]
47
- function describing the bezier curve.
79
+ bezier_func : :class:`typing.Callable` [[:class:`float` | :class:`~.ColVector`], :class:`~.Point3D` | :class:`~.Point3D_Array`]
80
+ Function describing the Bézier curve. The behaviour of this function depends on
81
+ the shape of ``points``:
82
+
83
+ * If ``points`` was a :math:`(d+1, 3)` array representing a single Bézier curve,
84
+ then ``bezier_func`` can receive either:
85
+
86
+ * a :class:`float` ``t``, in which case it returns a
87
+ single :math:`(1, 3)`-shaped :class:`~.Point3D` representing the evaluation
88
+ of the Bézier at ``t``, or
89
+ * an :math:`(n, 1)`-shaped :class:`~.ColVector`
90
+ containing :math:`n` values to evaluate the Bézier curve at, returning instead
91
+ an :math:`(n, 3)`-shaped :class:`~.Point3D_Array` containing the points
92
+ resulting from evaluating the Bézier at each of the :math:`n` values.
93
+ .. warning::
94
+ If passing a vector of :math:`t`-values to ``bezier_func``, it **must**
95
+ be a column vector/matrix of shape :math:`(n, 1)`. Passing an 1D array of
96
+ shape :math:`(n,)` is not supported and **will result in undefined behaviour**.
97
+
98
+ * If ``points`` was a :math:`(d+1, M, 3)` array describing :math:`M` Bézier curves,
99
+ then ``bezier_func`` can receive either:
100
+
101
+ * a :class:`float` ``t``, in which case it returns an
102
+ :math:`(M, 3)`-shaped :class:`~.Point3D_Array` representing the evaluation
103
+ of the :math:`M` Bézier curves at the same value ``t``, or
104
+ * an :math:`(M, 1)`-shaped
105
+ :class:`~.ColVector` containing :math:`M` values, such that the :math:`i`-th
106
+ Bézier curve defined by ``points`` is evaluated at the corresponding :math:`i`-th
107
+ value in ``t``, returning again an :math:`(M, 3)`-shaped :class:`~.Point3D_Array`
108
+ containing those :math:`M` evaluations.
109
+ .. warning::
110
+ Unlike the previous case, if you pass a :class:`~.ColVector` to ``bezier_func``,
111
+ it **must** contain exactly :math:`M` values, each value for each of the :math:`M`
112
+ Bézier curves defined by ``points``. Any array of shape other than :math:`(M, 1)`
113
+ **will result in undefined behaviour**.
48
114
  """
49
- n = len(points) - 1
50
-
51
- # Cubic Bezier curve
52
- if n == 3:
53
- return (
54
- lambda t: (1 - t) ** 3 * points[0]
55
- + 3 * t * (1 - t) ** 2 * points[1]
56
- + 3 * (1 - t) * t**2 * points[2]
57
- + t**3 * points[3]
58
- )
59
- # Quadratic Bezier curve
60
- if n == 2:
61
- return (
62
- lambda t: (1 - t) ** 2 * points[0]
63
- + 2 * t * (1 - t) * points[1]
64
- + t**2 * points[2]
65
- )
115
+ P = np.asarray(points)
116
+ degree = P.shape[0] - 1
66
117
 
67
- return lambda t: sum(
68
- ((1 - t) ** (n - k)) * (t**k) * choose(n, k) * point
69
- for k, point in enumerate(points)
70
- )
118
+ if degree == 0:
119
+
120
+ def zero_bezier(t: float | ColVector) -> Point3D | Point3D_Array:
121
+ return np.ones_like(t) * P[0]
71
122
 
123
+ return zero_bezier
72
124
 
73
- def partial_bezier_points(points: np.ndarray, a: float, b: float) -> np.ndarray:
74
- """Given an array of points which define bezier curve, and two numbers 0<=a<b<=1, return an array of the same size,
75
- which describes the portion of the original bezier curve on the interval [a, b].
125
+ if degree == 1:
76
126
 
77
- This algorithm is pretty nifty, and pretty dense.
127
+ def linear_bezier(t: float | ColVector) -> Point3D | Point3D_Array:
128
+ return P[0] + t * (P[1] - P[0])
129
+
130
+ return linear_bezier
131
+
132
+ if degree == 2:
133
+
134
+ def quadratic_bezier(t: float | ColVector) -> Point3D | Point3D_Array:
135
+ t2 = t * t
136
+ mt = 1 - t
137
+ mt2 = mt * mt
138
+ return mt2 * P[0] + 2 * t * mt * P[1] + t2 * P[2]
139
+
140
+ return quadratic_bezier
141
+
142
+ if degree == 3:
143
+
144
+ def cubic_bezier(t: float | ColVector) -> Point3D | Point3D_Array:
145
+ t2 = t * t
146
+ t3 = t2 * t
147
+ mt = 1 - t
148
+ mt2 = mt * mt
149
+ mt3 = mt2 * mt
150
+ return mt3 * P[0] + 3 * t * mt2 * P[1] + 3 * t2 * mt * P[2] + t3 * P[3]
151
+
152
+ return cubic_bezier
153
+
154
+ def nth_grade_bezier(t: float | ColVector) -> Point3D | Point3D_Array:
155
+ is_scalar = not isinstance(t, np.ndarray)
156
+ if is_scalar:
157
+ B = np.empty((1, *P.shape))
158
+ else:
159
+ assert isinstance(t, np.ndarray)
160
+ t = t.reshape(-1, *[1 for dim in P.shape])
161
+ B = np.empty((t.shape[0], *P.shape))
162
+ B[:] = P
163
+
164
+ for i in range(degree):
165
+ # After the i-th iteration (i in [0, ..., d-1]) there are evaluations at t
166
+ # of (d-i) Bezier curves of grade (i+1), stored in the first d-i slots of B
167
+ B[:, : degree - i] += t * (B[:, 1 : degree - i + 1] - B[:, : degree - i])
168
+
169
+ # In the end, there shall be the evaluation at t of a single Bezier curve of
170
+ # grade d, stored in the first slot of B
171
+ if is_scalar:
172
+ val: Point3D = B[0, 0]
173
+ return val
174
+ return B[:, 0]
175
+
176
+ return nth_grade_bezier
177
+
178
+
179
+ def partial_bezier_points(points: BezierPointsLike, a: float, b: float) -> BezierPoints:
180
+ r"""Given an array of ``points`` which define a Bézier curve, and two numbers :math:`a, b`
181
+ such that :math:`0 \le a < b \le 1`, return an array of the same size, which describes the
182
+ portion of the original Bézier curve on the interval :math:`[a, b]`.
183
+
184
+ :func:`partial_bezier_points` is conceptually equivalent to calling :func:`split_bezier`
185
+ twice and discarding unused Bézier curves, but this is more efficient and doesn't waste
186
+ computations.
187
+
188
+ .. seealso::
189
+ See :func:`split_bezier` for an explanation on how to split Bézier curves.
190
+
191
+ .. note::
192
+ To find the portion of a Bézier curve with :math:`t` between :math:`a` and :math:`b`:
193
+
194
+ 1. Split the curve at :math:`t = a` and extract its 2nd subcurve.
195
+ 2. We cannot evaluate the new subcurve at :math:`t = b` because its range of values for :math:`t` is different.
196
+ To find the correct value, we need to transform the interval :math:`[a, 1]` into :math:`[0, 1]`
197
+ by first subtracting :math:`a` to get :math:`[0, 1-a]` and then dividing by :math:`1-a`. Thus, our new
198
+ value must be :math:`t = \frac{b - a}{1 - a}`. Define :math:`u = \frac{b - a}{1 - a}`.
199
+ 3. Split the subcurve at :math:`t = u` and extract its 1st subcurve.
200
+
201
+ The final portion is a linear combination of points, and thus the process can be
202
+ summarized as a linear transformation by some matrix in terms of :math:`a` and :math:`b`.
203
+ This matrix is given explicitly for Bézier curves up to degree 3, which are often used in Manim.
204
+ For higher degrees, the algorithm described previously is used.
205
+
206
+ For the case of a quadratic Bézier curve:
207
+
208
+ * Step 1:
209
+
210
+ .. math::
211
+ H'_1
212
+ =
213
+ \begin{pmatrix}
214
+ (1-a)^2 & 2(1-a)a & a^2 \\
215
+ 0 & (1-a) & a \\
216
+ 0 & 0 & 1
217
+ \end{pmatrix}
218
+ \begin{pmatrix}
219
+ p_0 \\
220
+ p_1 \\
221
+ p_2
222
+ \end{pmatrix}
223
+
224
+ * Step 2:
225
+
226
+ .. math::
227
+ H''_0
228
+ &=
229
+ \begin{pmatrix}
230
+ 1 & 0 & 0 \\
231
+ (1-u) & u & 0\\
232
+ (1-u)^2 & 2(1-u)u & u^2
233
+ \end{pmatrix}
234
+ H'_1
235
+ \\
236
+ &
237
+ \\
238
+ &=
239
+ \begin{pmatrix}
240
+ 1 & 0 & 0 \\
241
+ (1-u) & u & 0\\
242
+ (1-u)^2 & 2(1-u)u & u^2
243
+ \end{pmatrix}
244
+ \begin{pmatrix}
245
+ (1-a)^2 & 2(1-a)a & a^2 \\
246
+ 0 & (1-a) & a \\
247
+ 0 & 0 & 1
248
+ \end{pmatrix}
249
+ \begin{pmatrix}
250
+ p_0 \\
251
+ p_1 \\
252
+ p_2
253
+ \end{pmatrix}
254
+ \\
255
+ &
256
+ \\
257
+ &=
258
+ \begin{pmatrix}
259
+ (1-a)^2 & 2(1-a)a & a^2 \\
260
+ (1-a)(1-b) & a(1-b) + (1-a)b & ab \\
261
+ (1-b)^2 & 2(1-b)b & b^2
262
+ \end{pmatrix}
263
+ \begin{pmatrix}
264
+ p_0 \\
265
+ p_1 \\
266
+ p_2
267
+ \end{pmatrix}
268
+
269
+ from where one can define a :math:`(3, 3)` matrix :math:`P_2` which, when applied over
270
+ the array of ``points``, will return the desired partial quadratic Bézier curve:
271
+
272
+ .. math::
273
+ P_2
274
+ =
275
+ \begin{pmatrix}
276
+ (1-a)^2 & 2(1-a)a & a^2 \\
277
+ (1-a)(1-b) & a(1-b) + (1-a)b & ab \\
278
+ (1-b)^2 & 2(1-b)b & b^2
279
+ \end{pmatrix}
280
+
281
+ Similarly, for the cubic Bézier curve case, one can define the following
282
+ :math:`(4, 4)` matrix :math:`P_3`:
283
+
284
+ .. math::
285
+ P_3
286
+ =
287
+ \begin{pmatrix}
288
+ (1-a)^3 & 3(1-a)^2a & 3(1-a)a^2 & a^3 \\
289
+ (1-a)^2(1-b) & 2(1-a)a(1-b) + (1-a)^2b & a^2(1-b) + 2(1-a)ab & a^2b \\
290
+ (1-a)(1-b)^2 & a(1-b)^2 + 2(1-a)(1-b)b & 2a(1-b)b + (1-a)b^2 & ab^2 \\
291
+ (1-b)^3 & 3(1-b)^2b & 3(1-b)b^2 & b^3
292
+ \end{pmatrix}
78
293
 
79
294
  Parameters
80
295
  ----------
@@ -87,154 +302,763 @@ def partial_bezier_points(points: np.ndarray, a: float, b: float) -> np.ndarray:
87
302
 
88
303
  Returns
89
304
  -------
90
- np.ndarray
91
- Set of points defining the partial bezier curve.
305
+ :class:`~.BezierPoints`
306
+ An array containing the control points defining the partial Bézier curve.
92
307
  """
308
+ # Border cases
93
309
  if a == 1:
94
- return [points[-1]] * len(points)
95
-
96
- a_to_1 = np.array([bezier(points[i:])(a) for i in range(len(points))])
97
- end_prop = (b - a) / (1.0 - a)
98
- return np.array([bezier(a_to_1[: i + 1])(end_prop) for i in range(len(points))])
99
-
100
-
101
- # Shortened version of partial_bezier_points just for quadratics,
102
- # since this is called a fair amount
103
- def partial_quadratic_bezier_points(points, a, b):
104
- if a == 1:
105
- return 3 * [points[-1]]
106
-
107
- def curve(t):
108
- return (
109
- points[0] * (1 - t) * (1 - t)
110
- + 2 * points[1] * t * (1 - t)
111
- + points[2] * t * t
310
+ arr = np.array(points)
311
+ arr[:] = arr[-1]
312
+ return arr
313
+ if b == 0:
314
+ arr = np.array(points)
315
+ arr[:] = arr[0]
316
+ return arr
317
+
318
+ points = np.asarray(points)
319
+ degree = points.shape[0] - 1
320
+
321
+ if degree == 3:
322
+ ma, mb = 1 - a, 1 - b
323
+ a2, b2, ma2, mb2 = a * a, b * b, ma * ma, mb * mb
324
+ a3, b3, ma3, mb3 = a2 * a, b2 * b, ma2 * ma, mb2 * mb
325
+
326
+ portion_matrix = np.array(
327
+ [
328
+ [ma3, 3 * ma2 * a, 3 * ma * a2, a3],
329
+ [ma2 * mb, 2 * ma * a * mb + ma2 * b, a2 * mb + 2 * ma * a * b, a2 * b],
330
+ [ma * mb2, a * mb2 + 2 * ma * mb * b, 2 * a * mb * b + ma * b2, a * b2],
331
+ [mb3, 3 * mb2 * b, 3 * mb * b2, b3],
332
+ ]
112
333
  )
334
+ return portion_matrix @ points
113
335
 
114
- # bezier(points)
115
- h0 = curve(a) if a > 0 else points[0]
116
- h2 = curve(b) if b < 1 else points[2]
117
- h1_prime = (1 - a) * points[1] + a * points[2]
118
- end_prop = (b - a) / (1.0 - a)
119
- h1 = (1 - end_prop) * h0 + end_prop * h1_prime
120
- return [h0, h1, h2]
336
+ if degree == 2:
337
+ ma, mb = 1 - a, 1 - b
121
338
 
339
+ portion_matrix = np.array(
340
+ [
341
+ [ma * ma, 2 * a * ma, a * a],
342
+ [ma * mb, a * mb + ma * b, a * b],
343
+ [mb * mb, 2 * b * mb, b * b],
344
+ ]
345
+ )
346
+ return portion_matrix @ points
347
+
348
+ if degree == 1:
349
+ direction = points[1] - points[0]
350
+ return np.array(
351
+ [
352
+ points[0] + a * direction,
353
+ points[0] + b * direction,
354
+ ]
355
+ )
122
356
 
123
- def split_quadratic_bezier(points: Iterable[float], t: float) -> np.ndarray:
124
- """Split a quadratic Bézier curve at argument ``t`` into two quadratic curves.
357
+ if degree == 0:
358
+ return points
359
+
360
+ # Fallback case for nth degree Béziers
361
+ # It is convenient that np.array copies points
362
+ arr = np.array(points, dtype=float)
363
+ N = arr.shape[0]
364
+
365
+ # Current state for an example Bézier curve C0 = [P0, P1, P2, P3]:
366
+ # arr = [P0, P1, P2, P3]
367
+ if a != 0:
368
+ for i in range(1, N):
369
+ # 1st iter: arr = [L0(a), L1(a), L2(a), P3]
370
+ # 2nd iter: arr = [Q0(a), Q1(a), L2(a), P3]
371
+ # 3rd iter: arr = [C0(a), Q1(a), L2(a), P3]
372
+ arr[: N - i] += a * (arr[1 : N - i + 1] - arr[: N - i])
373
+
374
+ # For faster calculations we shall define mu = 1 - u = (1 - b) / (1 - a).
375
+ # This is because:
376
+ # L0'(u) = P0' + u(P1' - P0')
377
+ # = (1-u)P0' + uP1'
378
+ # = muP0' + (1-mu)P1'
379
+ # = P1' + mu(P0' - P1)
380
+ # In this way, one can do something similar to the first loop.
381
+ #
382
+ # Current state:
383
+ # arr = [C0(a), Q1(a), L2(a), P3]
384
+ # = [P0', P1', P2', P3']
385
+ if b != 1:
386
+ mu = (1 - b) / (1 - a)
387
+ for i in range(1, N):
388
+ # 1st iter: arr = [P0', L0'(u), L1'(u), L2'(u)]
389
+ # 2nd iter: arr = [P0', L0'(u), Q0'(u), Q1'(u)]
390
+ # 3rd iter: arr = [P0', L0'(u), Q0'(u), C0'(u)]
391
+ arr[i:] += mu * (arr[i - 1 : -1] - arr[i:])
392
+
393
+ return arr
394
+
395
+
396
+ def split_bezier(points: BezierPointsLike, t: float) -> Spline:
397
+ r"""Split a Bézier curve at argument ``t`` into two curves.
398
+
399
+ .. note::
400
+
401
+ .. seealso::
402
+ `A Primer on Bézier Curves #10: Splitting curves. Pomax. <https://pomax.github.io/bezierinfo/#splitting>`_
403
+
404
+ As an example for a cubic Bézier curve, let :math:`p_0, p_1, p_2, p_3` be the points
405
+ needed for the curve :math:`C_0 = [p_0, \ p_1, \ p_2, \ p_3]`.
406
+
407
+ Define the 3 linear Béziers :math:`L_0, L_1, L_2` as interpolations of :math:`p_0, p_1, p_2, p_3`:
408
+
409
+ .. math::
410
+ L_0(t) &= p_0 + t(p_1 - p_0) \\
411
+ L_1(t) &= p_1 + t(p_2 - p_1) \\
412
+ L_2(t) &= p_2 + t(p_3 - p_2)
413
+
414
+ Define the 2 quadratic Béziers :math:`Q_0, Q_1` as interpolations of :math:`L_0, L_1, L_2`:
415
+
416
+ .. math::
417
+ Q_0(t) &= L_0(t) + t(L_1(t) - L_0(t)) \\
418
+ Q_1(t) &= L_1(t) + t(L_2(t) - L_1(t))
419
+
420
+ Then :math:`C_0` is the following interpolation of :math:`Q_0` and :math:`Q_1`:
421
+
422
+ .. math::
423
+ C_0(t) = Q_0(t) + t(Q_1(t) - Q_0(t))
424
+
425
+ Evaluating :math:`C_0` at a value :math:`t=t'` splits :math:`C_0` into two cubic Béziers :math:`H_0`
426
+ and :math:`H_1`, defined by some of the points we calculated earlier:
427
+
428
+ .. math::
429
+ H_0 &= [p_0, &\ L_0(t'), &\ Q_0(t'), &\ C_0(t') &] \\
430
+ H_1 &= [p_0(t'), &\ Q_1(t'), &\ L_2(t'), &\ p_3 &]
431
+
432
+ As the resulting curves are obtained from linear combinations of ``points``, everything can
433
+ be encoded into a matrix for efficiency, which is done for Bézier curves of degree up to 3.
434
+
435
+ .. seealso::
436
+ `A Primer on Bézier Curves #11: Splitting curves using matrices. Pomax. <https://pomax.github.io/bezierinfo/#matrixsplit>`_
437
+
438
+ For the simpler case of a quadratic Bézier curve:
439
+
440
+ .. math::
441
+ H_0
442
+ &=
443
+ \begin{pmatrix}
444
+ p_0 \\
445
+ (1-t) p_0 + t p_1 \\
446
+ (1-t)^2 p_0 + 2(1-t)t p_1 + t^2 p_2 \\
447
+ \end{pmatrix}
448
+ &=
449
+ \begin{pmatrix}
450
+ 1 & 0 & 0 \\
451
+ (1-t) & t & 0\\
452
+ (1-t)^2 & 2(1-t)t & t^2
453
+ \end{pmatrix}
454
+ \begin{pmatrix}
455
+ p_0 \\
456
+ p_1 \\
457
+ p_2
458
+ \end{pmatrix}
459
+ \\
460
+ &
461
+ \\
462
+ H_1
463
+ &=
464
+ \begin{pmatrix}
465
+ (1-t)^2 p_0 + 2(1-t)t p_1 + t^2 p_2 \\
466
+ (1-t) p_1 + t p_2 \\
467
+ p_2
468
+ \end{pmatrix}
469
+ &=
470
+ \begin{pmatrix}
471
+ (1-t)^2 & 2(1-t)t & t^2 \\
472
+ 0 & (1-t) & t \\
473
+ 0 & 0 & 1
474
+ \end{pmatrix}
475
+ \begin{pmatrix}
476
+ p_0 \\
477
+ p_1 \\
478
+ p_2
479
+ \end{pmatrix}
480
+
481
+ from where one can define a :math:`(6, 3)` split matrix :math:`S_2` which can multiply
482
+ the array of ``points`` to compute the return value:
483
+
484
+ .. math::
485
+ S_2
486
+ &=
487
+ \begin{pmatrix}
488
+ 1 & 0 & 0 \\
489
+ (1-t) & t & 0 \\
490
+ (1-t)^2 & 2(1-t)t & t^2 \\
491
+ (1-t)^2 & 2(1-t)t & t^2 \\
492
+ 0 & (1-t) & t \\
493
+ 0 & 0 & 1
494
+ \end{pmatrix}
495
+ \\
496
+ &
497
+ \\
498
+ S_2 P
499
+ &=
500
+ \begin{pmatrix}
501
+ 1 & 0 & 0 \\
502
+ (1-t) & t & 0 \\
503
+ (1-t)^2 & 2(1-t)t & t^2 \\
504
+ (1-t)^2 & 2(1-t)t & t^2 \\
505
+ 0 & (1-t) & t \\
506
+ 0 & 0 & 1
507
+ \end{pmatrix}
508
+ \begin{pmatrix}
509
+ p_0 \\
510
+ p_1 \\
511
+ p_2
512
+ \end{pmatrix}
513
+ =
514
+ \begin{pmatrix}
515
+ \vert \\
516
+ H_0 \\
517
+ \vert \\
518
+ \vert \\
519
+ H_1 \\
520
+ \vert
521
+ \end{pmatrix}
522
+
523
+ For the previous example with a cubic Bézier curve:
524
+
525
+ .. math::
526
+ H_0
527
+ &=
528
+ \begin{pmatrix}
529
+ p_0 \\
530
+ (1-t) p_0 + t p_1 \\
531
+ (1-t)^2 p_0 + 2(1-t)t p_1 + t^2 p_2 \\
532
+ (1-t)^3 p_0 + 3(1-t)^2 t p_1 + 3(1-t)t^2 p_2 + t^3 p_3
533
+ \end{pmatrix}
534
+ &=
535
+ \begin{pmatrix}
536
+ 1 & 0 & 0 & 0 \\
537
+ (1-t) & t & 0 & 0 \\
538
+ (1-t)^2 & 2(1-t)t & t^2 & 0 \\
539
+ (1-t)^3 & 3(1-t)^2 t & 3(1-t)t^2 & t^3
540
+ \end{pmatrix}
541
+ \begin{pmatrix}
542
+ p_0 \\
543
+ p_1 \\
544
+ p_2 \\
545
+ p_3
546
+ \end{pmatrix}
547
+ \\
548
+ &
549
+ \\
550
+ H_1
551
+ &=
552
+ \begin{pmatrix}
553
+ (1-t)^3 p_0 + 3(1-t)^2 t p_1 + 3(1-t)t^2 p_2 + t^3 p_3 \\
554
+ (1-t)^2 p_1 + 2(1-t)t p_2 + t^2 p_3 \\
555
+ (1-t) p_2 + t p_3 \\
556
+ p_3
557
+ \end{pmatrix}
558
+ &=
559
+ \begin{pmatrix}
560
+ (1-t)^3 & 3(1-t)^2 t & 3(1-t)t^2 & t^3 \\
561
+ 0 & (1-t)^2 & 2(1-t)t & t^2 \\
562
+ 0 & 0 & (1-t) & t \\
563
+ 0 & 0 & 0 & 1
564
+ \end{pmatrix}
565
+ \begin{pmatrix}
566
+ p_0 \\
567
+ p_1 \\
568
+ p_2 \\
569
+ p_3
570
+ \end{pmatrix}
571
+
572
+ from where one can define a :math:`(8, 4)` split matrix :math:`S_3` which can multiply
573
+ the array of ``points`` to compute the return value:
574
+
575
+ .. math::
576
+ S_3
577
+ &=
578
+ \begin{pmatrix}
579
+ 1 & 0 & 0 & 0 \\
580
+ (1-t) & t & 0 & 0 \\
581
+ (1-t)^2 & 2(1-t)t & t^2 & 0 \\
582
+ (1-t)^3 & 3(1-t)^2 t & 3(1-t)t^2 & t^3 \\
583
+ (1-t)^3 & 3(1-t)^2 t & 3(1-t)t^2 & t^3 \\
584
+ 0 & (1-t)^2 & 2(1-t)t & t^2 \\
585
+ 0 & 0 & (1-t) & t \\
586
+ 0 & 0 & 0 & 1
587
+ \end{pmatrix}
588
+ \\
589
+ &
590
+ \\
591
+ S_3 P
592
+ &=
593
+ \begin{pmatrix}
594
+ 1 & 0 & 0 & 0 \\
595
+ (1-t) & t & 0 & 0 \\
596
+ (1-t)^2 & 2(1-t)t & t^2 & 0 \\
597
+ (1-t)^3 & 3(1-t)^2 t & 3(1-t)t^2 & t^3 \\
598
+ (1-t)^3 & 3(1-t)^2 t & 3(1-t)t^2 & t^3 \\
599
+ 0 & (1-t)^2 & 2(1-t)t & t^2 \\
600
+ 0 & 0 & (1-t) & t \\
601
+ 0 & 0 & 0 & 1
602
+ \end{pmatrix}
603
+ \begin{pmatrix}
604
+ p_0 \\
605
+ p_1 \\
606
+ p_2 \\
607
+ p_3
608
+ \end{pmatrix}
609
+ =
610
+ \begin{pmatrix}
611
+ \vert \\
612
+ H_0 \\
613
+ \vert \\
614
+ \vert \\
615
+ H_1 \\
616
+ \vert
617
+ \end{pmatrix}
125
618
 
126
619
  Parameters
127
620
  ----------
128
621
  points
129
- The control points of the bezier curve
130
- has shape ``[a1, h1, b1]``
622
+ The control points of the Bézier curve.
131
623
 
132
624
  t
133
- The ``t``-value at which to split the Bézier curve
625
+ The ``t``-value at which to split the Bézier curve.
134
626
 
135
627
  Returns
136
628
  -------
137
- The two Bézier curves as a list of tuples,
138
- has the shape ``[a1, h1, b1], [a2, h2, b2]``
629
+ :class:`~.Point3D_Array`
630
+ An array containing the control points defining the two Bézier curves.
139
631
  """
140
- a1, h1, a2 = points
141
- s1 = interpolate(a1, h1, t)
142
- s2 = interpolate(h1, a2, t)
143
- p = interpolate(s1, s2, t)
632
+ points = np.asarray(points)
633
+ N, dim = points.shape
634
+ degree = N - 1
635
+
636
+ if degree == 3:
637
+ mt = 1 - t
638
+ mt2 = mt * mt
639
+ mt3 = mt2 * mt
640
+ t2 = t * t
641
+ t3 = t2 * t
642
+ two_mt_t = 2 * mt * t
643
+ three_mt2_t = 3 * mt2 * t
644
+ three_mt_t2 = 3 * mt * t2
645
+
646
+ # Split matrix S3 explained in the docstring
647
+ split_matrix = np.array(
648
+ [
649
+ [1, 0, 0, 0],
650
+ [mt, t, 0, 0],
651
+ [mt2, two_mt_t, t2, 0],
652
+ [mt3, three_mt2_t, three_mt_t2, t3],
653
+ [mt3, three_mt2_t, three_mt_t2, t3],
654
+ [0, mt2, two_mt_t, t2],
655
+ [0, 0, mt, t],
656
+ [0, 0, 0, 1],
657
+ ]
658
+ )
144
659
 
145
- return np.array([a1, s1, p, p, s2, a2])
660
+ return split_matrix @ points
661
+
662
+ if degree == 2:
663
+ mt = 1 - t
664
+ mt2 = mt * mt
665
+ t2 = t * t
666
+ two_tmt = 2 * t * mt
667
+
668
+ # Split matrix S2 explained in the docstring
669
+ split_matrix = np.array(
670
+ [
671
+ [1, 0, 0],
672
+ [mt, t, 0],
673
+ [mt2, two_tmt, t2],
674
+ [mt2, two_tmt, t2],
675
+ [0, mt, t],
676
+ [0, 0, 1],
677
+ ]
678
+ )
146
679
 
680
+ return split_matrix @ points
147
681
 
148
- def subdivide_quadratic_bezier(points: Iterable[float], n: int) -> np.ndarray:
149
- """Subdivide a quadratic Bézier curve into ``n`` subcurves which have the same shape.
682
+ if degree == 1:
683
+ middle = points[0] + t * (points[1] - points[0])
684
+ return np.array([points[0], middle, middle, points[1]])
685
+
686
+ if degree == 0:
687
+ return np.array([points[0], points[0]])
688
+
689
+ # Fallback case for nth degree Béziers
690
+ arr = np.empty((2, N, dim))
691
+ arr[1] = points
692
+ arr[0, 0] = points[0]
693
+
694
+ # Example for a cubic Bézier
695
+ # arr[0] = [P0 .. .. ..]
696
+ # arr[1] = [P0 P1 P2 P3]
697
+ for i in range(1, N):
698
+ # 1st iter: arr[1] = [L0 L1 L2 P3]
699
+ # 2nd iter: arr[1] = [Q0 Q1 L2 P3]
700
+ # 3rd iter: arr[1] = [C0 Q1 L2 P3]
701
+ arr[1, : N - i] += t * (arr[1, 1 : N - i + 1] - arr[1, : N - i])
702
+ # 1st iter: arr[0] = [P0 L0 .. ..]
703
+ # 2nd iter: arr[0] = [P0 L0 Q0 ..]
704
+ # 3rd iter: arr[0] = [P0 L0 Q0 C0]
705
+ arr[0, i] = arr[1, 0]
706
+
707
+ return arr.reshape(2 * N, dim)
150
708
 
151
- The points at which the curve is split are located at the
152
- arguments :math:`t = i/n` for :math:`i = 1, ..., n-1`.
709
+
710
+ # Memos explained in subdivide_bezier docstring
711
+ SUBDIVISION_MATRICES: list[dict[int, MatrixMN]] = [{} for i in range(4)]
712
+
713
+
714
+ def _get_subdivision_matrix(n_points: int, n_divisions: int) -> MatrixMN:
715
+ """Gets the matrix which subdivides a Bézier curve of
716
+ ``n_points`` control points into ``n_divisions`` parts.
717
+
718
+ Auxiliary function for :func:`subdivide_bezier`. See its
719
+ docstrings for an explanation of the matrix build process.
153
720
 
154
721
  Parameters
155
722
  ----------
156
- points
157
- The control points of the Bézier curve in form ``[a1, h1, b1]``
158
-
159
- n
160
- The number of curves to subdivide the Bézier curve into
723
+ n_points
724
+ The number of control points of the Bézier curve to
725
+ subdivide. This function only handles up to 4 points.
726
+ n_divisions
727
+ The number of parts to subdivide the Bézier curve into.
161
728
 
162
729
  Returns
163
730
  -------
164
- The new points for the Bézier curve in the form ``[a1, h1, b1, a2, h2, b2, ...]``
731
+ MatrixMN
732
+ The matrix which, upon multiplying the control points of the
733
+ Bézier curve, subdivides it into ``n_divisions`` parts.
734
+ """
735
+ if n_points not in (1, 2, 3, 4):
736
+ raise NotImplementedError(
737
+ "This function does not support subdividing Bézier "
738
+ "curves with 0 or more than 4 control points."
739
+ )
740
+
741
+ subdivision_matrix = SUBDIVISION_MATRICES[n_points - 1].get(n_divisions, None)
742
+ if subdivision_matrix is not None:
743
+ return subdivision_matrix
744
+
745
+ subdivision_matrix = np.empty((n_points * n_divisions, n_points))
746
+
747
+ # Cubic Bézier
748
+ if n_points == 4:
749
+ for i in range(n_divisions):
750
+ i2 = i * i
751
+ i3 = i2 * i
752
+ ip1 = i + 1
753
+ ip12 = ip1 * ip1
754
+ ip13 = ip12 * ip1
755
+ nmi = n_divisions - i
756
+ nmi2 = nmi * nmi
757
+ nmi3 = nmi2 * nmi
758
+ nmim1 = nmi - 1
759
+ nmim12 = nmim1 * nmim1
760
+ nmim13 = nmim12 * nmim1
761
+
762
+ subdivision_matrix[4 * i : 4 * (i + 1)] = np.array(
763
+ [
764
+ [
765
+ nmi3,
766
+ 3 * nmi2 * i,
767
+ 3 * nmi * i2,
768
+ i3,
769
+ ],
770
+ [
771
+ nmi2 * nmim1,
772
+ 2 * nmi * nmim1 * i + nmi2 * ip1,
773
+ nmim1 * i2 + 2 * nmi * i * ip1,
774
+ i2 * ip1,
775
+ ],
776
+ [
777
+ nmi * nmim12,
778
+ nmim12 * i + 2 * nmi * nmim1 * ip1,
779
+ 2 * nmim1 * i * ip1 + nmi * ip12,
780
+ i * ip12,
781
+ ],
782
+ [
783
+ nmim13,
784
+ 3 * nmim12 * ip1,
785
+ 3 * nmim1 * ip12,
786
+ ip13,
787
+ ],
788
+ ]
789
+ )
790
+ subdivision_matrix /= n_divisions * n_divisions * n_divisions
791
+
792
+ # Quadratic Bézier
793
+ elif n_points == 3:
794
+ for i in range(n_divisions):
795
+ ip1 = i + 1
796
+ nmi = n_divisions - i
797
+ nmim1 = nmi - 1
798
+ subdivision_matrix[3 * i : 3 * (i + 1)] = np.array(
799
+ [
800
+ [nmi * nmi, 2 * i * nmi, i * i],
801
+ [nmi * nmim1, i * nmim1 + ip1 * nmi, i * ip1],
802
+ [nmim1 * nmim1, 2 * ip1 * nmim1, ip1 * ip1],
803
+ ]
804
+ )
805
+ subdivision_matrix /= n_divisions * n_divisions
806
+
807
+ # Linear Bézier (straight line)
808
+ elif n_points == 2:
809
+ aux_range = np.arange(n_divisions + 1)
810
+ subdivision_matrix[::2, 1] = aux_range[:-1]
811
+ subdivision_matrix[1::2, 1] = aux_range[1:]
812
+ subdivision_matrix[:, 0] = subdivision_matrix[::-1, 1]
813
+ subdivision_matrix /= n_divisions
814
+
815
+ # Zero-degree Bézier (single point)
816
+ elif n_points == 1:
817
+ subdivision_matrix[:] = 1
818
+
819
+ SUBDIVISION_MATRICES[n_points - 1][n_divisions] = subdivision_matrix
820
+ return subdivision_matrix
821
+
822
+
823
+ def subdivide_bezier(points: BezierPointsLike, n_divisions: int) -> Spline:
824
+ r"""Subdivide a Bézier curve into :math:`n` subcurves which have the same shape.
825
+
826
+ The points at which the curve is split are located at the
827
+ arguments :math:`t = \frac{i}{n}`, for :math:`i \in \{1, ..., n-1\}`.
828
+
829
+ .. seealso::
830
+
831
+ * See :func:`split_bezier` for an explanation on how to split Bézier curves.
832
+ * See :func:`partial_bezier_points` for an extra understanding of this function.
833
+
834
+
835
+ .. note::
836
+ The resulting subcurves can be expressed as linear combinations of
837
+ ``points``, which can be encoded in a single matrix that is precalculated
838
+ for 2nd and 3rd degree Bézier curves.
839
+
840
+ As an example for a quadratic Bézier curve: taking inspiration from the
841
+ explanation in :func:`partial_bezier_points`, where the following matrix
842
+ :math:`P_2` was defined to extract the portion of a quadratic Bézier
843
+ curve for :math:`t \in [a, b]`:
844
+
845
+ .. math::
846
+ P_2
847
+ =
848
+ \begin{pmatrix}
849
+ (1-a)^2 & 2(1-a)a & a^2 \\
850
+ (1-a)(1-b) & a(1-b) + (1-a)b & ab \\
851
+ (1-b)^2 & 2(1-b)b & b^2
852
+ \end{pmatrix}
853
+
854
+ the plan is to replace :math:`[a, b]` with
855
+ :math:`\left[ \frac{i-1}{n}, \frac{i}{n} \right], \ \forall i \in \{1, ..., n\}`.
856
+
857
+ As an example for :math:`n = 2` divisions, construct :math:`P_1` for
858
+ the interval :math:`\left[ 0, \frac{1}{2} \right]`, and :math:`P_2` for the
859
+ interval :math:`\left[ \frac{1}{2}, 1 \right]`:
860
+
861
+ .. math::
862
+ P_1
863
+ =
864
+ \begin{pmatrix}
865
+ 1 & 0 & 0 \\
866
+ 0.5 & 0.5 & 0 \\
867
+ 0.25 & 0.5 & 0.25
868
+ \end{pmatrix}
869
+ ,
870
+ \quad
871
+ P_2
872
+ =
873
+ \begin{pmatrix}
874
+ 0.25 & 0.5 & 0.25 \\
875
+ 0 & 0.5 & 0.5 \\
876
+ 0 & 0 & 1
877
+ \end{pmatrix}
878
+
879
+ Therefore, the following :math:`(6, 3)` subdivision matrix :math:`D_2` can be
880
+ constructed, which will subdivide an array of ``points`` into 2 parts:
881
+
882
+ .. math::
883
+ D_2
884
+ =
885
+ \begin{pmatrix}
886
+ M_1 \\
887
+ M_2
888
+ \end{pmatrix}
889
+ =
890
+ \begin{pmatrix}
891
+ 1 & 0 & 0 \\
892
+ 0.5 & 0.5 & 0 \\
893
+ 0.25 & 0.5 & 0.25 \\
894
+ 0.25 & 0.5 & 0.25 \\
895
+ 0 & 0.5 & 0.5 \\
896
+ 0 & 0 & 1
897
+ \end{pmatrix}
898
+
899
+ For quadratic and cubic Bézier curves, the subdivision matrices are memoized for
900
+ efficiency. For higher degree curves, an iterative algorithm inspired by the
901
+ one from :func:`split_bezier` is used instead.
165
902
 
166
903
  .. image:: /_static/bezier_subdivision_example.png
167
904
 
168
- """
169
- beziers = []
170
- current = points
171
- for i in range(n, 0, -1):
172
- tmp = split_quadratic_bezier(current, 1 / i)
173
- beziers.append(tmp[:3])
174
- current = tmp[3:]
175
- return np.asarray(beziers).reshape(-1, 3)
905
+ Parameters
906
+ ----------
907
+ points
908
+ The control points of the Bézier curve.
176
909
 
910
+ n_divisions
911
+ The number of curves to subdivide the Bézier curve into
177
912
 
178
- def quadratic_bezier_remap(
179
- triplets: Iterable[Iterable[float]], new_number_of_curves: int
180
- ):
181
- """Remaps the number of curves to a higher amount by splitting bezier curves
913
+ Returns
914
+ -------
915
+ :class:`~.Spline`
916
+ An array containing the points defining the new :math:`n` subcurves.
917
+ """
918
+ points = np.asarray(points)
919
+ if n_divisions == 1:
920
+ return points
921
+
922
+ N, dim = points.shape
923
+
924
+ if N <= 4:
925
+ subdivision_matrix = _get_subdivision_matrix(N, n_divisions)
926
+ return subdivision_matrix @ points
927
+
928
+ # Fallback case for an nth degree Bézier: successive splitting
929
+ beziers = np.empty((n_divisions, N, dim))
930
+ beziers[-1] = points
931
+ for curve_num in range(n_divisions - 1, 0, -1):
932
+ curr = beziers[curve_num]
933
+ prev = beziers[curve_num - 1]
934
+ prev[0] = curr[0]
935
+ a = (n_divisions - curve_num) / (n_divisions - curve_num + 1)
936
+ # Current state for an example cubic Bézier curve:
937
+ # prev = [P0 .. .. ..]
938
+ # curr = [P0 P1 P2 P3]
939
+ for i in range(1, N):
940
+ # 1st iter: curr = [L0 L1 L2 P3]
941
+ # 2nd iter: curr = [Q0 Q1 L2 P3]
942
+ # 3rd iter: curr = [C0 Q1 L2 P3]
943
+ curr[: N - i] += a * (curr[1 : N - i + 1] - curr[: N - i])
944
+ # 1st iter: prev = [P0 L0 .. ..]
945
+ # 2nd iter: prev = [P0 L0 Q0 ..]
946
+ # 3rd iter: prev = [P0 L0 Q0 C0]
947
+ prev[i] = curr[0]
948
+
949
+ return beziers.reshape(n_divisions * N, dim)
950
+
951
+
952
+ def bezier_remap(
953
+ bezier_tuples: BezierPointsLike_Array,
954
+ new_number_of_curves: int,
955
+ ) -> BezierPoints_Array:
956
+ """Subdivides each curve in ``bezier_tuples`` into as many parts as necessary, until the final number of
957
+ curves reaches a desired amount, ``new_number_of_curves``.
182
958
 
183
959
  Parameters
184
960
  ----------
185
- triplets
186
- The triplets of the quadratic bezier curves to be remapped
961
+ bezier_tuples
962
+ An array of multiple Bézier curves of degree :math:`d` to be remapped. The shape of this array
963
+ must be ``(current_number_of_curves, nppc, dim)``, where:
964
+
965
+ * ``current_number_of_curves`` is the current amount of curves in the array ``bezier_tuples``,
966
+ * ``nppc`` is the amount of points per curve, such that their degree is ``nppc-1``, and
967
+ * ``dim`` is the dimension of the points, usually :math:`3`.
187
968
 
188
969
  new_number_of_curves
189
970
  The number of curves that the output will contain. This needs to be higher than the current number.
190
971
 
191
972
  Returns
192
973
  -------
193
- The new triplets for the quadratic bezier curves.
974
+ :class:`~.BezierPoints_Array`
975
+ The new array of shape ``(new_number_of_curves, nppc, dim)``,
976
+ containing the new Bézier curves after the remap.
194
977
  """
195
- difference = new_number_of_curves - len(triplets)
196
- if difference <= 0:
197
- return triplets
198
- new_triplets = np.zeros((new_number_of_curves, 3, 3))
199
- idx = 0
200
- for triplet in triplets:
201
- if difference > 0:
202
- tmp_noc = int(np.ceil(difference / len(triplets))) + 1
203
- tmp = subdivide_quadratic_bezier(triplet, tmp_noc).reshape(-1, 3, 3)
204
- for i in range(tmp_noc):
205
- new_triplets[idx + i] = tmp[i]
206
- difference -= tmp_noc - 1
207
- idx += tmp_noc
208
- else:
209
- new_triplets[idx] = triplet
210
- idx += 1
211
- return new_triplets
212
-
213
- """
214
- This is an alternate version of the function just for documentation purposes
215
- --------
978
+ bezier_tuples = np.asarray(bezier_tuples)
979
+ current_number_of_curves, nppc, dim = bezier_tuples.shape
980
+ # This is an array with values ranging from 0
981
+ # up to curr_num_curves, with repeats such that
982
+ # its total length is target_num_curves. For example,
983
+ # with curr_num_curves = 10, target_num_curves = 15, this
984
+ # would be [0, 0, 1, 2, 2, 3, 4, 4, 5, 6, 6, 7, 8, 8, 9].
985
+ repeat_indices = (
986
+ np.arange(new_number_of_curves, dtype="i") * current_number_of_curves
987
+ ) // new_number_of_curves
988
+
989
+ # If the nth term of this list is k, it means
990
+ # that the nth curve of our path should be split
991
+ # into k pieces.
992
+ # In the above example our array had the following elements
993
+ # [0, 0, 1, 2, 2, 3, 4, 4, 5, 6, 6, 7, 8, 8, 9]
994
+ # We have two 0s, one 1, two 2s and so on.
995
+ # The split factors array would hence be:
996
+ # [2, 1, 2, 1, 2, 1, 2, 1, 2, 1]
997
+ split_factors = np.zeros(current_number_of_curves, dtype="i")
998
+ np.add.at(split_factors, repeat_indices, 1)
999
+
1000
+ new_tuples = np.empty((new_number_of_curves, nppc, dim))
1001
+ index = 0
1002
+ for curve, sf in zip(bezier_tuples, split_factors):
1003
+ new_tuples[index : index + sf] = subdivide_bezier(curve, sf).reshape(
1004
+ sf, nppc, dim
1005
+ )
1006
+ index += sf
216
1007
 
217
- difference = new_number_of_curves - len(triplets)
218
- if difference <= 0:
219
- return triplets
220
- new_triplets = []
221
- for triplet in triplets:
222
- if difference > 0:
223
- tmp_noc = int(np.ceil(difference / len(triplets))) + 1
224
- tmp = subdivide_quadratic_bezier(triplet, tmp_noc).reshape(-1, 3, 3)
225
- for i in range(tmp_noc):
226
- new_triplets.append(tmp[i])
227
- difference -= tmp_noc - 1
228
- else:
229
- new_triplets.append(triplet)
230
- return new_triplets
231
- """
1008
+ return new_tuples
232
1009
 
233
1010
 
234
1011
  # Linear interpolation variants
235
1012
 
236
1013
 
237
- def interpolate(start: int, end: int, alpha: float) -> float:
1014
+ @overload
1015
+ def interpolate(start: float, end: float, alpha: float) -> float: ...
1016
+
1017
+
1018
+ @overload
1019
+ def interpolate(start: float, end: float, alpha: ColVector) -> ColVector: ...
1020
+
1021
+
1022
+ @overload
1023
+ def interpolate(start: Point3D, end: Point3D, alpha: float) -> Point3D: ...
1024
+
1025
+
1026
+ @overload
1027
+ def interpolate(start: Point3D, end: Point3D, alpha: ColVector) -> Point3D_Array: ...
1028
+
1029
+
1030
+ def interpolate(
1031
+ start: float | Point3D,
1032
+ end: float | Point3D,
1033
+ alpha: float | ColVector,
1034
+ ) -> float | ColVector | Point3D | Point3D_Array:
1035
+ """Linearly interpolates between two values ``start`` and ``end``.
1036
+
1037
+ Parameters
1038
+ ----------
1039
+ start
1040
+ The start of the range.
1041
+ end
1042
+ The end of the range.
1043
+ alpha
1044
+ A float between 0 and 1, or an :math:`(n, 1)` column vector containing
1045
+ :math:`n` floats between 0 and 1 to interpolate in a vectorized fashion.
1046
+
1047
+ Returns
1048
+ -------
1049
+ :class:`float` | :class:`~.ColVector` | :class:`~.Point3D` | :class:`~.Point3D_Array`
1050
+ The result of the linear interpolation.
1051
+
1052
+ * If ``start`` and ``end`` are of type :class:`float`, and:
1053
+
1054
+ * ``alpha`` is also a :class:`float`, the return is simply another :class:`float`.
1055
+ * ``alpha`` is a :class:`~.ColVector`, the return is another :class:`~.ColVector`.
1056
+
1057
+ * If ``start`` and ``end`` are of type :class:`~.Point3D`, and:
1058
+
1059
+ * ``alpha`` is a :class:`float`, the return is another :class:`~.Point3D`.
1060
+ * ``alpha`` is a :class:`~.ColVector`, the return is a :class:`~.Point3D_Array`.
1061
+ """
238
1062
  return (1 - alpha) * start + alpha * end
239
1063
 
240
1064
 
@@ -244,277 +1068,904 @@ def integer_interpolate(
244
1068
  alpha: float,
245
1069
  ) -> tuple[int, float]:
246
1070
  """
247
- Alpha is a float between 0 and 1. This returns
248
- an integer between start and end (inclusive) representing
249
- appropriate interpolation between them, along with a
250
- "residue" representing a new proportion between the
251
- returned integer and the next one of the
252
- list.
253
-
254
- For example, if start=0, end=10, alpha=0.46, This
255
- would return (4, 0.6).
1071
+ This is a variant of interpolate that returns an integer and the residual
1072
+
1073
+ Parameters
1074
+ ----------
1075
+ start
1076
+ The start of the range
1077
+ end
1078
+ The end of the range
1079
+ alpha
1080
+ a float between 0 and 1.
1081
+
1082
+ Returns
1083
+ -------
1084
+ tuple[int, float]
1085
+ This returns an integer between start and end (inclusive) representing
1086
+ appropriate interpolation between them, along with a
1087
+ "residue" representing a new proportion between the
1088
+ returned integer and the next one of the
1089
+ list.
1090
+
1091
+ Example
1092
+ -------
1093
+
1094
+ .. code-block:: pycon
1095
+
1096
+ >>> integer, residue = integer_interpolate(start=0, end=10, alpha=0.46)
1097
+ >>> np.allclose((integer, residue), (4, 0.6))
1098
+ True
256
1099
  """
257
1100
  if alpha >= 1:
258
- return (end - 1, 1.0)
1101
+ return (int(end - 1), 1.0)
259
1102
  if alpha <= 0:
260
- return (start, 0)
1103
+ return (int(start), 0)
261
1104
  value = int(interpolate(start, end, alpha))
262
1105
  residue = ((end - start) * alpha) % 1
263
1106
  return (value, residue)
264
1107
 
265
1108
 
266
- def mid(start: float, end: float) -> float:
1109
+ @overload
1110
+ def mid(start: float, end: float) -> float: ...
1111
+
1112
+
1113
+ @overload
1114
+ def mid(start: Point3D, end: Point3D) -> Point3D: ...
1115
+
1116
+
1117
+ def mid(start: float | Point3D, end: float | Point3D) -> float | Point3D:
1118
+ """Returns the midpoint between two values.
1119
+
1120
+ Parameters
1121
+ ----------
1122
+ start
1123
+ The first value
1124
+ end
1125
+ The second value
1126
+
1127
+ Returns
1128
+ -------
1129
+ The midpoint between the two values
1130
+ """
267
1131
  return (start + end) / 2.0
268
1132
 
269
1133
 
270
- def inverse_interpolate(start: float, end: float, value: float) -> np.ndarray:
1134
+ @overload
1135
+ def inverse_interpolate(start: float, end: float, value: float) -> float: ...
1136
+
1137
+
1138
+ @overload
1139
+ def inverse_interpolate(start: float, end: float, value: Point3D) -> Point3D: ...
1140
+
1141
+
1142
+ @overload
1143
+ def inverse_interpolate(start: Point3D, end: Point3D, value: Point3D) -> Point3D: ...
1144
+
1145
+
1146
+ def inverse_interpolate(
1147
+ start: float | Point3D,
1148
+ end: float | Point3D,
1149
+ value: float | Point3D,
1150
+ ) -> float | Point3D:
1151
+ """Perform inverse interpolation to determine the alpha
1152
+ values that would produce the specified ``value``
1153
+ given the ``start`` and ``end`` values or points.
1154
+
1155
+ Parameters
1156
+ ----------
1157
+ start
1158
+ The start value or point of the interpolation.
1159
+ end
1160
+ The end value or point of the interpolation.
1161
+ value
1162
+ The value or point for which the alpha value
1163
+ should be determined.
1164
+
1165
+ Returns
1166
+ -------
1167
+ The alpha values producing the given input
1168
+ when interpolating between ``start`` and ``end``.
1169
+
1170
+ Example
1171
+ -------
1172
+
1173
+ .. code-block:: pycon
1174
+
1175
+ >>> inverse_interpolate(start=2, end=6, value=4)
1176
+ np.float64(0.5)
1177
+
1178
+ >>> start = np.array([1, 2, 1])
1179
+ >>> end = np.array([7, 8, 11])
1180
+ >>> value = np.array([4, 5, 5])
1181
+ >>> inverse_interpolate(start, end, value)
1182
+ array([0.5, 0.5, 0.4])
1183
+ """
271
1184
  return np.true_divide(value - start, end - start)
272
1185
 
273
1186
 
1187
+ @overload
274
1188
  def match_interpolate(
275
1189
  new_start: float,
276
1190
  new_end: float,
277
1191
  old_start: float,
278
1192
  old_end: float,
279
1193
  old_value: float,
280
- ) -> np.ndarray:
1194
+ ) -> float: ...
1195
+
1196
+
1197
+ @overload
1198
+ def match_interpolate(
1199
+ new_start: float,
1200
+ new_end: float,
1201
+ old_start: float,
1202
+ old_end: float,
1203
+ old_value: Point3D,
1204
+ ) -> Point3D: ...
1205
+
1206
+
1207
+ def match_interpolate(
1208
+ new_start: float,
1209
+ new_end: float,
1210
+ old_start: float,
1211
+ old_end: float,
1212
+ old_value: float | Point3D,
1213
+ ) -> float | Point3D:
1214
+ """Interpolate a value from an old range to a new range.
1215
+
1216
+ Parameters
1217
+ ----------
1218
+ new_start
1219
+ The start of the new range.
1220
+ new_end
1221
+ The end of the new range.
1222
+ old_start
1223
+ The start of the old range.
1224
+ old_end
1225
+ The end of the old range.
1226
+ old_value
1227
+ The value within the old range whose corresponding
1228
+ value in the new range (with the same alpha value)
1229
+ is desired.
1230
+
1231
+ Returns
1232
+ -------
1233
+ The interpolated value within the new range.
1234
+
1235
+ Examples
1236
+ --------
1237
+ >>> match_interpolate(0, 100, 10, 20, 15)
1238
+ np.float64(50.0)
1239
+ """
1240
+ old_alpha = inverse_interpolate(old_start, old_end, old_value)
281
1241
  return interpolate(
282
1242
  new_start,
283
1243
  new_end,
284
- inverse_interpolate(old_start, old_end, old_value),
1244
+ old_alpha,
285
1245
  )
286
1246
 
287
1247
 
288
- # Figuring out which bezier curves most smoothly connect a sequence of points
1248
+ # Figuring out which Bézier curves most smoothly connect a sequence of points
1249
+ def get_smooth_cubic_bezier_handle_points(
1250
+ anchors: Point3DLike_Array,
1251
+ ) -> tuple[Point3D_Array, Point3D_Array]:
1252
+ """Given an array of anchors for a cubic spline (array of connected cubic
1253
+ Bézier curves), compute the 1st and 2nd handle for every curve, so that
1254
+ the resulting spline is smooth.
289
1255
 
1256
+ Parameters
1257
+ ----------
1258
+ anchors
1259
+ Anchors of a cubic spline.
290
1260
 
291
- def get_smooth_cubic_bezier_handle_points(points):
292
- points = np.array(points)
293
- num_handles = len(points) - 1
294
- dim = points.shape[1]
295
- if num_handles < 1:
296
- return np.zeros((0, dim)), np.zeros((0, dim))
297
- # Must solve 2*num_handles equations to get the handles.
298
- # l and u are the number of lower an upper diagonal rows
299
- # in the matrix to solve.
300
- l, u = 2, 1
301
- # diag is a representation of the matrix in diagonal form
302
- # See https://www.particleincell.com/2012/bezier-splines/
303
- # for how to arrive at these equations
304
- diag = np.zeros((l + u + 1, 2 * num_handles))
305
- diag[0, 1::2] = -1
306
- diag[0, 2::2] = 1
307
- diag[1, 0::2] = 2
308
- diag[1, 1::2] = 1
309
- diag[2, 1:-2:2] = -2
310
- diag[3, 0:-3:2] = 1
311
- # last
312
- diag[2, -2] = -1
313
- diag[1, -1] = 2
314
- # This is the b as in Ax = b, where we are solving for x,
315
- # and A is represented using diag. However, think of entries
316
- # to x and b as being points in space, not numbers
317
- b = np.zeros((2 * num_handles, dim))
318
- b[1::2] = 2 * points[1:]
319
- b[0] = points[0]
320
- b[-1] = points[-1]
321
-
322
- def solve_func(b):
323
- return linalg.solve_banded((l, u), diag, b)
324
-
325
- use_closed_solve_function = is_closed(points)
326
- if use_closed_solve_function:
327
- # Get equations to relate first and last points
328
- matrix = diag_to_matrix((l, u), diag)
329
- # last row handles second derivative
330
- matrix[-1, [0, 1, -2, -1]] = [2, -1, 1, -2]
331
- # first row handles first derivative
332
- matrix[0, :] = np.zeros(matrix.shape[1])
333
- matrix[0, [0, -1]] = [1, 1]
334
- b[0] = 2 * points[0]
335
- b[-1] = np.zeros(dim)
336
-
337
- def closed_curve_solve_func(b):
338
- return linalg.solve(matrix, b)
339
-
340
- handle_pairs = np.zeros((2 * num_handles, dim))
341
- for i in range(dim):
342
- if use_closed_solve_function:
343
- handle_pairs[:, i] = closed_curve_solve_func(b[:, i])
344
- else:
345
- handle_pairs[:, i] = solve_func(b[:, i])
346
- return handle_pairs[0::2], handle_pairs[1::2]
1261
+ Returns
1262
+ -------
1263
+ :class:`tuple` [:class:`~.Point3D_Array`, :class:`~.Point3D_Array`]
1264
+ A tuple of two arrays: one containing the 1st handle for every curve in
1265
+ the cubic spline, and the other containing the 2nd handles.
1266
+ """
1267
+ anchors = np.asarray(anchors)
1268
+ n_anchors = anchors.shape[0]
347
1269
 
1270
+ # If there's a single anchor, there's no Bézier curve.
1271
+ # Return empty arrays.
1272
+ if n_anchors == 1:
1273
+ dim = anchors.shape[1]
1274
+ return np.zeros((0, dim)), np.zeros((0, dim))
348
1275
 
349
- def get_smooth_handle_points(
350
- points: np.ndarray,
351
- ) -> tuple[np.ndarray, np.ndarray]:
352
- """Given some anchors (points), compute handles so the resulting bezier curve is smooth.
1276
+ # If there are only two anchors (thus only one pair of handles),
1277
+ # they can only be an interpolation of these two anchors with alphas
1278
+ # 1/3 and 2/3, which will draw a straight line between the anchors.
1279
+ if n_anchors == 2:
1280
+ val = interpolate(anchors[0], anchors[1], np.array([[1 / 3], [2 / 3]]))
1281
+ return (val[0], val[1])
1282
+
1283
+ # Handle different cases depending on whether the points form a closed
1284
+ # curve or not
1285
+ curve_is_closed = is_closed(anchors)
1286
+ if curve_is_closed:
1287
+ return get_smooth_closed_cubic_bezier_handle_points(anchors)
1288
+ else:
1289
+ return get_smooth_open_cubic_bezier_handle_points(anchors)
1290
+
1291
+
1292
+ CP_CLOSED_MEMO = np.array([1 / 3])
1293
+ UP_CLOSED_MEMO = np.array([1 / 3])
1294
+
1295
+
1296
+ def get_smooth_closed_cubic_bezier_handle_points(
1297
+ anchors: Point3DLike_Array,
1298
+ ) -> tuple[Point3D_Array, Point3D_Array]:
1299
+ r"""Special case of :func:`get_smooth_cubic_bezier_handle_points`,
1300
+ when the ``anchors`` form a closed loop.
1301
+
1302
+ .. note::
1303
+ A system of equations must be solved to get the first handles of
1304
+ every Bézier curve (referred to as :math:`H_1`).
1305
+ Then :math:`H_2` (the second handles) can be obtained separately.
1306
+
1307
+ .. seealso::
1308
+ The equations were obtained from:
1309
+
1310
+ * `Conditions on control points for continuous curvature. (2016). Jaco Stuifbergen. <http://www.jacos.nl/jacos_html/spline/theory/theory_2.html>`_
1311
+
1312
+ In general, if there are :math:`N+1` anchors, there will be :math:`N` Bézier curves
1313
+ and thus :math:`N` pairs of handles to find. We must solve the following
1314
+ system of equations for the 1st handles (example for :math:`N = 5`):
1315
+
1316
+ .. math::
1317
+ \begin{pmatrix}
1318
+ 4 & 1 & 0 & 0 & 1 \\
1319
+ 1 & 4 & 1 & 0 & 0 \\
1320
+ 0 & 1 & 4 & 1 & 0 \\
1321
+ 0 & 0 & 1 & 4 & 1 \\
1322
+ 1 & 0 & 0 & 1 & 4
1323
+ \end{pmatrix}
1324
+ \begin{pmatrix}
1325
+ H_{1,0} \\
1326
+ H_{1,1} \\
1327
+ H_{1,2} \\
1328
+ H_{1,3} \\
1329
+ H_{1,4}
1330
+ \end{pmatrix}
1331
+ =
1332
+ \begin{pmatrix}
1333
+ 4A_0 + 2A_1 \\
1334
+ 4A_1 + 2A_2 \\
1335
+ 4A_2 + 2A_3 \\
1336
+ 4A_3 + 2A_4 \\
1337
+ 4A_4 + 2A_5
1338
+ \end{pmatrix}
1339
+
1340
+ which will be expressed as :math:`RH_1 = D`.
1341
+
1342
+ :math:`R` is almost a tridiagonal matrix, so we could use Thomas' algorithm.
1343
+
1344
+ .. seealso::
1345
+ `Tridiagonal matrix algorithm. Wikipedia. <https://en.wikipedia.org/wiki/Tridiagonal_matrix_algorithm>`_
1346
+
1347
+ However, :math:`R` has ones at the opposite corners. A solution to this is
1348
+ the first decomposition proposed in the link below, with :math:`\alpha = 1`:
1349
+
1350
+ .. seealso::
1351
+ `Tridiagonal matrix algorithm # Variants. Wikipedia. <https://en.wikipedia.org/wiki/Tridiagonal_matrix_algorithm#Variants>`_
1352
+
1353
+ .. math::
1354
+ R
1355
+ =
1356
+ \begin{pmatrix}
1357
+ 4 & 1 & 0 & 0 & 1 \\
1358
+ 1 & 4 & 1 & 0 & 0 \\
1359
+ 0 & 1 & 4 & 1 & 0 \\
1360
+ 0 & 0 & 1 & 4 & 1 \\
1361
+ 1 & 0 & 0 & 1 & 4
1362
+ \end{pmatrix}
1363
+ &=
1364
+ \begin{pmatrix}
1365
+ 3 & 1 & 0 & 0 & 0 \\
1366
+ 1 & 4 & 1 & 0 & 0 \\
1367
+ 0 & 1 & 4 & 1 & 0 \\
1368
+ 0 & 0 & 1 & 4 & 1 \\
1369
+ 0 & 0 & 0 & 1 & 3
1370
+ \end{pmatrix}
1371
+ +
1372
+ \begin{pmatrix}
1373
+ 1 & 0 & 0 & 0 & 1 \\
1374
+ 0 & 0 & 0 & 0 & 0 \\
1375
+ 0 & 0 & 0 & 0 & 0 \\
1376
+ 0 & 0 & 0 & 0 & 0 \\
1377
+ 1 & 0 & 0 & 0 & 1
1378
+ \end{pmatrix}
1379
+ \\
1380
+ &
1381
+ \\
1382
+ &=
1383
+ \begin{pmatrix}
1384
+ 3 & 1 & 0 & 0 & 0 \\
1385
+ 1 & 4 & 1 & 0 & 0 \\
1386
+ 0 & 1 & 4 & 1 & 0 \\
1387
+ 0 & 0 & 1 & 4 & 1 \\
1388
+ 0 & 0 & 0 & 1 & 3
1389
+ \end{pmatrix}
1390
+ +
1391
+ \begin{pmatrix}
1392
+ 1 \\
1393
+ 0 \\
1394
+ 0 \\
1395
+ 0 \\
1396
+ 1
1397
+ \end{pmatrix}
1398
+ \begin{pmatrix}
1399
+ 1 & 0 & 0 & 0 & 1
1400
+ \end{pmatrix}
1401
+ \\
1402
+ &
1403
+ \\
1404
+ &=
1405
+ T + uv^t
1406
+
1407
+ We decompose :math:`R = T + uv^t`, where :math:`T` is a tridiagonal matrix, and
1408
+ :math:`u, v` are :math:`N`-D vectors such that :math:`u_0 = u_{N-1} = v_0 = v_{N-1} = 1`,
1409
+ and :math:`u_i = v_i = 0, \forall i \in \{1, ..., N-2\}`.
1410
+
1411
+ Thus:
1412
+
1413
+ .. math::
1414
+ RH_1 &= D \\
1415
+ \Rightarrow (T + uv^t)H_1 &= D
1416
+
1417
+ If we find a vector :math:`q` such that :math:`Tq = u`:
1418
+
1419
+ .. math::
1420
+ \Rightarrow (T + Tqv^t)H_1 &= D \\
1421
+ \Rightarrow T(I + qv^t)H_1 &= D \\
1422
+ \Rightarrow H_1 &= (I + qv^t)^{-1} T^{-1} D
1423
+
1424
+ According to Sherman-Morrison's formula:
1425
+
1426
+ .. seealso::
1427
+ `Sherman-Morrison's formula. Wikipedia. <https://en.wikipedia.org/wiki/Sherman%E2%80%93Morrison_formula>`_
1428
+
1429
+ .. math::
1430
+ (I + qv^t)^{-1} = I - \frac{1}{1 + v^tq} qv^t
1431
+
1432
+ If we find :math:`Y = T^{-1} D`, or in other words, if we solve for
1433
+ :math:`Y` in :math:`TY = D`:
1434
+
1435
+ .. math::
1436
+ H_1 &= (I + qv^t)^{-1} T^{-1} D \\
1437
+ &= (I + qv^t)^{-1} Y \\
1438
+ &= (I - \frac{1}{1 + v^tq} qv^t) Y \\
1439
+ &= Y - \frac{1}{1 + v^tq} qv^tY
1440
+
1441
+ Therefore, we must solve for :math:`q` and :math:`Y` in :math:`Tq = u` and :math:`TY = D`.
1442
+ As :math:`T` is now tridiagonal, we shall use Thomas' algorithm.
1443
+
1444
+ Define:
1445
+
1446
+ * :math:`a = [a_0, \ a_1, \ ..., \ a_{N-2}]` as :math:`T`'s lower diagonal of :math:`N-1` elements,
1447
+ such that :math:`a_0 = a_1 = ... = a_{N-2} = 1`, so this diagonal is filled with ones;
1448
+ * :math:`b = [b_0, \ b_1, \ ..., \ b_{N-2}, \ b_{N-1}]` as :math:`T`'s main diagonal of :math:`N` elements,
1449
+ such that :math:`b_0 = b_{N-1} = 3`, and :math:`b_1 = b_2 = ... = b_{N-2} = 4`;
1450
+ * :math:`c = [c_0, \ c_1, \ ..., \ c_{N-2}]` as :math:`T`'s upper diagonal of :math:`N-1` elements,
1451
+ such that :math:`c_0 = c_1 = ... = c_{N-2} = 1`: this diagonal is also filled with ones.
1452
+
1453
+ If, according to Thomas' algorithm, we define:
1454
+
1455
+ .. math::
1456
+ c'_0 &= \frac{c_0}{b_0} & \\
1457
+ c'_i &= \frac{c_i}{b_i - a_{i-1} c'_{i-1}}, & \quad \forall i \in \{1, ..., N-2\} \\
1458
+ & & \\
1459
+ u'_0 &= \frac{u_0}{b_0} & \\
1460
+ u'_i &= \frac{u_i - a_{i-1} u'_{i-1}}{b_i - a_{i-1} c'_{i-1}}, & \quad \forall i \in \{1, ..., N-1\} \\
1461
+ & & \\
1462
+ D'_0 &= \frac{1}{b_0} D_0 & \\
1463
+ D'_i &= \frac{1}{b_i - a_{i-1} c'_{i-1}} (D_i - a_{i-1} D'_{i-1}), & \quad \forall i \in \{1, ..., N-1\}
1464
+
1465
+ Then:
1466
+
1467
+ .. math::
1468
+ c'_0 &= \frac{1}{3} & \\
1469
+ c'_i &= \frac{1}{4 - c'_{i-1}}, & \quad \forall i \in \{1, ..., N-2\} \\
1470
+ & & \\
1471
+ u'_0 &= \frac{1}{3} & \\
1472
+ u'_i &= \frac{-u'_{i-1}}{4 - c'_{i-1}} = -c'_i u'_{i-1}, & \quad \forall i \in \{1, ..., N-2\} \\
1473
+ u'_{N-1} &= \frac{1 - u'_{N-2}}{3 - c'_{N-2}} & \\
1474
+ & & \\
1475
+ D'_0 &= \frac{1}{3} (4A_0 + 2A_1) & \\
1476
+ D'_i &= \frac{1}{4 - c'_{i-1}} (4A_i + 2A_{i+1} - D'_{i-1}) & \\
1477
+ &= c_i (4A_i + 2A_{i+1} - D'_{i-1}), & \quad \forall i \in \{1, ..., N-2\} \\
1478
+ D'_{N-1} &= \frac{1}{3 - c'_{N-2}} (4A_{N-1} + 2A_N - D'_{N-2}) &
1479
+
1480
+ Finally, we can do Backward Substitution to find :math:`q` and :math:`Y`:
1481
+
1482
+ .. math::
1483
+ q_{N-1} &= u'_{N-1} & \\
1484
+ q_i &= u'_{i} - c'_i q_{i+1}, & \quad \forall i \in \{0, ..., N-2\} \\
1485
+ & & \\
1486
+ Y_{N-1} &= D'_{N-1} & \\
1487
+ Y_i &= D'_i - c'_i Y_{i+1}, & \quad \forall i \in \{0, ..., N-2\}
1488
+
1489
+ With those values, we can finally calculate :math:`H_1 = Y - \frac{1}{1 + v^tq} qv^tY`.
1490
+ Given that :math:`v_0 = v_{N-1} = 1`, and :math:`v_1 = v_2 = ... = v_{N-2} = 0`, its dot products
1491
+ with :math:`q` and :math:`Y` are respectively :math:`v^tq = q_0 + q_{N-1}` and
1492
+ :math:`v^tY = Y_0 + Y_{N-1}`. Thus:
1493
+
1494
+ .. math::
1495
+ H_1 = Y - \frac{1}{1 + q_0 + q_{N-1}} q(Y_0 + Y_{N-1})
1496
+
1497
+ Once we have :math:`H_1`, we can get :math:`H_2` (the array of second handles) as follows:
1498
+
1499
+ .. math::
1500
+ H_{2, i} &= 2A_{i+1} - H_{1, i+1}, & \quad \forall i \in \{0, ..., N-2\} \\
1501
+ H_{2, N-1} &= 2A_0 - H_{1, 0} &
1502
+
1503
+ Because the matrix :math:`R` always follows the same pattern (and thus :math:`T, u, v` as well),
1504
+ we can define a memo list for :math:`c'` and :math:`u'` to avoid recalculation. We cannot
1505
+ memoize :math:`D` and :math:`Y`, however, because they are always different matrices. We
1506
+ cannot make a memo for :math:`q` either, but we can calculate it faster because :math:`u'`
1507
+ can be memoized.
353
1508
 
354
1509
  Parameters
355
1510
  ----------
356
- points
357
- Anchors.
1511
+ anchors
1512
+ Anchors of a closed cubic spline.
358
1513
 
359
1514
  Returns
360
1515
  -------
361
- typing.Tuple[np.ndarray, np.ndarray]
362
- Computed handles.
1516
+ :class:`tuple` [:class:`~.Point3D_Array`, :class:`~.Point3D_Array`]
1517
+ A tuple of two arrays: one containing the 1st handle for every curve in
1518
+ the closed cubic spline, and the other containing the 2nd handles.
363
1519
  """
364
- # NOTE points here are anchors.
365
- points = np.array(points)
366
- num_handles = len(points) - 1
367
- dim = points.shape[1]
368
- if num_handles < 1:
369
- return np.zeros((0, dim)), np.zeros((0, dim))
370
- # Must solve 2*num_handles equations to get the handles.
371
- # l and u are the number of lower an upper diagonal rows
372
- # in the matrix to solve.
373
- l, u = 2, 1
374
- # diag is a representation of the matrix in diagonal form
375
- # See https://www.particleincell.com/2012/bezier-splines/
376
- # for how to arrive at these equations
377
- diag = np.zeros((l + u + 1, 2 * num_handles))
378
- diag[0, 1::2] = -1
379
- diag[0, 2::2] = 1
380
- diag[1, 0::2] = 2
381
- diag[1, 1::2] = 1
382
- diag[2, 1:-2:2] = -2
383
- diag[3, 0:-3:2] = 1
384
- # last
385
- diag[2, -2] = -1
386
- diag[1, -1] = 2
387
- # This is the b as in Ax = b, where we are solving for x,
388
- # and A is represented using diag. However, think of entries
389
- # to x and b as being points in space, not numbers
390
- b = np.zeros((2 * num_handles, dim))
391
- b[1::2] = 2 * points[1:]
392
- b[0] = points[0]
393
- b[-1] = points[-1]
394
-
395
- def solve_func(b: np.ndarray) -> np.ndarray:
396
- return linalg.solve_banded((l, u), diag, b)
397
-
398
- use_closed_solve_function = is_closed(points)
399
- if use_closed_solve_function:
400
- # Get equations to relate first and last points
401
- matrix = diag_to_matrix((l, u), diag)
402
- # last row handles second derivative
403
- matrix[-1, [0, 1, -2, -1]] = [2, -1, 1, -2]
404
- # first row handles first derivative
405
- matrix[0, :] = np.zeros(matrix.shape[1])
406
- matrix[0, [0, -1]] = [1, 1]
407
- b[0] = 2 * points[0]
408
- b[-1] = np.zeros(dim)
409
-
410
- def closed_curve_solve_func(b: np.ndarray) -> np.ndarray:
411
- return linalg.solve(matrix, b)
412
-
413
- handle_pairs = np.zeros((2 * num_handles, dim))
414
- for i in range(dim):
415
- if use_closed_solve_function:
416
- handle_pairs[:, i] = closed_curve_solve_func(b[:, i])
417
- else:
418
- handle_pairs[:, i] = solve_func(b[:, i])
419
- return handle_pairs[0::2], handle_pairs[1::2]
1520
+ global CP_CLOSED_MEMO
1521
+ global UP_CLOSED_MEMO
1522
+
1523
+ A = np.asarray(anchors)
1524
+ N = A.shape[0] - 1
1525
+ dim = A.shape[1]
1526
+
1527
+ # Calculate cp (c prime) and up (u prime) with help from
1528
+ # CP_CLOSED_MEMO and UP_CLOSED_MEMO.
1529
+ len_memo = CP_CLOSED_MEMO.size
1530
+ if len_memo < N - 1:
1531
+ cp = np.empty(N - 1)
1532
+ up = np.empty(N - 1)
1533
+ cp[:len_memo] = CP_CLOSED_MEMO
1534
+ up[:len_memo] = UP_CLOSED_MEMO
1535
+ # Forward Substitution 1
1536
+ # Calculate up (at the same time we calculate cp).
1537
+ for i in range(len_memo, N - 1):
1538
+ cp[i] = 1 / (4 - cp[i - 1])
1539
+ up[i] = -cp[i] * up[i - 1]
1540
+ CP_CLOSED_MEMO = cp
1541
+ UP_CLOSED_MEMO = up
1542
+ else:
1543
+ cp = CP_CLOSED_MEMO[: N - 1]
1544
+ up = UP_CLOSED_MEMO[: N - 1]
1545
+
1546
+ # The last element of u' is different
1547
+ cp_last_division = 1 / (3 - cp[N - 2])
1548
+ up_last = cp_last_division * (1 - up[N - 2])
1549
+
1550
+ # Backward Substitution 1
1551
+ # Calculate q.
1552
+ q = np.empty((N, dim))
1553
+ q[N - 1] = up_last
1554
+ for i in range(N - 2, -1, -1):
1555
+ q[i] = up[i] - cp[i] * q[i + 1]
1556
+
1557
+ # Forward Substitution 2
1558
+ # Calculate Dp (D prime).
1559
+ Dp = np.empty((N, dim))
1560
+ AUX = 4 * A[:N] + 2 * A[1:] # Vectorize the sum for efficiency.
1561
+ Dp[0] = AUX[0] / 3
1562
+ for i in range(1, N - 1):
1563
+ Dp[i] = cp[i] * (AUX[i] - Dp[i - 1])
1564
+ Dp[N - 1] = cp_last_division * (AUX[N - 1] - Dp[N - 2])
1565
+
1566
+ # Backward Substitution
1567
+ # Calculate Y, which is defined as a view of Dp for efficiency
1568
+ # and semantic convenience at the same time.
1569
+ Y = Dp
1570
+ # Y[N-1] = Dp[N-1] (redundant)
1571
+ for i in range(N - 2, -1, -1):
1572
+ Y[i] = Dp[i] - cp[i] * Y[i + 1]
1573
+
1574
+ # Calculate H1.
1575
+ H1 = Y - 1 / (1 + q[0] + q[N - 1]) * q * (Y[0] + Y[N - 1])
1576
+
1577
+ # Calculate H2.
1578
+ H2 = np.empty((N, dim))
1579
+ H2[0 : N - 1] = 2 * A[1:N] - H1[1:N]
1580
+ H2[N - 1] = 2 * A[N] - H1[0]
1581
+
1582
+ return H1, H2
1583
+
1584
+
1585
+ CP_OPEN_MEMO = np.array([0.5])
1586
+
1587
+
1588
+ def get_smooth_open_cubic_bezier_handle_points(
1589
+ anchors: Point3DLike_Array,
1590
+ ) -> tuple[Point3D_Array, Point3D_Array]:
1591
+ r"""Special case of :func:`get_smooth_cubic_bezier_handle_points`,
1592
+ when the ``anchors`` do not form a closed loop.
1593
+
1594
+ .. note::
1595
+ A system of equations must be solved to get the first handles of
1596
+ every Bèzier curve (referred to as :math:`H_1`).
1597
+ Then :math:`H_2` (the second handles) can be obtained separately.
1598
+
1599
+ .. seealso::
1600
+ The equations were obtained from:
1601
+
1602
+ * `Smooth Bézier Spline Through Prescribed Points. (2012). Particle in Cell Consulting LLC. <https://www.particleincell.com/2012/bezier-splines/>`_
1603
+ * `Conditions on control points for continuous curvature. (2016). Jaco Stuifbergen. <http://www.jacos.nl/jacos_html/spline/theory/theory_2.html>`_
1604
+
1605
+ .. warning::
1606
+ The equations in the first webpage have some typos which were corrected in the comments.
1607
+
1608
+ In general, if there are :math:`N+1` anchors, there will be :math:`N` Bézier curves
1609
+ and thus :math:`N` pairs of handles to find. We must solve the following
1610
+ system of equations for the 1st handles (example for :math:`N = 5`):
1611
+
1612
+ .. math::
1613
+ \begin{pmatrix}
1614
+ 2 & 1 & 0 & 0 & 0 \\
1615
+ 1 & 4 & 1 & 0 & 0 \\
1616
+ 0 & 1 & 4 & 1 & 0 \\
1617
+ 0 & 0 & 1 & 4 & 1 \\
1618
+ 0 & 0 & 0 & 2 & 7
1619
+ \end{pmatrix}
1620
+ \begin{pmatrix}
1621
+ H_{1,0} \\
1622
+ H_{1,1} \\
1623
+ H_{1,2} \\
1624
+ H_{1,3} \\
1625
+ H_{1,4}
1626
+ \end{pmatrix}
1627
+ =
1628
+ \begin{pmatrix}
1629
+ A_0 + 2A_1 \\
1630
+ 4A_1 + 2A_2 \\
1631
+ 4A_2 + 2A_3 \\
1632
+ 4A_3 + 2A_4 \\
1633
+ 8A_4 + A_5
1634
+ \end{pmatrix}
1635
+
1636
+ which will be expressed as :math:`TH_1 = D`.
1637
+ :math:`T` is a tridiagonal matrix, so the system can be solved in :math:`O(N)`
1638
+ operations. Here we shall use Thomas' algorithm or the tridiagonal matrix
1639
+ algorithm.
1640
+
1641
+ .. seealso::
1642
+ `Tridiagonal matrix algorithm. Wikipedia. <https://en.wikipedia.org/wiki/Tridiagonal_matrix_algorithm>`_
1643
+
1644
+ Define:
1645
+
1646
+ * :math:`a = [a_0, \ a_1, \ ..., \ a_{N-2}]` as :math:`T`'s lower diagonal of :math:`N-1` elements,
1647
+ such that :math:`a_0 = a_1 = ... = a_{N-3} = 1`, and :math:`a_{N-2} = 2`;
1648
+ * :math:`b = [b_0, \ b_1, \ ..., \ b_{N-2}, \ b_{N-1}]` as :math:`T`'s main diagonal of :math:`N` elements,
1649
+ such that :math:`b_0 = 2`, :math:`b_1 = b_2 = ... = b_{N-2} = 4`, and :math:`b_{N-1} = 7`;
1650
+ * :math:`c = [c_0, \ c_1, \ ..., \ c_{N-2}]` as :math:`T`'s upper diagonal of :math:`{N-1}` elements,
1651
+ such that :math:`c_0 = c_1 = ... = c_{N-2} = 1`: this diagonal is filled with ones.
1652
+
1653
+ If, according to Thomas' algorithm, we define:
1654
+
1655
+ .. math::
1656
+ c'_0 &= \frac{c_0}{b_0} & \\
1657
+ c'_i &= \frac{c_i}{b_i - a_{i-1} c'_{i-1}}, & \quad \forall i \in \{1, ..., N-2\} \\
1658
+ & & \\
1659
+ D'_0 &= \frac{1}{b_0} D_0 & \\
1660
+ D'_i &= \frac{1}{b_i - a_{i-1} c'{i-1}} (D_i - a_{i-1} D'_{i-1}), & \quad \forall i \in \{1, ..., N-1\}
1661
+
1662
+ Then:
1663
+
1664
+ .. math::
1665
+ c'_0 &= 0.5 & \\
1666
+ c'_i &= \frac{1}{4 - c'_{i-1}}, & \quad \forall i \in \{1, ..., N-2\} \\
1667
+ & & \\
1668
+ D'_0 &= 0.5A_0 + A_1 & \\
1669
+ D'_i &= \frac{1}{4 - c'_{i-1}} (4A_i + 2A_{i+1} - D'_{i-1}) & \\
1670
+ &= c_i (4A_i + 2A_{i+1} - D'_{i-1}), & \quad \forall i \in \{1, ..., N-2\} \\
1671
+ D'_{N-1} &= \frac{1}{7 - 2c'_{N-2}} (8A_{N-1} + A_N - 2D'_{N-2}) &
1672
+
1673
+ Finally, we can do Backward Substitution to find :math:`H_1`:
1674
+
1675
+ .. math::
1676
+ H_{1, N-1} &= D'_{N-1} & \\
1677
+ H_{1, i} &= D'_i - c'_i H_{1, i+1}, & \quad \forall i \in \{0, ..., N-2\}
1678
+
1679
+ Once we have :math:`H_1`, we can get :math:`H_2` (the array of second handles) as follows:
1680
+
1681
+ .. math::
1682
+ H_{2, i} &= 2A_{i+1} - H_{1, i+1}, & \quad \forall i \in \{0, ..., N-2\} \\
1683
+ H_{2, N-1} &= 0.5A_N + 0.5H_{1, N-1} &
1684
+
1685
+ As the matrix :math:`T` always follows the same pattern, we can define a memo list
1686
+ for :math:`c'` to avoid recalculation. We cannot do the same for :math:`D`, however,
1687
+ because it is always a different matrix.
420
1688
 
1689
+ Parameters
1690
+ ----------
1691
+ anchors
1692
+ Anchors of an open cubic spline.
421
1693
 
422
- def diag_to_matrix(l_and_u: tuple[int, int], diag: np.ndarray) -> np.ndarray:
1694
+ Returns
1695
+ -------
1696
+ :class:`tuple` [:class:`~.Point3D_Array`, :class:`~.Point3D_Array`]
1697
+ A tuple of two arrays: one containing the 1st handle for every curve in
1698
+ the open cubic spline, and the other containing the 2nd handles.
423
1699
  """
424
- Converts array whose rows represent diagonal
425
- entries of a matrix into the matrix itself.
426
- See scipy.linalg.solve_banded
1700
+ global CP_OPEN_MEMO
1701
+
1702
+ A = np.asarray(anchors)
1703
+ N = A.shape[0] - 1
1704
+ dim = A.shape[1]
1705
+
1706
+ # Calculate cp (c prime) with help from CP_OPEN_MEMO.
1707
+ len_memo = CP_OPEN_MEMO.size
1708
+ if len_memo < N - 1:
1709
+ cp = np.empty(N - 1)
1710
+ cp[:len_memo] = CP_OPEN_MEMO
1711
+ for i in range(len_memo, N - 1):
1712
+ cp[i] = 1 / (4 - cp[i - 1])
1713
+ CP_OPEN_MEMO = cp
1714
+ else:
1715
+ cp = CP_OPEN_MEMO[: N - 1]
1716
+
1717
+ # Calculate Dp (D prime).
1718
+ Dp = np.empty((N, dim))
1719
+ Dp[0] = 0.5 * A[0] + A[1]
1720
+ AUX = 4 * A[1 : N - 1] + 2 * A[2:N] # Vectorize the sum for efficiency.
1721
+ for i in range(1, N - 1):
1722
+ Dp[i] = cp[i] * (AUX[i - 1] - Dp[i - 1])
1723
+ Dp[N - 1] = (1 / (7 - 2 * cp[N - 2])) * (8 * A[N - 1] + A[N] - 2 * Dp[N - 2])
1724
+
1725
+ # Backward Substitution.
1726
+ # H1 (array of the first handles) is defined as a view of Dp for efficiency
1727
+ # and semantic convenience at the same time.
1728
+ H1 = Dp
1729
+ # H1[N-1] = Dp[N-1] (redundant)
1730
+ for i in range(N - 2, -1, -1):
1731
+ H1[i] = Dp[i] - cp[i] * H1[i + 1]
1732
+
1733
+ # Calculate H2.
1734
+ H2 = np.empty((N, dim))
1735
+ H2[0 : N - 1] = 2 * A[1:N] - H1[1:N]
1736
+ H2[N - 1] = 0.5 * (A[N] + H1[N - 1])
1737
+
1738
+ return H1, H2
1739
+
1740
+
1741
+ @overload
1742
+ def get_quadratic_approximation_of_cubic(
1743
+ a0: Point3DLike, h0: Point3DLike, h1: Point3DLike, a1: Point3DLike
1744
+ ) -> QuadraticSpline: ...
1745
+
1746
+
1747
+ @overload
1748
+ def get_quadratic_approximation_of_cubic(
1749
+ a0: Point3DLike_Array,
1750
+ h0: Point3DLike_Array,
1751
+ h1: Point3DLike_Array,
1752
+ a1: Point3DLike_Array,
1753
+ ) -> QuadraticBezierPath: ...
1754
+
1755
+
1756
+ def get_quadratic_approximation_of_cubic(
1757
+ a0: Point3D | Point3D_Array,
1758
+ h0: Point3D | Point3D_Array,
1759
+ h1: Point3D | Point3D_Array,
1760
+ a1: Point3D | Point3D_Array,
1761
+ ) -> QuadraticSpline | QuadraticBezierPath:
1762
+ r"""If ``a0``, ``h0``, ``h1`` and ``a1`` are the control points of a cubic
1763
+ Bézier curve, approximate the curve with two quadratic Bézier curves and
1764
+ return an array of 6 points, where the first 3 points represent the first
1765
+ quadratic curve and the last 3 represent the second one.
1766
+
1767
+ Otherwise, if ``a0``, ``h0``, ``h1`` and ``a1`` are _arrays_ of :math:`N`
1768
+ points representing :math:`N` cubic Bézier curves, return an array of
1769
+ :math:`6N` points where each group of :math:`6` consecutive points
1770
+ approximates each of the :math:`N` curves in a similar way as above.
1771
+
1772
+ .. note::
1773
+ If the cubic spline given by the original cubic Bézier curves is
1774
+ smooth, this algorithm will generate a quadratic spline which is also
1775
+ smooth.
1776
+
1777
+ If a cubic Bézier is given by
1778
+
1779
+ .. math::
1780
+ C(t) = (1-t)^3 A_0 + 3(1-t)^2 t H_0 + 3(1-t)t^2 H_1 + t^3 A_1
1781
+
1782
+ where :math:`A_0`, :math:`H_0`, :math:`H_1` and :math:`A_1` are its
1783
+ control points, then this algorithm should generate two quadratic
1784
+ Béziers given by
1785
+
1786
+ .. math::
1787
+ Q_0(t) &= (1-t)^2 A_0 + 2(1-t)t M_0 + t^2 K \\
1788
+ Q_1(t) &= (1-t)^2 K + 2(1-t)t M_1 + t^2 A_1
1789
+
1790
+ where :math:`M_0` and :math:`M_1` are the respective handles to be
1791
+ found for both curves, and :math:`K` is the end anchor of the 1st curve
1792
+ and the start anchor of the 2nd, which must also be found.
1793
+
1794
+ To solve for :math:`M_0`, :math:`M_1` and :math:`K`, three conditions
1795
+ can be imposed:
1796
+
1797
+ 1. :math:`Q_0'(0) = \frac{1}{2}C'(0)`. The derivative of the first
1798
+ quadratic curve at :math:`t = 0` should be proportional to that of
1799
+ the original cubic curve, also at :math:`t = 0`. Because the cubic
1800
+ curve is split into two parts, it is necessary to divide this by
1801
+ two: the speed of a point travelling through the curve should be
1802
+ half of the original. This gives:
1803
+
1804
+ .. math::
1805
+ Q_0'(0) &= \frac{1}{2}C'(0) \\
1806
+ 2(M_0 - A_0) &= \frac{3}{2}(H_0 - A_0) \\
1807
+ 2M_0 - 2A_0 &= \frac{3}{2}H_0 - \frac{3}{2}A_0 \\
1808
+ 2M_0 &= \frac{3}{2}H_0 + \frac{1}{2}A_0 \\
1809
+ M_0 &= \frac{1}{4}(3H_0 + A_0)
1810
+
1811
+ 2. :math:`Q_1'(1) = \frac{1}{2}C'(1)`. The derivative of the second
1812
+ quadratic curve at :math:`t = 1` should be half of that of the
1813
+ original cubic curve for the same reasons as above, also at
1814
+ :math:`t = 1`. This gives:
1815
+
1816
+ .. math::
1817
+ Q_1'(1) &= \frac{1}{2}C'(1) \\
1818
+ 2(A_1 - M_1) &= \frac{3}{2}(A_1 - H_1) \\
1819
+ 2A_1 - 2M_1 &= \frac{3}{2}A_1 - \frac{3}{2}H_1 \\
1820
+ -2M_1 &= -\frac{1}{2}A_1 - \frac{3}{2}H_1 \\
1821
+ M_1 &= \frac{1}{4}(3H_1 + A_1)
1822
+
1823
+ 3. :math:`Q_0'(1) = Q_1'(0)`. The derivatives of both quadratic curves
1824
+ should match at the point :math:`K`, in order for the final spline
1825
+ to be smooth. This gives:
1826
+
1827
+ .. math::
1828
+ Q_0'(1) &= Q_1'(0) \\
1829
+ 2(K - M_0) &= 2(M_1 - K) \\
1830
+ 2K - 2M_0 &= 2M_1 - 2K \\
1831
+ 4K &= 2M_0 + 2M_1 \\
1832
+ K &= \frac{1}{2}(M_0 + M_1)
1833
+
1834
+ This is sufficient to find proper control points for the quadratic
1835
+ Bézier curves.
1836
+
1837
+ Parameters
1838
+ ----------
1839
+ a0
1840
+ The start anchor of a single cubic Bézier curve, or an array of
1841
+ :math:`N` start anchors for :math:`N` curves.
1842
+ h0
1843
+ The first handle of a single cubic Bézier curve, or an array of
1844
+ :math:`N` first handles for :math:`N` curves.
1845
+ h1
1846
+ The second handle of a single cubic Bézier curve, or an array of
1847
+ :math:`N` second handles for :math:`N` curves.
1848
+ a1
1849
+ The end anchor of a single cubic Bézier curve, or an array of
1850
+ :math:`N` end anchors for :math:`N` curves.
1851
+
1852
+ Returns
1853
+ -------
1854
+ result
1855
+ An array containing either 6 points for 2 quadratic Bézier curves
1856
+ approximating the original cubic curve, or :math:`6N` points for
1857
+ :math:`2N` quadratic curves approximating :math:`N` cubic curves.
1858
+
1859
+ Raises
1860
+ ------
1861
+ ValueError
1862
+ If ``a0``, ``h0``, ``h1`` and ``a1`` have different dimensions, or
1863
+ if their number of dimensions is not 1 or 2.
427
1864
  """
428
- l, u = l_and_u
429
- dim = diag.shape[1]
430
- matrix = np.zeros((dim, dim))
431
- for i in range(l + u + 1):
432
- np.fill_diagonal(
433
- matrix[max(0, i - u) :, max(0, u - i) :],
434
- diag[i, max(0, u - i) :],
435
- )
436
- return matrix
437
-
438
-
439
- # Given 4 control points for a cubic bezier curve (or arrays of such)
440
- # return control points for 2 quadratics (or 2n quadratics) approximating them.
441
- def get_quadratic_approximation_of_cubic(a0, h0, h1, a1):
442
- a0 = np.array(a0, ndmin=2)
443
- h0 = np.array(h0, ndmin=2)
444
- h1 = np.array(h1, ndmin=2)
445
- a1 = np.array(a1, ndmin=2)
446
- # Tangent vectors at the start and end.
447
- T0 = h0 - a0
448
- T1 = a1 - h1
449
-
450
- # Search for inflection points. If none are found, use the
451
- # midpoint as a cut point.
452
- # Based on http://www.caffeineowl.com/graphics/2d/vectorial/cubic-inflexion.html
453
- has_infl = np.ones(len(a0), dtype=bool)
454
-
455
- p = h0 - a0
456
- q = h1 - 2 * h0 + a0
457
- r = a1 - 3 * h1 + 3 * h0 - a0
458
-
459
- a = cross2d(q, r)
460
- b = cross2d(p, r)
461
- c = cross2d(p, q)
462
-
463
- disc = b * b - 4 * a * c
464
- has_infl &= disc > 0
465
- sqrt_disc = np.sqrt(np.abs(disc))
466
- settings = np.seterr(all="ignore")
467
- ti_bounds = []
468
- for sgn in [-1, +1]:
469
- ti = (-b + sgn * sqrt_disc) / (2 * a)
470
- ti[a == 0] = (-c / b)[a == 0]
471
- ti[(a == 0) & (b == 0)] = 0
472
- ti_bounds.append(ti)
473
- ti_min, ti_max = ti_bounds
474
- np.seterr(**settings)
475
- ti_min_in_range = has_infl & (0 < ti_min) & (ti_min < 1)
476
- ti_max_in_range = has_infl & (0 < ti_max) & (ti_max < 1)
477
-
478
- # Choose a value of t which starts at 0.5,
479
- # but is updated to one of the inflection points
480
- # if they lie between 0 and 1
481
-
482
- t_mid = 0.5 * np.ones(len(a0))
483
- t_mid[ti_min_in_range] = ti_min[ti_min_in_range]
484
- t_mid[ti_max_in_range] = ti_max[ti_max_in_range]
485
-
486
- m, n = a0.shape
487
- t_mid = t_mid.repeat(n).reshape((m, n))
488
-
489
- # Compute bezier point and tangent at the chosen value of t
490
- mid = bezier([a0, h0, h1, a1])(t_mid)
491
- Tm = bezier([h0 - a0, h1 - h0, a1 - h1])(t_mid)
492
-
493
- # Intersection between tangent lines at end points
494
- # and tangent in the middle
495
- i0 = find_intersection(a0, T0, mid, Tm)
496
- i1 = find_intersection(a1, T1, mid, Tm)
497
-
498
- m, n = np.shape(a0)
499
- result = np.zeros((6 * m, n))
500
- result[0::6] = a0
501
- result[1::6] = i0
502
- result[2::6] = mid
503
- result[3::6] = mid
504
- result[4::6] = i1
505
- result[5::6] = a1
1865
+ a0c = np.asarray(a0)
1866
+ h0c = np.asarray(h0)
1867
+ h1c = np.asarray(h1)
1868
+ a1c = np.asarray(a1)
1869
+
1870
+ if all(arr.ndim == 1 for arr in (a0c, h0c, h1c, a1c)):
1871
+ num_curves, dim = 1, a0c.shape[0]
1872
+ elif all(arr.ndim == 2 for arr in (a0c, h0c, h1c, a1c)):
1873
+ num_curves, dim = a0c.shape
1874
+ else:
1875
+ raise ValueError("All arguments must be Point3D or Point3D_Array.")
1876
+
1877
+ m0 = 0.25 * (3 * h0c + a0c)
1878
+ m1 = 0.25 * (3 * h1c + a1c)
1879
+ k = 0.5 * (m0 + m1)
1880
+
1881
+ result = np.empty((6 * num_curves, dim))
1882
+ result[0::6] = a0c
1883
+ result[1::6] = m0
1884
+ result[2::6] = k
1885
+ result[3::6] = k
1886
+ result[4::6] = m1
1887
+ result[5::6] = a1c
506
1888
  return result
507
1889
 
508
1890
 
509
- def is_closed(points: tuple[np.ndarray, np.ndarray]) -> bool:
510
- return np.allclose(points[0], points[-1])
1891
+ def is_closed(points: Point3D_Array) -> bool:
1892
+ """Returns ``True`` if the spline given by ``points`` is closed, by
1893
+ checking if its first and last points are close to each other, or``False``
1894
+ otherwise.
1895
+
1896
+ .. note::
1897
+
1898
+ This function reimplements :meth:`np.allclose`, because repeated
1899
+ calling of :meth:`np.allclose` for only 2 points is inefficient.
1900
+
1901
+ Parameters
1902
+ ----------
1903
+ points
1904
+ An array of points defining a spline.
1905
+
1906
+ Returns
1907
+ -------
1908
+ :class:`bool`
1909
+ Whether the first and last points of the array are close enough or not
1910
+ to be considered the same, thus considering the defined spline as
1911
+ closed.
1912
+
1913
+ Examples
1914
+ --------
1915
+ .. code-block:: pycon
1916
+
1917
+ >>> import numpy as np
1918
+ >>> from manim import is_closed
1919
+ >>> is_closed(
1920
+ ... np.array(
1921
+ ... [
1922
+ ... [0, 0, 0],
1923
+ ... [1, 2, 3],
1924
+ ... [3, 2, 1],
1925
+ ... [0, 0, 0],
1926
+ ... ]
1927
+ ... )
1928
+ ... )
1929
+ True
1930
+ >>> is_closed(
1931
+ ... np.array(
1932
+ ... [
1933
+ ... [0, 0, 0],
1934
+ ... [1, 2, 3],
1935
+ ... [3, 2, 1],
1936
+ ... [1e-10, 1e-10, 1e-10],
1937
+ ... ]
1938
+ ... )
1939
+ ... )
1940
+ True
1941
+ >>> is_closed(
1942
+ ... np.array(
1943
+ ... [
1944
+ ... [0, 0, 0],
1945
+ ... [1, 2, 3],
1946
+ ... [3, 2, 1],
1947
+ ... [1e-2, 1e-2, 1e-2],
1948
+ ... ]
1949
+ ... )
1950
+ ... )
1951
+ False
1952
+ """
1953
+ start, end = points[0], points[-1]
1954
+ rtol = 1e-5
1955
+ atol = 1e-8
1956
+ tolerance = atol + rtol * start
1957
+ if abs(end[0] - start[0]) > tolerance[0]:
1958
+ return False
1959
+ if abs(end[1] - start[1]) > tolerance[1]:
1960
+ return False
1961
+ return bool(abs(end[2] - start[2]) <= tolerance[2])
511
1962
 
512
1963
 
513
1964
  def proportions_along_bezier_curve_for_point(
514
- point: typing.Iterable[float | int],
515
- control_points: typing.Iterable[typing.Iterable[float | int]],
516
- round_to: float | int | None = 1e-6,
517
- ) -> np.ndarray:
1965
+ point: Point3DLike,
1966
+ control_points: BezierPointsLike,
1967
+ round_to: float = 1e-6,
1968
+ ) -> MatrixMN:
518
1969
  """Obtains the proportion along the bezier curve corresponding to a given point
519
1970
  given the bezier curve's control points.
520
1971
 
@@ -589,15 +2040,17 @@ def proportions_along_bezier_curve_for_point(
589
2040
  roots.append(polynom_roots)
590
2041
 
591
2042
  roots = [[root for root in rootlist if root.imag == 0] for rootlist in roots]
592
- roots = reduce(np.intersect1d, roots) # Get common roots.
593
- roots = np.array([r.real for r in roots if 0 <= r.real <= 1])
594
- return roots
2043
+ # Get common roots
2044
+ # arg-type: ignore
2045
+ roots = reduce(np.intersect1d, roots)
2046
+ result = np.asarray([r.real for r in roots if 0 <= r.real <= 1])
2047
+ return result
595
2048
 
596
2049
 
597
2050
  def point_lies_on_bezier(
598
- point: typing.Iterable[float | int],
599
- control_points: typing.Iterable[typing.Iterable[float | int]],
600
- round_to: float | int | None = 1e-6,
2051
+ point: Point3DLike,
2052
+ control_points: BezierPointsLike,
2053
+ round_to: float = 1e-6,
601
2054
  ) -> bool:
602
2055
  """Checks if a given point lies on the bezier curves with the given control points.
603
2056
 
@@ -621,7 +2074,6 @@ def point_lies_on_bezier(
621
2074
  bool
622
2075
  Whether the point lies on the curve.
623
2076
  """
624
-
625
2077
  roots = proportions_along_bezier_curve_for_point(point, control_points, round_to)
626
2078
 
627
2079
  return len(roots) > 0