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 <filesystem>
49#include <functional>
50#include <fstream>
51#include <map>
52#include <set>
53
54using namespace KI_TEST;
55
56
58{
60
61 std::unique_ptr<BOARD> LoadAllegroBoard( const std::string& aFileName )
62 {
63 std::string dataPath = KI_TEST::AllegroBoardFile( aFileName );
64
65 return m_allegroPlugin.LoadBoard( dataPath );
66 }
67
69};
70
71
72BOOST_FIXTURE_TEST_SUITE( AllegroImport, ALLEGRO_IMPORT_FIXTURE )
73
74
75
78BOOST_AUTO_TEST_CASE( FootprintRefDes )
79{
80 std::unique_ptr<BOARD> board = LoadAllegroBoard( "TRS80_POWER/TRS80_POWER.brd" );
81
82 BOOST_REQUIRE( board != nullptr );
83
84 int emptyRefDesCount = 0;
85 int validRefDesCount = 0;
86
87 for( FOOTPRINT* fp : board->Footprints() )
88 {
89 wxString refdes = fp->GetReference();
90
91 if( refdes.IsEmpty() )
92 emptyRefDesCount++;
93 else
94 validRefDesCount++;
95 }
96
97 BOOST_TEST_MESSAGE( "Valid RefDes: " << validRefDesCount << ", Empty: " << emptyRefDesCount );
98
99 // Most footprints should have valid reference designators
100 BOOST_CHECK_GT( validRefDesCount, emptyRefDesCount );
101}
102
103
108{
109 std::unique_ptr<BOARD> board = LoadAllegroBoard( "TRS80_POWER/TRS80_POWER.brd" );
110
111 BOOST_REQUIRE( board != nullptr );
112
113 int validPadCount = 0;
114 int zeroPadCount = 0;
115 int hugePadCount = 0;
116
117 for( FOOTPRINT* fp : board->Footprints() )
118 {
119 for( PAD* pad : fp->Pads() )
120 {
121 VECTOR2I size = pad->GetSize( F_Cu );
122
123 if( size.x == 0 || size.y == 0 )
124 {
125 zeroPadCount++;
126 BOOST_TEST_MESSAGE( "Zero-size pad in " << fp->GetReference() << " pad "
127 << pad->GetNumber() );
128 }
129 else if( size.x > 50000000 || size.y > 50000000 ) // > 50mm is suspicious
130 {
131 hugePadCount++;
132 BOOST_TEST_MESSAGE( "Huge pad in " << fp->GetReference() << " pad " << pad->GetNumber()
133 << ": " << size.x / 1000000.0 << "mm x "
134 << size.y / 1000000.0 << "mm" );
135 }
136 else
137 {
138 validPadCount++;
139 }
140 }
141 }
142
143 BOOST_TEST_MESSAGE( "Valid pads: " << validPadCount << ", Zero: " << zeroPadCount
144 << ", Huge: " << hugePadCount );
145
146 // No pads should be zero-size (this catches hardcoded fallbacks)
147 BOOST_CHECK_EQUAL( zeroPadCount, 0 );
148}
149
150
155{
156 std::unique_ptr<BOARD> board = LoadAllegroBoard( "TRS80_POWER/TRS80_POWER.brd" );
157
158 BOOST_REQUIRE( board != nullptr );
159
160 int validViaCount = 0;
161 int suspiciousViaCount = 0;
162
163 // Count vias with the hardcoded size (1000000 = 1mm exactly)
164 const int HARDCODED_SIZE = 1000000;
165
166 for( PCB_TRACK* track : board->Tracks() )
167 {
168 if( track->Type() == PCB_VIA_T )
169 {
170 PCB_VIA* via = static_cast<PCB_VIA*>( track );
171 int width = via->GetWidth( F_Cu );
172
173 if( width == HARDCODED_SIZE )
174 {
175 suspiciousViaCount++;
176 }
177 else if( width > 0 && width < 10000000 ) // 0 < size < 10mm is reasonable
178 {
179 validViaCount++;
180 }
181 }
182 }
183
184 BOOST_TEST_MESSAGE( "Valid vias: " << validViaCount << ", Hardcoded-size vias: " << suspiciousViaCount );
185
186 // This test will fail until via size is properly extracted from padstack
187 if( suspiciousViaCount > 0 )
188 {
189 BOOST_WARN_MESSAGE( false, "Found " << suspiciousViaCount << " vias with hardcoded 1mm size" );
190 }
191}
192
193
198{
199 std::unique_ptr<BOARD> board = LoadAllegroBoard( "TRS80_POWER/TRS80_POWER.brd" );
200
201 BOOST_REQUIRE( board != nullptr );
202
203 int validTrackCount = 0;
204 int zeroTrackCount = 0;
205
206 for( PCB_TRACK* track : board->Tracks() )
207 {
208 if( track->Type() == PCB_TRACE_T )
209 {
210 int width = track->GetWidth();
211
212 if( width == 0 )
213 zeroTrackCount++;
214 else if( width > 0 && width < 10000000 ) // 0 < width < 10mm
215 validTrackCount++;
216 }
217 }
218
219 BOOST_TEST_MESSAGE( "Valid tracks: " << validTrackCount << ", Zero-width: " << zeroTrackCount );
220
221 BOOST_CHECK_EQUAL( zeroTrackCount, 0 );
222}
223
224
229{
230 std::unique_ptr<BOARD> board = LoadAllegroBoard( "TRS80_POWER/TRS80_POWER.brd" );
231
232 BOOST_REQUIRE( board != nullptr );
233
234 int numberedPads = 0;
235 int unnumberedPads = 0;
236
237 for( FOOTPRINT* fp : board->Footprints() )
238 {
239 for( PAD* pad : fp->Pads() )
240 {
241 if( pad->GetNumber().IsEmpty() )
242 unnumberedPads++;
243 else
244 numberedPads++;
245 }
246 }
247
248 BOOST_TEST_MESSAGE( "Numbered pads: " << numberedPads << ", Unnumbered: " << unnumberedPads );
249
250 // All pads should have numbers for proper netlist generation
251 // This test will fail until pad numbers are properly set
252 if( unnumberedPads > 0 )
253 {
254 BOOST_WARN_MESSAGE( false, "Found " << unnumberedPads << " pads without numbers" );
255 }
256}
257
258
259static unsigned CountOutlineElements( const BOARD& board )
260{
261 unsigned count = 0;
262 for( const BOARD_ITEM* item : board.Drawings() )
263 {
264 if( item->Type() == PCB_SHAPE_T && item->GetLayer() == Edge_Cuts )
265 {
266 count++;
267 }
268 }
269 return count;
270}
271
272
273static void AssertOutlineValid( const BOARD& aBoard )
274{
275 // Verify outline forms a closed contour by checking that all segments connect
276 std::vector<SEG> outlineSegs;
277
278 for( BOARD_ITEM* item : aBoard.Drawings() )
279 {
280 if( item->Type() == PCB_SHAPE_T && item->GetLayer() == Edge_Cuts )
281 {
282 PCB_SHAPE* shape = static_cast<PCB_SHAPE*>( item );
283 switch( shape->GetShape() )
284 {
285 case SHAPE_T::SEGMENT:
286 case SHAPE_T::ARC:
287 case SHAPE_T::BEZIER:
288 {
289 outlineSegs.push_back( SEG( shape->GetStart(), shape->GetEnd() ) );
290 break;
291 }
293 {
294 // Rectangles are stored as a single item but represent 4 segments
295 VECTOR2I start = shape->GetStart();
296 VECTOR2I end = shape->GetEnd();
297
298 for( const auto& seg : KIGEOM::BoxToSegs( BOX2I( start, end ) ) )
299 {
300 outlineSegs.push_back( seg );
301 }
302 break;
303 }
304 case SHAPE_T::POLY:
305 {
306 std::vector<VECTOR2I> polyPoints = shape->GetPolyPoints();
307
308 for( size_t i = 0; i < polyPoints.size() - 1; i++ )
309 {
310 VECTOR2I start = polyPoints[i];
311 VECTOR2I end = polyPoints[( i + 1 ) % polyPoints.size()];
312 outlineSegs.emplace_back( start, end );
313 }
314 break;
315 }
316 case SHAPE_T::CIRCLE:
317 // Not really sure what we can do here? Zero-length seg?
318 outlineSegs.push_back( SEG( shape->GetStart(), shape->GetStart() ) );
319 break;
320 default:
321 BOOST_WARN_MESSAGE(
322 false, "Unexpected shape type in board outline: " << static_cast<int>( shape->GetShape() ) );
323 }
324 }
325 }
326
327 if( !outlineSegs.empty() )
328 {
329 // For a valid closed outline, the sum of all segment lengths should equal the perimeter
330 // and each endpoint should connect to another endpoint
331 int connectedCount = 0;
332
333 for( const SEG& seg : outlineSegs )
334 {
335 for( const SEG& other : outlineSegs )
336 {
337 if( &other == &seg )
338 continue;
339
340 // Check if this shape's start connects to another shape's start or end
341 if( seg.A == other.A || seg.A == other.B )
342 connectedCount++;
343
344 // Check if this shape's end connects to another shape's start or end
345 if( seg.B == other.A || seg.B == other.B )
346 connectedCount++;
347 }
348 }
349
350 // Each segment should connect at both ends for a closed outline
351 // For 4 segments, we expect 8 connections (2 per segment)
352 BOOST_TEST_MESSAGE( "Connected endpoints: " << connectedCount );
353 BOOST_CHECK_GE( connectedCount, outlineSegs.size() * 2 );
354 }
355}
356
357
361BOOST_AUTO_TEST_CASE( BoardOutline )
362{
363 std::unique_ptr<BOARD> board = LoadAllegroBoard( "TRS80_POWER/TRS80_POWER.brd" );
364
365 BOOST_REQUIRE( board != nullptr );
366
367 // Count shapes on Edge_Cuts layer
368 int outlineSegmentCount = CountOutlineElements( *board );
369
370 BOOST_TEST_MESSAGE( "Board outline elements: " << outlineSegmentCount );
371
372 // Board should have an outline - TRS80_POWER.brd has a rectangular outline (4 segments)
373 BOOST_CHECK_GE( outlineSegmentCount, 1 );
374
375 AssertOutlineValid( *board );
376}
377
378
382BOOST_AUTO_TEST_CASE( PadsInsideOutline )
383{
384 std::unique_ptr<BOARD> board = LoadAllegroBoard( "TRS80_POWER/TRS80_POWER.brd" );
385
386 BOOST_REQUIRE( board != nullptr );
387
388 // Get board bounding box from outline
389 BOX2I boardBbox;
390 bool hasBbox = false;
391
392 for( BOARD_ITEM* item : board->Drawings() )
393 {
394 if( item->Type() == PCB_SHAPE_T && item->GetLayer() == Edge_Cuts )
395 {
396 if( !hasBbox )
397 {
398 boardBbox = item->GetBoundingBox();
399 hasBbox = true;
400 }
401 else
402 {
403 boardBbox.Merge( item->GetBoundingBox() );
404 }
405 }
406 }
407
408 BOOST_REQUIRE_MESSAGE( hasBbox, "Board should have an outline" );
409
410 int padsInside = 0;
411 int padsOutside = 0;
412
413 // Inflate bbox slightly to account for edge cases
414 BOX2I testBbox = boardBbox;
415 testBbox.Inflate( 1000 ); // 1mm tolerance
416
417 for( FOOTPRINT* fp : board->Footprints() )
418 {
419 for( PAD* pad : fp->Pads() )
420 {
421 VECTOR2I padCenter = pad->GetPosition();
422
423 if( testBbox.Contains( padCenter ) )
424 {
425 padsInside++;
426 }
427 else
428 {
429 padsOutside++;
430 BOOST_TEST_MESSAGE( "Pad outside outline: " << fp->GetReference() << " pad "
431 << pad->GetNumber() << " at ("
432 << padCenter.x / 1000000.0 << ", "
433 << padCenter.y / 1000000.0 << ") mm" );
434 }
435 }
436 }
437
438 BOOST_TEST_MESSAGE( "Pads inside outline: " << padsInside << ", outside: " << padsOutside );
439
440 // Most pads should be inside the board outline
441 // Some boards may have off-board test points or fiducials
442 if( padsOutside > 0 )
443 {
444 BOOST_WARN_MESSAGE( false, "Found " << padsOutside << " pads outside board outline" );
445 }
446
447 // At minimum, most pads should be inside
448 BOOST_CHECK_GT( padsInside, padsOutside );
449}
450
451
456BOOST_AUTO_TEST_CASE( PreV16FileRejection )
457{
458 BOOST_CHECK_EXCEPTION(
459 LoadAllegroBoard( "v13_header/v13_header.brd" ), IO_ERROR,
460 []( const IO_ERROR& e )
461 {
462 wxString msg = e.What();
463
464 return msg.Contains( wxS( "predates Allegro 16.0" ) )
465 && msg.Contains( wxS( "Allegro PCB Design" ) );
466 } );
467}
468
469
475BOOST_AUTO_TEST_CASE( RectsZoneVsCopperPolygon )
476{
477 std::unique_ptr<BOARD> board = LoadAllegroBoard( "rects/rects.brd" );
478 BOOST_REQUIRE( board );
479
480 // Should have exactly one zone (the left rectangle as a zone fill)
481 BOOST_CHECK_EQUAL( board->Zones().size(), 1 );
482
483 ZONE* zone = board->Zones().front();
484 BOOST_CHECK( zone->GetNetCode() > 0 );
485 BOOST_CHECK( IsCopperLayer( zone->GetFirstLayer() ) );
486 BOOST_CHECK( zone->IsFilled() );
487
488 // Should have exactly one standalone copper polygon (the right rectangle)
489 int copperPolyCount = 0;
490 int copperPolyWithNet = 0;
491
492 for( BOARD_ITEM* item : board->Drawings() )
493 {
494 if( item->Type() == PCB_SHAPE_T )
495 {
496 PCB_SHAPE* shape = static_cast<PCB_SHAPE*>( item );
497
498 if( IsCopperLayer( shape->GetLayer() ) && shape->GetShape() == SHAPE_T::POLY )
499 {
500 copperPolyCount++;
501
502 if( shape->GetNetCode() > 0 )
503 copperPolyWithNet++;
504 }
505 }
506 }
507
508 BOOST_CHECK_EQUAL( copperPolyCount, 1 );
509 BOOST_CHECK_EQUAL( copperPolyWithNet, 1 );
510}
511
512
517{
518 std::unique_ptr<BOARD> board = LoadAllegroBoard( "copper_text/copper_text.brd" );
519 BOOST_REQUIRE( board );
520
521 int copperTextCount = 0;
522 bool foundTestingText = false;
523
524 for( BOARD_ITEM* item : board->Drawings() )
525 {
526 if( item->Type() == PCB_TEXT_T )
527 {
528 PCB_TEXT* text = static_cast<PCB_TEXT*>( item );
529
530 if( IsCopperLayer( text->GetLayer() ) )
531 {
532 copperTextCount++;
533
534 if( text->GetText() == wxS( "TESTING" ) )
535 {
536 foundTestingText = true;
537 BOOST_CHECK_EQUAL( text->GetLayer(), F_Cu );
538 }
539 }
540 }
541 }
542
543 BOOST_CHECK_MESSAGE( foundTestingText, "Board should contain 'TESTING' text on F.Cu" );
544 BOOST_CHECK_EQUAL( copperTextCount, 1 );
545}
546
547
548BOOST_AUTO_TEST_CASE( CoincidentGraphicsAreDropped )
549{
550 // Allegro stacks graphics that land on top of each other, on some designs a third of all
551 // items. They draw the same picture, so the importer keeps only the first of each
552 std::unique_ptr<BOARD> board = LoadAllegroBoard( "TRS80_POWER/TRS80_POWER.brd" );
553
554 const auto netOf = []( const BOARD_ITEM* aItem )
555 {
556 const BOARD_CONNECTED_ITEM* connected = dynamic_cast<const BOARD_CONNECTED_ITEM*>( aItem );
557 return connected ? connected->GetNetCode() : NETINFO_LIST::UNCONNECTED;
558 };
559
560 const auto coincident = [&]( const BOARD_ITEM* aFirst, const BOARD_ITEM* aSecond )
561 {
562 if( aFirst->Type() != aSecond->Type() || aFirst->GetLayer() != aSecond->GetLayer()
563 || netOf( aFirst ) != netOf( aSecond ) )
564 {
565 return false;
566 }
567
568 if( aFirst->Type() == PCB_SHAPE_T )
569 {
570 return static_cast<const PCB_SHAPE*>( aFirst )->Compare(
571 static_cast<const PCB_SHAPE*>( aSecond ) ) == 0;
572 }
573
574 if( aFirst->Type() == PCB_TEXT_T )
575 {
576 return static_cast<const PCB_TEXT*>( aFirst )->Compare(
577 static_cast<const PCB_TEXT*>( aSecond ) ) == 0;
578 }
579
580 return false;
581 };
582
583 const auto countCoincident = [&]( const DRAWINGS& aItems )
584 {
585 int count = 0;
586
587 for( size_t ii = 0; ii < aItems.size(); ++ii )
588 {
589 for( size_t jj = ii + 1; jj < aItems.size(); ++jj )
590 {
591 if( coincident( aItems[ii], aItems[jj] ) )
592 count++;
593 }
594 }
595
596 return count;
597 };
598
599 BOOST_REQUIRE( !board->Drawings().empty() );
600 BOOST_CHECK_EQUAL( countCoincident( board->Drawings() ), 0 );
601
602 BOOST_REQUIRE( !board->Footprints().empty() );
603
604 for( FOOTPRINT* footprint : board->Footprints() )
605 BOOST_CHECK_EQUAL( countCoincident( footprint->GraphicalItems() ), 0 );
606}
607
608
609BOOST_AUTO_TEST_CASE( ImportIsRepeatable )
610{
611 // Item ids are derived from the Allegro block keys, so importing a design twice has to
612 // produce the same ids. Random ids reshuffle the whole saved file on every import, because
613 // the s-expr writer orders items by uuid
614 const auto collectIds = []( const BOARD& aBoard )
615 {
616 std::vector<wxString> ids;
617
618 std::function<void( const BOARD_ITEM* )> walk =
619 [&]( const BOARD_ITEM* aItem )
620 {
621 ids.push_back( aItem->m_Uuid.AsString() );
622
623 // Group members are collected where they live on the board
624 if( aItem->Type() == PCB_GROUP_T )
625 return;
626
627 aItem->RunOnChildren(
628 [&]( BOARD_ITEM* aChild )
629 {
630 walk( aChild );
631 },
633 };
634
635 for( const BOARD_ITEM* item : const_cast<BOARD&>( aBoard ).GetItemSet() )
636 walk( item );
637
638 std::sort( ids.begin(), ids.end() );
639 return ids;
640 };
641
642 std::unique_ptr<BOARD> first = LoadAllegroBoard( "ProiectBoard/ProiectBoard.brd" );
643 std::unique_ptr<BOARD> second = LoadAllegroBoard( "ProiectBoard/ProiectBoard.brd" );
644
645 const std::vector<wxString> firstIds = collectIds( *first );
646 const std::vector<wxString> secondIds = collectIds( *second );
647
648 BOOST_REQUIRE( !firstIds.empty() );
649 BOOST_CHECK_EQUAL( firstIds.size(), secondIds.size() );
650 BOOST_CHECK( firstIds == secondIds );
651
652 BOOST_CHECK( std::adjacent_find( firstIds.begin(), firstIds.end() ) == firstIds.end() );
653}
654
655
656// The 3D model assignment lives on the Allegro package definition, so every placed instance
657// of a package that names one carries it
658BOOST_AUTO_TEST_CASE( Footprint3DModels )
659{
660 std::unique_ptr<BOARD> board = LoadAllegroBoard( "led_youtube/led_youtube.brd" );
661 BOOST_REQUIRE( board );
662
663 std::map<wxString, FP_3DMODEL> models;
664
665 for( FOOTPRINT* fp : board->Footprints() )
666 {
667 BOOST_REQUIRE_EQUAL( fp->Models().size(), 1u );
668 models.emplace( fp->Models().front().m_Filename, fp->Models().front() );
669 }
670
671 BOOST_REQUIRE_EQUAL( models.size(), 3u );
672 BOOST_CHECK_EQUAL( models.count( wxS( "led3d.stp" ) ), 1u );
673 BOOST_CHECK_EQUAL( models.count( wxS( "AC0805FR-07360RL.STEP" ) ), 1u );
674 BOOST_REQUIRE_EQUAL( models.count( wxS( "22272021.stp" ) ), 1u );
675
676 // Placement of the connector package is "MM,0.020000,-1.270000,1.580007,90.000,-0.000,90.000"
677 const FP_3DMODEL& conn = models.at( wxS( "22272021.stp" ) );
678
679 BOOST_CHECK_CLOSE( conn.m_Offset.x, 0.02, 1e-6 );
680 BOOST_CHECK_CLOSE( conn.m_Offset.y, -1.27, 1e-6 );
681 BOOST_CHECK_CLOSE( conn.m_Offset.z, 1.580007, 1e-6 );
682 BOOST_CHECK_CLOSE( conn.m_Rotation.x, -90.0, 1e-6 );
683 BOOST_CHECK_SMALL( conn.m_Rotation.y, 1e-9 );
684 BOOST_CHECK_CLOSE( conn.m_Rotation.z, -90.0, 1e-6 );
685}
686
687
689
690
691
695{
696 std::string filename;
697 bool expected_to_load; // Set false for known-broken boards
698};
699
700
705{
707
713 BOARD* GetCachedBoard( const std::string& aFilePath )
714 {
716 }
717
721 static std::vector<std::string> GetAllBoardFiles()
722 {
723 std::vector<std::string> boards;
724 std::string dataPath = KI_TEST::AllegroBoardDataDir( "" );
725
726 // For each board dir, look for .brd files and add them to the list of test cases
727 try
728 {
729 for( const auto& boardDir : std::filesystem::directory_iterator( dataPath ) )
730 {
731 if( !boardDir.is_directory() )
732 continue;
733
734 for( const auto& entry : std::filesystem::directory_iterator( boardDir ) )
735 {
736 if( entry.is_regular_file() && entry.path().extension() == ".brd" && entry.file_size() > 0 )
737 {
738 std::string name = entry.path().filename().string();
739
740 // v13_header.brd is intentionally pre-v16 and tested separately
741 if( name != "v13_header.brd" )
742 {
743 boards.push_back( boardDir.path().string() + "/" + name );
744 }
745 }
746 }
747 }
748 }
749 catch( const std::filesystem::filesystem_error& e )
750 {
751 BOOST_TEST_MESSAGE( "Failed to enumerate board files: " << e.what() );
752 }
753
754 std::sort( boards.begin(), boards.end() );
755 return boards;
756 }
757
759};
760
761BOOST_FIXTURE_TEST_SUITE( AllegroComprehensive, ALLEGRO_COMPREHENSIVE_FIXTURE )
762
763
764
769BOOST_AUTO_TEST_CASE( BeagleBone_OutermostZoneNets )
770{
771 std::string dataPath = KI_TEST::AllegroBoardFile( "BeagleBone_Black_RevC/BeagleBone_Black_RevC.brd" );
772
773 BOARD* board = GetCachedBoard( dataPath );
774 BOOST_REQUIRE( board );
775
776 const std::vector<wxString> expectedLayers = { wxS( "TOP" ), wxS( "LYR2_GND" ),
777 wxS( "LYR5_PWR" ), wxS( "BOTTOM" ) };
778
779 for( const wxString& layerName : expectedLayers )
780 {
781 BOOST_TEST_CONTEXT( "Outermost zone on " << layerName )
782 {
783 PCB_LAYER_ID layerId = board->GetLayerID( layerName );
784
785 BOOST_REQUIRE_MESSAGE( layerId != UNDEFINED_LAYER,
786 "Layer " << layerName << " should exist" );
787
788 const ZONE* largest = nullptr;
789 double largestArea = 0;
790
791 for( const ZONE* zone : board->Zones() )
792 {
793 if( zone->GetIsRuleArea() )
794 continue;
795
796 if( zone->GetNetCode() == 0 )
797 continue;
798
799 if( !zone->GetLayerSet().Contains( layerId ) )
800 continue;
801
802 BOX2I bbox = zone->GetBoundingBox();
803 double area = static_cast<double>( bbox.GetWidth() )
804 * static_cast<double>( bbox.GetHeight() );
805
806 if( area > largestArea )
807 {
808 largestArea = area;
809 largest = zone;
810 }
811 }
812
813 BOOST_REQUIRE_MESSAGE( largest != nullptr,
814 "Should find a netted copper zone on " << layerName );
815 BOOST_CHECK_EQUAL( largest->GetNetname(), wxString( wxS( "GND_EARTH" ) ) );
816 }
817 }
818}
819
820
824BOOST_AUTO_TEST_CASE( PadSizesPositive )
825{
826 std::vector<std::string> boards = GetAllBoardFiles();
827
828 for( const std::string& boardPath : boards )
829 {
830 std::string boardName = std::filesystem::path( boardPath ).filename().string();
831 BOARD* board = GetCachedBoard( boardPath );
832
833 if( !board )
834 continue;
835
836 BOOST_TEST_CONTEXT( "Testing board: " << boardName )
837 {
838 int negativePadCount = 0;
839
840 for( FOOTPRINT* fp : board->Footprints() )
841 {
842 for( PAD* pad : fp->Pads() )
843 {
844 VECTOR2I size = pad->GetSize( F_Cu );
845
846 if( size.x < 0 || size.y < 0 )
847 {
848 negativePadCount++;
849 BOOST_TEST_MESSAGE( boardName << ": Negative pad size in " << fp->GetReference()
850 << " pad " << pad->GetNumber() << ": " << size.x << " x "
851 << size.y );
852 }
853 }
854 }
855
856 BOOST_CHECK_EQUAL( negativePadCount, 0 );
857 }
858 }
859}
860
861
866BOOST_AUTO_TEST_CASE( ViaDrillNotLargerThanSize )
867{
868 std::vector<std::string> boards = GetAllBoardFiles();
869
870 for( const std::string& boardPath : boards )
871 {
872 std::string boardName = std::filesystem::path( boardPath ).filename().string();
873 BOARD* board = GetCachedBoard( boardPath );
874
875 if( !board )
876 continue;
877
878 BOOST_TEST_CONTEXT( "Testing board: " << boardName )
879 {
880 int invalidViaCount = 0;
881
882 for( PCB_TRACK* track : board->Tracks() )
883 {
884 if( track->Type() == PCB_VIA_T )
885 {
886 PCB_VIA* via = static_cast<PCB_VIA*>( track );
887 int drill = via->GetDrill();
888 int width = via->GetWidth( F_Cu );
889
890 if( drill > width )
891 {
892 invalidViaCount++;
893 BOOST_TEST_MESSAGE( boardName << ": Via at ("
894 << via->GetPosition().x / 1000000.0 << ", "
895 << via->GetPosition().y / 1000000.0
896 << ") has drill " << drill / 1000000.0
897 << "mm > width " << width / 1000000.0 << "mm" );
898 }
899 }
900 }
901
902 BOOST_CHECK_EQUAL( invalidViaCount, 0 );
903 }
904 }
905}
906
907
912BOOST_AUTO_TEST_CASE( SmdPadDetection )
913{
914 std::vector<std::string> boards = GetAllBoardFiles();
915
916 for( const std::string& boardPath : boards )
917 {
918 std::string boardName = std::filesystem::path( boardPath ).filename().string();
919 BOARD* board = GetCachedBoard( boardPath );
920
921 if( !board )
922 continue;
923
924 BOOST_TEST_CONTEXT( "Testing board: " << boardName )
925 {
926 int misclassifiedSmdCount = 0;
927 int correctSmdCount = 0;
928 int correctThCount = 0;
929
930 for( FOOTPRINT* fp : board->Footprints() )
931 {
932 for( PAD* pad : fp->Pads() )
933 {
934 bool hasDrill = pad->GetDrillSizeX() > 0 && pad->GetDrillSizeY() > 0;
935 PAD_ATTRIB attr = pad->GetAttribute();
936
937 if( !hasDrill && attr == PAD_ATTRIB::PTH )
938 {
939 misclassifiedSmdCount++;
940 BOOST_TEST_MESSAGE( boardName << ": Pad " << fp->GetReference()
941 << "." << pad->GetNumber()
942 << " has no drill but is marked as PTH (should be SMD)" );
943 }
944 else if( !hasDrill && attr == PAD_ATTRIB::SMD )
945 {
946 correctSmdCount++;
947 }
948 else if( hasDrill && ( attr == PAD_ATTRIB::PTH || attr == PAD_ATTRIB::NPTH ) )
949 {
950 correctThCount++;
951 }
952 }
953 }
954
955 BOOST_TEST_MESSAGE( boardName << ": Correct SMD=" << correctSmdCount
956 << ", Correct TH=" << correctThCount
957 << ", Misclassified=" << misclassifiedSmdCount );
958
959 BOOST_CHECK_EQUAL( misclassifiedSmdCount, 0 );
960 }
961 }
962}
963
964
969BOOST_AUTO_TEST_CASE( QuadPackagePadRotation )
970{
971 std::vector<std::string> boards = GetAllBoardFiles();
972
973 for( const std::string& boardPath : boards )
974 {
975 std::string boardName = std::filesystem::path( boardPath ).filename().string();
976 BOARD* board = GetCachedBoard( boardPath );
977
978 if( !board )
979 continue;
980
981 BOOST_TEST_CONTEXT( "Testing board: " << boardName )
982 {
983 int quadPackageCount = 0;
984 int packagesWithRotatedPads = 0;
985 int packagesWithUnrotatedPads = 0;
986
987 for( FOOTPRINT* fp : board->Footprints() )
988 {
989 wxString refdes = fp->GetReference().Upper();
990
991 // Look for ICs (typically U* prefix) that might be quad packages
992 if( !refdes.StartsWith( "U" ) )
993 continue;
994
995 // Must have at least 16 pads to be a quad package
996 if( fp->Pads().size() < 16 )
997 continue;
998
999 // Find bounding box of all pad centers to estimate package shape
1000 BOX2I padBounds;
1001 bool first = true;
1002
1003 for( PAD* pad : fp->Pads() )
1004 {
1005 VECTOR2I pos = pad->GetPosition();
1006
1007 if( first )
1008 {
1009 padBounds = BOX2I( pos, VECTOR2I( 0, 0 ) );
1010 first = false;
1011 }
1012 else
1013 {
1014 padBounds.Merge( pos );
1015 }
1016 }
1017
1018 // Must be roughly square to be a quad package
1019 int width = padBounds.GetWidth();
1020 int height = padBounds.GetHeight();
1021
1022 if( width == 0 || height == 0 )
1023 continue;
1024
1025 double aspectRatio = static_cast<double>( std::max( width, height ) ) /
1026 static_cast<double>( std::min( width, height ) );
1027
1028 if( aspectRatio > 2.0 )
1029 continue;
1030
1031 quadPackageCount++;
1032
1033 // Check if pads have varying orientations
1034 std::set<int> uniqueAngles;
1035
1036 for( PAD* pad : fp->Pads() )
1037 {
1038 EDA_ANGLE angle = pad->GetOrientation();
1039 angle.Normalize();
1040 int degrees = static_cast<int>( angle.AsDegrees() + 0.5 ) % 360;
1041 uniqueAngles.insert( degrees );
1042 }
1043
1044 // A properly imported quad package should have at least 2 different pad orientations
1045 // (for 2-sided packages) or 4 (for 4-sided packages like QFP)
1046 if( uniqueAngles.size() >= 2 )
1047 {
1048 packagesWithRotatedPads++;
1049 BOOST_TEST_MESSAGE( boardName << ": " << fp->GetReference()
1050 << " has " << uniqueAngles.size() << " unique pad orientations" );
1051 }
1052 else
1053 {
1054 packagesWithUnrotatedPads++;
1055 BOOST_TEST_MESSAGE( boardName << ": " << fp->GetReference()
1056 << " has only " << uniqueAngles.size()
1057 << " unique pad orientation (may be missing rotation)" );
1058 }
1059 }
1060
1061 if( quadPackageCount > 0 )
1062 {
1063 BOOST_TEST_MESSAGE( boardName << ": Found " << quadPackageCount
1064 << " potential quad packages, "
1065 << packagesWithRotatedPads << " with rotated pads, "
1066 << packagesWithUnrotatedPads << " without" );
1067
1068 // At least some packages should have rotated pads to confirm rotation parsing works.
1069 // Many packages may legitimately have all pads at the same orientation (BGAs, single-row).
1070 if( packagesWithRotatedPads == 0 && quadPackageCount > 0 )
1071 {
1072 BOOST_WARN_MESSAGE( false, boardName << " has no packages with rotated pads" );
1073 }
1074 }
1075 }
1076 }
1077}
1078
1079
1085BOOST_AUTO_TEST_CASE( FootprintLayerPlacement )
1086{
1087 std::string dataPath = KI_TEST::AllegroBoardFile( "BeagleBone_Black_RevC/BeagleBone_Black_RevC.brd" );
1088
1089 BOARD* board = GetCachedBoard( dataPath );
1090 BOOST_REQUIRE_MESSAGE( board != nullptr, "BeagleBone_Black_RevC.brd should load successfully" );
1091
1092 // Look for C78 which should be on the bottom layer
1093 FOOTPRINT* c78 = nullptr;
1094
1095 for( FOOTPRINT* fp : board->Footprints() )
1096 {
1097 if( fp->GetReference() == "C78" )
1098 {
1099 c78 = fp;
1100 break;
1101 }
1102 }
1103
1104 BOOST_REQUIRE_MESSAGE( c78 != nullptr, "Footprint C78 should exist in BeagleBone Black" );
1105
1106 PCB_LAYER_ID fpLayer = c78->GetLayer();
1107
1108 BOOST_TEST_MESSAGE( "C78 layer: " << board->GetLayerName( fpLayer ) << " (ID: " << fpLayer << ")" );
1109 BOOST_TEST_MESSAGE( "C78 is flipped: " << ( c78->IsFlipped() ? "yes" : "no" ) );
1110
1111 BOOST_CHECK_MESSAGE( fpLayer == B_Cu, "C78 should be on the bottom copper layer (B_Cu), got "
1112 << board->GetLayerName( fpLayer ) );
1113 BOOST_CHECK_MESSAGE( c78->IsFlipped(), "C78 should be flipped (IsFlipped() == true)" );
1114
1115 // Count footprints on top vs bottom to ensure we're parsing layer correctly
1116 int topCount = 0;
1117 int bottomCount = 0;
1118
1119 for( FOOTPRINT* fp : board->Footprints() )
1120 {
1121 if( fp->GetLayer() == F_Cu )
1122 topCount++;
1123 else if( fp->GetLayer() == B_Cu )
1124 bottomCount++;
1125 }
1126
1127 BOOST_TEST_MESSAGE( "Footprints on top: " << topCount << ", on bottom: " << bottomCount );
1128
1129 // BeagleBone should have components on both sides
1130 BOOST_CHECK_GT( topCount, 0 );
1131 BOOST_CHECK_GT( bottomCount, 0 );
1132}
1133
1134
1139BOOST_AUTO_TEST_CASE( ArcConnectivity )
1140{
1141 std::vector<std::string> boards = GetAllBoardFiles();
1142
1143 for( const std::string& boardPath : boards )
1144 {
1145 std::string boardName = std::filesystem::path( boardPath ).filename().string();
1146 BOARD* board = GetCachedBoard( boardPath );
1147
1148 if( !board )
1149 continue;
1150
1151 BOOST_TEST_CONTEXT( "Testing board: " << boardName )
1152 {
1153 int arcCount = 0;
1154 int disconnectedArcs = 0;
1155
1156 // Build a map of track endpoints per net for quick lookup
1157 std::map<int, std::vector<VECTOR2I>> netEndpoints;
1158
1159 for( PCB_TRACK* track : board->Tracks() )
1160 {
1161 int netCode = track->GetNetCode();
1162
1163 if( track->Type() == PCB_TRACE_T )
1164 {
1165 netEndpoints[netCode].push_back( track->GetStart() );
1166 netEndpoints[netCode].push_back( track->GetEnd() );
1167 }
1168 else if( track->Type() == PCB_VIA_T )
1169 {
1170 netEndpoints[netCode].push_back( track->GetPosition() );
1171 }
1172 }
1173
1174 // Also include pad positions
1175 for( FOOTPRINT* fp : board->Footprints() )
1176 {
1177 for( PAD* pad : fp->Pads() )
1178 {
1179 int netCode = pad->GetNetCode();
1180
1181 if( netCode > 0 )
1182 netEndpoints[netCode].push_back( pad->GetPosition() );
1183 }
1184 }
1185
1186 // Now check each arc
1187 for( PCB_TRACK* track : board->Tracks() )
1188 {
1189 if( track->Type() != PCB_ARC_T )
1190 continue;
1191
1192 arcCount++;
1193 PCB_ARC* arc = static_cast<PCB_ARC*>( track );
1194 int netCode = arc->GetNetCode();
1195
1196 VECTOR2I arcStart = arc->GetStart();
1197 VECTOR2I arcEnd = arc->GetEnd();
1198
1199 // Check if arc endpoints connect to something
1200 bool startConnected = false;
1201 bool endConnected = false;
1202 const int tolerance = 1000; // 1um tolerance
1203
1204 for( const VECTOR2I& pt : netEndpoints[netCode] )
1205 {
1206 if( ( pt - arcStart ).EuclideanNorm() < tolerance )
1207 startConnected = true;
1208
1209 if( ( pt - arcEnd ).EuclideanNorm() < tolerance )
1210 endConnected = true;
1211 }
1212
1213 // Arc should connect to at least one other track/pad at each end
1214 // (unless it's an isolated arc, which is unusual but possible)
1215 if( !startConnected && !endConnected && netEndpoints[netCode].size() > 2 )
1216 {
1217 disconnectedArcs++;
1218 BOOST_TEST_MESSAGE( boardName << ": Arc at ("
1219 << arcStart.x / 1000000.0 << ", "
1220 << arcStart.y / 1000000.0 << ") to ("
1221 << arcEnd.x / 1000000.0 << ", "
1222 << arcEnd.y / 1000000.0
1223 << ") appears disconnected from net " << netCode );
1224 }
1225 }
1226
1227 if( arcCount > 0 )
1228 {
1229 BOOST_TEST_MESSAGE( boardName << ": Found " << arcCount << " arcs, "
1230 << disconnectedArcs << " disconnected" );
1231 }
1232
1233 // Allow some disconnected arcs as they may be legitimate isolated features
1234 // but flag if more than 20% are disconnected
1235 if( arcCount > 5 )
1236 {
1237 BOOST_CHECK_LE( disconnectedArcs, arcCount / 5 );
1238 }
1239 }
1240 }
1241}
1242
1243
1260{
1261 wxString layer;
1262 int recordId = 0;
1263 wxString netName;
1264 double minX = 1e18, minY = 1e18, maxX = -1e18, maxY = -1e18;
1266
1267 void AddPoint( double aX, double aY )
1268 {
1269 minX = std::min( minX, aX );
1270 minY = std::min( minY, aY );
1271 maxX = std::max( maxX, aX );
1272 maxY = std::max( maxY, aY );
1273 segmentCount++;
1274 }
1275};
1276
1277
1279{
1280 std::set<wxString> netNames;
1281 std::set<wxString> refDes;
1282 std::map<wxString, wxString> refDesToSymName;
1283 std::map<wxString, std::set<wxString>> netToRefDes;
1284
1285 std::vector<ALG_ZONE_POLYGON> zonePolygons;
1286
1287 static std::vector<wxString> SplitAlgLine( const wxString& aLine )
1288 {
1289 std::vector<wxString> fields;
1290 wxString current;
1291
1292 for( size_t i = 0; i < aLine.size(); ++i )
1293 {
1294 if( aLine[i] == '!' )
1295 {
1296 fields.push_back( current );
1297 current.clear();
1298 }
1299 else
1300 {
1301 current += aLine[i];
1302 }
1303 }
1304
1305 if( !current.empty() )
1306 fields.push_back( current );
1307
1308 return fields;
1309 }
1310
1315 static int ParseRecordId( const wxString& aTag )
1316 {
1317 long val = -1;
1318 wxString tag = aTag.BeforeFirst( ' ' );
1319 tag.ToLong( &val );
1320 return static_cast<int>( val );
1321 }
1322
1323 static ALG_REFERENCE_DATA ParseAlgFile( const std::string& aPath )
1324 {
1325 ALG_REFERENCE_DATA data;
1326 std::ifstream file( aPath );
1327
1328 if( !file.is_open() )
1329 return data;
1330
1331 enum class SECTION
1332 {
1333 UNKNOWN,
1334 NET_NODES,
1335 SYM_PLACEMENT,
1336 GRAPHICS,
1337 };
1338
1339 SECTION currentSection = SECTION::UNKNOWN;
1340 std::string line;
1341
1342 // Accumulate zone segments grouped by (layer, recordId)
1343 std::map<std::pair<wxString, int>, ALG_ZONE_POLYGON> zoneMap;
1344
1345 while( std::getline( file, line ) )
1346 {
1347 if( line.empty() || line[0] == 'J' )
1348 continue;
1349
1350 if( line[0] == 'A' )
1351 {
1352 if( line.find( "NET_NAME_SORT!NODE_SORT!NET_NAME!REFDES!" ) != std::string::npos )
1353 currentSection = SECTION::NET_NODES;
1354 else if( line.find( "SYM_TYPE!SYM_NAME!REFDES!SYM_MIRROR!" ) != std::string::npos )
1355 currentSection = SECTION::SYM_PLACEMENT;
1356 else if( line.find( "CLASS!SUBCLASS!RECORD_TAG!GRAPHIC_DATA_NAME!" ) != std::string::npos )
1357 currentSection = SECTION::GRAPHICS;
1358 else
1359 currentSection = SECTION::UNKNOWN;
1360
1361 continue;
1362 }
1363
1364 if( line[0] != 'S' )
1365 continue;
1366
1367 auto fields = SplitAlgLine( wxString::FromUTF8( line ) );
1368
1369 switch( currentSection )
1370 {
1371 case SECTION::NET_NODES:
1372 {
1373 // S!sort!nodeSort!NET_NAME!REFDES!PIN!PIN_NAME!SUBCLASS!
1374 if( fields.size() >= 5 )
1375 {
1376 wxString netName = fields[3];
1377 wxString refdes = fields[4];
1378
1379 if( !netName.empty() )
1380 {
1381 data.netNames.insert( netName );
1382
1383 if( !refdes.empty() )
1384 data.netToRefDes[netName].insert( refdes );
1385 }
1386 }
1387
1388 break;
1389 }
1390 case SECTION::SYM_PLACEMENT:
1391 {
1392 // S!SYM_TYPE!SYM_NAME!REFDES!MIRROR!ROTATE!X!Y!CX!CY!LIB_PATH!
1393 if( fields.size() >= 4 )
1394 {
1395 wxString symType = fields[1];
1396 wxString symName = fields[2];
1397 wxString refdes = fields[3];
1398
1399 if( symType == wxT( "PACKAGE" ) && !refdes.empty() )
1400 {
1401 data.refDes.insert( refdes );
1402 data.refDesToSymName[refdes] = symName;
1403 }
1404 }
1405
1406 break;
1407 }
1408 case SECTION::GRAPHICS:
1409 {
1410 // Field layout (0-indexed after splitting on '!'):
1411 // 0=S, 1=CLASS, 2=SUBCLASS, 3=RECORD_TAG, 4=GRAPHIC_DATA_NAME,
1412 // 5=GRAPHIC_DATA_NUMBER, 6..15=GRAPHIC_DATA_1..10,
1413 // 16=PIN_NUMBER, ..., 23=NET_NAME
1414 if( fields.size() < 16 || fields[1] != wxT( "BOUNDARY" ) )
1415 break;
1416
1417 wxString closureType = fields[15];
1418
1419 if( closureType != wxT( "SHAPE" ) )
1420 break;
1421
1422 wxString layer = fields[2];
1423 int recordId = ParseRecordId( fields[3] );
1424
1425 if( recordId < 0 )
1426 break;
1427
1428 wxString netName;
1429
1430 if( fields.size() > 23 )
1431 netName = fields[23];
1432
1433 auto key = std::make_pair( layer, recordId );
1434 auto& zone = zoneMap[key];
1435 zone.layer = layer;
1436 zone.recordId = recordId;
1437
1438 if( !netName.empty() )
1439 zone.netName = netName;
1440
1441 double x1 = 0, y1 = 0, x2 = 0, y2 = 0;
1442
1443 if( fields.size() > 9 )
1444 {
1445 fields[6].ToDouble( &x1 );
1446 fields[7].ToDouble( &y1 );
1447 fields[8].ToDouble( &x2 );
1448 fields[9].ToDouble( &y2 );
1449 zone.AddPoint( x1, y1 );
1450 zone.AddPoint( x2, y2 );
1451 }
1452
1453 break;
1454 }
1455 default:
1456 break;
1457 }
1458 }
1459
1460 for( auto& [key, zone] : zoneMap )
1461 data.zonePolygons.push_back( std::move( zone ) );
1462
1463 return data;
1464 }
1465};
1466
1467
1469{
1470 std::string brdFile;
1471 std::string algFile;
1472};
1473
1474
1479static std::vector<BRD_ALG_PAIR> getBoardsWithAlg()
1480{
1481 std::string dataPath = KI_TEST::AllegroBoardDataDir( "" );
1482 std::vector<BRD_ALG_PAIR> boardsWithAlg;
1483
1484 for( const auto& boardDir : std::filesystem::directory_iterator( dataPath ) )
1485 {
1486 if( !boardDir.is_directory() )
1487 continue;
1488
1489 std::filesystem::path boardPath;
1490 std::filesystem::path algPath;
1491
1492 for( const auto& entry : std::filesystem::directory_iterator( boardDir ) )
1493 {
1494 if( !entry.is_regular_file() )
1495 continue;
1496
1497 if( entry.path().extension() == ".brd" )
1498 boardPath = entry.path();
1499 else if( entry.path().extension() == ".alg" )
1500 algPath = entry.path();
1501
1502 if( !boardPath.empty() && !algPath.empty() )
1503 {
1504 boardsWithAlg.push_back( { boardPath.string(), algPath.string() } );
1505 break;
1506 }
1507 }
1508 }
1509
1510 return boardsWithAlg;
1511}
1512
1513
1517BOOST_AUTO_TEST_CASE( AlgReferenceNetNames )
1518{
1519 std::vector<BRD_ALG_PAIR> boardsWithAlg = getBoardsWithAlg();
1520
1521 BOOST_REQUIRE_GT( boardsWithAlg.size(), 0u );
1522
1523 for( const auto& [brdFile, algFile] : boardsWithAlg )
1524 {
1525 BOOST_TEST_MESSAGE( "Validating net names: " << brdFile );
1526
1528 BOOST_REQUIRE_GT( algData.netNames.size(), 0u );
1529
1530 BOARD* board = GetCachedBoard( brdFile );
1531 BOOST_REQUIRE( board );
1532
1533 std::set<wxString> boardNets;
1534
1535 for( const NETINFO_ITEM* net : board->GetNetInfo() )
1536 {
1537 if( net->GetNetCode() > 0 )
1538 boardNets.insert( net->GetNetname() );
1539 }
1540
1541 int missingNets = 0;
1542
1543 for( const wxString& algNet : algData.netNames )
1544 {
1545 if( boardNets.find( algNet ) == boardNets.end() )
1546 {
1547 missingNets++;
1548
1549 if( missingNets <= 10 )
1550 BOOST_TEST_MESSAGE( " Missing net: " << algNet );
1551 }
1552 }
1553
1554 BOOST_TEST_MESSAGE( brdFile << ": .alg has " << algData.netNames.size() << " nets, board has "
1555 << boardNets.size() << ", missing " << missingNets );
1556
1557 BOOST_CHECK_EQUAL( missingNets, 0 );
1558 }
1559}
1560
1561
1565BOOST_AUTO_TEST_CASE( AlgReferenceComponentPlacement )
1566{
1567 std::vector<BRD_ALG_PAIR> boardsWithAlg = getBoardsWithAlg();
1568
1569 BOOST_REQUIRE_GT( boardsWithAlg.size(), 0u );
1570
1571 for( const auto& [brdFile, algFile] : boardsWithAlg )
1572 {
1573 BOOST_TEST_MESSAGE( "Validating components: " << brdFile );
1574
1576 BOOST_REQUIRE_GT( algData.refDes.size(), 0u );
1577
1578 BOARD* board = GetCachedBoard( brdFile );
1579 BOOST_REQUIRE( board );
1580
1581 std::set<wxString> boardRefDes;
1582
1583 for( const FOOTPRINT* fp : board->Footprints() )
1584 boardRefDes.insert( fp->GetReference() );
1585
1586 int missingRefDes = 0;
1587 int extraRefDes = 0;
1588
1589 for( const wxString& algRef : algData.refDes )
1590 {
1591 if( boardRefDes.find( algRef ) == boardRefDes.end() )
1592 {
1593 missingRefDes++;
1594
1595 if( missingRefDes <= 10 )
1596 BOOST_TEST_MESSAGE( " Missing refdes: " << algRef );
1597 }
1598 }
1599
1600 for( const wxString& boardRef : boardRefDes )
1601 {
1602 if( algData.refDes.find( boardRef ) == algData.refDes.end() )
1603 {
1604 extraRefDes++;
1605
1606 if( extraRefDes <= 10 )
1607 BOOST_TEST_MESSAGE( " Extra refdes in board: " << boardRef );
1608 }
1609 }
1610
1611 BOOST_TEST_MESSAGE( brdFile << ": .alg has " << algData.refDes.size()
1612 << " components, board has " << boardRefDes.size()
1613 << ", missing " << missingRefDes
1614 << ", extra " << extraRefDes );
1615
1616 BOOST_CHECK_EQUAL( missingRefDes, 0 );
1617 }
1618}
1619
1620
1624BOOST_AUTO_TEST_CASE( AllTracksPositiveWidth )
1625{
1626 std::vector<std::string> boards = GetAllBoardFiles();
1627
1628 for( const std::string& boardPath : boards )
1629 {
1630 std::string boardName = std::filesystem::path( boardPath ).filename().string();
1631 BOARD* board = GetCachedBoard( boardPath );
1632
1633 if( !board )
1634 continue;
1635
1636 BOOST_TEST_CONTEXT( "Testing board: " << boardName )
1637 {
1638 int zeroWidthCount = 0;
1639 int totalCount = 0;
1640
1641 for( PCB_TRACK* track : board->Tracks() )
1642 {
1643 if( track->Type() == PCB_TRACE_T || track->Type() == PCB_ARC_T )
1644 {
1645 totalCount++;
1646
1647 if( track->GetWidth() <= 0 )
1648 {
1649 zeroWidthCount++;
1650
1651 if( zeroWidthCount <= 5 )
1652 {
1653 BOOST_TEST_MESSAGE( boardName << ": Zero-width track at ("
1654 << track->GetStart().x / 1000000.0 << ", "
1655 << track->GetStart().y / 1000000.0 << ")" );
1656 }
1657 }
1658 }
1659 }
1660
1661 BOOST_CHECK_EQUAL( zeroWidthCount, 0 );
1662 }
1663 }
1664}
1665
1666
1674{
1675 enum class SEGMENT_TYPE
1676 {
1680 };
1681
1683 {
1685 double x1, y1, x2, y2;
1688 };
1689
1692 std::vector<OUTLINE_SEGMENT> designOutlineSegments;
1693 std::vector<OUTLINE_SEGMENT> outlineSegments;
1694
1695 double minX = std::numeric_limits<double>::max();
1696 double minY = std::numeric_limits<double>::max();
1697 double maxX = std::numeric_limits<double>::lowest();
1698 double maxY = std::numeric_limits<double>::lowest();
1699
1700 void updateBounds( double aX, double aY )
1701 {
1702 minX = std::min( minX, aX );
1703 minY = std::min( minY, aY );
1704 maxX = std::max( maxX, aX );
1705 maxY = std::max( maxY, aY );
1706 }
1707
1708 static ALG_OUTLINE_DATA ParseAlgOutlines( const std::string& aPath )
1709 {
1710 ALG_OUTLINE_DATA data;
1711 std::ifstream file( aPath );
1712
1713 if( !file.is_open() )
1714 return data;
1715
1716 std::string line;
1717
1718 while( std::getline( file, line ) )
1719 {
1720 if( line.empty() || line[0] != 'S' )
1721 continue;
1722
1723 auto fields = ALG_REFERENCE_DATA::SplitAlgLine( wxString::FromUTF8( line ) );
1724
1725 if( fields.size() < 10 )
1726 continue;
1727
1728 bool isDesignOutline = ( fields[1] == wxT( "BOARD GEOMETRY" )
1729 && fields[2] == wxT( "DESIGN_OUTLINE" ) );
1730
1731 bool isOutline = ( fields[1] == wxT( "BOARD GEOMETRY" )
1732 && fields[2] == wxT( "OUTLINE" ) );
1733
1734 if( !isDesignOutline && !isOutline )
1735 continue;
1736
1737 wxString shapeType = fields[4];
1738 OUTLINE_SEGMENT seg = {};
1739
1740 if( shapeType == wxT( "LINE" ) && fields.size() >= 10 )
1741 {
1743 fields[6].ToCDouble( &seg.x1 );
1744 fields[7].ToCDouble( &seg.y1 );
1745 fields[8].ToCDouble( &seg.x2 );
1746 fields[9].ToCDouble( &seg.y2 );
1747
1748 data.updateBounds( seg.x1, seg.y1 );
1749 data.updateBounds( seg.x2, seg.y2 );
1750 }
1751 else if( shapeType == wxT( "ARC" ) && fields.size() >= 15 )
1752 {
1753 seg.type = SEGMENT_TYPE::ARC;
1754 fields[6].ToCDouble( &seg.x1 );
1755 fields[7].ToCDouble( &seg.y1 );
1756 fields[8].ToCDouble( &seg.x2 );
1757 fields[9].ToCDouble( &seg.y2 );
1758 fields[10].ToCDouble( &seg.centerX );
1759 fields[11].ToCDouble( &seg.centerY );
1760 fields[12].ToCDouble( &seg.radius );
1761 seg.clockwise = ( fields[14] == wxT( "CLOCKWISE" ) );
1762
1763 data.updateBounds( seg.x1, seg.y1 );
1764 data.updateBounds( seg.x2, seg.y2 );
1765 }
1766 else if( shapeType == wxT( "RECTANGLE" ) && fields.size() >= 10 )
1767 {
1769 fields[6].ToCDouble( &seg.x1 );
1770 fields[7].ToCDouble( &seg.y1 );
1771 fields[8].ToCDouble( &seg.x2 );
1772 fields[9].ToCDouble( &seg.y2 );
1773
1774 data.updateBounds( seg.x1, seg.y1 );
1775 data.updateBounds( seg.x2, seg.y2 );
1776 }
1777 else
1778 {
1779 continue;
1780 }
1781
1782 if( isDesignOutline )
1783 {
1784 data.designOutlineCount++;
1785 data.designOutlineSegments.push_back( seg );
1786 }
1787 else
1788 {
1789 data.outlineCount++;
1790 data.outlineSegments.push_back( seg );
1791 }
1792 }
1793
1794 return data;
1795 }
1796
1802 {
1803 int count = 0;
1804
1805 for( const auto& seg : designOutlineSegments )
1806 {
1807 if( seg.type == SEGMENT_TYPE::RECTANGLE )
1808 count += 4;
1809 else
1810 count += 1;
1811 }
1812
1813 return count;
1814 }
1815};
1816
1817
1822BOOST_AUTO_TEST_CASE( OutlineSegmentCount )
1823{
1824 std::vector<BRD_ALG_PAIR> testBoards = getBoardsWithAlg();
1825
1826 BOOST_REQUIRE_GT( testBoards.size(), 0u );
1827
1828 for( const auto& [brdFile, algFile] : testBoards )
1829 {
1830 BOOST_TEST_CONTEXT( "Board: " << brdFile )
1831 {
1833
1834 if( algOutlines.designOutlineCount == 0 && algOutlines.outlineCount == 0 )
1835 {
1836 BOOST_TEST_MESSAGE( " No outline records in .alg, skipping" );
1837 continue;
1838 }
1839
1840 BOARD* board = GetCachedBoard( brdFile );
1841 BOOST_REQUIRE( board );
1842
1843 int edgeCutsCount = 0;
1844
1845 for( BOARD_ITEM* item : board->Drawings() )
1846 {
1847 if( item->Type() == PCB_SHAPE_T && item->GetLayer() == Edge_Cuts )
1848 edgeCutsCount++;
1849 }
1850
1851 int expectedCount = algOutlines.expectedEdgeCutsSegments();
1852
1853 BOOST_TEST_MESSAGE( " .alg DESIGN_OUTLINE records: " << algOutlines.designOutlineCount
1854 << " -> expected Edge_Cuts segments: " << expectedCount );
1855 BOOST_TEST_MESSAGE( " .alg OUTLINE records: " << algOutlines.outlineCount );
1856 BOOST_TEST_MESSAGE( " Binary import Edge_Cuts segments: " << edgeCutsCount );
1857
1858 BOOST_CHECK_EQUAL( edgeCutsCount, expectedCount );
1859 }
1860 }
1861}
1862
1863
1868BOOST_AUTO_TEST_CASE( OutlineBoundingBox )
1869{
1870 std::vector<BRD_ALG_PAIR> testBoards = getBoardsWithAlg();
1871
1872 BOOST_REQUIRE_GT( testBoards.size(), 0u );
1873
1874 // 1 mil = 25400 nm
1875 const double milToNm = 25400.0;
1876
1877 // Allow 2 mil tolerance for coordinate rounding across formats
1878 const int toleranceNm = static_cast<int>( 2.0 * milToNm );
1879
1880 for( const auto& [brdFile, algFile] : testBoards )
1881 {
1882 BOOST_TEST_CONTEXT( "Board: " << brdFile )
1883 {
1885
1886 if( algOutlines.designOutlineCount == 0 )
1887 {
1888 BOOST_TEST_MESSAGE( " No DESIGN_OUTLINE records in .alg, skipping" );
1889 continue;
1890 }
1891
1892 BOARD* board = GetCachedBoard( brdFile );
1893 BOOST_REQUIRE( board );
1894
1895 BOX2I boardBbox;
1896 bool hasBbox = false;
1897
1898 for( BOARD_ITEM* item : board->Drawings() )
1899 {
1900 if( item->Type() == PCB_SHAPE_T && item->GetLayer() == Edge_Cuts )
1901 {
1902 if( !hasBbox )
1903 {
1904 boardBbox = item->GetBoundingBox();
1905 hasBbox = true;
1906 }
1907 else
1908 {
1909 boardBbox.Merge( item->GetBoundingBox() );
1910 }
1911 }
1912 }
1913
1914 BOOST_REQUIRE_MESSAGE( hasBbox, "Board should have Edge_Cuts outline" );
1915
1916 // Convert .alg bounding box from mils to nm
1917 int algMinXnm = static_cast<int>( algOutlines.minX * milToNm );
1918 int algMinYnm = static_cast<int>( algOutlines.minY * milToNm );
1919 int algMaxXnm = static_cast<int>( algOutlines.maxX * milToNm );
1920 int algMaxYnm = static_cast<int>( algOutlines.maxY * milToNm );
1921 int algWidthNm = algMaxXnm - algMinXnm;
1922 int algHeightNm = algMaxYnm - algMinYnm;
1923
1924 int boardWidth = boardBbox.GetWidth();
1925 int boardHeight = boardBbox.GetHeight();
1926
1927 BOOST_TEST_MESSAGE( " .alg extent (mils): "
1928 << algOutlines.minX << "," << algOutlines.minY << " to "
1929 << algOutlines.maxX << "," << algOutlines.maxY
1930 << " = " << ( algOutlines.maxX - algOutlines.minX ) << " x "
1931 << ( algOutlines.maxY - algOutlines.minY ) );
1932 BOOST_TEST_MESSAGE( " Board bbox (nm): "
1933 << boardBbox.GetLeft() << "," << boardBbox.GetTop() << " to "
1934 << boardBbox.GetRight() << "," << boardBbox.GetBottom()
1935 << " = " << boardWidth << " x " << boardHeight );
1936 BOOST_TEST_MESSAGE( " .alg (nm): " << algWidthNm << " x " << algHeightNm );
1937
1938 // KiCad bounding boxes include line width so allow 3% tolerance
1939 BOOST_CHECK_CLOSE( static_cast<double>( boardWidth ),
1940 static_cast<double>( algWidthNm ), 3.0 );
1941 BOOST_CHECK_CLOSE( static_cast<double>( boardHeight ),
1942 static_cast<double>( algHeightNm ), 3.0 );
1943 }
1944 }
1945}
1946
1947
1955BOOST_AUTO_TEST_CASE( OutlineEndpoints )
1956{
1957 std::vector<BRD_ALG_PAIR> testBoards = getBoardsWithAlg();
1958
1959 BOOST_REQUIRE_GT( testBoards.size(), 0u );
1960
1961 const double milToNm = 25400.0;
1962 const int toleranceNm = static_cast<int>( 2.0 * milToNm );
1963
1964 for( const auto& [brdFile, algFile] : testBoards )
1965 {
1966 BOOST_TEST_CONTEXT( "Board: " << brdFile )
1967 {
1969
1970 if( algOutlines.designOutlineCount == 0 )
1971 continue;
1972
1973 // Only validate endpoint-by-endpoint for pure-LINE outlines
1974 bool allLines = true;
1975
1976 for( const auto& seg : algOutlines.designOutlineSegments )
1977 {
1980 {
1981 allLines = false;
1982 break;
1983 }
1984 }
1985
1986 if( !allLines )
1987 {
1988 BOOST_TEST_MESSAGE( " Outline has arcs, skipping endpoint-level validation" );
1989 continue;
1990 }
1991
1992 BOARD* board = GetCachedBoard( brdFile );
1993 BOOST_REQUIRE( board );
1994
1995 // Collect all Edge_Cuts segment endpoints
1996 struct ENDPOINT_PAIR
1997 {
1998 VECTOR2I start;
1999 VECTOR2I end;
2000 };
2001
2002 std::vector<ENDPOINT_PAIR> boardSegments;
2003
2004 for( BOARD_ITEM* item : board->Drawings() )
2005 {
2006 if( item->Type() != PCB_SHAPE_T || item->GetLayer() != Edge_Cuts )
2007 continue;
2008
2009 PCB_SHAPE* shape = static_cast<PCB_SHAPE*>( item );
2010
2011 if( shape->GetShape() == SHAPE_T::SEGMENT )
2012 boardSegments.push_back( { shape->GetStart(), shape->GetEnd() } );
2013 }
2014
2015 // Build expected segments from .alg, expanding RECTANGLEs into 4 segments
2016 std::vector<ENDPOINT_PAIR> algSegments;
2017
2018 for( const auto& seg : algOutlines.designOutlineSegments )
2019 {
2020 if( seg.type == ALG_OUTLINE_DATA::SEGMENT_TYPE::LINE )
2021 {
2022 VECTOR2I start( static_cast<int>( seg.x1 * milToNm ),
2023 static_cast<int>( seg.y1 * milToNm ) );
2024 VECTOR2I end( static_cast<int>( seg.x2 * milToNm ),
2025 static_cast<int>( seg.y2 * milToNm ) );
2026 algSegments.push_back( { start, end } );
2027 }
2028 else if( seg.type == ALG_OUTLINE_DATA::SEGMENT_TYPE::RECTANGLE )
2029 {
2030 int x1 = static_cast<int>( seg.x1 * milToNm );
2031 int y1 = static_cast<int>( seg.y1 * milToNm );
2032 int x2 = static_cast<int>( seg.x2 * milToNm );
2033 int y2 = static_cast<int>( seg.y2 * milToNm );
2034
2035 algSegments.push_back( { { x1, y1 }, { x2, y1 } } );
2036 algSegments.push_back( { { x2, y1 }, { x2, y2 } } );
2037 algSegments.push_back( { { x2, y2 }, { x1, y2 } } );
2038 algSegments.push_back( { { x1, y2 }, { x1, y1 } } );
2039 }
2040 }
2041
2042 BOOST_CHECK_EQUAL( boardSegments.size(), algSegments.size() );
2043
2044 if( boardSegments.size() != algSegments.size() )
2045 continue;
2046
2047 // Match each .alg segment to a board segment by finding closest start/end pair.
2048 // Allegro and KiCad may have opposite Y axis, so we compare using absolute
2049 // coordinate deltas.
2050 int matchedCount = 0;
2051
2052 std::vector<bool> used( boardSegments.size(), false );
2053
2054 for( size_t ai = 0; ai < algSegments.size(); ++ai )
2055 {
2056 const auto& algSeg = algSegments[ai];
2057 int bestIdx = -1;
2058 int64_t bestDist = std::numeric_limits<int64_t>::max();
2059
2060 for( size_t bi = 0; bi < boardSegments.size(); ++bi )
2061 {
2062 if( used[bi] )
2063 continue;
2064
2065 const auto& bSeg = boardSegments[bi];
2066
2067 // Try both orientations (start-start or start-end swap)
2068 auto dist = [&]( const VECTOR2I& aAlgPt, const VECTOR2I& aBoardPt ) -> int64_t
2069 {
2070 int64_t dx = std::abs( static_cast<int64_t>( aAlgPt.x )
2071 - static_cast<int64_t>( aBoardPt.x ) );
2072 int64_t dy = std::abs( static_cast<int64_t>( aAlgPt.y )
2073 - static_cast<int64_t>( aBoardPt.y ) );
2074 return dx + dy;
2075 };
2076
2077 int64_t d1 = dist( algSeg.start, bSeg.start ) + dist( algSeg.end, bSeg.end );
2078 int64_t d2 = dist( algSeg.start, bSeg.end ) + dist( algSeg.end, bSeg.start );
2079 int64_t d = std::min( d1, d2 );
2080
2081 if( d < bestDist )
2082 {
2083 bestDist = d;
2084 bestIdx = static_cast<int>( bi );
2085 }
2086 }
2087
2088 if( bestIdx >= 0 && bestDist < 2LL * toleranceNm )
2089 {
2090 used[bestIdx] = true;
2091 matchedCount++;
2092 }
2093 else
2094 {
2095 BOOST_TEST_MESSAGE( " Unmatched .alg segment " << ai << ": ("
2096 << algSeg.start.x / 1000000.0 << ", "
2097 << algSeg.start.y / 1000000.0 << ") -> ("
2098 << algSeg.end.x / 1000000.0 << ", "
2099 << algSeg.end.y / 1000000.0 << ") mm"
2100 << " bestDist=" << bestDist );
2101 }
2102 }
2103
2104 BOOST_TEST_MESSAGE( " Matched " << matchedCount << " / " << algSegments.size()
2105 << " outline segments" );
2106
2107 BOOST_CHECK_EQUAL( matchedCount, static_cast<int>( algSegments.size() ) );
2108 }
2109 }
2110}
2111
2112
2117BOOST_AUTO_TEST_CASE( PadDrillConsistency )
2118{
2119 std::vector<std::string> boards = GetAllBoardFiles();
2120
2121 for( const std::string& boardPath : boards )
2122 {
2123 std::string boardName = std::filesystem::path( boardPath ).filename().string();
2124 BOARD* board = GetCachedBoard( boardPath );
2125
2126 if( !board )
2127 continue;
2128
2129 BOOST_TEST_CONTEXT( "Testing board: " << boardName )
2130 {
2131 int pthNoDrill = 0;
2132 int smdWithDrill = 0;
2133
2134 for( FOOTPRINT* fp : board->Footprints() )
2135 {
2136 for( PAD* pad : fp->Pads() )
2137 {
2138 PAD_ATTRIB attr = pad->GetAttribute();
2139 bool hasDrill = pad->GetDrillSizeX() > 0;
2140
2141 if( attr == PAD_ATTRIB::PTH && !hasDrill )
2142 {
2143 pthNoDrill++;
2144
2145 if( pthNoDrill <= 5 )
2146 {
2147 BOOST_TEST_MESSAGE( boardName << ": PTH pad without drill: "
2148 << fp->GetReference() << "."
2149 << pad->GetNumber() );
2150 }
2151 }
2152
2153 if( attr == PAD_ATTRIB::SMD && hasDrill )
2154 {
2155 smdWithDrill++;
2156
2157 if( smdWithDrill <= 5 )
2158 {
2159 BOOST_TEST_MESSAGE( boardName << ": SMD pad with drill: "
2160 << fp->GetReference() << "."
2161 << pad->GetNumber() );
2162 }
2163 }
2164 }
2165 }
2166
2167 BOOST_CHECK_EQUAL( pthNoDrill, 0 );
2168 BOOST_CHECK_EQUAL( smdWithDrill, 0 );
2169 }
2170 }
2171}
2172
2173
2177BOOST_AUTO_TEST_CASE( ZoneCountMatchesAlg )
2178{
2179 std::vector<BRD_ALG_PAIR> boardsWithAlg = getBoardsWithAlg();
2180
2181 BOOST_REQUIRE_GT( boardsWithAlg.size(), 0u );
2182
2183 for( const auto& [brdFile, algFile] : boardsWithAlg )
2184 {
2185 BOOST_TEST_CONTEXT( "Zone count: " << brdFile )
2186 {
2188
2189 BOARD* board = GetCachedBoard( brdFile );
2190 BOOST_REQUIRE( board );
2191
2192 size_t boardCopperZoneLayers = 0;
2193
2194 for( const ZONE* zone : board->Zones() )
2195 {
2196 if( !zone->GetIsRuleArea() )
2197 boardCopperZoneLayers += ( zone->GetLayerSet() & LSET::AllCuMask() ).count();
2198 }
2199
2200 size_t algZoneCount = algData.zonePolygons.size();
2201
2202 BOOST_TEST_MESSAGE( brdFile << ": .alg has " << algZoneCount
2203 << " zone polygons, board has " << boardCopperZoneLayers
2204 << " copper zone-layers" );
2205
2206 BOOST_CHECK_EQUAL( static_cast<size_t>( boardCopperZoneLayers ), algZoneCount );
2207 }
2208 }
2209}
2210
2211
2215BOOST_AUTO_TEST_CASE( ZoneLayerDistribution )
2216{
2217 std::vector<BRD_ALG_PAIR> boardsWithAlg = getBoardsWithAlg();
2218
2219 BOOST_REQUIRE_GT( boardsWithAlg.size(), 0u );
2220
2221 for( const auto& [brdFile, algFile] : boardsWithAlg )
2222 {
2223 BOOST_TEST_CONTEXT( "Zone layers: " << brdFile )
2224 {
2226
2227 BOARD* board = GetCachedBoard( brdFile );
2228 BOOST_REQUIRE( board );
2229
2230 std::map<wxString, int> algLayerCounts;
2231
2232 for( const ALG_ZONE_POLYGON& zone : algData.zonePolygons )
2233 algLayerCounts[zone.layer]++;
2234
2235 std::map<wxString, int> boardLayerCounts;
2236
2237 for( const ZONE* zone : board->Zones() )
2238 {
2239 if( zone->GetIsRuleArea() )
2240 continue;
2241
2242 for( PCB_LAYER_ID layer : zone->GetLayerSet().Seq() )
2243 {
2244 if( IsCopperLayer( layer ) )
2245 boardLayerCounts[board->GetLayerName( layer )]++;
2246 }
2247 }
2248
2249 BOOST_TEST_MESSAGE( brdFile << " layer distribution:" );
2250
2251 for( const auto& [layer, count] : algLayerCounts )
2252 {
2253 auto it = boardLayerCounts.find( layer );
2254 int boardCount = ( it != boardLayerCounts.end() ) ? it->second : 0;
2255
2256 BOOST_TEST_MESSAGE( " " << layer << ": .alg=" << count << " board=" << boardCount );
2257 BOOST_CHECK_EQUAL( boardCount, count );
2258 }
2259 }
2260 }
2261}
2262
2263
2268BOOST_AUTO_TEST_CASE( ZoneBoundingBoxes )
2269{
2270 std::vector<BRD_ALG_PAIR> boardsWithAlg = getBoardsWithAlg();
2271
2272 BOOST_REQUIRE_GT( boardsWithAlg.size(), 0u );
2273
2274 for( const auto& [brdFile, algFile] : boardsWithAlg )
2275 {
2276 BOOST_TEST_CONTEXT( "Zone bboxes: " << brdFile )
2277 {
2279
2280 BOARD* board = GetCachedBoard( brdFile );
2281 BOOST_REQUIRE( board );
2282
2283 // Collect sorted areas per layer from .alg and board, then compare distributions
2284 const double milsToNm = 25400.0;
2285
2286 std::map<wxString, std::vector<double>> algAreas;
2287
2288 for( const ALG_ZONE_POLYGON& zone : algData.zonePolygons )
2289 {
2290 double w = ( zone.maxX - zone.minX ) * milsToNm;
2291 double h = ( zone.maxY - zone.minY ) * milsToNm;
2292 algAreas[zone.layer].push_back( w * h );
2293 }
2294
2295 std::map<wxString, std::vector<double>> boardAreas;
2296
2297 for( const ZONE* zone : board->Zones() )
2298 {
2299 if( zone->GetIsRuleArea() )
2300 continue;
2301
2302 BOX2I bbox = zone->GetBoundingBox();
2303 double area = static_cast<double>( bbox.GetWidth() )
2304 * static_cast<double>( bbox.GetHeight() );
2305
2306 for( PCB_LAYER_ID layer : zone->GetLayerSet().Seq() )
2307 {
2308 if( IsCopperLayer( layer ) )
2309 boardAreas[board->GetLayerName( layer )].push_back( area );
2310 }
2311 }
2312
2313 int matched = 0;
2314 int mismatched = 0;
2315
2316 for( auto& [layer, algList] : algAreas )
2317 {
2318 std::sort( algList.begin(), algList.end() );
2319 auto it = boardAreas.find( layer );
2320
2321 if( it == boardAreas.end() || it->second.size() != algList.size() )
2322 continue;
2323
2324 std::sort( it->second.begin(), it->second.end() );
2325
2326 for( size_t i = 0; i < algList.size(); ++i )
2327 {
2328 double ref = std::max( algList[i], 1.0 );
2329 double err = std::abs( it->second[i] - algList[i] ) / ref;
2330
2331 if( err < 0.10 )
2332 {
2333 matched++;
2334 }
2335 else
2336 {
2337 mismatched++;
2338
2339 if( mismatched <= 5 )
2340 {
2341 BOOST_TEST_MESSAGE( " " << layer << " index " << i
2342 << ": alg area " << algList[i] / ( milsToNm * milsToNm )
2343 << " sq mils vs board area "
2344 << it->second[i] / ( milsToNm * milsToNm )
2345 << " sq mils" );
2346 }
2347 }
2348 }
2349 }
2350
2351 BOOST_TEST_MESSAGE( brdFile << ": " << matched << " zone areas matched, "
2352 << mismatched << " mismatched" );
2353
2354 int total = matched + mismatched;
2355
2356 if( total > 0 )
2357 {
2358 BOOST_CHECK_GT( matched, total * 8 / 10 );
2359 }
2360 }
2361 }
2362}
2363
2364
2371BOOST_AUTO_TEST_CASE( PadContainedInFabOutline )
2372{
2373 std::string dataPath = KI_TEST::AllegroBoardFile( "BeagleBone_Black_RevC/BeagleBone_Black_RevC.brd" );
2374
2375 BOARD* board = GetCachedBoard( dataPath );
2376 BOOST_REQUIRE( board );
2377
2378 // Footprints known to have pads and fab outlines that enclose them.
2379 // P6 and P10 are excluded: they are bottom-side connectors whose assembly outlines
2380 // only cover the housing, not the full pin field.
2381 const std::set<wxString> targetRefs = { wxS( "J1" ), wxS( "P5" ), wxS( "U5" ),
2382 wxS( "U13" ), wxS( "C78" ) };
2383
2384 int testedCount = 0;
2385 int failedCount = 0;
2386
2387 for( FOOTPRINT* fp : board->Footprints() )
2388 {
2389 if( targetRefs.find( fp->GetReference() ) == targetRefs.end() )
2390 continue;
2391
2392 PCB_LAYER_ID fabLayer = fp->IsFlipped() ? B_Fab : F_Fab;
2393
2394 BOX2I fabBbox;
2395 bool hasFab = false;
2396
2397 for( BOARD_ITEM* item : fp->GraphicalItems() )
2398 {
2399 if( item->GetLayer() == fabLayer )
2400 {
2401 if( !hasFab )
2402 {
2403 fabBbox = item->GetBoundingBox();
2404 hasFab = true;
2405 }
2406 else
2407 {
2408 fabBbox.Merge( item->GetBoundingBox() );
2409 }
2410 }
2411 }
2412
2413 if( !hasFab || fp->Pads().empty() )
2414 continue;
2415
2416 // Allow generous tolerance: pad centers can extend slightly beyond the fab outline
2417 // (e.g. edge-mount connectors, thermal pads). 3mm handles most cases.
2418 BOX2I testBbox = fabBbox;
2419 testBbox.Inflate( 3000000 );
2420
2421 BOOST_TEST_CONTEXT( "Footprint " << fp->GetReference() )
2422 {
2423 testedCount++;
2424
2425 for( PAD* pad : fp->Pads() )
2426 {
2427 VECTOR2I padCenter = pad->GetPosition();
2428
2429 if( !testBbox.Contains( padCenter ) )
2430 {
2431 failedCount++;
2432 BOOST_TEST_MESSAGE( fp->GetReference() << " pad " << pad->GetNumber()
2433 << " at (" << padCenter.x / 1e6 << ", "
2434 << padCenter.y / 1e6 << ") mm is outside F.Fab bbox" );
2435 }
2436 }
2437 }
2438 }
2439
2440 BOOST_TEST_MESSAGE( "Tested " << testedCount << " footprints for pad containment" );
2441 BOOST_CHECK_GE( testedCount, 4 );
2442 BOOST_CHECK_EQUAL( failedCount, 0 );
2443}
2444
2445
2451BOOST_AUTO_TEST_CASE( PadOrientationP6P10 )
2452{
2453 std::string dataPath = KI_TEST::AllegroBoardFile( "BeagleBone_Black_RevC/BeagleBone_Black_RevC.brd" );
2454
2455 BOARD* board = GetCachedBoard( dataPath );
2456 BOOST_REQUIRE( board );
2457
2458 for( FOOTPRINT* fp : board->Footprints() )
2459 {
2460 wxString ref = fp->GetReference();
2461
2462 if( ref != wxS( "P6" ) && ref != wxS( "P10" ) )
2463 continue;
2464
2465 BOOST_TEST_CONTEXT( "Footprint " << ref )
2466 {
2467 for( PAD* pad : fp->Pads() )
2468 {
2469 long padNum = 0;
2470
2471 if( !pad->GetNumber().ToLong( &padNum ) )
2472 continue;
2473
2474 // Pads 1-19 on P6/P10 are rectangular SMD pads that should be wider than tall
2475 if( padNum < 1 || padNum > 19 )
2476 continue;
2477
2478 // GetBoundingBox accounts for rotation, giving visual dimensions
2479 BOX2I bbox = pad->GetBoundingBox();
2480 auto bboxW = bbox.GetWidth();
2481 auto bboxH = bbox.GetHeight();
2482
2483 // Skip square/circular pads where orientation doesn't affect shape
2484 if( bboxW == bboxH )
2485 continue;
2486
2487 BOOST_TEST_CONTEXT( "Pad " << pad->GetNumber() )
2488 {
2489 BOOST_CHECK_MESSAGE( bboxW > bboxH,
2490 ref << " pad " << pad->GetNumber()
2491 << " should be visually wider than tall: "
2492 << bboxW / 1e6 << " x " << bboxH / 1e6 << " mm" );
2493 }
2494 }
2495 }
2496 }
2497}
2498
2499
2508{
2509 std::string dataPath = KI_TEST::AllegroBoardFile( "BeagleBone_Black_RevC/BeagleBone_Black_RevC.brd" );
2510
2511 BOARD* board = GetCachedBoard( dataPath );
2512 BOOST_REQUIRE( board );
2513
2514 int oblongCount = 0;
2515
2516 for( FOOTPRINT* fp : board->Footprints() )
2517 {
2518 for( PAD* pad : fp->Pads() )
2519 {
2520 VECTOR2I drillSize = pad->GetDrillSize();
2521
2522 if( drillSize.x <= 0 || drillSize.y <= 0 )
2523 continue;
2524
2525 if( drillSize.x == drillSize.y )
2526 continue;
2527
2528 oblongCount++;
2529
2530 BOOST_TEST_CONTEXT( fp->GetReference() << " pad " << pad->GetNumber() )
2531 {
2532 BOOST_CHECK( pad->GetDrillShape() == PAD_DRILL_SHAPE::OBLONG );
2533
2534 BOOST_TEST_MESSAGE( fp->GetReference() << " pad " << pad->GetNumber()
2535 << " slot: " << drillSize.x / 1e6 << " x "
2536 << drillSize.y / 1e6 << " mm"
2537 << " attr=" << static_cast<int>( pad->GetAttribute() ) );
2538 }
2539 }
2540 }
2541
2542 BOOST_TEST_MESSAGE( "Found " << oblongCount << " oblong drill holes" );
2543 BOOST_CHECK_EQUAL( oblongCount, 7 );
2544}
2545
2546
2552BOOST_AUTO_TEST_CASE( FootprintOrientation )
2553{
2554 std::string dataPath = KI_TEST::AllegroBoardFile( "BeagleBone_Black_RevC/BeagleBone_Black_RevC.brd" );
2555
2556 BOARD* board = GetCachedBoard( dataPath );
2557
2558 BOOST_REQUIRE( board );
2559
2560 FOOTPRINT* j1 = nullptr;
2561
2562 for( FOOTPRINT* fp : board->Footprints() )
2563 {
2564 if( fp->GetReference() == wxT( "J1" ) )
2565 {
2566 j1 = fp;
2567 break;
2568 }
2569 }
2570
2571 BOOST_REQUIRE_MESSAGE( j1 != nullptr, "Footprint J1 must exist in BeagleBone_Black_RevC" );
2572
2573 EDA_ANGLE orientation = j1->GetOrientation();
2574 BOOST_TEST_MESSAGE( "J1 orientation: " << orientation.AsDegrees() << " degrees" );
2575 BOOST_CHECK_CLOSE( orientation.AsDegrees(), 90.0, 0.1 );
2576}
2577
2578
2583BOOST_AUTO_TEST_CASE( UIImportPath_NullBoard )
2584{
2585 std::string dataPath = KI_TEST::AllegroBoardFile( "ProiectBoard/ProiectBoard.brd" );
2586
2587 PCB_IO_ALLEGRO plugin;
2589 plugin.SetReporter( &reporter );
2590
2591 std::unique_ptr<BOARD> board;
2592
2593 try
2594 {
2595 board = plugin.LoadBoard( dataPath );
2596 }
2597 catch( const IO_ERROR& e )
2598 {
2599 BOOST_TEST_MESSAGE( "IO_ERROR: " << e.What() );
2600 }
2601 catch( const std::exception& e )
2602 {
2603 BOOST_TEST_MESSAGE( "Exception: " << e.what() );
2604 }
2605
2606 reporter.PrintAllMessages( "UIImportPath_NullBoard" );
2607
2608 BOOST_REQUIRE_MESSAGE( board != nullptr, "LoadBoard must return a valid board" );
2609
2610 BOOST_CHECK_GT( board->GetNetCount(), 0 );
2611 BOOST_CHECK_GT( board->Footprints().size(), 0 );
2612 BOOST_CHECK_GT( board->Tracks().size(), 0 );
2613 BOOST_CHECK_EQUAL( reporter.GetErrorCount(), 0 );
2614
2615 PrintBoardStats( board.get(), "ProiectBoard (UI path)" );
2616}
2617
2618
2627BOOST_AUTO_TEST_CASE( SmdPadLayerConsistency )
2628{
2629 std::vector<std::string> boards = GetAllBoardFiles();
2630
2631 for( const std::string& boardPath : boards )
2632 {
2633 std::string boardName = std::filesystem::path( boardPath ).filename().string();
2634 BOARD* board = GetCachedBoard( boardPath );
2635
2636 if( !board )
2637 continue;
2638
2639 BOOST_TEST_CONTEXT( "Testing board: " << boardName )
2640 {
2641 int inconsistentCount = 0;
2642
2643 for( FOOTPRINT* fp : board->Footprints() )
2644 {
2645 const bool onBottom = fp->IsFlipped();
2646
2647 for( PAD* pad : fp->Pads() )
2648 {
2649 if( pad->GetAttribute() != PAD_ATTRIB::SMD )
2650 continue;
2651
2652 LSET layers = pad->GetLayerSet();
2653 bool hasTopCopper = layers.Contains( F_Cu );
2654 bool hasBotCopper = layers.Contains( B_Cu );
2655
2656 if( onBottom && hasTopCopper && !hasBotCopper )
2657 {
2658 inconsistentCount++;
2659 BOOST_TEST_MESSAGE( boardName << ": " << fp->GetReference() << " pad "
2660 << pad->GetNumber() << " is on bottom footprint but SMD "
2661 << "pad has F.Cu without B.Cu" );
2662 }
2663 else if( !onBottom && hasBotCopper && !hasTopCopper )
2664 {
2665 inconsistentCount++;
2666 BOOST_TEST_MESSAGE( boardName << ": " << fp->GetReference() << " pad "
2667 << pad->GetNumber() << " is on top footprint but SMD "
2668 << "pad has B.Cu without F.Cu" );
2669 }
2670 }
2671 }
2672
2673 BOOST_CHECK_EQUAL( inconsistentCount, 0 );
2674 }
2675 }
2676}
2677
2678
2687BOOST_AUTO_TEST_CASE( SmdFootprintTechLayers )
2688{
2689 // Note that this test is NOT true for all boards - some boards have SMD FPs with
2690 // back-layer items.
2691 std::vector<std::string> boards = {
2692 KI_TEST::AllegroBoardFile( "EVK_BaseBoard/EVK_BaseBoard.brd" ),
2693 };
2694
2695 for( const std::string& boardPath : boards )
2696 {
2697 std::string boardName = std::filesystem::path( boardPath ).filename().string();
2698 BOARD* board = GetCachedBoard( boardPath );
2699
2700 BOOST_REQUIRE( board );
2701
2702 BOOST_TEST_CONTEXT( "Testing board: " << boardName )
2703 {
2704 int inconsistentCount = 0;
2705 int checkedFootprints = 0;
2706
2707 for( FOOTPRINT* fp : board->Footprints() )
2708 {
2709 // Only check SMD-only footprints (no through-hole pads)
2710 bool hasSmd = false;
2711 bool hasTH = false;
2712
2713 for( PAD* pad : fp->Pads() )
2714 {
2715 if( pad->GetAttribute() == PAD_ATTRIB::SMD )
2716 hasSmd = true;
2717 else if( pad->GetAttribute() == PAD_ATTRIB::PTH )
2718 hasTH = true;
2719 }
2720
2721 if( !hasSmd || hasTH )
2722 continue;
2723
2724 checkedFootprints++;
2725
2726 const bool onBottom = fp->IsFlipped();
2727
2728 for( BOARD_ITEM* item : fp->GraphicalItems() )
2729 {
2730 PCB_LAYER_ID layer = item->GetLayer();
2731
2732 if( !IsFrontLayer( layer ) && !IsBackLayer( layer ) )
2733 continue;
2734
2735 bool wrongSide = false;
2736
2737 if( onBottom && IsFrontLayer( layer ) )
2738 wrongSide = true;
2739 else if( !onBottom && IsBackLayer( layer ) )
2740 wrongSide = true;
2741
2742 if( wrongSide )
2743 {
2744 inconsistentCount++;
2746 boardName << ": " << fp->GetReference() << " is "
2747 << ( onBottom ? "bottom" : "top" ) << "-side SMD but has "
2748 << item->GetClass() << " on "
2749 << board->GetLayerName( layer ) );
2750 }
2751 }
2752 }
2753
2754 BOOST_TEST_MESSAGE( "Checked " << checkedFootprints
2755 << " SMD-only footprints for tech layer consistency" );
2756 BOOST_CHECK_EQUAL( inconsistentCount, 0 );
2757 }
2758 }
2759}
2760
2761
2768BOOST_AUTO_TEST_CASE( BeagleBone_DrillSlotOrientation )
2769{
2770 std::string dataPath = KI_TEST::AllegroBoardFile( "BeagleBone_Black_RevC/BeagleBone_Black_RevC.brd" );
2771
2772 BOARD* board = GetCachedBoard( dataPath );
2773 BOOST_REQUIRE( board );
2774
2775 struct SLOT_CHECK
2776 {
2777 wxString fpRef;
2778 wxString padNum;
2779 };
2780
2781 // These pads have vertical oblong copper shapes and should have taller-than-wide drill slots
2782 std::vector<SLOT_CHECK> checks = {
2783 { wxS( "P6" ), wxS( "20" ) },
2784 { wxS( "P6" ), wxS( "21" ) },
2785 { wxS( "P3" ), wxS( "6" ) },
2786 { wxS( "P1" ), wxS( "1" ) },
2787 { wxS( "P1" ), wxS( "2" ) },
2788 { wxS( "P1" ), wxS( "3" ) },
2789 };
2790
2791 for( const auto& check : checks )
2792 {
2793 BOOST_TEST_CONTEXT( check.fpRef << " pad " << check.padNum )
2794 {
2795 FOOTPRINT* fp = nullptr;
2796
2797 for( FOOTPRINT* candidate : board->Footprints() )
2798 {
2799 if( candidate->GetReference() == check.fpRef )
2800 {
2801 fp = candidate;
2802 break;
2803 }
2804 }
2805
2806 BOOST_REQUIRE_MESSAGE( fp != nullptr, "Footprint " << check.fpRef << " should exist" );
2807
2808 PAD* pad = nullptr;
2809
2810 for( PAD* candidate : fp->Pads() )
2811 {
2812 if( candidate->GetNumber() == check.padNum )
2813 {
2814 pad = candidate;
2815 break;
2816 }
2817 }
2818
2819 BOOST_REQUIRE_MESSAGE( pad != nullptr,
2820 "Pad " << check.padNum << " should exist on " << check.fpRef );
2821
2822 VECTOR2I padSize = pad->GetSize( F_Cu );
2823 VECTOR2I drillSize = pad->GetDrillSize();
2824
2825 BOOST_TEST_MESSAGE( check.fpRef << " pad " << check.padNum
2826 << ": pad=" << padSize.x << "x" << padSize.y
2827 << " drill=" << drillSize.x << "x" << drillSize.y );
2828
2829 // If the pad is oblong, the drill should match the pad's aspect ratio
2830 if( drillSize.x != drillSize.y )
2831 {
2832 bool padIsTaller = ( padSize.y > padSize.x );
2833 bool drillIsTaller = ( drillSize.y > drillSize.x );
2834
2835 BOOST_CHECK_MESSAGE( padIsTaller == drillIsTaller,
2836 "Drill slot should match pad orientation" );
2837 }
2838 }
2839 }
2840}
2841
2842
2847BOOST_AUTO_TEST_CASE( BeagleBone_ZoneFills )
2848{
2849 std::string dataPath = KI_TEST::AllegroBoardFile( "BeagleBone_Black_RevC/BeagleBone_Black_RevC.brd" );
2850
2851 BOARD* board = GetCachedBoard( dataPath );
2852 BOOST_REQUIRE( board );
2853
2854 int filledZoneCount = 0;
2855 int totalCopperZones = 0;
2856
2857 for( const ZONE* zone : board->Zones() )
2858 {
2859 if( zone->GetIsRuleArea() || zone->GetNetCode() == 0 )
2860 continue;
2861
2862 totalCopperZones++;
2863
2864 if( zone->IsFilled() )
2865 {
2866 filledZoneCount++;
2867
2868 BOOST_TEST_MESSAGE( "Filled zone: net=" << zone->GetNetname()
2869 << " layers=" << zone->GetLayerSet().count() );
2870 }
2871 }
2872
2873 BOOST_TEST_MESSAGE( "Total copper zones: " << totalCopperZones
2874 << ", filled: " << filledZoneCount );
2875
2876 BOOST_CHECK_GT( totalCopperZones, 0 );
2877 BOOST_CHECK_GT( filledZoneCount, 0 );
2878}
2879
2880
2885BOOST_AUTO_TEST_CASE( BeagleBone_Teardrops )
2886{
2887 std::string dataPath = KI_TEST::AllegroBoardFile( "BeagleBone_Black_RevC/BeagleBone_Black_RevC.brd" );
2888
2889 BOARD* board = GetCachedBoard( dataPath );
2890 BOOST_REQUIRE( board );
2891
2892 int teardropZones = 0;
2893
2894 for( const ZONE* zone : board->Zones() )
2895 {
2896 if( zone->IsTeardropArea() )
2897 teardropZones++;
2898 }
2899
2900 BOOST_CHECK_GT( teardropZones, 1000 );
2901 BOOST_TEST_MESSAGE( "Teardrop zones: " << teardropZones );
2902
2903 // Pads and vias anchoring teardrops must have teardrops enabled
2904 int padsWithTeardrops = 0;
2905 int totalPads = 0;
2906
2907 for( const FOOTPRINT* fp : board->Footprints() )
2908 {
2909 for( const PAD* pad : fp->Pads() )
2910 {
2911 totalPads++;
2912
2913 if( pad->GetTeardropsEnabled() )
2914 padsWithTeardrops++;
2915 }
2916 }
2917
2918 BOOST_CHECK_GT( padsWithTeardrops, 0 );
2919 BOOST_TEST_MESSAGE( "Pads with teardrops enabled: " << padsWithTeardrops << " / " << totalPads );
2920
2921 int viasWithTeardrops = 0;
2922 int totalVias = 0;
2923
2924 for( const PCB_TRACK* track : board->Tracks() )
2925 {
2926 if( track->Type() != PCB_VIA_T )
2927 continue;
2928
2929 totalVias++;
2930
2931 if( static_cast<const PCB_VIA*>( track )->GetTeardropsEnabled() )
2932 viasWithTeardrops++;
2933 }
2934
2935 BOOST_CHECK_GT( viasWithTeardrops, 0 );
2936 BOOST_TEST_MESSAGE( "Vias with teardrops enabled: " << viasWithTeardrops << " / " << totalVias );
2937}
2938
2939
2944BOOST_AUTO_TEST_CASE( PreV172_NoTeardrops )
2945{
2946 std::string dataPath = KI_TEST::AllegroBoardFile( "ProiectBoard/ProiectBoard.brd" );
2947
2948 BOARD* board = GetCachedBoard( dataPath );
2949 BOOST_REQUIRE( board );
2950
2951 int teardropZones = 0;
2952
2953 for( const ZONE* zone : board->Zones() )
2954 {
2955 if( zone->IsTeardropArea() )
2956 teardropZones++;
2957 }
2958
2959 BOOST_CHECK_EQUAL( teardropZones, 0 );
2960
2961 int padsWithTeardrops = 0;
2962
2963 for( const FOOTPRINT* fp : board->Footprints() )
2964 {
2965 for( const PAD* pad : fp->Pads() )
2966 {
2967 if( pad->GetTeardropsEnabled() )
2968 padsWithTeardrops++;
2969 }
2970 }
2971
2972 BOOST_CHECK_EQUAL( padsWithTeardrops, 0 );
2973}
2974
2975
2981BOOST_AUTO_TEST_CASE( LegacyNetclassFlags )
2982{
2983 std::string dataPath = KI_TEST::AllegroBoardFile( "TRS80_POWER/TRS80_POWER.brd" );
2984
2985 PCB_IO_ALLEGRO plugin;
2987 plugin.SetReporter( &reporter );
2988
2989 std::unique_ptr<BOARD> board = plugin.LoadBoard( dataPath );
2990
2991 BOOST_REQUIRE( board );
2992
2993 BOOST_CHECK_MESSAGE( board->m_LegacyNetclassesLoaded,
2994 "m_LegacyNetclassesLoaded must be true after Allegro import" );
2995 BOOST_CHECK_MESSAGE( board->m_LegacyDesignSettingsLoaded,
2996 "m_LegacyDesignSettingsLoaded must be true after Allegro import" );
2997}
2998
2999
3004BOOST_AUTO_TEST_CASE( NetclassesCreatedForAllBoards )
3005{
3006 std::vector<std::string> boards = GetAllBoardFiles();
3007
3008 for( const std::string& boardPath : boards )
3009 {
3010 std::string boardName = std::filesystem::path( boardPath ).filename().string();
3011 BOARD* board = GetCachedBoard( boardPath );
3012
3013 if( !board )
3014 continue;
3015
3016 BOOST_TEST_CONTEXT( "Testing board: " << boardName )
3017 {
3018 std::shared_ptr<NET_SETTINGS> netSettings = board->GetDesignSettings().m_NetSettings;
3019 const auto& netclasses = netSettings->GetNetclasses();
3020
3021 BOOST_TEST_MESSAGE( boardName << ": " << netclasses.size() << " netclasses" );
3022
3023 for( const auto& [name, nc] : netclasses )
3024 {
3025 BOOST_TEST_MESSAGE( " " << name << ": track="
3026 << nc->GetTrackWidth() << " clearance="
3027 << nc->GetClearance() );
3028
3029 // Constraint set netclasses should have positive track width.
3030 // Skip generated netclasses (DP_, MG_, W*mil) which may not set track width.
3031 if( !name.StartsWith( wxS( "DP_" ) )
3032 && !name.StartsWith( wxS( "MG_" ) )
3033 && !name.StartsWith( wxS( "W" ) ) )
3034 {
3035 BOOST_CHECK_MESSAGE( nc->HasTrackWidth(),
3036 name << " should have a track width" );
3037 BOOST_CHECK_MESSAGE( nc->GetTrackWidth() > 0,
3038 name << " track width should be positive" );
3039 }
3040 }
3041 }
3042 }
3043}
3044
3045
const char * name
General utilities for PCB file IO for QA programs.
BOX2< VECTOR2I > BOX2I
Definition box2.h:927
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:84
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:409
const NETINFO_LIST & GetNetInfo() const
Definition board.h:1207
const ZONES & Zones() const
Definition board.h:467
PCB_LAYER_ID GetLayerID(const wxString &aLayerName) const
Return the ID of a layer.
Definition board.cpp:916
const FOOTPRINTS & Footprints() const
Definition board.h:463
const TRACKS & Tracks() const
Definition board.h:461
const wxString GetLayerName(PCB_LAYER_ID aLayer) const
Return the name of a aLayer.
Definition board.cpp:936
BOARD_DESIGN_SETTINGS & GetDesignSettings() const
Definition board.cpp:1299
const DRAWINGS & Drawings() const
Definition board.h:465
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:653
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:229
double AsDegrees() const
Definition eda_angle.h:116
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:175
const VECTOR2I & GetEnd() const
Return the ending point of the graphic.
Definition eda_shape.h:325
const VECTOR2I & GetStart() const
Return the starting point of the graphic.
Definition eda_shape.h:275
EDA_ANGLE GetOrientation() const
Definition footprint.h:438
std::deque< PAD * > & Pads()
Definition footprint.h:404
PCB_LAYER_ID GetLayer() const override
Return the primary layer this item is on.
Definition footprint.h:449
bool IsFlipped() const
Definition footprint.h:660
VECTOR3D m_Offset
3D model offset (mm)
Definition footprint.h:183
VECTOR3D m_Rotation
3D model rotation (degrees)
Definition footprint.h:182
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:280
Definition pad.h:61
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:68
const VECTOR2I & GetStart() const
Definition pcb_track.h:93
const VECTOR2I & GetEnd() const
Definition pcb_track.h:90
Definition seg.h:38
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:596
@ 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
bool IsFrontLayer(PCB_LAYER_ID aLayerId)
Layer classification: check if it's a front layer.
Definition layer_ids.h:806
bool IsBackLayer(PCB_LAYER_ID aLayerId)
Layer classification: check if it's a back layer.
Definition layer_ids.h:829
bool IsCopperLayer(int aLayerId)
Test whether a layer is a copper layer.
Definition layer_ids.h:703
PCB_LAYER_ID
A quick note on layer IDs:
Definition layer_ids.h:56
@ Edge_Cuts
Definition layer_ids.h:108
@ B_Cu
Definition layer_ids.h:61
@ 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.
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.
std::string AllegroBoardFile(const std::string &aFileName)
EDA_ANGLE abs(const EDA_ANGLE &aAngle)
Definition eda_angle.h:411
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
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:683