mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-09 09:21:46 +00:00
append_asset - fix duplicated applications/organizations/pao
Noticed when starting new project with demo library it had 29 duplicated IfcApplications...
This commit is contained in:
@@ -151,6 +151,7 @@ class LibraryGenerator:
|
|||||||
application = ifcopenshell.api.owner.add_application(
|
application = ifcopenshell.api.owner.add_application(
|
||||||
self.file, application_full_name="Bonsai", application_identifier="Bonsai"
|
self.file, application_full_name="Bonsai", application_identifier="Bonsai"
|
||||||
)
|
)
|
||||||
|
# Override Bonsai methods for duration of the script.
|
||||||
ifcopenshell.api.owner.settings.get_user = lambda x: user
|
ifcopenshell.api.owner.settings.get_user = lambda x: user
|
||||||
ifcopenshell.api.owner.settings.get_application = lambda x: application
|
ifcopenshell.api.owner.settings.get_application = lambda x: application
|
||||||
|
|
||||||
|
|||||||
@@ -179,6 +179,12 @@ class Usecase:
|
|||||||
assume_asset_uniqueness_by_name: bool
|
assume_asset_uniqueness_by_name: bool
|
||||||
whitelisted_inverse_attributes: dict[str, list[str]]
|
whitelisted_inverse_attributes: dict[str, list[str]]
|
||||||
|
|
||||||
|
added_elements: dict[int, ifcopenshell.entity_instance]
|
||||||
|
"""Elements added with ``add_element``."""
|
||||||
|
|
||||||
|
reuse_identities: dict[int, ifcopenshell.entity_instance]
|
||||||
|
"""Mapping of old element ids to new elements, usually fiiled by ``file_add``."""
|
||||||
|
|
||||||
def execute(self):
|
def execute(self):
|
||||||
# mapping of old element ids to new elements
|
# mapping of old element ids to new elements
|
||||||
self.added_elements: dict[int, ifcopenshell.entity_instance] = {}
|
self.added_elements: dict[int, ifcopenshell.entity_instance] = {}
|
||||||
@@ -213,6 +219,16 @@ class Usecase:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def get_existing_element(self, element: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
|
def get_existing_element(self, element: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
|
||||||
|
"""Get existing element for a library element.
|
||||||
|
|
||||||
|
Return element if it was already added with ``add_element``
|
||||||
|
or if it's not necessary (model already has a replacement for it).
|
||||||
|
|
||||||
|
Note that if element is returned, it will be accepted as-is,
|
||||||
|
it's subgraph inverses won't be checked.
|
||||||
|
|
||||||
|
Return ``None`` if element wasn't added before and needs to be added.
|
||||||
|
"""
|
||||||
if element.id() in self.added_elements:
|
if element.id() in self.added_elements:
|
||||||
return self.added_elements[element.id()]
|
return self.added_elements[element.id()]
|
||||||
if element.is_a("IfcRoot"):
|
if element.is_a("IfcRoot"):
|
||||||
@@ -232,6 +248,20 @@ class Usecase:
|
|||||||
if name is None:
|
if name is None:
|
||||||
return None
|
return None
|
||||||
return next((e for e in self.file.by_type(element.is_a()) if e.Name == name), None)
|
return next((e for e in self.file.by_type(element.is_a()) if e.Name == name), None)
|
||||||
|
|
||||||
|
# Not really assets but if we don't check them here,
|
||||||
|
# their subgraph entities may be appended twice.
|
||||||
|
elif (ifc_class := element.is_a()) == "IfcOrganization":
|
||||||
|
attr_name = "Id" if self.file.schema == "IFC2X3" else "Identification"
|
||||||
|
org_id = getattr(element, attr_name)
|
||||||
|
if org_id is not None:
|
||||||
|
return next((e for e in self.file.by_type("IfcOrganization") if getattr(e, attr_name) == org_id), None)
|
||||||
|
elif ifc_class == "IfcPerson":
|
||||||
|
attr_name = "Id" if self.file.schema == "IFC2X3" else "Identification"
|
||||||
|
person_id = getattr(element, attr_name)
|
||||||
|
if person_id is not None:
|
||||||
|
return next((e for e in self.file.by_type("IfcPerson") if getattr(e, attr_name) == person_id), None)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -318,6 +348,7 @@ class Usecase:
|
|||||||
return element
|
return element
|
||||||
|
|
||||||
def add_element(self, element: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
|
def add_element(self, element: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
|
||||||
|
"""Add element and check all it's subgraph inverses."""
|
||||||
if element.id() == 0:
|
if element.id() == 0:
|
||||||
return
|
return
|
||||||
existing_element = self.get_existing_element(element)
|
existing_element = self.get_existing_element(element)
|
||||||
@@ -358,6 +389,7 @@ class Usecase:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def check_inverses(self, element: ifcopenshell.entity_instance) -> None:
|
def check_inverses(self, element: ifcopenshell.entity_instance) -> None:
|
||||||
|
"""Add inverse elements for the whitelisted inverse attributes."""
|
||||||
for source_class, attributes in self.whitelisted_inverse_attributes.items():
|
for source_class, attributes in self.whitelisted_inverse_attributes.items():
|
||||||
if not element.is_a(source_class):
|
if not element.is_a(source_class):
|
||||||
continue
|
continue
|
||||||
@@ -372,10 +404,15 @@ class Usecase:
|
|||||||
self.add_inverse_element(inverse)
|
self.add_inverse_element(inverse)
|
||||||
|
|
||||||
def add_inverse_element(self, element: ifcopenshell.entity_instance) -> None:
|
def add_inverse_element(self, element: ifcopenshell.entity_instance) -> None:
|
||||||
# Inverse attributes are added manually because they are basically
|
"""Add inverse element.
|
||||||
# relationships that can reference many other assets that we are not
|
|
||||||
# interested in.
|
|
||||||
|
|
||||||
|
Inverse elements are requiring different method than ``file_add``
|
||||||
|
because they can reference many other assets that we are not
|
||||||
|
interested in.
|
||||||
|
|
||||||
|
E.g. a IfcRelAssociatesMaterial referencing products unrelated
|
||||||
|
to the current asset.
|
||||||
|
"""
|
||||||
# For layer assignment we don't want to add it's items
|
# For layer assignment we don't want to add it's items
|
||||||
# to avoid adding representations / items that are not related to current append_asset.
|
# to avoid adding representations / items that are not related to current append_asset.
|
||||||
skip_not_reused_entities_attr_i = None
|
skip_not_reused_entities_attr_i = None
|
||||||
@@ -535,7 +572,32 @@ class Usecase:
|
|||||||
attributes_ = element.wrapped_data.declaration().as_entity().all_attributes()
|
attributes_ = element.wrapped_data.declaration().as_entity().all_attributes()
|
||||||
return attributes_
|
return attributes_
|
||||||
|
|
||||||
# Maybe element already exists.
|
def get_existing_element_(
|
||||||
|
subelement: ifcopenshell.entity_instance,
|
||||||
|
) -> Union[ifcopenshell.entity_instance, None]:
|
||||||
|
# Check identity because `subelement` might not be the current `element`,
|
||||||
|
# e.g. for IfcPersonAndOrganization.
|
||||||
|
element_identity = subelement.wrapped_data.identity()
|
||||||
|
if subelement_ := reuse_identities.get(element_identity):
|
||||||
|
return subelement_
|
||||||
|
|
||||||
|
ifc_class = subelement.is_a()
|
||||||
|
assert ifc_class in ("IfcOrganization", "IfcPerson")
|
||||||
|
attr_name = "Id" if ifc_file.schema == "IFC2X3" else "Identification"
|
||||||
|
subelement_id = getattr(subelement, attr_name)
|
||||||
|
|
||||||
|
if subelement_id is not None:
|
||||||
|
existing_org = next(
|
||||||
|
(e for e in ifc_file.by_type(ifc_class) if getattr(e, attr_name) == subelement_id), None
|
||||||
|
)
|
||||||
|
if existing_org is not None:
|
||||||
|
reuse_identities[element_identity] = existing_org
|
||||||
|
return existing_org
|
||||||
|
|
||||||
|
# Check if element already exists.
|
||||||
|
# NOTE: Ensure this part is in sync with `get_existing_element`,
|
||||||
|
# if some class is present here but not in `get_existing_element`,
|
||||||
|
# then it might create duplicated subelements.
|
||||||
if element.is_a("IfcProfileDef"):
|
if element.is_a("IfcProfileDef"):
|
||||||
profile_name = element.ProfileName
|
profile_name = element.ProfileName
|
||||||
if profile_name is not None:
|
if profile_name is not None:
|
||||||
@@ -545,12 +607,14 @@ class Usecase:
|
|||||||
if existing_profile is not None:
|
if existing_profile is not None:
|
||||||
reuse_identities[element_identity] = existing_profile
|
reuse_identities[element_identity] = existing_profile
|
||||||
return existing_profile
|
return existing_profile
|
||||||
|
|
||||||
elif element.is_a("IfcMaterial"):
|
elif element.is_a("IfcMaterial"):
|
||||||
material_name = element.Name
|
material_name = element.Name
|
||||||
existing_material = next((e for e in ifc_file.by_type("IfcMaterial") if e.Name == material_name), None)
|
existing_material = next((e for e in ifc_file.by_type("IfcMaterial") if e.Name == material_name), None)
|
||||||
if existing_material is not None:
|
if existing_material is not None:
|
||||||
reuse_identities[element_identity] = existing_material
|
reuse_identities[element_identity] = existing_material
|
||||||
return existing_material
|
return existing_material
|
||||||
|
|
||||||
elif element.is_a("IfcPresentationStyle"):
|
elif element.is_a("IfcPresentationStyle"):
|
||||||
style_name = element.Name
|
style_name = element.Name
|
||||||
if style_name is not None:
|
if style_name is not None:
|
||||||
@@ -559,7 +623,38 @@ class Usecase:
|
|||||||
reuse_identities[element_identity] = existing_style
|
reuse_identities[element_identity] = existing_style
|
||||||
return existing_style
|
return existing_style
|
||||||
|
|
||||||
attrs = {}
|
elif ifc_class == "IfcApplication":
|
||||||
|
app_id = element.ApplicationIdentifier
|
||||||
|
if app_id is not None:
|
||||||
|
existing_app = next(
|
||||||
|
(e for e in ifc_file.by_type("IfcApplication") if e.ApplicationIdentifier == app_id), None
|
||||||
|
)
|
||||||
|
if existing_app is not None:
|
||||||
|
reuse_identities[element_identity] = existing_app
|
||||||
|
return existing_app
|
||||||
|
|
||||||
|
elif ifc_class == "IfcOrganization":
|
||||||
|
existing_org = get_existing_element_(element)
|
||||||
|
if existing_org is not None:
|
||||||
|
reuse_identities[element_identity] = existing_org
|
||||||
|
return existing_org
|
||||||
|
|
||||||
|
elif ifc_class == "IfcPerson":
|
||||||
|
existing_person = get_existing_element_(element)
|
||||||
|
if existing_person is not None:
|
||||||
|
reuse_identities[element_identity] = existing_person
|
||||||
|
return existing_person
|
||||||
|
|
||||||
|
elif ifc_class == "IfcPersonAndOrganization":
|
||||||
|
if (person := get_existing_element_(element.ThePerson)) and (
|
||||||
|
org := get_existing_element_(element.TheOrganization)
|
||||||
|
):
|
||||||
|
for pao in ifc_file.by_type("IfcPersonAndOrganization"):
|
||||||
|
if pao.ThePerson == person and pao.TheOrganization == org:
|
||||||
|
reuse_identities[element_identity] = pao
|
||||||
|
return pao
|
||||||
|
|
||||||
|
attrs: dict[int, Any] = {}
|
||||||
|
|
||||||
# Utils method for the loop.
|
# Utils method for the loop.
|
||||||
def get_tuple_type(tuple_: tuple) -> type:
|
def get_tuple_type(tuple_: tuple) -> type:
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ 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.api.unit
|
||||||
|
import ifcopenshell.api.owner.settings
|
||||||
import ifcopenshell.util.element
|
import ifcopenshell.util.element
|
||||||
import ifcopenshell.util.placement
|
import ifcopenshell.util.placement
|
||||||
import ifcopenshell.util.unit
|
import ifcopenshell.util.unit
|
||||||
@@ -562,6 +563,42 @@ class TestAppendAssetIFC2X3(test.bootstrap.IFC2X3):
|
|||||||
representations = tuple(self.file.by_type("IfcRepresentation"))
|
representations = tuple(self.file.by_type("IfcRepresentation"))
|
||||||
assert self.file.by_type("IfcPresentationLayerAssignment")[0].AssignedItems == representations
|
assert self.file.by_type("IfcPresentationLayerAssignment")[0].AssignedItems == representations
|
||||||
|
|
||||||
|
def test_append_owner_history_without_producing_duplicates(self):
|
||||||
|
ifc_file = ifcopenshell.file()
|
||||||
|
library = ifcopenshell.file()
|
||||||
|
|
||||||
|
# Setup owner history.
|
||||||
|
ifcopenshell.api.owner.settings.factory_reset()
|
||||||
|
person = ifcopenshell.api.owner.add_person(library)
|
||||||
|
ifcopenshell.api.owner.add_role(library, person, "CONTRIBUTOR")
|
||||||
|
organization = ifcopenshell.api.owner.add_organisation(library)
|
||||||
|
user = ifcopenshell.api.owner.add_person_and_organisation(library, person=person, organisation=organization)
|
||||||
|
application = ifcopenshell.api.owner.add_application(library)
|
||||||
|
assert len(library.by_type("IfcPerson")) == 1
|
||||||
|
assert len(library.by_type("IfcPersonAndOrganization")) == 1
|
||||||
|
assert len(library.by_type("IfcApplication")) == 1
|
||||||
|
assert len(library.by_type("IfcOrganization")) == 2
|
||||||
|
|
||||||
|
# IfcOrganization's attributes.
|
||||||
|
assert len(library.by_type("IfcTelecomAddress")) == 1
|
||||||
|
# IfcPerson's attributes.
|
||||||
|
assert len(library.by_type("IfcActorRole")) == 2
|
||||||
|
|
||||||
|
# Add 2 wall types.
|
||||||
|
wall_type_1 = ifcopenshell.api.root.create_entity(library, ifc_class="IfcWallType")
|
||||||
|
wall_type_2 = ifcopenshell.api.root.create_entity(library, ifc_class="IfcWallType")
|
||||||
|
|
||||||
|
ifcopenshell.api.project.append_asset(ifc_file, library, wall_type_1)
|
||||||
|
ifcopenshell.api.project.append_asset(ifc_file, library, wall_type_2)
|
||||||
|
|
||||||
|
assert len(ifc_file.by_type("IfcPerson")) == 1
|
||||||
|
assert len(ifc_file.by_type("IfcPersonAndOrganization")) == 1
|
||||||
|
assert len(ifc_file.by_type("IfcApplication")) == 1
|
||||||
|
assert len(ifc_file.by_type("IfcOrganization")) == 2
|
||||||
|
# Ensure their attributes are also not duplicated.
|
||||||
|
assert len(ifc_file.by_type("IfcTelecomAddress")) == 1
|
||||||
|
assert len(ifc_file.by_type("IfcActorRole")) == 2
|
||||||
|
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
Reference in New Issue
Block a user