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.
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.
A) RelatingProduct is OLD → new annotation was incorrectly appended → 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.
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()
old_elements = set(old_to_new.keys())
@@ -2855,6 +2862,30 @@ class Geometry(bonsai.core.tool.Geometry):
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]
# 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:
# Case B: remove the stale original from the new product's rel
rel.RelatedObjects = [e for e in rel.RelatedObjects if e != original_elem]