simetri.geometry.bezier

Functions for working with Bezier curves. https://pomax.github.io/bezierinfo is a good resource for understanding Bezier curves.

  1"""Functions for working with Bezier curves.
  2https://pomax.github.io/bezierinfo is a good resource for understanding Bezier curves.
  3"""
  4
  5from typing import Sequence
  6from functools import lru_cache as memoize
  7
  8import numpy as np
  9
 10from ..graphics.shape import Shape
 11from ..graphics.all_enums import Types
 12from ..graphics.common import Point
 13from ..helpers.utilities import find_closest_value
 14from ..settings.settings import defaults
 15from .geometry import (
 16    distance,
 17    line_angle,
 18    line_by_point_angle_length,
 19    normal,
 20    norm,
 21    normalize,
 22)
 23
 24array = np.array
 25
 26cubic_poly_matrix = np.array(
 27    [[1, 0, 0, 0], [-3, 3, 0, 0], [3, -6, 3, 0], [-1, 3, -3, 1]]
 28)
 29
 30quad_poly_matrix = np.array([[1, 0, 0], [-2, 2, 0], [1, -2, 1]])
 31
 32
 33class Bezier(Shape):
 34    """A Bezier curve defined by control points.
 35
 36    For cubic Bezier curves: [V1, CP1, CP2, V2]
 37    For quadratic Bezier curves: [V1, CP, V2]
 38    Like all other geometry in simetri.graphics,
 39    bezier curves are represented as a sequence of points.
 40    Both quadratic and cubic bezier curves are supported.
 41    Number of control points determines the type of Bezier curve.
 42    A cubic Bezier curve has 4 control points, while a quadratic Bezier curve has 3.
 43    curve.subtype reflects this as Types.BEZIER or Types.Q_BEZIER.
 44
 45    Attributes:
 46        control_points (Sequence[Point]): Control points of the Bezier curve.
 47        cubic (bool): True if the Bezier curve is cubic, False if quadratic.
 48        matrix (array): Polynomial matrix for the Bezier curve.
 49    """
 50
 51    def __init__(
 52        self,
 53        control_points: Sequence[Point],
 54        xform_matrix: array = None,
 55        n_points=None,
 56        **kwargs,
 57    ) -> None:
 58        """Initializes a Bezier curve.
 59
 60        Args:
 61            control_points (Sequence[Point]): Control points of the Bezier curve.
 62            xform_matrix (array, optional): Transformation matrix. Defaults to None.
 63            n_points (int, optional): Number of points on the curve. Defaults to None.
 64            **kwargs: Additional keyword arguments.
 65
 66        Raises:
 67            ValueError: If the number of control points is not 3 or 4.
 68        """
 69        if len(control_points) == 3:
 70            if n_points is None:
 71                n = defaults["n_bezier_points"]
 72            else:
 73                n = n_points
 74            vertices = q_bezier_points(*control_points, n)
 75            super().__init__(
 76                vertices, subtype=Types.Q_BEZIER, xform_matrix=xform_matrix, **kwargs
 77            )
 78            self.cubic = False
 79            self.matrix = quad_poly_matrix @ array(control_points)
 80
 81        elif len(control_points) == 4:
 82            if n_points is None:
 83                n = defaults["n_bezier_points"]
 84            else:
 85                n = n_points
 86            vertices = bezier_points(*control_points, n)
 87            super().__init__(
 88                vertices, subtype=Types.BEZIER, xform_matrix=xform_matrix, **kwargs
 89            )
 90            self.cubic = True
 91            self.matrix = cubic_poly_matrix @ array(control_points)
 92        else:
 93            raise ValueError("Invalid number of control points.")
 94        self.control_points = control_points
 95
 96    @property
 97    def control_points(self) -> Sequence[Point]:
 98        """Return the control points of the Bezier curve.
 99
100        Returns:
101            Sequence[Point]: Control points of the Bezier curve.
102        """
103        return self.__dict__['control_points']
104
105    @control_points.setter
106    def control_points(self, new_control_points: Sequence[Point]) -> None:
107        """Set new control points for the Bezier curve.
108
109        Args:
110            new_control_points (Sequence[Point]): New control points.
111
112        Raises:
113            ValueError: If the number of control points is not 3 or 4.
114        """
115        self.control_points = new_control_points
116        if len(new_control_points) == 3:
117            vertices = q_bezier_points(*new_control_points)
118            self[:] = vertices
119            self.subtype = Types.Q_BEZIER
120        elif len(new_control_points) == 4:
121            vertices = bezier_points(*new_control_points)
122            self[:] = vertices
123            self.subtype = Types.BEZIER
124        else:
125            raise ValueError("Invalid number of control points.")
126
127    def copy(self) -> Shape:
128        """Return a copy of the Bezier curve.
129
130        Returns:
131            Shape: Copy of the Bezier curve.
132        """
133        # to do: copy style and other attributes
134        copy_ = Bezier(self.control_points, xform_matrix=self.xform_matrix, n_points=len(self.vertices))
135
136        return copy_
137
138    def point(self, t: float):
139        """Return the point on the Bezier curve at t.
140
141        Args:
142            t (float): Parameter t, where 0 <= t <= 1.
143
144        Returns:
145            list: Point on the Bezier curve at t.
146        """
147        # if self.cubic:
148        #     np.array([t**3, t**2, t, 1]) @ self.matrix
149        # else:
150        #     np.array([t**2, t, 1]) @ self.matrix
151
152    def point2(self, t: float):
153        """Return the point on the Bezier curve at t.
154
155        Args:
156            t (float): Parameter t, where 0 <= t <= 1.
157
158        Returns:
159            list: Point on the Bezier curve at t.
160        """
161        p0, p1, p2, p3 = self.control_points
162        m = 1 - t
163        m2 = m * m
164        m3 = m2 * m
165        t2 = t * t
166        t3 = t2 * t
167        if self.cubic:
168            x = m3 * p0[0] + 3 * m2 * t * p1[0] + 3 * m * t2 * p2[0] + t3 * p3[0]
169            y = m3 * p0[1] + 3 * m2 * t * p1[1] + 3 * m * t2 * p2[1] + t3 * p3[1]
170        else:
171            x = m2 * p0[0] + 2 * m * t * p1[0] + t2 * p2[0]
172            y = m2 * p0[1] + 2 * m * t * p1[1] + t2 * p2[1]
173
174        return [x, y]
175
176    def derivative(self, t: float):
177        """Return the derivative of the Bezier curve at t.
178
179        Args:
180            t (float): Parameter t, where 0 <= t <= 1.
181
182        Returns:
183            list: Derivative of the Bezier curve at t.
184        """
185        if self.cubic:
186            return get_cubic_derivative(t, self.control_points)
187        else:
188            return get_quadratic_derivative(t, self.control_points)
189
190    def normal(self, t: float):
191        """Return the normal of the Bezier curve at t.
192
193        Args:
194            t (float): Parameter t, where 0 <= t <= 1.
195
196        Returns:
197            list: Normal of the Bezier curve at t.
198        """
199        d = self.derivative(t)
200        q = np.sqrt(d[0] * d[0] + d[1] * d[1])
201        return [-d[1] / q, d[0] / q]
202
203    def tangent(self, t: float):
204        """Draw a unit tangent vector at t.
205
206        Args:
207            t (float): Parameter t, where 0 <= t <= 1.
208
209        Returns:
210            list: Unit tangent vector at t.
211        """
212        d = self.derivative(t)
213        m = np.sqrt(d[0] * d[0] + d[1] * d[1])
214        d = [d[0] / m, d[1] / m]
215        return d
216
217
218def equidistant_points(p0: Point, p1: Point, p2: Point, p3: Point, n_points=10):
219    """Return the points on a Bezier curve with equidistant spacing.
220
221    Args:
222        p0 (list): First control point.
223        p1 (list): Second control point.
224        p2 (list): Third control point.
225        p3 (list): Fourth control point.
226        n_points (int, optional): Number of points. Defaults to 10.
227
228    Returns:
229        tuple: Points on the Bezier curve, equidistant points, tangents, and normals.
230    """
231    controls = [p0, p1, p2, p3]
232    n = 100
233    points = bezier_points(p0, p1, p2, p3, n)
234    tot = 0
235    seg_lengths = [0]
236    tangents = [norm((p1[0] - p0[0], p1[1] - p0[1]))]
237    normals = [normal(p0, p1)]
238    eq_points = [p0]
239    for i in range(1, n):
240        dist = distance(points[i - 1], points[i])
241        tot += dist
242        seg_lengths.append(tot)
243
244    for i in range(1, n_points):
245        _, ind = find_closest_value(seg_lengths, i * tot / n_points)
246        pnt = points[ind]
247        eq_points.append(pnt)
248        d = get_cubic_derivative(ind / n, controls)
249        d1 = normalize(d)
250        p1 = pnt
251        p2 = get_normal(d)
252        tangents.append(d1)
253        normals.append(p2)
254
255    return points, eq_points, tangents, normals
256
257
258def offset_points(controls, offset, n_points, double=False):
259    """Return the points on the offset curve.
260
261    Args:
262        controls (list): Control points of the Bezier curve.
263        offset (float): Offset distance.
264        n_points (int): Number of points on the curve.
265        double (bool, optional): If True, return double offset points. Defaults to False.
266
267    Returns:
268        list: Points on the offset curve.
269    """
270    n = 100
271    points = bezier_points(*controls, n_points=n)
272    tot = 0
273    seg_lengths = [0]
274    for i in range(1, n):
275        dist = distance(points[i - 1], points[i])
276        tot += dist
277        seg_lengths.append(tot)
278
279    x, y = controls[0][:2]
280    np = normal(x, y)
281    x2, y2 = (x + offset * np[0], y + offset * np[1])
282    offset_pnts = [(x2, y2)]
283    if double:
284        p1, p2 = mirror_point((x, y), (x2, y2))
285        offset_pnts2 = [p2]
286    for i in range(1, n_points):
287        _, ind = find_closest_value(seg_lengths, i * tot / n)
288        pnt = points[ind]
289        d = get_cubic_derivative(ind / n, controls)
290        p1 = pnt
291        p2 = get_normal(d)
292        x2, y2 = p1[0] + offset * p2[0], p1[1] + offset * p2[1]
293        offset_pnts.append((x2, y2))
294        if double:
295            _, p3 = mirror_point(pnt, (x2, y2))
296            offset_pnts2.append(p3)
297
298    if double:
299        return offset_pnts, offset_pnts2
300    else:
301        return offset_pnts
302
303
304class BezierPoints(Shape):
305    """Points of a Bezier curve defined by the given control points.
306
307    These points are spaced evenly along the curve (unlike parametric points).
308    Normal and tangent unit vectors are also available at these points.
309
310    Attributes:
311        control_points (Sequence[Point]): Control points of the Bezier curve.
312        param_points (list): Parametric points on the Bezier curve.
313        tangents (list): Tangent vectors at the points.
314        normals (list): Normal vectors at the points.
315        n_points (int): Number of points on the curve.
316    """
317
318    def __init__(
319        self, control_points: Sequence[Point], n_points: int = 10, **kwargs
320    ) -> None:
321        """Initializes Bezier points.
322
323        Args:
324            control_points (Sequence[Point]): Control points of the Bezier curve.
325            n_points (int, optional): Number of points on the curve. Defaults to 10.
326            **kwargs: Additional keyword arguments.
327
328        Raises:
329            ValueError: If the number of control points is not 3 or 4.
330        """
331        if len(control_points) not in (4, 3):
332            raise ValueError("Invalid number of control points.")
333
334        param_points, vertices, tangents, normals = equidistant_points(
335            *control_points, n_points
336        )
337        super().__init__(vertices, subtype=Types.BEZIER_POINTS, **kwargs)
338        self.control_points = control_points
339        self.param_points = param_points
340        self.tangents = tangents
341        self.normals = normals
342        self.n_points = n_points
343
344    def offsets(self, offset: float, double: bool=False):
345        """Return the points on the offset curve.
346
347        Args:
348            offset (float): Offset distance.
349            double (bool, optional): If True, return double offset points. Defaults to False.
350
351        Returns:
352            list: Points on the offset curve.
353        """
354        offset_points1 = []
355        if double:
356            offset_points2 = []
357        for i, pnt in enumerate(self.vertices):
358            n_p = self.normals[i]
359            x2, y2 = pnt[0] + offset * n_p[0], pnt[1] + offset * n_p[1]
360            offset_points1.append((x2, y2))
361            if double:
362                _, p3 = mirror_point((x2, y2), pnt)
363                offset_points2.append(p3)
364
365        if double:
366            return offset_points1, offset_points2
367
368        return offset_points1
369
370
371M = array([[1, 0, 0, 0], [-3, 3, 0, 0], [3, -6, 3, 0], [-1, 3, -3, 1]])
372
373
374def bezier_points(p0, p1: Point, p2: Point, p3: Point, n_points=10):
375    """Return the points on a cubic Bezier curve.
376
377    Args:
378        p0 (list): First control point.
379        p1 (list): Second control point.
380        p2 (list): Third control point.
381        p3 (list): Fourth control point.
382        n_points (int, optional): Number of points. Defaults to 10.
383
384    Returns:
385        list: Points on the cubic Bezier curve.
386
387    Raises:
388        ValueError: If n_points is less than 5.
389    """
390    if n_points < 5:
391        raise ValueError("n_points must be at least 5.")
392
393    n = n_points
394    f = np.ones(n)
395    t = np.linspace(0, 1, n)
396    t2 = t * t
397    t3 = t2 * t
398    T = np.column_stack((f, t, t2, t3))
399    TM = T @ M
400    P = array([p0, p1, p2, p3])
401
402    return TM @ P
403
404
405MQ = array([[1, 0, 0], [-2, 2, 0], [1, -2, 1]])
406
407
408def q_bezier_points(p0: Point, p1: Point, p2: Point, n_points: int):
409    """Return the points on a quadratic Bezier curve.
410
411    Args:
412        p0 (list): First control point.
413        p1 (list): Second control point.
414        p2 (list): Third control point.
415        n_points (int): Number of points.
416
417    Returns:
418        list: Points on the quadratic Bezier curve.
419
420    Raises:
421        ValueError: If n_points is less than 5.
422    """
423    if n_points < 5:
424        raise ValueError("n_points must be at least 5.")
425
426    n = n_points
427    f = np.ones(n)
428    t = np.linspace(0, 1, n)
429    t2 = t * t
430    T = np.column_stack((f, t, t2))
431    TMQ = T @ MQ
432    P = array([p0, p1, p2])
433
434    return TMQ @ P
435
436
437def split_bezier(p0: Point, p1: Point, p2: Point, p3: Point, z:float, n_points=10):
438    """Split a cubic Bezier curve at t=z.
439
440    Args:
441        p0 (list): First control point.
442        p1 (list): Second control point.
443        p2 (list): Third control point.
444        p3 (list): Fourth control point.
445        z (float): Parameter z, where 0 <= z <= 1.
446        n_points (int, optional): Number of points. Defaults to 10.
447
448    Returns:
449        tuple: Two Bezier curves split at t=z.
450    """
451    p0 = array(p0)
452    p1 = array(p1)
453    p2 = array(p2)
454    p3 = array(p3)
455    bezier1 = [
456        [p0],
457        [z * p1 - (z - 1) * p0],
458        [z**2 * p2 - 2 * z * (z - 1) * p1 + (z - 1) ** 2 * p0],
459        [
460            z**3 * p3
461            - 3 * z**2 * (z - 1) * p2
462            + 3 * z * (z - 1) ** 2 * p1
463            - (z - 1) ** 3 * p0
464        ],
465    ]
466
467    bezier2 = [
468        [z**3 * p0],
469        [3 * z**2 * (z - 1) * p1 - 3 * z * (z - 1) ** 2 * p0],
470        [3 * z * (z - 1) * p2 - 3 * (z - 1) ** 2 * p1],
471        [z * p3 - (z - 1) * p2],
472    ]
473
474    return Bezier(bezier1, n_points=n_points), Bezier(bezier2, n_points=n_points)
475
476
477def split_q_bezier(p0: Point, p1: Point, p2: Point, z:float, n_points=10):
478    """Split a quadratic Bezier curve at t=z.
479
480    Args:
481        p0 (list): First control point.
482        p1 (list): Second control point.
483        p2 (list): Third control point.
484        z (float): Parameter z, where 0 <= z <= 1.
485        n_points (int, optional): Number of points. Defaults to 10.
486
487    Returns:
488        tuple: Two Bezier curves split at t=z.
489    """
490    p0 = array(p0)
491    p1 = array(p1)
492    p2 = array(p2)
493    bezier1 = [
494        [p0],
495        [z * p1 - (z - 1) * p0],
496        [z**2 * p2 - 2 * z * (z - 1) * p1 + (z - 1) ** 2 * p0],
497    ]
498
499    bezier2 = [
500        [z**2 * p0],
501        [2 * z * (z - 1) * p1 - (z - 1) ** 2 * p0],
502        [z * p2 - (z - 1) * p1],
503    ]
504
505    return Bezier(bezier1, n_points=n_points), Bezier(bezier2, n_points=n_points)
506
507
508def mirror_point(cp: Point, vertex: Point):
509    """Return the mirror of cp about vertex.
510
511    Args:
512        cp (list): Control point to be mirrored.
513        vertex (list): Vertex point.
514
515    Returns:
516        list: Mirrored control point.
517    """
518    length = distance(cp, vertex)
519    angle = line_angle(cp, vertex)
520    cp2 = line_by_point_angle_length(vertex, angle, length)
521    return cp2
522
523
524def curve(v1: Point, c1: Point, c2: Point, v2: Point, *args, **kwargs):
525    """Return a cubic Bezier curve/s.
526
527    Args:
528        v1 (list): First vertex.
529        c1 (list): First control point.
530        c2 (list): Second control point.
531        v2 (list): Second vertex.
532        *args: Additional control points and vertices.
533        **kwargs: Additional keyword arguments.
534
535    Returns:
536        list: List of cubic Bezier curves.
537
538    Raises:
539        ValueError: If the number of control points is invalid.
540    """
541    curves = [Bezier([v1, c1, c2, v2], **kwargs)]
542    last_vertex = v2
543    for arg in args:
544        if len(arg) == 2:
545            c3 = mirror_point(c2, v2)
546            v3 = arg[1]
547            c4 = arg[0]
548            curves.append(Bezier([last_vertex, c3, c4, v3], **kwargs))
549            last_vertex = v3
550        elif len(arg) == 3:
551            c3 = arg[0]
552            c4 = arg[1]
553            v3 = arg[2]
554            curves.append(Bezier([last_vertex, c3, c4, v3], **kwargs))
555            last_vertex = v3
556        else:
557            raise ValueError("Invalid number of control points.")
558
559    return curves
560
561
562def q_curve(v1: Point, c: Point, v2: Point, *args, **kwargs):
563    """Return a quadratic Bezier curve/s.
564
565    Args:
566        v1 (list): First vertex.
567        c (list): Control point.
568        v2 (list): Second vertex.
569        *args: Additional control points and vertices.
570        **kwargs: Additional keyword arguments.
571
572    Returns:
573        list: List of quadratic Bezier curves.
574
575    Raises:
576        ValueError: If the number of control points is invalid.
577    """
578    curves = [Bezier([v1, c, v2], **kwargs)]
579    last_vertex = v2
580    for arg in args:
581        if len(arg) == 1:
582            c3 = mirror_point(c, v2)
583            v3 = arg[0]
584            curves.append(Bezier([last_vertex, c3, v3], **kwargs))
585            last_vertex = v3
586        elif len(arg) == 2:
587            c3 = arg[0]
588            v3 = arg[1]
589            curves.append(Bezier([last_vertex, c3, v3], **kwargs))
590            last_vertex = v3
591        else:
592            raise ValueError("Invalid number of control points.")
593
594    return curves
595
596
597def get_quadratic_derivative(t: float, points: Sequence[Point]):
598    """Return the derivative of a quadratic Bezier curve at t.
599
600    Args:
601        t (float): Parameter t, where 0 <= t <= 1.
602        points (list): Control points of the Bezier curve.
603
604    Returns:
605        list: Derivative of the quadratic Bezier curve at t.
606    """
607    mt = 1 - t
608    d = [
609        2 * (points[1][0] - points[0][0]),
610        2 * (points[1][1] - points[0][1]),
611        2 * (points[2][0] - points[1][0]),
612        2 * (points[2][1] - points[1][1]),
613    ]
614
615    return [mt * d[0] + t * d[2], mt * d[1] + t * d[3]]
616
617
618def get_cubic_derivative(t: float, points: Sequence[Point]):
619    """Return the derivative of a cubic Bezier curve at t.
620
621    Args:
622        t (float): Parameter t, where 0 <= t <= 1.
623        points (list): Control points of the Bezier curve.
624
625    Returns:
626        list: Derivative of the cubic Bezier curve at t.
627    """
628    mt = 1 - t
629    a = mt * mt
630    b = 2 * mt * t
631    c = t * t
632    d = [
633        3 * (points[1][0] - points[0][0]),
634        3 * (points[1][1] - points[0][1]),
635        3 * (points[2][0] - points[1][0]),
636        3 * (points[2][1] - points[1][1]),
637        3 * (points[3][0] - points[2][0]),
638        3 * (points[3][1] - points[2][1]),
639    ]
640
641    return [a * d[0] + b * d[2] + c * d[4], a * d[1] + b * d[3] + c * d[5]]
642
643
644def get_normal(d: Sequence[float]):
645    """Return the normal of a given line.
646
647    Args:
648        d (list): Derivative of the line.
649
650    Returns:
651        list: Normal of the line.
652    """
653    q = np.sqrt(d[0] * d[0] + d[1] * d[1])
654    return [-d[1] / q, d[0] / q]
def array(unknown):

