From 7458040949219f22bf3919f8946f3d161ae58cf9 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 6 Mar 2025 15:34:23 +0500 Subject: [PATCH] Fix ifcpatch unit conversion for lists of ifc entities #6280 --- .../ifcopenshell/util/unit.py | 32 ++++++++++++++++--- .../test/util/test_unit.py | 32 +++++++++++++++++++ 2 files changed, 59 insertions(+), 5 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/util/unit.py b/src/ifcopenshell-python/ifcopenshell/util/unit.py index 47adbc284e..c28595ae5b 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/unit.py +++ b/src/ifcopenshell-python/ifcopenshell/util/unit.py @@ -24,6 +24,7 @@ from typing import Iterable from typing import Literal from typing import Optional from typing import Union +from typing import Generator import ifcopenshell import ifcopenshell.ifcopenshell_wrapper as ifcopenshell_wrapper @@ -803,9 +804,18 @@ def is_attr_type( return None -def iter_element_and_attributes_per_type( - ifc_file: ifcopenshell.file, attr_type_name: str -) -> Iterable[tuple[ifcopenshell.entity_instance, ifcopenshell_wrapper.attribute, Any]]: +FloatOrSequenceOfFloats = Union[float, tuple["FloatOrSequenceOfFloats", ...]] + + +def iter_element_and_attributes_per_type(ifc_file: ifcopenshell.file, attr_type_name: str) -> Generator[ + tuple[ + ifcopenshell.entity_instance, + ifcopenshell_wrapper.attribute, + Union[FloatOrSequenceOfFloats, ifcopenshell.entity_instance], + ], + None, + None, +]: schema: ifcopenshell_wrapper.schema_definition = ifcopenshell_wrapper.schema_by_name(ifc_file.schema_identifier) for element in ifc_file: @@ -826,6 +836,17 @@ def iter_element_and_attributes_per_type( if isinstance(val, ifcopenshell.entity_instance) and not val.is_a(attr_type_name): continue + elif isinstance(val, tuple): + if not val: + continue + val_ = val[0] + # If it's a tuple of entities, just yield the entities we need to edit. + if isinstance(val_, ifcopenshell.entity_instance): + for val_ in val: + if not val_.is_a(attr_type_name): + continue + yield element, attr, val_ + continue yield element, attr, val @@ -863,9 +884,10 @@ def convert_file_length_units(ifc_file: ifcopenshell.file, target_units: str = " # Traverse all elements and their nested attributes in the file and convert them for element, attr, val in iter_element_and_attributes_per_type(file_patched, "IfcLengthMeasure"): + # NOTE: There is no risk of editing same entities twice as they're all recreated + # after file is reloaded as `file_patched`. if isinstance(val, ifcopenshell.entity_instance): - new_value = convert_value(val.wrappedValue) - getattr(element, attr.name()).wrappedValue = new_value + val.wrappedValue = convert_value(val.wrappedValue) else: new_value = convert_value(val) setattr(element, attr.name(), new_value) diff --git a/src/ifcopenshell-python/test/util/test_unit.py b/src/ifcopenshell-python/test/util/test_unit.py index 5732c373a2..98c33397a9 100644 --- a/src/ifcopenshell-python/test/util/test_unit.py +++ b/src/ifcopenshell-python/test/util/test_unit.py @@ -247,6 +247,32 @@ class TestConvertFileLengthUnits(test.bootstrap.IFC2X3): rectangle = builder.rectangle((100, 100)) extrusion = builder.extrude(rectangle, 1000) + # IfcLengthMeasure entities. + product = ifcopenshell.api.root.create_entity(self.file, "IfcWall") + pset = ifcopenshell.api.pset.add_pset(self.file, product, "TestPset") + # Consider weird case when same entity is reused. + length_measure = self.file.create_entity("IfcLengthMeasure", 50.0) + enum_property = self.file.create_entity( + "IfcPropertyEnumeratedValue", + Name="Enum", + # Not entirely sure if there are real life cases when mixed typed entities used in the list + # but just to be safe. + EnumerationValues=[ + length_measure, + self.file.create_entity("IfcLabel", "TEXT"), + self.file.create_entity("IfcLengthMeasure", 250.0), + length_measure, + ], + ) + ifcopenshell.api.pset.edit_pset( + self.file, + pset, + properties={ + "Length": self.file.create_entity("IfcLengthMeasure", 25.0), + "Enum": enum_property, + }, + ) + ifcopenshell.api.unit.assign_unit(self.file, units=[unit]) output = subject.convert_file_length_units(self.file, target_units="METER") assert subject.get_full_unit_name(subject.get_project_unit(output, "LENGTHUNIT")) == "METRE" @@ -267,6 +293,12 @@ class TestConvertFileLengthUnits(test.bootstrap.IFC2X3): points = rectangle.Points.CoordList assert np.allclose(points, expected_points) + # IfcLengthMeasure entities. + product = output.by_type("IfcWall")[0] + pset_data = ifcopenshell.util.element.get_pset(product, "TestPset") + assert pset_data["Length"] == 0.025 + assert pset_data["Enum"] == [0.05, "TEXT", 0.25, 0.05] + def test_converting_map_conversion_if_there_is_no_map_unit(self): ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject") unit = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="LENGTHUNIT", prefix="MILLI")