KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_constraint_dimension.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
26
28
29#include <algorithm>
30#include <vector>
31
32#include <board.h>
33#include <footprint.h>
34#include <geometry/shape_arc.h>
36#include <i18n_utility.h>
37#include <pcb_dimension.h>
38#include <pcb_shape.h>
39#include <properties/property.h>
41#include <trigo.h>
42
46
48
49using namespace KI_TEST;
50
51namespace
52{
53PCB_DIM_ALIGNED* addAlignedDim( BOARD& aBoard, const VECTOR2I& aStart, const VECTOR2I& aEnd )
54{
56 dim->SetStart( aStart );
57 dim->SetEnd( aEnd );
58 dim->Update();
59 aBoard.Add( dim );
60 return dim;
61}
62
63
64PCB_DIM_RADIAL* addRadialDim( BOARD& aBoard, const VECTOR2I& aCenter, const VECTOR2I& aRim )
65{
66 PCB_DIM_RADIAL* dim = new PCB_DIM_RADIAL( &aBoard );
67 dim->SetStart( aCenter );
68 dim->SetEnd( aRim );
69 dim->Update();
70 aBoard.Add( dim );
71 return dim;
72}
73
74
75PCB_DIM_ORTHOGONAL* addOrthogonalDim( BOARD& aBoard, const VECTOR2I& aStart, const VECTOR2I& aEnd,
76 PCB_DIM_ORTHOGONAL::DIR aOrientation )
77{
78 PCB_DIM_ORTHOGONAL* dim = new PCB_DIM_ORTHOGONAL( &aBoard );
79 dim->SetOrientation( aOrientation );
80 dim->SetStart( aStart );
81 dim->SetEnd( aEnd );
82 dim->Update();
83 aBoard.Add( dim );
84 return dim;
85}
86
87
88PCB_CONSTRAINT* addDrivingLength( BOARD& aBoard, const KIID& aDimension, double aLengthIU )
89{
92 { { aDimension, CONSTRAINT_ANCHOR::START }, { aDimension, CONSTRAINT_ANCHOR::END } }, aLengthIU );
93 c->SetDriving( true );
94 return c;
95}
96
97
98// Dim-first member order matches draw-time auto-constrain
99PCB_CONSTRAINT* bindEndpoint( BOARD& aBoard, const KIID& aDimension, CONSTRAINT_ANCHOR aDimAnchor,
100 const CONSTRAINT_MEMBER& aTarget )
101{
102 return addConstraint( aBoard, PCB_CONSTRAINT_TYPE::COINCIDENT, { { aDimension, aDimAnchor }, aTarget } );
103}
104
105
106// Physical TL and BR corners regardless of stored start end order
107VECTOR2I rectTopLeft( const PCB_SHAPE* aRect )
108{
109 return VECTOR2I( std::min( aRect->GetStart().x, aRect->GetEnd().x ),
110 std::min( aRect->GetStart().y, aRect->GetEnd().y ) );
111}
112
113
114VECTOR2I rectBottomRight( const PCB_SHAPE* aRect )
115{
116 return VECTOR2I( std::max( aRect->GetStart().x, aRect->GetEnd().x ),
117 std::max( aRect->GetStart().y, aRect->GetEnd().y ) );
118}
119}
120
121
122BOOST_AUTO_TEST_SUITE( ConstraintDimension )
123
124
125// An aligned dimension exposes its two feature points; a leader (a control-point family) exposes
126// only its start.
127BOOST_AUTO_TEST_CASE( DimensionAnchorsPerFamily )
128{
129 BOARD board;
130 PCB_DIM_ALIGNED* aligned = addAlignedDim( board, { 0, 0 }, { 10 * MM, 0 } );
131
132 std::vector<CONSTRAINT_ANCHOR_POINT> anchors = ConstraintItemAnchors( aligned );
133 BOOST_REQUIRE_EQUAL( anchors.size(), 2 );
134 BOOST_CHECK( anchors[0].anchor == CONSTRAINT_ANCHOR::START );
135 BOOST_CHECK( anchors[1].anchor == CONSTRAINT_ANCHOR::END );
136
137 PCB_DIM_LEADER* leader = new PCB_DIM_LEADER( &board );
138 leader->SetStart( { 0, 0 } );
139 leader->SetEnd( { 5 * MM, 5 * MM } );
140 board.Add( leader );
141
142 BOOST_CHECK_EQUAL( ConstraintItemAnchors( leader ).size(), 1 ); // start only
143}
144
145
146// A coincident constraint binds a dimension's start to a segment endpoint; dragging that endpoint
147// pulls the dimension start along, while the dimension's unbound end stays put.
148BOOST_AUTO_TEST_CASE( CoincidentDimensionFollowsDraggedShape )
149{
150 BOARD board;
151 PCB_SHAPE* seg = addSegment( board, { 0, 0 }, { 10 * MM, 0 } );
152
153 PCB_DIM_ALIGNED* dim = addAlignedDim( board, { 10 * MM, 0 }, { 10 * MM, 5 * MM } );
154
156 { { seg->m_Uuid, CONSTRAINT_ANCHOR::END }, { dim->m_Uuid, CONSTRAINT_ANCHOR::START } } );
157
158 // A bound dimension carries its cluster's verdict, so the overlay can mark it.
160 auto segState = diag.shapeStates.find( seg->m_Uuid );
161 auto dimState = diag.shapeStates.find( dim->m_Uuid );
162
163 BOOST_REQUIRE( segState != diag.shapeStates.end() );
164 BOOST_REQUIRE( dimState != diag.shapeStates.end() );
165 BOOST_CHECK( dimState->second == segState->second );
166
167 const VECTOR2I dimEnd0 = dim->GetEnd();
168
169 std::vector<BOARD_ITEM*> staged;
170 SolveCluster( &board, { seg->m_Uuid, CONSTRAINT_ANCHOR::END }, { 12 * MM, 3 * MM }, nullptr,
171 [&]( BOARD_ITEM* i ) { staged.push_back( i ); } );
172
173 // The dimension start tracked the segment end; its unbound end did not move.
174 BOOST_CHECK_LE( ( dim->GetStart() - seg->GetEnd() ).EuclideanNorm(), 5000.0 );
175 BOOST_CHECK( ( dim->GetStart() - VECTOR2I( 10 * MM, 0 ) ).EuclideanNorm() > 20000.0 );
176 BOOST_CHECK_LE( ( dim->GetEnd() - dimEnd0 ).EuclideanNorm(), 5000.0 );
177 BOOST_CHECK( std::find( staged.begin(), staged.end(), dim ) != staged.end() );
178}
179
180
181// A dimension whose bound shape is deleted leaves the constraint in an error state, not deleted.
182BOOST_AUTO_TEST_CASE( DeletedShapeErrorsDimensionConstraint )
183{
184 BOARD board;
185 PCB_SHAPE* seg = addSegment( board, { 0, 0 }, { 10 * MM, 0 } );
186 KIID segId = seg->m_Uuid;
187
188 PCB_DIM_ALIGNED* dim = addAlignedDim( board, { 10 * MM, 0 }, { 10 * MM, 5 * MM } );
189
191 { { segId, CONSTRAINT_ANCHOR::END }, { dim->m_Uuid, CONSTRAINT_ANCHOR::START } } );
192
193 board.Remove( seg );
194 delete seg;
195
197 BOOST_CHECK_EQUAL( diag.errored.size(), 1 );
198}
199
200
201// The board-wide anchor picker offers a dimension's feature point, so coincident authoring can bind
202// a dimension by clicking near it.
203BOOST_AUTO_TEST_CASE( PickerOffersDimensionAnchor )
204{
205 BOARD board;
206 PCB_DIM_ALIGNED* dim = addAlignedDim( board, { 10 * MM, 0 }, { 10 * MM, 5 * MM } );
207
208 std::optional<CONSTRAINT_MEMBER> hit =
209 NearestConstraintAnchor( &board, VECTOR2I( 10 * MM, 100 ), MM );
210
211 BOOST_REQUIRE( hit.has_value() );
212 BOOST_CHECK( hit->m_item == dim->m_Uuid );
213 BOOST_CHECK( hit->m_anchor == CONSTRAINT_ANCHOR::START );
214}
215
216
217// Authoring a coincident with the dimension as the first member (the apply-on-create path pins the
218// first member) snaps the dimension's bound point onto the shape point.
219BOOST_AUTO_TEST_CASE( ApplyCoincidentDimensionFirstMember )
220{
221 BOARD board;
222 PCB_SHAPE* seg = addSegment( board, { 0, 0 }, { 10 * MM, 0 } );
223
224 // The dimension start sits 1 mm off the segment end it will bind to.
225 PCB_DIM_ALIGNED* dim = addAlignedDim( board, { 11 * MM, 0 }, { 11 * MM, 5 * MM } );
226
229 { { dim->m_Uuid, CONSTRAINT_ANCHOR::START }, { seg->m_Uuid, CONSTRAINT_ANCHOR::END } } );
230
231 std::vector<PCB_SHAPE*> modified;
232 ApplyConstraintImmediately( &board, c, &modified );
233
234 // The first member (the dimension start) is pinned, so the segment end snaps onto it.
235 BOOST_CHECK_LE( ( seg->GetEnd() - dim->GetStart() ).EuclideanNorm(), 5000.0 );
236}
237
238
239// Moving a whole shape re-solves its cluster, so a coincident-bound dimension follows the shape.
240BOOST_AUTO_TEST_CASE( WholeShapeMoveDragsBoundDimension )
241{
242 BOARD board;
243 PCB_SHAPE* seg = addSegment( board, { 0, 0 }, { 10 * MM, 0 } );
244
245 PCB_DIM_ALIGNED* dim = addAlignedDim( board, { 10 * MM, 0 }, { 10 * MM, 5 * MM } );
246
248 { { seg->m_Uuid, CONSTRAINT_ANCHOR::END }, { dim->m_Uuid, CONSTRAINT_ANCHOR::START } } );
249
250 seg->Move( { 0, 3 * MM } ); // whole-shape move
251 ReSolveShapeClusters( &board, { seg } );
252
253 // The dimension start followed the moved segment end.
254 BOOST_CHECK_LE( ( dim->GetStart() - seg->GetEnd() ).EuclideanNorm(), 5000.0 );
255 BOOST_CHECK( dim->GetStart().y > 1 * MM ); // it actually moved up
256}
257
258
259// A leader has no bindable END (its end is a control point), so a coincident on it cannot map and
260// is flagged errored rather than moving the control point.
261BOOST_AUTO_TEST_CASE( LeaderEndIsNotBindable )
262{
263 BOARD board;
264 PCB_SHAPE* seg = addSegment( board, { 0, 0 }, { 10 * MM, 0 } );
265
266 PCB_DIM_LEADER* leader = new PCB_DIM_LEADER( &board );
267 leader->SetStart( { 10 * MM, 0 } );
268 leader->SetEnd( { 15 * MM, 5 * MM } );
269 board.Add( leader );
270
273 { { seg->m_Uuid, CONSTRAINT_ANCHOR::END }, { leader->m_Uuid, CONSTRAINT_ANCHOR::END } } );
274
275 std::vector<PCB_CONSTRAINT*> constraints = { c };
277 BOOST_REQUIRE( adapter.Build( { seg }, constraints, nullptr, { leader } ) );
278
279 BOOST_CHECK( std::find( adapter.UnmappedConstraints().begin(), adapter.UnmappedConstraints().end(),
280 c->m_Uuid )
281 != adapter.UnmappedConstraints().end() );
282}
283
284
285// Driving length on centre-to-rim radial drives circle radius centre pinned
286BOOST_AUTO_TEST_CASE( RadialDrivingLengthDrivesCircleRadius )
287{
288 BOARD board;
289 PCB_SHAPE* circle = addCircle( board, { 0, 0 }, 5 * MM );
290
291 PCB_DIM_RADIAL* dim = addRadialDim( board, { 0, 0 }, { 5 * MM, 0 } );
292
293 // Bind as draw-time auto-constrain does centre coincident rim on outline
295 { { dim->m_Uuid, CONSTRAINT_ANCHOR::START }, { circle->m_Uuid, CONSTRAINT_ANCHOR::CENTER } } );
297 { { dim->m_Uuid, CONSTRAINT_ANCHOR::END }, { circle->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } } );
298
299 PCB_CONSTRAINT* driving = addDrivingLength( board, dim->m_Uuid, 8 * MM );
300
301 ApplyConstraintImmediately( &board, driving );
302
303 BOOST_CHECK_LE( std::abs( circle->GetRadius() - 8 * MM ), 5000 );
304 BOOST_CHECK_LE( ( circle->GetCenter() - VECTOR2I( 0, 0 ) ).EuclideanNorm(), 5000.0 );
305}
306
307
308// Radial text is hand placed so a driving radius change must carry it with the leader knee
309BOOST_AUTO_TEST_CASE( RadialDrivingLengthCarriesTextWithKnee )
310{
311 BOARD board;
312 PCB_SHAPE* circle = addCircle( board, { 0, 0 }, 5 * MM );
313
314 PCB_DIM_RADIAL* dim = addRadialDim( board, { 0, 0 }, { 5 * MM, 0 } );
315
316 const VECTOR2I textOffset( 3 * MM, -2 * MM );
317
318 dim->SetTextPos( dim->GetKnee() + textOffset );
319 dim->Update();
320
322 { { dim->m_Uuid, CONSTRAINT_ANCHOR::START }, { circle->m_Uuid, CONSTRAINT_ANCHOR::CENTER } } );
324 { { dim->m_Uuid, CONSTRAINT_ANCHOR::END }, { circle->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } } );
325
326 PCB_CONSTRAINT* driving = addDrivingLength( board, dim->m_Uuid, 12 * MM );
327
328 ApplyConstraintImmediately( &board, driving );
329
330 BOOST_CHECK_LE( std::abs( circle->GetRadius() - 12 * MM ), 5000 );
331 BOOST_CHECK_LE( std::abs( ( dim->GetEnd() - dim->GetStart() ).EuclideanNorm() - 12 * MM ), 5000.0 );
332
333 // Knee moved with the rim point so the text must have moved the same way
334 BOOST_CHECK_LE( ( ( dim->GetTextPos() - dim->GetKnee() ) - textOffset ).EuclideanNorm(), 5000.0 );
335}
336
337
338// Horizontal driving orthogonal fixes x gap only y stays free
339BOOST_AUTO_TEST_CASE( OrthogonalHorizontalDrivingSetsXAxisOnly )
340{
341 BOARD board;
342
343 PCB_DIM_ORTHOGONAL* dim =
344 addOrthogonalDim( board, { 0, 0 }, { 4 * MM, 3 * MM }, PCB_DIM_ORTHOGONAL::DIR::HORIZONTAL );
345
346 PCB_CONSTRAINT* driving = addDrivingLength( board, dim->m_Uuid, 10 * MM );
347
348 ApplyConstraintImmediately( &board, driving );
349
350 BOOST_CHECK_LE( std::abs( ( dim->GetEnd().x - dim->GetStart().x ) - 10 * MM ), 5000 );
351 BOOST_CHECK_LE( std::abs( dim->GetEnd().y - 3 * MM ), 5000 ); // y untouched
352}
353
354
355// Vertical driving orthogonal fixes y gap only x stays free
356BOOST_AUTO_TEST_CASE( OrthogonalVerticalDrivingSetsYAxisOnly )
357{
358 BOARD board;
359
360 PCB_DIM_ORTHOGONAL* dim =
361 addOrthogonalDim( board, { 0, 0 }, { 3 * MM, 4 * MM }, PCB_DIM_ORTHOGONAL::DIR::VERTICAL );
362
363 PCB_CONSTRAINT* driving = addDrivingLength( board, dim->m_Uuid, 10 * MM );
364
365 ApplyConstraintImmediately( &board, driving );
366
367 BOOST_CHECK_LE( std::abs( ( dim->GetEnd().y - dim->GetStart().y ) - 10 * MM ), 5000 );
368 BOOST_CHECK_LE( std::abs( dim->GetEnd().x - 3 * MM ), 5000 ); // x untouched
369}
370
371
372// Radial only binds when one circle or arc holds both centre and rim
373BOOST_AUTO_TEST_CASE( RadialBindingRequiresSameArcCenterAndOutline )
374{
375 BOARD board;
376 PCB_SHAPE* circle = addCircle( board, { 0, 0 }, 5 * MM );
377 const double tol = 10000; // 0.01 mm
378
379 PCB_DIM_RADIAL* dim = addRadialDim( board, { 0, 0 }, { 5 * MM, 0 } );
380
381 std::optional<KIID> hit =
382 SelectRadialDimensionTarget( &board, dim->m_Uuid, { 0, 0 }, { 5 * MM, 0 }, tol );
383 BOOST_REQUIRE( hit.has_value() );
384 BOOST_CHECK( *hit == circle->m_Uuid );
385
386 // Rim 3mm off a 5mm circle no binding
387 BOOST_CHECK( !SelectRadialDimensionTarget( &board, dim->m_Uuid, { 0, 0 }, { 3 * MM, 0 }, tol ) );
388
389 // Centre off circle centre no binding
390 BOOST_CHECK( !SelectRadialDimensionTarget( &board, dim->m_Uuid, { 2 * MM, 0 }, { 7 * MM, 0 }, tol ) );
391}
392
393
394// Centre on one circle rim on another binds neither
395BOOST_AUTO_TEST_CASE( RadialBindingRejectsCrossObjectCenterAndOutline )
396{
397 BOARD board;
398 const double tol = 10000;
399
400 addCircle( board, { 0, 0 }, 5 * MM ); // centre object
401 addCircle( board, { 50 * MM, 0 }, 5 * MM ); // outline object 55mm from first centre
402
403 BOOST_CHECK( !SelectRadialDimensionTarget( &board, KIID(), { 0, 0 }, { 55 * MM, 0 }, tol ) );
404}
405
406
407// Corner to corner dim binds both endpoints as indexed VERTEX anchors in canonical TL to BL order
408BOOST_AUTO_TEST_CASE( DimensionBindsRectCornerToCorner )
409{
410 BOARD board;
411 PCB_SHAPE* rect = addRect( board, { 0, 0 }, { 10 * MM, 10 * MM } );
412
413 KIID fakeDim; // dimension uuid not on the board as during interactive draw
414 std::vector<ENDPOINT_BINDING> bindings =
415 SelectEndpointBindings( &board, fakeDim, { 0, 0 }, VECTOR2I( 10 * MM, 10 * MM ), 1000.0 );
416
417 BOOST_REQUIRE_EQUAL( bindings.size(), 2 );
418 BOOST_CHECK( bindings[0].sourceAnchor == CONSTRAINT_ANCHOR::START );
419 BOOST_CHECK( bindings[0].target == CONSTRAINT_MEMBER( rect->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 0 ) );
420 BOOST_CHECK( bindings[1].sourceAnchor == CONSTRAINT_ANCHOR::END );
421 BOOST_CHECK( bindings[1].target == CONSTRAINT_MEMBER( rect->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 2 ) );
422
423 // Rect stored end before start still enumerates canonically index 0 always TL
424 PCB_SHAPE* reversed = addRect( board, { 30 * MM, 30 * MM }, { 20 * MM, 20 * MM } );
425 std::vector<CONSTRAINT_ANCHOR_POINT> anchors = ConstraintShapeAnchors( reversed );
426
427 BOOST_REQUIRE_EQUAL( anchors.size(), 4 );
428 BOOST_CHECK_EQUAL( anchors[0].index, 0 );
429 BOOST_CHECK_EQUAL( anchors[0].pos, VECTOR2I( 20 * MM, 20 * MM ) );
430 BOOST_CHECK_EQUAL( anchors[2].index, 2 );
431 BOOST_CHECK_EQUAL( anchors[2].pos, VECTOR2I( 30 * MM, 30 * MM ) );
432}
433
434
435// Drawn segment starting on a committed segment end binds coincident there the free end binds nothing
436BOOST_AUTO_TEST_CASE( ShapeEndpointBindsAtDrawTolerance )
437{
438 BOARD board;
439 PCB_SHAPE* first = addSegment( board, { 0, 0 }, { 10 * MM, 0 } );
440
441 const double tol = 0.01 * MM;
442
443 KIID newSeg; // uuid not on the board as during interactive draw
444 std::vector<ENDPOINT_BINDING> bindings =
445 SelectEndpointBindings( &board, newSeg, { 10 * MM, 0 }, VECTOR2I( 10 * MM, 20 * MM ), tol );
446
447 BOOST_REQUIRE_EQUAL( bindings.size(), 1 );
448 BOOST_CHECK( bindings[0].sourceAnchor == CONSTRAINT_ANCHOR::START );
449 BOOST_CHECK( bindings[0].target == CONSTRAINT_MEMBER( first->m_Uuid, CONSTRAINT_ANCHOR::END ) );
450
451 // Near the corner but outside draw tolerance binds nothing
452 bindings = SelectEndpointBindings( &board, newSeg, VECTOR2I( 10 * MM + MM / 10, 0 ), VECTOR2I( 30 * MM, 20 * MM ),
453 tol );
454
455 BOOST_CHECK( bindings.empty() );
456}
457
458
459// Dim across pentagon vertices binds by outline order VERTEX resolves to own index not vertex 0
460BOOST_AUTO_TEST_CASE( DimensionBindsPolygonVertices )
461{
462 BOARD board;
463
464 std::vector<VECTOR2I> pts = { { 0, 0 },
465 { 20 * MM, 5 * MM },
466 { 15 * MM, 20 * MM },
467 { 5 * MM, 20 * MM },
468 { -5 * MM, 10 * MM } };
469 PCB_SHAPE* poly = addPoly( board, pts );
470
471 KIID fakeDim;
472 std::vector<ENDPOINT_BINDING> bindings = SelectEndpointBindings( &board, fakeDim, pts[1], pts[3], 1000.0 );
473
474 BOOST_REQUIRE_EQUAL( bindings.size(), 2 );
475 BOOST_CHECK( bindings[0].sourceAnchor == CONSTRAINT_ANCHOR::START );
476 BOOST_CHECK( bindings[0].target == CONSTRAINT_MEMBER( poly->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 1 ) );
477 BOOST_CHECK( bindings[1].sourceAnchor == CONSTRAINT_ANCHOR::END );
478 BOOST_CHECK( bindings[1].target == CONSTRAINT_MEMBER( poly->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 3 ) );
479
480 // Anchor lookup must honour vertex index not just the VERTEX tag
481 std::optional<VECTOR2I> pos =
483
484 BOOST_REQUIRE( pos.has_value() );
485 BOOST_CHECK_EQUAL( *pos, pts[2] );
486}
487
488
489// Single polygon reaching both ends preferred over splitting onto a closer segment
490BOOST_AUTO_TEST_CASE( DimensionBindingPrefersSinglePolygonOverSplit )
491{
492 BOARD board;
493
494 PCB_SHAPE* poly = addPoly( board, { { 0, 0 },
495 { 10 * MM, 0 },
496 { 10 * MM, 10 * MM },
497 { 0, 10 * MM } } );
498 PCB_SHAPE* b = addSegment( board, { 2 * MM, 1 * MM }, { 2 * MM, 20 * MM } );
499
500 KIID fakeDim;
501 std::vector<ENDPOINT_BINDING> bindings =
502 SelectEndpointBindings( &board, fakeDim, { 0, 0 }, VECTOR2I( 2 * MM, 0 ), 12.0 * MM );
503
504 BOOST_REQUIRE_EQUAL( bindings.size(), 2 );
505 BOOST_CHECK( bindings[0].target == CONSTRAINT_MEMBER( poly->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 0 ) );
506 BOOST_CHECK( bindings[1].target == CONSTRAINT_MEMBER( poly->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 1 ) );
507
508 // Decoy segment never enters binding despite its start being nearest END
509 for( const ENDPOINT_BINDING& binding : bindings )
510 BOOST_CHECK( binding.target.m_item != b->m_Uuid );
511}
512
513
514// Arc-bearing outline not ingested by adapter so it offers no anchors
515BOOST_AUTO_TEST_CASE( ArcOutlinePolyOffersNoAnchors )
516{
517 BOARD board;
518
519 PCB_SHAPE* poly = new PCB_SHAPE( &board, SHAPE_T::POLY );
520
522 chain.Append( VECTOR2I( 10 * MM, 10 * MM ) );
523 chain.Append( VECTOR2I( 50 * MM, 10 * MM ) );
524 chain.Append( SHAPE_ARC( { 50 * MM, 10 * MM }, { 55 * MM, 25 * MM }, { 50 * MM, 40 * MM }, 0 ) );
525 chain.Append( VECTOR2I( 10 * MM, 40 * MM ) );
526 chain.SetClosed( true );
527
528 poly->GetPolyShape().AddOutline( chain );
529 board.Add( poly );
530
531 BOOST_REQUIRE_GT( poly->GetPolyShape().COutline( 0 ).ArcCount(), 0 );
532
533 BOOST_CHECK( ConstraintShapeAnchors( poly ).empty() );
534 BOOST_CHECK( !NearestConstraintAnchor( &board, VECTOR2I( 10 * MM, 10 * MM ), MM ) );
535}
536
537
538// Driving aligned dim widens rect to entered length height untouched endpoints ride corners
539BOOST_AUTO_TEST_CASE( AlignedDrivingLengthDrivesRectWidth )
540{
541 BOARD board;
542 PCB_SHAPE* rect = addRect( board, { 0, 0 }, { 10 * MM, 10 * MM } );
543
544 PCB_DIM_ALIGNED* dim = addAlignedDim( board, { 0, 0 }, { 10 * MM, 0 } ); // TL to TR
545
546 bindEndpoint( board, dim->m_Uuid, CONSTRAINT_ANCHOR::START,
547 { rect->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 0 } );
548 bindEndpoint( board, dim->m_Uuid, CONSTRAINT_ANCHOR::END,
549 { rect->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 1 } );
550
551 PCB_CONSTRAINT* driving = addDrivingLength( board, dim->m_Uuid, 15 * MM );
552
553 CONSTRAINT_DIAGNOSIS diag = ApplyConstraintImmediately( &board, driving );
554 BOOST_REQUIRE( diag.solved );
555
556 const VECTOR2I tl = rectTopLeft( rect );
557 const VECTOR2I br = rectBottomRight( rect );
558
559 BOOST_CHECK_LE( std::abs( ( br.x - tl.x ) - 15 * MM ), 5000 ); // width driven to the value
560 BOOST_CHECK_LE( std::abs( ( br.y - tl.y ) - 10 * MM ), 5000 ); // height untouched
561 BOOST_CHECK_LE( tl.EuclideanNorm(), 5000.0 ); // pinned TL corner held
562
563 // All four enumerated corners agree with solved extremes rectangle stayed coherent
564 std::vector<CONSTRAINT_ANCHOR_POINT> corners = ConstraintShapeAnchors( rect );
565 BOOST_REQUIRE_EQUAL( corners.size(), 4 );
566 BOOST_CHECK_EQUAL( corners[0].pos, tl );
567 BOOST_CHECK_EQUAL( corners[1].pos, VECTOR2I( br.x, tl.y ) );
568 BOOST_CHECK_EQUAL( corners[2].pos, br );
569 BOOST_CHECK_EQUAL( corners[3].pos, VECTOR2I( tl.x, br.y ) );
570
571 // Dimension endpoints followed corners through coincident bindings
572 BOOST_CHECK_LE( ( dim->GetStart() - corners[0].pos ).EuclideanNorm(), 5000.0 );
573 BOOST_CHECK_LE( ( dim->GetEnd() - corners[1].pos ).EuclideanNorm(), 5000.0 );
574}
575
576
577// Driving horizontal orthogonal on pentagon vertices forces x separation y and unbound vertices held
578BOOST_AUTO_TEST_CASE( OrthogonalDrivingLengthDrivesPolygonAxis )
579{
580 BOARD board;
581
582 std::vector<VECTOR2I> pts = { { 0, 0 },
583 { 20 * MM, 5 * MM },
584 { 15 * MM, 20 * MM },
585 { 5 * MM, 20 * MM },
586 { -5 * MM, 10 * MM } };
587 PCB_SHAPE* poly = addPoly( board, pts );
588
589 PCB_DIM_ORTHOGONAL* dim =
590 addOrthogonalDim( board, pts[1], pts[3], PCB_DIM_ORTHOGONAL::DIR::HORIZONTAL );
591
592 bindEndpoint( board, dim->m_Uuid, CONSTRAINT_ANCHOR::START,
593 { poly->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 1 } );
594 bindEndpoint( board, dim->m_Uuid, CONSTRAINT_ANCHOR::END,
595 { poly->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 3 } );
596
597 PCB_CONSTRAINT* driving = addDrivingLength( board, dim->m_Uuid, 20 * MM );
598
599 CONSTRAINT_DIAGNOSIS diag = ApplyConstraintImmediately( &board, driving );
600 BOOST_REQUIRE( diag.solved );
601
602 const SHAPE_LINE_CHAIN& outline = poly->GetPolyShape().COutline( 0 );
603
604 BOOST_CHECK_LE( std::abs( std::abs( outline.CPoint( 3 ).x - outline.CPoint( 1 ).x ) - 20 * MM ), 5000 );
605 BOOST_CHECK_LE( std::abs( outline.CPoint( 1 ).y - pts[1].y ), 5000 ); // y is not the driven axis
606 BOOST_CHECK_LE( std::abs( outline.CPoint( 3 ).y - pts[3].y ), 5000 );
607
608 for( int i : { 0, 2, 4 } )
609 BOOST_CHECK_LE( ( outline.CPoint( i ) - pts[i] ).EuclideanNorm(), 5000.0 ); // unbound vertices held
610
611 // Dimension endpoints followed the vertices they bind
612 BOOST_CHECK_LE( ( dim->GetStart() - outline.CPoint( 1 ) ).EuclideanNorm(), 5000.0 );
613 BOOST_CHECK_LE( ( dim->GetEnd() - outline.CPoint( 3 ) ).EuclideanNorm(), 5000.0 );
614}
615
616
617// Driven reference dim follows rect resize through move-tool re-solve length re-measures
618BOOST_AUTO_TEST_CASE( DrivenDimensionRemeasuresAfterRectResize )
619{
620 BOARD board;
621 PCB_SHAPE* rect = addRect( board, { 0, 0 }, { 10 * MM, 10 * MM } );
622
623 PCB_DIM_ALIGNED* dim = addAlignedDim( board, { 0, 0 }, { 10 * MM, 0 } );
624
625 bindEndpoint( board, dim->m_Uuid, CONSTRAINT_ANCHOR::START,
626 { rect->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 0 } );
627 bindEndpoint( board, dim->m_Uuid, CONSTRAINT_ANCHOR::END,
628 { rect->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 1 } );
629
630 // Driven mode carries a non-driving length that only mirrors measured geometry
631 PCB_CONSTRAINT* reference = addConstraint(
633 { { dim->m_Uuid, CONSTRAINT_ANCHOR::START }, { dim->m_Uuid, CONSTRAINT_ANCHOR::END } },
634 10.0 * MM );
635 reference->SetDriving( false );
636
637 rect->SetEnd( { 15 * MM, 10 * MM } ); // widen by 5mm edited outside the solver
638 ReSolveShapeClusters( &board, { rect } );
639
640 // Resize survived re-solve dimension endpoints tracked corners
641 BOOST_CHECK_LE( ( rectBottomRight( rect ) - VECTOR2I( 15 * MM, 10 * MM ) ).EuclideanNorm(), 5000.0 );
642 BOOST_CHECK_LE( ( dim->GetStart() - VECTOR2I( 0, 0 ) ).EuclideanNorm(), 5000.0 );
643 BOOST_CHECK_LE( ( dim->GetEnd() - VECTOR2I( 15 * MM, 0 ) ).EuclideanNorm(), 5000.0 );
644
645 BOOST_REQUIRE( reference->GetValue().has_value() );
646 BOOST_CHECK_LE( std::abs( *reference->GetValue() - 15.0 * MM ), 5000.0 );
647}
648
649
650// Properties edit is authoritative hold-edited re-solve keeps typed width and pulls bound items along
651BOOST_AUTO_TEST_CASE( HoldingEditedRectResizeFollowsBindings )
652{
653 BOARD board;
654 PCB_SHAPE* rect = addRect( board, { 0, 0 }, { 10 * MM, 10 * MM } );
655 PCB_SHAPE* seg = addSegment( board, { 10 * MM, 0 }, { 20 * MM, -5 * MM } );
656
658 { { seg->m_Uuid, CONSTRAINT_ANCHOR::START }, { rect->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 1 } } );
659
660 PCB_DIM_ALIGNED* dim = addAlignedDim( board, { 0, 0 }, { 10 * MM, 0 } ); // TL to TR
661
662 bindEndpoint( board, dim->m_Uuid, CONSTRAINT_ANCHOR::START,
663 { rect->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 0 } );
664 bindEndpoint( board, dim->m_Uuid, CONSTRAINT_ANCHOR::END,
665 { rect->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 1 } );
666
667 PCB_CONSTRAINT* reference = addConstraint(
669 { { dim->m_Uuid, CONSTRAINT_ANCHOR::START }, { dim->m_Uuid, CONSTRAINT_ANCHOR::END } },
670 10.0 * MM );
671 reference->SetDriving( false );
672
673 rect->SetEnd( { 15 * MM, 10 * MM } ); // widen by 5mm as a typed width edit would
674
675 std::vector<PCB_SHAPE*> modified;
676 ReSolveShapeClustersHoldingEdited( &board, { rect }, &modified );
677
678 // Edited geometry survived corner-bound segment start followed TR corner
679 BOOST_CHECK_LE( ( rectBottomRight( rect ) - VECTOR2I( 15 * MM, 10 * MM ) ).EuclideanNorm(), 5000.0 );
680 BOOST_CHECK_LE( ( rectTopLeft( rect ) - VECTOR2I( 0, 0 ) ).EuclideanNorm(), 5000.0 );
681 BOOST_CHECK_LE( ( seg->GetStart() - VECTOR2I( 15 * MM, 0 ) ).EuclideanNorm(), 5000.0 );
682 BOOST_CHECK( std::find( modified.begin(), modified.end(), seg ) != modified.end() );
683
684 // Both dimension endpoints ride the corners they bind Driven value re-measured
685 BOOST_CHECK_LE( ( dim->GetStart() - VECTOR2I( 0, 0 ) ).EuclideanNorm(), 5000.0 );
686 BOOST_CHECK_LE( ( dim->GetEnd() - VECTOR2I( 15 * MM, 0 ) ).EuclideanNorm(), 5000.0 );
687
688 BOOST_REQUIRE( reference->GetValue().has_value() );
689 BOOST_CHECK_LE( std::abs( *reference->GetValue() - 15.0 * MM ), 5000.0 );
690}
691
692
693// Stale vertex index left unmapped by Build does not poison solve Driving gate reports undrivable
694BOOST_AUTO_TEST_CASE( StaleVertexBindingDegradesAndGatesDriving )
695{
696 BOARD board;
697
698 std::vector<VECTOR2I> pts = { { 0, 0 },
699 { 20 * MM, 5 * MM },
700 { 15 * MM, 20 * MM },
701 { 5 * MM, 20 * MM },
702 { -5 * MM, 10 * MM } };
703 PCB_SHAPE* poly = addPoly( board, pts );
704
705 PCB_DIM_ALIGNED* dim = addAlignedDim( board, pts[1], pts[4] );
706
707 PCB_CONSTRAINT* startBinding = bindEndpoint( board, dim->m_Uuid, CONSTRAINT_ANCHOR::START,
708 { poly->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 1 } );
709 PCB_CONSTRAINT* endBinding = bindEndpoint( board, dim->m_Uuid, CONSTRAINT_ANCHOR::END,
710 { poly->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 4 } );
711
712 BOOST_CHECK( DimensionEndpointsBound( &board, dim ) );
713
714 poly->SetPolyPoints( { pts[0], pts[1], pts[2] } ); // vertex 4 no longer exists
715
716 std::vector<PCB_CONSTRAINT*> constraints( board.Constraints().begin(), board.Constraints().end() );
718
719 BOOST_REQUIRE( adapter.Build( { poly }, constraints, nullptr, { dim } ) );
720
721 const std::vector<KIID>& unmapped = adapter.UnmappedConstraints();
722 BOOST_CHECK( std::find( unmapped.begin(), unmapped.end(), endBinding->m_Uuid ) != unmapped.end() );
723 BOOST_CHECK( std::find( unmapped.begin(), unmapped.end(), startBinding->m_Uuid ) == unmapped.end() );
724
725 // Surviving binding still solves settled cluster stays put
726 BOOST_CHECK( adapter.Solve() );
727 BOOST_CHECK( adapter.Apply().empty() );
728
729 const SHAPE_LINE_CHAIN& outline = poly->GetPolyShape().COutline( 0 );
730
731 for( int i : { 0, 1, 2 } )
732 BOOST_CHECK_EQUAL( outline.CPoint( i ), pts[i] );
733
734 // Dialog offers Driving only through this predicate stale binding must fail it
735 BOOST_CHECK( !DimensionEndpointsBound( &board, dim ) );
736
737 // Restoring outline revives binding gate discriminates stale index from structurally broken
738 poly->SetPolyPoints( pts );
739 BOOST_CHECK( DimensionEndpointsBound( &board, dim ) );
740
741 // Deleting the bound item entirely fails gate through deleted-target leg
742 board.Remove( poly );
743 delete poly;
744 BOOST_CHECK( !DimensionEndpointsBound( &board, dim ) );
745}
746
747
748// Radial Driving gate needs centre and rim on one live circle or arc split legs never offer Driving
749BOOST_AUTO_TEST_CASE( RadialDrivingGateRequiresSingleMappableTarget )
750{
751 BOARD board;
752 PCB_SHAPE* circleA = addCircle( board, { 0, 0 }, 5 * MM );
753 PCB_SHAPE* circleB = addCircle( board, { 50 * MM, 0 }, 5 * MM );
754 PCB_SHAPE* seg = addSegment( board, { 0, 30 * MM }, { 10 * MM, 30 * MM } );
755
756 // Centre and rim both on circle A exactly as authored gates drivable
757 PCB_DIM_RADIAL* bound = addRadialDim( board, { 0, 0 }, { 5 * MM, 0 } );
759 { { bound->m_Uuid, CONSTRAINT_ANCHOR::START }, { circleA->m_Uuid, CONSTRAINT_ANCHOR::CENTER } } );
761 { { bound->m_Uuid, CONSTRAINT_ANCHOR::END }, { circleA->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } } );
762
763 BOOST_CHECK( DimensionEndpointsBound( &board, bound ) );
764
765 // Centre on A rim on B is not a radius of either object
766 PCB_DIM_RADIAL* split = addRadialDim( board, { 0, 0 }, { 55 * MM, 0 } );
768 { { split->m_Uuid, CONSTRAINT_ANCHOR::START }, { circleA->m_Uuid, CONSTRAINT_ANCHOR::CENTER } } );
770 { { split->m_Uuid, CONSTRAINT_ANCHOR::END }, { circleB->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } } );
771
772 BOOST_CHECK( !DimensionEndpointsBound( &board, split ) );
773
774 // Both legs share a segment but a segment carries no radius to drive
775 PCB_DIM_RADIAL* onSegment = addRadialDim( board, { 5 * MM, 30 * MM }, { 10 * MM, 30 * MM } );
777 { { onSegment->m_Uuid, CONSTRAINT_ANCHOR::START }, { seg->m_Uuid, CONSTRAINT_ANCHOR::CENTER } } );
779 { { onSegment->m_Uuid, CONSTRAINT_ANCHOR::END }, { seg->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } } );
780
781 BOOST_CHECK( !DimensionEndpointsBound( &board, onSegment ) );
782}
783
784
785// Rect stored end before start still binds VERTEX 0 to physical top-left not stored-start corner
786BOOST_AUTO_TEST_CASE( SwappedRectVertexZeroIsPhysicalTopLeft )
787{
788 BOARD board;
789 PCB_SHAPE* rect = addRect( board, { 10 * MM, 10 * MM }, { 0, 0 } ); // start is BR
790
791 PCB_SHAPE* seg = addSegment( board, { 20 * MM, 20 * MM }, { 2 * MM, 1 * MM } );
792
795 { { seg->m_Uuid, CONSTRAINT_ANCHOR::END }, { rect->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 0 } } );
796
798 BOOST_REQUIRE( diag.solved );
799
800 // First member the segment end is pinned rect physical top-left snapped onto it opposite corner held
801 BOOST_CHECK_LE( ( seg->GetEnd() - VECTOR2I( 2 * MM, 1 * MM ) ).EuclideanNorm(), 5000.0 );
802 BOOST_CHECK_LE( ( rectTopLeft( rect ) - VECTOR2I( 2 * MM, 1 * MM ) ).EuclideanNorm(), 5000.0 );
803 BOOST_CHECK_LE( ( rectBottomRight( rect ) - VECTOR2I( 10 * MM, 10 * MM ) ).EuclideanNorm(), 5000.0 );
804}
805
806
807// Insert ahead of bound vertex shifts persisted index member keeps its corner footprint constraints remap too
808BOOST_AUTO_TEST_CASE( VertexInsertShiftsMembersPastInsertionPoint )
809{
810 BOARD board;
811
812 std::vector<VECTOR2I> pts = { { 0, 0 },
813 { 20 * MM, 5 * MM },
814 { 15 * MM, 20 * MM },
815 { 5 * MM, 20 * MM },
816 { -5 * MM, 10 * MM } };
817 PCB_SHAPE* poly = addPoly( board, pts );
818 PCB_SHAPE* seg = addSegment( board, pts[3], { 30 * MM, 0 } );
819
822 { poly->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 3 } } );
823
824 FOOTPRINT* fp = new FOOTPRINT( &board );
825 board.Add( fp );
826
828 onFootprint->AddMember( poly->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 4 );
829 fp->Add( onFootprint );
830
831 // Mutate outline the way addCorner does then remap with same edit description
832 poly->GetPolyShape().InsertVertex( 2, ( pts[1] + pts[2] ) / 2 );
833
834 std::vector<BOARD_ITEM*> modified;
835 std::vector<BOARD_ITEM*> removed;
836
837 RemapPolygonVertexMembers( &board, poly->m_Uuid, 2, 1,
838 [&]( BOARD_ITEM* aItem ) { modified.push_back( aItem ); },
839 [&]( BOARD_ITEM* aItem ) { removed.push_back( aItem ); } );
840
841 BOOST_CHECK( onBoard->GetMembers()[1] == CONSTRAINT_MEMBER( poly->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 4 ) );
842 BOOST_CHECK( onFootprint->GetMembers()[0] == CONSTRAINT_MEMBER( poly->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 5 ) );
843
844 // Remapped members resolve to the corners they were authored on
845 std::optional<VECTOR2I> boardPos = ConstraintAnchorPosition( &board, onBoard->GetMembers()[1] );
846 std::optional<VECTOR2I> fpPos = ConstraintAnchorPosition( &board, onFootprint->GetMembers()[0] );
847
848 BOOST_REQUIRE( boardPos.has_value() );
849 BOOST_CHECK_EQUAL( *boardPos, pts[3] );
850 BOOST_REQUIRE( fpPos.has_value() );
851 BOOST_CHECK_EQUAL( *fpPos, pts[4] );
852
853 // Both re-indexed constraints were staged an insertion never retires one
854 BOOST_CHECK_EQUAL( modified.size(), 2 );
855 BOOST_CHECK( removed.empty() );
856}
857
858
859// Delete vertex below bound one shifts index down still same physical corner
860BOOST_AUTO_TEST_CASE( VertexDeleteBelowShiftsMemberDown )
861{
862 BOARD board;
863
864 std::vector<VECTOR2I> pts = { { 0, 0 },
865 { 20 * MM, 5 * MM },
866 { 15 * MM, 20 * MM },
867 { 5 * MM, 20 * MM },
868 { -5 * MM, 10 * MM } };
869 PCB_SHAPE* poly = addPoly( board, pts );
870 PCB_SHAPE* seg = addSegment( board, pts[3], { 30 * MM, 0 } );
871
874 { poly->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 3 } } );
875
876 // Mutate outline the way removeCorner does
877 poly->GetPolyShape().RemoveVertex( 1 );
878
879 std::vector<BOARD_ITEM*> removed;
880
881 RemapPolygonVertexMembers( &board, poly->m_Uuid, 1, -1,
882 []( BOARD_ITEM* ) {},
883 [&]( BOARD_ITEM* aItem ) { removed.push_back( aItem ); } );
884
885 BOOST_CHECK( c->GetMembers()[1] == CONSTRAINT_MEMBER( poly->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 2 ) );
886
887 std::optional<VECTOR2I> pos = ConstraintAnchorPosition( &board, c->GetMembers()[1] );
888
889 BOOST_REQUIRE( pos.has_value() );
890 BOOST_CHECK_EQUAL( *pos, pts[3] );
891 BOOST_CHECK( removed.empty() );
892}
893
894
895// Delete bound vertex itself leaves nothing to express whole constraint staged for removal
896BOOST_AUTO_TEST_CASE( VertexDeleteOfBoundVertexRetiresConstraint )
897{
898 BOARD board;
899
900 std::vector<VECTOR2I> pts = { { 0, 0 },
901 { 20 * MM, 5 * MM },
902 { 15 * MM, 20 * MM },
903 { 5 * MM, 20 * MM },
904 { -5 * MM, 10 * MM } };
905 PCB_SHAPE* poly = addPoly( board, pts );
906 PCB_SHAPE* seg = addSegment( board, pts[3], { 30 * MM, 0 } );
907
910 { poly->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 3 } } );
911
912 poly->GetPolyShape().RemoveVertex( 3 );
913
914 std::vector<BOARD_ITEM*> modified;
915 std::vector<BOARD_ITEM*> removed;
916
917 RemapPolygonVertexMembers( &board, poly->m_Uuid, 3, -1,
918 [&]( BOARD_ITEM* aItem ) { modified.push_back( aItem ); },
919 [&]( BOARD_ITEM* aItem ) { removed.push_back( aItem ); } );
920
921 // Retiring constraint staged whole never edited undo keeps authored members
922 BOOST_REQUIRE_EQUAL( removed.size(), 1 );
923 BOOST_CHECK( removed[0] == c );
924 BOOST_CHECK( modified.empty() );
925 BOOST_CHECK( c->GetMembers()[1] == CONSTRAINT_MEMBER( poly->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 3 ) );
926
927 // Complete the edit the way the commit would
928 board.Remove( c );
929 delete c;
930
931 BOOST_CHECK( board.Constraints().empty() );
932}
933
934
935// Chamfer composes insert then delete order is load-bearing reversed order nets wrong shift
936BOOST_AUTO_TEST_CASE( ChamferRemapOrderNetsOneHigherPastTheCorner )
937{
938 BOARD board;
939
940 std::vector<VECTOR2I> pts = { { 0, 0 },
941 { 20 * MM, 5 * MM },
942 { 15 * MM, 20 * MM },
943 { 5 * MM, 20 * MM },
944 { -5 * MM, 10 * MM } };
945 PCB_SHAPE* poly = addPoly( board, pts );
946
947 const int k = 2;
948
950 { { poly->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, k - 1 } } );
952 { { poly->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, k } } );
954 { { poly->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, k + 1 } } );
956 { { poly->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, k },
957 { poly->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, k + 2 } } );
958
959 // Mutate outline the way chamferCorner does replacing corner k with two chamfer points
960 VECTOR2I a = pts[k] + ( pts[k - 1] - pts[k] ) / 4;
961 VECTOR2I b = pts[k] + ( pts[k + 1] - pts[k] ) / 4;
962
963 poly->GetPolyShape().RemoveVertex( k );
964 poly->GetPolyShape().InsertVertex( k, b );
965 poly->GetPolyShape().InsertVertex( k, a );
966
967 std::vector<BOARD_ITEM*> modified;
968 std::vector<BOARD_ITEM*> removed;
969
970 auto modify = [&]( BOARD_ITEM* aItem ) { modified.push_back( aItem ); };
971 auto remove = [&]( BOARD_ITEM* aItem ) { removed.push_back( aItem ); };
972
973 RemapPolygonVertexMembers( &board, poly->m_Uuid, k + 1, 2, modify, remove );
974 RemapPolygonVertexMembers( &board, poly->m_Uuid, k, -1, modify, remove );
975
976 BOOST_CHECK( below->GetMembers()[0] == CONSTRAINT_MEMBER( poly->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, k - 1 ) );
977 BOOST_CHECK( above->GetMembers()[0] == CONSTRAINT_MEMBER( poly->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, k + 2 ) );
978
979 // Netted member still resolves to its own corner one past the chamfer points
980 std::optional<VECTOR2I> pos = ConstraintAnchorPosition( &board, above->GetMembers()[0] );
981
982 BOOST_REQUIRE( pos.has_value() );
983 BOOST_CHECK_EQUAL( *pos, pts[k + 1] );
984
985 // Both constraints on the chamfered corner retire mixed one was Modify-staged first by insert pass
986 BOOST_REQUIRE_EQUAL( removed.size(), 2 );
987 BOOST_CHECK_EQUAL( std::ranges::count( removed, onCorner ), 1 );
988 BOOST_CHECK_EQUAL( std::ranges::count( removed, both ), 1 );
989 BOOST_CHECK_EQUAL( std::ranges::count( modified, both ), 1 );
990 BOOST_CHECK_EQUAL( std::ranges::count( modified, onCorner ), 0 );
991 BOOST_CHECK_EQUAL( std::ranges::count( modified, below ), 0 );
992 BOOST_CHECK( both->GetMembers()[0] == CONSTRAINT_MEMBER( poly->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, k ) );
993 BOOST_CHECK( both->GetMembers()[1] == CONSTRAINT_MEMBER( poly->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, k + 4 ) );
994}
995
996
997// Remapping scoped to edited polygon other shapes and lower indices never move
998BOOST_AUTO_TEST_CASE( VertexRemapLeavesOtherShapesAndLowerIndicesAlone )
999{
1000 BOARD board;
1001
1002 std::vector<VECTOR2I> pts = { { 0, 0 },
1003 { 20 * MM, 5 * MM },
1004 { 15 * MM, 20 * MM },
1005 { 5 * MM, 20 * MM },
1006 { -5 * MM, 10 * MM } };
1007 PCB_SHAPE* edited = addPoly( board, pts );
1008
1009 std::vector<VECTOR2I> otherPts = { { 40 * MM, 0 }, { 60 * MM, 0 }, { 60 * MM, 20 * MM }, { 40 * MM, 20 * MM } };
1010 PCB_SHAPE* other = addPoly( board, otherPts );
1011 PCB_SHAPE* rect = addRect( board, { 0, 40 * MM }, { 10 * MM, 50 * MM } );
1012 PCB_SHAPE* seg = addSegment( board, { 0, 60 * MM }, { 10 * MM, 60 * MM } );
1013
1015 { { other->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 3 } } );
1017 { { seg->m_Uuid, CONSTRAINT_ANCHOR::START },
1018 { rect->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 2 } } );
1020 { { seg->m_Uuid, CONSTRAINT_ANCHOR::END },
1021 { edited->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 1 } } );
1022
1023 edited->GetPolyShape().InsertVertex( 2, ( pts[1] + pts[2] ) / 2 );
1024
1025 std::vector<BOARD_ITEM*> modified;
1026 std::vector<BOARD_ITEM*> removed;
1027
1028 RemapPolygonVertexMembers( &board, edited->m_Uuid, 2, 1,
1029 [&]( BOARD_ITEM* aItem ) { modified.push_back( aItem ); },
1030 [&]( BOARD_ITEM* aItem ) { removed.push_back( aItem ); } );
1031
1032 BOOST_CHECK( onOther->GetMembers()[0] == CONSTRAINT_MEMBER( other->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 3 ) );
1033 BOOST_CHECK( onRect->GetMembers()[1] == CONSTRAINT_MEMBER( rect->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 2 ) );
1034 BOOST_CHECK( below->GetMembers()[1] == CONSTRAINT_MEMBER( edited->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 1 ) );
1035
1036 // No constraint needed re-indexing so nothing was staged
1037 BOOST_CHECK( modified.empty() );
1038 BOOST_CHECK( removed.empty() );
1039}
1040
1041
1042// Cardinal rotation keeps RECTANGLE corner roles re-canonicalize VERTEX 0 resolves to new top-left
1043BOOST_AUTO_TEST_CASE( CardinalRotationRecanonicalizesRectRoles )
1044{
1045 BOARD board;
1046 PCB_SHAPE* rect = addRect( board, { 2 * MM, 1 * MM }, { 12 * MM, 5 * MM } );
1047
1048 std::vector<CONSTRAINT_ANCHOR_POINT> before = ConstraintShapeAnchors( rect );
1049 BOOST_REQUIRE_EQUAL( before.size(), 4 );
1050
1051 const VECTOR2I pivot( 0, 0 );
1052 const EDA_ANGLE angle( 90, DEGREES_T );
1053
1054 rect->Rotate( pivot, angle );
1056
1057 // Physical corners after the turn from pre-rotation enumeration
1058 std::vector<VECTOR2I> rotated;
1059
1060 for( const CONSTRAINT_ANCHOR_POINT& a : before )
1061 {
1062 VECTOR2I p = a.pos;
1063 RotatePoint( p, pivot, angle );
1064 rotated.push_back( p );
1065 }
1066
1067 VECTOR2I tl = rotated[0];
1068 VECTOR2I br = rotated[0];
1069
1070 for( const VECTOR2I& p : rotated )
1071 {
1072 tl.x = std::min( tl.x, p.x );
1073 tl.y = std::min( tl.y, p.y );
1074 br.x = std::max( br.x, p.x );
1075 br.y = std::max( br.y, p.y );
1076 }
1077
1078 std::vector<CONSTRAINT_ANCHOR_POINT> after = ConstraintShapeAnchors( rect );
1079 BOOST_REQUIRE_EQUAL( after.size(), 4 );
1080 BOOST_CHECK_EQUAL( after[0].pos, tl );
1081 BOOST_CHECK_EQUAL( after[1].pos, VECTOR2I( br.x, tl.y ) );
1082 BOOST_CHECK_EQUAL( after[2].pos, br );
1083 BOOST_CHECK_EQUAL( after[3].pos, VECTOR2I( tl.x, br.y ) );
1084
1085 // Role semantics not corner tracking member resolves to new top-left a different physical corner
1086 std::optional<VECTOR2I> v0 =
1088
1089 BOOST_REQUIRE( v0.has_value() );
1090 BOOST_CHECK_EQUAL( *v0, tl );
1091 BOOST_CHECK( *v0 != rotated[0] );
1092}
1093
1094
1095// Non-cardinal rotation converts rect to a 4-vertex POLY under same KIID canonical order keeps bindings
1096BOOST_AUTO_TEST_CASE( NonCardinalRotationKeepsRectVertexBindings )
1097{
1098 BOARD board;
1099
1100 auto checkRect = [&]( PCB_SHAPE* rect )
1101 {
1102 std::vector<CONSTRAINT_ANCHOR_POINT> before = ConstraintShapeAnchors( rect );
1103 BOOST_REQUIRE_EQUAL( before.size(), 4 );
1104
1105 PCB_DIM_ALIGNED* dim = addAlignedDim( board, before[0].pos, before[1].pos );
1106
1107 bindEndpoint( board, dim->m_Uuid, CONSTRAINT_ANCHOR::START,
1108 { rect->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 0 } );
1109 bindEndpoint( board, dim->m_Uuid, CONSTRAINT_ANCHOR::END,
1110 { rect->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 1 } );
1111
1112 BOOST_CHECK( DimensionEndpointsBound( &board, dim ) );
1113
1114 const VECTOR2I pivot = rect->GetCenter();
1115 const EDA_ANGLE angle( 30, DEGREES_T );
1116
1117 rect->Rotate( pivot, angle );
1118
1119 BOOST_REQUIRE( rect->GetShape() == SHAPE_T::POLY );
1120
1121 const SHAPE_LINE_CHAIN& outline = rect->GetPolyShape().COutline( 0 );
1122 BOOST_REQUIRE_EQUAL( outline.PointCount(), 4 );
1123 BOOST_CHECK_EQUAL( outline.ArcCount(), 0 );
1124
1125 for( int i = 0; i < 4; ++i )
1126 {
1127 VECTOR2I expected = before[i].pos;
1128 RotatePoint( expected, pivot, angle );
1129
1130 std::optional<VECTOR2I> pos =
1131 ConstraintAnchorPosition( &board, { rect->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, i } );
1132
1133 BOOST_REQUIRE( pos.has_value() );
1134 BOOST_CHECK_LE( ( *pos - expected ).EuclideanNorm(), 2.0 ); // integer rounding only
1135 }
1136
1137 BOOST_CHECK( DimensionEndpointsBound( &board, dim ) );
1138 };
1139
1140 // One rect stored start at TL one end before start conversion normalizes both to same order
1141 checkRect( addRect( board, { 0, 0 }, { 10 * MM, 4 * MM } ) );
1142 checkRect( addRect( board, { 40 * MM, 4 * MM }, { 30 * MM, 0 } ) );
1143}
1144
1145
1146// Rounded rect rotated off-cardinal becomes arc-bearing outline gate rejects members degrade to unmapped
1147BOOST_AUTO_TEST_CASE( RoundedRectNonCardinalRotationDegradesToUnmapped )
1148{
1149 BOARD board;
1150 PCB_SHAPE* rect = addRect( board, { 0, 0 }, { 10 * MM, 6 * MM } );
1151 rect->SetCornerRadius( 1 * MM );
1152
1153 std::vector<CONSTRAINT_ANCHOR_POINT> before = ConstraintShapeAnchors( rect );
1154 BOOST_REQUIRE_EQUAL( before.size(), 4 );
1155
1156 PCB_DIM_ALIGNED* dim = addAlignedDim( board, before[0].pos, before[1].pos );
1157
1158 PCB_CONSTRAINT* startBinding = bindEndpoint( board, dim->m_Uuid, CONSTRAINT_ANCHOR::START,
1159 { rect->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 0 } );
1160 PCB_CONSTRAINT* endBinding = bindEndpoint( board, dim->m_Uuid, CONSTRAINT_ANCHOR::END,
1161 { rect->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 1 } );
1162
1163 BOOST_CHECK( DimensionEndpointsBound( &board, dim ) );
1164
1165 rect->Rotate( { 0, 0 }, EDA_ANGLE( 30, DEGREES_T ) );
1166
1167 BOOST_REQUIRE( rect->GetShape() == SHAPE_T::POLY );
1168 BOOST_CHECK_GT( rect->GetPolyShape().COutline( 0 ).ArcCount(), 0 );
1169
1170 // No anchors no resolution no Driving offer
1171 BOOST_CHECK( ConstraintShapeAnchors( rect ).empty() );
1172 BOOST_CHECK( !ConstraintAnchorPosition( &board, { rect->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 0 } ) );
1173 BOOST_CHECK( !DimensionEndpointsBound( &board, dim ) );
1174
1175 // Adapter leaves both bindings unmapped rather than poisoning the build
1176 std::vector<PCB_CONSTRAINT*> constraints( board.Constraints().begin(), board.Constraints().end() );
1178
1179 BOOST_REQUIRE( adapter.Build( { rect }, constraints, nullptr, { dim } ) );
1180
1181 const std::vector<KIID>& unmapped = adapter.UnmappedConstraints();
1182 BOOST_CHECK( std::find( unmapped.begin(), unmapped.end(), startBinding->m_Uuid ) != unmapped.end() );
1183 BOOST_CHECK( std::find( unmapped.begin(), unmapped.end(), endBinding->m_Uuid ) != unmapped.end() );
1184}
1185
1186
1187// Driving gate scans footprint-parented constraints too stale footprint binding revokes it
1188BOOST_AUTO_TEST_CASE( FootprintParentedBindingGatesDriving )
1189{
1190 BOARD board;
1191 FOOTPRINT* fp = new FOOTPRINT( &board );
1192 board.Add( fp );
1193
1194 PCB_SHAPE* rect = new PCB_SHAPE( fp, SHAPE_T::RECTANGLE );
1195 rect->SetStart( { 0, 0 } );
1196 rect->SetEnd( { 10 * MM, 10 * MM } );
1197 fp->Add( rect );
1198
1199 PCB_DIM_ALIGNED* dim = addAlignedDim( board, { 0, 0 }, { 10 * MM, 0 } );
1200
1201 auto bindOnFootprint = [&]( CONSTRAINT_ANCHOR aDimAnchor, int aCorner )
1202 {
1204 c->AddMember( dim->m_Uuid, aDimAnchor );
1205 c->AddMember( rect->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, aCorner );
1206 fp->Add( c );
1207 return c;
1208 };
1209
1210 bindOnFootprint( CONSTRAINT_ANCHOR::START, 0 );
1211 PCB_CONSTRAINT* endBinding = bindOnFootprint( CONSTRAINT_ANCHOR::END, 1 );
1212
1213 // Nothing lives at board level drivable verdict can only come from footprint scan
1214 BOOST_CHECK( board.Constraints().empty() );
1215 BOOST_CHECK( DimensionEndpointsBound( &board, dim ) );
1216
1217 // Stale vertex index on footprint-parented binding no longer resolves gate reports undrivable
1218 endBinding->Members()[1].m_index = 7;
1219 BOOST_CHECK( !DimensionEndpointsBound( &board, dim ) );
1220}
1221
1222
1223namespace
1224{
1225// Staging callbacks a live commit would provide recording touched items and completing add remove
1226struct MODE_STAGING
1227{
1228 BOARD& board;
1229 std::vector<BOARD_ITEM*> modified;
1230 std::vector<BOARD_ITEM*> added;
1231 std::vector<BOARD_ITEM*> removed;
1232
1233 std::function<void( BOARD_ITEM* )> modify()
1234 {
1235 return [this]( BOARD_ITEM* aItem ) { modified.push_back( aItem ); };
1236 }
1237
1238 std::function<void( BOARD_ITEM* )> add()
1239 {
1240 return [this]( BOARD_ITEM* aItem )
1241 {
1242 added.push_back( aItem );
1243 board.Add( aItem );
1244 };
1245 }
1246
1247 std::function<void( BOARD_ITEM* )> remove()
1248 {
1249 return [this]( BOARD_ITEM* aItem )
1250 {
1251 removed.push_back( aItem );
1252 board.Remove( aItem );
1253 delete aItem;
1254 };
1255 }
1256};
1257}
1258
1259
1260// Value mode derives from board state Driven default Arbitrary on override Driving needs self length
1261// Only aligned orthogonal radial dimensions carry a mode
1262BOOST_AUTO_TEST_CASE( ValueModeDerivation )
1263{
1264 BOARD board;
1265 PCB_DIM_ALIGNED* dim = addAlignedDim( board, { 0, 0 }, { 10 * MM, 0 } );
1266
1267 BOOST_CHECK( DimensionValueMode( &board, dim ) == DIM_VALUE_MODE::DRIVEN );
1268 BOOST_CHECK( dim->GetValueMode() == DIM_VALUE_MODE::DRIVEN );
1269
1270 dim->ChangeOverrideText( wxS( "custom" ) );
1271 BOOST_CHECK( DimensionValueMode( &board, dim ) == DIM_VALUE_MODE::ARBITRARY );
1272
1273 PCB_CONSTRAINT* reference = addConstraint(
1276 10.0 * MM );
1277 reference->SetDriving( false );
1278
1279 BOOST_CHECK( FindDimensionLengthConstraint( &board, dim ) == reference );
1280 BOOST_CHECK( DimensionValueMode( &board, dim ) == DIM_VALUE_MODE::ARBITRARY );
1281
1282 dim->SetOverrideTextEnabled( false );
1283 BOOST_CHECK( DimensionValueMode( &board, dim ) == DIM_VALUE_MODE::DRIVEN );
1284
1285 reference->SetDriving( true );
1286 BOOST_CHECK( DimensionValueMode( &board, dim ) == DIM_VALUE_MODE::DRIVING );
1287
1288 // Only value-bearing families carry the mode
1289 PCB_DIM_LEADER* leader = new PCB_DIM_LEADER( &board );
1290 board.Add( leader );
1291
1292 BOOST_CHECK( DimensionHasValueMode( dim ) );
1293 BOOST_CHECK( !DimensionHasValueMode( leader ) );
1294 BOOST_CHECK( !DimensionHasValueMode( nullptr ) );
1295}
1296
1297
1298// Mode transitions through shared setter Driven to Driving creates length Arbitrary installs text
1299// Driven again clears override every touched item goes through staging callbacks
1300BOOST_AUTO_TEST_CASE( SetValueModeTransitions )
1301{
1302 BOARD board;
1303 PCB_SHAPE* rect = addRect( board, { 0, 0 }, { 10 * MM, 10 * MM } );
1304
1305 PCB_DIM_ALIGNED* dim = addAlignedDim( board, { 0, 0 }, { 10 * MM, 0 } );
1306
1307 bindEndpoint( board, dim->m_Uuid, CONSTRAINT_ANCHOR::START,
1308 { rect->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 0 } );
1309 bindEndpoint( board, dim->m_Uuid, CONSTRAINT_ANCHOR::END,
1310 { rect->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 1 } );
1311
1312 BOOST_REQUIRE( DimensionCanDrive( &board, dim ) );
1313
1314 // Driven to Driving creates the self driving length with the seeded value
1315 MODE_STAGING toDriving{ board };
1316 PCB_CONSTRAINT* driving =
1317 SetDimensionValueMode( &board, dim, DIM_VALUE_MODE::DRIVING, 12 * MM, std::nullopt,
1318 toDriving.modify(), toDriving.add(), toDriving.remove() );
1319
1320 BOOST_REQUIRE( driving );
1321 BOOST_CHECK( FindDimensionLengthConstraint( &board, dim ) == driving );
1322 BOOST_CHECK( driving->IsDriving() );
1323 BOOST_REQUIRE( driving->GetValue().has_value() );
1324 BOOST_CHECK_EQUAL( *driving->GetValue(), 12 * MM );
1325 BOOST_CHECK( DimensionValueMode( &board, dim ) == DIM_VALUE_MODE::DRIVING );
1326 BOOST_CHECK_EQUAL( toDriving.added.size(), 1 );
1327 BOOST_CHECK( toDriving.added[0] == driving );
1328 BOOST_CHECK( toDriving.removed.empty() );
1329
1330 // Driving to Arbitrary retires the driving length and installs custom text
1331 MODE_STAGING toArbitrary{ board };
1333 SetDimensionValueMode( &board, dim, DIM_VALUE_MODE::ARBITRARY, std::nullopt,
1334 wxString( wxS( "REF" ) ), toArbitrary.modify(), toArbitrary.add(),
1335 toArbitrary.remove() );
1336
1337 BOOST_CHECK( !result );
1338 BOOST_CHECK( !FindDimensionLengthConstraint( &board, dim ) );
1339 BOOST_CHECK( DimensionValueMode( &board, dim ) == DIM_VALUE_MODE::ARBITRARY );
1340 BOOST_CHECK_EQUAL( dim->GetOverrideText(), wxS( "REF" ) );
1341 BOOST_CHECK_EQUAL( toArbitrary.removed.size(), 1 );
1342
1343 // Arbitrary to Driven just clears the override bindings stay
1344 MODE_STAGING toDriven{ board };
1345 SetDimensionValueMode( &board, dim, DIM_VALUE_MODE::DRIVEN, std::nullopt, std::nullopt,
1346 toDriven.modify(), toDriven.add(), toDriven.remove() );
1347
1348 BOOST_CHECK( DimensionValueMode( &board, dim ) == DIM_VALUE_MODE::DRIVEN );
1349 BOOST_CHECK( !dim->GetOverrideTextEnabled() );
1350 BOOST_CHECK( toDriven.added.empty() );
1351 BOOST_CHECK( toDriven.removed.empty() );
1352 BOOST_CHECK( DimensionEndpointsBound( &board, dim ) );
1353}
1354
1355
1356// Driving transition rejected board untouched when unbound or length absent or non-positive
1357BOOST_AUTO_TEST_CASE( SetValueModeDrivingRejections )
1358{
1359 BOARD board;
1360 PCB_DIM_ALIGNED* unbound = addAlignedDim( board, { 0, 0 }, { 10 * MM, 0 } );
1361
1362 MODE_STAGING staging{ board };
1363
1364 BOOST_CHECK( !SetDimensionValueMode( &board, unbound, DIM_VALUE_MODE::DRIVING, 12 * MM,
1365 std::nullopt, staging.modify(), staging.add(),
1366 staging.remove() ) );
1367
1368 PCB_SHAPE* rect = addRect( board, { 0, 0 }, { 10 * MM, 10 * MM } );
1369 PCB_DIM_ALIGNED* bound = addAlignedDim( board, { 0, 0 }, { 10 * MM, 0 } );
1370
1371 bindEndpoint( board, bound->m_Uuid, CONSTRAINT_ANCHOR::START,
1372 { rect->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 0 } );
1373 bindEndpoint( board, bound->m_Uuid, CONSTRAINT_ANCHOR::END,
1374 { rect->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 1 } );
1375
1376 BOOST_CHECK( !SetDimensionValueMode( &board, bound, DIM_VALUE_MODE::DRIVING, std::nullopt,
1377 std::nullopt, staging.modify(), staging.add(),
1378 staging.remove() ) );
1379 BOOST_CHECK( !SetDimensionValueMode( &board, bound, DIM_VALUE_MODE::DRIVING, 0, std::nullopt,
1380 staging.modify(), staging.add(), staging.remove() ) );
1381 BOOST_CHECK( !SetDimensionValueMode( &board, bound, DIM_VALUE_MODE::DRIVING, -5 * MM,
1382 std::nullopt, staging.modify(), staging.add(),
1383 staging.remove() ) );
1384
1385 // A leader never takes a value mode at all
1386 PCB_DIM_LEADER* leader = new PCB_DIM_LEADER( &board );
1387 board.Add( leader );
1388
1389 BOOST_CHECK( !SetDimensionValueMode( &board, leader, DIM_VALUE_MODE::DRIVING, 12 * MM,
1390 std::nullopt, staging.modify(), staging.add(),
1391 staging.remove() ) );
1392
1393 BOOST_CHECK( staging.modified.empty() );
1394 BOOST_CHECK( staging.added.empty() );
1395 BOOST_CHECK( staging.removed.empty() );
1396 BOOST_CHECK( !FindDimensionLengthConstraint( &board, unbound ) );
1397 BOOST_CHECK( !FindDimensionLengthConstraint( &board, bound ) );
1398 BOOST_CHECK( DimensionValueMode( &board, unbound ) == DIM_VALUE_MODE::DRIVEN );
1399 BOOST_CHECK( DimensionValueMode( &board, bound ) == DIM_VALUE_MODE::DRIVEN );
1400}
1401
1402
1403// Panel Driving flow end to end shared setter authors length solving drives rect to width
1404BOOST_AUTO_TEST_CASE( SetValueModeDrivingResolvesGeometry )
1405{
1406 BOARD board;
1407 PCB_SHAPE* rect = addRect( board, { 0, 0 }, { 10 * MM, 10 * MM } );
1408
1409 PCB_DIM_ALIGNED* dim = addAlignedDim( board, { 0, 0 }, { 10 * MM, 0 } ); // TL to TR
1410
1411 bindEndpoint( board, dim->m_Uuid, CONSTRAINT_ANCHOR::START,
1412 { rect->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 0 } );
1413 bindEndpoint( board, dim->m_Uuid, CONSTRAINT_ANCHOR::END,
1414 { rect->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 1 } );
1415
1416 MODE_STAGING staging{ board };
1417 PCB_CONSTRAINT* driving =
1418 SetDimensionValueMode( &board, dim, DIM_VALUE_MODE::DRIVING, 15 * MM, std::nullopt,
1419 staging.modify(), staging.add(), staging.remove() );
1420
1421 BOOST_REQUIRE( driving );
1422
1423 CONSTRAINT_DIAGNOSIS diag = ApplyConstraintImmediately( &board, driving );
1424 BOOST_REQUIRE( diag.solved );
1425
1426 const VECTOR2I tl = rectTopLeft( rect );
1427 const VECTOR2I br = rectBottomRight( rect );
1428
1429 BOOST_CHECK_LE( std::abs( ( br.x - tl.x ) - 15 * MM ), 5000 ); // width driven to the value
1430 BOOST_CHECK_LE( std::abs( ( br.y - tl.y ) - 10 * MM ), 5000 ); // height untouched
1431 BOOST_CHECK_LE( tl.EuclideanNorm(), 5000.0 ); // pinned TL corner held
1432
1433 // Dimension endpoints ride the corners value field text reflects driving length not stale measurement
1434 BOOST_CHECK_LE( ( dim->GetStart() - tl ).EuclideanNorm(), 5000.0 );
1435 BOOST_CHECK_LE( ( dim->GetEnd() - VECTOR2I( br.x, tl.y ) ).EuclideanNorm(), 5000.0 );
1436 BOOST_CHECK( dim->GetValueMode() == DIM_VALUE_MODE::DRIVING );
1437}
1438
1439
1440// Property surface the docked panel drives Value Mode and Value exist only for value-bearing dims
1441// Value read-only while Driven validators gate an unbound Driving switch and non-positive length
1442BOOST_AUTO_TEST_CASE( ValueModePropertySurface )
1443{
1444 BOARD board;
1445 PCB_SHAPE* rect = addRect( board, { 0, 0 }, { 10 * MM, 10 * MM } );
1446
1447 PCB_DIM_ALIGNED* dim = addAlignedDim( board, { 0, 0 }, { 10 * MM, 0 } );
1448
1449 bindEndpoint( board, dim->m_Uuid, CONSTRAINT_ANCHOR::START,
1450 { rect->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 0 } );
1451 bindEndpoint( board, dim->m_Uuid, CONSTRAINT_ANCHOR::END,
1452 { rect->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 1 } );
1453
1454 PCB_DIM_LEADER* leader = new PCB_DIM_LEADER( &board );
1455 board.Add( leader );
1456
1458 propMgr.Rebuild();
1459
1460 PROPERTY_BASE* modeProp = propMgr.GetProperty( TYPE_HASH( PCB_DIMENSION_BASE ), _HKI( "Value Mode" ) );
1461 PROPERTY_BASE* valueProp = propMgr.GetProperty( TYPE_HASH( PCB_DIMENSION_BASE ), _HKI( "Value" ) );
1462
1463 BOOST_REQUIRE( modeProp );
1464 BOOST_REQUIRE( valueProp );
1465
1466 BOOST_CHECK( propMgr.IsAvailableFor( TYPE_HASH( PCB_DIM_ALIGNED ), modeProp, dim ) );
1467 BOOST_CHECK( propMgr.IsAvailableFor( TYPE_HASH( PCB_DIM_ALIGNED ), valueProp, dim ) );
1468 BOOST_CHECK( !propMgr.IsAvailableFor( TYPE_HASH( PCB_DIM_LEADER ), modeProp, leader ) );
1469 BOOST_CHECK( !propMgr.IsAvailableFor( TYPE_HASH( PCB_DIM_LEADER ), valueProp, leader ) );
1470
1471 // Driven shows the measured value read-only
1472 BOOST_CHECK( !propMgr.IsWriteableFor( TYPE_HASH( PCB_DIM_ALIGNED ), valueProp, dim ) );
1473 BOOST_CHECK_EQUAL( dim->GetValueFieldText(), dim->GetValueText() );
1474
1475 // A bound dimension may switch to Driving same switch on an unbound one is vetoed
1476 BOOST_CHECK( !modeProp->Validate( wxAny( static_cast<int>( DIM_VALUE_MODE::DRIVING ) ), dim ) );
1477
1478 PCB_DIM_ALIGNED* unbound = addAlignedDim( board, { 0, 20 * MM }, { 10 * MM, 20 * MM } );
1479
1480 BOOST_CHECK( modeProp->Validate( wxAny( static_cast<int>( DIM_VALUE_MODE::DRIVING ) ), unbound ) );
1481 BOOST_CHECK( !modeProp->Validate( wxAny( static_cast<int>( DIM_VALUE_MODE::ARBITRARY ) ), unbound ) );
1482
1483 // Arbitrary makes the value writable and verbatim through the property setter
1484 dim->ChangeValueMode( DIM_VALUE_MODE::ARBITRARY );
1485
1486 BOOST_CHECK( propMgr.IsWriteableFor( TYPE_HASH( PCB_DIM_ALIGNED ), valueProp, dim ) );
1487
1488 dim->ChangeValueFieldText( wxS( "10 mm MAX" ) );
1489 BOOST_CHECK_EQUAL( dim->GetValueFieldText(), wxS( "10 mm MAX" ) );
1490 BOOST_CHECK_EQUAL( dim->GetOverrideText(), wxS( "10 mm MAX" ) );
1491
1492 // Driving gates the value to a positive length and shows the constraint value
1493 dim->ChangeValueMode( DIM_VALUE_MODE::DRIVEN );
1494
1495 MODE_STAGING staging{ board };
1496 SetDimensionValueMode( &board, dim, DIM_VALUE_MODE::DRIVING, 12 * MM, std::nullopt,
1497 staging.modify(), staging.add(), staging.remove() );
1498
1499 BOOST_CHECK( valueProp->Validate( wxAny( wxString( wxS( "0" ) ) ), dim ) );
1500 BOOST_CHECK( valueProp->Validate( wxAny( wxString( wxS( "-3" ) ) ), dim ) );
1501 BOOST_CHECK( valueProp->Validate( wxAny( wxString( wxS( "text" ) ) ), dim ) );
1502 BOOST_CHECK( !valueProp->Validate( wxAny( wxString( wxS( "15" ) ) ), dim ) );
1503
1504 // A Driven dimension accepts anything the setter will ignore so no veto there
1505 BOOST_CHECK( !valueProp->Validate( wxAny( wxString( wxS( "0" ) ) ), unbound ) );
1506}
1507
1508
int index
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 ...
BOARD_CONSTRAINT_DIAGNOSTICS DiagnoseBoardConstraints(BOARD *aBoard)
Diagnose every constraint cluster on the board (validate only – geometry is not changed) and return t...
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.
A base class for any item which can be embedded within the BOARD container class, and therefore insta...
Definition board_item.h:83
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
void Remove(BOARD_ITEM *aBoardItem, REMOVE_MODE aMode=REMOVE_MODE::NORMAL) override
Removes an item from the container.
Definition board.cpp:1503
const KIID m_Uuid
Definition eda_item.h:531
void SetCornerRadius(int aRadius)
SHAPE_POLY_SET & GetPolyShape()
SHAPE_T GetShape() const
Definition eda_shape.h:185
const VECTOR2I & GetEnd() const
Return the ending point of the graphic.
Definition eda_shape.h:240
const VECTOR2I & GetStart() const
Return the starting point of the graphic.
Definition eda_shape.h:190
void SetPolyPoints(const std::vector< VECTOR2I > &aPoints)
void Add(BOARD_ITEM *aItem, ADD_MODE aMode=ADD_MODE::INSERT, bool aSkipConnectivity=false) override
Removes an item from the container.
Definition kiid.h:46
A geometric constraint between board items (issue #2329).
const std::vector< CONSTRAINT_MEMBER > & GetMembers() const
std::optional< double > GetValue() const
bool IsDriving() const
A driving constraint forces its value; a reference (non-driving) one only measures it.
void AddMember(const KIID &aItem, CONSTRAINT_ANCHOR aAnchor=CONSTRAINT_ANCHOR::WHOLE, int aIndex=-1)
void SetDriving(bool aDriving)
std::vector< CONSTRAINT_MEMBER > & Members()
Abstract dimension API.
void Update()
Update the dimension's cached text and geometry.
virtual void SetEnd(const VECTOR2I &aPoint)
virtual void SetStart(const VECTOR2I &aPoint)
void ChangeOverrideText(const wxString &aValue)
void SetOverrideTextEnabled(bool aOverride)
DIM_VALUE_MODE GetValueMode() const
Value mode from board state via DimensionValueMode.
virtual VECTOR2I GetEnd() const
virtual VECTOR2I GetStart() const
The dimension's origin is the first feature point for the dimension.
For better understanding of the points that make a dimension:
A leader is a dimension-like object pointing to a specific point.
An orthogonal dimension is like an aligned dimension, but the extension lines are locked to the X or ...
void SetOrientation(DIR aOrientation)
Set the orientation of the dimension line (so, perpendicular to the feature lines).
A radial dimension indicates either the radius or diameter of an arc or circle.
void Rotate(const VECTOR2I &aRotCentre, const EDA_ANGLE &aAngle) override
Rotate this object.
void SetEnd(const VECTOR2I &aEnd) override
void Move(const VECTOR2I &aMoveVector) override
Move this object.
void SetStart(const VECTOR2I &aStart) override
VALIDATOR_RESULT Validate(const wxAny &&aValue, EDA_ITEM *aItem)
Definition property.h:355
Provide class metadata.Helper macro to map type hashes to names.
bool IsWriteableFor(TYPE_ID aItemClass, PROPERTY_BASE *aProp, INSPECTABLE *aItem)
Checks overriden availability and original availability of a property, returns false if the property ...
static PROPERTY_MANAGER & Instance()
void Rebuild()
Rebuild the list of all registered properties.
PROPERTY_BASE * GetProperty(TYPE_ID aType, const wxString &aProperty) const
Return a property for a specific type.
bool IsAvailableFor(TYPE_ID aItemClass, PROPERTY_BASE *aProp, INSPECTABLE *aItem)
Checks overriden availability and original availability of a property, returns false if the property ...
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
void InsertVertex(int aGlobalIndex, const VECTOR2I &aNewVertex)
Adds a vertex in the globally indexed position aGlobalIndex.
int AddOutline(const SHAPE_LINE_CHAIN &aOutline)
Adds a new outline to the set and returns its index.
void RemoveVertex(int aGlobalIndex)
Delete the aGlobalIndex-th vertex.
const SHAPE_LINE_CHAIN & COutline(int aIndex) const
T EuclideanNorm() const
Compute the Euclidean norm of the vector, which is defined as sqrt(x ** 2 + y ** 2).
Definition vector2d.h:279
DIM_VALUE_MODE DimensionValueMode(BOARD *aBoard, const PCB_DIMENSION_BASE *aDimension)
The value mode aDimension is in, derived from state: a self driving length means Driving,...
void RemapPolygonVertexMembers(BOARD *aBoard, const KIID &aPoly, int aChangedIndex, int aDelta, const std::function< void(BOARD_ITEM *)> &aBeforeModify, const std::function< void(BOARD_ITEM *)> &aBeforeRemove)
Repoint persisted VERTEX constraint members after an outline edit of polygon aPoly inserts or removes...
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...
bool DimensionHasValueMode(const PCB_DIMENSION_BASE *aDimension)
True for dimension types with a measured value aligned orthogonal or radial offering the Driven Drivi...
PCB_CONSTRAINT * FindDimensionLengthConstraint(BOARD *aBoard, const PCB_DIMENSION_BASE *aDimension)
Self FIXED_LENGTH constraint whose members are exactly aDimension START and END or nullptr the drivin...
std::vector< CONSTRAINT_ANCHOR_POINT > ConstraintItemAnchors(const BOARD_ITEM *aItem)
The constraint anchors an item exposes.
std::vector< CONSTRAINT_ANCHOR_POINT > ConstraintShapeAnchors(const PCB_SHAPE *aShape)
Enumerate a shape constraint anchors with positions segment and arc endpoints arc centre circle centr...
std::optional< KIID > SelectRadialDimensionTarget(BOARD *aBoard, const KIID &aDimension, const VECTOR2I &aCenter, const VECTOR2I &aRim, double aMaxDist)
Single circle or arc a radial dimension binds to or std::nullopt.
std::optional< VECTOR2I > ConstraintAnchorPosition(BOARD *aBoard, const CONSTRAINT_MEMBER &aMember)
Current location of a constraint member's anchor (its shape's START/END/CENTER, or a dimension's feat...
std::optional< CONSTRAINT_MEMBER > NearestConstraintAnchor(BOARD *aBoard, const VECTOR2I &aPos, double aMaxDist, const std::vector< CONSTRAINT_MEMBER > &aExclude)
Find the constrainable-item anchor (a shape's segment/arc endpoint or centre, or a dimension's featur...
bool DimensionCanDrive(BOARD *aBoard, const PCB_DIMENSION_BASE *aDimension)
True when Driving mode may be offered for aDimension needs both endpoints bound via DimensionEndpoint...
bool DimensionEndpointsBound(BOARD *aBoard, const PCB_DIMENSION_BASE *aDimension)
True when both of aDimension measured endpoints are bound to anchors that still resolve a coincident ...
PCB_CONSTRAINT * SetDimensionValueMode(BOARD *aBoard, PCB_DIMENSION_BASE *aDimension, DIM_VALUE_MODE aMode, std::optional< int > aDrivingLengthIU, const std::optional< wxString > &aOverrideText, const std::function< void(BOARD_ITEM *)> &aBeforeModify, const std::function< void(BOARD_ITEM *)> &aStageAdd, const std::function< void(BOARD_ITEM *)> &aBeforeRemove)
Apply a value mode transition to aDimension Driving creates or updates the driving length with aDrivi...
static bool empty(const wxTextEntryBase *aCtrl)
@ DEGREES_T
Definition eda_angle.h:31
@ RECTANGLE
Use RECTANGLE instead of RECT to avoid collision in a Windows header.
Definition eda_shape.h:47
Some functions to handle hotkeys in KiCad.
PCB_SHAPE * addSegment(BOARD &aBoard, const VECTOR2I &aStart, const VECTOR2I &aEnd)
PCB_SHAPE * addPoly(BOARD &aBoard, const std::vector< VECTOR2I > &aPoints)
PCB_SHAPE * addCircle(BOARD &aBoard, const VECTOR2I &aCenter, int aRadius)
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
#define _HKI(x)
Definition page_info.cpp:40
CONSTRAINT_ANCHOR
Which feature of a referenced board item participates in a constraint.
@ 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.
@ FIXED_POSITION
A point is locked at its current location.
@ COINCIDENT
Two points are made to coincide.
@ POINT_ON_LINE
A point lies on a segment's supporting line.
@ FIXED_LENGTH
A segment has a driving length value.
#define TYPE_HASH(x)
Definition property.h:74
static std::vector< std::string > split(const std::string &aStr, const std::string &aDelim)
Split the input string into a vector of output strings.
Board-wide diagnostics for the constraint overlay and info bar.
std::vector< KIID > errored
Invalid constraints (member missing, deleted, or of a kind incompatible with the type).
A selectable feature of a shape (a segment endpoint, arc centre, ...) and its location.
The outcome of a constraint solve, in plain data so callers need not know planegcs.
bool solved
Solver reached Success or Converged.
One participant in a constraint: a referenced board item plus the feature of that item that participa...
One of a drawn item feature points bound coincident to an object anchor by draw time auto constrain s...
BOOST_AUTO_TEST_CASE(HorizontalAlignment)
BOOST_AUTO_TEST_SUITE(CadstarPartParser)
BOOST_AUTO_TEST_CASE(DimensionAnchorsPerFamily)
BOOST_REQUIRE(intersection.has_value()==c.ExpectedIntersection.has_value())
BOOST_AUTO_TEST_SUITE_END()
VECTOR3I expected(15, 30, 45)
const SHAPE_LINE_CHAIN chain
SHAPE_CIRCLE circle(c.m_circle_center, c.m_circle_radius)
wxString result
Test unit parsing edge cases and error handling.
BOOST_CHECK_EQUAL(result, "25.4")
void RotatePoint(int *pX, int *pY, const EDA_ANGLE &aAngle)
Calculate the new point of coord coord pX, pY, for a rotation center 0, 0.
Definition trigo.cpp:225
@ PCB_DIM_ALIGNED_T
class PCB_DIM_ALIGNED, a linear dimension (graphic item)
Definition typeinfo.h:95
VECTOR2< int32_t > VECTOR2I
Definition vector2d.h:683