array(object, dtype=None, *, copy=True, order='K', subok=False, ndmin=0, like=None)

Create an array.

Parameters

object : array_like An array, any object exposing the array interface, an object whose __array__ method returns an array, or any (nested) sequence. If object is a scalar, a 0-dimensional array containing object is returned. dtype : data-type, optional The desired data-type for the array. If not given, NumPy will try to use a default dtype that can represent the values (by applying promotion rules when necessary.) copy : bool, optional If True (default), then the array data is copied. If None, a copy will only be made if __array__ returns a copy, if obj is a nested sequence, or if a copy is needed to satisfy any of the other requirements (dtype, order, etc.). Note that any copy of the data is shallow, i.e., for arrays with object dtype, the new array will point to the same objects. See Examples for ndarray.copy. For False it raises a ValueError if a copy cannot be avoided. Default: True. order : {'K', 'A', 'C', 'F'}, optional Specify the memory layout of the array. If object is not an array, the newly created array will be in C order (row major) unless 'F' is specified, in which case it will be in Fortran order (column major). If object is an array the following holds.

===== ========= ===================================================
order  no copy                     copy=True
===== ========= ===================================================
'K'   unchanged F & C order preserved, otherwise most similar order
'A'   unchanged F order if input is F and not C, otherwise C order
'C'   C order   C order
'F'   F order   F order
===== ========= ===================================================

