diff --git a/src/bonsai/test/tool/test_spatial.py b/src/bonsai/test/tool/test_spatial.py index 534eb76592..be31fa7150 100644 --- a/src/bonsai/test/tool/test_spatial.py +++ b/src/bonsai/test/tool/test_spatial.py @@ -685,10 +685,16 @@ class TestRegenerateSpaceFromRealIfc2x3(NewFile): ifc = self.load_house_with_garage() (original_bounds, original_origin), (regen_bounds, regen_origin) = self._regenerate_space(ifc, 2363) assert (regen_origin - original_origin).length < 0.02 - # X, Y, Z-min stable; Z-max may differ because the regenerated space - # correctly detects the roof and clips to a different top elevation. - for j in (0, 1, 4): + # X and Y stable; Z may differ because the regenerated space detects the + # sloped roof and clips the extrusion. + for j in (0, 1, 2, 3, 4): 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): ifc = self.load_house_with_garage() diff --git a/src/ifcopenshell-python/ifcopenshell/util/space.py b/src/ifcopenshell-python/ifcopenshell/util/space.py index d918628f10..31aa34b4c6 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/space.py +++ b/src/ifcopenshell-python/ifcopenshell/util/space.py @@ -302,15 +302,22 @@ def get_vertical_bounding_planes( bounds = space_polygon.bounds cx = (bounds[0] + bounds[2]) / 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: - sample_offsets.append((0.25 * (bounds[2] - bounds[0]), 0.0)) - sample_offsets.append((-0.25 * (bounds[2] - bounds[0]), 0.0)) + sample_points.append((cx + 0.25 * (bounds[2] - bounds[0]), cy)) + sample_points.append((cx - 0.25 * (bounds[2] - bounds[0]), cy)) if bounds[3] - bounds[1] > 0.1: - sample_offsets.append((0.0, 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]))) + 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: return "EXTRUDE_CLIP", [] # open top / void below: no bounding planes