From bd1871bd44a79b8942229cf52759ada92e357842 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 16 Jun 2025 13:25:50 +0500 Subject: [PATCH] Basic IfcApplications UI Just so you can view IfcApplications in the project from UI Example - https://files.catbox.moe/15a72n.png --- .../bonsai/bim/module/owner/__init__.py | 2 + src/bonsai/bonsai/bim/module/owner/data.py | 30 +++++++++++ .../bonsai/bim/module/owner/operator.py | 53 +++++++++++++++++++ src/bonsai/bonsai/bim/module/owner/ui.py | 40 +++++++++++++- 4 files changed, 123 insertions(+), 2 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/owner/__init__.py b/src/bonsai/bonsai/bim/module/owner/__init__.py index 8b9f9b5f37..9318b42ba0 100644 --- a/src/bonsai/bonsai/bim/module/owner/__init__.py +++ b/src/bonsai/bonsai/bim/module/owner/__init__.py @@ -48,6 +48,7 @@ classes = ( operator.RemoveActor, operator.RemoveAddress, operator.RemoveAddressAttribute, + operator.RemoveApplication, operator.RemoveOrganisation, operator.RemovePerson, operator.RemovePersonAndOrganisation, @@ -61,6 +62,7 @@ classes = ( ui.BIM_PT_owner, ui.BIM_PT_actor, ui.BIM_PT_object_actor, + ui.BIM_PT_applications, ) diff --git a/src/bonsai/bonsai/bim/module/owner/data.py b/src/bonsai/bonsai/bim/module/owner/data.py index 4fb53a6da1..b94fd1dac5 100644 --- a/src/bonsai/bonsai/bim/module/owner/data.py +++ b/src/bonsai/bonsai/bim/module/owner/data.py @@ -21,6 +21,7 @@ import ifcopenshell import bonsai.tool as tool from typing import Any, Union from ifcopenshell.util.doc import get_entity_doc +from natsort import natsorted def refresh(): @@ -340,3 +341,32 @@ class ObjectActorData: @classmethod def get_roles(cls, parent: ifcopenshell.entity_instance) -> list[Union[str, None]]: return [r.UserDefinedRole or r.Role for r in parent.Roles or []] + + +class ApplicationsData: + data = {} + is_loaded = False + + @classmethod + def load(cls) -> None: + cls.is_loaded = True + cls.data = { + "applications": cls.applications(), + } + + @classmethod + def applications(cls) -> list[dict[str, Any]]: + applications: list[dict[str, Any]] = [] + ifc_file = tool.Ifc.get() + for application in ifc_file.by_type("IfcApplication"): + total_inverses = ifc_file.get_total_inverses(application) + name = application.ApplicationFullName + if total_inverses == 0: + name += " (unused)" + applications.append( + { + "id": application.id(), + "name": name, + } + ) + return natsorted(applications, key=lambda x: x["name"]) diff --git a/src/bonsai/bonsai/bim/module/owner/operator.py b/src/bonsai/bonsai/bim/module/owner/operator.py index 41b1b77192..4d3b90b8de 100644 --- a/src/bonsai/bonsai/bim/module/owner/operator.py +++ b/src/bonsai/bonsai/bim/module/owner/operator.py @@ -19,6 +19,7 @@ import bpy import bonsai.tool as tool import bonsai.core.owner as core +import ifcopenshell.api.owner from ifcopenshell.api.owner.add_address import ADDRESS_TYPE from typing import TYPE_CHECKING, get_args @@ -468,3 +469,55 @@ class UnassignActor(bpy.types.Operator, tool.Ifc.Operator): assert (obj := context.active_object) assert (element := tool.Ifc.get_entity(obj)) core.unassign_actor(tool.Ifc, actor=tool.Ifc.get().by_id(self.actor), element=element) + + +class RemoveApplication(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.remove_application" + bl_label = "Remove Application" + bl_options = {"REGISTER", "UNDO"} + bl_description = ( + "Remove provided IfcApplication." + "\n\nFor safety will only work on applications without inverses (they are typically marked as '(unused)'." + ) + application_id: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration] + + if TYPE_CHECKING: + application_id: int + + @classmethod + def description(cls, context, properties) -> str: + description = cls.bl_description + application_id: int + if not (application_id := properties.application_id): + return description + ifc_file = tool.Ifc.get() + application = ifc_file.by_id(application_id) + + application_info = "Additional application info:" + application_info += f"\n- Developer: {application.ApplicationDeveloper.Name}" + application_info += f"\n- Version: {application.Version}" + application_info += f"\n- Identifier: {application.ApplicationIdentifier}" + application_info += f"\n- Number of inverses: {ifc_file.get_total_inverses(application)}" + application_info += f"\n(try to remove application to see the full list of inverses in system console)" + + description += f"\n\n{application_info}" + return description + + def invoke(self, context, event): + ifc_file = tool.Ifc.get() + assert (application := ifc_file.by_id(self.application_id)) + if total_inverses := ifc_file.get_total_inverses(application): + print(f"Present inverses for {application}:") + for inverse in ifc_file.get_inverse(application): + print(f"- {inverse}") + self.report( + {"ERROR"}, + f"This IfcApplication still has {total_inverses} inverses, deletion is unsafe. " + "Check system console for the details.", + ) + return {"CANCELLED"} + return self.execute(context) + + def _execute(self, context): + ifc_file = tool.Ifc.get() + ifcopenshell.api.owner.remove_application(ifc_file, ifc_file.by_id(self.application_id)) diff --git a/src/bonsai/bonsai/bim/module/owner/ui.py b/src/bonsai/bonsai/bim/module/owner/ui.py index 233e4cf689..6918670fe2 100644 --- a/src/bonsai/bonsai/bim/module/owner/ui.py +++ b/src/bonsai/bonsai/bim/module/owner/ui.py @@ -20,7 +20,14 @@ import bpy import bonsai.bim.helper import bonsai.tool as tool from typing import Any -from bonsai.bim.module.owner.data import PeopleData, OrganisationsData, OwnerData, ActorData, ObjectActorData +from bonsai.bim.module.owner.data import ( + PeopleData, + OrganisationsData, + OwnerData, + ActorData, + ObjectActorData, + ApplicationsData, +) def draw_roles(box: bpy.types.UILayout, parent: dict[str, Any]) -> None: @@ -240,7 +247,7 @@ class BIM_PT_owner(bpy.types.Panel): class BIM_PT_actor(bpy.types.Panel): - bl_label = "Actor" + bl_label = "Actors" bl_idname = "BIM_PT_actor" bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" @@ -326,3 +333,32 @@ class BIM_PT_object_actor(bpy.types.Panel): row.label(text=actor["name"], icon="USER") row.label(text=actor["role"]) row.operator("bim.unassign_actor", icon="X", text="").actor = actor["id"] + + +class BIM_PT_applications(bpy.types.Panel): + bl_label = "Applications" + bl_idname = "BIM_PT_applications" + bl_options = {"DEFAULT_CLOSED"} + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "scene" + bl_parent_id = "BIM_PT_tab_stakeholders" + + @classmethod + def poll(cls, context) -> bool: + return bool(tool.Ifc.get()) + + def draw(self, context): + if not ApplicationsData.is_loaded: + ApplicationsData.load() + + assert (layout := self.layout) + props = tool.Owner.get_owner_props() + + apps_data: list[dict[str, Any]] = ApplicationsData.data["applications"] + if not apps_data: + layout.label(text="No Applications Found") + for app_data in apps_data: + row = layout.row() + row.label(text=app_data["name"]) + row.operator("bim.remove_application", icon="X", text="").application_id = app_data["id"]