mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-09 17:31:45 +00:00
Wire space volume strategy detection
Dispatch on EXTRUDE_CLIP vs B-rep when building space volumes, and fix fixture placement and visibility bugs in the spatial tests. Generated with the assistance of an AI coding tool.
This commit is contained in:
@@ -874,7 +874,9 @@ class Spatial(bonsai.core.tool.Spatial):
|
||||
return ifcopenshell.util.space.get_boundary_lines(tool.Ifc.get(), cache["shapes"], cut_z)
|
||||
|
||||
@classmethod
|
||||
def get_space_polygon_from_context_visible_objects(cls, x: float, y: float, container: Optional[ifcopenshell.entity_instance] = None) -> tuple[
|
||||
def get_space_polygon_from_context_visible_objects(
|
||||
cls, x: float, y: float, container: Optional[ifcopenshell.entity_instance] = None
|
||||
) -> tuple[
|
||||
Union[shapely.Polygon, Literal["NO POLYGONS FOUND", "NO POLYGON FOR POINT"]],
|
||||
list[ifcopenshell.entity_instance],
|
||||
]:
|
||||
@@ -921,6 +923,63 @@ class Spatial(bonsai.core.tool.Spatial):
|
||||
tool.Ifc.get(), cache["shapes"], space_polygon, base_z, bounding_walls
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_space_volume_strategy(
|
||||
cls,
|
||||
space_polygon: shapely.Polygon,
|
||||
base_z: float,
|
||||
bounding_walls: list[ifcopenshell.entity_instance],
|
||||
container: Optional[ifcopenshell.entity_instance] = None,
|
||||
) -> tuple[str, Optional[list], Optional[list]]:
|
||||
"""Decide how to build the space volume (clipped extrusion or B-rep).
|
||||
|
||||
Rays are cast from the RL cut elevation (``container_z + props.rl3``), the
|
||||
same level at which the space footprint polygon was found.
|
||||
"""
|
||||
ifc_file = tool.Ifc.get()
|
||||
cache = cls.get_or_build_geom_cache()
|
||||
start_z = None
|
||||
if container is None:
|
||||
container = tool.Root.get_default_container()
|
||||
if container is not None:
|
||||
container_obj = tool.Ifc.get_object(container)
|
||||
props = tool.Model.get_model_props()
|
||||
start_z = container_obj.matrix_world.translation.z + props.rl3
|
||||
tree = ifcopenshell.geom.tree(ifc_file)
|
||||
settings = ifcopenshell.geom.settings()
|
||||
settings.set("disable-opening-subtractions", True)
|
||||
settings.set("use-world-coords", True)
|
||||
tree.add_file(ifc_file, settings)
|
||||
return ifcopenshell.util.space.detect_space_volume_strategy(
|
||||
ifc_file, cache["shapes"], tree, space_polygon, base_z, bounding_walls, start_z=start_z
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def set_brep_representation_from_mesh(
|
||||
cls,
|
||||
obj: bpy.types.Object,
|
||||
element: ifcopenshell.entity_instance,
|
||||
item: ifcopenshell.entity_instance,
|
||||
) -> None:
|
||||
"""Assign a representation item (clipped solid or B-rep) to the element."""
|
||||
old_body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
|
||||
if old_body:
|
||||
context = old_body.ContextOfItems
|
||||
ifcopenshell.api.geometry.unassign_representation(tool.Ifc.get(), product=element, representation=old_body)
|
||||
ifcopenshell.api.geometry.remove_representation(tool.Ifc.get(), representation=old_body)
|
||||
else:
|
||||
context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
|
||||
|
||||
builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get())
|
||||
new_body = builder.get_representation(context, item)
|
||||
ifcopenshell.api.geometry.assign_representation(tool.Ifc.get(), product=element, representation=new_body)
|
||||
bonsai.core.geometry.switch_representation(
|
||||
tool.Ifc,
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=new_body,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def debug_shape(cls, foo: shapely.Polygon) -> None:
|
||||
coords = [(p[0], p[1], 0) for p in foo.exterior.coords]
|
||||
@@ -1257,13 +1316,45 @@ class Spatial(bonsai.core.tool.Spatial):
|
||||
poly: Polygon,
|
||||
h: float,
|
||||
polygon_is_si: bool = True,
|
||||
bounding_walls: Optional[list[ifcopenshell.entity_instance]] = None,
|
||||
container: Optional[ifcopenshell.entity_instance] = None,
|
||||
) -> None:
|
||||
"""Create or replace the IFC body representation of a space from a polygon.
|
||||
|
||||
:param h: The height in SI (meters).
|
||||
"""
|
||||
# Remove collinear points introduced by the mesh bisection so the
|
||||
# footprint polygon has a minimal vertex count.
|
||||
poly = poly.simplify(0, preserve_topology=True)
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
cls.set_extrusion_representation_from_polygon(obj, element, poly, h / unit_scale, polygon_is_si)
|
||||
ifc_file = tool.Ifc.get()
|
||||
x, y, z = obj.matrix_world.translation
|
||||
base_z = z / unit_scale
|
||||
if cls.get_spatial_props().force_space_height:
|
||||
cls.set_extrusion_representation_from_polygon(obj, element, poly, h / unit_scale, polygon_is_si)
|
||||
return
|
||||
if bounding_walls is None:
|
||||
bounding_walls = []
|
||||
if container is None:
|
||||
container = ifcopenshell.util.element.get_container(element)
|
||||
if container is not None:
|
||||
for wall in ifc_file.by_type("IfcWall"):
|
||||
if wall in ifcopenshell.util.element.get_decomposition(container):
|
||||
bounding_walls.append(wall)
|
||||
strategy, top_planes, bottom_planes = cls.get_space_volume_strategy(poly, base_z, bounding_walls, container)
|
||||
if strategy == "EXTRUDE_CLIP" and top_planes:
|
||||
item = ifcopenshell.util.space.build_extruded_clipped_space(
|
||||
ifc_file, poly, base_z, top_planes, bottom_planes or []
|
||||
)
|
||||
cls.set_brep_representation_from_mesh(obj, element, item)
|
||||
else:
|
||||
item = ifcopenshell.util.space.build_brep_space(
|
||||
ifc_file, element, cls.get_or_build_geom_cache()["shapes"], poly, base_z
|
||||
)
|
||||
if item is None:
|
||||
cls.set_extrusion_representation_from_polygon(obj, element, poly, h / unit_scale, polygon_is_si)
|
||||
else:
|
||||
cls.set_brep_representation_from_mesh(obj, element, item)
|
||||
|
||||
@classmethod
|
||||
def set_obj_origin_to_cursor_position_and_zero_elevation(cls, obj: bpy.types.Object) -> None:
|
||||
|
||||
@@ -26,7 +26,8 @@ import ifcopenshell.api.root
|
||||
import ifcopenshell.api.spatial
|
||||
import ifcopenshell.util.representation
|
||||
import numpy as np
|
||||
from mathutils import Matrix
|
||||
import shapely
|
||||
from mathutils import Matrix, Vector
|
||||
|
||||
import bonsai.core.tool
|
||||
import bonsai.tool as tool
|
||||
@@ -270,7 +271,7 @@ class _BlockHelper:
|
||||
wall = ifcopenshell.api.root.create_entity(ifc, ifc_class="IfcWall")
|
||||
placement_2d = ifc.createIfcAxis2Placement2D(ifc.createIfcCartesianPoint([0.0, 0.0]))
|
||||
profile = ifc.createIfcRectangleProfileDef("AREA", None, placement_2d, 10.0, 10.0)
|
||||
placement_3d = ifc.createIfcAxis2Placement3D(ifc.createIfcCartesianPoint([-5.0, -5.0, 0.0]))
|
||||
placement_3d = ifc.createIfcAxis2Placement3D(ifc.createIfcCartesianPoint([0.0, 0.0, 0.0]))
|
||||
extrusion = ifc.createIfcExtrudedAreaSolid(
|
||||
profile, placement_3d, ifc.createIfcDirection([0.0, 0.0, 1.0]), height
|
||||
)
|
||||
@@ -285,7 +286,7 @@ class _BlockHelper:
|
||||
slab = ifcopenshell.api.root.create_entity(ifc, ifc_class="IfcSlab")
|
||||
placement_2d = ifc.createIfcAxis2Placement2D(ifc.createIfcCartesianPoint([0.0, 0.0]))
|
||||
profile = ifc.createIfcRectangleProfileDef("AREA", None, placement_2d, 12.0, 12.0)
|
||||
placement_3d = ifc.createIfcAxis2Placement3D(ifc.createIfcCartesianPoint([-6.0, -6.0, z]))
|
||||
placement_3d = ifc.createIfcAxis2Placement3D(ifc.createIfcCartesianPoint([0.0, 0.0, z]))
|
||||
extrusion = ifc.createIfcExtrudedAreaSolid(profile, placement_3d, ifc.createIfcDirection([0.0, 0.0, 1.0]), 1.0)
|
||||
shape_rep = ifc.createIfcShapeRepresentation(ctx, "Body", "SweptSolid", [extrusion])
|
||||
slab.Representation = ifc.createIfcProductDefinitionShape(None, None, [shape_rep])
|
||||
@@ -384,6 +385,7 @@ class TestGenerateSpace(NewFile):
|
||||
spatial_props = tool.Spatial.get_spatial_props()
|
||||
spatial_props.space_height = 6
|
||||
bpy.context.view_layer.objects.active = space
|
||||
space.hide_viewport = False
|
||||
space.select_set(True)
|
||||
|
||||
bpy.ops.bim.apply_space_height_to_selection()
|
||||
@@ -426,3 +428,78 @@ class TestGenerateSpace(NewFile):
|
||||
bpy.ops.bim.generate_space()
|
||||
new_height = space.dimensions.z
|
||||
assert new_height != original_height or new_height > 0
|
||||
|
||||
def test_regenerate_space_from_centered_cube_representation(self):
|
||||
bpy.ops.bim.create_project()
|
||||
ifc = tool.Ifc.get()
|
||||
_BlockHelper.create_wall(ifc, height=10.0)
|
||||
scene = bpy.context.scene
|
||||
scene.cursor.location = (0, 0, 0)
|
||||
|
||||
# Create a space with a unit cube PolygonalFaceSet centered at local origin.
|
||||
ctx = ifcopenshell.util.representation.get_context(ifc, "Model", "Body", "MODEL_VIEW")
|
||||
points = ifc.createIfcCartesianPointList3D(
|
||||
[
|
||||
(-0.5, -0.5, -0.5),
|
||||
(-0.5, -0.5, 0.5),
|
||||
(-0.5, 0.5, -0.5),
|
||||
(-0.5, 0.5, 0.5),
|
||||
(0.5, -0.5, -0.5),
|
||||
(0.5, -0.5, 0.5),
|
||||
(0.5, 0.5, -0.5),
|
||||
(0.5, 0.5, 0.5),
|
||||
]
|
||||
)
|
||||
faces = [
|
||||
ifc.createIfcIndexedPolygonalFace([1, 2, 4, 3]),
|
||||
ifc.createIfcIndexedPolygonalFace([3, 4, 8, 7]),
|
||||
ifc.createIfcIndexedPolygonalFace([7, 8, 6, 5]),
|
||||
ifc.createIfcIndexedPolygonalFace([5, 6, 2, 1]),
|
||||
ifc.createIfcIndexedPolygonalFace([3, 7, 5, 1]),
|
||||
ifc.createIfcIndexedPolygonalFace([8, 4, 2, 6]),
|
||||
]
|
||||
face_set = ifc.createIfcPolygonalFaceSet(points, True, faces)
|
||||
shape_rep = ifc.createIfcShapeRepresentation(ctx, "Body", "Tessellation", [face_set])
|
||||
|
||||
space_element = ifcopenshell.api.root.create_entity(ifc, ifc_class="IfcSpace")
|
||||
space_element.Representation = ifc.createIfcProductDefinitionShape(None, None, [shape_rep])
|
||||
|
||||
bpy.ops.mesh.primitive_cube_add(size=1, location=(0, 0, 5))
|
||||
obj = bpy.data.objects["Cube"]
|
||||
scene.collection.objects.link(obj)
|
||||
tool.Ifc.link(space_element, obj)
|
||||
bpy.context.view_layer.update()
|
||||
obj.name = "MySpace"
|
||||
|
||||
# Check the cube's world bottom Z before regeneration.
|
||||
bottom_z = (obj.matrix_world @ Vector(obj.bound_box[0])).z
|
||||
assert np.isclose(bottom_z, 4.5), f"Expected bottom_z=4.5, got {bottom_z}"
|
||||
|
||||
# Regenerate the space.
|
||||
bpy.context.view_layer.objects.active = obj
|
||||
obj.select_set(True)
|
||||
bpy.ops.bim.generate_space()
|
||||
|
||||
mesh = obj.data
|
||||
assert isinstance(mesh, bpy.types.Mesh)
|
||||
verts = [v.co.z for v in mesh.vertices]
|
||||
min_z = min(verts)
|
||||
max_z = max(verts)
|
||||
assert min_z >= 0, f"Expected extrusion to start at local z>=0, got min_z={min_z}"
|
||||
assert max_z > 0, f"Expected extrusion to have positive height, got max_z={max_z}"
|
||||
assert np.isclose(obj.location.z, 4.5, atol=0.01), f"Expected location.z=4.5, got {obj.location.z}"
|
||||
|
||||
|
||||
class TestSpaceVolumeStrategy(NewFile):
|
||||
def test_vertical_box_returns_extrude_clip(self):
|
||||
bpy.ops.bim.create_project()
|
||||
ifc = tool.Ifc.get()
|
||||
_BlockHelper.create_wall(ifc, height=10.0)
|
||||
_BlockHelper.create_slab(ifc, z=4.0)
|
||||
space_polygon = shapely.box(-5, -5, 5, 5)
|
||||
strategy, top, bottom = subject.get_space_volume_strategy(space_polygon, 0.0, [ifc.by_type("IfcWall")[0]])
|
||||
assert strategy == "EXTRUDE_CLIP"
|
||||
assert len(top) == 1
|
||||
assert len(bottom) == 0
|
||||
|
||||
class TestGenerateSpaceSlopedRoof(NewFile):
|
||||
|
||||
Reference in New Issue
Block a user