simetri.graphics.path

Path module for graphics package.

  1"""Path module for graphics package."""
  2
  3from dataclasses import dataclass
  4from math import sin, cos, pi
  5from collections import deque
  6from typing_extensions import Self
  7
  8import numpy as np
  9
 10from .core import StyleMixin
 11from .batch import Batch
 12from .shape import Shape
 13from .common import Point, common_properties
 14from ..helpers.validation import validate_args
 15from .all_enums import PathOperation as PathOps
 16from .all_enums import Types
 17from ..canvas.style_map import shape_style_map, ShapeStyle, shape_args
 18from ..geometry.bezier import Bezier
 19from ..geometry.hobby import hobby_shape
 20from ..geometry.geometry import (
 21    homogenize,
 22    positive_angle,
 23    polar_to_cartesian,
 24    sine_points,
 25    close_points2
 26)
 27from ..geometry.ellipse import (
 28    ellipse_point,
 29    ellipse_tangent,
 30    elliptic_arc_points,
 31)
 32from ..geometry.geometry import extended_line, line_angle, line_by_point_angle_length
 33from .affine import translation_matrix, rotation_matrix
 34from ..settings.settings import defaults
 35
 36array = np.array
 37
 38
 39@dataclass
 40class Operation:
 41    """An operation for a Path object.
 42
 43    Attributes:
 44        subtype (Types): The subtype of the operation.
 45        data (tuple): The data associated with the operation.
 46        name (str): The name of the operation.
 47    """
 48
 49    subtype: Types
 50    data: tuple
 51    name: str = ""
 52
 53    def __post_init__(self):
 54        """Post-initialization to set the type and common properties."""
 55        self.type = Types.PATH_OPERATION
 56        common_properties(self, False)
 57
 58
 59class LinPath(Batch, StyleMixin):
 60    """LinerPath.
 61    A LinPath object is a container for various linear elements.
 62    Path objects can be transformed like other Shape and Batch objects.
 63    """
 64
 65    def __init__(self, start: Point = (0, 0), angle: float = pi/2, **kwargs):
 66        """Initialize a Path object.
 67
 68        Args:
 69            start (Point, optional): The starting point of the path. Defaults to (0, 0).
 70            angle (float, optional): The heading angle of the path. Defaults to pi/2.
 71            **kwargs: Additional keyword arguments. Common properties are line_width,
 72            line_color, stroke, etc.
 73        """
 74        if "style" in kwargs:
 75            self.__dict__["style"] = kwargs["style"]
 76            del kwargs["style"]
 77        else:
 78            self.__dict__["style"] = ShapeStyle()
 79        self.__dict__["_style_map"] = shape_style_map
 80        self._set_aliases()
 81        valid_args = shape_args
 82        validate_args(kwargs, valid_args)
 83        self.pos = start
 84        self.start = start
 85        self.angle = angle  # heading angle
 86        self.operations = []
 87        self.objects = []
 88        self.even_odd = True  # False is non-zero winding rule
 89        super().__init__(**kwargs)
 90        self.subtype = Types.LINPATH
 91        self.cur_shape = Shape([start])
 92        self.append(self.cur_shape)
 93        self.rc = self.r_coord  # alias for r_coord
 94        self.rp = self.r_polar  # alias for rel_polar
 95        self.handles = []
 96        self.stack = deque()
 97        for key, value in kwargs.items():
 98            setattr(self, key, value)
 99        common_properties(self)
