diff --git a/src/bonsai/bonsai/core/spatial.py b/src/bonsai/bonsai/core/spatial.py index 7c66e8b075..4eb1664d1a 100644 --- a/src/bonsai/bonsai/core/spatial.py +++ b/src/bonsai/bonsai/core/spatial.py @@ -234,7 +234,17 @@ def generate_space( h = auto_h if element and element.is_a("IfcSpace"): - spatial.set_space_representation_from_polygon(active_obj, element, space_polygon, h, polygon_is_si=True) + assert active_obj + active_obj.location.z = z + spatial.set_space_representation_from_polygon( + active_obj, + element, + space_polygon, + h, + polygon_is_si=True, + bounding_walls=bounding_walls, + container=container, + ) else: if relating_type: name = model.generate_occurrence_name(relating_type, "IfcSpace") @@ -247,7 +257,9 @@ def generate_space( spatial.assign_ifcspace_class_to_obj(obj) element = ifc.get_entity(obj) - spatial.set_space_representation_from_polygon(obj, element, space_polygon, h, polygon_is_si=True) + spatial.set_space_representation_from_polygon( + obj, element, space_polygon, h, polygon_is_si=True, bounding_walls=bounding_walls, container=container + ) if relating_type: spatial.assign_relating_type_to_element(ifc, type, element, relating_type) diff --git a/src/bonsai/test/tool/test_spatial.py b/src/bonsai/test/tool/test_spatial.py index e00a29bcad..2fc67eb423 100644 --- a/src/bonsai/test/tool/test_spatial.py +++ b/src/bonsai/test/tool/test_spatial.py @@ -279,6 +279,21 @@ class _BlockHelper: wall.Representation = ifc.createIfcProductDefinitionShape(None, None, [shape_rep]) return wall, extrusion + @staticmethod + def create_thin_wall(ifc, cx, cy, width, depth, height=10.0): + """Create an IFC wall with a thin block representation centered at (cx, cy).""" + ctx = ifcopenshell.util.representation.get_context(ifc, "Model", "Body", "MODEL_VIEW") + 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, width, depth) + placement_3d = ifc.createIfcAxis2Placement3D(ifc.createIfcCartesianPoint([cx, cy, 0.0])) + extrusion = ifc.createIfcExtrudedAreaSolid( + profile, placement_3d, ifc.createIfcDirection([0.0, 0.0, 1.0]), height + ) + shape_rep = ifc.createIfcShapeRepresentation(ctx, "Body", "SweptSolid", [extrusion]) + wall.Representation = ifc.createIfcProductDefinitionShape(None, None, [shape_rep]) + return wall + @staticmethod def create_slab(ifc, z=4.0): """Create an IfcSlab with a 12x12x1.0 block representation at bottom_z={z}.""" @@ -490,6 +505,62 @@ class TestGenerateSpace(NewFile): assert np.isclose(obj.location.z, 4.5, atol=0.01), f"Expected location.z=4.5, got {obj.location.z}" +class TestGenerateSpaceSlopedRoof(NewFile): + def _create_shed_roof(self, ifc, z=4.0, rise=3.0): + """Create an IfcRoof whose underside is a sloped plane across the footprint. + + Triangular prism: vertical profile (in the y-z plane) extruded along +x. + Profile points (u, v) with placement loc=(-5, 0, z), axis=(1,0,0), + ref=(0,0,1): (0,-5) -> world (-5,-5,z) eave bottom + (rise,-5) -> world (-5,-5,z+rise) eave top + (rise,5) -> world (-5,5,z+rise) ridge + ExtrudedDirection (0,0,1) is local, mapping to world +x; depth 10 spans + x in [-5, 5]. + """ + ctx = ifcopenshell.util.representation.get_context(ifc, "Model", "Body", "MODEL_VIEW") + roof = ifcopenshell.api.root.create_entity(ifc, ifc_class="IfcRoof") + pts = [ + ifc.createIfcCartesianPoint((0.0, -5.0)), + ifc.createIfcCartesianPoint((float(rise), -5.0)), + ifc.createIfcCartesianPoint((float(rise), 5.0)), + ] + polyline = ifc.createIfcPolyline(pts) + profile = ifc.createIfcArbitraryClosedProfileDef(ProfileType="CURVE", OuterCurve=polyline) + placement = ifc.createIfcAxis2Placement3D( + ifc.createIfcCartesianPoint((-5.0, 0.0, z)), + ifc.createIfcDirection((1.0, 0.0, 0.0)), + ifc.createIfcDirection((0.0, 0.0, 1.0)), + ) + extrude_dir = ifc.createIfcDirection((0.0, 0.0, 1.0)) + solid = ifc.createIfcExtrudedAreaSolid(profile, placement, extrude_dir, 10.0) + rep = ifc.createIfcShapeRepresentation(ctx, "Body", "SweptSolid", [solid]) + ifcopenshell.api.geometry.assign_representation(ifc, product=roof, representation=rep) + return roof + + def test_generate_space_under_shed_roof(self): + bpy.ops.bim.create_project() + ifc = tool.Ifc.get() + _BlockHelper.create_thin_wall(ifc, 0.0, 4.8, 10.0, 0.4) + _BlockHelper.create_thin_wall(ifc, 0.0, -4.8, 10.0, 0.4) + _BlockHelper.create_thin_wall(ifc, 4.8, 0.0, 0.4, 10.0) + _BlockHelper.create_thin_wall(ifc, -4.8, 0.0, 0.4, 10.0) + self._create_shed_roof(ifc, z=4.0, rise=3.0) + bpy.context.scene.cursor.location = (0, 0, 0) + + bpy.ops.bim.generate_space() + space = bpy.data.objects["IfcSpace/Space"] + mesh = space.data + assert isinstance(mesh, bpy.types.Mesh) + verts = np.array([v.co for v in mesh.vertices]) + min_z = verts[:, 2].min() + max_z = verts[:, 2].max() + assert min_z >= -0.1 + assert max_z > 0 + top_z_north = max([v[2] for v in verts if v[1] > 1]) + top_z_south = max([v[2] for v in verts if v[1] < -1]) + assert abs(top_z_north - top_z_south) > 0.05, f"Top should slope along y: {top_z_north} vs {top_z_south}" + + class TestSpaceVolumeStrategy(NewFile): def test_vertical_box_returns_extrude_clip(self): bpy.ops.bim.create_project()