KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_snap_resolver.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 * Copyright The KiCad Developers, see AUTHORS.txt for contributors.
5 *
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU General Public License
8 * as published by the Free Software Foundation; either version 3
9 * of the License, or (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program. If not, see <https://www.gnu.org/licenses/>.
18 */
19
20#include <boost/test/unit_test.hpp>
21
22#include <kiid.h>
23#include <snap/snap_resolver.h>
24
25#include <algorithm>
26#include <chrono>
27#include <iomanip>
28#include <sstream>
29
30
31namespace
32{
33SNAP_TARGET_ID targetId( const char* aName )
34{
35 return KIID::FromName( aName ).AsBytes();
36}
37
38
39SNAP_STABLE_ID id( const char* aFeature )
40{
41 return { SNAP_ID_KIND::ITEM_GEOMETRY, targetId( aFeature ), 0, 0 };
42}
43
44
45std::string targetBytes( const SNAP_STABLE_ID& aId )
46{
47 std::ostringstream stream;
48
49 for( uint8_t byte : aId.target )
50 stream << std::hex << std::setfill( '0' ) << std::setw( 2 ) << static_cast<int>( byte );
51
52 return stream.str();
53}
54
55
56std::string resultSignature( const SNAP_RESULT& aResult )
57{
58 std::ostringstream stream;
59 stream << static_cast<int>( aResult.status ) << ':' << aResult.position.x << ',' << aResult.position.y << ':'
60 << aResult.remainingDof;
61
62 for( const SNAP_STABLE_ID& accepted : aResult.accepted )
63 {
64 stream << ':' << static_cast<int>( accepted.kind ) << ',' << targetBytes( accepted ) << ','
65 << accepted.featureIndex << ',' << accepted.solutionBranch;
66 }
67
68 stream << std::hexfloat;
69
70 for( double residual : aResult.quantizedResiduals )
71 stream << ':' << residual;
72
73 for( const SNAP_GUIDE& guide : aResult.guides )
74 {
75 stream << ':' << guide.start.x << ',' << guide.start.y << ',' << guide.end.x << ',' << guide.end.y << ','
76 << static_cast<int>( guide.style );
77 }
78
79 return stream.str();
80}
81} // namespace
82
83
84BOOST_AUTO_TEST_SUITE( SnapResolver )
85
86
87BOOST_AUTO_TEST_CASE( StableIdentityUsesTypedKindAndTarget )
88{
89 const SNAP_TARGET_ID target = targetId( "target" );
90 const SNAP_STABLE_ID first{ SNAP_ID_KIND::ITEM_GEOMETRY, target, 2, 1 };
91 const SNAP_STABLE_ID same{ SNAP_ID_KIND::ITEM_GEOMETRY, target, 2, 1 };
92 const SNAP_STABLE_ID differentKind{ SNAP_ID_KIND::INTRINSIC_ANCHOR, target, 2, 1 };
93
94 BOOST_CHECK( first == same );
95 BOOST_CHECK( first != differentKind );
96 BOOST_CHECK( SNAP_STABLE_ID{}.target == SNAP_TARGET_ID{} );
97 BOOST_CHECK( KIID::FromBytes( target ) == KIID::FromName( "target" ) );
98}
99
100
101BOOST_AUTO_TEST_CASE( PointIdentityPreservesCoordinateOrder )
102{
103 BOOST_CHECK( MakePointSnapId( SNAP_ID_KIND::CONSTRUCTION, { 1, 2 } )
105}
106
107
108BOOST_AUTO_TEST_CASE( IntersectionIdentityIsSourceOrderIndependent )
109{
110 const SNAP_STABLE_ID first{ SNAP_ID_KIND::ITEM_GEOMETRY, KIID( "00000000-0000-0000-0000-000000000001" ).AsBytes(),
111 1, 2 };
112 const SNAP_STABLE_ID second{ SNAP_ID_KIND::ITEM_GEOMETRY, KIID( "00000000-0000-0000-0000-000000000002" ).AsBytes(),
113 3, 4 };
114
115 BOOST_CHECK( MakeIntersectionSnapId( first, second, 5 ) == MakeIntersectionSnapId( second, first, 5 ) );
116}
117
118
119BOOST_AUTO_TEST_CASE( CompositeIdentityIsOrderIndependentWithoutXorCancellation )
120{
121 const SNAP_TARGET_ID first = KIID( "00000000-0000-0000-0000-000000000001" ).AsBytes();
122 const SNAP_TARGET_ID second = KIID( "00000000-0000-0000-0000-000000000002" ).AsBytes();
123
124 BOOST_CHECK( MakeCompositeSnapId( SNAP_ID_KIND::INTRINSIC_ANCHOR, { first, second }, 3 )
125 == MakeCompositeSnapId( SNAP_ID_KIND::INTRINSIC_ANCHOR, { second, first }, 3 ) );
126 BOOST_CHECK( MakeCompositeSnapId( SNAP_ID_KIND::INTRINSIC_ANCHOR, { first, first }, 3 ).target
127 != SNAP_TARGET_ID{} );
128
129 // The kind and target count are hashed ahead of the targets and must not be lost to them
130 BOOST_CHECK( MakeCompositeSnapId( SNAP_ID_KIND::INTRINSIC_ANCHOR, { first, second }, 3 ).target
131 != MakeCompositeSnapId( SNAP_ID_KIND::CONSTRUCTED_ANCHOR, { first, second }, 3 ).target );
132}
133
134
135BOOST_AUTO_TEST_CASE( IdentityConstructionHasStableGoldenBytes )
136{
137 const SNAP_STABLE_ID first{ SNAP_ID_KIND::ITEM_GEOMETRY, KIID( "00000000-0000-0000-0000-000000000001" ).AsBytes(),
138 1, 2 };
140 KIID( "00000000-0000-0000-0000-000000000002" ).AsBytes(), 3, 4 };
141
142 BOOST_CHECK_EQUAL( targetBytes( MakePointSnapId( SNAP_ID_KIND::CONSTRUCTION, { 123, -456 }, 7 ) ),
143 "b1fa72f98d0eaaf396ce1f1030e5f274" );
144 BOOST_CHECK_EQUAL( targetBytes( MakeDerivedSnapId( SNAP_ID_KIND::BOUNDS_X, first, 8, 9 ) ),
145 "38ec5cc638db882f25564f708cd908d4" );
146 BOOST_CHECK_EQUAL( targetBytes( MakeIntersectionSnapId( first, second, 5 ) ), "b260a931293228a311feb6c704737605" );
148 { KIID( "00000000-0000-0000-0000-000000000002" ).AsBytes(),
149 KIID( "00000000-0000-0000-0000-000000000001" ).AsBytes() },
150 3 ) ),
151 "8f02794a0e8da19751d9debf2e2eeeee" );
152}
153
154
155BOOST_AUTO_TEST_CASE( AuthoredRelationWinsEveryTransientTier )
156{
157 SNAP_SOURCE_CONTEXT context;
158 context.sourcePoint = { 18, 19 };
159
160 SNAP_RESOLVER resolver;
162 SNAP_CANDIDATE_SUBTYPE::CURSOR, { 18, 19 }, 0.0 ) );
164 SNAP_CANDIDATE_SUBTYPE::FINITE_MANIFOLD, { 20, 20 }, 1.0 ) );
166 SNAP_CANDIDATE_SUBTYPE::INTRINSIC_ANCHOR, { 10, 10 }, 20.0 ) );
167
168 SNAP_RESULT result = resolver.Resolve( context );
169
170 BOOST_REQUIRE_EQUAL( result.status, SNAP_RESULT_STATUS::SUCCESS );
171 BOOST_CHECK_EQUAL( result.position, VECTOR2I( 10, 10 ) );
172 BOOST_REQUIRE_EQUAL( result.accepted.size(), 1 );
173 BOOST_CHECK( result.accepted.front() == id( "authored" ) );
174}
175
176
177BOOST_AUTO_TEST_CASE( AngleRejectsIncompatibleObjectWithoutRelaxing )
178{
179 SNAP_SOURCE_CONTEXT context;
180 context.sourcePoint = { 12, 4 };
181
182 SNAP_RESOLVER resolver;
184 SNAP_CANDIDATE_SUBTYPE::ANGLE_BRANCH, { 0, 0 }, { 1.0, 0.0 }, 4.0 ) );
189
190 SNAP_RESULT result = resolver.Resolve( context );
191
192 BOOST_REQUIRE_EQUAL( result.status, SNAP_RESULT_STATUS::SUCCESS );
193 BOOST_CHECK_EQUAL( result.position, VECTOR2I( 10, 0 ) );
194 BOOST_CHECK( result.Accepted( id( "angle" ) ) );
195 BOOST_CHECK( !result.Accepted( id( "object" ) ) );
196 BOOST_CHECK( result.Accepted( id( "grid-x" ) ) );
197}
198
199
200BOOST_AUTO_TEST_CASE( CompatibleObjectAxesCompose )
201{
202 SNAP_SOURCE_CONTEXT context;
203 context.sourcePoint = { 14, 17 };
204
205 SNAP_RESOLVER resolver;
210
211 SNAP_RESULT result = resolver.Resolve( context );
212
213 BOOST_REQUIRE_EQUAL( result.status, SNAP_RESULT_STATUS::SUCCESS );
214 BOOST_CHECK_EQUAL( result.position, VECTOR2I( 10, 20 ) );
215 BOOST_CHECK( result.Accepted( id( "align-x" ) ) );
216 BOOST_CHECK( result.Accepted( id( "align-y" ) ) );
217 BOOST_CHECK_EQUAL( result.remainingDof, 0 );
218}
219
220
221BOOST_AUTO_TEST_CASE( ConflictingGridAxisLeavesCompatibleAxisAccepted )
222{
223 SNAP_SOURCE_CONTEXT context;
224 context.sourcePoint = { 9, 13 };
225
226 SNAP_RESOLVER resolver;
228 SNAP_CANDIDATE_SUBTYPE::ANGLE_BRANCH, { 0, 0 }, { 1.0, 1.0 }, 3.0 ) );
233
234 SNAP_RESULT result = resolver.Resolve( context );
235
236 BOOST_REQUIRE_EQUAL( result.status, SNAP_RESULT_STATUS::SUCCESS );
237 BOOST_CHECK_EQUAL( result.position, VECTOR2I( 10, 10 ) );
238 BOOST_CHECK( result.Accepted( id( "grid-x" ) ) );
239 BOOST_CHECK( !result.Accepted( id( "grid-y" ) ) );
240}
241
242
243BOOST_AUTO_TEST_CASE( NearParallelIntersectionOutsideCoordinateRangeIsInfeasible )
244{
245 // Real-world capture: two nearly-collinear track segments pass the rank test but
246 // intersect far outside the int coordinate space, firing the KiROUND assert.
247 SNAP_SOURCE_CONTEXT context;
248 context.sourcePoint = { 142936256, 71674527 };
249
250 SNAP_RESOLVER resolver;
252 SNAP_CANDIDATE_SUBTYPE::FINITE_MANIFOLD, { 142470148, 74198721 },
253 { 1006370.0, -3755829.0 }, 0.25851349837571413 ) );
255 SNAP_CANDIDATE_SUBTYPE::FINITE_MANIFOLD, { 141987238, 74262298 },
256 { 1006358.0, -3755779.0 }, 0.31431204619285724 ) );
257
258 SNAP_RESULT result = resolver.Resolve( context );
259
260 BOOST_REQUIRE_EQUAL( result.status, SNAP_RESULT_STATUS::SUCCESS );
261 BOOST_CHECK_EQUAL( result.position, VECTOR2I( 143132419, 71727089 ) );
262 BOOST_CHECK( result.Accepted( id( "segment-a" ) ) );
263 BOOST_CHECK( !result.Accepted( id( "segment-b" ) ) );
264 BOOST_CHECK_EQUAL( result.remainingDof, 1 );
265}
266
267
268BOOST_AUTO_TEST_CASE( NearParallelIntersectionInsideCoordinateRangeComposes )
269{
270 // Artificial pair whose intersection is nonsensically far away but still representable.
271 SNAP_SOURCE_CONTEXT context;
272 context.sourcePoint = { 10, 0 };
273
274 SNAP_RESOLVER resolver;
276 SNAP_CANDIDATE_SUBTYPE::FINITE_MANIFOLD, { 0, 0 }, { 1.0, 0.0 },
277 0.0 ) );
279 SNAP_CANDIDATE_SUBTYPE::FINITE_MANIFOLD, { 0, 1000 }, { 1.0, 0.001 },
280 5.0 ) );
281
282 SNAP_RESULT result = resolver.Resolve( context );
283
284 BOOST_REQUIRE_EQUAL( result.status, SNAP_RESULT_STATUS::SUCCESS );
285 BOOST_CHECK_EQUAL( result.position, VECTOR2I( -1000000, 0 ) );
286 BOOST_CHECK( result.Accepted( id( "segment-a" ) ) );
287 BOOST_CHECK( result.Accepted( id( "segment-b" ) ) );
288 BOOST_CHECK_EQUAL( result.remainingDof, 0 );
289}
290
291
292BOOST_AUTO_TEST_CASE( StableIdentityBreaksExactRankTie )
293{
294 SNAP_SOURCE_CONTEXT context;
295 context.sourcePoint = { 0, 0 };
296
297 SNAP_RESOLVER resolver;
302
303 SNAP_RESULT result = resolver.Resolve( context );
304
305 BOOST_REQUIRE_EQUAL( result.accepted.size(), 1 );
306 BOOST_CHECK( result.accepted.front() == std::min( id( "a" ), id( "z" ) ) );
307}
308
309
310BOOST_AUTO_TEST_CASE( FixedCursorPathIsIndependentOfCandidateStorageOrder )
311{
312 const std::vector<VECTOR2I> cursorPath = { { 0, 0 }, { 3, 5 }, { 7, 9 }, { 11, 13 } };
313
314 const auto resolvePath = [&]( bool aReverse )
315 {
316 std::vector<std::string> signatures;
317
318 for( const VECTOR2I& cursor : cursorPath )
319 {
320 std::vector<SNAP_CANDIDATE> candidates = {
322 cursor.x + 2, 0.2 ),
324 cursor.x + 1, 0.2 ),
326 cursor.y - 1, 0.1 )
327 };
328
329 if( aReverse )
330 std::reverse( candidates.begin(), candidates.end() );
331
332 SNAP_SOURCE_CONTEXT context;
333 context.sourcePoint = cursor;
334 SNAP_RESOLVER resolver;
335
336 for( SNAP_CANDIDATE& candidate : candidates )
337 resolver.AddCandidate( std::move( candidate ) );
338
339 signatures.push_back( resultSignature( resolver.Resolve( context ) ) );
340 }
341
342 return signatures;
343 };
344
345 BOOST_CHECK( resolvePath( false ) == resolvePath( true ) );
346}
347
348
349BOOST_AUTO_TEST_CASE( ReferenceAffinityOutranksDistance )
350{
351 SNAP_SOURCE_CONTEXT context;
352 context.sourcePoint = { 0, 0 };
353
356 preferred.referenceAffinity = 0;
357
360 fallback.referenceAffinity = 1;
361
362 SNAP_RESOLVER resolver;
363 resolver.AddCandidate( std::move( fallback ) );
364 resolver.AddCandidate( std::move( preferred ) );
365
366 SNAP_RESULT result = resolver.Resolve( context );
367
368 BOOST_CHECK( result.Accepted( id( "preferred" ) ) );
369 BOOST_CHECK( !result.Accepted( id( "fallback" ) ) );
370}
371
372
373BOOST_AUTO_TEST_CASE( InfeasiblePreferredReferenceFallsBack )
374{
375 SNAP_SOURCE_CONTEXT context;
376 context.sourcePoint = { 0, 0 };
377
380 preferred.referenceAffinity = 0;
381
384 fallback.referenceAffinity = 1;
385
386 SNAP_RESOLVER resolver;
387 resolver.AddCandidate( std::move( fallback ) );
388 resolver.AddCandidate( std::move( preferred ) );
389 resolver.SetFeasibilityCallback(
390 [&]( const SNAP_SOURCE_CONTEXT&, const std::vector<SNAP_CANDIDATE>& aTrial )
391 {
393 result.position = context.sourcePoint;
394
395 if( aTrial.empty() )
396 return result;
397
398 if( aTrial.back().id == id( "preferred" ) )
399 {
401 return result;
402 }
403
404 result.position.x = KiROUND( aTrial.back().origin.x );
405 result.remainingDof = 1;
406 return result;
407 } );
408
409 SNAP_RESULT result = resolver.Resolve( context );
410
411 BOOST_CHECK( !result.Accepted( id( "preferred" ) ) );
412 BOOST_CHECK( result.Accepted( id( "fallback" ) ) );
413 BOOST_CHECK_EQUAL( result.position.x, 1 );
414}
415
416
417BOOST_AUTO_TEST_CASE( FullyConstrainedAnchorSuppressesRedundantLayoutFeedback )
418{
419 SNAP_SOURCE_CONTEXT context;
420 context.sourcePoint = { 9, 9 };
421
422 SNAP_RESOLVER resolver;
424 SNAP_CANDIDATE_SUBTYPE::INTRINSIC_ANCHOR, { 10, 10 }, 1.0 ) );
425
428 resolver.AddCandidate( std::move( alignment ) );
429
430 SNAP_RESULT result = resolver.Resolve( context );
431
432 BOOST_REQUIRE_EQUAL( result.accepted.size(), 1 );
433 BOOST_CHECK( result.Accepted( id( "anchor" ) ) );
434 BOOST_CHECK( !result.Accepted( id( "alignment" ) ) );
435}
436
437
438BOOST_AUTO_TEST_CASE( AdjacentAngleBranchesRemainAlternatives )
439{
440 SNAP_SOURCE_CONTEXT context;
441 context.sourcePoint = { 10, 4 };
442
443 SNAP_RESOLVER resolver;
444 resolver.AddCandidate( SNAP_CANDIDATE::Line( id( "horizontal" ), SNAP_PRIORITY_TIER::ANGLE,
445 SNAP_CANDIDATE_SUBTYPE::ANGLE_BRANCH, { 0, 0 }, { 1.0, 0.0 }, 4.0 ) );
447 SNAP_CANDIDATE_SUBTYPE::ANGLE_BRANCH, { 0, 0 }, { 0.0, 1.0 }, 10.0 ) );
448
449 SNAP_RESULT result = resolver.Resolve( context );
450
451 BOOST_CHECK_EQUAL( result.position, VECTOR2I( 10, 0 ) );
452 BOOST_CHECK( result.Accepted( id( "horizontal" ) ) );
453 BOOST_CHECK( !result.Accepted( id( "vertical" ) ) );
454}
455
456
457BOOST_AUTO_TEST_CASE( JointFeasibilityRunsFromCommonBaseForEveryTrial )
458{
459 SNAP_SOURCE_CONTEXT context;
460 context.sourcePoint = { 12, 4 };
461 int calls = 0;
462
463 SNAP_RESOLVER resolver;
465 SNAP_CANDIDATE_SUBTYPE::ANGLE_BRANCH, { 0, 0 }, { 1.0, 0.0 }, 4.0 ) );
470 resolver.SetFeasibilityCallback(
471 [&]( const SNAP_SOURCE_CONTEXT&, const std::vector<SNAP_CANDIDATE>& aTrial )
472 {
473 ++calls;
475
476 if( aTrial.empty() )
477 {
478 result.position = context.sourcePoint;
479 return result;
480 }
481
482 if( aTrial.back().id == id( "object" ) )
483 {
485 return result;
486 }
487
488 result.position = aTrial.back().id == id( "grid-x" ) ? VECTOR2I( 10, 0 ) : VECTOR2I( 12, 0 );
489 result.remainingDof = aTrial.size() == 1 ? 1 : 0;
490 return result;
491 } );
492
493 SNAP_RESULT result = resolver.Resolve( context );
494
495 BOOST_CHECK_EQUAL( calls, 4 );
496 BOOST_CHECK_EQUAL( result.position, VECTOR2I( 10, 0 ) );
497 BOOST_CHECK( result.Accepted( id( "angle" ) ) );
498 BOOST_CHECK( !result.Accepted( id( "object" ) ) );
499 BOOST_CHECK( result.Accepted( id( "grid-x" ) ) );
500}
501
502
503BOOST_AUTO_TEST_CASE( MandatoryPrefixCompletesAfterDeadline )
504{
505 using CLOCK = std::chrono::steady_clock;
506
507 SNAP_SOURCE_CONTEXT context;
508 context.sourcePoint = { 8, 3 };
509 CLOCK::time_point now;
510 int optionalCalls = 0;
511
512 SNAP_RESOLVER resolver;
513 resolver.SetClock(
514 [&]
515 {
516 return now;
517 } );
518 resolver.SetDeadline( std::chrono::milliseconds( 8 ) );
519 resolver.SetRetainedCandidate( id( "retained" ) );
521 SNAP_CANDIDATE_SUBTYPE::ANGLE_BRANCH, { 0, 0 }, { 1.0, 0.0 }, 3.0 ) );
523 SNAP_CANDIDATE_SUBTYPE::ANGLE_BRANCH, { 0, 0 }, { 1.0, 1.0 }, 4.0 ) );
530 resolver.SetFeasibilityCallback(
531 [&]( const SNAP_SOURCE_CONTEXT&, const std::vector<SNAP_CANDIDATE>& aTrial )
532 {
534 result.position = context.sourcePoint;
535
536 if( !aTrial.empty() )
537 {
538 now += std::chrono::milliseconds( 3 );
539
540 if( aTrial.back().id == id( "optional" ) )
541 ++optionalCalls;
542 }
543
544 return result;
545 } );
546
547 SNAP_RESULT result = resolver.Resolve( context );
548
549 BOOST_CHECK_EQUAL( optionalCalls, 0 );
551 BOOST_CHECK( result.Accepted( id( "angle-a" ) ) );
552 BOOST_CHECK( result.Accepted( id( "retained" ) ) );
553 BOOST_CHECK( result.Accepted( id( "intrinsic" ) ) );
554}
555
556
557BOOST_AUTO_TEST_CASE( RetainedAngleBranchUsesFivePixelHysteresis )
558{
559 SNAP_SOURCE_CONTEXT context;
560 context.sourcePoint = { 10, 4 };
561
562 SNAP_RESOLVER resolver;
563 resolver.SetRetainedCandidate( id( "horizontal" ) );
564 resolver.AddCandidate( SNAP_CANDIDATE::Line( id( "horizontal" ), SNAP_PRIORITY_TIER::ANGLE,
565 SNAP_CANDIDATE_SUBTYPE::ANGLE_BRANCH, { 0, 0 }, { 1.0, 0.0 }, 0.16 ) );
567 SNAP_CANDIDATE_SUBTYPE::ANGLE_BRANCH, { 0, 0 }, { 1.0, 1.0 }, 0.02 ) );
568
569 SNAP_RESULT result = resolver.Resolve( context );
570
571 BOOST_CHECK( result.Accepted( id( "horizontal" ) ) );
572 BOOST_CHECK( !result.Accepted( id( "diagonal" ) ) );
573}
574
575
576BOOST_AUTO_TEST_CASE( StickyCandidateHoldsWithinHysteresis )
577{
578 // A snap accepted on the previous resolve keeps winning even when a competitor for the same
579 // axis is nearer, as long as the gap is inside the ranking hysteresis. Without this the
580 // equal-spacing solutions jitter frame to frame.
581 SNAP_SOURCE_CONTEXT context;
582 context.sourcePoint = { 4, 0 };
583
584 SNAP_RESOLVER resolver;
585 resolver.SetStickyCandidates( { id( "held" ) } );
590
591 SNAP_RESULT result = resolver.Resolve( context );
592
593 BOOST_CHECK( result.Accepted( id( "held" ) ) );
594 BOOST_CHECK( !result.Accepted( id( "nearer" ) ) );
595}
596
597
598BOOST_AUTO_TEST_CASE( StickyCandidateReleasesBeyondHysteresis )
599{
600 // Once a competitor is clearly nearer (beyond the hysteresis margin) the sticky snap yields, so
601 // the hysteresis stabilizes selection without trapping the cursor on a stale solution.
602 SNAP_SOURCE_CONTEXT context;
603 context.sourcePoint = { 4, 0 };
604
605 SNAP_RESOLVER resolver;
606 resolver.SetStickyCandidates( { id( "held" ) } );
611
612 SNAP_RESULT result = resolver.Resolve( context );
613
614 BOOST_CHECK( result.Accepted( id( "nearer" ) ) );
615 BOOST_CHECK( !result.Accepted( id( "held" ) ) );
616}
617
618
619BOOST_AUTO_TEST_CASE( TraceCallbackReportsRankingTrialsAndResult )
620{
621 SNAP_SOURCE_CONTEXT context;
622 context.sourcePoint = { 4, 0 };
623 std::vector<std::string> trace;
624
625 SNAP_RESOLVER resolver;
626 resolver.SetTraceCallback(
627 [&]( const std::string& aMessage )
628 {
629 trace.push_back( aMessage );
630 } );
631 resolver.SetStickyCandidates( { id( "held" ) } );
634 resolver.AddCandidate( std::move( held ) );
637
638 resolver.Resolve( context );
639
640 const auto contains = [&]( const char* aText )
641 {
642 return std::any_of( trace.begin(), trace.end(),
643 [&]( const std::string& aMessage )
644 {
645 return aMessage.find( aText ) != std::string::npos;
646 } );
647 };
648
649 BOOST_CHECK( contains( "rank index=0" ) );
650 BOOST_CHECK( contains( "sticky=1" ) );
651 BOOST_CHECK( contains( "trial id=" ) );
652 BOOST_CHECK( contains( "accepted=1" ) );
653 BOOST_CHECK( contains( "result status=SUCCESS position=(0,0) accepted=[" ) );
654}
655
656
657BOOST_AUTO_TEST_CASE( IntrinsicObjectOutranksIncompatibleRetainedTarget )
658{
659 SNAP_SOURCE_CONTEXT context;
660 context.sourcePoint = { 3, 4 };
661
662 SNAP_RESOLVER resolver;
663 resolver.SetRetainedCandidate( id( "retained" ) );
665 SNAP_CANDIDATE_SUBTYPE::FINITE_MANIFOLD, { 10, 10 }, 0.0 ) );
668
669 SNAP_RESULT result = resolver.Resolve( context );
670
671 BOOST_CHECK( result.Accepted( id( "intrinsic" ) ) );
672 BOOST_CHECK( !result.Accepted( id( "retained" ) ) );
673 BOOST_CHECK_EQUAL( result.position, VECTOR2I( 2, 2 ) );
674}
675
676
677BOOST_AUTO_TEST_CASE( RunningTrialFinishesButNoFurtherTrialStartsAfterOverrun )
678{
679 using CLOCK = std::chrono::steady_clock;
680
681 SNAP_SOURCE_CONTEXT context;
682 context.sourcePoint = { 4, 6 };
683 CLOCK::time_point now;
684 int trials = 0;
685
686 SNAP_RESOLVER resolver;
687 resolver.SetClock(
688 [&]
689 {
690 return now;
691 } );
692 resolver.SetDeadline( std::chrono::milliseconds( 8 ) );
697 resolver.SetFeasibilityCallback(
698 [&]( const SNAP_SOURCE_CONTEXT&, const std::vector<SNAP_CANDIDATE>& )
699 {
701 result.position = context.sourcePoint;
702 ++trials;
703
704 if( trials == 2 )
705 now += std::chrono::milliseconds( 9 );
706
707 return result;
708 } );
709
710 SNAP_RESULT result = resolver.Resolve( context );
711
712 BOOST_CHECK_EQUAL( trials, 2 );
714 BOOST_CHECK( result.Accepted( std::min( id( "first" ), id( "second" ) ) ) );
715 BOOST_CHECK( !result.Accepted( std::max( id( "first" ), id( "second" ) ) ) );
716}
717
718
719BOOST_AUTO_TEST_CASE( AcceptedCandidateForwardsAllGuides )
720{
721 SNAP_SOURCE_CONTEXT context;
722 context.sourcePoint = { 10, 10 };
723
726 accepted.guides = { { { 0, 0 }, { 5, 0 }, SNAP_GUIDE_STYLE::DIMENSION_BRACKET },
727 { { 10, 0 }, { 15, 0 }, SNAP_GUIDE_STYLE::DIMENSION_BRACKET } };
728
731 rejected.guides = { { { 20, 0 }, { 25, 0 }, SNAP_GUIDE_STYLE::DIMENSION_BRACKET } };
732
733 SNAP_RESOLVER resolver;
734 resolver.AddCandidate( std::move( rejected ) );
735 resolver.AddCandidate( std::move( accepted ) );
736
737 SNAP_RESULT result = resolver.Resolve( context );
738
739 BOOST_REQUIRE_EQUAL( result.guides.size(), 2 );
740 BOOST_CHECK_EQUAL( result.guides[0].start, VECTOR2I( 0, 0 ) );
741 BOOST_CHECK_EQUAL( result.guides[0].end, VECTOR2I( 5, 0 ) );
742 BOOST_CHECK_EQUAL( result.guides[1].start, VECTOR2I( 10, 0 ) );
743 BOOST_CHECK_EQUAL( result.guides[1].end, VECTOR2I( 15, 0 ) );
744}
745
746
constexpr BOX2I KiROUND(const BOX2D &aBoxD)
Definition box2.h:982
Definition kiid.h:46
std::array< uint8_t, 16 > AsBytes() const
Definition kiid.cpp:276
static KIID FromBytes(const std::array< uint8_t, 16 > &aBytes)
Definition kiid.cpp:250
static KIID FromName(const std::string &aName)
Return a KIID derived from a name, the same name always gives the same KIID.
Definition kiid.cpp:237
SNAP_RESULT Resolve(const SNAP_SOURCE_CONTEXT &aContext) const
void SetFeasibilityCallback(FEASIBILITY_CALLBACK aCallback)
void SetStickyCandidates(std::vector< SNAP_STABLE_ID > aIds)
Bias the ranking toward candidates accepted on a previous resolve so the chosen snap stays put until ...
void SetDeadline(CLOCK::duration aDeadline)
void SetClock(CLOCK_CALLBACK aClock)
void SetTraceCallback(TRACE_CALLBACK aCallback)
void AddCandidate(SNAP_CANDIDATE aCandidate)
void SetRetainedCandidate(std::optional< SNAP_STABLE_ID > aId)
std::chrono::steady_clock CLOCK
SNAP_STABLE_ID MakeIntersectionSnapId(const SNAP_STABLE_ID &aFirst, const SNAP_STABLE_ID &aSecond, int aSolutionBranch)
SNAP_STABLE_ID MakePointSnapId(SNAP_ID_KIND aKind, const VECTOR2I &aPoint, int aFeatureIndex=0)
SNAP_STABLE_ID MakeCompositeSnapId(SNAP_ID_KIND aKind, const std::vector< SNAP_TARGET_ID > &aTargets, int aFeatureIndex=0)
SNAP_STABLE_ID MakeDerivedSnapId(SNAP_ID_KIND aKind, const SNAP_STABLE_ID &aSource, int aFeatureIndex=0, int aSolutionBranch=0)
std::array< uint8_t, 16 > SNAP_TARGET_ID
static SNAP_CANDIDATE Point(SNAP_STABLE_ID aId, SNAP_PRIORITY_TIER aPriority, SNAP_CANDIDATE_SUBTYPE aSubtype, const VECTOR2I &aPoint, double aResidual)
static SNAP_CANDIDATE Line(SNAP_STABLE_ID aId, SNAP_PRIORITY_TIER aPriority, SNAP_CANDIDATE_SUBTYPE aSubtype, const VECTOR2I &aOrigin, const VECTOR2D &aDirection, double aResidual)
static SNAP_CANDIDATE AxisY(SNAP_STABLE_ID aId, SNAP_PRIORITY_TIER aPriority, SNAP_CANDIDATE_SUBTYPE aSubtype, int aCoordinate, double aResidual)
static SNAP_CANDIDATE AxisX(SNAP_STABLE_ID aId, SNAP_PRIORITY_TIER aPriority, SNAP_CANDIDATE_SUBTYPE aSubtype, int aCoordinate, double aResidual)
std::vector< SNAP_GUIDE > guides
SNAP_GUIDE_STYLE style
VECTOR2I end
VECTOR2I start
std::vector< double > quantizedResiduals
VECTOR2I position
std::vector< SNAP_GUIDE > guides
std::vector< SNAP_STABLE_ID > accepted
SNAP_RESULT_STATUS status
SNAP_ID_KIND kind
SNAP_TARGET_ID target
BOOST_AUTO_TEST_CASE(HorizontalAlignment)
BOOST_AUTO_TEST_SUITE(CadstarPartParser)
BOOST_AUTO_TEST_SUITE_END()
static std::vector< int > candidates(const SEGMENT_INDEX &aIndex, const SEG &aQuery, int aPadding)
BOOST_AUTO_TEST_CASE(StableIdentityUsesTypedKindAndTarget)
wxString result
Test unit parsing edge cases and error handling.
BOOST_CHECK_EQUAL(result, "25.4")
VECTOR2< int32_t > VECTOR2I
Definition vector2d.h:708