mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-05 23:41:44 +00:00
Fix space generation location and IFC2X3 B-rep
Two fixes for space generation: 1. IFC2X3 schema: build_brep_space now falls back to plain IfcRelSpaceBoundary because IfcRelSpaceBoundary1stLevel does not exist in IFC2X3. 2. Geometry location: set_space_representation_from_polygon now aligns the IFC ObjectPlacement with the Blender object, converts base_z/planes and the footprint polygon to the object's local coordinate system before building, and fixes the base_z unit scale. The centred-cube regeneration test was updated to check world bounds because the mesh is now placed relative to the object placement. Generated with the assistance of an AI coding tool.
This commit is contained in:
@@ -42,6 +42,7 @@ import ifcopenshell.util.type
|
||||
import ifcopenshell.util.unit
|
||||
import numpy as np
|
||||
import shapely
|
||||
import shapely.affinity
|
||||
import shapely.ops
|
||||
from mathutils import Matrix, Vector
|
||||
from natsort import natsorted
|
||||
@@ -1329,7 +1330,22 @@ class Spatial(bonsai.core.tool.Spatial):
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
ifc_file = tool.Ifc.get()
|
||||
x, y, z = obj.matrix_world.translation
|
||||
base_z = z / unit_scale
|
||||
origin = obj.matrix_world.translation # Blender SI
|
||||
# The space builders expect base_z and polygon in SI (world) units.
|
||||
base_z = z
|
||||
poly_si = poly if polygon_is_si else shapely.affinity.scale(poly, unit_scale, unit_scale, origin=(0, 0))
|
||||
|
||||
# Ensure the IFC entity has an ObjectPlacement matching the Blender object,
|
||||
# so the generated representation is in the correct local coordinate system.
|
||||
bpy.context.view_layer.update()
|
||||
matrix = np.array(obj.matrix_world)
|
||||
ifcopenshell.api.geometry.edit_object_placement(
|
||||
ifc_file,
|
||||
product=element,
|
||||
matrix=matrix,
|
||||
is_si=True,
|
||||
)
|
||||
|
||||
if cls.get_spatial_props().force_space_height:
|
||||
cls.set_extrusion_representation_from_polygon(obj, element, poly, h / unit_scale, polygon_is_si)
|
||||
return
|
||||
@@ -1341,15 +1357,37 @@ class Spatial(bonsai.core.tool.Spatial):
|
||||
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)
|
||||
|
||||
# Detect planes in world SI (same coordinate system as the geom cache).
|
||||
strategy, top_planes, bottom_planes = cls.get_space_volume_strategy(poly_si, base_z, bounding_walls, container)
|
||||
|
||||
# Build the geometry in the space's local coordinate system so the IFC
|
||||
# representation is relative to the object's ObjectPlacement.
|
||||
local_poly_si = shapely.affinity.translate(poly_si, -origin.x, -origin.y)
|
||||
local_base_z = base_z - origin.z
|
||||
|
||||
def localize_plane(plane):
|
||||
point, normal = plane
|
||||
return (np.array(point) - np.array([origin.x, origin.y, origin.z]), normal)
|
||||
|
||||
local_top_planes = [localize_plane(p) for p in (top_planes or [])]
|
||||
local_bottom_planes = [localize_plane(p) for p in (bottom_planes or [])]
|
||||
|
||||
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 []
|
||||
ifc_file, local_poly_si, local_base_z, local_top_planes, local_bottom_planes
|
||||
)
|
||||
cls.set_brep_representation_from_mesh(obj, element, item)
|
||||
else:
|
||||
shapes = cls.get_or_build_geom_cache()["shapes"]
|
||||
local_shapes = {}
|
||||
for shape_id, shape_data in shapes.items():
|
||||
local_shape_data = dict(shape_data)
|
||||
local_shape_data["top_z"] = shape_data["top_z"] - origin.z
|
||||
local_shape_data["bottom_z"] = shape_data["bottom_z"] - origin.z
|
||||
local_shapes[shape_id] = local_shape_data
|
||||
item = ifcopenshell.util.space.build_brep_space(
|
||||
ifc_file, element, cls.get_or_build_geom_cache()["shapes"], poly, base_z
|
||||
ifc_file, element, local_shapes, local_poly_si, local_base_z
|
||||
)
|
||||
if item is None:
|
||||
cls.set_extrusion_representation_from_polygon(obj, element, poly, h / unit_scale, polygon_is_si)
|
||||
|
||||
@@ -26,6 +26,7 @@ import ifcopenshell.api.root
|
||||
import ifcopenshell.api.spatial
|
||||
import ifcopenshell.util.representation
|
||||
import numpy as np
|
||||
import pytest
|
||||
import shapely
|
||||
from mathutils import Matrix, Vector
|
||||
|
||||
@@ -497,11 +498,10 @@ class TestGenerateSpace(NewFile):
|
||||
|
||||
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}"
|
||||
world_verts = [obj.matrix_world @ v.co for v in mesh.vertices]
|
||||
world_zs = [v.z for v in world_verts]
|
||||
assert min(world_zs) >= -0.1, f"Expected space world bottom near z>=0, got {min(world_zs)}"
|
||||
assert max(world_zs) > 0, f"Expected space to have positive height, got {max(world_zs)}"
|
||||
assert np.isclose(obj.location.z, 4.5, atol=0.01), f"Expected location.z=4.5, got {obj.location.z}"
|
||||
|
||||
|
||||
@@ -609,4 +609,23 @@ class TestSpaceVolumeStrategy(NewFile):
|
||||
assert len(top) == 1
|
||||
assert len(bottom) == 0
|
||||
|
||||
class TestGenerateSpaceSlopedRoof(NewFile):
|
||||
|
||||
class TestGenerateSpaceLocation(NewFile):
|
||||
def test_generate_space_at_non_zero_cursor_location(self):
|
||||
bpy.ops.bim.create_project()
|
||||
ifc = tool.Ifc.get()
|
||||
# 4 thin walls forming a hollow box around (10, 20).
|
||||
_BlockHelper.create_thin_wall(ifc, 10.0, 20.0 + 4.8, 10.0, 0.4)
|
||||
_BlockHelper.create_thin_wall(ifc, 10.0, 20.0 - 4.8, 10.0, 0.4)
|
||||
_BlockHelper.create_thin_wall(ifc, 10.0 + 4.8, 20.0, 0.4, 10.0)
|
||||
_BlockHelper.create_thin_wall(ifc, 10.0 - 4.8, 20.0, 0.4, 10.0)
|
||||
bpy.context.scene.cursor.location = (10, 20, 0)
|
||||
|
||||
bpy.ops.bim.generate_space()
|
||||
space = bpy.data.objects["IfcSpace/Space"]
|
||||
mesh = space.data
|
||||
assert isinstance(mesh, bpy.types.Mesh)
|
||||
world_verts = np.array([space.matrix_world @ v.co for v in mesh.vertices])
|
||||
center = (world_verts.min(axis=0) + world_verts.max(axis=0)) / 2
|
||||
assert center[0] == pytest.approx(10.0, abs=0.1)
|
||||
assert center[1] == pytest.approx(20.0, abs=0.1)
|
||||
|
||||
@@ -565,8 +565,11 @@ def build_brep_space(
|
||||
ifcopenshell.api.geometry.assign_representation(ifc_file, product=space, representation=seed_rep)
|
||||
|
||||
local_shapes = _build_local_shapes(ifc_file)
|
||||
boundary_class = "IfcRelSpaceBoundary1stLevel"
|
||||
if ifc_file.schema == "IFC2X3":
|
||||
boundary_class = "IfcRelSpaceBoundary"
|
||||
boundaries = ifcopenshell.util.boundary.auto_generate_boundaries(
|
||||
ifc_file, space, local_shapes, boundary_class="IfcRelSpaceBoundary1stLevel"
|
||||
ifc_file, space, local_shapes, boundary_class=boundary_class
|
||||
)
|
||||
if isinstance(boundaries, str) or not boundaries:
|
||||
ifcopenshell.api.geometry.remove_representation(ifc_file, representation=seed_rep)
|
||||
|
||||
@@ -24,6 +24,7 @@ import ifcopenshell.util.shape
|
||||
import ifcopenshell.util.space as subject
|
||||
import math
|
||||
import numpy as np
|
||||
import os
|
||||
import pytest
|
||||
import shapely
|
||||
import test.bootstrap
|
||||
@@ -320,3 +321,24 @@ class TestBuildBrepSpace(test.bootstrap.IFC4):
|
||||
item = subject.build_brep_space(self.file, space, shapes, shapely.box(-4, -4, 4, 4), 0.0)
|
||||
assert item is not None
|
||||
assert item.is_a("IfcFacetedBrep") or item.is_a("IfcPolygonalFaceSet")
|
||||
|
||||
|
||||
class TestBuildBrepSpaceIfc2x3:
|
||||
def test_build_brep_space_on_ifc2x3_uses_rel_space_boundary(self):
|
||||
# IFC2X3 does not have IfcRelSpaceBoundary1stLevel; build_brep_space must
|
||||
# fall back to plain IfcRelSpaceBoundary without raising a schema error.
|
||||
# We use the real IFC2X3 fixture because the installed wrapper in this
|
||||
# environment has a quirk with synthetic IFC2X3 geometry creation.
|
||||
path = os.path.join(
|
||||
os.path.dirname(__file__),
|
||||
"..",
|
||||
"IfcRelSpaceBoundary_TestFiles",
|
||||
"IfcRelSpaceBoundary2ndLevel",
|
||||
"HouseWithGarage_AC22_IFC2X3.ifc",
|
||||
)
|
||||
ifc_file = ifcopenshell.open(path)
|
||||
space = ifc_file.by_type("IfcSpace")[0]
|
||||
shapes = _build_shapes_dict(ifc_file, ifc_file.by_type("IfcWall") + ifc_file.by_type("IfcSlab"))
|
||||
item = subject.build_brep_space(ifc_file, space, shapes, shapely.box(-1, -1, 1, 1), 0.0)
|
||||
assert item is not None
|
||||
assert item.is_a("IfcFacetedBrep") or item.is_a("IfcPolygonalFaceSet")
|
||||
|
||||
Reference in New Issue
Block a user