Files
kicad-source-mirror/qa/tests/common/test_grid_helper.cpp
T
Seth Hillbrand 42928c8b2f Fix Move command grabbing from grid point instead of anchor
The Move command grabbed footprints from the nearest grid point rather
than the footprint anchor. This was caused by the reference point being
grid-aligned at drag start (commit 89be3fd390), which was a workaround
for fractional-nanometer positioning errors with non-page display origin.

Fix the root cause instead: AlignGrid(VECTOR2I, VECTOR2D, VECTOR2D)
relied on implicit VECTOR2D->VECTOR2I truncation to pass grid parameters
to computeNearest. For grid sizes that aren't exact in IEEE 754 double
(e.g., 0.254mm = 10 mil becomes 253999.999... instead of 254000), the
truncation produces incorrect grid sizes. Use KiROUND for the conversion
so all grid operations produce exact grid multiples.

With AlignGrid returning correct positions, the move tool no longer needs
to grid-align its reference point. Restore the anchor-based grab and the
GetMoveWarpsCursor() preference that 89be3fd390 removed.

Fixes https://gitlab.com/kicad/code/kicad/-/issues/23308
2026-03-10 11:07:39 -07:00

488 lines
16 KiB
C++

/*
* This program source code file is part of KiCad, a free EDA CAD application.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#define BOOST_TEST_NO_MAIN
#include <boost/test/unit_test.hpp>
#include <tool/grid_helper.h>
void TEST_CLEAR_ANCHORS( GRID_HELPER& helper )
{
helper.clearAnchors();
}
BOOST_AUTO_TEST_SUITE( GridHelperTest )
BOOST_AUTO_TEST_CASE( DefaultConstructor )
{
GRID_HELPER helper;
// Test default state
BOOST_CHECK( helper.GetSnap() );
BOOST_CHECK( helper.GetUseGrid() );
// Test that manual setters work
helper.SetGridSize( VECTOR2D( 100, 100 ) );
helper.SetOrigin( VECTOR2I( 50, 50 ) );
helper.SetGridSnapping( true );
VECTOR2I grid = helper.GetGrid();
BOOST_CHECK_EQUAL( grid.x, 100 );
BOOST_CHECK_EQUAL( grid.y, 100 );
VECTOR2I origin = helper.GetOrigin();
BOOST_CHECK_EQUAL( origin.x, 50 );
BOOST_CHECK_EQUAL( origin.y, 50 );
}
BOOST_AUTO_TEST_CASE( AlignBasic )
{
GRID_HELPER helper;
helper.SetGridSize( VECTOR2D( 100, 100 ) );
helper.SetOrigin( VECTOR2I( 0, 0 ) );
helper.SetGridSnapping( true );
// Test basic alignment - should round to nearest grid point
VECTOR2I aligned = helper.Align( VECTOR2I( 149, 251 ) );
BOOST_CHECK_EQUAL( aligned.x, 100 );
BOOST_CHECK_EQUAL( aligned.y, 300 );
// Test exact grid points
aligned = helper.Align( VECTOR2I( 200, 300 ) );
BOOST_CHECK_EQUAL( aligned.x, 200 );
BOOST_CHECK_EQUAL( aligned.y, 300 );
// Test negative coordinates
aligned = helper.Align( VECTOR2I( -149, -251 ) );
BOOST_CHECK_EQUAL( aligned.x, -100 );
BOOST_CHECK_EQUAL( aligned.y, -300 );
}
BOOST_AUTO_TEST_CASE( AlignGridWithCustomGrid )
{
GRID_HELPER helper;
helper.SetGridSize( VECTOR2D( 50, 50 ) );
helper.SetOrigin( VECTOR2I( 0, 0 ) );
helper.SetGridSnapping( true );
VECTOR2I aligned = helper.AlignGrid( VECTOR2I( 26, 74 ) );
BOOST_CHECK_EQUAL( aligned.x, 50 );
BOOST_CHECK_EQUAL( aligned.y, 50 );
// Test AlignGrid with specific grid parameter
aligned = helper.AlignGrid( VECTOR2I( 26, 74 ), VECTOR2D( 25, 25 ), VECTOR2D( 0, 0 ) );
BOOST_CHECK_EQUAL( aligned.x, 25 );
BOOST_CHECK_EQUAL( aligned.y, 75 );
}
BOOST_AUTO_TEST_CASE( AlignWithOriginOffset )
{
GRID_HELPER helper;
helper.SetGridSize( VECTOR2D( 100, 100 ) );
helper.SetOrigin( VECTOR2I( 25, 25 ) );
helper.SetGridSnapping( true );
// When grid has an origin offset, alignment should work from the new reference point
VECTOR2I aligned = helper.AlignGrid( VECTOR2I( 149, 251 ) );
BOOST_CHECK_EQUAL( aligned.x, 125 );
BOOST_CHECK_EQUAL( aligned.y, 225 );
}
BOOST_AUTO_TEST_CASE( AlignWithAuxiliaryAxes )
{
GRID_HELPER helper;
helper.SetGridSize( VECTOR2D( 100, 100 ) );
helper.SetOrigin( VECTOR2I( 0, 0 ) );
helper.SetGridSnapping( true );
// Set auxiliary axis at (75, 75)
helper.SetAuxAxes( true, VECTOR2I( 75, 75 ) );
// Point closer to aux axis than grid should snap to aux axis
VECTOR2I aligned = helper.Align( VECTOR2I( 80, 80 ) );
BOOST_CHECK_EQUAL( aligned.x, 75 ); // Closer to aux axis X
BOOST_CHECK_EQUAL( aligned.y, 75 ); // Closer to aux axis Y
// Point closer to grid than aux axis should snap to grid
aligned = helper.Align( VECTOR2I( 95, 95 ) );
BOOST_CHECK_EQUAL( aligned.x, 100 ); // Closer to grid
BOOST_CHECK_EQUAL( aligned.y, 100 ); // Closer to grid
// Disable aux axes
helper.SetAuxAxes( false );
aligned = helper.Align( VECTOR2I( 80, 80 ) );
BOOST_CHECK_EQUAL( aligned.x, 100 ); // Should snap to grid only
BOOST_CHECK_EQUAL( aligned.y, 100 );
}
BOOST_AUTO_TEST_CASE( GridSnappingDisabled )
{
GRID_HELPER helper;
helper.SetGridSize( VECTOR2D( 100, 100 ) );
helper.SetOrigin( VECTOR2I( 0, 0 ) );
helper.SetGridSnapping( false ); // Disable grid snapping
// When grid snapping is disabled, Align should return original point
VECTOR2I original( 149, 251 );
VECTOR2I aligned = helper.Align( original );
BOOST_CHECK_EQUAL( aligned.x, original.x );
BOOST_CHECK_EQUAL( aligned.y, original.y );
// AlignGrid should still work regardless of grid snapping setting
aligned = helper.AlignGrid( original );
BOOST_CHECK_EQUAL( aligned.x, 100 );
BOOST_CHECK_EQUAL( aligned.y, 300 );
}
BOOST_AUTO_TEST_CASE( UseGridDisabled )
{
GRID_HELPER helper;
helper.SetGridSize( VECTOR2D( 100, 100 ) );
helper.SetOrigin( VECTOR2I( 0, 0 ) );
helper.SetGridSnapping( true );
helper.SetUseGrid( false ); // Disable grid usage
// When grid usage is disabled, Align should return original point
VECTOR2I original( 149, 251 );
VECTOR2I aligned = helper.Align( original );
BOOST_CHECK_EQUAL( aligned.x, original.x );
BOOST_CHECK_EQUAL( aligned.y, original.y );
}
BOOST_AUTO_TEST_CASE( AsymmetricGrid )
{
GRID_HELPER helper;
helper.SetGridSize( VECTOR2D( 25, 75 ) ); // Different X and Y grid sizes
helper.SetOrigin( VECTOR2I( 0, 0 ) );
helper.SetGridSnapping( true );
VECTOR2I aligned = helper.Align( VECTOR2I( 30, 100 ) );
BOOST_CHECK_EQUAL( aligned.x, 25 ); // Nearest 25-unit boundary
BOOST_CHECK_EQUAL( aligned.y, 75 ); // Nearest 75-unit boundary
aligned = helper.Align( VECTOR2I( 40, 120 ) );
BOOST_CHECK_EQUAL( aligned.x, 50 ); // Next 25-unit boundary
BOOST_CHECK_EQUAL( aligned.y, 150 ); // Next 75-unit boundary
}
BOOST_AUTO_TEST_CASE( SnapFlags )
{
GRID_HELPER helper;
// Test snap flag getters/setters
BOOST_CHECK( helper.GetSnap() ); // Default should be true
helper.SetSnap( false );
BOOST_CHECK( !helper.GetSnap() );
helper.SetSnap( true );
BOOST_CHECK( helper.GetSnap() );
// Test grid usage flag
BOOST_CHECK( helper.GetUseGrid() ); // Default should be true
helper.SetUseGrid( false );
BOOST_CHECK( !helper.GetUseGrid() );
helper.SetUseGrid( true );
BOOST_CHECK( helper.GetUseGrid() );
}
BOOST_AUTO_TEST_CASE( MaskOperations )
{
GRID_HELPER helper;
// Test mask operations
helper.SetMask( GRID_HELPER::CORNER | GRID_HELPER::OUTLINE );
helper.SetMaskFlag( GRID_HELPER::SNAPPABLE );
helper.ClearMaskFlag( GRID_HELPER::CORNER );
// These don't have getters, so we can't verify the mask state directly
// but we can verify the methods don't crash
}
BOOST_AUTO_TEST_CASE( SkipPoint )
{
GRID_HELPER helper;
// Test skip point operations
helper.SetSkipPoint( VECTOR2I( 100, 100 ) );
helper.ClearSkipPoint();
// These methods should not crash
}
BOOST_AUTO_TEST_CASE( GridTypeAlignment )
{
GRID_HELPER helper;
helper.SetGridSize( VECTOR2D( 100, 100 ) );
helper.SetOrigin( VECTOR2I( 0, 0 ) );
helper.SetGridSnapping( true );
// Test alignment with specific grid type
VECTOR2I aligned = helper.Align( VECTOR2I( 149, 251 ), GRID_CURRENT );
BOOST_CHECK_EQUAL( aligned.x, 100 );
BOOST_CHECK_EQUAL( aligned.y, 300 );
aligned = helper.AlignGrid( VECTOR2I( 149, 251 ), GRID_CURRENT );
BOOST_CHECK_EQUAL( aligned.x, 100 );
BOOST_CHECK_EQUAL( aligned.y, 300 );
}
BOOST_AUTO_TEST_CASE( EdgeCases )
{
GRID_HELPER helper;
helper.SetGridSize( VECTOR2D( 1, 1 ) ); // Very small grid
helper.SetOrigin( VECTOR2I( 0, 0 ) );
helper.SetGridSnapping( true );
// Test with very small grid
VECTOR2I aligned = helper.Align( VECTOR2I( 5, 5 ) );
BOOST_CHECK_EQUAL( aligned.x, 5 );
BOOST_CHECK_EQUAL( aligned.y, 5 );
// Test with zero point
aligned = helper.Align( VECTOR2I( 0, 0 ) );
BOOST_CHECK_EQUAL( aligned.x, 0 );
BOOST_CHECK_EQUAL( aligned.y, 0 );
// Test with large grid
helper.SetGridSize( VECTOR2D( 10000, 10000 ) );
aligned = helper.Align( VECTOR2I( 3000, 7000 ) );
BOOST_CHECK_EQUAL( aligned.x, 0 ); // Closer to 0 than 10000
BOOST_CHECK_EQUAL( aligned.y, 10000 ); // Closer to 10000 than 0
}
BOOST_AUTO_TEST_CASE( GetGridSize )
{
GRID_HELPER helper;
// Test GetGridSize with different grid types
helper.SetGridSize( VECTOR2D( 50, 75 ) );
VECTOR2D gridSize = helper.GetGridSize( GRID_CURRENT );
BOOST_CHECK_EQUAL( gridSize.x, 50 );
BOOST_CHECK_EQUAL( gridSize.y, 75 );
// Other grid types should return the same in the base implementation
gridSize = helper.GetGridSize( GRID_CONNECTABLE );
BOOST_CHECK_EQUAL( gridSize.x, 50 );
BOOST_CHECK_EQUAL( gridSize.y, 75 );
}
BOOST_AUTO_TEST_CASE( VisibleGrid )
{
GRID_HELPER helper;
helper.SetVisibleGridSize( VECTOR2D( 25, 35 ) );
VECTOR2D visibleGrid = helper.GetVisibleGrid();
BOOST_CHECK_EQUAL( visibleGrid.x, 25 );
BOOST_CHECK_EQUAL( visibleGrid.y, 35 );
}
BOOST_AUTO_TEST_CASE( SnapPointManagement )
{
GRID_HELPER helper;
// Initially should have no snapped point
auto snappedPoint = helper.GetSnappedPoint();
BOOST_CHECK( !snappedPoint.has_value() );
// After clearing anchors, still no snapped point
TEST_CLEAR_ANCHORS( helper );
snappedPoint = helper.GetSnappedPoint();
BOOST_CHECK( !snappedPoint.has_value() );
}
BOOST_AUTO_TEST_CASE( AlignGridWithNonPageOrigin )
{
// Issue #21800: Grid snapping should produce positions that are exact multiples
// of the grid size relative to the grid origin, regardless of display origin setting.
// When grid sizes go through VECTOR2D, floating-point imprecision in the
// VECTOR2D -> VECTOR2I truncation could produce incorrect grid positions.
GRID_HELPER helper;
helper.SetGridSnapping( true );
// PCB IU_PER_MM = 1000000 (nanometers)
constexpr int IU_PER_MM = 1000000;
struct TestCase
{
const char* name;
double gridSizeMM;
int gridOriginX;
int gridOriginY;
int pointX;
int pointY;
int expectedX;
int expectedY;
};
// Test various grid sizes, including ones that don't convert exactly from mm to nm
std::vector<TestCase> cases = {
// 1mm grid with non-zero origin
{ "1mm grid, origin at 47.3mm",
1.0, 47300000, 25300000,
127400000, 95400000,
0, 0 },
// 0.5mm grid
{ "0.5mm grid, origin at 47.3mm",
0.5, 47300000, 25300000,
127400000, 95400000,
0, 0 },
// 0.1mm grid (0.1 is NOT exact in IEEE 754 double)
{ "0.1mm grid, origin at 47.3mm",
0.1, 47300000, 25300000,
127340000, 95340000,
0, 0 },
// 25 mil = 0.635mm (0.635 is NOT exact in IEEE 754 double)
{ "25mil grid, origin at 47.625mm",
0.635, 47625000, 25400000,
127960000, 95250000,
0, 0 },
// 10 mil = 0.254mm (0.254 is NOT exact in IEEE 754 double)
{ "10mil grid, origin at 47.752mm",
0.254, 47752000, 25400000,
127960000, 95250000,
0, 0 },
// 50 mil = 1.27mm
{ "50mil grid, origin at 47.625mm",
1.27, 47625000, 25400000,
127960000, 95250000,
0, 0 },
};
// Compute all expected values using integer grid size (the ground truth)
for( auto& tc : cases )
{
int gridSizeIU = KiROUND( tc.gridSizeMM * IU_PER_MM );
tc.expectedX = KiROUND( double( tc.pointX - tc.gridOriginX ) / gridSizeIU )
* gridSizeIU + tc.gridOriginX;
tc.expectedY = KiROUND( double( tc.pointY - tc.gridOriginY ) / gridSizeIU )
* gridSizeIU + tc.gridOriginY;
}
for( const auto& tc : cases )
{
BOOST_TEST_CONTEXT( tc.name )
{
int gridSizeIU = KiROUND( tc.gridSizeMM * IU_PER_MM );
// Simulate the grid size coming through VECTOR2D (as it does from GAL)
VECTOR2D gridD( tc.gridSizeMM * IU_PER_MM, tc.gridSizeMM * IU_PER_MM );
VECTOR2D offsetD( tc.gridOriginX, tc.gridOriginY );
helper.SetGridSize( gridD );
helper.SetOrigin( VECTOR2I( tc.gridOriginX, tc.gridOriginY ) );
VECTOR2I point( tc.pointX, tc.pointY );
// Test AlignGrid with VECTOR2D parameters (the path that goes through
// implicit VECTOR2D -> VECTOR2I truncation in computeNearest)
VECTOR2I resultD = helper.AlignGrid( point, gridD, offsetD );
// Test AlignGrid with no parameters (uses GetGrid() which rounds properly)
VECTOR2I resultI = helper.AlignGrid( point );
BOOST_CHECK_EQUAL( resultD.x, tc.expectedX );
BOOST_CHECK_EQUAL( resultD.y, tc.expectedY );
BOOST_CHECK_EQUAL( resultI.x, tc.expectedX );
BOOST_CHECK_EQUAL( resultI.y, tc.expectedY );
// Verify that the result is on-grid
BOOST_CHECK_EQUAL( ( resultD.x - tc.gridOriginX ) % gridSizeIU, 0 );
BOOST_CHECK_EQUAL( ( resultD.y - tc.gridOriginY ) % gridSizeIU, 0 );
}
}
}
BOOST_AUTO_TEST_CASE( MovementFromOffGridAnchor )
{
// Issue #23308 / #21800: When moving a footprint by its anchor, the reference point
// should be the actual anchor (not grid-snapped). The final position must still land
// exactly on-grid because AlignGrid now rounds VECTOR2D grid parameters correctly.
GRID_HELPER helper;
helper.SetGridSnapping( true );
constexpr int IU_PER_MM = 1000000;
struct TestCase
{
const char* name;
double gridSizeMM;
int anchorX;
int anchorY;
int mouseTargetX;
int mouseTargetY;
};
std::vector<TestCase> cases = {
// Anchor at 0.05mm on a 0.1mm grid
{ "0.1mm grid, anchor at half-grid",
0.1, 50000, 50000, 300000, 300000 },
// Anchor at 0.127mm on a 0.254mm (10 mil) grid
{ "10mil grid, anchor at half-grid",
0.254, 127000, 127000, 762000, 508000 },
// Anchor at 0.3175mm on a 0.635mm (25 mil) grid
{ "25mil grid, anchor at half-grid",
0.635, 317500, 317500, 1905000, 1270000 },
};
for( const auto& tc : cases )
{
BOOST_TEST_CONTEXT( tc.name )
{
int gridSizeIU = KiROUND( tc.gridSizeMM * IU_PER_MM );
VECTOR2D gridD( tc.gridSizeMM * IU_PER_MM, tc.gridSizeMM * IU_PER_MM );
VECTOR2D offsetD( 0, 0 );
// Simulate the cursor snapping to grid at the target mouse position. This is
// what BestSnapAnchor does during the move loop.
VECTOR2I snappedTarget = helper.AlignGrid( VECTOR2I( tc.mouseTargetX, tc.mouseTargetY ),
gridD, offsetD );
// The movement delta starts from the actual anchor (off-grid), not a grid-snapped ref
VECTOR2I anchor( tc.anchorX, tc.anchorY );
VECTOR2I movement = snappedTarget - anchor;
VECTOR2I finalPos = anchor + movement;
// Final position must be exactly on-grid
BOOST_CHECK_EQUAL( finalPos.x % gridSizeIU, 0 );
BOOST_CHECK_EQUAL( finalPos.y % gridSizeIU, 0 );
// And it must equal the snapped target exactly
BOOST_CHECK_EQUAL( finalPos.x, snappedTarget.x );
BOOST_CHECK_EQUAL( finalPos.y, snappedTarget.y );
}
}
}
BOOST_AUTO_TEST_SUITE_END()