From 42d01ac222f757f39aff3c63f9e80ec67839c2a7 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 23 Jan 2025 12:58:28 +0500 Subject: [PATCH] Fix append_asset conversion issue after 3ffc0bc #6012 Custom file.add wasn't considering IfcLengthMeasure attributes... --- .../ifcopenshell/api/project/append_asset.py | 65 +++++++++++++++++-- .../ifcopenshell/util/unit.py | 1 + .../test/api/project/test_append_asset.py | 29 +++++++++ 3 files changed, 90 insertions(+), 5 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/api/project/append_asset.py b/src/ifcopenshell-python/ifcopenshell/api/project/append_asset.py index 0f09195546..ceeda11066 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/project/append_asset.py +++ b/src/ifcopenshell-python/ifcopenshell/api/project/append_asset.py @@ -17,6 +17,7 @@ # along with IfcOpenShell. If not, see . import ifcopenshell +import ifcopenshell.ifcopenshell_wrapper as W import ifcopenshell.api.geometry import ifcopenshell.api.type import ifcopenshell.api.project @@ -25,7 +26,9 @@ import ifcopenshell.api.owner.settings import ifcopenshell.util.element import ifcopenshell.util.geolocation import ifcopenshell.util.placement -from typing import Optional, Any, Union, Literal, get_args +import ifcopenshell.util.unit +from typing import Optional, Any, Union, Literal, get_args, Callable +from functools import partial APPENDABLE_ASSET = Literal[ @@ -425,7 +428,9 @@ class Usecase: context_identifier=added_context.ContextIdentifier, ) - def file_add(self, element: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance: + def file_add( + self, element: ifcopenshell.entity_instance, conversion_factor: Optional[float] = None + ) -> ifcopenshell.entity_instance: """Reimplementation of `file.add` but taking into account that some elements (profiles, materials) are already existing (checking by their name) and shouldn't be duplicated. @@ -433,7 +438,6 @@ class Usecase: and there is no control to prevent it from adding certain type of elements. """ ifc_file = self.file - return ifc_file.add(element) if not self.assume_asset_uniqueness_by_name: return ifc_file.add(element) @@ -442,6 +446,24 @@ class Usecase: if added_element := reuse_identities.get(element_identity): return added_element + def get_conversion_factor() -> float: + nonlocal conversion_factor + if conversion_factor is not None: + return conversion_factor + library_scale = ifcopenshell.util.unit.calculate_unit_scale(self.settings["library"]) + current_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file) + conversion_factor = library_scale / current_scale + return conversion_factor + + attributes_ = None + + def get_attributes() -> tuple[W.attribute, ...]: + nonlocal attributes_ + if attributes_ is not None: + return attributes_ + attributes_ = element.wrapped_data.declaration().as_entity().all_attributes() + return attributes_ + # Maybe element already exists. if element.is_a("IfcProfileDef"): profile_name = element.ProfileName @@ -460,16 +482,49 @@ class Usecase: return existing_material attrs = {} + + # Utils method for the loop. + def get_tuple_type(tuple_: tuple) -> type: + while isinstance(tuple_, tuple): + tuple_ = tuple_[0] + return type(tuple_) + + def is_length_measure(attribute: W.attribute) -> bool: + return ">" in str(attribute.type_of_attribute()) + + def apply_to_array(arr: Any, func: Callable[[Any], Any]) -> Any: + if isinstance(arr, tuple): + return tuple(apply_to_array(sub, func) for sub in arr) + return func(arr) + + file_add_ = partial(self.file_add, conversion_factor=conversion_factor) + apply_conversion = partial(lambda x: x * conversion_factor) + + # Migrate attributes to another file. for attr_index, attr_value in enumerate(element): # `None` is set by default already. if attr_value is None: continue + elif isinstance(attr_value, ifcopenshell.entity_instance): attr_value = self.file_add(attr_value) + elif isinstance(attr_value, tuple): # Assume type is consistent across the tuple. - if isinstance(attr_value[0], ifcopenshell.entity_instance): - attr_value = tuple(self.file_add(e) for e in attr_value) + tuple_type = get_tuple_type(attr_value) + if tuple_type == ifcopenshell.entity_instance: + attr_value = apply_to_array(attr_value, file_add_) + elif tuple_type == float: + attributes = get_attributes() + if is_length_measure(attributes[attr_index]): + get_conversion_factor() # Ensure conversion factor is not None. + attr_value = apply_to_array(attr_value, apply_conversion) + + elif isinstance(attr_value, float): + attributes = get_attributes() + if is_length_measure(attributes[attr_index]): + attr_value *= get_conversion_factor() + attrs[attr_index] = attr_value # Adding entity at the end just to keep it consistent with `file.add`. diff --git a/src/ifcopenshell-python/ifcopenshell/util/unit.py b/src/ifcopenshell-python/ifcopenshell/util/unit.py index a8dd0dfdb4..d125cb0128 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/unit.py +++ b/src/ifcopenshell-python/ifcopenshell/util/unit.py @@ -639,6 +639,7 @@ def calculate_unit_scale(ifc_file: ifcopenshell.file, unit_type: str = "LENGTHUN :param unit_type: The type of SI unit, defaults to "LENGTHUNIT" :returns: The scale factor """ + # Currently we assume that all ifc projects must have IfcProject. if not (units := ifc_file.by_type("IfcProject")[0].UnitsInContext): return 1 unit_scale = 1 diff --git a/src/ifcopenshell-python/test/api/project/test_append_asset.py b/src/ifcopenshell-python/test/api/project/test_append_asset.py index 07679888a0..7ba94d79b9 100644 --- a/src/ifcopenshell-python/test/api/project/test_append_asset.py +++ b/src/ifcopenshell-python/test/api/project/test_append_asset.py @@ -29,9 +29,12 @@ import ifcopenshell.api.context import ifcopenshell.api.project import ifcopenshell.api.material import ifcopenshell.api.profile +import ifcopenshell.api.unit import ifcopenshell.util.element import ifcopenshell.util.placement +import ifcopenshell.util.unit import numpy as np +from ifcopenshell.util.shape_builder import ShapeBuilder class TestAppendAssetIFC2X3(test.bootstrap.IFC2X3): @@ -114,6 +117,7 @@ class TestAppendAssetIFC2X3(test.bootstrap.IFC2X3): assert set(self.file.by_type("IfcWall")) == set() def test_append_two_type_products_sharing_the_same_material_indirectly_via_a_material_set(self): + ifcopenshell.api.root.create_entity(self.file, "IfcProject") library = ifcopenshell.api.project.create_file(version=self.file.schema) ifcopenshell.api.root.create_entity(library, "IfcProject") element1 = ifcopenshell.api.root.create_entity(library, ifc_class="IfcWallType") @@ -162,7 +166,9 @@ class TestAppendAssetIFC2X3(test.bootstrap.IFC2X3): assert self.file.by_type("IfcStyledItem")[0].Item == self.file.by_type("IfcBoundingBox")[0] def test_append_product_with_styles_to_reuse_styleditems(self): + ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject") library = ifcopenshell.api.project.create_file(version=self.file.schema) + ifcopenshell.api.root.create_entity(library, ifc_class="IfcProject") element_type = ifcopenshell.api.root.create_entity(library, ifc_class="IfcWallType") history = library.createIfcOwnerHistory() element_type.OwnerHistory = history @@ -453,6 +459,29 @@ class TestAppendAssetIFC2X3(test.bootstrap.IFC2X3): assert "Test" in pset_data assert ifcopenshell.util.element.get_psets(element2_) == pset_data + def test_file_add_to_convert_units(self): + library = ifcopenshell.file() + builder = ShapeBuilder(library) + ifcopenshell.api.root.create_entity(library, "IfcProject") + unit = ifcopenshell.api.unit.add_si_unit(library, unit_type="LENGTHUNIT", prefix="MILLI") + ifcopenshell.api.unit.assign_unit(library, units=[unit]) + ifc_file = ifcopenshell.file() + ifcopenshell.api.root.create_entity(ifc_file, "IfcProject") + unit = ifcopenshell.api.unit.add_si_unit(ifc_file, unit_type="LENGTHUNIT") + ifcopenshell.api.unit.assign_unit(ifc_file, units=[unit]) + + # Simple floats. + profile = ifcopenshell.api.profile.add_parameterized_profile(library, "IfcCircleProfileDef") + profile.Radius = 10.0 + new_profile = ifcopenshell.api.project.append_asset(ifc_file, library, profile) + assert new_profile.Radius == 0.01 + + # Aggregates of floats. + arbitrary_profile = builder.profile(builder.rectangle((1000, 1000))) + new_arbitrary_profile = ifcopenshell.api.project.append_asset(ifc_file, library, arbitrary_profile) + updated_points = np.array(arbitrary_profile.OuterCurve.Points) * 0.001 + assert np.allclose(updated_points, new_arbitrary_profile.OuterCurve.Points) + class TestAppendAssetIFC4(test.bootstrap.IFC4, TestAppendAssetIFC2X3): # NOTE: breaks in IFC2X3 since IfcProfileDef doesn't have "HasProperties" inverse in ifc2x3