mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-09 09:21:46 +00:00
Add space volume strategy detection
Detect whether a space can be built as a clipped extrusion or needs a B-rep fallback, based on wall face orientation and top/bottom bounding planes. Generated with the assistance of an AI coding tool.
This commit is contained in:
@@ -345,3 +345,60 @@ def get_vertical_bounding_planes(
|
||||
planes.append((anchor, mean_normal))
|
||||
|
||||
return "EXTRUDE_CLIP", planes
|
||||
|
||||
|
||||
def detect_space_volume_strategy(
|
||||
ifc_file: ifcopenshell.file,
|
||||
shapes: dict,
|
||||
tree: ifcopenshell.geom.tree,
|
||||
space_polygon: shapely.Polygon,
|
||||
base_z: float,
|
||||
bounding_walls: list[ifcopenshell.entity_instance],
|
||||
start_z: Optional[float] = None,
|
||||
) -> tuple[str, Optional[list], Optional[list]]:
|
||||
"""Decide whether a space can be represented as a clipped extrusion.
|
||||
|
||||
A space is "EXTRUDE_CLIP" when all bounding walls have vertical side faces
|
||||
and the detected top/bottom bounding planes are few and piecewise-planar
|
||||
(0-2 top planes, 0-1 bottom plane). Otherwise it is "BREP".
|
||||
|
||||
:param ifc_file: The IFC file.
|
||||
:param shapes: Cached element shapes.
|
||||
:param tree: Geometry tree with bounding elements.
|
||||
:param space_polygon: Space footprint in world XY.
|
||||
:param base_z: Base elevation in SI.
|
||||
:param bounding_walls: List of wall elements bounding the space.
|
||||
:param start_z: Ray-cast origin elevation (RL cut level) in SI.
|
||||
:return: ("EXTRUDE_CLIP", top_planes, bottom_planes) or ("BREP", None, None).
|
||||
"""
|
||||
tol = 0.02
|
||||
for wall in bounding_walls:
|
||||
shape_data = shapes.get(wall.id())
|
||||
if not shape_data:
|
||||
continue
|
||||
verts = shape_data["verts"]
|
||||
faces = shape_data["faces"]
|
||||
if len(verts) == 0 or len(faces) == 0:
|
||||
continue
|
||||
v1 = verts[faces[:, 1]] - verts[faces[:, 0]]
|
||||
v2 = verts[faces[:, 2]] - verts[faces[:, 0]]
|
||||
normals = np.cross(v1, v2)
|
||||
norms = np.linalg.norm(normals, axis=1)
|
||||
normals = normals[norms > 1e-8]
|
||||
if len(normals) == 0:
|
||||
continue
|
||||
normals = normals / np.linalg.norm(normals, axis=1)[:, np.newaxis]
|
||||
side_mask = np.abs(normals[:, 2]) < 0.5
|
||||
if np.any(side_mask) and np.mean(np.abs(normals[side_mask, 2])) > tol:
|
||||
return "BREP", None, None
|
||||
|
||||
_, top_planes = get_vertical_bounding_planes(ifc_file, shapes, tree, space_polygon, base_z, "UP", start_z=start_z)
|
||||
|
||||
_, bottom_planes = get_vertical_bounding_planes(
|
||||
ifc_file, shapes, tree, space_polygon, base_z, "DOWN", start_z=start_z
|
||||
)
|
||||
|
||||
if len(top_planes) > 2 or len(bottom_planes) > 1:
|
||||
return "BREP", None, None
|
||||
|
||||
return "EXTRUDE_CLIP", top_planes, bottom_planes
|
||||
|
||||
@@ -48,7 +48,7 @@ def _build_shapes_dict(ifc_file, elements):
|
||||
return shapes
|
||||
|
||||
|
||||
def _add_extruded_body(ifc_file, element, coords_2d, depth, z_offset=0.0):
|
||||
def _add_extruded_body(ifc_file, element, coords_2d, depth, z_offset=0.0, direction=(0.0, 0.0, 1.0)):
|
||||
"""Add a body representation (extruded polyline) to an element."""
|
||||
if not ifc_file.by_type("IfcProject"):
|
||||
ifcopenshell.api.root.create_entity(ifc_file, ifc_class="IfcProject")
|
||||
@@ -76,7 +76,7 @@ def _add_extruded_body(ifc_file, element, coords_2d, depth, z_offset=0.0):
|
||||
ifc_file.createIfcDirection((0.0, 0.0, 1.0)),
|
||||
ifc_file.createIfcDirection((1.0, 0.0, 0.0)),
|
||||
)
|
||||
direction = ifc_file.createIfcDirection((0.0, 0.0, 1.0))
|
||||
direction = ifc_file.createIfcDirection(direction)
|
||||
solid = ifc_file.createIfcExtrudedAreaSolid(profile, placement, direction, depth)
|
||||
rep = ifc_file.create_entity(
|
||||
"IfcShapeRepresentation",
|
||||
@@ -221,3 +221,47 @@ class TestGetVerticalBoundingPlanes(test.bootstrap.IFC4):
|
||||
assert strategy == "EXTRUDE_CLIP"
|
||||
assert len(planes) == 1
|
||||
assert np.allclose(planes[0][0], [0.0, 0.0, 3.0], atol=0.1)
|
||||
|
||||
|
||||
class TestDetectSpaceVolumeStrategy(test.bootstrap.IFC4):
|
||||
def test_vertical_walls_flat_slab_returns_extrude_clip(self):
|
||||
wall = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall")
|
||||
slab = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcSlab")
|
||||
_add_extruded_body(self.file, wall, [[-5, -5], [5, -5], [5, 5], [-5, 5]], 3.0)
|
||||
_add_extruded_body(self.file, slab, [[-5, -5], [5, -5], [5, 5], [-5, 5]], 0.3, z_offset=3.0)
|
||||
shapes = _build_shapes_dict(self.file, [wall, slab])
|
||||
tree = ifcopenshell.geom.tree(self.file)
|
||||
tree.add_file(self.file, ifcopenshell.geom.settings())
|
||||
strategy, top, bottom = subject.detect_space_volume_strategy(
|
||||
self.file, shapes, tree, shapely.box(-5, -5, 5, 5), 0.0, [wall]
|
||||
)
|
||||
assert strategy == "EXTRUDE_CLIP"
|
||||
assert len(top) == 1
|
||||
assert len(bottom) == 0
|
||||
|
||||
def test_vertical_walls_flat_slab_returns_extrude_clip_from_rl_origin(self):
|
||||
# Same as above but rays cast from an RL cut elevation (z=1.0).
|
||||
wall = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall")
|
||||
slab = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcSlab")
|
||||
_add_extruded_body(self.file, wall, [[-5, -5], [5, -5], [5, 5], [-5, 5]], 3.0)
|
||||
_add_extruded_body(self.file, slab, [[-5, -5], [5, -5], [5, 5], [-5, 5]], 0.3, z_offset=3.0)
|
||||
shapes = _build_shapes_dict(self.file, [wall, slab])
|
||||
tree = ifcopenshell.geom.tree(self.file)
|
||||
tree.add_file(self.file, ifcopenshell.geom.settings())
|
||||
strategy, top, bottom = subject.detect_space_volume_strategy(
|
||||
self.file, shapes, tree, shapely.box(-5, -5, 5, 5), 0.0, [wall], start_z=1.0
|
||||
)
|
||||
assert strategy == "EXTRUDE_CLIP"
|
||||
assert len(top) == 1
|
||||
assert len(bottom) == 0
|
||||
|
||||
def test_sloped_wall_returns_brep(self):
|
||||
wall = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall")
|
||||
_add_extruded_body(self.file, wall, [[-5, -5], [5, -5], [5, 5], [-5, 5]], 6.0, direction=(0.0, 0.5, 1.0))
|
||||
shapes = _build_shapes_dict(self.file, [wall])
|
||||
tree = ifcopenshell.geom.tree(self.file)
|
||||
tree.add_file(self.file, ifcopenshell.geom.settings())
|
||||
strategy, _, _ = subject.detect_space_volume_strategy(
|
||||
self.file, shapes, tree, shapely.box(-4, -4, 4, 4), 0.0, [wall]
|
||||
)
|
||||
assert strategy == "BREP"
|
||||
|
||||
Reference in New Issue
Block a user