@@ -1124,14 +1124,19 @@ def to_array(self):
1124
1124
return tuple (pt .to_array () for pt in self .vertices )
1125
1125
1126
1126
def _to_bool_poly (self ):
1127
- """A hidden method used to translate the Polygon2D to a BooleanPolygon.
1128
-
1129
- This is necessary before performing any boolean operations with
1130
- the polygon.
1131
- """
1127
+ """Translate the Polygon2D to a BooleanPolygon."""
1132
1128
b_pts = (pb .BooleanPoint (pt .x , pt .y ) for pt in self .vertices )
1133
1129
return pb .BooleanPolygon ([b_pts ])
1134
1130
1131
+ def _to_snapped_bool_poly (self , snap_ref_polygon , tolerance ):
1132
+ """Snap a Polygon2D to this one and translate it to a BooleanPolygon.
1133
+
1134
+ This is necessary to ensure that boolean operations will succeed between
1135
+ two polygons.
1136
+ """
1137
+ new_poly = snap_ref_polygon .snap_to_polygon (self , tolerance )
1138
+ return new_poly ._to_bool_poly ()
1139
+
1135
1140
@staticmethod
1136
1141
def _from_bool_poly (bool_polygon ):
1137
1142
"""Get a list of Polygon2D from a BooleanPolygon object."""
@@ -1159,7 +1164,9 @@ def boolean_union(self, polygon, tolerance):
1159
1164
A list of Polygon2D representing the union of the two polygons.
1160
1165
"""
1161
1166
result = pb .union (
1162
- self ._to_bool_poly (), polygon ._to_bool_poly (), tolerance )
1167
+ self ._to_bool_poly (),
1168
+ polygon ._to_snapped_bool_poly (self , tolerance ),
1169
+ tolerance / 100 )
1163
1170
return Polygon2D ._from_bool_poly (result )
1164
1171
1165
1172
def boolean_intersect (self , polygon , tolerance ):
@@ -1176,7 +1183,9 @@ def boolean_intersect(self, polygon, tolerance):
1176
1183
Will be an empty list if no overlap exists between the polygons.
1177
1184
"""
1178
1185
result = pb .intersect (
1179
- self ._to_bool_poly (), polygon ._to_bool_poly (), tolerance )
1186
+ self ._to_bool_poly (),
1187
+ polygon ._to_snapped_bool_poly (self , tolerance ),
1188
+ tolerance / 100 )
1180
1189
return Polygon2D ._from_bool_poly (result )
1181
1190
1182
1191
def boolean_difference (self , polygon , tolerance ):
@@ -1195,7 +1204,9 @@ def boolean_difference(self, polygon, tolerance):
1195
1204
is no overlap between the polygons.
1196
1205
"""
1197
1206
result = pb .difference (
1198
- self ._to_bool_poly (), polygon ._to_bool_poly (), tolerance )
1207
+ self ._to_bool_poly (),
1208
+ polygon ._to_snapped_bool_poly (self , tolerance ),
1209
+ tolerance / 100 )
1199
1210
return Polygon2D ._from_bool_poly (result )
1200
1211
1201
1212
def boolean_xor (self , polygon , tolerance ):
@@ -1224,7 +1235,9 @@ def boolean_xor(self, polygon, tolerance):
1224
1235
in the two.
1225
1236
"""
1226
1237
result = pb .xor (
1227
- self ._to_bool_poly (), polygon ._to_bool_poly (), tolerance )
1238
+ self ._to_bool_poly (),
1239
+ polygon ._to_snapped_bool_poly (self , tolerance ),
1240
+ tolerance / 100 )
1228
1241
return Polygon2D ._from_bool_poly (result )
1229
1242
1230
1243
@staticmethod
@@ -1320,7 +1333,9 @@ def boolean_split(polygon1, polygon2, tolerance):
1320
1333
makes a split version of polygon2.
1321
1334
"""
1322
1335
int_result , poly1_result , poly2_result = pb .split (
1323
- polygon1 ._to_bool_poly (), polygon2 ._to_bool_poly (), tolerance )
1336
+ polygon1 ._to_bool_poly (),
1337
+ polygon2 ._to_snapped_bool_poly (polygon1 , tolerance ),
1338
+ tolerance / 100 )
1324
1339
intersection = Polygon2D ._from_bool_poly (int_result )
1325
1340
poly1_difference = Polygon2D ._from_bool_poly (poly1_result )
1326
1341
poly2_difference = Polygon2D ._from_bool_poly (poly2_result )
@@ -1724,6 +1739,154 @@ def gap_crossing_boundary(polygons, min_separation, tolerance):
1724
1739
1725
1740
return closed_polys
1726
1741
1742
+ @staticmethod
1743
+ def common_axes (
1744
+ polygons , direction , min_distance , merge_distance , fraction_to_keep ,
1745
+ angle_tolerance
1746
+ ):
1747
+ """Get LineSegment2Ds for the most common axes across a set of Polygon2Ds.
1748
+
1749
+ This is often useful as a step before aligning a set of polygons to these
1750
+ common axes.
1751
+
1752
+ Args:
1753
+ polygons: A list or tuple of Polygon2D objects for which common axes
1754
+ will be evaluated.
1755
+ direction: A Vector2D object to represent the direction in which the
1756
+ common axes will be evaluated and generated
1757
+ min_distance: The minimum distance at which common axes will be evaluated.
1758
+ This value should typically be a little larger than the model
1759
+ tolerance (eg. 5 to 20 times the tolerance) in order to ensure that
1760
+ possible common axes across the input polygons are not missed.
1761
+ merge_distance: The distance at which common axes next to one another
1762
+ will be merged into a single axis. This should typically be 2-3
1763
+ times the min_distance in order to avoid generating several axes
1764
+ that are immediately adjacent to one another. When using this
1765
+ method to generate axes for alignment, this merge_distance should
1766
+ be in the range of the alignment distance.
1767
+ fraction_to_keep: A number between 0 and 1 representing the fraction of
1768
+ all possible axes that will be kept in the result. Depending on
1769
+ the complexity of the input geometry, something between 0.1 and
1770
+ 0.3 is typically appropriate.
1771
+ angle_tolerance: The max angle difference in radians that the polygon
1772
+ segments direction can differ from the input direction before the
1773
+ segments are not factored into this calculation of common axes.
1774
+
1775
+ Returns:
1776
+ A list of LineSegment2D objects for the common axes across the
1777
+ input polygons.
1778
+ """
1779
+ # gather the relevant segments of the input polygons
1780
+ min_ang , max_ang = angle_tolerance , math .pi - angle_tolerance
1781
+ rel_segs = []
1782
+ for p_gon in polygons :
1783
+ for seg in p_gon .segments :
1784
+ try :
1785
+ s_ang = direction .angle (seg .v )
1786
+ if s_ang < min_ang or s_ang > max_ang :
1787
+ rel_segs .append (seg )
1788
+ except ZeroDivisionError : # zero length segment to ignore
1789
+ continue
1790
+ if len (rel_segs ) == 0 :
1791
+ return [] # none of the polygon segments are relevant in the direction
1792
+
1793
+ # determine the extents around the polygons and the input direction
1794
+ gen_vec = direction .rotate (math .pi / 2 )
1795
+ axis_angle = Vector2D (0 , 1 ).angle_counterclockwise (gen_vec )
1796
+ orient_poly = polygons
1797
+ if axis_angle != 0 : # rotate geometry to the bounding box
1798
+ cpt = polygons [0 ].vertices [0 ]
1799
+ orient_poly = [pl .rotate (- axis_angle , cpt ) for pl in polygons ]
1800
+ xx = Polygon2D ._bounding_domain_x (orient_poly )
1801
+ yy = Polygon2D ._bounding_domain_y (orient_poly )
1802
+ min_pt = Point2D (xx [0 ], yy [0 ])
1803
+ max_pt = Point2D (xx [1 ], yy [1 ])
1804
+ if axis_angle != 0 : # rotate the points back
1805
+ min_pt = min_pt .rotate (axis_angle , cpt )
1806
+ max_pt = max_pt .rotate (axis_angle , cpt )
1807
+
1808
+ # generate all possible axes from the extents and min_distance
1809
+ axis_vec = direction .normalize () * (xx [1 ] - xx [0 ])
1810
+ incr_vec = gen_vec .normalize () * (min_distance )
1811
+ current_pt = min_pt
1812
+ current_dist , max_dist = 0 , yy [1 ] - yy [0 ]
1813
+ all_axes = []
1814
+ while current_dist < max_dist :
1815
+ axis = LineSegment2D (current_pt , axis_vec )
1816
+ all_axes .append (axis )
1817
+ current_pt = current_pt .move (incr_vec )
1818
+ current_dist += min_distance
1819
+
1820
+ # evaluate the axes based on how many relevant segments they are next to
1821
+ mid_pts = [seg .midpoint for seg in rel_segs ]
1822
+ rel_axes , axes_value = [], []
1823
+ for axis in all_axes :
1824
+ axis_val = 0
1825
+ for pt in mid_pts :
1826
+ if axis .distance_to_point (pt ) <= merge_distance :
1827
+ axis_val += 1
1828
+ if axis_val != 0 :
1829
+ rel_axes .append (axis )
1830
+ axes_value .append (axis_val )
1831
+ if len (rel_axes ) == 0 :
1832
+ return [] # none of the generated axes are relevant
1833
+
1834
+ # sort the axes by how relevant they are to segments and keep a certain fraction
1835
+ count_to_keep = int (len (all_axes ) * fraction_to_keep )
1836
+ i_to_keep = [i for _ , i in sorted (zip (axes_value , range (len (rel_axes ))))]
1837
+ i_to_keep .reverse ()
1838
+ if count_to_keep == 0 :
1839
+ count_to_keep = 1
1840
+ elif count_to_keep > len (i_to_keep ):
1841
+ count_to_keep = len (i_to_keep )
1842
+ rel_i = i_to_keep [:count_to_keep ]
1843
+ rel_i .sort ()
1844
+ rel_axes = [rel_axes [i ] for i in rel_i ]
1845
+
1846
+ # group the axes by proximity
1847
+ last_ax = rel_axes [0 ]
1848
+ axes_groups = [[last_ax ]]
1849
+ for axis in rel_axes [1 :]:
1850
+ if axis .p .distance_to_point (last_ax .p ) <= merge_distance :
1851
+ axes_groups [- 1 ].append (axis )
1852
+ else : # start a new group
1853
+ axes_groups .append ([axis ])
1854
+ last_ax = axis
1855
+
1856
+ # average the line segments that are within the merge_distance of one another
1857
+ final_axes = []
1858
+ for ax_group in axes_groups :
1859
+ if len (ax_group ) == 1 :
1860
+ final_axes .append (ax_group [0 ])
1861
+ else :
1862
+ st_pt_x = (ax_group [0 ].p1 .x + ax_group [- 1 ].p1 .x ) / 2
1863
+ st_pt_y = (ax_group [0 ].p1 .y + ax_group [- 1 ].p1 .y ) / 2
1864
+ avg_ax = LineSegment2D (Point2D (st_pt_x , st_pt_y ), axis_vec )
1865
+ final_axes .append (avg_ax )
1866
+ return final_axes
1867
+
1868
+ @staticmethod
1869
+ def _bounding_domain_x (geometries ):
1870
+ """Get minimum and maximum X coordinates of multiple polygons."""
1871
+ min_x , max_x = geometries [0 ].min .x , geometries [0 ].max .x
1872
+ for geom in geometries [1 :]:
1873
+ if geom .min .x < min_x :
1874
+ min_x = geom .min .x
1875
+ if geom .max .x > max_x :
1876
+ max_x = geom .max .x
1877
+ return min_x , max_x
1878
+
1879
+ @staticmethod
1880
+ def _bounding_domain_y (geometries ):
1881
+ """Get minimum and maximum Y coordinates of multiple polygons."""
1882
+ min_y , max_y = geometries [0 ].min .y , geometries [0 ].max .y
1883
+ for geom in geometries [1 :]:
1884
+ if geom .min .y < min_y :
1885
+ min_y = geom .min .y
1886
+ if geom .max .y > max_y :
1887
+ max_y = geom .max .y
1888
+ return min_y , max_y
1889
+
1727
1890
def _point_in_polygon (self , tolerance ):
1728
1891
"""Get a Point2D that is always reliably inside this Polygon2D.
1729
1892
0 commit comments