diff --git a/src/bonsai/bonsai/bim/module/bsdd/__init__.py b/src/bonsai/bonsai/bim/module/bsdd/__init__.py index 6b14130fe1..ee1c2c49c8 100644 --- a/src/bonsai/bonsai/bim/module/bsdd/__init__.py +++ b/src/bonsai/bonsai/bim/module/bsdd/__init__.py @@ -20,15 +20,20 @@ import bpy from . import ui, prop, operator classes = ( - operator.GetBSDDClassificationProperties, + operator.AddBSDDProperties, + operator.ImportBSDDClasses, operator.LoadBSDDDictionaries, - operator.SearchBSDDClass, + operator.SearchBSDDClassifications, + operator.SearchBSDDProperties, prop.BSDDDictionary, prop.BSDDClassification, + prop.BSDDProperty, prop.BSDDPset, prop.BIMBSDDProperties, - ui.BIM_UL_bsdd_dictionaries, ui.BIM_UL_bsdd_classifications, + ui.BIM_UL_bsdd_dictionaries, + ui.BIM_UL_bsdd_classes, + ui.BIM_UL_bsdd_properties, ui.BIM_PT_bsdd, ) diff --git a/src/bonsai/bonsai/bim/module/bsdd/operator.py b/src/bonsai/bonsai/bim/module/bsdd/operator.py index d9df26349a..111c64c40d 100644 --- a/src/bonsai/bonsai/bim/module/bsdd/operator.py +++ b/src/bonsai/bonsai/bim/module/bsdd/operator.py @@ -19,6 +19,7 @@ import bpy import bsdd import bonsai.tool as tool +import ifcopenshell.util.element from bonsai.core import bsdd as core @@ -32,28 +33,73 @@ class LoadBSDDDictionaries(bpy.types.Operator): return {"FINISHED"} -class SearchBSDDClass(bpy.types.Operator): +class SearchBSDDClassifications(bpy.types.Operator): bl_idname = "bim.search_bsdd_classifications" bl_label = "Search bSDD Class" bl_description = "Search for bSDD classes by the provided keyword" bl_options = {"REGISTER", "UNDO"} def execute(self, context): - keyword = context.scene.BIMBSDDProperties.keyword - classes_found = core.search_class(keyword, tool.Bsdd) - self.report({"INFO"}, f"{classes_found} bSDD classes found for '{keyword}'.") + total = core.search_bsdd_class(tool.Bsdd, context.scene.BIMBSDDProperties.keyword) + self.report({"INFO"}, f"{total} bSDD classes found.") return {"FINISHED"} -class GetBSDDClassificationProperties(bpy.types.Operator): - bl_idname = "bim.get_bsdd_classification_properties" - bl_label = "Search bSDD Class Properties" - bl_description = "Search for bSDD class properties for the currently selected class" +class ImportBSDDClasses(bpy.types.Operator): + bl_idname = "bim.import_bsdd_classes" + bl_label = "Import bSDD Classes" + bl_description = "Load bSDD classes that apply to the current element" bl_options = {"REGISTER", "UNDO"} + obj: bpy.props.StringProperty() + obj_type: bpy.props.StringProperty() def execute(self, context): - pset_data = core.get_class_properties(tool.Bsdd) - psets_found = len(pset_data) - props_found = sum(len(pset) for pset in pset_data.values()) - self.report({"INFO"}, f"{psets_found} psets found ({props_found} props) for the active classification.") + props = tool.Bsdd.get_bsdd_props() + core.import_bsdd_classes(tool.Bsdd, self.obj, self.obj_type) + self.report({"INFO"}, f"{len(props.classes)} bSDD classes found.") return {"FINISHED"} + + +class SearchBSDDProperties(bpy.types.Operator): + bl_idname = "bim.search_bsdd_properties" + bl_label = "Search bSDD Properties" + bl_description = "Search for bSDD properties that apply to the current element" + bl_options = {"REGISTER", "UNDO"} + obj: bpy.props.StringProperty() + obj_type: bpy.props.StringProperty() + + def execute(self, context): + props = tool.Bsdd.get_bsdd_props() + core.search_bsdd_properties(tool.Bsdd, context.scene.BIMBSDDProperties.keyword, self.obj, self.obj_type) + self.report({"INFO"}, f"{len(props.properties)} bSDD properties found.") + return {"FINISHED"} + + +class AddBSDDProperties(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.add_bsdd_properties" + bl_label = "Add bSDD Properties" + bl_description = "Add selected bSDD properties" + bl_options = {"REGISTER", "UNDO"} + obj: bpy.props.StringProperty() + obj_type: bpy.props.StringProperty() + + def _execute(self, context): + self.file = tool.Ifc.get() + bprops = tool.Bsdd.get_bsdd_props() + + psets = {} + for selected_property in bprops.selected_properties: + psets.setdefault(selected_property.metadata, {})[selected_property.name] = selected_property.get_value() + + props = tool.Pset.get_pset_props(self.obj, self.obj_type) + ifc_definition_id = tool.Blender.get_obj_ifc_definition_id(self.obj, self.obj_type, context) + element = tool.Ifc.get().by_id(ifc_definition_id) + properties = {} + + current_psets = ifcopenshell.util.element.get_psets(element, verbose=True) + for pset_name, properties in psets.items(): + if pset := current_psets.get(pset_name, None): + pset = self.file.by_id(pset["id"]) + else: + pset = ifcopenshell.api.run("pset.add_pset", self.file, product=element, name=pset_name) + ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties=properties) diff --git a/src/bonsai/bonsai/bim/module/bsdd/prop.py b/src/bonsai/bonsai/bim/module/bsdd/prop.py index 0b2bfda6a1..bdf84fe8d9 100644 --- a/src/bonsai/bonsai/bim/module/bsdd/prop.py +++ b/src/bonsai/bonsai/bim/module/bsdd/prop.py @@ -17,6 +17,7 @@ # along with Bonsai. If not, see . import bpy +import bonsai.tool as tool from bpy.types import PropertyGroup from bonsai.bim.module.bsdd.data import BSDDData from bonsai.bim.prop import Attribute, StrProperty @@ -30,6 +31,7 @@ from bpy.props import ( FloatVectorProperty, CollectionProperty, ) +from typing import Union def get_active_dictionary(self, context): @@ -42,6 +44,15 @@ def update_is_active(self: "BSDDDictionary", context: bpy.types.Context) -> None BSDDData.data["active_dictionary"] = BSDDData.active_dictionary() +def update_is_selected(self: "BSDDProperty", context: bpy.types.Context) -> None: + tool.Bsdd.import_selected_properties() + + +def update_active_class_index(self: "BIMBSDDProperties", context: bpy.types.Context) -> None: + tool.Bsdd.import_class_properties() + BSDDData.data["active_dictionary"] = BSDDData.active_dictionary() + + class BSDDDictionary(PropertyGroup): name: StringProperty(name="Name") uri: StringProperty(name="URI") @@ -57,11 +68,21 @@ class BSDDDictionary(PropertyGroup): class BSDDClassification(PropertyGroup): name: StringProperty(name="Name") reference_code: StringProperty(name="Reference Code") - uri: StringProperty(name="Namespace URI") + uri: StringProperty(name="URI") dictionary_name: StringProperty(name="Dictionary Name") dictionary_namespace_uri: StringProperty(name="Dictionary Namespace URI") +class BSDDProperty(PropertyGroup): + name: StringProperty(name="Name") + code: StringProperty(name="Code") + uri: StringProperty(name="URI") + pset: StringProperty(name="Pset") + is_selected: BoolProperty( + name="Is Selected", description="Select to add or edit this property", default=False, update=update_is_selected + ) + + class BSDDPset(PropertyGroup): name: StringProperty(name="Name") properties: CollectionProperty(name="Properties", type=Attribute) @@ -75,6 +96,19 @@ class BIMBSDDProperties(PropertyGroup): active_dictionary_index: IntProperty(name="Active Dictionary Index") classifications: CollectionProperty(name="Classifications", type=BSDDClassification) active_classification_index: IntProperty(name="Active Classification Index") + property_filter_mode: EnumProperty( + name="Property Filter Mode", + items=[ + ("CLASS", "By Class", "Browse properties by class or group"), + ("KEYWORD", "By Keyword", "Search properties directly using a keyword"), + ], + default="CLASS", + ) + classes: CollectionProperty(name="Classes", type=BSDDClassification) + active_class_index: IntProperty(name="Active Class Index", update=update_active_class_index) + properties: CollectionProperty(name="Properties", type=BSDDProperty) + active_property_index: IntProperty(name="Active Property Index") + selected_properties: CollectionProperty(name="Selected Properties", type=Attribute) keyword: StringProperty(name="Keyword", description="Query for bsdd classes search, case and accent insensitive") should_filter_ifc_class: BoolProperty( name="Filter Active IFC Class", @@ -96,3 +130,7 @@ class BIMBSDDProperties(PropertyGroup): name="Load Test Dictionaries", description="Load dictionaries that are for testing only", default=False ) classification_psets: CollectionProperty(name="Classification Psets", type=BSDDPset) + + @property + def active_class(self) -> Union[BSDDClassification, None]: + return tool.Blender.get_active_uilist_element(self.classes, self.active_class_index) diff --git a/src/bonsai/bonsai/bim/module/bsdd/ui.py b/src/bonsai/bonsai/bim/module/bsdd/ui.py index e5fe13188c..6828948cfd 100644 --- a/src/bonsai/bonsai/bim/module/bsdd/ui.py +++ b/src/bonsai/bonsai/bim/module/bsdd/ui.py @@ -54,7 +54,7 @@ class BIM_PT_bsdd(Panel): selected_dictionary = None if selected_dictionary: - layout.label(text="Selected dictionary:") + layout.label(text="Selected Dictionary:") box = layout.box() row = box.row(align=True) row.label(text="Language") @@ -92,3 +92,22 @@ class BIM_UL_bsdd_classifications(UIList): row.label(text=item.reference_code) row.label(text=item.name) row.operator("bim.open_uri", text="", icon="URL").uri = item.uri + + +class BIM_UL_bsdd_classes(UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_propname): + if item: + row = layout.row(align=True) + row.label(text=item.name) + row.operator("bim.open_uri", text="", icon="URL").uri = item.uri + + +class BIM_UL_bsdd_properties(UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_propname): + if item: + row = layout.row(align=True) + row.label(text=item.name) + if item.pset: + row.label(text=item.pset) + row.operator("bim.open_uri", text="", icon="URL").uri = item.uri + row.prop(item, "is_selected", icon="CHECKBOX_HLT" if item.is_selected else "CHECKBOX_DEHLT", text="", emboss=False) diff --git a/src/bonsai/bonsai/bim/module/classification/data.py b/src/bonsai/bonsai/bim/module/classification/data.py index 35bf6900ec..bdc70acfb0 100644 --- a/src/bonsai/bonsai/bim/module/classification/data.py +++ b/src/bonsai/bonsai/bim/module/classification/data.py @@ -41,6 +41,7 @@ class ClassificationsData: cls.data["has_classification_file"] = cls.has_classification_file() cls.data["classifications"] = cls.classifications() cls.data["available_classifications"] = cls.available_classifications() + cls.data["classification_source"] = cls.classification_source() @classmethod def has_classification_file(cls): @@ -62,6 +63,19 @@ class ClassificationsData: return [] return [(str(e.id()), e.Name, "") for e in IfcStore.classification_file.by_type("IfcClassification")] + @classmethod + def classification_source(cls): + 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] + if dictionaries: + items.append(("BSDD", "All Active bSDDs", "")) + items.extend(dictionaries) + return items + class ReferencesData: @classmethod diff --git a/src/bonsai/bonsai/bim/module/classification/operator.py b/src/bonsai/bonsai/bim/module/classification/operator.py index 7dad93fc26..13d973a8a6 100644 --- a/src/bonsai/bonsai/bim/module/classification/operator.py +++ b/src/bonsai/bonsai/bim/module/classification/operator.py @@ -115,11 +115,12 @@ class AddClassificationFromBSDD(bpy.types.Operator, tool.Ifc.Operator): bl_options = {"REGISTER", "UNDO"} def _execute(self, context): - props = context.scene.BIMBSDDProperties - if props.active_dictionary == "ALL": - dictionaries = [d.uri for d in props.dictionaries if d.is_active] + cprops = context.scene.BIMClassificationProperties + bprops = tool.Bsdd.get_bsdd_props() + if cprops.classification_source == "BSDD": + dictionaries = [d.uri for d in bprops.dictionaries if d.is_active] else: - dictionaries = [props.active_dictionary] + dictionaries = [cprops.classification_source] for uri in dictionaries: if not (dictionary := tool.Bsdd.get_dictionary(uri)): continue diff --git a/src/bonsai/bonsai/bim/module/classification/prop.py b/src/bonsai/bonsai/bim/module/classification/prop.py index a39dfb74e5..0c09799285 100644 --- a/src/bonsai/bonsai/bim/module/classification/prop.py +++ b/src/bonsai/bonsai/bim/module/classification/prop.py @@ -44,6 +44,12 @@ def get_classifications(self, context): return ClassificationReferencesData.data["classifications"] +def get_classification_source(self, context): + if not ClassificationsData.is_loaded: + ClassificationsData.load() + return ClassificationsData.data["classification_source"] + + class ClassificationReference(PropertyGroup): name: StringProperty(name="Name") identification: StringProperty(name="Identification") @@ -54,15 +60,7 @@ class ClassificationReference(PropertyGroup): class BIMClassificationProperties(PropertyGroup): is_adding: BoolProperty(name="Is Adding", default=False) - classification_source: EnumProperty( - items=[ - ("FILE", "IFC File", ""), - ("BSDD", "buildingSMART Data Dictionary", ""), - ("MANUAL", "Manual Entry", ""), - ], - name="Classification Source", - default="FILE", - ) + classification_source: EnumProperty(items=get_classification_source, name="Classification Source") available_classifications: EnumProperty(items=get_available_classifications, name="Available Classifications") classification_attributes: CollectionProperty(name="Classification Attributes", type=Attribute) active_classification_id: IntProperty(name="Active Classification Id") diff --git a/src/bonsai/bonsai/bim/module/classification/ui.py b/src/bonsai/bonsai/bim/module/classification/ui.py index 7816828857..fc808a206b 100644 --- a/src/bonsai/bonsai/bim/module/classification/ui.py +++ b/src/bonsai/bonsai/bim/module/classification/ui.py @@ -54,10 +54,10 @@ class BIM_PT_classifications(Panel): if self.props.classification_source == "FILE": self.draw_add_file_ui(context) - elif self.props.classification_source == "BSDD": - self.draw_add_bsdd_ui(context) elif self.props.classification_source == "MANUAL": self.draw_add_manual_ui(context) + else: + self.draw_add_bsdd_ui(context) for classification in ClassificationsData.data["classifications"]: if self.props.active_classification_id == classification["id"]: @@ -76,11 +76,6 @@ class BIM_PT_classifications(Panel): row.operator("bim.enable_adding_manual_classification", text="Add Classification", icon="ADD") def draw_add_bsdd_ui(self, context): - self.bprops = context.scene.BIMBSDDProperties - - row = self.layout.row() - row.prop(self.bprops, "active_dictionary", text="") - row = self.layout.row() row.operator("bim.add_classification_from_bsdd", icon="ADD") @@ -139,10 +134,10 @@ class ReferenceUI: if self.sprops.classification_source == "FILE": self.draw_add_file_ui(context) - elif self.sprops.classification_source == "BSDD": - self.draw_add_bsdd_ui(context) elif self.sprops.classification_source == "MANUAL": self.draw_add_manual_ui(context) + else: + self.draw_add_bsdd_ui(context) def draw_add_manual_ui(self, context): row = self.layout.row() @@ -158,17 +153,11 @@ class ReferenceUI: row.operator("bim.enable_adding_manual_classification_reference", text="Add Reference", icon="ADD") def draw_add_bsdd_ui(self, context): - row = self.layout.row() - row.prop(self.bprops, "active_dictionary", text="") - row = self.layout.row(align=True) row.prop(self.bprops, "keyword", text="") + row.prop(self.bprops, "should_filter_ifc_class", text="", icon="FILTER") row.operator("bim.search_bsdd_classifications", text="", icon="VIEWZOOM") - row = self.layout.row() - row.prop(self.bprops, "should_filter_ifc_class") - row.prop(self.bprops, "use_only_ifc_properties") - if len(self.bprops.classifications): self.layout.template_list( "BIM_UL_bsdd_classifications", @@ -189,21 +178,6 @@ class ReferenceUI: ) op.obj = self.obj op.obj_type = self.obj_type - row.operator("bim.get_bsdd_classification_properties", text="", icon="COPY_ID") - - if len(self.bprops.classification_psets): - use_only_ifc_properties = self.bprops.use_only_ifc_properties - for pset in self.bprops.classification_psets: - properties = pset.properties - if use_only_ifc_properties: - properties = [p for p in properties if p.metadata == "IFC"] - if not properties: - continue - - box = self.layout.box() - row = box.row() - row.label(text=pset.name, icon="COPY_ID") - bonsai.bim.helper.draw_attributes(properties, box) def draw_add_file_ui(self, context): if not self.data.data["active_classification_library"]: diff --git a/src/bonsai/bonsai/bim/module/pset/prop.py b/src/bonsai/bonsai/bim/module/pset/prop.py index 81768c6f95..4533907cdb 100644 --- a/src/bonsai/bonsai/bim/module/pset/prop.py +++ b/src/bonsai/bonsai/bim/module/pset/prop.py @@ -81,7 +81,24 @@ def get_pset_name(self, context): results = get_profile_pset_names(self, context) elif prop_type == "WorkSchedulePsetProperties": results = get_work_schedule_pset_names(self, context) - return [("BBIM_CUSTOM", "Custom Pset", "Create a property set without using a template."), None] + results + items = [("BBIM_CUSTOM", "Custom Pset", "Create a property set without using a template.")] + bprops = tool.Bsdd.get_bsdd_props() + dictionaries = [(d.uri, f"bSDD: {d.name}", "") for d in bprops.dictionaries if d.is_active] + if dictionaries: + items.extend( + [ + None, + ( + "BBIM_BSDD", + "All Data Dictionaries", + "Manage properties from all active buildingSMART Data Dictionaries", + ), + ] + ) + items.extend(dictionaries) + items.append(None) + items.extend(results) + return items def get_object_pset_name(self, context): diff --git a/src/bonsai/bonsai/bim/module/pset/ui.py b/src/bonsai/bonsai/bim/module/pset/ui.py index 19d275430f..071d330b94 100644 --- a/src/bonsai/bonsai/bim/module/pset/ui.py +++ b/src/bonsai/bonsai/bim/module/pset/ui.py @@ -20,7 +20,7 @@ from __future__ import annotations import bpy import bonsai.tool as tool from bpy.types import Panel -from bonsai.bim.helper import prop_with_search, get_display_value +from bonsai.bim.helper import prop_with_search, get_display_value, draw_attribute from bonsai.bim.module.pset.data import ( ObjectPsetsData, ObjectQtosData, @@ -256,11 +256,74 @@ class BIM_PT_object_psets(Panel): ObjectPsetsData.load() props = context.active_object.PsetProperties + self.bprops = context.scene.BIMBSDDProperties row = self.layout.row(align=True) prop_with_search(row, props, "pset_name", text="") - op = row.operator("bim.add_pset", icon="ADD", text="") - op.obj = context.active_object.name - op.obj_type = "Object" + if props.pset_name != "BBIM_BSDD" and not props.pset_name.startswith(tool.Bsdd.identifier_url): + op = row.operator("bim.add_pset", icon="ADD", text="") + op.obj = context.active_object.name + op.obj_type = "Object" + else: + row = self.layout.row(align=True) + row.prop(self.bprops, "property_filter_mode", text="") + if self.bprops.property_filter_mode == "CLASS": + row.prop(self.bprops, "should_filter_ifc_class", text="", icon="FILTER") + op = row.operator("bim.import_bsdd_classes", text="", icon="FILE_REFRESH") + op.obj = context.active_object.name + op.obj_type = "Object" + + if len(self.bprops.classes): + self.layout.template_list( + "BIM_UL_bsdd_classes", + "", + self.bprops, + "classes", + self.bprops, + "active_class_index", + ) + if len(self.bprops.properties): + self.layout.template_list( + "BIM_UL_bsdd_properties", + "", + self.bprops, + "properties", + self.bprops, + "active_property_index", + ) + else: + row = self.layout.row() + row.label(text="No Results") + else: + row = self.layout.row() + row.label(text="No Results") + elif self.bprops.property_filter_mode == "KEYWORD": + row.prop(self.bprops, "keyword", text="") + op = row.operator("bim.search_bsdd_properties", text="", icon="VIEWZOOM") + op.obj = context.active_object.name + op.obj_type = "Object" + + if len(self.bprops.properties): + self.layout.template_list( + "BIM_UL_bsdd_properties", + "", + self.bprops, + "properties", + self.bprops, + "active_property_index", + ) + else: + row = self.layout.row() + row.label(text="No Results") + + for selected_property in self.bprops.selected_properties: + row = self.layout.row(align=True) + # row.prop(selected_property, "metadata", text="") + draw_attribute(selected_property, row) + + row = self.layout.row() + op = row.operator("bim.add_bsdd_properties", icon="ADD") + op.obj = context.active_object.name + op.obj_type = "Object" global_props = tool.Pset.get_global_pset_props() if not props.active_pset_id and props.active_pset_name and props.active_pset_type == "PSET": diff --git a/src/bonsai/bonsai/core/bsdd.py b/src/bonsai/bonsai/core/bsdd.py index 1523e08f81..5c16b7e9ec 100644 --- a/src/bonsai/bonsai/core/bsdd.py +++ b/src/bonsai/bonsai/core/bsdd.py @@ -26,14 +26,12 @@ if TYPE_CHECKING: import bonsai.tool as tool -def get_class_properties(bsdd: tool.Bsdd) -> dict[str, dict[str, Any]]: - bsdd.clear_class_psets() - data = bsdd.get_active_class_data() - pset_dict = bsdd.get_property_dict(data) - if pset_dict is None: - return {} - bsdd.create_class_psets(pset_dict) - return pset_dict +def import_bsdd_classes(bsdd: tool.Bsdd, obj, obj_type) -> int: + return bsdd.import_classes(obj, obj_type) + + +def search_bsdd_properties(bsdd: tool.Bsdd, keyword: str, obj, obj_type) -> int: + return bsdd.import_properties(obj, obj_type, keyword) def load_bsdd(bsdd: tool.Bsdd) -> None: @@ -41,7 +39,7 @@ def load_bsdd(bsdd: tool.Bsdd) -> None: bsdd.create_dictionaries(bsdd.get_dictionaries()) -def search_class(keyword: str, bsdd: tool.Bsdd) -> int: +def search_bsdd_class(bsdd: tool.Bsdd, keyword: str) -> int: bsdd.clear_classes() related_entities = bsdd.get_related_ifc_entities() return bsdd.search_class(keyword, related_entities) diff --git a/src/bonsai/bonsai/tool/bsdd.py b/src/bonsai/bonsai/tool/bsdd.py index bb5a79e1c6..9e196bae67 100644 --- a/src/bonsai/bonsai/tool/bsdd.py +++ b/src/bonsai/bonsai/tool/bsdd.py @@ -5,11 +5,15 @@ import json import bsdd import ifcopenshell.util.type import ifcopenshell.util.element +import ifcopenshell.util.classification from typing import Any, Union, Optional, TYPE_CHECKING class Bsdd(bonsai.core.tool.Bsdd): + identifier_url = "https://identifier.buildingsmart.org" client = bsdd.Client() + bsdd_classes: dict[str, dict] = {} + bsdd_properties: dict[str, dict] = {} @classmethod def get_bsdd_props(cls): @@ -23,6 +27,10 @@ class Bsdd(bonsai.core.tool.Bsdd): def clear_classes(cls) -> None: bpy.context.scene.BIMBSDDProperties.classifications.clear() + @classmethod + def clear_properties(cls) -> None: + bpy.context.scene.BIMBSDDProperties.properties.clear() + @classmethod def clear_dictionaries(cls) -> None: bpy.context.scene.BIMBSDDProperties.dictionaries.clear() @@ -94,10 +102,12 @@ class Bsdd(bonsai.core.tool.Bsdd): return list(filter(lambda d: d["status"] in statuses, dicts)) @classmethod - def get_property_dict(cls, class_data: Union[bsdd.ClassContractV1, dict]) -> Union[dict[str, dict[str, Any]], None]: + def get_class_properties( + cls, class_data: Union[bsdd.ClassContractV1, dict] + ) -> Union[dict[str, dict[str, Any]], None]: properties = class_data.get("classProperties", None) if not properties: - return None + return {} ifc_class = class_data.get("relatedIfcEntityNames") or "" if ifc_class: @@ -155,11 +165,12 @@ class Bsdd(bonsai.core.tool.Bsdd): limit: int = 100, should_paginate: bool = True, ): - props = cls.get_bsdd_props() + cprops = bpy.context.scene.BIMClassificationProperties + bprops = cls.get_bsdd_props() dictionary_uris = ( - [d.uri for d in props.dictionaries if d.is_active] - if props.active_dictionary == "ALL" - else [props.active_dictionary] + [d.uri for d in bprops.dictionaries if d.is_active] + if cprops.classification_source == "BSDD" + else [cprops.classification_source] ) for dictionary_uri in dictionary_uris: for related_ifc_entity in related_ifc_entities or [None]: @@ -174,7 +185,7 @@ class Bsdd(bonsai.core.tool.Bsdd): dictionary_name = response.get("name", "") dictionary_namespace_uri = response.get("uri", "") for _class in sorted(response.get("classes", []), key=lambda c: c["referenceCode"]): - prop = props.classifications.add() + prop = bprops.classifications.add() prop.name = _class["name"] prop.reference_code = _class["referenceCode"] prop.uri = _class["uri"] @@ -198,3 +209,159 @@ class Bsdd(bonsai.core.tool.Bsdd): @classmethod def should_filter_ifc_class(cls) -> bool: return bpy.context.scene.BIMBSDDProperties.should_filter_ifc_class + + @classmethod + def get_bsdd_class(cls, uri: str) -> dict: + if not (bsdd_class := cls.bsdd_classes.get(uri, {})): + bsdd_class = cls.client.get_class(uri) + cls.bsdd_classes[uri] = bsdd_class + return bsdd_class + + @classmethod + def get_bsdd_property(cls, uri: str) -> dict: + if not (bsdd_property := cls.bsdd_properties.get(uri, {})): + bsdd_property = cls.client.get_property(uri, include_classes=True) + cls.bsdd_properties[uri] = bsdd_property + return bsdd_property + + @classmethod + def import_classes(cls, obj, obj_type) -> None: + pprops = tool.Pset.get_pset_props(obj, obj_type) + props = cls.get_bsdd_props() + props.classes.clear() + + classes = set() + for obj in tool.Blender.get_selected_objects(include_active=True): + if element := tool.Ifc.get_entity(obj): + for reference in ifcopenshell.util.classification.get_references(element): + if (uri := reference.Location) and uri.startswith(cls.identifier_url): + classes.add((reference[1] or reference[2] or "Unnamed", uri)) + + dictionary_uris = ( + [d.uri for d in props.dictionaries if d.is_active] + if pprops.pset_name == "BBIM_BSDD" + else [pprops.pset_name] + ) + related_ifc_entities = cls.get_related_ifc_entities() + for dictionary_uri in dictionary_uris: + for related_ifc_entity in related_ifc_entities or [None]: + bsdd_classes = cls.client.get_classes( + dictionary_uri=dictionary_uri, + class_type="GroupOfProperties", + use_nested_classes=False, + related_ifc_entity=related_ifc_entity, + ) + for bsdd_class in bsdd_classes["classes"]: + classes.add((bsdd_class["name"], bsdd_class["uri"])) + + for bsdd_class in classes: + new = props.classes.add() + new.name = bsdd_class[0] + new.uri = bsdd_class[1] + + @classmethod + def import_class_properties(cls) -> None: + props = cls.get_bsdd_props() + props.properties.clear() + if not (active_class := props.active_class): + return + if not (bsdd_class := cls.get_bsdd_class(active_class.uri)): + return + for bsdd_prop in bsdd_class.get("classProperties", []): + if not bsdd_prop.get("propertySet", None): + continue + cls.bsdd_properties[bsdd_prop["uri"]] = bsdd_prop + new = props.properties.add() + new.name = bsdd_prop["name"] + new.pset = bsdd_prop["propertySet"] + new.uri = bsdd_prop["uri"] + + @classmethod + def import_properties(cls, obj, obj_type, keyword) -> None: + props = cls.get_bsdd_props() + props.properties.clear() + pprops = tool.Pset.get_pset_props(obj, obj_type) + dictionary_uris = ( + [d.uri for d in props.dictionaries if d.is_active] + if pprops.pset_name == "BBIM_BSDD" + else [pprops.pset_name] + ) + for dictionary_uri in dictionary_uris: + for bsdd_prop in cls.client.get_properties(dictionary_uri, keyword)["properties"]: + new = props.properties.add() + new.name = bsdd_prop["name"] + new.uri = bsdd_prop["uri"] + + @classmethod + def import_selected_properties(cls) -> None: + props = cls.get_bsdd_props() + data_type_map = { + "String": "string", + "Real": "float", + "Boolean": "boolean", + } + imported_props = set() + for bsdd_prop in props.properties: + if not bsdd_prop.is_selected: + continue + if not (pset_name := bsdd_prop.pset): + prop_data = cls.get_bsdd_property(bsdd_prop.uri) + pset_name = prop_data.get("propertyClasses", [{}])[0].get("propertySet", "") + + imported_props.add((pset_name, bsdd_prop.name)) + if ( + selected_property := props.selected_properties.get(bsdd_prop.name) + ) and selected_property.metadata == pset_name: + continue + + data = cls.bsdd_properties[bsdd_prop.uri] + + predefined_value = data.get("predefinedValue") + if predefined_value: + possible_values = [predefined_value] + else: + possible_values = data.get("allowedValues", []) or [] + possible_values = [v["value"] for v in possible_values] + + new = props.selected_properties.add() + new.name = bsdd_prop.name + if possible_values: + new.enum_items = json.dumps(possible_values) + new.data_type = "enum" + else: + new.data_type = data_type_map.get(data["dataType"], "string") + new.description = data.get("description", "") + new.metadata = pset_name + + to_remove = [] + for i, selected_property in enumerate(props.selected_properties): + if (selected_property.metadata, selected_property.name) not in imported_props: + to_remove.append(i) + + for i in to_remove[::-1]: + props.selected_properties.remove(i) + + @classmethod + def get_applicable_psets(cls, element: ifcopenshell.entity_instance): + uris = set() + for reference in ifcopenshell.util.classification.get_references(element): + if (uri := reference.Location) and uri.startswith(cls.identifier_url): + uris.add(uri) + psets = set() + for uri in uris: + if not (bsdd_class := cls.bsdd_classes.get(uri, None)): + continue + for class_pset in bsdd_class.get("classProperties", []): + if not (pset_name := class_pset.get("propertySet", None)): + continue + psets.add((uri, bsdd_class["name"], pset_name)) + return psets + + @classmethod + def is_applicable(cls, pset_uri: str, element: ifcopenshell.entity_instance) -> bool: + uris = set() + for reference in ifcopenshell.util.classification.get_references(element): + if (uri := reference.Location) and uri.startswith(cls.identifier_url): + uris.add(uri) + class_uri, pset_name = pset_uri.rsplit("#", 1) + return class_uri in uris diff --git a/src/bonsai/bonsai/tool/pset.py b/src/bonsai/bonsai/tool/pset.py index d979fdb1ac..851c756461 100644 --- a/src/bonsai/bonsai/tool/pset.py +++ b/src/bonsai/bonsai/tool/pset.py @@ -73,7 +73,7 @@ class Pset(bonsai.core.tool.Pset): def get_pset_name(cls, obj: str, obj_type: tool.Ifc.OBJECT_TYPE, pset_type: PSET_TYPE = "PSET") -> str: props = cls.get_pset_props(obj, obj_type) name = props.pset_name if pset_type == "PSET" else props.qto_name - if name == "BBIM_CUSTOM": + if name in ("BBIM_CUSTOM", "BBIM_BSDD"): return "" return name diff --git a/src/bsdd/bsdd.py b/src/bsdd/bsdd.py index f634a422d1..eeacb08879 100644 --- a/src/bsdd/bsdd.py +++ b/src/bsdd/bsdd.py @@ -658,13 +658,25 @@ class Client: return self.get(endpoint, params) def get_properties( - self, dictionary_uri: str, offset: int = 0, limit: int = 100, language_code: str = "", version: int = 1 + self, + dictionary_uri: str, + search_text: str = "", + offset: int = 0, + limit: int = 100, + language_code: str = "", + version: int = 1, ) -> DictionaryPropertiesResponseContractV1: """ Get Dictionary with its properties """ endpoint = f"Dictionary/v{version}/Properties" - params = {"Uri": dictionary_uri, "languageCode": language_code, "offset": offset, "limit": limit} + params = { + "Uri": dictionary_uri, + "SearchText": search_text, + "languageCode": language_code, + "offset": offset, + "limit": limit, + } return self.get(endpoint, params) def get_class( diff --git a/src/ifcopenshell-python/ifcopenshell/util/classification.py b/src/ifcopenshell-python/ifcopenshell/util/classification.py index 1c2112036f..06f9b0e31a 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/classification.py +++ b/src/ifcopenshell-python/ifcopenshell/util/classification.py @@ -21,6 +21,12 @@ from typing import Optional def get_references(element: ifcopenshell.entity_instance, should_inherit=True) -> set[ifcopenshell.entity_instance]: + """Gets classification references associated with the element + + :param should_inherit: If true, classification references are inherited + from the type. Classifications can be overriden per system. + :return: A set of IfcClassificationReference + """ results = set() if not element.is_a("IfcRoot"): if (references := getattr(element, "HasExternalReferences", None)) is not None or ( @@ -52,6 +58,11 @@ def get_references(element: ifcopenshell.entity_instance, should_inherit=True) - def get_classification(reference: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance: + """Get the IfcClassification that a classification reference belongs to + + :param reference: An IfcClassificationReference + :return: IfcClassification + """ if reference.is_a("IfcClassification"): return reference return get_classification(reference.ReferencedSource) if reference.ReferencedSource is not None else None