diff --git a/src/bonsai/bonsai/bim/module/geometry/__init__.py b/src/bonsai/bonsai/bim/module/geometry/__init__.py index f07aa0b19f..1caba354ba 100644 --- a/src/bonsai/bonsai/bim/module/geometry/__init__.py +++ b/src/bonsai/bonsai/bim/module/geometry/__init__.py @@ -36,6 +36,7 @@ classes = ( operator.EnableEditingRepresentationItems, operator.FlipObject, operator.GetRepresentationIfcParameters, + operator.ImportRepresentationItems, operator.OverrideDelete, operator.OverrideDuplicateMove, operator.OverrideDuplicateMoveLinked, @@ -60,6 +61,7 @@ classes = ( operator.UpdateParametricRepresentation, operator.UpdateRepresentation, prop.RepresentationItem, + prop.RepresentationItemObject, prop.ShapeAspect, prop.BIMObjectGeometryProperties, prop.BIMGeometryProperties, diff --git a/src/bonsai/bonsai/bim/module/geometry/decorator.py b/src/bonsai/bonsai/bim/module/geometry/decorator.py new file mode 100644 index 0000000000..4db123c04e --- /dev/null +++ b/src/bonsai/bonsai/bim/module/geometry/decorator.py @@ -0,0 +1,138 @@ +# Bonsai - OpenBIM Blender Add-on +# Copyright (C) 2024 Dion Moult +# +# This file is part of Bonsai. +# +# Bonsai is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Bonsai is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Bonsai. If not, see . + +import blf +import gpu +import json +import bmesh +import bonsai.tool as tool +from bpy.types import SpaceView3D +from mathutils import Vector +from gpu_extras.batch import batch_for_shader +from bpy_extras.view3d_utils import location_3d_to_region_2d + + +class ItemDecorator: + is_installed = False + handlers = [] + + @classmethod + def install(cls, context): + if cls.is_installed: + cls.uninstall() + handler = cls() + cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_text, (context,), "WINDOW", "POST_PIXEL")) + cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw, (context,), "WINDOW", "POST_VIEW")) + cls.is_installed = True + + @classmethod + def uninstall(cls): + for handler in cls.handlers: + try: + SpaceView3D.draw_handler_remove(handler, "WINDOW") + except ValueError: + pass + cls.is_installed = False + + def draw_batch(self, shader_type, content_pos, color, indices=None): + shader = self.line_shader if shader_type == "LINES" else self.shader + batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices) + shader.uniform_float("color", color) + batch.draw(shader) + + def draw_text(self, context): + self.addon_prefs = tool.Blender.get_addon_preferences() + selected_elements_color = self.addon_prefs.decorator_color_selected + unselected_elements_color = self.addon_prefs.decorator_color_unselected + special_elements_color = self.addon_prefs.decorator_color_special + + font_id = 0 + blf.size(font_id, 12) + blf.enable(font_id, blf.SHADOW) + color = selected_elements_color + blf.color(font_id, *color) + + for item in context.scene.BIMGeometryProperties.item_objs: + if (obj := item.obj) and obj.hide_get() == False: + if obj.select_get(): + centroid = obj.matrix_world @ Vector(obj.bound_box[0]).lerp(Vector(obj.bound_box[6]), 0.5) + tag = obj.name.split("/")[1] + coords_2d = location_3d_to_region_2d(context.region, context.region_data, centroid) + if coords_2d: + w, h = blf.dimensions(font_id, tag) + coords_2d -= Vector((w * 0.5, h * 0.5)) + blf.position(font_id, coords_2d[0], coords_2d[1], 0) + blf.draw(font_id, tag) + + def draw(self, context): + def transparent_color(color, alpha=0.3): + color = [i for i in color] + color[3] = alpha + return color + + self.addon_prefs = tool.Blender.get_addon_preferences() + selected_elements_color = self.addon_prefs.decorator_color_selected + unselected_elements_color = self.addon_prefs.decorator_color_unselected + special_elements_color = self.addon_prefs.decorator_color_special + + gpu.state.point_size_set(6) + gpu.state.blend_set("ALPHA") + + self.line_shader = gpu.shader.from_builtin("POLYLINE_UNIFORM_COLOR") + self.line_shader.bind() # required to be able to change uniforms of the shader + # POLYLINE_UNIFORM_COLOR specific uniforms + self.line_shader.uniform_float("viewportSize", (context.region.width, context.region.height)) + self.line_shader.uniform_float("lineWidth", 2.0) + + # general shader + self.shader = gpu.shader.from_builtin("UNIFORM_COLOR") + + selected_verts = [] + selected_edges = [] + selected_tris = [] + unselected_verts = [] + unselected_edges = [] + unselected_tris = [] + for item in context.scene.BIMGeometryProperties.item_objs: + if (obj := item.obj) and obj.hide_get() == False: + if obj.select_get(): + if context.mode != "OBJECT": + continue + edges = selected_edges + verts = selected_verts + offset = len(selected_verts) + selected_verts.extend([tuple(obj.matrix_world @ v.co) for v in obj.data.vertices]) + selected_tris.extend([tuple([i + offset for i in t.vertices]) for t in obj.data.loop_triangles]) + else: + offset = len(unselected_verts) + unselected_verts.extend([tuple(obj.matrix_world @ v.co) for v in obj.data.vertices]) + unselected_tris.extend([tuple([i + offset for i in t.vertices]) for t in obj.data.loop_triangles]) + 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)]) + + if unselected_verts: + self.draw_batch("LINES", unselected_verts, transparent_color(unselected_elements_color), unselected_edges) + self.draw_batch("TRIS", unselected_verts, transparent_color(special_elements_color), unselected_tris) + + if selected_verts: + self.draw_batch("LINES", selected_verts, selected_elements_color, selected_edges) + self.draw_batch("TRIS", selected_verts, transparent_color(selected_elements_color), selected_tris) diff --git a/src/bonsai/bonsai/bim/module/geometry/operator.py b/src/bonsai/bonsai/bim/module/geometry/operator.py index 4991f18978..cd154017e9 100644 --- a/src/bonsai/bonsai/bim/module/geometry/operator.py +++ b/src/bonsai/bonsai/bim/module/geometry/operator.py @@ -2214,3 +2214,96 @@ class RemoveRepresentationItemFromShapeAspect(bpy.types.Operator, tool.Ifc.Opera if not styled_item.Styles: ifc_file.remove(styled_item) + + +class ImportRepresentationItems(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.xxx_import_representation_items" + bl_label = "Import Representation Items" + bl_options = {"REGISTER", "UNDO"} + + def _execute(self, context): + 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() + + 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() + + item = tool.Ifc.get().by_id(item_id) + 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) + + 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 641695a16e..1bb06837d8 100644 --- a/src/bonsai/bonsai/bim/module/geometry/prop.py +++ b/src/bonsai/bonsai/bim/module/geometry/prop.py @@ -18,7 +18,7 @@ import bpy import bonsai.tool as tool -from bonsai.bim.prop import StrProperty, Attribute +from bonsai.bim.prop import StrProperty, Attribute, ObjProperty from bonsai.bim.module.geometry.data import RepresentationsData, ViewportData from bpy.types import PropertyGroup from bpy.props import ( @@ -100,6 +100,16 @@ class RepresentationItem(PropertyGroup): tags: StringProperty(name="Tags") +class RepresentationItemObject(PropertyGroup): + name: StringProperty(name="Name") + ifc_definition_id: IntProperty(name="IFC Definition ID") + obj: PointerProperty(type=bpy.types.Object) + verts: StringProperty(name="Verts") + edges: StringProperty(name="Edges") + special_verts: StringProperty(name="Special Verts") + special_edges: StringProperty(name="Special Edges") + + class ShapeAspect(PropertyGroup): name: StringProperty( name="Name", @@ -137,6 +147,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) + item_objs: CollectionProperty(name="Item Objects", type=RepresentationItemObject) def is_object_valid_for_representation_copy(self, obj: bpy.types.Object) -> bool: return bool(obj != bpy.context.active_object and obj.data) diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index 97190f1b94..3ea324506e 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -820,6 +820,7 @@ class LoadProjectElements(bpy.types.Operator): tool.Project.set_default_context() tool.Project.set_default_modeling_dimensions() tool.Root.reload_grid_decorator() + tool.Root.reload_item_decorator() return {"FINISHED"} def get_decomposition_elements(self): diff --git a/src/bonsai/bonsai/tool/root.py b/src/bonsai/bonsai/tool/root.py index 13a47b386c..0522ab6ea0 100644 --- a/src/bonsai/bonsai/tool/root.py +++ b/src/bonsai/bonsai/tool/root.py @@ -28,6 +28,7 @@ import bonsai.core.geometry import bonsai.tool as tool from typing import Union, Optional, Any from bonsai.bim.module.spatial.decorator import GridDecorator +from bonsai.bim.module.geometry.decorator import ItemDecorator class Root(bonsai.core.tool.Root): @@ -214,6 +215,12 @@ class Root(bonsai.core.tool.Root): new.obj = obj GridDecorator.install(bpy.context) + @classmethod + def reload_item_decorator(cls) -> None: + item_objs = bpy.context.scene.BIMGeometryProperties.item_objs + item_objs.clear() + ItemDecorator.install(bpy.context) + @classmethod def link_object_data(cls, source_obj: bpy.types.Object, destination_obj: bpy.types.Object) -> None: destination_obj.data = source_obj.data