Skip to content

Commit

Permalink
fix(boolean): Improve boolean operations with better tolerance check
Browse files Browse the repository at this point in the history
  • Loading branch information
chriswmackey committed Nov 15, 2024
1 parent da11268 commit 5313c2c
Show file tree
Hide file tree
Showing 2 changed files with 70 additions and 40 deletions.
63 changes: 43 additions & 20 deletions ladybug_geometry/geometry2d/polygon.py
Original file line number Diff line number Diff line change
Expand Up @@ -1294,10 +1294,28 @@ def _to_snapped_bool_poly(self, snap_ref_polygon, tolerance):
return new_poly._to_bool_poly()

@staticmethod
def _from_bool_poly(bool_polygon):
"""Get a list of Polygon2D from a BooleanPolygon object."""
return [Polygon2D(tuple(Point2D(pt.x, pt.y) for pt in new_poly))
for new_poly in bool_polygon.regions if len(new_poly) > 2]
def _from_bool_poly(bool_polygon, tolerance=None):
"""Get a list of Polygon2D from a BooleanPolygon object.
Args:
bool_polygon: A BooleanPolygon to be interpreted to a list of Polygon2D.
tolerance: An optional tolerance value to be used to remove
degenerate objects from the result. If None, the result may
contain degenerate objects.
"""
polys = []
for new_poly in bool_polygon.regions:
if len(new_poly) > 2:
poly = Polygon2D(tuple(Point2D(pt.x, pt.y) for pt in new_poly))
if tolerance is not None:
try:
poly = poly.remove_duplicate_vertices(tolerance)
polys.append(poly)
except AssertionError:
pass # degenerate polygon to be removed
else:
polys.append(poly)
return polys

def boolean_union(self, polygon, tolerance):
"""Get a list of Polygon2D for the union of this Polygon and another.
Expand All @@ -1322,8 +1340,9 @@ def boolean_union(self, polygon, tolerance):
result = pb.union(
self._to_bool_poly(),
polygon._to_snapped_bool_poly(self, tolerance),
tolerance / 100)
return Polygon2D._from_bool_poly(result)
tolerance / 1000
)
return Polygon2D._from_bool_poly(result, tolerance)

def boolean_intersect(self, polygon, tolerance):
"""Get a list of Polygon2D for the intersection of this Polygon and another.
Expand All @@ -1341,8 +1360,9 @@ def boolean_intersect(self, polygon, tolerance):
result = pb.intersect(
self._to_bool_poly(),
polygon._to_snapped_bool_poly(self, tolerance),
tolerance / 100)
return Polygon2D._from_bool_poly(result)
tolerance / 1000
)
return Polygon2D._from_bool_poly(result, tolerance)

def boolean_difference(self, polygon, tolerance):
"""Get a list of Polygon2D for the subtraction of another polygon from this one.
Expand All @@ -1362,8 +1382,9 @@ def boolean_difference(self, polygon, tolerance):
result = pb.difference(
self._to_bool_poly(),
polygon._to_snapped_bool_poly(self, tolerance),
tolerance / 100)
return Polygon2D._from_bool_poly(result)
tolerance / 1000
)
return Polygon2D._from_bool_poly(result, tolerance)

def boolean_xor(self, polygon, tolerance):
"""Get Polygon2D list for the exclusive disjunction of this polygon and another.
Expand Down Expand Up @@ -1393,8 +1414,9 @@ def boolean_xor(self, polygon, tolerance):
result = pb.xor(
self._to_bool_poly(),
polygon._to_snapped_bool_poly(self, tolerance),
tolerance / 100)
return Polygon2D._from_bool_poly(result)
tolerance / 1000
)
return Polygon2D._from_bool_poly(result, tolerance)

