diff --git a/src/ifcopenshell-python/ifcopenshell/api/nest/assign_object.py b/src/ifcopenshell-python/ifcopenshell/api/nest/assign_object.py index b49adc72a5..227163f467 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/nest/assign_object.py +++ b/src/ifcopenshell-python/ifcopenshell/api/nest/assign_object.py @@ -104,20 +104,20 @@ def assign_object( return ifc2x3 = file.schema == "IFC2X3" + related_objects_set = set(related_objects) - related_objects = set(settings["related_objects"]) - relating_object = settings["relating_object"] if ifc2x3: is_nested_by = next((i for i in relating_object.IsDecomposedBy if i.is_a("IfcRelNests")), None) else: is_nested_by = next((i for i in relating_object.IsNestedBy), None) + # NOTE: maintain .RelatedObjects order as it has meaning in IFC previous_nests_rels: set[ifcopenshell.entity_instance] = set() objects_without_nests: list[ifcopenshell.entity_instance] = [] objects_with_nests: list[ifcopenshell.entity_instance] = [] # check if there is anything to change - for object in related_objects: + for object in related_objects_set: if ifc2x3: object_rel = next((i for i in object.Decomposes if i.is_a("IfcRelNests")), None) else: @@ -143,7 +143,7 @@ def assign_object( # unassign elements from previous nests for nests in previous_nests_rels: - cur_related_objects = set(nests.RelatedObjects) - related_objects + cur_related_objects = [o for o in nests.RelatedObjects if o not in related_objects_set] if cur_related_objects: nests.RelatedObjects = list(cur_related_objects) ifcopenshell.api.run("owner.update_owner_history", file, **{"element": nests}) @@ -155,7 +155,11 @@ def assign_object( # assign elements to a new nesting if is_nested_by: - is_nested_by.RelatedObjects = list(set(is_nested_by.RelatedObjects) | related_objects) + cur_related_objects = list(is_nested_by.RelatedObjects) + cur_related_objects_set = set(cur_related_objects) + is_nested_by.RelatedObjects = cur_related_objects + [ + o for o in related_objects if o not in cur_related_objects_set + ] ifcopenshell.api.run("owner.update_owner_history", file, **{"element": is_nested_by}) else: is_nested_by = file.create_entity( @@ -163,7 +167,7 @@ def assign_object( **{ "GlobalId": ifcopenshell.guid.new(), "OwnerHistory": ifcopenshell.api.run("owner.create_owner_history", file), - "RelatedObjects": list(related_objects), + "RelatedObjects": related_objects, "RelatingObject": relating_object, } ) diff --git a/src/ifcopenshell-python/ifcopenshell/api/nest/unassign_object.py b/src/ifcopenshell-python/ifcopenshell/api/nest/unassign_object.py index 42e35777ad..bfd5549ad2 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/nest/unassign_object.py +++ b/src/ifcopenshell-python/ifcopenshell/api/nest/unassign_object.py @@ -49,23 +49,23 @@ def unassign_object(file: ifcopenshell.file, related_objects: list[ifcopenshell. # nothing is returned, relationship is removed ifcopenshell.api.run("nest.unassign_object", model, related_objects=[subtask2]) """ - settings = {"related_objects": related_objects} - related_objects = set(settings["related_objects"]) + # NOTE: maintain .RelatedObjects order as it has meaning in IFC + related_objects_set = set(related_objects) ifc2x3 = file.schema == "IFC2X3" if ifc2x3: rels = set( rel - for object in related_objects + for object in related_objects_set if (rel := next((rel for rel in object.Decomposes if rel.is_a("IfcRelNests")), None)) ) else: rels = set(rel for object in related_objects if (rel := next((rel for rel in object.Nests), None))) for rel in rels: - related_objects = set(rel.RelatedObjects) - related_objects - if related_objects: - rel.RelatedObjects = list(related_objects) + cur_related_objects = [o for o in rel.RelatedObjects if o not in related_objects_set] + if cur_related_objects: + rel.RelatedObjects = cur_related_objects ifcopenshell.api.run("owner.update_owner_history", file, **{"element": rel}) else: history = rel.OwnerHistory diff --git a/src/ifcopenshell-python/test/api/nest/test_assign_object.py b/src/ifcopenshell-python/test/api/nest/test_assign_object.py index 25d4259627..181ff09fd7 100644 --- a/src/ifcopenshell-python/test/api/nest/test_assign_object.py +++ b/src/ifcopenshell-python/test/api/nest/test_assign_object.py @@ -66,6 +66,23 @@ class TestAssignObject(test.bootstrap.IFC4): with pytest.raises(RuntimeError): self.file.by_id(rel_id) + def test_maintain_assignment_order_in_related_objects(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcTask") + subelements = [ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcTask") for _ in range(5)] + for i in range(5): + ifcopenshell.api.run( + "nest.assign_object", self.file, related_objects=subelements[: i + 1], relating_object=element + ) + rel = self.file.by_type("IfcRelNests")[0] + assert rel.RelatedObjects == tuple(subelements[: i + 1]) + + # maintain the order in the affected relationships too + element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcTask") + ifcopenshell.api.run( + "nest.assign_object", self.file, related_objects=subelements[2:3], relating_object=element2 + ) + assert rel.RelatedObjects == tuple(subelements[:2] + subelements[3:]) + class TestAssignObjectIFC2X3(test.bootstrap.IFC2X3, TestAssignObject): pass diff --git a/src/ifcopenshell-python/test/api/nest/test_unassign_object.py b/src/ifcopenshell-python/test/api/nest/test_unassign_object.py index 479ab0bcce..a23c452adf 100644 --- a/src/ifcopenshell-python/test/api/nest/test_unassign_object.py +++ b/src/ifcopenshell-python/test/api/nest/test_unassign_object.py @@ -50,5 +50,21 @@ class TestUnassignObject(test.bootstrap.IFC4): ifcopenshell.api.run("nest.unassign_object", self.file, related_objects=[subelement]) assert len(self.file.by_type("IfcRelNests")) == 0 + def test_maintain_assignment_order_in_related_objects(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcTask") + subelements = [ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcTask") for _ in range(5)] + rel = self.file.create_entity("IfcRelNests", RelatingObject=element, RelatedObjects=subelements) + + original_order = subelements.copy() + # unassign elements in some random order + # skip 1 element to make sure rel won't get removed + removed_elements = set() + for i in (0, 3, 2, 4): + subelement = subelements[i] + ifcopenshell.api.run("nest.unassign_object", self.file, related_objects=[subelement]) + removed_elements.add(subelement) + assert rel.RelatedObjects == tuple([o for o in original_order if o not in removed_elements]) + + class TestUnassignObjectIFC2X3(test.bootstrap.IFC2X3, TestUnassignObject): pass