From 628a76e487458b5e7a08a7a5f2bace1859eacb7c Mon Sep 17 00:00:00 2001 From: falken10vdl Date: Fri, 9 Jan 2026 02:02:43 +0100 Subject: [PATCH 01/11] Simplified handling of Ports in MEP --- .../bonsai/bim/module/system/__init__.py | 3 + .../bonsai/bim/module/system/operator.py | 96 +++++++++- src/bonsai/bonsai/bim/module/system/prop.py | 60 +++++- src/bonsai/bonsai/bim/module/system/ui.py | 175 ++++++++++++------ src/bonsai/bonsai/core/system.py | 5 +- src/bonsai/bonsai/tool/system.py | 26 +++ 6 files changed, 300 insertions(+), 65 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/system/__init__.py b/src/bonsai/bonsai/bim/module/system/__init__.py index 92782ae6fe..4c805f282d 100644 --- a/src/bonsai/bonsai/bim/module/system/__init__.py +++ b/src/bonsai/bonsai/bim/module/system/__init__.py @@ -21,11 +21,13 @@ from . import ui, prop, operator, decorator classes = ( operator.AddPort, + operator.AddRelatedPortConnection, operator.AddSystem, operator.AddZone, operator.AssignSystem, operator.AssignUnassignFlowControl, operator.ConnectPort, + operator.CycleFlowDirection, operator.DisableEditingSystem, operator.DisableEditingZone, operator.DisableSystemEditingUI, @@ -43,6 +45,7 @@ classes = ( operator.RemoveZone, operator.SelectSystemProducts, operator.SetFlowDirection, + operator.ShowPortFlowError, operator.ShowPorts, operator.UnassignSystem, operator.UnloadZones, diff --git a/src/bonsai/bonsai/bim/module/system/operator.py b/src/bonsai/bonsai/bim/module/system/operator.py index add214c09d..9084f1a6b8 100644 --- a/src/bonsai/bonsai/bim/module/system/operator.py +++ b/src/bonsai/bonsai/bim/module/system/operator.py @@ -217,7 +217,7 @@ class HidePorts(bpy.types.Operator, tool.Ifc.Operator): class AddPort(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.add_port" - bl_description = "Add port at current cursor position" + bl_description = "Add USERDEFINED port at current cursor position" bl_label = "Add Port" bl_options = {"REGISTER", "UNDO"} @@ -249,6 +249,42 @@ class ConnectPort(bpy.types.Operator, tool.Ifc.Operator): core.connect_port(tool.Ifc, port1=tool.Ifc.get_entity(obj1), port2=tool.Ifc.get_entity(obj2)) +class AddRelatedPortConnection(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.add_related_port_connection" + bl_label = "Connect Port" + bl_description = "Click to select a port to connect to" + bl_options = {"REGISTER", "UNDO"} + + element_id: bpy.props.IntProperty(default=0, options={"SKIP_SAVE"}) + + def invoke(self, context, event): + if self.element_id == 0: + self.report({'ERROR'}, "No port specified") + return {'CANCELLED'} + context.window_manager.modal_handler_add(self) + return {'RUNNING_MODAL'} + + def modal(self, context, event): + if event.type == 'LEFTMOUSE' and event.value == 'PRESS': + if context.active_object: + target_element = tool.Ifc.get_entity(context.active_object) + if target_element and target_element.is_a("IfcDistributionPort"): + source_element = tool.Ifc.get().by_id(self.element_id) + core.connect_port(tool.Ifc, port1=source_element, port2=target_element) + self.report({'INFO'}, "Ports connected") + return {'FINISHED'} + else: + self.report({'WARNING'}, "Selected object is not a port") + return {'RUNNING_MODAL'} + elif event.type in {'RIGHTMOUSE', 'ESC'}: + self.report({'INFO'}, "Cancelled") + return {'CANCELLED'} + return {'RUNNING_MODAL'} + + def _execute(self, context): + return {'CANCELLED'} + + class DisconnectPort(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.disconnect_port" bl_label = "Disconnect Ports" @@ -256,6 +292,9 @@ class DisconnectPort(bpy.types.Operator, tool.Ifc.Operator): element_id: bpy.props.IntProperty(default=0, options={"SKIP_SAVE"}) + def invoke(self, context, event): + return context.window_manager.invoke_confirm(self, event) + def _execute(self, context): if self.element_id != 0: element = tool.Ifc.get().by_id(self.element_id) @@ -379,6 +418,61 @@ class SetFlowDirection(bpy.types.Operator, tool.Ifc.Operator): return {"CANCELLED"} +class CycleFlowDirection(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.cycle_flow_direction" + bl_label = "Cycle Flow Direction" + bl_options = {"REGISTER", "UNDO"} + port_id: bpy.props.IntProperty() + + @classmethod + def description(cls, context, operator): + try: + port = tool.Ifc.get().by_id(operator.port_id) + if port and port.is_a("IfcDistributionPort"): + current_direction = port.FlowDirection or "NOTDEFINED" + return f"Current flow direction: {current_direction}. Click to cycle: SOURCE → SINK → SOURCEANDSINK → NOTDEFINED" + except: + pass + return "Cycle through flow directions: SOURCE → SINK → SOURCEANDSINK → NOTDEFINED → SOURCE..." + + def _execute(self, context): + port = tool.Ifc.get().by_id(self.port_id) + if not port or not port.is_a("IfcDistributionPort"): + return {"CANCELLED"} + + current_direction = port.FlowDirection or "NOTDEFINED" + flow_cycle = ["SOURCE", "SINK", "SOURCEANDSINK", "NOTDEFINED"] + + try: + current_index = flow_cycle.index(current_direction) + next_direction = flow_cycle[(current_index + 1) % len(flow_cycle)] + except ValueError: + next_direction = "SOURCE" + + tool.Ifc.run("attribute.edit_attributes", product=port, attributes={"FlowDirection": next_direction}) + + PortData.is_loaded = False + + return {"FINISHED"} + + +class ShowPortFlowError(bpy.types.Operator): + bl_idname = "bim.show_port_flow_error" + bl_label = "" + bl_options = {"REGISTER"} + port_flow: bpy.props.StringProperty() + connected_flow: bpy.props.StringProperty() + + @classmethod + def description(cls, context, operator): + if operator.port_flow and operator.connected_flow: + return f"Semantic error: Incompatible flow directions ({operator.port_flow} connected to {operator.connected_flow})" + return "Semantic error: Incompatible flow directions" + + def execute(self, context): + return {"FINISHED"} + + class LoadZones(bpy.types.Operator): bl_idname = "bim.load_zones" bl_label = "Load Zones" diff --git a/src/bonsai/bonsai/bim/module/system/prop.py b/src/bonsai/bonsai/bim/module/system/prop.py index a603446c9f..6b82d650af 100644 --- a/src/bonsai/bonsai/bim/module/system/prop.py +++ b/src/bonsai/bonsai/bim/module/system/prop.py @@ -19,7 +19,8 @@ import bpy import bonsai.bim.handler import bonsai.tool as tool -from bonsai.bim.module.system.data import SystemData +import bonsai.core.system as core +from bonsai.bim.module.system.data import SystemData, PortData import bonsai.bim.module.system.decorator as decorator from bonsai.bim.prop import StrProperty, Attribute from bpy.types import PropertyGroup @@ -92,6 +93,56 @@ def toggle_decorations(self: "BIMSystemProperties", context: bpy.types.Context) decorator.SystemDecorator.uninstall() +def is_port_available_for_connection(self: "BIMSystemProperties", obj: bpy.types.Object) -> bool: + element = tool.Ifc.get_entity(obj) + if not element or not element.is_a("IfcDistributionPort"): + return False + connected_port = tool.System.get_connected_port(element) + if connected_port is not None: + return False + + if bpy.context.active_object: + active_element = tool.Ifc.get_entity(bpy.context.active_object) + if active_element: + active_ports = tool.System.get_ports(active_element) + if element in active_ports: + return False + + return True + + +def update_related_port_object(self: "BIMSystemProperties", context: bpy.types.Context) -> None: + if self.related_port_object is None: + return + if not context.active_object: + return + + source_element = tool.Ifc.get_entity(context.active_object) + target_element = tool.Ifc.get_entity(self.related_port_object) + + if not source_element or not target_element: + self.related_port_object = None + return + + if not target_element.is_a("IfcDistributionPort"): + self.related_port_object = None + return + + source_port = None + for port in tool.System.get_ports(source_element): + if not tool.System.get_connected_port(port): + source_port = port + break + + if not source_port: + self.related_port_object = None + return + + core.connect_port(tool.Ifc, port1=source_port, port2=target_element) + self.related_port_object = None + PortData.is_loaded = False + + class BIMSystemProperties(PropertyGroup): system_attributes: CollectionProperty(name="System Attributes", type=Attribute) is_editing: BoolProperty(name="Is Editing", default=False) @@ -107,6 +158,12 @@ class BIMSystemProperties(PropertyGroup): should_draw_decorations: BoolProperty( name="Should Draw Decorations", description="Toggle system decorations", update=toggle_decorations ) + related_port_object: PointerProperty( + type=bpy.types.Object, + name="Connect To Port", + update=update_related_port_object, + poll=is_port_available_for_connection, + ) if TYPE_CHECKING: system_attributes: bpy.types.bpy_prop_collection_idprop[Attribute] @@ -119,6 +176,7 @@ class BIMSystemProperties(PropertyGroup): edited_system_id: int system_class: str should_draw_decorations: bool + related_port_object: Union[bpy.types.Object, None] @property def active_system_ui_item(self) -> Union[System, None]: diff --git a/src/bonsai/bonsai/bim/module/system/ui.py b/src/bonsai/bonsai/bim/module/system/ui.py index 2d555f24c2..3574f95469 100644 --- a/src/bonsai/bonsai/bim/module/system/ui.py +++ b/src/bonsai/bonsai/bim/module/system/ui.py @@ -162,45 +162,75 @@ class BIM_PT_ports(Panel): if total_ports == 0: return - row = self.layout.row(align=True) - row.label(text="Change Flow Direction:") - - current_flow_direction = PortData.data["selected_objects_flow_direction"] - for flow_direction in FLOW_DIRECTION_TO_ICON.keys(): - row.operator( - "bim.set_flow_direction", - icon=FLOW_DIRECTION_TO_ICON[flow_direction], - depress=flow_direction == current_flow_direction, - text="", - ).direction = flow_direction - row.enabled = len(context.selected_objects) == 2 + props = tool.System.get_system_props() row = self.layout.row(align=True) - row.label(text="Ports located on object and connected objects:") + row.label(text="Ports located on object and connected Port/Objects:") + row = self.layout.row(align=True) - cols = [row.column(align=True) for i in range(6)] + cols = [row.column(align=True) for i in range(10)] for port_data in PortData.data["located_ports_data"]: flow_direction_icon = FLOW_DIRECTION_TO_ICON[port_data["FlowDirection"] or "NOTDEFINED"] - if port_data["port_obj_name"]: - cols[0].label(text="", icon=flow_direction_icon) - cols[1].operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF").ifc_id = port_data["id"] - cols[2].label(text=port_data["port_obj_name"]) + + if port_data["connected_obj_name"]: + cols[0].operator("bim.disconnect_port", text="", icon="UNLINKED").element_id = port_data["id"] + op = cols[1].operator("bim.cycle_flow_direction", text="", icon=flow_direction_icon, emboss=True) + op.port_id = port_data["id"] else: - cols[0].label(text="", icon=flow_direction_icon) - cols[1].label(text="", icon="HIDE_ON") - cols[2].label(text="Port is hidden") + cols[0].label(text="", icon="BLANK1") + op = cols[1].operator("bim.cycle_flow_direction", text="", icon=flow_direction_icon, emboss=True) + op.port_id = port_data["id"] + + # Port information + if port_data["port_obj_name"]: + cols[2].operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF").ifc_id = port_data["id"] + cols[3].label(text=port_data["port_obj_name"]) + else: + cols[2].label(text="", icon="HIDE_ON") + cols[3].label(text="Port is hidden") if port_data["connected_obj_name"]: connected_obj = bpy.data.objects[port_data["connected_obj_name"]] - cols[3].operator("bim.disconnect_port", text="", icon="UNLINKED").element_id = port_data["id"] + + port = tool.Ifc.get().by_id(port_data["id"]) + connected_port = tool.System.get_connected_port(port) + if connected_port: + connected_port_flow_dir = FLOW_DIRECTION_TO_ICON[connected_port.FlowDirection or "NOTDEFINED"] + op = cols[4].operator("bim.cycle_flow_direction", text="", icon=connected_port_flow_dir, emboss=True) + op.port_id = connected_port.id() + cols[5].operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF").ifc_id = connected_port.id() + connected_port_obj = tool.Ifc.get_object(connected_port) + cols[6].label(text=connected_port_obj.name if connected_port_obj else "Hidden Port") + else: + cols[4].label(text="", icon="BLANK1") + cols[5].label(text="", icon="BLANK1") + cols[6].label(text="") + ifc_id = tool.Blender.get_ifc_definition_id(connected_obj) - cols[4].operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF").ifc_id = ifc_id - cols[5].label(text=port_data["connected_obj_name"]) + cols[7].operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF").ifc_id = ifc_id + cols[8].label(text=port_data["connected_obj_name"]) + + if connected_port: + port_flow = port.FlowDirection or "NOTDEFINED" + connected_flow = connected_port.FlowDirection or "NOTDEFINED" + if (port_flow == "SOURCE" and connected_flow == "SOURCE") or \ + (port_flow == "SINK" and connected_flow == "SINK"): + op = cols[9].operator("bim.show_port_flow_error", text="", icon="ERROR", emboss=False) + op.port_flow = port_flow + op.connected_flow = connected_flow + else: + cols[9].label(text="", icon="BLANK1") + else: + cols[9].label(text="", icon="BLANK1") else: - cols[3].label(text="", icon="UNLINKED") cols[4].label(text="", icon="BLANK1") - cols[5].label(text="Port is disconnected") + cols[5].label(text="", icon="BLANK1") + cols[6].alignment = 'LEFT' + cols[6].prop(props, "related_port_object", text="") + cols[7].label(text="", icon="BLANK1") + cols[8].label(text="Port is disconnected") + cols[9].label(text="", icon="BLANK1") class BIM_PT_port(Panel): @@ -227,7 +257,12 @@ class BIM_PT_port(Panel): layout = self.layout row = layout.row(align=True) - row.label(text="IfcDistributionPort") + + if not PortData.is_loaded: + PortData.load() + + relating_object_name = PortData.data["port_relating_object_name"] if PortData.data["is_port"] else "" + row.label(text=f"IfcDistributionPort located in: {relating_object_name}") row.operator("bim.connect_port", icon="PLUGIN", text="") row.operator("bim.disconnect_port", icon="UNLINKED", text="") row.operator("bim.remove_port", icon="X", text="") @@ -239,41 +274,61 @@ class BIM_PT_port(Panel): return element = tool.Ifc.get_entity(context.active_object) - current_flow_direction = str(element.FlowDirection) + props = tool.System.get_system_props() + row = layout.row(align=True) - row.label(text="Flow Direction:") - row.label(text=current_flow_direction) - - # port located on - row = layout.row(align=True) - relating_object_name = PortData.data["port_relating_object_name"] - relating_object = bpy.data.objects[relating_object_name] - row.label(text="Port located on:") - row.label(text=relating_object_name) - ifc_id = tool.Blender.get_ifc_definition_id(relating_object) - row.operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF").ifc_id = ifc_id - - # object connected to the port - row = layout.row(align=True) - connected_object_name = PortData.data["port_connected_object_name"] - if connected_object_name: - connected_object = bpy.data.objects[connected_object_name] - row.label(text="Port connected to:") - row.label(text=connected_object_name) - ifc_id = tool.Blender.get_ifc_definition_id(connected_object) - row.operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF").ifc_id = ifc_id + cols = [row.column(align=True) for i in range(10)] + + flow_direction_icon = FLOW_DIRECTION_TO_ICON[element.FlowDirection or "NOTDEFINED"] + connected_port = tool.System.get_connected_port(element) + + if connected_port: + cols[0].operator("bim.disconnect_port", text="", icon="UNLINKED") + op = cols[1].operator("bim.cycle_flow_direction", text="", icon=flow_direction_icon, emboss=True) + op.port_id = element.id() else: - row.label(text="Port is not connected to any element") - - row = layout.row(align=True) - row.label(text="Change Flow Direction:") - for flow_direction in FLOW_DIRECTION_TO_ICON.keys(): - row.operator( - "bim.set_flow_direction", - icon=FLOW_DIRECTION_TO_ICON[flow_direction], - depress=flow_direction == current_flow_direction, - text="", - ).direction = flow_direction + cols[0].label(text="", icon="BLANK1") + op = cols[1].operator("bim.cycle_flow_direction", text="", icon=flow_direction_icon, emboss=True) + op.port_id = element.id() + + cols[2].operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF").ifc_id = element.id() + cols[3].label(text=context.active_object.name) + + if connected_port: + connected_port_flow_dir = FLOW_DIRECTION_TO_ICON[connected_port.FlowDirection or "NOTDEFINED"] + op = cols[4].operator("bim.cycle_flow_direction", text="", icon=connected_port_flow_dir, emboss=True) + op.port_id = connected_port.id() + cols[5].operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF").ifc_id = connected_port.id() + connected_port_obj = tool.Ifc.get_object(connected_port) + cols[6].label(text=connected_port_obj.name if connected_port_obj else "Hidden Port") + + connected_object_name = PortData.data["port_connected_object_name"] + if connected_object_name: + connected_obj = bpy.data.objects[connected_object_name] + ifc_id = tool.Blender.get_ifc_definition_id(connected_obj) + cols[7].operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF").ifc_id = ifc_id + cols[8].label(text=connected_object_name) + else: + cols[7].label(text="", icon="BLANK1") + cols[8].label(text="") + + port_flow = element.FlowDirection or "NOTDEFINED" + connected_flow = connected_port.FlowDirection or "NOTDEFINED" + if (port_flow == "SOURCE" and connected_flow == "SOURCE") or \ + (port_flow == "SINK" and connected_flow == "SINK"): + op = cols[9].operator("bim.show_port_flow_error", text="", icon="ERROR", emboss=False) + op.port_flow = port_flow + op.connected_flow = connected_flow + else: + cols[9].label(text="", icon="BLANK1") + else: + cols[4].label(text="", icon="BLANK1") + cols[5].label(text="", icon="BLANK1") + cols[6].alignment = 'LEFT' + cols[6].prop(props, "related_port_object", text="") + cols[7].label(text="", icon="BLANK1") + cols[8].label(text="Port is disconnected") + cols[9].label(text="", icon="BLANK1") class BIM_PT_flow_controls(Panel): diff --git a/src/bonsai/bonsai/core/system.py b/src/bonsai/bonsai/core/system.py index fcdb95e72d..7a1df28280 100644 --- a/src/bonsai/bonsai/core/system.py +++ b/src/bonsai/bonsai/core/system.py @@ -123,9 +123,8 @@ def hide_ports(ifc: type[tool.Ifc], system: type[tool.System], element: ifcopens def add_port(ifc: type[tool.Ifc], system: type[tool.System], element: ifcopenshell.entity_instance) -> None: system.load_ports(element, system.get_ports(element)) - obj = system.create_empty_at_cursor_with_element_orientation(element) - port = system.run_root_assign_class(obj=obj, ifc_class="IfcDistributionPort", should_add_representation=False) - ifc.run("system.assign_port", element=element, port=port) + port = system.create_port_at_cursor(element) + system.load_ports(element, [port]) def remove_port(ifc: type[tool.Ifc], system: type[tool.System], port: ifcopenshell.entity_instance) -> None: diff --git a/src/bonsai/bonsai/tool/system.py b/src/bonsai/bonsai/tool/system.py index efaaa802c5..519136a1f8 100644 --- a/src/bonsai/bonsai/tool/system.py +++ b/src/bonsai/bonsai/tool/system.py @@ -107,6 +107,32 @@ class System(bonsai.core.tool.System): bpy.context.scene.collection.objects.link(obj) return obj + @classmethod + def create_port_at_cursor(cls, element: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance: + ifc_file = tool.Ifc.get() + element_obj = tool.Ifc.get_object(element) + + port = ifcopenshell.api.system.add_port(ifc_file, element=element) + port.FlowDirection = "NOTDEFINED" + port.PredefinedType = "USERDEFINED" + + systems = ifcopenshell.util.system.get_element_systems(element) + if systems: + system = systems[0] + if hasattr(system, "PredefinedType") and system.PredefinedType: + port.SystemType = system.PredefinedType + else: + port.SystemType = "USERDEFINED" + else: + port.SystemType = "USERDEFINED" + + matrix = element_obj.matrix_world.copy() + matrix.translation = bpy.context.scene.cursor.matrix.translation + + ifcopenshell.api.geometry.edit_object_placement(ifc_file, product=port, matrix=matrix, is_si=True) + + return port + @classmethod def delete_element_objects(cls, elements: list[ifcopenshell.entity_instance]) -> None: for element in elements: From 44632357286c09361204ebb104540623c61b8820 Mon Sep 17 00:00:00 2001 From: falken10vdl Date: Fri, 9 Jan 2026 08:43:46 +0100 Subject: [PATCH 02/11] Remove ShowPortFlowError operator and update flow direction handling for connected ports --- .../bonsai/bim/module/system/__init__.py | 1 - .../bonsai/bim/module/system/operator.py | 29 ++++++++----------- src/bonsai/bonsai/bim/module/system/ui.py | 29 ++----------------- 3 files changed, 14 insertions(+), 45 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/system/__init__.py b/src/bonsai/bonsai/bim/module/system/__init__.py index 4c805f282d..f44270cbe8 100644 --- a/src/bonsai/bonsai/bim/module/system/__init__.py +++ b/src/bonsai/bonsai/bim/module/system/__init__.py @@ -45,7 +45,6 @@ classes = ( operator.RemoveZone, operator.SelectSystemProducts, operator.SetFlowDirection, - operator.ShowPortFlowError, operator.ShowPorts, operator.UnassignSystem, operator.UnloadZones, diff --git a/src/bonsai/bonsai/bim/module/system/operator.py b/src/bonsai/bonsai/bim/module/system/operator.py index 9084f1a6b8..80c15e9799 100644 --- a/src/bonsai/bonsai/bim/module/system/operator.py +++ b/src/bonsai/bonsai/bim/module/system/operator.py @@ -451,28 +451,23 @@ class CycleFlowDirection(bpy.types.Operator, tool.Ifc.Operator): tool.Ifc.run("attribute.edit_attributes", product=port, attributes={"FlowDirection": next_direction}) + connected_port = tool.System.get_connected_port(port) + if connected_port: + # Map flow direction to connected port's complementary direction + connected_direction_map = { + "SOURCE": "SINK", + "SINK": "SOURCE", + "SOURCEANDSINK": "SOURCEANDSINK", + "NOTDEFINED": "NOTDEFINED" + } + connected_direction = connected_direction_map.get(next_direction, "NOTDEFINED") + tool.Ifc.run("attribute.edit_attributes", product=connected_port, attributes={"FlowDirection": connected_direction}) + PortData.is_loaded = False return {"FINISHED"} -class ShowPortFlowError(bpy.types.Operator): - bl_idname = "bim.show_port_flow_error" - bl_label = "" - bl_options = {"REGISTER"} - port_flow: bpy.props.StringProperty() - connected_flow: bpy.props.StringProperty() - - @classmethod - def description(cls, context, operator): - if operator.port_flow and operator.connected_flow: - return f"Semantic error: Incompatible flow directions ({operator.port_flow} connected to {operator.connected_flow})" - return "Semantic error: Incompatible flow directions" - - def execute(self, context): - return {"FINISHED"} - - class LoadZones(bpy.types.Operator): bl_idname = "bim.load_zones" bl_label = "Load Zones" diff --git a/src/bonsai/bonsai/bim/module/system/ui.py b/src/bonsai/bonsai/bim/module/system/ui.py index 3574f95469..24907f8650 100644 --- a/src/bonsai/bonsai/bim/module/system/ui.py +++ b/src/bonsai/bonsai/bim/module/system/ui.py @@ -168,7 +168,7 @@ class BIM_PT_ports(Panel): row.label(text="Ports located on object and connected Port/Objects:") row = self.layout.row(align=True) - cols = [row.column(align=True) for i in range(10)] + cols = [row.column(align=True) for i in range(9)] for port_data in PortData.data["located_ports_data"]: flow_direction_icon = FLOW_DIRECTION_TO_ICON[port_data["FlowDirection"] or "NOTDEFINED"] @@ -210,19 +210,6 @@ class BIM_PT_ports(Panel): ifc_id = tool.Blender.get_ifc_definition_id(connected_obj) cols[7].operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF").ifc_id = ifc_id cols[8].label(text=port_data["connected_obj_name"]) - - if connected_port: - port_flow = port.FlowDirection or "NOTDEFINED" - connected_flow = connected_port.FlowDirection or "NOTDEFINED" - if (port_flow == "SOURCE" and connected_flow == "SOURCE") or \ - (port_flow == "SINK" and connected_flow == "SINK"): - op = cols[9].operator("bim.show_port_flow_error", text="", icon="ERROR", emboss=False) - op.port_flow = port_flow - op.connected_flow = connected_flow - else: - cols[9].label(text="", icon="BLANK1") - else: - cols[9].label(text="", icon="BLANK1") else: cols[4].label(text="", icon="BLANK1") cols[5].label(text="", icon="BLANK1") @@ -230,7 +217,6 @@ class BIM_PT_ports(Panel): cols[6].prop(props, "related_port_object", text="") cols[7].label(text="", icon="BLANK1") cols[8].label(text="Port is disconnected") - cols[9].label(text="", icon="BLANK1") class BIM_PT_port(Panel): @@ -277,7 +263,7 @@ class BIM_PT_port(Panel): props = tool.System.get_system_props() row = layout.row(align=True) - cols = [row.column(align=True) for i in range(10)] + cols = [row.column(align=True) for i in range(9)] flow_direction_icon = FLOW_DIRECTION_TO_ICON[element.FlowDirection or "NOTDEFINED"] connected_port = tool.System.get_connected_port(element) @@ -311,16 +297,6 @@ class BIM_PT_port(Panel): else: cols[7].label(text="", icon="BLANK1") cols[8].label(text="") - - port_flow = element.FlowDirection or "NOTDEFINED" - connected_flow = connected_port.FlowDirection or "NOTDEFINED" - if (port_flow == "SOURCE" and connected_flow == "SOURCE") or \ - (port_flow == "SINK" and connected_flow == "SINK"): - op = cols[9].operator("bim.show_port_flow_error", text="", icon="ERROR", emboss=False) - op.port_flow = port_flow - op.connected_flow = connected_flow - else: - cols[9].label(text="", icon="BLANK1") else: cols[4].label(text="", icon="BLANK1") cols[5].label(text="", icon="BLANK1") @@ -328,7 +304,6 @@ class BIM_PT_port(Panel): cols[6].prop(props, "related_port_object", text="") cols[7].label(text="", icon="BLANK1") cols[8].label(text="Port is disconnected") - cols[9].label(text="", icon="BLANK1") class BIM_PT_flow_controls(Panel): From ed7ae0da9c286f63af05ff1d5ee05b71f70729ab Mon Sep 17 00:00:00 2001 From: falken10vdl Date: Fri, 9 Jan 2026 13:18:01 +0100 Subject: [PATCH 03/11] Enhance show_ports (also shows ports of connected objects) and refine UI layout --- .../bonsai/bim/module/system/operator.py | 15 +++++- src/bonsai/bonsai/bim/module/system/prop.py | 7 +-- src/bonsai/bonsai/bim/module/system/ui.py | 54 ++++++++++++++----- 3 files changed, 57 insertions(+), 19 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/system/operator.py b/src/bonsai/bonsai/bim/module/system/operator.py index 80c15e9799..e60f4500ec 100644 --- a/src/bonsai/bonsai/bim/module/system/operator.py +++ b/src/bonsai/bonsai/bim/module/system/operator.py @@ -186,6 +186,7 @@ class ShowPorts(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.show_ports" bl_label = "Show Ports" bl_options = {"REGISTER", "UNDO"} + element_id: bpy.props.IntProperty(default=0, options={"SKIP_SAVE"}) @classmethod def poll(cls, context): @@ -198,7 +199,19 @@ class ShowPorts(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): # Ifc.Operator - as operator will sync object's position with IFC. - core.show_ports(tool.Ifc, tool.System, tool.Spatial, element=tool.Ifc.get_entity(context.active_object)) + if self.element_id != 0: + element = tool.Ifc.get().by_id(self.element_id) + else: + element = tool.Ifc.get_entity(context.active_object) + core.show_ports(tool.Ifc, tool.System, tool.Spatial, element=element) + + for port in tool.System.get_ports(element): + connected_port = tool.System.get_connected_port(port) + if connected_port: + connected_port_obj = tool.Ifc.get_object(connected_port) + if not connected_port_obj: + parent_element = tool.System.get_port_relating_element(connected_port) + core.show_ports(tool.Ifc, tool.System, tool.Spatial, element=parent_element) class HidePorts(bpy.types.Operator, tool.Ifc.Operator): diff --git a/src/bonsai/bonsai/bim/module/system/prop.py b/src/bonsai/bonsai/bim/module/system/prop.py index 6b82d650af..09d9bf5e38 100644 --- a/src/bonsai/bonsai/bim/module/system/prop.py +++ b/src/bonsai/bonsai/bim/module/system/prop.py @@ -121,11 +121,9 @@ def update_related_port_object(self: "BIMSystemProperties", context: bpy.types.C target_element = tool.Ifc.get_entity(self.related_port_object) if not source_element or not target_element: - self.related_port_object = None return if not target_element.is_a("IfcDistributionPort"): - self.related_port_object = None return source_port = None @@ -135,11 +133,9 @@ def update_related_port_object(self: "BIMSystemProperties", context: bpy.types.C break if not source_port: - self.related_port_object = None return core.connect_port(tool.Ifc, port1=source_port, port2=target_element) - self.related_port_object = None PortData.is_loaded = False @@ -161,8 +157,9 @@ class BIMSystemProperties(PropertyGroup): related_port_object: PointerProperty( type=bpy.types.Object, name="Connect To Port", - update=update_related_port_object, + description="Select a port to connect to. Use eyedropper to pick a port object", poll=is_port_available_for_connection, + update=update_related_port_object, ) if TYPE_CHECKING: diff --git a/src/bonsai/bonsai/bim/module/system/ui.py b/src/bonsai/bonsai/bim/module/system/ui.py index 24907f8650..dee872b463 100644 --- a/src/bonsai/bonsai/bim/module/system/ui.py +++ b/src/bonsai/bonsai/bim/module/system/ui.py @@ -169,6 +169,9 @@ class BIM_PT_ports(Panel): row = self.layout.row(align=True) cols = [row.column(align=True) for i in range(9)] + cols[3].scale_x = 1.0 + cols[6].scale_x = 1.0 + cols[8].scale_x = 1.33 for port_data in PortData.data["located_ports_data"]: flow_direction_icon = FLOW_DIRECTION_TO_ICON[port_data["FlowDirection"] or "NOTDEFINED"] @@ -178,7 +181,9 @@ class BIM_PT_ports(Panel): op = cols[1].operator("bim.cycle_flow_direction", text="", icon=flow_direction_icon, emboss=True) op.port_id = port_data["id"] else: - cols[0].label(text="", icon="BLANK1") + blank0 = cols[0].column(align=True) + blank0.scale_x = 0.1 + blank0.label(text="", icon="BLANK1") op = cols[1].operator("bim.cycle_flow_direction", text="", icon=flow_direction_icon, emboss=True) op.port_id = port_data["id"] @@ -196,26 +201,49 @@ class BIM_PT_ports(Panel): port = tool.Ifc.get().by_id(port_data["id"]) connected_port = tool.System.get_connected_port(port) if connected_port: - connected_port_flow_dir = FLOW_DIRECTION_TO_ICON[connected_port.FlowDirection or "NOTDEFINED"] - op = cols[4].operator("bim.cycle_flow_direction", text="", icon=connected_port_flow_dir, emboss=True) - op.port_id = connected_port.id() - cols[5].operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF").ifc_id = connected_port.id() connected_port_obj = tool.Ifc.get_object(connected_port) - cols[6].label(text=connected_port_obj.name if connected_port_obj else "Hidden Port") + if connected_port_obj: + connected_port_flow_dir = FLOW_DIRECTION_TO_ICON[connected_port.FlowDirection or "NOTDEFINED"] + op = cols[4].operator("bim.cycle_flow_direction", text="", icon=connected_port_flow_dir, emboss=True) + op.port_id = connected_port.id() + cols[5].operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF").ifc_id = connected_port.id() + cols[6].label(text=connected_port_obj.name) + else: + blank4 = cols[4].column(align=True) + blank4.scale_x = 0.1 + blank4.label(text="", icon="BLANK1") + blank5 = cols[5].column(align=True) + blank5.scale_x = 0.1 + blank5.label(text="", icon="BLANK1") + cols[6].label(text="Port is hidden") else: - cols[4].label(text="", icon="BLANK1") - cols[5].label(text="", icon="BLANK1") + blank4 = cols[4].column(align=True) + blank4.scale_x = 0.1 + blank4.label(text="", icon="BLANK1") + blank5 = cols[5].column(align=True) + blank5.scale_x = 0.1 + blank5.label(text="", icon="BLANK1") cols[6].label(text="") ifc_id = tool.Blender.get_ifc_definition_id(connected_obj) cols[7].operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF").ifc_id = ifc_id cols[8].label(text=port_data["connected_obj_name"]) else: - cols[4].label(text="", icon="BLANK1") - cols[5].label(text="", icon="BLANK1") - cols[6].alignment = 'LEFT' - cols[6].prop(props, "related_port_object", text="") - cols[7].label(text="", icon="BLANK1") + blank4 = cols[4].column(align=True) + blank4.scale_x = 0.1 + blank4.label(text="", icon="BLANK1") + blank5 = cols[5].column(align=True) + blank5.scale_x = 0.1 + blank5.label(text="", icon="BLANK1") + + col6_row = cols[6].row(align=True) + col6_row.alignment = 'LEFT' + col6_row.prop(props, "related_port_object", text="", icon='OBJECT_DATA') + col6_row.scale_x = 0.35 + + blank7 = cols[7].column(align=True) + blank7.scale_x = 0.1 + blank7.label(text="", icon="BLANK1") cols[8].label(text="Port is disconnected") From c41496a9c3f1178f3804e68f3bb1d3be577c8b25 Mon Sep 17 00:00:00 2001 From: falken10vdl Date: Fri, 9 Jan 2026 17:11:02 +0100 Subject: [PATCH 04/11] import IfcDistributionPorts with assets when executing append_library_element and for type products, use the type product's collection, not the container --- src/bonsai/bonsai/tool/system.py | 21 ++++++++++++++----- .../ifcopenshell/api/project/append_asset.py | 7 ++++++- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/src/bonsai/bonsai/tool/system.py b/src/bonsai/bonsai/tool/system.py index 519136a1f8..38a93a3649 100644 --- a/src/bonsai/bonsai/tool/system.py +++ b/src/bonsai/bonsai/tool/system.py @@ -218,11 +218,22 @@ class System(bonsai.core.tool.System): ifc_importer.process_context_filter() ifc_importer.create_generic_elements(set(ports_to_create)) - container = ifcopenshell.util.element.get_container(element) - if container: - collection = tool.Blender.get_object_bim_props(tool.Ifc.get_object(container)).collection - ifc_importer.collections[container.GlobalId] = collection - ifc_importer.place_objects_in_collections() + if element.is_a("IfcTypeProduct"): + target_collection = None + for collection in obj.users_collection: + target_collection = collection + break + + if target_collection: + for port_obj in ifc_importer.added_data.values(): + if isinstance(port_obj, bpy.types.Object): + tool.Collector.link_collection_object_safe(target_collection, port_obj) + else: + container = ifcopenshell.util.element.get_container(element) + if container: + collection = tool.Blender.get_object_bim_props(tool.Ifc.get_object(container)).collection + ifc_importer.collections[container.GlobalId] = collection + ifc_importer.place_objects_in_collections() for port_obj in ifc_importer.added_data.values(): assert isinstance(port_obj, bpy.types.Object) diff --git a/src/ifcopenshell-python/ifcopenshell/api/project/append_asset.py b/src/ifcopenshell-python/ifcopenshell/api/project/append_asset.py index 893b32cc02..0b703b6064 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/project/append_asset.py +++ b/src/ifcopenshell-python/ifcopenshell/api/project/append_asset.py @@ -381,6 +381,7 @@ class Usecase: def append_type_product(self): self.whitelisted_inverse_attributes = { "IfcObjectDefinition": ["HasAssociations"], + "IfcDistributionElementType": ["IsNestedBy"], self.base_material_class: ["HasExternalReferences", "HasProperties", "HasRepresentation"], "IfcRepresentationItem": ["StyledByItem", "LayerAssignment"], "IfcRepresentation": ["LayerAssignments"], @@ -397,6 +398,7 @@ class Usecase: "IfcObjectDefinition": ["HasAssociations"], "IfcObject": ["IsDefinedBy.IfcRelDefinesByProperties"], "IfcElement": ["HasOpenings"], + "IfcDistributionElement": ["IsNestedBy"], self.base_material_class: ["HasExternalReferences", "HasProperties", "HasRepresentation"], "IfcRepresentationItem": [ "StyledByItem", @@ -488,7 +490,8 @@ class Usecase: attribute_class = None if "." in attribute: attribute, attribute_class = attribute.split(".") - for inverse in getattr(element, attribute, []): + inverse_values = getattr(element, attribute, []) + for inverse in inverse_values: if attribute_class and inverse.is_a(attribute_class): self.add_inverse_element(inverse) elif not attribute_class: @@ -569,6 +572,8 @@ class Usecase: return False elif element.is_a("IfcRoot") and self.by_guid(element.GlobalId) is not None: return False + elif element.is_a("IfcDistributionPort"): + return False elif element.is_a(self.target_class): return True elif self.target_class == "IfcProduct" and element.is_a("IfcTypeProduct"): From dfbaa6ac8ee368d5b178919a3340aaa9962c331b Mon Sep 17 00:00:00 2001 From: falken10vdl Date: Fri, 9 Jan 2026 17:39:42 +0100 Subject: [PATCH 05/11] Reset related_port_object to None after connecting ports --- src/bonsai/bonsai/bim/module/system/prop.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/bonsai/bonsai/bim/module/system/prop.py b/src/bonsai/bonsai/bim/module/system/prop.py index 09d9bf5e38..6e9681694b 100644 --- a/src/bonsai/bonsai/bim/module/system/prop.py +++ b/src/bonsai/bonsai/bim/module/system/prop.py @@ -137,6 +137,7 @@ def update_related_port_object(self: "BIMSystemProperties", context: bpy.types.C core.connect_port(tool.Ifc, port1=source_port, port2=target_element) PortData.is_loaded = False + self.related_port_object = None class BIMSystemProperties(PropertyGroup): From 7ef11361b0625b8e1e32432a60b86eca76ba058c Mon Sep 17 00:00:00 2001 From: falken10vdl Date: Sat, 10 Jan 2026 10:12:25 +0100 Subject: [PATCH 06/11] Update flow direction icons for ports in UI --- src/bonsai/bonsai/bim/module/system/ui.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/system/ui.py b/src/bonsai/bonsai/bim/module/system/ui.py index dee872b463..6e71e5486f 100644 --- a/src/bonsai/bonsai/bim/module/system/ui.py +++ b/src/bonsai/bonsai/bim/module/system/ui.py @@ -30,10 +30,10 @@ if TYPE_CHECKING: FLOW_DIRECTION_TO_ICON = { - "SOURCE": "FORWARD", - "SINK": "BACK", - "SOURCEANDSINK": "ARROW_LEFTRIGHT", - "NOTDEFINED": "CHECKBOX_DEHLT", + "SOURCE": "FULLSCREEN_ENTER", + "SINK": "FULLSCREEN_EXIT", + "SOURCEANDSINK": "CHECKBOX_DEHLT", + "NOTDEFINED": "QUESTION", } From 0243519a68445eab760d68b954b1abd7ed8933f5 Mon Sep 17 00:00:00 2001 From: falken10vdl Date: Sun, 11 Jan 2026 14:22:58 +0100 Subject: [PATCH 07/11] Refactor port connection logic and UI integration for improved usability --- .../bonsai/bim/module/system/operator.py | 64 ++++++++--------- src/bonsai/bonsai/bim/module/system/prop.py | 71 ++++++------------- src/bonsai/bonsai/bim/module/system/ui.py | 14 ++-- src/bonsai/bonsai/tool/system.py | 10 +-- 4 files changed, 58 insertions(+), 101 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/system/operator.py b/src/bonsai/bonsai/bim/module/system/operator.py index e60f4500ec..7f298064fc 100644 --- a/src/bonsai/bonsai/bim/module/system/operator.py +++ b/src/bonsai/bonsai/bim/module/system/operator.py @@ -265,37 +265,31 @@ class ConnectPort(bpy.types.Operator, tool.Ifc.Operator): class AddRelatedPortConnection(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.add_related_port_connection" bl_label = "Connect Port" - bl_description = "Click to select a port to connect to" bl_options = {"REGISTER", "UNDO"} - - element_id: bpy.props.IntProperty(default=0, options={"SKIP_SAVE"}) - + + relating_port_id: bpy.props.IntProperty() + def invoke(self, context, event): - if self.element_id == 0: - self.report({'ERROR'}, "No port specified") - return {'CANCELLED'} - context.window_manager.modal_handler_add(self) - return {'RUNNING_MODAL'} - - def modal(self, context, event): - if event.type == 'LEFTMOUSE' and event.value == 'PRESS': - if context.active_object: - target_element = tool.Ifc.get_entity(context.active_object) - if target_element and target_element.is_a("IfcDistributionPort"): - source_element = tool.Ifc.get().by_id(self.element_id) - core.connect_port(tool.Ifc, port1=source_element, port2=target_element) - self.report({'INFO'}, "Ports connected") - return {'FINISHED'} - else: - self.report({'WARNING'}, "Selected object is not a port") - return {'RUNNING_MODAL'} - elif event.type in {'RIGHTMOUSE', 'ESC'}: - self.report({'INFO'}, "Cancelled") - return {'CANCELLED'} - return {'RUNNING_MODAL'} - + return context.window_manager.invoke_props_dialog(self) + + def draw(self, context): + props = tool.System.get_system_props() + self.layout.prop(props, "related_port", text="Select Port") + def _execute(self, context): - return {'CANCELLED'} + props = tool.System.get_system_props() + + if not props.related_port or props.related_port == "NONE": + return {"CANCELLED"} + + port_obj = bpy.data.objects.get(props.related_port) + related_port = tool.Ifc.get_entity(port_obj) + relating_port = tool.Ifc.get().by_id(self.relating_port_id) + + core.connect_port(tool.Ifc, port1=relating_port, port2=related_port) + PortData.is_loaded = False + + return {"FINISHED"} class DisconnectPort(bpy.types.Operator, tool.Ifc.Operator): @@ -454,19 +448,19 @@ class CycleFlowDirection(bpy.types.Operator, tool.Ifc.Operator): return {"CANCELLED"} current_direction = port.FlowDirection or "NOTDEFINED" - flow_cycle = ["SOURCE", "SINK", "SOURCEANDSINK", "NOTDEFINED"] - try: - current_index = flow_cycle.index(current_direction) - next_direction = flow_cycle[(current_index + 1) % len(flow_cycle)] - except ValueError: - next_direction = "SOURCE" + flow_cycle_map = { + "SOURCE": "SINK", + "SINK": "SOURCEANDSINK", + "SOURCEANDSINK": "NOTDEFINED", + "NOTDEFINED": "SOURCE" + } + next_direction = flow_cycle_map.get(current_direction, "SOURCE") tool.Ifc.run("attribute.edit_attributes", product=port, attributes={"FlowDirection": next_direction}) connected_port = tool.System.get_connected_port(port) if connected_port: - # Map flow direction to connected port's complementary direction connected_direction_map = { "SOURCE": "SINK", "SINK": "SOURCE", diff --git a/src/bonsai/bonsai/bim/module/system/prop.py b/src/bonsai/bonsai/bim/module/system/prop.py index 6e9681694b..bb72fda1a9 100644 --- a/src/bonsai/bonsai/bim/module/system/prop.py +++ b/src/bonsai/bonsai/bim/module/system/prop.py @@ -34,7 +34,7 @@ from bpy.props import ( FloatVectorProperty, CollectionProperty, ) -from typing import TYPE_CHECKING, Union +from typing import TYPE_CHECKING, Union, Optional def get_system_class(self: "BIMSystemProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]: @@ -93,51 +93,24 @@ def toggle_decorations(self: "BIMSystemProperties", context: bpy.types.Context) decorator.SystemDecorator.uninstall() -def is_port_available_for_connection(self: "BIMSystemProperties", obj: bpy.types.Object) -> bool: - element = tool.Ifc.get_entity(obj) - if not element or not element.is_a("IfcDistributionPort"): - return False - connected_port = tool.System.get_connected_port(element) - if connected_port is not None: - return False +def get_available_ports_for_connection(self: "BIMSystemProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]: + items = [] + active_object_ports = set(tool.System.get_ports(tool.Ifc.get_entity(context.active_object))) - if bpy.context.active_object: - active_element = tool.Ifc.get_entity(bpy.context.active_object) - if active_element: - active_ports = tool.System.get_ports(active_element) - if element in active_ports: - return False + ifc_file = tool.Ifc.get() + for ifc_port in ifc_file.by_type("IfcDistributionPort"): + port = tool.Ifc.get_object(ifc_port) + if not port: + continue + + if tool.System.get_connected_port(ifc_port) is not None or ifc_port in active_object_ports: + continue + + port_object = tool.Ifc.get_object(tool.System.get_port_relating_element(ifc_port)) + suggestion = f"{port_object.name} > {port.name}" + items.append((port.name, suggestion, "")) - return True - - -def update_related_port_object(self: "BIMSystemProperties", context: bpy.types.Context) -> None: - if self.related_port_object is None: - return - if not context.active_object: - return - - source_element = tool.Ifc.get_entity(context.active_object) - target_element = tool.Ifc.get_entity(self.related_port_object) - - if not source_element or not target_element: - return - - if not target_element.is_a("IfcDistributionPort"): - return - - source_port = None - for port in tool.System.get_ports(source_element): - if not tool.System.get_connected_port(port): - source_port = port - break - - if not source_port: - return - - core.connect_port(tool.Ifc, port1=source_port, port2=target_element) - PortData.is_loaded = False - self.related_port_object = None + return items if items else [("NONE", "Ports are hidden or not available", "")] class BIMSystemProperties(PropertyGroup): @@ -155,12 +128,10 @@ class BIMSystemProperties(PropertyGroup): should_draw_decorations: BoolProperty( name="Should Draw Decorations", description="Toggle system decorations", update=toggle_decorations ) - related_port_object: PointerProperty( - type=bpy.types.Object, + related_port: EnumProperty( name="Connect To Port", - description="Select a port to connect to. Use eyedropper to pick a port object", - poll=is_port_available_for_connection, - update=update_related_port_object, + description="Select a port to connect to", + items=get_available_ports_for_connection, ) if TYPE_CHECKING: @@ -174,7 +145,7 @@ class BIMSystemProperties(PropertyGroup): edited_system_id: int system_class: str should_draw_decorations: bool - related_port_object: Union[bpy.types.Object, None] + related_port: str @property def active_system_ui_item(self) -> Union[System, None]: diff --git a/src/bonsai/bonsai/bim/module/system/ui.py b/src/bonsai/bonsai/bim/module/system/ui.py index 6e71e5486f..c4b142c31f 100644 --- a/src/bonsai/bonsai/bim/module/system/ui.py +++ b/src/bonsai/bonsai/bim/module/system/ui.py @@ -181,9 +181,8 @@ class BIM_PT_ports(Panel): op = cols[1].operator("bim.cycle_flow_direction", text="", icon=flow_direction_icon, emboss=True) op.port_id = port_data["id"] else: - blank0 = cols[0].column(align=True) - blank0.scale_x = 0.1 - blank0.label(text="", icon="BLANK1") + op = cols[0].operator("bim.add_related_port_connection", text="", icon='PLUGIN') + op.relating_port_id = port_data["id"] op = cols[1].operator("bim.cycle_flow_direction", text="", icon=flow_direction_icon, emboss=True) op.port_id = port_data["id"] @@ -236,15 +235,14 @@ class BIM_PT_ports(Panel): blank5.scale_x = 0.1 blank5.label(text="", icon="BLANK1") - col6_row = cols[6].row(align=True) - col6_row.alignment = 'LEFT' - col6_row.prop(props, "related_port_object", text="", icon='OBJECT_DATA') - col6_row.scale_x = 0.35 + cols[6].label(text="Port is disconnected") blank7 = cols[7].column(align=True) blank7.scale_x = 0.1 blank7.label(text="", icon="BLANK1") - cols[8].label(text="Port is disconnected") + blank8 = cols[8].column(align=True) + blank8.scale_x = 0.1 + blank8.label(text="", icon="BLANK1") class BIM_PT_port(Panel): diff --git a/src/bonsai/bonsai/tool/system.py b/src/bonsai/bonsai/tool/system.py index 38a93a3649..0236b40fbd 100644 --- a/src/bonsai/bonsai/tool/system.py +++ b/src/bonsai/bonsai/tool/system.py @@ -117,14 +117,8 @@ class System(bonsai.core.tool.System): port.PredefinedType = "USERDEFINED" systems = ifcopenshell.util.system.get_element_systems(element) - if systems: - system = systems[0] - if hasattr(system, "PredefinedType") and system.PredefinedType: - port.SystemType = system.PredefinedType - else: - port.SystemType = "USERDEFINED" - else: - port.SystemType = "USERDEFINED" + system = systems[0] if systems else None + port.SystemType = getattr(system, "PredefinedType", None) or "USERDEFINED" matrix = element_obj.matrix_world.copy() matrix.translation = bpy.context.scene.cursor.matrix.translation From e5ccd26762fe03b56e2a874da91880edfeb46d62 Mon Sep 17 00:00:00 2001 From: falken10vdl Date: Sun, 11 Jan 2026 14:46:17 +0100 Subject: [PATCH 08/11] Update port UI to indicate disconnection status more clearly --- src/bonsai/bonsai/bim/module/system/ui.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/system/ui.py b/src/bonsai/bonsai/bim/module/system/ui.py index c4b142c31f..ad8d5b5c03 100644 --- a/src/bonsai/bonsai/bim/module/system/ui.py +++ b/src/bonsai/bonsai/bim/module/system/ui.py @@ -326,10 +326,9 @@ class BIM_PT_port(Panel): else: cols[4].label(text="", icon="BLANK1") cols[5].label(text="", icon="BLANK1") - cols[6].alignment = 'LEFT' - cols[6].prop(props, "related_port_object", text="") + cols[6].label(text="Port is disconnected") cols[7].label(text="", icon="BLANK1") - cols[8].label(text="Port is disconnected") + cols[8].label(text="") class BIM_PT_flow_controls(Panel): From 1eb2b65268989a072bb994a73821c4ed7c2f1a51 Mon Sep 17 00:00:00 2001 From: falken10vdl Date: Mon, 12 Jan 2026 12:19:13 +0100 Subject: [PATCH 09/11] Add flow direction handling to port connection logic and UI updates --- src/bonsai/bonsai/bim/module/system/operator.py | 9 ++++++--- src/bonsai/bonsai/bim/module/system/prop.py | 4 ++-- src/bonsai/bonsai/bim/module/system/ui.py | 7 ++++--- src/bonsai/bonsai/core/system.py | 4 ++-- .../ifcopenshell/api/system/disconnect_port.py | 2 -- 5 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/system/operator.py b/src/bonsai/bonsai/bim/module/system/operator.py index 7f298064fc..049307b32f 100644 --- a/src/bonsai/bonsai/bim/module/system/operator.py +++ b/src/bonsai/bonsai/bim/module/system/operator.py @@ -259,7 +259,8 @@ class ConnectPort(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj1 = context.active_object obj2 = context.selected_objects[0] if context.selected_objects[1] == obj1 else context.selected_objects[1] - core.connect_port(tool.Ifc, port1=tool.Ifc.get_entity(obj1), port2=tool.Ifc.get_entity(obj2)) + direction = tool.Ifc.get_entity(obj1).FlowDirection or "NOTDEFINED" + core.connect_port(tool.Ifc, port1=tool.Ifc.get_entity(obj1), port2=tool.Ifc.get_entity(obj2), direction=direction) class AddRelatedPortConnection(bpy.types.Operator, tool.Ifc.Operator): @@ -286,7 +287,8 @@ class AddRelatedPortConnection(bpy.types.Operator, tool.Ifc.Operator): related_port = tool.Ifc.get_entity(port_obj) relating_port = tool.Ifc.get().by_id(self.relating_port_id) - core.connect_port(tool.Ifc, port1=relating_port, port2=related_port) + direction = relating_port.FlowDirection or "NOTDEFINED" + core.connect_port(tool.Ifc, port1=relating_port, port2=related_port, direction=direction) PortData.is_loaded = False return {"FINISHED"} @@ -356,7 +358,8 @@ class MEPConnectElements(bpy.types.Operator, tool.Ifc.Operator): ports_distance[(port1, port2)] = distance closest_ports = min(ports_distance, key=lambda x: ports_distance[x]) - core.connect_port(tool.Ifc, *closest_ports) + direction = closest_ports[0].FlowDirection or "NOTDEFINED" + core.connect_port(tool.Ifc, *closest_ports, direction=direction) bpy.ops.bim.regenerate_distribution_element() return {"FINISHED"} diff --git a/src/bonsai/bonsai/bim/module/system/prop.py b/src/bonsai/bonsai/bim/module/system/prop.py index bb72fda1a9..e4e56a79a0 100644 --- a/src/bonsai/bonsai/bim/module/system/prop.py +++ b/src/bonsai/bonsai/bim/module/system/prop.py @@ -20,7 +20,7 @@ import bpy import bonsai.bim.handler import bonsai.tool as tool import bonsai.core.system as core -from bonsai.bim.module.system.data import SystemData, PortData +from bonsai.bim.module.system.data import SystemData import bonsai.bim.module.system.decorator as decorator from bonsai.bim.prop import StrProperty, Attribute from bpy.types import PropertyGroup @@ -34,7 +34,7 @@ from bpy.props import ( FloatVectorProperty, CollectionProperty, ) -from typing import TYPE_CHECKING, Union, Optional +from typing import TYPE_CHECKING, Union def get_system_class(self: "BIMSystemProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]: diff --git a/src/bonsai/bonsai/bim/module/system/ui.py b/src/bonsai/bonsai/bim/module/system/ui.py index ad8d5b5c03..5e904a3206 100644 --- a/src/bonsai/bonsai/bim/module/system/ui.py +++ b/src/bonsai/bonsai/bim/module/system/ui.py @@ -275,8 +275,6 @@ class BIM_PT_port(Panel): relating_object_name = PortData.data["port_relating_object_name"] if PortData.data["is_port"] else "" row.label(text=f"IfcDistributionPort located in: {relating_object_name}") - row.operator("bim.connect_port", icon="PLUGIN", text="") - row.operator("bim.disconnect_port", icon="UNLINKED", text="") row.operator("bim.remove_port", icon="X", text="") if not PortData.is_loaded: @@ -290,6 +288,9 @@ class BIM_PT_port(Panel): row = layout.row(align=True) cols = [row.column(align=True) for i in range(9)] + cols[3].scale_x = 1.0 + cols[6].scale_x = 1.0 + cols[8].scale_x = 1.33 flow_direction_icon = FLOW_DIRECTION_TO_ICON[element.FlowDirection or "NOTDEFINED"] connected_port = tool.System.get_connected_port(element) @@ -299,7 +300,7 @@ class BIM_PT_port(Panel): op = cols[1].operator("bim.cycle_flow_direction", text="", icon=flow_direction_icon, emboss=True) op.port_id = element.id() else: - cols[0].label(text="", icon="BLANK1") + cols[0].operator("bim.connect_port", icon="PLUGIN", text="") op = cols[1].operator("bim.cycle_flow_direction", text="", icon=flow_direction_icon, emboss=True) op.port_id = element.id() diff --git a/src/bonsai/bonsai/core/system.py b/src/bonsai/bonsai/core/system.py index 7a1df28280..87a43307b7 100644 --- a/src/bonsai/bonsai/core/system.py +++ b/src/bonsai/bonsai/core/system.py @@ -132,8 +132,8 @@ def remove_port(ifc: type[tool.Ifc], system: type[tool.System], port: ifcopenshe ifc.run("root.remove_product", product=port) -def connect_port(ifc: type[tool.Ifc], port1: ifcopenshell.entity_instance, port2: ifcopenshell.entity_instance) -> None: - ifc.run("system.connect_port", port1=port1, port2=port2) +def connect_port(ifc: type[tool.Ifc], port1: ifcopenshell.entity_instance, port2: ifcopenshell.entity_instance, direction: str = "NOTDEFINED") -> None: + ifc.run("system.connect_port", port1=port1, port2=port2, direction=direction) def disconnect_port(ifc: type[tool.Ifc], port: ifcopenshell.entity_instance) -> None: diff --git a/src/ifcopenshell-python/ifcopenshell/api/system/disconnect_port.py b/src/ifcopenshell-python/ifcopenshell/api/system/disconnect_port.py index 8ffbfcf380..c53e4ae160 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/system/disconnect_port.py +++ b/src/ifcopenshell-python/ifcopenshell/api/system/disconnect_port.py @@ -66,8 +66,6 @@ def disconnect_port(file: ifcopenshell.file, port: ifcopenshell.entity_instance) rels += port.ConnectedFrom or () for rel in rels: - rel.RelatingPort.FlowDirection = None - rel.RelatedPort.FlowDirection = None history = rel.OwnerHistory file.remove(rel) if history: From 791a41d6496a8a64ce2585577beb4e5d2c5386bd Mon Sep 17 00:00:00 2001 From: falken10vdl Date: Tue, 13 Jan 2026 08:49:57 +0100 Subject: [PATCH 10/11] cleanup and black . --- .../bonsai/bim/module/system/operator.py | 55 +++++++++---------- src/bonsai/bonsai/bim/module/system/prop.py | 13 +++-- src/bonsai/bonsai/bim/module/system/ui.py | 55 +++++++++---------- src/bonsai/bonsai/core/system.py | 7 ++- src/bonsai/bonsai/tool/system.py | 1 + .../ifcopenshell/api/project/append_asset.py | 3 +- 6 files changed, 66 insertions(+), 68 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/system/operator.py b/src/bonsai/bonsai/bim/module/system/operator.py index 049307b32f..066e466bd0 100644 --- a/src/bonsai/bonsai/bim/module/system/operator.py +++ b/src/bonsai/bonsai/bim/module/system/operator.py @@ -186,7 +186,6 @@ class ShowPorts(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.show_ports" bl_label = "Show Ports" bl_options = {"REGISTER", "UNDO"} - element_id: bpy.props.IntProperty(default=0, options={"SKIP_SAVE"}) @classmethod def poll(cls, context): @@ -199,12 +198,9 @@ class ShowPorts(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): # Ifc.Operator - as operator will sync object's position with IFC. - if self.element_id != 0: - element = tool.Ifc.get().by_id(self.element_id) - else: - element = tool.Ifc.get_entity(context.active_object) + element = tool.Ifc.get_entity(context.active_object) core.show_ports(tool.Ifc, tool.System, tool.Spatial, element=element) - + for port in tool.System.get_ports(element): connected_port = tool.System.get_connected_port(port) if connected_port: @@ -260,37 +256,39 @@ class ConnectPort(bpy.types.Operator, tool.Ifc.Operator): obj1 = context.active_object obj2 = context.selected_objects[0] if context.selected_objects[1] == obj1 else context.selected_objects[1] direction = tool.Ifc.get_entity(obj1).FlowDirection or "NOTDEFINED" - core.connect_port(tool.Ifc, port1=tool.Ifc.get_entity(obj1), port2=tool.Ifc.get_entity(obj2), direction=direction) + core.connect_port( + tool.Ifc, port1=tool.Ifc.get_entity(obj1), port2=tool.Ifc.get_entity(obj2), direction=direction + ) class AddRelatedPortConnection(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.add_related_port_connection" bl_label = "Connect Port" bl_options = {"REGISTER", "UNDO"} - + relating_port_id: bpy.props.IntProperty() - + def invoke(self, context, event): return context.window_manager.invoke_props_dialog(self) - + def draw(self, context): props = tool.System.get_system_props() self.layout.prop(props, "related_port", text="Select Port") - + def _execute(self, context): props = tool.System.get_system_props() - + if not props.related_port or props.related_port == "NONE": return {"CANCELLED"} - + port_obj = bpy.data.objects.get(props.related_port) related_port = tool.Ifc.get_entity(port_obj) relating_port = tool.Ifc.get().by_id(self.relating_port_id) - + direction = relating_port.FlowDirection or "NOTDEFINED" core.connect_port(tool.Ifc, port1=relating_port, port2=related_port, direction=direction) PortData.is_loaded = False - + return {"FINISHED"} @@ -436,13 +434,10 @@ class CycleFlowDirection(bpy.types.Operator, tool.Ifc.Operator): @classmethod def description(cls, context, operator): - try: - port = tool.Ifc.get().by_id(operator.port_id) - if port and port.is_a("IfcDistributionPort"): - current_direction = port.FlowDirection or "NOTDEFINED" - return f"Current flow direction: {current_direction}. Click to cycle: SOURCE → SINK → SOURCEANDSINK → NOTDEFINED" - except: - pass + port = tool.Ifc.get().by_id(operator.port_id) + if port and port.is_a("IfcDistributionPort"): + current_direction = port.FlowDirection or "NOTDEFINED" + return f"Current flow direction: {current_direction}. Click to cycle: SOURCE → SINK → SOURCEANDSINK → NOTDEFINED" return "Cycle through flow directions: SOURCE → SINK → SOURCEANDSINK → NOTDEFINED → SOURCE..." def _execute(self, context): @@ -451,30 +446,32 @@ class CycleFlowDirection(bpy.types.Operator, tool.Ifc.Operator): return {"CANCELLED"} current_direction = port.FlowDirection or "NOTDEFINED" - + flow_cycle_map = { "SOURCE": "SINK", "SINK": "SOURCEANDSINK", "SOURCEANDSINK": "NOTDEFINED", - "NOTDEFINED": "SOURCE" + "NOTDEFINED": "SOURCE", } next_direction = flow_cycle_map.get(current_direction, "SOURCE") tool.Ifc.run("attribute.edit_attributes", product=port, attributes={"FlowDirection": next_direction}) - + connected_port = tool.System.get_connected_port(port) if connected_port: connected_direction_map = { "SOURCE": "SINK", "SINK": "SOURCE", "SOURCEANDSINK": "SOURCEANDSINK", - "NOTDEFINED": "NOTDEFINED" + "NOTDEFINED": "NOTDEFINED", } connected_direction = connected_direction_map.get(next_direction, "NOTDEFINED") - tool.Ifc.run("attribute.edit_attributes", product=connected_port, attributes={"FlowDirection": connected_direction}) - + tool.Ifc.run( + "attribute.edit_attributes", product=connected_port, attributes={"FlowDirection": connected_direction} + ) + PortData.is_loaded = False - + return {"FINISHED"} diff --git a/src/bonsai/bonsai/bim/module/system/prop.py b/src/bonsai/bonsai/bim/module/system/prop.py index e4e56a79a0..8954ab67f2 100644 --- a/src/bonsai/bonsai/bim/module/system/prop.py +++ b/src/bonsai/bonsai/bim/module/system/prop.py @@ -19,7 +19,6 @@ import bpy import bonsai.bim.handler import bonsai.tool as tool -import bonsai.core.system as core from bonsai.bim.module.system.data import SystemData import bonsai.bim.module.system.decorator as decorator from bonsai.bim.prop import StrProperty, Attribute @@ -93,23 +92,25 @@ def toggle_decorations(self: "BIMSystemProperties", context: bpy.types.Context) decorator.SystemDecorator.uninstall() -def get_available_ports_for_connection(self: "BIMSystemProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]: +def get_available_ports_for_connection( + self: "BIMSystemProperties", context: bpy.types.Context +) -> list[tuple[str, str, str]]: items = [] active_object_ports = set(tool.System.get_ports(tool.Ifc.get_entity(context.active_object))) - + ifc_file = tool.Ifc.get() for ifc_port in ifc_file.by_type("IfcDistributionPort"): port = tool.Ifc.get_object(ifc_port) if not port: continue - + if tool.System.get_connected_port(ifc_port) is not None or ifc_port in active_object_ports: continue - + port_object = tool.Ifc.get_object(tool.System.get_port_relating_element(ifc_port)) suggestion = f"{port_object.name} > {port.name}" items.append((port.name, suggestion, "")) - + return items if items else [("NONE", "Ports are hidden or not available", "")] diff --git a/src/bonsai/bonsai/bim/module/system/ui.py b/src/bonsai/bonsai/bim/module/system/ui.py index 5e904a3206..e41295f40e 100644 --- a/src/bonsai/bonsai/bim/module/system/ui.py +++ b/src/bonsai/bonsai/bim/module/system/ui.py @@ -162,11 +162,9 @@ class BIM_PT_ports(Panel): if total_ports == 0: return - props = tool.System.get_system_props() - row = self.layout.row(align=True) - row.label(text="Ports located on object and connected Port/Objects:") - + row.label(text=f"Ports located in: {context.active_object.name} and connected Port/Objects:") + row = self.layout.row(align=True) cols = [row.column(align=True) for i in range(9)] cols[3].scale_x = 1.0 @@ -175,18 +173,17 @@ class BIM_PT_ports(Panel): for port_data in PortData.data["located_ports_data"]: flow_direction_icon = FLOW_DIRECTION_TO_ICON[port_data["FlowDirection"] or "NOTDEFINED"] - + if port_data["connected_obj_name"]: cols[0].operator("bim.disconnect_port", text="", icon="UNLINKED").element_id = port_data["id"] op = cols[1].operator("bim.cycle_flow_direction", text="", icon=flow_direction_icon, emboss=True) op.port_id = port_data["id"] else: - op = cols[0].operator("bim.add_related_port_connection", text="", icon='PLUGIN') + op = cols[0].operator("bim.add_related_port_connection", text="", icon="PLUGIN") op.relating_port_id = port_data["id"] op = cols[1].operator("bim.cycle_flow_direction", text="", icon=flow_direction_icon, emboss=True) op.port_id = port_data["id"] - - # Port information + if port_data["port_obj_name"]: cols[2].operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF").ifc_id = port_data["id"] cols[3].label(text=port_data["port_obj_name"]) @@ -196,16 +193,20 @@ class BIM_PT_ports(Panel): if port_data["connected_obj_name"]: connected_obj = bpy.data.objects[port_data["connected_obj_name"]] - + port = tool.Ifc.get().by_id(port_data["id"]) connected_port = tool.System.get_connected_port(port) if connected_port: connected_port_obj = tool.Ifc.get_object(connected_port) if connected_port_obj: connected_port_flow_dir = FLOW_DIRECTION_TO_ICON[connected_port.FlowDirection or "NOTDEFINED"] - op = cols[4].operator("bim.cycle_flow_direction", text="", icon=connected_port_flow_dir, emboss=True) + op = cols[4].operator( + "bim.cycle_flow_direction", text="", icon=connected_port_flow_dir, emboss=True + ) op.port_id = connected_port.id() - cols[5].operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF").ifc_id = connected_port.id() + cols[5].operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF").ifc_id = ( + connected_port.id() + ) cols[6].label(text=connected_port_obj.name) else: blank4 = cols[4].column(align=True) @@ -223,7 +224,7 @@ class BIM_PT_ports(Panel): blank5.scale_x = 0.1 blank5.label(text="", icon="BLANK1") cols[6].label(text="") - + ifc_id = tool.Blender.get_ifc_definition_id(connected_obj) cols[7].operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF").ifc_id = ifc_id cols[8].label(text=port_data["connected_obj_name"]) @@ -234,9 +235,9 @@ class BIM_PT_ports(Panel): blank5 = cols[5].column(align=True) blank5.scale_x = 0.1 blank5.label(text="", icon="BLANK1") - + cols[6].label(text="Port is disconnected") - + blank7 = cols[7].column(align=True) blank7.scale_x = 0.1 blank7.label(text="", icon="BLANK1") @@ -265,17 +266,8 @@ class BIM_PT_port(Panel): return True def draw(self, context): - self.props = tool.System.get_system_props() - layout = self.layout row = layout.row(align=True) - - if not PortData.is_loaded: - PortData.load() - - relating_object_name = PortData.data["port_relating_object_name"] if PortData.data["is_port"] else "" - row.label(text=f"IfcDistributionPort located in: {relating_object_name}") - row.operator("bim.remove_port", icon="X", text="") if not PortData.is_loaded: PortData.load() @@ -283,18 +275,21 @@ class BIM_PT_port(Panel): if not PortData.data["is_port"]: return + relating_object_name = PortData.data["port_relating_object_name"] + row.label(text=f"IfcDistributionPort located in: {relating_object_name}") + row.operator("bim.remove_port", icon="X", text="") + element = tool.Ifc.get_entity(context.active_object) - props = tool.System.get_system_props() - + row = layout.row(align=True) cols = [row.column(align=True) for i in range(9)] cols[3].scale_x = 1.0 cols[6].scale_x = 1.0 cols[8].scale_x = 1.33 - + flow_direction_icon = FLOW_DIRECTION_TO_ICON[element.FlowDirection or "NOTDEFINED"] connected_port = tool.System.get_connected_port(element) - + if connected_port: cols[0].operator("bim.disconnect_port", text="", icon="UNLINKED") op = cols[1].operator("bim.cycle_flow_direction", text="", icon=flow_direction_icon, emboss=True) @@ -303,10 +298,10 @@ class BIM_PT_port(Panel): cols[0].operator("bim.connect_port", icon="PLUGIN", text="") op = cols[1].operator("bim.cycle_flow_direction", text="", icon=flow_direction_icon, emboss=True) op.port_id = element.id() - + cols[2].operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF").ifc_id = element.id() cols[3].label(text=context.active_object.name) - + if connected_port: connected_port_flow_dir = FLOW_DIRECTION_TO_ICON[connected_port.FlowDirection or "NOTDEFINED"] op = cols[4].operator("bim.cycle_flow_direction", text="", icon=connected_port_flow_dir, emboss=True) @@ -314,7 +309,7 @@ class BIM_PT_port(Panel): cols[5].operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF").ifc_id = connected_port.id() connected_port_obj = tool.Ifc.get_object(connected_port) cols[6].label(text=connected_port_obj.name if connected_port_obj else "Hidden Port") - + connected_object_name = PortData.data["port_connected_object_name"] if connected_object_name: connected_obj = bpy.data.objects[connected_object_name] diff --git a/src/bonsai/bonsai/core/system.py b/src/bonsai/bonsai/core/system.py index 87a43307b7..01f1b7166e 100644 --- a/src/bonsai/bonsai/core/system.py +++ b/src/bonsai/bonsai/core/system.py @@ -132,7 +132,12 @@ def remove_port(ifc: type[tool.Ifc], system: type[tool.System], port: ifcopenshe ifc.run("root.remove_product", product=port) -def connect_port(ifc: type[tool.Ifc], port1: ifcopenshell.entity_instance, port2: ifcopenshell.entity_instance, direction: str = "NOTDEFINED") -> None: +def connect_port( + ifc: type[tool.Ifc], + port1: ifcopenshell.entity_instance, + port2: ifcopenshell.entity_instance, + direction: str = "NOTDEFINED", +) -> None: ifc.run("system.connect_port", port1=port1, port2=port2, direction=direction) diff --git a/src/bonsai/bonsai/tool/system.py b/src/bonsai/bonsai/tool/system.py index 0236b40fbd..3566dbd863 100644 --- a/src/bonsai/bonsai/tool/system.py +++ b/src/bonsai/bonsai/tool/system.py @@ -100,6 +100,7 @@ class System(bonsai.core.tool.System): @classmethod def create_empty_at_cursor_with_element_orientation(cls, element: ifcopenshell.entity_instance) -> bpy.types.Object: + # Is this necessary anymore? element_obj = tool.Ifc.get_object(element) obj = bpy.data.objects.new("Port", None) obj.matrix_world = element_obj.matrix_world diff --git a/src/ifcopenshell-python/ifcopenshell/api/project/append_asset.py b/src/ifcopenshell-python/ifcopenshell/api/project/append_asset.py index 0b703b6064..0a5aee0e1c 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/project/append_asset.py +++ b/src/ifcopenshell-python/ifcopenshell/api/project/append_asset.py @@ -490,8 +490,7 @@ class Usecase: attribute_class = None if "." in attribute: attribute, attribute_class = attribute.split(".") - inverse_values = getattr(element, attribute, []) - for inverse in inverse_values: + for inverse in getattr(element, attribute, []): if attribute_class and inverse.is_a(attribute_class): self.add_inverse_element(inverse) elif not attribute_class: From 96a0e9f72d218c662193475eaa1bd221e10ec07f Mon Sep 17 00:00:00 2001 From: falken10vdl Date: Tue, 13 Jan 2026 10:07:10 +0100 Subject: [PATCH 11/11] Adapt tests for create_port_at_cursor method --- src/bonsai/bonsai/core/tool.py | 1 + src/bonsai/test/core/test_system.py | 9 ++++----- src/bonsai/test/tool/test_system.py | 13 +++++++++++++ 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/bonsai/bonsai/core/tool.py b/src/bonsai/bonsai/core/tool.py index d8e4dd2396..002f643e55 100644 --- a/src/bonsai/bonsai/core/tool.py +++ b/src/bonsai/bonsai/core/tool.py @@ -1074,6 +1074,7 @@ class Surveyor: @interface class System: def create_empty_at_cursor_with_element_orientation(cls, element): pass + def create_port_at_cursor(cls, system): pass def delete_element_objects(cls, elements): pass def disable_editing_system(cls): pass def disable_system_editing_ui(cls): pass diff --git a/src/bonsai/test/core/test_system.py b/src/bonsai/test/core/test_system.py index d907059b98..4612e14d69 100644 --- a/src/bonsai/test/core/test_system.py +++ b/src/bonsai/test/core/test_system.py @@ -153,11 +153,10 @@ class TestAddPort: def test_run(self, ifc, system): system.get_ports("element").should_be_called().will_return(["port"]) system.load_ports("element", ["port"]).should_be_called() - system.create_empty_at_cursor_with_element_orientation("element").should_be_called().will_return("obj") - system.run_root_assign_class( - obj="obj", ifc_class="IfcDistributionPort", should_add_representation=False - ).should_be_called().will_return("port") - ifc.run("system.assign_port", element="element", port="port").should_be_called() + #system.create_empty_at_cursor_with_element_orientation("element").should_be_called().will_return("obj") + #system.run_root_assign_class(obj="obj", ifc_class="IfcDistributionPort", should_add_representation=False).should_be_called().will_return("port") + system.create_port_at_cursor("element").should_be_called().will_return("port") + system.load_ports("element", ["port"]).should_be_called() subject.add_port(ifc, system, element="element") diff --git a/src/bonsai/test/tool/test_system.py b/src/bonsai/test/tool/test_system.py index de8e9026b0..ba20305140 100644 --- a/src/bonsai/test/tool/test_system.py +++ b/src/bonsai/test/tool/test_system.py @@ -103,6 +103,19 @@ class TestCreateEmptyAtCursorWithElementOrientation(NewFile): obj = subject.create_empty_at_cursor_with_element_orientation(element) assert obj.matrix_world == bpy.context.scene.cursor.matrix +class TestCreatePortAtCursor(NewFile): + def test_run(self): + assert bpy.context.scene + ifc = ifcopenshell.file() + tool.Ifc().set(ifc) + system = ifcopenshell.api.system.add_system(ifc) + element = ifcopenshell.api.root.create_entity(ifc, ifc_class="IfcDuctSegment") + ifcopenshell.api.system.assign_system(ifc, products=[element], system=system) + obj = tool.Ifc.link(element, bpy.data.objects.new("Object", None)) + port = subject.create_port_at_cursor(element) + assert port.is_a("IfcDistributionPort") + assert ifcopenshell.util.system.get_ports(element) == [port] + class TestDeleteElementObjects(NewFile): def test_run(self):