When ``copy=None`` and a copy is made for other reasons, the result is
the same as if ``copy=True``, with some exceptions for 'A', see the
Notes section. The default order is 'K'.

subok : bool, optional If True, then sub-classes will be passed-through, otherwise the returned array will be forced to be a base-class array (default). ndmin : int, optional Specifies the minimum number of dimensions that the resulting array should have. Ones will be prepended to the shape as needed to meet this requirement. like : array_like, optional Reference object to allow the creation of arrays which are not NumPy arrays. If an array-like passed in as like supports the __array_function__ protocol, the result will be defined by it. In this case, it ensures the creation of an array object compatible with that passed in via this argument.

*New in version 1.20.0.*

Returns

out : ndarray An array object satisfying the specified requirements.

See Also

empty_like : Return an empty array with shape and type of input. ones_like : Return an array of ones with shape and type of input. zeros_like : Return an array of zeros with shape and type of input. full_like : Return a new array with shape of input filled with value. empty : Return a new uninitialized array. ones : Return a new array setting values to one. zeros : Return a new array setting values to zero. full : Return a new array of given shape filled with value. copy: Return an array copy of the given object.

Notes

When order is 'A' and object is an array in neither 'C' nor 'F' order, and a copy is forced by a change in dtype, then the order of the result is not necessarily 'C' as expected. This is likely a bug.

