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;
58
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( std::make_unique<GCS::System>() )
289{
290}
291
292
294{
295 // GCS::System frees the Constraint objects it owns; m_params outlives it (member order).
296 m_gcs->clear();
297}
298
299
301{
302 // Pin and hold targets pushed here never reclaimed since the deque cannot shrink while GCS holds
303 // pointers into it and growth stays bounded since every caller builds a fresh adapter per solve
304 m_params.push_back( aValue );
305 return static_cast<int>( m_params.size() ) - 1;
306}
307
308
310{
311 if( aConstraint && !aConstraint->IsDriving() )
312 m_referenceConstraints.push_back( aConstraint );
313}
314
315
316void BOARD_CONSTRAINT_ADAPTER::ApplyReferenceValues( const std::function<void( BOARD_ITEM* )>& aBeforeWrite )
317{
318 if( !m_built )
319 return;
320
321 for( PCB_CONSTRAINT* constraint : m_referenceConstraints )
322 {
323 const std::vector<CONSTRAINT_MEMBER>& members = constraint->GetMembers();
324
325 if( members.empty() )
326 continue;
327
328 auto it = m_shapeVars.find( members.front().m_item );
329
330 if( it == m_shapeVars.end() )
331 continue;
332
333 const PCB_SHAPE* shape = it->second.shape;
334 std::optional<double> value;
335 double tol = 1.0; // IU for a length/radius; degrees for an angle
336
337 // Measure from the shape geometry Apply() just wrote, not the solver param, so a settled
338 // solve re-measures the same rounded value and never churns undo.
339 switch( constraint->GetConstraintType() )
340 {
342
343 // A two-point form has no owning segment so re-measure the distance between its two
344 // member anchors from the same solved params Apply rounded from
345 if( members.size() == 2 )
346 {
347 auto anchorPos = [&]( const CONSTRAINT_MEMBER& aMember ) -> std::optional<VECTOR2I>
348 {
349 ANCHOR_PARAMS anchor = anchorParams( aMember );
350
351 if( !anchor.IsValid() )
352 return std::nullopt;
353
356 };
357
358 std::optional<VECTOR2I> pa = anchorPos( members[0] );
359 std::optional<VECTOR2I> pb = anchorPos( members[1] );
360
361 if( pa && pb )
362 {
363 // An orthogonal dimension measures one axis so re-measure that axis component
364 // an aligned or radial two-point form measures the euclidean distance instead
366 {
367 bool horizontal = ortho->GetOrientation() == PCB_DIM_ORTHOGONAL::DIR::HORIZONTAL;
368 value = horizontal ? std::abs( pb->x - pa->x ) : std::abs( pb->y - pa->y );
369 }
370 else
371 {
372 value = ( *pb - *pa ).EuclideanNorm();
373 }
374 }
375 }
376 else
377 {
378 value = ( shape->GetEnd() - shape->GetStart() ).EuclideanNorm();
379 }
380
381 break;
382
384 value = shape->GetRadius();
385 break;
386
388 {
389 if( members.size() != 2 )
390 break;
391
392 auto other = m_shapeVars.find( members[1].m_item );
393
394 if( other == m_shapeVars.end() )
395 break;
396
397 const PCB_SHAPE* shapeB = other->second.shape;
398 value = MeasureCornerAngle( SEG( shape->GetStart(), shape->GetEnd() ),
399 SEG( shapeB->GetStart(), shapeB->GetEnd() ) ).AsDegrees();
400 tol = 1e-3;
401 break;
402 }
403
405 value = shape->GetArcAngle().AsDegrees();
406 tol = 1e-3;
407 break;
408
409 default:
410 break;
411 }
412
413 if( !value )
414 continue;
415
416 std::optional<double> current = constraint->GetValue();
417
418 if( current && std::abs( *value - *current ) <= tol )
419 continue;
420
421 if( aBeforeWrite )
422 aBeforeWrite( constraint );
423
424 constraint->SetValue( value );
425 }
426}
427
428
431{
432 auto it = m_shapeVars.find( aMember.m_item );
433
434 if( it == m_shapeVars.end() )
435 return {};
436
437 const SHAPE_VARS& vars = it->second;
438
439 // Segment bezier and dimension point-pair store endpoints in startX and endX while a circle or
440 // closed ellipse parks its centre in startX instead so those kinds must never alias endpoints
441 bool hasEndpoints = vars.kind == SHAPE_KIND::SEGMENT || vars.kind == SHAPE_KIND::BEZIER
442 || vars.kind == SHAPE_KIND::POINT_PAIR;
443
444 // Consecutive x and y pair for these kinds
445 auto pairAt = []( int aXIndex ) -> ANCHOR_PARAMS
446 {
447 return aXIndex >= 0 ? ANCHOR_PARAMS{ aXIndex, aXIndex + 1 } : ANCHOR_PARAMS();
448 };
449
450 // A rect exposes only its four indexed corners each an alias over a mixed start and end pair so
451 // START END and CENTER never resolve indices 0 to 3 follow the canonical TL TR BR BL order
452 if( vars.kind == SHAPE_KIND::RECT )
453 {
454 if( aMember.m_anchor != CONSTRAINT_ANCHOR::VERTEX || aMember.m_index < 0 || aMember.m_index > 3 )
455 return {};
456
457 int leftX = vars.startIsLeft ? vars.startX : vars.endX;
458 int rightX = vars.startIsLeft ? vars.endX : vars.startX;
459 int topY = ( vars.startIsTop ? vars.startX : vars.endX ) + 1;
460 int botY = ( vars.startIsTop ? vars.endX : vars.startX ) + 1;
461
462 switch( aMember.m_index )
463 {
464 case 0: return { leftX, topY };
465 case 1: return { rightX, topY };
466 case 2: return { rightX, botY };
467 default: return { leftX, botY };
468 }
469 }
470
471 // A polygon exposes only its indexed outline-0 vertices each a consecutive pair at
472 // startX plus 2 times index a stale index from a shrunk outline resolves to nothing
473 if( vars.kind == SHAPE_KIND::POLYGON )
474 {
475 if( aMember.m_anchor != CONSTRAINT_ANCHOR::VERTEX || aMember.m_index < 0
476 || aMember.m_index >= vars.vertexCount )
477 {
478 return {};
479 }
480
481 return pairAt( vars.startX + 2 * aMember.m_index );
482 }
483
484 switch( aMember.m_anchor )
485 {
487 if( vars.kind == SHAPE_KIND::ARC || vars.kind == SHAPE_KIND::ELLIPSE_ARC )
488 return pairAt( vars.arcStartX );
489
490 return hasEndpoints ? pairAt( vars.startX ) : ANCHOR_PARAMS();
492 if( vars.kind == SHAPE_KIND::ARC || vars.kind == SHAPE_KIND::ELLIPSE_ARC )
493 return pairAt( vars.arcEndX );
494
495 return hasEndpoints ? pairAt( vars.endX ) : ANCHOR_PARAMS();
497 return hasEndpoints ? ANCHOR_PARAMS() : pairAt( vars.startX );
498 default:
499 return {};
500 }
501}
502
503
505 const std::vector<CONSTRAINT_MEMBER>& aMembers ) const
506{
507 if( aMembers.size() != 2 || aMembers[0].m_item != aMembers[1].m_item )
508 return nullptr;
509
510 const CONSTRAINT_ANCHOR a0 = aMembers[0].m_anchor;
511 const CONSTRAINT_ANCHOR a1 = aMembers[1].m_anchor;
512
513 const bool startEnd = ( a0 == CONSTRAINT_ANCHOR::START && a1 == CONSTRAINT_ANCHOR::END )
515
516 if( !startEnd )
517 return nullptr;
518
519 auto it = m_shapeVars.find( aMembers[0].m_item );
520
521 if( it == m_shapeVars.end() || it->second.kind != SHAPE_KIND::POINT_PAIR || !it->second.dimension
522 || it->second.dimension->Type() != PCB_DIM_ORTHOGONAL_T )
523 {
524 return nullptr;
525 }
526
527 return static_cast<PCB_DIM_ORTHOGONAL*>( it->second.dimension );
528}
529
530
531bool BOARD_CONSTRAINT_ADAPTER::Build( const std::vector<PCB_SHAPE*>& aShapes,
532 const std::vector<PCB_CONSTRAINT*>& aConstraints,
533 const std::set<KIID>* aFixedShapes,
534 const std::vector<PCB_DIMENSION_BASE*>& aDimensions )
535{
536 m_params.clear();
537 m_shapeVars.clear();
538 m_tagToConstraint.clear();
539 m_nonDrivingTags.clear();
540 m_tagMembers.clear();
542 m_unmapped.clear();
544 m_gcs->clear();
545 m_built = false;
546
547 if( aShapes.empty() && aDimensions.empty() )
548 return false;
549
550 // Centre on the first item's start point so normalized coordinates stay small for a board far
551 // from the origin.
552 VECTOR2I origin = !aShapes.empty() ? aShapes.front()->GetStart() : aDimensions.front()->GetStart();
553 m_originX = origin.x;
554 m_originY = origin.y;
556 m_invScale = 1.0 / m_scale;
557
558 // [first, last) parameter spans of locked shapes, folded into fixedParams below so the solver
559 // treats a locked shape as an immovable reference.
560 std::vector<std::pair<int, int>> lockedRanges;
561
562 // Fixed focus-offset params of ellipses, folded into fixedParams below.
563 std::vector<int> ellipseOffsetParams;
564
565 for( PCB_SHAPE* shape : aShapes )
566 {
567 SHAPE_VARS vars;
568 vars.shape = shape;
569 int firstParam = static_cast<int>( m_params.size() );
570
571 if( shape->GetShape() == SHAPE_T::SEGMENT )
572 {
574 vars.startX = pushParam( normalizeX( shape->GetStart().x ) );
575 pushParam( normalizeY( shape->GetStart().y ) );
576 vars.endX = pushParam( normalizeX( shape->GetEnd().x ) );
577 pushParam( normalizeY( shape->GetEnd().y ) );
578 }
579 else if( shape->GetShape() == SHAPE_T::RECTANGLE )
580 {
581 // Only the two stored corners are params the four VERTEX corners alias mixed pairs of
582 // them through anchorParams so rectness can never be violated corner roles frozen here
583 vars.kind = SHAPE_KIND::RECT;
584 vars.startIsLeft = shape->GetStart().x <= shape->GetEnd().x;
585 vars.startIsTop = shape->GetStart().y <= shape->GetEnd().y;
586 vars.startX = pushParam( normalizeX( shape->GetStart().x ) );
587 pushParam( normalizeY( shape->GetStart().y ) );
588 vars.endX = pushParam( normalizeX( shape->GetEnd().x ) );
589 pushParam( normalizeY( shape->GetEnd().y ) );
590 }
591 else if( shape->GetShape() == SHAPE_T::POLY )
592 {
593 // Only a single hole-free arc-free outline is modeled since write-back rebuilds one
594 // outline and would destroy anything else so other polys are skipped and read unmapped
595 if( !ConstraintPolygonIsModelable( shape ) )
596 continue;
597
598 const SHAPE_LINE_CHAIN& outline = shape->GetPolyShape().COutline( 0 );
599
601 vars.vertexCount = outline.PointCount();
602 vars.startX = pushParam( normalizeX( outline.CPoint( 0 ).x ) );
603 pushParam( normalizeY( outline.CPoint( 0 ).y ) );
604
605 for( int i = 1; i < outline.PointCount(); ++i )
606 {
607 pushParam( normalizeX( outline.CPoint( i ).x ) );
608 pushParam( normalizeY( outline.CPoint( i ).y ) );
609 }
610 }
611 else if( shape->GetShape() == SHAPE_T::BEZIER )
612 {
613 // Only the endpoints participate in the solve the control handles are not solver
614 // variables and follow their adjacent endpoint in Apply
616 vars.startX = pushParam( normalizeX( shape->GetStart().x ) );
617 pushParam( normalizeY( shape->GetStart().y ) );
618 vars.endX = pushParam( normalizeX( shape->GetEnd().x ) );
619 pushParam( normalizeY( shape->GetEnd().y ) );
620 }
621 else if( shape->GetShape() == SHAPE_T::CIRCLE )
622 {
624 vars.startX = pushParam( normalizeX( shape->GetCenter().x ) );
625 pushParam( normalizeY( shape->GetCenter().y ) );
626 vars.radius = pushParam( shape->GetRadius() * m_invScale );
627 }
628 else if( shape->GetShape() == SHAPE_T::ARC )
629 {
630 vars.kind = SHAPE_KIND::ARC;
631
632 VECTOR2I center = shape->GetCenter();
633 VECTOR2I start = shape->GetStart();
634 VECTOR2I end = shape->GetEnd();
635
636 double cx = normalizeX( center.x );
637 double cy = normalizeY( center.y );
638 double sx = normalizeX( start.x );
639 double sy = normalizeY( start.y );
640 double ex = normalizeX( end.x );
641 double ey = normalizeY( end.y );
642
643 vars.startX = pushParam( cx );
644 pushParam( cy );
645 vars.radius = pushParam( shape->GetRadius() * m_invScale );
646 vars.arcStartX = pushParam( sx );
647 pushParam( sy );
648 vars.arcEndX = pushParam( ex );
649 pushParam( ey );
650 vars.startAngle = pushParam( std::atan2( sy - cy, sx - cx ) );
651 vars.endAngle = pushParam( std::atan2( ey - cy, ex - cx ) );
652
653 GCS::Arc arc;
654 arc.center = GCS::Point{ &m_params[vars.startX], &m_params[vars.startX + 1] };
655 arc.rad = &m_params[vars.radius];
656 arc.start = GCS::Point{ &m_params[vars.arcStartX], &m_params[vars.arcStartX + 1] };
657 arc.end = GCS::Point{ &m_params[vars.arcEndX], &m_params[vars.arcEndX + 1] };
658 arc.startAngle = &m_params[vars.startAngle];
659 arc.endAngle = &m_params[vars.endAngle];
660 m_gcs->addConstraintArcRules( arc );
661 }
662 else if( shape->GetShape() == SHAPE_T::ELLIPSE || shape->GetShape() == SHAPE_T::ELLIPSE_ARC )
663 {
665
666 VECTOR2I center = shape->GetEllipseCenter();
667 double major = shape->GetEllipseMajorRadius();
668 double minor = shape->GetEllipseMinorRadius();
669 double phi = shape->GetEllipseRotation().AsRadians();
670
671 // GCS parameterizes an ellipse as center + first focus + minor radius.
672 double focal = major > minor ? std::sqrt( major * major - minor * minor ) : 0.0;
673 double cx = normalizeX( center.x );
674 double cy = normalizeY( center.y );
675
676 vars.startX = pushParam( cx );
677 pushParam( cy );
678 vars.focusX = pushParam( cx + focal * m_invScale * std::cos( phi ) );
679 pushParam( cy + focal * m_invScale * std::sin( phi ) );
680 vars.radius = pushParam( std::min( major, minor ) * m_invScale );
681
682 // Tie the focus to the center, or a solve moving the center would leave the focus
683 // behind and distort the ellipse.
684 int offX = pushParam( focal * m_invScale * std::cos( phi ) );
685 int offY = pushParam( focal * m_invScale * std::sin( phi ) );
686 ellipseOffsetParams.push_back( offX );
687 ellipseOffsetParams.push_back( offY );
688
689 m_gcs->addConstraintDifference( &m_params[vars.startX], &m_params[vars.focusX], &m_params[offX] );
690 m_gcs->addConstraintDifference( &m_params[vars.startX + 1], &m_params[vars.focusX + 1], &m_params[offY] );
691
692 if( vars.kind == SHAPE_KIND::ELLIPSE_ARC )
693 {
694 VECTOR2I start = shape->GetStart();
695 VECTOR2I end = shape->GetEnd();
696
697 vars.arcStartX = pushParam( normalizeX( start.x ) );
698 pushParam( normalizeY( start.y ) );
699 vars.arcEndX = pushParam( normalizeX( end.x ) );
700 pushParam( normalizeY( end.y ) );
701 vars.startAngle = pushParam( shape->GetEllipseStartAngle().AsRadians() );
702 vars.endAngle = pushParam( shape->GetEllipseEndAngle().AsRadians() );
703
704 GCS::ArcOfEllipse arc;
705 arc.center = GCS::Point{ &m_params[vars.startX], &m_params[vars.startX + 1] };
706 arc.focus1 = GCS::Point{ &m_params[vars.focusX], &m_params[vars.focusX + 1] };
707 arc.radmin = &m_params[vars.radius];
708 arc.start = GCS::Point{ &m_params[vars.arcStartX], &m_params[vars.arcStartX + 1] };
709 arc.end = GCS::Point{ &m_params[vars.arcEndX], &m_params[vars.arcEndX + 1] };
710 arc.startAngle = &m_params[vars.startAngle];
711 arc.endAngle = &m_params[vars.endAngle];
712 m_gcs->addConstraintArcOfEllipseRules( arc );
713 }
714 }
715 else
716 {
717 continue; // other shapes are not mapped
718 }
719
720 // Freeze a locked or caller-pinned shape's whole span so the solver moves only the rest of
721 // the cluster.
722 if( ConstraintItemIsLocked( shape ) || ( aFixedShapes && aFixedShapes->count( shape->m_Uuid ) ) )
723 lockedRanges.emplace_back( firstParam, static_cast<int>( m_params.size() ) );
724
725 m_shapeVars[shape->m_Uuid] = vars;
726 }
727
728 // Each dimension contributes its two feature points, so a coincident constraint can pull the
729 // dimension along with the shape it is bound to.
730 for( PCB_DIMENSION_BASE* dimension : aDimensions )
731 {
732 SHAPE_VARS vars;
734 vars.dimension = dimension;
735 vars.startX = pushParam( normalizeX( dimension->GetStart().x ) );
736 pushParam( normalizeY( dimension->GetStart().y ) );
737
738 // Only aligned/orthogonal/radial dimensions have a second feature point; a leader or centre
739 // mark's end is a control point, so it is never a bindable anchor (endX stays -1).
740 switch( dimension->Type() )
741 {
744 case PCB_DIM_RADIAL_T:
745 vars.endX = pushParam( normalizeX( dimension->GetEnd().x ) );
746 pushParam( normalizeY( dimension->GetEnd().y ) );
747 break;
748
749 default:
750 break;
751 }
752
753 m_shapeVars[dimension->m_Uuid] = vars;
754 }
755
756 // Params the solver may not change. Grounded points, driving constants (lengths, radii) and
757 // locked-shape params go here; everything else stays an unknown.
758 std::set<int> fixedParams;
759
760 for( const auto& [first, last] : lockedRanges )
761 {
762 for( int i = first; i < last; ++i )
763 fixedParams.insert( i );
764 }
765
766 for( int i : ellipseOffsetParams )
767 fixedParams.insert( i );
768
769 auto pointAt = [&]( int aXIndex ) -> GCS::Point
770 {
771 return GCS::Point{ &m_params[aXIndex], &m_params[aXIndex + 1] };
772 };
773
774 // Anchor x and y need not be consecutive since a rect corner aliases mixed start end params
775 // pointAt stays for SHAPE_VARS internal consecutive pairs
776 auto pointFor = [&]( const ANCHOR_PARAMS& aParams ) -> GCS::Point
777 {
778 return GCS::Point{ &m_params[aParams.x], &m_params[aParams.y] };
779 };
780
781 auto lineFor = [&]( const CONSTRAINT_MEMBER& aMember, GCS::Line& aLine ) -> bool
782 {
783 auto it = m_shapeVars.find( aMember.m_item );
784
785 if( it == m_shapeVars.end() || it->second.kind != SHAPE_KIND::SEGMENT
786 || aMember.m_anchor != CONSTRAINT_ANCHOR::WHOLE )
787 {
788 return false;
789 }
790
791 aLine.p1 = pointAt( it->second.startX );
792 aLine.p2 = pointAt( it->second.endX );
793 return true;
794 };
795
796 // A circle for radial constraints; an arc is accepted too (its center + radius are shared
797 // with the Circle base, which is all addConstraintEqualRadius/CircleRadius/concentric need).
798 auto circleFor = [&]( const CONSTRAINT_MEMBER& aMember, GCS::Circle& aCircle ) -> bool
799 {
800 auto it = m_shapeVars.find( aMember.m_item );
801
802 if( it == m_shapeVars.end()
803 || ( it->second.kind != SHAPE_KIND::CIRCLE && it->second.kind != SHAPE_KIND::ARC ) )
804 {
805 return false;
806 }
807
808 aCircle.center = pointAt( it->second.startX );
809 aCircle.rad = &m_params[it->second.radius];
810 return true;
811 };
812
813 // An ellipse for ellipse-target constraints; an elliptical arc is accepted too (its center,
814 // focus and minor radius are shared with the Ellipse base).
815 auto ellipseFor = [&]( const CONSTRAINT_MEMBER& aMember, GCS::Ellipse& aEllipse ) -> bool
816 {
817 auto it = m_shapeVars.find( aMember.m_item );
818
819 if( it == m_shapeVars.end()
820 || ( it->second.kind != SHAPE_KIND::ELLIPSE && it->second.kind != SHAPE_KIND::ELLIPSE_ARC ) )
821 {
822 return false;
823 }
824
825 aEllipse.center = pointAt( it->second.startX );
826 aEllipse.focus1 = pointAt( it->second.focusX );
827 aEllipse.radmin = &m_params[it->second.radius];
828 return true;
829 };
830
831 // Push a driving constant (length/radius), normalized from IU, returning its stable index.
832 auto pushConstant = [&]( double aIU ) -> int
833 {
834 int idx = pushParam( aIU * m_invScale );
835 fixedParams.insert( idx );
836 return idx;
837 };
838
839 int tag = 1;
840
841 for( PCB_CONSTRAINT* constraint : aConstraints )
842 {
843 const std::vector<CONSTRAINT_MEMBER>& members = constraint->GetMembers();
844 bool mapped = false;
845
846 switch( constraint->GetConstraintType() )
847 {
849 {
850 GCS::Line l1, l2;
851
852 if( members.size() == 2 && lineFor( members[0], l1 ) && lineFor( members[1], l2 ) )
853 {
854 m_gcs->addConstraintParallel( l1, l2, tag );
855 mapped = true;
856 }
857
858 break;
859 }
860
862 {
863 // One whole segment aligns its own two endpoints while two point anchors align the pair
864 // the user picked so a corner can be leveled without a segment between the points
865 if( members.size() == 1 )
866 {
867 GCS::Line l;
868
869 if( lineFor( members[0], l ) )
870 {
871 m_gcs->addConstraintHorizontal( l, tag );
872 mapped = true;
873 }
874 }
875 else if( members.size() == 2 )
876 {
877 ANCHOR_PARAMS a = anchorParams( members[0] );
878 ANCHOR_PARAMS b = anchorParams( members[1] );
879
880 if( a.IsValid() && b.IsValid() )
881 {
882 GCS::Point p1 = pointFor( a );
883 GCS::Point p2 = pointFor( b );
884 m_gcs->addConstraintHorizontal( p1, p2, tag );
885 mapped = true;
886 }
887 }
888
889 break;
890 }
891
893 {
894 if( members.size() == 1 )
895 {
896 GCS::Line l;
897
898 if( lineFor( members[0], l ) )
899 {
900 m_gcs->addConstraintVertical( l, tag );
901 mapped = true;
902 }
903 }
904 else if( members.size() == 2 )
905 {
906 ANCHOR_PARAMS a = anchorParams( members[0] );
907 ANCHOR_PARAMS b = anchorParams( members[1] );
908
909 if( a.IsValid() && b.IsValid() )
910 {
911 GCS::Point p1 = pointFor( a );
912 GCS::Point p2 = pointFor( b );
913 m_gcs->addConstraintVertical( p1, p2, tag );
914 mapped = true;
915 }
916 }
917
918 break;
919 }
920
922 {
923 ANCHOR_PARAMS a = members.size() == 2 ? anchorParams( members[0] ) : ANCHOR_PARAMS();
924 ANCHOR_PARAMS b = members.size() == 2 ? anchorParams( members[1] ) : ANCHOR_PARAMS();
925
926 if( a.IsValid() && b.IsValid() )
927 {
928 GCS::Point p1 = pointFor( a );
929 GCS::Point p2 = pointFor( b );
930 m_gcs->addConstraintP2PCoincident( p1, p2, tag );
931 mapped = true;
932 }
933
934 break;
935 }
936
938 {
939 ANCHOR_PARAMS a = members.size() == 1 ? anchorParams( members[0] ) : ANCHOR_PARAMS();
940
941 if( a.IsValid() )
942 {
943 fixedParams.insert( a.x );
944 fixedParams.insert( a.y );
945 mapped = true; // enforced by omission from the unknowns, not a solver constraint
946 }
947
948 break;
949 }
950
952 {
953 GCS::Line l1, l2;
954
955 if( members.size() == 2 && lineFor( members[0], l1 ) && lineFor( members[1], l2 ) )
956 {
957 m_gcs->addConstraintPerpendicular( l1, l2, tag );
958 mapped = true;
959 }
960
961 break;
962 }
963
965 {
966 GCS::Line l1, l2;
967
968 if( members.size() == 2 && lineFor( members[0], l1 ) && lineFor( members[1], l2 ) )
969 {
970 m_gcs->addConstraintEqualLength( l1, l2, tag );
971 mapped = true;
972 }
973
974 break;
975 }
976
978 {
979 ANCHOR_PARAMS p = members.size() == 2 ? anchorParams( members[0] ) : ANCHOR_PARAMS();
980 GCS::Line l;
981 GCS::Circle circ;
982 GCS::Ellipse ell;
983
984 if( p.IsValid() && lineFor( members[1], l ) )
985 {
986 GCS::Point point = pointFor( p );
987 m_gcs->addConstraintPointOnLine( point, l, tag );
988 mapped = true;
989 }
990 else if( p.IsValid() && circleFor( members[1], circ ) )
991 {
992 // A circle or arc target keeps the point on its circumference.
993 GCS::Point point = pointFor( p );
994 m_gcs->addConstraintPointOnCircle( point, circ, tag );
995 mapped = true;
996 }
997 else if( p.IsValid() && ellipseFor( members[1], ell ) )
998 {
999 GCS::Point point = pointFor( p );
1000 m_gcs->addConstraintPointOnEllipse( point, ell, tag );
1001 mapped = true;
1002 }
1003
1004 break;
1005 }
1006
1008 {
1009 ANCHOR_PARAMS p = members.size() == 2 ? anchorParams( members[0] ) : ANCHOR_PARAMS();
1010 GCS::Line seg;
1011
1012 // A midpoint constraint is the segment's endpoints being symmetric about the point.
1013 if( p.IsValid() && lineFor( members[1], seg ) )
1014 {
1015 GCS::Point mid = pointFor( p );
1016 m_gcs->addConstraintP2PSymmetric( seg.p1, seg.p2, mid, tag );
1017 mapped = true;
1018 }
1019
1020 break;
1021 }
1022
1024 {
1025 GCS::Line l1, l2;
1026
1027 // Both endpoints of the second segment lie on the first's supporting line.
1028 if( members.size() == 2 && lineFor( members[0], l1 ) && lineFor( members[1], l2 ) )
1029 {
1030 m_gcs->addConstraintPointOnLine( l2.p1, l1, tag );
1031 m_gcs->addConstraintPointOnLine( l2.p2, l1, tag );
1032 mapped = true;
1033 }
1034
1035 break;
1036 }
1037
1039 {
1040 ANCHOR_PARAMS a = members.size() == 3 ? anchorParams( members[0] ) : ANCHOR_PARAMS();
1041 ANCHOR_PARAMS b = members.size() == 3 ? anchorParams( members[1] ) : ANCHOR_PARAMS();
1042 GCS::Line axis;
1043
1044 if( a.IsValid() && b.IsValid() && lineFor( members[2], axis ) )
1045 {
1046 GCS::Point pa = pointFor( a );
1047 GCS::Point pb = pointFor( b );
1048 m_gcs->addConstraintP2PSymmetric( pa, pb, axis, tag );
1049 mapped = true;
1050 }
1051
1052 break;
1053 }
1054
1056 {
1057 GCS::Line l;
1058
1059 if( members.size() == 1 && constraint->HasValue() && lineFor( members[0], l ) )
1060 {
1061 int len = pushConstant( *constraint->GetValue() );
1062 m_gcs->addConstraintP2PDistance( l.p1, l.p2, &m_params[len], tag, constraint->IsDriving() );
1063 recordReferenceValue( constraint );
1064
1065 if( constraint->IsDriving() )
1066 {
1067 if( auto it = m_shapeVars.find( members[0].m_item ); it != m_shapeVars.end() )
1068 it->second.fixedLengthParam = len;
1069 }
1070
1071 mapped = true;
1072 }
1073 // Two point anchors fix the distance between them with no segment between so a driving
1074 // aligned dimension can drive its own endpoints and the geometry coincident with them
1075 else if( members.size() == 2 && constraint->HasValue() )
1076 {
1077 ANCHOR_PARAMS a = anchorParams( members[0] );
1078 ANCHOR_PARAMS b = anchorParams( members[1] );
1079
1080 if( a.IsValid() && b.IsValid() )
1081 {
1083
1084 if( ortho )
1085 {
1086 // An orthogonal dimension measures one axis so the driving length fixes that
1087 // axis and matches updateGeometry sign convention keeping the current side
1088 bool horiz = ortho->GetOrientation() == PCB_DIM_ORTHOGONAL::DIR::HORIZONTAL;
1089 int aAxis = horiz ? a.x : a.y;
1090 int bAxis = horiz ? b.x : b.y;
1091 double gap = m_params[bAxis] - m_params[aAxis];
1092 double sign = gap >= 0.0 ? 1.0 : -1.0;
1093 int len = pushConstant( sign * *constraint->GetValue() );
1094
1095 m_gcs->addConstraintDifference( &m_params[aAxis], &m_params[bAxis],
1096 &m_params[len], tag, constraint->IsDriving() );
1097 }
1098 else
1099 {
1100 GCS::Point pa = pointFor( a );
1101 GCS::Point pb = pointFor( b );
1102 int len = pushConstant( *constraint->GetValue() );
1103 m_gcs->addConstraintP2PDistance( pa, pb, &m_params[len], tag,
1104 constraint->IsDriving() );
1105 }
1106
1107 recordReferenceValue( constraint );
1108 mapped = true;
1109 }
1110 }
1111
1112 break;
1113 }
1114
1116 {
1117 GCS::Circle c;
1118
1119 if( members.size() == 1 && constraint->HasValue() && circleFor( members[0], c ) )
1120 {
1121 int rad = pushConstant( *constraint->GetValue() );
1122 m_gcs->addConstraintCircleRadius( c, &m_params[rad], tag, constraint->IsDriving() );
1123 recordReferenceValue( constraint );
1124 mapped = true;
1125 }
1126
1127 break;
1128 }
1129
1131 {
1132 GCS::Circle c1, c2;
1133
1134 if( members.size() == 2 && circleFor( members[0], c1 ) && circleFor( members[1], c2 ) )
1135 {
1136 m_gcs->addConstraintEqualRadius( c1, c2, tag );
1137 mapped = true;
1138 }
1139
1140 break;
1141 }
1142
1144 {
1145 // Any center-bearing shape can be concentric, ellipses included.
1146 auto centerOf = [&]( const CONSTRAINT_MEMBER& aMember, GCS::Point& aOut ) -> bool
1147 {
1148 GCS::Circle c;
1149 GCS::Ellipse e;
1150
1151 if( circleFor( aMember, c ) )
1152 {
1153 aOut = c.center;
1154 return true;
1155 }
1156
1157 if( ellipseFor( aMember, e ) )
1158 {
1159 aOut = e.center;
1160 return true;
1161 }
1162
1163 return false;
1164 };
1165
1166 GCS::Point p1, p2;
1167
1168 if( members.size() == 2 && centerOf( members[0], p1 ) && centerOf( members[1], p2 ) )
1169 {
1170 m_gcs->addConstraintP2PCoincident( p1, p2, tag );
1171 mapped = true;
1172 }
1173
1174 break;
1175 }
1176
1178 {
1179 GCS::Line l1, l2;
1180
1181 if( members.size() == 2 && constraint->HasValue() && lineFor( members[0], l1 )
1182 && lineFor( members[1], l2 ) && !isDegenerateLine( l1 ) && !isDegenerateLine( l2 ) )
1183 {
1184 int angle = pushParam( directedAngleForCorner( l1, l2, *constraint->GetValue() ) );
1185 fixedParams.insert( angle );
1186 m_gcs->addConstraintL2LAngle( l1, l2, &m_params[angle], tag, constraint->IsDriving() );
1187 recordReferenceValue( constraint );
1188 mapped = true;
1189 }
1190
1191 break;
1192 }
1193
1195 {
1196 auto vit = members.size() == 1 ? m_shapeVars.find( members[0].m_item ) : m_shapeVars.end();
1197
1198 // A value outside (0, 360) is a degenerate sweep (from a corrupt file or the API); leave
1199 // it unmapped so it reads as errored rather than merging the endpoints.
1200 bool validSweep = constraint->HasValue() && *constraint->GetValue() > 0.0
1201 && *constraint->GetValue() < 360.0;
1202
1203 if( vit != m_shapeVars.end() && vit->second.kind == SHAPE_KIND::ARC && validSweep )
1204 {
1205 const SHAPE_VARS& vars = vit->second;
1206 double target = arcSweepTarget( m_params[vars.startAngle], m_params[vars.endAngle],
1207 *constraint->GetValue() );
1208 int tgt = pushParam( target );
1209 fixedParams.insert( tgt );
1210 m_gcs->addConstraintDifference( &m_params[vars.startAngle], &m_params[vars.endAngle],
1211 &m_params[tgt], tag, constraint->IsDriving() );
1212 recordReferenceValue( constraint );
1213 mapped = true;
1214 }
1215
1216 break;
1217 }
1218
1220 {
1221 if( members.size() != 2 )
1222 break;
1223
1224 GCS::Line l;
1225 GCS::Circle c1, c2;
1226 GCS::Ellipse ell;
1227
1228 // The members may come in either order.
1229 int lineIdx = lineFor( members[0], l ) ? 0 : ( lineFor( members[1], l ) ? 1 : -1 );
1230
1231 if( lineIdx >= 0 )
1232 {
1233 const CONSTRAINT_MEMBER& other = members[lineIdx == 0 ? 1 : 0];
1234
1235 if( circleFor( other, c1 ) )
1236 {
1237 // Keep the circle on the side of the line it is on now.
1238 double dx = *l.p2.x - *l.p1.x;
1239 double dy = *l.p2.y - *l.p1.y;
1240 double cross = dx * ( *c1.center.y - *l.p1.y ) - dy * ( *c1.center.x - *l.p1.x );
1241
1242 m_gcs->addConstraintTangent( l, c1, cross > 0.0, tag );
1243 mapped = true;
1244 }
1245 else if( ellipseFor( other, ell ) )
1246 {
1247 m_gcs->addConstraintTangent( l, ell, tag );
1248 mapped = true;
1249 }
1250 }
1251 else if( circleFor( members[0], c1 ) && circleFor( members[1], c2 ) )
1252 {
1253 m_gcs->addConstraintTangent( c1, c2, tag );
1254 mapped = true;
1255 }
1256
1257 break;
1258 }
1259
1260 default:
1261 break; // UNDEFINED and point-anchored families handled elsewhere
1262 }
1263
1264 // An unmappable constraint (wrong member count/kind for its type) is skipped so it cannot
1265 // disable solving for the whole connected cluster; the remaining constraints still solve.
1266 // It is recorded so the caller can flag it as errored rather than dropping it silently.
1267 if( !mapped )
1268 {
1269 m_unmapped.push_back( constraint->m_Uuid );
1270 continue;
1271 }
1272
1273 m_tagToConstraint[tag] = constraint->m_Uuid;
1274
1275 if( !constraint->IsDriving() )
1276 m_nonDrivingTags.insert( tag );
1277
1278 for( const CONSTRAINT_MEMBER& member : constraint->GetMembers() )
1279 m_tagMembers[tag].push_back( member.m_item );
1280
1281 // Remember shapes a direction or angle constraint could shrink to a point so the stabilize
1282 // solve length-holds just those and leaves dragged-along neighbours free to grow
1283 switch( constraint->GetConstraintType() )
1284 {
1287
1288 // The two-point form aligns loose anchors not the owning segments lengths so only the
1289 // whole-segment form marks its member for the length-hold stabilization
1290 if( constraint->GetMembers().size() != 1 )
1291 break;
1292
1293 [[fallthrough]];
1294
1300 for( const CONSTRAINT_MEMBER& member : constraint->GetMembers() )
1301 m_angleConstrainedShapes.insert( member.m_item );
1302
1303 break;
1304
1305 default:
1306 break;
1307 }
1308
1309 tag++;
1310 }
1311
1312 // Reserve the hold tags just past the mapped constraints, so they can never collide with a
1313 // real constraint's tag however large the cluster grows.
1314 m_lengthHoldTag = tag;
1315 m_resizeRadiusTag = tag + 1;
1316
1317 // Ground each dimension endpoint no mapped constraint bound, so an attached dimension adds zero
1318 // free DOF and only its bound point can move (through the shape it is coincident with). A locked
1319 // dimension is grounded whole, like a locked shape.
1320 std::set<KIID> unmappedConstraints( m_unmapped.begin(), m_unmapped.end() );
1321 std::set<int> referencedDimParams;
1322
1323 for( PCB_CONSTRAINT* constraint : aConstraints )
1324 {
1325 if( unmappedConstraints.contains( constraint->m_Uuid ) )
1326 continue; // an unmapped constraint enforces nothing, so it references no param
1327
1328 for( const CONSTRAINT_MEMBER& member : constraint->GetMembers() )
1329 {
1330 auto it = m_shapeVars.find( member.m_item );
1331
1332 if( it != m_shapeVars.end() && it->second.kind == SHAPE_KIND::POINT_PAIR )
1333 {
1334 if( ANCHOR_PARAMS anchor = anchorParams( member ); anchor.IsValid() )
1335 referencedDimParams.insert( anchor.x );
1336 }
1337 }
1338 }
1339
1340 for( const auto& [kiid, vars] : m_shapeVars )
1341 {
1342 if( vars.kind != SHAPE_KIND::POINT_PAIR )
1343 continue;
1344
1345 bool locked = ConstraintItemIsLocked( vars.dimension );
1346
1347 for( int pointX : { vars.startX, vars.endX } )
1348 {
1349 if( pointX >= 0 && ( locked || !referencedDimParams.contains( pointX ) ) )
1350 {
1351 fixedParams.insert( pointX );
1352 fixedParams.insert( pointX + 1 );
1353 }
1354 }
1355 }
1356
1357 // Everything not grounded or held as a driving constant is an unknown the solver may move.
1358 GCS::VEC_pD unknowns;
1359
1360 for( int i = 0; i < static_cast<int>( m_params.size() ); ++i )
1361 {
1362 if( !fixedParams.contains( i ) )
1363 unknowns.push_back( &m_params[i] );
1364 }
1365
1366 m_gcs->declareUnknowns( unknowns );
1367 m_gcs->initSolution();
1368 m_gcs->maxIter = MAX_SOLVE_ITERATIONS;
1369
1370 m_built = true;
1371 return true;
1372}
1373
1374
1375bool BOARD_CONSTRAINT_ADAPTER::Solve( bool aStabilize )
1376{
1377 if( !m_built )
1378 return false;
1379
1380 // A hard hold here (no drag pin) so a contradiction cannot hide by collapsing a segment or arc.
1381 if( aStabilize )
1382 {
1385 }
1386
1387 // With no shape singled out as edited hold every shape where it sits for a minimal-movement solve
1388 // these soft pins live in the null space of the hard constraints so the diagnosis stays untouched
1389 pinUneditedShapes( {}, GCS::DefaultTemporaryConstraint );
1390
1391 m_gcs->initSolution();
1392 int ret = m_gcs->solve();
1393 m_gcs->applySolution();
1394
1395 bool solved = solveSucceeded( ret );
1396
1397 m_gcs->clearByTag( GCS::DefaultTemporaryConstraint );
1398
1399 if( aStabilize )
1400 m_gcs->clearByTag( m_lengthHoldTag );
1401
1402 return solved;
1403}
1404
1405
1407{
1408 if( !m_built )
1409 return false;
1410
1411 for( const auto& [kiid, vars] : m_shapeVars )
1412 {
1413 // Preserve every curve's free radius while re-solving the resized cluster. This covers
1414 // ellipses too; their focus-to-center offset params are fixed at Build, so pinning the
1415 // minor radius pins the whole shape (focal distance and rotation cannot drift).
1416 if( vars.radius < 0 )
1417 continue;
1418
1419 int target = pushParam( m_params[vars.radius] );
1420 GCS::Circle c;
1421 c.center = GCS::Point{ &m_params[vars.startX], &m_params[vars.startX + 1] };
1422 c.rad = &m_params[vars.radius];
1423
1424 if( kiid == aResizedShape )
1425 {
1426 // The user set this radius, so hold it hard. Pin the centre yielding so the shape moves
1427 // only if a locked neighbour leaves no other way to stay tangent.
1428 m_gcs->addConstraintCircleRadius( c, &m_params[target], m_resizeRadiusTag, true );
1429
1430 int cx = pushParam( m_params[vars.startX] );
1431 int cy = pushParam( m_params[vars.startX + 1] );
1432 m_gcs->addConstraintCoordinateX( c.center, &m_params[cx], GCS::DefaultTemporaryConstraint );
1433 m_gcs->addConstraintCoordinateY( c.center, &m_params[cy], GCS::DefaultTemporaryConstraint );
1434 }
1435 else
1436 {
1437 // Neighbours keep their size unless a real radius constraint says otherwise.
1438 m_gcs->addConstraintCircleRadius( c, &m_params[target], GCS::DefaultTemporaryConstraint );
1439 }
1440 }
1441
1442 // Hold every neighbour where it sits so a resize translates only the shapes a constraint forces
1443 // the resized shape's own centre is pinned above so exclude it here
1444 pinUneditedShapes( { aResizedShape }, GCS::DefaultTemporaryConstraint );
1445
1446 // The radius loop above holds nothing of a polygon so a resized polygon needs its own
1447 // minimal-movement vertex pins here
1448 holdPolygonVertices( { aResizedShape }, GCS::DefaultTemporaryConstraint );
1449
1450 m_gcs->initSolution();
1451 int ret = m_gcs->solve();
1452 m_gcs->applySolution();
1453
1454 bool solved = solveSucceeded( ret );
1455
1456 m_gcs->clearByTag( GCS::DefaultTemporaryConstraint );
1457 m_gcs->clearByTag( m_resizeRadiusTag );
1458
1459 return solved;
1460}
1461
1462
1463bool BOARD_CONSTRAINT_ADAPTER::Solve( const CONSTRAINT_MEMBER& aDragged, const VECTOR2I& aCursor, bool aStabilize,
1464 const std::set<KIID>& aEdited,
1465 const std::optional<std::pair<CONSTRAINT_MEMBER, VECTOR2I>>& aCoDragged )
1466{
1467 if( !m_built )
1468 return false;
1469
1470 ANCHOR_PARAMS params = anchorParams( aDragged );
1471
1472 if( !params.IsValid() )
1473 return false;
1474
1475 // Reuse fixed backing slots for the cursor target so repeated drag solves (warm-started from
1476 // the previous solution still in m_params) never grow the backing store.
1477 if( m_dragTargetX < 0 )
1478 {
1479 m_dragTargetX = pushParam( 0.0 );
1480 m_dragTargetY = pushParam( 0.0 );
1481 }
1482
1483 double targetX = normalizeX( aCursor.x );
1484 double targetY = normalizeY( aCursor.y );
1485
1486 // For an arc endpoint, project the target onto the arc's current circle so the cursor pin agrees
1487 // with the centre + radius holds pinDraggedShapeRest adds, regardless of whether the caller
1488 // already projected. Without this an off-circle target and the holds fight and the arc drifts.
1489 if( auto it = m_shapeVars.find( aDragged.m_item );
1490 !aStabilize && it != m_shapeVars.end() && it->second.kind == SHAPE_KIND::ARC
1491 && ( aDragged.m_anchor == CONSTRAINT_ANCHOR::START || aDragged.m_anchor == CONSTRAINT_ANCHOR::END ) )
1492 {
1493 const SHAPE_VARS& vars = it->second;
1494 double dx = targetX - m_params[vars.startX];
1495 double dy = targetY - m_params[vars.startX + 1];
1496 double len = std::hypot( dx, dy );
1497
1498 if( len > 1e-9 )
1499 {
1500 double radius = m_params[vars.radius];
1501 targetX = m_params[vars.startX] + dx * radius / len;
1502 targetY = m_params[vars.startX + 1] + dy * radius / len;
1503 }
1504 }
1505
1506 if( auto it = m_shapeVars.find( aDragged.m_item );
1507 !aStabilize && it != m_shapeVars.end() && it->second.kind == SHAPE_KIND::SEGMENT
1508 && it->second.fixedLengthParam >= 0
1509 && ( aDragged.m_anchor == CONSTRAINT_ANCHOR::START || aDragged.m_anchor == CONSTRAINT_ANCHOR::END ) )
1510 {
1511 const SHAPE_VARS& vars = it->second;
1512 int farX = aDragged.m_anchor == CONSTRAINT_ANCHOR::START ? vars.endX : vars.startX;
1513 double segLen = m_params[vars.fixedLengthParam];
1514 double dx = targetX - m_params[farX];
1515 double dy = targetY - m_params[farX + 1];
1516 double len = std::hypot( dx, dy );
1517
1518 if( len > 1e-9 && segLen > 1e-9 )
1519 {
1520 targetX = m_params[farX] + dx * segLen / len;
1521 targetY = m_params[farX + 1] + dy * segLen / len;
1522 }
1523 }
1524
1525 m_params[m_dragTargetX] = targetX;
1526 m_params[m_dragTargetY] = targetY;
1527
1528 GCS::Point anchor = GCS::Point{ &m_params[params.x], &m_params[params.y] };
1529
1530 // A temporary negatively tagged pin yields to the real constraints when over-constrained and
1531 // keeps the default weight far above the stay-put pins so a hard-linked neighbour follows it
1532 m_gcs->addConstraintCoordinateX( anchor, &m_params[m_dragTargetX], GCS::DefaultTemporaryConstraint );
1533 m_gcs->addConstraintCoordinateY( anchor, &m_params[m_dragTargetY], GCS::DefaultTemporaryConstraint );
1534
1535 // The co-dragged anchor gets a plain pin at the same weight since only polygon edge drags carry
1536 // one the rest-hold exclusion below shares the same validity check so it is never left unheld
1537 const CONSTRAINT_MEMBER* coDragged = nullptr;
1538
1539 if( aCoDragged )
1540 {
1541 ANCHOR_PARAMS coParams = anchorParams( aCoDragged->first );
1542
1543 if( coParams.IsValid() )
1544 {
1545 coDragged = &aCoDragged->first;
1546
1547 if( m_coDragTargetX < 0 )
1548 {
1549 m_coDragTargetX = pushParam( 0.0 );
1550 m_coDragTargetY = pushParam( 0.0 );
1551 }
1552
1553 m_params[m_coDragTargetX] = normalizeX( aCoDragged->second.x );
1554 m_params[m_coDragTargetY] = normalizeY( aCoDragged->second.y );
1555
1556 GCS::Point coAnchor = GCS::Point{ &m_params[coParams.x], &m_params[coParams.y] };
1557
1558 m_gcs->addConstraintCoordinateX( coAnchor, &m_params[m_coDragTargetX], GCS::DefaultTemporaryConstraint );
1559 m_gcs->addConstraintCoordinateY( coAnchor, &m_params[m_coDragTargetY], GCS::DefaultTemporaryConstraint );
1560 }
1561 }
1562
1563 // Only while live-dragging one handle. The settle/apply paths (aStabilize) instead let the
1564 // pinned shape's rest move to meet a newly applied relation, e.g. a fixed-length shrink.
1565 if( !aStabilize )
1566 pinDraggedShapeRest( aDragged, GCS::DefaultTemporaryConstraint, coDragged );
1567
1568 // Protect only shapes a direction or angle constraint could collapse a merely dragged-along
1569 // neighbour keeps its own stay-put pins instead
1570 if( aStabilize )
1571 {
1572 holdFreeSegmentLengths( GCS::DefaultTemporaryConstraint, m_angleConstrainedShapes );
1573 holdFreeArcRadii( GCS::DefaultTemporaryConstraint, m_angleConstrainedShapes );
1574 }
1575
1576 // Hold every other cluster shape where it sits so only edited shapes and whatever a hard
1577 // constraint forces actually move every genuinely edited shape is excluded here
1578 std::set<KIID> editedShapes = aEdited;
1579 editedShapes.insert( aDragged.m_item );
1580
1581 if( coDragged )
1582 editedShapes.insert( coDragged->m_item );
1583
1584 pinUneditedShapes( editedShapes, GCS::DefaultTemporaryConstraint );
1585
1586 // An edited polygon is excluded above but its unbound vertices still need minimal-movement pins
1587 // a live-dragged polygon is left out too since pinDraggedShapeRest already holds its other vertices
1588 std::set<KIID> heldPolygons = editedShapes;
1589
1590 if( !aStabilize )
1591 heldPolygons.erase( aDragged.m_item );
1592
1593 holdPolygonVertices( heldPolygons, GCS::DefaultTemporaryConstraint );
1594
1595 m_gcs->initSolution();
1596 int ret = m_gcs->solve();
1597 m_gcs->applySolution();
1598
1599 bool solved = solveSucceeded( ret );
1600
1601 m_gcs->clearByTag( GCS::DefaultTemporaryConstraint );
1602
1603 return solved;
1604}
1605
1606
1608{
1609 if( aSolveResult == GCS::Success || aSolveResult == GCS::Converged )
1610 return true;
1611
1612 // planegcs bases Success only on the hard subsystem residual but the soft stay-put subsystem
1613 // perturbs the SQP trajectory so a valid solve often still reports Failed judge the hard
1614 // constraints directly instead a genuine contradiction still leaves a large or non-finite residual
1615 const double residualTol = 1e-3;
1616
1617 // Tag-0 residual is the structural curve rules for arc ellipse and focus which must hold for
1618 // coherent geometry a NaN means no tag-0 constraint exists for a segment-only cluster not a failure
1619 double structuralErr = m_gcs->calculateConstraintErrorByTag( 0 );
1620
1621 if( std::isfinite( structuralErr ) && std::abs( structuralErr ) > residualTol )
1622 return false;
1623
1624 for( const auto& [tag, kiid] : m_tagToConstraint )
1625 {
1626 // A reference non-driving constraint only measures so its residual is never a failure
1627 if( m_nonDrivingTags.contains( tag ) )
1628 continue;
1629
1630 double err = m_gcs->calculateConstraintErrorByTag( tag );
1631
1632 if( !std::isfinite( err ) || std::abs( err ) > residualTol )
1633 return false;
1634 }
1635
1636 return true;
1637}
1638
1639
1640void BOARD_CONSTRAINT_ADAPTER::softPinPoint( const ANCHOR_PARAMS& aPoint, int aTag, std::optional<double> aWeight )
1641{
1642 // A zero weight would neutralize the pin instead of tiering it
1643 wxASSERT( !aWeight || *aWeight > 0 );
1644
1645 if( !aPoint.IsValid() )
1646 return;
1647
1648 int pinX = pushParam( m_params[aPoint.x] );
1649 int pinY = pushParam( m_params[aPoint.y] );
1650 GCS::Point point{ &m_params[aPoint.x], &m_params[aPoint.y] };
1651
1652 int cx = m_gcs->addConstraintCoordinateX( point, &m_params[pinX], aTag );
1653 int cy = m_gcs->addConstraintCoordinateY( point, &m_params[pinY], aTag );
1654
1655 if( aWeight )
1656 {
1657 m_gcs->rescaleConstraint( cx, *aWeight );
1658 m_gcs->rescaleConstraint( cy, *aWeight );
1659 }
1660}
1661
1662
1663void BOARD_CONSTRAINT_ADAPTER::softPinPoint( int aPointX, int aTag, std::optional<double> aWeight )
1664{
1665 if( aPointX >= 0 )
1666 softPinPoint( ANCHOR_PARAMS{ aPointX, aPointX + 1 }, aTag, aWeight );
1667}
1668
1669
1671 const CONSTRAINT_MEMBER* aCoDragged )
1672{
1673 auto it = m_shapeVars.find( aDragged.m_item );
1674
1675 if( it == m_shapeVars.end() )
1676 return;
1677
1678 const SHAPE_VARS& vars = it->second;
1679
1680 // The pins are temporary and keep the default weight far above the stay-put pins so they hold
1681 // the dragged shape rest against a neighbour weaker stay-put pin while a real constraint still wins
1682 if( vars.kind == SHAPE_KIND::SEGMENT || vars.kind == SHAPE_KIND::BEZIER )
1683 {
1684 if( aDragged.m_anchor == CONSTRAINT_ANCHOR::START )
1685 softPinPoint( vars.endX, aTag );
1686 else if( aDragged.m_anchor == CONSTRAINT_ANCHOR::END )
1687 softPinPoint( vars.startX, aTag );
1688 }
1689 else if( vars.kind == SHAPE_KIND::ARC )
1690 {
1691 if( aDragged.m_anchor == CONSTRAINT_ANCHOR::START || aDragged.m_anchor == CONSTRAINT_ANCHOR::END )
1692 {
1693 // Hold the circle (centre + radius) and the far endpoint, so only the dragged endpoint
1694 // sweeps along the arc instead of the whole arc drifting or ballooning.
1695 softPinPoint( vars.startX, aTag );
1696 holdArcRadius( vars, aTag );
1697 softPinPoint( aDragged.m_anchor == CONSTRAINT_ANCHOR::START ? vars.arcEndX : vars.arcStartX, aTag );
1698 }
1699 else if( aDragged.m_anchor == CONSTRAINT_ANCHOR::CENTER )
1700 {
1701 // Hold both endpoints, so dragging the centre changes the radius but keeps the ends.
1702 softPinPoint( vars.arcStartX, aTag );
1703 softPinPoint( vars.arcEndX, aTag );
1704 }
1705 }
1706 else if( vars.kind == SHAPE_KIND::RECT && aDragged.m_anchor == CONSTRAINT_ANCHOR::VERTEX )
1707 {
1708 // Hold the diagonally opposite corner so grabbing a corner handle resizes the rectangle
1709 // about it instead of the whole shape drifting
1711 ( aDragged.m_index + 2 ) % 4 ) ),
1712 aTag );
1713 }
1714 else if( vars.kind == SHAPE_KIND::POLYGON && aDragged.m_anchor == CONSTRAINT_ANCHOR::VERTEX )
1715 {
1716 // Hold every other vertex so grabbing one handle moves only its own vertices an edge drag
1717 // names its second vertex through the co-dragged member whose pin must not be fought here
1718 for( int i = 0; i < vars.vertexCount; ++i )
1719 {
1720 if( i == aDragged.m_index )
1721 continue;
1722
1723 if( aCoDragged && aCoDragged->m_item == aDragged.m_item && i == aCoDragged->m_index )
1724 continue;
1725
1726 softPinPoint( vars.startX + 2 * i, aTag );
1727 }
1728 }
1729}
1730
1731
1732void BOARD_CONSTRAINT_ADAPTER::pinUneditedShapes( const std::set<KIID>& aEdited, int aTag )
1733{
1734 // The stay-put pins are the weakest tier so the drive pins and hard constraints all win while a
1735 // neighbour with slack still holds exactly since nothing else acts on its coordinates
1736
1737 // Hold a curve radius scalar so a circle or ellipse with slack keeps its size
1738 auto holdRadius = [&]( const SHAPE_VARS& aVars )
1739 {
1740 GCS::Circle circle;
1741 circle.center = GCS::Point{ &m_params[aVars.startX], &m_params[aVars.startX + 1] };
1742 circle.rad = &m_params[aVars.radius];
1743
1744 int rad = pushParam( m_params[aVars.radius] );
1745 m_gcs->rescaleConstraint( m_gcs->addConstraintCircleRadius( circle, &m_params[rad], aTag ), STAY_PUT_WEIGHT );
1746 };
1747
1748 for( const auto& [kiid, vars] : m_shapeVars )
1749 {
1750 if( aEdited.contains( kiid ) )
1751 continue;
1752
1753 // A locked shape or dimension is already frozen at Build so a soft pin would only add
1754 // redundant work
1755 if( ConstraintItemIsLocked( vars.shape ) || ConstraintItemIsLocked( vars.dimension ) )
1756 continue;
1757
1758 switch( vars.kind )
1759 {
1761 case SHAPE_KIND::BEZIER:
1763 case SHAPE_KIND::RECT:
1764 // Pinning both stored points fixes the whole shape a segment entirely and a rect four
1765 // corners with them since the corners alias these params
1766 softPinPoint( vars.startX, aTag, STAY_PUT_WEIGHT );
1767 softPinPoint( vars.endX, aTag, STAY_PUT_WEIGHT );
1768 break;
1769
1770 case SHAPE_KIND::CIRCLE:
1772 // A circle or closed ellipse has no endpoints so hold the centre and the radius scalar
1773 // the ellipse focus-to-centre offset is fixed at Build so this pins the whole shape
1774 softPinPoint( vars.startX, aTag, STAY_PUT_WEIGHT );
1775 holdRadius( vars );
1776 break;
1777
1778 case SHAPE_KIND::ARC:
1780 softPinPoint( vars.startX, aTag, STAY_PUT_WEIGHT );
1781 softPinPoint( vars.arcStartX, aTag, STAY_PUT_WEIGHT );
1782 softPinPoint( vars.arcEndX, aTag, STAY_PUT_WEIGHT );
1783 holdRadius( vars );
1784 break;
1785
1787 // Every vertex bound ones included the pins live in the null space of the hard
1788 // constraints so a driving length still moves the vertices it binds
1789 for( int i = 0; i < vars.vertexCount; ++i )
1790 softPinPoint( vars.startX + 2 * i, aTag, STAY_PUT_WEIGHT );
1791
1792 break;
1793 }
1794 }
1795}
1796
1797
1798void BOARD_CONSTRAINT_ADAPTER::holdPolygonVertices( const std::set<KIID>& aShapes, int aTag )
1799{
1800 for( const auto& [kiid, vars] : m_shapeVars )
1801 {
1802 if( vars.kind != SHAPE_KIND::POLYGON || ConstraintItemIsLocked( vars.shape ) )
1803 continue;
1804
1805 if( !aShapes.contains( kiid ) )
1806 continue;
1807
1808 for( int i = 0; i < vars.vertexCount; ++i )
1809 softPinPoint( vars.startX + 2 * i, aTag, STAY_PUT_WEIGHT );
1810 }
1811}
1812
1813
1814void BOARD_CONSTRAINT_ADAPTER::holdFreeSegmentLengths( int aTag, const std::set<KIID>& aShapes )
1815{
1816 // Hold each named free segment length so an angle constraint cannot collapse it to a point
1817 for( const auto& [kiid, vars] : m_shapeVars )
1818 {
1819 if( vars.kind != SHAPE_KIND::SEGMENT || ConstraintItemIsLocked( vars.shape ) )
1820 continue;
1821
1822 if( !aShapes.contains( kiid ) )
1823 continue;
1824
1825 GCS::Point p1{ &m_params[vars.startX], &m_params[vars.startX + 1] };
1826 GCS::Point p2{ &m_params[vars.endX], &m_params[vars.endX + 1] };
1827 double dx = m_params[vars.endX] - m_params[vars.startX];
1828 double dy = m_params[vars.endX + 1] - m_params[vars.startX + 1];
1829 int len = pushParam( std::hypot( dx, dy ) );
1830 m_gcs->addConstraintP2PDistance( p1, p2, &m_params[len], aTag );
1831 }
1832}
1833
1834
1836{
1837 GCS::Circle circle;
1838 circle.center = GCS::Point{ &m_params[aVars.startX], &m_params[aVars.startX + 1] };
1839 circle.rad = &m_params[aVars.radius];
1840
1841 int rad = pushParam( m_params[aVars.radius] );
1842 m_gcs->addConstraintCircleRadius( circle, &m_params[rad], aTag );
1843}
1844
1845
1846void BOARD_CONSTRAINT_ADAPTER::holdFreeArcRadii( int aTag, const std::set<KIID>& aShapes )
1847{
1848 // Hold each named free arc radius so an ARC_ANGLE change rotates an endpoint instead of the
1849 // solver collapsing the arc to a point a real FIXED_RADIUS still wins since this hold is temporary
1850 for( const auto& [kiid, vars] : m_shapeVars )
1851 {
1852 if( vars.kind != SHAPE_KIND::ARC || ConstraintItemIsLocked( vars.shape ) )
1853 continue;
1854
1855 if( !aShapes.contains( kiid ) )
1856 continue;
1857
1858 holdArcRadius( vars, aTag );
1859 }
1860}
1861
1862
1863std::vector<PCB_SHAPE*> BOARD_CONSTRAINT_ADAPTER::Apply( const std::function<void( BOARD_ITEM* )>& aBeforeWrite )
1864{
1865 std::vector<PCB_SHAPE*> changed;
1866
1867 if( !m_built )
1868 return changed;
1869
1870 // The solved point at param index aX (its y is the next slot), back in IU.
1871 auto pointAt = [&]( int aX )
1872 {
1873 return VECTOR2I( KiROUND( denormalizeX( m_params[aX] ) ), KiROUND( denormalizeY( m_params[aX + 1] ) ) );
1874 };
1875
1876 for( const auto& [kiid, vars] : m_shapeVars )
1877 {
1878 if( vars.kind == SHAPE_KIND::POINT_PAIR )
1879 {
1880 VECTOR2I start = pointAt( vars.startX );
1881
1882 // A leader or centre mark has no bindable end (endX == -1); leave its end untouched.
1883 VECTOR2I end = vars.endX >= 0 ? pointAt( vars.endX ) : vars.dimension->GetEnd();
1884
1885 if( start == vars.dimension->GetStart() && end == vars.dimension->GetEnd() )
1886 continue;
1887
1888 if( aBeforeWrite )
1889 aBeforeWrite( vars.dimension );
1890
1891 vars.dimension->SetStart( start );
1892 vars.dimension->SetEnd( end );
1893 vars.dimension->Update(); // SetStart/SetEnd alone do not re-derive the crossbar and text
1894 continue;
1895 }
1896
1897 if( vars.kind == SHAPE_KIND::CIRCLE )
1898 {
1899 VECTOR2I center = pointAt( vars.startX );
1900 int radius = KiROUND( m_params[vars.radius] * m_scale );
1901
1902 if( center == vars.shape->GetCenter() && radius == vars.shape->GetRadius() )
1903 continue;
1904
1905 if( aBeforeWrite )
1906 aBeforeWrite( vars.shape );
1907
1908 vars.shape->SetCenter( center );
1909 vars.shape->SetRadius( radius );
1910 changed.push_back( vars.shape );
1911 continue;
1912 }
1913
1914 if( vars.kind == SHAPE_KIND::ARC )
1915 {
1916 // A sub-micron radius is a collapse, not intent, so leave the arc as it was rather than
1917 // write a degenerate ring the diagnostics would not catch.
1918 if( m_params[vars.radius] * m_scale < 1000.0 && vars.shape->GetRadius() >= 1000 )
1919 continue;
1920
1921 VECTOR2I center = pointAt( vars.startX );
1922 VECTOR2I start = pointAt( vars.arcStartX );
1923 VECTOR2I end = pointAt( vars.arcEndX );
1924
1925 if( start == vars.shape->GetStart() && end == vars.shape->GetEnd()
1926 && center == vars.shape->GetCenter() )
1927 {
1928 continue;
1929 }
1930
1931 if( aBeforeWrite )
1932 aBeforeWrite( vars.shape );
1933
1934 // The mid lies on the solved circle at the bisector of the swept angles. Take the
1935 // bisector on the side that keeps the original winding so the arc does not flip.
1936 double rad = m_params[vars.radius] * m_scale;
1937 double sa = std::atan2( start.y - center.y, start.x - center.x );
1938 double ea = std::atan2( end.y - center.y, end.x - center.x );
1939
1940 if( vars.shape->IsClockwiseArc() )
1941 {
1942 if( ea > sa )
1943 ea -= 2.0 * M_PI;
1944 }
1945 else if( ea < sa )
1946 {
1947 ea += 2.0 * M_PI;
1948 }
1949
1950 double midAngle = 0.5 * ( sa + ea );
1951 VECTOR2I mid( KiROUND( center.x + rad * std::cos( midAngle ) ),
1952 KiROUND( center.y + rad * std::sin( midAngle ) ) );
1953
1954 vars.shape->SetArcGeometry( start, mid, end );
1955 changed.push_back( vars.shape );
1956 continue;
1957 }
1958
1959 if( vars.kind == SHAPE_KIND::ELLIPSE || vars.kind == SHAPE_KIND::ELLIPSE_ARC )
1960 {
1961 VECTOR2I center = pointAt( vars.startX );
1962
1963 // Recover major radius and rotation from the solved focus.
1964 double fx = ( m_params[vars.focusX] - m_params[vars.startX] ) * m_scale;
1965 double fy = ( m_params[vars.focusX + 1] - m_params[vars.startX + 1] ) * m_scale;
1966 double focal = std::hypot( fx, fy );
1967 double minor = m_params[vars.radius] * m_scale;
1968 double major = std::sqrt( focal * focal + minor * minor );
1969
1970 // A focal distance below 1 IU means a circle-degenerate ellipse with no defined
1971 // rotation, so keep the shape's current one.
1972 EDA_ANGLE rotation =
1973 focal > 1.0 ? EDA_ANGLE( std::atan2( fy, fx ), RADIANS_T ) : vars.shape->GetEllipseRotation();
1974
1975 EDA_ANGLE startAngle = vars.shape->GetEllipseStartAngle();
1976 EDA_ANGLE endAngle = vars.shape->GetEllipseEndAngle();
1977
1978 if( vars.kind == SHAPE_KIND::ELLIPSE_ARC )
1979 {
1980 startAngle = EDA_ANGLE( m_params[vars.startAngle], RADIANS_T );
1981 endAngle = EDA_ANGLE( m_params[vars.endAngle], RADIANS_T );
1982 }
1983
1984 auto sameAngle = []( const EDA_ANGLE& aA, const EDA_ANGLE& aB )
1985 {
1986 return std::abs( ( aA - aB ).Normalize180().AsDegrees() ) < 1e-6;
1987 };
1988
1989 if( center == vars.shape->GetEllipseCenter() && KiROUND( major ) == vars.shape->GetEllipseMajorRadius()
1990 && KiROUND( minor ) == vars.shape->GetEllipseMinorRadius()
1991 && sameAngle( rotation, vars.shape->GetEllipseRotation() )
1992 && sameAngle( startAngle, vars.shape->GetEllipseStartAngle() )
1993 && sameAngle( endAngle, vars.shape->GetEllipseEndAngle() ) )
1994 {
1995 continue;
1996 }
1997
1998 if( aBeforeWrite )
1999 aBeforeWrite( vars.shape );
2000
2001 vars.shape->SetEllipseCenter( center );
2002 vars.shape->SetEllipseRotation( rotation );
2003 vars.shape->SetEllipseMajorRadius( KiROUND( major ) );
2004 vars.shape->SetEllipseMinorRadius( KiROUND( minor ) );
2005
2006 if( vars.kind == SHAPE_KIND::ELLIPSE_ARC )
2007 {
2008 vars.shape->SetEllipseStartAngle( startAngle );
2009 vars.shape->SetEllipseEndAngle( endAngle );
2010 }
2011
2012 changed.push_back( vars.shape );
2013 continue;
2014 }
2015
2016 if( vars.kind == SHAPE_KIND::BEZIER )
2017 {
2018 VECTOR2I start = pointAt( vars.startX );
2019 VECTOR2I end = pointAt( vars.endX );
2020
2021 if( start == vars.shape->GetStart() && end == vars.shape->GetEnd() )
2022 continue;
2023
2024 if( aBeforeWrite )
2025 aBeforeWrite( vars.shape );
2026
2027 // Translate each control handle by its adjacent endpoint delta so the curve shape rides
2028 // along instead of shearing when an endpoint moves
2029 VECTOR2I startDelta = start - vars.shape->GetStart();
2030 VECTOR2I endDelta = end - vars.shape->GetEnd();
2031
2032 vars.shape->SetBezierC1( vars.shape->GetBezierC1() + startDelta );
2033 vars.shape->SetBezierC2( vars.shape->GetBezierC2() + endDelta );
2034 vars.shape->SetStart( start );
2035 vars.shape->SetEnd( end );
2036 vars.shape->RebuildBezierToSegmentsPointsList();
2037 changed.push_back( vars.shape );
2038 continue;
2039 }
2040
2041 if( vars.kind == SHAPE_KIND::POLYGON )
2042 {
2043 const SHAPE_POLY_SET& polySet = vars.shape->GetPolyShape();
2044
2045 // A vertex-count mismatch means an external edit changed the outline CPoint wraps indices
2046 // so a stale count would silently resurrect dropped vertices skip the write instead
2047 if( polySet.OutlineCount() != 1 || polySet.HoleCount( 0 ) != 0
2048 || polySet.COutline( 0 ).PointCount() != vars.vertexCount )
2049 {
2050 continue;
2051 }
2052
2053 std::vector<VECTOR2I> points;
2054 points.reserve( vars.vertexCount );
2055
2056 for( int i = 0; i < vars.vertexCount; ++i )
2057 points.push_back( pointAt( vars.startX + 2 * i ) );
2058
2059 const SHAPE_LINE_CHAIN& outline = polySet.COutline( 0 );
2060 bool moved = false;
2061
2062 for( int i = 0; i < vars.vertexCount; ++i )
2063 {
2064 if( points[i] != outline.CPoint( i ) )
2065 {
2066 moved = true;
2067 break;
2068 }
2069 }
2070
2071 if( !moved )
2072 continue;
2073
2074 if( aBeforeWrite )
2075 aBeforeWrite( vars.shape );
2076
2077 // SetPolyPoints rebuilds the poly wholesale dropping cached derived geometry with it
2078 // ingestion only admits single-outline hole-free polys so nothing else is lost here
2079 vars.shape->SetPolyPoints( points );
2080 changed.push_back( vars.shape );
2081 continue;
2082 }
2083
2084 if( vars.kind == SHAPE_KIND::RECT )
2085 {
2086 VECTOR2I start = pointAt( vars.startX );
2087 VECTOR2I end = pointAt( vars.endX );
2088
2089 if( start == vars.shape->GetStart() && end == vars.shape->GetEnd() )
2090 continue;
2091
2092 // A sub-micron side is a collapse not intent and a zero-area rect vanishes from the
2093 // canvas so drop the write using the segment guard floor applied per axis
2094 const int collapseFloor = 1000;
2095 int newWidth = std::abs( end.x - start.x );
2096 int newHeight = std::abs( end.y - start.y );
2097 int curWidth = std::abs( vars.shape->GetEnd().x - vars.shape->GetStart().x );
2098 int curHeight = std::abs( vars.shape->GetEnd().y - vars.shape->GetStart().y );
2099
2100 if( ( newWidth < collapseFloor && curWidth >= collapseFloor )
2101 || ( newHeight < collapseFloor && curHeight >= collapseFloor ) )
2102 {
2103 continue;
2104 }
2105
2106 if( aBeforeWrite )
2107 aBeforeWrite( vars.shape );
2108
2109 // SetStart and SetEnd do not re-clamp the stored corner radius so re-apply it through the
2110 // setter whose half-shorter-side clamp keeps a shrunk rect radius in range
2111 int cornerRadius = vars.shape->GetCornerRadius();
2112
2113 vars.shape->SetStart( start );
2114 vars.shape->SetEnd( end );
2115
2116 if( cornerRadius > 0 )
2117 vars.shape->SetCornerRadius( cornerRadius );
2118
2119 changed.push_back( vars.shape );
2120 continue;
2121 }
2122
2123 VECTOR2I start = pointAt( vars.startX );
2124 VECTOR2I end = pointAt( vars.endX );
2125
2126 if( start == vars.shape->GetStart() && end == vars.shape->GetEnd() )
2127 continue;
2128
2129 // A sub-micron result is a collapse, not intent, and a zero-length line vanishes from the
2130 // canvas, so drop it.
2131 const double collapseFloor = 1000.0;
2132 double newLen = ( end - start ).EuclideanNorm();
2133 double curLen = ( vars.shape->GetEnd() - vars.shape->GetStart() ).EuclideanNorm();
2134
2135 if( newLen < collapseFloor && curLen >= collapseFloor )
2136 continue;
2137
2138 if( aBeforeWrite )
2139 aBeforeWrite( vars.shape );
2140
2141 vars.shape->SetStart( start );
2142 vars.shape->SetEnd( end );
2143 changed.push_back( vars.shape );
2144 }
2145
2146 return changed;
2147}
2148
2149
2151{
2153
2154 if( !m_built )
2155 return diag;
2156
2157 m_gcs->diagnose();
2158 diag.freeDof = m_gcs->dofsNumber();
2159
2160 GCS::VEC_I conflictingTags;
2161 GCS::VEC_I redundantTags;
2162 m_gcs->getConflicting( conflictingTags );
2163 m_gcs->getRedundant( redundantTags );
2164
2165 auto tagsToKiids = [&]( const GCS::VEC_I& aTags, std::vector<KIID>& aOut )
2166 {
2167 for( int t : aTags )
2168 {
2169 auto it = m_tagToConstraint.find( t );
2170
2171 if( it != m_tagToConstraint.end() )
2172 aOut.push_back( it->second );
2173 }
2174 };
2175
2176 tagsToKiids( conflictingTags, diag.conflicting );
2177 tagsToKiids( redundantTags, diag.redundant );
2178
2179 auto flagConflict = [&]( const KIID& aKiid )
2180 {
2181 if( std::ranges::find( diag.conflicting, aKiid ) == diag.conflicting.end() )
2182 diag.conflicting.push_back( aKiid );
2183 };
2184
2185 // Also flag any constraint the geometry does not satisfy. The rank analysis can miss it.
2186 const double residualTol = 1e-3;
2187
2188 for( const auto& [tag, kiid] : m_tagToConstraint )
2189 {
2190 // A reference constraint only measures; its stored value drifting from the geometry is
2191 // never a contradiction.
2192 if( m_nonDrivingTags.contains( tag ) )
2193 continue;
2194
2195 double err = m_gcs->calculateConstraintErrorByTag( tag );
2196
2197 // A nan is a degenerate system, not a conflict. A real contradiction leaves a finite error.
2198 if( !std::isfinite( err ) || std::abs( err ) <= residualTol )
2199 continue;
2200
2201 flagConflict( kiid );
2202 }
2203
2204 // The solve collapsing a segment to a point (e.g. horizontal and vertical at once) satisfies the
2205 // constraints with zero residual, so flag the constraints incident on the collapsed segments.
2206 const double normFloor = 1e-3;
2207 std::set<KIID> collapsedShapes;
2208
2209 for( const auto& [k, vars] : m_shapeVars )
2210 {
2211 if( vars.kind != SHAPE_KIND::SEGMENT )
2212 continue;
2213
2214 double solvedLen = std::hypot( m_params[vars.endX] - m_params[vars.startX],
2215 m_params[vars.endX + 1] - m_params[vars.startX + 1] );
2216 double origLen = ( vars.shape->GetEnd() - vars.shape->GetStart() ).EuclideanNorm() * m_invScale;
2217
2218 if( origLen > normFloor && solvedLen < normFloor )
2219 collapsedShapes.insert( k );
2220 }
2221
2222 if( !collapsedShapes.empty() )
2223 {
2224 bool attributed = false;
2225
2226 for( const auto& [tag, kiid] : m_tagToConstraint )
2227 {
2228 // A reference measurement never causes a collapse.
2229 if( m_nonDrivingTags.contains( tag ) )
2230 continue;
2231
2232 auto members = m_tagMembers.find( tag );
2233
2234 if( members == m_tagMembers.end() )
2235 continue;
2236
2237 bool incident = std::any_of( members->second.begin(), members->second.end(),
2238 [&]( const KIID& aMember )
2239 {
2240 return collapsedShapes.contains( aMember );
2241 } );
2242
2243 if( incident )
2244 {
2245 flagConflict( kiid );
2246 attributed = true;
2247 }
2248 }
2249
2250 // A collapse no mapped constraint touches should not happen; keep the whole-cluster flag
2251 // as a fallback so the contradiction is never silently dropped.
2252 if( !attributed )
2253 {
2254 for( const auto& [tag, kiid] : m_tagToConstraint )
2255 {
2256 if( !m_nonDrivingTags.contains( tag ) )
2257 flagConflict( kiid );
2258 }
2259 }
2260 }
2261
2262 // .solved reflects the last Solve(), which Diagnose() does not run; the caller sets it.
2263 return diag;
2264}
2265
2266
2267CONSTRAINT_DIAGNOSIS SolveCluster( BOARD* aBoard, const CONSTRAINT_MEMBER& aDragged, const VECTOR2I& aCursor,
2268 std::vector<PCB_SHAPE*>* aModified,
2269 const std::function<void( BOARD_ITEM* )>& aBeforeModify, bool aIncludeDragged,
2270 bool aStabilize, const std::set<KIID>& aEdited,
2271 const std::optional<std::pair<CONSTRAINT_MEMBER, VECTOR2I>>& aCoDragged )
2272{
2274
2275 if( !aBoard )
2276 return diag;
2277
2278 // The cluster is the connected component of the shape<->constraint graph containing the
2279 // dragged shape, since a constraint links the shapes of all its members.
2280 auto shapeToConstraints = buildShapeConstraintMap( aBoard, collectAllConstraints( aBoard ) );
2281
2282 std::unordered_set<KIID> clusterShapes;
2283 std::vector<PCB_CONSTRAINT*> clusterConstraints;
2284 collectConstraintCluster( shapeToConstraints, aDragged.m_item, clusterShapes, clusterConstraints );
2285
2286 std::vector<PCB_SHAPE*> shapes = resolveClusterShapes( aBoard, clusterShapes );
2287 std::vector<PCB_DIMENSION_BASE*> dimensions = resolveClusterDimensions( aBoard, clusterShapes );
2288
2289 if( ( shapes.empty() && dimensions.empty() ) || clusterConstraints.empty() )
2290 return diag;
2291
2293
2294 if( !adapter.Build( shapes, clusterConstraints, nullptr, dimensions ) )
2295 return diag;
2296
2297 bool solved = adapter.Solve( aDragged, aCursor, aStabilize, aEdited, aCoDragged );
2298
2299 if( !solved )
2300 return diag; // leave geometry untouched on a failed/diverged solve
2301
2302 PCB_SHAPE* draggedShape = dynamic_cast<PCB_SHAPE*>( aBoard->ResolveItem( aDragged.m_item, true ) );
2303
2304 // Stage each neighbor (not the dragged shape, which the caller stages itself) just before
2305 // its geometry is written, so the whole re-derivation is one undoable transaction.
2306 std::vector<PCB_SHAPE*> changed = adapter.Apply(
2307 [&]( BOARD_ITEM* aItem )
2308 {
2309 if( ( aIncludeDragged || aItem != draggedShape ) && aBeforeModify )
2310 aBeforeModify( aItem );
2311 } );
2312
2313 adapter.ApplyReferenceValues( aBeforeModify );
2314
2315 diag = adapter.Diagnose();
2316 diag.solved = true;
2317
2318 if( aModified )
2319 {
2320 std::ranges::copy_if( changed, std::back_inserter( *aModified ),
2321 [&]( const PCB_SHAPE* aShape )
2322 { return aIncludeDragged || aShape != draggedShape; } );
2323 }
2324
2325 return diag;
2326}
2327
2328
2330 std::vector<PCB_SHAPE*>* aModified,
2331 const std::function<void( BOARD_ITEM* )>& aBeforeModify )
2332{
2333 if( !aBoard || !aConstraint || aConstraint->GetMembers().empty() )
2334 return {};
2335
2336 // Pin the first member's anchor where it is; the solver moves the rest of the cluster (and the
2337 // pinned shape's own free geometry) to meet the new relation. A WHOLE member is pinned at the
2338 // shape's first concrete anchor -- START for a segment/arc, CENTER for a circle.
2339 CONSTRAINT_MEMBER pin = aConstraint->GetMembers().front();
2340
2341 if( pin.m_anchor == CONSTRAINT_ANCHOR::WHOLE )
2342 {
2343 PCB_SHAPE* shape = dynamic_cast<PCB_SHAPE*>( aBoard->ResolveItem( pin.m_item, true ) );
2344
2345 if( !shape )
2346 return {};
2347
2348 std::vector<CONSTRAINT_ANCHOR_POINT> anchors = ConstraintShapeAnchors( shape );
2349
2350 if( anchors.empty() )
2351 return {};
2352
2353 pin.m_anchor = anchors.front().anchor;
2354 pin.m_index = anchors.front().index;
2355 }
2356
2357 std::optional<VECTOR2I> pos = ConstraintAnchorPosition( aBoard, pin );
2358
2359 if( !pos )
2360 return {};
2361
2362 // The pinned shape itself can move (e.g. a fixed-length segment's far end), and the caller does
2363 // not stage it separately, so report it too.
2364 return SolveCluster( aBoard, pin, *pos, aModified, aBeforeModify, /* aIncludeDragged */ true,
2365 /* aStabilize */ true );
2366}
2367
2368
2369void ReSolveShapeClusters( BOARD* aBoard, const std::vector<PCB_SHAPE*>& aShapes, std::vector<PCB_SHAPE*>* aModified,
2370 const std::function<void( BOARD_ITEM* )>& aBeforeModify )
2371{
2372 if( !aBoard )
2373 return;
2374
2375 auto shapeToConstraints = buildShapeConstraintMap( aBoard, collectAllConstraints( aBoard ) );
2376
2377 // Solve each affected cluster once, no matter how many of its shapes were edited.
2378 std::set<KIID> visited;
2379
2380 for( PCB_SHAPE* shape : aShapes )
2381 {
2382 if( !shape || visited.contains( shape->m_Uuid ) || !shapeToConstraints.contains( shape->m_Uuid ) )
2383 continue;
2384
2385 std::unordered_set<KIID> clusterShapes;
2386 std::vector<PCB_CONSTRAINT*> clusterConstraints;
2387 collectConstraintCluster( shapeToConstraints, shape->m_Uuid, clusterShapes, clusterConstraints, &visited );
2388
2389 std::vector<CONSTRAINT_ANCHOR_POINT> anchors = ConstraintShapeAnchors( shape );
2390
2391 if( anchors.empty() )
2392 continue;
2393
2394 // Every edited shape that fell into this cluster must stay free only the untouched
2395 // neighbours get stay-put pins solving from a single seed would pin the others back
2396 std::set<KIID> edited;
2397
2398 for( PCB_SHAPE* other : aShapes )
2399 {
2400 if( other && clusterShapes.contains( other->m_Uuid ) )
2401 edited.insert( other->m_Uuid );
2402 }
2403
2404 SolveCluster( aBoard, { shape->m_Uuid, anchors.front().anchor, anchors.front().index },
2405 anchors.front().pos, aModified, aBeforeModify,
2406 /* aIncludeDragged */ true, /* aStabilize */ false, edited );
2407 }
2408}
2409
2410
2411void ReSolveShapeClustersHoldingEdited( BOARD* aBoard, const std::vector<PCB_SHAPE*>& aEditedShapes,
2412 std::vector<PCB_SHAPE*>* aModified,
2413 const std::function<void( BOARD_ITEM* )>& aBeforeModify )
2414{
2415 if( !aBoard )
2416 return;
2417
2418 auto shapeToConstraints = buildShapeConstraintMap( aBoard, collectAllConstraints( aBoard ) );
2419 std::set<KIID> visited;
2420
2421 for( PCB_SHAPE* seed : aEditedShapes )
2422 {
2423 if( !seed || visited.contains( seed->m_Uuid ) || !shapeToConstraints.contains( seed->m_Uuid ) )
2424 continue;
2425
2426 std::unordered_set<KIID> clusterShapes;
2427 std::vector<PCB_CONSTRAINT*> clusterConstraints;
2428 collectConstraintCluster( shapeToConstraints, seed->m_Uuid, clusterShapes, clusterConstraints, &visited );
2429
2430 std::vector<PCB_SHAPE*> shapes = resolveClusterShapes( aBoard, clusterShapes );
2431 std::vector<PCB_DIMENSION_BASE*> dimensions = resolveClusterDimensions( aBoard, clusterShapes );
2432
2433 if( shapes.empty() || clusterConstraints.empty() )
2434 continue;
2435
2436 // A properties edit is authoritative since the user typed exact values so every edited shape
2437 // is held fixed across all its DOF and only constrained neighbours move to satisfy relations
2438 std::set<KIID> fixed;
2439
2440 for( PCB_SHAPE* edited : aEditedShapes )
2441 {
2442 if( edited && clusterShapes.contains( edited->m_Uuid ) )
2443 fixed.insert( edited->m_Uuid );
2444 }
2445
2447
2448 if( !adapter.Build( shapes, clusterConstraints, &fixed, dimensions ) || !adapter.Solve( true ) )
2449 continue;
2450
2451 std::vector<PCB_SHAPE*> changed = adapter.Apply( aBeforeModify );
2452
2453 // Driven reference values re-measure against the held geometry so a dimension bound to an
2454 // edited shape reads its new size instead of the stale one
2455 adapter.ApplyReferenceValues( aBeforeModify );
2456
2457 if( aModified )
2458 aModified->insert( aModified->end(), changed.begin(), changed.end() );
2459 }
2460}
2461
2462
2463void ReSolveAfterShapeResize( BOARD* aBoard, PCB_SHAPE* aShape, std::vector<PCB_SHAPE*>* aModified,
2464 const std::function<void( BOARD_ITEM* )>& aBeforeModify )
2465{
2466 if( !aBoard || !aShape )
2467 return;
2468
2469 auto shapeToConstraints = buildShapeConstraintMap( aBoard, collectAllConstraints( aBoard ) );
2470
2471 if( !shapeToConstraints.contains( aShape->m_Uuid ) )
2472 return;
2473
2474 std::unordered_set<KIID> clusterShapes;
2475 std::vector<PCB_CONSTRAINT*> clusterConstraints;
2476 collectConstraintCluster( shapeToConstraints, aShape->m_Uuid, clusterShapes, clusterConstraints );
2477
2478 std::vector<PCB_SHAPE*> shapes = resolveClusterShapes( aBoard, clusterShapes );
2479 std::vector<PCB_DIMENSION_BASE*> dimensions = resolveClusterDimensions( aBoard, clusterShapes );
2480
2481 if( shapes.empty() || clusterConstraints.empty() )
2482 return;
2483
2485
2486 if( !adapter.Build( shapes, clusterConstraints, nullptr, dimensions )
2487 || !adapter.SolveAfterResize( aShape->m_Uuid ) )
2488 {
2489 return;
2490 }
2491
2492 std::vector<PCB_SHAPE*> changed = adapter.Apply(
2493 [&]( BOARD_ITEM* aChanged )
2494 {
2495 if( aBeforeModify )
2496 aBeforeModify( aChanged );
2497 } );
2498
2499 adapter.ApplyReferenceValues( aBeforeModify );
2500
2501 if( aModified )
2502 aModified->insert( aModified->end(), changed.begin(), changed.end() );
2503}
2504
2505
2506// Diagnose one cluster in isolation an unbuildable cluster with no resolvable members or no
2507// constraints contributes nothing so its result stays empty
2509 const std::unordered_set<KIID>& aClusterShapes,
2510 const std::vector<PCB_CONSTRAINT*>& aClusterConstraints )
2511{
2513
2514 std::vector<PCB_SHAPE*> shapes = resolveClusterShapes( aBoard, aClusterShapes );
2515 std::vector<PCB_DIMENSION_BASE*> dimensions = resolveClusterDimensions( aBoard, aClusterShapes );
2516
2518
2519 if( ( shapes.empty() && dimensions.empty() ) || aClusterConstraints.empty()
2520 || !adapter.Build( shapes, aClusterConstraints, nullptr, dimensions ) )
2521 {
2522 return result;
2523 }
2524
2525 // A constraint Build() could not map (e.g. a shape was changed to an incompatible kind) is not
2526 // enforced; flag it so the user sees it is broken rather than silently ignored.
2527 result.erroredUnmapped = adapter.UnmappedConstraints();
2528
2529 // Solve first so the residual check sees a real contradiction not an unsolved constraint a
2530 // contradictory cluster deliberately diverges here since the stabilize holds forbid the escape
2531 adapter.Solve( true );
2532
2533 CONSTRAINT_DIAGNOSIS diag = adapter.Diagnose();
2534
2535 if( diag.IsOverConstrained() )
2537 else if( diag.IsUnderConstrained() )
2539 else
2541
2542 for( PCB_SHAPE* shape : shapes )
2543 result.shapeIds.push_back( shape->m_Uuid );
2544
2545 // Bound dimensions join their cluster's verdict so the overlay can mark them; a free dimension
2546 // never reaches a cluster and so keeps no state.
2547 for( PCB_DIMENSION_BASE* dimension : dimensions )
2548 result.dimensionIds.push_back( dimension->m_Uuid );
2549
2550 if( diag.freeDof > 0 )
2551 result.freeDof = diag.freeDof;
2552
2553 result.conflicting = diag.conflicting;
2554 result.redundant = diag.redundant;
2555
2556 return result;
2557}
2558
2559
2560// Fold one cluster's verdict into the board-wide result an empty unbuildable cluster leaves
2561// aResult untouched
2563{
2564 for( const KIID& id : aCluster.shapeIds )
2565 aResult.shapeStates[id] = aCluster.state;
2566
2567 for( const KIID& id : aCluster.dimensionIds )
2568 aResult.shapeStates[id] = aCluster.state;
2569
2570 aResult.totalFreeDof += aCluster.freeDof;
2571
2572 aResult.conflicting.insert( aResult.conflicting.end(), aCluster.conflicting.begin(),
2573 aCluster.conflicting.end() );
2574 aResult.redundant.insert( aResult.redundant.end(), aCluster.redundant.begin(),
2575 aCluster.redundant.end() );
2576 aResult.errored.insert( aResult.errored.end(), aCluster.erroredUnmapped.begin(),
2577 aCluster.erroredUnmapped.end() );
2578}
2579
2580
2581// A constraint with a dangling member is unmappable so it can be flagged by both the adjacency
2582// scan and Build report each errored constraint once here
2584{
2585 std::sort( aResult.errored.begin(), aResult.errored.end() );
2586 aResult.errored.erase( std::unique( aResult.errored.begin(), aResult.errored.end() ),
2587 aResult.errored.end() );
2588}
2589
2590
2592{
2594
2595 if( !aBoard )
2596 return result;
2597
2598 std::vector<PCB_CONSTRAINT*> boardConstraints = collectAllConstraints( aBoard );
2599
2600 std::unordered_map<KIID, std::vector<PCB_CONSTRAINT*>> shapeToConstraints =
2601 buildShapeConstraintMap( aBoard, boardConstraints, &result.errored );
2602
2603 std::set<KIID> visitedShapes;
2604
2605 for( const auto& [seedShape, seedConstraints] : shapeToConstraints )
2606 {
2607 if( visitedShapes.contains( seedShape ) )
2608 continue;
2609
2610 std::unordered_set<KIID> clusterShapes;
2611 std::vector<PCB_CONSTRAINT*> clusterConstraints;
2612 collectConstraintCluster( shapeToConstraints, seedShape, clusterShapes, clusterConstraints,
2613 &visitedShapes );
2614
2616 diagnoseSingleCluster( aBoard, clusterShapes, clusterConstraints ) );
2617 }
2618
2620
2621 return result;
2622}
2623
2624
2625// Boost-style mix so a change in any hashed field changes the cluster hash
2626static void hashCombine( std::size_t& aSeed, std::size_t aValue )
2627{
2628 aSeed ^= aValue + 0x9e3779b97f4a7c15ULL + ( aSeed << 6 ) + ( aSeed >> 2 );
2629}
2630
2631
2632static void hashInt( std::size_t& aSeed, long long aValue )
2633{
2634 hashCombine( aSeed, static_cast<std::size_t>( aValue ) );
2635}
2636
2637
2638static void hashDouble( std::size_t& aSeed, double aValue )
2639{
2640 // Hash the exact bit pattern so any coordinate or value change invalidates the cache folding
2641 // both 32-bit halves so a double change is never lost where size_t is only 32 bits wide
2642 std::uint64_t bits = 0;
2643 static_assert( sizeof( bits ) == sizeof( aValue ) );
2644 std::memcpy( &bits, &aValue, sizeof( bits ) );
2645 hashCombine( aSeed, static_cast<std::size_t>( bits & 0xFFFFFFFFULL ) );
2646 hashCombine( aSeed, static_cast<std::size_t>( bits >> 32 ) );
2647}
2648
2649
2650static void hashPoint( std::size_t& aSeed, const VECTOR2I& aPoint )
2651{
2652 hashInt( aSeed, aPoint.x );
2653 hashInt( aSeed, aPoint.y );
2654}
2655
2656
2657static void hashKiid( std::size_t& aSeed, const KIID& aId )
2658{
2659 hashCombine( aSeed, std::hash<KIID>{}( aId ) );
2660}
2661
2662
2663// Hash every solve input a PCB_SHAPE contributes dispatched by kind so no getter is called on a
2664// shape type it asserts for since GetRadius and GetCenter are unimplemented for the wrong kinds
2665static void hashShape( std::size_t& aSeed, const PCB_SHAPE* aShape )
2666{
2667 hashInt( aSeed, static_cast<int>( aShape->GetShape() ) );
2668
2669 // Build freezes a locked shape including via its parent footprint so the lock state changes
2670 // the solve even when the geometry does not
2671 hashInt( aSeed, ConstraintItemIsLocked( aShape ) ? 1 : 0 );
2672
2673 switch( aShape->GetShape() )
2674 {
2675 case SHAPE_T::SEGMENT:
2676 case SHAPE_T::RECTANGLE:
2677 hashPoint( aSeed, aShape->GetStart() );
2678 hashPoint( aSeed, aShape->GetEnd() );
2679 break;
2680
2681 case SHAPE_T::BEZIER:
2682 hashPoint( aSeed, aShape->GetStart() );
2683 hashPoint( aSeed, aShape->GetEnd() );
2684 hashPoint( aSeed, aShape->GetBezierC1() );
2685 hashPoint( aSeed, aShape->GetBezierC2() );
2686 break;
2687
2688 case SHAPE_T::CIRCLE:
2689 hashPoint( aSeed, aShape->GetCenter() );
2690 hashInt( aSeed, aShape->GetRadius() );
2691 break;
2692
2693 case SHAPE_T::POLY:
2694 {
2695 const SHAPE_POLY_SET& poly = aShape->GetPolyShape();
2696
2697 // The outline hole and arc counts gate ingestion so any of them changing must invalidate
2698 // even when no vertex moved
2699 hashInt( aSeed, poly.OutlineCount() );
2700
2701 if( poly.OutlineCount() > 0 )
2702 {
2703 hashInt( aSeed, poly.HoleCount( 0 ) );
2704
2705 const SHAPE_LINE_CHAIN& outline = poly.COutline( 0 );
2706
2707 hashInt( aSeed, static_cast<long long>( outline.ArcCount() ) );
2708
2709 for( int i = 0; i < outline.PointCount(); ++i )
2710 hashPoint( aSeed, outline.CPoint( i ) );
2711 }
2712
2713 break;
2714 }
2715
2716 case SHAPE_T::ARC:
2717 hashPoint( aSeed, aShape->GetStart() );
2718 hashPoint( aSeed, aShape->GetEnd() );
2719 hashPoint( aSeed, aShape->GetCenter() );
2720 hashInt( aSeed, aShape->GetRadius() );
2721 break;
2722
2723 case SHAPE_T::ELLIPSE:
2725 hashPoint( aSeed, aShape->GetEllipseCenter() );
2726 hashInt( aSeed, aShape->GetEllipseMajorRadius() );
2727 hashInt( aSeed, aShape->GetEllipseMinorRadius() );
2728 hashDouble( aSeed, aShape->GetEllipseRotation().AsRadians() );
2729
2730 if( aShape->GetShape() == SHAPE_T::ELLIPSE_ARC )
2731 {
2732 hashPoint( aSeed, aShape->GetStart() );
2733 hashPoint( aSeed, aShape->GetEnd() );
2734 hashDouble( aSeed, aShape->GetEllipseStartAngle().AsRadians() );
2735 hashDouble( aSeed, aShape->GetEllipseEndAngle().AsRadians() );
2736 }
2737
2738 break;
2739
2740 default:
2741 // Build reads the front shape start for the cluster normalization origin even when the kind
2742 // is unmapped so a change there can still shift the solve hash it here too
2743 hashPoint( aSeed, aShape->GetStart() );
2744 break;
2745 }
2746}
2747
2748
2749static void hashDimension( std::size_t& aSeed, const PCB_DIMENSION_BASE* aDimension )
2750{
2751 hashInt( aSeed, static_cast<int>( aDimension->Type() ) );
2752 hashPoint( aSeed, aDimension->GetStart() );
2753 hashPoint( aSeed, aDimension->GetEnd() );
2754
2755 // An orthogonal dimension driving length fixes the axis its orientation selects so a
2756 // horizontal to vertical flip changes the solve without moving the endpoints
2757 if( aDimension->Type() == PCB_DIM_ORTHOGONAL_T )
2758 {
2759 hashInt( aSeed, static_cast<int>(
2760 static_cast<const PCB_DIM_ORTHOGONAL*>( aDimension )->GetOrientation() ) );
2761 }
2762}
2763
2764
2765static void hashConstraint( std::size_t& aSeed, const PCB_CONSTRAINT* aConstraint )
2766{
2767 hashKiid( aSeed, aConstraint->m_Uuid );
2768 hashInt( aSeed, static_cast<int>( aConstraint->GetConstraintType() ) );
2769 hashInt( aSeed, aConstraint->IsDriving() ? 1 : 0 );
2770 hashInt( aSeed, aConstraint->HasValue() ? 1 : 0 );
2771
2772 if( aConstraint->HasValue() )
2773 hashDouble( aSeed, *aConstraint->GetValue() );
2774
2775 for( const CONSTRAINT_MEMBER& member : aConstraint->GetMembers() )
2776 {
2777 hashKiid( aSeed, member.m_item );
2778 hashInt( aSeed, static_cast<int>( member.m_anchor ) );
2779 hashInt( aSeed, member.m_index );
2780 }
2781}
2782
2783
2784// A hash over every input diagnoseSingleCluster would solve on so any geometry value or membership
2785// change invalidates the cached result err toward hashing more since a missed field leaves it stale
2786static std::size_t hashCluster( BOARD* aBoard, const std::unordered_set<KIID>& aClusterShapes,
2787 const std::vector<PCB_CONSTRAINT*>& aClusterConstraints )
2788{
2789 std::size_t seed = 0;
2790
2791 std::vector<KIID> ids( aClusterShapes.begin(), aClusterShapes.end() );
2792 std::sort( ids.begin(), ids.end() );
2793
2794 for( const KIID& id : ids )
2795 {
2796 BOARD_ITEM* item = aBoard->ResolveItem( id, true );
2797
2798 if( PCB_SHAPE* shape = dynamic_cast<PCB_SHAPE*>( item ) )
2799 hashShape( seed, shape );
2800 else if( PCB_DIMENSION_BASE* dimension = dynamic_cast<PCB_DIMENSION_BASE*>( item ) )
2801 hashDimension( seed, dimension );
2802 }
2803
2804 std::vector<PCB_CONSTRAINT*> constraints = aClusterConstraints;
2805 std::sort( constraints.begin(), constraints.end(),
2806 []( const PCB_CONSTRAINT* aLhs, const PCB_CONSTRAINT* aRhs )
2807 { return aLhs->m_Uuid < aRhs->m_Uuid; } );
2808
2809 for( const PCB_CONSTRAINT* constraint : constraints )
2810 hashConstraint( seed, constraint );
2811
2812 return seed;
2813}
2814
2815
2817{
2818 m_cache.clear();
2819}
2820
2821
2823{
2825
2826 if( !aBoard )
2827 {
2828 m_cache.clear();
2829 return result;
2830 }
2831
2832 std::vector<PCB_CONSTRAINT*> boardConstraints = collectAllConstraints( aBoard );
2833
2834 // The map-scan errored dangling members is recomputed in full every call so it can never go
2835 // stale however aggressively the per-cluster diagnoses are cached
2836 std::unordered_map<KIID, std::vector<PCB_CONSTRAINT*>> shapeToConstraints =
2837 buildShapeConstraintMap( aBoard, boardConstraints, &result.errored );
2838
2839 std::set<KIID> visitedShapes;
2840 std::set<std::vector<KIID>> seenKeys;
2841
2842 for( const auto& [seedShape, seedConstraints] : shapeToConstraints )
2843 {
2844 if( visitedShapes.contains( seedShape ) )
2845 continue;
2846
2847 std::unordered_set<KIID> clusterShapes;
2848 std::vector<PCB_CONSTRAINT*> clusterConstraints;
2849 collectConstraintCluster( shapeToConstraints, seedShape, clusterShapes, clusterConstraints,
2850 &visitedShapes );
2851
2852 // Clusters partition the board constraints so the sorted constraint-id set uniquely
2853 // identifies a cluster and stays put across a geometry or value edit the hash catches those
2854 std::vector<KIID> key;
2855
2856 for( const PCB_CONSTRAINT* constraint : clusterConstraints )
2857 key.push_back( constraint->m_Uuid );
2858
2859 std::sort( key.begin(), key.end() );
2860
2861 std::size_t hash = hashCluster( aBoard, clusterShapes, clusterConstraints );
2862 auto it = m_cache.find( key );
2863
2864 if( it == m_cache.end() || it->second.hash != hash )
2865 {
2866 CLUSTER_DIAGNOSIS cluster =
2867 diagnoseSingleCluster( aBoard, clusterShapes, clusterConstraints );
2868 m_solveCount++;
2869 it = m_cache.insert_or_assign( key, CACHE_ENTRY{ hash, std::move( cluster ) } ).first;
2870 }
2871
2872 assembleClusterInto( result, it->second.result );
2873 seenKeys.insert( key );
2874 }
2875
2876 // Drop cache entries for clusters no longer present so a stale result can never leak into a
2877 // later pass that happens to rebuild the same key
2878 for( auto it = m_cache.begin(); it != m_cache.end(); )
2879 {
2880 if( seenKeys.contains( it->first ) )
2881 ++it;
2882 else
2883 it = m_cache.erase( it );
2884 }
2885
2887
2888 return result;
2889}
@ FPHOLDER
Definition board.h:365
static CLUSTER_DIAGNOSIS diagnoseSingleCluster(BOARD *aBoard, const std::unordered_set< KIID > &aClusterShapes, const std::vector< PCB_CONSTRAINT * > &aClusterConstraints)
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
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)
void 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 ...
CONSTRAINT_DIAGNOSIS ApplyConstraintImmediately(BOARD *aBoard, const PCB_CONSTRAINT *aConstraint, std::vector< PCB_SHAPE * > *aModified, const std::function< void(BOARD_ITEM *)> &aBeforeModify)
Solve a just-created constraint's cluster so the geometry snaps to satisfy it (SolidWorks-style),...
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)
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 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)
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)
Gather the cluster of shapes transitively constrained with the dragged shape, solve with the dragged ...
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.
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)
Solve the system, pinning a dragged anchor to a cursor position.
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...
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...
double m_originX
IU offset subtracted from x before scaling (cluster anchor).
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...
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.
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::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 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.
std::unique_ptr< GCS::System > m_gcs
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.
double m_originY
IU offset subtracted from y before scaling.
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.
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.
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:1913
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
bool IsLocked() const override
Definition footprint.h:637
Definition kiid.h:44
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 ...
VECTOR2I GetCenter() const override
This defaults to the center of the bounding box if not overridden.
Definition pcb_shape.h:78
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
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
STL namespace.
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.
@ 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.
bool moved
std::vector< double > vA
KIBIS_PIN * pin
const uint32_t seed
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