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
parent 3346a59284
commit 5b79cefee2
3 changed files with 67 additions and 4 deletions
+29
View File
@@ -52,6 +52,35 @@ class TestAssignContainer:
collector.assign("obj2").should_be_called()
subject.assign_container(ifc, collector, spatial, container="container", objs=["obj"])
def test_root_resolves_to_self_for_a_filling(self, ifc, collector, spatial):
ifc.get_entity("door_obj").should_be_called().will_return("door")
spatial.get_root_element("door").should_be_called().will_return("door")
spatial.disable_editing("door_obj").should_be_called()
spatial.get_decomposition("door").should_be_called().will_return(["door"])
spatial.can_contain("container", "door").should_be_called().will_return(True)
ifc.run("spatial.assign_container", products=["door"], relating_structure="container").should_be_called()
ifc.get_object("door").should_be_called().will_return("door_obj")
collector.assign("door_obj").should_be_called()
subject.assign_container(ifc, collector, spatial, container="container", objs=["door_obj"])
def test_can_contain_is_evaluated_per_root_element(self, ifc, collector, spatial):
ifc.get_entity("door_obj").should_be_called().will_return("door")
spatial.get_root_element("door").should_be_called().will_return("door")
spatial.disable_editing("door_obj").should_be_called()
spatial.get_decomposition("door").should_be_called().will_return(["door"])
ifc.get_entity("opening_obj").should_be_called().will_return("opening")
spatial.get_root_element("opening").should_be_called().will_return("opening")
spatial.disable_editing("opening_obj").should_be_called()
spatial.get_decomposition("opening").should_be_called().will_return(["opening"])
spatial.can_contain("container", "door").should_be_called().will_return(True)
spatial.can_contain("container", "opening").should_be_called().will_return(False)
ifc.run("spatial.assign_container", products=["door"], relating_structure="container").should_be_called()
ifc.get_object("door").should_be_called().will_return("door_obj")
ifc.get_object("opening").should_be_called().will_return("opening_obj")
collector.assign("door_obj").should_be_called()
collector.assign("opening_obj").should_be_called()
subject.assign_container(ifc, collector, spatial, container="container", objs=["door_obj", "opening_obj"])
class TestEnableEditingContainer:
def test_run(self, spatial):