diff --git a/src/bonsai/bonsai/bim/helper.py b/src/bonsai/bonsai/bim/helper.py index 0fbbb94901..31be04f874 100644 --- a/src/bonsai/bonsai/bim/helper.py +++ b/src/bonsai/bonsai/bim/helper.py @@ -273,6 +273,15 @@ def export_attributes( return attributes +def process_exported_entity_attribute(attributes: dict[str, Any], attribute_name: str) -> None: + entity_id = attributes[attribute_name] + if entity_id is None: + # Maybe it was removed by now and enum is invalid. + del attributes[attribute_name] + else: + attributes[attribute_name] = tool.Ifc.get().by_id(int(entity_id)) + + ENUM_ITEMS_DATA = Union[bpy.types.PropertyGroup, bpy.types.ID, bpy.types.Operator, bpy.types.OperatorProperties] diff --git a/src/bonsai/bonsai/bim/prop.py b/src/bonsai/bonsai/bim/prop.py index 8a72d8505f..552e127116 100644 --- a/src/bonsai/bonsai/bim/prop.py +++ b/src/bonsai/bonsai/bim/prop.py @@ -84,7 +84,13 @@ def cache_string(s: Any) -> str: cache_string.data: dict[str, str] = {} -def get_attribute_enum_values(prop: "Attribute", context: bpy.types.Context) -> list[tuple[str, str, str]]: +def get_attribute_enum_values(prop: "Attribute", context: bpy.types.Context) -> tool.Blender.BLENDER_ENUM_ITEMS: + if "EnumData" not in globals() or TYPE_CHECKING: + from bonsai.bim.ui import EnumData + + if dynamic_identifier := prop.enum_items_dynamic: + return EnumData.get_data(dynamic_identifier) + # Support weird buildingSMART dictionary mappings which behave like enums items: list[tuple[str, str, str]] = [] data = json.loads(prop.enum_items) @@ -314,6 +320,11 @@ class Attribute(PropertyGroup): name="Value", description=tooltip, get=get_length_value, set=set_length_value, unit="LENGTH" ) enum_items: StringProperty(name="Value") + """Json serialized mapping of enum items: + Typically a dictionary of string identifiers to item names. + """ + enum_items_dynamic: StringProperty() + """Dynamic enum items identifier.""" enum_descriptions: CollectionProperty(type=StrProperty) enum_value: EnumProperty(items=get_attribute_enum_values, name="Value", update=update_attribute_value) filepath_value: PointerProperty(type=MultipleFileSelect) @@ -343,6 +354,7 @@ class Attribute(PropertyGroup): float_value: float length_value: float enum_items: str + enum_items_dynamic: str enum_descriptions: bpy.types.bpy_prop_collection_idprop[StrProperty] enum_value: str filepath_value: MultipleFileSelect @@ -365,7 +377,11 @@ class Attribute(PropertyGroup): return self.string_value.replace("\\n", "\n") if self.data_type == "file": return [f.name for f in self.filepath_value.file_list] - value = getattr(self, str(self.get_value_name()), None) + value_name = self.get_value_name() + if value_name == "enum_value": + value = tool.Blender.get_enum_safe(self, "enum_value") + else: + value = getattr(self, value_name, None) if self.special_type == "LOGICAL" and value != "UNKNOWN": # IfcOpenShell expects bool if IfcLogical is True/False. value = value == "TRUE" diff --git a/src/bonsai/bonsai/bim/ui.py b/src/bonsai/bonsai/bim/ui.py index a53238fc6a..980d8c1232 100644 --- a/src/bonsai/bonsai/bim/ui.py +++ b/src/bonsai/bonsai/bim/ui.py @@ -36,6 +36,7 @@ import bonsai.tool as tool from ifcopenshell.util.file import IfcHeaderExtractor from bonsai.bim.prop import Attribute from typing import Optional, TYPE_CHECKING +from natsort import natsorted class IFCFileSelector: @@ -1167,6 +1168,22 @@ class BIM_PT_tab_operations(Panel): def refresh(): UIData.is_loaded = False + EnumData.data.clear() + + +class EnumData: + data: dict[str, tool.Blender.BLENDER_ENUM_ITEMS] = {} + + @classmethod + def get_data(cls, identifier: str) -> tool.Blender.BLENDER_ENUM_ITEMS: + if identifier not in EnumData.data: + cls.data[identifier] = getattr(cls, identifier)() + return cls.data[identifier] + + @classmethod + def organizations(cls) -> tool.Blender.BLENDER_ENUM_ITEMS: + organizations = tool.Ifc.get().by_type("IfcOrganization") + return natsorted(((str(e.id()), e.Name, "") for e in organizations), key=lambda x: x[1]) class UIData: diff --git a/src/bonsai/bonsai/tool/blender.py b/src/bonsai/bonsai/tool/blender.py index 6317c9cb1a..ca45881b55 100644 --- a/src/bonsai/bonsai/tool/blender.py +++ b/src/bonsai/bonsai/tool/blender.py @@ -93,7 +93,7 @@ class Blender(bonsai.core.tool.Blender): - (identifier, name, description, icon, number) """ - BLENDER_ENUM_ITEMS = list[BLENDER_ENUM_ITEM] + BLENDER_ENUM_ITEMS = Iterable[BLENDER_ENUM_ITEM] @classmethod def activate_camera(cls, obj: bpy.types.Object) -> None: diff --git a/src/bonsai/bonsai/tool/owner.py b/src/bonsai/bonsai/tool/owner.py index 2e6c0d3cdf..bbd05c7caa 100644 --- a/src/bonsai/bonsai/tool/owner.py +++ b/src/bonsai/bonsai/tool/owner.py @@ -26,6 +26,7 @@ from typing import Union, Any, TYPE_CHECKING, Literal, assert_never if TYPE_CHECKING: from bonsai.bim.module.owner.prop import BIMOwnerProperties + from bonsai.bim.prop import Attribute class Owner(bonsai.core.tool.Owner): @@ -327,10 +328,23 @@ class Owner(bonsai.core.tool.Owner): application = tool.Ifc.get().by_id(props.active_application_id) props.application_attributes.clear() - bonsai.bim.helper.import_attributes("IfcApplication", props.application_attributes, application.get_info()) + def callback(name: str, prop: Union[Attribute, None], data: dict[str, Any]): + if name == "ApplicationDeveloper": + new = props.application_attributes.add() + new.name = name + new.data_type = "enum" + new.is_optional = False + new.enum_items_dynamic = "organizations" + new.enum_value = str(data["ApplicationDeveloper"].id()) + return True + + bonsai.bim.helper.import_attributes( + "IfcApplication", props.application_attributes, application.get_info(), callback + ) @classmethod def export_application_attributes(cls) -> dict[str, Any]: props = cls.get_owner_props() attributes = bonsai.bim.helper.export_attributes(props.application_attributes) + bonsai.bim.helper.process_exported_entity_attribute(attributes, "ApplicationDeveloper") return attributes diff --git a/src/bonsai/test/tool/test_owner.py b/src/bonsai/test/tool/test_owner.py index 4ff1af6946..84ca456e96 100644 --- a/src/bonsai/test/tool/test_owner.py +++ b/src/bonsai/test/tool/test_owner.py @@ -23,6 +23,8 @@ import bonsai.core.tool import bonsai.tool as tool from test.bim.bootstrap import NewFile from bonsai.tool.owner import Owner as subject +from typing import Any +from functools import cache class TestImplementsTool(NewFile): @@ -584,12 +586,18 @@ class TestSetUser(NewFile): class TestApplicationUI(NewFile): ifc_class = "IfcApplication" - attrs = { + attrs_: dict[str, Any] = { "Version": "v001", "ApplicationFullName": "IfcOpenShell", "ApplicationIdentifier": "IfcOpenShell", } + @cache + def get_attrs(self, ifc_file: ifcopenshell.file) -> dict[str, Any]: + attrs = self.attrs_.copy() + attrs["ApplicationDeveloper"] = ifc_file.create_entity("IfcOrganization", Name="Test") + return attrs + def test_set(self): application = ifcopenshell.file().create_entity(self.ifc_class) subject.set_application(application) @@ -614,14 +622,16 @@ class TestApplicationUI(NewFile): ifc = ifcopenshell.file() tool.Ifc.set(ifc) application = ifc.create_entity(self.ifc_class) - for attr, value in self.attrs.items(): + for attr, value in self.get_attrs(ifc).items(): setattr(application, attr, value) subject.set_application(application) subject.import_application_attributes() props = subject.get_owner_props() - for attr, value in self.attrs.items(): + for attr, value in self.get_attrs(ifc).items(): + if isinstance(value, ifcopenshell.entity_instance): + value = str(value.id()) assert props.application_attributes[attr].get_value() == value def test_export(self): self.test_import() - assert subject.export_application_attributes() == self.attrs + assert subject.export_application_attributes() == self.get_attrs(tool.Ifc.get())