100        self.closed = False
101
102    def __getattr__(self, name):
103        """Retrieve an attribute of the shape.
104
105        Args:
106            name (str): The attribute name to return.
107
108        Returns:
109            Any: The value of the attribute.
110
111        Raises:
112            AttributeError: If the attribute cannot be found.
113        """
114        try:
115            res = super().__getattr__(name)
116        except AttributeError:
117            res = self.__dict__[name]
118        return res
119
120    def __bool__(self):
121        """Return True if the path has operations.
122        Batch may have no elements yet still be True.
123
124        Returns:
125            bool: True if the path has operations.
126        """
127        return bool(self.operations)
128
129    def _create_object(self):
130        """Create an object using the last operation."""
131        PO = PathOps
132        op = self.operations[-1]
133        op_type = op.subtype
134        data = op.data
135        if op_type in [PO.MOVE_TO, PO.R_MOVE]:
136            self.cur_shape = Shape([data])
137            self.append(self.cur_shape)
138            self.objects.append(None)
139        elif op_type in [PO.LINE_TO, PO.R_LINE, PO.H_LINE, PO.V_LINE, PO.FORWARD]:
140            self.objects.append(Shape(data))
141            self.cur_shape.append(data[1])
142        elif op_type in [PO.SEGMENTS]:
143            self.objects.append(Shape(data[1]))
144            self.cur_shape.extend(data[1])
145        elif op_type in [PO.SINE, PO.BLEND_SINE]:
146            self.objects.append(Shape(data[0]))
147            self.cur_shape.extend(data[0])
148        elif op_type in [PO.CUBIC_TO, PO.QUAD_TO]:
149            n_points = defaults["n_bezier_points"]
150            curve = Bezier(data, n_points=n_points)
151            self.objects.append(curve)
152            self.cur_shape.extend(curve.vertices[1:])
153            if op_type == PO.CUBIC_TO:
154                self.handles.extend([(data[0], data[1]), (data[2], data[3])])
155            else:
156                self.handles.append((data[0], data[1]))
157                self.handles.append((data[1], data[2]))
158        elif op_type in [PO.HOBBY_TO]:
159            n_points = defaults['n_hobby_points']
160            curve = hobby_shape(data[1], n_points=n_points)
161            self.objects.append(Shape(curve.vertices))
162        elif op_type in [PO.ARC, PO.BLEND_ARC]:
163            self.objects.append(Shape(data[-1]))
164            self.cur_shape.extend(data[-1][1:])
165        elif op_type in [PO.CLOSE]:
166            self.cur_shape.closed = True
167            self.cur_shape = Shape([self.pos])
168            self.objects.append(None)
169            self.append(self.cur_shape)
170        else:
171            raise ValueError(f"Invalid operation type: {op_type}")
172
173    def copy(self) -> "LinPath":
174        """Return a copy of the path.
175
176        Returns:
177            LinPath: The copied path object.
178        """
179
180        new_path = LinPath(start=self.start)
181        new_path.pos = self.pos
182        new_path.angle = self.angle
183        new_path.operations = self.operations.copy()
184        new_path.objects = []
185        for obj in self.objects:
186            if obj is not None:
187                new_path.objects.append(obj.copy())
188        new_path.even_odd = self.even_odd
189        new_path.cur_shape = self.cur_shape.copy()
190        new_path.handles = self.handles.copy()
191        new_path.stack = deque(self.stack)
192        for attrib in shape_style_map:
193            setattr(new_path, attrib, getattr(self, attrib))
194
195        return new_path
196
197    def _add(self, pos, op, data, pnt2=None, **kwargs):
198        """Add an operation to the path.
199
200        Args:
201            pos (Point): The position of the operation.
202            op (PathOps): The operation type.
203            data (tuple): The data for the operation.
204            pnt2 (Point, optional): An optional second point for the operation. Defaults to None.
205            **kwargs: Additional keyword arguments.
206        """
207        self.operations.append(Operation(op, data))
208        if op in [PathOps.ARC, PathOps.BLEND_ARC, PathOps.SINE, PathOps.BLEND_SINE]:
209            self.angle = data[1]
210        else:
211            if pnt2 is not None:
212                self.angle = line_angle(pnt2, pos)
213            else:
214                self.angle = line_angle(self.pos, pos)
215        self._create_object()
216        if "name" in kwargs:
217            setattr(self, kwargs["name"], self.operations[-1])
218        list(pos)[:2]
219        self.pos = pos
220
221    def push(self):
222        """Push the current position onto the stack."""
223        self.stack.append((self.pos, self.angle))
224
225    def pop(self):
226        """Pop the last position from the stack."""
227        if self.stack:
228            self.pos, self.angle = self.stack.pop()
229
230    def r_coord(self, dx: float, dy: float) -> Point:
231        """Return the relative coordinates of a point in a
232        coordinate system with the path's midpoint and y-axis aligned
233        with the path.angle.
234
235        Args:
236            dx (float): The x offset.
237            dy (float): The y offset.
238
239        Returns:
240            tuple: The relative coordinates.
241        """
242        x, y = self.pos[:2]
243        theta = self.angle - pi / 2
244        x1 = dx * cos(theta) - dy * sin(theta) + x
245        y1 = dx * sin(theta) + dy * cos(theta) + y
246
247        return x1, y1
248
249    def r_polar(self, r: float, angle: float) -> Point:
250        """Return the relative coordinates of a point in a polar
251        coordinate system with the path's midpoint and 0 degree axis aligned
252        with the path.angle.
253
254        Args:
255            r (float): The radius.
256            angle (float): The angle in radians.
257
258        Returns:
259            tuple: The relative coordinates.
260        """
261        x, y = polar_to_cartesian(r, angle + self.angle - pi / 2)[:2]
262        x1, y1 = self.pos[:2]
263
264        return x1 + x, y1 + y
265
266    def line_to(self, point: Point, **kwargs) -> Self:
267        """Add a line to the path.
268
269        Args:
270            point (Point): The end point of the line.
271            **kwargs: Additional keyword arguments.
272
273        Returns:
274            Path: The path object.
275        """
276        self._add(point, PathOps.LINE_TO, (self.pos, point))
277
278        return self
279
280    def forward(self, length: float, **kwargs) -> Self:
281        """Extend the path by the given length.
282
283        Args:
284            length (float): The length to extend.
285            **kwargs: Additional keyword arguments.
286
287        Returns:
288            Path: The path object.
289
290        Raises:
291            ValueError: If the path angle is not set.
292        """
293        if self.angle is None:
294            raise ValueError("Path angle is not set.")
295        else:
296            x, y = line_by_point_angle_length(self.pos, self.angle, length)[1][:2]
297        self._add((x, y), PathOps.FORWARD, (self.pos, (x, y)))
298
299        return self
300
301    def move_to(self, point: Point, **kwargs) -> Self:
302        """Move the path to a new point.
303
304        Args:
305            point (Point): The new point.
306            **kwargs: Additional keyword arguments.
307
308        Returns:
309            Path: The path object.
310        """
311        self._add(point, PathOps.MOVE_TO, point)
312
313        return self
314
315    def r_line(self, dx: float, dy: float, **kwargs) -> Self:
316        """Add a relative line to the path.
317
318        Args:
319            dx (float): The x offset.
320            dy (float): The y offset.
321            **kwargs: Additional keyword arguments.
322
323        Returns:
324            Path: The path object.
325        """
326        point = self.pos[0] + dx, self.pos[1] + dy
327        self._add(point, PathOps.R_LINE, (self.pos, point))
328
329        return self
330
331    def r_move(self, dx: float = 0, dy: float = 0, **kwargs) -> Self:
332        """Move the path to a new relative point.
333
334        Args:
335            dx (float): The x offset.
336            dy (float): The y offset.
337            **kwargs: Additional keyword arguments.
338
339        Returns:
340            Path: The path object.
341        """
342        x, y = self.pos[:2]
343        point = (x + dx, y + dy)
344        self._add(point, PathOps.R_MOVE, point)
345        return self
346
347    def h_line(self, length: float, **kwargs) -> Self:
348        """Add a horizontal line to the path.
349
350        Args:
351            length (float): The length of the line.
352            **kwargs: Additional keyword arguments.
353
354        Returns:
355            Path: The path object.
356        """
357        x, y = self.pos[0] + length, self.pos[1]
358        self._add((x, y), PathOps.H_LINE, (self.pos, (x, y)))
359        return self
360
361    def v_line(self, length: float, **kwargs) -> Self:
362        """Add a vertical line to the path.
363
364        Args:
365            length (float): The length of the line.
366            **kwargs: Additional keyword arguments.
367
368        Returns:
369            Path: The path object.
370        """
371        x, y = self.pos[0], self.pos[1] + length
372        self._add((x, y), PathOps.V_LINE, (self.pos, (x, y)))
373        return self
374
375    def segments(self, points, **kwargs) -> Self:
376        """Add a series of line segments to the path.
377
378        Args:
379            points (list): The points of the segments.
380            **kwargs: Additional keyword arguments.
381
382        Returns:
383            Path: The path object.
384        """
385
386        self._add(points[-1], PathOps.SEGMENTS, (self.pos, points), pnt2=points[-2], **kwargs)
387        return self
388
389    def cubic_to(self, control1: Point, control2: Point, end: Point, *args, **kwargs) -> Self:
390        """Add a Bézier curve with two control points to the path. Multiple blended curves can be added
391        by providing additional arguments.
392
393        Args:
394            control1 (Point): The first control point.
395            control2 (Point): The second control point.
396            end (Point): The end point of the curve.
397            *args: Additional arguments for blended curves.
398            **kwargs: Additional keyword arguments.
399
400        Returns:
401            Path: The path object.
402        """
403        self._add(
404            end,
405            PathOps.CUBIC_TO,
406            (self.pos, control1, control2, end),
407            pnt2=control2,
408            **kwargs,
409        )
410        return self
411
412    def hobby_to(self, points, **kwargs) -> Self:
413        """Add a Hobby curve to the path.
414
415        Args:
416            points (list): The points of the Hobby curve.
417            **kwargs: Additional keyword arguments.
418
419        Returns:
420            Path: The path object.
421        """
422        self._add(points[-1], PathOps.HOBBY_TO, (self.pos, points))
423        return self
424
425
426    def quad_to(self, control: Point, end: Point, *args, **kwargs) -> Self:
427        """Add a quadratic Bézier curve to the path. Multiple blended curves can be added by providing
428        additional arguments.
429
430        Args:
431            control (Point): The control point.
432            end (Point): The end point of the curve.
433            *args: Additional arguments for blended curves.
434            **kwargs: Additional keyword arguments.
435
436        Returns:
437            Path: The path object.
438
439        Raises:
440            ValueError: If an argument does not have exactly two elements.
441        """
442        self._add(
443            end, PathOps.QUAD_TO, (self.pos[:2], control, end[:2]), pnt2=control, **kwargs
444        )
445        pos = end
446        for arg in args:
447            if len(arg) != 2:
448                raise ValueError("Invalid number of arguments for curve.")
449            if isinstance(arg[0], (int, float)):
450                # (length, end)
451                length = arg[0]
452                control = extended_line(length, control, pos)
453                end = arg[1]
454                self._add(end, PathOps.QUAD_TO, (pos, control, end), pnt2=control)
455                pos = end
456            elif isinstance(arg[0], (list, tuple)):
457                # (control, end)
458                control = arg[0]
459                end = arg[1]
460                self._add(end, PathOps.QUAD_TO, (pos, control, end), pnt2=control)
461                pos = end
462        return self
463
464    def blend_cubic(self, control1_length, control2: Point, end: Point, **kwargs) -> Self:
465        """Add a cubic Bézier curve to the path where the first control point is computed based on a length.
466
467        Args:
468            control1_length (float): The length to the first control point.
469            control2 (Point): The second control point.
470            end (Point): The end point of the curve.
471            **kwargs: Additional keyword arguments.
472
473        Returns:
474            Path: The path object.
475        """
476        c1 = line_by_point_angle_length(self.pos, self.angle, control1_length)[1]
477        self._add(
478            end,
479            PathOps.CUBIC_TO,
480            (self.pos, c1, control2, end),
481            pnt2=control2,
482            **kwargs,
483        )
484        return self
485
486    def blend_quad(self, control_length, end: Point, **kwargs) -> Self:
487        """Add a quadratic Bézier curve to the path where the control point is computed based on a length.
488
489        Args:
490            control_length (float): The length to the control point.
491            end (Point): The end point of the curve.
492            **kwargs: Additional keyword arguments.
493
494        Returns:
495            Path: The path object.
496        """
497        pos = list(self.pos[:2])
498        c1 = line_by_point_angle_length(pos, self.angle, control_length)[1]
499        self._add(end, PathOps.QUAD_TO, (pos, c1, end), pnt2=c1, **kwargs)
500        return self
501
502    def arc(
503        self,
504        radius_x: float,
505        radius_y: float,
506        start_angle: float,
507        span_angle: float,
508        rot_angle: float = 0,
509        n_points=None,
510        **kwargs,
511    ) -> Self:
512        """Add an arc to the path. The arc is defined by an ellipse (with rx as half-width and ry as half-height).
513        The sign of the span angle determines the drawing direction.
514
515        Args:
516            radius_x (float): The x radius of the arc.
517            radius_y (float): The y radius of the arc.
518            start_angle (float): The starting angle of the arc.
519            span_angle (float): The span angle of the arc.
520            rot_angle (float, optional): The rotation angle of the arc. Defaults to 0.
521            n_points (int, optional): The number of points to use for the arc. Defaults to None.
522            **kwargs: Additional keyword arguments.
523
524        Returns:
525            Path: The path object.
526        """
527        rx = radius_x
528        ry = radius_y
529        start_angle = positive_angle(start_angle)
530        clockwise = span_angle < 0
531        if n_points is None:
532            n_points = defaults["n_arc_points"]
533        points = elliptic_arc_points((0, 0), rx, ry, start_angle, span_angle, n_points)
534        start = points[0]
535        end = points[-1]
536        # Translate the start to the current position and rotate by the rotation angle.
537        dx = self.pos[0] - start[0]
538        dy = self.pos[1] - start[1]
539        rotocenter = start
540        if rot_angle != 0:
541            points = (
542                homogenize(points)
543                @ rotation_matrix(rot_angle, rotocenter)
544                @ translation_matrix(dx, dy)
545            )
546        else:
547            points = homogenize(points) @ translation_matrix(dx, dy)
548        tangent_angle = ellipse_tangent(rx, ry, *end) + rot_angle
549        if clockwise:
550            tangent_angle += pi
551        pos = points[-1]
552        self._add(
553            pos,
554            PathOps.ARC,
555            (pos, tangent_angle, rx, ry, start_angle, span_angle, rot_angle, points),
556        )
557        return self
558
559    def blend_arc(
560        self,
561        radius_x: float,
562        radius_y: float,
563        start_angle: float,
564        span_angle: float,
565        sharp=False,
566        n_points=None,
567        **kwargs,
568    ) -> Self:
569        """Add a blended elliptic arc to the path.
570
571        Args:
572            radius_x (float): The x radius of the arc.
573            radius_y (float): The y radius of the arc.
574            start_angle (float): The starting angle of the arc.
575            span_angle (float): The span angle of the arc.
576            sharp (bool, optional): Whether the arc is sharp. Defaults to False.
577            n_points (int, optional): The number of points to use for the arc. Defaults to None.
578            **kwargs: Additional keyword arguments.
579
580        Returns:
581            Path: The path object.
582        """
583        rx = radius_x
584        ry = radius_y
585        start_angle = positive_angle(start_angle)
586        clockwise = span_angle < 0
587        if n_points is None:
588            n_points = defaults["n_arc_points"]
589        points = elliptic_arc_points((0, 0), rx, ry, start_angle, span_angle, n_points)
590        start = points[0]
591        end = points[-1]
592        # Translate the start to the current position and rotate by the computed rotation angle.
593        dx = self.pos[0] - start[0]
594        dy = self.pos[1] - start[1]
595        rotocenter = start
596        tangent = ellipse_tangent(rx, ry, *start)
597        rot_angle = self.angle - tangent
598        if clockwise:
599            rot_angle += pi
600        if sharp:
601            rot_angle += pi
602        points = (
603            homogenize(points)
604            @ rotation_matrix(rot_angle, rotocenter)
605            @ translation_matrix(dx, dy)
606        )
607        tangent_angle = ellipse_tangent(rx, ry, *end) + rot_angle
608        if clockwise:
609            tangent_angle += pi
610        pos = points[-1][:2]
611        self._add(
612            pos,
613            PathOps.ARC,
614            (pos, tangent_angle, rx, ry, start_angle, span_angle, rot_angle, points),
615        )
616        return self
617
618    def sine(
619        self,
620        period: float = 40,
621        amplitude: float = 20,
622        duration: float = 40,
623        phase_angle: float = 0,
624        rot_angle: float = 0,
625        damping: float = 0,
626        n_points: int = 100,
627        **kwargs,
628    ) -> Self:
629        """Add a sine wave to the path.
630
631        Args:
632            period (float, optional): _description_. Defaults to 40.
633            amplitude (float, optional): _description_. Defaults to 20.
634            duration (float, optional): _description_. Defaults to 1.
635            n_points (int, optional): _description_. Defaults to 100.
636            phase_angle (float, optional): _description_. Defaults to 0.
637            damping (float, optional): _description_. Defaults to 0.
638            rot_angle (float, optional): _description_. Defaults to 0.
639
640        Returns:
641            Path: The path object.
642        """
643
644        points = sine_points(
645            period, amplitude, duration, n_points, phase_angle, damping
646        )
647        if rot_angle != 0:
648            points = homogenize(points) @ rotation_matrix(rot_angle, points[0])
649        points = homogenize(points) @ translation_matrix(*self.pos[:2])
650        angle = line_angle(points[-2], points[-1])
651        self._add(points[-1], PathOps.SINE, (points, angle))
652        return self
653
654    def blend_sine(
655        self,
656        period: float = 40,
657        amplitude: float = 20,
658        duration: float = 40,
659        phase_angle: float = 0,
660        damping: float = 0,
661        n_points: int = 100,
662        **kwargs,
663    ) -> Self:
664        """Add a blended sine wave to the path.
665
666        Args:
667            amplitude (float): The amplitude of the wave.
668            frequency (float): The frequency of the wave.
669            length (float): The length of the wave.
670            **kwargs: Additional keyword arguments.
671
672        Returns:
673            Path: The path object.
674        """
675
676        points = sine_points(
677            period, amplitude, duration, n_points, phase_angle, damping
678        )
679        start_angle = line_angle(points[0], points[1])
680        rot_angle = self.angle - start_angle
681        points = homogenize(points) @ rotation_matrix(rot_angle, points[0])
682        points = homogenize(points) @ translation_matrix(*self.pos[:2])
683        angle = line_angle(points[-2], points[-1])
684        self._add(points[-1], PathOps.SINE, (points, angle))
685        return self
686
687    def close(self, **kwargs) -> Self:
688        """Close the path.
689
690        Args:
691            **kwargs: Additional keyword arguments.
692
693        Returns:
694            Path: The path object.
695        """
696        self._add(self.pos, PathOps.CLOSE, None, **kwargs)
697        return self
698
699    @property
700    def vertices(self):
701        """Return the vertices of the path.
702
703        Returns:
704            list: The vertices of the path.
705        """
706        vertices = []
707        last_vert = None
708        dist_tol2 = defaults["dist_tol"] ** 2
709        for obj in self.objects:
710            if obj is not None and obj.vertices:
711                obj_verts = obj.vertices
712                if last_vert:
713                    if close_points2(last_vert,  obj_verts[0], dist_tol2):
714                        vertices.extend(obj_verts[1:])
715                    else:
716                        vertices.extend(obj_verts)
717                else:
718                    vertices.extend(obj_verts)
719                last_vert = obj_verts[-1]
720
721        return vertices
722
723    def set_style(self, name, value, **kwargs) -> Self:
724        """Set the style of the path.
725
726        Args:
727            name (str): The name of the style.
728            value (Any): The value of the style.
729            **kwargs: Additional keyword arguments.
730
731        Returns:
732            Path: The path object.
733        """
734        self.operations.append((PathOps.STYLE, (name, value, kwargs)))
735        return self
736
737    def _update(self, xform_matrix: array, reps: int = 0) -> Batch:
738        """Used internally. Update the shape with a transformation matrix.
739
740        Args:
741            xform_matrix (array): The transformation matrix.
742            reps (int, optional): The number of repetitions, defaults to 0.
743
744        Returns:
745            Batch: The updated shape or a batch of shapes.
746        """
747        if reps == 0:
748            for obj in self.objects:
749                if obj is not None:
750                    obj._update(xform_matrix)
751            res = self
752        else:
753            paths = [self]
754            path = self
755            for _ in range(reps):
756                path = path.copy()
757                path._update(xform_matrix)
758                paths.append(path)
759            res = Batch(paths)
760
761        return res
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]])
@dataclass
class Operation:
40@dataclass
41class Operation:
42    """An operation for a Path object.
43
44    Attributes:
45        subtype (Types): The subtype of the operation.
46        data (tuple): The data associated with the operation.
47        name (str): The name of the operation.
48    """
49
50    subtype: Types
51    data: tuple
52    name: str = ""
53
54    def __post_init__(self):
55        """Post-initialization to set the type and common properties."""
56        self.type = Types.PATH_OPERATION
57        common_properties(self, False)

