KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_altium_pcb_import.cpp
Go to the documentation of this file.
1/*
2 * This program source code file is part of KiCad, a free EDA CAD application.
3 *
4 * Copyright The KiCad Developers, see AUTHORS.txt for contributors.
5 *
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU General Public License
8 * as published by the Free Software Foundation; either version 2
9 * of the License, or (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program. If not, see <https://www.gnu.org/licenses/>.
18 */
19
24
28
31
32#include <board.h>
34#include <footprint.h>
36#include <common.h>
37#include <core/utf8.h>
38#include <eda_text.h>
39#include <netinfo.h>
40#include <netclass.h>
41#include <pcb_track.h>
42#include <pcb_shape.h>
43#include <pcb_generator.h>
45#include <project.h>
46#include <pcb_text.h>
49#include <zone.h>
50
51#include <map>
52#include <set>
53#include <string>
54#include <vector>
55
56
63
64
65BOOST_FIXTURE_TEST_SUITE( AltiumPcbImport, ALTIUM_PCB_IMPORT_FIXTURE )
66
67
68
72BOOST_AUTO_TEST_CASE( BoardLoadNoAssertions )
73{
74 std::string dataPath = KI_TEST::GetPcbnewTestDataDir()
75 + "plugins/altium/eDP_adapter_dvt1_source/eDP_adapter_dvt1.PcbDoc";
76
77 std::unique_ptr<BOARD> board = std::make_unique<BOARD>();
78
79 // Load the board - should not trigger any assertions
80 m_altiumPlugin.LoadBoard( dataPath, board.get(), nullptr );
81
82 BOOST_REQUIRE( board );
83
84 // Basic sanity checks
85 BOOST_CHECK( board->GetNetCount() > 0 );
86 BOOST_CHECK( board->Footprints().size() > 0 );
87}
88
89
90// GetImportedCachedLibraryFootprints() is caller-owns, so Altium must clone rather than alias
91// aliasing would let the reconciler (takes ownership) double-free the board
92BOOST_AUTO_TEST_CASE( CachedLibraryFootprintsAreOwnedCopies )
93{
94 std::string dataPath =
95 KI_TEST::GetPcbnewTestDataDir() + "plugins/altium/HiFive/HiFive1.B01.PcbDoc";
96
97 std::unique_ptr<BOARD> board = std::make_unique<BOARD>();
98 m_altiumPlugin.LoadBoard( dataPath, board.get(), nullptr );
99
100 BOOST_REQUIRE( board );
101 BOOST_REQUIRE_GT( board->Footprints().size(), 0 );
102
103 std::vector<FOOTPRINT*> cached = m_altiumPlugin.GetImportedCachedLibraryFootprints();
104
105 // adopt ownership so the clones free with the test
106 std::vector<std::unique_ptr<FOOTPRINT>> owned;
107
108 for( FOOTPRINT* fp : cached )
109 owned.emplace_back( fp );
110
111 BOOST_CHECK_EQUAL( cached.size(), board->Footprints().size() );
112
113 std::set<FOOTPRINT*> boardFootprints( board->Footprints().begin(), board->Footprints().end() );
114
115 // no returned footprint may alias a board-owned one
116 for( FOOTPRINT* fp : cached )
117 {
118 BOOST_CHECK_MESSAGE( boardFootprints.count( fp ) == 0,
119 "GetImportedCachedLibraryFootprints returned a board-owned footprint" );
120 }
121}
122
123
131BOOST_AUTO_TEST_CASE( NetclassAssignment )
132{
133 // HiFive1.B01.PcbDoc has Altium netclass definitions
134 std::string dataPath = KI_TEST::GetPcbnewTestDataDir() + "plugins/altium/HiFive/HiFive1.B01.PcbDoc";
135
136 std::unique_ptr<BOARD> board = std::make_unique<BOARD>();
137
138 m_altiumPlugin.LoadBoard( dataPath, board.get(), nullptr );
139
140 BOOST_REQUIRE( board );
141
142 // Get the net settings which contains pattern assignments
143 std::shared_ptr<NET_SETTINGS> netSettings = board->GetDesignSettings().m_NetSettings;
144
145 BOOST_REQUIRE( netSettings );
146
147 // Check if there are any pattern assignments in the board
148 auto& patternAssignments = netSettings->GetNetclassPatternAssignments();
149
150 // The HiFive board should have netclass definitions - require this for the test to be meaningful
151 BOOST_REQUIRE_MESSAGE( patternAssignments.size() > 0,
152 "Test file must have netclass pattern assignments" );
153
154 // For each net that has a pattern assignment, verify that the NETINFO_ITEM
155 // has a netclass directly assigned (not just through pattern resolution)
156 bool foundAssignedNet = false;
157
158 for( NETINFO_ITEM* net : board->GetNetInfo() )
159 {
160 if( net->GetNetCode() <= 0 )
161 continue;
162
163 // Get the netclass directly from the NETINFO_ITEM
164 NETCLASS* directNetclass = net->GetNetClass();
165
166 // Get the effective netclass from pattern resolution
167 std::shared_ptr<NETCLASS> effectiveNetclass =
168 netSettings->GetEffectiveNetClass( net->GetNetname() );
169
170 // If this net has a non-default effective netclass, the direct assignment
171 // should also be non-default (this is what the fix ensures)
172 if( effectiveNetclass && effectiveNetclass->GetName() != NETCLASS::Default )
173 {
174 BOOST_CHECK_MESSAGE(
175 directNetclass != nullptr,
176 wxString::Format( "Net '%s' should have a direct netclass assignment",
177 net->GetNetname() ) );
178
179 if( directNetclass )
180 {
181 foundAssignedNet = true;
182
183 // The direct netclass should match what effective resolution returns
184 // (or be part of the effective class for multi-netclass scenarios)
185 BOOST_CHECK_MESSAGE(
186 directNetclass->GetName() != NETCLASS::Default,
187 wxString::Format( "Net '%s' should not have default netclass, "
188 "expected effective class or component",
189 net->GetNetname() ) );
190 }
191 }
192 }
193
194 // If there were pattern assignments, we should have found at least one assigned net
195 BOOST_CHECK_MESSAGE( foundAssignedNet,
196 "At least one net should have a non-default netclass assigned" );
197}
198
199
206 const std::string& aRelativePath )
207{
208 std::string dataPath = KI_TEST::GetPcbnewTestDataDir() + aRelativePath;
209
210 std::unique_ptr<BOARD> board = std::make_unique<BOARD>();
211 aPlugin.LoadBoard( dataPath, board.get(), nullptr );
212
213 BOOST_REQUIRE( board );
214
215 int fillZoneCount = 0;
216 int fillZonesWithClearance = 0;
217
218 for( ZONE* zone : board->Zones() )
219 {
220 if( !zone->IsOnCopperLayer() || zone->GetIsRuleArea() || zone->IsTeardropArea() )
221 continue;
222
223 fillZoneCount++;
224
225 if( zone->GetLocalClearance().has_value() && zone->GetLocalClearance().value() > 0 )
226 fillZonesWithClearance++;
227 }
228
229 BOOST_CHECK_GT( fillZoneCount, 0 );
230
231 BOOST_CHECK_MESSAGE( fillZonesWithClearance == fillZoneCount,
232 wxString::Format( "%s: %d/%d copper fill zones have clearance set",
233 aRelativePath, fillZonesWithClearance,
234 fillZoneCount ) );
235}
236
237
238BOOST_AUTO_TEST_CASE( ZoneClearances_eDP )
239{
241 m_altiumPlugin, "plugins/altium/eDP_adapter_dvt1_source/eDP_adapter_dvt1.PcbDoc" );
242}
243
244
245BOOST_AUTO_TEST_CASE( ZoneClearances_HiFive )
246{
248 "plugins/altium/HiFive/HiFive1.B01.PcbDoc" );
249}
250
251
259BOOST_AUTO_TEST_CASE( ScopeExprMatchesPolygon )
260{
261 // Positive matches: expressions that reference polygons
262 BOOST_CHECK( altiumScopeExprMatchesPolygon( wxT( "InPolygon" ) ) );
263 BOOST_CHECK( altiumScopeExprMatchesPolygon( wxT( "InPoly" ) ) );
264 BOOST_CHECK( altiumScopeExprMatchesPolygon( wxT( "IsPolygon" ) ) );
265 BOOST_CHECK( altiumScopeExprMatchesPolygon( wxT( "IsPoly" ) ) );
266
267 // Case insensitivity
268 BOOST_CHECK( altiumScopeExprMatchesPolygon( wxT( "inpolygon" ) ) );
269 BOOST_CHECK( altiumScopeExprMatchesPolygon( wxT( "INPOLYGON" ) ) );
270 BOOST_CHECK( altiumScopeExprMatchesPolygon( wxT( "inPOLY" ) ) );
271
272 // Contained within longer expressions
273 BOOST_CHECK( altiumScopeExprMatchesPolygon( wxT( "InPolygon And InNet('GND')" ) ) );
274 BOOST_CHECK( altiumScopeExprMatchesPolygon( wxT( "(InPoly) Or IsVia" ) ) );
275
276 // Negative matches: expressions that don't reference polygons
277 BOOST_CHECK( !altiumScopeExprMatchesPolygon( wxT( "All" ) ) );
278 BOOST_CHECK( !altiumScopeExprMatchesPolygon( wxT( "IsVia" ) ) );
279 BOOST_CHECK( !altiumScopeExprMatchesPolygon( wxT( "IsTrack" ) ) );
280 BOOST_CHECK( !altiumScopeExprMatchesPolygon( wxT( "InNet('GND')" ) ) );
281 BOOST_CHECK( !altiumScopeExprMatchesPolygon( wxT( "InComponent('U1')" ) ) );
282 BOOST_CHECK( !altiumScopeExprMatchesPolygon( wxT( "" ) ) );
283}
284
285
291BOOST_AUTO_TEST_CASE( SelectAltiumPolygonRule_PriorityOrder )
292{
293 auto makeRule = []( int aPriority, const wxString& aScope1, const wxString& aScope2,
294 int aClearance )
295 {
296 ARULE6 rule;
297 rule.priority = aPriority;
298 rule.scope1expr = aScope1;
299 rule.scope2expr = aScope2;
300 rule.clearanceGap = aClearance;
301 return rule;
302 };
303
304 // Sorted by priority ascending, matching the order produced by ParseRules6Data.
305 std::vector<ARULE6> rules = {
306 makeRule( 1, wxT( "InPolygon And InNet('GND')" ), wxT( "All" ), 100 ),
307 makeRule( 2, wxT( "InPolygon" ), wxT( "All" ), 200 ),
308 makeRule( 3, wxT( "All" ), wxT( "All" ), 300 ),
309 makeRule( 4, wxT( "All" ), wxT( "All" ), 400 ),
310 };
311
312 const ARULE6* selected = selectAltiumPolygonRule( rules );
313 BOOST_REQUIRE( selected != nullptr );
314 BOOST_CHECK_EQUAL( selected->priority, 1 );
315 BOOST_CHECK_EQUAL( selected->clearanceGap, 100 );
316
317 rules.erase( rules.begin() );
318 selected = selectAltiumPolygonRule( rules );
319 BOOST_REQUIRE( selected != nullptr );
320 BOOST_CHECK_EQUAL( selected->priority, 2 );
321 BOOST_CHECK_EQUAL( selected->clearanceGap, 200 );
322
323 rules.erase( rules.begin() );
324 BOOST_CHECK( selectAltiumPolygonRule( rules ) == nullptr );
325
326 BOOST_CHECK( selectAltiumPolygonRule( {} ) == nullptr );
327}
328
329
340BOOST_AUTO_TEST_CASE( Via_HoleReferencedMaskTenting )
341{
342 std::string dataPath = KI_TEST::GetPcbnewTestDataDir()
343 + "plugins/altium/issue24456/Fastino_Ground_Isolator.PcbDoc";
344
345 std::unique_ptr<BOARD> board = std::make_unique<BOARD>();
346
347 m_altiumPlugin.LoadBoard( dataPath, board.get(), nullptr );
348
349 BOOST_REQUIRE( board );
350
351 int viaCount = 0;
352 int frontExposed = 0;
353 int backExposed = 0;
354
355 for( PCB_TRACK* track : board->Tracks() )
356 {
357 if( track->Type() != PCB_VIA_T )
358 continue;
359
360 const PCB_VIA* via = static_cast<const PCB_VIA*>( track );
361 viaCount++;
362
363 if( via->GetFrontTentingMode() != TENTING_MODE::TENTED )
364 frontExposed++;
365
366 if( via->GetBackTentingMode() != TENTING_MODE::TENTED )
367 backExposed++;
368 }
369
370 BOOST_REQUIRE_GT( viaCount, 0 );
371
372 // Every via on this board carries a hole-referenced mask opening narrower than its land, so the
373 // importer must tent both sides of all of them.
374 BOOST_CHECK_MESSAGE( frontExposed == 0,
375 wxString::Format( "%d of %d vias left front-exposed despite a "
376 "hole-referenced mask",
377 frontExposed, viaCount ) );
378 BOOST_CHECK_MESSAGE( backExposed == 0,
379 wxString::Format( "%d of %d vias left back-exposed despite a "
380 "hole-referenced mask",
381 backExposed, viaCount ) );
382
383 // Guard the tenting heuristic's boundary cases directly. A wide hole-referenced opening that
384 // clears the land must NOT tent, a land-referenced via must NOT be silently tented, and an
385 // explicit Altium tent flag must always tent regardless of expansion mode.
386 const uint32_t holeSize = 300000; // 0.3mm
387 const int landWidth = 600000; // 0.6mm
388
389 BOOST_CHECK( !altiumViaSideIsTented( /*tentFlag*/ false, /*manual*/ true, /*fromHole*/ true,
390 holeSize, /*expansion*/ 500000, landWidth ) );
391 BOOST_CHECK( !altiumViaSideIsTented( /*tentFlag*/ false, /*manual*/ true, /*fromHole*/ false,
392 holeSize, /*expansion*/ 30000, landWidth ) );
393 BOOST_CHECK( altiumViaSideIsTented( /*tentFlag*/ true, /*manual*/ false, /*fromHole*/ false,
394 holeSize, /*expansion*/ 0, landWidth ) );
395
396 // A narrow hole-referenced opening (hole + 2 * expansion <= land) tents the side.
397 BOOST_CHECK( altiumViaSideIsTented( /*tentFlag*/ false, /*manual*/ true, /*fromHole*/ true,
398 holeSize, /*expansion*/ 30000, landWidth ) );
399}
400
401
409BOOST_AUTO_TEST_CASE( StackupDielectricLossTangent )
410{
411 std::string dataPath = KI_TEST::GetPcbnewTestDataDir()
412 + "plugins/altium/issue24456/Fastino_Ground_Isolator.PcbDoc";
413
414 std::unique_ptr<BOARD> board = std::make_unique<BOARD>();
415
416 m_altiumPlugin.LoadBoard( dataPath, board.get(), nullptr );
417
418 BOOST_REQUIRE( board );
419
420 const BOARD_STACKUP& stackup = board->GetDesignSettings().GetStackupDescriptor();
421
422 int dielectricCount = 0;
423 int dielectricWithTangent = 0;
424
425 for( const BOARD_STACKUP_ITEM* item : stackup.GetList() )
426 {
427 if( item->GetType() != BS_ITEM_TYPE_DIELECTRIC )
428 continue;
429
430 for( int sub = 0; sub < item->GetSublayersCount(); sub++ )
431 {
432 // Only count dielectric sublayers that carry a real dielectric (non-zero thickness)
433 if( item->GetThickness( sub ) <= 0 )
434 continue;
435
436 dielectricCount++;
437
438 double tangent = item->GetLossTangent( sub );
439
440 if( tangent > 0. )
441 {
442 dielectricWithTangent++;
443
444 // Every prepreg/core dielectric in this board uses a 0.020 loss tangent.
445 BOOST_CHECK_CLOSE( tangent, 0.020, 1e-6 );
446 }
447 }
448 }
449
450 BOOST_REQUIRE_GT( dielectricCount, 0 );
451
452 // All of the board's substantive dielectrics carry a loss tangent in the Altium source, so
453 // every imported dielectric sublayer must receive it.
454 BOOST_CHECK_MESSAGE( dielectricWithTangent == dielectricCount,
455 wxString::Format( "Only %d of %d dielectric sublayers received a loss "
456 "tangent from the Altium stackup",
457 dielectricWithTangent, dielectricCount ) );
458}
459
460
467BOOST_AUTO_TEST_CASE( ProjectParametersToTextVars )
468{
469 std::string dataDir = KI_TEST::GetPcbnewTestDataDir() + "plugins/altium/issue24456/";
470 std::string pcbDoc = dataDir + "Fastino_Ground_Isolator.PcbDoc";
471 std::string prjPcb = dataDir + "Fastino_Ground_Isolator.PrjPcb";
472
473 SETTINGS_MANAGER settingsManager;
474 settingsManager.LoadProject( "" );
475 PROJECT& project = settingsManager.Prj();
476
477 std::map<std::string, UTF8> props;
478 props["project_file"] = prjPcb;
479
480 std::unique_ptr<BOARD> board = std::make_unique<BOARD>();
481
482 m_altiumPlugin.LoadBoard( pcbDoc, board.get(), &props, &project );
483
484 const std::map<wxString, wxString>& textVars = project.GetTextVars();
485
486 // The parameter that the issue reports as broken must resolve to its value.
487 BOOST_REQUIRE( textVars.count( wxS( "PCB_REVISION" ) ) );
488 BOOST_CHECK_EQUAL( textVars.at( wxS( "PCB_REVISION" ) ), wxS( "A" ) );
489
490 // A representative sample of the remaining project parameters must all be present.
491 BOOST_CHECK_EQUAL( textVars.at( wxS( "COMPANY_NAME" ) ), wxS( "ETH Zurich" ) );
492 BOOST_CHECK_EQUAL( textVars.at( wxS( "PROJECT_NAME" ) ), wxS( "Fastino Ground Isolator" ) );
493 BOOST_CHECK_EQUAL( textVars.at( wxS( "REVISION_MAJOR" ) ), wxS( "1" ) );
494 BOOST_CHECK_EQUAL( textVars.at( wxS( "YEAR" ) ), wxS( "2026" ) );
495
496 // Board text referencing the special string now resolves through the project variable.
497 wxString resolved = ExpandTextVars( wxS( "${PCB_REVISION}" ), &project );
498 BOOST_CHECK_EQUAL( resolved, wxS( "A" ) );
499
500 // End-to-end: an actual imported board text that references ${PCB_REVISION} must render its
501 // value once the board is linked to the project carrying the variable. This guards against a
502 // regression in the Altium special-string conversion as well as the variable registration.
503 board->SetProject( &project, true /* reference only */ );
504
505 bool sawResolvedBoardText = false;
506
507 for( BOARD_ITEM* item : board->Drawings() )
508 {
509 const EDA_TEXT* text = dynamic_cast<const EDA_TEXT*>( item );
510
511 if( text && text->GetText().Contains( wxS( "${PCB_REVISION}" ) ) )
512 {
513 wxString shown = text->GetShownText( false );
514 BOOST_CHECK( !shown.Contains( wxS( "${PCB_REVISION}" ) ) );
515 BOOST_CHECK( shown.Contains( wxS( "A" ) ) );
516 sawResolvedBoardText = true;
517 }
518 }
519
520 BOOST_CHECK( sawResolvedBoardText );
521}
522
523
528BOOST_AUTO_TEST_CASE( ProjectParametersPreserveExisting )
529{
530 std::string dataDir = KI_TEST::GetPcbnewTestDataDir() + "plugins/altium/issue24456/";
531 std::string pcbDoc = dataDir + "Fastino_Ground_Isolator.PcbDoc";
532 std::string prjPcb = dataDir + "Fastino_Ground_Isolator.PrjPcb";
533
534 SETTINGS_MANAGER settingsManager;
535 settingsManager.LoadProject( "" );
536 PROJECT& project = settingsManager.Prj();
537 project.GetTextVars()[wxS( "PCB_REVISION" )] = wxS( "user-set" );
538
539 std::map<std::string, UTF8> props;
540 props["project_file"] = prjPcb;
541
542 std::unique_ptr<BOARD> board = std::make_unique<BOARD>();
543
544 m_altiumPlugin.LoadBoard( pcbDoc, board.get(), &props, &project );
545
546 // A pre-existing variable wins over the imported parameter.
547 BOOST_CHECK_EQUAL( project.GetTextVars().at( wxS( "PCB_REVISION" ) ), wxS( "user-set" ) );
548
549 // Other parameters are still imported.
550 BOOST_CHECK_EQUAL( project.GetTextVars().at( wxS( "COMPANY_NAME" ) ), wxS( "ETH Zurich" ) );
551}
552
553
564BOOST_AUTO_TEST_CASE( CopperAndMaskTextCoincide )
565{
566 std::string dataPath = KI_TEST::GetPcbnewTestDataDir()
567 + "plugins/altium/issue24456/Fastino_Ground_Isolator.PcbDoc";
568
569 std::unique_ptr<BOARD> board = std::make_unique<BOARD>();
570 m_altiumPlugin.LoadBoard( dataPath, board.get(), nullptr );
571 BOOST_REQUIRE( board );
572
573 std::vector<PCB_TEXT*> copperTexts;
574 std::vector<PCB_TEXT*> maskTexts;
575
576 for( BOARD_ITEM* item : board->Drawings() )
577 {
578 if( item->Type() != PCB_TEXT_T )
579 continue;
580
581 PCB_TEXT* text = static_cast<PCB_TEXT*>( item );
582 PCB_LAYER_ID layer = text->GetLayer();
583
584 if( IsCopperLayer( layer ) )
585 copperTexts.push_back( text );
586 else if( layer == F_Mask || layer == B_Mask )
587 maskTexts.push_back( text );
588 }
589
590 BOOST_REQUIRE_MESSAGE( !copperTexts.empty(), "Board must contain copper-layer text" );
591 BOOST_REQUIRE_MESSAGE( !maskTexts.empty(), "Board must contain soldermask-layer text" );
592
593 // A copper string and a mask string are the same logical label when they share text, rotation
594 // and mirroring. KiCad's IU tolerance for "coincident" is tight; before the fix the gap was
595 // 50000-75000 IU (0.05-0.075 mm).
596 const int tolerance = 1000; // 1 micron
597
598 int matchedPairs = 0;
599
600 for( PCB_TEXT* copper : copperTexts )
601 {
602 for( PCB_TEXT* mask : maskTexts )
603 {
604 if( copper->GetText() != mask->GetText()
605 || copper->GetTextAngle() != mask->GetTextAngle()
606 || copper->IsMirrored() != mask->IsMirrored() )
607 {
608 continue;
609 }
610
611 // Only treat them as the same label when they are already near each other; distinct
612 // labels that happen to share a glyph (e.g. several "+" pads) must not be cross-matched.
613 VECTOR2I delta = copper->GetTextPos() - mask->GetTextPos();
614
615 if( std::abs( delta.x ) > 500000 || std::abs( delta.y ) > 500000 )
616 continue;
617
618 matchedPairs++;
619
620 BOOST_CHECK_MESSAGE(
621 std::abs( delta.x ) <= tolerance && std::abs( delta.y ) <= tolerance,
622 wxString::Format( "Copper/mask copies of '%s' diverge by (%d, %d) IU",
623 copper->GetText(), delta.x, delta.y ) );
624 }
625 }
626
627 BOOST_CHECK_MESSAGE( matchedPairs > 0,
628 "Expected at least one coincident copper/soldermask text pair" );
629}
630
631
642BOOST_AUTO_TEST_CASE( LengthTuningPatterns )
643{
644 std::string dataPath =
645 KI_TEST::GetPcbnewTestDataDir() + "plugins/altium/issue24654/PCB1.PcbDoc";
646
647 std::unique_ptr<BOARD> board = std::make_unique<BOARD>();
648 m_altiumPlugin.LoadBoard( dataPath, board.get(), nullptr );
649
650 BOOST_REQUIRE( board );
651
652 int tuningCount = 0;
653 int singleCount = 0;
654 int diffPairCount = 0;
655
656 for( PCB_GENERATOR* generator : board->Generators() )
657 {
658 PCB_TUNING_PATTERN* pattern = dynamic_cast<PCB_TUNING_PATTERN*>( generator );
659
660 if( !pattern )
661 continue;
662
663 tuningCount++;
664
665 // Each pattern must wrap the real imported copper and carry Altium's meander parameters.
666 BOOST_CHECK_MESSAGE( !pattern->GetBoardItems().empty(),
667 "Imported tuning pattern wraps no copper" );
668 BOOST_CHECK_GT( pattern->GetMaxAmplitude(), 0 );
669 BOOST_CHECK_GT( pattern->GetSpacing(), 0 );
670
671 // The members must be copper tracks/arcs that the importer placed on the board.
672 std::set<int> memberNets;
673
674 for( BOARD_ITEM* item : pattern->GetBoardItems() )
675 {
676 BOOST_CHECK( item->Type() == PCB_TRACE_T || item->Type() == PCB_ARC_T );
677 BOOST_CHECK( item->GetParentGroup() == pattern );
678
679 if( BOARD_CONNECTED_ITEM* bci = dynamic_cast<BOARD_CONNECTED_ITEM*>( item ) )
680 memberNets.insert( bci->GetNetCode() );
681 }
682
683 if( pattern->GetTuningMode() == DIFF_PAIR )
684 {
685 diffPairCount++;
686
687 // A differential-pair meander must keep both nets; collapsing them to one would
688 // corrupt half the routing.
689 BOOST_CHECK_EQUAL( memberNets.size(), 2 );
690 }
691 else if( pattern->GetTuningMode() == SINGLE )
692 {
693 singleCount++;
694 BOOST_CHECK_EQUAL( memberNets.size(), 1 );
695 }
696 }
697
698 BOOST_CHECK_EQUAL( tuningCount, 8 );
699 BOOST_CHECK_EQUAL( singleCount, 4 );
700 BOOST_CHECK_EQUAL( diffPairCount, 4 );
701}
702
703
704// https://gitlab.com/kicad/code/kicad/-/issues/24847
705// Keepout regions defined inside an Altium footprint must be imported at the footprint's board
706// location. The importer keeps the Altium absolute coordinates instead of re-basing them to the
707// footprint origin, so the keepout zone lands far from the footprint that owns it.
708BOOST_AUTO_TEST_CASE( Issue24847_FootprintKeepoutPlacement )
709{
710 std::string dataPath = KI_TEST::GetPcbnewTestDataDir()
711 + "plugins/altium/issue24847/PCB1.PcbDoc";
712
713 std::unique_ptr<BOARD> board = std::make_unique<BOARD>();
714 m_altiumPlugin.LoadBoard( dataPath, board.get(), nullptr );
715
716 BOOST_REQUIRE( board );
717 BOOST_REQUIRE_GT( board->Footprints().size(), 0 );
718
719 int keepoutZoneCount = 0;
720
721 for( FOOTPRINT* footprint : board->Footprints() )
722 {
723 const VECTOR2I fpPos = footprint->GetPosition();
724
725 for( ZONE* zone : footprint->Zones() )
726 {
727 keepoutZoneCount++;
728
729 const VECTOR2I zoneCenter = zone->GetBoundingBox().GetCenter();
730 const double distMm = ( zoneCenter - fpPos ).EuclideanNorm() / 1e6;
731
732 BOOST_TEST_MESSAGE( "footprint " << footprint->GetReference().ToStdString()
733 << " at (" << fpPos.x << "," << fpPos.y
734 << ") keepout center (" << zoneCenter.x << ","
735 << zoneCenter.y << ") dist " << distMm << " mm" );
736
737 // A footprint-local keepout sits on its footprint anchor; the pre-fix regression put
738 // it over 100mm away, so anything past a couple of mm is a placement failure.
739 BOOST_CHECK_MESSAGE( distMm < 2.0,
740 "Keepout in footprint " << footprint->GetReference().ToStdString()
741 << " is " << distMm << " mm from its footprint origin" );
742 }
743 }
744
745 // PCB1.PcbDoc carries exactly three footprint-local keepouts (Z1, Z2, R1); dropping any is a
746 // regression the placement check alone would not catch.
747 BOOST_CHECK_EQUAL( keepoutZoneCount, 3 );
748}
749
750
751// https://gitlab.com/kicad/code/kicad/-/issues/13750
752// Copper regions can carry a soldermask relief; the importer dropped it, losing the aperture
753BOOST_AUTO_TEST_CASE( RegionSolderMaskExpansion )
754{
755 std::string dataPath = KI_TEST::GetPcbnewTestDataDir()
756 + "plugins/altium/issue13750/"
757 "altium2kicad_region_soldermask_expansion.PcbDoc";
758
759 std::unique_ptr<BOARD> board = std::make_unique<BOARD>();
760 m_altiumPlugin.LoadBoard( dataPath, board.get(), nullptr );
761 BOOST_REQUIRE( board );
762
763 std::vector<PCB_SHAPE*> copperPolys;
764 std::vector<PCB_SHAPE*> maskPolys;
765
766 for( BOARD_ITEM* item : board->Drawings() )
767 {
768 if( item->Type() != PCB_SHAPE_T )
769 continue;
770
771 PCB_SHAPE* shape = static_cast<PCB_SHAPE*>( item );
772
773 if( shape->GetShape() != SHAPE_T::POLY )
774 continue;
775
776 if( shape->GetLayer() == F_Cu )
777 copperPolys.push_back( shape );
778 else if( shape->GetLayer() == F_Mask )
779 maskPolys.push_back( shape );
780 }
781
782 // Two top-copper regions, exactly one of which carries the soldermask relief
783 BOOST_REQUIRE_EQUAL( copperPolys.size(), 2 );
784 BOOST_REQUIRE_EQUAL( maskPolys.size(), 1 );
785
786 // Zero expansion here, so the mask aperture must exactly match its region, not just overlap it,
787 // and must match exactly one region, else an off-by-one primitive association would still pass
788 PCB_SHAPE* mask = maskPolys.front();
789 int exactMatches = 0;
790
791 for( PCB_SHAPE* copper : copperPolys )
792 {
793 SHAPE_POLY_SET missing = copper->GetPolyShape().CloneDropTriangulation();
794 missing.BooleanSubtract( mask->GetPolyShape() );
795
797 extra.BooleanSubtract( copper->GetPolyShape() );
798
799 if( missing.IsEmpty() && extra.IsEmpty() )
800 exactMatches++;
801 }
802
803 BOOST_CHECK_MESSAGE( exactMatches == 1, "F_Mask relief aperture must exactly match exactly one copper region" );
804}
805
806
bool altiumScopeExprMatchesPolygon(const wxString &aExpr)
Return true if an Altium rule scope expression targets polygon pour primitives (matches InPolygon,...
const ARULE6 * selectAltiumPolygonRule(const std::vector< ARULE6 > &aRulesByPriorityAsc)
Select the highest Altium-priority rule whose scope references polygons.
bool altiumViaSideIsTented(bool aTentFlag, bool aManual, bool aFromHole, uint32_t aHoleSize, int32_t aMaskExpansion, int aLandDiameter)
Decide whether one side of an Altium via should be tented when imported into KiCad.
General utilities for PCB file IO for QA programs.
@ BS_ITEM_TYPE_DIELECTRIC
A base class derived from BOARD_ITEM for items that can be connected and have a net,...
A base class for any item which can be embedded within the BOARD container class, and therefore insta...
Definition board_item.h:83
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
SHAPE_POLY_SET & GetPolyShape()
SHAPE_T GetShape() const
Definition eda_shape.h:185
A mix-in class (via multiple inheritance) that handles texts such as labels, parts,...
Definition eda_text.h:89
A collection of nets and the parameters used to route or test these nets.
Definition netclass.h:38
static const char Default[]
the name of the default NETCLASS
Definition netclass.h:40
const wxString GetName() const
Gets the name of this (maybe aggregate) netclass in a format for internal usage or for export to exte...
Definition netclass.cpp:354
Handle the data for a net.
Definition netinfo.h:46
std::unordered_set< BOARD_ITEM * > GetBoardItems() const
Definition pcb_group.cpp:98
BOARD * LoadBoard(const wxString &aFileName, BOARD *aAppendToMe, const std::map< std::string, UTF8 > *aProperties, PROJECT *aProject=nullptr) override
Load information from some input file format that this PCB_IO implementation knows about into either ...
PCB_LAYER_ID GetLayer() const override
Return the primary layer this item is on.
Definition pcb_shape.h:68
LENGTH_TUNING_MODE GetTuningMode() const
Container for project specific data.
Definition project.h:63
bool LoadProject(const wxString &aFullPath, bool aSetActive=true)
Load a project or sets up a new project with a specified path.
PROJECT & Prj() const
A helper while we are not MDI-capable – return the one and only project.
Represent a set of closed polygons.
bool IsEmpty() const
Return true if the set is empty (no polygons at all)
SHAPE_POLY_SET CloneDropTriangulation() const
void BooleanSubtract(const SHAPE_POLY_SET &b)
Perform boolean polyset difference.
Handle a list of polygons defining a copper zone.
Definition zone.h:70
wxString ExpandTextVars(const wxString &aSource, const PROJECT *aProject, int aFlags)
Definition common.cpp:59
The common library.
bool IsCopperLayer(int aLayerId)
Test whether a layer is a copper layer.
Definition layer_ids.h:683
PCB_LAYER_ID
A quick note on layer IDs:
Definition layer_ids.h:56
@ B_Mask
Definition layer_ids.h:94
@ F_Mask
Definition layer_ids.h:93
@ 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
@ DIFF_PAIR
ALTIUM_PCB_IMPORT_FIXTURE()=default
PCB_IO_ALTIUM_DESIGNER m_altiumPlugin
wxString scope1expr
wxString scope2expr
BOOST_AUTO_TEST_CASE(BoardLoadNoAssertions)
Test basic board loading - verifies that the Altium import doesn't trigger any assertions during the ...
static void checkAllCopperFillZonesHaveClearance(PCB_IO_ALTIUM_DESIGNER &aPlugin, const std::string &aRelativePath)
Verify that copper zones in imported Altium boards have non-zero local clearance values derived from ...
BOOST_AUTO_TEST_CASE(HorizontalAlignment)
BOOST_REQUIRE(intersection.has_value()==c.ExpectedIntersection.has_value())
BOOST_AUTO_TEST_SUITE_END()
BOOST_TEST_MESSAGE("Polyline has "<< chain.PointCount()<< " points")
BOOST_CHECK_EQUAL(result, "25.4")
int delta
@ PCB_SHAPE_T
class PCB_SHAPE, a segment not on copper layers
Definition typeinfo.h:81
@ 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