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
130
131BOOST_AUTO_TEST_CASE( IdentityConstructionHasStableGoldenBytes )
132{
133 const SNAP_STABLE_ID first{ SNAP_ID_KIND::ITEM_GEOMETRY, KIID( "00000000-0000-0000-0000-000000000001" ).AsBytes(),
134 1, 2 };
136 KIID( "00000000-0000-0000-0000-000000000002" ).AsBytes(), 3, 4 };
137
138 BOOST_CHECK_EQUAL( targetBytes( MakePointSnapId( SNAP_ID_KIND::CONSTRUCTION, { 123, -456 }, 7 ) ),
139 "b1fa72f98d0eaaf396ce1f1030e5f274" );
140 BOOST_CHECK_EQUAL( targetBytes( MakeDerivedSnapId( SNAP_ID_KIND::BOUNDS_X, first, 8, 9 ) ),
141 "38ec5cc638db882f25564f708cd908d4" );
142 BOOST_CHECK_EQUAL( targetBytes( MakeIntersectionSnapId( first, second, 5 ) ), "b260a931293228a311feb6c704737605" );
144 { KIID( "00000000-0000-0000-0000-000000000002" ).AsBytes(),
145 KIID( "00000000-0000-0000-0000-000000000001" ).AsBytes() },
146 3 ) ),
147 "9084556ddbcdd0543f07abd73d7fb746" );
148}
149
150
151BOOST_AUTO_TEST_CASE( AuthoredRelationWinsEveryTransientTier )
152{
153 SNAP_SOURCE_CONTEXT context;
154 context.sourcePoint = { 18, 19 };
155
157 resolver.AddCandidate( SNAP_CANDIDATE::Point( id( "cursor" ), SNAP_PRIORITY_TIER::CURSOR,
158 SNAP_CANDIDATE_SUBTYPE::CURSOR, { 18, 19 }, 0.0 ) );
159 resolver.AddCandidate( SNAP_CANDIDATE::Point( id( "object" ), SNAP_PRIORITY_TIER::OBJECT,
160 SNAP_CANDIDATE_SUBTYPE::FINITE_MANIFOLD, { 20, 20 }, 1.0 ) );
162 SNAP_CANDIDATE_SUBTYPE::INTRINSIC_ANCHOR, { 10, 10 }, 20.0 ) );
163
164 SNAP_RESULT result = resolver.Resolve( context );
165
166 BOOST_REQUIRE_EQUAL( result.status, SNAP_RESULT_STATUS::SUCCESS );
167 BOOST_CHECK_EQUAL( result.position, VECTOR2I( 10, 10 ) );
168 BOOST_REQUIRE_EQUAL( result.accepted.size(), 1 );
169 BOOST_CHECK( result.accepted.front() == id( "authored" ) );
170}
171
172
173BOOST_AUTO_TEST_CASE( AngleRejectsIncompatibleObjectWithoutRelaxing )
174{
175 SNAP_SOURCE_CONTEXT context;
176 context.sourcePoint = { 12, 4 };
177
179 resolver.AddCandidate( SNAP_CANDIDATE::Line( id( "angle" ), SNAP_PRIORITY_TIER::ANGLE,
180 SNAP_CANDIDATE_SUBTYPE::ANGLE_BRANCH, { 0, 0 }, { 1.0, 0.0 }, 4.0 ) );
181 resolver.AddCandidate( SNAP_CANDIDATE::Point( id( "object" ), SNAP_PRIORITY_TIER::OBJECT,
183 resolver.AddCandidate( SNAP_CANDIDATE::AxisX( id( "grid-x" ), SNAP_PRIORITY_TIER::GRID,
185
186 SNAP_RESULT result = resolver.Resolve( context );
187
188 BOOST_REQUIRE_EQUAL( result.status, SNAP_RESULT_STATUS::SUCCESS );
189 BOOST_CHECK_EQUAL( result.position, VECTOR2I( 10, 0 ) );
190 BOOST_CHECK( result.Accepted( id( "angle" ) ) );
191 BOOST_CHECK( !result.Accepted( id( "object" ) ) );
192 BOOST_CHECK( result.Accepted( id( "grid-x" ) ) );
193}
194
195
196BOOST_AUTO_TEST_CASE( CompatibleObjectAxesCompose )
197{
198 SNAP_SOURCE_CONTEXT context;
199 context.sourcePoint = { 14, 17 };
200
202 resolver.AddCandidate( SNAP_CANDIDATE::AxisX( id( "align-x" ), SNAP_PRIORITY_TIER::OBJECT,
204 resolver.AddCandidate( SNAP_CANDIDATE::AxisY( id( "align-y" ), SNAP_PRIORITY_TIER::OBJECT,
206
207 SNAP_RESULT result = resolver.Resolve( context );
208
209 BOOST_REQUIRE_EQUAL( result.status, SNAP_RESULT_STATUS::SUCCESS );
210 BOOST_CHECK_EQUAL( result.position, VECTOR2I( 10, 20 ) );
211 BOOST_CHECK( result.Accepted( id( "align-x" ) ) );
212 BOOST_CHECK( result.Accepted( id( "align-y" ) ) );
213 BOOST_CHECK_EQUAL( result.remainingDof, 0 );
214}
215
216
217BOOST_AUTO_TEST_CASE( ConflictingGridAxisLeavesCompatibleAxisAccepted )
218{
219 SNAP_SOURCE_CONTEXT context;
220 context.sourcePoint = { 9, 13 };
221
223 resolver.AddCandidate( SNAP_CANDIDATE::Line( id( "angle" ), SNAP_PRIORITY_TIER::ANGLE,
224 SNAP_CANDIDATE_SUBTYPE::ANGLE_BRANCH, { 0, 0 }, { 1.0, 1.0 }, 3.0 ) );
225 resolver.AddCandidate( SNAP_CANDIDATE::AxisX( id( "grid-x" ), SNAP_PRIORITY_TIER::GRID,
227 resolver.AddCandidate( SNAP_CANDIDATE::AxisY( id( "grid-y" ), SNAP_PRIORITY_TIER::GRID,
229
230 SNAP_RESULT result = resolver.Resolve( context );
231
232 BOOST_REQUIRE_EQUAL( result.status, SNAP_RESULT_STATUS::SUCCESS );
233 BOOST_CHECK_EQUAL( result.position, VECTOR2I( 10, 10 ) );
234 BOOST_CHECK( result.Accepted( id( "grid-x" ) ) );
235 BOOST_CHECK( !result.Accepted( id( "grid-y" ) ) );
236}
237
238
239BOOST_AUTO_TEST_CASE( NearParallelIntersectionOutsideCoordinateRangeIsInfeasible )
240{
241 // Real-world capture: two nearly-collinear track segments pass the rank test but
242 // intersect far outside the int coordinate space, firing the KiROUND assert.
243 SNAP_SOURCE_CONTEXT context;
244 context.sourcePoint = { 142936256, 71674527 };
245
247 resolver.AddCandidate( SNAP_CANDIDATE::Line( id( "segment-a" ), SNAP_PRIORITY_TIER::OBJECT,
248 SNAP_CANDIDATE_SUBTYPE::FINITE_MANIFOLD, { 142470148, 74198721 },
249 { 1006370.0, -3755829.0 }, 0.25851349837571413 ) );
250 resolver.AddCandidate( SNAP_CANDIDATE::Line( id( "segment-b" ), SNAP_PRIORITY_TIER::OBJECT,
251 SNAP_CANDIDATE_SUBTYPE::FINITE_MANIFOLD, { 141987238, 74262298 },
252 { 1006358.0, -3755779.0 }, 0.31431204619285724 ) );
253
254 SNAP_RESULT result = resolver.Resolve( context );
255
256 BOOST_REQUIRE_EQUAL( result.status, SNAP_RESULT_STATUS::SUCCESS );
257 BOOST_CHECK_EQUAL( result.position, VECTOR2I( 143132419, 71727089 ) );
258 BOOST_CHECK( result.Accepted( id( "segment-a" ) ) );
259 BOOST_CHECK( !result.Accepted( id( "segment-b" ) ) );
260 BOOST_CHECK_EQUAL( result.remainingDof, 1 );
261}
262
263
264BOOST_AUTO_TEST_CASE( NearParallelIntersectionInsideCoordinateRangeComposes )
265{
266 // Artificial pair whose intersection is nonsensically far away but still representable.
267 SNAP_SOURCE_CONTEXT context;
268 context.sourcePoint = { 10, 0 };
269
271 resolver.AddCandidate( SNAP_CANDIDATE::Line( id( "segment-a" ), SNAP_PRIORITY_TIER::OBJECT,
272 SNAP_CANDIDATE_SUBTYPE::FINITE_MANIFOLD, { 0, 0 }, { 1.0, 0.0 },
273 0.0 ) );
274 resolver.AddCandidate( SNAP_CANDIDATE::Line( id( "segment-b" ), SNAP_PRIORITY_TIER::OBJECT,
275 SNAP_CANDIDATE_SUBTYPE::FINITE_MANIFOLD, { 0, 1000 }, { 1.0, 0.001 },
276 5.0 ) );
277
278 SNAP_RESULT result = resolver.Resolve( context );
279
280 BOOST_REQUIRE_EQUAL( result.status, SNAP_RESULT_STATUS::SUCCESS );
281 BOOST_CHECK_EQUAL( result.position, VECTOR2I( -1000000, 0 ) );
282 BOOST_CHECK( result.Accepted( id( "segment-a" ) ) );
283 BOOST_CHECK( result.Accepted( id( "segment-b" ) ) );
284 BOOST_CHECK_EQUAL( result.remainingDof, 0 );
285}
286
287
288BOOST_AUTO_TEST_CASE( StableIdentityBreaksExactRankTie )
289{
290 SNAP_SOURCE_CONTEXT context;
291 context.sourcePoint = { 0, 0 };
292
298
299 SNAP_RESULT result = resolver.Resolve( context );
300
301 BOOST_REQUIRE_EQUAL( result.accepted.size(), 1 );
302 BOOST_CHECK( result.accepted.front() == std::min( id( "a" ), id( "z" ) ) );
303}
304
305
306BOOST_AUTO_TEST_CASE( FixedCursorPathIsIndependentOfCandidateStorageOrder )
307{
308 const std::vector<VECTOR2I> cursorPath = { { 0, 0 }, { 3, 5 }, { 7, 9 }, { 11, 13 } };
309
310 const auto resolvePath = [&]( bool aReverse )
311 {
312 std::vector<std::string> signatures;
313
314 for( const VECTOR2I& cursor : cursorPath )
315 {
316 std::vector<SNAP_CANDIDATE> candidates = {
318 cursor.x + 2, 0.2 ),
320 cursor.x + 1, 0.2 ),
322 cursor.y - 1, 0.1 )
323 };
324
325 if( aReverse )
326 std::reverse( candidates.begin(), candidates.end() );
327
328 SNAP_SOURCE_CONTEXT context;
329 context.sourcePoint = cursor;
331
332 for( SNAP_CANDIDATE& candidate : candidates )
333 resolver.AddCandidate( std::move( candidate ) );
334
335 signatures.push_back( resultSignature( resolver.Resolve( context ) ) );
336 }
337
338 return signatures;
339 };
340
341 BOOST_CHECK( resolvePath( false ) == resolvePath( true ) );
342}
343
344
345BOOST_AUTO_TEST_CASE( ReferenceAffinityOutranksDistance )
346{
347 SNAP_SOURCE_CONTEXT context;
348 context.sourcePoint = { 0, 0 };
349
352 preferred.referenceAffinity = 0;
353
356 fallback.referenceAffinity = 1;
357
359 resolver.AddCandidate( std::move( fallback ) );
360 resolver.AddCandidate( std::move( preferred ) );
361
362 SNAP_RESULT result = resolver.Resolve( context );
363
364 BOOST_CHECK( result.Accepted( id( "preferred" ) ) );
365 BOOST_CHECK( !result.Accepted( id( "fallback" ) ) );
366}
367
368
369BOOST_AUTO_TEST_CASE( InfeasiblePreferredReferenceFallsBack )
370{
371 SNAP_SOURCE_CONTEXT context;
372 context.sourcePoint = { 0, 0 };
373
376 preferred.referenceAffinity = 0;
377
380 fallback.referenceAffinity = 1;
381
383 resolver.AddCandidate( std::move( fallback ) );
384 resolver.AddCandidate( std::move( preferred ) );
385 resolver.SetFeasibilityCallback(
386 [&]( const SNAP_SOURCE_CONTEXT&, const std::vector<SNAP_CANDIDATE>& aTrial )
387 {
389 result.position = context.sourcePoint;
390
391 if( aTrial.empty() )
392 return result;
393
394 if( aTrial.back().id == id( "preferred" ) )
395 {
397 return result;
398 }
399
400 result.position.x = KiROUND( aTrial.back().origin.x );
401 result.remainingDof = 1;
402 return result;
403 } );
404
405 SNAP_RESULT result = resolver.Resolve( context );
406
407 BOOST_CHECK( !result.Accepted( id( "preferred" ) ) );
408 BOOST_CHECK( result.Accepted( id( "fallback" ) ) );
409 BOOST_CHECK_EQUAL( result.position.x, 1 );
410}
411
412
413BOOST_AUTO_TEST_CASE( FullyConstrainedAnchorSuppressesRedundantLayoutFeedback )
414{
415 SNAP_SOURCE_CONTEXT context;
416 context.sourcePoint = { 9, 9 };
417
419 resolver.AddCandidate( SNAP_CANDIDATE::Point( id( "anchor" ), SNAP_PRIORITY_TIER::OBJECT,
420 SNAP_CANDIDATE_SUBTYPE::INTRINSIC_ANCHOR, { 10, 10 }, 1.0 ) );
421
424 resolver.AddCandidate( std::move( alignment ) );
425
426 SNAP_RESULT result = resolver.Resolve( context );
427
428 BOOST_REQUIRE_EQUAL( result.accepted.size(), 1 );
429 BOOST_CHECK( result.Accepted( id( "anchor" ) ) );
430 BOOST_CHECK( !result.Accepted( id( "alignment" ) ) );
431}
432
433
434BOOST_AUTO_TEST_CASE( AdjacentAngleBranchesRemainAlternatives )
435{
436 SNAP_SOURCE_CONTEXT context;
437 context.sourcePoint = { 10, 4 };
438
440 resolver.AddCandidate( SNAP_CANDIDATE::Line( id( "horizontal" ), SNAP_PRIORITY_TIER::ANGLE,
441 SNAP_CANDIDATE_SUBTYPE::ANGLE_BRANCH, { 0, 0 }, { 1.0, 0.0 }, 4.0 ) );
442 resolver.AddCandidate( SNAP_CANDIDATE::Line( id( "vertical" ), SNAP_PRIORITY_TIER::ANGLE,
443 SNAP_CANDIDATE_SUBTYPE::ANGLE_BRANCH, { 0, 0 }, { 0.0, 1.0 }, 10.0 ) );
444
445 SNAP_RESULT result = resolver.Resolve( context );
446
447 BOOST_CHECK_EQUAL( result.position, VECTOR2I( 10, 0 ) );
448 BOOST_CHECK( result.Accepted( id( "horizontal" ) ) );
449 BOOST_CHECK( !result.Accepted( id( "vertical" ) ) );
450}
451
452
453BOOST_AUTO_TEST_CASE( JointFeasibilityRunsFromCommonBaseForEveryTrial )
454{
455 SNAP_SOURCE_CONTEXT context;
456 context.sourcePoint = { 12, 4 };
457 int calls = 0;
458
460 resolver.AddCandidate( SNAP_CANDIDATE::Line( id( "angle" ), SNAP_PRIORITY_TIER::ANGLE,
461 SNAP_CANDIDATE_SUBTYPE::ANGLE_BRANCH, { 0, 0 }, { 1.0, 0.0 }, 4.0 ) );
462 resolver.AddCandidate( SNAP_CANDIDATE::Point( id( "object" ), SNAP_PRIORITY_TIER::OBJECT,
464 resolver.AddCandidate( SNAP_CANDIDATE::AxisX( id( "grid-x" ), SNAP_PRIORITY_TIER::GRID,
466 resolver.SetFeasibilityCallback(
467 [&]( const SNAP_SOURCE_CONTEXT&, const std::vector<SNAP_CANDIDATE>& aTrial )
468 {
469 ++calls;
471
472 if( aTrial.empty() )
473 {
474 result.position = context.sourcePoint;
475 return result;
476 }
477
478 if( aTrial.back().id == id( "object" ) )
479 {
481 return result;
482 }
483
484 result.position = aTrial.back().id == id( "grid-x" ) ? VECTOR2I( 10, 0 ) : VECTOR2I( 12, 0 );
485 result.remainingDof = aTrial.size() == 1 ? 1 : 0;
486 return result;
487 } );
488
489 SNAP_RESULT result = resolver.Resolve( context );
490
491 BOOST_CHECK_EQUAL( calls, 4 );
492 BOOST_CHECK_EQUAL( result.position, VECTOR2I( 10, 0 ) );
493 BOOST_CHECK( result.Accepted( id( "angle" ) ) );
494 BOOST_CHECK( !result.Accepted( id( "object" ) ) );
495 BOOST_CHECK( result.Accepted( id( "grid-x" ) ) );
496}
497
498
499BOOST_AUTO_TEST_CASE( MandatoryPrefixCompletesAfterDeadline )
500{
501 using CLOCK = std::chrono::steady_clock;
502
503 SNAP_SOURCE_CONTEXT context;
504 context.sourcePoint = { 8, 3 };
505 CLOCK::time_point now;
506 int optionalCalls = 0;
507
509 resolver.SetClock(
510 [&]
511 {
512 return now;
513 } );
514 resolver.SetDeadline( std::chrono::milliseconds( 8 ) );
515 resolver.SetRetainedCandidate( id( "retained" ) );
516 resolver.AddCandidate( SNAP_CANDIDATE::Line( id( "angle-a" ), SNAP_PRIORITY_TIER::ANGLE,
517 SNAP_CANDIDATE_SUBTYPE::ANGLE_BRANCH, { 0, 0 }, { 1.0, 0.0 }, 3.0 ) );
518 resolver.AddCandidate( SNAP_CANDIDATE::Line( id( "angle-b" ), SNAP_PRIORITY_TIER::ANGLE,
519 SNAP_CANDIDATE_SUBTYPE::ANGLE_BRANCH, { 0, 0 }, { 1.0, 1.0 }, 4.0 ) );
520 resolver.AddCandidate( SNAP_CANDIDATE::Point( id( "retained" ), SNAP_PRIORITY_TIER::OBJECT,
522 resolver.AddCandidate( SNAP_CANDIDATE::AxisX( id( "intrinsic" ), SNAP_PRIORITY_TIER::OBJECT,
524 resolver.AddCandidate( SNAP_CANDIDATE::AxisY( id( "optional" ), SNAP_PRIORITY_TIER::GRID,
526 resolver.SetFeasibilityCallback(
527 [&]( const SNAP_SOURCE_CONTEXT&, const std::vector<SNAP_CANDIDATE>& aTrial )
528 {
530 result.position = context.sourcePoint;
531
532 if( !aTrial.empty() )
533 {
534 now += std::chrono::milliseconds( 3 );
535
536 if( aTrial.back().id == id( "optional" ) )
537 ++optionalCalls;
538 }
539
540 return result;
541 } );
542
543 SNAP_RESULT result = resolver.Resolve( context );
544
545 BOOST_CHECK_EQUAL( optionalCalls, 0 );
547 BOOST_CHECK( result.Accepted( id( "angle-a" ) ) );
548 BOOST_CHECK( result.Accepted( id( "retained" ) ) );
549 BOOST_CHECK( result.Accepted( id( "intrinsic" ) ) );
550}
551
552
553BOOST_AUTO_TEST_CASE( RetainedAngleBranchUsesFivePixelHysteresis )
554{
555 SNAP_SOURCE_CONTEXT context;
556 context.sourcePoint = { 10, 4 };
557
559 resolver.SetRetainedCandidate( id( "horizontal" ) );
560 resolver.AddCandidate( SNAP_CANDIDATE::Line( id( "horizontal" ), SNAP_PRIORITY_TIER::ANGLE,
561 SNAP_CANDIDATE_SUBTYPE::ANGLE_BRANCH, { 0, 0 }, { 1.0, 0.0 }, 0.16 ) );
562 resolver.AddCandidate( SNAP_CANDIDATE::Line( id( "diagonal" ), SNAP_PRIORITY_TIER::ANGLE,
563 SNAP_CANDIDATE_SUBTYPE::ANGLE_BRANCH, { 0, 0 }, { 1.0, 1.0 }, 0.02 ) );
564
565 SNAP_RESULT result = resolver.Resolve( context );
566
567 BOOST_CHECK( result.Accepted( id( "horizontal" ) ) );
568 BOOST_CHECK( !result.Accepted( id( "diagonal" ) ) );
569}
570
571
572BOOST_AUTO_TEST_CASE( StickyCandidateHoldsWithinHysteresis )
573{
574 // A snap accepted on the previous resolve keeps winning even when a competitor for the same
575 // axis is nearer, as long as the gap is inside the ranking hysteresis. Without this the
576 // equal-spacing solutions jitter frame to frame.
577 SNAP_SOURCE_CONTEXT context;
578 context.sourcePoint = { 4, 0 };
579
581 resolver.SetStickyCandidates( { id( "held" ) } );
582 resolver.AddCandidate( SNAP_CANDIDATE::AxisX( id( "held" ), SNAP_PRIORITY_TIER::OBJECT,
584 resolver.AddCandidate( SNAP_CANDIDATE::AxisX( id( "nearer" ), SNAP_PRIORITY_TIER::OBJECT,
586
587 SNAP_RESULT result = resolver.Resolve( context );
588
589 BOOST_CHECK( result.Accepted( id( "held" ) ) );
590 BOOST_CHECK( !result.Accepted( id( "nearer" ) ) );
591}
592
593
594BOOST_AUTO_TEST_CASE( StickyCandidateReleasesBeyondHysteresis )
595{
596 // Once a competitor is clearly nearer (beyond the hysteresis margin) the sticky snap yields, so
597 // the hysteresis stabilizes selection without trapping the cursor on a stale solution.
598 SNAP_SOURCE_CONTEXT context;
599 context.sourcePoint = { 4, 0 };
600
602 resolver.SetStickyCandidates( { id( "held" ) } );
603 resolver.AddCandidate( SNAP_CANDIDATE::AxisX( id( "held" ), SNAP_PRIORITY_TIER::OBJECT,
605 resolver.AddCandidate( SNAP_CANDIDATE::AxisX( id( "nearer" ), SNAP_PRIORITY_TIER::OBJECT,
607
608 SNAP_RESULT result = resolver.Resolve( context );
609
610 BOOST_CHECK( result.Accepted( id( "nearer" ) ) );
611 BOOST_CHECK( !result.Accepted( id( "held" ) ) );
612}
613
614
615BOOST_AUTO_TEST_CASE( TraceCallbackReportsRankingTrialsAndResult )
616{
617 SNAP_SOURCE_CONTEXT context;
618 context.sourcePoint = { 4, 0 };
619 std::vector<std::string> trace;
620
622 resolver.SetTraceCallback(
623 [&]( const std::string& aMessage )
624 {
625 trace.push_back( aMessage );
626 } );
627 resolver.SetStickyCandidates( { id( "held" ) } );
630 resolver.AddCandidate( std::move( held ) );
631 resolver.AddCandidate( SNAP_CANDIDATE::AxisX( id( "nearer" ), SNAP_PRIORITY_TIER::OBJECT,
633
634 resolver.Resolve( context );
635
636 const auto contains = [&]( const char* aText )
637 {
638 return std::any_of( trace.begin(), trace.end(),
639 [&]( const std::string& aMessage )
640 {
641 return aMessage.find( aText ) != std::string::npos;
642 } );
643 };
644
645 BOOST_CHECK( contains( "rank index=0" ) );
646 BOOST_CHECK( contains( "sticky=1" ) );
647 BOOST_CHECK( contains( "trial id=" ) );
648 BOOST_CHECK( contains( "accepted=1" ) );
649 BOOST_CHECK( contains( "result status=SUCCESS position=(0,0) accepted=[" ) );
650}
651
652
653BOOST_AUTO_TEST_CASE( IntrinsicObjectOutranksIncompatibleRetainedTarget )
654{
655 SNAP_SOURCE_CONTEXT context;
656 context.sourcePoint = { 3, 4 };
657
659 resolver.SetRetainedCandidate( id( "retained" ) );
660 resolver.AddCandidate( SNAP_CANDIDATE::Point( id( "retained" ), SNAP_PRIORITY_TIER::OBJECT,
661 SNAP_CANDIDATE_SUBTYPE::FINITE_MANIFOLD, { 10, 10 }, 0.0 ) );
662 resolver.AddCandidate( SNAP_CANDIDATE::Point( id( "intrinsic" ), SNAP_PRIORITY_TIER::OBJECT,
664
665 SNAP_RESULT result = resolver.Resolve( context );
666
667 BOOST_CHECK( result.Accepted( id( "intrinsic" ) ) );
668 BOOST_CHECK( !result.Accepted( id( "retained" ) ) );
669 BOOST_CHECK_EQUAL( result.position, VECTOR2I( 2, 2 ) );
670}
671
672
673BOOST_AUTO_TEST_CASE( RunningTrialFinishesButNoFurtherTrialStartsAfterOverrun )
674{
675 using CLOCK = std::chrono::steady_clock;
676
677 SNAP_SOURCE_CONTEXT context;
678 context.sourcePoint = { 4, 6 };
679 CLOCK::time_point now;
680 int trials = 0;
681
683 resolver.SetClock(
684 [&]
685 {
686 return now;
687 } );
688 resolver.SetDeadline( std::chrono::milliseconds( 8 ) );
689 resolver.AddCandidate( SNAP_CANDIDATE::AxisX( id( "first" ), SNAP_PRIORITY_TIER::OBJECT,
691 resolver.AddCandidate( SNAP_CANDIDATE::AxisY( id( "second" ), SNAP_PRIORITY_TIER::OBJECT,
693 resolver.SetFeasibilityCallback(
694 [&]( const SNAP_SOURCE_CONTEXT&, const std::vector<SNAP_CANDIDATE>& )
695 {
697 result.position = context.sourcePoint;
698 ++trials;
699
700 if( trials == 2 )
701 now += std::chrono::milliseconds( 9 );
702
703 return result;
704 } );
705
706 SNAP_RESULT result = resolver.Resolve( context );
707
708 BOOST_CHECK_EQUAL( trials, 2 );
710 BOOST_CHECK( result.Accepted( std::min( id( "first" ), id( "second" ) ) ) );
711 BOOST_CHECK( !result.Accepted( std::max( id( "first" ), id( "second" ) ) ) );
712}
713
714
715BOOST_AUTO_TEST_CASE( AcceptedCandidateForwardsAllGuides )
716{
717 SNAP_SOURCE_CONTEXT context;
718 context.sourcePoint = { 10, 10 };
719
722 accepted.guides = { { { 0, 0 }, { 5, 0 }, SNAP_GUIDE_STYLE::DIMENSION_BRACKET },
723 { { 10, 0 }, { 15, 0 }, SNAP_GUIDE_STYLE::DIMENSION_BRACKET } };
724
727 rejected.guides = { { { 20, 0 }, { 25, 0 }, SNAP_GUIDE_STYLE::DIMENSION_BRACKET } };
728
730 resolver.AddCandidate( std::move( rejected ) );
731 resolver.AddCandidate( std::move( accepted ) );
732
733 SNAP_RESULT result = resolver.Resolve( context );
734
735 BOOST_REQUIRE_EQUAL( result.guides.size(), 2 );
736 BOOST_CHECK_EQUAL( result.guides[0].start, VECTOR2I( 0, 0 ) );
737 BOOST_CHECK_EQUAL( result.guides[0].end, VECTOR2I( 5, 0 ) );
738 BOOST_CHECK_EQUAL( result.guides[1].start, VECTOR2I( 10, 0 ) );
739 BOOST_CHECK_EQUAL( result.guides[1].end, VECTOR2I( 15, 0 ) );
740}
741
742
constexpr BOX2I KiROUND(const BOX2D &aBoxD)
Definition box2.h:995
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
static FILENAME_RESOLVER * resolver
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()
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:683