From d0f20371bdcbb62364d88d6feea00d797c106ff0 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Fri, 20 Mar 2026 23:10:00 +1100 Subject: [PATCH] Add feature to get parent of a particular IFC class --- .../ifcopenshell/util/element.py | 18 +++++++++-- .../ifcopenshell/util/selector.py | 8 ++--- .../test/util/test_element.py | 30 +++++++++++++++++++ 3 files changed, 50 insertions(+), 6 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/util/element.py b/src/ifcopenshell-python/ifcopenshell/util/element.py index 149606cb14..1c52ebc49d 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/element.py +++ b/src/ifcopenshell-python/ifcopenshell/util/element.py @@ -1234,7 +1234,9 @@ def get_controls(element: ifcopenshell.entity_instance) -> Generator[ifcopenshel yield rel.RelatingControl -def get_parent(element: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]: +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 @@ -1251,6 +1253,8 @@ def get_parent(element: ifcopenshell.entity_instance) -> Union[ifcopenshell.enti - Voiding: the opening voids another physical element, such as a hole in a wall :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: @@ -1260,7 +1264,7 @@ def get_parent(element: ifcopenshell.entity_instance) -> Union[ifcopenshell.enti element = file.by_type("IfcWall")[0] parent = ifcopenshell.util.element.get_parent(element) """ - return ( + parent = ( get_container(element, should_get_direct=True) or get_aggregate(element) or get_nest(element) @@ -1268,6 +1272,16 @@ def get_parent(element: ifcopenshell.entity_instance) -> Union[ifcopenshell.enti or get_voided_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 diff --git a/src/ifcopenshell-python/ifcopenshell/util/selector.py b/src/ifcopenshell-python/ifcopenshell/util/selector.py index c6ee820ab8..bbe8125927 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/selector.py +++ b/src/ifcopenshell-python/ifcopenshell/util/selector.py @@ -440,13 +440,13 @@ def _get_element_value(element: ifcopenshell.entity_instance, keys: list[str]) - elif key == "container": value = ifcopenshell.util.element.get_container(value) elif key == "space": - value = ifcopenshell.util.element.get_container(value, ifc_class="IfcSpace") + value = ifcopenshell.util.element.get_parent(value, ifc_class="IfcSpace") elif key == "storey": - value = ifcopenshell.util.element.get_container(value, ifc_class="IfcBuildingStorey") + value = ifcopenshell.util.element.get_parent(value, ifc_class="IfcBuildingStorey") elif key == "building": - value = ifcopenshell.util.element.get_container(value, ifc_class="IfcBuilding") + value = ifcopenshell.util.element.get_parent(value, ifc_class="IfcBuilding") elif key == "site": - value = ifcopenshell.util.element.get_container(value, ifc_class="IfcSite") + value = ifcopenshell.util.element.get_parent(value, ifc_class="IfcSite") elif key == "parent": value = ifcopenshell.util.element.get_parent(value) elif key in ("types", "occurrences"): diff --git a/src/ifcopenshell-python/test/util/test_element.py b/src/ifcopenshell-python/test/util/test_element.py index 1308e957c1..5eb6034402 100644 --- a/src/ifcopenshell-python/test/util/test_element.py +++ b/src/ifcopenshell-python/test/util/test_element.py @@ -38,6 +38,7 @@ import ifcopenshell.api.sequence import ifcopenshell.api.spatial import ifcopenshell.api.style import ifcopenshell.api.type +import ifcopenshell.api.feature import ifcopenshell.guid import ifcopenshell.util.element as subject import test.bootstrap @@ -891,6 +892,35 @@ class TestGetlayers(test.bootstrap.IFC4, TestGetlayersIFC2X3): assert subject.get_layers(self.file, element) == [layer] +class TestGetParentIFC4(test.bootstrap.IFC4): + def test_getting_the_parent_of_an_element(self): + element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") + building = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcBuilding") + ifcopenshell.api.spatial.assign_container(self.file, products=[element], relating_structure=building) + assert subject.get_parent(element) == building + + def test_getting_the_specific_parent_of_an_element(self): + element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") + building = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcBuilding") + storey = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcBuildingStorey") + ifcopenshell.api.aggregate.assign_object(self.file, products=[storey], relating_object=building) + ifcopenshell.api.spatial.assign_container(self.file, products=[element], relating_structure=storey) + assert subject.get_parent(element, ifc_class="IfcBuilding") == building + assert subject.get_parent(element, ifc_class="IfcSite") == None + + def test_getting_the_specific_parent_of_an_element_via_voiding(self): + wall = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") + building = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcBuilding") + ifcopenshell.api.spatial.assign_container(self.file, products=[wall], relating_structure=building) + opening = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcOpeningElement") + ifcopenshell.api.feature.add_feature(self.file, feature=opening, element=wall) + window = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWindow") + ifcopenshell.api.feature.add_filling(self.file, opening=opening, element=window) + assert subject.get_parent(window, ifc_class="IfcWall") == wall + assert subject.get_parent(window, ifc_class="IfcBuilding") == building + assert subject.get_parent(window, ifc_class="IfcSite") == None + + class TestGetContainerIFC4(test.bootstrap.IFC4): def test_getting_the_spatial_container_of_an_element(self): element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall")