From 683b7173482d62ea3f91975eb177eac797467405 Mon Sep 17 00:00:00 2001 From: Ryan Schultz Date: Fri, 1 May 2026 18:34:20 -0500 Subject: [PATCH] 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. --- src/bonsai/bonsai/tool/geometry.py | 44 ++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/src/bonsai/bonsai/tool/geometry.py b/src/bonsai/bonsai/tool/geometry.py index c4b7cd0ef6..4457e8e4ca 100644 --- a/src/bonsai/bonsai/tool/geometry.py +++ b/src/bonsai/bonsai/tool/geometry.py @@ -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()