From 5313c2c65de860150c5eba86333b2f068bc24630 Mon Sep 17 00:00:00 2001 From: Chris Mackey Date: Fri, 15 Nov 2024 10:30:36 -0800 Subject: [PATCH] fix(boolean): Improve boolean operations with better tolerance check --- ladybug_geometry/geometry2d/polygon.py | 63 ++++++++++++++++++-------- ladybug_geometry/geometry3d/face.py | 47 +++++++++++-------- 2 files changed, 70 insertions(+), 40 deletions(-) diff --git a/ladybug_geometry/geometry2d/polygon.py b/ladybug_geometry/geometry2d/polygon.py index a0b4cacf..c742b094 100644 --- a/ladybug_geometry/geometry2d/polygon.py +++ b/ladybug_geometry/geometry2d/polygon.py @@ -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. @@ -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. @@ -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. @@ -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. @@ -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): @@ -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): @@ -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): @@ -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 diff --git a/ladybug_geometry/geometry3d/face.py b/ladybug_geometry/geometry3d/face.py index 8bd1507d..e4c56581 100644 --- a/ladybug_geometry/geometry3d/face.py +++ b/ladybug_geometry/geometry3d/face.py @@ -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: @@ -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): @@ -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 @@ -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 @@ -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 @@ -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 @@ -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]]]