Add clipped extrusion space builder

Build IfcExtrudedAreaSolid clipped by top/bottom half-space planes.

Generated with the assistance of an AI coding tool.
This commit is contained in:
CyrilWaechter
2026-08-03 11:54:26 +02:00
parent 29b9d8807e
commit 5651cd6494
2 changed files with 78 additions and 0 deletions
@@ -29,8 +29,11 @@ from __future__ import annotations
from typing import Literal, Optional, Union from typing import Literal, Optional, Union
import ifcopenshell import ifcopenshell
import ifcopenshell.api
import ifcopenshell.util.element import ifcopenshell.util.element
import ifcopenshell.util.shape import ifcopenshell.util.shape
import ifcopenshell.util.shape_builder
import ifcopenshell.util.unit
import numpy as np import numpy as np
import shapely import shapely
@@ -402,3 +405,65 @@ def detect_space_volume_strategy(
return "BREP", None, None return "BREP", None, None
return "EXTRUDE_CLIP", top_planes, bottom_planes return "EXTRUDE_CLIP", top_planes, bottom_planes
def build_extruded_clipped_space(
ifc_file: ifcopenshell.file,
space_polygon: shapely.Polygon,
base_z: float,
top_planes: list[tuple[np.ndarray, np.ndarray]],
bottom_planes: list[tuple[np.ndarray, np.ndarray]],
) -> ifcopenshell.entity_instance:
"""Build an IfcExtrudedAreaSolid clipped to top/bottom planes.
:param ifc_file: The IFC file.
:param space_polygon: Footprint polygon in world XY.
:param base_z: Base elevation in SI.
:param top_planes: List of (point, normal) tuples for top clipping planes.
:param bottom_planes: List of (point, normal) tuples for bottom clipping planes.
:return: IfcBooleanClippingResult chain.
"""
builder = ifcopenshell.util.shape_builder.ShapeBuilder(ifc_file)
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
centroid = np.array([space_polygon.centroid.x, space_polygon.centroid.y])
exterior = [
(float(p[0] - centroid[0]) / unit_scale, float(p[1] - centroid[1]) / unit_scale)
for p in space_polygon.exterior.coords[:-1]
]
inner_curves = []
for interior in space_polygon.interiors:
inner = [
(float(p[0] - centroid[0]) / unit_scale, float(p[1] - centroid[1]) / unit_scale)
for p in interior.coords[:-1]
]
inner_curve = builder.polyline(inner, closed=True)
inner_curves.append(inner_curve)
outer_curve = builder.polyline(exterior, closed=True)
profile = builder.profile(outer_curve, inner_curves=inner_curves)
all_z = [base_z]
for point, _ in top_planes + bottom_planes:
all_z.append(float(point[2]))
min_z = min(all_z)
max_z = max(all_z)
height = max_z - min_z
extrusion = builder.extrude(
profile,
magnitude=height / unit_scale,
position=[(centroid[0] / unit_scale), (centroid[1] / unit_scale), min_z / unit_scale],
)
result = extrusion
for point, normal in top_planes + bottom_planes:
# clip_solid takes location in SI; it converts to project units internally.
result = ifcopenshell.api.geometry.clip_solid(
ifc_file,
item=result,
location=[float(point[i]) for i in range(3)],
normal=[float(normal[i]) for i in range(3)],
)
return result
@@ -265,3 +265,16 @@ class TestDetectSpaceVolumeStrategy(test.bootstrap.IFC4):
self.file, shapes, tree, shapely.box(-4, -4, 4, 4), 0.0, [wall] self.file, shapes, tree, shapely.box(-4, -4, 4, 4), 0.0, [wall]
) )
assert strategy == "BREP" assert strategy == "BREP"
class TestBuildExtrudedClippedSpace(test.bootstrap.IFC4):
def test_shed_roof_clips_to_sloped_plane(self):
space_polygon = shapely.box(-5, -5, 5, 5)
top_plane = (np.array([0.0, 0.0, 4.0]), np.array([0.0, 0.0, 1.0]))
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])
assert item.is_a("IfcBooleanClippingResult")
assert item.SecondOperand.is_a("IfcHalfSpaceSolid")
# Chain: bottom clip -> top clip -> IfcExtrudedAreaSolid.
assert item.FirstOperand.is_a("IfcBooleanClippingResult")
assert item.FirstOperand.FirstOperand.is_a("IfcExtrudedAreaSolid")