Fix #8138: door/window container assignment no-op

Spatial.get_root_element walks aggregate / nest / filled-void /
voided-element chains and core.assign_container assigns the container
to whatever the walk returns. For an IfcDoor the filled-void hop
redirects to the IfcOpeningElement, then voided-element to the host
wall, so a user who selects a door and runs bim.assign_container ends
up targeting the wall — and silently no-ops on the door if the wall is
already in the target storey.

Per IFC4 / IFC4.3 (IfcDoor, IfcWindow): the spatial containment of a
filling is defined independently of the filling relationship. Major
exporters (Revit, ArchiCAD, Tekla, Allplan) emit independent
ContainedInStructure on doors / windows accordingly. Drop the
filled-void / voided-element hops from the walk; aggregate and nest
remain — those are true sub-part relationships where the parent
legitimately owns the container.

New TestGetRootElement in test/tool pins the new contract (filling
resolves to itself) plus the retained aggregate / nest / loose-element
paths so a future PR that re-adds either hop is caught. Two new
TestAssignContainer cases in test/core pin filling-to-self through the
core layer and per-element can_contain filtering.

Generated with the assistance of an AI coding tool.
This commit is contained in:
Gorgious56
2026-06-08 18:53:50 +02:00
committed by Thomas Krijnen
parent 8374dd6d46
commit 92172880a4
3 changed files with 67 additions and 4 deletions
+37
View File
@@ -19,6 +19,9 @@
import bpy
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.api.aggregate
import ifcopenshell.api.feature
import ifcopenshell.api.nest
import ifcopenshell.api.root
import ifcopenshell.api.spatial
import numpy as np
@@ -148,6 +151,40 @@ class TestGetContainer(NewFile):
assert subject.get_container(wall) == site
class TestGetRootElement(NewFile):
def test_a_door_filling_a_wall_is_its_own_root_element(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
wall = ifc.createIfcWall()
opening = ifc.createIfcOpeningElement()
door = ifc.createIfcDoor()
ifcopenshell.api.feature.add_feature(ifc, feature=opening, element=wall)
ifcopenshell.api.feature.add_filling(ifc, opening=opening, element=door)
assert subject.get_root_element(door) == door
def test_an_aggregated_element_walks_to_its_aggregate_root(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
assembly = ifc.createIfcElementAssembly()
beam = ifc.createIfcBeam()
ifcopenshell.api.aggregate.assign_object(ifc, products=[beam], relating_object=assembly)
assert subject.get_root_element(beam) == assembly
def test_a_nested_element_walks_to_its_nest_root(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
parent_task = ifc.createIfcTask()
child_task = ifc.createIfcTask()
ifcopenshell.api.nest.assign_object(ifc, related_objects=[child_task], relating_object=parent_task)
assert subject.get_root_element(child_task) == parent_task
def test_a_loose_element_is_its_own_root(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
wall = ifc.createIfcWall()
assert subject.get_root_element(wall) == wall
class TestGetDecomposedElements(NewFile):
def test_run(self):
ifc = ifcopenshell.file()