diff --git a/src/bonsai/bonsai/bim/module/csv/operator.py b/src/bonsai/bonsai/bim/module/csv/operator.py index c02fb1e7f6..7d59bc5a92 100644 --- a/src/bonsai/bonsai/bim/module/csv/operator.py +++ b/src/bonsai/bonsai/bim/module/csv/operator.py @@ -329,6 +329,7 @@ class ImportIfcCsv(bpy.types.Operator, tool.Ifc.Operator): empty=props.empty_value, bool_true=props.true_value, bool_false=props.false_value, + concat=props.concat_value, ) if not props.should_load_from_memory: ifc_file.write(props.csv_ifc_file) diff --git a/src/ifccsv/ifccsv.py b/src/ifccsv/ifccsv.py index e474724ee0..0030bc84d1 100755 --- a/src/ifccsv/ifccsv.py +++ b/src/ifccsv/ifccsv.py @@ -405,6 +405,7 @@ class IfcCsv: empty: str = "", bool_true: str = "YES", bool_false: str = "NO", + concat: str = ", ", ) -> None: """ Args: @@ -413,11 +414,11 @@ class IfcCsv: ext: FILE_FORMAT = table.split(".")[-1].lower() if ext == "csv": - self.import_csv(ifc_file, table, attributes, delimiter, null, empty, bool_true, bool_false) + self.import_csv(ifc_file, table, attributes, delimiter, null, empty, bool_true, bool_false, concat) elif ext == "ods": - self.import_ods(ifc_file, table, attributes, null, empty, bool_true, bool_false) + self.import_ods(ifc_file, table, attributes, null, empty, bool_true, bool_false, concat) elif ext == "xlsx": - self.import_xlsx(ifc_file, table, attributes, null, empty, bool_true, bool_false) + self.import_xlsx(ifc_file, table, attributes, null, empty, bool_true, bool_false, concat) def import_csv( self, @@ -429,6 +430,7 @@ class IfcCsv: empty: str = "", bool_true: str = "YES", bool_false: str = "NO", + concat: str = ", ", ) -> None: with open(table, newline="", encoding="utf-8") as f: reader = csv.reader(f, delimiter=delimiter) @@ -441,17 +443,19 @@ class IfcCsv: elif len(attributes) == len(headers) - 1: attributes.insert(0, "") # The GlobalId column continue - self.process_row(ifc_file, row, headers, attributes, null, empty, bool_true, bool_false) + self.process_row(ifc_file, row, headers, attributes, null, empty, bool_true, bool_false, concat) - def import_xlsx(self, ifc_file, table, attributes, null, empty, bool_true, bool_false): + def import_xlsx(self, ifc_file, table, attributes, null, empty, bool_true, bool_false, concat) -> None: df = pd.read_excel(table) self.import_pd(ifc_file, df, attributes, null, empty, bool_true, bool_false) - def import_ods(self, ifc_file, table, attributes, null, empty, bool_true, bool_false): + def import_ods(self, ifc_file, table, attributes, null, empty, bool_true, bool_false, concat) -> None: df = pd.read_excel(table, engine="odf") self.import_pd(ifc_file, df, attributes, null, empty, bool_true, bool_false) - def import_pd(self, ifc_file, df, attributes=None, null="-", empty="", bool_true="YES", bool_false="NO"): + def import_pd( + self, ifc_file, df, attributes=None, null="-", empty="", bool_true="YES", bool_false="NO", concat=", " + ) -> None: headers = df.columns.tolist() if not attributes: @@ -460,7 +464,7 @@ class IfcCsv: attributes.insert(0, "") # The GlobalId column for _, row in df.iterrows(): - self.process_row(ifc_file, row.tolist(), headers, attributes, null, empty, bool_true, bool_false) + self.process_row(ifc_file, row.tolist(), headers, attributes, null, empty, bool_true, bool_false, concat) def process_row( self, @@ -472,6 +476,7 @@ class IfcCsv: empty: str, bool_true: str, bool_false: str, + concat: str, ) -> None: try: element = ifc_file.by_guid(row[0]) @@ -490,7 +495,7 @@ class IfcCsv: elif value == bool_false: value = False key = attributes[i] or headers[i] - ifcopenshell.util.selector.set_element_value(ifc_file, element, key, value) + ifcopenshell.util.selector.set_element_value(ifc_file, element, key, value, concat=concat) if __name__ == "__main__": @@ -552,5 +557,6 @@ if __name__ == "__main__": delimiter=args.delimiter, null=args.null, empty=args.empty, + concat=args.concat, ) ifc_file.write(args.ifc) diff --git a/src/ifcopenshell-python/ifcopenshell/util/selector.py b/src/ifcopenshell-python/ifcopenshell/util/selector.py index 5995fe6d32..2c598df030 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/selector.py +++ b/src/ifcopenshell-python/ifcopenshell/util/selector.py @@ -28,6 +28,7 @@ import ifcopenshell.util.element import ifcopenshell.util.fm import ifcopenshell.util.geolocation import ifcopenshell.util.placement +import ifcopenshell.util.pset import ifcopenshell.util.schema import ifcopenshell.util.shape import ifcopenshell.util.system @@ -464,7 +465,17 @@ def set_element_value( element: Union[ifcopenshell.entity_instance, Iterable[ifcopenshell.entity_instance], None], query: Union[str, list[str]], value: Any, + *, + concat: str = ", ", ) -> None: + """Set element value based on the provided query. + + :param element: IFC element to change. + :param query: String query to identify the attribute to change. + :param value: Value to set. + :param concat: Concatenation symbol, used only to deserialize property + set enum values from string values. + """ original_element = element if isinstance(query, (list, tuple)): keys = query @@ -640,6 +651,55 @@ def set_element_value( elif pset.is_a("IfcElementQuantity") and prop_value != float(value): ifcopenshell.api.pset.edit_qto(ifc_file, qto=pset, properties={prop: float(value)}) elif pset.is_a("IfcPropertySet") and element.get(key, None) != value: + + def process_pset_prop_value(pset: ifcopenshell.entity_instance, prop: str, value: Any) -> Any: + """Try to process value for edit_pset. + + `edit_pset` is expecting a sequence of values + for enum properties, not just a string of some-symbol-separated values. + """ + if not isinstance(value, str): + return value + template = ifcopenshell.util.pset.get_template(ifc_file.schema) + pset_template = template.get_by_name(pset.Name) + if pset_template is None: + return value + for prop_template in pset_template.HasPropertyTemplates: + # 2 IfcSimplePropertyTemplate.Name + if prop_template[2] != prop: + continue + + # 4 IfcSimplePropertyTemplate.TemplateType + if prop_template[4] != "P_ENUMERATEDVALUE": + # Not a enum property. + return value + + # 7 IfcSimplePropertyTemplate.Enumerators + if (enumeration := prop_template[7]) is None: + # Enum property but without enumerators, + # make it a sequence to keep it assignable as a enum. + return (value,) + + # 1 IfcPropertyEnumeration.EnumerationValues + available_enum_values = {v.wrappedValue for v in enumeration[1]} + if value in available_enum_values: + # Valid enum item, just keep it a sequence. + return (value,) + + # Taking a wild guess that it's `concat` separated list. + enum_values = value.split(concat) + if not all(v in available_enum_values for v in enum_values): + raise Exception( + "Error setting pset enum property.\n" + f"Invalid enum values for property '{prop} in pset '{pset}': '{', '.join(enum_values)}'.\n" + f"Possible enum values for this property: {', '.join(available_enum_values)}." + ) + return enum_values + + # Couldn't find property template for this prop - delegate decision to edit_pset. + return value + + value = process_pset_prop_value(pset, key, value) ifcopenshell.api.pset.edit_pset(ifc_file, pset=pset, properties={key: value}) elif pset.is_a("IfcElementQuantity"): try: @@ -661,7 +721,7 @@ def set_element_value( return raise SetElementValueException( - f"Failed to set value for element '{original_element}' with query '{query}' (invalid or unsupported query)." + f"Failed to set value '{value}' for element '{original_element}' with query '{query}' (invalid or unsupported query)." )