Simplified handling of Ports in MEP

This commit is contained in:
falken10vdl
2026-01-09 02:02:43 +01:00
parent e9eca5e3f2
commit 628a76e487
6 changed files with 300 additions and 65 deletions
@@ -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,
@@ -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"
+59 -1
View File
@@ -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]:
+115 -60
View File
@@ -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):
+2 -3
View File
@@ -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:
+26
View File
@@ -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: