From 4cfe1f96f8038552065128a06f5e09dc5794c2d4 Mon Sep 17 00:00:00 2001 From: Richard Brice <37087370+RickBrice@users.noreply.github.com> Date: Tue, 11 Aug 2026 14:47:44 -0700 Subject: [PATCH] Gets the pick list for bSDD properties with Allowed Values working --- src/bonsai/bonsai/tool/bsdd.py | 43 +++++++++++++++++++++++++++++++--- src/bonsai/bonsai/tool/pset.py | 23 ++++++++++++++++++ src/bsdd/bsdd.py | 2 +- 3 files changed, 64 insertions(+), 4 deletions(-) diff --git a/src/bonsai/bonsai/tool/bsdd.py b/src/bonsai/bonsai/tool/bsdd.py index 72aba9d7b2..78d9080c1a 100644 --- a/src/bonsai/bonsai/tool/bsdd.py +++ b/src/bonsai/bonsai/tool/bsdd.py @@ -170,7 +170,7 @@ class Bsdd(bonsai.core.tool.Bsdd): psets.setdefault(pset, {}) predefined_value = prop.get("predefinedValue") - if predefined_value: + if predefined_value and predefined_value != "None": possible_values = [predefined_value] else: possible_values = prop.get("allowedValues", []) or [] @@ -369,7 +369,7 @@ class Bsdd(bonsai.core.tool.Bsdd): data = cls.bsdd_properties[bsdd_prop.uri] predefined_value = data.get("predefinedValue") - if predefined_value: + if predefined_value and predefined_value != "None": possible_values = [predefined_value] else: possible_values = data.get("allowedValues", []) or [] @@ -401,7 +401,13 @@ class Bsdd(bonsai.core.tool.Bsdd): uris.add(uri) psets = set() for uri in uris: - if not (bsdd_class := cls.bsdd_classes.get(uri, None)): + try: + # Cache may not be populated yet (e.g. a fresh session that never + # browsed this class), so fetch on a cache miss instead of skipping. + bsdd_class = cls.get_bsdd_class(uri) + except Exception: + continue + if not bsdd_class: continue for class_pset in bsdd_class.get("classProperties", []): if not (pset_name := class_pset.get("propertySet", None)): @@ -409,6 +415,37 @@ class Bsdd(bonsai.core.tool.Bsdd): psets.add((uri, bsdd_class["name"], pset_name)) return psets + @classmethod + def get_bsdd_pset_property_values( + cls, element: ifcopenshell.entity_instance, pset_name: str + ) -> dict[str, list[str]]: + """Map bSDD property code -> allowed/predefined values for properties bSDD says + are applicable to `element` (via its classification references) under `pset_name`. + + Used to recognise a Pset property as bSDD-sourced even outside the dedicated + bSDD add-property flow (e.g. a Pset created in a previous session), so it can + still be edited as a picklist. + """ + result: dict[str, list[str]] = {} + for uri, class_name, applicable_pset_name in cls.get_applicable_psets(element): + if applicable_pset_name != pset_name: + continue + bsdd_class = cls.get_bsdd_class(uri) + for class_prop in bsdd_class.get("classProperties", []): + if class_prop.get("propertySet") != pset_name: + continue + code = class_prop.get("propertyCode") + if not code or code in result: + continue + predefined_value = class_prop.get("predefinedValue") + if predefined_value and predefined_value != "None": + result[code] = [predefined_value] + continue + allowed_values = class_prop.get("allowedValues", []) or [] + if allowed_values: + result[code] = [v["value"] for v in allowed_values] + return result + @classmethod def is_applicable(cls, pset_uri: str, element: ifcopenshell.entity_instance) -> bool: uris = set() diff --git a/src/bonsai/bonsai/tool/pset.py b/src/bonsai/bonsai/tool/pset.py index 47d5155eee..2a4d350e14 100644 --- a/src/bonsai/bonsai/tool/pset.py +++ b/src/bonsai/bonsai/tool/pset.py @@ -223,6 +223,18 @@ class Pset(bonsai.core.tool.Pset): elif pset.is_a("IfcMaterialProperties") or pset.is_a("IfcProfileProperties"): pset_props = pset.Properties + # If this Pset's owning element is classified against a bSDD class that defines + # this Pset, recognise properties matching bSDD property codes as picklists, + # even though the Pset wasn't necessarily created through the bSDD add-property UI. + bsdd_allowed_values: dict[str, list[str]] = {} + if pset.is_a("IfcPropertySet"): + elements = ifcopenshell.util.element.get_elements_by_pset(pset) + if elements: + try: + bsdd_allowed_values = tool.Bsdd.get_bsdd_pset_property_values(next(iter(elements)), pset.Name) + except Exception: + pass + prop_templates: dict[str, ifcopenshell.entity_instance] = {} if pset_template: prop_templates = {prop.Name: prop for prop in pset_template.HasPropertyTemplates} @@ -286,6 +298,17 @@ class Pset(bonsai.core.tool.Pset): metadata.set_value(metadata.get_value_default() if metadata.is_null else value) process_prop_description(metadata) + if prop.is_a("IfcPropertySingleValue") and (possible_values := bsdd_allowed_values.get(prop.Name)): + str_value = None if value is None else str(value) + if str_value is not None and str_value not in possible_values: + # Preserve a legacy/imported value that doesn't match the current + # bSDD enumeration instead of silently dropping it. + possible_values = [*possible_values, str_value] + metadata.enum_items = json.dumps(possible_values) + metadata.data_type = "enum" + if str_value is not None: + metadata.enum_value = str_value + @classmethod def get_prop_template_primitive_type(cls, prop_template: ifcopenshell.entity_instance) -> str: if prop_template.TemplateType in ["Q_LENGTH", "Q_AREA", "Q_VOLUME", "Q_WEIGHT", "Q_TIME"]: diff --git a/src/bsdd/bsdd.py b/src/bsdd/bsdd.py index 466067e53a..95322744a9 100644 --- a/src/bsdd/bsdd.py +++ b/src/bsdd/bsdd.py @@ -1026,7 +1026,7 @@ def apply_ifc_classification_properties( psets = ifcopenshell.util.element.get_psets(element) for prop in classificationProperties: predefinedValue = prop.get("predefinedValue") - if not predefinedValue or prop.get("propertyDomainName") != "IFC": + if not predefinedValue or predefinedValue == "None" or prop.get("propertyDomainName") != "IFC": continue pset = psets.get(prop["propertySet"]) if pset: