diff --git a/src/bcf/bcf/agnostic/model.py b/src/bcf/bcf/agnostic/model.py index 726a2e483a..5358c3c45d 100644 --- a/src/bcf/bcf/agnostic/model.py +++ b/src/bcf/bcf/agnostic/model.py @@ -3,3 +3,5 @@ import bcf.v3.model from typing import Union BimSnippet = Union[bcf.v2.model.BimSnippet, bcf.v3.model.BimSnippet] +DocumentReference = Union[bcf.v2.model.TopicDocumentReference, bcf.v3.model.DocumentReference] +HeaderFile = Union[bcf.v2.model.HeaderFile, bcf.v3.model.File] diff --git a/src/bcf/bcf/agnostic/topic.py b/src/bcf/bcf/agnostic/topic.py index 166ab9091d..f8a56ad61f 100644 --- a/src/bcf/bcf/agnostic/topic.py +++ b/src/bcf/bcf/agnostic/topic.py @@ -1,5 +1,93 @@ +import tempfile +import bcf.v2.bcfxml +import bcf.v2.model import bcf.v2.topic +import bcf.v3.bcfxml +import bcf.v3.model import bcf.v3.topic -from typing import Union +import bcf.agnostic.model as mdl +from pathlib import Path +from typing import Union, Optional +from typing_extensions import assert_never TopicHandler = Union[bcf.v2.topic.TopicHandler, bcf.v3.topic.TopicHandler] + + +def extract_file( + topic: TopicHandler, + entity: Union[mdl.HeaderFile, mdl.BimSnippet, mdl.DocumentReference], + bcfxml: Optional[Union[bcf.v2.bcfxml.BcfXml, bcf.v3.bcfxml.BcfXml]] = None, + outfile: Optional[Path] = None, +) -> Union[Path, str, None]: + """Extracts an element with a file into a temporary directory + + These include header files, bim snippets, document references, and + viewpoint bitmaps. External reference are not downloaded. Instead, the + URI reference is returned. + + :param entity: The entity with a file reference to extract + :param outfile: If provided, save the header file to that location. + Otherwise, a temporary directory is created and the filename is + derived from the header's original filename. + :param bcfxml: The BCF XML file to use for resolving document references files. + Required only for BCF v3 document references (in BCF v3 internal documents + are stored at BCF root, not in the topic). + :return: The filepath of the extracted file. It may be a URL if the + header file is external. + """ + if isinstance(entity, mdl.DocumentReference): + if isinstance(entity, bcf.v2.model.TopicDocumentReference): + reference = entity.referenced_document + else: + reference = entity.document_guid + else: + reference = entity.reference + + if not reference: + return None + + # For v3 document references external documents are detected by empty document_guid. + if not isinstance(entity, bcf.v3.model.DocumentReference) and entity.is_external: + return reference + + if isinstance(entity, bcf.v3.model.DocumentReference): + # Extract document reference filename and contents. + if not bcfxml: + raise TypeError("bcfxml is required for BCF v3 document references.") + assert isinstance(bcfxml, bcf.v3.bcfxml.BcfXml) + error_msg = f"BCF XML is missing document with guid '{reference}'." + if not bcfxml.documents: + raise Exception(error_msg) + definition_docs = bcfxml.documents.definition.documents + if not definition_docs: + raise Exception(error_msg) + docs = next((doc for doc in definition_docs.document if doc.guid == reference), None) + if not docs: + raise Exception(error_msg) + filename = docs.filename + bytes_data = bcfxml.documents.documents[filename] + else: + if isinstance(entity, mdl.BimSnippet): + bytes_data = topic.bim_snippet + assert isinstance(bytes_data, bytes) + elif isinstance(entity, mdl.HeaderFile): + bytes_data = topic.reference_files[reference] + elif isinstance(entity, bcf.v2.model.TopicDocumentReference): + assert isinstance(topic, bcf.v2.topic.TopicHandler) + bytes_data = topic.document_references[reference] + else: + assert_never(entity) + + # We don't really need it if 'outfile' is None, just keeping type checker happy. + if isinstance(entity, mdl.HeaderFile) and entity.filename: + filename = entity.filename + else: + filename = Path(reference).name + + if not outfile: + outfile = Path(tempfile.mkdtemp()) / filename + + with open(outfile, "wb") as f: + f.write(bytes_data) + + return outfile diff --git a/src/bcf/bcf/v2/topic.py b/src/bcf/bcf/v2/topic.py index f8013e5d8a..0a26cbcc93 100644 --- a/src/bcf/bcf/v2/topic.py +++ b/src/bcf/bcf/v2/topic.py @@ -240,54 +240,6 @@ class TopicHandler: real_path = real_path.parent if path_part == ".." else real_path.joinpath(path_part) destination_zip.writestr(real_path.at, self.document_references[doc.referenced_document]) - def extract_file( - self, entity: Union[mdl.HeaderFile, mdl.BimSnippet, mdl.TopicDocumentReference], outfile: Optional[Path] = None - ) -> Union[Path, str, None]: - """Extracts an element with a file into a temporary directory - - These include header files, bim snippets, document references, and - viewpoint bitmaps. External reference are not downloaded. Instead, the - URI reference is returned. - - :param entity: The entity with a file reference to extract - :param outfile: If provided, save the header file to that location. - Otherwise, a temporary directory is created and the filename is - derived from the header's original filename. - :return: The filepath of the extracted file. It may be a URL if the - header file is external. - :rtype: Path - """ - if hasattr(entity, "reference"): - reference = entity.reference - else: - reference = entity.referenced_document - - if not reference: - return - - if getattr(entity, "is_external", False): - return entity.reference - - resolved_reference = self._topic_dir - - for part in Path(reference).parts: - if part == "..": - resolved_reference = resolved_reference.parent - else: - resolved_reference = resolved_reference.joinpath(part) - - if not outfile: - if getattr(entity, "filename", None): - filename = entity.filename - else: - filename = resolved_reference.name - outfile = Path(tempfile.mkdtemp()) / filename - - with open(outfile, "wb") as f: - f.write(resolved_reference.read_bytes()) - - return outfile - def add_viewpoint(self, element: entity_instance) -> VisualizationInfoHandler: """Add a viewpoint pointed at the placement of an IFC element to the topic. diff --git a/src/bonsai/bonsai/bim/module/bcf/operator.py b/src/bonsai/bonsai/bim/module/bcf/operator.py index 09bec2054d..e113844037 100644 --- a/src/bonsai/bonsai/bim/module/bcf/operator.py +++ b/src/bonsai/bonsai/bim/module/bcf/operator.py @@ -1493,14 +1493,10 @@ class LoadBcfHeaderIfcFile(bpy.types.Operator): 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_path = tool.Bcf.get_path() topic = bcfxml.topics[context.scene.BCFProperties.active_topic.name] - entity = topic.header.file[self.index] - ifc_path = str(topic.extract_file(entity)) + entity = tool.Bcf.get_topic_header_files(topic)[self.index] + ifc_path = bcf.agnostic.topic.extract_file(topic, entity) bpy.ops.bim.load_project(filepath=ifc_path) if bcf_path: bpy.ops.bim.load_bcf_project(filepath=bcf_path) @@ -1518,20 +1514,21 @@ class ExtractBcfFile(bpy.types.Operator): 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"} - topic = bcfxml.topics[context.scene.BCFProperties.active_topic.name] if self.entity_type == "HEADER_FILE": - entity = topic.header.file[self.index] + entity = tool.Bcf.get_topic_header_files(topic)[self.index] elif self.entity_type == "BIM_SNIPPET": - entity = topic.markup.topic.bim_snippet + entity = topic.topic.bim_snippet elif self.entity_type == "DOCUMENT_REFERENCE": - entity = topic.markup.topic.document_reference[self.index] + entity = tool.Bcf.get_topic_document_references(topic)[self.index] + else: + assert False - webbrowser.open(str(topic.extract_file(entity).parent)) + assert entity + filepath = bcf.agnostic.topic.extract_file(topic, entity, bcfxml) + assert isinstance(filepath, Path) + webbrowser.open(str(filepath.parent)) return {"FINISHED"}