KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_constraint_lock.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 <cmath>
22#include <set>
23#include <vector>
24
26
27#include <board.h>
28#include <footprint.h>
29#include <pcb_shape.h>
32
34
35using namespace KI_TEST;
36
37
38namespace
39{
40double segLength( const PCB_SHAPE* aSeg )
41{
42 return ( aSeg->GetEnd() - aSeg->GetStart() ).EuclideanNorm();
43}
44} // namespace
45
46
47BOOST_AUTO_TEST_SUITE( ConstraintSolverLock )
48
49
50// A locked segment is an immovable reference: an equal-length constraint stretches only the free
51// segment, leaving the locked one untouched. Without lock-awareness the solver splits the change
52// across both and the locked segment moves, so this fails on the pre-fix code.
53BOOST_AUTO_TEST_CASE( LockedSegmentIsNotMoved )
54{
55 BOARD board;
56
57 PCB_SHAPE* locked = addSegment( board, { 0, 0 }, { 10 * MM, 0 } ); // length 10 mm
58 PCB_SHAPE* free = addSegment( board, { 0, 5 * MM }, { 4 * MM, 5 * MM } ); // length 4 mm
59
60 locked->SetLocked( true );
61
63 { { locked->m_Uuid, CONSTRAINT_ANCHOR::WHOLE }, { free->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } } );
64
65 std::vector<PCB_SHAPE*> shapes{ locked, free };
66 solveAndApply( board, shapes );
67
68 // The locked reference is byte-for-byte unchanged...
69 BOOST_CHECK_EQUAL( locked->GetStart(), VECTOR2I( 0, 0 ) );
70 BOOST_CHECK_EQUAL( locked->GetEnd(), VECTOR2I( 10 * MM, 0 ) );
71
72 // ...and the free segment took the locked segment's length.
73 BOOST_CHECK_LE( std::abs( segLength( free ) - 10.0 * MM ), 5000.0 );
74}
75
76
77// The same guarantee for circles: a locked circle keeps its radius under equal-radius, so the free
78// circle grows to it. The locked circle is ordered second, where a lock-unaware solve would move
79// it (the solver keeps the first-added radius), which is what makes this a regression.
80BOOST_AUTO_TEST_CASE( LockedCircleIsNotMoved )
81{
82 BOARD board;
83
84 PCB_SHAPE* free = addCircle( board, { 20 * MM, 0 }, 2 * MM );
85 PCB_SHAPE* locked = addCircle( board, { 0, 0 }, 5 * MM );
86
87 locked->SetLocked( true );
88
91
92 std::vector<PCB_SHAPE*> shapes{ free, locked };
93 solveAndApply( board, shapes );
94
95 BOOST_CHECK_EQUAL( locked->GetCenter(), VECTOR2I( 0, 0 ) );
96 BOOST_CHECK_EQUAL( locked->GetRadius(), 5 * MM );
97
98 BOOST_CHECK_LE( std::abs( free->GetRadius() - 5 * MM ), 5000 );
99}
100
101
102// The interactive create path (ApplyConstraintImmediately) must honour the lock even when the
103// locked shape is not the constraint's first member: it is neither moved nor reported as modified.
104BOOST_AUTO_TEST_CASE( LockedMemberNotMovedOnApply )
105{
106 BOARD board;
107
108 PCB_SHAPE* free = addSegment( board, { 0, 5 * MM }, { 4 * MM, 5 * MM } ); // member 0, length 4 mm
109 PCB_SHAPE* locked = addSegment( board, { 0, 0 }, { 10 * MM, 0 } ); // member 1, length 10 mm
110
111 locked->SetLocked( true );
112
115 { { free->m_Uuid, CONSTRAINT_ANCHOR::WHOLE }, { locked->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } } );
116
117 std::vector<PCB_SHAPE*> modified;
118 ApplyConstraintImmediately( &board, c, &modified,
119 []( BOARD_ITEM* )
120 {
121 } );
122
123 // The locked segment did not move and is not staged for undo.
124 BOOST_CHECK_EQUAL( locked->GetStart(), VECTOR2I( 0, 0 ) );
125 BOOST_CHECK_EQUAL( locked->GetEnd(), VECTOR2I( 10 * MM, 0 ) );
126 BOOST_CHECK( std::find( modified.begin(), modified.end(), locked ) == modified.end() );
127
128 // The free segment took the locked segment's length.
129 BOOST_CHECK_LE( std::abs( segLength( free ) - 10.0 * MM ), 5000.0 );
130}
131
132
133// Authoring an angle constraint against a locked reference must rotate the free segment, not shrink
134// it to a point. On the pre-fix solver the free segment collapsed to zero length and vanished, so
135// this fails without the stabilize pins and the collapse floor.
136BOOST_AUTO_TEST_CASE( PerpendicularDoesNotCollapseFreeSegment )
137{
138 BOARD board;
139
140 PCB_SHAPE* locked = addSegment( board, { 0, 0 }, { 10 * MM, 0 } ); // horizontal reference
141 PCB_SHAPE* free = addSegment( board, { 0, 5 * MM }, { 4 * MM, 6 * MM } ); // angled, length ~4.1 mm
142
143 locked->SetLocked( true );
144
147 { { locked->m_Uuid, CONSTRAINT_ANCHOR::WHOLE }, { free->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } } );
148
149 std::vector<PCB_SHAPE*> modified;
150 ApplyConstraintImmediately( &board, c, &modified,
151 []( BOARD_ITEM* )
152 {
153 } );
154
155 // The free segment kept a real length instead of collapsing...
156 BOOST_CHECK_GT( segLength( free ), 1 * MM );
157
158 // ...and turned perpendicular to the horizontal reference (vertical: endpoints share X).
159 BOOST_CHECK_LE( std::abs( free->GetEnd().x - free->GetStart().x ), 1000 );
160}
161
162
163// Parallel and perpendicular on the same two lines contradict, so both are flagged and the geometry
164// is left alone.
165BOOST_AUTO_TEST_CASE( ContradictoryConstraintsAreFlagged )
166{
167 BOARD board;
168
169 PCB_SHAPE* a = addSegment( board, { 0, 0 }, { 10 * MM, 0 } ); // horizontal
170 PCB_SHAPE* b = addSegment( board, { 0, 5 * MM }, { 4 * MM, 9 * MM } ); // 45 degrees
171
172 PCB_CONSTRAINT* par =
174 { { a->m_Uuid, CONSTRAINT_ANCHOR::WHOLE }, { b->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } } );
175 PCB_CONSTRAINT* perp =
177 { { a->m_Uuid, CONSTRAINT_ANCHOR::WHOLE }, { b->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } } );
178
180 std::set<KIID> conflicting( d.conflicting.begin(), d.conflicting.end() );
181
182 BOOST_CHECK( conflicting.count( par->m_Uuid ) );
183 BOOST_CHECK( conflicting.count( perp->m_Uuid ) );
184
185 // Diagnosis does not move geometry.
186 BOOST_CHECK_EQUAL( b->GetStart(), VECTOR2I( 0, 5 * MM ) );
187 BOOST_CHECK_EQUAL( b->GetEnd(), VECTOR2I( 4 * MM, 9 * MM ) );
188}
189
190
191// Two exactly-parallel lines with both parallel and perpendicular is still a contradiction. The
192// diagnostic solve must hold lengths hard, or it hides the conflict by collapsing a line to a point.
193BOOST_AUTO_TEST_CASE( ExactlyParallelPerpAndParallelFlagged )
194{
195 BOARD board;
196
197 // Two lines with identical direction (the failing case from a real board).
198 PCB_SHAPE* a = addSegment( board, { 190120000, 74420000 }, { 163850000, 100690000 } );
199 PCB_SHAPE* b = addSegment( board, { 202080000, 74420000 }, { 175810000, 100690000 } );
200
202 { { a->m_Uuid, CONSTRAINT_ANCHOR::WHOLE }, { b->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } } );
204 { { a->m_Uuid, CONSTRAINT_ANCHOR::WHOLE }, { b->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } } );
205
207
208 BOOST_CHECK( !d.conflicting.empty() );
209
210 // Diagnosis leaves both lines intact.
211 BOOST_CHECK_GT( segLength( a ), 1 * MM );
212 BOOST_CHECK_GT( segLength( b ), 1 * MM );
213}
214
215
216// Horizontal and vertical on one segment can only be met by a zero-length point, which the solver
217// reaches by collapsing the segment. The collapse itself must be flagged as over-constrained.
218BOOST_AUTO_TEST_CASE( HorizontalPlusVerticalIsFlagged )
219{
220 BOARD board;
221
222 PCB_SHAPE* s = addSegment( board, { 0, 0 }, { 10 * MM, 2 * MM } );
223
226
227 BOOST_CHECK( !DiagnoseBoardConstraints( &board ).conflicting.empty() );
228 BOOST_CHECK_GT( segLength( s ), 1 * MM ); // diagnosis does not move the board geometry
229}
230
231
232// An authored length constraint (solved, so the geometry satisfies it) is not falsely flagged by
233// the diagnostic length hold.
234BOOST_AUTO_TEST_CASE( AuthoredLengthConstraintNotFlagged )
235{
236 BOARD board;
237
238 PCB_SHAPE* s1 = addSegment( board, { 0, 0 }, { 10 * MM, 0 } );
239 PCB_SHAPE* s2 = addSegment( board, { 0, 5 * MM }, { 4 * MM, 5 * MM } );
240
241 PCB_CONSTRAINT* c =
243 { { s1->m_Uuid, CONSTRAINT_ANCHOR::WHOLE }, { s2->m_Uuid, CONSTRAINT_ANCHOR::WHOLE } } );
244
245 std::vector<PCB_SHAPE*> modified;
246 ApplyConstraintImmediately( &board, c, &modified,
247 []( BOARD_ITEM* )
248 {
249 } );
250
251 BOOST_CHECK( DiagnoseBoardConstraints( &board ).conflicting.empty() );
252}
253
254
255// A graphic inside a locked footprint carries no lock bit of its own, but the footprint's lock must
256// still freeze it: the solver stretches only the free board segment. BOARD_ITEM::IsLocked() never
257// consults the parent footprint, so this fails without ConstraintItemIsLocked.
258BOOST_AUTO_TEST_CASE( LockedFootprintChildIsNotMoved )
259{
260 BOARD board;
261
262 FOOTPRINT* footprint = new FOOTPRINT( &board );
263 board.Add( footprint );
264 footprint->SetLocked( true );
265
266 PCB_SHAPE* child = new PCB_SHAPE( footprint, SHAPE_T::SEGMENT );
267 child->SetStart( { 0, 0 } );
268 child->SetEnd( { 10 * MM, 0 } );
269 footprint->Add( child );
270
271 PCB_SHAPE* free = addSegment( board, { 0, 5 * MM }, { 4 * MM, 5 * MM } ); // length 4 mm
272
273 BOOST_CHECK( ConstraintItemIsLocked( child ) );
274
277
278 std::vector<PCB_SHAPE*> shapes{ child, free };
279 solveAndApply( board, shapes );
280
281 BOOST_CHECK_EQUAL( child->GetStart(), VECTOR2I( 0, 0 ) );
282 BOOST_CHECK_EQUAL( child->GetEnd(), VECTOR2I( 10 * MM, 0 ) );
283
284 BOOST_CHECK_LE( std::abs( segLength( free ) - 10.0 * MM ), 5000.0 );
285}
286
287
288// In the footprint editor nothing is ever lock-frozen, even when the edited footprint carries a
289// stale lock bit, mirroring BOARD_ITEM::IsLocked()'s FPHOLDER exemption.
290BOOST_AUTO_TEST_CASE( FootprintEditorIgnoresLocks )
291{
292 BOARD board;
294
295 FOOTPRINT* footprint = new FOOTPRINT( &board );
296 board.Add( footprint );
297 footprint->SetLocked( true );
298
299 PCB_SHAPE* child = new PCB_SHAPE( footprint, SHAPE_T::SEGMENT );
300 child->SetStart( { 0, 0 } );
301 child->SetEnd( { 10 * MM, 0 } );
302 footprint->Add( child );
303
304 // Only solver members (shapes, dimensions) are queried; the footprint itself is not, and its
305 // own IsLocked() reports the raw bit regardless of board use.
306 BOOST_CHECK( !ConstraintItemIsLocked( child ) );
307}
308
309
@ FPHOLDER
Definition board.h:365
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),...
bool ConstraintItemIsLocked(const BOARD_ITEM *aItem)
True when the solver must treat aItem as immovable, either locked itself or living inside a locked fo...
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
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
void SetBoardUse(BOARD_USE aUse)
Set what the board is going to be used for.
Definition board.h:385
const KIID m_Uuid
Definition eda_item.h:531
int GetRadius() const
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 SetLocked(bool isLocked) override
Set the #MODULE_is_LOCKED bit in the m_ModuleStatus.
Definition footprint.h:647
void Add(BOARD_ITEM *aItem, ADD_MODE aMode=ADD_MODE::INSERT, bool aSkipConnectivity=false) override
Removes an item from the container.
A geometric constraint between board items (issue #2329).
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)
void solveAndApply(BOARD &aBoard, const std::vector< PCB_SHAPE * > &aShapes)
Build the cluster of aShapes against every constraint on aBoard, solve it, and write the result back ...
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).
@ VERTICAL
A segment (or two points) is vertical.
@ PERPENDICULAR
Two segments are perpendicular.
@ HORIZONTAL
A segment (or two points) is horizontal.
@ EQUAL_RADIUS
Two arcs/circles have equal radius.
@ PARALLEL
Two segments are parallel.
@ EQUAL_LENGTH
Two segments have equal length.
Board-wide diagnostics for the constraint overlay and info bar.
BOOST_AUTO_TEST_CASE(HorizontalAlignment)
BOOST_AUTO_TEST_SUITE(CadstarPartParser)
BOOST_AUTO_TEST_CASE(LockedSegmentIsNotMoved)
BOOST_AUTO_TEST_SUITE_END()
BOOST_CHECK_EQUAL(result, "25.4")
VECTOR2< int32_t > VECTOR2I
Definition vector2d.h:683