Skip to content

Commit

Permalink
fix(roof): Improve roof resolution to always remove overlaps
Browse files Browse the repository at this point in the history
This commit includes several improvements that come courtesy of lots of real-world models. At this point, even the messiest Revit roofs that I have found can all be resolved into something that does not have any overlaps, meaning that they should always produce solid room volumes that account for the roof.
  • Loading branch information
chriswmackey committed Oct 4, 2024
1 parent 60f4e12 commit 2f6a568
Show file tree
Hide file tree
Showing 2 changed files with 90 additions and 43 deletions.
114 changes: 74 additions & 40 deletions dragonfly/roof.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,47 +276,81 @@ def resolved_geometry(self, tolerance=0.01):
for i in gei:
poly_1 = geo_2d[i]
pln_1 = planes[i]
try:
for j in gei[i + 1:]:
poly_2 = geo_2d[j]
pln_2 = planes[j]
poly_relationship = poly_1.polygon_relationship(poly_2, tolerance)
if poly_relationship == 0:
# resolve the overlap between the polygons
overlap_count += 1
try:
overlap_polys = poly_1.boolean_intersect(poly_2, tolerance)
except Exception: # tolerance is likely not set correctly
for j in gei[i + 1:]:
poly_2 = geo_2d[j]
pln_2 = planes[j]
poly_relationship = poly_1.polygon_relationship(poly_2, tolerance)
tol = tolerance # temporary tolerance value that may be adjusted
if poly_relationship == 0:
# resolve the overlap between the polygons
overlap_count += 1
try:
overlap_polys = poly_1.boolean_intersect(poly_2, tol)
except Exception as e: # tolerance is likely not correct
try: # make an attempt at a slightly lower tol
tol = tol / 10
overlap_polys = poly_1.boolean_intersect(poly_1, tol)
except Exception:
print('Failed to get boolean intersect.\n{}'.format(e))
continue
for o_poly in overlap_polys:
o_face_1, o_face_2 = [], []
for pt in o_poly.vertices:
pt1 = pln_1.project_point(Point3D(pt.x, pt.y), proj_dir)
pt2 = pln_2.project_point(Point3D(pt.x, pt.y), proj_dir)
o_face_1.append(pt1)
o_face_2.append(pt2)
o_face_1 = Face3D(o_face_1, plane=pln_1)
o_face_2 = Face3D(o_face_2, plane=pln_2)
if o_face_1.center.z > o_face_2.center.z:
# remove the overlap from the first polygon
try:
new_polys = poly_1.boolean_difference(o_poly, tolerance)
poly_1 = new_polys[0] if len(new_polys) == 1 else poly_1
geo_2d[i] = poly_1
except Exception: # tolerance is likely not set correctly
pass
else: # remove the overlap from the second polygon
try:
new_polys = poly_2.boolean_difference(o_poly, tolerance)
poly_2 = new_polys[0] if len(new_polys) == 1 else poly_2
geo_2d[j] = poly_2
except Exception: # tolerance is likely not set correctly
pass
elif poly_relationship == 1:
# polygon is completely inside the other; remove it
remove_i.append(j)
except IndexError:
pass # we have reached the end of the list
for o_poly in overlap_polys:
o_face_1, o_face_2 = [], []
for pt in o_poly.vertices:
pt1 = pln_1.project_point(Point3D(pt.x, pt.y), proj_dir)
pt2 = pln_2.project_point(Point3D(pt.x, pt.y), proj_dir)
o_face_1.append(pt1)
o_face_2.append(pt2)
o_face_1 = Face3D(o_face_1, plane=pln_1)
o_face_2 = Face3D(o_face_2, plane=pln_2)
if o_face_1.center.z > o_face_2.center.z:
try: # remove the overlap from the first polygon
np = poly_1.boolean_difference(o_poly, tol)
if len(np) == 0: # eliminated the polygon
remove_i.append(i)
else: # part-removed
try:
poly_1 = np[0].remove_colinear_vertices(tol)
geo_2d[i] = poly_1
except AssertionError: # degenerate result
remove_i.append(i)
if len(np) > 1: # split the polygon to multiple
for ply in np[1:]:
try:
cp = ply.remove_colinear_vertices(tol)
geo_2d.append(cp)
planes.append(pln_1)
gei.append(len(gei))
except AssertionError: # degenerate result
pass
except Exception as e: # tolerance is likely not correct
print('Failed to get boolean difference.\n{}'.format(e))
pass
else: # remove the overlap from the second polygon
try:
np = poly_2.boolean_difference(o_poly, tol)
if len(np) == 0: # eliminated the polygon
remove_i.append(j)
else: # part-removed
try:
poly_2 = np[0].remove_colinear_vertices(tol)
geo_2d[j] = poly_2
except AssertionError: # degenerate result
remove_i.append(j)
if len(np) > 1: # split the polygon to multiple
for ply in np[1:]:
try:
cp = ply.remove_colinear_vertices(tol)
geo_2d.append(cp)
planes.append(pln_2)
gei.append(len(gei))
except AssertionError: # degenerate result
pass
except Exception as e: # tolerance is likely not correct
print('Failed to get boolean difference.\n{}'.format(e))
pass
elif poly_relationship == 1:
# polygon is completely inside the other; remove it
remove_i.append(j)

# if any overlaps were found, rebuild the 3D roof geometry
if overlap_count != 0 or len(remove_i) != 0:
Expand Down
19 changes: 16 additions & 3 deletions tests/model_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from dragonfly.building import Building
from dragonfly.story import Story
from dragonfly.room2d import Room2D
from dragonfly.roof import RoofSpecification
from dragonfly.context import ContextShade
from dragonfly.windowparameter import SimpleWindowRatio
from dragonfly.projection import meters_to_long_lat_factors
Expand Down Expand Up @@ -696,9 +697,21 @@ def test_roof_resolved_geometry():
upper_story = model.buildings[0][-1]
assert upper_story.roof is not None

res_rof = upper_story.roof.resolved_geometry(0.03)
assert sum(g.area for g in res_rof) <= 86916.0
assert len(res_rof) == 86
res_geo = upper_story.roof.resolved_geometry(0.003)
assert sum(g.area for g in res_geo) <= 86916.0
res_roof = RoofSpecification(res_geo)
assert res_roof.overlap_count(0.003) == 0


def test_roof_resolved_geometry_2():
"""Test another case with the roof.resolved_geometry method."""
model_file = './tests/json/model_with_bldg_roofs.dfjson'
model = Model.from_file(model_file)
upper_story = model.buildings[0][-1]

res_geo = upper_story.roof.resolved_geometry(0.003)
res_roof = RoofSpecification(res_geo)
assert res_roof.overlap_count(0.003) == 0


def test_large_room_with_roof():
Expand Down

0 comments on commit 2f6a568

Please sign in to comment.