KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_constraint_combinations.cpp
Go to the documentation of this file.
1/*
2 * A matrix of constraint combinations: each satisfiable case must diagnose clean, each contradiction
3 * must be flagged. Guards the over-constrained detection against regressions across many shapes.
4 */
5
6#include <algorithm>
7#include <cmath>
8#include <functional>
9#include <vector>
10
12
13#include <board.h>
14#include <pcb_shape.h>
17#include <core/mirror.h>
18#include <geometry/eda_angle.h>
19#include <geometry/seg.h>
20
22
23using namespace KI_TEST;
24
25namespace
26{
27void author( BOARD& aBoard, PCB_CONSTRAINT_TYPE aType, const std::vector<CONSTRAINT_MEMBER>& aMembers,
28 std::optional<double> aValue = std::nullopt )
29{
30 PCB_CONSTRAINT* c = addConstraint( aBoard, aType, aMembers, aValue );
31 std::vector<PCB_SHAPE*> mod;
32 ApplyConstraintImmediately( &aBoard, c, &mod,
33 []( BOARD_ITEM* )
34 {
35 } );
36}
37
39{
40 return { s->m_Uuid, CONSTRAINT_ANCHOR::WHOLE };
41}
42
43// Each scenario runs in this helper's frame, so only one BOARD is on the stack at a time.
44void run( const char* aName, bool aExpectConflict, const std::function<void( BOARD& )>& aBuild )
45{
46 BOARD board;
47 aBuild( board );
48
50
51 BOOST_CHECK_MESSAGE( ( !d.conflicting.empty() ) == aExpectConflict,
52 aName << ": expected " << ( aExpectConflict ? "conflict" : "clean" )
53 << " but conflicting=" << d.conflicting.size() );
54}
55} // namespace
56
57BOOST_AUTO_TEST_SUITE( ConstraintCombinations )
58
60{
61 using T = PCB_CONSTRAINT_TYPE;
62
63 // ---- Satisfiable (expect CLEAN) ----
64 run( "parallel(2 seg)", false,
65 []( BOARD& b )
66 {
67 auto* s1 = addSegment( b, { 0, 0 }, { 10 * MM, 0 } );
68 auto* s2 = addSegment( b, { 0, 5 * MM }, { 8 * MM, 3 * MM } );
69 author( b, T::PARALLEL, { whole( s1 ), whole( s2 ) } );
70 } );
71 run( "perpendicular(2 seg)", false,
72 []( BOARD& b )
73 {
74 auto* s1 = addSegment( b, { 0, 0 }, { 10 * MM, 0 } );
75 auto* s2 = addSegment( b, { 0, 5 * MM }, { 8 * MM, 3 * MM } );
76 author( b, T::PERPENDICULAR, { whole( s1 ), whole( s2 ) } );
77 } );
78 run( "horizontal(1 seg)", false,
79 []( BOARD& b )
80 {
81 auto* s = addSegment( b, { 0, 0 }, { 10 * MM, 2 * MM } );
82 author( b, T::HORIZONTAL, { whole( s ) } );
83 } );
84 run( "vertical(1 seg)", false,
85 []( BOARD& b )
86 {
87 auto* s = addSegment( b, { 0, 0 }, { 2 * MM, 10 * MM } );
88 author( b, T::VERTICAL, { whole( s ) } );
89 } );
90 run( "collinear(2 seg)", false,
91 []( BOARD& b )
92 {
93 auto* s1 = addSegment( b, { 0, 0 }, { 10 * MM, 0 } );
94 auto* s2 = addSegment( b, { 12 * MM, 1 * MM }, { 20 * MM, 2 * MM } );
95 author( b, T::COLLINEAR, { whole( s1 ), whole( s2 ) } );
96 } );
97 run( "equal_length(2 seg)", false,
98 []( BOARD& b )
99 {
100 auto* s1 = addSegment( b, { 0, 0 }, { 10 * MM, 0 } );
101 auto* s2 = addSegment( b, { 0, 5 * MM }, { 4 * MM, 5 * MM } );
102 author( b, T::EQUAL_LENGTH, { whole( s1 ), whole( s2 ) } );
103 } );
104 run( "fixed_length(1 seg)", false,
105 []( BOARD& b )
106 {
107 auto* s = addSegment( b, { 0, 0 }, { 4 * MM, 0 } );
108 author( b, T::FIXED_LENGTH, { whole( s ) }, 10.0 * MM );
109 } );
110 run( "angular(2 seg)", false,
111 []( BOARD& b )
112 {
113 auto* s1 = addSegment( b, { 0, 0 }, { 10 * MM, 0 } );
114 auto* s2 = addSegment( b, { 0, 0 }, { 0, 10 * MM } );
115 author( b, T::ANGULAR_DIMENSION, { whole( s1 ), whole( s2 ) }, 90.0 );
116 } );
117 run( "concentric(2 circ)", false,
118 []( BOARD& b )
119 {
120 auto* c1 = addCircle( b, { 0, 0 }, 5 * MM );
121 auto* c2 = addCircle( b, { 2 * MM, 1 * MM }, 3 * MM );
122 author( b, T::CONCENTRIC, { whole( c1 ), whole( c2 ) } );
123 } );
124 run( "equal_radius(2 circ)", false,
125 []( BOARD& b )
126 {
127 auto* c1 = addCircle( b, { 0, 0 }, 5 * MM );
128 auto* c2 = addCircle( b, { 20 * MM, 0 }, 3 * MM );
129 author( b, T::EQUAL_RADIUS, { whole( c1 ), whole( c2 ) } );
130 } );
131 run( "fixed_radius(1 circ)", false,
132 []( BOARD& b )
133 {
134 auto* c = addCircle( b, { 0, 0 }, 5 * MM );
135 author( b, T::FIXED_RADIUS, { whole( c ) }, 8.0 * MM );
136 } );
137 run( "tangent(line,circ)", false,
138 []( BOARD& b )
139 {
140 auto* s = addSegment( b, { 0, 0 }, { 20 * MM, 0 } );
141 auto* c = addCircle( b, { 10 * MM, 8 * MM }, 3 * MM );
142 author( b, T::TANGENT, { whole( s ), whole( c ) } );
143 } );
144 run( "tangent(2 circ)", false,
145 []( BOARD& b )
146 {
147 auto* c1 = addCircle( b, { 0, 0 }, 5 * MM );
148 auto* c2 = addCircle( b, { 15 * MM, 0 }, 4 * MM );
149 author( b, T::TANGENT, { whole( c1 ), whole( c2 ) } );
150 } );
151
152 // ---- Contradictions (expect CONFLICT) ----
153 run( "parallel+perpendicular", true,
154 []( BOARD& b )
155 {
156 auto* s1 = addSegment( b, { 0, 0 }, { 10 * MM, 0 } );
157 auto* s2 = addSegment( b, { 0, 5 * MM }, { 8 * MM, 3 * MM } );
158 author( b, T::PARALLEL, { whole( s1 ), whole( s2 ) } );
159 author( b, T::PERPENDICULAR, { whole( s1 ), whole( s2 ) } );
160 } );
161 run( "horizontal+vertical", true,
162 []( BOARD& b )
163 {
164 auto* s = addSegment( b, { 0, 0 }, { 10 * MM, 2 * MM } );
165 author( b, T::HORIZONTAL, { whole( s ) } );
166 author( b, T::VERTICAL, { whole( s ) } );
167 } );
168 run( "collinear+perpendicular", true,
169 []( BOARD& b )
170 {
171 auto* s1 = addSegment( b, { 0, 0 }, { 10 * MM, 0 } );
172 auto* s2 = addSegment( b, { 12 * MM, 1 * MM }, { 20 * MM, 2 * MM } );
173 author( b, T::COLLINEAR, { whole( s1 ), whole( s2 ) } );
174 author( b, T::PERPENDICULAR, { whole( s1 ), whole( s2 ) } );
175 } );
176 run( "equal_radius+two fixed_radius", true,
177 []( BOARD& b )
178 {
179 auto* c1 = addCircle( b, { 0, 0 }, 5 * MM );
180 auto* c2 = addCircle( b, { 20 * MM, 0 }, 3 * MM );
181 author( b, T::EQUAL_RADIUS, { whole( c1 ), whole( c2 ) } );
182 author( b, T::FIXED_RADIUS, { whole( c1 ) }, 5.0 * MM );
183 author( b, T::FIXED_RADIUS, { whole( c2 ) }, 9.0 * MM );
184 } );
185
186 // ---- Redundant (expect CLEAN, redundant>0) ----
187 run( "horiz+horiz+parallel(redundant)", false,
188 []( BOARD& b )
189 {
190 auto* s1 = addSegment( b, { 0, 0 }, { 10 * MM, 0 } );
191 auto* s2 = addSegment( b, { 0, 5 * MM }, { 10 * MM, 5 * MM } );
192 author( b, T::HORIZONTAL, { whole( s1 ) } );
193 author( b, T::HORIZONTAL, { whole( s2 ) } );
194 author( b, T::PARALLEL, { whole( s1 ), whole( s2 ) } );
195 } );
196
197 // ---- Locked variants ----
198 run( "perpendicular(locked+free)", false,
199 []( BOARD& b )
200 {
201 auto* lk = addSegment( b, { 0, 0 }, { 10 * MM, 0 } );
202 auto* fr = addSegment( b, { 0, 5 * MM }, { 8 * MM, 3 * MM } );
203 lk->SetLocked( true );
204 author( b, T::PERPENDICULAR, { whole( lk ), whole( fr ) } );
205 } );
206 run( "parallel+perp(locked+free)", true,
207 []( BOARD& b )
208 {
209 auto* lk = addSegment( b, { 0, 0 }, { 10 * MM, 0 } );
210 auto* fr = addSegment( b, { 0, 5 * MM }, { 8 * MM, 3 * MM } );
211 lk->SetLocked( true );
212 author( b, T::PARALLEL, { whole( lk ), whole( fr ) } );
213 author( b, T::PERPENDICULAR, { whole( lk ), whole( fr ) } );
214 } );
215}
216
217// Flipping a shape breaks its constraint, and the transform re-solve must restore it. Here a circle
218// tangent to a line is flipped away, then re-solved back to tangent.
219BOOST_AUTO_TEST_CASE( FlipRestoresTangent )
220{
221 BOARD b;
222 auto* line = addSegment( b, { 0, 0 }, { 20 * MM, 0 } );
223 auto* circ = addCircle( b, { 10 * MM, 5 * MM }, 5 * MM );
224 author( b, PCB_CONSTRAINT_TYPE::TANGENT, { whole( line ), whole( circ ) } );
225
226 auto tangentErr = [&]()
227 {
228 SEG seg( line->GetStart(), line->GetEnd() );
229 return std::abs( (double) seg.LineDistance( circ->GetCenter() ) - circ->GetRadius() );
230 };
231
232 BOOST_CHECK_LE( tangentErr(), 5000.0 ); // authored: tangent
233
234 circ->Flip( { 10 * MM, 30 * MM }, FLIP_DIRECTION::TOP_BOTTOM );
235 BOOST_CHECK_GT( tangentErr(), 1 * MM ); // flip broke tangency
236
237 std::vector<PCB_SHAPE*> shapes{ circ }, mod;
238 ReSolveShapeClusters( &b, shapes, &mod,
239 []( BOARD_ITEM* )
240 {
241 } );
242
243 BOOST_CHECK_LE( tangentErr(), 5000.0 ); // re-solve restored tangency
244 BOOST_CHECK( DiagnoseBoardConstraints( &b ).conflicting.empty() );
245}
246
247// Rotating a shape breaks its constraint, and the transform re-solve must restore it. Here one of
248// two parallel segments is rotated away, then re-solved back to parallel.
249BOOST_AUTO_TEST_CASE( RotateRestoresParallel )
250{
251 BOARD b;
252 auto* a = addSegment( b, { 0, 0 }, { 10 * MM, 0 } );
253 auto* c = addSegment( b, { 0, 5 * MM }, { 10 * MM, 5 * MM } );
254 author( b, PCB_CONSTRAINT_TYPE::PARALLEL, { whole( a ), whole( c ) } );
255
256 // |sin(angle between the two segment directions)|: zero when parallel.
257 auto parallelErr = [&]()
258 {
259 VECTOR2I da = a->GetEnd() - a->GetStart();
260 VECTOR2I dc = c->GetEnd() - c->GetStart();
261 double cross = (double) da.x * dc.y - (double) da.y * dc.x;
262 return std::abs( cross / ( da.EuclideanNorm() * dc.EuclideanNorm() ) );
263 };
264
265 BOOST_CHECK_LE( parallelErr(), 1e-3 ); // authored: parallel
266
267 c->Rotate( c->GetStart(), EDA_ANGLE( 30.0, DEGREES_T ) );
268 BOOST_CHECK_GT( parallelErr(), 0.1 ); // rotate broke parallelism
269
270 std::vector<PCB_SHAPE*> shapes{ c }, mod;
271 ReSolveShapeClusters( &b, shapes, &mod,
272 []( BOARD_ITEM* )
273 {
274 } );
275
276 // The re-solve pulls the pair back near parallel (approximate, not solver-exact) and the
277 // constraint stays satisfiable.
278 BOOST_CHECK_LE( parallelErr(), 0.05 );
279 BOOST_CHECK( DiagnoseBoardConstraints( &b ).conflicting.empty() );
280}
281
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),...
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.
BOARD_CONSTRAINT_DIAGNOSTICS DiagnoseBoardConstraints(BOARD *aBoard)
Diagnose every constraint cluster on the board (validate only – geometry is not changed) and return t...
A base class for any item which can be embedded within the BOARD container class, and therefore insta...
Definition board_item.h:83
void SetLocked(bool aLocked) override
Definition board_item.h:367
Information pertinent to a Pcbnew printed circuit board.
Definition board.h:373
const KIID m_Uuid
Definition eda_item.h:531
A geometric constraint between board items (issue #2329).
Definition seg.h:38
int LineDistance(const VECTOR2I &aP, bool aDetermineSide=false) const
Return the closest Euclidean distance between point aP and the line defined by the ends of segment (t...
Definition seg.cpp:742
T EuclideanNorm() const
Compute the Euclidean norm of the vector, which is defined as sqrt(x ** 2 + y ** 2).
Definition vector2d.h:279
@ DEGREES_T
Definition eda_angle.h:31
@ TOP_BOTTOM
Flip top to bottom (around the X axis)
Definition mirror.h:25
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)
EDA_ANGLE abs(const EDA_ANGLE &aAngle)
Definition eda_angle.h:400
@ WHOLE
The item as a whole (a segment as a line, a circle).
PCB_CONSTRAINT_TYPE
The geometric relationship a PCB_CONSTRAINT enforces between its members.
@ TANGENT
A line and a curve, or two curves, touch tangentially.
@ PARALLEL
Two segments are parallel.
Board-wide diagnostics for the constraint overlay and info bar.
One participant in a constraint: a referenced board item plus the feature of that item that participa...
BOOST_AUTO_TEST_CASE(HorizontalAlignment)
BOOST_AUTO_TEST_SUITE(CadstarPartParser)
BOOST_AUTO_TEST_CASE(Sweep)
BOOST_AUTO_TEST_SUITE_END()
BOOST_CHECK_MESSAGE(totalMismatches==0, std::to_string(totalMismatches)+" board(s) with strategy disagreements")
VECTOR2< int32_t > VECTOR2I
Definition vector2d.h:683