diff --git a/src/bonsai/bonsai/core/spatial.py b/src/bonsai/bonsai/core/spatial.py index 5086a8a872..5ce6d7c253 100644 --- a/src/bonsai/bonsai/core/spatial.py +++ b/src/bonsai/bonsai/core/spatial.py @@ -219,13 +219,8 @@ def generate_space( else: assert space_polygon - bm = spatial.get_bmesh_from_polygon(space_polygon, h=h, polygon_is_si=True) - - mesh = spatial.get_named_mesh_from_bmesh(name="Space", bmesh=bm) - if element and element.is_a("IfcSpace"): - mesh = spatial.get_transformed_mesh_from_local_to_global(mesh) - spatial.edit_active_space_obj_from_mesh(mesh) + spatial.set_space_representation_from_polygon(active_obj, element, space_polygon, h, polygon_is_si=True) spatial.translate_obj_to_z_location(active_obj, z) else: if relating_type: @@ -233,12 +228,13 @@ def generate_space( else: name = "Space" - obj = spatial.get_named_obj_from_mesh(name, mesh) + obj = spatial.create_object(name) spatial.set_obj_origin_to_cursor_position_and_zero_elevation(obj) spatial.translate_obj_to_z_location(obj, z) 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) if relating_type: spatial.assign_relating_type_to_element(ifc, type, element, relating_type) @@ -257,16 +253,16 @@ def generate_spaces_from_walls( for i, linear_ring in enumerate(union.interiors): poly = spatial.get_buffered_poly_from_linear_ring(linear_ring) - bm = spatial.get_bmesh_from_polygon(poly, h, polygon_is_si=False) - name = "Space" + str(i) - obj = spatial.get_named_obj_from_bmesh(name, bmesh=bm) - - spatial.set_obj_origin_to_bboxcenter_and_zero_elevation(obj) + obj = spatial.create_object(name) + spatial.set_obj_origin_to_polygon_center(obj, poly, polygon_is_si=False) spatial.translate_obj_to_z_location(obj, z) spatial.assign_ifcspace_class_to_obj(obj) + element = ifc.get_entity(obj) + spatial.set_space_representation_from_polygon(obj, element, poly, h, polygon_is_si=False) + def toggle_space_visibility(ifc: type[tool.Ifc], spatial: type[tool.Spatial]) -> None: model = ifc.get() diff --git a/src/bonsai/bonsai/core/tool.py b/src/bonsai/bonsai/core/tool.py index 09a4fe90f0..fa3ba8a860 100644 --- a/src/bonsai/bonsai/core/tool.py +++ b/src/bonsai/bonsai/core/tool.py @@ -1004,9 +1004,10 @@ class Spatial: def get_named_obj_from_mesh(cls, name, mesh): pass def get_named_mesh_from_bmesh(cls, name, bmesh): pass def get_transformed_mesh_from_local_to_global(cls, mesh): pass - def edit_active_space_obj_from_mesh(cls, mesh): pass + def set_space_representation_from_polygon(cls, obj, element, poly, h, polygon_is_si=True): pass + def create_object(cls, name): pass + def set_obj_origin_to_polygon_center(cls, obj, poly, polygon_is_si=True): pass def set_obj_origin_to_bboxcenter(cls, obj): pass - def set_obj_origin_to_bboxcenter_and_zero_elevation(cls, obj): pass def set_obj_origin_to_cursor_position_and_zero_elevation(cls, obj): pass def get_selected_objects(cls): pass def get_active_obj(cls): pass diff --git a/src/bonsai/bonsai/tool/spatial.py b/src/bonsai/bonsai/tool/spatial.py index b185b9ab59..e598f3fb9d 100644 --- a/src/bonsai/bonsai/tool/spatial.py +++ b/src/bonsai/bonsai/tool/spatial.py @@ -28,6 +28,7 @@ import bmesh import bpy import ifcopenshell import ifcopenshell.api.attribute +import ifcopenshell.api.geometry import ifcopenshell.api.type import ifcopenshell.geom import ifcopenshell.util.classification @@ -1033,6 +1034,23 @@ class Spatial(bonsai.core.tool.Spatial): obj = cls.get_named_obj_from_mesh(name, mesh) return obj + @classmethod + def create_object(cls, name: str) -> bpy.types.Object: + mesh = bpy.data.meshes.new(name=name) + obj = bpy.data.objects.new(name, mesh) + return obj + + @classmethod + def set_obj_origin_to_polygon_center( + cls, obj: bpy.types.Object, poly: Polygon, polygon_is_si: bool = True + ) -> None: + unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) + centroid = poly.centroid + if polygon_is_si: + obj.location = Vector((centroid.x, centroid.y, 0)) + else: + obj.location = Vector((centroid.x * unit_scale, centroid.y * unit_scale, 0)) + @classmethod def get_named_obj_from_mesh(cls, name: str, mesh: bpy.types.Mesh) -> bpy.types.Object: obj = bpy.data.objects.new(name, mesh) @@ -1054,17 +1072,55 @@ class Spatial(bonsai.core.tool.Spatial): return mesh @classmethod - def edit_active_space_obj_from_mesh(cls, mesh: bpy.types.Mesh) -> None: - active_obj = bpy.context.active_object - old_mesh = active_obj.data - old_mesh_name = old_mesh.name - assert active_obj and isinstance(old_mesh, bpy.types.Mesh) - tool.Geometry.get_mesh_props(mesh).ifc_definition_id = tool.Geometry.get_mesh_props(old_mesh).ifc_definition_id - tool.Geometry.change_object_data(active_obj, mesh, is_global=True) - tool.Ifc.edit(active_obj) - tool.Blender.remove_data_block(old_mesh) - # Rename after old mesh is removed to avoid .001 suffix. - mesh.name = old_mesh_name + def set_space_representation_from_polygon( + cls, + obj: bpy.types.Object, + element: ifcopenshell.entity_instance, + poly: Polygon, + h: float, + polygon_is_si: bool = True, + ) -> None: + """Create or replace the IFC body representation of a space from a polygon. + + :param obj: The Blender object for the space. + :param element: The IfcSpace entity. + :param poly: The space polygon in world space. + :param h: The height in SI (meters). + :param polygon_is_si: True if polygon coords are in SI, False if in IFC file units. + """ + ifc_file = tool.Ifc.get() + unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file) + builder = ifcopenshell.util.shape_builder.ShapeBuilder(ifc_file) + + bpy.context.view_layer.update() + mat_inv = obj.matrix_world.inverted() + coords_2d = [] + for v in shapely.get_exterior_ring(poly).coords[:-1]: + world_si = Vector((v[0], v[1], 0)) + if not polygon_is_si: + world_si = world_si * unit_scale + local_si = mat_inv @ world_si + coords_2d.append(Vector((local_si.x, local_si.y)) / unit_scale) + + curve = builder.polyline(coords_2d, closed=True) + item = builder.extrude(curve, magnitude=h / unit_scale) + + old_body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW") + if old_body: + context = old_body.ContextOfItems + ifcopenshell.api.geometry.unassign_representation(ifc_file, product=element, representation=old_body) + ifcopenshell.api.geometry.remove_representation(ifc_file, representation=old_body) + else: + context = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW") + + new_body = builder.get_representation(context, item) + ifcopenshell.api.geometry.assign_representation(ifc_file, product=element, representation=new_body) + bonsai.core.geometry.switch_representation( + tool.Ifc, + tool.Geometry, + obj=obj, + representation=new_body, + ) @classmethod def set_obj_origin_to_bboxcenter(cls, obj: bpy.types.Object) -> None: @@ -1082,24 +1138,6 @@ class Spatial(bonsai.core.tool.Spatial): vert.co = inverted @ aux_vector obj.location = newLoc - @classmethod - def set_obj_origin_to_bboxcenter_and_zero_elevation(cls, obj: bpy.types.Object) -> None: - mat = obj.matrix_world - inverted = mat.inverted() - local_bbox_center = 0.125 * sum((Vector(b) for b in obj.bound_box), Vector()) - global_bbox_center = mat @ local_bbox_center - global_obj_origin = global_bbox_center - global_obj_origin.z = 0 - - oldLoc = obj.location - newLoc = global_obj_origin - diff = newLoc - oldLoc - for vert in obj.data.vertices: - aux_vector = mat @ vert.co - aux_vector = aux_vector - diff - vert.co = inverted @ aux_vector - obj.location = newLoc - @classmethod def set_obj_origin_to_cursor_position_and_zero_elevation(cls, obj: bpy.types.Object) -> None: mat = obj.matrix_world @@ -1188,7 +1226,14 @@ class Spatial(bonsai.core.tool.Spatial): @classmethod def assign_ifcspace_class_to_obj(cls, obj: bpy.types.Object) -> None: - bpy.ops.bim.assign_class(obj=obj.name, ifc_class="IfcSpace") + bonsai.core.root.assign_class( + tool.Ifc, + tool.Collector, + tool.Root, + obj=obj, + ifc_class="IfcSpace", + should_add_representation=False, + ) @classmethod def assign_type_to_obj(cls, obj: bpy.types.Object) -> None: