bezier
¶
bezier package¶
Helper for Bézier Curves, Triangles, and Higher Order Objects.
Intended to perform basic operations on Bézier objects such as intersections, length/area/etc. computations, subdivision, implicitization and other relevant information.
Plotting utilities will also be provided.
Submodules¶
bezier.curve module¶
Helper for Bézier Curves.
See Curve-Curve Intersection for examples using the
Curve
class to find intersections.
-
class
bezier.curve.
Curve
(nodes, start=0.0, end=1.0, root=None, _copy=True)¶ Bases:
bezier._base.Base
Represents a Bézier curve.
We take the traditional definition: a Bézier curve is a mapping from \(s \in \left[0, 1\right]\) to convex combinations of points \(v_0, v_1, \ldots, v_n\) in some vector space:
\[B(s) = \sum_{j = 0}^n \binom{n}{j} s^j (1 - s)^{n - j} \cdot v_j\]>>> import bezier >>> nodes = np.array([ ... [0.0 , 0.0], ... [0.625, 0.5], ... [1.0 , 0.5], ... ]) >>> curve = bezier.Curve(nodes) >>> curve <Curve (degree=2, dimension=2)>
Parameters: - nodes (numpy.ndarray) – The nodes in the curve. The rows represent each node while the columns are the dimension of the ambient space.
- start (
Optional
[float
]) – The beginning of the sub-interval that this curve represents. - end (
Optional
[float
]) – The end of the sub-interval that this curve represents. - root (
Optional
[Curve
]) – The root curve that contains this current curve. - _copy (bool) – Flag indicating if the nodes should be copied before
being stored. Defaults to
True
since callers may freely mutatenodes
after passing in.
-
length
¶ float: The length of the current curve.
-
start
¶ float: Start of sub-interval this curve represents.
This value is used to track the current curve in the re-parameterization / subdivision process. The curve is still defined on the unit interval, but this value illustrates how this curve relates to a “parent” curve. For example:
>>> nodes = np.array([ ... [0.0, 0.0], ... [1.0, 2.0], ... ]) >>> curve = bezier.Curve(nodes) >>> curve <Curve (degree=1, dimension=2)> >>> left, right = curve.subdivide() >>> left <Curve (degree=1, dimension=2, start=0, end=0.5)> >>> right <Curve (degree=1, dimension=2, start=0.5, end=1)> >>> _, mid_right = left.subdivide() >>> mid_right <Curve (degree=1, dimension=2, start=0.25, end=0.5)> >>> mid_right.nodes array([[ 0.25, 0.5 ], [ 0.5 , 1. ]])
-
root
¶ Curve: The “root” curve that contains the current curve.
This indicates that the current curve is a section of the “root” curve. For example:
>>> _, right = curve.subdivide() >>> right <Curve (degree=2, dimension=2, start=0.5, end=1)> >>> right.root is curve True >>> right.evaluate(0.0) == curve.evaluate(0.5) array([ True, True], dtype=bool) >>> >>> mid_left, _ = right.subdivide() >>> mid_left <Curve (degree=2, dimension=2, start=0.5, end=0.75)> >>> mid_left.root is curve True >>> mid_left.evaluate(1.0) == curve.evaluate(0.75) array([ True, True], dtype=bool)
-
edge_index
¶ Optional
[int
] : The index of the edge among a group of edges.>>> curve.edge_index 1 >>> curve.previous_edge <Curve (degree=1, dimension=2)> >>> curve.previous_edge.edge_index 0 >>> curve.next_edge <Curve (degree=1, dimension=2)> >>> curve.next_edge.edge_index 2
This is intended to be used when a
Curve
is created as part of a larger structure like aSurface
orCurvedPolygon
.
-
next_edge
¶ Optional
[Curve
] : An edge that comes after the current one.This is intended to be used when a
Curve
is created as part of a larger structure like aSurface
orCurvedPolygon
.
-
previous_edge
¶ Optional
[Curve
] : An edge that comes before the current one.This is intended to be used when a
Curve
is created as part of a larger structure like aSurface
orCurvedPolygon
.
-
evaluate
(s)¶ Evaluate \(B(s)\) along the curve.
See
evaluate_multi()
for more details.>>> nodes = np.array([ ... [0.0 , 0.0], ... [0.625, 0.5], ... [1.0 , 0.5], ... ]) >>> curve = bezier.Curve(nodes) >>> curve.evaluate(0.75) array([ 0.796875, 0.46875 ])
Parameters: s (float) – Parameter along the curve. Returns: The point on the curve (as a one dimensional NumPy array). Return type: numpy.ndarray
-
evaluate_multi
(s_vals)¶ Evaluate \(B(s)\) for multiple points along the curve.
This is done by first evaluating each member of the Bernstein basis at each value in
s_vals
and then applying those to the control points for the current curve.This is done instead of using de Casteljau’s algorithm. Implementing de Casteljau is problematic because it requires a choice between one of two methods:
- vectorize operations of the form \((1 - s)v + s w\),
which requires a copy of the curve’s control points for
each value in
s_vals
- avoid vectorization and compute each point in serial
Instead, we can use vectorized operations to build up the Bernstein basis values.
>>> nodes = np.array([ ... [0.0, 0.0, 0.0], ... [1.0, 2.0, 3.0], ... ]) >>> curve = bezier.Curve(nodes) >>> curve <Curve (degree=1, dimension=3)> >>> s_vals = np.linspace(0.0, 1.0, 5) >>> curve.evaluate_multi(s_vals) array([[ 0. , 0. , 0. ], [ 0.25, 0.5 , 0.75], [ 0.5 , 1. , 1.5 ], [ 0.75, 1.5 , 2.25], [ 1. , 2. , 3. ]])
Parameters: s_vals (numpy.ndarray) – Parameters along the curve (as a 1D array). Returns: The points on the curve. As a two dimensional NumPy array, with the rows corresponding to each s
value and the columns to the dimension.Return type: numpy.ndarray - vectorize operations of the form \((1 - s)v + s w\),
which requires a copy of the curve’s control points for
each value in
-
plot
(num_pts, ax=None, show=False)¶ Plot the current curve.
Parameters: - num_pts (int) – Number of points to plot.
- ax (
Optional
[matplotlib.artist.Artist
]) – matplotlib axis object to add plot to. - show (
Optional
[bool
]) – Flag indicating if the plot should be shown.
Returns: The axis containing the plot. This may be a newly created axis.
Return type: Raises: NotImplementedError
– If the curve’s dimension is not2
.
-
subdivide
()¶ Split the curve \(B(s)\) into a left and right half.
Takes the interval \(\left[0, 1\right]\) and splits the curve into \(B_1 = B\left(\left[0, \frac{1}{2}\right]\right)\) and \(B_2 = B\left(\left[\frac{1}{2}, 1\right]\right)\). In order to do this, also reparameterizes the curve, hence the resulting left and right halves have new nodes.
>>> nodes = np.array([ ... [0.0 , 0.0], ... [1.25, 3.0], ... [2.0 , 1.0], ... ]) >>> curve = bezier.Curve(nodes) >>> left, right = curve.subdivide() >>> left <Curve (degree=2, dimension=2, start=0, end=0.5)> >>> left.nodes array([[ 0. , 0. ], [ 0.625, 1.5 ], [ 1.125, 1.75 ]]) >>> right <Curve (degree=2, dimension=2, start=0.5, end=1)> >>> right.nodes array([[ 1.125, 1.75 ], [ 1.625, 2. ], [ 2. , 1. ]])
Returns: The left and right sub-curves. Return type: Tuple
[Curve
,Curve
]
-
intersect
(other)¶ Find the points of intersection with another curve.
See Curve-Curve Intersection for more details.
>>> nodes1 = np.array([ ... [0.0 , 0.0 ], ... [0.375, 0.75 ], ... [0.75 , 0.375], ... ]) >>> curve1 = bezier.Curve(nodes1) >>> nodes2 = np.array([ ... [0.5, 0.0 ], ... [0.5, 0.75], ... ]) >>> curve2 = bezier.Curve(nodes2) >>> intersections = curve1.intersect(curve2) >>> intersections array([[ 0.5, 0.5]])
Parameters: other (Curve) – Other curve to intersect with.
Returns: Array of intersection points (possibly empty).
Return type: Raises: TypeError
– Ifother
is not a curve.NotImplementedError
– If both curves aren’t two-dimensional.
-
elevate
()¶ Return a degree-elevated version of the current curve.
Does this by converting the current nodes \(v_0, \ldots, v_n\) to new nodes \(w_0, \ldots, w_{n + 1}\) where
\[\begin{split}\begin{align*} w_0 &= v_0 \\ w_j &= \frac{j}{n + 1} v_{j - 1} + \frac{n + 1 - j}{n + 1} v_j \\ w_{n + 1} &= v_n \end{align*}\end{split}\]Returns: The degree-elevated curve. Return type: Curve
-
specialize
(start, end)¶ Specialize the curve to a given sub-interval.
>>> curve = bezier.Curve(np.array([ ... [0.0, 0.0], ... [0.5, 1.0], ... [1.0, 0.0], ... ])) >>> new_curve = curve.specialize(-0.25, 0.75) >>> new_curve <Curve (degree=2, dimension=2, start=-0.25, end=0.75)> >>> new_curve.nodes array([[-0.25 , -0.625], [ 0.25 , 0.875], [ 0.75 , 0.375]])
This is generalized version of
subdivide()
, and can even match the output of that method:>>> left, right = curve.subdivide() >>> left == curve.specialize(0.0, 0.5) True >>> right == curve.specialize(0.5, 1.0) True
Parameters: Returns: The newly-specialized curve.
Return type:
-
__eq__
(other)¶ Check equality against another shape.
Returns: Boolean indicating if the shapes are the same. Return type: bool
-
__ne__
(other)¶ Check inequality against another shape.
Returns: Boolean indicating if the shapes are not the same. Return type: bool
-
copy
()¶ Make a copy of the current shape.
Returns: Instance of the current shape.
-
degree
¶ int: The degree of the current shape.
-
dimension
¶ int: The dimension that the shape lives in.
For example, if the shape lives in \(\mathbf{R}^3\), then the dimension is
3
.
-
locate
(point)¶ Find a point on the current curve.
Solves for \(s\) in \(B(s) = p\).
Note
A unique solution is only guaranteed if the current curve has no self-intersections. This code assumes, but doesn’t check, that this is true.
>>> curve = bezier.Curve(np.array([ ... [0.0, 0.0], ... [1.0, 2.0], ... [3.0, 1.0], ... [4.0, 0.0], ... ])) >>> point1 = np.array([[3.09375, 0.703125]]) >>> s = curve.locate(point1) >>> s 0.75 >>> point2 = np.array([[2.0, 0.5]]) >>> curve.locate(point2) is None True
Parameters: point (numpy.ndarray) – A ( 1xD
) point on the curve, where \(D\) is the dimension of the curve.Returns: The parameter value (\(s\)) corresponding to point
orNone
if the point is not on thecurve
.Return type: Optional
[float
]Raises: ValueError
– If the dimension of thepoint
doesn’t match the dimension of the current curve.
-
nodes
¶ numpy.ndarray: The nodes that define the current shape.
bezier.curved_polygon module¶
Curved polygon and associated helpers.
A curved polygon (in \(\mathbf{R}^2\)) is defined by the collection of Bézier curves that determine the boundary.
-
class
bezier.curved_polygon.
CurvedPolygon
(*edges)¶ Bases:
object
Represents an object defined by its curved boundary.
The boundary is a piecewise defined collection of Bézier curves.
Note
The direction of the nodes in each
Curve
on the boundary is important: we check that one curve begins where the last one ended.>>> import bezier >>> edge0 = bezier.Curve(np.array([ ... [0.0, 0.0], ... [1.0, -1.0], ... [2.0, 0.0], ... ])) >>> edge1 = bezier.Curve(np.array([ ... [2.0, 0.0], ... [2.0, 1.0], ... ])) >>> edge2 = bezier.Curve(np.array([ ... [2.0, 1.0], ... [1.0, 2.0], ... [0.0, 1.0], ... ])) >>> edge3 = bezier.Curve(np.array([ ... [0.0, 1.0], ... [0.0, 0.0], ... ])) >>> curved_poly = bezier.CurvedPolygon( ... edge0, edge1, edge2, edge3) >>> curved_poly <CurvedPolygon (num_sides=4)>
Though the endpoints of edge pair of edges are verified to match, the curved polygon as a whole is not verified, so creating a curved polygon with self-intersections is possible:
>>> edge0 = bezier.Curve(np.array([ ... [0.0, 0.0], ... [1.0, 0.0], ... ])) >>> edge1 = bezier.Curve(np.array([ ... [1.0 , 0.0], ... [1.25, 0.5], ... [1.0 , 1.0], ... ])) >>> edge2 = bezier.Curve(np.array([ ... [1.0, 1.0], ... [2.0, 1.0], ... ])) >>> edge3 = bezier.Curve(np.array([ ... [2.0, 1.0 ], ... [1.0, 0.75], ... [0.0, 0.0 ], ... ])) >>> curved_poly = bezier.CurvedPolygon( ... edge0, edge1, edge2, edge3) >>> curved_poly <CurvedPolygon (num_sides=4)>
Parameters: edges ( Tuple
[Curve
, ... ]) – The boundary edges of the curved polygon.-
num_sides
¶ int: The number of sides in the current polygon.
-
plot
(pts_per_edge, color=None, ax=None, show=False)¶ Plot the current curved polygon.
Parameters: Returns: The axis containing the plot. This may be a newly created axis.
Return type:
-
bezier.surface module¶
Helper for Bézier Surfaces / Triangles.
-
class
bezier.surface.
Surface
(nodes, base_x=0.0, base_y=0.0, width=1.0, _copy=True)¶ Bases:
bezier._base.Base
Represents a Bézier surface.
We define a Bézier triangle as a mapping from the unit simplex in 2D (i.e. the unit triangle) onto a surface in an arbitrary dimension. We use barycentric coordinates
\[\lambda_1 = 1 - s - t, \lambda_2 = s, \lambda_3 = t\]for points in
\[\left\{(s, t) \mid 0 \leq s, t, s + t \leq 1\right\}.\]As with curves, using these weights we get convex combinations of points \(v_{i, j, k}\) in some vector space:
\[B\left(\lambda_1, \lambda_2, \lambda_3\right) = \sum_{i + j + k = d} \binom{d}{i \, j \, k} \lambda_1^i \lambda_2^j \lambda_3^k \cdot v_{i, j, k}\]Note
We assume the nodes are ordered from left-to-right and from bottom-to-top. So for example, the linear triangle:
(0,0,1) (1,0,0) (0,1,0)
is ordered as
\[\begin{split}\left[\begin{array}{c c c} v_{1,0,0} & v_{0,1,0} & v_{0,0,1} \end{array}\right]^T\end{split}\]the quadratic triangle:
(0,0,2) (1,0,1) (0,1,1) (2,0,0) (1,1,0) (0,2,0)
is ordered as
\[\begin{split}\left[\begin{array}{c c c c c c} v_{2,0,0} & v_{1,1,0} & v_{0,2,0} & v_{1,0,1} & v_{0,1,1} & v_{0,0,2} \end{array}\right]^T\end{split}\]the cubic triangle:
(0,0,3) (1,0,2) (0,1,2) (2,0,1) (1,1,1) (0,2,1) (3,0,0) (2,1,0) (1,2,0) (0,3,0)
is ordered as
\[\begin{split}\left[\begin{array}{c c c c c c c c c c} v_{3,0,0} & v_{2,1,0} & v_{1,2,0} & v_{0,3,0} & v_{2,0,1} & v_{1,1,1} & v_{0,2,1} & v_{1,0,2} & v_{0,1,2} & v_{0,0,3} \end{array}\right]^T\end{split}\]and so on.
>>> import bezier >>> nodes = np.array([ ... [0.0 , 0.0 ], ... [0.5 , 0.0 ], ... [1.0 , 0.25 ], ... [0.125, 0.5 ], ... [0.375, 0.375], ... [0.25 , 1.0 ], ... ]) >>> surface = bezier.Surface(nodes) >>> surface <Surface (degree=2, dimension=2)>
Parameters: - nodes (numpy.ndarray) – The nodes in the surface. The rows represent each node while the columns are the dimension of the ambient space.
- base_x (
Optional
[float
]) – The \(x\)-coordinate of the base vertex of the sub-triangle that this surface represents. - base_y (
Optional
[float
]) – The \(y\)-coordinate of the base vertex of the sub-triangle that this surface represents. - width (
Optional
[float
]) – The width of the sub-triangle that this surface represents. - _copy (bool) – Flag indicating if the nodes should be copied before
being stored. Defaults to
True
since callers may freely mutatenodes
after passing in.
-
area
¶ float: The area of the current surface.
Raises: NotImplementedError
– If the area isn’t already cached.
-
width
¶ float: The “width” of the parameterized triangle.
When re-parameterizing (e.g. via
subdivide()
) we specialize the surface from the unit triangle to some sub-triangle. After doing this, we re-parameterize so that that sub-triangle is treated like the unit triangle.To track which sub-triangle we are in during the subdivision process, we use the coordinates of the base vertex as well as the “width” of each leg.
>>> surface.base_x, surface.base_y (0.0, 0.0) >>> surface.width 1.0
Upon subdivision, the width halves (and potentially changes sign) and the vertex moves to one of four points:
>>> _, sub_surface_b, sub_surface_c, _ = surface.subdivide() >>> sub_surface_b.base_x, sub_surface_b.base_y (0.5, 0.5) >>> sub_surface_b.width -0.5 >>> sub_surface_c.base_x, sub_surface_c.base_y (0.5, 0.0) >>> sub_surface_c.width 0.5
-
edges
¶ tuple: The edges of the surface.
>>> nodes = np.array([ ... [0.0 , 0.0 ], ... [0.5 , -0.1875], ... [1.0 , 0.0 ], ... [0.1875, 0.5 ], ... [0.625 , 0.625 ], ... [0.0 , 1.0 ], ... ]) >>> surface = bezier.Surface(nodes) >>> edge1, _, _ = surface.edges >>> edge1 <Curve (degree=2, dimension=2)> >>> edge1.nodes array([[ 0. , 0. ], [ 0.5 , -0.1875], [ 1. , 0. ]])
Returns: The edges of the surface. Return type: Tuple
[Curve
,Curve
,Curve
]
-
evaluate_barycentric
(lambda1, lambda2, lambda3)¶ Compute a point on the surface.
Evaluates \(B\left(\lambda_1, \lambda_2, \lambda_3\right)\).
>>> nodes = np.array([ ... [0.0 , 0.0 ], ... [0.5 , 0.0 ], ... [1.0 , 0.25 ], ... [0.125, 0.5 ], ... [0.375, 0.375], ... [0.25 , 1.0 ], ... ]) >>> surface = bezier.Surface(nodes) >>> point = surface.evaluate_barycentric(0.125, 0.125, 0.75) >>> point array([ 0.265625 , 0.73046875])
However, this can’t be used for points outside the reference triangle:
>>> surface.evaluate_barycentric(-0.25, 0.75, 0.5) Traceback (most recent call last): ... ValueError: ('Parameters must be positive', -0.25, 0.75, 0.5)
or for non-Barycentric coordinates;
>>> surface.evaluate_barycentric(0.25, 0.25, 0.25) Traceback (most recent call last): ... ValueError: ('Values do not sum to 1', 0.25, 0.25, 0.25)
Parameters: Returns: The point on the surface (as a one dimensional NumPy array).
Return type: Raises: ValueError
– If the weights are not valid barycentric coordinates, i.e. they don’t sum to1
.ValueError
– If some weights are negative.
-
evaluate_cartesian
(s, t)¶ Compute a point on the surface.
Evaluates \(B\left(1 - s - t, s, t\right)\) by calling
evaluate_barycentric()
.Parameters: Returns: The point on the surface (as a one dimensional NumPy array).
Return type:
-
evaluate_multi
(param_vals)¶ Compute multiple points on the surface.
If
param_vals
has two columns, this method treats them as Cartesian:>>> nodes = np.array([ ... [ 0.0, 0.0], ... [ 2.0, 1.0], ... [-3.0, 2.0], ... ]) >>> surface = bezier.Surface(nodes) >>> surface <Surface (degree=1, dimension=2)> >>> param_vals = np.array([ ... [0.0 , 0.0 ], ... [0.125, 0.625], ... [0.5 , 0.5 ], ... ]) >>> points = surface.evaluate_multi(param_vals) >>> points array([[ 0. , 0. ], [-1.625, 1.375], [-0.5 , 1.5 ]])
and if
param_vals
has three columns, treats them as Barycentric:>>> nodes = np.array([ ... [ 0. , 0. ], ... [ 1. , 0.75], ... [ 2. , 1. ], ... [-1.5, 1. ], ... [-0.5, 1.5 ], ... [-3. , 2. ], ... ]) >>> surface = bezier.Surface(nodes) >>> surface <Surface (degree=2, dimension=2)> >>> param_vals = np.array([ ... [0. , 0.25, 0.75 ], ... [1. , 0. , 0. ], ... [0.25 , 0.5 , 0.25 ], ... [0.375, 0.25, 0.375], ... ]) >>> points = surface.evaluate_multi(param_vals) >>> points array([[-1.75 , 1.75 ], [ 0. , 0. ], [ 0.25 , 1.0625 ], [-0.625 , 1.046875]])
Note
This currently just uses
evaluate_cartesian()
andevaluate_barycentric()
so is less performant than it could be.Parameters: param_vals (numpy.ndarray) – Array of parameter values (as a 2D array).
Returns: The point on the surface.
Return type: Raises: ValueError
– Ifparam_vals
is not a 2D array.ValueError
– Ifparam_vals
doesn’t have 2 or 3 columns.
-
plot
(pts_per_edge, color=None, ax=None, with_nodes=False, show=False)¶ Plot the current surface.
Parameters: - pts_per_edge (int) – Number of points to plot per edge.
- color (
Optional
[Tuple
[float
,float
,float
] ]) – Color as RGB profile. - ax (
Optional
[matplotlib.artist.Artist
]) – matplotlib axis object to add plot to. - with_nodes (
Optional
[bool
]) – Determines if the control points should be added to the plot. Off by default. - show (
Optional
[bool
]) – Flag indicating if the plot should be shown.
Returns: The axis containing the plot. This may be a newly created axis.
Return type: Raises: NotImplementedError
– If the surface’s dimension is not2
.
-
subdivide
()¶ Split the surface into four sub-surfaces.
Does so by taking the unit triangle (i.e. the domain of the surface) and splitting it into four sub-triangles
Then the surface is re-parameterized via the map to/from the given sub-triangles and the unit triangle.
For example, when a degree two surface is subdivided:
>>> nodes = np.array([ ... [-1.0 , 0.0 ], ... [ 0.5 , 0.5 ], ... [ 2.0 , 0.0 ], ... [ 0.25, 1.75], ... [ 2.0 , 3.0 ], ... [ 0.0 , 4.0 ], ... ]) >>> surface = bezier.Surface(nodes) >>> _, sub_surface_b, _, _ = surface.subdivide() >>> sub_surface_b <Surface (degree=2, dimension=2, base=(0.5, 0.5), width=-0.5)> >>> sub_surface_b.nodes array([[ 1.5 , 2.5 ], [ 0.6875, 2.3125], [-0.125 , 1.875 ], [ 1.1875, 1.3125], [ 0.4375, 1.3125], [ 0.5 , 0.25 ]])
Returns: The lower left, central, lower right and upper left sub-surfaces (in that order). Return type: Tuple
[Surface
,Surface
,Surface
,Surface
]
-
is_valid
¶ bool: Flag indicating if the surface is “valid”.
Here, “valid” means there are no self-intersections or singularities.
This checks if the Jacobian of the map from the reference triangle is nonzero. For example, a linear “surface” with collinear points is invalid:
>>> nodes = np.array([ ... [0.0, 0.0], ... [1.0, 1.0], ... [2.0, 2.0], ... ]) >>> surface = bezier.Surface(nodes) >>> surface.is_valid False
while a quadratic surface with one straight side:
>>> nodes = np.array([ ... [ 0.0 , 0.0 ], ... [ 0.5 , 0.125], ... [ 1.0 , 0.0 ], ... [-0.125, 0.5 ], ... [ 0.5 , 0.5 ], ... [ 0.0 , 1.0 ], ... ]) >>> surface = bezier.Surface(nodes) >>> surface.is_valid True
though not all higher degree surfaces are valid:
>>> nodes = np.array([ ... [1.0, 0.0], ... [0.0, 0.0], ... [1.0, 1.0], ... [0.0, 0.0], ... [0.0, 0.0], ... [0.0, 1.0], ... ]) >>> surface = bezier.Surface(nodes) >>> surface.is_valid False
-
locate
(point)¶ Find a point on the current surface.
Solves for \(s\) and \(t\) in \(B(s, t) = p\).
Note
A unique solution is only guaranteed if the current surface is valid. This code assumes a valid surface, but doesn’t check.
>>> surface = bezier.Surface(np.array([ ... [0.0 , 0.0 ], ... [0.5 , -0.25], ... [1.0 , 0.0 ], ... [0.25, 0.5 ], ... [0.75, 0.75], ... [0.0 , 1.0 ], ... ])) >>> point = np.array([[0.59375, 0.25]]) >>> s, t = surface.locate(point) >>> s 0.5 >>> t 0.25
Parameters: point (numpy.ndarray) – A (
1xD
) point on the surface, where \(D\) is the dimension of the surface.Returns: The \(s\) and \(t\) values corresponding to
x_val
andy_val
orNone
if the point is not on thesurface
.Return type: Raises: NotImplementedError
– If the surface isn’t in \(\mathbf{R}^2\).ValueError
– If the dimension of thepoint
doesn’t match the dimension of the current surface.
-
intersect
(other)¶ Find the common intersection with another surface.
Parameters: other (Surface) – Other surface to intersect with.
Returns: List of intersection objects (possibly empty).
Return type: Raises: TypeError
– Ifother
is not a surface.NotImplementedError
– If both surfaces aren’t two-dimensional.
-
elevate
()¶ Return a degree-elevated version of the current surface.
Does this by converting the current nodes \(\left\{v_{i, j, k}\right\}_{i + j + k = d}\) to new nodes \(\left\{w_{i, j, k}\right\}_{i + j + k = d + 1}\). Does so by re-writing
\[E\left(\lambda_1, \lambda_2, \lambda_3\right) = \left(\lambda_1 + \lambda_2 + \lambda_3\right) B\left(\lambda_1, \lambda_2, \lambda_3\right) = \sum_{i + j + k = d + 1} \binom{d + 1}{i \, j \, k} \lambda_1^i \lambda_2^j \lambda_3^k \cdot w_{i, j, k}\]In this form, we must have
\[\begin{split}\begin{align*} \binom{d + 1}{i \, j \, k} \cdot w_{i, j, k} &= \binom{d}{i - 1 \, j \, k} \cdot v_{i - 1, j, k} + \binom{d}{i \, j - 1 \, k} \cdot v_{i, j - 1, k} + \binom{d}{i \, j \, k - 1} \cdot v_{i, j, k - 1} \\ \Longleftrightarrow (d + 1) \cdot w_{i, j, k} &= i \cdot v_{i - 1, j, k} + j \cdot v_{i, j - 1, k} + k \cdot v_{i, j, k - 1} \end{align*}\end{split}\]where we assume that, for example, \(v_{i, j, k - 1}\) is \(0\) (or any other unused value) if \(k = 0\).
>>> surface = bezier.Surface(np.array([ ... [0.0, 0.0], ... [1.0, 0.0], ... [0.0, 1.0], ... ])) >>> surface <Surface (degree=1, dimension=2)> >>> new_surface = surface.elevate() >>> new_surface <Surface (degree=2, dimension=2)> >>> new_surface.nodes array([[ 0. , 0. ], [ 0.5, 0. ], [ 1. , 0. ], [ 0. , 0.5], [ 0.5, 0.5], [ 0. , 1. ]])
Returns: The degree-elevated surface. Return type: Surface
-
__eq__
(other)¶ Check equality against another shape.
Returns: Boolean indicating if the shapes are the same. Return type: bool
-
__ne__
(other)¶ Check inequality against another shape.
Returns: Boolean indicating if the shapes are not the same. Return type: bool
-
copy
()¶ Make a copy of the current shape.
Returns: Instance of the current shape.
-
degree
¶ int: The degree of the current shape.
-
dimension
¶ int: The dimension that the shape lives in.
For example, if the shape lives in \(\mathbf{R}^3\), then the dimension is
3
.
-
nodes
¶ numpy.ndarray: The nodes that define the current shape.
Curve-Curve Intersection¶
The problem of intersecting two curves is a difficult one
in computational geometry. The Curve.intersect()
method uses a combination of curve subdivision, bounding
box intersection, and curve approximation (by lines) to
find intersections.
Curve-Line Intersection¶
>>> curve1 = bezier.Curve(np.array([
... [0.0, 0.0],
... [0.5, 1.0],
... [1.0, 0.0],
... ]))
>>> curve2 = bezier.Curve(np.array([
... [0.0, 0.375],
... [1.0, 0.375],
... ]))
>>> curve1.intersect(curve2)
array([[ 0.25 , 0.375],
[ 0.75 , 0.375]])

