KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_constraint_diagnostics.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
20#include <algorithm>
21#include <vector>
22
24
25#include <board.h>
26#include <core/kicad_algo.h>
27#include <footprint.h>
28#include <pcb_shape.h>
31
33
34using namespace KI_TEST;
35
36
37BOOST_AUTO_TEST_SUITE( ConstraintSolverDiagnostics )
38
39
40// Each cluster on the board maps to the correct per-shape verdict, and unconstrained shapes
41// get no entry.
42BOOST_AUTO_TEST_CASE( PerClusterVerdicts )
43{
44 BOARD board;
45
46 // Cluster 1: a segment with both endpoints pinned -> well constrained (zero free DOF).
47 PCB_SHAPE* wellSeg = addSegment( board, { 0, 0 }, { 10 * MM, 0 } );
49 { { wellSeg->m_Uuid, CONSTRAINT_ANCHOR::START } } );
51 { { wellSeg->m_Uuid, CONSTRAINT_ANCHOR::END } } );
52
53 // Cluster 2: a segment with only a direction constraint -> under constrained.
54 PCB_SHAPE* underSeg = addSegment( board, { 0, 20 * MM }, { 10 * MM, 22 * MM } );
56 { { underSeg->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } } );
57
58 // An unconstrained segment.
59 PCB_SHAPE* loneSeg = addSegment( board, { 0, 40 * MM }, { 5 * MM, 40 * MM } );
60
62
63 BOOST_CHECK( diag.shapeStates[wellSeg->m_Uuid] == CONSTRAINT_STATE::WELL_CONSTRAINED );
64 BOOST_CHECK( diag.shapeStates[underSeg->m_Uuid] == CONSTRAINT_STATE::UNDER_CONSTRAINED );
65 BOOST_CHECK( diag.shapeStates.find( loneSeg->m_Uuid ) == diag.shapeStates.end() );
66
67 // The under-constrained cluster contributes free DOF to the board total.
68 BOOST_CHECK_GT( diag.totalFreeDof, 0 );
69}
70
71
72// Two contradictory fixed-length constraints on one segment are flagged over-constrained.
73BOOST_AUTO_TEST_CASE( ConflictingClusterIsOverConstrained )
74{
75 BOARD board;
76
77 PCB_SHAPE* seg = addSegment( board, { 0, 0 }, { 10 * MM, 0 } );
79 { { seg->m_Uuid, CONSTRAINT_ANCHOR::START } } );
81 { { seg->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } }, 10.0 * MM );
83 { { seg->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } }, 8.0 * MM );
84
86
87 // The contradiction is surfaced: the shape is not reported as cleanly well-constrained.
88 BOOST_CHECK( diag.shapeStates[seg->m_Uuid] == CONSTRAINT_STATE::OVER_CONSTRAINED );
89 BOOST_CHECK( !diag.conflicting.empty() );
90}
91
92
93// A constraint referencing a deleted item is reported as errored (Zulip 2026-06-18 error state).
94BOOST_AUTO_TEST_CASE( DanglingMemberIsErrored )
95{
96 BOARD board;
97
98 PCB_SHAPE* a = addSegment( board, { 0, 0 }, { 10 * MM, 0 } );
99
101 c->AddMember( a->m_Uuid, CONSTRAINT_ANCHOR::END );
102 c->AddMember( KIID(), CONSTRAINT_ANCHOR::START ); // references a non-existent item
103 board.Add( c );
104
105 KIID constraintId = c->m_Uuid;
106
108
109 BOOST_REQUIRE_EQUAL( diag.errored.size(), 1 );
110 BOOST_CHECK( diag.errored[0] == constraintId );
111}
112
113
114// A constraint whose member kind is incompatible with its type (a parallel referencing a circle,
115// e.g. after a shape was changed to a circle) must not disable the whole cluster: it is reported
116// errored while a valid constraint in the same cluster still takes effect.
117BOOST_AUTO_TEST_CASE( IncompatibleKindConstraintIsErroredAndClusterStillSolves )
118{
119 BOARD board;
120
121 PCB_SHAPE* seg = addSegment( board, { 0, 0 }, { 10 * MM, 2 * MM } );
122 PCB_SHAPE* circle = addCircle( board, { 0, 20 * MM }, 5 * MM );
123
124 // Malformed: a parallel cannot map a circle onto a line, so it is unmappable.
126 bad->AddMember( seg->m_Uuid, CONSTRAINT_ANCHOR::WHOLE );
127 bad->AddMember( circle->m_Uuid, CONSTRAINT_ANCHOR::WHOLE );
128 board.Add( bad );
129
130 // A valid constraint in the same cluster (shares seg) must still be honoured.
132 { { seg->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } } );
133
135
136 BOOST_CHECK( alg::contains( diag.errored, bad->m_Uuid ) );
137
138 // The cluster was not disabled by the bad constraint: its shapes still get a verdict.
139 BOOST_CHECK( diag.shapeStates.find( seg->m_Uuid ) != diag.shapeStates.end() );
140}
141
142
143// A circle has no endpoints, so a constraint anchored on a circle's START/END is unmappable and is
144// reported errored -- it must never silently alias the circle's centre (which shares that slot).
145BOOST_AUTO_TEST_CASE( CircleEndpointAnchorIsErrored )
146{
147 BOARD board;
148
149 PCB_SHAPE* circle = addCircle( board, { 0, 0 }, 5 * MM );
150 PCB_SHAPE* seg = addSegment( board, { 20 * MM, 0 }, { 30 * MM, 0 } );
151
153 c->AddMember( circle->m_Uuid, CONSTRAINT_ANCHOR::START ); // circles have no START
154 c->AddMember( seg->m_Uuid, CONSTRAINT_ANCHOR::START );
155 board.Add( c );
156
158
159 BOOST_CHECK( alg::contains( diag.errored, c->m_Uuid ) );
160}
161
162
163// Constraints owned by a footprint (footprint-editor scope) are diagnosed too, not only
164// board-level ones.
165BOOST_AUTO_TEST_CASE( FootprintScopedConstraintsAreDiagnosed )
166{
167 BOARD board;
168
169 FOOTPRINT* fp = new FOOTPRINT( &board );
170 board.Add( fp );
171
172 PCB_SHAPE* seg = new PCB_SHAPE( fp, SHAPE_T::SEGMENT );
173 seg->SetStart( { 0, 0 } );
174 seg->SetEnd( { 10 * MM, 2 * MM } );
175 fp->Add( seg );
176
179 fp->Add( c );
180
182
183 // The footprint's segment is reported (under-constrained: only a direction is fixed).
184 BOOST_REQUIRE( diag.shapeStates.find( seg->m_Uuid ) != diag.shapeStates.end() );
185 BOOST_CHECK( diag.shapeStates[seg->m_Uuid] == CONSTRAINT_STATE::UNDER_CONSTRAINED );
186}
187
188
189// A reference (non-driving) value that drifted from the geometry it measures is annotation, not a
190// contradiction, so the residual pass must not paint it as conflicting.
191BOOST_AUTO_TEST_CASE( DriftedReferenceValueIsNotConflicting )
192{
193 BOARD board;
194
195 PCB_SHAPE* seg = addSegment( board, { 0, 0 }, { 10 * MM, 0 } );
196
198 { { seg->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } }, 5.0 * MM );
199 len->SetDriving( false );
200
202
203 BOOST_CHECK( !alg::contains( diag.conflicting, len->m_Uuid ) );
204}
205
206
207// A contradiction on one segment of a multi-shape cluster still flags the constraints incident on
208// that segment, and the diagnosis never moves the geometry.
209BOOST_AUTO_TEST_CASE( ContradictionInMultiShapeClusterIsFlagged )
210{
211 BOARD board;
212
213 PCB_SHAPE* doomed = addSegment( board, { 0, 0 }, { 10 * MM, 2 * MM } );
214 PCB_SHAPE* bystander = addSegment( board, { 0, 20 * MM }, { 10 * MM, 20 * MM } );
215
217 { { doomed->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } } );
219 { { doomed->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } } );
220
222 { { doomed->m_Uuid, CONSTRAINT_ANCHOR::WHOLE },
223 { bystander->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } } );
224
226
227 BOOST_CHECK( alg::contains( diag.conflicting, h->m_Uuid )
228 || alg::contains( diag.conflicting, v->m_Uuid ) );
229
230 BOOST_CHECK_EQUAL( doomed->GetStart(), VECTOR2I( 0, 0 ) );
231 BOOST_CHECK_EQUAL( doomed->GetEnd(), VECTOR2I( 10 * MM, 2 * MM ) );
232}
233
234
235// A plain (non-stabilize) solve satisfies horizontal+vertical by collapsing the segment with zero
236// residual, so only the collapse check can flag it. The flags must land on the constraints
237// incident on the collapsed segment; a satisfied constraint on a connected bystander and a
238// reference measurement stay clean.
239BOOST_AUTO_TEST_CASE( ConvergentCollapseFlagsOnlyIncidentConstraints )
240{
241 BOARD board;
242
243 PCB_SHAPE* doomed = addSegment( board, { 0, 0 }, { 10 * MM, 2 * MM } );
244 PCB_SHAPE* bystander = addSegment( board, { 0, 0 }, { 10 * MM, 0 } );
245
247 { { doomed->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } } );
249 { { doomed->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } } );
250
251 // Joins the cluster at a shared endpoint without constraining the doomed segment's length.
253 { { doomed->m_Uuid, CONSTRAINT_ANCHOR::START },
254 { bystander->m_Uuid, CONSTRAINT_ANCHOR::START } } );
255
257 { { bystander->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } } );
258
260 { { doomed->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } }, 5.0 * MM );
261 reference->SetDriving( false );
262
263 std::vector<PCB_CONSTRAINT*> constraints( board.Constraints().begin(), board.Constraints().end() );
264 std::vector<PCB_SHAPE*> shapes{ doomed, bystander };
265
267 BOOST_REQUIRE( adapter.Build( shapes, constraints ) );
268 BOOST_REQUIRE( adapter.Solve() );
269
270 CONSTRAINT_DIAGNOSIS diag = adapter.Diagnose();
271
272 BOOST_CHECK( alg::contains( diag.conflicting, h->m_Uuid )
273 || alg::contains( diag.conflicting, v->m_Uuid ) );
274
275 BOOST_CHECK( !alg::contains( diag.conflicting, bystanderOnly->m_Uuid ) );
276 BOOST_CHECK( !alg::contains( diag.conflicting, reference->m_Uuid ) );
277}
278
279
281
282
283BOOST_AUTO_TEST_SUITE( ConstraintDiagnoserIncremental )
284
285
286// Two independent clusters move a shape in one only that cluster re solves SolveCount up by one
287// other served from cache both verdicts stay correct
288BOOST_AUTO_TEST_CASE( MovingOneShapeReSolvesOnlyItsCluster )
289{
290 BOARD board;
291
292 // Cluster A only a direction constraint under constrained
293 PCB_SHAPE* segA = addSegment( board, { 0, 0 }, { 10 * MM, 2 * MM } );
295 { { segA->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } } );
296
297 // Cluster B both endpoints pinned well constrained disjoint from A
298 PCB_SHAPE* segB = addSegment( board, { 0, 40 * MM }, { 10 * MM, 40 * MM } );
300 { { segB->m_Uuid, CONSTRAINT_ANCHOR::START } } );
302 { { segB->m_Uuid, CONSTRAINT_ANCHOR::END } } );
303
305
306 BOARD_CONSTRAINT_DIAGNOSTICS first = diagnoser.Diagnose( &board );
307 BOOST_CHECK_EQUAL( diagnoser.SolveCount(), 2 );
308 BOOST_CHECK( first.shapeStates[segA->m_Uuid] == CONSTRAINT_STATE::UNDER_CONSTRAINED );
309 BOOST_CHECK( first.shapeStates[segB->m_Uuid] == CONSTRAINT_STATE::WELL_CONSTRAINED );
310
311 // Edit cluster A only constraint set unchanged key survives only hash moves
312 segA->SetEnd( { 12 * MM, 5 * MM } );
313
314 BOARD_CONSTRAINT_DIAGNOSTICS second = diagnoser.Diagnose( &board );
315
316 // Exactly one more cluster was solved -- cluster B came from cache.
317 BOOST_CHECK_EQUAL( diagnoser.SolveCount(), 3 );
318 BOOST_CHECK( second.shapeStates[segA->m_Uuid] == CONSTRAINT_STATE::UNDER_CONSTRAINED );
319 BOOST_CHECK( second.shapeStates[segB->m_Uuid] == CONSTRAINT_STATE::WELL_CONSTRAINED );
320
321 // Nothing changed re solves nothing
322 diagnoser.Diagnose( &board );
323 BOOST_CHECK_EQUAL( diagnoser.SolveCount(), 3 );
324}
325
326
327// Adding a constraint to one cluster changes only that cluster's key/hash, so only it re-solves.
328BOOST_AUTO_TEST_CASE( AddingConstraintReSolvesOnlyItsCluster )
329{
330 BOARD board;
331
332 // Already horizontal pinning both endpoints later stays consistent
333 PCB_SHAPE* segA = addSegment( board, { 0, 0 }, { 10 * MM, 0 } );
335 { { segA->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } } );
336
337 PCB_SHAPE* segB = addSegment( board, { 0, 40 * MM }, { 10 * MM, 40 * MM } );
339 { { segB->m_Uuid, CONSTRAINT_ANCHOR::START } } );
341 { { segB->m_Uuid, CONSTRAINT_ANCHOR::END } } );
342
344
345 diagnoser.Diagnose( &board );
346 BOOST_CHECK_EQUAL( diagnoser.SolveCount(), 2 );
347
348 // Pin A endpoints too well constrained now only A key changes
350 { { segA->m_Uuid, CONSTRAINT_ANCHOR::START } } );
352 { { segA->m_Uuid, CONSTRAINT_ANCHOR::END } } );
353
354 BOARD_CONSTRAINT_DIAGNOSTICS after = diagnoser.Diagnose( &board );
355
356 // Cluster A re-solved once (its key changed); cluster B stayed cached.
357 BOOST_CHECK_EQUAL( diagnoser.SolveCount(), 3 );
358 BOOST_CHECK( after.shapeStates[segA->m_Uuid] == CONSTRAINT_STATE::WELL_CONSTRAINED );
359 BOOST_CHECK( after.shapeStates[segB->m_Uuid] == CONSTRAINT_STATE::WELL_CONSTRAINED );
360}
361
362
363// Editing a driving value changes only its cluster's hash, so only that cluster re-solves.
364BOOST_AUTO_TEST_CASE( ChangingDrivingValueReSolvesOnlyItsCluster )
365{
366 BOARD board;
367
368 PCB_SHAPE* segA = addSegment( board, { 0, 0 }, { 10 * MM, 0 } );
370 { { segA->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } }, 10.0 * MM );
371 lenA->SetDriving( true );
372
373 PCB_SHAPE* segB = addSegment( board, { 0, 40 * MM }, { 10 * MM, 40 * MM } );
375 { { segB->m_Uuid, CONSTRAINT_ANCHOR::START } } );
377 { { segB->m_Uuid, CONSTRAINT_ANCHOR::END } } );
378
380
381 diagnoser.Diagnose( &board );
382 BOOST_CHECK_EQUAL( diagnoser.SolveCount(), 2 );
383
384 lenA->SetValue( 8.0 * MM );
385
386 diagnoser.Diagnose( &board );
387 BOOST_CHECK_EQUAL( diagnoser.SolveCount(), 3 );
388}
389
390
391// Incremental diagnoser matches free function board wide despite cluster caching
392BOOST_AUTO_TEST_CASE( DiagnoserMatchesFreeFunction )
393{
394 BOARD board;
395
396 // A well-constrained cluster.
397 PCB_SHAPE* pinned = addSegment( board, { 0, 0 }, { 10 * MM, 0 } );
399 { { pinned->m_Uuid, CONSTRAINT_ANCHOR::START } } );
401 { { pinned->m_Uuid, CONSTRAINT_ANCHOR::END } } );
402
403 // An under-constrained cluster.
404 PCB_SHAPE* loose = addSegment( board, { 0, 20 * MM }, { 10 * MM, 22 * MM } );
406 { { loose->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } } );
407
408 // An over-constrained cluster.
409 PCB_SHAPE* clash = addSegment( board, { 0, 40 * MM }, { 10 * MM, 40 * MM } );
411 { { clash->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } }, 10.0 * MM );
413 { { clash->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } }, 8.0 * MM );
414
415 // Dangling member constraint for map scan errored path
417 dangling->AddMember( pinned->m_Uuid, CONSTRAINT_ANCHOR::START );
418 dangling->AddMember( KIID(), CONSTRAINT_ANCHOR::START );
419 board.Add( dangling );
420
422
424 BOARD_CONSTRAINT_DIAGNOSTICS incremental = diagnoser.Diagnose( &board );
425
426 auto sorted = []( std::vector<KIID> aVec )
427 {
428 std::sort( aVec.begin(), aVec.end() );
429 return aVec;
430 };
431
432 BOOST_CHECK_EQUAL( incremental.totalFreeDof, reference.totalFreeDof );
433 BOOST_CHECK( incremental.shapeStates == reference.shapeStates );
434 BOOST_CHECK( sorted( incremental.conflicting ) == sorted( reference.conflicting ) );
435 BOOST_CHECK( sorted( incremental.redundant ) == sorted( reference.redundant ) );
436 BOOST_CHECK( sorted( incremental.errored ) == sorted( reference.errored ) );
437
438 // A re-diagnose with no change re-solves nothing, and still matches.
439 std::size_t before = diagnoser.SolveCount();
440 BOARD_CONSTRAINT_DIAGNOSTICS again = diagnoser.Diagnose( &board );
441 BOOST_CHECK_EQUAL( diagnoser.SolveCount(), before );
442 BOOST_CHECK( again.shapeStates == reference.shapeStates );
443}
444
445
BOARD_CONSTRAINT_DIAGNOSTICS DiagnoseBoardConstraints(BOARD *aBoard)
Diagnose every constraint cluster on the board (validate only – geometry is not changed) and return t...
@ 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).
Translates KiCad board geometry to and from the planegcs solver (issue #2329).
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.
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.
CONSTRAINT_DIAGNOSIS Diagnose()
Report degrees of freedom and conflicting/redundant constraints.
An incremental DiagnoseBoardConstraints for the interactive edit path.
std::size_t SolveCount() const
Clusters actually re-solved across this diagnoser lifetime for testing cache isolation.
BOARD_CONSTRAINT_DIAGNOSTICS Diagnose(BOARD *aBoard)
Diagnose every cluster reusing cached per-cluster results whose solve inputs are unchanged.
Information pertinent to a Pcbnew printed circuit board.
Definition board.h:373
void Add(BOARD_ITEM *aItem, ADD_MODE aMode=ADD_MODE::INSERT, bool aSkipConnectivity=false) override
Removes an item from the container.
Definition board.cpp:1355
const CONSTRAINTS & Constraints() const
Geometric constraints (#2329) owned by this board.
Definition board.h:465
const KIID m_Uuid
Definition eda_item.h:531
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
void Add(BOARD_ITEM *aItem, ADD_MODE aMode=ADD_MODE::INSERT, bool aSkipConnectivity=false) override
Removes an item from the container.
Definition kiid.h:44
A geometric constraint between board items (issue #2329).
void AddMember(const KIID &aItem, CONSTRAINT_ANCHOR aAnchor=CONSTRAINT_ANCHOR::WHOLE, int aIndex=-1)
void SetDriving(bool aDriving)
void SetEnd(const VECTOR2I &aEnd) override
void SetStart(const VECTOR2I &aStart) override
@ SEGMENT
Definition eda_shape.h:46
PCB_SHAPE * addSegment(BOARD &aBoard, const VECTOR2I &aStart, const VECTOR2I &aEnd)
PCB_SHAPE * addCircle(BOARD &aBoard, const VECTOR2I &aCenter, int aRadius)
constexpr int MM
PCB_CONSTRAINT * addConstraint(BOARD &aBoard, PCB_CONSTRAINT_TYPE aType, const std::vector< CONSTRAINT_MEMBER > &aMembers, std::optional< double > aValue=std::nullopt)
bool contains(const _Container &__container, _Value __value)
Returns true if the container contains the given value.
Definition kicad_algo.h:96
@ 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.
@ FIXED_POSITION
A point is locked at its current location.
@ VERTICAL
A segment (or two points) is vertical.
@ COINCIDENT
Two points are made to coincide.
@ HORIZONTAL
A segment (or two points) is horizontal.
@ FIXED_LENGTH
A segment has a driving length value.
@ PARALLEL
Two segments are parallel.
@ EQUAL_LENGTH
Two segments have equal length.
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).
The outcome of a constraint solve, in plain data so callers need not know planegcs.
std::vector< KIID > conflicting
Constraints the solver reports as over-constraining.
BOOST_AUTO_TEST_CASE(HorizontalAlignment)
BOOST_AUTO_TEST_SUITE(CadstarPartParser)
BOOST_AUTO_TEST_CASE(PerClusterVerdicts)
BOOST_REQUIRE(intersection.has_value()==c.ExpectedIntersection.has_value())
BOOST_AUTO_TEST_SUITE_END()
SHAPE_CIRCLE circle(c.m_circle_center, c.m_circle_radius)
BOOST_CHECK_EQUAL(result, "25.4")
VECTOR2< int32_t > VECTOR2I
Definition vector2d.h:683