diff --git a/src/bonsai/bonsai/bim/module/aggregate/operator.py b/src/bonsai/bonsai/bim/module/aggregate/operator.py index be6272c3ab..e9f23afece 100644 --- a/src/bonsai/bonsai/bim/module/aggregate/operator.py +++ b/src/bonsai/bonsai/bim/module/aggregate/operator.py @@ -224,7 +224,7 @@ class BIM_OT_add_aggregate(bpy.types.Operator, tool.Ifc.Operator): tool.Collector, tool.Spatial, container=current_container, - element_obj=aggregate, + objs=[aggregate], ) core.assign_object(tool.Ifc, tool.Aggregate, tool.Collector, relating_obj=aggregate, related_obj=obj) diff --git a/src/bonsai/bonsai/bim/module/geometry/operator.py b/src/bonsai/bonsai/bim/module/geometry/operator.py index 15f3052935..fbd694d219 100644 --- a/src/bonsai/bonsai/bim/module/geometry/operator.py +++ b/src/bonsai/bonsai/bim/module/geometry/operator.py @@ -1730,7 +1730,7 @@ class RefreshLinkedAggregate(bpy.types.Operator, tool.Ifc.Operator): tool.Collector, tool.Spatial, container=original_data[matching_group_id][index]["Container"], - element_obj=obj, + objs=[obj], ) for part in ifcopenshell.util.element.get_parts(tool.Ifc.get_entity(obj)): tool.Collector.assign(tool.Ifc.get_object(part)) diff --git a/src/bonsai/bonsai/bim/module/model/product.py b/src/bonsai/bonsai/bim/module/model/product.py index 8ca5712116..012cba4889 100644 --- a/src/bonsai/bonsai/bim/module/model/product.py +++ b/src/bonsai/bonsai/bim/module/model/product.py @@ -470,7 +470,7 @@ class AddOccurrence(bpy.types.Operator, tool.Ifc.Operator): parent = ifcopenshell.util.element.get_container(building_element) if parent: bonsai.core.spatial.assign_container( - tool.Ifc, tool.Collector, tool.Spatial, container=parent, element_obj=obj + tool.Ifc, tool.Collector, tool.Spatial, container=parent, objs=[obj] ) # set occurrences properties for the types defined with modifiers @@ -493,7 +493,7 @@ class AddOccurrence(bpy.types.Operator, tool.Ifc.Operator): else: if self.container_obj: bonsai.core.spatial.assign_container( - tool.Ifc, tool.Collector, tool.Spatial, container=self.container, element_obj=obj + tool.Ifc, tool.Collector, tool.Spatial, container=self.container, objs=[obj] ) if props.rl_mode == "BOTTOM": obj.location.z = self.container_obj.location.z - tool.Blender.get_object_bounding_box(obj)["min_z"] diff --git a/src/bonsai/bonsai/bim/module/spatial/operator.py b/src/bonsai/bonsai/bim/module/spatial/operator.py index 04617cc025..23c0c28335 100644 --- a/src/bonsai/bonsai/bim/module/spatial/operator.py +++ b/src/bonsai/bonsai/bim/module/spatial/operator.py @@ -171,28 +171,9 @@ class AssignContainer(bpy.types.Operator, tool.Ifc.Operator): else: return - objs: list[bpy.types.Object] = [] - # In IFC element can be either contained of aggregated, - # tehrefore we skip aggregated elements here to prevent confusion. - # Can't handle it in `poll` since user might just select bunch of elements - # and try to assign a container to them - # and excluding aggregates because of the `poll` failing might get awkward. - skipped_aggregates = 0 - for obj in tool.Blender.get_selected_objects(): - if not (element := tool.Ifc.get_entity(obj)): - continue - if ifcopenshell.util.element.get_aggregate(element): - skipped_aggregates += 1 - continue - objs.append(obj) - - for element_obj in objs: - core.assign_container(tool.Ifc, tool.Collector, tool.Spatial, container=container, element_obj=element_obj) - - aggregates_msg = "" - if skipped_aggregates: - aggregates_msg = f" {skipped_aggregates} aggregated elements skipped." - self.report({"INFO"}, f"{len(objs)} elements assigned.{aggregates_msg}") + core.assign_container( + tool.Ifc, tool.Collector, tool.Spatial, container=container, objs=tool.Blender.get_selected_objects() + ) class EnableEditingContainer(bpy.types.Operator): diff --git a/src/bonsai/bonsai/bim/module/spatial/prop.py b/src/bonsai/bonsai/bim/module/spatial/prop.py index 18ddc6080d..5c48566d08 100644 --- a/src/bonsai/bonsai/bim/module/spatial/prop.py +++ b/src/bonsai/bonsai/bim/module/spatial/prop.py @@ -175,8 +175,8 @@ def poll_container_obj(self: "BIMObjectSpatialProperties", container_obj: bpy.ty obj = self.id_data if ( (container := tool.Ifc.get_entity(container_obj)) - and (tool.Ifc.get_entity(obj)) - and tool.Spatial.can_contain(container, obj) + and (element := tool.Ifc.get_entity(obj)) + and tool.Spatial.can_contain(container, element) ): return True return False diff --git a/src/bonsai/bonsai/core/spatial.py b/src/bonsai/bonsai/core/spatial.py index 4207d033d4..c464f84766 100644 --- a/src/bonsai/bonsai/core/spatial.py +++ b/src/bonsai/bonsai/core/spatial.py @@ -52,15 +52,22 @@ def assign_container( collector: type[tool.Collector], spatial: type[tool.Spatial], container: ifcopenshell.entity_instance, - element_obj: Optional[bpy.types.Object] = None, + objs: Optional[bpy.types.Object] = None, ) -> Union[ifcopenshell.entity_instance, None]: - if not spatial.can_contain(container, element_obj): - return - assert element_obj # Type checker. - rel = ifc.run("spatial.assign_container", products=[ifc.get_entity(element_obj)], relating_structure=container) - spatial.disable_editing(element_obj) - collector.assign(element_obj) - return rel + root_elements = set() + all_elements = set() + for obj in objs: + if not (element := ifc.get_entity(obj)): + continue + root_element = spatial.get_root_element(element) + root_elements.add(root_element) + spatial.disable_editing(obj) + all_elements.add(root_element) + all_elements.update(spatial.get_decomposition(root_element)) + if products := [e for e in root_elements if spatial.can_contain(container, root_element)]: + ifc.run("spatial.assign_container", products=products, relating_structure=container) + for element in all_elements: + collector.assign(ifc.get_object(element)) def enable_editing_container(spatial: type[tool.Spatial], obj: bpy.types.Object) -> None: @@ -98,7 +105,7 @@ def copy_to_container( copied_obj = spatial.duplicate_object_and_data(obj) spatial.set_relative_object_matrix(copied_obj, to_container_obj, matrix) result_objs.append(spatial.run_root_copy_class(obj=copied_obj)) - spatial.run_spatial_assign_container(container=to_container, element_obj=copied_obj) + spatial.run_spatial_assign_container(container=to_container, objs=[copied_obj]) spatial.disable_editing(obj) return result_objs diff --git a/src/bonsai/bonsai/core/tool.py b/src/bonsai/bonsai/core/tool.py index 9d78297c4e..aad79c097e 100644 --- a/src/bonsai/bonsai/core/tool.py +++ b/src/bonsai/bonsai/core/tool.py @@ -954,12 +954,14 @@ class Spatial: def get_object_matrix(cls, obj): pass def get_relative_object_matrix(cls, target_obj, relative_to_obj): pass def get_selected_product_types(cls): pass + def get_root_element(cls, element): pass + def get_decomposition(cls, element): pass def get_selected_products(cls): pass def import_spatial_decomposition(cls): pass def import_spatial_element(cls, element, level_index): pass def load_contained_elements(cls): pass def run_root_copy_class(cls, obj): pass - def run_spatial_assign_container(cls, container, element_obj): pass + def run_spatial_assign_container(cls, container, objs): pass def run_spatial_import_spatial_decomposition(cls): pass def select_object(cls, obj): pass def select_products(cls, products, unhide=False): pass diff --git a/src/bonsai/bonsai/tool/spatial.py b/src/bonsai/bonsai/tool/spatial.py index 70a92f4cff..d38d895c22 100644 --- a/src/bonsai/bonsai/tool/spatial.py +++ b/src/bonsai/bonsai/tool/spatial.py @@ -74,9 +74,25 @@ class Spatial(bonsai.core.tool.Spatial): return bpy.context.scene.BIMGridProperties @classmethod - def can_contain(cls, container: ifcopenshell.entity_instance, element_obj: Union[bpy.types.Object, None]) -> bool: - if not (element := tool.Ifc.get_entity(element_obj)): - return False + def get_decomposition(cls, element: ifcopenshell.entity_instance) -> list(ifcopenshell.entity_instance): + return ifcopenshell.util.element.get_decomposition(element) + + @classmethod + 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) + ): + element = parent + else: + break + return element + + @classmethod + def can_contain(cls, container: ifcopenshell.entity_instance, element: ifcopenshell.entity_instance) -> bool: if tool.Ifc.get_schema() == "IFC2X3": if not container.is_a("IfcSpatialStructureElement"): return False @@ -147,10 +163,10 @@ class Spatial(bonsai.core.tool.Spatial): @classmethod def run_spatial_assign_container( - cls, container: ifcopenshell.entity_instance, element_obj: bpy.types.Object + cls, container: ifcopenshell.entity_instance, objs: list[bpy.types.Object] ) -> Union[ifcopenshell.entity_instance, None]: return bonsai.core.spatial.assign_container( - tool.Ifc, tool.Collector, tool.Spatial, container=container, element_obj=element_obj + tool.Ifc, tool.Collector, tool.Spatial, container=container, objs=objs ) @classmethod diff --git a/src/bonsai/test/core/test_spatial.py b/src/bonsai/test/core/test_spatial.py index 0ae9695ea2..ddc6116fdf 100644 --- a/src/bonsai/test/core/test_spatial.py +++ b/src/bonsai/test/core/test_spatial.py @@ -38,16 +38,19 @@ class TestDereferenceStructure: class TestAssignContainer: def test_run(self, ifc, collector, spatial): - spatial.can_contain("container", "element_obj").should_be_called().will_return(True) - ifc.get_entity("element_obj").should_be_called().will_return("element") - ifc.run( - "spatial.assign_container", products=["element"], relating_structure="container" - ).should_be_called().will_return("rel") - spatial.disable_editing("element_obj").should_be_called() - collector.assign("element_obj").should_be_called() - assert ( - subject.assign_container(ifc, collector, spatial, container="container", element_obj="element_obj") == "rel" - ) + ifc.get_entity("obj").should_be_called().will_return("element") + spatial.get_root_element("element").should_be_called().will_return("aggregate") + spatial.get_decomposition("aggregate").should_be_called().will_return(["element", "element2"]) + spatial.can_contain("container", "aggregate").should_be_called().will_return(True) + ifc.run("spatial.assign_container", products=["aggregate"], relating_structure="container").should_be_called() + spatial.disable_editing("obj").should_be_called() + ifc.get_object("aggregate").should_be_called().will_return("aggregate_obj") + ifc.get_object("element").should_be_called().will_return("obj") + ifc.get_object("element2").should_be_called().will_return("obj2") + collector.assign("aggregate_obj").should_be_called() + collector.assign("obj").should_be_called() + collector.assign("obj2").should_be_called() + subject.assign_container(ifc, collector, spatial, container="container", objs=["obj"]) class TestEnableEditingContainer: @@ -82,7 +85,7 @@ class TestCopyToContainer: spatial.duplicate_object_and_data("obj").should_be_called().will_return("new_obj") spatial.set_relative_object_matrix("new_obj", "to_container_obj", "matrix").should_be_called() spatial.run_root_copy_class(obj="new_obj").should_be_called() - spatial.run_spatial_assign_container(container="to_container", element_obj="new_obj").should_be_called() + spatial.run_spatial_assign_container(container="to_container", objs=["new_obj"]).should_be_called() spatial.disable_editing("obj").should_be_called() @@ -97,7 +100,7 @@ class TestCopyToContainer: spatial.duplicate_object_and_data("obj").should_be_called().will_return("new_obj") spatial.set_relative_object_matrix("new_obj", "to_container_obj", "matrix").should_be_called() spatial.run_root_copy_class(obj="new_obj").should_be_called() - spatial.run_spatial_assign_container(container="to_container", element_obj="new_obj").should_be_called() + spatial.run_spatial_assign_container(container="to_container", objs=["new_obj"]).should_be_called() spatial.disable_editing("obj").should_be_called() diff --git a/src/bonsai/test/tool/test_spatial.py b/src/bonsai/test/tool/test_spatial.py index 299e615f2f..005f370b01 100644 --- a/src/bonsai/test/tool/test_spatial.py +++ b/src/bonsai/test/tool/test_spatial.py @@ -43,9 +43,7 @@ class TestCanContain(NewFile): structure_obj = bpy.data.objects.new("Object", None) tool.Ifc.link(structure, structure_obj) element = ifc.createIfcWall() - element_obj = bpy.data.objects.new("Object", None) - tool.Ifc.link(element, element_obj) - assert subject.can_contain(structure, element_obj) is True + assert subject.can_contain(structure, element) is True def test_a_spatial_structure_element_can_contain_an_element_ifc2x3(self): ifc = ifcopenshell.file(schema="IFC2X3") @@ -54,9 +52,7 @@ class TestCanContain(NewFile): structure_obj = bpy.data.objects.new("Object", None) tool.Ifc.link(structure, structure_obj) element = ifc.createIfcWall() - element_obj = bpy.data.objects.new("Object", None) - tool.Ifc.link(element, element_obj) - assert subject.can_contain(structure, element_obj) is True + assert subject.can_contain(structure, element) is True def test_a_spatial_zone_element_cannot_contain_an_element(self): ifc = ifcopenshell.file() @@ -65,14 +61,7 @@ class TestCanContain(NewFile): structure_obj = bpy.data.objects.new("Object", None) tool.Ifc.link(structure, structure_obj) element = ifc.createIfcWall() - element_obj = bpy.data.objects.new("Object", None) - tool.Ifc.link(element, element_obj) - assert subject.can_contain(structure, element_obj) is False - - def test_unlinked_elements_cannot_contain_anything(self): - structure_obj = bpy.data.objects.new("Object", None) - element_obj = bpy.data.objects.new("Object", None) - assert subject.can_contain(structure_obj, element_obj) is False + assert subject.can_contain(structure, element) is False def test_a_non_spatial_element_cannot_contain_anything(self): ifc = ifcopenshell.file() @@ -81,9 +70,7 @@ class TestCanContain(NewFile): structure_obj = bpy.data.objects.new("Object", None) tool.Ifc.link(structure, structure_obj) element = ifc.createIfcWall() - element_obj = bpy.data.objects.new("Object", None) - tool.Ifc.link(element, element_obj) - assert subject.can_contain(structure, element_obj) is False + assert subject.can_contain(structure, element) is False def test_a_non_element_cannot_be_contained(self): ifc = ifcopenshell.file() @@ -92,9 +79,7 @@ class TestCanContain(NewFile): structure_obj = bpy.data.objects.new("Object", None) tool.Ifc.link(structure, structure_obj) element = ifc.createIfcTask() - element_obj = bpy.data.objects.new("Object", None) - tool.Ifc.link(element, element_obj) - assert subject.can_contain(structure, element_obj) is False + assert subject.can_contain(structure, element) is False def test_other_non_elements_that_have_a_contained_in_structure_attribute_can_be_contained(self): ifc = ifcopenshell.file() @@ -103,9 +88,7 @@ class TestCanContain(NewFile): structure_obj = bpy.data.objects.new("Object", None) tool.Ifc.link(structure, structure_obj) element = ifc.createIfcGrid() - element_obj = bpy.data.objects.new("Object", None) - tool.Ifc.link(element, element_obj) - assert subject.can_contain(structure, element_obj) is True + assert subject.can_contain(structure, element) is True class TestCanReference(NewFile):