From b6cfd165c268eb67f1cf22691d6f421c1b2cde7e Mon Sep 17 00:00:00 2001 From: falken10 Date: Mon, 7 Jul 2025 23:58:02 +0200 Subject: [PATCH] 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