Skip to content

Commit 61d0410

Browse files
chriswmackeyChris Mackey
authored andcommitted
fix(face): Add methods to union and group Face3D by overlap
1 parent b5ac25e commit 61d0410

File tree

3 files changed

+142
-4
lines changed

3 files changed

+142
-4
lines changed

ladybug_geometry/geometry2d/polygon.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1378,7 +1378,7 @@ def overlapping_bounding_rect(polygon1, polygon2, tolerance):
13781378
def group_by_overlap(polygons, tolerance):
13791379
"""Group Polygon2Ds that overlap one another greater than the tolerance.
13801380
1381-
This is useful as a pre-step before running Polygon2d.boolean_union_all()
1381+
This is useful as a pre-step before running Polygon2D.boolean_union_all()
13821382
in order to assess whether unionizing is necessary and to ensure that
13831383
it is only performed among the necessary groups of polygons.
13841384
@@ -1396,13 +1396,13 @@ def group_by_overlap(polygons, tolerance):
13961396
for poly in polygons[1:]:
13971397
group_found = False
13981398
for poly_group in grouped_polys:
1399-
if group_found:
1400-
break
14011399
for oth_poly in poly_group:
14021400
if poly.polygon_relationship(oth_poly, tolerance) >= 0:
14031401
poly_group.append(poly)
14041402
group_found = True
14051403
break
1404+
if group_found:
1405+
break
14061406
if not group_found: # the polygon does not overlap with any of the others
14071407
grouped_polys.append([poly]) # make a new group for the polygon
14081408
return grouped_polys

ladybug_geometry/geometry3d/face.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1824,6 +1824,62 @@ def sub_rects_from_rect_dimensions(
18241824
base_plane)]
18251825
return final_faces
18261826

1827+
@staticmethod
1828+
def coplanar_union(face1, face2, tolerance, angle_tolerance):
1829+
"""Boolean Union two coplanar Face3D with one another.
1830+
1831+
Args:
1832+
face1: A Face3D for the first face that will be unioned with the second face.
1833+
face2: A Face3D for the second face that will be unioned with the first face.
1834+
tolerance: The minimum distance between points before they are considered
1835+
distinct from one another.
1836+
angle_tolerance: The max angle in radians that the plane normals can
1837+
differ from one another in order for them to be considered coplanar.
1838+
1839+
Returns:
1840+
A single Face3D for the Union of the two input Face3D. When the faces
1841+
are not coplanar or they do not overlap, None will be returned.
1842+
"""
1843+
# test whether the faces are coplanar
1844+
prim_pl = face1.plane
1845+
if not prim_pl.is_coplanar_tolerance(face2.plane, tolerance, angle_tolerance):
1846+
return None
1847+
# test whether the two polygons have any overlap in 2D space
1848+
f1_poly = face1.boundary_polygon2d
1849+
f2_poly = Polygon2D(tuple(prim_pl.xyz_to_xy(pt) for pt in face2.boundary))
1850+
if not Polygon2D.overlapping_bounding_rect(f1_poly, f2_poly, tolerance):
1851+
return None
1852+
if f1_poly.polygon_relationship(f2_poly, tolerance) == -1:
1853+
return None
1854+
# snap the polygons to one another to avoid tolerance issues
1855+
try:
1856+
f1_poly = f1_poly.remove_colinear_vertices(tolerance)
1857+
f2_poly = f2_poly.remove_colinear_vertices(tolerance)
1858+
except AssertionError: # degenerate faces input
1859+
return None
1860+
s2_poly = f1_poly.snap_to_polygon(f2_poly, tolerance)
1861+
# get BooleanPolygons of the two faces
1862+
f1_polys = [(pb.BooleanPoint(pt.x, pt.y) for pt in f1_poly.vertices)]
1863+
f2_polys = [(pb.BooleanPoint(pt.x, pt.y) for pt in s2_poly.vertices)]
1864+
if face1.has_holes:
1865+
for hole in face1.hole_polygon2d:
1866+
f1_polys.append((pb.BooleanPoint(pt.x, pt.y) for pt in hole.vertices))
1867+
if face2.has_holes:
1868+
for hole in face2.holes:
1869+
h_pt2d = (prim_pl.xyz_to_xy(pt) for pt in hole)
1870+
f2_polys.append((pb.BooleanPoint(pt.x, pt.y) for pt in h_pt2d))
1871+
b_poly1 = pb.BooleanPolygon(f1_polys)
1872+
b_poly2 = pb.BooleanPolygon(f2_polys)
1873+
# split the two boolean polygons with one another
1874+
int_tol = tolerance / 100
1875+
try:
1876+
poly_result = pb.union(b_poly1, b_poly2, int_tol)
1877+
except Exception:
1878+
return [face1], [face2] # typically a tolerance issue causing failure
1879+
# rebuild the Face3D from the results and return them
1880+
union_faces = Face3D._from_bool_poly(poly_result, prim_pl)
1881+
return union_faces[0]
1882+
18271883
@staticmethod
18281884
def coplanar_split(face1, face2, tolerance, angle_tolerance):
18291885
"""Split two coplanar Face3D with one another (ensuring matching overlapped area)
@@ -1928,6 +1984,45 @@ def _from_bool_poly(bool_polygon, plane):
19281984
face_3d.append(Face3D(pg_3d[0], plane, holes=pg_3d[1:]))
19291985
return face_3d
19301986

