diff --git a/src/blenderbim/blenderbim/bim/module/drawing/decoration.py b/src/blenderbim/blenderbim/bim/module/drawing/decoration.py index 6b21e95bee..ebff8b188d 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/decoration.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/decoration.py @@ -1926,7 +1926,7 @@ class DecorationsHandler: if cls.installed: cls.uninstall() handler = cls() - # NOTE: we USE POST_PIXEL here so that we can draw use both 3D_POLYLINE_UNIFORM_COLOR + # NOTE: we USE POST_PIXEL here so that we can use both 3D_POLYLINE_UNIFORM_COLOR # and drawing text in the same handler. BUT this means that we supply coordinates in WINSPACE cls.installed = SpaceView3D.draw_handler_add(handler, (context,), "WINDOW", "POST_PIXEL") @@ -1944,6 +1944,7 @@ class DecorationsHandler: self.decorators[object_type] = self.decorators["FALL"] def get_objects_and_decorators(self, collection): + # TODO: do it in data instead of the handler for performance? results = [] for obj in collection.all_objects: diff --git a/src/blenderbim/blenderbim/bim/module/system/__init__.py b/src/blenderbim/blenderbim/bim/module/system/__init__.py index a57b7fb5a8..ea95b8965e 100644 --- a/src/blenderbim/blenderbim/bim/module/system/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/system/__init__.py @@ -17,7 +17,7 @@ # along with BlenderBIM Add-on. If not, see . import bpy -from . import ui, prop, operator +from . import ui, prop, operator, decorator classes = ( operator.AddPort, @@ -51,7 +51,9 @@ classes = ( def register(): bpy.types.Scene.BIMSystemProperties = bpy.props.PointerProperty(type=prop.BIMSystemProperties) + bpy.app.handlers.load_post.append(decorator.toggle_decorations_on_load) def unregister(): del bpy.types.Scene.BIMSystemProperties + bpy.app.handlers.load_post.remove(decorator.toggle_decorations_on_load) diff --git a/src/blenderbim/blenderbim/bim/module/system/data.py b/src/blenderbim/blenderbim/bim/module/system/data.py index 6c71e88dc9..9dd348fe13 100644 --- a/src/blenderbim/blenderbim/bim/module/system/data.py +++ b/src/blenderbim/blenderbim/bim/module/system/data.py @@ -27,6 +27,7 @@ def refresh(): SystemData.is_loaded = False ObjectSystemData.is_loaded = False PortData.is_loaded = False + SystemDecorationData.is_loaded = False class SystemData: @@ -140,3 +141,19 @@ class PortData: data.append((port, port_obj, connected_element)) return data + + +class SystemDecorationData: + data = {} + is_loaded = False + + @classmethod + def load(cls): + cls.data = { + "decoration_data": cls.decoration_data(), + } + cls.is_loaded = True + + @classmethod + def decoration_data(cls): + return tool.System.get_decoration_data() diff --git a/src/blenderbim/blenderbim/bim/module/system/decorator.py b/src/blenderbim/blenderbim/bim/module/system/decorator.py new file mode 100644 index 0000000000..244c8d9a5f --- /dev/null +++ b/src/blenderbim/blenderbim/bim/module/system/decorator.py @@ -0,0 +1,145 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2023 Dion Moult , @Andrej730 +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on 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. +# +# BlenderBIM Add-on 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 BlenderBIM Add-on. If not, see . + +import bpy +import gpu +import bmesh +import blenderbim.tool as tool +from math import sin, cos, radians +from bpy.types import SpaceView3D +from mathutils import Vector, Matrix +from gpu_extras.batch import batch_for_shader +import ifcopenshell +from blenderbim.bim.module.system.data import SystemDecorationData +from bpy.app.handlers import persistent + + +ERROR_ELEMENTS_COLOR = (1, 0.2, 0.322, 1) # RED +UNSPECIAL_ELEMENT_COLOR = (0.2, 0.2, 0.2, 1) # GREY + + +def transparent_color(color, alpha=0.1): + color = [i for i in color] + color[3] = alpha + return color + + +@persistent +def toggle_decorations_on_load(*args): + if bpy.context.scene.BIMSystemProperties.should_draw_decorations: + SystemDecorator.install(bpy.context) + else: + SystemDecorator.uninstall() + + +class SystemDecorator: + installed = None + + @classmethod + def install(cls, context, get_custom_bmesh=None, draw_faces=False, exit_edit_mode_callback=None): + """Note that operators that change mesh in `exit_edit_mode_callback` can freeze blender. + The workaround is to move their code to function and use it for callback. + + Example: https://devtalk.blender.org/t/calling-operator-that-saves-bmesh-freezes-blender-forever/28595""" + if cls.installed: + cls.uninstall() + handler = cls() + cls.installed = SpaceView3D.draw_handler_add( + handler, (context, get_custom_bmesh, draw_faces, exit_edit_mode_callback), "WINDOW", "POST_VIEW" + ) + + @classmethod + def uninstall(cls): + try: + SpaceView3D.draw_handler_remove(cls.installed, "WINDOW") + except ValueError: + pass + cls.installed = None + + 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_faces(self, bm, vertices_coords): + """mutates original bm (triangulates it) + so the triangulation edges will be shown too + """ + traingulated_bm = bm + bmesh.ops.triangulate(traingulated_bm, faces=traingulated_bm.faces) + + face_indices = [[v.index for v in f.verts] for f in traingulated_bm.faces] + faces_color = transparent_color(self.addon_prefs.decorator_color_special) + self.draw_batch("TRIS", vertices_coords, faces_color, face_indices) + + def __call__(self, context, get_custom_bmesh=None, draw_faces=False, exit_edit_mode_callback=None): + self.addon_prefs = context.preferences.addons["blenderbim"].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") + + ### Actually drawing + all_vertices = [] + error_vertices = [] + selected_vertices = [] + unselected_vertices = [] + # special = associated with arcs/circles + special_vertices = [] + special_vertex_indices = {} + selected_edges = [] + unselected_edges = [] + arc_edges = [] + roof_angle_edges = [] + preview_edges = [] + + if not SystemDecorationData.is_loaded: + SystemDecorationData.load() + + decoration_data = SystemDecorationData.data["decoration_data"] + all_vertices = decoration_data["all_vertices"] + preview_edges = decoration_data["preview_edges"] + special_vertices = decoration_data["special_vertices"] + selected_edges = decoration_data["selected_edges"] + selected_vertices = decoration_data["selected_vertices"] + + ### Actually drawing + # 3D_POLYLINE_UNIFORM_COLOR is good for smoothed lines since `bgl.enable(GL_LINE_SMOOTH)` is deprecated + self.line_shader = gpu.shader.from_builtin("3D_POLYLINE_UNIFORM_COLOR") + self.line_shader.bind() + # 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("3D_UNIFORM_COLOR") + self.shader.bind() + + self.draw_batch("LINES", all_vertices, transparent_color(unselected_elements_color), unselected_edges) + self.draw_batch("LINES", all_vertices, selected_elements_color, selected_edges) + self.draw_batch("LINES", all_vertices, UNSPECIAL_ELEMENT_COLOR, arc_edges) + self.draw_batch("LINES", all_vertices, special_elements_color, preview_edges) + self.draw_batch("LINES", all_vertices, special_elements_color, roof_angle_edges) + + self.draw_batch("POINTS", unselected_vertices, transparent_color(unselected_elements_color, 0.5)) + self.draw_batch("POINTS", error_vertices, ERROR_ELEMENTS_COLOR) + self.draw_batch("POINTS", special_vertices, special_elements_color) + self.draw_batch("POINTS", selected_vertices, selected_elements_color) diff --git a/src/blenderbim/blenderbim/bim/module/system/prop.py b/src/blenderbim/blenderbim/bim/module/system/prop.py index 3d31414ffd..f1ffa3263c 100644 --- a/src/blenderbim/blenderbim/bim/module/system/prop.py +++ b/src/blenderbim/blenderbim/bim/module/system/prop.py @@ -18,6 +18,7 @@ import bpy from blenderbim.bim.module.system.data import SystemData +import blenderbim.bim.module.system.decorator as decorator from blenderbim.bim.prop import StrProperty, Attribute from bpy.types import PropertyGroup from bpy.props import ( @@ -44,6 +45,14 @@ class System(PropertyGroup): ifc_definition_id: IntProperty(name="IFC Definition ID") +def toggle_decorations(self, context): + toggle = self.should_draw_decorations + if toggle: + decorator.SystemDecorator.install(context) + else: + decorator.SystemDecorator.uninstall() + + class BIMSystemProperties(PropertyGroup): system_attributes: CollectionProperty(name="System Attributes", type=Attribute) is_editing: BoolProperty(name="Is Editing", default=False) @@ -52,3 +61,6 @@ class BIMSystemProperties(PropertyGroup): active_system_index: IntProperty(name="Active System Index") active_system_id: IntProperty(name="Active System Id") system_class: EnumProperty(items=get_system_class, name="Class") + should_draw_decorations: BoolProperty( + name="Should Draw Decorations", description="Toggle system decorations", update=toggle_decorations + ) diff --git a/src/blenderbim/blenderbim/bim/module/system/ui.py b/src/blenderbim/blenderbim/bim/module/system/ui.py index 5b60c151e7..d8e6b764c6 100644 --- a/src/blenderbim/blenderbim/bim/module/system/ui.py +++ b/src/blenderbim/blenderbim/bim/module/system/ui.py @@ -101,6 +101,10 @@ class BIM_PT_object_systems(Panel): if not ObjectSystemData.is_loaded: ObjectSystemData.load() self.props = context.scene.BIMSystemProperties + + row = self.layout.row(align=True) + row.prop(self.props, "should_draw_decorations") + if self.props.is_editing: row = self.layout.row() row.alignment = "RIGHT" diff --git a/src/blenderbim/blenderbim/tool/system.py b/src/blenderbim/blenderbim/tool/system.py index 13e25cf6af..87bdcc1a65 100644 --- a/src/blenderbim/blenderbim/tool/system.py +++ b/src/blenderbim/blenderbim/tool/system.py @@ -182,3 +182,80 @@ class System(blenderbim.core.tool.System): @classmethod def set_active_system(cls, system): bpy.context.scene.BIMSystemProperties.active_system_id = system.id() + + @classmethod + def get_decoration_data(cls): + all_vertices = [] + preview_edges = [] + special_vertices = [] + selected_edges = [] + selected_vertices = [] + + si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) + start_vert_i = 0 + + if bpy.context.active_object and (active_element := tool.Ifc.get_entity(bpy.context.active_object)): + selected_elements = cls.get_connected_elements(active_element) + else: + selected_elements = set() + + # TODO: get only objects visible in viewport + objects = set(bpy.data.objects) - set(bpy.data.collections["Types"].objects) + for obj in objects: + start_vert_i = len(all_vertices) + if obj.hide_get(): + continue + + if not isinstance(obj.data, bpy.types.Mesh): + continue + + element = tool.Ifc.get_entity(obj) + if not element: + continue + + if not cls.is_mep_element(element): + continue + + ports = tool.System.get_ports(element) + + for port in ports: + position = tool.Model.get_element_matrix(port).translation * si_conversion + all_vertices.append(position) + + verts = range(start_vert_i, start_vert_i + len(ports)) + edges = [(i, i + 1) for i in range(start_vert_i, start_vert_i + len(ports) - 1)] + if element in selected_elements: + selected_vertices.extend(verts) + selected_edges.extend(edges) + else: + special_vertices.extend(verts) + preview_edges.extend(edges) + + decoration_data = { + "all_vertices": all_vertices, + "preview_edges": preview_edges, + "special_vertices": [all_vertices[i] for i in special_vertices], + "selected_edges": selected_edges, + "selected_vertices": [all_vertices[i] for i in selected_vertices], + } + return decoration_data + + @classmethod + def get_connected_elements(cls, element, elements=None): + if elements is None: + elements = set((element,)) + + connected_elements = ifcopenshell.util.system.get_connected_from(element) + connected_elements += ifcopenshell.util.system.get_connected_to(element) + + for element in connected_elements: + if element in elements: + continue + elements.add(element) + cls.get_connected_elements(element, elements) + + return elements + + @classmethod + def is_mep_element(cls, element): + return element.is_a("IfcFlowSegment") or element.is_a("IfcFlowFitting")