mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-09 09:21:46 +00:00
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:
@@ -80,10 +80,7 @@ class Spatial(bonsai.core.tool.Spatial):
|
||||
def get_root_element(cls, element: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance:
|
||||
while True:
|
||||
if parent := (
|
||||
ifcopenshell.util.element.get_aggregate(element)
|
||||
or ifcopenshell.util.element.get_nest(element)
|
||||
or ifcopenshell.util.element.get_filled_void(element)
|
||||
or ifcopenshell.util.element.get_voided_element(element)
|
||||
ifcopenshell.util.element.get_aggregate(element) or ifcopenshell.util.element.get_nest(element)
|
||||
):
|
||||
element = parent
|
||||
else:
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user