manim 0.18.0.post0__py3-none-any.whl → 0.19.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of manim might be problematic. Click here for more details.
- manim/__init__.py +3 -6
- manim/__main__.py +61 -20
- manim/_config/__init__.py +6 -3
- manim/_config/cli_colors.py +16 -8
- manim/_config/default.cfg +1 -3
- manim/_config/logger_utils.py +14 -8
- manim/_config/utils.py +651 -472
- manim/animation/animation.py +152 -5
- manim/animation/composition.py +80 -39
- manim/animation/creation.py +196 -14
- manim/animation/fading.py +5 -9
- manim/animation/indication.py +103 -47
- manim/animation/movement.py +22 -5
- manim/animation/rotation.py +3 -2
- manim/animation/specialized.py +4 -6
- manim/animation/speedmodifier.py +10 -5
- manim/animation/transform.py +4 -5
- manim/animation/transform_matching_parts.py +1 -1
- manim/animation/updaters/mobject_update_utils.py +17 -14
- manim/camera/camera.py +15 -6
- manim/cli/__init__.py +17 -0
- manim/cli/cfg/group.py +70 -44
- manim/cli/checkhealth/checks.py +93 -75
- manim/cli/checkhealth/commands.py +14 -5
- manim/cli/default_group.py +157 -25
- manim/cli/init/commands.py +32 -24
- manim/cli/plugins/commands.py +16 -3
- manim/cli/render/commands.py +72 -60
- manim/cli/render/ease_of_access_options.py +4 -3
- manim/cli/render/global_options.py +51 -15
- manim/cli/render/output_options.py +6 -5
- manim/cli/render/render_options.py +97 -32
- manim/constants.py +65 -19
- manim/gui/gui.py +2 -0
- manim/mobject/frame.py +0 -1
- manim/mobject/geometry/arc.py +112 -78
- manim/mobject/geometry/boolean_ops.py +32 -25
- manim/mobject/geometry/labeled.py +300 -77
- manim/mobject/geometry/line.py +132 -64
- manim/mobject/geometry/polygram.py +126 -30
- manim/mobject/geometry/shape_matchers.py +35 -15
- manim/mobject/geometry/tips.py +38 -29
- manim/mobject/graph.py +414 -133
- manim/mobject/graphing/coordinate_systems.py +126 -64
- manim/mobject/graphing/functions.py +25 -15
- manim/mobject/graphing/number_line.py +24 -10
- manim/mobject/graphing/probability.py +2 -10
- manim/mobject/graphing/scale.py +6 -5
- manim/mobject/matrix.py +17 -19
- manim/mobject/mobject.py +314 -165
- manim/mobject/opengl/opengl_compatibility.py +2 -0
- manim/mobject/opengl/opengl_geometry.py +30 -9
- manim/mobject/opengl/opengl_image_mobject.py +2 -0
- manim/mobject/opengl/opengl_mobject.py +509 -343
- manim/mobject/opengl/opengl_point_cloud_mobject.py +5 -7
- manim/mobject/opengl/opengl_surface.py +3 -2
- manim/mobject/opengl/opengl_three_dimensions.py +2 -0
- manim/mobject/opengl/opengl_vectorized_mobject.py +46 -79
- manim/mobject/svg/brace.py +63 -13
- manim/mobject/svg/svg_mobject.py +4 -3
- manim/mobject/table.py +11 -13
- manim/mobject/text/code_mobject.py +186 -548
- manim/mobject/text/numbers.py +9 -7
- manim/mobject/text/tex_mobject.py +23 -14
- manim/mobject/text/text_mobject.py +70 -24
- manim/mobject/three_d/polyhedra.py +98 -1
- manim/mobject/three_d/three_d_utils.py +4 -4
- manim/mobject/three_d/three_dimensions.py +62 -34
- manim/mobject/types/image_mobject.py +42 -24
- manim/mobject/types/point_cloud_mobject.py +105 -67
- manim/mobject/types/vectorized_mobject.py +496 -228
- manim/mobject/value_tracker.py +5 -4
- manim/mobject/vector_field.py +5 -5
- manim/opengl/__init__.py +3 -3
- manim/plugins/__init__.py +14 -1
- manim/plugins/plugins_flags.py +14 -8
- manim/renderer/cairo_renderer.py +20 -10
- manim/renderer/opengl_renderer.py +21 -23
- manim/renderer/opengl_renderer_window.py +2 -0
- manim/renderer/shader.py +2 -3
- manim/renderer/shader_wrapper.py +5 -2
- manim/renderer/vectorized_mobject_rendering.py +5 -0
- manim/scene/moving_camera_scene.py +23 -0
- manim/scene/scene.py +90 -43
- manim/scene/scene_file_writer.py +316 -165
- manim/scene/section.py +17 -15
- manim/scene/three_d_scene.py +13 -21
- manim/scene/vector_space_scene.py +22 -9
- manim/typing.py +830 -70
- manim/utils/bezier.py +1667 -399
- manim/utils/caching.py +13 -5
- manim/utils/color/AS2700.py +2 -0
- manim/utils/color/BS381.py +3 -0
- manim/utils/color/DVIPSNAMES.py +96 -0
- manim/utils/color/SVGNAMES.py +179 -0
- manim/utils/color/X11.py +3 -0
- manim/utils/color/XKCD.py +3 -0
- manim/utils/color/__init__.py +8 -5
- manim/utils/color/core.py +844 -309
- manim/utils/color/manim_colors.py +7 -9
- manim/utils/commands.py +48 -20
- manim/utils/config_ops.py +18 -13
- manim/utils/debug.py +8 -7
- manim/utils/deprecation.py +90 -40
- manim/utils/docbuild/__init__.py +17 -0
- manim/utils/docbuild/autoaliasattr_directive.py +234 -0
- manim/utils/docbuild/autocolor_directive.py +21 -17
- manim/utils/docbuild/manim_directive.py +50 -35
- manim/utils/docbuild/module_parsing.py +245 -0
- manim/utils/exceptions.py +6 -0
- manim/utils/family.py +5 -3
- manim/utils/family_ops.py +17 -4
- manim/utils/file_ops.py +26 -16
- manim/utils/hashing.py +9 -7
- manim/utils/images.py +10 -4
- manim/utils/ipython_magic.py +14 -8
- manim/utils/iterables.py +161 -119
- manim/utils/module_ops.py +57 -19
- manim/utils/opengl.py +83 -24
- manim/utils/parameter_parsing.py +32 -0
- manim/utils/paths.py +21 -23
- manim/utils/polylabel.py +168 -0
- manim/utils/qhull.py +218 -0
- manim/utils/rate_functions.py +74 -39
- manim/utils/simple_functions.py +24 -15
- manim/utils/sounds.py +7 -1
- manim/utils/space_ops.py +125 -69
- manim/utils/testing/__init__.py +17 -0
- manim/utils/testing/_frames_testers.py +13 -8
- manim/utils/testing/_show_diff.py +5 -3
- manim/utils/testing/_test_class_makers.py +33 -18
- manim/utils/testing/frames_comparison.py +27 -19
- manim/utils/tex.py +127 -197
- manim/utils/tex_file_writing.py +47 -45
- manim/utils/tex_templates.py +2 -1
- manim/utils/unit.py +6 -5
- {manim-0.18.0.post0.dist-info → manim-0.19.0.dist-info}/LICENSE.community +1 -1
- {manim-0.18.0.post0.dist-info → manim-0.19.0.dist-info}/METADATA +40 -39
- manim-0.19.0.dist-info/RECORD +221 -0
- {manim-0.18.0.post0.dist-info → manim-0.19.0.dist-info}/WHEEL +1 -1
- manim/cli/new/__init__.py +0 -0
- manim/cli/new/group.py +0 -189
- manim/plugins/import_plugins.py +0 -43
- manim-0.18.0.post0.dist-info/RECORD +0 -217
- {manim-0.18.0.post0.dist-info → manim-0.19.0.dist-info}/LICENSE +0 -0
- {manim-0.18.0.post0.dist-info → manim-0.19.0.dist-info}/entry_points.txt +0 -0
manim/utils/bezier.py
CHANGED
|
@@ -2,95 +2,294 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from manim.typing import (
|
|
6
|
-
BezierPoints,
|
|
7
|
-
ColVector,
|
|
8
|
-
MatrixMN,
|
|
9
|
-
Point3D,
|
|
10
|
-
Point3D_Array,
|
|
11
|
-
PointDType,
|
|
12
|
-
QuadraticBezierPoints,
|
|
13
|
-
QuadraticBezierPoints_Array,
|
|
14
|
-
)
|
|
15
|
-
|
|
16
5
|
__all__ = [
|
|
17
6
|
"bezier",
|
|
18
7
|
"partial_bezier_points",
|
|
19
|
-
"
|
|
8
|
+
"split_bezier",
|
|
9
|
+
"subdivide_bezier",
|
|
10
|
+
"bezier_remap",
|
|
20
11
|
"interpolate",
|
|
21
12
|
"integer_interpolate",
|
|
22
13
|
"mid",
|
|
23
14
|
"inverse_interpolate",
|
|
24
15
|
"match_interpolate",
|
|
25
|
-
"get_smooth_handle_points",
|
|
26
16
|
"get_smooth_cubic_bezier_handle_points",
|
|
27
|
-
"diag_to_matrix",
|
|
28
17
|
"is_closed",
|
|
29
18
|
"proportions_along_bezier_curve_for_point",
|
|
30
19
|
"point_lies_on_bezier",
|
|
31
20
|
]
|
|
32
21
|
|
|
33
22
|
|
|
23
|
+
from collections.abc import Sequence
|
|
34
24
|
from functools import reduce
|
|
35
|
-
from typing import
|
|
25
|
+
from typing import TYPE_CHECKING, Callable, overload
|
|
36
26
|
|
|
37
27
|
import numpy as np
|
|
38
|
-
import numpy.typing as npt
|
|
39
|
-
from scipy import linalg
|
|
40
28
|
|
|
41
|
-
from
|
|
42
|
-
|
|
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]: ...
|
|
43
62
|
|
|
44
63
|
|
|
45
64
|
def bezier(
|
|
46
|
-
points:
|
|
47
|
-
) -> Callable[[float],
|
|
48
|
-
"""Classic implementation of a
|
|
65
|
+
points: Point3D_Array | Sequence[Point3D_Array],
|
|
66
|
+
) -> Callable[[float | ColVector], Point3D_Array]:
|
|
67
|
+
"""Classic implementation of a Bézier curve.
|
|
49
68
|
|
|
50
69
|
Parameters
|
|
51
70
|
----------
|
|
52
71
|
points
|
|
53
|
-
points defining
|
|
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.
|
|
54
76
|
|
|
55
77
|
Returns
|
|
56
78
|
-------
|
|
57
|
-
|
|
58
|
-
|
|
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**.
|
|
59
114
|
"""
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if n == 3:
|
|
63
|
-
return lambda t: np.asarray(
|
|
64
|
-
(1 - t) ** 3 * points[0]
|
|
65
|
-
+ 3 * t * (1 - t) ** 2 * points[1]
|
|
66
|
-
+ 3 * (1 - t) * t**2 * points[2]
|
|
67
|
-
+ t**3 * points[3],
|
|
68
|
-
dtype=PointDType,
|
|
69
|
-
)
|
|
70
|
-
# Quadratic Bezier curve
|
|
71
|
-
if n == 2:
|
|
72
|
-
return lambda t: np.asarray(
|
|
73
|
-
(1 - t) ** 2 * points[0] + 2 * t * (1 - t) * points[1] + t**2 * points[2],
|
|
74
|
-
dtype=PointDType,
|
|
75
|
-
)
|
|
115
|
+
P = np.asarray(points)
|
|
116
|
+
degree = P.shape[0] - 1
|
|
76
117
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
118
|
+
if degree == 0:
|
|
119
|
+
|
|
120
|
+
def zero_bezier(t: float | ColVector) -> Point3D | Point3D_Array:
|
|
121
|
+
return np.ones_like(t) * P[0]
|
|
122
|
+
|
|
123
|
+
return zero_bezier
|
|
124
|
+
|
|
125
|
+
if degree == 1:
|
|
126
|
+
|
|
127
|
+
def linear_bezier(t: float | ColVector) -> Point3D | Point3D_Array:
|
|
128
|
+
return P[0] + t * (P[1] - P[0])
|
|
129
|
+
|
|
130
|
+
return linear_bezier
|
|
86
131
|
|
|
132
|
+
if degree == 2:
|
|
87
133
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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]
|
|
92
139
|
|
|
93
|
-
|
|
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}
|
|
94
293
|
|
|
95
294
|
Parameters
|
|
96
295
|
----------
|
|
@@ -103,176 +302,763 @@ def partial_bezier_points(points: BezierPoints, a: float, b: float) -> BezierPoi
|
|
|
103
302
|
|
|
104
303
|
Returns
|
|
105
304
|
-------
|
|
106
|
-
|
|
107
|
-
|
|
305
|
+
:class:`~.BezierPoints`
|
|
306
|
+
An array containing the control points defining the partial Bézier curve.
|
|
108
307
|
"""
|
|
109
|
-
|
|
308
|
+
# Border cases
|
|
110
309
|
if a == 1:
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
return np.asarray(
|
|
119
|
-
[bezier(a_to_1[: i + 1])(end_prop) for i in range(_len)],
|
|
120
|
-
dtype=PointDType,
|
|
121
|
-
)
|
|
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
|
|
122
317
|
|
|
318
|
+
points = np.asarray(points)
|
|
319
|
+
degree = points.shape[0] - 1
|
|
123
320
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
) -> QuadraticBezierPoints:
|
|
129
|
-
if a == 1:
|
|
130
|
-
return np.asarray(3 * [points[-1]])
|
|
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
|
|
131
325
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
+
]
|
|
137
333
|
)
|
|
334
|
+
return portion_matrix @ points
|
|
138
335
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
h2 = curve(b) if b < 1 else points[2]
|
|
142
|
-
h1_prime = (1 - a) * points[1] + a * points[2]
|
|
143
|
-
end_prop = (b - a) / (1.0 - a)
|
|
144
|
-
h1 = (1 - end_prop) * h0 + end_prop * h1_prime
|
|
145
|
-
return np.asarray((h0, h1, h2))
|
|
336
|
+
if degree == 2:
|
|
337
|
+
ma, mb = 1 - a, 1 - b
|
|
146
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
|
|
147
347
|
|
|
148
|
-
|
|
149
|
-
|
|
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
|
+
)
|
|
356
|
+
|
|
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}
|
|
150
618
|
|
|
151
619
|
Parameters
|
|
152
620
|
----------
|
|
153
621
|
points
|
|
154
|
-
The control points of the
|
|
155
|
-
has shape ``[a1, h1, b1]``
|
|
622
|
+
The control points of the Bézier curve.
|
|
156
623
|
|
|
157
624
|
t
|
|
158
|
-
The ``t``-value at which to split the Bézier curve
|
|
625
|
+
The ``t``-value at which to split the Bézier curve.
|
|
159
626
|
|
|
160
627
|
Returns
|
|
161
628
|
-------
|
|
162
|
-
|
|
163
|
-
|
|
629
|
+
:class:`~.Point3D_Array`
|
|
630
|
+
An array containing the control points defining the two Bézier curves.
|
|
164
631
|
"""
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
+
)
|
|
169
659
|
|
|
170
|
-
|
|
660
|
+
return split_matrix @ points
|
|
171
661
|
|
|
662
|
+
if degree == 2:
|
|
663
|
+
mt = 1 - t
|
|
664
|
+
mt2 = mt * mt
|
|
665
|
+
t2 = t * t
|
|
666
|
+
two_tmt = 2 * t * mt
|
|
172
667
|
|
|
173
|
-
|
|
174
|
-
|
|
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
|
+
)
|
|
679
|
+
|
|
680
|
+
return split_matrix @ points
|
|
681
|
+
|
|
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)
|
|
708
|
+
|
|
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.
|
|
720
|
+
|
|
721
|
+
Parameters
|
|
722
|
+
----------
|
|
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.
|
|
728
|
+
|
|
729
|
+
Returns
|
|
730
|
+
-------
|
|
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.
|
|
175
825
|
|
|
176
826
|
The points at which the curve is split are located at the
|
|
177
|
-
arguments :math:`t = i
|
|
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.
|
|
902
|
+
|
|
903
|
+
.. image:: /_static/bezier_subdivision_example.png
|
|
178
904
|
|
|
179
905
|
Parameters
|
|
180
906
|
----------
|
|
181
907
|
points
|
|
182
|
-
The control points of the Bézier curve
|
|
908
|
+
The control points of the Bézier curve.
|
|
183
909
|
|
|
184
|
-
|
|
910
|
+
n_divisions
|
|
185
911
|
The number of curves to subdivide the Bézier curve into
|
|
186
912
|
|
|
187
913
|
Returns
|
|
188
914
|
-------
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
.. image:: /_static/bezier_subdivision_example.png
|
|
192
|
-
|
|
915
|
+
:class:`~.Spline`
|
|
916
|
+
An array containing the points defining the new :math:`n` subcurves.
|
|
193
917
|
"""
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
for j in range(0, n):
|
|
197
|
-
i = n - j
|
|
198
|
-
tmp = split_quadratic_bezier(current, 1 / i)
|
|
199
|
-
beziers[j] = tmp[:3]
|
|
200
|
-
current = tmp[3:]
|
|
201
|
-
return beziers.reshape(-1, 3)
|
|
918
|
+
if n_divisions == 1:
|
|
919
|
+
return points
|
|
202
920
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
921
|
+
points = np.asarray(points)
|
|
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``.
|
|
208
958
|
|
|
209
959
|
Parameters
|
|
210
960
|
----------
|
|
211
|
-
|
|
212
|
-
|
|
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`.
|
|
213
968
|
|
|
214
969
|
new_number_of_curves
|
|
215
970
|
The number of curves that the output will contain. This needs to be higher than the current number.
|
|
216
971
|
|
|
217
972
|
Returns
|
|
218
973
|
-------
|
|
219
|
-
|
|
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.
|
|
220
977
|
"""
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
|
242
1007
|
|
|
243
|
-
|
|
244
|
-
if difference <= 0:
|
|
245
|
-
return triplets
|
|
246
|
-
new_triplets = []
|
|
247
|
-
for triplet in triplets:
|
|
248
|
-
if difference > 0:
|
|
249
|
-
tmp_noc = int(np.ceil(difference / len(triplets))) + 1
|
|
250
|
-
tmp = subdivide_quadratic_bezier(triplet, tmp_noc).reshape(-1, 3, 3)
|
|
251
|
-
for i in range(tmp_noc):
|
|
252
|
-
new_triplets.append(tmp[i])
|
|
253
|
-
difference -= tmp_noc - 1
|
|
254
|
-
else:
|
|
255
|
-
new_triplets.append(triplet)
|
|
256
|
-
return new_triplets
|
|
257
|
-
"""
|
|
1008
|
+
return new_tuples
|
|
258
1009
|
|
|
259
1010
|
|
|
260
1011
|
# Linear interpolation variants
|
|
261
1012
|
|
|
262
1013
|
|
|
263
1014
|
@overload
|
|
264
|
-
def interpolate(start: float, end: float, alpha: float) -> float:
|
|
265
|
-
|
|
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: ...
|
|
266
1024
|
|
|
267
1025
|
|
|
268
1026
|
@overload
|
|
269
|
-
def interpolate(start: Point3D, end: Point3D, alpha:
|
|
270
|
-
...
|
|
1027
|
+
def interpolate(start: Point3D, end: Point3D, alpha: ColVector) -> Point3D_Array: ...
|
|
271
1028
|
|
|
272
1029
|
|
|
273
1030
|
def interpolate(
|
|
274
|
-
start:
|
|
275
|
-
|
|
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
|
+
"""
|
|
276
1062
|
return (1 - alpha) * start + alpha * end
|
|
277
1063
|
|
|
278
1064
|
|
|
@@ -321,13 +1107,11 @@ def integer_interpolate(
|
|
|
321
1107
|
|
|
322
1108
|
|
|
323
1109
|
@overload
|
|
324
|
-
def mid(start: float, end: float) -> float:
|
|
325
|
-
...
|
|
1110
|
+
def mid(start: float, end: float) -> float: ...
|
|
326
1111
|
|
|
327
1112
|
|
|
328
1113
|
@overload
|
|
329
|
-
def mid(start: Point3D, end: Point3D) -> Point3D:
|
|
330
|
-
...
|
|
1114
|
+
def mid(start: Point3D, end: Point3D) -> Point3D: ...
|
|
331
1115
|
|
|
332
1116
|
|
|
333
1117
|
def mid(start: float | Point3D, end: float | Point3D) -> float | Point3D:
|
|
@@ -348,22 +1132,21 @@ def mid(start: float | Point3D, end: float | Point3D) -> float | Point3D:
|
|
|
348
1132
|
|
|
349
1133
|
|
|
350
1134
|
@overload
|
|
351
|
-
def inverse_interpolate(start: float, end: float, value: float) -> float:
|
|
352
|
-
...
|
|
1135
|
+
def inverse_interpolate(start: float, end: float, value: float) -> float: ...
|
|
353
1136
|
|
|
354
1137
|
|
|
355
1138
|
@overload
|
|
356
|
-
def inverse_interpolate(start: float, end: float, value: Point3D) -> Point3D:
|
|
357
|
-
...
|
|
1139
|
+
def inverse_interpolate(start: float, end: float, value: Point3D) -> Point3D: ...
|
|
358
1140
|
|
|
359
1141
|
|
|
360
1142
|
@overload
|
|
361
|
-
def inverse_interpolate(start: Point3D, end: Point3D, value: Point3D) -> Point3D:
|
|
362
|
-
...
|
|
1143
|
+
def inverse_interpolate(start: Point3D, end: Point3D, value: Point3D) -> Point3D: ...
|
|
363
1144
|
|
|
364
1145
|
|
|
365
1146
|
def inverse_interpolate(
|
|
366
|
-
start: float | Point3D,
|
|
1147
|
+
start: float | Point3D,
|
|
1148
|
+
end: float | Point3D,
|
|
1149
|
+
value: float | Point3D,
|
|
367
1150
|
) -> float | Point3D:
|
|
368
1151
|
"""Perform inverse interpolation to determine the alpha
|
|
369
1152
|
values that would produce the specified ``value``
|
|
@@ -390,7 +1173,7 @@ def inverse_interpolate(
|
|
|
390
1173
|
.. code-block:: pycon
|
|
391
1174
|
|
|
392
1175
|
>>> inverse_interpolate(start=2, end=6, value=4)
|
|
393
|
-
0.5
|
|
1176
|
+
np.float64(0.5)
|
|
394
1177
|
|
|
395
1178
|
>>> start = np.array([1, 2, 1])
|
|
396
1179
|
>>> end = np.array([7, 8, 11])
|
|
@@ -408,8 +1191,7 @@ def match_interpolate(
|
|
|
408
1191
|
old_start: float,
|
|
409
1192
|
old_end: float,
|
|
410
1193
|
old_value: float,
|
|
411
|
-
) -> float:
|
|
412
|
-
...
|
|
1194
|
+
) -> float: ...
|
|
413
1195
|
|
|
414
1196
|
|
|
415
1197
|
@overload
|
|
@@ -419,8 +1201,7 @@ def match_interpolate(
|
|
|
419
1201
|
old_start: float,
|
|
420
1202
|
old_end: float,
|
|
421
1203
|
old_value: Point3D,
|
|
422
|
-
) -> Point3D:
|
|
423
|
-
...
|
|
1204
|
+
) -> Point3D: ...
|
|
424
1205
|
|
|
425
1206
|
|
|
426
1207
|
def match_interpolate(
|
|
@@ -454,249 +1235,737 @@ def match_interpolate(
|
|
|
454
1235
|
Examples
|
|
455
1236
|
--------
|
|
456
1237
|
>>> match_interpolate(0, 100, 10, 20, 15)
|
|
457
|
-
50.0
|
|
1238
|
+
np.float64(50.0)
|
|
458
1239
|
"""
|
|
459
1240
|
old_alpha = inverse_interpolate(old_start, old_end, old_value)
|
|
460
1241
|
return interpolate(
|
|
461
1242
|
new_start,
|
|
462
1243
|
new_end,
|
|
463
|
-
old_alpha,
|
|
1244
|
+
old_alpha,
|
|
464
1245
|
)
|
|
465
1246
|
|
|
466
1247
|
|
|
1248
|
+
# Figuring out which Bézier curves most smoothly connect a sequence of points
|
|
467
1249
|
def get_smooth_cubic_bezier_handle_points(
|
|
468
|
-
|
|
469
|
-
) -> tuple[
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
if num_handles < 1:
|
|
474
|
-
return np.zeros((0, dim)), np.zeros((0, dim))
|
|
475
|
-
# Must solve 2*num_handles equations to get the handles.
|
|
476
|
-
# l and u are the number of lower an upper diagonal rows
|
|
477
|
-
# in the matrix to solve.
|
|
478
|
-
l, u = 2, 1
|
|
479
|
-
# diag is a representation of the matrix in diagonal form
|
|
480
|
-
# See https://www.particleincell.com/2012/bezier-splines/
|
|
481
|
-
# for how to arrive at these equations
|
|
482
|
-
diag: MatrixMN = np.zeros((l + u + 1, 2 * num_handles))
|
|
483
|
-
diag[0, 1::2] = -1
|
|
484
|
-
diag[0, 2::2] = 1
|
|
485
|
-
diag[1, 0::2] = 2
|
|
486
|
-
diag[1, 1::2] = 1
|
|
487
|
-
diag[2, 1:-2:2] = -2
|
|
488
|
-
diag[3, 0:-3:2] = 1
|
|
489
|
-
# last
|
|
490
|
-
diag[2, -2] = -1
|
|
491
|
-
diag[1, -1] = 2
|
|
492
|
-
# This is the b as in Ax = b, where we are solving for x,
|
|
493
|
-
# and A is represented using diag. However, think of entries
|
|
494
|
-
# to x and b as being points in space, not numbers
|
|
495
|
-
b: Point3D_Array = np.zeros((2 * num_handles, dim))
|
|
496
|
-
b[1::2] = 2 * points[1:]
|
|
497
|
-
b[0] = points[0]
|
|
498
|
-
b[-1] = points[-1]
|
|
499
|
-
|
|
500
|
-
def solve_func(b: ColVector) -> ColVector | MatrixMN:
|
|
501
|
-
return linalg.solve_banded((l, u), diag, b) # type: ignore
|
|
502
|
-
|
|
503
|
-
use_closed_solve_function = is_closed(points)
|
|
504
|
-
if use_closed_solve_function:
|
|
505
|
-
# Get equations to relate first and last points
|
|
506
|
-
matrix = diag_to_matrix((l, u), diag)
|
|
507
|
-
# last row handles second derivative
|
|
508
|
-
matrix[-1, [0, 1, -2, -1]] = [2, -1, 1, -2]
|
|
509
|
-
# first row handles first derivative
|
|
510
|
-
matrix[0, :] = np.zeros(matrix.shape[1])
|
|
511
|
-
matrix[0, [0, -1]] = [1, 1]
|
|
512
|
-
b[0] = 2 * points[0]
|
|
513
|
-
b[-1] = np.zeros(dim)
|
|
514
|
-
|
|
515
|
-
def closed_curve_solve_func(b: ColVector) -> ColVector | MatrixMN:
|
|
516
|
-
return linalg.solve(matrix, b) # type: ignore
|
|
517
|
-
|
|
518
|
-
handle_pairs = np.zeros((2 * num_handles, dim))
|
|
519
|
-
for i in range(dim):
|
|
520
|
-
if use_closed_solve_function:
|
|
521
|
-
handle_pairs[:, i] = closed_curve_solve_func(b[:, i])
|
|
522
|
-
else:
|
|
523
|
-
handle_pairs[:, i] = solve_func(b[:, i])
|
|
524
|
-
return handle_pairs[0::2], handle_pairs[1::2]
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
def get_smooth_handle_points(
|
|
528
|
-
points: BezierPoints,
|
|
529
|
-
) -> tuple[BezierPoints, BezierPoints]:
|
|
530
|
-
"""Given some anchors (points), compute handles so the resulting bezier curve is smooth.
|
|
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.
|
|
531
1255
|
|
|
532
1256
|
Parameters
|
|
533
1257
|
----------
|
|
534
|
-
|
|
535
|
-
Anchors.
|
|
1258
|
+
anchors
|
|
1259
|
+
Anchors of a cubic spline.
|
|
536
1260
|
|
|
537
1261
|
Returns
|
|
538
1262
|
-------
|
|
539
|
-
|
|
540
|
-
|
|
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.
|
|
541
1266
|
"""
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
1267
|
+
anchors = np.asarray(anchors)
|
|
1268
|
+
n_anchors = anchors.shape[0]
|
|
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]
|
|
547
1274
|
return np.zeros((0, dim)), np.zeros((0, dim))
|
|
548
|
-
# Must solve 2*num_handles equations to get the handles.
|
|
549
|
-
# l and u are the number of lower an upper diagonal rows
|
|
550
|
-
# in the matrix to solve.
|
|
551
|
-
l, u = 2, 1
|
|
552
|
-
# diag is a representation of the matrix in diagonal form
|
|
553
|
-
# See https://www.particleincell.com/2012/bezier-splines/
|
|
554
|
-
# for how to arrive at these equations
|
|
555
|
-
diag: MatrixMN = np.zeros((l + u + 1, 2 * num_handles))
|
|
556
|
-
diag[0, 1::2] = -1
|
|
557
|
-
diag[0, 2::2] = 1
|
|
558
|
-
diag[1, 0::2] = 2
|
|
559
|
-
diag[1, 1::2] = 1
|
|
560
|
-
diag[2, 1:-2:2] = -2
|
|
561
|
-
diag[3, 0:-3:2] = 1
|
|
562
|
-
# last
|
|
563
|
-
diag[2, -2] = -1
|
|
564
|
-
diag[1, -1] = 2
|
|
565
|
-
# This is the b as in Ax = b, where we are solving for x,
|
|
566
|
-
# and A is represented using diag. However, think of entries
|
|
567
|
-
# to x and b as being points in space, not numbers
|
|
568
|
-
b = np.zeros((2 * num_handles, dim))
|
|
569
|
-
b[1::2] = 2 * points[1:]
|
|
570
|
-
b[0] = points[0]
|
|
571
|
-
b[-1] = points[-1]
|
|
572
|
-
|
|
573
|
-
def solve_func(b: ColVector) -> ColVector | MatrixMN:
|
|
574
|
-
return linalg.solve_banded((l, u), diag, b) # type: ignore
|
|
575
|
-
|
|
576
|
-
use_closed_solve_function = is_closed(points)
|
|
577
|
-
if use_closed_solve_function:
|
|
578
|
-
# Get equations to relate first and last points
|
|
579
|
-
matrix = diag_to_matrix((l, u), diag)
|
|
580
|
-
# last row handles second derivative
|
|
581
|
-
matrix[-1, [0, 1, -2, -1]] = [2, -1, 1, -2]
|
|
582
|
-
# first row handles first derivative
|
|
583
|
-
matrix[0, :] = np.zeros(matrix.shape[1])
|
|
584
|
-
matrix[0, [0, -1]] = [1, 1]
|
|
585
|
-
b[0] = 2 * points[0]
|
|
586
|
-
b[-1] = np.zeros(dim)
|
|
587
|
-
|
|
588
|
-
def closed_curve_solve_func(b: ColVector) -> ColVector | MatrixMN:
|
|
589
|
-
return linalg.solve(matrix, b) # type: ignore
|
|
590
|
-
|
|
591
|
-
handle_pairs = np.zeros((2 * num_handles, dim))
|
|
592
|
-
for i in range(dim):
|
|
593
|
-
if use_closed_solve_function:
|
|
594
|
-
handle_pairs[:, i] = closed_curve_solve_func(b[:, i])
|
|
595
|
-
else:
|
|
596
|
-
handle_pairs[:, i] = solve_func(b[:, i])
|
|
597
|
-
return handle_pairs[0::2], handle_pairs[1::2]
|
|
598
1275
|
|
|
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.
|
|
599
1508
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
1509
|
+
Parameters
|
|
1510
|
+
----------
|
|
1511
|
+
anchors
|
|
1512
|
+
Anchors of a closed cubic spline.
|
|
1513
|
+
|
|
1514
|
+
Returns
|
|
1515
|
+
-------
|
|
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.
|
|
603
1519
|
"""
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
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.
|
|
1688
|
+
|
|
1689
|
+
Parameters
|
|
1690
|
+
----------
|
|
1691
|
+
anchors
|
|
1692
|
+
Anchors of an open cubic spline.
|
|
1693
|
+
|
|
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.
|
|
607
1699
|
"""
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
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: ...
|
|
617
1754
|
|
|
618
1755
|
|
|
619
|
-
# Given 4 control points for a cubic bezier curve (or arrays of such)
|
|
620
|
-
# return control points for 2 quadratics (or 2n quadratics) approximating them.
|
|
621
1756
|
def get_quadratic_approximation_of_cubic(
|
|
622
|
-
a0: Point3D
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
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.
|
|
1864
|
+
"""
|
|
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
|
|
688
1888
|
return result
|
|
689
1889
|
|
|
690
1890
|
|
|
691
1891
|
def is_closed(points: Point3D_Array) -> bool:
|
|
692
|
-
|
|
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])
|
|
693
1962
|
|
|
694
1963
|
|
|
695
1964
|
def proportions_along_bezier_curve_for_point(
|
|
696
|
-
point:
|
|
697
|
-
control_points:
|
|
1965
|
+
point: Point3DLike,
|
|
1966
|
+
control_points: BezierPointsLike,
|
|
698
1967
|
round_to: float = 1e-6,
|
|
699
|
-
) ->
|
|
1968
|
+
) -> MatrixMN:
|
|
700
1969
|
"""Obtains the proportion along the bezier curve corresponding to a given point
|
|
701
1970
|
given the bezier curve's control points.
|
|
702
1971
|
|
|
@@ -765,7 +2034,7 @@ def proportions_along_bezier_curve_for_point(
|
|
|
765
2034
|
# Roots will be none, but in this specific instance, we don't need to consider that.
|
|
766
2035
|
continue
|
|
767
2036
|
bezier_polynom = np.polynomial.Polynomial(terms[::-1])
|
|
768
|
-
polynom_roots = bezier_polynom.roots()
|
|
2037
|
+
polynom_roots = bezier_polynom.roots()
|
|
769
2038
|
if len(polynom_roots) > 0:
|
|
770
2039
|
polynom_roots = np.around(polynom_roots, int(np.log10(1 / round_to)))
|
|
771
2040
|
roots.append(polynom_roots)
|
|
@@ -773,14 +2042,14 @@ def proportions_along_bezier_curve_for_point(
|
|
|
773
2042
|
roots = [[root for root in rootlist if root.imag == 0] for rootlist in roots]
|
|
774
2043
|
# Get common roots
|
|
775
2044
|
# arg-type: ignore
|
|
776
|
-
roots = reduce(np.intersect1d, roots)
|
|
2045
|
+
roots = reduce(np.intersect1d, roots)
|
|
777
2046
|
result = np.asarray([r.real for r in roots if 0 <= r.real <= 1])
|
|
778
2047
|
return result
|
|
779
2048
|
|
|
780
2049
|
|
|
781
2050
|
def point_lies_on_bezier(
|
|
782
|
-
point:
|
|
783
|
-
control_points:
|
|
2051
|
+
point: Point3DLike,
|
|
2052
|
+
control_points: BezierPointsLike,
|
|
784
2053
|
round_to: float = 1e-6,
|
|
785
2054
|
) -> bool:
|
|
786
2055
|
"""Checks if a given point lies on the bezier curves with the given control points.
|
|
@@ -805,7 +2074,6 @@ def point_lies_on_bezier(
|
|
|
805
2074
|
bool
|
|
806
2075
|
Whether the point lies on the curve.
|
|
807
2076
|
"""
|
|
808
|
-
|
|
809
2077
|
roots = proportions_along_bezier_curve_for_point(point, control_points, round_to)
|
|
810
2078
|
|
|
811
2079
|
return len(roots) > 0
|