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