From 60063ac1c7b165b1dc94a7690ab9a72e654278d3 Mon Sep 17 00:00:00 2001 From: Ryan Schultz Date: Sun, 22 Mar 2026 21:57:55 -0500 Subject: [PATCH] Add tests for IfcGridAxis fixes - Add test_create_axis_curve.py with three unit tests: basic polyline creation, safe removal of an unshared existing curve, and preservation of a shared curve when only one referencing axis is updated (regression for the shallow-copy duplication bug) - Add feature scenario "Export IFC - with duplicate-of-duplicate grid axis locations preserved" to project.feature, reproducing the case where duplicates of duplicates lost their positions on save/reload --- src/bonsai/test/bim/feature/project.feature | 15 ++++ .../test/api/grid/test_create_axis_curve.py | 88 +++++++++++++++++++ 2 files changed, 103 insertions(+) create mode 100644 src/ifcopenshell-python/test/api/grid/test_create_axis_curve.py diff --git a/src/bonsai/test/bim/feature/project.feature b/src/bonsai/test/bim/feature/project.feature index 0db6041b56..f81fdafaa7 100644 --- a/src/bonsai/test/bim/feature/project.feature +++ b/src/bonsai/test/bim/feature/project.feature @@ -920,6 +920,21 @@ Scenario: Export IFC - with moved grid axis location synchronised And I load previously saved IFC project Then the object "IfcGridAxis/01" bottom left corner is at "1,-2,0" +Scenario: Export IFC - with duplicate-of-duplicate grid axis locations preserved + Given an empty IFC project + And I press "bim.add_grid" + And I set "scene.BIMGridProperties.is_locked" to "False" + And the object "IfcGridAxis/01" is selected + And I duplicate the selected objects + And the object "IfcGridAxis/01.001" is moved to "1,0,0" + And the object "IfcGridAxis/01.001" is selected + And I duplicate the selected objects + And the object "IfcGridAxis/01.002" is moved to "2,0,0" + When I save IFC project + And I load previously saved IFC project + Then the object "IfcGridAxis/01.001" bottom left corner is at "1,-2,0" + And the object "IfcGridAxis/01.002" bottom left corner is at "2,-2,0" + Scenario: Export IFC - with changed object scale ignored Given an empty IFC project And I add a cube diff --git a/src/ifcopenshell-python/test/api/grid/test_create_axis_curve.py b/src/ifcopenshell-python/test/api/grid/test_create_axis_curve.py new file mode 100644 index 0000000000..8b03f4a08f --- /dev/null +++ b/src/ifcopenshell-python/test/api/grid/test_create_axis_curve.py @@ -0,0 +1,88 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2021 Dion Moult +# +# This file is part of IfcOpenShell. +# +# IfcOpenShell is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# IfcOpenShell is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with IfcOpenShell. If not, see . + +import numpy as np + +import ifcopenshell.api.grid +import test.bootstrap + + +class TestCreateAxisCurve(test.bootstrap.IFC4): + def make_grid_with_axis(self, axis_tag="A"): + grid = self.file.createIfcGrid() + grid.ObjectPlacement = self.file.createIfcLocalPlacement( + RelativePlacement=self.file.createIfcAxis2Placement3D( + Location=self.file.createIfcCartesianPoint([0.0, 0.0, 0.0]) + ) + ) + axis = ifcopenshell.api.grid.create_grid_axis( + self.file, axis_tag=axis_tag, same_sense=True, uvw_axes="UAxes", grid=grid + ) + return grid, axis + + def test_creates_a_polyline_axis_curve(self): + _, axis = self.make_grid_with_axis() + ifcopenshell.api.grid.create_axis_curve( + self.file, p1=np.array([0.0, 0.0, 0.0]), p2=np.array([10.0, 0.0, 0.0]), grid_axis=axis + ) + assert axis.AxisCurve is not None + assert axis.AxisCurve.is_a("IfcPolyline") + assert len(axis.AxisCurve.Points) == 2 + + def test_replaces_existing_curve_when_unshared(self): + """Calling create_axis_curve again on the same axis replaces the old curve + and removes the old curve from the file when nothing else references it.""" + _, axis = self.make_grid_with_axis() + ifcopenshell.api.grid.create_axis_curve( + self.file, p1=np.array([0.0, 0.0, 0.0]), p2=np.array([10.0, 0.0, 0.0]), grid_axis=axis + ) + old_curve_id = axis.AxisCurve.id() + ifcopenshell.api.grid.create_axis_curve( + self.file, p1=np.array([1.0, 0.0, 0.0]), p2=np.array([11.0, 0.0, 0.0]), grid_axis=axis + ) + assert axis.AxisCurve.id() != old_curve_id + assert self.file.by_id(old_curve_id) is None + + def test_does_not_remove_shared_curve(self): + """When two axes share the same AxisCurve (e.g. after a shallow copy during + duplication), updating one axis must not destroy the curve still referenced + by the other axis.""" + grid, axis = self.make_grid_with_axis() + axis2 = ifcopenshell.api.grid.create_grid_axis( + self.file, axis_tag="B", same_sense=True, uvw_axes="UAxes", grid=grid + ) + ifcopenshell.api.grid.create_axis_curve( + self.file, p1=np.array([0.0, 0.0, 0.0]), p2=np.array([10.0, 0.0, 0.0]), grid_axis=axis + ) + shared_curve = axis.AxisCurve + shared_curve_id = shared_curve.id() + # Simulate what copy_class produces: a duplicate axis that shares the + # source's AxisCurve rather than having its own copy. + axis2.AxisCurve = shared_curve + assert self.file.get_total_inverses(shared_curve) == 2 + + # Updating axis1's curve must not remove the curve that axis2 still needs. + ifcopenshell.api.grid.create_axis_curve( + self.file, p1=np.array([1.0, 0.0, 0.0]), p2=np.array([11.0, 0.0, 0.0]), grid_axis=axis + ) + assert axis2.AxisCurve.id() == shared_curve_id + assert self.file.by_id(shared_curve_id) is not None + + +class TestCreateAxisCurveIFC2X3(test.bootstrap.IFC2X3, TestCreateAxisCurve): + pass