From a1d99b544a4f44bc9356799164243aefc57aa91f Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Wed, 18 Sep 2024 21:35:18 +1000 Subject: [PATCH] Basic support for deleting items, and editing meshlike items --- src/bonsai/bonsai/bim/import_ifc.py | 41 +--- src/bonsai/bonsai/bim/module/geometry/data.py | 10 +- .../bonsai/bim/module/geometry/decorator.py | 21 +- .../bonsai/bim/module/geometry/operator.py | 186 +++++++++--------- src/bonsai/bonsai/bim/module/geometry/prop.py | 13 ++ src/bonsai/bonsai/bim/ui.py | 6 +- src/bonsai/bonsai/tool/geometry.py | 38 ++++ src/bonsai/bonsai/tool/loader.py | 45 +++++ 8 files changed, 218 insertions(+), 142 deletions(-) diff --git a/src/bonsai/bonsai/bim/import_ifc.py b/src/bonsai/bonsai/bim/import_ifc.py index 2a7831bad0..9abff58357 100644 --- a/src/bonsai/bonsai/bim/import_ifc.py +++ b/src/bonsai/bonsai/bim/import_ifc.py @@ -1370,46 +1370,7 @@ class IfcImporter: verts = geometry.verts mesh["has_cartesian_point_offset"] = False - if geometry.faces: - num_vertices = len(verts) // 3 - total_faces = len(geometry.faces) - loop_start = range(0, total_faces, 3) - num_loops = total_faces // 3 - loop_total = [3] * num_loops - num_vertex_indices = len(geometry.faces) - - # See bug 3546 - # ios_edges holds true edges that aren't triangulated. - # - # we do `.tolist()` because Blender can't assign `np.int32` to it's custom attributes - mesh["ios_edges"] = list(set(tuple(e) for e in ifcopenshell.util.shape.get_edges(geometry).tolist())) - mesh["ios_item_ids"] = ifcopenshell.util.shape.get_representation_item_ids(geometry).tolist() - - mesh.vertices.add(num_vertices) - mesh.vertices.foreach_set("co", verts) - mesh.loops.add(num_vertex_indices) - mesh.loops.foreach_set("vertex_index", geometry.faces) - mesh.polygons.add(num_loops) - mesh.polygons.foreach_set("loop_start", loop_start) - mesh.polygons.foreach_set("loop_total", loop_total) - mesh.polygons.foreach_set("use_smooth", [0] * total_faces) - mesh.update() - - rep_str: str = geometry.id - if "openings" not in rep_str: - rep_id = rep_str.split("-", 1)[0] - rep = self.file.by_id(int(rep_id)) - tool.Loader.load_indexed_colour_map(rep, mesh) - else: - e = geometry.edges - v = verts - vertices = [[v[i], v[i + 1], v[i + 2]] for i in range(0, len(v), 3)] - edges = [[e[i], e[i + 1]] for i in range(0, len(e), 2)] - mesh.from_pydata(vertices, edges, []) - - mesh["ios_materials"] = [m.instance_id() for m in geometry.materials] - mesh["ios_material_ids"] = geometry.material_ids - return mesh + return tool.Loader.convert_geometry_to_mesh(geometry, mesh, verts=verts) except: self.ifc_import_settings.logger.error("Could not create mesh for %s", element) import traceback diff --git a/src/bonsai/bonsai/bim/module/geometry/data.py b/src/bonsai/bonsai/bim/module/geometry/data.py index 0c8ba8eec1..8bfdb7d574 100644 --- a/src/bonsai/bonsai/bim/module/geometry/data.py +++ b/src/bonsai/bonsai/bim/module/geometry/data.py @@ -47,18 +47,18 @@ class ViewportData: @classmethod def mode(cls): obj = bpy.context.active_object - obj_mode = [("OBJECT", "IFC Object Mode", "", "OBJECT_DATAMODE", 0)] - mesh_modes = [ + modes = [ ("OBJECT", "IFC Object Mode", "", "OBJECT_DATAMODE", 0), - ("EDIT", "IFC Edit Mode", "", "EDITMODE_HLT", 1), + ("ITEM", "IFC Item Mode", "", "MESH_DATA", 1), ] if ( not obj or not tool.Blender.is_editable(obj) or ((element := tool.Ifc.get_entity(obj)) and tool.Geometry.is_locked(element)) ): - return obj_mode - return mesh_modes + return modes + modes.append(("EDIT", "IFC Edit Mode", "", "EDITMODE_HLT", 2)) + return modes class RepresentationsData: diff --git a/src/bonsai/bonsai/bim/module/geometry/decorator.py b/src/bonsai/bonsai/bim/module/geometry/decorator.py index 4db123c04e..b01fee4868 100644 --- a/src/bonsai/bonsai/bim/module/geometry/decorator.py +++ b/src/bonsai/bonsai/bim/module/geometry/decorator.py @@ -18,7 +18,6 @@ import blf import gpu -import json import bmesh import bonsai.tool as tool from bpy.types import SpaceView3D @@ -125,9 +124,25 @@ class ItemDecorator: edges = unselected_edges verts = unselected_verts i = len(verts) - edges.extend([[ei + i for ei in e] for e in json.loads(item.edges)]) + matrix_world = obj.matrix_world - verts.extend([matrix_world @ Vector(v) for v in json.loads(item.verts)]) + bbox_verts = [matrix_world @ Vector(co) for co in obj.bound_box] + bbox_edges = [ + (0 + i, 3 + i), + (3 + i, 7 + i), + (7 + i, 4 + i), + (4 + i, 0 + i), + (0 + i, 1 + i), + (3 + i, 2 + i), + (7 + i, 6 + i), + (4 + i, 5 + i), + (1 + i, 2 + i), + (2 + i, 6 + i), + (6 + i, 5 + i), + (5 + i, 1 + i), + ] + edges.extend(bbox_edges) + verts.extend(bbox_verts) if unselected_verts: self.draw_batch("LINES", unselected_verts, transparent_color(unselected_elements_color), unselected_edges) diff --git a/src/bonsai/bonsai/bim/module/geometry/operator.py b/src/bonsai/bonsai/bim/module/geometry/operator.py index 4210aaceb3..149ffb51ee 100644 --- a/src/bonsai/bonsai/bim/module/geometry/operator.py +++ b/src/bonsai/bonsai/bim/module/geometry/operator.py @@ -16,12 +16,11 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . +import re import bpy import bmesh import logging import numpy as np -import json -import re import ifcopenshell import ifcopenshell.util.unit import ifcopenshell.util.element @@ -637,6 +636,8 @@ class OverrideDelete(bpy.types.Operator): self.report({"INFO"}, "Elements that are part of an array cannot be deleted.") return {"FINISHED"} tool.Geometry.delete_ifc_object(obj) + elif tool.Geometry.is_representation_item(obj): + tool.Geometry.delete_ifc_item(obj) else: bpy.data.objects.remove(obj) @@ -1591,6 +1592,12 @@ class OverrideModeSetEdit(bpy.types.Operator, tool.Ifc.Operator): return bpy.ops.bim.enable_editing_boundary_geometry() for obj in selected_objs: + if obj == context.scene.BIMGeometryProperties.representation_obj: + self.report({"ERROR"}, f"Element '{obj.name}' is in item mode and cannot be edited directly.") + obj.select_set(False) + continue + if tool.Geometry.is_representation_item(obj): + return self.enable_editing_representation_item(context, obj) if not tool.Blender.is_editable(obj): self.report({"INFO"}, f"Object cannot be edited: {obj.name}") obj.select_set(False) @@ -1694,6 +1701,21 @@ class OverrideModeSetEdit(bpy.types.Operator, tool.Ifc.Operator): context.view_layer.objects.active = active_obj return {"FINISHED"} + def enable_editing_representation_item(self, context, obj): + item = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id) + if tool.Geometry.is_meshlike_item(item): + tool.Blender.select_and_activate_single_object(context, obj) + obj.data.BIMMeshProperties.mesh_checksum = tool.Geometry.get_mesh_checksum(obj.data) + if tool.Blender.toggle_edit_mode(context) == {"CANCELLED"}: + return {"CANCELLED"} + context.scene.BIMGeometryProperties.is_changing_mode = True + if context.scene.BIMGeometryProperties.mode != "EDIT": + context.scene.BIMGeometryProperties.mode = "EDIT" + context.scene.BIMGeometryProperties.is_changing_mode = False + return {"FINISHED"} + else: + self.report({"INFO"}, f"Editing {item.is_a()} geometry is not supported") + def invoke(self, context, event): return IfcStore.execute_ifc_operator(self, context, is_invoke=True) @@ -1716,28 +1738,49 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator): if self.should_save: bpy.ops.bim.update_representation(obj=obj.name, ifc_representation_class="") if getattr(tool.Ifc.get_entity(obj), "HasOpenings", False): - self.reload_representation(obj) + tool.Geometry.reload_representation(obj) else: - self.reload_representation(obj) + tool.Geometry.reload_representation(obj) tool.Ifc.finish_edit(obj) for obj in self.unchanged_objs_with_openings: - self.reload_representation(obj) + tool.Geometry.reload_representation(obj) tool.Ifc.finish_edit(obj) return {"FINISHED"} - def reload_representation(self, obj): - representation = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id) - core.switch_representation( - tool.Ifc, - tool.Geometry, - obj=obj, - representation=representation, - should_reload=True, - is_global=True, - should_sync_changes_first=False, - apply_openings=True, - ) + def edit_representation_item(self, obj): + item = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id) + if tool.Geometry.is_meshlike_item(item): + if tool.Geometry.has_geometric_data(obj) and obj.data.polygons: + if obj.data.BIMMeshProperties.mesh_checksum == tool.Geometry.get_mesh_checksum(obj.data): + return {"FINISHED"} + builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get()) + unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) + verts = [v.co / unit_scale for v in obj.data.vertices] + faces = [p.vertices[:] for p in obj.data.polygons] + if item.is_a("IfcAdvancedBrep"): + new_item = builder.faceted_brep(verts, faces) + else: + new_item = builder.mesh(verts, faces) + for inverse in tool.Ifc.get().get_inverse(item): + ifcopenshell.util.element.replace_attribute(inverse, item, new_item) + ifcopenshell.util.element.remove_deep2(tool.Ifc.get(), item) + obj.data.BIMMeshProperties.ifc_definition_id = new_item.id() + tool.Geometry.reload_representation(bpy.context.scene.BIMGeometryProperties.representation_obj) + return {"FINISHED"} + else: + self.reload_item(obj) + return {"FINISHED"} + return {"FINISHED"} + + def reload_item(self, obj): + tool.Loader.settings.contexts = ifcopenshell.util.representation.get_prioritised_contexts(tool.Ifc.get()) + tool.Loader.settings.context_settings = tool.Loader.create_settings() + tool.Loader.settings.gross_context_settings = tool.Loader.create_settings(is_gross=True) + item = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id) + geometry = tool.Loader.create_generic_shape(item) + obj.data.clear_geometry() + tool.Loader.convert_geometry_to_mesh(geometry, obj.data) def invoke(self, context, event): return IfcStore.execute_ifc_operator(self, context, is_invoke=True) @@ -1750,8 +1793,12 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator): bpy.ops.object.mode_set(mode="EDIT", toggle=True) context.scene.BIMGeometryProperties.is_changing_mode = True - if context.scene.BIMGeometryProperties.mode != "OBJECT": - context.scene.BIMGeometryProperties.mode = "OBJECT" + if context.scene.BIMGeometryProperties.representation_obj: + if context.scene.BIMGeometryProperties.mode != "ITEM": + context.scene.BIMGeometryProperties.mode = "ITEM" + else: + if context.scene.BIMGeometryProperties.mode != "OBJECT": + context.scene.BIMGeometryProperties.mode = "OBJECT" context.scene.BIMGeometryProperties.is_changing_mode = False if not tool.Ifc.get(): @@ -1761,6 +1808,8 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator): element = tool.Ifc.get_entity(context.active_object) if element and element.is_a("IfcRelSpaceBoundary"): return bpy.ops.bim.edit_boundary_geometry() + elif tool.Geometry.is_representation_item(context.active_object): + return self.edit_representation_item(context.active_object) objs = context.selected_objects or [context.active_object] @@ -1909,7 +1958,6 @@ class RemoveRepresentationItem(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.remove_representation_item" bl_label = "Remove Representation Item" bl_options = {"REGISTER", "UNDO"} - representation_item_id: bpy.props.IntProperty() @classmethod @@ -1923,7 +1971,6 @@ class RemoveRepresentationItem(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = context.active_object - props = obj.BIMGeometryProperties ifc_file = tool.Ifc.get() representation_item = ifc_file.by_id(self.representation_item_id) @@ -2217,7 +2264,7 @@ class RemoveRepresentationItemFromShapeAspect(bpy.types.Operator, tool.Ifc.Opera class ImportRepresentationItems(bpy.types.Operator, tool.Ifc.Operator): - bl_idname = "bim.xxx_import_representation_items" + bl_idname = "bim.import_representation_items" bl_label = "Import Representation Items" bl_options = {"REGISTER", "UNDO"} @@ -2225,85 +2272,38 @@ class ImportRepresentationItems(bpy.types.Operator, tool.Ifc.Operator): obj = context.active_object tool.Geometry.apply_item_ids_as_vertex_groups(obj) tool.Geometry.dissolve_triangulated_edges(obj) - bm_dict = self.separate_faces_by_vertex_group(obj) - # bm_dict = self.separate_faces_by_id(obj) props = context.scene.BIMGeometryProperties - props.item_objs.clear() + props.representation_obj = obj + tool.Geometry.lock_object(obj) - for item_id, bm in bm_dict.items(): - item_mesh = bpy.data.meshes.new(f"mesh_id_{item_id}") - bm.to_mesh(item_mesh) - bm.free() + context.scene.BIMGeometryProperties.is_changing_mode = True + if context.scene.BIMGeometryProperties.mode != "ITEM": + context.scene.BIMGeometryProperties.mode = "ITEM" + context.scene.BIMGeometryProperties.is_changing_mode = False + item_ids = set() + for vgroup in obj.vertex_groups: + if vgroup.name.startswith("ios_item_id_"): + item_ids.add(int(vgroup.name.split("_")[-1])) + + tool.Loader.settings.contexts = ifcopenshell.util.representation.get_prioritised_contexts(tool.Ifc.get()) + tool.Loader.settings.context_settings = tool.Loader.create_settings() + tool.Loader.settings.gross_context_settings = tool.Loader.create_settings(is_gross=True) + + for item_id in item_ids: item = tool.Ifc.get().by_id(item_id) + item_mesh = bpy.data.meshes.new(f"Item/{item.is_a()}/{item_id}") + item_mesh.BIMMeshProperties.ifc_definition_id = item_id + geometry = tool.Loader.create_generic_shape(item) + tool.Loader.convert_geometry_to_mesh(geometry, item_mesh) + item_obj = bpy.data.objects.new(f"Item/{item.is_a()}/{item_id}", item_mesh) item_obj.matrix_world = obj.matrix_world item_obj.show_in_front = True bpy.context.collection.objects.link(item_obj) + if tool.Geometry.is_meshlike_item(item): + tool.Geometry.lock_object(item_obj) + new = props.item_objs.add() new.obj = item_obj - - verts = [list(co) for co in item_obj.bound_box] - edges = [(0, 3), (3, 7), (7, 4), (4, 0), (0, 1), (3, 2), (7, 6), (4, 5), (1, 2), (2, 6), (6, 5), (5, 1)] - new.verts = json.dumps(verts) - new.edges = json.dumps(edges) - - def separate_faces_by_id(self, obj): - mesh = obj.data - bm = bmesh.new() - bm.from_mesh(mesh) - results = {} - face_ids = mesh["ios_item_ids"] - unique_ids = set(face_ids) - - for item_id in unique_ids: - item_bm = bmesh.new() - for face, face_id in zip(bm.faces, face_ids): - if face_id == item_id: - # Copy the face and its vertices into the new BMesh - new_face_verts = [item_bm.verts.new(v.co) for v in face.verts] - item_bm.faces.new(new_face_verts) - item_bm.verts.ensure_lookup_table() - item_bm.faces.ensure_lookup_table() - results[item_id] = item_bm - - return results - - def separate_faces_by_vertex_group(self, obj): - mesh = obj.data - bm = bmesh.new() - bm.from_mesh(mesh) - - # Ensure the bmesh has up-to-date vertex weights (vertex groups) - bm.verts.layers.deform.verify() - - results = {} - for vgroup in obj.vertex_groups: - group_bm = bmesh.new() - deform_layer = bm.verts.layers.deform.active - - # Iterate over the faces and check if all vertices of the face belong to the vertex group - for face in bm.faces: - face_in_group = True - for vert in face.verts: - # Get the vertex groups the vertex belongs to - deform = vert[deform_layer] - - # If this vertex does not belong to the current vertex group, mark the face as outside the group - if vgroup.index not in deform: - face_in_group = False - break - - # If all vertices of the face belong to the current vertex group, add the face to the new BMesh - if face_in_group: - new_face_verts = [group_bm.verts.new(v.co) for v in face.verts] - group_bm.faces.new(new_face_verts) - - # Ensure the mesh is valid and doesn't have duplicate elements - group_bm.verts.ensure_lookup_table() - group_bm.faces.ensure_lookup_table() - - results[int(vgroup.name.split("_")[3])] = group_bm - - return results diff --git a/src/bonsai/bonsai/bim/module/geometry/prop.py b/src/bonsai/bonsai/bim/module/geometry/prop.py index 1bb06837d8..a53949f4fb 100644 --- a/src/bonsai/bonsai/bim/module/geometry/prop.py +++ b/src/bonsai/bonsai/bim/module/geometry/prop.py @@ -43,11 +43,23 @@ def update_mode(self, context): if self.is_changing_mode: return if self.mode == "OBJECT": + if self.representation_obj: + tool.Geometry.unlock_object(self.representation_obj) + self.representation_obj = None bpy.ops.bim.override_mode_set_object("INVOKE_DEFAULT") + elif self.mode == "ITEM": + bpy.ops.bim.import_representation_items() elif self.mode == "EDIT": bpy.ops.bim.override_mode_set_edit("INVOKE_DEFAULT") +def update_representation_obj(self, context): + for item_obj in self.item_objs: + if item_obj.obj: + bpy.data.objects.remove(item_obj.obj) + self.item_objs.clear() + + def get_mode(self, context): if not ViewportData.is_loaded: ViewportData.load() @@ -147,6 +159,7 @@ class BIMGeometryProperties(PropertyGroup): should_force_triangulation: BoolProperty(name="Force Triangulation", default=False) is_changing_mode: BoolProperty(name="Is Changing Mode", default=False) mode: EnumProperty(items=get_mode, name="IFC Interaction Mode", update=update_mode) + representation_obj: PointerProperty(name="Representation Object", type=bpy.types.Object, update=update_representation_obj) item_objs: CollectionProperty(name="Item Objects", type=RepresentationItemObject) def is_object_valid_for_representation_copy(self, obj: bpy.types.Object) -> bool: diff --git a/src/bonsai/bonsai/bim/ui.py b/src/bonsai/bonsai/bim/ui.py index 2fe2e99377..8438041c2d 100644 --- a/src/bonsai/bonsai/bim/ui.py +++ b/src/bonsai/bonsai/bim/ui.py @@ -456,7 +456,11 @@ class BIM_PT_tabs(Panel): op.uri = "https://docs.bonsaibim.org/guides/troubleshooting.html#saving-and-loading-blend-files" row.operator("bim.close_blend_warning", text="", icon="CANCEL") - if not context.mode.startswith(context.scene.BIMGeometryProperties.mode): + if context.mode == "OBJECT" and context.scene.BIMGeometryProperties.mode in ("OBJECT", "ITEM"): + pass + elif context.mode.startswith("EDIT") and context.scene.BIMGeometryProperties.mode == "EDIT": + pass + else: box = self.layout.box() box.alert = True row = box.row(align=True) diff --git a/src/bonsai/bonsai/tool/geometry.py b/src/bonsai/bonsai/tool/geometry.py index 0c36c6730e..e362460b0b 100644 --- a/src/bonsai/bonsai/tool/geometry.py +++ b/src/bonsai/bonsai/tool/geometry.py @@ -22,6 +22,7 @@ import struct import hashlib import logging import numpy as np +import multiprocessing import ifcopenshell import ifcopenshell.api import ifcopenshell.api.grid @@ -114,6 +115,20 @@ class Geometry(bonsai.core.tool.Geometry): obj.lock_location = (False, False, False) obj.lock_rotation = (False, False, False) + @classmethod + def delete_ifc_item(cls, obj: bpy.types.Object) -> None: + props = bpy.context.scene.BIMGeometryProperties + if len(props.item_objs) == 1: + return + for i, item_obj in enumerate(props.item_objs): + if item_obj.obj == obj: + props.item_objs.remove(i) + break + item = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id) + cls.remove_representation_item(item) + cls.reload_representation(props.representation_obj) + bpy.data.objects.remove(obj) + @classmethod def delete_ifc_object(cls, obj: bpy.types.Object) -> None: element = tool.Ifc.get_entity(obj) @@ -184,6 +199,8 @@ class Geometry(bonsai.core.tool.Geometry): bonsai.core.spatial.import_spatial_decomposition(tool.Spatial) try: obj.name + if bpy.context.scene.BIMGeometryProperties.representation_obj == obj: + bpy.context.scene.BIMGeometryProperties.representation_obj = None bpy.data.objects.remove(obj) except: pass @@ -401,6 +418,11 @@ class Geometry(bonsai.core.tool.Geometry): if obj.data and hasattr(obj.data, "BIMMeshProperties") and obj.data.BIMMeshProperties.ifc_definition_id: return tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id) + @classmethod + def get_data_representation(cls, data: bpy.types.Mesh) -> ifcopenshell.entity_instance | None: + if hasattr(data, "BIMMeshProperties") and data.BIMMeshProperties.ifc_definition_id: + return tool.Ifc.get().by_id(data.BIMMeshProperties.ifc_definition_id) + @classmethod def get_active_representation_context(cls, obj: bpy.types.Object) -> ifcopenshell.entity_instance: active_representation = tool.Geometry.get_active_representation(obj) @@ -806,6 +828,10 @@ class Geometry(bonsai.core.tool.Geometry): return True return False + @classmethod + def is_meshlike_item(cls, item: ifcopenshell.entity_instance) -> bool: + return item.is_a("IfcTessellatedItem") or item.is_a("IfcManifoldSolidBrep") + @classmethod def is_profile_based(cls, data: bpy.types.Mesh) -> bool: return data.BIMMeshProperties.subshape_type == "PROFILE" @@ -816,6 +842,14 @@ class Geometry(bonsai.core.tool.Geometry): "SweptSolid", ) + @classmethod + def is_representation_item(cls, obj: bpy.types.Object) -> bool: + return ( + obj.data + and (ifc_id := obj.data.BIMMeshProperties.ifc_definition_id) + and tool.Ifc.get().by_id(ifc_id).is_a("IfcRepresentationItem") + ) + @classmethod def is_text_literal(cls, representation: ifcopenshell.entity_instance) -> bool: items = ifcopenshell.util.representation.resolve_items(representation) @@ -1365,3 +1399,7 @@ class Geometry(bonsai.core.tool.Geometry): use_immediate_repr = apply_openings and bool(getattr(element, "HasOpenings", None)) use_immediate_repr = use_immediate_repr or cls.has_material_style_override(element) return use_immediate_repr + + @classmethod + def get_elements_by_representation(cls, representation): + return ifcopenshell.util.element.get_elements_by_representation(tool.Ifc.get(), representation) diff --git a/src/bonsai/bonsai/tool/loader.py b/src/bonsai/bonsai/tool/loader.py index 6530fbfcb0..34de704904 100644 --- a/src/bonsai/bonsai/tool/loader.py +++ b/src/bonsai/bonsai/tool/loader.py @@ -879,3 +879,48 @@ class Loader(bonsai.core.tool.Loader): float(props.blender_x_axis_ordinate), ) return Matrix(matrix.tolist()) + + @classmethod + def convert_geometry_to_mesh(cls, geometry, mesh: bpy.types.Mesh, verts=None) -> bpy.types.Mesh: + if verts is None: + verts = geometry.verts + if geometry.faces: + num_vertices = len(verts) // 3 + total_faces = len(geometry.faces) + loop_start = range(0, total_faces, 3) + num_loops = total_faces // 3 + loop_total = [3] * num_loops + num_vertex_indices = len(geometry.faces) + + # See bug 3546 + # ios_edges holds true edges that aren't triangulated. + # + # we do `.tolist()` because Blender can't assign `np.int32` to it's custom attributes + mesh["ios_edges"] = list(set(tuple(e) for e in ifcopenshell.util.shape.get_edges(geometry).tolist())) + mesh["ios_item_ids"] = ifcopenshell.util.shape.get_representation_item_ids(geometry).tolist() + + mesh.vertices.add(num_vertices) + mesh.vertices.foreach_set("co", verts) + mesh.loops.add(num_vertex_indices) + mesh.loops.foreach_set("vertex_index", geometry.faces) + mesh.polygons.add(num_loops) + mesh.polygons.foreach_set("loop_start", loop_start) + mesh.polygons.foreach_set("loop_total", loop_total) + mesh.polygons.foreach_set("use_smooth", [0] * total_faces) + mesh.update() + + # TODO: geometry id is not always an int. + rep_id = geometry.id + if rep_id.isdigit(): + if (rep := tool.Ifc.get().by_id(int(rep_id))) and rep.is_a("IfcShapeRepresentation"): + tool.Loader.load_indexed_colour_map(rep, mesh) + else: + e = geometry.edges + v = verts + vertices = [[v[i], v[i + 1], v[i + 2]] for i in range(0, len(v), 3)] + edges = [[e[i], e[i + 1]] for i in range(0, len(e), 2)] + mesh.from_pydata(vertices, edges, []) + + mesh["ios_materials"] = [m.instance_id() for m in geometry.materials] + mesh["ios_material_ids"] = geometry.material_ids + return mesh