KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_via_stitch.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, you may find one here:
18 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
19 */
20
21#include <boost/test/unit_test.hpp>
22
23#include <algorithm>
24#include <filesystem>
25#include <fstream>
26#include <iterator>
27#include <set>
28#include <string>
29#include <vector>
30
31#include <base_units.h>
32#include <board.h>
33#include <board_commit.h>
35#include <drc/drc_engine.h>
36#include <footprint.h>
39#include <netinfo.h>
40#include <pad.h>
41#include <pcb_track.h>
43#include <tool/tool_manager.h>
44#include <zone.h>
45#include <zones.h>
46
50
51BOOST_AUTO_TEST_SUITE( ViaStitch )
52
53static SHAPE_POLY_SET makeRectPoly( int aX1, int aY1, int aX2, int aY2 )
54{
55 SHAPE_POLY_SET poly;
56 poly.NewOutline();
57 poly.Append( aX1, aY1 );
58 poly.Append( aX2, aY1 );
59 poly.Append( aX2, aY2 );
60 poly.Append( aX1, aY2 );
61 return poly;
62}
63
64
65static void configureStitch( PCB_VIA_STITCH* aStitch )
66{
67 aStitch->SetOutline( makeRectPoly( pcbIUScale.mmToIU( 2 ), pcbIUScale.mmToIU( 2 ),
68 pcbIUScale.mmToIU( 18 ), pcbIUScale.mmToIU( 18 ) ) );
69 aStitch->SetPitch( pcbIUScale.mmToIU( 2 ) );
72 aStitch->SetSeed( 1234 );
73 aStitch->ViaTemplate()->SetWidth( PADSTACK::ALL_LAYERS, pcbIUScale.mmToIU( 0.6 ) );
74 aStitch->ViaTemplate()->SetDrill( pcbIUScale.mmToIU( 0.3 ) );
75 aStitch->SetNetCode( 1 );
76}
77
78
79static void checkOutlinesEqual( const SHAPE_POLY_SET& aExpected, const SHAPE_POLY_SET& aActual )
80{
81 BOOST_REQUIRE_EQUAL( aActual.OutlineCount(), aExpected.OutlineCount() );
82
83 const SHAPE_LINE_CHAIN& expected = aExpected.COutline( 0 );
84 const SHAPE_LINE_CHAIN& actual = aActual.COutline( 0 );
85
86 BOOST_REQUIRE_EQUAL( actual.PointCount(), expected.PointCount() );
87
88 for( int i = 0; i < expected.PointCount(); ++i )
89 BOOST_CHECK_EQUAL( actual.CPoint( i ), expected.CPoint( i ) );
90}
91
92
93BOOST_AUTO_TEST_CASE( BakedPatternConstantsLocked )
94{
95 // Extra idiot checking that nobody touches the constants
96 // Or else existing boards will change stitch patterns
99 BOOST_CHECK( !PCB_VIA_STITCH::bakedPoissonTile().empty() );
100}
101
102
103BOOST_AUTO_TEST_CASE( GuardEnvelopeSampling )
104{
105 // Guarding previously broke down at pitches > 1.7mm
106 const int pitch = pcbIUScale.mmToIU( 2 );
107
108 // 20mm x 10mm rectangle: a 60mm perimeter, so ~30 samples before the spacing filter.
109 SHAPE_POLY_SET envelope = makeRectPoly( 0, 0, pcbIUScale.mmToIU( 20 ),
110 pcbIUScale.mmToIU( 10 ) );
111
112 auto acceptAll = []( const VECTOR2I& ) { return true; };
113
114 // Nothing inside the rectangle to guard, so no pair is exempt from the spacing minimum.
115 SHAPE_POLY_SET noGuarded;
116
117 std::vector<VECTOR2I> samples =
118 PCB_VIA_STITCH::SampleGuardEnvelope( envelope, noGuarded, pitch, acceptAll );
119
120 // A 60mm perimeter walked every 2mm can't produce many fewer than 30 without the
121 // spacing filter having gone haywire; the corners are the only place it can bite.
122 BOOST_CHECK_GE( samples.size(), 26u );
123 BOOST_CHECK_LE( samples.size(), 30u );
124
125 // Every sample lands on the envelope boundary.
126 for( const VECTOR2I& pt : samples )
127 {
128 BOOST_CHECK_MESSAGE( envelope.Collide( pt, pcbIUScale.mmToIU( 0.001 ) ),
129 "sample " << pt.x << "," << pt.y << " left the envelope" );
130 }
131
132 // No two vias end up closer than the 0.7 * pitch minimum.
133 const int64_t minDistSq = (int64_t) ( pitch * 0.7 ) * (int64_t) ( pitch * 0.7 );
134
135 for( size_t i = 0; i < samples.size(); ++i )
136 {
137 for( size_t j = i + 1; j < samples.size(); ++j )
138 {
139 VECTOR2I d = samples[i] - samples[j];
140 BOOST_CHECK_GE( (int64_t) d.x * d.x + (int64_t) d.y * d.y, minDistSq );
141 }
142 }
143
144 // Positions the caller rejects are dropped rather than nudged elsewhere.
145 const int midX = pcbIUScale.mmToIU( 10 );
146
147 std::vector<VECTOR2I> leftHalf = PCB_VIA_STITCH::SampleGuardEnvelope(
148 envelope, noGuarded, pitch,
149 [&]( const VECTOR2I& aPt )
150 {
151 return aPt.x < midX;
152 } );
153
154 BOOST_CHECK( !leftHalf.empty() );
155 BOOST_CHECK_LT( leftHalf.size(), samples.size() );
156
157 for( const VECTOR2I& pt : leftHalf )
158 BOOST_CHECK_LT( pt.x, midX );
159
160 // A degenerate pitch yields nothing instead of looping forever.
161 BOOST_CHECK( PCB_VIA_STITCH::SampleGuardEnvelope( envelope, noGuarded, 0, acceptAll ).empty() );
162 BOOST_CHECK( PCB_VIA_STITCH::SampleGuardEnvelope( envelope, noGuarded, -1, acceptAll ).empty() );
163}
164
165
166BOOST_AUTO_TEST_CASE( GuardEnvelopeCoversBothSidesAtCoarsePitch )
167{
168 // A guard envelope is a thin slab: the two rows of vias face each other across only
169 // (trackWidth + viaSize + 2 * clearance), which a coarse pitch's 0.7 * pitch spacing
170 // minimum swallows whole. Culling on that alone leaves one side of the trace bare.
171 const int trackWidth = pcbIUScale.mmToIU( 0.2 );
172 const int envelopeOffset = pcbIUScale.mmToIU( 0.607 ); // via radius + clearance + slop
173 const int traceLen = pcbIUScale.mmToIU( 20 );
174 const int centreY = pcbIUScale.mmToIU( 10 );
175
176 // Horizontal trace, and the slab standing off it on both sides.
177 SHAPE_POLY_SET guarded = makeRectPoly( 0, centreY - trackWidth / 2, traceLen,
178 centreY + trackWidth / 2 );
179 SHAPE_POLY_SET envelope = makeRectPoly( -envelopeOffset, centreY - envelopeOffset,
180 traceLen + envelopeOffset,
181 centreY + envelopeOffset );
182
183 auto acceptAll = []( const VECTOR2I& ) { return true; };
184
185 // 2mm is the default pitch, and 0.7 * 2mm = 1.4mm overshoots the 1.214mm gap between
186 // the facing rows.
187 const int pitch = pcbIUScale.mmToIU( 2 );
188 BOOST_REQUIRE_GT( pitch * 0.7, 2.0 * envelopeOffset );
189
190 std::vector<VECTOR2I> samples =
191 PCB_VIA_STITCH::SampleGuardEnvelope( envelope, guarded, pitch, acceptAll );
192
193 int above = 0;
194 int below = 0;
195
196 for( const VECTOR2I& pt : samples )
197 {
198 if( pt.y < centreY )
199 above++;
200 else if( pt.y > centreY )
201 below++;
202 }
203
204 // Both sides of a 20mm trace should carry a full row at 2mm pitch, not a scattering.
205 BOOST_CHECK_GE( above, 8 );
206 BOOST_CHECK_GE( below, 8 );
207
208 // The exemption is for facing pairs only: along one side the pitch spacing still holds.
209 std::vector<int> aboveXs;
210
211 for( const VECTOR2I& pt : samples )
212 {
213 if( pt.y < centreY )
214 aboveXs.push_back( pt.x );
215 }
216
217 std::sort( aboveXs.begin(), aboveXs.end() );
218
219 for( size_t i = 1; i < aboveXs.size(); ++i )
220 BOOST_CHECK_GE( aboveXs[i] - aboveXs[i - 1], (int) ( pitch * 0.7 ) );
221}
222
223
224BOOST_AUTO_TEST_CASE( PropertiesRoundTrip )
225{
226 BOARD board;
227 board.Add( new NETINFO_ITEM( &board, wxT( "GND" ), 1 ) );
228
230 a.SetParent( &board );
231 configureStitch( &a );
232 a.ExcludePosition( VECTOR2I( pcbIUScale.mmToIU( 6 ), pcbIUScale.mmToIU( 6 ) ) );
233
235 b.SetParent( &board );
237
239 BOOST_CHECK_EQUAL( (int) b.GetLayout(), (int) a.GetLayout() );
240 BOOST_CHECK_EQUAL( (int) b.GetMode(), (int) a.GetMode() );
243
245
246 auto aCells = a.GetProperties().get_opt<std::vector<VECTOR2I>>( "excluded_grid_cells" );
247 auto bCells = b.GetProperties().get_opt<std::vector<VECTOR2I>>( "excluded_grid_cells" );
248
249 BOOST_REQUIRE( aCells.has_value() );
250 BOOST_REQUIRE( bCells.has_value() );
251 BOOST_REQUIRE_EQUAL( bCells->size(), aCells->size() );
252
253 for( size_t i = 0; i < aCells->size(); ++i )
254 BOOST_CHECK_EQUAL( ( *bCells )[i], ( *aCells )[i] );
255}
256
257
258BOOST_AUTO_TEST_CASE( SexprSaveLoad )
259{
260 auto board = std::make_unique<BOARD>();
261 board->Add( new NETINFO_ITEM( board.get(), wxT( "GND" ), 1 ) );
262
263 ZONE* zone = new ZONE( board.get() );
264 zone->SetLayer( F_Cu );
265 zone->SetNetCode( 1 );
266 zone->Outline()->NewOutline();
267 zone->Outline()->Append( 0, 0 );
268 zone->Outline()->Append( pcbIUScale.mmToIU( 1 ), 0 );
269 zone->Outline()->Append( pcbIUScale.mmToIU( 1 ), pcbIUScale.mmToIU( 1 ) );
270 zone->Outline()->Append( 0, pcbIUScale.mmToIU( 1 ) );
271 board->Add( zone );
272
273 PCB_VIA_STITCH* stitch = new PCB_VIA_STITCH();
274 board->Add( stitch );
275 configureStitch( stitch );
276 stitch->ExcludePosition( VECTOR2I( pcbIUScale.mmToIU( 6 ), pcbIUScale.mmToIU( 6 ) ) );
277
278 auto path = std::filesystem::temp_directory_path() / "qa_via_stitch_roundtrip.kicad_pcb";
279
280 ::KI_TEST::DumpBoardToFile( *board, path.string() );
281
282 std::unique_ptr<BOARD> board2 = ::KI_TEST::ReadBoardFromFileOrStream( path.string() );
283
284 BOOST_REQUIRE_EQUAL( board2->Generators().size(), 1 );
285
286 PCB_VIA_STITCH* loaded = dynamic_cast<PCB_VIA_STITCH*>( board2->Generators().front() );
287 BOOST_REQUIRE( loaded );
288
289 BOOST_CHECK_EQUAL( loaded->GetPitch(), stitch->GetPitch() );
290 BOOST_CHECK_EQUAL( (int) loaded->GetLayout(), (int) stitch->GetLayout() );
291 BOOST_CHECK_EQUAL( (int) loaded->GetMode(), (int) stitch->GetMode() );
292 BOOST_CHECK_EQUAL( loaded->GetSeed(), stitch->GetSeed() );
293
294 // The template via travels through the (templates ...) section
295 BOOST_CHECK_EQUAL( loaded->GetViaSize(), stitch->GetViaSize() );
296 BOOST_CHECK_EQUAL( loaded->GetViaDrill(), stitch->GetViaDrill() );
297
298 // Net is stored by name and re-resolved against the loaded board
299 NETINFO_ITEM* gnd = board2->FindNet( wxT( "GND" ) );
300 BOOST_REQUIRE( gnd );
301 BOOST_CHECK_EQUAL( loaded->GetNetCode(), gnd->GetNetCode() );
302
303 checkOutlinesEqual( stitch->Outline(), loaded->Outline() );
304
305 auto savedCells = stitch->GetProperties().get_opt<std::vector<VECTOR2I>>( "excluded_grid_cells" );
306 auto loadedCells = loaded->GetProperties().get_opt<std::vector<VECTOR2I>>( "excluded_grid_cells" );
307
308 BOOST_REQUIRE( savedCells.has_value() );
309 BOOST_REQUIRE( loadedCells.has_value() );
310 BOOST_REQUIRE_EQUAL( loadedCells->size(), savedCells->size() );
311
312 // (6mm, 6mm) on a 2mm staggered grid is cell (3, 3).
313 BOOST_REQUIRE_EQUAL( savedCells->size(), 1 );
314 BOOST_CHECK_EQUAL( savedCells->front(), VECTOR2I( 3, 3 ) );
315 BOOST_CHECK_EQUAL( loadedCells->front(), VECTOR2I( 3, 3 ) );
316
317 std::ifstream in( path );
318 std::string text( ( std::istreambuf_iterator<char>( in ) ),
319 std::istreambuf_iterator<char>() );
320
321 BOOST_CHECK( text.find( "excluded_grid_cells" ) != std::string::npos );
322 BOOST_CHECK( text.find( "(ij 3 3)" ) != std::string::npos );
323}
324
325
326// An exclusion identifies a via within one specific pattern, so a layout or mode change drops
327// it rather than translating it. The alternative silently relocates the user's work: the same
328// cell index sits half a pitch away under STAGGERED, and means nothing at all under POISSON.
329BOOST_AUTO_TEST_CASE( ExclusionsClearedOnLayoutOrModeChange )
330{
331 BOARD board;
332 board.Add( new NETINFO_ITEM( &board, wxT( "GND" ), 1 ) );
333
334 const VECTOR2I pos( pcbIUScale.mmToIU( 6 ), pcbIUScale.mmToIU( 6 ) );
335
336 auto excluded = []( const PCB_VIA_STITCH& aStitch )
337 {
338 STRING_ANY_MAP props = aStitch.GetProperties();
339 return props.get_opt<std::vector<VECTOR2I>>( "excluded_grid_cells" ).has_value()
340 || props.get_opt<SHAPE_LINE_CHAIN>( "excluded_positions" ).has_value();
341 };
342
343 // PLAIN -> STAGGERED
344 {
345 PCB_VIA_STITCH stitch;
346 stitch.SetParent( &board );
347 configureStitch( &stitch );
349 stitch.ExcludePosition( pos );
350
351 BOOST_REQUIRE( stitch.HasExclusions() );
352
354
355 BOOST_CHECK( !stitch.HasExclusions() );
356 BOOST_CHECK( !excluded( stitch ) );
357 }
358
359 // PLAIN -> POISSON
360 {
361 PCB_VIA_STITCH stitch;
362 stitch.SetParent( &board );
363 configureStitch( &stitch );
365 stitch.ExcludePosition( pos );
366
368
369 BOOST_CHECK( !stitch.HasExclusions() );
370
371 // The POISSON exclusion that replaces it is stored verbatim, not as an index
372 stitch.ExcludePosition( pos );
373
374 STRING_ANY_MAP props = stitch.GetProperties();
375 auto chain = props.get_opt<SHAPE_LINE_CHAIN>( "excluded_positions" );
376
377 BOOST_CHECK( !props.get_opt<std::vector<VECTOR2I>>( "excluded_grid_cells" ).has_value() );
378 BOOST_REQUIRE( chain.has_value() );
379 BOOST_REQUIRE_EQUAL( chain->PointCount(), 1 );
380 BOOST_CHECK_EQUAL( chain->CPoint( 0 ), pos );
381 }
382
383 // STITCH -> GUARD clears too
384 {
385 PCB_VIA_STITCH stitch;
386 stitch.SetParent( &board );
387 configureStitch( &stitch );
388 stitch.ExcludePosition( pos );
389
391
392 BOOST_CHECK( !stitch.HasExclusions() );
393 }
394
395 // Re-assigning the value already in effect must not throw exclusions away
396 {
397 PCB_VIA_STITCH stitch;
398 stitch.SetParent( &board );
399 configureStitch( &stitch );
401 stitch.ExcludePosition( pos );
402
405
406 BOOST_CHECK( stitch.HasExclusions() );
407 }
408
409 // Loading must not run through the setters and wipe what the file carries
410 {
411 PCB_VIA_STITCH saved;
412 saved.SetParent( &board );
413 configureStitch( &saved );
415 saved.ExcludePosition( pos );
416
417 PCB_VIA_STITCH loaded;
418 loaded.SetParent( &board );
419 loaded.SetProperties( saved.GetProperties() );
420
421 BOOST_CHECK( loaded.HasExclusions() );
422 }
423}
424
425
426// Negative indices are ordinary: the grid is anchored to the board origin, so cells left of
427// or above it are addressed with negative col/row and must survive the format unchanged.
428BOOST_AUTO_TEST_CASE( NegativeGridCellsRoundTrip )
429{
430 BOARD board;
431 board.Add( new NETINFO_ITEM( &board, wxT( "GND" ), 1 ) );
432
433 PCB_VIA_STITCH stitch;
434 stitch.SetParent( &board );
435 configureStitch( &stitch );
437
438 stitch.ExcludePosition( VECTOR2I( pcbIUScale.mmToIU( -4 ), pcbIUScale.mmToIU( -6 ) ) );
439
440 PCB_VIA_STITCH loaded;
441 loaded.SetParent( &board );
442 loaded.SetProperties( stitch.GetProperties() );
443
444 auto cells = loaded.GetProperties().get_opt<std::vector<VECTOR2I>>( "excluded_grid_cells" );
445
446 BOOST_REQUIRE( cells.has_value() );
447 BOOST_REQUIRE_EQUAL( cells->size(), 1 );
448 BOOST_CHECK_EQUAL( cells->front(), VECTOR2I( -2, -3 ) );
449}
450
451
452// Shared plumbing for driving a stitch generator through Update(). Derived fixtures supply
453// the board and point m_stitch at the generator sitting on it.
455{
458 {
459 m_toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, nullptr );
461 m_toolMgr.RegisterTool( m_dummyTool );
462 }
463
465 {
466 BOARD_COMMIT commit( m_dummyTool );
467
468 m_stitch->EditStart( nullptr, m_board.get(), &commit );
469 m_stitch->Update( nullptr, m_board.get(), &commit );
470 m_stitch->EditFinish( nullptr, m_board.get(), &commit );
471
472 commit.Push( wxT( "regen" ), SKIP_UNDO | SKIP_SET_DIRTY | SKIP_CONNECTIVITY );
473 }
474
475 std::set<KIID> childViaIds() const
476 {
477 std::set<KIID> ids;
478
479 for( BOARD_ITEM* item : m_stitch->GetBoardItems() )
480 {
481 if( item->Type() == PCB_VIA_T )
482 ids.insert( item->m_Uuid );
483 }
484
485 return ids;
486 }
487
488 std::set<VECTOR2I> childViaPositions() const
489 {
490 std::set<VECTOR2I> positions;
491
492 for( BOARD_ITEM* item : m_stitch->GetBoardItems() )
493 {
494 if( item->Type() == PCB_VIA_T )
495 positions.insert( item->GetPosition() );
496 }
497
498 return positions;
499 }
500
501 std::unique_ptr<BOARD> m_board;
505};
506
507
508// An empty board with a GND net and a live DRC engine. Derived fixtures pour the copper they
509// need, then call finishSetup() to fill it and attach a stitch generator.
511{
513 {
514 m_board = std::make_unique<BOARD>();
515 m_board->Add( new NETINFO_ITEM( m_board.get(), wxT( "GND" ), 1 ) );
516
517 auto drcEngine =
518 std::make_shared<DRC_ENGINE>( m_board.get(), &m_board->GetDesignSettings() );
519 drcEngine->InitEngine( wxFileName() );
520 m_board->GetDesignSettings().m_DRCEngine = drcEngine;
521 }
522
524 void addGndZone( const LSET& aLayers, int aLeft, int aRight )
525 {
526 ZONE* zone = new ZONE( m_board.get() );
527 zone->SetLayerSet( aLayers );
528 zone->SetNetCode( 1 );
529 zone->Outline()->NewOutline();
530 zone->Outline()->Append( aLeft, 0 );
531 zone->Outline()->Append( aRight, 0 );
532 zone->Outline()->Append( aRight, pcbIUScale.mmToIU( 20 ) );
533 zone->Outline()->Append( aLeft, pcbIUScale.mmToIU( 20 ) );
534 m_board->Add( zone );
535 }
536
539 {
541
542 m_stitch = new PCB_VIA_STITCH();
543 m_board->Add( m_stitch );
546
548 }
549};
550
551
552// A GND pour on both outer layers, covering the whole board
554{
556 {
557 addGndZone( LSET( { F_Cu, B_Cu } ), 0, pcbIUScale.mmToIU( 20 ) );
558 finishSetup();
559 }
560};
561
562
563// A 4-layer board whose two inner planes cover everything, with an F_Cu pour that only
564// reaches the left half of them
566{
568 {
569 m_board->SetCopperLayerCount( 4 );
570 m_board->GetDesignSettings().SetCopperLayerCount( 4 );
571 m_board->SetEnabledLayers( m_board->GetEnabledLayers() | LSET::AllCuMask( 4 ) );
572
573 addGndZone( LSET( { In1_Cu, In2_Cu } ), 0, pcbIUScale.mmToIU( 20 ) );
574 addGndZone( LSET( { F_Cu } ), 0, pcbIUScale.mmToIU( 10 ) );
575
576 finishSetup();
577 }
578};
579
580
581// Loads a saved board out of qa/data/pcbnew and picks up the via-stitch generator on it, so a
582// real design's zones, netclasses and design settings drive the placement.
584{
586 {
587 // The board borrows the project owned by m_settingsManager, which is destroyed right
588 // after this body runs.
589 if( m_board )
590 {
591 m_board->SetProject( nullptr );
592 m_board = nullptr;
593 }
594 }
595
596 void loadBoard( const wxString& aRelPath )
597 {
600
602
603 for( PCB_GENERATOR* generator : m_board->Generators() )
604 {
605 if( PCB_VIA_STITCH* stitch = dynamic_cast<PCB_VIA_STITCH*>( generator ) )
606 {
607 BOOST_REQUIRE_MESSAGE( !m_stitch, "more than one stitch generator on the board" );
608 m_stitch = stitch;
609 }
610 }
611
612 BOOST_REQUIRE_MESSAGE( m_stitch, "no via-stitch generator on the board" );
613
615 }
616
618};
619
620
621// Round-tripping the layout through POISSON and back must land the grid exactly where it
622// started. The origin re-anchoring heuristic in Update() treats any child via that isn't on
623// the grid as a user-dragged via defining a new origin — and after a POISSON pass every child
624// is off-grid, so it would silently re-anchor the whole grid to an arbitrary Poisson via.
625BOOST_FIXTURE_TEST_CASE( LayoutRoundTripKeepsGridAnchored, STITCH_UPDATE_FIXTURE )
626{
627 regenerate();
628
629 std::set<VECTOR2I> before = childViaPositions();
630 BOOST_REQUIRE_GT( before.size(), 10 );
631
632 // Out to POISSON and back. Exclusions are deliberately not involved: they are cleared by
633 // the layout change, whereas the grid anchor must be completely unaffected by it.
634 m_stitch->SetLayout( PCB_VIA_STITCH_LAYOUT::POISSON );
635 regenerate();
636
637 m_stitch->SetLayout( PCB_VIA_STITCH_LAYOUT::PLAIN );
638 regenerate();
639
640 std::set<VECTOR2I> after = childViaPositions();
641
642 BOOST_CHECK_EQUAL( after.size(), before.size() );
643
644 for( const VECTOR2I& pos : before )
645 BOOST_CHECK_EQUAL( after.count( pos ), 1 );
646}
647
648
649// Same root cause as the layout round trip: after a pitch change every existing via is off the
650// new grid, and the re-anchoring heuristic would treat the first one as a deliberate drag.
651// Changing the pitch must keep the grid anchored where it was.
652BOOST_FIXTURE_TEST_CASE( PitchChangeKeepsGridAnchored, STITCH_UPDATE_FIXTURE )
653{
654 regenerate();
655 BOOST_REQUIRE_GT( childViaPositions().size(), 10 );
656
657 // Out to a different pitch and back
658 m_stitch->SetPitch( pcbIUScale.mmToIU( 3 ) );
659 regenerate();
660
661 std::set<VECTOR2I> coarse = childViaPositions();
662 BOOST_REQUIRE( !coarse.empty() );
663
664 m_stitch->SetPitch( pcbIUScale.mmToIU( 2 ) );
665 regenerate();
666
667 // Every via must sit on the original 2mm lattice, i.e. at an exact multiple of the pitch
668 for( const VECTOR2I& pos : childViaPositions() )
669 {
670 BOOST_CHECK_EQUAL( pos.x % pcbIUScale.mmToIU( 2 ), 0 );
671 BOOST_CHECK_EQUAL( pos.y % pcbIUScale.mmToIU( 2 ), 0 );
672 }
673}
674
675
677{
678 regenerate();
679
680 std::set<KIID> first = childViaIds();
681
682 // The 16mm x 16mm outline at 2mm pitch should have 10 vias
683 BOOST_REQUIRE_GT( first.size(), 10 );
684
685 // Vias must respect the pitch
686 std::vector<VECTOR2I> positions;
687
688 for( BOARD_ITEM* item : m_stitch->GetBoardItems() )
689 {
690 if( item->Type() == PCB_VIA_T )
691 positions.push_back( item->GetPosition() );
692 }
693
694 const int64_t minDistSq =
695 (int64_t) pcbIUScale.mmToIU( 2 ) * pcbIUScale.mmToIU( 2 ) - 1;
696
697 for( size_t i = 0; i < positions.size(); ++i )
698 {
699 for( size_t j = i + 1; j < positions.size(); ++j )
700 {
701 VECTOR2I d = positions[i] - positions[j];
702 int64_t distSq = (int64_t) d.x * d.x + (int64_t) d.y * d.y;
703
704 BOOST_CHECK_MESSAGE( distSq >= minDistSq,
705 "vias " << i << " and " << j << " closer than the pitch" );
706 }
707 }
708
709 // Regenerating an unchanged board, none of the vias should change
710 regenerate();
711
712 std::set<KIID> second = childViaIds();
713
714 BOOST_CHECK( first == second );
715}
716
717
719{
720 regenerate();
721
722 std::set<KIID> before = childViaIds();
723 BOOST_REQUIRE_GT( before.size(), 10 );
724
725 // Exclude one existing via's position: regeneration must remove exactly that via and
726 // leave every other via untouched
727 PCB_VIA* victim = nullptr;
728
729 for( BOARD_ITEM* item : m_stitch->GetBoardItems() )
730 {
731 if( item->Type() == PCB_VIA_T )
732 {
733 victim = static_cast<PCB_VIA*>( item );
734 break;
735 }
736 }
737
738 BOOST_REQUIRE( victim );
739
740 KIID victimId = victim->m_Uuid;
741 VECTOR2I victimPos = victim->GetPosition();
742
743 m_stitch->ExcludePosition( victimPos );
744 regenerate();
745
746 std::set<KIID> after = childViaIds();
747
748 BOOST_CHECK_EQUAL( after.size(), before.size() - 1 );
749 BOOST_CHECK_EQUAL( after.count( victimId ), 0 );
750
751 for( const KIID& id : after )
752 BOOST_CHECK_EQUAL( before.count( id ), 1 );
753
754 // Clearing the exclusion brings a via back at that position (with a new identity)
755 m_stitch->ClearExclusion( victimPos );
756 regenerate();
757
758 std::set<KIID> restored = childViaIds();
759
760 BOOST_CHECK_EQUAL( restored.size(), before.size() );
761
762 bool found = false;
763
764 for( BOARD_ITEM* item : m_stitch->GetBoardItems() )
765 {
766 if( item->Type() == PCB_VIA_T && item->GetPosition() == victimPos )
767 found = true;
768 }
769
770 BOOST_CHECK( found );
771}
772
773
774BOOST_FIXTURE_TEST_CASE( ClearAllExclusionsRestoresEveryVia, STITCH_UPDATE_FIXTURE )
775{
776 regenerate();
777
778 std::set<KIID> before = childViaIds();
779 BOOST_REQUIRE_GT( before.size(), 10 );
780
781 std::vector<VECTOR2I> victimPositions;
782
783 for( BOARD_ITEM* item : m_stitch->GetBoardItems() )
784 {
785 if( item->Type() == PCB_VIA_T && victimPositions.size() < 3 )
786 victimPositions.push_back( item->GetPosition() );
787 }
788
789 BOOST_REQUIRE_EQUAL( victimPositions.size(), 3 );
790 BOOST_CHECK( !m_stitch->HasExclusions() );
791
792 for( const VECTOR2I& pos : victimPositions )
793 m_stitch->ExcludePosition( pos );
794
795 BOOST_CHECK( m_stitch->HasExclusions() );
796
797 regenerate();
798 BOOST_CHECK_EQUAL( childViaIds().size(), before.size() - 3 );
799
800 m_stitch->ClearAllExclusions();
801 BOOST_CHECK( !m_stitch->HasExclusions() );
802
803 regenerate();
804 BOOST_CHECK_EQUAL( childViaIds().size(), before.size() );
805
806 // Every excluded position must be occupied again
807 for( const VECTOR2I& pos : victimPositions )
808 {
809 bool found = false;
810
811 for( BOARD_ITEM* item : m_stitch->GetBoardItems() )
812 {
813 if( item->Type() == PCB_VIA_T && item->GetPosition() == pos )
814 found = true;
815 }
816
817 BOOST_CHECK( found );
818 }
819}
820
821// A via needs same-net copper on two of the layers it spans, not on every one of them. The
822// two inner planes overlap across the whole stitch outline and must be stitched to each other
823// everywhere, including the right half that the F_Cu pour doesn't reach.
824BOOST_FIXTURE_TEST_CASE( PartialOuterPourStillStitchesInnerPlanes, STITCH_PARTIAL_LAYER_FIXTURE )
825{
826 regenerate();
827
828 std::set<VECTOR2I> positions = childViaPositions();
829 BOOST_REQUIRE_GT( positions.size(), 10 );
830
831 bool overThreeLayers = false; // F_Cu plus both planes
832 bool overTwoLayers = false; // the planes alone
833
834 for( const VECTOR2I& pos : positions )
835 {
836 if( pos.x < pcbIUScale.mmToIU( 8 ) )
837 overThreeLayers = true;
838 else if( pos.x > pcbIUScale.mmToIU( 12 ) )
839 overTwoLayers = true;
840 }
841
842 BOOST_CHECK( overThreeLayers );
843 BOOST_CHECK_MESSAGE( overTwoLayers,
844 "no vias placed where only the two inner planes overlap" );
845}
846
847
848// The same partial-coverage case on a real board (issue 25303): GND on both inner planes
849// across the whole area, a small GND pour on F.Cu, and a stitch outline running well past the
850// right edge of that pour. The stitching has to carry on across the inner planes alone.
851BOOST_FIXTURE_TEST_CASE( PartialTopPourStillStitchesInnerPlanes, STITCH_BOARD_FIXTURE )
852{
853 loadBoard( wxT( "issue25303" ) );
854
855 // The saved board carries the generator but none of its vias
856 BOOST_REQUIRE( childViaPositions().empty() );
857
858 regenerate();
859
860 std::set<VECTOR2I> positions = childViaPositions();
861 BOOST_REQUIRE_GT( positions.size(), 100 );
862
863 // The F.Cu pour runs stops at about x = 107mm; the inner planes and the stitch outline both
864 // carry on past x = 124mm.
865 const int pourEdge = pcbIUScale.mmToIU( 107 );
866
867 int underPour = 0;
868 int beyondPour = 0;
869 int maxX = INT_MIN;
870
871 for( const VECTOR2I& pos : positions )
872 {
873 if( pos.x < pourEdge )
874 underPour++;
875 else
876 beyondPour++;
877
878 maxX = std::max( maxX, pos.x );
879 }
880
881 BOOST_CHECK_GT( underPour, 20 );
882 BOOST_CHECK_MESSAGE( beyondPour > 20, "stitching stopped at the edge of the F.Cu pour" );
883
884 // ...and it has to reach the far side of the outline, not just spill over the pour edge
885 BOOST_CHECK_GT( maxX, pcbIUScale.mmToIU( 122 ) );
886
887 // Every via belongs to the stitch net and sits inside the outline
888 const SHAPE_POLY_SET& outline = m_stitch->Outline();
889
890 for( BOARD_ITEM* item : m_stitch->GetBoardItems() )
891 {
892 BOOST_REQUIRE_EQUAL( item->Type(), PCB_VIA_T );
893
894 PCB_VIA* via = static_cast<PCB_VIA*>( item );
895
896 BOOST_CHECK_EQUAL( via->GetNetCode(), m_stitch->GetNetCode() );
897 BOOST_CHECK( outline.Contains( via->GetPosition() ) );
898 }
899}
900
901// Pads have to be avoided in their own right, not just when they happen to be cross-net copper.
902// The GND pour on this board (issue 25265) connects to pads solidly instead of through thermal
903// reliefs, so it floods straight over its own pads -- and a stitch position sitting on one of
904// them looks like any other patch of same-net zone fill. Vias used to land right on top.
905BOOST_FIXTURE_TEST_CASE( PadsBlockStitchingWithSolidZoneConnections, STITCH_BOARD_FIXTURE )
906{
907 loadBoard( wxT( "issue25265" ) );
908
909 // The saved board carries the generator but none of its vias
910 BOOST_REQUIRE( childViaPositions().empty() );
911
912 // Guard the premise: the pour has to be solid-connected, and it has to have pads of its own
913 // sitting under the stitch outline, or the case being tested isn't on the board any more.
914 for( ZONE* zone : m_board->Zones() )
915 BOOST_REQUIRE_EQUAL( (int) zone->GetPadConnection(), (int) ZONE_CONNECTION::FULL );
916
917 const std::vector<PAD*> pads = m_board->GetPads();
918 int sameNetPadsInside = 0;
919
920 for( PAD* pad : pads )
921 {
922 if( pad->GetNetCode() == m_stitch->GetNetCode()
923 && m_stitch->Outline().Contains( pad->GetPosition() ) )
924 {
925 sameNetPadsInside++;
926 }
927 }
928
929 BOOST_REQUIRE_MESSAGE( sameNetPadsInside > 0,
930 "no pads on the stitch net sit inside the stitch outline" );
931
932 regenerate();
933
934 BOOST_REQUIRE_GT( childViaPositions().size(), 100 );
935
936 for( BOARD_ITEM* item : m_stitch->GetBoardItems() )
937 {
938 BOOST_REQUIRE_EQUAL( item->Type(), PCB_VIA_T );
939
940 PCB_VIA* via = static_cast<PCB_VIA*>( item );
941
942 for( PCB_LAYER_ID layer : { F_Cu, B_Cu } )
943 {
944 SHAPE_CIRCLE viaCopper( via->GetPosition(), via->GetWidth( layer ) / 2 );
945
946 for( PAD* pad : pads )
947 {
948 if( !pad->FlashLayer( layer ) )
949 continue;
950
951 BOOST_CHECK_MESSAGE(
952 !pad->GetEffectiveShape( layer )->Collide( &viaCopper ),
953 wxString::Format( "Via at (%.3f, %.3f) sits on pad %s of %s (net %s)",
954 pcbIUScale.IUTomm( via->GetPosition().x ),
955 pcbIUScale.IUTomm( via->GetPosition().y ),
956 pad->GetNumber(),
957 pad->GetParentFootprint()->GetReferenceAsString(),
958 pad->GetNetname() ) );
959 }
960 }
961 }
962}
963
964// Validate that footprints containing no via rule areas are accounted for.
965BOOST_FIXTURE_TEST_CASE( FootprintRuleAreaBlocksStitchVias, STITCH_BOARD_FIXTURE )
966{
967 loadBoard( wxT( "stitch_fp_rulearea" ) );
968
969 ZONE* ruleArea = nullptr;
970
971 for( FOOTPRINT* footprint : m_board->Footprints() )
972 {
973 for( ZONE* zone : footprint->Zones() )
974 {
975 if( zone->GetIsRuleArea() && zone->GetDoNotAllowVias() )
976 {
977 BOOST_REQUIRE_MESSAGE( !ruleArea, "more than one via keepout on the board" );
978 ruleArea = zone;
979 }
980 }
981 }
982
983 BOOST_REQUIRE_MESSAGE( ruleArea, "no footprint via keepout on the board" );
984
985 SHAPE_POLY_SET keepout = ruleArea->GetBoardOutline();
986 keepout.BuildBBoxCaches();
987
988 // Sanity: The keepout has to sit wholly inside the stitch outline, so a via
989 // missing from it can only be the keepout's doing...
990 SHAPE_POLY_SET outsideStitch = keepout;
991 outsideStitch.BooleanSubtract( m_stitch->Outline() );
992
993 BOOST_REQUIRE_MESSAGE( outsideStitch.IsEmpty(),
994 "keepout is not wholly inside the stitch outline" );
995
996 // Sanity: The footprint-frame outline has to land somewhere else entirely, or reading the
997 // wrong frame would happen to give the right answer and the test would prove nothing.
998 BOOST_REQUIRE_MESSAGE(
999 !keepout.BBox().Intersects( ruleArea->GetLibraryOutline().BBox() ),
1000 "footprint frame and board frame overlap; test can't discriminate" );
1001
1002 regenerate();
1003
1004 const std::set<VECTOR2I> positions = childViaPositions();
1005
1006 BOOST_REQUIRE_GT( positions.size(), 20 );
1007
1008 for( const VECTOR2I& pos : positions )
1009 {
1010 BOOST_CHECK_MESSAGE(
1011 !keepout.Contains( pos, -1, 0, true ),
1012 wxString::Format( "Via at (%.3f, %.3f) sits inside the footprint via keepout",
1013 pcbIUScale.IUTomm( pos.x ), pcbIUScale.IUTomm( pos.y ) ) );
1014 }
1015}
1016
constexpr EDA_IU_SCALE pcbIUScale
Definition base_units.h:121
General utilities for PCB file IO for QA programs.
virtual void Push(const wxString &aMessage=wxEmptyString, int aCommitFlags=0) override
Execute the changes.
A base class for any item which can be embedded within the BOARD container class, and therefore insta...
Definition board_item.h:84
Information pertinent to a Pcbnew printed circuit board.
Definition board.h:409
void Add(BOARD_ITEM *aItem, ADD_MODE aMode=ADD_MODE::INSERT, bool aSkipConnectivity=false) override
Removes an item from the container.
Definition board.cpp:1497
constexpr bool Intersects(const BOX2< Vec > &aRect) const
Definition box2.h:308
const KIID m_Uuid
Definition eda_item.h:597
virtual void SetParent(EDA_ITEM *aParent)
Definition eda_item.cpp:153
Definition kiid.h:46
LSET is a set of PCB_LAYER_IDs.
Definition lset.h:37
static LSET AllCuMask(int aCuLayerCount)
Return a mask holding the requested number of Cu PCB_LAYER_IDs.
Definition lset.cpp:595
Handle the data for a net.
Definition netinfo.h:50
int GetNetCode() const
Definition netinfo.h:104
static constexpr PCB_LAYER_ID ALL_LAYERS
! The layer identifier to use for the single defintion on normal padstacks
Definition padstack.h:179
Definition pad.h:61
void SetOutline(const SHAPE_POLY_SET &aOutline)
const SHAPE_POLY_SET & Outline() const
int GetViaSize() const
static std::vector< VECTOR2I > SampleGuardEnvelope(const SHAPE_POLY_SET &aEnvelope, const SHAPE_POLY_SET &aGuarded, int aPitch, const std::function< bool(const VECTOR2I &)> &aIsValid)
Determine via positions around the perimeter of a guard envelope.
PCB_VIA * ViaTemplate()
int GetViaDrill() const
int GetNetCode() const
void SetMode(PCB_VIA_STITCH_MODE aVal)
PCB_VIA_STITCH_LAYOUT GetLayout() const
void SetPitch(int aVal)
static constexpr uint32_t POISSON_TILE_SEED
Fixed seed for baking the toroidal Poisson pattern.
bool HasExclusions() const
const STRING_ANY_MAP GetProperties() const override
PCB_VIA_STITCH_MODE GetMode() const
static const std::vector< VECTOR2D > & bakedPoissonTile()
Helper method to cache the poisson tile we pattern.
static constexpr int POISSON_TILE_PITCHES
Tile size for the POISSON layout, in units of pitch.
void SetNetCode(int aNetCode)
int GetPitch() const
uint32_t GetSeed() const
void SetProperties(const STRING_ANY_MAP &aProps) override
void SetLayout(PCB_VIA_STITCH_LAYOUT aVal)
void ExcludePosition(const VECTOR2I &aPos)
Add a board position to the exclusion set so the next Update() skips it.
void SetSeed(uint32_t aVal)
VECTOR2I GetPosition() const override
Definition pcb_track.h:580
void SetDrill(int aDrill)
Definition pcb_track.h:771
void SetWidth(int aWidth) override
Represent a polyline containing arcs as well as line segments: A chain of connected line and/or arc s...
Represent a set of closed polygons.
bool IsEmpty() const
Return true if the set is empty (no polygons at all)
bool Collide(const SHAPE *aShape, int aClearance=0, int *aActual=nullptr, VECTOR2I *aLocation=nullptr) const override
Check if the boundary of shape (this) lies closer to the shape aShape than aClearance,...
int Append(int x, int y, int aOutline=-1, int aHole=-1, bool aAllowDuplication=false)
Appends a vertex at the end of the given outline/hole (default: the last outline)
SHAPE_LINE_CHAIN & Outline(int aIndex)
Return the reference to aIndex-th outline in the set.
int NewOutline()
Creates a new empty polygon in the set and returns its index.
void BuildBBoxCaches() const
Construct BBoxCaches for Contains(), below.
int OutlineCount() const
Return the number of outlines in the set.
bool Contains(const VECTOR2I &aP, int aSubpolyIndex=-1, int aAccuracy=0, bool aUseBBoxCaches=false) const
Return true if a given subpolygon contains the point aP.
void BooleanSubtract(const SHAPE_POLY_SET &b)
Perform boolean polyset difference.
const SHAPE_LINE_CHAIN & COutline(int aIndex) const
const BOX2I BBox(int aClearance=0) const override
Compute a bounding box of the shape, with a margin of aClearance a collision.
A name/value tuple with unique names and wxAny values.
std::optional< T > get_opt(const std::string &aKey) const
Master controller class:
Handle a list of polygons defining a copper zone.
Definition zone.h:70
virtual void SetLayer(PCB_LAYER_ID aLayer) override
Set the layer this item is on.
Definition zone.cpp:641
SHAPE_POLY_SET * Outline()
Definition zone.h:418
bool SetNetCode(int aNetCode, bool aNoAssert) override
Override that clamps the netcode to 0 when this zone is in copper-thieving fill mode.
Definition zone.cpp:623
SHAPE_POLY_SET GetBoardOutline() const
Definition zone.cpp:896
void SetLayerSet(const LSET &aLayerSet) override
Definition zone.cpp:666
SHAPE_POLY_SET GetLibraryOutline() const
Definition zone.cpp:890
static bool empty(const wxTextEntryBase *aCtrl)
PCB_LAYER_ID
A quick note on layer IDs:
Definition layer_ids.h:56
@ B_Cu
Definition layer_ids.h:61
@ In2_Cu
Definition layer_ids.h:63
@ In1_Cu
Definition layer_ids.h:62
@ F_Cu
Definition layer_ids.h:60
void LoadBoard(SETTINGS_MANAGER &aSettingsManager, const wxString &aRelPath, std::unique_ptr< BOARD > &aBoard)
void FillZones(BOARD *m_board)
std::unique_ptr< BOARD > ReadBoardFromFileOrStream(const std::string &aFilename, std::istream &aFallback)
Read a board from a file, or another stream, as appropriate.
void DumpBoardToFile(BOARD &board, const std::filesystem::path &aFilename)
Utility function to simply write a Board out to a file.
@ STITCH
Fill the outline with vias with a pattern.
@ GUARD
Place vias to guard a net contained within.
@ POISSON
Tiled poisson distribution.
@ STAGGERED
Odd rows shifted by half the pitch.
@ PLAIN
Regular row/column grid.
#define SKIP_CONNECTIVITY
Definition sch_commit.h:41
#define SKIP_SET_DIRTY
Definition sch_commit.h:40
#define SKIP_UNDO
Definition sch_commit.h:38
SETTINGS_MANAGER m_settingsManager
void loadBoard(const wxString &aRelPath)
void attachToolManager()
The BOARD_COMMITs in regenerate() need a tool to hang off.
PCB_VIA_STITCH * m_stitch
std::set< VECTOR2I > childViaPositions() const
std::set< KIID > childViaIds() const
KI_TEST::DUMMY_TOOL * m_dummyTool
std::unique_ptr< BOARD > m_board
void addGndZone(const LSET &aLayers, int aLeft, int aRight)
Add a full-height GND pour on aLayers, spanning x from aLeft to aRight.
void finishSetup()
Fill the poured zones and attach a stitch generator ready to Update().
BOOST_AUTO_TEST_SUITE(CadstarPartParser)
BOOST_REQUIRE(intersection.has_value()==c.ExpectedIntersection.has_value())
BOOST_AUTO_TEST_SUITE_END()
std::string path
VECTOR3I expected(15, 30, 45)
const SHAPE_LINE_CHAIN chain
int actual
BOOST_CHECK_EQUAL(result, "25.4")
BOOST_AUTO_TEST_CASE(BakedPatternConstantsLocked)
static void checkOutlinesEqual(const SHAPE_POLY_SET &aExpected, const SHAPE_POLY_SET &aActual)
static void configureStitch(PCB_VIA_STITCH *aStitch)
BOOST_FIXTURE_TEST_CASE(LayoutRoundTripKeepsGridAnchored, STITCH_UPDATE_FIXTURE)
static SHAPE_POLY_SET makeRectPoly(int aX1, int aY1, int aX2, int aY2)
@ PCB_VIA_T
class PCB_VIA, a via (like a track segment on a copper layer)
Definition typeinfo.h:89
VECTOR2< int32_t > VECTOR2I
Definition vector2d.h:683
@ FULL
pads are covered by copper
Definition zones.h:47