KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_constraint_drag.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 <array>
22#include <cmath>
23#include <vector>
24
26#include <tool/tool_manager.h>
28
29#include <board.h>
30#include <board_commit.h>
31#include <pcb_shape.h>
32#include <geometry/shape_arc.h>
36
37
38BOOST_AUTO_TEST_SUITE( ConstraintSolverDrag )
39
40
41namespace
42{
43constexpr int MM = 1000000;
44
45
46SNAP_TARGET_ID targetId( const char* aName )
47{
48 return KIID::FromName( aName ).AsBytes();
49}
50
51
52SNAP_STABLE_ID testSnapId( const char* aName, SNAP_ID_KIND aKind = SNAP_ID_KIND::ITEM_GEOMETRY )
53{
54 return { aKind, targetId( aName ) };
55}
56
57
58struct DRAG_FIXTURE
59{
60 BOARD board;
61 TOOL_MANAGER mgr;
62 KI_TEST::DUMMY_TOOL* tool;
63
64 DRAG_FIXTURE() :
65 tool( new KI_TEST::DUMMY_TOOL() )
66 {
67 mgr.SetEnvironment( &board, nullptr, nullptr, nullptr, nullptr );
68 mgr.RegisterTool( tool );
69 }
70
71 PCB_SHAPE* addSegment( const VECTOR2I& aStart, const VECTOR2I& aEnd )
72 {
73 PCB_SHAPE* seg = new PCB_SHAPE( &board, SHAPE_T::SEGMENT );
74 seg->SetStart( aStart );
75 seg->SetEnd( aEnd );
76 board.Add( seg );
77 return seg;
78 }
79
80 PCB_SHAPE* addCircle( const VECTOR2I& aCenter, int aRadius )
81 {
82 PCB_SHAPE* circle = new PCB_SHAPE( &board, SHAPE_T::CIRCLE );
83 circle->SetCenter( aCenter );
84 circle->SetRadius( aRadius );
85 board.Add( circle );
86 return circle;
87 }
88
89 PCB_SHAPE* addArc( const VECTOR2I& aStart, const VECTOR2I& aMid, const VECTOR2I& aEnd )
90 {
91 PCB_SHAPE* arc = new PCB_SHAPE( &board, SHAPE_T::ARC );
92 arc->SetArcGeometry( aStart, aMid, aEnd );
93 board.Add( arc );
94 return arc;
95 }
96
97 PCB_SHAPE* addRect( const VECTOR2I& aStart, const VECTOR2I& aEnd )
98 {
99 PCB_SHAPE* rect = new PCB_SHAPE( &board, SHAPE_T::RECTANGLE );
100 rect->SetStart( aStart );
101 rect->SetEnd( aEnd );
102 board.Add( rect );
103 return rect;
104 }
105
106 PCB_SHAPE* addPoly( const std::vector<VECTOR2I>& aPoints )
107 {
108 PCB_SHAPE* poly = new PCB_SHAPE( &board, SHAPE_T::POLY );
109 poly->SetPolyPoints( aPoints );
110 board.Add( poly );
111 return poly;
112 }
113
114 PCB_CONSTRAINT* addCoincident( PCB_SHAPE* aA, CONSTRAINT_ANCHOR aAnchorA, PCB_SHAPE* aB,
115 CONSTRAINT_ANCHOR aAnchorB )
116 {
117 PCB_CONSTRAINT* c = new PCB_CONSTRAINT( &board, PCB_CONSTRAINT_TYPE::COINCIDENT );
118 c->AddMember( aA->m_Uuid, aAnchorA );
119 c->AddMember( aB->m_Uuid, aAnchorB );
120 board.Add( c );
121 return c;
122 }
123};
124
125
126// Mirrors PCB_POINT_EDITOR updateItem staging dragged shape moving it then rederiving constrained cluster
127// staging each neighbor before it changes
128void simulateDrag( BOARD_COMMIT& aCommit, BOARD* aBoard, PCB_SHAPE* aShape,
129 CONSTRAINT_ANCHOR aAnchor, const VECTOR2I& aCursor,
130 std::vector<PCB_SHAPE*>* aModified )
131{
132 aCommit.Modify( aShape );
133
134 if( aAnchor == CONSTRAINT_ANCHOR::START )
135 aShape->SetStart( aCursor );
136 else if( aAnchor == CONSTRAINT_ANCHOR::END )
137 aShape->SetEnd( aCursor );
138 else if( aAnchor == CONSTRAINT_ANCHOR::CENTER )
139 aShape->SetCenter( aCursor );
140
141 SolveCluster( aBoard, { aShape->m_Uuid, aAnchor }, aCursor, aModified,
142 [&]( BOARD_ITEM* aNeighbor )
143 {
144 aCommit.Modify( aNeighbor );
145 } );
146}
147} // namespace
148
149
150// Dragging one end of a corner re-derives the coincident neighbor; reverting the same commit
151// restores both shapes (one undoable transaction).
152BOOST_FIXTURE_TEST_CASE( DragReDerivesNeighborRevertRestores, DRAG_FIXTURE )
153{
154 PCB_SHAPE* a = addSegment( { 0, 0 }, { 10 * MM, 0 } );
155 PCB_SHAPE* b = addSegment( { 10 * MM, 0 }, { 10 * MM, 10 * MM } );
156
158 c->AddMember( a->m_Uuid, CONSTRAINT_ANCHOR::END );
159 c->AddMember( b->m_Uuid, CONSTRAINT_ANCHOR::START );
160 board.Add( c );
161
162 const VECTOR2I aEnd0 = a->GetEnd();
163 const VECTOR2I bStart0 = b->GetStart();
164
165 std::vector<PCB_SHAPE*> modified;
166
167 BOARD_COMMIT commit( tool );
168 simulateDrag( commit, &board, a, CONSTRAINT_ANCHOR::END, { 12 * MM, 3 * MM }, &modified );
169
170 // The neighbor followed the dragged corner and is reported as modified.
171 BOOST_CHECK_LE( ( a->GetEnd() - b->GetStart() ).EuclideanNorm(), 100 );
172 BOOST_CHECK( std::find( modified.begin(), modified.end(), b ) != modified.end() );
173 BOOST_CHECK( a->GetEnd() != aEnd0 );
174
175 // Revert restores every shape the transaction touched.
176 commit.Revert();
177 BOOST_CHECK_EQUAL( a->GetEnd(), aEnd0 );
178 BOOST_CHECK_EQUAL( b->GetStart(), bStart0 );
179}
180
181
182BOOST_FIXTURE_TEST_CASE( PersistentDragSessionWarmStartsSuccessivePreviewFrames, DRAG_FIXTURE )
183{
184 PCB_SHAPE* a = addSegment( { 0, 0 }, { 10 * MM, 0 } );
185 PCB_SHAPE* b = addSegment( { 10 * MM, 0 }, { 10 * MM, 10 * MM } );
186 addCoincident( a, CONSTRAINT_ANCHOR::END, b, CONSTRAINT_ANCHOR::START );
187
190 std::vector<PCB_SHAPE*> modified;
191 BOARD_COMMIT commit( tool );
192 const VECTOR2I originalAEnd = a->GetEnd();
193 const VECTOR2I originalBStart = b->GetStart();
194
195 BOOST_REQUIRE( session.Build( &board, dragged ) );
196 BOOST_CHECK( session.Matches( dragged ) );
197 commit.Modify( a );
198
199 CONSTRAINT_DIAGNOSIS first = session.Solve(
200 { 12 * MM, 2 * MM }, &modified,
201 [&]( BOARD_ITEM* aItem )
202 {
203 commit.Modify( aItem );
204 },
205 false, false );
206 BOOST_REQUIRE( first.solved );
207 BOOST_CHECK_LE( ( a->GetEnd() - b->GetStart() ).EuclideanNorm(), 100 );
208
209 CONSTRAINT_DIAGNOSIS second = session.Solve(
210 { 14 * MM, 4 * MM }, &modified,
211 [&]( BOARD_ITEM* aItem )
212 {
213 commit.Modify( aItem );
214 },
215 false, false );
216 BOOST_REQUIRE( second.solved );
217 BOOST_CHECK_LE( ( a->GetEnd() - b->GetStart() ).EuclideanNorm(), 100 );
218 BOOST_CHECK_LE( ( a->GetEnd() - VECTOR2I( 14 * MM, 4 * MM ) ).EuclideanNorm(), 100 );
219
220 commit.Revert();
221 BOOST_CHECK_EQUAL( a->GetEnd(), originalAEnd );
222 BOOST_CHECK_EQUAL( b->GetStart(), originalBStart );
223}
224
225
226BOOST_FIXTURE_TEST_CASE( PersistentDragSessionRejectsInexactSnapTarget, DRAG_FIXTURE )
227{
228 PCB_SHAPE* seg = addSegment( { 0, 0 }, { 10 * MM, 0 } );
229
231 length->AddMember( seg->m_Uuid, CONSTRAINT_ANCHOR::WHOLE );
232 length->SetValue( 10.0 * MM );
233 board.Add( length );
234
237 BOOST_CHECK( session.IsExactFeasible( { 10 * MM, 0 } ) );
238 BOOST_CHECK( session.IsExactFeasible( { 10 * MM, 0 } ) );
239 BOOST_CHECK( !session.IsExactFeasible( { 20 * MM, 0 } ) );
240 BOOST_CHECK_EQUAL( seg->GetEnd(), VECTOR2I( 10 * MM, 0 ) );
241}
242
243
244BOOST_FIXTURE_TEST_CASE( PersistentDragSessionExactifiesEqualGapTarget, DRAG_FIXTURE )
245{
246 PCB_SHAPE* seg = addSegment( { 0, 0 }, { 10 * MM, 0 } );
247
249 length->AddMember( seg->m_Uuid, CONSTRAINT_ANCHOR::WHOLE );
250 length->SetValue( 10.0 * MM );
251 board.Add( length );
252
254 horizontal->AddMember( seg->m_Uuid, CONSTRAINT_ANCHOR::WHOLE );
255 board.Add( horizontal );
256
259
260 SNAP_SOURCE_CONTEXT context;
261 context.sourcePoint = { 10 * MM, 0 };
262 SNAP_CANDIDATE gap =
266
267 SNAP_RESULT result = session.ResolveCandidates( context, { gap } );
269 BOOST_REQUIRE_EQUAL( result.quantizedResiduals.size(), 1 );
270 BOOST_CHECK_EQUAL( result.quantizedResiduals.front(), 0.0 );
271 BOOST_CHECK_EQUAL( seg->GetEnd(), VECTOR2I( 10 * MM, 0 ) );
272}
273
274
275BOOST_FIXTURE_TEST_CASE( PersistentMoveSessionArbitratesBeforeApplying, DRAG_FIXTURE )
276{
277 PCB_SHAPE* moved = addSegment( { 0, 0 }, { 10 * MM, 0 } );
278 PCB_SHAPE* neighbor = addSegment( { 10 * MM, 0 }, { 20 * MM, 0 } );
279 addCoincident( moved, CONSTRAINT_ANCHOR::END, neighbor, CONSTRAINT_ANCHOR::START );
280
282 BOOST_REQUIRE( session.Build( &board, { moved }, { 0, 0 } ) );
283
284 SNAP_SOURCE_CONTEXT context;
285 context.sourcePoint = { 5 * MM, 0 };
286 SNAP_CANDIDATE target = SNAP_CANDIDATE::Point( testSnapId( "target" ), SNAP_PRIORITY_TIER::OBJECT,
288
289 // A feasibility callback reports the residual and the freedom consumed; SNAP_RESOLVER owns
290 // the accepted list, so honouring a two-degree candidate shows up as zero freedom left.
291 SNAP_RESULT result = session.ResolveCandidates( context, { target } );
293 BOOST_CHECK_EQUAL( result.position, context.sourcePoint );
294 BOOST_REQUIRE_EQUAL( result.quantizedResiduals.size(), 1 );
295 BOOST_CHECK_EQUAL( result.quantizedResiduals.front(), 0.0 );
296 BOOST_CHECK_EQUAL( result.remainingDof, 0 );
297 BOOST_CHECK_EQUAL( moved->GetStart(), VECTOR2I( 0, 0 ) );
298 BOOST_CHECK_EQUAL( neighbor->GetStart(), VECTOR2I( 10 * MM, 0 ) );
299
300 std::vector<PCB_SHAPE*> modified;
301 BOOST_REQUIRE( session.Solve( result.position, &modified, {} ) );
302 BOOST_CHECK_EQUAL( moved->GetStart(), VECTOR2I( 5 * MM, 0 ) );
303 BOOST_CHECK_EQUAL( moved->GetEnd(), VECTOR2I( 15 * MM, 0 ) );
304 BOOST_CHECK_EQUAL( neighbor->GetStart(), VECTOR2I( 15 * MM, 0 ) );
305}
306
307
308BOOST_FIXTURE_TEST_CASE( RigidTranslationPreservesRoundShapeRadii, DRAG_FIXTURE )
309{
310 PCB_SHAPE* circle = addCircle( { 0, 0 }, 4 * MM );
311 PCB_SHAPE* arc = addArc( { 20 * MM, 0 }, { 17 * MM, 3 * MM }, { 14 * MM, 0 } );
312 PCB_SHAPE* ellipse = new PCB_SHAPE( &board, SHAPE_T::ELLIPSE );
313 ellipse->SetEllipseCenter( { 30 * MM, 0 } );
314 ellipse->SetEllipseMajorRadius( 6 * MM );
315 ellipse->SetEllipseMinorRadius( 3 * MM );
316 board.Add( ellipse );
317
318 const int circleRadius = circle->GetRadius();
319 const int arcRadius = arc->GetRadius();
320 const int ellipseMinorRadius = ellipse->GetEllipseMinorRadius();
321
323 BOOST_REQUIRE( adapter.Build( { circle, arc, ellipse }, {} ) );
325 adapter.SolveRigidTranslation( { circle->m_Uuid, arc->m_Uuid, ellipse->m_Uuid }, { 3 * MM, 2 * MM } ) );
326 adapter.Apply();
327
328 BOOST_CHECK_EQUAL( circle->GetRadius(), circleRadius );
329 BOOST_CHECK_EQUAL( arc->GetRadius(), arcRadius );
330 BOOST_CHECK_EQUAL( ellipse->GetEllipseMinorRadius(), ellipseMinorRadius );
331}
332
333
334BOOST_AUTO_TEST_CASE( UnbuiltMoveSessionRejectsCandidates )
335{
337 SNAP_SOURCE_CONTEXT context;
338 context.sourcePoint = { 10, 20 };
339 SNAP_CANDIDATE candidate = SNAP_CANDIDATE::Point( testSnapId( "point" ), SNAP_PRIORITY_TIER::OBJECT,
341
342 SNAP_RESULT result = session.ResolveCandidates( context, { candidate } );
343
344 BOOST_CHECK( result.status == SNAP_RESULT_STATUS::INCOMPATIBLE );
345}
346
347
348BOOST_FIXTURE_TEST_CASE( PersistentMoveSessionRejectsIncompatibleTarget, DRAG_FIXTURE )
349{
350 PCB_SHAPE* moved = addSegment( { 0, 0 }, { 10 * MM, 0 } );
351 PCB_SHAPE* locked = addSegment( { 10 * MM, 0 }, { 20 * MM, 0 } );
352 locked->SetLocked( true );
354
356 BOOST_REQUIRE( session.Build( &board, { moved }, { 0, 0 } ) );
357
358 SNAP_SOURCE_CONTEXT context;
359 context.sourcePoint = { 5 * MM, 0 };
360 SNAP_CANDIDATE target = SNAP_CANDIDATE::Point( testSnapId( "blocked" ), SNAP_PRIORITY_TIER::OBJECT,
362
363 BOOST_CHECK( session.ResolveCandidates( context, { target } ).status == SNAP_RESULT_STATUS::INCOMPATIBLE );
364 BOOST_CHECK_EQUAL( moved->GetStart(), VECTOR2I( 0, 0 ) );
365 BOOST_CHECK_EQUAL( locked->GetStart(), VECTOR2I( 10 * MM, 0 ) );
366}
367
368
369BOOST_FIXTURE_TEST_CASE( PersistentMoveSessionFallsBackAfterRejectedObject, DRAG_FIXTURE )
370{
371 PCB_SHAPE* moved = addSegment( { 0, 0 }, { 10 * MM, 0 } );
372 PCB_SHAPE* locked = addSegment( { 10 * MM, 0 }, { 20 * MM, 0 } );
373 locked->SetLocked( true );
375
376 auto session = std::make_shared<BOARD_CONSTRAINT_MOVE_SESSION>();
377 BOOST_REQUIRE( session->Build( &board, { moved }, { 0, 0 } ) );
378
379 SNAP_SOURCE_CONTEXT context;
380 context.sourcePoint = { 5 * MM, 0 };
381 SNAP_STABLE_ID objectId = testSnapId( "blocked" );
382 SNAP_STABLE_ID gridId = testSnapId( "grid-x", SNAP_ID_KIND::GRID_X );
383 SNAP_RESOLVER resolver;
384 resolver.SetFeasibilityCallback(
385 [session]( const SNAP_SOURCE_CONTEXT& aContext, const std::vector<SNAP_CANDIDATE>& aCandidates )
386 {
387 return session->ResolveCandidates( aContext, aCandidates );
388 } );
391 0.0 ) );
392 resolver.AddCandidate(
394
395 SNAP_RESULT result = resolver.Resolve( context );
397 BOOST_CHECK( !result.Accepted( objectId ) );
398 BOOST_CHECK( result.Accepted( gridId ) );
399 BOOST_CHECK_EQUAL( result.position, VECTOR2I( 0, 0 ) );
400}
401
402
403BOOST_FIXTURE_TEST_CASE( PersistentMoveSessionReportsEachAcceptedSnapOnce, DRAG_FIXTURE )
404{
405 PCB_SHAPE* moved = addSegment( { 0, 0 }, { 10 * MM, 0 } );
406 PCB_SHAPE* neighbor = addSegment( { 10 * MM, 0 }, { 20 * MM, 0 } );
407 addCoincident( moved, CONSTRAINT_ANCHOR::END, neighbor, CONSTRAINT_ANCHOR::START );
408
409 auto session = std::make_shared<BOARD_CONSTRAINT_MOVE_SESSION>();
410 BOOST_REQUIRE( session->Build( &board, { moved }, { 0, 0 } ) );
411
412 SNAP_SOURCE_CONTEXT context;
413 context.sourcePoint = { 5 * MM, 0 };
414 SNAP_STABLE_ID targetId = testSnapId( "target" );
417 target.guides.push_back( { { 0, 0 }, context.sourcePoint } );
418
419 SNAP_RESOLVER resolver;
420 resolver.SetFeasibilityCallback(
421 [session]( const SNAP_SOURCE_CONTEXT& aContext, const std::vector<SNAP_CANDIDATE>& aCandidates )
422 {
423 return session->ResolveCandidates( aContext, aCandidates );
424 } );
425 resolver.AddCandidate( target );
426
427 // The resolver appends the id and the guides itself. A session that reported them too would
428 // enter the snap in the sticky set twice and draw its guide twice every frame.
429 SNAP_RESULT result = resolver.Resolve( context );
431 BOOST_CHECK_EQUAL( std::count( result.accepted.begin(), result.accepted.end(), targetId ), 1 );
432 BOOST_CHECK_EQUAL( result.guides.size(), 1 );
433}
434
435
436BOOST_FIXTURE_TEST_CASE( PersistentMoveSessionJointlySolvesAuthoredAndObjectManifolds, DRAG_FIXTURE )
437{
438 PCB_SHAPE* moved = addSegment( { 0, 0 }, { 10 * MM, 0 } );
439 PCB_SHAPE* guide = addSegment( { -20 * MM, 0 }, { 20 * MM, 0 } );
440 guide->SetLocked( true );
441
443 onLine->AddMember( moved->m_Uuid, CONSTRAINT_ANCHOR::START );
444 onLine->AddMember( guide->m_Uuid, CONSTRAINT_ANCHOR::WHOLE );
445 board.Add( onLine );
446
448 BOOST_REQUIRE( session.Build( &board, { moved }, { 0, 0 } ) );
449
450 SNAP_SOURCE_CONTEXT context;
451 context.sourcePoint = { 5 * MM, 3 * MM };
452 SNAP_CANDIDATE vertical = SNAP_CANDIDATE::Line( testSnapId( "vertical" ), SNAP_PRIORITY_TIER::OBJECT,
454 { 0.0, 40.0 * MM }, 0.0 );
455
456 SNAP_RESULT base = session.ResolveCandidates( context, {} );
458 BOOST_CHECK_EQUAL( base.position, VECTOR2I( 5 * MM, 0 ) );
459
460 SNAP_RESULT result = session.ResolveCandidates( context, { vertical } );
462 BOOST_REQUIRE_EQUAL( result.quantizedResiduals.size(), 1 );
463 BOOST_CHECK_EQUAL( result.quantizedResiduals.front(), 0.0 );
464 BOOST_CHECK_EQUAL( result.remainingDof, 1 );
465 BOOST_CHECK_EQUAL( result.position, VECTOR2I( 5 * MM, 0 ) );
466}
467
468
469BOOST_FIXTURE_TEST_CASE( PersistentMoveSessionJointlySolvesDisconnectedClusters, DRAG_FIXTURE )
470{
471 PCB_SHAPE* first = addSegment( { 0, 0 }, { 10 * MM, 0 } );
472 PCB_SHAPE* firstGuide = addSegment( { -100 * MM, 0 }, { 100 * MM, 0 } );
473 PCB_SHAPE* second = addSegment( { 0, 10 * MM }, { 10 * MM, 10 * MM } );
474 PCB_SHAPE* secondGuide = addSegment( { -100 * MM, 9 * MM }, { 100 * MM, 11 * MM } );
475 firstGuide->SetLocked( true );
476 secondGuide->SetLocked( true );
477
479 firstOnLine->AddMember( first->m_Uuid, CONSTRAINT_ANCHOR::START );
480 firstOnLine->AddMember( firstGuide->m_Uuid, CONSTRAINT_ANCHOR::WHOLE );
481 board.Add( firstOnLine );
482
484 secondOnLine->AddMember( second->m_Uuid, CONSTRAINT_ANCHOR::START );
485 secondOnLine->AddMember( secondGuide->m_Uuid, CONSTRAINT_ANCHOR::WHOLE );
486 board.Add( secondOnLine );
487
489 BOOST_REQUIRE( session.Build( &board, { first, second }, { 0, 0 } ) );
490
491 SNAP_SOURCE_CONTEXT context;
492 context.sourcePoint = { 20 * MM, 5 * MM };
493 SNAP_RESULT result = session.ResolveCandidates( context, {} );
494
496 BOOST_CHECK_EQUAL( result.position, VECTOR2I( 0, 0 ) );
497}
498
499
500BOOST_FIXTURE_TEST_CASE( PersistentDragSessionFindsJointlyFeasibleManifoldPoint, DRAG_FIXTURE )
501{
502 PCB_SHAPE* seg = addSegment( { 0, 0 }, { 10 * MM, 0 } );
503
505 horizontal->AddMember( seg->m_Uuid, CONSTRAINT_ANCHOR::WHOLE );
506 board.Add( horizontal );
507
510
511 SNAP_SOURCE_CONTEXT context;
512 context.sourcePoint = { 6 * MM, 7 * MM };
513 SNAP_CANDIDATE manifold = SNAP_CANDIDATE::Line( testSnapId( "vertical" ), SNAP_PRIORITY_TIER::OBJECT,
515 { 0.0, 40.0 * MM }, 0.0 );
516
517 SNAP_RESULT result = session.ResolveCandidates( context, { manifold } );
518
520 BOOST_CHECK_EQUAL( result.position.x, 6 * MM );
521 BOOST_CHECK_LE( std::abs( result.position.y ), 1000 );
522 BOOST_CHECK_EQUAL( seg->GetEnd(), VECTOR2I( 10 * MM, 0 ) );
523
524 manifold.origin = { -20 * MM, 20 * MM };
525 manifold.direction = { 40.0 * MM, 0.0 };
526 BOOST_CHECK( session.ResolveCandidates( context, { manifold } ).status == SNAP_RESULT_STATUS::INCOMPATIBLE );
527}
528
529
530BOOST_FIXTURE_TEST_CASE( PersistentDragSessionPreservesGeometryOnBaseConflict, DRAG_FIXTURE )
531{
532 PCB_SHAPE* seg = addSegment( { 0, 0 }, { 12 * MM, 0 } );
533
535 length->AddMember( seg->m_Uuid, CONSTRAINT_ANCHOR::WHOLE );
536 length->SetValue( 10.0 * MM );
537 board.Add( length );
538
541
542 seg->SetEnd( { 20 * MM, 0 } );
543 CONSTRAINT_DIAGNOSIS diagnosis = session.Solve( { 20 * MM, 0 }, nullptr, {}, false, false );
544
545 BOOST_CHECK( !diagnosis.solved );
546 BOOST_CHECK_EQUAL( seg->GetEnd(), VECTOR2I( 12 * MM, 0 ) );
547 BOOST_CHECK( !session.IsExactFeasible( { 12 * MM, 0 } ) );
548}
549
550
551// A whole cluster inside the moved selection travels rigidly, so a constraint set it already
552// could not satisfy must not block the move.
553BOOST_FIXTURE_TEST_CASE( WholeClusterMoveSurvivesPreExistingConflict, DRAG_FIXTURE )
554{
555 // Two sides of a closed outline, corner tied, both marked horizontal. Adjacent sides cannot
556 // both be horizontal, which is the state this board arrived in
557 PCB_SHAPE* bottom = addSegment( { 0, 0 }, { 10 * MM, 0 } );
558 PCB_SHAPE* right = addSegment( { 10 * MM, 0 }, { 10 * MM, 10 * MM } );
559
561 corner->AddMember( bottom->m_Uuid, CONSTRAINT_ANCHOR::END );
562 corner->AddMember( right->m_Uuid, CONSTRAINT_ANCHOR::START );
563 board.Add( corner );
564
565 for( PCB_SHAPE* seg : { bottom, right } )
566 {
568 horizontal->AddMember( seg->m_Uuid, CONSTRAINT_ANCHOR::WHOLE );
569 board.Add( horizontal );
570 }
571
573 BOOST_REQUIRE( session.Build( &board, { bottom, right }, { 0, 0 } ) );
574
575 std::vector<PCB_SHAPE*> modified;
576 BOOST_REQUIRE( session.Solve( { 5 * MM, 5 * MM }, &modified, {} ) );
577
578 BOOST_CHECK_EQUAL( bottom->GetStart(), VECTOR2I( 5 * MM, 5 * MM ) );
579 BOOST_CHECK_EQUAL( right->GetEnd(), VECTOR2I( 15 * MM, 15 * MM ) );
580}
581
582
583// A pinned point cannot travel, so a whole-cluster move that would carry it off is still refused
584// and the geometry is left where it was.
585BOOST_FIXTURE_TEST_CASE( WholeClusterMoveWithFixedPoint, DRAG_FIXTURE )
586{
587 PCB_SHAPE* bottom = addSegment( { 0, 0 }, { 10 * MM, 0 } );
588 PCB_SHAPE* right = addSegment( { 10 * MM, 0 }, { 10 * MM, 10 * MM } );
589
591 corner->AddMember( bottom->m_Uuid, CONSTRAINT_ANCHOR::END );
592 corner->AddMember( right->m_Uuid, CONSTRAINT_ANCHOR::START );
593 board.Add( corner );
594
596 pinned->AddMember( bottom->m_Uuid, CONSTRAINT_ANCHOR::START );
597 board.Add( pinned );
598
600 BOOST_REQUIRE( session.Build( &board, { bottom, right }, { 0, 0 } ) );
601
602 std::vector<PCB_SHAPE*> modified;
603
604 BOOST_CHECK( !session.Solve( { 5 * MM, 5 * MM }, &modified, {} ) );
605 BOOST_CHECK_EQUAL( bottom->GetStart(), VECTOR2I( 0, 0 ) );
606 BOOST_CHECK_EQUAL( right->GetEnd(), VECTOR2I( 10 * MM, 10 * MM ) );
607}
608
609
610// A pin grounds its own point and nothing else, so a free corner in the same cluster still drags.
611BOOST_FIXTURE_TEST_CASE( DragFreeCornerBesideFixedPoint, DRAG_FIXTURE )
612{
613 PCB_SHAPE* bottom = addSegment( { 0, 0 }, { 10 * MM, 0 } );
614 PCB_SHAPE* right = addSegment( { 10 * MM, 0 }, { 10 * MM, 10 * MM } );
615
616 addCoincident( bottom, CONSTRAINT_ANCHOR::END, right, CONSTRAINT_ANCHOR::START );
617
619 pinned->AddMember( bottom->m_Uuid, CONSTRAINT_ANCHOR::START );
620 board.Add( pinned );
621
623 BOOST_REQUIRE( session.Build( &board, CONSTRAINT_MEMBER( right->m_Uuid, CONSTRAINT_ANCHOR::START ) ) );
624 BOOST_REQUIRE( !session.HasBaseConflict() );
625
626 right->SetStart( { 12 * MM, 3 * MM } );
627 CONSTRAINT_DIAGNOSIS diagnosis = session.Solve( { 12 * MM, 3 * MM }, nullptr, {}, false, false );
628
629 BOOST_CHECK( diagnosis.solved );
630 BOOST_CHECK( diagnosis.conflicting.empty() );
631 BOOST_CHECK_LE( ( right->GetStart() - VECTOR2I( 12 * MM, 3 * MM ) ).EuclideanNorm(), 1000 );
632 BOOST_CHECK_LE( ( bottom->GetEnd() - right->GetStart() ).EuclideanNorm(), 1000 );
633 BOOST_CHECK_EQUAL( bottom->GetStart(), VECTOR2I( 0, 0 ) );
634}
635
636
637// Standing still is always feasible, so a pinned cluster keeps its snapping instead of reporting
638// itself unusable on every frame.
639BOOST_FIXTURE_TEST_CASE( PinnedClusterStaysSnappableAtRest, DRAG_FIXTURE )
640{
641 PCB_SHAPE* bottom = addSegment( { 0, 0 }, { 10 * MM, 0 } );
642 PCB_SHAPE* right = addSegment( { 10 * MM, 0 }, { 10 * MM, 10 * MM } );
643
644 addCoincident( bottom, CONSTRAINT_ANCHOR::END, right, CONSTRAINT_ANCHOR::START );
645
647 pinned->AddMember( bottom->m_Uuid, CONSTRAINT_ANCHOR::START );
648 board.Add( pinned );
649
651 BOOST_REQUIRE( session.Build( &board, { bottom, right }, { 0, 0 } ) );
652
653 SNAP_SOURCE_CONTEXT context;
654 context.sourcePoint = { 0, 0 };
655 SNAP_RESULT result = session.ResolveCandidates( context, {} );
656
657 BOOST_CHECK( result.status == SNAP_RESULT_STATUS::SUCCESS );
658 BOOST_CHECK_EQUAL( result.position, VECTOR2I( 0, 0 ) );
659}
660
661
662// Moving part of a cluster still works when the pin sits on a shape left behind.
663BOOST_FIXTURE_TEST_CASE( PartialClusterMoveBesideFixedPoint, DRAG_FIXTURE )
664{
665 PCB_SHAPE* bottom = addSegment( { 0, 0 }, { 10 * MM, 0 } );
666 PCB_SHAPE* right = addSegment( { 10 * MM, 0 }, { 10 * MM, 10 * MM } );
667
668 addCoincident( bottom, CONSTRAINT_ANCHOR::END, right, CONSTRAINT_ANCHOR::START );
669
671 pinned->AddMember( bottom->m_Uuid, CONSTRAINT_ANCHOR::START );
672 board.Add( pinned );
673
675 BOOST_REQUIRE( session.Build( &board, { right }, { 10 * MM, 0 } ) );
676
677 std::vector<PCB_SHAPE*> moved;
678 BOOST_REQUIRE( session.Solve( { 13 * MM, 4 * MM }, &moved, {} ) );
679
680 BOOST_CHECK_EQUAL( right->GetStart(), VECTOR2I( 13 * MM, 4 * MM ) );
681 BOOST_CHECK_EQUAL( bottom->GetStart(), VECTOR2I( 0, 0 ) );
682 BOOST_CHECK_LE( ( bottom->GetEnd() - right->GetStart() ).EuclideanNorm(), 1000 );
683}
684
685
686// Dragging one circle's centre re-derives a concentric neighbor (centre-anchor drag on a
687// non-segment shape).
688BOOST_FIXTURE_TEST_CASE( DragCircleCentreMovesConcentricNeighbor, DRAG_FIXTURE )
689{
690 PCB_SHAPE* a = addCircle( { 0, 0 }, 5 * MM );
691 PCB_SHAPE* b = addCircle( { 0, 0 }, 8 * MM );
692
694 c->AddMember( a->m_Uuid, CONSTRAINT_ANCHOR::WHOLE );
695 c->AddMember( b->m_Uuid, CONSTRAINT_ANCHOR::WHOLE );
696 board.Add( c );
697
698 std::vector<PCB_SHAPE*> modified;
699
700 BOARD_COMMIT commit( tool );
701 simulateDrag( commit, &board, a, CONSTRAINT_ANCHOR::CENTER, { 4 * MM, 3 * MM }, &modified );
702
703 // The concentric neighbor's centre followed the dragged centre.
704 BOOST_CHECK_LE( ( a->GetCenter() - b->GetCenter() ).EuclideanNorm(), 2000 );
705 BOOST_CHECK( std::find( modified.begin(), modified.end(), b ) != modified.end() );
706
707 commit.Revert();
708}
709
710
711// When the cluster cannot be solved (here, an unmapped constraint family), neighbors are left
712// untouched -- nothing is half-moved or staged.
713BOOST_FIXTURE_TEST_CASE( UnsolvableClusterLeavesNeighborUntouched, DRAG_FIXTURE )
714{
715 PCB_SHAPE* a = addSegment( { 0, 0 }, { 10 * MM, 0 } );
716 PCB_SHAPE* b = addSegment( { 10 * MM, 0 }, { 10 * MM, 10 * MM } );
717
718 // Concentric needs circles; given two segments the adapter cannot map it, so Build() leaves it
719 // unenforced and the solve is skipped, leaving the neighbor where it was.
721 c->AddMember( a->m_Uuid, CONSTRAINT_ANCHOR::WHOLE );
722 c->AddMember( b->m_Uuid, CONSTRAINT_ANCHOR::WHOLE );
723 board.Add( c );
724
725 const VECTOR2I bStart0 = b->GetStart();
726 const VECTOR2I bEnd0 = b->GetEnd();
727
728 std::vector<PCB_SHAPE*> modified;
729
730 BOARD_COMMIT commit( tool );
731 simulateDrag( commit, &board, a, CONSTRAINT_ANCHOR::END, { 12 * MM, 3 * MM }, &modified );
732
733 BOOST_CHECK( modified.empty() );
734 BOOST_CHECK_EQUAL( b->GetStart(), bStart0 );
735 BOOST_CHECK_EQUAL( b->GetEnd(), bEnd0 );
736
737 commit.Revert();
738}
739
740
741// Deleting a referenced shape leaves the constraint in place (in an error state), it is not
742// cascade-deleted (Zulip "Geometry Constraint Solver", 2026-06-18: deleting an object should put
743// the constraint in an error state, not delete it).
744BOOST_FIXTURE_TEST_CASE( DeleteShapeLeavesConstraintInErrorState, DRAG_FIXTURE )
745{
746 PCB_SHAPE* a = addSegment( { 0, 0 }, { 10 * MM, 0 } );
747 PCB_SHAPE* b = addSegment( { 10 * MM, 0 }, { 10 * MM, 10 * MM } );
748
749 KIID aId = a->m_Uuid;
750
753 c->AddMember( b->m_Uuid, CONSTRAINT_ANCHOR::START );
754 board.Add( c );
755
756 BOOST_REQUIRE_EQUAL( board.Constraints().size(), 1 );
757
758 BOARD_COMMIT commit( tool );
759 commit.Remove( a );
760 commit.Push( wxT( "delete segment" ) );
761
762 // The constraint survives and still holds the now-dangling reference to the deleted shape.
763 BOOST_REQUIRE_EQUAL( board.Constraints().size(), 1 );
764 PCB_CONSTRAINT* survivor = board.Constraints().front();
765 BOOST_REQUIRE_EQUAL( survivor->GetMembers().size(), 2 );
766
767 bool referencesDeleted = false;
768
769 for( const CONSTRAINT_MEMBER& m : survivor->GetMembers() )
770 {
771 if( m.m_item == aId )
772 referencesDeleted = true;
773 }
774
775 BOOST_CHECK( referencesDeleted );
776 BOOST_CHECK( board.ResolveItem( aId, true ) == nullptr ); // the shape is gone
777}
778
779
780// A whole-shape move breaks a point-on-line relation. ReSolveShapeClusters restores it with the
781// moved shape pinned where it was dropped.
782BOOST_FIXTURE_TEST_CASE( MoveReSolvesCluster, DRAG_FIXTURE )
783{
784 PCB_SHAPE* line = addSegment( { 0, 0 }, { 20 * MM, 0 } );
785 PCB_SHAPE* seg = addSegment( { 2 * MM, 5 * MM }, { 5 * MM, 0 } );
786
788 c->AddMember( seg->m_Uuid, CONSTRAINT_ANCHOR::END );
789 c->AddMember( line->m_Uuid, CONSTRAINT_ANCHOR::WHOLE );
790 board.Add( c );
791
794 board.Add( f1 );
795
798 board.Add( f2 );
799
800 seg->Move( { 0, 3 * MM } );
801 BOOST_CHECK_EQUAL( seg->GetEnd().y, 3 * MM );
802
803 std::vector<PCB_SHAPE*> modified;
804 ReSolveShapeClusters( &board, { seg }, &modified );
805
806 // The end is back on the line, the dropped start held, and the fixed line did not move.
807 BOOST_CHECK_LE( std::abs( seg->GetEnd().y ), 5000 );
808 BOOST_CHECK_LE( ( seg->GetStart() - VECTOR2I( 2 * MM, 8 * MM ) ).EuclideanNorm(), 5000.0 );
809 BOOST_CHECK_EQUAL( line->GetStart(), VECTOR2I( 0, 0 ) );
810 BOOST_CHECK_EQUAL( line->GetEnd(), VECTOR2I( 20 * MM, 0 ) );
811 BOOST_CHECK( std::find( modified.begin(), modified.end(), seg ) != modified.end() );
812}
813
814
815// The live move drag stages every neighbor it pulls along into the drag commit, so cancelling the
816// move (localCommit.Revert) must restore both the moved shape and its solved neighbor.
817BOOST_FIXTURE_TEST_CASE( MoveReSolveStagesNeighborsRevertRestores, DRAG_FIXTURE )
818{
819 PCB_SHAPE* a = addSegment( { 0, 0 }, { 10 * MM, 0 } );
820 PCB_SHAPE* b = addSegment( { 10 * MM, 0 }, { 10 * MM, 10 * MM } );
821
822 addCoincident( a, CONSTRAINT_ANCHOR::END, b, CONSTRAINT_ANCHOR::START );
823
824 const VECTOR2I aStart0 = a->GetStart();
825 const VECTOR2I aEnd0 = a->GetEnd();
826 const VECTOR2I bStart0 = b->GetStart();
827
828 // Mirrors drag loop staging moved shape translating it then resolving cluster into same commit
829 // staging each neighbor the solver touches
830 BOARD_COMMIT commit( tool );
831 std::vector<PCB_SHAPE*> modified;
832
833 commit.Modify( a );
834 a->Move( { 0, 4 * MM } );
835
836 ReSolveShapeClusters( &board, { a }, &modified,
837 [&]( BOARD_ITEM* aItem ) { commit.Modify( aItem ); } );
838
839 // The neighbor followed the moved corner.
840 BOOST_CHECK_LE( ( a->GetEnd() - b->GetStart() ).EuclideanNorm(), 100 );
841 BOOST_CHECK( b->GetStart() != bStart0 );
842
843 // Cancelling the move restores every shape the drag commit staged.
844 commit.Revert();
845 BOOST_CHECK_EQUAL( a->GetStart(), aStart0 );
846 BOOST_CHECK_EQUAL( a->GetEnd(), aEnd0 );
847 BOOST_CHECK_EQUAL( b->GetStart(), bStart0 );
848}
849
850
851// Mouse-up pushes the last painted preview without another solve. The pushed transaction must
852// therefore preserve every coordinate that was visible in the final preview frame.
853BOOST_FIXTURE_TEST_CASE( MoveCommitEqualsLastPaintedPreview, DRAG_FIXTURE )
854{
855 PCB_SHAPE* a = addSegment( { 0, 0 }, { 10 * MM, 0 } );
856 PCB_SHAPE* b = addSegment( { 10 * MM, 0 }, { 10 * MM, 10 * MM } );
857
858 addCoincident( a, CONSTRAINT_ANCHOR::END, b, CONSTRAINT_ANCHOR::START );
859
860 BOARD_COMMIT commit( tool );
861 std::vector<PCB_SHAPE*> modified;
862
863 commit.Modify( a );
864 a->Move( { 2 * MM, 4 * MM } );
865
866 ReSolveShapeClusters( &board, { a }, &modified,
867 [&]( BOARD_ITEM* aItem )
868 {
869 commit.Modify( aItem );
870 } );
871
872 const std::array<VECTOR2I, 4> painted = { a->GetStart(), a->GetEnd(), b->GetStart(), b->GetEnd() };
873
874 commit.Push( wxT( "Move" ) );
875
876 const std::array<VECTOR2I, 4> committed = { a->GetStart(), a->GetEnd(), b->GetStart(), b->GetEnd() };
877
878 BOOST_CHECK_EQUAL_COLLECTIONS( committed.begin(), committed.end(), painted.begin(), painted.end() );
879}
880
881
882// The caller-owned, Pack and Duplicate move paths pass no constraint shapes, so the drag collects
883// none and the settle-solve runs over an empty set. That must stage nothing and touch no neighbor,
884// mirroring the guard that keeps those commits free of constraint side effects.
885BOOST_FIXTURE_TEST_CASE( MoveEmptyShapesStagesNoNeighbor, DRAG_FIXTURE )
886{
887 PCB_SHAPE* a = addSegment( { 0, 0 }, { 10 * MM, 0 } );
888 PCB_SHAPE* b = addSegment( { 10 * MM, 0 }, { 10 * MM, 10 * MM } );
889
890 addCoincident( a, CONSTRAINT_ANCHOR::END, b, CONSTRAINT_ANCHOR::START );
891
892 const VECTOR2I bStart0 = b->GetStart();
893
894 BOARD_COMMIT commit( tool );
895 std::vector<PCB_SHAPE*> modified;
896 int staged = 0;
897
898 ReSolveShapeClusters( &board, {}, &modified,
899 [&]( BOARD_ITEM* ) { staged++; } );
900
901 BOOST_CHECK_EQUAL( staged, 0 );
902 BOOST_CHECK( modified.empty() );
903 BOOST_CHECK_EQUAL( b->GetStart(), bStart0 );
904 BOOST_CHECK( commit.Empty() );
905}
906
907
908// A fixed-length segment dragged by one endpoint rotates about the other end, which stays put.
909// Without pinning the far end the solver is free to translate the whole segment (Zulip "Constraint
910// solver", 2026-07-10: "we need to pin the other end of the line when moving, not only the length").
911BOOST_FIXTURE_TEST_CASE( FixedLengthDragPinsFarEnd, DRAG_FIXTURE )
912{
913 PCB_SHAPE* seg = addSegment( { 0, 0 }, { 10 * MM, 0 } );
914
916 len->AddMember( seg->m_Uuid, CONSTRAINT_ANCHOR::WHOLE );
917 len->SetValue( 10.0 * MM );
918 board.Add( len );
919
920 const VECTOR2I start0 = seg->GetStart();
921
922 std::vector<PCB_SHAPE*> modified;
923
924 BOARD_COMMIT commit( tool );
925
926 // Drag END to a 6-8-10 point, so the held 10 mm length lands it exactly on the cursor.
927 simulateDrag( commit, &board, seg, CONSTRAINT_ANCHOR::END, { 6 * MM, 8 * MM }, &modified );
928
929 // The far (start) end held where it was, and the length is unchanged.
930 BOOST_CHECK_LE( ( seg->GetStart() - start0 ).EuclideanNorm(), 5000.0 );
931 BOOST_CHECK_LE( std::abs( ( seg->GetEnd() - seg->GetStart() ).EuclideanNorm() - 10.0 * MM ), 5000.0 );
932
933 // The dragged end reached the cursor, which sat on the length circle.
934 BOOST_CHECK_LE( ( seg->GetEnd() - VECTOR2I( 6 * MM, 8 * MM ) ).EuclideanNorm(), 20000.0 );
935
936 commit.Revert();
937}
938
939
940// A fixed-length segment dragged by one end holds the far end even when the cursor is off the length
941// circle. The far end used to drift to split the pin error between the two ends.
942BOOST_FIXTURE_TEST_CASE( FixedLengthDragOffCircleHoldsFarEnd, DRAG_FIXTURE )
943{
944 PCB_SHAPE* seg = addSegment( { 0, 0 }, { 10 * MM, 0 } );
945
947 len->AddMember( seg->m_Uuid, CONSTRAINT_ANCHOR::WHOLE );
948 len->SetValue( 10.0 * MM );
949 board.Add( len );
950
951 const VECTOR2I start0 = seg->GetStart();
952
953 std::vector<PCB_SHAPE*> modified;
954 BOARD_COMMIT commit( tool );
955
956 // Cursor 20 mm out on +x, off the 10 mm circle. Far end holds, dragged end lands at {10 mm, 0}.
957 simulateDrag( commit, &board, seg, CONSTRAINT_ANCHOR::END, { 20 * MM, 0 }, &modified );
958
959 BOOST_CHECK_LE( ( seg->GetStart() - start0 ).EuclideanNorm(), 5000.0 );
960 BOOST_CHECK_LE( std::abs( ( seg->GetEnd() - seg->GetStart() ).EuclideanNorm() - 10.0 * MM ), 5000.0 );
961 BOOST_CHECK_LE( ( seg->GetEnd() - VECTOR2I( 10 * MM, 0 ) ).EuclideanNorm(), 20000.0 );
962
963 commit.Revert();
964}
965
966
967// Dragging one endpoint of a constrained arc holds the circle (centre + radius) and the far
968// endpoint, so only the dragged endpoint sweeps -- the arc does not drift or balloon.
969BOOST_FIXTURE_TEST_CASE( ArcEndpointDragHoldsCircleAndFarEnd, DRAG_FIXTURE )
970{
971 PCB_SHAPE* arc = addArc( { 10 * MM, 0 }, { 7071068, 7071068 }, { 0, 10 * MM } ); // 90 deg, r 10
972 PCB_SHAPE* seg = addSegment( { 0, 10 * MM }, { 5 * MM, 15 * MM } );
973 addCoincident( arc, CONSTRAINT_ANCHOR::END, seg, CONSTRAINT_ANCHOR::START );
974
975 const VECTOR2I center0 = arc->GetCenter();
976 const int radius0 = arc->GetRadius();
977 const VECTOR2I end0 = arc->GetEnd();
978
979 // Even an off-circle target is projected onto the held circle inside the adapter, so the centre,
980 // radius and far end stay put and only the dragged endpoint sweeps.
981 std::vector<PCB_SHAPE*> modified;
982 BOARD_COMMIT commit( tool );
983 SolveCluster( &board, { arc->m_Uuid, CONSTRAINT_ANCHOR::START }, { 12 * MM, 3 * MM }, &modified,
984 [&]( BOARD_ITEM* aItem ) { commit.Modify( aItem ); } );
985
986 BOOST_CHECK_LE( ( arc->GetCenter() - center0 ).EuclideanNorm(), 20000.0 );
987 BOOST_CHECK_LE( std::abs( arc->GetRadius() - radius0 ), 20000 );
988 BOOST_CHECK_LE( ( arc->GetEnd() - end0 ).EuclideanNorm(), 20000.0 );
989 BOOST_CHECK( ( arc->GetStart() - VECTOR2I( 10 * MM, 0 ) ).EuclideanNorm() > 20000.0 );
990}
991
992
993// Real FIXED_RADIUS on arc overrides temporary radius hold
994// Dragging endpoint keeps driven radius so far end moves off old spot to stay on the now larger circle
995BOOST_FIXTURE_TEST_CASE( ArcEndpointDragYieldsToFixedRadius, DRAG_FIXTURE )
996{
997 PCB_SHAPE* arc = addArc( { 10 * MM, 0 }, { 7071068, 7071068 }, { 0, 10 * MM } ); // r 10
998
1000 r->AddMember( arc->m_Uuid, CONSTRAINT_ANCHOR::WHOLE );
1001 r->SetValue( 12.0 * MM ); // drive the radius larger than the current 10 mm
1002 board.Add( r );
1003
1004 std::vector<PCB_SHAPE*> modified;
1005 BOARD_COMMIT commit( tool );
1006 SolveCluster( &board, { arc->m_Uuid, CONSTRAINT_ANCHOR::START }, { 12 * MM, 0 }, &modified,
1007 [&]( BOARD_ITEM* aItem ) { commit.Modify( aItem ); } );
1008
1009 // The driving radius wins over the temporary hold.
1010 BOOST_CHECK_LE( std::abs( arc->GetRadius() - 12 * MM ), 20000 );
1011}
1012
1013
1014// Dragging a rectangle corner resizes about the diagonally opposite corner, which
1015// pinDraggedShapeRest holds. The rect stores its corners swapped (start is the bottom-right), so a
1016// broken canonical corner-role mapping would drive or hold the wrong corner and fail both checks.
1017BOOST_FIXTURE_TEST_CASE( RectCornerDragHoldsOppositeCorner, DRAG_FIXTURE )
1018{
1019 PCB_SHAPE* rect = addRect( { 10 * MM, 10 * MM }, { 0, 0 } ); // swapped storage
1020 PCB_SHAPE* seg = addSegment( { 10 * MM, 0 }, { 15 * MM, -5 * MM } );
1021
1022 // Tie the dragged corner (canonical index 1, the corner at (10mm, 0)) to a segment end so the
1023 // cluster contains a mappable constraint and the solve runs.
1025 c->AddMember( rect->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 1 );
1026 c->AddMember( seg->m_Uuid, CONSTRAINT_ANCHOR::START );
1027 board.Add( c );
1028
1029 const VECTOR2I cursor( 12 * MM, -2 * MM );
1030
1031 std::vector<PCB_SHAPE*> modified;
1032 BOARD_COMMIT commit( tool );
1033 SolveCluster( &board, { rect->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 1 }, cursor, &modified,
1034 [&]( BOARD_ITEM* aItem ) { commit.Modify( aItem ); } );
1035
1036 VECTOR2I tl( std::min( rect->GetStart().x, rect->GetEnd().x ),
1037 std::min( rect->GetStart().y, rect->GetEnd().y ) );
1038 VECTOR2I br( std::max( rect->GetStart().x, rect->GetEnd().x ),
1039 std::max( rect->GetStart().y, rect->GetEnd().y ) );
1040
1041 // The dragged top-right corner landed on the cursor and pulled the coincident segment along.
1042 BOOST_CHECK_LE( ( VECTOR2I( br.x, tl.y ) - cursor ).EuclideanNorm(), 5000.0 );
1043 BOOST_CHECK_LE( ( seg->GetStart() - cursor ).EuclideanNorm(), 5000.0 );
1044
1045 // The opposite (bottom-left) corner held its position.
1046 BOOST_CHECK_LE( ( VECTOR2I( tl.x, br.y ) - VECTOR2I( 0, 10 * MM ) ).EuclideanNorm(), 5000.0 );
1047}
1048
1049
1050// Dragging one polygon vertex moves only that vertex; pinDraggedShapeRest holds every other vertex
1051// so the rest of the outline does not drift.
1052BOOST_FIXTURE_TEST_CASE( PolyVertexDragHoldsOtherVertices, DRAG_FIXTURE )
1053{
1054 const std::vector<VECTOR2I> points{ { 0, 0 },
1055 { 10 * MM, 0 },
1056 { 13 * MM, 8 * MM },
1057 { 5 * MM, 14 * MM },
1058 { -3 * MM, 8 * MM } };
1059
1060 PCB_SHAPE* poly = addPoly( points );
1061 PCB_SHAPE* seg = addSegment( { 0, 0 }, { -5 * MM, -5 * MM } );
1062
1063 // Tie one vertex to a segment start so the cluster contains a mappable constraint.
1065 c->AddMember( poly->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 0 );
1066 c->AddMember( seg->m_Uuid, CONSTRAINT_ANCHOR::START );
1067 board.Add( c );
1068
1069 const VECTOR2I cursor( 16 * MM, 9 * MM );
1070
1071 std::vector<PCB_SHAPE*> modified;
1072 BOARD_COMMIT commit( tool );
1073 SolveCluster( &board, { poly->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 2 }, cursor, &modified,
1074 [&]( BOARD_ITEM* aItem ) { commit.Modify( aItem ); } );
1075
1076 const SHAPE_LINE_CHAIN& outline = poly->GetPolyShape().COutline( 0 );
1077
1078 BOOST_REQUIRE_EQUAL( outline.PointCount(), 5 );
1079 BOOST_CHECK_LE( ( outline.CPoint( 2 ) - cursor ).EuclideanNorm(), 5000.0 );
1080
1081 for( int i : { 0, 1, 3, 4 } )
1082 BOOST_CHECK_LE( ( outline.CPoint( i ) - points[i] ).EuclideanNorm(), 5000.0 );
1083}
1084
1085
1086// The point-editor bridge maps a dragged rectangle corner handle to its canonical min/max corner,
1087// independent of which diagonal the shape stores; ordinals past the corners map to nothing, so the
1088// centre and radius handles fall through to the authoritative-shape re-solve instead.
1089BOOST_FIXTURE_TEST_CASE( VertexForRectCornerMapsCanonicalIndex, DRAG_FIXTURE )
1090{
1091 PCB_SHAPE* rect = addRect( { 0, 0 }, { 10 * MM, 10 * MM } );
1092 PCB_SHAPE* swapped = addRect( { 10 * MM, 10 * MM }, { 0, 0 } ); // swapped storage
1093
1094 for( PCB_SHAPE* shape : { rect, swapped } )
1095 {
1096 const std::vector<VECTOR2I> corners{ { 0, 0 },
1097 { 10 * MM, 0 },
1098 { 10 * MM, 10 * MM },
1099 { 0, 10 * MM } };
1100
1101 for( size_t i = 0; i < corners.size(); ++i )
1102 {
1103 std::optional<CONSTRAINT_ANCHOR_POINT> vertex = ConstraintShapeVertex( shape, (int) i );
1104
1105 BOOST_REQUIRE( vertex.has_value() );
1106 BOOST_CHECK( vertex->anchor == CONSTRAINT_ANCHOR::VERTEX );
1107 BOOST_CHECK_EQUAL( vertex->index, (int) i );
1108 BOOST_CHECK( vertex->pos == corners[i] );
1109 }
1110 }
1111
1112 BOOST_CHECK( !ConstraintShapeVertex( rect, 4 ) );
1113 BOOST_CHECK( !ConstraintShapeVertex( rect, -1 ) );
1114
1115 // A segment exposes endpoint anchors, not vertices, so an ordinal on it maps to nothing.
1116 PCB_SHAPE* seg = addSegment( { 0, 0 }, { 10 * MM, 0 } );
1117 BOOST_CHECK( !ConstraintShapeVertex( seg, 0 ) );
1118}
1119
1120
1121// The point-editor bridge maps a dragged polygon vertex handle to its outline ordinal; an ordinal
1122// past the outline, or any ordinal on an arc-bearing polygon, maps to nothing.
1123BOOST_FIXTURE_TEST_CASE( VertexForPolyMapsOrdinal, DRAG_FIXTURE )
1124{
1125 const std::vector<VECTOR2I> points{ { 0, 0 },
1126 { 10 * MM, 0 },
1127 { 13 * MM, 8 * MM },
1128 { 5 * MM, 14 * MM },
1129 { -3 * MM, 8 * MM } };
1130
1131 PCB_SHAPE* poly = addPoly( points );
1132
1133 for( size_t i = 0; i < points.size(); ++i )
1134 {
1135 std::optional<CONSTRAINT_ANCHOR_POINT> vertex = ConstraintShapeVertex( poly, (int) i );
1136
1137 BOOST_REQUIRE( vertex.has_value() );
1138 BOOST_CHECK( vertex->anchor == CONSTRAINT_ANCHOR::VERTEX );
1139 BOOST_CHECK_EQUAL( vertex->index, (int) i );
1140 BOOST_CHECK( vertex->pos == points[i] );
1141 }
1142
1143 BOOST_CHECK( !ConstraintShapeVertex( poly, (int) points.size() ) );
1144
1145 PCB_SHAPE* arcPoly = new PCB_SHAPE( &board, SHAPE_T::POLY );
1146
1148 chain.Append( VECTOR2I( 10 * MM, 10 * MM ) );
1149 chain.Append( VECTOR2I( 50 * MM, 10 * MM ) );
1150 chain.Append( SHAPE_ARC( { 50 * MM, 10 * MM }, { 55 * MM, 25 * MM }, { 50 * MM, 40 * MM }, 0 ) );
1151 chain.Append( VECTOR2I( 10 * MM, 40 * MM ) );
1152 chain.SetClosed( true );
1153
1154 arcPoly->GetPolyShape().AddOutline( chain );
1155 board.Add( arcPoly );
1156
1157 BOOST_REQUIRE_GT( arcPoly->GetPolyShape().COutline( 0 ).ArcCount(), 0 );
1158
1159 // The solver never ingests an arc-bearing outline, so its vertices must not map to members.
1160 BOOST_CHECK( !ConstraintShapeVertex( arcPoly, 0 ) );
1161}
1162
1163
1164// Mimics point editor end to end for rectangle corner
1165// Behavior moves corner first bridge maps dragged handle ordinal to canonical corner and solve pulls coincident segment along
1166BOOST_FIXTURE_TEST_CASE( RectCornerDragMapsThenSolves, DRAG_FIXTURE )
1167{
1168 PCB_SHAPE* rect = addRect( { 0, 0 }, { 10 * MM, 10 * MM } );
1169 PCB_SHAPE* seg = addSegment( { 10 * MM, 0 }, { 15 * MM, -5 * MM } );
1170
1172 c->AddMember( rect->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 1 );
1173 c->AddMember( seg->m_Uuid, CONSTRAINT_ANCHOR::START );
1174 board.Add( c );
1175
1176 // Drag the top-right corner as RECTANGLE_POINT_EDIT_BEHAVIOR::UpdateItem would.
1177 const VECTOR2I cursor( 12 * MM, -2 * MM );
1178 rect->SetTop( cursor.y );
1179 rect->SetRight( cursor.x );
1180
1181 std::optional<CONSTRAINT_ANCHOR_POINT> vertex = ConstraintShapeVertex( rect, 1 );
1182
1183 BOOST_REQUIRE( vertex.has_value() );
1184 BOOST_CHECK( vertex->pos == cursor );
1185
1186 std::vector<PCB_SHAPE*> modified;
1187 BOARD_COMMIT commit( tool );
1188 SolveCluster( &board, { rect->m_Uuid, vertex->anchor, vertex->index }, vertex->pos, &modified,
1189 [&]( BOARD_ITEM* aItem ) { commit.Modify( aItem ); } );
1190
1191 BOOST_CHECK_LE( ( seg->GetStart() - cursor ).EuclideanNorm(), 5000.0 );
1192}
1193
1194
1195// PinEditedCorner clamps a corner drag at minimum size so shape holds clamped corner while edit point keeps raw cursor
1196// Mapping by handle ordinal and solving toward post clamp corner keeps neighbor riding its real position
1197BOOST_FIXTURE_TEST_CASE( ClampedRectCornerDragSolvesToClampedCorner, DRAG_FIXTURE )
1198{
1199 PCB_SHAPE* rect = addRect( { 0, 0 }, { 10 * MM, 10 * MM } );
1200 PCB_SHAPE* seg = addSegment( { 10 * MM, 0 }, { 15 * MM, -5 * MM } );
1201
1203 c->AddMember( rect->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 1 );
1204 c->AddMember( seg->m_Uuid, CONSTRAINT_ANCHOR::START );
1205 board.Add( c );
1206
1207 // Drag the top-right corner far past the left edge; the behavior clamps x to the minimum
1208 // width while y follows the cursor, so the shape corner and the raw cursor diverge.
1209 const VECTOR2I rawCursor( -5 * MM, -2 * MM );
1210 const int minWidth = 25400; // 1 mil floor applied by PinEditedCorner
1211 const VECTOR2I clamped( minWidth, rawCursor.y );
1212
1213 rect->SetTop( clamped.y );
1214 rect->SetRight( clamped.x );
1215
1216 std::optional<CONSTRAINT_ANCHOR_POINT> vertex = ConstraintShapeVertex( rect, 1 );
1217
1218 BOOST_REQUIRE( vertex.has_value() );
1219 BOOST_CHECK( vertex->pos == clamped );
1220 BOOST_CHECK( vertex->pos != rawCursor );
1221
1222 std::vector<PCB_SHAPE*> modified;
1223 BOARD_COMMIT commit( tool );
1224 SolveCluster( &board, { rect->m_Uuid, vertex->anchor, vertex->index }, vertex->pos, &modified,
1225 [&]( BOARD_ITEM* aItem ) { commit.Modify( aItem ); } );
1226
1227 BOOST_CHECK_LE( ( seg->GetStart() - clamped ).EuclideanNorm(), 5000.0 );
1228}
1229
1230
1231namespace
1232{
1233VECTOR2I dragRectTopLeft( const PCB_SHAPE* aRect )
1234{
1235 return VECTOR2I( std::min( aRect->GetStart().x, aRect->GetEnd().x ),
1236 std::min( aRect->GetStart().y, aRect->GetEnd().y ) );
1237}
1238
1239
1240VECTOR2I dragRectBotRight( const PCB_SHAPE* aRect )
1241{
1242 return VECTOR2I( std::max( aRect->GetStart().x, aRect->GetEnd().x ),
1243 std::max( aRect->GetStart().y, aRect->GetEnd().y ) );
1244}
1245
1246
1247PCB_CONSTRAINT* addDrivingVertexLength( BOARD& aBoard, PCB_SHAPE* aShape, int aIndexA, int aIndexB,
1248 double aLengthIU )
1249{
1251 c->AddMember( aShape->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, aIndexA );
1252 c->AddMember( aShape->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, aIndexB );
1253 c->SetValue( aLengthIU );
1254 c->SetDriving( true );
1255 aBoard.Add( c );
1256 return c;
1257}
1258} // namespace
1259
1260
1261// Driving width on top side must survive adjacent side drag where side handles used to bypass solver and violate length
1262// Pinning side canonical corner routes it through same drag solve as corner drag so hard length wins and rectangle translates
1263BOOST_FIXTURE_TEST_CASE( RectSideDragEnforcesDrivingWidth, DRAG_FIXTURE )
1264{
1265 PCB_SHAPE* rect = addRect( { 0, 0 }, { 10 * MM, 10 * MM } );
1266
1267 addDrivingVertexLength( board, rect, 0, 1, 10.0 * MM ); // TL-TR width
1268
1269 // Drag the right side 5 mm out, as RECTANGLE_POINT_EDIT_BEHAVIOR::UpdateItem would.
1270 rect->SetRight( 15 * MM );
1271
1272 // Side RECT_RIGHT (1) maps to canonical corner 1 (TR), read back post-move.
1273 std::optional<CONSTRAINT_ANCHOR_POINT> corner = ConstraintShapeVertex( rect, 1 );
1274
1275 BOOST_REQUIRE( corner.has_value() );
1276 BOOST_CHECK( corner->pos == VECTOR2I( 15 * MM, 0 ) );
1277
1278 std::vector<PCB_SHAPE*> modified;
1279 BOARD_COMMIT commit( tool );
1280 SolveCluster( &board, { rect->m_Uuid, corner->anchor, corner->index }, corner->pos, &modified,
1281 [&]( BOARD_ITEM* aItem ) { commit.Modify( aItem ); } );
1282
1283 const VECTOR2I tl = dragRectTopLeft( rect );
1284 const VECTOR2I br = dragRectBotRight( rect );
1285
1286 // The driving width held against the drag and the height stayed untouched.
1287 BOOST_CHECK_LE( std::abs( ( br.x - tl.x ) - 10 * MM ), 20000 );
1288 BOOST_CHECK_LE( std::abs( ( br.y - tl.y ) - 10 * MM ), 20000 );
1289
1290 // Drag was not simply refused rect moved toward cursor strictly between original and requested span
1291 BOOST_CHECK_GT( tl.x, 1 * MM );
1292 BOOST_CHECK_LT( br.x, 14 * MM );
1293
1294 BOOST_TEST_MESSAGE( "side drag under driving width settled TL at " << tl.x << "," << tl.y );
1295
1296 // The four enumerated corners agree with the solved extremes, so the rect stayed axis-aligned.
1297 std::vector<CONSTRAINT_ANCHOR_POINT> corners = ConstraintShapeAnchors( rect );
1298
1299 BOOST_REQUIRE_EQUAL( corners.size(), 4 );
1300 BOOST_CHECK_EQUAL( corners[0].pos, tl );
1301 BOOST_CHECK_EQUAL( corners[2].pos, br );
1302}
1303
1304
1305// Dragging bottom side leaves top side width constraint satisfiable so resize applies exactly
1306// Height follows handle while held top left corner and driven width stay intact
1307BOOST_FIXTURE_TEST_CASE( RectSideDragOffConstrainedAxisResizes, DRAG_FIXTURE )
1308{
1309 PCB_SHAPE* rect = addRect( { 0, 0 }, { 10 * MM, 10 * MM } );
1310
1311 addDrivingVertexLength( board, rect, 0, 1, 10.0 * MM ); // TL-TR width
1312
1313 // Drag the bottom side 5 mm down; side RECT_BOT (2) maps to canonical corner 2 (BR).
1314 rect->SetBottom( 15 * MM );
1315
1316 std::optional<CONSTRAINT_ANCHOR_POINT> corner = ConstraintShapeVertex( rect, 2 );
1317
1318 BOOST_REQUIRE( corner.has_value() );
1319 BOOST_CHECK( corner->pos == VECTOR2I( 10 * MM, 15 * MM ) );
1320
1321 std::vector<PCB_SHAPE*> modified;
1322 BOARD_COMMIT commit( tool );
1323 SolveCluster( &board, { rect->m_Uuid, corner->anchor, corner->index }, corner->pos, &modified,
1324 [&]( BOARD_ITEM* aItem ) { commit.Modify( aItem ); } );
1325
1326 const VECTOR2I tl = dragRectTopLeft( rect );
1327 const VECTOR2I br = dragRectBotRight( rect );
1328
1329 BOOST_CHECK_LE( tl.EuclideanNorm(), 5000.0 );
1330 BOOST_CHECK_LE( std::abs( ( br.x - tl.x ) - 10 * MM ), 5000 );
1331 BOOST_CHECK_LE( std::abs( ( br.y - tl.y ) - 15 * MM ), 5000 );
1332}
1333
1334
1335// A side drag on a rectangle whose own dimensions are unconstrained resizes exactly as the handle
1336// placed it; the coincident neighbor on an unmoved corner stays put.
1337BOOST_FIXTURE_TEST_CASE( UnconstrainedRectSideDragResizesExactly, DRAG_FIXTURE )
1338{
1339 PCB_SHAPE* rect = addRect( { 0, 0 }, { 10 * MM, 10 * MM } );
1340 PCB_SHAPE* seg = addSegment( { 0, 0 }, { -5 * MM, -5 * MM } );
1341
1343 c->AddMember( rect->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 0 );
1344 c->AddMember( seg->m_Uuid, CONSTRAINT_ANCHOR::START );
1345 board.Add( c );
1346
1347 rect->SetRight( 15 * MM );
1348
1349 std::optional<CONSTRAINT_ANCHOR_POINT> corner = ConstraintShapeVertex( rect, 1 );
1350
1351 BOOST_REQUIRE( corner.has_value() );
1352
1353 std::vector<PCB_SHAPE*> modified;
1354 BOARD_COMMIT commit( tool );
1355 SolveCluster( &board, { rect->m_Uuid, corner->anchor, corner->index }, corner->pos, &modified,
1356 [&]( BOARD_ITEM* aItem ) { commit.Modify( aItem ); } );
1357
1358 BOOST_CHECK_LE( dragRectTopLeft( rect ).EuclideanNorm(), 5000.0 );
1359 BOOST_CHECK_LE( ( dragRectBotRight( rect ) - VECTOR2I( 15 * MM, 10 * MM ) ).EuclideanNorm(), 5000.0 );
1360 BOOST_CHECK_LE( seg->GetStart().EuclideanNorm(), 5000.0 );
1361}
1362
1363
1364// Driving length on polygon edge must survive a drag of that same edge
1365// Both endpoints ride co dragged pins that yield to hard length so edge lands at driven length and unbound vertices hold
1366BOOST_FIXTURE_TEST_CASE( PolyEdgeDragEnforcesDrivingLength, DRAG_FIXTURE )
1367{
1368 const std::vector<VECTOR2I> points{ { 0, 0 },
1369 { 10 * MM, 0 },
1370 { 13 * MM, 8 * MM },
1371 { 5 * MM, 14 * MM },
1372 { -3 * MM, 8 * MM } };
1373
1374 PCB_SHAPE* poly = addPoly( points );
1375 const double edgeLen = std::hypot( 3.0, 8.0 ) * MM; // the 1-2 edge's current length
1376
1377 addDrivingVertexLength( board, poly, 1, 2, edgeLen );
1378
1379 // Drag the 1-2 edge so both vertices move and its length would stretch.
1380 std::vector<VECTOR2I> dragged = points;
1381 dragged[1] = { 8 * MM, -2 * MM };
1382 dragged[2] = { 16 * MM, 9 * MM };
1383 poly->SetPolyPoints( dragged );
1384
1385 std::vector<PCB_SHAPE*> modified;
1386 BOARD_COMMIT commit( tool );
1387 SolveCluster( &board, { poly->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 1 }, dragged[1], &modified,
1388 [&]( BOARD_ITEM* aItem ) { commit.Modify( aItem ); },
1389 /* aIncludeDragged */ false, /* aStabilize */ false, {},
1390 std::pair{ CONSTRAINT_MEMBER( poly->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 2 ), dragged[2] } );
1391
1392 const SHAPE_LINE_CHAIN& outline = poly->GetPolyShape().COutline( 0 );
1393
1394 double solvedLen = ( outline.CPoint( 2 ) - outline.CPoint( 1 ) ).EuclideanNorm();
1395
1396 BOOST_CHECK_LE( std::abs( solvedLen - edgeLen ), 20000.0 );
1397
1398 // Drag was not simply refused edge midpoint moved out of solver noise toward requested midpoint
1399 // Length constrains only vertex separation so midpoint is free to follow landing closer than start
1400 const VECTOR2I midBefore = ( points[1] + points[2] ) / 2;
1401 const VECTOR2I midTarget = ( dragged[1] + dragged[2] ) / 2;
1402 const VECTOR2I midSolved = ( outline.CPoint( 1 ) + outline.CPoint( 2 ) ) / 2;
1403
1404 BOOST_CHECK_LT( ( midSolved - midTarget ).EuclideanNorm(), ( midBefore - midTarget ).EuclideanNorm() );
1405 BOOST_CHECK_GT( ( midSolved - midBefore ).EuclideanNorm(), 250000.0 );
1406
1407 for( int i : { 0, 3, 4 } )
1408 BOOST_CHECK_LE( ( outline.CPoint( i ) - points[i] ).EuclideanNorm(), 5000.0 );
1409}
1410
1411
1412// An unconstrained polygon edge drag lands both vertices exactly where the handle placed them; the
1413// vertex-bound segment follows and the unbound vertices hold.
1414BOOST_FIXTURE_TEST_CASE( UnconstrainedPolyEdgeDragMovesBothVertices, DRAG_FIXTURE )
1415{
1416 const std::vector<VECTOR2I> points{ { 0, 0 },
1417 { 10 * MM, 0 },
1418 { 13 * MM, 8 * MM },
1419 { 5 * MM, 14 * MM },
1420 { -3 * MM, 8 * MM } };
1421
1422 PCB_SHAPE* poly = addPoly( points );
1423 PCB_SHAPE* seg = addSegment( points[1], { 25 * MM, -5 * MM } );
1424
1426 c->AddMember( poly->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 1 );
1427 c->AddMember( seg->m_Uuid, CONSTRAINT_ANCHOR::START );
1428 board.Add( c );
1429
1430 // Translate the 1-2 edge 3 mm right, as the edge handle would.
1431 std::vector<VECTOR2I> dragged = points;
1432 dragged[1] += VECTOR2I( 3 * MM, 0 );
1433 dragged[2] += VECTOR2I( 3 * MM, 0 );
1434 poly->SetPolyPoints( dragged );
1435
1436 std::vector<PCB_SHAPE*> modified;
1437 BOARD_COMMIT commit( tool );
1438 SolveCluster( &board, { poly->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 1 }, dragged[1], &modified,
1439 [&]( BOARD_ITEM* aItem ) { commit.Modify( aItem ); },
1440 /* aIncludeDragged */ false, /* aStabilize */ false, {},
1441 std::pair{ CONSTRAINT_MEMBER( poly->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 2 ), dragged[2] } );
1442
1443 const SHAPE_LINE_CHAIN& outline = poly->GetPolyShape().COutline( 0 );
1444
1445 BOOST_CHECK_LE( ( outline.CPoint( 1 ) - dragged[1] ).EuclideanNorm(), 5000.0 );
1446 BOOST_CHECK_LE( ( outline.CPoint( 2 ) - dragged[2] ).EuclideanNorm(), 5000.0 );
1447 BOOST_CHECK_LE( ( seg->GetStart() - dragged[1] ).EuclideanNorm(), 5000.0 );
1448 BOOST_CHECK( std::find( modified.begin(), modified.end(), seg ) != modified.end() );
1449
1450 for( int i : { 0, 3, 4 } )
1451 BOOST_CHECK_LE( ( outline.CPoint( i ) - points[i] ).EuclideanNorm(), 5000.0 );
1452}
1453
1454
1455// Dragging an arc's centre holds both endpoints, so the centre moves and the radius adapts.
1456BOOST_FIXTURE_TEST_CASE( ArcCentreDragHoldsEndpoints, DRAG_FIXTURE )
1457{
1458 PCB_SHAPE* arc = addArc( { 10 * MM, 0 }, { 7071068, 7071068 }, { 0, 10 * MM } );
1459 PCB_SHAPE* seg = addSegment( { 0, 10 * MM }, { 5 * MM, 15 * MM } );
1460 addCoincident( arc, CONSTRAINT_ANCHOR::END, seg, CONSTRAINT_ANCHOR::START );
1461
1462 const VECTOR2I start0 = arc->GetStart();
1463 const VECTOR2I end0 = arc->GetEnd();
1464 const int radius0 = arc->GetRadius();
1465
1466 std::vector<PCB_SHAPE*> modified;
1467 BOARD_COMMIT commit( tool );
1468 SolveCluster( &board, { arc->m_Uuid, CONSTRAINT_ANCHOR::CENTER }, { 1 * MM, 1 * MM }, &modified,
1469 [&]( BOARD_ITEM* aItem )
1470 {
1471 commit.Modify( aItem );
1472 } );
1473
1474 BOOST_CHECK_LE( ( arc->GetStart() - start0 ).EuclideanNorm(), 20000.0 );
1475 BOOST_CHECK_LE( ( arc->GetEnd() - end0 ).EuclideanNorm(), 20000.0 );
1476 BOOST_CHECK( ( arc->GetCenter() - VECTOR2I( 0, 0 ) ).EuclideanNorm() > 100000.0 );
1477 BOOST_CHECK_GT( std::abs( arc->GetRadius() - radius0 ), 100000 );
1478}
1479
1480
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, const std::set< KIID > &aFixedShapes, bool aHoldDraggedRigid)
Gather the cluster of shapes transitively constrained with the dragged shape, solve with the dragged ...
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.
void Push(const wxString &aMessage=wxEmptyString, int aCommitFlags=0) override
Execute the changes.
void Revert() override
Revert the commit by restoring the modified items state.
Translates KiCad board geometry to and from the planegcs solver (issue #2329).
bool SolveRigidTranslation(const std::set< KIID > &aEditedShapes, const VECTOR2I &aTranslation)
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.
std::vector< PCB_SHAPE * > Apply(const std::function< void(BOARD_ITEM *)> &aBeforeWrite={})
Write the solved coordinates back into the shapes, de-normalized to IU.
bool IsExactFeasible(const VECTOR2I &aTarget)
bool Build(BOARD *aBoard, const CONSTRAINT_MEMBER &aDragged)
CONSTRAINT_DIAGNOSIS Solve(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=std::nullopt)
SNAP_RESULT ResolveCandidates(const SNAP_SOURCE_CONTEXT &aContext, const std::vector< SNAP_CANDIDATE > &aCandidates)
bool Matches(const CONSTRAINT_MEMBER &aDragged) const
bool Solve(const VECTOR2I &aTarget, std::vector< PCB_SHAPE * > *aModified, const std::function< void(BOARD_ITEM *)> &aBeforeModify)
bool Build(BOARD *aBoard, const std::vector< PCB_SHAPE * > &aEditedShapes, const VECTOR2I &aReference)
SNAP_RESULT ResolveCandidates(const SNAP_SOURCE_CONTEXT &aContext, const std::vector< SNAP_CANDIDATE > &aCandidates)
A base class for any item which can be embedded within the BOARD container class, and therefore insta...
Definition board_item.h:85
void SetLocked(bool aLocked) override
Definition board_item.h:427
Information pertinent to a Pcbnew printed circuit board.
Definition board.h:411
void Add(BOARD_ITEM *aItem, ADD_MODE aMode=ADD_MODE::INSERT, bool aSkipConnectivity=false) override
Removes an item from the container.
Definition board.cpp:1520
const CONSTRAINTS & Constraints() const
Geometric constraints (#2329) owned by this board.
Definition board.h:522
BOARD_ITEM * ResolveItem(const KIID &aID, bool aAllowNullptrReturn=false) const
Definition board.cpp:2142
COMMIT & Remove(EDA_ITEM *aItem, BASE_SCREEN *aScreen=nullptr)
Remove a new item from the model.
Definition commit.h:86
bool Empty() const
Definition commit.h:145
COMMIT & Modify(EDA_ITEM *aItem, BASE_SCREEN *aScreen=nullptr, RECURSE_MODE aRecurse=RECURSE_MODE::NO_RECURSE)
Modify a given item in the model.
Definition commit.h:102
const KIID m_Uuid
Definition eda_item.h:599
SHAPE_POLY_SET & GetPolyShape()
int GetRadius() const
virtual void SetBottom(int val)
Definition eda_shape.h:370
virtual void SetTop(int val)
Definition eda_shape.h:367
const VECTOR2I & GetEnd() const
Return the ending point of the graphic.
Definition eda_shape.h:333
const VECTOR2I & GetStart() const
Return the starting point of the graphic.
Definition eda_shape.h:283
void SetPolyPoints(const std::vector< VECTOR2I > &aPoints)
virtual void SetRight(int val)
Definition eda_shape.h:369
Definition kiid.h:46
std::array< uint8_t, 16 > AsBytes() const
Definition kiid.cpp:276
static KIID FromName(const std::string &aName)
Return a KIID derived from a name, the same name always gives the same KIID.
Definition kiid.cpp:237
A geometric constraint between board items (issue #2329).
const std::vector< CONSTRAINT_MEMBER > & GetMembers() const
void AddMember(const KIID &aItem, CONSTRAINT_ANCHOR aAnchor=CONSTRAINT_ANCHOR::WHOLE, int aIndex=-1)
void SetValue(std::optional< double > aValue)
void SetDriving(bool aDriving)
void SetCenter(const VECTOR2I &aCenter) override
VECTOR2I GetCenter() const override
This defaults to the center of the bounding box if not overridden.
Definition pcb_shape.h:80
void SetEnd(const VECTOR2I &aEnd) override
void Move(const VECTOR2I &aMoveVector) override
Move this object.
void SetStart(const VECTOR2I &aStart) override
void SetArcGeometry(const VECTOR2I &aStart, const VECTOR2I &aMid, const VECTOR2I &aEnd) override
Set the three controlling points for an arc.
void SetRadius(int aRadius)
void SetCenter(const VECTOR2I &aCenter)
Represent a polyline containing arcs as well as line segments: A chain of connected line and/or arc s...
size_t ArcCount() const
int AddOutline(const SHAPE_LINE_CHAIN &aOutline)
Adds a new outline to the set and returns its index.
const SHAPE_LINE_CHAIN & COutline(int aIndex) const
SNAP_RESULT Resolve(const SNAP_SOURCE_CONTEXT &aContext) const
void SetFeasibilityCallback(FEASIBILITY_CALLBACK aCallback)
void AddCandidate(SNAP_CANDIDATE aCandidate)
std::optional< CONSTRAINT_ANCHOR_POINT > ConstraintShapeVertex(const PCB_SHAPE *aShape, int aIndex)
VERTEX anchor at ordinal aIndex of a rectangle or eligible polygon or std::nullopt if the shape has n...
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...
@ ELLIPSE
Definition eda_shape.h:62
@ SEGMENT
Definition eda_shape.h:56
@ RECTANGLE
Use RECTANGLE instead of RECT to avoid collision in a Windows header.
Definition eda_shape.h:57
PCB_SHAPE * addPoly(BOARD &aBoard, const std::vector< VECTOR2I > &aPoints)
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 * addRect(BOARD &aBoard, const VECTOR2I &aStart, const VECTOR2I &aEnd)
EDA_ANGLE abs(const EDA_ANGLE &aAngle)
Definition eda_angle.h:437
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.
@ FIXED_POSITION
A point is locked at its current location.
@ COINCIDENT
Two points are made to coincide.
@ FIXED_RADIUS
An arc/circle has a driving radius value.
@ HORIZONTAL
A segment (or two points) is horizontal.
@ POINT_ON_LINE
A point lies on a segment's supporting line.
@ FIXED_LENGTH
A segment has a driving length value.
static bool addSegment(VRML_LAYER &model, IDF_SEGMENT *seg, int icont, int iseg)
SNAP_ID_KIND
std::array< uint8_t, 16 > SNAP_TARGET_ID
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.
One participant in a constraint: a referenced board item plus the feature of that item that participa...
static SNAP_CANDIDATE Point(SNAP_STABLE_ID aId, SNAP_PRIORITY_TIER aPriority, SNAP_CANDIDATE_SUBTYPE aSubtype, const VECTOR2I &aPoint, double aResidual)
static SNAP_CANDIDATE Line(SNAP_STABLE_ID aId, SNAP_PRIORITY_TIER aPriority, SNAP_CANDIDATE_SUBTYPE aSubtype, const VECTOR2I &aOrigin, const VECTOR2D &aDirection, double aResidual)
static SNAP_CANDIDATE AxisX(SNAP_STABLE_ID aId, SNAP_PRIORITY_TIER aPriority, SNAP_CANDIDATE_SUBTYPE aSubtype, int aCoordinate, double aResidual)
std::vector< SNAP_GUIDE > guides
SNAP_RELATION relation
SNAP_RESULT_STATUS status
BOOST_AUTO_TEST_SUITE(CadstarPartParser)
BOOST_FIXTURE_TEST_CASE(DragReDerivesNeighborRevertRestores, DRAG_FIXTURE)
BOOST_AUTO_TEST_CASE(UnbuiltMoveSessionRejectsCandidates)
BOOST_CHECK_EQUAL_COLLECTIONS(mixed.begin(), mixed.end(), expMixed.begin(), expMixed.end())
BOOST_REQUIRE(intersection.has_value()==c.ExpectedIntersection.has_value())
BOOST_AUTO_TEST_SUITE_END()
bool moved
static const long long MM
const SHAPE_LINE_CHAIN chain
SHAPE_CIRCLE circle(c.m_circle_center, c.m_circle_radius)
BOOST_TEST_MESSAGE("Polyline has "<< chain.PointCount()<< " points")
wxString result
Test unit parsing edge cases and error handling.
BOOST_CHECK_EQUAL(result, "25.4")
VECTOR2< int32_t > VECTOR2I
Definition vector2d.h:708