>>> curve1 = bezier.Curve(np.array([
... [0.0, 0.0],
... [0.5, 1.0],
... [1.0, 0.0],
... ]))
>>> curve2 = bezier.Curve(np.array([
... [0.5, 0.0 ],
... [0.5, 0.75],
... ]))
>>> curve1.intersect(curve2)
array([[ 0.5, 0.5]])

>>> curve1 = bezier.Curve(np.array([
... [0.0, 0.0],
... [4.5, 9.0],
... [9.0, 0.0],
... ]))
>>> curve2 = bezier.Curve(np.array([
... [0.0, 8.0],
... [6.0, 0.0],
... ]))
>>> curve1.intersect(curve2)
array([[ 3., 4.]])

>>> curve1 = bezier.Curve(np.array([
... [0.0, 0.375],
... [1.0, 0.375],
... ]))
>>> curve2 = bezier.Curve(np.array([
... [0.5, 0.0 ],
... [0.5, 0.75],
... ]))
>>> curve1.intersect(curve2)
array([[ 0.5 , 0.375]])

>>> curve1 = bezier.Curve(np.array([
... [-1.0, 1.0],
... [ 0.5, 0.5],
... [ 0.0, 2.0],
... ]))
>>> curve2 = bezier.Curve(np.array([
... [ 0.5 , 0.5 ],
... [-0.25, 1.25],
... ]))
>>> curve1.intersect(curve2)
array([[ 0., 1.]])