An operation for a Path object.

Attributes:
  • subtype (Types): The subtype of the operation.
  • data (tuple): The data associated with the operation.
  • name (str): The name of the operation.
Operation( subtype: simetri.graphics.all_enums.Types, data: tuple, name: str = '')
data: tuple
name: str = ''
 60class LinPath(Batch, StyleMixin):
 61    """LinerPath.
 62    A LinPath object is a container for various linear elements.
 63    Path objects can be transformed like other Shape and Batch objects.
 64    """
 65
 66    def __init__(self, start: Point = (0, 0), angle: float = pi/2, **kwargs):
 67        """Initialize a Path object.
 68
 69        Args:
 70            start (Point, optional): The starting point of the path. Defaults to (0, 0).
 71            angle (float, optional): The heading angle of the path. Defaults to pi/2.
 72            **kwargs: Additional keyword arguments. Common properties are line_width,
 73            line_color, stroke, etc.
 74        """
 75        if "style" in kwargs:
 76            self.__dict__["style"] = kwargs["style"]
 77            del kwargs["style"]
 78        else:
 79            self.__dict__["style"] = ShapeStyle()
 80        self.__dict__["_style_map"] = shape_style_map
 81        self._set_aliases()
 82        valid_args = shape_args
 83        validate_args(kwargs, valid_args)
 84        self.pos = start
 85        self.start = start
 86        self.angle = angle  # heading angle
 87        self.operations = []
 88        self.objects = []
 89        self.even_odd = True  # False is non-zero winding rule
 90        super().__init__(**kwargs)
 91        self.subtype = Types.LINPATH
 92        self.cur_shape = Shape([start])
 93        self.append(self.cur_shape)
 94        self.rc = self.r_coord  # alias for r_coord
 95        self.rp = self.r_polar  # alias for rel_polar
 96        self.handles = []
 97        self.stack = deque()
 98        for key, value in kwargs.items():
 99            setattr(self, key, value)
