Bonsai: hand the duplicated annotation to the duplicated product (#9049)

Case A only removed the new element from the original product's relationship.
That was enough while copy_class also cloned the product's relationship onto
the duplicate, because case B then stripped the stale original from it and the
duplicate was left holding the new annotation.

PR #9054 stops copy_class sharing annotations that way, since that sharing is
what leaves one annotation assigned to two products (#4014, #9049). With that
clone gone, nothing recreates the link, and duplicating a product together
with its annotation leaves the annotation copy assigned to no product at all,
which is worse than the duplicate it replaced.

Case A now follows the product: when the relating product was duplicated in
the same operation, the new element is assigned to the corresponding copy,
pairing them by index so the i-th copy of an annotation labels the i-th copy
of the product. It appends to the new product's existing relationship when it
has one and creates a fresh one otherwise, and skips entirely when the element
is already assigned to that product, which is what happens for related objects
copy_class still carries over, such as the task that outputs the product.

That last guard is what makes this safe to merge in either order relative to
#9054. Verified over five scenarios against both the old and the new
copy_class: duplicating product and annotation together, the annotation alone,
the product alone, and the two task equivalents so the 4D output link is not
disturbed. With the old copy_class every case this PR already handled still
passes; the only failure is duplicating the product alone, which is the bug
#9054 fixes and which this PR never covered.

Generated with the assistance of an AI coding tool.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Ryan Schultz
2026-07-29 10:29:34 -05:00
parent 683b717348
commit df35a8954b
+33 -2
View File
@@ -2837,9 +2837,16 @@ class Geometry(bonsai.core.tool.Geometry):
causing both old and new products to share stale RelatedObjects lists. causing both old and new products to share stale RelatedObjects lists.
Three cases, identified by whether the RelatingProduct was also duplicated: Three cases, identified by whether the RelatingProduct was also duplicated:
A) RelatingProduct is OLD → new annotation was incorrectly appended → remove it. A) RelatingProduct is OLD → new annotation was incorrectly appended → remove it,
B) RelatingProduct is NEW → stale original annotation was carried over → remove it. and hand it to the duplicate of that product, which is where it belongs.
B) RelatingProduct is NEW → stale original was carried over → remove it.
C) RelatingProduct unchanged → create a genuinely new rel for the new annotation. C) RelatingProduct unchanged → create a genuinely new rel for the new annotation.
Case A used to only remove, because copy_class also cloned the product's rel onto
the duplicate, so case B did the re-linking. copy_class no longer shares annotations
that way (#9049), so case A has to make the link itself or the duplicated annotation
would end up assigned to nothing. Case B still fires for related objects that are
not annotations, such as the task that outputs the product.
""" """
ifc_file = tool.Ifc.get() ifc_file = tool.Ifc.get()
old_elements = set(old_to_new.keys()) old_elements = set(old_to_new.keys())
@@ -2855,6 +2862,30 @@ class Geometry(bonsai.core.tool.Geometry):
if relating_product in old_elements: if relating_product in old_elements:
# Case A: remove new elem from the old product's rel # Case A: remove new elem from the old product's rel
rel.RelatedObjects = [e for e in rel.RelatedObjects if e != new_elem] rel.RelatedObjects = [e for e in rel.RelatedObjects if e != new_elem]
# The product was duplicated too, so follow it: the i-th copy of the
# annotation belongs to the i-th copy of the product it labels.
new_products = old_to_new.get(relating_product) or []
index = old_to_new[original_elem].index(new_elem)
if index < len(new_products):
new_product = new_products[index]
if not any(
r.is_a("IfcRelAssignsToProduct") and r.RelatingProduct == new_product
for r in new_elem.HasAssignments or []
):
new_rel = next(
(r for r in new_product.ReferencedBy or [] if r.is_a("IfcRelAssignsToProduct")),
None,
)
if new_rel:
new_rel.RelatedObjects = list(new_rel.RelatedObjects) + [new_elem]
else:
ifc_file.create_entity(
"IfcRelAssignsToProduct",
GlobalId=ifcopenshell.guid.new(),
OwnerHistory=rel.OwnerHistory,
RelatedObjects=[new_elem],
RelatingProduct=new_product,
)
elif relating_product in new_elements: elif relating_product in new_elements:
# Case B: remove the stale original from the new product's rel # Case B: remove the stale original from the new product's rel
rel.RelatedObjects = [e for e in rel.RelatedObjects if e != original_elem] rel.RelatedObjects = [e for e in rel.RelatedObjects if e != original_elem]