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
This commit is contained in:
Ryan Schultz
2026-03-22 21:57:55 -05:00
parent f2d3e226b4
commit 60063ac1c7
2 changed files with 103 additions and 0 deletions
@@ -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
@@ -0,0 +1,88 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# 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 <http://www.gnu.org/licenses/>.
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