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// Horizontal driving orthogonal fixes x gap only y stays free
309BOOST_AUTO_TEST_CASE( OrthogonalHorizontalDrivingSetsXAxisOnly )
310{
311 BOARD board;
312
313 PCB_DIM_ORTHOGONAL* dim =
314 addOrthogonalDim( board, { 0, 0 }, { 4 * MM, 3 * MM }, PCB_DIM_ORTHOGONAL::DIR::HORIZONTAL );
315
316 PCB_CONSTRAINT* driving = addDrivingLength( board, dim->m_Uuid, 10 * MM );
317
318 ApplyConstraintImmediately( &board, driving );
319
320 BOOST_CHECK_LE( std::abs( ( dim->GetEnd().x - dim->GetStart().x ) - 10 * MM ), 5000 );
321 BOOST_CHECK_LE( std::abs( dim->GetEnd().y - 3 * MM ), 5000 ); // y untouched
322}
323
324
325// Vertical driving orthogonal fixes y gap only x stays free
326BOOST_AUTO_TEST_CASE( OrthogonalVerticalDrivingSetsYAxisOnly )
327{
328 BOARD board;
329
330 PCB_DIM_ORTHOGONAL* dim =
331 addOrthogonalDim( board, { 0, 0 }, { 3 * MM, 4 * MM }, PCB_DIM_ORTHOGONAL::DIR::VERTICAL );
332
333 PCB_CONSTRAINT* driving = addDrivingLength( board, dim->m_Uuid, 10 * MM );
334
335 ApplyConstraintImmediately( &board, driving );
336
337 BOOST_CHECK_LE( std::abs( ( dim->GetEnd().y - dim->GetStart().y ) - 10 * MM ), 5000 );
338 BOOST_CHECK_LE( std::abs( dim->GetEnd().x - 3 * MM ), 5000 ); // x untouched
339}
340
341
342// Radial only binds when one circle or arc holds both centre and rim
343BOOST_AUTO_TEST_CASE( RadialBindingRequiresSameArcCenterAndOutline )
344{
345 BOARD board;
346 PCB_SHAPE* circle = addCircle( board, { 0, 0 }, 5 * MM );
347 const double tol = 10000; // 0.01 mm
348
349 PCB_DIM_RADIAL* dim = addRadialDim( board, { 0, 0 }, { 5 * MM, 0 } );
350
351 std::optional<KIID> hit =
352 SelectRadialDimensionTarget( &board, dim->m_Uuid, { 0, 0 }, { 5 * MM, 0 }, tol );
353 BOOST_REQUIRE( hit.has_value() );
354 BOOST_CHECK( *hit == circle->m_Uuid );
355
356 // Rim 3mm off a 5mm circle no binding
357 BOOST_CHECK( !SelectRadialDimensionTarget( &board, dim->m_Uuid, { 0, 0 }, { 3 * MM, 0 }, tol ) );
358
359 // Centre off circle centre no binding
360 BOOST_CHECK( !SelectRadialDimensionTarget( &board, dim->m_Uuid, { 2 * MM, 0 }, { 7 * MM, 0 }, tol ) );
361}
362
363
364// Centre on one circle rim on another binds neither
365BOOST_AUTO_TEST_CASE( RadialBindingRejectsCrossObjectCenterAndOutline )
366{
367 BOARD board;
368 const double tol = 10000;
369
370 addCircle( board, { 0, 0 }, 5 * MM ); // centre object
371 addCircle( board, { 50 * MM, 0 }, 5 * MM ); // outline object 55mm from first centre
372
373 BOOST_CHECK( !SelectRadialDimensionTarget( &board, KIID(), { 0, 0 }, { 55 * MM, 0 }, tol ) );
374}
375
376
377// Corner to corner dim binds both endpoints as indexed VERTEX anchors in canonical TL to BL order
378BOOST_AUTO_TEST_CASE( DimensionBindsRectCornerToCorner )
379{
380 BOARD board;
381 PCB_SHAPE* rect = addRect( board, { 0, 0 }, { 10 * MM, 10 * MM } );
382
383 KIID fakeDim; // dimension uuid not on the board as during interactive draw
384 std::vector<DIMENSION_ENDPOINT_BINDING> bindings =
385 SelectDimensionEndpointBindings( &board, fakeDim, { 0, 0 }, VECTOR2I( 10 * MM, 10 * MM ), 1000.0 );
386
387 BOOST_REQUIRE_EQUAL( bindings.size(), 2 );
388 BOOST_CHECK( bindings[0].dimAnchor == CONSTRAINT_ANCHOR::START );
389 BOOST_CHECK( bindings[0].target == CONSTRAINT_MEMBER( rect->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 0 ) );
390 BOOST_CHECK( bindings[1].dimAnchor == CONSTRAINT_ANCHOR::END );
391 BOOST_CHECK( bindings[1].target == CONSTRAINT_MEMBER( rect->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 2 ) );
392
393 // Rect stored end before start still enumerates canonically index 0 always TL
394 PCB_SHAPE* reversed = addRect( board, { 30 * MM, 30 * MM }, { 20 * MM, 20 * MM } );
395 std::vector<CONSTRAINT_ANCHOR_POINT> anchors = ConstraintShapeAnchors( reversed );
396
397 BOOST_REQUIRE_EQUAL( anchors.size(), 4 );
398 BOOST_CHECK_EQUAL( anchors[0].index, 0 );
399 BOOST_CHECK_EQUAL( anchors[0].pos, VECTOR2I( 20 * MM, 20 * MM ) );
400 BOOST_CHECK_EQUAL( anchors[2].index, 2 );
401 BOOST_CHECK_EQUAL( anchors[2].pos, VECTOR2I( 30 * MM, 30 * MM ) );
402}
403
404
405// Dim across pentagon vertices binds by outline order VERTEX resolves to own index not vertex 0
406BOOST_AUTO_TEST_CASE( DimensionBindsPolygonVertices )
407{
408 BOARD board;
409
410 std::vector<VECTOR2I> pts = { { 0, 0 },
411 { 20 * MM, 5 * MM },
412 { 15 * MM, 20 * MM },
413 { 5 * MM, 20 * MM },
414 { -5 * MM, 10 * MM } };
415 PCB_SHAPE* poly = addPoly( board, pts );
416
417 KIID fakeDim;
418 std::vector<DIMENSION_ENDPOINT_BINDING> bindings =
419 SelectDimensionEndpointBindings( &board, fakeDim, pts[1], pts[3], 1000.0 );
420
421 BOOST_REQUIRE_EQUAL( bindings.size(), 2 );
422 BOOST_CHECK( bindings[0].dimAnchor == CONSTRAINT_ANCHOR::START );
423 BOOST_CHECK( bindings[0].target == CONSTRAINT_MEMBER( poly->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 1 ) );
424 BOOST_CHECK( bindings[1].dimAnchor == CONSTRAINT_ANCHOR::END );
425 BOOST_CHECK( bindings[1].target == CONSTRAINT_MEMBER( poly->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 3 ) );
426
427 // Anchor lookup must honour vertex index not just the VERTEX tag
428 std::optional<VECTOR2I> pos =
430
431 BOOST_REQUIRE( pos.has_value() );
432 BOOST_CHECK_EQUAL( *pos, pts[2] );
433}
434
435
436// Single polygon reaching both ends preferred over splitting onto a closer segment
437BOOST_AUTO_TEST_CASE( DimensionBindingPrefersSinglePolygonOverSplit )
438{
439 BOARD board;
440
441 PCB_SHAPE* poly = addPoly( board, { { 0, 0 },
442 { 10 * MM, 0 },
443 { 10 * MM, 10 * MM },
444 { 0, 10 * MM } } );
445 PCB_SHAPE* b = addSegment( board, { 2 * MM, 1 * MM }, { 2 * MM, 20 * MM } );
446
447 KIID fakeDim;
448 std::vector<DIMENSION_ENDPOINT_BINDING> bindings =
449 SelectDimensionEndpointBindings( &board, fakeDim, { 0, 0 }, VECTOR2I( 2 * MM, 0 ), 12.0 * MM );
450
451 BOOST_REQUIRE_EQUAL( bindings.size(), 2 );
452 BOOST_CHECK( bindings[0].target == CONSTRAINT_MEMBER( poly->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 0 ) );
453 BOOST_CHECK( bindings[1].target == CONSTRAINT_MEMBER( poly->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 1 ) );
454
455 // Decoy segment never enters binding despite its start being nearest END
456 for( const DIMENSION_ENDPOINT_BINDING& binding : bindings )
457 BOOST_CHECK( binding.target.m_item != b->m_Uuid );
458}
459
460
461// Arc-bearing outline not ingested by adapter so it offers no anchors
462BOOST_AUTO_TEST_CASE( ArcOutlinePolyOffersNoAnchors )
463{
464 BOARD board;
465
466 PCB_SHAPE* poly = new PCB_SHAPE( &board, SHAPE_T::POLY );
467
469 chain.Append( VECTOR2I( 10 * MM, 10 * MM ) );
470 chain.Append( VECTOR2I( 50 * MM, 10 * MM ) );
471 chain.Append( SHAPE_ARC( { 50 * MM, 10 * MM }, { 55 * MM, 25 * MM }, { 50 * MM, 40 * MM }, 0 ) );
472 chain.Append( VECTOR2I( 10 * MM, 40 * MM ) );
473 chain.SetClosed( true );
474
475 poly->GetPolyShape().AddOutline( chain );
476 board.Add( poly );
477
478 BOOST_REQUIRE_GT( poly->GetPolyShape().COutline( 0 ).ArcCount(), 0 );
479
480 BOOST_CHECK( ConstraintShapeAnchors( poly ).empty() );
481 BOOST_CHECK( !NearestConstraintAnchor( &board, VECTOR2I( 10 * MM, 10 * MM ), MM ) );
482}
483
484
485// Driving aligned dim widens rect to entered length height untouched endpoints ride corners
486BOOST_AUTO_TEST_CASE( AlignedDrivingLengthDrivesRectWidth )
487{
488 BOARD board;
489 PCB_SHAPE* rect = addRect( board, { 0, 0 }, { 10 * MM, 10 * MM } );
490
491 PCB_DIM_ALIGNED* dim = addAlignedDim( board, { 0, 0 }, { 10 * MM, 0 } ); // TL to TR
492
493 bindEndpoint( board, dim->m_Uuid, CONSTRAINT_ANCHOR::START,
494 { rect->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 0 } );
495 bindEndpoint( board, dim->m_Uuid, CONSTRAINT_ANCHOR::END,
496 { rect->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 1 } );
497
498 PCB_CONSTRAINT* driving = addDrivingLength( board, dim->m_Uuid, 15 * MM );
499
500 CONSTRAINT_DIAGNOSIS diag = ApplyConstraintImmediately( &board, driving );
501 BOOST_REQUIRE( diag.solved );
502
503 const VECTOR2I tl = rectTopLeft( rect );
504 const VECTOR2I br = rectBottomRight( rect );
505
506 BOOST_CHECK_LE( std::abs( ( br.x - tl.x ) - 15 * MM ), 5000 ); // width driven to the value
507 BOOST_CHECK_LE( std::abs( ( br.y - tl.y ) - 10 * MM ), 5000 ); // height untouched
508 BOOST_CHECK_LE( tl.EuclideanNorm(), 5000.0 ); // pinned TL corner held
509
510 // All four enumerated corners agree with solved extremes rectangle stayed coherent
511 std::vector<CONSTRAINT_ANCHOR_POINT> corners = ConstraintShapeAnchors( rect );
512 BOOST_REQUIRE_EQUAL( corners.size(), 4 );
513 BOOST_CHECK_EQUAL( corners[0].pos, tl );
514 BOOST_CHECK_EQUAL( corners[1].pos, VECTOR2I( br.x, tl.y ) );
515 BOOST_CHECK_EQUAL( corners[2].pos, br );
516 BOOST_CHECK_EQUAL( corners[3].pos, VECTOR2I( tl.x, br.y ) );
517
518 // Dimension endpoints followed corners through coincident bindings
519 BOOST_CHECK_LE( ( dim->GetStart() - corners[0].pos ).EuclideanNorm(), 5000.0 );
520 BOOST_CHECK_LE( ( dim->GetEnd() - corners[1].pos ).EuclideanNorm(), 5000.0 );
521}
522
523
524// Driving horizontal orthogonal on pentagon vertices forces x separation y and unbound vertices held
525BOOST_AUTO_TEST_CASE( OrthogonalDrivingLengthDrivesPolygonAxis )
526{
527 BOARD board;
528
529 std::vector<VECTOR2I> pts = { { 0, 0 },
530 { 20 * MM, 5 * MM },
531 { 15 * MM, 20 * MM },
532 { 5 * MM, 20 * MM },
533 { -5 * MM, 10 * MM } };
534 PCB_SHAPE* poly = addPoly( board, pts );
535
536 PCB_DIM_ORTHOGONAL* dim =
537 addOrthogonalDim( board, pts[1], pts[3], PCB_DIM_ORTHOGONAL::DIR::HORIZONTAL );
538
539 bindEndpoint( board, dim->m_Uuid, CONSTRAINT_ANCHOR::START,
540 { poly->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 1 } );
541 bindEndpoint( board, dim->m_Uuid, CONSTRAINT_ANCHOR::END,
542 { poly->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 3 } );
543
544 PCB_CONSTRAINT* driving = addDrivingLength( board, dim->m_Uuid, 20 * MM );
545
546 CONSTRAINT_DIAGNOSIS diag = ApplyConstraintImmediately( &board, driving );
547 BOOST_REQUIRE( diag.solved );
548
549 const SHAPE_LINE_CHAIN& outline = poly->GetPolyShape().COutline( 0 );
550
551 BOOST_CHECK_LE( std::abs( std::abs( outline.CPoint( 3 ).x - outline.CPoint( 1 ).x ) - 20 * MM ), 5000 );
552 BOOST_CHECK_LE( std::abs( outline.CPoint( 1 ).y - pts[1].y ), 5000 ); // y is not the driven axis
553 BOOST_CHECK_LE( std::abs( outline.CPoint( 3 ).y - pts[3].y ), 5000 );
554
555 for( int i : { 0, 2, 4 } )
556 BOOST_CHECK_LE( ( outline.CPoint( i ) - pts[i] ).EuclideanNorm(), 5000.0 ); // unbound vertices held
557
558 // Dimension endpoints followed the vertices they bind
559 BOOST_CHECK_LE( ( dim->GetStart() - outline.CPoint( 1 ) ).EuclideanNorm(), 5000.0 );
560 BOOST_CHECK_LE( ( dim->GetEnd() - outline.CPoint( 3 ) ).EuclideanNorm(), 5000.0 );
561}
562
563
564// Driven reference dim follows rect resize through move-tool re-solve length re-measures
565BOOST_AUTO_TEST_CASE( DrivenDimensionRemeasuresAfterRectResize )
566{
567 BOARD board;
568 PCB_SHAPE* rect = addRect( board, { 0, 0 }, { 10 * MM, 10 * MM } );
569
570 PCB_DIM_ALIGNED* dim = addAlignedDim( board, { 0, 0 }, { 10 * MM, 0 } );
571
572 bindEndpoint( board, dim->m_Uuid, CONSTRAINT_ANCHOR::START,
573 { rect->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 0 } );
574 bindEndpoint( board, dim->m_Uuid, CONSTRAINT_ANCHOR::END,
575 { rect->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 1 } );
576
577 // Driven mode carries a non-driving length that only mirrors measured geometry
578 PCB_CONSTRAINT* reference = addConstraint(
580 { { dim->m_Uuid, CONSTRAINT_ANCHOR::START }, { dim->m_Uuid, CONSTRAINT_ANCHOR::END } },
581 10.0 * MM );
582 reference->SetDriving( false );
583
584 rect->SetEnd( { 15 * MM, 10 * MM } ); // widen by 5mm edited outside the solver
585 ReSolveShapeClusters( &board, { rect } );
586
587 // Resize survived re-solve dimension endpoints tracked corners
588 BOOST_CHECK_LE( ( rectBottomRight( rect ) - VECTOR2I( 15 * MM, 10 * MM ) ).EuclideanNorm(), 5000.0 );
589 BOOST_CHECK_LE( ( dim->GetStart() - VECTOR2I( 0, 0 ) ).EuclideanNorm(), 5000.0 );
590 BOOST_CHECK_LE( ( dim->GetEnd() - VECTOR2I( 15 * MM, 0 ) ).EuclideanNorm(), 5000.0 );
591
592 BOOST_REQUIRE( reference->GetValue().has_value() );
593 BOOST_CHECK_LE( std::abs( *reference->GetValue() - 15.0 * MM ), 5000.0 );
594}
595
596
597// Properties edit is authoritative hold-edited re-solve keeps typed width and pulls bound items along
598BOOST_AUTO_TEST_CASE( HoldingEditedRectResizeFollowsBindings )
599{
600 BOARD board;
601 PCB_SHAPE* rect = addRect( board, { 0, 0 }, { 10 * MM, 10 * MM } );
602 PCB_SHAPE* seg = addSegment( board, { 10 * MM, 0 }, { 20 * MM, -5 * MM } );
603
605 { { seg->m_Uuid, CONSTRAINT_ANCHOR::START }, { rect->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 1 } } );
606
607 PCB_DIM_ALIGNED* dim = addAlignedDim( board, { 0, 0 }, { 10 * MM, 0 } ); // TL to TR
608
609 bindEndpoint( board, dim->m_Uuid, CONSTRAINT_ANCHOR::START,
610 { rect->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 0 } );
611 bindEndpoint( board, dim->m_Uuid, CONSTRAINT_ANCHOR::END,
612 { rect->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 1 } );
613
614 PCB_CONSTRAINT* reference = addConstraint(
616 { { dim->m_Uuid, CONSTRAINT_ANCHOR::START }, { dim->m_Uuid, CONSTRAINT_ANCHOR::END } },
617 10.0 * MM );
618 reference->SetDriving( false );
619
620 rect->SetEnd( { 15 * MM, 10 * MM } ); // widen by 5mm as a typed width edit would
621
622 std::vector<PCB_SHAPE*> modified;
623 ReSolveShapeClustersHoldingEdited( &board, { rect }, &modified );
624
625 // Edited geometry survived corner-bound segment start followed TR corner
626 BOOST_CHECK_LE( ( rectBottomRight( rect ) - VECTOR2I( 15 * MM, 10 * MM ) ).EuclideanNorm(), 5000.0 );
627 BOOST_CHECK_LE( ( rectTopLeft( rect ) - VECTOR2I( 0, 0 ) ).EuclideanNorm(), 5000.0 );
628 BOOST_CHECK_LE( ( seg->GetStart() - VECTOR2I( 15 * MM, 0 ) ).EuclideanNorm(), 5000.0 );
629 BOOST_CHECK( std::find( modified.begin(), modified.end(), seg ) != modified.end() );
630
631 // Both dimension endpoints ride the corners they bind Driven value re-measured
632 BOOST_CHECK_LE( ( dim->GetStart() - VECTOR2I( 0, 0 ) ).EuclideanNorm(), 5000.0 );
633 BOOST_CHECK_LE( ( dim->GetEnd() - VECTOR2I( 15 * MM, 0 ) ).EuclideanNorm(), 5000.0 );
634
635 BOOST_REQUIRE( reference->GetValue().has_value() );
636 BOOST_CHECK_LE( std::abs( *reference->GetValue() - 15.0 * MM ), 5000.0 );
637}
638
639
640// Stale vertex index left unmapped by Build does not poison solve Driving gate reports undrivable
641BOOST_AUTO_TEST_CASE( StaleVertexBindingDegradesAndGatesDriving )
642{
643 BOARD board;
644
645 std::vector<VECTOR2I> pts = { { 0, 0 },
646 { 20 * MM, 5 * MM },
647 { 15 * MM, 20 * MM },
648 { 5 * MM, 20 * MM },
649 { -5 * MM, 10 * MM } };
650 PCB_SHAPE* poly = addPoly( board, pts );
651
652 PCB_DIM_ALIGNED* dim = addAlignedDim( board, pts[1], pts[4] );
653
654 PCB_CONSTRAINT* startBinding = bindEndpoint( board, dim->m_Uuid, CONSTRAINT_ANCHOR::START,
655 { poly->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 1 } );
656 PCB_CONSTRAINT* endBinding = bindEndpoint( board, dim->m_Uuid, CONSTRAINT_ANCHOR::END,
657 { poly->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 4 } );
658
659 BOOST_CHECK( DimensionEndpointsBound( &board, dim ) );
660
661 poly->SetPolyPoints( { pts[0], pts[1], pts[2] } ); // vertex 4 no longer exists
662
663 std::vector<PCB_CONSTRAINT*> constraints( board.Constraints().begin(), board.Constraints().end() );
665
666 BOOST_REQUIRE( adapter.Build( { poly }, constraints, nullptr, { dim } ) );
667
668 const std::vector<KIID>& unmapped = adapter.UnmappedConstraints();
669 BOOST_CHECK( std::find( unmapped.begin(), unmapped.end(), endBinding->m_Uuid ) != unmapped.end() );
670 BOOST_CHECK( std::find( unmapped.begin(), unmapped.end(), startBinding->m_Uuid ) == unmapped.end() );
671
672 // Surviving binding still solves settled cluster stays put
673 BOOST_CHECK( adapter.Solve() );
674 BOOST_CHECK( adapter.Apply().empty() );
675
676 const SHAPE_LINE_CHAIN& outline = poly->GetPolyShape().COutline( 0 );
677
678 for( int i : { 0, 1, 2 } )
679 BOOST_CHECK_EQUAL( outline.CPoint( i ), pts[i] );
680
681 // Dialog offers Driving only through this predicate stale binding must fail it
682 BOOST_CHECK( !DimensionEndpointsBound( &board, dim ) );
683
684 // Restoring outline revives binding gate discriminates stale index from structurally broken
685 poly->SetPolyPoints( pts );
686 BOOST_CHECK( DimensionEndpointsBound( &board, dim ) );
687
688 // Deleting the bound item entirely fails gate through deleted-target leg
689 board.Remove( poly );
690 delete poly;
691 BOOST_CHECK( !DimensionEndpointsBound( &board, dim ) );
692}
693
694
695// Radial Driving gate needs centre and rim on one live circle or arc split legs never offer Driving
696BOOST_AUTO_TEST_CASE( RadialDrivingGateRequiresSingleMappableTarget )
697{
698 BOARD board;
699 PCB_SHAPE* circleA = addCircle( board, { 0, 0 }, 5 * MM );
700 PCB_SHAPE* circleB = addCircle( board, { 50 * MM, 0 }, 5 * MM );
701 PCB_SHAPE* seg = addSegment( board, { 0, 30 * MM }, { 10 * MM, 30 * MM } );
702
703 // Centre and rim both on circle A exactly as authored gates drivable
704 PCB_DIM_RADIAL* bound = addRadialDim( board, { 0, 0 }, { 5 * MM, 0 } );
706 { { bound->m_Uuid, CONSTRAINT_ANCHOR::START }, { circleA->m_Uuid, CONSTRAINT_ANCHOR::CENTER } } );
708 { { bound->m_Uuid, CONSTRAINT_ANCHOR::END }, { circleA->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } } );
709
710 BOOST_CHECK( DimensionEndpointsBound( &board, bound ) );
711
712 // Centre on A rim on B is not a radius of either object
713 PCB_DIM_RADIAL* split = addRadialDim( board, { 0, 0 }, { 55 * MM, 0 } );
715 { { split->m_Uuid, CONSTRAINT_ANCHOR::START }, { circleA->m_Uuid, CONSTRAINT_ANCHOR::CENTER } } );
717 { { split->m_Uuid, CONSTRAINT_ANCHOR::END }, { circleB->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } } );
718
719 BOOST_CHECK( !DimensionEndpointsBound( &board, split ) );
720
721 // Both legs share a segment but a segment carries no radius to drive
722 PCB_DIM_RADIAL* onSegment = addRadialDim( board, { 5 * MM, 30 * MM }, { 10 * MM, 30 * MM } );
724 { { onSegment->m_Uuid, CONSTRAINT_ANCHOR::START }, { seg->m_Uuid, CONSTRAINT_ANCHOR::CENTER } } );
726 { { onSegment->m_Uuid, CONSTRAINT_ANCHOR::END }, { seg->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } } );
727
728 BOOST_CHECK( !DimensionEndpointsBound( &board, onSegment ) );
729}
730
731
732// Rect stored end before start still binds VERTEX 0 to physical top-left not stored-start corner
733BOOST_AUTO_TEST_CASE( SwappedRectVertexZeroIsPhysicalTopLeft )
734{
735 BOARD board;
736 PCB_SHAPE* rect = addRect( board, { 10 * MM, 10 * MM }, { 0, 0 } ); // start is BR
737
738 PCB_SHAPE* seg = addSegment( board, { 20 * MM, 20 * MM }, { 2 * MM, 1 * MM } );
739
742 { { seg->m_Uuid, CONSTRAINT_ANCHOR::END }, { rect->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 0 } } );
743
745 BOOST_REQUIRE( diag.solved );
746
747 // First member the segment end is pinned rect physical top-left snapped onto it opposite corner held
748 BOOST_CHECK_LE( ( seg->GetEnd() - VECTOR2I( 2 * MM, 1 * MM ) ).EuclideanNorm(), 5000.0 );
749 BOOST_CHECK_LE( ( rectTopLeft( rect ) - VECTOR2I( 2 * MM, 1 * MM ) ).EuclideanNorm(), 5000.0 );
750 BOOST_CHECK_LE( ( rectBottomRight( rect ) - VECTOR2I( 10 * MM, 10 * MM ) ).EuclideanNorm(), 5000.0 );
751}
752
753
754// Insert ahead of bound vertex shifts persisted index member keeps its corner footprint constraints remap too
755BOOST_AUTO_TEST_CASE( VertexInsertShiftsMembersPastInsertionPoint )
756{
757 BOARD board;
758
759 std::vector<VECTOR2I> pts = { { 0, 0 },
760 { 20 * MM, 5 * MM },
761 { 15 * MM, 20 * MM },
762 { 5 * MM, 20 * MM },
763 { -5 * MM, 10 * MM } };
764 PCB_SHAPE* poly = addPoly( board, pts );
765 PCB_SHAPE* seg = addSegment( board, pts[3], { 30 * MM, 0 } );
766
769 { poly->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 3 } } );
770
771 FOOTPRINT* fp = new FOOTPRINT( &board );
772 board.Add( fp );
773
775 onFootprint->AddMember( poly->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 4 );
776 fp->Add( onFootprint );
777
778 // Mutate outline the way addCorner does then remap with same edit description
779 poly->GetPolyShape().InsertVertex( 2, ( pts[1] + pts[2] ) / 2 );
780
781 std::vector<BOARD_ITEM*> modified;
782 std::vector<BOARD_ITEM*> removed;
783
784 RemapPolygonVertexMembers( &board, poly->m_Uuid, 2, 1,
785 [&]( BOARD_ITEM* aItem ) { modified.push_back( aItem ); },
786 [&]( BOARD_ITEM* aItem ) { removed.push_back( aItem ); } );
787
788 BOOST_CHECK( onBoard->GetMembers()[1] == CONSTRAINT_MEMBER( poly->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 4 ) );
789 BOOST_CHECK( onFootprint->GetMembers()[0] == CONSTRAINT_MEMBER( poly->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 5 ) );
790
791 // Remapped members resolve to the corners they were authored on
792 std::optional<VECTOR2I> boardPos = ConstraintAnchorPosition( &board, onBoard->GetMembers()[1] );
793 std::optional<VECTOR2I> fpPos = ConstraintAnchorPosition( &board, onFootprint->GetMembers()[0] );
794
795 BOOST_REQUIRE( boardPos.has_value() );
796 BOOST_CHECK_EQUAL( *boardPos, pts[3] );
797 BOOST_REQUIRE( fpPos.has_value() );
798 BOOST_CHECK_EQUAL( *fpPos, pts[4] );
799
800 // Both re-indexed constraints were staged an insertion never retires one
801 BOOST_CHECK_EQUAL( modified.size(), 2 );
802 BOOST_CHECK( removed.empty() );
803}
804
805
806// Delete vertex below bound one shifts index down still same physical corner
807BOOST_AUTO_TEST_CASE( VertexDeleteBelowShiftsMemberDown )
808{
809 BOARD board;
810
811 std::vector<VECTOR2I> pts = { { 0, 0 },
812 { 20 * MM, 5 * MM },
813 { 15 * MM, 20 * MM },
814 { 5 * MM, 20 * MM },
815 { -5 * MM, 10 * MM } };
816 PCB_SHAPE* poly = addPoly( board, pts );
817 PCB_SHAPE* seg = addSegment( board, pts[3], { 30 * MM, 0 } );
818
821 { poly->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 3 } } );
822
823 // Mutate outline the way removeCorner does
824 poly->GetPolyShape().RemoveVertex( 1 );
825
826 std::vector<BOARD_ITEM*> removed;
827
828 RemapPolygonVertexMembers( &board, poly->m_Uuid, 1, -1,
829 []( BOARD_ITEM* ) {},
830 [&]( BOARD_ITEM* aItem ) { removed.push_back( aItem ); } );
831
832 BOOST_CHECK( c->GetMembers()[1] == CONSTRAINT_MEMBER( poly->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 2 ) );
833
834 std::optional<VECTOR2I> pos = ConstraintAnchorPosition( &board, c->GetMembers()[1] );
835
836 BOOST_REQUIRE( pos.has_value() );
837 BOOST_CHECK_EQUAL( *pos, pts[3] );
838 BOOST_CHECK( removed.empty() );
839}
840
841
842// Delete bound vertex itself leaves nothing to express whole constraint staged for removal
843BOOST_AUTO_TEST_CASE( VertexDeleteOfBoundVertexRetiresConstraint )
844{
845 BOARD board;
846
847 std::vector<VECTOR2I> pts = { { 0, 0 },
848 { 20 * MM, 5 * MM },
849 { 15 * MM, 20 * MM },
850 { 5 * MM, 20 * MM },
851 { -5 * MM, 10 * MM } };
852 PCB_SHAPE* poly = addPoly( board, pts );
853 PCB_SHAPE* seg = addSegment( board, pts[3], { 30 * MM, 0 } );
854
857 { poly->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 3 } } );
858
859 poly->GetPolyShape().RemoveVertex( 3 );
860
861 std::vector<BOARD_ITEM*> modified;
862 std::vector<BOARD_ITEM*> removed;
863
864 RemapPolygonVertexMembers( &board, poly->m_Uuid, 3, -1,
865 [&]( BOARD_ITEM* aItem ) { modified.push_back( aItem ); },
866 [&]( BOARD_ITEM* aItem ) { removed.push_back( aItem ); } );
867
868 // Retiring constraint staged whole never edited undo keeps authored members
869 BOOST_REQUIRE_EQUAL( removed.size(), 1 );
870 BOOST_CHECK( removed[0] == c );
871 BOOST_CHECK( modified.empty() );
872 BOOST_CHECK( c->GetMembers()[1] == CONSTRAINT_MEMBER( poly->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 3 ) );
873
874 // Complete the edit the way the commit would
875 board.Remove( c );
876 delete c;
877
878 BOOST_CHECK( board.Constraints().empty() );
879}
880
881
882// Chamfer composes insert then delete order is load-bearing reversed order nets wrong shift
883BOOST_AUTO_TEST_CASE( ChamferRemapOrderNetsOneHigherPastTheCorner )
884{
885 BOARD board;
886
887 std::vector<VECTOR2I> pts = { { 0, 0 },
888 { 20 * MM, 5 * MM },
889 { 15 * MM, 20 * MM },
890 { 5 * MM, 20 * MM },
891 { -5 * MM, 10 * MM } };
892 PCB_SHAPE* poly = addPoly( board, pts );
893
894 const int k = 2;
895
897 { { poly->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, k - 1 } } );
899 { { poly->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, k } } );
901 { { poly->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, k + 1 } } );
903 { { poly->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, k },
904 { poly->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, k + 2 } } );
905
906 // Mutate outline the way chamferCorner does replacing corner k with two chamfer points
907 VECTOR2I a = pts[k] + ( pts[k - 1] - pts[k] ) / 4;
908 VECTOR2I b = pts[k] + ( pts[k + 1] - pts[k] ) / 4;
909
910 poly->GetPolyShape().RemoveVertex( k );
911 poly->GetPolyShape().InsertVertex( k, b );
912 poly->GetPolyShape().InsertVertex( k, a );
913
914 std::vector<BOARD_ITEM*> modified;
915 std::vector<BOARD_ITEM*> removed;
916
917 auto modify = [&]( BOARD_ITEM* aItem ) { modified.push_back( aItem ); };
918 auto remove = [&]( BOARD_ITEM* aItem ) { removed.push_back( aItem ); };
919
920 RemapPolygonVertexMembers( &board, poly->m_Uuid, k + 1, 2, modify, remove );
921 RemapPolygonVertexMembers( &board, poly->m_Uuid, k, -1, modify, remove );
922
923 BOOST_CHECK( below->GetMembers()[0] == CONSTRAINT_MEMBER( poly->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, k - 1 ) );
924 BOOST_CHECK( above->GetMembers()[0] == CONSTRAINT_MEMBER( poly->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, k + 2 ) );
925
926 // Netted member still resolves to its own corner one past the chamfer points
927 std::optional<VECTOR2I> pos = ConstraintAnchorPosition( &board, above->GetMembers()[0] );
928
929 BOOST_REQUIRE( pos.has_value() );
930 BOOST_CHECK_EQUAL( *pos, pts[k + 1] );
931
932 // Both constraints on the chamfered corner retire mixed one was Modify-staged first by insert pass
933 BOOST_REQUIRE_EQUAL( removed.size(), 2 );
934 BOOST_CHECK_EQUAL( std::ranges::count( removed, onCorner ), 1 );
935 BOOST_CHECK_EQUAL( std::ranges::count( removed, both ), 1 );
936 BOOST_CHECK_EQUAL( std::ranges::count( modified, both ), 1 );
937 BOOST_CHECK_EQUAL( std::ranges::count( modified, onCorner ), 0 );
938 BOOST_CHECK_EQUAL( std::ranges::count( modified, below ), 0 );
939 BOOST_CHECK( both->GetMembers()[0] == CONSTRAINT_MEMBER( poly->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, k ) );
940 BOOST_CHECK( both->GetMembers()[1] == CONSTRAINT_MEMBER( poly->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, k + 4 ) );
941}
942
943
944// Remapping scoped to edited polygon other shapes and lower indices never move
945BOOST_AUTO_TEST_CASE( VertexRemapLeavesOtherShapesAndLowerIndicesAlone )
946{
947 BOARD board;
948
949 std::vector<VECTOR2I> pts = { { 0, 0 },
950 { 20 * MM, 5 * MM },
951 { 15 * MM, 20 * MM },
952 { 5 * MM, 20 * MM },
953 { -5 * MM, 10 * MM } };
954 PCB_SHAPE* edited = addPoly( board, pts );
955
956 std::vector<VECTOR2I> otherPts = { { 40 * MM, 0 }, { 60 * MM, 0 }, { 60 * MM, 20 * MM }, { 40 * MM, 20 * MM } };
957 PCB_SHAPE* other = addPoly( board, otherPts );
958 PCB_SHAPE* rect = addRect( board, { 0, 40 * MM }, { 10 * MM, 50 * MM } );
959 PCB_SHAPE* seg = addSegment( board, { 0, 60 * MM }, { 10 * MM, 60 * MM } );
960
962 { { other->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 3 } } );
964 { { seg->m_Uuid, CONSTRAINT_ANCHOR::START },
965 { rect->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 2 } } );
967 { { seg->m_Uuid, CONSTRAINT_ANCHOR::END },
968 { edited->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 1 } } );
969
970 edited->GetPolyShape().InsertVertex( 2, ( pts[1] + pts[2] ) / 2 );
971
972 std::vector<BOARD_ITEM*> modified;
973 std::vector<BOARD_ITEM*> removed;
974
975 RemapPolygonVertexMembers( &board, edited->m_Uuid, 2, 1,
976 [&]( BOARD_ITEM* aItem ) { modified.push_back( aItem ); },
977 [&]( BOARD_ITEM* aItem ) { removed.push_back( aItem ); } );
978
979 BOOST_CHECK( onOther->GetMembers()[0] == CONSTRAINT_MEMBER( other->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 3 ) );
980 BOOST_CHECK( onRect->GetMembers()[1] == CONSTRAINT_MEMBER( rect->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 2 ) );
981 BOOST_CHECK( below->GetMembers()[1] == CONSTRAINT_MEMBER( edited->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 1 ) );
982
983 // No constraint needed re-indexing so nothing was staged
984 BOOST_CHECK( modified.empty() );
985 BOOST_CHECK( removed.empty() );
986}
987
988
989// Cardinal rotation keeps RECTANGLE corner roles re-canonicalize VERTEX 0 resolves to new top-left
990BOOST_AUTO_TEST_CASE( CardinalRotationRecanonicalizesRectRoles )
991{
992 BOARD board;
993 PCB_SHAPE* rect = addRect( board, { 2 * MM, 1 * MM }, { 12 * MM, 5 * MM } );
994
995 std::vector<CONSTRAINT_ANCHOR_POINT> before = ConstraintShapeAnchors( rect );
996 BOOST_REQUIRE_EQUAL( before.size(), 4 );
997
998 const VECTOR2I pivot( 0, 0 );
999 const EDA_ANGLE angle( 90, DEGREES_T );
1000
1001 rect->Rotate( pivot, angle );
1003
1004 // Physical corners after the turn from pre-rotation enumeration
1005 std::vector<VECTOR2I> rotated;
1006
1007 for( const CONSTRAINT_ANCHOR_POINT& a : before )
1008 {
1009 VECTOR2I p = a.pos;
1010 RotatePoint( p, pivot, angle );
1011 rotated.push_back( p );
1012 }
1013
1014 VECTOR2I tl = rotated[0];
1015 VECTOR2I br = rotated[0];
1016
1017 for( const VECTOR2I& p : rotated )
1018 {
1019 tl.x = std::min( tl.x, p.x );
1020 tl.y = std::min( tl.y, p.y );
1021 br.x = std::max( br.x, p.x );
1022 br.y = std::max( br.y, p.y );
1023 }
1024
1025 std::vector<CONSTRAINT_ANCHOR_POINT> after = ConstraintShapeAnchors( rect );
1026 BOOST_REQUIRE_EQUAL( after.size(), 4 );
1027 BOOST_CHECK_EQUAL( after[0].pos, tl );
1028 BOOST_CHECK_EQUAL( after[1].pos, VECTOR2I( br.x, tl.y ) );
1029 BOOST_CHECK_EQUAL( after[2].pos, br );
1030 BOOST_CHECK_EQUAL( after[3].pos, VECTOR2I( tl.x, br.y ) );
1031
1032 // Role semantics not corner tracking member resolves to new top-left a different physical corner
1033 std::optional<VECTOR2I> v0 =
1035
1036 BOOST_REQUIRE( v0.has_value() );
1037 BOOST_CHECK_EQUAL( *v0, tl );
1038 BOOST_CHECK( *v0 != rotated[0] );
1039}
1040
1041
1042// Non-cardinal rotation converts rect to a 4-vertex POLY under same KIID canonical order keeps bindings
1043BOOST_AUTO_TEST_CASE( NonCardinalRotationKeepsRectVertexBindings )
1044{
1045 BOARD board;
1046
1047 auto checkRect = [&]( PCB_SHAPE* rect )
1048 {
1049 std::vector<CONSTRAINT_ANCHOR_POINT> before = ConstraintShapeAnchors( rect );
1050 BOOST_REQUIRE_EQUAL( before.size(), 4 );
1051
1052 PCB_DIM_ALIGNED* dim = addAlignedDim( board, before[0].pos, before[1].pos );
1053
1054 bindEndpoint( board, dim->m_Uuid, CONSTRAINT_ANCHOR::START,
1055 { rect->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 0 } );
1056 bindEndpoint( board, dim->m_Uuid, CONSTRAINT_ANCHOR::END,
1057 { rect->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 1 } );
1058
1059 BOOST_CHECK( DimensionEndpointsBound( &board, dim ) );
1060
1061 const VECTOR2I pivot = rect->GetCenter();
1062 const EDA_ANGLE angle( 30, DEGREES_T );
1063
1064 rect->Rotate( pivot, angle );
1065
1066 BOOST_REQUIRE( rect->GetShape() == SHAPE_T::POLY );
1067
1068 const SHAPE_LINE_CHAIN& outline = rect->GetPolyShape().COutline( 0 );
1069 BOOST_REQUIRE_EQUAL( outline.PointCount(), 4 );
1070 BOOST_CHECK_EQUAL( outline.ArcCount(), 0 );
1071
1072 for( int i = 0; i < 4; ++i )
1073 {
1074 VECTOR2I expected = before[i].pos;
1075 RotatePoint( expected, pivot, angle );
1076
1077 std::optional<VECTOR2I> pos =
1078 ConstraintAnchorPosition( &board, { rect->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, i } );
1079
1080 BOOST_REQUIRE( pos.has_value() );
1081 BOOST_CHECK_LE( ( *pos - expected ).EuclideanNorm(), 2.0 ); // integer rounding only
1082 }
1083
1084 BOOST_CHECK( DimensionEndpointsBound( &board, dim ) );
1085 };
1086
1087 // One rect stored start at TL one end before start conversion normalizes both to same order
1088 checkRect( addRect( board, { 0, 0 }, { 10 * MM, 4 * MM } ) );
1089 checkRect( addRect( board, { 40 * MM, 4 * MM }, { 30 * MM, 0 } ) );
1090}
1091
1092
1093// Rounded rect rotated off-cardinal becomes arc-bearing outline gate rejects members degrade to unmapped
1094BOOST_AUTO_TEST_CASE( RoundedRectNonCardinalRotationDegradesToUnmapped )
1095{
1096 BOARD board;
1097 PCB_SHAPE* rect = addRect( board, { 0, 0 }, { 10 * MM, 6 * MM } );
1098 rect->SetCornerRadius( 1 * MM );
1099
1100 std::vector<CONSTRAINT_ANCHOR_POINT> before = ConstraintShapeAnchors( rect );
1101 BOOST_REQUIRE_EQUAL( before.size(), 4 );
1102
1103 PCB_DIM_ALIGNED* dim = addAlignedDim( board, before[0].pos, before[1].pos );
1104
1105 PCB_CONSTRAINT* startBinding = bindEndpoint( board, dim->m_Uuid, CONSTRAINT_ANCHOR::START,
1106 { rect->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 0 } );
1107 PCB_CONSTRAINT* endBinding = bindEndpoint( board, dim->m_Uuid, CONSTRAINT_ANCHOR::END,
1108 { rect->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 1 } );
1109
1110 BOOST_CHECK( DimensionEndpointsBound( &board, dim ) );
1111
1112 rect->Rotate( { 0, 0 }, EDA_ANGLE( 30, DEGREES_T ) );
1113
1114 BOOST_REQUIRE( rect->GetShape() == SHAPE_T::POLY );
1115 BOOST_CHECK_GT( rect->GetPolyShape().COutline( 0 ).ArcCount(), 0 );
1116
1117 // No anchors no resolution no Driving offer
1118 BOOST_CHECK( ConstraintShapeAnchors( rect ).empty() );
1119 BOOST_CHECK( !ConstraintAnchorPosition( &board, { rect->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 0 } ) );
1120 BOOST_CHECK( !DimensionEndpointsBound( &board, dim ) );
1121
1122 // Adapter leaves both bindings unmapped rather than poisoning the build
1123 std::vector<PCB_CONSTRAINT*> constraints( board.Constraints().begin(), board.Constraints().end() );
1125
1126 BOOST_REQUIRE( adapter.Build( { rect }, constraints, nullptr, { dim } ) );
1127
1128 const std::vector<KIID>& unmapped = adapter.UnmappedConstraints();
1129 BOOST_CHECK( std::find( unmapped.begin(), unmapped.end(), startBinding->m_Uuid ) != unmapped.end() );
1130 BOOST_CHECK( std::find( unmapped.begin(), unmapped.end(), endBinding->m_Uuid ) != unmapped.end() );
1131}
1132
1133
1134// Driving gate scans footprint-parented constraints too stale footprint binding revokes it
1135BOOST_AUTO_TEST_CASE( FootprintParentedBindingGatesDriving )
1136{
1137 BOARD board;
1138 FOOTPRINT* fp = new FOOTPRINT( &board );
1139 board.Add( fp );
1140
1141 PCB_SHAPE* rect = new PCB_SHAPE( fp, SHAPE_T::RECTANGLE );
1142 rect->SetStart( { 0, 0 } );
1143 rect->SetEnd( { 10 * MM, 10 * MM } );
1144 fp->Add( rect );
1145
1146 PCB_DIM_ALIGNED* dim = addAlignedDim( board, { 0, 0 }, { 10 * MM, 0 } );
1147
1148 auto bindOnFootprint = [&]( CONSTRAINT_ANCHOR aDimAnchor, int aCorner )
1149 {
1151 c->AddMember( dim->m_Uuid, aDimAnchor );
1152 c->AddMember( rect->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, aCorner );
1153 fp->Add( c );
1154 return c;
1155 };
1156
1157 bindOnFootprint( CONSTRAINT_ANCHOR::START, 0 );
1158 PCB_CONSTRAINT* endBinding = bindOnFootprint( CONSTRAINT_ANCHOR::END, 1 );
1159
1160 // Nothing lives at board level drivable verdict can only come from footprint scan
1161 BOOST_CHECK( board.Constraints().empty() );
1162 BOOST_CHECK( DimensionEndpointsBound( &board, dim ) );
1163
1164 // Stale vertex index on footprint-parented binding no longer resolves gate reports undrivable
1165 endBinding->Members()[1].m_index = 7;
1166 BOOST_CHECK( !DimensionEndpointsBound( &board, dim ) );
1167}
1168
1169
1170namespace
1171{
1172// Staging callbacks a live commit would provide recording touched items and completing add remove
1173struct MODE_STAGING
1174{
1175 BOARD& board;
1176 std::vector<BOARD_ITEM*> modified;
1177 std::vector<BOARD_ITEM*> added;
1178 std::vector<BOARD_ITEM*> removed;
1179
1180 std::function<void( BOARD_ITEM* )> modify()
1181 {
1182 return [this]( BOARD_ITEM* aItem ) { modified.push_back( aItem ); };
1183 }
1184
1185 std::function<void( BOARD_ITEM* )> add()
1186 {
1187 return [this]( BOARD_ITEM* aItem )
1188 {
1189 added.push_back( aItem );
1190 board.Add( aItem );
1191 };
1192 }
1193
1194 std::function<void( BOARD_ITEM* )> remove()
1195 {
1196 return [this]( BOARD_ITEM* aItem )
1197 {
1198 removed.push_back( aItem );
1199 board.Remove( aItem );
1200 delete aItem;
1201 };
1202 }
1203};
1204}
1205
1206
1207// Value mode derives from board state Driven default Arbitrary on override Driving needs self length
1208// Only aligned orthogonal radial dimensions carry a mode
1209BOOST_AUTO_TEST_CASE( ValueModeDerivation )
1210{
1211 BOARD board;
1212 PCB_DIM_ALIGNED* dim = addAlignedDim( board, { 0, 0 }, { 10 * MM, 0 } );
1213
1214 BOOST_CHECK( DimensionValueMode( &board, dim ) == DIM_VALUE_MODE::DRIVEN );
1215 BOOST_CHECK( dim->GetValueMode() == DIM_VALUE_MODE::DRIVEN );
1216
1217 dim->ChangeOverrideText( wxS( "custom" ) );
1218 BOOST_CHECK( DimensionValueMode( &board, dim ) == DIM_VALUE_MODE::ARBITRARY );
1219
1220 PCB_CONSTRAINT* reference = addConstraint(
1223 10.0 * MM );
1224 reference->SetDriving( false );
1225
1226 BOOST_CHECK( FindDimensionLengthConstraint( &board, dim ) == reference );
1227 BOOST_CHECK( DimensionValueMode( &board, dim ) == DIM_VALUE_MODE::ARBITRARY );
1228
1229 dim->SetOverrideTextEnabled( false );
1230 BOOST_CHECK( DimensionValueMode( &board, dim ) == DIM_VALUE_MODE::DRIVEN );
1231
1232 reference->SetDriving( true );
1233 BOOST_CHECK( DimensionValueMode( &board, dim ) == DIM_VALUE_MODE::DRIVING );
1234
1235 // Only value-bearing families carry the mode
1236 PCB_DIM_LEADER* leader = new PCB_DIM_LEADER( &board );
1237 board.Add( leader );
1238
1239 BOOST_CHECK( DimensionHasValueMode( dim ) );
1240 BOOST_CHECK( !DimensionHasValueMode( leader ) );
1241 BOOST_CHECK( !DimensionHasValueMode( nullptr ) );
1242}
1243
1244
1245// Mode transitions through shared setter Driven to Driving creates length Arbitrary installs text
1246// Driven again clears override every touched item goes through staging callbacks
1247BOOST_AUTO_TEST_CASE( SetValueModeTransitions )
1248{
1249 BOARD board;
1250 PCB_SHAPE* rect = addRect( board, { 0, 0 }, { 10 * MM, 10 * MM } );
1251
1252 PCB_DIM_ALIGNED* dim = addAlignedDim( board, { 0, 0 }, { 10 * MM, 0 } );
1253
1254 bindEndpoint( board, dim->m_Uuid, CONSTRAINT_ANCHOR::START,
1255 { rect->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 0 } );
1256 bindEndpoint( board, dim->m_Uuid, CONSTRAINT_ANCHOR::END,
1257 { rect->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 1 } );
1258
1259 BOOST_REQUIRE( DimensionCanDrive( &board, dim ) );
1260
1261 // Driven to Driving creates the self driving length with the seeded value
1262 MODE_STAGING toDriving{ board };
1263 PCB_CONSTRAINT* driving =
1264 SetDimensionValueMode( &board, dim, DIM_VALUE_MODE::DRIVING, 12 * MM, std::nullopt,
1265 toDriving.modify(), toDriving.add(), toDriving.remove() );
1266
1267 BOOST_REQUIRE( driving );
1268 BOOST_CHECK( FindDimensionLengthConstraint( &board, dim ) == driving );
1269 BOOST_CHECK( driving->IsDriving() );
1270 BOOST_REQUIRE( driving->GetValue().has_value() );
1271 BOOST_CHECK_EQUAL( *driving->GetValue(), 12 * MM );
1272 BOOST_CHECK( DimensionValueMode( &board, dim ) == DIM_VALUE_MODE::DRIVING );
1273 BOOST_CHECK_EQUAL( toDriving.added.size(), 1 );
1274 BOOST_CHECK( toDriving.added[0] == driving );
1275 BOOST_CHECK( toDriving.removed.empty() );
1276
1277 // Driving to Arbitrary retires the driving length and installs custom text
1278 MODE_STAGING toArbitrary{ board };
1280 SetDimensionValueMode( &board, dim, DIM_VALUE_MODE::ARBITRARY, std::nullopt,
1281 wxString( wxS( "REF" ) ), toArbitrary.modify(), toArbitrary.add(),
1282 toArbitrary.remove() );
1283
1284 BOOST_CHECK( !result );
1285 BOOST_CHECK( !FindDimensionLengthConstraint( &board, dim ) );
1286 BOOST_CHECK( DimensionValueMode( &board, dim ) == DIM_VALUE_MODE::ARBITRARY );
1287 BOOST_CHECK_EQUAL( dim->GetOverrideText(), wxS( "REF" ) );
1288 BOOST_CHECK_EQUAL( toArbitrary.removed.size(), 1 );
1289
1290 // Arbitrary to Driven just clears the override bindings stay
1291 MODE_STAGING toDriven{ board };
1292 SetDimensionValueMode( &board, dim, DIM_VALUE_MODE::DRIVEN, std::nullopt, std::nullopt,
1293 toDriven.modify(), toDriven.add(), toDriven.remove() );
1294
1295 BOOST_CHECK( DimensionValueMode( &board, dim ) == DIM_VALUE_MODE::DRIVEN );
1296 BOOST_CHECK( !dim->GetOverrideTextEnabled() );
1297 BOOST_CHECK( toDriven.added.empty() );
1298 BOOST_CHECK( toDriven.removed.empty() );
1299 BOOST_CHECK( DimensionEndpointsBound( &board, dim ) );
1300}
1301
1302
1303// Driving transition rejected board untouched when unbound or length absent or non-positive
1304BOOST_AUTO_TEST_CASE( SetValueModeDrivingRejections )
1305{
1306 BOARD board;
1307 PCB_DIM_ALIGNED* unbound = addAlignedDim( board, { 0, 0 }, { 10 * MM, 0 } );
1308
1309 MODE_STAGING staging{ board };
1310
1311 BOOST_CHECK( !SetDimensionValueMode( &board, unbound, DIM_VALUE_MODE::DRIVING, 12 * MM,
1312 std::nullopt, staging.modify(), staging.add(),
1313 staging.remove() ) );
1314
1315 PCB_SHAPE* rect = addRect( board, { 0, 0 }, { 10 * MM, 10 * MM } );
1316 PCB_DIM_ALIGNED* bound = addAlignedDim( board, { 0, 0 }, { 10 * MM, 0 } );
1317
1318 bindEndpoint( board, bound->m_Uuid, CONSTRAINT_ANCHOR::START,
1319 { rect->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 0 } );
1320 bindEndpoint( board, bound->m_Uuid, CONSTRAINT_ANCHOR::END,
1321 { rect->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 1 } );
1322
1323 BOOST_CHECK( !SetDimensionValueMode( &board, bound, DIM_VALUE_MODE::DRIVING, std::nullopt,
1324 std::nullopt, staging.modify(), staging.add(),
1325 staging.remove() ) );
1326 BOOST_CHECK( !SetDimensionValueMode( &board, bound, DIM_VALUE_MODE::DRIVING, 0, std::nullopt,
1327 staging.modify(), staging.add(), staging.remove() ) );
1328 BOOST_CHECK( !SetDimensionValueMode( &board, bound, DIM_VALUE_MODE::DRIVING, -5 * MM,
1329 std::nullopt, staging.modify(), staging.add(),
1330 staging.remove() ) );
1331
1332 // A leader never takes a value mode at all
1333 PCB_DIM_LEADER* leader = new PCB_DIM_LEADER( &board );
1334 board.Add( leader );
1335
1336 BOOST_CHECK( !SetDimensionValueMode( &board, leader, DIM_VALUE_MODE::DRIVING, 12 * MM,
1337 std::nullopt, staging.modify(), staging.add(),
1338 staging.remove() ) );
1339
1340 BOOST_CHECK( staging.modified.empty() );
1341 BOOST_CHECK( staging.added.empty() );
1342 BOOST_CHECK( staging.removed.empty() );
1343 BOOST_CHECK( !FindDimensionLengthConstraint( &board, unbound ) );
1344 BOOST_CHECK( !FindDimensionLengthConstraint( &board, bound ) );
1345 BOOST_CHECK( DimensionValueMode( &board, unbound ) == DIM_VALUE_MODE::DRIVEN );
1346 BOOST_CHECK( DimensionValueMode( &board, bound ) == DIM_VALUE_MODE::DRIVEN );
1347}
1348
1349
1350// Panel Driving flow end to end shared setter authors length solving drives rect to width
1351BOOST_AUTO_TEST_CASE( SetValueModeDrivingResolvesGeometry )
1352{
1353 BOARD board;
1354 PCB_SHAPE* rect = addRect( board, { 0, 0 }, { 10 * MM, 10 * MM } );
1355
1356 PCB_DIM_ALIGNED* dim = addAlignedDim( board, { 0, 0 }, { 10 * MM, 0 } ); // TL to TR
1357
1358 bindEndpoint( board, dim->m_Uuid, CONSTRAINT_ANCHOR::START,
1359 { rect->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 0 } );
1360 bindEndpoint( board, dim->m_Uuid, CONSTRAINT_ANCHOR::END,
1361 { rect->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 1 } );
1362
1363 MODE_STAGING staging{ board };
1364 PCB_CONSTRAINT* driving =
1365 SetDimensionValueMode( &board, dim, DIM_VALUE_MODE::DRIVING, 15 * MM, std::nullopt,
1366 staging.modify(), staging.add(), staging.remove() );
1367
1368 BOOST_REQUIRE( driving );
1369
1370 CONSTRAINT_DIAGNOSIS diag = ApplyConstraintImmediately( &board, driving );
1371 BOOST_REQUIRE( diag.solved );
1372
1373 const VECTOR2I tl = rectTopLeft( rect );
1374 const VECTOR2I br = rectBottomRight( rect );
1375
1376 BOOST_CHECK_LE( std::abs( ( br.x - tl.x ) - 15 * MM ), 5000 ); // width driven to the value
1377 BOOST_CHECK_LE( std::abs( ( br.y - tl.y ) - 10 * MM ), 5000 ); // height untouched
1378 BOOST_CHECK_LE( tl.EuclideanNorm(), 5000.0 ); // pinned TL corner held
1379
1380 // Dimension endpoints ride the corners value field text reflects driving length not stale measurement
1381 BOOST_CHECK_LE( ( dim->GetStart() - tl ).EuclideanNorm(), 5000.0 );
1382 BOOST_CHECK_LE( ( dim->GetEnd() - VECTOR2I( br.x, tl.y ) ).EuclideanNorm(), 5000.0 );
1383 BOOST_CHECK( dim->GetValueMode() == DIM_VALUE_MODE::DRIVING );
1384}
1385
1386
1387// Property surface the docked panel drives Value Mode and Value exist only for value-bearing dims
1388// Value read-only while Driven validators gate an unbound Driving switch and non-positive length
1389BOOST_AUTO_TEST_CASE( ValueModePropertySurface )
1390{
1391 BOARD board;
1392 PCB_SHAPE* rect = addRect( board, { 0, 0 }, { 10 * MM, 10 * MM } );
1393
1394 PCB_DIM_ALIGNED* dim = addAlignedDim( board, { 0, 0 }, { 10 * MM, 0 } );
1395
1396 bindEndpoint( board, dim->m_Uuid, CONSTRAINT_ANCHOR::START,
1397 { rect->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 0 } );
1398 bindEndpoint( board, dim->m_Uuid, CONSTRAINT_ANCHOR::END,
1399 { rect->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 1 } );
1400
1401 PCB_DIM_LEADER* leader = new PCB_DIM_LEADER( &board );
1402 board.Add( leader );
1403
1405 propMgr.Rebuild();
1406
1407 PROPERTY_BASE* modeProp = propMgr.GetProperty( TYPE_HASH( PCB_DIMENSION_BASE ), _HKI( "Value Mode" ) );
1408 PROPERTY_BASE* valueProp = propMgr.GetProperty( TYPE_HASH( PCB_DIMENSION_BASE ), _HKI( "Value" ) );
1409
1410 BOOST_REQUIRE( modeProp );
1411 BOOST_REQUIRE( valueProp );
1412
1413 BOOST_CHECK( propMgr.IsAvailableFor( TYPE_HASH( PCB_DIM_ALIGNED ), modeProp, dim ) );
1414 BOOST_CHECK( propMgr.IsAvailableFor( TYPE_HASH( PCB_DIM_ALIGNED ), valueProp, dim ) );
1415 BOOST_CHECK( !propMgr.IsAvailableFor( TYPE_HASH( PCB_DIM_LEADER ), modeProp, leader ) );
1416 BOOST_CHECK( !propMgr.IsAvailableFor( TYPE_HASH( PCB_DIM_LEADER ), valueProp, leader ) );
1417
1418 // Driven shows the measured value read-only
1419 BOOST_CHECK( !propMgr.IsWriteableFor( TYPE_HASH( PCB_DIM_ALIGNED ), valueProp, dim ) );
1420 BOOST_CHECK_EQUAL( dim->GetValueFieldText(), dim->GetValueText() );
1421
1422 // A bound dimension may switch to Driving same switch on an unbound one is vetoed
1423 BOOST_CHECK( !modeProp->Validate( wxAny( static_cast<int>( DIM_VALUE_MODE::DRIVING ) ), dim ) );
1424
1425 PCB_DIM_ALIGNED* unbound = addAlignedDim( board, { 0, 20 * MM }, { 10 * MM, 20 * MM } );
1426
1427 BOOST_CHECK( modeProp->Validate( wxAny( static_cast<int>( DIM_VALUE_MODE::DRIVING ) ), unbound ) );
1428 BOOST_CHECK( !modeProp->Validate( wxAny( static_cast<int>( DIM_VALUE_MODE::ARBITRARY ) ), unbound ) );
1429
1430 // Arbitrary makes the value writable and verbatim through the property setter
1431 dim->ChangeValueMode( DIM_VALUE_MODE::ARBITRARY );
1432
1433 BOOST_CHECK( propMgr.IsWriteableFor( TYPE_HASH( PCB_DIM_ALIGNED ), valueProp, dim ) );
1434
1435 dim->ChangeValueFieldText( wxS( "10 mm MAX" ) );
1436 BOOST_CHECK_EQUAL( dim->GetValueFieldText(), wxS( "10 mm MAX" ) );
1437 BOOST_CHECK_EQUAL( dim->GetOverrideText(), wxS( "10 mm MAX" ) );
1438
1439 // Driving gates the value to a positive length and shows the constraint value
1440 dim->ChangeValueMode( DIM_VALUE_MODE::DRIVEN );
1441
1442 MODE_STAGING staging{ board };
1443 SetDimensionValueMode( &board, dim, DIM_VALUE_MODE::DRIVING, 12 * MM, std::nullopt,
1444 staging.modify(), staging.add(), staging.remove() );
1445
1446 BOOST_CHECK( valueProp->Validate( wxAny( wxString( wxS( "0" ) ) ), dim ) );
1447 BOOST_CHECK( valueProp->Validate( wxAny( wxString( wxS( "-3" ) ) ), dim ) );
1448 BOOST_CHECK( valueProp->Validate( wxAny( wxString( wxS( "text" ) ) ), dim ) );
1449 BOOST_CHECK( !valueProp->Validate( wxAny( wxString( wxS( "15" ) ) ), dim ) );
1450
1451 // A Driven dimension accepts anything the setter will ignore so no veto there
1452 BOOST_CHECK( !valueProp->Validate( wxAny( wxString( wxS( "0" ) ) ), unbound ) );
1453}
1454
1455
int index
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 ...
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)
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:44
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...
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...
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...
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 dimension feature points bound coincident to an object anchor by draw time auto constrain di...
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