diff --git a/common/tool/grid_helper.cpp b/common/tool/grid_helper.cpp index 34f4446945..fbcde408b7 100644 --- a/common/tool/grid_helper.cpp +++ b/common/tool/grid_helper.cpp @@ -434,7 +434,11 @@ VECTOR2I GRID_HELPER::AlignGrid( const VECTOR2I& aPoint ) const VECTOR2I GRID_HELPER::AlignGrid( const VECTOR2I& aPoint, const VECTOR2D& aGrid, const VECTOR2D& aOffset ) const { - return computeNearest( aPoint, aGrid, aOffset ); + // Round the grid size and offset rather than relying on the implicit VECTOR2D->VECTOR2I + // truncation in computeNearest. Grid sizes that aren't exact in IEEE 754 (e.g., 0.254mm = + // 10 mil) would otherwise truncate to the wrong integer (253999 instead of 254000), + // producing positions that aren't true grid multiples. + return computeNearest( aPoint, KiROUND( aGrid ), KiROUND( aOffset ) ); } diff --git a/pcbnew/tools/edit_tool_move_fct.cpp b/pcbnew/tools/edit_tool_move_fct.cpp index 7a1a0b07da..2c8af2ab03 100644 --- a/pcbnew/tools/edit_tool_move_fct.cpp +++ b/pcbnew/tools/edit_tool_move_fct.cpp @@ -1291,30 +1291,19 @@ bool EDIT_TOOL::doMoveSelection( const TOOL_EVENT& aEvent, BOARD_COMMIT* aCommit } else { - // Get the best drag origin (nearest item anchor to where the user clicked) VECTOR2I dragOrigin = m_cursor; - // Grid-align the reference point so that movement deltas between - // grid-snapped cursor positions remain on-grid. Without this, dragging - // from a non-grid-aligned anchor (e.g. a pad center that doesn't fall on - // the current grid) produces fractional-nanometer position errors that - // become visible when Display Origin is set to Grid Origin or Aux Origin. - VECTOR2I snappedRef = grid.AlignGrid( dragOrigin, - grid.GetSelectionGrid( selection ) ); - selection.SetReferencePoint( snappedRef ); + selection.SetReferencePoint( dragOrigin ); - // Set up construction/snap lines at the actual item position for visual - // alignment, not the snapped position if( angleSnapMode != LEADER_MODE::DIRECT ) grid.SetSnapLineOrigin( dragOrigin ); grid.SetAuxAxes( true, dragOrigin ); - // Initialize m_cursor to the grid-aligned reference so that the first - // movement delta (m_cursor - prevPos) is grid-aligned. Without this, - // prevPos would be an unsnapped position and the first move would put - // items off-grid. - m_cursor = snappedRef; + if( !editFrame->GetMoveWarpsCursor() ) + m_cursor = originalCursorPos; + else + m_cursor = dragOrigin; } originalPos = selection.GetReferencePoint(); diff --git a/qa/tests/common/test_grid_helper.cpp b/qa/tests/common/test_grid_helper.cpp index 61df8d3eff..8446cd08d1 100644 --- a/qa/tests/common/test_grid_helper.cpp +++ b/qa/tests/common/test_grid_helper.cpp @@ -419,4 +419,70 @@ BOOST_AUTO_TEST_CASE( AlignGridWithNonPageOrigin ) } +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 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() \ No newline at end of file