From 5334d2b754dbbf39a241795c446ab8c010c4ad64 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Thu, 13 May 2021 11:55:25 +1000 Subject: [PATCH] Refactor pset module to follow the same design patterns as other modules, in preparation for complex property / quantity handling in the future --- .../blenderbim/bim/module/pset/operator.py | 140 ++++++++++---- .../blenderbim/bim/module/pset/ui.py | 54 +++--- .../ifcopenshell/api/pset/data.py | 171 +++++------------- 3 files changed, 173 insertions(+), 192 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/pset/operator.py b/src/blenderbim/blenderbim/bim/module/pset/operator.py index c5e53561fd..ca67ed2fd9 100644 --- a/src/blenderbim/blenderbim/bim/module/pset/operator.py +++ b/src/blenderbim/blenderbim/bim/module/pset/operator.py @@ -2,6 +2,8 @@ import bpy import json import ifcopenshell.api import ifcopenshell.util.unit +import ifcopenshell.util.pset +import ifcopenshell.util.attribute from blenderbim.bim.ifc import IfcStore from ifcopenshell.api.pset.data import Data from blenderbim.bim.module.pset.qto_calculator import QtoCalculator @@ -31,33 +33,100 @@ class EnablePsetEditing(bpy.types.Operator): obj = bpy.data.objects.get(self.obj) elif self.obj_type == "Material": obj = bpy.data.materials.get(self.obj) - props = obj.PsetProperties + self.props = obj.PsetProperties - while len(props.properties) > 0: - props.properties.remove(0) + while len(self.props.properties) > 0: + self.props.properties.remove(0) data = Data.psets if self.pset_id in Data.psets else Data.qtos - props.active_pset_name = data[self.pset_id]["Name"] - for prop in data[self.pset_id]["Properties"]: - new = props.properties.add() - new.name = prop["Name"] - new.is_null = prop["is_null"] - if prop["type"] == "string": - new.string_value = prop["value"] or "" - elif prop["type"] == "integer": - new.int_value = prop["value"] or 0 - elif prop["type"] == "float": - new.float_value = prop["value"] or 0.0 - elif prop["type"] == "boolean": - new.bool_value = prop["value"] or False - elif prop["type"] == "enum": - new.enum_items = json.dumps(prop["enum_items"]) - if prop["value"]: - new.enum_value = prop["value"] + pset_data = data[self.pset_id] + self.props.active_pset_name = pset_data["Name"] - props.active_pset_id = self.pset_id + templates = ifcopenshell.util.pset.get_template("IFC4") + pset_template = templates.get_by_name(pset_data["Name"]) + + if pset_template: + self.load_from_pset_template(pset_template, pset_data) + else: + self.load_from_pset_data(pset_data) + + self.props.active_pset_id = self.pset_id return {"FINISHED"} + def load_from_pset_template(self, pset_template, pset_data): + data = {Data.properties[p]["Name"]: Data.properties[p]["NominalValue"] for p in pset_data["Properties"]} + for prop_template in pset_template.HasPropertyTemplates: + if not prop_template.is_a("IfcSimplePropertyTemplate"): + continue # Other types not yet supported + + if prop_template.TemplateType == "P_SINGLEVALUE": + try: + data_type = ifcopenshell.util.attribute.get_primitive_type( + IfcStore.get_schema().declaration_by_name(prop_template.PrimaryMeasureType or "IfcLabel") + ) + except: + # TODO: Occurs if the data type is something that exists in IFC4 and not in IFC2X3. To fully fix + # this we need to generate the IFC2X3 pset template definitions. + continue + elif prop_template.TemplateType == "P_ENUMERATEDVALUE": + data_type = "enum" + enum_items = [v.wrappedValue for v in prop_template.Enumerators.EnumerationValues] + elif prop_template.TemplateType in ["Q_LENGTH", "Q_AREA", "Q_VOLUME", "Q_WEIGHT", "Q_TIME"]: + data_type = "float" + elif prop_template.TemplateType == "Q_COUNT": + data_type = "integer" + else: + continue # Other types not yet supported + + new = self.props.properties.add() + new.name = prop_template.Name + new.is_null = data.get(prop_template.Name, None) is None + new.data_type = data_type + + if data_type == "string": + new.string_value = "" if new.is_null else data[prop_template.Name] + elif data_type == "integer": + new.int_value = 0 if new.is_null else data[prop_template.Name] + elif data_type == "float": + new.float_value = 0.0 if new.is_null else data[prop_template.Name] + elif data_type == "boolean": + new.bool_value = False if new.is_null else data[prop_template.Name] + elif data_type == "enum": + new.enum_items = json.dumps(enum_items) + if data.get(prop_template.Name): + new.enum_value = data[prop_template.Name] + + def load_from_pset_data(self, pset_data): + for prop_id in pset_data["Properties"]: + prop = Data.properties[prop_id] + + value = prop["NominalValue"] + if isinstance(value, str): + data_type = "string" + elif isinstance(value, float): + data_type = "float" + elif isinstance(value, bool): + data_type = "boolean" + elif isinstance(value, int): + data_type = "integer" + else: + data_type = "string" + value = str(value) + + new = self.props.properties.add() + new.name = prop["Name"] + new.is_null = prop["NominalValue"] is None + new.data_type = data_type + + if data_type == "string": + new.string_value = "" if new.is_null else value + elif data_type == "integer": + new.int_value = 0 if new.is_null else value + elif data_type == "float": + new.float_value = 0.0 if new.is_null else value + elif data_type == "boolean": + new.bool_value = False if new.is_null else value + class DisablePsetEditing(bpy.types.Operator): bl_idname = "bim.disable_pset_editing" @@ -98,22 +167,19 @@ class EditPset(bpy.types.Operator): properties = json.loads(self.properties) else: data = Data.psets if pset_id in Data.psets else Data.qtos - for prop in data[pset_id]["Properties"]: - blender_prop = props.properties.get(prop["Name"]) - if not blender_prop: - continue - if blender_prop.is_null: - properties[prop["Name"]] = None - elif prop["type"] == "string": - properties[prop["Name"]] = blender_prop.string_value - elif prop["type"] == "boolean": - properties[prop["Name"]] = blender_prop.bool_value - elif prop["type"] == "integer": - properties[prop["Name"]] = blender_prop.int_value - elif prop["type"] == "float": - properties[prop["Name"]] = blender_prop.float_value - elif prop["type"] == "enum": - properties[prop["Name"]] = blender_prop.enum_value + for prop in props.properties: + if prop.is_null: + properties[prop.name] = None + elif prop.data_type == "string": + properties[prop.name] = prop.string_value + elif prop.data_type == "boolean": + properties[prop.name] = prop.bool_value + elif prop.data_type == "integer": + properties[prop.name] = prop.int_value + elif prop.data_type == "float": + properties[prop.name] = prop.float_value + elif prop.data_type == "enum": + properties[prop.name] = prop.enum_value if pset_id in Data.psets: ifcopenshell.api.run( diff --git a/src/blenderbim/blenderbim/bim/module/pset/ui.py b/src/blenderbim/blenderbim/bim/module/pset/ui.py index d6f5110a74..fd63268a90 100644 --- a/src/blenderbim/blenderbim/bim/module/pset/ui.py +++ b/src/blenderbim/blenderbim/bim/module/pset/ui.py @@ -6,6 +6,8 @@ from blenderbim.bim.ifc import IfcStore def draw_psetqto_ui(context, pset_id, pset, props, layout, obj_type): box = layout.box() row = box.row(align=True) + if "is_expanded" not in pset: + pset["is_expanded"] = True icon = "TRIA_DOWN" if pset["is_expanded"] else "TRIA_RIGHT" row.operator("bim.toggle_pset_expansion", icon=icon, text="", emboss=False).pset_id = pset_id if not props.active_pset_id: @@ -34,20 +36,21 @@ def draw_psetqto_ui(context, pset_id, pset, props, layout, obj_type): op.obj_type = obj_type if pset["is_expanded"]: if props.active_pset_id == pset_id: - for prop in pset["Properties"]: + for prop in props.properties: draw_psetqto_editable_ui(box, props, prop) else: has_props_displayed = False - for prop in pset["Properties"]: + for prop_id in pset["Properties"]: + prop = Data.properties[prop_id] if context.preferences.addons["blenderbim"].preferences.should_hide_empty_props and ( - prop["value"] is None or prop["value"] == "" + prop["NominalValue"] is None or prop["NominalValue"] == "" ): continue has_props_displayed = True row = box.row(align=True) row.scale_y = 0.8 row.label(text=prop["Name"]) - row.label(text=str(prop["value"])) + row.label(text=str(prop["NominalValue"])) if not has_props_displayed: row = box.row() row.scale_y = 0.8 @@ -56,33 +59,32 @@ def draw_psetqto_ui(context, pset_id, pset, props, layout, obj_type): def draw_psetqto_editable_ui(box, props, prop): row = box.row(align=True) - blender_prop = props.properties.get(prop["Name"]) - if prop["type"] == "string": - row.prop(blender_prop, "string_value", text=prop["Name"]) - elif prop["type"] == "integer": - row.prop(blender_prop, "int_value", text=prop["Name"]) - elif prop["type"] == "float": - row.prop(blender_prop, "float_value", text=prop["Name"]) - elif prop["type"] == "boolean": - row.prop(blender_prop, "bool_value", text=prop["Name"]) - elif prop["type"] == "enum": - row.prop(blender_prop, "enum_value", text=prop["Name"]) - row.prop(blender_prop, "is_null", icon="RADIOBUT_OFF" if blender_prop.is_null else "RADIOBUT_ON", text="") + if prop.data_type == "string": + row.prop(prop, "string_value", text=prop.name) + elif prop.data_type == "integer": + row.prop(prop, "int_value", text=prop.name) + elif prop.data_type == "float": + row.prop(prop, "float_value", text=prop.name) + elif prop.data_type == "boolean": + row.prop(prop, "bool_value", text=prop.name) + elif prop.data_type == "enum": + row.prop(prop, "enum_value", text=prop.name) + row.prop(prop, "is_null", icon="RADIOBUT_OFF" if prop.is_null else "RADIOBUT_ON", text="") if ( - "length" in prop["Name"].lower() - or "width" in prop["Name"].lower() - or "height" in prop["Name"].lower() - or "depth" in prop["Name"].lower() - or "perimeter" in prop["Name"].lower() + "length" in prop.name.lower() + or "width" in prop.name.lower() + or "height" in prop.name.lower() + or "depth" in prop.name.lower() + or "perimeter" in prop.name.lower() ): op = row.operator("bim.guess_quantity", icon="IPO_EASE_IN_OUT", text="") - op.prop = prop["Name"] - elif "area" in prop["Name"].lower(): + op.prop = prop.name + elif "area" in prop.name.lower(): op = row.operator("bim.guess_quantity", icon="MESH_CIRCLE", text="") - op.prop = prop["Name"] - elif "volume" in prop["Name"].lower(): + op.prop = prop.name + elif "volume" in prop.name.lower(): op = row.operator("bim.guess_quantity", icon="SPHERE", text="") - op.prop = prop["Name"] + op.prop = prop.name class BIM_PT_object_psets(Panel): diff --git a/src/ifcopenshell-python/ifcopenshell/api/pset/data.py b/src/ifcopenshell-python/ifcopenshell/api/pset/data.py index d76a350360..e68f9394ef 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/pset/data.py +++ b/src/ifcopenshell-python/ifcopenshell/api/pset/data.py @@ -7,18 +7,17 @@ class Data: products = {} psets = {} qtos = {} + properties = {} @classmethod def purge(cls): cls.products = {} cls.psets = {} cls.qtos = {} + cls.properties = {} @classmethod def load(cls, file, product_id): - cls._file = file - cls._psetqto = ifcopenshell.util.pset.get_template("IFC4") - cls._schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(file.schema) if not file: return product = file.by_id(product_id) @@ -59,133 +58,47 @@ class Data: @classmethod def add_pset(cls, pset, product_id): - new_pset = { - "Name": pset.Name, - "is_expanded": True, - "Properties": cls.get_properties_from_template(pset.Name) or [], - } - cls.products[product_id]["psets"].add(int(pset.id())) - cls.psets[int(pset.id())] = new_pset - try: - if hasattr(pset, "HasProperties"): - props = pset.HasProperties - elif hasattr(pset, "Properties"): # For IfcMaterialProperties - props = pset.Properties - except: - return # I've seen ArchiCAD produce invalid IFCs with empty data - # Invalid IFC, but some vendors like Solidworks do this so we accomodate it - if not props: - return + data = pset.get_info() + del data["OwnerHistory"] + del data["HasProperties"] + if hasattr(pset, "HasProperties"): + props = pset.HasProperties or [] + elif hasattr(pset, "Properties"): + props = pset.Properties or [] + # TODO: support more than single values + data["Properties"] = [p.id() for p in props if p.is_a("IfcPropertySingleValue")] + cls.psets[pset.id()] = data + cls.products[product_id]["psets"].add(pset.id()) for prop in props: - if prop.is_a("IfcPropertySingleValue") and prop.NominalValue: - has_existing_prop = False - for existing_prop in new_pset["Properties"]: - if existing_prop["Name"] == prop.Name: - if prop.NominalValue is None: - existing_prop["value"] = None - elif existing_prop["type"] == "string": - existing_prop["value"] = str(prop.NominalValue.wrappedValue) - elif existing_prop["type"] == "float": - existing_prop["value"] = float(prop.NominalValue.wrappedValue) - elif existing_prop["type"] == "integer": - existing_prop["value"] = int(prop.NominalValue.wrappedValue) - elif existing_prop["type"] == "boolean": - existing_prop["value"] = bool(prop.NominalValue.wrappedValue) - elif existing_prop["type"] == "enum": - existing_prop["value"] = str(prop.NominalValue.wrappedValue) - existing_prop["is_null"] = prop.NominalValue.wrappedValue is None - has_existing_prop = True - break - if not has_existing_prop: - value = prop.NominalValue.wrappedValue - if isinstance(value, str): - data_type = "string" - elif isinstance(value, float): - data_type = "float" - elif isinstance(value, bool): - data_type = "boolean" - elif isinstance(value, int): - data_type = "integer" - else: - data_type = "string" - value = str(value) - new_pset["Properties"].append( - { - "Name": prop.Name, - "value": value, - "type": data_type, - "enum_items": [], - "is_null": prop.NominalValue.wrappedValue is None, - } - ) + # TODO: support more than single values + if prop.is_a("IfcPropertySingleValue"): + cls.load_prop(prop) + + @classmethod + def load_prop(cls, prop): + data = prop.get_info() + if prop.is_a("IfcProperty"): + # TODO: support units + del data["Unit"] + if prop.NominalValue is not None: + data["NominalValue"] = prop.NominalValue.wrappedValue + elif prop.is_a("IfcPhysicalQuantity"): + # TODO: support units + del data["Unit"] + # For convenience, which trumps correctness in this case + data["NominalValue"] = prop[3] + cls.properties[prop.id()] = data @classmethod def add_qto(cls, qto, product_id): - new_qto = { - "Name": qto.Name, - "is_expanded": True, - "Properties": cls.get_properties_from_template(qto.Name) or [], - } - cls.products[product_id]["qtos"].add(int(qto.id())) - cls.qtos[int(qto.id())] = new_qto - for prop in qto.Quantities or []: - if prop.is_a("IfcPhysicalSimpleQuantity"): - value = prop[3] - has_existing_prop = False - for existing_prop in new_qto["Properties"]: - if existing_prop["Name"] == prop.Name: - existing_prop["value"] = float(value) - existing_prop["is_null"] = value is None - has_existing_prop = True - break - if not has_existing_prop: - new_qto["Properties"].append( - { - "Name": prop.Name, - "value": float(value), - "type": "float", - "enum_items": [], - "is_null": value is None, - } - ) - - @classmethod - def get_properties_from_template(cls, name): - template = cls._psetqto.get_by_name(name) - if not template: - return - properties = [] - for prop_template in template.HasPropertyTemplates: - if not prop_template.is_a("IfcSimplePropertyTemplate"): - continue # Other types not yet supported - enum_items = [] - - if prop_template.TemplateType == "P_SINGLEVALUE": - try: - data_type = ifcopenshell.util.attribute.get_primitive_type( - cls._schema.declaration_by_name(prop_template.PrimaryMeasureType or "IfcLabel") - ) - except: - # TODO: Occurs if the data type is something that exists in IFC4 and not in IFC2X3. To fully fix - # this we need to generate the IFC2X3 pset template definitions. - continue - elif prop_template.TemplateType == "P_ENUMERATEDVALUE": - data_type = "enum" - enum_items = [v.wrappedValue for v in prop_template.Enumerators.EnumerationValues] - elif prop_template.TemplateType in ["Q_LENGTH", "Q_AREA", "Q_VOLUME", "Q_WEIGHT", "Q_TIME"]: - data_type = "float" - elif prop_template.TemplateType == "Q_COUNT": - data_type = "integer" - else: - continue # Other types not yet supported - - properties.append( - { - "Name": prop_template.Name, - "value": None, - "type": data_type, - "enum_items": enum_items, - "is_null": True, - } - ) - return properties + data = qto.get_info() + del data["OwnerHistory"] + del data["Quantities"] + # TODO: support more than just simple quantities + # We call it properties for convenience, not for correctness + data["Properties"] = [q.id() for q in qto.Quantities or [] if q.is_a("IfcPhysicalSimpleQuantity")] + cls.qtos[qto.id()] = data + cls.products[product_id]["qtos"].add(qto.id()) + for quantity in qto.Quantities or []: + if quantity.is_a("IfcPhysicalSimpleQuantity"): + cls.load_prop(quantity)