mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-13 10:57:49 +00:00
e0a1988044
IfcSurfaceFeature (e.g. road markings) adheres to a host element through IfcRelAdheresToElement, a [1:1] cardinality hierarchical relationship in the same family as aggregation, containment and nesting since IFC4.3. The spatial traversal never followed it, so surface features had no resolvable parent or container: on import they landed in the Unsorted collection instead of the host's spatial collection, and were dropped entirely in DECOMPOSITION filter mode. Add get_adhered_element (feature to host) to the get_parent resolver chain and walk HasSurfaceFeatures in get_decomposition, plus a get_surface_features helper mirroring get_parts/get_contained. With get_parent resolving adherence, get_container now returns the host's spatial container, so tool.Collector places surface features under the host. Also follow HasSurfaceFeatures in the Bonsai DECOMPOSITION filter path so they load in that mode. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1963 lines
74 KiB
Python
1963 lines
74 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 import namedtuple
|
|
from collections.abc import Callable, Generator, Sequence
|
|
from typing import Any, Literal, Optional, Union, overload
|
|
|
|
import ifcopenshell
|
|
import ifcopenshell.guid
|
|
import ifcopenshell.util.element
|
|
import ifcopenshell.util.representation
|
|
|
|
MATERIAL_TYPE = Literal[
|
|
"IfcMaterial",
|
|
"IfcMaterialConstituentSet",
|
|
"IfcMaterialLayerSet",
|
|
"IfcMaterialLayerSetUsage",
|
|
"IfcMaterialProfileSet",
|
|
"IfcMaterialProfileSetUsage",
|
|
"IfcMaterialList",
|
|
]
|
|
|
|
PrioritisedLayer = namedtuple("PrioritisedLayer", "priority material thickness")
|
|
PrioritisedProfile = namedtuple("PrioritisedProfile", "priority material profile")
|
|
|
|
|
|
def get_pset(
|
|
element: ifcopenshell.entity_instance,
|
|
name: str,
|
|
prop: Optional[str] = None,
|
|
psets_only: bool = False,
|
|
qtos_only: bool = False,
|
|
should_inherit: bool = True,
|
|
verbose: bool = False,
|
|
) -> Union[Any, dict[str, Any]]:
|
|
"""Retrieve a single property set or single property
|
|
|
|
This is more efficient than ifcopenshell.util.element.get_psets if you know
|
|
exactly which property set and property you are after.
|
|
|
|
If should_inherit is true, the pset "id" only refers to the ID of the
|
|
occurrence, not the type's pset.
|
|
|
|
:param element: The IFC Element entity
|
|
:param name: The name of the pset
|
|
:param prop: The name of the property
|
|
:param psets_only: Default as False. Set to true if only property sets are needed.
|
|
:param qtos_only: Default as False. Set to true if only quantities are needed.
|
|
:param should_inherit: Default as True. Set to false if you don't want to inherit property sets from the Type.
|
|
:return: A dictionary of property names and values, or a single value if a
|
|
property is specified.
|
|
|
|
Example:
|
|
|
|
.. code:: python
|
|
|
|
element = ifc_file.by_type("IfcWall")[0]
|
|
psets_and_qtos = ifcopenshell.util.element.get_pset(element, "Pset_WallCommon")
|
|
"""
|
|
pset = None
|
|
type_pset = None
|
|
ifc_file = element.file
|
|
is_ifc2x3 = ifc_file.schema == "IFC2X3"
|
|
is_profile = False
|
|
|
|
if element.is_a("IfcTypeObject"):
|
|
for definition in element.HasPropertySets or []:
|
|
if definition.Name == name:
|
|
pset = definition
|
|
break
|
|
elif (
|
|
(is_ifc2x3_material := (is_ifc2x3 and element.is_a("IfcMaterial")))
|
|
or element.is_a("IfcMaterialDefinition")
|
|
or (is_profile := element.is_a("IfcProfileDef"))
|
|
):
|
|
if is_ifc2x3_material:
|
|
# Support extended props as they do have a name.
|
|
for definition in ifc_file.by_type("IfcExtendedMaterialProperties"):
|
|
if definition.Material == element and definition.Name == name:
|
|
pset = definition
|
|
break
|
|
elif is_ifc2x3 and is_profile:
|
|
# Don't support them as they don't have a name.
|
|
pass
|
|
else:
|
|
# IfcProfileDef or IfcMaterialDefinition, IFC4+.
|
|
for definition in element.HasProperties or []:
|
|
if definition.Name == name:
|
|
pset = definition
|
|
break
|
|
elif (is_defined_by := getattr(element, "IsDefinedBy", None)) is not None:
|
|
# other IfcObjectDefinition
|
|
if should_inherit:
|
|
element_type = ifcopenshell.util.element.get_type(element)
|
|
if element_type:
|
|
type_pset = get_pset(element_type, name, prop, should_inherit=False, verbose=verbose)
|
|
for relationship in is_defined_by:
|
|
if relationship.is_a("IfcRelDefinesByProperties"):
|
|
definition = relationship.RelatingPropertyDefinition
|
|
if definition.Name == name:
|
|
pset = definition
|
|
break
|
|
|
|
if pset:
|
|
if (
|
|
psets_only
|
|
and not pset.is_a("IfcPropertySet")
|
|
and not pset.is_a("IfcPreDefinedPropertySet")
|
|
and not (is_ifc2x3 and pset.is_a("IfcExtendedMaterialProperties"))
|
|
):
|
|
pset = None
|
|
elif qtos_only and not pset.is_a("IfcElementQuantity"):
|
|
pset = None
|
|
|
|
if type_pset is not None and not prop:
|
|
if psets_only or qtos_only:
|
|
type_pset_element = element.file.by_id(type_pset["id"])
|
|
if (
|
|
psets_only
|
|
and not type_pset_element.is_a("IfcPropertySet")
|
|
and not type_pset_element.is_a("IfcPreDefinedPropertySet")
|
|
):
|
|
type_pset = None
|
|
elif qtos_only and not type_pset_element.is_a("IfcElementQuantity"):
|
|
type_pset = None
|
|
|
|
if pset is None and type_pset is None:
|
|
return
|
|
|
|
if not prop:
|
|
if type_pset:
|
|
occurrence_pset = get_property_definition(pset, verbose=verbose)
|
|
if occurrence_pset:
|
|
type_pset.update(occurrence_pset)
|
|
return type_pset
|
|
return get_property_definition(pset, verbose=verbose)
|
|
|
|
value = get_property_definition(pset, prop=prop, verbose=verbose)
|
|
if value is None and type_pset is not None:
|
|
return type_pset
|
|
return value
|
|
|
|
|
|
def get_psets(
|
|
element: ifcopenshell.entity_instance, psets_only=False, qtos_only=False, should_inherit=True, verbose=False
|
|
) -> dict[str, dict[str, Any]]:
|
|
"""Retrieve property sets, their related properties' names & values and ids.
|
|
|
|
If should_inherit is true, the pset "id" only refers to the ID of the
|
|
occurrence, not the type's pset.
|
|
|
|
:param element: The IFC Element entity
|
|
:param psets_only: Default as False. Set to true if only property sets are needed.
|
|
:param qtos_only: Default as False. Set to true if only quantities are needed.
|
|
:param should_inherit: Default as True. Set to false if you don't want to inherit property sets from the Type.
|
|
:param verbose: More detailed prop values, defaults to False.
|
|
:return: Key, value pair of psets' names and their properties' names & values
|
|
|
|
Example:
|
|
|
|
.. code:: python
|
|
|
|
element = ifc_file.by_type("IfcWall")[0]
|
|
psets = ifcopenshell.util.element.get_psets(element, psets_only=True)
|
|
qsets = ifcopenshell.util.element.get_psets(element, qtos_only=True)
|
|
psets_and_qtos = ifcopenshell.util.element.get_psets(element)
|
|
"""
|
|
ifc_file = element.file
|
|
is_ifc2x3 = ifc_file.schema == "IFC2X3"
|
|
psets = {}
|
|
if element.is_a("IfcTypeObject"):
|
|
for definition in element.HasPropertySets or []:
|
|
if psets_only and not definition.is_a("IfcPropertySet") and not definition.is_a("IfcPreDefinedPropertySet"):
|
|
continue
|
|
if qtos_only and not definition.is_a("IfcElementQuantity"):
|
|
continue
|
|
psets.setdefault(definition.Name, {}).update(get_property_definition(definition, verbose=verbose))
|
|
# NOTE: doesn't account for IFC2X3 missing HasProperties
|
|
elif (
|
|
(is_ifc2x3_material := (is_ifc2x3 and element.is_a("IfcMaterial")))
|
|
or element.is_a("IfcMaterialDefinition")
|
|
or element.is_a("IfcProfileDef")
|
|
):
|
|
definitions: list[ifcopenshell.entity_instance]
|
|
if is_ifc2x3:
|
|
if is_ifc2x3_material:
|
|
# Only extended props have a name.
|
|
definitions = [d for d in ifc_file.by_type("IfcExtendedMaterialProperties") if d.Material == element]
|
|
else:
|
|
# Ignoring profiles as they don't have names.
|
|
definitions = []
|
|
else:
|
|
definitions = getattr(element, "HasProperties", None) or []
|
|
for definition in definitions:
|
|
if qtos_only:
|
|
continue
|
|
psets.setdefault(definition.Name, {}).update(get_property_definition(definition, verbose=verbose))
|
|
elif (is_defined_by := getattr(element, "IsDefinedBy", None)) is not None:
|
|
# other IfcObjectDefinition
|
|
if should_inherit:
|
|
element_type = ifcopenshell.util.element.get_type(element)
|
|
if element_type:
|
|
psets = get_psets(
|
|
element_type, psets_only=psets_only, qtos_only=qtos_only, should_inherit=False, verbose=verbose
|
|
)
|
|
for relationship in is_defined_by:
|
|
if relationship.is_a("IfcRelDefinesByProperties"):
|
|
definition = relationship.RelatingPropertyDefinition
|
|
if (
|
|
psets_only
|
|
and not definition.is_a("IfcPropertySet")
|
|
and not definition.is_a("IfcPreDefinedPropertySet")
|
|
):
|
|
continue
|
|
if qtos_only and not definition.is_a("IfcElementQuantity"):
|
|
continue
|
|
psets.setdefault(definition.Name, {}).update(get_property_definition(definition, verbose=verbose))
|
|
return psets
|
|
|
|
|
|
@overload
|
|
def get_property_definition(
|
|
definition: Optional[ifcopenshell.entity_instance], prop: None = None, verbose=False
|
|
) -> dict[str, Any]: ...
|
|
@overload
|
|
def get_property_definition(definition: Optional[ifcopenshell.entity_instance], prop: str, verbose=False) -> Any: ...
|
|
@overload
|
|
def get_property_definition(definition: None, prop: None = None, verbose: bool = False) -> None: ...
|
|
def get_property_definition(
|
|
definition: Optional[ifcopenshell.entity_instance], prop: Optional[str] = None, verbose=False
|
|
) -> Union[Any, dict[str, Any]]:
|
|
"""if prop name is not provided in `prop`, will return dict of all available properties
|
|
otherwise will return the value of the specified `prop`.
|
|
"""
|
|
if not definition:
|
|
return
|
|
|
|
ifc_class = definition.is_a()
|
|
|
|
if prop:
|
|
if ifc_class == "IfcElementQuantity":
|
|
return get_quantity(definition.Quantities, prop, verbose=verbose)
|
|
elif ifc_class == "IfcPropertySet":
|
|
return get_property(definition.HasProperties, prop, verbose=verbose)
|
|
elif ifc_class == "IfcMaterialProperties" or ifc_class == "IfcProfileProperties":
|
|
# IfcExtendedProperties
|
|
return get_property(definition.Properties, prop, verbose=verbose)
|
|
elif ifc_class == "IfcExtendedMaterialProperties":
|
|
# IFC2X3.
|
|
return get_property(definition.ExtendedProperties, prop, verbose=verbose)
|
|
else:
|
|
# Entity introduced in IFC4
|
|
# definition.is_a('IfcPreDefinedPropertySet'):
|
|
for i in range(4, len(definition)):
|
|
if definition.attribute_name(i) == prop:
|
|
if (v := definition[i]) is not None:
|
|
return v
|
|
return
|
|
|
|
props = {}
|
|
if ifc_class == "IfcElementQuantity":
|
|
# 5 IfcElementQuantity.Quantities
|
|
props.update(get_quantities(definition[5], verbose=verbose))
|
|
elif ifc_class == "IfcPropertySet":
|
|
# 5 IfcPropertySet.HasProperties
|
|
props.update(get_properties(definition[4], verbose=verbose))
|
|
elif ifc_class == "IfcMaterialProperties" or ifc_class == "IfcProfileProperties":
|
|
# 2 IfcExtendedProperties.Properties
|
|
props.update(get_properties(definition[2], verbose=verbose))
|
|
elif ifc_class == "IfcExtendedMaterialProperties":
|
|
# 1 IfcExtendedMaterialProperties.ExtendedProperties
|
|
props.update(get_properties(definition[1], verbose=verbose))
|
|
else:
|
|
# Entity introduced in IFC4
|
|
# definition.is_a('IfcPreDefinedPropertySet'):
|
|
for prop_i in range(4, len(definition)):
|
|
if (v := definition[prop_i]) is not None:
|
|
props[definition.attribute_name(prop_i)] = v
|
|
props["id"] = definition.id()
|
|
return props
|
|
|
|
|
|
@overload
|
|
def get_quantity(quantities: list[ifcopenshell.entity_instance], name: str, verbose: Literal[False] = False) -> Any: ...
|
|
@overload
|
|
def get_quantity(
|
|
quantities: list[ifcopenshell.entity_instance], name: str, verbose: Literal[True]
|
|
) -> dict[str, Any]: ...
|
|
def get_quantity(
|
|
quantities: list[ifcopenshell.entity_instance], name: str, verbose=False
|
|
) -> Union[Any, dict[str, Any]]:
|
|
for quantity in quantities or []:
|
|
# 0 IfcPhysicalQuantity.Name
|
|
if quantity[0] != name:
|
|
continue
|
|
if quantity.is_a("IfcPhysicalSimpleQuantity"):
|
|
# 3 IfcPhysicalSimpleQuantity.XXXValue
|
|
result = quantity[3]
|
|
elif quantity.is_a("IfcPhysicalComplexQuantity"):
|
|
data = {k: v for k, v in quantity.get_info().items() if v is not None and k != "Name"}
|
|
data["properties"] = get_quantities(quantity.HasQuantities, verbose=verbose)
|
|
del data["HasQuantities"]
|
|
result = data
|
|
if verbose:
|
|
result = {"id": quantity.id(), "class": quantity.is_a(), "value": result}
|
|
return result
|
|
|
|
|
|
@overload
|
|
def get_quantities(
|
|
quantities: list[ifcopenshell.entity_instance], verbose: Literal[False] = False
|
|
) -> dict[str, Any]: ...
|
|
@overload
|
|
def get_quantities(
|
|
quantities: list[ifcopenshell.entity_instance], verbose: Literal[True]
|
|
) -> dict[str, dict[str, Any]]: ...
|
|
def get_quantities(
|
|
quantities: list[ifcopenshell.entity_instance], verbose=False
|
|
) -> dict[str, Union[Any, dict[str, Any]]]:
|
|
results = {}
|
|
for quantity in quantities or []:
|
|
# 0 IfcPhysicalQuantity.Name
|
|
quantity_name = quantity[0]
|
|
if quantity.is_a("IfcPhysicalSimpleQuantity"):
|
|
# 3 IfcPhysicalSimpleQuantity.XXXValue
|
|
results[quantity_name] = quantity[3]
|
|
if verbose:
|
|
results[quantity_name] = {
|
|
"id": quantity.id(),
|
|
"class": quantity.is_a(),
|
|
"value": results[quantity_name],
|
|
}
|
|
elif quantity.is_a("IfcPhysicalComplexQuantity"):
|
|
data = {k: v for k, v in quantity.get_info().items() if v is not None and k != "Name"}
|
|
data["properties"] = get_quantities(quantity.HasQuantities, verbose=verbose)
|
|
del data["HasQuantities"]
|
|
results[quantity_name] = data
|
|
if verbose:
|
|
results[quantity_name] = {
|
|
"id": data["id"],
|
|
"class": data["class"],
|
|
"value": results[quantity_name],
|
|
}
|
|
return results
|
|
|
|
|
|
@overload
|
|
def get_property(properties: list[ifcopenshell.entity_instance], name: str, verbose: Literal[False] = False) -> Any: ...
|
|
@overload
|
|
def get_property(
|
|
properties: list[ifcopenshell.entity_instance], name: str, verbose: Literal[True]
|
|
) -> dict[str, Any]: ...
|
|
def get_property(
|
|
properties: list[ifcopenshell.entity_instance], name: str, verbose=False
|
|
) -> Union[Any, dict[str, Any]]:
|
|
for prop in properties or []:
|
|
if prop.Name != name:
|
|
continue
|
|
is_single_value = False # For now we pass value type only for single values.
|
|
if prop.is_a("IfcPropertySingleValue"):
|
|
# 2 IfcPropertySingleValue.NominalValue
|
|
result = v.wrappedValue if (v := prop[2]) else None
|
|
result_type = v.is_a() if v else None
|
|
is_single_value = True
|
|
elif prop.is_a("IfcPropertyEnumeratedValue"):
|
|
# 2 IfcPropertyEnumeratedValue.EnumerationValues
|
|
result = [v.wrappedValue for v in values] if (values := prop[2]) else None
|
|
elif prop.is_a("IfcPropertyListValue"):
|
|
# 2 IfcPropertyListValue.ListValues
|
|
result = [v.wrappedValue for v in values] if (values := prop[2]) else None
|
|
elif prop.is_a("IfcPropertyBoundedValue"):
|
|
data = prop.get_info()
|
|
del data["Unit"]
|
|
result = data
|
|
elif prop.is_a("IfcPropertyTableValue"):
|
|
result = prop.get_info()
|
|
elif prop.is_a("IfcComplexProperty"):
|
|
data = {k: v for k, v in prop.get_info().items() if v is not None and k != "Name"}
|
|
data["properties"] = get_properties(prop.HasProperties, verbose=verbose)
|
|
del data["HasProperties"]
|
|
result = data
|
|
if verbose:
|
|
result = {"id": prop.id(), "class": prop.is_a(), "value": result}
|
|
if is_single_value:
|
|
result["value_type"] = result_type
|
|
return result
|
|
|
|
|
|
@overload
|
|
def get_properties(
|
|
properties: list[ifcopenshell.entity_instance], verbose: Literal[False] = False
|
|
) -> dict[str, Any]: ...
|
|
@overload
|
|
def get_properties(
|
|
properties: list[ifcopenshell.entity_instance], verbose: Literal[True]
|
|
) -> dict[str, dict[str, Any]]: ...
|
|
def get_properties(
|
|
properties: list[ifcopenshell.entity_instance], verbose=False
|
|
) -> dict[str, Union[Any, dict[str, Any]]]:
|
|
results = {}
|
|
for prop in properties or []:
|
|
ifc_class = prop.is_a()
|
|
prop_name = prop[0] # 0 IfcProperty.Name
|
|
if ifc_class == "IfcPropertySingleValue":
|
|
# 2 IfcPropertySingleValue.NominalValue
|
|
results[prop_name] = v.wrappedValue if (v := prop[2]) else None
|
|
if verbose:
|
|
results[prop_name] = {
|
|
"id": prop.id(),
|
|
"class": prop.is_a(),
|
|
"value": results[prop_name],
|
|
"value_type": v.is_a() if v else None,
|
|
}
|
|
elif ifc_class == "IfcPropertyEnumeratedValue":
|
|
# 2 IfcPropertyEnumeratedValue.EnumerationValues
|
|
results[prop_name] = [v.wrappedValue for v in values] if (values := prop[2]) else None
|
|
if verbose:
|
|
results[prop_name] = {
|
|
"id": prop.id(),
|
|
"class": prop.is_a(),
|
|
"value": results[prop_name],
|
|
}
|
|
elif ifc_class == "IfcPropertyListValue":
|
|
# 2 IfcPropertyListValue.ListValues
|
|
results[prop_name] = [v.wrappedValue for v in values] if (values := prop[2]) else None
|
|
if verbose:
|
|
results[prop_name] = {
|
|
"id": prop.id(),
|
|
"class": prop.is_a(),
|
|
"value": results[prop_name],
|
|
}
|
|
elif ifc_class == "IfcPropertyBoundedValue":
|
|
data = prop.get_info()
|
|
del data["Unit"]
|
|
results[prop_name] = data
|
|
if verbose:
|
|
results[prop_name] = {
|
|
"id": data["id"],
|
|
"class": data["type"],
|
|
"value": results[prop_name],
|
|
}
|
|
elif ifc_class == "IfcPropertyTableValue":
|
|
data = prop.get_info()
|
|
results[prop_name] = data
|
|
if verbose:
|
|
results[prop_name] = {
|
|
"id": data["id"],
|
|
"class": data["type"],
|
|
"value": results[prop_name],
|
|
}
|
|
elif ifc_class == "IfcComplexProperty":
|
|
data = {k: v for k, v in prop.get_info().items() if v is not None and k != "Name"}
|
|
data["properties"] = get_properties(prop.HasProperties, verbose=verbose)
|
|
del data["HasProperties"]
|
|
results[prop_name] = data
|
|
if verbose:
|
|
results[prop_name] = {"id": data["id"], "class": data["type"], "value": results[prop_name]}
|
|
return results
|
|
|
|
|
|
def get_elements_by_pset(pset: ifcopenshell.entity_instance) -> set[ifcopenshell.entity_instance]:
|
|
"""Retrieve the elements (or element types) that are using the provided property set."""
|
|
is_ifc2x3 = pset.file.schema == "IFC2X3"
|
|
elements = set()
|
|
if pset.is_a("IfcPropertySet") or pset.is_a("IfcPreDefinedPropertySet") or pset.is_a("IfcElementQuantity"):
|
|
rels = pset.PropertyDefinitionOf if is_ifc2x3 else pset.DefinesOccurrence
|
|
for rel in rels:
|
|
elements.update(rel.RelatedObjects)
|
|
for element_type in pset.DefinesType:
|
|
elements.add(element_type)
|
|
elif pset.is_a("IfcProfileProperties"):
|
|
elements.add(pset.ProfileDefinition)
|
|
elif pset.is_a("IfcMaterialProperties"):
|
|
elements.add(pset.Material)
|
|
else:
|
|
raise Exception(f"Unexpected pset type: '{pset.is_a()}' ({pset}).")
|
|
return elements
|
|
|
|
|
|
def get_element_mass_density(element: ifcopenshell.entity_instance) -> Union[float, None]:
|
|
"""Calculate object mass density based on material's Pset_MaterialCommon.MassDensity.
|
|
|
|
:param element: IFC element entity.
|
|
:return: ``float`` mass density in project units if calculation was successful, ``None`` if element either
|
|
doesn't have a material or this type of material is not supported.
|
|
"""
|
|
material = ifcopenshell.util.element.get_material(element)
|
|
if material is None:
|
|
return
|
|
|
|
if (
|
|
material.is_a("IfcMaterialLayerSet")
|
|
or material.is_a("IfcMaterialProfileSet")
|
|
or material.is_a("IfcMaterialConstituentSet")
|
|
):
|
|
return
|
|
|
|
if material.is_a("IfcMaterial"):
|
|
material_mass_density = ifcopenshell.util.element.get_pset(material, "Pset_MaterialCommon", "MassDensity")
|
|
return material_mass_density
|
|
|
|
if material.is_a("IfcMaterialLayerSetUsage"):
|
|
material_layers = material.ForLayerSet.MaterialLayers
|
|
densities = []
|
|
thicknesses = []
|
|
obj_mass_density = 0
|
|
for material_layer in material_layers:
|
|
material_mass_density = ifcopenshell.util.element.get_pset(
|
|
material_layer.Material, "Pset_MaterialCommon", "MassDensity"
|
|
)
|
|
if material_mass_density is None:
|
|
return
|
|
densities.append(material_mass_density)
|
|
thickness = material_layer.LayerThickness
|
|
thicknesses.append(thickness)
|
|
obj_mass_density = obj_mass_density + (material_mass_density * thickness)
|
|
total_thickness = sum(thicknesses)
|
|
obj_mass_density = obj_mass_density / total_thickness
|
|
return obj_mass_density
|
|
|
|
if material.is_a("IfcMaterialProfileSetUsage"):
|
|
material_profiles = material.ForProfileSet.MaterialProfiles
|
|
if len(material_profiles) == 1:
|
|
material_mass_density = ifcopenshell.util.element.get_pset(
|
|
material_profiles[0].Material, "Pset_MaterialCommon", "MassDensity"
|
|
)
|
|
return material_mass_density
|
|
else:
|
|
return
|
|
|
|
|
|
def get_predefined_type(element: ifcopenshell.entity_instance) -> Union[str, None]:
|
|
"""Retrieves the PrefefinedType attribute of an element.
|
|
|
|
If the predefined type is user defined, the custom type (such as object
|
|
type, element type, or process type depending on the class) is returned
|
|
instead. Predefined types from the associated type element are also
|
|
considered first.
|
|
|
|
:param element: The IFC Element entity
|
|
:return: The predefined type of the element
|
|
|
|
Example:
|
|
|
|
.. code:: python
|
|
|
|
element = ifcopenshell.by_type("IfcWall")[0]
|
|
predefined_type = ifcopenshell.util.element.get_predefined_type(element)
|
|
"""
|
|
if element_type := get_type(element):
|
|
predefined_type = getattr(element_type, "PredefinedType", None)
|
|
if predefined_type == "USERDEFINED" or not predefined_type:
|
|
predefined_type = getattr(element_type, "ElementType", ...)
|
|
if predefined_type == ...:
|
|
predefined_type = getattr(element_type, "ProcessType", None)
|
|
if predefined_type and predefined_type != "NOTDEFINED":
|
|
return predefined_type
|
|
|
|
predefined_type = getattr(element, "PredefinedType", None)
|
|
if predefined_type == "USERDEFINED" or not predefined_type:
|
|
predefined_type = getattr(element, "ObjectType", None)
|
|
return predefined_type
|
|
|
|
|
|
def is_userdefined_type(element: ifcopenshell.entity_instance) -> bool:
|
|
"""Checks if the predefined type is userdefined
|
|
|
|
:param element: The IFC Element entity
|
|
:return: True if userdefined
|
|
|
|
Example:
|
|
|
|
.. code:: python
|
|
|
|
element = ifcopenshell.by_type("IfcWall")[0]
|
|
is_userdefined_type = ifcopenshell.util.element.is_userdefined_type(element)
|
|
"""
|
|
if element_type := get_type(element):
|
|
predefined_type = getattr(element_type, "PredefinedType", None)
|
|
if predefined_type == "USERDEFINED":
|
|
return True
|
|
elif not predefined_type:
|
|
predefined_type = getattr(element_type, "ElementType", ...)
|
|
if predefined_type == ...:
|
|
predefined_type = getattr(element_type, "ProcessType", None)
|
|
if predefined_type:
|
|
return True
|
|
if predefined_type and predefined_type != "NOTDEFINED":
|
|
return False
|
|
|
|
predefined_type = getattr(element, "PredefinedType", None)
|
|
if predefined_type == "USERDEFINED":
|
|
return True
|
|
elif not predefined_type:
|
|
return bool(getattr(element, "ObjectType", None))
|
|
return False
|
|
|
|
|
|
def get_type(element: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
|
|
"""Retrieves the construction type element of an element occurrence.
|
|
|
|
Note: `get_type(type_element) == type_element`.
|
|
|
|
:param element: The element occurrence (IfcObject)
|
|
:return: The related type element
|
|
|
|
Example:
|
|
|
|
.. code:: python
|
|
|
|
element = ifcopenshell.by_type("IfcWall")[0]
|
|
element_type = ifcopenshell.util.element.get_type(element)
|
|
"""
|
|
if element.is_a("IfcTypeObject"):
|
|
return element
|
|
|
|
schema = element.file.schema
|
|
if schema != "IFC2X3":
|
|
if is_typed_by := getattr(element, "IsTypedBy", ()):
|
|
return is_typed_by[0].RelatingType
|
|
return
|
|
|
|
if is_defined_by := getattr(element, "IsDefinedBy", ()): # IFC2X3
|
|
for relationship in is_defined_by:
|
|
if relationship.is_a("IfcRelDefinesByType"):
|
|
return relationship.RelatingType
|
|
|
|
|
|
def get_types(type: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
|
|
"""Get all the occurrences of a type element
|
|
|
|
:param type: The type element
|
|
:return: A list of occurrences of that type
|
|
|
|
Example:
|
|
|
|
.. code:: python
|
|
|
|
element_type = ifcopenshell.by_type("IfcWallType")[0]
|
|
walls = ifcopenshell.util.element.get_types(element_type)
|
|
"""
|
|
if type.file.schema == "IFC2X3":
|
|
if object_type_of := getattr(type, "ObjectTypeOf", ()):
|
|
return object_type_of[0].RelatedObjects
|
|
else:
|
|
if types := getattr(type, "Types", ()):
|
|
return types[0].RelatedObjects
|
|
return []
|
|
|
|
|
|
def get_shape_aspects(
|
|
element: ifcopenshell.entity_instance,
|
|
should_inherit: bool = True,
|
|
) -> list[ifcopenshell.entity_instance]:
|
|
"""Get element's shape aspects.
|
|
|
|
:param element: IfcProduct or IfcTypeProduct.
|
|
:param should_inherit: If True, the shape aspects of the element's type will be considered.
|
|
Useful in cases when IfcShapeAspects are assigned to the type's IfcRepresentationMap
|
|
instead of the element's IfcProductDefinitionShape.
|
|
:return: The associated shape aspects of the element.
|
|
|
|
Example:
|
|
|
|
.. code:: python
|
|
|
|
element = ifcopenshell.by_type("IfcWall")[0]
|
|
shape_aspect = ifcopenshell.util.element.get_shape_aspects(element)
|
|
"""
|
|
|
|
# IfcProduct
|
|
if (representation := getattr(element, "Representation", ...)) != ...:
|
|
shape_aspects: list[ifcopenshell.entity_instance] = []
|
|
if should_inherit and (element_type := get_type(element)):
|
|
shape_aspects.extend(get_shape_aspects(element_type))
|
|
shape_aspects.extend(representation.HasShapeAspects)
|
|
return shape_aspects
|
|
|
|
if element.file.schema == "IFC2X3":
|
|
return []
|
|
|
|
# IfcTypeProduct
|
|
shape_aspects = []
|
|
for representation_map in element.RepresentationMaps or []:
|
|
shape_aspects += representation_map.HasShapeAspects
|
|
return shape_aspects
|
|
|
|
|
|
def get_material(
|
|
element: ifcopenshell.entity_instance, should_skip_usage=False, should_inherit=True
|
|
) -> Union[ifcopenshell.entity_instance, None]:
|
|
"""Gets the material of the element
|
|
|
|
The material may be a single material, material set (layered, profiled, or
|
|
constituent), or a material set usage.
|
|
|
|
:param element: The element to get the material of.
|
|
:param should_skip_usage: If set to True, if the material is a material set
|
|
usage, the material set itself will be returned. Useful if you don't
|
|
care about occurrence usage parameters. If False, the usage will be
|
|
returned.
|
|
:param should_inherit: If True, any inherited materials from associated
|
|
types will be considered.
|
|
:return: The associated material of the element or `None`.
|
|
|
|
Example:
|
|
|
|
.. code:: python
|
|
|
|
element = ifcopenshell.by_type("IfcWall")[0]
|
|
material = ifcopenshell.util.element.get_material(element)
|
|
"""
|
|
if (has_associations := getattr(element, "HasAssociations", None)) is not None and has_associations:
|
|
for relationship in has_associations:
|
|
if relationship.is_a("IfcRelAssociatesMaterial"):
|
|
if should_skip_usage:
|
|
relating_material = relationship.RelatingMaterial
|
|
if relating_material.is_a("IfcMaterialLayerSetUsage"):
|
|
return relating_material.ForLayerSet
|
|
elif relating_material.is_a("IfcMaterialProfileSetUsage"):
|
|
return relating_material.ForProfileSet
|
|
return relationship.RelatingMaterial
|
|
if should_inherit:
|
|
relating_type = get_type(element)
|
|
if relating_type != element and (has_associations := getattr(relating_type, "HasAssociations", None)):
|
|
return get_material(relating_type, should_skip_usage)
|
|
|
|
|
|
def get_materials(
|
|
element: ifcopenshell.entity_instance, should_inherit: bool = True
|
|
) -> list[ifcopenshell.entity_instance]:
|
|
"""Gets individual materials of an element
|
|
|
|
If the element has a material set, the individual materials of that set are
|
|
returned as a list.
|
|
|
|
:param element: The element to get the materials of.
|
|
:param should_inherit: If True, any inherited materials from associated
|
|
types will be considered.
|
|
:return: The associated materials of the element.
|
|
|
|
Example:
|
|
|
|
.. code:: python
|
|
|
|
element = ifcopenshell.by_type("IfcWall")[0]
|
|
materials = ifcopenshell.util.element.get_materials(element)
|
|
"""
|
|
material = get_material(element, should_skip_usage=True, should_inherit=should_inherit)
|
|
if not material:
|
|
return []
|
|
elif material.is_a("IfcMaterial"):
|
|
return [material]
|
|
elif material.is_a("IfcMaterialLayerSet"):
|
|
return [l.Material for l in material.MaterialLayers]
|
|
elif material.is_a("IfcMaterialProfileSet"):
|
|
return [p.Material for p in material.MaterialProfiles]
|
|
elif material.is_a("IfcMaterialConstituentSet"):
|
|
return [c.Material for c in material.MaterialConstituents]
|
|
elif material.is_a("IfcMaterialList"):
|
|
return list(material.Materials)
|
|
else:
|
|
assert False, f"Unexpected material type: {material.is_a()}"
|
|
|
|
|
|
def get_styles(element: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
|
|
"""Retrieves the styles used in an element's representation.
|
|
|
|
Styles may be retreived from the material or the body representation.
|
|
|
|
:param element: The element to get the styles of.
|
|
:return: A list of surface styles
|
|
|
|
Example:
|
|
|
|
.. code:: python
|
|
|
|
wall = file.by_type("IfcWall")[0]
|
|
styles = ifcopenshell.util.element.get_styles(wall)
|
|
"""
|
|
styles = []
|
|
|
|
materials = ifcopenshell.util.element.get_materials(element)
|
|
for material in materials:
|
|
for material_definition_representation in material.HasRepresentation or []:
|
|
for representation in material_definition_representation.Representations:
|
|
for item in representation.Items:
|
|
styles.extend([s for s in item.Styles if s.is_a("IfcSurfaceStyle")])
|
|
|
|
body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
|
|
if not body:
|
|
return styles
|
|
|
|
for representation in [body]:
|
|
queue = list(representation.Items)
|
|
while queue:
|
|
item = queue.pop()
|
|
if item.is_a("IfcMappedItem"):
|
|
queue.extend(item.MappingSource.MappedRepresentation.Items)
|
|
if item.is_a("IfcBooleanResult"):
|
|
queue.append(item.FirstOperand)
|
|
queue.append(item.SecondOperand)
|
|
if item.StyledByItem:
|
|
styles.extend([s for s in item.StyledByItem[0].Styles if s.is_a("IfcSurfaceStyle")])
|
|
return styles
|
|
|
|
|
|
# TODO: ifc_file argument is unnecessary for some methods now
|
|
# since we have entity_instance.file, so we can deprecate it.
|
|
def get_elements_by_material(
|
|
ifc_file: Union[ifcopenshell.file, None], material: ifcopenshell.entity_instance
|
|
) -> set[ifcopenshell.entity_instance]:
|
|
"""Retrieves the elements related to a material.
|
|
|
|
This includes elements using the material as part of a material set or set
|
|
usage.
|
|
|
|
:param ifc_file: The IFC file
|
|
:param material: The IFC Material entity
|
|
:return: A set of elements using the to the material
|
|
|
|
Example:
|
|
|
|
.. code:: python
|
|
|
|
material = file.by_type("IfcMaterial")[0]
|
|
elements = ifcopenshell.util.element.get_elements_by_material(file, material)
|
|
"""
|
|
if not ifc_file:
|
|
ifc_file = material.file
|
|
results = set()
|
|
for inverse in ifc_file.get_inverse(material):
|
|
if inverse.is_a("IfcRelAssociatesMaterial"):
|
|
results.update(inverse.RelatedObjects or []) # See Revit bug #675
|
|
elif inverse.is_a("IfcMaterialLayer"):
|
|
for material_set in inverse.ToMaterialLayerSet:
|
|
results.update(get_elements_by_material(ifc_file, material_set))
|
|
elif inverse.is_a("IfcMaterialProfile"):
|
|
for material_set in inverse.ToMaterialProfileSet:
|
|
results.update(get_elements_by_material(ifc_file, material_set))
|
|
elif inverse.is_a("IfcMaterialConstituent"):
|
|
for material_set in inverse.ToMaterialConstituentSet:
|
|
results.update(get_elements_by_material(ifc_file, material_set))
|
|
elif inverse.is_a("IfcMaterialLayerSetUsage"):
|
|
results.update(get_elements_by_material(ifc_file, inverse))
|
|
elif inverse.is_a("IfcMaterialProfileSetUsage"):
|
|
results.update(get_elements_by_material(ifc_file, inverse))
|
|
elif inverse.is_a("IfcMaterialList"):
|
|
results.update(get_elements_by_material(ifc_file, inverse))
|
|
return results
|
|
|
|
|
|
def get_elements_by_style(
|
|
ifc_file: Union[ifcopenshell.file, None], style: ifcopenshell.entity_instance
|
|
) -> set[ifcopenshell.entity_instance]:
|
|
"""Retrieves the elements whose geometric representation uses a style
|
|
|
|
:param ifc_file: The IFC file
|
|
:param style: The IfcPresentationStyle entity
|
|
:return: The elements related to the style
|
|
|
|
Example:
|
|
|
|
.. code:: python
|
|
|
|
style = file.by_type("IfcSurfaceStyle")[0]
|
|
elements = ifcopenshell.util.element.get_elements_by_style(file, style)
|
|
"""
|
|
if not ifc_file:
|
|
ifc_file = style.file
|
|
results = set()
|
|
inverses = list(ifc_file.get_inverse(style))
|
|
while inverses:
|
|
inverse = inverses.pop()
|
|
inverse_class = inverse.is_a()
|
|
# IfcPresentationStyleAssignment for < IFC4X3.
|
|
# IfcFillAreaStyleHatching->IfcFillAreaStyle only for IfcCurveStyle.
|
|
# IfcFillAreaStyleTiles->IfcFillAreaStyle is not restricted to IfcCurveStyle.
|
|
if inverse_class in (
|
|
"IfcPresentationStyleAssignment",
|
|
"IfcFillAreaStyleHatching",
|
|
"IfcFillAreaStyle",
|
|
"IfcFillAreaStyleTiles",
|
|
):
|
|
inverses.extend(ifc_file.get_inverse(inverse))
|
|
continue
|
|
if not inverse.is_a("IfcStyledItem"):
|
|
continue
|
|
if geometry_item := inverse.Item:
|
|
for inverse_ in ifc_file.get_inverse(geometry_item):
|
|
if inverse_.is_a("IfcShapeRepresentation"):
|
|
results.update(get_elements_by_representation(ifc_file, inverse_))
|
|
# IfcFillAreaStyleTiles requires .Item to be set.
|
|
inverses.extend(ifc_file.get_inverse(inverse))
|
|
else:
|
|
styled_reps = [i for i in ifc_file.get_inverse(inverse) if i.is_a("IfcStyledRepresentation")]
|
|
for styled_rep in styled_reps:
|
|
for material_def_rep in styled_rep.OfProductRepresentation:
|
|
results.update(get_elements_by_material(ifc_file, material_def_rep.RepresentedMaterial))
|
|
return results
|
|
|
|
|
|
def get_elements_by_representation(
|
|
ifc_file: Union[ifcopenshell.file, None], representation: ifcopenshell.entity_instance
|
|
) -> set[ifcopenshell.entity_instance]:
|
|
"""Gets all elements using a geometric representation
|
|
|
|
:param ifc_file: The IFC file
|
|
:param representation: The IfcShapeRepresentation representation
|
|
:return: The elements using the geometric representation
|
|
|
|
Example:
|
|
|
|
.. code:: python
|
|
|
|
representation = file.by_type("IfcShapeRepresentation")[0]
|
|
elements = ifcopenshell.util.element.get_elements_by_representation(file, representation)
|
|
"""
|
|
if not ifc_file:
|
|
ifc_file = representation.file
|
|
results = set()
|
|
[results.update(pr.ShapeOfProduct) for pr in representation.OfProductRepresentation]
|
|
for rep_map in representation.RepresentationMap:
|
|
for inverse in ifc_file.get_inverse(rep_map):
|
|
if inverse.is_a("IfcTypeProduct"):
|
|
results.add(inverse)
|
|
elif inverse.is_a("IfcMappedItem"):
|
|
[
|
|
results.update(get_elements_by_representation(ifc_file, rep))
|
|
for rep in ifc_file.get_inverse(inverse)
|
|
if rep.is_a("IfcShapeRepresentation")
|
|
]
|
|
return results
|
|
|
|
|
|
def get_elements_by_profile(profile: ifcopenshell.entity_instance) -> set[ifcopenshell.entity_instance]:
|
|
"""Get all elements using provided IfcProfileDef.
|
|
|
|
Skip elements that have the profile in IfcMaterialProfileSet
|
|
but not actually use it in their representations.
|
|
|
|
:param profile: IfcProfileDef:
|
|
:return: The elements using the profile.
|
|
"""
|
|
ifc_file = profile.file
|
|
queue = ifc_file.get_inverse(profile)
|
|
processed: set[ifcopenshell.entity_instance] = set()
|
|
representations: set[ifcopenshell.entity_instance] = set()
|
|
while queue:
|
|
item = queue.pop()
|
|
if item.is_a("IfcRepresentationItem"):
|
|
queue.update(i for i in ifc_file.get_inverse(item) if i not in processed)
|
|
elif item.is_a("IfcShapeRepresentation"):
|
|
representations.add(item)
|
|
else:
|
|
pass
|
|
processed.add(item)
|
|
|
|
elements = set()
|
|
for representation in representations:
|
|
elements.update(get_elements_by_representation(ifc_file, representation))
|
|
return elements
|
|
|
|
|
|
def get_elements_by_layer(
|
|
ifc_file: Union[ifcopenshell.file, None], layer: ifcopenshell.entity_instance
|
|
) -> set[ifcopenshell.entity_instance]:
|
|
"""Get all the elements that are used by a presentation layer
|
|
|
|
:param ifc_file: The IFC file
|
|
:param layer: The IfcPresentationLayerAssignment layer
|
|
:return: The elements using the geometric representation
|
|
"""
|
|
if not ifc_file:
|
|
ifc_file = layer.file
|
|
results = set()
|
|
for item in layer.AssignedItems or []:
|
|
if item.is_a("IfcShapeRepresentation"):
|
|
results.update(get_elements_by_representation(ifc_file, item))
|
|
elif item.is_a("IfcRepresentationItem"):
|
|
for inverse in ifc_file.get_inverse(item):
|
|
if inverse.is_a("IfcShapeRepresentation"):
|
|
results.update(get_elements_by_representation(ifc_file, inverse))
|
|
return results
|
|
|
|
|
|
def get_layers(
|
|
ifc_file: Union[ifcopenshell.file, None], element: ifcopenshell.entity_instance
|
|
) -> list[ifcopenshell.entity_instance]:
|
|
"""Get the CAD layers that an element is part of
|
|
|
|
An element may have portions or all of its geometry assigned to a
|
|
traditional CAD presentation layer.
|
|
|
|
:param ifc_file: The IFC file object
|
|
:param element: The IFC element to interrogate
|
|
:return: A list of IfcPresentationLayerAssignment
|
|
|
|
Example:
|
|
|
|
.. code:: python
|
|
|
|
element = ifcopenshell.by_type("IfcWall")[0]
|
|
layers = ifcopenshell.util.element.get_layers(element)
|
|
"""
|
|
if not ifc_file:
|
|
ifc_file = element.file
|
|
layers = []
|
|
representations = []
|
|
if representation := getattr(element, "Representation", None):
|
|
representations = [representation]
|
|
elif representation_maps := getattr(element, "RepresentationMaps", None):
|
|
representations = representation_maps
|
|
for representation in representations:
|
|
for subelement in ifc_file.traverse(representation):
|
|
if subelement.is_a("IfcShapeRepresentation"):
|
|
layers.extend(subelement.LayerAssignments or [])
|
|
elif subelement.is_a("IfcGeometricRepresentationItem"):
|
|
if ifc_file.schema == "IFC2X3":
|
|
layers.extend(subelement.LayerAssignments or [])
|
|
else:
|
|
layers.extend(subelement.LayerAssignment or [])
|
|
return layers
|
|
|
|
|
|
def get_container(
|
|
element: ifcopenshell.entity_instance, should_get_direct: bool = False, ifc_class: Optional[str] = None
|
|
) -> Union[ifcopenshell.entity_instance, None]:
|
|
"""
|
|
Retrieves the spatial structure container of an element.
|
|
|
|
:param element: The IFC element
|
|
:param should_get_direct: If True, a result is only returned if the element
|
|
is directly contained in a spatial structure element. If False, an
|
|
indirect spatial container may be returned, such as if an element is a
|
|
part of an aggregate, and then if that aggregate is contained in a
|
|
spatial structure element.
|
|
:param ifc_class: Optionally filter the type of container you're after. For
|
|
example, you may be after the storey, not a space.
|
|
:return: The direct or indirect container of the element or None.
|
|
|
|
Example:
|
|
|
|
.. code:: python
|
|
|
|
element = file.by_type("IfcWall")[0]
|
|
container = ifcopenshell.util.element.get_container(element)
|
|
"""
|
|
if should_get_direct:
|
|
if (
|
|
contained_in_structure := getattr(element, "ContainedInStructure", None)
|
|
) is not None and contained_in_structure:
|
|
container = contained_in_structure[0].RelatingStructure
|
|
if not ifc_class:
|
|
return container
|
|
if container.is_a(ifc_class):
|
|
return container
|
|
elif contained_in_structure := getattr(element, "ContainedInStructure", None):
|
|
container = contained_in_structure[0].RelatingStructure
|
|
if not ifc_class:
|
|
return container
|
|
while container:
|
|
if container.is_a(ifc_class):
|
|
return container
|
|
container = get_aggregate(container)
|
|
elif parent := get_parent(element):
|
|
return get_container(parent, should_get_direct, ifc_class)
|
|
|
|
|
|
def get_referenced_structures(element: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
|
|
"""Retreives a list of referenced spatial elements
|
|
|
|
Typically useful for multistorey elements, such as columns or facade
|
|
elements, or elements that span multiple spaces or in-between spaces, such
|
|
as stairs, doors, etc.
|
|
|
|
:param element: The IFC element
|
|
:return: A list of IfcSpatialElement
|
|
|
|
Example:
|
|
|
|
.. code:: python
|
|
|
|
element = file.by_type("IfcWall")[0]
|
|
print(ifcopenshell.util.element.get_referenced_structures(element))
|
|
"""
|
|
return [r.RelatingStructure for r in getattr(element, "ReferencedInStructures", [])]
|
|
|
|
|
|
def get_structure_referenced_elements(structure: ifcopenshell.entity_instance) -> set[ifcopenshell.entity_instance]:
|
|
"""Retreives a set of elements referenced by a structure
|
|
|
|
:param structure: IfcSpatialElement
|
|
:return: A set of referenced elements, IfcSpatialReferenceSelect
|
|
|
|
Example:
|
|
|
|
.. code:: python
|
|
|
|
element = file.by_type("IfcBuildingStorey")[0]
|
|
print(ifcopenshell.util.element.get_structure_referenced_elements(element))
|
|
"""
|
|
referenced = set()
|
|
for rel in structure.ReferencesElements:
|
|
referenced.update(rel.RelatedElements)
|
|
return referenced
|
|
|
|
|
|
def get_decomposition(element: ifcopenshell.entity_instance, is_recursive=True) -> set[ifcopenshell.entity_instance]:
|
|
"""
|
|
Retrieves all subelements of an element based on the spatial decomposition
|
|
hierarchy. This includes all subspaces and elements contained in subspaces,
|
|
parts of an aggregate, all openings, all fills of any openings, and any
|
|
surface features adhering to an element (IFC4.3 and above).
|
|
|
|
:param element: The IFC element
|
|
:return: The decomposition of the element
|
|
|
|
Example:
|
|
|
|
.. code:: python
|
|
|
|
element = file.by_type("IfcProject")[0]
|
|
decomposition = ifcopenshell.util.element.get_decomposition(element)
|
|
"""
|
|
queue = [element]
|
|
results = set()
|
|
while queue:
|
|
element = queue.pop()
|
|
for rel in getattr(element, "ContainsElements", []):
|
|
related = rel.RelatedElements
|
|
queue.extend(related)
|
|
results.update(related)
|
|
for rel in getattr(element, "IsDecomposedBy", []):
|
|
related = rel.RelatedObjects
|
|
queue.extend(related)
|
|
results.update(related)
|
|
for rel in getattr(element, "HasOpenings", []):
|
|
related = rel.RelatedOpeningElement
|
|
queue.append(related)
|
|
results.add(related)
|
|
for rel in getattr(element, "HasFillings", []):
|
|
related = rel.RelatedBuildingElement
|
|
queue.append(related)
|
|
results.add(related)
|
|
for rel in getattr(element, "IsNestedBy", []):
|
|
related = rel.RelatedObjects
|
|
queue.extend(related)
|
|
results.update(related)
|
|
for rel in getattr(element, "HasSurfaceFeatures", []):
|
|
related = rel.RelatedSurfaceFeatures
|
|
queue.extend(related)
|
|
results.update(related)
|
|
if not is_recursive:
|
|
break
|
|
return results
|
|
|
|
|
|
def get_grouped_by(
|
|
element: ifcopenshell.entity_instance, is_recursive: bool = True
|
|
) -> list[ifcopenshell.entity_instance]:
|
|
"""Retrieves all subelements of an element based on the group.
|
|
|
|
:param element: IfcGroup entity
|
|
:return: All subelements of the group
|
|
|
|
Example:
|
|
|
|
.. code:: python
|
|
|
|
element = file.by_type("IfcGroup")[0]
|
|
subelements = ifcopenshell.util.element.get_grouped_by(element)
|
|
"""
|
|
queue = [element]
|
|
results = []
|
|
while queue:
|
|
element = queue.pop()
|
|
for rel in getattr(element, "IsGroupedBy", []):
|
|
related_objects = rel.RelatedObjects
|
|
queue.extend(related_objects)
|
|
results.extend(related_objects)
|
|
if not is_recursive:
|
|
break
|
|
return results
|
|
|
|
|
|
def get_groups(element: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
|
|
"""
|
|
Retrieves the groups of an element.
|
|
|
|
:param element: The IFC element
|
|
:return: List of IfcGroups element is assigned to.
|
|
|
|
Example:
|
|
|
|
.. code:: python
|
|
|
|
wall = file.by_type("IfcWall")[0]
|
|
group = ifcopenshell.util.element.get_groups(element)[0]
|
|
"""
|
|
groups = []
|
|
for rel in element.HasAssignments:
|
|
if rel.is_a("IfcRelAssignsToGroup"):
|
|
groups.append(rel.RelatingGroup)
|
|
return groups
|
|
|
|
|
|
def get_controls(element: ifcopenshell.entity_instance) -> Generator[ifcopenshell.entity_instance]:
|
|
"""
|
|
Retrieves the controls of an element.
|
|
|
|
:param element: The IFC element
|
|
:return: Generator of IfcControl elements assigned to the element.
|
|
|
|
Example:
|
|
|
|
.. code:: python
|
|
|
|
task = file.by_type("IfcTask")[0]
|
|
control = ifcopenshell.util.element.get_controls(task)[0]
|
|
"""
|
|
for rel in element.HasAssignments:
|
|
if rel.is_a("IfcRelAssignsToControl"):
|
|
yield rel.RelatingControl
|
|
|
|
|
|
def get_parent(
|
|
element: ifcopenshell.entity_instance, ifc_class: Optional[str] = None
|
|
) -> Union[ifcopenshell.entity_instance, None]:
|
|
"""Get the parent in the spatial heirarchy
|
|
|
|
IFC features a spatial hierarchy tree of all objects. Each spatial element
|
|
or physical element must be located inside this hierarchy exactly once.
|
|
|
|
The top level parent of this tree is the IfcProject, which has no parent.
|
|
|
|
All children may have parent-child relationships of one of the following types:
|
|
|
|
- Spatial containment: a physical object is located in a space
|
|
- Aggregation: a physical object is broken up into parts, or a spatial location is split into sub locations
|
|
- Nesting: components are attached to a host parent
|
|
- Filling: the physical element fills an opening, such as a window filling a hole
|
|
- Voiding: the opening voids another physical element, such as a hole in a wall
|
|
- Adherence: a surface feature adheres to a host element, such as a road
|
|
marking adhering to a road course (IFC4.3 and above)
|
|
|
|
:param element: Any physical or spatial element in the tree
|
|
:param ifc_class: Optionally filter the type of parent you're after. For
|
|
example, you may be after the storey, not a space.
|
|
:return: Its parent. This must exist for any valid file, or None if we've reached the IfcProject.
|
|
|
|
Example:
|
|
|
|
.. code:: python
|
|
|
|
element = file.by_type("IfcWall")[0]
|
|
parent = ifcopenshell.util.element.get_parent(element)
|
|
"""
|
|
parent = (
|
|
get_container(element, should_get_direct=True)
|
|
or get_aggregate(element)
|
|
or get_nest(element)
|
|
or get_filled_void(element)
|
|
or get_voided_element(element)
|
|
or get_adhered_element(element)
|
|
)
|
|
|
|
if not ifc_class:
|
|
return parent
|
|
|
|
while parent:
|
|
if parent.is_a(ifc_class):
|
|
return parent
|
|
parent = get_parent(parent)
|
|
|
|
return None
|
|
|
|
|
|
def get_filled_void(element: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
|
|
"""If the element is filling a void, get the void
|
|
|
|
Examples include windows and doors which fill a opening inside a wall.
|
|
|
|
:param element: The building element, typically a window or door
|
|
:return: The IfcOpeningElement that it is filling
|
|
|
|
Example:
|
|
|
|
.. code:: python
|
|
|
|
window = file.by_type("IfcWindow")[0]
|
|
opening = ifcopenshell.util.element.get_filled_void(window)
|
|
"""
|
|
if rel := getattr(element, "FillsVoids", None):
|
|
return rel[0].RelatingOpeningElement
|
|
|
|
|
|
def get_voided_element(element: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
|
|
"""For an opening, get the building element that the opening is voiding
|
|
|
|
For all valid models, this should never return None.
|
|
|
|
:param element: The IfcOpeningElement
|
|
:return: The building element, such as a wall or slab
|
|
|
|
Example:
|
|
|
|
.. code:: python
|
|
|
|
opening = file.by_type("IfcOpeningElement")[0]
|
|
element = ifcopenshell.util.element.get_voided_element(opening)
|
|
"""
|
|
if rel := getattr(element, "VoidsElements", None):
|
|
return rel[0].RelatingBuildingElement
|
|
|
|
|
|
def get_adhered_element(element: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
|
|
"""If the element is a surface feature, get the element it adheres to
|
|
|
|
In IFC4.3 an IfcSurfaceFeature (such as a road marking) adheres to a host
|
|
element through the IfcRelAdheresToElement relationship. This is a [1:1]
|
|
cardinality hierarchical relationship, in the same family as aggregation,
|
|
containment and nesting.
|
|
|
|
:param element: The IfcSurfaceFeature
|
|
:return: The host element that the surface feature adheres to
|
|
|
|
Example:
|
|
|
|
.. code:: python
|
|
|
|
marking = file.by_type("IfcSurfaceFeature")[0]
|
|
host = ifcopenshell.util.element.get_adhered_element(marking)
|
|
"""
|
|
if rel := getattr(element, "AdheresToElement", None):
|
|
return rel[0].RelatingElement
|
|
|
|
|
|
def get_aggregate(element: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
|
|
"""
|
|
Retrieves the aggregate parent of an element.
|
|
|
|
:param element: The IFC element
|
|
:return: The aggregate of the element
|
|
|
|
Example:
|
|
|
|
.. code:: python
|
|
|
|
element = file.by_type("IfcBeam")[0]
|
|
aggregate = ifcopenshell.util.element.get_aggregate(element)
|
|
"""
|
|
if not (decomposes := getattr(element, "Decomposes", None)):
|
|
return
|
|
is_ifc2x3 = element.file.schema == "IFC2X3"
|
|
rel: ifcopenshell.entity_instance = decomposes[0]
|
|
if is_ifc2x3 and not rel.is_a("IfcRelAggregates"):
|
|
# In IFCF2X3 Decomposes is used for both aggregates and nests,
|
|
# but only for 1 at the time.
|
|
return
|
|
return rel.RelatingObject
|
|
|
|
|
|
def get_nest(element: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
|
|
"""
|
|
Retrieves the nest parent of an element.
|
|
|
|
:param element: The IFC element
|
|
:return: The nested whole of the element
|
|
|
|
Example:
|
|
|
|
.. code:: python
|
|
|
|
element = file.by_type("IfcBeam")[0]
|
|
aggregate = ifcopenshell.util.element.get_nest(element)
|
|
"""
|
|
is_ifc2x3 = element.file.schema == "IFC2X3"
|
|
if is_ifc2x3:
|
|
if not (decomposes := getattr(element, "Decomposes", None)):
|
|
return
|
|
if decomposes[0].is_a("IfcRelNests"):
|
|
return decomposes[0].RelatingObject
|
|
else:
|
|
if nests := getattr(element, "Nests", None):
|
|
return nests[0].RelatingObject
|
|
|
|
|
|
def get_parts(element: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
|
|
"""
|
|
Retrieves the parts of an element that have an aggregation relationship.
|
|
|
|
:param element: The IFC element
|
|
:return: The parts of the element
|
|
|
|
Example:
|
|
|
|
.. code:: python
|
|
|
|
element = file.by_type("IfcElementAssembly")[0]
|
|
parts = ifcopenshell.util.element.get_parts(element)
|
|
"""
|
|
objects: list[ifcopenshell.entity_instance] = []
|
|
is_not_ifc2x3 = element.file.schema != "IFC2X3"
|
|
if is_decomposed_by := getattr(element, "IsDecomposedBy", ()):
|
|
for rel in is_decomposed_by:
|
|
if is_not_ifc2x3 or rel.is_a("IfcRelAggregates"):
|
|
objects.extend(rel.RelatedObjects)
|
|
return objects
|
|
|
|
|
|
def get_contained(element: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
|
|
"""
|
|
Retrieves the contained elements of spatial element.
|
|
|
|
:param element: The IFC element
|
|
:return: The parts of the element
|
|
|
|
Example:
|
|
|
|
.. code:: python
|
|
|
|
element = file.by_type("IfcBuildingStorey")[0]
|
|
elements = ifcopenshell.util.element.get_contained(element)
|
|
"""
|
|
objects: list[ifcopenshell.entity_instance] = []
|
|
if contains_elements := getattr(element, "ContainsElements", ()):
|
|
for rel in contains_elements:
|
|
objects.extend(rel.RelatedElements)
|
|
return objects
|
|
|
|
|
|
def get_surface_features(element: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
|
|
"""Retrieves the surface features that adhere to an element.
|
|
|
|
In IFC4.3 an IfcSurfaceFeature (such as a road marking) adheres to a host
|
|
element through the IfcRelAdheresToElement relationship.
|
|
|
|
:param element: The IFC element
|
|
:return: The surface features adhering to the element
|
|
|
|
Example:
|
|
|
|
.. code:: python
|
|
|
|
element = file.by_type("IfcCourse")[0]
|
|
markings = ifcopenshell.util.element.get_surface_features(element)
|
|
"""
|
|
objects: list[ifcopenshell.entity_instance] = []
|
|
if has_surface_features := getattr(element, "HasSurfaceFeatures", ()):
|
|
for rel in has_surface_features:
|
|
objects.extend(rel.RelatedSurfaceFeatures)
|
|
return objects
|
|
|
|
|
|
def get_components(
|
|
element: ifcopenshell.entity_instance, include_ports: bool = False
|
|
) -> list[ifcopenshell.entity_instance]:
|
|
"""
|
|
Retrieves the components of an element that have an nest relationship.
|
|
|
|
For nested ports, see ifcopenshell.util.system.
|
|
|
|
:param element: The IFC element
|
|
:param include_ports: Default as False. Set to true if you also want to get ports.
|
|
:return: The components of the element
|
|
|
|
Example:
|
|
|
|
.. code:: python
|
|
|
|
element = file.by_type("IfcElementAssembly")[0]
|
|
components = ifcopenshell.util.element.get_components(element)
|
|
"""
|
|
objects: list[ifcopenshell.entity_instance] = []
|
|
is_ifc2x3 = element.file.schema == "IFC2X3"
|
|
if is_ifc2x3:
|
|
if is_decomposed_by := getattr(element, "IsDecomposedBy", ()):
|
|
for rel in is_decomposed_by:
|
|
if rel.is_a("IfcRelNests"):
|
|
objects.extend(rel.RelatedObjects)
|
|
else:
|
|
if is_nested_by := getattr(element, "IsNestedBy", None):
|
|
for rel in is_nested_by:
|
|
objects.extend(rel.RelatedObjects)
|
|
if include_ports:
|
|
return objects
|
|
return [e for e in objects if not e.is_a("IfcPort")]
|
|
|
|
|
|
ReferenceData = namedtuple("ReferenceData", "inverse_attribute, rel_class, relating_element_attribute")
|
|
|
|
# References below are omitted because they do not introduce
|
|
# any additional referenced objects besides the objects
|
|
# from their supertype IfcExternalReference
|
|
# - IfcExternallyDefinedHatchStyle
|
|
# - IfcExternallyDefinedSurfaceStyle
|
|
# - IfcExternallyDefinedTextFont
|
|
|
|
REFERENCE_TYPES: dict[str, ReferenceData] = {
|
|
"IfcClassificationReference": ReferenceData(
|
|
"ClassificationRefForObjects",
|
|
"IfcRelAssociatesClassification",
|
|
"RelatingClassification",
|
|
),
|
|
"IfcDocumentReference": ReferenceData("DocumentRefForObjects", "IfcRelAssociatesDocument", "RelatingDocument"),
|
|
"IfcLibraryReference": ReferenceData("LibraryRefForObjects", "IfcRelAssociatesLibrary", "RelatingLibrary"),
|
|
"IfcClassification": ReferenceData(
|
|
"ClassificationForObjects", "IfcRelAssociatesClassification", "RelatingClassification"
|
|
),
|
|
"IfcDocumentInformation": ReferenceData("DocumentInfoForObjects", "IfcRelAssociatesDocument", "RelatingDocument"),
|
|
"IfcLibraryInformation": ReferenceData("LibraryInfoForObjects", "IfcRelAssociatesLibrary", "RelatingLibrary"),
|
|
}
|
|
|
|
|
|
def get_referenced_elements(reference: ifcopenshell.entity_instance) -> set[ifcopenshell.entity_instance]:
|
|
"""Get all elements with assigned `reference`
|
|
|
|
:param reference: IfcExternalReference/IfcExternalInformation subtype reference
|
|
:return: The elements with assigned `reference`
|
|
|
|
Example:
|
|
|
|
.. code:: python
|
|
|
|
reference = file.by_type("IfcClassificationReference")[0]
|
|
elements = ifcopenshell.util.element.get_referenced_elements(reference)
|
|
"""
|
|
|
|
related_objects: set[ifcopenshell.entity_instance] = set()
|
|
ifc_file = reference.file
|
|
ifc_class = reference.is_a()
|
|
|
|
if ifc_file.schema == "IFC2X3":
|
|
reference_data = REFERENCE_TYPES.get(ifc_class)
|
|
if reference_data:
|
|
for rel in ifc_file.by_type(reference_data.rel_class):
|
|
if getattr(rel, reference_data.relating_element_attribute) == reference:
|
|
related_objects.update(rel.RelatedObjects)
|
|
|
|
else:
|
|
if reference.is_a("IfcExternalReference"):
|
|
# IfcExternalReference
|
|
for external_rel in reference.ExternalReferenceForResources:
|
|
related_objects.update(external_rel.RelatedResourceObjects)
|
|
|
|
reference_data = REFERENCE_TYPES.get(ifc_class)
|
|
if reference_data:
|
|
for rel in getattr(reference, reference_data.inverse_attribute):
|
|
related_objects.update(rel.RelatedObjects)
|
|
|
|
return related_objects
|
|
|
|
|
|
def replace_element(element: ifcopenshell.entity_instance, replacement: ifcopenshell.entity_instance) -> None:
|
|
for inverse in element.file.get_inverse(element):
|
|
replace_attribute(inverse, element, replacement)
|
|
|
|
|
|
def replace_attribute(element: ifcopenshell.entity_instance, old: Any, new: Any) -> None:
|
|
for i, attribute_value in enumerate(element):
|
|
if has_element_reference(attribute_value, old):
|
|
element[i] = element.walk(lambda v: v == old, lambda v: new, attribute_value)
|
|
|
|
|
|
def has_element_reference(value: Any, element: ifcopenshell.entity_instance) -> bool:
|
|
if isinstance(value, (tuple, list)):
|
|
for v in value:
|
|
if has_element_reference(v, element):
|
|
return True
|
|
return False
|
|
return value == element
|
|
|
|
|
|
def remove_deep(ifc_file: Union[ifcopenshell.file, None], element: ifcopenshell.entity_instance) -> None:
|
|
"""Recursively purges a subgraph safely.
|
|
|
|
Do not use, use remove_deep2() instead.
|
|
"""
|
|
# @todo maybe some sort of try-finally mechanism.
|
|
if not ifc_file:
|
|
ifc_file = element.file
|
|
ifc_file.batch()
|
|
subgraph = list(ifc_file.traverse(element, breadth_first=True))
|
|
subgraph_set = set(subgraph)
|
|
for ref in subgraph[::-1]:
|
|
if ref.id() and len(set(ifc_file.get_inverse(ref)) - subgraph_set) == 0:
|
|
ifc_file.remove(ref)
|
|
ifc_file.unbatch()
|
|
|
|
|
|
def batch_remove_deep2(ifc_file: ifcopenshell.file) -> None:
|
|
"""Enable batch removal after running remove_deep2 using serialisation
|
|
|
|
See #944 and #3226. Removing elements in an IFC graph is slow as a lot of
|
|
mappings need to be edited. In larger models (>100MB) and when removing
|
|
many elements (>10000), it is faster to serialise the IFC, remove elements
|
|
using string replacement, and then reload the modified serialised IFC.
|
|
|
|
The trade-off is that extra memory will be used, and string replacement
|
|
only works with remove_deep2 where the removed elements have no inverses.
|
|
In addition, transaction history will be lost, and any scripts using this
|
|
method will have to refetch elements from the reloaded IFC and cannot rely
|
|
on existing variables in memory.
|
|
|
|
:param ifc_file: The IFC file object
|
|
|
|
Example:
|
|
|
|
.. code:: python
|
|
|
|
element1 = model.by_id(123)
|
|
element2 = model.by_id(456)
|
|
|
|
ifcopenshell.util.element.batch_remove_deep2(model)
|
|
ifcopenshell.util.element.remove_deep2(model, element2)
|
|
|
|
# Notice how we reload the model.
|
|
model = ifcopenshell.util.element.unbatch_remove_deep2(model)
|
|
|
|
print(element1) # Don't call element1!
|
|
"""
|
|
ifc_file.to_delete = set()
|
|
|
|
|
|
def unbatch_remove_deep2(ifc_file: ifcopenshell.file) -> ifcopenshell.file:
|
|
"""Finish removing elements batched from remove_deep2 using string replacement
|
|
|
|
See documentation for batch_remove_deep2.
|
|
|
|
:param ifc_file: The IFC file object
|
|
:return: A newly loaded file with the elements removed.
|
|
"""
|
|
assert ifc_file.to_delete is not None
|
|
ifc_string = ifc_file.to_string()
|
|
lines = iter(ifc_string.split("\n"))
|
|
ids_to_delete = iter(sorted([e.id() for e in ifc_file.to_delete]))
|
|
id_to_delete = next(ids_to_delete, None)
|
|
result: list[str] = []
|
|
|
|
for line in lines:
|
|
if id_to_delete is None:
|
|
result.append(line)
|
|
continue
|
|
|
|
if line.startswith(f"#{id_to_delete}="):
|
|
id_to_delete = next(ids_to_delete, None)
|
|
else:
|
|
result.append(line)
|
|
|
|
ifc_file.to_delete = None
|
|
return ifcopenshell.file.from_string("\n".join(result))
|
|
|
|
|
|
def remove_deep2(
|
|
ifc_file: Union[ifcopenshell.file, None],
|
|
element: ifcopenshell.entity_instance,
|
|
also_consider: list[ifcopenshell.entity_instance] = [],
|
|
do_not_delete: set[ifcopenshell.entity_instance] = set(),
|
|
) -> None:
|
|
"""Recursively purges a subgraph safely, starting at an element
|
|
|
|
This should always be used instead of remove_deep. See #1812. The start
|
|
element must have no inverses. The subgraph to be purged is calculated using
|
|
all forward relationships determined by the traverse() function.
|
|
|
|
The deletion process starts at element and traverses forward through the
|
|
subgraph. Each subelement is checked for any inverses outside the subgraph.
|
|
If there are no inverses outside, it may be safely purged. If there are
|
|
inverses that aren't part of this subgraph, that subelement, and all of its
|
|
subelements (i.e. that entire branch of subelements) will not be deleted as
|
|
it is used elsewhere.
|
|
|
|
For simple subgraphs, traverse() is sufficient to fully represent all
|
|
related subelements. When it isn't, the ``also_consider`` argument may be
|
|
used. These are typically inverses futher down the subelement chain.
|
|
|
|
Note that remove_deep2 will _not_ remove elements in also_consider. Instead,
|
|
it is only used as a consideration for whether or not an element has all
|
|
inverses fully contained in the subgraph.
|
|
|
|
The do_not_delete argument contains all elements that may be part of the
|
|
subgraph but are protected from deletion.
|
|
|
|
:param ifc_file: The IFC file object
|
|
:param also_consider: elements to also consider as a part of a subgraph
|
|
Order could matter for perfomance - elements that reference `element`
|
|
directly should go first for the better performance.
|
|
:param do_not_delete: elements to protect from deletion
|
|
:param element: The starting element that defines the subgraph
|
|
"""
|
|
# ifc_file.batch()
|
|
if not ifc_file:
|
|
ifc_file = element.file
|
|
total_inverses = ifc_file.get_total_inverses(element)
|
|
if total_inverses > 0:
|
|
|
|
def are_inverses_contained() -> bool:
|
|
also_considered_inverses = 0
|
|
|
|
for considered_element in also_consider:
|
|
traverse = ifc_file.traverse(considered_element, max_levels=1)
|
|
if element in traverse:
|
|
also_considered_inverses += 1
|
|
if total_inverses == also_considered_inverses:
|
|
return True
|
|
return False
|
|
|
|
if not are_inverses_contained():
|
|
return
|
|
|
|
to_delete: set[ifcopenshell.entity_instance] = set()
|
|
subgraph = list(ifc_file.traverse(element, breadth_first=True))
|
|
subgraph.extend(also_consider)
|
|
subgraph_set = set(subgraph)
|
|
subelement_queue = [element]
|
|
|
|
# Cache already processed entities to avoid traversing them multiple time.
|
|
# E.g. lots of IFCINDEXEDPOLYCURVES may reference the same IFCCARTESIANPOINTLIST2D.
|
|
processed_ids: set[int] = set()
|
|
|
|
while subelement_queue:
|
|
subelement = subelement_queue.pop(0)
|
|
subelement_id = subelement.id()
|
|
if (
|
|
subelement_id
|
|
and subelement_id not in processed_ids
|
|
and subelement not in do_not_delete
|
|
and (
|
|
# 0 or 1 inverses guarantees that the subelement only exists in this subgraph
|
|
ifc_file.get_total_inverses(subelement) < 2
|
|
# Alternatively, let's ensure all inverses are within the subgraph
|
|
or len(set(ifc_file.get_inverse(subelement)) - subgraph_set) == 0
|
|
)
|
|
):
|
|
to_delete.add(subelement)
|
|
subelement_queue.extend(ifc_file.traverse(subelement, max_levels=1)[1:])
|
|
# See #3052. IfcOpenShell is extremely slow in removing elements if
|
|
# the element has an inverse, and that inverse references that
|
|
# element in a big list. The most common example is an
|
|
# IfcPolygonalFaceSet with a Faces attribute of tens of thousands
|
|
# of IfcIndexedPolygonalFace. In this situation, removing a
|
|
# IfcIndexedPolygonalFace will take very, very long. If we are
|
|
# going to delete an element (i.e. added to the to_delete set), we
|
|
# clear any large lists (10 is an arbitrary threshold) to prevent
|
|
# this issue.
|
|
for i, attribute in enumerate(subelement):
|
|
if isinstance(attribute, tuple) and len(attribute) > 10:
|
|
subelement[i] = []
|
|
processed_ids.add(subelement_id)
|
|
|
|
if ifc_file.to_delete is not None:
|
|
ifc_file.to_delete.update(to_delete)
|
|
return
|
|
|
|
# We delete elements from subgraph in reverse order to allow batching to work
|
|
for subelement in filter(lambda e: e in to_delete, subgraph[::-1]):
|
|
ifc_file.remove(subelement)
|
|
# ifc_file.unbatch()
|
|
|
|
|
|
def copy(
|
|
ifc_file: Union[ifcopenshell.file, None], element: ifcopenshell.entity_instance
|
|
) -> ifcopenshell.entity_instance:
|
|
"""
|
|
Copy a single element. Any referenced elements are not copied.
|
|
|
|
GlobalIds are regenerated.
|
|
|
|
:param ifc_file: The IFC file object
|
|
:param element: The IFC element to copy
|
|
:return: The newly copied element
|
|
"""
|
|
if not ifc_file:
|
|
ifc_file = element.file
|
|
new = ifc_file.create_entity(element.is_a())
|
|
for i, attribute in enumerate(element):
|
|
if attribute is None:
|
|
continue
|
|
if new.attribute_name(i) == "GlobalId":
|
|
new[i] = ifcopenshell.guid.new()
|
|
else:
|
|
new[i] = attribute
|
|
return new
|
|
|
|
|
|
def copy_deep(
|
|
ifc_file: Union[ifcopenshell.file, None],
|
|
element: ifcopenshell.entity_instance,
|
|
exclude: Optional[Sequence[str]] = None,
|
|
exclude_callback: Optional[Callable[[ifcopenshell.entity_instance], bool]] = None,
|
|
copied_entities: Optional[dict[int, ifcopenshell.entity_instance]] = None,
|
|
) -> ifcopenshell.entity_instance:
|
|
"""
|
|
Recursively copy an element and all of its directly related subelements.
|
|
|
|
GlobalIds are regenerated.
|
|
|
|
:param ifc_file: The IFC file object
|
|
:param element: The IFC element to copy
|
|
:param exclude: An optional list of strings of IFC class names to not copy.
|
|
If any of the subelement is this class, it will not be copied and the
|
|
original instance will be referenced.
|
|
:param exclude_callback: A callback to determine whether or not to exclude
|
|
an entity or not. Returns True to exclude and False to exclude.
|
|
:param copied_entities: A dictionary of IDs as keys and entities as values
|
|
to reuse when coming across the same entity twice. This can typically
|
|
be left as None.
|
|
:return: The newly copied element
|
|
"""
|
|
if not ifc_file:
|
|
ifc_file = element.file
|
|
if copied_entities is None:
|
|
copied_entities = {}
|
|
else:
|
|
copied_entity = copied_entities.get(element.id(), None)
|
|
if copied_entity:
|
|
return copied_entity
|
|
new = ifc_file.create_entity(element.is_a())
|
|
if element.id():
|
|
copied_entities[element.id()] = new
|
|
for i, attribute in enumerate(element):
|
|
if attribute is None:
|
|
continue
|
|
if isinstance(attribute, ifcopenshell.entity_instance):
|
|
if exclude and any([attribute.is_a(e) for e in exclude]):
|
|
pass
|
|
elif exclude_callback and exclude_callback(attribute):
|
|
pass
|
|
else:
|
|
attribute = copy_deep(
|
|
ifc_file,
|
|
attribute,
|
|
exclude=exclude,
|
|
copied_entities=copied_entities,
|
|
exclude_callback=exclude_callback,
|
|
)
|
|
elif isinstance(attribute, tuple) and attribute and isinstance(attribute[0], ifcopenshell.entity_instance):
|
|
if exclude and any([attribute[0].is_a(e) for e in exclude]):
|
|
pass
|
|
elif exclude_callback and exclude_callback(attribute[0]):
|
|
pass
|
|
else:
|
|
attribute = list(attribute)
|
|
for j, item in enumerate(attribute):
|
|
attribute[j] = copy_deep(
|
|
ifc_file,
|
|
item,
|
|
exclude=exclude,
|
|
exclude_callback=exclude_callback,
|
|
copied_entities=copied_entities,
|
|
)
|
|
if new.attribute_name(i) == "GlobalId":
|
|
new[i] = ifcopenshell.guid.new()
|
|
else:
|
|
new[i] = attribute
|
|
return new
|
|
|
|
|
|
def has_property(product: ifcopenshell.entity_instance, property_name: str) -> bool:
|
|
"""
|
|
Check if a product has a property with a given name.
|
|
|
|
:param product: The IFC product
|
|
:param property_name: The property name
|
|
:return: True if the product has the property, False otherwise
|
|
|
|
Example:
|
|
|
|
.. code:: python
|
|
|
|
product = file.by_type("IfcWall")[0]
|
|
has_property = ifcopenshell.util.element.has_property(product, "NetArea")
|
|
"""
|
|
if not property_name:
|
|
return True
|
|
qtos = get_psets(product, qtos_only=True)
|
|
return any(property_name in quantities.keys() for quantities in qtos.values())
|
|
|
|
|
|
def get_openings(element: ifcopenshell.entity_instance) -> Generator[ifcopenshell.entity_instance, None, None]:
|
|
"""Get element openings as IfcRelVoidsElements.
|
|
|
|
Use `.RelatedOpeningElement` to get the opening element.
|
|
|
|
:param element: IfcElement.
|
|
:return: Generator of IfcRelVoidsElements.
|
|
"""
|
|
for element_rel in getattr(element, "HasOpenings", ()):
|
|
yield element_rel
|
|
|
|
if aggregate := get_aggregate(element):
|
|
yield from get_openings(aggregate)
|
|
|
|
|
|
def has_openings(element: ifcopenshell.entity_instance) -> bool:
|
|
"""Check if the element has openings.
|
|
|
|
:param element: IfcElement.
|
|
:return: True if element has openings.
|
|
"""
|
|
return bool(next(get_openings(element), False))
|
|
|
|
|
|
def get_material_layers(element: ifcopenshell.entity_instance) -> list[PrioritisedLayer]:
|
|
"""
|
|
Retrieves all material layers assigned to an element.
|
|
:param element: The IFC element
|
|
:return: A list of IfcMaterialLayer entities
|
|
|
|
Example:
|
|
.. code:: python
|
|
element = ifcopenshell.by_type("IfcWall")[0]
|
|
material_layers = ifcopenshell.util.element.get_material_layers(element)
|
|
"""
|
|
material = ifcopenshell.util.element.get_material(element, should_skip_usage=True)
|
|
if not material or not material.is_a("IfcMaterialLayerSet"):
|
|
return []
|
|
return [
|
|
PrioritisedLayer(getattr(layer, "Priority", 0) or 0, layer.Material, layer.LayerThickness)
|
|
for layer in material.MaterialLayers
|
|
]
|
|
|
|
|
|
def get_material_profiles(element: ifcopenshell.entity_instance) -> list[PrioritisedProfile]:
|
|
"""
|
|
Retrieves all material profiles assigned to an element.
|
|
|
|
:param element: The IFC element
|
|
:return: A list of IfcMaterialProfile entities
|
|
|
|
Example:
|
|
|
|
.. code:: python
|
|
|
|
element = ifcopenshell.by_type("IfcBeam")[0]
|
|
material_profiles = ifcopenshell.util.element.get_material_profiles(element)
|
|
"""
|
|
material = ifcopenshell.util.element.get_material(element, should_skip_usage=True)
|
|
if not material or not material.is_a("IfcMaterialProfileSet"):
|
|
return []
|
|
return [
|
|
PrioritisedProfile(
|
|
getattr(material_profile, "Priority", 0) or 0, material_profile.Material, material_profile.Profile
|
|
)
|
|
for material_profile in material.MaterialProfiles
|
|
]
|