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