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
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]])
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
Inherited Members
- simetri.graphics.batch.Batch
- type
- modifiers
- blend_mode
- alpha
- line_alpha
- fill_alpha
- text_alpha
- clip
- mask
- even_odd_rule
- blend_group
- transparency_group
- set_attribs
- set_batch_attr
- proximity
- append
- reverse
- insert
- remove
- clear
- extend
- iter_elements
- all_elements
- all_shapes
- all_vertices
- all_segments
- as_graph
- graph_summary
- merge_shapes
- all_polygons
- b_box