Curved Intersections¶
For curves which intersect at exact floating point numbers, we can typically compute the intersection with zero error:
>>> curve1 = bezier.Curve(np.array([
... [0.0, 0.0],
... [0.5, 1.0],
... [1.0, 0.0],
... ]))
>>> curve2 = bezier.Curve(np.array([
... [0.0, 0.75],
... [0.5, -0.25],
... [1.0, 0.75],
... ]))
>>> curve1.intersect(curve2)
array([[ 0.25 , 0.375],
[ 0.75 , 0.375]])

>>> curve1 = bezier.Curve(np.array([
... [0.0, 0.0],
... [1.5, 3.0],
... [3.0, 0.0],
... ]))
>>> curve2 = bezier.Curve(np.array([
... [ 3.0 , 1.5 ],
... [ 2.625, -0.90625],
... [-0.75 , 2.4375 ],
... ]))
>>> curve1.intersect(curve2)
array([[ 0.75 , 1.125 ],
[ 2.625 , 0.65625]])

>>> curve1 = bezier.Curve(np.array([
... [0.0 , 0.0 ],
... [0.375, 0.75 ],
... [0.75 , 0.375],
... ]))
>>> curve2 = bezier.Curve(np.array([
... [0.25 , 0.5625],
... [0.625, 0.1875],
... [1.0 , 0.9375],
... ]))
>>> curve1.intersect(curve2)
array([[ 0.375 , 0.46875],
[ 0.625 , 0.46875]])

