KiCad PCB EDA Suite
Loading...
Searching...
No Matches
board_constraint_adapter.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 <iterator>
24#include <ranges>
25#include <cmath>
26#include <cstdint>
27#include <cstring>
28#include <functional>
29#include <limits>
30#include <set>
31#include <unordered_map>
32#include <unordered_set>
33
34#include <wx/debug.h>
35
36#include <board.h>
37#include <footprint.h>
38#include <geometry/seg.h>
39#include <math/util.h>
40#include <pcb_dimension.h>
41#include <pcb_shape.h>
42
44
45#include <GCS.h>
46
47
48// IU are nanometres; one normalized unit is one millimetre (1e6 IU). Squaring raw IU in the
49// residuals (~1e16) is badly conditioned, so the cluster is solved in this millimetre frame.
50static constexpr double IU_PER_NORM_UNIT = 1e6;
51
52// Bound the solver so an interactive drag can never stall the UI thread.
53static constexpr int MAX_SOLVE_ITERATIONS = 100;
54
55// Shares the drag and stabilize temporary subsystem kept far weaker so it only fills the null space
56// and never fights a real drive or cursor pin
57static constexpr double STAY_PUT_WEIGHT = 1e-4;
58static constexpr double CURSOR_WEIGHT = 1e-2;
59
60
61// Every constraint owned by the board or by any of its footprints. In the footprint editor the
62// "board" is a footprint holder, so footprint-scoped constraints must be gathered too.
63static std::vector<PCB_CONSTRAINT*> collectAllConstraints( BOARD* aBoard )
64{
65 std::vector<PCB_CONSTRAINT*> all( aBoard->Constraints().begin(), aBoard->Constraints().end() );
66
67 for( FOOTPRINT* footprint : aBoard->Footprints() )
68 all.insert( all.end(), footprint->Constraints().begin(), footprint->Constraints().end() );
69
70 return all;
71}
72
73
75{
76 if( !aBoard )
77 return false;
78
79 if( !aBoard->Constraints().empty() )
80 return true;
81
82 return std::ranges::any_of( aBoard->Footprints(),
83 []( const FOOTPRINT* aFootprint )
84 { return !aFootprint->Constraints().empty(); } );
85}
86
87
88std::vector<VECTOR2I> ConstraintPinnedPositions( BOARD* aBoard, const std::set<KIID>& aItems )
89{
90 std::vector<VECTOR2I> positions;
91
92 if( !aBoard )
93 return positions;
94
95 for( const PCB_CONSTRAINT* constraint : collectAllConstraints( aBoard ) )
96 {
97 if( constraint->GetConstraintType() != PCB_CONSTRAINT_TYPE::FIXED_POSITION )
98 continue;
99
100 for( const CONSTRAINT_MEMBER& member : constraint->GetMembers() )
101 {
102 if( !aItems.contains( member.m_item ) )
103 continue;
104
105 std::optional<VECTOR2I> pos = ConstraintAnchorPosition( aBoard, member );
106
107 if( pos && std::ranges::find( positions, *pos ) == positions.end() )
108 positions.push_back( *pos );
109 }
110 }
111
112 return positions;
113}
114
115
117{
118 if( !aItem )
119 return false;
120
121 if( aItem->IsLocked() )
122 return true;
123
124 const FOOTPRINT* parent = aItem->GetParentFootprint();
125
126 return parent && parent->IsLocked();
127}
128
129
130// A board-wide pass checks members against the board's ids first since a missing id costs a full
131// board scan in BOARD::ResolveItem()
132static BOARD_ITEM* resolveMember( BOARD* aBoard, const std::unordered_set<KIID>* aOnBoard, const KIID& aId )
133{
134 if( aOnBoard && !aOnBoard->contains( aId ) )
135 return nullptr;
136
137 return aBoard->ResolveItem( aId, true );
138}
139
140
141// Adjacency from a member KIID to the constraints touching it. When @p aErrored is given, any
142// constraint that cannot be satisfied -- it has no members, or a member that does not resolve to a
143// constrainable item (a shape or dimension) -- is recorded there (error state), since such a
144// constraint never reaches the solver's per-cluster mapping where the remaining error cases are caught.
145static std::unordered_map<KIID, std::vector<PCB_CONSTRAINT*>>
146buildShapeConstraintMap( BOARD* aBoard, const std::vector<PCB_CONSTRAINT*>& aConstraints,
147 std::vector<KIID>* aErrored = nullptr, const std::unordered_set<KIID>* aOnBoard = nullptr )
148{
149 std::unordered_map<KIID, std::vector<PCB_CONSTRAINT*>> map;
150
151 for( PCB_CONSTRAINT* constraint : aConstraints )
152 {
153 bool errored = constraint->GetMembers().empty();
154
155 for( const CONSTRAINT_MEMBER& member : constraint->GetMembers() )
156 {
157 map[member.m_item].push_back( constraint );
158
159 // Members must resolve to a constrainable item (shape or dimension); a deleted or
160 // wrong-typed item is an error.
161 if( aErrored && !IsConstrainableItem( resolveMember( aBoard, aOnBoard, member.m_item ) ) )
162 errored = true;
163 }
164
165 if( errored && aErrored )
166 aErrored->push_back( constraint->m_Uuid );
167 }
168
169 return map;
170}
171
172
173// Walk the connected component (shapes + constraints) that contains @p aSeed. Shapes reached are
174// added to @p aVisited when provided, so a caller iterating seeds can skip an already-walked one.
176 const std::unordered_map<KIID, std::vector<PCB_CONSTRAINT*>>& aMap, const KIID& aSeed,
177 std::unordered_set<KIID>& aClusterShapes, std::vector<PCB_CONSTRAINT*>& aClusterConstraints,
178 std::set<KIID>* aVisited = nullptr )
179{
180 std::set<KIID> used;
181 std::vector<KIID> frontier{ aSeed };
182 aClusterShapes.insert( aSeed );
183
184 while( !frontier.empty() )
185 {
186 KIID shapeId = frontier.back();
187 frontier.pop_back();
188
189 if( aVisited )
190 aVisited->insert( shapeId );
191
192 auto it = aMap.find( shapeId );
193
194 if( it == aMap.end() )
195 continue;
196
197 for( PCB_CONSTRAINT* constraint : it->second )
198 {
199 if( used.insert( constraint->m_Uuid ).second )
200 aClusterConstraints.push_back( constraint );
201
202 for( const CONSTRAINT_MEMBER& member : constraint->GetMembers() )
203 {
204 if( aClusterShapes.insert( member.m_item ).second )
205 frontier.push_back( member.m_item );
206 }
207 }
208 }
209}
210
211
212// Resolve a set of shape KIIDs to the live PCB_SHAPEs, dropping any that no longer exist.
213// Resolve a set of KIIDs to the live items of type T among them, dropping other kinds and deleted
214// items.
215template <typename T>
216static std::vector<T*> resolveClusterItems( BOARD* aBoard, const std::unordered_set<KIID>& aIds,
217 const std::unordered_set<KIID>* aOnBoard )
218{
219 std::vector<T*> items;
220
221 for( const KIID& id : aIds )
222 {
223 if( T* item = dynamic_cast<T*>( resolveMember( aBoard, aOnBoard, id ) ) )
224 items.push_back( item );
225 }
226
227 return items;
228}
229
230
231static std::vector<PCB_SHAPE*> resolveClusterShapes( BOARD* aBoard, const std::unordered_set<KIID>& aIds,
232 const std::unordered_set<KIID>* aOnBoard = nullptr )
233{
234 return resolveClusterItems<PCB_SHAPE>( aBoard, aIds, aOnBoard );
235}
236
237
238static std::vector<PCB_DIMENSION_BASE*> resolveClusterDimensions( BOARD* aBoard,
239 const std::unordered_set<KIID>& aIds,
240 const std::unordered_set<KIID>* aOnBoard = nullptr )
241{
242 return resolveClusterItems<PCB_DIMENSION_BASE>( aBoard, aIds, aOnBoard );
243}
244
245
246// The stored angular value is the undirected corner angle [0, 180]; planegcs drives the directed
247// angle from l1's direction (p1->p2) to l2's. Return the directed target (radians) that realizes
248// the corner for the present geometry.
249//
250// The corner rays run from the shared vertex outward, so the vertex's endpoint parity fixes how the
251// corner maps to the directed angle: corner = |Normalize180( theta + (vB - vA) * pi )|, where vA/vB
252// are the vertex endpoint indices (0 = p1, 1 = p2). Only two directed targets realize the corner
253// for that parity, { alpha, -alpha } shifted by (vB - vA) * pi; the one nearest the current directed
254// angle keeps the configuration rather than flipping to the mirror. Considering the other parity's
255// targets too (the naive four-candidate set) would let an obtuse->acute value edit pick the stale
256// complement and no-op the solve. Not shared with the arc-sweep mapping, which is mod 2*pi.
257// A zero-length line has no direction, so its angle equation is singular; skip mapping it.
258static bool isDegenerateLine( const GCS::Line& aLine )
259{
260 return std::hypot( *aLine.p2.x - *aLine.p1.x, *aLine.p2.y - *aLine.p1.y ) < 1e-9;
261}
262
263
264static double directedAngleForCorner( const GCS::Line& aL1, const GCS::Line& aL2, double aCornerDeg )
265{
266 const double ax[2] = { *aL1.p1.x, *aL1.p2.x };
267 const double ay[2] = { *aL1.p1.y, *aL1.p2.y };
268 const double bx[2] = { *aL2.p1.x, *aL2.p2.x };
269 const double by[2] = { *aL2.p1.y, *aL2.p2.y };
270
271 int vA = 0, vB = 0;
272 double best = std::numeric_limits<double>::max();
273
274 for( int i = 0; i < 2; ++i )
275 {
276 for( int j = 0; j < 2; ++j )
277 {
278 double dist = std::hypot( ax[i] - bx[j], ay[i] - by[j] );
279
280 if( dist < best )
281 {
282 best = dist;
283 vA = i;
284 vB = j;
285 }
286 }
287 }
288
289 double d1x = ax[1] - ax[0], d1y = ay[1] - ay[0];
290 double d2x = bx[1] - bx[0], d2y = by[1] - by[0];
291 double theta = std::atan2( d1x * d2y - d1y * d2x, d1x * d2x + d1y * d2y );
292
293 // Fold to [0, pi] so a value from an out-of-range file, API, or an earlier signed-angle board
294 // still maps to a well-defined corner.
295 double alpha = std::abs( std::remainder( aCornerDeg * M_PI / 180.0, 2.0 * M_PI ) );
296 double shift = ( vB - vA ) * M_PI;
297 double c1 = alpha - shift;
298 double c2 = -alpha - shift;
299
300 double d1 = std::abs( std::remainder( theta - c1, 2.0 * M_PI ) );
301 double d2 = std::abs( std::remainder( theta - c2, 2.0 * M_PI ) );
302
303 return d1 <= d2 ? c1 : c2;
304}
305
306
307// The stored value is the unsigned swept angle; the solver constrains the endpoints' polar
308// separation endAngle - startAngle. KiCad arcs are canonically positive-sweep (SetArcGeometry
309// swaps the ends so IsClockwiseArc is always false), so the target is +alpha. Return its
310// representative (mod 2*pi) nearest the current separation, so the solver only rotates an endpoint
311// rather than jumping the param a full turn. This is the arc's own mod-2*pi mapping, deliberately
312// not the line-angle mod-pi one.
313static double arcSweepTarget( double aStartAngle, double aEndAngle, double aSweepDeg )
314{
315 double current = aEndAngle - aStartAngle;
316 double alpha = std::abs( aSweepDeg ) * M_PI / 180.0;
317
318 return current - std::remainder( current - alpha, 2.0 * M_PI );
319}
320
321
323 m_gcs( &m_system.Solver() ),
324 m_params( m_system.ParameterStorage() )
325{
326}
327
328
332
333
335{
336 return m_system.AddParameter( aValue );
337}
338
339
341{
343 m_temporaryParams.push_back( pushParam( aValue ) );
344
346 m_params[index] = aValue;
347 return index;
348}
349
350
355
356
358{
359 if( aConstraint && !aConstraint->IsDriving() )
360 m_referenceConstraints.push_back( aConstraint );
361}
362
363
364void BOARD_CONSTRAINT_ADAPTER::ApplyReferenceValues( const std::function<void( BOARD_ITEM* )>& aBeforeWrite )
365{
366 if( !m_built )
367 return;
368
369 for( PCB_CONSTRAINT* constraint : m_referenceConstraints )
370 {
371 const std::vector<CONSTRAINT_MEMBER>& members = constraint->GetMembers();
372
373 if( members.empty() )
374 continue;
375
376 auto it = m_shapeVars.find( members.front().m_item );
377
378 if( it == m_shapeVars.end() )
379 continue;
380
381 const PCB_SHAPE* shape = it->second.shape;
382 std::optional<double> value;
383 double tol = 1.0; // IU for a length/radius; degrees for an angle
384
385 // Measure from the shape geometry Apply() just wrote, not the solver param, so a settled
386 // solve re-measures the same rounded value and never churns undo.
387 switch( constraint->GetConstraintType() )
388 {
390
391 // A two-point form has no owning segment so re-measure the distance between its two
392 // member anchors from the same solved params Apply rounded from
393 if( members.size() == 2 )
394 {
395 auto anchorPos = [&]( const CONSTRAINT_MEMBER& aMember ) -> std::optional<VECTOR2I>
396 {
397 ANCHOR_PARAMS anchor = anchorParams( aMember );
398
399 if( !anchor.IsValid() )
400 return std::nullopt;
401
404 };
405
406 std::optional<VECTOR2I> pa = anchorPos( members[0] );
407 std::optional<VECTOR2I> pb = anchorPos( members[1] );
408
409 if( pa && pb )
410 {
411 // An orthogonal dimension measures one axis so re-measure that axis component
412 // an aligned or radial two-point form measures the euclidean distance instead
414 {
415 bool horizontal = ortho->GetOrientation() == PCB_DIM_ORTHOGONAL::DIR::HORIZONTAL;
416 value = horizontal ? std::abs( pb->x - pa->x ) : std::abs( pb->y - pa->y );
417 }
418 else
419 {
420 value = ( *pb - *pa ).EuclideanNorm();
421 }
422 }
423 }
424 else
425 {
426 value = ( shape->GetEnd() - shape->GetStart() ).EuclideanNorm();
427 }
428
429 break;
430
432 value = shape->GetRadius();
433 break;
434
436 {
437 if( members.size() != 2 )
438 break;
439
440 auto other = m_shapeVars.find( members[1].m_item );
441
442 if( other == m_shapeVars.end() )
443 break;
444
445 const PCB_SHAPE* shapeB = other->second.shape;
446 value = MeasureCornerAngle( SEG( shape->GetStart(), shape->GetEnd() ),
447 SEG( shapeB->GetStart(), shapeB->GetEnd() ) )
448 .AsDegrees();
449 tol = 1e-3;
450 break;
451 }
452
454 value = shape->GetArcAngle().AsDegrees();
455 tol = 1e-3;
456 break;
457
458 default:
459 break;
460 }
461
462 if( !value )
463 continue;
464
465 std::optional<double> current = constraint->GetValue();
466
467 if( current && std::abs( *value - *current ) <= tol )
468 continue;
469
470 if( aBeforeWrite )
471 aBeforeWrite( constraint );
472
473 constraint->SetValue( value );
474 }
475}
476
477
479{
480 auto it = m_shapeVars.find( aMember.m_item );
481
482 if( it == m_shapeVars.end() )
483 return {};
484
485 const SHAPE_VARS& vars = it->second;
486
487 // Segment bezier and dimension point-pair store endpoints in startX and endX while a circle or
488 // closed ellipse parks its centre in startX instead so those kinds must never alias endpoints
489 bool hasEndpoints = vars.kind == SHAPE_KIND::SEGMENT || vars.kind == SHAPE_KIND::BEZIER
490 || vars.kind == SHAPE_KIND::POINT_PAIR;
491
492 // Consecutive x and y pair for these kinds
493 auto pairAt = []( int aXIndex ) -> ANCHOR_PARAMS
494 {
495 return aXIndex >= 0 ? ANCHOR_PARAMS{ aXIndex, aXIndex + 1 } : ANCHOR_PARAMS();
496 };
497
498 // A rect exposes only its four indexed corners each an alias over a mixed start and end pair so
499 // START END and CENTER never resolve indices 0 to 3 follow the canonical TL TR BR BL order
500 if( vars.kind == SHAPE_KIND::RECTANGLE )
501 {
502 if( aMember.m_anchor != CONSTRAINT_ANCHOR::VERTEX || aMember.m_index < 0 || aMember.m_index > 3 )
503 return {};
504
505 int leftX = vars.startIsLeft ? vars.startX : vars.endX;
506 int rightX = vars.startIsLeft ? vars.endX : vars.startX;
507 int topY = ( vars.startIsTop ? vars.startX : vars.endX ) + 1;
508 int botY = ( vars.startIsTop ? vars.endX : vars.startX ) + 1;
509
510 switch( aMember.m_index )
511 {
512 case 0: return { leftX, topY };
513 case 1: return { rightX, topY };
514 case 2: return { rightX, botY };
515 default: return { leftX, botY };
516 }
517 }
518
519 // A polygon exposes only its indexed outline-0 vertices each a consecutive pair at
520 // startX plus 2 times index a stale index from a shrunk outline resolves to nothing
521 if( vars.kind == SHAPE_KIND::POLYGON )
522 {
523 if( aMember.m_anchor != CONSTRAINT_ANCHOR::VERTEX || aMember.m_index < 0
524 || aMember.m_index >= vars.vertexCount )
525 {
526 return {};
527 }
528
529 return pairAt( vars.startX + 2 * aMember.m_index );
530 }
531
532 switch( aMember.m_anchor )
533 {
535 if( vars.kind == SHAPE_KIND::ARC || vars.kind == SHAPE_KIND::ELLIPSE_ARC )
536 return pairAt( vars.arcStartX );
537
538 return hasEndpoints ? pairAt( vars.startX ) : ANCHOR_PARAMS();
540 if( vars.kind == SHAPE_KIND::ARC || vars.kind == SHAPE_KIND::ELLIPSE_ARC )
541 return pairAt( vars.arcEndX );
542
543 return hasEndpoints ? pairAt( vars.endX ) : ANCHOR_PARAMS();
544 case CONSTRAINT_ANCHOR::CENTER: return hasEndpoints ? ANCHOR_PARAMS() : pairAt( vars.startX );
545 default: return {};
546 }
547}
548
549
551BOARD_CONSTRAINT_ADAPTER::orthogonalDimensionForMembers( const std::vector<CONSTRAINT_MEMBER>& aMembers ) const
552{
553 if( aMembers.size() != 2 || aMembers[0].m_item != aMembers[1].m_item )
554 return nullptr;
555
556 const CONSTRAINT_ANCHOR a0 = aMembers[0].m_anchor;
557 const CONSTRAINT_ANCHOR a1 = aMembers[1].m_anchor;
558
559 const bool startEnd = ( a0 == CONSTRAINT_ANCHOR::START && a1 == CONSTRAINT_ANCHOR::END )
561
562 if( !startEnd )
563 return nullptr;
564
565 auto it = m_shapeVars.find( aMembers[0].m_item );
566
567 if( it == m_shapeVars.end() || it->second.kind != SHAPE_KIND::POINT_PAIR || !it->second.dimension
568 || it->second.dimension->Type() != PCB_DIM_ORTHOGONAL_T )
569 {
570 return nullptr;
571 }
572
573 return static_cast<PCB_DIM_ORTHOGONAL*>( it->second.dimension );
574}
575
576
577bool BOARD_CONSTRAINT_ADAPTER::Build( const std::vector<PCB_SHAPE*>& aShapes,
578 const std::vector<PCB_CONSTRAINT*>& aConstraints,
579 const std::set<KIID>* aFixedShapes,
580 const std::vector<PCB_DIMENSION_BASE*>& aDimensions )
581{
582 m_system.Clear();
583 m_shapeVars.clear();
584 m_tagToConstraint.clear();
585 m_nonDrivingTags.clear();
586 m_tagMembers.clear();
588 m_unmapped.clear();
590 m_built = false;
591
592 // The temporary-parameter pool aliases indices into the parameter store just cleared above.
593 // Rebuilding without resetting it would hand out stale indices past the new store's end.
594 m_temporaryParams.clear();
596
597 if( aShapes.empty() && aDimensions.empty() )
598 return false;
599
600 // Centre on the first item's start point so normalized coordinates stay small for a board far
601 // from the origin.
602 VECTOR2I origin = !aShapes.empty() ? aShapes.front()->GetStart() : aDimensions.front()->GetStart();
604 m_invScale = 1.0 / m_scale;
605 m_system.SetCoordinateFrame( origin, m_scale );
606
607 // [first, last) parameter spans of locked shapes, folded into fixedParams below so the solver
608 // treats a locked shape as an immovable reference.
609 std::vector<std::pair<int, int>> lockedRanges;
610
611 // Fixed focus-offset params of ellipses, folded into fixedParams below.
612 std::vector<int> ellipseOffsetParams;
613
614 for( PCB_SHAPE* shape : aShapes )
615 {
616 SHAPE_VARS vars;
617 vars.shape = shape;
618 int firstParam = static_cast<int>( m_params.size() );
619
620 if( shape->GetShape() == SHAPE_T::SEGMENT )
621 {
623 vars.startX = pushParam( normalizeX( shape->GetStart().x ) );
624 pushParam( normalizeY( shape->GetStart().y ) );
625 vars.endX = pushParam( normalizeX( shape->GetEnd().x ) );
626 pushParam( normalizeY( shape->GetEnd().y ) );
627 }
628 else if( shape->GetShape() == SHAPE_T::RECTANGLE )
629 {
630 // Only the two stored corners are params the four VERTEX corners alias mixed pairs of
631 // them through anchorParams so rectness can never be violated corner roles frozen here
633 vars.startIsLeft = shape->GetStart().x <= shape->GetEnd().x;
634 vars.startIsTop = shape->GetStart().y <= shape->GetEnd().y;
635 vars.startX = pushParam( normalizeX( shape->GetStart().x ) );
636 pushParam( normalizeY( shape->GetStart().y ) );
637 vars.endX = pushParam( normalizeX( shape->GetEnd().x ) );
638 pushParam( normalizeY( shape->GetEnd().y ) );
639 }
640 else if( shape->GetShape() == SHAPE_T::POLY )
641 {
642 // Only a single hole-free arc-free outline is modeled since write-back rebuilds one
643 // outline and would destroy anything else so other polys are skipped and read unmapped
644 if( !ConstraintPolygonIsModelable( shape ) )
645 continue;
646
647 const SHAPE_LINE_CHAIN& outline = shape->GetPolyShape().COutline( 0 );
648
650 vars.vertexCount = outline.PointCount();
651 vars.startX = pushParam( normalizeX( outline.CPoint( 0 ).x ) );
652 pushParam( normalizeY( outline.CPoint( 0 ).y ) );
653
654 for( int i = 1; i < outline.PointCount(); ++i )
655 {
656 pushParam( normalizeX( outline.CPoint( i ).x ) );
657 pushParam( normalizeY( outline.CPoint( i ).y ) );
658 }
659 }
660 else if( shape->GetShape() == SHAPE_T::BEZIER )
661 {
662 // Only the endpoints participate in the solve the control handles are not solver
663 // variables and follow their adjacent endpoint in Apply
665 vars.startX = pushParam( normalizeX( shape->GetStart().x ) );
666 pushParam( normalizeY( shape->GetStart().y ) );
667 vars.endX = pushParam( normalizeX( shape->GetEnd().x ) );
668 pushParam( normalizeY( shape->GetEnd().y ) );
669 }
670 else if( shape->GetShape() == SHAPE_T::CIRCLE )
671 {
673 vars.startX = pushParam( normalizeX( shape->GetCenter().x ) );
674 pushParam( normalizeY( shape->GetCenter().y ) );
675 vars.radius = pushParam( shape->GetRadius() * m_invScale );
676 }
677 else if( shape->GetShape() == SHAPE_T::ARC )
678 {
679 vars.kind = SHAPE_KIND::ARC;
680
681 VECTOR2I center = shape->GetCenter();
682 VECTOR2I start = shape->GetStart();
683 VECTOR2I end = shape->GetEnd();
684
685 double cx = normalizeX( center.x );
686 double cy = normalizeY( center.y );
687 double sx = normalizeX( start.x );
688 double sy = normalizeY( start.y );
689 double ex = normalizeX( end.x );
690 double ey = normalizeY( end.y );
691
692 vars.startX = pushParam( cx );
693 pushParam( cy );
694 vars.radius = pushParam( shape->GetRadius() * m_invScale );
695 vars.arcStartX = pushParam( sx );
696 pushParam( sy );
697 vars.arcEndX = pushParam( ex );
698 pushParam( ey );
699 vars.startAngle = pushParam( std::atan2( sy - cy, sx - cx ) );
700 vars.endAngle = pushParam( std::atan2( ey - cy, ex - cx ) );
701
702 GCS::Arc arc;
703 arc.center = GCS::Point{ &m_params[vars.startX], &m_params[vars.startX + 1] };
704 arc.rad = &m_params[vars.radius];
705 arc.start = GCS::Point{ &m_params[vars.arcStartX], &m_params[vars.arcStartX + 1] };
706 arc.end = GCS::Point{ &m_params[vars.arcEndX], &m_params[vars.arcEndX + 1] };
707 arc.startAngle = &m_params[vars.startAngle];
708 arc.endAngle = &m_params[vars.endAngle];
709 m_gcs->addConstraintArcRules( arc );
710 }
711 else if( shape->GetShape() == SHAPE_T::ELLIPSE || shape->GetShape() == SHAPE_T::ELLIPSE_ARC )
712 {
714
715 VECTOR2I center = shape->GetEllipseCenter();
716 double major = shape->GetEllipseMajorRadius();
717 double minor = shape->GetEllipseMinorRadius();
718 double phi = shape->GetEllipseRotation().AsRadians();
719
720 // GCS parameterizes an ellipse as center + first focus + minor radius.
721 double focal = major > minor ? std::sqrt( major * major - minor * minor ) : 0.0;
722 double cx = normalizeX( center.x );
723 double cy = normalizeY( center.y );
724
725 vars.startX = pushParam( cx );
726 pushParam( cy );
727 vars.focusX = pushParam( cx + focal * m_invScale * std::cos( phi ) );
728 pushParam( cy + focal * m_invScale * std::sin( phi ) );
729 vars.radius = pushParam( std::min( major, minor ) * m_invScale );
730
731 // Tie the focus to the center, or a solve moving the center would leave the focus
732 // behind and distort the ellipse.
733 int offX = pushParam( focal * m_invScale * std::cos( phi ) );
734 int offY = pushParam( focal * m_invScale * std::sin( phi ) );
735 ellipseOffsetParams.push_back( offX );
736 ellipseOffsetParams.push_back( offY );
737
738 m_gcs->addConstraintDifference( &m_params[vars.startX], &m_params[vars.focusX], &m_params[offX] );
739 m_gcs->addConstraintDifference( &m_params[vars.startX + 1], &m_params[vars.focusX + 1], &m_params[offY] );
740
741 if( vars.kind == SHAPE_KIND::ELLIPSE_ARC )
742 {
743 VECTOR2I start = shape->GetStart();
744 VECTOR2I end = shape->GetEnd();
745
746 vars.arcStartX = pushParam( normalizeX( start.x ) );
747 pushParam( normalizeY( start.y ) );
748 vars.arcEndX = pushParam( normalizeX( end.x ) );
749 pushParam( normalizeY( end.y ) );
750 vars.startAngle = pushParam( shape->GetEllipseStartAngle().AsRadians() );
751 vars.endAngle = pushParam( shape->GetEllipseEndAngle().AsRadians() );
752
753 GCS::ArcOfEllipse arc;
754 arc.center = GCS::Point{ &m_params[vars.startX], &m_params[vars.startX + 1] };
755 arc.focus1 = GCS::Point{ &m_params[vars.focusX], &m_params[vars.focusX + 1] };
756 arc.radmin = &m_params[vars.radius];
757 arc.start = GCS::Point{ &m_params[vars.arcStartX], &m_params[vars.arcStartX + 1] };
758 arc.end = GCS::Point{ &m_params[vars.arcEndX], &m_params[vars.arcEndX + 1] };
759 arc.startAngle = &m_params[vars.startAngle];
760 arc.endAngle = &m_params[vars.endAngle];
761 m_gcs->addConstraintArcOfEllipseRules( arc );
762 }
763 }
764 else
765 {
766 continue; // other shapes are not mapped
767 }
768
769 // Freeze a locked or caller-pinned shape's whole span so the solver moves only the rest of
770 // the cluster.
771 if( ConstraintItemIsLocked( shape ) || ( aFixedShapes && aFixedShapes->count( shape->m_Uuid ) ) )
772 lockedRanges.emplace_back( firstParam, static_cast<int>( m_params.size() ) );
773
774 m_shapeVars[shape->m_Uuid] = vars;
775 }
776
777 // Each dimension contributes its two feature points, so a coincident constraint can pull the
778 // dimension along with the shape it is bound to.
779 for( PCB_DIMENSION_BASE* dimension : aDimensions )
780 {
781 SHAPE_VARS vars;
783 vars.dimension = dimension;
784 vars.startX = pushParam( normalizeX( dimension->GetStart().x ) );
785 pushParam( normalizeY( dimension->GetStart().y ) );
786
787 // Only aligned/orthogonal/radial dimensions have a second feature point; a leader or centre
788 // mark's end is a control point, so it is never a bindable anchor (endX stays -1).
789 switch( dimension->Type() )
790 {
793 case PCB_DIM_RADIAL_T:
794 vars.endX = pushParam( normalizeX( dimension->GetEnd().x ) );
795 pushParam( normalizeY( dimension->GetEnd().y ) );
796 break;
797
798 default:
799 break;
800 }
801
802 m_shapeVars[dimension->m_Uuid] = vars;
803 }
804
805 // Params the solver may not change. Grounded points, driving constants (lengths, radii) and
806 // locked-shape params go here; everything else stays an unknown.
807 std::set<int> fixedParams;
808
809 for( const auto& [first, last] : lockedRanges )
810 {
811 for( int i = first; i < last; ++i )
812 fixedParams.insert( i );
813 }
814
815 for( int i : ellipseOffsetParams )
816 fixedParams.insert( i );
817
818 auto pointAt = [&]( int aXIndex ) -> GCS::Point
819 {
820 return GCS::Point{ &m_params[aXIndex], &m_params[aXIndex + 1] };
821 };
822
823 // Anchor x and y need not be consecutive since a rect corner aliases mixed start end params
824 // pointAt stays for SHAPE_VARS internal consecutive pairs
825 auto pointFor = [&]( const ANCHOR_PARAMS& aParams ) -> GCS::Point
826 {
827 return GCS::Point{ &m_params[aParams.x], &m_params[aParams.y] };
828 };
829
830 auto lineFor = [&]( const CONSTRAINT_MEMBER& aMember, GCS::Line& aLine ) -> bool
831 {
832 auto it = m_shapeVars.find( aMember.m_item );
833
834 if( it == m_shapeVars.end() || it->second.kind != SHAPE_KIND::SEGMENT
835 || aMember.m_anchor != CONSTRAINT_ANCHOR::WHOLE )
836 {
837 return false;
838 }
839
840 aLine.p1 = pointAt( it->second.startX );
841 aLine.p2 = pointAt( it->second.endX );
842 return true;
843 };
844
845 // A circle for radial constraints; an arc is accepted too (its center + radius are shared
846 // with the Circle base, which is all addConstraintEqualRadius/CircleRadius/concentric need).
847 auto circleFor = [&]( const CONSTRAINT_MEMBER& aMember, GCS::Circle& aCircle ) -> bool
848 {
849 auto it = m_shapeVars.find( aMember.m_item );
850
851 if( it == m_shapeVars.end()
852 || ( it->second.kind != SHAPE_KIND::CIRCLE && it->second.kind != SHAPE_KIND::ARC ) )
853 {
854 return false;
855 }
856
857 aCircle.center = pointAt( it->second.startX );
858 aCircle.rad = &m_params[it->second.radius];
859 return true;
860 };
861
862 // An ellipse for ellipse-target constraints; an elliptical arc is accepted too (its center,
863 // focus and minor radius are shared with the Ellipse base).
864 auto ellipseFor = [&]( const CONSTRAINT_MEMBER& aMember, GCS::Ellipse& aEllipse ) -> bool
865 {
866 auto it = m_shapeVars.find( aMember.m_item );
867
868 if( it == m_shapeVars.end()
869 || ( it->second.kind != SHAPE_KIND::ELLIPSE && it->second.kind != SHAPE_KIND::ELLIPSE_ARC ) )
870 {
871 return false;
872 }
873
874 aEllipse.center = pointAt( it->second.startX );
875 aEllipse.focus1 = pointAt( it->second.focusX );
876 aEllipse.radmin = &m_params[it->second.radius];
877 return true;
878 };
879
880 // Push a driving constant (length/radius), normalized from IU, returning its stable index.
881 auto pushConstant = [&]( double aIU ) -> int
882 {
883 int idx = pushParam( aIU * m_invScale );
884 fixedParams.insert( idx );
885 return idx;
886 };
887
888 int tag = 1;
889
890 for( PCB_CONSTRAINT* constraint : aConstraints )
891 {
892 const std::vector<CONSTRAINT_MEMBER>& members = constraint->GetMembers();
893 bool mapped = false;
894
895 switch( constraint->GetConstraintType() )
896 {
898 {
899 GCS::Line l1, l2;
900
901 if( members.size() == 2 && lineFor( members[0], l1 ) && lineFor( members[1], l2 ) )
902 {
903 m_gcs->addConstraintParallel( l1, l2, tag );
904 mapped = true;
905 }
906
907 break;
908 }
909
911 {
912 // One whole segment aligns its own two endpoints while two point anchors align the pair
913 // the user picked so a corner can be leveled without a segment between the points
914 if( members.size() == 1 )
915 {
916 GCS::Line l;
917
918 if( lineFor( members[0], l ) )
919 {
920 m_gcs->addConstraintHorizontal( l, tag );
921 mapped = true;
922 }
923 }
924 else if( members.size() == 2 )
925 {
926 ANCHOR_PARAMS a = anchorParams( members[0] );
927 ANCHOR_PARAMS b = anchorParams( members[1] );
928
929 if( a.IsValid() && b.IsValid() )
930 {
931 GCS::Point p1 = pointFor( a );
932 GCS::Point p2 = pointFor( b );
933 m_gcs->addConstraintHorizontal( p1, p2, tag );
934 mapped = true;
935 }
936 }
937
938 break;
939 }
940
942 {
943 if( members.size() == 1 )
944 {
945 GCS::Line l;
946
947 if( lineFor( members[0], l ) )
948 {
949 m_gcs->addConstraintVertical( l, tag );
950 mapped = true;
951 }
952 }
953 else if( members.size() == 2 )
954 {
955 ANCHOR_PARAMS a = anchorParams( members[0] );
956 ANCHOR_PARAMS b = anchorParams( members[1] );
957
958 if( a.IsValid() && b.IsValid() )
959 {
960 GCS::Point p1 = pointFor( a );
961 GCS::Point p2 = pointFor( b );
962 m_gcs->addConstraintVertical( p1, p2, tag );
963 mapped = true;
964 }
965 }
966
967 break;
968 }
969
971 {
972 ANCHOR_PARAMS a = members.size() == 2 ? anchorParams( members[0] ) : ANCHOR_PARAMS();
973 ANCHOR_PARAMS b = members.size() == 2 ? anchorParams( members[1] ) : ANCHOR_PARAMS();
974
975 if( a.IsValid() && b.IsValid() )
976 {
977 GCS::Point p1 = pointFor( a );
978 GCS::Point p2 = pointFor( b );
979 m_gcs->addConstraintP2PCoincident( p1, p2, tag );
980 mapped = true;
981 }
982
983 break;
984 }
985
987 {
988 ANCHOR_PARAMS a = members.size() == 1 ? anchorParams( members[0] ) : ANCHOR_PARAMS();
989
990 if( a.IsValid() )
991 {
992 fixedParams.insert( a.x );
993 fixedParams.insert( a.y );
994 mapped = true; // enforced by omission from the unknowns, not a solver constraint
995 }
996
997 break;
998 }
999
1001 {
1002 GCS::Line l1, l2;
1003
1004 if( members.size() == 2 && lineFor( members[0], l1 ) && lineFor( members[1], l2 ) )
1005 {
1006 m_gcs->addConstraintPerpendicular( l1, l2, tag );
1007 mapped = true;
1008 }
1009
1010 break;
1011 }
1012
1014 {
1015 GCS::Line l1, l2;
1016
1017 if( members.size() == 2 && lineFor( members[0], l1 ) && lineFor( members[1], l2 ) )
1018 {
1019 m_gcs->addConstraintEqualLength( l1, l2, tag );
1020 mapped = true;
1021 }
1022
1023 break;
1024 }
1025
1027 {
1028 ANCHOR_PARAMS p = members.size() == 2 ? anchorParams( members[0] ) : ANCHOR_PARAMS();
1029 GCS::Line l;
1030 GCS::Circle circ;
1031 GCS::Ellipse ell;
1032
1033 if( p.IsValid() && lineFor( members[1], l ) )
1034 {
1035 GCS::Point point = pointFor( p );
1036 m_gcs->addConstraintPointOnLine( point, l, tag );
1037 mapped = true;
1038 }
1039 else if( p.IsValid() && circleFor( members[1], circ ) )
1040 {
1041 // A circle or arc target keeps the point on its circumference.
1042 GCS::Point point = pointFor( p );
1043 m_gcs->addConstraintPointOnCircle( point, circ, tag );
1044 mapped = true;
1045 }
1046 else if( p.IsValid() && ellipseFor( members[1], ell ) )
1047 {
1048 GCS::Point point = pointFor( p );
1049 m_gcs->addConstraintPointOnEllipse( point, ell, tag );
1050 mapped = true;
1051 }
1052
1053 break;
1054 }
1055
1057 {
1058 ANCHOR_PARAMS p = members.size() == 2 ? anchorParams( members[0] ) : ANCHOR_PARAMS();
1059 GCS::Line seg;
1060
1061 // A midpoint constraint is the segment's endpoints being symmetric about the point.
1062 if( p.IsValid() && lineFor( members[1], seg ) )
1063 {
1064 GCS::Point mid = pointFor( p );
1065 m_gcs->addConstraintP2PSymmetric( seg.p1, seg.p2, mid, tag );
1066 mapped = true;
1067 }
1068
1069 break;
1070 }
1071
1073 {
1074 GCS::Line l1, l2;
1075
1076 // Both endpoints of the second segment lie on the first's supporting line.
1077 if( members.size() == 2 && lineFor( members[0], l1 ) && lineFor( members[1], l2 ) )
1078 {
1079 m_gcs->addConstraintPointOnLine( l2.p1, l1, tag );
1080 m_gcs->addConstraintPointOnLine( l2.p2, l1, tag );
1081 mapped = true;
1082 }
1083
1084 break;
1085 }
1086
1088 {
1089 ANCHOR_PARAMS a = members.size() == 3 ? anchorParams( members[0] ) : ANCHOR_PARAMS();
1090 ANCHOR_PARAMS b = members.size() == 3 ? anchorParams( members[1] ) : ANCHOR_PARAMS();
1091 GCS::Line axis;
1092
1093 if( a.IsValid() && b.IsValid() && lineFor( members[2], axis ) )
1094 {
1095 GCS::Point pa = pointFor( a );
1096 GCS::Point pb = pointFor( b );
1097 m_gcs->addConstraintP2PSymmetric( pa, pb, axis, tag );
1098 mapped = true;
1099 }
1100
1101 break;
1102 }
1103
1105 {
1106 GCS::Line l;
1107
1108 if( members.size() == 1 && constraint->HasValue() && lineFor( members[0], l ) )
1109 {
1110 int len = pushConstant( *constraint->GetValue() );
1111 m_gcs->addConstraintP2PDistance( l.p1, l.p2, &m_params[len], tag, constraint->IsDriving() );
1112 recordReferenceValue( constraint );
1113
1114 if( constraint->IsDriving() )
1115 {
1116 if( auto it = m_shapeVars.find( members[0].m_item ); it != m_shapeVars.end() )
1117 it->second.fixedLengthParam = len;
1118 }
1119
1120 mapped = true;
1121 }
1122 // Two point anchors fix the distance between them with no segment between so a driving
1123 // aligned dimension can drive its own endpoints and the geometry coincident with them
1124 else if( members.size() == 2 && constraint->HasValue() )
1125 {
1126 ANCHOR_PARAMS a = anchorParams( members[0] );
1127 ANCHOR_PARAMS b = anchorParams( members[1] );
1128
1129 if( a.IsValid() && b.IsValid() )
1130 {
1132
1133 if( ortho )
1134 {
1135 // An orthogonal dimension measures one axis so the driving length fixes that
1136 // axis and matches updateGeometry sign convention keeping the current side
1137 bool horiz = ortho->GetOrientation() == PCB_DIM_ORTHOGONAL::DIR::HORIZONTAL;
1138 int aAxis = horiz ? a.x : a.y;
1139 int bAxis = horiz ? b.x : b.y;
1140 double gap = m_params[bAxis] - m_params[aAxis];
1141 double sign = gap >= 0.0 ? 1.0 : -1.0;
1142 int len = pushConstant( sign * *constraint->GetValue() );
1143
1144 m_gcs->addConstraintDifference( &m_params[aAxis], &m_params[bAxis],
1145 &m_params[len], tag, constraint->IsDriving() );
1146 }
1147 else
1148 {
1149 GCS::Point pa = pointFor( a );
1150 GCS::Point pb = pointFor( b );
1151 int len = pushConstant( *constraint->GetValue() );
1152 m_gcs->addConstraintP2PDistance( pa, pb, &m_params[len], tag,
1153 constraint->IsDriving() );
1154 }
1155
1156 recordReferenceValue( constraint );
1157 mapped = true;
1158 }
1159 }
1160
1161 break;
1162 }
1163
1165 {
1166 GCS::Circle c;
1167
1168 if( members.size() == 1 && constraint->HasValue() && circleFor( members[0], c ) )
1169 {
1170 int rad = pushConstant( *constraint->GetValue() );
1171 m_gcs->addConstraintCircleRadius( c, &m_params[rad], tag, constraint->IsDriving() );
1172 recordReferenceValue( constraint );
1173 mapped = true;
1174 }
1175
1176 break;
1177 }
1178
1180 {
1181 GCS::Circle c1, c2;
1182
1183 if( members.size() == 2 && circleFor( members[0], c1 ) && circleFor( members[1], c2 ) )
1184 {
1185 m_gcs->addConstraintEqualRadius( c1, c2, tag );
1186 mapped = true;
1187 }
1188
1189 break;
1190 }
1191
1193 {
1194 // Any center-bearing shape can be concentric, ellipses included.
1195 auto centerOf = [&]( const CONSTRAINT_MEMBER& aMember, GCS::Point& aOut ) -> bool
1196 {
1197 GCS::Circle c;
1198 GCS::Ellipse e;
1199
1200 if( circleFor( aMember, c ) )
1201 {
1202 aOut = c.center;
1203 return true;
1204 }
1205
1206 if( ellipseFor( aMember, e ) )
1207 {
1208 aOut = e.center;
1209 return true;
1210 }
1211
1212 return false;
1213 };
1214
1215 GCS::Point p1, p2;
1216
1217 if( members.size() == 2 && centerOf( members[0], p1 ) && centerOf( members[1], p2 ) )
1218 {
1219 m_gcs->addConstraintP2PCoincident( p1, p2, tag );
1220 mapped = true;
1221 }
1222
1223 break;
1224 }
1225
1227 {
1228 GCS::Line l1, l2;
1229
1230 if( members.size() == 2 && constraint->HasValue() && lineFor( members[0], l1 )
1231 && lineFor( members[1], l2 ) && !isDegenerateLine( l1 ) && !isDegenerateLine( l2 ) )
1232 {
1233 int angle = pushParam( directedAngleForCorner( l1, l2, *constraint->GetValue() ) );
1234 fixedParams.insert( angle );
1235 m_gcs->addConstraintL2LAngle( l1, l2, &m_params[angle], tag, constraint->IsDriving() );
1236 recordReferenceValue( constraint );
1237 mapped = true;
1238 }
1239
1240 break;
1241 }
1242
1244 {
1245 auto vit = members.size() == 1 ? m_shapeVars.find( members[0].m_item ) : m_shapeVars.end();
1246
1247 // A value outside (0, 360) is a degenerate sweep (from a corrupt file or the API); leave
1248 // it unmapped so it reads as errored rather than merging the endpoints.
1249 bool validSweep = constraint->HasValue() && *constraint->GetValue() > 0.0
1250 && *constraint->GetValue() < 360.0;
1251
1252 if( vit != m_shapeVars.end() && vit->second.kind == SHAPE_KIND::ARC && validSweep )
1253 {
1254 const SHAPE_VARS& vars = vit->second;
1255 double target = arcSweepTarget( m_params[vars.startAngle], m_params[vars.endAngle],
1256 *constraint->GetValue() );
1257 int tgt = pushParam( target );
1258 fixedParams.insert( tgt );
1259 m_gcs->addConstraintDifference( &m_params[vars.startAngle], &m_params[vars.endAngle],
1260 &m_params[tgt], tag, constraint->IsDriving() );
1261 recordReferenceValue( constraint );
1262 mapped = true;
1263 }
1264
1265 break;
1266 }
1267
1269 {
1270 if( members.size() != 2 )
1271 break;
1272
1273 GCS::Line l;
1274 GCS::Circle c1, c2;
1275 GCS::Ellipse ell;
1276
1277 // The members may come in either order.
1278 int lineIdx = lineFor( members[0], l ) ? 0 : ( lineFor( members[1], l ) ? 1 : -1 );
1279
1280 if( lineIdx >= 0 )
1281 {
1282 const CONSTRAINT_MEMBER& other = members[lineIdx == 0 ? 1 : 0];
1283
1284 if( circleFor( other, c1 ) )
1285 {
1286 // Keep the circle on the side of the line it is on now.
1287 double dx = *l.p2.x - *l.p1.x;
1288 double dy = *l.p2.y - *l.p1.y;
1289 double cross = dx * ( *c1.center.y - *l.p1.y ) - dy * ( *c1.center.x - *l.p1.x );
1290
1291 m_gcs->addConstraintTangent( l, c1, cross > 0.0, tag );
1292 mapped = true;
1293 }
1294 else if( ellipseFor( other, ell ) )
1295 {
1296 m_gcs->addConstraintTangent( l, ell, tag );
1297 mapped = true;
1298 }
1299 }
1300 else if( circleFor( members[0], c1 ) && circleFor( members[1], c2 ) )
1301 {
1302 m_gcs->addConstraintTangent( c1, c2, tag );
1303 mapped = true;
1304 }
1305
1306 break;
1307 }
1308
1309 default:
1310 break; // UNDEFINED and point-anchored families handled elsewhere
1311 }
1312
1313 // An unmappable constraint (wrong member count/kind for its type) is skipped so it cannot
1314 // disable solving for the whole connected cluster; the remaining constraints still solve.
1315 // It is recorded so the caller can flag it as errored rather than dropping it silently.
1316 if( !mapped )
1317 {
1318 m_unmapped.push_back( constraint->m_Uuid );
1319 continue;
1320 }
1321
1322 // A fixed position is enforced by grounding, not by a solver constraint
1323 if( constraint->GetConstraintType() != PCB_CONSTRAINT_TYPE::FIXED_POSITION )
1324 m_tagToConstraint[tag] = constraint->m_Uuid;
1325
1326 if( !constraint->IsDriving() )
1327 m_nonDrivingTags.insert( tag );
1328
1329 for( const CONSTRAINT_MEMBER& member : constraint->GetMembers() )
1330 m_tagMembers[tag].push_back( member.m_item );
1331
1332 // Remember shapes a direction or angle constraint could shrink to a point so the stabilize
1333 // solve length-holds just those and leaves dragged-along neighbours free to grow
1334 switch( constraint->GetConstraintType() )
1335 {
1338
1339 // The two-point form aligns loose anchors not the owning segments lengths so only the
1340 // whole-segment form marks its member for the length-hold stabilization
1341 if( constraint->GetMembers().size() != 1 )
1342 break;
1343
1344 [[fallthrough]];
1345
1351 for( const CONSTRAINT_MEMBER& member : constraint->GetMembers() )
1352 m_angleConstrainedShapes.insert( member.m_item );
1353
1354 break;
1355
1356 default:
1357 break;
1358 }
1359
1360 tag++;
1361 }
1362
1363 // Reserve the hold tags just past the mapped constraints, so they can never collide with a
1364 // real constraint's tag however large the cluster grows.
1365 m_lengthHoldTag = tag;
1366 m_resizeRadiusTag = tag + 1;
1367
1368 // Ground each dimension endpoint no mapped constraint bound, so an attached dimension adds zero
1369 // free DOF and only its bound point can move (through the shape it is coincident with). A locked
1370 // dimension is grounded whole, like a locked shape.
1371 std::set<KIID> unmappedConstraints( m_unmapped.begin(), m_unmapped.end() );
1372 std::set<int> referencedDimParams;
1373
1374 for( PCB_CONSTRAINT* constraint : aConstraints )
1375 {
1376 if( unmappedConstraints.contains( constraint->m_Uuid ) )
1377 continue; // an unmapped constraint enforces nothing, so it references no param
1378
1379 for( const CONSTRAINT_MEMBER& member : constraint->GetMembers() )
1380 {
1381 auto it = m_shapeVars.find( member.m_item );
1382
1383 if( it != m_shapeVars.end() && it->second.kind == SHAPE_KIND::POINT_PAIR )
1384 {
1385 if( ANCHOR_PARAMS anchor = anchorParams( member ); anchor.IsValid() )
1386 referencedDimParams.insert( anchor.x );
1387 }
1388 }
1389 }
1390
1391 for( const auto& [kiid, vars] : m_shapeVars )
1392 {
1393 if( vars.kind != SHAPE_KIND::POINT_PAIR )
1394 continue;
1395
1396 bool locked = ConstraintItemIsLocked( vars.dimension );
1397
1398 for( int pointX : { vars.startX, vars.endX } )
1399 {
1400 if( pointX >= 0 && ( locked || !referencedDimParams.contains( pointX ) ) )
1401 {
1402 fixedParams.insert( pointX );
1403 fixedParams.insert( pointX + 1 );
1404 }
1405 }
1406 }
1407
1408 // Everything not grounded or held as a driving constant is an unknown the solver may move.
1409 GCS::VEC_pD unknowns;
1410
1411 for( int i = 0; i < static_cast<int>( m_params.size() ); ++i )
1412 {
1413 if( !fixedParams.contains( i ) )
1414 unknowns.push_back( &m_params[i] );
1415 }
1416
1417 m_fixedParams = fixedParams;
1418 m_gcs->declareUnknowns( unknowns );
1419 m_gcs->initSolution();
1420 m_gcs->maxIter = MAX_SOLVE_ITERATIONS;
1421
1422 m_dragTargetX = pushParam( 0.0 );
1423 m_dragTargetY = pushParam( 0.0 );
1424 m_coDragTargetX = pushParam( 0.0 );
1425 m_coDragTargetY = pushParam( 0.0 );
1426
1427 m_built = true;
1428 return true;
1429}
1430
1431
1432bool BOARD_CONSTRAINT_ADAPTER::Solve( bool aStabilize )
1433{
1434 if( !m_built )
1435 return false;
1436
1438
1439 // A hard hold here (no drag pin) so a contradiction cannot hide by collapsing a segment or arc.
1440 if( aStabilize )
1441 {
1444 }
1445
1446 // With no shape singled out as edited hold every shape where it sits for a minimal-movement solve
1447 // these soft pins live in the null space of the hard constraints so the diagnosis stays untouched
1448 pinUneditedShapes( {}, GCS::DefaultTemporaryConstraint );
1449
1450 m_gcs->initSolution();
1451 int ret = m_gcs->solve();
1452 m_gcs->applySolution();
1453
1454 bool solved = solveSucceeded( ret );
1455
1456 m_gcs->clearByTag( GCS::DefaultTemporaryConstraint );
1457
1458 if( aStabilize )
1459 m_gcs->clearByTag( m_lengthHoldTag );
1460
1461 return solved;
1462}
1463
1464
1466{
1467 if( !m_built )
1468 return false;
1469
1471
1472 for( const auto& [kiid, vars] : m_shapeVars )
1473 {
1474 // Preserve every curve's free radius while re-solving the resized cluster. This covers
1475 // ellipses too; their focus-to-center offset params are fixed at Build, so pinning the
1476 // minor radius pins the whole shape (focal distance and rotation cannot drift).
1477 if( vars.radius < 0 )
1478 continue;
1479
1480 int target = temporaryParam( m_params[vars.radius] );
1481 GCS::Circle c;
1482 c.center = GCS::Point{ &m_params[vars.startX], &m_params[vars.startX + 1] };
1483 c.rad = &m_params[vars.radius];
1484
1485 if( kiid == aResizedShape )
1486 {
1487 // The user set this radius, so hold it hard. Pin the centre yielding so the shape moves
1488 // only if a locked neighbour leaves no other way to stay tangent.
1489 m_gcs->addConstraintCircleRadius( c, &m_params[target], m_resizeRadiusTag, true );
1490
1491 int cx = temporaryParam( m_params[vars.startX] );
1492 int cy = temporaryParam( m_params[vars.startX + 1] );
1493 m_gcs->addConstraintCoordinateX( c.center, &m_params[cx], GCS::DefaultTemporaryConstraint );
1494 m_gcs->addConstraintCoordinateY( c.center, &m_params[cy], GCS::DefaultTemporaryConstraint );
1495 }
1496 else
1497 {
1498 // Neighbours keep their size unless a real radius constraint says otherwise.
1499 m_gcs->addConstraintCircleRadius( c, &m_params[target], GCS::DefaultTemporaryConstraint );
1500 }
1501 }
1502
1503 // Hold every neighbour where it sits so a resize translates only the shapes a constraint forces
1504 // the resized shape's own centre is pinned above so exclude it here
1505 pinUneditedShapes( { aResizedShape }, GCS::DefaultTemporaryConstraint );
1506
1507 // The radius loop above holds nothing of a polygon so a resized polygon needs its own
1508 // minimal-movement vertex pins here
1509 holdPolygonVertices( { aResizedShape }, GCS::DefaultTemporaryConstraint );
1510
1511 m_gcs->initSolution();
1512 int ret = m_gcs->solve();
1513 m_gcs->applySolution();
1514
1515 bool solved = solveSucceeded( ret );
1516
1517 m_gcs->clearByTag( GCS::DefaultTemporaryConstraint );
1518 m_gcs->clearByTag( m_resizeRadiusTag );
1519
1520 return solved;
1521}
1522
1523
1524bool BOARD_CONSTRAINT_ADAPTER::Solve( const CONSTRAINT_MEMBER& aDragged, const VECTOR2I& aCursor, bool aStabilize,
1525 const std::set<KIID>& aEdited,
1526 const std::optional<std::pair<CONSTRAINT_MEMBER, VECTOR2I>>& aCoDragged,
1527 bool aHoldDraggedRigid )
1528{
1529 if( !m_built )
1530 return false;
1531
1533
1534 ANCHOR_PARAMS params = anchorParams( aDragged );
1535
1536 if( !params.IsValid() )
1537 return false;
1538
1539 // Reuse fixed backing slots for the cursor target so repeated drag solves (warm-started from
1540 // the previous solution still in m_params) never grow the backing store.
1541 double targetX = normalizeX( aCursor.x );
1542 double targetY = normalizeY( aCursor.y );
1543
1544 // For an arc endpoint, project the target onto the arc's current circle so the cursor pin agrees
1545 // with the centre + radius holds pinDraggedShapeRest adds, regardless of whether the caller
1546 // already projected. Without this an off-circle target and the holds fight and the arc drifts.
1547 if( auto it = m_shapeVars.find( aDragged.m_item );
1548 !aStabilize && it != m_shapeVars.end() && it->second.kind == SHAPE_KIND::ARC
1549 && ( aDragged.m_anchor == CONSTRAINT_ANCHOR::START || aDragged.m_anchor == CONSTRAINT_ANCHOR::END ) )
1550 {
1551 const SHAPE_VARS& vars = it->second;
1552 double dx = targetX - m_params[vars.startX];
1553 double dy = targetY - m_params[vars.startX + 1];
1554 double len = std::hypot( dx, dy );
1555
1556 if( len > 1e-9 )
1557 {
1558 double radius = m_params[vars.radius];
1559 targetX = m_params[vars.startX] + dx * radius / len;
1560 targetY = m_params[vars.startX + 1] + dy * radius / len;
1561 }
1562 }
1563
1564 if( auto it = m_shapeVars.find( aDragged.m_item );
1565 !aStabilize && it != m_shapeVars.end() && it->second.kind == SHAPE_KIND::SEGMENT
1566 && it->second.fixedLengthParam >= 0
1567 && ( aDragged.m_anchor == CONSTRAINT_ANCHOR::START || aDragged.m_anchor == CONSTRAINT_ANCHOR::END ) )
1568 {
1569 const SHAPE_VARS& vars = it->second;
1570 int farX = aDragged.m_anchor == CONSTRAINT_ANCHOR::START ? vars.endX : vars.startX;
1571 double segLen = m_params[vars.fixedLengthParam];
1572 double dx = targetX - m_params[farX];
1573 double dy = targetY - m_params[farX + 1];
1574 double len = std::hypot( dx, dy );
1575
1576 if( len > 1e-9 && segLen > 1e-9 )
1577 {
1578 targetX = m_params[farX] + dx * segLen / len;
1579 targetY = m_params[farX + 1] + dy * segLen / len;
1580 }
1581 }
1582
1583 m_params[m_dragTargetX] = targetX;
1584 m_params[m_dragTargetY] = targetY;
1585
1586 GCS::Point anchor = GCS::Point{ &m_params[params.x], &m_params[params.y] };
1587
1588 // A temporary negatively tagged pin yields to the real constraints when over-constrained and
1589 // keeps the default weight far above the stay-put pins so a hard-linked neighbour follows it
1590 m_gcs->addConstraintCoordinateX( anchor, &m_params[m_dragTargetX], GCS::DefaultTemporaryConstraint );
1591 m_gcs->addConstraintCoordinateY( anchor, &m_params[m_dragTargetY], GCS::DefaultTemporaryConstraint );
1592
1593 // The co-dragged anchor gets a plain pin at the same weight since only polygon edge drags carry
1594 // one the rest-hold exclusion below shares the same validity check so it is never left unheld
1595 const CONSTRAINT_MEMBER* coDragged = nullptr;
1596
1597 if( aCoDragged )
1598 {
1599 ANCHOR_PARAMS coParams = anchorParams( aCoDragged->first );
1600
1601 if( coParams.IsValid() )
1602 {
1603 coDragged = &aCoDragged->first;
1604
1605 m_params[m_coDragTargetX] = normalizeX( aCoDragged->second.x );
1606 m_params[m_coDragTargetY] = normalizeY( aCoDragged->second.y );
1607
1608 GCS::Point coAnchor = GCS::Point{ &m_params[coParams.x], &m_params[coParams.y] };
1609
1610 m_gcs->addConstraintCoordinateX( coAnchor, &m_params[m_coDragTargetX], GCS::DefaultTemporaryConstraint );
1611 m_gcs->addConstraintCoordinateY( coAnchor, &m_params[m_coDragTargetY], GCS::DefaultTemporaryConstraint );
1612 }
1613 }
1614
1615 // Only while live-dragging one handle. The settle/apply paths (aStabilize) instead let the
1616 // pinned shape's rest move to meet a newly applied relation, e.g. a fixed-length shrink.
1617 if( !aStabilize )
1618 pinDraggedShapeRest( aDragged, GCS::DefaultTemporaryConstraint, coDragged );
1619
1620 // Protect only shapes a direction or angle constraint could collapse a merely dragged-along
1621 // neighbour keeps its own stay-put pins instead
1622 if( aStabilize )
1623 {
1624 holdFreeSegmentLengths( GCS::DefaultTemporaryConstraint, m_angleConstrainedShapes );
1625 holdFreeArcRadii( GCS::DefaultTemporaryConstraint, m_angleConstrainedShapes );
1626
1627 if( aHoldDraggedRigid )
1628 holdShapesRigid( GCS::DefaultTemporaryConstraint, { aDragged.m_item } );
1629 }
1630
1631 // Hold every other cluster shape where it sits so only edited shapes and whatever a hard
1632 // constraint forces actually move every genuinely edited shape is excluded here
1633 std::set<KIID> editedShapes = aEdited;
1634 editedShapes.insert( aDragged.m_item );
1635
1636 if( coDragged )
1637 editedShapes.insert( coDragged->m_item );
1638
1639 pinUneditedShapes( editedShapes, GCS::DefaultTemporaryConstraint );
1640
1641 // An edited polygon is excluded above but its unbound vertices still need minimal-movement pins
1642 // a live-dragged polygon is left out too since pinDraggedShapeRest already holds its other vertices
1643 std::set<KIID> heldPolygons = editedShapes;
1644
1645 if( !aStabilize )
1646 heldPolygons.erase( aDragged.m_item );
1647
1648 holdPolygonVertices( heldPolygons, GCS::DefaultTemporaryConstraint );
1649
1650 m_gcs->initSolution();
1651 int ret = m_gcs->solve();
1652 m_gcs->applySolution();
1653
1654 bool solved = solveSucceeded( ret );
1655
1656 m_gcs->clearByTag( GCS::DefaultTemporaryConstraint );
1657
1658 return solved;
1659}
1660
1661
1663 const std::vector<SNAP_CANDIDATE>& aCandidates,
1664 const VECTOR2I& aOffset )
1665{
1666 GCS::Point anchor{ &m_params[aAnchor.x], &m_params[aAnchor.y] };
1667 bool addedRelation = false;
1668
1669 const auto addCoordinateX = [&]( double aCoordinate )
1670 {
1671 int target = temporaryParam( normalizeX( KiROUND( aCoordinate ) + aOffset.x ) );
1672 m_gcs->addConstraintCoordinateX( anchor, &m_params[target], GCS::DefaultTemporaryConstraint );
1673 addedRelation = true;
1674 };
1675 const auto addCoordinateY = [&]( double aCoordinate )
1676 {
1677 int target = temporaryParam( normalizeY( KiROUND( aCoordinate ) + aOffset.y ) );
1678 m_gcs->addConstraintCoordinateY( anchor, &m_params[target], GCS::DefaultTemporaryConstraint );
1679 addedRelation = true;
1680 };
1681 const auto addLine = [&]( const SNAP_CANDIDATE& aCandidate )
1682 {
1683 if( aCandidate.direction.SquaredEuclideanNorm() <= 1e-12 )
1684 return;
1685
1686 VECTOR2D direction = aCandidate.direction * ( 1000000.0 / aCandidate.direction.EuclideanNorm() );
1687 VECTOR2D origin = aCandidate.origin + VECTOR2D( aOffset );
1688 int x1 = temporaryParam( normalizeX( KiROUND( origin.x ) ) );
1689 int y1 = temporaryParam( normalizeY( KiROUND( origin.y ) ) );
1690 int x2 = temporaryParam( normalizeX( KiROUND( origin.x + direction.x ) ) );
1691 int y2 = temporaryParam( normalizeY( KiROUND( origin.y + direction.y ) ) );
1692 GCS::Line line;
1693 line.p1 = GCS::Point{ &m_params[x1], &m_params[y1] };
1694 line.p2 = GCS::Point{ &m_params[x2], &m_params[y2] };
1695 m_gcs->addConstraintPointOnLine( anchor, line, GCS::DefaultTemporaryConstraint );
1696 addedRelation = true;
1697 };
1698
1699 for( const SNAP_CANDIDATE& candidate : aCandidates )
1700 {
1701 switch( candidate.relation )
1702 {
1706 addCoordinateX( candidate.origin.x );
1707 addCoordinateY( candidate.origin.y );
1708 break;
1709
1711 case SNAP_RELATION::GRID_X: addCoordinateX( candidate.origin.x ); break;
1712
1714 case SNAP_RELATION::GRID_Y: addCoordinateY( candidate.origin.y ); break;
1715
1718 if( candidate.direction.x != 0.0 )
1719 addCoordinateX( candidate.origin.x );
1720 else if( candidate.direction.y != 0.0 )
1721 addCoordinateY( candidate.origin.y );
1722
1723 break;
1724
1728 case SNAP_RELATION::ANGLE: addLine( candidate ); break;
1729
1732 {
1733 if( !candidate.manifold )
1734 break;
1735
1737 int radius = 0;
1738
1739 if( const CIRCLE* circle = std::get_if<CIRCLE>( &*candidate.manifold ) )
1740 {
1741 center = circle->Center;
1742 radius = circle->Radius;
1743 }
1744 else if( const SHAPE_ARC* arc = std::get_if<SHAPE_ARC>( &*candidate.manifold ) )
1745 {
1746 center = arc->GetCenter();
1747 radius = arc->GetRadius();
1748 }
1749 else
1750 {
1751 break;
1752 }
1753
1754 center += aOffset;
1755 int centerX = temporaryParam( normalizeX( center.x ) );
1756 int centerY = temporaryParam( normalizeY( center.y ) );
1757 int radiusParam = temporaryParam( radius * m_invScale );
1758 GCS::Circle circle;
1759 circle.center = GCS::Point{ &m_params[centerX], &m_params[centerY] };
1760 circle.rad = &m_params[radiusParam];
1761 m_gcs->addConstraintPointOnCircle( anchor, circle, GCS::DefaultTemporaryConstraint );
1762 addedRelation = true;
1763 break;
1764 }
1765 }
1766 }
1767
1768 return addedRelation;
1769}
1770
1771
1773BOARD_CONSTRAINT_ADAPTER::collectRigidState( const std::set<KIID>& aEditedShapes ) const
1774{
1775 RIGID_STATE state;
1776
1777 const auto addPoint = [&]( int aPointX )
1778 {
1779 if( aPointX >= 0 )
1780 state.points.insert( aPointX );
1781 };
1782
1783 for( const KIID& id : aEditedShapes )
1784 {
1785 auto it = m_shapeVars.find( id );
1786
1787 if( it == m_shapeVars.end() )
1788 continue;
1789
1790 const SHAPE_VARS& vars = it->second;
1791
1792 switch( vars.kind )
1793 {
1796 case SHAPE_KIND::BEZIER:
1798 addPoint( vars.startX );
1799 addPoint( vars.endX );
1800 break;
1801
1802 case SHAPE_KIND::CIRCLE:
1803 addPoint( vars.startX );
1804 state.radii.push_back( { vars.startX, vars.radius } );
1805 break;
1806
1808 addPoint( vars.startX );
1809 addPoint( vars.focusX );
1810 state.radii.push_back( { vars.startX, vars.radius } );
1811 break;
1812
1813 case SHAPE_KIND::ARC:
1814 addPoint( vars.startX );
1815 addPoint( vars.arcStartX );
1816 addPoint( vars.arcEndX );
1817 state.radii.push_back( { vars.startX, vars.radius } );
1818 break;
1819
1821 addPoint( vars.startX );
1822 addPoint( vars.focusX );
1823 addPoint( vars.arcStartX );
1824 addPoint( vars.arcEndX );
1825 state.radii.push_back( { vars.startX, vars.radius } );
1826 break;
1827
1829 for( int i = 0; i < vars.vertexCount; ++i )
1830 addPoint( vars.startX + 2 * i );
1831
1832 break;
1833 }
1834 }
1835
1836 return state;
1837}
1838
1839
1840void BOARD_CONSTRAINT_ADAPTER::holdRigidRadii( const std::vector<RIGID_RADIUS_HOLD>& aRadii, int aTag )
1841{
1842 for( const RIGID_RADIUS_HOLD& hold : aRadii )
1843 {
1844 int target = temporaryParam( m_params[hold.radius] );
1845 GCS::Circle circle;
1846 circle.center = GCS::Point{ &m_params[hold.centerX], &m_params[hold.centerX + 1] };
1847 circle.rad = &m_params[hold.radius];
1848 m_gcs->addConstraintCircleRadius( circle, &m_params[target], aTag );
1849 }
1850}
1851
1852
1854 const std::vector<SNAP_CANDIDATE>& aCandidates,
1855 const VECTOR2I& aCursor )
1856{
1857 if( !m_built )
1858 return false;
1859
1860 ANCHOR_PARAMS params = anchorParams( aDragged );
1861
1862 if( !params.IsValid() )
1863 return false;
1864
1866
1867 if( !addSnapRelations( params, aCandidates, {} ) )
1868 return false;
1869
1870 GCS::Point anchor{ &m_params[params.x], &m_params[params.y] };
1871
1872 int cursorX = temporaryParam( normalizeX( aCursor.x ) );
1873 int cursorY = temporaryParam( normalizeY( aCursor.y ) );
1874 int xPin = m_gcs->addConstraintCoordinateX( anchor, &m_params[cursorX], GCS::DefaultTemporaryConstraint );
1875 int yPin = m_gcs->addConstraintCoordinateY( anchor, &m_params[cursorY], GCS::DefaultTemporaryConstraint );
1876 m_gcs->rescaleConstraint( xPin, CURSOR_WEIGHT );
1877 m_gcs->rescaleConstraint( yPin, CURSOR_WEIGHT );
1878
1879 pinDraggedShapeRest( aDragged, GCS::DefaultTemporaryConstraint );
1880 pinUneditedShapes( { aDragged.m_item }, GCS::DefaultTemporaryConstraint );
1881
1882 m_gcs->initSolution();
1883 int ret = m_gcs->solve();
1884 m_gcs->applySolution();
1885 bool solved = solveSucceeded( ret );
1886
1887 m_gcs->clearByTag( GCS::DefaultTemporaryConstraint );
1888 return solved;
1889}
1890
1891
1892bool BOARD_CONSTRAINT_ADAPTER::TravelsWhole( const std::set<KIID>& aEditedShapes ) const
1893{
1894 return std::ranges::all_of( m_shapeVars,
1895 [&]( const auto& aEntry )
1896 {
1897 return aEditedShapes.contains( aEntry.first )
1898 && !ConstraintItemIsLocked( aEntry.second.shape )
1899 && !ConstraintItemIsLocked( aEntry.second.dimension );
1900 } );
1901}
1902
1903
1904bool BOARD_CONSTRAINT_ADAPTER::SolveRigidTranslation( const std::set<KIID>& aEditedShapes,
1905 const VECTOR2I& aTranslation )
1906{
1907 if( !m_built || aEditedShapes.empty() )
1908 return false;
1909
1910 RIGID_STATE state = collectRigidState( aEditedShapes );
1911
1912 if( state.points.empty() )
1913 return false;
1914
1915 const bool movesGroundedPoint =
1916 std::ranges::any_of( state.points,
1917 [&]( int aPointX )
1918 {
1919 return m_fixedParams.contains( aPointX ) || m_fixedParams.contains( aPointX + 1 );
1920 } );
1921
1922 // A pinned or locked coordinate cannot travel
1923 if( movesGroundedPoint && aTranslation != VECTOR2I() )
1924 return false;
1925
1926 const double dx = aTranslation.x * m_invScale;
1927 const double dy = aTranslation.y * m_invScale;
1928 std::map<int, VECTOR2I> exactTargets;
1929
1930 for( int pointX : state.points )
1931 {
1932 m_params[pointX] += dx;
1933 m_params[pointX + 1] += dy;
1934 exactTargets.emplace( pointX, VECTOR2I( KiROUND( denormalizeX( m_params[pointX] ) ),
1935 KiROUND( denormalizeY( m_params[pointX + 1] ) ) ) );
1936 }
1937
1938 // Moving every shape a constraint touches cannot change what it measures
1939 if( TravelsWhole( aEditedShapes ) )
1940 return true;
1941
1943
1944 for( int pointX : state.points )
1945 softPinPoint( pointX, GCS::DefaultTemporaryConstraint );
1946
1947 holdRigidRadii( state.radii, GCS::DefaultTemporaryConstraint );
1948
1949 pinUneditedShapes( aEditedShapes, GCS::DefaultTemporaryConstraint );
1950 m_gcs->initSolution();
1951 int ret = m_gcs->solve();
1952 m_gcs->applySolution();
1953 bool solved = solveSucceeded( ret );
1954
1955 for( const auto& [pointX, target] : exactTargets )
1956 {
1957 VECTOR2I resolved( KiROUND( denormalizeX( m_params[pointX] ) ),
1958 KiROUND( denormalizeY( m_params[pointX + 1] ) ) );
1959 solved = solved && resolved == target;
1960 }
1961
1962 m_gcs->clearByTag( GCS::DefaultTemporaryConstraint );
1963 return solved && QuantizedRelationsSatisfied();
1964}
1965
1966
1967bool BOARD_CONSTRAINT_ADAPTER::SolveRigidSnapRelations( const std::set<KIID>& aEditedShapes, const VECTOR2I& aReference,
1968 const std::vector<SNAP_CANDIDATE>& aCandidates,
1969 const VECTOR2I& aCursor, VECTOR2I& aResolvedCursor )
1970{
1971 if( !m_built || aEditedShapes.empty() )
1972 return false;
1973
1974 RIGID_STATE state = collectRigidState( aEditedShapes );
1975
1976 if( state.points.empty() )
1977 return false;
1978
1980 const int anchorX = *state.points.begin();
1981 const VECTOR2I anchorAtBaseline( KiROUND( denormalizeX( m_params[anchorX] ) ),
1982 KiROUND( denormalizeY( m_params[anchorX + 1] ) ) );
1983 const VECTOR2I cursorToAnchor = anchorAtBaseline - aReference;
1984 std::map<int, VECTOR2I> baselinePoints;
1985
1986 for( int pointX : state.points )
1987 {
1988 baselinePoints.emplace( pointX, VECTOR2I( KiROUND( denormalizeX( m_params[pointX] ) ),
1989 KiROUND( denormalizeY( m_params[pointX + 1] ) ) ) );
1990
1991 if( pointX == anchorX )
1992 continue;
1993
1994 int offsetX = temporaryParam( m_params[pointX] - m_params[anchorX] );
1995 int offsetY = temporaryParam( m_params[pointX + 1] - m_params[anchorX + 1] );
1996 m_gcs->addConstraintDifference( &m_params[anchorX], &m_params[pointX], &m_params[offsetX],
1997 GCS::DefaultTemporaryConstraint );
1998 m_gcs->addConstraintDifference( &m_params[anchorX + 1], &m_params[pointX + 1], &m_params[offsetY],
1999 GCS::DefaultTemporaryConstraint );
2000 }
2001
2002 bool addedRelation = addSnapRelations( { anchorX, anchorX + 1 }, aCandidates, cursorToAnchor );
2003 holdRigidRadii( state.radii, GCS::DefaultTemporaryConstraint );
2004
2005 GCS::Point anchor{ &m_params[anchorX], &m_params[anchorX + 1] };
2006 int cursorX = temporaryParam( normalizeX( aCursor.x + cursorToAnchor.x ) );
2007 int cursorY = temporaryParam( normalizeY( aCursor.y + cursorToAnchor.y ) );
2008 int xPin = m_gcs->addConstraintCoordinateX( anchor, &m_params[cursorX], GCS::DefaultTemporaryConstraint );
2009 int yPin = m_gcs->addConstraintCoordinateY( anchor, &m_params[cursorY], GCS::DefaultTemporaryConstraint );
2010 m_gcs->rescaleConstraint( xPin, CURSOR_WEIGHT );
2011 m_gcs->rescaleConstraint( yPin, CURSOR_WEIGHT );
2012 pinUneditedShapes( aEditedShapes, GCS::DefaultTemporaryConstraint );
2013 m_gcs->initSolution();
2014 int ret = m_gcs->solve();
2015 m_gcs->applySolution();
2016 bool solved = solveSucceeded( ret ) && ( addedRelation || aCandidates.empty() );
2017
2018 VECTOR2I resolvedAnchor( KiROUND( denormalizeX( m_params[anchorX] ) ),
2019 KiROUND( denormalizeY( m_params[anchorX + 1] ) ) );
2020 aResolvedCursor = resolvedAnchor - cursorToAnchor;
2021 VECTOR2I translation = aResolvedCursor - aReference;
2022
2023 for( const auto& [pointX, baseline] : baselinePoints )
2024 {
2025 VECTOR2I resolved( KiROUND( denormalizeX( m_params[pointX] ) ),
2026 KiROUND( denormalizeY( m_params[pointX + 1] ) ) );
2027 solved = solved && resolved == baseline + translation;
2028 }
2029
2030 m_gcs->clearByTag( GCS::DefaultTemporaryConstraint );
2031
2032 if( !solved || !QuantizedRelationsSatisfied() )
2033 return false;
2034
2035 SNAP_SOURCE_CONTEXT verifyContext;
2036 verifyContext.sourcePoint = aResolvedCursor;
2037 SNAP_RESOLVER verify;
2038
2039 for( const SNAP_CANDIDATE& candidate : aCandidates )
2040 verify.AddCandidate( candidate );
2041
2042 SNAP_RESULT verified = verify.Resolve( verifyContext );
2043
2044 for( const SNAP_CANDIDATE& candidate : aCandidates )
2045 {
2046 if( !verified.Accepted( candidate.id ) )
2047 return false;
2048 }
2049
2050 return verified.position == aResolvedCursor;
2051}
2052
2053
2054std::optional<VECTOR2I> BOARD_CONSTRAINT_ADAPTER::AnchorPosition( const CONSTRAINT_MEMBER& aMember ) const
2055{
2056 ANCHOR_PARAMS params = anchorParams( aMember );
2057
2058 if( !params.IsValid() )
2059 return std::nullopt;
2060
2061 return VECTOR2I( KiROUND( denormalizeX( m_params[params.x] ) ),
2062 KiROUND( denormalizeY( m_params[params.y] ) ) );
2063}
2064
2065
2067{
2068 if( aSolveResult == GCS::Success || aSolveResult == GCS::Converged )
2069 return true;
2070
2071 return hardRelationsSatisfied();
2072}
2073
2074
2076{
2077 // planegcs bases Success only on the hard subsystem residual but the soft stay-put subsystem
2078 // perturbs the SQP trajectory so a valid solve often still reports Failed judge the hard
2079 // constraints directly instead a genuine contradiction still leaves a large or non-finite residual
2080 const double residualTol = 1e-3;
2081
2082 // Tag-0 residual is the structural curve rules for arc ellipse and focus which must hold for
2083 // coherent geometry a NaN means no tag-0 constraint exists for a segment-only cluster not a failure
2084 double structuralErr = m_gcs->calculateConstraintErrorByTag( 0 );
2085
2086 if( std::isfinite( structuralErr ) && std::abs( structuralErr ) > residualTol )
2087 return false;
2088
2089 for( const auto& [tag, kiid] : m_tagToConstraint )
2090 {
2091 // A reference non-driving constraint only measures so its residual is never a failure
2092 if( m_nonDrivingTags.contains( tag ) )
2093 continue;
2094
2095 double err = m_gcs->calculateConstraintErrorByTag( tag );
2096
2097 if( !std::isfinite( err ) || std::abs( err ) > residualTol )
2098 return false;
2099 }
2100
2101 return true;
2102}
2103
2104
2106{
2107 SNAPSHOT snapshot = Snapshot();
2108 std::set<int> xParams;
2109 std::set<int> yParams;
2110 std::set<int> radiusParams;
2111
2112 const auto addPoint =
2113 [&]( int aX )
2114 {
2115 if( aX >= 0 )
2116 {
2117 xParams.insert( aX );
2118 yParams.insert( aX + 1 );
2119 }
2120 };
2121
2122 for( const auto& [kiid, vars] : m_shapeVars )
2123 {
2124 addPoint( vars.startX );
2125 addPoint( vars.endX );
2126 addPoint( vars.arcStartX );
2127 addPoint( vars.arcEndX );
2128 addPoint( vars.focusX );
2129
2130 if( vars.kind == SHAPE_KIND::POLYGON )
2131 {
2132 for( int i = 0; i < vars.vertexCount; ++i )
2133 addPoint( vars.startX + 2 * i );
2134 }
2135
2136 if( vars.radius >= 0 )
2137 radiusParams.insert( vars.radius );
2138 }
2139
2140 for( int index : xParams )
2142
2143 for( int index : yParams )
2145
2146 for( int index : radiusParams )
2148
2149 bool valid = hardRelationsSatisfied();
2150 Restore( snapshot );
2151 return valid;
2152}
2153
2154
2155void BOARD_CONSTRAINT_ADAPTER::softPinPoint( const ANCHOR_PARAMS& aPoint, int aTag, std::optional<double> aWeight )
2156{
2157 // A zero weight would neutralize the pin instead of tiering it
2158 wxASSERT( !aWeight || *aWeight > 0 );
2159
2160 if( !aPoint.IsValid() )
2161 return;
2162
2163 int pinX = temporaryParam( m_params[aPoint.x] );
2164 int pinY = temporaryParam( m_params[aPoint.y] );
2165 GCS::Point point{ &m_params[aPoint.x], &m_params[aPoint.y] };
2166
2167 int cx = m_gcs->addConstraintCoordinateX( point, &m_params[pinX], aTag );
2168 int cy = m_gcs->addConstraintCoordinateY( point, &m_params[pinY], aTag );
2169
2170 if( aWeight )
2171 {
2172 m_gcs->rescaleConstraint( cx, *aWeight );
2173 m_gcs->rescaleConstraint( cy, *aWeight );
2174 }
2175}
2176
2177
2178void BOARD_CONSTRAINT_ADAPTER::softPinPoint( int aPointX, int aTag, std::optional<double> aWeight )
2179{
2180 if( aPointX >= 0 )
2181 softPinPoint( ANCHOR_PARAMS{ aPointX, aPointX + 1 }, aTag, aWeight );
2182}
2183
2184
2186 const CONSTRAINT_MEMBER* aCoDragged )
2187{
2188 auto it = m_shapeVars.find( aDragged.m_item );
2189
2190 if( it == m_shapeVars.end() )
2191 return;
2192
2193 const SHAPE_VARS& vars = it->second;
2194
2195 // The pins are temporary and keep the default weight far above the stay-put pins so they hold
2196 // the dragged shape rest against a neighbour weaker stay-put pin while a real constraint still wins
2197 if( vars.kind == SHAPE_KIND::SEGMENT || vars.kind == SHAPE_KIND::BEZIER )
2198 {
2199 if( aDragged.m_anchor == CONSTRAINT_ANCHOR::START )
2200 softPinPoint( vars.endX, aTag );
2201 else if( aDragged.m_anchor == CONSTRAINT_ANCHOR::END )
2202 softPinPoint( vars.startX, aTag );
2203 }
2204 else if( vars.kind == SHAPE_KIND::ARC )
2205 {
2206 if( aDragged.m_anchor == CONSTRAINT_ANCHOR::START || aDragged.m_anchor == CONSTRAINT_ANCHOR::END )
2207 {
2208 // Hold the circle (centre + radius) and the far endpoint, so only the dragged endpoint
2209 // sweeps along the arc instead of the whole arc drifting or ballooning.
2210 softPinPoint( vars.startX, aTag );
2211 holdArcRadius( vars, aTag );
2212 softPinPoint( aDragged.m_anchor == CONSTRAINT_ANCHOR::START ? vars.arcEndX : vars.arcStartX, aTag );
2213 }
2214 else if( aDragged.m_anchor == CONSTRAINT_ANCHOR::CENTER )
2215 {
2216 // Hold both endpoints, so dragging the centre changes the radius but keeps the ends.
2217 softPinPoint( vars.arcStartX, aTag );
2218 softPinPoint( vars.arcEndX, aTag );
2219 }
2220 }
2221 else if( vars.kind == SHAPE_KIND::RECTANGLE && aDragged.m_anchor == CONSTRAINT_ANCHOR::VERTEX )
2222 {
2223 // Hold the diagonally opposite corner so grabbing a corner handle resizes the rectangle
2224 // about it instead of the whole shape drifting
2226 ( aDragged.m_index + 2 ) % 4 ) ),
2227 aTag );
2228 }
2229 else if( vars.kind == SHAPE_KIND::POLYGON && aDragged.m_anchor == CONSTRAINT_ANCHOR::VERTEX )
2230 {
2231 // Hold every other vertex so grabbing one handle moves only its own vertices an edge drag
2232 // names its second vertex through the co-dragged member whose pin must not be fought here
2233 for( int i = 0; i < vars.vertexCount; ++i )
2234 {
2235 if( i == aDragged.m_index )
2236 continue;
2237
2238 if( aCoDragged && aCoDragged->m_item == aDragged.m_item && i == aCoDragged->m_index )
2239 continue;
2240
2241 softPinPoint( vars.startX + 2 * i, aTag );
2242 }
2243 }
2244}
2245
2246
2247void BOARD_CONSTRAINT_ADAPTER::pinUneditedShapes( const std::set<KIID>& aEdited, int aTag )
2248{
2249 // The stay-put pins are the weakest tier so the drive pins and hard constraints all win while a
2250 // neighbour with slack still holds exactly since nothing else acts on its coordinates
2251
2252 // Hold a curve radius scalar so a circle or ellipse with slack keeps its size
2253 auto holdRadius = [&]( const SHAPE_VARS& aVars )
2254 {
2255 GCS::Circle circle;
2256 circle.center = GCS::Point{ &m_params[aVars.startX], &m_params[aVars.startX + 1] };
2257 circle.rad = &m_params[aVars.radius];
2258
2259 int rad = temporaryParam( m_params[aVars.radius] );
2260 m_gcs->rescaleConstraint( m_gcs->addConstraintCircleRadius( circle, &m_params[rad], aTag ), STAY_PUT_WEIGHT );
2261 };
2262
2263 for( const auto& [kiid, vars] : m_shapeVars )
2264 {
2265 if( aEdited.contains( kiid ) )
2266 continue;
2267
2268 // A locked shape or dimension is already frozen at Build so a soft pin would only add
2269 // redundant work
2270 if( ConstraintItemIsLocked( vars.shape ) || ConstraintItemIsLocked( vars.dimension ) )
2271 continue;
2272
2273 switch( vars.kind )
2274 {
2276 case SHAPE_KIND::BEZIER:
2279 // Pinning both stored points fixes the whole shape a segment entirely and a rect four
2280 // corners with them since the corners alias these params
2281 softPinPoint( vars.startX, aTag, STAY_PUT_WEIGHT );
2282 softPinPoint( vars.endX, aTag, STAY_PUT_WEIGHT );
2283 break;
2284
2285 case SHAPE_KIND::CIRCLE:
2287 // A circle or closed ellipse has no endpoints so hold the centre and the radius scalar
2288 // the ellipse focus-to-centre offset is fixed at Build so this pins the whole shape
2289 softPinPoint( vars.startX, aTag, STAY_PUT_WEIGHT );
2290 holdRadius( vars );
2291 break;
2292
2293 case SHAPE_KIND::ARC:
2295 softPinPoint( vars.startX, aTag, STAY_PUT_WEIGHT );
2296 softPinPoint( vars.arcStartX, aTag, STAY_PUT_WEIGHT );
2297 softPinPoint( vars.arcEndX, aTag, STAY_PUT_WEIGHT );
2298 holdRadius( vars );
2299 break;
2300
2302 // Every vertex bound ones included the pins live in the null space of the hard
2303 // constraints so a driving length still moves the vertices it binds
2304 for( int i = 0; i < vars.vertexCount; ++i )
2305 softPinPoint( vars.startX + 2 * i, aTag, STAY_PUT_WEIGHT );
2306
2307 break;
2308 }
2309 }
2310}
2311
2312
2313void BOARD_CONSTRAINT_ADAPTER::holdPolygonVertices( const std::set<KIID>& aShapes, int aTag )
2314{
2315 for( const auto& [kiid, vars] : m_shapeVars )
2316 {
2317 if( vars.kind != SHAPE_KIND::POLYGON || ConstraintItemIsLocked( vars.shape ) )
2318 continue;
2319
2320 if( !aShapes.contains( kiid ) )
2321 continue;
2322
2323 for( int i = 0; i < vars.vertexCount; ++i )
2324 softPinPoint( vars.startX + 2 * i, aTag, STAY_PUT_WEIGHT );
2325 }
2326}
2327
2328
2329void BOARD_CONSTRAINT_ADAPTER::holdFreeSegmentLengths( int aTag, const std::set<KIID>& aShapes )
2330{
2331 // Hold each named free segment length so an angle constraint cannot collapse it to a point
2332 for( const auto& [kiid, vars] : m_shapeVars )
2333 {
2334 if( vars.kind != SHAPE_KIND::SEGMENT || ConstraintItemIsLocked( vars.shape ) )
2335 continue;
2336
2337 if( !aShapes.contains( kiid ) )
2338 continue;
2339
2340 GCS::Point p1{ &m_params[vars.startX], &m_params[vars.startX + 1] };
2341 GCS::Point p2{ &m_params[vars.endX], &m_params[vars.endX + 1] };
2342 double dx = m_params[vars.endX] - m_params[vars.startX];
2343 double dy = m_params[vars.endX + 1] - m_params[vars.startX + 1];
2344 int len = temporaryParam( std::hypot( dx, dy ) );
2345 m_gcs->addConstraintP2PDistance( p1, p2, &m_params[len], aTag );
2346 }
2347}
2348
2349
2351{
2352 GCS::Circle circle;
2353 circle.center = GCS::Point{ &m_params[aVars.startX], &m_params[aVars.startX + 1] };
2354 circle.rad = &m_params[aVars.radius];
2355
2356 int rad = temporaryParam( m_params[aVars.radius] );
2357 m_gcs->addConstraintCircleRadius( circle, &m_params[rad], aTag );
2358}
2359
2360
2361void BOARD_CONSTRAINT_ADAPTER::holdFreeArcRadii( int aTag, const std::set<KIID>& aShapes )
2362{
2363 // Hold each named free arc radius so an ARC_ANGLE change rotates an endpoint instead of the
2364 // solver collapsing the arc to a point a real FIXED_RADIUS still wins since this hold is temporary
2365 for( const auto& [kiid, vars] : m_shapeVars )
2366 {
2367 if( vars.kind != SHAPE_KIND::ARC || ConstraintItemIsLocked( vars.shape ) )
2368 continue;
2369
2370 if( !aShapes.contains( kiid ) )
2371 continue;
2372
2373 holdArcRadius( vars, aTag );
2374 }
2375}
2376
2377
2378void BOARD_CONSTRAINT_ADAPTER::holdShapesRigid( int aTag, const std::set<KIID>& aShapes )
2379{
2380 for( const auto& [kiid, vars] : m_shapeVars )
2381 {
2382 // A dimension has no shape params to hold, and a locked shape is frozen at Build already.
2383 if( !vars.shape || ConstraintItemIsLocked( vars.shape ) || !aShapes.contains( kiid ) )
2384 continue;
2385
2386 RIGID_STATE state = collectRigidState( { kiid } );
2387
2388 if( state.points.empty() )
2389 continue;
2390
2391 // Every other point holds its offset from the first, so the shape travels but cannot deform.
2392 const int anchorX = *state.points.begin();
2393
2394 for( int pointX : state.points )
2395 {
2396 if( pointX == anchorX )
2397 continue;
2398
2399 int offsetX = temporaryParam( m_params[pointX] - m_params[anchorX] );
2400 int offsetY = temporaryParam( m_params[pointX + 1] - m_params[anchorX + 1] );
2401
2402 m_gcs->addConstraintDifference( &m_params[anchorX], &m_params[pointX], &m_params[offsetX], aTag );
2403 m_gcs->addConstraintDifference( &m_params[anchorX + 1], &m_params[pointX + 1], &m_params[offsetY], aTag );
2404 }
2405
2406 holdRigidRadii( state.radii, aTag );
2407 }
2408}
2409
2410
2411std::vector<PCB_SHAPE*> BOARD_CONSTRAINT_ADAPTER::Apply( const std::function<void( BOARD_ITEM* )>& aBeforeWrite )
2412{
2413 std::vector<PCB_SHAPE*> changed;
2414
2415 if( !m_built )
2416 return changed;
2417
2418 // The solved point at param index aX (its y is the next slot), back in IU.
2419 auto pointAt = [&]( int aX )
2420 {
2421 return VECTOR2I( KiROUND( denormalizeX( m_params[aX] ) ), KiROUND( denormalizeY( m_params[aX + 1] ) ) );
2422 };
2423
2424 for( const auto& [kiid, vars] : m_shapeVars )
2425 {
2426 if( vars.kind == SHAPE_KIND::POINT_PAIR )
2427 {
2428 VECTOR2I start = pointAt( vars.startX );
2429
2430 // A leader or centre mark has no bindable end (endX == -1); leave its end untouched.
2431 VECTOR2I end = vars.endX >= 0 ? pointAt( vars.endX ) : vars.dimension->GetEnd();
2432
2433 if( start == vars.dimension->GetStart() && end == vars.dimension->GetEnd() )
2434 continue;
2435
2436 if( aBeforeWrite )
2437 aBeforeWrite( vars.dimension );
2438
2439 PCB_DIM_RADIAL* radial = vars.dimension->Type() == PCB_DIM_RADIAL_T
2440 ? static_cast<PCB_DIM_RADIAL*>( vars.dimension )
2441 : nullptr;
2442 VECTOR2I oldKnee = radial ? radial->GetKnee() : VECTOR2I();
2443
2444 vars.dimension->SetStart( start );
2445 vars.dimension->SetEnd( end );
2446
2447 if( radial )
2448 radial->SetTextPos( radial->GetTextPos() + radial->GetKnee() - oldKnee );
2449
2450 vars.dimension->Update(); // SetStart/SetEnd alone do not re-derive the crossbar and text
2451 continue;
2452 }
2453
2454 if( vars.kind == SHAPE_KIND::CIRCLE )
2455 {
2456 VECTOR2I center = pointAt( vars.startX );
2457 int radius = KiROUND( m_params[vars.radius] * m_scale );
2458
2459 if( center == vars.shape->GetCenter() && radius == vars.shape->GetRadius() )
2460 continue;
2461
2462 if( aBeforeWrite )
2463 aBeforeWrite( vars.shape );
2464
2465 vars.shape->SetCenter( center );
2466 vars.shape->SetRadius( radius );
2467 changed.push_back( vars.shape );
2468 continue;
2469 }
2470
2471 if( vars.kind == SHAPE_KIND::ARC )
2472 {
2473 // A sub-micron radius is a collapse, not intent, so leave the arc as it was rather than
2474 // write a degenerate ring the diagnostics would not catch.
2475 if( m_params[vars.radius] * m_scale < 1000.0 && vars.shape->GetRadius() >= 1000 )
2476 continue;
2477
2478 VECTOR2I center = pointAt( vars.startX );
2479 VECTOR2I start = pointAt( vars.arcStartX );
2480 VECTOR2I end = pointAt( vars.arcEndX );
2481
2482 if( start == vars.shape->GetStart() && end == vars.shape->GetEnd()
2483 && center == vars.shape->GetCenter() )
2484 {
2485 continue;
2486 }
2487
2488 if( aBeforeWrite )
2489 aBeforeWrite( vars.shape );
2490
2491 // The mid lies on the solved circle at the bisector of the swept angles. Take the
2492 // bisector on the side that keeps the original winding so the arc does not flip.
2493 double rad = m_params[vars.radius] * m_scale;
2494 double sa = std::atan2( start.y - center.y, start.x - center.x );
2495 double ea = std::atan2( end.y - center.y, end.x - center.x );
2496
2497 if( vars.shape->IsClockwiseArc() )
2498 {
2499 if( ea > sa )
2500 ea -= 2.0 * M_PI;
2501 }
2502 else if( ea < sa )
2503 {
2504 ea += 2.0 * M_PI;
2505 }
2506
2507 double midAngle = 0.5 * ( sa + ea );
2508 VECTOR2I mid( KiROUND( center.x + rad * std::cos( midAngle ) ),
2509 KiROUND( center.y + rad * std::sin( midAngle ) ) );
2510
2511 vars.shape->SetArcGeometry( start, mid, end );
2512 changed.push_back( vars.shape );
2513 continue;
2514 }
2515
2516 if( vars.kind == SHAPE_KIND::ELLIPSE || vars.kind == SHAPE_KIND::ELLIPSE_ARC )
2517 {
2518 VECTOR2I center = pointAt( vars.startX );
2519
2520 // Recover major radius and rotation from the solved focus.
2521 double fx = ( m_params[vars.focusX] - m_params[vars.startX] ) * m_scale;
2522 double fy = ( m_params[vars.focusX + 1] - m_params[vars.startX + 1] ) * m_scale;
2523 double focal = std::hypot( fx, fy );
2524 double minor = m_params[vars.radius] * m_scale;
2525 double major = std::sqrt( focal * focal + minor * minor );
2526
2527 // A focal distance below 1 IU means a circle-degenerate ellipse with no defined
2528 // rotation, so keep the shape's current one.
2529 EDA_ANGLE rotation =
2530 focal > 1.0 ? EDA_ANGLE( std::atan2( fy, fx ), RADIANS_T ) : vars.shape->GetEllipseRotation();
2531
2532 EDA_ANGLE startAngle = vars.shape->GetEllipseStartAngle();
2533 EDA_ANGLE endAngle = vars.shape->GetEllipseEndAngle();
2534
2535 if( vars.kind == SHAPE_KIND::ELLIPSE_ARC )
2536 {
2537 startAngle = EDA_ANGLE( m_params[vars.startAngle], RADIANS_T );
2538 endAngle = EDA_ANGLE( m_params[vars.endAngle], RADIANS_T );
2539 }
2540
2541 auto sameAngle = []( const EDA_ANGLE& aA, const EDA_ANGLE& aB )
2542 {
2543 return std::abs( ( aA - aB ).Normalize180().AsDegrees() ) < 1e-6;
2544 };
2545
2546 if( center == vars.shape->GetEllipseCenter() && KiROUND( major ) == vars.shape->GetEllipseMajorRadius()
2547 && KiROUND( minor ) == vars.shape->GetEllipseMinorRadius()
2548 && sameAngle( rotation, vars.shape->GetEllipseRotation() )
2549 && sameAngle( startAngle, vars.shape->GetEllipseStartAngle() )
2550 && sameAngle( endAngle, vars.shape->GetEllipseEndAngle() ) )
2551 {
2552 continue;
2553 }
2554
2555 if( aBeforeWrite )
2556 aBeforeWrite( vars.shape );
2557
2558 vars.shape->SetEllipseCenter( center );
2559 vars.shape->SetEllipseRotation( rotation );
2560 vars.shape->SetEllipseMajorRadius( KiROUND( major ) );
2561 vars.shape->SetEllipseMinorRadius( KiROUND( minor ) );
2562
2563 if( vars.kind == SHAPE_KIND::ELLIPSE_ARC )
2564 {
2565 vars.shape->SetEllipseStartAngle( startAngle );
2566 vars.shape->SetEllipseEndAngle( endAngle );
2567 }
2568
2569 changed.push_back( vars.shape );
2570 continue;
2571 }
2572
2573 if( vars.kind == SHAPE_KIND::BEZIER )
2574 {
2575 VECTOR2I start = pointAt( vars.startX );
2576 VECTOR2I end = pointAt( vars.endX );
2577
2578 if( start == vars.shape->GetStart() && end == vars.shape->GetEnd() )
2579 continue;
2580
2581 if( aBeforeWrite )
2582 aBeforeWrite( vars.shape );
2583
2584 // Translate each control handle by its adjacent endpoint delta so the curve shape rides
2585 // along instead of shearing when an endpoint moves
2586 VECTOR2I startDelta = start - vars.shape->GetStart();
2587 VECTOR2I endDelta = end - vars.shape->GetEnd();
2588
2589 vars.shape->SetBezierC1( vars.shape->GetBezierC1() + startDelta );
2590 vars.shape->SetBezierC2( vars.shape->GetBezierC2() + endDelta );
2591 vars.shape->SetStart( start );
2592 vars.shape->SetEnd( end );
2593 vars.shape->RebuildBezierToSegmentsPointsList();
2594 changed.push_back( vars.shape );
2595 continue;
2596 }
2597
2598 if( vars.kind == SHAPE_KIND::POLYGON )
2599 {
2600 const SHAPE_POLY_SET& polySet = vars.shape->GetPolyShape();
2601
2602 // A vertex-count mismatch means an external edit changed the outline CPoint wraps indices
2603 // so a stale count would silently resurrect dropped vertices skip the write instead
2604 if( polySet.OutlineCount() != 1 || polySet.HoleCount( 0 ) != 0
2605 || polySet.COutline( 0 ).PointCount() != vars.vertexCount )
2606 {
2607 continue;
2608 }
2609
2610 std::vector<VECTOR2I> points;
2611 points.reserve( vars.vertexCount );
2612
2613 for( int i = 0; i < vars.vertexCount; ++i )
2614 points.push_back( pointAt( vars.startX + 2 * i ) );
2615
2616 const SHAPE_LINE_CHAIN& outline = polySet.COutline( 0 );
2617 bool moved = false;
2618
2619 for( int i = 0; i < vars.vertexCount; ++i )
2620 {
2621 if( points[i] != outline.CPoint( i ) )
2622 {
2623 moved = true;
2624 break;
2625 }
2626 }
2627
2628 if( !moved )
2629 continue;
2630
2631 if( aBeforeWrite )
2632 aBeforeWrite( vars.shape );
2633
2634 // SetPolyPoints rebuilds the poly wholesale dropping cached derived geometry with it
2635 // ingestion only admits single-outline hole-free polys so nothing else is lost here
2636 vars.shape->SetPolyPoints( points );
2637 changed.push_back( vars.shape );
2638 continue;
2639 }
2640
2641 if( vars.kind == SHAPE_KIND::RECTANGLE )
2642 {
2643 VECTOR2I start = pointAt( vars.startX );
2644 VECTOR2I end = pointAt( vars.endX );
2645
2646 if( start == vars.shape->GetStart() && end == vars.shape->GetEnd() )
2647 continue;
2648
2649 // A sub-micron side is a collapse not intent and a zero-area rect vanishes from the
2650 // canvas so drop the write using the segment guard floor applied per axis
2651 const int collapseFloor = 1000;
2652 int newWidth = std::abs( end.x - start.x );
2653 int newHeight = std::abs( end.y - start.y );
2654 int curWidth = std::abs( vars.shape->GetEnd().x - vars.shape->GetStart().x );
2655 int curHeight = std::abs( vars.shape->GetEnd().y - vars.shape->GetStart().y );
2656
2657 if( ( newWidth < collapseFloor && curWidth >= collapseFloor )
2658 || ( newHeight < collapseFloor && curHeight >= collapseFloor ) )
2659 {
2660 continue;
2661 }
2662
2663 if( aBeforeWrite )
2664 aBeforeWrite( vars.shape );
2665
2666 // SetStart and SetEnd do not re-clamp the stored corner radius so re-apply it through the
2667 // setter whose half-shorter-side clamp keeps a shrunk rect radius in range
2668 int cornerRadius = vars.shape->GetCornerRadius();
2669
2670 vars.shape->SetStart( start );
2671 vars.shape->SetEnd( end );
2672
2673 if( cornerRadius > 0 )
2674 vars.shape->SetCornerRadius( cornerRadius );
2675
2676 changed.push_back( vars.shape );
2677 continue;
2678 }
2679
2680 VECTOR2I start = pointAt( vars.startX );
2681 VECTOR2I end = pointAt( vars.endX );
2682
2683 if( start == vars.shape->GetStart() && end == vars.shape->GetEnd() )
2684 continue;
2685
2686 // A sub-micron result is a collapse, not intent, and a zero-length line vanishes from the
2687 // canvas, so drop it.
2688 const double collapseFloor = 1000.0;
2689 double newLen = ( end - start ).EuclideanNorm();
2690 double curLen = ( vars.shape->GetEnd() - vars.shape->GetStart() ).EuclideanNorm();
2691
2692 if( newLen < collapseFloor && curLen >= collapseFloor )
2693 continue;
2694
2695 if( aBeforeWrite )
2696 aBeforeWrite( vars.shape );
2697
2698 vars.shape->SetStart( start );
2699 vars.shape->SetEnd( end );
2700 changed.push_back( vars.shape );
2701 }
2702
2703 return changed;
2704}
2705
2706
2708{
2710
2711 if( !m_built )
2712 return diag;
2713
2714 m_gcs->diagnose();
2715 diag.freeDof = m_gcs->dofsNumber();
2716
2717 GCS::VEC_I conflictingTags;
2718 GCS::VEC_I redundantTags;
2719 m_gcs->getConflicting( conflictingTags );
2720 m_gcs->getRedundant( redundantTags );
2721
2722 auto tagsToKiids = [&]( const GCS::VEC_I& aTags, std::vector<KIID>& aOut )
2723 {
2724 for( int t : aTags )
2725 {
2726 auto it = m_tagToConstraint.find( t );
2727
2728 if( it != m_tagToConstraint.end() )
2729 aOut.push_back( it->second );
2730 }
2731 };
2732
2733 tagsToKiids( conflictingTags, diag.conflicting );
2734 tagsToKiids( redundantTags, diag.redundant );
2735
2736 auto flagConflict = [&]( const KIID& aKiid )
2737 {
2738 if( std::ranges::find( diag.conflicting, aKiid ) == diag.conflicting.end() )
2739 diag.conflicting.push_back( aKiid );
2740 };
2741
2742 // Also flag any constraint the geometry does not satisfy. The rank analysis can miss it.
2743 const double residualTol = 1e-3;
2744
2745 for( const auto& [tag, kiid] : m_tagToConstraint )
2746 {
2747 // A reference constraint only measures; its stored value drifting from the geometry is
2748 // never a contradiction.
2749 if( m_nonDrivingTags.contains( tag ) )
2750 continue;
2751
2752 double err = m_gcs->calculateConstraintErrorByTag( tag );
2753
2754 // A nan is a degenerate system, not a conflict. A real contradiction leaves a finite error.
2755 if( !std::isfinite( err ) || std::abs( err ) <= residualTol )
2756 continue;
2757
2758 flagConflict( kiid );
2759 }
2760
2761 // The solve collapsing a segment to a point (e.g. horizontal and vertical at once) satisfies the
2762 // constraints with zero residual, so flag the constraints incident on the collapsed segments.
2763 const double normFloor = 1e-3;
2764 std::set<KIID> collapsedShapes;
2765
2766 for( const auto& [k, vars] : m_shapeVars )
2767 {
2768 if( vars.kind != SHAPE_KIND::SEGMENT )
2769 continue;
2770
2771 double solvedLen = std::hypot( m_params[vars.endX] - m_params[vars.startX],
2772 m_params[vars.endX + 1] - m_params[vars.startX + 1] );
2773 double origLen = ( vars.shape->GetEnd() - vars.shape->GetStart() ).EuclideanNorm() * m_invScale;
2774
2775 if( origLen > normFloor && solvedLen < normFloor )
2776 collapsedShapes.insert( k );
2777 }
2778
2779 if( !collapsedShapes.empty() )
2780 {
2781 bool attributed = false;
2782
2783 for( const auto& [tag, kiid] : m_tagToConstraint )
2784 {
2785 // A reference measurement never causes a collapse.
2786 if( m_nonDrivingTags.contains( tag ) )
2787 continue;
2788
2789 auto members = m_tagMembers.find( tag );
2790
2791 if( members == m_tagMembers.end() )
2792 continue;
2793
2794 bool incident = std::any_of( members->second.begin(), members->second.end(),
2795 [&]( const KIID& aMember )
2796 {
2797 return collapsedShapes.contains( aMember );
2798 } );
2799
2800 if( incident )
2801 {
2802 flagConflict( kiid );
2803 attributed = true;
2804 }
2805 }
2806
2807 // A collapse no mapped constraint touches should not happen; keep the whole-cluster flag
2808 // as a fallback so the contradiction is never silently dropped.
2809 if( !attributed )
2810 {
2811 for( const auto& [tag, kiid] : m_tagToConstraint )
2812 {
2813 if( !m_nonDrivingTags.contains( tag ) )
2814 flagConflict( kiid );
2815 }
2816 }
2817 }
2818
2819 // .solved reflects the last Solve(), which Diagnose() does not run; the caller sets it.
2820 return diag;
2821}
2822
2823
2825{
2826 m_adapter.reset();
2827 m_baseline.clear();
2828 m_baseConflict = false;
2829}
2830
2831
2833 const std::unordered_set<KIID>& aClusterShapes,
2834 const std::vector<PCB_CONSTRAINT*>& aConstraints )
2835{
2836 m_board = aBoard;
2837 reset();
2838
2839 std::vector<PCB_SHAPE*> shapes = resolveClusterShapes( aBoard, aClusterShapes );
2840 std::vector<PCB_DIMENSION_BASE*> dimensions = resolveClusterDimensions( aBoard, aClusterShapes );
2841
2842 if( ( shapes.empty() && dimensions.empty() ) || aConstraints.empty() )
2843 return false;
2844
2845 auto adapter = std::make_unique<BOARD_CONSTRAINT_ADAPTER>();
2846
2847 if( !adapter->Build( shapes, aConstraints, nullptr, dimensions ) )
2848 return false;
2849
2850 m_adapter = std::move( adapter );
2851 m_baseline = m_adapter->Snapshot();
2852 m_baseConflict = !m_adapter->CurrentRelationsSatisfied();
2853 return true;
2854}
2855
2856
2858{
2860 result.position = aContext.sourcePoint;
2861
2862 if( m_baseConflict )
2864 else if( !m_adapter )
2866
2867 return result;
2868}
2869
2870
2872 double aResidual )
2873{
2874 aResult.quantizedResiduals.push_back( aResidual );
2875 aResult.remainingDof = std::max( 0, aResult.remainingDof - aCandidate.consumedDof );
2876}
2877
2878
2880 const std::vector<PCB_SHAPE*>& aEditedShapes,
2881 const VECTOR2I& aReference )
2882{
2883 m_reference = aReference;
2884 m_edited.clear();
2885 reset();
2886
2887 if( !aBoard || aEditedShapes.empty() )
2888 return false;
2889
2890 auto shapeToConstraints = buildShapeConstraintMap( aBoard, collectAllConstraints( aBoard ) );
2891 std::set<KIID> visited;
2892 std::unordered_set<KIID> sessionShapes;
2893 std::vector<PCB_CONSTRAINT*> sessionConstraints;
2894
2895 // Every seed's cluster joins one adapter: a move is rigid, so shapes that constrain each
2896 // other must be solved together even when the user grabbed them separately.
2897 for( PCB_SHAPE* seed : aEditedShapes )
2898 {
2899 if( !seed || visited.contains( seed->m_Uuid )
2900 || !shapeToConstraints.contains( seed->m_Uuid ) )
2901 {
2902 continue;
2903 }
2904
2905 std::unordered_set<KIID> clusterShapes;
2906 std::vector<PCB_CONSTRAINT*> clusterConstraints;
2907 collectConstraintCluster( shapeToConstraints, seed->m_Uuid, clusterShapes,
2908 clusterConstraints, &visited );
2909
2910 for( PCB_SHAPE* edited : aEditedShapes )
2911 {
2912 if( edited && clusterShapes.contains( edited->m_Uuid ) )
2913 m_edited.insert( edited->m_Uuid );
2914 }
2915
2916 sessionShapes.insert( clusterShapes.begin(), clusterShapes.end() );
2917 sessionConstraints.insert( sessionConstraints.end(), clusterConstraints.begin(),
2918 clusterConstraints.end() );
2919 }
2920
2921 if( m_edited.empty() )
2922 return false;
2923
2924 return buildCluster( aBoard, sessionShapes, sessionConstraints );
2925}
2926
2927
2929{
2930 if( !usable() )
2931 return false;
2932
2933 bool feasible = rewind() && m_adapter->SolveRigidTranslation( m_edited, aTarget - m_reference );
2934 rewind();
2935 return feasible;
2936}
2937
2938
2940 const SNAP_SOURCE_CONTEXT& aContext, const std::vector<SNAP_CANDIDATE>& aCandidates )
2941{
2942 SNAP_RESULT result = startResult( aContext );
2943
2944 if( result.status != SNAP_RESULT_STATUS::SUCCESS )
2945 return result;
2946
2947 // The cluster may already sit on every candidate, in which case the move is a no-op and the
2948 // unconstrained answer is the right one.
2949 if( !aCandidates.empty() && feasibleAt( m_reference ) )
2950 {
2951 SNAP_SOURCE_CONTEXT referenceContext = aContext;
2952 referenceContext.sourcePoint = m_reference;
2953 SNAP_RESOLVER referenceResolver;
2954
2955 for( const SNAP_CANDIDATE& candidate : aCandidates )
2956 referenceResolver.AddCandidate( candidate );
2957
2958 SNAP_RESULT referenceResult = referenceResolver.Resolve( referenceContext );
2959 bool allAccepted = referenceResult.position == m_reference;
2960
2961 for( const SNAP_CANDIDATE& candidate : aCandidates )
2962 allAccepted = allAccepted && referenceResult.Accepted( candidate.id );
2963
2964 if( allAccepted )
2965 return referenceResult;
2966 }
2967
2968 VECTOR2I projected;
2969 bool solved = rewind()
2970 && m_adapter->SolveRigidSnapRelations( m_edited, m_reference, aCandidates,
2971 aContext.sourcePoint, projected );
2972 rewind();
2973
2974 if( !solved )
2975 {
2976 if( aCandidates.empty() && feasibleAt( m_reference ) )
2977 {
2978 result.position = m_reference;
2979 return result;
2980 }
2981
2983 return result;
2984 }
2985
2986 if( !feasibleAt( projected ) )
2987 {
2989 return result;
2990 }
2991
2992 result.position = projected;
2993
2994 // A rigid move lands the cluster exactly on whatever it accepted, so every residual is zero.
2995 for( const SNAP_CANDIDATE& candidate : aCandidates )
2996 accept( result, candidate, 0.0 );
2997
2998 return result;
2999}
3000
3001
3003 const VECTOR2I& aTarget, std::vector<PCB_SHAPE*>* aModified,
3004 const std::function<void( BOARD_ITEM* )>& aBeforeModify )
3005{
3006 // A cluster that travels whole carries any base conflict along unchanged, so the conflict is no
3007 // reason to refuse the move
3008 if( !m_adapter || ( HasBaseConflict() && !m_adapter->TravelsWhole( m_edited ) ) )
3009 return false;
3010
3011 if( !rewind() || !m_adapter->SolveRigidTranslation( m_edited, aTarget - m_reference ) )
3012 {
3013 rewind();
3014 return false;
3015 }
3016
3017 std::vector<PCB_SHAPE*> changed = m_adapter->Apply( aBeforeModify );
3018 m_adapter->ApplyReferenceValues( aBeforeModify );
3019
3020 if( aModified )
3021 aModified->insert( aModified->end(), changed.begin(), changed.end() );
3022
3023 return true;
3024}
3025
3026
3028{
3029 m_board = aBoard;
3030 m_dragged = aDragged;
3031 m_draggedShape = nullptr;
3032 reset();
3033
3034 if( !m_board )
3035 return false;
3036
3037 auto shapeToConstraints = buildShapeConstraintMap( m_board, collectAllConstraints( m_board ) );
3038 std::unordered_set<KIID> clusterShapes;
3039 std::vector<PCB_CONSTRAINT*> clusterConstraints;
3040 collectConstraintCluster( shapeToConstraints, m_dragged.m_item, clusterShapes, clusterConstraints );
3041
3042 if( !buildCluster( m_board, clusterShapes, clusterConstraints ) )
3043 return false;
3044
3045 m_draggedShape = dynamic_cast<PCB_SHAPE*>( m_board->ResolveItem( m_dragged.m_item, true ) );
3046 return true;
3047}
3048
3049
3051{
3052 return m_adapter && m_dragged.m_item == aDragged.m_item
3053 && m_dragged.m_anchor == aDragged.m_anchor && m_dragged.m_index == aDragged.m_index;
3054}
3055
3056
3058{
3059 if( !usable() || !rewind() )
3060 return false;
3061
3062 bool solved = m_adapter->Solve( m_dragged, aTarget, false );
3063 std::optional<VECTOR2I> resolved = solved ? m_adapter->AnchorPosition( m_dragged ) : std::nullopt;
3064 bool quantizedValid = resolved && *resolved == aTarget && m_adapter->QuantizedRelationsSatisfied();
3065 rewind();
3066 return quantizedValid;
3067}
3068
3069
3071 const SNAP_SOURCE_CONTEXT& aContext, const std::vector<SNAP_CANDIDATE>& aCandidates )
3072{
3073 SNAP_RESULT result = startResult( aContext );
3074
3075 if( result.status != SNAP_RESULT_STATUS::SUCCESS || aCandidates.empty() )
3076 return result;
3077
3078 if( !rewind() )
3079 {
3081 return result;
3082 }
3083
3084 if( !m_adapter->SolveSnapRelations( m_dragged, aCandidates, aContext.sourcePoint ) )
3085 {
3086 rewind();
3088 return result;
3089 }
3090
3091 std::optional<VECTOR2I> resolved = m_adapter->AnchorPosition( m_dragged );
3092
3093 if( !resolved )
3094 {
3095 rewind();
3097 return result;
3098 }
3099
3100 result.position = *resolved;
3101
3102 const auto candidateResidual =
3103 [&]( const SNAP_CANDIDATE& aCandidate ) -> std::optional<double>
3104 {
3105 VECTOR2D point( result.position );
3106
3107 switch( aCandidate.relation )
3108 {
3112 return ( point - aCandidate.origin ).EuclideanNorm();
3113
3116 return std::abs( point.x - aCandidate.origin.x );
3117
3120 return std::abs( point.y - aCandidate.origin.y );
3121
3124 if( aCandidate.direction.x != 0.0 )
3125 return std::abs( point.x - aCandidate.origin.x );
3126
3127 if( aCandidate.direction.y != 0.0 )
3128 return std::abs( point.y - aCandidate.origin.y );
3129
3130 return std::nullopt;
3131
3136 {
3137 double divisor = aCandidate.direction.SquaredEuclideanNorm();
3138
3139 if( divisor <= 1e-12 )
3140 return std::nullopt;
3141
3142 VECTOR2D offset = point - aCandidate.origin;
3143 double parameter = offset.Dot( aCandidate.direction ) / divisor;
3144
3145 if( aCandidate.relation == SNAP_RELATION::POINT_ON_RAY && parameter < 0.0 )
3146 return std::nullopt;
3147
3148 if( aCandidate.relation == SNAP_RELATION::POINT_ON_SEGMENT
3149 && ( parameter < 0.0 || parameter > 1.0 ) )
3150 {
3151 return std::nullopt;
3152 }
3153
3154 return std::abs( offset.x * aCandidate.direction.y
3155 - offset.y * aCandidate.direction.x )
3156 / std::sqrt( divisor );
3157 }
3158
3161 {
3162 if( !aCandidate.manifold )
3163 return std::nullopt;
3164
3165 if( const CIRCLE* circle = std::get_if<CIRCLE>( &*aCandidate.manifold ) )
3166 {
3167 return std::abs( result.position.Distance( circle->Center )
3168 - circle->Radius );
3169 }
3170
3171 if( const SHAPE_ARC* arc = std::get_if<SHAPE_ARC>( &*aCandidate.manifold ) )
3172 {
3173 if( !arc->Collide( result.position, 2 ) )
3174 return std::nullopt;
3175
3176 return std::abs( result.position.Distance( arc->GetCenter() )
3177 - arc->GetRadius() );
3178 }
3179
3180 return std::nullopt;
3181 }
3182 }
3183
3184 return std::nullopt;
3185 };
3186
3187 for( const SNAP_CANDIDATE& candidate : aCandidates )
3188 {
3189 std::optional<double> residual = candidateResidual( candidate );
3190 bool discrete = candidate.relation == SNAP_RELATION::COINCIDENCE
3191 || candidate.relation == SNAP_RELATION::TANGENT
3192 || candidate.relation == SNAP_RELATION::NORMAL
3193 || candidate.relation == SNAP_RELATION::X_COORDINATE
3194 || candidate.relation == SNAP_RELATION::Y_COORDINATE
3195 || candidate.relation == SNAP_RELATION::GRID_X
3196 || candidate.relation == SNAP_RELATION::GRID_Y
3197 || candidate.relation == SNAP_RELATION::BBOX_ALIGNMENT
3198 || candidate.relation == SNAP_RELATION::BBOX_EQUAL_GAP;
3199
3200 if( !residual || ( discrete ? *residual != 0.0 : *residual > 2.0 ) )
3201 {
3202 rewind();
3204 return result;
3205 }
3206
3207 accept( result, candidate, *residual );
3208 }
3209
3210 if( !m_adapter->QuantizedRelationsSatisfied() )
3212
3213 rewind();
3214 return result;
3215}
3216
3217
3219 const VECTOR2I& aCursor, std::vector<PCB_SHAPE*>* aModified,
3220 const std::function<void( BOARD_ITEM* )>& aBeforeModify, bool aIncludeDragged,
3221 bool aStabilize, const std::set<KIID>& aEdited,
3222 const std::optional<std::pair<CONSTRAINT_MEMBER, VECTOR2I>>& aCoDragged )
3223{
3225
3226 if( !rewind() )
3227 return diag;
3228
3229 const auto notifyModify = [&]( BOARD_ITEM* aItem )
3230 {
3231 if( ( aIncludeDragged || aItem != m_draggedShape ) && aBeforeModify )
3232 aBeforeModify( aItem );
3233 };
3234
3235 if( m_baseConflict )
3236 {
3237 m_adapter->Apply( notifyModify );
3238 return m_adapter->Diagnose();
3239 }
3240
3241 if( !m_adapter->Solve( m_dragged, aCursor, aStabilize, aEdited, aCoDragged ) )
3242 {
3243 rewind();
3244 m_adapter->Apply( notifyModify );
3245 return diag;
3246 }
3247
3248 std::vector<PCB_SHAPE*> changed = m_adapter->Apply( notifyModify );
3249
3250 m_adapter->ApplyReferenceValues( aBeforeModify );
3251 m_baseline = m_adapter->Snapshot();
3252 diag = m_adapter->Diagnose();
3253 diag.solved = true;
3254
3255 if( aModified )
3256 {
3257 std::ranges::copy_if( changed, std::back_inserter( *aModified ),
3258 [&]( const PCB_SHAPE* aShape )
3259 {
3260 return aIncludeDragged || aShape != m_draggedShape;
3261 } );
3262 }
3263
3264 return diag;
3265}
3266
3267
3268CONSTRAINT_DIAGNOSIS SolveCluster( BOARD* aBoard, const CONSTRAINT_MEMBER& aDragged, const VECTOR2I& aCursor,
3269 std::vector<PCB_SHAPE*>* aModified,
3270 const std::function<void( BOARD_ITEM* )>& aBeforeModify, bool aIncludeDragged,
3271 bool aStabilize, const std::set<KIID>& aEdited,
3272 const std::optional<std::pair<CONSTRAINT_MEMBER, VECTOR2I>>& aCoDragged,
3273 const std::set<KIID>& aFixedShapes, bool aHoldDraggedRigid )
3274{
3276
3277 if( !aBoard )
3278 return diag;
3279
3280 // The cluster is the connected component of the shape<->constraint graph containing the
3281 // dragged shape, since a constraint links the shapes of all its members.
3282 auto shapeToConstraints = buildShapeConstraintMap( aBoard, collectAllConstraints( aBoard ) );
3283
3284 std::unordered_set<KIID> clusterShapes;
3285 std::vector<PCB_CONSTRAINT*> clusterConstraints;
3286 collectConstraintCluster( shapeToConstraints, aDragged.m_item, clusterShapes, clusterConstraints );
3287
3288 std::vector<PCB_SHAPE*> shapes = resolveClusterShapes( aBoard, clusterShapes );
3289 std::vector<PCB_DIMENSION_BASE*> dimensions = resolveClusterDimensions( aBoard, clusterShapes );
3290
3291 if( ( shapes.empty() && dimensions.empty() ) || clusterConstraints.empty() )
3292 return diag;
3293
3295
3296 if( !adapter.Build( shapes, clusterConstraints, &aFixedShapes, dimensions ) )
3297 return diag;
3298
3299 bool solved = adapter.Solve( aDragged, aCursor, aStabilize, aEdited, aCoDragged, aHoldDraggedRigid );
3300
3301 if( !solved )
3302 return diag; // leave geometry untouched on a failed/diverged solve
3303
3304 PCB_SHAPE* draggedShape = dynamic_cast<PCB_SHAPE*>( aBoard->ResolveItem( aDragged.m_item, true ) );
3305
3306 // Stage each neighbor (not the dragged shape, which the caller stages itself) just before
3307 // its geometry is written, so the whole re-derivation is one undoable transaction.
3308 std::vector<PCB_SHAPE*> changed = adapter.Apply(
3309 [&]( BOARD_ITEM* aItem )
3310 {
3311 if( ( aIncludeDragged || aItem != draggedShape ) && aBeforeModify )
3312 aBeforeModify( aItem );
3313 } );
3314
3315 adapter.ApplyReferenceValues( aBeforeModify );
3316
3317 diag = adapter.Diagnose();
3318 diag.solved = true;
3319
3320 if( aModified )
3321 {
3322 std::ranges::copy_if( changed, std::back_inserter( *aModified ),
3323 [&]( const PCB_SHAPE* aShape )
3324 { return aIncludeDragged || aShape != draggedShape; } );
3325 }
3326
3327 return diag;
3328}
3329
3330
3331std::set<KIID> ConstraintReferenceShapes( BOARD* aBoard, const PCB_CONSTRAINT* aConstraint )
3332{
3333 if( !aBoard || !aConstraint )
3334 return {};
3335
3336 PCB_CONSTRAINT_TYPE type = aConstraint->GetConstraintType();
3337
3339 return {};
3340
3341 const std::vector<CONSTRAINT_MEMBER>& members = aConstraint->GetMembers();
3342
3343 if( members.size() != 2 || members.front().m_item == members.back().m_item )
3344 return {};
3345
3346 // Build freezes shapes only, so a reference it cannot freeze must not be named as one.
3347 if( members.back().m_anchor != CONSTRAINT_ANCHOR::WHOLE
3348 || !dynamic_cast<PCB_SHAPE*>( aBoard->ResolveItem( members.back().m_item, true ) ) )
3349 {
3350 return {};
3351 }
3352
3353 // Freezing the line as well would leave nothing able to move, so let the line move to the point.
3354 if( ConstraintItemIsLocked( aBoard->ResolveItem( members.front().m_item, true ) ) )
3355 return {};
3356
3357 auto pinsThePoint = [&]( const PCB_CONSTRAINT* aOther )
3358 {
3359 return aOther->GetConstraintType() == PCB_CONSTRAINT_TYPE::FIXED_POSITION && !aOther->GetMembers().empty()
3360 && aOther->GetMembers().front() == members.front();
3361 };
3362
3363 if( std::ranges::any_of( collectAllConstraints( aBoard ), pinsThePoint ) )
3364 return {};
3365
3366 return { members.back().m_item };
3367}
3368
3369
3371 std::vector<PCB_SHAPE*>* aModified,
3372 const std::function<void( BOARD_ITEM* )>& aBeforeModify,
3373 const std::set<KIID>& aFixedShapes )
3374{
3375 if( !aBoard || !aConstraint || aConstraint->GetMembers().empty() )
3376 return {};
3377
3378 // Pin the first member's anchor where it is; the solver moves the rest of the cluster (and the
3379 // pinned shape's own free geometry) to meet the new relation. A WHOLE member is pinned at the
3380 // shape's first concrete anchor -- START for a segment/arc, CENTER for a circle.
3381 CONSTRAINT_MEMBER pin = aConstraint->GetMembers().front();
3382
3383 if( pin.m_anchor == CONSTRAINT_ANCHOR::WHOLE )
3384 {
3385 PCB_SHAPE* shape = dynamic_cast<PCB_SHAPE*>( aBoard->ResolveItem( pin.m_item, true ) );
3386
3387 if( !shape )
3388 return {};
3389
3390 std::vector<CONSTRAINT_ANCHOR_POINT> anchors = ConstraintShapeAnchors( shape );
3391
3392 if( anchors.empty() )
3393 return {};
3394
3395 pin.m_anchor = anchors.front().anchor;
3396 pin.m_index = anchors.front().index;
3397 }
3398
3399 std::optional<VECTOR2I> pos = ConstraintAnchorPosition( aBoard, pin );
3400
3401 if( !pos )
3402 return {};
3403
3404 // The pinned shape itself can move (e.g. a fixed-length segment's far end), and the caller does
3405 // not stage it separately, so report it too.
3406 return SolveCluster( aBoard, pin, *pos, aModified, aBeforeModify, /* aIncludeDragged */ true,
3407 /* aStabilize */ true, /* aEdited */ {}, /* aCoDragged */ std::nullopt, aFixedShapes,
3408 /* aHoldDraggedRigid */ !aFixedShapes.empty() );
3409}
3410
3411
3412void ReSolveShapeClusters( BOARD* aBoard, const std::vector<PCB_SHAPE*>& aShapes, std::vector<PCB_SHAPE*>* aModified,
3413 const std::function<void( BOARD_ITEM* )>& aBeforeModify )
3414{
3415 if( !aBoard )
3416 return;
3417
3418 auto shapeToConstraints = buildShapeConstraintMap( aBoard, collectAllConstraints( aBoard ) );
3419
3420 // Solve each affected cluster once, no matter how many of its shapes were edited.
3421 std::set<KIID> visited;
3422
3423 for( PCB_SHAPE* shape : aShapes )
3424 {
3425 if( !shape || visited.contains( shape->m_Uuid ) || !shapeToConstraints.contains( shape->m_Uuid ) )
3426 continue;
3427
3428 std::unordered_set<KIID> clusterShapes;
3429 std::vector<PCB_CONSTRAINT*> clusterConstraints;
3430 collectConstraintCluster( shapeToConstraints, shape->m_Uuid, clusterShapes, clusterConstraints, &visited );
3431
3432 std::vector<CONSTRAINT_ANCHOR_POINT> anchors = ConstraintShapeAnchors( shape );
3433
3434 if( anchors.empty() )
3435 continue;
3436
3437 // Every edited shape that fell into this cluster must stay free only the untouched
3438 // neighbours get stay-put pins solving from a single seed would pin the others back
3439 std::set<KIID> edited;
3440
3441 for( PCB_SHAPE* other : aShapes )
3442 {
3443 if( other && clusterShapes.contains( other->m_Uuid ) )
3444 edited.insert( other->m_Uuid );
3445 }
3446
3447 SolveCluster( aBoard, { shape->m_Uuid, anchors.front().anchor, anchors.front().index }, anchors.front().pos,
3448 aModified, aBeforeModify,
3449 /* aIncludeDragged */ true, /* aStabilize */ false, edited );
3450 }
3451}
3452
3453
3454bool ReSolveShapeClustersHoldingEdited( BOARD* aBoard, const std::vector<PCB_SHAPE*>& aEditedShapes,
3455 std::vector<PCB_SHAPE*>* aModified,
3456 const std::function<void( BOARD_ITEM* )>& aBeforeModify )
3457{
3458 if( !aBoard )
3459 return false;
3460
3461 auto shapeToConstraints = buildShapeConstraintMap( aBoard, collectAllConstraints( aBoard ) );
3462 std::set<KIID> visited;
3463 std::vector<std::unique_ptr<BOARD_CONSTRAINT_ADAPTER>> solvedClusters;
3464
3465 for( PCB_SHAPE* seed : aEditedShapes )
3466 {
3467 if( !seed || visited.contains( seed->m_Uuid ) || !shapeToConstraints.contains( seed->m_Uuid ) )
3468 continue;
3469
3470 std::unordered_set<KIID> clusterShapes;
3471 std::vector<PCB_CONSTRAINT*> clusterConstraints;
3472 collectConstraintCluster( shapeToConstraints, seed->m_Uuid, clusterShapes, clusterConstraints, &visited );
3473
3474 std::vector<PCB_SHAPE*> shapes = resolveClusterShapes( aBoard, clusterShapes );
3475 std::vector<PCB_DIMENSION_BASE*> dimensions = resolveClusterDimensions( aBoard, clusterShapes );
3476
3477 if( shapes.empty() || clusterConstraints.empty() )
3478 continue;
3479
3480 // A properties edit is authoritative since the user typed exact values so every edited shape
3481 // is held fixed across all its DOF and only constrained neighbours move to satisfy relations
3482 std::set<KIID> fixed;
3483
3484 for( PCB_SHAPE* edited : aEditedShapes )
3485 {
3486 if( edited && clusterShapes.contains( edited->m_Uuid ) )
3487 fixed.insert( edited->m_Uuid );
3488 }
3489
3490 auto adapter = std::make_unique<BOARD_CONSTRAINT_ADAPTER>();
3491
3492 if( !adapter->Build( shapes, clusterConstraints, &fixed, dimensions )
3493 || !adapter->Solve( true ) || !adapter->CurrentRelationsSatisfied() )
3494 {
3495 return false;
3496 }
3497
3498 solvedClusters.push_back( std::move( adapter ) );
3499 }
3500
3501 for( const std::unique_ptr<BOARD_CONSTRAINT_ADAPTER>& adapter : solvedClusters )
3502 {
3503 std::vector<PCB_SHAPE*> changed = adapter->Apply( aBeforeModify );
3504
3505 // Driven reference values re-measure against the held geometry so a dimension bound to an
3506 // edited shape reads its new size instead of the stale one
3507 adapter->ApplyReferenceValues( aBeforeModify );
3508
3509 if( aModified )
3510 aModified->insert( aModified->end(), changed.begin(), changed.end() );
3511 }
3512
3513 return true;
3514}
3515
3516
3517void ReSolveAfterShapeResize( BOARD* aBoard, PCB_SHAPE* aShape, std::vector<PCB_SHAPE*>* aModified,
3518 const std::function<void( BOARD_ITEM* )>& aBeforeModify )
3519{
3520 if( !aBoard || !aShape )
3521 return;
3522
3523 auto shapeToConstraints = buildShapeConstraintMap( aBoard, collectAllConstraints( aBoard ) );
3524
3525 if( !shapeToConstraints.contains( aShape->m_Uuid ) )
3526 return;
3527
3528 std::unordered_set<KIID> clusterShapes;
3529 std::vector<PCB_CONSTRAINT*> clusterConstraints;
3530 collectConstraintCluster( shapeToConstraints, aShape->m_Uuid, clusterShapes, clusterConstraints );
3531
3532 std::vector<PCB_SHAPE*> shapes = resolveClusterShapes( aBoard, clusterShapes );
3533 std::vector<PCB_DIMENSION_BASE*> dimensions = resolveClusterDimensions( aBoard, clusterShapes );
3534
3535 if( shapes.empty() || clusterConstraints.empty() )
3536 return;
3537
3539
3540 if( !adapter.Build( shapes, clusterConstraints, nullptr, dimensions )
3541 || !adapter.SolveAfterResize( aShape->m_Uuid ) )
3542 {
3543 return;
3544 }
3545
3546 std::vector<PCB_SHAPE*> changed = adapter.Apply(
3547 [&]( BOARD_ITEM* aChanged )
3548 {
3549 if( aBeforeModify )
3550 aBeforeModify( aChanged );
3551 } );
3552
3553 adapter.ApplyReferenceValues( aBeforeModify );
3554
3555 if( aModified )
3556 aModified->insert( aModified->end(), changed.begin(), changed.end() );
3557}
3558
3559
3560// Diagnose one cluster in isolation an unbuildable cluster with no resolvable members or no
3561// constraints contributes nothing so its result stays empty
3563 const std::unordered_set<KIID>& aClusterShapes,
3564 const std::vector<PCB_CONSTRAINT*>& aClusterConstraints,
3565 const std::unordered_set<KIID>& aOnBoard )
3566{
3568
3569 std::vector<PCB_SHAPE*> shapes = resolveClusterShapes( aBoard, aClusterShapes, &aOnBoard );
3570 std::vector<PCB_DIMENSION_BASE*> dimensions = resolveClusterDimensions( aBoard, aClusterShapes, &aOnBoard );
3571
3573
3574 if( ( shapes.empty() && dimensions.empty() ) || aClusterConstraints.empty()
3575 || !adapter.Build( shapes, aClusterConstraints, nullptr, dimensions ) )
3576 {
3577 return result;
3578 }
3579
3580 // A constraint Build() could not map (e.g. a shape was changed to an incompatible kind) is not
3581 // enforced; flag it so the user sees it is broken rather than silently ignored.
3582 result.erroredUnmapped = adapter.UnmappedConstraints();
3583
3584 // Solve first so the residual check sees a real contradiction not an unsolved constraint a
3585 // contradictory cluster deliberately diverges here since the stabilize holds forbid the escape
3586 adapter.Solve( true );
3587
3588 CONSTRAINT_DIAGNOSIS diag = adapter.Diagnose();
3589
3590 if( diag.IsOverConstrained() )
3592 else if( diag.IsUnderConstrained() )
3594 else
3596
3597 for( PCB_SHAPE* shape : shapes )
3598 result.shapeIds.push_back( shape->m_Uuid );
3599
3600 // Bound dimensions join their cluster's verdict so the overlay can mark them; a free dimension
3601 // never reaches a cluster and so keeps no state.
3602 for( PCB_DIMENSION_BASE* dimension : dimensions )
3603 result.dimensionIds.push_back( dimension->m_Uuid );
3604
3605 if( diag.freeDof > 0 )
3606 result.freeDof = diag.freeDof;
3607
3608 result.conflicting = diag.conflicting;
3609 result.redundant = diag.redundant;
3610
3611 return result;
3612}
3613
3614
3615// Fold one cluster's verdict into the board-wide result an empty unbuildable cluster leaves
3616// aResult untouched
3618{
3619 for( const KIID& id : aCluster.shapeIds )
3620 aResult.shapeStates[id] = aCluster.state;
3621
3622 for( const KIID& id : aCluster.dimensionIds )
3623 aResult.shapeStates[id] = aCluster.state;
3624
3625 aResult.totalFreeDof += aCluster.freeDof;
3626
3627 aResult.conflicting.insert( aResult.conflicting.end(), aCluster.conflicting.begin(),
3628 aCluster.conflicting.end() );
3629 aResult.redundant.insert( aResult.redundant.end(), aCluster.redundant.begin(),
3630 aCluster.redundant.end() );
3631 aResult.errored.insert( aResult.errored.end(), aCluster.erroredUnmapped.begin(),
3632 aCluster.erroredUnmapped.end() );
3633}
3634
3635
3636// A constraint with a dangling member is unmappable so it can be flagged by both the adjacency
3637// scan and Build report each errored constraint once here
3639{
3640 std::sort( aResult.errored.begin(), aResult.errored.end() );
3641 aResult.errored.erase( std::unique( aResult.errored.begin(), aResult.errored.end() ),
3642 aResult.errored.end() );
3643}
3644
3645
3647{
3649
3650 if( !aBoard )
3651 return result;
3652
3653 std::vector<PCB_CONSTRAINT*> boardConstraints = collectAllConstraints( aBoard );
3654
3655 if( boardConstraints.empty() )
3656 return result;
3657
3658 std::unordered_set<KIID> onBoard = CollectMemberCandidateIds( aBoard );
3659
3660 std::unordered_map<KIID, std::vector<PCB_CONSTRAINT*>> shapeToConstraints =
3661 buildShapeConstraintMap( aBoard, boardConstraints, &result.errored, &onBoard );
3662
3663 std::set<KIID> visitedShapes;
3664
3665 for( const auto& [seedShape, seedConstraints] : shapeToConstraints )
3666 {
3667 if( visitedShapes.contains( seedShape ) )
3668 continue;
3669
3670 std::unordered_set<KIID> clusterShapes;
3671 std::vector<PCB_CONSTRAINT*> clusterConstraints;
3672 collectConstraintCluster( shapeToConstraints, seedShape, clusterShapes, clusterConstraints,
3673 &visitedShapes );
3674
3676 diagnoseSingleCluster( aBoard, clusterShapes, clusterConstraints, onBoard ) );
3677 }
3678
3680
3681 return result;
3682}
3683
3684
3685// Boost-style mix so a change in any hashed field changes the cluster hash
3686static void hashCombine( std::size_t& aSeed, std::size_t aValue )
3687{
3688 aSeed ^= aValue + 0x9e3779b97f4a7c15ULL + ( aSeed << 6 ) + ( aSeed >> 2 );
3689}
3690
3691
3692static void hashInt( std::size_t& aSeed, long long aValue )
3693{
3694 hashCombine( aSeed, static_cast<std::size_t>( aValue ) );
3695}
3696
3697
3698static void hashDouble( std::size_t& aSeed, double aValue )
3699{
3700 // Hash the exact bit pattern so any coordinate or value change invalidates the cache folding
3701 // both 32-bit halves so a double change is never lost where size_t is only 32 bits wide
3702 std::uint64_t bits = 0;
3703 static_assert( sizeof( bits ) == sizeof( aValue ) );
3704 std::memcpy( &bits, &aValue, sizeof( bits ) );
3705 hashCombine( aSeed, static_cast<std::size_t>( bits & 0xFFFFFFFFULL ) );
3706 hashCombine( aSeed, static_cast<std::size_t>( bits >> 32 ) );
3707}
3708
3709
3710static void hashPoint( std::size_t& aSeed, const VECTOR2I& aPoint )
3711{
3712 hashInt( aSeed, aPoint.x );
3713 hashInt( aSeed, aPoint.y );
3714}
3715
3716
3717static void hashKiid( std::size_t& aSeed, const KIID& aId )
3718{
3719 hashCombine( aSeed, std::hash<KIID>{}( aId ) );
3720}
3721
3722
3723// Hash every solve input a PCB_SHAPE contributes dispatched by kind so no getter is called on a
3724// shape type it asserts for since GetRadius and GetCenter are unimplemented for the wrong kinds
3725static void hashShape( std::size_t& aSeed, const PCB_SHAPE* aShape )
3726{
3727 hashInt( aSeed, static_cast<int>( aShape->GetShape() ) );
3728
3729 // Build freezes a locked shape including via its parent footprint so the lock state changes
3730 // the solve even when the geometry does not
3731 hashInt( aSeed, ConstraintItemIsLocked( aShape ) ? 1 : 0 );
3732
3733 switch( aShape->GetShape() )
3734 {
3735 case SHAPE_T::SEGMENT:
3736 case SHAPE_T::RECTANGLE:
3737 hashPoint( aSeed, aShape->GetStart() );
3738 hashPoint( aSeed, aShape->GetEnd() );
3739 break;
3740
3741 case SHAPE_T::BEZIER:
3742 hashPoint( aSeed, aShape->GetStart() );
3743 hashPoint( aSeed, aShape->GetEnd() );
3744 hashPoint( aSeed, aShape->GetBezierC1() );
3745 hashPoint( aSeed, aShape->GetBezierC2() );
3746 break;
3747
3748 case SHAPE_T::CIRCLE:
3749 hashPoint( aSeed, aShape->GetCenter() );
3750 hashInt( aSeed, aShape->GetRadius() );
3751 break;
3752
3753 case SHAPE_T::POLY:
3754 {
3755 const SHAPE_POLY_SET& poly = aShape->GetPolyShape();
3756
3757 // The outline hole and arc counts gate ingestion so any of them changing must invalidate
3758 // even when no vertex moved
3759 hashInt( aSeed, poly.OutlineCount() );
3760
3761 if( poly.OutlineCount() > 0 )
3762 {
3763 hashInt( aSeed, poly.HoleCount( 0 ) );
3764
3765 const SHAPE_LINE_CHAIN& outline = poly.COutline( 0 );
3766
3767 hashInt( aSeed, static_cast<long long>( outline.ArcCount() ) );
3768
3769 for( int i = 0; i < outline.PointCount(); ++i )
3770 hashPoint( aSeed, outline.CPoint( i ) );
3771 }
3772
3773 break;
3774 }
3775
3776 case SHAPE_T::ARC:
3777 hashPoint( aSeed, aShape->GetStart() );
3778 hashPoint( aSeed, aShape->GetEnd() );
3779 hashPoint( aSeed, aShape->GetCenter() );
3780 hashInt( aSeed, aShape->GetRadius() );
3781 break;
3782
3783 case SHAPE_T::ELLIPSE:
3785 hashPoint( aSeed, aShape->GetEllipseCenter() );
3786 hashInt( aSeed, aShape->GetEllipseMajorRadius() );
3787 hashInt( aSeed, aShape->GetEllipseMinorRadius() );
3788 hashDouble( aSeed, aShape->GetEllipseRotation().AsRadians() );
3789
3790 if( aShape->GetShape() == SHAPE_T::ELLIPSE_ARC )
3791 {
3792 hashPoint( aSeed, aShape->GetStart() );
3793 hashPoint( aSeed, aShape->GetEnd() );
3794 hashDouble( aSeed, aShape->GetEllipseStartAngle().AsRadians() );
3795 hashDouble( aSeed, aShape->GetEllipseEndAngle().AsRadians() );
3796 }
3797
3798 break;
3799
3800 default:
3801 // Build reads the front shape start for the cluster normalization origin even when the kind
3802 // is unmapped so a change there can still shift the solve hash it here too
3803 hashPoint( aSeed, aShape->GetStart() );
3804 break;
3805 }
3806}
3807
3808
3809static void hashDimension( std::size_t& aSeed, const PCB_DIMENSION_BASE* aDimension )
3810{
3811 hashInt( aSeed, static_cast<int>( aDimension->Type() ) );
3812 hashPoint( aSeed, aDimension->GetStart() );
3813 hashPoint( aSeed, aDimension->GetEnd() );
3814
3815 // An orthogonal dimension driving length fixes the axis its orientation selects so a
3816 // horizontal to vertical flip changes the solve without moving the endpoints
3817 if( aDimension->Type() == PCB_DIM_ORTHOGONAL_T )
3818 {
3819 hashInt( aSeed, static_cast<int>(
3820 static_cast<const PCB_DIM_ORTHOGONAL*>( aDimension )->GetOrientation() ) );
3821 }
3822}
3823
3824
3825static void hashConstraint( std::size_t& aSeed, const PCB_CONSTRAINT* aConstraint )
3826{
3827 hashKiid( aSeed, aConstraint->m_Uuid );
3828 hashInt( aSeed, static_cast<int>( aConstraint->GetConstraintType() ) );
3829 hashInt( aSeed, aConstraint->IsDriving() ? 1 : 0 );
3830 hashInt( aSeed, aConstraint->HasValue() ? 1 : 0 );
3831
3832 if( aConstraint->HasValue() )
3833 hashDouble( aSeed, *aConstraint->GetValue() );
3834
3835 for( const CONSTRAINT_MEMBER& member : aConstraint->GetMembers() )
3836 {
3837 hashKiid( aSeed, member.m_item );
3838 hashInt( aSeed, static_cast<int>( member.m_anchor ) );
3839 hashInt( aSeed, member.m_index );
3840 }
3841}
3842
3843
3844// A hash over every input diagnoseSingleCluster would solve on so any geometry value or membership
3845// change invalidates the cached result err toward hashing more since a missed field leaves it stale
3846static std::size_t hashCluster( BOARD* aBoard, const std::unordered_set<KIID>& aClusterShapes,
3847 const std::vector<PCB_CONSTRAINT*>& aClusterConstraints,
3848 const std::unordered_set<KIID>& aOnBoard )
3849{
3850 std::size_t seed = 0;
3851
3852 std::vector<KIID> ids( aClusterShapes.begin(), aClusterShapes.end() );
3853 std::sort( ids.begin(), ids.end() );
3854
3855 for( const KIID& id : ids )
3856 {
3857 BOARD_ITEM* item = resolveMember( aBoard, &aOnBoard, id );
3858
3859 if( PCB_SHAPE* shape = dynamic_cast<PCB_SHAPE*>( item ) )
3860 hashShape( seed, shape );
3861 else if( PCB_DIMENSION_BASE* dimension = dynamic_cast<PCB_DIMENSION_BASE*>( item ) )
3862 hashDimension( seed, dimension );
3863 }
3864
3865 std::vector<PCB_CONSTRAINT*> constraints = aClusterConstraints;
3866 std::sort( constraints.begin(), constraints.end(),
3867 []( const PCB_CONSTRAINT* aLhs, const PCB_CONSTRAINT* aRhs )
3868 { return aLhs->m_Uuid < aRhs->m_Uuid; } );
3869
3870 for( const PCB_CONSTRAINT* constraint : constraints )
3871 hashConstraint( seed, constraint );
3872
3873 return seed;
3874}
3875
3876
3878{
3879 m_cache.clear();
3880}
3881
3882
3884{
3886
3887 if( !aBoard )
3888 {
3889 m_cache.clear();
3890 return result;
3891 }
3892
3893 std::vector<PCB_CONSTRAINT*> boardConstraints = collectAllConstraints( aBoard );
3894
3895 if( boardConstraints.empty() )
3896 {
3897 m_cache.clear();
3898 return result;
3899 }
3900
3901 std::unordered_set<KIID> onBoard = CollectMemberCandidateIds( aBoard );
3902
3903 // The map-scan errored dangling members is recomputed in full every call so it can never go
3904 // stale however aggressively the per-cluster diagnoses are cached
3905 std::unordered_map<KIID, std::vector<PCB_CONSTRAINT*>> shapeToConstraints =
3906 buildShapeConstraintMap( aBoard, boardConstraints, &result.errored, &onBoard );
3907
3908 std::set<KIID> visitedShapes;
3909 std::set<std::vector<KIID>> seenKeys;
3910
3911 for( const auto& [seedShape, seedConstraints] : shapeToConstraints )
3912 {
3913 if( visitedShapes.contains( seedShape ) )
3914 continue;
3915
3916 std::unordered_set<KIID> clusterShapes;
3917 std::vector<PCB_CONSTRAINT*> clusterConstraints;
3918 collectConstraintCluster( shapeToConstraints, seedShape, clusterShapes, clusterConstraints,
3919 &visitedShapes );
3920
3921 // Clusters partition the board constraints so the sorted constraint-id set uniquely
3922 // identifies a cluster and stays put across a geometry or value edit the hash catches those
3923 std::vector<KIID> key;
3924
3925 for( const PCB_CONSTRAINT* constraint : clusterConstraints )
3926 key.push_back( constraint->m_Uuid );
3927
3928 std::sort( key.begin(), key.end() );
3929
3930 std::size_t hash = hashCluster( aBoard, clusterShapes, clusterConstraints, onBoard );
3931 auto it = m_cache.find( key );
3932
3933 if( it == m_cache.end() || it->second.hash != hash )
3934 {
3935 CLUSTER_DIAGNOSIS cluster =
3936 diagnoseSingleCluster( aBoard, clusterShapes, clusterConstraints, onBoard );
3937 m_solveCount++;
3938 it = m_cache.insert_or_assign( key, CACHE_ENTRY{ hash, std::move( cluster ) } ).first;
3939 }
3940
3941 assembleClusterInto( result, it->second.result );
3942 seenKeys.insert( key );
3943 }
3944
3945 // Drop cache entries for clusters no longer present so a stale result can never leak into a
3946 // later pass that happens to rebuild the same key
3947 std::erase_if( m_cache, [&seenKeys]( const auto& entry ) { return !seenKeys.contains( entry.first ); } );
3948
3950
3951 return result;
3952}
int index
std::set< KIID > ConstraintReferenceShapes(BOARD *aBoard, const PCB_CONSTRAINT *aConstraint)
The shapes a just-authored constraint should treat as an immovable reference, for the caller to pass ...
static std::vector< PCB_CONSTRAINT * > collectAllConstraints(BOARD *aBoard)
static void hashCombine(std::size_t &aSeed, std::size_t aValue)
static void hashConstraint(std::size_t &aSeed, const PCB_CONSTRAINT *aConstraint)
static constexpr double STAY_PUT_WEIGHT
static void hashKiid(std::size_t &aSeed, const KIID &aId)
static void hashShape(std::size_t &aSeed, const PCB_SHAPE *aShape)
std::vector< VECTOR2I > ConstraintPinnedPositions(BOARD *aBoard, const std::set< KIID > &aItems)
The distinct board positions that fixed positions pin on aItems.
void ReSolveAfterShapeResize(BOARD *aBoard, PCB_SHAPE *aShape, std::vector< PCB_SHAPE * > *aModified, const std::function< void(BOARD_ITEM *)> &aBeforeModify)
Re-solve after a resize, e.g. a circle radius edit. Holds aShape fixed so its neighbors adjust.
static std::size_t hashCluster(BOARD *aBoard, const std::unordered_set< KIID > &aClusterShapes, const std::vector< PCB_CONSTRAINT * > &aClusterConstraints, const std::unordered_set< KIID > &aOnBoard)
bool BoardHasConstraints(BOARD *aBoard)
True if the board or any of its footprints carries at least one geometric constraint.
static void assembleClusterInto(BOARD_CONSTRAINT_DIAGNOSTICS &aResult, const CLUSTER_DIAGNOSIS &aCluster)
static void collectConstraintCluster(const std::unordered_map< KIID, std::vector< PCB_CONSTRAINT * > > &aMap, const KIID &aSeed, std::unordered_set< KIID > &aClusterShapes, std::vector< PCB_CONSTRAINT * > &aClusterConstraints, std::set< KIID > *aVisited=nullptr)
static constexpr double IU_PER_NORM_UNIT
CONSTRAINT_DIAGNOSIS ApplyConstraintImmediately(BOARD *aBoard, const PCB_CONSTRAINT *aConstraint, std::vector< PCB_SHAPE * > *aModified, const std::function< void(BOARD_ITEM *)> &aBeforeModify, const std::set< KIID > &aFixedShapes)
Solve a just-created constraint's cluster so the geometry snaps to satisfy it (SolidWorks-style),...
static void hashPoint(std::size_t &aSeed, const VECTOR2I &aPoint)
CONSTRAINT_DIAGNOSIS SolveCluster(BOARD *aBoard, const CONSTRAINT_MEMBER &aDragged, const VECTOR2I &aCursor, std::vector< PCB_SHAPE * > *aModified, const std::function< void(BOARD_ITEM *)> &aBeforeModify, bool aIncludeDragged, bool aStabilize, const std::set< KIID > &aEdited, const std::optional< std::pair< CONSTRAINT_MEMBER, VECTOR2I > > &aCoDragged, const std::set< KIID > &aFixedShapes, bool aHoldDraggedRigid)
Gather the cluster of shapes transitively constrained with the dragged shape, solve with the dragged ...
static std::vector< PCB_SHAPE * > resolveClusterShapes(BOARD *aBoard, const std::unordered_set< KIID > &aIds, const std::unordered_set< KIID > *aOnBoard=nullptr)
static void hashDimension(std::size_t &aSeed, const PCB_DIMENSION_BASE *aDimension)
static double arcSweepTarget(double aStartAngle, double aEndAngle, double aSweepDeg)
static CLUSTER_DIAGNOSIS diagnoseSingleCluster(BOARD *aBoard, const std::unordered_set< KIID > &aClusterShapes, const std::vector< PCB_CONSTRAINT * > &aClusterConstraints, const std::unordered_set< KIID > &aOnBoard)
static double directedAngleForCorner(const GCS::Line &aL1, const GCS::Line &aL2, double aCornerDeg)
static constexpr double CURSOR_WEIGHT
void ReSolveShapeClusters(BOARD *aBoard, const std::vector< PCB_SHAPE * > &aShapes, std::vector< PCB_SHAPE * > *aModified, const std::function< void(BOARD_ITEM *)> &aBeforeModify)
Re-solve the clusters of shapes edited outside the solver, e.g.
static void hashInt(std::size_t &aSeed, long long aValue)
bool ReSolveShapeClustersHoldingEdited(BOARD *aBoard, const std::vector< PCB_SHAPE * > &aEditedShapes, std::vector< PCB_SHAPE * > *aModified, const std::function< void(BOARD_ITEM *)> &aBeforeModify)
Re-solve clusters whose new geometry is authoritative holding every edited shape fully fixed so only ...
bool ConstraintItemIsLocked(const BOARD_ITEM *aItem)
True when the solver must treat aItem as immovable, either locked itself or living inside a locked fo...
static void dedupErrored(BOARD_CONSTRAINT_DIAGNOSTICS &aResult)
static std::unordered_map< KIID, std::vector< PCB_CONSTRAINT * > > buildShapeConstraintMap(BOARD *aBoard, const std::vector< PCB_CONSTRAINT * > &aConstraints, std::vector< KIID > *aErrored=nullptr, const std::unordered_set< KIID > *aOnBoard=nullptr)
static std::vector< PCB_DIMENSION_BASE * > resolveClusterDimensions(BOARD *aBoard, const std::unordered_set< KIID > &aIds, const std::unordered_set< KIID > *aOnBoard=nullptr)
BOARD_CONSTRAINT_DIAGNOSTICS DiagnoseBoardConstraints(BOARD *aBoard)
Diagnose every constraint cluster on the board (validate only – geometry is not changed) and return t...
static constexpr int MAX_SOLVE_ITERATIONS
static BOARD_ITEM * resolveMember(BOARD *aBoard, const std::unordered_set< KIID > *aOnBoard, const KIID &aId)
static std::vector< T * > resolveClusterItems(BOARD *aBoard, const std::unordered_set< KIID > &aIds, const std::unordered_set< KIID > *aOnBoard)
static bool isDegenerateLine(const GCS::Line &aLine)
static void hashDouble(std::size_t &aSeed, double aValue)
@ OVER_CONSTRAINED
In a cluster the solver reports as conflicting.
@ UNDER_CONSTRAINED
In a cluster with remaining free degrees of freedom.
@ WELL_CONSTRAINED
In a fully-determined cluster (zero free DOF).
bool ConstraintItemIsLocked(const BOARD_ITEM *aItem)
True when the solver must treat aItem as immovable, either locked itself or living inside a locked fo...
constexpr BOX2I KiROUND(const BOX2D &aBoxD)
Definition box2.h:982
Translates KiCad board geometry to and from the planegcs solver (issue #2329).
void recordReferenceValue(PCB_CONSTRAINT *aConstraint)
Note a non-driving valued constraint so its measured value can be read back after a solve.
void holdShapesRigid(int aTag, const std::set< KIID > &aShapes)
Hold the shapes in aShapes rigid, tagged aTag, so a shape the solve moves translates instead of stret...
int m_coDragTargetY
Backing slot for the co-dragged pin y target.
PCB_DIM_ORTHOGONAL * orthogonalDimensionForMembers(const std::vector< CONSTRAINT_MEMBER > &aMembers) const
The orthogonal dimension a two-point length constraint drives or nullptr requires both members to be ...
std::vector< KIID > m_unmapped
Constraints Build() could not map (not enforced).
double denormalizeX(double aNorm) const
std::set< int > m_fixedParams
Parameter indices the solver may not move, so a rigid translation may not either.
void softPinPoint(const ANCHOR_PARAMS &aPoint, int aTag, std::optional< double > aWeight=std::nullopt)
Pin the point at aPoint where it sits tagged aTag with aWeight rescaling the tier or the default weig...
bool addSnapRelations(const ANCHOR_PARAMS &aAnchor, const std::vector< SNAP_CANDIDATE > &aCandidates, const VECTOR2I &aOffset)
void pinUneditedShapes(const std::set< KIID > &aEdited, int aTag)
Soft-pin every cluster shape not in aEdited at its current geometry tagged aTag for a minimal-movemen...
bool Solve(const CONSTRAINT_MEMBER &aDragged, const VECTOR2I &aCursor, bool aStabilize=false, const std::set< KIID > &aEdited={}, const std::optional< std::pair< CONSTRAINT_MEMBER, VECTOR2I > > &aCoDragged=std::nullopt, bool aHoldDraggedRigid=false)
Solve the system, pinning a dragged anchor to a cursor position.
RIGID_STATE collectRigidState(const std::set< KIID > &aEditedShapes) const
int m_dragTargetX
Stable backing slot for the drag pin's x target (-1 = unset).
void holdFreeSegmentLengths(int aTag, const std::set< KIID > &aShapes)
Length hold on the free segments in aShapes tagged aTag so only those shapes are protected while a me...
std::vector< PCB_CONSTRAINT * > m_referenceConstraints
Non-driving valued, read back after a solve.
bool solveSucceeded(int aSolveResult)
Decide whether a solve reached a usable result a raw Success or Converged always qualifies while a Fa...
void holdRigidRadii(const std::vector< RIGID_RADIUS_HOLD > &aRadii, int aTag)
const std::vector< KIID > & UnmappedConstraints() const
Constraints from the last Build() that could not be mapped onto a solver primitive (wrong member coun...
ANCHOR_PARAMS anchorParams(const CONSTRAINT_MEMBER &aMember) const
Indices into m_params of the coordinates an anchor maps to invalid if the shape has no such anchor fo...
std::map< KIID, SHAPE_VARS > m_shapeVars
bool SolveAfterResize(const KIID &aResizedShape)
Solve after a resize.
int m_coDragTargetX
Backing slot for the co-dragged pin x target.
CONSTRAINT_SYSTEM_2D::SNAPSHOT SNAPSHOT
double denormalizeY(double aNorm) const
void pinDraggedShapeRest(const CONSTRAINT_MEMBER &aDragged, int aTag, const CONSTRAINT_MEMBER *aCoDragged=nullptr)
Hold the parts of the dragged shape meant to stay put tagged aTag a segment holds its far endpoint an...
std::map< int, std::vector< KIID > > m_tagMembers
Member items per tag, for collapse attribution.
int m_dragTargetY
Stable backing slot for the drag pin's y target.
double m_scale
IU per normalized unit.
std::optional< VECTOR2I > AnchorPosition(const CONSTRAINT_MEMBER &aMember) const
bool Restore(const SNAPSHOT &aSnapshot)
bool SolveRigidTranslation(const std::set< KIID > &aEditedShapes, const VECTOR2I &aTranslation)
std::map< int, KIID > m_tagToConstraint
std::set< KIID > m_angleConstrainedShapes
Shapes a direction or angle constraint could collapse to a point only these get a stabilize length or...
void holdFreeArcRadii(int aTag, const std::set< KIID > &aShapes)
Radius hold on the free arcs in aShapes tagged aTag so an angle change rotates an endpoint instead of...
bool SolveRigidSnapRelations(const std::set< KIID > &aEditedShapes, const VECTOR2I &aReference, const std::vector< SNAP_CANDIDATE > &aCandidates, const VECTOR2I &aCursor, VECTOR2I &aResolvedCursor)
bool Build(const std::vector< PCB_SHAPE * > &aShapes, const std::vector< PCB_CONSTRAINT * > &aConstraints, const std::set< KIID > *aFixedShapes=nullptr, const std::vector< PCB_DIMENSION_BASE * > &aDimensions={})
Translate a cluster into a planegcs system.
void holdPolygonVertices(const std::set< KIID > &aShapes, int aTag)
Soft-pin every vertex of each POLYGON in aShapes tagged aTag for edited shapes pinUneditedShapes excl...
std::vector< PCB_SHAPE * > Apply(const std::function< void(BOARD_ITEM *)> &aBeforeWrite={})
Write the solved coordinates back into the shapes, de-normalized to IU.
bool TravelsWhole(const std::set< KIID > &aEditedShapes) const
True when every shape in the cluster is in aEditedShapes and free to travel with it.
int pushParam(double aValue)
Append a normalized coordinate to the backing store, returning its stable index.
CONSTRAINT_DIAGNOSIS Diagnose()
Report degrees of freedom and conflicting/redundant constraints.
std::set< int > m_nonDrivingTags
Measurement-only; excluded from conflict residuals.
double normalizeX(int aIU) const
IU <-> normalized (millimetre, cluster-centred) frame, per axis.
@ RECTANGLE
An axis-aligned rectangle whose four corners alias the two stored corners params so rectness holds by...
@ POINT_PAIR
A dimension's two feature points (start + end); no line/curve geometry.
@ POLYGON
A single hole-free outline with one free param pair per vertex since write-back rebuilds one outline ...
@ BEZIER
A cubic bezier only its start and end endpoints are exposed as free points.
bool SolveSnapRelations(const CONSTRAINT_MEMBER &aDragged, const std::vector< SNAP_CANDIDATE > &aCandidates, const VECTOR2I &aCursor)
void ApplyReferenceValues(const std::function< void(BOARD_ITEM *)> &aBeforeWrite={})
Propagate solved reference (non-driving) constraint values back into their m_value so a reference dim...
void holdArcRadius(const SHAPE_VARS &aVars, int aTag)
Hold aVars's arc at its current radius (tagged aTag).
BOARD_CONSTRAINT_DIAGNOSTICS Diagnose(BOARD *aBoard)
Diagnose every cluster reusing cached per-cluster results whose solve inputs are unchanged.
std::map< std::vector< KIID >, CACHE_ENTRY > m_cache
void Clear()
Drop the cache call when the board or view reloads and item-identity assumptions break.
bool IsExactFeasible(const VECTOR2I &aTarget)
bool Build(BOARD *aBoard, const CONSTRAINT_MEMBER &aDragged)
CONSTRAINT_DIAGNOSIS Solve(const VECTOR2I &aCursor, std::vector< PCB_SHAPE * > *aModified, const std::function< void(BOARD_ITEM *)> &aBeforeModify, bool aIncludeDragged, bool aStabilize, const std::set< KIID > &aEdited={}, const std::optional< std::pair< CONSTRAINT_MEMBER, VECTOR2I > > &aCoDragged=std::nullopt)
SNAP_RESULT ResolveCandidates(const SNAP_SOURCE_CONTEXT &aContext, const std::vector< SNAP_CANDIDATE > &aCandidates)
bool Matches(const CONSTRAINT_MEMBER &aDragged) const
bool feasibleAt(const VECTOR2I &aTarget)
bool Solve(const VECTOR2I &aTarget, std::vector< PCB_SHAPE * > *aModified, const std::function< void(BOARD_ITEM *)> &aBeforeModify)
bool Build(BOARD *aBoard, const std::vector< PCB_SHAPE * > &aEditedShapes, const VECTOR2I &aReference)
SNAP_RESULT ResolveCandidates(const SNAP_SOURCE_CONTEXT &aContext, const std::vector< SNAP_CANDIDATE > &aCandidates)
BOARD_CONSTRAINT_ADAPTER::SNAPSHOT m_baseline
bool rewind()
Rewind to the state the next speculative solve starts from.
SNAP_RESULT startResult(const SNAP_SOURCE_CONTEXT &aContext) const
Seed a snap result at the cursor, rejecting a cluster that was already broken.
void reset()
Drop the built cluster, leaving the session unusable until the next successful build.
bool buildCluster(BOARD *aBoard, const std::unordered_set< KIID > &aClusterShapes, const std::vector< PCB_CONSTRAINT * > &aConstraints)
Assemble the adapter for one cluster and record its baseline.
std::unique_ptr< BOARD_CONSTRAINT_ADAPTER > m_adapter
static void accept(SNAP_RESULT &aResult, const SNAP_CANDIDATE &aCandidate, double aResidual)
Record one candidate the session honoured.
A base class for any item which can be embedded within the BOARD container class, and therefore insta...
Definition board_item.h:85
bool IsLocked() const override
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
BOARD_ITEM * ResolveItem(const KIID &aID, bool aAllowNullptrReturn=false) const
Definition board.cpp:2142
Represent basic circle geometry with utility geometry functions.
Definition circle.h:33
double AsDegrees() const
Definition eda_angle.h:115
double AsRadians() const
Definition eda_angle.h:119
const KIID m_Uuid
Definition eda_item.h:599
KICAD_T Type() const
Returns the type of object.
Definition eda_item.h:110
EDA_ANGLE GetArcAngle() const
int GetEllipseMinorRadius() const
Definition eda_shape.h:403
const VECTOR2I & GetBezierC2() const
Definition eda_shape.h:376
const VECTOR2I & GetEllipseCenter() const
Definition eda_shape.h:385
EDA_ANGLE GetEllipseEndAngle() const
Definition eda_shape.h:431
int GetEllipseMajorRadius() const
Definition eda_shape.h:394
SHAPE_POLY_SET & GetPolyShape()
EDA_ANGLE GetEllipseRotation() const
Definition eda_shape.h:412
int GetRadius() const
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
EDA_ANGLE GetEllipseStartAngle() const
Definition eda_shape.h:422
const VECTOR2I & GetBezierC1() const
Definition eda_shape.h:373
virtual void SetTextPos(const VECTOR2I &aPoint)
Definition eda_text.cpp:556
Definition kiid.h:46
A geometric constraint between board items (issue #2329).
const std::vector< CONSTRAINT_MEMBER > & GetMembers() const
std::optional< double > GetValue() const
bool IsDriving() const
A driving constraint forces its value; a reference (non-driving) one only measures it.
PCB_CONSTRAINT_TYPE GetConstraintType() const
bool HasValue() const
Abstract dimension API.
virtual VECTOR2I GetEnd() const
virtual VECTOR2I GetStart() const
The dimension's origin is the first feature point for the dimension.
An orthogonal dimension is like an aligned dimension, but the extension lines are locked to the X or ...
A radial dimension indicates either the radius or diameter of an arc or circle.
VECTOR2I GetKnee() const
VECTOR2I GetCenter() const override
This defaults to the center of the bounding box if not overridden.
Definition pcb_shape.h:80
VECTOR2I GetTextPos() const override
Definition pcb_text.cpp:461
Definition seg.h:38
Represent a polyline containing arcs as well as line segments: A chain of connected line and/or arc s...
int PointCount() const
Return the number of points (vertices) in this line chain.
const VECTOR2I & CPoint(int aIndex) const
Return a reference to a given point in the line chain.
size_t ArcCount() const
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
SNAP_RESULT Resolve(const SNAP_SOURCE_CONTEXT &aContext) const
void AddCandidate(SNAP_CANDIDATE aCandidate)
constexpr extended_type Dot(const VECTOR2< T > &aVector) const
Compute dot product of self with aVector.
Definition vector2d.h:567
bool IsConstrainableItem(const BOARD_ITEM *aItem)
True when aItem is one ResolveConstrainableItem() accepts: a PCB_SHAPE or a dimension.
EDA_ANGLE MeasureCornerAngle(const SEG &aA, const SEG &aB)
The corner angle between two segments, in the closed range [0, 180] degrees.
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::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< 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...
@ RADIANS_T
Definition eda_angle.h:31
@ 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
EDA_ANGLE abs(const EDA_ANGLE &aAngle)
Definition eda_angle.h:437
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.
Param indices of an anchor coordinates a rect corner aliases mixed start end params so y is not alway...
int radius
radius scalar (circle, arc) / minor radius (ellipse).
bool startIsLeft
Rect corner roles frozen at Build so VERTEX 0 to 3 as TL TR BR BL bind the same physical corners what...
int fixedLengthParam
param index of a driving fixed-length target, or -1.
int focusX
first focus.x (ellipse kinds only).
int startX
start.x (segment) / center.x (circle, arc, ellipse).
PCB_DIMENSION_BASE * dimension
set instead of shape for a POINT_PAIR
int vertexCount
Outline-0 vertex count of a POLYGON vertex i x param is startX plus 2 times i.
Board-wide diagnostics for the constraint overlay and info bar.
std::map< KIID, CONSTRAINT_STATE > shapeStates
std::vector< KIID > errored
Invalid constraints (member missing, deleted, or of a kind incompatible with the type).
One cluster's diagnosis, the unit DiagnoseBoardConstraints assembles the board-wide result from and B...
int freeDof
Remaining free DOF folded into the board total.
std::vector< KIID > dimensionIds
Cluster dimensions.
std::vector< KIID > conflicting
std::vector< KIID > erroredUnmapped
Constraints Build could not map and so not enforced.
std::vector< KIID > shapeIds
Cluster shapes in the order the state is written.
std::vector< KIID > redundant
The outcome of a constraint solve, in plain data so callers need not know planegcs.
bool solved
Solver reached Success or Converged.
std::vector< KIID > conflicting
Constraints the solver reports as over-constraining.
std::vector< KIID > redundant
Constraints the solver reports as redundant.
int freeDof
Remaining degrees of freedom (-1 if not diagnosed).
One participant in a constraint: a referenced board item plus the feature of that item that participa...
KIID m_item
Referenced board item, usually a PCB_SHAPE.
CONSTRAINT_ANCHOR m_anchor
Which feature of that item participates.
int m_index
Vertex ordinal; only meaningful for the VERTEX anchor.
std::vector< double > quantizedResiduals
VECTOR2I position
bool Accepted(const SNAP_STABLE_ID &aId) const
bool moved
std::vector< double > vA
KIBIS_PIN * pin
VECTOR2I center
int radius
VECTOR2I end
SHAPE_CIRCLE circle(c.m_circle_center, c.m_circle_radius)
wxString result
Test unit parsing edge cases and error handling.
#define M_PI
@ 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
constexpr int sign(T val)
Definition util.h:167
VECTOR2< int32_t > VECTOR2I
Definition vector2d.h:708
VECTOR2< double > VECTOR2D
Definition vector2d.h:707