827 lines
37 KiB
Python
827 lines
37 KiB
Python
# IfcOpenShell - IFC toolkit and geometry engine
|
|
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
|
|
#
|
|
# This file is part of IfcOpenShell.
|
|
#
|
|
# IfcOpenShell is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU Lesser General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# IfcOpenShell is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU Lesser General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU Lesser General Public License
|
|
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
from collections.abc import Callable
|
|
from functools import partial
|
|
from typing import Any, Literal, Optional, Union, get_args
|
|
|
|
import ifcopenshell
|
|
import ifcopenshell.api.context
|
|
import ifcopenshell.api.geometry
|
|
import ifcopenshell.api.owner.settings
|
|
import ifcopenshell.api.project
|
|
import ifcopenshell.api.type
|
|
import ifcopenshell.ifcopenshell_wrapper as W
|
|
import ifcopenshell.util.element
|
|
import ifcopenshell.util.geolocation
|
|
import ifcopenshell.util.placement
|
|
import ifcopenshell.util.unit
|
|
|
|
APPENDABLE_ASSET = Literal[
|
|
"IfcTypeProduct",
|
|
"IfcProduct",
|
|
"IfcMaterial",
|
|
"IfcCostSchedule",
|
|
"IfcProfileDef",
|
|
"IfcPresentationStyle",
|
|
]
|
|
APPENDABLE_ASSET_TYPES: tuple[APPENDABLE_ASSET, ...] = get_args(APPENDABLE_ASSET)
|
|
MATERIAL_SETS = ("IfcMaterialLayerSet", "IfcMaterialConstituentSet", "IfcMaterialProfileSet")
|
|
|
|
|
|
def append_asset(
|
|
file: ifcopenshell.file,
|
|
library: ifcopenshell.file,
|
|
element: ifcopenshell.entity_instance,
|
|
reuse_identities: Optional[dict[int, ifcopenshell.entity_instance]] = None,
|
|
assume_asset_uniqueness_by_name: bool = True,
|
|
) -> ifcopenshell.entity_instance:
|
|
"""Appends an asset from a library into the active project
|
|
|
|
A BIM library asset may be a type product (e.g. wall type), product
|
|
(e.g. pump), material, profile, or cost schedule.
|
|
|
|
This copies the asset from the specified library file into the active
|
|
project. It handles all details like ensuring that product materials,
|
|
styles, properties, quantities, and so on are preserved.
|
|
|
|
If an asset contains geometry, the geometric contexts are also
|
|
intelligentely transplanted such that existing equivalent contexts are
|
|
reused.
|
|
|
|
Do not mix units.
|
|
|
|
:param library: The file object containing the asset.
|
|
:param element: An element in the library file of the asset. It may be
|
|
an IfcTypeProduct, IfcProduct, IfcMaterial, IfcCostSchedule, or
|
|
IfcProfileDef.
|
|
:param reuse_identities: Optional dictionary of mapped entities' identities to the
|
|
already created elements. It will be used to avoid creating
|
|
duplicated inverse elements during multiple `project.append_asset` calls. If you want
|
|
to add just 1 asset or if added assets won't have any shared elements, then it can be left empty.
|
|
:param assume_asset_uniqueness_by_name: If True, checks if elements (profiles, materials, styles)
|
|
with the same name already exist in the project and reuses them instead of appending new ones.
|
|
:return: The appended element
|
|
|
|
Example:
|
|
|
|
.. code:: python
|
|
|
|
# Programmatically generate a library. You could do this visually too.
|
|
library = ifcopenshell.api.project.create_file()
|
|
root = ifcopenshell.api.root.create_entity(library, ifc_class="IfcProject", name="Demo Library")
|
|
context = ifcopenshell.api.root.create_entity(library,
|
|
ifc_class="IfcProjectLibrary", name="Demo Library")
|
|
ifcopenshell.api.project.assign_declaration(library, definitions=[context], relating_context=root)
|
|
|
|
# Assign units for our example library
|
|
unit = ifcopenshell.api.unit.add_si_unit(library,
|
|
unit_type="LENGTHUNIT", prefix="MILLI")
|
|
ifcopenshell.api.unit.assign_unit(library, units=[unit])
|
|
|
|
# Let's create a single asset of a 200mm thick concrete wall
|
|
wall_type = ifcopenshell.api.root.create_entity(library, ifc_class="IfcWallType", name="WAL01")
|
|
concrete = ifcopenshell.api.material.add_material(usecase.file, name="CON", category="concrete")
|
|
rel = ifcopenshell.api.material.assign_material(library,
|
|
products=[wall_type], type="IfcMaterialLayerSet")
|
|
layer = ifcopenshell.api.material.add_layer(library,
|
|
layer_set=rel.RelatingMaterial, material=concrete)
|
|
layer.Name = "Structure"
|
|
layer.LayerThickness = 200
|
|
|
|
# Mark our wall type as a reusable asset in our library.
|
|
ifcopenshell.api.project.assign_declaration(library,
|
|
definitions=[wall_type], relating_context=context)
|
|
|
|
# Let's imagine we're starting a new project
|
|
model = ifcopenshell.api.project.create_file()
|
|
project = ifcopenshell.api.root.create_entity(model, ifc_class="IfcProject", name="Test")
|
|
|
|
# Now we can easily append our wall type from our library
|
|
wall_type = ifcopenshell.api.project.append_asset(model, library=library, element=wall_type)
|
|
|
|
Example of adding multiple assets and avoiding duplicated inverses:
|
|
|
|
.. code:: python
|
|
|
|
# since occurrences of IfcWindow of the same type
|
|
# might have shared inverses (e.g. IfcStyledItem)
|
|
# we provide a dictionary that will be populated with newly created items
|
|
# and reused to avoid duplicated elements
|
|
reuse_identities = dict()
|
|
|
|
for element in ifcopenshell.util.selector.filter_elements(model, "IfcWindow"):
|
|
ifcopenshell.api.project.append_asset(
|
|
model, library=library,
|
|
element=wall_type
|
|
reuse_identities=reuse_identities
|
|
)
|
|
|
|
"""
|
|
usecase = Usecase()
|
|
usecase.file: ifcopenshell.file = file
|
|
usecase.settings = {
|
|
"library": library,
|
|
"element": element,
|
|
"reuse_identities": {} if reuse_identities is None else reuse_identities,
|
|
"assume_asset_uniqueness_by_name": assume_asset_uniqueness_by_name,
|
|
}
|
|
return usecase.execute()
|
|
|
|
|
|
class SafeRemovalContext:
|
|
"""Context manager to ensure `remove_deep` won't create invalid entities
|
|
in `reuse_identities` leading to possible crashes.
|
|
|
|
Should be always used if removing an entity that was possibly added by `file_add`.
|
|
"""
|
|
|
|
file: ifcopenshell.file
|
|
reuse_identities: dict[int, ifcopenshell.entity_instance]
|
|
|
|
assume_asset_uniqueness_by_name: bool
|
|
"""If `False`, then all job is done by `file.add`
|
|
and we don't need to worry about invalid entities."""
|
|
|
|
def __init__(
|
|
self,
|
|
ifc_file: ifcopenshell.file,
|
|
reuse_identities: dict[int, ifcopenshell.entity_instance],
|
|
assume_asset_uniqueness_by_name: bool,
|
|
):
|
|
self.file = ifc_file
|
|
self.reuse_identities = reuse_identities
|
|
self.assume_asset_uniqueness_by_name = assume_asset_uniqueness_by_name
|
|
|
|
def __enter__(self):
|
|
if not self.assume_asset_uniqueness_by_name:
|
|
return
|
|
|
|
ifcopenshell.util.element.batch_remove_deep2(self.file)
|
|
|
|
def __exit__(self, *args):
|
|
if not self.assume_asset_uniqueness_by_name:
|
|
return
|
|
|
|
# Collect identities.
|
|
removed_identities: dict[ifcopenshell.entity_instance, int] = {}
|
|
assert self.file.to_delete is not None
|
|
removed_elements = self.file.to_delete
|
|
for identity, element in self.reuse_identities.items():
|
|
if element in removed_elements:
|
|
removed_identities[element] = identity
|
|
assert len(removed_identities) == len(removed_elements)
|
|
|
|
# Actually remove elements.
|
|
for element in self.file.to_delete:
|
|
if element in self.file.to_delete:
|
|
self.file.remove(element)
|
|
self.file.to_delete = None
|
|
|
|
# Clean up dead identities.
|
|
for identity in removed_identities.values():
|
|
del self.reuse_identities[identity]
|
|
|
|
|
|
class Usecase:
|
|
file: ifcopenshell.file
|
|
settings: dict[str, Any]
|
|
assume_asset_uniqueness_by_name: bool
|
|
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):
|
|
# mapping of old element ids to new elements
|
|
self.added_elements: dict[int, ifcopenshell.entity_instance] = {}
|
|
self.reuse_identities: dict[int, ifcopenshell.entity_instance] = self.settings["reuse_identities"]
|
|
self.whitelisted_inverse_attributes = {}
|
|
self.base_material_class = "IfcMaterial" if self.file.schema == "IFC2X3" else "IfcMaterialDefinition"
|
|
self.assume_asset_uniqueness_by_name = self.settings["assume_asset_uniqueness_by_name"]
|
|
|
|
if self.settings["element"].is_a("IfcTypeProduct"):
|
|
self.target_class = "IfcTypeProduct"
|
|
return self.append_type_product()
|
|
elif self.settings["element"].is_a("IfcProduct"):
|
|
self.target_class = "IfcProduct"
|
|
return self.append_product()
|
|
elif self.settings["element"].is_a("IfcMaterial"):
|
|
self.target_class = "IfcMaterial"
|
|
return self.append_material()
|
|
elif self.settings["element"].is_a("IfcCostSchedule"):
|
|
self.target_class = "IfcCostSchedule"
|
|
return self.append_cost_schedule()
|
|
elif self.settings["element"].is_a("IfcProfileDef"):
|
|
self.target_class = "IfcProfileDef"
|
|
return self.append_profile_def()
|
|
elif self.settings["element"].is_a("IfcPresentationStyle"):
|
|
self.target_class = "IfcPresentationStyle"
|
|
return self.append_presentation_style()
|
|
|
|
def by_guid(self, guid: str) -> Union[ifcopenshell.entity_instance, None]:
|
|
try:
|
|
return self.file.by_guid(guid)
|
|
except RuntimeError:
|
|
return None
|
|
|
|
def material_sets_are_equal(self, set1: ifcopenshell.entity_instance, set2: ifcopenshell.entity_instance) -> bool:
|
|
"""Check if two material sets are structurally equivalent."""
|
|
if set1.is_a() != set2.is_a():
|
|
return False
|
|
|
|
ifc_class = set1.is_a()
|
|
|
|
if ifc_class == "IfcMaterialLayerSet":
|
|
layers1 = set1.MaterialLayers or []
|
|
layers2 = set2.MaterialLayers or []
|
|
if len(layers1) != len(layers2):
|
|
return False
|
|
for l1, l2 in zip(layers1, layers2):
|
|
if (l1.Material is None) != (l2.Material is None):
|
|
return False
|
|
if l1.Material and l1.Material.Name != l2.Material.Name:
|
|
return False
|
|
if l1.LayerThickness != l2.LayerThickness:
|
|
return False
|
|
|
|
elif ifc_class == "IfcMaterialConstituentSet":
|
|
constituents1 = set1.MaterialConstituents or []
|
|
constituents2 = set2.MaterialConstituents or []
|
|
if len(constituents1) != len(constituents2):
|
|
return False
|
|
for c1, c2 in zip(constituents1, constituents2):
|
|
if (c1.Material is None) != (c2.Material is None):
|
|
return False
|
|
if c1.Material and c1.Material.Name != c2.Material.Name:
|
|
return False
|
|
if c1.Name != c2.Name:
|
|
return False
|
|
|
|
elif ifc_class == "IfcMaterialProfileSet":
|
|
profiles1 = set1.MaterialProfiles or []
|
|
profiles2 = set2.MaterialProfiles or []
|
|
if len(profiles1) != len(profiles2):
|
|
return False
|
|
for p1, p2 in zip(profiles1, profiles2):
|
|
if (p1.Material is None) != (p2.Material is None):
|
|
return False
|
|
if p1.Material and p1.Material.Name != p2.Material.Name:
|
|
return False
|
|
if (p1.Profile is None) != (p2.Profile is None):
|
|
return False
|
|
if p1.Profile:
|
|
profile_name1 = getattr(p1.Profile, "ProfileName", None)
|
|
profile_name2 = getattr(p2.Profile, "ProfileName", None)
|
|
if profile_name1 != profile_name2:
|
|
return False
|
|
|
|
return True
|
|
|
|
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:
|
|
return self.added_elements[element.id()]
|
|
if element.is_a("IfcRoot"):
|
|
return self.by_guid(element.GlobalId)
|
|
elif not self.assume_asset_uniqueness_by_name:
|
|
return None
|
|
elif element.is_a("IfcMaterial"):
|
|
name = element.Name
|
|
return next((e for e in self.file.by_type("IfcMaterial") if e.Name == name), None)
|
|
|
|
elif element.is_a() in MATERIAL_SETS:
|
|
ifc_class = element.is_a()
|
|
name_attr = "LayerSetName" if ifc_class == "IfcMaterialLayerSet" else "Name"
|
|
material_set_name = getattr(element, name_attr)
|
|
if material_set_name is None:
|
|
return
|
|
for candidate in self.file.by_type(ifc_class):
|
|
if getattr(candidate, name_attr) == material_set_name:
|
|
if self.material_sets_are_equal(element, candidate):
|
|
return candidate
|
|
return None
|
|
|
|
elif element.is_a("IfcProfileDef"):
|
|
profile_name = element.ProfileName
|
|
if profile_name is None:
|
|
return None
|
|
return next((e for e in self.file.by_type("IfcProfileDef") if e.ProfileName == profile_name), None)
|
|
elif element.is_a("IfcPresentationStyle"):
|
|
name = element.Name
|
|
if name is None:
|
|
return 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:
|
|
return None
|
|
|
|
def append_material(self):
|
|
self.whitelisted_inverse_attributes = {
|
|
"IfcMaterial": ["HasExternalReferences", "HasProperties", "HasRepresentation"]
|
|
}
|
|
self.existing_contexts = self.file.by_type("IfcGeometricRepresentationContext")
|
|
element = self.add_element(self.settings["element"])
|
|
if element.HasRepresentation:
|
|
self.reuse_existing_contexts()
|
|
return element
|
|
|
|
def append_cost_schedule(self):
|
|
self.whitelisted_inverse_attributes = {"IfcCostSchedule": ["Controls"], "IfcCostItem": ["IsNestedBy"]}
|
|
return self.add_element(self.settings["element"])
|
|
|
|
def append_profile_def(self):
|
|
self.whitelisted_inverse_attributes = {"IfcProfileDef": ["HasProperties"]}
|
|
return self.add_element(self.settings["element"])
|
|
|
|
def append_presentation_style(self):
|
|
self.whitelisted_inverse_attributes = {}
|
|
return self.add_element(self.settings["element"])
|
|
|
|
def append_type_product(self):
|
|
self.whitelisted_inverse_attributes = {
|
|
"IfcObjectDefinition": ["HasAssociations"],
|
|
"IfcDistributionElementType": ["IsNestedBy"],
|
|
self.base_material_class: ["HasExternalReferences", "HasProperties", "HasRepresentation"],
|
|
"IfcRepresentationItem": ["StyledByItem", "LayerAssignment"],
|
|
"IfcRepresentation": ["LayerAssignments"],
|
|
"IfcProductDefinitionShape": ["HasShapeAspects"],
|
|
"IfcRepresentationMap": ["HasShapeAspects"],
|
|
}
|
|
self.existing_contexts = self.file.by_type("IfcGeometricRepresentationContext")
|
|
element = self.add_element(self.settings["element"])
|
|
self.reuse_existing_contexts()
|
|
return element
|
|
|
|
def append_product(self):
|
|
self.whitelisted_inverse_attributes = {
|
|
"IfcObjectDefinition": ["HasAssociations"],
|
|
"IfcObject": ["IsDefinedBy.IfcRelDefinesByProperties"],
|
|
"IfcElement": ["HasOpenings"],
|
|
"IfcDistributionElement": ["IsNestedBy"],
|
|
self.base_material_class: ["HasExternalReferences", "HasProperties", "HasRepresentation"],
|
|
"IfcRepresentationItem": [
|
|
"StyledByItem",
|
|
"LayerAssignments" if self.file.schema == "IFC2X3" else "LayerAssignment",
|
|
],
|
|
"IfcRepresentation": ["LayerAssignments"],
|
|
"IfcProductDefinitionShape": ["HasShapeAspects"],
|
|
"IfcRepresentationMap": ["HasShapeAspects"],
|
|
}
|
|
self.existing_contexts = self.file.by_type("IfcGeometricRepresentationContext")
|
|
element = self.add_element(self.settings["element"])
|
|
self.reuse_existing_contexts()
|
|
|
|
placement = element.ObjectPlacement
|
|
if placement is not None:
|
|
matrix = ifcopenshell.util.placement.get_local_placement(placement)
|
|
matrix = ifcopenshell.util.geolocation.auto_local2global(self.settings["library"], matrix)
|
|
matrix = ifcopenshell.util.geolocation.auto_global2local(self.file, matrix)
|
|
with SafeRemovalContext(self.file, self.reuse_identities, self.assume_asset_uniqueness_by_name):
|
|
ifcopenshell.api.geometry.edit_object_placement(self.file, element, matrix, is_si=False)
|
|
|
|
element_type = ifcopenshell.util.element.get_type(self.settings["element"])
|
|
if element_type:
|
|
ifcopenshell.api.owner.settings.factory_reset()
|
|
new_type = ifcopenshell.api.project.append_asset(
|
|
self.file,
|
|
library=self.settings["library"],
|
|
element=element_type,
|
|
reuse_identities=self.reuse_identities,
|
|
)
|
|
ifcopenshell.api.type.assign_type(
|
|
self.file,
|
|
should_run_listeners=False, # ty:ignore[unknown-argument]
|
|
related_objects=[element],
|
|
relating_type=new_type,
|
|
should_map_representations=False,
|
|
)
|
|
ifcopenshell.api.owner.settings.restore()
|
|
|
|
return element
|
|
|
|
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:
|
|
return
|
|
existing_element = self.get_existing_element(element)
|
|
if existing_element:
|
|
return existing_element
|
|
new = self.file_add(element)
|
|
self.added_elements[element.id()] = new
|
|
self.check_inverses(element)
|
|
subelement_queue = self.settings["library"].traverse(element, max_levels=1)[1:]
|
|
while subelement_queue:
|
|
subelement = subelement_queue.pop(0)
|
|
existing_element = self.get_existing_element(subelement)
|
|
if existing_element:
|
|
self.added_elements[subelement.id()] = existing_element
|
|
if not self.has_whitelisted_inverses(existing_element):
|
|
self.check_inverses(subelement)
|
|
else:
|
|
self.added_elements[subelement.id()] = self.file_add(subelement)
|
|
self.check_inverses(subelement)
|
|
subelement_queue.extend(self.settings["library"].traverse(subelement, max_levels=1)[1:])
|
|
return new
|
|
|
|
def has_whitelisted_inverses(self, element: ifcopenshell.entity_instance) -> bool:
|
|
for source_class, attributes in self.whitelisted_inverse_attributes.items():
|
|
if not element.is_a(source_class):
|
|
continue
|
|
for attribute in attributes:
|
|
attribute_class = None
|
|
if "." in attribute:
|
|
attribute, attribute_class = attribute.split(".")
|
|
value = getattr(element, attribute, [])
|
|
if attribute_class:
|
|
for subvalue in value:
|
|
if subvalue.is_a(attribute_class):
|
|
return True
|
|
elif value:
|
|
return True
|
|
return False
|
|
|
|
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():
|
|
if not element.is_a(source_class):
|
|
continue
|
|
for attribute in attributes:
|
|
attribute_class = None
|
|
if "." in attribute:
|
|
attribute, attribute_class = attribute.split(".")
|
|
for inverse in getattr(element, attribute, []):
|
|
if attribute_class and inverse.is_a(attribute_class):
|
|
self.add_inverse_element(inverse)
|
|
elif not attribute_class:
|
|
self.add_inverse_element(inverse)
|
|
|
|
def add_inverse_element(self, element: ifcopenshell.entity_instance) -> None:
|
|
"""Add inverse element.
|
|
|
|
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
|
|
# to avoid adding representations / items that are not related to current append_asset.
|
|
skip_not_reused_entities_attr_i = None
|
|
if element.is_a("IfcPresentationLayerAssignment"):
|
|
# 3 IfcPresentationLayerAssignment.AssignedItems
|
|
skip_not_reused_entities_attr_i = 2
|
|
|
|
element_identity = element.wrapped_data.identity()
|
|
|
|
# Check if inverse element was created before.
|
|
# Still need to recreate it again - e.g. it could be some rel
|
|
# that now needs it's RelatingObjects to be extended by the current asset.
|
|
existing_rel = None
|
|
if (new := self.reuse_identities.get(element_identity)) is not None:
|
|
# Currently known cases requiring attributes reassignment are rels.
|
|
if not new.is_a("IfcRelationship"):
|
|
return
|
|
elif element.is_a("IfcRelationship") and (existing_rel := self.by_guid(element.GlobalId)):
|
|
new = existing_rel
|
|
else:
|
|
new = self.file.create_entity(element.is_a())
|
|
self.reuse_identities[element_identity] = new
|
|
|
|
for i, attribute in enumerate(element):
|
|
new_attribute = None
|
|
if isinstance(attribute, ifcopenshell.entity_instance):
|
|
# Void and projection relationships are special because they
|
|
# are "dependent" relationships, so we always consider them.
|
|
# We do _not_ whitelist (i.e. in is_another_asset)
|
|
# IfcFeatureElement because you can have things like
|
|
# IfcRelAssociatesClassification to openings! We only ever want
|
|
# to consider IfcFeatureElements in IfcRelVoidsElements and
|
|
# IfcRelProjectsElements.
|
|
if element.is_a() in ("IfcRelVoidsElement", "IfcRelProjectsElement") or not self.is_another_asset(
|
|
attribute
|
|
):
|
|
new_attribute = self.add_element(attribute)
|
|
elif isinstance(attribute, tuple) and attribute and isinstance(attribute[0], ifcopenshell.entity_instance):
|
|
new_attribute = []
|
|
for item in attribute:
|
|
if self.is_another_asset(item):
|
|
continue
|
|
if skip_not_reused_entities_attr_i is not None and i == skip_not_reused_entities_attr_i:
|
|
identity = item.wrapped_data.identity()
|
|
if (item := self.reuse_identities.get(identity)) is None:
|
|
continue
|
|
else:
|
|
item = self.add_element(item)
|
|
new_attribute.append(item)
|
|
# If rel exists we need to make sure previously assigned elements are untouched
|
|
# e.g. not to assign a material or a pset from element.
|
|
if existing_rel:
|
|
new_attribute.extend(existing_rel[i])
|
|
new_attribute = list(set(new_attribute))
|
|
else:
|
|
new_attribute = attribute
|
|
if new_attribute is not None:
|
|
new[i] = new_attribute
|
|
|
|
def is_another_asset(self, element: ifcopenshell.entity_instance) -> bool:
|
|
"""Is IFC entity from inverse attribute is another asset to append that should be skipped."""
|
|
if element == self.settings["element"]:
|
|
return False
|
|
elif element.is_a("IfcRoot") and self.by_guid(element.GlobalId) is not None:
|
|
return False
|
|
elif element.is_a("IfcDistributionPort"):
|
|
return False
|
|
elif element.is_a(self.target_class):
|
|
return True
|
|
elif self.target_class == "IfcProduct" and element.is_a("IfcTypeProduct"):
|
|
return True
|
|
elif self.target_class == "IfcTypeProduct" and element.is_a("IfcProduct"):
|
|
return True
|
|
return False
|
|
|
|
def reuse_existing_contexts(self) -> None:
|
|
added_contexts = set([e for e in self.added_elements.values() if e.is_a("IfcGeometricRepresentationContext")])
|
|
added_contexts -= set(self.existing_contexts)
|
|
sorted_added_contexts = [c for c in added_contexts if c.is_a() == "IfcGeometricRepresentationContext"]
|
|
sorted_added_contexts.extend([c for c in added_contexts if c.is_a() == "IfcGeometricRepresentationSubContext"])
|
|
for added_context in sorted_added_contexts:
|
|
equivalent_existing_context = self.get_equivalent_existing_context(added_context)
|
|
if not equivalent_existing_context:
|
|
equivalent_existing_context = self.create_equivalent_context(added_context)
|
|
for inverse in self.file.get_inverse(added_context):
|
|
ifcopenshell.util.element.replace_attribute(inverse, added_context, equivalent_existing_context)
|
|
|
|
with SafeRemovalContext(self.file, self.reuse_identities, self.assume_asset_uniqueness_by_name):
|
|
for added_context in added_contexts:
|
|
ifcopenshell.util.element.remove_deep2(self.file, added_context)
|
|
|
|
def get_equivalent_existing_context(
|
|
self, added_context: ifcopenshell.entity_instance
|
|
) -> Union[ifcopenshell.entity_instance, None]:
|
|
for context in self.existing_contexts:
|
|
if context.is_a() != added_context.is_a():
|
|
continue
|
|
if context.is_a("IfcGeometricRepresentationSubContext"):
|
|
if (
|
|
context.ContextType == added_context.ContextType
|
|
and context.ContextIdentifier == added_context.ContextIdentifier
|
|
and context.TargetView == added_context.TargetView
|
|
):
|
|
return context
|
|
elif (
|
|
context.ContextType == added_context.ContextType
|
|
and context.ContextIdentifier == added_context.ContextIdentifier
|
|
):
|
|
return context
|
|
|
|
def create_equivalent_context(self, added_context: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance:
|
|
if added_context.is_a("IfcGeometricRepresentationSubContext"):
|
|
parent = self.get_equivalent_existing_context(added_context.ParentContext)
|
|
if not parent:
|
|
parent = self.create_equivalent_context(added_context.ParentContext)
|
|
self.existing_contexts.append(parent)
|
|
context = ifcopenshell.api.context.add_context(
|
|
self.file,
|
|
parent=parent,
|
|
context_type=added_context.ContextType,
|
|
context_identifier=added_context.ContextIdentifier,
|
|
target_view=added_context.TargetView,
|
|
)
|
|
else:
|
|
context = ifcopenshell.api.context.add_context(
|
|
self.file,
|
|
context_type=added_context.ContextType,
|
|
context_identifier=added_context.ContextIdentifier,
|
|
)
|
|
self.existing_contexts.append(context)
|
|
return context
|
|
|
|
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.
|
|
|
|
The problem with `file.add` it's recursively adding element and all it's attributes
|
|
and there is no control to prevent it from adding certain type of elements.
|
|
"""
|
|
|
|
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
|
|
|
|
ifc_file = self.file
|
|
if not self.assume_asset_uniqueness_by_name or element.id() == 0:
|
|
# file.add doesn't convert units for IfcLengthMeasure entities.
|
|
if element.is_a("IfcLengthMeasure"):
|
|
return ifc_file.create_entity(element.is_a(), element.wrappedValue * get_conversion_factor())
|
|
return ifc_file.add(element)
|
|
|
|
reuse_identities = self.reuse_identities
|
|
element_identity = element.wrapped_data.identity()
|
|
if added_element := reuse_identities.get(element_identity):
|
|
return added_element
|
|
|
|
ifc_class = element.is_a()
|
|
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_
|
|
|
|
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"):
|
|
profile_name = element.ProfileName
|
|
if profile_name is not None:
|
|
existing_profile = next(
|
|
(e for e in ifc_file.by_type("IfcProfileDef") if e.ProfileName == profile_name), None
|
|
)
|
|
if existing_profile is not None:
|
|
reuse_identities[element_identity] = existing_profile
|
|
return existing_profile
|
|
|
|
elif element.is_a("IfcMaterial"):
|
|
material_name = element.Name
|
|
existing_material = next((e for e in ifc_file.by_type("IfcMaterial") if e.Name == material_name), None)
|
|
if existing_material is not None:
|
|
reuse_identities[element_identity] = existing_material
|
|
return existing_material
|
|
|
|
elif ifc_class in MATERIAL_SETS:
|
|
name_attr = "LayerSetName" if ifc_class == "IfcMaterialLayerSet" else "Name"
|
|
material_set_name = getattr(element, name_attr)
|
|
if material_set_name is not None:
|
|
for candidate in ifc_file.by_type(ifc_class):
|
|
if getattr(candidate, name_attr) == material_set_name:
|
|
if self.material_sets_are_equal(element, candidate):
|
|
reuse_identities[element_identity] = candidate
|
|
return candidate
|
|
|
|
elif element.is_a("IfcPresentationStyle"):
|
|
style_name = element.Name
|
|
if style_name is not None:
|
|
existing_style = next((e for e in ifc_file.by_type(ifc_class) if e.Name == style_name), None)
|
|
if existing_style is not None:
|
|
reuse_identities[element_identity] = existing_style
|
|
return existing_style
|
|
|
|
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.
|
|
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 = 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 = file_add_(attr_value)
|
|
|
|
elif isinstance(attr_value, tuple):
|
|
# Assume type is consistent across the tuple.
|
|
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`.
|
|
new = ifc_file.create_entity(ifc_class)
|
|
reuse_identities[element_identity] = new
|
|
for attr_index, attr_value in attrs.items():
|
|
new[attr_index] = attr_value
|
|
|
|
return new
|