Even for curves which don’t intersect at exact floating point numbers, we can compute the intersection to machine precision:
>>> curve1 = bezier.Curve(np.array([
... [0.0, 0.0],
... [0.5, 1.0],
... [1.0, 0.0],
... ]))
>>> curve2 = bezier.Curve(np.array([
... [1.125, 0.5],
... [0.625, -0.5],
... [0.125, 0.5],
... ]))
>>> intersections = curve1.intersect(curve2)
>>> sq31 = np.sqrt(31.0)
>>> expected = np.array([
... [36 - 4 * sq31, 16 + sq31],
... [36 + 4 * sq31, 16 - sq31],
... ]) / 64.0
>>> max_err = np.max(np.abs(intersections - expected))
>>> np.log2(max_err)
-54.0

>>> curve1 = bezier.Curve(np.array([
... [0.0, 0.0],
... [0.5, 1.0],
... [1.0, 0.0],
... ]))
>>> curve2 = bezier.Curve(np.array([
... [0.0, 0.265625],
... [0.5, 0.234375],
... [1.0, 0.265625],
... ]))
>>> intersections = curve1.intersect(curve2)
>>> sq33 = np.sqrt(33.0)
>>> expected = np.array([
... [33 - 4 * sq33, 17],
... [33 + 4 * sq33, 17],
... ]) / 66.0
>>> max_err = np.max(np.abs(intersections - expected))
>>> np.log2(max_err)
-54.0

>>> curve1 = bezier.Curve(np.array([
... [0.0, 0.0],
... [0.5, 1.0],
... [1.0, 0.0],
... ]))
>>> curve2 = bezier.Curve(np.array([
... [0.0 , 0.0],
... [0.25, 2.0],
... [0.5 , -2.0],
... [0.75, 2.0],
... [1.0 , 0.0],
... ]))
>>> intersections = curve1.intersect(curve2)
>>> sq7 = np.sqrt(7.0)
>>> expected = np.array([
... [7 - sq7, 6],
... [7 + sq7, 6],
... [ 0, 0],
... [ 14, 0],
... ]) / 14.0
>>> max_err = np.max(np.abs(intersections - expected))
>>> np.log2(max_err)
-54.0

>>> curve1 = bezier.Curve(np.array([
... [-0.125, -0.28125],
... [ 0.5 , 1.28125],
... [ 1.125, -0.28125],
... ]))
>>> curve2 = bezier.Curve(np.array([
... [ 1.5625, -0.0625],
... [-1.5625, 0.25 ],
... [ 1.5625, 0.5625],
... ]))
>>> intersections = curve1.intersect(curve2)
>>> sq5 = np.sqrt(5.0)
>>> expected = np.array([
... [6 - 2 * sq5, 5 - sq5],
... [ 4, 6 ],
... [ 16, 0 ],
... [6 + 2 * sq5, 5 + sq5],
... ]) / 16.0
>>> max_err = np.max(np.abs(intersections - expected))
>>> np.log2(max_err)
-50.415...

For higher degree intersections, the error starts to get a little larger.
>>> curve1 = bezier.Curve(np.array([
... [0.25 , 0.625],
... [0.625, 0.25 ],
... [1.0 , 1.0 ],
... ]))
>>> curve2 = bezier.Curve(np.array([
... [0.0 , 0.5],
... [0.25, 1.0],
... [0.75, 1.5],
... [1.0 , 0.5],
... ]))
>>> intersections = curve1.intersect(curve2)
>>> s_vals = np.roots([486, -3726, 13905, -18405, 6213, 1231])
>>> _, s_val, _ = np.sort(s_vals[s_vals.imag == 0].real)
>>> x_val = (3 * s_val + 1) / 4
>>> y_val = (9 * s_val * s_val - 6 * s_val + 5) / 8
>>> expected = np.array([
... [x_val, y_val],
... ])
>>> max_err = np.max(np.abs(intersections - expected))
>>> np.log2(max_err)
-49.678...

>>> curve1 = bezier.Curve(np.array([
... [0.0, 8.0],
... [6.0, 0.0],
... ]))
>>> curve2 = bezier.Curve(np.array([
... [0.375, 7.0],
... [2.125, 8.0],
... [3.875, 0.0],
... [5.625, 1.0],
... ]))
>>> intersections = curve1.intersect(curve2)
>>> sq7 = np.sqrt(7.0)
>>> expected = np.array([
... [ 72, 96 ],
... [72 - 21 * sq7, 96 + 28 * sq7],
... [72 + 21 * sq7, 96 - 28 * sq7],
... ]) / 24.0
>>> max_err = np.max(np.abs(intersections - expected))
>>> np.log2(max_err)
-50.0

>>> curve1 = bezier.Curve(np.array([
... [0.0, 0.375],
... [1.0, 0.375],
... ]))
>>> curve2 = bezier.Curve(np.array([
... [0.125, 0.25 ],
... [0.375, 0.75 ],
... [0.625, 0.0 ],
... [0.875, 0.1875],
... ]))
>>> intersections = curve1.intersect(curve2)
>>> s_val1, s_val2, _ = np.sort(np.roots(
... [17920, -29760, 13512, -1691]))
>>> expected = np.array([
... [s_val2, 0.375],
... [s_val1, 0.375],
... ])
>>> max_err = np.max(np.abs(intersections - expected))
>>> np.log2(max_err)
-50.678...

Intersections at Endpoints¶
>>> curve1 = bezier.Curve(np.array([
... [0.0, 0.0],
... [0.5, 1.0],
... [1.0, 0.0],
... ]))
>>> curve2 = bezier.Curve(np.array([
... [1.0, 0.0],
... [1.5, -1.0],
... [2.0, 0.0],
... ]))
>>> curve1.intersect(curve2)
array([[ 1., 0.]])

>>> curve1 = bezier.Curve(np.array([
... [0.0, 0.0],
... [0.5, 1.0],
... [1.0, 0.0],
... ]))
>>> curve2 = bezier.Curve(np.array([
... [2.0, 0.0],
... [1.5, 1.0],
... [1.0, 0.0],
... ]))
>>> curve1.intersect(curve2)
array([[ 1., 0.]])

>>> curve1 = bezier.Curve(np.array([
... [0.0, 0.0],
... [4.5, 9.0],
... [9.0, 0.0],
... ]))
>>> curve2 = bezier.Curve(np.array([
... [11.0, 8.0],
... [ 7.0, 10.0],
... [ 3.0, 4.0],
... ]))
>>> curve1.intersect(curve2)
array([[ 3., 4.]])

Detecting Self-Intersections¶
>>> curve1 = bezier.Curve(np.array([
... [ 0.0 , 2.0 ],
... [-1.0 , 0.0 ],
... [ 1.0 , 1.0 ],
... [-0.75, 1.625],
... ]))
>>> left, right = curve1.subdivide()
>>> left.intersect(right)
array([[-0.09375 , 0.828125],
[-0.25 , 1.375 ]])

