diff --git a/src/bcf/bcf/v2/topic.py b/src/bcf/bcf/v2/topic.py index de68cb5dd7..f8013e5d8a 100644 --- a/src/bcf/bcf/v2/topic.py +++ b/src/bcf/bcf/v2/topic.py @@ -63,6 +63,11 @@ class TopicHandler: """Return the header of the topic.""" return self.markup.header if self.markup else None + @header.setter + def header(self, header: mdl.Header) -> None: + """Set the header of the topic.""" + self.markup.header = header + @property def comments(self) -> list[mdl.Comment]: """Return the comments of the topic.""" diff --git a/src/bcf/bcf/v3/topic.py b/src/bcf/bcf/v3/topic.py index 42a4269ddb..b0cfa4268d 100644 --- a/src/bcf/bcf/v3/topic.py +++ b/src/bcf/bcf/v3/topic.py @@ -27,6 +27,7 @@ class TopicHandler: ) -> None: self._markup: Optional[mdl.Markup] = None self._viewpoints: Optional[dict[str, VisualizationInfoHandler]] = None + self._reference_files: Optional[dict[str, bytes]] = None self._bim_snippet: Optional[bytes] = None self._xml_handler = xml_handler or XmlParserSerializer() self._topic_dir = topic_dir @@ -60,6 +61,11 @@ class TopicHandler: """Return the header of the topic.""" return self.markup.header + @header.setter + def header(self, header: mdl.Header) -> None: + """Set the header of the topic.""" + self.markup.header = header + @property def comments(self) -> list[mdl.Comment]: """Return the comments of the topic.""" @@ -103,6 +109,27 @@ class TopicHandler: return bim_snippet_path.read_bytes() return None + @property + def reference_files(self) -> dict[str, bytes]: + if self._reference_files is not None: + return self._reference_files + + self._reference_files = {} + if not self.header: + return self._reference_files + + if not self.header.files: + return self._reference_files + + for ref in self.header.files.file: + if ref.is_external: + continue + real_path = self._topic_dir + for path_part in ref.reference.split("/"): + real_path = real_path.parent if path_part == ".." else real_path.joinpath(path_part) + self._reference_files[ref.reference] = real_path.read_bytes() + return self._reference_files + @classmethod def create_new( cls, @@ -154,6 +181,7 @@ class TopicHandler: self._save_xml(destination_zip, self._markup, "markup.bcf") self._save_viewpoints(destination_zip, topic_dir) self._save_bim_snippet(destination_zip) + self._save_reference_files(destination_zip) def _save_viewpoints(self, destination_zip: ZipFileInterface, topic_dir: str) -> None: if not self.topic.viewpoints or not (viewpoints := self.topic.viewpoints.view_point): @@ -174,6 +202,19 @@ class TopicHandler: if self.bim_snippet: destination_zip.writestr(f"{self.topic.guid}/{ref_filename}", self.bim_snippet) + def _save_reference_files(self, destination_zip: ZipFileInterface) -> None: + if not self.header: + return + if not self.header.files: + return + for ref in self.header.files.file: + if ref.is_external or not ref.reference: + continue + real_path = self._topic_dir + for path_part in ref.reference.split("/"): + real_path = real_path.parent if path_part == ".." else real_path.joinpath(path_part) + destination_zip.writestr(real_path.at, self.reference_files[ref.reference]) + def add_viewpoint(self, element: entity_instance) -> VisualizationInfoHandler: """ Add a viewpoint tergeting 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 65283010cb..ab3791dd13 100644 --- a/src/bonsai/bonsai/bim/module/bcf/operator.py +++ b/src/bonsai/bonsai/bim/module/bcf/operator.py @@ -475,31 +475,46 @@ class AddBcfHeaderFile(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.file_reference - filename = Path(props.file_reference).name if os.path.exists(props.file_reference) else None + filepath = Path(props.file_reference) + file_bytes, filename = None, None + if filepath.is_file(): + filename = filepath.name + file_bytes = filepath.read_bytes() - if filename: - with open(props.file_reference, "r") as f: - topic.reference_files[filename] = f.read() + header_files = tool.Bcf.get_topic_header_files(topic) + if filename and file_bytes: + topic.reference_files[filename] = file_bytes + if bcf_v2: + header_file = bcf.v2.model.HeaderFile( + filename=filename, + date=XmlDateTime.now(), + reference=props.file_reference if is_external else filename, + ifc_project=props.file_ifc_project, + ifc_spatial_structure_element=props.file_ifc_spatial_structure_element, + is_external=is_external, + ) + assert tool.Bcf.is_list_of(header_files, bcf.v2.model.HeaderFile) + header_files.append(header_file) + else: + header_file = bcf.v3.model.File( + filename=filename, + date=XmlDateTime.now(), + reference=props.file_reference if is_external else filename, + ifc_project=props.file_ifc_project, + ifc_spatial_structure_element=props.file_ifc_spatial_structure_element, + is_external=is_external, + ) + assert tool.Bcf.is_list_of(header_files, bcf.v3.model.File) + header_files.append(header_file) - header_file = bcf.v2.model.HeaderFile( - filename=filename, - date=XmlDateTime.now(), - reference=props.file_reference if is_external else Path(props.file_reference).name, - ifc_project=props.file_ifc_project, - ifc_spatial_structure_element=props.file_ifc_spatial_structure_element, - is_external=is_external, - ) - topic.header.file.append(header_file) + tool.Bcf.set_topic_header_files(topic, header_files) props.refresh_topic(context) return {"FINISHED"} @@ -652,14 +667,12 @@ class RemoveBcfFile(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"} - props = context.scene.BCFProperties blender_topic = props.active_topic topic = bcfxml.topics[blender_topic.name] - del topic.header.file[self.index] + header_files = tool.Bcf.get_topic_header_files(topic) + del header_files[self.index] + tool.Bcf.set_topic_header_files(topic, header_files) props.refresh_topic(context) return {"FINISHED"} @@ -734,10 +747,6 @@ class AddBcfDocumentReference(bpy.types.Operator): filename = document_path.name document_bytes = document_path.read_bytes() - if filename: - with open(props.document_reference, "rb") as f: - document_bytes = f.read() - document_references = tool.Bcf.get_topic_document_references(topic) if bcf_v2: assert isinstance(topic, bcf.v2.topic.TopicHandler) diff --git a/src/bonsai/bonsai/tool/bcf.py b/src/bonsai/bonsai/tool/bcf.py index 106af04971..516e9ff0f4 100644 --- a/src/bonsai/bonsai/tool/bcf.py +++ b/src/bonsai/bonsai/tool/bcf.py @@ -112,6 +112,37 @@ class Bcf(bonsai.core.tool.Bcf): header_files = header_files.file if header_files else [] return header_files + @classmethod + def set_topic_header_files( + cls, + topic: bcf.agnostic.topic.TopicHandler, + files: Union[list[bcf.v2.model.HeaderFile], list[bcf.v3.model.File]], + ) -> None: + if isinstance(topic, bcf.v2.topic.TopicHandler): + header = topic.header + if not header: + if not files: + return + topic.header = (header := bcf.v2.model.Header()) + if not header.file: + if not files: + return + assert cls.is_list_of(files, bcf.v2.model.HeaderFile) + header.file = files + else: + header = topic.header + if not header: + if not files: + return + header = bcf.v3.model.Header() + header_files = header.files + if not header_files: + if not files: + return + header.files = (header_files := bcf.v3.model.HeaderFiles()) + assert cls.is_list_of(files, bcf.v3.model.File) + header_files.file = files + @classmethod def get_topic_labels(cls, topic: bcf.agnostic.topic.TopicHandler) -> list[str]: if isinstance(topic, bcf.v2.topic.TopicHandler):