100        common_properties(self)
101        self.closed = False
102
103    def __getattr__(self, name):
104        """Retrieve an attribute of the shape.
105
106        Args:
107            name (str): The attribute name to return.
108
109        Returns:
110            Any: The value of the attribute.
111
112        Raises:
113            AttributeError: If the attribute cannot be found.
114        """
115        try:
116            res = super().__getattr__(name)
117        except AttributeError:
118            res = self.__dict__[name]
119        return res
120
121    def __bool__(self):
122        """Return True if the path has operations.
123        Batch may have no elements yet still be True.
124
125        Returns:
126            bool: True if the path has operations.
127        """
128        return bool(self.operations)
129
130    def _create_object(self):
131        """Create an object using the last operation."""
132        PO = PathOps
133        op = self.operations[-1]
134        op_type = op.subtype
135        data = op.data
136        if op_type in [PO.MOVE_TO, PO.R_MOVE]:
137            self.cur_shape = Shape([data])
138            self.append(self.cur_shape)
139            self.objects.append(None)
140        elif op_type in [PO.LINE_TO, PO.R_LINE, PO.H_LINE, PO.V_LINE, PO.FORWARD]:
141            self.objects.append(Shape(data))
142            self.cur_shape.append(data[1])
143        elif op_type in [PO.SEGMENTS]:
144            self.objects.append(Shape(data[1]))
145            self.cur_shape.extend(data[1])
146        elif op_type in [PO.SINE, PO.BLEND_SINE]:
147            self.objects.append(Shape(data[0]))
148            self.cur_shape.extend(data[0])
149        elif op_type in [PO.CUBIC_TO, PO.QUAD_TO]:
150            n_points = defaults["n_bezier_points"]
151            curve = Bezier(data, n_points=n_points)
152            self.objects.append(curve)
153            self.cur_shape.extend(curve.vertices[1:])
154            if op_type == PO.CUBIC_TO:
155                self.handles.extend([(data[0], data[1]), (data[2], data[3])])
156            else:
157                self.handles.append((data[0], data[1]))
158                self.handles.append((data[1], data[2]))
159        elif op_type in [PO.HOBBY_TO]:
160            n_points = defaults['n_hobby_points']
161            curve = hobby_shape(data[1], n_points=n_points)
162            self.objects.append(Shape(curve.vertices))
163        elif op_type in [PO.ARC, PO.BLEND_ARC]:
164            self.objects.append(Shape(data[-1]))
165            self.cur_shape.extend(data[-1][1:])
166        elif op_type in [PO.CLOSE]:
167            self.cur_shape.closed = True
168            self.cur_shape = Shape([self.pos])
169            self.objects.append(None)
170            self.append(self.cur_shape)
171        else:
172            raise ValueError(f"Invalid operation type: {op_type}")
173
174    def copy(self) -> "LinPath":
175        """Return a copy of the path.
176
177        Returns:
178            LinPath: The copied path object.
179        """
180
181        new_path = LinPath(start=self.start)
182        new_path.pos = self.pos
183        new_path.angle = self.angle
184        new_path.operations = self.operations.copy()
185        new_path.objects = []
186        for obj in self.objects:
187            if obj is not None:
188                new_path.objects.append(obj.copy())
189        new_path.even_odd = self.even_odd
190        new_path.cur_shape = self.cur_shape.copy()
191        new_path.handles = self.handles.copy()
192        new_path.stack = deque(self.stack)
193        for attrib in shape_style_map:
194            setattr(new_path, attrib, getattr(self, attrib))
195
196        return new_path
197
198    def _add(self, pos, op, data, pnt2=None, **kwargs):
199        """Add an operation to the path.
200
201        Args:
202            pos (Point): The position of the operation.
203            op (PathOps): The operation type.
204            data (tuple): The data for the operation.
205            pnt2 (Point, optional): An optional second point for the operation. Defaults to None.
206            **kwargs: Additional keyword arguments.
207        """
208        self.operations.append(Operation(op, data))
209        if op in [PathOps.ARC, PathOps.BLEND_ARC, PathOps.SINE, PathOps.BLEND_SINE]:
210            self.angle = data[1]
211        else:
212            if pnt2 is not None:
213                self.angle = line_angle(pnt2, pos)
214            else:
215                self.angle = line_angle(self.pos, pos)
216        self._create_object()
217        if "name" in kwargs:
218            setattr(self, kwargs["name"], self.operations[-1])
219        list(pos)[:2]
220        self.pos = pos
221
222    def push(self):
223        """Push the current position onto the stack."""
224        self.stack.append((self.pos, self.angle))
225
226    def pop(self):
227        """Pop the last position from the stack."""
228        if self.stack:
229            self.pos, self.angle = self.stack.pop()
230
231    def r_coord(self, dx: float, dy: float) -> Point:
232        """Return the relative coordinates of a point in a
233        coordinate system with the path's midpoint and y-axis aligned
234        with the path.angle.
235
236        Args:
237            dx (float): The x offset.
238            dy (float): The y offset.
239
240        Returns:
241            tuple: The relative coordinates.
242        """
243        x, y = self.pos[:2]
244        theta = self.angle - pi / 2
245        x1 = dx * cos(theta) - dy * sin(theta) + x
246        y1 = dx * sin(theta) + dy * cos(theta) + y
247
248        return x1, y1
249
250    def r_polar(self, r: float, angle: float) -> Point:
251        """Return the relative coordinates of a point in a polar
252        coordinate system with the path's midpoint and 0 degree axis aligned
253        with the path.angle.
254
255        Args:
256            r (float): The radius.
257            angle (float): The angle in radians.
258
259        Returns:
260            tuple: The relative coordinates.
261        """
262        x, y = polar_to_cartesian(r, angle + self.angle - pi / 2)[:2]
263        x1, y1 = self.pos[:2]
264
265        return x1 + x, y1 + y
266
267    def line_to(self, point: Point, **kwargs) -> Self:
268        """Add a line to the path.
269
270        Args:
271            point (Point): The end point of the line.
272            **kwargs: Additional keyword arguments.
273
274        Returns:
275            Path: The path object.
276        """
277        self._add(point, PathOps.LINE_TO, (self.pos, point))
278
279        return self
280
281    def forward(self, length: float, **kwargs) -> Self:
282        """Extend the path by the given length.
283
284        Args:
285            length (float): The length to extend.
286            **kwargs: Additional keyword arguments.
287
288        Returns:
289            Path: The path object.
290
291        Raises:
292            ValueError: If the path angle is not set.
293        """
294        if self.angle is None:
295            raise ValueError("Path angle is not set.")
296        else:
297            x, y = line_by_point_angle_length(self.pos, self.angle, length)[1][:2]
298        self._add((x, y), PathOps.FORWARD, (self.pos, (x, y)))
299
300        return self
301
302    def move_to(self, point: Point, **kwargs) -> Self:
303        """Move the path to a new point.
304
305        Args:
306            point (Point): The new point.
307            **kwargs: Additional keyword arguments.
308
309        Returns:
310            Path: The path object.
311        """
312        self._add(point, PathOps.MOVE_TO, point)
313
314        return self
315
316    def r_line(self, dx: float, dy: float, **kwargs) -> Self:
317        """Add a relative line to the path.
318
319        Args:
320            dx (float): The x offset.
321            dy (float): The y offset.
322            **kwargs: Additional keyword arguments.
323
324        Returns:
325            Path: The path object.
326        """
327        point = self.pos[0] + dx, self.pos[1] + dy
328        self._add(point, PathOps.R_LINE, (self.pos, point))
329
330        return self
331
332    def r_move(self, dx: float = 0, dy: float = 0, **kwargs) -> Self:
333        """Move the path to a new relative point.
334
335        Args:
336            dx (float): The x offset.
337            dy (float): The y offset.
338            **kwargs: Additional keyword arguments.
339
340        Returns:
341            Path: The path object.
342        """
343        x, y = self.pos[:2]
344        point = (x + dx, y + dy)
345        self._add(point, PathOps.R_MOVE, point)
346        return self
347
348    def h_line(self, length: float, **kwargs) -> Self:
349        """Add a horizontal line to the path.
350
351        Args:
352            length (float): The length of the line.
353            **kwargs: Additional keyword arguments.
354
355        Returns:
356            Path: The path object.
357        """
358        x, y = self.pos[0] + length, self.pos[1]
359        self._add((x, y), PathOps.H_LINE, (self.pos, (x, y)))
360        return self
361
362    def v_line(self, length: float, **kwargs) -> Self:
363        """Add a vertical line to the path.
364
365        Args:
366            length (float): The length of the line.
367            **kwargs: Additional keyword arguments.
368
369        Returns:
370            Path: The path object.
371        """
372        x, y = self.pos[0], self.pos[1] + length
373        self._add((x, y), PathOps.V_LINE, (self.pos, (x, y)))
374        return self
375
376    def segments(self, points, **kwargs) -> Self:
377        """Add a series of line segments to the path.
378
379        Args:
380            points (list): The points of the segments.
381            **kwargs: Additional keyword arguments.
382
383        Returns:
384            Path: The path object.
385        """
386
387        self._add(points[-1], PathOps.SEGMENTS, (self.pos, points), pnt2=points[-2], **kwargs)
388        return self
389
390    def cubic_to(self, control1: Point, control2: Point, end: Point, *args, **kwargs) -> Self:
391        """Add a Bézier curve with two control points to the path. Multiple blended curves can be added
392        by providing additional arguments.
393
394        Args:
395            control1 (Point): The first control point.
396            control2 (Point): The second control point.
397            end (Point): The end point of the curve.
398            *args: Additional arguments for blended curves.
399            **kwargs: Additional keyword arguments.
400
401        Returns:
402            Path: The path object.
403        """
404        self._add(
405            end,
406            PathOps.CUBIC_TO,
407            (self.pos, control1, control2, end),
408            pnt2=control2,
409            **kwargs,
410        )
411        return self
412
413    def hobby_to(self, points, **kwargs) -> Self:
414        """Add a Hobby curve to the path.
415
416        Args:
417            points (list): The points of the Hobby curve.
418            **kwargs: Additional keyword arguments.
419
420        Returns:
421            Path: The path object.
422        """
423        self._add(points[-1], PathOps.HOBBY_TO, (self.pos, points))
424        return self
425
426
427    def quad_to(self, control: Point, end: Point, *args, **kwargs) -> Self:
428        """Add a quadratic Bézier curve to the path. Multiple blended curves can be added by providing
429        additional arguments.
430
431        Args:
432            control (Point): The control point.
433            end (Point): The end point of the curve.
434            *args: Additional arguments for blended curves.
435            **kwargs: Additional keyword arguments.
436
437        Returns:
438            Path: The path object.
439
440        Raises:
441            ValueError: If an argument does not have exactly two elements.
442        """
443        self._add(
444            end, PathOps.QUAD_TO, (self.pos[:2], control, end[:2]), pnt2=control, **kwargs
445        )
446        pos = end
447        for arg in args:
448            if len(arg) != 2:
449                raise ValueError("Invalid number of arguments for curve.")
450            if isinstance(arg[0], (int, float)):
451                # (length, end)
452                length = arg[0]
453                control = extended_line(length, control, pos)
454                end = arg[1]
455                self._add(end, PathOps.QUAD_TO, (pos, control, end), pnt2=control)
456                pos = end
457            elif isinstance(arg[0], (list, tuple)):
458                # (control, end)
459                control = arg[0]
460                end = arg[1]
461                self._add(end, PathOps.QUAD_TO, (pos, control, end), pnt2=control)
462                pos = end
463        return self
464
465    def blend_cubic(self, control1_length, control2: Point, end: Point, **kwargs) -> Self:
466        """Add a cubic Bézier curve to the path where the first control point is computed based on a length.
467
468        Args:
469            control1_length (float): The length to the first control point.
470            control2 (Point): The second control point.
471            end (Point): The end point of the curve.
472            **kwargs: Additional keyword arguments.
473
474        Returns:
475            Path: The path object.
476        """
477        c1 = line_by_point_angle_length(self.pos, self.angle, control1_length)[1]
478        self._add(
479            end,
480            PathOps.CUBIC_TO,
481            (self.pos, c1, control2, end),
482            pnt2=control2,
483            **kwargs,
484        )
485        return self
486
487    def blend_quad(self, control_length, end: Point, **kwargs) -> Self:
488        """Add a quadratic Bézier curve to the path where the control point is computed based on a length.
489
490        Args:
491            control_length (float): The length to the control point.
492            end (Point): The end point of the curve.
493            **kwargs: Additional keyword arguments.
494
495        Returns:
496            Path: The path object.
497        """
498        pos = list(self.pos[:2])
499        c1 = line_by_point_angle_length(pos, self.angle, control_length)[1]
500        self._add(end, PathOps.QUAD_TO, (pos, c1, end), pnt2=c1, **kwargs)
501        return self
502
503    def arc(
504        self,
505        radius_x: float,
506        radius_y: float,
507        start_angle: float,
508        span_angle: float,
509        rot_angle: float = 0,
510        n_points=None,
511        **kwargs,
512    ) -> Self:
513        """Add an arc to the path. The arc is defined by an ellipse (with rx as half-width and ry as half-height).
514        The sign of the span angle determines the drawing direction.
515
516        Args:
517            radius_x (float): The x radius of the arc.
518            radius_y (float): The y radius of the arc.
519            start_angle (float): The starting angle of the arc.
520            span_angle (float): The span angle of the arc.
521            rot_angle (float, optional): The rotation angle of the arc. Defaults to 0.
522            n_points (int, optional): The number of points to use for the arc. Defaults to None.
523            **kwargs: Additional keyword arguments.
524
525        Returns:
526            Path: The path object.
527        """
528        rx = radius_x
529        ry = radius_y
530        start_angle = positive_angle(start_angle)
531        clockwise = span_angle < 0
532        if n_points is None:
533            n_points = defaults["n_arc_points"]
534        points = elliptic_arc_points((0, 0), rx, ry, start_angle, span_angle, n_points)
535        start = points[0]
536        end = points[-1]
537        # Translate the start to the current position and rotate by the rotation angle.
538        dx = self.pos[0] - start[0]
539        dy = self.pos[1] - start[1]
540        rotocenter = start
541        if rot_angle != 0:
542            points = (
543                homogenize(points)
544                @ rotation_matrix(rot_angle, rotocenter)
545                @ translation_matrix(dx, dy)
546            )
547        else:
548            points = homogenize(points) @ translation_matrix(dx, dy)
549        tangent_angle = ellipse_tangent(rx, ry, *end) + rot_angle
550        if clockwise:
551            tangent_angle += pi
552        pos = points[-1]
553        self._add(
554            pos,
555            PathOps.ARC,
556            (pos, tangent_angle, rx, ry, start_angle, span_angle, rot_angle, points),
557        )
558        return self
559
560    def blend_arc(
561        self,
562        radius_x: float,
563        radius_y: float,
564        start_angle: float,
565        span_angle: float,
566        sharp=False,
567        n_points=None,
568        **kwargs,
569    ) -> Self:
570        """Add a blended elliptic arc to the path.
571
572        Args:
573            radius_x (float): The x radius of the arc.
574            radius_y (float): The y radius of the arc.
575            start_angle (float): The starting angle of the arc.
576            span_angle (float): The span angle of the arc.
577            sharp (bool, optional): Whether the arc is sharp. Defaults to False.
578            n_points (int, optional): The number of points to use for the arc. Defaults to None.
579            **kwargs: Additional keyword arguments.
580
581        Returns:
582            Path: The path object.
583        """
584        rx = radius_x
585        ry = radius_y
586        start_angle = positive_angle(start_angle)
587        clockwise = span_angle < 0
588        if n_points is None:
589            n_points = defaults["n_arc_points"]
590        points = elliptic_arc_points((0, 0), rx, ry, start_angle, span_angle, n_points)
591        start = points[0]
592        end = points[-1]
593        # Translate the start to the current position and rotate by the computed rotation angle.
594        dx = self.pos[0] - start[0]
595        dy = self.pos[1] - start[1]
596        rotocenter = start
597        tangent = ellipse_tangent(rx, ry, *start)
598        rot_angle = self.angle - tangent
599        if clockwise:
600            rot_angle += pi
601        if sharp:
602            rot_angle += pi
603        points = (
604            homogenize(points)
605            @ rotation_matrix(rot_angle, rotocenter)
606            @ translation_matrix(dx, dy)
607        )
608        tangent_angle = ellipse_tangent(rx, ry, *end) + rot_angle
609        if clockwise:
610            tangent_angle += pi
611        pos = points[-1][:2]
612        self._add(
613            pos,
614            PathOps.ARC,
615            (pos, tangent_angle, rx, ry, start_angle, span_angle, rot_angle, points),
616        )
617        return self
618
619    def sine(
620        self,
621        period: float = 40,
622        amplitude: float = 20,
623        duration: float = 40,
624        phase_angle: float = 0,
625        rot_angle: float = 0,
626        damping: float = 0,
627        n_points: int = 100,
628        **kwargs,
629    ) -> Self:
630        """Add a sine wave to the path.
631
632        Args:
633            period (float, optional): _description_. Defaults to 40.
634            amplitude (float, optional): _description_. Defaults to 20.
635            duration (float, optional): _description_. Defaults to 1.
636            n_points (int, optional): _description_. Defaults to 100.
637            phase_angle (float, optional): _description_. Defaults to 0.
638            damping (float, optional): _description_. Defaults to 0.
639            rot_angle (float, optional): _description_. Defaults to 0.
640
641        Returns:
642            Path: The path object.
643        """
644
645        points = sine_points(
646            period, amplitude, duration, n_points, phase_angle, damping
647        )
648        if rot_angle != 0:
649            points = homogenize(points) @ rotation_matrix(rot_angle, points[0])
650        points = homogenize(points) @ translation_matrix(*self.pos[:2])
651        angle = line_angle(points[-2], points[-1])
652        self._add(points[-1], PathOps.SINE, (points, angle))
653        return self
654
655    def blend_sine(
656        self,
657        period: float = 40,
658        amplitude: float = 20,
659        duration: float = 40,
660        phase_angle: float = 0,
661        damping: float = 0,
662        n_points: int = 100,
663        **kwargs,
664    ) -> Self:
665        """Add a blended sine wave to the path.
666
667        Args:
668            amplitude (float): The amplitude of the wave.
669            frequency (float): The frequency of the wave.
670            length (float): The length of the wave.
671            **kwargs: Additional keyword arguments.
672
673        Returns:
674            Path: The path object.
675        """
676
677        points = sine_points(
678            period, amplitude, duration, n_points, phase_angle, damping
679        )
680        start_angle = line_angle(points[0], points[1])
681        rot_angle = self.angle - start_angle
682        points = homogenize(points) @ rotation_matrix(rot_angle, points[0])
683        points = homogenize(points) @ translation_matrix(*self.pos[:2])
684        angle = line_angle(points[-2], points[-1])
685        self._add(points[-1], PathOps.SINE, (points, angle))
686        return self
687
688    def close(self, **kwargs) -> Self:
689        """Close the path.
690
691        Args:
692            **kwargs: Additional keyword arguments.
693
694        Returns:
695            Path: The path object.
696        """
697        self._add(self.pos, PathOps.CLOSE, None, **kwargs)
698        return self
699
700    @property
701    def vertices(self):
702        """Return the vertices of the path.
703
704        Returns:
705            list: The vertices of the path.
706        """
707        vertices = []
708        last_vert = None
709        dist_tol2 = defaults["dist_tol"] ** 2
710        for obj in self.objects:
711            if obj is not None and obj.vertices:
712                obj_verts = obj.vertices
713                if last_vert:
714                    if close_points2(last_vert,  obj_verts[0], dist_tol2):
715                        vertices.extend(obj_verts[1:])
716                    else:
717                        vertices.extend(obj_verts)
718                else:
719                    vertices.extend(obj_verts)
720                last_vert = obj_verts[-1]
721
722        return vertices
723
724    def set_style(self, name, value, **kwargs) -> Self:
725        """Set the style of the path.
726
727        Args:
728            name (str): The name of the style.
729            value (Any): The value of the style.
730            **kwargs: Additional keyword arguments.
731
732        Returns:
733            Path: The path object.
734        """
735        self.operations.append((PathOps.STYLE, (name, value, kwargs)))
736        return self
737
738    def _update(self, xform_matrix: array, reps: int = 0) -> Batch:
739        """Used internally. Update the shape with a transformation matrix.
740
741        Args:
742            xform_matrix (array): The transformation matrix.
743            reps (int, optional): The number of repetitions, defaults to 0.
744
745        Returns:
746            Batch: The updated shape or a batch of shapes.
747        """
748        if reps == 0:
749            for obj in self.objects:
750                if obj is not None:
751                    obj._update(xform_matrix)
752            res = self
753        else:
754            paths = [self]
755            path = self
756            for _ in range(reps):
757                path = path.copy()
758                path._update(xform_matrix)
759                paths.append(path)
760            res = Batch(paths)
761
762        return res

