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