From 4f047274bf2cc7011bee920af414598a13fe595e Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Fri, 7 Jan 2022 16:09:54 +1100 Subject: [PATCH] The IfcOpenShell API now supports automatic primitive casting for editing properties. See #1943. --- .../blenderbim/bim/module/pset/operator.py | 3 +- src/blenderbim/blenderbim/bim/prop.py | 56 ++++++++++--------- .../ifcopenshell/api/pset/edit_pset.py | 35 ++++++++++-- .../test/api/pset/test_edit_pset.py | 16 ++++++ 4 files changed, 77 insertions(+), 33 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/pset/operator.py b/src/blenderbim/blenderbim/bim/module/pset/operator.py index ba2669a340..a5520d3815 100644 --- a/src/blenderbim/blenderbim/bim/module/pset/operator.py +++ b/src/blenderbim/blenderbim/bim/module/pset/operator.py @@ -148,7 +148,6 @@ class EnablePsetEditing(bpy.types.Operator): new.is_null = data.get(prop_template.Name, None) is None new.is_optional = True new.data_type = data_type - new.enum_data_type = prop_template.PrimaryMeasureType if data_type == "string": new.string_value = "" if new.is_null else data[prop_template.Name] @@ -161,7 +160,7 @@ class EnablePsetEditing(bpy.types.Operator): elif data_type == "enum": new.enum_items = json.dumps(enum_items) if data.get(prop_template.Name): - new.enum_value = data[prop_template.Name] + new.enum_value = str(data[prop_template.Name]) def load_from_pset_data(self, pset_data): for prop_id in pset_data["Properties"]: diff --git a/src/blenderbim/blenderbim/bim/prop.py b/src/blenderbim/blenderbim/bim/prop.py index c69170704d..4145642be1 100644 --- a/src/blenderbim/blenderbim/bim/prop.py +++ b/src/blenderbim/blenderbim/bim/prop.py @@ -82,35 +82,45 @@ def update_is_visible(self, context): pass -def InternStr(s): - if not hasattr(InternStr, "StringCache"): # Another way to define a function attribute - InternStr.StringCache = defaultdict(str) - InternStr.StringCache[s] = s - return InternStr.StringCache[s] +# If we don't cache strings, accents get mangled due to a Blender bug +# https://blender.stackexchange.com/questions/216230/is-there-a-workaround-for-the-known-bug-in-dynamic-enumproperty +# https://github.com/IfcOpenShell/IfcOpenShell/pull/1945 +# https://github.com/IfcOpenShell/IfcOpenShell/issues/1941 +def cache_string(s): + s = str(s) + if not hasattr(cache_string, "data"): # Another way to define a function attribute + cache_string.data = defaultdict(str) + cache_string.data[s] = s + return cache_string.data[s] -InternStr.StringCache = {} + +cache_string.data = {} def getAttributeEnumValues(prop, context): # Support weird buildingSMART dictionary mappings which behave like enums items = [] data = json.loads(prop.enum_items) - + if isinstance(data, dict): for k, v in data.items(): - items.append(( - InternStr(k), - InternStr(v), - "", - )) + items.append( + ( + cache_string(k), + cache_string(v), + "", + ) + ) else: for e in data: - items.append(( - InternStr(e), - InternStr(e), - "", - )) - + items.append( + ( + cache_string(e), + cache_string(e), + "", + ) + ) + return items @@ -173,17 +183,11 @@ class Attribute(PropertyGroup): is_optional: BoolProperty(name="Is Optional") enum_items: StringProperty(name="Value") enum_value: EnumProperty(items=getAttributeEnumValues, name="Value", update=updateAttributeValue) - enum_data_type: StringProperty(name="Enum Data Type") - + def get_value(self): if self.is_null: return None - if self.data_type == "enum": - type_map = blenderbim.bim.schema.ifc.type_map - type_fn = {'integer':int, 'string':str, 'float':float, 'bool': bool}[type_map[self.enum_data_type]] - return type_fn(self.enum_value) - else: - return getattr(self, str(self.get_value_name()), None) + return getattr(self, str(self.get_value_name()), None) def get_value_default(self): if self.data_type == "string": diff --git a/src/ifcopenshell-python/ifcopenshell/api/pset/edit_pset.py b/src/ifcopenshell-python/ifcopenshell/api/pset/edit_pset.py index 7a73bcaade..b40be21d00 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/pset/edit_pset.py +++ b/src/ifcopenshell-python/ifcopenshell/api/pset/edit_pset.py @@ -41,7 +41,10 @@ class Usecase: elif isinstance(value, ifcopenshell.entity_instance): prop.NominalValue = value else: - primary_measure_type = self.get_primary_measure_type(prop.Name, old_value=prop.NominalValue, new_value=value) + primary_measure_type = self.get_primary_measure_type( + prop.Name, old_value=prop.NominalValue, new_value=value + ) + value = self.cast_value_to_primary_measure_type(value, primary_measure_type) prop.NominalValue = self.file.create_entity(primary_measure_type, value) del self.settings["properties"][prop.Name] @@ -50,13 +53,16 @@ class Usecase: for name, value in self.settings["properties"].items(): if value is None: continue - primary_measure_type = self.get_primary_measure_type(name, new_value=value) - if hasattr(value, "is_a"): - value = value.wrappedValue + if isinstance(value, ifcopenshell.entity_instance): + nominal_value = value + else: + primary_measure_type = self.get_primary_measure_type(name, new_value=value) + value = self.cast_value_to_primary_measure_type(value, primary_measure_type) + nominal_value = self.file.create_entity(primary_measure_type, value) properties.append( self.file.create_entity( "IfcPropertySingleValue", - **{"Name": name, "NominalValue": self.file.create_entity(primary_measure_type, value)}, + **{"Name": name, "NominalValue": nominal_value}, ) ) return properties @@ -94,3 +100,22 @@ class Usecase: return "IfcBoolean" elif isinstance(new_value, int): return "IfcInteger" + + def cast_value_to_primary_measure_type(self, value, primary_measure_type): + type_str = self.file.create_entity(primary_measure_type).attribute_type(0) + type_fn = { + "AGGREGATE OF DOUBLE": list, + "AGGREGATE OF INT": list, + "AGGREGATE OF ENTITY INSTANCE": list, + "BINARY": bytes, + "LOGICAL": str, + "BOOL": bool, + "INT": int, + "DOUBLE": float, + "STRING": str, + }[type_str] + if type_str == "AGGREGATE OF DOUBLE": + return [float(i) for i in value] + elif type_str == "AGGREGATE OF INT": + return [int(i) for i in value] + return type_fn(value) diff --git a/src/ifcopenshell-python/test/api/pset/test_edit_pset.py b/src/ifcopenshell-python/test/api/pset/test_edit_pset.py index 4bcd0e354c..72d5e39e47 100644 --- a/src/ifcopenshell-python/test/api/pset/test_edit_pset.py +++ b/src/ifcopenshell-python/test/api/pset/test_edit_pset.py @@ -49,6 +49,22 @@ class TestEditPset(test.bootstrap.IFC4): assert pset.HasProperties[1].Name == "Status" assert pset.HasProperties[1].NominalValue is None + def test_editing_a_templated_pset_with_automatic_casting_of_primitive_data_types(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + pset = ifcopenshell.api.run("pset.add_pset", self.file, product=element, name="Pset_WallCommon") + ifcopenshell.api.run( + "pset.edit_pset", + self.file, + pset=pset, + properties={"ThermalTransmittance": "42"}, + ) + ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"Reference": "bar", "Status": None}) + pset = element.IsDefinedBy[0].RelatingPropertyDefinition + + assert pset.HasProperties[0].Name == "ThermalTransmittance" + assert pset.HasProperties[0].NominalValue.is_a("IfcThermalTransmittanceMeasure") + assert pset.HasProperties[0].NominalValue.wrappedValue == 42 + def test_not_adding_a_property_if_it_is_none(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") pset = ifcopenshell.api.run("pset.add_pset", self.file, product=element, name="Pset_WallCommon")