LinerPath. A LinPath object is a container for various linear elements. Path objects can be transformed like other Shape and Batch objects.

LinPath( start: Sequence[float] = (0, 0), angle: float = 1.5707963267948966, **kwargs)
 66    def __init__(self, start: Point = (0, 0), angle: float = pi/2, **kwargs):
 67        """Initialize a Path object.
 68
 69        Args:
 70            start (Point, optional): The starting point of the path. Defaults to (0, 0).
 71            angle (float, optional): The heading angle of the path. Defaults to pi/2.
 72            **kwargs: Additional keyword arguments. Common properties are line_width,
 73            line_color, stroke, etc.
 74        """
 75        if "style" in kwargs:
 76            self.__dict__["style"] = kwargs["style"]
 77            del kwargs["style"]
 78        else:
 79            self.__dict__["style"] = ShapeStyle()
 80        self.__dict__["_style_map"] = shape_style_map
 81        self._set_aliases()
 82        valid_args = shape_args
 83        validate_args(kwargs, valid_args)
 84        self.pos = start
 85        self.start = start
 86        self.angle = angle  # heading angle
 87        self.operations = []
 88        self.objects = []
 89        self.even_odd = True  # False is non-zero winding rule
 90        super().__init__(**kwargs)
 91        self.subtype = Types.LINPATH
 92        self.cur_shape = Shape([start])
 93        self.append(self.cur_shape)
 94        self.rc = self.r_coord  # alias for r_coord
 95        self.rp = self.r_polar  # alias for rel_polar
 96        self.handles = []
 97        self.stack = deque()
 98        for key, value in kwargs.items():
 99            setattr(self, key, value)
