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")