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")