Examples

>>> np.array([1, 2, 3])
array([1, 2, 3])

Upcasting:

>>> np.array([1, 2, 3.0])
array([ 1.,  2.,  3.])

More than one dimension:

>>> np.array([[1, 2], [3, 4]])
array([[1, 2],
       [3, 4]])

Minimum dimensions 2:

>>> np.array([1, 2, 3], ndmin=2)
array([[1, 2, 3]])

Type provided:

>>> np.array([1, 2, 3], dtype=complex)
array([ 1.+0.j,  2.+0.j,  3.+0.j])

Data-type consisting of more than one element:

>>> x = np.array([(1,2),(3,4)],dtype=[('a','<i4'),('b','<i4')])
>>> x['a']
array([1, 3])

Creating an array from sub-classes:

>>> np.array(np.asmatrix('1 2; 3 4'))
array([[1, 2],
       [3, 4]])
>>> np.array(np.asmatrix('1 2; 3 4'), subok=True)
matrix([[1, 2],
        [3, 4]])
cubic_poly_matrix = array([[ 1, 0, 0, 0], [-3, 3, 0, 0], [ 3, -6, 3, 0], [-1, 3, -3, 1]])
quad_poly_matrix = array([[ 1, 0, 0], [-2, 2, 0], [ 1, -2, 1]])
class Bezier(simetri.graphics.shape.Shape):
 34class Bezier(Shape):
 35    """A Bezier curve defined by control points.
 36
 37    For cubic Bezier curves: [V1, CP1, CP2, V2]
 38    For quadratic Bezier curves: [V1, CP, V2]
 39    Like all other geometry in simetri.graphics,
 40    bezier curves are represented as a sequence of points.
 41    Both quadratic and cubic bezier curves are supported.
 42    Number of control points determines the type of Bezier curve.
 43    A cubic Bezier curve has 4 control points, while a quadratic Bezier curve has 3.
 44    curve.subtype reflects this as Types.BEZIER or Types.Q_BEZIER.
 45
 46    Attributes:
 47        control_points (Sequence[Point]): Control points of the Bezier curve.
 48        cubic (bool): True if the Bezier curve is cubic, False if quadratic.
 49        matrix (array): Polynomial matrix for the Bezier curve.
 50    """
 51
 52    def __init__(
 53        self,
 54        control_points: Sequence[Point],
 55        xform_matrix: array = None,
 56        n_points=None,
 57        **kwargs,
 58    ) -> None:
 59        """Initializes a Bezier curve.
 60
 61        Args:
 62            control_points (Sequence[Point]): Control points of the Bezier curve.
 63            xform_matrix (array, optional): Transformation matrix. Defaults to None.
 64            n_points (int, optional): Number of points on the curve. Defaults to None.
 65            **kwargs: Additional keyword arguments.
 66
 67        Raises:
 68            ValueError: If the number of control points is not 3 or 4.
 69        """
 70        if len(control_points) == 3:
 71            if n_points is None:
 72                n = defaults["n_bezier_points"]
 73            else:
 74                n = n_points
 75            vertices = q_bezier_points(*control_points, n)
 76            super().__init__(
 77                vertices, subtype=Types.Q_BEZIER, xform_matrix=xform_matrix, **kwargs
 78            )
 79            self.cubic = False
 80            self.matrix = quad_poly_matrix @ array(control_points)
 81
 82        elif len(control_points) == 4:
 83            if n_points is None:
 84                n = defaults["n_bezier_points"]
 85            else:
 86                n = n_points
 87            vertices = bezier_points(*control_points, n)
 88            super().__init__(
 89                vertices, subtype=Types.BEZIER, xform_matrix=xform_matrix, **kwargs
 90            )
 91            self.cubic = True
 92            self.matrix = cubic_poly_matrix @ array(control_points)
 93        else:
 94            raise ValueError("Invalid number of control points.")
 95        self.control_points = control_points
 96
 97    @property
 98    def control_points(self) -> Sequence[Point]:
 99        """Return the control points of the Bezier curve.
100
101        Returns:
102            Sequence[Point]: Control points of the Bezier curve.
103        """
104        return self.__dict__['control_points']
105
106    @control_points.setter
107    def control_points(self, new_control_points: Sequence[Point]) -> None:
108        """Set new control points for the Bezier curve.
109
110        Args:
111            new_control_points (Sequence[Point]): New control points.
112
113        Raises:
114            ValueError: If the number of control points is not 3 or 4.
115        """
116        self.control_points = new_control_points
117        if len(new_control_points) == 3:
118            vertices = q_bezier_points(*new_control_points)
119            self[:] = vertices
120            self.subtype = Types.Q_BEZIER
121        elif len(new_control_points) == 4:
122            vertices = bezier_points(*new_control_points)
123            self[:] = vertices
124            self.subtype = Types.BEZIER
125        else:
126            raise ValueError("Invalid number of control points.")
127
128    def copy(self) -> Shape:
129        """Return a copy of the Bezier curve.
130
131        Returns:
132            Shape: Copy of the Bezier curve.
133        """
134        # to do: copy style and other attributes
135        copy_ = Bezier(self.control_points, xform_matrix=self.xform_matrix, n_points=len(self.vertices))
136
137        return copy_
138
139    def point(self, t: float):
140        """Return the point on the Bezier curve at t.
141
142        Args:
143            t (float): Parameter t, where 0 <= t <= 1.
144
145        Returns:
146            list: Point on the Bezier curve at t.
147        """
148        # if self.cubic:
149        #     np.array([t**3, t**2, t, 1]) @ self.matrix
150        # else:
151        #     np.array([t**2, t, 1]) @ self.matrix
152
153    def point2(self, t: float):
154        """Return the point on the Bezier curve at t.
155
156        Args:
157            t (float): Parameter t, where 0 <= t <= 1.
158
159        Returns:
160            list: Point on the Bezier curve at t.
161        """
162        p0, p1, p2, p3 = self.control_points
163        m = 1 - t
164        m2 = m * m
165        m3 = m2 * m
166        t2 = t * t
167        t3 = t2 * t
168        if self.cubic:
169            x = m3 * p0[0] + 3 * m2 * t * p1[0] + 3 * m * t2 * p2[0] + t3 * p3[0]
170            y = m3 * p0[1] + 3 * m2 * t * p1[1] + 3 * m * t2 * p2[1] + t3 * p3[1]
171        else:
172            x = m2 * p0[0] + 2 * m * t * p1[0] + t2 * p2[0]
173            y = m2 * p0[1] + 2 * m * t * p1[1] + t2 * p2[1]
174
175        return [x, y]
176
177    def derivative(self, t: float):
178        """Return the derivative of the Bezier curve at t.
179
180        Args:
181            t (float): Parameter t, where 0 <= t <= 1.
182
183        Returns:
184            list: Derivative of the Bezier curve at t.
185        """
186        if self.cubic:
187            return get_cubic_derivative(t, self.control_points)
188        else:
189            return get_quadratic_derivative(t, self.control_points)
190
191    def normal(self, t: float):
192        """Return the normal of the Bezier curve at t.
193
194        Args:
195            t (float): Parameter t, where 0 <= t <= 1.
196
197        Returns:
198            list: Normal of the Bezier curve at t.
199        """
200        d = self.derivative(t)
201        q = np.sqrt(d[0] * d[0] + d[1] * d[1])
202        return [-d[1] / q, d[0] / q]
203
204    def tangent(self, t: float):
205        """Draw a unit tangent vector at t.
206
207        Args:
208            t (float): Parameter t, where 0 <= t <= 1.
209
210        Returns:
211            list: Unit tangent vector at t.
212        """
213        d = self.derivative(t)
214        m = np.sqrt(d[0] * d[0] + d[1] * d[1])
215        d = [d[0] / m, d[1] / m]
216        return d