1987+
@staticmethod
1988+
def group_by_coplanar_overlap(faces, tolerance):
1989+
"""Group coplanar Face3Ds depending on whether they overlap one another.
1990+
1991+
This is useful as a pre-step before running Face3D.coplanar_union()
1992+
in order to assess whether unionizing is necessary and to ensure that
1993+
it is only performed among the necessary groups of faces.
1994+
1995+
Args:
1996+
faces: A list of Face3D to be grouped by their overlapping.
1997+
tolerance: The minimum distance from the edge of a neighboring Face3D
1998+
at which a point is considered to overlap with that Face3D.
1999+
2000+
Returns:
2001+
A list of lists where each sub-list represents a group of Face3Ds
2002+
that all overlap with one another.
2003+
"""
2004+
# create polygons for all of the faces
2005+
r_plane = faces[0].plane
2006+
polygons = [Polygon2D([r_plane.xyz_to_xy(pt) for pt in face.vertices])
2007+
for face in faces]
2008+
# loop through the polygons and check to see if it overlaps with the others
2009+
grouped_polys, grouped_faces = [[polygons[0]]], [[faces[0]]]
2010+
for poly, face in zip(polygons[1:], faces[1:]):
2011+
group_found = False
2012+
for poly_group, face_group in zip(grouped_polys, grouped_faces):
2013+
for oth_poly in poly_group:
2014+
if poly.polygon_relationship(oth_poly, tolerance) >= 0:
2015+
poly_group.append(poly)
2016+
face_group.append(face)
2017+
group_found = True
2018+
break
2019+
if group_found:
2020+
break
2021+
if not group_found: # the polygon does not overlap with any of the others
2022+
grouped_polys.append([poly]) # make a new group for the polygon
2023+
grouped_faces.append([face]) # make a new group for the face
2024+
return grouped_faces
2025+
19312026
@staticmethod
19322027
def join_coplanar_faces(faces, tolerance):
19332028
"""Join a list of coplanar Face3Ds together to get as few as possible.

tests/face3d_test.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1505,8 +1505,29 @@ def test_sub_faces_by_ratio_sub_rectangle_tol_issue():
15051505
assert face_1.is_sub_face(sf, 0.01, 1)
15061506

15071507

1508+
def test_coplanar_union():
1509+
"""Test the coplanar_union method."""
1510+
b_pts1 = (Point3D(-14.79, -36.61, 0.00), Point3D(6.68, -36.61, 0.00),
1511+
Point3D(6.68, -10.37, 0.00), Point3D(-14.79, -10.37, 0.00))
1512+
h_pts1 = (
1513+
(Point3D(-9.71, -22.66, 0.00), Point3D(-9.71, -16.19, 0.00),
1514+
Point3D(-5.01, -16.19, 0.00), Point3D(-5.01, -22.66, 0.00)),
1515+
(Point3D(0.31, -35.20, 0.00), Point3D(0.31, -29.89, 0.00),
1516+
Point3D(5.15, -29.89, 0.00), Point3D(5.15, -35.20, 0.00))
1517+
)
1518+
b_pts2 = (Point3D(-7.02, -32.34, 0.00), Point3D(13.23, -32.34, 0.00),
1519+
Point3D(13.23, -18.57, 0.00), Point3D(-7.02, -18.57, 0.00))
1520+
face1 = Face3D(b_pts1, holes=h_pts1)
1521+
face2 = Face3D(b_pts2)
1522+
1523+
face_union = Face3D.coplanar_union(face1, face2, 0.01, 1)
1524+
1525+
assert isinstance(face_union, Face3D)
1526+
assert len(face_union.holes) == 2
1527+
1528+
15081529
def test_coplanar_split():
1509-
"""Test the coplanar_split_method."""
1530+
"""Test the coplanar_split method."""
15101531
b_pts1 = (Point3D(-14.79, -36.61, 0.00), Point3D(6.68, -36.61, 0.00),
15111532
Point3D(6.68, -10.37, 0.00), Point3D(-14.79, -10.37, 0.00))
15121533
h_pts1 = (
@@ -1526,6 +1547,28 @@ def test_coplanar_split():
15261547
assert len(split2) == 4
15271548

15281549

1550+
def test_group_by_coplanar_overlap():
1551+
"""Test the group_by_coplanar_overlap method."""
1552+
bound_pts1 = [Point3D(0, 0), Point3D(4, 0), Point3D(4, 4), Point3D(0, 4)]
1553+
bound_pts2 = [Point3D(2, 2), Point3D(6, 2), Point3D(6, 6), Point3D(2, 6)]
1554+
bound_pts3 = [Point3D(6, 6), Point3D(7, 6), Point3D(7, 7), Point3D(6, 7)]
1555+
face1 = Face3D(bound_pts1)
1556+
face2 = Face3D(bound_pts2)
1557+
face3 = Face3D(bound_pts3)
1558+
1559+
all_faces = [face1, face2, face3]
1560+
1561+
grouped_faces = Face3D.group_by_coplanar_overlap(all_faces, 0.01)
1562+
assert len(grouped_faces) == 2
1563+
assert len(grouped_faces[0]) == 2
1564+
assert len(grouped_faces[1]) == 1
1565+
1566+
grouped_faces = Face3D.group_by_coplanar_overlap(list(reversed(all_faces)), 0.01)
1567+
assert len(grouped_faces) == 2
1568+
assert len(grouped_faces[0]) == 1
1569+
assert len(grouped_faces[1]) == 2
1570+
1571+
15291572
def test_join_coplanar_faces():
15301573
"""Test the join_coplanar_faces method."""
15311574
geo_file = './tests/json/polygons_for_joined_boundary.json'

0 commit comments

Comments
 (0)