100        common_properties(self)
101        self.closed = False

Initialize a Path object.

Arguments:
  • start (Point, optional): The starting point of the path. Defaults to (0, 0).
  • angle (float, optional): The heading angle of the path. Defaults to pi/2.
  • **kwargs: Additional keyword arguments. Common properties are line_width,
  • line_color, stroke, etc.
pos
start
angle
operations
objects
even_odd
subtype
cur_shape
rc
rp
handles
stack
closed
def copy(self) -> LinPath:
174    def copy(self) -> "LinPath":
175        """Return a copy of the path.
176
177        Returns:
178            LinPath: The copied path object.
179        """
180
181        new_path = LinPath(start=self.start)
182        new_path.pos = self.pos
183        new_path.angle = self.angle
184        new_path.operations = self.operations.copy()
185        new_path.objects = []
186        for obj in self.objects:
187            if obj is not None:
188                new_path.objects.append(obj.copy())
189        new_path.even_odd = self.even_odd
190        new_path.cur_shape = self.cur_shape.copy()
191        new_path.handles = self.handles.copy()
192        new_path.stack = deque(self.stack)
193        for attrib in shape_style_map:
194            setattr(new_path, attrib, getattr(self, attrib))
195
196        return new_path

Return a copy of the path.

Returns:

LinPath: The copied path object.

def push(self):
222    def push(self):
223        """Push the current position onto the stack."""
224        self.stack.append((self.pos, self.angle))

Push the current position onto the stack.

def pop(self):
226    def pop(self):
227        """Pop the last position from the stack."""
228        if self.stack:
229            self.pos, self.angle = self.stack.pop()

Pop the last position from the stack.

def r_coord(self, dx: float, dy: float) -> Sequence[float]:
231    def r_coord(self, dx: float, dy: float) -> Point:
232        """Return the relative coordinates of a point in a
233        coordinate system with the path's midpoint and y-axis aligned
234        with the path.angle.
235
236        Args:
237            dx (float): The x offset.
238            dy (float): The y offset.
239
240        Returns:
241            tuple: The relative coordinates.
242        """
243        x, y = self.pos[:2]
244        theta = self.angle - pi / 2
245        x1 = dx * cos(theta) - dy * sin(theta) + x
246        y1 = dx * sin(theta) + dy * cos(theta) + y
247
248        return x1, y1

Return the relative coordinates of a point in a coordinate system with the path's midpoint and y-axis aligned with the path.angle.

Arguments:
  • dx (float): The x offset.
  • dy (float): The y offset.
Returns:

tuple: The relative coordinates.

def r_polar(self, r: float, angle: float) -> Sequence[float]:
250    def r_polar(self, r: float, angle: float) -> Point:
251        """Return the relative coordinates of a point in a polar
252        coordinate system with the path's midpoint and 0 degree axis aligned
253        with the path.angle.
254
255        Args:
256            r (float): The radius.
257            angle (float): The angle in radians.
258
259        Returns:
260            tuple: The relative coordinates.
261        """
262        x, y = polar_to_cartesian(r, angle + self.angle - pi / 2)[:2]
263        x1, y1 = self.pos[:2]
264
265        return x1 + x, y1 + y

Return the relative coordinates of a point in a polar coordinate system with the path's midpoint and 0 degree axis aligned with the path.angle.

Arguments:
  • r (float): The radius.
  • angle (float): The angle in radians.
Returns:

tuple: The relative coordinates.

def line_to(self, point: Sequence[float], **kwargs) -> typing_extensions.Self:
267    def line_to(self, point: Point, **kwargs) -> Self:
268        """Add a line to the path.
269
270        Args:
271            point (Point): The end point of the line.
272            **kwargs: Additional keyword arguments.
273
274        Returns:
275            Path: The path object.
276        """
277        self._add(point, PathOps.LINE_TO, (self.pos, point))
278
279        return self

Add a line to the path.

Arguments:
  • point (Point): The end point of the line.
  • **kwargs: Additional keyword arguments.
Returns:

Path: The path object.

def forward(self, length: float, **kwargs) -> typing_extensions.Self:
281    def forward(self, length: float, **kwargs) -> Self:
282        """Extend the path by the given length.
283
284        Args:
285            length (float): The length to extend.
286            **kwargs: Additional keyword arguments.
287
288        Returns:
289            Path: The path object.
290
291        Raises:
292            ValueError: If the path angle is not set.
293        """
294        if self.angle is None:
295            raise ValueError("Path angle is not set.")
296        else:
297            x, y = line_by_point_angle_length(self.pos, self.angle, length)[1][:2]
298        self._add((x, y), PathOps.FORWARD, (self.pos, (x, y)))
299
300        return self

Extend the path by the given length.

Arguments:
  • length (float): The length to extend.
  • **kwargs: Additional keyword arguments.
Returns:

Path: The path object.

Raises:
  • ValueError: If the path angle is not set.
def move_to(self, point: Sequence[float], **kwargs) -> typing_extensions.Self:
302    def move_to(self, point: Point, **kwargs) -> Self:
303        """Move the path to a new point.
304
305        Args:
306            point (Point): The new point.
307            **kwargs: Additional keyword arguments.
308
309        Returns:
310            Path: The path object.
311        """
312        self._add(point, PathOps.MOVE_TO, point)
313
314        return self

Move the path to a new point.

Arguments:
  • point (Point): The new point.
  • **kwargs: Additional keyword arguments.
Returns:

Path: The path object.

def r_line(self, dx: float, dy: float, **kwargs) -> typing_extensions.Self:
316    def r_line(self, dx: float, dy: float, **kwargs) -> Self:
317        """Add a relative line to the path.
318
319        Args:
320            dx (float): The x offset.
321            dy (float): The y offset.
322            **kwargs: Additional keyword arguments.
323
324        Returns:
325            Path: The path object.
326        """
327        point = self.pos[0] + dx, self.pos[1] + dy
328        self._add(point, PathOps.R_LINE, (self.pos, point))
329
330        return self

Add a relative line to the path.

Arguments:
  • dx (float): The x offset.
  • dy (float): The y offset.
  • **kwargs: Additional keyword arguments.
Returns:

Path: The path object.

