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