KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_snap_inference.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_inference.h>
25
26#include <algorithm>
27#include <chrono>
28#include <ctime>
29#include <map>
30
31
32namespace
33{
34SNAP_TARGET_ID targetId( const char* aName )
35{
36 return KIID::FromName( aName ).AsBytes();
37}
38
39
40SNAP_STABLE_ID featureId( const char* aItem, const char* aFeature )
41{
42 return { SNAP_ID_KIND::ITEM_GEOMETRY, targetId( aItem ), static_cast<int>( KIID::FromName( aFeature ).Hash() ), 0 };
43}
44
45
46template <typename T>
47VECTOR2<T> transpose( const VECTOR2<T>& aPoint )
48{
49 return { aPoint.y, aPoint.x };
50}
51
52
53BOX2I transpose( const BOX2I& aBox )
54{
55 return { transpose( aBox.GetOrigin() ), transpose( aBox.GetSize() ) };
56}
57
58
59SNAP_ID_KIND transpose( SNAP_ID_KIND aKind )
60{
61 switch( aKind )
62 {
73 default: return aKind;
74 }
75}
76
77
78SNAP_CANDIDATE transpose( SNAP_CANDIDATE aCandidate )
79{
80 aCandidate.id.kind = transpose( aCandidate.id.kind );
81 aCandidate.origin = transpose( aCandidate.origin );
82 aCandidate.direction = transpose( aCandidate.direction );
83
84 for( SNAP_GUIDE& guide : aCandidate.guides )
85 {
86 guide.start = transpose( guide.start );
87 guide.end = transpose( guide.end );
88 }
89
90 return aCandidate;
91}
92
93
94void checkTransposeEquivalent( std::vector<SNAP_CANDIDATE> aExpected, std::vector<SNAP_CANDIDATE> aTransposed )
95{
96 for( SNAP_CANDIDATE& candidate : aTransposed )
97 candidate = transpose( std::move( candidate ) );
98
99 const auto byId = []( const SNAP_CANDIDATE& aLeft, const SNAP_CANDIDATE& aRight )
100 {
101 return aLeft.id < aRight.id;
102 };
103 std::sort( aExpected.begin(), aExpected.end(), byId );
104 std::sort( aTransposed.begin(), aTransposed.end(), byId );
105 BOOST_REQUIRE_EQUAL( aExpected.size(), aTransposed.size() );
106
107 for( size_t i = 0; i < aExpected.size(); ++i )
108 {
109 const SNAP_CANDIDATE& expected = aExpected[i];
110 const SNAP_CANDIDATE& actual = aTransposed[i];
111 BOOST_CHECK( expected.id == actual.id );
112 BOOST_CHECK( expected.priority == actual.priority );
113 BOOST_CHECK( expected.subtype == actual.subtype );
114 BOOST_CHECK( expected.relation == actual.relation );
115 BOOST_CHECK_EQUAL( expected.origin, actual.origin );
116 BOOST_CHECK_EQUAL( expected.direction, actual.direction );
117 BOOST_CHECK_EQUAL( expected.normalizedScreenResidual, actual.normalizedScreenResidual );
118 BOOST_CHECK_EQUAL( expected.referenceAffinity, actual.referenceAffinity );
119 BOOST_CHECK_EQUAL( expected.consumedDof, actual.consumedDof );
120 BOOST_CHECK_EQUAL( expected.finite, actual.finite );
121 BOOST_CHECK_EQUAL( expected.domainStart, actual.domainStart );
122 BOOST_CHECK_EQUAL( expected.domainEnd, actual.domainEnd );
123 BOOST_REQUIRE_EQUAL( expected.guides.size(), actual.guides.size() );
124
125 for( size_t guide = 0; guide < expected.guides.size(); ++guide )
126 {
127 BOOST_CHECK_EQUAL( expected.guides[guide].start, actual.guides[guide].start );
128 BOOST_CHECK_EQUAL( expected.guides[guide].end, actual.guides[guide].end );
129 BOOST_CHECK( expected.guides[guide].style == actual.guides[guide].style );
130 }
131
132 BOOST_CHECK( expected.manifold == actual.manifold );
133 }
134}
135} // namespace
136
137
138BOOST_AUTO_TEST_SUITE( SnapInference )
139
140
141BOOST_AUTO_TEST_CASE( GroupDefaultsPreserveConservativeInference )
142{
144
145 BOOST_CHECK( settings.objectGeometry );
146 BOOST_CHECK( settings.constructionExtensions );
147 BOOST_CHECK( !settings.tangentNormal );
148 BOOST_CHECK( !settings.alignmentDistribution );
149}
150
151
152BOOST_AUTO_TEST_CASE( FiniteSegmentsRejectExtensionOnlyIntersection )
153{
155 provider.AddPath( { featureId( "horizontal", "edge" ), SEG( { 0, 0 }, { 10, 0 } ) } );
156 provider.AddPath( { featureId( "vertical", "edge" ), SEG( { 20, -10 }, { 20, 10 } ) } );
157
158 SNAP_SOURCE_CONTEXT context;
159 context.sourcePoint = { 20, 0 };
160
161 std::vector<SNAP_CANDIDATE> finite = provider.CollectObjectGeometry( context, 25 );
162 BOOST_CHECK( std::none_of( finite.begin(), finite.end(),
163 []( const SNAP_CANDIDATE& aCandidate )
164 {
165 return aCandidate.subtype == SNAP_CANDIDATE_SUBTYPE::INTERSECTION;
166 } ) );
167
168 provider.ActivateExtension( featureId( "horizontal", "edge" ) );
169 std::vector<SNAP_CANDIDATE> extended = provider.CollectObjectGeometry( context, 25 );
170 BOOST_CHECK( std::any_of( extended.begin(), extended.end(),
171 []( const SNAP_CANDIDATE& aCandidate )
172 {
173 return aCandidate.subtype == SNAP_CANDIDATE_SUBTYPE::INTERSECTION;
174 } ) );
175}
176
177
178BOOST_AUTO_TEST_CASE( IntersectionBranchIdentityDoesNotFollowCursorDistance )
179{
181 SNAP_STABLE_ID first = featureId( "first", "circle" );
182 first.featureIndex = 2;
183 SNAP_STABLE_ID second = featureId( "second", "circle" );
184 second.solutionBranch = 3;
185 provider.AddPath( { first, CIRCLE( { -3, 0 }, 5 ) } );
186 provider.AddPath( { second, CIRCLE( { 3, 0 }, 5 ) } );
187
188 const auto intersectionIds = [&]( const VECTOR2I& aCursor )
189 {
190 SNAP_SOURCE_CONTEXT context;
191 context.sourcePoint = aCursor;
192 std::map<VECTOR2I, SNAP_STABLE_ID> ids;
193
194 for( const SNAP_CANDIDATE& candidate : provider.CollectObjectGeometry( context, 20 ) )
195 {
196 if( candidate.subtype == SNAP_CANDIDATE_SUBTYPE::INTERSECTION )
197 {
198 ids.emplace( VECTOR2I( KiROUND( candidate.origin.x ), KiROUND( candidate.origin.y ) ), candidate.id );
199 }
200 }
201
202 return ids;
203 };
204
205 auto upperCursor = intersectionIds( { 0, 10 } );
206 auto lowerCursor = intersectionIds( { 0, -10 } );
207 BOOST_REQUIRE_EQUAL( upperCursor.size(), 2 );
208 BOOST_CHECK( upperCursor == lowerCursor );
209 BOOST_CHECK( upperCursor.begin()->second.kind == SNAP_ID_KIND::INTERSECTION );
210 BOOST_CHECK( upperCursor.begin()->second.target
211 == MakeIntersectionSnapId( first, second, upperCursor.begin()->second.solutionBranch ).target );
212}
213
214
215BOOST_AUTO_TEST_CASE( IntersectionBranchIdentitySurvivesSiblingRadiusFiltering )
216{
218 provider.AddPath( { featureId( "first", "circle" ), CIRCLE( { -3, 0 }, 5 ) } );
219 provider.AddPath( { featureId( "second", "circle" ), CIRCLE( { 3, 0 }, 5 ) } );
220
221 SNAP_SOURCE_CONTEXT context;
222 context.sourcePoint = { 0, 4 };
223
224 const auto upperIntersectionId = [&]( int aRadius )
225 {
226 for( const SNAP_CANDIDATE& candidate : provider.CollectObjectGeometry( context, aRadius ) )
227 {
228 if( candidate.subtype == SNAP_CANDIDATE_SUBTYPE::INTERSECTION && candidate.origin == VECTOR2D( 0.0, 4.0 ) )
229 {
230 return std::optional<SNAP_STABLE_ID>( candidate.id );
231 }
232 }
233
234 return std::optional<SNAP_STABLE_ID>();
235 };
236
237 std::optional<SNAP_STABLE_ID> isolated = upperIntersectionId( 1 );
238 std::optional<SNAP_STABLE_ID> withSibling = upperIntersectionId( 10 );
239 BOOST_REQUIRE( isolated );
240 BOOST_REQUIRE( withSibling );
241 BOOST_CHECK( *isolated == *withSibling );
242}
243
244
245BOOST_AUTO_TEST_CASE( ActiveExtensionRayRetainsItsFiniteDirection )
246{
248 provider.AddPath(
249 { featureId( "line", "extension" ), HALF_LINE( VECTOR2I( 0, 0 ), VECTOR2I( 10, 0 ) ), false, true } );
250
251 SNAP_SOURCE_CONTEXT context;
252 context.sourcePoint = { -20, 0 };
253
254 BOOST_CHECK( provider.CollectObjectGeometry( context, 10 ).empty() );
255
256 context.sourcePoint = { 5, 2 };
257 std::vector<SNAP_CANDIDATE> candidates = provider.CollectObjectGeometry( context, 10 );
258
259 BOOST_REQUIRE_EQUAL( candidates.size(), 1 );
260 BOOST_CHECK( candidates.front().subtype == SNAP_CANDIDATE_SUBTYPE::ACTIVE_EXTENSION );
261 BOOST_CHECK( candidates.front().relation == SNAP_RELATION::POINT_ON_RAY );
262
264 resolver.AddCandidate( candidates.front() );
265 SNAP_RESULT result = resolver.Resolve( context );
266 BOOST_CHECK_EQUAL( result.position, VECTOR2I( 5, 0 ) );
267}
268
269
270BOOST_AUTO_TEST_CASE( MovingFeaturesExcludedButStationarySelfFeaturesRemain )
271{
273 provider.AddPath( { featureId( "polygon", "moving-edge" ), SEG( { 0, 0 }, { 10, 0 } ) } );
274 provider.AddPath( { featureId( "polygon", "fixed-edge" ), SEG( { 0, 10 }, { 10, 10 } ) } );
275
276 SNAP_SOURCE_CONTEXT context;
277 context.sourcePoint = { 5, 5 };
278 context.movingFeatures.push_back( featureId( "polygon", "moving-edge" ) );
279 context.stationarySelfFeatures.push_back( featureId( "polygon", "fixed-edge" ) );
280
281 std::vector<SNAP_CANDIDATE> candidates = provider.CollectObjectGeometry( context, 20 );
282
283 BOOST_CHECK( std::none_of( candidates.begin(), candidates.end(),
284 []( const SNAP_CANDIDATE& aCandidate )
285 {
286 return aCandidate.id == featureId( "polygon", "moving-edge" );
287 } ) );
288 BOOST_CHECK( std::any_of( candidates.begin(), candidates.end(),
289 []( const SNAP_CANDIDATE& aCandidate )
290 {
291 return aCandidate.id == featureId( "polygon", "fixed-edge" );
292 } ) );
293}
294
295
296BOOST_AUTO_TEST_CASE( CircleNormalEnumeratesBothFiniteBranches )
297{
299 provider.AddPath( { featureId( "circle", "outline" ), CIRCLE( { 0, 0 }, 5 ) } );
300
301 SNAP_SOURCE_CONTEXT context;
302 context.sourcePoint = { 8, 1 };
303 context.stationarySourceLeg = VECTOR2I( 10, 0 );
304
305 std::vector<SNAP_CANDIDATE> candidates = provider.CollectTangentNormal( context, 20, false, true );
306
307 BOOST_REQUIRE_EQUAL( candidates.size(), 2 );
308 BOOST_CHECK( std::any_of( candidates.begin(), candidates.end(),
309 []( const SNAP_CANDIDATE& aCandidate )
310 {
311 return aCandidate.origin == VECTOR2D( 5.0, 0.0 );
312 } ) );
313 BOOST_CHECK( std::any_of( candidates.begin(), candidates.end(),
314 []( const SNAP_CANDIDATE& aCandidate )
315 {
316 return aCandidate.origin == VECTOR2D( -5.0, 0.0 );
317 } ) );
318}
319
320
321BOOST_AUTO_TEST_CASE( NormalBranchIdentityDoesNotDependOnTangentSetting )
322{
324 provider.AddPath( { featureId( "circle", "outline" ), CIRCLE( { 0, 0 }, 5 ) } );
325
326 SNAP_SOURCE_CONTEXT context;
327 context.sourcePoint = { 5, 0 };
328 context.stationarySourceLeg = VECTOR2I( 10, 0 );
329
330 const auto positiveNormalId = [&]( bool aTangentEnabled )
331 {
332 for( const SNAP_CANDIDATE& candidate : provider.CollectTangentNormal( context, 20, aTangentEnabled, true ) )
333 {
334 if( candidate.relation == SNAP_RELATION::NORMAL && candidate.origin == VECTOR2D( 5.0, 0.0 ) )
335 {
336 return std::optional<SNAP_STABLE_ID>( candidate.id );
337 }
338 }
339
340 return std::optional<SNAP_STABLE_ID>();
341 };
342
343 std::optional<SNAP_STABLE_ID> withoutTangents = positiveNormalId( false );
344 std::optional<SNAP_STABLE_ID> withTangents = positiveNormalId( true );
345 BOOST_REQUIRE( withoutTangents );
346 BOOST_REQUIRE( withTangents );
347 BOOST_CHECK( *withoutTangents == *withTangents );
348}
349
350
351BOOST_AUTO_TEST_CASE( ArcNormalRejectsContactOutsideFiniteDomain )
352{
354 provider.AddPath( { featureId( "arc", "outline" ), SHAPE_ARC( VECTOR2I( 0, 0 ), VECTOR2I( 5, 0 ), ANGLE_90 ) } );
355
356 SNAP_SOURCE_CONTEXT context;
357 context.sourcePoint = { 5, 1 };
358 context.stationarySourceLeg = VECTOR2I( 10, 0 );
359
360 std::vector<SNAP_CANDIDATE> candidates = provider.CollectTangentNormal( context, 20, false, true );
361
362 BOOST_REQUIRE_EQUAL( candidates.size(), 1 );
363 BOOST_CHECK_GT( candidates.front().origin.x, 0.0 );
364 BOOST_CHECK( candidates.front().relation == SNAP_RELATION::NORMAL );
365
367 resolver.AddCandidate( candidates.front() );
368 SNAP_RESULT result = resolver.Resolve( context );
369 BOOST_CHECK_EQUAL( result.position,
370 VECTOR2I( KiROUND( candidates.front().origin.x ), KiROUND( candidates.front().origin.y ) ) );
371}
372
373
374BOOST_AUTO_TEST_CASE( CircleRemainsComposableManifold )
375{
377 provider.AddPath( { featureId( "circle", "outline" ), CIRCLE( { 0, 0 }, 5 ) } );
378
379 SNAP_SOURCE_CONTEXT context;
380 context.sourcePoint = { 4, 4 };
381
383
384 for( SNAP_CANDIDATE& candidate : provider.CollectObjectGeometry( context, 20 ) )
385 resolver.AddCandidate( std::move( candidate ) );
386
387 resolver.AddCandidate( SNAP_CANDIDATE::AxisY( featureId( "grid", "y" ), SNAP_PRIORITY_TIER::GRID,
389
390 SNAP_RESULT result = resolver.Resolve( context );
391
392 BOOST_CHECK_EQUAL( result.position, VECTOR2I( 4, 3 ) );
393 BOOST_CHECK( result.Accepted( featureId( "circle", "outline" ) ) );
394 BOOST_CHECK( result.Accepted( featureId( "grid", "y" ) ) );
395 BOOST_CHECK_EQUAL( result.remainingDof, 0 );
396}
397
398
399BOOST_AUTO_TEST_CASE( BoundingBoxFeaturesTranslateSourcePointExactly )
400{
402 provider.AddBounds( { featureId( "target", "bounds" ), BOX2I( { 0, 0 }, { 20, 20 } ) } );
403
404 SNAP_SOURCE_CONTEXT context;
405 context.sourcePoint = { 45, 45 };
406 context.movingBounds = BOX2I( { 40, 40 }, { 10, 10 } );
407
408 std::vector<SNAP_CANDIDATE> candidates = provider.CollectAlignment( context, 50 );
409
410 BOOST_CHECK( std::any_of( candidates.begin(), candidates.end(),
411 []( const SNAP_CANDIDATE& aCandidate )
412 {
413 return aCandidate.relation == SNAP_RELATION::BBOX_ALIGNMENT
414 && aCandidate.origin.x == 5.0;
415 } ) );
416 BOOST_CHECK( std::any_of( candidates.begin(), candidates.end(),
417 []( const SNAP_CANDIDATE& aCandidate )
418 {
419 return aCandidate.relation == SNAP_RELATION::BBOX_ALIGNMENT
420 && aCandidate.origin.y == 25.0;
421 } ) );
422
423 auto xCandidate =
424 std::find_if( candidates.begin(), candidates.end(),
425 []( const SNAP_CANDIDATE& aCandidate )
426 {
427 return aCandidate.relation == SNAP_RELATION::BBOX_ALIGNMENT && aCandidate.origin.x == 5.0;
428 } );
429 BOOST_REQUIRE( xCandidate != candidates.end() );
431 resolver.AddCandidate( *xCandidate );
432 BOOST_CHECK_EQUAL( resolver.Resolve( context ).position, VECTOR2I( 5, 45 ) );
433}
434
435
436BOOST_AUTO_TEST_CASE( ProjectedMovingBoundsDoNotOscillateAfterSnap )
437{
439 provider.AddBounds( { featureId( "target", "bounds" ), BOX2I( { 0, 0 }, { 20, 20 } ) } );
440
441 SNAP_SOURCE_CONTEXT context;
442 context.sourcePoint = { 45, 48 };
443 context.movingReferencePoint = VECTOR2I( 45, 45 );
444 context.movingBounds = BOX2I( { 5, 5 }, { 10, 10 } );
445
446 std::vector<SNAP_CANDIDATE> candidates = provider.CollectAlignment( context, 50 );
447 auto centerToCenter =
448 std::find_if( candidates.begin(), candidates.end(),
449 []( const SNAP_CANDIDATE& aCandidate )
450 {
451 return aCandidate.id.kind == SNAP_ID_KIND::BOUNDS_Y && aCandidate.id.featureIndex == 4;
452 } );
453
454 BOOST_REQUIRE( centerToCenter != candidates.end() );
455 BOOST_CHECK_EQUAL( centerToCenter->origin.y, 45.0 );
456
458 resolver.SetStickyCandidates( { centerToCenter->id } );
459 resolver.AddCandidate( *centerToCenter );
460
461 BOOST_CHECK_EQUAL( resolver.Resolve( context ).position.y, 45 );
462}
463
464
465BOOST_AUTO_TEST_CASE( AlignmentGuideIdentifiesMatchedTargetFeature )
466{
468 provider.AddBounds( { featureId( "target", "bounds" ), BOX2I( { 0, 0 }, { 20, 20 } ) } );
469
470 SNAP_SOURCE_CONTEXT context;
471 context.sourcePoint = { 45, 48 };
472 context.movingReferencePoint = VECTOR2I( 45, 45 );
473 context.movingBounds = BOX2I( { 5, 5 }, { 10, 10 } );
474
475 std::vector<SNAP_CANDIDATE> candidates = provider.CollectAlignment( context, 50 );
476 auto centerToCenter =
477 std::find_if( candidates.begin(), candidates.end(),
478 []( const SNAP_CANDIDATE& aCandidate )
479 {
480 return aCandidate.id.kind == SNAP_ID_KIND::BOUNDS_Y && aCandidate.id.featureIndex == 4;
481 } );
482
483 BOOST_REQUIRE( centerToCenter != candidates.end() );
484 BOOST_REQUIRE_EQUAL( centerToCenter->guides.size(), 1 );
485 BOOST_CHECK_EQUAL( centerToCenter->guides.front().start, VECTOR2I( 0, 10 ) );
486 BOOST_CHECK_EQUAL( centerToCenter->guides.front().end, VECTOR2I( 20, 10 ) );
487}
488
489
490BOOST_AUTO_TEST_CASE( BoundsReferencePrefersMatchingFeatures )
491{
493 provider.AddBounds( { featureId( "target", "bounds" ), BOX2I( { 0, 0 }, { 20, 20 } ) } );
494
495 SNAP_SOURCE_CONTEXT context;
496 context.sourcePoint = { 45, 45 };
497 context.movingBounds = BOX2I( { 40, 40 }, { 10, 10 } );
499
500 std::vector<SNAP_CANDIDATE> candidates = provider.CollectAlignment( context, 50 );
501 auto centerToCenter =
502 std::find_if( candidates.begin(), candidates.end(),
503 []( const SNAP_CANDIDATE& aCandidate )
504 {
505 return aCandidate.id.kind == SNAP_ID_KIND::BOUNDS_X && aCandidate.id.featureIndex == 4;
506 } );
507 auto leftToLeft =
508 std::find_if( candidates.begin(), candidates.end(),
509 []( const SNAP_CANDIDATE& aCandidate )
510 {
511 return aCandidate.id.kind == SNAP_ID_KIND::BOUNDS_X && aCandidate.id.featureIndex == 0;
512 } );
513
514 BOOST_REQUIRE( centerToCenter != candidates.end() );
515 BOOST_REQUIRE( leftToLeft != candidates.end() );
516 BOOST_CHECK_EQUAL( centerToCenter->referenceAffinity, 0 );
517 BOOST_CHECK_GT( leftToLeft->referenceAffinity, centerToCenter->referenceAffinity );
518}
519
520
521BOOST_AUTO_TEST_CASE( PadCenterReferencePrefersPadCenterTargets )
522{
524 provider.AddBounds( { featureId( "target", "bounds" ), BOX2I( { 0, 0 }, { 20, 20 } ) } );
525 provider.AddAlignmentPoint(
526 { featureId( "target-pad", "center" ), { 0, 10 } } );
527
528 SNAP_SOURCE_CONTEXT context;
529 context.sourcePoint = { 2, 40 };
530 context.movingBounds = BOX2I( { 2, 35 }, { 10, 10 } );
532
533 std::vector<SNAP_CANDIDATE> candidates = provider.CollectAlignment( context, 50 );
534 auto padCenter = std::find_if( candidates.begin(), candidates.end(),
535 []( const SNAP_CANDIDATE& aCandidate )
536 {
537 return aCandidate.id.kind == SNAP_ID_KIND::ANCHOR_POINT_X;
538 } );
539 auto boundsFallback = std::find_if( candidates.begin(), candidates.end(),
540 []( const SNAP_CANDIDATE& aCandidate )
541 {
542 return aCandidate.id.kind == SNAP_ID_KIND::BOUNDS_X;
543 } );
544
545 BOOST_REQUIRE( padCenter != candidates.end() );
546 BOOST_REQUIRE( boundsFallback != candidates.end() );
547 BOOST_CHECK_EQUAL( padCenter->origin.x, 0.0 );
548 BOOST_CHECK_EQUAL( padCenter->referenceAffinity, 0 );
549 BOOST_CHECK_GT( boundsFallback->referenceAffinity, padCenter->referenceAffinity );
550}
551
552
553BOOST_AUTO_TEST_CASE( AlignmentAcceptsOneFeatureIdentityPerAxis )
554{
556 provider.AddBounds( { featureId( "target", "bounds" ), BOX2I( { 0, 0 }, { 20, 20 } ) } );
557
558 SNAP_SOURCE_CONTEXT context;
559 context.sourcePoint = { 45, 45 };
560 context.movingReferencePoint = VECTOR2I( 45, 45 );
561 context.movingBounds = BOX2I( { 5, 5 }, { 20, 20 } );
562
564
565 for( SNAP_CANDIDATE candidate : provider.CollectAlignment( context, 50 ) )
566 resolver.AddCandidate( std::move( candidate ) );
567
568 SNAP_RESULT result = resolver.Resolve( context );
569 size_t xCount = 0;
570 size_t yCount = 0;
571
572 for( const SNAP_STABLE_ID& accepted : result.accepted )
573 {
574 xCount += accepted.kind == SNAP_ID_KIND::BOUNDS_X;
575 yCount += accepted.kind == SNAP_ID_KIND::BOUNDS_Y;
576 }
577
578 BOOST_CHECK_EQUAL( xCount, 1 );
579 BOOST_CHECK_EQUAL( yCount, 1 );
580}
581
582
583BOOST_AUTO_TEST_CASE( DenseAlignmentBoundsCandidateCountBeforeResolution )
584{
586
587 for( int index = 0; index < 1000; ++index )
588 {
589 provider.AddBounds( { featureId( std::to_string( index ).c_str(), "bounds" ),
590 BOX2I( { index % 5, index % 7 }, { 20, 20 } ) } );
591 }
592
593 SNAP_SOURCE_CONTEXT context;
594 context.sourcePoint = { 10, 10 };
595 context.movingBounds = BOX2I( { 0, 0 }, { 20, 20 } );
596
597 BOOST_CHECK_EQUAL( provider.CollectAlignment( context, 50 ).size(), 128 );
598}
599
600
601BOOST_AUTO_TEST_CASE( LayoutInferenceIsInvariantUnderAxisTranspose )
602{
603 const std::array<SNAP_OBJECT_BOUNDS, 2> bounds = {
604 SNAP_OBJECT_BOUNDS{ featureId( "first", "bounds" ), BOX2I( { 0, 0 }, { 10, 12 } ) },
605 SNAP_OBJECT_BOUNDS{ featureId( "second", "bounds" ), BOX2I( { 30, 1 }, { 10, 9 } ) }
606 };
607 const SNAP_ALIGNMENT_POINT padCenter{ featureId( "pad", "center" ),
608 { 33, 6 } };
609
610 const auto makeProvider = [&]( bool aTranspose )
611 {
613
614 for( SNAP_OBJECT_BOUNDS bound : bounds )
615 {
616 if( aTranspose )
617 bound.bounds = transpose( bound.bounds );
618
619 provider.AddBounds( std::move( bound ) );
620 }
621
622 SNAP_ALIGNMENT_POINT point = padCenter;
623
624 if( aTranspose )
625 point.position = transpose( point.position );
626
627 provider.AddAlignmentPoint( std::move( point ) );
628 return provider;
629 };
630
631 const auto makeContext = []( SNAP_REFERENCE_KIND aReferenceKind, bool aTranspose )
632 {
633 SNAP_SOURCE_CONTEXT context;
634 context.sourcePoint = { 53, 7 };
635 context.movingBounds = BOX2I( { 50, 2 }, { 10, 8 } );
636 context.movingReferencePoint = VECTOR2I( 55, 6 );
637 context.referencePreference = { aReferenceKind, 2, 0 };
638
639 if( aTranspose )
640 {
641 context.sourcePoint = transpose( context.sourcePoint );
642 context.movingBounds = transpose( *context.movingBounds );
643 context.movingReferencePoint = transpose( *context.movingReferencePoint );
645 }
646
647 return context;
648 };
649
651 {
652 SNAP_INFERENCE_PROVIDER original = makeProvider( false );
653 SNAP_INFERENCE_PROVIDER transposed = makeProvider( true );
654 checkTransposeEquivalent( original.CollectAlignment( makeContext( reference, false ), 100 ),
655 transposed.CollectAlignment( makeContext( reference, true ), 100 ) );
656 }
657
658 SNAP_INFERENCE_PROVIDER original = makeProvider( false );
659 SNAP_INFERENCE_PROVIDER transposed = makeProvider( true );
660 std::vector<SNAP_CANDIDATE> originalSpacing =
661 original.CollectEqualSpacing( makeContext( SNAP_REFERENCE_KIND::NONE, false ), 100 );
662 std::vector<SNAP_CANDIDATE> transposedSpacing =
663 transposed.CollectEqualSpacing( makeContext( SNAP_REFERENCE_KIND::NONE, true ), 100 );
664
665 for( int branch : { -1, 0, 1 } )
666 {
667 BOOST_CHECK( std::any_of( originalSpacing.begin(), originalSpacing.end(),
668 [&]( const SNAP_CANDIDATE& aCandidate )
669 {
670 return aCandidate.id.solutionBranch == branch
671 && ( aCandidate.id.kind == SNAP_ID_KIND::EQUAL_GAP_X
672 || aCandidate.id.kind == SNAP_ID_KIND::COPY_GAP_X );
673 } ) );
674 }
675
676 checkTransposeEquivalent( std::move( originalSpacing ), std::move( transposedSpacing ) );
677}
678
679
680BOOST_AUTO_TEST_CASE( EqualGapCentersBetweenImmediateNeighbors )
681{
683 provider.AddBounds( { featureId( "left", "bounds" ), BOX2I( { 0, 0 }, { 10, 10 } ) } );
684 provider.AddBounds( { featureId( "right", "bounds" ), BOX2I( { 30, 0 }, { 10, 10 } ) } );
685
686 SNAP_SOURCE_CONTEXT context;
687 context.sourcePoint = { 55, 5 };
688 context.movingBounds = BOX2I( { 50, 0 }, { 10, 10 } );
689
690 std::vector<SNAP_CANDIDATE> candidates = provider.CollectEqualSpacing( context, 100 );
691
692 BOOST_CHECK( std::any_of( candidates.begin(), candidates.end(),
693 []( const SNAP_CANDIDATE& aCandidate )
694 {
695 return aCandidate.id.kind == SNAP_ID_KIND::EQUAL_GAP_X
696 && aCandidate.relation == SNAP_RELATION::BBOX_EQUAL_GAP
697 && aCandidate.origin.x == 20.0;
698 } ) );
699
700 auto equalGap = std::find_if( candidates.begin(), candidates.end(),
701 []( const SNAP_CANDIDATE& aCandidate )
702 {
703 return aCandidate.id.kind == SNAP_ID_KIND::EQUAL_GAP_X;
704 } );
705 BOOST_REQUIRE( equalGap != candidates.end() );
707 resolver.AddCandidate( *equalGap );
708 BOOST_CHECK_EQUAL( resolver.Resolve( context ).position, VECTOR2I( 20, 5 ) );
709}
710
711
712BOOST_AUTO_TEST_CASE( EqualHorizontalGapCarriesBothBracketSpans )
713{
715 provider.AddBounds( { featureId( "left", "bounds" ), BOX2I( { 0, 0 }, { 10, 10 } ) } );
716 provider.AddBounds( { featureId( "right", "bounds" ), BOX2I( { 30, 0 }, { 10, 10 } ) } );
717
718 SNAP_SOURCE_CONTEXT context;
719 context.sourcePoint = { 55, 5 };
720 context.movingBounds = BOX2I( { 50, 0 }, { 10, 10 } );
721
722 std::vector<SNAP_CANDIDATE> candidates = provider.CollectEqualSpacing( context, 100 );
723 auto equalGap = std::find_if( candidates.begin(), candidates.end(),
724 []( const SNAP_CANDIDATE& aCandidate )
725 {
726 return aCandidate.id.kind == SNAP_ID_KIND::EQUAL_GAP_X;
727 } );
728
729 BOOST_REQUIRE( equalGap != candidates.end() );
730 BOOST_REQUIRE_EQUAL( equalGap->guides.size(), 2 );
731 BOOST_CHECK( equalGap->guides[0].style == SNAP_GUIDE_STYLE::DIMENSION_BRACKET );
732 BOOST_CHECK_EQUAL( equalGap->guides[0].start, VECTOR2I( 10, 10 ) );
733 BOOST_CHECK_EQUAL( equalGap->guides[0].end, VECTOR2I( 15, 10 ) );
734 BOOST_CHECK( equalGap->guides[1].style == SNAP_GUIDE_STYLE::DIMENSION_BRACKET );
735 BOOST_CHECK_EQUAL( equalGap->guides[1].start, VECTOR2I( 25, 10 ) );
736 BOOST_CHECK_EQUAL( equalGap->guides[1].end, VECTOR2I( 30, 10 ) );
737}
738
739
740BOOST_AUTO_TEST_CASE( EqualVerticalGapCarriesBothBracketSpans )
741{
743 provider.AddBounds( { featureId( "top", "bounds" ), BOX2I( { 0, 0 }, { 10, 10 } ) } );
744 provider.AddBounds( { featureId( "bottom", "bounds" ), BOX2I( { 0, 30 }, { 10, 10 } ) } );
745
746 SNAP_SOURCE_CONTEXT context;
747 context.sourcePoint = { 5, 55 };
748 context.movingBounds = BOX2I( { 0, 50 }, { 10, 10 } );
749
750 std::vector<SNAP_CANDIDATE> candidates = provider.CollectEqualSpacing( context, 100 );
751 auto equalGap = std::find_if( candidates.begin(), candidates.end(),
752 []( const SNAP_CANDIDATE& aCandidate )
753 {
754 return aCandidate.id.kind == SNAP_ID_KIND::EQUAL_GAP_Y;
755 } );
756
757 BOOST_REQUIRE( equalGap != candidates.end() );
758 BOOST_REQUIRE_EQUAL( equalGap->guides.size(), 2 );
759 BOOST_CHECK( equalGap->guides[0].style == SNAP_GUIDE_STYLE::DIMENSION_BRACKET );
760 BOOST_CHECK_EQUAL( equalGap->guides[0].start, VECTOR2I( 10, 10 ) );
761 BOOST_CHECK_EQUAL( equalGap->guides[0].end, VECTOR2I( 10, 15 ) );
762 BOOST_CHECK( equalGap->guides[1].style == SNAP_GUIDE_STYLE::DIMENSION_BRACKET );
763 BOOST_CHECK_EQUAL( equalGap->guides[1].start, VECTOR2I( 10, 25 ) );
764 BOOST_CHECK_EQUAL( equalGap->guides[1].end, VECTOR2I( 10, 30 ) );
765}
766
767
768BOOST_AUTO_TEST_CASE( EqualGapPreservesSourceOffsetFromMovingBounds )
769{
771 provider.AddBounds( { featureId( "left", "bounds" ), BOX2I( { 0, 0 }, { 10, 10 } ) } );
772 provider.AddBounds( { featureId( "right", "bounds" ), BOX2I( { 30, 0 }, { 10, 10 } ) } );
773
774 SNAP_SOURCE_CONTEXT context;
775 context.sourcePoint = { 52, 2 };
776 context.movingBounds = BOX2I( { 50, 0 }, { 10, 10 } );
777
778 std::vector<SNAP_CANDIDATE> candidates = provider.CollectEqualSpacing( context, 100 );
779
780 BOOST_CHECK( std::any_of( candidates.begin(), candidates.end(),
781 []( const SNAP_CANDIDATE& aCandidate )
782 {
783 return aCandidate.id.kind == SNAP_ID_KIND::EQUAL_GAP_X && aCandidate.origin.x == 17.0;
784 } ) );
785}
786
787
788BOOST_AUTO_TEST_CASE( EqualGapIgnoresReferencePreference )
789{
791 provider.AddBounds( { featureId( "left", "bounds" ), BOX2I( { 0, 0 }, { 10, 10 } ) } );
792 provider.AddBounds( { featureId( "right", "bounds" ), BOX2I( { 30, 0 }, { 10, 10 } ) } );
793
794 SNAP_SOURCE_CONTEXT context;
795 context.sourcePoint = { 52, 2 };
796 context.movingBounds = BOX2I( { 50, 0 }, { 10, 10 } );
798
799 std::vector<SNAP_CANDIDATE> candidates = provider.CollectEqualSpacing( context, 100 );
800 auto equalGap = std::find_if( candidates.begin(), candidates.end(),
801 []( const SNAP_CANDIDATE& aCandidate )
802 {
803 return aCandidate.id.kind == SNAP_ID_KIND::EQUAL_GAP_X;
804 } );
805
806 BOOST_REQUIRE( equalGap != candidates.end() );
807 BOOST_CHECK_EQUAL( equalGap->origin.x, 17.0 );
808 BOOST_CHECK_EQUAL( equalGap->referenceAffinity, 0 );
809}
810
811
812BOOST_AUTO_TEST_CASE( EqualGapCopiesAdjacentStationaryGap )
813{
815 provider.AddBounds( { featureId( "first", "bounds" ), BOX2I( { 0, 0 }, { 10, 10 } ) } );
816 provider.AddBounds( { featureId( "second", "bounds" ), BOX2I( { 20, 0 }, { 10, 10 } ) } );
817
818 SNAP_SOURCE_CONTEXT context;
819 context.sourcePoint = { 44, 5 };
820 context.movingBounds = BOX2I( { 39, 0 }, { 10, 10 } );
821
822 std::vector<SNAP_CANDIDATE> candidates = provider.CollectEqualSpacing( context, 20 );
823
824 auto copiedGap = std::find_if( candidates.begin(), candidates.end(),
825 []( const SNAP_CANDIDATE& aCandidate )
826 {
827 return aCandidate.id.kind == SNAP_ID_KIND::COPY_GAP_X
828 && aCandidate.id.solutionBranch > 0 && aCandidate.origin.x == 45.0;
829 } );
830
831 BOOST_REQUIRE( copiedGap != candidates.end() );
832 BOOST_REQUIRE_EQUAL( copiedGap->guides.size(), 2 );
833 BOOST_CHECK_EQUAL( copiedGap->guides[0].start, VECTOR2I( 10, 10 ) );
834 BOOST_CHECK_EQUAL( copiedGap->guides[0].end, VECTOR2I( 20, 10 ) );
835 BOOST_CHECK_EQUAL( copiedGap->guides[1].start, VECTOR2I( 30, 10 ) );
836 BOOST_CHECK_EQUAL( copiedGap->guides[1].end, VECTOR2I( 40, 10 ) );
837}
838
839
840BOOST_AUTO_TEST_CASE( EqualGapCopiesAdjacentStationaryGapBeforePair )
841{
843 provider.AddBounds( { featureId( "first", "bounds" ), BOX2I( { 0, 0 }, { 10, 10 } ) } );
844 provider.AddBounds( { featureId( "second", "bounds" ), BOX2I( { 20, 0 }, { 10, 10 } ) } );
845
846 SNAP_SOURCE_CONTEXT context;
847 context.sourcePoint = { -14, 5 };
848 context.movingBounds = BOX2I( { -19, 0 }, { 10, 10 } );
849
850 std::vector<SNAP_CANDIDATE> candidates = provider.CollectEqualSpacing( context, 20 );
851 auto copiedGap = std::find_if( candidates.begin(), candidates.end(),
852 []( const SNAP_CANDIDATE& aCandidate )
853 {
854 return aCandidate.id.kind == SNAP_ID_KIND::COPY_GAP_X
855 && aCandidate.id.solutionBranch < 0 && aCandidate.origin.x == -15.0;
856 } );
857
858 BOOST_REQUIRE( copiedGap != candidates.end() );
859 BOOST_REQUIRE_EQUAL( copiedGap->guides.size(), 2 );
860 BOOST_CHECK_EQUAL( copiedGap->guides[0].start, VECTOR2I( -10, 10 ) );
861 BOOST_CHECK_EQUAL( copiedGap->guides[0].end, VECTOR2I( 0, 10 ) );
862 BOOST_CHECK_EQUAL( copiedGap->guides[1].start, VECTOR2I( 10, 10 ) );
863 BOOST_CHECK_EQUAL( copiedGap->guides[1].end, VECTOR2I( 20, 10 ) );
864}
865
866
867BOOST_AUTO_TEST_CASE( DenseEqualSpacingBoundsCandidateCountBeforeResolution )
868{
870
871 for( int index = 0; index < 1000; ++index )
872 {
873 provider.AddBounds(
874 { featureId( std::to_string( index ).c_str(), "bounds" ), BOX2I( { index * 30, 0 }, { 10, 10 } ) } );
875 }
876
877 SNAP_SOURCE_CONTEXT context;
878 context.sourcePoint = { 0, 5 };
879 context.movingBounds = BOX2I( { -10, 0 }, { 10, 10 } );
880
881 BOOST_CHECK_EQUAL( provider.CollectEqualSpacing( context, 100000 ).size(), 128 );
882}
883
884
885BOOST_AUTO_TEST_CASE( LayoutRejectsMovingSelectionDescendants )
886{
888 provider.AddBounds( { featureId( "child", "bounds" ), BOX2I( { 0, 0 }, { 10, 10 } ),
889 featureId( "selection", "bounds" ).target } );
890
891 SNAP_SOURCE_CONTEXT context;
892 context.sourcePoint = { 12, 5 };
893 context.movingBounds = BOX2I( { 10, 0 }, { 10, 10 } );
894 context.movingItem = featureId( "selection", "bounds" );
895
896 BOOST_CHECK( provider.CollectAlignment( context, 50 ).empty() );
897 BOOST_CHECK( provider.CollectEqualSpacing( context, 50 ).empty() );
898}
899
900
901BOOST_AUTO_TEST_CASE( CandidateDensityP95StaysWithinFourMilliseconds )
902{
904
905 for( int i = 0; i < 10000; ++i )
906 {
907 provider.AddPath( { featureId( std::to_string( i ).c_str(), "edge" ),
908 SEG( VECTOR2I( 0, i % 20 ), VECTOR2I( 50, i % 20 ) ) } );
909 }
910
911 SNAP_SOURCE_CONTEXT context;
912 context.sourcePoint = { 10, 0 };
913 std::vector<std::chrono::microseconds> samples;
914
915 for( int run = 0; run < 20; ++run )
916 {
917 std::clock_t start = std::clock();
918 std::vector<SNAP_CANDIDATE> candidates = provider.CollectObjectGeometry( context, 25 );
919 samples.emplace_back( ( std::clock() - start ) * 1000000 / CLOCKS_PER_SEC );
920 BOOST_CHECK_EQUAL( candidates.size(), 64 );
921 }
922
923 std::sort( samples.begin(), samples.end() );
924 std::chrono::microseconds p95 = samples[18];
925 BOOST_TEST_MESSAGE( "10k-object candidate generation p95: " << p95.count() << " us" );
926 BOOST_CHECK_LE( p95.count(), 4000 );
927}
928
929
int index
BOX2< VECTOR2I > BOX2I
Definition box2.h:918
constexpr BOX2I KiROUND(const BOX2D &aBoxD)
Definition box2.h:986
constexpr const Vec & GetOrigin() const
Definition box2.h:206
constexpr const SizeVec & GetSize() const
Definition box2.h:202
size_t Hash() const
Definition kiid.cpp:231
std::array< uint8_t, 16 > AsBytes() const
Definition kiid.cpp:276
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
Definition seg.h:38
std::vector< SNAP_CANDIDATE > CollectObjectGeometry(const SNAP_SOURCE_CONTEXT &aContext, int aRadius) const
std::vector< SNAP_CANDIDATE > CollectEqualSpacing(const SNAP_SOURCE_CONTEXT &aContext, int aRadius) const
void AddBounds(SNAP_OBJECT_BOUNDS aBounds)
void ActivateExtension(const SNAP_STABLE_ID &aId)
std::vector< SNAP_CANDIDATE > CollectTangentNormal(const SNAP_SOURCE_CONTEXT &aContext, int aRadius, bool aTangentEnabled, bool aNormalEnabled) const
void AddPath(SNAP_OBJECT_PATH aPath)
void AddAlignmentPoint(SNAP_ALIGNMENT_POINT aPoint)
std::vector< SNAP_CANDIDATE > CollectAlignment(const SNAP_SOURCE_CONTEXT &aContext, int aRadius) const
Define a general 2D-vector/point.
Definition vector2d.h:67
static constexpr EDA_ANGLE ANGLE_90
Definition eda_angle.h:413
static FILENAME_RESOLVER * resolver
SNAP_REFERENCE_KIND
SNAP_STABLE_ID MakeIntersectionSnapId(const SNAP_STABLE_ID &aFirst, const SNAP_STABLE_ID &aSecond, int aSolutionBranch)
SNAP_ID_KIND
std::array< uint8_t, 16 > SNAP_TARGET_ID
A named point a parent object offers for alignment, such as a pad center or a pin end.
SNAP_STABLE_ID id
static SNAP_CANDIDATE AxisY(SNAP_STABLE_ID aId, SNAP_PRIORITY_TIER aPriority, SNAP_CANDIDATE_SUBTYPE aSubtype, int aCoordinate, double aResidual)
std::vector< SNAP_GUIDE > guides
VECTOR2I end
VECTOR2I start
std::optional< VECTOR2I > stationarySourceLeg
std::vector< SNAP_STABLE_ID > movingFeatures
std::optional< BOX2I > movingBounds
std::optional< VECTOR2I > movingReferencePoint
std::optional< SNAP_STABLE_ID > movingItem
std::vector< SNAP_STABLE_ID > stationarySelfFeatures
SNAP_REFERENCE_PREFERENCE referencePreference
SNAP_ID_KIND kind
BOOST_AUTO_TEST_CASE(HorizontalAlignment)
BOOST_AUTO_TEST_SUITE(CadstarPartParser)
BOOST_REQUIRE(intersection.has_value()==c.ExpectedIntersection.has_value())
BOOST_AUTO_TEST_SUITE_END()
VECTOR3I expected(15, 30, 45)
BOOST_TEST_MESSAGE("Polyline has "<< chain.PointCount()<< " points")
int actual
BOOST_AUTO_TEST_CASE(GroupDefaultsPreserveConservativeInference)
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