simetri.graphics.batch
Batch objects are used for grouping other Shape and Batch objects.
1"""Batch objects are used for grouping other Shape and Batch objects. 2""" 3 4from typing import Any, Iterator, List, Sequence 5 6from numpy import around, array 7from typing_extensions import Self, Dict 8import networkx as nx 9 10 11from .all_enums import Types, batch_types, get_enum_value 12from .common import common_properties, _set_Nones, Point, Line 13from .core import Base 14from .bbox import bounding_box 15from ..canvas.style_map import batch_args 16from ..helpers.validation import validate_args 17from ..geometry.geometry import( 18 fix_degen_points, 19 get_polygons, 20 all_close_points, 21 mid_point, 22 distance, 23 connected_pairs, 24 round_segment, 25 round_point 26) 27from ..helpers.graph import is_cycle, is_open_walk, Graph 28from ..settings.settings import defaults 29 30from .merge import _merge_shapes, _merge_collinears 31 32 33class Batch(Base): 34 """ 35 A Batch object is a collection of other objects (Batch, Shape, 36 and Tag objects). It can be used to apply a transformation to 37 all the objects in the Batch. It is used for creating 1D and 2D 38 patterns of objects. all_vertices, all_elements, etc. means a flat 39 list of the specified object gathered recursively from all the 40 elements in the Batch. 41 """ 42 43 def __init__( 44 self, 45 elements: Sequence[Any] = None, 46 modifiers: Sequence["Modifier"] = None, 47 subtype: Types = Types.BATCH, 48 **kwargs, 49 ): 50 """ 51 Initialize a Batch object. 52 53 Args: 54 elements (Sequence[Any], optional): The elements to include in the batch. 55 modifiers (Sequence[Modifier], optional): The modifiers to apply to the batch. 56 subtype (Types, optional): The subtype of the batch. 57 kwargs (dict): Additional keyword arguments. 58 """ 59 validate_args(kwargs, batch_args) 60 if elements and not isinstance(elements, (list, tuple)): 61 self.elements = [elements] 62 else: 63 self.elements = elements if elements is not None else [] 64 self.type = Types.BATCH 65 self.subtype = get_enum_value(Types, subtype) 66 self.modifiers = modifiers 67 self.blend_mode = None 68 self.alpha = None 69 self.line_alpha = None 70 self.fill_alpha = None 71 self.text_alpha = None 72 self.clip = False # if clip is True, the batch.mask is used as a clip path 73 self.mask = None 74 self.even_odd_rule = False 75 self.blend_group = False 76 self.transparency_group = False 77 common_properties(self) 78 for key, value in kwargs.items(): 79 setattr(self, key, value) 80 81 def set_attribs(self, attrib, value): 82 """ 83 Sets the attribute to the given value for all elements in the batch if it is applicable. 84 85 Args: 86 attrib (str): The attribute to set. 87 value (Any): The value to set the attribute to. 88 """ 89 for element in self.elements: 90 if element.type == Types.BATCH: 91 setattr(element, attrib, value) 92 elif hasattr(element, attrib): 93 setattr(element, attrib, value) 94 95 def set_batch_attr(self, attrib: str, value: Any) -> Self: 96 """ 97 Sets the attribute to the given value for the batch itself. 98 batch.attrib = value would set the attribute to the elements 99 of the batch object but not the batch itself. 100 101 Args: 102 attrib (str): The attribute to set. 103 value (Any): The value to set the attribute to. 104 105 Returns: 106 Self: The batch object. 107 """ 108 self.__dict__[attrib] = value 109 110 def __str__(self): 111 """ 112 Return a string representation of the batch. 113 114 Returns: 115 str: The string representation of the batch. 116 """ 117 if self.elements is None or len(self.elements) == 0: 118 res = "Batch()" 119 elif len(self.elements) in [1, 2]: 120 res = f"Batch({self.elements})" 121 else: 122 res = f"Batch({self.elements[0]}...{self.elements[-1]})" 123 return res 124 125 def __repr__(self): 126 """ 127 Return a string representation of the batch. 128 129 Returns: 130 str: The string representation of the batch. 131 """ 132 return self.__str__() 133 134 def __len__(self): 135 """ 136 Return the number of elements in the batch. 137 138 Returns: 139 int: The number of elements in the batch. 140 """ 141 return len(self.elements) 142 143 def __getitem__(self, subscript): 144 """ 145 Get the element(s) at the given subscript. 146 147 Args: 148 subscript (int or slice): The subscript to get the element(s) from. 149 150 Returns: 151 Any: The element(s) at the given subscript. 152 """ 153 if isinstance(subscript, slice): 154 res = self.elements[subscript.start : subscript.stop : subscript.step] 155 else: 156 res = self.elements[subscript] 157 return res 158 159 def __setitem__(self, subscript, value): 160 """ 161 Set the element(s) at the given subscript. 162 163 Args: 164 subscript (int or slice): The subscript to set the element(s) at. 165 value (Any): The value to set the element(s) to. 166 """ 167 elements = self.elements 168 if isinstance(subscript, slice): 169 elements[subscript.start : subscript.stop : subscript.step] = value 170 elif isinstance(subscript, int): 171 elements[subscript] = value 172 else: 173 raise TypeError("Invalid subscript type") 174 175 def __add__(self, other: "Batch") -> "Batch": 176 """ 177 Add another batch to this batch. 178 179 Args: 180 other (Batch): The other batch to add. 181 182 Returns: 183 Batch: The combined batch. 184 185 Raises: 186 RuntimeError: If the other object is not a batch. 187 """ 188 if other.type == Types.BATCH: 189 batch = self.copy() 190 for element in other.elements: 191 batch.append(element) 192 res = batch 193 else: 194 raise RuntimeError( 195 "Invalid object. Only Batch objects can be added together!" 196 ) 197 return res 198 199 def __bool__(self): 200 """ 201 Return whether the batch has any elements. 202 203 Returns: 204 bool: True if the batch has elements, False otherwise. 205 """ 206 return len(self.elements) > 0 207 208 def __iter__(self): 209 """ 210 Return an iterator over the elements in the batch. 211 212 Returns: 213 Iterator: An iterator over the elements in the batch. 214 """ 215 return iter(self.elements) 216 217 def _duplicates(self, elements): 218 """ 219 Check for duplicate elements in the batch. 220 221 Args: 222 elements (Sequence[Any]): The elements to check for duplicates. 223 224 Raises: 225 ValueError: If duplicate elements are found. 226 227 Returns: 228 bool: True if duplicates are found, False otherwise. 229 """ 230 for element in elements: 231 ids = [x.id for x in self.elements] 232 if element.id in ids: 233 raise ValueError("Only unique elements are allowed!") 234 235 return len(set(elements)) != len(elements) 236 237 def proximity(self, dist_tol: float = None, n: int = 5) -> list[Point]: 238 """ 239 Returns the n closest points in the batch. 240 241 Args: 242 dist_tol (float, optional): The distance tolerance for proximity. 243 n (int, optional): The number of closest points to return. 244 245 Returns: 246 list[Point]: The n closest points in the batch. 247 """ 248 if dist_tol is None: 249 dist_tol = defaults["dist_tol"] 250 vertices = self.all_vertices 251 vertices = [(*v, i) for i, v in enumerate(vertices)] 252 _, pairs = all_close_points(vertices, dist_tol=dist_tol, with_dist=True) 253 return [pair for pair in pairs if pair[2] > 0][:n] 254 255 def append(self, element: Any) -> Self: 256 """ 257 Appends the element to the batch. 258 259 Args: 260 element (Any): The element to append. 261 262 Returns: 263 Self: The batch object. 264 """ 265 if element not in self.elements: 266 self.elements.append(element) 267 return self 268 269 def reverse(self) -> Self: 270 """ 271 Reverses the order of the elements in the batch. 272 273 Returns: 274 Self: The batch object. 275 """ 276 self.elements = self.elements[::-1] 277 return self 278 279 def insert(self, index, element: Any) -> Self: 280 """ 281 Inserts the element at the given index. 282 283 Args: 284 index (int): The index to insert the element at. 285 element (Any): The element to insert. 286 287 Returns: 288 Self: The batch object. 289 """ 290 if element not in self.elements: 291 self.elements.insert(index, element) 292 293 return self 294 295 def remove(self, element: Any) -> Self: 296 """ 297 Removes the element from the batch. 298 299 Args: 300 element (Any): The element to remove. 301 302 Returns: 303 Self: The batch object. 304 """ 305 if element in self.elements: 306 self.elements.remove(element) 307 return self 308 309 def pop(self, index: int) -> Any: 310 """ 311 Removes the element at the given index and returns it. 312 313 Args: 314 index (int): The index to remove the element from. 315 316 Returns: 317 Any: The removed element. 318 """ 319 return self.elements.pop(index) 320 321 def clear(self) -> Self: 322 """ 323 Removes all elements from the batch. 324 325 Returns: 326 Self: The batch object. 327 """ 328 self.elements = [] 329 return self 330 331 def extend(self, elements: Sequence[Any]) -> Self: 332 """ 333 Extends the batch with the given elements. 334 335 Args: 336 elements (Sequence[Any]): The elements to extend the batch with. 337 338 Returns: 339 Self: The batch object. 340 """ 341 for element in elements: 342 if element not in self.elements: 343 self.elements.append(element) 344 345 return self 346 347 def iter_elements(self, element_type: Types = None) -> Iterator: 348 """Iterate over all elements in the batch, including the elements 349 in the nested batches. 350 351 Args: 352 element_type (Types, optional): The type of elements to iterate over. Defaults to None. 353 354 Returns: 355 Iterator: An iterator over the elements in the batch. 356 """ 357 for elem in self.elements: 358 if elem.type == Types.BATCH: 359 yield from elem.iter_elements(element_type) 360 else: 361 if element_type is None: 362 yield elem 363 elif elem.type == element_type: 364 yield elem 365 366 @property 367 def all_elements(self) -> list[Any]: 368 """Return a list of all elements in the batch, 369 including the elements in the nested batches. 370 371 Returns: 372 list[Any]: A list of all elements in the batch. 373 """ 374 elements = [] 375 for elem in self.elements: 376 if elem.type == Types.BATCH: 377 elements.extend(elem.all_elements) 378 else: 379 elements.append(elem) 380 return elements 381 382 @property 383 def all_shapes(self) -> list["Shape"]: 384 """Return a list of all shapes in the batch. 385 386 Returns: 387 list[Shape]: A list of all shapes in the batch. 388 """ 389 elements = self.all_elements 390 shapes = [] 391 for element in elements: 392 if element.type == Types.SHAPE: 393 shapes.append(element) 394 return shapes 395 396 @property 397 def all_vertices(self) -> list[Point]: 398 """Return a list of all points in the batch in their 399 transformed positions. 400 401 Returns: 402 list[Point]: A list of all points in the batch in their transformed positions. 403 """ 404 elements = self.all_elements 405 vertices = [] 406 for element in elements: 407 if element.type == Types.SHAPE: 408 vertices.extend(element.vertices) 409 elif element.type == Types.BATCH: 410 vertices.extend(element.all_vertices) 411 return vertices 412 413 @property 414 def all_segments(self) -> list[Line]: 415 """Return a list of all segments in the batch. 416 417 Returns: 418 list[Line]: A list of all segments in the batch. 419 """ 420 elements = self.all_elements 421 segments = [] 422 for element in elements: 423 if element.type == Types.SHAPE: 424 segments.extend(element.vertex_pairs) 425 return segments 426 427 428 def _get_graph_nodes_and_edges(self, dist_tol: float = None, n_round=None): 429 """Get the graph nodes and edges for the batch. 430 431 Args: 432 dist_tol (float, optional): The distance tolerance for proximity. Defaults to None. 433 n_round (int, optional): The number of decimal places to round to. Defaults to None. 434 435 Returns: 436 tuple: A tuple containing the node coordinates and edges. 437 """ 438 if n_round is None: 439 n_round = defaults["n_round"] 440 _set_Nones(self, ["dist_tol", "n_round"], [dist_tol, n_round]) 441 vertices = self.all_vertices 442 shapes = self.all_shapes 443 d_ind_coords = {} 444 point_id = [] 445 rounded_vertices = [] 446 for i, vert in enumerate(vertices): 447 coords = tuple(around(vert, n_round)) 448 rounded_vertices.append(coords) 449 d_ind_coords[i] = coords 450 point_id.append([vert[0], vert[1], i]) 451 452 _, pairs = all_close_points(point_id, dist_tol=dist_tol, with_dist=True) 453 454 for pair in pairs: 455 id1, id2, _ = pair 456 average = tuple(mid_point(vertices[id1], vertices[id2])) 457 d_ind_coords[id1] = average 458 d_ind_coords[id2] = average 459 rounded_vertices[id1] = average 460 rounded_vertices[id2] = average 461 462 d_coords_node_id = {} 463 d_node_id__rounded_coords = {} 464 465 s_rounded_vertices = set(rounded_vertices) 466 for i, vertex in enumerate(s_rounded_vertices): 467 d_coords_node_id[vertex] = i 468 d_node_id__rounded_coords[i] = vertex 469 470 edges = [] 471 ind = 0 472 for shape in shapes: 473 node_ids = [] 474 s_vertices = shape.vertices[:] 475 for vertex in s_vertices: 476 node_ids.append(d_coords_node_id[rounded_vertices[ind]]) 477 ind += 1 478 edges.extend(connected_pairs(node_ids)) 479 if shape.closed: 480 edges.append((node_ids[-1], node_ids[0])) 481 482 return d_node_id__rounded_coords, edges 483 484 def as_graph( 485 self, 486 directed: bool = False, 487 weighted: bool = False, 488 dist_tol: float = None, 489 atol=None, 490 n_round: int = None, 491 ) -> Graph: 492 """Return the batch as a Graph object. 493 Graph.nx is the networkx graph. 494 495 Args: 496 directed (bool, optional): Whether the graph is directed. Defaults to False. 497 weighted (bool, optional): Whether the graph is weighted. Defaults to False. 498 dist_tol (float, optional): The distance tolerance for proximity. Defaults to None. 499 atol (optional): The absolute tolerance. Defaults to None. 500 n_round (int, optional): The number of decimal places to round to. Defaults to None. 501 502 Returns: 503 Graph: The batch as a Graph object. 504 """ 505 _set_Nones(self, ["dist_tol", "atol", "n_round"], [dist_tol, atol, n_round]) 506 d_node_id_coords, edges = self._get_graph_nodes_and_edges(dist_tol, n_round) 507 if directed: 508 nx_graph = nx.DiGraph() 509 graph_type = Types.DIRECTED 510 else: 511 nx_graph = nx.Graph() 512 graph_type = Types.UNDIRECTED 513 514 for id_, coords in d_node_id_coords.items(): 515 nx_graph.add_node(id_, pos=coords) 516 517 if weighted: 518 for edge in edges: 519 p1 = d_node_id_coords[edge[0]] 520 p2 = d_node_id_coords[edge[1]] 521 nx_graph.add_edge(edge[0], edge[1], weight=distance(p1, p2)) 522 subtype = Types.WEIGHTED 523 else: 524 nx_graph.update(edges) 525 subtype = Types.NONE 526 527 graph = Graph(type=graph_type, subtype=subtype, nx_graph=nx_graph) 528 return graph 529 530 def graph_summary(self, dist_tol: float = None, n_round: int = None) -> str: 531 """Returns a representation of the Batch object as a graph. 532 533 Args: 534 dist_tol (float, optional): The distance tolerance for proximity. Defaults to None. 535 n_round (int, optional): The number of decimal places to round to. Defaults to None. 536 537 Returns: 538 str: A representation of the Batch object as a graph. 539 """ 540 if dist_tol is None: 541 dist_tol = defaults["dist_tol"] 542 if n_round is None: 543 n_round = defaults["n_round"] 544 all_shapes = self.all_shapes 545 all_vertices = self.all_vertices 546 lines = [] 547 lines.append("Batch summary:") 548 lines.append(f"# shapes: {len(all_shapes)}") 549 lines.append(f"# vertices: {len(all_vertices)}") 550 for shape in self.all_shapes: 551 if shape.subtype: 552 s = ( 553 f"# vertices in shape(id: {shape.id}, subtype: " 554 f"{shape.subtype}): {len(shape.vertices)}" 555 ) 556 else: 557 s = f"# vertices in shape(id: {shape.id}): " f"{len(shape.vertices)}" 558 lines.append(s) 559 graph = self.as_graph(dist_tol=dist_tol, n_round=n_round).nx_graph 560 561 for island in nx.connected_components(graph): 562 lines.append(f"Island: {island}") 563 if is_cycle(graph, island): 564 lines.append(f"Cycle: {len(island)} nodes") 565 elif is_open_walk(graph, island): 566 lines.append(f"Open Walk: {len(island)} nodes") 567 else: 568 degens = [node for node in island if graph.degree(node) > 2] 569 degrees = f"{[(node, graph.degree(node)) for node in degens]}" 570 lines.append(f"Degenerate: {len(island)} nodes") 571 lines.append(f"(Node, Degree): {degrees}") 572 lines.append("-" * 40) 573 574 return "\n".join(lines) 575 576 def _merge_collinears(self, edges, n_round=2): 577 """Merge collinear edges in the batch. 578 579 Args: 580 d_node_id_coords (dict): The node coordinates. 581 edges (list): The edges to merge. 582 tol (float, optional): The tolerance for merging. Defaults to None. 583 rtol (float, optional): The relative tolerance. Defaults to None. 584 atol (float, optional): The absolute tolerance. Defaults to None. 585 586 Returns: 587 list: The merged edges. 588 """ 589 return _merge_collinears(self, edges, n_round=n_round) 590 591 def merge_shapes( 592 self, dist_tol: float = None, n_round: int = None) -> Self: 593 """Merges the shapes in the batch if they are connected. 594 Returns a new batch with the merged shapes as well as the shapes 595 as well as the shapes that could not be merged. 596 597 Args: 598 tol (float, optional): The tolerance for merging shapes. Defaults to None. 599 rtol (float, optional): The relative tolerance. Defaults to None. 600 atol (float, optional): The absolute tolerance. Defaults to None. 601 602 Returns: 603 Self: The batch object with merged shapes. 604 """ 605 return _merge_shapes(self, dist_tol=dist_tol, n_round=n_round) 606 607 def _get_edges_and_segments(self, dist_tol: float = None, n_round: int = None): 608 """Get the edges and segments for the batch. 609 610 Args: 611 dist_tol (float, optional): The distance tolerance for proximity. Defaults to None. 612 n_round (int, optional): The number of decimal places to round to. Defaults to None. 613 614 Returns: 615 tuple: A tuple containing the edges and segments. 616 """ 617 if dist_tol is None: 618 dist_tol = defaults["dist_tol"] 619 if n_round is None: 620 n_round = defaults["n_round"] 621 d_coord_node = self.d_coord_node 622 segments = self.all_segments 623 segments = [round_segment(segment, n_round) for segment in segments] 624 edges = [] 625 for seg in segments: 626 p1, p2 = seg 627 id1 = d_coord_node[p1] 628 id2 = d_coord_node[p2] 629 edges.append((id1, id2)) 630 631 return edges, segments 632 633 def _set_node_dictionaries(self, coords: List[Point], n_round: int=2) -> List[Dict]: 634 '''Set dictionaries for nodes and coordinates. 635 d_node_coord: Dictionary of node id to coordinates. 636 d_coord_node: Dictionary of coordinates to node id. 637 638 Args: 639 nodes (List[Point]): List of vertices. 640 n_round (int, optional): Number of rounding digits. Defaults to 2. 641 ''' 642 643 coords = [tuple(round_point(coord, n_round)) for coord in coords] 644 coords = list(set(coords)) # remove duplicates 645 coords.sort() # sort by x coordinates 646 coords.sort(key=lambda x: x[1]) # sort by y coordinates 647 648 d_node_coord = {} 649 d_coord_node = {} 650 651 for i, coord in enumerate(coords): 652 d_node_coord[i] = coord 653 d_coord_node[coord] = i 654 655 self.d_node_coord = d_node_coord 656 self.d_coord_node = d_coord_node 657 658 def all_polygons(self, dist_tol: float = None) -> list: 659 """Return a list of all polygons in the batch in their 660 transformed positions. 661 662 Args: 663 dist_tol (float, optional): The distance tolerance for proximity. Defaults to None. 664 665 Returns: 666 list: A list of all polygons in the batch. 667 """ 668 if dist_tol is None: 669 dist_tol = defaults["dist_tol"] 670 exclude = [] 671 include = [] 672 for shape in self.all_shapes: 673 if len(shape.primary_points) > 2 and shape.closed: 674 vertices = shape.vertices 675 exclude.append(vertices) 676 else: 677 include.append(shape) 678 polylines = [] 679 for element in include: 680 points = element.vertices 681 points = fix_degen_points(points, dist_tol=dist_tol, closed=element.closed) 682 polylines.append(points) 683 fixed_polylines = [] 684 if polylines: 685 for polyline in polylines: 686 fixed_polylines.append( 687 fix_degen_points(polyline, dist_tol=dist_tol, closed=True) 688 ) 689 polygons = get_polygons(fixed_polylines, dist_tol=dist_tol) 690 res = polygons + exclude 691 else: 692 res = exclude 693 return res 694 695 def copy(self) -> "Batch": 696 """Returns a copy of the batch. 697 698 Returns: 699 Batch: A copy of the batch. 700 """ 701 b = Batch(modifiers=self.modifiers) 702 if self.elements: 703 b.elements = [elem.copy() for elem in self.elements] 704 else: 705 b.elements = [] 706 custom_attribs = custom_batch_attributes(self) 707 for attrib in custom_attribs: 708 setattr(b, attrib, getattr(self, attrib)) 709 return b 710 711 @property 712 def b_box(self): 713 """Returns the bounding box of the batch. 714 715 Returns: 716 BoundingBox: The bounding box of the batch. 717 """ 718 xy_list = [] 719 for elem in self.elements: 720 if hasattr(elem, "b_box"): 721 xy_list.extend(elem.b_box.corners) # To do: we should eliminate this. Just add all points. 722 # To do: memoize the bounding box 723 return bounding_box(array(xy_list)) 724 725 def _modify(self, modifier): 726 """Apply a modifier to the batch. 727 728 Args: 729 modifier (Modifier): The modifier to apply. 730 """ 731 modifier.apply() 732 733 def _update(self, xform_matrix, reps: int = 0): 734 """Updates the batch with the given transformation matrix. 735 If reps is 0, the transformation is applied to all elements. 736 If reps is greater than 0, the transformation creates 737 new elements with the transformed xform_matrix. 738 739 Args: 740 xform_matrix (ndarray): The transformation matrix. 741 reps (int, optional): The number of repetitions. Defaults to 0. 742 """ 743 if reps == 0: 744 for element in self.elements: 745 element._update(xform_matrix, reps=0) 746 if self.modifiers: 747 for modifier in self.modifiers: 748 modifier.apply(element) 749 else: 750 elements = self.elements[:] 751 new = [] 752 for _ in range(reps): 753 for element in elements: 754 new_element = element.copy() 755 new_element._update(xform_matrix) 756 self.elements.append(new_element) 757 new.append(new_element) 758 if self.modifiers: 759 for modifier in self.modifiers: 760 modifier.apply(new_element) 761 elements = new[:] 762 new = [] 763 return self 764 765 766def custom_batch_attributes(item: Batch) -> List[str]: 767 """ 768 Return a list of custom attributes of a Shape or 769 Batch instance. 770 771 Args: 772 item (Batch): The batch object. 773 774 Returns: 775 List[str]: A list of custom attributes. 776 """ 777 from .shape import Shape 778 779 if isinstance(item, Batch): 780 dummy_shape = Shape([(0, 0), (1, 0)]) 781 dummy = Batch([dummy_shape]) 782 else: 783 raise TypeError("Invalid item type") 784 native_attribs = set(dir(dummy)) 785 custom_attribs = set(dir(item)) - native_attribs 786 787 return list(custom_attribs)
34class Batch(Base): 35 """ 36 A Batch object is a collection of other objects (Batch, Shape, 37 and Tag objects). It can be used to apply a transformation to 38 all the objects in the Batch. It is used for creating 1D and 2D 39 patterns of objects. all_vertices, all_elements, etc. means a flat 40 list of the specified object gathered recursively from all the 41 elements in the Batch. 42 """ 43 44 def __init__( 45 self, 46 elements: Sequence[Any] = None, 47 modifiers: Sequence["Modifier"] = None, 48 subtype: Types = Types.BATCH, 49 **kwargs, 50 ): 51 """ 52 Initialize a Batch object. 53 54 Args: 55 elements (Sequence[Any], optional): The elements to include in the batch. 56 modifiers (Sequence[Modifier], optional): The modifiers to apply to the batch. 57 subtype (Types, optional): The subtype of the batch. 58 kwargs (dict): Additional keyword arguments. 59 """ 60 validate_args(kwargs, batch_args) 61 if elements and not isinstance(elements, (list, tuple)): 62 self.elements = [elements] 63 else: 64 self.elements = elements if elements is not None else [] 65 self.type = Types.BATCH 66 self.subtype = get_enum_value(Types, subtype) 67 self.modifiers = modifiers 68 self.blend_mode = None 69 self.alpha = None 70 self.line_alpha = None 71 self.fill_alpha = None 72 self.text_alpha = None 73 self.clip = False # if clip is True, the batch.mask is used as a clip path 74 self.mask = None 75 self.even_odd_rule = False 76 self.blend_group = False 77 self.transparency_group = False 78 common_properties(self) 79 for key, value in kwargs.items(): 80 setattr(self, key, value) 81 82 def set_attribs(self, attrib, value): 83 """ 84 Sets the attribute to the given value for all elements in the batch if it is applicable. 85 86 Args: 87 attrib (str): The attribute to set. 88 value (Any): The value to set the attribute to. 89 """ 90 for element in self.elements: 91 if element.type == Types.BATCH: 92 setattr(element, attrib, value) 93 elif hasattr(element, attrib): 94 setattr(element, attrib, value) 95 96 def set_batch_attr(self, attrib: str, value: Any) -> Self: 97 """ 98 Sets the attribute to the given value for the batch itself. 99 batch.attrib = value would set the attribute to the elements 100 of the batch object but not the batch itself. 101 102 Args: 103 attrib (str): The attribute to set. 104 value (Any): The value to set the attribute to. 105 106 Returns: 107 Self: The batch object. 108 """ 109 self.__dict__[attrib] = value 110 111 def __str__(self): 112 """ 113 Return a string representation of the batch. 114 115 Returns: 116 str: The string representation of the batch. 117 """ 118 if self.elements is None or len(self.elements) == 0: 119 res = "Batch()" 120 elif len(self.elements) in [1, 2]: 121 res = f"Batch({self.elements})" 122 else: 123 res = f"Batch({self.elements[0]}...{self.elements[-1]})" 124 return res 125 126 def __repr__(self): 127 """ 128 Return a string representation of the batch. 129 130 Returns: 131 str: The string representation of the batch. 132 """ 133 return self.__str__() 134 135 def __len__(self): 136 """ 137 Return the number of elements in the batch. 138 139 Returns: 140 int: The number of elements in the batch. 141 """ 142 return len(self.elements) 143 144 def __getitem__(self, subscript): 145 """ 146 Get the element(s) at the given subscript. 147 148 Args: 149 subscript (int or slice): The subscript to get the element(s) from. 150 151 Returns: 152 Any: The element(s) at the given subscript. 153 """ 154 if isinstance(subscript, slice): 155 res = self.elements[subscript.start : subscript.stop : subscript.step] 156 else: 157 res = self.elements[subscript] 158 return res 159 160 def __setitem__(self, subscript, value): 161 """ 162 Set the element(s) at the given subscript. 163 164 Args: 165 subscript (int or slice): The subscript to set the element(s) at. 166 value (Any): The value to set the element(s) to. 167 """ 168 elements = self.elements 169 if isinstance(subscript, slice): 170 elements[subscript.start : subscript.stop : subscript.step] = value 171 elif isinstance(subscript, int): 172 elements[subscript] = value 173 else: 174 raise TypeError("Invalid subscript type") 175 176 def __add__(self, other: "Batch") -> "Batch": 177 """ 178 Add another batch to this batch. 179 180 Args: 181 other (Batch): The other batch to add. 182 183 Returns: 184 Batch: The combined batch. 185 186 Raises: 187 RuntimeError: If the other object is not a batch. 188 """ 189 if other.type == Types.BATCH: 190 batch = self.copy() 191 for element in other.elements: 192 batch.append(element) 193 res = batch 194 else: 195 raise RuntimeError( 196 "Invalid object. Only Batch objects can be added together!" 197 ) 198 return res 199 200 def __bool__(self): 201 """ 202 Return whether the batch has any elements. 203 204 Returns: 205 bool: True if the batch has elements, False otherwise. 206 """ 207 return len(self.elements) > 0 208 209 def __iter__(self): 210 """ 211 Return an iterator over the elements in the batch. 212 213 Returns: 214 Iterator: An iterator over the elements in the batch. 215 """ 216 return iter(self.elements) 217 218 def _duplicates(self, elements): 219 """ 220 Check for duplicate elements in the batch. 221 222 Args: 223 elements (Sequence[Any]): The elements to check for duplicates. 224 225 Raises: 226 ValueError: If duplicate elements are found. 227 228 Returns: 229 bool: True if duplicates are found, False otherwise. 230 """ 231 for element in elements: 232 ids = [x.id for x in self.elements] 233 if element.id in ids: 234 raise ValueError("Only unique elements are allowed!") 235 236 return len(set(elements)) != len(elements) 237 238 def proximity(self, dist_tol: float = None, n: int = 5) -> list[Point]: 239 """ 240 Returns the n closest points in the batch. 241 242 Args: 243 dist_tol (float, optional): The distance tolerance for proximity. 244 n (int, optional): The number of closest points to return. 245 246 Returns: 247 list[Point]: The n closest points in the batch. 248 """ 249 if dist_tol is None: 250 dist_tol = defaults["dist_tol"] 251 vertices = self.all_vertices 252 vertices = [(*v, i) for i, v in enumerate(vertices)] 253 _, pairs = all_close_points(vertices, dist_tol=dist_tol, with_dist=True) 254 return [pair for pair in pairs if pair[2] > 0][:n] 255 256 def append(self, element: Any) -> Self: 257 """ 258 Appends the element to the batch. 259 260 Args: 261 element (Any): The element to append. 262 263 Returns: 264 Self: The batch object. 265 """ 266 if element not in self.elements: 267 self.elements.append(element) 268 return self 269 270 def reverse(self) -> Self: 271 """ 272 Reverses the order of the elements in the batch. 273 274 Returns: 275 Self: The batch object. 276 """ 277 self.elements = self.elements[::-1] 278 return self 279 280 def insert(self, index, element: Any) -> Self: 281 """ 282 Inserts the element at the given index. 283 284 Args: 285 index (int): The index to insert the element at. 286 element (Any): The element to insert. 287 288 Returns: 289 Self: The batch object. 290 """ 291 if element not in self.elements: 292 self.elements.insert(index, element) 293 294 return self 295 296 def remove(self, element: Any) -> Self: 297 """ 298 Removes the element from the batch. 299 300 Args: 301 element (Any): The element to remove. 302 303 Returns: 304 Self: The batch object. 305 """ 306 if element in self.elements: 307 self.elements.remove(element) 308 return self 309 310 def pop(self, index: int) -> Any: 311 """ 312 Removes the element at the given index and returns it. 313 314 Args: 315 index (int): The index to remove the element from. 316 317 Returns: 318 Any: The removed element. 319 """ 320 return self.elements.pop(index) 321 322 def clear(self) -> Self: 323 """ 324 Removes all elements from the batch. 325 326 Returns: 327 Self: The batch object. 328 """ 329 self.elements = [] 330 return self 331 332 def extend(self, elements: Sequence[Any]) -> Self: 333 """ 334 Extends the batch with the given elements. 335 336 Args: 337 elements (Sequence[Any]): The elements to extend the batch with. 338 339 Returns: 340 Self: The batch object. 341 """ 342 for element in elements: 343 if element not in self.elements: 344 self.elements.append(element) 345 346 return self 347 348 def iter_elements(self, element_type: Types = None) -> Iterator: 349 """Iterate over all elements in the batch, including the elements 350 in the nested batches. 351 352 Args: 353 element_type (Types, optional): The type of elements to iterate over. Defaults to None. 354 355 Returns: 356 Iterator: An iterator over the elements in the batch. 357 """ 358 for elem in self.elements: 359 if elem.type == Types.BATCH: 360 yield from elem.iter_elements(element_type) 361 else: 362 if element_type is None: 363 yield elem 364 elif elem.type == element_type: 365 yield elem 366 367 @property 368 def all_elements(self) -> list[Any]: 369 """Return a list of all elements in the batch, 370 including the elements in the nested batches. 371 372 Returns: 373 list[Any]: A list of all elements in the batch. 374 """ 375 elements = [] 376 for elem in self.elements: 377 if elem.type == Types.BATCH: 378 elements.extend(elem.all_elements) 379 else: 380 elements.append(elem) 381 return elements 382 383 @property 384 def all_shapes(self) -> list["Shape"]: 385 """Return a list of all shapes in the batch. 386 387 Returns: 388 list[Shape]: A list of all shapes in the batch. 389 """ 390 elements = self.all_elements 391 shapes = [] 392 for element in elements: 393 if element.type == Types.SHAPE: 394 shapes.append(element) 395 return shapes 396 397 @property 398 def all_vertices(self) -> list[Point]: 399 """Return a list of all points in the batch in their 400 transformed positions. 401 402 Returns: 403 list[Point]: A list of all points in the batch in their transformed positions. 404 """ 405 elements = self.all_elements 406 vertices = [] 407 for element in elements: 408 if element.type == Types.SHAPE: 409 vertices.extend(element.vertices) 410 elif element.type == Types.BATCH: 411 vertices.extend(element.all_vertices) 412 return vertices 413 414 @property 415 def all_segments(self) -> list[Line]: 416 """Return a list of all segments in the batch. 417 418 Returns: 419 list[Line]: A list of all segments in the batch. 420 """ 421 elements = self.all_elements 422 segments = [] 423 for element in elements: 424 if element.type == Types.SHAPE: 425 segments.extend(element.vertex_pairs) 426 return segments 427 428 429 def _get_graph_nodes_and_edges(self, dist_tol: float = None, n_round=None): 430 """Get the graph nodes and edges for the batch. 431 432 Args: 433 dist_tol (float, optional): The distance tolerance for proximity. Defaults to None. 434 n_round (int, optional): The number of decimal places to round to. Defaults to None. 435 436 Returns: 437 tuple: A tuple containing the node coordinates and edges. 438 """ 439 if n_round is None: 440 n_round = defaults["n_round"] 441 _set_Nones(self, ["dist_tol", "n_round"], [dist_tol, n_round]) 442 vertices = self.all_vertices 443 shapes = self.all_shapes 444 d_ind_coords = {} 445 point_id = [] 446 rounded_vertices = [] 447 for i, vert in enumerate(vertices): 448 coords = tuple(around(vert, n_round)) 449 rounded_vertices.append(coords) 450 d_ind_coords[i] = coords 451 point_id.append([vert[0], vert[1], i]) 452 453 _, pairs = all_close_points(point_id, dist_tol=dist_tol, with_dist=True) 454 455 for pair in pairs: 456 id1, id2, _ = pair 457 average = tuple(mid_point(vertices[id1], vertices[id2])) 458 d_ind_coords[id1] = average 459 d_ind_coords[id2] = average 460 rounded_vertices[id1] = average 461 rounded_vertices[id2] = average 462 463 d_coords_node_id = {} 464 d_node_id__rounded_coords = {} 465 466 s_rounded_vertices = set(rounded_vertices) 467 for i, vertex in enumerate(s_rounded_vertices): 468 d_coords_node_id[vertex] = i 469 d_node_id__rounded_coords[i] = vertex 470 471 edges = [] 472 ind = 0 473 for shape in shapes: 474 node_ids = [] 475 s_vertices = shape.vertices[:] 476 for vertex in s_vertices: 477 node_ids.append(d_coords_node_id[rounded_vertices[ind]]) 478 ind += 1 479 edges.extend(connected_pairs(node_ids)) 480 if shape.closed: 481 edges.append((node_ids[-1], node_ids[0])) 482 483 return d_node_id__rounded_coords, edges 484 485 def as_graph( 486 self, 487 directed: bool = False, 488 weighted: bool = False, 489 dist_tol: float = None, 490 atol=None, 491 n_round: int = None, 492 ) -> Graph: 493 """Return the batch as a Graph object. 494 Graph.nx is the networkx graph. 495 496 Args: 497 directed (bool, optional): Whether the graph is directed. Defaults to False. 498 weighted (bool, optional): Whether the graph is weighted. Defaults to False. 499 dist_tol (float, optional): The distance tolerance for proximity. Defaults to None. 500 atol (optional): The absolute tolerance. Defaults to None. 501 n_round (int, optional): The number of decimal places to round to. Defaults to None. 502 503 Returns: 504 Graph: The batch as a Graph object. 505 """ 506 _set_Nones(self, ["dist_tol", "atol", "n_round"], [dist_tol, atol, n_round]) 507 d_node_id_coords, edges = self._get_graph_nodes_and_edges(dist_tol, n_round) 508 if directed: 509 nx_graph = nx.DiGraph() 510 graph_type = Types.DIRECTED 511 else: 512 nx_graph = nx.Graph() 513 graph_type = Types.UNDIRECTED 514 515 for id_, coords in d_node_id_coords.items(): 516 nx_graph.add_node(id_, pos=coords) 517 518 if weighted: 519 for edge in edges: 520 p1 = d_node_id_coords[edge[0]] 521 p2 = d_node_id_coords[edge[1]] 522 nx_graph.add_edge(edge[0], edge[1], weight=distance(p1, p2)) 523 subtype = Types.WEIGHTED 524 else: 525 nx_graph.update(edges) 526 subtype = Types.NONE 527 528 graph = Graph(type=graph_type, subtype=subtype, nx_graph=nx_graph) 529 return graph 530 531 def graph_summary(self, dist_tol: float = None, n_round: int = None) -> str: 532 """Returns a representation of the Batch object as a graph. 533 534 Args: 535 dist_tol (float, optional): The distance tolerance for proximity. Defaults to None. 536 n_round (int, optional): The number of decimal places to round to. Defaults to None. 537 538 Returns: 539 str: A representation of the Batch object as a graph. 540 """ 541 if dist_tol is None: 542 dist_tol = defaults["dist_tol"] 543 if n_round is None: 544 n_round = defaults["n_round"] 545 all_shapes = self.all_shapes 546 all_vertices = self.all_vertices 547 lines = [] 548 lines.append("Batch summary:") 549 lines.append(f"# shapes: {len(all_shapes)}") 550 lines.append(f"# vertices: {len(all_vertices)}") 551 for shape in self.all_shapes: 552 if shape.subtype: 553 s = ( 554 f"# vertices in shape(id: {shape.id}, subtype: " 555 f"{shape.subtype}): {len(shape.vertices)}" 556 ) 557 else: 558 s = f"# vertices in shape(id: {shape.id}): " f"{len(shape.vertices)}" 559 lines.append(s) 560 graph = self.as_graph(dist_tol=dist_tol, n_round=n_round).nx_graph 561 562 for island in nx.connected_components(graph): 563 lines.append(f"Island: {island}") 564 if is_cycle(graph, island): 565 lines.append(f"Cycle: {len(island)} nodes") 566 elif is_open_walk(graph, island): 567 lines.append(f"Open Walk: {len(island)} nodes") 568 else: 569 degens = [node for node in island if graph.degree(node) > 2] 570 degrees = f"{[(node, graph.degree(node)) for node in degens]}" 571 lines.append(f"Degenerate: {len(island)} nodes") 572 lines.append(f"(Node, Degree): {degrees}") 573 lines.append("-" * 40) 574 575 return "\n".join(lines) 576 577 def _merge_collinears(self, edges, n_round=2): 578 """Merge collinear edges in the batch. 579 580 Args: 581 d_node_id_coords (dict): The node coordinates. 582 edges (list): The edges to merge. 583 tol (float, optional): The tolerance for merging. Defaults to None. 584 rtol (float, optional): The relative tolerance. Defaults to None. 585 atol (float, optional): The absolute tolerance. Defaults to None. 586 587 Returns: 588 list: The merged edges. 589 """ 590 return _merge_collinears(self, edges, n_round=n_round) 591 592 def merge_shapes( 593 self, dist_tol: float = None, n_round: int = None) -> Self: 594 """Merges the shapes in the batch if they are connected. 595 Returns a new batch with the merged shapes as well as the shapes 596 as well as the shapes that could not be merged. 597 598 Args: 599 tol (float, optional): The tolerance for merging shapes. Defaults to None. 600 rtol (float, optional): The relative tolerance. Defaults to None. 601 atol (float, optional): The absolute tolerance. Defaults to None. 602 603 Returns: 604 Self: The batch object with merged shapes. 605 """ 606 return _merge_shapes(self, dist_tol=dist_tol, n_round=n_round) 607 608 def _get_edges_and_segments(self, dist_tol: float = None, n_round: int = None): 609 """Get the edges and segments for the batch. 610 611 Args: 612 dist_tol (float, optional): The distance tolerance for proximity. Defaults to None. 613 n_round (int, optional): The number of decimal places to round to. Defaults to None. 614 615 Returns: 616 tuple: A tuple containing the edges and segments. 617 """ 618 if dist_tol is None: 619 dist_tol = defaults["dist_tol"] 620 if n_round is None: 621 n_round = defaults["n_round"] 622 d_coord_node = self.d_coord_node 623 segments = self.all_segments 624 segments = [round_segment(segment, n_round) for segment in segments] 625 edges = [] 626 for seg in segments: 627 p1, p2 = seg 628 id1 = d_coord_node[p1] 629 id2 = d_coord_node[p2] 630 edges.append((id1, id2)) 631 632 return edges, segments 633 634 def _set_node_dictionaries(self, coords: List[Point], n_round: int=2) -> List[Dict]: 635 '''Set dictionaries for nodes and coordinates. 636 d_node_coord: Dictionary of node id to coordinates. 637 d_coord_node: Dictionary of coordinates to node id. 638 639 Args: 640 nodes (List[Point]): List of vertices. 641 n_round (int, optional): Number of rounding digits. Defaults to 2. 642 ''' 643 644 coords = [tuple(round_point(coord, n_round)) for coord in coords] 645 coords = list(set(coords)) # remove duplicates 646 coords.sort() # sort by x coordinates 647 coords.sort(key=lambda x: x[1]) # sort by y coordinates 648 649 d_node_coord = {} 650 d_coord_node = {} 651 652 for i, coord in enumerate(coords): 653 d_node_coord[i] = coord 654 d_coord_node[coord] = i 655 656 self.d_node_coord = d_node_coord 657 self.d_coord_node = d_coord_node 658 659 def all_polygons(self, dist_tol: float = None) -> list: 660 """Return a list of all polygons in the batch in their 661 transformed positions. 662 663 Args: 664 dist_tol (float, optional): The distance tolerance for proximity. Defaults to None. 665 666 Returns: 667 list: A list of all polygons in the batch. 668 """ 669 if dist_tol is None: 670 dist_tol = defaults["dist_tol"] 671 exclude = [] 672 include = [] 673 for shape in self.all_shapes: 674 if len(shape.primary_points) > 2 and shape.closed: 675 vertices = shape.vertices 676 exclude.append(vertices) 677 else: 678 include.append(shape) 679 polylines = [] 680 for element in include: 681 points = element.vertices 682 points = fix_degen_points(points, dist_tol=dist_tol, closed=element.closed) 683 polylines.append(points) 684 fixed_polylines = [] 685 if polylines: 686 for polyline in polylines: 687 fixed_polylines.append( 688 fix_degen_points(polyline, dist_tol=dist_tol, closed=True) 689 ) 690 polygons = get_polygons(fixed_polylines, dist_tol=dist_tol) 691 res = polygons + exclude 692 else: 693 res = exclude 694 return res 695 696 def copy(self) -> "Batch": 697 """Returns a copy of the batch. 698 699 Returns: 700 Batch: A copy of the batch. 701 """ 702 b = Batch(modifiers=self.modifiers) 703 if self.elements: 704 b.elements = [elem.copy() for elem in self.elements] 705 else: 706 b.elements = [] 707 custom_attribs = custom_batch_attributes(self) 708 for attrib in custom_attribs: 709 setattr(b, attrib, getattr(self, attrib)) 710 return b 711 712 @property 713 def b_box(self): 714 """Returns the bounding box of the batch. 715 716 Returns: 717 BoundingBox: The bounding box of the batch. 718 """ 719 xy_list = [] 720 for elem in self.elements: 721 if hasattr(elem, "b_box"): 722 xy_list.extend(elem.b_box.corners) # To do: we should eliminate this. Just add all points. 723 # To do: memoize the bounding box 724 return bounding_box(array(xy_list)) 725 726 def _modify(self, modifier): 727 """Apply a modifier to the batch. 728 729 Args: 730 modifier (Modifier): The modifier to apply. 731 """ 732 modifier.apply() 733 734 def _update(self, xform_matrix, reps: int = 0): 735 """Updates the batch with the given transformation matrix. 736 If reps is 0, the transformation is applied to all elements. 737 If reps is greater than 0, the transformation creates 738 new elements with the transformed xform_matrix. 739 740 Args: 741 xform_matrix (ndarray): The transformation matrix. 742 reps (int, optional): The number of repetitions. Defaults to 0. 743 """ 744 if reps == 0: 745 for element in self.elements: 746 element._update(xform_matrix, reps=0) 747 if self.modifiers: 748 for modifier in self.modifiers: 749 modifier.apply(element) 750 else: 751 elements = self.elements[:] 752 new = [] 753 for _ in range(reps): 754 for element in elements: 755 new_element = element.copy() 756 new_element._update(xform_matrix) 757 self.elements.append(new_element) 758 new.append(new_element) 759 if self.modifiers: 760 for modifier in self.modifiers: 761 modifier.apply(new_element) 762 elements = new[:] 763 new = [] 764 return self
A Batch object is a collection of other objects (Batch, Shape, and Tag objects). It can be used to apply a transformation to all the objects in the Batch. It is used for creating 1D and 2D patterns of objects. all_vertices, all_elements, etc. means a flat list of the specified object gathered recursively from all the elements in the Batch.
44 def __init__( 45 self, 46 elements: Sequence[Any] = None, 47 modifiers: Sequence["Modifier"] = None, 48 subtype: Types = Types.BATCH, 49 **kwargs, 50 ): 51 """ 52 Initialize a Batch object. 53 54 Args: 55 elements (Sequence[Any], optional): The elements to include in the batch. 56 modifiers (Sequence[Modifier], optional): The modifiers to apply to the batch. 57 subtype (Types, optional): The subtype of the batch. 58 kwargs (dict): Additional keyword arguments. 59 """ 60 validate_args(kwargs, batch_args) 61 if elements and not isinstance(elements, (list, tuple)): 62 self.elements = [elements] 63 else: 64 self.elements = elements if elements is not None else [] 65 self.type = Types.BATCH 66 self.subtype = get_enum_value(Types, subtype) 67 self.modifiers = modifiers 68 self.blend_mode = None 69 self.alpha = None 70 self.line_alpha = None 71 self.fill_alpha = None 72 self.text_alpha = None 73 self.clip = False # if clip is True, the batch.mask is used as a clip path 74 self.mask = None 75 self.even_odd_rule = False 76 self.blend_group = False 77 self.transparency_group = False 78 common_properties(self) 79 for key, value in kwargs.items(): 80 setattr(self, key, value)
Initialize a Batch object.
Arguments:
- elements (Sequence[Any], optional): The elements to include in the batch.
- modifiers (Sequence[Modifier], optional): The modifiers to apply to the batch.
- subtype (Types, optional): The subtype of the batch.
- kwargs (dict): Additional keyword arguments.
82 def set_attribs(self, attrib, value): 83 """ 84 Sets the attribute to the given value for all elements in the batch if it is applicable. 85 86 Args: 87 attrib (str): The attribute to set. 88 value (Any): The value to set the attribute to. 89 """ 90 for element in self.elements: 91 if element.type == Types.BATCH: 92 setattr(element, attrib, value) 93 elif hasattr(element, attrib): 94 setattr(element, attrib, value)
Sets the attribute to the given value for all elements in the batch if it is applicable.
Arguments:
- attrib (str): The attribute to set.
- value (Any): The value to set the attribute to.
96 def set_batch_attr(self, attrib: str, value: Any) -> Self: 97 """ 98 Sets the attribute to the given value for the batch itself. 99 batch.attrib = value would set the attribute to the elements 100 of the batch object but not the batch itself. 101 102 Args: 103 attrib (str): The attribute to set. 104 value (Any): The value to set the attribute to. 105 106 Returns: 107 Self: The batch object. 108 """ 109 self.__dict__[attrib] = value
Sets the attribute to the given value for the batch itself. batch.attrib = value would set the attribute to the elements of the batch object but not the batch itself.
Arguments:
- attrib (str): The attribute to set.
- value (Any): The value to set the attribute to.
Returns:
Self: The batch object.
238 def proximity(self, dist_tol: float = None, n: int = 5) -> list[Point]: 239 """ 240 Returns the n closest points in the batch. 241 242 Args: 243 dist_tol (float, optional): The distance tolerance for proximity. 244 n (int, optional): The number of closest points to return. 245 246 Returns: 247 list[Point]: The n closest points in the batch. 248 """ 249 if dist_tol is None: 250 dist_tol = defaults["dist_tol"] 251 vertices = self.all_vertices 252 vertices = [(*v, i) for i, v in enumerate(vertices)] 253 _, pairs = all_close_points(vertices, dist_tol=dist_tol, with_dist=True) 254 return [pair for pair in pairs if pair[2] > 0][:n]
Returns the n closest points in the batch.
Arguments:
- dist_tol (float, optional): The distance tolerance for proximity.
- n (int, optional): The number of closest points to return.
Returns:
list[Point]: The n closest points in the batch.
256 def append(self, element: Any) -> Self: 257 """ 258 Appends the element to the batch. 259 260 Args: 261 element (Any): The element to append. 262 263 Returns: 264 Self: The batch object. 265 """ 266 if element not in self.elements: 267 self.elements.append(element) 268 return self
Appends the element to the batch.
Arguments:
- element (Any): The element to append.
Returns:
Self: The batch object.
270 def reverse(self) -> Self: 271 """ 272 Reverses the order of the elements in the batch. 273 274 Returns: 275 Self: The batch object. 276 """ 277 self.elements = self.elements[::-1] 278 return self
Reverses the order of the elements in the batch.
Returns:
Self: The batch object.
280 def insert(self, index, element: Any) -> Self: 281 """ 282 Inserts the element at the given index. 283 284 Args: 285 index (int): The index to insert the element at. 286 element (Any): The element to insert. 287 288 Returns: 289 Self: The batch object. 290 """ 291 if element not in self.elements: 292 self.elements.insert(index, element) 293 294 return self
Inserts the element at the given index.
Arguments:
- index (int): The index to insert the element at.
- element (Any): The element to insert.
Returns:
Self: The batch object.
296 def remove(self, element: Any) -> Self: 297 """ 298 Removes the element from the batch. 299 300 Args: 301 element (Any): The element to remove. 302 303 Returns: 304 Self: The batch object. 305 """ 306 if element in self.elements: 307 self.elements.remove(element) 308 return self
Removes the element from the batch.
Arguments:
- element (Any): The element to remove.
Returns:
Self: The batch object.
310 def pop(self, index: int) -> Any: 311 """ 312 Removes the element at the given index and returns it. 313 314 Args: 315 index (int): The index to remove the element from. 316 317 Returns: 318 Any: The removed element. 319 """ 320 return self.elements.pop(index)
Removes the element at the given index and returns it.
Arguments:
- index (int): The index to remove the element from.
Returns:
Any: The removed element.
322 def clear(self) -> Self: 323 """ 324 Removes all elements from the batch. 325 326 Returns: 327 Self: The batch object. 328 """ 329 self.elements = [] 330 return self
Removes all elements from the batch.
Returns:
Self: The batch object.
332 def extend(self, elements: Sequence[Any]) -> Self: 333 """ 334 Extends the batch with the given elements. 335 336 Args: 337 elements (Sequence[Any]): The elements to extend the batch with. 338 339 Returns: 340 Self: The batch object. 341 """ 342 for element in elements: 343 if element not in self.elements: 344 self.elements.append(element) 345 346 return self
Extends the batch with the given elements.
Arguments:
- elements (Sequence[Any]): The elements to extend the batch with.
Returns:
Self: The batch object.
348 def iter_elements(self, element_type: Types = None) -> Iterator: 349 """Iterate over all elements in the batch, including the elements 350 in the nested batches. 351 352 Args: 353 element_type (Types, optional): The type of elements to iterate over. Defaults to None. 354 355 Returns: 356 Iterator: An iterator over the elements in the batch. 357 """ 358 for elem in self.elements: 359 if elem.type == Types.BATCH: 360 yield from elem.iter_elements(element_type) 361 else: 362 if element_type is None: 363 yield elem 364 elif elem.type == element_type: 365 yield elem
Iterate over all elements in the batch, including the elements in the nested batches.
Arguments:
- element_type (Types, optional): The type of elements to iterate over. Defaults to None.
Returns:
Iterator: An iterator over the elements in the batch.
367 @property 368 def all_elements(self) -> list[Any]: 369 """Return a list of all elements in the batch, 370 including the elements in the nested batches. 371 372 Returns: 373 list[Any]: A list of all elements in the batch. 374 """ 375 elements = [] 376 for elem in self.elements: 377 if elem.type == Types.BATCH: 378 elements.extend(elem.all_elements) 379 else: 380 elements.append(elem) 381 return elements
Return a list of all elements in the batch, including the elements in the nested batches.
Returns:
list[Any]: A list of all elements in the batch.
383 @property 384 def all_shapes(self) -> list["Shape"]: 385 """Return a list of all shapes in the batch. 386 387 Returns: 388 list[Shape]: A list of all shapes in the batch. 389 """ 390 elements = self.all_elements 391 shapes = [] 392 for element in elements: 393 if element.type == Types.SHAPE: 394 shapes.append(element) 395 return shapes
Return a list of all shapes in the batch.
Returns:
list[Shape]: A list of all shapes in the batch.
397 @property 398 def all_vertices(self) -> list[Point]: 399 """Return a list of all points in the batch in their 400 transformed positions. 401 402 Returns: 403 list[Point]: A list of all points in the batch in their transformed positions. 404 """ 405 elements = self.all_elements 406 vertices = [] 407 for element in elements: 408 if element.type == Types.SHAPE: 409 vertices.extend(element.vertices) 410 elif element.type == Types.BATCH: 411 vertices.extend(element.all_vertices) 412 return vertices
Return a list of all points in the batch in their transformed positions.
Returns:
list[Point]: A list of all points in the batch in their transformed positions.
414 @property 415 def all_segments(self) -> list[Line]: 416 """Return a list of all segments in the batch. 417 418 Returns: 419 list[Line]: A list of all segments in the batch. 420 """ 421 elements = self.all_elements 422 segments = [] 423 for element in elements: 424 if element.type == Types.SHAPE: 425 segments.extend(element.vertex_pairs) 426 return segments
Return a list of all segments in the batch.
Returns:
list[Line]: A list of all segments in the batch.
485 def as_graph( 486 self, 487 directed: bool = False, 488 weighted: bool = False, 489 dist_tol: float = None, 490 atol=None, 491 n_round: int = None, 492 ) -> Graph: 493 """Return the batch as a Graph object. 494 Graph.nx is the networkx graph. 495 496 Args: 497 directed (bool, optional): Whether the graph is directed. Defaults to False. 498 weighted (bool, optional): Whether the graph is weighted. Defaults to False. 499 dist_tol (float, optional): The distance tolerance for proximity. Defaults to None. 500 atol (optional): The absolute tolerance. Defaults to None. 501 n_round (int, optional): The number of decimal places to round to. Defaults to None. 502 503 Returns: 504 Graph: The batch as a Graph object. 505 """ 506 _set_Nones(self, ["dist_tol", "atol", "n_round"], [dist_tol, atol, n_round]) 507 d_node_id_coords, edges = self._get_graph_nodes_and_edges(dist_tol, n_round) 508 if directed: 509 nx_graph = nx.DiGraph() 510 graph_type = Types.DIRECTED 511 else: 512 nx_graph = nx.Graph() 513 graph_type = Types.UNDIRECTED 514 515 for id_, coords in d_node_id_coords.items(): 516 nx_graph.add_node(id_, pos=coords) 517 518 if weighted: 519 for edge in edges: 520 p1 = d_node_id_coords[edge[0]] 521 p2 = d_node_id_coords[edge[1]] 522 nx_graph.add_edge(edge[0], edge[1], weight=distance(p1, p2)) 523 subtype = Types.WEIGHTED 524 else: 525 nx_graph.update(edges) 526 subtype = Types.NONE 527 528 graph = Graph(type=graph_type, subtype=subtype, nx_graph=nx_graph) 529 return graph
Return the batch as a Graph object. Graph.nx is the networkx graph.
Arguments:
- directed (bool, optional): Whether the graph is directed. Defaults to False.
- weighted (bool, optional): Whether the graph is weighted. Defaults to False.
- dist_tol (float, optional): The distance tolerance for proximity. Defaults to None.
- atol (optional): The absolute tolerance. Defaults to None.
- n_round (int, optional): The number of decimal places to round to. Defaults to None.
Returns:
Graph: The batch as a Graph object.
531 def graph_summary(self, dist_tol: float = None, n_round: int = None) -> str: 532 """Returns a representation of the Batch object as a graph. 533 534 Args: 535 dist_tol (float, optional): The distance tolerance for proximity. Defaults to None. 536 n_round (int, optional): The number of decimal places to round to. Defaults to None. 537 538 Returns: 539 str: A representation of the Batch object as a graph. 540 """ 541 if dist_tol is None: 542 dist_tol = defaults["dist_tol"] 543 if n_round is None: 544 n_round = defaults["n_round"] 545 all_shapes = self.all_shapes 546 all_vertices = self.all_vertices 547 lines = [] 548 lines.append("Batch summary:") 549 lines.append(f"# shapes: {len(all_shapes)}") 550 lines.append(f"# vertices: {len(all_vertices)}") 551 for shape in self.all_shapes: 552 if shape.subtype: 553 s = ( 554 f"# vertices in shape(id: {shape.id}, subtype: " 555 f"{shape.subtype}): {len(shape.vertices)}" 556 ) 557 else: 558 s = f"# vertices in shape(id: {shape.id}): " f"{len(shape.vertices)}" 559 lines.append(s) 560 graph = self.as_graph(dist_tol=dist_tol, n_round=n_round).nx_graph 561 562 for island in nx.connected_components(graph): 563 lines.append(f"Island: {island}") 564 if is_cycle(graph, island): 565 lines.append(f"Cycle: {len(island)} nodes") 566 elif is_open_walk(graph, island): 567 lines.append(f"Open Walk: {len(island)} nodes") 568 else: 569 degens = [node for node in island if graph.degree(node) > 2] 570 degrees = f"{[(node, graph.degree(node)) for node in degens]}" 571 lines.append(f"Degenerate: {len(island)} nodes") 572 lines.append(f"(Node, Degree): {degrees}") 573 lines.append("-" * 40) 574 575 return "\n".join(lines)
Returns a representation of the Batch object as a graph.
Arguments:
- dist_tol (float, optional): The distance tolerance for proximity. Defaults to None.
- n_round (int, optional): The number of decimal places to round to. Defaults to None.
Returns:
str: A representation of the Batch object as a graph.
592 def merge_shapes( 593 self, dist_tol: float = None, n_round: int = None) -> Self: 594 """Merges the shapes in the batch if they are connected. 595 Returns a new batch with the merged shapes as well as the shapes 596 as well as the shapes that could not be merged. 597 598 Args: 599 tol (float, optional): The tolerance for merging shapes. Defaults to None. 600 rtol (float, optional): The relative tolerance. Defaults to None. 601 atol (float, optional): The absolute tolerance. Defaults to None. 602 603 Returns: 604 Self: The batch object with merged shapes. 605 """ 606 return _merge_shapes(self, dist_tol=dist_tol, n_round=n_round)
Merges the shapes in the batch if they are connected. Returns a new batch with the merged shapes as well as the shapes as well as the shapes that could not be merged.
Arguments:
- tol (float, optional): The tolerance for merging shapes. Defaults to None.
- rtol (float, optional): The relative tolerance. Defaults to None.
- atol (float, optional): The absolute tolerance. Defaults to None.
Returns:
Self: The batch object with merged shapes.
659 def all_polygons(self, dist_tol: float = None) -> list: 660 """Return a list of all polygons in the batch in their 661 transformed positions. 662 663 Args: 664 dist_tol (float, optional): The distance tolerance for proximity. Defaults to None. 665 666 Returns: 667 list: A list of all polygons in the batch. 668 """ 669 if dist_tol is None: 670 dist_tol = defaults["dist_tol"] 671 exclude = [] 672 include = [] 673 for shape in self.all_shapes: 674 if len(shape.primary_points) > 2 and shape.closed: 675 vertices = shape.vertices 676 exclude.append(vertices) 677 else: 678 include.append(shape) 679 polylines = [] 680 for element in include: 681 points = element.vertices 682 points = fix_degen_points(points, dist_tol=dist_tol, closed=element.closed) 683 polylines.append(points) 684 fixed_polylines = [] 685 if polylines: 686 for polyline in polylines: 687 fixed_polylines.append( 688 fix_degen_points(polyline, dist_tol=dist_tol, closed=True) 689 ) 690 polygons = get_polygons(fixed_polylines, dist_tol=dist_tol) 691 res = polygons + exclude 692 else: 693 res = exclude 694 return res
Return a list of all polygons in the batch in their transformed positions.
Arguments:
- dist_tol (float, optional): The distance tolerance for proximity. Defaults to None.
Returns:
list: A list of all polygons in the batch.
696 def copy(self) -> "Batch": 697 """Returns a copy of the batch. 698 699 Returns: 700 Batch: A copy of the batch. 701 """ 702 b = Batch(modifiers=self.modifiers) 703 if self.elements: 704 b.elements = [elem.copy() for elem in self.elements] 705 else: 706 b.elements = [] 707 custom_attribs = custom_batch_attributes(self) 708 for attrib in custom_attribs: 709 setattr(b, attrib, getattr(self, attrib)) 710 return b
Returns a copy of the batch.
Returns:
Batch: A copy of the batch.
712 @property 713 def b_box(self): 714 """Returns the bounding box of the batch. 715 716 Returns: 717 BoundingBox: The bounding box of the batch. 718 """ 719 xy_list = [] 720 for elem in self.elements: 721 if hasattr(elem, "b_box"): 722 xy_list.extend(elem.b_box.corners) # To do: we should eliminate this. Just add all points. 723 # To do: memoize the bounding box 724 return bounding_box(array(xy_list))
Returns the bounding box of the batch.
Returns:
BoundingBox: The bounding box of the batch.
767def custom_batch_attributes(item: Batch) -> List[str]: 768 """ 769 Return a list of custom attributes of a Shape or 770 Batch instance. 771 772 Args: 773 item (Batch): The batch object. 774 775 Returns: 776 List[str]: A list of custom attributes. 777 """ 778 from .shape import Shape 779 780 if isinstance(item, Batch): 781 dummy_shape = Shape([(0, 0), (1, 0)]) 782 dummy = Batch([dummy_shape]) 783 else: 784 raise TypeError("Invalid item type") 785 native_attribs = set(dir(dummy)) 786 custom_attribs = set(dir(item)) - native_attribs 787 788 return list(custom_attribs)
Return a list of custom attributes of a Shape or Batch instance.
Arguments:
- item (Batch): The batch object.
Returns:
List[str]: A list of custom attributes.