KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_allegro_import.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
24
25#include "allegro_test_utils.h"
29
31
32#include <board.h>
34#include <footprint.h>
35#include <pad.h>
36#include <pcb_shape.h>
37#include <pcb_text.h>
38#include <pcb_track.h>
39#include <zone.h>
41#include <netinfo.h>
42#include <netclass.h>
45#include <reporter.h>
46
47#include <algorithm>
48#include <cstdlib>
49#include <filesystem>
50#include <functional>
51#include <fstream>
52#include <map>
53#include <set>
54
55using namespace KI_TEST;
56
57
59{
61
62 std::unique_ptr<BOARD> LoadAllegroBoard( const std::string& aFileName )
63 {
64 std::string dataPath = KI_TEST::AllegroBoardFile( aFileName );
65
66 return m_allegroPlugin.LoadBoard( dataPath );
67 }
68
70};
71
72
73BOOST_FIXTURE_TEST_SUITE( AllegroImport, ALLEGRO_IMPORT_FIXTURE )
74
75
76
79BOOST_AUTO_TEST_CASE( FootprintRefDes )
80{
81 std::unique_ptr<BOARD> board = LoadAllegroBoard( "TRS80_POWER/TRS80_POWER.brd" );
82
83 BOOST_REQUIRE( board != nullptr );
84
85 int emptyRefDesCount = 0;
86 int validRefDesCount = 0;
87
88 for( FOOTPRINT* fp : board->Footprints() )
89 {
90 wxString refdes = fp->GetReference();
91
92 if( refdes.IsEmpty() )
93 emptyRefDesCount++;
94 else
95 validRefDesCount++;
96 }
97
98 BOOST_TEST_MESSAGE( "Valid RefDes: " << validRefDesCount << ", Empty: " << emptyRefDesCount );
99
100 // Most footprints should have valid reference designators
101 BOOST_CHECK_GT( validRefDesCount, emptyRefDesCount );
102}
103
104
108BOOST_AUTO_TEST_CASE( UnreferencedFootprintsAreBoardOnly )
109{
110 std::unique_ptr<BOARD> board = LoadAllegroBoard( "TRS80_POWER/TRS80_POWER.brd" );
111
112 BOOST_REQUIRE( board != nullptr );
113
114 int unreferenced = 0;
115
116 for( FOOTPRINT* fp : board->Footprints() )
117 {
118 if( !fp->GetReference().IsEmpty() )
119 continue;
120
121 unreferenced++;
122 BOOST_CHECK( fp->GetAttributes() & FP_BOARD_ONLY );
123 }
124
125 BOOST_CHECK_GT( unreferenced, 0 );
126}
127
128
132BOOST_AUTO_TEST_CASE( FootprintLibItemNames )
133{
134 std::unique_ptr<BOARD> board = LoadAllegroBoard( "TRS80_POWER/TRS80_POWER.brd" );
135
136 BOOST_REQUIRE( board != nullptr );
137
138 int unnamed = 0;
139
140 for( FOOTPRINT* fp : board->Footprints() )
141 {
142 BOOST_TEST_MESSAGE( fp->GetReference() << " -> " << fp->GetFPID().GetLibItemName().wx_str() );
143
144 if( !fp->GetReference().IsEmpty() && fp->GetFPID().GetLibItemName().empty() )
145 unnamed++;
146
147 BOOST_CHECK( fp->GetFPID().GetLibNickname().empty() );
148 }
149
150 BOOST_CHECK_EQUAL( unnamed, 0 );
151}
152
153
157BOOST_AUTO_TEST_CASE( UnplatedHolesHaveSize )
158{
159 std::unique_ptr<BOARD> board = LoadAllegroBoard( "EVK_BaseBoard/EVK_BaseBoard.brd" );
160
161 BOOST_REQUIRE( board != nullptr );
162
163 int zeroSized = 0;
164
165 for( FOOTPRINT* fp : board->Footprints() )
166 {
167 for( PAD* pad : fp->Pads() )
168 {
169 pad->Padstack().ForEachUniqueLayer(
170 [&]( PCB_LAYER_ID layer )
171 {
172 if( pad->GetSize( layer ).x <= 0 || pad->GetSize( layer ).y <= 0 )
173 zeroSized++;
174 } );
175 }
176 }
177
178 BOOST_CHECK_EQUAL( zeroSized, 0 );
179}
180
181
185BOOST_AUTO_TEST_CASE( NearStraightArcsSurviveReorientation )
186{
187 const char* corpusEnv = std::getenv( "KICAD_ORCAD_CORPUS" );
188
189 if( !corpusEnv || !*corpusEnv )
190 return;
191
192 std::filesystem::path brd;
193
194 for( const std::filesystem::directory_entry& entry : std::filesystem::recursive_directory_iterator( corpusEnv ) )
195 {
196 if( entry.is_regular_file() && entry.path().filename() == "EDA-02862-V1-0.brd" )
197 brd = entry.path();
198 }
199
200 if( brd.empty() )
201 return;
202
203 std::unique_ptr<BOARD> board = m_allegroPlugin.LoadBoard( brd.string() );
204
205 BOOST_REQUIRE( board != nullptr );
206
207 for( FOOTPRINT* fp : board->Footprints() )
208 {
209 std::unique_ptr<FOOTPRINT> copy( static_cast<FOOTPRINT*>( fp->Clone() ) );
210 copy->SetOrientation( ANGLE_0 );
211 }
212}
213
214
218BOOST_AUTO_TEST_CASE( Release161Loads )
219{
220 const char* corpusEnv = std::getenv( "KICAD_ORCAD_CORPUS" );
221
222 if( !corpusEnv || !*corpusEnv )
223 return;
224
225 std::filesystem::path brd;
226
227 for( const std::filesystem::directory_entry& entry : std::filesystem::recursive_directory_iterator( corpusEnv ) )
228 {
229 if( entry.is_regular_file() && entry.path().filename() == "EDA-02152-V1-0.brd" )
230 brd = entry.path();
231 }
232
233 if( brd.empty() )
234 return;
235
236 std::unique_ptr<BOARD> board = m_allegroPlugin.LoadBoard( brd.string() );
237
238 BOOST_REQUIRE( board != nullptr );
239 BOOST_CHECK_EQUAL( board->Footprints().size(), 61u );
240}
241
242
246BOOST_AUTO_TEST_CASE( PlaneLayerLinesAreNotCopper )
247{
248 std::unique_ptr<BOARD> board = LoadAllegroBoard( "EVK_BaseBoard/EVK_BaseBoard.brd" );
249
250 BOOST_REQUIRE( board != nullptr );
251
252 int copperLines = 0;
253
254 for( BOARD_ITEM* item : board->Drawings() )
255 {
256 if( item->Type() == PCB_SHAPE_T && IsInnerCopperLayer( item->GetLayer() ) )
257 copperLines++;
258 }
259
260 BOOST_CHECK_EQUAL( copperLines, 0 );
261}
262
263
268BOOST_AUTO_TEST_CASE( PlaneLayerLinesAreNotCopperBefore165 )
269{
270 const char* corpusEnv = std::getenv( "KICAD_ORCAD_CORPUS" );
271
272 if( !corpusEnv || !*corpusEnv )
273 return;
274
275 std::filesystem::path brd;
276
277 for( const std::filesystem::directory_entry& entry : std::filesystem::recursive_directory_iterator( corpusEnv ) )
278 {
279 if( entry.is_regular_file() && entry.path().filename() == "EDA-02152-V1-0.brd" )
280 brd = entry.path();
281 }
282
283 if( brd.empty() )
284 return;
285
286 std::unique_ptr<BOARD> board = m_allegroPlugin.LoadBoard( brd.string() );
287
288 BOOST_REQUIRE( board != nullptr );
289
290 int copperLines = 0;
291
292 for( BOARD_ITEM* item : board->Drawings() )
293 {
294 if( item->Type() == PCB_SHAPE_T && IsInnerCopperLayer( item->GetLayer() )
295 && static_cast<PCB_SHAPE*>( item )->GetShape() == SHAPE_T::SEGMENT )
296 {
297 copperLines++;
298 }
299 }
300
301 BOOST_CHECK_EQUAL( copperLines, 0 );
302}
303
304
308BOOST_AUTO_TEST_CASE( OverlappingPadsAreNetTie )
309{
310 const char* corpusEnv = std::getenv( "KICAD_ORCAD_CORPUS" );
311
312 if( !corpusEnv || !*corpusEnv )
313 return;
314
315 std::filesystem::path brd;
316
317 for( const std::filesystem::directory_entry& entry : std::filesystem::recursive_directory_iterator( corpusEnv ) )
318 {
319 if( entry.is_regular_file() && entry.path().filename() == "USB-PD w QC3_0.brd" )
320 brd = entry.path();
321 }
322
323 if( brd.empty() )
324 return;
325
326 std::unique_ptr<BOARD> board = m_allegroPlugin.LoadBoard( brd.string() );
327
328 BOOST_REQUIRE( board != nullptr );
329
330 FOOTPRINT* tie = board->FindFootprintByReference( wxS( "NT1" ) );
331
332 BOOST_REQUIRE( tie != nullptr );
333 BOOST_REQUIRE_EQUAL( tie->GetNetTiePadGroups().size(), 1 );
334 BOOST_CHECK_EQUAL( tie->GetNetTiePadGroups()[0], wxS( "1,2" ) );
335}
336
337
341BOOST_AUTO_TEST_CASE( V164PadstackMaskAndPaste )
342{
343 const char* corpusEnv = std::getenv( "KICAD_ORCAD_CORPUS" );
344
345 if( !corpusEnv || !*corpusEnv )
346 return;
347
348 // EDA-02016 has 8 NOPM_* SMD padstacks, 4 of them also NOSM_*. EDA-03470 has 4 NOPM_* and 6 fiducials
349 // without paste, and its 7 bottom-start pads keep their opening in the bottom mask slot.
350 const std::map<std::string, std::pair<int, int>> expected = { { "EDA-02016-V8-0.brd", { 43, 4 } },
351 { "EDA-03470-V1-0.brd", { 138, 0 } } };
352
353 std::map<std::string, std::filesystem::path> boards;
354
355 for( const std::filesystem::directory_entry& entry : std::filesystem::recursive_directory_iterator( corpusEnv ) )
356 {
357 if( entry.is_regular_file() && expected.count( entry.path().filename().string() ) )
358 boards[entry.path().filename().string()] = entry.path();
359 }
360
361 for( const auto& [name, brd] : boards )
362 {
364 {
365 std::unique_ptr<BOARD> board = m_allegroPlugin.LoadBoard( brd.string() );
366
367 BOOST_REQUIRE( board != nullptr );
368
369 int pasted = 0;
370 int unmasked = 0;
371
372 for( FOOTPRINT* fp : board->Footprints() )
373 {
374 for( PAD* pad : fp->Pads() )
375 {
376 if( pad->GetAttribute() != PAD_ATTRIB::SMD )
377 continue;
378
379 const LSET layers = pad->GetLayerSet();
380
381 if( layers.Contains( F_Paste ) || layers.Contains( B_Paste ) )
382 pasted++;
383
384 if( !layers.Contains( F_Mask ) && !layers.Contains( B_Mask ) )
385 unmasked++;
386 }
387 }
388
389 BOOST_CHECK_EQUAL( pasted, expected.at( name ).first );
390 BOOST_CHECK_EQUAL( unmasked, expected.at( name ).second );
391 }
392 }
393}
394
395
399BOOST_AUTO_TEST_CASE( EdgeConnectorBottomFingers )
400{
401 const char* corpusEnv = std::getenv( "KICAD_ORCAD_CORPUS" );
402
403 if( !corpusEnv || !*corpusEnv )
404 return;
405
406 std::filesystem::path brd;
407
408 for( const std::filesystem::directory_entry& entry : std::filesystem::recursive_directory_iterator( corpusEnv ) )
409 {
410 if( entry.is_regular_file() && entry.path().filename() == "600-20192-01_CYW920822M2P4XXI040_REV1.0.brd" )
411 brd = entry.path();
412 }
413
414 if( brd.empty() )
415 return;
416
417 std::unique_ptr<BOARD> board = m_allegroPlugin.LoadBoard( brd.string() );
418
419 BOOST_REQUIRE( board != nullptr );
420
421 FOOTPRINT* edge = board->FindFootprintByReference( wxS( "J1" ) );
422
423 BOOST_REQUIRE( edge != nullptr );
424
425 const PAD* top = edge->FindPadByNumber( wxS( "39" ) );
426 const PAD* bottom = edge->FindPadByNumber( wxS( "40" ) );
427
428 BOOST_REQUIRE( top && bottom );
429 BOOST_CHECK( top->IsOnLayer( F_Cu ) && !top->IsOnLayer( B_Cu ) );
430 BOOST_CHECK( bottom->IsOnLayer( B_Cu ) && !bottom->IsOnLayer( F_Cu ) );
431 BOOST_CHECK( bottom->IsOnLayer( B_Mask ) && !bottom->IsOnLayer( F_Mask ) );
432}
433
434
438BOOST_AUTO_TEST_CASE( AntiEtchLinesAreNotCopper )
439{
440 const char* corpusEnv = std::getenv( "KICAD_ORCAD_CORPUS" );
441
442 if( !corpusEnv || !*corpusEnv )
443 return;
444
445 std::filesystem::path brd;
446
447 for( const std::filesystem::directory_entry& entry : std::filesystem::recursive_directory_iterator( corpusEnv ) )
448 {
449 if( entry.is_regular_file() && entry.path().filename() == "EDA-02067-V1-0.brd" )
450 brd = entry.path();
451 }
452
453 if( brd.empty() )
454 return;
455
456 std::unique_ptr<BOARD> board = m_allegroPlugin.LoadBoard( brd.string() );
457
458 BOOST_REQUIRE( board != nullptr );
459
460 int copperLines = 0;
461
462 for( BOARD_ITEM* item : board->Drawings() )
463 {
464 if( item->Type() == PCB_SHAPE_T && IsInnerCopperLayer( item->GetLayer() )
465 && static_cast<PCB_SHAPE*>( item )->GetShape() == SHAPE_T::SEGMENT )
466 {
467 copperLines++;
468 }
469 }
470
471 BOOST_CHECK_EQUAL( copperLines, 0 );
472}
473
474
478BOOST_AUTO_TEST_CASE( PlaneCopperIsAZone )
479{
480 const char* corpusEnv = std::getenv( "KICAD_ORCAD_CORPUS" );
481
482 if( !corpusEnv || !*corpusEnv )
483 return;
484
485 std::filesystem::path brd;
486
487 for( const std::filesystem::directory_entry& entry : std::filesystem::recursive_directory_iterator( corpusEnv ) )
488 {
489 if( entry.is_regular_file() && entry.path().filename() == "EDA-02153-V1-0.brd" )
490 brd = entry.path();
491 }
492
493 if( brd.empty() )
494 return;
495
496 std::unique_ptr<BOARD> board = m_allegroPlugin.LoadBoard( brd.string() );
497
498 BOOST_REQUIRE( board != nullptr );
499
500 int planePolygons = 0;
501 int planeZones = 0;
502
503 for( BOARD_ITEM* item : board->Drawings() )
504 {
505 if( item->Type() == PCB_SHAPE_T && IsInnerCopperLayer( item->GetLayer() )
506 && static_cast<PCB_SHAPE*>( item )->GetShape() == SHAPE_T::POLY )
507 {
508 planePolygons++;
509 }
510 }
511
512 for( ZONE* zone : board->Zones() )
513 {
514 if( !zone->GetIsRuleArea() && IsInnerCopperLayer( zone->GetFirstLayer() ) && zone->GetNetCode() > 0 )
515 {
516 planeZones++;
517
518 // The board's rules set the clearance, and thin fill is not dropped
519 BOOST_CHECK( zone->GetLocalClearance().has_value() );
520 BOOST_CHECK_EQUAL( zone->GetLocalClearance().value_or( -1 ), 0 );
521 BOOST_CHECK_EQUAL( zone->GetMinThickness(), 0 );
522 }
523 }
524
525 BOOST_CHECK_EQUAL( planePolygons, 0 );
526 BOOST_CHECK_GT( planeZones, 0 );
527
528 KI_TEST::FillZones( board.get() );
529
530 int filledPlanes = 0;
531 int coveredVias = 0;
532
533 for( ZONE* zone : board->Zones() )
534 {
535 PCB_LAYER_ID layer = zone->GetFirstLayer();
536
537 if( !zone->GetIsRuleArea() && IsInnerCopperLayer( layer ) && zone->HasFilledPolysForLayer( layer )
538 && !zone->GetFilledPolysList( layer )->IsEmpty() )
539 {
540 filledPlanes++;
541 }
542 }
543
544 for( PCB_TRACK* track : board->Tracks() )
545 {
546 if( track->Type() != PCB_VIA_T )
547 continue;
548
549 for( ZONE* zone : board->Zones() )
550 {
551 PCB_LAYER_ID layer = zone->GetFirstLayer();
552
553 if( zone->GetIsRuleArea() || zone->GetNetCode() == track->GetNetCode() || !IsInnerCopperLayer( layer )
554 || !track->IsOnLayer( layer ) || !zone->HasFilledPolysForLayer( layer ) )
555 {
556 continue;
557 }
558
559 if( zone->GetFilledPolysList( layer )->Collide( track->GetEffectiveShape( layer ).get() ) )
560 coveredVias++;
561 }
562 }
563
564 BOOST_CHECK_GT( filledPlanes, 0 );
565 BOOST_CHECK_EQUAL( coveredVias, 0 );
566}
567
568
572BOOST_AUTO_TEST_CASE( PlaneFillRespectsVoids )
573{
574 std::unique_ptr<BOARD> board = LoadAllegroBoard( "EVK_BaseBoard/EVK_BaseBoard.brd" );
575
576 BOOST_REQUIRE( board != nullptr );
577
578 std::vector<ZONE*> voids;
579
580 for( ZONE* zone : board->Zones() )
581 {
582 if( zone->GetIsRuleArea() && zone->GetZoneName() == wxS( "Plane void" ) )
583 voids.push_back( zone );
584 }
585
586 BOOST_REQUIRE( !voids.empty() );
587
588 // A zone is saved with only its first polygon, so each void area holds exactly one
589 for( ZONE* area : voids )
590 BOOST_CHECK_EQUAL( area->Outline()->OutlineCount(), 1 );
591
592 KI_TEST::FillZones( board.get() );
593
594 double overlap = 0;
595
596 for( ZONE* area : voids )
597 {
598 PCB_LAYER_ID layer = area->GetFirstLayer();
599
600 for( ZONE* zone : board->Zones() )
601 {
602 if( zone->GetIsRuleArea() || !zone->IsOnLayer( layer ) || !zone->HasFilledPolysForLayer( layer ) )
603 continue;
604
605 SHAPE_POLY_SET common( *zone->GetFilledPolysList( layer ) );
606 common.BooleanIntersection( *area->Outline() );
607 overlap += common.Area();
608 }
609 }
610
611 // slivers from rounding are fine, a closed void is not
612 BOOST_CHECK_LT( overlap, 1e10 );
613}
614
615
619BOOST_AUTO_TEST_CASE( CustomPadsAreOnePolygon )
620{
621 for( const char* name : { "EVK_BaseBoard/EVK_BaseBoard.brd", "EVK_SOM/EVK_SOM.brd",
622 "BeagleBone-AI/BeagleBone-AI.brd", "CutiePi_V2_3_v166/CutiePi_V2_3.brd",
623 "mainBoard/mainBoard.brd", "ABX00162_UNOQ/20250806_UNOQ.brd" } )
624 {
625 std::unique_ptr<BOARD> board = LoadAllegroBoard( name );
626
627 BOOST_REQUIRE( board );
628
629 int multi = 0;
630
631 for( FOOTPRINT* fp : board->Footprints() )
632 {
633 for( PAD* pad : fp->Pads() )
634 {
635 for( PCB_LAYER_ID layer : pad->Padstack().UniqueLayers() )
636 {
637 if( pad->GetShape( layer ) != PAD_SHAPE::CUSTOM )
638 continue;
639
640 if( pad->GetEffectivePolygon( layer )->OutlineCount() != 1 )
641 multi++;
642 }
643 }
644 }
645
646 BOOST_TEST_CONTEXT( name ) { BOOST_CHECK_EQUAL( multi, 0 ); }
647 }
648}
649
650
655{
656 std::unique_ptr<BOARD> board = LoadAllegroBoard( "TRS80_POWER/TRS80_POWER.brd" );
657
658 BOOST_REQUIRE( board != nullptr );
659
660 int validPadCount = 0;
661 int zeroPadCount = 0;
662 int hugePadCount = 0;
663
664 for( FOOTPRINT* fp : board->Footprints() )
665 {
666 for( PAD* pad : fp->Pads() )
667 {
668 VECTOR2I size = pad->GetSize( F_Cu );
669
670 if( size.x == 0 || size.y == 0 )
671 {
672 zeroPadCount++;
673 BOOST_TEST_MESSAGE( "Zero-size pad in " << fp->GetReference() << " pad "
674 << pad->GetNumber() );
675 }
676 else if( size.x > 50000000 || size.y > 50000000 ) // > 50mm is suspicious
677 {
678 hugePadCount++;
679 BOOST_TEST_MESSAGE( "Huge pad in " << fp->GetReference() << " pad " << pad->GetNumber()
680 << ": " << size.x / 1000000.0 << "mm x "
681 << size.y / 1000000.0 << "mm" );
682 }
683 else
684 {
685 validPadCount++;
686 }
687 }
688 }
689
690 BOOST_TEST_MESSAGE( "Valid pads: " << validPadCount << ", Zero: " << zeroPadCount
691 << ", Huge: " << hugePadCount );
692
693 // No pads should be zero-size (this catches hardcoded fallbacks)
694 BOOST_CHECK_EQUAL( zeroPadCount, 0 );
695}
696
697
702{
703 std::unique_ptr<BOARD> board = LoadAllegroBoard( "TRS80_POWER/TRS80_POWER.brd" );
704
705 BOOST_REQUIRE( board != nullptr );
706
707 int validViaCount = 0;
708 int suspiciousViaCount = 0;
709
710 // Count vias with the hardcoded size (1000000 = 1mm exactly)
711 const int HARDCODED_SIZE = 1000000;
712
713 for( PCB_TRACK* track : board->Tracks() )
714 {
715 if( track->Type() == PCB_VIA_T )
716 {
717 PCB_VIA* via = static_cast<PCB_VIA*>( track );
718 int width = via->GetWidth( F_Cu );
719
720 if( width == HARDCODED_SIZE )
721 {
722 suspiciousViaCount++;
723 }
724 else if( width > 0 && width < 10000000 ) // 0 < size < 10mm is reasonable
725 {
726 validViaCount++;
727 }
728 }
729 }
730
731 BOOST_TEST_MESSAGE( "Valid vias: " << validViaCount << ", Hardcoded-size vias: " << suspiciousViaCount );
732
733 // This test will fail until via size is properly extracted from padstack
734 if( suspiciousViaCount > 0 )
735 {
736 BOOST_WARN_MESSAGE( false, "Found " << suspiciousViaCount << " vias with hardcoded 1mm size" );
737 }
738}
739
740
745{
746 std::unique_ptr<BOARD> board = LoadAllegroBoard( "TRS80_POWER/TRS80_POWER.brd" );
747
748 BOOST_REQUIRE( board != nullptr );
749
750 int validTrackCount = 0;
751 int zeroTrackCount = 0;
752
753 for( PCB_TRACK* track : board->Tracks() )
754 {
755 if( track->Type() == PCB_TRACE_T )
756 {
757 int width = track->GetWidth();
758
759 if( width == 0 )
760 zeroTrackCount++;
761 else if( width > 0 && width < 10000000 ) // 0 < width < 10mm
762 validTrackCount++;
763 }
764 }
765
766 BOOST_TEST_MESSAGE( "Valid tracks: " << validTrackCount << ", Zero-width: " << zeroTrackCount );
767
768 BOOST_CHECK_EQUAL( zeroTrackCount, 0 );
769}
770
771
776{
777 std::unique_ptr<BOARD> board = LoadAllegroBoard( "TRS80_POWER/TRS80_POWER.brd" );
778
779 BOOST_REQUIRE( board != nullptr );
780
781 int numberedPads = 0;
782 int unnumberedPads = 0;
783
784 for( FOOTPRINT* fp : board->Footprints() )
785 {
786 for( PAD* pad : fp->Pads() )
787 {
788 if( pad->GetNumber().IsEmpty() )
789 unnumberedPads++;
790 else
791 numberedPads++;
792 }
793 }
794
795 BOOST_TEST_MESSAGE( "Numbered pads: " << numberedPads << ", Unnumbered: " << unnumberedPads );
796
797 // All pads should have numbers for proper netlist generation
798 // This test will fail until pad numbers are properly set
799 if( unnumberedPads > 0 )
800 {
801 BOOST_WARN_MESSAGE( false, "Found " << unnumberedPads << " pads without numbers" );
802 }
803}
804
805
806static unsigned CountOutlineElements( const BOARD& board )
807{
808 unsigned count = 0;
809 for( const BOARD_ITEM* item : board.Drawings() )
810 {
811 if( item->Type() == PCB_SHAPE_T && item->GetLayer() == Edge_Cuts )
812 {
813 count++;
814 }
815 }
816 return count;
817}
818
819
820static void AssertOutlineValid( const BOARD& aBoard )
821{
822 // Verify outline forms a closed contour by checking that all segments connect
823 std::vector<SEG> outlineSegs;
824
825 for( BOARD_ITEM* item : aBoard.Drawings() )
826 {
827 if( item->Type() == PCB_SHAPE_T && item->GetLayer() == Edge_Cuts )
828 {
829 PCB_SHAPE* shape = static_cast<PCB_SHAPE*>( item );
830 switch( shape->GetShape() )
831 {
832 case SHAPE_T::SEGMENT:
833 case SHAPE_T::ARC:
834 case SHAPE_T::BEZIER:
835 {
836 outlineSegs.push_back( SEG( shape->GetStart(), shape->GetEnd() ) );
837 break;
838 }
840 {
841 // Rectangles are stored as a single item but represent 4 segments
842 VECTOR2I start = shape->GetStart();
843 VECTOR2I end = shape->GetEnd();
844
845 for( const auto& seg : KIGEOM::BoxToSegs( BOX2I( start, end ) ) )
846 {
847 outlineSegs.push_back( seg );
848 }
849 break;
850 }
851 case SHAPE_T::POLY:
852 {
853 std::vector<VECTOR2I> polyPoints = shape->GetPolyPoints();
854
855 for( size_t i = 0; i < polyPoints.size() - 1; i++ )
856 {
857 VECTOR2I start = polyPoints[i];
858 VECTOR2I end = polyPoints[( i + 1 ) % polyPoints.size()];
859 outlineSegs.emplace_back( start, end );
860 }
861 break;
862 }
863 case SHAPE_T::CIRCLE:
864 // Not really sure what we can do here? Zero-length seg?
865 outlineSegs.push_back( SEG( shape->GetStart(), shape->GetStart() ) );
866 break;
867 default:
868 BOOST_WARN_MESSAGE(
869 false, "Unexpected shape type in board outline: " << static_cast<int>( shape->GetShape() ) );
870 }
871 }
872 }
873
874 if( !outlineSegs.empty() )
875 {
876 // For a valid closed outline, the sum of all segment lengths should equal the perimeter
877 // and each endpoint should connect to another endpoint
878 int connectedCount = 0;
879
880 for( const SEG& seg : outlineSegs )
881 {
882 for( const SEG& other : outlineSegs )
883 {
884 if( &other == &seg )
885 continue;
886
887 // Check if this shape's start connects to another shape's start or end
888 if( seg.A == other.A || seg.A == other.B )
889 connectedCount++;
890
891 // Check if this shape's end connects to another shape's start or end
892 if( seg.B == other.A || seg.B == other.B )
893 connectedCount++;
894 }
895 }
896
897 // Each segment should connect at both ends for a closed outline
898 // For 4 segments, we expect 8 connections (2 per segment)
899 BOOST_TEST_MESSAGE( "Connected endpoints: " << connectedCount );
900 BOOST_CHECK_GE( connectedCount, outlineSegs.size() * 2 );
901 }
902}
903
904
908BOOST_AUTO_TEST_CASE( BoardOutline )
909{
910 std::unique_ptr<BOARD> board = LoadAllegroBoard( "TRS80_POWER/TRS80_POWER.brd" );
911
912 BOOST_REQUIRE( board != nullptr );
913
914 // Count shapes on Edge_Cuts layer
915 int outlineSegmentCount = CountOutlineElements( *board );
916
917 BOOST_TEST_MESSAGE( "Board outline elements: " << outlineSegmentCount );
918
919 // Board should have an outline - TRS80_POWER.brd has a rectangular outline (4 segments)
920 BOOST_CHECK_GE( outlineSegmentCount, 1 );
921
922 AssertOutlineValid( *board );
923}
924
925
929BOOST_AUTO_TEST_CASE( PadsInsideOutline )
930{
931 std::unique_ptr<BOARD> board = LoadAllegroBoard( "TRS80_POWER/TRS80_POWER.brd" );
932
933 BOOST_REQUIRE( board != nullptr );
934
935 // Get board bounding box from outline
936 BOX2I boardBbox;
937 bool hasBbox = false;
938
939 for( BOARD_ITEM* item : board->Drawings() )
940 {
941 if( item->Type() == PCB_SHAPE_T && item->GetLayer() == Edge_Cuts )
942 {
943 if( !hasBbox )
944 {
945 boardBbox = item->GetBoundingBox();
946 hasBbox = true;
947 }
948 else
949 {
950 boardBbox.Merge( item->GetBoundingBox() );
951 }
952 }
953 }
954
955 BOOST_REQUIRE_MESSAGE( hasBbox, "Board should have an outline" );
956
957 int padsInside = 0;
958 int padsOutside = 0;
959
960 // Inflate bbox slightly to account for edge cases
961 BOX2I testBbox = boardBbox;
962 testBbox.Inflate( 1000 ); // 1mm tolerance
963
964 for( FOOTPRINT* fp : board->Footprints() )
965 {
966 for( PAD* pad : fp->Pads() )
967 {
968 VECTOR2I padCenter = pad->GetPosition();
969
970 if( testBbox.Contains( padCenter ) )
971 {
972 padsInside++;
973 }
974 else
975 {
976 padsOutside++;
977 BOOST_TEST_MESSAGE( "Pad outside outline: " << fp->GetReference() << " pad "
978 << pad->GetNumber() << " at ("
979 << padCenter.x / 1000000.0 << ", "
980 << padCenter.y / 1000000.0 << ") mm" );
981 }
982 }
983 }
984
985 BOOST_TEST_MESSAGE( "Pads inside outline: " << padsInside << ", outside: " << padsOutside );
986
987 // Most pads should be inside the board outline
988 // Some boards may have off-board test points or fiducials
989 if( padsOutside > 0 )
990 {
991 BOOST_WARN_MESSAGE( false, "Found " << padsOutside << " pads outside board outline" );
992 }
993
994 // At minimum, most pads should be inside
995 BOOST_CHECK_GT( padsInside, padsOutside );
996}
997
998
1003BOOST_AUTO_TEST_CASE( PreV16FileRejection )
1004{
1005 BOOST_CHECK_EXCEPTION(
1006 LoadAllegroBoard( "v13_header/v13_header.brd" ), IO_ERROR,
1007 []( const IO_ERROR& e )
1008 {
1009 wxString msg = e.What();
1010
1011 return msg.Contains( wxS( "predates Allegro 16.0" ) )
1012 && msg.Contains( wxS( "Allegro PCB Design" ) );
1013 } );
1014}
1015
1016
1022BOOST_AUTO_TEST_CASE( RectsZoneVsCopperPolygon )
1023{
1024 std::unique_ptr<BOARD> board = LoadAllegroBoard( "rects/rects.brd" );
1025 BOOST_REQUIRE( board );
1026
1027 // Should have exactly one zone (the left rectangle as a zone fill)
1028 BOOST_CHECK_EQUAL( board->Zones().size(), 1 );
1029
1030 ZONE* zone = board->Zones().front();
1031 BOOST_CHECK( zone->GetNetCode() > 0 );
1032 BOOST_CHECK( IsCopperLayer( zone->GetFirstLayer() ) );
1033 BOOST_CHECK( zone->IsFilled() );
1034
1035 // Should have exactly one standalone copper polygon (the right rectangle)
1036 int copperPolyCount = 0;
1037 int copperPolyWithNet = 0;
1038
1039 for( BOARD_ITEM* item : board->Drawings() )
1040 {
1041 if( item->Type() == PCB_SHAPE_T )
1042 {
1043 PCB_SHAPE* shape = static_cast<PCB_SHAPE*>( item );
1044
1045 if( IsCopperLayer( shape->GetLayer() ) && shape->GetShape() == SHAPE_T::POLY )
1046 {
1047 copperPolyCount++;
1048
1049 if( shape->GetNetCode() > 0 )
1050 copperPolyWithNet++;
1051 }
1052 }
1053 }
1054
1055 BOOST_CHECK_EQUAL( copperPolyCount, 1 );
1056 BOOST_CHECK_EQUAL( copperPolyWithNet, 1 );
1057}
1058
1059
1064{
1065 std::unique_ptr<BOARD> board = LoadAllegroBoard( "copper_text/copper_text.brd" );
1066 BOOST_REQUIRE( board );
1067
1068 int copperTextCount = 0;
1069 bool foundTestingText = false;
1070
1071 for( BOARD_ITEM* item : board->Drawings() )
1072 {
1073 if( item->Type() == PCB_TEXT_T )
1074 {
1075 PCB_TEXT* text = static_cast<PCB_TEXT*>( item );
1076
1077 if( IsCopperLayer( text->GetLayer() ) )
1078 {
1079 copperTextCount++;
1080
1081 if( text->GetText() == wxS( "TESTING" ) )
1082 {
1083 foundTestingText = true;
1084 BOOST_CHECK_EQUAL( text->GetLayer(), F_Cu );
1085 }
1086 }
1087 }
1088 }
1089
1090 BOOST_CHECK_MESSAGE( foundTestingText, "Board should contain 'TESTING' text on F.Cu" );
1091 BOOST_CHECK_EQUAL( copperTextCount, 1 );
1092}
1093
1094
1095BOOST_AUTO_TEST_CASE( CoincidentGraphicsAreDropped )
1096{
1097 // Allegro stacks graphics that land on top of each other, on some designs a third of all
1098 // items. They draw the same picture, so the importer keeps only the first of each
1099 std::unique_ptr<BOARD> board = LoadAllegroBoard( "TRS80_POWER/TRS80_POWER.brd" );
1100
1101 const auto netOf = []( const BOARD_ITEM* aItem )
1102 {
1103 const BOARD_CONNECTED_ITEM* connected = dynamic_cast<const BOARD_CONNECTED_ITEM*>( aItem );
1104 return connected ? connected->GetNetCode() : NETINFO_LIST::UNCONNECTED;
1105 };
1106
1107 const auto coincident = [&]( const BOARD_ITEM* aFirst, const BOARD_ITEM* aSecond )
1108 {
1109 if( aFirst->Type() != aSecond->Type() || aFirst->GetLayer() != aSecond->GetLayer()
1110 || netOf( aFirst ) != netOf( aSecond ) )
1111 {
1112 return false;
1113 }
1114
1115 if( aFirst->Type() == PCB_SHAPE_T )
1116 {
1117 return static_cast<const PCB_SHAPE*>( aFirst )->Compare(
1118 static_cast<const PCB_SHAPE*>( aSecond ) ) == 0;
1119 }
1120
1121 if( aFirst->Type() == PCB_TEXT_T )
1122 {
1123 return static_cast<const PCB_TEXT*>( aFirst )->Compare(
1124 static_cast<const PCB_TEXT*>( aSecond ) ) == 0;
1125 }
1126
1127 return false;
1128 };
1129
1130 const auto countCoincident = [&]( const DRAWINGS& aItems )
1131 {
1132 int count = 0;
1133
1134 for( size_t ii = 0; ii < aItems.size(); ++ii )
1135 {
1136 for( size_t jj = ii + 1; jj < aItems.size(); ++jj )
1137 {
1138 if( coincident( aItems[ii], aItems[jj] ) )
1139 count++;
1140 }
1141 }
1142
1143 return count;
1144 };
1145
1146 BOOST_REQUIRE( !board->Drawings().empty() );
1147 BOOST_CHECK_EQUAL( countCoincident( board->Drawings() ), 0 );
1148
1149 BOOST_REQUIRE( !board->Footprints().empty() );
1150
1151 for( FOOTPRINT* footprint : board->Footprints() )
1152 BOOST_CHECK_EQUAL( countCoincident( footprint->GraphicalItems() ), 0 );
1153}
1154
1155
1156BOOST_AUTO_TEST_CASE( ImportIsRepeatable )
1157{
1158 // Item ids are derived from the Allegro block keys, so importing a design twice has to
1159 // produce the same ids. Random ids reshuffle the whole saved file on every import, because
1160 // the s-expr writer orders items by uuid
1161 const auto collectIds = []( const BOARD& aBoard )
1162 {
1163 std::vector<wxString> ids;
1164
1165 std::function<void( const BOARD_ITEM* )> walk =
1166 [&]( const BOARD_ITEM* aItem )
1167 {
1168 ids.push_back( aItem->m_Uuid.AsString() );
1169
1170 // Group members are collected where they live on the board
1171 if( aItem->Type() == PCB_GROUP_T )
1172 return;
1173
1174 aItem->RunOnChildren(
1175 [&]( BOARD_ITEM* aChild )
1176 {
1177 walk( aChild );
1178 },
1180 };
1181
1182 for( const BOARD_ITEM* item : const_cast<BOARD&>( aBoard ).GetItemSet() )
1183 walk( item );
1184
1185 std::sort( ids.begin(), ids.end() );
1186 return ids;
1187 };
1188
1189 std::unique_ptr<BOARD> first = LoadAllegroBoard( "ProiectBoard/ProiectBoard.brd" );
1190 std::unique_ptr<BOARD> second = LoadAllegroBoard( "ProiectBoard/ProiectBoard.brd" );
1191
1192 const std::vector<wxString> firstIds = collectIds( *first );
1193 const std::vector<wxString> secondIds = collectIds( *second );
1194
1195 BOOST_REQUIRE( !firstIds.empty() );
1196 BOOST_CHECK_EQUAL( firstIds.size(), secondIds.size() );
1197 BOOST_CHECK( firstIds == secondIds );
1198
1199 BOOST_CHECK( std::adjacent_find( firstIds.begin(), firstIds.end() ) == firstIds.end() );
1200}
1201
1202
1203// The 3D model assignment lives on the Allegro package definition, so every placed instance
1204// of a package that names one carries it
1205BOOST_AUTO_TEST_CASE( Footprint3DModels )
1206{
1207 std::unique_ptr<BOARD> board = LoadAllegroBoard( "led_youtube/led_youtube.brd" );
1208 BOOST_REQUIRE( board );
1209
1210 std::map<wxString, FP_3DMODEL> models;
1211
1212 for( FOOTPRINT* fp : board->Footprints() )
1213 {
1214 BOOST_REQUIRE_EQUAL( fp->Models().size(), 1u );
1215 models.emplace( fp->Models().front().m_Filename, fp->Models().front() );
1216 }
1217
1218 BOOST_REQUIRE_EQUAL( models.size(), 3u );
1219 BOOST_CHECK_EQUAL( models.count( wxS( "led3d.stp" ) ), 1u );
1220 BOOST_CHECK_EQUAL( models.count( wxS( "AC0805FR-07360RL.STEP" ) ), 1u );
1221 BOOST_REQUIRE_EQUAL( models.count( wxS( "22272021.stp" ) ), 1u );
1222
1223 // Placement of the connector package is "MM,0.020000,-1.270000,1.580007,90.000,-0.000,90.000"
1224 const FP_3DMODEL& conn = models.at( wxS( "22272021.stp" ) );
1225
1226 BOOST_CHECK_CLOSE( conn.m_Offset.x, 0.02, 1e-6 );
1227 BOOST_CHECK_CLOSE( conn.m_Offset.y, -1.27, 1e-6 );
1228 BOOST_CHECK_CLOSE( conn.m_Offset.z, 1.580007, 1e-6 );
1229 BOOST_CHECK_CLOSE( conn.m_Rotation.x, -90.0, 1e-6 );
1230 BOOST_CHECK_SMALL( conn.m_Rotation.y, 1e-9 );
1231 BOOST_CHECK_CLOSE( conn.m_Rotation.z, -90.0, 1e-6 );
1232}
1233
1234
1236
1237
1238
1242{
1243 std::string filename;
1244 bool expected_to_load; // Set false for known-broken boards
1245};
1246
1247
1252{
1254
1260 BOARD* GetCachedBoard( const std::string& aFilePath )
1261 {
1263 }
1264
1268 static std::vector<std::string> GetAllBoardFiles()
1269 {
1270 std::vector<std::string> boards;
1271 std::string dataPath = KI_TEST::AllegroBoardDataDir( "" );
1272
1273 // For each board dir, look for .brd files and add them to the list of test cases
1274 try
1275 {
1276 for( const auto& boardDir : std::filesystem::directory_iterator( dataPath ) )
1277 {
1278 if( !boardDir.is_directory() )
1279 continue;
1280
1281 for( const auto& entry : std::filesystem::directory_iterator( boardDir ) )
1282 {
1283 if( entry.is_regular_file() && entry.path().extension() == ".brd" && entry.file_size() > 0 )
1284 {
1285 std::string name = entry.path().filename().string();
1286
1287 // v13_header.brd is intentionally pre-v16 and tested separately
1288 if( name != "v13_header.brd" )
1289 {
1290 boards.push_back( boardDir.path().string() + "/" + name );
1291 }
1292 }
1293 }
1294 }
1295 }
1296 catch( const std::filesystem::filesystem_error& e )
1297 {
1298 BOOST_TEST_MESSAGE( "Failed to enumerate board files: " << e.what() );
1299 }
1300
1301 std::sort( boards.begin(), boards.end() );
1302 return boards;
1303 }
1304
1306};
1307
1308BOOST_FIXTURE_TEST_SUITE( AllegroComprehensive, ALLEGRO_COMPREHENSIVE_FIXTURE )
1309
1310
1311
1316BOOST_AUTO_TEST_CASE( BeagleBone_OutermostZoneNets )
1317{
1318 std::string dataPath = KI_TEST::AllegroBoardFile( "BeagleBone_Black_RevC/BeagleBone_Black_RevC.brd" );
1319
1320 BOARD* board = GetCachedBoard( dataPath );
1321 BOOST_REQUIRE( board );
1322
1323 const std::vector<wxString> expectedLayers = { wxS( "TOP" ), wxS( "LYR2_GND" ),
1324 wxS( "LYR5_PWR" ), wxS( "BOTTOM" ) };
1325
1326 for( const wxString& layerName : expectedLayers )
1327 {
1328 BOOST_TEST_CONTEXT( "Outermost zone on " << layerName )
1329 {
1330 PCB_LAYER_ID layerId = board->GetLayerID( layerName );
1331
1332 BOOST_REQUIRE_MESSAGE( layerId != UNDEFINED_LAYER,
1333 "Layer " << layerName << " should exist" );
1334
1335 const ZONE* largest = nullptr;
1336 double largestArea = 0;
1337
1338 for( const ZONE* zone : board->Zones() )
1339 {
1340 if( zone->GetIsRuleArea() )
1341 continue;
1342
1343 if( zone->GetNetCode() == 0 )
1344 continue;
1345
1346 if( !zone->GetLayerSet().Contains( layerId ) )
1347 continue;
1348
1349 BOX2I bbox = zone->GetBoundingBox();
1350 double area = static_cast<double>( bbox.GetWidth() )
1351 * static_cast<double>( bbox.GetHeight() );
1352
1353 if( area > largestArea )
1354 {
1355 largestArea = area;
1356 largest = zone;
1357 }
1358 }
1359
1360 BOOST_REQUIRE_MESSAGE( largest != nullptr,
1361 "Should find a netted copper zone on " << layerName );
1362 BOOST_CHECK_EQUAL( largest->GetNetname(), wxString( wxS( "GND_EARTH" ) ) );
1363 }
1364 }
1365}
1366
1367
1371BOOST_AUTO_TEST_CASE( PadSizesPositive )
1372{
1373 std::vector<std::string> boards = GetAllBoardFiles();
1374
1375 for( const std::string& boardPath : boards )
1376 {
1377 std::string boardName = std::filesystem::path( boardPath ).filename().string();
1378 BOARD* board = GetCachedBoard( boardPath );
1379
1380 if( !board )
1381 continue;
1382
1383 BOOST_TEST_CONTEXT( "Testing board: " << boardName )
1384 {
1385 int negativePadCount = 0;
1386
1387 for( FOOTPRINT* fp : board->Footprints() )
1388 {
1389 for( PAD* pad : fp->Pads() )
1390 {
1391 VECTOR2I size = pad->GetSize( F_Cu );
1392
1393 if( size.x < 0 || size.y < 0 )
1394 {
1395 negativePadCount++;
1396 BOOST_TEST_MESSAGE( boardName << ": Negative pad size in " << fp->GetReference()
1397 << " pad " << pad->GetNumber() << ": " << size.x << " x "
1398 << size.y );
1399 }
1400 }
1401 }
1402
1403 BOOST_CHECK_EQUAL( negativePadCount, 0 );
1404 }
1405 }
1406}
1407
1408
1413BOOST_AUTO_TEST_CASE( ViaDrillNotLargerThanSize )
1414{
1415 std::vector<std::string> boards = GetAllBoardFiles();
1416
1417 for( const std::string& boardPath : boards )
1418 {
1419 std::string boardName = std::filesystem::path( boardPath ).filename().string();
1420 BOARD* board = GetCachedBoard( boardPath );
1421
1422 if( !board )
1423 continue;
1424
1425 BOOST_TEST_CONTEXT( "Testing board: " << boardName )
1426 {
1427 int invalidViaCount = 0;
1428
1429 for( PCB_TRACK* track : board->Tracks() )
1430 {
1431 if( track->Type() == PCB_VIA_T )
1432 {
1433 PCB_VIA* via = static_cast<PCB_VIA*>( track );
1434 int drill = via->GetDrill();
1435 int width = via->GetWidth( F_Cu );
1436
1437 if( drill > width )
1438 {
1439 invalidViaCount++;
1440 BOOST_TEST_MESSAGE( boardName << ": Via at ("
1441 << via->GetPosition().x / 1000000.0 << ", "
1442 << via->GetPosition().y / 1000000.0
1443 << ") has drill " << drill / 1000000.0
1444 << "mm > width " << width / 1000000.0 << "mm" );
1445 }
1446 }
1447 }
1448
1449 BOOST_CHECK_EQUAL( invalidViaCount, 0 );
1450 }
1451 }
1452}
1453
1454
1459BOOST_AUTO_TEST_CASE( SmdPadDetection )
1460{
1461 std::vector<std::string> boards = GetAllBoardFiles();
1462
1463 for( const std::string& boardPath : boards )
1464 {
1465 std::string boardName = std::filesystem::path( boardPath ).filename().string();
1466 BOARD* board = GetCachedBoard( boardPath );
1467
1468 if( !board )
1469 continue;
1470
1471 BOOST_TEST_CONTEXT( "Testing board: " << boardName )
1472 {
1473 int misclassifiedSmdCount = 0;
1474 int correctSmdCount = 0;
1475 int correctThCount = 0;
1476
1477 for( FOOTPRINT* fp : board->Footprints() )
1478 {
1479 for( PAD* pad : fp->Pads() )
1480 {
1481 bool hasDrill = pad->GetDrillSize().x > 0 && pad->GetDrillSize().y > 0;
1482 PAD_ATTRIB attr = pad->GetAttribute();
1483
1484 if( !hasDrill && attr == PAD_ATTRIB::PTH )
1485 {
1486 misclassifiedSmdCount++;
1487 BOOST_TEST_MESSAGE( boardName << ": Pad " << fp->GetReference()
1488 << "." << pad->GetNumber()
1489 << " has no drill but is marked as PTH (should be SMD)" );
1490 }
1491 else if( !hasDrill && attr == PAD_ATTRIB::SMD )
1492 {
1493 correctSmdCount++;
1494 }
1495 else if( hasDrill && ( attr == PAD_ATTRIB::PTH || attr == PAD_ATTRIB::NPTH ) )
1496 {
1497 correctThCount++;
1498 }
1499 }
1500 }
1501
1502 BOOST_TEST_MESSAGE( boardName << ": Correct SMD=" << correctSmdCount
1503 << ", Correct TH=" << correctThCount
1504 << ", Misclassified=" << misclassifiedSmdCount );
1505
1506 BOOST_CHECK_EQUAL( misclassifiedSmdCount, 0 );
1507 }
1508 }
1509}
1510
1511
1516BOOST_AUTO_TEST_CASE( QuadPackagePadRotation )
1517{
1518 std::vector<std::string> boards = GetAllBoardFiles();
1519
1520 for( const std::string& boardPath : boards )
1521 {
1522 std::string boardName = std::filesystem::path( boardPath ).filename().string();
1523 BOARD* board = GetCachedBoard( boardPath );
1524
1525 if( !board )
1526 continue;
1527
1528 BOOST_TEST_CONTEXT( "Testing board: " << boardName )
1529 {
1530 int quadPackageCount = 0;
1531 int packagesWithRotatedPads = 0;
1532 int packagesWithUnrotatedPads = 0;
1533
1534 for( FOOTPRINT* fp : board->Footprints() )
1535 {
1536 wxString refdes = fp->GetReference().Upper();
1537
1538 // Look for ICs (typically U* prefix) that might be quad packages
1539 if( !refdes.StartsWith( "U" ) )
1540 continue;
1541
1542 // Must have at least 16 pads to be a quad package
1543 if( fp->Pads().size() < 16 )
1544 continue;
1545
1546 // Find bounding box of all pad centers to estimate package shape
1547 BOX2I padBounds;
1548 bool first = true;
1549
1550 for( PAD* pad : fp->Pads() )
1551 {
1552 VECTOR2I pos = pad->GetPosition();
1553
1554 if( first )
1555 {
1556 padBounds = BOX2I( pos, VECTOR2I( 0, 0 ) );
1557 first = false;
1558 }
1559 else
1560 {
1561 padBounds.Merge( pos );
1562 }
1563 }
1564
1565 // Must be roughly square to be a quad package
1566 int width = padBounds.GetWidth();
1567 int height = padBounds.GetHeight();
1568
1569 if( width == 0 || height == 0 )
1570 continue;
1571
1572 double aspectRatio = static_cast<double>( std::max( width, height ) ) /
1573 static_cast<double>( std::min( width, height ) );
1574
1575 if( aspectRatio > 2.0 )
1576 continue;
1577
1578 quadPackageCount++;
1579
1580 // Check if pads have varying orientations
1581 std::set<int> uniqueAngles;
1582
1583 for( PAD* pad : fp->Pads() )
1584 {
1585 EDA_ANGLE angle = pad->GetOrientation();
1586 angle.Normalize();
1587 int degrees = static_cast<int>( angle.AsDegrees() + 0.5 ) % 360;
1588 uniqueAngles.insert( degrees );
1589 }
1590
1591 // A properly imported quad package should have at least 2 different pad orientations
1592 // (for 2-sided packages) or 4 (for 4-sided packages like QFP)
1593 if( uniqueAngles.size() >= 2 )
1594 {
1595 packagesWithRotatedPads++;
1596 BOOST_TEST_MESSAGE( boardName << ": " << fp->GetReference()
1597 << " has " << uniqueAngles.size() << " unique pad orientations" );
1598 }
1599 else
1600 {
1601 packagesWithUnrotatedPads++;
1602 BOOST_TEST_MESSAGE( boardName << ": " << fp->GetReference()
1603 << " has only " << uniqueAngles.size()
1604 << " unique pad orientation (may be missing rotation)" );
1605 }
1606 }
1607
1608 if( quadPackageCount > 0 )
1609 {
1610 BOOST_TEST_MESSAGE( boardName << ": Found " << quadPackageCount
1611 << " potential quad packages, "
1612 << packagesWithRotatedPads << " with rotated pads, "
1613 << packagesWithUnrotatedPads << " without" );
1614
1615 // At least some packages should have rotated pads to confirm rotation parsing works.
1616 // Many packages may legitimately have all pads at the same orientation (BGAs, single-row).
1617 if( packagesWithRotatedPads == 0 && quadPackageCount > 0 )
1618 {
1619 BOOST_WARN_MESSAGE( false, boardName << " has no packages with rotated pads" );
1620 }
1621 }
1622 }
1623 }
1624}
1625
1626
1632BOOST_AUTO_TEST_CASE( FootprintLayerPlacement )
1633{
1634 std::string dataPath = KI_TEST::AllegroBoardFile( "BeagleBone_Black_RevC/BeagleBone_Black_RevC.brd" );
1635
1636 BOARD* board = GetCachedBoard( dataPath );
1637 BOOST_REQUIRE_MESSAGE( board != nullptr, "BeagleBone_Black_RevC.brd should load successfully" );
1638
1639 // Look for C78 which should be on the bottom layer
1640 FOOTPRINT* c78 = nullptr;
1641
1642 for( FOOTPRINT* fp : board->Footprints() )
1643 {
1644 if( fp->GetReference() == "C78" )
1645 {
1646 c78 = fp;
1647 break;
1648 }
1649 }
1650
1651 BOOST_REQUIRE_MESSAGE( c78 != nullptr, "Footprint C78 should exist in BeagleBone Black" );
1652
1653 PCB_LAYER_ID fpLayer = c78->GetLayer();
1654
1655 BOOST_TEST_MESSAGE( "C78 layer: " << board->GetLayerName( fpLayer ) << " (ID: " << fpLayer << ")" );
1656 BOOST_TEST_MESSAGE( "C78 is flipped: " << ( c78->IsFlipped() ? "yes" : "no" ) );
1657
1658 BOOST_CHECK_MESSAGE( fpLayer == B_Cu, "C78 should be on the bottom copper layer (B_Cu), got "
1659 << board->GetLayerName( fpLayer ) );
1660 BOOST_CHECK_MESSAGE( c78->IsFlipped(), "C78 should be flipped (IsFlipped() == true)" );
1661
1662 // Count footprints on top vs bottom to ensure we're parsing layer correctly
1663 int topCount = 0;
1664 int bottomCount = 0;
1665
1666 for( FOOTPRINT* fp : board->Footprints() )
1667 {
1668 if( fp->GetLayer() == F_Cu )
1669 topCount++;
1670 else if( fp->GetLayer() == B_Cu )
1671 bottomCount++;
1672 }
1673
1674 BOOST_TEST_MESSAGE( "Footprints on top: " << topCount << ", on bottom: " << bottomCount );
1675
1676 // BeagleBone should have components on both sides
1677 BOOST_CHECK_GT( topCount, 0 );
1678 BOOST_CHECK_GT( bottomCount, 0 );
1679}
1680
1681
1686BOOST_AUTO_TEST_CASE( ArcConnectivity )
1687{
1688 std::vector<std::string> boards = GetAllBoardFiles();
1689
1690 for( const std::string& boardPath : boards )
1691 {
1692 std::string boardName = std::filesystem::path( boardPath ).filename().string();
1693 BOARD* board = GetCachedBoard( boardPath );
1694
1695 if( !board )
1696 continue;
1697
1698 BOOST_TEST_CONTEXT( "Testing board: " << boardName )
1699 {
1700 int arcCount = 0;
1701 int disconnectedArcs = 0;
1702
1703 // Build a map of track endpoints per net for quick lookup
1704 std::map<int, std::vector<VECTOR2I>> netEndpoints;
1705
1706 for( PCB_TRACK* track : board->Tracks() )
1707 {
1708 int netCode = track->GetNetCode();
1709
1710 if( track->Type() == PCB_TRACE_T )
1711 {
1712 netEndpoints[netCode].push_back( track->GetStart() );
1713 netEndpoints[netCode].push_back( track->GetEnd() );
1714 }
1715 else if( track->Type() == PCB_VIA_T )
1716 {
1717 netEndpoints[netCode].push_back( track->GetPosition() );
1718 }
1719 }
1720
1721 // Also include pad positions
1722 for( FOOTPRINT* fp : board->Footprints() )
1723 {
1724 for( PAD* pad : fp->Pads() )
1725 {
1726 int netCode = pad->GetNetCode();
1727
1728 if( netCode > 0 )
1729 netEndpoints[netCode].push_back( pad->GetPosition() );
1730 }
1731 }
1732
1733 // Now check each arc
1734 for( PCB_TRACK* track : board->Tracks() )
1735 {
1736 if( track->Type() != PCB_ARC_T )
1737 continue;
1738
1739 arcCount++;
1740 PCB_ARC* arc = static_cast<PCB_ARC*>( track );
1741 int netCode = arc->GetNetCode();
1742
1743 VECTOR2I arcStart = arc->GetStart();
1744 VECTOR2I arcEnd = arc->GetEnd();
1745
1746 // Check if arc endpoints connect to something
1747 bool startConnected = false;
1748 bool endConnected = false;
1749 const int tolerance = 1000; // 1um tolerance
1750
1751 for( const VECTOR2I& pt : netEndpoints[netCode] )
1752 {
1753 if( ( pt - arcStart ).EuclideanNorm() < tolerance )
1754 startConnected = true;
1755
1756 if( ( pt - arcEnd ).EuclideanNorm() < tolerance )
1757 endConnected = true;
1758 }
1759
1760 // Arc should connect to at least one other track/pad at each end
1761 // (unless it's an isolated arc, which is unusual but possible)
1762 if( !startConnected && !endConnected && netEndpoints[netCode].size() > 2 )
1763 {
1764 disconnectedArcs++;
1765 BOOST_TEST_MESSAGE( boardName << ": Arc at ("
1766 << arcStart.x / 1000000.0 << ", "
1767 << arcStart.y / 1000000.0 << ") to ("
1768 << arcEnd.x / 1000000.0 << ", "
1769 << arcEnd.y / 1000000.0
1770 << ") appears disconnected from net " << netCode );
1771 }
1772 }
1773
1774 if( arcCount > 0 )
1775 {
1776 BOOST_TEST_MESSAGE( boardName << ": Found " << arcCount << " arcs, "
1777 << disconnectedArcs << " disconnected" );
1778 }
1779
1780 // Allow some disconnected arcs as they may be legitimate isolated features
1781 // but flag if more than 20% are disconnected
1782 if( arcCount > 5 )
1783 {
1784 BOOST_CHECK_LE( disconnectedArcs, arcCount / 5 );
1785 }
1786 }
1787 }
1788}
1789
1790
1807{
1808 wxString layer;
1809 int recordId = 0;
1810 wxString netName;
1811 double minX = 1e18, minY = 1e18, maxX = -1e18, maxY = -1e18;
1813
1814 void AddPoint( double aX, double aY )
1815 {
1816 minX = std::min( minX, aX );
1817 minY = std::min( minY, aY );
1818 maxX = std::max( maxX, aX );
1819 maxY = std::max( maxY, aY );
1820 segmentCount++;
1821 }
1822};
1823
1824
1826{
1827 std::set<wxString> netNames;
1828 std::set<wxString> refDes;
1829 std::map<wxString, wxString> refDesToSymName;
1830 std::map<wxString, std::set<wxString>> netToRefDes;
1831
1832 std::vector<ALG_ZONE_POLYGON> zonePolygons;
1833
1834 static std::vector<wxString> SplitAlgLine( const wxString& aLine )
1835 {
1836 std::vector<wxString> fields;
1837 wxString current;
1838
1839 for( size_t i = 0; i < aLine.size(); ++i )
1840 {
1841 if( aLine[i] == '!' )
1842 {
1843 fields.push_back( current );
1844 current.clear();
1845 }
1846 else
1847 {
1848 current += aLine[i];
1849 }
1850 }
1851
1852 if( !current.empty() )
1853 fields.push_back( current );
1854
1855 return fields;
1856 }
1857
1862 static int ParseRecordId( const wxString& aTag )
1863 {
1864 long val = -1;
1865 wxString tag = aTag.BeforeFirst( ' ' );
1866 tag.ToLong( &val );
1867 return static_cast<int>( val );
1868 }
1869
1870 static ALG_REFERENCE_DATA ParseAlgFile( const std::string& aPath )
1871 {
1872 ALG_REFERENCE_DATA data;
1873 std::ifstream file( aPath );
1874
1875 if( !file.is_open() )
1876 return data;
1877
1878 enum class SECTION
1879 {
1880 UNKNOWN,
1881 NET_NODES,
1882 SYM_PLACEMENT,
1883 GRAPHICS,
1884 };
1885
1886 SECTION currentSection = SECTION::UNKNOWN;
1887 std::string line;
1888
1889 // Accumulate zone segments grouped by (layer, recordId)
1890 std::map<std::pair<wxString, int>, ALG_ZONE_POLYGON> zoneMap;
1891
1892 while( std::getline( file, line ) )
1893 {
1894 if( line.empty() || line[0] == 'J' )
1895 continue;
1896
1897 if( line[0] == 'A' )
1898 {
1899 if( line.find( "NET_NAME_SORT!NODE_SORT!NET_NAME!REFDES!" ) != std::string::npos )
1900 currentSection = SECTION::NET_NODES;
1901 else if( line.find( "SYM_TYPE!SYM_NAME!REFDES!SYM_MIRROR!" ) != std::string::npos )
1902 currentSection = SECTION::SYM_PLACEMENT;
1903 else if( line.find( "CLASS!SUBCLASS!RECORD_TAG!GRAPHIC_DATA_NAME!" ) != std::string::npos )
1904 currentSection = SECTION::GRAPHICS;
1905 else
1906 currentSection = SECTION::UNKNOWN;
1907
1908 continue;
1909 }
1910
1911 if( line[0] != 'S' )
1912 continue;
1913
1914 auto fields = SplitAlgLine( wxString::FromUTF8( line ) );
1915
1916 switch( currentSection )
1917 {
1918 case SECTION::NET_NODES:
1919 {
1920 // S!sort!nodeSort!NET_NAME!REFDES!PIN!PIN_NAME!SUBCLASS!
1921 if( fields.size() >= 5 )
1922 {
1923 wxString netName = fields[3];
1924 wxString refdes = fields[4];
1925
1926 if( !netName.empty() )
1927 {
1928 data.netNames.insert( netName );
1929
1930 if( !refdes.empty() )
1931 data.netToRefDes[netName].insert( refdes );
1932 }
1933 }
1934
1935 break;
1936 }
1937 case SECTION::SYM_PLACEMENT:
1938 {
1939 // S!SYM_TYPE!SYM_NAME!REFDES!MIRROR!ROTATE!X!Y!CX!CY!LIB_PATH!
1940 if( fields.size() >= 4 )
1941 {
1942 wxString symType = fields[1];
1943 wxString symName = fields[2];
1944 wxString refdes = fields[3];
1945
1946 if( symType == wxT( "PACKAGE" ) && !refdes.empty() )
1947 {
1948 data.refDes.insert( refdes );
1949 data.refDesToSymName[refdes] = symName;
1950 }
1951 }
1952
1953 break;
1954 }
1955 case SECTION::GRAPHICS:
1956 {
1957 // Field layout (0-indexed after splitting on '!'):
1958 // 0=S, 1=CLASS, 2=SUBCLASS, 3=RECORD_TAG, 4=GRAPHIC_DATA_NAME,
1959 // 5=GRAPHIC_DATA_NUMBER, 6..15=GRAPHIC_DATA_1..10,
1960 // 16=PIN_NUMBER, ..., 23=NET_NAME
1961 if( fields.size() < 16 || fields[1] != wxT( "BOUNDARY" ) )
1962 break;
1963
1964 wxString closureType = fields[15];
1965
1966 if( closureType != wxT( "SHAPE" ) )
1967 break;
1968
1969 wxString layer = fields[2];
1970 int recordId = ParseRecordId( fields[3] );
1971
1972 if( recordId < 0 )
1973 break;
1974
1975 wxString netName;
1976
1977 if( fields.size() > 23 )
1978 netName = fields[23];
1979
1980 auto key = std::make_pair( layer, recordId );
1981 auto& zone = zoneMap[key];
1982 zone.layer = layer;
1983 zone.recordId = recordId;
1984
1985 if( !netName.empty() )
1986 zone.netName = netName;
1987
1988 double x1 = 0, y1 = 0, x2 = 0, y2 = 0;
1989
1990 if( fields.size() > 9 )
1991 {
1992 fields[6].ToDouble( &x1 );
1993 fields[7].ToDouble( &y1 );
1994 fields[8].ToDouble( &x2 );
1995 fields[9].ToDouble( &y2 );
1996 zone.AddPoint( x1, y1 );
1997 zone.AddPoint( x2, y2 );
1998 }
1999
2000 break;
2001 }
2002 default:
2003 break;
2004 }
2005 }
2006
2007 for( auto& [key, zone] : zoneMap )
2008 data.zonePolygons.push_back( std::move( zone ) );
2009
2010 return data;
2011 }
2012};
2013
2014
2016{
2017 std::string brdFile;
2018 std::string algFile;
2019};
2020
2021
2026static std::vector<BRD_ALG_PAIR> getBoardsWithAlg()
2027{
2028 std::string dataPath = KI_TEST::AllegroBoardDataDir( "" );
2029 std::vector<BRD_ALG_PAIR> boardsWithAlg;
2030
2031 for( const auto& boardDir : std::filesystem::directory_iterator( dataPath ) )
2032 {
2033 if( !boardDir.is_directory() )
2034 continue;
2035
2036 std::filesystem::path boardPath;
2037 std::filesystem::path algPath;
2038
2039 for( const auto& entry : std::filesystem::directory_iterator( boardDir ) )
2040 {
2041 if( !entry.is_regular_file() )
2042 continue;
2043
2044 if( entry.path().extension() == ".brd" )
2045 boardPath = entry.path();
2046 else if( entry.path().extension() == ".alg" )
2047 algPath = entry.path();
2048
2049 if( !boardPath.empty() && !algPath.empty() )
2050 {
2051 boardsWithAlg.push_back( { boardPath.string(), algPath.string() } );
2052 break;
2053 }
2054 }
2055 }
2056
2057 return boardsWithAlg;
2058}
2059
2060
2064BOOST_AUTO_TEST_CASE( AlgReferenceNetNames )
2065{
2066 std::vector<BRD_ALG_PAIR> boardsWithAlg = getBoardsWithAlg();
2067
2068 BOOST_REQUIRE_GT( boardsWithAlg.size(), 0u );
2069
2070 for( const auto& [brdFile, algFile] : boardsWithAlg )
2071 {
2072 BOOST_TEST_MESSAGE( "Validating net names: " << brdFile );
2073
2075 BOOST_REQUIRE_GT( algData.netNames.size(), 0u );
2076
2077 BOARD* board = GetCachedBoard( brdFile );
2078 BOOST_REQUIRE( board );
2079
2080 std::set<wxString> boardNets;
2081
2082 for( const NETINFO_ITEM* net : board->GetNetInfo() )
2083 {
2084 if( net->GetNetCode() > 0 )
2085 boardNets.insert( net->GetNetname() );
2086 }
2087
2088 int missingNets = 0;
2089
2090 for( const wxString& algNet : algData.netNames )
2091 {
2092 if( boardNets.find( algNet ) == boardNets.end() )
2093 {
2094 missingNets++;
2095
2096 if( missingNets <= 10 )
2097 BOOST_TEST_MESSAGE( " Missing net: " << algNet );
2098 }
2099 }
2100
2101 BOOST_TEST_MESSAGE( brdFile << ": .alg has " << algData.netNames.size() << " nets, board has "
2102 << boardNets.size() << ", missing " << missingNets );
2103
2104 BOOST_CHECK_EQUAL( missingNets, 0 );
2105 }
2106}
2107
2108
2112BOOST_AUTO_TEST_CASE( AlgReferenceComponentPlacement )
2113{
2114 std::vector<BRD_ALG_PAIR> boardsWithAlg = getBoardsWithAlg();
2115
2116 BOOST_REQUIRE_GT( boardsWithAlg.size(), 0u );
2117
2118 for( const auto& [brdFile, algFile] : boardsWithAlg )
2119 {
2120 BOOST_TEST_MESSAGE( "Validating components: " << brdFile );
2121
2123 BOOST_REQUIRE_GT( algData.refDes.size(), 0u );
2124
2125 BOARD* board = GetCachedBoard( brdFile );
2126 BOOST_REQUIRE( board );
2127
2128 std::set<wxString> boardRefDes;
2129
2130 for( const FOOTPRINT* fp : board->Footprints() )
2131 boardRefDes.insert( fp->GetReference() );
2132
2133 int missingRefDes = 0;
2134 int extraRefDes = 0;
2135
2136 for( const wxString& algRef : algData.refDes )
2137 {
2138 if( boardRefDes.find( algRef ) == boardRefDes.end() )
2139 {
2140 missingRefDes++;
2141
2142 if( missingRefDes <= 10 )
2143 BOOST_TEST_MESSAGE( " Missing refdes: " << algRef );
2144 }
2145 }
2146
2147 for( const wxString& boardRef : boardRefDes )
2148 {
2149 if( algData.refDes.find( boardRef ) == algData.refDes.end() )
2150 {
2151 extraRefDes++;
2152
2153 if( extraRefDes <= 10 )
2154 BOOST_TEST_MESSAGE( " Extra refdes in board: " << boardRef );
2155 }
2156 }
2157
2158 BOOST_TEST_MESSAGE( brdFile << ": .alg has " << algData.refDes.size()
2159 << " components, board has " << boardRefDes.size()
2160 << ", missing " << missingRefDes
2161 << ", extra " << extraRefDes );
2162
2163 BOOST_CHECK_EQUAL( missingRefDes, 0 );
2164 }
2165}
2166
2167
2171BOOST_AUTO_TEST_CASE( AllTracksPositiveWidth )
2172{
2173 std::vector<std::string> boards = GetAllBoardFiles();
2174
2175 for( const std::string& boardPath : boards )
2176 {
2177 std::string boardName = std::filesystem::path( boardPath ).filename().string();
2178 BOARD* board = GetCachedBoard( boardPath );
2179
2180 if( !board )
2181 continue;
2182
2183 BOOST_TEST_CONTEXT( "Testing board: " << boardName )
2184 {
2185 int zeroWidthCount = 0;
2186 int totalCount = 0;
2187
2188 for( PCB_TRACK* track : board->Tracks() )
2189 {
2190 if( track->Type() == PCB_TRACE_T || track->Type() == PCB_ARC_T )
2191 {
2192 totalCount++;
2193
2194 if( track->GetWidth() <= 0 )
2195 {
2196 zeroWidthCount++;
2197
2198 if( zeroWidthCount <= 5 )
2199 {
2200 BOOST_TEST_MESSAGE( boardName << ": Zero-width track at ("
2201 << track->GetStart().x / 1000000.0 << ", "
2202 << track->GetStart().y / 1000000.0 << ")" );
2203 }
2204 }
2205 }
2206 }
2207
2208 BOOST_CHECK_EQUAL( zeroWidthCount, 0 );
2209 }
2210 }
2211}
2212
2213
2221{
2222 enum class SEGMENT_TYPE
2223 {
2227 };
2228
2230 {
2232 double x1, y1, x2, y2;
2235 };
2236
2239 std::vector<OUTLINE_SEGMENT> designOutlineSegments;
2240 std::vector<OUTLINE_SEGMENT> outlineSegments;
2241
2242 double minX = std::numeric_limits<double>::max();
2243 double minY = std::numeric_limits<double>::max();
2244 double maxX = std::numeric_limits<double>::lowest();
2245 double maxY = std::numeric_limits<double>::lowest();
2246
2247 void updateBounds( double aX, double aY )
2248 {
2249 minX = std::min( minX, aX );
2250 minY = std::min( minY, aY );
2251 maxX = std::max( maxX, aX );
2252 maxY = std::max( maxY, aY );
2253 }
2254
2255 static ALG_OUTLINE_DATA ParseAlgOutlines( const std::string& aPath )
2256 {
2257 ALG_OUTLINE_DATA data;
2258 std::ifstream file( aPath );
2259
2260 if( !file.is_open() )
2261 return data;
2262
2263 std::string line;
2264
2265 while( std::getline( file, line ) )
2266 {
2267 if( line.empty() || line[0] != 'S' )
2268 continue;
2269
2270 auto fields = ALG_REFERENCE_DATA::SplitAlgLine( wxString::FromUTF8( line ) );
2271
2272 if( fields.size() < 10 )
2273 continue;
2274
2275 bool isDesignOutline = ( fields[1] == wxT( "BOARD GEOMETRY" )
2276 && fields[2] == wxT( "DESIGN_OUTLINE" ) );
2277
2278 bool isOutline = ( fields[1] == wxT( "BOARD GEOMETRY" )
2279 && fields[2] == wxT( "OUTLINE" ) );
2280
2281 if( !isDesignOutline && !isOutline )
2282 continue;
2283
2284 wxString shapeType = fields[4];
2285 OUTLINE_SEGMENT seg = {};
2286
2287 if( shapeType == wxT( "LINE" ) && fields.size() >= 10 )
2288 {
2290 fields[6].ToCDouble( &seg.x1 );
2291 fields[7].ToCDouble( &seg.y1 );
2292 fields[8].ToCDouble( &seg.x2 );
2293 fields[9].ToCDouble( &seg.y2 );
2294
2295 data.updateBounds( seg.x1, seg.y1 );
2296 data.updateBounds( seg.x2, seg.y2 );
2297 }
2298 else if( shapeType == wxT( "ARC" ) && fields.size() >= 15 )
2299 {
2300 seg.type = SEGMENT_TYPE::ARC;
2301 fields[6].ToCDouble( &seg.x1 );
2302 fields[7].ToCDouble( &seg.y1 );
2303 fields[8].ToCDouble( &seg.x2 );
2304 fields[9].ToCDouble( &seg.y2 );
2305 fields[10].ToCDouble( &seg.centerX );
2306 fields[11].ToCDouble( &seg.centerY );
2307 fields[12].ToCDouble( &seg.radius );
2308 seg.clockwise = ( fields[14] == wxT( "CLOCKWISE" ) );
2309
2310 data.updateBounds( seg.x1, seg.y1 );
2311 data.updateBounds( seg.x2, seg.y2 );
2312 }
2313 else if( shapeType == wxT( "RECTANGLE" ) && fields.size() >= 10 )
2314 {
2316 fields[6].ToCDouble( &seg.x1 );
2317 fields[7].ToCDouble( &seg.y1 );
2318 fields[8].ToCDouble( &seg.x2 );
2319 fields[9].ToCDouble( &seg.y2 );
2320
2321 data.updateBounds( seg.x1, seg.y1 );
2322 data.updateBounds( seg.x2, seg.y2 );
2323 }
2324 else
2325 {
2326 continue;
2327 }
2328
2329 if( isDesignOutline )
2330 {
2331 data.designOutlineCount++;
2332 data.designOutlineSegments.push_back( seg );
2333 }
2334 else
2335 {
2336 data.outlineCount++;
2337 data.outlineSegments.push_back( seg );
2338 }
2339 }
2340
2341 return data;
2342 }
2343
2349 {
2350 int count = 0;
2351
2352 for( const auto& seg : designOutlineSegments )
2353 {
2354 if( seg.type == SEGMENT_TYPE::RECTANGLE )
2355 count += 4;
2356 else
2357 count += 1;
2358 }
2359
2360 return count;
2361 }
2362};
2363
2364
2369BOOST_AUTO_TEST_CASE( OutlineSegmentCount )
2370{
2371 std::vector<BRD_ALG_PAIR> testBoards = getBoardsWithAlg();
2372
2373 BOOST_REQUIRE_GT( testBoards.size(), 0u );
2374
2375 for( const auto& [brdFile, algFile] : testBoards )
2376 {
2377 BOOST_TEST_CONTEXT( "Board: " << brdFile )
2378 {
2380
2381 if( algOutlines.designOutlineCount == 0 && algOutlines.outlineCount == 0 )
2382 {
2383 BOOST_TEST_MESSAGE( " No outline records in .alg, skipping" );
2384 continue;
2385 }
2386
2387 BOARD* board = GetCachedBoard( brdFile );
2388 BOOST_REQUIRE( board );
2389
2390 int edgeCutsCount = 0;
2391
2392 for( BOARD_ITEM* item : board->Drawings() )
2393 {
2394 if( item->Type() == PCB_SHAPE_T && item->GetLayer() == Edge_Cuts )
2395 edgeCutsCount++;
2396 }
2397
2398 int expectedCount = algOutlines.expectedEdgeCutsSegments();
2399
2400 BOOST_TEST_MESSAGE( " .alg DESIGN_OUTLINE records: " << algOutlines.designOutlineCount
2401 << " -> expected Edge_Cuts segments: " << expectedCount );
2402 BOOST_TEST_MESSAGE( " .alg OUTLINE records: " << algOutlines.outlineCount );
2403 BOOST_TEST_MESSAGE( " Binary import Edge_Cuts segments: " << edgeCutsCount );
2404
2405 BOOST_CHECK_EQUAL( edgeCutsCount, expectedCount );
2406 }
2407 }
2408}
2409
2410
2415BOOST_AUTO_TEST_CASE( OutlineBoundingBox )
2416{
2417 std::vector<BRD_ALG_PAIR> testBoards = getBoardsWithAlg();
2418
2419 BOOST_REQUIRE_GT( testBoards.size(), 0u );
2420
2421 // 1 mil = 25400 nm
2422 const double milToNm = 25400.0;
2423
2424 // Allow 2 mil tolerance for coordinate rounding across formats
2425 const int toleranceNm = static_cast<int>( 2.0 * milToNm );
2426
2427 for( const auto& [brdFile, algFile] : testBoards )
2428 {
2429 BOOST_TEST_CONTEXT( "Board: " << brdFile )
2430 {
2432
2433 if( algOutlines.designOutlineCount == 0 )
2434 {
2435 BOOST_TEST_MESSAGE( " No DESIGN_OUTLINE records in .alg, skipping" );
2436 continue;
2437 }
2438
2439 BOARD* board = GetCachedBoard( brdFile );
2440 BOOST_REQUIRE( board );
2441
2442 BOX2I boardBbox;
2443 bool hasBbox = false;
2444
2445 for( BOARD_ITEM* item : board->Drawings() )
2446 {
2447 if( item->Type() == PCB_SHAPE_T && item->GetLayer() == Edge_Cuts )
2448 {
2449 if( !hasBbox )
2450 {
2451 boardBbox = item->GetBoundingBox();
2452 hasBbox = true;
2453 }
2454 else
2455 {
2456 boardBbox.Merge( item->GetBoundingBox() );
2457 }
2458 }
2459 }
2460
2461 BOOST_REQUIRE_MESSAGE( hasBbox, "Board should have Edge_Cuts outline" );
2462
2463 // Convert .alg bounding box from mils to nm
2464 int algMinXnm = static_cast<int>( algOutlines.minX * milToNm );
2465 int algMinYnm = static_cast<int>( algOutlines.minY * milToNm );
2466 int algMaxXnm = static_cast<int>( algOutlines.maxX * milToNm );
2467 int algMaxYnm = static_cast<int>( algOutlines.maxY * milToNm );
2468 int algWidthNm = algMaxXnm - algMinXnm;
2469 int algHeightNm = algMaxYnm - algMinYnm;
2470
2471 int boardWidth = boardBbox.GetWidth();
2472 int boardHeight = boardBbox.GetHeight();
2473
2474 BOOST_TEST_MESSAGE( " .alg extent (mils): "
2475 << algOutlines.minX << "," << algOutlines.minY << " to "
2476 << algOutlines.maxX << "," << algOutlines.maxY
2477 << " = " << ( algOutlines.maxX - algOutlines.minX ) << " x "
2478 << ( algOutlines.maxY - algOutlines.minY ) );
2479 BOOST_TEST_MESSAGE( " Board bbox (nm): "
2480 << boardBbox.GetLeft() << "," << boardBbox.GetTop() << " to "
2481 << boardBbox.GetRight() << "," << boardBbox.GetBottom()
2482 << " = " << boardWidth << " x " << boardHeight );
2483 BOOST_TEST_MESSAGE( " .alg (nm): " << algWidthNm << " x " << algHeightNm );
2484
2485 // KiCad bounding boxes include line width so allow 3% tolerance
2486 BOOST_CHECK_CLOSE( static_cast<double>( boardWidth ),
2487 static_cast<double>( algWidthNm ), 3.0 );
2488 BOOST_CHECK_CLOSE( static_cast<double>( boardHeight ),
2489 static_cast<double>( algHeightNm ), 3.0 );
2490 }
2491 }
2492}
2493
2494
2502BOOST_AUTO_TEST_CASE( OutlineEndpoints )
2503{
2504 std::vector<BRD_ALG_PAIR> testBoards = getBoardsWithAlg();
2505
2506 BOOST_REQUIRE_GT( testBoards.size(), 0u );
2507
2508 const double milToNm = 25400.0;
2509 const int toleranceNm = static_cast<int>( 2.0 * milToNm );
2510
2511 for( const auto& [brdFile, algFile] : testBoards )
2512 {
2513 BOOST_TEST_CONTEXT( "Board: " << brdFile )
2514 {
2516
2517 if( algOutlines.designOutlineCount == 0 )
2518 continue;
2519
2520 // Only validate endpoint-by-endpoint for pure-LINE outlines
2521 bool allLines = true;
2522
2523 for( const auto& seg : algOutlines.designOutlineSegments )
2524 {
2527 {
2528 allLines = false;
2529 break;
2530 }
2531 }
2532
2533 if( !allLines )
2534 {
2535 BOOST_TEST_MESSAGE( " Outline has arcs, skipping endpoint-level validation" );
2536 continue;
2537 }
2538
2539 BOARD* board = GetCachedBoard( brdFile );
2540 BOOST_REQUIRE( board );
2541
2542 // Collect all Edge_Cuts segment endpoints
2543 struct ENDPOINT_PAIR
2544 {
2545 VECTOR2I start;
2546 VECTOR2I end;
2547 };
2548
2549 std::vector<ENDPOINT_PAIR> boardSegments;
2550
2551 for( BOARD_ITEM* item : board->Drawings() )
2552 {
2553 if( item->Type() != PCB_SHAPE_T || item->GetLayer() != Edge_Cuts )
2554 continue;
2555
2556 PCB_SHAPE* shape = static_cast<PCB_SHAPE*>( item );
2557
2558 if( shape->GetShape() == SHAPE_T::SEGMENT )
2559 boardSegments.push_back( { shape->GetStart(), shape->GetEnd() } );
2560 }
2561
2562 // Build expected segments from .alg, expanding RECTANGLEs into 4 segments
2563 std::vector<ENDPOINT_PAIR> algSegments;
2564
2565 for( const auto& seg : algOutlines.designOutlineSegments )
2566 {
2567 if( seg.type == ALG_OUTLINE_DATA::SEGMENT_TYPE::LINE )
2568 {
2569 VECTOR2I start( static_cast<int>( seg.x1 * milToNm ),
2570 static_cast<int>( seg.y1 * milToNm ) );
2571 VECTOR2I end( static_cast<int>( seg.x2 * milToNm ),
2572 static_cast<int>( seg.y2 * milToNm ) );
2573 algSegments.push_back( { start, end } );
2574 }
2575 else if( seg.type == ALG_OUTLINE_DATA::SEGMENT_TYPE::RECTANGLE )
2576 {
2577 int x1 = static_cast<int>( seg.x1 * milToNm );
2578 int y1 = static_cast<int>( seg.y1 * milToNm );
2579 int x2 = static_cast<int>( seg.x2 * milToNm );
2580 int y2 = static_cast<int>( seg.y2 * milToNm );
2581
2582 algSegments.push_back( { { x1, y1 }, { x2, y1 } } );
2583 algSegments.push_back( { { x2, y1 }, { x2, y2 } } );
2584 algSegments.push_back( { { x2, y2 }, { x1, y2 } } );
2585 algSegments.push_back( { { x1, y2 }, { x1, y1 } } );
2586 }
2587 }
2588
2589 BOOST_CHECK_EQUAL( boardSegments.size(), algSegments.size() );
2590
2591 if( boardSegments.size() != algSegments.size() )
2592 continue;
2593
2594 // Match each .alg segment to a board segment by finding closest start/end pair.
2595 // Allegro and KiCad may have opposite Y axis, so we compare using absolute
2596 // coordinate deltas.
2597 int matchedCount = 0;
2598
2599 std::vector<bool> used( boardSegments.size(), false );
2600
2601 for( size_t ai = 0; ai < algSegments.size(); ++ai )
2602 {
2603 const auto& algSeg = algSegments[ai];
2604 int bestIdx = -1;
2605 int64_t bestDist = std::numeric_limits<int64_t>::max();
2606
2607 for( size_t bi = 0; bi < boardSegments.size(); ++bi )
2608 {
2609 if( used[bi] )
2610 continue;
2611
2612 const auto& bSeg = boardSegments[bi];
2613
2614 // Try both orientations (start-start or start-end swap)
2615 auto dist = [&]( const VECTOR2I& aAlgPt, const VECTOR2I& aBoardPt ) -> int64_t
2616 {
2617 int64_t dx = std::abs( static_cast<int64_t>( aAlgPt.x )
2618 - static_cast<int64_t>( aBoardPt.x ) );
2619 int64_t dy = std::abs( static_cast<int64_t>( aAlgPt.y )
2620 - static_cast<int64_t>( aBoardPt.y ) );
2621 return dx + dy;
2622 };
2623
2624 int64_t d1 = dist( algSeg.start, bSeg.start ) + dist( algSeg.end, bSeg.end );
2625 int64_t d2 = dist( algSeg.start, bSeg.end ) + dist( algSeg.end, bSeg.start );
2626 int64_t d = std::min( d1, d2 );
2627
2628 if( d < bestDist )
2629 {
2630 bestDist = d;
2631 bestIdx = static_cast<int>( bi );
2632 }
2633 }
2634
2635 if( bestIdx >= 0 && bestDist < 2LL * toleranceNm )
2636 {
2637 used[bestIdx] = true;
2638 matchedCount++;
2639 }
2640 else
2641 {
2642 BOOST_TEST_MESSAGE( " Unmatched .alg segment " << ai << ": ("
2643 << algSeg.start.x / 1000000.0 << ", "
2644 << algSeg.start.y / 1000000.0 << ") -> ("
2645 << algSeg.end.x / 1000000.0 << ", "
2646 << algSeg.end.y / 1000000.0 << ") mm"
2647 << " bestDist=" << bestDist );
2648 }
2649 }
2650
2651 BOOST_TEST_MESSAGE( " Matched " << matchedCount << " / " << algSegments.size()
2652 << " outline segments" );
2653
2654 BOOST_CHECK_EQUAL( matchedCount, static_cast<int>( algSegments.size() ) );
2655 }
2656 }
2657}
2658
2659
2664BOOST_AUTO_TEST_CASE( PadDrillConsistency )
2665{
2666 std::vector<std::string> boards = GetAllBoardFiles();
2667
2668 for( const std::string& boardPath : boards )
2669 {
2670 std::string boardName = std::filesystem::path( boardPath ).filename().string();
2671 BOARD* board = GetCachedBoard( boardPath );
2672
2673 if( !board )
2674 continue;
2675
2676 BOOST_TEST_CONTEXT( "Testing board: " << boardName )
2677 {
2678 int pthNoDrill = 0;
2679 int smdWithDrill = 0;
2680
2681 for( FOOTPRINT* fp : board->Footprints() )
2682 {
2683 for( PAD* pad : fp->Pads() )
2684 {
2685 PAD_ATTRIB attr = pad->GetAttribute();
2686 bool hasDrill = pad->GetDrillSize().x > 0;
2687
2688 if( attr == PAD_ATTRIB::PTH && !hasDrill )
2689 {
2690 pthNoDrill++;
2691
2692 if( pthNoDrill <= 5 )
2693 {
2694 BOOST_TEST_MESSAGE( boardName << ": PTH pad without drill: "
2695 << fp->GetReference() << "."
2696 << pad->GetNumber() );
2697 }
2698 }
2699
2700 if( attr == PAD_ATTRIB::SMD && hasDrill )
2701 {
2702 smdWithDrill++;
2703
2704 if( smdWithDrill <= 5 )
2705 {
2706 BOOST_TEST_MESSAGE( boardName << ": SMD pad with drill: "
2707 << fp->GetReference() << "."
2708 << pad->GetNumber() );
2709 }
2710 }
2711 }
2712 }
2713
2714 BOOST_CHECK_EQUAL( pthNoDrill, 0 );
2715 BOOST_CHECK_EQUAL( smdWithDrill, 0 );
2716 }
2717 }
2718}
2719
2720
2724BOOST_AUTO_TEST_CASE( ZoneCountMatchesAlg )
2725{
2726 std::vector<BRD_ALG_PAIR> boardsWithAlg = getBoardsWithAlg();
2727
2728 BOOST_REQUIRE_GT( boardsWithAlg.size(), 0u );
2729
2730 for( const auto& [brdFile, algFile] : boardsWithAlg )
2731 {
2732 BOOST_TEST_CONTEXT( "Zone count: " << brdFile )
2733 {
2735
2736 BOARD* board = GetCachedBoard( brdFile );
2737 BOOST_REQUIRE( board );
2738
2739 size_t boardCopperZoneLayers = 0;
2740
2741 for( const ZONE* zone : board->Zones() )
2742 {
2743 if( !zone->GetIsRuleArea() )
2744 boardCopperZoneLayers += ( zone->GetLayerSet() & LSET::AllCuMask() ).count();
2745 }
2746
2747 size_t algZoneCount = algData.zonePolygons.size();
2748
2749 BOOST_TEST_MESSAGE( brdFile << ": .alg has " << algZoneCount
2750 << " zone polygons, board has " << boardCopperZoneLayers
2751 << " copper zone-layers" );
2752
2753 BOOST_CHECK_EQUAL( static_cast<size_t>( boardCopperZoneLayers ), algZoneCount );
2754 }
2755 }
2756}
2757
2758
2762BOOST_AUTO_TEST_CASE( ZoneLayerDistribution )
2763{
2764 std::vector<BRD_ALG_PAIR> boardsWithAlg = getBoardsWithAlg();
2765
2766 BOOST_REQUIRE_GT( boardsWithAlg.size(), 0u );
2767
2768 for( const auto& [brdFile, algFile] : boardsWithAlg )
2769 {
2770 BOOST_TEST_CONTEXT( "Zone layers: " << brdFile )
2771 {
2773
2774 BOARD* board = GetCachedBoard( brdFile );
2775 BOOST_REQUIRE( board );
2776
2777 std::map<wxString, int> algLayerCounts;
2778
2779 for( const ALG_ZONE_POLYGON& zone : algData.zonePolygons )
2780 algLayerCounts[zone.layer]++;
2781
2782 std::map<wxString, int> boardLayerCounts;
2783
2784 for( const ZONE* zone : board->Zones() )
2785 {
2786 if( zone->GetIsRuleArea() )
2787 continue;
2788
2789 for( PCB_LAYER_ID layer : zone->GetLayerSet().Seq() )
2790 {
2791 if( IsCopperLayer( layer ) )
2792 boardLayerCounts[board->GetLayerName( layer )]++;
2793 }
2794 }
2795
2796 BOOST_TEST_MESSAGE( brdFile << " layer distribution:" );
2797
2798 for( const auto& [layer, count] : algLayerCounts )
2799 {
2800 auto it = boardLayerCounts.find( layer );
2801 int boardCount = ( it != boardLayerCounts.end() ) ? it->second : 0;
2802
2803 BOOST_TEST_MESSAGE( " " << layer << ": .alg=" << count << " board=" << boardCount );
2804 BOOST_CHECK_EQUAL( boardCount, count );
2805 }
2806 }
2807 }
2808}
2809
2810
2815BOOST_AUTO_TEST_CASE( ZoneBoundingBoxes )
2816{
2817 std::vector<BRD_ALG_PAIR> boardsWithAlg = getBoardsWithAlg();
2818
2819 BOOST_REQUIRE_GT( boardsWithAlg.size(), 0u );
2820
2821 for( const auto& [brdFile, algFile] : boardsWithAlg )
2822 {
2823 BOOST_TEST_CONTEXT( "Zone bboxes: " << brdFile )
2824 {
2826
2827 BOARD* board = GetCachedBoard( brdFile );
2828 BOOST_REQUIRE( board );
2829
2830 // Collect sorted areas per layer from .alg and board, then compare distributions
2831 const double milsToNm = 25400.0;
2832
2833 std::map<wxString, std::vector<double>> algAreas;
2834
2835 for( const ALG_ZONE_POLYGON& zone : algData.zonePolygons )
2836 {
2837 double w = ( zone.maxX - zone.minX ) * milsToNm;
2838 double h = ( zone.maxY - zone.minY ) * milsToNm;
2839 algAreas[zone.layer].push_back( w * h );
2840 }
2841
2842 std::map<wxString, std::vector<double>> boardAreas;
2843
2844 for( const ZONE* zone : board->Zones() )
2845 {
2846 if( zone->GetIsRuleArea() )
2847 continue;
2848
2849 BOX2I bbox = zone->GetBoundingBox();
2850 double area = static_cast<double>( bbox.GetWidth() )
2851 * static_cast<double>( bbox.GetHeight() );
2852
2853 for( PCB_LAYER_ID layer : zone->GetLayerSet().Seq() )
2854 {
2855 if( IsCopperLayer( layer ) )
2856 boardAreas[board->GetLayerName( layer )].push_back( area );
2857 }
2858 }
2859
2860 int matched = 0;
2861 int mismatched = 0;
2862
2863 for( auto& [layer, algList] : algAreas )
2864 {
2865 std::sort( algList.begin(), algList.end() );
2866 auto it = boardAreas.find( layer );
2867
2868 if( it == boardAreas.end() || it->second.size() != algList.size() )
2869 continue;
2870
2871 std::sort( it->second.begin(), it->second.end() );
2872
2873 for( size_t i = 0; i < algList.size(); ++i )
2874 {
2875 double ref = std::max( algList[i], 1.0 );
2876 double err = std::abs( it->second[i] - algList[i] ) / ref;
2877
2878 if( err < 0.10 )
2879 {
2880 matched++;
2881 }
2882 else
2883 {
2884 mismatched++;
2885
2886 if( mismatched <= 5 )
2887 {
2888 BOOST_TEST_MESSAGE( " " << layer << " index " << i
2889 << ": alg area " << algList[i] / ( milsToNm * milsToNm )
2890 << " sq mils vs board area "
2891 << it->second[i] / ( milsToNm * milsToNm )
2892 << " sq mils" );
2893 }
2894 }
2895 }
2896 }
2897
2898 BOOST_TEST_MESSAGE( brdFile << ": " << matched << " zone areas matched, "
2899 << mismatched << " mismatched" );
2900
2901 int total = matched + mismatched;
2902
2903 if( total > 0 )
2904 {
2905 BOOST_CHECK_GT( matched, total * 8 / 10 );
2906 }
2907 }
2908 }
2909}
2910
2911
2918BOOST_AUTO_TEST_CASE( PadContainedInFabOutline )
2919{
2920 std::string dataPath = KI_TEST::AllegroBoardFile( "BeagleBone_Black_RevC/BeagleBone_Black_RevC.brd" );
2921
2922 BOARD* board = GetCachedBoard( dataPath );
2923 BOOST_REQUIRE( board );
2924
2925 // Footprints known to have pads and fab outlines that enclose them.
2926 // P6 and P10 are excluded: they are bottom-side connectors whose assembly outlines
2927 // only cover the housing, not the full pin field.
2928 const std::set<wxString> targetRefs = { wxS( "J1" ), wxS( "P5" ), wxS( "U5" ),
2929 wxS( "U13" ), wxS( "C78" ) };
2930
2931 int testedCount = 0;
2932 int failedCount = 0;
2933
2934 for( FOOTPRINT* fp : board->Footprints() )
2935 {
2936 if( targetRefs.find( fp->GetReference() ) == targetRefs.end() )
2937 continue;
2938
2939 PCB_LAYER_ID fabLayer = fp->IsFlipped() ? B_Fab : F_Fab;
2940
2941 BOX2I fabBbox;
2942 bool hasFab = false;
2943
2944 for( BOARD_ITEM* item : fp->GraphicalItems() )
2945 {
2946 if( item->GetLayer() == fabLayer )
2947 {
2948 if( !hasFab )
2949 {
2950 fabBbox = item->GetBoundingBox();
2951 hasFab = true;
2952 }
2953 else
2954 {
2955 fabBbox.Merge( item->GetBoundingBox() );
2956 }
2957 }
2958 }
2959
2960 if( !hasFab || fp->Pads().empty() )
2961 continue;
2962
2963 // Allow generous tolerance: pad centers can extend slightly beyond the fab outline
2964 // (e.g. edge-mount connectors, thermal pads). 3mm handles most cases.
2965 BOX2I testBbox = fabBbox;
2966 testBbox.Inflate( 3000000 );
2967
2968 BOOST_TEST_CONTEXT( "Footprint " << fp->GetReference() )
2969 {
2970 testedCount++;
2971
2972 for( PAD* pad : fp->Pads() )
2973 {
2974 VECTOR2I padCenter = pad->GetPosition();
2975
2976 if( !testBbox.Contains( padCenter ) )
2977 {
2978 failedCount++;
2979 BOOST_TEST_MESSAGE( fp->GetReference() << " pad " << pad->GetNumber()
2980 << " at (" << padCenter.x / 1e6 << ", "
2981 << padCenter.y / 1e6 << ") mm is outside F.Fab bbox" );
2982 }
2983 }
2984 }
2985 }
2986
2987 BOOST_TEST_MESSAGE( "Tested " << testedCount << " footprints for pad containment" );
2988 BOOST_CHECK_GE( testedCount, 4 );
2989 BOOST_CHECK_EQUAL( failedCount, 0 );
2990}
2991
2992
2998BOOST_AUTO_TEST_CASE( PadOrientationP6P10 )
2999{
3000 std::string dataPath = KI_TEST::AllegroBoardFile( "BeagleBone_Black_RevC/BeagleBone_Black_RevC.brd" );
3001
3002 BOARD* board = GetCachedBoard( dataPath );
3003 BOOST_REQUIRE( board );
3004
3005 for( FOOTPRINT* fp : board->Footprints() )
3006 {
3007 wxString ref = fp->GetReference();
3008
3009 if( ref != wxS( "P6" ) && ref != wxS( "P10" ) )
3010 continue;
3011
3012 BOOST_TEST_CONTEXT( "Footprint " << ref )
3013 {
3014 for( PAD* pad : fp->Pads() )
3015 {
3016 long padNum = 0;
3017
3018 if( !pad->GetNumber().ToLong( &padNum ) )
3019 continue;
3020
3021 // Pads 1-19 on P6/P10 are rectangular SMD pads that should be wider than tall
3022 if( padNum < 1 || padNum > 19 )
3023 continue;
3024
3025 // GetBoundingBox accounts for rotation, giving visual dimensions
3026 BOX2I bbox = pad->GetBoundingBox();
3027 auto bboxW = bbox.GetWidth();
3028 auto bboxH = bbox.GetHeight();
3029
3030 // Skip square/circular pads where orientation doesn't affect shape
3031 if( bboxW == bboxH )
3032 continue;
3033
3034 BOOST_TEST_CONTEXT( "Pad " << pad->GetNumber() )
3035 {
3036 BOOST_CHECK_MESSAGE( bboxW > bboxH,
3037 ref << " pad " << pad->GetNumber()
3038 << " should be visually wider than tall: "
3039 << bboxW / 1e6 << " x " << bboxH / 1e6 << " mm" );
3040 }
3041 }
3042 }
3043 }
3044}
3045
3046
3055{
3056 std::string dataPath = KI_TEST::AllegroBoardFile( "BeagleBone_Black_RevC/BeagleBone_Black_RevC.brd" );
3057
3058 BOARD* board = GetCachedBoard( dataPath );
3059 BOOST_REQUIRE( board );
3060
3061 int oblongCount = 0;
3062
3063 for( FOOTPRINT* fp : board->Footprints() )
3064 {
3065 for( PAD* pad : fp->Pads() )
3066 {
3067 VECTOR2I drillSize = pad->GetDrillSize();
3068
3069 if( drillSize.x <= 0 || drillSize.y <= 0 )
3070 continue;
3071
3072 if( drillSize.x == drillSize.y )
3073 continue;
3074
3075 oblongCount++;
3076
3077 BOOST_TEST_CONTEXT( fp->GetReference() << " pad " << pad->GetNumber() )
3078 {
3079 BOOST_CHECK( pad->GetDrillShape() == PAD_DRILL_SHAPE::OBLONG );
3080
3081 BOOST_TEST_MESSAGE( fp->GetReference() << " pad " << pad->GetNumber()
3082 << " slot: " << drillSize.x / 1e6 << " x "
3083 << drillSize.y / 1e6 << " mm"
3084 << " attr=" << static_cast<int>( pad->GetAttribute() ) );
3085 }
3086 }
3087 }
3088
3089 BOOST_TEST_MESSAGE( "Found " << oblongCount << " oblong drill holes" );
3090 BOOST_CHECK_EQUAL( oblongCount, 7 );
3091}
3092
3093
3099BOOST_AUTO_TEST_CASE( FootprintOrientation )
3100{
3101 std::string dataPath = KI_TEST::AllegroBoardFile( "BeagleBone_Black_RevC/BeagleBone_Black_RevC.brd" );
3102
3103 BOARD* board = GetCachedBoard( dataPath );
3104
3105 BOOST_REQUIRE( board );
3106
3107 FOOTPRINT* j1 = nullptr;
3108
3109 for( FOOTPRINT* fp : board->Footprints() )
3110 {
3111 if( fp->GetReference() == wxT( "J1" ) )
3112 {
3113 j1 = fp;
3114 break;
3115 }
3116 }
3117
3118 BOOST_REQUIRE_MESSAGE( j1 != nullptr, "Footprint J1 must exist in BeagleBone_Black_RevC" );
3119
3120 EDA_ANGLE orientation = j1->GetOrientation();
3121 BOOST_TEST_MESSAGE( "J1 orientation: " << orientation.AsDegrees() << " degrees" );
3122 BOOST_CHECK_CLOSE( orientation.AsDegrees(), 90.0, 0.1 );
3123}
3124
3125
3130BOOST_AUTO_TEST_CASE( UIImportPath_NullBoard )
3131{
3132 std::string dataPath = KI_TEST::AllegroBoardFile( "ProiectBoard/ProiectBoard.brd" );
3133
3134 PCB_IO_ALLEGRO plugin;
3136 plugin.SetReporter( &reporter );
3137
3138 std::unique_ptr<BOARD> board;
3139
3140 try
3141 {
3142 board = plugin.LoadBoard( dataPath );
3143 }
3144 catch( const IO_ERROR& e )
3145 {
3146 BOOST_TEST_MESSAGE( "IO_ERROR: " << e.What() );
3147 }
3148 catch( const std::exception& e )
3149 {
3150 BOOST_TEST_MESSAGE( "Exception: " << e.what() );
3151 }
3152
3153 reporter.PrintAllMessages( "UIImportPath_NullBoard" );
3154
3155 BOOST_REQUIRE_MESSAGE( board != nullptr, "LoadBoard must return a valid board" );
3156
3157 BOOST_CHECK_GT( board->GetNetCount(), 0 );
3158 BOOST_CHECK_GT( board->Footprints().size(), 0 );
3159 BOOST_CHECK_GT( board->Tracks().size(), 0 );
3160 BOOST_CHECK_EQUAL( reporter.GetErrorCount(), 0 );
3161
3162 PrintBoardStats( board.get(), "ProiectBoard (UI path)" );
3163}
3164
3165
3174BOOST_AUTO_TEST_CASE( SmdPadLayerConsistency )
3175{
3176 std::vector<std::string> boards = GetAllBoardFiles();
3177
3178 for( const std::string& boardPath : boards )
3179 {
3180 std::string boardName = std::filesystem::path( boardPath ).filename().string();
3181 BOARD* board = GetCachedBoard( boardPath );
3182
3183 if( !board )
3184 continue;
3185
3186 BOOST_TEST_CONTEXT( "Testing board: " << boardName )
3187 {
3188 int inconsistentCount = 0;
3189
3190 for( FOOTPRINT* fp : board->Footprints() )
3191 {
3192 const bool onBottom = fp->IsFlipped();
3193
3194 for( PAD* pad : fp->Pads() )
3195 {
3196 if( pad->GetAttribute() != PAD_ATTRIB::SMD )
3197 continue;
3198
3199 LSET layers = pad->GetLayerSet();
3200 bool hasTopCopper = layers.Contains( F_Cu );
3201 bool hasBotCopper = layers.Contains( B_Cu );
3202
3203 if( onBottom && hasTopCopper && !hasBotCopper )
3204 {
3205 inconsistentCount++;
3206 BOOST_TEST_MESSAGE( boardName << ": " << fp->GetReference() << " pad "
3207 << pad->GetNumber() << " is on bottom footprint but SMD "
3208 << "pad has F.Cu without B.Cu" );
3209 }
3210 else if( !onBottom && hasBotCopper && !hasTopCopper )
3211 {
3212 inconsistentCount++;
3213 BOOST_TEST_MESSAGE( boardName << ": " << fp->GetReference() << " pad "
3214 << pad->GetNumber() << " is on top footprint but SMD "
3215 << "pad has B.Cu without F.Cu" );
3216 }
3217 }
3218 }
3219
3220 BOOST_CHECK_EQUAL( inconsistentCount, 0 );
3221 }
3222 }
3223}
3224
3225
3234BOOST_AUTO_TEST_CASE( SmdFootprintTechLayers )
3235{
3236 // Note that this test is NOT true for all boards - some boards have SMD FPs with
3237 // back-layer items.
3238 std::vector<std::string> boards = {
3239 KI_TEST::AllegroBoardFile( "EVK_BaseBoard/EVK_BaseBoard.brd" ),
3240 };
3241
3242 for( const std::string& boardPath : boards )
3243 {
3244 std::string boardName = std::filesystem::path( boardPath ).filename().string();
3245 BOARD* board = GetCachedBoard( boardPath );
3246
3247 BOOST_REQUIRE( board );
3248
3249 BOOST_TEST_CONTEXT( "Testing board: " << boardName )
3250 {
3251 int inconsistentCount = 0;
3252 int checkedFootprints = 0;
3253
3254 for( FOOTPRINT* fp : board->Footprints() )
3255 {
3256 // Only check SMD-only footprints (no through-hole pads)
3257 bool hasSmd = false;
3258 bool hasTH = false;
3259
3260 for( PAD* pad : fp->Pads() )
3261 {
3262 if( pad->GetAttribute() == PAD_ATTRIB::SMD )
3263 hasSmd = true;
3264 else if( pad->GetAttribute() == PAD_ATTRIB::PTH )
3265 hasTH = true;
3266 }
3267
3268 if( !hasSmd || hasTH )
3269 continue;
3270
3271 checkedFootprints++;
3272
3273 const bool onBottom = fp->IsFlipped();
3274
3275 for( BOARD_ITEM* item : fp->GraphicalItems() )
3276 {
3277 PCB_LAYER_ID layer = item->GetLayer();
3278
3279 if( !IsFrontLayer( layer ) && !IsBackLayer( layer ) )
3280 continue;
3281
3282 bool wrongSide = false;
3283
3284 if( onBottom && IsFrontLayer( layer ) )
3285 wrongSide = true;
3286 else if( !onBottom && IsBackLayer( layer ) )
3287 wrongSide = true;
3288
3289 if( wrongSide )
3290 {
3291 inconsistentCount++;
3293 boardName << ": " << fp->GetReference() << " is "
3294 << ( onBottom ? "bottom" : "top" ) << "-side SMD but has "
3295 << item->GetClass() << " on "
3296 << board->GetLayerName( layer ) );
3297 }
3298 }
3299 }
3300
3301 BOOST_TEST_MESSAGE( "Checked " << checkedFootprints
3302 << " SMD-only footprints for tech layer consistency" );
3303 BOOST_CHECK_EQUAL( inconsistentCount, 0 );
3304 }
3305 }
3306}
3307
3308
3315BOOST_AUTO_TEST_CASE( BeagleBone_DrillSlotOrientation )
3316{
3317 std::string dataPath = KI_TEST::AllegroBoardFile( "BeagleBone_Black_RevC/BeagleBone_Black_RevC.brd" );
3318
3319 BOARD* board = GetCachedBoard( dataPath );
3320 BOOST_REQUIRE( board );
3321
3322 struct SLOT_CHECK
3323 {
3324 wxString fpRef;
3325 wxString padNum;
3326 };
3327
3328 // These pads have vertical oblong copper shapes and should have taller-than-wide drill slots
3329 std::vector<SLOT_CHECK> checks = {
3330 { wxS( "P6" ), wxS( "20" ) },
3331 { wxS( "P6" ), wxS( "21" ) },
3332 { wxS( "P3" ), wxS( "6" ) },
3333 { wxS( "P1" ), wxS( "1" ) },
3334 { wxS( "P1" ), wxS( "2" ) },
3335 { wxS( "P1" ), wxS( "3" ) },
3336 };
3337
3338 for( const auto& check : checks )
3339 {
3340 BOOST_TEST_CONTEXT( check.fpRef << " pad " << check.padNum )
3341 {
3342 FOOTPRINT* fp = nullptr;
3343
3344 for( FOOTPRINT* candidate : board->Footprints() )
3345 {
3346 if( candidate->GetReference() == check.fpRef )
3347 {
3348 fp = candidate;
3349 break;
3350 }
3351 }
3352
3353 BOOST_REQUIRE_MESSAGE( fp != nullptr, "Footprint " << check.fpRef << " should exist" );
3354
3355 PAD* pad = nullptr;
3356
3357 for( PAD* candidate : fp->Pads() )
3358 {
3359 if( candidate->GetNumber() == check.padNum )
3360 {
3361 pad = candidate;
3362 break;
3363 }
3364 }
3365
3366 BOOST_REQUIRE_MESSAGE( pad != nullptr,
3367 "Pad " << check.padNum << " should exist on " << check.fpRef );
3368
3369 VECTOR2I padSize = pad->GetSize( F_Cu );
3370 VECTOR2I drillSize = pad->GetDrillSize();
3371
3372 BOOST_TEST_MESSAGE( check.fpRef << " pad " << check.padNum
3373 << ": pad=" << padSize.x << "x" << padSize.y
3374 << " drill=" << drillSize.x << "x" << drillSize.y );
3375
3376 // If the pad is oblong, the drill should match the pad's aspect ratio
3377 if( drillSize.x != drillSize.y )
3378 {
3379 bool padIsTaller = ( padSize.y > padSize.x );
3380 bool drillIsTaller = ( drillSize.y > drillSize.x );
3381
3382 BOOST_CHECK_MESSAGE( padIsTaller == drillIsTaller,
3383 "Drill slot should match pad orientation" );
3384 }
3385 }
3386 }
3387}
3388
3389
3394BOOST_AUTO_TEST_CASE( BeagleBone_ZoneFills )
3395{
3396 std::string dataPath = KI_TEST::AllegroBoardFile( "BeagleBone_Black_RevC/BeagleBone_Black_RevC.brd" );
3397
3398 BOARD* board = GetCachedBoard( dataPath );
3399 BOOST_REQUIRE( board );
3400
3401 int filledZoneCount = 0;
3402 int totalCopperZones = 0;
3403
3404 for( const ZONE* zone : board->Zones() )
3405 {
3406 if( zone->GetIsRuleArea() || zone->GetNetCode() == 0 )
3407 continue;
3408
3409 totalCopperZones++;
3410
3411 if( zone->IsFilled() )
3412 {
3413 filledZoneCount++;
3414
3415 BOOST_TEST_MESSAGE( "Filled zone: net=" << zone->GetNetname()
3416 << " layers=" << zone->GetLayerSet().count() );
3417 }
3418 }
3419
3420 BOOST_TEST_MESSAGE( "Total copper zones: " << totalCopperZones
3421 << ", filled: " << filledZoneCount );
3422
3423 BOOST_CHECK_GT( totalCopperZones, 0 );
3424 BOOST_CHECK_GT( filledZoneCount, 0 );
3425}
3426
3427
3432BOOST_AUTO_TEST_CASE( BeagleBone_Teardrops )
3433{
3434 std::string dataPath = KI_TEST::AllegroBoardFile( "BeagleBone_Black_RevC/BeagleBone_Black_RevC.brd" );
3435
3436 BOARD* board = GetCachedBoard( dataPath );
3437 BOOST_REQUIRE( board );
3438
3439 int teardropZones = 0;
3440
3441 for( const ZONE* zone : board->Zones() )
3442 {
3443 if( zone->IsTeardropArea() )
3444 teardropZones++;
3445 }
3446
3447 BOOST_CHECK_GT( teardropZones, 1000 );
3448 BOOST_TEST_MESSAGE( "Teardrop zones: " << teardropZones );
3449
3450 // Pads and vias anchoring teardrops must have teardrops enabled
3451 int padsWithTeardrops = 0;
3452 int totalPads = 0;
3453
3454 for( const FOOTPRINT* fp : board->Footprints() )
3455 {
3456 for( const PAD* pad : fp->Pads() )
3457 {
3458 totalPads++;
3459
3460 if( pad->GetTeardropsEnabled() )
3461 padsWithTeardrops++;
3462 }
3463 }
3464
3465 BOOST_CHECK_GT( padsWithTeardrops, 0 );
3466 BOOST_TEST_MESSAGE( "Pads with teardrops enabled: " << padsWithTeardrops << " / " << totalPads );
3467
3468 int viasWithTeardrops = 0;
3469 int totalVias = 0;
3470
3471 for( const PCB_TRACK* track : board->Tracks() )
3472 {
3473 if( track->Type() != PCB_VIA_T )
3474 continue;
3475
3476 totalVias++;
3477
3478 if( static_cast<const PCB_VIA*>( track )->GetTeardropsEnabled() )
3479 viasWithTeardrops++;
3480 }
3481
3482 BOOST_CHECK_GT( viasWithTeardrops, 0 );
3483 BOOST_TEST_MESSAGE( "Vias with teardrops enabled: " << viasWithTeardrops << " / " << totalVias );
3484}
3485
3486
3487BOOST_AUTO_TEST_CASE( BeagleBone_TeardropFillsHaveNoArcs )
3488{
3489 std::string dataPath = KI_TEST::AllegroBoardFile( "BeagleBone_Black_RevC/BeagleBone_Black_RevC.brd" );
3490
3491 BOARD* board = GetCachedBoard( dataPath );
3492 BOOST_REQUIRE( board );
3493
3494 int arcFills = 0;
3495
3496 for( const ZONE* zone : board->Zones() )
3497 {
3498 if( !zone->IsTeardropArea() )
3499 continue;
3500
3501 for( PCB_LAYER_ID layer : zone->GetLayerSet() )
3502 {
3503 if( !zone->HasFilledPolysForLayer( layer ) )
3504 continue;
3505
3506 const std::shared_ptr<SHAPE_POLY_SET> fill = zone->GetFilledPolysList( layer );
3507
3508 for( int i = 0; i < fill->OutlineCount(); ++i )
3509 {
3510 if( fill->COutline( i ).ArcCount() > 0 )
3511 ++arcFills;
3512 }
3513 }
3514 }
3515
3516 BOOST_CHECK_EQUAL( arcFills, 0 );
3517}
3518
3519
3524BOOST_AUTO_TEST_CASE( PreV172_NoTeardrops )
3525{
3526 std::string dataPath = KI_TEST::AllegroBoardFile( "ProiectBoard/ProiectBoard.brd" );
3527
3528 BOARD* board = GetCachedBoard( dataPath );
3529 BOOST_REQUIRE( board );
3530
3531 int teardropZones = 0;
3532
3533 for( const ZONE* zone : board->Zones() )
3534 {
3535 if( zone->IsTeardropArea() )
3536 teardropZones++;
3537 }
3538
3539 BOOST_CHECK_EQUAL( teardropZones, 0 );
3540
3541 int padsWithTeardrops = 0;
3542
3543 for( const FOOTPRINT* fp : board->Footprints() )
3544 {
3545 for( const PAD* pad : fp->Pads() )
3546 {
3547 if( pad->GetTeardropsEnabled() )
3548 padsWithTeardrops++;
3549 }
3550 }
3551
3552 BOOST_CHECK_EQUAL( padsWithTeardrops, 0 );
3553}
3554
3555
3561BOOST_AUTO_TEST_CASE( LegacyNetclassFlags )
3562{
3563 std::string dataPath = KI_TEST::AllegroBoardFile( "TRS80_POWER/TRS80_POWER.brd" );
3564
3565 PCB_IO_ALLEGRO plugin;
3567 plugin.SetReporter( &reporter );
3568
3569 std::unique_ptr<BOARD> board = plugin.LoadBoard( dataPath );
3570
3571 BOOST_REQUIRE( board );
3572
3573 BOOST_CHECK_MESSAGE( board->m_LegacyNetclassesLoaded,
3574 "m_LegacyNetclassesLoaded must be true after Allegro import" );
3575 BOOST_CHECK_MESSAGE( board->m_LegacyDesignSettingsLoaded,
3576 "m_LegacyDesignSettingsLoaded must be true after Allegro import" );
3577}
3578
3579
3584BOOST_AUTO_TEST_CASE( NetclassesCreatedForAllBoards )
3585{
3586 std::vector<std::string> boards = GetAllBoardFiles();
3587
3588 for( const std::string& boardPath : boards )
3589 {
3590 std::string boardName = std::filesystem::path( boardPath ).filename().string();
3591 BOARD* board = GetCachedBoard( boardPath );
3592
3593 if( !board )
3594 continue;
3595
3596 BOOST_TEST_CONTEXT( "Testing board: " << boardName )
3597 {
3598 std::shared_ptr<NET_SETTINGS> netSettings = board->GetDesignSettings().m_NetSettings;
3599 const auto& netclasses = netSettings->GetNetclasses();
3600
3601 BOOST_TEST_MESSAGE( boardName << ": " << netclasses.size() << " netclasses" );
3602
3603 for( const auto& [name, nc] : netclasses )
3604 {
3605 BOOST_TEST_MESSAGE( " " << name << ": track="
3606 << nc->GetTrackWidth() << " clearance="
3607 << nc->GetClearance() );
3608
3609 // Constraint set netclasses should have positive track width.
3610 // Skip generated netclasses (DP_, MG_, W*mil) which may not set track width.
3611 if( !name.StartsWith( wxS( "DP_" ) )
3612 && !name.StartsWith( wxS( "MG_" ) )
3613 && !name.StartsWith( wxS( "W" ) ) )
3614 {
3615 BOOST_CHECK_MESSAGE( nc->HasTrackWidth(),
3616 name << " should have a track width" );
3617 BOOST_CHECK_MESSAGE( nc->GetTrackWidth() > 0,
3618 name << " track width should be positive" );
3619 }
3620 }
3621 }
3622 }
3623}
3624
3625
const char * name
General utilities for PCB file IO for QA programs.
BOX2< VECTOR2I > BOX2I
Definition box2.h:914
A base class derived from BOARD_ITEM for items that can be connected and have a net,...
std::shared_ptr< NET_SETTINGS > m_NetSettings
A base class for any item which can be embedded within the BOARD container class, and therefore insta...
Definition board_item.h:85
virtual PCB_LAYER_ID GetLayer() const
Return the primary layer this item is on.
Information pertinent to a Pcbnew printed circuit board.
Definition board.h:411
const NETINFO_LIST & GetNetInfo() const
Definition board.h:1217
const ZONES & Zones() const
Definition board.h:469
PCB_LAYER_ID GetLayerID(const wxString &aLayerName) const
Return the ID of a layer.
Definition board.cpp:912
const FOOTPRINTS & Footprints() const
Definition board.h:465
const TRACKS & Tracks() const
Definition board.h:463
const wxString GetLayerName(PCB_LAYER_ID aLayer) const
Return the name of a aLayer.
Definition board.cpp:932
BOARD_DESIGN_SETTINGS & GetDesignSettings()
Definition board.cpp:1290
const DRAWINGS & Drawings() const
Definition board.h:467
constexpr BOX2< Vec > & Inflate(coord_type dx, coord_type dy)
Inflates the rectangle horizontally by dx and vertically by dy.
Definition box2.h:553
constexpr size_type GetWidth() const
Definition box2.h:211
constexpr BOX2< Vec > & Merge(const BOX2< Vec > &aRect)
Modify the position and size of the rectangle in order to contain aRect.
Definition box2.h:594
constexpr size_type GetHeight() const
Definition box2.h:212
constexpr coord_type GetLeft() const
Definition box2.h:225
constexpr bool Contains(const Vec &aPoint) const
Definition box2.h:165
constexpr coord_type GetRight() const
Definition box2.h:214
constexpr coord_type GetTop() const
Definition box2.h:226
constexpr coord_type GetBottom() const
Definition box2.h:219
EDA_ANGLE Normalize()
Definition eda_angle.h:228
double AsDegrees() const
Definition eda_angle.h:115
KICAD_T Type() const
Returns the type of object.
Definition eda_item.h:110
std::vector< VECTOR2I > GetPolyPoints() const
Duplicate the polygon outlines into a flat list of VECTOR2I points.
SHAPE_T GetShape() const
Definition eda_shape.h:177
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
EDA_ANGLE GetOrientation() const
Definition footprint.h:382
std::deque< PAD * > & Pads()
Definition footprint.h:355
PCB_LAYER_ID GetLayer() const override
Return the primary layer this item is on.
Definition footprint.h:393
bool IsFlipped() const
Definition footprint.h:620
const std::vector< wxString > & GetNetTiePadGroups() const
Definition footprint.h:567
PAD * FindPadByNumber(const wxString &aPadNumber, PAD *aSearchAfterMe=nullptr) const
Return a PAD with a matching number.
VECTOR3D m_Offset
3D model offset (mm)
Definition footprint.h:134
VECTOR3D m_Rotation
3D model rotation (degrees)
Definition footprint.h:133
virtual void SetReporter(REPORTER *aReporter)
Set an optional reporter for warnings/errors.
Definition io_base.h:89
Hold an error message and may be used when throwing exceptions containing meaningful error messages.
virtual const wxString What() const
A composite of Problem() and Where()
virtual const char * what() const override
std::exception interface, returned as UTF-8
static ALLEGRO_CACHED_LOADER & GetInstance()
Get the singleton instance of the Allegro board cache loader.
BOARD * GetCachedBoard(const std::string &aFilePath)
Get a cached board for the given file path, or load it if not already cached, without forcing a reloa...
Custom REPORTER that captures all messages for later analysis in the unit test framework.
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
bool Contains(PCB_LAYER_ID aLayer) const
See if the layer set contains a PCB layer.
Definition lset.h:63
Handle the data for a net.
Definition netinfo.h:50
static const int UNCONNECTED
Constant that holds the "unconnected net" number (typically 0) all items "connected" to this net are ...
Definition netinfo.h:281
Definition pad.h:61
bool IsOnLayer(PCB_LAYER_ID aLayer) const override
Test to see if this object is on the given layer.
Definition pad.h:924
std::unique_ptr< BOARD > LoadBoard(const wxString &aFileName, const std::map< std::string, UTF8 > *aProperties=nullptr, PROJECT *aProject=nullptr)
Load information from some input file format that this PCB_IO implementation knows about into new BOA...
Definition pcb_io.cpp:72
PCB_LAYER_ID GetLayer() const override
Return the primary layer this item is on.
Definition pcb_shape.h:70
const VECTOR2I & GetStart() const
Definition pcb_track.h:98
const VECTOR2I & GetEnd() const
Definition pcb_track.h:95
Definition seg.h:38
Represent a set of closed polygons.
double Area()
Return the area of this poly set.
void BooleanIntersection(const SHAPE_POLY_SET &b)
Perform boolean polyset intersection.
Handle a list of polygons defining a copper zone.
Definition zone.h:70
bool IsFilled() const
Definition zone.h:306
PCB_LAYER_ID GetFirstLayer() const
Definition zone.cpp:630
static constexpr EDA_ANGLE ANGLE_0
Definition eda_angle.h:448
@ NO_RECURSE
Definition eda_item.h:52
@ SEGMENT
Definition eda_shape.h:56
@ RECTANGLE
Use RECTANGLE instead of RECT to avoid collision in a Windows header.
Definition eda_shape.h:57
@ FP_BOARD_ONLY
Definition footprint.h:90
bool IsFrontLayer(PCB_LAYER_ID aLayerId)
Layer classification: check if it's a front layer.
Definition layer_ids.h:844
bool IsBackLayer(PCB_LAYER_ID aLayerId)
Layer classification: check if it's a back layer.
Definition layer_ids.h:867
bool IsCopperLayer(int aLayerId)
Test whether a layer is a copper layer.
Definition layer_ids.h:741
bool IsInnerCopperLayer(int aLayerId)
Test whether a layer is an inner (In1_Cu to In30_Cu) copper layer.
Definition layer_ids.h:763
PCB_LAYER_ID
A quick note on layer IDs:
Definition layer_ids.h:56
@ Edge_Cuts
Definition layer_ids.h:108
@ F_Paste
Definition layer_ids.h:100
@ B_Mask
Definition layer_ids.h:94
@ B_Cu
Definition layer_ids.h:61
@ F_Mask
Definition layer_ids.h:93
@ B_Paste
Definition layer_ids.h:101
@ F_Fab
Definition layer_ids.h:115
@ UNDEFINED_LAYER
Definition layer_ids.h:57
@ F_Cu
Definition layer_ids.h:60
@ B_Fab
Definition layer_ids.h:114
std::array< SEG, 4 > BoxToSegs(const BOX2I &aBox)
Decompose a BOX2 into four segments.
Test utilities for timestamps.
std::string AllegroBoardDataDir(const std::string &aBoardName)
void PrintBoardStats(const BOARD *aBoard, const std::string &aBoardName)
Print detailed board statistics for debugging using test-framework logging.
void FillZones(BOARD *m_board)
std::string AllegroBoardFile(const std::string &aFileName)
EDA_ANGLE abs(const EDA_ANGLE &aAngle)
Definition eda_angle.h:437
PAD_ATTRIB
The set of pad shapes, used with PAD::{Set,Get}Attribute().
Definition padstack.h:96
@ NPTH
like PAD_PTH, but not plated mechanical use only, no connection allowed
Definition padstack.h:102
@ SMD
Smd pad, appears on the solder paste layer (default)
Definition padstack.h:98
@ PTH
Plated through hole pad.
Definition padstack.h:97
std::deque< BOARD_ITEM * > DRAWINGS
Utility functions for working with shapes.
Parse board outline geometry from a .alg ASCII reference file.
void updateBounds(double aX, double aY)
static ALG_OUTLINE_DATA ParseAlgOutlines(const std::string &aPath)
std::vector< OUTLINE_SEGMENT > designOutlineSegments
std::vector< OUTLINE_SEGMENT > outlineSegments
int expectedEdgeCutsSegments() const
Expected number of Edge_Cuts segments when translating to KiCad.
std::set< wxString > netNames
static ALG_REFERENCE_DATA ParseAlgFile(const std::string &aPath)
std::map< wxString, wxString > refDesToSymName
std::set< wxString > refDes
static std::vector< wxString > SplitAlgLine(const wxString &aLine)
std::vector< ALG_ZONE_POLYGON > zonePolygons
static int ParseRecordId(const wxString &aTag)
Extract the integer record ID from a RECORD_TAG field like "36 1 0".
std::map< wxString, std::set< wxString > > netToRefDes
Parse a FabMaster .alg file and extract reference data for cross-validation.
void AddPoint(double aX, double aY)
Data for parameterized all-boards test.
Fixture for comprehensive board import tests with error capturing.
BOARD * GetCachedBoard(const std::string &aFilePath)
Get a cached board, loading it on first access.
static std::vector< std::string > GetAllBoardFiles()
Get list of all .brd files in the Allegro test data directory.
std::unique_ptr< BOARD > LoadAllegroBoard(const std::string &aFileName)
BOOST_AUTO_TEST_CASE(FootprintRefDes)
Test that footprints have valid reference designators.
static void AssertOutlineValid(const BOARD &aBoard)
static std::vector< BRD_ALG_PAIR > getBoardsWithAlg()
Get a list of all board files in the test data that have a corresponding .alg reference file.
static unsigned CountOutlineElements(const BOARD &board)
BOOST_AUTO_TEST_CASE(HorizontalAlignment)
BOOST_REQUIRE(intersection.has_value()==c.ExpectedIntersection.has_value())
BOOST_AUTO_TEST_SUITE_END()
IbisParser parser & reporter
KIBIS top(path, &reporter)
VECTOR3I expected(15, 30, 45)
VECTOR2I end
BOOST_TEST_CONTEXT("Test Clearance")
BOOST_TEST_MESSAGE("Polyline has "<< chain.PointCount()<< " points")
BOOST_CHECK_EQUAL(result, "25.4")
@ PCB_SHAPE_T
class PCB_SHAPE, a segment not on copper layers
Definition typeinfo.h:80
@ PCB_VIA_T
class PCB_VIA, a via (like a track segment on a copper layer)
Definition typeinfo.h:89
@ PCB_GROUP_T
class PCB_GROUP, a set of BOARD_ITEMs
Definition typeinfo.h:103
@ PCB_TEXT_T
class PCB_TEXT, text on a layer
Definition typeinfo.h:84
@ PCB_ARC_T
class PCB_ARC, an arc track segment on a copper layer
Definition typeinfo.h:90
@ PCB_TRACE_T
class PCB_TRACK, a track segment (segment on a copper layer)
Definition typeinfo.h:88
VECTOR2< int32_t > VECTOR2I
Definition vector2d.h:708