Fix append_asset conversion issue after 3ffc0bc #6012

Custom file.add wasn't considering IfcLengthMeasure attributes...
This commit is contained in:
Andrej730
2025-01-23 12:58:28 +05:00
parent 5bc91ec4e4
commit 42d01ac222
3 changed files with 90 additions and 5 deletions
@@ -17,6 +17,7 @@
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>. # along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell import ifcopenshell
import ifcopenshell.ifcopenshell_wrapper as W
import ifcopenshell.api.geometry import ifcopenshell.api.geometry
import ifcopenshell.api.type import ifcopenshell.api.type
import ifcopenshell.api.project import ifcopenshell.api.project
@@ -25,7 +26,9 @@ import ifcopenshell.api.owner.settings
import ifcopenshell.util.element import ifcopenshell.util.element
import ifcopenshell.util.geolocation import ifcopenshell.util.geolocation
import ifcopenshell.util.placement 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[ APPENDABLE_ASSET = Literal[
@@ -425,7 +428,9 @@ class Usecase:
context_identifier=added_context.ContextIdentifier, 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) """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. 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. and there is no control to prevent it from adding certain type of elements.
""" """
ifc_file = self.file ifc_file = self.file
return ifc_file.add(element)
if not self.assume_asset_uniqueness_by_name: if not self.assume_asset_uniqueness_by_name:
return ifc_file.add(element) return ifc_file.add(element)
@@ -442,6 +446,24 @@ class Usecase:
if added_element := reuse_identities.get(element_identity): if added_element := reuse_identities.get(element_identity):
return added_element 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. # Maybe element already exists.
if element.is_a("IfcProfileDef"): if element.is_a("IfcProfileDef"):
profile_name = element.ProfileName profile_name = element.ProfileName
@@ -460,16 +482,49 @@ class Usecase:
return existing_material return existing_material
attrs = {} 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 "<type IfcLengthMeasure: <real>>" 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): for attr_index, attr_value in enumerate(element):
# `None` is set by default already. # `None` is set by default already.
if attr_value is None: if attr_value is None:
continue continue
elif isinstance(attr_value, ifcopenshell.entity_instance): elif isinstance(attr_value, ifcopenshell.entity_instance):
attr_value = self.file_add(attr_value) attr_value = self.file_add(attr_value)
elif isinstance(attr_value, tuple): elif isinstance(attr_value, tuple):
# Assume type is consistent across the tuple. # Assume type is consistent across the tuple.
if isinstance(attr_value[0], ifcopenshell.entity_instance): tuple_type = get_tuple_type(attr_value)
attr_value = tuple(self.file_add(e) for e in 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 attrs[attr_index] = attr_value
# Adding entity at the end just to keep it consistent with `file.add`. # Adding entity at the end just to keep it consistent with `file.add`.
@@ -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" :param unit_type: The type of SI unit, defaults to "LENGTHUNIT"
:returns: The scale factor :returns: The scale factor
""" """
# Currently we assume that all ifc projects must have IfcProject.
if not (units := ifc_file.by_type("IfcProject")[0].UnitsInContext): if not (units := ifc_file.by_type("IfcProject")[0].UnitsInContext):
return 1 return 1
unit_scale = 1 unit_scale = 1
@@ -29,9 +29,12 @@ import ifcopenshell.api.context
import ifcopenshell.api.project import ifcopenshell.api.project
import ifcopenshell.api.material import ifcopenshell.api.material
import ifcopenshell.api.profile import ifcopenshell.api.profile
import ifcopenshell.api.unit
import ifcopenshell.util.element import ifcopenshell.util.element
import ifcopenshell.util.placement import ifcopenshell.util.placement
import ifcopenshell.util.unit
import numpy as np import numpy as np
from ifcopenshell.util.shape_builder import ShapeBuilder
class TestAppendAssetIFC2X3(test.bootstrap.IFC2X3): class TestAppendAssetIFC2X3(test.bootstrap.IFC2X3):
@@ -114,6 +117,7 @@ class TestAppendAssetIFC2X3(test.bootstrap.IFC2X3):
assert set(self.file.by_type("IfcWall")) == set() assert set(self.file.by_type("IfcWall")) == set()
def test_append_two_type_products_sharing_the_same_material_indirectly_via_a_material_set(self): 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) library = ifcopenshell.api.project.create_file(version=self.file.schema)
ifcopenshell.api.root.create_entity(library, "IfcProject") ifcopenshell.api.root.create_entity(library, "IfcProject")
element1 = ifcopenshell.api.root.create_entity(library, ifc_class="IfcWallType") 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] assert self.file.by_type("IfcStyledItem")[0].Item == self.file.by_type("IfcBoundingBox")[0]
def test_append_product_with_styles_to_reuse_styleditems(self): 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) 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") element_type = ifcopenshell.api.root.create_entity(library, ifc_class="IfcWallType")
history = library.createIfcOwnerHistory() history = library.createIfcOwnerHistory()
element_type.OwnerHistory = history element_type.OwnerHistory = history
@@ -453,6 +459,29 @@ class TestAppendAssetIFC2X3(test.bootstrap.IFC2X3):
assert "Test" in pset_data assert "Test" in pset_data
assert ifcopenshell.util.element.get_psets(element2_) == 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): class TestAppendAssetIFC4(test.bootstrap.IFC4, TestAppendAssetIFC2X3):
# NOTE: breaks in IFC2X3 since IfcProfileDef doesn't have "HasProperties" inverse in ifc2x3 # NOTE: breaks in IFC2X3 since IfcProfileDef doesn't have "HasProperties" inverse in ifc2x3