nest api to maintain order of related objects #4698

order of objects in .RelatedObjects is important (e.g. for cost items, it's the order of their appearance), so we should maintain it and cannot use sets for .RelatedObjects
This commit is contained in:
Andrej730
2024-05-20 16:24:35 +05:00
parent fb88499197
commit 678dbaa66a
4 changed files with 49 additions and 12 deletions
@@ -104,20 +104,20 @@ def assign_object(
return return
ifc2x3 = file.schema == "IFC2X3" ifc2x3 = file.schema == "IFC2X3"
related_objects_set = set(related_objects)
related_objects = set(settings["related_objects"])
relating_object = settings["relating_object"]
if ifc2x3: if ifc2x3:
is_nested_by = next((i for i in relating_object.IsDecomposedBy if i.is_a("IfcRelNests")), None) is_nested_by = next((i for i in relating_object.IsDecomposedBy if i.is_a("IfcRelNests")), None)
else: else:
is_nested_by = next((i for i in relating_object.IsNestedBy), None) 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() previous_nests_rels: set[ifcopenshell.entity_instance] = set()
objects_without_nests: list[ifcopenshell.entity_instance] = [] objects_without_nests: list[ifcopenshell.entity_instance] = []
objects_with_nests: list[ifcopenshell.entity_instance] = [] objects_with_nests: list[ifcopenshell.entity_instance] = []
# check if there is anything to change # check if there is anything to change
for object in related_objects: for object in related_objects_set:
if ifc2x3: if ifc2x3:
object_rel = next((i for i in object.Decomposes if i.is_a("IfcRelNests")), None) object_rel = next((i for i in object.Decomposes if i.is_a("IfcRelNests")), None)
else: else:
@@ -143,7 +143,7 @@ def assign_object(
# unassign elements from previous nests # unassign elements from previous nests
for nests in previous_nests_rels: 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: if cur_related_objects:
nests.RelatedObjects = list(cur_related_objects) nests.RelatedObjects = list(cur_related_objects)
ifcopenshell.api.run("owner.update_owner_history", file, **{"element": nests}) ifcopenshell.api.run("owner.update_owner_history", file, **{"element": nests})
@@ -155,7 +155,11 @@ def assign_object(
# assign elements to a new nesting # assign elements to a new nesting
if is_nested_by: 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}) ifcopenshell.api.run("owner.update_owner_history", file, **{"element": is_nested_by})
else: else:
is_nested_by = file.create_entity( is_nested_by = file.create_entity(
@@ -163,7 +167,7 @@ def assign_object(
**{ **{
"GlobalId": ifcopenshell.guid.new(), "GlobalId": ifcopenshell.guid.new(),
"OwnerHistory": ifcopenshell.api.run("owner.create_owner_history", file), "OwnerHistory": ifcopenshell.api.run("owner.create_owner_history", file),
"RelatedObjects": list(related_objects), "RelatedObjects": related_objects,
"RelatingObject": relating_object, "RelatingObject": relating_object,
} }
) )
@@ -49,23 +49,23 @@ def unassign_object(file: ifcopenshell.file, related_objects: list[ifcopenshell.
# nothing is returned, relationship is removed # nothing is returned, relationship is removed
ifcopenshell.api.run("nest.unassign_object", model, related_objects=[subtask2]) 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" ifc2x3 = file.schema == "IFC2X3"
if ifc2x3: if ifc2x3:
rels = set( rels = set(
rel 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)) if (rel := next((rel for rel in object.Decomposes if rel.is_a("IfcRelNests")), None))
) )
else: else:
rels = set(rel for object in related_objects if (rel := next((rel for rel in object.Nests), None))) rels = set(rel for object in related_objects if (rel := next((rel for rel in object.Nests), None)))
for rel in rels: for rel in rels:
related_objects = set(rel.RelatedObjects) - related_objects cur_related_objects = [o for o in rel.RelatedObjects if o not in related_objects_set]
if related_objects: if cur_related_objects:
rel.RelatedObjects = list(related_objects) rel.RelatedObjects = cur_related_objects
ifcopenshell.api.run("owner.update_owner_history", file, **{"element": rel}) ifcopenshell.api.run("owner.update_owner_history", file, **{"element": rel})
else: else:
history = rel.OwnerHistory history = rel.OwnerHistory
@@ -66,6 +66,23 @@ class TestAssignObject(test.bootstrap.IFC4):
with pytest.raises(RuntimeError): with pytest.raises(RuntimeError):
self.file.by_id(rel_id) 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): class TestAssignObjectIFC2X3(test.bootstrap.IFC2X3, TestAssignObject):
pass pass
@@ -50,5 +50,21 @@ class TestUnassignObject(test.bootstrap.IFC4):
ifcopenshell.api.run("nest.unassign_object", self.file, related_objects=[subelement]) ifcopenshell.api.run("nest.unassign_object", self.file, related_objects=[subelement])
assert len(self.file.by_type("IfcRelNests")) == 0 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): class TestUnassignObjectIFC2X3(test.bootstrap.IFC2X3, TestUnassignObject):
pass pass