KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_pns_basics.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
22#include <optional>
23
24#include <pcbnew/board.h>
25#include <pcbnew/pad.h>
26#include <pcbnew/pcb_track.h>
28
30#include <geometry/shape_arc.h>
31#include <geometry/eda_angle.h>
33#include <router/pns_dragger.h>
35#include <router/pns_arc.h>
36#include <router/pns_line.h>
37#include <router/pns_item.h>
39#include <router/pns_node.h>
40#include <router/pns_router.h>
41#include <router/pns_segment.h>
42#include <router/pns_shove.h>
44#include <router/pns_solid.h>
45#include <router/pns_topology.h>
46#include <router/pns_via.h>
47
48static bool isCopper( const PNS::ITEM* aItem )
49{
50 if( !aItem )
51 return false;
52
53 BOARD_ITEM* parent = aItem->Parent();
54
55 if( parent && parent->Type() == PCB_PAD_T )
56 {
57 PAD* pad = static_cast<PAD*>( parent );
58
59 if( pad->IsAperturePad() || pad->IsNPTHWithNoCopper() )
60 return false;
61 }
62
63 return true;
64}
65
66
67static bool isHole( const PNS::ITEM* aItem )
68{
69 if( !aItem )
70 return false;
71
72 return aItem->OfKind( PNS::ITEM::HOLE_T );
73}
74
75
76static bool isEdge( const PNS::ITEM* aItem )
77{
78 if( !aItem )
79 return false;
80
81 const BOARD_ITEM *parent = aItem->BoardItem();
82
83 return parent && ( parent->IsOnLayer( Edge_Cuts ) || parent->IsOnLayer( Margin ) );
84}
85
86
88{
89public:
93
95
96 virtual int Clearance( const PNS::ITEM* aA, const PNS::ITEM* aB,
97 bool aUseClearanceEpsilon = true ) override
98 {
99 PNS::CONSTRAINT constraint;
100 int rv = 0;
101 PNS_LAYER_RANGE layers;
102
103 if( !aB )
104 layers = aA->Layers();
105 else if( isEdge( aA ) )
106 layers = aB->Layers();
107 else if( isEdge( aB ) )
108 layers = aA->Layers();
109 else
110 layers = aA->Layers().Intersection( aB->Layers() );
111
112 // Normalize layer range (no -1 magic numbers)
114
115 // electrical clearances are net-aware; physical clearances are net-blind; same-net or
116 // free-pad pairs with no positive physical rule fall back to -1.
117 const bool sameNet = aA && aB && aA->Net() && aA->Net() == aB->Net();
118 const bool freePad = aA && aB && ( aA->IsFreePad() || aB->IsFreePad() );
119
120 for( int layer = layers.Start(); layer <= layers.End(); ++layer )
121 {
122 if( !sameNet && !freePad )
123 {
124 if( isHole( aA ) && isHole( aB ) )
125 {
126 if( QueryConstraint( PNS::CONSTRAINT_TYPE::CT_HOLE_TO_HOLE, aA, aB, layer, &constraint ) )
127 {
128 if( constraint.m_Value.Min() > rv )
129 rv = constraint.m_Value.Min();
130 }
131 }
132 else if( isHole( aA ) || isHole( aB ) )
133 {
134 if( QueryConstraint( PNS::CONSTRAINT_TYPE::CT_HOLE_CLEARANCE, aA, aB, layer, &constraint ) )
135 {
136 if( constraint.m_Value.Min() > rv )
137 rv = constraint.m_Value.Min();
138 }
139 }
140 else if( isCopper( aA ) && ( !aB || isCopper( aB ) ) )
141 {
142 if( QueryConstraint( PNS::CONSTRAINT_TYPE::CT_CLEARANCE, aA, aB, layer, &constraint ) )
143 {
144 if( constraint.m_Value.Min() > rv )
145 rv = constraint.m_Value.Min();
146 }
147 }
148 else if( isEdge( aA ) || ( aB && isEdge( aB ) ) )
149 {
150 if( QueryConstraint( PNS::CONSTRAINT_TYPE::CT_EDGE_CLEARANCE, aA, aB, layer, &constraint ) )
151 {
152 if( constraint.m_Value.Min() > rv )
153 rv = constraint.m_Value.Min();
154 }
155 }
156 }
157
158 if( isHole( aA ) || isHole( aB ) )
159 {
160 if( QueryConstraint( PNS::CONSTRAINT_TYPE::CT_PHYSICAL_HOLE_CLEARANCE, aA, aB, layer, &constraint ) )
161 {
162 if( constraint.m_Value.Min() > rv )
163 rv = constraint.m_Value.Min();
164 }
165 }
166
167 if( QueryConstraint( PNS::CONSTRAINT_TYPE::CT_PHYSICAL_CLEARANCE, aA, aB, layer, &constraint ) )
168 {
169 if( constraint.m_Value.Min() > rv )
170 rv = constraint.m_Value.Min();
171 }
172 }
173
174 if( ( sameNet || freePad ) && rv == 0 )
175 rv = -1;
176
177 return rv;
178 }
179
181
182 virtual PNS::NET_HANDLE DpCoupledNet( PNS::NET_HANDLE aNet ) override { return nullptr; }
183 virtual int DpNetPolarity( PNS::NET_HANDLE aNet ) override { return -1; }
184
185 virtual bool DpNetPair( const PNS::ITEM* aItem, PNS::NET_HANDLE& aNetP,
186 PNS::NET_HANDLE& aNetN ) override
187 {
188 return false;
189 }
190
191 virtual int NetCode( PNS::NET_HANDLE aNet ) override
192 {
193 return -1;
194 }
195
196 virtual wxString NetName( PNS::NET_HANDLE aNet ) override
197 {
198 return wxEmptyString;
199 }
200
201 virtual bool QueryConstraint( PNS::CONSTRAINT_TYPE aType, const PNS::ITEM* aItemA,
202 const PNS::ITEM* aItemB, int aLayer,
203 PNS::CONSTRAINT* aConstraint ) override
204 {
205 ITEM_KEY key;
206
207 key.a = aItemA;
208 key.b = aItemB;
209 key.type = aType;
210
211 auto it = m_ruleMap.find( key );
212
213 if( it == m_ruleMap.end() )
214 {
215 int cl;
216 switch( aType )
217 {
223 return false;
225 break;
228 return false;
230 break;
231 default: return false;
232 }
233
234 //printf("GetDef %s %s %d cl %d\n", aItemA->KindStr().c_str(), aItemB->KindStr().c_str(), aType, cl );
235
236 aConstraint->m_Type = aType;
237 aConstraint->m_Value.SetMin( cl );
238
239 return true;
240 }
241 else
242 {
243 *aConstraint = it->second;
244 }
245
246 return true;
247 }
248
249 int ClearanceEpsilon() const override { return m_clearanceEpsilon; }
250
251 struct ITEM_KEY
252 {
253 const PNS::ITEM* a = nullptr;
254 const PNS::ITEM* b = nullptr;
256
257 bool operator==( const ITEM_KEY& other ) const
258 {
259 return a == other.a && b == other.b && type == other.type;
260 }
261
262 bool operator<( const ITEM_KEY& other ) const
263 {
264 if( a < other.a )
265 {
266 return true;
267 }
268 else if ( a == other.a )
269 {
270 if( b < other.b )
271 return true;
272 else if ( b == other.b )
273 return type < other.type;
274 }
275
276 return false;
277 }
278 };
279
280 bool IsInNetTie( const PNS::ITEM* aA ) override { return false; }
281
282 bool IsNetTieExclusion( const PNS::ITEM* aItem, const VECTOR2I& aCollisionPos,
283 const PNS::ITEM* aCollidingItem ) override
284 {
285 return false;
286 }
287
288 bool IsDrilledHole( const PNS::ITEM* aItem ) override { return false; }
289
290 bool IsNonPlatedSlot( const PNS::ITEM* aItem ) override { return false; }
291
292 bool IsKeepout( const PNS::ITEM* aObstacle, const PNS::ITEM* aItem, bool* aEnforce ) override
293 {
294 return false;
295 }
296
297 void AddMockRule( PNS::CONSTRAINT_TYPE aType, const PNS::ITEM* aItemA, const PNS::ITEM* aItemB,
298 PNS::CONSTRAINT& aConstraint )
299 {
300 ITEM_KEY key;
301
302 key.a = aItemA;
303 key.b = aItemB;
304 key.type = aType;
305
306 m_ruleMap[key] = aConstraint;
307 }
308
309 int m_defaultClearance = 200000;
310 int m_defaultHole2Hole = 220000;
312 int m_defaultPhysicalClearance = 0; // 0 means "rule does not match this pair"
313 int m_defaultPhysicalHoleClearance = 0; // 0 means "rule does not match this pair"
315
316private:
317 std::map<ITEM_KEY, PNS::CONSTRAINT> m_ruleMap;
319};
320
321struct PNS_TEST_FIXTURE;
322
324{
325public:
327 m_testFixture( aFixture )
328 {}
329
331
332 void HideItem( PNS::ITEM* aItem ) override {};
333 void DisplayItem( const PNS::ITEM* aItem, int aClearance, bool aEdit = false,
334 int aFlags = 0 ) override {};
336
337 bool TestInheritTrackWidth( PNS::ITEM* aItem, int* aInheritedWidth,
338 const VECTOR2I& aStartPosition = VECTOR2I() )
339 {
340 m_startLayer = aItem->Layer();
341 return inheritTrackWidth( aItem, aInheritedWidth, aStartPosition );
342 }
343
344private:
346};
347
348
350{
352 {
353 m_router = new PNS::ROUTER;
354 m_iface = new MOCK_PNS_KICAD_IFACE( this );
355 m_router->SetInterface( m_iface );
356 }
357
359 {
360 delete m_router;
361 delete m_iface;
362 }
363
368 //std::unique_ptr<BOARD> m_board;
369};
370
371
376
377
378BOOST_FIXTURE_TEST_CASE( PNSShoveOwnsRootLineHistory, PNS_TEST_FIXTURE )
379{
380 PNS::NODE world;
381 world.SetRuleResolver( &m_ruleResolver );
382
383 PNS::SEGMENT segment( SEG( VECTOR2I( 0, 0 ), VECTOR2I( 1000000, 0 ) ),
384 reinterpret_cast<PNS::NET_HANDLE>( 1 ) );
385 segment.SetLayers( PNS_LAYER_RANGE( F_Cu ) );
386
387 PNS::LINE line;
388 line.Line().Append( VECTOR2I( 0, 0 ) );
389 line.Line().Append( VECTOR2I( 1000000, 0 ) );
390 line.SetLayers( PNS_LAYER_RANGE( F_Cu ) );
391
392 PNS::SHOVE shove( &world, m_router );
393 shove.SetShovePolicy( &segment, PNS::SHOVE::SHP_SHOVE );
395}
396
397static void dumpObstacles( const PNS::NODE::OBSTACLES &obstacles )
398{
399 for( const PNS::OBSTACLE& obs : obstacles )
400 {
401 BOOST_TEST_MESSAGE( wxString::Format( "%p [%s] - %p [%s], clearance %d",
402 obs.m_head, obs.m_head->KindStr().c_str(),
403 obs.m_item, obs.m_item->KindStr().c_str(),
404 obs.m_clearance ) );
405 }
406}
407
409{
410 PNS::VIA* v1 = new PNS::VIA( VECTOR2I( 0, 1000000 ), PNS_LAYER_RANGE( F_Cu, B_Cu ), 50000, 10000 );
411 PNS::VIA* v2 = new PNS::VIA( VECTOR2I( 0, 2000000 ), PNS_LAYER_RANGE( F_Cu, B_Cu ), 50000, 10000 );
412
413 std::unique_ptr<PNS::NODE> world ( new PNS::NODE );
414
415 v1->SetNet( (PNS::NET_HANDLE) 1 );
416 v2->SetNet( (PNS::NET_HANDLE) 2 );
417
418 world->SetMaxClearance( 10000000 );
419 world->SetRuleResolver( &m_ruleResolver );
420
421 world->AddRaw( v1 );
422 world->AddRaw( v2 );
423
424 BOOST_TEST_MESSAGE( "via to via, no violations" );
425 {
426 PNS::NODE::OBSTACLES obstacles;
427 int count = world->QueryColliding( v1, obstacles );
428 dumpObstacles( obstacles );
429 BOOST_CHECK_EQUAL( obstacles.size(), 0 );
430 BOOST_CHECK_EQUAL( count, 0 );
431 }
432
433 BOOST_TEST_MESSAGE( "via to via, forced copper to copper violation" );
434 {
435 PNS::NODE::OBSTACLES obstacles;
436 m_ruleResolver.m_defaultClearance = 1000000;
437 world->QueryColliding( v1, obstacles );
438 dumpObstacles( obstacles );
439
440 BOOST_CHECK_EQUAL( obstacles.size(), 1 );
441 const auto& first = *obstacles.begin();
442
443 BOOST_CHECK_EQUAL( first.m_head, v1 );
444 BOOST_CHECK_EQUAL( first.m_item, v2 );
445 BOOST_CHECK_EQUAL( first.m_clearance, m_ruleResolver.m_defaultClearance );
446 }
447
448 BOOST_TEST_MESSAGE( "via to via, forced hole to hole violation" );
449 {
450 PNS::NODE::OBSTACLES obstacles;
451 m_ruleResolver.m_defaultClearance = 200000;
452 m_ruleResolver.m_defaultHole2Hole = 1000000;
453
454 world->QueryColliding( v1, obstacles );
455 dumpObstacles( obstacles );
456
457 BOOST_CHECK_EQUAL( obstacles.size(), 1 );
458 auto iter = obstacles.begin();
459 const auto& first = *iter++;
460
461 BOOST_CHECK_EQUAL( first.m_head, v1->Hole() );
462 BOOST_CHECK_EQUAL( first.m_item, v2->Hole() );
463 BOOST_CHECK_EQUAL( first.m_clearance, m_ruleResolver.m_defaultHole2Hole );
464 }
465
466 BOOST_TEST_MESSAGE( "via to via, forced copper to hole violation" );
467 {
468 PNS::NODE::OBSTACLES obstacles;
469 m_ruleResolver.m_defaultHole2Hole = 220000;
470 m_ruleResolver.m_defaultHole2Copper = 1000000;
471
472 world->QueryColliding( v1, obstacles );
473 dumpObstacles( obstacles );
474
475 BOOST_CHECK_EQUAL( obstacles.size(), 2 );
476 auto iter = obstacles.begin();
477 const auto& first = *iter++;
478
479 // There is no guarantee on what order the two collisions will be in...
480 BOOST_CHECK( ( first.m_head == v1 && first.m_item == v2->Hole() )
481 || ( first.m_head == v1->Hole() && first.m_item == v2 ) );
482
483 BOOST_CHECK_EQUAL( first.m_clearance, m_ruleResolver.m_defaultHole2Copper );
484 }
485}
486
487
488BOOST_FIXTURE_TEST_CASE( PNSViaBackdrillRetention, PNS_TEST_FIXTURE )
489{
490 PNS::VIA via( VECTOR2I( 1000, 2000 ), PNS_LAYER_RANGE( F_Cu, B_Cu ), 40000, 20000, nullptr,
492 via.SetHoleLayers( PNS_LAYER_RANGE( F_Cu, In2_Cu ) );
493 via.SetHolePostMachining( std::optional<PAD_DRILL_POST_MACHINING_MODE>( PAD_DRILL_POST_MACHINING_MODE::COUNTERSINK ) );
494 via.SetSecondaryDrill( std::optional<int>( 12000 ) );
495 via.SetSecondaryHoleLayers( std::optional<PNS_LAYER_RANGE>( PNS_LAYER_RANGE( F_Cu, In1_Cu ) ) );
496 via.SetSecondaryHolePostMachining( std::optional<PAD_DRILL_POST_MACHINING_MODE>( PAD_DRILL_POST_MACHINING_MODE::NOT_POST_MACHINED ) );
497
498 PNS::VIA viaCopy( via );
499 std::unique_ptr<PNS::VIA> viaClone( via.Clone() );
500
501 auto checkVia = [&]( const PNS::VIA& candidate )
502 {
503 BOOST_CHECK_EQUAL( candidate.HoleLayers().Start(), via.HoleLayers().Start() );
504 BOOST_CHECK_EQUAL( candidate.HoleLayers().End(), via.HoleLayers().End() );
505 BOOST_CHECK( candidate.HolePostMachining().has_value() );
506 BOOST_CHECK( candidate.HolePostMachining().value() == PAD_DRILL_POST_MACHINING_MODE::COUNTERSINK );
507 BOOST_CHECK( candidate.SecondaryDrill().has_value() );
508 BOOST_CHECK_EQUAL( candidate.SecondaryDrill().value(), via.SecondaryDrill().value() );
509 BOOST_CHECK( candidate.SecondaryHoleLayers().has_value() );
510 BOOST_CHECK_EQUAL( candidate.SecondaryHoleLayers()->Start(),
511 via.SecondaryHoleLayers()->Start() );
512 BOOST_CHECK_EQUAL( candidate.SecondaryHoleLayers()->End(),
513 via.SecondaryHoleLayers()->End() );
514 BOOST_CHECK( candidate.SecondaryHolePostMachining().has_value() );
515
516 // run this BOOST_CHECK only if possible to avoid crash
517 if( candidate.SecondaryHolePostMachining().has_value() )
518 BOOST_CHECK( candidate.SecondaryHolePostMachining().value() == via.SecondaryHolePostMachining().value() );
519 };
520
521 checkVia( viaCopy );
522 checkVia( *viaClone );
523}
524
525
526BOOST_AUTO_TEST_CASE( PCBViaBackdrillCloneRetainsData )
527{
528 BOARD board;
529 PCB_VIA via( &board );
530
531 via.SetPrimaryDrillStartLayer( F_Cu );
532 via.SetPrimaryDrillEndLayer( B_Cu );
533 via.SetFrontPostMachining( std::optional<PAD_DRILL_POST_MACHINING_MODE>( PAD_DRILL_POST_MACHINING_MODE::COUNTERSINK ) );
534 via.SetSecondaryDrillSize( std::optional<int>( 15000 ) );
535 via.SetSecondaryDrillStartLayer( F_Cu );
536 via.SetSecondaryDrillEndLayer( In2_Cu );
537
538 via.SetBackPostMachining( std::optional<PAD_DRILL_POST_MACHINING_MODE>( PAD_DRILL_POST_MACHINING_MODE::COUNTERBORE ) );
539 via.SetTertiaryDrillSize( std::optional<int>( 8000 ) );
540 via.SetTertiaryDrillStartLayer( B_Cu );
541 via.SetTertiaryDrillEndLayer( In4_Cu );
542
543 PCB_VIA viaCopy( via );
544 std::unique_ptr<PCB_VIA> viaClone( static_cast<PCB_VIA*>( via.Clone() ) );
545
546 auto checkVia = [&]( const PCB_VIA& candidate )
547 {
548 BOOST_CHECK_EQUAL( candidate.GetPrimaryDrillStartLayer(), via.GetPrimaryDrillStartLayer() );
549 BOOST_CHECK_EQUAL( candidate.GetPrimaryDrillEndLayer(), via.GetPrimaryDrillEndLayer() );
550 BOOST_CHECK( candidate.GetFrontPostMachining().has_value() );
551 BOOST_CHECK_EQUAL( static_cast<int>( candidate.GetFrontPostMachining().value() ),
552 static_cast<int>( via.GetFrontPostMachining().value() ) );
553 BOOST_CHECK( candidate.GetSecondaryDrillSize().has_value() );
554 BOOST_CHECK_EQUAL( candidate.GetSecondaryDrillSize().value(),
555 via.GetSecondaryDrillSize().value() );
556 BOOST_CHECK_EQUAL( candidate.GetSecondaryDrillStartLayer(),
557 via.GetSecondaryDrillStartLayer() );
558 BOOST_CHECK_EQUAL( candidate.GetSecondaryDrillEndLayer(),
559 via.GetSecondaryDrillEndLayer() );
560
561 BOOST_CHECK( candidate.GetBackPostMachining().has_value() );
562 BOOST_CHECK_EQUAL( static_cast<int>( candidate.GetBackPostMachining().value() ),
563 static_cast<int>( via.GetBackPostMachining().value() ) );
564 BOOST_CHECK( candidate.GetTertiaryDrillSize().has_value() );
565 BOOST_CHECK_EQUAL( candidate.GetTertiaryDrillSize().value(),
566 via.GetTertiaryDrillSize().value() );
567 BOOST_CHECK_EQUAL( candidate.GetTertiaryDrillStartLayer(),
568 via.GetTertiaryDrillStartLayer() );
569 BOOST_CHECK_EQUAL( candidate.GetTertiaryDrillEndLayer(),
570 via.GetTertiaryDrillEndLayer() );
571 };
572
573 checkVia( viaCopy );
574 checkVia( *viaClone );
575}
576
577
586BOOST_AUTO_TEST_CASE( PNSLayerRangeSwapBehavior )
587{
588 // On a 2-layer board with FRONT_INNER_BACK mode, BoardCopperLayerCount() returns 2.
589 // The code would calculate PNS_LAYER_RANGE(1, 2 - 2) = PNS_LAYER_RANGE(1, 0)
590 // Since start > end, the constructor swaps them to (0, 1), which would span
591 // both F_Cu and B_Cu incorrectly.
592
593 PNS_LAYER_RANGE innerLayersRange2Layer( 1, 0 ); // What would happen on 2-layer board
594
595 // Verify the swap behavior that causes the bug
596 BOOST_CHECK_EQUAL( innerLayersRange2Layer.Start(), 0 );
597 BOOST_CHECK_EQUAL( innerLayersRange2Layer.End(), 1 );
598 BOOST_CHECK( innerLayersRange2Layer.Overlaps( 0 ) ); // F_Cu
599 BOOST_CHECK( innerLayersRange2Layer.Overlaps( 1 ) ); // B_Cu
600
601 // On a 4-layer board, inner layers are 1 and 2, so PNS_LAYER_RANGE(1, 4-2) = (1, 2)
602 PNS_LAYER_RANGE innerLayersRange4Layer( 1, 2 ); // Correct for 4-layer board
603
604 BOOST_CHECK_EQUAL( innerLayersRange4Layer.Start(), 1 );
605 BOOST_CHECK_EQUAL( innerLayersRange4Layer.End(), 2 );
606 BOOST_CHECK( !innerLayersRange4Layer.Overlaps( 0 ) ); // F_Cu - should not overlap
607 BOOST_CHECK( innerLayersRange4Layer.Overlaps( 1 ) ); // In1_Cu
608 BOOST_CHECK( innerLayersRange4Layer.Overlaps( 2 ) ); // In2_Cu
609 BOOST_CHECK( !innerLayersRange4Layer.Overlaps( 3 ) ); // B_Cu - should not overlap
610}
611
612
620BOOST_FIXTURE_TEST_CASE( PNSSegmentSplitPreservesLockedState, PNS_TEST_FIXTURE )
621{
622 std::unique_ptr<PNS::NODE> world( new PNS::NODE );
623 world->SetMaxClearance( 10000000 );
624 world->SetRuleResolver( &m_ruleResolver );
625
627
628 VECTOR2I segStart( 0, 0 );
629 VECTOR2I segEnd( 10000000, 0 );
630 VECTOR2I splitPt( 5000000, 0 );
631
632 PNS::SEGMENT* lockedSeg = new PNS::SEGMENT( SEG( segStart, segEnd ), net );
633 lockedSeg->SetWidth( 250000 );
634 lockedSeg->SetLayers( PNS_LAYER_RANGE( F_Cu ) );
635 lockedSeg->Mark( PNS::MK_LOCKED );
636
637 BOOST_CHECK( lockedSeg->IsLocked() );
638
639 world->AddRaw( lockedSeg );
640
641 // Clone the locked segment and set up two halves (simulating SplitAdjacentSegments)
642 std::unique_ptr<PNS::SEGMENT> clone1( PNS::Clone( *lockedSeg ) );
643 std::unique_ptr<PNS::SEGMENT> clone2( PNS::Clone( *lockedSeg ) );
644
645 clone1->SetEnds( segStart, splitPt );
646 clone2->SetEnds( splitPt, segEnd );
647
648 BOOST_CHECK_MESSAGE( clone1->IsLocked(),
649 "First half of split locked segment must retain locked state" );
650 BOOST_CHECK_MESSAGE( clone2->IsLocked(),
651 "Second half of split locked segment must retain locked state" );
652 BOOST_CHECK_EQUAL( clone1->Width(), lockedSeg->Width() );
653 BOOST_CHECK_EQUAL( clone2->Width(), lockedSeg->Width() );
654}
655
656
664BOOST_FIXTURE_TEST_CASE( PNSInheritTrackWidthCursorProximity, PNS_TEST_FIXTURE )
665{
666 std::unique_ptr<PNS::NODE> world( new PNS::NODE );
667 world->SetMaxClearance( 10000000 );
668 world->SetRuleResolver( &m_ruleResolver );
669
670 VECTOR2I padPos( 0, 0 );
672
673 // Pad at origin with a small circular shape
675 pad->SetShape( new SHAPE_CIRCLE( padPos, 500000 ) );
676 pad->SetPos( padPos );
677 pad->SetLayers( PNS_LAYER_RANGE( F_Cu ) );
678 pad->SetNet( net );
679 world->AddRaw( pad );
680
681 // Narrow track going right (250um width)
682 int narrowWidth = 250000;
683 PNS::SEGMENT* narrowSeg = new PNS::SEGMENT( SEG( padPos, VECTOR2I( 5000000, 0 ) ), net );
684 narrowSeg->SetWidth( narrowWidth );
685 narrowSeg->SetLayers( PNS_LAYER_RANGE( F_Cu ) );
686 world->AddRaw( narrowSeg );
687
688 // Wide track going up (500um width)
689 int wideWidth = 500000;
690 PNS::SEGMENT* wideSeg = new PNS::SEGMENT( SEG( padPos, VECTOR2I( 0, -5000000 ) ), net );
691 wideSeg->SetWidth( wideWidth );
692 wideSeg->SetLayers( PNS_LAYER_RANGE( F_Cu ) );
693 world->AddRaw( wideSeg );
694
695 int inherited = 0;
696
697 // Without cursor position, should fall back to minimum width
698 BOOST_CHECK( m_iface->TestInheritTrackWidth( pad, &inherited ) );
699 BOOST_CHECK_EQUAL( inherited, narrowWidth );
700
701 // Cursor near the narrow track (to the right) should select narrow width
702 inherited = 0;
703 BOOST_CHECK( m_iface->TestInheritTrackWidth( pad, &inherited, VECTOR2I( 2000000, 0 ) ) );
704 BOOST_CHECK_EQUAL( inherited, narrowWidth );
705
706 // Cursor near the wide track (upward) should select wide width
707 inherited = 0;
708 BOOST_CHECK( m_iface->TestInheritTrackWidth( pad, &inherited, VECTOR2I( 0, -2000000 ) ) );
709 BOOST_CHECK_EQUAL( inherited, wideWidth );
710
711 // Cursor slightly offset toward narrow track should still select narrow
712 inherited = 0;
713 BOOST_CHECK( m_iface->TestInheritTrackWidth( pad, &inherited, VECTOR2I( 100000, 50000 ) ) );
714 BOOST_CHECK_EQUAL( inherited, narrowWidth );
715
716 // Cursor slightly offset toward wide track should select wide
717 inherited = 0;
718 BOOST_CHECK( m_iface->TestInheritTrackWidth( pad, &inherited, VECTOR2I( 50000, -100000 ) ) );
719 BOOST_CHECK_EQUAL( inherited, wideWidth );
720}
721
722
730BOOST_AUTO_TEST_CASE( PCBExprGeometryDependentFunctionDetection )
731{
732 PCBEXPR_COMPILER compiler( new PCBEXPR_UNIT_RESOLVER() );
733
734 auto compileAndCheck = [&]( const wxString& aExpr, bool aExpectGeometry )
735 {
736 PCBEXPR_UCODE ucode;
737 PCBEXPR_CONTEXT ctx( 0, F_Cu );
738
739 bool ok = compiler.Compile( aExpr.ToUTF8().data(), &ucode, &ctx );
740 BOOST_CHECK_MESSAGE( ok, "Failed to compile: " + aExpr );
741
742 if( ok )
743 {
744 BOOST_CHECK_MESSAGE( ucode.HasGeometryDependentFunctions() == aExpectGeometry,
745 wxString::Format( "Expression '%s': expected geometry=%s, got %s",
746 aExpr,
747 aExpectGeometry ? "true" : "false",
749 ? "true" : "false" ) );
750 }
751 };
752
753 // Property-based conditions should NOT be geometry-dependent
754 compileAndCheck( wxT( "A.NetClass == 'Power'" ), false );
755 compileAndCheck( wxT( "A.Type == 'via'" ), false );
756 compileAndCheck( wxT( "A.NetName == '/VCC'" ), false );
757
758 // Geometry-dependent functions SHOULD be detected
759 compileAndCheck( wxT( "A.intersectsCourtyard('U1')" ), true );
760 compileAndCheck( wxT( "A.intersectsArea('Zone1')" ), true );
761 compileAndCheck( wxT( "A.enclosedByArea('Zone1')" ), true );
762 compileAndCheck( wxT( "A.intersectsFrontCourtyard('U1')" ), true );
763 compileAndCheck( wxT( "A.intersectsBackCourtyard('U1')" ), true );
764
765 // Deprecated aliases should also be detected
766 compileAndCheck( wxT( "A.insideCourtyard('U1')" ), true );
767 compileAndCheck( wxT( "A.insideArea('Zone1')" ), true );
768}
769
770
779BOOST_FIXTURE_TEST_CASE( PNSCollideSimpleNullShapeGuard, PNS_TEST_FIXTURE )
780{
781 std::unique_ptr<PNS::NODE> world( new PNS::NODE );
782 world->SetMaxClearance( 10000000 );
783 world->SetRuleResolver( &m_ruleResolver );
784
787
788 PNS::SOLID* solid1 = new PNS::SOLID;
789 solid1->SetShape( new SHAPE_CIRCLE( VECTOR2I( 0, 0 ), 500000 ) );
790 solid1->SetPos( VECTOR2I( 0, 0 ) );
791 solid1->SetLayers( PNS_LAYER_RANGE( F_Cu ) );
792 solid1->SetNet( net1 );
793
794 PNS::SOLID* solid2 = new PNS::SOLID;
795 solid2->SetShape( new SHAPE_CIRCLE( VECTOR2I( 100000, 0 ), 500000 ) );
796 solid2->SetPos( VECTOR2I( 100000, 0 ) );
797 solid2->SetLayers( PNS_LAYER_RANGE( F_Cu ) );
798 solid2->SetNet( net2 );
799
800 world->AddRaw( solid1 );
801 world->AddRaw( solid2 );
802
803 // Verify collision works normally with valid shapes
804 PNS::NODE::OBSTACLES obstacles;
805 int count = world->QueryColliding( solid1, obstacles );
806 BOOST_CHECK( count > 0 );
807
808 // Exercise the null-shape guard in collideSimple by calling Collide() directly with a
809 // null-shape solid as the head item. This bypasses the spatial index (which requires a
810 // valid shape for bounding-box computation) and hits the exact code path the guard protects.
811 PNS::SOLID nullShapeSolid;
812 nullShapeSolid.SetPos( VECTOR2I( 0, 0 ) );
813 nullShapeSolid.SetLayers( PNS_LAYER_RANGE( F_Cu ) );
814 nullShapeSolid.SetNet( net2 );
815
816 bool collided = solid1->Collide( &nullShapeSolid, world.get(), F_Cu, nullptr );
817 BOOST_CHECK( !collided );
818}
819
820
829BOOST_FIXTURE_TEST_CASE( PNSComponentDraggerBasicDrag, PNS_TEST_FIXTURE )
830{
831 std::unique_ptr<PNS::NODE> world( new PNS::NODE );
832 world->SetMaxClearance( 10000000 );
833 world->SetRuleResolver( &m_ruleResolver );
834
836
837 VECTOR2I pad1Pos( 0, 0 );
838
839 PNS::SOLID* pad1 = new PNS::SOLID;
840 pad1->SetShape( new SHAPE_CIRCLE( pad1Pos, 500000 ) );
841 pad1->SetPos( pad1Pos );
842 pad1->SetLayers( PNS_LAYER_RANGE( F_Cu ) );
843 pad1->SetNet( net1 );
844 pad1->SetRoutable( true );
845
846 VECTOR2I traceEnd( 2500000, 2500000 );
847 PNS::SEGMENT* trace = new PNS::SEGMENT( SEG( pad1Pos, traceEnd ), net1 );
848 trace->SetWidth( 250000 );
849 trace->SetLayers( PNS_LAYER_RANGE( F_Cu ) );
850
851 world->AddRaw( pad1 );
852 world->AddRaw( trace );
853
854 PNS::COMPONENT_DRAGGER dragger( m_router );
855 dragger.SetWorld( world.get() );
856
857 PNS::ITEM_SET itemsToDrag;
858 itemsToDrag.Add( pad1 );
859
860 bool started = dragger.Start( pad1Pos, itemsToDrag );
861 BOOST_REQUIRE( started );
862
863 // Simulate multiple drag events (mimics mouse movement during drag)
864 VECTOR2I dragPositions[] = {
865 VECTOR2I( 100000, 100000 ),
866 VECTOR2I( 500000, 500000 ),
867 VECTOR2I( 1000000, 1000000 ),
868 VECTOR2I( 500000, 200000 ),
869 VECTOR2I( 0, 0 )
870 };
871
872 for( const VECTOR2I& pos : dragPositions )
873 {
874 bool dragOk = dragger.Drag( pos );
875 BOOST_CHECK( dragOk );
876
877 PNS::NODE* currentNode = dragger.CurrentNode();
878 BOOST_CHECK( currentNode != nullptr );
879 }
880
881 // Verify the dragged items set is populated
882 PNS::ITEM_SET traces = dragger.Traces();
883 BOOST_CHECK( traces.Size() > 0 );
884
885 // Clean up branch nodes before the world is destroyed
886 world->KillChildren();
887}
888
889
890// Dragging the apex of an isolated arc outward keeps a single arc in the chain and
891// changes its radius. Exercises the LINE::DragArc geometric core added for in-router
892// arc dragging.
893BOOST_AUTO_TEST_CASE( PNSLineDragArcResize )
894{
895 // 90-degree CCW arc, centre at origin, radius 1mm.
896 SHAPE_ARC arc( VECTOR2I( 0, 0 ), VECTOR2I( 1000000, 0 ), EDA_ANGLE( 90, DEGREES_T ), 250000 );
897
899 chain.SetWidth( 250000 );
900 chain.Append( arc );
901
902 PNS::LINE line;
903 line.SetWidth( 250000 );
904 line.Line() = chain;
905
906 BOOST_REQUIRE_EQUAL( line.CLine().ArcCount(), 1 );
907
908 double oldRadius = line.CLine().CArcs()[0].GetRadius();
909 int apexIdx = line.CLine().PointCount() / 2;
910
911 // Pull the apex toward the tangent corner at (1mm, 1mm) to grow the radius.
912 line.DragArc( VECTOR2I( 950000, 950000 ), apexIdx );
913
914 BOOST_CHECK_EQUAL( line.CLine().ArcCount(), 1 );
915 BOOST_CHECK( line.CLine().PointCount() >= 2 );
916 BOOST_CHECK( line.CLine().CArcs()[0].GetRadius() != oldRadius );
917}
918
919
920// Driving the arc endpoints together collapses the arc out of the chain (the
921// "collapse to nothing drops it from the route" path in LINE::DragArc).
922BOOST_AUTO_TEST_CASE( PNSLineDragArcCollapse )
923{
924 SHAPE_ARC arc( VECTOR2I( 0, 0 ), VECTOR2I( 1000000, 0 ), EDA_ANGLE( 90, DEGREES_T ), 250000 );
925
927 chain.SetWidth( 250000 );
928 chain.Append( arc );
929
930 PNS::LINE line;
931 line.SetWidth( 250000 );
932 line.Line() = chain;
933
934 BOOST_REQUIRE_EQUAL( line.CLine().ArcCount(), 1 );
935
936 // Drag almost onto the tangent corner at (1mm, 1mm), shrinking the arc until its
937 // endpoints fall within the keep-track threshold and the arc is dropped. Staying a
938 // hair off the corner keeps the constructed radius positive.
939 line.DragArc( VECTOR2I( 999950, 999950 ), line.CLine().PointCount() / 2 );
940
941 BOOST_CHECK_EQUAL( line.CLine().ArcCount(), 0 );
942}
943
944
945// An arc whose central angle reaches 180 degrees cannot be dragged, and the refusal
946// is reported through the router's failure reason so the UI can surface it.
947BOOST_FIXTURE_TEST_CASE( PNSDragArcRejectsNear180, PNS_TEST_FIXTURE )
948{
949 PNS::ROUTING_SETTINGS settings( nullptr, "" );
950 m_router->LoadSettings( &settings );
951
953
954 auto makeArc = [&]( const EDA_ANGLE& aAngle ) -> PNS::ARC*
955 {
956 SHAPE_ARC sa( VECTOR2I( 0, 0 ), VECTOR2I( 1000000, 0 ), aAngle, 250000 );
957 PNS::ARC* a = new PNS::ARC( sa, net );
959 return a;
960 };
961
962 // Shallow arc: drag starts and no failure is reported.
963 {
964 std::unique_ptr<PNS::NODE> world( new PNS::NODE );
965 world->SetMaxClearance( 10000000 );
966 world->SetRuleResolver( &m_ruleResolver );
967
968 PNS::ARC* arc = makeArc( EDA_ANGLE( 90, DEGREES_T ) );
969 world->AddRaw( arc );
970
971 PNS::DRAGGER dragger( m_router );
972 dragger.SetWorld( world.get() );
973 dragger.SetMode( PNS::DM_ARC );
974
975 PNS::ITEM_SET items;
976 items.Add( arc );
977
978 m_router->SetFailureReason( wxEmptyString );
979 BOOST_CHECK( dragger.Start( arc->Anchor( 0 ), items ) );
980 BOOST_CHECK( m_router->FailureReason().IsEmpty() );
981
982 world->KillChildren();
983 }
984
985 // Major arc (>= 180 deg): drag is refused with an explanatory failure reason.
986 {
987 std::unique_ptr<PNS::NODE> world( new PNS::NODE );
988 world->SetMaxClearance( 10000000 );
989 world->SetRuleResolver( &m_ruleResolver );
990
991 PNS::ARC* arc = makeArc( EDA_ANGLE( 270, DEGREES_T ) );
992 world->AddRaw( arc );
993
994 PNS::DRAGGER dragger( m_router );
995 dragger.SetWorld( world.get() );
996 dragger.SetMode( PNS::DM_ARC );
997
998 PNS::ITEM_SET items;
999 items.Add( arc );
1000
1001 m_router->SetFailureReason( wxEmptyString );
1002 BOOST_CHECK( !dragger.Start( arc->Anchor( 0 ), items ) );
1003 BOOST_CHECK( !m_router->FailureReason().IsEmpty() );
1004
1005 world->KillChildren();
1006 }
1007
1008 // Clockwise major arc (negative central angle): the magnitude is what matters, so
1009 // it must be refused just like its CCW counterpart.
1010 {
1011 std::unique_ptr<PNS::NODE> world( new PNS::NODE );
1012 world->SetMaxClearance( 10000000 );
1013 world->SetRuleResolver( &m_ruleResolver );
1014
1015 PNS::ARC* arc = makeArc( EDA_ANGLE( -270, DEGREES_T ) );
1016 world->AddRaw( arc );
1017
1018 PNS::DRAGGER dragger( m_router );
1019 dragger.SetWorld( world.get() );
1020 dragger.SetMode( PNS::DM_ARC );
1021
1022 PNS::ITEM_SET items;
1023 items.Add( arc );
1024
1025 m_router->SetFailureReason( wxEmptyString );
1026 BOOST_CHECK( !dragger.Start( arc->Anchor( 0 ), items ) );
1027 BOOST_CHECK( !m_router->FailureReason().IsEmpty() );
1028
1029 world->KillChildren();
1030 }
1031}
1032
1033
1034// Base mock's NetCode() returns -1 for everything, which reads as unnetted; use a real net here
1035namespace
1036{
1037struct NETCODE_RULE_RESOLVER : public MOCK_RULE_RESOLVER
1038{
1039 int NetCode( PNS::NET_HANDLE aNet ) override { return aNet ? 1 : -1; }
1040};
1041
1042PNS::ITEM* queryFinishAnchor( PNS::NODE& aWorld, const PNS::LINE& aTrack )
1043{
1044 PNS::TOPOLOGY topo( &aWorld );
1045 VECTOR2I anchorPoint;
1046 PNS_LAYER_RANGE anchorLayers;
1047 PNS::ITEM* anchorItem = nullptr;
1048
1049 BOOST_REQUIRE( topo.NearestUnconnectedAnchorPoint( &aTrack, anchorPoint, anchorLayers,
1050 anchorItem ) );
1051 return anchorItem;
1052}
1053} // namespace
1054
1055
1056// F-key finish adds the track to a temporary branch node; a joint linking only to that
1057// track must still yield a persistent-world anchor, not a dangling branch pointer
1058//
1059// Regression test for https://gitlab.com/kicad/code/kicad/-/issues/24985
1060BOOST_FIXTURE_TEST_CASE( PNSFinishAnchorNoDanglingBranchItem, PNS_TEST_FIXTURE )
1061{
1062 NETCODE_RULE_RESOLVER resolver;
1063
1064 PNS::NODE world;
1065 world.SetMaxClearance( 10000000 );
1066 world.SetRuleResolver( &resolver );
1067
1069
1070 // Persistent unconnected target on the same net, away from the track
1071 PNS::SEGMENT* target = new PNS::SEGMENT( SEG( VECTOR2I( 10000000, 10000000 ),
1072 VECTOR2I( 12000000, 10000000 ) ), net );
1073 target->SetWidth( 250000 );
1074 target->SetLayers( PNS_LAYER_RANGE( F_Cu ) );
1075 world.AddRaw( target );
1076
1077 // Closed loop; end joint's two links are both owned by the temporary branch node
1078 PNS::LINE track;
1079 track.SetLayers( PNS_LAYER_RANGE( F_Cu ) );
1080 track.SetNet( net );
1081 track.SetWidth( 250000 );
1082 track.Line().Append( VECTOR2I( 0, 0 ) );
1083 track.Line().Append( VECTOR2I( 2000000, 0 ) );
1084 track.Line().Append( VECTOR2I( 2000000, 2000000 ) );
1085 track.Line().Append( VECTOR2I( 0, 2000000 ) );
1086 track.Line().Append( VECTOR2I( 0, 0 ) );
1087
1088 // Anchor must be the persistent target, never a link owned by the destroyed temp branch
1089 BOOST_CHECK_EQUAL( queryFinishAnchor( world, track ), target );
1090}
1091
1092
1093// ConnectedJoints must cross arcs when subtracting the track; stopping at an arc left
1094// temporary primitives beyond it selectable as the anchor, reviving the dangling pointer
1095//
1096// Regression test for https://gitlab.com/kicad/code/kicad/-/issues/24985
1097BOOST_FIXTURE_TEST_CASE( PNSFinishAnchorCrossesArcInConnectivity, PNS_TEST_FIXTURE )
1098{
1099 NETCODE_RULE_RESOLVER resolver;
1100
1101 PNS::NODE world;
1102 world.SetMaxClearance( 10000000 );
1103 world.SetRuleResolver( &resolver );
1104
1106
1107 // Farther from the track end than its own far segment, wins only once that's subtracted
1108 PNS::SEGMENT* target = new PNS::SEGMENT( SEG( VECTOR2I( 7000000, 0 ),
1109 VECTOR2I( 8000000, 0 ) ), net );
1110 target->SetWidth( 250000 );
1111 target->SetLayers( PNS_LAYER_RANGE( F_Cu ) );
1112 world.AddRaw( target );
1113
1114 // Segment-arc-segment track; far segment lies across the arc, so connectivity must cross it
1115 PNS::LINE track;
1116 track.SetLayers( PNS_LAYER_RANGE( F_Cu ) );
1117 track.SetNet( net );
1118 track.SetWidth( 250000 );
1119 track.Line().Append( VECTOR2I( 0, 0 ) );
1120 track.Line().Append( VECTOR2I( 1000000, 0 ) );
1121 track.Line().Append( SHAPE_ARC( VECTOR2I( 1000000, 0 ), VECTOR2I( 1500000, 500000 ),
1122 VECTOR2I( 2000000, 0 ), 0 ) );
1123 track.Line().Append( VECTOR2I( 3000000, 0 ) );
1124
1125 // Anchor must be the persistent target, not a branch-owned primitive across the arc
1126 BOOST_CHECK_EQUAL( queryFinishAnchor( world, track ), target );
1127}
1128
1129
1130// Regression tests for issues #18658 and #24132. Physical clearance rules must be
1131// enforced for same-net and free-pad pairs without disturbing the fast path on
1132// boards that do not define them.
1133
1134namespace
1135{
1136PNS::VIA* makeVia( const VECTOR2I& aPos, PNS::NET_HANDLE aNet )
1137{
1138 PNS::VIA* v = new PNS::VIA( aPos, PNS_LAYER_RANGE( F_Cu, B_Cu ), 50000, 10000 );
1139 v->SetNet( aNet );
1140 return v;
1141}
1142
1143PNS::SOLID* makePad( const VECTOR2I& aPos, PNS::NET_HANDLE aNet, bool aFreePad = false )
1144{
1145 PNS::SOLID* s = new PNS::SOLID;
1146 s->SetShape( new SHAPE_CIRCLE( aPos, 250000 ) );
1147 s->SetPos( aPos );
1149 s->SetNet( aNet );
1150 s->SetIsFreePad( aFreePad );
1151 return s;
1152}
1153} // namespace
1154
1155
1156// Cross-net via vs via with no physical rules: ordinary CT_CLEARANCE applies.
1157BOOST_FIXTURE_TEST_CASE( PNSCrossNetViaViaElectricalClearanceBaseline, PNS_TEST_FIXTURE )
1158{
1159 std::unique_ptr<PNS::NODE> world( new PNS::NODE );
1160 world->SetMaxClearance( 10000000 );
1161 world->SetRuleResolver( &m_ruleResolver );
1162
1163 PNS::VIA* v1 = makeVia( VECTOR2I( 0, 0 ), (PNS::NET_HANDLE) 1 );
1164 PNS::VIA* v2 = makeVia( VECTOR2I( 0, 100000 ), (PNS::NET_HANDLE) 2 );
1165 world->AddRaw( v1 );
1166 world->AddRaw( v2 );
1167
1168 m_ruleResolver.m_defaultClearance = 1000000;
1169
1170 PNS::NODE::OBSTACLES obstacles;
1171 world->QueryColliding( v1, obstacles );
1172
1173 BOOST_CHECK_GE( obstacles.size(), (size_t) 1 );
1174}
1175
1176
1177// Same-net via vs via with no physical rules: fast path keeps clearance = -1.
1178BOOST_FIXTURE_TEST_CASE( PNSSameNetNoPhysicalRulesFastPathNoCollision, PNS_TEST_FIXTURE )
1179{
1180 std::unique_ptr<PNS::NODE> world( new PNS::NODE );
1181 world->SetMaxClearance( 10000000 );
1182 world->SetRuleResolver( &m_ruleResolver );
1183
1185 world->AddRaw( makeVia( VECTOR2I( 0, 0 ), net ) );
1186 PNS::VIA* v1 = makeVia( VECTOR2I( 0, 100000 ), net );
1187 world->AddRaw( v1 );
1188
1189 m_ruleResolver.m_hasUserPhysicalRules = false;
1190 m_ruleResolver.m_defaultClearance = 1000000;
1191
1192 PNS::NODE::OBSTACLES obstacles;
1193 world->QueryColliding( v1, obstacles );
1194
1195 BOOST_CHECK_EQUAL( obstacles.size(), (size_t) 0 );
1196}
1197
1198
1199// Same-net via vs via with a matching physical_clearance rule: collision reported.
1200// Regression scenario for issues #18658 and #24132.
1201BOOST_FIXTURE_TEST_CASE( PNSSameNetWithPhysicalRuleCollides, PNS_TEST_FIXTURE )
1202{
1203 std::unique_ptr<PNS::NODE> world( new PNS::NODE );
1204 world->SetMaxClearance( 10000000 );
1205 world->SetRuleResolver( &m_ruleResolver );
1206
1208 world->AddRaw( makeVia( VECTOR2I( 0, 0 ), net ) );
1209 PNS::VIA* v1 = makeVia( VECTOR2I( 0, 100000 ), net );
1210 world->AddRaw( v1 );
1211
1212 m_ruleResolver.m_hasUserPhysicalRules = true;
1213 m_ruleResolver.m_defaultPhysicalClearance = 1000000;
1214
1215 PNS::NODE::OBSTACLES obstacles;
1216 world->QueryColliding( v1, obstacles );
1217
1218 BOOST_CHECK_GE( obstacles.size(), (size_t) 1 );
1219 if( !obstacles.empty() )
1220 BOOST_CHECK_EQUAL( obstacles.begin()->m_clearance, 1000000 );
1221}
1222
1223
1224// Free pad vs cross-net pair, no physical rules: fast path applies.
1225BOOST_FIXTURE_TEST_CASE( PNSFreePadNoPhysicalRulesFastPath, PNS_TEST_FIXTURE )
1226{
1227 std::unique_ptr<PNS::NODE> world( new PNS::NODE );
1228 world->SetMaxClearance( 10000000 );
1229 world->SetRuleResolver( &m_ruleResolver );
1230
1231 PNS::SOLID* freePad = makePad( VECTOR2I( 0, 0 ), (PNS::NET_HANDLE) 1, /*aFreePad=*/true );
1232 PNS::VIA* v = makeVia( VECTOR2I( 0, 100000 ), (PNS::NET_HANDLE) 2 );
1233 world->AddRaw( freePad );
1234 world->AddRaw( v );
1235
1236 m_ruleResolver.m_hasUserPhysicalRules = false;
1237 m_ruleResolver.m_defaultClearance = 1000000;
1238
1239 PNS::NODE::OBSTACLES obstacles;
1240 world->QueryColliding( v, obstacles );
1241
1242 BOOST_CHECK_EQUAL( obstacles.size(), (size_t) 0 );
1243}
1244
1245
1246// Free pad vs cross-net pair, physical rules present but no match: safety net must
1247// keep free pads from reporting collisions just because the board has a physical
1248// rule somewhere.
1250{
1251 std::unique_ptr<PNS::NODE> world( new PNS::NODE );
1252 world->SetMaxClearance( 10000000 );
1253 world->SetRuleResolver( &m_ruleResolver );
1254
1255 PNS::SOLID* freePad = makePad( VECTOR2I( 0, 0 ), (PNS::NET_HANDLE) 1, /*aFreePad=*/true );
1256 PNS::VIA* v = makeVia( VECTOR2I( 0, 100000 ), (PNS::NET_HANDLE) 2 );
1257 world->AddRaw( freePad );
1258 world->AddRaw( v );
1259
1260 m_ruleResolver.m_hasUserPhysicalRules = true;
1261 m_ruleResolver.m_defaultPhysicalClearance = 0; // rule does not match
1262 m_ruleResolver.m_defaultClearance = 1000000; // would collide if !sameNet block runs
1263
1264 PNS::NODE::OBSTACLES obstacles;
1265 world->QueryColliding( v, obstacles );
1266
1267 BOOST_CHECK_EQUAL( obstacles.size(), (size_t) 0 );
1268}
1269
1270
1271// Free pad with a matching physical_clearance rule: collision reported.
1272BOOST_FIXTURE_TEST_CASE( PNSFreePadPhysicalRuleEnforced, PNS_TEST_FIXTURE )
1273{
1274 std::unique_ptr<PNS::NODE> world( new PNS::NODE );
1275 world->SetMaxClearance( 10000000 );
1276 world->SetRuleResolver( &m_ruleResolver );
1277
1278 PNS::SOLID* freePad = makePad( VECTOR2I( 0, 0 ), (PNS::NET_HANDLE) 1, /*aFreePad=*/true );
1279 PNS::VIA* v = makeVia( VECTOR2I( 0, 100000 ), (PNS::NET_HANDLE) 2 );
1280 world->AddRaw( freePad );
1281 world->AddRaw( v );
1282
1283 m_ruleResolver.m_hasUserPhysicalRules = true;
1284 m_ruleResolver.m_defaultPhysicalClearance = 1000000;
1285
1286 PNS::NODE::OBSTACLES obstacles;
1287 world->QueryColliding( v, obstacles );
1288
1289 BOOST_CHECK_GE( obstacles.size(), (size_t) 1 );
1290}
1291
1292
1293// Same-net via vs pad with a matching physical_hole_clearance rule. Covers the
1294// drill-into-pad half of issue #24132 and exercises CT_PHYSICAL_HOLE_CLEARANCE.
1295BOOST_FIXTURE_TEST_CASE( PNSSameNetPhysicalHoleClearance, PNS_TEST_FIXTURE )
1296{
1297 std::unique_ptr<PNS::NODE> world( new PNS::NODE );
1298 world->SetMaxClearance( 10000000 );
1299 world->SetRuleResolver( &m_ruleResolver );
1300
1302 PNS::SOLID* pad = makePad( VECTOR2I( 0, 0 ), net );
1303 PNS::VIA* via = makeVia( VECTOR2I( 0, 100000 ), net );
1304 world->AddRaw( pad );
1305 world->AddRaw( via );
1306
1307 m_ruleResolver.m_hasUserPhysicalRules = true;
1308 m_ruleResolver.m_defaultPhysicalHoleClearance = 1000000;
1309
1310 PNS::NODE::OBSTACLES obstacles;
1311 world->QueryColliding( via, obstacles );
1312
1313 BOOST_CHECK_GE( obstacles.size(), (size_t) 1 );
1314}
1315
1316
1317BOOST_FIXTURE_TEST_CASE( PNSSameNetSafetyNetOnOverlap, PNS_TEST_FIXTURE )
1318{
1319 std::unique_ptr<PNS::NODE> world( new PNS::NODE );
1320 world->SetMaxClearance( 10000000 );
1321 world->SetRuleResolver( &m_ruleResolver );
1322
1324 world->AddRaw( makeVia( VECTOR2I( 0, 0 ), net ) );
1325 PNS::VIA* v1 = makeVia( VECTOR2I( 0, 30000 ), net ); // overlaps the first via
1326 world->AddRaw( v1 );
1327
1328 m_ruleResolver.m_hasUserPhysicalRules = true;
1329 m_ruleResolver.m_defaultPhysicalClearance = 0;
1330
1331 PNS::NODE::OBSTACLES obstacles;
1332 world->QueryColliding( v1, obstacles );
1333
1334 BOOST_CHECK_EQUAL( obstacles.size(), (size_t) 0 );
1335}
1336
1337
1338// Same-net pad+via with both physical_clearance and physical_hole_clearance
1339// matching: the resolver returns max across the two query points.
1340BOOST_FIXTURE_TEST_CASE( PNSBothPhysicalConstraintsMaxWins, PNS_TEST_FIXTURE )
1341{
1342 std::unique_ptr<PNS::NODE> world( new PNS::NODE );
1343 world->SetMaxClearance( 10000000 );
1344 world->SetRuleResolver( &m_ruleResolver );
1345
1347 PNS::SOLID* pad = makePad( VECTOR2I( 0, 0 ), net );
1348 PNS::VIA* via = makeVia( VECTOR2I( 0, 100000 ), net );
1349 world->AddRaw( pad );
1350 world->AddRaw( via );
1351
1352 m_ruleResolver.m_hasUserPhysicalRules = true;
1353 m_ruleResolver.m_defaultPhysicalClearance = 100000;
1354 m_ruleResolver.m_defaultPhysicalHoleClearance = 2000000;
1355
1356 PNS::NODE::OBSTACLES obstacles;
1357 world->QueryColliding( via, obstacles );
1358
1359 BOOST_CHECK_GE( obstacles.size(), (size_t) 1 );
1360
1361 // The recursive collideSimple call for the via's hole inserts a separate OBSTACLE
1362 // with the max-accumulated clearance, so look across all entries rather than
1363 // relying on std::set ordering (which is by pointer).
1364 int maxClearance = 0;
1365 for( const PNS::OBSTACLE& obs : obstacles )
1366 maxClearance = std::max( maxClearance, obs.m_clearance );
1367 BOOST_CHECK_EQUAL( maxClearance, 2000000 );
1368}
1369
1370
1371// Diff pair vias must respect copper-to-hole clearance, not just copper-to-copper and
1372// hole-to-hole. EffectiveDiffPairViaGap() is the copper-edge-to-copper-edge distance the
1373// placer fits vias to; it converts each clearance rule to that reference by subtracting the
1374// annular ring(s) of via copper that sit between a hole edge and the copper edge.
1375//
1376// Regression test for https://gitlab.com/kicad/code/kicad/-/issues/21623 where the placer
1377// ignored copper-to-hole clearance and produced DRC violations on diff pair vias.
1378BOOST_AUTO_TEST_CASE( PNSDiffPairViaGapCopperToHoleClearance )
1379{
1380 PNS::SIZES_SETTINGS sizes;
1381
1382 // 600um copper diameter over a 300um drill leaves a 150um annular ring.
1383 sizes.SetViaDiameter( 600000 );
1384 sizes.SetViaDrill( 300000 );
1385 sizes.SetDiffPairViaGapSameAsTraceGap( false );
1386
1388
1389 // Copper-to-hole binds because 400um from a hole edge to the neighbour's copper edge is
1390 // 400000 - 150000 = 250000 copper-to-copper, exceeding both other rules.
1391 sizes.SetDiffPairViaGap( 200000 );
1392 sizes.SetDiffPairHoleToHole( 500000 ); // 500000 - 300000 = 200000 copper-to-copper
1393 sizes.SetDiffPairCopperToHole( 400000 );
1394
1395 BOOST_CHECK_EQUAL( sizes.GetDiffPairCopperToHole(), 400000 );
1396 BOOST_CHECK_EQUAL( sizes.EffectiveDiffPairViaGap(), 250000 );
1397
1398 // Hole-to-hole binds when it is the largest converted rule.
1399 sizes.SetDiffPairHoleToHole( 900000 ); // 900000 - 300000 = 600000 copper-to-copper
1400 BOOST_CHECK_EQUAL( sizes.EffectiveDiffPairViaGap(), 600000 );
1401
1402 // Plain copper-to-copper gap binds when the hole-based rules are slack.
1403 sizes.SetDiffPairHoleToHole( 0 );
1404 sizes.SetDiffPairCopperToHole( 0 );
1405 BOOST_CHECK_EQUAL( sizes.EffectiveDiffPairViaGap(), 200000 );
1406}
A base class for any item which can be embedded within the BOARD container class, and therefore insta...
Definition board_item.h:83
virtual bool IsOnLayer(PCB_LAYER_ID aLayer) const
Test to see if this object is on the given layer.
Definition board_item.h:377
Information pertinent to a Pcbnew printed circuit board.
Definition board.h:373
KICAD_T Type() const
Returns the type of object.
Definition eda_item.h:108
bool Compile(const wxString &aString, UCODE *aCode, CONTEXT *aPreflightContext)
T Min() const
Definition minoptmax.h:29
void SetMin(T v)
Definition minoptmax.h:38
PNS::RULE_RESOLVER * GetRuleResolver() override
void DisplayItem(const PNS::ITEM *aItem, int aClearance, bool aEdit=false, int aFlags=0) override
void HideItem(PNS::ITEM *aItem) override
MOCK_PNS_KICAD_IFACE(PNS_TEST_FIXTURE *aFixture)
PNS_TEST_FIXTURE * m_testFixture
bool TestInheritTrackWidth(PNS::ITEM *aItem, int *aInheritedWidth, const VECTOR2I &aStartPosition=VECTOR2I())
virtual int NetCode(PNS::NET_HANDLE aNet) override
bool IsNetTieExclusion(const PNS::ITEM *aItem, const VECTOR2I &aCollisionPos, const PNS::ITEM *aCollidingItem) override
bool IsKeepout(const PNS::ITEM *aObstacle, const PNS::ITEM *aItem, bool *aEnforce) override
virtual int Clearance(const PNS::ITEM *aA, const PNS::ITEM *aB, bool aUseClearanceEpsilon=true) override
virtual bool QueryConstraint(PNS::CONSTRAINT_TYPE aType, const PNS::ITEM *aItemA, const PNS::ITEM *aItemB, int aLayer, PNS::CONSTRAINT *aConstraint) override
bool IsInNetTie(const PNS::ITEM *aA) override
std::map< ITEM_KEY, PNS::CONSTRAINT > m_ruleMap
virtual PNS::NET_HANDLE DpCoupledNet(PNS::NET_HANDLE aNet) override
bool IsDrilledHole(const PNS::ITEM *aItem) override
void AddMockRule(PNS::CONSTRAINT_TYPE aType, const PNS::ITEM *aItemA, const PNS::ITEM *aItemB, PNS::CONSTRAINT &aConstraint)
bool IsNonPlatedSlot(const PNS::ITEM *aItem) override
bool HasUserDefinedPhysicalConstraint() override
virtual bool DpNetPair(const PNS::ITEM *aItem, PNS::NET_HANDLE &aNetP, PNS::NET_HANDLE &aNetN) override
virtual wxString NetName(PNS::NET_HANDLE aNet) override
int ClearanceEpsilon() const override
virtual int DpNetPolarity(PNS::NET_HANDLE aNet) override
Definition pad.h:61
bool HasGeometryDependentFunctions() const
virtual VECTOR2I Anchor(int n) const override
Definition pns_arc.h:100
const ITEM_SET Traces() override
Function Traces()
NODE * CurrentNode() const override
Function CurrentNode()
bool Start(const VECTOR2I &aP, ITEM_SET &aPrimitives) override
Function Start()
bool Drag(const VECTOR2I &aP) override
Function Drag()
DRAGGER.
Definition pns_dragger.h:48
virtual bool Start(const VECTOR2I &aP, ITEM_SET &aPrimitives) override
Function Start()
void SetMode(PNS::DRAG_MODE aDragMode) override
virtual void SetWorld(NODE *aWorld)
Function SetWorld()
int Size() const
void Add(const LINE &aLine)
Base class for PNS router board items.
Definition pns_item.h:98
BOARD_ITEM * Parent() const
Definition pns_item.h:199
bool IsFreePad() const
Definition pns_item.h:288
void SetLayers(const PNS_LAYER_RANGE &aLayers)
Definition pns_item.h:213
void SetIsFreePad(bool aIsFreePad=true)
Definition pns_item.h:286
const PNS_LAYER_RANGE & Layers() const
Definition pns_item.h:212
virtual NET_HANDLE Net() const
Definition pns_item.h:210
void SetNet(NET_HANDLE aNet)
Definition pns_item.h:209
virtual int Layer() const
Definition pns_item.h:216
bool Collide(const ITEM *aHead, const NODE *aNode, int aLayer, COLLISION_SEARCH_CONTEXT *aCtx=nullptr) const
Check for a collision (clearance violation) with between us and item aOther.
Definition pns_item.cpp:305
bool OfKind(int aKindMask) const
Definition pns_item.h:181
virtual void Mark(int aMarker) const
Definition pns_item.h:261
virtual BOARD_ITEM * BoardItem() const
Definition pns_item.h:207
void SetRoutable(bool aRoutable)
Definition pns_item.h:283
bool IsLocked() const
Definition pns_item.h:278
Represents a track on a PCB, connecting two non-trivial joints (that is, vias, pads,...
Definition pns_line.h:62
void DragArc(const VECTOR2I &aP, int aIndex)
Definition pns_line.cpp:863
const SHAPE_LINE_CHAIN & CLine() const
Definition pns_line.h:142
SHAPE_LINE_CHAIN & Line()
Definition pns_line.h:141
void SetWidth(int aWidth)
Return line width.
Definition pns_line.h:155
Keep the router "world" - i.e.
Definition pns_node.h:242
void SetMaxClearance(int aClearance)
Assign a clearance resolution function object.
Definition pns_node.h:280
void AddRaw(ITEM *aItem, bool aAllowRedundant=false)
Definition pns_node.h:520
std::set< OBSTACLE > OBSTACLES
Definition pns_node.h:254
void SetRuleResolver(RULE_RESOLVER *aFunc)
Definition pns_node.h:286
Contain all persistent settings of the router, such as the mode, optimization effort,...
int Width() const override
Definition pns_segment.h:96
void SetWidth(int aWidth) override
Definition pns_segment.h:91
The actual Push and Shove algorithm.
Definition pns_shove.h:47
void SetShovePolicy(const LINKED_ITEM *aItem, int aPolicy)
void SetDiffPairViaGapSameAsTraceGap(bool aEnable)
void SetDiffPairCopperToHole(int aCopperToHole)
int GetDiffPairCopperToHole() const
void SetViaDrill(int aDrill)
void SetDiffPairViaGap(int aGap)
void SetDiffPairHoleToHole(int aHoleToHole)
void SetViaDiameter(int aDiameter)
int EffectiveDiffPairViaGap() const
void SetPos(const VECTOR2I &aCenter)
Definition pns_solid.cpp:81
void SetShape(SHAPE *shape)
Definition pns_solid.h:113
bool inheritTrackWidth(PNS::ITEM *aItem, int *aInheritedWidth, const VECTOR2I &aStartPosition)
Represent a contiguous set of PCB layers.
int Start() const
bool Overlaps(const PNS_LAYER_RANGE &aOther) const
int End() const
PNS_LAYER_RANGE Intersection(const PNS_LAYER_RANGE &aOther) const
Shortcut for comparisons/overlap tests.
Definition seg.h:38
Represent a polyline containing arcs as well as line segments: A chain of connected line and/or arc s...
int PointCount() const
Return the number of points (vertices) in this line chain.
const std::vector< SHAPE_ARC > & CArcs() const
void Append(int aX, int aY, bool aAllowDuplication=false)
Append a new point at the end of the line chain.
size_t ArcCount() const
@ DEGREES_T
Definition eda_angle.h:31
static FILENAME_RESOLVER * resolver
constexpr PCB_LAYER_ID PCBNEW_LAYER_ID_START
Definition layer_ids.h:170
@ Edge_Cuts
Definition layer_ids.h:108
@ B_Cu
Definition layer_ids.h:61
@ In2_Cu
Definition layer_ids.h:63
@ Margin
Definition layer_ids.h:109
@ In4_Cu
Definition layer_ids.h:65
@ In1_Cu
Definition layer_ids.h:62
@ PCB_LAYER_ID_COUNT
Definition layer_ids.h:167
@ F_Cu
Definition layer_ids.h:60
CONSTRAINT_TYPE
Definition pns_node.h:52
void * NET_HANDLE
Definition pns_item.h:55
@ DM_ARC
Definition pns_router.h:81
@ MK_LOCKED
Definition pns_item.h:45
std::unique_ptr< typename std::remove_const< T >::type > Clone(const T &aItem)
Definition pns_item.h:344
bool operator<(const ITEM_KEY &other) const
bool operator==(const ITEM_KEY &other) const
An abstract function object, returning a design rule (clearance, diff pair gap, etc) required between...
Definition pns_node.h:74
MINOPTMAX< int > m_Value
Definition pns_node.h:76
CONSTRAINT_TYPE m_Type
Definition pns_node.h:75
Hold an object colliding with another object, along with some useful data about the collision.
Definition pns_node.h:89
SETTINGS_MANAGER m_settingsManager
MOCK_RULE_RESOLVER m_ruleResolver
PNS::ROUTER * m_router
MOCK_PNS_KICAD_IFACE * m_iface
BOOST_REQUIRE(intersection.has_value()==c.ExpectedIntersection.has_value())
VECTOR3I v1(5, 5, 5)
static bool isEdge(const PNS::ITEM *aItem)
static bool isHole(const PNS::ITEM *aItem)
static void dumpObstacles(const PNS::NODE::OBSTACLES &obstacles)
static bool isCopper(const PNS::ITEM *aItem)
BOOST_AUTO_TEST_CASE(PCBViaBackdrillCloneRetainsData)
BOOST_FIXTURE_TEST_CASE(PNSShoveOwnsRootLineHistory, PNS_TEST_FIXTURE)
const SHAPE_LINE_CHAIN chain
BOOST_TEST_MESSAGE("Polyline has "<< chain.PointCount()<< " points")
BOOST_CHECK_EQUAL(result, "25.4")
VECTOR2I v2(1, 0)
@ PCB_PAD_T
class PAD, a pad in a footprint
Definition typeinfo.h:80
VECTOR2< int32_t > VECTOR2I
Definition vector2d.h:683