Implemented tree like structure for documents

This commit is contained in:
falken10
2025-06-20 10:14:02 +02:00
committed by falken10vdl
parent 0c3153ab7f
commit 5ea66730d4
11 changed files with 873 additions and 301 deletions
@@ -18,32 +18,47 @@
import bpy import bpy
from . import ui, prop, operator from . import ui, prop, operator
from bpy.types import VIEW3D_MT_object_context_menu
classes = ( classes = (
operator.AddDocumentReference, operator.AddDocumentReference,
operator.AddInformation, operator.AddInformation,
operator.AssignDocument, operator.AssignDocument,
operator.DisableDocumentEditingUI, operator.DisableDocumentEditingUI,
operator.DisableObjectDocumentEditingUI,
operator.DisableEditingDocument, operator.DisableEditingDocument,
operator.EditDocument, operator.EditDocument,
operator.EnableEditingDocument, operator.EnableEditingDocument,
operator.LoadDocument, operator.LoadDocument,
operator.LoadParentDocument, operator.LoadObjectDocuments,
operator.LoadProjectDocuments, operator.LoadProjectDocuments,
operator.RemoveDocument, operator.RemoveDocument,
operator.SelectDocumentObjects, operator.SelectDocumentObjects,
operator.ToggleDocument,
operator.UnassignDocument, operator.UnassignDocument,
operator.UpdateAssignedDocuments,
operator.OpenIFCDocument,
prop.Document, prop.Document,
prop.DocumentObject,
prop.AssignedDocument,
prop.ExpandedDocuments,
prop.BIMDocumentProperties, prop.BIMDocumentProperties,
ui.BIM_PT_documents, ui.BIM_PT_documents,
ui.BIM_PT_object_documents, ui.BIM_PT_object_documents,
ui.BIM_UL_documents, ui.BIM_UL_documents,
ui.BIM_UL_document_objects,
ui.BIM_UL_assigned_documents,
ui.BIM_MT_object_documents_context_menu,
) )
def register(): def register():
bpy.types.Scene.BIMDocumentProperties = bpy.props.PointerProperty(type=prop.BIMDocumentProperties) 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(): def unregister():
del bpy.types.Scene.BIMDocumentProperties del bpy.types.Scene.BIMDocumentProperties
del bpy.types.Scene.ExpandedDocuments
VIEW3D_MT_object_context_menu.remove(ui.add_object_documents_context_menu)
+90 -33
View File
@@ -35,30 +35,69 @@ class DocumentData:
@classmethod @classmethod
def load(cls): def load(cls):
cls.data = { cls.data = {
"total_information": cls.total_information(), "total_document_informations": cls.total_document_informations(),
"parent_document": cls.parent_document(), "total_document_references": cls.total_document_references(),
"total_referenced_objects": cls.total_referenced_objects(),
"document_objects": cls.document_objects(),
} }
cls.is_loaded = True cls.is_loaded = True
@classmethod @classmethod
def total_information(cls): def total_document_informations(cls):
return len( file = tool.Ifc.get()
[ info_count = len(file.by_type("IfcDocumentInformation"))
rel return info_count
for rel in tool.Ifc.get().by_type("IfcProject")[0].HasAssociations or []
if rel.is_a("IfcRelAssociatesDocument") and rel.RelatingDocument.is_a("IfcDocumentInformation")
]
)
@classmethod @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() props = tool.Document.get_document_props()
if len(props.breadcrumbs): props.document_objects.clear()
parent = tool.Ifc.get().by_id(int(props.breadcrumbs[-1].name))
if tool.Ifc.get_schema() == "IFC2X3": if "document_objects" not in cls.data or document_id not in cls.data["document_objects"]:
return str(parent.DocumentId) return
return str(parent.Identification)
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: class ObjectDocumentData:
@@ -80,31 +119,47 @@ class ObjectDocumentData:
return results return results
for rel in getattr(element, "HasAssociations", []): for rel in getattr(element, "HasAssociations", []):
if rel.is_a("IfcRelAssociatesDocument"): 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 continue
name = rel.RelatingDocument.Name name = rel.RelatingDocument.Name
if tool.Ifc.get_schema() == "IFC2X3": location = None
if not name and rel.RelatingDocument.ReferenceToDocument: identification = None
name = rel.RelatingDocument.ReferenceToDocument[0].Name description = None
identification = rel.RelatingDocument.ItemReference if is_information:
if not identification and rel.RelatingDocument.ReferenceToDocument: if tool.Ifc.get_schema() == "IFC2X3":
identification = rel.RelatingDocument.ReferenceToDocument[0].DocumentId identification = rel.RelatingDocument.DocumentId
else:
identification = rel.RelatingDocument.Identification
location = getattr(rel.RelatingDocument, "Location", None)
location = rel.RelatingDocument.Location
else: else:
if not name and rel.RelatingDocument.ReferencedDocument: description = rel.RelatingDocument.Description
name = rel.RelatingDocument.ReferencedDocument.Name if tool.Ifc.get_schema() == "IFC2X3":
if not name and rel.RelatingDocument.ReferenceToDocument:
name = rel.RelatingDocument.ReferenceToDocument[0].Name
identification = rel.RelatingDocument.Identification identification = rel.RelatingDocument.ItemReference
if not identification and rel.RelatingDocument.ReferencedDocument: if not identification and rel.RelatingDocument.ReferenceToDocument:
identification = rel.RelatingDocument.ReferencedDocument.Identification 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 identification = rel.RelatingDocument.Identification
if location is None and rel.RelatingDocument.ReferencedDocument: if not identification and rel.RelatingDocument.ReferencedDocument:
location = rel.RelatingDocument.ReferencedDocument.Location 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 location:
if not "://" in location: if not "://" in location:
@@ -118,6 +173,8 @@ class ObjectDocumentData:
"identification": identification, "identification": identification,
"name": name, "name": name,
"location": location, "location": location,
"is_information": is_information,
"description": description,
} }
) )
return results return results
+277 -15
View File
@@ -24,6 +24,24 @@ import ifcopenshell.util.element
import bonsai.bim.handler import bonsai.bim.handler
import bonsai.tool as tool import bonsai.tool as tool
import bonsai.core.document as core 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): class LoadProjectDocuments(bpy.types.Operator):
@@ -33,7 +51,7 @@ class LoadProjectDocuments(bpy.types.Operator):
def execute(self, context): def execute(self, context):
core.load_project_documents(tool.Document) core.load_project_documents(tool.Document)
bonsai.bim.handler.refresh_ui_data() # Update breadcrumbs data. update_document_objects()
return {"FINISHED"} return {"FINISHED"}
@@ -45,18 +63,8 @@ class LoadDocument(bpy.types.Operator):
def execute(self, context): def execute(self, context):
core.load_document(tool.Document, document=tool.Ifc.get().by_id(self.document)) core.load_document(tool.Document, document=tool.Ifc.get().by_id(self.document))
bonsai.bim.handler.refresh_ui_data() # Update breadcrumbs data. bonsai.bim.handler.refresh_ui_data() # Is this needed?
return {"FINISHED"} update_document_objects()
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.
return {"FINISHED"} return {"FINISHED"}
@@ -70,6 +78,17 @@ class DisableDocumentEditingUI(bpy.types.Operator):
return {"FINISHED"} 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): class EnableEditingDocument(bpy.types.Operator):
bl_idname = "bim.enable_editing_document" bl_idname = "bim.enable_editing_document"
bl_label = "Enable Editing Document" bl_label = "Enable Editing Document"
@@ -77,6 +96,8 @@ class EnableEditingDocument(bpy.types.Operator):
document: bpy.props.IntProperty() document: bpy.props.IntProperty()
def execute(self, context): 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)) core.enable_editing_document(tool.Document, document=tool.Ifc.get().by_id(self.document))
return {"FINISHED"} return {"FINISHED"}
@@ -87,6 +108,8 @@ class DisableEditingDocument(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
def execute(self, context): def execute(self, context):
props = tool.Document.get_document_props()
props.is_document_editing = False
core.disable_editing_document(tool.Document) core.disable_editing_document(tool.Document)
return {"FINISHED"} return {"FINISHED"}
@@ -97,7 +120,41 @@ class AddInformation(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
def _execute(self, context): 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): class AddDocumentReference(bpy.types.Operator, tool.Ifc.Operator):
@@ -106,7 +163,33 @@ class AddDocumentReference(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
def _execute(self, context): 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) 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): class EditDocument(bpy.types.Operator, tool.Ifc.Operator):
@@ -116,7 +199,16 @@ class EditDocument(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context): def _execute(self, context):
props = tool.Document.get_document_props() 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): 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)) 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): class AssignDocument(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.assign_document" bl_idname = "bim.assign_document"
bl_label = "Assign Document" bl_label = "Assign Document"
@@ -145,6 +270,11 @@ class AssignDocument(bpy.types.Operator, tool.Ifc.Operator):
if element: if element:
core.assign_document(tool.Ifc, product=element, document=document) 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): class UnassignDocument(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.unassign_document" bl_idname = "bim.unassign_document"
@@ -160,6 +290,19 @@ class UnassignDocument(bpy.types.Operator, tool.Ifc.Operator):
element = tool.Ifc.get_entity(obj) element = tool.Ifc.get_entity(obj)
if element: if element:
core.unassign_document(tool.Ifc, product=element, document=document) 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): class SelectDocumentObjects(bpy.types.Operator):
@@ -182,3 +325,122 @@ class SelectDocumentObjects(bpy.types.Operator):
i += 1 i += 1
self.report({"INFO"}, f"{i} objects selected.") self.report({"INFO"}, f"{i} objects selected.")
return {"FINISHED"} 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"}
+67 -9
View File
@@ -30,6 +30,7 @@ from bpy.props import (
FloatVectorProperty, FloatVectorProperty,
CollectionProperty, CollectionProperty,
) )
from bonsai.bim.module.document.data import DocumentData
from typing import TYPE_CHECKING, Union 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) 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): class Document(PropertyGroup):
name: StringProperty(name="Name", update=update_document_name) name: StringProperty(name="Name")
identification: StringProperty(name="Identification", update=update_document_identification) identification: StringProperty(name="Identification")
is_information: BoolProperty( description: StringProperty(name="Description")
name="Is Information", is_information: BoolProperty(name="Is Information")
description="Whether element is IfcDocumentInformation, otherwise it's IfcDocumentReference.", 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") ifc_definition_id: IntProperty(name="IFC Definition ID")
if TYPE_CHECKING: 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 identification: str
is_information: bool is_information: bool
ifc_definition_id: int ifc_definition_id: int
location: str
class BIMDocumentProperties(PropertyGroup): class BIMDocumentProperties(PropertyGroup):
document_attributes: CollectionProperty(name="Document Attributes", type=Attribute) document_attributes: CollectionProperty(name="Document Attributes", type=Attribute)
active_document_id: IntProperty(name="Active Document Id") active_document_id: IntProperty(name="Active Document Id")
documents: CollectionProperty(name="Documents", type=Document) documents: CollectionProperty(name="Documents", type=Document)
breadcrumbs: CollectionProperty(name="Breadcrumbs", type=StrProperty) active_document_index: IntProperty(name="Active Document Index", update=update_active_document)
active_document_index: IntProperty(name="Active Document Index")
is_editing: BoolProperty(name="Is Editing", default=False) 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: if TYPE_CHECKING:
document_attributes: bpy.types.bpy_prop_collection_idprop[Attribute] document_attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
active_document_id: int active_document_id: int
documents: bpy.types.bpy_prop_collection_idprop[Document] documents: bpy.types.bpy_prop_collection_idprop[Document]
breadcrumbs: bpy.types.bpy_prop_collection_idprop[StrProperty]
active_document_index: int active_document_index: int
is_editing: bool 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 @property
def active_document(self) -> Union[Document, None]: def active_document(self) -> Union[Document, None]:
+238 -60
View File
@@ -16,6 +16,7 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>. # along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import bpy
import bonsai.tool as tool import bonsai.tool as tool
from bpy.types import Panel, UIList from bpy.types import Panel, UIList
from bonsai.bim.helper import draw_attributes from bonsai.bim.helper import draw_attributes
@@ -42,43 +43,74 @@ class BIM_PT_documents(Panel):
self.props = tool.Document.get_document_props() self.props = tool.Document.get_document_props()
row = self.layout.row(align=True) 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: 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: 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: if not self.props.is_editing:
return return
row = self.layout.row(align=True) row = self.layout.row(align=True)
if self.props.breadcrumbs: row.alignment = "RIGHT"
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")
active_document = self.props.active_document if self.props.is_document_editing:
if self.props.active_document_id:
row.operator("bim.edit_document", text="", icon="CHECKMARK") row.operator("bim.edit_document", text="", icon="CHECKMARK")
row.operator("bim.disable_editing_document", text="", icon="CANCEL") row.operator("bim.disable_editing_document", text="", icon="CANCEL")
elif active_document: else:
ifc_definition_id = active_document.ifc_definition_id row.operator("bim.add_information", text="", icon="ADD")
row.operator("bim.select_document_objects", text="", icon="RESTRICT_SELECT_OFF").document = (
ifc_definition_id if self.props.documents and self.props.active_document_index < len(self.props.documents):
) active_doc = self.props.documents[self.props.active_document_index]
row.operator("bim.assign_document", text="", icon="BRUSH_DATA").document = ifc_definition_id if active_doc.is_information and active_doc.ifc_definition_id != -1:
row.operator("bim.enable_editing_document", text="", icon="GREASEPENCIL").document = ifc_definition_id row.operator("bim.add_document_reference", text="", icon="FILE_HIDDEN")
row.operator("bim.remove_document", text="", icon="X").document = ifc_definition_id
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") self.layout.template_list("BIM_UL_documents", "", self.props, "documents", self.props, "active_document_index")
if self.props.active_document_id: if self.props.is_document_editing:
draw_attributes(self.props.document_attributes, self.layout) 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): class BIM_PT_object_documents(Panel):
@@ -110,57 +142,203 @@ class BIM_PT_object_documents(Panel):
self.props = tool.Document.get_document_props() self.props = tool.Document.get_document_props()
self.file = tool.Ifc.get() self.file = tool.Ifc.get()
self.draw_add_ui() doc_count = len(ObjectDocumentData.data["documents"])
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
row = self.layout.row(align=True) row = self.layout.row(align=True)
if self.props.breadcrumbs: row.label(text="{} Documents Assigned".format(doc_count), icon="FILE")
row.operator("bim.load_parent_document", text="", icon="FRAME_PREV")
row.label(text=DocumentData.data["parent_document"]) if self.props.is_object_editing:
row.operator("bim.disable_object_document_editing_ui", text="", icon="CANCEL")
else: 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" row.alignment = "RIGHT"
if self.props.documents and self.props.active_document_index < len(self.props.documents): if self.props.documents and self.props.active_document_index < len(self.props.documents):
document = self.props.documents[self.props.active_document_index] 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")
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): 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): def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
if item: if item:
row = layout.row(align=True) row = layout.row(align=True)
if item.is_information: 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") row.label(text="", icon="FILE")
else: else:
row.label(text="", icon="BLANK1")
row.label(text="", icon="FILE_HIDDEN") row.label(text="", icon="FILE_HIDDEN")
split1 = row.split(factor=0.1) split1 = row.split(factor=0.2)
# split1.label(text=item.identification) split1.label(text=item.identification or "")
split1.prop(item, "identification", text="", emboss=False)
split2 = split1.split(factor=0.9) split2 = split1.split(factor=1.0)
split2.prop(item, "name", text="", emboss=False) 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}")
+58 -44
View File
@@ -21,6 +21,7 @@ from typing import TYPE_CHECKING, Optional
if TYPE_CHECKING: if TYPE_CHECKING:
import bpy import bpy
import json
import ifcopenshell import ifcopenshell
import bonsai.tool as tool import bonsai.tool as tool
@@ -28,28 +29,22 @@ if TYPE_CHECKING:
def load_project_documents(document: tool.Document) -> None: def load_project_documents(document: tool.Document) -> None:
document.clear_document_tree() document.clear_document_tree()
document.import_project_documents() document.import_project_documents()
document.clear_breadcrumbs()
document.enable_editing_ui() document.enable_editing_ui()
def load_document(document_tool: tool.Document, document: ifcopenshell.entity_instance) -> None: def load_document(document_tool: tool.Document, document: ifcopenshell.entity_instance) -> None:
document_tool.clear_document_tree() document_tool.clear_document_tree()
document_tool.import_subdocuments(document) try:
document_tool.import_references(document) 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.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: 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: 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.import_document_attributes(document)
document_tool.set_active_document(document)
def disable_editing_document(document: tool.Document) -> None: 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: def add_information(ifc: tool.Ifc, document_tool: tool.Document, parent=None) -> ifcopenshell.entity_instance:
document.clear_document_tree() document_tool.clear_document_tree()
parent = document.get_active_breadcrumb()
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) information = ifc.run("document.add_information", parent=parent)
ifc.run("document.add_reference", information=information) ifc.run("document.add_reference", information=information)
if parent: if parent and parent.is_a("IfcDocumentInformation"):
document.import_subdocuments(parent) try:
document.import_references(parent) expanded_docs = json.loads(bpy.context.scene.ExpandedDocuments.json_string)
else: except (AttributeError, json.JSONDecodeError):
document.import_project_documents() 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: def add_reference(ifc: tool.Ifc, document: tool.Document) -> None:
parent = document.get_active_breadcrumb() props = document.get_document_props()
assert parent parent = None
ifc.run("document.add_reference", information=parent)
document.clear_document_tree() if props.documents and props.active_document_index < len(props.documents):
document.import_subdocuments(parent) selected_document = props.documents[props.active_document_index]
document.import_references(parent) 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: 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) ifc.run("document.edit_reference", reference=document, attributes=attributes)
document_tool.disable_editing_document() document_tool.disable_editing_document()
document_tool.clear_document_tree() document_tool.clear_document_tree()
parent = document_tool.get_active_breadcrumb() document_tool.import_project_documents()
if parent:
document_tool.import_subdocuments(parent)
document_tool.import_references(parent)
else:
document_tool.import_project_documents()
def remove_document(ifc: tool.Ifc, document_tool: tool.Document, document: ifcopenshell.entity_instance) -> None: 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) ifc.run("document.remove_information", information=document)
else: else:
ifc.run("document.remove_reference", reference=document) ifc.run("document.remove_reference", reference=document)
parent = document_tool.get_active_breadcrumb() document_tool.import_project_documents()
if parent:
document_tool.import_subdocuments(parent)
document_tool.import_references(parent)
else:
document_tool.import_project_documents()
def assign_document( def assign_document(
-6
View File
@@ -287,20 +287,14 @@ class Debug:
@interface @interface
class Document: class Document:
def add_breadcrumb(cls, document): pass
def clear_breadcrumbs(cls): pass
def clear_document_tree(cls): pass def clear_document_tree(cls): pass
def disable_editing_document(cls): pass def disable_editing_document(cls): pass
def disable_editing_ui(cls): pass def disable_editing_ui(cls): pass
def enable_editing_ui(cls): pass def enable_editing_ui(cls): pass
def export_document_attributes(cls): pass def export_document_attributes(cls): pass
def get_active_breadcrumb(cls): pass
def import_document_attributes(cls, document): pass def import_document_attributes(cls, document): pass
def import_project_documents(cls): 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 is_document_information(cls, document): pass
def remove_latest_breadcrumb(cls): pass
def set_active_document(cls, document): pass def set_active_document(cls, document): pass
+127 -54
View File
@@ -18,6 +18,7 @@
from __future__ import annotations from __future__ import annotations
import bpy import bpy
import json
import ifcopenshell.util.system import ifcopenshell.util.system
import bonsai.bim.helper import bonsai.bim.helper
import bonsai.core.tool import bonsai.core.tool
@@ -33,17 +34,6 @@ class Document(bonsai.core.tool.Document):
def get_document_props(cls) -> BIMDocumentProperties: def get_document_props(cls) -> BIMDocumentProperties:
return bpy.context.scene.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 @classmethod
def clear_document_tree(cls) -> None: def clear_document_tree(cls) -> None:
props = cls.get_document_props() props = cls.get_document_props()
@@ -69,18 +59,15 @@ class Document(bonsai.core.tool.Document):
props = cls.get_document_props() props = cls.get_document_props()
return bonsai.bim.helper.export_attributes(props.document_attributes) 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 @classmethod
def import_document_attributes(cls, document: ifcopenshell.entity_instance) -> None: def import_document_attributes(cls, document: ifcopenshell.entity_instance) -> None:
props = cls.get_document_props() props = cls.get_document_props()
props.document_attributes.clear() 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": if attr_name != "Name":
return None # Proceed normally return None # Proceed normally
@@ -100,52 +87,138 @@ class Document(bonsai.core.tool.Document):
def import_project_documents(cls) -> None: def import_project_documents(cls) -> None:
props = cls.get_document_props() props = cls.get_document_props()
props.documents.clear() 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 []: for rel in project.HasAssociations or []:
if rel.is_a("IfcRelAssociatesDocument") and rel.RelatingDocument.is_a("IfcDocumentInformation"): if rel.is_a("IfcRelAssociatesDocument") and rel.RelatingDocument.is_a("IfcDocumentInformation"):
element = rel.RelatingDocument is_child = False
new = props.documents.add() for children in document_children.values():
new.ifc_definition_id = element.id() if rel.RelatingDocument in children:
new["name"] = element.Name or "Unnamed" is_child = True
new.is_information = True break
new["identification"] = cls.get_document_information_id(element)
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 @classmethod
def import_references(cls, document: ifcopenshell.entity_instance) -> None: def _process_document(cls, document, props, document_children, expanded_documents, depth):
props = cls.get_document_props() new = props.documents.add()
is_ifc2x3 = tool.Ifc.get_schema() == "IFC2X3" new.ifc_definition_id = document.id()
references = cls.get_document_references(document) new.is_information = document.is_a("IfcDocumentInformation")
for element in references: new.tree_depth = depth
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
@classmethod file = document.file
def import_subdocuments(cls, document: ifcopenshell.entity_instance) -> None: if new.is_information:
props = cls.get_document_props() new.name = document.Name or "Unnamed"
if document.IsPointer: new.identification = cls.get_document_information_id(document) or ""
for element in document.IsPointer[0].RelatedDocuments or []: new.location = document.Location or ""
new = props.documents.add() else:
new.ifc_definition_id = element.id() new.name = document.Name or ""
new["name"] = element.Name or "Unnamed" new.identification = cls.get_external_reference_id(document) or ""
new.is_information = True new.description = document.Description or ""
new["identification"] = cls.get_document_information_id(element) 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 @classmethod
def is_document_information(cls, document: ifcopenshell.entity_instance) -> bool: def is_document_information(cls, document: ifcopenshell.entity_instance) -> bool:
return document.is_a("IfcDocumentInformation") 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 @classmethod
def set_active_document(cls, document: ifcopenshell.entity_instance) -> None: def set_active_document(cls, document: ifcopenshell.entity_instance) -> None:
props = cls.get_document_props() props = cls.get_document_props()
@@ -14,15 +14,6 @@ Scenario: Load document
When I press "bim.load_document(document={information})" When I press "bim.load_document(document={information})"
Then nothing happens 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 Scenario: Disable document editing UI
Given an empty IFC project Given an empty IFC project
And I press "bim.load_project_documents" And I press "bim.load_project_documents"
-18
View File
@@ -25,7 +25,6 @@ class TestLoadProjectDocuments:
def test_run(self, document): def test_run(self, document):
document.clear_document_tree().should_be_called() document.clear_document_tree().should_be_called()
document.import_project_documents().should_be_called() document.import_project_documents().should_be_called()
document.clear_breadcrumbs().should_be_called()
document.enable_editing_ui().should_be_called() document.enable_editing_ui().should_be_called()
subject.load_project_documents(document) subject.load_project_documents(document)
@@ -33,8 +32,6 @@ class TestLoadProjectDocuments:
class TestLoadDocument: class TestLoadDocument:
def test_run(self, document): def test_run(self, document):
document.clear_document_tree().should_be_called() 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.disable_editing_document().should_be_called()
document.add_breadcrumb("document").should_be_called() document.add_breadcrumb("document").should_be_called()
subject.load_document(document, document="document") subject.load_document(document, document="document")
@@ -63,7 +60,6 @@ class TestDisableEditingDocument:
class TestAddInformation: class TestAddInformation:
def test_add_and_reload_tree_at_project_root(self, ifc, document): def test_add_and_reload_tree_at_project_root(self, ifc, document):
document.clear_document_tree().should_be_called() 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_information", parent=None).should_be_called().will_return("information")
ifc.run("document.add_reference", information="information").should_be_called() ifc.run("document.add_reference", information="information").should_be_called()
document.import_project_documents().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): def test_add_and_reload_tree_at_current_parent(self, ifc, document):
document.clear_document_tree().should_be_called() 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_information", parent="parent").should_be_called().will_return("information")
ifc.run("document.add_reference", information="information").should_be_called() 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) subject.add_information(ifc, document)
class TestAddReference: class TestAddReference:
def test_run(self, ifc, document): 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() ifc.run("document.add_reference", information="parent").should_be_called()
document.clear_document_tree().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) subject.add_reference(ifc, document)
@@ -96,7 +86,6 @@ class TestEditDocument:
ifc.run("document.edit_information", information="document", attributes="attributes").should_be_called() ifc.run("document.edit_information", information="document", attributes="attributes").should_be_called()
document.disable_editing_document().should_be_called() document.disable_editing_document().should_be_called()
document.clear_document_tree().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() document.import_project_documents().should_be_called()
subject.edit_document(ifc, document, document="document") 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() ifc.run("document.edit_reference", reference="document", attributes="attributes").should_be_called()
document.disable_editing_document().should_be_called() document.disable_editing_document().should_be_called()
document.clear_document_tree().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") subject.edit_document(ifc, document, document="document")
@@ -117,7 +103,6 @@ class TestRemoveDocument:
document.clear_document_tree().should_be_called() document.clear_document_tree().should_be_called()
document.is_document_information("document").should_be_called().will_return(True) document.is_document_information("document").should_be_called().will_return(True)
ifc.run("document.remove_information", information="document").should_be_called() 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() document.import_project_documents().should_be_called()
subject.remove_document(ifc, document, document="document") subject.remove_document(ifc, document, document="document")
@@ -125,9 +110,6 @@ class TestRemoveDocument:
document.clear_document_tree().should_be_called() document.clear_document_tree().should_be_called()
document.is_document_information("document").should_be_called().will_return(False) document.is_document_information("document").should_be_called().will_return(False)
ifc.run("document.remove_reference", reference="document").should_be_called() 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") subject.remove_document(ifc, document, document="document")
-52
View File
@@ -31,24 +31,6 @@ class TestImplementsTool(NewFile):
assert isinstance(subject(), bonsai.core.tool.Document) 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): class TestClearDocumentTree(NewFile):
def test_run(self): def test_run(self):
props = tool.Document.get_document_props() 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): class TestImportDocumentAttributes(NewFile):
def test_importing_information(self): def test_importing_information(self):
ifc = ifcopenshell.file() ifc = ifcopenshell.file()
@@ -197,22 +170,6 @@ class TestImportReferences(NewFile):
assert props.documents[0].is_information is False 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): class TestIsDocumentInformation(NewFile):
def test_run(self): def test_run(self):
ifc = ifcopenshell.file() ifc = ifcopenshell.file()
@@ -222,15 +179,6 @@ class TestIsDocumentInformation(NewFile):
assert subject.is_document_information(reference) is False 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): class TestSetActiveDocument(NewFile):
def test_run(self): def test_run(self):
ifc = ifcopenshell.file() ifc = ifcopenshell.file()