From e0a1988044154307bb0ca0a79dbec47564a14241 Mon Sep 17 00:00:00 2001 From: Petru Conduraru Date: Wed, 8 Jul 2026 11:57:56 +0300 Subject: [PATCH] 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 --- .../bonsai/bim/module/project/operator.py | 5 ++ .../ifcopenshell/util/element.py | 55 ++++++++++++++++++- 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index da5ee77fe2..b0d9fc166d 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -1294,6 +1294,11 @@ class LoadProjectElements(bpy.types.Operator): if element.IsDecomposedBy: for subelement in element.IsDecomposedBy[0].RelatedObjects: decomposed_elements.add(subelement) + # IfcSurfaceFeature (e.g. road markings) adhere to a host element + # via IfcRelAdheresToElement, a [1:1] hierarchical relationship in + # the same family as aggregation, containment and nesting (IFC4.3). + for rel in getattr(element, "HasSurfaceFeatures", ()): + decomposed_elements.update(rel.RelatedSurfaceFeatures) if decomposed_elements: self.append_decomposed_elements(decomposed_elements) elements.update(decomposed_elements) diff --git a/src/ifcopenshell-python/ifcopenshell/util/element.py b/src/ifcopenshell-python/ifcopenshell/util/element.py index 8a8a817336..8ca93fff41 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/element.py +++ b/src/ifcopenshell-python/ifcopenshell/util/element.py @@ -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]: