diff --git a/src/blenderbim/pytest.ini b/src/blenderbim/pytest.ini index 5d7b6426f3..18cf5abd13 100644 --- a/src/blenderbim/pytest.ini +++ b/src/blenderbim/pytest.ini @@ -1,5 +1,6 @@ [pytest] markers = + aggregate context geometry owner diff --git a/src/ifcopenshell-python/ifcopenshell/api/aggregate/assign_object.py b/src/ifcopenshell-python/ifcopenshell/api/aggregate/assign_object.py index 379ee8fdce..13af016807 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/aggregate/assign_object.py +++ b/src/ifcopenshell-python/ifcopenshell/api/aggregate/assign_object.py @@ -26,6 +26,10 @@ class Usecase: if decomposes and decomposes == is_decomposed_by: return + container = ifcopenshell.util.element.get_container(self.settings["product"], should_get_direct=True) + if container: + ifcopenshell.api.run("spatial.remove_container", self.file, product=self.settings["product"]) + if decomposes: related_objects = list(decomposes.RelatedObjects) related_objects.remove(self.settings["product"]) @@ -50,4 +54,15 @@ class Usecase: "RelatingObject": self.settings["relating_object"], } ) + + placement = getattr(self.settings["product"], "ObjectPlacement", None) + if placement and placement.is_a("IfcLocalPlacement"): + ifcopenshell.api.run( + "geometry.edit_object_placement", + self.file, + product=self.settings["product"], + matrix=ifcopenshell.util.placement.get_local_placement(self.settings["product"].ObjectPlacement), + is_si=False, + ) + return is_decomposed_by diff --git a/src/ifcopenshell-python/ifcopenshell/api/spatial/assign_container.py b/src/ifcopenshell-python/ifcopenshell/api/spatial/assign_container.py index 149dea4d55..fa920ef862 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/spatial/assign_container.py +++ b/src/ifcopenshell-python/ifcopenshell/api/spatial/assign_container.py @@ -60,3 +60,5 @@ class Usecase: matrix=ifcopenshell.util.placement.get_local_placement(self.settings["product"].ObjectPlacement), is_si=False, ) + + return contains_elements diff --git a/src/ifcopenshell-python/ifcopenshell/util/element.py b/src/ifcopenshell-python/ifcopenshell/util/element.py index b5d12a64e8..36b9bc782a 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/element.py +++ b/src/ifcopenshell-python/ifcopenshell/util/element.py @@ -78,12 +78,16 @@ def get_material(element, should_skip_usage=False): return get_material(relating_type, should_skip_usage) -def get_container(element): - aggregate = get_aggregate(element) - if aggregate: - return get_container(aggregate) - if hasattr(element, "ContainedInStructure") and element.ContainedInStructure: - return element.ContainedInStructure[0].RelatingStructure +def get_container(element, should_get_direct=False): + if should_get_direct: + if hasattr(element, "ContainedInStructure") and element.ContainedInStructure: + return element.ContainedInStructure[0].RelatingStructure + else: + aggregate = get_aggregate(element) + if aggregate: + return get_container(aggregate, should_get_direct) + if hasattr(element, "ContainedInStructure") and element.ContainedInStructure: + return element.ContainedInStructure[0].RelatingStructure def get_decomposition(element): diff --git a/src/ifcopenshell-python/test/api/aggregate/test_assign_object.py b/src/ifcopenshell-python/test/api/aggregate/test_assign_object.py new file mode 100644 index 0000000000..216b0a7066 --- /dev/null +++ b/src/ifcopenshell-python/test/api/aggregate/test_assign_object.py @@ -0,0 +1,101 @@ +import numpy +import pytest +import test.bootstrap +import ifcopenshell.api +import ifcopenshell.util.element +import ifcopenshell.util.placement + + +class TestAssignObject(test.bootstrap.IFC4): + def test_assigning_a_container(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcSite") + subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") + rel = ifcopenshell.api.run("aggregate.assign_object", self.file, product=subelement, relating_object=element) + assert ifcopenshell.util.element.get_aggregate(subelement) == element + assert rel.is_a("IfcRelAggregates") + + def test_doing_nothing_if_the_aggregate_is_already_assigned(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcSite") + subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") + ifcopenshell.api.run("aggregate.assign_object", self.file, product=subelement, relating_object=element) + total_elements = len([e for e in self.file]) + ifcopenshell.api.run("aggregate.assign_object", self.file, product=subelement, relating_object=element) + assert len([e for e in self.file]) == total_elements + + def test_that_old_aggregate_relationships_are_updated_if_they_still_have_elements(self): + element1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcSite") + element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcSite") + subelement1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") + subelement2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") + ifcopenshell.api.run("aggregate.assign_object", self.file, product=subelement1, relating_object=element1) + ifcopenshell.api.run("aggregate.assign_object", self.file, product=subelement2, relating_object=element1) + rel = subelement1.Decomposes[0] + assert len(rel.RelatedObjects) == 2 + ifcopenshell.api.run("aggregate.assign_object", self.file, product=subelement1, relating_object=element2) + assert len(rel.RelatedObjects) == 1 + + def test_that_old_aggregate_relationships_are_purged_if_no_more_elements_are_contained(self): + element1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcSite") + element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcSite") + subelement1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") + ifcopenshell.api.run("aggregate.assign_object", self.file, product=subelement1, relating_object=element1) + rel_id = subelement1.Decomposes[0].id() + ifcopenshell.api.run("aggregate.assign_object", self.file, product=subelement1, relating_object=element2) + with pytest.raises(RuntimeError): + self.file.by_id(rel_id) + + def test_assigning_a_container_does_not_shift_object_placements(self): + ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject") + ifcopenshell.api.run("unit.assign_unit", self.file) + element1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcSite") + element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcSite") + subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") + ifcopenshell.api.run("aggregate.assign_object", self.file, product=subelement, relating_object=element1) + matrix1 = numpy.array( + ( + (1.0, 0.0, 0.0, 1.0), + (0.0, 1.0, 0.0, 1.0), + (0.0, 0.0, 1.0, 1.0), + (0.0, 0.0, 0.0, 1.0), + ) + ) + matrix2 = numpy.array( + ( + (1.0, 0.0, 0.0, 2.0), + (0.0, 1.0, 0.0, 2.0), + (0.0, 0.0, 1.0, 2.0), + (0.0, 0.0, 0.0, 1.0), + ) + ) + ifcopenshell.api.run( + "geometry.edit_object_placement", self.file, product=element1, matrix=matrix1.copy(), is_si=False + ) + ifcopenshell.api.run( + "geometry.edit_object_placement", self.file, product=element2, matrix=matrix2.copy(), is_si=False + ) + ifcopenshell.api.run( + "geometry.edit_object_placement", self.file, product=subelement, matrix=matrix1.copy(), is_si=False + ) + ifcopenshell.api.run("aggregate.assign_object", self.file, product=subelement, relating_object=element2) + assert subelement.ObjectPlacement.PlacementRelTo.PlacesObject[0] == element2 + assert numpy.array_equal(ifcopenshell.util.placement.get_local_placement(subelement.ObjectPlacement), matrix1) + + def test_not_updating_placement_if_placement_is_not_relative(self): + ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject") + ifcopenshell.api.run("unit.assign_unit", self.file) + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcSite") + subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") + placement = self.file.createIfcGridPlacement() + subelement.ObjectPlacement = placement + ifcopenshell.api.run("aggregate.assign_object", self.file, product=subelement, relating_object=element) + assert subelement.ObjectPlacement == placement + + def test_removing_containment_if_it_exists(self): + ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject") + ifcopenshell.api.run("unit.assign_unit", self.file) + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcElementAssembly") + container = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuildingStorey") + subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + ifcopenshell.api.run("spatial.assign_container", self.file, product=subelement, relating_structure=container) + ifcopenshell.api.run("aggregate.assign_object", self.file, product=subelement, relating_object=element) + assert not ifcopenshell.util.element.get_container(subelement, should_get_direct=True) diff --git a/src/ifcopenshell-python/test/api/spatial/test_assign_container.py b/src/ifcopenshell-python/test/api/spatial/test_assign_container.py index b06dbfdfe2..b24412ef31 100644 --- a/src/ifcopenshell-python/test/api/spatial/test_assign_container.py +++ b/src/ifcopenshell-python/test/api/spatial/test_assign_container.py @@ -10,8 +10,11 @@ class TestAssignContainer(test.bootstrap.IFC4): def test_assigning_a_container(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") - ifcopenshell.api.run("spatial.assign_container", self.file, product=subelement, relating_structure=element) + rel = ifcopenshell.api.run( + "spatial.assign_container", self.file, product=subelement, relating_structure=element + ) assert ifcopenshell.util.element.get_container(subelement) == element + assert rel.is_a("IfcRelContainedInSpatialStructure") def test_doing_nothing_if_the_container_is_already_assigned(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") diff --git a/src/ifcopenshell-python/test/util/test_element.py b/src/ifcopenshell-python/test/util/test_element.py index ec0a40984b..0602014388 100644 --- a/src/ifcopenshell-python/test/util/test_element.py +++ b/src/ifcopenshell-python/test/util/test_element.py @@ -178,6 +178,14 @@ class TestGetContainerIFC4(test.bootstrap.IFC4): ifcopenshell.api.run("aggregate.assign_object", self.file, product=subelement, relating_object=element) assert ifcopenshell.util.element.get_container(subelement) == building + def test_getting_nothing_if_we_enforce_only_getting_direct_spatial_containers(self): + subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcElementAssembly") + building = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") + ifcopenshell.api.run("spatial.assign_container", self.file, product=element, relating_structure=building) + ifcopenshell.api.run("aggregate.assign_object", self.file, product=subelement, relating_object=element) + assert ifcopenshell.util.element.get_container(subelement, should_get_direct=True) is None + class TestGetDecompositionIFC4(test.bootstrap.IFC4): def test_getting_decomposed_subelements_of_an_element(self):