KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_constraint_adapter.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 2
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 <cmath>
21#include <cstdlib>
22#include <vector>
23
25
26#include <board.h>
27#include <pcb_shape.h>
31
33
34using namespace KI_TEST;
35
36
37namespace
38{
39int slopeDiffY( const PCB_SHAPE* aSeg )
40{
41 return std::abs( aSeg->GetEnd().y - aSeg->GetStart().y );
42}
43}
44
45
46BOOST_AUTO_TEST_SUITE( ConstraintSolverAdapter )
47
48
49// A free segment made parallel to a fixed horizontal reference becomes horizontal.
50BOOST_AUTO_TEST_CASE( ParallelToFixedHorizontal )
51{
52 BOARD board;
53
54 PCB_SHAPE* ref = addSegment( board, { 0, 0 }, { 10 * MM, 0 } );
55 PCB_SHAPE* free = addSegment( board, { 1 * MM, 5 * MM }, { 9 * MM, 7 * MM } );
56
58 { { ref->m_Uuid, CONSTRAINT_ANCHOR::START } } );
60 { { ref->m_Uuid, CONSTRAINT_ANCHOR::END } } );
63 { free->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } } );
64
65 std::vector<PCB_SHAPE*> shapes{ ref, free };
66 solveAndApply( board, shapes );
67
68 // free is now horizontal (endpoints share Y) and ref is untouched.
69 BOOST_CHECK_LE( slopeDiffY( free ), 100 ); // within 100 nm of horizontal
70 BOOST_CHECK_EQUAL( ref->GetStart(), VECTOR2I( 0, 0 ) );
71 BOOST_CHECK_EQUAL( ref->GetEnd(), VECTOR2I( 10 * MM, 0 ) );
72}
73
74
75// The same cluster placed far from the origin still solves (normalization conditions it).
76BOOST_AUTO_TEST_CASE( ConditioningFarFromOrigin )
77{
78 BOARD board;
79
80 const VECTOR2I off( 500 * MM, 700 * MM );
81
82 PCB_SHAPE* ref = addSegment( board, off, off + VECTOR2I( 10 * MM, 0 ) );
83 PCB_SHAPE* free = addSegment( board, off + VECTOR2I( 1 * MM, 5 * MM ),
84 off + VECTOR2I( 9 * MM, 7 * MM ) );
85
87 { { ref->m_Uuid, CONSTRAINT_ANCHOR::START } } );
89 { { ref->m_Uuid, CONSTRAINT_ANCHOR::END } } );
92 { free->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } } );
93
94 std::vector<PCB_SHAPE*> shapes{ ref, free };
95 solveAndApply( board, shapes );
96
97 BOOST_CHECK_LE( slopeDiffY( free ), 100 );
98}
99
100
101// Dragging a coincident corner re-derives the neighbor through SolveCluster.
102BOOST_AUTO_TEST_CASE( DragCoincidentCornerMovesNeighbor )
103{
104 BOARD board;
105
106 // Two segments share a corner A end coincident with B start
107 PCB_SHAPE* a = addSegment( board, { 0, 0 }, { 10 * MM, 0 } );
108 PCB_SHAPE* b = addSegment( board, { 10 * MM, 0 }, { 10 * MM, 10 * MM } );
109
112 { b->m_Uuid, CONSTRAINT_ANCHOR::START } } );
113
114 // Drag A.end to a new spot; B.start must follow to stay coincident.
116 std::vector<PCB_SHAPE*> modified;
117
118 CONSTRAINT_DIAGNOSIS diag = SolveCluster( &board, dragged, { 12 * MM, 3 * MM }, &modified );
119
120 BOOST_REQUIRE( diag.solved );
121
122 // A.end and B.start moved together and remain coincident.
123 BOOST_CHECK_LE( ( a->GetEnd() - b->GetStart() ).EuclideanNorm(), 100 );
124 BOOST_CHECK_LE( ( a->GetEnd() - VECTOR2I( 12 * MM, 3 * MM ) ).EuclideanNorm(), 1000 );
125
126 // The neighbor B is reported as modified; the dragged shape A is not.
127 BOOST_CHECK( std::find( modified.begin(), modified.end(), b ) != modified.end() );
128 BOOST_CHECK( std::find( modified.begin(), modified.end(), a ) == modified.end() );
129}
130
131
132// Lone segment with one direction constraint is under constrained
133BOOST_AUTO_TEST_CASE( DiagnosisUnderConstrained )
134{
135 BOARD board;
136
137 PCB_SHAPE* seg = addSegment( board, { 0, 0 }, { 10 * MM, 2 * MM } );
139 { { seg->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } } );
140
141 std::vector<PCB_SHAPE*> shapes{ seg };
142 std::vector<PCB_CONSTRAINT*> constraints( board.Constraints().begin(), board.Constraints().end() );
143
145 BOOST_REQUIRE( adapter.Build( shapes, constraints ) );
146 BOOST_REQUIRE( adapter.Solve() );
147
148 CONSTRAINT_DIAGNOSIS diag = adapter.Diagnose();
149 BOOST_CHECK( diag.freeDof > 0 );
150 BOOST_CHECK( !diag.IsOverConstrained() );
151}
152
153
154// Duplicated constraint reported redundant and attributed by uuid
155// True conflicts need distance and length constraints arriving in step 6 so directional subset here is only degenerate
156BOOST_AUTO_TEST_CASE( DiagnosisRedundant )
157{
158 BOARD board;
159
160 PCB_SHAPE* seg = addSegment( board, { 0, 0 }, { 10 * MM, 2 * MM } );
161
163 { { seg->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } } );
165 { { seg->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } } );
166
167 std::vector<PCB_SHAPE*> shapes{ seg };
168 std::vector<PCB_CONSTRAINT*> constraints( board.Constraints().begin(), board.Constraints().end() );
169
171 BOOST_REQUIRE( adapter.Build( shapes, constraints ) );
172 BOOST_REQUIRE( adapter.Solve() );
173
174 CONSTRAINT_DIAGNOSIS diag = adapter.Diagnose();
175
176 BOOST_CHECK( !diag.redundant.empty() );
177
178 // The redundancy is attributed to a real constraint by uuid.
179 bool attributed = false;
180
181 for( const KIID& id : diag.redundant )
182 {
183 for( PCB_CONSTRAINT* c : board.Constraints() )
184 {
185 if( c->m_Uuid == id )
186 attributed = true;
187 }
188 }
189
190 BOOST_CHECK( attributed );
191 (void) dup;
192}
193
194
195// Fixed reference plus parallel fixes direction of free segment
196// Drag pins remaining freedom reaching zero free DOF
197BOOST_AUTO_TEST_CASE( DiagnosisWellConstrained )
198{
199 BOARD board;
200
201 PCB_SHAPE* ref = addSegment( board, { 0, 0 }, { 10 * MM, 0 } );
202 PCB_SHAPE* free = addSegment( board, { 1 * MM, 5 * MM }, { 9 * MM, 5 * MM } );
203
205 { { free->m_Uuid, CONSTRAINT_ANCHOR::START } } );
207 { { free->m_Uuid, CONSTRAINT_ANCHOR::END } } );
209 { { ref->m_Uuid, CONSTRAINT_ANCHOR::START } } );
211 { { ref->m_Uuid, CONSTRAINT_ANCHOR::END } } );
212
213 std::vector<PCB_SHAPE*> shapes{ ref, free };
214 std::vector<PCB_CONSTRAINT*> constraints( board.Constraints().begin(), board.Constraints().end() );
215
217 BOOST_REQUIRE( adapter.Build( shapes, constraints ) );
218
219 bool solved = adapter.Solve();
220 BOOST_REQUIRE( solved );
221
222 CONSTRAINT_DIAGNOSIS diag = adapter.Diagnose();
223 diag.solved = solved; // Diagnose() does not run a solve; carry the verdict like real callers.
224
225 // Every coordinate is grounded, so there is no remaining freedom.
226 BOOST_CHECK_EQUAL( diag.freeDof, 0 );
227 BOOST_CHECK( diag.IsWellConstrained() );
228}
229
230
231// A free segment made perpendicular to a fixed horizontal reference becomes vertical.
232BOOST_AUTO_TEST_CASE( PerpendicularToFixedHorizontal )
233{
234 BOARD board;
235
236 PCB_SHAPE* ref = addSegment( board, { 0, 0 }, { 10 * MM, 0 } );
237 PCB_SHAPE* free = addSegment( board, { 1 * MM, 5 * MM }, { 9 * MM, 7 * MM } );
238
240 { { ref->m_Uuid, CONSTRAINT_ANCHOR::START } } );
242 { { ref->m_Uuid, CONSTRAINT_ANCHOR::END } } );
245 { free->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } } );
246
247 std::vector<PCB_SHAPE*> shapes{ ref, free };
248 solveAndApply( board, shapes );
249
250 BOOST_CHECK_LE( std::abs( free->GetEnd().x - free->GetStart().x ), 100 ); // vertical
251}
252
253
254// A free segment made equal-length to a fixed reference takes the reference's length.
255BOOST_AUTO_TEST_CASE( EqualLengthToFixedReference )
256{
257 BOARD board;
258
259 PCB_SHAPE* ref = addSegment( board, { 0, 0 }, { 10 * MM, 0 } ); // length 10 mm
260 PCB_SHAPE* free = addSegment( board, { 0, 20 * MM }, { 4 * MM, 20 * MM } ); // length 4 mm
261
263 { { ref->m_Uuid, CONSTRAINT_ANCHOR::START } } );
265 { { ref->m_Uuid, CONSTRAINT_ANCHOR::END } } );
267 { { free->m_Uuid, CONSTRAINT_ANCHOR::START } } );
270 { free->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } } );
271
272 std::vector<PCB_SHAPE*> shapes{ ref, free };
273 solveAndApply( board, shapes );
274
275 double freeLen = ( free->GetEnd() - free->GetStart() ).EuclideanNorm();
276 BOOST_CHECK_LE( std::abs( freeLen - 10.0 * MM ), 1000 );
277}
278
279
280// A point dragged onto a segment ends up collinear with it (point-on-line).
281BOOST_AUTO_TEST_CASE( PointOnLineLandsOnSegment )
282{
283 BOARD board;
284
285 PCB_SHAPE* line = addSegment( board, { 0, 0 }, { 10 * MM, 0 } ); // the y=0 line
286 PCB_SHAPE* probe = addSegment( board, { 5 * MM, 5 * MM }, { 6 * MM, 6 * MM } );
287
289 { { line->m_Uuid, CONSTRAINT_ANCHOR::START } } );
291 { { line->m_Uuid, CONSTRAINT_ANCHOR::END } } );
293 { { probe->m_Uuid, CONSTRAINT_ANCHOR::START },
294 { line->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } } );
295
296 std::vector<PCB_SHAPE*> shapes{ line, probe };
297 solveAndApply( board, shapes );
298
299 // probe's start now lies on the y=0 line.
300 BOOST_CHECK_LE( std::abs( probe->GetStart().y ), 100 );
301}
302
303
304// A point-on-line constraint with a circle target lands the point on the circumference.
305BOOST_AUTO_TEST_CASE( PointOnLineLandsOnCircle )
306{
307 BOARD board;
308
309 PCB_SHAPE* circle = addCircle( board, { 10 * MM, 10 * MM }, 5 * MM );
310 PCB_SHAPE* probe = addSegment( board, { 0, 0 }, { 2 * MM, 1 * MM } );
311
312 circle->SetLocked( true );
313
315 { { probe->m_Uuid, CONSTRAINT_ANCHOR::END }, { circle->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } } );
316
317 std::vector<PCB_SHAPE*> shapes{ circle, probe };
318 solveAndApply( board, shapes );
319
320 double distToCenter = ( probe->GetEnd() - circle->GetCenter() ).EuclideanNorm();
321
322 BOOST_CHECK_LE( std::abs( distToCenter - 5.0 * MM ), 5000.0 );
323 BOOST_CHECK_EQUAL( circle->GetCenter(), VECTOR2I( 10 * MM, 10 * MM ) );
324 BOOST_CHECK_EQUAL( circle->GetRadius(), 5 * MM );
325}
326
327
328// A point-on-line constraint with an ellipse target lands the point on the outline.
329BOOST_AUTO_TEST_CASE( PointOnLineLandsOnEllipse )
330{
331 BOARD board;
332
333 PCB_SHAPE* ellipse = addEllipse( board, { 10 * MM, 10 * MM }, 8 * MM, 4 * MM, EDA_ANGLE( 30.0, DEGREES_T ) );
334 PCB_SHAPE* probe = addSegment( board, { 0, 0 }, { 2 * MM, 1 * MM } );
335
336 ellipse->SetLocked( true );
337
339 { { probe->m_Uuid, CONSTRAINT_ANCHOR::END }, { ellipse->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } } );
340
341 std::vector<PCB_SHAPE*> shapes{ ellipse, probe };
342 solveAndApply( board, shapes );
343
344 BOOST_CHECK_LE( std::abs( ellipseEquationAt( ellipse, probe->GetEnd() ) - 1.0 ), 1e-3 );
345 BOOST_CHECK_EQUAL( ellipse->GetEllipseCenter(), VECTOR2I( 10 * MM, 10 * MM ) );
346 BOOST_CHECK_EQUAL( ellipse->GetEllipseMajorRadius(), 8 * MM );
347 BOOST_CHECK_EQUAL( ellipse->GetEllipseMinorRadius(), 4 * MM );
348}
349
350
351// Same but target is elliptical arc point lands on its underlying ellipse
352BOOST_AUTO_TEST_CASE( PointOnLineLandsOnEllipticalArc )
353{
354 BOARD board;
355
356 PCB_SHAPE* arc = addEllipseArc( board, { 10 * MM, 10 * MM }, 8 * MM, 4 * MM, EDA_ANGLE( 30.0, DEGREES_T ),
357 EDA_ANGLE( 0.0, DEGREES_T ), EDA_ANGLE( 120.0, DEGREES_T ) );
358 PCB_SHAPE* probe = addSegment( board, { 0, 0 }, { 2 * MM, 1 * MM } );
359
360 arc->SetLocked( true );
361
363 { { probe->m_Uuid, CONSTRAINT_ANCHOR::END }, { arc->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } } );
364
365 std::vector<PCB_SHAPE*> shapes{ arc, probe };
366 solveAndApply( board, shapes );
367
368 BOOST_CHECK_LE( std::abs( ellipseEquationAt( arc, probe->GetEnd() ) - 1.0 ), 1e-3 );
369}
370
371
372// A free segment made tangent to a locked circle ends up with the center at exactly one radius
373// from its supporting line, on the side the circle started on.
374BOOST_AUTO_TEST_CASE( TangentLineToCircle )
375{
376 BOARD board;
377
378 PCB_SHAPE* circle = addCircle( board, { 10 * MM, 10 * MM }, 5 * MM );
379 PCB_SHAPE* line = addSegment( board, { 0, 0 }, { 20 * MM, 0 } );
380
381 circle->SetLocked( true );
382
384 { { line->m_Uuid, CONSTRAINT_ANCHOR::WHOLE }, { circle->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } } );
385
386 std::vector<PCB_SHAPE*> shapes{ circle, line };
387 solveAndApply( board, shapes );
388
389 VECTOR2D d = VECTOR2D( line->GetEnd() - line->GetStart() );
390 VECTOR2D toCenter = VECTOR2D( circle->GetCenter() - line->GetStart() );
391 double dist = std::abs( d.Cross( toCenter ) ) / d.EuclideanNorm();
392
393 BOOST_CHECK_LE( std::abs( dist - 5.0 * MM ), 5000.0 );
394 BOOST_CHECK_EQUAL( circle->GetCenter(), VECTOR2I( 10 * MM, 10 * MM ) );
395}
396
397
398// A free circle made tangent to a locked circle moves until the two touch externally.
399BOOST_AUTO_TEST_CASE( TangentCircleToCircle )
400{
401 BOARD board;
402
403 PCB_SHAPE* fixed = addCircle( board, { 0, 0 }, 5 * MM );
404 PCB_SHAPE* free = addCircle( board, { 20 * MM, 0 }, 3 * MM );
405
406 fixed->SetLocked( true );
407
409 { { fixed->m_Uuid, CONSTRAINT_ANCHOR::WHOLE }, { free->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } } );
410
411 std::vector<PCB_SHAPE*> shapes{ fixed, free };
412 solveAndApply( board, shapes );
413
414 // The free circle's radius is also a solver variable, so assert the tangency itself.
415 double centerDist = ( free->GetCenter() - fixed->GetCenter() ).EuclideanNorm();
416 double radiusSum = 5.0 * MM + free->GetRadius();
417
418 BOOST_CHECK_LE( std::abs( centerDist - radiusSum ), 5000.0 );
419 BOOST_CHECK_GT( free->GetRadius(), 0 );
420 BOOST_CHECK_EQUAL( fixed->GetCenter(), VECTOR2I( 0, 0 ) );
421}
422
423
424// Resizing one of two tangent circles re-solves the other so they still touch. The neighbor moves,
425// the resized circle stays put. This is the radius-edit path that a plain re-solve would miss.
426BOOST_AUTO_TEST_CASE( ResizeTangentCircleMovesNeighbor )
427{
428 BOARD board;
429
430 PCB_SHAPE* resized = addCircle( board, { 0, 0 }, 5 * MM );
431 PCB_SHAPE* neighbor = addCircle( board, { 8 * MM, 0 }, 3 * MM ); // externally tangent 5 + 3 = 8
432
434 { { resized->m_Uuid, CONSTRAINT_ANCHOR::WHOLE }, { neighbor->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } } );
435
436 // Grow the first circle, as the radius handle would.
437 resized->SetRadius( 7 * MM );
438
439 std::vector<PCB_SHAPE*> modified;
440 ReSolveAfterShapeResize( &board, resized, &modified );
441
442 // They still touch, the resized circle kept its new radius and center, and the neighbor moved.
443 double centerDist = ( neighbor->GetCenter() - resized->GetCenter() ).EuclideanNorm();
444
445 BOOST_CHECK_LE( std::abs( centerDist - ( 7.0 * MM + neighbor->GetRadius() ) ), 5000.0 );
446 BOOST_CHECK_EQUAL( resized->GetRadius(), 7 * MM );
447 BOOST_CHECK_EQUAL( resized->GetCenter(), VECTOR2I( 0, 0 ) );
448 BOOST_CHECK( std::find( modified.begin(), modified.end(), neighbor ) != modified.end() );
449
450 // The neighbor translates to stay tangent, keeping its own radius, not distorting it.
451 BOOST_CHECK_EQUAL( neighbor->GetRadius(), 3 * MM );
452 BOOST_CHECK_LE( std::abs( centerDist - 10 * MM ), 5000.0 );
453}
454
455
456// Resizing circle tangent to LOCKED circle
457// Locked one cannot move so resized circle keeps new radius and translates to stay tangent
458BOOST_AUTO_TEST_CASE( ResizeTangentToLockedTranslates )
459{
460 BOARD board;
461
462 PCB_SHAPE* locked = addCircle( board, { 0, 0 }, 5 * MM );
463 PCB_SHAPE* resized = addCircle( board, { 8 * MM, 0 }, 3 * MM ); // tangent 5 + 3 = 8
464
465 locked->SetLocked( true );
466
468 { { resized->m_Uuid, CONSTRAINT_ANCHOR::WHOLE }, { locked->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } } );
469
470 resized->SetRadius( 4 * MM ); // grow it, breaking tangency
471
472 std::vector<PCB_SHAPE*> modified;
473 ReSolveAfterShapeResize( &board, resized, &modified );
474
475 double centerDist = ( resized->GetCenter() - locked->GetCenter() ).EuclideanNorm();
476
477 // Still tangent 5 + 4 = 9 resized circle kept new radius and locked one held
478 BOOST_CHECK_LE( std::abs( centerDist - 9 * MM ), 5000.0 );
479 BOOST_CHECK_EQUAL( resized->GetRadius(), 4 * MM );
480 BOOST_CHECK_EQUAL( locked->GetCenter(), VECTOR2I( 0, 0 ) );
481 BOOST_CHECK_EQUAL( locked->GetRadius(), 5 * MM );
482}
483
484
485// Radius hold yields to real radius constraint
486// Resizing one equal radius circle resizes other to match
487BOOST_AUTO_TEST_CASE( ResizeEqualRadiusStillPropagates )
488{
489 BOARD board;
490
491 PCB_SHAPE* resized = addCircle( board, { 0, 0 }, 5 * MM );
492 PCB_SHAPE* neighbor = addCircle( board, { 20 * MM, 0 }, 5 * MM );
493
495 { { resized->m_Uuid, CONSTRAINT_ANCHOR::WHOLE }, { neighbor->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } } );
496
497 resized->SetRadius( 8 * MM );
498
499 ReSolveAfterShapeResize( &board, resized, nullptr );
500
501 BOOST_CHECK_LE( std::abs( neighbor->GetRadius() - 8 * MM ), 5000 );
502}
503
504
505// Properties edit is authoritative hold edited circle fixed keep typed radius exact resize neighbor to match
506// Plain move semantics would leave radius free and solve it back instead
507BOOST_AUTO_TEST_CASE( HoldingEditedCircleKeepsTypedRadius )
508{
509 BOARD board;
510
511 PCB_SHAPE* edited = addCircle( board, { 0, 0 }, 5 * MM );
512 PCB_SHAPE* neighbor = addCircle( board, { 20 * MM, 0 }, 5 * MM );
513
515 { { edited->m_Uuid, CONSTRAINT_ANCHOR::WHOLE },
516 { neighbor->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } } );
517
518 // The user typed a new radius on the edited circle through the properties panel.
519 edited->SetRadius( 8 * MM );
520
521 ReSolveShapeClustersHoldingEdited( &board, { edited }, nullptr );
522
523 BOOST_CHECK_EQUAL( edited->GetRadius(), 8 * MM ); // typed value survives exactly
524 BOOST_CHECK_LE( std::abs( neighbor->GetRadius() - 8 * MM ), 5000 ); // neighbor adjusted to match
525}
526
527
528BOOST_AUTO_TEST_CASE( HoldingEditedReportsConflictBeforeWritingAnyCluster )
529{
530 BOARD board;
531 PCB_SHAPE* edited = addSegment( board, { 0, 0 }, { 10 * MM, 0 } );
532 PCB_SHAPE* locked = addSegment( board, { 10 * MM, 0 }, { 20 * MM, 0 } );
533 locked->SetLocked( true );
534
536 { { edited->m_Uuid, CONSTRAINT_ANCHOR::END },
537 { locked->m_Uuid, CONSTRAINT_ANCHOR::START } } );
538
539 edited->Move( { 5 * MM, 0 } );
540 VECTOR2I lockedStart = locked->GetStart();
541
542 BOOST_CHECK( !ReSolveShapeClustersHoldingEdited( &board, { edited }, nullptr ) );
543 BOOST_CHECK_EQUAL( locked->GetStart(), lockedStart );
544}
545
546
547// A properties edit of a polygon's geometry is authoritative, so the hold-edited re-solve keeps the
548// edited outline exactly, the vertex-bound segment follows, and the unbound vertices hold.
549BOOST_AUTO_TEST_CASE( HoldingEditedPolygonResizeMovesBoundNeighbor )
550{
551 BOARD board;
552
553 std::vector<VECTOR2I> pts = { { 0, 0 },
554 { 20 * MM, 5 * MM },
555 { 15 * MM, 20 * MM },
556 { 5 * MM, 20 * MM },
557 { -5 * MM, 10 * MM } };
558 PCB_SHAPE* poly = addPoly( board, pts );
559 PCB_SHAPE* seg = addSegment( board, pts[1], { 40 * MM, 0 } );
560
563
564 // Move the 1-2 edge 3 mm right, as a typed geometry edit would.
565 std::vector<VECTOR2I> dragged = pts;
566 dragged[1] += VECTOR2I( 3 * MM, 0 );
567 dragged[2] += VECTOR2I( 3 * MM, 0 );
568 poly->SetPolyPoints( dragged );
569
570 std::vector<PCB_SHAPE*> modified;
571 ReSolveShapeClustersHoldingEdited( &board, { poly }, &modified );
572
573 const SHAPE_LINE_CHAIN& outline = poly->GetPolyShape().COutline( 0 );
574
575 BOOST_CHECK_LE( ( outline.CPoint( 1 ) - dragged[1] ).EuclideanNorm(), 5000.0 );
576 BOOST_CHECK_LE( ( outline.CPoint( 2 ) - dragged[2] ).EuclideanNorm(), 5000.0 );
577 BOOST_CHECK_LE( ( seg->GetStart() - dragged[1] ).EuclideanNorm(), 5000.0 );
578 BOOST_CHECK( std::find( modified.begin(), modified.end(), seg ) != modified.end() );
579
580 for( int i : { 0, 3, 4 } )
581 BOOST_CHECK_LE( ( outline.CPoint( i ) - pts[i] ).EuclideanNorm(), 5000.0 );
582}
583
584
585// A resize solve holds the resized ellipse's minor radius; with the focus offset fixed at Build,
586// that pins the whole shape, so the user's new size survives the re-solve undistorted.
587BOOST_AUTO_TEST_CASE( ResizeEllipseKeepsShape )
588{
589 BOARD board;
590
591 PCB_SHAPE* resized = addEllipse( board, { 0, 0 }, 6 * MM, 3 * MM, EDA_ANGLE( 30.0, DEGREES_T ) );
592 PCB_SHAPE* neighbor = addCircle( board, { 20 * MM, 0 }, 2 * MM );
593
595 { { resized->m_Uuid, CONSTRAINT_ANCHOR::WHOLE }, { neighbor->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } } );
596
597 // Shrink the minor radius, as the resize handle would.
598 resized->SetEllipseMinorRadius( 2 * MM );
599
600 ReSolveAfterShapeResize( &board, resized, nullptr );
601
602 BOOST_CHECK_LE( std::abs( resized->GetEllipseMajorRadius() - 6 * MM ), 5000 );
603 BOOST_CHECK_LE( std::abs( resized->GetEllipseMinorRadius() - 2 * MM ), 5000 );
604 BOOST_CHECK_LE(
605 std::abs( ( resized->GetEllipseRotation() - EDA_ANGLE( 30.0, DEGREES_T ) ).Normalize180().AsDegrees() ),
606 0.01 );
607
608 // The circle came to the ellipse's center; the resized shape stayed put.
609 BOOST_CHECK_LE( ( resized->GetEllipseCenter() - VECTOR2I( 0, 0 ) ).EuclideanNorm(), 5000.0 );
610 BOOST_CHECK_LE( ( neighbor->GetCenter() - resized->GetEllipseCenter() ).EuclideanNorm(), 5000.0 );
611}
612
613
614// Free ellipse concentric with locked circle moves center without distorting
615// Focus follows center so major and minor radius and rotation stay preserved
616BOOST_AUTO_TEST_CASE( ConcentricEllipseKeepsShape )
617{
618 BOARD board;
619
620 PCB_SHAPE* circle = addCircle( board, { 20 * MM, 15 * MM }, 3 * MM );
621 PCB_SHAPE* ellipse = addEllipse( board, { 0, 0 }, 8 * MM, 4 * MM, EDA_ANGLE( 30.0, DEGREES_T ) );
622
623 circle->SetLocked( true );
624
626 { { ellipse->m_Uuid, CONSTRAINT_ANCHOR::WHOLE }, { circle->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } } );
627
628 std::vector<PCB_SHAPE*> shapes{ circle, ellipse };
629 solveAndApply( board, shapes );
630
631 BOOST_CHECK_LE( ( ellipse->GetEllipseCenter() - VECTOR2I( 20 * MM, 15 * MM ) ).EuclideanNorm(), 5000.0 );
632 BOOST_CHECK_LE( std::abs( ellipse->GetEllipseMajorRadius() - 8 * MM ), 5000 );
633 BOOST_CHECK_LE( std::abs( ellipse->GetEllipseMinorRadius() - 4 * MM ), 5000 );
634 BOOST_CHECK_LE(
635 std::abs( ( ellipse->GetEllipseRotation() - EDA_ANGLE( 30.0, DEGREES_T ) ).Normalize180().AsDegrees() ),
636 0.01 );
637}
638
639
640// A probe point made the midpoint of a fixed segment lands at its center.
641BOOST_AUTO_TEST_CASE( MidpointOfFixedSegment )
642{
643 BOARD board;
644
645 PCB_SHAPE* seg = addSegment( board, { 0, 0 }, { 10 * MM, 4 * MM } ); // midpoint (5,2) mm
646 PCB_SHAPE* probe = addSegment( board, { 1 * MM, 1 * MM }, { 2 * MM, 2 * MM } );
647
649 { { seg->m_Uuid, CONSTRAINT_ANCHOR::START } } );
651 { { seg->m_Uuid, CONSTRAINT_ANCHOR::END } } );
653 { { probe->m_Uuid, CONSTRAINT_ANCHOR::START },
654 { seg->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } } );
655
656 std::vector<PCB_SHAPE*> shapes{ seg, probe };
657 solveAndApply( board, shapes );
658
659 BOOST_CHECK_LE( ( probe->GetStart() - VECTOR2I( 5 * MM, 2 * MM ) ).EuclideanNorm(), 1000 );
660}
661
662
663// A free segment made collinear with a fixed horizontal reference lands on the reference's line.
664BOOST_AUTO_TEST_CASE( CollinearWithFixedReference )
665{
666 BOARD board;
667
668 PCB_SHAPE* ref = addSegment( board, { 0, 0 }, { 10 * MM, 0 } ); // the y=0 line
669 PCB_SHAPE* free = addSegment( board, { 2 * MM, 5 * MM }, { 8 * MM, 6 * MM } );
670
672 { { ref->m_Uuid, CONSTRAINT_ANCHOR::START } } );
674 { { ref->m_Uuid, CONSTRAINT_ANCHOR::END } } );
677 { free->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } } );
678
679 std::vector<PCB_SHAPE*> shapes{ ref, free };
680 solveAndApply( board, shapes );
681
682 BOOST_CHECK_LE( std::abs( free->GetStart().y ), 100 );
683 BOOST_CHECK_LE( std::abs( free->GetEnd().y ), 100 );
684}
685
686
687// Two points made symmetric about a fixed vertical axis end up mirrored across it.
688BOOST_AUTO_TEST_CASE( SymmetricAboutFixedAxis )
689{
690 BOARD board;
691
692 // Axis is the fixed vertical line x = 5 mm.
693 PCB_SHAPE* axis = addSegment( board, { 5 * MM, 0 }, { 5 * MM, 10 * MM } );
694 PCB_SHAPE* segA = addSegment( board, { 2 * MM, 3 * MM }, { 2 * MM, 4 * MM } );
695 PCB_SHAPE* segB = addSegment( board, { 9 * MM, 3 * MM }, { 9 * MM, 4 * MM } );
696
698 { { axis->m_Uuid, CONSTRAINT_ANCHOR::START } } );
700 { { axis->m_Uuid, CONSTRAINT_ANCHOR::END } } );
702 { { segA->m_Uuid, CONSTRAINT_ANCHOR::START } } );
704 { { segA->m_Uuid, CONSTRAINT_ANCHOR::START },
705 { segB->m_Uuid, CONSTRAINT_ANCHOR::START },
706 { axis->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } } );
707
708 std::vector<PCB_SHAPE*> shapes{ axis, segA, segB };
709 solveAndApply( board, shapes );
710
711 // segB.start is the mirror of segA.start (2mm) across x = 5mm, i.e. x = 8mm, same y.
712 BOOST_CHECK_LE( std::abs( segB->GetStart().x - 8 * MM ), 1000 );
713 BOOST_CHECK_LE( std::abs( segB->GetStart().y - segA->GetStart().y ), 1000 );
714}
715
716
717// A segment with a fixed start and a driving fixed-length takes that length.
718BOOST_AUTO_TEST_CASE( FixedLengthDrivesSegmentLength )
719{
720 BOARD board;
721
722 PCB_SHAPE* seg = addSegment( board, { 0, 0 }, { 3 * MM, 0 } );
723
725 { { seg->m_Uuid, CONSTRAINT_ANCHOR::START } } );
726
728 { { seg->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } } );
729 len->SetValue( 8.0 * MM );
730
731 std::vector<PCB_SHAPE*> shapes{ seg };
732 solveAndApply( board, shapes );
733
734 double length = ( seg->GetEnd() - seg->GetStart() ).EuclideanNorm();
735 BOOST_CHECK_LE( std::abs( length - 8.0 * MM ), 1000 );
736}
737
738
739// Driving fixed length over two point anchors not a whole segment drives distance between them
740// Matches aligned dimension Driving mode binding its own START and END
741BOOST_AUTO_TEST_CASE( FixedLengthTwoPointDrivesDistance )
742{
743 BOARD board;
744
745 PCB_SHAPE* seg = addSegment( board, { 0, 0 }, { 3 * MM, 0 } );
746
748 { { seg->m_Uuid, CONSTRAINT_ANCHOR::START } } );
749
752 { seg->m_Uuid, CONSTRAINT_ANCHOR::END } } );
753 len->SetValue( 8.0 * MM );
754 len->SetDriving( true );
755
756 std::vector<PCB_SHAPE*> shapes{ seg };
757 solveAndApply( board, shapes );
758
759 double length = ( seg->GetEnd() - seg->GetStart() ).EuclideanNorm();
760 BOOST_CHECK_LE( std::abs( length - 8.0 * MM ), 1000 );
761 BOOST_CHECK_LE( ( seg->GetEnd() - VECTOR2I( 8 * MM, 0 ) ).EuclideanNorm(), 5000 );
762}
763
764
765// A rectangle's corners are aliases over its two stored corners, so a driving fixed length between
766// two adjacent corners resizes the rectangle while it stays a rectangle by construction.
767BOOST_AUTO_TEST_CASE( RectCornerFixedLengthDrivesWidth )
768{
769 BOARD board;
770
771 PCB_SHAPE* rect = addRect( board, { 10 * MM, 10 * MM }, { 50 * MM, 40 * MM } );
772
773 // VERTEX 0 and 1 are the top-left and top-right corners, so this drives the width.
775 { { rect->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 0 },
776 { rect->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 1 } },
777 30.0 * MM );
778 len->SetDriving( true );
779
780 std::vector<PCB_SHAPE*> shapes{ rect };
781 std::vector<PCB_CONSTRAINT*> constraints( board.Constraints().begin(), board.Constraints().end() );
782
784 BOOST_REQUIRE( adapter.Build( shapes, constraints ) );
785 BOOST_CHECK( adapter.UnmappedConstraints().empty() );
786 BOOST_REQUIRE( adapter.Solve( true ) );
787 adapter.Apply();
788
789 int width = std::abs( rect->GetEnd().x - rect->GetStart().x );
790 int height = std::abs( rect->GetEnd().y - rect->GetStart().y );
791
792 BOOST_CHECK_LE( std::abs( width - 30 * MM ), 5000 );
793 BOOST_CHECK_LE( std::abs( height - 30 * MM ), 5000 );
794
795 // The corners kept their min/max relationship, so the rect did not fold through itself.
796 BOOST_CHECK_LT( rect->GetStart().x, rect->GetEnd().x );
797 BOOST_CHECK_LT( rect->GetStart().y, rect->GetEnd().y );
798}
799
800
801// Same physical rectangle stored with start and end swapped
802// VERTEX indices canonicalized from initial geometry so 0 and 3 still name left edge corners
803BOOST_AUTO_TEST_CASE( RectSwappedStorageBindsSamePhysicalCorners )
804{
805 BOARD board;
806
807 // Stored start is the bottom-right corner; the physical rect is (10,10)-(50,40).
808 PCB_SHAPE* rect = addRect( board, { 50 * MM, 40 * MM }, { 10 * MM, 10 * MM } );
809
811 { { rect->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 0 },
812 { rect->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 3 } },
813 20.0 * MM );
814 len->SetDriving( true );
815
816 std::vector<PCB_SHAPE*> shapes{ rect };
817 std::vector<PCB_CONSTRAINT*> constraints( board.Constraints().begin(), board.Constraints().end() );
818
820 BOOST_REQUIRE( adapter.Build( shapes, constraints ) );
821 BOOST_CHECK( adapter.UnmappedConstraints().empty() );
822 BOOST_REQUIRE( adapter.Solve( true ) );
823 adapter.Apply();
824
825 int width = std::abs( rect->GetEnd().x - rect->GetStart().x );
826 int height = std::abs( rect->GetEnd().y - rect->GetStart().y );
827
828 BOOST_CHECK_LE( std::abs( height - 20 * MM ), 5000 );
829 BOOST_CHECK_LE( std::abs( width - 40 * MM ), 5000 );
830}
831
832
833// An out-of-range corner index cannot map; Build still succeeds and the geometry is untouched.
834BOOST_AUTO_TEST_CASE( RectInvalidVertexIndexUnmapped )
835{
836 BOARD board;
837
838 PCB_SHAPE* rect = addRect( board, { 10 * MM, 10 * MM }, { 50 * MM, 40 * MM } );
839
841 { { rect->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 7 },
842 { rect->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 0 } },
843 30.0 * MM );
844 len->SetDriving( true );
845
846 std::vector<PCB_SHAPE*> shapes{ rect };
847 std::vector<PCB_CONSTRAINT*> constraints( board.Constraints().begin(), board.Constraints().end() );
848
850 BOOST_REQUIRE( adapter.Build( shapes, constraints ) );
851
852 BOOST_REQUIRE_EQUAL( adapter.UnmappedConstraints().size(), 1 );
853 BOOST_CHECK( adapter.UnmappedConstraints().front() == len->m_Uuid );
854
855 BOOST_REQUIRE( adapter.Solve( true ) );
856 adapter.Apply();
857
858 BOOST_CHECK_EQUAL( rect->GetStart(), VECTOR2I( 10 * MM, 10 * MM ) );
859 BOOST_CHECK_EQUAL( rect->GetEnd(), VECTOR2I( 50 * MM, 40 * MM ) );
860}
861
862
863// Shrinking rounded rectangle reclamps stored corner radius
864// SetStart and SetEnd alone leave serialized radius out of range so Apply must push it back through clamping setter
865BOOST_AUTO_TEST_CASE( RectShrinkReclampsCornerRadius )
866{
867 BOARD board;
868
869 PCB_SHAPE* rect = addRect( board, { 10 * MM, 10 * MM }, { 50 * MM, 40 * MM } );
870 rect->SetCornerRadius( 10 * MM );
871
873 { { rect->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 0 },
874 { rect->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 1 } },
875 8.0 * MM );
876 len->SetDriving( true );
877
878 std::vector<PCB_SHAPE*> shapes{ rect };
879 std::vector<PCB_CONSTRAINT*> constraints( board.Constraints().begin(), board.Constraints().end() );
880
882 BOOST_REQUIRE( adapter.Build( shapes, constraints ) );
883 BOOST_REQUIRE( adapter.Solve( true ) );
884 adapter.Apply();
885
886 int width = std::abs( rect->GetEnd().x - rect->GetStart().x );
887
888 BOOST_CHECK_LE( std::abs( width - 8 * MM ), 5000 );
889
890 // The radius clamped to half the new shorter side, so the stored value stays coherent.
891 BOOST_CHECK_LE( rect->GetCornerRadius(), width / 2 );
892 BOOST_CHECK_LE( std::abs( rect->GetCornerRadius() - 4 * MM ), 5000 );
893}
894
895
896// A solve that would squash a rect side below the sub-micron floor is a collapse, not intent, so
897// Apply must leave the geometry untouched.
898BOOST_AUTO_TEST_CASE( RectCollapseGuardHoldsGeometry )
899{
900 BOARD board;
901
902 PCB_SHAPE* rect = addRect( board, { 10 * MM, 10 * MM }, { 50 * MM, 40 * MM } );
903
904 // A driving width below the collapse floor (0.5 um) deterministically forces the collapse.
906 { { rect->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 0 },
907 { rect->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 1 } },
908 0.0005 * MM );
909 len->SetDriving( true );
910
911 std::vector<PCB_SHAPE*> shapes{ rect };
912 std::vector<PCB_CONSTRAINT*> constraints( board.Constraints().begin(), board.Constraints().end() );
913
915 BOOST_REQUIRE( adapter.Build( shapes, constraints ) );
916 adapter.Solve( true );
917
918 std::vector<PCB_SHAPE*> changed = adapter.Apply();
919
920 BOOST_CHECK( changed.empty() );
921 BOOST_CHECK_EQUAL( rect->GetStart(), VECTOR2I( 10 * MM, 10 * MM ) );
922 BOOST_CHECK_EQUAL( rect->GetEnd(), VECTOR2I( 50 * MM, 40 * MM ) );
923}
924
925
926// A driving fixed length between two polygon vertices moves only the bound vertices; every other
927// vertex keeps its minimal-movement soft pin and stays put.
928BOOST_AUTO_TEST_CASE( PolyVertexFixedLengthMovesBoundVerticesOnly )
929{
930 BOARD board;
931
932 const std::vector<VECTOR2I> points{ { 30 * MM, 20 * MM },
933 { 39 * MM, 27 * MM },
934 { 36 * MM, 38 * MM },
935 { 24 * MM, 38 * MM },
936 { 21 * MM, 27 * MM } };
937
938 PCB_SHAPE* poly = addPoly( board, points );
939
940 // Vertices 1 and 3 start ~18.6mm apart; drive them to 24mm.
942 { { poly->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 1 },
943 { poly->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 3 } },
944 24.0 * MM );
945 len->SetDriving( true );
946
947 std::vector<PCB_SHAPE*> shapes{ poly };
948 std::vector<PCB_CONSTRAINT*> constraints( board.Constraints().begin(), board.Constraints().end() );
949
951 BOOST_REQUIRE( adapter.Build( shapes, constraints ) );
952 BOOST_CHECK( adapter.UnmappedConstraints().empty() );
953 BOOST_REQUIRE( adapter.Solve( true ) );
954 adapter.Apply();
955
956 const SHAPE_LINE_CHAIN& outline = poly->GetPolyShape().COutline( 0 );
957 BOOST_REQUIRE_EQUAL( outline.PointCount(), 5 );
958
959 double driven = ( outline.CPoint( 3 ) - outline.CPoint( 1 ) ).EuclideanNorm();
960 BOOST_CHECK_LE( std::abs( driven - 24.0 * MM ), 5000 );
961
962 // The unbound vertices held their soft pins exactly.
963 for( int i : { 0, 2, 4 } )
964 BOOST_CHECK_LE( ( outline.CPoint( i ) - points[i] ).EuclideanNorm(), 1000 );
965}
966
967
968// A stale vertex index (the poly has only five vertices) cannot map; Build still succeeds, the
969// constraint lands in UnmappedConstraints() and the geometry is untouched.
970BOOST_AUTO_TEST_CASE( PolyStaleVertexIndexUnmapped )
971{
972 BOARD board;
973
974 const std::vector<VECTOR2I> points{ { 30 * MM, 20 * MM },
975 { 39 * MM, 27 * MM },
976 { 36 * MM, 38 * MM },
977 { 24 * MM, 38 * MM },
978 { 21 * MM, 27 * MM } };
979
980 PCB_SHAPE* poly = addPoly( board, points );
981
983 { { poly->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 9 },
984 { poly->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 0 } },
985 30.0 * MM );
986 len->SetDriving( true );
987
988 std::vector<PCB_SHAPE*> shapes{ poly };
989 std::vector<PCB_CONSTRAINT*> constraints( board.Constraints().begin(), board.Constraints().end() );
990
992 BOOST_REQUIRE( adapter.Build( shapes, constraints ) );
993
994 BOOST_REQUIRE_EQUAL( adapter.UnmappedConstraints().size(), 1 );
995 BOOST_CHECK( adapter.UnmappedConstraints().front() == len->m_Uuid );
996
997 BOOST_REQUIRE( adapter.Solve( true ) );
998 adapter.Apply();
999
1000 const SHAPE_LINE_CHAIN& outline = poly->GetPolyShape().COutline( 0 );
1001
1002 for( int i = 0; i < 5; ++i )
1003 BOOST_CHECK_EQUAL( outline.CPoint( i ), points[i] );
1004}
1005
1006
1007// A poly with a hole is not ingested (write-back rebuilds a single outline, which would destroy the
1008// hole), so a constraint on it stays unmapped and the geometry is untouched.
1009BOOST_AUTO_TEST_CASE( PolyWithHoleStaysUnmapped )
1010{
1011 BOARD board;
1012
1013 const std::vector<VECTOR2I> points{ { 10 * MM, 10 * MM },
1014 { 50 * MM, 10 * MM },
1015 { 50 * MM, 40 * MM },
1016 { 10 * MM, 40 * MM } };
1017
1018 PCB_SHAPE* poly = addPoly( board, points );
1019
1020 SHAPE_POLY_SET& polySet = poly->GetPolyShape();
1021 polySet.NewHole( 0 );
1022 polySet.Append( 20 * MM, 20 * MM, 0, 0 );
1023 polySet.Append( 30 * MM, 20 * MM, 0, 0 );
1024 polySet.Append( 30 * MM, 30 * MM, 0, 0 );
1025
1027 { { poly->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 0 },
1028 { poly->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 1 } },
1029 30.0 * MM );
1030 len->SetDriving( true );
1031
1032 std::vector<PCB_SHAPE*> shapes{ poly };
1033 std::vector<PCB_CONSTRAINT*> constraints( board.Constraints().begin(), board.Constraints().end() );
1034
1036 BOOST_REQUIRE( adapter.Build( shapes, constraints ) );
1037
1038 BOOST_REQUIRE_EQUAL( adapter.UnmappedConstraints().size(), 1 );
1039 BOOST_CHECK( adapter.UnmappedConstraints().front() == len->m_Uuid );
1040
1041 BOOST_REQUIRE( adapter.Solve( true ) );
1042 adapter.Apply();
1043
1044 BOOST_CHECK_EQUAL( poly->GetPolyShape().HoleCount( 0 ), 1 );
1045
1046 const SHAPE_LINE_CHAIN& outline = poly->GetPolyShape().COutline( 0 );
1047
1048 for( int i = 0; i < 4; ++i )
1049 BOOST_CHECK_EQUAL( outline.CPoint( i ), points[i] );
1050}
1051
1052
1053// A poly whose outline carries an arc is not ingested (write-back would polygonize the arc and drop
1054// its metadata), so a constraint on it stays unmapped and the arc survives Solve + Apply.
1055BOOST_AUTO_TEST_CASE( PolyArcOutlineStaysUnmapped )
1056{
1057 BOARD board;
1058
1059 PCB_SHAPE* poly = new PCB_SHAPE( &board, SHAPE_T::POLY );
1060
1062 chain.Append( VECTOR2I( 10 * MM, 10 * MM ) );
1063 chain.Append( VECTOR2I( 50 * MM, 10 * MM ) );
1064 chain.Append( SHAPE_ARC( { 50 * MM, 10 * MM }, { 55 * MM, 25 * MM }, { 50 * MM, 40 * MM }, 0 ) );
1065 chain.Append( VECTOR2I( 10 * MM, 40 * MM ) );
1066 chain.SetClosed( true );
1067
1068 poly->GetPolyShape().AddOutline( chain );
1069 board.Add( poly );
1070
1071 BOOST_REQUIRE_GT( poly->GetPolyShape().COutline( 0 ).ArcCount(), 0 );
1072
1074 { { poly->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 0 },
1075 { poly->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 1 } },
1076 30.0 * MM );
1077 len->SetDriving( true );
1078
1079 std::vector<PCB_SHAPE*> shapes{ poly };
1080 std::vector<PCB_CONSTRAINT*> constraints( board.Constraints().begin(), board.Constraints().end() );
1081
1083 BOOST_REQUIRE( adapter.Build( shapes, constraints ) );
1084
1085 BOOST_REQUIRE_EQUAL( adapter.UnmappedConstraints().size(), 1 );
1086 BOOST_CHECK( adapter.UnmappedConstraints().front() == len->m_Uuid );
1087
1088 BOOST_REQUIRE( adapter.Solve( true ) );
1089 adapter.Apply();
1090
1091 const SHAPE_LINE_CHAIN& outline = poly->GetPolyShape().COutline( 0 );
1092
1093 BOOST_CHECK_GT( outline.ArcCount(), 0 );
1094 BOOST_CHECK_EQUAL( outline.CPoint( 0 ), VECTOR2I( 10 * MM, 10 * MM ) );
1095 BOOST_CHECK_EQUAL( outline.CPoint( 1 ), VECTOR2I( 50 * MM, 10 * MM ) );
1096}
1097
1098
1099// A poly with a second outline is not ingested (write-back rebuilds a single outline, which would
1100// destroy the other one), so a constraint on it stays unmapped and both outlines survive.
1101BOOST_AUTO_TEST_CASE( PolyTwoOutlinesStaysUnmapped )
1102{
1103 BOARD board;
1104
1105 const std::vector<VECTOR2I> points{ { 10 * MM, 10 * MM },
1106 { 50 * MM, 10 * MM },
1107 { 50 * MM, 40 * MM },
1108 { 10 * MM, 40 * MM } };
1109
1110 PCB_SHAPE* poly = addPoly( board, points );
1111
1112 SHAPE_POLY_SET& polySet = poly->GetPolyShape();
1113 polySet.NewOutline();
1114 polySet.Append( 60 * MM, 10 * MM, 1 );
1115 polySet.Append( 70 * MM, 10 * MM, 1 );
1116 polySet.Append( 70 * MM, 20 * MM, 1 );
1117
1119 { { poly->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 0 },
1120 { poly->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 1 } },
1121 30.0 * MM );
1122 len->SetDriving( true );
1123
1124 std::vector<PCB_SHAPE*> shapes{ poly };
1125 std::vector<PCB_CONSTRAINT*> constraints( board.Constraints().begin(), board.Constraints().end() );
1126
1128 BOOST_REQUIRE( adapter.Build( shapes, constraints ) );
1129
1130 BOOST_REQUIRE_EQUAL( adapter.UnmappedConstraints().size(), 1 );
1131 BOOST_CHECK( adapter.UnmappedConstraints().front() == len->m_Uuid );
1132
1133 BOOST_REQUIRE( adapter.Solve( true ) );
1134 adapter.Apply();
1135
1137
1138 const SHAPE_LINE_CHAIN& outline = poly->GetPolyShape().COutline( 0 );
1139
1140 for( int i = 0; i < 4; ++i )
1141 BOOST_CHECK_EQUAL( outline.CPoint( i ), points[i] );
1142}
1143
1144
1145// A poly whose outline has no vertices contributes no params; a constraint naming it stays unmapped
1146// and the rest of the cluster still solves.
1147BOOST_AUTO_TEST_CASE( PolyEmptyOutlineStaysUnmapped )
1148{
1149 BOARD board;
1150
1151 PCB_SHAPE* poly = addPoly( board, {} );
1152 PCB_SHAPE* seg = addSegment( board, { 0, 0 }, { 3 * MM, 0 } );
1153
1155 { { poly->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 0 },
1156 { seg->m_Uuid, CONSTRAINT_ANCHOR::START } },
1157 30.0 * MM );
1158 len->SetDriving( true );
1159
1160 std::vector<PCB_SHAPE*> shapes{ poly, seg };
1161 std::vector<PCB_CONSTRAINT*> constraints( board.Constraints().begin(), board.Constraints().end() );
1162
1164 BOOST_REQUIRE( adapter.Build( shapes, constraints ) );
1165
1166 BOOST_REQUIRE_EQUAL( adapter.UnmappedConstraints().size(), 1 );
1167 BOOST_CHECK( adapter.UnmappedConstraints().front() == len->m_Uuid );
1168
1169 BOOST_REQUIRE( adapter.Solve( true ) );
1170 adapter.Apply();
1171
1172 BOOST_CHECK_EQUAL( seg->GetStart(), VECTOR2I( 0, 0 ) );
1173 BOOST_CHECK_EQUAL( seg->GetEnd(), VECTOR2I( 3 * MM, 0 ) );
1174}
1175
1176
1177// A circle with a driving fixed-radius takes that radius.
1178BOOST_AUTO_TEST_CASE( FixedRadiusDrivesCircle )
1179{
1180 BOARD board;
1181
1182 PCB_SHAPE* circle = addCircle( board, { 0, 0 }, 3 * MM );
1183
1185 { { circle->m_Uuid, CONSTRAINT_ANCHOR::CENTER } } );
1186
1188 { { circle->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } } );
1189 fr->SetValue( 5.0 * MM );
1190
1191 std::vector<PCB_SHAPE*> shapes{ circle };
1192 solveAndApply( board, shapes );
1193
1194 BOOST_CHECK_LE( std::abs( circle->GetRadius() - 5 * MM ), 1000 );
1195}
1196
1197
1198// Two circles made equal-radius end up the same size as the fixed reference.
1199BOOST_AUTO_TEST_CASE( EqualRadiusMatchesReference )
1200{
1201 BOARD board;
1202
1203 PCB_SHAPE* ref = addCircle( board, { 0, 0 }, 4 * MM );
1204 PCB_SHAPE* other = addCircle( board, { 30 * MM, 0 }, 1 * MM );
1205
1207 { { ref->m_Uuid, CONSTRAINT_ANCHOR::CENTER } } );
1209 { { ref->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } } );
1210 fr->SetValue( 4.0 * MM );
1212 { { ref->m_Uuid, CONSTRAINT_ANCHOR::WHOLE },
1213 { other->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } } );
1214
1215 std::vector<PCB_SHAPE*> shapes{ ref, other };
1216 solveAndApply( board, shapes );
1217
1218 BOOST_CHECK_LE( std::abs( other->GetRadius() - 4 * MM ), 1000 );
1219}
1220
1221
1222// Two circles made concentric end up sharing the fixed reference's center.
1223BOOST_AUTO_TEST_CASE( ConcentricSharesCenter )
1224{
1225 BOARD board;
1226
1227 PCB_SHAPE* ref = addCircle( board, { 5 * MM, 5 * MM }, 4 * MM );
1228 PCB_SHAPE* other = addCircle( board, { 30 * MM, 0 }, 2 * MM );
1229
1231 { { ref->m_Uuid, CONSTRAINT_ANCHOR::CENTER } } );
1233 { { ref->m_Uuid, CONSTRAINT_ANCHOR::WHOLE },
1234 { other->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } } );
1235
1236 std::vector<PCB_SHAPE*> shapes{ ref, other };
1237 solveAndApply( board, shapes );
1238
1239 BOOST_CHECK_LE( ( other->GetCenter() - VECTOR2I( 5 * MM, 5 * MM ) ).EuclideanNorm(), 1000 );
1240}
1241
1242
1243// A driving angular dimension forces the angle between two segments.
1244BOOST_AUTO_TEST_CASE( AngularDimensionDrivesAngle )
1245{
1246 BOARD board;
1247
1248 PCB_SHAPE* ref = addSegment( board, { 0, 0 }, { 10 * MM, 0 } ); // along +x
1249 PCB_SHAPE* arm = addSegment( board, { 0, 0 }, { 8 * MM, 1 * MM } );
1250
1252 { { ref->m_Uuid, CONSTRAINT_ANCHOR::START } } );
1254 { { ref->m_Uuid, CONSTRAINT_ANCHOR::END } } );
1256 { { arm->m_Uuid, CONSTRAINT_ANCHOR::START } } );
1257
1259 { { ref->m_Uuid, CONSTRAINT_ANCHOR::WHOLE },
1260 { arm->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } } );
1261 dim->SetValue( 90.0 ); // degrees
1262
1263 std::vector<PCB_SHAPE*> shapes{ ref, arm };
1264 solveAndApply( board, shapes );
1265
1266 // arm starts at the origin; at 90 deg to the +x reference it is vertical.
1267 BOOST_CHECK_LE( std::abs( arm->GetEnd().x - arm->GetStart().x ), 100 );
1268}
1269
1270
1271// A fixed-radius driving value resizes an arc (verifies the arc setup solves).
1272BOOST_AUTO_TEST_CASE( FixedRadiusArc )
1273{
1274 BOARD board;
1275
1276 PCB_SHAPE* arc = addArc( board, { 5 * MM, 0 }, { 3535533, 3535533 }, { 0, 5 * MM } ); // R = 5mm
1277
1279 { { arc->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } } );
1280 fr->SetValue( 6.0 * MM );
1281
1282 std::vector<PCB_SHAPE*> shapes{ arc };
1283 solveAndApply( board, shapes );
1284
1285 BOOST_CHECK_LE( std::abs( arc->GetRadius() - 6 * MM ), 2000 );
1286}
1287
1288
1289// An arc made equal-radius to a fixed-radius reference arc takes that radius.
1290BOOST_AUTO_TEST_CASE( EqualRadiusArcs )
1291{
1292 BOARD board;
1293
1294 PCB_SHAPE* ref = addArc( board, { 5 * MM, 0 }, { 3535533, 3535533 }, { 0, 5 * MM } ); // R = 5mm
1295 // A clean R = 4 mm quarter arc centred at (30,0).
1296 PCB_SHAPE* other = addArc( board, { 34 * MM, 0 }, { 32828427, 2828427 }, { 30 * MM, 4 * MM } );
1297
1299 { { ref->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } } );
1300 fr->SetValue( 5.0 * MM );
1302 { { ref->m_Uuid, CONSTRAINT_ANCHOR::WHOLE },
1303 { other->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } } );
1304
1305 std::vector<PCB_SHAPE*> shapes{ ref, other };
1306 solveAndApply( board, shapes );
1307
1308 BOOST_CHECK_LE( std::abs( other->GetRadius() - 5 * MM ), 3000 );
1309}
1310
1311
1312// A bezier endpoint coincident with a fixed point maps and snaps onto that point, and each control
1313// handle rides along with its adjacent endpoint so the curve is translated rather than sheared.
1314BOOST_AUTO_TEST_CASE( BezierEndpointCoincidentSnaps )
1315{
1316 BOARD board;
1317
1318 PCB_SHAPE* anchor = addSegment( board, { 20 * MM, 5 * MM }, { 25 * MM, 5 * MM } );
1319 PCB_SHAPE* bezier = addBezier( board, { 0, 0 }, { 3 * MM, 8 * MM }, { 7 * MM, 8 * MM }, { 10 * MM, 0 } );
1320
1321 const VECTOR2I startBefore = bezier->GetStart();
1322 const VECTOR2I c1Before = bezier->GetBezierC1();
1323 const VECTOR2I c2Before = bezier->GetBezierC2();
1324 const VECTOR2I endBefore = bezier->GetEnd();
1325
1327 { { anchor->m_Uuid, CONSTRAINT_ANCHOR::START } } );
1329 { { bezier->m_Uuid, CONSTRAINT_ANCHOR::END },
1330 { anchor->m_Uuid, CONSTRAINT_ANCHOR::START } } );
1331
1332 std::vector<PCB_SHAPE*> shapes{ anchor, bezier };
1333 std::vector<PCB_CONSTRAINT*> constraints( board.Constraints().begin(), board.Constraints().end() );
1334
1336 BOOST_REQUIRE( adapter.Build( shapes, constraints ) );
1337
1338 // The coincident on the bezier endpoint is a real solver constraint, not silently dropped.
1339 BOOST_CHECK( adapter.UnmappedConstraints().empty() );
1340
1341 BOOST_REQUIRE( adapter.Solve() );
1342 adapter.Apply();
1343
1344 // The dragged-free endpoint landed on the fixed anchor.
1345 BOOST_CHECK_LE( ( bezier->GetEnd() - VECTOR2I( 20 * MM, 5 * MM ) ).EuclideanNorm(), 1000 );
1346
1347 // The far endpoint held; its control handle did not move.
1348 BOOST_CHECK_EQUAL( bezier->GetStart(), startBefore );
1349 BOOST_CHECK_EQUAL( bezier->GetBezierC1(), c1Before );
1350
1351 // The moved endpoint carried its own control handle by the same delta, preserving the curve.
1352 VECTOR2I endDelta = bezier->GetEnd() - endBefore;
1353 BOOST_CHECK_EQUAL( bezier->GetBezierC2() - c2Before, endDelta );
1354}
1355
1356
1357// Horizontal accepts two point anchors too not just a whole segment so picked points end level
1358BOOST_AUTO_TEST_CASE( HorizontalAlignsTwoPoints )
1359{
1360 BOARD board;
1361
1362 // Two separate segments whose end anchors start at different heights.
1363 PCB_SHAPE* a = addSegment( board, { 0, 0 }, { 5 * MM, 2 * MM } );
1364 PCB_SHAPE* b = addSegment( board, { 10 * MM, 0 }, { 15 * MM, 8 * MM } );
1365
1368 { b->m_Uuid, CONSTRAINT_ANCHOR::END } } );
1369
1370 std::vector<PCB_SHAPE*> shapes{ a, b };
1371 std::vector<PCB_CONSTRAINT*> constraints( board.Constraints().begin(), board.Constraints().end() );
1372
1374 BOOST_REQUIRE( adapter.Build( shapes, constraints ) );
1375
1376 // The two-point horizontal is a real solver constraint, not silently dropped.
1377 BOOST_CHECK( adapter.UnmappedConstraints().empty() );
1378
1379 BOOST_REQUIRE( adapter.Solve() );
1380 adapter.Apply();
1381
1382 BOOST_CHECK_LE( std::abs( a->GetEnd().y - b->GetEnd().y ), 1000 ); // within 1 um
1383}
1384
1385
1386// Vertical accepts two point anchors too so picked points end at same x
1387BOOST_AUTO_TEST_CASE( VerticalAlignsTwoPoints )
1388{
1389 BOARD board;
1390
1391 PCB_SHAPE* a = addSegment( board, { 0, 0 }, { 2 * MM, 5 * MM } );
1392 PCB_SHAPE* b = addSegment( board, { 0, 10 * MM }, { 8 * MM, 15 * MM } );
1393
1396 { b->m_Uuid, CONSTRAINT_ANCHOR::END } } );
1397
1398 std::vector<PCB_SHAPE*> shapes{ a, b };
1399 std::vector<PCB_CONSTRAINT*> constraints( board.Constraints().begin(), board.Constraints().end() );
1400
1402 BOOST_REQUIRE( adapter.Build( shapes, constraints ) );
1403 BOOST_CHECK( adapter.UnmappedConstraints().empty() );
1404
1405 BOOST_REQUIRE( adapter.Solve() );
1406 adapter.Apply();
1407
1408 BOOST_CHECK_LE( std::abs( a->GetEnd().x - b->GetEnd().x ), 1000 );
1409}
1410
1411
1412// Dragging a bezier endpoint that is coincident with a segment corner pulls the segment along.
1413BOOST_AUTO_TEST_CASE( DragBezierEndpointMovesCoincidentNeighbor )
1414{
1415 BOARD board;
1416
1417 PCB_SHAPE* bezier = addBezier( board, { 0, 0 }, { 3 * MM, 8 * MM }, { 7 * MM, 8 * MM }, { 10 * MM, 0 } );
1418 PCB_SHAPE* seg = addSegment( board, { 10 * MM, 0 }, { 10 * MM, 10 * MM } );
1419
1421 { { bezier->m_Uuid, CONSTRAINT_ANCHOR::END },
1422 { seg->m_Uuid, CONSTRAINT_ANCHOR::START } } );
1423
1425 std::vector<PCB_SHAPE*> modified;
1426
1427 CONSTRAINT_DIAGNOSIS diag = SolveCluster( &board, dragged, { 13 * MM, 4 * MM }, &modified );
1428
1429 BOOST_REQUIRE( diag.solved );
1430
1431 // The bezier end and the segment start moved together and stay coincident.
1432 BOOST_CHECK_LE( ( bezier->GetEnd() - seg->GetStart() ).EuclideanNorm(), 100 );
1433 BOOST_CHECK_LE( ( bezier->GetEnd() - VECTOR2I( 13 * MM, 4 * MM ) ).EuclideanNorm(), 1000 );
1434
1435 // The neighbor segment is reported modified; the dragged bezier is not.
1436 BOOST_CHECK( std::find( modified.begin(), modified.end(), seg ) != modified.end() );
1437 BOOST_CHECK( std::find( modified.begin(), modified.end(), bezier ) == modified.end() );
1438}
1439
1440
1441// Minimal movement issue 2 resolve moves fewest shapes
1442// Horizontal on X drops corner so tied start of Y follows but free end of Y stays put
1443BOOST_AUTO_TEST_CASE( MinimalMovementSettleHoldsSlackFreeEnd )
1444{
1445 BOARD board;
1446
1447 PCB_SHAPE* x = addSegment( board, { 0, 0 }, { 10 * MM, 2 * MM } ); // slightly sloped
1448 PCB_SHAPE* y = addSegment( board, { 10 * MM, 2 * MM }, { 10 * MM, 12 * MM } );
1449
1451 { { x->m_Uuid, CONSTRAINT_ANCHOR::END }, { y->m_Uuid, CONSTRAINT_ANCHOR::START } } );
1453 { { x->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } } );
1454
1455 const VECTOR2I xEnd0 = x->GetEnd();
1456 const VECTOR2I yEnd0 = y->GetEnd();
1457
1458 std::vector<PCB_SHAPE*> modified;
1459 ApplyConstraintImmediately( &board, horizontal, &modified, []( BOARD_ITEM* ) {} );
1460
1461 // X turned horizontal, so its end dropped to y = 0.
1462 BOOST_CHECK_LE( slopeDiffY( x ), 100 );
1463
1464 // Must move start of Y stayed coincident with moved corner
1465 BOOST_CHECK_LE( ( y->GetStart() - x->GetEnd() ).EuclideanNorm(), 5000.0 );
1466 BOOST_CHECK( x->GetEnd() != xEnd0 );
1467
1468 // Slack held free end of Y did not drift so Y grew instead of translating
1469 BOOST_CHECK_LE( ( y->GetEnd() - yEnd0 ).EuclideanNorm(), 5000.0 );
1470}
1471
1472
1473// A driving fixed length grows the edited segment; the solver must extend it along its current
1474// direction (its free end reaching the minimal spot) rather than rotating it, and the coincident
1475// neighbour's own free end must not drift. A plain re-solve rotates the free-direction segment to an
1476// arbitrary point on the length circle, dragging the neighbour with it.
1477BOOST_AUTO_TEST_CASE( MinimalMovementSettleExtendsWithoutRotating )
1478{
1479 BOARD board;
1480
1481 PCB_SHAPE* x = addSegment( board, { 0, 0 }, { 4 * MM, 0 } ); // along +x, fixed start
1482 PCB_SHAPE* y = addSegment( board, { 4 * MM, 0 }, { 4 * MM, 10 * MM } );
1483
1485 { { x->m_Uuid, CONSTRAINT_ANCHOR::START } } );
1487 { { x->m_Uuid, CONSTRAINT_ANCHOR::END }, { y->m_Uuid, CONSTRAINT_ANCHOR::START } } );
1489 { { x->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } }, 10.0 * MM );
1490
1491 const VECTOR2I yEnd0 = y->GetEnd();
1492
1493 std::vector<PCB_SHAPE*> modified;
1494 ApplyConstraintImmediately( &board, len, &modified, []( BOARD_ITEM* ) {} );
1495
1496 // Must move X extends straight to length 10 landing near x 10mm y 0 unrotated
1497 BOOST_CHECK_LE( ( x->GetEnd() - VECTOR2I( 10 * MM, 0 ) ).EuclideanNorm(), 5000.0 );
1498 BOOST_CHECK_LE( ( y->GetStart() - x->GetEnd() ).EuclideanNorm(), 5000.0 );
1499
1500 // Slack held free far end of neighbour stayed put
1501 BOOST_CHECK_LE( ( y->GetEnd() - yEnd0 ).EuclideanNorm(), 5000.0 );
1502}
1503
1504
1505// The must-move guarantee on the interactive drag path: dragging a coincident corner pulls the
1506// hard-tied neighbour end along while its opposite, unconstrained end stays put.
1507BOOST_AUTO_TEST_CASE( MinimalMovementDragHoldsNeighborFreeEnd )
1508{
1509 BOARD board;
1510
1511 PCB_SHAPE* a = addSegment( board, { 0, 0 }, { 10 * MM, 0 } );
1512 PCB_SHAPE* b = addSegment( board, { 10 * MM, 0 }, { 10 * MM, 10 * MM } );
1513
1516 { b->m_Uuid, CONSTRAINT_ANCHOR::START } } );
1517
1518 const VECTOR2I bEnd0 = b->GetEnd();
1519
1520 std::vector<PCB_SHAPE*> modified;
1522 { 12 * MM, 3 * MM }, &modified );
1523
1524 BOOST_REQUIRE( diag.solved );
1525
1526 // Must move coincident end followed dragged corner to cursor
1527 BOOST_CHECK_LE( ( a->GetEnd() - b->GetStart() ).EuclideanNorm(), 100 );
1528 BOOST_CHECK_LE( ( a->GetEnd() - VECTOR2I( 12 * MM, 3 * MM ) ).EuclideanNorm(), 5000.0 );
1529 BOOST_CHECK( std::find( modified.begin(), modified.end(), b ) != modified.end() );
1530
1531 // Slack held free far end of neighbour stayed put
1532 BOOST_CHECK_LE( ( b->GetEnd() - bEnd0 ).EuclideanNorm(), 5000.0 );
1533}
1534
1535
1536// Minimal movement issue 2 with multiple shapes edited together A and B translate as a pair only B touches slack neighbour N
1537// Fewest objects solve must leave N put so threading the full edited set stops B stay pin fighting N pin
1538BOOST_AUTO_TEST_CASE( MinimalMovementMultiEditedHoldsSlackNeighbor )
1539{
1540 BOARD board;
1541
1542 PCB_SHAPE* a = addSegment( board, { 0, 0 }, { 10 * MM, 0 } );
1543 PCB_SHAPE* b = addSegment( board, { 10 * MM, 0 }, { 20 * MM, 0 } );
1544 PCB_SHAPE* n = addSegment( board, { 20 * MM, 0 }, { 30 * MM, 0 } );
1545
1547 { { a->m_Uuid, CONSTRAINT_ANCHOR::END }, { b->m_Uuid, CONSTRAINT_ANCHOR::START } } );
1549 { { b->m_Uuid, CONSTRAINT_ANCHOR::END }, { n->m_Uuid, CONSTRAINT_ANCHOR::START } } );
1550
1551 const VECTOR2I nStart0 = n->GetStart();
1552 const VECTOR2I nEnd0 = n->GetEnd();
1553
1554 a->Move( { 0, 5 * MM } );
1555 b->Move( { 0, 5 * MM } );
1556
1557 std::vector<PCB_SHAPE*> modified;
1558 ReSolveShapeClusters( &board, { a, b }, &modified );
1559
1560 // The slack neighbour is untouched -- both endpoints hold and it is not staged as modified.
1561 BOOST_CHECK_LE( ( n->GetStart() - nStart0 ).EuclideanNorm(), 100 );
1562 BOOST_CHECK_LE( ( n->GetEnd() - nEnd0 ).EuclideanNorm(), 100 );
1563 BOOST_CHECK( std::find( modified.begin(), modified.end(), n ) == modified.end() );
1564
1565 // Both edited shapes keep the geometry the edit gave them at the pinned/seed corner.
1566 BOOST_CHECK_LE( ( a->GetStart() - VECTOR2I( 0, 5 * MM ) ).EuclideanNorm(), 5000.0 );
1567 BOOST_CHECK_LE( ( a->GetEnd() - VECTOR2I( 10 * MM, 5 * MM ) ).EuclideanNorm(), 5000.0 );
1568 BOOST_CHECK_LE( ( b->GetStart() - VECTOR2I( 10 * MM, 5 * MM ) ).EuclideanNorm(), 5000.0 );
1569}
1570
1571
1572// The draw-time binding heuristic prefers a single object whose anchors reach both dimension
1573// endpoints, binding START and END to that one object.
1574BOOST_AUTO_TEST_CASE( DimensionBindingPrefersSameObject )
1575{
1576 BOARD board;
1577
1578 PCB_SHAPE* seg = addSegment( board, { 0, 0 }, { 10 * MM, 0 } );
1579
1580 KIID fakeDim; // a dimension uuid that is not on the board, as during interactive draw
1581 std::vector<ENDPOINT_BINDING> bindings =
1582 SelectEndpointBindings( &board, fakeDim, { 0, 0 }, VECTOR2I( 10 * MM, 0 ), 1000.0 );
1583
1584 BOOST_REQUIRE_EQUAL( bindings.size(), 2 );
1585
1586 // Both ends bind to the same segment, one to START and one to END.
1587 BOOST_CHECK( bindings[0].sourceAnchor == CONSTRAINT_ANCHOR::START );
1588 BOOST_CHECK( bindings[0].target == CONSTRAINT_MEMBER( seg->m_Uuid, CONSTRAINT_ANCHOR::START ) );
1589 BOOST_CHECK( bindings[1].sourceAnchor == CONSTRAINT_ANCHOR::END );
1590 BOOST_CHECK( bindings[1].target == CONSTRAINT_MEMBER( seg->m_Uuid, CONSTRAINT_ANCHOR::END ) );
1591}
1592
1593
1594// When no single object reaches both endpoints, each end binds to its own nearest anchor, on
1595// different objects.
1596BOOST_AUTO_TEST_CASE( DimensionBindingPerEndpointDifferentObjects )
1597{
1598 BOARD board;
1599
1600 PCB_SHAPE* a = addSegment( board, { 0, 0 }, { 5 * MM, 5 * MM } );
1601 PCB_SHAPE* b = addSegment( board, { 20 * MM, 0 }, { 25 * MM, 5 * MM } );
1602
1603 KIID fakeDim;
1604 std::vector<ENDPOINT_BINDING> bindings =
1605 SelectEndpointBindings( &board, fakeDim, { 0, 0 }, VECTOR2I( 20 * MM, 0 ), 1000.0 );
1606
1607 BOOST_REQUIRE_EQUAL( bindings.size(), 2 );
1608 BOOST_CHECK( bindings[0].target == CONSTRAINT_MEMBER( a->m_Uuid, CONSTRAINT_ANCHOR::START ) );
1609 BOOST_CHECK( bindings[1].target == CONSTRAINT_MEMBER( b->m_Uuid, CONSTRAINT_ANCHOR::START ) );
1610}
1611
1612
1613// Only one endpoint near an object still binds (partial is fine); the far endpoint is left free.
1614BOOST_AUTO_TEST_CASE( DimensionBindingPartial )
1615{
1616 BOARD board;
1617
1618 PCB_SHAPE* a = addSegment( board, { 0, 0 }, { 5 * MM, 5 * MM } );
1619
1620 KIID fakeDim;
1621 std::vector<ENDPOINT_BINDING> bindings =
1622 SelectEndpointBindings( &board, fakeDim, { 0, 0 }, VECTOR2I( 50 * MM, 50 * MM ), 1000.0 );
1623
1624 BOOST_REQUIRE_EQUAL( bindings.size(), 1 );
1625 BOOST_CHECK( bindings[0].sourceAnchor == CONSTRAINT_ANCHOR::START );
1626 BOOST_CHECK( bindings[0].target == CONSTRAINT_MEMBER( a->m_Uuid, CONSTRAINT_ANCHOR::START ) );
1627 (void) a;
1628}
1629
1630
1631// Single object with START nearest both dimension ends must still bind both via distinct anchor pair not split END onto closer neighbour
1632// End near A START at 2mm beats A END at 8mm yet A spans both through START and END so A wins over B
1633BOOST_AUTO_TEST_CASE( DimensionBindingDistinctPairOverSplit )
1634{
1635 BOARD board;
1636
1637 PCB_SHAPE* a = addSegment( board, { 0, 0 }, { 10 * MM, 0 } );
1638 PCB_SHAPE* b = addSegment( board, { 2 * MM, 1 * MM }, { 2 * MM, 20 * MM } );
1639
1640 KIID fakeDim;
1641 std::vector<ENDPOINT_BINDING> bindings =
1642 SelectEndpointBindings( &board, fakeDim, { 0, 0 }, VECTOR2I( 2 * MM, 0 ), 12.0 * MM );
1643
1644 BOOST_REQUIRE_EQUAL( bindings.size(), 2 );
1645
1646 // Both ends bind to segment A on distinct anchors; segment B never enters the binding.
1647 BOOST_CHECK( bindings[0].sourceAnchor == CONSTRAINT_ANCHOR::START );
1648 BOOST_CHECK( bindings[0].target == CONSTRAINT_MEMBER( a->m_Uuid, CONSTRAINT_ANCHOR::START ) );
1649 BOOST_CHECK( bindings[1].sourceAnchor == CONSTRAINT_ANCHOR::END );
1650 BOOST_CHECK( bindings[1].target == CONSTRAINT_MEMBER( a->m_Uuid, CONSTRAINT_ANCHOR::END ) );
1651 (void) b;
1652}
1653
1654
void ReSolveAfterShapeResize(BOARD *aBoard, PCB_SHAPE *aShape, std::vector< PCB_SHAPE * > *aModified, const std::function< void(BOARD_ITEM *)> &aBeforeModify)
Re-solve after a resize, e.g. a circle radius edit. Holds aShape fixed so its neighbors adjust.
CONSTRAINT_DIAGNOSIS ApplyConstraintImmediately(BOARD *aBoard, const PCB_CONSTRAINT *aConstraint, std::vector< PCB_SHAPE * > *aModified, const std::function< void(BOARD_ITEM *)> &aBeforeModify, const std::set< KIID > &aFixedShapes)
Solve a just-created constraint's cluster so the geometry snaps to satisfy it (SolidWorks-style),...
CONSTRAINT_DIAGNOSIS SolveCluster(BOARD *aBoard, const CONSTRAINT_MEMBER &aDragged, const VECTOR2I &aCursor, std::vector< PCB_SHAPE * > *aModified, const std::function< void(BOARD_ITEM *)> &aBeforeModify, bool aIncludeDragged, bool aStabilize, const std::set< KIID > &aEdited, const std::optional< std::pair< CONSTRAINT_MEMBER, VECTOR2I > > &aCoDragged, const std::set< KIID > &aFixedShapes, bool aHoldDraggedRigid)
Gather the cluster of shapes transitively constrained with the dragged shape, solve with the dragged ...
void ReSolveShapeClusters(BOARD *aBoard, const std::vector< PCB_SHAPE * > &aShapes, std::vector< PCB_SHAPE * > *aModified, const std::function< void(BOARD_ITEM *)> &aBeforeModify)
Re-solve the clusters of shapes edited outside the solver, e.g.
bool ReSolveShapeClustersHoldingEdited(BOARD *aBoard, const std::vector< PCB_SHAPE * > &aEditedShapes, std::vector< PCB_SHAPE * > *aModified, const std::function< void(BOARD_ITEM *)> &aBeforeModify)
Re-solve clusters whose new geometry is authoritative holding every edited shape fully fixed so only ...
Translates KiCad board geometry to and from the planegcs solver (issue #2329).
bool Solve(const CONSTRAINT_MEMBER &aDragged, const VECTOR2I &aCursor, bool aStabilize=false, const std::set< KIID > &aEdited={}, const std::optional< std::pair< CONSTRAINT_MEMBER, VECTOR2I > > &aCoDragged=std::nullopt, bool aHoldDraggedRigid=false)
Solve the system, pinning a dragged anchor to a cursor position.
const std::vector< KIID > & UnmappedConstraints() const
Constraints from the last Build() that could not be mapped onto a solver primitive (wrong member coun...
bool Build(const std::vector< PCB_SHAPE * > &aShapes, const std::vector< PCB_CONSTRAINT * > &aConstraints, const std::set< KIID > *aFixedShapes=nullptr, const std::vector< PCB_DIMENSION_BASE * > &aDimensions={})
Translate a cluster into a planegcs system.
std::vector< PCB_SHAPE * > Apply(const std::function< void(BOARD_ITEM *)> &aBeforeWrite={})
Write the solved coordinates back into the shapes, de-normalized to IU.
CONSTRAINT_DIAGNOSIS Diagnose()
Report degrees of freedom and conflicting/redundant constraints.
A base class for any item which can be embedded within the BOARD container class, and therefore insta...
Definition board_item.h:83
void SetLocked(bool aLocked) override
Definition board_item.h:386
Information pertinent to a Pcbnew printed circuit board.
Definition board.h:373
void Add(BOARD_ITEM *aItem, ADD_MODE aMode=ADD_MODE::INSERT, bool aSkipConnectivity=false) override
Removes an item from the container.
Definition board.cpp:1355
const CONSTRAINTS & Constraints() const
Geometric constraints (#2329) owned by this board.
Definition board.h:465
const KIID m_Uuid
Definition eda_item.h:531
int GetEllipseMinorRadius() const
Definition eda_shape.h:310
const VECTOR2I & GetEllipseCenter() const
Definition eda_shape.h:292
void SetCornerRadius(int aRadius)
int GetEllipseMajorRadius() const
Definition eda_shape.h:301
SHAPE_POLY_SET & GetPolyShape()
EDA_ANGLE GetEllipseRotation() const
Definition eda_shape.h:319
int GetRadius() const
const VECTOR2I & GetEnd() const
Return the ending point of the graphic.
Definition eda_shape.h:240
void SetRadius(int aX)
Definition eda_shape.h:265
const VECTOR2I & GetStart() const
Return the starting point of the graphic.
Definition eda_shape.h:190
void SetPolyPoints(const std::vector< VECTOR2I > &aPoints)
int GetCornerRadius() const
Definition kiid.h:46
A geometric constraint between board items (issue #2329).
void SetValue(std::optional< double > aValue)
void SetDriving(bool aDriving)
VECTOR2I GetCenter() const override
This defaults to the center of the bounding box if not overridden.
Definition pcb_shape.h:78
void SetEllipseMinorRadius(int aR) override
void Move(const VECTOR2I &aMoveVector) override
Move this object.
Represent a polyline containing arcs as well as line segments: A chain of connected line and/or arc s...
int PointCount() const
Return the number of points (vertices) in this line chain.
const VECTOR2I & CPoint(int aIndex) const
Return a reference to a given point in the line chain.
size_t ArcCount() const
Represent a set of closed polygons.
int AddOutline(const SHAPE_LINE_CHAIN &aOutline)
Adds a new outline to the set and returns its index.
int HoleCount(int aOutline) const
Returns the number of holes in a given outline.
int Append(int x, int y, int aOutline=-1, int aHole=-1, bool aAllowDuplication=false)
Appends a vertex at the end of the given outline/hole (default: the last outline)
int NewOutline()
Creates a new empty polygon in the set and returns its index.
int NewHole(int aOutline=-1)
Creates a new hole in a given outline.
int OutlineCount() const
Return the number of outlines in the set.
const SHAPE_LINE_CHAIN & COutline(int aIndex) const
constexpr extended_type Cross(const VECTOR2< T > &aVector) const
Compute cross product of self with aVector.
Definition vector2d.h:534
T EuclideanNorm() const
Compute the Euclidean norm of the vector, which is defined as sqrt(x ** 2 + y ** 2).
Definition vector2d.h:279
std::vector< ENDPOINT_BINDING > SelectEndpointBindings(BOARD *aBoard, const KIID &aItem, const VECTOR2I &aStart, const std::optional< VECTOR2I > &aEnd, double aMaxDist)
Choose the coincident bindings a freshly drawn item endpoints should take so it tracks the geometry i...
@ DEGREES_T
Definition eda_angle.h:31
PCB_SHAPE * addSegment(BOARD &aBoard, const VECTOR2I &aStart, const VECTOR2I &aEnd)
PCB_SHAPE * addEllipseArc(BOARD &aBoard, const VECTOR2I &aCenter, int aMajor, int aMinor, const EDA_ANGLE &aRotation, const EDA_ANGLE &aStart, const EDA_ANGLE &aEnd)
PCB_SHAPE * addPoly(BOARD &aBoard, const std::vector< VECTOR2I > &aPoints)
double ellipseEquationAt(const PCB_SHAPE *aEllipse, const VECTOR2I &aPos)
Squared-normalized ellipse equation value at aPos: 1.0 exactly on the outline.
PCB_SHAPE * addArc(BOARD &aBoard, const VECTOR2I &aStart, const VECTOR2I &aMid, const VECTOR2I &aEnd)
PCB_SHAPE * addCircle(BOARD &aBoard, const VECTOR2I &aCenter, int aRadius)
PCB_SHAPE * addBezier(BOARD &aBoard, const VECTOR2I &aStart, const VECTOR2I &aCtrl1, const VECTOR2I &aCtrl2, const VECTOR2I &aEnd)
void solveAndApply(BOARD &aBoard, const std::vector< PCB_SHAPE * > &aShapes)
Build the cluster of aShapes against every constraint on aBoard, solve it, and write the result back ...
PCB_SHAPE * addEllipse(BOARD &aBoard, const VECTOR2I &aCenter, int aMajor, int aMinor, const EDA_ANGLE &aRotation)
constexpr int MM
PCB_CONSTRAINT * addConstraint(BOARD &aBoard, PCB_CONSTRAINT_TYPE aType, const std::vector< CONSTRAINT_MEMBER > &aMembers, std::optional< double > aValue=std::nullopt)
PCB_SHAPE * addRect(BOARD &aBoard, const VECTOR2I &aStart, const VECTOR2I &aEnd)
EDA_ANGLE abs(const EDA_ANGLE &aAngle)
Definition eda_angle.h:400
@ VERTEX
An indexed rectangle corner or polygon outline vertex; pairs with CONSTRAINT_MEMBER::m_index.
@ WHOLE
The item as a whole (a segment as a line, a circle).
@ START
First endpoint of a segment or arc.
@ END
Second endpoint of a segment or arc.
@ CENTER
Center of an arc or circle.
@ CONCENTRIC
Two arcs/circles share a center.
@ SYMMETRIC
Two points are mirror images about an axis.
@ FIXED_POSITION
A point is locked at its current location.
@ VERTICAL
A segment (or two points) is vertical.
@ TANGENT
A line and a curve, or two curves, touch tangentially.
@ COINCIDENT
Two points are made to coincide.
@ PERPENDICULAR
Two segments are perpendicular.
@ FIXED_RADIUS
An arc/circle has a driving radius value.
@ HORIZONTAL
A segment (or two points) is horizontal.
@ EQUAL_RADIUS
Two arcs/circles have equal radius.
@ MIDPOINT
A point is the midpoint of a segment.
@ POINT_ON_LINE
A point lies on a segment's supporting line.
@ FIXED_LENGTH
A segment has a driving length value.
@ ANGULAR_DIMENSION
An angle between members (driving or reference).
@ COLLINEAR
Two segments lie on the same line.
@ PARALLEL
Two segments are parallel.
@ EQUAL_LENGTH
Two segments have equal length.
The outcome of a constraint solve, in plain data so callers need not know planegcs.
bool solved
Solver reached Success or Converged.
std::vector< KIID > redundant
Constraints the solver reports as redundant.
int freeDof
Remaining degrees of freedom (-1 if not diagnosed).
One participant in a constraint: a referenced board item plus the feature of that item that participa...
BOOST_AUTO_TEST_CASE(HorizontalAlignment)
BOOST_AUTO_TEST_SUITE(CadstarPartParser)
BOOST_AUTO_TEST_CASE(ParallelToFixedHorizontal)
BOOST_REQUIRE(intersection.has_value()==c.ExpectedIntersection.has_value())
BOOST_AUTO_TEST_SUITE_END()
const SHAPE_LINE_CHAIN chain
SHAPE_CIRCLE circle(c.m_circle_center, c.m_circle_radius)
BOOST_CHECK_EQUAL(result, "25.4")
VECTOR2< int32_t > VECTOR2I
Definition vector2d.h:683
VECTOR2< double > VECTOR2D
Definition vector2d.h:682