diff --git a/src/bonsai/bonsai/bim/module/bcf/operator.py b/src/bonsai/bonsai/bim/module/bcf/operator.py index 7786fbfeb1..c4a5464ebf 100644 --- a/src/bonsai/bonsai/bim/module/bcf/operator.py +++ b/src/bonsai/bonsai/bim/module/bcf/operator.py @@ -18,6 +18,8 @@ import os import bcf.v3 +import bcf.v3.bcfxml +import bcf.v3.document import bcf.v3.model import bcf.v3.topic import bpy @@ -714,30 +716,70 @@ class AddBcfDocumentReference(bpy.types.Operator): def execute(self, context): bcfxml = bcfstore.BcfStore.get_bcfxml() assert bcfxml - - if not (version := (bcfxml.version.version_id or "")).startswith("2"): - self.report({"INFO"}, f"BCF {version} is not yet supported: {self.bl_rna.bl_idname}.") - return {"FINISHED"} + bcf_v2 = (bcfxml.version.version_id or "").startswith("2") props = context.scene.BCFProperties blender_topic = props.active_topic topic = bcfxml.topics[blender_topic.name] is_external = "://" in props.document_reference - filename = Path(props.document_reference).name if os.path.exists(props.document_reference) else None + + document_path = Path(props.document_reference) + document_bytes, filename = None, None + if document_path.is_file(): + filename = document_path.name + document_bytes = document_path.read_bytes() if filename: with open(props.document_reference, "rb") as f: - topic.document_references[filename] = f.read() + document_bytes = f.read() - document_reference = bcf.v2.model.TopicDocumentReference( - referenced_document=props.document_reference if is_external else filename, - description=props.document_reference_description or None, - guid=str(uuid.uuid4()), - is_external=is_external, - ) + document_references = tool.Bcf.get_topic_document_references(topic) + if bcf_v2: + assert isinstance(topic, bcf.v2.topic.TopicHandler) + if document_bytes and filename: + topic.document_references[filename] = document_bytes + assert tool.Bcf.is_list_of(document_references, bcf.v2.model.TopicDocumentReference) + document_reference = bcf.v2.model.TopicDocumentReference( + referenced_document=props.document_reference if is_external else filename, + description=props.document_reference_description or None, + guid=str(uuid.uuid4()), + is_external=is_external, + ) + document_references.append(document_reference) + else: + document_guid = None + if not is_external: + assert filename and document_bytes + assert isinstance(bcfxml, bcf.v3.bcfxml.BcfXml) + bcf_docs = bcfxml.documents - topic.topic.document_reference.append(document_reference) + if bcf_docs: + doc_definition = bcf_docs.definition + else: + doc_definition = bcf.v3.model.DocumentInfo() + bcf_docs = bcf.v3.document.DocumentsHandler(doc_definition) + bcfxml._documents = bcf_docs + + bcf_docs.documents[filename] = document_bytes + document_guid = str(uuid.uuid4()) + document = bcf.v3.model.Document( + filename=filename, description=props.document_description, guid=document_guid + ) + doc_definition_docs = doc_definition.documents + if not doc_definition_docs: + doc_definition.documents = (doc_definition_docs := bcf.v3.model.DocumentInfoDocuments()) + doc_definition_docs.document.append(document) + + assert tool.Bcf.is_list_of(document_references, bcf.v3.model.DocumentReference) + document_reference = bcf.v3.model.DocumentReference( + document_guid=document_guid, + url=props.document_reference if is_external else None, + description=props.document_reference_description or None, + guid=str(uuid.uuid4()), + ) + document_references.append(document_reference) + tool.Bcf.set_topic_document_references(topic, document_references) bpy.ops.bim.load_bcf_topic(topic_guid=topic.guid, topic_index=props.active_topic_index) props.document_reference = "" @@ -877,15 +919,59 @@ class RemoveBcfDocumentReference(bpy.types.Operator): def execute(self, context): bcfxml = bcfstore.BcfStore.get_bcfxml() assert bcfxml - - if not (version := (bcfxml.version.version_id or "")).startswith("2"): - self.report({"INFO"}, f"BCF {version} is not yet supported: {self.bl_rna.bl_idname}.") - return {"FINISHED"} + bcf_v2 = (bcfxml.version.version_id or "").startswith("2") props = context.scene.BCFProperties blender_topic = props.active_topic topic = bcfxml.topics[blender_topic.name] - del topic.topic.document_reference[self.index] + + document_references = tool.Bcf.get_topic_document_references(topic) + topic_index: int = self.index + document_reference = document_references[topic_index] + + # Remove document contents. + if bcf_v2: + assert isinstance(topic, bcf.v2.topic.TopicHandler) + assert isinstance(document_reference, bcf.v2.model.TopicDocumentReference) + if not document_reference.is_external: + ref_document = document_reference.referenced_document + assert ref_document + del topic.document_references[ref_document] + else: + assert isinstance(document_reference, bcf.v3.model.DocumentReference) + document_guid = document_reference.document_guid + # For bcf v3 documents are stored in bcfxml, not in the topic. + if document_guid: + assert isinstance(bcfxml, bcf.v3.bcfxml.BcfXml) + + # As there's no bcfxml document manager ui yet, + # we remove document if it's not referenced by any topic. + present_in_other_topics = False + for topic_ in bcfxml.topics.values(): + if topic_ == topic: + continue + document_references = tool.Bcf.get_topic_document_references(topic_) + assert tool.Bcf.is_list_of(document_references, bcf.v3.model.DocumentReference) + for ref_ in document_references: + if ref_.document_guid == document_guid: + present_in_other_topics = True + break + if present_in_other_topics: + break + + if not present_in_other_topics: + doc_handler = bcfxml.documents + assert doc_handler + docs = doc_handler.definition.documents + assert docs + doc = next(d for d in docs.document if d.guid == document_guid) + del doc_handler.documents[doc.filename] + docs.document.remove(doc) + + # Remove document reference. + del document_references[topic_index] + tool.Bcf.set_topic_document_references(topic, document_references) + bpy.ops.bim.load_bcf_topic(topic_guid=topic.guid, topic_index=props.active_topic_index) return {"FINISHED"} diff --git a/src/bonsai/bonsai/bim/module/bcf/prop.py b/src/bonsai/bonsai/bim/module/bcf/prop.py index a2f2220dee..99061c8e1f 100644 --- a/src/bonsai/bonsai/bim/module/bcf/prop.py +++ b/src/bonsai/bonsai/bim/module/bcf/prop.py @@ -210,6 +210,7 @@ class BCFProperties(PropertyGroup): bim_snippet_schema: StringProperty(default="", name="Schema") document_reference: StringProperty(default="", name="Referenced Document") document_reference_description: StringProperty(default="", name="Description") + document_description: StringProperty(default="", name="Document Description") related_topic: StringProperty(name="Related Topic") comment: StringProperty(default="", name="Comment") has_related_viewpoint: BoolProperty(name="Has Related Viewpoint", default=False) diff --git a/src/bonsai/bonsai/bim/module/bcf/ui.py b/src/bonsai/bonsai/bim/module/bcf/ui.py index 398a650303..0b081daba7 100644 --- a/src/bonsai/bonsai/bim/module/bcf/ui.py +++ b/src/bonsai/bonsai/bim/module/bcf/ui.py @@ -116,6 +116,10 @@ class BIM_PT_bcf_metadata(Panel): bl_parent_id = "BIM_PT_bcf" def draw(self, context): + # TODO: in bcf v3 documents can be added to the bcf + # without adding them to specific topic. + # Currently we just handle it the same way as in v2 + # documents are accessed and managed from topics. layout = self.layout layout.use_property_split = True layout.use_property_decorate = False @@ -127,6 +131,8 @@ class BIM_PT_bcf_metadata(Panel): if not bcfxml or props.active_topic_index >= len(props.topics): layout.label(text="No BCF project is loaded") return + bcf_verison = bcfxml.version.version_id or "" + bcf_v3 = bcf_verison.startswith("3") topic = props.active_topic bcf_topic = bcfxml.topics[topic.name] @@ -239,7 +245,12 @@ class BIM_PT_bcf_metadata(Panel): row.prop(props, "document_reference") row.operator("bim.select_bcf_document_reference", icon="FILE_FOLDER", text="") row = layout.row() - row.prop(props, "document_reference_description") + row.prop( + props, "document_reference_description", text="Reference Description" if bcf_v3 else "Document Description" + ) + if bcf_v3: + row = layout.row() + row.prop(props, "document_description") row = layout.row() row.operator("bim.add_bcf_document_reference") diff --git a/src/bonsai/bonsai/tool/bcf.py b/src/bonsai/bonsai/tool/bcf.py index 6508801350..2b89aec287 100644 --- a/src/bonsai/bonsai/tool/bcf.py +++ b/src/bonsai/bonsai/tool/bcf.py @@ -26,7 +26,10 @@ import bcf.v2.topic import bcf.v3.model import bcf.v3.topic import bcf.agnostic.topic -from typing import Any, Union, Optional +from typing import Any, Union, TypeVar, TypeGuard + + +T = TypeVar("T") class Bcf(bonsai.core.tool.Bcf): @@ -34,6 +37,13 @@ class Bcf(bonsai.core.tool.Bcf): def get_path(cls) -> str: return bpy.context.scene.BCFProperties.bcf_file + @classmethod + def is_list_of(cls, a: list[Any], t: type[T]) -> TypeGuard[list[T]]: + """Narrow type for a list assuming list is not mixing types.""" + if not a: + return True + return isinstance(a[0], t) + # BCF version agnostic API. ## topic @classmethod @@ -47,6 +57,26 @@ class Bcf(bonsai.core.tool.Bcf): document_references = document_references.document_reference if document_references else [] return document_references + @classmethod + def set_topic_document_references( + cls, + topic: bcf.agnostic.topic.TopicHandler, + document_references: Union[list[bcf.v2.model.TopicDocumentReference], list[bcf.v3.model.DocumentReference]], + ) -> None: + topic_ = topic.topic + if isinstance(topic_, bcf.v2.model.Topic): + assert cls.is_list_of(document_references, bcf.v2.model.TopicDocumentReference) + topic_.document_reference = document_references + else: + assert cls.is_list_of(document_references, bcf.v3.model.DocumentReference) + topic_document_references = topic_.document_references + if topic_document_references is None: + if not document_references: + return + topic_.document_references = (topic_document_references := bcf.v3.model.TopicDocumentReferences()) + return + topic_document_references.document_reference = document_references + @classmethod def get_topic_header_files( cls, topic: bcf.agnostic.topic.TopicHandler