Add footprint corner sampling for roof detection

get_vertical_bounding_planes now also casts rays from the footprint
polygon's corner vertices, offset slightly inward toward the centroid.
This catches bounding elements (e.g. sloped roofs) that only cover a
corner of the space.

Generated with the assistance of an AI coding tool.
This commit is contained in:
CyrilWaechter
2026-08-04 02:44:15 +02:00
parent c8d39c6cc5
commit 4e5607b054
2 changed files with 22 additions and 9 deletions
+9 -3
View File
@@ -685,10 +685,16 @@ class TestRegenerateSpaceFromRealIfc2x3(NewFile):
ifc = self.load_house_with_garage() ifc = self.load_house_with_garage()
(original_bounds, original_origin), (regen_bounds, regen_origin) = self._regenerate_space(ifc, 2363) (original_bounds, original_origin), (regen_bounds, regen_origin) = self._regenerate_space(ifc, 2363)
assert (regen_origin - original_origin).length < 0.02 assert (regen_origin - original_origin).length < 0.02
# X, Y, Z-min stable; Z-max may differ because the regenerated space # X and Y stable; Z may differ because the regenerated space detects the
# correctly detects the roof and clips to a different top elevation. # sloped roof and clips the extrusion.
for j in (0, 1, 4): for j in (0, 1, 2, 3, 4):
assert regen_bounds[j] == pytest.approx(original_bounds[j], abs=0.02) assert regen_bounds[j] == pytest.approx(original_bounds[j], abs=0.02)
# Verify the regenerated body contains boolean clipping (roof clipping).
space = ifc.by_id(2363)
body = ifcopenshell.util.representation.get_representation(space, "Model", "Body", "MODEL_VIEW")
assert body is not None
boolean_items = [i for i in (body.Items or []) if i.is_a("IfcBooleanClippingResult")]
assert len(boolean_items) >= 1, "Expected roof clipping but got no boolean result"
def test_regenerate_space_twice_does_not_duplicate_half_spaces(self): def test_regenerate_space_twice_does_not_duplicate_half_spaces(self):
ifc = self.load_house_with_garage() ifc = self.load_house_with_garage()
@@ -302,15 +302,22 @@ def get_vertical_bounding_planes(
bounds = space_polygon.bounds bounds = space_polygon.bounds
cx = (bounds[0] + bounds[2]) / 2.0 cx = (bounds[0] + bounds[2]) / 2.0
cy = (bounds[1] + bounds[3]) / 2.0 cy = (bounds[1] + bounds[3]) / 2.0
sample_offsets = [(0.0, 0.0)] sample_points = [(cx, cy)]
if bounds[2] - bounds[0] > 0.1: if bounds[2] - bounds[0] > 0.1:
sample_offsets.append((0.25 * (bounds[2] - bounds[0]), 0.0)) sample_points.append((cx + 0.25 * (bounds[2] - bounds[0]), cy))
sample_offsets.append((-0.25 * (bounds[2] - bounds[0]), 0.0)) sample_points.append((cx - 0.25 * (bounds[2] - bounds[0]), cy))
if bounds[3] - bounds[1] > 0.1: if bounds[3] - bounds[1] > 0.1:
sample_offsets.append((0.0, 0.25 * (bounds[3] - bounds[1]))) sample_points.append((cx, cy + 0.25 * (bounds[3] - bounds[1])))
sample_offsets.append((0.0, -0.25 * (bounds[3] - bounds[1]))) sample_points.append((cx, cy - 0.25 * (bounds[3] - bounds[1])))
hits = _nearest_ray_hits(tree, [(cx + dx, cy + dy, origin_z) for dx, dy in sample_offsets], ray_dir) # Sample from footprint corners, offset toward centroid so the ray origin
# sits inside the space (not on a bounding wall). This catches elements
# (e.g. sloped roofs) that only cover a corner of the space.
for coords in space_polygon.exterior.coords[:-1]:
vx, vy = coords[0], coords[1]
sample_points.append((vx + 0.05 * (cx - vx), vy + 0.05 * (cy - vy)))
hits = _nearest_ray_hits(tree, [(x, y, origin_z) for x, y in sample_points], ray_dir)
if not hits: if not hits:
return "EXTRUDE_CLIP", [] # open top / void below: no bounding planes return "EXTRUDE_CLIP", [] # open top / void below: no bounding planes