Limitations¶
Intersections that occur at points of tangency are in general problematic. For example, consider
The first curve is the zero set of \(y - 2x(1 - x)\), so plugging in the second curve gives
This shows that a point of tangency is equivalent to a repeated root of a polynomial. For this example, the intersection process successfully terminates
>>> curve1 = bezier.Curve(np.array([
... [0.0, 0.0],
... [0.5, 1.0],
... [1.0, 0.0],
... ]))
>>> curve2 = bezier.Curve(np.array([
... [0.0, 1.0],
... [0.5, 0.0],
... [1.0, 1.0],
... ]))
>>> curve1.intersect(curve2)
array([[ 0.5, 0.5]])

However this library mostly avoids (for now) computing tangent intersections. For example, the curves

have a tangent intersection that this library fails to compute:
>>> curve1 = bezier.Curve(np.array([
... [0.0 , 0.0 ],
... [0.375, 0.75 ],
... [0.75 , 0.375],
... ]))
>>> curve2 = bezier.Curve(np.array([
... [0.25 , 0.625],
... [0.625, 0.25 ],
... [1.0 , 1.0 ],
... ]))
>>> curve1.intersect(curve2)
Traceback (most recent call last):
...
NotImplementedError: Line segments parallel.
This failure comes from the fact that the linear approximations of the curves near the point of intersection are parallel.
As above, we can find some cases where tangent intersections are resolved:
>>> curve1 = bezier.Curve(np.array([
... [0.0, 0.0],
... [4.5, 9.0],
... [9.0, 0.0],
... ]))
>>> curve2 = bezier.Curve(np.array([
... [3.0, 4.5],
... [8.0, 4.5],
... ]))
>>> curve1.intersect(curve2)
array([[ 4.5, 4.5]])

but even by rotating an intersection (from above) that we know works

we still see a failure
>>> curve1 = bezier.Curve(np.array([
... [ 0.0, 0.0],
... [-0.5, 1.5],
... [ 1.0, 1.0],
... ]))
>>> curve2 = bezier.Curve(np.array([
... [-1.0, 1.0],
... [ 0.5, 0.5],
... [ 0.0, 2.0],
... ]))
>>> curve1.intersect(curve2)
Traceback (most recent call last):
...
NotImplementedError: The number of candidate intersections is too high.
In addition to points of tangency, coincident curve segments are (for now) not supported. For the curves