def r_move(self, dx: float = 0, dy: float = 0, **kwargs) -> typing_extensions.Self:
332    def r_move(self, dx: float = 0, dy: float = 0, **kwargs) -> Self:
333        """Move the path to a new relative point.
334
335        Args:
336            dx (float): The x offset.
337            dy (float): The y offset.
338            **kwargs: Additional keyword arguments.
339
340        Returns:
341            Path: The path object.
342        """
343        x, y = self.pos[:2]
344        point = (x + dx, y + dy)
345        self._add(point, PathOps.R_MOVE, point)
346        return self

Move the path to a new relative point.

Arguments:
  • dx (float): The x offset.
  • dy (float): The y offset.
  • **kwargs: Additional keyword arguments.
Returns:

Path: The path object.

def h_line(self, length: float, **kwargs) -> typing_extensions.Self:
348    def h_line(self, length: float, **kwargs) -> Self:
349        """Add a horizontal line to the path.
350
351        Args:
352            length (float): The length of the line.
353            **kwargs: Additional keyword arguments.
354
355        Returns:
356            Path: The path object.
357        """
358        x, y = self.pos[0] + length, self.pos[1]
359        self._add((x, y), PathOps.H_LINE, (self.pos, (x, y)))
360        return self

Add a horizontal line to the path.

Arguments:
  • length (float): The length of the line.
  • **kwargs: Additional keyword arguments.
Returns:

Path: The path object.

def v_line(self, length: float, **kwargs) -> typing_extensions.Self:
362    def v_line(self, length: float, **kwargs) -> Self:
363        """Add a vertical line to the path.
364
365        Args:
366            length (float): The length of the line.
367            **kwargs: Additional keyword arguments.
368
369        Returns:
370            Path: The path object.
371        """
372        x, y = self.pos[0], self.pos[1] + length
373        self._add((x, y), PathOps.V_LINE, (self.pos, (x, y)))
374        return self

Add a vertical line to the path.

Arguments:
  • length (float): The length of the line.
  • **kwargs: Additional keyword arguments.
Returns:

Path: The path object.

def segments(self, points, **kwargs) -> typing_extensions.Self:
376    def segments(self, points, **kwargs) -> Self:
377        """Add a series of line segments to the path.
378
379        Args:
380            points (list): The points of the segments.
381            **kwargs: Additional keyword arguments.
382
383        Returns:
384            Path: The path object.
385        """
386
387        self._add(points[-1], PathOps.SEGMENTS, (self.pos, points), pnt2=points[-2], **kwargs)
388        return self

Add a series of line segments to the path.

Arguments:
  • points (list): The points of the segments.
  • **kwargs: Additional keyword arguments.
Returns:

Path: The path object.

def cubic_to( self, control1: Sequence[float], control2: Sequence[float], end: Sequence[float], *args, **kwargs) -> typing_extensions.Self:
390    def cubic_to(self, control1: Point, control2: Point, end: Point, *args, **kwargs) -> Self:
391        """Add a Bézier curve with two control points to the path. Multiple blended curves can be added
392        by providing additional arguments.
393
394        Args:
395            control1 (Point): The first control point.
396            control2 (Point): The second control point.
397            end (Point): The end point of the curve.
398            *args: Additional arguments for blended curves.
399            **kwargs: Additional keyword arguments.
400
401        Returns:
402            Path: The path object.
403        """
404        self._add(
405            end,
406            PathOps.CUBIC_TO,
407            (self.pos, control1, control2, end),
408            pnt2=control2,
409            **kwargs,
410        )
411        return self

Add a Bézier curve with two control points to the path. Multiple blended curves can be added by providing additional arguments.

Arguments:
  • control1 (Point): The first control point.
  • control2 (Point): The second control point.
  • end (Point): The end point of the curve.
  • *args: Additional arguments for blended curves.
  • **kwargs: Additional keyword arguments.
Returns:

Path: The path object.

def hobby_to(self, points, **kwargs) -> typing_extensions.Self:
413    def hobby_to(self, points, **kwargs) -> Self:
414        """Add a Hobby curve to the path.
415
416        Args:
417            points (list): The points of the Hobby curve.
418            **kwargs: Additional keyword arguments.
419
420        Returns:
421            Path: The path object.
422        """
423        self._add(points[-1], PathOps.HOBBY_TO, (self.pos, points))
424        return self

Add a Hobby curve to the path.

Arguments:
  • points (list): The points of the Hobby curve.
  • **kwargs: Additional keyword arguments.
Returns:

Path: The path object.

def quad_to( self, control: Sequence[float], end: Sequence[float], *args, **kwargs) -> typing_extensions.Self:
427    def quad_to(self, control: Point, end: Point, *args, **kwargs) -> Self:
428        """Add a quadratic Bézier curve to the path. Multiple blended curves can be added by providing
429        additional arguments.
430
431        Args:
432            control (Point): The control point.
433            end (Point): The end point of the curve.
434            *args: Additional arguments for blended curves.
435            **kwargs: Additional keyword arguments.
436
437        Returns:
438            Path: The path object.
439
440        Raises:
441            ValueError: If an argument does not have exactly two elements.
442        """
443        self._add(
444            end, PathOps.QUAD_TO, (self.pos[:2], control, end[:2]), pnt2=control, **kwargs
445        )
446        pos = end
447        for arg in args:
448            if len(arg) != 2:
449                raise ValueError("Invalid number of arguments for curve.")
450            if isinstance(arg[0], (int, float)):
451                # (length, end)
452                length = arg[0]
453                control = extended_line(length, control, pos)
454                end = arg[1]
455                self._add(end, PathOps.QUAD_TO, (pos, control, end), pnt2=control)
456                pos = end
457            elif isinstance(arg[0], (list, tuple)):
458                # (control, end)
459                control = arg[0]
460                end = arg[1]
461                self._add(end, PathOps.QUAD_TO, (pos, control, end), pnt2=control)
462                pos = end
463        return self

Add a quadratic Bézier curve to the path. Multiple blended curves can be added by providing additional arguments.

Arguments:
  • control (Point): The control point.
  • end (Point): The end point of the curve.
  • *args: Additional arguments for blended curves.
  • **kwargs: Additional keyword arguments.
Returns:

Path: The path object.

Raises:
  • ValueError: If an argument does not have exactly two elements.
def blend_cubic( self, control1_length, control2: Sequence[float], end: Sequence[float], **kwargs) -> typing_extensions.Self:
465    def blend_cubic(self, control1_length, control2: Point, end: Point, **kwargs) -> Self:
466        """Add a cubic Bézier curve to the path where the first control point is computed based on a length.
467
468        Args:
469            control1_length (float): The length to the first control point.
470            control2 (Point): The second control point.
471            end (Point): The end point of the curve.
472            **kwargs: Additional keyword arguments.
473
474        Returns:
475            Path: The path object.
476        """
477        c1 = line_by_point_angle_length(self.pos, self.angle, control1_length)[1]
478        self._add(
479            end,
480            PathOps.CUBIC_TO,
481            (self.pos, c1, control2, end),
482            pnt2=control2,
483            **kwargs,
484        )
485        return self

Add a cubic Bézier curve to the path where the first control point is computed based on a length.

Arguments:
  • control1_length (float): The length to the first control point.
  • control2 (Point): The second control point.
  • end (Point): The end point of the curve.
  • **kwargs: Additional keyword arguments.
Returns:

Path: The path object.

def blend_quad( self, control_length, end: Sequence[float], **kwargs) -> typing_extensions.Self:
487    def blend_quad(self, control_length, end: Point, **kwargs) -> Self:
488        """Add a quadratic Bézier curve to the path where the control point is computed based on a length.
489
490        Args:
491            control_length (float): The length to the control point.
492            end (Point): The end point of the curve.
493            **kwargs: Additional keyword arguments.
494
495        Returns:
496            Path: The path object.
497        """
498        pos = list(self.pos[:2])
499        c1 = line_by_point_angle_length(pos, self.angle, control_length)[1]
500        self._add(end, PathOps.QUAD_TO, (pos, c1, end), pnt2=c1, **kwargs)
501        return self

Add a quadratic Bézier curve to the path where the control point is computed based on a length.

Arguments:
  • control_length (float): The length to the control point.
  • end (Point): The end point of the curve.
  • **kwargs: Additional keyword arguments.
Returns:

Path: The path object.

def arc( self, radius_x: float, radius_y: float, start_angle: float, span_angle: float, rot_angle: float = 0, n_points=None, **kwargs) -> typing_extensions.Self:
503    def arc(
504        self,
505        radius_x: float,
506        radius_y: float,
507        start_angle: float,
508        span_angle: float,
509        rot_angle: float = 0,
510        n_points=None,
511        **kwargs,
512    ) -> Self:
513        """Add an arc to the path. The arc is defined by an ellipse (with rx as half-width and ry as half-height).
514        The sign of the span angle determines the drawing direction.
515
516        Args:
517            radius_x (float): The x radius of the arc.
518            radius_y (float): The y radius of the arc.
519            start_angle (float): The starting angle of the arc.
520            span_angle (float): The span angle of the arc.
521            rot_angle (float, optional): The rotation angle of the arc. Defaults to 0.
522            n_points (int, optional): The number of points to use for the arc. Defaults to None.
523            **kwargs: Additional keyword arguments.
524
525        Returns:
526            Path: The path object.
527        """
528        rx = radius_x
529        ry = radius_y
530        start_angle = positive_angle(start_angle)
531        clockwise = span_angle < 0
532        if n_points is None:
533            n_points = defaults["n_arc_points"]
534        points = elliptic_arc_points((0, 0), rx, ry, start_angle, span_angle, n_points)
535        start = points[0]
536        end = points[-1]
537        # Translate the start to the current position and rotate by the rotation angle.
538        dx = self.pos[0] - start[0]
539        dy = self.pos[1] - start[1]
540        rotocenter = start
541        if rot_angle != 0:
542            points = (
543                homogenize(points)
544                @ rotation_matrix(rot_angle, rotocenter)
545                @ translation_matrix(dx, dy)
546            )
547        else:
548            points = homogenize(points) @ translation_matrix(dx, dy)
549        tangent_angle = ellipse_tangent(rx, ry, *end) + rot_angle
550        if clockwise:
551            tangent_angle += pi
552        pos = points[-1]
553        self._add(
554            pos,
555            PathOps.ARC,
556            (pos, tangent_angle, rx, ry, start_angle, span_angle, rot_angle, points),
557        )
558        return self

