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
528// A properties edit of a polygon's geometry is authoritative, so the hold-edited re-solve keeps the
529// edited outline exactly, the vertex-bound segment follows, and the unbound vertices hold.
530BOOST_AUTO_TEST_CASE( HoldingEditedPolygonResizeMovesBoundNeighbor )
531{
532 BOARD board;
533
534 std::vector<VECTOR2I> pts = { { 0, 0 },
535 { 20 * MM, 5 * MM },
536 { 15 * MM, 20 * MM },
537 { 5 * MM, 20 * MM },
538 { -5 * MM, 10 * MM } };
539 PCB_SHAPE* poly = addPoly( board, pts );
540 PCB_SHAPE* seg = addSegment( board, pts[1], { 40 * MM, 0 } );
541
544
545 // Move the 1-2 edge 3 mm right, as a typed geometry edit would.
546 std::vector<VECTOR2I> dragged = pts;
547 dragged[1] += VECTOR2I( 3 * MM, 0 );
548 dragged[2] += VECTOR2I( 3 * MM, 0 );
549 poly->SetPolyPoints( dragged );
550
551 std::vector<PCB_SHAPE*> modified;
552 ReSolveShapeClustersHoldingEdited( &board, { poly }, &modified );
553
554 const SHAPE_LINE_CHAIN& outline = poly->GetPolyShape().COutline( 0 );
555
556 BOOST_CHECK_LE( ( outline.CPoint( 1 ) - dragged[1] ).EuclideanNorm(), 5000.0 );
557 BOOST_CHECK_LE( ( outline.CPoint( 2 ) - dragged[2] ).EuclideanNorm(), 5000.0 );
558 BOOST_CHECK_LE( ( seg->GetStart() - dragged[1] ).EuclideanNorm(), 5000.0 );
559 BOOST_CHECK( std::find( modified.begin(), modified.end(), seg ) != modified.end() );
560
561 for( int i : { 0, 3, 4 } )
562 BOOST_CHECK_LE( ( outline.CPoint( i ) - pts[i] ).EuclideanNorm(), 5000.0 );
563}
564
565
566// A resize solve holds the resized ellipse's minor radius; with the focus offset fixed at Build,
567// that pins the whole shape, so the user's new size survives the re-solve undistorted.
568BOOST_AUTO_TEST_CASE( ResizeEllipseKeepsShape )
569{
570 BOARD board;
571
572 PCB_SHAPE* resized = addEllipse( board, { 0, 0 }, 6 * MM, 3 * MM, EDA_ANGLE( 30.0, DEGREES_T ) );
573 PCB_SHAPE* neighbor = addCircle( board, { 20 * MM, 0 }, 2 * MM );
574
576 { { resized->m_Uuid, CONSTRAINT_ANCHOR::WHOLE }, { neighbor->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } } );
577
578 // Shrink the minor radius, as the resize handle would.
579 resized->SetEllipseMinorRadius( 2 * MM );
580
581 ReSolveAfterShapeResize( &board, resized, nullptr );
582
583 BOOST_CHECK_LE( std::abs( resized->GetEllipseMajorRadius() - 6 * MM ), 5000 );
584 BOOST_CHECK_LE( std::abs( resized->GetEllipseMinorRadius() - 2 * MM ), 5000 );
585 BOOST_CHECK_LE(
586 std::abs( ( resized->GetEllipseRotation() - EDA_ANGLE( 30.0, DEGREES_T ) ).Normalize180().AsDegrees() ),
587 0.01 );
588
589 // The circle came to the ellipse's center; the resized shape stayed put.
590 BOOST_CHECK_LE( ( resized->GetEllipseCenter() - VECTOR2I( 0, 0 ) ).EuclideanNorm(), 5000.0 );
591 BOOST_CHECK_LE( ( neighbor->GetCenter() - resized->GetEllipseCenter() ).EuclideanNorm(), 5000.0 );
592}
593
594
595// Free ellipse concentric with locked circle moves center without distorting
596// Focus follows center so major and minor radius and rotation stay preserved
597BOOST_AUTO_TEST_CASE( ConcentricEllipseKeepsShape )
598{
599 BOARD board;
600
601 PCB_SHAPE* circle = addCircle( board, { 20 * MM, 15 * MM }, 3 * MM );
602 PCB_SHAPE* ellipse = addEllipse( board, { 0, 0 }, 8 * MM, 4 * MM, EDA_ANGLE( 30.0, DEGREES_T ) );
603
604 circle->SetLocked( true );
605
607 { { ellipse->m_Uuid, CONSTRAINT_ANCHOR::WHOLE }, { circle->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } } );
608
609 std::vector<PCB_SHAPE*> shapes{ circle, ellipse };
610 solveAndApply( board, shapes );
611
612 BOOST_CHECK_LE( ( ellipse->GetEllipseCenter() - VECTOR2I( 20 * MM, 15 * MM ) ).EuclideanNorm(), 5000.0 );
613 BOOST_CHECK_LE( std::abs( ellipse->GetEllipseMajorRadius() - 8 * MM ), 5000 );
614 BOOST_CHECK_LE( std::abs( ellipse->GetEllipseMinorRadius() - 4 * MM ), 5000 );
615 BOOST_CHECK_LE(
616 std::abs( ( ellipse->GetEllipseRotation() - EDA_ANGLE( 30.0, DEGREES_T ) ).Normalize180().AsDegrees() ),
617 0.01 );
618}
619
620
621// A probe point made the midpoint of a fixed segment lands at its center.
622BOOST_AUTO_TEST_CASE( MidpointOfFixedSegment )
623{
624 BOARD board;
625
626 PCB_SHAPE* seg = addSegment( board, { 0, 0 }, { 10 * MM, 4 * MM } ); // midpoint (5,2) mm
627 PCB_SHAPE* probe = addSegment( board, { 1 * MM, 1 * MM }, { 2 * MM, 2 * MM } );
628
630 { { seg->m_Uuid, CONSTRAINT_ANCHOR::START } } );
632 { { seg->m_Uuid, CONSTRAINT_ANCHOR::END } } );
634 { { probe->m_Uuid, CONSTRAINT_ANCHOR::START },
635 { seg->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } } );
636
637 std::vector<PCB_SHAPE*> shapes{ seg, probe };
638 solveAndApply( board, shapes );
639
640 BOOST_CHECK_LE( ( probe->GetStart() - VECTOR2I( 5 * MM, 2 * MM ) ).EuclideanNorm(), 1000 );
641}
642
643
644// A free segment made collinear with a fixed horizontal reference lands on the reference's line.
645BOOST_AUTO_TEST_CASE( CollinearWithFixedReference )
646{
647 BOARD board;
648
649 PCB_SHAPE* ref = addSegment( board, { 0, 0 }, { 10 * MM, 0 } ); // the y=0 line
650 PCB_SHAPE* free = addSegment( board, { 2 * MM, 5 * MM }, { 8 * MM, 6 * MM } );
651
653 { { ref->m_Uuid, CONSTRAINT_ANCHOR::START } } );
655 { { ref->m_Uuid, CONSTRAINT_ANCHOR::END } } );
658 { free->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } } );
659
660 std::vector<PCB_SHAPE*> shapes{ ref, free };
661 solveAndApply( board, shapes );
662
663 BOOST_CHECK_LE( std::abs( free->GetStart().y ), 100 );
664 BOOST_CHECK_LE( std::abs( free->GetEnd().y ), 100 );
665}
666
667
668// Two points made symmetric about a fixed vertical axis end up mirrored across it.
669BOOST_AUTO_TEST_CASE( SymmetricAboutFixedAxis )
670{
671 BOARD board;
672
673 // Axis is the fixed vertical line x = 5 mm.
674 PCB_SHAPE* axis = addSegment( board, { 5 * MM, 0 }, { 5 * MM, 10 * MM } );
675 PCB_SHAPE* segA = addSegment( board, { 2 * MM, 3 * MM }, { 2 * MM, 4 * MM } );
676 PCB_SHAPE* segB = addSegment( board, { 9 * MM, 3 * MM }, { 9 * MM, 4 * MM } );
677
679 { { axis->m_Uuid, CONSTRAINT_ANCHOR::START } } );
681 { { axis->m_Uuid, CONSTRAINT_ANCHOR::END } } );
683 { { segA->m_Uuid, CONSTRAINT_ANCHOR::START } } );
685 { { segA->m_Uuid, CONSTRAINT_ANCHOR::START },
686 { segB->m_Uuid, CONSTRAINT_ANCHOR::START },
687 { axis->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } } );
688
689 std::vector<PCB_SHAPE*> shapes{ axis, segA, segB };
690 solveAndApply( board, shapes );
691
692 // segB.start is the mirror of segA.start (2mm) across x = 5mm, i.e. x = 8mm, same y.
693 BOOST_CHECK_LE( std::abs( segB->GetStart().x - 8 * MM ), 1000 );
694 BOOST_CHECK_LE( std::abs( segB->GetStart().y - segA->GetStart().y ), 1000 );
695}
696
697
698// A segment with a fixed start and a driving fixed-length takes that length.
699BOOST_AUTO_TEST_CASE( FixedLengthDrivesSegmentLength )
700{
701 BOARD board;
702
703 PCB_SHAPE* seg = addSegment( board, { 0, 0 }, { 3 * MM, 0 } );
704
706 { { seg->m_Uuid, CONSTRAINT_ANCHOR::START } } );
707
709 { { seg->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } } );
710 len->SetValue( 8.0 * MM );
711
712 std::vector<PCB_SHAPE*> shapes{ seg };
713 solveAndApply( board, shapes );
714
715 double length = ( seg->GetEnd() - seg->GetStart() ).EuclideanNorm();
716 BOOST_CHECK_LE( std::abs( length - 8.0 * MM ), 1000 );
717}
718
719
720// Driving fixed length over two point anchors not a whole segment drives distance between them
721// Matches aligned dimension Driving mode binding its own START and END
722BOOST_AUTO_TEST_CASE( FixedLengthTwoPointDrivesDistance )
723{
724 BOARD board;
725
726 PCB_SHAPE* seg = addSegment( board, { 0, 0 }, { 3 * MM, 0 } );
727
729 { { seg->m_Uuid, CONSTRAINT_ANCHOR::START } } );
730
733 { seg->m_Uuid, CONSTRAINT_ANCHOR::END } } );
734 len->SetValue( 8.0 * MM );
735 len->SetDriving( true );
736
737 std::vector<PCB_SHAPE*> shapes{ seg };
738 solveAndApply( board, shapes );
739
740 double length = ( seg->GetEnd() - seg->GetStart() ).EuclideanNorm();
741 BOOST_CHECK_LE( std::abs( length - 8.0 * MM ), 1000 );
742 BOOST_CHECK_LE( ( seg->GetEnd() - VECTOR2I( 8 * MM, 0 ) ).EuclideanNorm(), 5000 );
743}
744
745
746// A rectangle's corners are aliases over its two stored corners, so a driving fixed length between
747// two adjacent corners resizes the rectangle while it stays a rectangle by construction.
748BOOST_AUTO_TEST_CASE( RectCornerFixedLengthDrivesWidth )
749{
750 BOARD board;
751
752 PCB_SHAPE* rect = addRect( board, { 10 * MM, 10 * MM }, { 50 * MM, 40 * MM } );
753
754 // VERTEX 0 and 1 are the top-left and top-right corners, so this drives the width.
756 { { rect->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 0 },
757 { rect->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 1 } },
758 30.0 * MM );
759 len->SetDriving( true );
760
761 std::vector<PCB_SHAPE*> shapes{ rect };
762 std::vector<PCB_CONSTRAINT*> constraints( board.Constraints().begin(), board.Constraints().end() );
763
765 BOOST_REQUIRE( adapter.Build( shapes, constraints ) );
766 BOOST_CHECK( adapter.UnmappedConstraints().empty() );
767 BOOST_REQUIRE( adapter.Solve( true ) );
768 adapter.Apply();
769
770 int width = std::abs( rect->GetEnd().x - rect->GetStart().x );
771 int height = std::abs( rect->GetEnd().y - rect->GetStart().y );
772
773 BOOST_CHECK_LE( std::abs( width - 30 * MM ), 5000 );
774 BOOST_CHECK_LE( std::abs( height - 30 * MM ), 5000 );
775
776 // The corners kept their min/max relationship, so the rect did not fold through itself.
777 BOOST_CHECK_LT( rect->GetStart().x, rect->GetEnd().x );
778 BOOST_CHECK_LT( rect->GetStart().y, rect->GetEnd().y );
779}
780
781
782// Same physical rectangle stored with start and end swapped
783// VERTEX indices canonicalized from initial geometry so 0 and 3 still name left edge corners
784BOOST_AUTO_TEST_CASE( RectSwappedStorageBindsSamePhysicalCorners )
785{
786 BOARD board;
787
788 // Stored start is the bottom-right corner; the physical rect is (10,10)-(50,40).
789 PCB_SHAPE* rect = addRect( board, { 50 * MM, 40 * MM }, { 10 * MM, 10 * MM } );
790
792 { { rect->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 0 },
793 { rect->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 3 } },
794 20.0 * MM );
795 len->SetDriving( true );
796
797 std::vector<PCB_SHAPE*> shapes{ rect };
798 std::vector<PCB_CONSTRAINT*> constraints( board.Constraints().begin(), board.Constraints().end() );
799
801 BOOST_REQUIRE( adapter.Build( shapes, constraints ) );
802 BOOST_CHECK( adapter.UnmappedConstraints().empty() );
803 BOOST_REQUIRE( adapter.Solve( true ) );
804 adapter.Apply();
805
806 int width = std::abs( rect->GetEnd().x - rect->GetStart().x );
807 int height = std::abs( rect->GetEnd().y - rect->GetStart().y );
808
809 BOOST_CHECK_LE( std::abs( height - 20 * MM ), 5000 );
810 BOOST_CHECK_LE( std::abs( width - 40 * MM ), 5000 );
811}
812
813
814// An out-of-range corner index cannot map; Build still succeeds and the geometry is untouched.
815BOOST_AUTO_TEST_CASE( RectInvalidVertexIndexUnmapped )
816{
817 BOARD board;
818
819 PCB_SHAPE* rect = addRect( board, { 10 * MM, 10 * MM }, { 50 * MM, 40 * MM } );
820
822 { { rect->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 7 },
823 { rect->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 0 } },
824 30.0 * MM );
825 len->SetDriving( true );
826
827 std::vector<PCB_SHAPE*> shapes{ rect };
828 std::vector<PCB_CONSTRAINT*> constraints( board.Constraints().begin(), board.Constraints().end() );
829
831 BOOST_REQUIRE( adapter.Build( shapes, constraints ) );
832
833 BOOST_REQUIRE_EQUAL( adapter.UnmappedConstraints().size(), 1 );
834 BOOST_CHECK( adapter.UnmappedConstraints().front() == len->m_Uuid );
835
836 BOOST_REQUIRE( adapter.Solve( true ) );
837 adapter.Apply();
838
839 BOOST_CHECK_EQUAL( rect->GetStart(), VECTOR2I( 10 * MM, 10 * MM ) );
840 BOOST_CHECK_EQUAL( rect->GetEnd(), VECTOR2I( 50 * MM, 40 * MM ) );
841}
842
843
844// Shrinking rounded rectangle reclamps stored corner radius
845// SetStart and SetEnd alone leave serialized radius out of range so Apply must push it back through clamping setter
846BOOST_AUTO_TEST_CASE( RectShrinkReclampsCornerRadius )
847{
848 BOARD board;
849
850 PCB_SHAPE* rect = addRect( board, { 10 * MM, 10 * MM }, { 50 * MM, 40 * MM } );
851 rect->SetCornerRadius( 10 * MM );
852
854 { { rect->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 0 },
855 { rect->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 1 } },
856 8.0 * MM );
857 len->SetDriving( true );
858
859 std::vector<PCB_SHAPE*> shapes{ rect };
860 std::vector<PCB_CONSTRAINT*> constraints( board.Constraints().begin(), board.Constraints().end() );
861
863 BOOST_REQUIRE( adapter.Build( shapes, constraints ) );
864 BOOST_REQUIRE( adapter.Solve( true ) );
865 adapter.Apply();
866
867 int width = std::abs( rect->GetEnd().x - rect->GetStart().x );
868
869 BOOST_CHECK_LE( std::abs( width - 8 * MM ), 5000 );
870
871 // The radius clamped to half the new shorter side, so the stored value stays coherent.
872 BOOST_CHECK_LE( rect->GetCornerRadius(), width / 2 );
873 BOOST_CHECK_LE( std::abs( rect->GetCornerRadius() - 4 * MM ), 5000 );
874}
875
876
877// A solve that would squash a rect side below the sub-micron floor is a collapse, not intent, so
878// Apply must leave the geometry untouched.
879BOOST_AUTO_TEST_CASE( RectCollapseGuardHoldsGeometry )
880{
881 BOARD board;
882
883 PCB_SHAPE* rect = addRect( board, { 10 * MM, 10 * MM }, { 50 * MM, 40 * MM } );
884
885 // A driving width below the collapse floor (0.5 um) deterministically forces the collapse.
887 { { rect->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 0 },
888 { rect->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 1 } },
889 0.0005 * MM );
890 len->SetDriving( true );
891
892 std::vector<PCB_SHAPE*> shapes{ rect };
893 std::vector<PCB_CONSTRAINT*> constraints( board.Constraints().begin(), board.Constraints().end() );
894
896 BOOST_REQUIRE( adapter.Build( shapes, constraints ) );
897 adapter.Solve( true );
898
899 std::vector<PCB_SHAPE*> changed = adapter.Apply();
900
901 BOOST_CHECK( changed.empty() );
902 BOOST_CHECK_EQUAL( rect->GetStart(), VECTOR2I( 10 * MM, 10 * MM ) );
903 BOOST_CHECK_EQUAL( rect->GetEnd(), VECTOR2I( 50 * MM, 40 * MM ) );
904}
905
906
907// A driving fixed length between two polygon vertices moves only the bound vertices; every other
908// vertex keeps its minimal-movement soft pin and stays put.
909BOOST_AUTO_TEST_CASE( PolyVertexFixedLengthMovesBoundVerticesOnly )
910{
911 BOARD board;
912
913 const std::vector<VECTOR2I> points{ { 30 * MM, 20 * MM },
914 { 39 * MM, 27 * MM },
915 { 36 * MM, 38 * MM },
916 { 24 * MM, 38 * MM },
917 { 21 * MM, 27 * MM } };
918
919 PCB_SHAPE* poly = addPoly( board, points );
920
921 // Vertices 1 and 3 start ~18.6mm apart; drive them to 24mm.
923 { { poly->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 1 },
924 { poly->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 3 } },
925 24.0 * MM );
926 len->SetDriving( true );
927
928 std::vector<PCB_SHAPE*> shapes{ poly };
929 std::vector<PCB_CONSTRAINT*> constraints( board.Constraints().begin(), board.Constraints().end() );
930
932 BOOST_REQUIRE( adapter.Build( shapes, constraints ) );
933 BOOST_CHECK( adapter.UnmappedConstraints().empty() );
934 BOOST_REQUIRE( adapter.Solve( true ) );
935 adapter.Apply();
936
937 const SHAPE_LINE_CHAIN& outline = poly->GetPolyShape().COutline( 0 );
938 BOOST_REQUIRE_EQUAL( outline.PointCount(), 5 );
939
940 double driven = ( outline.CPoint( 3 ) - outline.CPoint( 1 ) ).EuclideanNorm();
941 BOOST_CHECK_LE( std::abs( driven - 24.0 * MM ), 5000 );
942
943 // The unbound vertices held their soft pins exactly.
944 for( int i : { 0, 2, 4 } )
945 BOOST_CHECK_LE( ( outline.CPoint( i ) - points[i] ).EuclideanNorm(), 1000 );
946}
947
948
949// A stale vertex index (the poly has only five vertices) cannot map; Build still succeeds, the
950// constraint lands in UnmappedConstraints() and the geometry is untouched.
951BOOST_AUTO_TEST_CASE( PolyStaleVertexIndexUnmapped )
952{
953 BOARD board;
954
955 const std::vector<VECTOR2I> points{ { 30 * MM, 20 * MM },
956 { 39 * MM, 27 * MM },
957 { 36 * MM, 38 * MM },
958 { 24 * MM, 38 * MM },
959 { 21 * MM, 27 * MM } };
960
961 PCB_SHAPE* poly = addPoly( board, points );
962
964 { { poly->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 9 },
965 { poly->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 0 } },
966 30.0 * MM );
967 len->SetDriving( true );
968
969 std::vector<PCB_SHAPE*> shapes{ poly };
970 std::vector<PCB_CONSTRAINT*> constraints( board.Constraints().begin(), board.Constraints().end() );
971
973 BOOST_REQUIRE( adapter.Build( shapes, constraints ) );
974
975 BOOST_REQUIRE_EQUAL( adapter.UnmappedConstraints().size(), 1 );
976 BOOST_CHECK( adapter.UnmappedConstraints().front() == len->m_Uuid );
977
978 BOOST_REQUIRE( adapter.Solve( true ) );
979 adapter.Apply();
980
981 const SHAPE_LINE_CHAIN& outline = poly->GetPolyShape().COutline( 0 );
982
983 for( int i = 0; i < 5; ++i )
984 BOOST_CHECK_EQUAL( outline.CPoint( i ), points[i] );
985}
986
987
988// A poly with a hole is not ingested (write-back rebuilds a single outline, which would destroy the
989// hole), so a constraint on it stays unmapped and the geometry is untouched.
990BOOST_AUTO_TEST_CASE( PolyWithHoleStaysUnmapped )
991{
992 BOARD board;
993
994 const std::vector<VECTOR2I> points{ { 10 * MM, 10 * MM },
995 { 50 * MM, 10 * MM },
996 { 50 * MM, 40 * MM },
997 { 10 * MM, 40 * MM } };
998
999 PCB_SHAPE* poly = addPoly( board, points );
1000
1001 SHAPE_POLY_SET& polySet = poly->GetPolyShape();
1002 polySet.NewHole( 0 );
1003 polySet.Append( 20 * MM, 20 * MM, 0, 0 );
1004 polySet.Append( 30 * MM, 20 * MM, 0, 0 );
1005 polySet.Append( 30 * MM, 30 * MM, 0, 0 );
1006
1008 { { poly->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 0 },
1009 { poly->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 1 } },
1010 30.0 * MM );
1011 len->SetDriving( true );
1012
1013 std::vector<PCB_SHAPE*> shapes{ poly };
1014 std::vector<PCB_CONSTRAINT*> constraints( board.Constraints().begin(), board.Constraints().end() );
1015
1017 BOOST_REQUIRE( adapter.Build( shapes, constraints ) );
1018
1019 BOOST_REQUIRE_EQUAL( adapter.UnmappedConstraints().size(), 1 );
1020 BOOST_CHECK( adapter.UnmappedConstraints().front() == len->m_Uuid );
1021
1022 BOOST_REQUIRE( adapter.Solve( true ) );
1023 adapter.Apply();
1024
1025 BOOST_CHECK_EQUAL( poly->GetPolyShape().HoleCount( 0 ), 1 );
1026
1027 const SHAPE_LINE_CHAIN& outline = poly->GetPolyShape().COutline( 0 );
1028
1029 for( int i = 0; i < 4; ++i )
1030 BOOST_CHECK_EQUAL( outline.CPoint( i ), points[i] );
1031}
1032
1033
1034// A poly whose outline carries an arc is not ingested (write-back would polygonize the arc and drop
1035// its metadata), so a constraint on it stays unmapped and the arc survives Solve + Apply.
1036BOOST_AUTO_TEST_CASE( PolyArcOutlineStaysUnmapped )
1037{
1038 BOARD board;
1039
1040 PCB_SHAPE* poly = new PCB_SHAPE( &board, SHAPE_T::POLY );
1041
1043 chain.Append( VECTOR2I( 10 * MM, 10 * MM ) );
1044 chain.Append( VECTOR2I( 50 * MM, 10 * MM ) );
1045 chain.Append( SHAPE_ARC( { 50 * MM, 10 * MM }, { 55 * MM, 25 * MM }, { 50 * MM, 40 * MM }, 0 ) );
1046 chain.Append( VECTOR2I( 10 * MM, 40 * MM ) );
1047 chain.SetClosed( true );
1048
1049 poly->GetPolyShape().AddOutline( chain );
1050 board.Add( poly );
1051
1052 BOOST_REQUIRE_GT( poly->GetPolyShape().COutline( 0 ).ArcCount(), 0 );
1053
1055 { { poly->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 0 },
1056 { poly->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 1 } },
1057 30.0 * MM );
1058 len->SetDriving( true );
1059
1060 std::vector<PCB_SHAPE*> shapes{ poly };
1061 std::vector<PCB_CONSTRAINT*> constraints( board.Constraints().begin(), board.Constraints().end() );
1062
1064 BOOST_REQUIRE( adapter.Build( shapes, constraints ) );
1065
1066 BOOST_REQUIRE_EQUAL( adapter.UnmappedConstraints().size(), 1 );
1067 BOOST_CHECK( adapter.UnmappedConstraints().front() == len->m_Uuid );
1068
1069 BOOST_REQUIRE( adapter.Solve( true ) );
1070 adapter.Apply();
1071
1072 const SHAPE_LINE_CHAIN& outline = poly->GetPolyShape().COutline( 0 );
1073
1074 BOOST_CHECK_GT( outline.ArcCount(), 0 );
1075 BOOST_CHECK_EQUAL( outline.CPoint( 0 ), VECTOR2I( 10 * MM, 10 * MM ) );
1076 BOOST_CHECK_EQUAL( outline.CPoint( 1 ), VECTOR2I( 50 * MM, 10 * MM ) );
1077}
1078
1079
1080// A poly with a second outline is not ingested (write-back rebuilds a single outline, which would
1081// destroy the other one), so a constraint on it stays unmapped and both outlines survive.
1082BOOST_AUTO_TEST_CASE( PolyTwoOutlinesStaysUnmapped )
1083{
1084 BOARD board;
1085
1086 const std::vector<VECTOR2I> points{ { 10 * MM, 10 * MM },
1087 { 50 * MM, 10 * MM },
1088 { 50 * MM, 40 * MM },
1089 { 10 * MM, 40 * MM } };
1090
1091 PCB_SHAPE* poly = addPoly( board, points );
1092
1093 SHAPE_POLY_SET& polySet = poly->GetPolyShape();
1094 polySet.NewOutline();
1095 polySet.Append( 60 * MM, 10 * MM, 1 );
1096 polySet.Append( 70 * MM, 10 * MM, 1 );
1097 polySet.Append( 70 * MM, 20 * MM, 1 );
1098
1100 { { poly->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 0 },
1101 { poly->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 1 } },
1102 30.0 * MM );
1103 len->SetDriving( true );
1104
1105 std::vector<PCB_SHAPE*> shapes{ poly };
1106 std::vector<PCB_CONSTRAINT*> constraints( board.Constraints().begin(), board.Constraints().end() );
1107
1109 BOOST_REQUIRE( adapter.Build( shapes, constraints ) );
1110
1111 BOOST_REQUIRE_EQUAL( adapter.UnmappedConstraints().size(), 1 );
1112 BOOST_CHECK( adapter.UnmappedConstraints().front() == len->m_Uuid );
1113
1114 BOOST_REQUIRE( adapter.Solve( true ) );
1115 adapter.Apply();
1116
1118
1119 const SHAPE_LINE_CHAIN& outline = poly->GetPolyShape().COutline( 0 );
1120
1121 for( int i = 0; i < 4; ++i )
1122 BOOST_CHECK_EQUAL( outline.CPoint( i ), points[i] );
1123}
1124
1125
1126// A poly whose outline has no vertices contributes no params; a constraint naming it stays unmapped
1127// and the rest of the cluster still solves.
1128BOOST_AUTO_TEST_CASE( PolyEmptyOutlineStaysUnmapped )
1129{
1130 BOARD board;
1131
1132 PCB_SHAPE* poly = addPoly( board, {} );
1133 PCB_SHAPE* seg = addSegment( board, { 0, 0 }, { 3 * MM, 0 } );
1134
1136 { { poly->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 0 },
1137 { seg->m_Uuid, CONSTRAINT_ANCHOR::START } },
1138 30.0 * MM );
1139 len->SetDriving( true );
1140
1141 std::vector<PCB_SHAPE*> shapes{ poly, seg };
1142 std::vector<PCB_CONSTRAINT*> constraints( board.Constraints().begin(), board.Constraints().end() );
1143
1145 BOOST_REQUIRE( adapter.Build( shapes, constraints ) );
1146
1147 BOOST_REQUIRE_EQUAL( adapter.UnmappedConstraints().size(), 1 );
1148 BOOST_CHECK( adapter.UnmappedConstraints().front() == len->m_Uuid );
1149
1150 BOOST_REQUIRE( adapter.Solve( true ) );
1151 adapter.Apply();
1152
1153 BOOST_CHECK_EQUAL( seg->GetStart(), VECTOR2I( 0, 0 ) );
1154 BOOST_CHECK_EQUAL( seg->GetEnd(), VECTOR2I( 3 * MM, 0 ) );
1155}
1156
1157
1158// A circle with a driving fixed-radius takes that radius.
1159BOOST_AUTO_TEST_CASE( FixedRadiusDrivesCircle )
1160{
1161 BOARD board;
1162
1163 PCB_SHAPE* circle = addCircle( board, { 0, 0 }, 3 * MM );
1164
1166 { { circle->m_Uuid, CONSTRAINT_ANCHOR::CENTER } } );
1167
1169 { { circle->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } } );
1170 fr->SetValue( 5.0 * MM );
1171
1172 std::vector<PCB_SHAPE*> shapes{ circle };
1173 solveAndApply( board, shapes );
1174
1175 BOOST_CHECK_LE( std::abs( circle->GetRadius() - 5 * MM ), 1000 );
1176}
1177
1178
1179// Two circles made equal-radius end up the same size as the fixed reference.
1180BOOST_AUTO_TEST_CASE( EqualRadiusMatchesReference )
1181{
1182 BOARD board;
1183
1184 PCB_SHAPE* ref = addCircle( board, { 0, 0 }, 4 * MM );
1185 PCB_SHAPE* other = addCircle( board, { 30 * MM, 0 }, 1 * MM );
1186
1188 { { ref->m_Uuid, CONSTRAINT_ANCHOR::CENTER } } );
1190 { { ref->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } } );
1191 fr->SetValue( 4.0 * MM );
1193 { { ref->m_Uuid, CONSTRAINT_ANCHOR::WHOLE },
1194 { other->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } } );
1195
1196 std::vector<PCB_SHAPE*> shapes{ ref, other };
1197 solveAndApply( board, shapes );
1198
1199 BOOST_CHECK_LE( std::abs( other->GetRadius() - 4 * MM ), 1000 );
1200}
1201
1202
1203// Two circles made concentric end up sharing the fixed reference's center.
1204BOOST_AUTO_TEST_CASE( ConcentricSharesCenter )
1205{
1206 BOARD board;
1207
1208 PCB_SHAPE* ref = addCircle( board, { 5 * MM, 5 * MM }, 4 * MM );
1209 PCB_SHAPE* other = addCircle( board, { 30 * MM, 0 }, 2 * MM );
1210
1212 { { ref->m_Uuid, CONSTRAINT_ANCHOR::CENTER } } );
1214 { { ref->m_Uuid, CONSTRAINT_ANCHOR::WHOLE },
1215 { other->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } } );
1216
1217 std::vector<PCB_SHAPE*> shapes{ ref, other };
1218 solveAndApply( board, shapes );
1219
1220 BOOST_CHECK_LE( ( other->GetCenter() - VECTOR2I( 5 * MM, 5 * MM ) ).EuclideanNorm(), 1000 );
1221}
1222
1223
1224// A driving angular dimension forces the angle between two segments.
1225BOOST_AUTO_TEST_CASE( AngularDimensionDrivesAngle )
1226{
1227 BOARD board;
1228
1229 PCB_SHAPE* ref = addSegment( board, { 0, 0 }, { 10 * MM, 0 } ); // along +x
1230 PCB_SHAPE* arm = addSegment( board, { 0, 0 }, { 8 * MM, 1 * MM } );
1231
1233 { { ref->m_Uuid, CONSTRAINT_ANCHOR::START } } );
1235 { { ref->m_Uuid, CONSTRAINT_ANCHOR::END } } );
1237 { { arm->m_Uuid, CONSTRAINT_ANCHOR::START } } );
1238
1240 { { ref->m_Uuid, CONSTRAINT_ANCHOR::WHOLE },
1241 { arm->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } } );
1242 dim->SetValue( 90.0 ); // degrees
1243
1244 std::vector<PCB_SHAPE*> shapes{ ref, arm };
1245 solveAndApply( board, shapes );
1246
1247 // arm starts at the origin; at 90 deg to the +x reference it is vertical.
1248 BOOST_CHECK_LE( std::abs( arm->GetEnd().x - arm->GetStart().x ), 100 );
1249}
1250
1251
1252// A fixed-radius driving value resizes an arc (verifies the arc setup solves).
1253BOOST_AUTO_TEST_CASE( FixedRadiusArc )
1254{
1255 BOARD board;
1256
1257 PCB_SHAPE* arc = addArc( board, { 5 * MM, 0 }, { 3535533, 3535533 }, { 0, 5 * MM } ); // R = 5mm
1258
1260 { { arc->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } } );
1261 fr->SetValue( 6.0 * MM );
1262
1263 std::vector<PCB_SHAPE*> shapes{ arc };
1264 solveAndApply( board, shapes );
1265
1266 BOOST_CHECK_LE( std::abs( arc->GetRadius() - 6 * MM ), 2000 );
1267}
1268
1269
1270// An arc made equal-radius to a fixed-radius reference arc takes that radius.
1271BOOST_AUTO_TEST_CASE( EqualRadiusArcs )
1272{
1273 BOARD board;
1274
1275 PCB_SHAPE* ref = addArc( board, { 5 * MM, 0 }, { 3535533, 3535533 }, { 0, 5 * MM } ); // R = 5mm
1276 // A clean R = 4 mm quarter arc centred at (30,0).
1277 PCB_SHAPE* other = addArc( board, { 34 * MM, 0 }, { 32828427, 2828427 }, { 30 * MM, 4 * MM } );
1278
1280 { { ref->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } } );
1281 fr->SetValue( 5.0 * MM );
1283 { { ref->m_Uuid, CONSTRAINT_ANCHOR::WHOLE },
1284 { other->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } } );
1285
1286 std::vector<PCB_SHAPE*> shapes{ ref, other };
1287 solveAndApply( board, shapes );
1288
1289 BOOST_CHECK_LE( std::abs( other->GetRadius() - 5 * MM ), 3000 );
1290}
1291
1292
1293// A bezier endpoint coincident with a fixed point maps and snaps onto that point, and each control
1294// handle rides along with its adjacent endpoint so the curve is translated rather than sheared.
1295BOOST_AUTO_TEST_CASE( BezierEndpointCoincidentSnaps )
1296{
1297 BOARD board;
1298
1299 PCB_SHAPE* anchor = addSegment( board, { 20 * MM, 5 * MM }, { 25 * MM, 5 * MM } );
1300 PCB_SHAPE* bezier = addBezier( board, { 0, 0 }, { 3 * MM, 8 * MM }, { 7 * MM, 8 * MM }, { 10 * MM, 0 } );
1301
1302 const VECTOR2I startBefore = bezier->GetStart();
1303 const VECTOR2I c1Before = bezier->GetBezierC1();
1304 const VECTOR2I c2Before = bezier->GetBezierC2();
1305 const VECTOR2I endBefore = bezier->GetEnd();
1306
1308 { { anchor->m_Uuid, CONSTRAINT_ANCHOR::START } } );
1310 { { bezier->m_Uuid, CONSTRAINT_ANCHOR::END },
1311 { anchor->m_Uuid, CONSTRAINT_ANCHOR::START } } );
1312
1313 std::vector<PCB_SHAPE*> shapes{ anchor, bezier };
1314 std::vector<PCB_CONSTRAINT*> constraints( board.Constraints().begin(), board.Constraints().end() );
1315
1317 BOOST_REQUIRE( adapter.Build( shapes, constraints ) );
1318
1319 // The coincident on the bezier endpoint is a real solver constraint, not silently dropped.
1320 BOOST_CHECK( adapter.UnmappedConstraints().empty() );
1321
1322 BOOST_REQUIRE( adapter.Solve() );
1323 adapter.Apply();
1324
1325 // The dragged-free endpoint landed on the fixed anchor.
1326 BOOST_CHECK_LE( ( bezier->GetEnd() - VECTOR2I( 20 * MM, 5 * MM ) ).EuclideanNorm(), 1000 );
1327
1328 // The far endpoint held; its control handle did not move.
1329 BOOST_CHECK_EQUAL( bezier->GetStart(), startBefore );
1330 BOOST_CHECK_EQUAL( bezier->GetBezierC1(), c1Before );
1331
1332 // The moved endpoint carried its own control handle by the same delta, preserving the curve.
1333 VECTOR2I endDelta = bezier->GetEnd() - endBefore;
1334 BOOST_CHECK_EQUAL( bezier->GetBezierC2() - c2Before, endDelta );
1335}
1336
1337
1338// Horizontal accepts two point anchors too not just a whole segment so picked points end level
1339BOOST_AUTO_TEST_CASE( HorizontalAlignsTwoPoints )
1340{
1341 BOARD board;
1342
1343 // Two separate segments whose end anchors start at different heights.
1344 PCB_SHAPE* a = addSegment( board, { 0, 0 }, { 5 * MM, 2 * MM } );
1345 PCB_SHAPE* b = addSegment( board, { 10 * MM, 0 }, { 15 * MM, 8 * MM } );
1346
1349 { b->m_Uuid, CONSTRAINT_ANCHOR::END } } );
1350
1351 std::vector<PCB_SHAPE*> shapes{ a, b };
1352 std::vector<PCB_CONSTRAINT*> constraints( board.Constraints().begin(), board.Constraints().end() );
1353
1355 BOOST_REQUIRE( adapter.Build( shapes, constraints ) );
1356
1357 // The two-point horizontal is a real solver constraint, not silently dropped.
1358 BOOST_CHECK( adapter.UnmappedConstraints().empty() );
1359
1360 BOOST_REQUIRE( adapter.Solve() );
1361 adapter.Apply();
1362
1363 BOOST_CHECK_LE( std::abs( a->GetEnd().y - b->GetEnd().y ), 1000 ); // within 1 um
1364}
1365
1366
1367// Vertical accepts two point anchors too so picked points end at same x
1368BOOST_AUTO_TEST_CASE( VerticalAlignsTwoPoints )
1369{
1370 BOARD board;
1371
1372 PCB_SHAPE* a = addSegment( board, { 0, 0 }, { 2 * MM, 5 * MM } );
1373 PCB_SHAPE* b = addSegment( board, { 0, 10 * MM }, { 8 * MM, 15 * MM } );
1374
1377 { b->m_Uuid, CONSTRAINT_ANCHOR::END } } );
1378
1379 std::vector<PCB_SHAPE*> shapes{ a, b };
1380 std::vector<PCB_CONSTRAINT*> constraints( board.Constraints().begin(), board.Constraints().end() );
1381
1383 BOOST_REQUIRE( adapter.Build( shapes, constraints ) );
1384 BOOST_CHECK( adapter.UnmappedConstraints().empty() );
1385
1386 BOOST_REQUIRE( adapter.Solve() );
1387 adapter.Apply();
1388
1389 BOOST_CHECK_LE( std::abs( a->GetEnd().x - b->GetEnd().x ), 1000 );
1390}
1391
1392
1393// Dragging a bezier endpoint that is coincident with a segment corner pulls the segment along.
1394BOOST_AUTO_TEST_CASE( DragBezierEndpointMovesCoincidentNeighbor )
1395{
1396 BOARD board;
1397
1398 PCB_SHAPE* bezier = addBezier( board, { 0, 0 }, { 3 * MM, 8 * MM }, { 7 * MM, 8 * MM }, { 10 * MM, 0 } );
1399 PCB_SHAPE* seg = addSegment( board, { 10 * MM, 0 }, { 10 * MM, 10 * MM } );
1400
1402 { { bezier->m_Uuid, CONSTRAINT_ANCHOR::END },
1403 { seg->m_Uuid, CONSTRAINT_ANCHOR::START } } );
1404
1406 std::vector<PCB_SHAPE*> modified;
1407
1408 CONSTRAINT_DIAGNOSIS diag = SolveCluster( &board, dragged, { 13 * MM, 4 * MM }, &modified );
1409
1410 BOOST_REQUIRE( diag.solved );
1411
1412 // The bezier end and the segment start moved together and stay coincident.
1413 BOOST_CHECK_LE( ( bezier->GetEnd() - seg->GetStart() ).EuclideanNorm(), 100 );
1414 BOOST_CHECK_LE( ( bezier->GetEnd() - VECTOR2I( 13 * MM, 4 * MM ) ).EuclideanNorm(), 1000 );
1415
1416 // The neighbor segment is reported modified; the dragged bezier is not.
1417 BOOST_CHECK( std::find( modified.begin(), modified.end(), seg ) != modified.end() );
1418 BOOST_CHECK( std::find( modified.begin(), modified.end(), bezier ) == modified.end() );
1419}
1420
1421
1422// Minimal movement issue 2 resolve moves fewest shapes
1423// Horizontal on X drops corner so tied start of Y follows but free end of Y stays put
1424BOOST_AUTO_TEST_CASE( MinimalMovementSettleHoldsSlackFreeEnd )
1425{
1426 BOARD board;
1427
1428 PCB_SHAPE* x = addSegment( board, { 0, 0 }, { 10 * MM, 2 * MM } ); // slightly sloped
1429 PCB_SHAPE* y = addSegment( board, { 10 * MM, 2 * MM }, { 10 * MM, 12 * MM } );
1430
1432 { { x->m_Uuid, CONSTRAINT_ANCHOR::END }, { y->m_Uuid, CONSTRAINT_ANCHOR::START } } );
1434 { { x->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } } );
1435
1436 const VECTOR2I xEnd0 = x->GetEnd();
1437 const VECTOR2I yEnd0 = y->GetEnd();
1438
1439 std::vector<PCB_SHAPE*> modified;
1440 ApplyConstraintImmediately( &board, horizontal, &modified, []( BOARD_ITEM* ) {} );
1441
1442 // X turned horizontal, so its end dropped to y = 0.
1443 BOOST_CHECK_LE( slopeDiffY( x ), 100 );
1444
1445 // Must move start of Y stayed coincident with moved corner
1446 BOOST_CHECK_LE( ( y->GetStart() - x->GetEnd() ).EuclideanNorm(), 5000.0 );
1447 BOOST_CHECK( x->GetEnd() != xEnd0 );
1448
1449 // Slack held free end of Y did not drift so Y grew instead of translating
1450 BOOST_CHECK_LE( ( y->GetEnd() - yEnd0 ).EuclideanNorm(), 5000.0 );
1451}
1452
1453
1454// A driving fixed length grows the edited segment; the solver must extend it along its current
1455// direction (its free end reaching the minimal spot) rather than rotating it, and the coincident
1456// neighbour's own free end must not drift. A plain re-solve rotates the free-direction segment to an
1457// arbitrary point on the length circle, dragging the neighbour with it.
1458BOOST_AUTO_TEST_CASE( MinimalMovementSettleExtendsWithoutRotating )
1459{
1460 BOARD board;
1461
1462 PCB_SHAPE* x = addSegment( board, { 0, 0 }, { 4 * MM, 0 } ); // along +x, fixed start
1463 PCB_SHAPE* y = addSegment( board, { 4 * MM, 0 }, { 4 * MM, 10 * MM } );
1464
1466 { { x->m_Uuid, CONSTRAINT_ANCHOR::START } } );
1468 { { x->m_Uuid, CONSTRAINT_ANCHOR::END }, { y->m_Uuid, CONSTRAINT_ANCHOR::START } } );
1470 { { x->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } }, 10.0 * MM );
1471
1472 const VECTOR2I yEnd0 = y->GetEnd();
1473
1474 std::vector<PCB_SHAPE*> modified;
1475 ApplyConstraintImmediately( &board, len, &modified, []( BOARD_ITEM* ) {} );
1476
1477 // Must move X extends straight to length 10 landing near x 10mm y 0 unrotated
1478 BOOST_CHECK_LE( ( x->GetEnd() - VECTOR2I( 10 * MM, 0 ) ).EuclideanNorm(), 5000.0 );
1479 BOOST_CHECK_LE( ( y->GetStart() - x->GetEnd() ).EuclideanNorm(), 5000.0 );
1480
1481 // Slack held free far end of neighbour stayed put
1482 BOOST_CHECK_LE( ( y->GetEnd() - yEnd0 ).EuclideanNorm(), 5000.0 );
1483}
1484
1485
1486// The must-move guarantee on the interactive drag path: dragging a coincident corner pulls the
1487// hard-tied neighbour end along while its opposite, unconstrained end stays put.
1488BOOST_AUTO_TEST_CASE( MinimalMovementDragHoldsNeighborFreeEnd )
1489{
1490 BOARD board;
1491
1492 PCB_SHAPE* a = addSegment( board, { 0, 0 }, { 10 * MM, 0 } );
1493 PCB_SHAPE* b = addSegment( board, { 10 * MM, 0 }, { 10 * MM, 10 * MM } );
1494
1497 { b->m_Uuid, CONSTRAINT_ANCHOR::START } } );
1498
1499 const VECTOR2I bEnd0 = b->GetEnd();
1500
1501 std::vector<PCB_SHAPE*> modified;
1503 { 12 * MM, 3 * MM }, &modified );
1504
1505 BOOST_REQUIRE( diag.solved );
1506
1507 // Must move coincident end followed dragged corner to cursor
1508 BOOST_CHECK_LE( ( a->GetEnd() - b->GetStart() ).EuclideanNorm(), 100 );
1509 BOOST_CHECK_LE( ( a->GetEnd() - VECTOR2I( 12 * MM, 3 * MM ) ).EuclideanNorm(), 5000.0 );
1510 BOOST_CHECK( std::find( modified.begin(), modified.end(), b ) != modified.end() );
1511
1512 // Slack held free far end of neighbour stayed put
1513 BOOST_CHECK_LE( ( b->GetEnd() - bEnd0 ).EuclideanNorm(), 5000.0 );
1514}
1515
1516
1517// Minimal movement issue 2 with multiple shapes edited together A and B translate as a pair only B touches slack neighbour N
1518// Fewest objects solve must leave N put so threading the full edited set stops B stay pin fighting N pin
1519BOOST_AUTO_TEST_CASE( MinimalMovementMultiEditedHoldsSlackNeighbor )
1520{
1521 BOARD board;
1522
1523 PCB_SHAPE* a = addSegment( board, { 0, 0 }, { 10 * MM, 0 } );
1524 PCB_SHAPE* b = addSegment( board, { 10 * MM, 0 }, { 20 * MM, 0 } );
1525 PCB_SHAPE* n = addSegment( board, { 20 * MM, 0 }, { 30 * MM, 0 } );
1526
1528 { { a->m_Uuid, CONSTRAINT_ANCHOR::END }, { b->m_Uuid, CONSTRAINT_ANCHOR::START } } );
1530 { { b->m_Uuid, CONSTRAINT_ANCHOR::END }, { n->m_Uuid, CONSTRAINT_ANCHOR::START } } );
1531
1532 const VECTOR2I nStart0 = n->GetStart();
1533 const VECTOR2I nEnd0 = n->GetEnd();
1534
1535 a->Move( { 0, 5 * MM } );
1536 b->Move( { 0, 5 * MM } );
1537
1538 std::vector<PCB_SHAPE*> modified;
1539 ReSolveShapeClusters( &board, { a, b }, &modified );
1540
1541 // The slack neighbour is untouched -- both endpoints hold and it is not staged as modified.
1542 BOOST_CHECK_LE( ( n->GetStart() - nStart0 ).EuclideanNorm(), 100 );
1543 BOOST_CHECK_LE( ( n->GetEnd() - nEnd0 ).EuclideanNorm(), 100 );
1544 BOOST_CHECK( std::find( modified.begin(), modified.end(), n ) == modified.end() );
1545
1546 // Both edited shapes keep the geometry the edit gave them at the pinned/seed corner.
1547 BOOST_CHECK_LE( ( a->GetStart() - VECTOR2I( 0, 5 * MM ) ).EuclideanNorm(), 5000.0 );
1548 BOOST_CHECK_LE( ( a->GetEnd() - VECTOR2I( 10 * MM, 5 * MM ) ).EuclideanNorm(), 5000.0 );
1549 BOOST_CHECK_LE( ( b->GetStart() - VECTOR2I( 10 * MM, 5 * MM ) ).EuclideanNorm(), 5000.0 );
1550}
1551
1552
1553// The draw-time binding heuristic prefers a single object whose anchors reach both dimension
1554// endpoints, binding START and END to that one object.
1555BOOST_AUTO_TEST_CASE( DimensionBindingPrefersSameObject )
1556{
1557 BOARD board;
1558
1559 PCB_SHAPE* seg = addSegment( board, { 0, 0 }, { 10 * MM, 0 } );
1560
1561 KIID fakeDim; // a dimension uuid that is not on the board, as during interactive draw
1562 std::vector<DIMENSION_ENDPOINT_BINDING> bindings =
1563 SelectDimensionEndpointBindings( &board, fakeDim, { 0, 0 }, VECTOR2I( 10 * MM, 0 ), 1000.0 );
1564
1565 BOOST_REQUIRE_EQUAL( bindings.size(), 2 );
1566
1567 // Both ends bind to the same segment, one to START and one to END.
1568 BOOST_CHECK( bindings[0].dimAnchor == CONSTRAINT_ANCHOR::START );
1569 BOOST_CHECK( bindings[0].target == CONSTRAINT_MEMBER( seg->m_Uuid, CONSTRAINT_ANCHOR::START ) );
1570 BOOST_CHECK( bindings[1].dimAnchor == CONSTRAINT_ANCHOR::END );
1571 BOOST_CHECK( bindings[1].target == CONSTRAINT_MEMBER( seg->m_Uuid, CONSTRAINT_ANCHOR::END ) );
1572}
1573
1574
1575// When no single object reaches both endpoints, each end binds to its own nearest anchor, on
1576// different objects.
1577BOOST_AUTO_TEST_CASE( DimensionBindingPerEndpointDifferentObjects )
1578{
1579 BOARD board;
1580
1581 PCB_SHAPE* a = addSegment( board, { 0, 0 }, { 5 * MM, 5 * MM } );
1582 PCB_SHAPE* b = addSegment( board, { 20 * MM, 0 }, { 25 * MM, 5 * MM } );
1583
1584 KIID fakeDim;
1585 std::vector<DIMENSION_ENDPOINT_BINDING> bindings =
1586 SelectDimensionEndpointBindings( &board, fakeDim, { 0, 0 }, VECTOR2I( 20 * MM, 0 ), 1000.0 );
1587
1588 BOOST_REQUIRE_EQUAL( bindings.size(), 2 );
1589 BOOST_CHECK( bindings[0].target == CONSTRAINT_MEMBER( a->m_Uuid, CONSTRAINT_ANCHOR::START ) );
1590 BOOST_CHECK( bindings[1].target == CONSTRAINT_MEMBER( b->m_Uuid, CONSTRAINT_ANCHOR::START ) );
1591}
1592
1593
1594// Only one endpoint near an object still binds (partial is fine); the far endpoint is left free.
1595BOOST_AUTO_TEST_CASE( DimensionBindingPartial )
1596{
1597 BOARD board;
1598
1599 PCB_SHAPE* a = addSegment( board, { 0, 0 }, { 5 * MM, 5 * MM } );
1600
1601 KIID fakeDim;
1602 std::vector<DIMENSION_ENDPOINT_BINDING> bindings =
1603 SelectDimensionEndpointBindings( &board, fakeDim, { 0, 0 }, VECTOR2I( 50 * MM, 50 * MM ), 1000.0 );
1604
1605 BOOST_REQUIRE_EQUAL( bindings.size(), 1 );
1606 BOOST_CHECK( bindings[0].dimAnchor == CONSTRAINT_ANCHOR::START );
1607 BOOST_CHECK( bindings[0].target == CONSTRAINT_MEMBER( a->m_Uuid, CONSTRAINT_ANCHOR::START ) );
1608 (void) a;
1609}
1610
1611
1612// Single object with START nearest both dimension ends must still bind both via distinct anchor pair not split END onto closer neighbour
1613// End near A START at 2mm beats A END at 8mm yet A spans both through START and END so A wins over B
1614BOOST_AUTO_TEST_CASE( DimensionBindingDistinctPairOverSplit )
1615{
1616 BOARD board;
1617
1618 PCB_SHAPE* a = addSegment( board, { 0, 0 }, { 10 * MM, 0 } );
1619 PCB_SHAPE* b = addSegment( board, { 2 * MM, 1 * MM }, { 2 * MM, 20 * MM } );
1620
1621 KIID fakeDim;
1622 std::vector<DIMENSION_ENDPOINT_BINDING> bindings =
1623 SelectDimensionEndpointBindings( &board, fakeDim, { 0, 0 }, VECTOR2I( 2 * MM, 0 ),
1624 12.0 * MM );
1625
1626 BOOST_REQUIRE_EQUAL( bindings.size(), 2 );
1627
1628 // Both ends bind to segment A on distinct anchors; segment B never enters the binding.
1629 BOOST_CHECK( bindings[0].dimAnchor == CONSTRAINT_ANCHOR::START );
1630 BOOST_CHECK( bindings[0].target == CONSTRAINT_MEMBER( a->m_Uuid, CONSTRAINT_ANCHOR::START ) );
1631 BOOST_CHECK( bindings[1].dimAnchor == CONSTRAINT_ANCHOR::END );
1632 BOOST_CHECK( bindings[1].target == CONSTRAINT_MEMBER( a->m_Uuid, CONSTRAINT_ANCHOR::END ) );
1633 (void) b;
1634}
1635
1636
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.
void 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 ...
CONSTRAINT_DIAGNOSIS ApplyConstraintImmediately(BOARD *aBoard, const PCB_CONSTRAINT *aConstraint, std::vector< PCB_SHAPE * > *aModified, const std::function< void(BOARD_ITEM *)> &aBeforeModify)
Solve a just-created constraint's cluster so the geometry snaps to satisfy it (SolidWorks-style),...
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.
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)
Gather the cluster of shapes transitively constrained with the dragged shape, solve with the dragged ...
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)
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:367
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:44
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< DIMENSION_ENDPOINT_BINDING > SelectDimensionEndpointBindings(BOARD *aBoard, const KIID &aDimension, const VECTOR2I &aStart, const std::optional< VECTOR2I > &aEnd, double aMaxDist)
Choose the coincident bindings a freshly drawn dimension endpoints should take so it tracks the geome...
@ 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