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, see <https://www.gnu.org/licenses/>.
18 */
19
23
26#include <layer_ids.h>
27#include <padstack.h>
28#include <board.h>
29#include <pcb_text.h>
30#include <pcb_shape.h>
31#include <pcb_field.h>
32#include <pad.h>
33#include <pcb_track.h>
34#include <footprint.h>
35#include <zone.h>
37#include <pcb_dimension.h>
40#include <map>
41#include <set>
42
43
45{
46 std::string dir;
47 std::string file;
48};
49
50
51static const PADS_BOARD_INFO PADS_BOARDS[] = {
52 { "ClaySight_MK1", "ClaySight_MK1.asc" }, // V10.0 BASIC
53 { "TMS1mmX19", "TMS1mmX19.asc" }, // V9.5 BASIC MILS
54 { "MC4_PLUS_CSHAPE", "MC4_PLUS_CSHAPE.asc" }, // V9.5 MILS
55 { "MC2_PLUS_REV1", "MC2_PLUS_REV1.asc" }, // V9.4 METRIC
56 { "Ems4_Rev2", "Ems4_Rev2.asc" }, // V9.4 MILS
57 { "LCORE_4", "LCORE_4.asc" }, // V9.0 METRIC
58 { "LCORE_2", "LCORE_2.asc" }, // V2005.0 METRIC
59 { "Dexter_MotorCtrl", "Dexter_MotorCtrl.asc" }, // V2007.0 MILS
60 { "MAIS_FC", "MAIS_FC.asc" }, // V5.0 METRIC
61 { "ClaySight_MK2", "ClaySight_MK2.asc" }, // V10.0 BASIC (copper lines)
62};
63
64
65static wxString GetBoardPath( const PADS_BOARD_INFO& aBoard )
66{
67 return KI_TEST::GetPcbnewTestDataDir() + "plugins/pads/" + aBoard.dir + "/" + aBoard.file;
68}
69
70
74static std::unique_ptr<BOARD> LoadAndVerify( const PADS_BOARD_INFO& aBoard )
75{
76 PCB_IO_PADS plugin;
77
78 wxString filename = GetBoardPath( aBoard );
79
80 BOOST_CHECK_MESSAGE( plugin.CanReadBoard( filename ),
81 aBoard.dir << " should be a readable PADS file" );
82
83 std::unique_ptr<BOARD> board;
84
85 try
86 {
87 board.reset( plugin.LoadBoard( filename, nullptr, nullptr, nullptr ) );
88 }
89 catch( const std::exception& e )
90 {
91 BOOST_WARN_MESSAGE( false,
92 aBoard.dir << " threw exception during load: " << e.what() );
93 return board;
94 }
95
96 BOOST_REQUIRE_MESSAGE( board != nullptr, aBoard.dir << " failed to load" );
97 BOOST_CHECK_MESSAGE( board->Footprints().size() > 0,
98 aBoard.dir << " should have footprints" );
99
100 return board;
101}
102
103
111static void RunStructuralChecks( const PADS_BOARD_INFO& aBoard )
112{
113 std::unique_ptr<BOARD> board = LoadAndVerify( aBoard );
114
115 if( !board )
116 return;
117
118 BOOST_WARN_MESSAGE( board->Tracks().size() > 0,
119 aBoard.dir << " has no tracks (parser may not support this format version)" );
120
121 if( board->Tracks().size() > 0 && board->Footprints().size() > 0 )
122 {
123 BOX2I fpBbox;
124 fpBbox.SetMaximum();
125
126 for( FOOTPRINT* fp : board->Footprints() )
127 fpBbox.Merge( fp->GetBoundingBox() );
128
129 BOX2I trackBbox;
130 trackBbox.SetMaximum();
131
132 for( PCB_TRACK* trk : board->Tracks() )
133 trackBbox.Merge( trk->GetBoundingBox() );
134
135 BOOST_CHECK_MESSAGE( fpBbox.Intersects( trackBbox ),
136 aBoard.dir << " footprint and track bounding boxes should overlap" );
137 }
138
139 // No duplicate through-hole vias at the same position
140 std::set<std::pair<int, int>> viaPositions;
141 bool hasDuplicate = false;
142
143 for( PCB_TRACK* trk : board->Tracks() )
144 {
145 PCB_VIA* via = dynamic_cast<PCB_VIA*>( trk );
146
147 if( !via || via->GetViaType() != VIATYPE::THROUGH )
148 continue;
149
150 auto key = std::make_pair( via->GetPosition().x, via->GetPosition().y );
151
152 if( viaPositions.count( key ) )
153 {
154 hasDuplicate = true;
155 break;
156 }
157
158 viaPositions.insert( key );
159 }
160
161 BOOST_CHECK_MESSAGE( !hasDuplicate,
162 aBoard.dir << " should have no duplicate through-hole vias" );
163
164 // All imported tracks must be on copper layers
165 for( PCB_TRACK* trk : board->Tracks() )
166 {
167 if( trk->Type() == PCB_TRACE_T || trk->Type() == PCB_ARC_T )
168 {
169 BOOST_CHECK_MESSAGE( IsCopperLayer( trk->GetLayer() ),
170 aBoard.dir << " track on non-copper layer " << trk->GetLayer() );
171 }
172 }
173
174 // Pad size check uses WARN since some boards have pads the parser doesn't handle yet
175 for( FOOTPRINT* fp : board->Footprints() )
176 {
177 for( PAD* pad : fp->Pads() )
178 {
179 BOOST_WARN_MESSAGE( pad->GetSize( PADSTACK::ALL_LAYERS ).x > 0
180 && pad->GetSize( PADSTACK::ALL_LAYERS ).y > 0,
181 aBoard.dir << " " << fp->GetReference() << " pad has zero size" );
182 }
183 }
184
185 // Every zone outline must have non-empty contours
186 for( ZONE* zone : board->Zones() )
187 {
188 const SHAPE_POLY_SET* outline = zone->Outline();
189 BOOST_REQUIRE_MESSAGE( outline != nullptr,
190 aBoard.dir << " zone has null outline" );
191
192 for( int ii = 0; ii < outline->OutlineCount(); ++ii )
193 {
194 BOOST_CHECK_MESSAGE( outline->COutline( ii ).PointCount() >= 3,
195 aBoard.dir << " zone outline " << ii << " has "
196 << outline->COutline( ii ).PointCount() << " points" );
197 }
198 }
199}
200
201
202BOOST_AUTO_TEST_SUITE( PADS_IMPORT )
203
204
205BOOST_AUTO_TEST_CASE( ImportClaySight_MK1 )
206{
208}
209
210
218BOOST_AUTO_TEST_CASE( ClaySight_MK1_ElementCounts )
219{
220 std::unique_ptr<BOARD> board = LoadAndVerify( PADS_BOARDS[0] );
221
222 BOOST_REQUIRE( board != nullptr );
223
224 // Footprints: 36 parts in the *PART* section
225 BOOST_CHECK_EQUAL( board->Footprints().size(), 36 );
226
227 // Total pads across all footprints
228 int totalPads = 0;
229
230 for( FOOTPRINT* fp : board->Footprints() )
231 totalPads += fp->Pads().size();
232
233 BOOST_CHECK_EQUAL( totalPads, 140 );
234
235 // Tracks: routed signal segments from 32 *SIGNAL* sections
236 int traceCount = 0;
237 int viaCount = 0;
238
239 for( PCB_TRACK* trk : board->Tracks() )
240 {
241 if( trk->Type() == PCB_TRACE_T || trk->Type() == PCB_ARC_T )
242 traceCount++;
243 else if( trk->Type() == PCB_VIA_T )
244 viaCount++;
245 }
246
247 BOOST_CHECK_EQUAL( traceCount, 247 );
248
249 // No vias on this 2-layer board (empty *VIA* section)
250 BOOST_CHECK_EQUAL( viaCount, 0 );
251
252 // No zones (empty *POUR* section)
253 BOOST_CHECK_EQUAL( board->Zones().size(), 0 );
254
255 // Board outline on Edge.Cuts
256 int edgeCutsCount = 0;
257
258 for( BOARD_ITEM* item : board->Drawings() )
259 {
260 if( PCB_SHAPE* shape = dynamic_cast<PCB_SHAPE*>( item ) )
261 {
262 if( shape->GetLayer() == Edge_Cuts )
263 edgeCutsCount++;
264 }
265 }
266
267 BOOST_CHECK_EQUAL( edgeCutsCount, 6 );
268
269 // Free text items from the *TEXT* section
270 int textCount = 0;
271
272 for( BOARD_ITEM* item : board->Drawings() )
273 {
274 if( dynamic_cast<PCB_TEXT*>( item ) )
275 textCount++;
276 }
277
278 BOOST_CHECK_EQUAL( textCount, 17 );
279
280 // Net assignments: tracks and pads should reference named nets
281 std::set<wxString> trackNets;
282
283 for( PCB_TRACK* trk : board->Tracks() )
284 {
285 NETINFO_ITEM* net = trk->GetNet();
286
287 if( net && !net->GetNetname().IsEmpty() )
288 trackNets.insert( net->GetNetname() );
289 }
290
291 BOOST_CHECK_EQUAL( trackNets.size(), 32 );
292
293 // All traces on copper layers (F.Cu or B.Cu for this 2-layer board)
294 for( PCB_TRACK* trk : board->Tracks() )
295 {
296 if( trk->Type() == PCB_TRACE_T )
297 {
298 PCB_LAYER_ID layer = trk->GetLayer();
299 BOOST_CHECK_MESSAGE( layer == F_Cu || layer == B_Cu,
300 "trace on unexpected layer " << layer );
301 }
302 }
303}
304
305
306BOOST_AUTO_TEST_CASE( ImportTMS1mmX19 )
307{
309}
310
311
312BOOST_AUTO_TEST_CASE( ImportMC4_PLUS_CSHAPE )
313{
315}
316
317
318BOOST_AUTO_TEST_CASE( ImportMC2_PLUS_REV1 )
319{
321}
322
323
324BOOST_AUTO_TEST_CASE( ImportEms4_Rev2 )
325{
327}
328
329
330BOOST_AUTO_TEST_CASE( ImportLCORE_4 )
331{
333}
334
335
336BOOST_AUTO_TEST_CASE( ImportLCORE_2 )
337{
339}
340
341
342BOOST_AUTO_TEST_CASE( ImportDexter_MotorCtrl )
343{
345}
346
347
348BOOST_AUTO_TEST_CASE( ImportMAIS_FC )
349{
351}
352
353
354BOOST_AUTO_TEST_CASE( ImportNonCopperTrackSkipped )
355{
356 // Test that tracks on non-copper layers are skipped without crashing
357 PCB_IO_PADS plugin;
358
359 wxString filename = KI_TEST::GetPcbnewTestDataDir() + "plugins/pads/synthetic_noncopper_track.asc";
360
361 std::unique_ptr<BOARD> board( plugin.LoadBoard( filename, nullptr, nullptr, nullptr ) );
362
363 BOOST_REQUIRE( board != nullptr );
364
365 int track_count = 0;
366
367 for( PCB_TRACK* track : board->Tracks() )
368 {
369 if( track->Type() == PCB_TRACE_T || track->Type() == PCB_ARC_T )
370 {
371 track_count++;
372 BOOST_CHECK( IsCopperLayer( track->GetLayer() ) );
373 }
374 }
375
376 BOOST_CHECK( track_count > 0 );
377}
378
379
380BOOST_AUTO_TEST_CASE( ImportTextOnUnmappedLayer )
381{
382 // Test that text on unmapped layers is assigned to Comments layer without crashing
383 PCB_IO_PADS plugin;
384
385 wxString filename = KI_TEST::GetPcbnewTestDataDir() + "plugins/pads/synthetic_unmapped_text_layer.asc";
386
387 std::unique_ptr<BOARD> board( plugin.LoadBoard( filename, nullptr, nullptr, nullptr ) );
388
389 BOOST_REQUIRE( board != nullptr );
390
391 int silkscreen_count = 0;
392 int comments_count = 0;
393 int copper_count = 0;
394
395 for( BOARD_ITEM* item : board->Drawings() )
396 {
397 if( PCB_TEXT* text = dynamic_cast<PCB_TEXT*>( item ) )
398 {
399 PCB_LAYER_ID layer = text->GetLayer();
400
401 BOOST_CHECK( layer != UNDEFINED_LAYER );
402
403 if( layer == F_SilkS )
404 silkscreen_count++;
405 else if( layer == Cmts_User )
406 comments_count++;
407 else if( layer == F_Cu )
408 copper_count++;
409 }
410 }
411
412 BOOST_CHECK_EQUAL( silkscreen_count, 1 );
413 BOOST_CHECK_EQUAL( comments_count, 1 );
414 BOOST_CHECK_EQUAL( copper_count, 1 );
415}
416
417
418BOOST_AUTO_TEST_CASE( ImportClaySight_MK2 )
419{
421}
422
423
431BOOST_AUTO_TEST_CASE( ClaySight_MK2_ElementCounts )
432{
433 std::unique_ptr<BOARD> board = LoadAndVerify( PADS_BOARDS[9] );
434
435 BOOST_REQUIRE( board != nullptr );
436
437 // 10 parts: U1 (RPi Pico), SU1-SU8 (TO-92), U2 (ULN2003A)
438 BOOST_CHECK_EQUAL( board->Footprints().size(), 10 );
439
440 // U1=40 pads, SU1-SU8=3 each (24), U2=16 pads = 80 total
441 int totalPads = 0;
442
443 for( FOOTPRINT* fp : board->Footprints() )
444 totalPads += fp->Pads().size();
445
446 BOOST_CHECK_EQUAL( totalPads, 80 );
447
448 int traceCount = 0;
449 int viaCount = 0;
450
451 for( PCB_TRACK* trk : board->Tracks() )
452 {
453 if( trk->Type() == PCB_TRACE_T || trk->Type() == PCB_ARC_T )
454 traceCount++;
455 else if( trk->Type() == PCB_VIA_T )
456 viaCount++;
457 }
458
459 // 138 track segments from 2 *SIGNAL* route sections only
460 BOOST_CHECK_EQUAL( traceCount, 138 );
461 BOOST_CHECK_EQUAL( viaCount, 0 );
462
463 // 2 nets from *SIGNAL* routes: N$12982 and N$12975
464 std::set<wxString> trackNets;
465
466 for( PCB_TRACK* trk : board->Tracks() )
467 {
468 NETINFO_ITEM* net = trk->GetNet();
469
470 if( net && !net->GetNetname().IsEmpty() )
471 trackNets.insert( net->GetNetname() );
472 }
473
474 BOOST_CHECK_EQUAL( trackNets.size(), 2 );
475
476 // All traces on copper layers
477 for( PCB_TRACK* trk : board->Tracks() )
478 {
479 if( trk->Type() == PCB_TRACE_T )
480 {
481 BOOST_CHECK_MESSAGE( IsCopperLayer( trk->GetLayer() ),
482 "trace on non-copper layer " << trk->GetLayer() );
483 }
484 }
485
486 // 72 COPPER items on layer 126 become silkscreen graphics. 64 of these
487 // (16 groups of 4 axis-aligned segments) are detected as rectangles. The
488 // remaining 8 are individual segments. Plus 44 segments from LINES items.
489 int silkCount = 0;
490 int rectCount = 0;
491
492 for( BOARD_ITEM* item : board->Drawings() )
493 {
494 if( PCB_SHAPE* shape = dynamic_cast<PCB_SHAPE*>( item ) )
495 {
496 if( shape->GetLayer() == F_SilkS )
497 {
498 silkCount++;
499
500 if( shape->GetShape() == SHAPE_T::RECTANGLE )
501 rectCount++;
502 }
503 }
504 }
505
506 BOOST_CHECK_EQUAL( silkCount, 68 );
507 BOOST_CHECK_EQUAL( rectCount, 16 );
508
509 // Default via size from JMPVIA_1 definition (drill=457505, size=915010 BASIC)
510 const BOARD_DESIGN_SETTINGS& bds = board->GetDesignSettings();
511 std::shared_ptr<NETCLASS> defaultNc = bds.m_NetSettings->GetDefaultNetclass();
512 BOOST_CHECK( defaultNc->GetViaDiameter() > 0 );
513 BOOST_CHECK( defaultNc->GetViaDrill() > 0 );
514 BOOST_CHECK( defaultNc->GetViaDiameter() > defaultNc->GetViaDrill() );
516
517 // Copper-to-edge clearance from OUTLINE_TO_* rules (227990 BASIC)
518 BOOST_CHECK( bds.m_CopperEdgeClearance > 0 );
519
520 // Board outline on Edge.Cuts (rectangular outline = 4 segments)
521 int edgeCutsCount = 0;
522
523 for( BOARD_ITEM* item : board->Drawings() )
524 {
525 if( PCB_SHAPE* shape = dynamic_cast<PCB_SHAPE*>( item ) )
526 {
527 if( shape->GetLayer() == Edge_Cuts )
528 edgeCutsCount++;
529 }
530 }
531
532 BOOST_CHECK_EQUAL( edgeCutsCount, 4 );
533
534 // 1 board-level text item on silkscreen with multi-line content
535 int textCount = 0;
536 wxString textContent;
537
538 for( BOARD_ITEM* item : board->Drawings() )
539 {
540 if( item->Type() == PCB_TEXT_T )
541 {
542 PCB_TEXT* text = static_cast<PCB_TEXT*>( item );
543 textContent = text->GetText();
544 textCount++;
545 }
546 }
547
548 BOOST_CHECK_EQUAL( textCount, 1 );
549
550 // EasyEDA exports encode newlines as underscores in text content.
551 // The parser converts them back to newlines for proper multi-line display.
552 BOOST_CHECK( textContent.Contains( wxT( "\n" ) ) );
553 BOOST_CHECK( !textContent.Contains( wxT( "_" ) ) );
554 BOOST_CHECK( textContent.Contains( wxT( "CLAYSIGHT MCU V.2" ) ) );
555 BOOST_CHECK( textContent.Contains( wxT( "The Ohio State University" ) ) );
556}
557
558
565BOOST_AUTO_TEST_CASE( MAIS_FC_Stackup )
566{
567 std::unique_ptr<BOARD> board = LoadAndVerify( PADS_BOARDS[8] );
568
569 BOOST_REQUIRE( board != nullptr );
570
571 const BOARD_DESIGN_SETTINGS& bds = board->GetDesignSettings();
572 BOOST_CHECK( bds.m_HasStackup );
573
574 const BOARD_STACKUP& stackup = bds.GetStackupDescriptor();
575
576 bool foundCopperThickness = false;
577 bool foundDielectric = false;
578
579 for( BOARD_STACKUP_ITEM* item : stackup.GetList() )
580 {
581 if( item->GetType() == BOARD_STACKUP_ITEM_TYPE::BS_ITEM_TYPE_COPPER )
582 {
583 if( item->GetThickness() > 0 )
584 foundCopperThickness = true;
585 }
586 else if( item->GetType() == BOARD_STACKUP_ITEM_TYPE::BS_ITEM_TYPE_DIELECTRIC )
587 {
588 if( item->GetEpsilonR() > 3.0 )
589 foundDielectric = true;
590 }
591 }
592
593 BOOST_CHECK_MESSAGE( foundCopperThickness, "stackup should have non-zero copper thickness" );
594 BOOST_CHECK_MESSAGE( foundDielectric, "stackup should have dielectric constant > 3.0" );
595 BOOST_CHECK( bds.GetBoardThickness() > 0 );
596}
597
598
607BOOST_AUTO_TEST_CASE( ImportDegeneratePourSkipped )
608{
609 PCB_IO_PADS plugin;
610
611 wxString filename = KI_TEST::GetPcbnewTestDataDir()
612 + "plugins/pads/synthetic_degenerate_pour.asc";
613
614 std::unique_ptr<BOARD> board( plugin.LoadBoard( filename, nullptr, nullptr, nullptr ) );
615
616 BOOST_REQUIRE( board != nullptr );
617
618 // Only the valid 4-point pour should produce a zone.
619 // The PADTHERM (2 SEG pieces with 2 points each) and
620 // VIATHERM (1 SEG piece with 2 points) must be skipped.
621 BOOST_CHECK_EQUAL( board->Zones().size(), 1 );
622
623 // The single valid zone must have a non-degenerate outline
624 if( board->Zones().size() == 1 )
625 {
626 ZONE* zone = board->Zones()[0];
627 BOOST_CHECK( zone->Outline()->OutlineCount() == 1 );
628 BOOST_CHECK( zone->Outline()->COutline( 0 ).PointCount() >= 3 );
629 }
630}
631
632
641BOOST_AUTO_TEST_CASE( ImportFilledCopperSingleOutline )
642{
643 PCB_IO_PADS plugin;
644
645 wxString filename = KI_TEST::GetPcbnewTestDataDir()
646 + "plugins/pads/synthetic_filled_copper.asc";
647
648 std::unique_ptr<BOARD> board( plugin.LoadBoard( filename, nullptr, nullptr, nullptr ) );
649
650 BOOST_REQUIRE( board != nullptr );
651 BOOST_REQUIRE_EQUAL( board->Zones().size(), 1 );
652
653 ZONE* zone = board->Zones()[0];
654 BOOST_CHECK_EQUAL( zone->Outline()->OutlineCount(), 1 );
655 BOOST_CHECK( zone->Outline()->COutline( 0 ).PointCount() >= 3 );
656}
657
658
668BOOST_AUTO_TEST_CASE( Importer_SpecificFixes )
669{
670 PCB_IO_PADS plugin;
671
672 wxString filename = KI_TEST::GetPcbnewTestDataDir() + "plugins/pads/Importer.asc";
673
674 std::unique_ptr<BOARD> board( plugin.LoadBoard( filename, nullptr, nullptr, nullptr ) );
675
676 BOOST_REQUIRE( board != nullptr );
677
678 // Bug 1: Graphics on layers 18/19/20 must be imported.
679 // The LAYERSTACK_6L_35U block has 556 pieces mostly on layer 18, plus layer 20.
680 // The DRW59706864 block has a BOARD outline on layer 0 (Edge.Cuts).
681 // Count graphics on Dwgs_User and Cmts_User to verify documentation layers imported.
682 int dwgsUserCount = 0;
683
684 for( BOARD_ITEM* item : board->Drawings() )
685 {
686 if( PCB_SHAPE* shape = dynamic_cast<PCB_SHAPE*>( item ) )
687 {
688 if( shape->GetLayer() == Dwgs_User )
689 dwgsUserCount++;
690 }
691 }
692
693 BOOST_CHECK_MESSAGE( dwgsUserCount > 0,
694 "graphics on PADS layer 18 (drill drawing) should map to Dwgs_User" );
695
696 // Bug 3: U1 pads should be oval (OF shape), not circular (RT thermal).
697 // U1 has part type DIO_RECT_3PH_1600V_100A with DIOB_D100JHT160V decal.
698 // PAD 0 stack has OF 0.000 11550000 on layer -2 (4.8mm height, 11.55mm width).
699 FOOTPRINT* u1 = nullptr;
700
701 for( FOOTPRINT* fp : board->Footprints() )
702 {
703 if( fp->GetReference() == wxT( "U1" ) )
704 {
705 u1 = fp;
706 break;
707 }
708 }
709
710 BOOST_REQUIRE_MESSAGE( u1 != nullptr, "U1 footprint should exist" );
711
712 bool foundOvalPad = false;
713
714 for( PAD* pad : u1->Pads() )
715 {
716 VECTOR2I padSize = pad->GetSize( PADSTACK::ALL_LAYERS );
717
718 if( pad->GetShape( PADSTACK::ALL_LAYERS ) == PAD_SHAPE::OVAL && padSize.x != padSize.y )
719 {
720 foundOvalPad = true;
721 break;
722 }
723 }
724
725 BOOST_CHECK_MESSAGE( foundOvalPad, "U1 should have oval pads (OF shape, not RT thermal)" );
726
727 // Bug 2: Copper pours should not have duplicates from HATOUT records.
728 // The file has 13 POUROUT records. HATOUT records should become fills, not zones.
729 // Count zones that are NOT rule areas (actual copper pours).
730 int pourZoneCount = 0;
731 int filledZoneCount = 0;
732
733 for( ZONE* zone : board->Zones() )
734 {
735 if( !zone->GetIsRuleArea() )
736 {
737 pourZoneCount++;
738
739 if( zone->IsFilled() )
740 filledZoneCount++;
741 }
742 }
743
744 BOOST_CHECK_MESSAGE( pourZoneCount <= 13,
745 "should not have duplicate zones from HATOUT; got " << pourZoneCount );
746
747 BOOST_CHECK_MESSAGE( filledZoneCount > 0, "HATOUT records should produce filled zones" );
748
749 // Bug 4: Dimension line should not be skewed.
750 // DIM92271615 measures 110.00mm horizontal. Start=(0,9000000) end=(165000000,1500000).
751 // After fix, both endpoints should have the same Y for horizontal measurement.
752 int dimCount = 0;
753
754 for( BOARD_ITEM* item : board->Drawings() )
755 {
756 if( PCB_DIM_ALIGNED* dim = dynamic_cast<PCB_DIM_ALIGNED*>( item ) )
757 {
758 dimCount++;
759
760 VECTOR2I start = dim->GetStart();
761 VECTOR2I end = dim->GetEnd();
762
763 BOOST_CHECK_MESSAGE( start.y == end.y,
764 "horizontal dimension endpoints should have equal Y coordinates; "
765 "start.y=" << start.y << " end.y=" << end.y );
766 }
767 }
768
769 BOOST_CHECK_MESSAGE( dimCount > 0, "should have at least one dimension" );
770}
771
772
785BOOST_AUTO_TEST_CASE( Peka_ViaImport )
786{
787 PCB_IO_PADS plugin;
788
789 wxString filename = KI_TEST::GetPcbnewTestDataDir() + "plugins/pads/peka.asc";
790
791 std::unique_ptr<BOARD> board( plugin.LoadBoard( filename, nullptr, nullptr, nullptr ) );
792
793 BOOST_REQUIRE( board != nullptr );
794
795 // Collect all vias and check for duplicates
796 std::map<std::pair<int, int>, int> viaPositionCount;
797 int blindCount = 0;
798 int throughCount = 0;
799
800 // Copper pad size from the STANDARDVIA definition is 1371600 BASIC units.
801 // At BASIC_TO_NM = 25400/38100, that converts to ~914,400 nm (36 mil).
802 // The soldermask opening is 2400300 BASIC = ~1,600,200 nm (63 mil).
803 // Via size must use the copper value, not the mask opening.
804 const int maxExpectedViaWidth = 1200000; // 1.2mm, well above 36 mil copper pad
805
806 int oversizedViaCount = 0;
807
808 for( PCB_TRACK* track : board->Tracks() )
809 {
810 PCB_VIA* via = dynamic_cast<PCB_VIA*>( track );
811
812 if( !via )
813 continue;
814
815 VECTOR2I pos = via->GetPosition();
816 auto key = std::make_pair( pos.x, pos.y );
817 viaPositionCount[key]++;
818
819 if( via->GetViaType() == VIATYPE::BLIND )
820 blindCount++;
821 else if( via->GetViaType() == VIATYPE::THROUGH )
822 throughCount++;
823
824 if( via->GetWidth( F_Cu ) > maxExpectedViaWidth )
825 oversizedViaCount++;
826 }
827
828 // All vias in this 4-layer board span top-to-bottom, so none should be blind
829 BOOST_CHECK_MESSAGE( blindCount == 0,
830 "no vias should be blind; STANDARDVIA spans all copper layers; got "
831 << blindCount << " blind vias" );
832
833 BOOST_CHECK_MESSAGE( throughCount > 0, "should have through-hole vias" );
834
835 // No via should use the soldermask opening as its pad size
836 BOOST_CHECK_MESSAGE( oversizedViaCount == 0,
837 "via size should use copper pad, not soldermask opening; got "
838 << oversizedViaCount << " oversized vias" );
839
840 // No duplicate vias at the same position
841 int duplicateCount = 0;
842
843 for( const auto& [pos, count] : viaPositionCount )
844 {
845 if( count > 1 )
846 duplicateCount++;
847 }
848
849 BOOST_CHECK_MESSAGE( duplicateCount == 0,
850 "should not have duplicate vias at the same position; got "
851 << duplicateCount << " positions with duplicates" );
852
853 // STANDARDVIA has a layer 25 (front mask) entry but no layer 28 (back mask),
854 // so the back should be tented. JMPVIA has no mask layers at all.
855 // At minimum, every via should have the back tented.
856 int backTentedCount = 0;
857 int totalVias = 0;
858
859 for( PCB_TRACK* track : board->Tracks() )
860 {
861 PCB_VIA* via = dynamic_cast<PCB_VIA*>( track );
862
863 if( !via )
864 continue;
865
866 totalVias++;
867
868 if( via->GetBackTentingMode() == TENTING_MODE::TENTED )
869 backTentedCount++;
870 }
871
872 BOOST_CHECK_MESSAGE( backTentedCount == totalVias,
873 "vias without soldermask opening should be tented; "
874 << backTentedCount << " of " << totalVias << " back-tented" );
875}
876
877
886BOOST_AUTO_TEST_CASE( Importer_OvalDrillHits )
887{
888 PCB_IO_PADS plugin;
889
890 wxString filename = KI_TEST::GetPcbnewTestDataDir() + "plugins/pads/Importer.asc";
891
892 std::unique_ptr<BOARD> board( plugin.LoadBoard( filename, nullptr, nullptr, nullptr ) );
893
894 BOOST_REQUIRE( board != nullptr );
895
896 FOOTPRINT* u1 = nullptr;
897
898 for( FOOTPRINT* fp : board->Footprints() )
899 {
900 if( fp->GetReference() == "U1" )
901 {
902 u1 = fp;
903 break;
904 }
905 }
906
907 BOOST_REQUIRE_MESSAGE( u1, "U1 not found on board" );
908
909 // BASIC-to-nm: value * 25400 / 38100 = value * 2/3
910 // drill = 2250000 BASIC -> 1500000 nm (1.5mm)
911 // slot_length = 9000000 BASIC -> 6000000 nm (6.0mm)
912 const int expectedMajor = 6000000;
913 const int expectedMinor = 1500000;
914 const int tolerance = 10000; // 10um
915
916 int oblongCount = 0;
917
918 for( PAD* pad : u1->Pads() )
919 {
920 wxString padNum = pad->GetNumber();
921
922 if( padNum == "1" || padNum == "2" || padNum == "3"
923 || padNum == "4" || padNum == "5" )
924 {
926 "pad " << padNum << " should have oblong drill" );
927
928 VECTOR2I drillSize = pad->GetDrillSize();
929 int major = std::max( drillSize.x, drillSize.y );
930 int minor = std::min( drillSize.x, drillSize.y );
931
932 BOOST_CHECK_MESSAGE( std::abs( major - expectedMajor ) < tolerance,
933 "pad " << padNum << " drill major axis " << major
934 << " should be ~" << expectedMajor );
935
936 BOOST_CHECK_MESSAGE( std::abs( minor - expectedMinor ) < tolerance,
937 "pad " << padNum << " drill minor axis " << minor
938 << " should be ~" << expectedMinor );
939
940 if( pad->GetDrillShape() == PAD_DRILL_SHAPE::OBLONG )
941 oblongCount++;
942 }
943 }
944
945 BOOST_CHECK_MESSAGE( oblongCount == 5,
946 "expected 5 pads with oblong drill, got " << oblongCount );
947}
948
949
958BOOST_AUTO_TEST_CASE( Peka_AlternateDecalDrill )
959{
960 PCB_IO_PADS plugin;
961
962 wxString filename = KI_TEST::GetPcbnewTestDataDir() + "plugins/pads/peka.asc";
963
964 std::unique_ptr<BOARD> board( plugin.LoadBoard( filename, nullptr, nullptr, nullptr ) );
965
966 BOOST_REQUIRE( board != nullptr );
967
968 FOOTPRINT* m4 = nullptr;
969
970 for( FOOTPRINT* fp : board->Footprints() )
971 {
972 if( fp->GetReference() == "M4" )
973 {
974 m4 = fp;
975 break;
976 }
977 }
978
979 BOOST_REQUIRE_MESSAGE( m4, "M4 not found on board" );
980
981 // MTHOLEAAAB: pad = 9525000 BASIC * 2/3 = 6350000 nm (250 mil)
982 // drill = 4762500 BASIC * 2/3 = 3175000 nm (125 mil)
983 const int expectedPadSize = 6350000;
984 const int expectedDrill = 3175000;
985 const int tolerance = 10000;
986
987 BOOST_REQUIRE_MESSAGE( m4->Pads().size() == 1,
988 "MTHOLEAAAB has 1 terminal; got " << m4->Pads().size() );
989
990 PAD* pad = m4->Pads().front();
991
993 "M4 pad 1 drill should be circular" );
994
995 VECTOR2I padSize = pad->GetSize( F_Cu );
996 int padDim = std::max( padSize.x, padSize.y );
997
998 BOOST_CHECK_MESSAGE( std::abs( padDim - expectedPadSize ) < tolerance,
999 "M4 pad size " << padDim << " should be ~" << expectedPadSize
1000 << " (250 mil)" );
1001
1002 VECTOR2I drillSize = pad->GetDrillSize();
1003 int drillDim = std::max( drillSize.x, drillSize.y );
1004
1005 BOOST_CHECK_MESSAGE( std::abs( drillDim - expectedDrill ) < tolerance,
1006 "M4 drill size " << drillDim << " should be ~" << expectedDrill
1007 << " (125 mil)" );
1008}
1009
1010
1019BOOST_AUTO_TEST_CASE( Peka_ZoneFillNoSelfIntersection )
1020{
1021 PCB_IO_PADS plugin;
1022
1023 wxString filename = KI_TEST::GetPcbnewTestDataDir() + "plugins/pads/peka.asc";
1024
1025 std::unique_ptr<BOARD> board( plugin.LoadBoard( filename, nullptr, nullptr, nullptr ) );
1026
1027 BOOST_REQUIRE( board != nullptr );
1028
1029 // Check whether any two non-adjacent segments in the polygon truly
1030 // cross. Clipper2 BooleanSubtract can produce bridge edges where
1031 // non-adjacent segments share endpoints at T-junctions. These are
1032 // valid geometry, not real crossings.
1033 auto hasTrueCrossing = []( const SHAPE_POLY_SET& aPoly, int aIdx ) -> bool
1034 {
1035 std::vector<SEG> segs;
1036
1037 for( auto it = aPoly.CIterateSegmentsWithHoles( aIdx ); it; it++ )
1038 segs.emplace_back( *it );
1039
1040 for( size_t i = 0; i < segs.size(); i++ )
1041 {
1042 for( size_t j = i + 1; j < segs.size(); j++ )
1043 {
1044 // Segments sharing any endpoint are either adjacent in the
1045 // contour or bridge junctions from Clipper2.
1046 if( segs[i].A == segs[j].A || segs[i].A == segs[j].B
1047 || segs[i].B == segs[j].A || segs[i].B == segs[j].B )
1048 {
1049 continue;
1050 }
1051
1052 if( segs[i].Intersects( segs[j] ) )
1053 return true;
1054 }
1055 }
1056
1057 return false;
1058 };
1059
1060 int zonesChecked = 0;
1061
1062 for( ZONE* zone : board->Zones() )
1063 {
1064 if( !zone->IsFilled() )
1065 continue;
1066
1067 for( PCB_LAYER_ID layer : zone->GetLayerSet().Seq() )
1068 {
1069 if( !zone->HasFilledPolysForLayer( layer ) )
1070 continue;
1071
1072 std::shared_ptr<SHAPE_POLY_SET> fill = zone->GetFilledPolysList( layer );
1073
1074 if( !fill || fill->OutlineCount() == 0 )
1075 continue;
1076
1077 zonesChecked++;
1078
1079 for( int pi = 0; pi < fill->OutlineCount(); pi++ )
1080 {
1081 if( fill->Outline( pi ).PointCount() < 3 )
1082 continue;
1083
1085 !hasTrueCrossing( *fill, pi ),
1086 "zone \"" << zone->GetNetname() << "\" on "
1087 << board->GetLayerName( layer )
1088 << " outline " << pi
1089 << " has self-intersecting fill polygon" );
1090 }
1091 }
1092 }
1093
1094 BOOST_CHECK_MESSAGE( zonesChecked > 0, "no filled zones found to check" );
1095}
1096
1097
1112BOOST_AUTO_TEST_CASE( ImportMaskPasteLayers )
1113{
1114 PCB_IO_PADS plugin;
1115
1116 wxString filename =
1117 KI_TEST::GetPcbnewTestDataDir() + "plugins/pads/synthetic_mask_paste.asc";
1118
1119 std::unique_ptr<BOARD> board( plugin.LoadBoard( filename, nullptr, nullptr, nullptr ) );
1120
1121 BOOST_REQUIRE( board != nullptr );
1122 BOOST_REQUIRE_EQUAL( board->Footprints().size(), 5 );
1123
1124 auto findFP = [&]( const wxString& aRef ) -> FOOTPRINT*
1125 {
1126 for( FOOTPRINT* fp : board->Footprints() )
1127 if( fp->GetReference() == aRef )
1128 return fp;
1129 return nullptr;
1130 };
1131
1132 // U1: explicit F.Mask (layer 21) and F.Paste (layer 23) in pad stack
1133 {
1134 FOOTPRINT* u1 = findFP( "U1" );
1135 BOOST_REQUIRE_MESSAGE( u1, "U1 should exist" );
1136 BOOST_REQUIRE_EQUAL( u1->Pads().size(), 1 );
1137
1138 PAD* pad = u1->Pads().front();
1139 BOOST_CHECK_MESSAGE( pad->IsOnLayer( F_Cu ), "U1 pad should be on F.Cu" );
1140 BOOST_CHECK_MESSAGE( pad->IsOnLayer( F_Mask ), "U1 pad should have F.Mask (explicit in stack)" );
1141 BOOST_CHECK_MESSAGE( pad->IsOnLayer( F_Paste ), "U1 pad should have F.Paste (explicit in stack)" );
1142 BOOST_CHECK_MESSAGE( !pad->IsOnLayer( B_Cu ), "U1 SMD pad should not be on B.Cu" );
1143 }
1144
1145 // U2: explicit F.Mask (layer 21) only; F.Paste added by fallback
1146 {
1147 FOOTPRINT* u2 = findFP( "U2" );
1148 BOOST_REQUIRE_MESSAGE( u2, "U2 should exist" );
1149 BOOST_REQUIRE_EQUAL( u2->Pads().size(), 1 );
1150
1151 PAD* pad = u2->Pads().front();
1152 BOOST_CHECK_MESSAGE( pad->IsOnLayer( F_Cu ), "U2 pad should be on F.Cu" );
1153 BOOST_CHECK_MESSAGE( pad->IsOnLayer( F_Mask ), "U2 pad should have F.Mask (explicit in stack)" );
1154 BOOST_CHECK_MESSAGE( pad->IsOnLayer( F_Paste ), "U2 pad should have F.Paste (fallback for SMD)" );
1155 BOOST_CHECK_MESSAGE( !pad->IsOnLayer( B_Cu ), "U2 SMD pad should not be on B.Cu" );
1156 }
1157
1158 // U3: no mask entries; both F.Mask and F.Paste added by fallback
1159 {
1160 FOOTPRINT* u3 = findFP( "U3" );
1161 BOOST_REQUIRE_MESSAGE( u3, "U3 should exist" );
1162 BOOST_REQUIRE_EQUAL( u3->Pads().size(), 1 );
1163
1164 PAD* pad = u3->Pads().front();
1165 BOOST_CHECK_MESSAGE( pad->IsOnLayer( F_Cu ), "U3 pad should be on F.Cu" );
1166 BOOST_CHECK_MESSAGE( pad->IsOnLayer( F_Mask ), "U3 pad should have F.Mask (SMD fallback)" );
1167 BOOST_CHECK_MESSAGE( pad->IsOnLayer( F_Paste ), "U3 pad should have F.Paste (SMD fallback)" );
1168 BOOST_CHECK_MESSAGE( !pad->IsOnLayer( B_Cu ), "U3 SMD pad should not be on B.Cu" );
1169 }
1170
1171 // U4: PTH pad with explicit F.Mask (layer 21) and B.Mask (layer 28).
1172 // No paste layers are present in the stack, so F.Paste/B.Paste must not be set.
1173 {
1174 FOOTPRINT* u4 = findFP( "U4" );
1175 BOOST_REQUIRE_MESSAGE( u4, "U4 should exist" );
1176 BOOST_REQUIRE_EQUAL( u4->Pads().size(), 1 );
1177
1178 PAD* pad = u4->Pads().front();
1179 BOOST_CHECK_MESSAGE( pad->IsOnLayer( F_Cu ), "U4 PTH pad should be on F.Cu" );
1180 BOOST_CHECK_MESSAGE( pad->IsOnLayer( B_Cu ), "U4 PTH pad should be on B.Cu" );
1181 BOOST_CHECK_MESSAGE( pad->IsOnLayer( F_Mask ), "U4 pad should have F.Mask (explicit in stack)" );
1182 BOOST_CHECK_MESSAGE( pad->IsOnLayer( B_Mask ), "U4 pad should have B.Mask (explicit in stack)" );
1183 BOOST_CHECK_MESSAGE( !pad->IsOnLayer( F_Paste ), "U4 PTH pad should not have F.Paste" );
1184 }
1185
1186 // U5: SMD pad with explicit zero-size F.Paste entry (layer 23 size 0).
1187 // A zero-size entry means "intentionally no paste on this layer".
1188 // The SMD fallback must not re-enable F.Paste for this pad.
1189 {
1190 FOOTPRINT* u5 = findFP( "U5" );
1191 BOOST_REQUIRE_MESSAGE( u5, "U5 should exist" );
1192 BOOST_REQUIRE_EQUAL( u5->Pads().size(), 1 );
1193
1194 PAD* pad = u5->Pads().front();
1195 BOOST_CHECK_MESSAGE( pad->IsOnLayer( F_Cu ), "U5 pad should be on F.Cu" );
1196 BOOST_CHECK_MESSAGE( pad->IsOnLayer( F_Mask ), "U5 pad should have F.Mask (SMD fallback)" );
1197 BOOST_CHECK_MESSAGE( !pad->IsOnLayer( F_Paste ), "U5 pad should NOT have F.Paste (explicitly zero-size)" );
1198 BOOST_CHECK_MESSAGE( !pad->IsOnLayer( B_Cu ), "U5 SMD pad should not be on B.Cu" );
1199 }
1200}
1201
1202
1207BOOST_AUTO_TEST_CASE( ImportMaskPasteLayersIssue23254 )
1208{
1209 PCB_IO_PADS plugin;
1210
1211 wxString filename =
1212 KI_TEST::GetPcbnewTestDataDir() + "plugins/pads/issue23254/issue23254.asc";
1213
1214 std::unique_ptr<BOARD> board( plugin.LoadBoard( filename, nullptr, nullptr, nullptr ) );
1215
1216 BOOST_REQUIRE( board != nullptr );
1217
1218 bool foundSmdWithMask = false;
1219
1220 for( FOOTPRINT* fp : board->Footprints() )
1221 {
1222 for( PAD* pad : fp->Pads() )
1223 {
1224 if( pad->GetAttribute() == PAD_ATTRIB::SMD && pad->IsOnLayer( F_Mask )
1225 && pad->IsOnLayer( F_Paste ) )
1226 {
1227 foundSmdWithMask = true;
1228 break;
1229 }
1230 }
1231
1232 if( foundSmdWithMask )
1233 break;
1234 }
1235
1236 BOOST_CHECK_MESSAGE( foundSmdWithMask,
1237 "At least one SMD pad in issue23254.asc should have F.Mask and F.Paste" );
1238}
1239
1240
1245BOOST_AUTO_TEST_CASE( ImportIssue23352 )
1246{
1247 PCB_IO_PADS plugin;
1248 wxString filename = KI_TEST::GetPcbnewTestDataDir() + "plugins/pads/issue23352.asc";
1249
1250 std::unique_ptr<BOARD> board;
1251 board.reset( plugin.LoadBoard( filename, nullptr, nullptr, nullptr ) );
1252 BOOST_REQUIRE( board != nullptr );
1253
1254 // Issue 1: Square pads should be imported as RECTANGLE, not CIRCLE.
1255 // The CON_2X1M part has PAD 1 with shape "S" (square) on F_Cu.
1256 bool foundSquarePad = false;
1257
1258 for( FOOTPRINT* fp : board->Footprints() )
1259 {
1260 for( PAD* pad : fp->Pads() )
1261 {
1262 if( pad->GetShape( F_Cu ) == PAD_SHAPE::RECTANGLE )
1263 {
1264 foundSquarePad = true;
1265 break;
1266 }
1267 }
1268
1269 if( foundSquarePad )
1270 break;
1271 }
1272
1273 BOOST_CHECK_MESSAGE( foundSquarePad,
1274 "At least one pad should have RECTANGLE shape (square pad import)" );
1275
1276 // Issue 2: Zone connection should default to FULL (solid), not THERMAL.
1277 // Pads with RT/ST entries should have per-pad THERMAL override.
1278 bool foundZoneWithFull = false;
1279 bool foundPadWithThermal = false;
1280 bool foundPadWithoutThermal = false;
1281
1282 for( ZONE* zone : board->Zones() )
1283 {
1284 if( zone->GetPadConnection() == ZONE_CONNECTION::FULL )
1285 {
1286 foundZoneWithFull = true;
1287 break;
1288 }
1289 }
1290
1291 BOOST_CHECK_MESSAGE( foundZoneWithFull,
1292 "Zones should default to FULL (solid) connection" );
1293
1294 for( FOOTPRINT* fp : board->Footprints() )
1295 {
1296 for( PAD* pad : fp->Pads() )
1297 {
1298 if( pad->GetLocalZoneConnection() == ZONE_CONNECTION::THERMAL )
1299 foundPadWithThermal = true;
1300 else
1301 foundPadWithoutThermal = true;
1302 }
1303 }
1304
1305 BOOST_CHECK_MESSAGE( foundPadWithThermal,
1306 "Pads with RT/ST entries should have per-pad THERMAL connection" );
1307 BOOST_CHECK_MESSAGE( foundPadWithoutThermal,
1308 "Pads without RT/ST entries should not have per-pad THERMAL override" );
1309
1310 // Issue 3: Netclasses should be imported with their rules.
1311 const BOARD_DESIGN_SETTINGS& bds = board->GetDesignSettings();
1312 const auto& netclasses = bds.m_NetSettings->GetNetclasses();
1313
1314 auto nc1It = netclasses.find( wxT( "NETTCLASS1" ) );
1315 auto nc2It = netclasses.find( wxT( "NETTCLASS2" ) );
1316
1317 BOOST_CHECK_MESSAGE( nc1It != netclasses.end(), "NETTCLASS1 should exist" );
1318 BOOST_CHECK_MESSAGE( nc2It != netclasses.end(), "NETTCLASS2 should exist" );
1319
1320 if( nc1It != netclasses.end() )
1321 {
1322 BOOST_CHECK_MESSAGE( nc1It->second->HasTrackWidth(),
1323 "NETTCLASS1 should have a track width rule" );
1324 }
1325
1326 if( nc2It != netclasses.end() )
1327 {
1328 BOOST_CHECK_MESSAGE( nc2It->second->HasTrackWidth(),
1329 "NETTCLASS2 should have a track width rule" );
1330 BOOST_CHECK_MESSAGE( nc2It->second->HasClearance(),
1331 "NETTCLASS2 should have a clearance rule" );
1332 }
1333
1334 // Verify net-to-class assignments from the NET_CLASS DATA block
1335 const auto& patterns = bds.m_NetSettings->GetNetclassPatternAssignments();
1336 std::map<wxString, wxString> netAssignments;
1337
1338 for( const auto& [matcher, ncName] : patterns )
1339 netAssignments[matcher->GetPattern()] = ncName;
1340
1341 BOOST_CHECK_MESSAGE( netAssignments.count( wxT( "+24V0" ) ),
1342 "+24V0 should be assigned to a net class" );
1343 BOOST_CHECK_MESSAGE( netAssignments.count( wxT( "+24V0_FILTER" ) ),
1344 "+24V0_FILTER should be assigned to a net class" );
1345 BOOST_CHECK_MESSAGE( netAssignments.count( wxT( "+24V0_FILTER_RTN" ) ),
1346 "+24V0_FILTER_RTN should be assigned to a net class" );
1347
1348 if( netAssignments.count( wxT( "+24V0" ) ) )
1349 {
1350 BOOST_CHECK_EQUAL( netAssignments[wxT( "+24V0" )], wxT( "NETTCLASS1" ) );
1351 }
1352
1353 if( netAssignments.count( wxT( "+24V0_FILTER_RTN" ) ) )
1354 {
1355 BOOST_CHECK_EQUAL( netAssignments[wxT( "+24V0_FILTER_RTN" )], wxT( "NETTCLASS2" ) );
1356 }
1357}
1358
1359
1368BOOST_AUTO_TEST_CASE( Issue23393_NetClassImport )
1369{
1370 PCB_IO_PADS plugin;
1371 wxString filename = KI_TEST::GetPcbnewTestDataDir() + "plugins/pads/issue23393/demo.asc";
1372
1373 std::unique_ptr<BOARD> board( plugin.LoadBoard( filename, nullptr, nullptr, nullptr ) );
1374 BOOST_REQUIRE( board != nullptr );
1375
1376 const BOARD_DESIGN_SETTINGS& bds = board->GetDesignSettings();
1377 const auto& netclasses = bds.m_NetSettings->GetNetclasses();
1378
1379 BOOST_CHECK_MESSAGE( netclasses.find( wxT( "NETTCLASS1" ) ) != netclasses.end(),
1380 "NETTCLASS1 should be imported" );
1381 BOOST_CHECK_MESSAGE( netclasses.find( wxT( "NETTCLASS2" ) ) != netclasses.end(),
1382 "NETTCLASS2 should be imported" );
1383
1384 // Verify net-to-class assignments
1385 const auto& patterns = bds.m_NetSettings->GetNetclassPatternAssignments();
1386 std::map<wxString, wxString> netAssignments;
1387
1388 for( const auto& [matcher, ncName] : patterns )
1389 netAssignments[matcher->GetPattern()] = ncName;
1390
1391 // NETTCLASS1 should contain +24V0 and +24V0_FILTER
1392 BOOST_CHECK_EQUAL( netAssignments[wxT( "+24V0" )], wxT( "NETTCLASS1" ) );
1393 BOOST_CHECK_EQUAL( netAssignments[wxT( "+24V0_FILTER" )], wxT( "NETTCLASS1" ) );
1394
1395 // NETTCLASS2 should contain +24V0_FILTER_RTN, +24V0_RTN, GND_CHASSIS
1396 BOOST_CHECK_EQUAL( netAssignments[wxT( "+24V0_FILTER_RTN" )], wxT( "NETTCLASS2" ) );
1397 BOOST_CHECK_EQUAL( netAssignments[wxT( "+24V0_RTN" )], wxT( "NETTCLASS2" ) );
1398 BOOST_CHECK_EQUAL( netAssignments[wxT( "GND_CHASSIS" )], wxT( "NETTCLASS2" ) );
1399
1400 // NETTCLASS2 RULE_SET has TRACK_TO_TRACK 4500000 BASIC
1401 auto nc2It = netclasses.find( wxT( "NETTCLASS2" ) );
1402
1403 if( nc2It != netclasses.end() )
1404 {
1405 BOOST_CHECK_MESSAGE( nc2It->second->HasClearance(),
1406 "NETTCLASS2 should have clearance from RULE_SET" );
1407 BOOST_CHECK_MESSAGE( nc2It->second->HasTrackWidth(),
1408 "NETTCLASS2 should have track width from RULE_SET" );
1409 }
1410}
1411
1412
1423BOOST_AUTO_TEST_CASE( Issue23612_RouteArcSpansNeighbours )
1424{
1425 PCB_IO_PADS plugin;
1426
1427 wxString filename = KI_TEST::GetPcbnewTestDataDir()
1428 + "plugins/pads/issue23540/test_import.asc";
1429
1430 std::unique_ptr<BOARD> board( plugin.LoadBoard( filename, nullptr, nullptr, nullptr ) );
1431
1432 BOOST_REQUIRE( board != nullptr );
1433
1434 int arcCount = 0;
1435 PCB_ARC* routeArc = nullptr;
1436
1437 for( PCB_TRACK* trk : board->Tracks() )
1438 {
1439 if( trk->Type() != PCB_ARC_T )
1440 continue;
1441
1442 PCB_ARC* arc = static_cast<PCB_ARC*>( trk );
1443 arcCount++;
1444 routeArc = arc;
1445
1446 EDA_ANGLE angle = arc->GetAngle();
1447 double absDeg = std::abs( angle.AsDegrees() );
1448
1449 BOOST_CHECK_MESSAGE( absDeg > 170.0 && absDeg < 190.0,
1450 "route arc angle " << absDeg << " should be ~180 degrees (semicircle)" );
1451
1452 VECTOR2I mid = arc->GetMid();
1453 VECTOR2I start = arc->GetStart();
1454 VECTOR2I end = arc->GetEnd();
1455
1456 // In PADS the CW arc from left to right goes upward. After the Y-axis
1457 // flip to KiCad coordinates, "upward on screen" means smaller Y values.
1458 int chordY = ( start.y + end.y ) / 2;
1459
1460 BOOST_CHECK_MESSAGE( mid.y < chordY,
1461 "arc midpoint Y=" << mid.y << " should be above (less than) "
1462 "chord center Y=" << chordY );
1463 }
1464
1465 BOOST_REQUIRE_MESSAGE( arcCount == 1,
1466 "expected exactly 1 PCB_ARC from route CW/CCW arc, got " << arcCount );
1467
1468 // The arc's net carries only the arc: treating the center corner as a vertex
1469 // leaves a straight track from the center to the pad on the same net.
1470 int straightOnArcNet = 0;
1471
1472 for( PCB_TRACK* trk : board->Tracks() )
1473 {
1474 if( trk->Type() == PCB_TRACE_T && trk->GetNetCode() == routeArc->GetNetCode() )
1475 straightOnArcNet++;
1476 }
1477
1478 BOOST_CHECK_MESSAGE( straightOnArcNet == 0,
1479 "route arc net should contain no straight track remnant, got "
1480 << straightOnArcNet );
1481}
1482
1483
1499BOOST_AUTO_TEST_CASE( ImportFingerPadOffsetIssue23425 )
1500{
1501 PCB_IO_PADS plugin;
1502
1503 wxString filename = KI_TEST::GetPcbnewTestDataDir()
1504 + "plugins/pads/issue23425/controlCARDDockingStation.asc";
1505
1506 std::unique_ptr<BOARD> board( plugin.LoadBoard( filename, nullptr, nullptr, nullptr ) );
1507
1508 BOOST_REQUIRE( board != nullptr );
1509
1510 FOOTPRINT* j3 = nullptr;
1511
1512 for( FOOTPRINT* fp : board->Footprints() )
1513 {
1514 if( fp->GetReference() == wxT( "J3" ) )
1515 {
1516 j3 = fp;
1517 break;
1518 }
1519 }
1520
1521 BOOST_REQUIRE_MESSAGE( j3, "J3 (HSEC8 edge connector) not found on board" );
1522
1523 // FINOFFSET 1143000 BASIC units * (25400 / 38100) = 762000 nm (30 mil)
1524 const int expectedOffset = 762000;
1525 const int tolerance = 1000; // 1um
1526
1527 int offsetPadCount = 0;
1528
1529 for( PAD* pad : j3->Pads() )
1530 {
1531 VECTOR2I offset = pad->GetOffset( F_Cu );
1532
1533 if( offset == VECTOR2I( 0, 0 ) )
1534 continue;
1535
1536 offsetPadCount++;
1537
1538 // Stored unrotated in pad-local space: all magnitude on X, none on Y.
1539 BOOST_CHECK_MESSAGE( offset.y == 0,
1540 "J3 pad " << pad->GetNumber()
1541 << " offset Y should be 0 (unrotated pad-local), got " << offset.y );
1542
1543 BOOST_CHECK_MESSAGE( std::abs( std::abs( offset.x ) - expectedOffset ) < tolerance,
1544 "J3 pad " << pad->GetNumber()
1545 << " offset X magnitude " << std::abs( offset.x )
1546 << " should be ~" << expectedOffset );
1547 }
1548
1549 // The connector's signal fingers all carry the offset; before the fix none of
1550 // them satisfied the checks above. Require a substantial number so a parser
1551 // change that stops applying the offset entirely cannot pass silently.
1552 BOOST_CHECK_MESSAGE( offsetPadCount >= 90,
1553 "expected the HSEC8 finger pads to carry a finger offset; got "
1554 << offsetPadCount );
1555}
1556
1557
1569BOOST_AUTO_TEST_CASE( ImportIssue23391 )
1570{
1571 PCB_IO_PADS plugin;
1572
1573 wxString filename = KI_TEST::GetPcbnewTestDataDir() + "plugins/pads/issue23391.asc";
1574
1575 std::unique_ptr<BOARD> board( plugin.LoadBoard( filename, nullptr, nullptr, nullptr ) );
1576
1577 BOOST_REQUIRE( board != nullptr );
1578
1579 // Collect mounting-hole footprints (MTG_HOLE_TYPE: E1/E2 top, E3/E4 bottom)
1580 // and connector footprints (SQ_PIN1_TYPE: X1 top, X2 bottom).
1581 PAD* mtg_top_pad = nullptr;
1582 PAD* mtg_bot_pad = nullptr;
1583 PAD* conn_top_pin1 = nullptr;
1584 PAD* conn_bot_pin1 = nullptr;
1585
1586 for( FOOTPRINT* fp : board->Footprints() )
1587 {
1588 wxString ref = fp->GetReference();
1589
1590 for( PAD* pad : fp->Pads() )
1591 {
1592 if( ref == "E1" )
1593 mtg_top_pad = pad;
1594 else if( ref == "E3" )
1595 mtg_bot_pad = pad;
1596 else if( ref == "X1" && pad->GetNumber() == "1" )
1597 conn_top_pin1 = pad;
1598 else if( ref == "X2" && pad->GetNumber() == "1" )
1599 conn_bot_pin1 = pad;
1600 }
1601 }
1602
1603 BOOST_REQUIRE_MESSAGE( mtg_top_pad, "E1 (top mounting hole) not found" );
1604 BOOST_REQUIRE_MESSAGE( mtg_bot_pad, "E3 (bottom mounting hole) not found" );
1605 BOOST_REQUIRE_MESSAGE( conn_top_pin1, "X1 pin 1 (top connector square pad) not found" );
1606 BOOST_REQUIRE_MESSAGE( conn_bot_pin1, "X2 pin 1 (bottom connector square pad) not found" );
1607
1608 // Same-shape / different-size padstack must remain in NORMAL mode.
1610 mtg_top_pad->Padstack().Mode() == PADSTACK::MODE::NORMAL,
1611 "Mounting hole with same shape but different sizes should use NORMAL padstack mode" );
1612
1614 mtg_bot_pad->Padstack().Mode() == PADSTACK::MODE::NORMAL,
1615 "Bottom-placed mounting hole should also use NORMAL padstack mode" );
1616
1617 // Both instances of the same footprint must have identical pad sizes.
1618 VECTOR2I top_size = mtg_top_pad->GetSize( F_Cu );
1619 VECTOR2I bot_size = mtg_bot_pad->GetSize( F_Cu );
1620
1622 top_size == bot_size,
1623 "Top and bottom mounting holes should have equal pad size on F_Cu; "
1624 "top=" << top_size.x << " bot=" << bot_size.x );
1625
1626 // The primary (layer -2) size must be used, not the secondary (layer -1) size.
1627 // In the test file layer -2 = 200 mils and layer -1 = 150 mils.
1628 // At 1 mil = 25400 nm, 200 mils = 5080000 nm.
1630 top_size.x > 0,
1631 "Mounting hole pad size must be non-zero" );
1632
1633 // Different-shape padstack (square vs round) must still use FRONT_INNER_BACK.
1635 conn_top_pin1->Padstack().Mode() == PADSTACK::MODE::FRONT_INNER_BACK,
1636 "Connector pin-1 with square-on-top / round-on-bottom must use FRONT_INNER_BACK" );
1637
1638 // Square (RECTANGLE) shape must appear on F_Cu for the top-placed connector.
1640 conn_top_pin1->GetShape( F_Cu ) == PAD_SHAPE::RECTANGLE,
1641 "Top connector pin-1 must have RECTANGLE shape on F_Cu" );
1642
1644 conn_top_pin1->GetShape( B_Cu ) == PAD_SHAPE::CIRCLE,
1645 "Top connector pin-1 must have CIRCLE shape on B_Cu" );
1646
1647 // After Flip, the bottom-placed connector pin-1 must have the shapes swapped.
1649 conn_bot_pin1->GetShape( F_Cu ) == PAD_SHAPE::CIRCLE,
1650 "Bottom connector pin-1 must have CIRCLE shape on F_Cu after flip" );
1651
1653 conn_bot_pin1->GetShape( B_Cu ) == PAD_SHAPE::RECTANGLE,
1654 "Bottom connector pin-1 must have RECTANGLE shape on B_Cu after flip" );
1655}
1656
1657
1674BOOST_AUTO_TEST_CASE( InCircuitTestPointImport )
1675{
1676 PCB_IO_PADS plugin;
1677
1678 wxString filename =
1679 KI_TEST::GetPcbnewTestDataDir() + "plugins/pads/synthetic_testpoint.asc";
1680
1681 std::unique_ptr<BOARD> board( plugin.LoadBoard( filename, nullptr, nullptr, nullptr ) );
1682
1683 BOOST_REQUIRE( board != nullptr );
1684
1685 // R1 (1 part) + 2 test point footprints = 3 total
1686 BOOST_CHECK_EQUAL( board->Footprints().size(), 3u );
1687
1688 FOOTPRINT* tpBottom = nullptr;
1689 FOOTPRINT* tpTop = nullptr;
1690
1691 for( FOOTPRINT* fp : board->Footprints() )
1692 {
1693 if( fp->GetValue() == wxT( "TP_BOTTOM_SMD" ) )
1694 tpBottom = fp;
1695 else if( fp->GetValue() == wxT( "TP_TOP_SMD" ) )
1696 tpTop = fp;
1697 }
1698
1699 // TP_BOTTOM_SMD: stack has soldermask bottom (layer 28) -> must land on B.Cu
1700 BOOST_REQUIRE_MESSAGE( tpBottom, "TP_BOTTOM_SMD test point footprint should exist" );
1701 BOOST_CHECK_EQUAL( tpBottom->Pads().size(), 1u );
1702
1703 if( tpBottom->Pads().size() == 1 )
1704 {
1705 PAD* pad = tpBottom->Pads().front();
1706 BOOST_CHECK_MESSAGE( pad->IsOnLayer( B_Cu ),
1707 "TP_BOTTOM_SMD pad should be on B.Cu" );
1708 BOOST_CHECK_MESSAGE( !pad->IsOnLayer( F_Cu ),
1709 "TP_BOTTOM_SMD pad should not be on F.Cu" );
1710
1711 // Pad size: 1200000 BASIC * 2/3 = 800000 nm (0.8mm). Allow 5% tolerance.
1712 int padSize = pad->GetSize( PADSTACK::ALL_LAYERS ).x;
1713 BOOST_CHECK_MESSAGE( padSize > 700000 && padSize < 900000,
1714 "TP_BOTTOM_SMD pad size " << padSize << " should be ~800000 nm" );
1715 }
1716
1717 // TP_TOP_SMD: explicit top copper pad (layer -2) -> must land on F.Cu
1718 BOOST_REQUIRE_MESSAGE( tpTop, "TP_TOP_SMD test point footprint should exist" );
1719 BOOST_CHECK_EQUAL( tpTop->Pads().size(), 1u );
1720
1721 if( tpTop->Pads().size() == 1 )
1722 {
1723 PAD* pad = tpTop->Pads().front();
1724 BOOST_CHECK_MESSAGE( pad->IsOnLayer( F_Cu ),
1725 "TP_TOP_SMD pad should be on F.Cu" );
1726 BOOST_CHECK_MESSAGE( !pad->IsOnLayer( B_Cu ),
1727 "TP_TOP_SMD pad should not be on B.Cu" );
1728
1729 int padSize = pad->GetSize( PADSTACK::ALL_LAYERS ).x;
1730 BOOST_CHECK_MESSAGE( padSize > 700000 && padSize < 900000,
1731 "TP_TOP_SMD pad size " << padSize << " should be ~800000 nm" );
1732 }
1733
1734 // Test point positions must NOT also appear as bare PCB_VIA objects.
1735 // Before the fix, loadTracksAndVias() placed a PCB_VIA at each test point
1736 // position, creating a duplicate and causing DRC open-connection errors.
1737 VECTOR2I tpBottomPos( 0, 0 );
1738 VECTOR2I tpTopPos( 0, 0 );
1739
1740 if( tpBottom )
1741 tpBottomPos = tpBottom->GetPosition();
1742
1743 if( tpTop )
1744 tpTopPos = tpTop->GetPosition();
1745
1746 for( PCB_TRACK* trk : board->Tracks() )
1747 {
1748 PCB_VIA* via = dynamic_cast<PCB_VIA*>( trk );
1749
1750 if( !via )
1751 continue;
1752
1753 VECTOR2I pos = via->GetPosition();
1754
1755 BOOST_CHECK_MESSAGE( pos != tpBottomPos,
1756 "TP_BOTTOM_SMD position should not have a bare PCB_VIA" );
1757 BOOST_CHECK_MESSAGE( pos != tpTopPos,
1758 "TP_TOP_SMD position should not have a bare PCB_VIA" );
1759 }
1760}
1761
1762
1773BOOST_AUTO_TEST_CASE( Issue23856_TextAndPadOrientation )
1774{
1775 PCB_IO_PADS plugin;
1776 wxString filename = KI_TEST::GetPcbnewTestDataDir() + "plugins/pads/issue23856.asc";
1777
1778 std::unique_ptr<BOARD> board;
1779 board.reset( plugin.LoadBoard( filename, nullptr, nullptr, nullptr ) );
1780 BOOST_REQUIRE( board != nullptr );
1781
1782 // Issue 1 + 3: free text with the copyright character must survive import,
1783 // and back-side text must be mirrored.
1784 int copyrightCount = 0;
1785 bool frontCopyrightNotMirrored = false;
1786 bool backCopyrightMirrored = false;
1787
1788 for( BOARD_ITEM* item : board->Drawings() )
1789 {
1790 PCB_TEXT* t = dynamic_cast<PCB_TEXT*>( item );
1791
1792 if( !t )
1793 continue;
1794
1795 // No imported free text should be empty (empty == dropped on decode).
1796 BOOST_CHECK_MESSAGE( !t->GetText().IsEmpty(),
1797 "imported free text should not be empty" );
1798
1799 if( t->GetText().Contains( wxT( "TEXMATE" ) ) )
1800 {
1801 copyrightCount++;
1802
1803 // Copyright sign previously broke the UTF-8 decode.
1804 BOOST_CHECK_MESSAGE( t->GetText().Contains( wxString::FromUTF8( "©" ) ),
1805 "copyright text should retain the (c) character" );
1806
1807 if( t->GetLayer() == F_SilkS )
1808 {
1810 "front silkscreen text should not be mirrored" );
1811 frontCopyrightNotMirrored = true;
1812 }
1813 else if( IsBackLayer( t->GetLayer() ) )
1814 {
1816 "back-side text should be mirrored" );
1817 backCopyrightMirrored = true;
1818 }
1819 }
1820 }
1821
1822 BOOST_CHECK_MESSAGE( copyrightCount >= 2,
1823 "expected the copyright text on both front and back, got " << copyrightCount );
1824 BOOST_CHECK( frontCopyrightNotMirrored );
1825 BOOST_CHECK( backCopyrightMirrored );
1826
1827 // Issue 2: CN1 finger pads use FINORI 90, which must not be reset to zero by
1828 // the back-side round entry of the through-hole stack.
1829 bool foundCN1 = false;
1830
1831 for( FOOTPRINT* fp : board->Footprints() )
1832 {
1833 if( fp->GetReference() != wxT( "CN1" ) )
1834 continue;
1835
1836 foundCN1 = true;
1837
1838 BOOST_CHECK_MESSAGE( fp->Pads().size() >= 10,
1839 "CN1 should have at least 10 pads, got " << fp->Pads().size() );
1840
1841 for( PAD* pad : fp->Pads() )
1842 {
1843 // Oval/rectangle finger pads must carry the 90 degree finger rotation.
1844 if( pad->GetShape( F_Cu ) == PAD_SHAPE::OVAL
1845 || pad->GetShape( F_Cu ) == PAD_SHAPE::RECTANGLE )
1846 {
1848 pad->GetOrientation() == EDA_ANGLE( 90, DEGREES_T ),
1849 "CN1 finger pad " << pad->GetNumber().ToStdString()
1850 << " should be oriented 90 degrees, got "
1851 << pad->GetOrientation().AsDegrees() );
1852 }
1853 }
1854 }
1855
1856 BOOST_CHECK_MESSAGE( foundCN1, "CN1 footprint should be imported" );
1857}
1858
1859
1867BOOST_AUTO_TEST_CASE( Issue23392_ThermalReliefGap )
1868{
1869 PCB_IO_PADS plugin;
1870 wxString filename = KI_TEST::GetPcbnewTestDataDir() + "plugins/pads/issue23393/demo.asc";
1871
1872 std::unique_ptr<BOARD> board( plugin.LoadBoard( filename, nullptr, nullptr, nullptr ) );
1873 BOOST_REQUIRE( board != nullptr );
1874
1875 auto findFP = [&]( const wxString& aRef ) -> FOOTPRINT*
1876 {
1877 for( FOOTPRINT* fp : board->Footprints() )
1878 {
1879 if( fp->GetReference() == aRef )
1880 return fp;
1881 }
1882
1883 return nullptr;
1884 };
1885
1886 const int tolerance = 5000; // 5 um
1887
1888 // L1 carries an RT relief whose outer diameter (4110000) exceeds the pad size
1889 // (3750000), so every pad gets a THERMAL connection, a spoke-width override, and a
1890 // gap override of (4110000 - 3750000) / 2 scaled to internal units.
1891 {
1892 FOOTPRINT* l1 = findFP( "L1" );
1893 BOOST_REQUIRE_MESSAGE( l1, "L1 footprint should be imported" );
1894 BOOST_REQUIRE( !l1->Pads().empty() );
1895
1896 for( PAD* pad : l1->Pads() )
1897 {
1898 BOOST_CHECK_MESSAGE( pad->GetLocalZoneConnection() == ZONE_CONNECTION::THERMAL,
1899 "L1 pad " << pad->GetNumber().ToStdString()
1900 << " should have THERMAL zone connection" );
1901
1902 std::optional<int> spoke = pad->GetLocalThermalSpokeWidthOverride();
1903 BOOST_REQUIRE_MESSAGE( spoke.has_value(),
1904 "L1 pad " << pad->GetNumber().ToStdString()
1905 << " should have a spoke-width override" );
1906 BOOST_CHECK_MESSAGE( std::abs( spoke.value() - 1000000 ) < tolerance,
1907 "L1 pad " << pad->GetNumber().ToStdString() << " spoke width "
1908 << spoke.value() << " should be ~1000000 nm" );
1909
1910 std::optional<int> gap = pad->GetLocalThermalGapOverride();
1911 BOOST_REQUIRE_MESSAGE( gap.has_value(),
1912 "L1 pad " << pad->GetNumber().ToStdString()
1913 << " should have a thermal gap override" );
1914 BOOST_CHECK_MESSAGE( std::abs( gap.value() - 120000 ) < tolerance,
1915 "L1 pad " << pad->GetNumber().ToStdString() << " thermal gap "
1916 << gap.value() << " should be ~120000 nm" );
1917 }
1918 }
1919
1920 // E1 carries an RT relief whose outer diameter equals the pad size, so it must have a
1921 // THERMAL connection but NO gap override.
1922 {
1923 FOOTPRINT* e1 = findFP( "E1" );
1924 BOOST_REQUIRE_MESSAGE( e1, "E1 footprint should be imported" );
1925 BOOST_REQUIRE( !e1->Pads().empty() );
1926
1927 PAD* pad = e1->Pads().front();
1928 BOOST_CHECK_MESSAGE( pad->GetLocalZoneConnection() == ZONE_CONNECTION::THERMAL,
1929 "E1 pad should have THERMAL zone connection" );
1930 BOOST_CHECK_MESSAGE( !pad->GetLocalThermalGapOverride().has_value(),
1931 "E1 pad should NOT have a thermal gap override (outer == pad size)" );
1932 }
1933}
1934
1935
1943BOOST_AUTO_TEST_CASE( Issue23241_V5Parts )
1944{
1945 PCB_IO_PADS plugin;
1946
1947 wxString filename =
1948 KI_TEST::GetPcbnewTestDataDir() + "plugins/pads/issue23241/partsandattr.asc";
1949
1950 std::unique_ptr<BOARD> board( plugin.LoadBoard( filename, nullptr, nullptr, nullptr ) );
1951
1952 BOOST_REQUIRE( board != nullptr );
1953
1954 // The file contains parts on both layers. Verify the specific parts mentioned
1955 // in the bug report are present.
1956 std::set<wxString> refDes;
1957
1958 for( FOOTPRINT* fp : board->Footprints() )
1959 refDes.insert( fp->GetReference() );
1960
1961 BOOST_CHECK_MESSAGE( refDes.count( wxT( "J1" ) ), "J1 should be present" );
1962 BOOST_CHECK_MESSAGE( refDes.count( wxT( "J2" ) ), "J2 should be present" );
1963 BOOST_CHECK_MESSAGE( refDes.count( wxT( "J3" ) ), "J3 should be present" );
1964 BOOST_CHECK_MESSAGE( refDes.count( wxT( "J4" ) ), "J4 should be present" );
1965 BOOST_CHECK_MESSAGE( refDes.count( wxT( "J5" ) ), "J5 should be present" );
1966 BOOST_CHECK_MESSAGE( refDes.count( wxT( "U1" ) ), "U1 should be present" );
1967 BOOST_CHECK_MESSAGE( refDes.count( wxT( "U2" ) ), "U2 should be present" );
1968 BOOST_CHECK_MESSAGE( refDes.count( wxT( "U3" ) ), "U3 should be present" );
1969 BOOST_CHECK_MESSAGE( refDes.count( wxT( "U4" ) ), "U4 should be present" );
1970}
1971
1972
1979BOOST_AUTO_TEST_CASE( Issue23241_V5DecalTerminals )
1980{
1981 PADS_IO::PARSER parser;
1982
1983 wxString filename =
1984 KI_TEST::GetPcbnewTestDataDir() + "plugins/pads/issue23241/partsandattr.asc";
1985
1986 parser.Parse( filename );
1987
1988 const auto& decals = parser.GetPartDecals();
1989
1990 auto it = decals.find( "104130-6" );
1991 BOOST_REQUIRE_MESSAGE( it != decals.end(), "104130-6 decal should exist" );
1992 BOOST_REQUIRE_EQUAL( it->second.terminals.size(), 34u );
1993
1994 auto sop_it = decals.find( "SOP16" );
1995 BOOST_REQUIRE_MESSAGE( sop_it != decals.end(), "SOP16 decal should exist" );
1996 BOOST_REQUIRE_EQUAL( sop_it->second.terminals.size(), 16u );
1997
1998 // V5.0 terminals carry no pin number, so the parser synthesizes sequential
1999 // names 1..N. Empty or duplicate names would break pad-to-net mapping, which
2000 // a bare count check cannot catch.
2001 BOOST_CHECK_EQUAL( it->second.terminals.front().name, "1" );
2002 BOOST_CHECK_EQUAL( it->second.terminals.back().name, "34" );
2003 BOOST_CHECK_EQUAL( sop_it->second.terminals.front().name, "1" );
2004 BOOST_CHECK_EQUAL( sop_it->second.terminals.back().name, "16" );
2005}
2006
2007
2018BOOST_AUTO_TEST_CASE( Issue23297_RfPadCornerRadius )
2019{
2020 PCB_IO_PADS plugin;
2021
2022 wxString filename = KI_TEST::GetPcbnewTestDataDir() + "plugins/pads/issue23297.asc";
2023
2024 std::unique_ptr<BOARD> board( plugin.LoadBoard( filename, nullptr, nullptr, nullptr ) );
2025
2026 BOOST_REQUIRE( board != nullptr );
2027
2028 const double tolerance = 0.005;
2029
2030 struct EXPECTED
2031 {
2032 int padCount;
2033 double ratio;
2034 };
2035
2036 const std::map<wxString, EXPECTED> expected = {
2037 { wxT( "R1" ), { 2, 105000.0 / 900000.0 } },
2038 { wxT( "D1" ), { 6, 225000.0 / 825000.0 } },
2039 };
2040
2041 int checkedRefs = 0;
2042
2043 for( FOOTPRINT* fp : board->Footprints() )
2044 {
2045 auto it = expected.find( fp->GetReference() );
2046
2047 if( it == expected.end() )
2048 continue;
2049
2050 checkedRefs++;
2051
2052 int roundRectCount = 0;
2053
2054 for( PAD* pad : fp->Pads() )
2055 {
2057 fp->GetReference() << " pad " << pad->GetNumber()
2058 << " should import as roundrect" );
2059
2060 if( pad->GetShape( F_Cu ) != PAD_SHAPE::ROUNDRECT )
2061 continue;
2062
2064 std::abs( pad->GetRoundRectRadiusRatio( F_Cu ) - it->second.ratio ) < tolerance,
2065 fp->GetReference() << " pad " << pad->GetNumber() << " ratio "
2066 << pad->GetRoundRectRadiusRatio( F_Cu ) << " should be ~" << it->second.ratio );
2067
2068 roundRectCount++;
2069 }
2070
2071 BOOST_CHECK_MESSAGE( roundRectCount == it->second.padCount,
2072 fp->GetReference() << " should have " << it->second.padCount
2073 << " roundrect pads, got " << roundRectCount );
2074 }
2075
2076 BOOST_CHECK_MESSAGE( checkedRefs == (int) expected.size(),
2077 "expected R1 and D1 footprints to be imported, found " << checkedRefs );
2078}
2079
2080
General utilities for PCB file IO for QA programs.
@ BS_ITEM_TYPE_COPPER
@ BS_ITEM_TYPE_DIELECTRIC
BOX2< VECTOR2I > BOX2I
Definition box2.h:918
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:81
virtual PCB_LAYER_ID GetLayer() const
Return the primary layer this item is on.
Definition board_item.h:265
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:76
constexpr BOX2< Vec > & Merge(const BOX2< Vec > &aRect)
Modify the position and size of the rectangle in order to contain aRect.
Definition box2.h:654
constexpr bool Intersects(const BOX2< Vec > &aRect) const
Definition box2.h:307
double AsDegrees() const
Definition eda_angle.h:116
virtual const wxString & GetText() const
Return the string associated with the text object.
Definition eda_text.h:110
bool IsMirrored() const
Definition eda_text.h:211
std::deque< PAD * > & Pads()
Definition footprint.h:375
VECTOR2I GetPosition() const override
Definition footprint.h:403
Handle the data for a net.
Definition netinfo.h:46
const wxString & GetNetname() const
Definition netinfo.h:100
const std::map< wxString, std::shared_ptr< NETCLASS > > & GetNetclasses() const
Gets all netclasses.
std::shared_ptr< NETCLASS > GetDefaultNetclass() const
Gets the default netclass for the project.
std::vector< std::pair< std::unique_ptr< EDA_COMBINED_MATCHER >, wxString > > & GetNetclassPatternAssignments()
Gets the netclass pattern assignments.
@ NORMAL
Shape is the same on all layers.
Definition padstack.h:171
@ FRONT_INNER_BACK
Up to three shapes can be defined (F_Cu, inner copper layers, B_Cu)
Definition padstack.h:172
MODE Mode() const
Definition padstack.h:335
static constexpr PCB_LAYER_ID ALL_LAYERS
! Temporary layer identifier to identify code that is not padstack-aware
Definition padstack.h:177
const std::map< std::string, PART_DECAL > & GetPartDecals() const
void Parse(const wxString &aFileName)
Definition pad.h:61
PAD_SHAPE GetShape(PCB_LAYER_ID aLayer) const
Definition pad.h:202
VECTOR2I GetSize(PCB_LAYER_ID aLayer) const
Definition pad.cpp:287
const PADSTACK & Padstack() const
Definition pad.h:326
EDA_ANGLE GetAngle() const
const VECTOR2I & GetMid() const
Definition pcb_track.h:286
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.
const VECTOR2I & GetStart() const
Definition pcb_track.h:93
const VECTOR2I & GetEnd() const
Definition pcb_track.h:90
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:70
SHAPE_POLY_SET * Outline()
Definition zone.h:418
@ DEGREES_T
Definition eda_angle.h:31
@ RECTANGLE
Use RECTANGLE instead of RECT to avoid collision in a Windows header.
Definition eda_shape.h:47
bool IsBackLayer(PCB_LAYER_ID aLayerId)
Layer classification: check if it's a back layer.
Definition layer_ids.h:801
bool IsCopperLayer(int aLayerId)
Test whether a layer is a copper layer.
Definition layer_ids.h:675
PCB_LAYER_ID
A quick note on layer IDs:
Definition layer_ids.h:56
@ Edge_Cuts
Definition layer_ids.h:108
@ Dwgs_User
Definition layer_ids.h:103
@ F_Paste
Definition layer_ids.h:100
@ Cmts_User
Definition layer_ids.h:104
@ B_Mask
Definition layer_ids.h:94
@ B_Cu
Definition layer_ids.h:61
@ F_Mask
Definition layer_ids.h:93
@ F_SilkS
Definition layer_ids.h:96
@ UNDEFINED_LAYER
Definition layer_ids.h:57
@ F_Cu
Definition layer_ids.h:60
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
@ SMD
Smd pad, appears on the solder paste layer (default)
Definition padstack.h:99
@ ROUNDRECT
Definition padstack.h:57
@ RECTANGLE
Definition padstack.h:54
BOOST_AUTO_TEST_CASE(HorizontalAlignment)
BOOST_AUTO_TEST_SUITE(CadstarPartParser)
BOOST_REQUIRE(intersection.has_value()==c.ExpectedIntersection.has_value())
BOOST_AUTO_TEST_SUITE_END()
VECTOR3I expected(15, 30, 45)
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)
BOOST_CHECK_MESSAGE(totalMismatches==0, std::to_string(totalMismatches)+" board(s) with strategy disagreements")
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:90
@ PCB_TEXT_T
class PCB_TEXT, text on a layer
Definition typeinfo.h:85
@ PCB_ARC_T
class PCB_ARC, an arc track segment on a copper layer
Definition typeinfo.h:91
@ PCB_TRACE_T
class PCB_TRACK, a track segment (segment on a copper layer)
Definition typeinfo.h:89
VECTOR2< int32_t > VECTOR2I
Definition vector2d.h:683
@ THERMAL
Use thermal relief for pads.
Definition zones.h:46
@ FULL
pads are covered by copper
Definition zones.h:47