the library fails as well
>>> curve1 = bezier.Curve(np.array([
... [0.0, 0.0],
... [0.5, 1.0],
... [1.0, 0.0],
... ]))
>>> curve2 = bezier.Curve(np.array([
... [0.25, 0.375],
... [0.75, 0.875],
... [1.25, -0.625],
... ]))
>>> curve1.intersect(curve2)
Traceback (most recent call last):
...
NotImplementedError: The number of candidate intersections is too high.
Algorithm Helpers¶
In an attempt to thoroughly vet each algorithm used in this library, each computation is split into small units that can be tested independently.
Though many of these computational units aren’t provided as part of the public interface of this library, they are still interesting. (Possibly) more importantly, it’s useful to see these algorithms at work.
In this document, these helper functions and objects are documented. This is to help with the exposition of the computation and does not imply that these are part of the stable public interface.
-
bezier._intersection_helpers.
linearization_error
(curve)¶ Compute the maximum error of a linear approximation.
Helper for
Linearization
, which is used during the curve-curve intersection process.We use the line
\[L(s) = v_0 (1 - s) + v_n s\]and compute a bound on the maximum error
\[\max_{s \in \left[0, 1\right]} \|B(s) - L(s)\|_2.\]Rather than computing the actual maximum (a tight bound), we use an upper bound via the remainder from Lagrange interpolation in each component. This leaves us with \(\frac{s(s - 1)}{2!}\) times the second derivative in each component.
The second derivative curve is degree \(d = n - 2\) and is given by
\[B''(s) = n(n - 1) \sum_{j = 0}^{d} \binom{d}{j} s^j (1 - s)^{d - j} \cdot \Delta^2 v_j\]Due to this form (and the convex combination property of Bézier Curves) we know each component of the second derivative will be bounded by the maximum of that component among the \(\Delta^2 v_j\).
For example, the curve
\[\begin{split}B(s) = \left[\begin{array}{c} 0 \\ 0 \end{array}\right] (1 - s)^2 + \left[\begin{array}{c} 3 \\ 1 \end{array}\right] 2s(1 - s) + \left[\begin{array}{c} 9 \\ -2 \end{array}\right] s^2\end{split}\]has \(B''(s) \equiv \left[\begin{array}{c} 6 \\ -8 \end{array}\right]\) which has norm \(10\) everywhere, hence the maximum error is
\[\left.\frac{s(1 - s)}{2!} \cdot 10\right|_{s = \frac{1}{2}} = \frac{5}{4}.\]>>> nodes = np.array([ ... [0.0, 0.0], ... [3.0, 1.0], ... [9.0, -2.0], ... ]) >>> curve = bezier.Curve(nodes) >>> linearization_error(curve) 1.25
As a non-example, consider a “pathological” set of control points:
\[\begin{split}B(s) = \left[\begin{array}{c} 0 \\ 0 \end{array}\right] (1 - s)^3 + \left[\begin{array}{c} 5 \\ 12 \end{array}\right] 3s(1 - s)^2 + \left[\begin{array}{c} 10 \\ 24 \end{array}\right] 3s^2(1 - s) + \left[\begin{array}{c} 30 \\ 72 \end{array}\right] s^3\end{split}\]By construction, this lies on the line \(y = \frac{12x}{5}\), but the parametrization is cubic: \(12 \cdot x(s) = 5 \cdot y(s) = 180s(s^2 + 1)\). Hence, the fact that the curve is a line is not accounted for and we take the worse case among the nodes in:
\[\begin{split}B''(s) = 3 \cdot 2 \cdot \left( \left[\begin{array}{c} 0 \\ 0 \end{array}\right] (1 - s) + \left[\begin{array}{c} 15 \\ 36 \end{array}\right] s\right)\end{split}\]which gives a nonzero maximum error:
>>> nodes = np.array([ ... [ 0.0, 0.0], ... [ 5.0, 12.0], ... [10.0, 24.0], ... [30.0, 72.0], ... ]) >>> curve = bezier.Curve(nodes) >>> linearization_error(curve) 29.25
Though it may seem that
0
is a more appropriate answer, consider the goal of this function. We seek to linearize curves and then intersect the linear approximations. Then the \(s\)-values from the line-line intersection is lifted back to the curves. Thus the error \(\|B(s) - L(s)\|_2\) is more relevant than the underyling algebraic curve containing \(B(s)\).Note
It may be more appropriate to use a relative linearization error rather than the absolute error provided here. It’s unclear if the domain \(\left[0, 1\right]\) means the error is already adequately scaled or if the error should be scaled by the arc length of the curve or the (easier-to-compute) length of the line.
Parameters: curve (Curve) – A curve to be approximated by a line. Returns: The maximum error between the curve and the linear approximation. Return type: float
-
bezier._intersection_helpers.
newton_refine
(s, curve1, t, curve2)¶ Apply one step of 2D Newton’s method.
We want to use Newton’s method on the function
\[F(s, t) = B_1(s) - B_2(t)\]to refine \(\left(s_{\ast}, t_{\ast}\right)\). Using this, and the Jacobian \(DF\), we “solve”
\[\begin{split}\left[\begin{array}{c} 0 \\ 0 \end{array}\right] \approx F\left(s_{\ast} + \Delta s, t_{\ast} + \Delta t\right) \approx F\left(s_{\ast}, t_{\ast}\right) + \left[\begin{array}{c c} B_1'\left(s_{\ast}\right) & - B_2'\left(t_{\ast}\right) \end{array}\right] \left[\begin{array}{c} \Delta s \\ \Delta t \end{array}\right]\end{split}\]and refine with the component updates \(\Delta s\) and \(\Delta t\).
Note
This implementation assumes
curve1
andcurve2
live in \(\mathbf{R}^2\).For example, the curves
\[\begin{split}\begin{align*} B_1(s) &= \left[\begin{array}{c} 0 \\ 0 \end{array}\right] (1 - s)^2 + \left[\begin{array}{c} 2 \\ 4 \end{array}\right] 2s(1 - s) + \left[\begin{array}{c} 4 \\ 0 \end{array}\right] s^2 \\ B_2(t) &= \left[\begin{array}{c} 2 \\ 0 \end{array}\right] (1 - t) + \left[\begin{array}{c} 0 \\ 3 \end{array}\right] t \end{align*}\end{split}\]intersect at the point \(B_1\left(\frac{1}{4}\right) = B_2\left(\frac{1}{2}\right) = \frac{1}{2} \left[\begin{array}{c} 2 \\ 3 \end{array}\right]\).
However, starting from the wrong point we have
\[\begin{split}\begin{align*} F\left(\frac{3}{8}, \frac{1}{4}\right) &= \frac{1}{8} \left[\begin{array}{c} 0 \\ 9 \end{array}\right] \\ DF\left(\frac{3}{8}, \frac{1}{4}\right) &= \left[\begin{array}{c c} 4 & 2 \\ 2 & -3 \end{array}\right] \\ \Longrightarrow \left[\begin{array}{c} \Delta s \\ \Delta t \end{array}\right] &= \frac{9}{64} \left[\begin{array}{c} -1 \\ 2 \end{array}\right]. \end{align*}\end{split}\]>>> nodes1 = np.array([ ... [0.0, 0.0], ... [2.0, 4.0], ... [4.0, 0.0], ... ]) >>> curve1 = bezier.Curve(nodes1) >>> nodes2 = np.array([ ... [2.0, 0.0], ... [0.0, 3.0], ... ]) >>> curve2 = bezier.Curve(nodes2) >>> s, t = 0.375, 0.25 >>> new_s, new_t = newton_refine(s, curve1, t, curve2) >>> 64.0 * (new_s - s) -9.0 >>> 64.0 * (new_t - t) 18.0
For “typical” curves, we converge to a solution quadratically. This means that the number of correct digits doubles every iteration (until machine precision is reached).
>>> curve1 = bezier.Curve(np.array([ ... [0.0, 0.0], ... [0.25, 2.0], ... [0.5, -2.0], ... [0.75, 2.0], ... [1.0, 0.0], ... ])) >>> curve2 = bezier.Curve(np.array([ ... [0.0, 1.0], ... [0.25, 0.5], ... [0.5, 0.5], ... [0.75, 0.5], ... [1.0, 0.0], ... ])) >>> # The expected intersection is the only real root of >>> # 28 s^3 - 30 s^2 + 9 s - 1. >>> omega = (28.0 * np.sqrt(17.0) + 132.0)**(1.0 / 3.0) / 28.0 >>> expected = 5.0 / 14.0 + omega + 1 / (49.0 * omega) >>> s_vals = [0.625, None, None, None, None] >>> t = 0.625 >>> np.log2(abs(expected - s_vals[0])) -4.399... >>> s_vals[1], t = newton_refine(s_vals[0], curve1, t, curve2) >>> np.log2(abs(expected - s_vals[1])) -7.901... >>> s_vals[2], t = newton_refine(s_vals[1], curve1, t, curve2) >>> np.log2(abs(expected - s_vals[2])) -16.010... >>> s_vals[3], t = newton_refine(s_vals[2], curve1, t, curve2) >>> np.log2(abs(expected - s_vals[3])) -32.110... >>> s_vals[4], t = newton_refine(s_vals[3], curve1, t, curve2) >>> s_vals[4] == expected True
However, when the intersection occurs at a point of tangency, the convergence becomes linear. This means that the number of correct digits added each iteration is roughly constant.
>>> curve1 = bezier.Curve(np.array([ ... [0.0, 0.0], ... [0.5, 1.0], ... [1.0, 0.0], ... ])) >>> curve2 = bezier.Curve(np.array([ ... [0.0, 0.5], ... [1.0, 0.5], ... ])) >>> expected = 0.5 >>> s_vals = [0.375, None, None, None, None, None] >>> t = 0.375 >>> np.log2(abs(expected - s_vals[0])) -3.0 >>> s_vals[1], t = newton_refine(s_vals[0], curve1, t, curve2) >>> np.log2(abs(expected - s_vals[1])) -4.0 >>> s_vals[2], t = newton_refine(s_vals[1], curve1, t, curve2) >>> np.log2(abs(expected - s_vals[2])) -5.0 >>> s_vals[3], t = newton_refine(s_vals[2], curve1, t, curve2) >>> np.log2(abs(expected - s_vals[3])) -6.0 >>> s_vals[4], t = newton_refine(s_vals[3], curve1, t, curve2) >>> np.log2(abs(expected - s_vals[4])) -7.0 >>> s_vals[5], t = newton_refine(s_vals[4], curve1, t, curve2) >>> np.log2(abs(expected - s_vals[5])) -8.0
Unfortunately, the process terminates with an error that is not close to machine precision \(\varepsilon\) when \(\Delta s = \Delta t = 0\).
>>> s1 = t1 = 0.5 - 0.5**27 >>> np.log2(0.5 - s1) -27.0 >>> s2, t2 = newton_refine(s1, curve1, t1, curve2) >>> s2 == t2 True >>> np.log2(0.5 - s2) -28.0 >>> s3, t3 = newton_refine(s2, curve1, t2, curve2) >>> s3 == t3 == s2 True
Due to round-off near the point of tangency, the final error resembles \(\sqrt{\varepsilon}\) rather than machine precision as expected.
Parameters: Returns: The refined parameters from a single Newton step.
Return type:
-
bezier._intersection_helpers.
segment_intersection
(start0, end0, start1, end1)¶ Determine the intersection of two line segments.
Assumes each line is parametric
\[\begin{split}\begin{alignat*}{2} L_0(s) &= S_0 (1 - s) + E_0 s &&= S_0 + s \Delta_0 \\ L_1(t) &= S_1 (1 - t) + E_1 t &&= S_1 + t \Delta_1. \end{alignat*}\end{split}\]To solve \(S_0 + s \Delta_0 = S_1 + t \Delta_1\), we use the cross product:
\[\left(S_0 + s \Delta_0\right) \times \Delta_1 = \left(S_1 + t \Delta_1\right) \times \Delta_1 \Longrightarrow s \left(\Delta_0 \times \Delta_1\right) = \left(S_1 - S_0\right) \times \Delta_1.\]Similarly
\[\Delta_0 \times \left(S_0 + s \Delta_0\right) = \Delta_0 \times \left(S_1 + t \Delta_1\right) \Longrightarrow \left(S_1 - S_0\right) \times \Delta_0 = \Delta_0 \times \left(S_0 - S_1\right) = t \left(\Delta_0 \times \Delta_1\right).\]Note
Since our points are in \(\mathbf{R}^2\), the “traditional” cross-product in \(\mathbf{R}^3\) will always point in the \(z\) direction, so in the above we mean the \(z\) component of the cross product, rather than the entire vector.
For example, the diagonal lines
\[\begin{split}\begin{align*} L_0(s) &= \left[\begin{array}{c} 0 \\ 0 \end{array}\right] (1 - s) + \left[\begin{array}{c} 2 \\ 2 \end{array}\right] s \\ L_1(t) &= \left[\begin{array}{c} -1 \\ 2 \end{array}\right] (1 - t) + \left[\begin{array}{c} 1 \\ 0 \end{array}\right] t \end{align*}\end{split}\]intersect at \(L_0\left(\frac{1}{4}\right) = L_1\left(\frac{3}{4}\right) = \frac{1}{2} \left[\begin{array}{c} 1 \\ 1 \end{array}\right]\).
>>> start0 = np.array([[0.0, 0.0]]) >>> end0 = np.array([[2.0, 2.0]]) >>> start1 = np.array([[-1.0, 2.0]]) >>> end1 = np.array([[1.0, 0.0]]) >>> s, t, _ = segment_intersection(start0, end0, start1, end1) >>> s 0.25 >>> t 0.75
Taking the parallel (but different) lines
\[\begin{split}\begin{align*} L_0(s) &= \left[\begin{array}{c} 1 \\ 0 \end{array}\right] (1 - s) + \left[\begin{array}{c} 0 \\ 1 \end{array}\right] s \\ L_1(t) &= \left[\begin{array}{c} -1 \\ 3 \end{array}\right] (1 - t) + \left[\begin{array}{c} 3 \\ -1 \end{array}\right] t \end{align*}\end{split}\]we should be able to determine that the lines don’t intersect, but this function is not meant for that check:
>>> start0 = np.array([[1.0, 0.0]]) >>> end0 = np.array([[0.0, 1.0]]) >>> start1 = np.array([[-1.0, 3.0]]) >>> end1 = np.array([[3.0, -1.0]]) >>> _, _, success = segment_intersection(start0, end0, start1, end1) >>> success False
Instead, we use
parallel_different()
:>>> parallel_different(start0, end0, start1, end1) True
Parameters: - start0 (numpy.ndarray) – A 1x2 NumPy array that is the start vector \(S_0\) of the parametric line \(L_0(s)\).
- end0 (numpy.ndarray) – A 1x2 NumPy array that is the end vector \(E_0\) of the parametric line \(L_0(s)\).
- start1 (numpy.ndarray) – A 1x2 NumPy array that is the start vector \(S_1\) of the parametric line \(L_1(s)\).
- end1 (numpy.ndarray) – A 1x2 NumPy array that is the end vector \(E_1\) of the parametric line \(L_1(s)\).
Returns: Pair of \(s_{\ast}\) and \(t_{\ast}\) such that the lines intersect: \(L_0\left(s_{\ast}\right) = L_1\left(t_{\ast}\right)\) and then a boolean indicating if an intersection was found.
Return type:
-
bezier._intersection_helpers.
parallel_different
(start0, end0, start1, end1)¶ Checks if two parallel lines ever meet.
Meant as a back-up when
segment_intersection()
fails.Note
This function assumes but never verifies that the lines are parallel.
In the case that the segments are parallel and lie on the exact same line, finding a unique intersection is not possible. However, if they are parallel but on different lines, then there is a guarantee of no intersection.
In
segment_intersection()
, we utilized the normal form of the lines (via the cross product):\[\begin{split}\begin{align*} L_0(s) \times \Delta_0 &\equiv S_0 \times \Delta_0 \\ L_1(t) \times \Delta_1 &\equiv S_1 \times \Delta_1 \end{align*}\end{split}\]So, we can detect if \(S_1\) is on the first line by checking if
\[S_0 \times \Delta_0 \stackrel{?}{=} S_1 \times \Delta_0.\]If it is not on the first line, then we are done, the segments don’t meet:
>>> # Line: y = 1 >>> start0 = np.array([[0.0, 1.0]]) >>> end0 = np.array([[1.0, 1.0]]) >>> # Vertical shift up: y = 2 >>> start1 = np.array([[-1.0, 2.0]]) >>> end1 = np.array([[3.0, 2.0]]) >>> parallel_different(start0, end0, start1, end1) True
If \(S_1\) is on the first line, we want to check that \(S_1\) and \(E_1\) define parameters outside of \(\left[0, 1\right]\). To compute these parameters:
\[L_1(t) = S_0 + s_{\ast} \Delta_0 \Longrightarrow s_{\ast} = \frac{\Delta_0^T \left( L_1(t) - S_0\right)}{\Delta_0^T \Delta_0}.\]For example, the intervals \(\left[0, 1\right]\) and \(\left[\frac{3}{2}, 2\right]\) (via \(S_1 = S_0 + \frac{3}{2} \Delta_0\) and \(E_1 = S_0 + 2 \Delta_0\)) correspond to segments that don’t meet:
>>> start0 = np.array([[1.0, 0.0]]) >>> delta0 = np.array([[2.0, -1.0]]) >>> end0 = start0 + 1.0 * delta0 >>> start1 = start0 + 1.5 * delta0 >>> end1 = start0 + 2.0 * delta0 >>> parallel_different(start0, end0, start1, end1) True
but if the intervals overlap, like \(\left[0, 1\right]\) and \(\left[-1, \frac{1}{2}\right]\), the segments meet:
>>> start1 = start0 - 1.0 * delta0 >>> end1 = start0 + 0.5 * delta0 >>> parallel_different(start0, end0, start1, end1) False
Similarly, if the second interval completely contains the first, the segments meet:
>>> start1 = start0 + 3.0 * delta0 >>> end1 = start0 - 2.0 * delta0 >>> parallel_different(start0, end0, start1, end1) False
Note
This function doesn’t currently allow wiggle room around the desired value, i.e. the two values must be bitwise identical. However, the most “correct” version of this function likely should allow for some round off.
Parameters: - start0 (numpy.ndarray) – A 1x2 NumPy array that is the start vector \(S_0\) of the parametric line \(L_0(s)\).
- end0 (numpy.ndarray) – A 1x2 NumPy array that is the end vector \(E_0\) of the parametric line \(L_0(s)\).
- start1 (numpy.ndarray) – A 1x2 NumPy array that is the start vector \(S_1\) of the parametric line \(L_1(s)\).
- end1 (numpy.ndarray) – A 1x2 NumPy array that is the end vector \(E_1\) of the parametric line \(L_1(s)\).
Returns: Indicating if the lines are different.
Return type:
-
bezier._curve_helpers.
get_curvature
(nodes, degree, tangent_vec, s)¶ Compute the signed curvature of a curve at \(s\).
Computed via
\[\frac{B'(s) \times B''(s)}{\left\lVert B'(s) \right\rVert_2^3}\]>>> nodes = np.array([ ... [1.0 , 0.0], ... [0.75, 2.0], ... [0.5 , -2.0], ... [0.25, 2.0], ... [0.0 , 0.0], ... ]) >>> s = 0.5 >>> tangent_vec = hodograph(nodes, s) >>> tangent_vec array([-1., 0.]) >>> curvature = get_curvature(nodes, 4, tangent_vec, s) >>> curvature -12.0
Parameters: - nodes (numpy.ndarray) – The nodes of a curve.
- degree (int) – The degree of the curve.
- tangent_vec (numpy.ndarray) – The already computed value of \(B'(s)\)
- s (float) – The parameter value along the curve.
Returns: The signed curvature.
Return type:
-
bezier._curve_helpers.
newton_refine
(curve, point, s)¶ Refine a solution to \(B(s) = p\) using Newton’s method.
Computes updates via
\[\mathbf{0} \approx \left(B\left(s_{\ast}\right) - p\right) + B'\left(s_{\ast}\right) \Delta s\]For example, consider the curve
\[\begin{split}B(s) = \left[\begin{array}{c} 0 \\ 0 \end{array}\right] (1 - s)^2 + \left[\begin{array}{c} 1 \\ 2 \end{array}\right] 2 (1 - s) s + \left[\begin{array}{c} 3 \\ 1 \end{array}\right] s^2\end{split}\]and the point \(B\left(\frac{1}{4}\right) = \frac{1}{16} \left[\begin{array}{c} 9 \\ 13 \end{array}\right]\).
Starting from the wrong point \(s = \frac{3}{4}\), we have
\[\begin{split}\begin{align*} p - B\left(\frac{1}{2}\right) &= -\frac{1}{2} \left[\begin{array}{c} 3 \\ 1 \end{array}\right] \\ B'\left(\frac{1}{2}\right) &= \frac{1}{2} \left[\begin{array}{c} 7 \\ -1 \end{array}\right] \\ \Longrightarrow \frac{1}{4} \left[\begin{array}{c c} 7 & -1 \end{array}\right] \left[\begin{array}{c} 7 \\ -1 \end{array}\right] \Delta s &= -\frac{1}{4} \left[\begin{array}{c c} 7 & -1 \end{array}\right] \left[\begin{array}{c} 3 \\ 1 \end{array}\right] \\ \Longrightarrow \Delta s &= -\frac{2}{5} \end{align*}\end{split}\]>>> curve = bezier.Curve(np.array([ ... [0.0, 0.0], ... [1.0, 2.0], ... [3.0, 1.0], ... ])) >>> point = curve.evaluate_multi(np.array([0.25])) >>> point array([[ 0.5625, 0.8125]]) >>> s = 0.75 >>> new_s = newton_refine(curve, point, s) >>> 5 * (new_s - s) -2.0
On curves that are not “valid” (i.e. \(B(s)\) is not injective with non-zero gradient), Newton’s method may break down and converge linearly:
>>> curve = bezier.Curve(np.array([ ... [ 6.0, -3.0], ... [-2.0, 3.0], ... [-2.0, -3.0], ... [ 6.0, 3.0], ... ])) >>> expected = 0.5 >>> point = curve.evaluate_multi(np.array([expected])) >>> point array([[ 0., 0.]]) >>> s_vals = [0.625, None, None, None, None, None] >>> np.log2(abs(expected - s_vals[0])) -3.0 >>> s_vals[1] = newton_refine(curve, point, s_vals[0]) >>> np.log2(abs(expected - s_vals[1])) -3.983... >>> s_vals[2] = newton_refine(curve, point, s_vals[1]) >>> np.log2(abs(expected - s_vals[2])) -4.979... >>> s_vals[3] = newton_refine(curve, point, s_vals[2]) >>> np.log2(abs(expected - s_vals[3])) -5.978... >>> s_vals[4] = newton_refine(curve, point, s_vals[3]) >>> np.log2(abs(expected - s_vals[4])) -6.978... >>> s_vals[5] = newton_refine(curve, point, s_vals[4]) >>> np.log2(abs(expected - s_vals[5])) -7.978...
Due to round-off, the Newton process terminates with an error that is not close to machine precision \(\varepsilon\) when \(\Delta s = 0\).
>>> s_vals = [0.625] >>> new_s = newton_refine(curve, point, s_vals[-1]) >>> while new_s not in s_vals: ... s_vals.append(new_s) ... new_s = newton_refine(curve, point, s_vals[-1]) ... >>> len(s_vals) 27 >>> terminal_s = s_vals[-1] >>> terminal_s == newton_refine(curve, point, terminal_s) True >>> np.log2(terminal_s - 0.5) -29.580...
Due to round-off near the cusp, the final error resembles \(\sqrt{\varepsilon}\) rather than machine precision as expected.
Parameters: - curve (Curve) – The curve to refine a point on.
- point (numpy.ndarray) – A point on
curve
. - s (float) – An “almost” solution to \(B(s) = p\).
Returns: The updated value \(s + \Delta s\).
Return type:
-
class
bezier._intersection_helpers.
Intersection
(left, s, right, t, point=None)¶ Representation of a curve-curve intersection.
Parameters: - left (Curve) – The “left” curve in the intersection.
- s (float) – The parameter along
left
where the intersection occurs. - right (Curve) – The “right” curve in the intersection.
- t (float) – The parameter along
right
where the intersection occurs. - point (
Optional
[numpy.ndarray
]) – The point where the two curves actually intersect. If not provided, will be computed on the fly when first accessed.
-
left
¶ Curve: The “left” curve in the intersection.
-
right
¶ Curve: The “right” curve in the intersection.
-
point
¶ numpy.ndarray: The point where the intersection occurs.
-
interior_curve
¶ int: Which of the curves is on the interior.
Will be
0
for theleft
,1
for theright
and-1
if it is ambiguous (e.g. the curves are tangent but moving in opposite directions).Raises: AttributeError
– If the value has not already been set.
-
class
bezier._intersection_helpers.
Linearization
(curve, error=None)¶ A linearization of a curve.
This class is provided as a stand-in for a curve, so it provides a similar interface.
Parameters: - curve (Curve) – A curve that is linearized.
- error (
Optional
[float
]) – The linearization error. If not provided, this value is computed on the fly vialinearization_error()
.
-
subdivide
()¶ Do-nothing method to match the
Curve
interface.Returns: List of all subdivided parts, which is just the current object. Return type: Tuple
[Linearization
]
-
curve
¶ Curve: The curve that this linearization approximates.
-
error
¶ float: The linearization error for the linearized curve.
-
start_node
¶ numpy.ndarray: The start vector of this linearization.
-
end_node
¶ numpy.ndarray: The end vector of this linearization.
-
classmethod
from_shape
(shape)¶ Try to linearize a curve (or an already linearized curve).
Parameters: shape ( Union
[Curve
,Linearization
]) – A curve or an already linearized curve.Returns: The (potentially linearized) curve. Return type: Union
[Curve
,Linearization
]
-
bezier._surface_helpers.
newton_refine
(surface, x_val, y_val, s, t)¶ Refine a solution to \(B(s, t) = p\) using Newton’s method.
Computes updates via
\[\begin{split}\left[\begin{array}{c} 0 \\ 0 \end{array}\right] \approx \left(B\left(s_{\ast}, t_{\ast}\right) - \left[\begin{array}{c} x \\ y \end{array}\right]\right) + \left[\begin{array}{c c} B_s\left(s_{\ast}, t_{\ast}\right) & B_t\left(s_{\ast}, t_{\ast}\right) \end{array}\right] \left[\begin{array}{c} \Delta s \\ \Delta t \end{array}\right]\end{split}\]For example, (with weights \(\lambda_1 = 1 - s - t, \lambda_2 = s, \lambda_3 = t\)) consider the surface
\[\begin{split}B(s, t) = \left[\begin{array}{c} 0 \\ 0 \end{array}\right] \lambda_1^2 + \left[\begin{array}{c} 1 \\ 0 \end{array}\right] 2 \lambda_1 \lambda_2 + \left[\begin{array}{c} 2 \\ 0 \end{array}\right] \lambda_2^2 + \left[\begin{array}{c} 2 \\ 1 \end{array}\right] 2 \lambda_1 \lambda_3 + \left[\begin{array}{c} 2 \\ 2 \end{array}\right] 2 \lambda_2 \lambda_1 + \left[\begin{array}{c} 0 \\ 2 \end{array}\right] \lambda_3^2\end{split}\]and the point \(B\left(\frac{1}{4}, \frac{1}{2}\right) = \frac{1}{4} \left[\begin{array}{c} 5 \\ 5 \end{array}\right]\).
Starting from the wrong point \(s = \frac{1}{2}, t = \frac{1}{4}\), we have
\[\begin{split}\begin{align*} \left[\begin{array}{c} x \\ y \end{array}\right] - B\left(\frac{1}{2}, \frac{1}{4}\right) &= \frac{1}{4} \left[\begin{array}{c} -1 \\ 2 \end{array}\right] \\ DB\left(\frac{1}{2}, \frac{1}{4}\right) &= \frac{1}{2} \left[\begin{array}{c c} 3 & 2 \\ 1 & 6 \end{array}\right] \\ \Longrightarrow \left[\begin{array}{c} \Delta s \\ \Delta t \end{array}\right] &= \frac{1}{32} \left[\begin{array}{c} -10 \\ 7 \end{array}\right] \end{align*}\end{split}\]>>> surface = bezier.Surface(np.array([ ... [0.0, 0.0], ... [1.0, 0.0], ... [2.0, 0.0], ... [2.0, 1.0], ... [2.0, 2.0], ... [0.0, 2.0], ... ])) >>> surface.is_valid True >>> x_val, y_val = surface.evaluate_cartesian(0.25, 0.5) >>> x_val, y_val (1.25, 1.25) >>> s, t = 0.5, 0.25 >>> new_s, new_t = newton_refine(surface, x_val, y_val, s, t) >>> 32 * (new_s - s) -10.0 >>> 32 * (new_t - t) 7.0
Parameters: Returns: The refined \(s\) and \(t\) values.
Return type:
-
bezier._surface_helpers.
classify_intersection
(intersection)¶ Determine which curve is on the “inside of the intersection”.
This is intended to be a helper for forming a
CurvedPolygon
from the edge intersections of twoSurface
-s. In order to move from one intersection to another (or to the end of an edge), the interior edge must be determined at the point of intersection.The “typical” case is on the interior of both edges:
>>> curve1 = bezier.Curve(np.array([ ... [1.0 , 0.0 ], ... [1.75, 0.25], ... [2.0 , 1.0 ], ... ])) >>> curve2 = bezier.Curve(np.array([ ... [0.0 , 0.0 ], ... [1.6875, 0.0625], ... [2.0 , 0.5 ], ... ])) >>> s, t = 0.25, 0.5 >>> curve1.evaluate(s) == curve2.evaluate(t) array([ True, True], dtype=bool) >>> tangent1 = hodograph(curve1, s) >>> tangent1 array([ 1.25, 0.75]) >>> tangent2 = hodograph(curve2, t) >>> tangent2 array([ 2. , 0.5]) >>> intersection = Intersection(curve1, s, curve2, t) >>> classify_intersection(intersection) <IntersectionClassification.first: 'first'>
We determine the interior / left one by using the right-hand rule: by embedding the tangent vectors in \(\mathbf{R}^3\), we compute
\[\begin{split}\left[\begin{array}{c} x_1'(s) \\ y_1'(s) \\ 0 \end{array}\right] \times \left[\begin{array}{c} x_2'(t) \\ y_2'(t) \\ 0 \end{array}\right] = \left[\begin{array}{c} 0 \\ 0 \\ x_1'(s) y_2'(t) - x_2'(t) y_1'(s) \end{array}\right].\end{split}\]If the cross-product quantity \(B_1'(s) \times B_2'(t) = x_1'(s) y_2'(t) - x_2'(t) y_1'(s)\) is positive, then the first curve is “to the right”, i.e. the second curve is interior. If the cross-product is negative, the first curve is interior.
When \(B_1'(s) \times B_2'(t) = 0\), the tangent vectors are parallel, i.e. the intersection is a point of tangency:
>>> curve1 = bezier.Curve(np.array([ ... [1.0, 0.0], ... [1.5, 1.0], ... [2.0, 0.0], ... ])) >>> curve2 = bezier.Curve(np.array([ ... [0.0, 0.0], ... [1.5, 1.0], ... [3.0, 0.0], ... ])) >>> s, t = 0.5, 0.5 >>> curve1.evaluate(s) == curve2.evaluate(t) array([ True, True], dtype=bool) >>> intersection = Intersection(curve1, s, curve2, t) >>> classify_intersection(intersection) <IntersectionClassification.tangent_second: 'tangent_second'>
Depending on the direction of the parameterizations, the interior curve may change, but we can use the (signed) curvature of each curve at that point to determine which is on the interior:
>>> curve1 = bezier.Curve(np.array([ ... [2.0, 0.0], ... [1.5, 1.0], ... [1.0, 0.0], ... ])) >>> curve2 = bezier.Curve(np.array([ ... [3.0, 0.0], ... [1.5, 1.0], ... [0.0, 0.0], ... ])) >>> s, t = 0.5, 0.5 >>> curve1.evaluate(s) == curve2.evaluate(t) array([ True, True], dtype=bool) >>> intersection = Intersection(curve1, s, curve2, t) >>> classify_intersection(intersection) <IntersectionClassification.tangent_first: 'tangent_first'>
When the curves are moving in opposite directions at a point of tangency, there is no side to choose. Either the point of tangency is not part of any
CurvedPolygon
intersection>>> curve1 = bezier.Curve(np.array([ ... [2.0, 0.0], ... [1.5, 1.0], ... [1.0, 0.0], ... ])) >>> curve2 = bezier.Curve(np.array([ ... [0.0, 0.0], ... [1.5, 1.0], ... [3.0, 0.0], ... ])) >>> s, t = 0.5, 0.5 >>> curve1.evaluate(s) == curve2.evaluate(t) array([ True, True], dtype=bool) >>> intersection = Intersection(curve1, s, curve2, t) >>> classify_intersection(intersection) <IntersectionClassification.opposed: 'opposed'>
or the point of tangency is a “degenerate” part of two
CurvedPolygon
intersections. It is “degenerate” because from one direction, the point should be classified as1
and from another as0
:>>> curve1 = bezier.Curve(np.array([ ... [1.0, 0.0], ... [1.5, 1.0], ... [2.0, 0.0], ... ])) >>> curve2 = bezier.Curve(np.array([ ... [3.0, 0.0], ... [1.5, 1.0], ... [0.0, 0.0], ... ])) >>> s, t = 0.5, 0.5 >>> curve1.evaluate(s) == curve2.evaluate(t) array([ True, True], dtype=bool) >>> intersection = Intersection(curve1, s, curve2, t) >>> classify_intersection(intersection) Traceback (most recent call last): ... NotImplementedError: Curves moving in opposite direction but define overlapping arcs.
However, if the curvature of each curve is identical, we don’t try to distinguish further:
>>> curve1 = bezier.Curve(np.array([ ... [ 0.375, 0.0625], ... [-0.125, -0.0625], ... [-0.125, 0.0625], ... ])) >>> curve2 = bezier.Curve(np.array([ ... [ 0.75, 0.25], ... [-0.25, -0.25], ... [-0.25, 0.25], ... ])) >>> s, t = 0.5, 0.5 >>> curve1.evaluate(s) == curve2.evaluate(t) array([ True, True], dtype=bool) >>> hodograph(curve1, s) array([-0.5, 0. ]) >>> hodograph(curve2, t) array([-1., 0.]) >>> curvature(curve1, s) -2.0 >>> curvature(curve2, t) -2.0 >>> intersection = Intersection(curve1, s, curve2, t) >>> classify_intersection(intersection) Traceback (most recent call last): ... NotImplementedError: Tangent curves have same curvature.
In addition to points of tangency, intersections that happen at the end of an edge need special handling:
>>> curve1a = bezier.Curve(np.array([ ... [0.0, 0.0 ], ... [4.5, 0.0 ], ... [9.0, 2.25], ... ])) >>> curve2 = bezier.Curve(np.array([ ... [11.25, 0.0], ... [ 9.0 , 4.5], ... [ 2.75, 1.0], ... ])) >>> s, t = 1.0, 0.375 >>> curve1a.evaluate(s) == curve2.evaluate(t) array([ True, True], dtype=bool) >>> intersection = Intersection(curve1a, s, curve2, t) >>> classify_intersection(intersection) Traceback (most recent call last): ... ValueError: ('Intersection occurs at the end of an edge', 's', 1.0, 't', 0.375) >>> >>> curve1b = bezier.Curve(np.array([ ... [9.0, 2.25 ], ... [4.5, 2.375], ... [0.0, 2.5 ], ... ])) >>> curve1b.evaluate(0.0) == curve2.evaluate(t) array([ True, True], dtype=bool) >>> intersection = Intersection(curve1b, 0.0, curve2, t) >>> classify_intersection(intersection) <IntersectionClassification.first: 'first'>
Note
This assumes the intersection occurs in \(\mathbf{R}^2\) but doesn’t check this.
Note
This function doesn’t allow wiggle room / round-off when checking endpoints, nor when checking if the cross-product is near zero, nor when curvatures are compared. However, the most “correct” version of this function likely should allow for some round off.
Parameters: intersection (Intersection) – An intersection object. Returns: The “inside” curve type, based on the classification enum. Return type: IntersectionClassification Raises: ValueError
– If the intersection occurs at the end of either curve involved. This is because we want to classify which curve to move forward on, and we can’t move past the end of a segment.
bezier
is a Python helper for Bézier Curves, Triangles, and
Higher Order Objects.
This library provides:
Dive in and take a look!
