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