KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_pads_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 (C) 2025 KiCad Developers, see AUTHORS.txt for contributors.
5 *
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU General Public License
8 * as published by the Free Software Foundation; either version 2
9 * of the License, or (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, you may find one here:
18 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
19 * or you may search the http://www.gnu.org website for the version 2 license,
20 * or you may write to the Free Software Foundation, Inc.,
21 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
22 */
23
27
29#include <layer_ids.h>
30#include <padstack.h>
31#include <board.h>
32#include <pcb_text.h>
33#include <pcb_shape.h>
34#include <pcb_field.h>
35#include <pad.h>
36#include <pcb_track.h>
37#include <footprint.h>
38#include <zone.h>
40#include <pcb_dimension.h>
43#include <set>
44
45
47{
48 std::string dir;
49 std::string file;
50};
51
52
53static const PADS_BOARD_INFO PADS_BOARDS[] = {
54 { "ClaySight_MK1", "ClaySight_MK1.asc" }, // V10.0 BASIC
55 { "TMS1mmX19", "TMS1mmX19.asc" }, // V9.5 BASIC MILS
56 { "MC4_PLUS_CSHAPE", "MC4_PLUS_CSHAPE.asc" }, // V9.5 MILS
57 { "MC2_PLUS_REV1", "MC2_PLUS_REV1.asc" }, // V9.4 METRIC
58 { "Ems4_Rev2", "Ems4_Rev2.asc" }, // V9.4 MILS
59 { "LCORE_4", "LCORE_4.asc" }, // V9.0 METRIC
60 { "LCORE_2", "LCORE_2.asc" }, // V2005.0 METRIC
61 { "Dexter_MotorCtrl", "Dexter_MotorCtrl.asc" }, // V2007.0 MILS
62 { "MAIS_FC", "MAIS_FC.asc" }, // V5.0 METRIC
63 { "ClaySight_MK2", "ClaySight_MK2.asc" }, // V10.0 BASIC (copper lines)
64};
65
66
67static wxString GetBoardPath( const PADS_BOARD_INFO& aBoard )
68{
69 return KI_TEST::GetPcbnewTestDataDir() + "plugins/pads/" + aBoard.dir + "/" + aBoard.file;
70}
71
72
76static std::unique_ptr<BOARD> LoadAndVerify( const PADS_BOARD_INFO& aBoard )
77{
78 PCB_IO_PADS plugin;
79
80 wxString filename = GetBoardPath( aBoard );
81
82 BOOST_CHECK_MESSAGE( plugin.CanReadBoard( filename ),
83 aBoard.dir << " should be a readable PADS file" );
84
85 std::unique_ptr<BOARD> board;
86
87 try
88 {
89 board.reset( plugin.LoadBoard( filename, nullptr, nullptr, nullptr ) );
90 }
91 catch( const std::exception& e )
92 {
93 BOOST_WARN_MESSAGE( false,
94 aBoard.dir << " threw exception during load: " << e.what() );
95 return board;
96 }
97
98 BOOST_REQUIRE_MESSAGE( board != nullptr, aBoard.dir << " failed to load" );
99 BOOST_CHECK_MESSAGE( board->Footprints().size() > 0,
100 aBoard.dir << " should have footprints" );
101
102 return board;
103}
104
105
113static void RunStructuralChecks( const PADS_BOARD_INFO& aBoard )
114{
115 std::unique_ptr<BOARD> board = LoadAndVerify( aBoard );
116
117 if( !board )
118 return;
119
120 BOOST_WARN_MESSAGE( board->Tracks().size() > 0,
121 aBoard.dir << " has no tracks (parser may not support this format version)" );
122
123 if( board->Tracks().size() > 0 && board->Footprints().size() > 0 )
124 {
125 BOX2I fpBbox;
126 fpBbox.SetMaximum();
127
128 for( FOOTPRINT* fp : board->Footprints() )
129 fpBbox.Merge( fp->GetBoundingBox() );
130
131 BOX2I trackBbox;
132 trackBbox.SetMaximum();
133
134 for( PCB_TRACK* trk : board->Tracks() )
135 trackBbox.Merge( trk->GetBoundingBox() );
136
137 BOOST_CHECK_MESSAGE( fpBbox.Intersects( trackBbox ),
138 aBoard.dir << " footprint and track bounding boxes should overlap" );
139 }
140
141 // No duplicate through-hole vias at the same position
142 std::set<std::pair<int, int>> viaPositions;
143 bool hasDuplicate = false;
144
145 for( PCB_TRACK* trk : board->Tracks() )
146 {
147 PCB_VIA* via = dynamic_cast<PCB_VIA*>( trk );
148
149 if( !via || via->GetViaType() != VIATYPE::THROUGH )
150 continue;
151
152 auto key = std::make_pair( via->GetPosition().x, via->GetPosition().y );
153
154 if( viaPositions.count( key ) )
155 {
156 hasDuplicate = true;
157 break;
158 }
159
160 viaPositions.insert( key );
161 }
162
163 BOOST_CHECK_MESSAGE( !hasDuplicate,
164 aBoard.dir << " should have no duplicate through-hole vias" );
165
166 // All imported tracks must be on copper layers
167 for( PCB_TRACK* trk : board->Tracks() )
168 {
169 if( trk->Type() == PCB_TRACE_T || trk->Type() == PCB_ARC_T )
170 {
171 BOOST_CHECK_MESSAGE( IsCopperLayer( trk->GetLayer() ),
172 aBoard.dir << " track on non-copper layer " << trk->GetLayer() );
173 }
174 }
175
176 // Pad size check uses WARN since some boards have pads the parser doesn't handle yet
177 for( FOOTPRINT* fp : board->Footprints() )
178 {
179 for( PAD* pad : fp->Pads() )
180 {
181 BOOST_WARN_MESSAGE( pad->GetSize( PADSTACK::ALL_LAYERS ).x > 0
182 && pad->GetSize( PADSTACK::ALL_LAYERS ).y > 0,
183 aBoard.dir << " " << fp->GetReference() << " pad has zero size" );
184 }
185 }
186
187 // Every zone outline must have non-empty contours
188 for( ZONE* zone : board->Zones() )
189 {
190 const SHAPE_POLY_SET* outline = zone->Outline();
191 BOOST_REQUIRE_MESSAGE( outline != nullptr,
192 aBoard.dir << " zone has null outline" );
193
194 for( int ii = 0; ii < outline->OutlineCount(); ++ii )
195 {
196 BOOST_CHECK_MESSAGE( outline->COutline( ii ).PointCount() >= 3,
197 aBoard.dir << " zone outline " << ii << " has "
198 << outline->COutline( ii ).PointCount() << " points" );
199 }
200 }
201}
202
203
204BOOST_AUTO_TEST_SUITE( PADS_IMPORT )
205
206
207BOOST_AUTO_TEST_CASE( ImportClaySight_MK1 )
208{
210}
211
212
220BOOST_AUTO_TEST_CASE( ClaySight_MK1_ElementCounts )
221{
222 std::unique_ptr<BOARD> board = LoadAndVerify( PADS_BOARDS[0] );
223
224 BOOST_REQUIRE( board != nullptr );
225
226 // Footprints: 36 parts in the *PART* section
227 BOOST_CHECK_EQUAL( board->Footprints().size(), 36 );
228
229 // Total pads across all footprints
230 int totalPads = 0;
231
232 for( FOOTPRINT* fp : board->Footprints() )
233 totalPads += fp->Pads().size();
234
235 BOOST_CHECK_EQUAL( totalPads, 140 );
236
237 // Tracks: routed signal segments from 32 *SIGNAL* sections
238 int traceCount = 0;
239 int viaCount = 0;
240
241 for( PCB_TRACK* trk : board->Tracks() )
242 {
243 if( trk->Type() == PCB_TRACE_T || trk->Type() == PCB_ARC_T )
244 traceCount++;
245 else if( trk->Type() == PCB_VIA_T )
246 viaCount++;
247 }
248
249 BOOST_CHECK_EQUAL( traceCount, 247 );
250
251 // No vias on this 2-layer board (empty *VIA* section)
252 BOOST_CHECK_EQUAL( viaCount, 0 );
253
254 // No zones (empty *POUR* section)
255 BOOST_CHECK_EQUAL( board->Zones().size(), 0 );
256
257 // Board outline on Edge.Cuts
258 int edgeCutsCount = 0;
259
260 for( BOARD_ITEM* item : board->Drawings() )
261 {
262 if( PCB_SHAPE* shape = dynamic_cast<PCB_SHAPE*>( item ) )
263 {
264 if( shape->GetLayer() == Edge_Cuts )
265 edgeCutsCount++;
266 }
267 }
268
269 BOOST_CHECK_EQUAL( edgeCutsCount, 6 );
270
271 // Free text items from the *TEXT* section
272 int textCount = 0;
273
274 for( BOARD_ITEM* item : board->Drawings() )
275 {
276 if( dynamic_cast<PCB_TEXT*>( item ) )
277 textCount++;
278 }
279
280 BOOST_CHECK_EQUAL( textCount, 17 );
281
282 // Net assignments: tracks and pads should reference named nets
283 std::set<wxString> trackNets;
284
285 for( PCB_TRACK* trk : board->Tracks() )
286 {
287 NETINFO_ITEM* net = trk->GetNet();
288
289 if( net && !net->GetNetname().IsEmpty() )
290 trackNets.insert( net->GetNetname() );
291 }
292
293 BOOST_CHECK_EQUAL( trackNets.size(), 32 );
294
295 // All traces on copper layers (F.Cu or B.Cu for this 2-layer board)
296 for( PCB_TRACK* trk : board->Tracks() )
297 {
298 if( trk->Type() == PCB_TRACE_T )
299 {
300 PCB_LAYER_ID layer = trk->GetLayer();
301 BOOST_CHECK_MESSAGE( layer == F_Cu || layer == B_Cu,
302 "trace on unexpected layer " << layer );
303 }
304 }
305}
306
307
308BOOST_AUTO_TEST_CASE( ImportTMS1mmX19 )
309{
311}
312
313
314BOOST_AUTO_TEST_CASE( ImportMC4_PLUS_CSHAPE )
315{
317}
318
319
320BOOST_AUTO_TEST_CASE( ImportMC2_PLUS_REV1 )
321{
323}
324
325
326BOOST_AUTO_TEST_CASE( ImportEms4_Rev2 )
327{
329}
330
331
332BOOST_AUTO_TEST_CASE( ImportLCORE_4 )
333{
335}
336
337
338BOOST_AUTO_TEST_CASE( ImportLCORE_2 )
339{
341}
342
343
344BOOST_AUTO_TEST_CASE( ImportDexter_MotorCtrl )
345{
347}
348
349
350BOOST_AUTO_TEST_CASE( ImportMAIS_FC )
351{
353}
354
355
356BOOST_AUTO_TEST_CASE( ImportNonCopperTrackSkipped )
357{
358 // Test that tracks on non-copper layers are skipped without crashing
359 PCB_IO_PADS plugin;
360
361 wxString filename = KI_TEST::GetPcbnewTestDataDir() + "plugins/pads/synthetic_noncopper_track.asc";
362
363 std::unique_ptr<BOARD> board( plugin.LoadBoard( filename, nullptr, nullptr, nullptr ) );
364
365 BOOST_REQUIRE( board != nullptr );
366
367 int track_count = 0;
368
369 for( PCB_TRACK* track : board->Tracks() )
370 {
371 if( track->Type() == PCB_TRACE_T || track->Type() == PCB_ARC_T )
372 {
373 track_count++;
374 BOOST_CHECK( IsCopperLayer( track->GetLayer() ) );
375 }
376 }
377
378 BOOST_CHECK( track_count > 0 );
379}
380
381
382BOOST_AUTO_TEST_CASE( ImportTextOnUnmappedLayer )
383{
384 // Test that text on unmapped layers is assigned to Comments layer without crashing
385 PCB_IO_PADS plugin;
386
387 wxString filename = KI_TEST::GetPcbnewTestDataDir() + "plugins/pads/synthetic_unmapped_text_layer.asc";
388
389 std::unique_ptr<BOARD> board( plugin.LoadBoard( filename, nullptr, nullptr, nullptr ) );
390
391 BOOST_REQUIRE( board != nullptr );
392
393 int silkscreen_count = 0;
394 int comments_count = 0;
395 int copper_count = 0;
396
397 for( BOARD_ITEM* item : board->Drawings() )
398 {
399 if( PCB_TEXT* text = dynamic_cast<PCB_TEXT*>( item ) )
400 {
401 PCB_LAYER_ID layer = text->GetLayer();
402
403 BOOST_CHECK( layer != UNDEFINED_LAYER );
404
405 if( layer == F_SilkS )
406 silkscreen_count++;
407 else if( layer == Cmts_User )
408 comments_count++;
409 else if( layer == F_Cu )
410 copper_count++;
411 }
412 }
413
414 BOOST_CHECK_EQUAL( silkscreen_count, 1 );
415 BOOST_CHECK_EQUAL( comments_count, 1 );
416 BOOST_CHECK_EQUAL( copper_count, 1 );
417}
418
419
420BOOST_AUTO_TEST_CASE( ImportClaySight_MK2 )
421{
423}
424
425
433BOOST_AUTO_TEST_CASE( ClaySight_MK2_ElementCounts )
434{
435 std::unique_ptr<BOARD> board = LoadAndVerify( PADS_BOARDS[9] );
436
437 BOOST_REQUIRE( board != nullptr );
438
439 // 10 parts: U1 (RPi Pico), SU1-SU8 (TO-92), U2 (ULN2003A)
440 BOOST_CHECK_EQUAL( board->Footprints().size(), 10 );
441
442 // U1=40 pads, SU1-SU8=3 each (24), U2=16 pads = 80 total
443 int totalPads = 0;
444
445 for( FOOTPRINT* fp : board->Footprints() )
446 totalPads += fp->Pads().size();
447
448 BOOST_CHECK_EQUAL( totalPads, 80 );
449
450 int traceCount = 0;
451 int viaCount = 0;
452
453 for( PCB_TRACK* trk : board->Tracks() )
454 {
455 if( trk->Type() == PCB_TRACE_T || trk->Type() == PCB_ARC_T )
456 traceCount++;
457 else if( trk->Type() == PCB_VIA_T )
458 viaCount++;
459 }
460
461 // 138 track segments from 2 *SIGNAL* route sections only
462 BOOST_CHECK_EQUAL( traceCount, 138 );
463 BOOST_CHECK_EQUAL( viaCount, 0 );
464
465 // 2 nets from *SIGNAL* routes: N$12982 and N$12975
466 std::set<wxString> trackNets;
467
468 for( PCB_TRACK* trk : board->Tracks() )
469 {
470 NETINFO_ITEM* net = trk->GetNet();
471
472 if( net && !net->GetNetname().IsEmpty() )
473 trackNets.insert( net->GetNetname() );
474 }
475
476 BOOST_CHECK_EQUAL( trackNets.size(), 2 );
477
478 // All traces on copper layers
479 for( PCB_TRACK* trk : board->Tracks() )
480 {
481 if( trk->Type() == PCB_TRACE_T )
482 {
483 BOOST_CHECK_MESSAGE( IsCopperLayer( trk->GetLayer() ),
484 "trace on non-copper layer " << trk->GetLayer() );
485 }
486 }
487
488 // 72 COPPER items on layer 126 become silkscreen graphics. 64 of these
489 // (16 groups of 4 axis-aligned segments) are detected as rectangles. The
490 // remaining 8 are individual segments. Plus 44 segments from LINES items.
491 int silkCount = 0;
492 int rectCount = 0;
493
494 for( BOARD_ITEM* item : board->Drawings() )
495 {
496 if( PCB_SHAPE* shape = dynamic_cast<PCB_SHAPE*>( item ) )
497 {
498 if( shape->GetLayer() == F_SilkS )
499 {
500 silkCount++;
501
502 if( shape->GetShape() == SHAPE_T::RECTANGLE )
503 rectCount++;
504 }
505 }
506 }
507
508 BOOST_CHECK_EQUAL( silkCount, 68 );
509 BOOST_CHECK_EQUAL( rectCount, 16 );
510
511 // Default via size from JMPVIA_1 definition (drill=457505, size=915010 BASIC)
512 const BOARD_DESIGN_SETTINGS& bds = board->GetDesignSettings();
513 std::shared_ptr<NETCLASS> defaultNc = bds.m_NetSettings->GetDefaultNetclass();
514 BOOST_CHECK( defaultNc->GetViaDiameter() > 0 );
515 BOOST_CHECK( defaultNc->GetViaDrill() > 0 );
516 BOOST_CHECK( defaultNc->GetViaDiameter() > defaultNc->GetViaDrill() );
518
519 // Copper-to-edge clearance from OUTLINE_TO_* rules (227990 BASIC)
520 BOOST_CHECK( bds.m_CopperEdgeClearance > 0 );
521
522 // Board outline on Edge.Cuts (rectangular outline = 4 segments)
523 int edgeCutsCount = 0;
524
525 for( BOARD_ITEM* item : board->Drawings() )
526 {
527 if( PCB_SHAPE* shape = dynamic_cast<PCB_SHAPE*>( item ) )
528 {
529 if( shape->GetLayer() == Edge_Cuts )
530 edgeCutsCount++;
531 }
532 }
533
534 BOOST_CHECK_EQUAL( edgeCutsCount, 4 );
535
536 // 1 board-level text item on silkscreen with multi-line content
537 int textCount = 0;
538 wxString textContent;
539
540 for( BOARD_ITEM* item : board->Drawings() )
541 {
542 if( item->Type() == PCB_TEXT_T )
543 {
544 PCB_TEXT* text = static_cast<PCB_TEXT*>( item );
545 textContent = text->GetText();
546 textCount++;
547 }
548 }
549
550 BOOST_CHECK_EQUAL( textCount, 1 );
551
552 // EasyEDA exports encode newlines as underscores in text content.
553 // The parser converts them back to newlines for proper multi-line display.
554 BOOST_CHECK( textContent.Contains( wxT( "\n" ) ) );
555 BOOST_CHECK( !textContent.Contains( wxT( "_" ) ) );
556 BOOST_CHECK( textContent.Contains( wxT( "CLAYSIGHT MCU V.2" ) ) );
557 BOOST_CHECK( textContent.Contains( wxT( "The Ohio State University" ) ) );
558}
559
560
567BOOST_AUTO_TEST_CASE( MAIS_FC_Stackup )
568{
569 std::unique_ptr<BOARD> board = LoadAndVerify( PADS_BOARDS[8] );
570
571 BOOST_REQUIRE( board != nullptr );
572
573 const BOARD_DESIGN_SETTINGS& bds = board->GetDesignSettings();
574 BOOST_CHECK( bds.m_HasStackup );
575
576 const BOARD_STACKUP& stackup = bds.GetStackupDescriptor();
577
578 bool foundCopperThickness = false;
579 bool foundDielectric = false;
580
581 for( BOARD_STACKUP_ITEM* item : stackup.GetList() )
582 {
583 if( item->GetType() == BOARD_STACKUP_ITEM_TYPE::BS_ITEM_TYPE_COPPER )
584 {
585 if( item->GetThickness() > 0 )
586 foundCopperThickness = true;
587 }
588 else if( item->GetType() == BOARD_STACKUP_ITEM_TYPE::BS_ITEM_TYPE_DIELECTRIC )
589 {
590 if( item->GetEpsilonR() > 3.0 )
591 foundDielectric = true;
592 }
593 }
594
595 BOOST_CHECK_MESSAGE( foundCopperThickness, "stackup should have non-zero copper thickness" );
596 BOOST_CHECK_MESSAGE( foundDielectric, "stackup should have dielectric constant > 3.0" );
597 BOOST_CHECK( bds.GetBoardThickness() > 0 );
598}
599
600
609BOOST_AUTO_TEST_CASE( ImportDegeneratePourSkipped )
610{
611 PCB_IO_PADS plugin;
612
613 wxString filename = KI_TEST::GetPcbnewTestDataDir()
614 + "plugins/pads/synthetic_degenerate_pour.asc";
615
616 std::unique_ptr<BOARD> board( plugin.LoadBoard( filename, nullptr, nullptr, nullptr ) );
617
618 BOOST_REQUIRE( board != nullptr );
619
620 // Only the valid 4-point pour should produce a zone.
621 // The PADTHERM (2 SEG pieces with 2 points each) and
622 // VIATHERM (1 SEG piece with 2 points) must be skipped.
623 BOOST_CHECK_EQUAL( board->Zones().size(), 1 );
624
625 // The single valid zone must have a non-degenerate outline
626 if( board->Zones().size() == 1 )
627 {
628 ZONE* zone = board->Zones()[0];
629 BOOST_CHECK( zone->Outline()->OutlineCount() == 1 );
630 BOOST_CHECK( zone->Outline()->COutline( 0 ).PointCount() >= 3 );
631 }
632}
633
634
643BOOST_AUTO_TEST_CASE( ImportFilledCopperSingleOutline )
644{
645 PCB_IO_PADS plugin;
646
647 wxString filename = KI_TEST::GetPcbnewTestDataDir()
648 + "plugins/pads/synthetic_filled_copper.asc";
649
650 std::unique_ptr<BOARD> board( plugin.LoadBoard( filename, nullptr, nullptr, nullptr ) );
651
652 BOOST_REQUIRE( board != nullptr );
653 BOOST_REQUIRE_EQUAL( board->Zones().size(), 1 );
654
655 ZONE* zone = board->Zones()[0];
656 BOOST_CHECK_EQUAL( zone->Outline()->OutlineCount(), 1 );
657 BOOST_CHECK( zone->Outline()->COutline( 0 ).PointCount() >= 3 );
658}
659
660
670BOOST_AUTO_TEST_CASE( Importer_SpecificFixes )
671{
672 PCB_IO_PADS plugin;
673
674 wxString filename = KI_TEST::GetPcbnewTestDataDir() + "plugins/pads/Importer.asc";
675
676 std::unique_ptr<BOARD> board( plugin.LoadBoard( filename, nullptr, nullptr, nullptr ) );
677
678 BOOST_REQUIRE( board != nullptr );
679
680 // Bug 1: Graphics on layers 18/19/20 must be imported.
681 // The LAYERSTACK_6L_35U block has 556 pieces mostly on layer 18, plus layer 20.
682 // The DRW59706864 block has a BOARD outline on layer 0 (Edge.Cuts).
683 // Count graphics on Dwgs_User and Cmts_User to verify documentation layers imported.
684 int dwgsUserCount = 0;
685
686 for( BOARD_ITEM* item : board->Drawings() )
687 {
688 if( PCB_SHAPE* shape = dynamic_cast<PCB_SHAPE*>( item ) )
689 {
690 if( shape->GetLayer() == Dwgs_User )
691 dwgsUserCount++;
692 }
693 }
694
695 BOOST_CHECK_MESSAGE( dwgsUserCount > 0,
696 "graphics on PADS layer 18 (drill drawing) should map to Dwgs_User" );
697
698 // Bug 3: U1 pads should be oval (OF shape), not circular (RT thermal).
699 // U1 has part type DIO_RECT_3PH_1600V_100A with DIOB_D100JHT160V decal.
700 // PAD 0 stack has OF 0.000 11550000 on layer -2 (4.8mm height, 11.55mm width).
701 FOOTPRINT* u1 = nullptr;
702
703 for( FOOTPRINT* fp : board->Footprints() )
704 {
705 if( fp->GetReference() == wxT( "U1" ) )
706 {
707 u1 = fp;
708 break;
709 }
710 }
711
712 BOOST_REQUIRE_MESSAGE( u1 != nullptr, "U1 footprint should exist" );
713
714 bool foundOvalPad = false;
715
716 for( PAD* pad : u1->Pads() )
717 {
718 VECTOR2I padSize = pad->GetSize( PADSTACK::ALL_LAYERS );
719
720 if( pad->GetShape( PADSTACK::ALL_LAYERS ) == PAD_SHAPE::OVAL && padSize.x != padSize.y )
721 {
722 foundOvalPad = true;
723 break;
724 }
725 }
726
727 BOOST_CHECK_MESSAGE( foundOvalPad, "U1 should have oval pads (OF shape, not RT thermal)" );
728
729 // Bug 2: Copper pours should not have duplicates from HATOUT records.
730 // The file has 13 POUROUT records. HATOUT records should become fills, not zones.
731 // Count zones that are NOT rule areas (actual copper pours).
732 int pourZoneCount = 0;
733 int filledZoneCount = 0;
734
735 for( ZONE* zone : board->Zones() )
736 {
737 if( !zone->GetIsRuleArea() )
738 {
739 pourZoneCount++;
740
741 if( zone->IsFilled() )
742 filledZoneCount++;
743 }
744 }
745
746 BOOST_CHECK_MESSAGE( pourZoneCount <= 13,
747 "should not have duplicate zones from HATOUT; got " << pourZoneCount );
748
749 BOOST_CHECK_MESSAGE( filledZoneCount > 0, "HATOUT records should produce filled zones" );
750
751 // Bug 4: Dimension line should not be skewed.
752 // DIM92271615 measures 110.00mm horizontal. Start=(0,9000000) end=(165000000,1500000).
753 // After fix, both endpoints should have the same Y for horizontal measurement.
754 int dimCount = 0;
755
756 for( BOARD_ITEM* item : board->Drawings() )
757 {
758 if( PCB_DIM_ALIGNED* dim = dynamic_cast<PCB_DIM_ALIGNED*>( item ) )
759 {
760 dimCount++;
761
762 VECTOR2I start = dim->GetStart();
763 VECTOR2I end = dim->GetEnd();
764
765 BOOST_CHECK_MESSAGE( start.y == end.y,
766 "horizontal dimension endpoints should have equal Y coordinates; "
767 "start.y=" << start.y << " end.y=" << end.y );
768 }
769 }
770
771 BOOST_CHECK_MESSAGE( dimCount > 0, "should have at least one dimension" );
772}
773
774
787BOOST_AUTO_TEST_CASE( Peka_ViaImport )
788{
789 PCB_IO_PADS plugin;
790
791 wxString filename = KI_TEST::GetPcbnewTestDataDir() + "plugins/pads/peka.asc";
792
793 std::unique_ptr<BOARD> board( plugin.LoadBoard( filename, nullptr, nullptr, nullptr ) );
794
795 BOOST_REQUIRE( board != nullptr );
796
797 // Collect all vias and check for duplicates
798 std::map<std::pair<int, int>, int> viaPositionCount;
799 int blindCount = 0;
800 int throughCount = 0;
801
802 // Copper pad size from the STANDARDVIA definition is 1371600 BASIC units.
803 // At BASIC_TO_NM = 25400/38100, that converts to ~914,400 nm (36 mil).
804 // The soldermask opening is 2400300 BASIC = ~1,600,200 nm (63 mil).
805 // Via size must use the copper value, not the mask opening.
806 const int maxExpectedViaWidth = 1200000; // 1.2mm, well above 36 mil copper pad
807
808 int oversizedViaCount = 0;
809
810 for( PCB_TRACK* track : board->Tracks() )
811 {
812 PCB_VIA* via = dynamic_cast<PCB_VIA*>( track );
813
814 if( !via )
815 continue;
816
817 VECTOR2I pos = via->GetPosition();
818 auto key = std::make_pair( pos.x, pos.y );
819 viaPositionCount[key]++;
820
821 if( via->GetViaType() == VIATYPE::BLIND )
822 blindCount++;
823 else if( via->GetViaType() == VIATYPE::THROUGH )
824 throughCount++;
825
826 if( via->GetWidth( F_Cu ) > maxExpectedViaWidth )
827 oversizedViaCount++;
828 }
829
830 // All vias in this 4-layer board span top-to-bottom, so none should be blind
831 BOOST_CHECK_MESSAGE( blindCount == 0,
832 "no vias should be blind; STANDARDVIA spans all copper layers; got "
833 << blindCount << " blind vias" );
834
835 BOOST_CHECK_MESSAGE( throughCount > 0, "should have through-hole vias" );
836
837 // No via should use the soldermask opening as its pad size
838 BOOST_CHECK_MESSAGE( oversizedViaCount == 0,
839 "via size should use copper pad, not soldermask opening; got "
840 << oversizedViaCount << " oversized vias" );
841
842 // No duplicate vias at the same position
843 int duplicateCount = 0;
844
845 for( const auto& [pos, count] : viaPositionCount )
846 {
847 if( count > 1 )
848 duplicateCount++;
849 }
850
851 BOOST_CHECK_MESSAGE( duplicateCount == 0,
852 "should not have duplicate vias at the same position; got "
853 << duplicateCount << " positions with duplicates" );
854
855 // STANDARDVIA has a layer 25 (front mask) entry but no layer 28 (back mask),
856 // so the back should be tented. JMPVIA has no mask layers at all.
857 // At minimum, every via should have the back tented.
858 int backTentedCount = 0;
859 int totalVias = 0;
860
861 for( PCB_TRACK* track : board->Tracks() )
862 {
863 PCB_VIA* via = dynamic_cast<PCB_VIA*>( track );
864
865 if( !via )
866 continue;
867
868 totalVias++;
869
870 if( via->GetBackTentingMode() == TENTING_MODE::TENTED )
871 backTentedCount++;
872 }
873
874 BOOST_CHECK_MESSAGE( backTentedCount == totalVias,
875 "vias without soldermask opening should be tented; "
876 << backTentedCount << " of " << totalVias << " back-tented" );
877}
878
879
888BOOST_AUTO_TEST_CASE( Importer_OvalDrillHits )
889{
890 PCB_IO_PADS plugin;
891
892 wxString filename = KI_TEST::GetPcbnewTestDataDir() + "plugins/pads/Importer.asc";
893
894 std::unique_ptr<BOARD> board( plugin.LoadBoard( filename, nullptr, nullptr, nullptr ) );
895
896 BOOST_REQUIRE( board != nullptr );
897
898 FOOTPRINT* u1 = nullptr;
899
900 for( FOOTPRINT* fp : board->Footprints() )
901 {
902 if( fp->GetReference() == "U1" )
903 {
904 u1 = fp;
905 break;
906 }
907 }
908
909 BOOST_REQUIRE_MESSAGE( u1, "U1 not found on board" );
910
911 // BASIC-to-nm: value * 25400 / 38100 = value * 2/3
912 // drill = 2250000 BASIC -> 1500000 nm (1.5mm)
913 // slot_length = 9000000 BASIC -> 6000000 nm (6.0mm)
914 const int expectedMajor = 6000000;
915 const int expectedMinor = 1500000;
916 const int tolerance = 10000; // 10um
917
918 int oblongCount = 0;
919
920 for( PAD* pad : u1->Pads() )
921 {
922 wxString padNum = pad->GetNumber();
923
924 if( padNum == "1" || padNum == "2" || padNum == "3"
925 || padNum == "4" || padNum == "5" )
926 {
927 BOOST_CHECK_MESSAGE( pad->GetDrillShape() == PAD_DRILL_SHAPE::OBLONG,
928 "pad " << padNum << " should have oblong drill" );
929
930 VECTOR2I drillSize = pad->GetDrillSize();
931 int major = std::max( drillSize.x, drillSize.y );
932 int minor = std::min( drillSize.x, drillSize.y );
933
934 BOOST_CHECK_MESSAGE( std::abs( major - expectedMajor ) < tolerance,
935 "pad " << padNum << " drill major axis " << major
936 << " should be ~" << expectedMajor );
937
938 BOOST_CHECK_MESSAGE( std::abs( minor - expectedMinor ) < tolerance,
939 "pad " << padNum << " drill minor axis " << minor
940 << " should be ~" << expectedMinor );
941
942 if( pad->GetDrillShape() == PAD_DRILL_SHAPE::OBLONG )
943 oblongCount++;
944 }
945 }
946
947 BOOST_CHECK_MESSAGE( oblongCount == 5,
948 "expected 5 pads with oblong drill, got " << oblongCount );
949}
950
951
960BOOST_AUTO_TEST_CASE( Peka_AlternateDecalDrill )
961{
962 PCB_IO_PADS plugin;
963
964 wxString filename = KI_TEST::GetPcbnewTestDataDir() + "plugins/pads/peka.asc";
965
966 std::unique_ptr<BOARD> board( plugin.LoadBoard( filename, nullptr, nullptr, nullptr ) );
967
968 BOOST_REQUIRE( board != nullptr );
969
970 FOOTPRINT* m4 = nullptr;
971
972 for( FOOTPRINT* fp : board->Footprints() )
973 {
974 if( fp->GetReference() == "M4" )
975 {
976 m4 = fp;
977 break;
978 }
979 }
980
981 BOOST_REQUIRE_MESSAGE( m4, "M4 not found on board" );
982
983 // MTHOLEAAAB: pad = 9525000 BASIC * 2/3 = 6350000 nm (250 mil)
984 // drill = 4762500 BASIC * 2/3 = 3175000 nm (125 mil)
985 const int expectedPadSize = 6350000;
986 const int expectedDrill = 3175000;
987 const int tolerance = 10000;
988
989 BOOST_REQUIRE_MESSAGE( m4->Pads().size() == 1,
990 "MTHOLEAAAB has 1 terminal; got " << m4->Pads().size() );
991
992 PAD* pad = m4->Pads().front();
993
994 BOOST_CHECK_MESSAGE( pad->GetDrillShape() == PAD_DRILL_SHAPE::CIRCLE,
995 "M4 pad 1 drill should be circular" );
996
997 VECTOR2I padSize = pad->GetSize( F_Cu );
998 int padDim = std::max( padSize.x, padSize.y );
999
1000 BOOST_CHECK_MESSAGE( std::abs( padDim - expectedPadSize ) < tolerance,
1001 "M4 pad size " << padDim << " should be ~" << expectedPadSize
1002 << " (250 mil)" );
1003
1004 VECTOR2I drillSize = pad->GetDrillSize();
1005 int drillDim = std::max( drillSize.x, drillSize.y );
1006
1007 BOOST_CHECK_MESSAGE( std::abs( drillDim - expectedDrill ) < tolerance,
1008 "M4 drill size " << drillDim << " should be ~" << expectedDrill
1009 << " (125 mil)" );
1010}
1011
1012
1021BOOST_AUTO_TEST_CASE( Peka_ZoneFillNoSelfIntersection )
1022{
1023 PCB_IO_PADS plugin;
1024
1025 wxString filename = KI_TEST::GetPcbnewTestDataDir() + "plugins/pads/peka.asc";
1026
1027 std::unique_ptr<BOARD> board( plugin.LoadBoard( filename, nullptr, nullptr, nullptr ) );
1028
1029 BOOST_REQUIRE( board != nullptr );
1030
1031 // Check whether any two non-adjacent segments in the polygon truly
1032 // cross. Clipper2 BooleanSubtract can produce bridge edges where
1033 // non-adjacent segments share endpoints at T-junctions. These are
1034 // valid geometry, not real crossings.
1035 auto hasTrueCrossing = []( const SHAPE_POLY_SET& aPoly, int aIdx ) -> bool
1036 {
1037 std::vector<SEG> segs;
1038
1039 for( auto it = aPoly.CIterateSegmentsWithHoles( aIdx ); it; it++ )
1040 segs.emplace_back( *it );
1041
1042 for( size_t i = 0; i < segs.size(); i++ )
1043 {
1044 for( size_t j = i + 1; j < segs.size(); j++ )
1045 {
1046 // Segments sharing any endpoint are either adjacent in the
1047 // contour or bridge junctions from Clipper2.
1048 if( segs[i].A == segs[j].A || segs[i].A == segs[j].B
1049 || segs[i].B == segs[j].A || segs[i].B == segs[j].B )
1050 {
1051 continue;
1052 }
1053
1054 if( segs[i].Collide( segs[j], 0 ) )
1055 return true;
1056 }
1057 }
1058
1059 return false;
1060 };
1061
1062 int zonesChecked = 0;
1063
1064 for( ZONE* zone : board->Zones() )
1065 {
1066 if( !zone->IsFilled() )
1067 continue;
1068
1069 for( PCB_LAYER_ID layer : zone->GetLayerSet().Seq() )
1070 {
1071 if( !zone->HasFilledPolysForLayer( layer ) )
1072 continue;
1073
1074 std::shared_ptr<SHAPE_POLY_SET> fill = zone->GetFilledPolysList( layer );
1075
1076 if( !fill || fill->OutlineCount() == 0 )
1077 continue;
1078
1079 zonesChecked++;
1080
1081 for( int pi = 0; pi < fill->OutlineCount(); pi++ )
1082 {
1083 if( fill->Outline( pi ).PointCount() < 3 )
1084 continue;
1085
1086 BOOST_CHECK_MESSAGE(
1087 !hasTrueCrossing( *fill, pi ),
1088 "zone \"" << zone->GetNetname() << "\" on "
1089 << board->GetLayerName( layer )
1090 << " outline " << pi
1091 << " has self-intersecting fill polygon" );
1092 }
1093 }
1094 }
1095
1096 BOOST_CHECK_MESSAGE( zonesChecked > 0, "no filled zones found to check" );
1097}
1098
1099
General utilities for PCB file IO for QA programs.
@ BS_ITEM_TYPE_COPPER
@ BS_ITEM_TYPE_DIELECTRIC
BOX2< VECTOR2I > BOX2I
Definition box2.h:922
Container for design settings for a BOARD object.
std::shared_ptr< NET_SETTINGS > m_NetSettings
int GetBoardThickness() const
The full thickness of the board including copper and masks.
BOARD_STACKUP & GetStackupDescriptor()
std::vector< VIA_DIMENSION > m_ViasDimensionsList
A base class for any item which can be embedded within the BOARD container class, and therefore insta...
Definition board_item.h:84
Manage one layer needed to make a physical board.
Manage layers needed to make a physical board.
const std::vector< BOARD_STACKUP_ITEM * > & GetList() const
constexpr void SetMaximum()
Definition box2.h:80
constexpr BOX2< Vec > & Merge(const BOX2< Vec > &aRect)
Modify the position and size of the rectangle in order to contain aRect.
Definition box2.h:658
constexpr bool Intersects(const BOX2< Vec > &aRect) const
Definition box2.h:311
std::deque< PAD * > & Pads()
Definition footprint.h:306
Handle the data for a net.
Definition netinfo.h:54
const wxString & GetNetname() const
Definition netinfo.h:112
std::shared_ptr< NETCLASS > GetDefaultNetclass()
Gets the default netclass for the project.
static constexpr PCB_LAYER_ID ALL_LAYERS
! Temporary layer identifier to identify code that is not padstack-aware
Definition padstack.h:177
Definition pad.h:55
For better understanding of the points that make a dimension:
BOARD * LoadBoard(const wxString &aFileName, BOARD *aAppendToMe, const std::map< std::string, UTF8 > *aProperties, PROJECT *aProject) override
Load information from some input file format that this PCB_IO implementation knows about into either ...
bool CanReadBoard(const wxString &aFileName) const override
Checks if this PCB_IO can read the specified board file.
int PointCount() const
Return the number of points (vertices) in this line chain.
Represent a set of closed polygons.
SHAPE_LINE_CHAIN & Outline(int aIndex)
Return the reference to aIndex-th outline in the set.
CONST_SEGMENT_ITERATOR CIterateSegmentsWithHoles() const
Return an iterator object, for the aOutline-th outline in the set (with holes).
int OutlineCount() const
Return the number of outlines in the set.
const SHAPE_LINE_CHAIN & COutline(int aIndex) const
Handle a list of polygons defining a copper zone.
Definition zone.h:73
SHAPE_POLY_SET * Outline()
Definition zone.h:340
@ RECTANGLE
Use RECTANGLE instead of RECT to avoid collision in a Windows header.
Definition eda_shape.h:46
bool IsCopperLayer(int aLayerId)
Test whether a layer is a copper layer.
Definition layer_ids.h:677
PCB_LAYER_ID
A quick note on layer IDs:
Definition layer_ids.h:60
@ Edge_Cuts
Definition layer_ids.h:112
@ Dwgs_User
Definition layer_ids.h:107
@ Cmts_User
Definition layer_ids.h:108
@ B_Cu
Definition layer_ids.h:65
@ F_SilkS
Definition layer_ids.h:100
@ UNDEFINED_LAYER
Definition layer_ids.h:61
@ F_Cu
Definition layer_ids.h:64
std::string GetPcbnewTestDataDir()
Utility which returns a path to the data directory where the test board files are stored.
EDA_ANGLE abs(const EDA_ANGLE &aAngle)
Definition eda_angle.h:400
@ THROUGH
Definition pcb_track.h:68
static bool Collide(const SHAPE_CIRCLE &aA, const SHAPE_CIRCLE &aB, int aClearance, int *aActual, VECTOR2I *aLocation, VECTOR2I *aMTV)
BOOST_AUTO_TEST_CASE(HorizontalAlignment)
BOOST_AUTO_TEST_SUITE(CadstarPartParser)
BOOST_REQUIRE(intersection.has_value()==c.ExpectedIntersection.has_value())
BOOST_AUTO_TEST_SUITE_END()
static void RunStructuralChecks(const PADS_BOARD_INFO &aBoard)
Run structural integrity checks on a successfully loaded board.
static wxString GetBoardPath(const PADS_BOARD_INFO &aBoard)
static const PADS_BOARD_INFO PADS_BOARDS[]
static std::unique_ptr< BOARD > LoadAndVerify(const PADS_BOARD_INFO &aBoard)
Verify that the PADS file is recognized and loads without crashing.
BOOST_AUTO_TEST_CASE(ImportClaySight_MK1)
VECTOR2I end
BOOST_CHECK_EQUAL(result, "25.4")
@ PCB_VIA_T
class PCB_VIA, a via (like a track segment on a copper layer)
Definition typeinfo.h:97
@ PCB_TEXT_T
class PCB_TEXT, text on a layer
Definition typeinfo.h:92
@ PCB_ARC_T
class PCB_ARC, an arc track segment on a copper layer
Definition typeinfo.h:98
@ PCB_TRACE_T
class PCB_TRACK, a track segment (segment on a copper layer)
Definition typeinfo.h:96
VECTOR2< int32_t > VECTOR2I
Definition vector2d.h:695