Bonsai | Copy IfcAnnotation leader from one IFC file to another with the product assignment data etc
Hi,
Let me explain my workflow, I had an IFC project , my teammate want to work on labelling the annotations in some elevations within the same project, we wanted to work , simultaneously, so he did the annotations.
The dimensions were easily appended using the project library and then assigned group.

but for the leaders wit product assignments, it was not easy, as all the relations with the objects gone.

, > Is there any easy way or better workflow? , I tried a python script after several iterations and it worked somehow :0, but not sure whether the ifc file is damaged, no errors in opening :),
The script is pasted below:
import os
import ifcopenshell
import numpy as np
from ifcopenshell.util.placement import get_local_placement
# ===== Paths =====
source_path = r"file_from_leader to be copied.ifc"
target_path = r"file_to _the leader is pasted.ifc"
save_path = r"path_to save_the_new_File.ifc"
# ===== Load files =====
source_ifc = ifcopenshell.open(source_path)
target_ifc = ifcopenshell.open(target_path)
# ===== IDs to copy =====
target_ids = ["0ta3j1AHvE98CkvIVBF_aC", "3kus5IHHf0Q8LaBDJglxSu", "1ZMfUHnzXBqvlF7ej6787n"]
# ===== Create base file from target =====
new_ifc = ifcopenshell.file(schema=target_ifc.schema)
for entity in target_ifc:
new_ifc.add(entity)
# ===== Placement creation =====
def create_placement(ifc_file, location=(0, 0, 0), z_axis=(0, 0, 1), x_axis=(1, 0, 0)):
"""Create a simple IfcLocalPlacement at origin."""
point = ifc_file.createIfcCartesianPoint(location)
z_axis_dir = ifc_file.createIfcDirection(z_axis)
x_axis_dir = ifc_file.createIfcDirection(x_axis)
axis_placement = ifc_file.createIfcAxis2Placement3D(point, z_axis_dir, x_axis_dir)
return ifc_file.createIfcLocalPlacement(None, axis_placement)
# ===== Deep copy placement =====
def deep_copy_placement(source_placement, target_file, copied_entities=None):
"""Recursively copy IfcLocalPlacement and dependencies."""
if source_placement is None:
return None
if copied_entities is None:
copied_entities = {}
if source_placement.id() in copied_entities:
return copied_entities[source_placement.id()]
# Copy PlacementRelTo recursively
placement_relto = None
if hasattr(source_placement, 'PlacementRelTo') and source_placement.PlacementRelTo:
placement_relto = deep_copy_placement(source_placement.PlacementRelTo, target_file, copied_entities)
# Copy RelativePlacement
relative_placement = None
if hasattr(source_placement, 'RelativePlacement') and source_placement.RelativePlacement:
rp = source_placement.RelativePlacement
point = target_file.createIfcCartesianPoint(tuple(rp.Location.Coordinates))
z_dir = target_file.createIfcDirection(tuple(rp.Axis.DirectionRatios)) if rp.Axis else None
x_dir = target_file.createIfcDirection(tuple(rp.RefDirection.DirectionRatios)) if rp.RefDirection else None
relative_placement = target_file.createIfcAxis2Placement3D(point, z_dir, x_dir)
# Create the new placement
new_placement = target_file.createIfcLocalPlacement(placement_relto, relative_placement)
copied_entities[source_placement.id()] = new_placement
return new_placement
# ===== Deep copy any IFC entity (for geometry) =====
def deep_copy_entity(entity, target_file, copied_entities):
"""Generic recursive entity copier."""
if entity is None:
return None
if entity.id() in copied_entities:
return copied_entities[entity.id()]
kwargs = {}
for k, v in entity.get_info().items():
if k in ['id', 'type'] or v is None:
continue
if hasattr(v, 'is_a'): # nested IFC entity
kwargs[k] = deep_copy_entity(v, target_file, copied_entities)
elif isinstance(v, (list, tuple)):
new_list = []
for val in v:
if hasattr(val, 'is_a'):
new_list.append(deep_copy_entity(val, target_file, copied_entities))
else:
new_list.append(val)
kwargs[k] = tuple(new_list)
else:
kwargs[k] = v
new_ent = target_file.create_entity(entity.is_a(), **kwargs)
copied_entities[entity.id()] = new_ent
return new_ent
# ===== Deep copy representation =====
def deep_copy_representation(source_rep, target_file, copied_entities=None):
if copied_entities is None:
copied_entities = {}
return deep_copy_entity(source_rep, target_file, copied_entities)
# ===== Main loop =====
for guid in target_ids:
annotation = source_ifc.by_guid(guid)
if not annotation:
print(f"❌ Source annotation {guid} not found")
continue
print(f"\nProcessing {guid} ({annotation.is_a()})")
# Copy placement deeply or create at origin
if not hasattr(annotation, 'ObjectPlacement') or not annotation.ObjectPlacement:
placement = create_placement(new_ifc)
print("📌 Created new placement at origin")
else:
try:
placement = deep_copy_placement(annotation.ObjectPlacement, new_ifc)
print("📌 Copied existing placement (deep copy)")
except:
placement = create_placement(new_ifc)
print("⚠️ Failed to copy placement, created new one at origin")
# Copy annotation entity (without placement/representation)
new_annotation = new_ifc.create_entity(
annotation.is_a(),
**{k: v for k, v in annotation.get_info().items()
if k not in ['id', 'type', 'ObjectPlacement', 'Representation'] and v is not None}
)
new_annotation.ObjectPlacement = placement
# Copy representation deeply (including IndexedPolyCurve + point list)
if hasattr(annotation, 'Representation') and annotation.Representation:
representation = deep_copy_representation(annotation.Representation, new_ifc)
new_annotation.Representation = representation
print("📐 Deep copied representation with geometry")
# Copy product assignment
for rel in getattr(annotation, "HasAssignments", []):
if rel.is_a("IfcRelAssignsToProduct"):
product_guid = rel.RelatingProduct.GlobalId
target_product = new_ifc.by_guid(product_guid)
if target_product:
new_ifc.create_entity(
"IfcRelAssignsToProduct",
GlobalId=ifcopenshell.guid.new(),
OwnerHistory=target_product.OwnerHistory,
Name=rel.Name or "Annotation Link",
RelatedObjects=[new_annotation],
RelatingProduct=target_product
)
print(f"📎 Linked to product {product_guid}")
print(f"✅ Successfully processed {guid}")
# ===== Save file =====
print("\nSaving file...")
new_ifc.write(save_path)
print(f"✅ File saved to: {save_path}")
# ===== Verification =====
print("\n=== FINAL VERIFICATION ===")
try:
saved_ifc = ifcopenshell.open(save_path)
for guid in target_ids:
annotation = saved_ifc.by_guid(guid)
if annotation:
print(f"\n🔍 {guid} verification:")
print(f"Type: {annotation.is_a()}")
# Placement check
if hasattr(annotation, 'ObjectPlacement') and annotation.ObjectPlacement:
try:
matrix = get_local_placement(annotation.ObjectPlacement)
print("Placement matrix:")
print(np.array_str(matrix, precision=2))
except Exception as e:
print(f"⚠️ Placement error: {str(e)}")
else:
print("❌ Missing ObjectPlacement")
# Representation check
if hasattr(annotation, 'Representation') and annotation.Representation:
print(f"Representation type: {annotation.Representation.is_a()}")
if annotation.Representation.is_a('IfcProductDefinitionShape'):
reps = getattr(annotation.Representation, 'Representations', [])
print(f"- {len(reps)} representations")
for rep in reps:
print(f" - {rep.is_a()} with {len(getattr(rep, 'Items', []))} items")
else:
print("⚠️ No representation found")
print("✅ Verification complete")
else:
print(f"❌ {guid} not found in saved file")
except Exception as e:
print(f"⚠️ Verification error: {str(e)}")
print("\nProcess completed successfully")
Tagged:

Comments
Have you tried the git workflow that's native with Bonsai? Would allow multiple collaborators to work concurrently on the 'same model'. Just be aware not to modify anything that your collaborator will touch, which can be hard to accomplish sometimes. @brunopostle has recently improved the merging of small conflicts. I'm not sure these changes, however, made it into the latest release yet.
BTW, i think once cracked, this Git workflow with IFC and Bonsai is one of the more ground breaking parts of using Bonsai. I don't think the typical user has really realized its potential yet.
Yeah , tried the git workflow , its wonderful.
I deleted an from object from my branch which was present in my collaborators branch, which made the file merge conflict.
Without modifying same elements, its good.
The workaround for this is to delete the modified element (so it is deleted in both branches) and the merge will work. If you want to keep the modified element, duplicate it before deleting it.