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#include <set>
26
27#include <base_units.h>
28#include <board.h>
29#include <core/kicad_algo.h>
30#include <footprint.h>
31#include <geometry/eda_angle.h>
32#include <geometry/seg.h>
35#include <pcb_dimension.h>
36#include <pcb_shape.h>
37
38
39namespace
40{
41bool isSegment( const BOARD_ITEM* aItem )
42{
43 return aItem->Type() == PCB_SHAPE_T
44 && static_cast<const PCB_SHAPE*>( aItem )->GetShape() == SHAPE_T::SEGMENT;
45}
46
47
48bool isCircleOrArc( const BOARD_ITEM* aItem )
49{
50 if( aItem->Type() != PCB_SHAPE_T )
51 return false;
52
53 SHAPE_T shape = static_cast<const PCB_SHAPE*>( aItem )->GetShape();
54 return shape == SHAPE_T::CIRCLE || shape == SHAPE_T::ARC;
55}
56
57
58bool isArc( const BOARD_ITEM* aItem )
59{
60 return aItem->Type() == PCB_SHAPE_T
61 && static_cast<const PCB_SHAPE*>( aItem )->GetShape() == SHAPE_T::ARC;
62}
63
64
65bool isEllipseKind( const BOARD_ITEM* aItem )
66{
67 if( aItem->Type() != PCB_SHAPE_T )
68 return false;
69
70 SHAPE_T shape = static_cast<const PCB_SHAPE*>( aItem )->GetShape();
71 return shape == SHAPE_T::ELLIPSE || shape == SHAPE_T::ELLIPSE_ARC;
72}
73
74
75bool allSegments( const std::vector<BOARD_ITEM*>& aItems )
76{
77 return std::ranges::all_of( aItems, isSegment );
78}
79
80
81// Circles and arcs have a radius the solver can equate or fix.
82bool allRadial( const std::vector<BOARD_ITEM*>& aItems )
83{
84 return std::ranges::all_of( aItems, isCircleOrArc );
85}
86
87
88// Circles, arcs and ellipses all have a centre the solver can make concentric.
89bool allCentered( const std::vector<BOARD_ITEM*>& aItems )
90{
91 return std::ranges::all_of( aItems,
92 []( const BOARD_ITEM* aItem )
93 {
94 return isCircleOrArc( aItem ) || isEllipseKind( aItem );
95 } );
96}
97}
98
99
100EDA_ANGLE MeasureCornerAngle( const SEG& aA, const SEG& aB )
101{
102 const VECTOR2I aEnds[2] = { aA.A, aA.B };
103 const VECTOR2I bEnds[2] = { aB.A, aB.B };
104
105 // The vertex is the closest endpoint pair; the rays run from it toward each other endpoint.
106 int vA = 0, vB = 0;
107 SEG::ecoord best = ( aEnds[0] - bEnds[0] ).SquaredEuclideanNorm();
108
109 for( int i = 0; i < 2; ++i )
110 {
111 for( int j = 0; j < 2; ++j )
112 {
113 SEG::ecoord dist = ( aEnds[i] - bEnds[j] ).SquaredEuclideanNorm();
114
115 if( dist < best )
116 {
117 best = dist;
118 vA = i;
119 vB = j;
120 }
121 }
122 }
123
124 // Orient both segments from the shared vertex outward so SEG::Angle reads the corner the rays
125 // open. It uses each segment's true direction (not a midpoint ray), so a small gap between the
126 // near endpoints does not skew the measurement, and it returns [0, 180] without folding past 90.
127 return SEG( aEnds[vA], aEnds[1 - vA] ).Angle( SEG( bEnds[vB], bEnds[1 - vB] ) );
128}
129
130
131std::unique_ptr<PCB_CONSTRAINT> BuildConstraintFromItems( BOARD_ITEM* aParent,
133 const std::vector<BOARD_ITEM*>& aItems )
134{
135 // Build a constraint of aType with every selected item bound by its WHOLE anchor.
136 auto makeWhole = [&]()
137 {
138 std::unique_ptr<PCB_CONSTRAINT> c = std::make_unique<PCB_CONSTRAINT>( aParent, aType );
139
140 for( BOARD_ITEM* item : aItems )
141 c->AddMember( item->m_Uuid, CONSTRAINT_ANCHOR::WHOLE );
142
143 return c;
144 };
145
146 switch( aType )
147 {
152 {
153 if( aItems.size() != 2 || !allSegments( aItems ) )
154 return nullptr;
155
156 return makeWhole();
157 }
158
161 {
162 if( aItems.size() != 1 || !isSegment( aItems[0] ) )
163 return nullptr;
164
165 return makeWhole();
166 }
167
169 {
170 if( aItems.size() != 1 || !isSegment( aItems[0] ) )
171 return nullptr;
172
173 const PCB_SHAPE* seg = static_cast<const PCB_SHAPE*>( aItems[0] );
174
175 std::unique_ptr<PCB_CONSTRAINT> c = makeWhole();
176 c->SetValue( ( seg->GetEnd() - seg->GetStart() ).EuclideanNorm() );
177 return c;
178 }
179
181 {
182 if( aItems.size() != 2 || !allCentered( aItems ) )
183 return nullptr;
184
185 return makeWhole();
186 }
187
189 {
190 if( aItems.size() != 2 || !allRadial( aItems ) )
191 return nullptr;
192
193 return makeWhole();
194 }
195
197 {
198 if( aItems.size() != 2 || !allSegments( aItems ) )
199 return nullptr;
200
201 const PCB_SHAPE* a = static_cast<const PCB_SHAPE*>( aItems[0] );
202 const PCB_SHAPE* b = static_cast<const PCB_SHAPE*>( aItems[1] );
203
204 // A zero-length segment has no direction, so the corner angle is undefined and the solver's
205 // angle equation is singular.
206 if( a->GetStart() == a->GetEnd() || b->GetStart() == b->GetEnd() )
207 return nullptr;
208
209 std::unique_ptr<PCB_CONSTRAINT> c = makeWhole();
210 c->SetValue( MeasureCornerAngle( SEG( a->GetStart(), a->GetEnd() ),
211 SEG( b->GetStart(), b->GetEnd() ) ).AsDegrees() );
212 return c;
213 }
214
216 {
217 if( aItems.size() != 1 || !isCircleOrArc( aItems[0] ) )
218 return nullptr;
219
220 std::unique_ptr<PCB_CONSTRAINT> c = makeWhole();
221 c->SetValue( static_cast<const PCB_SHAPE*>( aItems[0] )->GetRadius() );
222 return c;
223 }
224
226 {
227 if( aItems.size() != 1 || !isArc( aItems[0] ) )
228 return nullptr;
229
230 std::unique_ptr<PCB_CONSTRAINT> c = makeWhole();
231 c->SetValue( static_cast<const PCB_SHAPE*>( aItems[0] )->GetArcAngle().AsDegrees() );
232 return c;
233 }
234
236 {
237 if( aItems.size() != 2 )
238 return nullptr;
239
240 const BOARD_ITEM* a = aItems[0];
241 const BOARD_ITEM* b = aItems[1];
242
243 auto isCurve = []( const BOARD_ITEM* aItem )
244 {
245 return isCircleOrArc( aItem ) || isEllipseKind( aItem );
246 };
247
248 bool lineCurve = ( isSegment( a ) && isCurve( b ) ) || ( isSegment( b ) && isCurve( a ) );
249 bool curveCurve = isCircleOrArc( a ) && isCircleOrArc( b );
250
251 if( !lineCurve && !curveCurve )
252 return nullptr;
253
254 return makeWhole();
255 }
256
257 default:
258 // Point-anchored families (coincident, midpoint, symmetric, ...) need point selection,
259 // which the whole-shape authoring tool does not yet provide.
260 return nullptr;
261 }
262}
263
264
266{
267 wxString format;
268
269 // No default: an added type must be classified here, or the build fails rather than shipping
270 // a silently hintless constraint. Each arm mirrors the matching BuildConstraintFromItems rule.
271 switch( aType )
272 {
277 format = _( "%s needs two line segments. Click them to constrain." );
278 break;
279
280 // A zero-length segment has no direction, so the corner angle would be singular
282 format = _( "%s needs two line segments of nonzero length. Click them to constrain." );
283 break;
284
287 format = _( "%s needs one line segment, or two anchor points. Click them to constrain." );
288 break;
289
291 format = _( "%s needs one line segment. Click it to constrain." );
292 break;
293
294 // allCentered also accepts ellipses, which have a centre but no single radius
296 format = _( "%s needs two arcs, circles or ellipses. Click them to constrain." );
297 break;
298
300 format = _( "%s needs two arcs or circles. Click them to constrain." );
301 break;
302
304 format = _( "%s needs one arc or circle. Click it to constrain." );
305 break;
306
308 format = _( "%s needs one arc. Click it to constrain." );
309 break;
310
311 // Two curves must both be arcs or circles; an ellipse pairs only with a line
313 format = _( "%s needs a line and a curve, or two arcs or circles. Click them to constrain." );
314 break;
315
316 // Authored by clicking anchors, so there is never a selection to reject
323 return wxEmptyString;
324 }
325
326 return wxString::Format( format, ConstraintTypeLabel( aType ) );
327}
328
329
330std::vector<CONSTRAINT_ANCHOR_POINT> ConstraintShapeAnchors( const PCB_SHAPE* aShape )
331{
332 std::vector<CONSTRAINT_ANCHOR_POINT> anchors;
333
334 if( !aShape )
335 return anchors;
336
337 switch( aShape->GetShape() )
338 {
339 case SHAPE_T::SEGMENT:
340 case SHAPE_T::BEZIER:
341 return { { CONSTRAINT_ANCHOR::START, aShape->GetStart() },
342 { CONSTRAINT_ANCHOR::END, aShape->GetEnd() } };
343
344 case SHAPE_T::ARC:
346 return { { CONSTRAINT_ANCHOR::START, aShape->GetStart() },
347 { CONSTRAINT_ANCHOR::END, aShape->GetEnd() },
348 { CONSTRAINT_ANCHOR::CENTER, aShape->GetCenter() } };
349
350 case SHAPE_T::CIRCLE:
351 case SHAPE_T::ELLIPSE:
352 return { { CONSTRAINT_ANCHOR::CENTER, aShape->GetCenter() } };
353
355 {
356 // TL TR BR BL order must match frozen corner roles of adapter
357 VECTOR2I s = aShape->GetStart();
358 VECTOR2I e = aShape->GetEnd();
359 VECTOR2I tl( std::min( s.x, e.x ), std::min( s.y, e.y ) );
360 VECTOR2I br( std::max( s.x, e.x ), std::max( s.y, e.y ) );
361
362 return { { CONSTRAINT_ANCHOR::VERTEX, tl, 0 },
363 { CONSTRAINT_ANCHOR::VERTEX, VECTOR2I( br.x, tl.y ), 1 },
364 { CONSTRAINT_ANCHOR::VERTEX, br, 2 },
365 { CONSTRAINT_ANCHOR::VERTEX, VECTOR2I( tl.x, br.y ), 3 } };
366 }
367
368 case SHAPE_T::POLY:
369 {
370 // Same eligibility gate as adapter ingestion so picker never offers unmappable anchor
371 if( !ConstraintPolygonIsModelable( aShape ) )
372 return anchors;
373
374 const SHAPE_LINE_CHAIN& outline = aShape->GetPolyShape().COutline( 0 );
375
376 for( int i = 0; i < outline.PointCount(); ++i )
377 anchors.push_back( { CONSTRAINT_ANCHOR::VERTEX, outline.CPoint( i ), i } );
378
379 return anchors;
380 }
381
382 default:
383 return anchors;
384 }
385}
386
387
389{
390 if( !aShape || aShape->GetShape() != SHAPE_T::POLY )
391 return false;
392
393 const SHAPE_POLY_SET& polySet = aShape->GetPolyShape();
394
395 if( polySet.OutlineCount() != 1 || polySet.HoleCount( 0 ) > 0 )
396 return false;
397
398 const SHAPE_LINE_CHAIN& outline = polySet.COutline( 0 );
399
400 return outline.PointCount() > 0 && outline.ArcCount() == 0;
401}
402
403
404std::optional<CONSTRAINT_ANCHOR_POINT> ConstraintShapeVertex( const PCB_SHAPE* aShape, int aIndex )
405{
406 if( !aShape || aIndex < 0 )
407 return std::nullopt;
408
409 if( aShape->GetShape() != SHAPE_T::RECTANGLE && aShape->GetShape() != SHAPE_T::POLY )
410 return std::nullopt;
411
412 std::vector<CONSTRAINT_ANCHOR_POINT> anchors = ConstraintShapeAnchors( aShape );
413
414 if( aIndex >= (int) anchors.size() )
415 return std::nullopt;
416
417 return anchors[aIndex];
418}
419
420
421std::optional<CONSTRAINT_MEMBER> NearestAnchorAmong( const std::vector<PCB_SHAPE*>& aShapes,
422 const VECTOR2I& aPos, double aMaxDist )
423{
424 double best = aMaxDist;
425 std::optional<CONSTRAINT_MEMBER> result;
426
427 for( const PCB_SHAPE* shape : aShapes )
428 {
429 for( const CONSTRAINT_ANCHOR_POINT& a : ConstraintShapeAnchors( shape ) )
430 {
431 double dist = ( a.pos - aPos ).EuclideanNorm();
432
433 if( dist <= best )
434 {
435 best = dist;
436 result = CONSTRAINT_MEMBER( shape->m_Uuid, a.anchor, a.index );
437 }
438 }
439 }
440
441 return result;
442}
443
444
445std::vector<PCB_SHAPE*> CollectConstraintShapes( BOARD* aBoard )
446{
447 std::vector<PCB_SHAPE*> shapes;
448
449 if( !aBoard )
450 return shapes;
451
452 for( BOARD_ITEM* item : aBoard->Drawings() )
453 {
454 if( PCB_SHAPE* shape = dynamic_cast<PCB_SHAPE*>( item ); shape && aBoard->IsLayerVisible( shape->GetLayer() ) )
455 {
456 shapes.push_back( shape );
457 }
458 }
459
460 for( FOOTPRINT* footprint : aBoard->Footprints() )
461 {
462 for( BOARD_ITEM* item : footprint->GraphicalItems() )
463 {
464 if( PCB_SHAPE* shape = dynamic_cast<PCB_SHAPE*>( item );
465 shape && aBoard->IsLayerVisible( shape->GetLayer() ) )
466 {
467 shapes.push_back( shape );
468 }
469 }
470 }
471
472 return shapes;
473}
474
475
476std::vector<BOARD_ITEM*> CollectConstrainableItems( BOARD* aBoard )
477{
478 std::vector<BOARD_ITEM*> items;
479
480 if( !aBoard )
481 return items;
482
483 auto collect =
484 [&]( const auto& aContainer )
485 {
486 for( BOARD_ITEM* item : aContainer )
487 {
488 if( ( item->Type() == PCB_SHAPE_T || dynamic_cast<PCB_DIMENSION_BASE*>( item ) )
489 && aBoard->IsLayerVisible( item->GetLayer() ) )
490 {
491 items.push_back( item );
492 }
493 }
494 };
495
496 collect( aBoard->Drawings() );
497
498 for( FOOTPRINT* footprint : aBoard->Footprints() )
499 collect( footprint->GraphicalItems() );
500
501 return items;
502}
503
504
505std::optional<CONSTRAINT_MEMBER> NearestConstraintAnchor( BOARD* aBoard, const VECTOR2I& aPos,
506 double aMaxDist,
507 const std::vector<CONSTRAINT_MEMBER>& aExclude )
508{
509 double best = aMaxDist;
510 std::optional<CONSTRAINT_MEMBER> result;
511
512 for( BOARD_ITEM* item : CollectConstrainableItems( aBoard ) )
513 {
514 for( const CONSTRAINT_ANCHOR_POINT& a : ConstraintItemAnchors( item ) )
515 {
516 CONSTRAINT_MEMBER candidate( item->m_Uuid, a.anchor, a.index );
517
518 // Skip already picked handle so distinct coincident endpoint stays reachable
519 if( alg::contains( aExclude, candidate ) )
520 continue;
521
522 double dist = ( a.pos - aPos ).EuclideanNorm();
523
524 if( dist <= best )
525 {
526 best = dist;
527 result = candidate;
528 }
529 }
530 }
531
532 return result;
533}
534
535
536std::vector<ENDPOINT_BINDING> SelectEndpointBindings( BOARD* aBoard, const KIID& aItem, const VECTOR2I& aStart,
537 const std::optional<VECTOR2I>& aEnd, double aMaxDist )
538{
539 std::vector<ENDPOINT_BINDING> bindings;
540
541 if( !aBoard )
542 return bindings;
543
544 // Best pair of distinct anchors on one item within aMaxDist minimizing summed distance
545 // Distinct anchors required or endpoints merge and pairs judged jointly not per end nearest
546 using ANCHOR_PAIR = std::pair<CONSTRAINT_MEMBER, CONSTRAINT_MEMBER>;
547
548 auto bestPairOn = [&]( BOARD_ITEM* aCandidate ) -> std::optional<std::pair<ANCHOR_PAIR, double>>
549 {
550 std::vector<CONSTRAINT_ANCHOR_POINT> anchors = ConstraintItemAnchors( aCandidate );
551
552 // Sum decomposes per endpoint best and runner up END anchors computed once serve every
553 // START candidate keeps a dense polygon linear in vertex count instead of quadratic
554 const size_t none = anchors.size();
555 size_t bestEnd = none;
556 size_t secondEnd = none;
557 std::vector<double> dEnd( anchors.size(), 0.0 );
558
559 for( size_t j = 0; j < anchors.size(); ++j )
560 {
561 dEnd[j] = ( anchors[j].pos - *aEnd ).EuclideanNorm();
562
563 if( dEnd[j] > aMaxDist )
564 continue;
565
566 if( bestEnd == none || dEnd[j] < dEnd[bestEnd] )
567 {
568 secondEnd = bestEnd;
569 bestEnd = j;
570 }
571 else if( secondEnd == none || dEnd[j] < dEnd[secondEnd] )
572 {
573 secondEnd = j;
574 }
575 }
576
577 if( bestEnd == none )
578 return std::nullopt;
579
580 std::optional<std::pair<ANCHOR_PAIR, double>> best;
581
582 for( size_t i = 0; i < anchors.size(); ++i )
583 {
584 double dStart = ( anchors[i].pos - aStart ).EuclideanNorm();
585
586 if( dStart > aMaxDist )
587 continue;
588
589 size_t j = ( i == bestEnd ) ? secondEnd : bestEnd;
590
591 if( j == none )
592 continue;
593
594 double sum = dStart + dEnd[j];
595
596 if( !best || sum < best->second )
597 {
598 best = std::make_pair(
599 ANCHOR_PAIR{ CONSTRAINT_MEMBER( aCandidate->m_Uuid, anchors[i].anchor, anchors[i].index ),
600 CONSTRAINT_MEMBER( aCandidate->m_Uuid, anchors[j].anchor, anchors[j].index ) },
601 sum );
602 }
603 }
604
605 return best;
606 };
607
608 // Prefer single object reaching both endpoints so a single feature dimension stays bound at
609 // both ends
610 if( aEnd )
611 {
612 std::optional<ANCHOR_PAIR> bestPair;
613 double bestSum = 0.0;
614
615 for( BOARD_ITEM* item : CollectConstrainableItems( aBoard ) )
616 {
617 if( item->m_Uuid == aItem )
618 continue;
619
620 auto pair = bestPairOn( item );
621
622 if( !pair )
623 continue;
624
625 if( !bestPair || pair->second < bestSum )
626 {
627 bestSum = pair->second;
628 bestPair = pair->first;
629 }
630 }
631
632 if( bestPair )
633 {
634 bindings.push_back( { CONSTRAINT_ANCHOR::START, bestPair->first } );
635 bindings.push_back( { CONSTRAINT_ANCHOR::END, bestPair->second } );
636 return bindings;
637 }
638 }
639
640 // Else bind each endpoint to its own nearest anchor the two may land on different objects
641 // and either may find nothing
642 std::vector<CONSTRAINT_MEMBER> exclude{ { aItem, CONSTRAINT_ANCHOR::START }, { aItem, CONSTRAINT_ANCHOR::END } };
643
644 if( auto startTarget = NearestConstraintAnchor( aBoard, aStart, aMaxDist, exclude ) )
645 {
646 bindings.push_back( { CONSTRAINT_ANCHOR::START, *startTarget } );
647 exclude.push_back( *startTarget );
648 }
649
650 if( aEnd )
651 {
652 if( auto endTarget = NearestConstraintAnchor( aBoard, *aEnd, aMaxDist, exclude ) )
653 bindings.push_back( { CONSTRAINT_ANCHOR::END, *endTarget } );
654 }
655
656 return bindings;
657}
658
659
661{
662 if( !aBoard )
663 return nullptr;
664
665 BOARD_ITEM* item = aBoard->ResolveItem( aId, true );
666
667 return item && ( item->Type() == PCB_SHAPE_T || dynamic_cast<PCB_DIMENSION_BASE*>( item ) )
668 ? item
669 : nullptr;
670}
671
672
673std::optional<KIID> NearestOutlineShape( BOARD* aBoard, const VECTOR2I& aPos, double aMaxDist, bool aAllowCircle )
674{
675 double best = aMaxDist;
676 std::optional<KIID> result;
677
678 for( PCB_SHAPE* shape : CollectConstraintShapes( aBoard ) )
679 {
680 const SHAPE_T shapeType = shape->GetShape();
681 double dist = 0;
682
683 if( shapeType == SHAPE_T::SEGMENT )
684 {
685 dist = SEG( shape->GetStart(), shape->GetEnd() ).Distance( aPos );
686 }
687 else if( aAllowCircle && ( shapeType == SHAPE_T::CIRCLE || shapeType == SHAPE_T::ARC ) )
688 {
689 dist = std::abs( ( aPos - shape->GetCenter() ).EuclideanNorm() - shape->GetRadius() );
690 }
691 else if( aAllowCircle && ( shapeType == SHAPE_T::ELLIPSE || shapeType == SHAPE_T::ELLIPSE_ARC ) )
692 {
693 // Radial distance to the outline at the click's polar angle in the ellipse frame.
694 // Not the exact outline distance, but exact on the outline, which is all a snap needs.
695 double a = shape->GetEllipseMajorRadius();
696 double b = shape->GetEllipseMinorRadius();
697 double phi = shape->GetEllipseRotation().AsRadians();
698 VECTOR2D d = VECTOR2D( aPos - shape->GetEllipseCenter() );
699 double lx = d.x * std::cos( phi ) + d.y * std::sin( phi );
700 double ly = -d.x * std::sin( phi ) + d.y * std::cos( phi );
701 double r = std::hypot( lx, ly );
702
703 if( a <= 0 || b <= 0 )
704 continue;
705
706 double theta = std::atan2( ly, lx );
707 double re = a * b / std::hypot( b * std::cos( theta ), a * std::sin( theta ) );
708
709 dist = std::abs( r - re );
710 }
711 else
712 {
713 continue;
714 }
715
716 if( dist <= best )
717 {
718 best = dist;
719 result = shape->m_Uuid;
720 }
721 }
722
723 return result;
724}
725
726
727std::vector<CONSTRAINT_ANCHOR_POINT> ConstraintItemAnchors( const BOARD_ITEM* aItem )
728{
729 if( !aItem )
730 return {};
731
732 if( aItem->Type() == PCB_SHAPE_T )
733 return ConstraintShapeAnchors( static_cast<const PCB_SHAPE*>( aItem ) );
734
735 if( const PCB_DIMENSION_BASE* dim = dynamic_cast<const PCB_DIMENSION_BASE*>( aItem ) )
736 {
737 std::vector<CONSTRAINT_ANCHOR_POINT> anchors;
738 anchors.push_back( { CONSTRAINT_ANCHOR::START, dim->GetStart() } );
739
740 // Only aligned/orthogonal/radial dimensions have a second measured feature point; a leader
741 // or centre mark's second point is a control point.
742 switch( aItem->Type() )
743 {
746 case PCB_DIM_RADIAL_T:
747 anchors.push_back( { CONSTRAINT_ANCHOR::END, dim->GetEnd() } );
748 break;
749
750 default:
751 break;
752 }
753
754 return anchors;
755 }
756
757 return {};
758}
759
760
761std::optional<VECTOR2I> ConstraintAnchorPosition( BOARD* aBoard, const CONSTRAINT_MEMBER& aMember )
762{
764 {
765 // VERTEX anchor needs its ordinal too else every vertex member resolves to vertex 0
766 if( a.anchor == aMember.m_anchor
767 && ( a.anchor != CONSTRAINT_ANCHOR::VERTEX || a.index == aMember.m_index ) )
768 {
769 return a.pos;
770 }
771 }
772
773 return std::nullopt;
774}
775
776
777double InitialConstraintValue( PCB_CONSTRAINT_TYPE aType, double aMeasured,
778 const std::map<PCB_CONSTRAINT_TYPE, double>& aRemembered )
779{
780 auto it = aRemembered.find( aType );
781
782 return it != aRemembered.end() ? it->second : aMeasured;
783}
784
785
786std::optional<KIID> NearestConstrainedShape( const std::vector<PCB_SHAPE*>& aCandidates,
787 const VECTOR2I& aPos, int aMaxDist )
788{
789 auto it = std::ranges::find_if( aCandidates,
790 [&]( const PCB_SHAPE* aShape )
791 {
792 return aShape && aShape->HitTest( aPos, aMaxDist );
793 } );
794
795 return it == aCandidates.end() ? std::nullopt : std::optional<KIID>( ( *it )->m_Uuid );
796}
797
798
799std::optional<KIID> SelectRadialDimensionTarget( BOARD* aBoard, const KIID& aDimension,
800 const VECTOR2I& aCenter, const VECTOR2I& aRim,
801 double aMaxDist )
802{
803 if( !aBoard )
804 return std::nullopt;
805
806 std::optional<KIID> best;
807 double bestErr = 0.0;
808
809 for( PCB_SHAPE* shape : CollectConstraintShapes( aBoard ) )
810 {
811 if( shape->m_Uuid == aDimension || !isCircleOrArc( shape ) )
812 continue;
813
814 // Centre and rim must land on the same circle or arc centre and circumference or else a
815 // radial dimension over unrelated geometry would bind spuriously
816 std::optional<VECTOR2I> centerPos;
817
818 for( const CONSTRAINT_ANCHOR_POINT& a : ConstraintShapeAnchors( shape ) )
819 {
820 if( a.anchor == CONSTRAINT_ANCHOR::CENTER )
821 centerPos = a.pos;
822 }
823
824 if( !centerPos )
825 continue;
826
827 double centerErr = ( *centerPos - aCenter ).EuclideanNorm();
828
829 if( centerErr > aMaxDist )
830 continue;
831
832 double rimErr = std::abs( ( aRim - *centerPos ).EuclideanNorm() - shape->GetRadius() );
833
834 if( rimErr > aMaxDist )
835 continue;
836
837 // Arc outline is swept portion only not the whole circle so a rim point off the arc must
838 // not bind
839 if( shape->GetShape() == SHAPE_T::ARC && !shape->HitTest( aRim, KiROUND( aMaxDist ) ) )
840 continue;
841
842 double err = centerErr + rimErr;
843
844 if( !best || err < bestErr )
845 {
846 bestErr = err;
847 best = shape->m_Uuid;
848 }
849 }
850
851 return best;
852}
853
854
855bool DimensionEndpointsBound( BOARD* aBoard, const PCB_DIMENSION_BASE* aDimension )
856{
857 if( !aBoard || !aDimension )
858 return false;
859
860 const CONSTRAINT_MEMBER startMember( aDimension->m_Uuid, CONSTRAINT_ANCHOR::START );
861 const CONSTRAINT_MEMBER endMember( aDimension->m_Uuid, CONSTRAINT_ANCHOR::END );
862
863 auto anyConstraint = [&]( const auto& aMatch )
864 {
865 if( std::ranges::any_of( aBoard->Constraints(), aMatch ) )
866 return true;
867
868 // Bindings are parented to the owning dimension footprint not necessarily the first so
869 // every footprint must be scanned to match the write path
870 return std::ranges::any_of( aBoard->Footprints(),
871 [&]( const FOOTPRINT* aFootprint )
872 { return std::ranges::any_of( aFootprint->Constraints(), aMatch ); } );
873 };
874
875 // Radial dimension binds centre coincident plus rim on outline of one circle or arc
876 // Legs on different objects or an object that cannot play the radius role never offer Driving
877 if( aDimension->Type() == PCB_DIM_RADIAL_T )
878 {
879 auto rimOnItem = [&]( const KIID& aItem )
880 {
881 return anyConstraint(
882 [&]( const PCB_CONSTRAINT* aConstraint )
883 {
885 return false;
886
887 // Point on line binding is asymmetric the dimension rim point is member 0
888 // and the object outline WHOLE anchor is member 1
889 const std::vector<CONSTRAINT_MEMBER>& members = aConstraint->GetMembers();
890
891 return members.size() == 2 && members[0] == endMember
892 && members[1] == CONSTRAINT_MEMBER( aItem, CONSTRAINT_ANCHOR::WHOLE );
893 } );
894 };
895
896 return anyConstraint(
897 [&]( const PCB_CONSTRAINT* aConstraint )
898 {
900 return false;
901
902 const std::vector<CONSTRAINT_MEMBER>& members = aConstraint->GetMembers();
903
904 if( members.size() != 2 )
905 return false;
906
907 // Authored dimension first but coincident is symmetric so accept either order
908 const CONSTRAINT_MEMBER* target = nullptr;
909
910 if( members[0] == startMember )
911 target = &members[1];
912 else if( members[1] == startMember )
913 target = &members[0];
914
915 if( !target || target->m_anchor != CONSTRAINT_ANCHOR::CENTER )
916 return false;
917
918 BOARD_ITEM* item = ResolveConstrainableItem( aBoard, target->m_item );
919
920 return item && isCircleOrArc( item ) && rimOnItem( target->m_item );
921 } );
922 }
923
924 // Aligned or orthogonal needs a coincident per endpoint whose target still resolves a target
925 // pointing at a deleted item or a stale vertex index does not count
926 auto hasCoincident = [&]( const CONSTRAINT_MEMBER& aMember )
927 {
928 return anyConstraint(
929 [&]( const PCB_CONSTRAINT* aConstraint )
930 {
932 return false;
933
934 const std::vector<CONSTRAINT_MEMBER>& members = aConstraint->GetMembers();
935
936 // Must pair with a distinct target not itself
937 if( members.size() != 2 || members[0].m_item == members[1].m_item )
938 return false;
939
940 if( members[0] == aMember )
941 return ConstraintAnchorPosition( aBoard, members[1] ).has_value();
942
943 return members[1] == aMember
944 && ConstraintAnchorPosition( aBoard, members[0] ).has_value();
945 } );
946 };
947
948 return hasCoincident( startMember ) && hasCoincident( endMember );
949}
950
951
953{
954 if( !aDimension )
955 return false;
956
957 switch( aDimension->Type() )
958 {
961 case PCB_DIM_RADIAL_T:
962 return true;
963
964 default:
965 return false;
966 }
967}
968
969
971{
972 if( !aBoard || !aDimension )
973 return nullptr;
974
975 const CONSTRAINT_MEMBER startMember( aDimension->m_Uuid, CONSTRAINT_ANCHOR::START );
976 const CONSTRAINT_MEMBER endMember( aDimension->m_Uuid, CONSTRAINT_ANCHOR::END );
977
978 auto matches = [&]( const PCB_CONSTRAINT* aConstraint )
979 {
980 if( aConstraint->GetConstraintType() != PCB_CONSTRAINT_TYPE::FIXED_LENGTH )
981 return false;
982
983 const std::vector<CONSTRAINT_MEMBER>& members = aConstraint->GetMembers();
984
985 return members.size() == 2
986 && ( ( members[0] == startMember && members[1] == endMember )
987 || ( members[0] == endMember && members[1] == startMember ) );
988 };
989
990 auto scan = [&]( const CONSTRAINTS& aList ) -> PCB_CONSTRAINT*
991 {
992 auto it = std::ranges::find_if( aList, matches );
993 return it != aList.end() ? *it : nullptr;
994 };
995
996 if( PCB_CONSTRAINT* c = scan( aBoard->Constraints() ) )
997 return c;
998
999 // Driving length is parented to the owning dimension footprint not necessarily the first so
1000 // scan every footprint to match the write
1001 for( FOOTPRINT* footprint : aBoard->Footprints() )
1002 {
1003 if( PCB_CONSTRAINT* c = scan( footprint->Constraints() ) )
1004 return c;
1005 }
1006
1007 return nullptr;
1008}
1009
1010
1011bool DimensionCanDrive( BOARD* aBoard, const PCB_DIMENSION_BASE* aDimension )
1012{
1013 if( !DimensionHasValueMode( aDimension ) )
1014 return false;
1015
1016 if( DimensionEndpointsBound( aBoard, aDimension ) )
1017 return true;
1018
1019 PCB_CONSTRAINT* existing = FindDimensionLengthConstraint( aBoard, aDimension );
1020
1021 return existing && existing->IsDriving();
1022}
1023
1024
1026{
1027 PCB_CONSTRAINT* lengthConstraint = FindDimensionLengthConstraint( aBoard, aDimension );
1028
1029 if( lengthConstraint && lengthConstraint->IsDriving() )
1031
1032 if( aDimension && aDimension->GetOverrideTextEnabled() )
1034
1036}
1037
1038
1040 std::optional<int> aDrivingLengthIU,
1041 const std::optional<wxString>& aOverrideText,
1042 const std::function<void( BOARD_ITEM* )>& aBeforeModify,
1043 const std::function<void( BOARD_ITEM* )>& aStageAdd,
1044 const std::function<void( BOARD_ITEM* )>& aBeforeRemove )
1045{
1046 if( !aBoard || !DimensionHasValueMode( aDimension ) )
1047 return nullptr;
1048
1049 PCB_CONSTRAINT* existing = FindDimensionLengthConstraint( aBoard, aDimension );
1050
1051 if( aMode == DIM_VALUE_MODE::DRIVING )
1052 {
1053 // Unbound dimension has no geometry to drive and a non positive length would collapse the
1054 // constraint so the transition rejects with the board untouched
1055 if( !aDrivingLengthIU || *aDrivingLengthIU <= 0 || !DimensionCanDrive( aBoard, aDimension ) )
1056 return nullptr;
1057
1058 aBeforeModify( aDimension );
1059 aDimension->SetOverrideTextEnabled( false );
1060 aDimension->Update();
1061
1062 if( existing )
1063 {
1064 aBeforeModify( existing );
1065 existing->SetValue( *aDrivingLengthIU );
1066 existing->SetDriving( true );
1067 return existing;
1068 }
1069
1070 BOARD_ITEM* parent = aDimension->GetParentFootprint()
1071 ? static_cast<BOARD_ITEM*>( aDimension->GetParentFootprint() )
1072 : static_cast<BOARD_ITEM*>( aBoard );
1073
1074 auto constraint = std::make_unique<PCB_CONSTRAINT>( parent, PCB_CONSTRAINT_TYPE::FIXED_LENGTH );
1075 constraint->AddMember( aDimension->m_Uuid, CONSTRAINT_ANCHOR::START );
1076 constraint->AddMember( aDimension->m_Uuid, CONSTRAINT_ANCHOR::END );
1077 constraint->SetValue( *aDrivingLengthIU );
1078 constraint->SetDriving( true );
1079
1080 PCB_CONSTRAINT* added = constraint.get();
1081 aStageAdd( constraint.release() );
1082 return added;
1083 }
1084
1085 aBeforeModify( aDimension );
1086 aDimension->SetOverrideTextEnabled( aMode == DIM_VALUE_MODE::ARBITRARY );
1087
1088 if( aMode == DIM_VALUE_MODE::ARBITRARY && aOverrideText )
1089 aDimension->SetOverrideText( *aOverrideText );
1090
1091 aDimension->Update();
1092
1093 // Driven and Arbitrary both measure geometry natively so any driving length is dropped
1094 if( existing )
1095 aBeforeRemove( existing );
1096
1097 return nullptr;
1098}
1099
1100
1101void RemapPolygonVertexMembers( BOARD* aBoard, const KIID& aPoly, int aChangedIndex, int aDelta,
1102 const std::function<void( BOARD_ITEM* )>& aBeforeModify,
1103 const std::function<void( BOARD_ITEM* )>& aBeforeRemove )
1104{
1105 if( !aBoard || aDelta == 0 )
1106 return;
1107
1108 auto remapIn = [&]( const CONSTRAINTS& aConstraints )
1109 {
1110 for( PCB_CONSTRAINT* constraint : aConstraints )
1111 {
1112 bool shifts = false;
1113 bool doomed = false;
1114
1115 for( const CONSTRAINT_MEMBER& member : constraint->GetMembers() )
1116 {
1117 if( member.m_item != aPoly || member.m_anchor != CONSTRAINT_ANCHOR::VERTEX )
1118 continue;
1119
1120 if( aDelta < 0 && member.m_index == aChangedIndex )
1121 doomed = true;
1122 else if( member.m_index >= aChangedIndex )
1123 shifts = true;
1124 }
1125
1126 // Deleted vertex drags its bound member down and no fixed arity solver form survives
1127 // losing one so the whole constraint retires left unedited the staged removal image
1128 // keeps the authored members for undo
1129 if( doomed )
1130 {
1131 aBeforeRemove( constraint );
1132 continue;
1133 }
1134
1135 if( !shifts )
1136 continue;
1137
1138 aBeforeModify( constraint );
1139
1140 for( CONSTRAINT_MEMBER& member : constraint->Members() )
1141 {
1142 if( member.m_item == aPoly && member.m_anchor == CONSTRAINT_ANCHOR::VERTEX
1143 && member.m_index >= aChangedIndex )
1144 {
1145 member.m_index += aDelta;
1146 }
1147 }
1148 }
1149 };
1150
1151 remapIn( aBoard->Constraints() );
1152
1153 for( FOOTPRINT* footprint : aBoard->Footprints() )
1154 remapIn( footprint->Constraints() );
1155}
1156
1157
1158bool ConstraintIsDuplicateOnBoard( BOARD* aBoard, const PCB_CONSTRAINT* aConstraint )
1159{
1160 auto scan = [&]( const CONSTRAINTS& aList )
1161 {
1162 return std::ranges::any_of( aList,
1163 [&]( const PCB_CONSTRAINT* aExisting )
1164 {
1165 return ConstraintsAreDuplicate( *aExisting, *aConstraint );
1166 } );
1167 };
1168
1169 if( scan( aBoard->Constraints() ) )
1170 return true;
1171
1172 return std::ranges::any_of( aBoard->Footprints(),
1173 [&]( FOOTPRINT* aFootprint )
1174 {
1175 return scan( aFootprint->Constraints() );
1176 } );
1177}
1178
1179
1180namespace
1181{
1182// Tuning knobs for draw time auto constraints
1183// The bind tolerance accepts only exact landings while the corridor also captures near misses
1184constexpr double AUTO_BIND_TOL_MM = 0.01;
1185constexpr double AUTO_CORRIDOR_MM = 0.25;
1186constexpr double AUTO_TANGENT_TOL_DEG = 10.0;
1187
1188
1189// Tangent direction of a segment or circular shape at aPos or nullopt for other kinds
1190std::optional<double> tangentDirAt( const PCB_SHAPE* aShape, const VECTOR2I& aPos )
1191{
1192 switch( aShape->GetShape() )
1193 {
1194 case SHAPE_T::SEGMENT:
1195 {
1196 VECTOR2D dir( aShape->GetEnd() - aShape->GetStart() );
1197
1198 if( dir.EuclideanNorm() == 0 )
1199 return std::nullopt;
1200
1201 return std::atan2( dir.y, dir.x );
1202 }
1203
1204 case SHAPE_T::ARC:
1205 case SHAPE_T::CIRCLE:
1206 {
1207 VECTOR2D radial( aPos - aShape->GetCenter() );
1208
1209 if( radial.EuclideanNorm() == 0 )
1210 return std::nullopt;
1211
1212 return std::atan2( radial.y, radial.x ) + M_PI / 2;
1213 }
1214
1215 default: return std::nullopt;
1216 }
1217}
1218
1219
1220// A drawn shape meeting aTarget within a few degrees of tangency gets a tangent constraint
1221// The target is the first member so the snap solve moves the drawn shape not the board
1222std::unique_ptr<PCB_CONSTRAINT> makeTangent( const PCB_SHAPE* aShape, const PCB_SHAPE* aTarget, const VECTOR2I& aPos,
1223 BOARD_ITEM* aParent )
1224{
1225 const double tangentTol = AUTO_TANGENT_TOL_DEG * M_PI / 180.0;
1226
1227 bool curveInvolved = aShape->GetShape() == SHAPE_T::ARC || aTarget->GetShape() == SHAPE_T::ARC
1228 || aTarget->GetShape() == SHAPE_T::CIRCLE;
1229
1230 if( !curveInvolved )
1231 return nullptr;
1232
1233 std::optional<double> myDir = tangentDirAt( aShape, aPos );
1234 std::optional<double> otherDir = tangentDirAt( aTarget, aPos );
1235
1236 if( !myDir || !otherDir )
1237 return nullptr;
1238
1239 double diff = std::fabs( std::fmod( *myDir - *otherDir, M_PI ) );
1240 diff = std::min( diff, M_PI - diff );
1241
1242 if( diff > tangentTol )
1243 return nullptr;
1244
1245 auto constraint = std::make_unique<PCB_CONSTRAINT>( aParent, PCB_CONSTRAINT_TYPE::TANGENT );
1246 constraint->AddMember( aTarget->m_Uuid, CONSTRAINT_ANCHOR::WHOLE );
1247 constraint->AddMember( aShape->m_Uuid, CONSTRAINT_ANCHOR::WHOLE );
1248
1249 return constraint;
1250}
1251
1252
1253// Append unless an equal constraint exists on the board or already in this batch
1254// One draw can touch the same target twice so the batch check matters
1255void addUnlessDuplicate( BOARD* aBoard, std::vector<AUTO_CONSTRAINT>& aResult,
1256 std::unique_ptr<PCB_CONSTRAINT> aConstraint, bool aNeedsSolve )
1257{
1258 if( ConstraintIsDuplicateOnBoard( aBoard, aConstraint.get() ) )
1259 return;
1260
1261 if( std::ranges::any_of( aResult,
1262 [&]( const AUTO_CONSTRAINT& aEntry )
1263 {
1264 return ConstraintsAreDuplicate( *aEntry.constraint, *aConstraint );
1265 } ) )
1266 {
1267 return;
1268 }
1269
1270 aResult.push_back( { std::move( aConstraint ), aNeedsSolve } );
1271}
1272
1273
1274// A circle or closed ellipse has no endpoints so only its centre binds
1275// Another curve centre reads as concentric an anchor coincides and an outline holds the centre
1276std::vector<AUTO_CONSTRAINT> selectCenterBindings( BOARD* aBoard, const PCB_SHAPE* aShape, BOARD_ITEM* aParent )
1277{
1278 std::vector<AUTO_CONSTRAINT> result;
1279
1280 const double tol = pcbIUScale.mmToIU( AUTO_BIND_TOL_MM );
1281 VECTOR2I center = aShape->GetCenter();
1282
1283 std::vector<CONSTRAINT_MEMBER> exclude = { { aShape->m_Uuid, CONSTRAINT_ANCHOR::CENTER } };
1284
1285 if( std::optional<CONSTRAINT_MEMBER> target = NearestConstraintAnchor( aBoard, center, tol, exclude ) )
1286 {
1287 std::unique_ptr<PCB_CONSTRAINT> constraint;
1288
1289 if( target->m_anchor == CONSTRAINT_ANCHOR::CENTER )
1290 {
1291 constraint = std::make_unique<PCB_CONSTRAINT>( aParent, PCB_CONSTRAINT_TYPE::CONCENTRIC );
1292 constraint->AddMember( target->m_item, CONSTRAINT_ANCHOR::WHOLE );
1293 constraint->AddMember( aShape->m_Uuid, CONSTRAINT_ANCHOR::WHOLE );
1294 }
1295 else
1296 {
1297 constraint = std::make_unique<PCB_CONSTRAINT>( aParent, PCB_CONSTRAINT_TYPE::COINCIDENT );
1298 constraint->AddMember( aShape->m_Uuid, CONSTRAINT_ANCHOR::CENTER );
1299 constraint->AddMember( target->m_item, target->m_anchor, target->m_index );
1300 }
1301
1302 addUnlessDuplicate( aBoard, result, std::move( constraint ), false );
1303 }
1304 else if( std::optional<KIID> outline = NearestOutlineShape( aBoard, center, tol, true ) )
1305 {
1306 if( *outline != aShape->m_Uuid )
1307 {
1308 auto constraint = std::make_unique<PCB_CONSTRAINT>( aParent, PCB_CONSTRAINT_TYPE::POINT_ON_LINE );
1309 constraint->AddMember( aShape->m_Uuid, CONSTRAINT_ANCHOR::CENTER );
1310 constraint->AddMember( *outline, CONSTRAINT_ANCHOR::WHOLE );
1311
1312 addUnlessDuplicate( aBoard, result, std::move( constraint ), false );
1313 }
1314 }
1315
1316 return result;
1317}
1318
1319
1320// Endpoints landing on existing anchors bind coincident
1321void selectEndpointCoincidents( BOARD* aBoard, const PCB_SHAPE* aShape, BOARD_ITEM* aParent,
1322 std::vector<AUTO_CONSTRAINT>& aResult, std::set<CONSTRAINT_ANCHOR>& aBound,
1323 std::vector<std::pair<VECTOR2I, KIID>>& aTouches )
1324{
1325 const double tol = pcbIUScale.mmToIU( AUTO_BIND_TOL_MM );
1326
1327 std::vector<ENDPOINT_BINDING> bindings =
1328 SelectEndpointBindings( aBoard, aShape->m_Uuid, aShape->GetStart(), aShape->GetEnd(), tol );
1329
1330 for( const ENDPOINT_BINDING& binding : bindings )
1331 {
1332 auto constraint = std::make_unique<PCB_CONSTRAINT>( aParent, PCB_CONSTRAINT_TYPE::COINCIDENT );
1333 constraint->AddMember( aShape->m_Uuid, binding.sourceAnchor );
1334 constraint->AddMember( binding.target.m_item, binding.target.m_anchor, binding.target.m_index );
1335
1336 aBound.insert( binding.sourceAnchor );
1337
1338 VECTOR2I pos = binding.sourceAnchor == CONSTRAINT_ANCHOR::START ? aShape->GetStart() : aShape->GetEnd();
1339 aTouches.emplace_back( pos, binding.target.m_item );
1340
1341 addUnlessDuplicate( aBoard, aResult, std::move( constraint ), false );
1342 }
1343}
1344
1345
1346// An endpoint with no anchor to coincide with but sitting on an outline binds point on line
1347// Landing on a segment midpoint means the midpoint snap was used so bind midpoint instead
1348void selectOutlineFallbacks( BOARD* aBoard, const PCB_SHAPE* aShape, BOARD_ITEM* aParent,
1349 std::vector<AUTO_CONSTRAINT>& aResult, const std::set<CONSTRAINT_ANCHOR>& aBound,
1350 std::vector<std::pair<VECTOR2I, KIID>>& aTouches )
1351{
1352 const double tol = pcbIUScale.mmToIU( AUTO_BIND_TOL_MM );
1353
1355 {
1356 if( aBound.contains( anchor ) )
1357 continue;
1358
1359 VECTOR2I pos = anchor == CONSTRAINT_ANCHOR::START ? aShape->GetStart() : aShape->GetEnd();
1360 std::optional<KIID> target = NearestOutlineShape( aBoard, pos, tol, true );
1361
1362 if( !target || *target == aShape->m_Uuid )
1363 continue;
1364
1366 PCB_SHAPE* targetShape = dynamic_cast<PCB_SHAPE*>( aBoard->ResolveItem( *target, true ) );
1367
1368 if( targetShape && targetShape->GetShape() == SHAPE_T::SEGMENT )
1369 {
1370 VECTOR2I mid = targetShape->GetStart() + ( targetShape->GetEnd() - targetShape->GetStart() ) / 2;
1371
1372 if( ( pos - mid ).EuclideanNorm() <= tol )
1374 }
1375
1376 auto constraint = std::make_unique<PCB_CONSTRAINT>( aParent, type );
1377 constraint->AddMember( aShape->m_Uuid, anchor );
1378 constraint->AddMember( *target, CONSTRAINT_ANCHOR::WHOLE );
1379
1380 aTouches.emplace_back( pos, *target );
1381
1382 addUnlessDuplicate( aBoard, aResult, std::move( constraint ), false );
1383 }
1384}
1385
1386
1387// A segment drawn through or near an existing shape anchor pins that point on the new segment
1388// Near misses within the corridor bind too and the post push solve pulls the line onto them
1389void selectCorridorPins( BOARD* aBoard, const PCB_SHAPE* aShape, BOARD_ITEM* aParent,
1390 std::vector<AUTO_CONSTRAINT>& aResult )
1391{
1392 if( aShape->GetShape() != SHAPE_T::SEGMENT || aShape->GetStart() == aShape->GetEnd() )
1393 return;
1394
1395 const double tol = pcbIUScale.mmToIU( AUTO_BIND_TOL_MM );
1396 const double corridor = pcbIUScale.mmToIU( AUTO_CORRIDOR_MM );
1397
1398 SEG span( aShape->GetStart(), aShape->GetEnd() );
1399 std::vector<VECTOR2I> boundPos;
1400
1401 for( BOARD_ITEM* item : CollectConstrainableItems( aBoard ) )
1402 {
1403 if( item->m_Uuid == aShape->m_Uuid || item->Type() != PCB_SHAPE_T )
1404 continue;
1405
1407 {
1408 if( span.Distance( anchor.pos ) > corridor )
1409 continue;
1410
1411 // The drawn endpoints already bound above so only true mid span hits count
1412 if( ( anchor.pos - aShape->GetStart() ).EuclideanNorm() <= corridor
1413 || ( anchor.pos - aShape->GetEnd() ).EuclideanNorm() <= corridor )
1414 {
1415 continue;
1416 }
1417
1418 // Chained corners stack two anchors on one spot and one binding is enough
1419 if( std::ranges::any_of( boundPos,
1420 [&]( const VECTOR2I& aP )
1421 {
1422 return ( anchor.pos - aP ).EuclideanNorm() <= tol;
1423 } ) )
1424 {
1425 continue;
1426 }
1427
1428 boundPos.push_back( anchor.pos );
1429
1430 auto constraint = std::make_unique<PCB_CONSTRAINT>( aParent, PCB_CONSTRAINT_TYPE::POINT_ON_LINE );
1431 constraint->AddMember( item->m_Uuid, anchor.anchor, anchor.index );
1432 constraint->AddMember( aShape->m_Uuid, CONSTRAINT_ANCHOR::WHOLE );
1433
1434 addUnlessDuplicate( aBoard, aResult, std::move( constraint ), true );
1435 }
1436 }
1437}
1438
1439
1440// Tangents where the drawn shape touched another shape near tangency
1441void selectTangents( BOARD* aBoard, const PCB_SHAPE* aShape, BOARD_ITEM* aParent, std::vector<AUTO_CONSTRAINT>& aResult,
1442 const std::vector<std::pair<VECTOR2I, KIID>>& aTouches )
1443{
1444 for( const auto& [pos, id] : aTouches )
1445 {
1446 PCB_SHAPE* target = dynamic_cast<PCB_SHAPE*>( aBoard->ResolveItem( id, true ) );
1447
1448 if( target && target != aShape )
1449 {
1450 if( std::unique_ptr<PCB_CONSTRAINT> tangent = makeTangent( aShape, target, pos, aParent ) )
1451 addUnlessDuplicate( aBoard, aResult, std::move( tangent ), true );
1452 }
1453 }
1454}
1455
1456
1457// A segment drawn in a constrained line mode keeps its axis
1458void selectAxisConstraint( BOARD* aBoard, const PCB_SHAPE* aShape, BOARD_ITEM* aParent,
1459 std::vector<AUTO_CONSTRAINT>& aResult )
1460{
1461 if( aShape->GetShape() != SHAPE_T::SEGMENT || aShape->GetStart() == aShape->GetEnd() )
1462 return;
1463
1464 bool horizontal = aShape->GetStart().y == aShape->GetEnd().y;
1465 bool vertical = aShape->GetStart().x == aShape->GetEnd().x;
1466
1467 if( !horizontal && !vertical )
1468 return;
1469
1470 auto constraint = std::make_unique<PCB_CONSTRAINT>( aParent, horizontal ? PCB_CONSTRAINT_TYPE::HORIZONTAL
1472 constraint->AddMember( aShape->m_Uuid, CONSTRAINT_ANCHOR::WHOLE );
1473
1474 addUnlessDuplicate( aBoard, aResult, std::move( constraint ), false );
1475}
1476} // namespace
1477
1478
1479std::vector<AUTO_CONSTRAINT> SelectShapeAutoConstraints( BOARD* aBoard, const PCB_SHAPE* aShape, BOARD_ITEM* aParent,
1480 bool aAxisConstraint )
1481{
1482 std::vector<AUTO_CONSTRAINT> result;
1483
1484 if( !aBoard || !aShape )
1485 return result;
1486
1487 if( aShape->GetShape() == SHAPE_T::CIRCLE || aShape->GetShape() == SHAPE_T::ELLIPSE )
1488 return selectCenterBindings( aBoard, aShape, aParent );
1489
1490 std::set<CONSTRAINT_ANCHOR> bound;
1491 std::vector<std::pair<VECTOR2I, KIID>> touches;
1492
1493 selectEndpointCoincidents( aBoard, aShape, aParent, result, bound, touches );
1494 selectOutlineFallbacks( aBoard, aShape, aParent, result, bound, touches );
1495 selectCorridorPins( aBoard, aShape, aParent, result );
1496 selectTangents( aBoard, aShape, aParent, result, touches );
1497
1498 if( aAxisConstraint )
1499 selectAxisConstraint( aBoard, aShape, aParent, result );
1500
1501 return result;
1502}
constexpr EDA_IU_SCALE pcbIUScale
Definition base_units.h:121
constexpr BOX2I KiROUND(const BOX2D &aBoxD)
Definition box2.h:995
A base class for any item which can be embedded within the BOARD container class, and therefore insta...
Definition board_item.h:84
FOOTPRINT * GetParentFootprint() const
Information pertinent to a Pcbnew printed circuit board.
Definition board.h:409
const FOOTPRINTS & Footprints() const
Definition board.h:463
const CONSTRAINTS & Constraints() const
Geometric constraints (#2329) owned by this board.
Definition board.h:513
bool IsLayerVisible(PCB_LAYER_ID aLayer) const
A proxy function that calls the correspondent function in m_BoardSettings tests whether a given layer...
Definition board.cpp:1189
BOARD_ITEM * ResolveItem(const KIID &aID, bool aAllowNullptrReturn=false) const
Definition board.cpp:2114
const DRAWINGS & Drawings() const
Definition board.h:465
double AsDegrees() const
Definition eda_angle.h:116
const KIID m_Uuid
Definition eda_item.h:587
KICAD_T Type() const
Returns the type of object.
Definition eda_item.h:110
SHAPE_POLY_SET & GetPolyShape()
SHAPE_T GetShape() const
Definition eda_shape.h:175
const VECTOR2I & GetEnd() const
Return the ending point of the graphic.
Definition eda_shape.h:325
const VECTOR2I & GetStart() const
Return the starting point of the graphic.
Definition eda_shape.h:275
Definition kiid.h:46
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:160
Definition seg.h:38
VECTOR2I A
Definition seg.h:45
VECTOR2I::extended_type ecoord
Definition seg.h:40
VECTOR2I B
Definition seg.h:46
int Distance(const SEG &aSeg) const
Compute minimum Euclidean distance to segment aSeg.
Definition seg.cpp:709
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 a visible layer (drawings plus footprint graphics) – the candidates constraints ca...
std::vector< AUTO_CONSTRAINT > SelectShapeAutoConstraints(BOARD *aBoard, const PCB_SHAPE *aShape, BOARD_ITEM *aParent, bool aAxisConstraint)
Choose the constraints a freshly drawn shape should get from what its features landed on.
std::vector< ENDPOINT_BINDING > SelectEndpointBindings(BOARD *aBoard, const KIID &aItem, const VECTOR2I &aStart, const std::optional< VECTOR2I > &aEnd, double aMaxDist)
Choose the coincident bindings a freshly drawn item endpoints should take so it tracks the geometry i...
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...
wxString ConstraintSelectionHint(PCB_CONSTRAINT_TYPE aType)
A sentence naming what aType needs selected, for the moment a selection does not fit it and the tool ...
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 > NearestOutlineShape(BOARD *aBoard, const VECTOR2I &aPos, double aMaxDist, bool aAllowCircle)
The shape whose outline is nearest aPos within aMaxDist.
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 a visible layer – 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...
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 (...
bool ConstraintIsDuplicateOnBoard(BOARD *aBoard, const PCB_CONSTRAINT *aConstraint)
True if the board or one of its footprints already carries an equal constraint.
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...
#define _(s)
SHAPE_T
Definition eda_shape.h:54
@ ELLIPSE
Definition eda_shape.h:62
@ SEGMENT
Definition eda_shape.h:56
@ RECTANGLE
Use RECTANGLE instead of RECT to avoid collision in a Windows header.
Definition eda_shape.h:57
@ ELLIPSE_ARC
Definition eda_shape.h:63
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:411
bool ConstraintsAreDuplicate(const PCB_CONSTRAINT &aA, const PCB_CONSTRAINT &aB)
True if two constraints express the same relation, meaning the same type and the same members compare...
wxString ConstraintTypeLabel(PCB_CONSTRAINT_TYPE aType)
Human-readable name of a constraint type (e.g. "Parallel"), for menus and lists.
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.
PCB_CONSTRAINT_TYPE
The geometric relationship a PCB_CONSTRAINT enforces between its members.
@ CONCENTRIC
Two arcs/circles share a center.
@ SYMMETRIC
Two points are mirror images about an axis.
@ FIXED_POSITION
A point is locked at its current location.
@ VERTICAL
A segment (or two points) is vertical.
@ TANGENT
A line and a curve, or two curves, touch tangentially.
@ COINCIDENT
Two points are made to coincide.
@ PERPENDICULAR
Two segments are perpendicular.
@ FIXED_RADIUS
An arc/circle has a driving radius value.
@ HORIZONTAL
A segment (or two points) is horizontal.
@ EQUAL_RADIUS
Two arcs/circles have equal radius.
@ MIDPOINT
A point is the midpoint of a segment.
@ POINT_ON_LINE
A point lies on a segment's supporting line.
@ FIXED_LENGTH
A segment has a driving length value.
@ ANGULAR_DIMENSION
An angle between members (driving or reference).
@ COLLINEAR
Two segments lie on the same line.
@ 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
One constraint chosen for a freshly drawn shape.
std::unique_ptr< PCB_CONSTRAINT > constraint
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.
One of a drawn item feature points bound coincident to an object anchor by draw time auto constrain s...
std::vector< double > vA
VECTOR2I center
wxString result
Test unit parsing edge cases and error handling.
#define M_PI
@ PCB_SHAPE_T
class PCB_SHAPE, a segment not on copper layers
Definition typeinfo.h:80
@ PCB_DIM_ORTHOGONAL_T
class PCB_DIM_ORTHOGONAL, a linear dimension constrained to x/y
Definition typeinfo.h:98
@ PCB_DIM_ALIGNED_T
class PCB_DIM_ALIGNED, a linear dimension (graphic item)
Definition typeinfo.h:94
@ PCB_DIM_RADIAL_T
class PCB_DIM_RADIAL, a radius or diameter dimension
Definition typeinfo.h:97
VECTOR2< int32_t > VECTOR2I
Definition vector2d.h:683
VECTOR2< double > VECTOR2D
Definition vector2d.h:682