From 2f1de2f104f30bb390722cb1882e7cd41ca65825 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 18 Jul 2025 16:46:11 +0500 Subject: [PATCH] Autoload active bsdd from IFC file Now we store in the IFC file list of active bsdd user was using in Bonsai. So when you open IFC file it automatically marks dictionaries as active and there's no need to go to bsdd UI, unless there are new dictionaries to add/remove. Example - https://files.catbox.moe/kzwa2n.mp4 --- src/bonsai/bonsai/bim/module/bsdd/data.py | 5 +- src/bonsai/bonsai/bim/module/bsdd/prop.py | 2 + .../bonsai/bim/module/classification/data.py | 5 +- src/bonsai/bonsai/tool/bsdd.py | 84 ++++++++++++++++++- src/bonsai/test/tool/test_loader.py | 24 +++--- 5 files changed, 100 insertions(+), 20 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/bsdd/data.py b/src/bonsai/bonsai/bim/module/bsdd/data.py index 513ca2de8b..7905ca75c1 100644 --- a/src/bonsai/bonsai/bim/module/bsdd/data.py +++ b/src/bonsai/bonsai/bim/module/bsdd/data.py @@ -38,8 +38,7 @@ class BSDDData: cls.data["active_dictionary"] = cls.active_dictionary() @classmethod - def active_dictionary(cls): - props = tool.Bsdd.get_bsdd_props() + def active_dictionary(cls) -> tool.Blender.BLENDER_ENUM_ITEMS: results = [("ALL", "All Dictionaries", "All active dictionaries")] - results.extend([(d.uri, d.name, f"{d.status} - {d.version}") for d in props.dictionaries if d.is_active]) + results.extend(tool.Bsdd.get_active_bsdd_enum_items()) return results diff --git a/src/bonsai/bonsai/bim/module/bsdd/prop.py b/src/bonsai/bonsai/bim/module/bsdd/prop.py index 3f71ff4752..3b427ba626 100644 --- a/src/bonsai/bonsai/bim/module/bsdd/prop.py +++ b/src/bonsai/bonsai/bim/module/bsdd/prop.py @@ -42,6 +42,8 @@ def get_active_dictionary(self: "BIMBSDDProperties", context: object) -> tool.Bl def update_is_active(self: "BSDDDictionary", context: bpy.types.Context) -> None: + tool.Bsdd.save_active_bsdd_to_ifc() + BSDDData.data["active_dictionary"] = BSDDData.active_dictionary() if ClassificationsData.is_loaded: props = tool.Classification.get_classification_props() diff --git a/src/bonsai/bonsai/bim/module/classification/data.py b/src/bonsai/bonsai/bim/module/classification/data.py index c8d40a32a1..424e3f790e 100644 --- a/src/bonsai/bonsai/bim/module/classification/data.py +++ b/src/bonsai/bonsai/bim/module/classification/data.py @@ -65,13 +65,12 @@ class ClassificationsData: return [(str(e.id()), e.Name, "") for e in IfcStore.classification_file.by_type("IfcClassification")] @classmethod - def classification_source(cls): + def classification_source(cls) -> tool.Blender.BLENDER_ENUM_ITEMS: items = [ ("FILE", "IFC File", ""), ("MANUAL", "Manual Entry", ""), ] - bprops = tool.Bsdd.get_bsdd_props() - dictionaries = [(d.uri, f"bSDD: {d.name}", "") for d in bprops.dictionaries if d.is_active] + dictionaries = tool.Bsdd.get_active_bsdd_enum_items() if dictionaries: items.append(("BSDD", "All Active bSDDs", "")) items.extend(dictionaries) diff --git a/src/bonsai/bonsai/tool/bsdd.py b/src/bonsai/bonsai/tool/bsdd.py index d54f2965db..0e772e9241 100644 --- a/src/bonsai/bonsai/tool/bsdd.py +++ b/src/bonsai/bonsai/tool/bsdd.py @@ -22,13 +22,14 @@ import bonsai.tool as tool import bpy import json import bsdd +import ifcopenshell.api.library import ifcopenshell.util.type import ifcopenshell.util.element import ifcopenshell.util.classification -from typing import Any, Union, Optional, TYPE_CHECKING +from typing import Any, Union, TYPE_CHECKING, TypedDict if TYPE_CHECKING: - from bonsai.bim.module.bsdd.prop import BIMBSDDProperties + from bonsai.bim.module.bsdd.prop import BIMBSDDProperties, BSDDDictionary class Bsdd(bonsai.core.tool.Bsdd): @@ -84,6 +85,7 @@ class Bsdd(bonsai.core.tool.Bsdd): @classmethod def create_dictionaries(cls, dictionaries: list[bsdd.DictionaryContractV1]) -> None: props = cls.get_bsdd_props() + active_bsdds: set[str] = {r.Location for r in cls.get_active_bsdd_ifc()} for dictionary in sorted(dictionaries, key=lambda d: d["name"]): new = props.dictionaries.add() new.name = dictionary["name"] @@ -92,6 +94,7 @@ class Bsdd(bonsai.core.tool.Bsdd): new.organization_name_owner = dictionary["organizationNameOwner"] new.status = dictionary["status"] new.version = dictionary["version"] + new["is_active"] = dictionary["uri"] in active_bsdds @classmethod def get_active_class_data(cls) -> Union[bsdd.ClassContractV1, dict]: @@ -392,3 +395,80 @@ class Bsdd(bonsai.core.tool.Bsdd): uris.add(uri) class_uri, pset_name = pset_uri.rsplit("#", 1) return class_uri in uris + + @classmethod + def get_active_bsdd_library(cls) -> Union[ifcopenshell.entity_instance, None]: + for document in tool.Ifc.get().by_type("IfcLibraryInformation"): + if document.Name == "BBIM_Active_bSDD": + return document + + @classmethod + def get_refs(cls, library: ifcopenshell.entity_instance) -> tuple[ifcopenshell.entity_instance, ...]: + if library.file.schema == "IFC2X3": + return library.LibraryReference or () + return library.HasLibraryReferences + + @classmethod + def get_active_bsdd_ifc(cls) -> tuple[ifcopenshell.entity_instance, ...]: + library = cls.get_active_bsdd_library() + if not library: + return () + return cls.get_refs(library) + + class BSDDJsonData(TypedDict): + name: str + description: str + + @classmethod + def save_active_bsdd_to_ifc(cls) -> None: + props = cls.get_bsdd_props() + assert props.dictionaries + + ifc_file = tool.Ifc.get() + library = cls.get_active_bsdd_library() + if not library: + library = ifcopenshell.api.library.add_library(ifc_file, "BBIM_Active_bSDD") + + active_bsdd: dict[str, BSDDDictionary] = {d.uri: d for d in props.dictionaries if d.is_active} + refs_to_remove: list[ifcopenshell.entity_instance] = [] + active_bsdd_ifc: set[str] = set() + for ref in cls.get_refs(library): + uri = ref.Location + if uri not in active_bsdd: + refs_to_remove.append(ref) + continue + active_bsdd_ifc.add(uri) + + # There is a possible edge case here. + # E.g. user had some dicts from test/preview previously active. + # But now other user opens file in Bonsai, where test/preview are not enabled in preferences, + # and makes changes to active dictionaries. + # Then, this will also unmark those test/preview dicts as active, + # which can be sometimes confusing. + # + # But the alternative is to only change status just for the dictionary + # changed by users, but then we need to come up with some other mechanism + # how to keep this library clean from dead urls + # (e.g. when dictionaries will go out of date). + for ref in refs_to_remove: + ifcopenshell.api.library.remove_reference(ifc_file, ref) + + missing_bsdd = set(active_bsdd) - active_bsdd_ifc + for uri in missing_bsdd: + blender_dict = active_bsdd[uri] + ref = ifcopenshell.api.library.add_reference(ifc_file, library) + ref.Location = blender_dict.uri + # Can't use Description as it's not present in IFC2X3. + data: Bsdd.BSDDJsonData = { + "name": blender_dict.name, + "description": f"{blender_dict.status} - {blender_dict.version}", + } + ref.Name = json.dumps(data) + + @classmethod + def get_active_bsdd_enum_items(cls) -> list[tuple[str, str, str]]: + results: list[tuple[str, str, str]] = [] + for ref in tool.Bsdd.get_active_bsdd_ifc(): + data: Bsdd.BSDDJsonData = json.loads(ref.Name) + results.append((ref.Location, data["name"], data["description"])) + return results diff --git a/src/bonsai/test/tool/test_loader.py b/src/bonsai/test/tool/test_loader.py index e0c5766421..b128988aba 100644 --- a/src/bonsai/test/tool/test_loader.py +++ b/src/bonsai/test/tool/test_loader.py @@ -18,7 +18,9 @@ import bpy import bmesh +import json import ifcopenshell +import ifcopenshell.api.library import ifcopenshell.api.style import ifcopenshell.util.schema import bonsai.core.tool @@ -527,22 +529,20 @@ class TestSetupActiveBsddClassification(NewFile): props.export_schema = schema_ bpy.ops.bim.create_project() ifc_file = tool.Ifc.get() - name = "CCI Construction" + data: tool.Bsdd.BSDDJsonData = {"name": "CCI Construction", "description": ""} base_uri = "https://identifier.buildingsmart.org/uri/molio/cciconstruction/1.0" - if schema == "IFC2X3": - uri = "https://identifier.buildingsmart.org/uri/molio/cciconstruction/1.0/class/A-BDC" - classification = ifc_file.create_entity("IfcClassification", Name=name) - ifc_file.create_entity("IfcClassificationReference", Location=uri, ReferencedSource=classification) - else: - attr_name = "Specification" if schema == "IFC4X3" else "Location" - classification = ifc_file.create_entity("IfcClassification", Name=name) - setattr(classification, attr_name, base_uri) + + library = ifcopenshell.api.library.add_library(ifc_file, "BBIM_Active_bSDD") + reference = ifcopenshell.api.library.add_reference(ifc_file, library) + reference.Location = base_uri + reference.Name = json.dumps(data) filepath = "test/files/temp/test.ifc" ifc_file.write(filepath) bpy.ops.bim.load_project(filepath=filepath) - props = tool.Bsdd.get_bsdd_props() - assert props.active_domain == name - assert props.active_uri == base_uri + props = tool.Classification.get_classification_props() + + # No errors means this uri present in the enum. + props.classification_source = base_uri def test_set_load_and_set_active_bsdd_ifc2x3(self): self.run_test("IFC2X3")