Follow IfcRelAdheresToElement so IfcSurfaceFeature road markings import #8375

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>
This commit is contained in:
Petru Conduraru
2026-07-08 11:57:56 +03:00
committed by Thomas Krijnen
parent 644b92263d
commit e0a1988044
2 changed files with 59 additions and 1 deletions
@@ -1125,7 +1125,8 @@ def get_decomposition(element: ifcopenshell.entity_instance, is_recursive=True)
"""
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, and all fills of any openings.
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
@@ -1161,6 +1162,10 @@ def get_decomposition(element: ifcopenshell.entity_instance, is_recursive=True)
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
@@ -1251,6 +1256,8 @@ def get_parent(
- 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
@@ -1270,6 +1277,7 @@ def get_parent(
or get_nest(element)
or get_filled_void(element)
or get_voided_element(element)
or get_adhered_element(element)
)
if not ifc_class:
@@ -1321,6 +1329,28 @@ def get_voided_element(element: ifcopenshell.entity_instance) -> Union[ifcopensh
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.
@@ -1415,6 +1445,29 @@ def get_contained(element: ifcopenshell.entity_instance) -> list[ifcopenshell.en
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]: