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