Closes #6621: Fix IfcRelAssignsToProduct on duplicate

When duplicating a product+annotation pair linked via
IfcRelAssignsToProduct, copy_class's generic else-branch
appended the new annotation to every existing rel that
referenced the original, leaving both old and new products
sharing stale RelatedObjects.

Adds fix_assigns_to_product_after_duplication to
tool.Geometry, called at the end of duplicate_ifc_objects.
It corrects the three cases: new annotation in old product's
rel (remove it), stale original in new product's rel (remove
it), and annotation-only duplicate (build a fresh unique rel).

Generated with the assistance of an AI coding tool.
This commit is contained in:
Ryan Schultz
2026-05-01 18:34:20 -05:00
parent 256d5a63f1
commit 683b717348
+44
View File
@@ -2533,6 +2533,7 @@ class Geometry(bonsai.core.tool.Geometry):
# is complete.
cls._recalculate_walls_with_new_connections(old_to_new)
cls.fix_assigns_to_product_after_duplication(old_to_new)
bonsai.bim.handler.refresh_ui_data()
tool.Root.reload_grid_decorator()
return old_to_new, new_active_obj or active_object
@@ -2825,6 +2826,49 @@ class Geometry(bonsai.core.tool.Geometry):
if entity in old_to_new.keys() or single_obj:
cls.remove_connection(connection)
@classmethod
def fix_assigns_to_product_after_duplication(
cls, old_to_new: dict[ifcopenshell.entity_instance, list[ifcopenshell.entity_instance]]
) -> None:
"""Fix IfcRelAssignsToProduct relationships broken by copy_class duplication.
copy_class hits the generic else-branch for IfcRelAssignsToProduct and
appends the new element into every existing rel that referenced the original,
causing both old and new products to share stale RelatedObjects lists.
Three cases, identified by whether the RelatingProduct was also duplicated:
A) RelatingProduct is OLD → new annotation was incorrectly appended → remove it.
B) RelatingProduct is NEW → stale original annotation was carried over → remove it.
C) RelatingProduct unchanged → create a genuinely new rel for the new annotation.
"""
ifc_file = tool.Ifc.get()
old_elements = set(old_to_new.keys())
new_to_old = {new: old for old, new_list in old_to_new.items() for new in new_list}
new_elements = set(new_to_old.keys())
for new_elem in new_elements:
original_elem = new_to_old[new_elem]
for rel in list(ifc_file.get_inverse(new_elem)):
if not rel.is_a("IfcRelAssignsToProduct") or new_elem not in rel.RelatedObjects:
continue
relating_product = rel.RelatingProduct
if relating_product in old_elements:
# Case A: remove new elem from the old product's rel
rel.RelatedObjects = [e for e in rel.RelatedObjects if e != new_elem]
elif relating_product in new_elements:
# Case B: remove the stale original from the new product's rel
rel.RelatedObjects = [e for e in rel.RelatedObjects if e != original_elem]
else:
# Case C: product not duplicated — build a fresh unique rel
rel.RelatedObjects = [e for e in rel.RelatedObjects if e != new_elem]
ifc_file.create_entity(
"IfcRelAssignsToProduct",
GlobalId=ifcopenshell.guid.new(),
OwnerHistory=rel.OwnerHistory,
RelatedObjects=[new_elem],
RelatingProduct=relating_product,
)
@classmethod
def remove_linked_aggregate_data(cls, old_to_new):
ifc_file = tool.Ifc.get()