A Bezier curve defined by control points.

For cubic Bezier curves: [V1, CP1, CP2, V2] For quadratic Bezier curves: [V1, CP, V2] Like all other geometry in simetri.graphics, bezier curves are represented as a sequence of points. Both quadratic and cubic bezier curves are supported. Number of control points determines the type of Bezier curve. A cubic Bezier curve has 4 control points, while a quadratic Bezier curve has 3. curve.subtype reflects this as Types.BEZIER or Types.Q_BEZIER.

Attributes:
  • control_points (Sequence[Point]): Control points of the Bezier curve.
  • cubic (bool): True if the Bezier curve is cubic, False if quadratic.
  • matrix (array): Polynomial matrix for the Bezier curve.
Bezier( control_points: Sequence[Sequence[float]], xform_matrix: <built-in function array> = None, n_points=None, **kwargs)
52    def __init__(
53        self,
54        control_points: Sequence[Point],
55        xform_matrix: array = None,
56        n_points=None,
57        **kwargs,
58    ) -> None:
59        """Initializes a Bezier curve.
60
61        Args:
62            control_points (Sequence[Point]): Control points of the Bezier curve.
63            xform_matrix (array, optional): Transformation matrix. Defaults to None.
64            n_points (int, optional): Number of points on the curve. Defaults to None.
65            **kwargs: Additional keyword arguments.
66
67        Raises:
68            ValueError: If the number of control points is not 3 or 4.
69        """
70        if len(control_points) == 3:
71            if n_points is None:
72                n = defaults["n_bezier_points"]
73            else:
74                n = n_points
75            vertices = q_bezier_points(*control_points, n)
76            super().__init__(
77                vertices, subtype=Types.Q_BEZIER, xform_matrix=xform_matrix, **kwargs
78            )
79            self.cubic = False
80            self.matrix = quad_poly_matrix @ array(control_points)
81
82        elif len(control_points) == 4:
83            if n_points is None:
84                n = defaults["n_bezier_points"]
85            else:
86                n = n_points
87            vertices = bezier_points(*control_points, n)
88            super().__init__(
89                vertices, subtype=Types.BEZIER, xform_matrix=xform_matrix, **kwargs
90            )
91            self.cubic = True
92            self.matrix = cubic_poly_matrix @ array(control_points)
93        else:
94            raise ValueError("Invalid number of control points.")
95        self.control_points = control_points

Initializes a Bezier curve.

Arguments:
  • control_points (Sequence[Point]): Control points of the Bezier curve.
  • xform_matrix (array, optional): Transformation matrix. Defaults to None.
  • n_points (int, optional): Number of points on the curve. Defaults to None.
  • **kwargs: Additional keyword arguments.
Raises:
  • ValueError: If the number of control points is not 3 or 4.
control_points: Sequence[Sequence[float]]
 97    @property
 98    def control_points(self) -> Sequence[Point]:
 99        """Return the control points of the Bezier curve.
100
101        Returns:
102            Sequence[Point]: Control points of the Bezier curve.
103        """
104        return self.__dict__['control_points']

Return the control points of the Bezier curve.

Returns:

Sequence[Point]: Control points of the Bezier curve.

def copy(self) -> simetri.graphics.shape.Shape:
128    def copy(self) -> Shape:
129        """Return a copy of the Bezier curve.
130
131        Returns:
132            Shape: Copy of the Bezier curve.
133        """
134        # to do: copy style and other attributes
135        copy_ = Bezier(self.control_points, xform_matrix=self.xform_matrix, n_points=len(self.vertices))
136
137        return copy_

Return a copy of the Bezier curve.

Returns:

Shape: Copy of the Bezier curve.

def point(self, t: float):
139    def point(self, t: float):
140        """Return the point on the Bezier curve at t.
141
142        Args:
143            t (float): Parameter t, where 0 <= t <= 1.
144
145        Returns:
146            list: Point on the Bezier curve at t.
147        """
148        # if self.cubic:
149        #     np.array([t**3, t**2, t, 1]) @ self.matrix
150        # else:
151        #     np.array([t**2, t, 1]) @ self.matrix

Return the point on the Bezier curve at t.

Arguments:
  • t (float): Parameter t, where 0 <= t <= 1.
Returns:

list: Point on the Bezier curve at t.

def point2(self, t: float):
153    def point2(self, t: float):
154        """Return the point on the Bezier curve at t.
155
156        Args:
157            t (float): Parameter t, where 0 <= t <= 1.
158
159        Returns:
160            list: Point on the Bezier curve at t.
161        """
162        p0, p1, p2, p3 = self.control_points
163        m = 1 - t
164        m2 = m * m
165        m3 = m2 * m
166        t2 = t * t
167        t3 = t2 * t
168        if self.cubic:
169            x = m3 * p0[0] + 3 * m2 * t * p1[0] + 3 * m * t2 * p2[0] + t3 * p3[0]
170            y = m3 * p0[1] + 3 * m2 * t * p1[1] + 3 * m * t2 * p2[1] + t3 * p3[1]
171        else:
172            x = m2 * p0[0] + 2 * m * t * p1[0] + t2 * p2[0]
173            y = m2 * p0[1] + 2 * m * t * p1[1] + t2 * p2[1]
174
175        return [x, y]

Return the point on the Bezier curve at t.

Arguments:
  • t (float): Parameter t, where 0 <= t <= 1.
Returns:

list: Point on the Bezier curve at t.

def derivative(self, t: float):
177    def derivative(self, t: float):
178        """Return the derivative of the Bezier curve at t.
179
180        Args:
181            t (float): Parameter t, where 0 <= t <= 1.
182
183        Returns:
184            list: Derivative of the Bezier curve at t.
185        """
186        if self.cubic:
187            return get_cubic_derivative(t, self.control_points)
188        else:
189            return get_quadratic_derivative(t, self.control_points)

Return the derivative of the Bezier curve at t.

Arguments:
  • t (float): Parameter t, where 0 <= t <= 1.
Returns:

list: Derivative of the Bezier curve at t.

