simetri.lace.lace
Simetri library's interlace objects.
1"""Simetri library's interlace objects.""" 2 3from itertools import combinations 4from typing import Iterator, List, Any, Union 5from collections import OrderedDict 6 7import networkx as nx 8import numpy as np 9 10from numpy import isclose 11 12from ..graphics.shape import Shape, custom_attributes 13from ..graphics.batch import Batch 14 15from ..colors import colors 16from ..geometry.geometry import ( 17 connected_pairs, 18 polygon_area, 19 distance, 20 double_offset_polygons, 21 get_polygons, 22 double_offset_polylines, 23 intersection2, 24 right_handed, 25 polygon_cg, 26 round_point, 27 offset_polygon, 28 offset_polygon_points, 29 convex_hull, 30 close_points2, 31 polygon_center, 32) 33from ..helpers.graph import get_cycles 34from ..graphics.common import get_defaults, common_properties 35from ..graphics.all_enums import Types, Connection 36from ..canvas.style_map import shape_style_map, ShapeStyle 37from ..canvas.canvas import Canvas 38from ..settings.settings import defaults 39from ..helpers.utilities import flatten, group_into_bins 40from ..helpers.validation import validate_args 41 42 43def _set_style(obj: Any, attribs): 44 for attr in attribs: 45 setattr(obj, attr, getattr(defaults["style"], attr)) 46 47 48array = np.array 49 50 51# Lace (Batch) 52# parallel_polyline_list (list) 53# parallel_polyline1 (ParallelPolyline-Batch-PARALLELPOLYLINE) 54# polyline_list (list) 55# | polyline1 (Polyline-Shape) 56# | divisions (list) 57# | division1 (Division-Shape) 58# | p1 (tuple) 59# | p2 (tuple) 60# | sections (list) 61# | section1(Section-Shape) 62# | p1 (tuple) 63# | p2 64# | is_overlap (bool) 65# | overlap (Overlap-Batch) 66# |overlaps (list) 67# | overlap1(Overlap-Batch) 68# | divisions (list) 69# | division1(Division-Shape) 70# | p1 (tuple) 71# | p2 (tuple) 72# | sections (list) 73# | section1(Section-Shape) 74# | start (Intersection-Shape) 75# | end (Intersection-Shape) 76# | overlap 77# | 78# | 79# |fragments 80# fragment1(Shape) 81# divisions 82# division1(Shape) 83# p1 84# p2 85# sections 86# section1 87 88# plaits (list) 89# plait1 (Plait-Shape) 90 91# All objects in this module is a subclass of the Shape or Batch class. 92# They are used to compute the interlacing patterns. 93 94# Example of a Lace object. 95 96# /\ 97# //\\ /\ 98# // \//\\ 99# // /\\ \\ 100# // // \\ \\ 101# \\ \\ // // 102# \\ \// // 103# \\ //\\// 104# \\// \/ 105# \/ 106 107# Example of a ParallelPolylines object. The lace object above has two 108# ParallelPolylines objects. Main polylines are not shown, they are 109# located in the middle of the offset polylines. 110 111# /\ 112# //\\ 113# // \\ 114# // \\ 115# \\ // 116# \\ // 117# \\// 118# \/ 119 120# Example of a Fragment object. 121# They are polygons or polylines. 122# The lace object above has three fragments. 123# This is the middle fragment. 124 125# /\ 126# / \ 127# \ / 128# \/ 129 130# Example of a plait object. They are polylines. 131# Used for drawing under/over interlacing. 132 133# /\ 134# \ \ 135# \ \ 136# / / 137# / / 138# \/ 139 140# Example of an Overlap object. 141# The lace object above has two overlap regions. 142 143# /\ 144# \/ 145 146# Example of a Polyline object. 147 148# /\ 149# / \ 150# / \ 151# / \ 152# / \ 153# \ / 154# \ / 155# \ / 156# \ / 157# \/ 158 159# Example of an Division object. 160# Each polyline is made up of one or more "Division" objects. 161# Each division is divided into sections 162# Maybe "Divisions" would be a better name instead of "Division"? 163# * 164# \ 165# * 166# \ 167# * 168# \ 169# * 170# Example of a Section object. 171# Each division is made up of one or more sections. 172# Sections have intersection points at their ends. 173# * 174# \ 175# * 176# * intersections have a point, division1, division2 attributes. 177# """ 178 179class Intersection(Shape): 180 """Intersection of two divisions. They are at the endpoints of Section 181 objects. A division can have multiple sections and multiple 182 intersections. They can be located at the end of a division. 183 184 Args: 185 point (tuple): (x, y) coordinates of the intersection point. 186 division1 (Division): First division. 187 division2 (Division, optional): Second division. Defaults to None. 188 endpoint (bool, optional): If the intersection is at the end of a division, then endpoint is True. Defaults to False. 189 **kwargs: Additional attributes for cosmetic/drawing purposes. 190 """ 191 192 def __init__(self, point: tuple, division1: "Division", division2: "Division" = None, endpoint: bool = False, **kwargs) -> None: 193 super().__init__([point], xform_matrix=None, subtype=Types.INTERSECTION, **kwargs) 194 self._point = point 195 self.division1 = division1 196 self.division2 = division2 197 self.overlap = None 198 self.endpoint = endpoint 199 self.division = None # used for fragment divisions' DCEL structure 200 201 common_properties(self, id_only=True) 202 203 def _update(self, xform_matrix: array, reps=0): 204 """Update the transformation matrix of the intersection. 205 206 Args: 207 xform_matrix (array): Transformation matrix. 208 reps (int, optional): Number of repetitions. Defaults to 0. 209 210 Returns: 211 Any: Updated intersection or list of updated intersections. 212 """ 213 if reps == 0: 214 self.xform_matrix = self.xform_matrix @ xform_matrix 215 res = self 216 else: 217 res = [] 218 for _ in range(reps): 219 shape = self.copy() 220 shape._update(xform_matrix) 221 res.append(shape) 222 223 return res 224 225 def copy(self): 226 """Create a copy of the intersection. 227 228 Returns: 229 Intersection: A copy of the intersection. 230 """ 231 intersection = Intersection(self.point, self.division1, self.division2) 232 for attrib in shape_style_map: 233 setattr(intersection, attrib, getattr(self, attrib)) 234 custom_attribs = custom_attributes(self) 235 for attrib in custom_attribs: 236 setattr(intersection, attrib, getattr(self, attrib)) 237 return intersection 238 239 @property 240 def point(self): 241 """Return the intersection point. 242 243 Returns: 244 list: Intersection point coordinates. 245 """ 246 return list(np.array([*self._point, 1.0]) @ self.xform_matrix)[:2] 247 248 def __str__(self): 249 """String representation of the intersection. 250 251 Returns: 252 str: String representation. 253 """ 254 return ( 255 f"Intersection({[round(x, defaults['n_round']) for x in self.point]}, " 256 f"{tuple(list([self.division1, self.division2]))}" 257 ) 258 259 def __repr__(self): 260 """String representation of the intersection. 261 262 Returns: 263 str: String representation. 264 """ 265 return str(self) 266 267 def __eq__(self, other): 268 """Check if two intersections are equal. 269 270 Args: 271 other (Intersection): Another intersection. 272 273 Returns: 274 bool: True if equal, False otherwise. 275 """ 276 return close_points2(self.point, other.point, dist2=defaults["dist_tol"] ** 2) 277 278 279class Partition(Shape): 280 """These are the polygons of the non-interlaced geometry. 281 Fragments and partitions are scaled versions of each other. 282 283 Args: 284 points (list): List of points defining the partition. 285 **kwargs: Additional attributes for cosmetic/drawing purposes. 286 """ 287 288 def __init__(self, points, **kwargs): 289 super().__init__(points, **kwargs) 290 self.subtype = Types.PART 291 self.area = polygon_area(self.vertices) 292 self.CG = polygon_cg(self.vertices) 293 common_properties(self) 294 295 def __str__(self): 296 """String representation of the partition. 297 298 Returns: 299 str: String representation. 300 """ 301 return f"Part({self.vertices})" 302 303 def __repr__(self): 304 """String representation of the partition. 305 306 Returns: 307 str: String representation. 308 """ 309 return self.__str__() 310 311 312class Fragment(Shape): 313 """A Fragment is a collection of section objects that are connected 314 to each other. These sections are already defined. They belong to 315 the polyline objects in a lace. Fragments can be open or closed. 316 They are created by the lace object. 317 318 Args: 319 points (list): List of points defining the fragment. 320 **kwargs: Additional attributes for cosmetic/drawing purposes. 321 """ 322 323 def __init__(self, points, **kwargs): 324 super().__init__(points, **kwargs) 325 self.subtype = Types.FRAGMENT 326 self.area = polygon_area(self.vertices) 327 self.sections = [] 328 self.intersections = [] 329 self.inner_lines = [] 330 self._divisions = [] 331 self.CG = polygon_cg(self.vertices) 332 common_properties(self) 333 334 def __str__(self): 335 """String representation of the fragment. 336 337 Returns: 338 str: String representation. 339 """ 340 return f"Fragment({self.vertices})" 341 342 def __repr__(self): 343 """String representation of the fragment. 344 345 Returns: 346 str: String representation. 347 """ 348 return self.__str__() 349 350 @property 351 def divisions(self): 352 """Return the divisions of the fragment. 353 354 Returns: 355 list: List of divisions. 356 """ 357 return self._divisions 358 359 @property 360 def center(self): 361 """Return the center of the fragment. 362 363 Returns: 364 list: Center coordinates. 365 """ 366 return self.CG 367 368 def _set_divisions(self, dist_tol=None): 369 if dist_tol is None: 370 dist_tol = defaults["dist_tol"] 371 dist_tol2 = dist_tol * dist_tol # squared distance tolerance 372 d_points__section = {} 373 for section in self.sections: 374 start = section.start.point 375 end = section.end.point 376 start = round(start[0], 2), round(start[1], 2) 377 end = round(end[0], 2), round(end[1], 2) 378 d_points__section[(start, end)] = section 379 d_points__section[(end, start)] = section 380 381 coord_pairs = connected_pairs(self.vertices) 382 self._divisions = [] 383 for pair in coord_pairs: 384 x1, y1 = round_point(pair[0]) 385 x2, y2 = round_point(pair[1]) 386 division = Division((x1, y1), (x2, y2)) 387 division.section = d_points__section[((x1, y1), (x2, y2))] 388 division.fragment = self 389 start_point = round_point(division.section.start.point) 390 end_point = round_point(division.section.end.point) 391 if close_points2(start_point, (x1, y1), dist2=dist_tol2): 392 division.intersections = [division.section.start, division.section.end] 393 division.section.start.division = division 394 elif close_points2(end_point, (x1, y1), dist2=dist_tol2): 395 division.intersections = [division.section.end, division.section.start] 396 division.section.end.division = division 397 else: 398 raise ValueError("Division does not match section") 399 self._divisions.append(division) 400 n = len(self._divisions) 401 for i, division in enumerate(self._divisions): 402 division.prev = self._divisions[i - 1] 403 division.next = self._divisions[(i + 1) % n] 404 405 def _set_twin_divisions(self): 406 for division in self.divisions: 407 section = division.section 408 if section.twin and section.twin.fragment: 409 twin_fragment = section.twin.fragment 410 distances = [] 411 for _, division2 in enumerate(twin_fragment.divisions): 412 dist = distance( 413 division.section.mid_point, division2.section.mid_point 414 ) 415 distances.append((dist, division2)) 416 distances.sort(key=lambda x: x[0]) 417 division.twin = distances[0][1] 418 419 420class Section(Shape): 421 """A section is a line segment between two intersections. 422 A division can have multiple sections. Sections are used to 423 draw the over/under plaits. 424 425 Args: 426 start (Intersection): Start intersection. 427 end (Intersection): End intersection. 428 is_overlap (bool, optional): If the section is an overlap. Defaults to False. 429 overlap (Overlap, optional): Overlap object. Defaults to None. 430 is_over (bool, optional): If the section is over. Defaults to False. 431 twin (Section, optional): Twin section. Defaults to None. 432 fragment (Fragment, optional): Fragment object. Defaults to None. 433 **kwargs: Additional attributes for cosmetic/drawing purposes. 434 """ 435 436 def __init__( 437 self, 438 start: Intersection = None, 439 end: Intersection = None, 440 is_overlap: bool = False, 441 overlap: "Overlap" = None, 442 is_over: bool = False, 443 twin: "Section" = None, 444 fragment: "Fragment" = None, 445 **kwargs, 446 ): 447 super().__init__([start.point, end.point], subtype=Types.SECTION, **kwargs) 448 self.start = start 449 self.end = end 450 self.is_overlap = is_overlap 451 self.overlap = overlap 452 self.is_over = is_over 453 self.twin = twin 454 self.fragment = fragment 455 super().__init__([start.point, end.point], subtype=Types.SECTION, **kwargs) 456 self.length = distance(self.start.point, self.end.point) 457 self.mid_point = [ 458 (self.start.point[0] + self.end.point[0]) / 2, 459 (self.start.point[1] + self.end.point[1]) / 2, 460 ] 461 common_properties(self) 462 463 def copy(self): 464 """Create a copy of the section. 465 466 Returns: 467 Section: A copy of the section. 468 """ 469 overlap = self.overlap.copy() if self.overlap else None 470 start = self.start.copy() 471 end = self.end.copy() 472 section = Section(start, end, self.is_overlap, overlap, self.is_over) 473 474 return section 475 476 def end_point(self): 477 """Return the end point of the section. 478 479 Returns: 480 Intersection: End intersection. 481 """ 482 if self.start.endpoint: 483 res = self.start 484 elif self.end.endpoint: 485 res = self.end 486 else: 487 res = None 488 489 return res 490 491 def __str__(self): 492 """String representation of the section. 493 494 Returns: 495 str: String representation. 496 """ 497 return f"Section({self.start}, {self.end})" 498 499 def __repr__(self): 500 """String representation of the section. 501 502 Returns: 503 str: String representation. 504 """ 505 return self.__str__() 506 507 @property 508 def is_endpoint(self): 509 """Return True if the section is an endpoint. 510 511 Returns: 512 bool: True if endpoint, False otherwise. 513 """ 514 return self.start.endpoint or self.end.endpoint 515 516 517class Overlap(Batch): 518 """An overlap is a collection of four connected sections. 519 520 Args: 521 intersections (list[Intersection], optional): List of intersections. Defaults to None. 522 sections (list[Section], optional): List of sections. Defaults to None. 523 visited (bool, optional): If the overlap is visited. Defaults to False. 524 drawable (bool, optional): If the overlap is drawable. Defaults to True. 525 **kwargs: Additional attributes for cosmetic/drawing purposes. 526 """ 527 528 def __init__( 529 self, 530 intersections: list[Intersection] = None, 531 sections: list[Section] = None, 532 visited=False, 533 drawable=True, 534 **kwargs, 535 ): 536 self.intersections = intersections 537 self.sections = sections 538 super().__init__(sections, **kwargs) 539 self.subtype = Types.OVERLAP 540 self.visited = visited 541 self.drawable = drawable 542 common_properties(self) 543 544 def __str__(self): 545 """String representation of the overlap. 546 547 Returns: 548 str: String representation. 549 """ 550 return f"Overlap({self.id})" 551 552 def __repr__(self): 553 """String representation of the overlap. 554 555 Returns: 556 str: String representation. 557 """ 558 return f"Overlap({self.id})" 559 560 561class Division(Shape): 562 """A division is a line segment between two intersections. 563 564 Args: 565 p1 (tuple): Start point. 566 p2 (tuple): End point. 567 xform_matrix (array, optional): Transformation matrix. Defaults to None. 568 **kwargs: Additional attributes for cosmetic/drawing purposes. 569 """ 570 571 def __init__(self, p1, p2, xform_matrix=None, **kwargs): 572 super().__init__([p1, p2], subtype=Types.DIVISION, **kwargs) 573 self.p1 = p1 574 self.p2 = p2 575 self.intersections = [] 576 self.twin = None # used for fragment divisions only 577 self.section = None # used for fragment divisions only 578 self.fragment = None # used for fragment divisions only 579 self.next = None # used for fragment divisions only 580 self.prev = None # used for fragment divisions only 581 self.sections = [] 582 super().__init__( 583 [p1, p2], subtype=Types.DIVISION, xform_matrix=xform_matrix, **kwargs 584 ) 585 common_properties(self) 586 587 def _update(self, xform_matrix, reps=0): 588 """Update the transformation matrix of the division. 589 590 Args: 591 xform_matrix (array): Transformation matrix. 592 reps (int, optional): Number of repetitions. Defaults to 0. 593 594 Returns: 595 Any: Updated division or list of updated divisions. 596 """ 597 if reps == 0: 598 self.xform_matrix = self.xform_matrix @ xform_matrix 599 res = self 600 else: 601 res = [] 602 for _ in range(reps): 603 shape = self.copy() 604 shape._update(xform_matrix) 605 res.append(shape) 606 607 return res 608 609 def __str__(self): 610 """String representation of the division. 611 612 Returns: 613 str: String representation. 614 """ 615 return ( 616 f"Division(({self.p1[0]:.2f}, {self.p1[1]:.2f}), " 617 f"({self.p2[0]:.2f}, {self.p2[1]:.2f}))" 618 ) 619 620 def __repr__(self): 621 """String representation of the division. 622 623 Returns: 624 str: String representation. 625 """ 626 return ( 627 f"Division(({self.p1[0]:.2f}, {self.p1[1]:.2f}), " 628 f"({self.p2[0]:.2f}, {self.p2[1]:.2f}))" 629 ) 630 631 def copy( 632 self, 633 section: Section = None, 634 twin: Section = None, 635 ): 636 """Create a copy of the division. 637 638 Args: 639 section (Section, optional): Section object. Defaults to None. 640 twin (Section, optional): Twin section. Defaults to None. 641 642 Returns: 643 Division: A copy of the division. 644 """ 645 division = Division(self.p1[:], self.p2[:], np.copy(self.xform_matrix)) 646 for attrib in shape_style_map: 647 setattr(division, attrib, getattr(self, attrib)) 648 division.twin = twin 649 division.section = section 650 division.fragment = self.fragment 651 division.next = self.next 652 division.prev = self.prev 653 division.sections = [x.copy() for x in self.sections] 654 custom_attributes_ = custom_attributes(self) 655 for attrib in custom_attributes_: 656 setattr(division, attrib, getattr(self, attrib)) 657 return division 658 659 def _merged_sections(self): 660 """Merge sections of the division. 661 662 Returns: 663 list: List of merged sections. 664 """ 665 chains = [] 666 chain = [self.intersections[0]] 667 sections = self.sections[:] 668 for section in sections: 669 if not section.is_over: 670 if section.start.id == chain[-1].id: 671 chain.append(section.end) 672 else: 673 chains.append(chain) 674 chain = [section.start, section.end] 675 if chain not in chains: 676 chains.append(chain) 677 return chains 678 679 def _sort_intersections(self) -> None: 680 """Sort intersections of the division.""" 681 self.intersections.sort(key=lambda x: distance(self.p1, x.point)) 682 683 def is_connected(self, other: "Division") -> bool: 684 """Return True if the division is connected to another division. 685 686 Args: 687 other (Division): Another division. 688 689 Returns: 690 bool: True if connected, False otherwise. 691 """ 692 return self.p1 in other.end_points or self.p2 in other.end_points 693 694 @property 695 def end_points(self): 696 """Return the end points of the division. 697 698 Returns: 699 list: List of end points. 700 """ 701 return [self.p1, self.p2] 702 703 @property 704 def start(self) -> Intersection: 705 """Return the start intersection of the division. 706 707 Returns: 708 Intersection: Start intersection. 709 """ 710 return self.intersections[0] 711 712 @property 713 def end(self) -> Intersection: 714 """Return the end intersection of the division. 715 716 Returns: 717 Intersection: End intersection. 718 """ 719 return self.intersections[-1] 720 721 722class Polyline(Shape): 723 """ 724 Connected points, similar to Shape objects. 725 They can be closed or open. 726 They are defined by a sequence of points. 727 They have divisions, sections, and intersections. 728 729 Args: 730 points (list): List of points defining the polyline. 731 closed (bool, optional): If the polyline is closed. Defaults to True. 732 xform_matrix (array, optional): Transformation matrix. Defaults to None. 733 **kwargs: Additional attributes for cosmetic/drawing purposes. 734 """ 735 736 def __init__(self, points, closed=True, xform_matrix=None, **kwargs): 737 self.__dict__["style"] = ShapeStyle() 738 self.__dict__["_style_map"] = shape_style_map 739 self._set_aliases() 740 self.closed = closed 741 kwargs["subtype"] = Types.POLYLINE 742 super().__init__(points, closed=closed, xform_matrix=xform_matrix, **kwargs) 743 self._set_divisions() 744 if not self.closed: 745 self._set_intersections() 746 common_properties(self) 747 748 def _update(self, xform_matrix, reps=0): 749 """Update the transformation matrix of the polyline. 750 751 Args: 752 xform_matrix (array): Transformation matrix. 753 reps (int, optional): Number of repetitions. Defaults to 0. 754 755 Returns: 756 Any: Updated polyline or list of updated polylines. 757 """ 758 if reps == 0: 759 self.xform_matrix = self.xform_matrix @ xform_matrix 760 for division in self.divisions: 761 division._update(xform_matrix, reps=reps) 762 res = self 763 else: 764 res = [] 765 for _ in range(reps): 766 shape = self.copy() 767 shape._update(xform_matrix) 768 res.append(shape) 769 770 return res 771 772 def __str__(self): 773 """String representation of the polyline. 774 775 Returns: 776 str: String representation. 777 """ 778 return f"Polyline({self.final_coords[:, :2]})" 779 780 def __repr__(self): 781 """String representation of the polyline. 782 783 Returns: 784 str: String representation. 785 """ 786 return self.__str__() 787 788 def iter_sections(self) -> Iterator: 789 """Iterate over the sections of the polyline. 790 791 Yields: 792 Section: Section object. 793 """ 794 for division in self.divisions: 795 yield from division.sections 796 797 def iter_intersections(self): 798 """Iterate over the intersections of the polyline. 799 800 Yields: 801 Intersection: Intersection object. 802 """ 803 for division in self.divisions: 804 yield from division.intersections 805 806 @property 807 def intersections(self): 808 """Return the intersections of the polyline. 809 810 Returns: 811 list: List of intersections. 812 """ 813 res = [] 814 for division in self.divisions: 815 res.extend(division.intersections) 816 return res 817 818 @property 819 def area(self): 820 """Return the area of the polygon. 821 822 Returns: 823 float: Area of the polygon. 824 """ 825 return polygon_area(self.vertices) 826 827 @property 828 def sections(self): 829 """Return the sections of the polyline. 830 831 Returns: 832 list: List of sections. 833 """ 834 sections = [] 835 for division in self.divisions: 836 sections.extend(division.sections) 837 return sections 838 839 @property 840 def divisions(self): 841 """Return the divisions of the polyline. 842 843 Returns: 844 list: List of divisions. 845 """ 846 return self.__dict__["divisions"] 847 848 def _set_divisions(self): 849 vertices = self.vertices 850 if self.closed: 851 vertices = list(vertices) + [vertices[0]] 852 pairs = connected_pairs(vertices) 853 divisions = [Division(p1, p2) for p1, p2 in pairs] 854 self.__dict__["divisions"] = divisions 855 856 def _set_intersections(self): 857 """Fake intersections for open lines.""" 858 division1 = self.divisions[0] 859 division2 = self.divisions[-1] 860 x1 = Intersection(division1.p1, division1, None, True) 861 division1.intersections = [x1] 862 x2 = Intersection(division2.p2, division2, None, True) 863 if division1.id == division2.id: 864 division1.intersections.append(x2) 865 else: 866 division2.intersections = [x2] 867 868 869class ParallelPolyline(Batch): 870 """A ParallelPolylines is a collection of parallel Polylines. 871 They are defined by a main polyline and a list of offset 872 values (that can be negative or positive). 873 874 Args: 875 polyline (Polyline): Main polyline. 876 offset (float): Offset value. 877 lace (Lace): Lace object. 878 under (bool, optional): If the polyline is under. Defaults to False. 879 closed (bool, optional): If the polyline is closed. Defaults to True. 880 dist_tol (float, optional): Distance tolerance. Defaults to None. 881 **kwargs: Additional attributes for cosmetic/drawing purposes. 882 """ 883 884 def __init__( 885 self, 886 polyline, 887 offset, 888 lace, 889 under=False, 890 closed=True, 891 dist_tol=None, 892 **kwargs, 893 ): 894 if dist_tol is None: 895 dist_tol = defaults["dist_tol"] 896 dist_tol2 = dist_tol * dist_tol 897 self.polyline = polyline 898 self.offset = offset 899 self.dist_tol = dist_tol 900 self.dist_tol2 = dist_tol2 901 self.closed = closed 902 self._set_offset_polylines() 903 self.polyline_list = [self.polyline] + self.offset_poly_list 904 super().__init__(self.polyline_list, **kwargs) 905 self.subtype = Types.PARALLEL_POLYLINE 906 self.overlaps = None 907 self.under = under 908 common_properties(self) 909 910 @property 911 def sections(self) -> List[Section]: 912 """Return the sections of the parallel polyline. 913 914 Returns: 915 list: List of sections. 916 """ 917 sects = [] 918 for polyline in self.polyline_list: 919 sects.extend(polyline.sections) 920 return sects 921 922 def _set_offset_polylines(self): 923 polyline = self.polyline 924 if self.closed: 925 vertices = list(polyline.vertices) 926 vertices = vertices + [vertices[0]] 927 offset_polygons = double_offset_polygons( 928 vertices, self.offset, dist_tol=self.dist_tol) 929 else: 930 offset_polylines = double_offset_polylines(polyline.vertices, self.offset) 931 polylines = [] 932 if self.closed: 933 for polygon in offset_polygons: 934 polylines.append(Polyline(polygon, closed=self.closed)) 935 else: 936 for polyline in offset_polylines: 937 polylines.append(Polyline(polyline, closed=self.closed)) 938 939 self.offset_poly_list = polylines 940 941 942class Lace(Batch): 943 """ 944 A Lace is a collection of ParallelPolylines objects. 945 They are used to create interlace patterns. 946 947 Args: 948 polygon_shapes (Union[Batch, list[Shape]], optional): List of polygon shapes. Defaults to None. 949 polyline_shapes (Union[Batch, list[Shape]], optional): List of polyline shapes. Defaults to None. 950 offset (float, optional): Offset value. Defaults to 2. 951 rtol (float, optional): Relative tolerance. Defaults to None. 952 swatch (list, optional): Swatch list. Defaults to None. 953 breakpoints (list, optional): Breakpoints list. Defaults to None. 954 plait_color (colors.Color, optional): Plait color. Defaults to None. 955 draw_fragments (bool, optional): If fragments should be drawn. Defaults to True. 956 palette (list, optional): Palette list. Defaults to None. 957 color_step (int, optional): Color step. Defaults to 1. 958 with_plaits (bool, optional): If plaits should be included. Defaults to True. 959 area_threshold (float, optional): Area threshold. Defaults to None. 960 radius_threshold (float, optional): Radius threshold. Defaults to None. 961 **kwargs: Additional attributes for cosmetic/drawing purposes. 962 """ 963 964 # @timing 965 def __init__( 966 self, 967 polygon_shapes: Union[Batch, list[Shape]] = None, 968 polyline_shapes: Union[Batch, list[Shape]] = None, 969 offset: float = 2, 970 rtol: float = None, 971 swatch: list = None, 972 breakpoints: list = None, 973 plait_color: colors.Color = None, 974 draw_fragments: bool = True, 975 palette: list = None, 976 color_step: int = 1, 977 with_plaits: bool = True, 978 area_threshold: float = None, 979 radius_threshold: float = None, 980 **kwargs, 981 ) -> None: 982 validate_args(kwargs, shape_style_map) 983 ( 984 rtol, 985 swatch, 986 plait_color, 987 draw_fragments, 988 area_threshold, 989 radius_threshold, 990 ) = get_defaults( 991 [ 992 "rtol", 993 "swatch", 994 "plait_color", 995 "draw_fragments", 996 "area_threshold", 997 "radius_threshold", 998 ], 999 [ 1000 rtol, 1001 swatch, 1002 plait_color, 1003 draw_fragments, 1004 area_threshold, 1005 radius_threshold, 1006 ], 1007 ) 1008 if polygon_shapes: 1009 polygon_shapes = polygon_shapes.merge_shapes() 1010 self.polygon_shapes = self._check_polygons(polygon_shapes) 1011 else: 1012 self.polygon_shapes = [] 1013 if polyline_shapes: 1014 polyline_shapes = polyline_shapes.merge_shapes() 1015 self.polyline_shapes = self._check_polylines(polyline_shapes) 1016 else: 1017 self.polyline_shapes = [] 1018 if not self.polygon_shapes and not self.polyline_shapes: 1019 msg = "Lace.__init__ : No polygons or polylines found." 1020 raise ValueError(msg) 1021 self.polyline_shapes = polyline_shapes 1022 self.offset = offset 1023 self.main_intersections = None 1024 self.offset_intersections = None 1025 self.xform_matrix = np.eye(3) 1026 self.rtol = rtol 1027 self.swatch = swatch 1028 self.breakpoints = breakpoints 1029 self.plait_color = plait_color 1030 self.draw_fragments = draw_fragments 1031 self.palette = palette 1032 self.color_step = color_step 1033 self.with_plaits = with_plaits 1034 self.d_intersections = {} # key, value:intersection.id, intersection 1035 self.d_connections = {} 1036 self.plaits = [] 1037 self.overlaps = [] 1038 self._groups = None 1039 self.area_threshold = area_threshold 1040 self.radius_threshold = radius_threshold 1041 if kwargs and "_copy" in kwargs: 1042 # pass the pre-computed values 1043 for k, v in kwargs: 1044 if k == "_copy": 1045 continue 1046 else: 1047 setattr(self, k, v) 1048 else: 1049 self._set_polyline_list() # main divisions are set here along with polylines 1050 # polyline.divisions is the list of Division objects 1051 self._set_parallel_poly_list() 1052 # start_time2 = time.perf_counter() 1053 self._set_intersections() 1054 # end_time2 = time.perf_counter() 1055 # print( 1056 # f"Lace.__init__ intersections computed in {end_time2 - start_time2:0.4f} seconds" 1057 # ) 1058 1059 self._set_overlaps() 1060 self._set_twin_sections() 1061 self._set_fragments() 1062 if not self.polyline_shapes: 1063 self._set_outline() 1064 # self._set_partitions() 1065 self._set_over_under() 1066 if self.with_plaits: 1067 self.set_plaits() 1068 # self._set_convex_hull() 1069 # self._set_concave_hull() 1070 # self._set_fragment_groups() 1071 # self._set_partition_groups() 1072 self._b_box = None 1073 elements = [polyline for polyline in self.parallel_poly_list] + self.fragments 1074 1075 if "debug" in kwargs: 1076 kwargs.pop("debug") 1077 super().__init__(elements, **kwargs) 1078 if kwargs and "_copy" not in kwargs: 1079 for k, v in kwargs.items(): 1080 if k in shape_style_map: 1081 setattr(self, k, v) # todo: we should check for valid values here 1082 else: 1083 raise AttributeError(f"{k}. Invalid attribute!") 1084 self.subtype = Types.LACE 1085 common_properties(self) 1086 1087 @property 1088 def center(self): 1089 """Return the center of the lace. 1090 1091 Returns: 1092 list: Center coordinates. 1093 """ 1094 return self.outline.CG 1095 1096 @property 1097 def fragment_groups(self): 1098 """Return the fragment groups of the lace. 1099 1100 Returns: 1101 dict: Dictionary of fragment groups. 1102 """ 1103 center = self.center 1104 radius_frag = [] 1105 for fragment in self.fragments: 1106 radius = int(distance(center, fragment.CG)) 1107 for rad, frag in radius_frag: 1108 if abs(radius - rad) <= 2: 1109 radius = rad 1110 break 1111 radius_frag.append((radius, fragment)) 1112 1113 radius_frag.sort(key=lambda x: x[0]) 1114 1115 d_groups = {} 1116 for i, (radius, fragment) in enumerate(radius_frag): 1117 if i == 0: 1118 d_groups[radius] = [fragment] 1119 else: 1120 if radius in d_groups: 1121 d_groups[radius].append(fragment) 1122 else: 1123 d_groups[radius] = [fragment] 1124 1125 return d_groups 1126 1127 def _check_polygons(self, polygon_shapes): 1128 if isinstance(polygon_shapes, Batch): 1129 polygon_shapes = polygon_shapes.all_shapes 1130 for polygon in polygon_shapes: 1131 if len(polygon.primary_points) < 3: 1132 msg = "Lace.__init__ found polygon with less than 3 points." 1133 raise ValueError(msg) 1134 if not polygon.closed: 1135 msg = "Lace.__init__ : Invalid polygons" 1136 raise ValueError(msg) 1137 if polygon.primary_points[0] != polygon.primary_points[-1]: 1138 polygon.primary_points.append(polygon.primary_points[0]) 1139 # check if the polygons are clockwise 1140 for polygon in polygon_shapes: 1141 if not right_handed(polygon.vertices): 1142 polygon.primary_points.reverse() 1143 1144 return polygon_shapes 1145 1146 def _check_polylines(self, polyline_shapes): 1147 if isinstance(polyline_shapes, Batch): 1148 polyline_shapes = polyline_shapes.all_shapes 1149 for polyline in polyline_shapes: 1150 if len(polyline.primary_points) < 2: 1151 msg = "Lace.__init__ found polyline with less than 2 points." 1152 raise ValueError(msg) 1153 1154 return polyline_shapes 1155 1156 def _update(self, xform_matrix, reps=0): 1157 """Update the transformation matrix of the lace. 1158 1159 Args: 1160 xform_matrix (array): Transformation matrix. 1161 reps (int, optional): Number of repetitions. Defaults to 0. 1162 1163 Returns: 1164 Any: Updated lace or list of updated laces. 1165 """ 1166 if reps == 0: 1167 self.xform_matrix = self.xform_matrix @ xform_matrix 1168 for polygon in self.polygon_shapes: 1169 polygon._update(xform_matrix) 1170 1171 if self.polyline_shapes: 1172 for polyline in self.polyline_shapes: 1173 polyline._update(xform_matrix) 1174 1175 for polyline in self.parallel_poly_list: 1176 polyline._update(xform_matrix) 1177 1178 for fragment in self.fragments: 1179 fragment._update(xform_matrix) 1180 1181 for intersection in self.main_intersections: 1182 intersection._update(xform_matrix) 1183 1184 for intersection in self.offset_intersections: 1185 intersection._update(xform_matrix) 1186 1187 for overlap in self.overlaps: 1188 overlap._update(xform_matrix) 1189 1190 for plait in self.plaits: 1191 plait._update(xform_matrix) 1192 1193 return self 1194 else: 1195 res = [] 1196 for _ in range(reps): 1197 shape = self.copy() 1198 shape._update(xform_matrix) 1199 res.append(shape) 1200 return res 1201 1202 # @timing 1203 def _set_twin_sections(self): 1204 for par_poly in self.parallel_poly_list: 1205 poly1, poly2 = par_poly.offset_poly_list 1206 for i, sec in enumerate(poly1.iter_sections()): 1207 sec1 = sec 1208 sec2 = poly2.sections[i] 1209 sec1.twin = sec2 1210 sec2.twin = sec1 1211 1212 # @timing 1213 def _set_partitions(self): 1214 for fragment in self.fragments: 1215 self.partitions = [] 1216 for fragment in self.fragments: 1217 partition = Shape(offset_polygon(fragment.vertices, self.offset)) 1218 self.partitions.append(partition) 1219 1220 # To do: This doesn't work if we have polyline shapes! 1221 def _set_outline(self): 1222 # outline is a special fragment that covers the whole lace 1223 areas = [] 1224 for fragment in self.fragments: 1225 areas.append((fragment.area, fragment)) 1226 areas.sort(reverse=True, key=lambda x: x[0]) 1227 self.outline = areas[0][1] 1228 self.fragments.remove(self.outline) 1229 # perimenter is the outline of the partitions 1230 self.perimeter = Shape(offset_polygon(self.outline.vertices, -self.offset)) 1231 # skeleton is the input polylines that the lace is based on 1232 self.skeleton = Batch(self.polyline_list) 1233 1234 def set_fragment_groups(self): 1235 # to do : handle repeated code. same in _set_partition_groups 1236 areas = [] 1237 for i, fragment in enumerate(self.fragments): 1238 areas.append((fragment.area, i)) 1239 areas.sort() 1240 bins = group_into_bins(areas, self.area_threshold) 1241 self.fragments_by_area = OrderedDict() 1242 for i, bin in enumerate(bins): 1243 area_values = [x[0] for x in bin] 1244 key = sum([x[0] for x in bin]) / len(bin) 1245 fragments = [] 1246 for area, ind in areas: 1247 if area in area_values: 1248 fragments.append(self.fragments[ind]) 1249 self.fragments_by_area[key] = fragments 1250 1251 radii = [] 1252 for i, fragment in enumerate(self.fragments): 1253 radii.append((distance(self.center, fragment.CG), i)) 1254 radii.sort() 1255 bins = group_into_bins(radii, self.radius_threshold) 1256 self.fragments_by_radius = OrderedDict() 1257 for i, bin in enumerate(bins): 1258 radius_values = [x[0] for x in bin] 1259 key = sum([x[0] for x in bin]) / len(bin) 1260 fragments = [] 1261 for radius, ind in radii: 1262 if radius in radius_values: 1263 fragments.append(self.fragments[ind]) 1264 self.fragments_by_radius[key] = fragments 1265 1266 # @timing 1267 def _set_partition_groups(self): 1268 # to do : handle repeated code. same in set_fragment_groups 1269 areas = [] 1270 for i, partition in enumerate(self.partitions): 1271 areas.append((partition.area, i)) 1272 areas.sort() 1273 bins = group_into_bins(areas, self.area_threshold) 1274 self.partitions_by_area = OrderedDict() 1275 for i, bin_ in enumerate(bins): 1276 area_values = [x[0] for x in bin_] 1277 key = sum([x[0] for x in bin_]) / len(bin_) 1278 partitions = [] 1279 for area, ind in areas: 1280 if area in area_values: 1281 partitions.append(self.partitions[ind]) 1282 self.partitions_by_area[key] = partitions 1283 1284 radii = [] 1285 for i, partition in enumerate(self.partitions): 1286 CG = polygon_center(partition.vertices) 1287 radii.append((distance(self.center, CG), i)) 1288 radii.sort() 1289 bins = group_into_bins(radii, self.radius_threshold) 1290 self.partitions_by_radius = OrderedDict() 1291 for i, bin_ in enumerate(bins): 1292 radius_values = [x[0] for x in bin_] 1293 key = sum([x[0] for x in bin_]) / len(bin_) 1294 partitions = [] 1295 for radius, ind in radii: 1296 if radius in radius_values: 1297 partitions.append(self.partitions[ind]) 1298 self.partitions_by_radius[key] = partitions 1299 1300 # @timing 1301 def _set_fragments(self): 1302 G = nx.Graph() 1303 for section in self.iter_offset_sections(): 1304 if section.is_overlap: 1305 continue 1306 G.add_edge(section.start.id, section.end.id, section=section) 1307 1308 cycles = nx.cycle_basis(G) 1309 fragments = [] 1310 d_x = self.d_intersections 1311 for cycle in cycles: 1312 cycle.append(cycle[0]) 1313 nodes = cycle 1314 edges = connected_pairs(cycle) 1315 sections = [G.edges[edge]["section"] for edge in edges] 1316 s_intersections = set() 1317 for section in sections: 1318 s_intersections.add(section.start.id) 1319 s_intersections.add(section.end.id) 1320 intersections = [self.d_intersections[i] for i in s_intersections] 1321 points = [d_x[x_id].point for x_id in nodes] 1322 if not right_handed(points): 1323 points.reverse() 1324 fragment = Fragment(points) 1325 fragment.sections = sections 1326 fragment.intersections = intersections 1327 fragments.append(fragment) 1328 1329 for fragment in fragments: 1330 for section in fragment.sections: 1331 section.fragment = fragment 1332 1333 for fragment in fragments: 1334 fragment._set_divisions() 1335 for fragment in fragments: 1336 fragment._set_twin_divisions() 1337 1338 self.fragments = fragments 1339 1340 def _set_concave_hull(self): 1341 self.concave_hull = self.outline.vertices 1342 1343 def _set_convex_hull(self): 1344 self.convex_hull = convex_hull(self.outline.vertices) 1345 1346 def copy(self): 1347 class Dummy(Lace): 1348 pass 1349 1350 # we need to copy the polyline_list and parallel_poly_list 1351 for polyline in self.polyline_list: 1352 polyline.copy() 1353 1354 def get_sketch(self): 1355 """ 1356 Create and return a Sketch object. Sketch is a Batch object 1357 with Shape elements corresponding to the vertices of the plaits 1358 and fragments of the Lace instance. They have 'plaits' and 1359 'fragments' attributes to hold lists of Shape objects populated 1360 with plait and fragment vertices of the Lace instance 1361 respectively. They are used for drawing multiple copies of the 1362 original lace pattern. They are light-weight compared to the 1363 Lace objects since they only contain sufficient data to draw the 1364 lace objects. Hundreds of these objects can be used to create 1365 wallpaper patterns or other patterns without having to contain 1366 unnecessary data. They do not share points with the original 1367 Lace object. 1368 1369 Arguments: 1370 ---------- 1371 None 1372 1373 Prerequisites: 1374 -------------- 1375 * A lace object to be copied. 1376 1377 Side effects: 1378 ------------- 1379 None 1380 1381 Return: 1382 -------- 1383 A Sketch object. 1384 """ 1385 fragments = [] 1386 for fragment in self.fragments: 1387 polygon = Shape(fragment.vertices) 1388 polygon.fill = True 1389 polygon.subtype = Types.FRAGMENT 1390 fragments.append(polygon) 1391 1392 plaits = [] 1393 for plait in self.plaits: 1394 polygon = Shape(plait.vertices) 1395 polygon.fill = True 1396 polygon.subtype = Types.PLAIT 1397 plaits.append(polygon) 1398 1399 sketch = Batch((fragments + plaits)) 1400 sketch.fragments = fragments 1401 sketch.plaits = plaits 1402 sketch.outline = self.outline 1403 sketch.subtype = Types.SKETCH 1404 1405 sketch.draw_plaits = True 1406 sketch.draw_fragments = True 1407 return sketch 1408 1409 def group_fragments(self, tol=None): 1410 """Group the fragments by the number of vertices and the area. 1411 1412 Args: 1413 tol (float, optional): Tolerance value. Defaults to None. 1414 1415 Returns: 1416 list: List of grouped fragments. 1417 """ 1418 if tol is None: 1419 tol = defaults["tol"] 1420 frags = self.fragments 1421 vert_groups = [ 1422 [frag for frag in frags if len(frag.vertices) == n] 1423 for n in set([len(f.vertices) for f in frags]) 1424 ] 1425 groups = [] 1426 for group in vert_groups: 1427 areas = [ 1428 [f for f in group if isclose(f.area, area, rtol=tol)] 1429 for area in set([frag.area for frag in group]) 1430 ] 1431 areas.sort(key=lambda x: x[0].area, reverse=True) 1432 groups.append(areas) 1433 groups.sort(key=lambda x: x[0][0].area, reverse=True) 1434 1435 return groups 1436 1437 def get_fragment_cycles(self): 1438 """ 1439 Iterate over the offset sections and create a graph of the 1440 intersections (start and end of the sections). Then find the 1441 cycles in the graph. self.d_intersections is used to map 1442 the graph nodes to the actual intersection points. 1443 1444 Returns: 1445 list: List of fragment cycles. 1446 """ 1447 graph_edges = [] 1448 for section in self.iter_offset_sections(): 1449 if section.is_overlap: 1450 continue 1451 graph_edges.append((section.start.id, section.end.id)) 1452 1453 return get_cycles(graph_edges) 1454 1455 def _set_inner_lines(self, item, n, offset, line_color=colors.blue, line_width=1): 1456 for i in range(n): 1457 vertices = item.vertices 1458 dist_tol = defaults["dist_tol"] 1459 offset_poly = offset_polygon_points( 1460 vertices, offset * (i + 1), dist_tol=dist_tol 1461 ) 1462 shape = Shape(offset_poly) 1463 shape.fill = False 1464 shape.line_width = line_width 1465 shape.line_color = line_color 1466 item.inner_lines.append(shape) 1467 1468 def set_plait_lines(self, n, offset, line_color=colors.blue, line_width=1): 1469 """Create offset lines inside the plaits of the lace. 1470 1471 Args: 1472 n (int): Number of lines. 1473 offset (float): Offset value. 1474 line_color (colors.Color, optional): Line color. Defaults to colors.blue. 1475 line_width (int, optional): Line width. Defaults to 1. 1476 """ 1477 for plait in self.plaits: 1478 plait.inner_lines = [] 1479 self._set_inner_lines(plait, n, offset, line_color, line_width) 1480 1481 def set_fragment_lines( 1482 self, 1483 n: int, 1484 offset: float, 1485 line_color: colors.Color = colors.blue, 1486 line_width=1, 1487 ) -> None: 1488 """ 1489 Create offset lines inside the fragments of the lace. 1490 1491 Args: 1492 n (int): Number of lines. 1493 offset (float): Offset value. 1494 line_color (colors.Color, optional): Line color. Defaults to colors.blue. 1495 line_width (int, optional): Line width. Defaults to 1. 1496 """ 1497 for fragment in self.fragments: 1498 fragment.inner_lines = [] 1499 self._set_inner_lines(fragment, n, offset, line_color, line_width) 1500 1501 @property 1502 def all_divisions(self) -> List: 1503 """ 1504 Return a list of all the divisions (both main and offset) in the lace. 1505 1506 Returns: 1507 list: List of all divisions. 1508 """ 1509 res = [] 1510 for parallel_polyline in self.parallel_poly_list: 1511 for polyline in parallel_polyline.polyline_list: 1512 res.extend(polyline.divisions) 1513 return res 1514 1515 def iter_main_intersections(self) -> Iterator: 1516 """Iterate over the main intersections. 1517 1518 Yields: 1519 Intersection: Intersection object. 1520 """ 1521 for ppoly in self.parallel_poly_list: 1522 for division in ppoly.polyline.divisions: 1523 yield from division.intersections 1524 1525 def iter_offset_intersections(self) -> Iterator: 1526 """ 1527 Iterate over the offset intersections. 1528 1529 Yields: 1530 Intersection: Intersection object. 1531 """ 1532 for ppoly in self.parallel_poly_list: 1533 for poly in ppoly.offset_poly_list: 1534 for division in poly.divisions: 1535 yield from division.intersections 1536 1537 def iter_offset_sections(self) -> Iterator: 1538 """ 1539 Iterate over the offset sections. 1540 1541 Yields: 1542 Section: Section object. 1543 """ 1544 for ppoly in self.parallel_poly_list: 1545 for poly in ppoly.offset_poly_list: 1546 for division in poly.divisions: 1547 yield from division.sections 1548 1549 def iter_main_sections(self) -> Iterator: 1550 """Iterate over the main sections. 1551 1552 Yields: 1553 Section: Section object. 1554 """ 1555 for ppoly in self.parallel_poly_list: 1556 for division in ppoly.polyline.divisions: 1557 yield from division.sections 1558 1559 def iter_offset_divisions(self) -> Iterator: 1560 """ 1561 Iterate over the offset divisions. 1562 1563 Yields: 1564 Division: Division object. 1565 """ 1566 for ppoly in self.parallel_poly_list: 1567 for poly in ppoly.offset_poly_list: 1568 yield from poly.divisions 1569 1570 def iter_main_divisions(self) -> Iterator: 1571 """ 1572 Iterate over the main divisions. 1573 1574 Yields: 1575 Division: Division object. 1576 """ 1577 for ppoly in self.parallel_poly_list: 1578 yield from ppoly.polyline.divisions 1579 1580 @property 1581 def main_divisions(self) -> List[Division]: 1582 """Main divisions are the divisions of the main polyline. 1583 1584 Returns: 1585 list: List of main divisions. 1586 """ 1587 res = [] 1588 for parallel_polyline in self.parallel_poly_list: 1589 res.extend(parallel_polyline.polyline.divisions) 1590 return res 1591 1592 @property 1593 def offset_divisions(self) -> List[Division]: 1594 """Offset divisions are the divisions of the offset polylines. 1595 1596 Returns: 1597 list: List of offset divisions. 1598 """ 1599 res = [] 1600 for parallel_polyline in self.parallel_poly_list: 1601 for polyline in parallel_polyline.offset_poly_list: 1602 res.extend(polyline.divisions) 1603 return res 1604 1605 @property 1606 def intersections(self) -> List[Intersection]: 1607 """Return all the intersections in the parallel_poly_list. 1608 1609 Returns: 1610 list: List of intersections. 1611 """ 1612 res = [] 1613 for parallel_polyline in self.parallel_poly_list: 1614 for polyline in parallel_polyline.polyline_list: 1615 res.extend(polyline.intersections) 1616 return res 1617 1618 # @timing 1619 def _set_polyline_list(self): 1620 """ 1621 Populate the self.polyline_list list with Polyline objects. 1622 1623 * Internal use only. 1624 1625 Arguments: 1626 ---------- 1627 None 1628 1629 Return: 1630 -------- 1631 None 1632 1633 Prerequisites: 1634 -------------- 1635 1636 * self.polygon_shapes and/or self.polyline_shapes must be 1637 established. 1638 """ 1639 self.polyline_list = [] 1640 if self.polygon_shapes: 1641 for polygon in self.polygon_shapes: 1642 self.polyline_list.append(Polyline(polygon.vertices, closed=True)) 1643 if self.polyline_shapes: 1644 for polyline in self.polyline_shapes: 1645 self.polyline_list.append(Polyline(polyline.vertices, closed=False)) 1646 1647 # @timing 1648 def _set_parallel_poly_list(self): 1649 """ 1650 Populate the self.parallel_poly_list list with ParallelPolyline 1651 objects. 1652 1653 Arguments: 1654 ---------- 1655 None 1656 1657 Return: 1658 -------- 1659 None 1660 1661 Prerequisites: 1662 -------------- 1663 * self.polygon_shapes and/or self.polyline_shapes must be 1664 established prior to this. 1665 * Parallel polylines are created by offsetting the original 1666 polygon and polyline shapes in two directions using the 1667 self.offset value. 1668 1669 Notes: 1670 ------ 1671 This method is called by the Lace constructor. It is not 1672 for users to call directly. Without this method, the Lace 1673 object cannot be created. 1674 """ 1675 self.parallel_poly_list = [] 1676 if self.polyline_list: 1677 for _, polyline in enumerate(self.polyline_list): 1678 self.parallel_poly_list.append( 1679 ParallelPolyline( 1680 polyline, 1681 self.offset, 1682 lace=self, 1683 closed=polyline.closed, 1684 dist_tol=defaults["dist_tol"] 1685 ) 1686 ) 1687 1688 # @timing 1689 def _set_overlaps(self): 1690 """ 1691 Populate the self.overlaps list with Overlap objects. Side 1692 effects listed below. 1693 1694 Arguments: 1695 ---------- 1696 None 1697 1698 Return: 1699 -------- 1700 None 1701 1702 Side Effects: 1703 ------------- 1704 * self.overlaps is populated with Overlap objects. 1705 * Section objects' overlap attribute is populated with the 1706 corresponding Overlap object that they are a part of. Not 1707 all sections will have an overlap. 1708 1709 Prerequisites: 1710 -------------- 1711 self.polyline and self.parallel_poly_list must be populated. 1712 self.main_intersections, self.offset_sections and 1713 self.d_intersections must be populated prior to creating 1714 the overlaps. 1715 1716 Notes: 1717 ------ 1718 This method is called by the Lace constructor. It is not 1719 for users to call directly. 1720 Without this method, the Lace object cannot be created. 1721 """ 1722 G = nx.Graph() 1723 for section in self.iter_offset_sections(): 1724 if section.is_overlap: 1725 G.add_edge(section.start.id, section.end.id, section=section) 1726 cycles = nx.cycle_basis(G) 1727 for cycle in cycles: 1728 cycle.append(cycle[0]) 1729 edges = connected_pairs(cycle) 1730 sections = [G.edges[edge]["section"] for edge in edges] 1731 s_intersections = set() 1732 for section in sections: 1733 s_intersections.add(section.start.id) 1734 s_intersections.add(section.end.id) 1735 intersections = [self.d_intersections[i] for i in s_intersections] 1736 overlap = Overlap(intersections=intersections, sections=sections) 1737 for section in sections: 1738 section.overlap = overlap 1739 for edge in edges: 1740 section = G.edges[edge]["section"] 1741 section.start.overlap = overlap 1742 section.end.overlap = overlap 1743 self.overlaps.append(overlap) 1744 for overlap in self.overlaps: 1745 for section in overlap.sections: 1746 if section.is_over: 1747 line_width = 3 1748 else: 1749 line_width = 1 1750 line = Shape( 1751 [section.start.point, section.end.point], line_width=line_width 1752 ) 1753 for division in self.offset_divisions: 1754 p1 = division.start.point 1755 p2 = division.end.point 1756 1757 def set_plaits(self): 1758 """ 1759 Populate the self.plaits list with Plait objects. Plaits are 1760 optional for drawing. They form the under/over interlacing. They 1761 are created if the "with_plaits" argument is set to be True in 1762 the constructor. with_plaits is True by default but this can be 1763 changed by setting the auto_plaits value to False in the 1764 settings.py This method can be called by the user to create the 1765 plaits after the creation of the Lace object if they were not 1766 created initally. 1767 1768 * Can be called by users. 1769 1770 Arguments: 1771 ---------- 1772 None 1773 1774 Return: 1775 -------- 1776 None 1777 1778 Side Effects: 1779 ------------- 1780 * self.plaits is populated with Plait objects. 1781 1782 Prerequisites: 1783 -------------- 1784 self.polyline and self.parallel_poly_list must be populated. 1785 self.divisions and self.intersections must be populated. 1786 self.overlaps must be populated. 1787 1788 Where used: 1789 ----------- 1790 Lace.__init__ 1791 1792 Notes: 1793 ------ 1794 This method is called by the Lace constructor. It is not 1795 for users to call directly. Without this method, the Lace 1796 object cannot be created. 1797 """ 1798 if self.plaits: 1799 return 1800 plait_sections = [] 1801 for division in self.iter_offset_divisions(): 1802 merged_sections = division._merged_sections() 1803 for merged in merged_sections: 1804 plait_sections.append((merged[0], merged[-1])) 1805 1806 # connect the open ends of the polyline_shapes 1807 for ppoly in self.parallel_poly_list: 1808 if not ppoly.closed: 1809 polyline1 = ppoly.offset_poly_list[0] 1810 polyline2 = ppoly.offset_poly_list[1] 1811 p1_start_x = polyline1.intersections[0] 1812 p1_end_x = polyline1.intersections[-1] 1813 p2_start_x = polyline2.intersections[0] 1814 p2_end_x = polyline2.intersections[-1] 1815 1816 plait_sections.append((p1_start_x, p2_start_x)) 1817 plait_sections.append((p1_end_x, p2_end_x)) 1818 for sec in self.iter_offset_sections(): 1819 if not sec.is_over and sec.is_overlap: 1820 plait_sections.append((sec.start, sec.end)) 1821 1822 graph_edges = [(r[0].id, r[1].id) for r in plait_sections] 1823 cycles = get_cycles(graph_edges) 1824 plaits = [] 1825 count = 0 1826 for cycle in cycles: 1827 cycle = connected_pairs(cycle) 1828 dup = cycle[1:] 1829 plait = [cycle[0][0], cycle[0][1]] 1830 for _ in range(len(cycle) - 1): 1831 last = plait[-1] 1832 for edge in dup: 1833 if edge[0] == last: 1834 plait.append(edge[1]) 1835 dup.remove(edge) 1836 break 1837 if edge[1] == last: 1838 plait.append(edge[0]) 1839 dup.remove(edge) 1840 break 1841 plaits.append(plait) 1842 count += 1 1843 d_x = self.d_intersections 1844 for plait in plaits: 1845 intersections = [d_x[x] for x in plait] 1846 vertices = [x.point for x in intersections] 1847 if not right_handed(vertices): 1848 vertices.reverse() 1849 plait.reverse() 1850 shape = Shape(vertices) 1851 shape.intersections = intersections 1852 shape.fill_color = colors.gold 1853 shape.inner_lines = None 1854 shape.subtype = Types.PLAIT 1855 self.plaits.append(shape) 1856 1857 # @timing 1858 def _set_intersections(self): 1859 """ 1860 Compute all intersection points (by calling all_intersections) 1861 among the divisions of the polylines (both main and offset). 1862 Populate the self.main_intersections and 1863 self.offset_intersections lists with Intersection objects. This 1864 method is called by the Lace constructor and customized to be 1865 used with Lace objects only. Without this method, the Lace 1866 object cannot be created. 1867 1868 * Internal use only! 1869 1870 Arguments: 1871 ---------- 1872 None 1873 1874 Return: 1875 -------- 1876 None 1877 1878 Side Effects: 1879 ------------- 1880 * self.main_intersections are populated. 1881 * self.offset_intersections are populated. 1882 * "sections" attribute of the divisions are populated. 1883 * "is_overlap" attribute of the sections are populated. 1884 * "intersections" attribute of the divisions are populated. 1885 * "endpoint" attribute of the intersections are populated. 1886 1887 Where used: 1888 ----------- 1889 Lace.__init__ 1890 1891 Prerequisites: 1892 -------------- 1893 * self.main_divisions must be populated. 1894 * self.offset_divisions must be populated. 1895 * Two endpoint intersections of the divisions must be set. 1896 1897 Notes: 1898 ------ 1899 This method is called by the Lace constructor. It is not 1900 for users to call directly. Without this method, the Lace 1901 object cannot be created. 1902 Works only for regular under/over interlacing. 1903 """ 1904 # set intersections for the main polylines 1905 main_divisions = self.main_divisions 1906 offset_divisions = self.offset_divisions 1907 self.main_intersections = all_intersections( 1908 main_divisions, self.d_intersections, self.d_connections 1909 ) 1910 1911 # set sections for the main divisions 1912 self.main_sections = [] 1913 for division in main_divisions: 1914 division.intersections[0].endpoint = True 1915 division.intersections[-1].endpoint = True 1916 segments = connected_pairs(division.intersections) 1917 division.sections = [] 1918 for i, segment in enumerate(segments): 1919 section = Section(*segment) 1920 if i % 2 == 1: 1921 section.is_overlap = True 1922 division.sections.append(section) 1923 self.main_sections.append(section) 1924 # set intersections for the offset polylines 1925 self.offset_intersections = all_intersections( 1926 offset_divisions, self.d_intersections, self.d_connections 1927 ) 1928 # set sections for the offset divisions 1929 self.offset_sections = [] 1930 for i, division in enumerate(offset_divisions): 1931 division.intersections[0].endpoint = True 1932 division.intersections[-1].endpoint = True 1933 lines = connected_pairs(division.intersections) 1934 division.sections = [] 1935 for j, line in enumerate(lines): 1936 section = Section(*line) 1937 if j % 2 == 1: 1938 section.is_overlap = True 1939 division.sections.append(section) 1940 self.offset_sections.append(section) 1941 1942 def _all_polygons(self, polylines, rtol=None): 1943 """Return a list of polygons from a list of lists of points. 1944 polylines: [[(x1, y1), (x2, y2)], [(x3, y3), (x4, y4)], ...] 1945 return [[(x1, y1), (x2, y2), (x3, y3), ...], ...] 1946 1947 Args: 1948 polylines (list): List of lists of points. 1949 rtol (float, optional): Relative tolerance. Defaults to None. 1950 1951 Returns: 1952 list: List of polygons. 1953 """ 1954 if rtol is None: 1955 rtol = self.rtol 1956 return get_polygons(polylines, rtol) 1957 1958 # @timing 1959 def _set_over_under(self): 1960 def next_poly(exclude): 1961 for ppoly in self.parallel_poly_list: 1962 poly1, poly2 = ppoly.offset_poly_list 1963 if poly1 in exclude: 1964 continue 1965 ind = 0 1966 for sec in poly1.iter_sections(): 1967 if sec.is_overlap: 1968 if sec.overlap.drawable and sec.overlap.visited: 1969 even_odd = ind % 2 == 1 1970 return (poly1, poly2, even_odd) 1971 ind += 1 1972 return (None, None, None) 1973 1974 for ppoly in self.parallel_poly_list: 1975 poly1, poly2 = ppoly.offset_poly_list 1976 exclude = [] 1977 even_odd = 0 1978 while poly1: 1979 ind = 0 1980 for i, division in enumerate(poly1.divisions): 1981 for j, section in enumerate(division.sections): 1982 if section.is_overlap: 1983 if section.overlap is None: 1984 msg = ( 1985 "Overlap section in the lace has no " 1986 "overlap object.\n" 1987 "Try different offset value and/or " 1988 "tolerance." 1989 ) 1990 raise RuntimeError(msg) 1991 section.overlap.visited = True 1992 if ind % 2 == even_odd: 1993 if section.overlap.drawable: 1994 section.overlap.drawable = False 1995 section.is_over = True 1996 section2 = poly2.divisions[i].sections[j] 1997 section2.is_over = True 1998 ind += 1 1999 exclude.extend([poly1, poly2]) 2000 poly1, poly2, even_odd = next_poly(exclude) 2001 2002 def fragment_edge_graph(self) -> nx.Graph: 2003 """ 2004 Return a networkx graph of the connected fragments. 2005 If two fragments have a "common" division then they are connected. 2006 2007 Returns: 2008 nx.Graph: Graph of connected fragments. 2009 """ 2010 G = nx.Graph() 2011 fragments = [(f.area, f) for f in self.fragments] 2012 fragments.sort(key=lambda x: x[0], reverse=True) 2013 for fragment in [f[1] for f in fragments[1:]]: 2014 for division in fragment.divisions: 2015 if division.twin and division.twin.fragment: 2016 fragment2 = division.twin.fragment 2017 G.add_node(fragment.id, fragment=fragment) 2018 G.add_node(fragment2.id, fragment=fragment2) 2019 G.add_edge(fragment.id, fragment2.id, edge=division) 2020 return G 2021 2022 def fragment_vertex_graph(self) -> nx.Graph: 2023 """ 2024 Return a networkx graph of the connected fragments. 2025 If two fragments have a "common" vertex then they are connected. 2026 2027 Returns: 2028 nx.Graph: Graph of connected fragments. 2029 """ 2030 def get_neighbours(intersection): 2031 division = intersection.division 2032 if not division.next.twin: 2033 return None 2034 fragments = [division.fragment] 2035 start_division_id = division.id 2036 if division.next.twin: 2037 twin_id = division.next.twin.id 2038 else: 2039 return None 2040 while twin_id != start_division_id: 2041 if division.next.twin: 2042 if division.fragment.id not in [x.id for x in fragments]: 2043 fragments.append(division.fragment) 2044 twin_id = division.next.twin.id 2045 division = division.next.twin 2046 else: 2047 if division.next.fragment.id not in [x.id for x in fragments]: 2048 fragments.append(division.next.fragment) 2049 break 2050 2051 return fragments 2052 2053 neighbours = [] 2054 for fragment in self.fragments: 2055 for intersection in fragment.intersections: 2056 neighbours.append(get_neighbours(intersection)) 2057 G = self.fragment_edge_graph() 2058 G2 = nx.Graph() 2059 for n in neighbours: 2060 if n: 2061 if len(n) > 2: 2062 for pair in combinations(n, 2): 2063 ids = tuple([x.id for x in pair]) 2064 if ids not in G.edges: 2065 G2.add_node(ids[0], fragment=pair[0]) 2066 G2.add_node(ids[1], fragment=pair[1]) 2067 G2.add_edge(*ids) 2068 return G2 2069 2070 2071def all_intersections( 2072 division_list: list[Division], 2073 d_intersections: dict[int, Intersection], 2074 d_connections: dict[frozenset, Intersection], 2075 loom=False, 2076) -> list[Intersection]: 2077 """ 2078 Find all intersections of the given divisions. Sweep-line algorithm 2079 without a self-balancing tree. Instead of a self-balancing tree, 2080 it uses a numpy array to sort and filter the divisions. For the 2081 number of divisions that are commonly needed in a lace, this is 2082 sufficiently fast. It is also more robust and much easier to 2083 understand and debug. Tested with tens of thousands of divisions but 2084 not millions. The book has a section on this algorithm. 2085 simetri.geometry.py has another version (called 2086 all_intersections) for finding intersections among a given 2087 list of divisions. 2088 2089 Arguments: 2090 ---------- 2091 division_list: list of Division objects. 2092 2093 Side Effects: 2094 ------------- 2095 * Modifies the given division objects (in the division_list) in place 2096 by adding the intersections to the divisions' "intersections" 2097 attribute. 2098 * Updates the d_intersections 2099 * Updates the d_connections 2100 2101 Return: 2102 -------- 2103 A list of all intersection objects among the given division 2104 list. 2105 """ 2106 # register fake intersections at the endpoints of the open lines 2107 for division in division_list: 2108 if division.intersections: 2109 for x in division.intersections: 2110 d_intersections[x.id] = x 2111 2112 # All objects are assigned an integer id attribute when they are created 2113 division_array = array( 2114 [flatten(division.vertices) + [division.id] for division in division_list] 2115 ) 2116 n_divisions = division_array.shape[0] # number of divisions 2117 # precompute the min and max x and y values for each division 2118 # these will be used with the sweep line algorithm 2119 xmin = np.minimum(division_array[:, 0], division_array[:, 2]).reshape( 2120 n_divisions, 1 2121 ) 2122 xmax = np.maximum(division_array[:, 0], division_array[:, 2]).reshape( 2123 n_divisions, 1 2124 ) 2125 ymin = np.minimum(division_array[:, 1], division_array[:, 3]).reshape( 2126 n_divisions, 1 2127 ) 2128 ymax = np.maximum(division_array[:, 1], division_array[:, 3]).reshape( 2129 n_divisions, 1 2130 ) 2131 division_array = np.concatenate((division_array, xmin, ymin, xmax, ymax), 1) 2132 d_divisions = {} 2133 for division in division_list: 2134 d_divisions[division.id] = division 2135 i_id, i_xmin, i_ymin, i_xmax, i_ymax = range(4, 9) # column indices 2136 # sort by xmin values 2137 division_array = division_array[division_array[:, i_xmin].argsort()] 2138 intersections = [] 2139 for i in range(n_divisions): 2140 x1, y1, x2, y2, id1, sl_xmin, sl_ymin, sl_xmax, sl_ymax = division_array[i, :] 2141 division1_vertices = [x1, y1, x2, y2] 2142 start = i + 1 # search should start from the next division 2143 # filter the array by checking if the divisions' bounding-boxes are 2144 # overlapping with the bounding-box of the current division 2145 candidates = division_array[start:, :][ 2146 ( 2147 ( 2148 (division_array[start:, i_xmax] >= sl_xmin) 2149 & (division_array[start:, i_xmin] <= sl_xmax) 2150 ) 2151 & ( 2152 (division_array[start:, i_ymax] >= sl_ymin) 2153 & (division_array[start:, i_ymin] <= sl_ymax) 2154 ) 2155 ) 2156 ] 2157 for candid in candidates: 2158 id2 = candid[i_id] 2159 division2_vertices = candid[:4] 2160 if loom: 2161 x1, y1 = division1_vertices[:2] 2162 x2, y2 = division2_vertices[:2] 2163 if x1 == x2 or y1 == y2: 2164 continue 2165 connection_type, x_point = intersection2( 2166 *division1_vertices, *division2_vertices 2167 ) 2168 if connection_type not in [ 2169 Connection.DISJOINT, 2170 Connection.NONE, 2171 Connection.PARALLEL, 2172 ]: 2173 division1, division2 = d_divisions[int(id1)], d_divisions[int(id2)] 2174 x_point__e1_2 = frozenset((division1.id, division2.id)) 2175 if x_point__e1_2 not in d_connections: 2176 inters_obj = Intersection(x_point, division1, division2) 2177 d_intersections[inters_obj.id] = inters_obj 2178 d_connections[x_point__e1_2] = inters_obj 2179 division1.intersections.append(inters_obj) 2180 division2.intersections.append(inters_obj) 2181 intersections.append(inters_obj) 2182 2183 for division in division_list: 2184 division._sort_intersections() 2185 return intersections 2186 2187 2188def merge_nodes( 2189 division_list: list[Division], 2190 d_intersections: dict[int, Intersection], 2191 d_connections: dict[frozenset, Intersection], 2192 loom=False, 2193) -> list[Intersection]: 2194 """ 2195 Find all intersections of the given divisions. Sweep-line algorithm 2196 without a self-balancing tree. Instead of a self-balancing tree, 2197 it uses a numpy array to sort and filter the divisions. For the 2198 number of divisions that are commonly needed in a lace, this is 2199 sufficiently fast. It is also more robust and much easier to 2200 understand and debug. Tested with tens of thousands of divisions but 2201 not millions. The book has a section on this algorithm. 2202 simetri.geometry.py has another version (called 2203 all_intersections) for finding intersections among a given 2204 list of divisions. 2205 2206 Arguments: 2207 ---------- 2208 division_list: list of division objects. 2209 2210 Side Effects: 2211 ------------- 2212 * Modifies the given division objects (in the division_list) in place 2213 by adding the intersections to the divisions' "intersections" 2214 attribute. 2215 * Updates the d_intersections 2216 * Updates the d_connections 2217 2218 Return: 2219 -------- 2220 A list of all intersection objects among the given division 2221 list. 2222 """ 2223 # register fake intersections at the endpoints of the open lines 2224 for division in division_list: 2225 if division.intersections: 2226 for x in division.intersections: 2227 d_intersections[x.id] = x 2228 2229 # All objects are assigned an integer id attribute when they are created 2230 division_array = array( 2231 [flatten(division.vertices) + [division.id] for division in division_list] 2232 ) 2233 n_divisions = division_array.shape[0] # number of divisions 2234 # precompute the min and max x and y values for each division 2235 # these will be used with the sweep line algorithm 2236 xmin = np.minimum(division_array[:, 0], division_array[:, 2]).reshape( 2237 n_divisions, 1 2238 ) 2239 xmax = np.maximum(division_array[:, 0], division_array[:, 2]).reshape( 2240 n_divisions, 1 2241 ) 2242 ymin = np.minimum(division_array[:, 1], division_array[:, 3]).reshape( 2243 n_divisions, 1 2244 ) 2245 ymax = np.maximum(division_array[:, 1], division_array[:, 3]).reshape( 2246 n_divisions, 1 2247 ) 2248 division_array = np.concatenate((division_array, xmin, ymin, xmax, ymax), 1) 2249 d_divisions = {} 2250 for division in division_list: 2251 d_divisions[division.id] = division 2252 i_id, i_xmin, i_ymin, i_xmax, i_ymax = range(4, 9) # column indices 2253 # sort by xmin values 2254 division_array = division_array[division_array[:, i_xmin].argsort()] 2255 intersections = [] 2256 for i in range(n_divisions): 2257 x1, y1, x2, y2, id1, sl_xmin, sl_ymin, sl_xmax, sl_ymax = division_array[i, :] 2258 division1_vertices = [x1, y1, x2, y2] 2259 start = i + 1 # search should start from the next division 2260 # filter the array by checking if the divisions' bounding-boxes are 2261 # overlapping with the bounding-box of the current division 2262 candidates = division_array[start:, :][ 2263 ( 2264 ( 2265 (division_array[start:, i_xmax] >= sl_xmin) 2266 & (division_array[start:, i_xmin] <= sl_xmax) 2267 ) 2268 & ( 2269 (division_array[start:, i_ymax] >= sl_ymin) 2270 & (division_array[start:, i_ymin] <= sl_ymax) 2271 ) 2272 ) 2273 ] 2274 for candid in candidates: 2275 id2 = candid[i_id] 2276 division2_vertices = candid[:4] 2277 if loom: 2278 x1, y1 = division1_vertices[:2] 2279 x2, y2 = division2_vertices[:2] 2280 if x1 == x2 or y1 == y2: 2281 continue 2282 connection_type, x_point = intersection2( 2283 *division1_vertices, *division2_vertices 2284 ) 2285 if connection_type not in [ 2286 Connection.DISJOINT, 2287 Connection.NONE, 2288 Connection.PARALLEL, 2289 ]: 2290 division1, division2 = d_divisions[int(id1)], d_divisions[int(id2)] 2291 x_point__e1_2 = frozenset((division1.id, division2.id)) 2292 if x_point__e1_2 not in d_connections: 2293 inters_obj = Intersection(x_point, division1, division2) 2294 d_intersections[inters_obj.id] = inters_obj 2295 d_connections[x_point__e1_2] = inters_obj 2296 division1.intersections.append(inters_obj) 2297 division2.intersections.append(inters_obj) 2298 intersections.append(inters_obj) 2299 2300 for division in division_list: 2301 division._sort_intersections() 2302 return intersections
array(object, dtype=None, *, copy=True, order='K', subok=False, ndmin=0, like=None)
Create an array.
Parameters
object : array_like
An array, any object exposing the array interface, an object whose
__array__
method returns an array, or any (nested) sequence.
If object is a scalar, a 0-dimensional array containing object is
returned.
dtype : data-type, optional
The desired data-type for the array. If not given, NumPy will try to use
a default dtype
that can represent the values (by applying promotion
rules when necessary.)
copy : bool, optional
If True
(default), then the array data is copied. If None
,
a copy will only be made if __array__
returns a copy, if obj is
a nested sequence, or if a copy is needed to satisfy any of the other
requirements (dtype
, order
, etc.). Note that any copy of
the data is shallow, i.e., for arrays with object dtype, the new
array will point to the same objects. See Examples for ndarray.copy
.
For False
it raises a ValueError
if a copy cannot be avoided.
Default: True
.
order : {'K', 'A', 'C', 'F'}, optional
Specify the memory layout of the array. If object is not an array, the
newly created array will be in C order (row major) unless 'F' is
specified, in which case it will be in Fortran order (column major).
If object is an array the following holds.
===== ========= ===================================================
order no copy copy=True
===== ========= ===================================================
'K' unchanged F & C order preserved, otherwise most similar order
'A' unchanged F order if input is F and not C, otherwise C order
'C' C order C order
'F' F order F order
===== ========= ===================================================
When ``copy=None`` and a copy is made for other reasons, the result is
the same as if ``copy=True``, with some exceptions for 'A', see the
Notes section. The default order is 'K'.
subok : bool, optional
If True, then sub-classes will be passed-through, otherwise
the returned array will be forced to be a base-class array (default).
ndmin : int, optional
Specifies the minimum number of dimensions that the resulting
array should have. Ones will be prepended to the shape as
needed to meet this requirement.
like : array_like, optional
Reference object to allow the creation of arrays which are not
NumPy arrays. If an array-like passed in as like
supports
the __array_function__
protocol, the result will be defined
by it. In this case, it ensures the creation of an array object
compatible with that passed in via this argument.
*New in version 1.20.0.*
Returns
out : ndarray An array object satisfying the specified requirements.
See Also
empty_like : Return an empty array with shape and type of input. ones_like : Return an array of ones with shape and type of input. zeros_like : Return an array of zeros with shape and type of input. full_like : Return a new array with shape of input filled with value. empty : Return a new uninitialized array. ones : Return a new array setting values to one. zeros : Return a new array setting values to zero. full : Return a new array of given shape filled with value. copy: Return an array copy of the given object.
Notes
When order is 'A' and object
is an array in neither 'C' nor 'F' order,
and a copy is forced by a change in dtype, then the order of the result is
not necessarily 'C' as expected. This is likely a bug.
Examples
>>> np.array([1, 2, 3])
array([1, 2, 3])
Upcasting:
>>> np.array([1, 2, 3.0])
array([ 1., 2., 3.])
More than one dimension:
>>> np.array([[1, 2], [3, 4]])
array([[1, 2],
[3, 4]])
Minimum dimensions 2:
>>> np.array([1, 2, 3], ndmin=2)
array([[1, 2, 3]])
Type provided:
>>> np.array([1, 2, 3], dtype=complex)
array([ 1.+0.j, 2.+0.j, 3.+0.j])
Data-type consisting of more than one element:
>>> x = np.array([(1,2),(3,4)],dtype=[('a','<i4'),('b','<i4')])
>>> x['a']
array([1, 3])
Creating an array from sub-classes:
>>> np.array(np.asmatrix('1 2; 3 4'))
array([[1, 2],
[3, 4]])
>>> np.array(np.asmatrix('1 2; 3 4'), subok=True)
matrix([[1, 2],
[3, 4]])
180class Intersection(Shape): 181 """Intersection of two divisions. They are at the endpoints of Section 182 objects. A division can have multiple sections and multiple 183 intersections. They can be located at the end of a division. 184 185 Args: 186 point (tuple): (x, y) coordinates of the intersection point. 187 division1 (Division): First division. 188 division2 (Division, optional): Second division. Defaults to None. 189 endpoint (bool, optional): If the intersection is at the end of a division, then endpoint is True. Defaults to False. 190 **kwargs: Additional attributes for cosmetic/drawing purposes. 191 """ 192 193 def __init__(self, point: tuple, division1: "Division", division2: "Division" = None, endpoint: bool = False, **kwargs) -> None: 194 super().__init__([point], xform_matrix=None, subtype=Types.INTERSECTION, **kwargs) 195 self._point = point 196 self.division1 = division1 197 self.division2 = division2 198 self.overlap = None 199 self.endpoint = endpoint 200 self.division = None # used for fragment divisions' DCEL structure 201 202 common_properties(self, id_only=True) 203 204 def _update(self, xform_matrix: array, reps=0): 205 """Update the transformation matrix of the intersection. 206 207 Args: 208 xform_matrix (array): Transformation matrix. 209 reps (int, optional): Number of repetitions. Defaults to 0. 210 211 Returns: 212 Any: Updated intersection or list of updated intersections. 213 """ 214 if reps == 0: 215 self.xform_matrix = self.xform_matrix @ xform_matrix 216 res = self 217 else: 218 res = [] 219 for _ in range(reps): 220 shape = self.copy() 221 shape._update(xform_matrix) 222 res.append(shape) 223 224 return res 225 226 def copy(self): 227 """Create a copy of the intersection. 228 229 Returns: 230 Intersection: A copy of the intersection. 231 """ 232 intersection = Intersection(self.point, self.division1, self.division2) 233 for attrib in shape_style_map: 234 setattr(intersection, attrib, getattr(self, attrib)) 235 custom_attribs = custom_attributes(self) 236 for attrib in custom_attribs: 237 setattr(intersection, attrib, getattr(self, attrib)) 238 return intersection 239 240 @property 241 def point(self): 242 """Return the intersection point. 243 244 Returns: 245 list: Intersection point coordinates. 246 """ 247 return list(np.array([*self._point, 1.0]) @ self.xform_matrix)[:2] 248 249 def __str__(self): 250 """String representation of the intersection. 251 252 Returns: 253 str: String representation. 254 """ 255 return ( 256 f"Intersection({[round(x, defaults['n_round']) for x in self.point]}, " 257 f"{tuple(list([self.division1, self.division2]))}" 258 ) 259 260 def __repr__(self): 261 """String representation of the intersection. 262 263 Returns: 264 str: String representation. 265 """ 266 return str(self) 267 268 def __eq__(self, other): 269 """Check if two intersections are equal. 270 271 Args: 272 other (Intersection): Another intersection. 273 274 Returns: 275 bool: True if equal, False otherwise. 276 """ 277 return close_points2(self.point, other.point, dist2=defaults["dist_tol"] ** 2)
Intersection of two divisions. They are at the endpoints of Section objects. A division can have multiple sections and multiple intersections. They can be located at the end of a division.
Arguments:
- point (tuple): (x, y) coordinates of the intersection point.
- division1 (Division): First division.
- division2 (Division, optional): Second division. Defaults to None.
- endpoint (bool, optional): If the intersection is at the end of a division, then endpoint is True. Defaults to False.
- **kwargs: Additional attributes for cosmetic/drawing purposes.
193 def __init__(self, point: tuple, division1: "Division", division2: "Division" = None, endpoint: bool = False, **kwargs) -> None: 194 super().__init__([point], xform_matrix=None, subtype=Types.INTERSECTION, **kwargs) 195 self._point = point 196 self.division1 = division1 197 self.division2 = division2 198 self.overlap = None 199 self.endpoint = endpoint 200 self.division = None # used for fragment divisions' DCEL structure 201 202 common_properties(self, id_only=True)
Initialize a Shape object.
Arguments:
- points (Sequence[Point], optional): The points that make up the shape.
- closed (bool, optional): Whether the shape is closed. Defaults to False.
- xform_matrix (np.array, optional): The transformation matrix. Defaults to None.
- **kwargs (dict): Additional attributes for the shape.
Raises:
- ValueError: If the provided subtype is not valid.
226 def copy(self): 227 """Create a copy of the intersection. 228 229 Returns: 230 Intersection: A copy of the intersection. 231 """ 232 intersection = Intersection(self.point, self.division1, self.division2) 233 for attrib in shape_style_map: 234 setattr(intersection, attrib, getattr(self, attrib)) 235 custom_attribs = custom_attributes(self) 236 for attrib in custom_attribs: 237 setattr(intersection, attrib, getattr(self, attrib)) 238 return intersection
Create a copy of the intersection.
Returns:
Intersection: A copy of the intersection.
240 @property 241 def point(self): 242 """Return the intersection point. 243 244 Returns: 245 list: Intersection point coordinates. 246 """ 247 return list(np.array([*self._point, 1.0]) @ self.xform_matrix)[:2]
Return the intersection point.
Returns:
list: Intersection point coordinates.
Inherited Members
280class Partition(Shape): 281 """These are the polygons of the non-interlaced geometry. 282 Fragments and partitions are scaled versions of each other. 283 284 Args: 285 points (list): List of points defining the partition. 286 **kwargs: Additional attributes for cosmetic/drawing purposes. 287 """ 288 289 def __init__(self, points, **kwargs): 290 super().__init__(points, **kwargs) 291 self.subtype = Types.PART 292 self.area = polygon_area(self.vertices) 293 self.CG = polygon_cg(self.vertices) 294 common_properties(self) 295 296 def __str__(self): 297 """String representation of the partition. 298 299 Returns: 300 str: String representation. 301 """ 302 return f"Part({self.vertices})" 303 304 def __repr__(self): 305 """String representation of the partition. 306 307 Returns: 308 str: String representation. 309 """ 310 return self.__str__()
These are the polygons of the non-interlaced geometry. Fragments and partitions are scaled versions of each other.
Arguments:
- points (list): List of points defining the partition.
- **kwargs: Additional attributes for cosmetic/drawing purposes.
289 def __init__(self, points, **kwargs): 290 super().__init__(points, **kwargs) 291 self.subtype = Types.PART 292 self.area = polygon_area(self.vertices) 293 self.CG = polygon_cg(self.vertices) 294 common_properties(self)
Initialize a Shape object.
Arguments:
- points (Sequence[Point], optional): The points that make up the shape.
- closed (bool, optional): Whether the shape is closed. Defaults to False.
- xform_matrix (np.array, optional): The transformation matrix. Defaults to None.
- **kwargs (dict): Additional attributes for the shape.
Raises:
- ValueError: If the provided subtype is not valid.
672 @property 673 def area(self) -> float: 674 """Return the area of the shape. 675 676 Returns: 677 float: The area of the shape. 678 """ 679 if self.closed: 680 vertices = self.vertices[:] 681 if not close_points2(vertices[0], vertices[-1], dist2=self.dist_tol2): 682 vertices = list(vertices) + [vertices[0]] 683 res = polygon_area(vertices) 684 else: 685 res = 0 686 687 return res
Return the area of the shape.
Returns:
float: The area of the shape.
Inherited Members
313class Fragment(Shape): 314 """A Fragment is a collection of section objects that are connected 315 to each other. These sections are already defined. They belong to 316 the polyline objects in a lace. Fragments can be open or closed. 317 They are created by the lace object. 318 319 Args: 320 points (list): List of points defining the fragment. 321 **kwargs: Additional attributes for cosmetic/drawing purposes. 322 """ 323 324 def __init__(self, points, **kwargs): 325 super().__init__(points, **kwargs) 326 self.subtype = Types.FRAGMENT 327 self.area = polygon_area(self.vertices) 328 self.sections = [] 329 self.intersections = [] 330 self.inner_lines = [] 331 self._divisions = [] 332 self.CG = polygon_cg(self.vertices) 333 common_properties(self) 334 335 def __str__(self): 336 """String representation of the fragment. 337 338 Returns: 339 str: String representation. 340 """ 341 return f"Fragment({self.vertices})" 342 343 def __repr__(self): 344 """String representation of the fragment. 345 346 Returns: 347 str: String representation. 348 """ 349 return self.__str__() 350 351 @property 352 def divisions(self): 353 """Return the divisions of the fragment. 354 355 Returns: 356 list: List of divisions. 357 """ 358 return self._divisions 359 360 @property 361 def center(self): 362 """Return the center of the fragment. 363 364 Returns: 365 list: Center coordinates. 366 """ 367 return self.CG 368 369 def _set_divisions(self, dist_tol=None): 370 if dist_tol is None: 371 dist_tol = defaults["dist_tol"] 372 dist_tol2 = dist_tol * dist_tol # squared distance tolerance 373 d_points__section = {} 374 for section in self.sections: 375 start = section.start.point 376 end = section.end.point 377 start = round(start[0], 2), round(start[1], 2) 378 end = round(end[0], 2), round(end[1], 2) 379 d_points__section[(start, end)] = section 380 d_points__section[(end, start)] = section 381 382 coord_pairs = connected_pairs(self.vertices) 383 self._divisions = [] 384 for pair in coord_pairs: 385 x1, y1 = round_point(pair[0]) 386 x2, y2 = round_point(pair[1]) 387 division = Division((x1, y1), (x2, y2)) 388 division.section = d_points__section[((x1, y1), (x2, y2))] 389 division.fragment = self 390 start_point = round_point(division.section.start.point) 391 end_point = round_point(division.section.end.point) 392 if close_points2(start_point, (x1, y1), dist2=dist_tol2): 393 division.intersections = [division.section.start, division.section.end] 394 division.section.start.division = division 395 elif close_points2(end_point, (x1, y1), dist2=dist_tol2): 396 division.intersections = [division.section.end, division.section.start] 397 division.section.end.division = division 398 else: 399 raise ValueError("Division does not match section") 400 self._divisions.append(division) 401 n = len(self._divisions) 402 for i, division in enumerate(self._divisions): 403 division.prev = self._divisions[i - 1] 404 division.next = self._divisions[(i + 1) % n] 405 406 def _set_twin_divisions(self): 407 for division in self.divisions: 408 section = division.section 409 if section.twin and section.twin.fragment: 410 twin_fragment = section.twin.fragment 411 distances = [] 412 for _, division2 in enumerate(twin_fragment.divisions): 413 dist = distance( 414 division.section.mid_point, division2.section.mid_point 415 ) 416 distances.append((dist, division2)) 417 distances.sort(key=lambda x: x[0]) 418 division.twin = distances[0][1]
A Fragment is a collection of section objects that are connected to each other. These sections are already defined. They belong to the polyline objects in a lace. Fragments can be open or closed. They are created by the lace object.
Arguments:
- points (list): List of points defining the fragment.
- **kwargs: Additional attributes for cosmetic/drawing purposes.
324 def __init__(self, points, **kwargs): 325 super().__init__(points, **kwargs) 326 self.subtype = Types.FRAGMENT 327 self.area = polygon_area(self.vertices) 328 self.sections = [] 329 self.intersections = [] 330 self.inner_lines = [] 331 self._divisions = [] 332 self.CG = polygon_cg(self.vertices) 333 common_properties(self)
Initialize a Shape object.
Arguments:
- points (Sequence[Point], optional): The points that make up the shape.
- closed (bool, optional): Whether the shape is closed. Defaults to False.
- xform_matrix (np.array, optional): The transformation matrix. Defaults to None.
- **kwargs (dict): Additional attributes for the shape.
Raises:
- ValueError: If the provided subtype is not valid.
672 @property 673 def area(self) -> float: 674 """Return the area of the shape. 675 676 Returns: 677 float: The area of the shape. 678 """ 679 if self.closed: 680 vertices = self.vertices[:] 681 if not close_points2(vertices[0], vertices[-1], dist2=self.dist_tol2): 682 vertices = list(vertices) + [vertices[0]] 683 res = polygon_area(vertices) 684 else: 685 res = 0 686 687 return res
Return the area of the shape.
Returns:
float: The area of the shape.
351 @property 352 def divisions(self): 353 """Return the divisions of the fragment. 354 355 Returns: 356 list: List of divisions. 357 """ 358 return self._divisions
Return the divisions of the fragment.
Returns:
list: List of divisions.
360 @property 361 def center(self): 362 """Return the center of the fragment. 363 364 Returns: 365 list: Center coordinates. 366 """ 367 return self.CG
Return the center of the fragment.
Returns:
list: Center coordinates.
Inherited Members
421class Section(Shape): 422 """A section is a line segment between two intersections. 423 A division can have multiple sections. Sections are used to 424 draw the over/under plaits. 425 426 Args: 427 start (Intersection): Start intersection. 428 end (Intersection): End intersection. 429 is_overlap (bool, optional): If the section is an overlap. Defaults to False. 430 overlap (Overlap, optional): Overlap object. Defaults to None. 431 is_over (bool, optional): If the section is over. Defaults to False. 432 twin (Section, optional): Twin section. Defaults to None. 433 fragment (Fragment, optional): Fragment object. Defaults to None. 434 **kwargs: Additional attributes for cosmetic/drawing purposes. 435 """ 436 437 def __init__( 438 self, 439 start: Intersection = None, 440 end: Intersection = None, 441 is_overlap: bool = False, 442 overlap: "Overlap" = None, 443 is_over: bool = False, 444 twin: "Section" = None, 445 fragment: "Fragment" = None, 446 **kwargs, 447 ): 448 super().__init__([start.point, end.point], subtype=Types.SECTION, **kwargs) 449 self.start = start 450 self.end = end 451 self.is_overlap = is_overlap 452 self.overlap = overlap 453 self.is_over = is_over 454 self.twin = twin 455 self.fragment = fragment 456 super().__init__([start.point, end.point], subtype=Types.SECTION, **kwargs) 457 self.length = distance(self.start.point, self.end.point) 458 self.mid_point = [ 459 (self.start.point[0] + self.end.point[0]) / 2, 460 (self.start.point[1] + self.end.point[1]) / 2, 461 ] 462 common_properties(self) 463 464 def copy(self): 465 """Create a copy of the section. 466 467 Returns: 468 Section: A copy of the section. 469 """ 470 overlap = self.overlap.copy() if self.overlap else None 471 start = self.start.copy() 472 end = self.end.copy() 473 section = Section(start, end, self.is_overlap, overlap, self.is_over) 474 475 return section 476 477 def end_point(self): 478 """Return the end point of the section. 479 480 Returns: 481 Intersection: End intersection. 482 """ 483 if self.start.endpoint: 484 res = self.start 485 elif self.end.endpoint: 486 res = self.end 487 else: 488 res = None 489 490 return res 491 492 def __str__(self): 493 """String representation of the section. 494 495 Returns: 496 str: String representation. 497 """ 498 return f"Section({self.start}, {self.end})" 499 500 def __repr__(self): 501 """String representation of the section. 502 503 Returns: 504 str: String representation. 505 """ 506 return self.__str__() 507 508 @property 509 def is_endpoint(self): 510 """Return True if the section is an endpoint. 511 512 Returns: 513 bool: True if endpoint, False otherwise. 514 """ 515 return self.start.endpoint or self.end.endpoint
A section is a line segment between two intersections. A division can have multiple sections. Sections are used to draw the over/under plaits.
Arguments:
- start (Intersection): Start intersection.
- end (Intersection): End intersection.
- is_overlap (bool, optional): If the section is an overlap. Defaults to False.
- overlap (Overlap, optional): Overlap object. Defaults to None.
- is_over (bool, optional): If the section is over. Defaults to False.
- twin (Section, optional): Twin section. Defaults to None.
- fragment (Fragment, optional): Fragment object. Defaults to None.
- **kwargs: Additional attributes for cosmetic/drawing purposes.
437 def __init__( 438 self, 439 start: Intersection = None, 440 end: Intersection = None, 441 is_overlap: bool = False, 442 overlap: "Overlap" = None, 443 is_over: bool = False, 444 twin: "Section" = None, 445 fragment: "Fragment" = None, 446 **kwargs, 447 ): 448 super().__init__([start.point, end.point], subtype=Types.SECTION, **kwargs) 449 self.start = start 450 self.end = end 451 self.is_overlap = is_overlap 452 self.overlap = overlap 453 self.is_over = is_over 454 self.twin = twin 455 self.fragment = fragment 456 super().__init__([start.point, end.point], subtype=Types.SECTION, **kwargs) 457 self.length = distance(self.start.point, self.end.point) 458 self.mid_point = [ 459 (self.start.point[0] + self.end.point[0]) / 2, 460 (self.start.point[1] + self.end.point[1]) / 2, 461 ] 462 common_properties(self)
Initialize a Shape object.
Arguments:
- points (Sequence[Point], optional): The points that make up the shape.
- closed (bool, optional): Whether the shape is closed. Defaults to False.
- xform_matrix (np.array, optional): The transformation matrix. Defaults to None.
- **kwargs (dict): Additional attributes for the shape.
Raises:
- ValueError: If the provided subtype is not valid.
464 def copy(self): 465 """Create a copy of the section. 466 467 Returns: 468 Section: A copy of the section. 469 """ 470 overlap = self.overlap.copy() if self.overlap else None 471 start = self.start.copy() 472 end = self.end.copy() 473 section = Section(start, end, self.is_overlap, overlap, self.is_over) 474 475 return section
Create a copy of the section.
Returns:
Section: A copy of the section.
477 def end_point(self): 478 """Return the end point of the section. 479 480 Returns: 481 Intersection: End intersection. 482 """ 483 if self.start.endpoint: 484 res = self.start 485 elif self.end.endpoint: 486 res = self.end 487 else: 488 res = None 489 490 return res
Return the end point of the section.
Returns:
Intersection: End intersection.
508 @property 509 def is_endpoint(self): 510 """Return True if the section is an endpoint. 511 512 Returns: 513 bool: True if endpoint, False otherwise. 514 """ 515 return self.start.endpoint or self.end.endpoint
Return True if the section is an endpoint.
Returns:
bool: True if endpoint, False otherwise.
Inherited Members
518class Overlap(Batch): 519 """An overlap is a collection of four connected sections. 520 521 Args: 522 intersections (list[Intersection], optional): List of intersections. Defaults to None. 523 sections (list[Section], optional): List of sections. Defaults to None. 524 visited (bool, optional): If the overlap is visited. Defaults to False. 525 drawable (bool, optional): If the overlap is drawable. Defaults to True. 526 **kwargs: Additional attributes for cosmetic/drawing purposes. 527 """ 528 529 def __init__( 530 self, 531 intersections: list[Intersection] = None, 532 sections: list[Section] = None, 533 visited=False, 534 drawable=True, 535 **kwargs, 536 ): 537 self.intersections = intersections 538 self.sections = sections 539 super().__init__(sections, **kwargs) 540 self.subtype = Types.OVERLAP 541 self.visited = visited 542 self.drawable = drawable 543 common_properties(self) 544 545 def __str__(self): 546 """String representation of the overlap. 547 548 Returns: 549 str: String representation. 550 """ 551 return f"Overlap({self.id})" 552 553 def __repr__(self): 554 """String representation of the overlap. 555 556 Returns: 557 str: String representation. 558 """ 559 return f"Overlap({self.id})"
An overlap is a collection of four connected sections.
Arguments:
- intersections (list[Intersection], optional): List of intersections. Defaults to None.
- sections (list[Section], optional): List of sections. Defaults to None.
- visited (bool, optional): If the overlap is visited. Defaults to False.
- drawable (bool, optional): If the overlap is drawable. Defaults to True.
- **kwargs: Additional attributes for cosmetic/drawing purposes.
529 def __init__( 530 self, 531 intersections: list[Intersection] = None, 532 sections: list[Section] = None, 533 visited=False, 534 drawable=True, 535 **kwargs, 536 ): 537 self.intersections = intersections 538 self.sections = sections 539 super().__init__(sections, **kwargs) 540 self.subtype = Types.OVERLAP 541 self.visited = visited 542 self.drawable = drawable 543 common_properties(self)
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.
Inherited Members
- simetri.graphics.batch.Batch
- type
- modifiers
- blend_mode
- alpha
- line_alpha
- fill_alpha
- text_alpha
- clip
- mask
- even_odd_rule
- blend_group
- transparency_group
- set_attribs
- set_batch_attr
- proximity
- append
- reverse
- insert
- remove
- pop
- clear
- extend
- iter_elements
- all_elements
- all_shapes
- all_vertices
- all_segments
- as_graph
- graph_summary
- merge_shapes
- all_polygons
- copy
- b_box
562class Division(Shape): 563 """A division is a line segment between two intersections. 564 565 Args: 566 p1 (tuple): Start point. 567 p2 (tuple): End point. 568 xform_matrix (array, optional): Transformation matrix. Defaults to None. 569 **kwargs: Additional attributes for cosmetic/drawing purposes. 570 """ 571 572 def __init__(self, p1, p2, xform_matrix=None, **kwargs): 573 super().__init__([p1, p2], subtype=Types.DIVISION, **kwargs) 574 self.p1 = p1 575 self.p2 = p2 576 self.intersections = [] 577 self.twin = None # used for fragment divisions only 578 self.section = None # used for fragment divisions only 579 self.fragment = None # used for fragment divisions only 580 self.next = None # used for fragment divisions only 581 self.prev = None # used for fragment divisions only 582 self.sections = [] 583 super().__init__( 584 [p1, p2], subtype=Types.DIVISION, xform_matrix=xform_matrix, **kwargs 585 ) 586 common_properties(self) 587 588 def _update(self, xform_matrix, reps=0): 589 """Update the transformation matrix of the division. 590 591 Args: 592 xform_matrix (array): Transformation matrix. 593 reps (int, optional): Number of repetitions. Defaults to 0. 594 595 Returns: 596 Any: Updated division or list of updated divisions. 597 """ 598 if reps == 0: 599 self.xform_matrix = self.xform_matrix @ xform_matrix 600 res = self 601 else: 602 res = [] 603 for _ in range(reps): 604 shape = self.copy() 605 shape._update(xform_matrix) 606 res.append(shape) 607 608 return res 609 610 def __str__(self): 611 """String representation of the division. 612 613 Returns: 614 str: String representation. 615 """ 616 return ( 617 f"Division(({self.p1[0]:.2f}, {self.p1[1]:.2f}), " 618 f"({self.p2[0]:.2f}, {self.p2[1]:.2f}))" 619 ) 620 621 def __repr__(self): 622 """String representation of the division. 623 624 Returns: 625 str: String representation. 626 """ 627 return ( 628 f"Division(({self.p1[0]:.2f}, {self.p1[1]:.2f}), " 629 f"({self.p2[0]:.2f}, {self.p2[1]:.2f}))" 630 ) 631 632 def copy( 633 self, 634 section: Section = None, 635 twin: Section = None, 636 ): 637 """Create a copy of the division. 638 639 Args: 640 section (Section, optional): Section object. Defaults to None. 641 twin (Section, optional): Twin section. Defaults to None. 642 643 Returns: 644 Division: A copy of the division. 645 """ 646 division = Division(self.p1[:], self.p2[:], np.copy(self.xform_matrix)) 647 for attrib in shape_style_map: 648 setattr(division, attrib, getattr(self, attrib)) 649 division.twin = twin 650 division.section = section 651 division.fragment = self.fragment 652 division.next = self.next 653 division.prev = self.prev 654 division.sections = [x.copy() for x in self.sections] 655 custom_attributes_ = custom_attributes(self) 656 for attrib in custom_attributes_: 657 setattr(division, attrib, getattr(self, attrib)) 658 return division 659 660 def _merged_sections(self): 661 """Merge sections of the division. 662 663 Returns: 664 list: List of merged sections. 665 """ 666 chains = [] 667 chain = [self.intersections[0]] 668 sections = self.sections[:] 669 for section in sections: 670 if not section.is_over: 671 if section.start.id == chain[-1].id: 672 chain.append(section.end) 673 else: 674 chains.append(chain) 675 chain = [section.start, section.end] 676 if chain not in chains: 677 chains.append(chain) 678 return chains 679 680 def _sort_intersections(self) -> None: 681 """Sort intersections of the division.""" 682 self.intersections.sort(key=lambda x: distance(self.p1, x.point)) 683 684 def is_connected(self, other: "Division") -> bool: 685 """Return True if the division is connected to another division. 686 687 Args: 688 other (Division): Another division. 689 690 Returns: 691 bool: True if connected, False otherwise. 692 """ 693 return self.p1 in other.end_points or self.p2 in other.end_points 694 695 @property 696 def end_points(self): 697 """Return the end points of the division. 698 699 Returns: 700 list: List of end points. 701 """ 702 return [self.p1, self.p2] 703 704 @property 705 def start(self) -> Intersection: 706 """Return the start intersection of the division. 707 708 Returns: 709 Intersection: Start intersection. 710 """ 711 return self.intersections[0] 712 713 @property 714 def end(self) -> Intersection: 715 """Return the end intersection of the division. 716 717 Returns: 718 Intersection: End intersection. 719 """ 720 return self.intersections[-1]
A division is a line segment between two intersections.
Arguments:
- p1 (tuple): Start point.
- p2 (tuple): End point.
- xform_matrix (array, optional): Transformation matrix. Defaults to None.
- **kwargs: Additional attributes for cosmetic/drawing purposes.
572 def __init__(self, p1, p2, xform_matrix=None, **kwargs): 573 super().__init__([p1, p2], subtype=Types.DIVISION, **kwargs) 574 self.p1 = p1 575 self.p2 = p2 576 self.intersections = [] 577 self.twin = None # used for fragment divisions only 578 self.section = None # used for fragment divisions only 579 self.fragment = None # used for fragment divisions only 580 self.next = None # used for fragment divisions only 581 self.prev = None # used for fragment divisions only 582 self.sections = [] 583 super().__init__( 584 [p1, p2], subtype=Types.DIVISION, xform_matrix=xform_matrix, **kwargs 585 ) 586 common_properties(self)
Initialize a Shape object.
Arguments:
- points (Sequence[Point], optional): The points that make up the shape.
- closed (bool, optional): Whether the shape is closed. Defaults to False.
- xform_matrix (np.array, optional): The transformation matrix. Defaults to None.
- **kwargs (dict): Additional attributes for the shape.
Raises:
- ValueError: If the provided subtype is not valid.
632 def copy( 633 self, 634 section: Section = None, 635 twin: Section = None, 636 ): 637 """Create a copy of the division. 638 639 Args: 640 section (Section, optional): Section object. Defaults to None. 641 twin (Section, optional): Twin section. Defaults to None. 642 643 Returns: 644 Division: A copy of the division. 645 """ 646 division = Division(self.p1[:], self.p2[:], np.copy(self.xform_matrix)) 647 for attrib in shape_style_map: 648 setattr(division, attrib, getattr(self, attrib)) 649 division.twin = twin 650 division.section = section 651 division.fragment = self.fragment 652 division.next = self.next 653 division.prev = self.prev 654 division.sections = [x.copy() for x in self.sections] 655 custom_attributes_ = custom_attributes(self) 656 for attrib in custom_attributes_: 657 setattr(division, attrib, getattr(self, attrib)) 658 return division
Create a copy of the division.
Arguments:
- section (Section, optional): Section object. Defaults to None.
- twin (Section, optional): Twin section. Defaults to None.
Returns:
Division: A copy of the division.
684 def is_connected(self, other: "Division") -> bool: 685 """Return True if the division is connected to another division. 686 687 Args: 688 other (Division): Another division. 689 690 Returns: 691 bool: True if connected, False otherwise. 692 """ 693 return self.p1 in other.end_points or self.p2 in other.end_points
Return True if the division is connected to another division.
Arguments:
- other (Division): Another division.
Returns:
bool: True if connected, False otherwise.
695 @property 696 def end_points(self): 697 """Return the end points of the division. 698 699 Returns: 700 list: List of end points. 701 """ 702 return [self.p1, self.p2]
Return the end points of the division.
Returns:
list: List of end points.
704 @property 705 def start(self) -> Intersection: 706 """Return the start intersection of the division. 707 708 Returns: 709 Intersection: Start intersection. 710 """ 711 return self.intersections[0]
Return the start intersection of the division.
Returns:
Intersection: Start intersection.
713 @property 714 def end(self) -> Intersection: 715 """Return the end intersection of the division. 716 717 Returns: 718 Intersection: End intersection. 719 """ 720 return self.intersections[-1]
Return the end intersection of the division.
Returns:
Intersection: End intersection.
Inherited Members
723class Polyline(Shape): 724 """ 725 Connected points, similar to Shape objects. 726 They can be closed or open. 727 They are defined by a sequence of points. 728 They have divisions, sections, and intersections. 729 730 Args: 731 points (list): List of points defining the polyline. 732 closed (bool, optional): If the polyline is closed. Defaults to True. 733 xform_matrix (array, optional): Transformation matrix. Defaults to None. 734 **kwargs: Additional attributes for cosmetic/drawing purposes. 735 """ 736 737 def __init__(self, points, closed=True, xform_matrix=None, **kwargs): 738 self.__dict__["style"] = ShapeStyle() 739 self.__dict__["_style_map"] = shape_style_map 740 self._set_aliases() 741 self.closed = closed 742 kwargs["subtype"] = Types.POLYLINE 743 super().__init__(points, closed=closed, xform_matrix=xform_matrix, **kwargs) 744 self._set_divisions() 745 if not self.closed: 746 self._set_intersections() 747 common_properties(self) 748 749 def _update(self, xform_matrix, reps=0): 750 """Update the transformation matrix of the polyline. 751 752 Args: 753 xform_matrix (array): Transformation matrix. 754 reps (int, optional): Number of repetitions. Defaults to 0. 755 756 Returns: 757 Any: Updated polyline or list of updated polylines. 758 """ 759 if reps == 0: 760 self.xform_matrix = self.xform_matrix @ xform_matrix 761 for division in self.divisions: 762 division._update(xform_matrix, reps=reps) 763 res = self 764 else: 765 res = [] 766 for _ in range(reps): 767 shape = self.copy() 768 shape._update(xform_matrix) 769 res.append(shape) 770 771 return res 772 773 def __str__(self): 774 """String representation of the polyline. 775 776 Returns: 777 str: String representation. 778 """ 779 return f"Polyline({self.final_coords[:, :2]})" 780 781 def __repr__(self): 782 """String representation of the polyline. 783 784 Returns: 785 str: String representation. 786 """ 787 return self.__str__() 788 789 def iter_sections(self) -> Iterator: 790 """Iterate over the sections of the polyline. 791 792 Yields: 793 Section: Section object. 794 """ 795 for division in self.divisions: 796 yield from division.sections 797 798 def iter_intersections(self): 799 """Iterate over the intersections of the polyline. 800 801 Yields: 802 Intersection: Intersection object. 803 """ 804 for division in self.divisions: 805 yield from division.intersections 806 807 @property 808 def intersections(self): 809 """Return the intersections of the polyline. 810 811 Returns: 812 list: List of intersections. 813 """ 814 res = [] 815 for division in self.divisions: 816 res.extend(division.intersections) 817 return res 818 819 @property 820 def area(self): 821 """Return the area of the polygon. 822 823 Returns: 824 float: Area of the polygon. 825 """ 826 return polygon_area(self.vertices) 827 828 @property 829 def sections(self): 830 """Return the sections of the polyline. 831 832 Returns: 833 list: List of sections. 834 """ 835 sections = [] 836 for division in self.divisions: 837 sections.extend(division.sections) 838 return sections 839 840 @property 841 def divisions(self): 842 """Return the divisions of the polyline. 843 844 Returns: 845 list: List of divisions. 846 """ 847 return self.__dict__["divisions"] 848 849 def _set_divisions(self): 850 vertices = self.vertices 851 if self.closed: 852 vertices = list(vertices) + [vertices[0]] 853 pairs = connected_pairs(vertices) 854 divisions = [Division(p1, p2) for p1, p2 in pairs] 855 self.__dict__["divisions"] = divisions 856 857 def _set_intersections(self): 858 """Fake intersections for open lines.""" 859 division1 = self.divisions[0] 860 division2 = self.divisions[-1] 861 x1 = Intersection(division1.p1, division1, None, True) 862 division1.intersections = [x1] 863 x2 = Intersection(division2.p2, division2, None, True) 864 if division1.id == division2.id: 865 division1.intersections.append(x2) 866 else: 867 division2.intersections = [x2]
Connected points, similar to Shape objects. They can be closed or open. They are defined by a sequence of points. They have divisions, sections, and intersections.
Arguments:
- points (list): List of points defining the polyline.
- closed (bool, optional): If the polyline is closed. Defaults to True.
- xform_matrix (array, optional): Transformation matrix. Defaults to None.
- **kwargs: Additional attributes for cosmetic/drawing purposes.
737 def __init__(self, points, closed=True, xform_matrix=None, **kwargs): 738 self.__dict__["style"] = ShapeStyle() 739 self.__dict__["_style_map"] = shape_style_map 740 self._set_aliases() 741 self.closed = closed 742 kwargs["subtype"] = Types.POLYLINE 743 super().__init__(points, closed=closed, xform_matrix=xform_matrix, **kwargs) 744 self._set_divisions() 745 if not self.closed: 746 self._set_intersections() 747 common_properties(self)
Initialize a Shape object.
Arguments:
- points (Sequence[Point], optional): The points that make up the shape.
- closed (bool, optional): Whether the shape is closed. Defaults to False.
- xform_matrix (np.array, optional): The transformation matrix. Defaults to None.
- **kwargs (dict): Additional attributes for the shape.
Raises:
- ValueError: If the provided subtype is not valid.
789 def iter_sections(self) -> Iterator: 790 """Iterate over the sections of the polyline. 791 792 Yields: 793 Section: Section object. 794 """ 795 for division in self.divisions: 796 yield from division.sections
Iterate over the sections of the polyline.
Yields:
Section: Section object.
798 def iter_intersections(self): 799 """Iterate over the intersections of the polyline. 800 801 Yields: 802 Intersection: Intersection object. 803 """ 804 for division in self.divisions: 805 yield from division.intersections
Iterate over the intersections of the polyline.
Yields:
Intersection: Intersection object.
807 @property 808 def intersections(self): 809 """Return the intersections of the polyline. 810 811 Returns: 812 list: List of intersections. 813 """ 814 res = [] 815 for division in self.divisions: 816 res.extend(division.intersections) 817 return res
Return the intersections of the polyline.
Returns:
list: List of intersections.
819 @property 820 def area(self): 821 """Return the area of the polygon. 822 823 Returns: 824 float: Area of the polygon. 825 """ 826 return polygon_area(self.vertices)
Return the area of the polygon.
Returns:
float: Area of the polygon.
828 @property 829 def sections(self): 830 """Return the sections of the polyline. 831 832 Returns: 833 list: List of sections. 834 """ 835 sections = [] 836 for division in self.divisions: 837 sections.extend(division.sections) 838 return sections
Return the sections of the polyline.
Returns:
list: List of sections.
840 @property 841 def divisions(self): 842 """Return the divisions of the polyline. 843 844 Returns: 845 list: List of divisions. 846 """ 847 return self.__dict__["divisions"]
Return the divisions of the polyline.
Returns:
list: List of divisions.
Inherited Members
870class ParallelPolyline(Batch): 871 """A ParallelPolylines is a collection of parallel Polylines. 872 They are defined by a main polyline and a list of offset 873 values (that can be negative or positive). 874 875 Args: 876 polyline (Polyline): Main polyline. 877 offset (float): Offset value. 878 lace (Lace): Lace object. 879 under (bool, optional): If the polyline is under. Defaults to False. 880 closed (bool, optional): If the polyline is closed. Defaults to True. 881 dist_tol (float, optional): Distance tolerance. Defaults to None. 882 **kwargs: Additional attributes for cosmetic/drawing purposes. 883 """ 884 885 def __init__( 886 self, 887 polyline, 888 offset, 889 lace, 890 under=False, 891 closed=True, 892 dist_tol=None, 893 **kwargs, 894 ): 895 if dist_tol is None: 896 dist_tol = defaults["dist_tol"] 897 dist_tol2 = dist_tol * dist_tol 898 self.polyline = polyline 899 self.offset = offset 900 self.dist_tol = dist_tol 901 self.dist_tol2 = dist_tol2 902 self.closed = closed 903 self._set_offset_polylines() 904 self.polyline_list = [self.polyline] + self.offset_poly_list 905 super().__init__(self.polyline_list, **kwargs) 906 self.subtype = Types.PARALLEL_POLYLINE 907 self.overlaps = None 908 self.under = under 909 common_properties(self) 910 911 @property 912 def sections(self) -> List[Section]: 913 """Return the sections of the parallel polyline. 914 915 Returns: 916 list: List of sections. 917 """ 918 sects = [] 919 for polyline in self.polyline_list: 920 sects.extend(polyline.sections) 921 return sects 922 923 def _set_offset_polylines(self): 924 polyline = self.polyline 925 if self.closed: 926 vertices = list(polyline.vertices) 927 vertices = vertices + [vertices[0]] 928 offset_polygons = double_offset_polygons( 929 vertices, self.offset, dist_tol=self.dist_tol) 930 else: 931 offset_polylines = double_offset_polylines(polyline.vertices, self.offset) 932 polylines = [] 933 if self.closed: 934 for polygon in offset_polygons: 935 polylines.append(Polyline(polygon, closed=self.closed)) 936 else: 937 for polyline in offset_polylines: 938 polylines.append(Polyline(polyline, closed=self.closed)) 939 940 self.offset_poly_list = polylines
A ParallelPolylines is a collection of parallel Polylines. They are defined by a main polyline and a list of offset values (that can be negative or positive).
Arguments:
- polyline (Polyline): Main polyline.
- offset (float): Offset value.
- lace (Lace): Lace object.
- under (bool, optional): If the polyline is under. Defaults to False.
- closed (bool, optional): If the polyline is closed. Defaults to True.
- dist_tol (float, optional): Distance tolerance. Defaults to None.
- **kwargs: Additional attributes for cosmetic/drawing purposes.
885 def __init__( 886 self, 887 polyline, 888 offset, 889 lace, 890 under=False, 891 closed=True, 892 dist_tol=None, 893 **kwargs, 894 ): 895 if dist_tol is None: 896 dist_tol = defaults["dist_tol"] 897 dist_tol2 = dist_tol * dist_tol 898 self.polyline = polyline 899 self.offset = offset 900 self.dist_tol = dist_tol 901 self.dist_tol2 = dist_tol2 902 self.closed = closed 903 self._set_offset_polylines() 904 self.polyline_list = [self.polyline] + self.offset_poly_list 905 super().__init__(self.polyline_list, **kwargs) 906 self.subtype = Types.PARALLEL_POLYLINE 907 self.overlaps = None 908 self.under = under 909 common_properties(self)
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.
911 @property 912 def sections(self) -> List[Section]: 913 """Return the sections of the parallel polyline. 914 915 Returns: 916 list: List of sections. 917 """ 918 sects = [] 919 for polyline in self.polyline_list: 920 sects.extend(polyline.sections) 921 return sects
Return the sections of the parallel polyline.
Returns:
list: List of sections.
Inherited Members
- simetri.graphics.batch.Batch
- type
- modifiers
- blend_mode
- alpha
- line_alpha
- fill_alpha
- text_alpha
- clip
- mask
- even_odd_rule
- blend_group
- transparency_group
- set_attribs
- set_batch_attr
- proximity
- append
- reverse
- insert
- remove
- pop
- clear
- extend
- iter_elements
- all_elements
- all_shapes
- all_vertices
- all_segments
- as_graph
- graph_summary
- merge_shapes
- all_polygons
- copy
- b_box
943class Lace(Batch): 944 """ 945 A Lace is a collection of ParallelPolylines objects. 946 They are used to create interlace patterns. 947 948 Args: 949 polygon_shapes (Union[Batch, list[Shape]], optional): List of polygon shapes. Defaults to None. 950 polyline_shapes (Union[Batch, list[Shape]], optional): List of polyline shapes. Defaults to None. 951 offset (float, optional): Offset value. Defaults to 2. 952 rtol (float, optional): Relative tolerance. Defaults to None. 953 swatch (list, optional): Swatch list. Defaults to None. 954 breakpoints (list, optional): Breakpoints list. Defaults to None. 955 plait_color (colors.Color, optional): Plait color. Defaults to None. 956 draw_fragments (bool, optional): If fragments should be drawn. Defaults to True. 957 palette (list, optional): Palette list. Defaults to None. 958 color_step (int, optional): Color step. Defaults to 1. 959 with_plaits (bool, optional): If plaits should be included. Defaults to True. 960 area_threshold (float, optional): Area threshold. Defaults to None. 961 radius_threshold (float, optional): Radius threshold. Defaults to None. 962 **kwargs: Additional attributes for cosmetic/drawing purposes. 963 """ 964 965 # @timing 966 def __init__( 967 self, 968 polygon_shapes: Union[Batch, list[Shape]] = None, 969 polyline_shapes: Union[Batch, list[Shape]] = None, 970 offset: float = 2, 971 rtol: float = None, 972 swatch: list = None, 973 breakpoints: list = None, 974 plait_color: colors.Color = None, 975 draw_fragments: bool = True, 976 palette: list = None, 977 color_step: int = 1, 978 with_plaits: bool = True, 979 area_threshold: float = None, 980 radius_threshold: float = None, 981 **kwargs, 982 ) -> None: 983 validate_args(kwargs, shape_style_map) 984 ( 985 rtol, 986 swatch, 987 plait_color, 988 draw_fragments, 989 area_threshold, 990 radius_threshold, 991 ) = get_defaults( 992 [ 993 "rtol", 994 "swatch", 995 "plait_color", 996 "draw_fragments", 997 "area_threshold", 998 "radius_threshold", 999 ], 1000 [ 1001 rtol, 1002 swatch, 1003 plait_color, 1004 draw_fragments, 1005 area_threshold, 1006 radius_threshold, 1007 ], 1008 ) 1009 if polygon_shapes: 1010 polygon_shapes = polygon_shapes.merge_shapes() 1011 self.polygon_shapes = self._check_polygons(polygon_shapes) 1012 else: 1013 self.polygon_shapes = [] 1014 if polyline_shapes: 1015 polyline_shapes = polyline_shapes.merge_shapes() 1016 self.polyline_shapes = self._check_polylines(polyline_shapes) 1017 else: 1018 self.polyline_shapes = [] 1019 if not self.polygon_shapes and not self.polyline_shapes: 1020 msg = "Lace.__init__ : No polygons or polylines found." 1021 raise ValueError(msg) 1022 self.polyline_shapes = polyline_shapes 1023 self.offset = offset 1024 self.main_intersections = None 1025 self.offset_intersections = None 1026 self.xform_matrix = np.eye(3) 1027 self.rtol = rtol 1028 self.swatch = swatch 1029 self.breakpoints = breakpoints 1030 self.plait_color = plait_color 1031 self.draw_fragments = draw_fragments 1032 self.palette = palette 1033 self.color_step = color_step 1034 self.with_plaits = with_plaits 1035 self.d_intersections = {} # key, value:intersection.id, intersection 1036 self.d_connections = {} 1037 self.plaits = [] 1038 self.overlaps = [] 1039 self._groups = None 1040 self.area_threshold = area_threshold 1041 self.radius_threshold = radius_threshold 1042 if kwargs and "_copy" in kwargs: 1043 # pass the pre-computed values 1044 for k, v in kwargs: 1045 if k == "_copy": 1046 continue 1047 else: 1048 setattr(self, k, v) 1049 else: 1050 self._set_polyline_list() # main divisions are set here along with polylines 1051 # polyline.divisions is the list of Division objects 1052 self._set_parallel_poly_list() 1053 # start_time2 = time.perf_counter() 1054 self._set_intersections() 1055 # end_time2 = time.perf_counter() 1056 # print( 1057 # f"Lace.__init__ intersections computed in {end_time2 - start_time2:0.4f} seconds" 1058 # ) 1059 1060 self._set_overlaps() 1061 self._set_twin_sections() 1062 self._set_fragments() 1063 if not self.polyline_shapes: 1064 self._set_outline() 1065 # self._set_partitions() 1066 self._set_over_under() 1067 if self.with_plaits: 1068 self.set_plaits() 1069 # self._set_convex_hull() 1070 # self._set_concave_hull() 1071 # self._set_fragment_groups() 1072 # self._set_partition_groups() 1073 self._b_box = None 1074 elements = [polyline for polyline in self.parallel_poly_list] + self.fragments 1075 1076 if "debug" in kwargs: 1077 kwargs.pop("debug") 1078 super().__init__(elements, **kwargs) 1079 if kwargs and "_copy" not in kwargs: 1080 for k, v in kwargs.items(): 1081 if k in shape_style_map: 1082 setattr(self, k, v) # todo: we should check for valid values here 1083 else: 1084 raise AttributeError(f"{k}. Invalid attribute!") 1085 self.subtype = Types.LACE 1086 common_properties(self) 1087 1088 @property 1089 def center(self): 1090 """Return the center of the lace. 1091 1092 Returns: 1093 list: Center coordinates. 1094 """ 1095 return self.outline.CG 1096 1097 @property 1098 def fragment_groups(self): 1099 """Return the fragment groups of the lace. 1100 1101 Returns: 1102 dict: Dictionary of fragment groups. 1103 """ 1104 center = self.center 1105 radius_frag = [] 1106 for fragment in self.fragments: 1107 radius = int(distance(center, fragment.CG)) 1108 for rad, frag in radius_frag: 1109 if abs(radius - rad) <= 2: 1110 radius = rad 1111 break 1112 radius_frag.append((radius, fragment)) 1113 1114 radius_frag.sort(key=lambda x: x[0]) 1115 1116 d_groups = {} 1117 for i, (radius, fragment) in enumerate(radius_frag): 1118 if i == 0: 1119 d_groups[radius] = [fragment] 1120 else: 1121 if radius in d_groups: 1122 d_groups[radius].append(fragment) 1123 else: 1124 d_groups[radius] = [fragment] 1125 1126 return d_groups 1127 1128 def _check_polygons(self, polygon_shapes): 1129 if isinstance(polygon_shapes, Batch): 1130 polygon_shapes = polygon_shapes.all_shapes 1131 for polygon in polygon_shapes: 1132 if len(polygon.primary_points) < 3: 1133 msg = "Lace.__init__ found polygon with less than 3 points." 1134 raise ValueError(msg) 1135 if not polygon.closed: 1136 msg = "Lace.__init__ : Invalid polygons" 1137 raise ValueError(msg) 1138 if polygon.primary_points[0] != polygon.primary_points[-1]: 1139 polygon.primary_points.append(polygon.primary_points[0]) 1140 # check if the polygons are clockwise 1141 for polygon in polygon_shapes: 1142 if not right_handed(polygon.vertices): 1143 polygon.primary_points.reverse() 1144 1145 return polygon_shapes 1146 1147 def _check_polylines(self, polyline_shapes): 1148 if isinstance(polyline_shapes, Batch): 1149 polyline_shapes = polyline_shapes.all_shapes 1150 for polyline in polyline_shapes: 1151 if len(polyline.primary_points) < 2: 1152 msg = "Lace.__init__ found polyline with less than 2 points." 1153 raise ValueError(msg) 1154 1155 return polyline_shapes 1156 1157 def _update(self, xform_matrix, reps=0): 1158 """Update the transformation matrix of the lace. 1159 1160 Args: 1161 xform_matrix (array): Transformation matrix. 1162 reps (int, optional): Number of repetitions. Defaults to 0. 1163 1164 Returns: 1165 Any: Updated lace or list of updated laces. 1166 """ 1167 if reps == 0: 1168 self.xform_matrix = self.xform_matrix @ xform_matrix 1169 for polygon in self.polygon_shapes: 1170 polygon._update(xform_matrix) 1171 1172 if self.polyline_shapes: 1173 for polyline in self.polyline_shapes: 1174 polyline._update(xform_matrix) 1175 1176 for polyline in self.parallel_poly_list: 1177 polyline._update(xform_matrix) 1178 1179 for fragment in self.fragments: 1180 fragment._update(xform_matrix) 1181 1182 for intersection in self.main_intersections: 1183 intersection._update(xform_matrix) 1184 1185 for intersection in self.offset_intersections: 1186 intersection._update(xform_matrix) 1187 1188 for overlap in self.overlaps: 1189 overlap._update(xform_matrix) 1190 1191 for plait in self.plaits: 1192 plait._update(xform_matrix) 1193 1194 return self 1195 else: 1196 res = [] 1197 for _ in range(reps): 1198 shape = self.copy() 1199 shape._update(xform_matrix) 1200 res.append(shape) 1201 return res 1202 1203 # @timing 1204 def _set_twin_sections(self): 1205 for par_poly in self.parallel_poly_list: 1206 poly1, poly2 = par_poly.offset_poly_list 1207 for i, sec in enumerate(poly1.iter_sections()): 1208 sec1 = sec 1209 sec2 = poly2.sections[i] 1210 sec1.twin = sec2 1211 sec2.twin = sec1 1212 1213 # @timing 1214 def _set_partitions(self): 1215 for fragment in self.fragments: 1216 self.partitions = [] 1217 for fragment in self.fragments: 1218 partition = Shape(offset_polygon(fragment.vertices, self.offset)) 1219 self.partitions.append(partition) 1220 1221 # To do: This doesn't work if we have polyline shapes! 1222 def _set_outline(self): 1223 # outline is a special fragment that covers the whole lace 1224 areas = [] 1225 for fragment in self.fragments: 1226 areas.append((fragment.area, fragment)) 1227 areas.sort(reverse=True, key=lambda x: x[0]) 1228 self.outline = areas[0][1] 1229 self.fragments.remove(self.outline) 1230 # perimenter is the outline of the partitions 1231 self.perimeter = Shape(offset_polygon(self.outline.vertices, -self.offset)) 1232 # skeleton is the input polylines that the lace is based on 1233 self.skeleton = Batch(self.polyline_list) 1234 1235 def set_fragment_groups(self): 1236 # to do : handle repeated code. same in _set_partition_groups 1237 areas = [] 1238 for i, fragment in enumerate(self.fragments): 1239 areas.append((fragment.area, i)) 1240 areas.sort() 1241 bins = group_into_bins(areas, self.area_threshold) 1242 self.fragments_by_area = OrderedDict() 1243 for i, bin in enumerate(bins): 1244 area_values = [x[0] for x in bin] 1245 key = sum([x[0] for x in bin]) / len(bin) 1246 fragments = [] 1247 for area, ind in areas: 1248 if area in area_values: 1249 fragments.append(self.fragments[ind]) 1250 self.fragments_by_area[key] = fragments 1251 1252 radii = [] 1253 for i, fragment in enumerate(self.fragments): 1254 radii.append((distance(self.center, fragment.CG), i)) 1255 radii.sort() 1256 bins = group_into_bins(radii, self.radius_threshold) 1257 self.fragments_by_radius = OrderedDict() 1258 for i, bin in enumerate(bins): 1259 radius_values = [x[0] for x in bin] 1260 key = sum([x[0] for x in bin]) / len(bin) 1261 fragments = [] 1262 for radius, ind in radii: 1263 if radius in radius_values: 1264 fragments.append(self.fragments[ind]) 1265 self.fragments_by_radius[key] = fragments 1266 1267 # @timing 1268 def _set_partition_groups(self): 1269 # to do : handle repeated code. same in set_fragment_groups 1270 areas = [] 1271 for i, partition in enumerate(self.partitions): 1272 areas.append((partition.area, i)) 1273 areas.sort() 1274 bins = group_into_bins(areas, self.area_threshold) 1275 self.partitions_by_area = OrderedDict() 1276 for i, bin_ in enumerate(bins): 1277 area_values = [x[0] for x in bin_] 1278 key = sum([x[0] for x in bin_]) / len(bin_) 1279 partitions = [] 1280 for area, ind in areas: 1281 if area in area_values: 1282 partitions.append(self.partitions[ind]) 1283 self.partitions_by_area[key] = partitions 1284 1285 radii = [] 1286 for i, partition in enumerate(self.partitions): 1287 CG = polygon_center(partition.vertices) 1288 radii.append((distance(self.center, CG), i)) 1289 radii.sort() 1290 bins = group_into_bins(radii, self.radius_threshold) 1291 self.partitions_by_radius = OrderedDict() 1292 for i, bin_ in enumerate(bins): 1293 radius_values = [x[0] for x in bin_] 1294 key = sum([x[0] for x in bin_]) / len(bin_) 1295 partitions = [] 1296 for radius, ind in radii: 1297 if radius in radius_values: 1298 partitions.append(self.partitions[ind]) 1299 self.partitions_by_radius[key] = partitions 1300 1301 # @timing 1302 def _set_fragments(self): 1303 G = nx.Graph() 1304 for section in self.iter_offset_sections(): 1305 if section.is_overlap: 1306 continue 1307 G.add_edge(section.start.id, section.end.id, section=section) 1308 1309 cycles = nx.cycle_basis(G) 1310 fragments = [] 1311 d_x = self.d_intersections 1312 for cycle in cycles: 1313 cycle.append(cycle[0]) 1314 nodes = cycle 1315 edges = connected_pairs(cycle) 1316 sections = [G.edges[edge]["section"] for edge in edges] 1317 s_intersections = set() 1318 for section in sections: 1319 s_intersections.add(section.start.id) 1320 s_intersections.add(section.end.id) 1321 intersections = [self.d_intersections[i] for i in s_intersections] 1322 points = [d_x[x_id].point for x_id in nodes] 1323 if not right_handed(points): 1324 points.reverse() 1325 fragment = Fragment(points) 1326 fragment.sections = sections 1327 fragment.intersections = intersections 1328 fragments.append(fragment) 1329 1330 for fragment in fragments: 1331 for section in fragment.sections: 1332 section.fragment = fragment 1333 1334 for fragment in fragments: 1335 fragment._set_divisions() 1336 for fragment in fragments: 1337 fragment._set_twin_divisions() 1338 1339 self.fragments = fragments 1340 1341 def _set_concave_hull(self): 1342 self.concave_hull = self.outline.vertices 1343 1344 def _set_convex_hull(self): 1345 self.convex_hull = convex_hull(self.outline.vertices) 1346 1347 def copy(self): 1348 class Dummy(Lace): 1349 pass 1350 1351 # we need to copy the polyline_list and parallel_poly_list 1352 for polyline in self.polyline_list: 1353 polyline.copy() 1354 1355 def get_sketch(self): 1356 """ 1357 Create and return a Sketch object. Sketch is a Batch object 1358 with Shape elements corresponding to the vertices of the plaits 1359 and fragments of the Lace instance. They have 'plaits' and 1360 'fragments' attributes to hold lists of Shape objects populated 1361 with plait and fragment vertices of the Lace instance 1362 respectively. They are used for drawing multiple copies of the 1363 original lace pattern. They are light-weight compared to the 1364 Lace objects since they only contain sufficient data to draw the 1365 lace objects. Hundreds of these objects can be used to create 1366 wallpaper patterns or other patterns without having to contain 1367 unnecessary data. They do not share points with the original 1368 Lace object. 1369 1370 Arguments: 1371 ---------- 1372 None 1373 1374 Prerequisites: 1375 -------------- 1376 * A lace object to be copied. 1377 1378 Side effects: 1379 ------------- 1380 None 1381 1382 Return: 1383 -------- 1384 A Sketch object. 1385 """ 1386 fragments = [] 1387 for fragment in self.fragments: 1388 polygon = Shape(fragment.vertices) 1389 polygon.fill = True 1390 polygon.subtype = Types.FRAGMENT 1391 fragments.append(polygon) 1392 1393 plaits = [] 1394 for plait in self.plaits: 1395 polygon = Shape(plait.vertices) 1396 polygon.fill = True 1397 polygon.subtype = Types.PLAIT 1398 plaits.append(polygon) 1399 1400 sketch = Batch((fragments + plaits)) 1401 sketch.fragments = fragments 1402 sketch.plaits = plaits 1403 sketch.outline = self.outline 1404 sketch.subtype = Types.SKETCH 1405 1406 sketch.draw_plaits = True 1407 sketch.draw_fragments = True 1408 return sketch 1409 1410 def group_fragments(self, tol=None): 1411 """Group the fragments by the number of vertices and the area. 1412 1413 Args: 1414 tol (float, optional): Tolerance value. Defaults to None. 1415 1416 Returns: 1417 list: List of grouped fragments. 1418 """ 1419 if tol is None: 1420 tol = defaults["tol"] 1421 frags = self.fragments 1422 vert_groups = [ 1423 [frag for frag in frags if len(frag.vertices) == n] 1424 for n in set([len(f.vertices) for f in frags]) 1425 ] 1426 groups = [] 1427 for group in vert_groups: 1428 areas = [ 1429 [f for f in group if isclose(f.area, area, rtol=tol)] 1430 for area in set([frag.area for frag in group]) 1431 ] 1432 areas.sort(key=lambda x: x[0].area, reverse=True) 1433 groups.append(areas) 1434 groups.sort(key=lambda x: x[0][0].area, reverse=True) 1435 1436 return groups 1437 1438 def get_fragment_cycles(self): 1439 """ 1440 Iterate over the offset sections and create a graph of the 1441 intersections (start and end of the sections). Then find the 1442 cycles in the graph. self.d_intersections is used to map 1443 the graph nodes to the actual intersection points. 1444 1445 Returns: 1446 list: List of fragment cycles. 1447 """ 1448 graph_edges = [] 1449 for section in self.iter_offset_sections(): 1450 if section.is_overlap: 1451 continue 1452 graph_edges.append((section.start.id, section.end.id)) 1453 1454 return get_cycles(graph_edges) 1455 1456 def _set_inner_lines(self, item, n, offset, line_color=colors.blue, line_width=1): 1457 for i in range(n): 1458 vertices = item.vertices 1459 dist_tol = defaults["dist_tol"] 1460 offset_poly = offset_polygon_points( 1461 vertices, offset * (i + 1), dist_tol=dist_tol 1462 ) 1463 shape = Shape(offset_poly) 1464 shape.fill = False 1465 shape.line_width = line_width 1466 shape.line_color = line_color 1467 item.inner_lines.append(shape) 1468 1469 def set_plait_lines(self, n, offset, line_color=colors.blue, line_width=1): 1470 """Create offset lines inside the plaits of the lace. 1471 1472 Args: 1473 n (int): Number of lines. 1474 offset (float): Offset value. 1475 line_color (colors.Color, optional): Line color. Defaults to colors.blue. 1476 line_width (int, optional): Line width. Defaults to 1. 1477 """ 1478 for plait in self.plaits: 1479 plait.inner_lines = [] 1480 self._set_inner_lines(plait, n, offset, line_color, line_width) 1481 1482 def set_fragment_lines( 1483 self, 1484 n: int, 1485 offset: float, 1486 line_color: colors.Color = colors.blue, 1487 line_width=1, 1488 ) -> None: 1489 """ 1490 Create offset lines inside the fragments of the lace. 1491 1492 Args: 1493 n (int): Number of lines. 1494 offset (float): Offset value. 1495 line_color (colors.Color, optional): Line color. Defaults to colors.blue. 1496 line_width (int, optional): Line width. Defaults to 1. 1497 """ 1498 for fragment in self.fragments: 1499 fragment.inner_lines = [] 1500 self._set_inner_lines(fragment, n, offset, line_color, line_width) 1501 1502 @property 1503 def all_divisions(self) -> List: 1504 """ 1505 Return a list of all the divisions (both main and offset) in the lace. 1506 1507 Returns: 1508 list: List of all divisions. 1509 """ 1510 res = [] 1511 for parallel_polyline in self.parallel_poly_list: 1512 for polyline in parallel_polyline.polyline_list: 1513 res.extend(polyline.divisions) 1514 return res 1515 1516 def iter_main_intersections(self) -> Iterator: 1517 """Iterate over the main intersections. 1518 1519 Yields: 1520 Intersection: Intersection object. 1521 """ 1522 for ppoly in self.parallel_poly_list: 1523 for division in ppoly.polyline.divisions: 1524 yield from division.intersections 1525 1526 def iter_offset_intersections(self) -> Iterator: 1527 """ 1528 Iterate over the offset intersections. 1529 1530 Yields: 1531 Intersection: Intersection object. 1532 """ 1533 for ppoly in self.parallel_poly_list: 1534 for poly in ppoly.offset_poly_list: 1535 for division in poly.divisions: 1536 yield from division.intersections 1537 1538 def iter_offset_sections(self) -> Iterator: 1539 """ 1540 Iterate over the offset sections. 1541 1542 Yields: 1543 Section: Section object. 1544 """ 1545 for ppoly in self.parallel_poly_list: 1546 for poly in ppoly.offset_poly_list: 1547 for division in poly.divisions: 1548 yield from division.sections 1549 1550 def iter_main_sections(self) -> Iterator: 1551 """Iterate over the main sections. 1552 1553 Yields: 1554 Section: Section object. 1555 """ 1556 for ppoly in self.parallel_poly_list: 1557 for division in ppoly.polyline.divisions: 1558 yield from division.sections 1559 1560 def iter_offset_divisions(self) -> Iterator: 1561 """ 1562 Iterate over the offset divisions. 1563 1564 Yields: 1565 Division: Division object. 1566 """ 1567 for ppoly in self.parallel_poly_list: 1568 for poly in ppoly.offset_poly_list: 1569 yield from poly.divisions 1570 1571 def iter_main_divisions(self) -> Iterator: 1572 """ 1573 Iterate over the main divisions. 1574 1575 Yields: 1576 Division: Division object. 1577 """ 1578 for ppoly in self.parallel_poly_list: 1579 yield from ppoly.polyline.divisions 1580 1581 @property 1582 def main_divisions(self) -> List[Division]: 1583 """Main divisions are the divisions of the main polyline. 1584 1585 Returns: 1586 list: List of main divisions. 1587 """ 1588 res = [] 1589 for parallel_polyline in self.parallel_poly_list: 1590 res.extend(parallel_polyline.polyline.divisions) 1591 return res 1592 1593 @property 1594 def offset_divisions(self) -> List[Division]: 1595 """Offset divisions are the divisions of the offset polylines. 1596 1597 Returns: 1598 list: List of offset divisions. 1599 """ 1600 res = [] 1601 for parallel_polyline in self.parallel_poly_list: 1602 for polyline in parallel_polyline.offset_poly_list: 1603 res.extend(polyline.divisions) 1604 return res 1605 1606 @property 1607 def intersections(self) -> List[Intersection]: 1608 """Return all the intersections in the parallel_poly_list. 1609 1610 Returns: 1611 list: List of intersections. 1612 """ 1613 res = [] 1614 for parallel_polyline in self.parallel_poly_list: 1615 for polyline in parallel_polyline.polyline_list: 1616 res.extend(polyline.intersections) 1617 return res 1618 1619 # @timing 1620 def _set_polyline_list(self): 1621 """ 1622 Populate the self.polyline_list list with Polyline objects. 1623 1624 * Internal use only. 1625 1626 Arguments: 1627 ---------- 1628 None 1629 1630 Return: 1631 -------- 1632 None 1633 1634 Prerequisites: 1635 -------------- 1636 1637 * self.polygon_shapes and/or self.polyline_shapes must be 1638 established. 1639 """ 1640 self.polyline_list = [] 1641 if self.polygon_shapes: 1642 for polygon in self.polygon_shapes: 1643 self.polyline_list.append(Polyline(polygon.vertices, closed=True)) 1644 if self.polyline_shapes: 1645 for polyline in self.polyline_shapes: 1646 self.polyline_list.append(Polyline(polyline.vertices, closed=False)) 1647 1648 # @timing 1649 def _set_parallel_poly_list(self): 1650 """ 1651 Populate the self.parallel_poly_list list with ParallelPolyline 1652 objects. 1653 1654 Arguments: 1655 ---------- 1656 None 1657 1658 Return: 1659 -------- 1660 None 1661 1662 Prerequisites: 1663 -------------- 1664 * self.polygon_shapes and/or self.polyline_shapes must be 1665 established prior to this. 1666 * Parallel polylines are created by offsetting the original 1667 polygon and polyline shapes in two directions using the 1668 self.offset value. 1669 1670 Notes: 1671 ------ 1672 This method is called by the Lace constructor. It is not 1673 for users to call directly. Without this method, the Lace 1674 object cannot be created. 1675 """ 1676 self.parallel_poly_list = [] 1677 if self.polyline_list: 1678 for _, polyline in enumerate(self.polyline_list): 1679 self.parallel_poly_list.append( 1680 ParallelPolyline( 1681 polyline, 1682 self.offset, 1683 lace=self, 1684 closed=polyline.closed, 1685 dist_tol=defaults["dist_tol"] 1686 ) 1687 ) 1688 1689 # @timing 1690 def _set_overlaps(self): 1691 """ 1692 Populate the self.overlaps list with Overlap objects. Side 1693 effects listed below. 1694 1695 Arguments: 1696 ---------- 1697 None 1698 1699 Return: 1700 -------- 1701 None 1702 1703 Side Effects: 1704 ------------- 1705 * self.overlaps is populated with Overlap objects. 1706 * Section objects' overlap attribute is populated with the 1707 corresponding Overlap object that they are a part of. Not 1708 all sections will have an overlap. 1709 1710 Prerequisites: 1711 -------------- 1712 self.polyline and self.parallel_poly_list must be populated. 1713 self.main_intersections, self.offset_sections and 1714 self.d_intersections must be populated prior to creating 1715 the overlaps. 1716 1717 Notes: 1718 ------ 1719 This method is called by the Lace constructor. It is not 1720 for users to call directly. 1721 Without this method, the Lace object cannot be created. 1722 """ 1723 G = nx.Graph() 1724 for section in self.iter_offset_sections(): 1725 if section.is_overlap: 1726 G.add_edge(section.start.id, section.end.id, section=section) 1727 cycles = nx.cycle_basis(G) 1728 for cycle in cycles: 1729 cycle.append(cycle[0]) 1730 edges = connected_pairs(cycle) 1731 sections = [G.edges[edge]["section"] for edge in edges] 1732 s_intersections = set() 1733 for section in sections: 1734 s_intersections.add(section.start.id) 1735 s_intersections.add(section.end.id) 1736 intersections = [self.d_intersections[i] for i in s_intersections] 1737 overlap = Overlap(intersections=intersections, sections=sections) 1738 for section in sections: 1739 section.overlap = overlap 1740 for edge in edges: 1741 section = G.edges[edge]["section"] 1742 section.start.overlap = overlap 1743 section.end.overlap = overlap 1744 self.overlaps.append(overlap) 1745 for overlap in self.overlaps: 1746 for section in overlap.sections: 1747 if section.is_over: 1748 line_width = 3 1749 else: 1750 line_width = 1 1751 line = Shape( 1752 [section.start.point, section.end.point], line_width=line_width 1753 ) 1754 for division in self.offset_divisions: 1755 p1 = division.start.point 1756 p2 = division.end.point 1757 1758 def set_plaits(self): 1759 """ 1760 Populate the self.plaits list with Plait objects. Plaits are 1761 optional for drawing. They form the under/over interlacing. They 1762 are created if the "with_plaits" argument is set to be True in 1763 the constructor. with_plaits is True by default but this can be 1764 changed by setting the auto_plaits value to False in the 1765 settings.py This method can be called by the user to create the 1766 plaits after the creation of the Lace object if they were not 1767 created initally. 1768 1769 * Can be called by users. 1770 1771 Arguments: 1772 ---------- 1773 None 1774 1775 Return: 1776 -------- 1777 None 1778 1779 Side Effects: 1780 ------------- 1781 * self.plaits is populated with Plait objects. 1782 1783 Prerequisites: 1784 -------------- 1785 self.polyline and self.parallel_poly_list must be populated. 1786 self.divisions and self.intersections must be populated. 1787 self.overlaps must be populated. 1788 1789 Where used: 1790 ----------- 1791 Lace.__init__ 1792 1793 Notes: 1794 ------ 1795 This method is called by the Lace constructor. It is not 1796 for users to call directly. Without this method, the Lace 1797 object cannot be created. 1798 """ 1799 if self.plaits: 1800 return 1801 plait_sections = [] 1802 for division in self.iter_offset_divisions(): 1803 merged_sections = division._merged_sections() 1804 for merged in merged_sections: 1805 plait_sections.append((merged[0], merged[-1])) 1806 1807 # connect the open ends of the polyline_shapes 1808 for ppoly in self.parallel_poly_list: 1809 if not ppoly.closed: 1810 polyline1 = ppoly.offset_poly_list[0] 1811 polyline2 = ppoly.offset_poly_list[1] 1812 p1_start_x = polyline1.intersections[0] 1813 p1_end_x = polyline1.intersections[-1] 1814 p2_start_x = polyline2.intersections[0] 1815 p2_end_x = polyline2.intersections[-1] 1816 1817 plait_sections.append((p1_start_x, p2_start_x)) 1818 plait_sections.append((p1_end_x, p2_end_x)) 1819 for sec in self.iter_offset_sections(): 1820 if not sec.is_over and sec.is_overlap: 1821 plait_sections.append((sec.start, sec.end)) 1822 1823 graph_edges = [(r[0].id, r[1].id) for r in plait_sections] 1824 cycles = get_cycles(graph_edges) 1825 plaits = [] 1826 count = 0 1827 for cycle in cycles: 1828 cycle = connected_pairs(cycle) 1829 dup = cycle[1:] 1830 plait = [cycle[0][0], cycle[0][1]] 1831 for _ in range(len(cycle) - 1): 1832 last = plait[-1] 1833 for edge in dup: 1834 if edge[0] == last: 1835 plait.append(edge[1]) 1836 dup.remove(edge) 1837 break 1838 if edge[1] == last: 1839 plait.append(edge[0]) 1840 dup.remove(edge) 1841 break 1842 plaits.append(plait) 1843 count += 1 1844 d_x = self.d_intersections 1845 for plait in plaits: 1846 intersections = [d_x[x] for x in plait] 1847 vertices = [x.point for x in intersections] 1848 if not right_handed(vertices): 1849 vertices.reverse() 1850 plait.reverse() 1851 shape = Shape(vertices) 1852 shape.intersections = intersections 1853 shape.fill_color = colors.gold 1854 shape.inner_lines = None 1855 shape.subtype = Types.PLAIT 1856 self.plaits.append(shape) 1857 1858 # @timing 1859 def _set_intersections(self): 1860 """ 1861 Compute all intersection points (by calling all_intersections) 1862 among the divisions of the polylines (both main and offset). 1863 Populate the self.main_intersections and 1864 self.offset_intersections lists with Intersection objects. This 1865 method is called by the Lace constructor and customized to be 1866 used with Lace objects only. Without this method, the Lace 1867 object cannot be created. 1868 1869 * Internal use only! 1870 1871 Arguments: 1872 ---------- 1873 None 1874 1875 Return: 1876 -------- 1877 None 1878 1879 Side Effects: 1880 ------------- 1881 * self.main_intersections are populated. 1882 * self.offset_intersections are populated. 1883 * "sections" attribute of the divisions are populated. 1884 * "is_overlap" attribute of the sections are populated. 1885 * "intersections" attribute of the divisions are populated. 1886 * "endpoint" attribute of the intersections are populated. 1887 1888 Where used: 1889 ----------- 1890 Lace.__init__ 1891 1892 Prerequisites: 1893 -------------- 1894 * self.main_divisions must be populated. 1895 * self.offset_divisions must be populated. 1896 * Two endpoint intersections of the divisions must be set. 1897 1898 Notes: 1899 ------ 1900 This method is called by the Lace constructor. It is not 1901 for users to call directly. Without this method, the Lace 1902 object cannot be created. 1903 Works only for regular under/over interlacing. 1904 """ 1905 # set intersections for the main polylines 1906 main_divisions = self.main_divisions 1907 offset_divisions = self.offset_divisions 1908 self.main_intersections = all_intersections( 1909 main_divisions, self.d_intersections, self.d_connections 1910 ) 1911 1912 # set sections for the main divisions 1913 self.main_sections = [] 1914 for division in main_divisions: 1915 division.intersections[0].endpoint = True 1916 division.intersections[-1].endpoint = True 1917 segments = connected_pairs(division.intersections) 1918 division.sections = [] 1919 for i, segment in enumerate(segments): 1920 section = Section(*segment) 1921 if i % 2 == 1: 1922 section.is_overlap = True 1923 division.sections.append(section) 1924 self.main_sections.append(section) 1925 # set intersections for the offset polylines 1926 self.offset_intersections = all_intersections( 1927 offset_divisions, self.d_intersections, self.d_connections 1928 ) 1929 # set sections for the offset divisions 1930 self.offset_sections = [] 1931 for i, division in enumerate(offset_divisions): 1932 division.intersections[0].endpoint = True 1933 division.intersections[-1].endpoint = True 1934 lines = connected_pairs(division.intersections) 1935 division.sections = [] 1936 for j, line in enumerate(lines): 1937 section = Section(*line) 1938 if j % 2 == 1: 1939 section.is_overlap = True 1940 division.sections.append(section) 1941 self.offset_sections.append(section) 1942 1943 def _all_polygons(self, polylines, rtol=None): 1944 """Return a list of polygons from a list of lists of points. 1945 polylines: [[(x1, y1), (x2, y2)], [(x3, y3), (x4, y4)], ...] 1946 return [[(x1, y1), (x2, y2), (x3, y3), ...], ...] 1947 1948 Args: 1949 polylines (list): List of lists of points. 1950 rtol (float, optional): Relative tolerance. Defaults to None. 1951 1952 Returns: 1953 list: List of polygons. 1954 """ 1955 if rtol is None: 1956 rtol = self.rtol 1957 return get_polygons(polylines, rtol) 1958 1959 # @timing 1960 def _set_over_under(self): 1961 def next_poly(exclude): 1962 for ppoly in self.parallel_poly_list: 1963 poly1, poly2 = ppoly.offset_poly_list 1964 if poly1 in exclude: 1965 continue 1966 ind = 0 1967 for sec in poly1.iter_sections(): 1968 if sec.is_overlap: 1969 if sec.overlap.drawable and sec.overlap.visited: 1970 even_odd = ind % 2 == 1 1971 return (poly1, poly2, even_odd) 1972 ind += 1 1973 return (None, None, None) 1974 1975 for ppoly in self.parallel_poly_list: 1976 poly1, poly2 = ppoly.offset_poly_list 1977 exclude = [] 1978 even_odd = 0 1979 while poly1: 1980 ind = 0 1981 for i, division in enumerate(poly1.divisions): 1982 for j, section in enumerate(division.sections): 1983 if section.is_overlap: 1984 if section.overlap is None: 1985 msg = ( 1986 "Overlap section in the lace has no " 1987 "overlap object.\n" 1988 "Try different offset value and/or " 1989 "tolerance." 1990 ) 1991 raise RuntimeError(msg) 1992 section.overlap.visited = True 1993 if ind % 2 == even_odd: 1994 if section.overlap.drawable: 1995 section.overlap.drawable = False 1996 section.is_over = True 1997 section2 = poly2.divisions[i].sections[j] 1998 section2.is_over = True 1999 ind += 1 2000 exclude.extend([poly1, poly2]) 2001 poly1, poly2, even_odd = next_poly(exclude) 2002 2003 def fragment_edge_graph(self) -> nx.Graph: 2004 """ 2005 Return a networkx graph of the connected fragments. 2006 If two fragments have a "common" division then they are connected. 2007 2008 Returns: 2009 nx.Graph: Graph of connected fragments. 2010 """ 2011 G = nx.Graph() 2012 fragments = [(f.area, f) for f in self.fragments] 2013 fragments.sort(key=lambda x: x[0], reverse=True) 2014 for fragment in [f[1] for f in fragments[1:]]: 2015 for division in fragment.divisions: 2016 if division.twin and division.twin.fragment: 2017 fragment2 = division.twin.fragment 2018 G.add_node(fragment.id, fragment=fragment) 2019 G.add_node(fragment2.id, fragment=fragment2) 2020 G.add_edge(fragment.id, fragment2.id, edge=division) 2021 return G 2022 2023 def fragment_vertex_graph(self) -> nx.Graph: 2024 """ 2025 Return a networkx graph of the connected fragments. 2026 If two fragments have a "common" vertex then they are connected. 2027 2028 Returns: 2029 nx.Graph: Graph of connected fragments. 2030 """ 2031 def get_neighbours(intersection): 2032 division = intersection.division 2033 if not division.next.twin: 2034 return None 2035 fragments = [division.fragment] 2036 start_division_id = division.id 2037 if division.next.twin: 2038 twin_id = division.next.twin.id 2039 else: 2040 return None 2041 while twin_id != start_division_id: 2042 if division.next.twin: 2043 if division.fragment.id not in [x.id for x in fragments]: 2044 fragments.append(division.fragment) 2045 twin_id = division.next.twin.id 2046 division = division.next.twin 2047 else: 2048 if division.next.fragment.id not in [x.id for x in fragments]: 2049 fragments.append(division.next.fragment) 2050 break 2051 2052 return fragments 2053 2054 neighbours = [] 2055 for fragment in self.fragments: 2056 for intersection in fragment.intersections: 2057 neighbours.append(get_neighbours(intersection)) 2058 G = self.fragment_edge_graph() 2059 G2 = nx.Graph() 2060 for n in neighbours: 2061 if n: 2062 if len(n) > 2: 2063 for pair in combinations(n, 2): 2064 ids = tuple([x.id for x in pair]) 2065 if ids not in G.edges: 2066 G2.add_node(ids[0], fragment=pair[0]) 2067 G2.add_node(ids[1], fragment=pair[1]) 2068 G2.add_edge(*ids) 2069 return G2
A Lace is a collection of ParallelPolylines objects. They are used to create interlace patterns.
Arguments:
- polygon_shapes (Union[Batch, list[Shape]], optional): List of polygon shapes. Defaults to None.
- polyline_shapes (Union[Batch, list[Shape]], optional): List of polyline shapes. Defaults to None.
- offset (float, optional): Offset value. Defaults to 2.
- rtol (float, optional): Relative tolerance. Defaults to None.
- swatch (list, optional): Swatch list. Defaults to None.
- breakpoints (list, optional): Breakpoints list. Defaults to None.
- plait_color (colors.Color, optional): Plait color. Defaults to None.
- draw_fragments (bool, optional): If fragments should be drawn. Defaults to True.
- palette (list, optional): Palette list. Defaults to None.
- color_step (int, optional): Color step. Defaults to 1.
- with_plaits (bool, optional): If plaits should be included. Defaults to True.
- area_threshold (float, optional): Area threshold. Defaults to None.
- radius_threshold (float, optional): Radius threshold. Defaults to None.
- **kwargs: Additional attributes for cosmetic/drawing purposes.
966 def __init__( 967 self, 968 polygon_shapes: Union[Batch, list[Shape]] = None, 969 polyline_shapes: Union[Batch, list[Shape]] = None, 970 offset: float = 2, 971 rtol: float = None, 972 swatch: list = None, 973 breakpoints: list = None, 974 plait_color: colors.Color = None, 975 draw_fragments: bool = True, 976 palette: list = None, 977 color_step: int = 1, 978 with_plaits: bool = True, 979 area_threshold: float = None, 980 radius_threshold: float = None, 981 **kwargs, 982 ) -> None: 983 validate_args(kwargs, shape_style_map) 984 ( 985 rtol, 986 swatch, 987 plait_color, 988 draw_fragments, 989 area_threshold, 990 radius_threshold, 991 ) = get_defaults( 992 [ 993 "rtol", 994 "swatch", 995 "plait_color", 996 "draw_fragments", 997 "area_threshold", 998 "radius_threshold", 999 ], 1000 [ 1001 rtol, 1002 swatch, 1003 plait_color, 1004 draw_fragments, 1005 area_threshold, 1006 radius_threshold, 1007 ], 1008 ) 1009 if polygon_shapes: 1010 polygon_shapes = polygon_shapes.merge_shapes() 1011 self.polygon_shapes = self._check_polygons(polygon_shapes) 1012 else: 1013 self.polygon_shapes = [] 1014 if polyline_shapes: 1015 polyline_shapes = polyline_shapes.merge_shapes() 1016 self.polyline_shapes = self._check_polylines(polyline_shapes) 1017 else: 1018 self.polyline_shapes = [] 1019 if not self.polygon_shapes and not self.polyline_shapes: 1020 msg = "Lace.__init__ : No polygons or polylines found." 1021 raise ValueError(msg) 1022 self.polyline_shapes = polyline_shapes 1023 self.offset = offset 1024 self.main_intersections = None 1025 self.offset_intersections = None 1026 self.xform_matrix = np.eye(3) 1027 self.rtol = rtol 1028 self.swatch = swatch 1029 self.breakpoints = breakpoints 1030 self.plait_color = plait_color 1031 self.draw_fragments = draw_fragments 1032 self.palette = palette 1033 self.color_step = color_step 1034 self.with_plaits = with_plaits 1035 self.d_intersections = {} # key, value:intersection.id, intersection 1036 self.d_connections = {} 1037 self.plaits = [] 1038 self.overlaps = [] 1039 self._groups = None 1040 self.area_threshold = area_threshold 1041 self.radius_threshold = radius_threshold 1042 if kwargs and "_copy" in kwargs: 1043 # pass the pre-computed values 1044 for k, v in kwargs: 1045 if k == "_copy": 1046 continue 1047 else: 1048 setattr(self, k, v) 1049 else: 1050 self._set_polyline_list() # main divisions are set here along with polylines 1051 # polyline.divisions is the list of Division objects 1052 self._set_parallel_poly_list() 1053 # start_time2 = time.perf_counter() 1054 self._set_intersections() 1055 # end_time2 = time.perf_counter() 1056 # print( 1057 # f"Lace.__init__ intersections computed in {end_time2 - start_time2:0.4f} seconds" 1058 # ) 1059 1060 self._set_overlaps() 1061 self._set_twin_sections() 1062 self._set_fragments() 1063 if not self.polyline_shapes: 1064 self._set_outline() 1065 # self._set_partitions() 1066 self._set_over_under() 1067 if self.with_plaits: 1068 self.set_plaits() 1069 # self._set_convex_hull() 1070 # self._set_concave_hull() 1071 # self._set_fragment_groups() 1072 # self._set_partition_groups() 1073 self._b_box = None 1074 elements = [polyline for polyline in self.parallel_poly_list] + self.fragments 1075 1076 if "debug" in kwargs: 1077 kwargs.pop("debug") 1078 super().__init__(elements, **kwargs) 1079 if kwargs and "_copy" not in kwargs: 1080 for k, v in kwargs.items(): 1081 if k in shape_style_map: 1082 setattr(self, k, v) # todo: we should check for valid values here 1083 else: 1084 raise AttributeError(f"{k}. Invalid attribute!") 1085 self.subtype = Types.LACE 1086 common_properties(self)
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.
1088 @property 1089 def center(self): 1090 """Return the center of the lace. 1091 1092 Returns: 1093 list: Center coordinates. 1094 """ 1095 return self.outline.CG
Return the center of the lace.
Returns:
list: Center coordinates.
1097 @property 1098 def fragment_groups(self): 1099 """Return the fragment groups of the lace. 1100 1101 Returns: 1102 dict: Dictionary of fragment groups. 1103 """ 1104 center = self.center 1105 radius_frag = [] 1106 for fragment in self.fragments: 1107 radius = int(distance(center, fragment.CG)) 1108 for rad, frag in radius_frag: 1109 if abs(radius - rad) <= 2: 1110 radius = rad 1111 break 1112 radius_frag.append((radius, fragment)) 1113 1114 radius_frag.sort(key=lambda x: x[0]) 1115 1116 d_groups = {} 1117 for i, (radius, fragment) in enumerate(radius_frag): 1118 if i == 0: 1119 d_groups[radius] = [fragment] 1120 else: 1121 if radius in d_groups: 1122 d_groups[radius].append(fragment) 1123 else: 1124 d_groups[radius] = [fragment] 1125 1126 return d_groups
Return the fragment groups of the lace.
Returns:
dict: Dictionary of fragment groups.
1235 def set_fragment_groups(self): 1236 # to do : handle repeated code. same in _set_partition_groups 1237 areas = [] 1238 for i, fragment in enumerate(self.fragments): 1239 areas.append((fragment.area, i)) 1240 areas.sort() 1241 bins = group_into_bins(areas, self.area_threshold) 1242 self.fragments_by_area = OrderedDict() 1243 for i, bin in enumerate(bins): 1244 area_values = [x[0] for x in bin] 1245 key = sum([x[0] for x in bin]) / len(bin) 1246 fragments = [] 1247 for area, ind in areas: 1248 if area in area_values: 1249 fragments.append(self.fragments[ind]) 1250 self.fragments_by_area[key] = fragments 1251 1252 radii = [] 1253 for i, fragment in enumerate(self.fragments): 1254 radii.append((distance(self.center, fragment.CG), i)) 1255 radii.sort() 1256 bins = group_into_bins(radii, self.radius_threshold) 1257 self.fragments_by_radius = OrderedDict() 1258 for i, bin in enumerate(bins): 1259 radius_values = [x[0] for x in bin] 1260 key = sum([x[0] for x in bin]) / len(bin) 1261 fragments = [] 1262 for radius, ind in radii: 1263 if radius in radius_values: 1264 fragments.append(self.fragments[ind]) 1265 self.fragments_by_radius[key] = fragments
1347 def copy(self): 1348 class Dummy(Lace): 1349 pass 1350 1351 # we need to copy the polyline_list and parallel_poly_list 1352 for polyline in self.polyline_list: 1353 polyline.copy()
Returns a copy of the batch.
Returns:
Batch: A copy of the batch.
1355 def get_sketch(self): 1356 """ 1357 Create and return a Sketch object. Sketch is a Batch object 1358 with Shape elements corresponding to the vertices of the plaits 1359 and fragments of the Lace instance. They have 'plaits' and 1360 'fragments' attributes to hold lists of Shape objects populated 1361 with plait and fragment vertices of the Lace instance 1362 respectively. They are used for drawing multiple copies of the 1363 original lace pattern. They are light-weight compared to the 1364 Lace objects since they only contain sufficient data to draw the 1365 lace objects. Hundreds of these objects can be used to create 1366 wallpaper patterns or other patterns without having to contain 1367 unnecessary data. They do not share points with the original 1368 Lace object. 1369 1370 Arguments: 1371 ---------- 1372 None 1373 1374 Prerequisites: 1375 -------------- 1376 * A lace object to be copied. 1377 1378 Side effects: 1379 ------------- 1380 None 1381 1382 Return: 1383 -------- 1384 A Sketch object. 1385 """ 1386 fragments = [] 1387 for fragment in self.fragments: 1388 polygon = Shape(fragment.vertices) 1389 polygon.fill = True 1390 polygon.subtype = Types.FRAGMENT 1391 fragments.append(polygon) 1392 1393 plaits = [] 1394 for plait in self.plaits: 1395 polygon = Shape(plait.vertices) 1396 polygon.fill = True 1397 polygon.subtype = Types.PLAIT 1398 plaits.append(polygon) 1399 1400 sketch = Batch((fragments + plaits)) 1401 sketch.fragments = fragments 1402 sketch.plaits = plaits 1403 sketch.outline = self.outline 1404 sketch.subtype = Types.SKETCH 1405 1406 sketch.draw_plaits = True 1407 sketch.draw_fragments = True 1408 return sketch
Create and return a Sketch object. Sketch is a Batch object with Shape elements corresponding to the vertices of the plaits and fragments of the Lace instance. They have 'plaits' and 'fragments' attributes to hold lists of Shape objects populated with plait and fragment vertices of the Lace instance respectively. They are used for drawing multiple copies of the original lace pattern. They are light-weight compared to the Lace objects since they only contain sufficient data to draw the lace objects. Hundreds of these objects can be used to create wallpaper patterns or other patterns without having to contain unnecessary data. They do not share points with the original Lace object.
Arguments:
None
Prerequisites:
* A lace object to be copied.
Side effects:
None
Return:
A Sketch object.
1410 def group_fragments(self, tol=None): 1411 """Group the fragments by the number of vertices and the area. 1412 1413 Args: 1414 tol (float, optional): Tolerance value. Defaults to None. 1415 1416 Returns: 1417 list: List of grouped fragments. 1418 """ 1419 if tol is None: 1420 tol = defaults["tol"] 1421 frags = self.fragments 1422 vert_groups = [ 1423 [frag for frag in frags if len(frag.vertices) == n] 1424 for n in set([len(f.vertices) for f in frags]) 1425 ] 1426 groups = [] 1427 for group in vert_groups: 1428 areas = [ 1429 [f for f in group if isclose(f.area, area, rtol=tol)] 1430 for area in set([frag.area for frag in group]) 1431 ] 1432 areas.sort(key=lambda x: x[0].area, reverse=True) 1433 groups.append(areas) 1434 groups.sort(key=lambda x: x[0][0].area, reverse=True) 1435 1436 return groups
Group the fragments by the number of vertices and the area.
Arguments:
- tol (float, optional): Tolerance value. Defaults to None.
Returns:
list: List of grouped fragments.
1438 def get_fragment_cycles(self): 1439 """ 1440 Iterate over the offset sections and create a graph of the 1441 intersections (start and end of the sections). Then find the 1442 cycles in the graph. self.d_intersections is used to map 1443 the graph nodes to the actual intersection points. 1444 1445 Returns: 1446 list: List of fragment cycles. 1447 """ 1448 graph_edges = [] 1449 for section in self.iter_offset_sections(): 1450 if section.is_overlap: 1451 continue 1452 graph_edges.append((section.start.id, section.end.id)) 1453 1454 return get_cycles(graph_edges)
Iterate over the offset sections and create a graph of the intersections (start and end of the sections). Then find the cycles in the graph. self.d_intersections is used to map the graph nodes to the actual intersection points.
Returns:
list: List of fragment cycles.
1469 def set_plait_lines(self, n, offset, line_color=colors.blue, line_width=1): 1470 """Create offset lines inside the plaits of the lace. 1471 1472 Args: 1473 n (int): Number of lines. 1474 offset (float): Offset value. 1475 line_color (colors.Color, optional): Line color. Defaults to colors.blue. 1476 line_width (int, optional): Line width. Defaults to 1. 1477 """ 1478 for plait in self.plaits: 1479 plait.inner_lines = [] 1480 self._set_inner_lines(plait, n, offset, line_color, line_width)
Create offset lines inside the plaits of the lace.
Arguments:
- n (int): Number of lines.
- offset (float): Offset value.
- line_color (colors.Color, optional): Line color. Defaults to colors.blue.
- line_width (int, optional): Line width. Defaults to 1.
1482 def set_fragment_lines( 1483 self, 1484 n: int, 1485 offset: float, 1486 line_color: colors.Color = colors.blue, 1487 line_width=1, 1488 ) -> None: 1489 """ 1490 Create offset lines inside the fragments of the lace. 1491 1492 Args: 1493 n (int): Number of lines. 1494 offset (float): Offset value. 1495 line_color (colors.Color, optional): Line color. Defaults to colors.blue. 1496 line_width (int, optional): Line width. Defaults to 1. 1497 """ 1498 for fragment in self.fragments: 1499 fragment.inner_lines = [] 1500 self._set_inner_lines(fragment, n, offset, line_color, line_width)
Create offset lines inside the fragments of the lace.
Arguments:
- n (int): Number of lines.
- offset (float): Offset value.
- line_color (colors.Color, optional): Line color. Defaults to colors.blue.
- line_width (int, optional): Line width. Defaults to 1.
1502 @property 1503 def all_divisions(self) -> List: 1504 """ 1505 Return a list of all the divisions (both main and offset) in the lace. 1506 1507 Returns: 1508 list: List of all divisions. 1509 """ 1510 res = [] 1511 for parallel_polyline in self.parallel_poly_list: 1512 for polyline in parallel_polyline.polyline_list: 1513 res.extend(polyline.divisions) 1514 return res
Return a list of all the divisions (both main and offset) in the lace.
Returns:
list: List of all divisions.
1516 def iter_main_intersections(self) -> Iterator: 1517 """Iterate over the main intersections. 1518 1519 Yields: 1520 Intersection: Intersection object. 1521 """ 1522 for ppoly in self.parallel_poly_list: 1523 for division in ppoly.polyline.divisions: 1524 yield from division.intersections
Iterate over the main intersections.
Yields:
Intersection: Intersection object.
1526 def iter_offset_intersections(self) -> Iterator: 1527 """ 1528 Iterate over the offset intersections. 1529 1530 Yields: 1531 Intersection: Intersection object. 1532 """ 1533 for ppoly in self.parallel_poly_list: 1534 for poly in ppoly.offset_poly_list: 1535 for division in poly.divisions: 1536 yield from division.intersections
Iterate over the offset intersections.
Yields:
Intersection: Intersection object.
1538 def iter_offset_sections(self) -> Iterator: 1539 """ 1540 Iterate over the offset sections. 1541 1542 Yields: 1543 Section: Section object. 1544 """ 1545 for ppoly in self.parallel_poly_list: 1546 for poly in ppoly.offset_poly_list: 1547 for division in poly.divisions: 1548 yield from division.sections
Iterate over the offset sections.
Yields:
Section: Section object.
1550 def iter_main_sections(self) -> Iterator: 1551 """Iterate over the main sections. 1552 1553 Yields: 1554 Section: Section object. 1555 """ 1556 for ppoly in self.parallel_poly_list: 1557 for division in ppoly.polyline.divisions: 1558 yield from division.sections
Iterate over the main sections.
Yields:
Section: Section object.
1560 def iter_offset_divisions(self) -> Iterator: 1561 """ 1562 Iterate over the offset divisions. 1563 1564 Yields: 1565 Division: Division object. 1566 """ 1567 for ppoly in self.parallel_poly_list: 1568 for poly in ppoly.offset_poly_list: 1569 yield from poly.divisions
Iterate over the offset divisions.
Yields:
Division: Division object.
1571 def iter_main_divisions(self) -> Iterator: 1572 """ 1573 Iterate over the main divisions. 1574 1575 Yields: 1576 Division: Division object. 1577 """ 1578 for ppoly in self.parallel_poly_list: 1579 yield from ppoly.polyline.divisions
Iterate over the main divisions.
Yields:
Division: Division object.
1581 @property 1582 def main_divisions(self) -> List[Division]: 1583 """Main divisions are the divisions of the main polyline. 1584 1585 Returns: 1586 list: List of main divisions. 1587 """ 1588 res = [] 1589 for parallel_polyline in self.parallel_poly_list: 1590 res.extend(parallel_polyline.polyline.divisions) 1591 return res
Main divisions are the divisions of the main polyline.
Returns:
list: List of main divisions.
1593 @property 1594 def offset_divisions(self) -> List[Division]: 1595 """Offset divisions are the divisions of the offset polylines. 1596 1597 Returns: 1598 list: List of offset divisions. 1599 """ 1600 res = [] 1601 for parallel_polyline in self.parallel_poly_list: 1602 for polyline in parallel_polyline.offset_poly_list: 1603 res.extend(polyline.divisions) 1604 return res
Offset divisions are the divisions of the offset polylines.
Returns:
list: List of offset divisions.
1606 @property 1607 def intersections(self) -> List[Intersection]: 1608 """Return all the intersections in the parallel_poly_list. 1609 1610 Returns: 1611 list: List of intersections. 1612 """ 1613 res = [] 1614 for parallel_polyline in self.parallel_poly_list: 1615 for polyline in parallel_polyline.polyline_list: 1616 res.extend(polyline.intersections) 1617 return res
Return all the intersections in the parallel_poly_list.
Returns:
list: List of intersections.
1758 def set_plaits(self): 1759 """ 1760 Populate the self.plaits list with Plait objects. Plaits are 1761 optional for drawing. They form the under/over interlacing. They 1762 are created if the "with_plaits" argument is set to be True in 1763 the constructor. with_plaits is True by default but this can be 1764 changed by setting the auto_plaits value to False in the 1765 settings.py This method can be called by the user to create the 1766 plaits after the creation of the Lace object if they were not 1767 created initally. 1768 1769 * Can be called by users. 1770 1771 Arguments: 1772 ---------- 1773 None 1774 1775 Return: 1776 -------- 1777 None 1778 1779 Side Effects: 1780 ------------- 1781 * self.plaits is populated with Plait objects. 1782 1783 Prerequisites: 1784 -------------- 1785 self.polyline and self.parallel_poly_list must be populated. 1786 self.divisions and self.intersections must be populated. 1787 self.overlaps must be populated. 1788 1789 Where used: 1790 ----------- 1791 Lace.__init__ 1792 1793 Notes: 1794 ------ 1795 This method is called by the Lace constructor. It is not 1796 for users to call directly. Without this method, the Lace 1797 object cannot be created. 1798 """ 1799 if self.plaits: 1800 return 1801 plait_sections = [] 1802 for division in self.iter_offset_divisions(): 1803 merged_sections = division._merged_sections() 1804 for merged in merged_sections: 1805 plait_sections.append((merged[0], merged[-1])) 1806 1807 # connect the open ends of the polyline_shapes 1808 for ppoly in self.parallel_poly_list: 1809 if not ppoly.closed: 1810 polyline1 = ppoly.offset_poly_list[0] 1811 polyline2 = ppoly.offset_poly_list[1] 1812 p1_start_x = polyline1.intersections[0] 1813 p1_end_x = polyline1.intersections[-1] 1814 p2_start_x = polyline2.intersections[0] 1815 p2_end_x = polyline2.intersections[-1] 1816 1817 plait_sections.append((p1_start_x, p2_start_x)) 1818 plait_sections.append((p1_end_x, p2_end_x)) 1819 for sec in self.iter_offset_sections(): 1820 if not sec.is_over and sec.is_overlap: 1821 plait_sections.append((sec.start, sec.end)) 1822 1823 graph_edges = [(r[0].id, r[1].id) for r in plait_sections] 1824 cycles = get_cycles(graph_edges) 1825 plaits = [] 1826 count = 0 1827 for cycle in cycles: 1828 cycle = connected_pairs(cycle) 1829 dup = cycle[1:] 1830 plait = [cycle[0][0], cycle[0][1]] 1831 for _ in range(len(cycle) - 1): 1832 last = plait[-1] 1833 for edge in dup: 1834 if edge[0] == last: 1835 plait.append(edge[1]) 1836 dup.remove(edge) 1837 break 1838 if edge[1] == last: 1839 plait.append(edge[0]) 1840 dup.remove(edge) 1841 break 1842 plaits.append(plait) 1843 count += 1 1844 d_x = self.d_intersections 1845 for plait in plaits: 1846 intersections = [d_x[x] for x in plait] 1847 vertices = [x.point for x in intersections] 1848 if not right_handed(vertices): 1849 vertices.reverse() 1850 plait.reverse() 1851 shape = Shape(vertices) 1852 shape.intersections = intersections 1853 shape.fill_color = colors.gold 1854 shape.inner_lines = None 1855 shape.subtype = Types.PLAIT 1856 self.plaits.append(shape)
Populate the self.plaits list with Plait objects. Plaits are optional for drawing. They form the under/over interlacing. They are created if the "with_plaits" argument is set to be True in the constructor. with_plaits is True by default but this can be changed by setting the auto_plaits value to False in the settings.py This method can be called by the user to create the plaits after the creation of the Lace object if they were not created initally.
- Can be called by users.
Arguments:
None
Return:
None
Side Effects:
* self.plaits is populated with Plait objects.
Prerequisites:
self.polyline and self.parallel_poly_list must be populated.
self.divisions and self.intersections must be populated.
self.overlaps must be populated.
Where used:
Lace.__init__
Notes:
This method is called by the Lace constructor. It is not
for users to call directly. Without this method, the Lace
object cannot be created.
2003 def fragment_edge_graph(self) -> nx.Graph: 2004 """ 2005 Return a networkx graph of the connected fragments. 2006 If two fragments have a "common" division then they are connected. 2007 2008 Returns: 2009 nx.Graph: Graph of connected fragments. 2010 """ 2011 G = nx.Graph() 2012 fragments = [(f.area, f) for f in self.fragments] 2013 fragments.sort(key=lambda x: x[0], reverse=True) 2014 for fragment in [f[1] for f in fragments[1:]]: 2015 for division in fragment.divisions: 2016 if division.twin and division.twin.fragment: 2017 fragment2 = division.twin.fragment 2018 G.add_node(fragment.id, fragment=fragment) 2019 G.add_node(fragment2.id, fragment=fragment2) 2020 G.add_edge(fragment.id, fragment2.id, edge=division) 2021 return G
Return a networkx graph of the connected fragments. If two fragments have a "common" division then they are connected.
Returns:
nx.Graph: Graph of connected fragments.
2023 def fragment_vertex_graph(self) -> nx.Graph: 2024 """ 2025 Return a networkx graph of the connected fragments. 2026 If two fragments have a "common" vertex then they are connected. 2027 2028 Returns: 2029 nx.Graph: Graph of connected fragments. 2030 """ 2031 def get_neighbours(intersection): 2032 division = intersection.division 2033 if not division.next.twin: 2034 return None 2035 fragments = [division.fragment] 2036 start_division_id = division.id 2037 if division.next.twin: 2038 twin_id = division.next.twin.id 2039 else: 2040 return None 2041 while twin_id != start_division_id: 2042 if division.next.twin: 2043 if division.fragment.id not in [x.id for x in fragments]: 2044 fragments.append(division.fragment) 2045 twin_id = division.next.twin.id 2046 division = division.next.twin 2047 else: 2048 if division.next.fragment.id not in [x.id for x in fragments]: 2049 fragments.append(division.next.fragment) 2050 break 2051 2052 return fragments 2053 2054 neighbours = [] 2055 for fragment in self.fragments: 2056 for intersection in fragment.intersections: 2057 neighbours.append(get_neighbours(intersection)) 2058 G = self.fragment_edge_graph() 2059 G2 = nx.Graph() 2060 for n in neighbours: 2061 if n: 2062 if len(n) > 2: 2063 for pair in combinations(n, 2): 2064 ids = tuple([x.id for x in pair]) 2065 if ids not in G.edges: 2066 G2.add_node(ids[0], fragment=pair[0]) 2067 G2.add_node(ids[1], fragment=pair[1]) 2068 G2.add_edge(*ids) 2069 return G2
Return a networkx graph of the connected fragments. If two fragments have a "common" vertex then they are connected.
Returns:
nx.Graph: Graph of connected fragments.
Inherited Members
- simetri.graphics.batch.Batch
- type
- modifiers
- blend_mode
- alpha
- line_alpha
- fill_alpha
- text_alpha
- clip
- mask
- even_odd_rule
- blend_group
- transparency_group
- set_attribs
- set_batch_attr
- proximity
- append
- reverse
- insert
- remove
- pop
- clear
- extend
- iter_elements
- all_elements
- all_shapes
- all_vertices
- all_segments
- as_graph
- graph_summary
- merge_shapes
- all_polygons
- b_box
2072def all_intersections( 2073 division_list: list[Division], 2074 d_intersections: dict[int, Intersection], 2075 d_connections: dict[frozenset, Intersection], 2076 loom=False, 2077) -> list[Intersection]: 2078 """ 2079 Find all intersections of the given divisions. Sweep-line algorithm 2080 without a self-balancing tree. Instead of a self-balancing tree, 2081 it uses a numpy array to sort and filter the divisions. For the 2082 number of divisions that are commonly needed in a lace, this is 2083 sufficiently fast. It is also more robust and much easier to 2084 understand and debug. Tested with tens of thousands of divisions but 2085 not millions. The book has a section on this algorithm. 2086 simetri.geometry.py has another version (called 2087 all_intersections) for finding intersections among a given 2088 list of divisions. 2089 2090 Arguments: 2091 ---------- 2092 division_list: list of Division objects. 2093 2094 Side Effects: 2095 ------------- 2096 * Modifies the given division objects (in the division_list) in place 2097 by adding the intersections to the divisions' "intersections" 2098 attribute. 2099 * Updates the d_intersections 2100 * Updates the d_connections 2101 2102 Return: 2103 -------- 2104 A list of all intersection objects among the given division 2105 list. 2106 """ 2107 # register fake intersections at the endpoints of the open lines 2108 for division in division_list: 2109 if division.intersections: 2110 for x in division.intersections: 2111 d_intersections[x.id] = x 2112 2113 # All objects are assigned an integer id attribute when they are created 2114 division_array = array( 2115 [flatten(division.vertices) + [division.id] for division in division_list] 2116 ) 2117 n_divisions = division_array.shape[0] # number of divisions 2118 # precompute the min and max x and y values for each division 2119 # these will be used with the sweep line algorithm 2120 xmin = np.minimum(division_array[:, 0], division_array[:, 2]).reshape( 2121 n_divisions, 1 2122 ) 2123 xmax = np.maximum(division_array[:, 0], division_array[:, 2]).reshape( 2124 n_divisions, 1 2125 ) 2126 ymin = np.minimum(division_array[:, 1], division_array[:, 3]).reshape( 2127 n_divisions, 1 2128 ) 2129 ymax = np.maximum(division_array[:, 1], division_array[:, 3]).reshape( 2130 n_divisions, 1 2131 ) 2132 division_array = np.concatenate((division_array, xmin, ymin, xmax, ymax), 1) 2133 d_divisions = {} 2134 for division in division_list: 2135 d_divisions[division.id] = division 2136 i_id, i_xmin, i_ymin, i_xmax, i_ymax = range(4, 9) # column indices 2137 # sort by xmin values 2138 division_array = division_array[division_array[:, i_xmin].argsort()] 2139 intersections = [] 2140 for i in range(n_divisions): 2141 x1, y1, x2, y2, id1, sl_xmin, sl_ymin, sl_xmax, sl_ymax = division_array[i, :] 2142 division1_vertices = [x1, y1, x2, y2] 2143 start = i + 1 # search should start from the next division 2144 # filter the array by checking if the divisions' bounding-boxes are 2145 # overlapping with the bounding-box of the current division 2146 candidates = division_array[start:, :][ 2147 ( 2148 ( 2149 (division_array[start:, i_xmax] >= sl_xmin) 2150 & (division_array[start:, i_xmin] <= sl_xmax) 2151 ) 2152 & ( 2153 (division_array[start:, i_ymax] >= sl_ymin) 2154 & (division_array[start:, i_ymin] <= sl_ymax) 2155 ) 2156 ) 2157 ] 2158 for candid in candidates: 2159 id2 = candid[i_id] 2160 division2_vertices = candid[:4] 2161 if loom: 2162 x1, y1 = division1_vertices[:2] 2163 x2, y2 = division2_vertices[:2] 2164 if x1 == x2 or y1 == y2: 2165 continue 2166 connection_type, x_point = intersection2( 2167 *division1_vertices, *division2_vertices 2168 ) 2169 if connection_type not in [ 2170 Connection.DISJOINT, 2171 Connection.NONE, 2172 Connection.PARALLEL, 2173 ]: 2174 division1, division2 = d_divisions[int(id1)], d_divisions[int(id2)] 2175 x_point__e1_2 = frozenset((division1.id, division2.id)) 2176 if x_point__e1_2 not in d_connections: 2177 inters_obj = Intersection(x_point, division1, division2) 2178 d_intersections[inters_obj.id] = inters_obj 2179 d_connections[x_point__e1_2] = inters_obj 2180 division1.intersections.append(inters_obj) 2181 division2.intersections.append(inters_obj) 2182 intersections.append(inters_obj) 2183 2184 for division in division_list: 2185 division._sort_intersections() 2186 return intersections
Find all intersections of the given divisions. Sweep-line algorithm without a self-balancing tree. Instead of a self-balancing tree, it uses a numpy array to sort and filter the divisions. For the number of divisions that are commonly needed in a lace, this is sufficiently fast. It is also more robust and much easier to understand and debug. Tested with tens of thousands of divisions but not millions. The book has a section on this algorithm. simetri.geometry.py has another version (called all_intersections) for finding intersections among a given list of divisions.
Arguments:
division_list: list of Division objects.
Side Effects:
* Modifies the given division objects (in the division_list) in place
by adding the intersections to the divisions' "intersections"
attribute.
* Updates the d_intersections
* Updates the d_connections
Return:
A list of all intersection objects among the given division
list.
2189def merge_nodes( 2190 division_list: list[Division], 2191 d_intersections: dict[int, Intersection], 2192 d_connections: dict[frozenset, Intersection], 2193 loom=False, 2194) -> list[Intersection]: 2195 """ 2196 Find all intersections of the given divisions. Sweep-line algorithm 2197 without a self-balancing tree. Instead of a self-balancing tree, 2198 it uses a numpy array to sort and filter the divisions. For the 2199 number of divisions that are commonly needed in a lace, this is 2200 sufficiently fast. It is also more robust and much easier to 2201 understand and debug. Tested with tens of thousands of divisions but 2202 not millions. The book has a section on this algorithm. 2203 simetri.geometry.py has another version (called 2204 all_intersections) for finding intersections among a given 2205 list of divisions. 2206 2207 Arguments: 2208 ---------- 2209 division_list: list of division objects. 2210 2211 Side Effects: 2212 ------------- 2213 * Modifies the given division objects (in the division_list) in place 2214 by adding the intersections to the divisions' "intersections" 2215 attribute. 2216 * Updates the d_intersections 2217 * Updates the d_connections 2218 2219 Return: 2220 -------- 2221 A list of all intersection objects among the given division 2222 list. 2223 """ 2224 # register fake intersections at the endpoints of the open lines 2225 for division in division_list: 2226 if division.intersections: 2227 for x in division.intersections: 2228 d_intersections[x.id] = x 2229 2230 # All objects are assigned an integer id attribute when they are created 2231 division_array = array( 2232 [flatten(division.vertices) + [division.id] for division in division_list] 2233 ) 2234 n_divisions = division_array.shape[0] # number of divisions 2235 # precompute the min and max x and y values for each division 2236 # these will be used with the sweep line algorithm 2237 xmin = np.minimum(division_array[:, 0], division_array[:, 2]).reshape( 2238 n_divisions, 1 2239 ) 2240 xmax = np.maximum(division_array[:, 0], division_array[:, 2]).reshape( 2241 n_divisions, 1 2242 ) 2243 ymin = np.minimum(division_array[:, 1], division_array[:, 3]).reshape( 2244 n_divisions, 1 2245 ) 2246 ymax = np.maximum(division_array[:, 1], division_array[:, 3]).reshape( 2247 n_divisions, 1 2248 ) 2249 division_array = np.concatenate((division_array, xmin, ymin, xmax, ymax), 1) 2250 d_divisions = {} 2251 for division in division_list: 2252 d_divisions[division.id] = division 2253 i_id, i_xmin, i_ymin, i_xmax, i_ymax = range(4, 9) # column indices 2254 # sort by xmin values 2255 division_array = division_array[division_array[:, i_xmin].argsort()] 2256 intersections = [] 2257 for i in range(n_divisions): 2258 x1, y1, x2, y2, id1, sl_xmin, sl_ymin, sl_xmax, sl_ymax = division_array[i, :] 2259 division1_vertices = [x1, y1, x2, y2] 2260 start = i + 1 # search should start from the next division 2261 # filter the array by checking if the divisions' bounding-boxes are 2262 # overlapping with the bounding-box of the current division 2263 candidates = division_array[start:, :][ 2264 ( 2265 ( 2266 (division_array[start:, i_xmax] >= sl_xmin) 2267 & (division_array[start:, i_xmin] <= sl_xmax) 2268 ) 2269 & ( 2270 (division_array[start:, i_ymax] >= sl_ymin) 2271 & (division_array[start:, i_ymin] <= sl_ymax) 2272 ) 2273 ) 2274 ] 2275 for candid in candidates: 2276 id2 = candid[i_id] 2277 division2_vertices = candid[:4] 2278 if loom: 2279 x1, y1 = division1_vertices[:2] 2280 x2, y2 = division2_vertices[:2] 2281 if x1 == x2 or y1 == y2: 2282 continue 2283 connection_type, x_point = intersection2( 2284 *division1_vertices, *division2_vertices 2285 ) 2286 if connection_type not in [ 2287 Connection.DISJOINT, 2288 Connection.NONE, 2289 Connection.PARALLEL, 2290 ]: 2291 division1, division2 = d_divisions[int(id1)], d_divisions[int(id2)] 2292 x_point__e1_2 = frozenset((division1.id, division2.id)) 2293 if x_point__e1_2 not in d_connections: 2294 inters_obj = Intersection(x_point, division1, division2) 2295 d_intersections[inters_obj.id] = inters_obj 2296 d_connections[x_point__e1_2] = inters_obj 2297 division1.intersections.append(inters_obj) 2298 division2.intersections.append(inters_obj) 2299 intersections.append(inters_obj) 2300 2301 for division in division_list: 2302 division._sort_intersections() 2303 return intersections
Find all intersections of the given divisions. Sweep-line algorithm without a self-balancing tree. Instead of a self-balancing tree, it uses a numpy array to sort and filter the divisions. For the number of divisions that are commonly needed in a lace, this is sufficiently fast. It is also more robust and much easier to understand and debug. Tested with tens of thousands of divisions but not millions. The book has a section on this algorithm. simetri.geometry.py has another version (called all_intersections) for finding intersections among a given list of divisions.
Arguments:
division_list: list of division objects.
Side Effects:
* Modifies the given division objects (in the division_list) in place
by adding the intersections to the divisions' "intersections"
attribute.
* Updates the d_intersections
* Updates the d_connections
Return:
A list of all intersection objects among the given division
list.