mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-19 14:41:25 +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))
|
planes.append((anchor, mean_normal))
|
||||||
|
|
||||||
return "EXTRUDE_CLIP", planes
|
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
|
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."""
|
"""Add a body representation (extruded polyline) to an element."""
|
||||||
if not ifc_file.by_type("IfcProject"):
|
if not ifc_file.by_type("IfcProject"):
|
||||||
ifcopenshell.api.root.create_entity(ifc_file, ifc_class="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((0.0, 0.0, 1.0)),
|
||||||
ifc_file.createIfcDirection((1.0, 0.0, 0.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)
|
solid = ifc_file.createIfcExtrudedAreaSolid(profile, placement, direction, depth)
|
||||||
rep = ifc_file.create_entity(
|
rep = ifc_file.create_entity(
|
||||||
"IfcShapeRepresentation",
|
"IfcShapeRepresentation",
|
||||||
@@ -221,3 +221,47 @@ class TestGetVerticalBoundingPlanes(test.bootstrap.IFC4):
|
|||||||
assert strategy == "EXTRUDE_CLIP"
|
assert strategy == "EXTRUDE_CLIP"
|
||||||
assert len(planes) == 1
|
assert len(planes) == 1
|
||||||
assert np.allclose(planes[0][0], [0.0, 0.0, 3.0], atol=0.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