@staticmethod
def snap_polygons(polygons, tolerance):
Expand Down Expand Up @@ -1445,8 +1467,8 @@ def boolean_union_all(polygons, tolerance):
"""
polygons = Polygon2D.snap_polygons(polygons, tolerance)
bool_polys = [poly._to_bool_poly() for poly in polygons]
result = pb.union_all(bool_polys, tolerance / 100)
return Polygon2D._from_bool_poly(result)
result = pb.union_all(bool_polys, tolerance / 1000)
return Polygon2D._from_bool_poly(result, tolerance)

@staticmethod
def boolean_intersect_all(polygons, tolerance):
Expand Down Expand Up @@ -1475,8 +1497,8 @@ def boolean_intersect_all(polygons, tolerance):
"""
polygons = Polygon2D.snap_polygons(polygons, tolerance)
bool_polys = [poly._to_bool_poly() for poly in polygons]
result = pb.intersect_all(bool_polys, tolerance / 100)
return Polygon2D._from_bool_poly(result)
result = pb.intersect_all(bool_polys, tolerance / 1000)
return Polygon2D._from_bool_poly(result, tolerance)

@staticmethod
def boolean_split(polygon1, polygon2, tolerance):
Expand Down Expand Up @@ -1516,10 +1538,11 @@ def boolean_split(polygon1, polygon2, tolerance):
int_result, poly1_result, poly2_result = pb.split(
polygon1._to_bool_poly(),
polygon2._to_snapped_bool_poly(polygon1, tolerance),
tolerance / 100)
intersection = Polygon2D._from_bool_poly(int_result)
poly1_difference = Polygon2D._from_bool_poly(poly1_result)
poly2_difference = Polygon2D._from_bool_poly(poly2_result)
tolerance / 1000
)
intersection = Polygon2D._from_bool_poly(int_result, tolerance)
poly1_difference = Polygon2D._from_bool_poly(poly1_result, tolerance)
poly2_difference = Polygon2D._from_bool_poly(poly2_result, tolerance)
return intersection, poly1_difference, poly2_difference

@staticmethod
Expand Down
47 changes: 27 additions & 20 deletions ladybug_geometry/geometry3d/face.py
Original file line number Diff line number Diff line change
Expand Up @@ -2231,7 +2231,7 @@ def coplanar_difference(self, faces, tolerance, angle_tolerance):
return [self]

# loop through the boolean polygons and subtract them
int_tol = tolerance / 100
int_tol = tolerance / 1000
for b_poly2 in relevant_b_polys:
# subtract the boolean polygons
try:
Expand All @@ -2240,7 +2240,7 @@ def coplanar_difference(self, faces, tolerance, angle_tolerance):
return [self] # typically a tolerance issue causing failure

# rebuild the Face3D from the result of the subtraction
return Face3D._from_bool_poly(b_poly1, prim_pl)
return Face3D._from_bool_poly(b_poly1, prim_pl, tolerance)

@staticmethod
def coplanar_union(face1, face2, tolerance, angle_tolerance):
Expand Down Expand Up @@ -2287,13 +2287,13 @@ def coplanar_union(face1, face2, tolerance, angle_tolerance):
b_poly1 = pb.BooleanPolygon(f1_polys)
b_poly2 = pb.BooleanPolygon(f2_polys)
# union the two boolean polygons with one another
int_tol = tolerance / 100
int_tol = tolerance / 1000
try:
poly_result = pb.union(b_poly1, b_poly2, int_tol)
except Exception:
return None # typically a tolerance issue causing failure
# rebuild the Face3D from the results and return them
union_faces = Face3D._from_bool_poly(poly_result, prim_pl)
union_faces = Face3D._from_bool_poly(poly_result, prim_pl, tolerance)
return union_faces[0]

@staticmethod
Expand Down Expand Up @@ -2344,13 +2344,13 @@ def coplanar_intersection(face1, face2, tolerance, angle_tolerance):
b_poly1 = pb.BooleanPolygon(f1_polys)
b_poly2 = pb.BooleanPolygon(f2_polys)
# intersect the two boolean polygons with one another
int_tol = tolerance / 100
int_tol = tolerance / 1000
try:
poly_result = pb.intersect(b_poly1, b_poly2, int_tol)
except Exception:
return None # typically a tolerance issue causing failure
# rebuild the Face3D from the results and return them
int_faces = Face3D._from_bool_poly(poly_result, prim_pl)
int_faces = Face3D._from_bool_poly(poly_result, prim_pl, tolerance)
return int_faces

