KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_constraint_authoring.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 <map>
21#include <memory>
22#include <vector>
23
25#include <tool/tool_manager.h>
27
28#include <board.h>
29#include <board_commit.h>
30#include <pcb_shape.h>
33
35
36using namespace KI_TEST;
37
38
39BOOST_AUTO_TEST_SUITE( ConstraintSolverAuthoring )
40
41
42BOOST_AUTO_TEST_CASE( BuildParallelFromTwoSegments )
43{
44 BOARD board;
45 PCB_SHAPE* a = addSegment( board, { 0, 0 }, { 10 * MM, 0 } );
46 PCB_SHAPE* b = addSegment( board, { 0, 5 * MM }, { 10 * MM, 6 * MM } );
47
48 std::unique_ptr<PCB_CONSTRAINT> c =
50
51 BOOST_REQUIRE( c );
52 BOOST_REQUIRE_EQUAL( c->GetMembers().size(), 2 );
53 BOOST_CHECK( c->GetMembers()[0].m_item == a->m_Uuid );
54 BOOST_CHECK( c->GetMembers()[0].m_anchor == CONSTRAINT_ANCHOR::WHOLE );
55 BOOST_CHECK( c->GetMembers()[1].m_item == b->m_Uuid );
56}
57
58
59BOOST_AUTO_TEST_CASE( BuildFixedLengthMeasuresGeometry )
60{
61 BOARD board;
62 PCB_SHAPE* seg = addSegment( board, { 0, 0 }, { 7 * MM, 0 } );
63
64 std::unique_ptr<PCB_CONSTRAINT> c =
66
67 BOOST_REQUIRE( c );
68 BOOST_REQUIRE( c->HasValue() );
69 BOOST_CHECK_CLOSE( *c->GetValue(), 7.0 * MM, 1e-6 );
70}
71
72
73BOOST_AUTO_TEST_CASE( BuildAngularMeasuresAngle )
74{
75 BOARD board;
76 PCB_SHAPE* a = addSegment( board, { 0, 0 }, { 10 * MM, 0 } ); // along +x
77 PCB_SHAPE* b = addSegment( board, { 0, 0 }, { 0, 10 * MM } ); // along +y (90 deg)
78
79 std::unique_ptr<PCB_CONSTRAINT> c =
81
82 BOOST_REQUIRE( c );
83 BOOST_REQUIRE_EQUAL( c->GetMembers().size(), 2 );
84 BOOST_REQUIRE( c->HasValue() );
85 BOOST_CHECK_CLOSE( *c->GetValue(), 90.0, 1e-6 ); // degrees
86}
87
88
89BOOST_AUTO_TEST_CASE( BuildAngularIsUndirectedCorner )
90{
91 BOARD board;
92 PCB_SHAPE* a = addSegment( board, { 0, 0 }, { 10 * MM, 0 } ); // ray +x
93 PCB_SHAPE* b = addSegment( board, { 0, 0 }, { 0, -10 * MM } ); // ray -y
94
95 std::unique_ptr<PCB_CONSTRAINT> c =
97
98 BOOST_REQUIRE( c );
99 BOOST_REQUIRE( c->HasValue() );
100
101 // The corner is undirected: the same 90 degree opening regardless of segment orientation.
102 BOOST_CHECK_CLOSE( *c->GetValue(), 90.0, 1e-3 );
103}
104
105
106// A 120 degree corner authored in all four endpoint permutations and both member orders stores 120,
107// never folded to 60 (Seth: obtuse outline corners are preserved).
108BOOST_AUTO_TEST_CASE( BuildAngularObtuseCornerInvariant )
109{
110 const VECTOR2I vertex{ 0, 0 };
111 const VECTOR2I aFar{ 10 * MM, 0 }; // ray at 0 deg
112 const VECTOR2I bFar{ -5 * MM, 8660254 }; // ray at 120 deg (10 mm at 120)
113
114 for( bool swapA : { false, true } )
115 {
116 for( bool swapB : { false, true } )
117 {
118 for( bool swapOrder : { false, true } )
119 {
120 BOARD board;
121 PCB_SHAPE* a = swapA ? addSegment( board, aFar, vertex )
122 : addSegment( board, vertex, aFar );
123 PCB_SHAPE* b = swapB ? addSegment( board, bFar, vertex )
124 : addSegment( board, vertex, bFar );
125
126 std::vector<BOARD_ITEM*> items = swapOrder ? std::vector<BOARD_ITEM*>{ b, a }
127 : std::vector<BOARD_ITEM*>{ a, b };
128
129 std::unique_ptr<PCB_CONSTRAINT> c = BuildConstraintFromItems(
131
132 BOOST_REQUIRE( c );
133 BOOST_REQUIRE( c->HasValue() );
134 BOOST_CHECK_CLOSE( *c->GetValue(), 120.0, 0.05 );
135 }
136 }
137 }
138}
139
140
141BOOST_AUTO_TEST_CASE( BuildAngularDomainEndpoints )
142{
143 {
144 // Both rays leave the vertex the same way: a 0 degree corner.
145 BOARD board;
146 PCB_SHAPE* a = addSegment( board, { 0, 0 }, { 10 * MM, 0 } );
147 PCB_SHAPE* b = addSegment( board, { 0, 0 }, { 20 * MM, 0 } );
149 BOOST_REQUIRE( c && c->HasValue() );
150 BOOST_CHECK_SMALL( *c->GetValue(), 1e-3 );
151 }
152 {
153 // Rays leave the vertex in opposite directions: a straight 180 degree corner.
154 BOARD board;
155 PCB_SHAPE* a = addSegment( board, { 0, 0 }, { -10 * MM, 0 } );
156 PCB_SHAPE* b = addSegment( board, { 0, 0 }, { 10 * MM, 0 } );
158 BOOST_REQUIRE( c && c->HasValue() );
159 BOOST_CHECK_CLOSE( *c->GetValue(), 180.0, 1e-3 );
160 }
161}
162
163
164// Two segments that do not touch still get a well-defined corner from nearest-endpoint pairing,
165// with no dependence on SEG::IntersectLines.
166BOOST_AUTO_TEST_CASE( BuildAngularDisconnectedLines )
167{
168 BOARD board;
169 PCB_SHAPE* a = addSegment( board, { 0, 0 }, { 10 * MM, 0 } ); // ray +x
170 PCB_SHAPE* b = addSegment( board, { 0, 2 * MM }, { 0, 12 * MM } ); // ray +y, 2 mm gap
171
172 std::unique_ptr<PCB_CONSTRAINT> c =
174
175 BOOST_REQUIRE( c && c->HasValue() );
176
177 // SEG::Angle uses each segment's true direction, so the gap does not skew the measurement.
178 BOOST_CHECK_CLOSE( *c->GetValue(), 90.0, 1e-3 );
179}
180
181
182BOOST_AUTO_TEST_CASE( WrongSelectionYieldsNoConstraint )
183{
184 BOARD board;
185 PCB_SHAPE* a = addSegment( board, { 0, 0 }, { 10 * MM, 0 } );
186
187 // Parallel needs two segments.
188 BOOST_CHECK( !BuildConstraintFromItems( &board, PCB_CONSTRAINT_TYPE::PARALLEL, { a } ) );
189
190 // Concentric needs circles, not a segment.
191 BOOST_CHECK( !BuildConstraintFromItems( &board, PCB_CONSTRAINT_TYPE::CONCENTRIC, { a, a } ) );
192
193 // Coincident needs point anchors, not whole shapes.
194 BOOST_CHECK( !BuildConstraintFromItems( &board, PCB_CONSTRAINT_TYPE::COINCIDENT, { a, a } ) );
195}
196
197
198BOOST_AUTO_TEST_CASE( AngularRejectsZeroLengthSegment )
199{
200 BOARD board;
201 PCB_SHAPE* a = addSegment( board, { 0, 0 }, { 10 * MM, 0 } );
202 PCB_SHAPE* zero = addSegment( board, { 5 * MM, 5 * MM }, { 5 * MM, 5 * MM } ); // no direction
203
205 { a, zero } ) );
206}
207
208
209// The nearest-anchor picker snaps a click to the closest shape endpoint/center.
210BOOST_AUTO_TEST_CASE( NearestAnchorSnapsToEndpoint )
211{
212 BOARD board;
213 PCB_SHAPE* seg = addSegment( board, { 0, 0 }, { 10 * MM, 0 } );
214
215 // Close to the end (10mm,0): snaps to that segment's END.
216 auto hit = NearestConstraintAnchor( &board, { 10 * MM + 100, 200 }, 1 * MM );
217 BOOST_REQUIRE( hit.has_value() );
218 BOOST_CHECK( hit->m_item == seg->m_Uuid );
219 BOOST_CHECK( hit->m_anchor == CONSTRAINT_ANCHOR::END );
220
221 // Far from any anchor: nothing within tolerance.
222 BOOST_CHECK( !NearestConstraintAnchor( &board, { 5 * MM, 5 * MM }, 1 * MM ).has_value() );
223}
224
225
226// Excluding a handle skips only that shape and anchor coincident endpoint elsewhere stays reachable
227BOOST_AUTO_TEST_CASE( NearestAnchorExcludesPickedHandleNotCoincident )
228{
229 BOARD board;
230 PCB_SHAPE* a = addSegment( board, { 0, 0 }, { 10 * MM, 0 } );
231 PCB_SHAPE* b = addSegment( board, { 10 * MM, 0 }, { 20 * MM, 0 } );
232
233 const VECTOR2I shared{ 10 * MM, 0 }; // a's END and b's START coincide here
234
235 std::optional<CONSTRAINT_MEMBER> first = NearestConstraintAnchor( &board, shared, 1 * MM );
236 BOOST_REQUIRE( first.has_value() );
237
238 // Exclude first handle still finds coincident endpoint as distinct member on b
239 std::optional<CONSTRAINT_MEMBER> second =
240 NearestConstraintAnchor( &board, shared, 1 * MM, { *first } );
241 BOOST_REQUIRE( second.has_value() );
242 BOOST_CHECK( *second != *first );
243 BOOST_CHECK( second->m_item != first->m_item );
244 BOOST_CHECK( ConstraintAnchorPosition( &board, *first )
245 == ConstraintAnchorPosition( &board, *second ) );
246
247 // Two handles are shape a and shape b own endpoints at the shared point
248 BOOST_CHECK( ( first->m_item == a->m_Uuid && second->m_item == b->m_Uuid )
249 || ( first->m_item == b->m_Uuid && second->m_item == a->m_Uuid ) );
250
251 // Both handles excluded nothing else within tolerance
252 BOOST_CHECK( !NearestConstraintAnchor( &board, shared, 1 * MM, { *first, *second } ).has_value() );
253}
254
255
256// Applying an authored constraint through a commit is a single undoable step.
257BOOST_AUTO_TEST_CASE( ApplyIsOneUndoStep )
258{
259 BOARD board;
260 TOOL_MANAGER mgr;
262 mgr.SetEnvironment( &board, nullptr, nullptr, nullptr, nullptr );
263 mgr.RegisterTool( tool );
264
265 PCB_SHAPE* a = addSegment( board, { 0, 0 }, { 10 * MM, 0 } );
266 PCB_SHAPE* b = addSegment( board, { 0, 5 * MM }, { 10 * MM, 6 * MM } );
267
268 std::unique_ptr<PCB_CONSTRAINT> c =
270 BOOST_REQUIRE( c );
271
272 BOARD_COMMIT commit( tool );
273 commit.Add( c.release() );
274 commit.Push( wxT( "add constraint" ) );
275
276 BOOST_CHECK_EQUAL( board.Constraints().size(), 1 );
277
278 // Authoring then reverting (here a subsequent removal) returns to no constraints in one step.
279 PCB_CONSTRAINT* added = board.Constraints().front();
280 BOARD_COMMIT removeCommit( tool );
281 removeCommit.Remove( added );
282 removeCommit.Push( wxT( "remove constraint" ) );
283
284 BOOST_CHECK( board.Constraints().empty() );
285}
286
287
288// The radial constraints accept arcs, and concentric also accepts ellipses, matching the solver.
289BOOST_AUTO_TEST_CASE( BuildRadialFromArcsAndEllipses )
290{
291 BOARD board;
292 PCB_SHAPE* circle = addCircle( board, { 0, 0 }, 5 * MM );
293 PCB_SHAPE* arc = addArc( board, { 10 * MM, 0 }, { 12 * MM, 2 * MM }, { 14 * MM, 0 } );
294 PCB_SHAPE* ellipse = addEllipse( board, { 30 * MM, 0 }, 8 * MM, 4 * MM, EDA_ANGLE( 0.0, DEGREES_T ) );
295
296 // Fixed radius and equal radius work on arcs.
297 BOOST_CHECK( BuildConstraintFromItems( &board, PCB_CONSTRAINT_TYPE::FIXED_RADIUS, { arc } ) );
298 BOOST_CHECK( BuildConstraintFromItems( &board, PCB_CONSTRAINT_TYPE::EQUAL_RADIUS, { circle, arc } ) );
299
300 // Concentric additionally works on ellipses.
301 BOOST_CHECK( BuildConstraintFromItems( &board, PCB_CONSTRAINT_TYPE::CONCENTRIC, { circle, ellipse } ) );
302 BOOST_CHECK( BuildConstraintFromItems( &board, PCB_CONSTRAINT_TYPE::CONCENTRIC, { arc, ellipse } ) );
303
304 // Equal radius still rejects an ellipse (no single radius).
305 BOOST_CHECK( !BuildConstraintFromItems( &board, PCB_CONSTRAINT_TYPE::EQUAL_RADIUS, { circle, ellipse } ) );
306}
307
308
309// Nothing remembered dialog opens on measured geometry
310BOOST_AUTO_TEST_CASE( InitialValueDefaultsToMeasured )
311{
312 std::map<PCB_CONSTRAINT_TYPE, double> remembered;
313
314 BOOST_CHECK_CLOSE( InitialConstraintValue( PCB_CONSTRAINT_TYPE::FIXED_LENGTH, 7.0 * MM, remembered ),
315 7.0 * MM, 1e-6 );
316 BOOST_CHECK_CLOSE( InitialConstraintValue( PCB_CONSTRAINT_TYPE::ARC_ANGLE, 90.0, remembered ), 90.0,
317 1e-6 );
318}
319
320
321// Once set remembered value overrides measurement
322BOOST_AUTO_TEST_CASE( InitialValueRemembersLastUsed )
323{
324 std::map<PCB_CONSTRAINT_TYPE, double> remembered;
325 remembered[PCB_CONSTRAINT_TYPE::FIXED_LENGTH] = 5.0 * MM;
326
327 BOOST_CHECK_CLOSE( InitialConstraintValue( PCB_CONSTRAINT_TYPE::FIXED_LENGTH, 7.0 * MM, remembered ),
328 5.0 * MM, 1e-6 );
329}
330
331
332// Keyed by type length value never bleeds into angle default
333BOOST_AUTO_TEST_CASE( InitialValueDoesNotMixTypes )
334{
335 std::map<PCB_CONSTRAINT_TYPE, double> remembered;
336 remembered[PCB_CONSTRAINT_TYPE::FIXED_LENGTH] = 5.0 * MM;
337
338 // ARC_ANGLE unremembered opens on measured 90 degrees
339 BOOST_CHECK_CLOSE( InitialConstraintValue( PCB_CONSTRAINT_TYPE::ARC_ANGLE, 90.0, remembered ), 90.0,
340 1e-6 );
341
342 // Recording an angle leaves the earlier length value untouched.
343 remembered[PCB_CONSTRAINT_TYPE::ARC_ANGLE] = 60.0;
344
345 BOOST_CHECK_CLOSE( InitialConstraintValue( PCB_CONSTRAINT_TYPE::FIXED_LENGTH, 7.0 * MM, remembered ),
346 5.0 * MM, 1e-6 );
347 BOOST_CHECK_CLOSE( InitialConstraintValue( PCB_CONSTRAINT_TYPE::ARC_ANGLE, 90.0, remembered ), 60.0,
348 1e-6 );
349}
350
351
virtual void Push(const wxString &aMessage=wxEmptyString, int aCommitFlags=0) override
Execute the changes.
Information pertinent to a Pcbnew printed circuit board.
Definition board.h:373
const CONSTRAINTS & Constraints() const
Geometric constraints (#2329) owned by this board.
Definition board.h:465
COMMIT & Remove(EDA_ITEM *aItem, BASE_SCREEN *aScreen=nullptr)
Remove a new item from the model.
Definition commit.h:86
COMMIT & Add(EDA_ITEM *aItem, BASE_SCREEN *aScreen=nullptr)
Add a new item to the model.
Definition commit.h:74
const KIID m_Uuid
Definition eda_item.h:531
A geometric constraint between board items (issue #2329).
Master controller class:
void RegisterTool(TOOL_BASE *aTool)
Add a tool to the manager set and sets it up.
void SetEnvironment(EDA_ITEM *aModel, KIGFX::VIEW *aView, KIGFX::VIEW_CONTROLS *aViewControls, APP_SETTINGS_BASE *aSettings, TOOLS_HOLDER *aFrame)
Set the work environment (model, view, view controls and the parent window).
double InitialConstraintValue(PCB_CONSTRAINT_TYPE aType, double aMeasured, const std::map< PCB_CONSTRAINT_TYPE, double > &aRemembered)
Value a freshly authored constraint dialog should open with.
std::unique_ptr< PCB_CONSTRAINT > BuildConstraintFromItems(BOARD_ITEM *aParent, PCB_CONSTRAINT_TYPE aType, const std::vector< BOARD_ITEM * > &aItems)
Build a constraint of aType from a set of selected board items, or nullptr if the selection does not ...
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...
std::optional< CONSTRAINT_MEMBER > NearestConstraintAnchor(BOARD *aBoard, const VECTOR2I &aPos, double aMaxDist, const std::vector< CONSTRAINT_MEMBER > &aExclude)
Find the constrainable-item anchor (a shape's segment/arc endpoint or centre, or a dimension's featur...
@ DEGREES_T
Definition eda_angle.h:31
PCB_SHAPE * addSegment(BOARD &aBoard, const VECTOR2I &aStart, const VECTOR2I &aEnd)
PCB_SHAPE * addArc(BOARD &aBoard, const VECTOR2I &aStart, const VECTOR2I &aMid, const VECTOR2I &aEnd)
PCB_SHAPE * addCircle(BOARD &aBoard, const VECTOR2I &aCenter, int aRadius)
PCB_SHAPE * addEllipse(BOARD &aBoard, const VECTOR2I &aCenter, int aMajor, int aMinor, const EDA_ANGLE &aRotation)
constexpr int MM
@ WHOLE
The item as a whole (a segment as a line, a circle).
@ END
Second endpoint of a segment or arc.
@ CONCENTRIC
Two arcs/circles share a center.
@ COINCIDENT
Two points are made to coincide.
@ FIXED_RADIUS
An arc/circle has a driving radius value.
@ EQUAL_RADIUS
Two arcs/circles have equal radius.
@ FIXED_LENGTH
A segment has a driving length value.
@ ANGULAR_DIMENSION
An angle between members (driving or reference).
@ ARC_ANGLE
An arc has a driving or reference swept-angle value.
@ PARALLEL
Two segments are parallel.
BOOST_AUTO_TEST_CASE(HorizontalAlignment)
BOOST_AUTO_TEST_SUITE(CadstarPartParser)
BOOST_AUTO_TEST_CASE(BuildParallelFromTwoSegments)
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