def normal(self, t: float):
191    def normal(self, t: float):
192        """Return the normal of the Bezier curve at t.
193
194        Args:
195            t (float): Parameter t, where 0 <= t <= 1.
196
197        Returns:
198            list: Normal of the Bezier curve at t.
199        """
200        d = self.derivative(t)
201        q = np.sqrt(d[0] * d[0] + d[1] * d[1])
202        return [-d[1] / q, d[0] / q]

Return the normal of the Bezier curve at t.

Arguments:
  • t (float): Parameter t, where 0 <= t <= 1.
Returns:

list: Normal of the Bezier curve at t.

def tangent(self, t: float):
204    def tangent(self, t: float):
205        """Draw a unit tangent vector at t.
206
207        Args:
208            t (float): Parameter t, where 0 <= t <= 1.
209
210        Returns:
211            list: Unit tangent vector at t.
212        """
213        d = self.derivative(t)
214        m = np.sqrt(d[0] * d[0] + d[1] * d[1])
215        d = [d[0] / m, d[1] / m]
216        return d

Draw a unit tangent vector at t.

Arguments:
  • t (float): Parameter t, where 0 <= t <= 1.
Returns:

list: Unit tangent vector at t.

def equidistant_points( p0: Sequence[float], p1: Sequence[float], p2: Sequence[float], p3: Sequence[float], n_points=10):
219def equidistant_points(p0: Point, p1: Point, p2: Point, p3: Point, n_points=10):
220    """Return the points on a Bezier curve with equidistant spacing.
221
222    Args:
223        p0 (list): First control point.
224        p1 (list): Second control point.
225        p2 (list): Third control point.
226        p3 (list): Fourth control point.
227        n_points (int, optional): Number of points. Defaults to 10.
228
229    Returns:
230        tuple: Points on the Bezier curve, equidistant points, tangents, and normals.
231    """
232    controls = [p0, p1, p2, p3]
233    n = 100
234    points = bezier_points(p0, p1, p2, p3, n)
235    tot = 0
236    seg_lengths = [0]
237    tangents = [norm((p1[0] - p0[0], p1[1] - p0[1]))]
238    normals = [normal(p0, p1)]
239    eq_points = [p0]
240    for i in range(1, n):
241        dist = distance(points[i - 1], points[i])
242        tot += dist
243        seg_lengths.append(tot)
244
245    for i in range(1, n_points):
246        _, ind = find_closest_value(seg_lengths, i * tot / n_points)
247        pnt = points[ind]
248        eq_points.append(pnt)
249        d = get_cubic_derivative(ind / n, controls)
250        d1 = normalize(d)
251        p1 = pnt
252        p2 = get_normal(d)
253        tangents.append(d1)
254        normals.append(p2)
255
256    return points, eq_points, tangents, normals

Return the points on a Bezier curve with equidistant spacing.

Arguments:
  • p0 (list): First control point.
  • p1 (list): Second control point.
  • p2 (list): Third control point.
  • p3 (list): Fourth control point.
  • n_points (int, optional): Number of points. Defaults to 10.
Returns:

tuple: Points on the Bezier curve, equidistant points, tangents, and normals.

def offset_points(controls, offset, n_points, double=False):
259def offset_points(controls, offset, n_points, double=False):
260    """Return the points on the offset curve.
261
262    Args:
263        controls (list): Control points of the Bezier curve.
264        offset (float): Offset distance.
265        n_points (int): Number of points on the curve.
266        double (bool, optional): If True, return double offset points. Defaults to False.
267
268    Returns:
269        list: Points on the offset curve.
270    """
271    n = 100
272    points = bezier_points(*controls, n_points=n)
273    tot = 0
274    seg_lengths = [0]
275    for i in range(1, n):
276        dist = distance(points[i - 1], points[i])
277        tot += dist
278        seg_lengths.append(tot)
279
280    x, y = controls[0][:2]
281    np = normal(x, y)
282    x2, y2 = (x + offset * np[0], y + offset * np[1])
283    offset_pnts = [(x2, y2)]
284    if double:
285        p1, p2 = mirror_point((x, y), (x2, y2))
286        offset_pnts2 = [p2]
287    for i in range(1, n_points):
288        _, ind = find_closest_value(seg_lengths, i * tot / n)
289        pnt = points[ind]
290        d = get_cubic_derivative(ind / n, controls)
291        p1 = pnt
292        p2 = get_normal(d)
293        x2, y2 = p1[0] + offset * p2[0], p1[1] + offset * p2[1]
294        offset_pnts.append((x2, y2))
295        if double:
296            _, p3 = mirror_point(pnt, (x2, y2))
297            offset_pnts2.append(p3)
298
299    if double:
300        return offset_pnts, offset_pnts2
301    else:
302        return offset_pnts

Return the points on the offset curve.

Arguments:
  • controls (list): Control points of the Bezier curve.
  • offset (float): Offset distance.
  • n_points (int): Number of points on the curve.
  • double (bool, optional): If True, return double offset points. Defaults to False.
Returns:

list: Points on the offset curve.

class BezierPoints(simetri.graphics.shape.Shape):
305class BezierPoints(Shape):
306    """Points of a Bezier curve defined by the given control points.
307
308    These points are spaced evenly along the curve (unlike parametric points).
309    Normal and tangent unit vectors are also available at these points.
310
311    Attributes:
312        control_points (Sequence[Point]): Control points of the Bezier curve.
313        param_points (list): Parametric points on the Bezier curve.
314        tangents (list): Tangent vectors at the points.
315        normals (list): Normal vectors at the points.
316        n_points (int): Number of points on the curve.
317    """
318
319    def __init__(
320        self, control_points: Sequence[Point], n_points: int = 10, **kwargs
321    ) -> None:
322        """Initializes Bezier points.
323
324        Args:
325            control_points (Sequence[Point]): Control points of the Bezier curve.
326            n_points (int, optional): Number of points on the curve. Defaults to 10.
327            **kwargs: Additional keyword arguments.
328
329        Raises:
330            ValueError: If the number of control points is not 3 or 4.
331        """
332        if len(control_points) not in (4, 3):
333            raise ValueError("Invalid number of control points.")
334
335        param_points, vertices, tangents, normals = equidistant_points(
336            *control_points, n_points
337        )
338        super().__init__(vertices, subtype=Types.BEZIER_POINTS, **kwargs)
339        self.control_points = control_points
340        self.param_points = param_points
341        self.tangents = tangents
342        self.normals = normals
343        self.n_points = n_points
344
345    def offsets(self, offset: float, double: bool=False):
346        """Return the points on the offset curve.
347
348        Args:
349            offset (float): Offset distance.
350            double (bool, optional): If True, return double offset points. Defaults to False.
351
352        Returns:
353            list: Points on the offset curve.
354        """
355        offset_points1 = []
356        if double:
357            offset_points2 = []
358        for i, pnt in enumerate(self.vertices):
359            n_p = self.normals[i]
360            x2, y2 = pnt[0] + offset * n_p[0], pnt[1] + offset * n_p[1]
361            offset_points1.append((x2, y2))
362            if double:
363                _, p3 = mirror_point((x2, y2), pnt)
364                offset_points2.append(p3)
365
366        if double:
367            return offset_points1, offset_points2
368
369        return offset_points1

Points of a Bezier curve defined by the given control points.

These points are spaced evenly along the curve (unlike parametric points). Normal and tangent unit vectors are also available at these points.

Attributes:
  • control_points (Sequence[Point]): Control points of the Bezier curve.
  • param_points (list): Parametric points on the Bezier curve.
  • tangents (list): Tangent vectors at the points.
  • normals (list): Normal vectors at the points.
  • n_points (int): Number of points on the curve.