@staticmethod
Expand Down Expand Up @@ -2416,15 +2416,15 @@ def coplanar_split(face1, face2, tolerance, angle_tolerance):
b_poly1 = pb.BooleanPolygon(f1_polys)
b_poly2 = pb.BooleanPolygon(f2_polys)
# split the two boolean polygons with one another
int_tol = tolerance / 100
int_tol = tolerance / 1000
try:
int_result, poly1_result, poly2_result = pb.split(b_poly1, b_poly2, int_tol)
except Exception:
return [face1], [face2] # typically a tolerance issue causing failure
# rebuild the Face3D from the results and return them
int_faces = Face3D._from_bool_poly(int_result, prim_pl)
poly1_faces = Face3D._from_bool_poly(poly1_result, prim_pl)
poly2_faces = Face3D._from_bool_poly(poly2_result, prim_pl)
int_faces = Face3D._from_bool_poly(int_result, prim_pl, tolerance)
poly1_faces = Face3D._from_bool_poly(poly1_result, prim_pl, tolerance)
poly2_faces = Face3D._from_bool_poly(poly2_result, prim_pl, tolerance)
face1_split = poly1_faces + int_faces
face2_split = poly2_faces + int_faces
return face1_split, face2_split
Expand Down Expand Up @@ -2495,11 +2495,11 @@ def coplanar_union_all(faces, tolerance, angle_tolerance):
except Exception:
return None # typically a tolerance issue causing failure
# rebuild the Face3D from the results and return them
union_faces = Face3D._from_bool_poly(poly_result, prim_pl)
union_faces = Face3D._from_bool_poly(poly_result, prim_pl, tolerance)
return union_faces

@staticmethod
def _from_bool_poly(bool_polygon, plane, snap_tolerance=None):
def _from_bool_poly(bool_polygon, plane, tolerance=None):
"""Get a list of Face3D from a BooleanPolygon.
This method will automatically check whether any of the regions is meant
Expand All @@ -2508,21 +2508,28 @@ def _from_bool_poly(bool_polygon, plane, snap_tolerance=None):
Args:
bool_polygon: A BooleanPolygon to be interpreted to Face3D.
plane: The Plane in which the resulting Face3Ds exist.
snap_tolerance: An optional tolerance value to be used to snap the
polygons together before turning them into Face3D. If None,
no snapping will occur.
tolerance: An optional tolerance value to be used to remove
degenerate objects from the result. If None, the result may
contain degenerate objects.
"""
# serialize the BooleanPolygon into Polygon2D
polys = [Polygon2D(tuple(Point2D(pt.x, pt.y) for pt in new_poly))
for new_poly in bool_polygon.regions if len(new_poly) > 2]
polys = []
for new_poly in bool_polygon.regions:
if len(new_poly) > 2:
poly = Polygon2D(tuple(Point2D(pt.x, pt.y) for pt in new_poly))
if tolerance is not None:
try:
poly = poly.remove_duplicate_vertices(tolerance)
polys.append(poly)
except AssertionError:
pass # degenerate polygon to be removed
else:
polys.append(poly)
if len(polys) == 0:
return []
if len(polys) == 1:
verts_3d = tuple(plane.xy_to_xyz(pt) for pt in polys[0].vertices)
return [Face3D(verts_3d, plane)]
# snap the polygons together if requested
if snap_tolerance is not None:
polys = Polygon2D.snap_polygons(polys, snap_tolerance)
# sort the polygons by area and check if any are inside the others
polys.sort(key=lambda x: x.area, reverse=True)
poly_groups = [[polys[0]]]
Expand Down

0 comments on commit 5313c2c

Please sign in to comment.