KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_grid_helper.cpp
Go to the documentation of this file.
1/*
2 * This program source code file is part of KiCad, a free EDA CAD application.
3 *
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; either version 3 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <https://www.gnu.org/licenses/>.
16 */
17
18#define BOOST_TEST_NO_MAIN
19#include <boost/test/unit_test.hpp>
20
21#include <tool/grid_helper.h>
22#include <tool/snap_frame.h>
23
25{
26 helper.clearAnchors();
27}
28
29
30namespace
31{
32class TEST_GRID_HELPER : public GRID_HELPER
33{
34public:
37
38 const std::vector<SEG>& DimensionBrackets() { return getSnapManager().GetViewItem().DimensionBrackets(); }
39
40 size_t AnchorCount() const { return m_anchors.size(); }
41
43 std::vector<VECTOR2I> AnchorPositions() const
44 {
45 std::vector<VECTOR2I> positions;
46
47 for( const ANCHOR& anchor : m_anchors )
48 positions.push_back( anchor.pos );
49
50 return positions;
51 }
52};
53} // namespace
54
55
56BOOST_AUTO_TEST_SUITE( GridHelperTest )
57
58BOOST_AUTO_TEST_CASE( DefaultConstructor )
59{
60 GRID_HELPER helper;
61
62 // Test default state
63 BOOST_CHECK( helper.GetSnap() );
64 BOOST_CHECK( helper.GetUseGrid() );
65
66 // Test that manual setters work
67 helper.SetGridSize( VECTOR2D( 100, 100 ) );
68 helper.SetOrigin( VECTOR2I( 50, 50 ) );
69 helper.SetGridSnapping( true );
70
71 VECTOR2I grid = helper.GetGrid();
72 BOOST_CHECK_EQUAL( grid.x, 100 );
73 BOOST_CHECK_EQUAL( grid.y, 100 );
74
75 VECTOR2I origin = helper.GetOrigin();
76 BOOST_CHECK_EQUAL( origin.x, 50 );
77 BOOST_CHECK_EQUAL( origin.y, 50 );
78}
79
80
81BOOST_AUTO_TEST_CASE( SnapResultGuideUpdateClearsStaleDimensionBrackets )
82{
83 TEST_GRID_HELPER helper;
84 SNAP_RESULT equalGap;
85 equalGap.guides = { { { 0, 0 }, { 5, 0 }, SNAP_GUIDE_STYLE::DIMENSION_BRACKET },
86 { { 10, 0 }, { 15, 0 }, SNAP_GUIDE_STYLE::DIMENSION_BRACKET } };
87
88 helper.applySnapResultGuides( equalGap );
89 BOOST_REQUIRE_EQUAL( helper.DimensionBrackets().size(), 2 );
90
91 SNAP_RESULT alignment;
92 alignment.guides = { { { 0, 0 }, { 0, 10 }, SNAP_GUIDE_STYLE::SNAP_LINE } };
93 helper.applySnapResultGuides( alignment );
94 BOOST_CHECK( helper.DimensionBrackets().empty() );
95}
96
97
98BOOST_AUTO_TEST_CASE( SnapFrameSelectsPayloadForAcceptedId )
99{
100 const SNAP_STABLE_ID rejectedId = MakePointSnapId( SNAP_ID_KIND::INTRINSIC_ANCHOR, { 10, 10 }, 1 );
101 const SNAP_STABLE_ID acceptedId = MakePointSnapId( SNAP_ID_KIND::INTRINSIC_ANCHOR, { 10, 10 }, 2 );
103 input.context.sourcePoint = { 9, 9 };
104 input.candidates.push_back( SNAP_CANDIDATE::Point( rejectedId, SNAP_PRIORITY_TIER::OBJECT,
105 SNAP_CANDIDATE_SUBTYPE::INTRINSIC_ANCHOR, { 10, 10 }, 0.1 ) );
106 input.candidates.push_back( SNAP_CANDIDATE::Point( acceptedId, SNAP_PRIORITY_TIER::OBJECT,
107 SNAP_CANDIDATE_SUBTYPE::INTRINSIC_ANCHOR, { 10, 10 }, 0.1 ) );
108 input.presentation.emplace( rejectedId, 1 );
109 input.presentation.emplace( acceptedId, 2 );
110 input.feasibility =
111 [rejectedId]( const SNAP_SOURCE_CONTEXT& aContext, const std::vector<SNAP_CANDIDATE>& aCandidates )
112 {
114 result.position = aContext.sourcePoint;
115
116 if( aCandidates.empty() )
117 return result;
118
119 if( aCandidates.back().id == rejectedId )
120 {
122 return result;
123 }
124
125 result.position = { 10, 10 };
126 result.remainingDof = 0;
127 return result;
128 };
129
130 SNAP_FRAME_OUTPUT<int> output = ResolveSnapFrame( std::move( input ) );
131
132 BOOST_REQUIRE_EQUAL( output.result.accepted.size(), 1 );
133 BOOST_CHECK( output.result.accepted.front() == acceptedId );
134 BOOST_REQUIRE( output.presentation );
135 BOOST_CHECK( output.presentation->id == acceptedId );
136 BOOST_CHECK_EQUAL( output.presentation->payload, 2 );
137}
138
139
141{
142 GRID_HELPER helper;
143 helper.SetGridSize( VECTOR2D( 100, 100 ) );
144 helper.SetOrigin( VECTOR2I( 0, 0 ) );
145 helper.SetGridSnapping( true );
146
147 // Test basic alignment - should round to nearest grid point
148 VECTOR2I aligned = helper.Align( VECTOR2I( 149, 251 ) );
149 BOOST_CHECK_EQUAL( aligned.x, 100 );
150 BOOST_CHECK_EQUAL( aligned.y, 300 );
151
152 // Test exact grid points
153 aligned = helper.Align( VECTOR2I( 200, 300 ) );
154 BOOST_CHECK_EQUAL( aligned.x, 200 );
155 BOOST_CHECK_EQUAL( aligned.y, 300 );
156
157 // Test negative coordinates
158 aligned = helper.Align( VECTOR2I( -149, -251 ) );
159 BOOST_CHECK_EQUAL( aligned.x, -100 );
160 BOOST_CHECK_EQUAL( aligned.y, -300 );
161}
162
163BOOST_AUTO_TEST_CASE( AlignGridWithCustomGrid )
164{
165 GRID_HELPER helper;
166 helper.SetGridSize( VECTOR2D( 50, 50 ) );
167 helper.SetOrigin( VECTOR2I( 0, 0 ) );
168 helper.SetGridSnapping( true );
169
170 VECTOR2I aligned = helper.AlignGrid( VECTOR2I( 26, 74 ) );
171 BOOST_CHECK_EQUAL( aligned.x, 50 );
172 BOOST_CHECK_EQUAL( aligned.y, 50 );
173
174 // Test AlignGrid with specific grid parameter
175 aligned = helper.AlignGrid( VECTOR2I( 26, 74 ), VECTOR2D( 25, 25 ), VECTOR2D( 0, 0 ) );
176 BOOST_CHECK_EQUAL( aligned.x, 25 );
177 BOOST_CHECK_EQUAL( aligned.y, 75 );
178}
179
180BOOST_AUTO_TEST_CASE( AlignWithOriginOffset )
181{
182 GRID_HELPER helper;
183 helper.SetGridSize( VECTOR2D( 100, 100 ) );
184 helper.SetOrigin( VECTOR2I( 25, 25 ) );
185 helper.SetGridSnapping( true );
186
187 // When grid has an origin offset, alignment should work from the new reference point
188 VECTOR2I aligned = helper.AlignGrid( VECTOR2I( 149, 251 ) );
189 BOOST_CHECK_EQUAL( aligned.x, 125 );
190 BOOST_CHECK_EQUAL( aligned.y, 225 );
191}
192
193BOOST_AUTO_TEST_CASE( AlignWithAuxiliaryAxes )
194{
195 GRID_HELPER helper;
196 helper.SetGridSize( VECTOR2D( 100, 100 ) );
197 helper.SetOrigin( VECTOR2I( 0, 0 ) );
198 helper.SetGridSnapping( true );
199
200 // Set auxiliary axis at (75, 75)
201 helper.SetAuxAxes( true, VECTOR2I( 75, 75 ) );
202
203 // Point closer to aux axis than grid should snap to aux axis
204 VECTOR2I aligned = helper.Align( VECTOR2I( 80, 80 ) );
205 BOOST_CHECK_EQUAL( aligned.x, 75 ); // Closer to aux axis X
206 BOOST_CHECK_EQUAL( aligned.y, 75 ); // Closer to aux axis Y
207
208 // Point closer to grid than aux axis should snap to grid
209 aligned = helper.Align( VECTOR2I( 95, 95 ) );
210 BOOST_CHECK_EQUAL( aligned.x, 100 ); // Closer to grid
211 BOOST_CHECK_EQUAL( aligned.y, 100 ); // Closer to grid
212
213 // Disable aux axes
214 helper.SetAuxAxes( false );
215 aligned = helper.Align( VECTOR2I( 80, 80 ) );
216 BOOST_CHECK_EQUAL( aligned.x, 100 ); // Should snap to grid only
217 BOOST_CHECK_EQUAL( aligned.y, 100 );
218}
219
220BOOST_AUTO_TEST_CASE( GridSnappingDisabled )
221{
222 GRID_HELPER helper;
223 helper.SetGridSize( VECTOR2D( 100, 100 ) );
224 helper.SetOrigin( VECTOR2I( 0, 0 ) );
225 helper.SetGridSnapping( false ); // Disable grid snapping
226
227 // When grid snapping is disabled, Align should return original point
228 VECTOR2I original( 149, 251 );
229 VECTOR2I aligned = helper.Align( original );
230 BOOST_CHECK_EQUAL( aligned.x, original.x );
231 BOOST_CHECK_EQUAL( aligned.y, original.y );
232
233 // AlignGrid should still work regardless of grid snapping setting
234 aligned = helper.AlignGrid( original );
235 BOOST_CHECK_EQUAL( aligned.x, 100 );
236 BOOST_CHECK_EQUAL( aligned.y, 300 );
237}
238
239BOOST_AUTO_TEST_CASE( UseGridDisabled )
240{
241 GRID_HELPER helper;
242 helper.SetGridSize( VECTOR2D( 100, 100 ) );
243 helper.SetOrigin( VECTOR2I( 0, 0 ) );
244 helper.SetGridSnapping( true );
245 helper.SetUseGrid( false ); // Disable grid usage
246
247 // When grid usage is disabled, Align should return original point
248 VECTOR2I original( 149, 251 );
249 VECTOR2I aligned = helper.Align( original );
250 BOOST_CHECK_EQUAL( aligned.x, original.x );
251 BOOST_CHECK_EQUAL( aligned.y, original.y );
252}
253
254BOOST_AUTO_TEST_CASE( AsymmetricGrid )
255{
256 GRID_HELPER helper;
257 helper.SetGridSize( VECTOR2D( 25, 75 ) ); // Different X and Y grid sizes
258 helper.SetOrigin( VECTOR2I( 0, 0 ) );
259 helper.SetGridSnapping( true );
260
261 VECTOR2I aligned = helper.Align( VECTOR2I( 30, 100 ) );
262 BOOST_CHECK_EQUAL( aligned.x, 25 ); // Nearest 25-unit boundary
263 BOOST_CHECK_EQUAL( aligned.y, 75 ); // Nearest 75-unit boundary
264
265 aligned = helper.Align( VECTOR2I( 40, 120 ) );
266 BOOST_CHECK_EQUAL( aligned.x, 50 ); // Next 25-unit boundary
267 BOOST_CHECK_EQUAL( aligned.y, 150 ); // Next 75-unit boundary
268}
269
271{
272 GRID_HELPER helper;
273
274 // Test snap flag getters/setters
275 BOOST_CHECK( helper.GetSnap() ); // Default should be true
276
277 helper.SetSnap( false );
278 BOOST_CHECK( !helper.GetSnap() );
279
280 helper.SetSnap( true );
281 BOOST_CHECK( helper.GetSnap() );
282
283 // Test grid usage flag
284 BOOST_CHECK( helper.GetUseGrid() ); // Default should be true
285
286 helper.SetUseGrid( false );
287 BOOST_CHECK( !helper.GetUseGrid() );
288
289 helper.SetUseGrid( true );
290 BOOST_CHECK( helper.GetUseGrid() );
291}
292
293BOOST_AUTO_TEST_CASE( MaskOperations )
294{
295 TEST_GRID_HELPER helper;
296
297 // An anchor is admitted only when the mask allows every one of its flags
298 helper.SetMask( GRID_HELPER::CORNER | GRID_HELPER::OUTLINE );
299
300 helper.addAnchor( VECTOR2I( 10, 10 ), GRID_HELPER::CORNER, nullptr );
301 BOOST_CHECK_EQUAL( helper.AnchorCount(), 1 );
302
303 helper.addAnchor( VECTOR2I( 20, 20 ), GRID_HELPER::SNAPPABLE, nullptr );
304 BOOST_CHECK_EQUAL( helper.AnchorCount(), 1 );
305
306 // A partially allowed anchor is rejected as a whole
307 helper.addAnchor( VECTOR2I( 30, 30 ), GRID_HELPER::CORNER | GRID_HELPER::SNAPPABLE, nullptr );
308 BOOST_CHECK_EQUAL( helper.AnchorCount(), 1 );
309
310 helper.SetMask( GRID_HELPER::ALL );
311 helper.addAnchor( VECTOR2I( 40, 40 ), GRID_HELPER::SNAPPABLE, nullptr );
312 BOOST_REQUIRE_EQUAL( helper.AnchorCount(), 2 );
313
314 helper.ClearMaskFlag( GRID_HELPER::SNAPPABLE );
315 helper.addAnchor( VECTOR2I( 50, 50 ), GRID_HELPER::SNAPPABLE, nullptr );
316 BOOST_CHECK_EQUAL( helper.AnchorCount(), 2 );
317
318 const std::vector<VECTOR2I> admitted = helper.AnchorPositions();
319 BOOST_CHECK( admitted == std::vector<VECTOR2I>( { VECTOR2I( 10, 10 ), VECTOR2I( 40, 40 ) } ) );
320}
321
323{
324 GRID_HELPER helper;
325 helper.SetGridSize( VECTOR2D( 100, 100 ) );
326 helper.SetOrigin( VECTOR2I( 0, 0 ) );
327 helper.SetGridSnapping( true );
328 helper.ClearSkipPoint();
329
330 // A horizontal construction line snaps to the grid column nearest the cursor, held at the
331 // line's own height
332 const VECTOR2D grid( 100, 100 );
333 const VECTOR2I cursor( 720, 510 );
334
335 helper.SetSnapLineOrigin( VECTOR2I( 500, 500 ) );
336 helper.SetSnapLineDirections( { VECTOR2I( 1, 0 ) } );
337
338 const VECTOR2I nearestGrid = helper.AlignGrid( cursor );
339
340 std::optional<VECTOR2I> snap = helper.SnapToConstructionLines( cursor, nearestGrid, grid, 50.0 );
341
342 BOOST_REQUIRE( snap.has_value() );
343 BOOST_CHECK_EQUAL( snap->x, 700 );
344 BOOST_CHECK_EQUAL( snap->y, 500 );
345
346 helper.SetSkipPoint( *snap );
347 BOOST_CHECK( !helper.SnapToConstructionLines( cursor, nearestGrid, grid, 50.0 ).has_value() );
348
349 // A skip point elsewhere leaves the candidate alone
350 helper.SetSkipPoint( VECTOR2I( 800, 500 ) );
351 BOOST_CHECK( helper.SnapToConstructionLines( cursor, nearestGrid, grid, 50.0 ) == snap );
352
353 helper.ClearSkipPoint();
354 BOOST_CHECK( helper.SnapToConstructionLines( cursor, nearestGrid, grid, 50.0 ) == snap );
355}
356
357BOOST_AUTO_TEST_CASE( SkipPointDefaultsToUnreachable )
358{
359 GRID_HELPER helper;
360 helper.SetGridSize( VECTOR2D( 100, 100 ) );
361 helper.SetOrigin( VECTOR2I( 0, 0 ) );
362 helper.SetGridSnapping( true );
363
364 // No ClearSkipPoint() here: a helper nobody has configured must still snap to the origin
365 helper.SetSnapLineOrigin( VECTOR2I( 0, 0 ) );
366 helper.SetSnapLineDirections( { VECTOR2I( 1, 0 ) } );
367
368 const VECTOR2I cursor( 10, 5 );
369 std::optional<VECTOR2I> snap =
370 helper.SnapToConstructionLines( cursor, helper.AlignGrid( cursor ), VECTOR2D( 100, 100 ), 50.0 );
371
372 BOOST_REQUIRE( snap.has_value() );
373 BOOST_CHECK_EQUAL( snap->x, 0 );
374 BOOST_CHECK_EQUAL( snap->y, 0 );
375}
376
377BOOST_AUTO_TEST_CASE( GridTypeAlignment )
378{
379 GRID_HELPER helper;
380 helper.SetGridSize( VECTOR2D( 100, 100 ) );
381 helper.SetOrigin( VECTOR2I( 0, 0 ) );
382 helper.SetGridSnapping( true );
383
384 // Test alignment with specific grid type
385 VECTOR2I aligned = helper.Align( VECTOR2I( 149, 251 ), GRID_CURRENT );
386 BOOST_CHECK_EQUAL( aligned.x, 100 );
387 BOOST_CHECK_EQUAL( aligned.y, 300 );
388
389 aligned = helper.AlignGrid( VECTOR2I( 149, 251 ), GRID_CURRENT );
390 BOOST_CHECK_EQUAL( aligned.x, 100 );
391 BOOST_CHECK_EQUAL( aligned.y, 300 );
392}
393
395{
396 GRID_HELPER helper;
397 helper.SetGridSize( VECTOR2D( 1, 1 ) ); // Very small grid
398 helper.SetOrigin( VECTOR2I( 0, 0 ) );
399 helper.SetGridSnapping( true );
400
401 // Test with very small grid
402 VECTOR2I aligned = helper.Align( VECTOR2I( 5, 5 ) );
403 BOOST_CHECK_EQUAL( aligned.x, 5 );
404 BOOST_CHECK_EQUAL( aligned.y, 5 );
405
406 // Test with zero point
407 aligned = helper.Align( VECTOR2I( 0, 0 ) );
408 BOOST_CHECK_EQUAL( aligned.x, 0 );
409 BOOST_CHECK_EQUAL( aligned.y, 0 );
410
411 // Test with large grid
412 helper.SetGridSize( VECTOR2D( 10000, 10000 ) );
413 aligned = helper.Align( VECTOR2I( 3000, 7000 ) );
414 BOOST_CHECK_EQUAL( aligned.x, 0 ); // Closer to 0 than 10000
415 BOOST_CHECK_EQUAL( aligned.y, 10000 ); // Closer to 10000 than 0
416}
417
419{
420 GRID_HELPER helper;
421
422 // Test GetGridSize with different grid types
423 helper.SetGridSize( VECTOR2D( 50, 75 ) );
424
425 VECTOR2D gridSize = helper.GetGridSize( GRID_CURRENT );
426 BOOST_CHECK_EQUAL( gridSize.x, 50 );
427 BOOST_CHECK_EQUAL( gridSize.y, 75 );
428
429 // Other grid types should return the same in the base implementation
430 gridSize = helper.GetGridSize( GRID_CONNECTABLE );
431 BOOST_CHECK_EQUAL( gridSize.x, 50 );
432 BOOST_CHECK_EQUAL( gridSize.y, 75 );
433}
434
436{
437 GRID_HELPER helper;
438 helper.SetVisibleGridSize( VECTOR2D( 25, 35 ) );
439
440 VECTOR2D visibleGrid = helper.GetVisibleGrid();
441 BOOST_CHECK_EQUAL( visibleGrid.x, 25 );
442 BOOST_CHECK_EQUAL( visibleGrid.y, 35 );
443}
444
445BOOST_AUTO_TEST_CASE( SnapPointManagement )
446{
447 GRID_HELPER helper;
448
449 // Initially should have no snapped point
450 auto snappedPoint = helper.GetSnappedPoint();
451 BOOST_CHECK( !snappedPoint.has_value() );
452
453 // After clearing anchors, still no snapped point
454 TEST_CLEAR_ANCHORS( helper );
455 snappedPoint = helper.GetSnappedPoint();
456 BOOST_CHECK( !snappedPoint.has_value() );
457}
458
459
460BOOST_AUTO_TEST_CASE( AlignGridWithNonPageOrigin )
461{
462 // Issue #21800: Grid snapping should produce positions that are exact multiples
463 // of the grid size relative to the grid origin, regardless of display origin setting.
464 // When grid sizes go through VECTOR2D, floating-point imprecision in the
465 // VECTOR2D -> VECTOR2I truncation could produce incorrect grid positions.
466
467 GRID_HELPER helper;
468 helper.SetGridSnapping( true );
469
470 // PCB IU_PER_MM = 1000000 (nanometers)
471 constexpr int IU_PER_MM = 1000000;
472
473 struct TestCase
474 {
475 const char* name;
476 double gridSizeMM;
477 int gridOriginX;
478 int gridOriginY;
479 int pointX;
480 int pointY;
481 int expectedX;
482 int expectedY;
483 };
484
485 // Test various grid sizes, including ones that don't convert exactly from mm to nm
486 std::vector<TestCase> cases = {
487 // 1mm grid with non-zero origin
488 { "1mm grid, origin at 47.3mm",
489 1.0, 47300000, 25300000,
490 127400000, 95400000,
491 0, 0 },
492
493 // 0.5mm grid
494 { "0.5mm grid, origin at 47.3mm",
495 0.5, 47300000, 25300000,
496 127400000, 95400000,
497 0, 0 },
498
499 // 0.1mm grid (0.1 is NOT exact in IEEE 754 double)
500 { "0.1mm grid, origin at 47.3mm",
501 0.1, 47300000, 25300000,
502 127340000, 95340000,
503 0, 0 },
504
505 // 25 mil = 0.635mm (0.635 is NOT exact in IEEE 754 double)
506 { "25mil grid, origin at 47.625mm",
507 0.635, 47625000, 25400000,
508 127960000, 95250000,
509 0, 0 },
510
511 // 10 mil = 0.254mm (0.254 is NOT exact in IEEE 754 double)
512 { "10mil grid, origin at 47.752mm",
513 0.254, 47752000, 25400000,
514 127960000, 95250000,
515 0, 0 },
516
517 // 50 mil = 1.27mm
518 { "50mil grid, origin at 47.625mm",
519 1.27, 47625000, 25400000,
520 127960000, 95250000,
521 0, 0 },
522 };
523
524 // Compute all expected values using integer grid size (the ground truth)
525 for( auto& tc : cases )
526 {
527 int gridSizeIU = KiROUND( tc.gridSizeMM * IU_PER_MM );
528
529 tc.expectedX = KiROUND( double( tc.pointX - tc.gridOriginX ) / gridSizeIU )
530 * gridSizeIU + tc.gridOriginX;
531 tc.expectedY = KiROUND( double( tc.pointY - tc.gridOriginY ) / gridSizeIU )
532 * gridSizeIU + tc.gridOriginY;
533 }
534
535 for( const auto& tc : cases )
536 {
537 BOOST_TEST_CONTEXT( tc.name )
538 {
539 int gridSizeIU = KiROUND( tc.gridSizeMM * IU_PER_MM );
540
541 // Simulate the grid size coming through VECTOR2D (as it does from GAL)
542 VECTOR2D gridD( tc.gridSizeMM * IU_PER_MM, tc.gridSizeMM * IU_PER_MM );
543 VECTOR2D offsetD( tc.gridOriginX, tc.gridOriginY );
544
545 helper.SetGridSize( gridD );
546 helper.SetOrigin( VECTOR2I( tc.gridOriginX, tc.gridOriginY ) );
547
548 VECTOR2I point( tc.pointX, tc.pointY );
549
550 // Test AlignGrid with VECTOR2D parameters (the path that goes through
551 // implicit VECTOR2D -> VECTOR2I truncation in computeNearest)
552 VECTOR2I resultD = helper.AlignGrid( point, gridD, offsetD );
553
554 // Test AlignGrid with no parameters (uses GetGrid() which rounds properly)
555 VECTOR2I resultI = helper.AlignGrid( point );
556
557 BOOST_CHECK_EQUAL( resultD.x, tc.expectedX );
558 BOOST_CHECK_EQUAL( resultD.y, tc.expectedY );
559 BOOST_CHECK_EQUAL( resultI.x, tc.expectedX );
560 BOOST_CHECK_EQUAL( resultI.y, tc.expectedY );
561
562 // Verify that the result is on-grid
563 BOOST_CHECK_EQUAL( ( resultD.x - tc.gridOriginX ) % gridSizeIU, 0 );
564 BOOST_CHECK_EQUAL( ( resultD.y - tc.gridOriginY ) % gridSizeIU, 0 );
565 }
566 }
567}
568
569
570BOOST_AUTO_TEST_CASE( MovementFromOffGridAnchor )
571{
572 // Issue #23308 / #21800: When moving a footprint by its anchor, the reference point
573 // should be the actual anchor (not grid-snapped). The final position must still land
574 // exactly on-grid because AlignGrid now rounds VECTOR2D grid parameters correctly.
575
576 GRID_HELPER helper;
577 helper.SetGridSnapping( true );
578
579 constexpr int IU_PER_MM = 1000000;
580
581 struct TestCase
582 {
583 const char* name;
584 double gridSizeMM;
585 int anchorX;
586 int anchorY;
587 int mouseTargetX;
588 int mouseTargetY;
589 };
590
591 std::vector<TestCase> cases = {
592 // Anchor at 0.05mm on a 0.1mm grid
593 { "0.1mm grid, anchor at half-grid",
594 0.1, 50000, 50000, 300000, 300000 },
595
596 // Anchor at 0.127mm on a 0.254mm (10 mil) grid
597 { "10mil grid, anchor at half-grid",
598 0.254, 127000, 127000, 762000, 508000 },
599
600 // Anchor at 0.3175mm on a 0.635mm (25 mil) grid
601 { "25mil grid, anchor at half-grid",
602 0.635, 317500, 317500, 1905000, 1270000 },
603 };
604
605 for( const auto& tc : cases )
606 {
607 BOOST_TEST_CONTEXT( tc.name )
608 {
609 int gridSizeIU = KiROUND( tc.gridSizeMM * IU_PER_MM );
610
611 VECTOR2D gridD( tc.gridSizeMM * IU_PER_MM, tc.gridSizeMM * IU_PER_MM );
612 VECTOR2D offsetD( 0, 0 );
613
614 // Simulate the cursor snapping to grid at the target mouse position. This is
615 // what ResolveSnap does during the move loop.
616 VECTOR2I snappedTarget = helper.AlignGrid( VECTOR2I( tc.mouseTargetX, tc.mouseTargetY ), gridD, offsetD );
617
618 // The movement delta starts from the actual anchor (off-grid), not a grid-snapped ref
619 VECTOR2I anchor( tc.anchorX, tc.anchorY );
620 VECTOR2I movement = snappedTarget - anchor;
621 VECTOR2I finalPos = anchor + movement;
622
623 // Final position must be exactly on-grid
624 BOOST_CHECK_EQUAL( finalPos.x % gridSizeIU, 0 );
625 BOOST_CHECK_EQUAL( finalPos.y % gridSizeIU, 0 );
626
627 // And it must equal the snapped target exactly
628 BOOST_CHECK_EQUAL( finalPos.x, snappedTarget.x );
629 BOOST_CHECK_EQUAL( finalPos.y, snappedTarget.y );
630 }
631 }
632}
633
634
const char * name
constexpr BOX2I KiROUND(const BOX2D &aBoxD)
Definition box2.h:986
KIGFX::CONSTRUCTION_GEOM & GetViewItem()
std::optional< VECTOR2I > SnapToConstructionLines(const VECTOR2I &aPoint, const VECTOR2I &aNearestGrid, const VECTOR2D &aGrid, double aSnapRange) const
void SetSnapLineDirections(const std::vector< VECTOR2I > &aDirections)
void addAnchor(const VECTOR2I &aPos, int aFlags, EDA_ITEM *aItem, int aPointTypes=POINT_TYPE::PT_NONE)
bool GetSnap() const
void SetSnap(bool aSnap)
void SetSkipPoint(const VECTOR2I &aPoint)
bool GetUseGrid() const
void SetOrigin(const VECTOR2I &aOrigin)
Definition grid_helper.h:96
void SetSnapLineOrigin(const VECTOR2I &aOrigin)
void SetVisibleGridSize(const VECTOR2D &aGrid)
Definition grid_helper.h:95
void ClearSkipPoint()
Clear the skip point by setting it to an unreachable position, thereby preventing matching.
void SetGridSnapping(bool aEnable)
Definition grid_helper.h:97
void SetUseGrid(bool aSnapToGrid)
std::optional< VECTOR2I > GetSnappedPoint() const
void SetAuxAxes(bool aEnable, const VECTOR2I &aOrigin=VECTOR2I(0, 0))
VECTOR2D GetVisibleGrid() const
virtual VECTOR2D GetGridSize(GRID_HELPER_GRIDS aGrid) const
Return the size of the specified grid.
VECTOR2I GetGrid() const
VECTOR2I GetOrigin() const
void SetGridSize(const VECTOR2D &aGrid)
Definition grid_helper.h:94
void clearAnchors()
virtual VECTOR2I Align(const VECTOR2I &aPoint, GRID_HELPER_GRIDS aGrid) const
void applySnapResultGuides(const SNAP_RESULT &aResult)
std::vector< ANCHOR > m_anchors
virtual VECTOR2I AlignGrid(const VECTOR2I &aPoint, GRID_HELPER_GRIDS aGrid) const
const std::vector< SEG > & DimensionBrackets() const
@ GRID_CURRENT
Definition grid_helper.h:57
@ GRID_CONNECTABLE
Definition grid_helper.h:59
SNAP_FRAME_OUTPUT< Payload > ResolveSnapFrame(SNAP_FRAME_INPUT< Payload > aInput)
Definition snap_frame.h:60
SNAP_STABLE_ID MakePointSnapId(SNAP_ID_KIND aKind, const VECTOR2I &aPoint, int aFeatureIndex=0)
static SNAP_CANDIDATE Point(SNAP_STABLE_ID aId, SNAP_PRIORITY_TIER aPriority, SNAP_CANDIDATE_SUBTYPE aSubtype, const VECTOR2I &aPoint, double aResidual)
SNAP_SOURCE_CONTEXT context
Definition snap_frame.h:32
std::optional< SNAP_FRAME_PRESENTATION< Payload > > presentation
Definition snap_frame.h:55
SNAP_RESULT result
Definition snap_frame.h:54
std::vector< SNAP_GUIDE > guides
std::vector< SNAP_STABLE_ID > accepted
BOOST_AUTO_TEST_CASE(HorizontalAlignment)
BOOST_AUTO_TEST_SUITE(CadstarPartParser)
void TEST_CLEAR_ANCHORS(GRID_HELPER &helper)
BOOST_AUTO_TEST_CASE(DefaultConstructor)
BOOST_REQUIRE(intersection.has_value()==c.ExpectedIntersection.has_value())
BOOST_AUTO_TEST_SUITE_END()
BOOST_TEST_CONTEXT("Test Clearance")
static constexpr double IU_PER_MM
Mock up a conversion function.
wxString result
Test unit parsing edge cases and error handling.
BOOST_CHECK_EQUAL(result, "25.4")
VECTOR2< int32_t > VECTOR2I
Definition vector2d.h:683
VECTOR2< double > VECTOR2D
Definition vector2d.h:682