From c0889c7f10b25491320e18cac4674e345d45ce0f Mon Sep 17 00:00:00 2001 From: Ryan Schultz Date: Sun, 22 Mar 2026 21:20:15 -0500 Subject: [PATCH] Fix IfcGridAxis duplication losing geometry on save When duplicating an IfcGridAxis one or more times before saving, duplicates shared the same IfcPolyline as the source via shallow copy. This caused two issues: (1) updating any one axis's AxisCurve during export would destroy the shared curve, corrupting others; (2) duplicates whose matrix_world checksum happened to match their current position were skipped entirely by the is_moved guard, so their moved position was never written to IFC. Three fixes: - geometry.py: call create_axis_curve immediately after copy_class for IfcGridAxis duplicates, so each new axis owns its AxisCurve from the moment of duplication rather than sharing the source's. - create_axis_curve.py: only remove the old AxisCurve when its inverse count drops to zero, preventing destruction of curves still referenced by other axes. - export_ifc.py: move the IfcGridAxis branch before the is_moved guard. Grid axes store position in AxisCurve geometry rather than ObjectPlacement, so is_moved is not a reliable gate. The internal matrices_differ check is the correct decision point, and record_object_position at the end keeps checksums in sync. Generated with the assistance of an AI coding tool. --- src/bonsai/bonsai/bim/export_ifc.py | 4 ++-- src/bonsai/bonsai/tool/geometry.py | 5 +++++ .../ifcopenshell/api/grid/create_axis_curve.py | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/bonsai/bonsai/bim/export_ifc.py b/src/bonsai/bonsai/bim/export_ifc.py index 993622ff1c..715f092ac4 100644 --- a/src/bonsai/bonsai/bim/export_ifc.py +++ b/src/bonsai/bonsai/bim/export_ifc.py @@ -120,10 +120,10 @@ class IfcExporter: # updata_representation will run edit_object_placement if object is scaled # and had no openings. return element - if not tool.Ifc.is_moved(obj): - return if element.is_a("IfcGridAxis"): return self.sync_grid_axis_object_placement(obj, element) + if not tool.Ifc.is_moved(obj): + return if not hasattr(element, "ObjectPlacement"): return bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj) diff --git a/src/bonsai/bonsai/tool/geometry.py b/src/bonsai/bonsai/tool/geometry.py index c4b7cd0ef6..4ba6a8b6b9 100644 --- a/src/bonsai/bonsai/tool/geometry.py +++ b/src/bonsai/bonsai/tool/geometry.py @@ -2670,6 +2670,11 @@ class Geometry(bonsai.core.tool.Geometry): # copy the actual class new = bonsai.core.root.copy_class(tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=new_obj) + # Give each duplicated IfcGridAxis its own AxisCurve so it doesn't + # share geometry with the source axis. + if new and new.is_a("IfcGridAxis"): + tool.Model.create_axis_curve(new_obj, new) + # clean up the orphaned mesh with ifc id of the original object to avoid confusion # IfcGridAxis keeps the same mesh data (it's pointing to ifc id 0, so it's not a problem) if new and temp_data and not new.is_a("IfcGridAxis"): diff --git a/src/ifcopenshell-python/ifcopenshell/api/grid/create_axis_curve.py b/src/ifcopenshell-python/ifcopenshell/api/grid/create_axis_curve.py index 9c4a9aa1c8..e6c1362d76 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/grid/create_axis_curve.py +++ b/src/ifcopenshell-python/ifcopenshell/api/grid/create_axis_curve.py @@ -89,5 +89,5 @@ def create_axis_curve( ), ) - if existing_curve: + if existing_curve and file.get_total_inverses(existing_curve) == 0: ifcopenshell.util.element.remove_deep2(file, existing_curve)