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)
class Batch(simetri.graphics.core.Base):
 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.

Batch( elements: Sequence[Any] = None, modifiers: Sequence[ForwardRef('Modifier')] = None, subtype: simetri.graphics.all_enums.Types = <Types.BATCH: 'BATCH'>, **kwargs)
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.
type
subtype
modifiers
blend_mode
alpha
line_alpha
fill_alpha
text_alpha
clip
mask
even_odd_rule
blend_group
transparency_group
def set_attribs(self, attrib, value):
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.
def set_batch_attr(self, attrib: str, value: Any) -> typing_extensions.Self:
 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.

def proximity(self, dist_tol: float = None, n: int = 5) -> list[typing.Sequence[float]]:
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.

def append(self, element: Any) -> typing_extensions.Self:
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.

def reverse(self) -> typing_extensions.Self:
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.

def insert(self, index, element: Any) -> typing_extensions.Self:
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.

def remove(self, element: Any) -> typing_extensions.Self:
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.

def pop(self, index: int) -> Any:
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.

def clear(self) -> typing_extensions.Self:
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.

def extend(self, elements: Sequence[Any]) -> typing_extensions.Self:
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.

def iter_elements(self, element_type: simetri.graphics.all_enums.Types = None) -> Iterator:
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.

all_elements: list[typing.Any]
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.

all_shapes: list['Shape']
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.

all_vertices: list[typing.Sequence[float]]
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.

all_segments: list[typing.Sequence[typing.Sequence]]
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.

def as_graph( self, directed: bool = False, weighted: bool = False, dist_tol: float = None, atol=None, n_round: int = None) -> simetri.helpers.graph.Graph:
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.

def graph_summary(self, dist_tol: float = None, n_round: int = None) -> str:
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.

def merge_shapes( self, dist_tol: float = None, n_round: int = None) -> typing_extensions.Self:
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.

def all_polygons(self, dist_tol: float = None) -> list:
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.

def copy(self) -> 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.

b_box
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.

def custom_batch_attributes(item: Batch) -> List[str]:
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.