KiCad PCB EDA Suite
Loading...
Searching...
No Matches
constraint_builder.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
21
22#include <algorithm>
23#include <cmath>
24#include <ranges>
25
26#include <board.h>
27#include <core/kicad_algo.h>
28#include <footprint.h>
29#include <geometry/eda_angle.h>
30#include <geometry/seg.h>
33#include <pcb_dimension.h>
34#include <pcb_shape.h>
35
36
37namespace
38{
39bool isSegment( const BOARD_ITEM* aItem )
40{
41 return aItem->Type() == PCB_SHAPE_T
42 && static_cast<const PCB_SHAPE*>( aItem )->GetShape() == SHAPE_T::SEGMENT;
43}
44
45
46bool isCircleOrArc( const BOARD_ITEM* aItem )
47{
48 if( aItem->Type() != PCB_SHAPE_T )
49 return false;
50
51 SHAPE_T shape = static_cast<const PCB_SHAPE*>( aItem )->GetShape();
52 return shape == SHAPE_T::CIRCLE || shape == SHAPE_T::ARC;
53}
54
55
56bool isArc( const BOARD_ITEM* aItem )
57{
58 return aItem->Type() == PCB_SHAPE_T
59 && static_cast<const PCB_SHAPE*>( aItem )->GetShape() == SHAPE_T::ARC;
60}
61
62
63bool isEllipseKind( const BOARD_ITEM* aItem )
64{
65 if( aItem->Type() != PCB_SHAPE_T )
66 return false;
67
68 SHAPE_T shape = static_cast<const PCB_SHAPE*>( aItem )->GetShape();
69 return shape == SHAPE_T::ELLIPSE || shape == SHAPE_T::ELLIPSE_ARC;
70}
71
72
73bool allSegments( const std::vector<BOARD_ITEM*>& aItems )
74{
75 return std::ranges::all_of( aItems, isSegment );
76}
77
78
79// Circles and arcs have a radius the solver can equate or fix.
80bool allRadial( const std::vector<BOARD_ITEM*>& aItems )
81{
82 return std::ranges::all_of( aItems, isCircleOrArc );
83}
84
85
86// Circles, arcs and ellipses all have a centre the solver can make concentric.
87bool allCentered( const std::vector<BOARD_ITEM*>& aItems )
88{
89 return std::ranges::all_of( aItems,
90 []( const BOARD_ITEM* aItem )
91 {
92 return isCircleOrArc( aItem ) || isEllipseKind( aItem );
93 } );
94}
95}
96
97
98EDA_ANGLE MeasureCornerAngle( const SEG& aA, const SEG& aB )
99{
100 const VECTOR2I aEnds[2] = { aA.A, aA.B };
101 const VECTOR2I bEnds[2] = { aB.A, aB.B };
102
103 // The vertex is the closest endpoint pair; the rays run from it toward each other endpoint.
104 int vA = 0, vB = 0;
105 SEG::ecoord best = ( aEnds[0] - bEnds[0] ).SquaredEuclideanNorm();
106
107 for( int i = 0; i < 2; ++i )
108 {
109 for( int j = 0; j < 2; ++j )
110 {
111 SEG::ecoord dist = ( aEnds[i] - bEnds[j] ).SquaredEuclideanNorm();
112
113 if( dist < best )
114 {
115 best = dist;
116 vA = i;
117 vB = j;
118 }
119 }
120 }
121
122 // Orient both segments from the shared vertex outward so SEG::Angle reads the corner the rays
123 // open. It uses each segment's true direction (not a midpoint ray), so a small gap between the
124 // near endpoints does not skew the measurement, and it returns [0, 180] without folding past 90.
125 return SEG( aEnds[vA], aEnds[1 - vA] ).Angle( SEG( bEnds[vB], bEnds[1 - vB] ) );
126}
127
128
129std::unique_ptr<PCB_CONSTRAINT> BuildConstraintFromItems( BOARD_ITEM* aParent,
131 const std::vector<BOARD_ITEM*>& aItems )
132{
133 // Build a constraint of aType with every selected item bound by its WHOLE anchor.
134 auto makeWhole = [&]()
135 {
136 std::unique_ptr<PCB_CONSTRAINT> c = std::make_unique<PCB_CONSTRAINT>( aParent, aType );
137
138 for( BOARD_ITEM* item : aItems )
139 c->AddMember( item->m_Uuid, CONSTRAINT_ANCHOR::WHOLE );
140
141 return c;
142 };
143
144 switch( aType )
145 {
150 {
151 if( aItems.size() != 2 || !allSegments( aItems ) )
152 return nullptr;
153
154 return makeWhole();
155 }
156
159 {
160 if( aItems.size() != 1 || !isSegment( aItems[0] ) )
161 return nullptr;
162
163 return makeWhole();
164 }
165
167 {
168 if( aItems.size() != 1 || !isSegment( aItems[0] ) )
169 return nullptr;
170
171 const PCB_SHAPE* seg = static_cast<const PCB_SHAPE*>( aItems[0] );
172
173 std::unique_ptr<PCB_CONSTRAINT> c = makeWhole();
174 c->SetValue( ( seg->GetEnd() - seg->GetStart() ).EuclideanNorm() );
175 return c;
176 }
177
179 {
180 if( aItems.size() != 2 || !allCentered( aItems ) )
181 return nullptr;
182
183 return makeWhole();
184 }
185
187 {
188 if( aItems.size() != 2 || !allRadial( aItems ) )
189 return nullptr;
190
191 return makeWhole();
192 }
193
195 {
196 if( aItems.size() != 2 || !allSegments( aItems ) )
197 return nullptr;
198
199 const PCB_SHAPE* a = static_cast<const PCB_SHAPE*>( aItems[0] );
200 const PCB_SHAPE* b = static_cast<const PCB_SHAPE*>( aItems[1] );
201
202 // A zero-length segment has no direction, so the corner angle is undefined and the solver's
203 // angle equation is singular.
204 if( a->GetStart() == a->GetEnd() || b->GetStart() == b->GetEnd() )
205 return nullptr;
206
207 std::unique_ptr<PCB_CONSTRAINT> c = makeWhole();
208 c->SetValue( MeasureCornerAngle( SEG( a->GetStart(), a->GetEnd() ),
209 SEG( b->GetStart(), b->GetEnd() ) ).AsDegrees() );
210 return c;
211 }
212
214 {
215 if( aItems.size() != 1 || !isCircleOrArc( aItems[0] ) )
216 return nullptr;
217
218 std::unique_ptr<PCB_CONSTRAINT> c = makeWhole();
219 c->SetValue( static_cast<const PCB_SHAPE*>( aItems[0] )->GetRadius() );
220 return c;
221 }
222
224 {
225 if( aItems.size() != 1 || !isArc( aItems[0] ) )
226 return nullptr;
227
228 std::unique_ptr<PCB_CONSTRAINT> c = makeWhole();
229 c->SetValue( static_cast<const PCB_SHAPE*>( aItems[0] )->GetArcAngle().AsDegrees() );
230 return c;
231 }
232
234 {
235 if( aItems.size() != 2 )
236 return nullptr;
237
238 const BOARD_ITEM* a = aItems[0];
239 const BOARD_ITEM* b = aItems[1];
240
241 auto isCurve = []( const BOARD_ITEM* aItem )
242 {
243 return isCircleOrArc( aItem ) || isEllipseKind( aItem );
244 };
245
246 bool lineCurve = ( isSegment( a ) && isCurve( b ) ) || ( isSegment( b ) && isCurve( a ) );
247 bool curveCurve = isCircleOrArc( a ) && isCircleOrArc( b );
248
249 if( !lineCurve && !curveCurve )
250 return nullptr;
251
252 return makeWhole();
253 }
254
255 default:
256 // Point-anchored families (coincident, midpoint, symmetric, ...) need point selection,
257 // which the whole-shape authoring tool does not yet provide.
258 return nullptr;
259 }
260}
261
262
263std::vector<CONSTRAINT_ANCHOR_POINT> ConstraintShapeAnchors( const PCB_SHAPE* aShape )
264{
265 std::vector<CONSTRAINT_ANCHOR_POINT> anchors;
266
267 if( !aShape )
268 return anchors;
269
270 switch( aShape->GetShape() )
271 {
272 case SHAPE_T::SEGMENT:
273 case SHAPE_T::BEZIER:
274 return { { CONSTRAINT_ANCHOR::START, aShape->GetStart() },
275 { CONSTRAINT_ANCHOR::END, aShape->GetEnd() } };
276
277 case SHAPE_T::ARC:
279 return { { CONSTRAINT_ANCHOR::START, aShape->GetStart() },
280 { CONSTRAINT_ANCHOR::END, aShape->GetEnd() },
281 { CONSTRAINT_ANCHOR::CENTER, aShape->GetCenter() } };
282
283 case SHAPE_T::CIRCLE:
284 case SHAPE_T::ELLIPSE:
285 return { { CONSTRAINT_ANCHOR::CENTER, aShape->GetCenter() } };
286
288 {
289 // TL TR BR BL order must match frozen corner roles of adapter
290 VECTOR2I s = aShape->GetStart();
291 VECTOR2I e = aShape->GetEnd();
292 VECTOR2I tl( std::min( s.x, e.x ), std::min( s.y, e.y ) );
293 VECTOR2I br( std::max( s.x, e.x ), std::max( s.y, e.y ) );
294
295 return { { CONSTRAINT_ANCHOR::VERTEX, tl, 0 },
296 { CONSTRAINT_ANCHOR::VERTEX, VECTOR2I( br.x, tl.y ), 1 },
297 { CONSTRAINT_ANCHOR::VERTEX, br, 2 },
298 { CONSTRAINT_ANCHOR::VERTEX, VECTOR2I( tl.x, br.y ), 3 } };
299 }
300
301 case SHAPE_T::POLY:
302 {
303 // Same eligibility gate as adapter ingestion so picker never offers unmappable anchor
304 if( !ConstraintPolygonIsModelable( aShape ) )
305 return anchors;
306
307 const SHAPE_LINE_CHAIN& outline = aShape->GetPolyShape().COutline( 0 );
308
309 for( int i = 0; i < outline.PointCount(); ++i )
310 anchors.push_back( { CONSTRAINT_ANCHOR::VERTEX, outline.CPoint( i ), i } );
311
312 return anchors;
313 }
314
315 default:
316 return anchors;
317 }
318}
319
320
322{
323 if( !aShape || aShape->GetShape() != SHAPE_T::POLY )
324 return false;
325
326 const SHAPE_POLY_SET& polySet = aShape->GetPolyShape();
327
328 if( polySet.OutlineCount() != 1 || polySet.HoleCount( 0 ) > 0 )
329 return false;
330
331 const SHAPE_LINE_CHAIN& outline = polySet.COutline( 0 );
332
333 return outline.PointCount() > 0 && outline.ArcCount() == 0;
334}
335
336
337std::optional<CONSTRAINT_ANCHOR_POINT> ConstraintShapeVertex( const PCB_SHAPE* aShape, int aIndex )
338{
339 if( !aShape || aIndex < 0 )
340 return std::nullopt;
341
342 if( aShape->GetShape() != SHAPE_T::RECTANGLE && aShape->GetShape() != SHAPE_T::POLY )
343 return std::nullopt;
344
345 std::vector<CONSTRAINT_ANCHOR_POINT> anchors = ConstraintShapeAnchors( aShape );
346
347 if( aIndex >= (int) anchors.size() )
348 return std::nullopt;
349
350 return anchors[aIndex];
351}
352
353
354std::optional<CONSTRAINT_MEMBER> NearestAnchorAmong( const std::vector<PCB_SHAPE*>& aShapes,
355 const VECTOR2I& aPos, double aMaxDist )
356{
357 double best = aMaxDist;
358 std::optional<CONSTRAINT_MEMBER> result;
359
360 for( const PCB_SHAPE* shape : aShapes )
361 {
362 for( const CONSTRAINT_ANCHOR_POINT& a : ConstraintShapeAnchors( shape ) )
363 {
364 double dist = ( a.pos - aPos ).EuclideanNorm();
365
366 if( dist <= best )
367 {
368 best = dist;
369 result = CONSTRAINT_MEMBER( shape->m_Uuid, a.anchor, a.index );
370 }
371 }
372 }
373
374 return result;
375}
376
377
378std::vector<PCB_SHAPE*> CollectConstraintShapes( BOARD* aBoard )
379{
380 std::vector<PCB_SHAPE*> shapes;
381
382 if( !aBoard )
383 return shapes;
384
385 for( BOARD_ITEM* item : aBoard->Drawings() )
386 {
387 if( PCB_SHAPE* shape = dynamic_cast<PCB_SHAPE*>( item ) )
388 shapes.push_back( shape );
389 }
390
391 for( FOOTPRINT* footprint : aBoard->Footprints() )
392 {
393 for( BOARD_ITEM* item : footprint->GraphicalItems() )
394 {
395 if( PCB_SHAPE* shape = dynamic_cast<PCB_SHAPE*>( item ) )
396 shapes.push_back( shape );
397 }
398 }
399
400 return shapes;
401}
402
403
404std::vector<BOARD_ITEM*> CollectConstrainableItems( BOARD* aBoard )
405{
406 std::vector<BOARD_ITEM*> items;
407
408 if( !aBoard )
409 return items;
410
411 auto collect =
412 [&]( const auto& aContainer )
413 {
414 for( BOARD_ITEM* item : aContainer )
415 {
416 if( item->Type() == PCB_SHAPE_T || dynamic_cast<PCB_DIMENSION_BASE*>( item ) )
417 items.push_back( item );
418 }
419 };
420
421 collect( aBoard->Drawings() );
422
423 for( FOOTPRINT* footprint : aBoard->Footprints() )
424 collect( footprint->GraphicalItems() );
425
426 return items;
427}
428
429
430std::optional<CONSTRAINT_MEMBER> NearestConstraintAnchor( BOARD* aBoard, const VECTOR2I& aPos,
431 double aMaxDist,
432 const std::vector<CONSTRAINT_MEMBER>& aExclude )
433{
434 double best = aMaxDist;
435 std::optional<CONSTRAINT_MEMBER> result;
436
437 for( BOARD_ITEM* item : CollectConstrainableItems( aBoard ) )
438 {
439 for( const CONSTRAINT_ANCHOR_POINT& a : ConstraintItemAnchors( item ) )
440 {
441 CONSTRAINT_MEMBER candidate( item->m_Uuid, a.anchor, a.index );
442
443 // Skip already picked handle so distinct coincident endpoint stays reachable
444 if( alg::contains( aExclude, candidate ) )
445 continue;
446
447 double dist = ( a.pos - aPos ).EuclideanNorm();
448
449 if( dist <= best )
450 {
451 best = dist;
452 result = candidate;
453 }
454 }
455 }
456
457 return result;
458}
459
460
461std::vector<DIMENSION_ENDPOINT_BINDING>
462SelectDimensionEndpointBindings( BOARD* aBoard, const KIID& aDimension, const VECTOR2I& aStart,
463 const std::optional<VECTOR2I>& aEnd, double aMaxDist )
464{
465 std::vector<DIMENSION_ENDPOINT_BINDING> bindings;
466
467 if( !aBoard )
468 return bindings;
469
470 // Best pair of distinct anchors on one item within aMaxDist minimizing summed distance
471 // Distinct anchors required or endpoints merge and pairs judged jointly not per end nearest
472 using ANCHOR_PAIR = std::pair<CONSTRAINT_MEMBER, CONSTRAINT_MEMBER>;
473
474 auto bestPairOn = [&]( BOARD_ITEM* aItem ) -> std::optional<std::pair<ANCHOR_PAIR, double>>
475 {
476 std::vector<CONSTRAINT_ANCHOR_POINT> anchors = ConstraintItemAnchors( aItem );
477
478 // Sum decomposes per endpoint best and runner up END anchors computed once serve every
479 // START candidate keeps a dense polygon linear in vertex count instead of quadratic
480 const size_t none = anchors.size();
481 size_t bestEnd = none;
482 size_t secondEnd = none;
483 std::vector<double> dEnd( anchors.size(), 0.0 );
484
485 for( size_t j = 0; j < anchors.size(); ++j )
486 {
487 dEnd[j] = ( anchors[j].pos - *aEnd ).EuclideanNorm();
488
489 if( dEnd[j] > aMaxDist )
490 continue;
491
492 if( bestEnd == none || dEnd[j] < dEnd[bestEnd] )
493 {
494 secondEnd = bestEnd;
495 bestEnd = j;
496 }
497 else if( secondEnd == none || dEnd[j] < dEnd[secondEnd] )
498 {
499 secondEnd = j;
500 }
501 }
502
503 if( bestEnd == none )
504 return std::nullopt;
505
506 std::optional<std::pair<ANCHOR_PAIR, double>> best;
507
508 for( size_t i = 0; i < anchors.size(); ++i )
509 {
510 double dStart = ( anchors[i].pos - aStart ).EuclideanNorm();
511
512 if( dStart > aMaxDist )
513 continue;
514
515 size_t j = ( i == bestEnd ) ? secondEnd : bestEnd;
516
517 if( j == none )
518 continue;
519
520 double sum = dStart + dEnd[j];
521
522 if( !best || sum < best->second )
523 {
524 best = std::make_pair(
525 ANCHOR_PAIR{ CONSTRAINT_MEMBER( aItem->m_Uuid, anchors[i].anchor,
526 anchors[i].index ),
527 CONSTRAINT_MEMBER( aItem->m_Uuid, anchors[j].anchor,
528 anchors[j].index ) },
529 sum );
530 }
531 }
532
533 return best;
534 };
535
536 // Prefer single object reaching both endpoints so a single feature dimension stays bound at
537 // both ends
538 if( aEnd )
539 {
540 std::optional<ANCHOR_PAIR> bestPair;
541 double bestSum = 0.0;
542
543 for( BOARD_ITEM* item : CollectConstrainableItems( aBoard ) )
544 {
545 if( item->m_Uuid == aDimension )
546 continue;
547
548 auto pair = bestPairOn( item );
549
550 if( !pair )
551 continue;
552
553 if( !bestPair || pair->second < bestSum )
554 {
555 bestSum = pair->second;
556 bestPair = pair->first;
557 }
558 }
559
560 if( bestPair )
561 {
562 bindings.push_back( { CONSTRAINT_ANCHOR::START, bestPair->first } );
563 bindings.push_back( { CONSTRAINT_ANCHOR::END, bestPair->second } );
564 return bindings;
565 }
566 }
567
568 // Else bind each endpoint to its own nearest anchor the two may land on different objects
569 // and either may find nothing
570 std::vector<CONSTRAINT_MEMBER> exclude{ { aDimension, CONSTRAINT_ANCHOR::START },
571 { aDimension, CONSTRAINT_ANCHOR::END } };
572
573 if( auto startTarget = NearestConstraintAnchor( aBoard, aStart, aMaxDist, exclude ) )
574 {
575 bindings.push_back( { CONSTRAINT_ANCHOR::START, *startTarget } );
576 exclude.push_back( *startTarget );
577 }
578
579 if( aEnd )
580 {
581 if( auto endTarget = NearestConstraintAnchor( aBoard, *aEnd, aMaxDist, exclude ) )
582 bindings.push_back( { CONSTRAINT_ANCHOR::END, *endTarget } );
583 }
584
585 return bindings;
586}
587
588
590{
591 if( !aBoard )
592 return nullptr;
593
594 BOARD_ITEM* item = aBoard->ResolveItem( aId, true );
595
596 return item && ( item->Type() == PCB_SHAPE_T || dynamic_cast<PCB_DIMENSION_BASE*>( item ) )
597 ? item
598 : nullptr;
599}
600
601
602std::vector<CONSTRAINT_ANCHOR_POINT> ConstraintItemAnchors( const BOARD_ITEM* aItem )
603{
604 if( !aItem )
605 return {};
606
607 if( aItem->Type() == PCB_SHAPE_T )
608 return ConstraintShapeAnchors( static_cast<const PCB_SHAPE*>( aItem ) );
609
610 if( const PCB_DIMENSION_BASE* dim = dynamic_cast<const PCB_DIMENSION_BASE*>( aItem ) )
611 {
612 std::vector<CONSTRAINT_ANCHOR_POINT> anchors;
613 anchors.push_back( { CONSTRAINT_ANCHOR::START, dim->GetStart() } );
614
615 // Only aligned/orthogonal/radial dimensions have a second measured feature point; a leader
616 // or centre mark's second point is a control point.
617 switch( aItem->Type() )
618 {
621 case PCB_DIM_RADIAL_T:
622 anchors.push_back( { CONSTRAINT_ANCHOR::END, dim->GetEnd() } );
623 break;
624
625 default:
626 break;
627 }
628
629 return anchors;
630 }
631
632 return {};
633}
634
635
636std::optional<VECTOR2I> ConstraintAnchorPosition( BOARD* aBoard, const CONSTRAINT_MEMBER& aMember )
637{
639 {
640 // VERTEX anchor needs its ordinal too else every vertex member resolves to vertex 0
641 if( a.anchor == aMember.m_anchor
642 && ( a.anchor != CONSTRAINT_ANCHOR::VERTEX || a.index == aMember.m_index ) )
643 {
644 return a.pos;
645 }
646 }
647
648 return std::nullopt;
649}
650
651
652double InitialConstraintValue( PCB_CONSTRAINT_TYPE aType, double aMeasured,
653 const std::map<PCB_CONSTRAINT_TYPE, double>& aRemembered )
654{
655 auto it = aRemembered.find( aType );
656
657 return it != aRemembered.end() ? it->second : aMeasured;
658}
659
660
661std::optional<KIID> NearestConstrainedShape( const std::vector<PCB_SHAPE*>& aCandidates,
662 const VECTOR2I& aPos, int aMaxDist )
663{
664 auto it = std::ranges::find_if( aCandidates,
665 [&]( const PCB_SHAPE* aShape )
666 {
667 return aShape && aShape->HitTest( aPos, aMaxDist );
668 } );
669
670 return it == aCandidates.end() ? std::nullopt : std::optional<KIID>( ( *it )->m_Uuid );
671}
672
673
674std::optional<KIID> SelectRadialDimensionTarget( BOARD* aBoard, const KIID& aDimension,
675 const VECTOR2I& aCenter, const VECTOR2I& aRim,
676 double aMaxDist )
677{
678 if( !aBoard )
679 return std::nullopt;
680
681 std::optional<KIID> best;
682 double bestErr = 0.0;
683
684 for( PCB_SHAPE* shape : CollectConstraintShapes( aBoard ) )
685 {
686 if( shape->m_Uuid == aDimension || !isCircleOrArc( shape ) )
687 continue;
688
689 // Centre and rim must land on the same circle or arc centre and circumference or else a
690 // radial dimension over unrelated geometry would bind spuriously
691 std::optional<VECTOR2I> centerPos;
692
693 for( const CONSTRAINT_ANCHOR_POINT& a : ConstraintShapeAnchors( shape ) )
694 {
695 if( a.anchor == CONSTRAINT_ANCHOR::CENTER )
696 centerPos = a.pos;
697 }
698
699 if( !centerPos )
700 continue;
701
702 double centerErr = ( *centerPos - aCenter ).EuclideanNorm();
703
704 if( centerErr > aMaxDist )
705 continue;
706
707 double rimErr = std::abs( ( aRim - *centerPos ).EuclideanNorm() - shape->GetRadius() );
708
709 if( rimErr > aMaxDist )
710 continue;
711
712 // Arc outline is swept portion only not the whole circle so a rim point off the arc must
713 // not bind
714 if( shape->GetShape() == SHAPE_T::ARC && !shape->HitTest( aRim, KiROUND( aMaxDist ) ) )
715 continue;
716
717 double err = centerErr + rimErr;
718
719 if( !best || err < bestErr )
720 {
721 bestErr = err;
722 best = shape->m_Uuid;
723 }
724 }
725
726 return best;
727}
728
729
730bool DimensionEndpointsBound( BOARD* aBoard, const PCB_DIMENSION_BASE* aDimension )
731{
732 if( !aBoard || !aDimension )
733 return false;
734
735 const CONSTRAINT_MEMBER startMember( aDimension->m_Uuid, CONSTRAINT_ANCHOR::START );
736 const CONSTRAINT_MEMBER endMember( aDimension->m_Uuid, CONSTRAINT_ANCHOR::END );
737
738 auto anyConstraint = [&]( const auto& aMatch )
739 {
740 if( std::ranges::any_of( aBoard->Constraints(), aMatch ) )
741 return true;
742
743 // Bindings are parented to the owning dimension footprint not necessarily the first so
744 // every footprint must be scanned to match the write path
745 return std::ranges::any_of( aBoard->Footprints(),
746 [&]( const FOOTPRINT* aFootprint )
747 { return std::ranges::any_of( aFootprint->Constraints(), aMatch ); } );
748 };
749
750 // Radial dimension binds centre coincident plus rim on outline of one circle or arc
751 // Legs on different objects or an object that cannot play the radius role never offer Driving
752 if( aDimension->Type() == PCB_DIM_RADIAL_T )
753 {
754 auto rimOnItem = [&]( const KIID& aItem )
755 {
756 return anyConstraint(
757 [&]( const PCB_CONSTRAINT* aConstraint )
758 {
760 return false;
761
762 // Point on line binding is asymmetric the dimension rim point is member 0
763 // and the object outline WHOLE anchor is member 1
764 const std::vector<CONSTRAINT_MEMBER>& members = aConstraint->GetMembers();
765
766 return members.size() == 2 && members[0] == endMember
767 && members[1] == CONSTRAINT_MEMBER( aItem, CONSTRAINT_ANCHOR::WHOLE );
768 } );
769 };
770
771 return anyConstraint(
772 [&]( const PCB_CONSTRAINT* aConstraint )
773 {
775 return false;
776
777 const std::vector<CONSTRAINT_MEMBER>& members = aConstraint->GetMembers();
778
779 if( members.size() != 2 )
780 return false;
781
782 // Authored dimension first but coincident is symmetric so accept either order
783 const CONSTRAINT_MEMBER* target = nullptr;
784
785 if( members[0] == startMember )
786 target = &members[1];
787 else if( members[1] == startMember )
788 target = &members[0];
789
790 if( !target || target->m_anchor != CONSTRAINT_ANCHOR::CENTER )
791 return false;
792
793 BOARD_ITEM* item = ResolveConstrainableItem( aBoard, target->m_item );
794
795 return item && isCircleOrArc( item ) && rimOnItem( target->m_item );
796 } );
797 }
798
799 // Aligned or orthogonal needs a coincident per endpoint whose target still resolves a target
800 // pointing at a deleted item or a stale vertex index does not count
801 auto hasCoincident = [&]( const CONSTRAINT_MEMBER& aMember )
802 {
803 return anyConstraint(
804 [&]( const PCB_CONSTRAINT* aConstraint )
805 {
807 return false;
808
809 const std::vector<CONSTRAINT_MEMBER>& members = aConstraint->GetMembers();
810
811 // Must pair with a distinct target not itself
812 if( members.size() != 2 || members[0].m_item == members[1].m_item )
813 return false;
814
815 if( members[0] == aMember )
816 return ConstraintAnchorPosition( aBoard, members[1] ).has_value();
817
818 return members[1] == aMember
819 && ConstraintAnchorPosition( aBoard, members[0] ).has_value();
820 } );
821 };
822
823 return hasCoincident( startMember ) && hasCoincident( endMember );
824}
825
826
828{
829 if( !aDimension )
830 return false;
831
832 switch( aDimension->Type() )
833 {
836 case PCB_DIM_RADIAL_T:
837 return true;
838
839 default:
840 return false;
841 }
842}
843
844
846{
847 if( !aBoard || !aDimension )
848 return nullptr;
849
850 const CONSTRAINT_MEMBER startMember( aDimension->m_Uuid, CONSTRAINT_ANCHOR::START );
851 const CONSTRAINT_MEMBER endMember( aDimension->m_Uuid, CONSTRAINT_ANCHOR::END );
852
853 auto matches = [&]( const PCB_CONSTRAINT* aConstraint )
854 {
855 if( aConstraint->GetConstraintType() != PCB_CONSTRAINT_TYPE::FIXED_LENGTH )
856 return false;
857
858 const std::vector<CONSTRAINT_MEMBER>& members = aConstraint->GetMembers();
859
860 return members.size() == 2
861 && ( ( members[0] == startMember && members[1] == endMember )
862 || ( members[0] == endMember && members[1] == startMember ) );
863 };
864
865 auto scan = [&]( const CONSTRAINTS& aList ) -> PCB_CONSTRAINT*
866 {
867 auto it = std::ranges::find_if( aList, matches );
868 return it != aList.end() ? *it : nullptr;
869 };
870
871 if( PCB_CONSTRAINT* c = scan( aBoard->Constraints() ) )
872 return c;
873
874 // Driving length is parented to the owning dimension footprint not necessarily the first so
875 // scan every footprint to match the write
876 for( FOOTPRINT* footprint : aBoard->Footprints() )
877 {
878 if( PCB_CONSTRAINT* c = scan( footprint->Constraints() ) )
879 return c;
880 }
881
882 return nullptr;
883}
884
885
886bool DimensionCanDrive( BOARD* aBoard, const PCB_DIMENSION_BASE* aDimension )
887{
888 if( !DimensionHasValueMode( aDimension ) )
889 return false;
890
891 if( DimensionEndpointsBound( aBoard, aDimension ) )
892 return true;
893
894 PCB_CONSTRAINT* existing = FindDimensionLengthConstraint( aBoard, aDimension );
895
896 return existing && existing->IsDriving();
897}
898
899
901{
902 PCB_CONSTRAINT* lengthConstraint = FindDimensionLengthConstraint( aBoard, aDimension );
903
904 if( lengthConstraint && lengthConstraint->IsDriving() )
906
907 if( aDimension && aDimension->GetOverrideTextEnabled() )
909
911}
912
913
915 std::optional<int> aDrivingLengthIU,
916 const std::optional<wxString>& aOverrideText,
917 const std::function<void( BOARD_ITEM* )>& aBeforeModify,
918 const std::function<void( BOARD_ITEM* )>& aStageAdd,
919 const std::function<void( BOARD_ITEM* )>& aBeforeRemove )
920{
921 if( !aBoard || !DimensionHasValueMode( aDimension ) )
922 return nullptr;
923
924 PCB_CONSTRAINT* existing = FindDimensionLengthConstraint( aBoard, aDimension );
925
926 if( aMode == DIM_VALUE_MODE::DRIVING )
927 {
928 // Unbound dimension has no geometry to drive and a non positive length would collapse the
929 // constraint so the transition rejects with the board untouched
930 if( !aDrivingLengthIU || *aDrivingLengthIU <= 0 || !DimensionCanDrive( aBoard, aDimension ) )
931 return nullptr;
932
933 aBeforeModify( aDimension );
934 aDimension->SetOverrideTextEnabled( false );
935 aDimension->Update();
936
937 if( existing )
938 {
939 aBeforeModify( existing );
940 existing->SetValue( *aDrivingLengthIU );
941 existing->SetDriving( true );
942 return existing;
943 }
944
945 BOARD_ITEM* parent = aDimension->GetParentFootprint()
946 ? static_cast<BOARD_ITEM*>( aDimension->GetParentFootprint() )
947 : static_cast<BOARD_ITEM*>( aBoard );
948
949 auto constraint = std::make_unique<PCB_CONSTRAINT>( parent, PCB_CONSTRAINT_TYPE::FIXED_LENGTH );
950 constraint->AddMember( aDimension->m_Uuid, CONSTRAINT_ANCHOR::START );
951 constraint->AddMember( aDimension->m_Uuid, CONSTRAINT_ANCHOR::END );
952 constraint->SetValue( *aDrivingLengthIU );
953 constraint->SetDriving( true );
954
955 PCB_CONSTRAINT* added = constraint.get();
956 aStageAdd( constraint.release() );
957 return added;
958 }
959
960 aBeforeModify( aDimension );
962
963 if( aMode == DIM_VALUE_MODE::ARBITRARY && aOverrideText )
964 aDimension->SetOverrideText( *aOverrideText );
965
966 aDimension->Update();
967
968 // Driven and Arbitrary both measure geometry natively so any driving length is dropped
969 if( existing )
970 aBeforeRemove( existing );
971
972 return nullptr;
973}
974
975
976void RemapPolygonVertexMembers( BOARD* aBoard, const KIID& aPoly, int aChangedIndex, int aDelta,
977 const std::function<void( BOARD_ITEM* )>& aBeforeModify,
978 const std::function<void( BOARD_ITEM* )>& aBeforeRemove )
979{
980 if( !aBoard || aDelta == 0 )
981 return;
982
983 auto remapIn = [&]( const CONSTRAINTS& aConstraints )
984 {
985 for( PCB_CONSTRAINT* constraint : aConstraints )
986 {
987 bool shifts = false;
988 bool doomed = false;
989
990 for( const CONSTRAINT_MEMBER& member : constraint->GetMembers() )
991 {
992 if( member.m_item != aPoly || member.m_anchor != CONSTRAINT_ANCHOR::VERTEX )
993 continue;
994
995 if( aDelta < 0 && member.m_index == aChangedIndex )
996 doomed = true;
997 else if( member.m_index >= aChangedIndex )
998 shifts = true;
999 }
1000
1001 // Deleted vertex drags its bound member down and no fixed arity solver form survives
1002 // losing one so the whole constraint retires left unedited the staged removal image
1003 // keeps the authored members for undo
1004 if( doomed )
1005 {
1006 aBeforeRemove( constraint );
1007 continue;
1008 }
1009
1010 if( !shifts )
1011 continue;
1012
1013 aBeforeModify( constraint );
1014
1015 for( CONSTRAINT_MEMBER& member : constraint->Members() )
1016 {
1017 if( member.m_item == aPoly && member.m_anchor == CONSTRAINT_ANCHOR::VERTEX
1018 && member.m_index >= aChangedIndex )
1019 {
1020 member.m_index += aDelta;
1021 }
1022 }
1023 }
1024 };
1025
1026 remapIn( aBoard->Constraints() );
1027
1028 for( FOOTPRINT* footprint : aBoard->Footprints() )
1029 remapIn( footprint->Constraints() );
1030}
constexpr BOX2I KiROUND(const BOX2D &aBoxD)
Definition box2.h:986
A base class for any item which can be embedded within the BOARD container class, and therefore insta...
Definition board_item.h:83
FOOTPRINT * GetParentFootprint() const
Information pertinent to a Pcbnew printed circuit board.
Definition board.h:373
const FOOTPRINTS & Footprints() const
Definition board.h:421
const CONSTRAINTS & Constraints() const
Geometric constraints (#2329) owned by this board.
Definition board.h:465
BOARD_ITEM * ResolveItem(const KIID &aID, bool aAllowNullptrReturn=false) const
Definition board.cpp:1913
const DRAWINGS & Drawings() const
Definition board.h:423
double AsDegrees() const
Definition eda_angle.h:116
const KIID m_Uuid
Definition eda_item.h:531
KICAD_T Type() const
Returns the type of object.
Definition eda_item.h:108
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
Definition kiid.h:44
A geometric constraint between board items (issue #2329).
const std::vector< CONSTRAINT_MEMBER > & GetMembers() const
bool IsDriving() const
A driving constraint forces its value; a reference (non-driving) one only measures it.
PCB_CONSTRAINT_TYPE GetConstraintType() const
void SetValue(std::optional< double > aValue)
void SetDriving(bool aDriving)
Abstract dimension API.
void Update()
Update the dimension's cached text and geometry.
void SetOverrideTextEnabled(bool aOverride)
void SetOverrideText(const wxString &aValue)
bool GetOverrideTextEnabled() const
VECTOR2I GetCenter() const override
This defaults to the center of the bounding box if not overridden.
Definition pcb_shape.h:78
bool HitTest(const VECTOR2I &aPosition, int aAccuracy=0) const override
Test if aPosition is inside or on the boundary of this item.
Definition pcb_shape.h:153
Definition seg.h:38
VECTOR2I A
Definition seg.h:45
VECTOR2I::extended_type ecoord
Definition seg.h:40
VECTOR2I B
Definition seg.h:46
EDA_ANGLE Angle(const SEG &aOther) const
Determine the smallest angle between two segments.
Definition seg.cpp:107
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.
size_t ArcCount() const
Represent a set of closed polygons.
int HoleCount(int aOutline) const
Returns the number of holes in a given outline.
int OutlineCount() const
Return the number of outlines in the set.
const SHAPE_LINE_CHAIN & COutline(int aIndex) const
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< PCB_SHAPE * > CollectConstraintShapes(BOARD *aBoard)
Every PCB_SHAPE on the board (drawings plus footprint graphics) – the candidates constraints can refe...
double InitialConstraintValue(PCB_CONSTRAINT_TYPE aType, double aMeasured, const std::map< PCB_CONSTRAINT_TYPE, double > &aRemembered)
Value a freshly authored constraint dialog should open with.
EDA_ANGLE MeasureCornerAngle(const SEG &aA, const SEG &aB)
The corner angle between two segments, in the closed range [0, 180] degrees.
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::optional< KIID > NearestConstrainedShape(const std::vector< PCB_SHAPE * > &aCandidates, const VECTOR2I &aPos, int aMaxDist)
The candidate shape whose outline aPos hits within aMaxDist, or std::nullopt.
std::optional< CONSTRAINT_ANCHOR_POINT > ConstraintShapeVertex(const PCB_SHAPE *aShape, int aIndex)
VERTEX anchor at ordinal aIndex of a rectangle or eligible polygon or std::nullopt if the shape has n...
std::unique_ptr< PCB_CONSTRAINT > BuildConstraintFromItems(BOARD_ITEM *aParent, PCB_CONSTRAINT_TYPE aType, const std::vector< BOARD_ITEM * > &aItems)
Build a constraint of aType from a set of selected board items, or nullptr if the selection does not ...
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::vector< BOARD_ITEM * > CollectConstrainableItems(BOARD *aBoard)
Every constrainable item on the board – shapes plus dimensions – for board-wide anchor picking.
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 ConstraintPolygonIsModelable(const PCB_SHAPE *aShape)
True when polygon has one non empty hole free arc free outline making it solver eligible Shared by ad...
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...
BOARD_ITEM * ResolveConstrainableItem(BOARD *aBoard, const KIID &aId)
The board item a constraint may reference: a PCB_SHAPE or a dimension, or nullptr for anything else (...
std::optional< CONSTRAINT_MEMBER > NearestAnchorAmong(const std::vector< PCB_SHAPE * > &aShapes, const VECTOR2I &aPos, double aMaxDist)
The {shape, anchor} member nearest aPos within aMaxDist among aShapes, or std::nullopt.
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...
DIM_VALUE_MODE
Mode a value bearing dimension value is in Driven mirrors measured geometry Driving forces geometry t...
SHAPE_T
Definition eda_shape.h:44
@ ELLIPSE
Definition eda_shape.h:52
@ SEGMENT
Definition eda_shape.h:46
@ RECTANGLE
Use RECTANGLE instead of RECT to avoid collision in a Windows header.
Definition eda_shape.h:47
@ ELLIPSE_ARC
Definition eda_shape.h:53
bool contains(const _Container &__container, _Value __value)
Returns true if the container contains the given value.
Definition kicad_algo.h:96
EDA_ANGLE abs(const EDA_ANGLE &aAngle)
Definition eda_angle.h:400
@ VERTEX
An indexed rectangle corner or polygon outline vertex; pairs with CONSTRAINT_MEMBER::m_index.
@ WHOLE
The item as a whole (a segment as a line, a circle).
@ START
First endpoint of a segment or arc.
@ END
Second endpoint of a segment or arc.
@ CENTER
Center of an arc or circle.
PCB_CONSTRAINT_TYPE
The geometric relationship a PCB_CONSTRAINT enforces between its members.
@ CONCENTRIC
Two arcs/circles share a center.
@ VERTICAL
A segment (or two points) is vertical.
@ TANGENT
A line and a curve, or two curves, touch tangentially.
@ COINCIDENT
Two points are made to coincide.
@ PERPENDICULAR
Two segments are perpendicular.
@ FIXED_RADIUS
An arc/circle has a driving radius value.
@ HORIZONTAL
A segment (or two points) is horizontal.
@ EQUAL_RADIUS
Two arcs/circles have equal radius.
@ POINT_ON_LINE
A point lies on a segment's supporting line.
@ FIXED_LENGTH
A segment has a driving length value.
@ ANGULAR_DIMENSION
An angle between members (driving or reference).
@ COLLINEAR
Two segments lie on the same line.
@ ARC_ANGLE
An arc has a driving or reference swept-angle value.
@ PARALLEL
Two segments are parallel.
@ EQUAL_LENGTH
Two segments have equal length.
std::deque< PCB_CONSTRAINT * > CONSTRAINTS
A selectable feature of a shape (a segment endpoint, arc centre, ...) and its location.
One participant in a constraint: a referenced board item plus the feature of that item that participa...
KIID m_item
Referenced board item, usually a PCB_SHAPE.
CONSTRAINT_ANCHOR m_anchor
Which feature of that item participates.
int m_index
Vertex ordinal; only meaningful for the VERTEX anchor.
std::vector< double > vA
wxString result
Test unit parsing edge cases and error handling.
@ PCB_SHAPE_T
class PCB_SHAPE, a segment not on copper layers
Definition typeinfo.h:81
@ PCB_DIM_ORTHOGONAL_T
class PCB_DIM_ORTHOGONAL, a linear dimension constrained to x/y
Definition typeinfo.h:99
@ PCB_DIM_ALIGNED_T
class PCB_DIM_ALIGNED, a linear dimension (graphic item)
Definition typeinfo.h:95
@ PCB_DIM_RADIAL_T
class PCB_DIM_RADIAL, a radius or diameter dimension
Definition typeinfo.h:98
VECTOR2< int32_t > VECTOR2I
Definition vector2d.h:683