From a7da317c3086b4898fe34049774301a2d66ba362 Mon Sep 17 00:00:00 2001 From: CyrilWaechter Date: Mon, 3 Aug 2026 14:51:23 +0200 Subject: [PATCH] Fix clipped space top reaching sloped planes The extrusion height was capped at the top/bottom plane anchor z (the mean of the ray-cast hits, near the footprint centre), so a sloped ceiling's high side stopped short of the plane (e.g. 5.5 m instead of 6.88 m for the shed roof test). Extend the extrusion to the plane's z at every footprint vertex before clipping, falling back to the base z as before. Also correct the mirrored profile-to-world mapping comment in the shed roof test helper (the ridge is at world y=-5, not y=+5). Generated with the assistance of an AI coding tool. --- src/bonsai/test/tool/test_spatial.py | 9 ++++++--- .../ifcopenshell/util/space.py | 18 +++++++++++++++++- .../test/util/test_space.py | 13 +++++++++++++ 3 files changed, 36 insertions(+), 4 deletions(-) diff --git a/src/bonsai/test/tool/test_spatial.py b/src/bonsai/test/tool/test_spatial.py index d0f396c300..a540e0fd8b 100644 --- a/src/bonsai/test/tool/test_spatial.py +++ b/src/bonsai/test/tool/test_spatial.py @@ -511,9 +511,12 @@ class TestGenerateSpaceSlopedRoof(NewFile): Triangular prism: vertical profile (in the y-z plane) extruded along +x. Profile points (u, v) with placement loc=(-5, 0, z), axis=(1,0,0), - ref=(0,0,1): (0,-5) -> world (-5,-5,z) eave bottom - (rise,-5) -> world (-5,-5,z+rise) eave top - (rise,5) -> world (-5,5,z+rise) ridge + ref=(0,0,1). The local frame maps u to world +z (u=0 -> z, u=rise -> + z+rise) and v to world -y (v=-5 -> y=+5, v=+5 -> y=-5): + (0,-5) -> world (-5, +5, z) eave (low) at north + (rise,-5) -> world (-5, +5, z+rise) vertical edge + (rise,5) -> world (-5, -5, z+rise) ridge at south + The underside is the sloped face from (y=+5, z) to (y=-5, z+rise). ExtrudedDirection (0,0,1) is local, mapping to world +x; depth 10 spans x in [-5, 5]. """ diff --git a/src/ifcopenshell-python/ifcopenshell/util/space.py b/src/ifcopenshell-python/ifcopenshell/util/space.py index 906aad08d0..86c9c42eb4 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/space.py +++ b/src/ifcopenshell-python/ifcopenshell/util/space.py @@ -410,6 +410,13 @@ def detect_space_volume_strategy( return "EXTRUDE_CLIP", top_planes, bottom_planes +def _footprint_coords(space_polygon: shapely.Polygon): + """Yield (x, y) boundary coordinates of a footprint polygon (exterior then holes).""" + for coords in [space_polygon.exterior.coords, *[ring.coords for ring in space_polygon.interiors]]: + for point in coords: + yield point[0], point[1] + + def build_extruded_clipped_space( ifc_file: ifcopenshell.file, space_polygon: shapely.Polygon, @@ -447,8 +454,17 @@ def build_extruded_clipped_space( profile = builder.profile(outer_curve, inner_curves=inner_curves) all_z = [base_z] - for point, _ in top_planes + bottom_planes: + for point, normal in top_planes + bottom_planes: all_z.append(float(point[2])) + for x, y in _footprint_coords(space_polygon): + # A sloped plane's height varies across the footprint. The anchor + # point is near the centre, so also cover the plane at the polygon + # vertices, otherwise the high side of a sloped ceiling is capped + # below the plane it should reach. + if abs(normal[2]) < 1e-6: + continue + plane_z = (float(np.dot(normal, point)) - float(normal[0]) * x - float(normal[1]) * y) / float(normal[2]) + all_z.append(plane_z) min_z = min(all_z) max_z = max(all_z) height = max_z - min_z diff --git a/src/ifcopenshell-python/test/util/test_space.py b/src/ifcopenshell-python/test/util/test_space.py index b556b4756c..84152e2d93 100644 --- a/src/ifcopenshell-python/test/util/test_space.py +++ b/src/ifcopenshell-python/test/util/test_space.py @@ -297,6 +297,19 @@ class TestBuildExtrudedClippedSpace(test.bootstrap.IFC4): assert item.FirstOperand.is_a("IfcBooleanClippingResult") assert item.FirstOperand.FirstOperand.is_a("IfcExtrudedAreaSolid") + def test_sloped_top_plane_reaches_footprint_max(self): + # A sloped ceiling's high side must reach the plane at the footprint + # edge (z=7.0 at y=-5), not be capped at the plane anchor z=5.5. + # 3D coords guard against the polygon carrying Z from the bisection. + space_polygon = shapely.Polygon([(-5, -5, 0), (5, -5, 0), (5, 5, 0), (-5, 5, 0)]) + top_plane = (np.array([0.0, 0.0, 5.5]), np.array([0.0, 0.287, 0.958])) + bottom_plane = (np.array([0.0, 0.0, 0.0]), np.array([0.0, 0.0, -1.0])) + item = subject.build_extruded_clipped_space(self.file, space_polygon, 0.0, [top_plane], [bottom_plane]) + shape = ifcopenshell.geom.create_shape(ifcopenshell.geom.settings(), item) + verts = ifcopenshell.util.shape.get_vertices(shape) + assert verts[:, 2].min() == pytest.approx(0.0, abs=0.1) + assert verts[:, 2].max() == pytest.approx(7.0, abs=0.1) + class TestBuildBrepSpace(test.bootstrap.IFC4): def test_sloped_wall_brep_has_faces(self):