From 5ea66730d41ee2170c32a62e6cba6ab0295a1121 Mon Sep 17 00:00:00 2001 From: falken10 Date: Fri, 20 Jun 2025 10:14:02 +0200 Subject: [PATCH 01/12] Implemented tree like structure for documents --- .../bonsai/bim/module/document/__init__.py | 17 +- src/bonsai/bonsai/bim/module/document/data.py | 123 ++++++-- .../bonsai/bim/module/document/operator.py | 292 ++++++++++++++++- src/bonsai/bonsai/bim/module/document/prop.py | 76 ++++- src/bonsai/bonsai/bim/module/document/ui.py | 298 ++++++++++++++---- src/bonsai/bonsai/core/document.py | 102 +++--- src/bonsai/bonsai/core/tool.py | 6 - src/bonsai/bonsai/tool/document.py | 181 +++++++---- src/bonsai/test/bim/feature/document.feature | 9 - src/bonsai/test/core/test_document.py | 18 -- src/bonsai/test/tool/test_document.py | 52 --- 11 files changed, 873 insertions(+), 301 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/document/__init__.py b/src/bonsai/bonsai/bim/module/document/__init__.py index f4eede1721..6a925d0f98 100644 --- a/src/bonsai/bonsai/bim/module/document/__init__.py +++ b/src/bonsai/bonsai/bim/module/document/__init__.py @@ -18,32 +18,47 @@ import bpy from . import ui, prop, operator +from bpy.types import VIEW3D_MT_object_context_menu classes = ( operator.AddDocumentReference, operator.AddInformation, operator.AssignDocument, operator.DisableDocumentEditingUI, + operator.DisableObjectDocumentEditingUI, operator.DisableEditingDocument, operator.EditDocument, operator.EnableEditingDocument, operator.LoadDocument, - operator.LoadParentDocument, + operator.LoadObjectDocuments, operator.LoadProjectDocuments, operator.RemoveDocument, operator.SelectDocumentObjects, + operator.ToggleDocument, operator.UnassignDocument, + operator.UpdateAssignedDocuments, + operator.OpenIFCDocument, prop.Document, + prop.DocumentObject, + prop.AssignedDocument, + prop.ExpandedDocuments, prop.BIMDocumentProperties, ui.BIM_PT_documents, ui.BIM_PT_object_documents, ui.BIM_UL_documents, + ui.BIM_UL_document_objects, + ui.BIM_UL_assigned_documents, + ui.BIM_MT_object_documents_context_menu, ) def register(): bpy.types.Scene.BIMDocumentProperties = bpy.props.PointerProperty(type=prop.BIMDocumentProperties) + bpy.types.Scene.ExpandedDocuments = bpy.props.PointerProperty(type=prop.ExpandedDocuments) + VIEW3D_MT_object_context_menu.append(ui.add_object_documents_context_menu) def unregister(): del bpy.types.Scene.BIMDocumentProperties + del bpy.types.Scene.ExpandedDocuments + VIEW3D_MT_object_context_menu.remove(ui.add_object_documents_context_menu) diff --git a/src/bonsai/bonsai/bim/module/document/data.py b/src/bonsai/bonsai/bim/module/document/data.py index 5cde82e499..be6b628bed 100644 --- a/src/bonsai/bonsai/bim/module/document/data.py +++ b/src/bonsai/bonsai/bim/module/document/data.py @@ -35,30 +35,69 @@ class DocumentData: @classmethod def load(cls): cls.data = { - "total_information": cls.total_information(), - "parent_document": cls.parent_document(), + "total_document_informations": cls.total_document_informations(), + "total_document_references": cls.total_document_references(), + "total_referenced_objects": cls.total_referenced_objects(), + "document_objects": cls.document_objects(), } cls.is_loaded = True @classmethod - def total_information(cls): - return len( - [ - rel - for rel in tool.Ifc.get().by_type("IfcProject")[0].HasAssociations or [] - if rel.is_a("IfcRelAssociatesDocument") and rel.RelatingDocument.is_a("IfcDocumentInformation") - ] - ) + def total_document_informations(cls): + file = tool.Ifc.get() + info_count = len(file.by_type("IfcDocumentInformation")) + return info_count @classmethod - def parent_document(cls): + def total_document_references(cls): + file = tool.Ifc.get() + ref_count = len(file.by_type("IfcDocumentReference")) + return ref_count + + @classmethod + def total_referenced_objects(cls): + file = tool.Ifc.get() + document_rels = file.by_type("IfcRelAssociatesDocument") + documented_objects = set() + for rel in document_rels: + for related_object in rel.RelatedObjects: + obj = tool.Ifc.get_object(related_object) + if obj: + documented_objects.add(related_object.id()) + + return len(documented_objects) + + @classmethod + def document_objects(cls): + document_objects = {} + file = tool.Ifc.get() + + for rel in file.by_type("IfcRelAssociatesDocument"): + document_id = rel.RelatingDocument.id() + if document_id not in document_objects: + document_objects[document_id] = [] + + for related_object in rel.RelatedObjects: + element = related_object + obj = tool.Ifc.get_object(element) + if obj: + document_objects[document_id].append({"id": element.id(), "name": obj.name, "obj": obj}) + + return document_objects + + @classmethod + def load_document_objects_into_props(cls, document_id): props = tool.Document.get_document_props() - if len(props.breadcrumbs): - parent = tool.Ifc.get().by_id(int(props.breadcrumbs[-1].name)) - if tool.Ifc.get_schema() == "IFC2X3": - return str(parent.DocumentId) - return str(parent.Identification) - return "" + props.document_objects.clear() + + if "document_objects" not in cls.data or document_id not in cls.data["document_objects"]: + return + + sorted_objects = sorted(cls.data["document_objects"][document_id], key=lambda x: x["name"].lower()) + + for obj_data in sorted_objects: + item = props.document_objects.add() + item.name = obj_data["name"] class ObjectDocumentData: @@ -80,31 +119,47 @@ class ObjectDocumentData: return results for rel in getattr(element, "HasAssociations", []): if rel.is_a("IfcRelAssociatesDocument"): - if not rel.RelatingDocument.is_a("IfcDocumentReference"): + is_information = rel.RelatingDocument.is_a("IfcDocumentInformation") + is_reference = rel.RelatingDocument.is_a("IfcDocumentReference") + + if not (is_information or is_reference): continue name = rel.RelatingDocument.Name - if tool.Ifc.get_schema() == "IFC2X3": - if not name and rel.RelatingDocument.ReferenceToDocument: - name = rel.RelatingDocument.ReferenceToDocument[0].Name + location = None + identification = None + description = None - identification = rel.RelatingDocument.ItemReference - if not identification and rel.RelatingDocument.ReferenceToDocument: - identification = rel.RelatingDocument.ReferenceToDocument[0].DocumentId + if is_information: + if tool.Ifc.get_schema() == "IFC2X3": + identification = rel.RelatingDocument.DocumentId + else: + identification = rel.RelatingDocument.Identification + + location = getattr(rel.RelatingDocument, "Location", None) - location = rel.RelatingDocument.Location else: - if not name and rel.RelatingDocument.ReferencedDocument: - name = rel.RelatingDocument.ReferencedDocument.Name + description = rel.RelatingDocument.Description + if tool.Ifc.get_schema() == "IFC2X3": + if not name and rel.RelatingDocument.ReferenceToDocument: + name = rel.RelatingDocument.ReferenceToDocument[0].Name - identification = rel.RelatingDocument.Identification - if not identification and rel.RelatingDocument.ReferencedDocument: - identification = rel.RelatingDocument.ReferencedDocument.Identification + identification = rel.RelatingDocument.ItemReference + if not identification and rel.RelatingDocument.ReferenceToDocument: + identification = rel.RelatingDocument.ReferenceToDocument[0].DocumentId + location = rel.RelatingDocument.Location + else: + if not name and rel.RelatingDocument.ReferencedDocument: + name = rel.RelatingDocument.ReferencedDocument.Name - location = rel.RelatingDocument.Location - if location is None and rel.RelatingDocument.ReferencedDocument: - location = rel.RelatingDocument.ReferencedDocument.Location + identification = rel.RelatingDocument.Identification + if not identification and rel.RelatingDocument.ReferencedDocument: + identification = rel.RelatingDocument.ReferencedDocument.Identification + + location = rel.RelatingDocument.Location + if location is None and rel.RelatingDocument.ReferencedDocument: + location = rel.RelatingDocument.ReferencedDocument.Location if location: if not "://" in location: @@ -118,6 +173,8 @@ class ObjectDocumentData: "identification": identification, "name": name, "location": location, + "is_information": is_information, + "description": description, } ) return results diff --git a/src/bonsai/bonsai/bim/module/document/operator.py b/src/bonsai/bonsai/bim/module/document/operator.py index da75e16e63..ffc81537ee 100644 --- a/src/bonsai/bonsai/bim/module/document/operator.py +++ b/src/bonsai/bonsai/bim/module/document/operator.py @@ -24,6 +24,24 @@ import ifcopenshell.util.element import bonsai.bim.handler import bonsai.tool as tool import bonsai.core.document as core +import subprocess +import os +from bonsai.bim.module.document.data import DocumentData, ObjectDocumentData + + +def update_document_objects(document_id=None): + DocumentData.is_loaded = False + DocumentData.load() + + if document_id is None: + props = tool.Document.get_document_props() + if props.documents and props.active_document_index < len(props.documents): + document = props.documents[props.active_document_index] + if document.ifc_definition_id: + document_id = document.ifc_definition_id + + if document_id: + DocumentData.load_document_objects_into_props(document_id) class LoadProjectDocuments(bpy.types.Operator): @@ -33,7 +51,7 @@ class LoadProjectDocuments(bpy.types.Operator): def execute(self, context): core.load_project_documents(tool.Document) - bonsai.bim.handler.refresh_ui_data() # Update breadcrumbs data. + update_document_objects() return {"FINISHED"} @@ -45,18 +63,8 @@ class LoadDocument(bpy.types.Operator): def execute(self, context): core.load_document(tool.Document, document=tool.Ifc.get().by_id(self.document)) - bonsai.bim.handler.refresh_ui_data() # Update breadcrumbs data. - return {"FINISHED"} - - -class LoadParentDocument(bpy.types.Operator): - bl_idname = "bim.load_parent_document" - bl_label = "Load Parent Document" - bl_options = {"REGISTER", "UNDO"} - - def execute(self, context): - core.load_parent_document(tool.Document) - bonsai.bim.handler.refresh_ui_data() # Update breadcrumbs data. + bonsai.bim.handler.refresh_ui_data() # Is this needed? + update_document_objects() return {"FINISHED"} @@ -70,6 +78,17 @@ class DisableDocumentEditingUI(bpy.types.Operator): return {"FINISHED"} +class DisableObjectDocumentEditingUI(bpy.types.Operator): + bl_idname = "bim.disable_object_document_editing_ui" + bl_label = "Disable Object Document Editing UI" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + props = tool.Document.get_document_props() + props.is_object_editing = False + return {"FINISHED"} + + class EnableEditingDocument(bpy.types.Operator): bl_idname = "bim.enable_editing_document" bl_label = "Enable Editing Document" @@ -77,6 +96,8 @@ class EnableEditingDocument(bpy.types.Operator): document: bpy.props.IntProperty() def execute(self, context): + props = tool.Document.get_document_props() + props.is_document_editing = True core.enable_editing_document(tool.Document, document=tool.Ifc.get().by_id(self.document)) return {"FINISHED"} @@ -87,6 +108,8 @@ class DisableEditingDocument(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} def execute(self, context): + props = tool.Document.get_document_props() + props.is_document_editing = False core.disable_editing_document(tool.Document) return {"FINISHED"} @@ -97,7 +120,41 @@ class AddInformation(bpy.types.Operator, tool.Ifc.Operator): bl_options = {"REGISTER", "UNDO"} def _execute(self, context): - core.add_information(tool.Ifc, tool.Document) + props = tool.Document.get_document_props() + parent = None + if props.documents and props.active_document_index < len(props.documents): + selected_document = props.documents[props.active_document_index] + + if selected_document.ifc_definition_id == -1: + parent = tool.Ifc.get().by_type("IfcProject")[0] if tool.Ifc.get().by_type("IfcProject") else None + elif selected_document.is_information: + parent = tool.Ifc.get().by_id(selected_document.ifc_definition_id) + else: + self.report({"ERROR"}, "Cannot add an information element as a child of a reference element") + return {"CANCELLED"} + else: + parent = tool.Ifc.get().by_type("IfcProject")[0] if tool.Ifc.get().by_type("IfcProject") else None + + core.add_information(tool.Ifc, tool.Document, parent) + + expanded_docs = [] + try: + expanded_docs = json.loads(context.scene.ExpandedDocuments.json_string) + except (AttributeError, json.JSONDecodeError): + pass + + project = tool.Ifc.get().by_type("IfcProject")[0] + virtual_root_id = -project.id() + if virtual_root_id in expanded_docs: + expanded_docs.remove(virtual_root_id) + + if parent and parent.is_a("IfcDocumentInformation"): + if parent.id() not in expanded_docs: + expanded_docs.append(parent.id()) + + context.scene.ExpandedDocuments.json_string = json.dumps(expanded_docs) + + bpy.ops.bim.load_project_documents() class AddDocumentReference(bpy.types.Operator, tool.Ifc.Operator): @@ -106,7 +163,33 @@ class AddDocumentReference(bpy.types.Operator, tool.Ifc.Operator): bl_options = {"REGISTER", "UNDO"} def _execute(self, context): + props = tool.Document.get_document_props() + + if not props.documents or props.active_document_index >= len(props.documents): + self.report({"ERROR"}, "No document selected") + return {"CANCELLED"} + + selected_document = props.documents[props.active_document_index] + + if not selected_document.is_information: + self.report({"ERROR"}, "Cannot add a reference to a reference element") + return {"CANCELLED"} + + parent = tool.Ifc.get().by_id(selected_document.ifc_definition_id) + + props.document_attributes.clear() core.add_reference(tool.Ifc, tool.Document) + expanded_docs = [] + try: + expanded_docs = json.loads(context.scene.ExpandedDocuments.json_string) + except (AttributeError, json.JSONDecodeError): + pass + + if parent.id() not in expanded_docs: + expanded_docs.append(parent.id()) + context.scene.ExpandedDocuments.json_string = json.dumps(expanded_docs) + + bpy.ops.bim.load_project_documents() class EditDocument(bpy.types.Operator, tool.Ifc.Operator): @@ -116,7 +199,16 @@ class EditDocument(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): props = tool.Document.get_document_props() - core.edit_document(tool.Ifc, tool.Document, document=tool.Ifc.get().by_id(props.active_document_id)) + if props.active_document_id: + core.edit_document(tool.Ifc, tool.Document, document=tool.Ifc.get().by_id(props.active_document_id)) + props.active_document_id = 0 + props.is_document_editing = False + DocumentData.is_loaded = False + DocumentData.load() + ObjectDocumentData.is_loaded = False + ObjectDocumentData.load() + bpy.ops.bim.update_assigned_documents() + bonsai.bim.handler.refresh_ui_data() class RemoveDocument(bpy.types.Operator, tool.Ifc.Operator): @@ -129,6 +221,39 @@ class RemoveDocument(bpy.types.Operator, tool.Ifc.Operator): core.remove_document(tool.Ifc, tool.Document, document=tool.Ifc.get().by_id(self.document)) +class UpdateAssignedDocuments(bpy.types.Operator): + bl_idname = "bim.update_assigned_documents" + bl_label = "Update Assigned Documents" + bl_description = "Update the list of documents assigned to the active object" + bl_options = {"REGISTER"} + + def execute(self, context): + ObjectDocumentData.is_loaded = False + ObjectDocumentData.load() + + props = tool.Document.get_document_props() + props.assigned_documents.clear() + + if not ObjectDocumentData.data.get("documents"): + return {"FINISHED"} + + sorted_docs = sorted( + ObjectDocumentData.data["documents"], + key=lambda doc: ((doc.get("identification") or "").lower(), (doc.get("name") or "").lower()), + ) + + for document in sorted_docs: + new = props.assigned_documents.add() + new.name = document["name"] or "Unnamed" + new.identification = document["identification"] or "*" + new.is_information = document.get("is_information", False) + new.ifc_definition_id = document["id"] + new.location = document.get("location") or "" + new.description = document.get("description") or "" + + return {"FINISHED"} + + class AssignDocument(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.assign_document" bl_label = "Assign Document" @@ -145,6 +270,11 @@ class AssignDocument(bpy.types.Operator, tool.Ifc.Operator): if element: core.assign_document(tool.Ifc, product=element, document=document) + update_document_objects(self.document) + ObjectDocumentData.is_loaded = False + ObjectDocumentData.load() + bpy.ops.bim.update_assigned_documents() + class UnassignDocument(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.unassign_document" @@ -160,6 +290,19 @@ class UnassignDocument(bpy.types.Operator, tool.Ifc.Operator): element = tool.Ifc.get_entity(obj) if element: core.unassign_document(tool.Ifc, product=element, document=document) + props = tool.Document.get_document_props() + active_document_id = None + if props.documents and props.active_document_index < len(props.documents): + active_document = props.documents[props.active_document_index] + active_document_id = active_document.ifc_definition_id + + if active_document_id and active_document_id != self.document: + update_document_objects(active_document_id) + else: + update_document_objects(self.document) + ObjectDocumentData.is_loaded = False + ObjectDocumentData.load() + bpy.ops.bim.update_assigned_documents() class SelectDocumentObjects(bpy.types.Operator): @@ -182,3 +325,122 @@ class SelectDocumentObjects(bpy.types.Operator): i += 1 self.report({"INFO"}, f"{i} objects selected.") return {"FINISHED"} + + +class LoadObjectDocuments(bpy.types.Operator): + bl_idname = "bim.load_object_documents" + bl_label = "Load Object Documents" + bl_description = "Load documents to assign to the selected object" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + if not ObjectDocumentData.is_loaded: + ObjectDocumentData.load() + + core.load_project_documents(tool.Document) + + props = tool.Document.get_document_props() + props.is_object_editing = True + + bonsai.bim.handler.refresh_ui_data() + + self.update_assigned_documents(props) + + return {"FINISHED"} + + def update_assigned_documents(self, props): + props.assigned_documents.clear() + + if not ObjectDocumentData.data.get("documents"): + return + + sorted_docs = sorted( + ObjectDocumentData.data["documents"], + key=lambda doc: ((doc.get("identification") or "").lower(), (doc.get("name") or "").lower()), + ) + + for document in sorted_docs: + new = props.assigned_documents.add() + new.name = document["name"] or "Unnamed" + new.identification = document["identification"] or "*" + new.is_information = document.get("is_information", False) + new.ifc_definition_id = document["id"] + new.location = document["location"] or "" + new.description = document["description"] or "" + + +class OpenIFCDocument(bpy.types.Operator): + bl_idname = "bim.open_ifc_document" + bl_label = "Open IFC Document" + bl_description = "Open the IFC document in a new Blender instance and load the project" + bl_options = {"REGISTER", "UNDO"} + + uri: bpy.props.StringProperty(name="URI") + + def execute(self, context): + + if not self.uri: + self.report({"ERROR"}, "No URI provided") + return {"CANCELLED"} + + file_path = self.uri + if file_path.startswith("file://"): + file_path = file_path[7:] + elif file_path.startswith("file:"): + file_path = file_path[5:] + + if not os.path.isabs(file_path): + file_path = os.path.abspath(file_path) + + if not os.path.exists(file_path): + self.report({"ERROR"}, f"IFC file not found: {file_path}") + return {"CANCELLED"} + + try: + subprocess.Popen( + [ + "blender", + "--python-expr", + f"import bpy; bpy.ops.bim.load_project(filepath='{file_path}', should_start_fresh_session=True)", + ] + ) + self.report({"INFO"}, f"Opening IFC file: {file_path} in a new Blender instance.") + except Exception as e: + self.report({"ERROR"}, f"Failed to open IFC file: {str(e)}") + + return {"FINISHED"} + + +class ToggleDocument(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.toggle_document" + bl_label = "Toggle Document" + bl_options = {"REGISTER", "UNDO"} + document: bpy.props.IntProperty() + option: bpy.props.StringProperty() + + def _execute(self, context): + expanded_documents = [] + try: + expanded_documents = json.loads(context.scene.ExpandedDocuments.json_string) + except (AttributeError, json.JSONDecodeError): + expanded_documents = [] + + document_id = self.document + + if self.option == "Expand" and document_id not in expanded_documents: + expanded_documents.append(document_id) + elif self.option == "Collapse" and document_id in expanded_documents: + expanded_documents.remove(document_id) + elif document_id == -1: + project = tool.Ifc.get().by_type("IfcProject")[0] + virtual_root_id = -project.id() + + if self.option == "Expand" and virtual_root_id not in expanded_documents: + expanded_documents.append(virtual_root_id) + elif self.option == "Collapse" and virtual_root_id in expanded_documents: + expanded_documents.remove(virtual_root_id) + + context.scene.ExpandedDocuments.json_string = json.dumps(expanded_documents) + + bpy.ops.bim.load_project_documents() + return {"FINISHED"} diff --git a/src/bonsai/bonsai/bim/module/document/prop.py b/src/bonsai/bonsai/bim/module/document/prop.py index f4e4b16686..88878f8bfc 100644 --- a/src/bonsai/bonsai/bim/module/document/prop.py +++ b/src/bonsai/bonsai/bim/module/document/prop.py @@ -30,6 +30,7 @@ from bpy.props import ( FloatVectorProperty, CollectionProperty, ) +from bonsai.bim.module.document.data import DocumentData from typing import TYPE_CHECKING, Union @@ -49,36 +50,93 @@ def update_document_identification(self: "Document", context: bpy.types.Context) tool.Document.set_external_reference_id(document, self.identification) +def update_active_document(self, context): + if self.documents and self.active_document_index < len(self.documents): + document = self.documents[self.active_document_index] + if document.ifc_definition_id: + DocumentData.load_document_objects_into_props(document.ifc_definition_id) + + class Document(PropertyGroup): - name: StringProperty(name="Name", update=update_document_name) - identification: StringProperty(name="Identification", update=update_document_identification) - is_information: BoolProperty( - name="Is Information", - description="Whether element is IfcDocumentInformation, otherwise it's IfcDocumentReference.", - ) + name: StringProperty(name="Name") + identification: StringProperty(name="Identification") + description: StringProperty(name="Description") + is_information: BoolProperty(name="Is Information") + ifc_definition_id: IntProperty(name="IFC Definition ID") + location: StringProperty(name="Location", default="") + tree_depth: IntProperty(name="Tree Depth", default=0) + has_children: BoolProperty(name="Has Children", default=False) + is_expanded: BoolProperty(name="Is Expanded", default=False) + + if TYPE_CHECKING: + name: str + identification: str + description: str + is_information: bool + ifc_definition_id: int + location: str + tree_depth: int + has_children: bool + is_expanded: bool + + +class ExpandedDocuments(PropertyGroup): + json_string: StringProperty(name="JSON String", default="[]") + + if TYPE_CHECKING: + json_string: str + + +class DocumentObject(PropertyGroup): + name: StringProperty(name="Name") ifc_definition_id: IntProperty(name="IFC Definition ID") if TYPE_CHECKING: + name: str + ifc_definition_id: int + + +class AssignedDocument(PropertyGroup): + name: StringProperty(name="Name") + identification: StringProperty(name="Identification") + description: StringProperty(name="Description", default="") + is_information: BoolProperty(name="Is Information") + ifc_definition_id: IntProperty(name="IFC Definition ID") + location: StringProperty(name="Location", default="") + + if TYPE_CHECKING: + name: str identification: str is_information: bool ifc_definition_id: int + location: str class BIMDocumentProperties(PropertyGroup): document_attributes: CollectionProperty(name="Document Attributes", type=Attribute) active_document_id: IntProperty(name="Active Document Id") documents: CollectionProperty(name="Documents", type=Document) - breadcrumbs: CollectionProperty(name="Breadcrumbs", type=StrProperty) - active_document_index: IntProperty(name="Active Document Index") + active_document_index: IntProperty(name="Active Document Index", update=update_active_document) is_editing: BoolProperty(name="Is Editing", default=False) + is_document_editing: BoolProperty(name="Is Document Editing", default=False) + is_object_editing: BoolProperty(name="Is Object Editing", default=False) + document_objects: CollectionProperty(name="Document Objects", type=DocumentObject) + active_document_object_index: IntProperty(name="Active Document Object Index") + assigned_documents: CollectionProperty(name="Assigned Documents", type=AssignedDocument) + active_assigned_document_index: IntProperty(name="Active Assigned Document Index") if TYPE_CHECKING: document_attributes: bpy.types.bpy_prop_collection_idprop[Attribute] active_document_id: int documents: bpy.types.bpy_prop_collection_idprop[Document] - breadcrumbs: bpy.types.bpy_prop_collection_idprop[StrProperty] active_document_index: int is_editing: bool + is_document_editing: bool + is_object_editing: bool + document_objects: bpy.types.bpy_prop_collection_idprop[DocumentObject] + active_document_object_index: int + assigned_documents: bpy.types.bpy_prop_collection_idprop[AssignedDocument] + active_assigned_document_index: int @property def active_document(self) -> Union[Document, None]: diff --git a/src/bonsai/bonsai/bim/module/document/ui.py b/src/bonsai/bonsai/bim/module/document/ui.py index 5897edbad8..eab2d146fb 100644 --- a/src/bonsai/bonsai/bim/module/document/ui.py +++ b/src/bonsai/bonsai/bim/module/document/ui.py @@ -16,6 +16,7 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . +import bpy import bonsai.tool as tool from bpy.types import Panel, UIList from bonsai.bim.helper import draw_attributes @@ -42,43 +43,74 @@ class BIM_PT_documents(Panel): self.props = tool.Document.get_document_props() row = self.layout.row(align=True) - row.label(text="{} Documents Found".format(DocumentData.data["total_information"]), icon="FILE") + split = row.split(factor=0.55) + + left_row = split.row(align=True) + left_row.label(text="{} Informations".format(DocumentData.data["total_document_informations"]), icon="FILE") + left_row.label(text="{} References".format(DocumentData.data["total_document_references"]), icon="FILE_HIDDEN") + + right_row = split.row(align=True) + right_row.label( + text="{} Objects Referenced".format(DocumentData.data["total_referenced_objects"]), icon="OBJECT_DATA" + ) if self.props.is_editing: - row.operator("bim.disable_document_editing_ui", text="", icon="CANCEL") + right_row.operator("bim.disable_document_editing_ui", text="", icon="CANCEL") else: - row.operator("bim.load_project_documents", text="", icon="IMPORT") + right_row.operator("bim.load_project_documents", text="", icon="IMPORT") if not self.props.is_editing: return row = self.layout.row(align=True) - if self.props.breadcrumbs: - row.operator("bim.load_parent_document", text="", icon="FRAME_PREV") - row.label(text=DocumentData.data["parent_document"]) - else: - row.alignment = "RIGHT" - row.operator("bim.add_information", text="", icon="ADD") - if self.props.breadcrumbs: - row.operator("bim.add_document_reference", text="", icon="FILE_HIDDEN") + row.alignment = "RIGHT" - active_document = self.props.active_document - - if self.props.active_document_id: + if self.props.is_document_editing: row.operator("bim.edit_document", text="", icon="CHECKMARK") row.operator("bim.disable_editing_document", text="", icon="CANCEL") - elif active_document: - ifc_definition_id = active_document.ifc_definition_id - row.operator("bim.select_document_objects", text="", icon="RESTRICT_SELECT_OFF").document = ( - ifc_definition_id - ) - row.operator("bim.assign_document", text="", icon="BRUSH_DATA").document = ifc_definition_id - row.operator("bim.enable_editing_document", text="", icon="GREASEPENCIL").document = ifc_definition_id - row.operator("bim.remove_document", text="", icon="X").document = ifc_definition_id + else: + row.operator("bim.add_information", text="", icon="ADD") + + if self.props.documents and self.props.active_document_index < len(self.props.documents): + active_doc = self.props.documents[self.props.active_document_index] + if active_doc.is_information and active_doc.ifc_definition_id != -1: + row.operator("bim.add_document_reference", text="", icon="FILE_HIDDEN") + + active_document = self.props.active_document + if active_document: + ifc_definition_id = active_document.ifc_definition_id + row.operator("bim.select_document_objects", text="", icon="RESTRICT_SELECT_OFF").document = ( + ifc_definition_id + ) + row.operator("bim.assign_document", text="", icon="BRUSH_DATA").document = ifc_definition_id + row.operator("bim.enable_editing_document", text="", icon="GREASEPENCIL").document = ifc_definition_id + row.operator("bim.remove_document", text="", icon="X").document = ifc_definition_id self.layout.template_list("BIM_UL_documents", "", self.props, "documents", self.props, "active_document_index") - if self.props.active_document_id: - draw_attributes(self.props.document_attributes, self.layout) + if self.props.is_document_editing: + active_document = self.props.active_document + if active_document.is_information: + draw_attributes(self.props.document_attributes, self.layout) + else: + draw_attributes(self.props.document_attributes, self.layout, filter_attributes=["Name"]) + + if ( + self.props.is_editing + and self.props.documents + and self.props.active_document_index < len(self.props.documents) + ): + document = self.props.documents[self.props.active_document_index] + box = self.layout.box() + row = box.row(align=True) + row.label(text="Assigned Objects", icon="OUTLINER_OB_EMPTY") + box.template_list( + "BIM_UL_document_objects", + "", + self.props, + "document_objects", + self.props, + "active_document_object_index", + ) class BIM_PT_object_documents(Panel): @@ -110,57 +142,203 @@ class BIM_PT_object_documents(Panel): self.props = tool.Document.get_document_props() self.file = tool.Ifc.get() - self.draw_add_ui() - - if not ObjectDocumentData.data["documents"]: - row = self.layout.row(align=True) - row.label(text="No Documents", icon="FILE") - - for document in ObjectDocumentData.data["documents"]: - row = self.layout.row(align=True) - row.label(text=document["identification"] or "*", icon="FILE") - row.label(text=document["name"] or "Unnamed") - if document["location"]: - row.operator("bim.open_uri", icon="URL", text="").uri = document["location"] - row.operator("bim.unassign_document", text="", icon="X").document = document["id"] - - def draw_add_ui(self): - if not self.props.is_editing: - row = self.layout.row(align=True) - row.operator("bim.load_project_documents", text="Assign Document References", icon="ADD") - return + doc_count = len(ObjectDocumentData.data["documents"]) row = self.layout.row(align=True) - if self.props.breadcrumbs: - row.operator("bim.load_parent_document", text="", icon="FRAME_PREV") - row.label(text=DocumentData.data["parent_document"]) + row.label(text="{} Documents Assigned".format(doc_count), icon="FILE") + + if self.props.is_object_editing: + row.operator("bim.disable_object_document_editing_ui", text="", icon="CANCEL") else: + row.operator("bim.load_object_documents", text="", icon="IMPORT") + + if not self.props.is_object_editing and doc_count == 0: + row = self.layout.row() + row.label(text="No documents assigned", icon="INFO") + return + + if self.props.is_object_editing: + self.draw_add_ui() + if doc_count > 0: + box = self.layout.box() + row = box.row(align=True) + row.label(text="Assigned Documents", icon="OUTLINER_OB_EMPTY") + + box.template_list( + "BIM_UL_assigned_documents", + "", + self.props, + "assigned_documents", + self.props, + "active_assigned_document_index", + ) + + def draw_add_ui(self): + if self.props.is_object_editing: + row = self.layout.row(align=True) row.alignment = "RIGHT" - if self.props.documents and self.props.active_document_index < len(self.props.documents): - document = self.props.documents[self.props.active_document_index] - if not document.is_information: - row.operator("bim.assign_document", text="", icon="ADD").document = document.ifc_definition_id - row.operator("bim.disable_document_editing_ui", text="", icon="CANCEL") + if self.props.documents and self.props.active_document_index < len(self.props.documents): + document = self.props.documents[self.props.active_document_index] - self.layout.template_list("BIM_UL_documents", "", self.props, "documents", self.props, "active_document_index") + assigned_doc_ids = [] + for doc in ObjectDocumentData.data["documents"]: + assigned_doc_ids.append(doc["id"]) + + if document.ifc_definition_id not in assigned_doc_ids: + doc_op = row.operator("bim.assign_document", text="", icon="BRUSH_DATA") + doc_op.document = document.ifc_definition_id # Pass the current document's ID + else: + row.label(text="", icon="CHECKMARK") + + self.layout.template_list( + "BIM_UL_documents", "", self.props, "documents", self.props, "active_document_index" + ) class BIM_UL_documents(UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_propname): + if item: + row = layout.row(align=True) + indent_depth = 0 + + if item.ifc_definition_id != -1: + if item.tree_depth > 1: + indent_depth = item.tree_depth - 1 + for i in range(indent_depth): + row.label(text="", icon="BLANK1") + if item.ifc_definition_id == -1: + row.label(text="", icon="OUTLINER_COLLECTION") + row.label(text=item.name) + return + if item.is_information and item.has_children: + op = row.operator( + "bim.toggle_document", icon="TRIA_DOWN" if item.is_expanded else "TRIA_RIGHT", text="", emboss=False + ) + op.document = item.ifc_definition_id + op.option = "Collapse" if item.is_expanded else "Expand" + elif item.is_information: + row.label(text="", icon="BLANK1") + if item.is_information: + row.label(text="", icon="FILE") + text = " - ".join([x for x in [item.name, item.location] if x]) + else: + row.label(text="", icon="FILE_HIDDEN") + text = " - ".join([x for x in [item.description, item.location] if x]) + split1 = row.split(factor=0.1) + split1.prop(item, "identification", text="", emboss=False) + split2 = split1.split(factor=0.8) + split2.label(text=text) + + if item.location: + if item.location.lower().endswith(".ifc"): + row.operator("bim.open_ifc_document", icon="HIDE_OFF", text="").uri = item.location + row.operator("bim.open_uri", icon="URL", text="").uri = item.location + + +class BIM_UL_document_objects(UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_propname): + if item: + row = layout.row(align=True) + row.prop(item, "name", text="", emboss=False, icon="OBJECT_DATA") + row.operator("bim.select_object", text="", icon="RESTRICT_SELECT_OFF").obj_name = item.name + + props = tool.Document.get_document_props() + if props.documents and props.active_document_index < len(props.documents): + document = props.documents[props.active_document_index] + + op = row.operator("bim.unassign_document", text="", icon="X") + op.document = document.ifc_definition_id + op.obj = item.name + + +class BIM_UL_assigned_documents(UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname): if item: row = layout.row(align=True) if item.is_information: - op = row.operator("bim.load_document", text="", emboss=False, icon="DISCLOSURE_TRI_RIGHT") - op.document = item.ifc_definition_id row.label(text="", icon="FILE") else: - row.label(text="", icon="BLANK1") row.label(text="", icon="FILE_HIDDEN") - split1 = row.split(factor=0.1) - # split1.label(text=item.identification) - split1.prop(item, "identification", text="", emboss=False) - split2 = split1.split(factor=0.9) - split2.prop(item, "name", text="", emboss=False) + split1 = row.split(factor=0.2) + split1.label(text=item.identification or "") + + split2 = split1.split(factor=1.0) + if item.is_information: + split2.label(text=item.name or "Unnamed") + else: + split2.label(text=item.description or "No Description") + + if item.location: + if item.location.lower().endswith(".ifc"): + row.operator("bim.open_ifc_document", icon="HIDE_OFF", text="").uri = item.location + row.operator("bim.open_uri", icon="URL", text="").uri = item.location + op = row.operator("bim.unassign_document", text="", icon="X") + op.document = item.ifc_definition_id + + +def add_object_documents_context_menu(self, context): + if not context.active_object: + return + + if not tool.Blender.get_ifc_definition_id(context.active_object): + return + + self.layout.separator() + self.layout.menu("BIM_MT_object_documents_context_menu", icon="FILE") + + +class BIM_MT_object_documents_context_menu(bpy.types.Menu): + bl_idname = "BIM_MT_object_documents_context_menu" + bl_label = "Documents" + + def draw(self, context): + layout = self.layout + + if not context.selected_objects: + layout.label(text="No documents", icon="INFO") + return + + if len(context.selected_objects) > 1: + layout.label(text="Select a single object to see its referenced documents", icon="INFO") + return + + obj = context.active_object + if not obj or not tool.Blender.get_ifc_definition_id(obj): + layout.label(text="No documents", icon="INFO") + return + + if not ObjectDocumentData.is_loaded: + ObjectDocumentData.load() + + if not ObjectDocumentData.data["documents"]: + layout.label(text="No Documents", icon="FILE") + else: + for document in ObjectDocumentData.data["documents"]: + row = layout.row(align=True) + + with_ifc_icon = document["location"] and document["location"].lower().endswith(".ifc") + with_url_icon = bool(document["location"]) + + if with_ifc_icon: + row.operator("bim.open_ifc_document", icon="HIDE_OFF", text="").uri = document["location"] + else: + row.label(text="", icon="BLANK1") + + if with_url_icon: + row.operator("bim.open_uri", icon="URL", text="").uri = document["location"] + else: + row.label(text="", icon="BLANK1") + + doc_entity = None + if "id" in document: + doc_entity = tool.Ifc.get().by_id(document["id"]) + + if doc_entity and doc_entity.is_a("IfcDocumentReference"): + display_text = document.get("description") or "" + else: + display_text = document.get("name") or "" + + row.label(text=f"{document['identification'] or ''}: {display_text}") diff --git a/src/bonsai/bonsai/core/document.py b/src/bonsai/bonsai/core/document.py index e82360dbb9..64de5799ed 100644 --- a/src/bonsai/bonsai/core/document.py +++ b/src/bonsai/bonsai/core/document.py @@ -21,6 +21,7 @@ from typing import TYPE_CHECKING, Optional if TYPE_CHECKING: import bpy + import json import ifcopenshell import bonsai.tool as tool @@ -28,28 +29,22 @@ if TYPE_CHECKING: def load_project_documents(document: tool.Document) -> None: document.clear_document_tree() document.import_project_documents() - document.clear_breadcrumbs() document.enable_editing_ui() def load_document(document_tool: tool.Document, document: ifcopenshell.entity_instance) -> None: document_tool.clear_document_tree() - document_tool.import_subdocuments(document) - document_tool.import_references(document) + try: + expanded_docs = json.loads(bpy.context.scene.ExpandedDocuments.json_string) + except (AttributeError, json.JSONDecodeError): + expanded_docs = [] + + if document.id() not in expanded_docs: + expanded_docs.append(document.id()) + bpy.context.scene.ExpandedDocuments.json_string = json.dumps(expanded_docs) + + document_tool.import_project_documents() document_tool.disable_editing_document() - document_tool.add_breadcrumb(document) - - -def load_parent_document(document: tool.Document) -> None: - document.clear_document_tree() - document.remove_latest_breadcrumb() - parent = document.get_active_breadcrumb() - if parent: - document.import_subdocuments(parent) - document.import_references(parent) - document.disable_editing_document() - else: - document.import_project_documents() def disable_document_editing_ui(document: tool.Document) -> None: @@ -58,33 +53,62 @@ def disable_document_editing_ui(document: tool.Document) -> None: def enable_editing_document(document_tool: tool.Document, document: ifcopenshell.entity_instance) -> None: + props = document_tool.get_document_props() + props.active_document_id = document.id() + props.is_document_editing = True document_tool.import_document_attributes(document) - document_tool.set_active_document(document) def disable_editing_document(document: tool.Document) -> None: - document.disable_editing_document() + props = document.get_document_props() + props.active_document_id = 0 + props.is_document_editing = False + props.document_attributes.clear() -def add_information(ifc: tool.Ifc, document: tool.Document) -> None: - document.clear_document_tree() - parent = document.get_active_breadcrumb() +def add_information(ifc: tool.Ifc, document_tool: tool.Document, parent=None) -> ifcopenshell.entity_instance: + document_tool.clear_document_tree() + + if parent is None and ifc.get().by_type("IfcProject"): + parent = ifc.get().by_type("IfcProject")[0] + information = ifc.run("document.add_information", parent=parent) ifc.run("document.add_reference", information=information) - if parent: - document.import_subdocuments(parent) - document.import_references(parent) - else: - document.import_project_documents() + if parent and parent.is_a("IfcDocumentInformation"): + try: + expanded_docs = json.loads(bpy.context.scene.ExpandedDocuments.json_string) + except (AttributeError, json.JSONDecodeError): + expanded_docs = [] + + if parent.id() not in expanded_docs: + expanded_docs.append(parent.id()) + bpy.context.scene.ExpandedDocuments.json_string = json.dumps(expanded_docs) + + document_tool.import_project_documents() def add_reference(ifc: tool.Ifc, document: tool.Document) -> None: - parent = document.get_active_breadcrumb() - assert parent - ifc.run("document.add_reference", information=parent) - document.clear_document_tree() - document.import_subdocuments(parent) - document.import_references(parent) + props = document.get_document_props() + parent = None + + if props.documents and props.active_document_index < len(props.documents): + selected_document = props.documents[props.active_document_index] + if selected_document.is_information: + parent = ifc.get().by_id(selected_document.ifc_definition_id) + + if parent: + reference = ifc.run("document.add_reference", information=parent) + reference.Location = "" + try: + expanded_docs = json.loads(bpy.context.scene.ExpandedDocuments.json_string) + except (AttributeError, json.JSONDecodeError): + expanded_docs = [] + + if parent.id() not in expanded_docs: + expanded_docs.append(parent.id()) + bpy.context.scene.ExpandedDocuments.json_string = json.dumps(expanded_docs) + + document.import_project_documents() def edit_document(ifc: tool.Ifc, document_tool: tool.Document, document: ifcopenshell.entity_instance) -> None: @@ -95,12 +119,7 @@ def edit_document(ifc: tool.Ifc, document_tool: tool.Document, document: ifcopen ifc.run("document.edit_reference", reference=document, attributes=attributes) document_tool.disable_editing_document() document_tool.clear_document_tree() - parent = document_tool.get_active_breadcrumb() - if parent: - document_tool.import_subdocuments(parent) - document_tool.import_references(parent) - else: - document_tool.import_project_documents() + document_tool.import_project_documents() def remove_document(ifc: tool.Ifc, document_tool: tool.Document, document: ifcopenshell.entity_instance) -> None: @@ -109,12 +128,7 @@ def remove_document(ifc: tool.Ifc, document_tool: tool.Document, document: ifcop ifc.run("document.remove_information", information=document) else: ifc.run("document.remove_reference", reference=document) - parent = document_tool.get_active_breadcrumb() - if parent: - document_tool.import_subdocuments(parent) - document_tool.import_references(parent) - else: - document_tool.import_project_documents() + document_tool.import_project_documents() def assign_document( diff --git a/src/bonsai/bonsai/core/tool.py b/src/bonsai/bonsai/core/tool.py index d8e4dd2396..4061914bee 100644 --- a/src/bonsai/bonsai/core/tool.py +++ b/src/bonsai/bonsai/core/tool.py @@ -287,20 +287,14 @@ class Debug: @interface class Document: - def add_breadcrumb(cls, document): pass - def clear_breadcrumbs(cls): pass def clear_document_tree(cls): pass def disable_editing_document(cls): pass def disable_editing_ui(cls): pass def enable_editing_ui(cls): pass def export_document_attributes(cls): pass - def get_active_breadcrumb(cls): pass def import_document_attributes(cls, document): pass def import_project_documents(cls): pass - def import_references(cls, document): pass - def import_subdocuments(cls, document): pass def is_document_information(cls, document): pass - def remove_latest_breadcrumb(cls): pass def set_active_document(cls, document): pass diff --git a/src/bonsai/bonsai/tool/document.py b/src/bonsai/bonsai/tool/document.py index 3a2f89aa94..5bb6ad813a 100644 --- a/src/bonsai/bonsai/tool/document.py +++ b/src/bonsai/bonsai/tool/document.py @@ -18,6 +18,7 @@ from __future__ import annotations import bpy +import json import ifcopenshell.util.system import bonsai.bim.helper import bonsai.core.tool @@ -33,17 +34,6 @@ class Document(bonsai.core.tool.Document): def get_document_props(cls) -> BIMDocumentProperties: return bpy.context.scene.BIMDocumentProperties - @classmethod - def add_breadcrumb(cls, document: ifcopenshell.entity_instance) -> None: - props = cls.get_document_props() - new = props.breadcrumbs.add() - new.name = str(document.id()) - - @classmethod - def clear_breadcrumbs(cls) -> None: - props = cls.get_document_props() - props.breadcrumbs.clear() - @classmethod def clear_document_tree(cls) -> None: props = cls.get_document_props() @@ -69,18 +59,15 @@ class Document(bonsai.core.tool.Document): props = cls.get_document_props() return bonsai.bim.helper.export_attributes(props.document_attributes) - @classmethod - def get_active_breadcrumb(cls) -> Union[ifcopenshell.entity_instance, None]: - props = cls.get_document_props() - if len(props.breadcrumbs): - return tool.Ifc.get().by_id(int(props.breadcrumbs[-1].name)) - @classmethod def import_document_attributes(cls, document: ifcopenshell.entity_instance) -> None: props = cls.get_document_props() props.document_attributes.clear() - def callback(attr_name: str, _, data: dict[str, Any]) -> Union[bool, None]: + def callback(attr_name: str, attr_value: Any, data: dict[str, Any]) -> Union[bool, None]: + if attr_name == "Location" and attr_value is None: + data[attr_name] = "" + return True if attr_name != "Name": return None # Proceed normally @@ -100,52 +87,138 @@ class Document(bonsai.core.tool.Document): def import_project_documents(cls) -> None: props = cls.get_document_props() props.documents.clear() - project = tool.Ifc.get().by_type("IfcProject")[0] + file = tool.Ifc.get() + try: + expanded_documents = json.loads(bpy.context.scene.ExpandedDocuments.json_string) + except (AttributeError, json.JSONDecodeError): + expanded_documents = [] + + project = file.by_type("IfcProject")[0] if file.by_type("IfcProject") else None + if not project: + return + + document_children = {} + + for rel in file.by_type("IfcDocumentInformationRelationship"): + parent_id = rel.RelatingDocument.id() + if parent_id not in document_children: + document_children[parent_id] = [] + + for child in rel.RelatedDocuments: + document_children[parent_id].append(child) + + is_ifc2x3 = file.schema == "IFC2X3" + + if is_ifc2x3: + for ref in file.by_type("IfcDocumentReference"): + if ref.ReferenceToDocument: + parent = ref.ReferenceToDocument[0] + parent_id = parent.id() + if parent_id not in document_children: + document_children[parent_id] = [] + document_children[parent_id].append(ref) + else: + for ref in file.by_type("IfcDocumentReference"): + if hasattr(ref, "ReferencedDocument") and ref.ReferencedDocument: + parent = ref.ReferencedDocument + parent_id = parent.id() + if parent_id not in document_children: + document_children[parent_id] = [] + document_children[parent_id].append(ref) + + root_documents = [] for rel in project.HasAssociations or []: if rel.is_a("IfcRelAssociatesDocument") and rel.RelatingDocument.is_a("IfcDocumentInformation"): - element = rel.RelatingDocument - new = props.documents.add() - new.ifc_definition_id = element.id() - new["name"] = element.Name or "Unnamed" - new.is_information = True - new["identification"] = cls.get_document_information_id(element) + is_child = False + for children in document_children.values(): + if rel.RelatingDocument in children: + is_child = True + break + + if not is_child: + root_documents.append(rel.RelatingDocument) + + root = props.documents.add() + root.ifc_definition_id = -1 + root.is_information = True + root.name = f"Project Documents ({project.Name or 'Unnamed Project'})" + root.identification = "" + root.location = "" + root.tree_depth = 0 + root.has_children = bool(root_documents) + + root_id = -project.id() + + root.is_expanded = root_id not in expanded_documents + + if root.is_expanded: + root_documents.sort( + key=lambda doc: ((cls.get_document_information_id(doc) or "").lower(), (doc.Name or "").lower()) + ) + + for doc in root_documents: + cls._process_document(doc, props, document_children, expanded_documents, 1) @classmethod - def import_references(cls, document: ifcopenshell.entity_instance) -> None: - props = cls.get_document_props() - is_ifc2x3 = tool.Ifc.get_schema() == "IFC2X3" - references = cls.get_document_references(document) - for element in references: - new = props.documents.add() - new.ifc_definition_id = element.id() - # Use Description + Location instead of Name as IFC has a restriction - # for IfcDocumentReference to have Name only if it has no ReferencedDocument. - name = " - ".join([x for x in [element.Description, element.Location] if x]) - new["name"] = name or "Unnamed" - new["identification"] = cls.get_external_reference_id(element) - new.is_information = False + def _process_document(cls, document, props, document_children, expanded_documents, depth): + new = props.documents.add() + new.ifc_definition_id = document.id() + new.is_information = document.is_a("IfcDocumentInformation") + new.tree_depth = depth - @classmethod - def import_subdocuments(cls, document: ifcopenshell.entity_instance) -> None: - props = cls.get_document_props() - if document.IsPointer: - for element in document.IsPointer[0].RelatedDocuments or []: - new = props.documents.add() - new.ifc_definition_id = element.id() - new["name"] = element.Name or "Unnamed" - new.is_information = True - new["identification"] = cls.get_document_information_id(element) or "*" + file = document.file + if new.is_information: + new.name = document.Name or "Unnamed" + new.identification = cls.get_document_information_id(document) or "" + new.location = document.Location or "" + else: + new.name = document.Name or "" + new.identification = cls.get_external_reference_id(document) or "" + new.description = document.Description or "" + new.location = document.Location or "" + + if not new.is_information: + if file.schema == "IFC2X3": + if document.ReferenceToDocument: + doc_info = document.ReferenceToDocument[0] + if not new.name: + new.name = doc_info.Name or "" + new.location = new.location or "" + else: + if hasattr(document, "ReferencedDocument") and document.ReferencedDocument: + doc_info = document.ReferencedDocument + if not new.name: + new.name = doc_info.Name or "" + new.location = new.location or "" + + doc_id = document.id() + has_children = doc_id in document_children and bool(document_children[doc_id]) + new.has_children = has_children + new.is_expanded = doc_id in expanded_documents + + if has_children and new.is_expanded: + children = document_children[doc_id] + + children.sort( + key=lambda doc: ( + doc.is_a("IfcDocumentInformation"), + ( + cls.get_document_information_id(doc) + if doc.is_a("IfcDocumentInformation") + else cls.get_external_reference_id(doc) or "" + ).lower(), + (doc.Name or "").lower(), + ), + reverse=True, + ) + + for child in children: + cls._process_document(child, props, document_children, expanded_documents, depth + 1) @classmethod def is_document_information(cls, document: ifcopenshell.entity_instance) -> bool: return document.is_a("IfcDocumentInformation") - @classmethod - def remove_latest_breadcrumb(cls) -> None: - props = cls.get_document_props() - if len(props.breadcrumbs): - props.breadcrumbs.remove(len(props.breadcrumbs) - 1) - @classmethod def set_active_document(cls, document: ifcopenshell.entity_instance) -> None: props = cls.get_document_props() diff --git a/src/bonsai/test/bim/feature/document.feature b/src/bonsai/test/bim/feature/document.feature index 9b4a34cfb4..9ef9657499 100644 --- a/src/bonsai/test/bim/feature/document.feature +++ b/src/bonsai/test/bim/feature/document.feature @@ -14,15 +14,6 @@ Scenario: Load document When I press "bim.load_document(document={information})" Then nothing happens -Scenario: Load parent document - Given an empty IFC project - And I press "bim.load_project_documents" - And I press "bim.add_information" - And the variable "information" is "{ifc}.by_type('IfcDocumentInformation')[-1].id()" - And I press "bim.load_document(document={information})" - When I press "bim.load_parent_document" - Then nothing happens - Scenario: Disable document editing UI Given an empty IFC project And I press "bim.load_project_documents" diff --git a/src/bonsai/test/core/test_document.py b/src/bonsai/test/core/test_document.py index 48421c1fe4..ab3563551d 100644 --- a/src/bonsai/test/core/test_document.py +++ b/src/bonsai/test/core/test_document.py @@ -25,7 +25,6 @@ class TestLoadProjectDocuments: def test_run(self, document): document.clear_document_tree().should_be_called() document.import_project_documents().should_be_called() - document.clear_breadcrumbs().should_be_called() document.enable_editing_ui().should_be_called() subject.load_project_documents(document) @@ -33,8 +32,6 @@ class TestLoadProjectDocuments: class TestLoadDocument: def test_run(self, document): document.clear_document_tree().should_be_called() - document.import_subdocuments("document").should_be_called() - document.import_references("document").should_be_called() document.disable_editing_document().should_be_called() document.add_breadcrumb("document").should_be_called() subject.load_document(document, document="document") @@ -63,7 +60,6 @@ class TestDisableEditingDocument: class TestAddInformation: def test_add_and_reload_tree_at_project_root(self, ifc, document): document.clear_document_tree().should_be_called() - document.get_active_breadcrumb().should_be_called().will_return(None) ifc.run("document.add_information", parent=None).should_be_called().will_return("information") ifc.run("document.add_reference", information="information").should_be_called() document.import_project_documents().should_be_called() @@ -71,21 +67,15 @@ class TestAddInformation: def test_add_and_reload_tree_at_current_parent(self, ifc, document): document.clear_document_tree().should_be_called() - document.get_active_breadcrumb().should_be_called().will_return("parent") ifc.run("document.add_information", parent="parent").should_be_called().will_return("information") ifc.run("document.add_reference", information="information").should_be_called() - document.import_subdocuments("parent").should_be_called() - document.import_references("parent").should_be_called() subject.add_information(ifc, document) class TestAddReference: def test_run(self, ifc, document): - document.get_active_breadcrumb().should_be_called().will_return("parent") ifc.run("document.add_reference", information="parent").should_be_called() document.clear_document_tree().should_be_called() - document.import_subdocuments("parent").should_be_called() - document.import_references("parent").should_be_called() subject.add_reference(ifc, document) @@ -96,7 +86,6 @@ class TestEditDocument: ifc.run("document.edit_information", information="document", attributes="attributes").should_be_called() document.disable_editing_document().should_be_called() document.clear_document_tree().should_be_called() - document.get_active_breadcrumb().should_be_called().will_return(None) document.import_project_documents().should_be_called() subject.edit_document(ifc, document, document="document") @@ -106,9 +95,6 @@ class TestEditDocument: ifc.run("document.edit_reference", reference="document", attributes="attributes").should_be_called() document.disable_editing_document().should_be_called() document.clear_document_tree().should_be_called() - document.get_active_breadcrumb().should_be_called().will_return("parent") - document.import_subdocuments("parent").should_be_called() - document.import_references("parent").should_be_called() subject.edit_document(ifc, document, document="document") @@ -117,7 +103,6 @@ class TestRemoveDocument: document.clear_document_tree().should_be_called() document.is_document_information("document").should_be_called().will_return(True) ifc.run("document.remove_information", information="document").should_be_called() - document.get_active_breadcrumb().should_be_called().will_return(None) document.import_project_documents().should_be_called() subject.remove_document(ifc, document, document="document") @@ -125,9 +110,6 @@ class TestRemoveDocument: document.clear_document_tree().should_be_called() document.is_document_information("document").should_be_called().will_return(False) ifc.run("document.remove_reference", reference="document").should_be_called() - document.get_active_breadcrumb().should_be_called().will_return("parent") - document.import_subdocuments("parent").should_be_called() - document.import_references("parent").should_be_called() subject.remove_document(ifc, document, document="document") diff --git a/src/bonsai/test/tool/test_document.py b/src/bonsai/test/tool/test_document.py index 9e3556afd5..3d313f6293 100644 --- a/src/bonsai/test/tool/test_document.py +++ b/src/bonsai/test/tool/test_document.py @@ -31,24 +31,6 @@ class TestImplementsTool(NewFile): assert isinstance(subject(), bonsai.core.tool.Document) -class TestAddBreadcrumb(NewFile): - def test_run(self): - ifc = ifcopenshell.file() - tool.Ifc().set(ifc) - document = ifc.createIfcDocumentInformation() - subject.add_breadcrumb(document) - props = tool.Document.get_document_props() - assert props.breadcrumbs[0].name == str(document.id()) - - -class TestClearBreadcrumbs(NewFile): - def test_run(self): - props = tool.Document.get_document_props() - props.breadcrumbs.add() - subject.clear_breadcrumbs() - assert len(props.breadcrumbs) == 0 - - class TestClearDocumentTree(NewFile): def test_run(self): props = tool.Document.get_document_props() @@ -103,15 +85,6 @@ class TestExportDocumentAttributes(NewFile): } -class TestGetActiveBreadcrumb(NewFile): - def test_run(self): - ifc = ifcopenshell.file() - tool.Ifc().set(ifc) - document = ifc.createIfcDocumentInformation() - subject.add_breadcrumb(document) - assert subject.get_active_breadcrumb() == document - - class TestImportDocumentAttributes(NewFile): def test_importing_information(self): ifc = ifcopenshell.file() @@ -197,22 +170,6 @@ class TestImportReferences(NewFile): assert props.documents[0].is_information is False -class TestImportSubdocuments(NewFile): - def test_run(self): - ifc = ifcopenshell.file() - tool.Ifc().set(ifc) - ifc.createIfcProject() - document = ifcopenshell.api.document.add_information(ifc) - subdocument = ifcopenshell.api.document.add_information(ifc, parent=document) - subject.import_subdocuments(document) - props = tool.Document.get_document_props() - assert len(props.documents) == 1 - assert props.documents[0].ifc_definition_id == subdocument.id() - assert props.documents[0].name == "Unnamed" - assert props.documents[0].identification == "X" - assert props.documents[0].is_information is True - - class TestIsDocumentInformation(NewFile): def test_run(self): ifc = ifcopenshell.file() @@ -222,15 +179,6 @@ class TestIsDocumentInformation(NewFile): assert subject.is_document_information(reference) is False -class TestRemoveLatestBreadcrumb(NewFile): - def test_run(self): - props = tool.Document.get_document_props() - props.breadcrumbs.add() - props.breadcrumbs.add() - subject.remove_latest_breadcrumb() - assert len(props.breadcrumbs) == 1 - - class TestSetActiveDocument(NewFile): def test_run(self): ifc = ifcopenshell.file() From d2adfc8c5d74c88125ed7257d000e6c53cd381dc Mon Sep 17 00:00:00 2001 From: falken10 Date: Fri, 20 Jun 2025 12:30:40 +0200 Subject: [PATCH 02/12] updated document ui --- src/bonsai/bonsai/bim/helper.py | 3 ++ src/bonsai/bonsai/bim/module/document/ui.py | 13 +++++--- src/bonsai/bonsai/core/document.py | 4 +-- src/bonsai/bonsai/tool/document.py | 33 +++++++++++---------- 4 files changed, 31 insertions(+), 22 deletions(-) diff --git a/src/bonsai/bonsai/bim/helper.py b/src/bonsai/bonsai/bim/helper.py index e76b512de3..eb8d9fa6e4 100644 --- a/src/bonsai/bonsai/bim/helper.py +++ b/src/bonsai/bonsai/bim/helper.py @@ -55,6 +55,7 @@ def draw_attributes( layout: bpy.types.UILayout, copy_operator: Optional[str] = None, popup_active_attribute: Optional[bonsai.bim.prop.Attribute] = None, + filter_attributes: list[str] = None, callback: Optional[Callable[[bonsai.bim.prop.Attribute, bpy.types.UILayout], None]] = None, *, enable_search: Union[bool, EllipsisType] = ..., @@ -75,6 +76,8 @@ def draw_attributes( """ for attribute in props: + if attribute.name in (filter_attributes or []): + continue row = layout.row(align=True) if attribute == popup_active_attribute: row.activate_init = True diff --git a/src/bonsai/bonsai/bim/module/document/ui.py b/src/bonsai/bonsai/bim/module/document/ui.py index eab2d146fb..d795cccdd4 100644 --- a/src/bonsai/bonsai/bim/module/document/ui.py +++ b/src/bonsai/bonsai/bim/module/document/ui.py @@ -68,7 +68,10 @@ class BIM_PT_documents(Panel): row.operator("bim.edit_document", text="", icon="CHECKMARK") row.operator("bim.disable_editing_document", text="", icon="CANCEL") else: - row.operator("bim.add_information", text="", icon="ADD") + if not self.props.documents or not self.props.active_document_index < len(self.props.documents) or \ + (self.props.active_document_index < len(self.props.documents) and + self.props.documents[self.props.active_document_index].is_information): + row.operator("bim.add_information", text="", icon="ADD") if self.props.documents and self.props.active_document_index < len(self.props.documents): active_doc = self.props.documents[self.props.active_document_index] @@ -81,10 +84,10 @@ class BIM_PT_documents(Panel): row.operator("bim.select_document_objects", text="", icon="RESTRICT_SELECT_OFF").document = ( ifc_definition_id ) + row.operator("bim.assign_document", text="", icon="BRUSH_DATA").document = ifc_definition_id row.operator("bim.enable_editing_document", text="", icon="GREASEPENCIL").document = ifc_definition_id row.operator("bim.remove_document", text="", icon="X").document = ifc_definition_id - self.layout.template_list("BIM_UL_documents", "", self.props, "documents", self.props, "active_document_index") if self.props.is_document_editing: @@ -185,10 +188,12 @@ class BIM_PT_object_documents(Panel): for doc in ObjectDocumentData.data["documents"]: assigned_doc_ids.append(doc["id"]) - if document.ifc_definition_id not in assigned_doc_ids: + # Only show assign button if the document is information (not reference) and not already assigned + if (document.is_information and + document.ifc_definition_id not in assigned_doc_ids): doc_op = row.operator("bim.assign_document", text="", icon="BRUSH_DATA") doc_op.document = document.ifc_definition_id # Pass the current document's ID - else: + elif document.ifc_definition_id in assigned_doc_ids: row.label(text="", icon="CHECKMARK") self.layout.template_list( diff --git a/src/bonsai/bonsai/core/document.py b/src/bonsai/bonsai/core/document.py index 64de5799ed..38ca080d83 100644 --- a/src/bonsai/bonsai/core/document.py +++ b/src/bonsai/bonsai/core/document.py @@ -18,10 +18,10 @@ from __future__ import annotations from typing import TYPE_CHECKING, Optional +import bpy +import json if TYPE_CHECKING: - import bpy - import json import ifcopenshell import bonsai.tool as tool diff --git a/src/bonsai/bonsai/tool/document.py b/src/bonsai/bonsai/tool/document.py index 5bb6ad813a..d3eeda694b 100644 --- a/src/bonsai/bonsai/tool/document.py +++ b/src/bonsai/bonsai/tool/document.py @@ -69,15 +69,12 @@ class Document(bonsai.core.tool.Document): data[attr_name] = "" return True if attr_name != "Name": - return None # Proceed normally + return None current_value = data[attr_name] - # If Name is already filled, display it so user would be able to correct invalid IFC. if current_value is not None: return None - # Skip import since IFC restricts Name to be filled - # for IfcDocumentReference with ReferencedDocument. return False import_callback = callback if document.is_a("IfcDocumentReference") else None @@ -199,20 +196,24 @@ class Document(bonsai.core.tool.Document): if has_children and new.is_expanded: children = document_children[doc_id] - children.sort( + info_children = [d for d in children if d.is_a("IfcDocumentInformation")] + ref_children = [d for d in children if not d.is_a("IfcDocumentInformation")] + + info_children.sort( key=lambda doc: ( - doc.is_a("IfcDocumentInformation"), - ( - cls.get_document_information_id(doc) - if doc.is_a("IfcDocumentInformation") - else cls.get_external_reference_id(doc) or "" - ).lower(), - (doc.Name or "").lower(), - ), - reverse=True, + (cls.get_document_information_id(doc) or "").lower(), + (doc.Name or "").lower() + ) ) - - for child in children: + + ref_children.sort( + key=lambda doc: ( + (cls.get_external_reference_id(doc) or "").lower(), + (doc.Description or doc.Name or "").lower() + ) + ) + + for child in info_children + ref_children: cls._process_document(child, props, document_children, expanded_documents, depth + 1) @classmethod From aa8c146f9da9add8a00fd48c9adea26cb2d99c45 Mon Sep 17 00:00:00 2001 From: falken10 Date: Tue, 1 Jul 2025 17:34:45 +0200 Subject: [PATCH 03/12] Refactoring based on developer's feedback --- .../bonsai/bim/module/document/__init__.py | 8 +-- src/bonsai/bonsai/bim/module/document/data.py | 48 +++++++------- .../bonsai/bim/module/document/operator.py | 33 +++++----- src/bonsai/bonsai/bim/module/document/prop.py | 9 +-- src/bonsai/bonsai/bim/module/document/ui.py | 31 ++++----- src/bonsai/bonsai/core/document.py | 56 ++++------------- src/bonsai/bonsai/tool/document.py | 63 ++++++++++++++++--- 7 files changed, 122 insertions(+), 126 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/document/__init__.py b/src/bonsai/bonsai/bim/module/document/__init__.py index 6a925d0f98..323520420e 100644 --- a/src/bonsai/bonsai/bim/module/document/__init__.py +++ b/src/bonsai/bonsai/bim/module/document/__init__.py @@ -18,7 +18,6 @@ import bpy from . import ui, prop, operator -from bpy.types import VIEW3D_MT_object_context_menu classes = ( operator.AddDocumentReference, @@ -41,7 +40,6 @@ classes = ( prop.Document, prop.DocumentObject, prop.AssignedDocument, - prop.ExpandedDocuments, prop.BIMDocumentProperties, ui.BIM_PT_documents, ui.BIM_PT_object_documents, @@ -54,11 +52,9 @@ classes = ( def register(): bpy.types.Scene.BIMDocumentProperties = bpy.props.PointerProperty(type=prop.BIMDocumentProperties) - bpy.types.Scene.ExpandedDocuments = bpy.props.PointerProperty(type=prop.ExpandedDocuments) - VIEW3D_MT_object_context_menu.append(ui.add_object_documents_context_menu) + bpy.types.VIEW3D_MT_object_context_menu.append(ui.add_object_documents_context_menu) def unregister(): del bpy.types.Scene.BIMDocumentProperties - del bpy.types.Scene.ExpandedDocuments - VIEW3D_MT_object_context_menu.remove(ui.add_object_documents_context_menu) + bpy.types.VIEW3D_MT_object_context_menu.remove(ui.add_object_documents_context_menu) diff --git a/src/bonsai/bonsai/bim/module/document/data.py b/src/bonsai/bonsai/bim/module/document/data.py index be6b628bed..15a89f8870 100644 --- a/src/bonsai/bonsai/bim/module/document/data.py +++ b/src/bonsai/bonsai/bim/module/document/data.py @@ -119,13 +119,15 @@ class ObjectDocumentData: return results for rel in getattr(element, "HasAssociations", []): if rel.is_a("IfcRelAssociatesDocument"): - is_information = rel.RelatingDocument.is_a("IfcDocumentInformation") - is_reference = rel.RelatingDocument.is_a("IfcDocumentReference") + relating_document = rel.RelatingDocument + + is_information = relating_document.is_a("IfcDocumentInformation") + is_reference = relating_document.is_a("IfcDocumentReference") if not (is_information or is_reference): continue - name = rel.RelatingDocument.Name + name = relating_document.Name location = None identification = None @@ -133,33 +135,35 @@ class ObjectDocumentData: if is_information: if tool.Ifc.get_schema() == "IFC2X3": - identification = rel.RelatingDocument.DocumentId + identification = relating_document.DocumentId else: - identification = rel.RelatingDocument.Identification + identification = relating_document.Identification - location = getattr(rel.RelatingDocument, "Location", None) + location = getattr(relating_document, "Location", None) else: - description = rel.RelatingDocument.Description + description = relating_document.Description if tool.Ifc.get_schema() == "IFC2X3": - if not name and rel.RelatingDocument.ReferenceToDocument: - name = rel.RelatingDocument.ReferenceToDocument[0].Name + reference_to_document = relating_document.ReferenceToDocument + if not name and reference_to_document: + name = reference_to_document[0].Name - identification = rel.RelatingDocument.ItemReference - if not identification and rel.RelatingDocument.ReferenceToDocument: - identification = rel.RelatingDocument.ReferenceToDocument[0].DocumentId - location = rel.RelatingDocument.Location + identification = relating_document.ItemReference + if not identification and reference_to_document: + identification = reference_to_document[0].DocumentId + location = relating_document.Location else: - if not name and rel.RelatingDocument.ReferencedDocument: - name = rel.RelatingDocument.ReferencedDocument.Name + referenced_document = relating_document.ReferencedDocument + if not name and referenced_document: + name = referenced_document.Name - identification = rel.RelatingDocument.Identification - if not identification and rel.RelatingDocument.ReferencedDocument: - identification = rel.RelatingDocument.ReferencedDocument.Identification + identification = relating_document.Identification + if not identification and referenced_document: + identification = referenced_document.Identification - location = rel.RelatingDocument.Location - if location is None and rel.RelatingDocument.ReferencedDocument: - location = rel.RelatingDocument.ReferencedDocument.Location + location = relating_document.Location + if location is None and referenced_document: + location = referenced_document.Location if location: if not "://" in location: @@ -169,7 +173,7 @@ class ObjectDocumentData: results.append( { - "id": rel.RelatingDocument.id(), + "id": relating_document.id(), "identification": identification, "name": name, "location": location, diff --git a/src/bonsai/bonsai/bim/module/document/operator.py b/src/bonsai/bonsai/bim/module/document/operator.py index ffc81537ee..ecc95f6ac8 100644 --- a/src/bonsai/bonsai/bim/module/document/operator.py +++ b/src/bonsai/bonsai/bim/module/document/operator.py @@ -35,10 +35,8 @@ def update_document_objects(document_id=None): if document_id is None: props = tool.Document.get_document_props() - if props.documents and props.active_document_index < len(props.documents): - document = props.documents[props.active_document_index] - if document.ifc_definition_id: - document_id = document.ifc_definition_id + if props.active_document and props.active_document.ifc_definition_id: + document_id = props.active_document.ifc_definition_id if document_id: DocumentData.load_document_objects_into_props(document_id) @@ -122,8 +120,8 @@ class AddInformation(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): props = tool.Document.get_document_props() parent = None - if props.documents and props.active_document_index < len(props.documents): - selected_document = props.documents[props.active_document_index] + if props.active_document: + selected_document = props.active_document if selected_document.ifc_definition_id == -1: parent = tool.Ifc.get().by_type("IfcProject")[0] if tool.Ifc.get().by_type("IfcProject") else None @@ -139,7 +137,7 @@ class AddInformation(bpy.types.Operator, tool.Ifc.Operator): expanded_docs = [] try: - expanded_docs = json.loads(context.scene.ExpandedDocuments.json_string) + expanded_docs = json.loads(props.json_string) except (AttributeError, json.JSONDecodeError): pass @@ -152,7 +150,7 @@ class AddInformation(bpy.types.Operator, tool.Ifc.Operator): if parent.id() not in expanded_docs: expanded_docs.append(parent.id()) - context.scene.ExpandedDocuments.json_string = json.dumps(expanded_docs) + props.json_string = json.dumps(expanded_docs) bpy.ops.bim.load_project_documents() @@ -165,11 +163,11 @@ class AddDocumentReference(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): props = tool.Document.get_document_props() - if not props.documents or props.active_document_index >= len(props.documents): + if not props.active_document: self.report({"ERROR"}, "No document selected") return {"CANCELLED"} - selected_document = props.documents[props.active_document_index] + selected_document = props.active_document if not selected_document.is_information: self.report({"ERROR"}, "Cannot add a reference to a reference element") @@ -181,13 +179,13 @@ class AddDocumentReference(bpy.types.Operator, tool.Ifc.Operator): core.add_reference(tool.Ifc, tool.Document) expanded_docs = [] try: - expanded_docs = json.loads(context.scene.ExpandedDocuments.json_string) + expanded_docs = json.loads(props.json_string) except (AttributeError, json.JSONDecodeError): pass if parent.id() not in expanded_docs: expanded_docs.append(parent.id()) - context.scene.ExpandedDocuments.json_string = json.dumps(expanded_docs) + props.json_string = json.dumps(expanded_docs) bpy.ops.bim.load_project_documents() @@ -292,9 +290,8 @@ class UnassignDocument(bpy.types.Operator, tool.Ifc.Operator): core.unassign_document(tool.Ifc, product=element, document=document) props = tool.Document.get_document_props() active_document_id = None - if props.documents and props.active_document_index < len(props.documents): - active_document = props.documents[props.active_document_index] - active_document_id = active_document.ifc_definition_id + if props.active_document: + active_document_id = props.active_document.ifc_definition_id if active_document_id and active_document_id != self.document: update_document_objects(active_document_id) @@ -420,8 +417,9 @@ class ToggleDocument(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): expanded_documents = [] + props = tool.Document.get_document_props() try: - expanded_documents = json.loads(context.scene.ExpandedDocuments.json_string) + expanded_documents = json.loads(props.json_string) except (AttributeError, json.JSONDecodeError): expanded_documents = [] @@ -439,8 +437,7 @@ class ToggleDocument(bpy.types.Operator, tool.Ifc.Operator): expanded_documents.append(virtual_root_id) elif self.option == "Collapse" and virtual_root_id in expanded_documents: expanded_documents.remove(virtual_root_id) - - context.scene.ExpandedDocuments.json_string = json.dumps(expanded_documents) + props.json_string = json.dumps(expanded_documents) bpy.ops.bim.load_project_documents() return {"FINISHED"} diff --git a/src/bonsai/bonsai/bim/module/document/prop.py b/src/bonsai/bonsai/bim/module/document/prop.py index 88878f8bfc..ce4a6a83cc 100644 --- a/src/bonsai/bonsai/bim/module/document/prop.py +++ b/src/bonsai/bonsai/bim/module/document/prop.py @@ -80,13 +80,6 @@ class Document(PropertyGroup): is_expanded: bool -class ExpandedDocuments(PropertyGroup): - json_string: StringProperty(name="JSON String", default="[]") - - if TYPE_CHECKING: - json_string: str - - class DocumentObject(PropertyGroup): name: StringProperty(name="Name") ifc_definition_id: IntProperty(name="IFC Definition ID") @@ -124,6 +117,7 @@ class BIMDocumentProperties(PropertyGroup): active_document_object_index: IntProperty(name="Active Document Object Index") assigned_documents: CollectionProperty(name="Assigned Documents", type=AssignedDocument) active_assigned_document_index: IntProperty(name="Active Assigned Document Index") + json_string: StringProperty(name="JSON String", default="[]") if TYPE_CHECKING: document_attributes: bpy.types.bpy_prop_collection_idprop[Attribute] @@ -137,6 +131,7 @@ class BIMDocumentProperties(PropertyGroup): active_document_object_index: int assigned_documents: bpy.types.bpy_prop_collection_idprop[AssignedDocument] active_assigned_document_index: int + json_string: str @property def active_document(self) -> Union[Document, None]: diff --git a/src/bonsai/bonsai/bim/module/document/ui.py b/src/bonsai/bonsai/bim/module/document/ui.py index d795cccdd4..bad9819205 100644 --- a/src/bonsai/bonsai/bim/module/document/ui.py +++ b/src/bonsai/bonsai/bim/module/document/ui.py @@ -68,14 +68,11 @@ class BIM_PT_documents(Panel): row.operator("bim.edit_document", text="", icon="CHECKMARK") row.operator("bim.disable_editing_document", text="", icon="CANCEL") else: - if not self.props.documents or not self.props.active_document_index < len(self.props.documents) or \ - (self.props.active_document_index < len(self.props.documents) and - self.props.documents[self.props.active_document_index].is_information): + if not self.props.active_document or self.props.active_document.is_information: row.operator("bim.add_information", text="", icon="ADD") - if self.props.documents and self.props.active_document_index < len(self.props.documents): - active_doc = self.props.documents[self.props.active_document_index] - if active_doc.is_information and active_doc.ifc_definition_id != -1: + if self.props.active_document: + if self.props.active_document.is_information and self.props.active_document.ifc_definition_id != -1: row.operator("bim.add_document_reference", text="", icon="FILE_HIDDEN") active_document = self.props.active_document @@ -84,7 +81,7 @@ class BIM_PT_documents(Panel): row.operator("bim.select_document_objects", text="", icon="RESTRICT_SELECT_OFF").document = ( ifc_definition_id ) - + row.operator("bim.assign_document", text="", icon="BRUSH_DATA").document = ifc_definition_id row.operator("bim.enable_editing_document", text="", icon="GREASEPENCIL").document = ifc_definition_id row.operator("bim.remove_document", text="", icon="X").document = ifc_definition_id @@ -97,12 +94,8 @@ class BIM_PT_documents(Panel): else: draw_attributes(self.props.document_attributes, self.layout, filter_attributes=["Name"]) - if ( - self.props.is_editing - and self.props.documents - and self.props.active_document_index < len(self.props.documents) - ): - document = self.props.documents[self.props.active_document_index] + if self.props.is_editing and self.props.active_document: + document = self.props.active_document box = self.layout.box() row = box.row(align=True) row.label(text="Assigned Objects", icon="OUTLINER_OB_EMPTY") @@ -181,21 +174,19 @@ class BIM_PT_object_documents(Panel): row = self.layout.row(align=True) row.alignment = "RIGHT" - if self.props.documents and self.props.active_document_index < len(self.props.documents): - document = self.props.documents[self.props.active_document_index] + if self.props.active_document: + document = self.props.active_document assigned_doc_ids = [] for doc in ObjectDocumentData.data["documents"]: assigned_doc_ids.append(doc["id"]) # Only show assign button if the document is information (not reference) and not already assigned - if (document.is_information and - document.ifc_definition_id not in assigned_doc_ids): + if document.is_information and document.ifc_definition_id not in assigned_doc_ids: doc_op = row.operator("bim.assign_document", text="", icon="BRUSH_DATA") doc_op.document = document.ifc_definition_id # Pass the current document's ID elif document.ifc_definition_id in assigned_doc_ids: row.label(text="", icon="CHECKMARK") - self.layout.template_list( "BIM_UL_documents", "", self.props, "documents", self.props, "active_document_index" ) @@ -249,8 +240,8 @@ class BIM_UL_document_objects(UIList): row.operator("bim.select_object", text="", icon="RESTRICT_SELECT_OFF").obj_name = item.name props = tool.Document.get_document_props() - if props.documents and props.active_document_index < len(props.documents): - document = props.documents[props.active_document_index] + if props.active_document: + document = props.active_document op = row.operator("bim.unassign_document", text="", icon="X") op.document = document.ifc_definition_id diff --git a/src/bonsai/bonsai/core/document.py b/src/bonsai/bonsai/core/document.py index 38ca080d83..56d92ea613 100644 --- a/src/bonsai/bonsai/core/document.py +++ b/src/bonsai/bonsai/core/document.py @@ -18,8 +18,6 @@ from __future__ import annotations from typing import TYPE_CHECKING, Optional -import bpy -import json if TYPE_CHECKING: import ifcopenshell @@ -34,15 +32,7 @@ def load_project_documents(document: tool.Document) -> None: def load_document(document_tool: tool.Document, document: ifcopenshell.entity_instance) -> None: document_tool.clear_document_tree() - try: - expanded_docs = json.loads(bpy.context.scene.ExpandedDocuments.json_string) - except (AttributeError, json.JSONDecodeError): - expanded_docs = [] - - if document.id() not in expanded_docs: - expanded_docs.append(document.id()) - bpy.context.scene.ExpandedDocuments.json_string = json.dumps(expanded_docs) - + document_tool.expand_document(document) document_tool.import_project_documents() document_tool.disable_editing_document() @@ -53,60 +43,40 @@ def disable_document_editing_ui(document: tool.Document) -> None: def enable_editing_document(document_tool: tool.Document, document: ifcopenshell.entity_instance) -> None: - props = document_tool.get_document_props() - props.active_document_id = document.id() - props.is_document_editing = True + document_tool.set_active_document(document) + document_tool.enable_document_editing() document_tool.import_document_attributes(document) def disable_editing_document(document: tool.Document) -> None: - props = document.get_document_props() - props.active_document_id = 0 - props.is_document_editing = False - props.document_attributes.clear() + document.clear_active_document() + document.disable_document_editing() + document.clear_document_attributes() def add_information(ifc: tool.Ifc, document_tool: tool.Document, parent=None) -> ifcopenshell.entity_instance: document_tool.clear_document_tree() - if parent is None and ifc.get().by_type("IfcProject"): - parent = ifc.get().by_type("IfcProject")[0] + if parent is None: + parent = document_tool.get_default_parent_for_information(ifc) information = ifc.run("document.add_information", parent=parent) ifc.run("document.add_reference", information=information) - if parent and parent.is_a("IfcDocumentInformation"): - try: - expanded_docs = json.loads(bpy.context.scene.ExpandedDocuments.json_string) - except (AttributeError, json.JSONDecodeError): - expanded_docs = [] - if parent.id() not in expanded_docs: - expanded_docs.append(parent.id()) - bpy.context.scene.ExpandedDocuments.json_string = json.dumps(expanded_docs) + if document_tool.is_document_information(parent): + document_tool.expand_document(parent) document_tool.import_project_documents() + return information def add_reference(ifc: tool.Ifc, document: tool.Document) -> None: - props = document.get_document_props() - parent = None - - if props.documents and props.active_document_index < len(props.documents): - selected_document = props.documents[props.active_document_index] - if selected_document.is_information: - parent = ifc.get().by_id(selected_document.ifc_definition_id) + parent = document.get_selected_document_information(ifc) if parent: reference = ifc.run("document.add_reference", information=parent) reference.Location = "" - try: - expanded_docs = json.loads(bpy.context.scene.ExpandedDocuments.json_string) - except (AttributeError, json.JSONDecodeError): - expanded_docs = [] - - if parent.id() not in expanded_docs: - expanded_docs.append(parent.id()) - bpy.context.scene.ExpandedDocuments.json_string = json.dumps(expanded_docs) + document.expand_document(parent) document.import_project_documents() diff --git a/src/bonsai/bonsai/tool/document.py b/src/bonsai/bonsai/tool/document.py index d3eeda694b..76ceaf52c3 100644 --- a/src/bonsai/bonsai/tool/document.py +++ b/src/bonsai/bonsai/tool/document.py @@ -23,6 +23,7 @@ import ifcopenshell.util.system import bonsai.bim.helper import bonsai.core.tool import bonsai.tool as tool +import json from typing import Any, Union, TYPE_CHECKING if TYPE_CHECKING: @@ -86,7 +87,7 @@ class Document(bonsai.core.tool.Document): props.documents.clear() file = tool.Ifc.get() try: - expanded_documents = json.loads(bpy.context.scene.ExpandedDocuments.json_string) + expanded_documents = json.loads(props.json_string) except (AttributeError, json.JSONDecodeError): expanded_documents = [] @@ -116,7 +117,7 @@ class Document(bonsai.core.tool.Document): document_children[parent_id].append(ref) else: for ref in file.by_type("IfcDocumentReference"): - if hasattr(ref, "ReferencedDocument") and ref.ReferencedDocument: + if ref.ReferencedDocument: parent = ref.ReferencedDocument parent_id = parent.id() if parent_id not in document_children: @@ -198,21 +199,18 @@ class Document(bonsai.core.tool.Document): info_children = [d for d in children if d.is_a("IfcDocumentInformation")] ref_children = [d for d in children if not d.is_a("IfcDocumentInformation")] - + info_children.sort( - key=lambda doc: ( - (cls.get_document_information_id(doc) or "").lower(), - (doc.Name or "").lower() - ) + key=lambda doc: ((cls.get_document_information_id(doc) or "").lower(), (doc.Name or "").lower()) ) - + ref_children.sort( key=lambda doc: ( (cls.get_external_reference_id(doc) or "").lower(), - (doc.Description or doc.Name or "").lower() + (doc.Description or doc.Name or "").lower(), ) ) - + for child in info_children + ref_children: cls._process_document(child, props, document_children, expanded_documents, depth + 1) @@ -253,3 +251,48 @@ class Document(bonsai.core.tool.Document): if document.file.schema == "IFC2X3": return document.DocumentReferences or () return document.HasDocumentReferences + + @classmethod + def enable_document_editing(cls) -> None: + props = cls.get_document_props() + props.is_editing = True + + @classmethod + def disable_document_editing(cls) -> None: + props = cls.get_document_props() + props.is_editing = False + + @classmethod + def clear_active_document(cls) -> None: + props = cls.get_document_props() + props.active_document_id = 0 + + @classmethod + def clear_document_attributes(cls) -> None: + props = cls.get_document_props() + props.document_attributes.clear() + + @classmethod + def expand_document(cls, document: ifcopenshell.entity_instance) -> None: + props = cls.get_document_props() + try: + expanded_docs = json.loads(props.json_string) + except (AttributeError, json.JSONDecodeError): + expanded_docs = [] + + if document.id() not in expanded_docs: + expanded_docs.append(document.id()) + props.json_string = json.dumps(expanded_docs) + + @classmethod + def get_default_parent_for_information(cls, ifc) -> Union[ifcopenshell.entity_instance, None]: + projects = ifc.get().by_type("IfcProject") + return projects[0] if projects else None + + @classmethod + def get_selected_document_information(cls, ifc) -> Union[ifcopenshell.entity_instance, None]: + props = cls.get_document_props() + + if props.active_document and props.active_document.is_information: + return ifc.get().by_id(props.active_document.ifc_definition_id) + return None From b6cfd165c268eb67f1cf22691d6f421c1b2cde7e Mon Sep 17 00:00:00 2001 From: falken10 Date: Mon, 7 Jul 2025 23:58:02 +0200 Subject: [PATCH 04/12] updates based on developers feedback --- .../bonsai/bim/module/document/__init__.py | 2 - src/bonsai/bonsai/bim/module/document/data.py | 29 ++- .../bonsai/bim/module/document/operator.py | 209 ++++-------------- src/bonsai/bonsai/bim/module/document/prop.py | 28 ++- src/bonsai/bonsai/bim/module/document/ui.py | 84 ++++--- src/bonsai/bonsai/core/document.py | 18 +- src/bonsai/bonsai/tool/document.py | 103 ++++++--- 7 files changed, 218 insertions(+), 255 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/document/__init__.py b/src/bonsai/bonsai/bim/module/document/__init__.py index 323520420e..9730a50a11 100644 --- a/src/bonsai/bonsai/bim/module/document/__init__.py +++ b/src/bonsai/bonsai/bim/module/document/__init__.py @@ -28,14 +28,12 @@ classes = ( operator.DisableEditingDocument, operator.EditDocument, operator.EnableEditingDocument, - operator.LoadDocument, operator.LoadObjectDocuments, operator.LoadProjectDocuments, operator.RemoveDocument, operator.SelectDocumentObjects, operator.ToggleDocument, operator.UnassignDocument, - operator.UpdateAssignedDocuments, operator.OpenIFCDocument, prop.Document, prop.DocumentObject, diff --git a/src/bonsai/bonsai/bim/module/document/data.py b/src/bonsai/bonsai/bim/module/document/data.py index 15a89f8870..7f788b1388 100644 --- a/src/bonsai/bonsai/bim/module/document/data.py +++ b/src/bonsai/bonsai/bim/module/document/data.py @@ -21,7 +21,7 @@ import bpy import ifcopenshell import ifcopenshell.util.schema import bonsai.tool as tool - +from natsort import natsorted def refresh(): DocumentData.is_loaded = False @@ -87,19 +87,21 @@ class DocumentData: @classmethod def load_document_objects_into_props(cls, document_id): + if not cls.is_loaded: + cls.load() + props = tool.Document.get_document_props() props.document_objects.clear() - if "document_objects" not in cls.data or document_id not in cls.data["document_objects"]: + if document_id not in cls.data["document_objects"]: return - sorted_objects = sorted(cls.data["document_objects"][document_id], key=lambda x: x["name"].lower()) + sorted_objects = natsorted(cls.data["document_objects"][document_id], key=lambda x: x["name"].lower()) for obj_data in sorted_objects: item = props.document_objects.add() item.name = obj_data["name"] - class ObjectDocumentData: data = {} is_loaded = False @@ -111,6 +113,19 @@ class ObjectDocumentData: } cls.is_loaded = True + @staticmethod + def convert_to_file_uri(location: str) -> str: + if not location: + return "" + + uri = location + if not uri.startswith("file://"): + if not os.path.isabs(uri): + uri = os.path.abspath(os.path.join(os.path.dirname(tool.Ifc.get_path()), uri)) + uri = "file://" + uri + return uri + + @classmethod def documents(cls): results = [] @@ -165,11 +180,7 @@ class ObjectDocumentData: if location is None and referenced_document: location = referenced_document.Location - if location: - if not "://" in location: - if not os.path.isabs(location): - location = os.path.abspath(os.path.join(os.path.dirname(tool.Ifc.get_path()), location)) - location = "file://" + location + location = cls.convert_to_file_uri(location) if location else None results.append( { diff --git a/src/bonsai/bonsai/bim/module/document/operator.py b/src/bonsai/bonsai/bim/module/document/operator.py index ecc95f6ac8..557e2c72af 100644 --- a/src/bonsai/bonsai/bim/module/document/operator.py +++ b/src/bonsai/bonsai/bim/module/document/operator.py @@ -18,29 +18,10 @@ import bpy import json -import ifcopenshell.api -import ifcopenshell.util.attribute -import ifcopenshell.util.element import bonsai.bim.handler import bonsai.tool as tool import bonsai.core.document as core -import subprocess -import os -from bonsai.bim.module.document.data import DocumentData, ObjectDocumentData - - -def update_document_objects(document_id=None): - DocumentData.is_loaded = False - DocumentData.load() - - if document_id is None: - props = tool.Document.get_document_props() - if props.active_document and props.active_document.ifc_definition_id: - document_id = props.active_document.ifc_definition_id - - if document_id: - DocumentData.load_document_objects_into_props(document_id) - +from .data import DocumentData, ObjectDocumentData class LoadProjectDocuments(bpy.types.Operator): bl_idname = "bim.load_project_documents" @@ -49,23 +30,8 @@ class LoadProjectDocuments(bpy.types.Operator): def execute(self, context): core.load_project_documents(tool.Document) - update_document_objects() return {"FINISHED"} - -class LoadDocument(bpy.types.Operator): - bl_idname = "bim.load_document" - bl_label = "Load Document" - bl_options = {"REGISTER", "UNDO"} - document: bpy.props.IntProperty() - - def execute(self, context): - core.load_document(tool.Document, document=tool.Ifc.get().by_id(self.document)) - bonsai.bim.handler.refresh_ui_data() # Is this needed? - update_document_objects() - return {"FINISHED"} - - class DisableDocumentEditingUI(bpy.types.Operator): bl_idname = "bim.disable_document_editing_ui" bl_label = "Disable Document Editing UI" @@ -82,8 +48,7 @@ class DisableObjectDocumentEditingUI(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} def execute(self, context): - props = tool.Document.get_document_props() - props.is_object_editing = False + core.disable_object_document_editing_ui(tool.Document) return {"FINISHED"} @@ -94,8 +59,6 @@ class EnableEditingDocument(bpy.types.Operator): document: bpy.props.IntProperty() def execute(self, context): - props = tool.Document.get_document_props() - props.is_document_editing = True core.enable_editing_document(tool.Document, document=tool.Ifc.get().by_id(self.document)) return {"FINISHED"} @@ -106,8 +69,6 @@ class DisableEditingDocument(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} def execute(self, context): - props = tool.Document.get_document_props() - props.is_document_editing = False core.disable_editing_document(tool.Document) return {"FINISHED"} @@ -123,15 +84,15 @@ class AddInformation(bpy.types.Operator, tool.Ifc.Operator): if props.active_document: selected_document = props.active_document - if selected_document.ifc_definition_id == -1: - parent = tool.Ifc.get().by_type("IfcProject")[0] if tool.Ifc.get().by_type("IfcProject") else None - elif selected_document.is_information: + if selected_document.document_type == "PROJECT": + parent = tool.Ifc.get().by_type("IfcProject")[0] + elif selected_document.document_type == "INFORMATION": parent = tool.Ifc.get().by_id(selected_document.ifc_definition_id) - else: + elif selected_document.document_type == "REFERENCE": self.report({"ERROR"}, "Cannot add an information element as a child of a reference element") return {"CANCELLED"} else: - parent = tool.Ifc.get().by_type("IfcProject")[0] if tool.Ifc.get().by_type("IfcProject") else None + parent = tool.Ifc.get().by_type("IfcProject")[0] core.add_information(tool.Ifc, tool.Document, parent) @@ -141,20 +102,13 @@ class AddInformation(bpy.types.Operator, tool.Ifc.Operator): except (AttributeError, json.JSONDecodeError): pass - project = tool.Ifc.get().by_type("IfcProject")[0] - virtual_root_id = -project.id() - if virtual_root_id in expanded_docs: - expanded_docs.remove(virtual_root_id) - if parent and parent.is_a("IfcDocumentInformation"): if parent.id() not in expanded_docs: expanded_docs.append(parent.id()) props.json_string = json.dumps(expanded_docs) - bpy.ops.bim.load_project_documents() - class AddDocumentReference(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.add_document_reference" bl_label = "Add Document Reference" @@ -169,8 +123,8 @@ class AddDocumentReference(bpy.types.Operator, tool.Ifc.Operator): selected_document = props.active_document - if not selected_document.is_information: - self.report({"ERROR"}, "Cannot add a reference to a reference element") + if selected_document.document_type != "INFORMATION": + self.report({"ERROR"}, "Cannot add a reference to a document that is not an information element") return {"CANCELLED"} parent = tool.Ifc.get().by_id(selected_document.ifc_definition_id) @@ -201,12 +155,7 @@ class EditDocument(bpy.types.Operator, tool.Ifc.Operator): core.edit_document(tool.Ifc, tool.Document, document=tool.Ifc.get().by_id(props.active_document_id)) props.active_document_id = 0 props.is_document_editing = False - DocumentData.is_loaded = False - DocumentData.load() - ObjectDocumentData.is_loaded = False - ObjectDocumentData.load() - bpy.ops.bim.update_assigned_documents() - bonsai.bim.handler.refresh_ui_data() + tool.Document.update_assigned_documents() class RemoveDocument(bpy.types.Operator, tool.Ifc.Operator): @@ -218,40 +167,6 @@ class RemoveDocument(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): core.remove_document(tool.Ifc, tool.Document, document=tool.Ifc.get().by_id(self.document)) - -class UpdateAssignedDocuments(bpy.types.Operator): - bl_idname = "bim.update_assigned_documents" - bl_label = "Update Assigned Documents" - bl_description = "Update the list of documents assigned to the active object" - bl_options = {"REGISTER"} - - def execute(self, context): - ObjectDocumentData.is_loaded = False - ObjectDocumentData.load() - - props = tool.Document.get_document_props() - props.assigned_documents.clear() - - if not ObjectDocumentData.data.get("documents"): - return {"FINISHED"} - - sorted_docs = sorted( - ObjectDocumentData.data["documents"], - key=lambda doc: ((doc.get("identification") or "").lower(), (doc.get("name") or "").lower()), - ) - - for document in sorted_docs: - new = props.assigned_documents.add() - new.name = document["name"] or "Unnamed" - new.identification = document["identification"] or "*" - new.is_information = document.get("is_information", False) - new.ifc_definition_id = document["id"] - new.location = document.get("location") or "" - new.description = document.get("description") or "" - - return {"FINISHED"} - - class AssignDocument(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.assign_document" bl_label = "Assign Document" @@ -268,10 +183,12 @@ class AssignDocument(bpy.types.Operator, tool.Ifc.Operator): if element: core.assign_document(tool.Ifc, product=element, document=document) - update_document_objects(self.document) + + tool.Document.update_document_objects(self.document) ObjectDocumentData.is_loaded = False ObjectDocumentData.load() - bpy.ops.bim.update_assigned_documents() + tool.Document.update_assigned_documents() + return {"FINISHED"} class UnassignDocument(bpy.types.Operator, tool.Ifc.Operator): @@ -285,21 +202,26 @@ class UnassignDocument(bpy.types.Operator, tool.Ifc.Operator): document = tool.Ifc.get().by_id(self.document) objs = [bpy.data.objects.get(self.obj)] if self.obj else tool.Blender.get_selected_objects() for obj in objs: - element = tool.Ifc.get_entity(obj) - if element: - core.unassign_document(tool.Ifc, product=element, document=document) + if obj: + element = tool.Ifc.get_entity(obj) + if element: + core.unassign_document(tool.Ifc, product=element, document=document) + props = tool.Document.get_document_props() active_document_id = None if props.active_document: active_document_id = props.active_document.ifc_definition_id if active_document_id and active_document_id != self.document: - update_document_objects(active_document_id) + tool.Document.update_document_objects(active_document_id) else: - update_document_objects(self.document) + tool.Document.update_document_objects() + ObjectDocumentData.is_loaded = False ObjectDocumentData.load() - bpy.ops.bim.update_assigned_documents() + + tool.Document.update_assigned_documents() + return {"FINISHED"} class SelectDocumentObjects(bpy.types.Operator): @@ -339,32 +261,10 @@ class LoadObjectDocuments(bpy.types.Operator): props = tool.Document.get_document_props() props.is_object_editing = True - bonsai.bim.handler.refresh_ui_data() - - self.update_assigned_documents(props) + tool.Document.update_assigned_documents() return {"FINISHED"} - def update_assigned_documents(self, props): - props.assigned_documents.clear() - - if not ObjectDocumentData.data.get("documents"): - return - - sorted_docs = sorted( - ObjectDocumentData.data["documents"], - key=lambda doc: ((doc.get("identification") or "").lower(), (doc.get("name") or "").lower()), - ) - - for document in sorted_docs: - new = props.assigned_documents.add() - new.name = document["name"] or "Unnamed" - new.identification = document["identification"] or "*" - new.is_information = document.get("is_information", False) - new.ifc_definition_id = document["id"] - new.location = document["location"] or "" - new.description = document["description"] or "" - class OpenIFCDocument(bpy.types.Operator): bl_idname = "bim.open_ifc_document" @@ -375,47 +275,37 @@ class OpenIFCDocument(bpy.types.Operator): uri: bpy.props.StringProperty(name="URI") def execute(self, context): - - if not self.uri: - self.report({"ERROR"}, "No URI provided") + import subprocess + import os + if not self.uri or not self.uri.lower().startswith("file://"): + self.report({"ERROR"}, "Only local file:// URIs are supported") return {"CANCELLED"} - file_path = self.uri - if file_path.startswith("file://"): - file_path = file_path[7:] - elif file_path.startswith("file:"): - file_path = file_path[5:] - - if not os.path.isabs(file_path): - file_path = os.path.abspath(file_path) - - if not os.path.exists(file_path): - self.report({"ERROR"}, f"IFC file not found: {file_path}") + filepath = self.uri[7:] # Remove file:// prefix + + if not os.path.exists(filepath): + self.report({"ERROR"}, f"File not found: {filepath}") return {"CANCELLED"} try: - subprocess.Popen( - [ - "blender", - "--python-expr", - f"import bpy; bpy.ops.bim.load_project(filepath='{file_path}', should_start_fresh_session=True)", - ] - ) - self.report({"INFO"}, f"Opening IFC file: {file_path} in a new Blender instance.") + blender_path = bpy.app.binary_path + args = [blender_path, "--python-expr", "import bpy; bpy.ops.bim.load_project(filepath='{}')".format(filepath)] + subprocess.Popen(args) + self.report({"INFO"}, f"Opening {filepath} in a new Blender instance") except Exception as e: self.report({"ERROR"}, f"Failed to open IFC file: {str(e)}") return {"FINISHED"} -class ToggleDocument(bpy.types.Operator, tool.Ifc.Operator): +class ToggleDocument(bpy.types.Operator): bl_idname = "bim.toggle_document" bl_label = "Toggle Document" bl_options = {"REGISTER", "UNDO"} document: bpy.props.IntProperty() option: bpy.props.StringProperty() - def _execute(self, context): + def execute(self, context): expanded_documents = [] props = tool.Document.get_document_props() try: @@ -425,19 +315,14 @@ class ToggleDocument(bpy.types.Operator, tool.Ifc.Operator): document_id = self.document - if self.option == "Expand" and document_id not in expanded_documents: - expanded_documents.append(document_id) - elif self.option == "Collapse" and document_id in expanded_documents: - expanded_documents.remove(document_id) - elif document_id == -1: - project = tool.Ifc.get().by_type("IfcProject")[0] - virtual_root_id = -project.id() + document = tool.Ifc.get().by_id(document_id) + if document: + if self.option == "Expand" and document_id not in expanded_documents: + expanded_documents.append(document_id) + elif self.option == "Collapse" and document_id in expanded_documents: + expanded_documents.remove(document_id) - if self.option == "Expand" and virtual_root_id not in expanded_documents: - expanded_documents.append(virtual_root_id) - elif self.option == "Collapse" and virtual_root_id in expanded_documents: - expanded_documents.remove(virtual_root_id) props.json_string = json.dumps(expanded_documents) - bpy.ops.bim.load_project_documents() return {"FINISHED"} + diff --git a/src/bonsai/bonsai/bim/module/document/prop.py b/src/bonsai/bonsai/bim/module/document/prop.py index ce4a6a83cc..92d60771f7 100644 --- a/src/bonsai/bonsai/bim/module/document/prop.py +++ b/src/bonsai/bonsai/bim/module/document/prop.py @@ -51,8 +51,7 @@ def update_document_identification(self: "Document", context: bpy.types.Context) def update_active_document(self, context): - if self.documents and self.active_document_index < len(self.documents): - document = self.documents[self.active_document_index] + if (document := self.active_document): if document.ifc_definition_id: DocumentData.load_document_objects_into_props(document.ifc_definition_id) @@ -61,23 +60,31 @@ class Document(PropertyGroup): name: StringProperty(name="Name") identification: StringProperty(name="Identification") description: StringProperty(name="Description") - is_information: BoolProperty(name="Is Information") ifc_definition_id: IntProperty(name="IFC Definition ID") location: StringProperty(name="Location", default="") tree_depth: IntProperty(name="Tree Depth", default=0) has_children: BoolProperty(name="Has Children", default=False) is_expanded: BoolProperty(name="Is Expanded", default=False) + document_type: EnumProperty( + name="Document Type", + items=[ + ("PROJECT", "Project", "Virtual project root node"), + ("INFORMATION", "Information", "IfcDocumentInformation"), + ("REFERENCE", "Reference", "IfcDocumentReference"), + ], + default="INFORMATION" + ) if TYPE_CHECKING: name: str identification: str description: str - is_information: bool ifc_definition_id: int location: str tree_depth: int has_children: bool is_expanded: bool + document_type: str class DocumentObject(PropertyGroup): @@ -93,17 +100,24 @@ class AssignedDocument(PropertyGroup): name: StringProperty(name="Name") identification: StringProperty(name="Identification") description: StringProperty(name="Description", default="") - is_information: BoolProperty(name="Is Information") ifc_definition_id: IntProperty(name="IFC Definition ID") location: StringProperty(name="Location", default="") + document_type: EnumProperty( + name="Document Type", + items=[ + ("PROJECT", "Project", "Virtual project root node"), + ("INFORMATION", "Information", "IfcDocumentInformation"), + ("REFERENCE", "Reference", "IfcDocumentReference"), + ], + default="INFORMATION" + ) if TYPE_CHECKING: name: str identification: str - is_information: bool ifc_definition_id: int location: str - + document_type: str class BIMDocumentProperties(PropertyGroup): document_attributes: CollectionProperty(name="Document Attributes", type=Attribute) diff --git a/src/bonsai/bonsai/bim/module/document/ui.py b/src/bonsai/bonsai/bim/module/document/ui.py index bad9819205..1563555b9a 100644 --- a/src/bonsai/bonsai/bim/module/document/ui.py +++ b/src/bonsai/bonsai/bim/module/document/ui.py @@ -20,8 +20,7 @@ import bpy import bonsai.tool as tool from bpy.types import Panel, UIList from bonsai.bim.helper import draw_attributes -from bonsai.bim.module.document.data import DocumentData, ObjectDocumentData - +from .data import DocumentData, ObjectDocumentData class BIM_PT_documents(Panel): bl_label = "Documents" @@ -46,8 +45,8 @@ class BIM_PT_documents(Panel): split = row.split(factor=0.55) left_row = split.row(align=True) - left_row.label(text="{} Informations".format(DocumentData.data["total_document_informations"]), icon="FILE") - left_row.label(text="{} References".format(DocumentData.data["total_document_references"]), icon="FILE_HIDDEN") + total_documents = DocumentData.data["total_document_informations"] + DocumentData.data["total_document_references"] + left_row.label(text="{} Documents".format(total_documents), icon="FILE") right_row = split.row(align=True) right_row.label( @@ -68,31 +67,31 @@ class BIM_PT_documents(Panel): row.operator("bim.edit_document", text="", icon="CHECKMARK") row.operator("bim.disable_editing_document", text="", icon="CANCEL") else: - if not self.props.active_document or self.props.active_document.is_information: + if not self.props.active_document or self.props.active_document.document_type in ["INFORMATION", "PROJECT"]: row.operator("bim.add_information", text="", icon="ADD") - if self.props.active_document: - if self.props.active_document.is_information and self.props.active_document.ifc_definition_id != -1: - row.operator("bim.add_document_reference", text="", icon="FILE_HIDDEN") + if self.props.active_document and ( + self.props.active_document.document_type == "INFORMATION" and + self.props.active_document.document_type != "PROJECT" + ): + row.operator("bim.add_document_reference", text="", icon="FILE_HIDDEN") active_document = self.props.active_document if active_document: ifc_definition_id = active_document.ifc_definition_id - row.operator("bim.select_document_objects", text="", icon="RESTRICT_SELECT_OFF").document = ( - ifc_definition_id - ) - - row.operator("bim.assign_document", text="", icon="BRUSH_DATA").document = ifc_definition_id - row.operator("bim.enable_editing_document", text="", icon="GREASEPENCIL").document = ifc_definition_id - row.operator("bim.remove_document", text="", icon="X").document = ifc_definition_id + + if active_document.document_type != "PROJECT": + row.operator("bim.select_document_objects", text="", icon="RESTRICT_SELECT_OFF").document = ( + ifc_definition_id + ) + row.operator("bim.assign_document", text="", icon="BRUSH_DATA").document = ifc_definition_id + row.operator("bim.enable_editing_document", text="", icon="GREASEPENCIL").document = ifc_definition_id + row.operator("bim.remove_document", text="", icon="X").document = ifc_definition_id self.layout.template_list("BIM_UL_documents", "", self.props, "documents", self.props, "active_document_index") if self.props.is_document_editing: active_document = self.props.active_document - if active_document.is_information: - draw_attributes(self.props.document_attributes, self.layout) - else: - draw_attributes(self.props.document_attributes, self.layout, filter_attributes=["Name"]) + draw_attributes(self.props.document_attributes, self.layout) if self.props.is_editing and self.props.active_document: document = self.props.active_document @@ -108,7 +107,6 @@ class BIM_PT_documents(Panel): "active_document_object_index", ) - class BIM_PT_object_documents(Panel): bl_label = "Documents" bl_idname = "BIM_PT_object_documents" @@ -119,6 +117,9 @@ class BIM_PT_object_documents(Panel): bl_order = 1 bl_parent_id = "BIM_PT_tab_misc" + # Class variable to track the last selected object + _last_object_id = None + @classmethod def poll(cls, context): if not (obj := context.active_object): @@ -130,10 +131,14 @@ class BIM_PT_object_documents(Panel): return True def draw(self, context): - if not ObjectDocumentData.is_loaded: + obj = context.active_object + current_ifc_id = tool.Blender.get_ifc_definition_id(obj) + + if BIM_PT_object_documents._last_object_id != current_ifc_id: + BIM_PT_object_documents._last_object_id = current_ifc_id + ObjectDocumentData.is_loaded = False ObjectDocumentData.load() - obj = context.active_object self.oprops = tool.Blender.get_object_bim_props(obj) self.props = tool.Document.get_document_props() self.file = tool.Ifc.get() @@ -181,10 +186,11 @@ class BIM_PT_object_documents(Panel): for doc in ObjectDocumentData.data["documents"]: assigned_doc_ids.append(doc["id"]) - # Only show assign button if the document is information (not reference) and not already assigned - if document.is_information and document.ifc_definition_id not in assigned_doc_ids: + if (document.document_type == "INFORMATION" and + document.document_type != "PROJECT" and + document.ifc_definition_id not in assigned_doc_ids): doc_op = row.operator("bim.assign_document", text="", icon="BRUSH_DATA") - doc_op.document = document.ifc_definition_id # Pass the current document's ID + doc_op.document = document.ifc_definition_id elif document.ifc_definition_id in assigned_doc_ids: row.label(text="", icon="CHECKMARK") self.layout.template_list( @@ -198,24 +204,28 @@ class BIM_UL_documents(UIList): row = layout.row(align=True) indent_depth = 0 - if item.ifc_definition_id != -1: + if item.document_type != "PROJECT": if item.tree_depth > 1: indent_depth = item.tree_depth - 1 + for i in range(indent_depth): row.label(text="", icon="BLANK1") - if item.ifc_definition_id == -1: + + if item.document_type == "PROJECT": row.label(text="", icon="OUTLINER_COLLECTION") row.label(text=item.name) return - if item.is_information and item.has_children: + + if item.document_type == "INFORMATION" and item.has_children: op = row.operator( "bim.toggle_document", icon="TRIA_DOWN" if item.is_expanded else "TRIA_RIGHT", text="", emboss=False ) op.document = item.ifc_definition_id op.option = "Collapse" if item.is_expanded else "Expand" - elif item.is_information: + elif item.document_type == "INFORMATION": row.label(text="", icon="BLANK1") - if item.is_information: + + if item.document_type == "INFORMATION": row.label(text="", icon="FILE") text = " - ".join([x for x in [item.name, item.location] if x]) else: @@ -227,9 +237,10 @@ class BIM_UL_documents(UIList): split2.label(text=text) if item.location: + uri = ObjectDocumentData.convert_to_file_uri(item.location) if item.location.lower().endswith(".ifc"): - row.operator("bim.open_ifc_document", icon="HIDE_OFF", text="").uri = item.location - row.operator("bim.open_uri", icon="URL", text="").uri = item.location + row.operator("bim.open_ifc_document", icon="HIDE_OFF", text="").uri = uri + row.operator("bim.open_uri", icon="URL", text="").uri = uri class BIM_UL_document_objects(UIList): @@ -253,7 +264,7 @@ class BIM_UL_assigned_documents(UIList): if item: row = layout.row(align=True) - if item.is_information: + if item.document_type == "INFORMATION": row.label(text="", icon="FILE") else: row.label(text="", icon="FILE_HIDDEN") @@ -262,15 +273,16 @@ class BIM_UL_assigned_documents(UIList): split1.label(text=item.identification or "") split2 = split1.split(factor=1.0) - if item.is_information: + if item.document_type == "INFORMATION": split2.label(text=item.name or "Unnamed") else: split2.label(text=item.description or "No Description") if item.location: + uri = ObjectDocumentData.convert_to_file_uri(item.location) if item.location.lower().endswith(".ifc"): - row.operator("bim.open_ifc_document", icon="HIDE_OFF", text="").uri = item.location - row.operator("bim.open_uri", icon="URL", text="").uri = item.location + row.operator("bim.open_ifc_document", icon="HIDE_OFF", text="").uri = uri + row.operator("bim.open_uri", icon="URL", text="").uri = uri op = row.operator("bim.unassign_document", text="", icon="X") op.document = item.ifc_definition_id diff --git a/src/bonsai/bonsai/core/document.py b/src/bonsai/bonsai/core/document.py index 56d92ea613..e55c077022 100644 --- a/src/bonsai/bonsai/core/document.py +++ b/src/bonsai/bonsai/core/document.py @@ -29,31 +29,27 @@ def load_project_documents(document: tool.Document) -> None: document.import_project_documents() document.enable_editing_ui() - -def load_document(document_tool: tool.Document, document: ifcopenshell.entity_instance) -> None: - document_tool.clear_document_tree() - document_tool.expand_document(document) - document_tool.import_project_documents() - document_tool.disable_editing_document() - - def disable_document_editing_ui(document: tool.Document) -> None: document.disable_editing_ui() document.disable_editing_document() +def disable_object_document_editing_ui(document: tool.Document) -> None: + props = document.get_document_props() + props.is_object_editing = False def enable_editing_document(document_tool: tool.Document, document: ifcopenshell.entity_instance) -> None: + props = document_tool.get_document_props() + props.is_document_editing = True document_tool.set_active_document(document) - document_tool.enable_document_editing() document_tool.import_document_attributes(document) def disable_editing_document(document: tool.Document) -> None: + props = document.get_document_props() + props.is_document_editing = False document.clear_active_document() - document.disable_document_editing() document.clear_document_attributes() - def add_information(ifc: tool.Ifc, document_tool: tool.Document, parent=None) -> ifcopenshell.entity_instance: document_tool.clear_document_tree() diff --git a/src/bonsai/bonsai/tool/document.py b/src/bonsai/bonsai/tool/document.py index 76ceaf52c3..cd51f35e8d 100644 --- a/src/bonsai/bonsai/tool/document.py +++ b/src/bonsai/bonsai/tool/document.py @@ -18,12 +18,11 @@ from __future__ import annotations import bpy -import json import ifcopenshell.util.system -import bonsai.bim.helper import bonsai.core.tool import bonsai.tool as tool import json +from natsort import natsorted from typing import Any, Union, TYPE_CHECKING if TYPE_CHECKING: @@ -70,12 +69,15 @@ class Document(bonsai.core.tool.Document): data[attr_name] = "" return True if attr_name != "Name": - return None + return None # Proceed normally current_value = data[attr_name] + # If Name is already filled, display it so user would be able to correct invalid IFC. if current_value is not None: return None + # Skip import since IFC restricts Name to be filled + # for IfcDocumentReference with ReferencedDocument. return False import_callback = callback if document.is_a("IfcDocumentReference") else None @@ -137,8 +139,8 @@ class Document(bonsai.core.tool.Document): root_documents.append(rel.RelatingDocument) root = props.documents.add() - root.ifc_definition_id = -1 - root.is_information = True + root.ifc_definition_id = -project.id() + root.document_type = "PROJECT" root.name = f"Project Documents ({project.Name or 'Unnamed Project'})" root.identification = "" root.location = "" @@ -150,8 +152,9 @@ class Document(bonsai.core.tool.Document): root.is_expanded = root_id not in expanded_documents if root.is_expanded: - root_documents.sort( - key=lambda doc: ((cls.get_document_information_id(doc) or "").lower(), (doc.Name or "").lower()) + root_documents = natsorted( + root_documents, + key=lambda doc: (cls.get_document_information_id(doc) or "", doc.Name or "") ) for doc in root_documents: @@ -161,11 +164,11 @@ class Document(bonsai.core.tool.Document): def _process_document(cls, document, props, document_children, expanded_documents, depth): new = props.documents.add() new.ifc_definition_id = document.id() - new.is_information = document.is_a("IfcDocumentInformation") + new.document_type = "INFORMATION" if document.is_a("IfcDocumentInformation") else "REFERENCE" new.tree_depth = depth file = document.file - if new.is_information: + if new.document_type == "INFORMATION": new.name = document.Name or "Unnamed" new.identification = cls.get_document_information_id(document) or "" new.location = document.Location or "" @@ -175,7 +178,7 @@ class Document(bonsai.core.tool.Document): new.description = document.Description or "" new.location = document.Location or "" - if not new.is_information: + if new.document_type == "REFERENCE": if file.schema == "IFC2X3": if document.ReferenceToDocument: doc_info = document.ReferenceToDocument[0] @@ -183,7 +186,7 @@ class Document(bonsai.core.tool.Document): new.name = doc_info.Name or "" new.location = new.location or "" else: - if hasattr(document, "ReferencedDocument") and document.ReferencedDocument: + if document.ReferencedDocument: doc_info = document.ReferencedDocument if not new.name: new.name = doc_info.Name or "" @@ -200,20 +203,22 @@ class Document(bonsai.core.tool.Document): info_children = [d for d in children if d.is_a("IfcDocumentInformation")] ref_children = [d for d in children if not d.is_a("IfcDocumentInformation")] - info_children.sort( - key=lambda doc: ((cls.get_document_information_id(doc) or "").lower(), (doc.Name or "").lower()) + info_children = natsorted( + info_children, + key=lambda doc: (cls.get_document_information_id(doc) or "", doc.Name or "") ) - ref_children.sort( + ref_children = natsorted( + ref_children, key=lambda doc: ( - (cls.get_external_reference_id(doc) or "").lower(), - (doc.Description or doc.Name or "").lower(), + cls.get_external_reference_id(doc) or "", + doc.Description or doc.Name or "" ) ) for child in info_children + ref_children: cls._process_document(child, props, document_children, expanded_documents, depth + 1) - + @classmethod def is_document_information(cls, document: ifcopenshell.entity_instance) -> bool: return document.is_a("IfcDocumentInformation") @@ -252,16 +257,6 @@ class Document(bonsai.core.tool.Document): return document.DocumentReferences or () return document.HasDocumentReferences - @classmethod - def enable_document_editing(cls) -> None: - props = cls.get_document_props() - props.is_editing = True - - @classmethod - def disable_document_editing(cls) -> None: - props = cls.get_document_props() - props.is_editing = False - @classmethod def clear_active_document(cls) -> None: props = cls.get_document_props() @@ -293,6 +288,58 @@ class Document(bonsai.core.tool.Document): def get_selected_document_information(cls, ifc) -> Union[ifcopenshell.entity_instance, None]: props = cls.get_document_props() - if props.active_document and props.active_document.is_information: + if props.active_document and props.active_document.document_type == "INFORMATION": return ifc.get().by_id(props.active_document.ifc_definition_id) return None + + @classmethod + def refresh_document_data(cls) -> None: + import bonsai.bim.module.document.data as document_data + document_data.DocumentData.is_loaded = False + document_data.DocumentData.load() + + @classmethod + def load_document_objects_into_props(cls, document_id: int) -> None: + import bonsai.bim.module.document.data as document_data + document_data.DocumentData.load_document_objects_into_props(document_id) + + @classmethod + def update_document_objects(cls, document_id: Union[int, None] = None) -> None: + cls.refresh_document_data() + + if document_id is None: + props = cls.get_document_props() + if props.active_document and props.active_document.ifc_definition_id > 0: + document_id = props.active_document.ifc_definition_id + + if document_id: + cls.load_document_objects_into_props(document_id) + + @classmethod + def update_assigned_documents(cls) -> None: + from bonsai.bim.module.document.data import ObjectDocumentData + + ObjectDocumentData.is_loaded = False + + props = cls.get_document_props() + props.assigned_documents.clear() + + if not ObjectDocumentData.is_loaded: + ObjectDocumentData.load() + + if not ObjectDocumentData.data.get("documents"): + return + + sorted_docs = sorted( + ObjectDocumentData.data["documents"], + key=lambda doc: ((doc.get("identification") or "").lower(), (doc.get("name") or "").lower()), + ) + + for document in sorted_docs: + new = props.assigned_documents.add() + new.name = document["name"] or "Unnamed" + new.identification = document["identification"] or "*" + new.document_type = "INFORMATION" if document.get("is_information", False) else "REFERENCE" + new.ifc_definition_id = document["id"] + new.location = document.get("location") or "" + new.description = document.get("description") or "" \ No newline at end of file From ca8eb9b3580e3f3c69f1a690abefcbf8ca12d96e Mon Sep 17 00:00:00 2001 From: falken10 Date: Tue, 8 Jul 2025 08:39:21 +0200 Subject: [PATCH 05/12] misc panel working --- .../bonsai/bim/module/document/__init__.py | 1 - src/bonsai/bonsai/bim/module/document/ui.py | 44 ++++--------------- 2 files changed, 8 insertions(+), 37 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/document/__init__.py b/src/bonsai/bonsai/bim/module/document/__init__.py index 9730a50a11..d2da8285b9 100644 --- a/src/bonsai/bonsai/bim/module/document/__init__.py +++ b/src/bonsai/bonsai/bim/module/document/__init__.py @@ -43,7 +43,6 @@ classes = ( ui.BIM_PT_object_documents, ui.BIM_UL_documents, ui.BIM_UL_document_objects, - ui.BIM_UL_assigned_documents, ui.BIM_MT_object_documents_context_menu, ) diff --git a/src/bonsai/bonsai/bim/module/document/ui.py b/src/bonsai/bonsai/bim/module/document/ui.py index 1563555b9a..d891836517 100644 --- a/src/bonsai/bonsai/bim/module/document/ui.py +++ b/src/bonsai/bonsai/bim/module/document/ui.py @@ -165,14 +165,14 @@ class BIM_PT_object_documents(Panel): row = box.row(align=True) row.label(text="Assigned Documents", icon="OUTLINER_OB_EMPTY") - box.template_list( - "BIM_UL_assigned_documents", - "", - self.props, - "assigned_documents", - self.props, - "active_assigned_document_index", - ) + + for document in ObjectDocumentData.data["documents"]: + row = self.layout.row(align=True) + row.label(text=document["identification"] or "*", icon="FILE") + row.label(text=document["name"] or "Unnamed") + if document["location"]: + row.operator("bim.open_uri", icon="URL", text="").uri = document["location"] + row.operator("bim.unassign_document", text="", icon="X").document = document["id"] def draw_add_ui(self): if self.props.is_object_editing: @@ -259,34 +259,6 @@ class BIM_UL_document_objects(UIList): op.obj = item.name -class BIM_UL_assigned_documents(UIList): - def draw_item(self, context, layout, data, item, icon, active_data, active_propname): - if item: - row = layout.row(align=True) - - if item.document_type == "INFORMATION": - row.label(text="", icon="FILE") - else: - row.label(text="", icon="FILE_HIDDEN") - - split1 = row.split(factor=0.2) - split1.label(text=item.identification or "") - - split2 = split1.split(factor=1.0) - if item.document_type == "INFORMATION": - split2.label(text=item.name or "Unnamed") - else: - split2.label(text=item.description or "No Description") - - if item.location: - uri = ObjectDocumentData.convert_to_file_uri(item.location) - if item.location.lower().endswith(".ifc"): - row.operator("bim.open_ifc_document", icon="HIDE_OFF", text="").uri = uri - row.operator("bim.open_uri", icon="URL", text="").uri = uri - op = row.operator("bim.unassign_document", text="", icon="X") - op.document = item.ifc_definition_id - - def add_object_documents_context_menu(self, context): if not context.active_object: return From 9c8c40c0ff97378c6d79c37653169fa7967c52c9 Mon Sep 17 00:00:00 2001 From: falken10 Date: Tue, 8 Jul 2025 16:24:02 +0200 Subject: [PATCH 06/12] nicer ui --- src/bonsai/bonsai/bim/module/document/prop.py | 2 - src/bonsai/bonsai/bim/module/document/ui.py | 44 +++++++++++-------- 2 files changed, 26 insertions(+), 20 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/document/prop.py b/src/bonsai/bonsai/bim/module/document/prop.py index 92d60771f7..b0964dd5c9 100644 --- a/src/bonsai/bonsai/bim/module/document/prop.py +++ b/src/bonsai/bonsai/bim/module/document/prop.py @@ -143,8 +143,6 @@ class BIMDocumentProperties(PropertyGroup): is_object_editing: bool document_objects: bpy.types.bpy_prop_collection_idprop[DocumentObject] active_document_object_index: int - assigned_documents: bpy.types.bpy_prop_collection_idprop[AssignedDocument] - active_assigned_document_index: int json_string: str @property diff --git a/src/bonsai/bonsai/bim/module/document/ui.py b/src/bonsai/bonsai/bim/module/document/ui.py index d891836517..a742130f94 100644 --- a/src/bonsai/bonsai/bim/module/document/ui.py +++ b/src/bonsai/bonsai/bim/module/document/ui.py @@ -117,9 +117,6 @@ class BIM_PT_object_documents(Panel): bl_order = 1 bl_parent_id = "BIM_PT_tab_misc" - # Class variable to track the last selected object - _last_object_id = None - @classmethod def poll(cls, context): if not (obj := context.active_object): @@ -132,11 +129,7 @@ class BIM_PT_object_documents(Panel): def draw(self, context): obj = context.active_object - current_ifc_id = tool.Blender.get_ifc_definition_id(obj) - - if BIM_PT_object_documents._last_object_id != current_ifc_id: - BIM_PT_object_documents._last_object_id = current_ifc_id - ObjectDocumentData.is_loaded = False + if not ObjectDocumentData.is_loaded: ObjectDocumentData.load() self.oprops = tool.Blender.get_object_bim_props(obj) @@ -160,19 +153,34 @@ class BIM_PT_object_documents(Panel): if self.props.is_object_editing: self.draw_add_ui() + box = self.layout.box() + row = box.row(align=True) + row.label(text="Assigned Documents", icon="OUTLINER_OB_EMPTY") + if doc_count > 0: - box = self.layout.box() - row = box.row(align=True) - row.label(text="Assigned Documents", icon="OUTLINER_OB_EMPTY") - - + col = box.column(align=True) for document in ObjectDocumentData.data["documents"]: - row = self.layout.row(align=True) - row.label(text=document["identification"] or "*", icon="FILE") - row.label(text=document["name"] or "Unnamed") + row = col.row(align=True) + + # Create a split layout to separate left and right sides + split = row.split(factor=0.7) # Adjust factor as needed (0.7 = 70% left, 30% right) + + # Left side - Document identification and name + left_side = split.row(align=True) + left_side.alignment = 'LEFT' + left_side.label(text=document["identification"] or "*", icon="FILE") + left_side.label(text=document["name"] or "Unnamed") + + # Right side - Action buttons + right_side = split.row(align=True) + right_side.alignment = 'RIGHT' # Align buttons to the right + if document["location"]: - row.operator("bim.open_uri", icon="URL", text="").uri = document["location"] - row.operator("bim.unassign_document", text="", icon="X").document = document["id"] + if document["location"].lower().endswith(".ifc"): + right_side.operator("bim.open_ifc_document", icon="HIDE_OFF", text="").uri = document["location"] + right_side.operator("bim.open_uri", icon="URL", text="").uri = document["location"] + + right_side.operator("bim.unassign_document", text="", icon="X").document = document["id"] def draw_add_ui(self): if self.props.is_object_editing: From 128fe66837f662ea835c207008d3b778436553c9 Mon Sep 17 00:00:00 2001 From: falken10 Date: Tue, 8 Jul 2025 19:45:12 +0200 Subject: [PATCH 07/12] cleanup and formatting --- .../bonsai/bim/module/document/__init__.py | 1 - src/bonsai/bonsai/bim/module/document/data.py | 24 ++------ .../bonsai/bim/module/document/operator.py | 32 +++++------ src/bonsai/bonsai/bim/module/document/prop.py | 31 +--------- src/bonsai/bonsai/bim/module/document/ui.py | 56 ++++++++++--------- src/bonsai/bonsai/core/document.py | 6 +- src/bonsai/bonsai/tool/document.py | 44 ++------------- 7 files changed, 64 insertions(+), 130 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/document/__init__.py b/src/bonsai/bonsai/bim/module/document/__init__.py index d2da8285b9..cc71d16306 100644 --- a/src/bonsai/bonsai/bim/module/document/__init__.py +++ b/src/bonsai/bonsai/bim/module/document/__init__.py @@ -37,7 +37,6 @@ classes = ( operator.OpenIFCDocument, prop.Document, prop.DocumentObject, - prop.AssignedDocument, prop.BIMDocumentProperties, ui.BIM_PT_documents, ui.BIM_PT_object_documents, diff --git a/src/bonsai/bonsai/bim/module/document/data.py b/src/bonsai/bonsai/bim/module/document/data.py index 7f788b1388..0ba0106881 100644 --- a/src/bonsai/bonsai/bim/module/document/data.py +++ b/src/bonsai/bonsai/bim/module/document/data.py @@ -23,10 +23,6 @@ import ifcopenshell.util.schema import bonsai.tool as tool from natsort import natsorted -def refresh(): - DocumentData.is_loaded = False - ObjectDocumentData.is_loaded = False - class DocumentData: data = {} @@ -35,24 +31,16 @@ class DocumentData: @classmethod def load(cls): cls.data = { - "total_document_informations": cls.total_document_informations(), - "total_document_references": cls.total_document_references(), + "total_documents": cls.total_documents(), "total_referenced_objects": cls.total_referenced_objects(), "document_objects": cls.document_objects(), } cls.is_loaded = True @classmethod - def total_document_informations(cls): + def total_documents(cls): file = tool.Ifc.get() - info_count = len(file.by_type("IfcDocumentInformation")) - return info_count - - @classmethod - def total_document_references(cls): - file = tool.Ifc.get() - ref_count = len(file.by_type("IfcDocumentReference")) - return ref_count + return len(file.by_type("IfcDocumentInformation")) + len(file.by_type("IfcDocumentReference")) @classmethod def total_referenced_objects(cls): @@ -89,7 +77,7 @@ class DocumentData: def load_document_objects_into_props(cls, document_id): if not cls.is_loaded: cls.load() - + props = tool.Document.get_document_props() props.document_objects.clear() @@ -102,6 +90,7 @@ class DocumentData: item = props.document_objects.add() item.name = obj_data["name"] + class ObjectDocumentData: data = {} is_loaded = False @@ -117,7 +106,7 @@ class ObjectDocumentData: def convert_to_file_uri(location: str) -> str: if not location: return "" - + uri = location if not uri.startswith("file://"): if not os.path.isabs(uri): @@ -125,7 +114,6 @@ class ObjectDocumentData: uri = "file://" + uri return uri - @classmethod def documents(cls): results = [] diff --git a/src/bonsai/bonsai/bim/module/document/operator.py b/src/bonsai/bonsai/bim/module/document/operator.py index 557e2c72af..c2ccd5edc9 100644 --- a/src/bonsai/bonsai/bim/module/document/operator.py +++ b/src/bonsai/bonsai/bim/module/document/operator.py @@ -23,6 +23,7 @@ import bonsai.tool as tool import bonsai.core.document as core from .data import DocumentData, ObjectDocumentData + class LoadProjectDocuments(bpy.types.Operator): bl_idname = "bim.load_project_documents" bl_label = "Load Project Documents" @@ -32,6 +33,7 @@ class LoadProjectDocuments(bpy.types.Operator): core.load_project_documents(tool.Document) return {"FINISHED"} + class DisableDocumentEditingUI(bpy.types.Operator): bl_idname = "bim.disable_document_editing_ui" bl_label = "Disable Document Editing UI" @@ -109,6 +111,7 @@ class AddInformation(bpy.types.Operator, tool.Ifc.Operator): props.json_string = json.dumps(expanded_docs) bpy.ops.bim.load_project_documents() + class AddDocumentReference(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.add_document_reference" bl_label = "Add Document Reference" @@ -154,8 +157,6 @@ class EditDocument(bpy.types.Operator, tool.Ifc.Operator): if props.active_document_id: core.edit_document(tool.Ifc, tool.Document, document=tool.Ifc.get().by_id(props.active_document_id)) props.active_document_id = 0 - props.is_document_editing = False - tool.Document.update_assigned_documents() class RemoveDocument(bpy.types.Operator, tool.Ifc.Operator): @@ -167,6 +168,7 @@ class RemoveDocument(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): core.remove_document(tool.Ifc, tool.Document, document=tool.Ifc.get().by_id(self.document)) + class AssignDocument(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.assign_document" bl_label = "Assign Document" @@ -183,11 +185,9 @@ class AssignDocument(bpy.types.Operator, tool.Ifc.Operator): if element: core.assign_document(tool.Ifc, product=element, document=document) - tool.Document.update_document_objects(self.document) ObjectDocumentData.is_loaded = False ObjectDocumentData.load() - tool.Document.update_assigned_documents() return {"FINISHED"} @@ -206,7 +206,7 @@ class UnassignDocument(bpy.types.Operator, tool.Ifc.Operator): element = tool.Ifc.get_entity(obj) if element: core.unassign_document(tool.Ifc, product=element, document=document) - + props = tool.Document.get_document_props() active_document_id = None if props.active_document: @@ -216,11 +216,9 @@ class UnassignDocument(bpy.types.Operator, tool.Ifc.Operator): tool.Document.update_document_objects(active_document_id) else: tool.Document.update_document_objects() - + ObjectDocumentData.is_loaded = False ObjectDocumentData.load() - - tool.Document.update_assigned_documents() return {"FINISHED"} @@ -253,16 +251,12 @@ class LoadObjectDocuments(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} def execute(self, context): - if not ObjectDocumentData.is_loaded: - ObjectDocumentData.load() - core.load_project_documents(tool.Document) props = tool.Document.get_document_props() props.is_object_editing = True - - tool.Document.update_assigned_documents() - + ObjectDocumentData.is_loaded = False + ObjectDocumentData.load() return {"FINISHED"} @@ -277,19 +271,24 @@ class OpenIFCDocument(bpy.types.Operator): def execute(self, context): import subprocess import os + if not self.uri or not self.uri.lower().startswith("file://"): self.report({"ERROR"}, "Only local file:// URIs are supported") return {"CANCELLED"} filepath = self.uri[7:] # Remove file:// prefix - + if not os.path.exists(filepath): self.report({"ERROR"}, f"File not found: {filepath}") return {"CANCELLED"} try: blender_path = bpy.app.binary_path - args = [blender_path, "--python-expr", "import bpy; bpy.ops.bim.load_project(filepath='{}')".format(filepath)] + args = [ + blender_path, + "--python-expr", + "import bpy; bpy.ops.bim.load_project(filepath='{}')".format(filepath), + ] subprocess.Popen(args) self.report({"INFO"}, f"Opening {filepath} in a new Blender instance") except Exception as e: @@ -325,4 +324,3 @@ class ToggleDocument(bpy.types.Operator): props.json_string = json.dumps(expanded_documents) bpy.ops.bim.load_project_documents() return {"FINISHED"} - diff --git a/src/bonsai/bonsai/bim/module/document/prop.py b/src/bonsai/bonsai/bim/module/document/prop.py index b0964dd5c9..5d9a978c69 100644 --- a/src/bonsai/bonsai/bim/module/document/prop.py +++ b/src/bonsai/bonsai/bim/module/document/prop.py @@ -51,7 +51,7 @@ def update_document_identification(self: "Document", context: bpy.types.Context) def update_active_document(self, context): - if (document := self.active_document): + if document := self.active_document: if document.ifc_definition_id: DocumentData.load_document_objects_into_props(document.ifc_definition_id) @@ -72,7 +72,7 @@ class Document(PropertyGroup): ("INFORMATION", "Information", "IfcDocumentInformation"), ("REFERENCE", "Reference", "IfcDocumentReference"), ], - default="INFORMATION" + default="INFORMATION", ) if TYPE_CHECKING: @@ -96,41 +96,15 @@ class DocumentObject(PropertyGroup): ifc_definition_id: int -class AssignedDocument(PropertyGroup): - name: StringProperty(name="Name") - identification: StringProperty(name="Identification") - description: StringProperty(name="Description", default="") - ifc_definition_id: IntProperty(name="IFC Definition ID") - location: StringProperty(name="Location", default="") - document_type: EnumProperty( - name="Document Type", - items=[ - ("PROJECT", "Project", "Virtual project root node"), - ("INFORMATION", "Information", "IfcDocumentInformation"), - ("REFERENCE", "Reference", "IfcDocumentReference"), - ], - default="INFORMATION" - ) - - if TYPE_CHECKING: - name: str - identification: str - ifc_definition_id: int - location: str - document_type: str - class BIMDocumentProperties(PropertyGroup): document_attributes: CollectionProperty(name="Document Attributes", type=Attribute) active_document_id: IntProperty(name="Active Document Id") documents: CollectionProperty(name="Documents", type=Document) active_document_index: IntProperty(name="Active Document Index", update=update_active_document) is_editing: BoolProperty(name="Is Editing", default=False) - is_document_editing: BoolProperty(name="Is Document Editing", default=False) is_object_editing: BoolProperty(name="Is Object Editing", default=False) document_objects: CollectionProperty(name="Document Objects", type=DocumentObject) active_document_object_index: IntProperty(name="Active Document Object Index") - assigned_documents: CollectionProperty(name="Assigned Documents", type=AssignedDocument) - active_assigned_document_index: IntProperty(name="Active Assigned Document Index") json_string: StringProperty(name="JSON String", default="[]") if TYPE_CHECKING: @@ -139,7 +113,6 @@ class BIMDocumentProperties(PropertyGroup): documents: bpy.types.bpy_prop_collection_idprop[Document] active_document_index: int is_editing: bool - is_document_editing: bool is_object_editing: bool document_objects: bpy.types.bpy_prop_collection_idprop[DocumentObject] active_document_object_index: int diff --git a/src/bonsai/bonsai/bim/module/document/ui.py b/src/bonsai/bonsai/bim/module/document/ui.py index a742130f94..84f7d04632 100644 --- a/src/bonsai/bonsai/bim/module/document/ui.py +++ b/src/bonsai/bonsai/bim/module/document/ui.py @@ -22,6 +22,7 @@ from bpy.types import Panel, UIList from bonsai.bim.helper import draw_attributes from .data import DocumentData, ObjectDocumentData + class BIM_PT_documents(Panel): bl_label = "Documents" bl_idname = "BIM_PT_documents" @@ -45,9 +46,7 @@ class BIM_PT_documents(Panel): split = row.split(factor=0.55) left_row = split.row(align=True) - total_documents = DocumentData.data["total_document_informations"] + DocumentData.data["total_document_references"] - left_row.label(text="{} Documents".format(total_documents), icon="FILE") - + left_row.label(text="{} Documents".format(DocumentData.data["total_documents"]), icon="FILE") right_row = split.row(align=True) right_row.label( text="{} Objects Referenced".format(DocumentData.data["total_referenced_objects"]), icon="OBJECT_DATA" @@ -63,7 +62,7 @@ class BIM_PT_documents(Panel): row = self.layout.row(align=True) row.alignment = "RIGHT" - if self.props.is_document_editing: + if self.props.active_document_id > 0: row.operator("bim.edit_document", text="", icon="CHECKMARK") row.operator("bim.disable_editing_document", text="", icon="CANCEL") else: @@ -71,25 +70,27 @@ class BIM_PT_documents(Panel): row.operator("bim.add_information", text="", icon="ADD") if self.props.active_document and ( - self.props.active_document.document_type == "INFORMATION" and - self.props.active_document.document_type != "PROJECT" + self.props.active_document.document_type == "INFORMATION" + and self.props.active_document.document_type != "PROJECT" ): row.operator("bim.add_document_reference", text="", icon="FILE_HIDDEN") active_document = self.props.active_document if active_document: ifc_definition_id = active_document.ifc_definition_id - + if active_document.document_type != "PROJECT": row.operator("bim.select_document_objects", text="", icon="RESTRICT_SELECT_OFF").document = ( ifc_definition_id ) row.operator("bim.assign_document", text="", icon="BRUSH_DATA").document = ifc_definition_id - row.operator("bim.enable_editing_document", text="", icon="GREASEPENCIL").document = ifc_definition_id - row.operator("bim.remove_document", text="", icon="X").document = ifc_definition_id + row.operator("bim.enable_editing_document", text="", icon="GREASEPENCIL").document = ( + ifc_definition_id + ) + row.operator("bim.remove_document", text="", icon="X").document = ifc_definition_id self.layout.template_list("BIM_UL_documents", "", self.props, "documents", self.props, "active_document_index") - if self.props.is_document_editing: + if self.props.active_document_id > 0: active_document = self.props.active_document draw_attributes(self.props.document_attributes, self.layout) @@ -107,6 +108,7 @@ class BIM_PT_documents(Panel): "active_document_object_index", ) + class BIM_PT_object_documents(Panel): bl_label = "Documents" bl_idname = "BIM_PT_object_documents" @@ -161,25 +163,27 @@ class BIM_PT_object_documents(Panel): col = box.column(align=True) for document in ObjectDocumentData.data["documents"]: row = col.row(align=True) - + # Create a split layout to separate left and right sides split = row.split(factor=0.7) # Adjust factor as needed (0.7 = 70% left, 30% right) - + # Left side - Document identification and name left_side = split.row(align=True) - left_side.alignment = 'LEFT' + left_side.alignment = "LEFT" left_side.label(text=document["identification"] or "*", icon="FILE") left_side.label(text=document["name"] or "Unnamed") - + # Right side - Action buttons right_side = split.row(align=True) - right_side.alignment = 'RIGHT' # Align buttons to the right - + right_side.alignment = "RIGHT" # Align buttons to the right + if document["location"]: if document["location"].lower().endswith(".ifc"): - right_side.operator("bim.open_ifc_document", icon="HIDE_OFF", text="").uri = document["location"] + right_side.operator("bim.open_ifc_document", icon="HIDE_OFF", text="").uri = document[ + "location" + ] right_side.operator("bim.open_uri", icon="URL", text="").uri = document["location"] - + right_side.operator("bim.unassign_document", text="", icon="X").document = document["id"] def draw_add_ui(self): @@ -194,9 +198,11 @@ class BIM_PT_object_documents(Panel): for doc in ObjectDocumentData.data["documents"]: assigned_doc_ids.append(doc["id"]) - if (document.document_type == "INFORMATION" and - document.document_type != "PROJECT" and - document.ifc_definition_id not in assigned_doc_ids): + if ( + document.document_type == "INFORMATION" + and document.document_type != "PROJECT" + and document.ifc_definition_id not in assigned_doc_ids + ): doc_op = row.operator("bim.assign_document", text="", icon="BRUSH_DATA") doc_op.document = document.ifc_definition_id elif document.ifc_definition_id in assigned_doc_ids: @@ -215,15 +221,15 @@ class BIM_UL_documents(UIList): if item.document_type != "PROJECT": if item.tree_depth > 1: indent_depth = item.tree_depth - 1 - + for i in range(indent_depth): row.label(text="", icon="BLANK1") - + if item.document_type == "PROJECT": row.label(text="", icon="OUTLINER_COLLECTION") row.label(text=item.name) return - + if item.document_type == "INFORMATION" and item.has_children: op = row.operator( "bim.toggle_document", icon="TRIA_DOWN" if item.is_expanded else "TRIA_RIGHT", text="", emboss=False @@ -232,7 +238,7 @@ class BIM_UL_documents(UIList): op.option = "Collapse" if item.is_expanded else "Expand" elif item.document_type == "INFORMATION": row.label(text="", icon="BLANK1") - + if item.document_type == "INFORMATION": row.label(text="", icon="FILE") text = " - ".join([x for x in [item.name, item.location] if x]) diff --git a/src/bonsai/bonsai/core/document.py b/src/bonsai/bonsai/core/document.py index e55c077022..07a167c785 100644 --- a/src/bonsai/bonsai/core/document.py +++ b/src/bonsai/bonsai/core/document.py @@ -29,27 +29,29 @@ def load_project_documents(document: tool.Document) -> None: document.import_project_documents() document.enable_editing_ui() + def disable_document_editing_ui(document: tool.Document) -> None: document.disable_editing_ui() document.disable_editing_document() + def disable_object_document_editing_ui(document: tool.Document) -> None: props = document.get_document_props() props.is_object_editing = False + def enable_editing_document(document_tool: tool.Document, document: ifcopenshell.entity_instance) -> None: props = document_tool.get_document_props() - props.is_document_editing = True document_tool.set_active_document(document) document_tool.import_document_attributes(document) def disable_editing_document(document: tool.Document) -> None: props = document.get_document_props() - props.is_document_editing = False document.clear_active_document() document.clear_document_attributes() + def add_information(ifc: tool.Ifc, document_tool: tool.Document, parent=None) -> ifcopenshell.entity_instance: document_tool.clear_document_tree() diff --git a/src/bonsai/bonsai/tool/document.py b/src/bonsai/bonsai/tool/document.py index cd51f35e8d..bbc20663dc 100644 --- a/src/bonsai/bonsai/tool/document.py +++ b/src/bonsai/bonsai/tool/document.py @@ -153,8 +153,7 @@ class Document(bonsai.core.tool.Document): if root.is_expanded: root_documents = natsorted( - root_documents, - key=lambda doc: (cls.get_document_information_id(doc) or "", doc.Name or "") + root_documents, key=lambda doc: (cls.get_document_information_id(doc) or "", doc.Name or "") ) for doc in root_documents: @@ -204,21 +203,17 @@ class Document(bonsai.core.tool.Document): ref_children = [d for d in children if not d.is_a("IfcDocumentInformation")] info_children = natsorted( - info_children, - key=lambda doc: (cls.get_document_information_id(doc) or "", doc.Name or "") + info_children, key=lambda doc: (cls.get_document_information_id(doc) or "", doc.Name or "") ) ref_children = natsorted( ref_children, - key=lambda doc: ( - cls.get_external_reference_id(doc) or "", - doc.Description or doc.Name or "" - ) + key=lambda doc: (cls.get_external_reference_id(doc) or "", doc.Description or doc.Name or ""), ) for child in info_children + ref_children: cls._process_document(child, props, document_children, expanded_documents, depth + 1) - + @classmethod def is_document_information(cls, document: ifcopenshell.entity_instance) -> bool: return document.is_a("IfcDocumentInformation") @@ -295,12 +290,14 @@ class Document(bonsai.core.tool.Document): @classmethod def refresh_document_data(cls) -> None: import bonsai.bim.module.document.data as document_data + document_data.DocumentData.is_loaded = False document_data.DocumentData.load() @classmethod def load_document_objects_into_props(cls, document_id: int) -> None: import bonsai.bim.module.document.data as document_data + document_data.DocumentData.load_document_objects_into_props(document_id) @classmethod @@ -314,32 +311,3 @@ class Document(bonsai.core.tool.Document): if document_id: cls.load_document_objects_into_props(document_id) - - @classmethod - def update_assigned_documents(cls) -> None: - from bonsai.bim.module.document.data import ObjectDocumentData - - ObjectDocumentData.is_loaded = False - - props = cls.get_document_props() - props.assigned_documents.clear() - - if not ObjectDocumentData.is_loaded: - ObjectDocumentData.load() - - if not ObjectDocumentData.data.get("documents"): - return - - sorted_docs = sorted( - ObjectDocumentData.data["documents"], - key=lambda doc: ((doc.get("identification") or "").lower(), (doc.get("name") or "").lower()), - ) - - for document in sorted_docs: - new = props.assigned_documents.add() - new.name = document["name"] or "Unnamed" - new.identification = document["identification"] or "*" - new.document_type = "INFORMATION" if document.get("is_information", False) else "REFERENCE" - new.ifc_definition_id = document["id"] - new.location = document.get("location") or "" - new.description = document.get("description") or "" \ No newline at end of file From 6f400c8e44bc9e097472542f4470c97f2bd46232 Mon Sep 17 00:00:00 2001 From: falken10vdl Date: Fri, 8 Aug 2025 18:48:15 +0200 Subject: [PATCH 08/12] updates based on core developer's feedback --- src/bonsai/bonsai/bim/helper.py | 3 -- src/bonsai/bonsai/bim/module/document/data.py | 19 ++++--------- .../bonsai/bim/module/document/operator.py | 28 +++++++------------ src/bonsai/bonsai/bim/module/document/prop.py | 24 +++------------- src/bonsai/bonsai/bim/module/document/ui.py | 17 ++++------- src/bonsai/bonsai/core/document.py | 5 +--- src/bonsai/bonsai/tool/document.py | 6 ++++ 7 files changed, 31 insertions(+), 71 deletions(-) diff --git a/src/bonsai/bonsai/bim/helper.py b/src/bonsai/bonsai/bim/helper.py index eb8d9fa6e4..e76b512de3 100644 --- a/src/bonsai/bonsai/bim/helper.py +++ b/src/bonsai/bonsai/bim/helper.py @@ -55,7 +55,6 @@ def draw_attributes( layout: bpy.types.UILayout, copy_operator: Optional[str] = None, popup_active_attribute: Optional[bonsai.bim.prop.Attribute] = None, - filter_attributes: list[str] = None, callback: Optional[Callable[[bonsai.bim.prop.Attribute, bpy.types.UILayout], None]] = None, *, enable_search: Union[bool, EllipsisType] = ..., @@ -76,8 +75,6 @@ def draw_attributes( """ for attribute in props: - if attribute.name in (filter_attributes or []): - continue row = layout.row(align=True) if attribute == popup_active_attribute: row.activate_init = True diff --git a/src/bonsai/bonsai/bim/module/document/data.py b/src/bonsai/bonsai/bim/module/document/data.py index 0ba0106881..7f91247b87 100644 --- a/src/bonsai/bonsai/bim/module/document/data.py +++ b/src/bonsai/bonsai/bim/module/document/data.py @@ -24,6 +24,11 @@ import bonsai.tool as tool from natsort import natsorted +def refresh(): + DocumentData.is_loaded = False + ObjectDocumentData.is_loaded = False + + class DocumentData: data = {} is_loaded = False @@ -32,7 +37,6 @@ class DocumentData: def load(cls): cls.data = { "total_documents": cls.total_documents(), - "total_referenced_objects": cls.total_referenced_objects(), "document_objects": cls.document_objects(), } cls.is_loaded = True @@ -42,19 +46,6 @@ class DocumentData: file = tool.Ifc.get() return len(file.by_type("IfcDocumentInformation")) + len(file.by_type("IfcDocumentReference")) - @classmethod - def total_referenced_objects(cls): - file = tool.Ifc.get() - document_rels = file.by_type("IfcRelAssociatesDocument") - documented_objects = set() - for rel in document_rels: - for related_object in rel.RelatedObjects: - obj = tool.Ifc.get_object(related_object) - if obj: - documented_objects.add(related_object.id()) - - return len(documented_objects) - @classmethod def document_objects(cls): document_objects = {} diff --git a/src/bonsai/bonsai/bim/module/document/operator.py b/src/bonsai/bonsai/bim/module/document/operator.py index c2ccd5edc9..10eab2f5ed 100644 --- a/src/bonsai/bonsai/bim/module/document/operator.py +++ b/src/bonsai/bonsai/bim/module/document/operator.py @@ -21,8 +21,7 @@ import json import bonsai.bim.handler import bonsai.tool as tool import bonsai.core.document as core -from .data import DocumentData, ObjectDocumentData - +from bonsai.bim.module.document.data import DocumentData, ObjectDocumentData class LoadProjectDocuments(bpy.types.Operator): bl_idname = "bim.load_project_documents" @@ -186,7 +185,6 @@ class AssignDocument(bpy.types.Operator, tool.Ifc.Operator): core.assign_document(tool.Ifc, product=element, document=document) tool.Document.update_document_objects(self.document) - ObjectDocumentData.is_loaded = False ObjectDocumentData.load() return {"FINISHED"} @@ -217,7 +215,6 @@ class UnassignDocument(bpy.types.Operator, tool.Ifc.Operator): else: tool.Document.update_document_objects() - ObjectDocumentData.is_loaded = False ObjectDocumentData.load() return {"FINISHED"} @@ -255,7 +252,6 @@ class LoadObjectDocuments(bpy.types.Operator): props = tool.Document.get_document_props() props.is_object_editing = True - ObjectDocumentData.is_loaded = False ObjectDocumentData.load() return {"FINISHED"} @@ -276,27 +272,23 @@ class OpenIFCDocument(bpy.types.Operator): self.report({"ERROR"}, "Only local file:// URIs are supported") return {"CANCELLED"} - filepath = self.uri[7:] # Remove file:// prefix + filepath = self.uri[7:] if not os.path.exists(filepath): self.report({"ERROR"}, f"File not found: {filepath}") return {"CANCELLED"} - try: - blender_path = bpy.app.binary_path - args = [ - blender_path, - "--python-expr", - "import bpy; bpy.ops.bim.load_project(filepath='{}')".format(filepath), - ] - subprocess.Popen(args) - self.report({"INFO"}, f"Opening {filepath} in a new Blender instance") - except Exception as e: - self.report({"ERROR"}, f"Failed to open IFC file: {str(e)}") + blender_path = bpy.app.binary_path + args = [ + blender_path, + "--python-expr", + "import bpy; bpy.ops.bim.load_project(filepath='{}')".format(filepath), + ] + subprocess.Popen(args) + self.report({"INFO"}, f"Opening {filepath} in a new Blender instance") return {"FINISHED"} - class ToggleDocument(bpy.types.Operator): bl_idname = "bim.toggle_document" bl_label = "Toggle Document" diff --git a/src/bonsai/bonsai/bim/module/document/prop.py b/src/bonsai/bonsai/bim/module/document/prop.py index 5d9a978c69..93369b546d 100644 --- a/src/bonsai/bonsai/bim/module/document/prop.py +++ b/src/bonsai/bonsai/bim/module/document/prop.py @@ -1,24 +1,7 @@ -# Bonsai - OpenBIM Blender Add-on -# Copyright (C) 2020, 2021 Dion Moult -# -# This file is part of Bonsai. -# -# Bonsai is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Bonsai is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Bonsai. If not, see . - import bpy import bonsai.tool as tool from bonsai.bim.prop import StrProperty, Attribute +from bonsai.bim.module.document.data import refresh from bpy.types import PropertyGroup from bpy.props import ( PointerProperty, @@ -50,7 +33,8 @@ def update_document_identification(self: "Document", context: bpy.types.Context) tool.Document.set_external_reference_id(document, self.identification) -def update_active_document(self, context): +def update_active_document_index(self, context): + refresh() if document := self.active_document: if document.ifc_definition_id: DocumentData.load_document_objects_into_props(document.ifc_definition_id) @@ -100,7 +84,7 @@ class BIMDocumentProperties(PropertyGroup): document_attributes: CollectionProperty(name="Document Attributes", type=Attribute) active_document_id: IntProperty(name="Active Document Id") documents: CollectionProperty(name="Documents", type=Document) - active_document_index: IntProperty(name="Active Document Index", update=update_active_document) + active_document_index: IntProperty(name="Active Document Index", update=update_active_document_index) is_editing: BoolProperty(name="Is Editing", default=False) is_object_editing: BoolProperty(name="Is Object Editing", default=False) document_objects: CollectionProperty(name="Document Objects", type=DocumentObject) diff --git a/src/bonsai/bonsai/bim/module/document/ui.py b/src/bonsai/bonsai/bim/module/document/ui.py index 84f7d04632..60e83fdcbe 100644 --- a/src/bonsai/bonsai/bim/module/document/ui.py +++ b/src/bonsai/bonsai/bim/module/document/ui.py @@ -20,8 +20,7 @@ import bpy import bonsai.tool as tool from bpy.types import Panel, UIList from bonsai.bim.helper import draw_attributes -from .data import DocumentData, ObjectDocumentData - +from bonsai.bim.module.document.data import DocumentData, ObjectDocumentData class BIM_PT_documents(Panel): bl_label = "Documents" @@ -43,18 +42,12 @@ class BIM_PT_documents(Panel): self.props = tool.Document.get_document_props() row = self.layout.row(align=True) - split = row.split(factor=0.55) - - left_row = split.row(align=True) - left_row.label(text="{} Documents".format(DocumentData.data["total_documents"]), icon="FILE") - right_row = split.row(align=True) - right_row.label( - text="{} Objects Referenced".format(DocumentData.data["total_referenced_objects"]), icon="OBJECT_DATA" - ) + row.label(text="{} Documents found".format(DocumentData.data["total_documents"]), icon="FILE") + if self.props.is_editing: - right_row.operator("bim.disable_document_editing_ui", text="", icon="CANCEL") + row.operator("bim.disable_document_editing_ui", text="", icon="CANCEL") else: - right_row.operator("bim.load_project_documents", text="", icon="IMPORT") + row.operator("bim.load_project_documents", text="", icon="IMPORT") if not self.props.is_editing: return diff --git a/src/bonsai/bonsai/core/document.py b/src/bonsai/bonsai/core/document.py index 07a167c785..67680a7a55 100644 --- a/src/bonsai/bonsai/core/document.py +++ b/src/bonsai/bonsai/core/document.py @@ -36,18 +36,15 @@ def disable_document_editing_ui(document: tool.Document) -> None: def disable_object_document_editing_ui(document: tool.Document) -> None: - props = document.get_document_props() - props.is_object_editing = False + document.disable_object_editing_ui() def enable_editing_document(document_tool: tool.Document, document: ifcopenshell.entity_instance) -> None: - props = document_tool.get_document_props() document_tool.set_active_document(document) document_tool.import_document_attributes(document) def disable_editing_document(document: tool.Document) -> None: - props = document.get_document_props() document.clear_active_document() document.clear_document_attributes() diff --git a/src/bonsai/bonsai/tool/document.py b/src/bonsai/bonsai/tool/document.py index bbc20663dc..28edfe12e0 100644 --- a/src/bonsai/bonsai/tool/document.py +++ b/src/bonsai/bonsai/tool/document.py @@ -19,6 +19,7 @@ from __future__ import annotations import bpy import ifcopenshell.util.system +import bonsai.bim.helper import bonsai.core.tool import bonsai.tool as tool import json @@ -44,6 +45,11 @@ class Document(bonsai.core.tool.Document): props = cls.get_document_props() props.active_document_id = 0 + @classmethod + def disable_object_editing_ui(cls) -> None: + props = cls.get_document_props() + props.is_object_editing = False + @classmethod def disable_editing_ui(cls) -> None: props = cls.get_document_props() From e4ba633d94e5ce68d1d6937a6a75af194ceb56d8 Mon Sep 17 00:00:00 2001 From: falken10vdl Date: Fri, 8 Aug 2025 20:28:40 +0200 Subject: [PATCH 09/12] cleanup nomenclature and some redundant code --- src/bonsai/bonsai/bim/module/document/data.py | 3 +- .../bonsai/bim/module/document/operator.py | 12 ++--- src/bonsai/bonsai/bim/module/document/ui.py | 4 +- src/bonsai/bonsai/core/document.py | 54 +++++++++---------- src/bonsai/bonsai/tool/document.py | 47 +++++++--------- 5 files changed, 54 insertions(+), 66 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/document/data.py b/src/bonsai/bonsai/bim/module/document/data.py index 7f91247b87..7fc05ca3d1 100644 --- a/src/bonsai/bonsai/bim/module/document/data.py +++ b/src/bonsai/bonsai/bim/module/document/data.py @@ -125,7 +125,6 @@ class ObjectDocumentData: location = None identification = None - description = None if is_information: if tool.Ifc.get_schema() == "IFC2X3": @@ -134,7 +133,7 @@ class ObjectDocumentData: identification = relating_document.Identification location = getattr(relating_document, "Location", None) - + description = getattr(relating_document, "Description", "No description") else: description = relating_document.Description if tool.Ifc.get_schema() == "IFC2X3": diff --git a/src/bonsai/bonsai/bim/module/document/operator.py b/src/bonsai/bonsai/bim/module/document/operator.py index 10eab2f5ed..f63b5e1693 100644 --- a/src/bonsai/bonsai/bim/module/document/operator.py +++ b/src/bonsai/bonsai/bim/module/document/operator.py @@ -60,7 +60,7 @@ class EnableEditingDocument(bpy.types.Operator): document: bpy.props.IntProperty() def execute(self, context): - core.enable_editing_document(tool.Document, document=tool.Ifc.get().by_id(self.document)) + core.enable_editing_document(tool.Document, ifc_document=tool.Ifc.get().by_id(self.document)) return {"FINISHED"} @@ -154,7 +154,7 @@ class EditDocument(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): props = tool.Document.get_document_props() if props.active_document_id: - core.edit_document(tool.Ifc, tool.Document, document=tool.Ifc.get().by_id(props.active_document_id)) + core.edit_document(tool.Ifc, tool.Document, ifc_document=tool.Ifc.get().by_id(props.active_document_id)) props.active_document_id = 0 @@ -165,7 +165,7 @@ class RemoveDocument(bpy.types.Operator, tool.Ifc.Operator): document: bpy.props.IntProperty() def _execute(self, context): - core.remove_document(tool.Ifc, tool.Document, document=tool.Ifc.get().by_id(self.document)) + core.remove_document(tool.Ifc, tool.Document, ifc_document=tool.Ifc.get().by_id(self.document)) class AssignDocument(bpy.types.Operator, tool.Ifc.Operator): @@ -177,12 +177,11 @@ class AssignDocument(bpy.types.Operator, tool.Ifc.Operator): document: bpy.props.IntProperty() def _execute(self, context): - document = tool.Ifc.get().by_id(self.document) objs = [bpy.data.objects[self.obj]] if self.obj else tool.Blender.get_selected_objects() for obj in objs: element = tool.Ifc.get_entity(obj) if element: - core.assign_document(tool.Ifc, product=element, document=document) + core.assign_document(tool.Ifc, product=element, ifc_document=tool.Ifc.get().by_id(self.document)) tool.Document.update_document_objects(self.document) ObjectDocumentData.load() @@ -197,13 +196,12 @@ class UnassignDocument(bpy.types.Operator, tool.Ifc.Operator): document: bpy.props.IntProperty() def _execute(self, context): - document = tool.Ifc.get().by_id(self.document) objs = [bpy.data.objects.get(self.obj)] if self.obj else tool.Blender.get_selected_objects() for obj in objs: if obj: element = tool.Ifc.get_entity(obj) if element: - core.unassign_document(tool.Ifc, product=element, document=document) + core.unassign_document(tool.Ifc, product=element, ifc_document=tool.Ifc.get().by_id(self.document)) props = tool.Document.get_document_props() active_document_id = None diff --git a/src/bonsai/bonsai/bim/module/document/ui.py b/src/bonsai/bonsai/bim/module/document/ui.py index 60e83fdcbe..68a9c5adb2 100644 --- a/src/bonsai/bonsai/bim/module/document/ui.py +++ b/src/bonsai/bonsai/bim/module/document/ui.py @@ -234,10 +234,10 @@ class BIM_UL_documents(UIList): if item.document_type == "INFORMATION": row.label(text="", icon="FILE") - text = " - ".join([x for x in [item.name, item.location] if x]) + text = " - ".join([x for x in [item.location, item.description, item.name] if x]) else: row.label(text="", icon="FILE_HIDDEN") - text = " - ".join([x for x in [item.description, item.location] if x]) + text = " - ".join([x for x in [item.location, item.description] if x]) split1 = row.split(factor=0.1) split1.prop(item, "identification", text="", emboss=False) split2 = split1.split(factor=0.8) diff --git a/src/bonsai/bonsai/core/document.py b/src/bonsai/bonsai/core/document.py index 67680a7a55..7b42295500 100644 --- a/src/bonsai/bonsai/core/document.py +++ b/src/bonsai/bonsai/core/document.py @@ -39,9 +39,9 @@ def disable_object_document_editing_ui(document: tool.Document) -> None: document.disable_object_editing_ui() -def enable_editing_document(document_tool: tool.Document, document: ifcopenshell.entity_instance) -> None: - document_tool.set_active_document(document) - document_tool.import_document_attributes(document) +def enable_editing_document(document: tool.Document, ifc_document: ifcopenshell.entity_instance) -> None: + document.set_active_document(ifc_document) + document.import_document_attributes(ifc_document) def disable_editing_document(document: tool.Document) -> None: @@ -49,19 +49,19 @@ def disable_editing_document(document: tool.Document) -> None: document.clear_document_attributes() -def add_information(ifc: tool.Ifc, document_tool: tool.Document, parent=None) -> ifcopenshell.entity_instance: - document_tool.clear_document_tree() +def add_information(ifc: tool.Ifc, document: tool.Document, parent=None) -> ifcopenshell.entity_instance: + document.clear_document_tree() if parent is None: - parent = document_tool.get_default_parent_for_information(ifc) + parent = document.get_default_parent_for_information(ifc) information = ifc.run("document.add_information", parent=parent) ifc.run("document.add_reference", information=information) - if document_tool.is_document_information(parent): - document_tool.expand_document(parent) + if document.is_document_information(parent): + document.expand_document(parent) - document_tool.import_project_documents() + document.import_project_documents() return information @@ -76,33 +76,33 @@ def add_reference(ifc: tool.Ifc, document: tool.Document) -> None: document.import_project_documents() -def edit_document(ifc: tool.Ifc, document_tool: tool.Document, document: ifcopenshell.entity_instance) -> None: - attributes = document_tool.export_document_attributes() - if document_tool.is_document_information(document): - ifc.run("document.edit_information", information=document, attributes=attributes) +def edit_document(ifc: tool.Ifc, document: tool.Document, ifc_document: ifcopenshell.entity_instance) -> None: + attributes = document.export_document_attributes() + if document.is_document_information(ifc_document): + ifc.run("document.edit_information", information=ifc_document, attributes=attributes) else: - ifc.run("document.edit_reference", reference=document, attributes=attributes) - document_tool.disable_editing_document() - document_tool.clear_document_tree() - document_tool.import_project_documents() + ifc.run("document.edit_reference", reference=ifc_document, attributes=attributes) + document.disable_editing_document() + document.clear_document_tree() + document.import_project_documents() -def remove_document(ifc: tool.Ifc, document_tool: tool.Document, document: ifcopenshell.entity_instance) -> None: - document_tool.clear_document_tree() - if document_tool.is_document_information(document): - ifc.run("document.remove_information", information=document) +def remove_document(ifc: tool.Ifc, document: tool.Document, ifc_document: ifcopenshell.entity_instance) -> None: + document.clear_document_tree() + if document.is_document_information(ifc_document): + ifc.run("document.remove_information", information=ifc_document) else: - ifc.run("document.remove_reference", reference=document) - document_tool.import_project_documents() + ifc.run("document.remove_reference", reference=ifc_document) + document.import_project_documents() def assign_document( - ifc: tool.Ifc, product: ifcopenshell.entity_instance, document: ifcopenshell.entity_instance + ifc: tool.Ifc, product: ifcopenshell.entity_instance, ifc_document: ifcopenshell.entity_instance ) -> None: - ifc.run("document.assign_document", products=[product], document=document) + ifc.run("document.assign_document", products=[product], document=ifc_document) def unassign_document( - ifc: tool.Ifc, product: ifcopenshell.entity_instance, document: ifcopenshell.entity_instance + ifc: tool.Ifc, product: ifcopenshell.entity_instance, ifc_document: ifcopenshell.entity_instance ) -> None: - ifc.run("document.unassign_document", products=[product], document=document) + ifc.run("document.unassign_document", products=[product], document=ifc_document) diff --git a/src/bonsai/bonsai/tool/document.py b/src/bonsai/bonsai/tool/document.py index 28edfe12e0..f8f45fffcf 100644 --- a/src/bonsai/bonsai/tool/document.py +++ b/src/bonsai/bonsai/tool/document.py @@ -172,30 +172,23 @@ class Document(bonsai.core.tool.Document): new.document_type = "INFORMATION" if document.is_a("IfcDocumentInformation") else "REFERENCE" new.tree_depth = depth - file = document.file + new.name = document.Name or "" + new.identification = cls.get_document_information_id(document) if new.document_type == "INFORMATION" else cls.get_external_reference_id(document) + new.identification = new.identification or "" + new.description = document.Description or "" + new.location = document.Location or "" + if new.document_type == "INFORMATION": new.name = document.Name or "Unnamed" - new.identification = cls.get_document_information_id(document) or "" - new.location = document.Location or "" - else: - new.name = document.Name or "" - new.identification = cls.get_external_reference_id(document) or "" - new.description = document.Description or "" - new.location = document.Location or "" - - if new.document_type == "REFERENCE": - if file.schema == "IFC2X3": - if document.ReferenceToDocument: - doc_info = document.ReferenceToDocument[0] - if not new.name: - new.name = doc_info.Name or "" - new.location = new.location or "" - else: - if document.ReferencedDocument: - doc_info = document.ReferencedDocument - if not new.name: - new.name = doc_info.Name or "" - new.location = new.location or "" + + elif new.document_type == "REFERENCE": + file = document.file + if file.schema == "IFC2X3": + if document.ReferenceToDocument and not new.name: + new.name = document.ReferenceToDocument[0].Name or "" + else: + if document.ReferencedDocument and not new.name: + new.name = document.ReferencedDocument.Name or "" doc_id = document.id() has_children = doc_id in document_children and bool(document_children[doc_id]) @@ -205,16 +198,14 @@ class Document(bonsai.core.tool.Document): if has_children and new.is_expanded: children = document_children[doc_id] - info_children = [d for d in children if d.is_a("IfcDocumentInformation")] - ref_children = [d for d in children if not d.is_a("IfcDocumentInformation")] - info_children = natsorted( - info_children, key=lambda doc: (cls.get_document_information_id(doc) or "", doc.Name or "") + [d for d in children if d.is_a("IfcDocumentInformation")], + key=lambda doc: (cls.get_document_information_id(doc) or "", doc.Name or "") ) ref_children = natsorted( - ref_children, - key=lambda doc: (cls.get_external_reference_id(doc) or "", doc.Description or doc.Name or ""), + [d for d in children if not d.is_a("IfcDocumentInformation")], + key=lambda doc: (cls.get_external_reference_id(doc) or "", doc.Description or doc.Name or "") ) for child in info_children + ref_children: From df6592c7b96a330ea9f84961e7bbe157f732ece4 Mon Sep 17 00:00:00 2001 From: falken10vdl Date: Thu, 21 Aug 2025 09:42:22 +0200 Subject: [PATCH 10/12] updated to get make test-tool MODULE=document working --- src/bonsai/test/tool/test_document.py | 78 +++++++++++++++++++-------- 1 file changed, 55 insertions(+), 23 deletions(-) diff --git a/src/bonsai/test/tool/test_document.py b/src/bonsai/test/tool/test_document.py index 3d313f6293..628f040923 100644 --- a/src/bonsai/test/tool/test_document.py +++ b/src/bonsai/test/tool/test_document.py @@ -22,6 +22,7 @@ import ifcopenshell.api import ifcopenshell.api.document import bonsai.core.tool import bonsai.tool as tool +import json from test.bim.bootstrap import NewFile from bonsai.tool.document import Document as subject @@ -139,35 +140,66 @@ class TestImportDocumentAttributes(NewFile): assert props.document_attributes["Description"].string_value == "Description" -class TestImportProjectDocuments(NewFile): +class TestImportProjectDocumentsExpanded(NewFile): def test_run(self): ifc = ifcopenshell.file() tool.Ifc().set(ifc) - ifc.createIfcProject() - document = ifcopenshell.api.document.add_information(ifc) - subject.import_project_documents() - props = tool.Document.get_document_props() - assert len(props.documents) == 1 - assert props.documents[0].ifc_definition_id == document.id() - assert props.documents[0].name == "Unnamed" - assert props.documents[0].identification == "X" - assert props.documents[0].is_information is True - - -class TestImportReferences(NewFile): - def test_run(self): - ifc = ifcopenshell.file() - tool.Ifc().set(ifc) - ifc.createIfcProject() + project = ifc.createIfcProject() document = ifcopenshell.api.document.add_information(ifc) reference = ifcopenshell.api.document.add_reference(ifc, information=document) - subject.import_references(document) + + props = tool.Document.get_document_props() - assert len(props.documents) == 1 - assert props.documents[0].ifc_definition_id == reference.id() - assert props.documents[0].name == "Unnamed" - assert props.documents[0].identification == "X" - assert props.documents[0].is_information is False + expanded_docs = [document.id()] # Mark document as expanded + props.json_string = json.dumps(expanded_docs) + + subject.import_project_documents() + props = tool.Document.get_document_props() + + # Should have project root + document + reference = 3 total + assert len(props.documents) == 3 + + assert props.documents[0].ifc_definition_id == -project.id() + assert props.documents[0].document_type == "PROJECT" + + doc_info = next((d for d in props.documents if d.ifc_definition_id == document.id()), None) + assert doc_info is not None + assert doc_info.document_type == "INFORMATION" + + doc_ref = next((d for d in props.documents if d.ifc_definition_id == reference.id()), None) + assert doc_ref is not None + assert doc_ref.location == "" + assert doc_ref.identification == "X" + assert doc_ref.document_type == "REFERENCE" + + +class TestImportProjectDocumentsCollapsed(NewFile): + def test_run(self): + ifc = ifcopenshell.file() + tool.Ifc().set(ifc) + project = ifc.createIfcProject() + document = ifcopenshell.api.document.add_information(ifc) + reference = ifcopenshell.api.document.add_reference(ifc, information=document) + + + props = tool.Document.get_document_props() + props.json_string = json.dumps([]) # Empty expanded list + + subject.import_project_documents() + props = tool.Document.get_document_props() + + # Should have project root + document = 2 total (reference not imported because parent is collapsed) + assert len(props.documents) == 2 + + assert props.documents[0].ifc_definition_id == -project.id() + assert props.documents[0].document_type == "PROJECT" + + doc_info = next((d for d in props.documents if d.ifc_definition_id == document.id()), None) + assert doc_info is not None + assert doc_info.document_type == "INFORMATION" + + doc_ref = next((d for d in props.documents if d.ifc_definition_id == reference.id()), None) + assert doc_ref is None class TestIsDocumentInformation(NewFile): From 454ed2c5a118f930eb58495121ea2a6558296801 Mon Sep 17 00:00:00 2001 From: falken10vdl Date: Thu, 21 Aug 2025 10:16:44 +0200 Subject: [PATCH 11/12] updated to get make test-bim MODULE=document working --- src/bonsai/test/bim/feature/document.feature | 39 ++++++-------------- 1 file changed, 12 insertions(+), 27 deletions(-) diff --git a/src/bonsai/test/bim/feature/document.feature b/src/bonsai/test/bim/feature/document.feature index 9ef9657499..35509c2314 100644 --- a/src/bonsai/test/bim/feature/document.feature +++ b/src/bonsai/test/bim/feature/document.feature @@ -6,14 +6,6 @@ Scenario: Load project documents When I press "bim.load_project_documents" Then nothing happens -Scenario: Load document - Given an empty IFC project - And I press "bim.load_project_documents" - And I press "bim.add_information" - And the variable "information" is "{ifc}.by_type('IfcDocumentInformation')[-1].id()" - When I press "bim.load_document(document={information})" - Then nothing happens - Scenario: Disable document editing UI Given an empty IFC project And I press "bim.load_project_documents" @@ -48,7 +40,8 @@ Scenario: Add document reference And I press "bim.load_project_documents" And I press "bim.add_information" And the variable "information" is "{ifc}.by_type('IfcDocumentInformation')[-1].id()" - And I press "bim.load_document(document={information})" + And I press "bim.load_project_documents" + And I set "scene.BIMDocumentProperties.active_document_index" to "1" When I press "bim.add_document_reference" Then nothing happens @@ -74,16 +67,12 @@ Scenario: Assign document And I press "bim.load_project_documents" And I press "bim.add_information" And the variable "information" is "{ifc}.by_type('IfcDocumentInformation')[-1].id()" - And I press "bim.load_document(document={information})" - And I press "bim.add_document_reference" - And the variable "reference" is "{ifc}.by_type('IfcDocumentReference')[-1].id()" And I add a cube And the object "Cube" is selected - And I look at the "Class" panel - And I set the "Products" property to "IfcElement" - And I set the "Class" property to "IfcWall" - And I click "Assign IFC Class" - When I press "bim.assign_document(document={reference})" + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" + And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" + And I press "bim.assign_class" + When I press "bim.assign_document(document={information})" Then nothing happens Scenario: Unassign document @@ -91,15 +80,11 @@ Scenario: Unassign document And I press "bim.load_project_documents" And I press "bim.add_information" And the variable "information" is "{ifc}.by_type('IfcDocumentInformation')[-1].id()" - And I press "bim.load_document(document={information})" - And I press "bim.add_document_reference" - And the variable "reference" is "{ifc}.by_type('IfcDocumentReference')[-1].id()" And I add a cube And the object "Cube" is selected - And I look at the "Class" panel - And I set the "Products" property to "IfcElement" - And I set the "Class" property to "IfcWall" - And I click "Assign IFC Class" - And I press "bim.assign_document(document={reference})" - When I press "bim.unassign_document(document={reference})" - Then nothing happens + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" + And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" + And I press "bim.assign_class" + And I press "bim.assign_document(document={information})" + When I press "bim.unassign_document(document={information})" + Then nothing happens \ No newline at end of file From 81fdf63bd03bfdf0eb2b6ded2f35a4d7b8131451 Mon Sep 17 00:00:00 2001 From: falken10vdl Date: Thu, 21 Aug 2025 12:05:41 +0200 Subject: [PATCH 12/12] adapted to get pytest -p no:pytest-blender test/core/test_document.py working. Black formating --- .../bonsai/bim/module/document/operator.py | 2 + src/bonsai/bonsai/bim/module/document/ui.py | 3 +- src/bonsai/bonsai/core/document.py | 4 +- src/bonsai/bonsai/core/tool.py | 14 ++++ src/bonsai/bonsai/tool/document.py | 24 ++++--- src/bonsai/test/core/test_document.py | 70 +++++++++++++------ src/bonsai/test/tool/test_document.py | 18 +++-- 7 files changed, 91 insertions(+), 44 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/document/operator.py b/src/bonsai/bonsai/bim/module/document/operator.py index f63b5e1693..447d3b1693 100644 --- a/src/bonsai/bonsai/bim/module/document/operator.py +++ b/src/bonsai/bonsai/bim/module/document/operator.py @@ -23,6 +23,7 @@ import bonsai.tool as tool import bonsai.core.document as core from bonsai.bim.module.document.data import DocumentData, ObjectDocumentData + class LoadProjectDocuments(bpy.types.Operator): bl_idname = "bim.load_project_documents" bl_label = "Load Project Documents" @@ -287,6 +288,7 @@ class OpenIFCDocument(bpy.types.Operator): return {"FINISHED"} + class ToggleDocument(bpy.types.Operator): bl_idname = "bim.toggle_document" bl_label = "Toggle Document" diff --git a/src/bonsai/bonsai/bim/module/document/ui.py b/src/bonsai/bonsai/bim/module/document/ui.py index 68a9c5adb2..c382f1c721 100644 --- a/src/bonsai/bonsai/bim/module/document/ui.py +++ b/src/bonsai/bonsai/bim/module/document/ui.py @@ -22,6 +22,7 @@ from bpy.types import Panel, UIList from bonsai.bim.helper import draw_attributes from bonsai.bim.module.document.data import DocumentData, ObjectDocumentData + class BIM_PT_documents(Panel): bl_label = "Documents" bl_idname = "BIM_PT_documents" @@ -43,7 +44,7 @@ class BIM_PT_documents(Panel): row = self.layout.row(align=True) row.label(text="{} Documents found".format(DocumentData.data["total_documents"]), icon="FILE") - + if self.props.is_editing: row.operator("bim.disable_document_editing_ui", text="", icon="CANCEL") else: diff --git a/src/bonsai/bonsai/core/document.py b/src/bonsai/bonsai/core/document.py index 7b42295500..ac49fee10d 100644 --- a/src/bonsai/bonsai/core/document.py +++ b/src/bonsai/bonsai/core/document.py @@ -53,7 +53,7 @@ def add_information(ifc: tool.Ifc, document: tool.Document, parent=None) -> ifco document.clear_document_tree() if parent is None: - parent = document.get_default_parent_for_information(ifc) + parent = document.get_default_parent_for_information() information = ifc.run("document.add_information", parent=parent) ifc.run("document.add_reference", information=information) @@ -66,7 +66,7 @@ def add_information(ifc: tool.Ifc, document: tool.Document, parent=None) -> ifco def add_reference(ifc: tool.Ifc, document: tool.Document) -> None: - parent = document.get_selected_document_information(ifc) + parent = document.get_selected_document_information() if parent: reference = ifc.run("document.add_reference", information=parent) diff --git a/src/bonsai/bonsai/core/tool.py b/src/bonsai/bonsai/core/tool.py index 4061914bee..36afd7d66c 100644 --- a/src/bonsai/bonsai/core/tool.py +++ b/src/bonsai/bonsai/core/tool.py @@ -289,6 +289,7 @@ class Debug: class Document: def clear_document_tree(cls): pass def disable_editing_document(cls): pass + def disable_object_editing_ui(cls): pass def disable_editing_ui(cls): pass def enable_editing_ui(cls): pass def export_document_attributes(cls): pass @@ -296,6 +297,19 @@ class Document: def import_project_documents(cls): pass def is_document_information(cls, document): pass def set_active_document(cls, document): pass + def clear_active_document(cls): pass + def clear_document_attributes(cls): pass + def expand_document(cls, document): pass + def get_default_parent_for_information(cls): pass + def get_selected_document_information(cls): pass + def get_document_information_id(cls, document): pass + def set_document_information_id(cls, document, value): pass + def get_external_reference_id(cls, reference): pass + def set_external_reference_id(cls, reference, value): pass + def get_document_references(cls, document): pass + def refresh_document_data(cls): pass + def load_document_objects_into_props(cls, document_id): pass + def update_document_objects(cls, document_id): pass @interface diff --git a/src/bonsai/bonsai/tool/document.py b/src/bonsai/bonsai/tool/document.py index f8f45fffcf..bf8e4d17b8 100644 --- a/src/bonsai/bonsai/tool/document.py +++ b/src/bonsai/bonsai/tool/document.py @@ -173,14 +173,18 @@ class Document(bonsai.core.tool.Document): new.tree_depth = depth new.name = document.Name or "" - new.identification = cls.get_document_information_id(document) if new.document_type == "INFORMATION" else cls.get_external_reference_id(document) + new.identification = ( + cls.get_document_information_id(document) + if new.document_type == "INFORMATION" + else cls.get_external_reference_id(document) + ) new.identification = new.identification or "" new.description = document.Description or "" new.location = document.Location or "" - + if new.document_type == "INFORMATION": new.name = document.Name or "Unnamed" - + elif new.document_type == "REFERENCE": file = document.file if file.schema == "IFC2X3": @@ -200,12 +204,12 @@ class Document(bonsai.core.tool.Document): info_children = natsorted( [d for d in children if d.is_a("IfcDocumentInformation")], - key=lambda doc: (cls.get_document_information_id(doc) or "", doc.Name or "") + key=lambda doc: (cls.get_document_information_id(doc) or "", doc.Name or ""), ) ref_children = natsorted( [d for d in children if not d.is_a("IfcDocumentInformation")], - key=lambda doc: (cls.get_external_reference_id(doc) or "", doc.Description or doc.Name or "") + key=lambda doc: (cls.get_external_reference_id(doc) or "", doc.Description or doc.Name or ""), ) for child in info_children + ref_children: @@ -272,16 +276,18 @@ class Document(bonsai.core.tool.Document): props.json_string = json.dumps(expanded_docs) @classmethod - def get_default_parent_for_information(cls, ifc) -> Union[ifcopenshell.entity_instance, None]: - projects = ifc.get().by_type("IfcProject") + def get_default_parent_for_information(cls) -> Union[ifcopenshell.entity_instance, None]: + file = tool.Ifc.get() + projects = file.by_type("IfcProject") return projects[0] if projects else None @classmethod - def get_selected_document_information(cls, ifc) -> Union[ifcopenshell.entity_instance, None]: + def get_selected_document_information(cls) -> Union[ifcopenshell.entity_instance, None]: props = cls.get_document_props() if props.active_document and props.active_document.document_type == "INFORMATION": - return ifc.get().by_id(props.active_document.ifc_definition_id) + file = tool.Ifc.get() + return file.by_id(props.active_document.ifc_definition_id) return None @classmethod diff --git a/src/bonsai/test/core/test_document.py b/src/bonsai/test/core/test_document.py index ab3563551d..5baf3e709f 100644 --- a/src/bonsai/test/core/test_document.py +++ b/src/bonsai/test/core/test_document.py @@ -16,7 +16,6 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . - import bonsai.core.document as subject from test.core.bootstrap import ifc, document @@ -29,14 +28,6 @@ class TestLoadProjectDocuments: subject.load_project_documents(document) -class TestLoadDocument: - def test_run(self, document): - document.clear_document_tree().should_be_called() - document.disable_editing_document().should_be_called() - document.add_breadcrumb("document").should_be_called() - subject.load_document(document, document="document") - - class TestDisableDocumentEditingUi: def test_run(self, document): document.disable_editing_ui().should_be_called() @@ -44,38 +35,71 @@ class TestDisableDocumentEditingUi: subject.disable_document_editing_ui(document) +class TestDisableObjectDocumentEditingUi: + def test_run(self, document): + document.disable_object_editing_ui().should_be_called() + subject.disable_object_document_editing_ui(document) + + class TestEnableEditingDocument: def test_run(self, document): - document.import_document_attributes("document").should_be_called() document.set_active_document("document").should_be_called() - subject.enable_editing_document(document, document="document") + document.import_document_attributes("document").should_be_called() + subject.enable_editing_document(document, ifc_document="document") class TestDisableEditingDocument: def test_run(self, document): - document.disable_editing_document().should_be_called() + document.clear_active_document().should_be_called() + document.clear_document_attributes().should_be_called() subject.disable_editing_document(document) class TestAddInformation: def test_add_and_reload_tree_at_project_root(self, ifc, document): document.clear_document_tree().should_be_called() - ifc.run("document.add_information", parent=None).should_be_called().will_return("information") + document.get_default_parent_for_information().should_be_called().will_return("default_parent") + ifc.run("document.add_information", parent="default_parent").should_be_called().will_return("information") ifc.run("document.add_reference", information="information").should_be_called() + document.is_document_information("default_parent").should_be_called().will_return(True) + document.expand_document("default_parent").should_be_called() document.import_project_documents().should_be_called() + subject.add_information(ifc, document) def test_add_and_reload_tree_at_current_parent(self, ifc, document): document.clear_document_tree().should_be_called() ifc.run("document.add_information", parent="parent").should_be_called().will_return("information") ifc.run("document.add_reference", information="information").should_be_called() - subject.add_information(ifc, document) + document.is_document_information("parent").should_be_called().will_return(True) + document.expand_document("parent").should_be_called() + document.import_project_documents().should_be_called() + + subject.add_information(ifc, document, parent="parent") + + def test_add_without_expanding_if_parent_is_not_information(self, ifc, document): + document.clear_document_tree().should_be_called() + ifc.run("document.add_information", parent="parent").should_be_called().will_return("information") + ifc.run("document.add_reference", information="information").should_be_called() + document.is_document_information("parent").should_be_called().will_return(False) + document.import_project_documents().should_be_called() + + subject.add_information(ifc, document, parent="parent") class TestAddReference: - def test_run(self, ifc, document): + def test_run_with_selected_parent(self, ifc, document): + document.get_selected_document_information().should_be_called().will_return("parent") ifc.run("document.add_reference", information="parent").should_be_called() - document.clear_document_tree().should_be_called() + document.expand_document("parent").should_be_called() + document.import_project_documents().should_be_called() + + subject.add_reference(ifc, document) + + def test_run_without_selected_parent(self, ifc, document): + document.get_selected_document_information().should_be_called().will_return(None) + document.import_project_documents().should_be_called() + subject.add_reference(ifc, document) @@ -87,7 +111,7 @@ class TestEditDocument: document.disable_editing_document().should_be_called() document.clear_document_tree().should_be_called() document.import_project_documents().should_be_called() - subject.edit_document(ifc, document, document="document") + subject.edit_document(ifc, document, ifc_document="document") def test_edit_reference(self, ifc, document): document.export_document_attributes().should_be_called().will_return("attributes") @@ -95,7 +119,8 @@ class TestEditDocument: ifc.run("document.edit_reference", reference="document", attributes="attributes").should_be_called() document.disable_editing_document().should_be_called() document.clear_document_tree().should_be_called() - subject.edit_document(ifc, document, document="document") + document.import_project_documents().should_be_called() + subject.edit_document(ifc, document, ifc_document="document") class TestRemoveDocument: @@ -104,22 +129,23 @@ class TestRemoveDocument: document.is_document_information("document").should_be_called().will_return(True) ifc.run("document.remove_information", information="document").should_be_called() document.import_project_documents().should_be_called() - subject.remove_document(ifc, document, document="document") + subject.remove_document(ifc, document, ifc_document="document") def test_remove_reference(self, ifc, document): document.clear_document_tree().should_be_called() document.is_document_information("document").should_be_called().will_return(False) ifc.run("document.remove_reference", reference="document").should_be_called() - subject.remove_document(ifc, document, document="document") + document.import_project_documents().should_be_called() + subject.remove_document(ifc, document, ifc_document="document") class TestAssignDocument: def test_run(self, ifc): ifc.run("document.assign_document", products=["product"], document="document").should_be_called() - subject.assign_document(ifc, product="product", document="document") + subject.assign_document(ifc, product="product", ifc_document="document") class TestUnassignDocument: def test_run(self, ifc): ifc.run("document.unassign_document", products=["product"], document="document").should_be_called() - subject.unassign_document(ifc, product="product", document="document") + subject.unassign_document(ifc, product="product", ifc_document="document") diff --git a/src/bonsai/test/tool/test_document.py b/src/bonsai/test/tool/test_document.py index 628f040923..64ec2791b0 100644 --- a/src/bonsai/test/tool/test_document.py +++ b/src/bonsai/test/tool/test_document.py @@ -148,24 +148,23 @@ class TestImportProjectDocumentsExpanded(NewFile): document = ifcopenshell.api.document.add_information(ifc) reference = ifcopenshell.api.document.add_reference(ifc, information=document) - props = tool.Document.get_document_props() expanded_docs = [document.id()] # Mark document as expanded props.json_string = json.dumps(expanded_docs) - + subject.import_project_documents() props = tool.Document.get_document_props() # Should have project root + document + reference = 3 total assert len(props.documents) == 3 - + assert props.documents[0].ifc_definition_id == -project.id() assert props.documents[0].document_type == "PROJECT" - + doc_info = next((d for d in props.documents if d.ifc_definition_id == document.id()), None) assert doc_info is not None assert doc_info.document_type == "INFORMATION" - + doc_ref = next((d for d in props.documents if d.ifc_definition_id == reference.id()), None) assert doc_ref is not None assert doc_ref.location == "" @@ -180,24 +179,23 @@ class TestImportProjectDocumentsCollapsed(NewFile): project = ifc.createIfcProject() document = ifcopenshell.api.document.add_information(ifc) reference = ifcopenshell.api.document.add_reference(ifc, information=document) - props = tool.Document.get_document_props() props.json_string = json.dumps([]) # Empty expanded list - + subject.import_project_documents() props = tool.Document.get_document_props() # Should have project root + document = 2 total (reference not imported because parent is collapsed) assert len(props.documents) == 2 - + assert props.documents[0].ifc_definition_id == -project.id() assert props.documents[0].document_type == "PROJECT" - + doc_info = next((d for d in props.documents if d.ifc_definition_id == document.id()), None) assert doc_info is not None assert doc_info.document_type == "INFORMATION" - + doc_ref = next((d for d in props.documents if d.ifc_definition_id == reference.id()), None) assert doc_ref is None