Add an arc to the path. The arc is defined by an ellipse (with rx as half-width and ry as half-height). The sign of the span angle determines the drawing direction.

Arguments:
  • radius_x (float): The x radius of the arc.
  • radius_y (float): The y radius of the arc.
  • start_angle (float): The starting angle of the arc.
  • span_angle (float): The span angle of the arc.
  • rot_angle (float, optional): The rotation angle of the arc. Defaults to 0.
  • n_points (int, optional): The number of points to use for the arc. Defaults to None.
  • **kwargs: Additional keyword arguments.
Returns:

Path: The path object.

def blend_arc( self, radius_x: float, radius_y: float, start_angle: float, span_angle: float, sharp=False, n_points=None, **kwargs) -> typing_extensions.Self:
560    def blend_arc(
561        self,
562        radius_x: float,
563        radius_y: float,
564        start_angle: float,
565        span_angle: float,
566        sharp=False,
567        n_points=None,
568        **kwargs,
569    ) -> Self:
570        """Add a blended elliptic arc to the path.
571
572        Args:
573            radius_x (float): The x radius of the arc.
574            radius_y (float): The y radius of the arc.
575            start_angle (float): The starting angle of the arc.
576            span_angle (float): The span angle of the arc.
577            sharp (bool, optional): Whether the arc is sharp. Defaults to False.
578            n_points (int, optional): The number of points to use for the arc. Defaults to None.
579            **kwargs: Additional keyword arguments.
580
581        Returns:
582            Path: The path object.
583        """
584        rx = radius_x
585        ry = radius_y
586        start_angle = positive_angle(start_angle)
587        clockwise = span_angle < 0
588        if n_points is None:
589            n_points = defaults["n_arc_points"]
590        points = elliptic_arc_points((0, 0), rx, ry, start_angle, span_angle, n_points)
591        start = points[0]
592        end = points[-1]
593        # Translate the start to the current position and rotate by the computed rotation angle.
594        dx = self.pos[0] - start[0]
595        dy = self.pos[1] - start[1]
596        rotocenter = start
597        tangent = ellipse_tangent(rx, ry, *start)
598        rot_angle = self.angle - tangent
599        if clockwise:
600            rot_angle += pi
601        if sharp:
602            rot_angle += pi
603        points = (
604            homogenize(points)
605            @ rotation_matrix(rot_angle, rotocenter)
606            @ translation_matrix(dx, dy)
607        )
608        tangent_angle = ellipse_tangent(rx, ry, *end) + rot_angle
609        if clockwise:
610            tangent_angle += pi
611        pos = points[-1][:2]
612        self._add(
613            pos,
614            PathOps.ARC,
615            (pos, tangent_angle, rx, ry, start_angle, span_angle, rot_angle, points),
616        )
617        return self

Add a blended elliptic arc to the path.

Arguments:
  • radius_x (float): The x radius of the arc.
  • radius_y (float): The y radius of the arc.
  • start_angle (float): The starting angle of the arc.
  • span_angle (float): The span angle of the arc.
  • sharp (bool, optional): Whether the arc is sharp. Defaults to False.
  • n_points (int, optional): The number of points to use for the arc. Defaults to None.
  • **kwargs: Additional keyword arguments.
Returns:

Path: The path object.

def sine( self, period: float = 40, amplitude: float = 20, duration: float = 40, phase_angle: float = 0, rot_angle: float = 0, damping: float = 0, n_points: int = 100, **kwargs) -> typing_extensions.Self:
619    def sine(
620        self,
621        period: float = 40,
622        amplitude: float = 20,
623        duration: float = 40,
624        phase_angle: float = 0,
625        rot_angle: float = 0,
626        damping: float = 0,
627        n_points: int = 100,
628        **kwargs,
629    ) -> Self:
630        """Add a sine wave to the path.
631
632        Args:
633            period (float, optional): _description_. Defaults to 40.
634            amplitude (float, optional): _description_. Defaults to 20.
635            duration (float, optional): _description_. Defaults to 1.
636            n_points (int, optional): _description_. Defaults to 100.
637            phase_angle (float, optional): _description_. Defaults to 0.
638            damping (float, optional): _description_. Defaults to 0.
639            rot_angle (float, optional): _description_. Defaults to 0.
640
641        Returns:
642            Path: The path object.
643        """
644
645        points = sine_points(
646            period, amplitude, duration, n_points, phase_angle, damping
647        )
648        if rot_angle != 0:
649            points = homogenize(points) @ rotation_matrix(rot_angle, points[0])
650        points = homogenize(points) @ translation_matrix(*self.pos[:2])
651        angle = line_angle(points[-2], points[-1])
652        self._add(points[-1], PathOps.SINE, (points, angle))
653        return self

Add a sine wave to the path.

Arguments:
  • period (float, optional): _description_. Defaults to 40.
  • amplitude (float, optional): _description_. Defaults to 20.
  • duration (float, optional): _description_. Defaults to 1.
  • n_points (int, optional): _description_. Defaults to 100.
  • phase_angle (float, optional): _description_. Defaults to 0.
  • damping (float, optional): _description_. Defaults to 0.
  • rot_angle (float, optional): _description_. Defaults to 0.
Returns:

Path: The path object.

def blend_sine( self, period: float = 40, amplitude: float = 20, duration: float = 40, phase_angle: float = 0, damping: float = 0, n_points: int = 100, **kwargs) -> typing_extensions.Self:
655    def blend_sine(
656        self,
657        period: float = 40,
658        amplitude: float = 20,
659        duration: float = 40,
660        phase_angle: float = 0,
661        damping: float = 0,
662        n_points: int = 100,
663        **kwargs,
664    ) -> Self:
665        """Add a blended sine wave to the path.
666
667        Args:
668            amplitude (float): The amplitude of the wave.
669            frequency (float): The frequency of the wave.
670            length (float): The length of the wave.
671            **kwargs: Additional keyword arguments.
672
673        Returns:
674            Path: The path object.
675        """
676
677        points = sine_points(
678            period, amplitude, duration, n_points, phase_angle, damping
679        )
680        start_angle = line_angle(points[0], points[1])
681        rot_angle = self.angle - start_angle
682        points = homogenize(points) @ rotation_matrix(rot_angle, points[0])
683        points = homogenize(points) @ translation_matrix(*self.pos[:2])
684        angle = line_angle(points[-2], points[-1])
685        self._add(points[-1], PathOps.SINE, (points, angle))
686        return self

Add a blended sine wave to the path.

Arguments:
  • amplitude (float): The amplitude of the wave.
  • frequency (float): The frequency of the wave.
  • length (float): The length of the wave.
  • **kwargs: Additional keyword arguments.
Returns:

Path: The path object.

def close(self, **kwargs) -> typing_extensions.Self:
688    def close(self, **kwargs) -> Self:
689        """Close the path.
690
691        Args:
692            **kwargs: Additional keyword arguments.
693
694        Returns:
695            Path: The path object.
696        """
697        self._add(self.pos, PathOps.CLOSE, None, **kwargs)
698        return self

Close the path.

Arguments:
  • **kwargs: Additional keyword arguments.
Returns:

Path: The path object.

vertices
700    @property
701    def vertices(self):
702        """Return the vertices of the path.
703
704        Returns:
705            list: The vertices of the path.
706        """
707        vertices = []
708        last_vert = None
709        dist_tol2 = defaults["dist_tol"] ** 2
710        for obj in self.objects:
711            if obj is not None and obj.vertices:
712                obj_verts = obj.vertices
713                if last_vert:
714                    if close_points2(last_vert,  obj_verts[0], dist_tol2):
715                        vertices.extend(obj_verts[1:])
716                    else:
717                        vertices.extend(obj_verts)
718                else:
719                    vertices.extend(obj_verts)
720                last_vert = obj_verts[-1]
721
722        return vertices

Return the vertices of the path.

Returns:

list: The vertices of the path.

def set_style(self, name, value, **kwargs) -> typing_extensions.Self:
724    def set_style(self, name, value, **kwargs) -> Self:
725        """Set the style of the path.
726
727        Args:
728            name (str): The name of the style.
729            value (Any): The value of the style.
730            **kwargs: Additional keyword arguments.
731
732        Returns:
733            Path: The path object.
734        """
735        self.operations.append((PathOps.STYLE, (name, value, kwargs)))
736        return self

Set the style of the path.

Arguments:
  • name (str): The name of the style.
  • value (Any): The value of the style.
  • **kwargs: Additional keyword arguments.
Returns:

Path: The path object.