BezierPoints( control_points: Sequence[Sequence[float]], n_points: int = 10, **kwargs)
319    def __init__(
320        self, control_points: Sequence[Point], n_points: int = 10, **kwargs
321    ) -> None:
322        """Initializes Bezier points.
323
324        Args:
325            control_points (Sequence[Point]): Control points of the Bezier curve.
326            n_points (int, optional): Number of points on the curve. Defaults to 10.
327            **kwargs: Additional keyword arguments.
328
329        Raises:
330            ValueError: If the number of control points is not 3 or 4.
331        """
332        if len(control_points) not in (4, 3):
333            raise ValueError("Invalid number of control points.")
334
335        param_points, vertices, tangents, normals = equidistant_points(
336            *control_points, n_points
337        )
338        super().__init__(vertices, subtype=Types.BEZIER_POINTS, **kwargs)
339        self.control_points = control_points
340        self.param_points = param_points
341        self.tangents = tangents
342        self.normals = normals
343        self.n_points = n_points

Initializes Bezier points.

Arguments:
  • control_points (Sequence[Point]): Control points of the Bezier curve.
  • n_points (int, optional): Number of points on the curve. Defaults to 10.
  • **kwargs: Additional keyword arguments.
Raises:
  • ValueError: If the number of control points is not 3 or 4.
control_points
param_points
tangents
normals
n_points
def offsets(self, offset: float, double: bool = False):
345    def offsets(self, offset: float, double: bool=False):
346        """Return the points on the offset curve.
347
348        Args:
349            offset (float): Offset distance.
350            double (bool, optional): If True, return double offset points. Defaults to False.
351
352        Returns:
353            list: Points on the offset curve.
354        """
355        offset_points1 = []
356        if double:
357            offset_points2 = []
358        for i, pnt in enumerate(self.vertices):
359            n_p = self.normals[i]
360            x2, y2 = pnt[0] + offset * n_p[0], pnt[1] + offset * n_p[1]
361            offset_points1.append((x2, y2))
362            if double:
363                _, p3 = mirror_point((x2, y2), pnt)
364                offset_points2.append(p3)
365
366        if double:
367            return offset_points1, offset_points2
368
369        return offset_points1

Return the points on the offset curve.

Arguments:
  • offset (float): Offset distance.
  • double (bool, optional): If True, return double offset points. Defaults to False.
Returns:

list: Points on the offset curve.

M = array([[ 1, 0, 0, 0], [-3, 3, 0, 0], [ 3, -6, 3, 0], [-1, 3, -3, 1]])
def bezier_points( p0, p1: Sequence[float], p2: Sequence[float], p3: Sequence[float], n_points=10):
375def bezier_points(p0, p1: Point, p2: Point, p3: Point, n_points=10):
376    """Return the points on a cubic Bezier curve.
377
378    Args:
379        p0 (list): First control point.
380        p1 (list): Second control point.
381        p2 (list): Third control point.
382        p3 (list): Fourth control point.
383        n_points (int, optional): Number of points. Defaults to 10.
384
385    Returns:
386        list: Points on the cubic Bezier curve.
387
388    Raises:
389        ValueError: If n_points is less than 5.
390    """
391    if n_points < 5:
392        raise ValueError("n_points must be at least 5.")
393
394    n = n_points
395    f = np.ones(n)
396    t = np.linspace(0, 1, n)
397    t2 = t * t
398    t3 = t2 * t
399    T = np.column_stack((f, t, t2, t3))
400    TM = T @ M
401    P = array([p0, p1, p2, p3])
402
403    return TM @ P

Return the points on a cubic Bezier curve.

Arguments:
  • p0 (list): First control point.
  • p1 (list): Second control point.
  • p2 (list): Third control point.
  • p3 (list): Fourth control point.
  • n_points (int, optional): Number of points. Defaults to 10.
Returns:

list: Points on the cubic Bezier curve.

Raises:
  • ValueError: If n_points is less than 5.
MQ = array([[ 1, 0, 0], [-2, 2, 0], [ 1, -2, 1]])
def q_bezier_points( p0: Sequence[float], p1: Sequence[float], p2: Sequence[float], n_points: int):
409def q_bezier_points(p0: Point, p1: Point, p2: Point, n_points: int):
410    """Return the points on a quadratic Bezier curve.
411
412    Args:
413        p0 (list): First control point.
414        p1 (list): Second control point.
415        p2 (list): Third control point.
416        n_points (int): Number of points.
417
418    Returns:
419        list: Points on the quadratic Bezier curve.
420
421    Raises:
422        ValueError: If n_points is less than 5.
423    """
424    if n_points < 5:
425        raise ValueError("n_points must be at least 5.")
426
427    n = n_points
428    f = np.ones(n)
429    t = np.linspace(0, 1, n)
430    t2 = t * t
431    T = np.column_stack((f, t, t2))
432    TMQ = T @ MQ
433    P = array([p0, p1, p2])
434
435    return TMQ @ P

Return the points on a quadratic Bezier curve.

Arguments:
  • p0 (list): First control point.
  • p1 (list): Second control point.
  • p2 (list): Third control point.
  • n_points (int): Number of points.
Returns:

list: Points on the quadratic Bezier curve.

Raises:
  • ValueError: If n_points is less than 5.
def split_bezier( p0: Sequence[float], p1: Sequence[float], p2: Sequence[float], p3: Sequence[float], z: float, n_points=10):
438def split_bezier(p0: Point, p1: Point, p2: Point, p3: Point, z:float, n_points=10):
439    """Split a cubic Bezier curve at t=z.
440
441    Args:
442        p0 (list): First control point.
443        p1 (list): Second control point.
444        p2 (list): Third control point.
445        p3 (list): Fourth control point.
446        z (float): Parameter z, where 0 <= z <= 1.
447        n_points (int, optional): Number of points. Defaults to 10.
448
449    Returns:
450        tuple: Two Bezier curves split at t=z.
451    """
452    p0 = array(p0)
453    p1 = array(p1)
454    p2 = array(p2)
455    p3 = array(p3)
456    bezier1 = [
457        [p0],
458        [z * p1 - (z - 1) * p0],
459        [z**2 * p2 - 2 * z * (z - 1) * p1 + (z - 1) ** 2 * p0],
460        [
461            z**3 * p3
462            - 3 * z**2 * (z - 1) * p2
463            + 3 * z * (z - 1) ** 2 * p1
464            - (z - 1) ** 3 * p0
465        ],
466    ]
467
468    bezier2 = [
469        [z**3 * p0],
470        [3 * z**2 * (z - 1) * p1 - 3 * z * (z - 1) ** 2 * p0],
471        [3 * z * (z - 1) * p2 - 3 * (z - 1) ** 2 * p1],
472        [z * p3 - (z - 1) * p2],
473    ]
474
475    return Bezier(bezier1, n_points=n_points), Bezier(bezier2, n_points=n_points)

Split a cubic Bezier curve at t=z.

Arguments:
  • p0 (list): First control point.
  • p1 (list): Second control point.
  • p2 (list): Third control point.
  • p3 (list): Fourth control point.
  • z (float): Parameter z, where 0 <= z <= 1.
  • n_points (int, optional): Number of points. Defaults to 10.
Returns:

tuple: Two Bezier curves split at t=z.

