updates based on developers feedback

This commit is contained in:
falken10
2025-07-07 23:58:02 +02:00
committed by falken10vdl
parent aa8c146f9d
commit b6cfd165c2
7 changed files with 218 additions and 255 deletions
@@ -28,14 +28,12 @@ classes = (
operator.DisableEditingDocument, operator.DisableEditingDocument,
operator.EditDocument, operator.EditDocument,
operator.EnableEditingDocument, operator.EnableEditingDocument,
operator.LoadDocument,
operator.LoadObjectDocuments, operator.LoadObjectDocuments,
operator.LoadProjectDocuments, operator.LoadProjectDocuments,
operator.RemoveDocument, operator.RemoveDocument,
operator.SelectDocumentObjects, operator.SelectDocumentObjects,
operator.ToggleDocument, operator.ToggleDocument,
operator.UnassignDocument, operator.UnassignDocument,
operator.UpdateAssignedDocuments,
operator.OpenIFCDocument, operator.OpenIFCDocument,
prop.Document, prop.Document,
prop.DocumentObject, prop.DocumentObject,
+20 -9
View File
@@ -21,7 +21,7 @@ import bpy
import ifcopenshell import ifcopenshell
import ifcopenshell.util.schema import ifcopenshell.util.schema
import bonsai.tool as tool import bonsai.tool as tool
from natsort import natsorted
def refresh(): def refresh():
DocumentData.is_loaded = False DocumentData.is_loaded = False
@@ -87,19 +87,21 @@ class DocumentData:
@classmethod @classmethod
def load_document_objects_into_props(cls, document_id): def load_document_objects_into_props(cls, document_id):
if not cls.is_loaded:
cls.load()
props = tool.Document.get_document_props() props = tool.Document.get_document_props()
props.document_objects.clear() 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 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: for obj_data in sorted_objects:
item = props.document_objects.add() item = props.document_objects.add()
item.name = obj_data["name"] item.name = obj_data["name"]
class ObjectDocumentData: class ObjectDocumentData:
data = {} data = {}
is_loaded = False is_loaded = False
@@ -111,6 +113,19 @@ class ObjectDocumentData:
} }
cls.is_loaded = True 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 @classmethod
def documents(cls): def documents(cls):
results = [] results = []
@@ -165,11 +180,7 @@ class ObjectDocumentData:
if location is None and referenced_document: if location is None and referenced_document:
location = referenced_document.Location location = referenced_document.Location
if location: location = cls.convert_to_file_uri(location) if location else None
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
results.append( results.append(
{ {
+47 -162
View File
@@ -18,29 +18,10 @@
import bpy import bpy
import json import json
import ifcopenshell.api
import ifcopenshell.util.attribute
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 from .data import DocumentData, ObjectDocumentData
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)
class LoadProjectDocuments(bpy.types.Operator): class LoadProjectDocuments(bpy.types.Operator):
bl_idname = "bim.load_project_documents" bl_idname = "bim.load_project_documents"
@@ -49,23 +30,8 @@ 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)
update_document_objects()
return {"FINISHED"} 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): class DisableDocumentEditingUI(bpy.types.Operator):
bl_idname = "bim.disable_document_editing_ui" bl_idname = "bim.disable_document_editing_ui"
bl_label = "Disable Document Editing UI" bl_label = "Disable Document Editing UI"
@@ -82,8 +48,7 @@ class DisableObjectDocumentEditingUI(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() core.disable_object_document_editing_ui(tool.Document)
props.is_object_editing = False
return {"FINISHED"} return {"FINISHED"}
@@ -94,8 +59,6 @@ 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"}
@@ -106,8 +69,6 @@ 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"}
@@ -123,15 +84,15 @@ class AddInformation(bpy.types.Operator, tool.Ifc.Operator):
if props.active_document: if props.active_document:
selected_document = props.active_document selected_document = props.active_document
if selected_document.ifc_definition_id == -1: if selected_document.document_type == "PROJECT":
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]
elif selected_document.is_information: elif selected_document.document_type == "INFORMATION":
parent = tool.Ifc.get().by_id(selected_document.ifc_definition_id) 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") self.report({"ERROR"}, "Cannot add an information element as a child of a reference element")
return {"CANCELLED"} return {"CANCELLED"}
else: 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) core.add_information(tool.Ifc, tool.Document, parent)
@@ -141,20 +102,13 @@ class AddInformation(bpy.types.Operator, tool.Ifc.Operator):
except (AttributeError, json.JSONDecodeError): except (AttributeError, json.JSONDecodeError):
pass 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 and parent.is_a("IfcDocumentInformation"):
if parent.id() not in expanded_docs: if parent.id() not in expanded_docs:
expanded_docs.append(parent.id()) expanded_docs.append(parent.id())
props.json_string = json.dumps(expanded_docs) props.json_string = json.dumps(expanded_docs)
bpy.ops.bim.load_project_documents() bpy.ops.bim.load_project_documents()
class AddDocumentReference(bpy.types.Operator, tool.Ifc.Operator): class AddDocumentReference(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_document_reference" bl_idname = "bim.add_document_reference"
bl_label = "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 selected_document = props.active_document
if not selected_document.is_information: if selected_document.document_type != "INFORMATION":
self.report({"ERROR"}, "Cannot add a reference to a reference element") self.report({"ERROR"}, "Cannot add a reference to a document that is not an information element")
return {"CANCELLED"} return {"CANCELLED"}
parent = tool.Ifc.get().by_id(selected_document.ifc_definition_id) 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)) core.edit_document(tool.Ifc, tool.Document, document=tool.Ifc.get().by_id(props.active_document_id))
props.active_document_id = 0 props.active_document_id = 0
props.is_document_editing = False props.is_document_editing = False
DocumentData.is_loaded = False tool.Document.update_assigned_documents()
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):
@@ -218,40 +167,6 @@ class RemoveDocument(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context): def _execute(self, context):
core.remove_document(tool.Ifc, tool.Document, document=tool.Ifc.get().by_id(self.document)) core.remove_document(tool.Ifc, tool.Document, 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"
@@ -268,10 +183,12 @@ 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)
tool.Document.update_document_objects(self.document)
ObjectDocumentData.is_loaded = False ObjectDocumentData.is_loaded = False
ObjectDocumentData.load() ObjectDocumentData.load()
bpy.ops.bim.update_assigned_documents() tool.Document.update_assigned_documents()
return {"FINISHED"}
class UnassignDocument(bpy.types.Operator, tool.Ifc.Operator): 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) document = tool.Ifc.get().by_id(self.document)
objs = [bpy.data.objects.get(self.obj)] if self.obj else tool.Blender.get_selected_objects() objs = [bpy.data.objects.get(self.obj)] if self.obj else tool.Blender.get_selected_objects()
for obj in objs: for obj in objs:
element = tool.Ifc.get_entity(obj) if obj:
if element: element = tool.Ifc.get_entity(obj)
core.unassign_document(tool.Ifc, product=element, document=document) if element:
core.unassign_document(tool.Ifc, product=element, document=document)
props = tool.Document.get_document_props() props = tool.Document.get_document_props()
active_document_id = None active_document_id = None
if props.active_document: if props.active_document:
active_document_id = props.active_document.ifc_definition_id active_document_id = props.active_document.ifc_definition_id
if active_document_id and active_document_id != self.document: 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: else:
update_document_objects(self.document) tool.Document.update_document_objects()
ObjectDocumentData.is_loaded = False ObjectDocumentData.is_loaded = False
ObjectDocumentData.load() ObjectDocumentData.load()
bpy.ops.bim.update_assigned_documents()
tool.Document.update_assigned_documents()
return {"FINISHED"}
class SelectDocumentObjects(bpy.types.Operator): class SelectDocumentObjects(bpy.types.Operator):
@@ -339,32 +261,10 @@ class LoadObjectDocuments(bpy.types.Operator):
props = tool.Document.get_document_props() props = tool.Document.get_document_props()
props.is_object_editing = True props.is_object_editing = True
bonsai.bim.handler.refresh_ui_data() tool.Document.update_assigned_documents()
self.update_assigned_documents(props)
return {"FINISHED"} 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): class OpenIFCDocument(bpy.types.Operator):
bl_idname = "bim.open_ifc_document" bl_idname = "bim.open_ifc_document"
@@ -375,47 +275,37 @@ class OpenIFCDocument(bpy.types.Operator):
uri: bpy.props.StringProperty(name="URI") uri: bpy.props.StringProperty(name="URI")
def execute(self, context): def execute(self, context):
import subprocess
if not self.uri: import os
self.report({"ERROR"}, "No URI provided") if not self.uri or not self.uri.lower().startswith("file://"):
self.report({"ERROR"}, "Only local file:// URIs are supported")
return {"CANCELLED"} return {"CANCELLED"}
file_path = self.uri filepath = self.uri[7:] # Remove file:// prefix
if file_path.startswith("file://"):
file_path = file_path[7:] if not os.path.exists(filepath):
elif file_path.startswith("file:"): self.report({"ERROR"}, f"File not found: {filepath}")
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"} return {"CANCELLED"}
try: try:
subprocess.Popen( blender_path = bpy.app.binary_path
[ args = [blender_path, "--python-expr", "import bpy; bpy.ops.bim.load_project(filepath='{}')".format(filepath)]
"blender", subprocess.Popen(args)
"--python-expr", self.report({"INFO"}, f"Opening {filepath} in a new Blender instance")
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: except Exception as e:
self.report({"ERROR"}, f"Failed to open IFC file: {str(e)}") self.report({"ERROR"}, f"Failed to open IFC file: {str(e)}")
return {"FINISHED"} return {"FINISHED"}
class ToggleDocument(bpy.types.Operator, tool.Ifc.Operator): class ToggleDocument(bpy.types.Operator):
bl_idname = "bim.toggle_document" bl_idname = "bim.toggle_document"
bl_label = "Toggle Document" bl_label = "Toggle Document"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
document: bpy.props.IntProperty() document: bpy.props.IntProperty()
option: bpy.props.StringProperty() option: bpy.props.StringProperty()
def _execute(self, context): def execute(self, context):
expanded_documents = [] expanded_documents = []
props = tool.Document.get_document_props() props = tool.Document.get_document_props()
try: try:
@@ -425,19 +315,14 @@ class ToggleDocument(bpy.types.Operator, tool.Ifc.Operator):
document_id = self.document document_id = self.document
if self.option == "Expand" and document_id not in expanded_documents: document = tool.Ifc.get().by_id(document_id)
expanded_documents.append(document_id) if document:
elif self.option == "Collapse" and document_id in expanded_documents: if self.option == "Expand" and document_id not in expanded_documents:
expanded_documents.remove(document_id) expanded_documents.append(document_id)
elif document_id == -1: elif self.option == "Collapse" and document_id in expanded_documents:
project = tool.Ifc.get().by_type("IfcProject")[0] expanded_documents.remove(document_id)
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)
props.json_string = json.dumps(expanded_documents) props.json_string = json.dumps(expanded_documents)
bpy.ops.bim.load_project_documents() bpy.ops.bim.load_project_documents()
return {"FINISHED"} return {"FINISHED"}
+21 -7
View File
@@ -51,8 +51,7 @@ def update_document_identification(self: "Document", context: bpy.types.Context)
def update_active_document(self, context): def update_active_document(self, context):
if self.documents and self.active_document_index < len(self.documents): if (document := self.active_document):
document = self.documents[self.active_document_index]
if document.ifc_definition_id: if document.ifc_definition_id:
DocumentData.load_document_objects_into_props(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") name: StringProperty(name="Name")
identification: StringProperty(name="Identification") identification: StringProperty(name="Identification")
description: StringProperty(name="Description") description: StringProperty(name="Description")
is_information: BoolProperty(name="Is Information")
ifc_definition_id: IntProperty(name="IFC Definition ID") ifc_definition_id: IntProperty(name="IFC Definition ID")
location: StringProperty(name="Location", default="") location: StringProperty(name="Location", default="")
tree_depth: IntProperty(name="Tree Depth", default=0) tree_depth: IntProperty(name="Tree Depth", default=0)
has_children: BoolProperty(name="Has Children", default=False) has_children: BoolProperty(name="Has Children", default=False)
is_expanded: BoolProperty(name="Is Expanded", 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: if TYPE_CHECKING:
name: str name: str
identification: str identification: str
description: str description: str
is_information: bool
ifc_definition_id: int ifc_definition_id: int
location: str location: str
tree_depth: int tree_depth: int
has_children: bool has_children: bool
is_expanded: bool is_expanded: bool
document_type: str
class DocumentObject(PropertyGroup): class DocumentObject(PropertyGroup):
@@ -93,17 +100,24 @@ class AssignedDocument(PropertyGroup):
name: StringProperty(name="Name") name: StringProperty(name="Name")
identification: StringProperty(name="Identification") identification: StringProperty(name="Identification")
description: StringProperty(name="Description", default="") description: StringProperty(name="Description", default="")
is_information: BoolProperty(name="Is Information")
ifc_definition_id: IntProperty(name="IFC Definition ID") ifc_definition_id: IntProperty(name="IFC Definition ID")
location: StringProperty(name="Location", default="") 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: if TYPE_CHECKING:
name: str name: str
identification: str identification: str
is_information: bool
ifc_definition_id: int ifc_definition_id: int
location: str location: str
document_type: str
class BIMDocumentProperties(PropertyGroup): class BIMDocumentProperties(PropertyGroup):
document_attributes: CollectionProperty(name="Document Attributes", type=Attribute) document_attributes: CollectionProperty(name="Document Attributes", type=Attribute)
+48 -36
View File
@@ -20,8 +20,7 @@ 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
from bonsai.bim.module.document.data import DocumentData, ObjectDocumentData from .data import DocumentData, ObjectDocumentData
class BIM_PT_documents(Panel): class BIM_PT_documents(Panel):
bl_label = "Documents" bl_label = "Documents"
@@ -46,8 +45,8 @@ class BIM_PT_documents(Panel):
split = row.split(factor=0.55) split = row.split(factor=0.55)
left_row = split.row(align=True) left_row = split.row(align=True)
left_row.label(text="{} Informations".format(DocumentData.data["total_document_informations"]), icon="FILE") total_documents = DocumentData.data["total_document_informations"] + DocumentData.data["total_document_references"]
left_row.label(text="{} References".format(DocumentData.data["total_document_references"]), icon="FILE_HIDDEN") left_row.label(text="{} Documents".format(total_documents), icon="FILE")
right_row = split.row(align=True) right_row = split.row(align=True)
right_row.label( right_row.label(
@@ -68,31 +67,31 @@ class BIM_PT_documents(Panel):
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")
else: 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") row.operator("bim.add_information", text="", icon="ADD")
if self.props.active_document: if self.props.active_document and (
if self.props.active_document.is_information and self.props.active_document.ifc_definition_id != -1: self.props.active_document.document_type == "INFORMATION" and
row.operator("bim.add_document_reference", text="", icon="FILE_HIDDEN") self.props.active_document.document_type != "PROJECT"
):
row.operator("bim.add_document_reference", text="", icon="FILE_HIDDEN")
active_document = self.props.active_document active_document = self.props.active_document
if active_document: if active_document:
ifc_definition_id = active_document.ifc_definition_id ifc_definition_id = active_document.ifc_definition_id
row.operator("bim.select_document_objects", text="", icon="RESTRICT_SELECT_OFF").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.assign_document", text="", icon="BRUSH_DATA").document = ifc_definition_id
row.operator("bim.remove_document", text="", icon="X").document = ifc_definition_id row.operator("bim.enable_editing_document", text="", icon="GREASEPENCIL").document = ifc_definition_id
row.operator("bim.remove_document", text="", icon="X").document = ifc_definition_id
self.layout.template_list("BIM_UL_documents", "", self.props, "documents", self.props, "active_document_index") self.layout.template_list("BIM_UL_documents", "", self.props, "documents", self.props, "active_document_index")
if self.props.is_document_editing: if self.props.is_document_editing:
active_document = self.props.active_document active_document = self.props.active_document
if active_document.is_information: draw_attributes(self.props.document_attributes, self.layout)
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.active_document: if self.props.is_editing and self.props.active_document:
document = self.props.active_document document = self.props.active_document
@@ -108,7 +107,6 @@ class BIM_PT_documents(Panel):
"active_document_object_index", "active_document_object_index",
) )
class BIM_PT_object_documents(Panel): class BIM_PT_object_documents(Panel):
bl_label = "Documents" bl_label = "Documents"
bl_idname = "BIM_PT_object_documents" bl_idname = "BIM_PT_object_documents"
@@ -119,6 +117,9 @@ class BIM_PT_object_documents(Panel):
bl_order = 1 bl_order = 1
bl_parent_id = "BIM_PT_tab_misc" bl_parent_id = "BIM_PT_tab_misc"
# Class variable to track the last selected object
_last_object_id = None
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
if not (obj := context.active_object): if not (obj := context.active_object):
@@ -130,10 +131,14 @@ class BIM_PT_object_documents(Panel):
return True return True
def draw(self, context): 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() ObjectDocumentData.load()
obj = context.active_object
self.oprops = tool.Blender.get_object_bim_props(obj) self.oprops = tool.Blender.get_object_bim_props(obj)
self.props = tool.Document.get_document_props() self.props = tool.Document.get_document_props()
self.file = tool.Ifc.get() self.file = tool.Ifc.get()
@@ -181,10 +186,11 @@ class BIM_PT_object_documents(Panel):
for doc in ObjectDocumentData.data["documents"]: for doc in ObjectDocumentData.data["documents"]:
assigned_doc_ids.append(doc["id"]) assigned_doc_ids.append(doc["id"])
# Only show assign button if the document is information (not reference) and not already assigned if (document.document_type == "INFORMATION" and
if document.is_information and document.ifc_definition_id not in assigned_doc_ids: 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 = 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: elif document.ifc_definition_id in assigned_doc_ids:
row.label(text="", icon="CHECKMARK") row.label(text="", icon="CHECKMARK")
self.layout.template_list( self.layout.template_list(
@@ -198,24 +204,28 @@ class BIM_UL_documents(UIList):
row = layout.row(align=True) row = layout.row(align=True)
indent_depth = 0 indent_depth = 0
if item.ifc_definition_id != -1: if item.document_type != "PROJECT":
if item.tree_depth > 1: if item.tree_depth > 1:
indent_depth = item.tree_depth - 1 indent_depth = item.tree_depth - 1
for i in range(indent_depth): for i in range(indent_depth):
row.label(text="", icon="BLANK1") 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="", icon="OUTLINER_COLLECTION")
row.label(text=item.name) row.label(text=item.name)
return return
if item.is_information and item.has_children:
if item.document_type == "INFORMATION" and item.has_children:
op = row.operator( op = row.operator(
"bim.toggle_document", icon="TRIA_DOWN" if item.is_expanded else "TRIA_RIGHT", text="", emboss=False "bim.toggle_document", icon="TRIA_DOWN" if item.is_expanded else "TRIA_RIGHT", text="", emboss=False
) )
op.document = item.ifc_definition_id op.document = item.ifc_definition_id
op.option = "Collapse" if item.is_expanded else "Expand" op.option = "Collapse" if item.is_expanded else "Expand"
elif item.is_information: elif item.document_type == "INFORMATION":
row.label(text="", icon="BLANK1") row.label(text="", icon="BLANK1")
if item.is_information:
if item.document_type == "INFORMATION":
row.label(text="", icon="FILE") row.label(text="", icon="FILE")
text = " - ".join([x for x in [item.name, item.location] if x]) text = " - ".join([x for x in [item.name, item.location] if x])
else: else:
@@ -227,9 +237,10 @@ class BIM_UL_documents(UIList):
split2.label(text=text) split2.label(text=text)
if item.location: if item.location:
uri = ObjectDocumentData.convert_to_file_uri(item.location)
if item.location.lower().endswith(".ifc"): if item.location.lower().endswith(".ifc"):
row.operator("bim.open_ifc_document", icon="HIDE_OFF", 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 = item.location row.operator("bim.open_uri", icon="URL", text="").uri = uri
class BIM_UL_document_objects(UIList): class BIM_UL_document_objects(UIList):
@@ -253,7 +264,7 @@ class BIM_UL_assigned_documents(UIList):
if item: if item:
row = layout.row(align=True) row = layout.row(align=True)
if item.is_information: if item.document_type == "INFORMATION":
row.label(text="", icon="FILE") row.label(text="", icon="FILE")
else: else:
row.label(text="", icon="FILE_HIDDEN") row.label(text="", icon="FILE_HIDDEN")
@@ -262,15 +273,16 @@ class BIM_UL_assigned_documents(UIList):
split1.label(text=item.identification or "") split1.label(text=item.identification or "")
split2 = split1.split(factor=1.0) split2 = split1.split(factor=1.0)
if item.is_information: if item.document_type == "INFORMATION":
split2.label(text=item.name or "Unnamed") split2.label(text=item.name or "Unnamed")
else: else:
split2.label(text=item.description or "No Description") split2.label(text=item.description or "No Description")
if item.location: if item.location:
uri = ObjectDocumentData.convert_to_file_uri(item.location)
if item.location.lower().endswith(".ifc"): if item.location.lower().endswith(".ifc"):
row.operator("bim.open_ifc_document", icon="HIDE_OFF", 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 = item.location row.operator("bim.open_uri", icon="URL", text="").uri = uri
op = row.operator("bim.unassign_document", text="", icon="X") op = row.operator("bim.unassign_document", text="", icon="X")
op.document = item.ifc_definition_id op.document = item.ifc_definition_id
+7 -11
View File
@@ -29,31 +29,27 @@ def load_project_documents(document: tool.Document) -> None:
document.import_project_documents() document.import_project_documents()
document.enable_editing_ui() 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: def disable_document_editing_ui(document: tool.Document) -> None:
document.disable_editing_ui() document.disable_editing_ui()
document.disable_editing_document() 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: 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.set_active_document(document)
document_tool.enable_document_editing()
document_tool.import_document_attributes(document) document_tool.import_document_attributes(document)
def disable_editing_document(document: tool.Document) -> None: def disable_editing_document(document: tool.Document) -> None:
props = document.get_document_props()
props.is_document_editing = False
document.clear_active_document() document.clear_active_document()
document.disable_document_editing()
document.clear_document_attributes() document.clear_document_attributes()
def add_information(ifc: tool.Ifc, document_tool: tool.Document, parent=None) -> ifcopenshell.entity_instance: def add_information(ifc: tool.Ifc, document_tool: tool.Document, parent=None) -> ifcopenshell.entity_instance:
document_tool.clear_document_tree() document_tool.clear_document_tree()
+75 -28
View File
@@ -18,12 +18,11 @@
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.core.tool import bonsai.core.tool
import bonsai.tool as tool import bonsai.tool as tool
import json import json
from natsort import natsorted
from typing import Any, Union, TYPE_CHECKING from typing import Any, Union, TYPE_CHECKING
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -70,12 +69,15 @@ class Document(bonsai.core.tool.Document):
data[attr_name] = "" data[attr_name] = ""
return True return True
if attr_name != "Name": if attr_name != "Name":
return None return None # Proceed normally
current_value = data[attr_name] 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: if current_value is not None:
return None return None
# Skip import since IFC restricts Name to be filled
# for IfcDocumentReference with ReferencedDocument.
return False return False
import_callback = callback if document.is_a("IfcDocumentReference") else None 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_documents.append(rel.RelatingDocument)
root = props.documents.add() root = props.documents.add()
root.ifc_definition_id = -1 root.ifc_definition_id = -project.id()
root.is_information = True root.document_type = "PROJECT"
root.name = f"Project Documents ({project.Name or 'Unnamed Project'})" root.name = f"Project Documents ({project.Name or 'Unnamed Project'})"
root.identification = "" root.identification = ""
root.location = "" root.location = ""
@@ -150,8 +152,9 @@ class Document(bonsai.core.tool.Document):
root.is_expanded = root_id not in expanded_documents root.is_expanded = root_id not in expanded_documents
if root.is_expanded: if root.is_expanded:
root_documents.sort( root_documents = natsorted(
key=lambda doc: ((cls.get_document_information_id(doc) or "").lower(), (doc.Name or "").lower()) root_documents,
key=lambda doc: (cls.get_document_information_id(doc) or "", doc.Name or "")
) )
for doc in root_documents: 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): def _process_document(cls, document, props, document_children, expanded_documents, depth):
new = props.documents.add() new = props.documents.add()
new.ifc_definition_id = document.id() 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 new.tree_depth = depth
file = document.file file = document.file
if new.is_information: if new.document_type == "INFORMATION":
new.name = document.Name or "Unnamed" new.name = document.Name or "Unnamed"
new.identification = cls.get_document_information_id(document) or "" new.identification = cls.get_document_information_id(document) or ""
new.location = document.Location or "" new.location = document.Location or ""
@@ -175,7 +178,7 @@ class Document(bonsai.core.tool.Document):
new.description = document.Description or "" new.description = document.Description or ""
new.location = document.Location or "" new.location = document.Location or ""
if not new.is_information: if new.document_type == "REFERENCE":
if file.schema == "IFC2X3": if file.schema == "IFC2X3":
if document.ReferenceToDocument: if document.ReferenceToDocument:
doc_info = document.ReferenceToDocument[0] doc_info = document.ReferenceToDocument[0]
@@ -183,7 +186,7 @@ class Document(bonsai.core.tool.Document):
new.name = doc_info.Name or "" new.name = doc_info.Name or ""
new.location = new.location or "" new.location = new.location or ""
else: else:
if hasattr(document, "ReferencedDocument") and document.ReferencedDocument: if document.ReferencedDocument:
doc_info = document.ReferencedDocument doc_info = document.ReferencedDocument
if not new.name: if not new.name:
new.name = doc_info.Name or "" 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")] 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")] ref_children = [d for d in children if not d.is_a("IfcDocumentInformation")]
info_children.sort( info_children = natsorted(
key=lambda doc: ((cls.get_document_information_id(doc) or "").lower(), (doc.Name or "").lower()) 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: ( key=lambda doc: (
(cls.get_external_reference_id(doc) or "").lower(), cls.get_external_reference_id(doc) or "",
(doc.Description or doc.Name or "").lower(), doc.Description or doc.Name or ""
) )
) )
for child in info_children + ref_children: for child in info_children + ref_children:
cls._process_document(child, props, document_children, expanded_documents, depth + 1) 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")
@@ -252,16 +257,6 @@ class Document(bonsai.core.tool.Document):
return document.DocumentReferences or () return document.DocumentReferences or ()
return document.HasDocumentReferences 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 @classmethod
def clear_active_document(cls) -> None: def clear_active_document(cls) -> None:
props = cls.get_document_props() 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]: def get_selected_document_information(cls, ifc) -> Union[ifcopenshell.entity_instance, None]:
props = cls.get_document_props() 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 ifc.get().by_id(props.active_document.ifc_definition_id)
return None 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 ""