def split_q_bezier( p0: Sequence[float], p1: Sequence[float], p2: Sequence[float], z: float, n_points=10):
478def split_q_bezier(p0: Point, p1: Point, p2: Point, z:float, n_points=10):
479    """Split a quadratic Bezier curve at t=z.
480
481    Args:
482        p0 (list): First control point.
483        p1 (list): Second control point.
484        p2 (list): Third control point.
485        z (float): Parameter z, where 0 <= z <= 1.
486        n_points (int, optional): Number of points. Defaults to 10.
487
488    Returns:
489        tuple: Two Bezier curves split at t=z.
490    """
491    p0 = array(p0)
492    p1 = array(p1)
493    p2 = array(p2)
494    bezier1 = [
495        [p0],
496        [z * p1 - (z - 1) * p0],
497        [z**2 * p2 - 2 * z * (z - 1) * p1 + (z - 1) ** 2 * p0],
498    ]
499
500    bezier2 = [
501        [z**2 * p0],
502        [2 * z * (z - 1) * p1 - (z - 1) ** 2 * p0],
503        [z * p2 - (z - 1) * p1],
504    ]
505
506    return Bezier(bezier1, n_points=n_points), Bezier(bezier2, n_points=n_points)

Split a quadratic Bezier curve at t=z.

Arguments:
  • p0 (list): First control point.
  • p1 (list): Second control point.
  • p2 (list): Third control point.
  • z (float): Parameter z, where 0 <= z <= 1.
  • n_points (int, optional): Number of points. Defaults to 10.
Returns:

tuple: Two Bezier curves split at t=z.

def mirror_point(cp: Sequence[float], vertex: Sequence[float]):
509def mirror_point(cp: Point, vertex: Point):
510    """Return the mirror of cp about vertex.
511
512    Args:
513        cp (list): Control point to be mirrored.
514        vertex (list): Vertex point.
515
516    Returns:
517        list: Mirrored control point.
518    """
519    length = distance(cp, vertex)
520    angle = line_angle(cp, vertex)
521    cp2 = line_by_point_angle_length(vertex, angle, length)
522    return cp2

Return the mirror of cp about vertex.

Arguments:
  • cp (list): Control point to be mirrored.
  • vertex (list): Vertex point.
Returns:

list: Mirrored control point.

def curve( v1: Sequence[float], c1: Sequence[float], c2: Sequence[float], v2: Sequence[float], *args, **kwargs):
525def curve(v1: Point, c1: Point, c2: Point, v2: Point, *args, **kwargs):
526    """Return a cubic Bezier curve/s.
527
528    Args:
529        v1 (list): First vertex.
530        c1 (list): First control point.
531        c2 (list): Second control point.
532        v2 (list): Second vertex.
533        *args: Additional control points and vertices.
534        **kwargs: Additional keyword arguments.
535
536    Returns:
537        list: List of cubic Bezier curves.
538
539    Raises:
540        ValueError: If the number of control points is invalid.
541    """
542    curves = [Bezier([v1, c1, c2, v2], **kwargs)]
543    last_vertex = v2
544    for arg in args:
545        if len(arg) == 2:
546            c3 = mirror_point(c2, v2)
547            v3 = arg[1]
548            c4 = arg[0]
549            curves.append(Bezier([last_vertex, c3, c4, v3], **kwargs))
550            last_vertex = v3
551        elif len(arg) == 3:
552            c3 = arg[0]
553            c4 = arg[1]
554            v3 = arg[2]
555            curves.append(Bezier([last_vertex, c3, c4, v3], **kwargs))
556            last_vertex = v3
557        else:
558            raise ValueError("Invalid number of control points.")
559
560    return curves

Return a cubic Bezier curve/s.

Arguments:
  • v1 (list): First vertex.
  • c1 (list): First control point.
  • c2 (list): Second control point.
  • v2 (list): Second vertex.
  • *args: Additional control points and vertices.
  • **kwargs: Additional keyword arguments.
Returns:

list: List of cubic Bezier curves.

Raises:
  • ValueError: If the number of control points is invalid.
def q_curve( v1: Sequence[float], c: Sequence[float], v2: Sequence[float], *args, **kwargs):
563def q_curve(v1: Point, c: Point, v2: Point, *args, **kwargs):
564    """Return a quadratic Bezier curve/s.
565
566    Args:
567        v1 (list): First vertex.
568        c (list): Control point.
569        v2 (list): Second vertex.
570        *args: Additional control points and vertices.
571        **kwargs: Additional keyword arguments.
572
573    Returns:
574        list: List of quadratic Bezier curves.
575
576    Raises:
577        ValueError: If the number of control points is invalid.
578    """
579    curves = [Bezier([v1, c, v2], **kwargs)]
580    last_vertex = v2
581    for arg in args:
582        if len(arg) == 1:
583            c3 = mirror_point(c, v2)
584            v3 = arg[0]
585            curves.append(Bezier([last_vertex, c3, v3], **kwargs))
586            last_vertex = v3
587        elif len(arg) == 2:
588            c3 = arg[0]
589            v3 = arg[1]
590            curves.append(Bezier([last_vertex, c3, v3], **kwargs))
591            last_vertex = v3
592        else:
593            raise ValueError("Invalid number of control points.")
594
595    return curves

Return a quadratic Bezier curve/s.

Arguments:
  • v1 (list): First vertex.
  • c (list): Control point.
  • v2 (list): Second vertex.
  • *args: Additional control points and vertices.
  • **kwargs: Additional keyword arguments.
Returns:

list: List of quadratic Bezier curves.

Raises:
  • ValueError: If the number of control points is invalid.
def get_quadratic_derivative(t: float, points: Sequence[Sequence[float]]):
598def get_quadratic_derivative(t: float, points: Sequence[Point]):
599    """Return the derivative of a quadratic Bezier curve at t.
600
601    Args:
602        t (float): Parameter t, where 0 <= t <= 1.
603        points (list): Control points of the Bezier curve.
604
605    Returns:
606        list: Derivative of the quadratic Bezier curve at t.
607    """
608    mt = 1 - t
609    d = [
610        2 * (points[1][0] - points[0][0]),
611        2 * (points[1][1] - points[0][1]),
612        2 * (points[2][0] - points[1][0]),
613        2 * (points[2][1] - points[1][1]),
614    ]
615
616    return [mt * d[0] + t * d[2], mt * d[1] + t * d[3]]

Return the derivative of a quadratic Bezier curve at t.

Arguments:
  • t (float): Parameter t, where 0 <= t <= 1.
  • points (list): Control points of the Bezier curve.
Returns:

list: Derivative of the quadratic Bezier curve at t.

def get_cubic_derivative(t: float, points: Sequence[Sequence[float]]):
619def get_cubic_derivative(t: float, points: Sequence[Point]):
620    """Return the derivative of a cubic Bezier curve at t.
621
622    Args:
623        t (float): Parameter t, where 0 <= t <= 1.
624        points (list): Control points of the Bezier curve.
625
626    Returns:
627        list: Derivative of the cubic Bezier curve at t.
628    """
629    mt = 1 - t
630    a = mt * mt
631    b = 2 * mt * t
632    c = t * t
633    d = [
634        3 * (points[1][0] - points[0][0]),
635        3 * (points[1][1] - points[0][1]),
636        3 * (points[2][0] - points[1][0]),
637        3 * (points[2][1] - points[1][1]),
638        3 * (points[3][0] - points[2][0]),
639        3 * (points[3][1] - points[2][1]),
640    ]
641
642    return [a * d[0] + b * d[2] + c * d[4], a * d[1] + b * d[3] + c * d[5]]

Return the derivative of a cubic Bezier curve at t.

Arguments:
  • t (float): Parameter t, where 0 <= t <= 1.
  • points (list): Control points of the Bezier curve.
Returns:

list: Derivative of the cubic Bezier curve at t.

def get_normal(d: Sequence[float]):
645def get_normal(d: Sequence[float]):
646    """Return the normal of a given line.
647
648    Args:
649        d (list): Derivative of the line.
650
651    Returns:
652        list: Normal of the line.
653    """
654    q = np.sqrt(d[0] * d[0] + d[1] * d[1])
655    return [-d[1] / q, d[0] / q]

Return the normal of a given line.

Arguments:
  • d (list): Derivative of the line.
Returns:

list: Normal of the line.