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
34
35#include <base_units.h>
36#include <compoundfilereader.h>
37
38#include <board.h>
41#include <footprint.h>
43#include <common.h>
44#include <core/utf8.h>
45#include <eda_text.h>
46#include <embedded_files.h>
47#include <ki_exception.h>
48#include <netinfo.h>
49#include <netclass.h>
50#include <pad.h>
51#include <pcb_track.h>
52#include <pcb_shape.h>
53#include <pcb_generator.h>
56#include <project.h>
57#include <pcb_text.h>
60#include <zone.h>
61
62#include <algorithm>
63#include <cmath>
64#include <cstring>
65#include <map>
66#include <set>
67#include <string>
68#include <vector>
69
70
72{
74
76 const CFB::COMPOUND_FILE_ENTRY* aEntry )
77 {
78 aImporter.ParseExtendedPrimitiveInformationData( aFile, aEntry );
79 }
80
81 static std::vector<std::pair<PCB_LAYER_ID, int>> getMaskExpansions( ALTIUM_PCB& aImporter, ALTIUM_RECORD aType,
82 int aPrimitiveIndex, ALTIUM_LAYER aLayer )
83 {
84 return aImporter.HelperGetSolderAndPasteMaskExpansions( aType, aPrimitiveIndex, aLayer );
85 }
86
87 static void convertConnectedArc( ALTIUM_PCB& aImporter, const AARC6& aArc, PCB_LAYER_ID aLayer )
88 {
90 aImporter.ConvertArcs6ToBoardItemOnLayer( aArc, aLayer );
91 }
92
93 static std::vector<const ZONE*> collectRuleAreas( const BOARD& aBoard )
94 {
95 std::vector<const ZONE*> ruleAreas;
96
97 for( const ZONE* zone : aBoard.Zones() )
98 {
99 if( zone->GetIsRuleArea() )
100 ruleAreas.push_back( zone );
101 }
102
103 for( const FOOTPRINT* footprint : aBoard.Footprints() )
104 {
105 for( const ZONE* zone : footprint->Zones() )
106 {
107 if( zone->GetIsRuleArea() )
108 ruleAreas.push_back( zone );
109 }
110 }
111
112 return ruleAreas;
113 }
114
115 static int getNetCode( ALTIUM_PCB& aImporter, const std::vector<int>& aNetcodes, uint16_t aId )
116 {
117 aImporter.m_altiumToKicadNetcodes = aNetcodes;
118 return aImporter.GetNetCode( aId );
119 }
120
122};
123
124
125BOOST_FIXTURE_TEST_SUITE( AltiumPcbImport, ALTIUM_PCB_IMPORT_FIXTURE )
126
127
128// An Altium net id equal to the number of known nets used to index one past the end
129BOOST_AUTO_TEST_CASE( NetCodeRejectsIdPastEnd )
130{
131 BOARD board;
132 LAYER_MAPPING_HANDLER layerMappingHandler;
133 ALTIUM_PCB importer( &board, nullptr, layerMappingHandler );
134
135 const std::vector<int> netcodes = { 1, 2, 3 };
136
137 BOOST_CHECK_EQUAL( getNetCode( importer, netcodes, 2 ), 3 );
138 BOOST_CHECK_THROW( getNetCode( importer, netcodes, 3 ), IO_ERROR );
139 BOOST_CHECK_EQUAL( getNetCode( importer, netcodes, ALTIUM_NET_UNCONNECTED ),
141}
142
143
144BOOST_AUTO_TEST_CASE( DimensionBoldPropertyParsing )
145{
146 std::string properties = "|BOLD=TRUE|TEXTLINEWIDTH=6mil";
147 properties.push_back( '\0' );
148
149 const uint16_t recordHeader = 1;
150 const uint32_t propertySize = static_cast<uint32_t>( properties.size() );
151 const size_t recordSize = sizeof( recordHeader ) + sizeof( propertySize ) + properties.size();
152
153 std::unique_ptr<char[]> content = std::make_unique<char[]>( recordSize );
154
155 std::memcpy( content.get(), &recordHeader, sizeof( recordHeader ) );
156 std::memcpy( content.get() + sizeof( recordHeader ), &propertySize, sizeof( propertySize ) );
157 std::memcpy( content.get() + sizeof( recordHeader ) + sizeof( propertySize ), properties.data(),
158 properties.size() );
159
160 ALTIUM_BINARY_PARSER reader( content, recordSize );
161 ADIMENSION6 dimension( reader );
162
163 BOOST_CHECK( dimension.textbold );
164 BOOST_CHECK_EQUAL( dimension.textlinewidth, pcbIUScale.mmToIU( 0.1524 ) );
165 BOOST_CHECK( !reader.HasParsingError() );
167}
168
169
170// Altium stores no per-component mounting style, so derive it from the pads. Leaving it unset
171// drops every footprint from SMD-only placement files and from ODB++/IPC-2581 mount type
172BOOST_AUTO_TEST_CASE( FootprintMountingStyleDerivedFromPads )
173{
174 std::string dataPath = KI_TEST::GetPcbnewTestDataDir() + "plugins/altium/HiFive/HiFive1.B01.PcbDoc";
175
176 std::unique_ptr<BOARD> board = m_altiumPlugin.LoadBoard( dataPath, nullptr );
177 BOOST_REQUIRE( board );
178 BOOST_REQUIRE( !board->Footprints().empty() );
179
180 int smd = 0;
181 int throughHole = 0;
182
183 for( FOOTPRINT* footprint : board->Footprints() )
184 {
185 const int expected = footprint->GetLikelyAttribute();
186 const int actual = footprint->GetAttributes() & ( FP_SMD | FP_THROUGH_HOLE );
187
188 // Matching the footprint checker's own heuristic is what keeps DRC from reporting a type
189 // mismatch on every footprint we just imported
190 BOOST_CHECK_MESSAGE( actual == expected,
191 wxString::Format( wxT( "Footprint %s has mounting style %d, expected %d" ),
192 footprint->GetReference(), actual, expected ) );
193
194 if( expected == FP_SMD )
195 smd++;
196 else if( expected == FP_THROUGH_HOLE )
197 throughHole++;
198 }
199
200 // Guard against the loop above passing because nothing was classified at all
201 BOOST_CHECK_GT( smd, 0 );
202 BOOST_CHECK_GT( throughHole, 0 );
203}
204
205
206BOOST_AUTO_TEST_CASE( ArcMaskExpansionUsesArcMetadata )
207{
208 std::string dataPath = KI_TEST::GetPcbnewTestDataDir() + "plugins/altium/issue24456/Fastino_Ground_Isolator.PcbDoc";
209
210 ALTIUM_PCB_COMPOUND_FILE file( wxString::FromUTF8( dataPath.c_str() ) );
211 const CFB::COMPOUND_FILE_ENTRY* data = file.FindStream( { "ExtendedPrimitiveInformation", "Data" } );
212
213 BOOST_REQUIRE( data );
214
215 BOARD board;
216 LAYER_MAPPING_HANDLER layerMappingHandler;
217 ALTIUM_PCB importer( &board, nullptr, layerMappingHandler );
218
219 parseExtendedPrimitiveInformation( importer, file, data );
220
221 std::vector<std::pair<PCB_LAYER_ID, int>> front =
222 getMaskExpansions( importer, ALTIUM_RECORD::ARC, 6, ALTIUM_LAYER::TOP_LAYER );
223
224 BOOST_REQUIRE_EQUAL( front.size(), 1 );
225 BOOST_CHECK_EQUAL( front[0].first, F_Paste );
226 BOOST_CHECK_EQUAL( front[0].second, pcbIUScale.mmToIU( -1.0 ) );
227
228 std::vector<std::pair<PCB_LAYER_ID, int>> back =
229 getMaskExpansions( importer, ALTIUM_RECORD::ARC, 70, ALTIUM_LAYER::BOTTOM_LAYER );
230
231 BOOST_REQUIRE_EQUAL( back.size(), 1 );
232 BOOST_CHECK_EQUAL( back[0].first, B_Paste );
233 BOOST_CHECK_EQUAL( back[0].second, pcbIUScale.mmToIU( -1.0 ) );
234}
235
236
237BOOST_AUTO_TEST_CASE( ConnectedFullCircleArcPreservesRingGeometry )
238{
239 constexpr uint32_t subrecordSize = 57;
240 constexpr size_t recordSize = 1 + sizeof( subrecordSize ) + subrecordSize;
241 std::unique_ptr<char[]> content = std::make_unique<char[]>( recordSize );
242 size_t offset = 0;
243
244 auto append = [&]( auto aValue )
245 {
246 std::memcpy( content.get() + offset, &aValue, sizeof( aValue ) );
247 offset += sizeof( aValue );
248 };
249
250 append( static_cast<uint8_t>( ALTIUM_RECORD::ARC ) );
251 append( subrecordSize );
252 append( static_cast<uint8_t>( ALTIUM_LAYER::TOP_LAYER ) );
253 append( uint8_t{ 0 } );
254 append( uint8_t{ 0 } );
255 append( uint16_t{ 0 } );
256 append( static_cast<uint16_t>( ALTIUM_POLYGON_NONE ) );
257 append( static_cast<uint16_t>( ALTIUM_COMPONENT_NONE ) );
258 append( uint32_t{ 0 } );
259 append( int32_t{ 0 } );
260 append( int32_t{ 0 } );
261 append( uint32_t{ 100000 } );
262 append( double{ 0.0 } );
263 append( double{ 360.0 } );
264 append( uint32_t{ 10000 } );
265 append( uint16_t{ 0 } );
266 append( uint8_t{ 0 } );
267 append( uint32_t{ 0 } );
268 append( static_cast<uint32_t>( ALTIUM_LAYER::UNKNOWN ) );
269 append( uint8_t{ 0 } );
270
271 BOOST_REQUIRE_EQUAL( offset, recordSize );
272
273 ALTIUM_BINARY_PARSER reader( content, recordSize );
274 AARC6 sourceArc( reader );
275 BOARD board;
276 LAYER_MAPPING_HANDLER layerMappingHandler;
277 ALTIUM_PCB importer( &board, nullptr, layerMappingHandler );
278
279 convertConnectedArc( importer, sourceArc, F_Cu );
280
281 BOOST_REQUIRE_EQUAL( board.Tracks().size(), 2 );
282
283 std::vector<const PCB_ARC*> arcs;
284
285 for( const PCB_TRACK* track : board.Tracks() )
286 {
287 BOOST_REQUIRE_EQUAL( track->Type(), PCB_ARC_T );
288 arcs.push_back( static_cast<const PCB_ARC*>( track ) );
289 }
290
291 for( const PCB_ARC* arc : arcs )
292 {
293 BOOST_CHECK_CLOSE( std::abs( arc->GetAngle().AsDegrees() ), 180.0, 1e-6 );
294 BOOST_CHECK_CLOSE( arc->GetRadius(), pcbIUScale.mmToIU( 0.254 ), 1e-6 );
295 BOOST_CHECK_EQUAL( arc->GetWidth(), pcbIUScale.mmToIU( 0.0254 ) );
296 BOOST_CHECK_EQUAL( arc->GetLayer(), F_Cu );
297 }
298
299 BOOST_CHECK( arcs[0]->GetStart() == arcs[1]->GetEnd() );
300 BOOST_CHECK( arcs[0]->GetEnd() == arcs[1]->GetStart() );
301 BOOST_CHECK( board.Drawings().empty() );
302}
303
304
309BOOST_AUTO_TEST_CASE( BoardLoadNoAssertions )
310{
311 std::string dataPath = KI_TEST::GetPcbnewTestDataDir()
312 + "plugins/altium/eDP_adapter_dvt1_source/eDP_adapter_dvt1.PcbDoc";
313
314 // Load the board - should not trigger any assertions
315 std::unique_ptr<BOARD> board = m_altiumPlugin.LoadBoard( dataPath, nullptr );
316
317 BOOST_REQUIRE( board );
318
319 // Basic sanity checks
320 BOOST_CHECK( board->GetNetCount() > 0 );
321 BOOST_CHECK( board->Footprints().size() > 0 );
322}
323
324
325// A courtyard must be one closed outline, so stray open pieces cannot stay on the courtyard layer
326BOOST_AUTO_TEST_CASE( CourtyardsAreClosed )
327{
328 int closed = 0;
329
330 for( const char* name : { "HiFive/HiFive1.B01.PcbDoc", "eDP_adapter_dvt1_source/eDP_adapter_dvt1.PcbDoc",
331 "issue24456/Fastino_Ground_Isolator.PcbDoc", "issue25362/OpenRex_V1I1_PCB_reduced.PcbDoc" } )
332 {
333 std::unique_ptr<BOARD> board = m_altiumPlugin.LoadBoard( KI_TEST::GetPcbnewTestDataDir() + "plugins/altium/" + name,
334 nullptr );
335
336 BOOST_REQUIRE( board );
337
338 int open = 0;
339
340 for( FOOTPRINT* footprint : board->Footprints() )
341 {
342 OUTLINE_ERROR_HANDLER onError = [&]( const wxString&, BOARD_ITEM*, BOARD_ITEM*, const VECTOR2I& )
343 {
344 open++;
345 };
346
347 footprint->BuildCourtyardCaches( &onError );
348
349 closed += footprint->GetCourtyard( F_CrtYd ).OutlineCount();
350 closed += footprint->GetCourtyard( B_CrtYd ).OutlineCount();
351 }
352
353 BOOST_TEST_CONTEXT( name ) { BOOST_CHECK_EQUAL( open, 0 ); }
354 }
355
356 // An outline moved off the layer would also report no error, so closed outlines must remain
357 BOOST_CHECK_GT( closed, 0 );
358}
359
360
361// A board outline with jogs from rounding in the file must still build into one closed shape
362BOOST_AUTO_TEST_CASE( OutlineBuilds )
363{
364 for( const char* name : { "HiFive/HiFive1.B01.PcbDoc", "eDP_adapter_dvt1_source/eDP_adapter_dvt1.PcbDoc",
365 "issue13750/altium2kicad_region_soldermask_expansion.PcbDoc",
366 "issue24456/Fastino_Ground_Isolator.PcbDoc", "issue24654/PCB1.PcbDoc",
367 "issue24847/PCB1.PcbDoc", "issue25362/OpenRex_V1I1_PCB_reduced.PcbDoc" } )
368 {
369 std::unique_ptr<BOARD> board = m_altiumPlugin.LoadBoard( KI_TEST::GetPcbnewTestDataDir() + "plugins/altium/" + name,
370 nullptr );
371
372 BOOST_REQUIRE( board );
373
374 int errors = 0;
375 SHAPE_POLY_SET outline;
376
377 OUTLINE_ERROR_HANDLER onError = [&]( const wxString&, BOARD_ITEM*, BOARD_ITEM*, const VECTOR2I& )
378 {
379 errors++;
380 };
381
382 // An inferred outline would hide an import that lost the edge shapes
383 const bool built = BuildBoardPolygonOutlines( board.get(), outline, pcbIUScale.mmToIU( 0.05 ),
384 board->GetOutlinesChainingEpsilon(), false, &onError );
385
387 {
388 BOOST_CHECK( built );
389 BOOST_CHECK_GT( outline.OutlineCount(), 0 );
390 BOOST_CHECK_EQUAL( errors, 0 );
391 }
392 }
393}
394
395
396// GetImportedCachedLibraryFootprints() is caller-owns, so Altium must clone rather than alias
397// aliasing would let the reconciler (takes ownership) double-free the board
398BOOST_AUTO_TEST_CASE( CachedLibraryFootprintsAreOwnedCopies )
399{
400 std::string dataPath =
401 KI_TEST::GetPcbnewTestDataDir() + "plugins/altium/HiFive/HiFive1.B01.PcbDoc";
402
403 std::unique_ptr<BOARD> board = m_altiumPlugin.LoadBoard( dataPath, nullptr );
404
405 BOOST_REQUIRE( board );
406 BOOST_REQUIRE_GT( board->Footprints().size(), 0 );
407
408 std::vector<FOOTPRINT*> cached = m_altiumPlugin.GetImportedCachedLibraryFootprints();
409
410 // adopt ownership so the clones free with the test
411 std::vector<std::unique_ptr<FOOTPRINT>> owned;
412
413 for( FOOTPRINT* fp : cached )
414 owned.emplace_back( fp );
415
416 BOOST_CHECK_EQUAL( cached.size(), board->Footprints().size() );
417
418 std::set<FOOTPRINT*> boardFootprints( board->Footprints().begin(), board->Footprints().end() );
419
420 // no returned footprint may alias a board-owned one
421 for( FOOTPRINT* fp : cached )
422 {
423 BOOST_CHECK_MESSAGE( boardFootprints.count( fp ) == 0,
424 "GetImportedCachedLibraryFootprints returned a board-owned footprint" );
425 }
426}
427
428
434BOOST_AUTO_TEST_CASE( UnusedInternalPlanesNotMapped )
435{
436 std::string dataPath =
437 KI_TEST::GetPcbnewTestDataDir() + "plugins/altium/eDP_adapter_dvt1_source/eDP_adapter_dvt1.PcbDoc";
438
439 std::set<wxString> mappedLayerNames;
440
441 m_altiumPlugin.RegisterCallback(
442 [&]( const std::vector<INPUT_LAYER_DESC>& aLayers )
443 {
444 for( const INPUT_LAYER_DESC& layer : aLayers )
445 mappedLayerNames.insert( layer.Name );
446
448 } );
449
450 std::unique_ptr<BOARD> board = m_altiumPlugin.LoadBoard( dataPath, nullptr );
451
452 BOOST_REQUIRE( board );
453 BOOST_CHECK( mappedLayerNames.count( wxS( "Internal Plane 1" ) ) );
454 BOOST_CHECK( mappedLayerNames.count( wxS( "Internal Plane 2" ) ) );
455
456 for( int plane = 3; plane <= 16; ++plane )
457 {
458 wxString name = wxString::Format( wxS( "Internal Plane %d" ), plane );
459 BOOST_CHECK_MESSAGE( !mappedLayerNames.count( name ),
460 wxString::Format( "Unused layer '%s' reached the mapper", name ) );
461 }
462}
463
464
472BOOST_AUTO_TEST_CASE( KeepoutRestrictionsImpliedByLayer )
473{
474 std::string dataPath =
475 KI_TEST::GetPcbnewTestDataDir() + "plugins/altium/eDP_adapter_dvt1_source/eDP_adapter_dvt1.PcbDoc";
476
477 std::unique_ptr<BOARD> board = m_altiumPlugin.LoadBoard( dataPath, nullptr );
478
479 BOOST_REQUIRE( board );
480
481 int planeSplits = 0;
482 int keepOutLayer = 0;
483 int inert = 0;
484
485 for( const ZONE* zone : collectRuleAreas( *board ) )
486 {
487 const bool tracks = zone->GetDoNotAllowTracks();
488 const bool vias = zone->GetDoNotAllowVias();
489 const bool pads = zone->GetDoNotAllowPads();
490 const bool fills = zone->GetDoNotAllowZoneFills();
491 const bool footprints = zone->GetDoNotAllowFootprints();
492
493 if( fills && !tracks && !vias && !pads && !footprints )
494 planeSplits++;
495 else if( fills && tracks && vias && pads && !footprints )
496 keepOutLayer++;
497 else if( !fills && !tracks && !vias && !pads && !footprints )
498 inert++;
499 }
500
501 BOOST_CHECK_EQUAL( inert, 0 );
502 BOOST_CHECK_EQUAL( planeSplits, 87 );
503 BOOST_CHECK_EQUAL( keepOutLayer, 8 );
504}
505
506
514BOOST_AUTO_TEST_CASE( ExplicitKeepoutRestrictionMaskIsRead )
515{
516 std::string dataPath = KI_TEST::GetPcbnewTestDataDir() + "plugins/altium/issue24456/Fastino_Ground_Isolator.PcbDoc";
517
518 std::unique_ptr<BOARD> board = m_altiumPlugin.LoadBoard( dataPath, nullptr );
519
520 BOOST_REQUIRE( board );
521
522 int viasOnly = 0;
523 int everything = 0;
524
525 for( const ZONE* zone : collectRuleAreas( *board ) )
526 {
527 const bool tracks = zone->GetDoNotAllowTracks();
528 const bool vias = zone->GetDoNotAllowVias();
529 const bool pads = zone->GetDoNotAllowPads();
530 const bool fills = zone->GetDoNotAllowZoneFills();
531
532 if( vias && !tracks && !pads && !fills )
533 viasOnly++;
534 else if( vias && tracks && pads && fills )
535 everything++;
536 }
537
538 BOOST_CHECK_EQUAL( everything, 0 );
539 BOOST_CHECK_EQUAL( viasOnly, 2 );
540}
541
542
550BOOST_AUTO_TEST_CASE( NetclassAssignment )
551{
552 // HiFive1.B01.PcbDoc has Altium netclass definitions
553 std::string dataPath = KI_TEST::GetPcbnewTestDataDir() + "plugins/altium/HiFive/HiFive1.B01.PcbDoc";
554
555 std::unique_ptr<BOARD> board = m_altiumPlugin.LoadBoard( dataPath, nullptr );
556
557 BOOST_REQUIRE( board );
558
559 // Get the net settings which contains pattern assignments
560 std::shared_ptr<NET_SETTINGS> netSettings = board->GetDesignSettings().m_NetSettings;
561
562 BOOST_REQUIRE( netSettings );
563
564 // Check if there are any pattern assignments in the board
565 auto& patternAssignments = netSettings->GetNetclassPatternAssignments();
566
567 // The HiFive board should have netclass definitions - require this for the test to be meaningful
568 BOOST_REQUIRE_MESSAGE( patternAssignments.size() > 0,
569 "Test file must have netclass pattern assignments" );
570
571 // For each net that has a pattern assignment, verify that the NETINFO_ITEM
572 // has a netclass directly assigned (not just through pattern resolution)
573 bool foundAssignedNet = false;
574
575 for( NETINFO_ITEM* net : board->GetNetInfo() )
576 {
577 if( net->GetNetCode() <= 0 )
578 continue;
579
580 // Get the netclass directly from the NETINFO_ITEM
581 NETCLASS* directNetclass = net->GetNetClass();
582
583 // Get the effective netclass from pattern resolution
584 std::shared_ptr<NETCLASS> effectiveNetclass =
585 netSettings->GetEffectiveNetClass( net->GetNetname() );
586
587 // If this net has a non-default effective netclass, the direct assignment
588 // should also be non-default (this is what the fix ensures)
589 if( effectiveNetclass && effectiveNetclass->GetName() != NETCLASS::Default )
590 {
591 BOOST_CHECK_MESSAGE(
592 directNetclass != nullptr,
593 wxString::Format( "Net '%s' should have a direct netclass assignment",
594 net->GetNetname() ) );
595
596 if( directNetclass )
597 {
598 foundAssignedNet = true;
599
600 // The direct netclass should match what effective resolution returns
601 // (or be part of the effective class for multi-netclass scenarios)
602 BOOST_CHECK_MESSAGE(
603 directNetclass->GetName() != NETCLASS::Default,
604 wxString::Format( "Net '%s' should not have default netclass, "
605 "expected effective class or component",
606 net->GetNetname() ) );
607 }
608 }
609 }
610
611 // If there were pattern assignments, we should have found at least one assigned net
612 BOOST_CHECK_MESSAGE( foundAssignedNet,
613 "At least one net should have a non-default netclass assigned" );
614}
615
616
622BOOST_AUTO_TEST_CASE( UnnamedNetPlaceholderAvoidsCollisions )
623{
624 std::unique_ptr<BOARD> board = std::make_unique<BOARD>();
625 int counter = 0;
626
627 wxString first = AltiumUnnamedNetName( *board, counter );
628 BOOST_CHECK_EQUAL( first, wxT( "__ALTIUM_UNNAMED_NET_1" ) );
629
630 board->Add( new NETINFO_ITEM( board.get(), first, -1 ), ADD_MODE::APPEND );
631 board->Add( new NETINFO_ITEM( board.get(), wxT( "__ALTIUM_UNNAMED_NET_2" ), -1 ),
633
634 wxString second = AltiumUnnamedNetName( *board, counter );
635 BOOST_CHECK_EQUAL( second, wxT( "__ALTIUM_UNNAMED_NET_3" ) );
636
637 board->Add( new NETINFO_ITEM( board.get(), second, -1 ), ADD_MODE::APPEND );
638
639 // Three distinct nets plus the unconnected net; a collision would have aliased one away
640 BOOST_CHECK_EQUAL( board->GetNetInfo().GetNetCount(), 4 );
641}
642
643
650BOOST_AUTO_TEST_CASE( RulesPopulateNetclasses )
651{
652 std::string dataPath =
653 KI_TEST::GetPcbnewTestDataDir() + "plugins/altium/issue24456/Fastino_Ground_Isolator.PcbDoc";
654
655 std::unique_ptr<BOARD> board = m_altiumPlugin.LoadBoard( dataPath, nullptr );
656
657 std::shared_ptr<NET_SETTINGS> netSettings = board->GetDesignSettings().m_NetSettings;
658 std::shared_ptr<NETCLASS> defaultNetclass = netSettings->GetDefaultNetclass();
659
660 BOOST_CHECK_EQUAL( defaultNetclass->GetTrackWidth(), pcbIUScale.mmToIU( 0.3 ) );
661 BOOST_CHECK_EQUAL( defaultNetclass->GetViaDiameter(), pcbIUScale.mmToIU( 0.6 ) );
662 BOOST_CHECK_EQUAL( defaultNetclass->GetViaDrill(), pcbIUScale.mmToIU( 0.3 ) );
663
664 auto it = netSettings->GetNetclasses().find( wxT( "50R" ) );
665
666 BOOST_REQUIRE( it != netSettings->GetNetclasses().end() );
667 BOOST_CHECK_EQUAL( it->second->GetTrackWidth(), pcbIUScale.mmToIU( 0.35 ) );
668
669 BOOST_CHECK( board->m_LegacyDesignSettingsLoaded );
670}
671
672
679BOOST_AUTO_TEST_CASE( UnresolvedNetclassRulesAreReported )
680{
681 auto messagesFor =
682 [this]( const std::string& aRelativePath )
683 {
685 m_altiumPlugin.SetReporter( &reporter );
686
687 std::unique_ptr<BOARD> board =
688 m_altiumPlugin.LoadBoard( KI_TEST::GetPcbnewTestDataDir() + aRelativePath, nullptr );
689 m_altiumPlugin.SetReporter( nullptr );
690
691 std::vector<wxString> hits;
692
693 for( const KI_TEST::CAPTURING_REPORTER::MESSAGE& message : reporter.GetMessages() )
694 {
695 if( message.text.Contains( wxS( "does not define" ) ) )
696 hits.push_back( message.text );
697 }
698
699 return hits;
700 };
701
702 std::vector<wxString> hiFive = messagesFor( "plugins/altium/HiFive/HiFive1.B01.PcbDoc" );
703
704 BOOST_REQUIRE_EQUAL( hiFive.size(), 1 );
705 BOOST_CHECK( hiFive[0].Contains( wxS( "'PWR'" ) ) );
706
707 // A board whose rules all resolve must not be told anything
708 BOOST_CHECK_EQUAL( messagesFor( "plugins/altium/issue24456/Fastino_Ground_Isolator.PcbDoc" ).size(), 0 );
709}
710
711
717BOOST_AUTO_TEST_CASE( NetclassRulePriority )
718{
719 auto makeWidthRule = []( int aPriority, bool aEnabled, const wxString& aScope2, int aWidth )
720 {
721 ARULE6 rule;
723 rule.priority = aPriority;
724 rule.enabled = aEnabled;
725 rule.scope1expr = wxT( "InNetClass('SIG')" );
726 rule.scope2expr = aScope2;
727 rule.preferredWidth = aWidth;
728 return rule;
729 };
730
731 BOARD board;
732 NET_SETTINGS& netSettings = *board.GetDesignSettings().m_NetSettings;
733 std::shared_ptr<NETCLASS> sig = std::make_shared<NETCLASS>( wxT( "SIG" ), false );
734
735 netSettings.SetNetclass( wxT( "SIG" ), sig );
736
737 // Sorted by priority ascending, matching the order produced by ParseRules6Data
738 std::map<ALTIUM_RULE_KIND, std::vector<ARULE6>> rules;
739 rules[ALTIUM_RULE_KIND::WIDTH] = {
740 makeWidthRule( 1, true, wxT( "All" ), 100 ),
741 makeWidthRule( 2, true, wxT( "All" ), 200 ),
742 };
743
744 ApplyAltiumNetclassRules( rules, netSettings );
745 BOOST_CHECK_EQUAL( sig->GetTrackWidth(), 100 );
746
747 // A disabled priority 1 rule leaves the next one in charge
748 sig->SetTrackWidth( std::optional<int>() );
749 rules[ALTIUM_RULE_KIND::WIDTH][0].enabled = false;
750
751 ApplyAltiumNetclassRules( rules, netSettings );
752 BOOST_CHECK_EQUAL( sig->GetTrackWidth(), 200 );
753
754 // A restricted second scope means the rule is not simply "this netclass"
755 sig->SetTrackWidth( std::optional<int>() );
756 rules[ALTIUM_RULE_KIND::WIDTH][1].scope2expr = wxT( "IsRegion" );
757
758 ApplyAltiumNetclassRules( rules, netSettings );
759 BOOST_CHECK( !sig->HasTrackWidth() );
760}
761
762
769 const std::string& aRelativePath )
770{
771 std::string dataPath = KI_TEST::GetPcbnewTestDataDir() + aRelativePath;
772
773 std::unique_ptr<BOARD> board = aPlugin.LoadBoard( dataPath, nullptr );
774
775 BOOST_REQUIRE( board );
776
777 int fillZoneCount = 0;
778 int fillZonesWithClearance = 0;
779
780 for( ZONE* zone : board->Zones() )
781 {
782 if( !zone->IsOnCopperLayer() || zone->GetIsRuleArea() || zone->IsTeardropArea() )
783 continue;
784
785 fillZoneCount++;
786
787 if( zone->GetLocalClearance().has_value() && zone->GetLocalClearance().value() > 0 )
788 fillZonesWithClearance++;
789 }
790
791 BOOST_CHECK_GT( fillZoneCount, 0 );
792
793 BOOST_CHECK_MESSAGE( fillZonesWithClearance == fillZoneCount,
794 wxString::Format( "%s: %d/%d copper fill zones have clearance set",
795 aRelativePath, fillZonesWithClearance,
796 fillZoneCount ) );
797}
798
799
800BOOST_AUTO_TEST_CASE( ZoneClearances_eDP )
801{
803 m_altiumPlugin, "plugins/altium/eDP_adapter_dvt1_source/eDP_adapter_dvt1.PcbDoc" );
804}
805
806
807BOOST_AUTO_TEST_CASE( ZoneClearances_HiFive )
808{
810 "plugins/altium/HiFive/HiFive1.B01.PcbDoc" );
811}
812
813
821BOOST_AUTO_TEST_CASE( ScopeExprMatchesPolygon )
822{
823 // Positive matches: expressions that reference polygons
824 BOOST_CHECK( altiumScopeExprMatchesPolygon( wxT( "InPolygon" ) ) );
825 BOOST_CHECK( altiumScopeExprMatchesPolygon( wxT( "InPoly" ) ) );
826 BOOST_CHECK( altiumScopeExprMatchesPolygon( wxT( "IsPolygon" ) ) );
827 BOOST_CHECK( altiumScopeExprMatchesPolygon( wxT( "IsPoly" ) ) );
828
829 // Case insensitivity
830 BOOST_CHECK( altiumScopeExprMatchesPolygon( wxT( "inpolygon" ) ) );
831 BOOST_CHECK( altiumScopeExprMatchesPolygon( wxT( "INPOLYGON" ) ) );
832 BOOST_CHECK( altiumScopeExprMatchesPolygon( wxT( "inPOLY" ) ) );
833
834 // Contained within longer expressions
835 BOOST_CHECK( altiumScopeExprMatchesPolygon( wxT( "InPolygon And InNet('GND')" ) ) );
836 BOOST_CHECK( altiumScopeExprMatchesPolygon( wxT( "(InPoly) Or IsVia" ) ) );
837
838 // Negative matches: expressions that don't reference polygons
839 BOOST_CHECK( !altiumScopeExprMatchesPolygon( wxT( "All" ) ) );
840 BOOST_CHECK( !altiumScopeExprMatchesPolygon( wxT( "IsVia" ) ) );
841 BOOST_CHECK( !altiumScopeExprMatchesPolygon( wxT( "IsTrack" ) ) );
842 BOOST_CHECK( !altiumScopeExprMatchesPolygon( wxT( "InNet('GND')" ) ) );
843 BOOST_CHECK( !altiumScopeExprMatchesPolygon( wxT( "InComponent('U1')" ) ) );
844 BOOST_CHECK( !altiumScopeExprMatchesPolygon( wxT( "" ) ) );
845}
846
847
853BOOST_AUTO_TEST_CASE( SelectAltiumPolygonRule_PriorityOrder )
854{
855 auto makeRule = []( int aPriority, const wxString& aScope1, const wxString& aScope2,
856 int aClearance )
857 {
858 ARULE6 rule;
859 rule.priority = aPriority;
860 rule.scope1expr = aScope1;
861 rule.scope2expr = aScope2;
862 rule.clearanceGap = aClearance;
863 return rule;
864 };
865
866 // Sorted by priority ascending, matching the order produced by ParseRules6Data.
867 std::vector<ARULE6> rules = {
868 makeRule( 1, wxT( "InPolygon And InNet('GND')" ), wxT( "All" ), 100 ),
869 makeRule( 2, wxT( "InPolygon" ), wxT( "All" ), 200 ),
870 makeRule( 3, wxT( "All" ), wxT( "All" ), 300 ),
871 makeRule( 4, wxT( "All" ), wxT( "All" ), 400 ),
872 };
873
874 const ARULE6* selected = selectAltiumPolygonRule( rules );
875 BOOST_REQUIRE( selected != nullptr );
876 BOOST_CHECK_EQUAL( selected->priority, 1 );
877 BOOST_CHECK_EQUAL( selected->clearanceGap, 100 );
878
879 rules.erase( rules.begin() );
880 selected = selectAltiumPolygonRule( rules );
881 BOOST_REQUIRE( selected != nullptr );
882 BOOST_CHECK_EQUAL( selected->priority, 2 );
883 BOOST_CHECK_EQUAL( selected->clearanceGap, 200 );
884
885 rules.erase( rules.begin() );
886 BOOST_CHECK( selectAltiumPolygonRule( rules ) == nullptr );
887
888 BOOST_CHECK( selectAltiumPolygonRule( {} ) == nullptr );
889
890 // A rule Altium ignores must not beat the one it actually applies
891 rules = {
892 makeRule( 1, wxT( "InPolygon And InNet('GND')" ), wxT( "All" ), 100 ),
893 makeRule( 2, wxT( "InPolygon" ), wxT( "All" ), 200 ),
894 };
895 rules[0].enabled = false;
896
897 selected = selectAltiumPolygonRule( rules );
898 BOOST_REQUIRE( selected != nullptr );
899 BOOST_CHECK_EQUAL( selected->clearanceGap, 200 );
900}
901
902
913BOOST_AUTO_TEST_CASE( Via_HoleReferencedMaskTenting )
914{
915 std::string dataPath = KI_TEST::GetPcbnewTestDataDir()
916 + "plugins/altium/issue24456/Fastino_Ground_Isolator.PcbDoc";
917
918 std::unique_ptr<BOARD> board = m_altiumPlugin.LoadBoard( dataPath, nullptr );
919
920 BOOST_REQUIRE( board );
921
922 int viaCount = 0;
923 int frontExposed = 0;
924 int backExposed = 0;
925
926 for( PCB_TRACK* track : board->Tracks() )
927 {
928 if( track->Type() != PCB_VIA_T )
929 continue;
930
931 const PCB_VIA* via = static_cast<const PCB_VIA*>( track );
932 viaCount++;
933
934 if( via->GetFrontTentingMode() != TENTING_MODE::TENTED )
935 frontExposed++;
936
937 if( via->GetBackTentingMode() != TENTING_MODE::TENTED )
938 backExposed++;
939 }
940
941 BOOST_REQUIRE_GT( viaCount, 0 );
942
943 // Every via on this board carries a hole-referenced mask opening narrower than its land, so the
944 // importer must tent both sides of all of them.
945 BOOST_CHECK_MESSAGE( frontExposed == 0,
946 wxString::Format( "%d of %d vias left front-exposed despite a "
947 "hole-referenced mask",
948 frontExposed, viaCount ) );
949 BOOST_CHECK_MESSAGE( backExposed == 0,
950 wxString::Format( "%d of %d vias left back-exposed despite a "
951 "hole-referenced mask",
952 backExposed, viaCount ) );
953
954 // Guard the tenting heuristic's boundary cases directly. A wide hole-referenced opening that
955 // clears the land must NOT tent, a land-referenced via must NOT be silently tented, and an
956 // explicit Altium tent flag must always tent regardless of expansion mode.
957 const uint32_t holeSize = 300000; // 0.3mm
958 const int landWidth = 600000; // 0.6mm
959
960 BOOST_CHECK( !altiumViaSideIsTented( /*tentFlag*/ false, /*manual*/ true, /*fromHole*/ true,
961 holeSize, /*expansion*/ 500000, landWidth ) );
962 BOOST_CHECK( !altiumViaSideIsTented( /*tentFlag*/ false, /*manual*/ true, /*fromHole*/ false,
963 holeSize, /*expansion*/ 30000, landWidth ) );
964 BOOST_CHECK( altiumViaSideIsTented( /*tentFlag*/ true, /*manual*/ false, /*fromHole*/ false,
965 holeSize, /*expansion*/ 0, landWidth ) );
966
967 // A narrow hole-referenced opening (hole + 2 * expansion <= land) tents the side.
968 BOOST_CHECK( altiumViaSideIsTented( /*tentFlag*/ false, /*manual*/ true, /*fromHole*/ true,
969 holeSize, /*expansion*/ 30000, landWidth ) );
970}
971
972
973BOOST_AUTO_TEST_CASE( SlotDrillDimensionsSurviveUnsupportedRotation )
974{
975 bool rotationSupported;
976
977 BOOST_CHECK_EQUAL( altiumSlotDrillSize( 550000, 850000, 0.0, rotationSupported ), VECTOR2I( 850000, 550000 ) );
978 BOOST_CHECK( rotationSupported );
979
980 BOOST_CHECK_EQUAL( altiumSlotDrillSize( 550000, 850000, 90.0, rotationSupported ), VECTOR2I( 550000, 850000 ) );
981 BOOST_CHECK( rotationSupported );
982
983 BOOST_CHECK_EQUAL( altiumSlotDrillSize( 550000, 850000, 45.0, rotationSupported ), VECTOR2I( 850000, 550000 ) );
984 BOOST_CHECK( !rotationSupported );
985}
986
987
995BOOST_AUTO_TEST_CASE( StackupDielectricLossTangent )
996{
997 std::string dataPath = KI_TEST::GetPcbnewTestDataDir()
998 + "plugins/altium/issue24456/Fastino_Ground_Isolator.PcbDoc";
999
1000 std::unique_ptr<BOARD> board = m_altiumPlugin.LoadBoard( dataPath, nullptr );
1001
1002 BOOST_REQUIRE( board );
1003
1004 const BOARD_DESIGN_SETTINGS& designSettings = board->GetDesignSettings();
1005 const BOARD_STACKUP& stackup = designSettings.GetStackupDescriptor();
1006
1007 BOOST_REQUIRE( designSettings.m_HasStackup );
1008 BOOST_CHECK( board->GetStackupOrDefault() == stackup );
1009
1010 int dielectricCount = 0;
1011 int dielectricWithTangent = 0;
1012
1013 for( const BOARD_STACKUP_ITEM* item : stackup.GetList() )
1014 {
1015 if( item->GetType() != BS_ITEM_TYPE_DIELECTRIC )
1016 continue;
1017
1018 for( int sub = 0; sub < item->GetSublayersCount(); sub++ )
1019 {
1020 // Only count dielectric sublayers that carry a real dielectric (non-zero thickness)
1021 if( item->GetThickness( sub ) <= 0 )
1022 continue;
1023
1024 dielectricCount++;
1025
1026 double tangent = item->GetLossTangent( sub );
1027
1028 if( tangent > 0. )
1029 {
1030 dielectricWithTangent++;
1031
1032 // Every prepreg/core dielectric in this board uses a 0.020 loss tangent.
1033 BOOST_CHECK_CLOSE( tangent, 0.020, 1e-6 );
1034 }
1035 }
1036 }
1037
1038 BOOST_REQUIRE_GT( dielectricCount, 0 );
1039
1040 // All of the board's substantive dielectrics carry a loss tangent in the Altium source, so
1041 // every imported dielectric sublayer must receive it.
1042 BOOST_CHECK_MESSAGE( dielectricWithTangent == dielectricCount,
1043 wxString::Format( "Only %d of %d dielectric sublayers received a loss "
1044 "tangent from the Altium stackup",
1045 dielectricWithTangent, dielectricCount ) );
1046}
1047
1048
1055BOOST_AUTO_TEST_CASE( ProjectParametersToTextVars )
1056{
1057 std::string dataDir = KI_TEST::GetPcbnewTestDataDir() + "plugins/altium/issue24456/";
1058 std::string pcbDoc = dataDir + "Fastino_Ground_Isolator.PcbDoc";
1059 std::string prjPcb = dataDir + "Fastino_Ground_Isolator.PrjPcb";
1060
1061 SETTINGS_MANAGER settingsManager;
1062 settingsManager.LoadProject( "" );
1063 PROJECT& project = settingsManager.Prj();
1064
1065 std::map<std::string, UTF8> props;
1066 props["project_file"] = prjPcb;
1067
1068 std::unique_ptr<BOARD> board = m_altiumPlugin.LoadBoard( pcbDoc, &props, &project );
1069
1070 const std::map<wxString, wxString>& textVars = project.GetTextVars();
1071
1072 // The parameter that the issue reports as broken must resolve to its value.
1073 BOOST_REQUIRE( textVars.count( wxS( "PCB_REVISION" ) ) );
1074 BOOST_CHECK_EQUAL( textVars.at( wxS( "PCB_REVISION" ) ), wxS( "A" ) );
1075
1076 // A representative sample of the remaining project parameters must all be present.
1077 BOOST_CHECK_EQUAL( textVars.at( wxS( "COMPANY_NAME" ) ), wxS( "ETH Zurich" ) );
1078 BOOST_CHECK_EQUAL( textVars.at( wxS( "PROJECT_NAME" ) ), wxS( "Fastino Ground Isolator" ) );
1079 BOOST_CHECK_EQUAL( textVars.at( wxS( "REVISION_MAJOR" ) ), wxS( "1" ) );
1080 BOOST_CHECK_EQUAL( textVars.at( wxS( "YEAR" ) ), wxS( "2026" ) );
1081
1082 // Board text referencing the special string now resolves through the project variable.
1083 wxString resolved = ExpandTextVars( wxS( "${PCB_REVISION}" ), &project, INTERNAL );
1084 BOOST_CHECK_EQUAL( resolved, wxS( "A" ) );
1085
1086 // End-to-end: an actual imported board text that references ${PCB_REVISION} must render its
1087 // value once the board is linked to the project carrying the variable. This guards against a
1088 // regression in the Altium special-string conversion as well as the variable registration.
1089 board->SetProject( &project, true /* reference only */ );
1090
1091 bool sawResolvedBoardText = false;
1092
1093 for( BOARD_ITEM* item : board->Drawings() )
1094 {
1095 const EDA_TEXT* text = dynamic_cast<const EDA_TEXT*>( item );
1096
1097 if( text && text->GetText().Contains( wxS( "${PCB_REVISION}" ) ) )
1098 {
1099 wxString shown = text->GetShownText( INTERNAL );
1100 BOOST_CHECK( !shown.Contains( wxS( "${PCB_REVISION}" ) ) );
1101 BOOST_CHECK( shown.Contains( wxS( "A" ) ) );
1102 sawResolvedBoardText = true;
1103 }
1104 }
1105
1106 BOOST_CHECK( sawResolvedBoardText );
1107}
1108
1109
1114BOOST_AUTO_TEST_CASE( ProjectParametersPreserveExisting )
1115{
1116 std::string dataDir = KI_TEST::GetPcbnewTestDataDir() + "plugins/altium/issue24456/";
1117 std::string pcbDoc = dataDir + "Fastino_Ground_Isolator.PcbDoc";
1118 std::string prjPcb = dataDir + "Fastino_Ground_Isolator.PrjPcb";
1119
1120 SETTINGS_MANAGER settingsManager;
1121 settingsManager.LoadProject( "" );
1122 PROJECT& project = settingsManager.Prj();
1123 project.GetTextVars()[wxS( "PCB_REVISION" )] = wxS( "user-set" );
1124
1125 std::map<std::string, UTF8> props;
1126 props["project_file"] = prjPcb;
1127
1128 std::unique_ptr<BOARD> board = m_altiumPlugin.LoadBoard( pcbDoc, &props, &project );
1129
1130 // A pre-existing variable wins over the imported parameter.
1131 BOOST_CHECK_EQUAL( project.GetTextVars().at( wxS( "PCB_REVISION" ) ), wxS( "user-set" ) );
1132
1133 // Other parameters are still imported.
1134 BOOST_CHECK_EQUAL( project.GetTextVars().at( wxS( "COMPANY_NAME" ) ), wxS( "ETH Zurich" ) );
1135}
1136
1137
1148BOOST_AUTO_TEST_CASE( CopperAndMaskTextCoincide )
1149{
1150 std::string dataPath = KI_TEST::GetPcbnewTestDataDir()
1151 + "plugins/altium/issue24456/Fastino_Ground_Isolator.PcbDoc";
1152
1153 std::unique_ptr<BOARD> board = m_altiumPlugin.LoadBoard( dataPath, nullptr );
1154 BOOST_REQUIRE( board );
1155
1156 std::vector<PCB_TEXT*> copperTexts;
1157 std::vector<PCB_TEXT*> maskTexts;
1158
1159 for( BOARD_ITEM* item : board->Drawings() )
1160 {
1161 if( item->Type() != PCB_TEXT_T )
1162 continue;
1163
1164 PCB_TEXT* text = static_cast<PCB_TEXT*>( item );
1165 PCB_LAYER_ID layer = text->GetLayer();
1166
1167 if( IsCopperLayer( layer ) )
1168 copperTexts.push_back( text );
1169 else if( layer == F_Mask || layer == B_Mask )
1170 maskTexts.push_back( text );
1171 }
1172
1173 BOOST_REQUIRE_MESSAGE( !copperTexts.empty(), "Board must contain copper-layer text" );
1174 BOOST_REQUIRE_MESSAGE( !maskTexts.empty(), "Board must contain soldermask-layer text" );
1175
1176 // A copper string and a mask string are the same logical label when they share text, rotation
1177 // and mirroring. KiCad's IU tolerance for "coincident" is tight; before the fix the gap was
1178 // 50000-75000 IU (0.05-0.075 mm).
1179 const int tolerance = 1000; // 1 micron
1180
1181 int matchedPairs = 0;
1182
1183 for( PCB_TEXT* copper : copperTexts )
1184 {
1185 for( PCB_TEXT* mask : maskTexts )
1186 {
1187 if( copper->GetText() != mask->GetText()
1188 || copper->GetTextAngle() != mask->GetTextAngle()
1189 || copper->IsMirrored() != mask->IsMirrored() )
1190 {
1191 continue;
1192 }
1193
1194 // Only treat them as the same label when they are already near each other; distinct
1195 // labels that happen to share a glyph (e.g. several "+" pads) must not be cross-matched.
1196 VECTOR2I delta = copper->GetTextPos() - mask->GetTextPos();
1197
1198 if( std::abs( delta.x ) > 500000 || std::abs( delta.y ) > 500000 )
1199 continue;
1200
1201 matchedPairs++;
1202
1203 BOOST_CHECK_MESSAGE(
1204 std::abs( delta.x ) <= tolerance && std::abs( delta.y ) <= tolerance,
1205 wxString::Format( "Copper/mask copies of '%s' diverge by (%d, %d) IU",
1206 copper->GetText(), delta.x, delta.y ) );
1207 }
1208 }
1209
1210 BOOST_CHECK_MESSAGE( matchedPairs > 0,
1211 "Expected at least one coincident copper/soldermask text pair" );
1212}
1213
1214
1225BOOST_AUTO_TEST_CASE( LengthTuningPatterns )
1226{
1227 std::string dataPath =
1228 KI_TEST::GetPcbnewTestDataDir() + "plugins/altium/issue24654/PCB1.PcbDoc";
1229
1230 std::unique_ptr<BOARD> board = m_altiumPlugin.LoadBoard( dataPath, nullptr );
1231
1232 BOOST_REQUIRE( board );
1233
1234 int tuningCount = 0;
1235 int singleCount = 0;
1236 int diffPairCount = 0;
1237
1238 std::set<wxString> patternNames;
1239
1240 for( PCB_GENERATOR* generator : board->Generators() )
1241 {
1242 PCB_TUNING_PATTERN* pattern = dynamic_cast<PCB_TUNING_PATTERN*>( generator );
1243
1244 if( !pattern )
1245 continue;
1246
1247 tuningCount++;
1248 patternNames.insert( pattern->GetName() );
1249
1250 // Each pattern must wrap the real imported copper and carry Altium's meander parameters.
1251 BOOST_CHECK_MESSAGE( !pattern->GetBoardItems().empty(),
1252 "Imported tuning pattern wraps no copper" );
1253 BOOST_CHECK_GT( pattern->GetMaxAmplitude(), 0 );
1254 BOOST_CHECK_GT( pattern->GetSpacing(), 0 );
1255
1256 // The members must be copper tracks/arcs that the importer placed on the board.
1257 std::set<int> memberNets;
1258
1259 for( BOARD_ITEM* item : pattern->GetBoardItems() )
1260 {
1261 BOOST_CHECK( item->Type() == PCB_TRACE_T || item->Type() == PCB_ARC_T );
1262 BOOST_CHECK( item->GetParentGroup() == pattern );
1263
1264 if( BOARD_CONNECTED_ITEM* bci = dynamic_cast<BOARD_CONNECTED_ITEM*>( item ) )
1265 memberNets.insert( bci->GetNetCode() );
1266 }
1267
1268 if( pattern->GetTuningMode() == DIFF_PAIR )
1269 {
1270 diffPairCount++;
1271
1272 // A differential-pair meander must keep both nets; collapsing them to one would
1273 // corrupt half the routing.
1274 BOOST_CHECK_EQUAL( memberNets.size(), 2 );
1275 }
1276 else if( pattern->GetTuningMode() == SINGLE )
1277 {
1278 singleCount++;
1279 BOOST_CHECK_EQUAL( memberNets.size(), 1 );
1280 }
1281 }
1282
1283 BOOST_CHECK_EQUAL( tuningCount, 8 );
1284 BOOST_CHECK_EQUAL( singleCount, 4 );
1285 BOOST_CHECK_EQUAL( diffPairCount, 4 );
1286
1287 // Each meander keeps the interactive union name Altium assigned it (from the UnionNames stream).
1288 const std::set<wxString> expectedNames = {
1289 wxT( "Interactive Length Tunings" ),
1290 wxT( "Interactive Length Tunings 1" ),
1291 wxT( "Interactive Length Tunings 2" ),
1292 wxT( "Interactive Length Tunings 3" ),
1293 wxT( "Interactive Diffpair Length Tunings" ),
1294 wxT( "Interactive Diffpair Length Tunings 1" ),
1295 wxT( "Interactive Diffpair Length Tunings 2" ),
1296 wxT( "Interactive Diffpair Length Tunings 3" ),
1297 };
1298
1299 BOOST_CHECK( patternNames == expectedNames );
1300}
1301
1302
1312BOOST_AUTO_TEST_CASE( LengthTuningPatternsAreSelectable )
1313{
1314 std::string dataPath =
1315 KI_TEST::GetPcbnewTestDataDir() + "plugins/altium/issue24654/PCB1.PcbDoc";
1316
1317 std::unique_ptr<BOARD> board = m_altiumPlugin.LoadBoard( dataPath, nullptr );
1318
1319 BOOST_REQUIRE( board );
1320
1321 int checked = 0;
1322
1323 for( PCB_GENERATOR* generator : board->Generators() )
1324 {
1325 PCB_TUNING_PATTERN* pattern = dynamic_cast<PCB_TUNING_PATTERN*>( generator );
1326
1327 if( !pattern )
1328 continue;
1329
1330 checked++;
1331
1332 BOOST_REQUIRE_MESSAGE( pattern->GetBaseLine().has_value(),
1333 "Imported tuning pattern has no baseline" );
1334 BOOST_REQUIRE_GT( pattern->GetBaseLine()->PointCount(), 1 );
1335
1336 const SHAPE_LINE_CHAIN& baseLine = *pattern->GetBaseLine();
1337
1338 // Editing snaps the origin onto copper, so the endpoints have to be the baseline's own
1339 BOOST_CHECK( pattern->GetPosition() == baseLine.CPoint( 0 ) );
1340 BOOST_CHECK( pattern->GetEnd() == baseLine.CLastPoint() );
1341
1342 if( pattern->GetTuningMode() == DIFF_PAIR )
1343 {
1344 BOOST_REQUIRE_MESSAGE( pattern->GetBaseLineCoupled().has_value(),
1345 "Imported diff-pair tuning pattern has no coupled baseline" );
1346 BOOST_CHECK_GT( pattern->GetBaseLineCoupled()->PointCount(), 1 );
1347 BOOST_CHECK_GT( pattern->GetDiffPairGap(), 0 );
1348 }
1349
1350 BOOST_CHECK_MESSAGE( !pattern->GetLastNetName().IsEmpty(),
1351 "Imported tuning pattern is not attributed to a net" );
1352
1353 // Clicking anywhere along the meander has to land on the generator
1354 for( int i = 0; i < baseLine.PointCount(); ++i )
1355 {
1356 BOOST_CHECK_MESSAGE( pattern->HitTest( baseLine.CPoint( i ), 0 ),
1357 "Tuning pattern is not hittable on its own baseline" );
1358 }
1359
1360 BOX2I bbox = pattern->GetBoundingBox();
1361
1362 BOOST_CHECK_MESSAGE( bbox.Contains( baseLine.BBox() ),
1363 "Tuning pattern bounding box does not cover its baseline" );
1364
1365 for( BOARD_ITEM* item : pattern->GetBoardItems() )
1366 {
1367 BOOST_CHECK_MESSAGE( bbox.Contains( item->GetBoundingBox() ),
1368 "Tuning pattern bounding box does not cover its member copper" );
1369 }
1370 }
1371
1372 BOOST_CHECK_EQUAL( checked, 8 );
1373}
1374
1375
1376// https://gitlab.com/kicad/code/kicad/-/issues/24847
1377// Keepout regions defined inside an Altium footprint must be imported at the footprint's board
1378// location. The importer keeps the Altium absolute coordinates instead of re-basing them to the
1379// footprint origin, so the keepout zone lands far from the footprint that owns it.
1380BOOST_AUTO_TEST_CASE( Issue24847_FootprintKeepoutPlacement )
1381{
1382 std::string dataPath = KI_TEST::GetPcbnewTestDataDir()
1383 + "plugins/altium/issue24847/PCB1.PcbDoc";
1384
1385 std::unique_ptr<BOARD> board = m_altiumPlugin.LoadBoard( dataPath, nullptr );
1386
1387 BOOST_REQUIRE( board );
1388 BOOST_REQUIRE_GT( board->Footprints().size(), 0 );
1389
1390 BOOST_CHECK_EQUAL( board->GetDesignSettings().m_CopperEdgeClearance, pcbIUScale.mmToIU( 0.4 ) );
1391
1392 int keepoutZoneCount = 0;
1393
1394 for( FOOTPRINT* footprint : board->Footprints() )
1395 {
1396 const VECTOR2I fpPos = footprint->GetPosition();
1397
1398 for( ZONE* zone : footprint->Zones() )
1399 {
1400 keepoutZoneCount++;
1401
1402 const VECTOR2I zoneCenter = zone->GetBoundingBox().GetCenter();
1403 const double distMm = ( zoneCenter - fpPos ).EuclideanNorm() / 1e6;
1404
1405 BOOST_TEST_MESSAGE( "footprint " << footprint->GetReference().ToStdString()
1406 << " at (" << fpPos.x << "," << fpPos.y
1407 << ") keepout center (" << zoneCenter.x << ","
1408 << zoneCenter.y << ") dist " << distMm << " mm" );
1409
1410 // A footprint-local keepout sits on its footprint anchor; the pre-fix regression put
1411 // it over 100mm away, so anything past a couple of mm is a placement failure.
1412 BOOST_CHECK_MESSAGE( distMm < 2.0,
1413 "Keepout in footprint " << footprint->GetReference().ToStdString()
1414 << " is " << distMm << " mm from its footprint origin" );
1415 }
1416 }
1417
1418 // PCB1.PcbDoc carries exactly three footprint-local keepouts (Z1, Z2, R1); dropping any is a
1419 // regression the placement check alone would not catch.
1420 BOOST_CHECK_EQUAL( keepoutZoneCount, 3 );
1421}
1422
1423
1424// https://gitlab.com/kicad/code/kicad/-/issues/13750
1425// Copper regions can carry a soldermask relief; the importer dropped it, losing the aperture
1426BOOST_AUTO_TEST_CASE( RegionSolderMaskExpansion )
1427{
1428 std::string dataPath = KI_TEST::GetPcbnewTestDataDir()
1429 + "plugins/altium/issue13750/"
1430 "altium2kicad_region_soldermask_expansion.PcbDoc";
1431
1432 std::unique_ptr<BOARD> board = m_altiumPlugin.LoadBoard( dataPath, nullptr );
1433 BOOST_REQUIRE( board );
1434
1435 std::vector<PCB_SHAPE*> copperPolys;
1436 std::vector<PCB_SHAPE*> maskPolys;
1437
1438 for( BOARD_ITEM* item : board->Drawings() )
1439 {
1440 if( item->Type() != PCB_SHAPE_T )
1441 continue;
1442
1443 PCB_SHAPE* shape = static_cast<PCB_SHAPE*>( item );
1444
1445 if( shape->GetShape() != SHAPE_T::POLY )
1446 continue;
1447
1448 if( shape->GetLayer() == F_Cu )
1449 copperPolys.push_back( shape );
1450 else if( shape->GetLayer() == F_Mask )
1451 maskPolys.push_back( shape );
1452 }
1453
1454 // Two top-copper regions, exactly one of which carries the soldermask relief
1455 BOOST_REQUIRE_EQUAL( copperPolys.size(), 2 );
1456 BOOST_REQUIRE_EQUAL( maskPolys.size(), 1 );
1457
1458 // Zero expansion here, so the mask aperture must exactly match its region, not just overlap it,
1459 // and must match exactly one region, else an off-by-one primitive association would still pass
1460 PCB_SHAPE* mask = maskPolys.front();
1461 int exactMatches = 0;
1462
1463 for( PCB_SHAPE* copper : copperPolys )
1464 {
1465 SHAPE_POLY_SET missing = copper->GetPolyShape().CloneDropTriangulation();
1466 missing.BooleanSubtract( mask->GetPolyShape() );
1467
1469 extra.BooleanSubtract( copper->GetPolyShape() );
1470
1471 if( missing.IsEmpty() && extra.IsEmpty() )
1472 exactMatches++;
1473 }
1474
1475 BOOST_CHECK_MESSAGE( exactMatches == 1, "F_Mask relief aperture must exactly match exactly one copper region" );
1476}
1477
1478
1479// A component copper region carries a net in Altium, but as a custom pad it used to import
1480// unconnected, breaking every connection the part makes through its own copper
1481BOOST_AUTO_TEST_CASE( ComponentCopperRegionNets )
1482{
1483 std::string dataPath =
1484 KI_TEST::GetPcbnewTestDataDir() + "plugins/altium/issue24456/Fastino_Ground_Isolator.PcbDoc";
1485
1486 std::unique_ptr<BOARD> board = m_altiumPlugin.LoadBoard( dataPath, nullptr );
1487
1488 int regionCount = 0;
1489 int connectedCount = 0;
1490
1491 for( FOOTPRINT* footprint : board->Footprints() )
1492 {
1493 for( PAD* pad : footprint->Pads() )
1494 {
1495 PCB_LAYER_ID copperLayer = pad->IsOnLayer( F_Cu ) ? F_Cu : B_Cu;
1496
1497 // Region pads sit on a 1 IU anchor, and one that touches its pad takes that number
1498 if( pad->GetShape( copperLayer ) != PAD_SHAPE::CUSTOM || pad->GetSize( copperLayer ).x > 1 )
1499 continue;
1500
1501 regionCount++;
1502
1503 if( pad->GetNetCode() > 0 )
1504 {
1505 connectedCount++;
1506 BOOST_CHECK( !pad->GetNetname().IsEmpty() );
1507 }
1508 }
1509 }
1510
1511 BOOST_REQUIRE_GT( regionCount, 0 );
1512 BOOST_CHECK_GT( connectedCount, 0 );
1513}
1514
1515
1516// Netted component copper must join the pad it touches, or it is left behind when the part moves
1517BOOST_AUTO_TEST_CASE( ComponentCopperJoinsItsPad )
1518{
1519 std::string dataPath =
1520 KI_TEST::GetPcbnewTestDataDir() + "plugins/altium/issue24456/Fastino_Ground_Isolator.PcbDoc";
1521
1522 std::unique_ptr<BOARD> board = m_altiumPlugin.LoadBoard( dataPath, nullptr );
1523
1524 int joined = 0;
1525
1526 for( FOOTPRINT* footprint : board->Footprints() )
1527 {
1528 for( PAD* pad : footprint->Pads() )
1529 {
1530 PCB_LAYER_ID copperLayer = pad->IsOnLayer( F_Cu ) ? F_Cu : B_Cu;
1531
1532 if( pad->GetShape( copperLayer ) == PAD_SHAPE::CUSTOM && pad->GetSize( copperLayer ).x <= 1
1533 && pad->GetNetCode() > 0 && !pad->GetNumber().IsEmpty() )
1534 {
1535 joined++;
1536 }
1537 }
1538 }
1539
1540 BOOST_TEST_MESSAGE( "joined " << joined );
1541 BOOST_CHECK_GT( joined, 0 );
1542}
1543
1544
1550BOOST_AUTO_TEST_CASE( ComponentCopperRegionOrientation )
1551{
1552 std::string dataPath =
1553 KI_TEST::GetPcbnewTestDataDir() + "plugins/altium/issue24456/Fastino_Ground_Isolator.PcbDoc";
1554
1555 std::unique_ptr<BOARD> board = m_altiumPlugin.LoadBoard( dataPath, nullptr );
1556
1557 int regionCount = 0;
1558
1559 for( FOOTPRINT* footprint : board->Footprints() )
1560 {
1561 if( footprint->GetOrientation().IsZero() )
1562 continue;
1563
1564 for( PAD* pad : footprint->Pads() )
1565 {
1566 PCB_LAYER_ID copperLayer = pad->IsOnLayer( F_Cu ) ? F_Cu : B_Cu;
1567
1568 // Region-derived pads are the custom-shaped ones Altium left without a number
1569 if( pad->GetShape( copperLayer ) != PAD_SHAPE::CUSTOM || !pad->GetNumber().IsEmpty() )
1570 continue;
1571
1572 regionCount++;
1573
1574 // PAD normalizes the library-frame angle, so cancelling the footprint rotation
1575 // leaves the absolute angle at a full turn rather than at zero
1576 EDA_ANGLE absolute = pad->GetOrientation();
1577 absolute.Normalize();
1578
1579 BOOST_CHECK_MESSAGE( absolute.IsZero(),
1580 wxString::Format( wxT( "Region pad in %s is rotated %.1f degrees" ),
1581 footprint->GetReference(), absolute.AsDegrees() ) );
1582 BOOST_CHECK( !pad->GetFPRelativeOrientation().IsZero() );
1583 }
1584 }
1585
1586 BOOST_REQUIRE_GT( regionCount, 0 );
1587}
1588
1589
1590// The importer recentres the board on the page after parsing and shifts both origins by that same
1591// vector, so only the origin's offset from the board outline survives the move
1593 const std::string& aRelativePath,
1594 double aExpectedXmm, double aExpectedYmm )
1595{
1596 std::string dataPath = KI_TEST::GetPcbnewTestDataDir() + aRelativePath;
1597
1598 std::unique_ptr<BOARD> board = aPlugin.LoadBoard( dataPath, nullptr );
1599
1600 BOOST_REQUIRE( board );
1601
1602 const BOARD_DESIGN_SETTINGS& bds = board->GetDesignSettings();
1603
1604 // Altium carries a single relative origin, so both KiCad origins have to land on it
1605 BOOST_CHECK( bds.GetGridOrigin() == bds.GetAuxOrigin() );
1606
1607 BOX2I outline = board->GetBoardEdgesBoundingBox();
1608
1609 BOOST_REQUIRE_GT( outline.GetArea(), 0 );
1610
1611 double offsetXmm = pcbIUScale.IUTomm( bds.GetGridOrigin().x - outline.GetLeft() );
1612 double offsetYmm = pcbIUScale.IUTomm( bds.GetGridOrigin().y - outline.GetTop() );
1613
1614 // Absorbs the edge-cut stroke width the bounding box adds, far below the tens of millimetres
1615 // separating the relative origin from the sheet corner on these boards
1616 const double toleranceMm = 1.0;
1617
1618 BOOST_CHECK_MESSAGE( std::abs( offsetXmm - aExpectedXmm ) < toleranceMm,
1619 wxString::Format( "%s: origin sits %.3f mm right of the outline corner, "
1620 "expected %.3f",
1621 aRelativePath, offsetXmm, aExpectedXmm ) );
1622
1623 BOOST_CHECK_MESSAGE( std::abs( offsetYmm - aExpectedYmm ) < toleranceMm,
1624 wxString::Format( "%s: origin sits %.3f mm below the outline corner, "
1625 "expected %.3f",
1626 aRelativePath, offsetYmm, aExpectedYmm ) );
1627}
1628
1629
1630// https://gitlab.com/kicad/code/kicad/-/issues/25057
1631BOOST_AUTO_TEST_CASE( RelativeOriginNotSheetCorner )
1632{
1633 // The sheet corner would land 186.600/142.375 mm from these offsets
1634 checkImportedOriginOffset( m_altiumPlugin,
1635 "plugins/altium/eDP_adapter_dvt1_source/eDP_adapter_dvt1.PcbDoc",
1636 46.300, 7.400 );
1637
1638 // Origin sits on the left edge; the sheet corner would land 51.308/50.165 mm away
1639 checkImportedOriginOffset( m_altiumPlugin, "plugins/altium/issue24847/PCB1.PcbDoc", 0.000,
1640 15.000 );
1641}
1642
1643
1644// https://gitlab.com/kicad/code/kicad/-/issues/25362
1645// FB36 has two component bodies naming one model; the importer used to hash and link the
1646// freed duplicate that AddFile() had already dropped
1647BOOST_AUTO_TEST_CASE( SharedComponentBodyModelEmbeddedOnce )
1648{
1649 std::string dataPath = KI_TEST::GetPcbnewTestDataDir()
1650 + "plugins/altium/issue25362/OpenRex_V1I1_PCB_reduced.PcbDoc";
1651
1652 std::unique_ptr<BOARD> board = m_altiumPlugin.LoadBoard( dataPath, nullptr );
1653
1654 BOOST_REQUIRE_EQUAL( board->Footprints().size(), 1 );
1655
1656 FOOTPRINT* footprint = board->Footprints().front();
1657 const EMBEDDED_FILES* files = footprint->GetEmbeddedFiles();
1658
1659 BOOST_REQUIRE_EQUAL( files->EmbeddedFileMap().size(), 1 );
1660
1661 const std::shared_ptr<EMBEDDED_FILES::EMBEDDED_FILE>& file =
1662 files->EmbeddedFileMap().begin()->second;
1663
1664 BOOST_CHECK_EQUAL( file->name, wxString( wxS( "C0603.step" ) ) );
1665 BOOST_CHECK( file->type == EMBEDDED_FILES::EMBEDDED_FILE::FILE_TYPE::MODEL );
1666
1667 // A freed file yields garbage here rather than the STEP payload, and the exact length plus
1668 // the closing tag pin the inflate loop against silently truncating a model
1669 const std::string step( file->decompressedData.begin(), file->decompressedData.end() );
1670
1671 BOOST_REQUIRE_EQUAL( step.size(), 126391u );
1672 BOOST_CHECK( step.starts_with( "ISO-10303-21;" ) );
1673 BOOST_CHECK( step.ends_with( "END-ISO-10303-21;\r\n" ) );
1674
1675 // CompressAndEncode runs on a worker thread against the file AddFile() handed back, so an
1676 // empty payload means that thread wrote into freed memory instead
1677 BOOST_CHECK_GT( file->compressedEncodedData.size(), 0 );
1678 BOOST_CHECK( file->Validate() );
1679
1680 // Both bodies keep their own placement but must resolve to the one embedded model
1681 BOOST_REQUIRE_EQUAL( footprint->Models().size(), 2 );
1682
1683 for( const FP_3DMODEL& model : footprint->Models() )
1684 BOOST_CHECK_EQUAL( model.m_Filename, file->GetLink() );
1685}
1686
1687
1688// The MID6 copper polygon of TI's TIDA-00204_revE3_PCB.PcbDoc, whose third vertex reads
1689// -111339.9634mil. That is 2.8 m, past the int range, so it used to clamp to INT_MIN + 8 and
1690// stretch the zone across the whole coordinate space
1691BOOST_AUTO_TEST_CASE( PolygonVertexOutsideCoordinateRangeIsDiscarded )
1692{
1693 std::map<wxString, wxString> props = {
1694 { wxT( "VX0" ), wxT( "3481.9999mil" ) }, { wxT( "VY0" ), wxT( "2818.9999mil" ) },
1695 { wxT( "VX1" ), wxT( "3506.9999mil" ) }, { wxT( "VY1" ), wxT( "2843.9999mil" ) },
1696 { wxT( "VX2" ), wxT( "-111339.9634mil" ) }, { wxT( "VY2" ), wxT( "2834mil" ) },
1697 { wxT( "VX3" ), wxT( "3756.9998mil" ) }, { wxT( "VY3" ), wxT( "2834mil" ) },
1698 { wxT( "VX4" ), wxT( "3771.9999mil" ) }, { wxT( "VY4" ), wxT( "2818.9999mil" ) },
1699 { wxT( "VX5" ), wxT( "3772mil" ) }, { wxT( "VY5" ), wxT( "2634mil" ) },
1700 { wxT( "VX6" ), wxT( "3757mil" ) }, { wxT( "VY6" ), wxT( "2619mil" ) },
1701 { wxT( "VX7" ), wxT( "3614.5mil" ) }, { wxT( "VY7" ), wxT( "2619.0001mil" ) },
1702 { wxT( "VX8" ), wxT( "3583.0001mil" ) }, { wxT( "VY8" ), wxT( "2587.5mil" ) },
1703 { wxT( "VX9" ), wxT( "3573.9998mil" ) }, { wxT( "VY9" ), wxT( "2587.4999mil" ) },
1704 { wxT( "VX10" ), wxT( "3542.5005mil" ) }, { wxT( "VY10" ), wxT( "2619.0001mil" ) },
1705 { wxT( "VX11" ), wxT( "3522mil" ) }, { wxT( "VY11" ), wxT( "2619mil" ) },
1706 { wxT( "VX12" ), wxT( "3497mil" ) }, { wxT( "VY12" ), wxT( "2619mil" ) },
1707 { wxT( "VX13" ), wxT( "3482mil" ) }, { wxT( "VY13" ), wxT( "2634mil" ) },
1708 { wxT( "VX14" ), wxT( "3481.9999mil" ) }, { wxT( "VY14" ), wxT( "2818.9999mil" ) },
1709 };
1710
1711 std::vector<ALTIUM_VERTICE> vertices;
1712 int discarded = 0;
1713
1714 altium_parse_polygons( props, vertices, &discarded );
1715
1716 BOOST_CHECK_EQUAL( discarded, 1 );
1717 BOOST_REQUIRE_EQUAL( vertices.size(), 14 );
1718
1719 BOX2I bbox;
1720
1721 for( const ALTIUM_VERTICE& vertex : vertices )
1722 bbox.Merge( vertex.position );
1723
1724 // The representable vertices span 7.366 x 6.5151 mm around x = 88.4 mm; keeping the clamped
1725 // one put the left edge at INT_MIN + 8 and overflowed the width itself
1726 BOOST_CHECK_EQUAL( bbox.GetLeft(), 88442800 );
1727 BOOST_CHECK_EQUAL( bbox.GetWidth(), 7366000 );
1728 BOOST_CHECK_EQUAL( bbox.GetHeight(), 6515100 );
1729
1730 // Every survivor keeps its own coordinates, so nothing was renumbered or shifted
1731 BOOST_CHECK_EQUAL( vertices[1].position, VECTOR2I( 89077800, -72237600 ) );
1732 BOOST_CHECK_EQUAL( vertices[2].position, VECTOR2I( 95427800, -71983600 ) );
1733}
1734
1735
1743BOOST_AUTO_TEST_CASE( NetNamesTakeSchematicCase )
1744{
1745 std::string dataDir = KI_TEST::GetPcbnewTestDataDir() + "plugins/altium/issue25374/";
1746
1747 std::map<std::string, UTF8> props;
1748 props["sch0"] = dataDir + "altium_import_netname.SchDoc";
1749
1751 std::unique_ptr<BOARD> board = plugin.LoadBoard( dataDir + "altium_import_netname.CSPcbDoc", &props );
1752
1753 BOOST_CHECK( board->FindNet( wxT( "all_low_letter" ) ) );
1754 BOOST_CHECK( board->FindNet( wxT( "UperCamelCase" ) ) );
1755 BOOST_CHECK( board->FindNet( wxT( "lowerCamelCase" ) ) );
1756 BOOST_CHECK( board->FindNet( wxT( "ALL_BIG_LETTER" ) ) );
1757 BOOST_CHECK( board->FindNet( wxT( "GND" ) ) );
1758 BOOST_CHECK( !board->FindNet( wxT( "ALL_LOW_LETTER" ) ) );
1759
1760 PCB_IO_ALTIUM_CIRCUIT_STUDIO standalonePlugin;
1761 std::unique_ptr<BOARD> standaloneBoard = standalonePlugin.LoadBoard( dataDir + "altium_import_netname.CSPcbDoc" );
1762
1763 BOOST_CHECK( standaloneBoard->FindNet( wxT( "ALL_LOW_LETTER" ) ) );
1764 BOOST_CHECK( !standaloneBoard->FindNet( wxT( "all_low_letter" ) ) );
1765}
1766
1767
const char * name
bool altiumScopeExprMatchesPolygon(const wxString &aExpr)
Return true if an Altium rule scope expression targets polygon pour primitives (matches InPolygon,...
void altium_parse_polygons(std::map< wxString, wxString > &aProps, std::vector< ALTIUM_VERTICE > &aVertices, int *aDiscarded)
Read the VX/VY/KIND/R/SA/EA/CX/CY vertex series out of a polygon or board outline record.
VECTOR2I altiumSlotDrillSize(uint32_t aHoleSize, uint32_t aSlotSize, double aSlotRotation, bool &aRotationSupported)
Convert an Altium slot's independent rotation and dimensions to KiCad's cardinal drill shape.
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.
const uint16_t ALTIUM_NET_UNCONNECTED
const uint16_t ALTIUM_POLYGON_NONE
ALTIUM_RECORD
const int ALTIUM_COMPONENT_NONE
wxString AltiumUnnamedNetName(const BOARD &aBoard, int &aCounter)
Invent a placeholder name for an Altium net that has none.
void ApplyAltiumNetclassRules(const std::map< ALTIUM_RULE_KIND, std::vector< ARULE6 > > &aRulesByKind, NET_SETTINGS &aNetSettings, std::vector< const ARULE6 * > *aUnresolved)
Copy the values of Altium rules scoped to a single netclass onto that netclass.
constexpr EDA_IU_SCALE pcbIUScale
Definition base_units.h:128
General utilities for PCB file IO for QA programs.
@ BS_ITEM_TYPE_DIELECTRIC
BOX2< VECTOR2I > BOX2I
Definition box2.h:914
const CFB::COMPOUND_FILE_ENTRY * FindStream(const std::vector< std::string > &aStreamPath) const
std::vector< int > m_altiumToKicadNetcodes
Definition altium_pcb.h:403
std::vector< std::pair< PCB_LAYER_ID, int > > HelperGetSolderAndPasteMaskExpansions(const ALTIUM_RECORD aType, const int aPrimitiveIndex, const ALTIUM_LAYER aAltiumLayer)
void ParseExtendedPrimitiveInformationData(const ALTIUM_PCB_COMPOUND_FILE &aAltiumPcbFile, const CFB::COMPOUND_FILE_ENTRY *aEntry)
int GetNetCode(uint16_t aId) const
void ConvertArcs6ToBoardItemOnLayer(const AARC6 &aElem, PCB_LAYER_ID aLayer)
A base class derived from BOARD_ITEM for items that can be connected and have a net,...
Container for design settings for a BOARD object.
std::shared_ptr< NET_SETTINGS > m_NetSettings
const VECTOR2I & GetGridOrigin() const
const VECTOR2I & GetAuxOrigin() const
BOARD_STACKUP & GetStackupDescriptor()
A base class for any item which can be embedded within the BOARD container class, and therefore insta...
Definition board_item.h:85
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
Information pertinent to a Pcbnew printed circuit board.
Definition board.h:411
const ZONES & Zones() const
Definition board.h:469
const FOOTPRINTS & Footprints() const
Definition board.h:465
const TRACKS & Tracks() const
Definition board.h:463
BOARD_DESIGN_SETTINGS & GetDesignSettings()
Definition board.cpp:1290
const DRAWINGS & Drawings() const
Definition board.h:467
constexpr size_type GetWidth() const
Definition box2.h:211
constexpr BOX2< Vec > & Merge(const BOX2< Vec > &aRect)
Modify the position and size of the rectangle in order to contain aRect.
Definition box2.h:594
constexpr ecoord_type GetArea() const
Return the area of the rectangle.
Definition box2.h:697
constexpr size_type GetHeight() const
Definition box2.h:212
constexpr coord_type GetLeft() const
Definition box2.h:225
constexpr bool Contains(const Vec &aPoint) const
Definition box2.h:165
constexpr coord_type GetTop() const
Definition box2.h:226
EDA_ANGLE Normalize()
Definition eda_angle.h:228
double AsDegrees() const
Definition eda_angle.h:115
bool IsZero() const
Definition eda_angle.h:135
wxString GetName() const
Definition eda_group.h:75
SHAPE_POLY_SET & GetPolyShape()
SHAPE_T GetShape() const
Definition eda_shape.h:177
A mix-in class (via multiple inheritance) that handles texts such as labels, parts,...
Definition eda_text.h:94
const std::map< wxString, std::shared_ptr< EMBEDDED_FILE > > & EmbeddedFileMap() const
Provide an iterable view of the file collection.
std::vector< FP_3DMODEL > & Models()
Definition footprint.h:375
EMBEDDED_FILES * GetEmbeddedFiles() override
Definition footprint.h:1280
Hold an error message and may be used when throwing exceptions containing meaningful error messages.
Custom REPORTER that captures all messages for later analysis in the unit test framework.
A collection of nets and the parameters used to route or test these nets.
Definition netclass.h:44
static const char Default[]
the name of the default NETCLASS
Definition netclass.h:46
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:380
Handle the data for a net.
Definition netinfo.h:50
static const int UNCONNECTED
Constant that holds the "unconnected net" number (typically 0) all items "connected" to this net are ...
Definition netinfo.h:281
NET_SETTINGS stores various net-related settings in a project context.
void SetNetclass(const wxString &netclassName, std::shared_ptr< NETCLASS > &netclass)
Sets the given netclass Calling user is responsible for resetting the effective netclass calculation ...
Definition pad.h:61
VECTOR2I GetPosition() const override
std::unordered_set< BOARD_ITEM * > GetBoardItems() const
static std::map< wxString, PCB_LAYER_ID > DefaultLayerMappingCallback(const std::vector< INPUT_LAYER_DESC > &aInputLayerDescriptionVector)
Return the automapped layers.
std::unique_ptr< BOARD > LoadBoard(const wxString &aFileName, const std::map< std::string, UTF8 > *aProperties=nullptr, PROJECT *aProject=nullptr)
Load information from some input file format that this PCB_IO implementation knows about into new BOA...
Definition pcb_io.cpp:72
PCB_LAYER_ID GetLayer() const override
Return the primary layer this item is on.
Definition pcb_shape.h:70
const VECTOR2I & GetEnd() const
LENGTH_TUNING_MODE GetTuningMode() const
const wxString & GetLastNetName() const
const std::optional< SHAPE_LINE_CHAIN > & GetBaseLine() const
const std::optional< SHAPE_LINE_CHAIN > & GetBaseLineCoupled() const
bool HitTest(const VECTOR2I &aPosition, int aAccuracy=0) const override
Test if aPosition is inside or on the boundary of this item.
const BOX2I GetBoundingBox() const override
Return the orthogonal bounding box of this object for display purposes.
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 polyline containing arcs as well as line segments: A chain of connected line and/or arc s...
int PointCount() const
Return the number of points (vertices) in this line chain.
const VECTOR2I & CPoint(int aIndex) const
Return a reference to a given point in the line chain.
const VECTOR2I & CLastPoint() const
Return the last point in the line chain.
const BOX2I BBox(int aClearance=0) const override
Compute a bounding box of the shape, with a margin of aClearance a collision.
Represent a set of closed polygons.
bool IsEmpty() const
Return true if the set is empty (no polygons at all)
int OutlineCount() const
Return the number of outlines in the set.
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, RESOLUTION_CONTEXT aContext)
Definition common.cpp:60
@ INTERNAL
Definition common.h:96
bool BuildBoardPolygonOutlines(BOARD *aBoard, SHAPE_POLY_SET &aOutlines, int aErrorMax, int aChainingEpsilon, bool aInferOutlineIfNecessary, OUTLINE_ERROR_HANDLER *aErrorHandler, bool aAllowUseArcsInPolygons)
Extract the board outlines and build a closed polygon from lines, arcs and circle items on edge cut l...
const std::function< void(const wxString &msg, BOARD_ITEM *itemA, BOARD_ITEM *itemB, const VECTOR2I &pt)> OUTLINE_ERROR_HANDLER
@ FP_SMD
Definition footprint.h:87
@ FP_THROUGH_HOLE
Definition footprint.h:86
std::string message
bool IsCopperLayer(int aLayerId)
Test whether a layer is a copper layer.
Definition layer_ids.h:741
PCB_LAYER_ID
A quick note on layer IDs:
Definition layer_ids.h:56
@ F_CrtYd
Definition layer_ids.h:112
@ F_Paste
Definition layer_ids.h:100
@ B_Mask
Definition layer_ids.h:94
@ B_Cu
Definition layer_ids.h:61
@ F_Mask
Definition layer_ids.h:93
@ B_Paste
Definition layer_ids.h:101
@ B_CrtYd
Definition layer_ids.h:111
@ 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:437
@ DIFF_PAIR
std::function< std::map< wxString, PCB_LAYER_ID >(const std::vector< INPUT_LAYER_DESC > &)> LAYER_MAPPING_HANDLER
Pointer to a function that takes a map of source and KiCad layers and returns a re-mapped version.
uint32_t textlinewidth
static int getNetCode(ALTIUM_PCB &aImporter, const std::vector< int > &aNetcodes, uint16_t aId)
ALTIUM_PCB_IMPORT_FIXTURE()=default
static std::vector< std::pair< PCB_LAYER_ID, int > > getMaskExpansions(ALTIUM_PCB &aImporter, ALTIUM_RECORD aType, int aPrimitiveIndex, ALTIUM_LAYER aLayer)
static void parseExtendedPrimitiveInformation(ALTIUM_PCB &aImporter, const ALTIUM_PCB_COMPOUND_FILE &aFile, const CFB::COMPOUND_FILE_ENTRY *aEntry)
static void convertConnectedArc(ALTIUM_PCB &aImporter, const AARC6 &aArc, PCB_LAYER_ID aLayer)
static std::vector< const ZONE * > collectRuleAreas(const BOARD &aBoard)
PCB_IO_ALTIUM_DESIGNER m_altiumPlugin
ALTIUM_RULE_KIND kind
wxString scope1expr
wxString scope2expr
Describes an imported layer and how it could be mapped to KiCad Layers.
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 ...
static void checkImportedOriginOffset(PCB_IO_ALTIUM_DESIGNER &aPlugin, const std::string &aRelativePath, double aExpectedXmm, double aExpectedYmm)
BOOST_AUTO_TEST_CASE(NetCodeRejectsIdPastEnd)
BOOST_AUTO_TEST_CASE(HorizontalAlignment)
BOOST_REQUIRE(intersection.has_value()==c.ExpectedIntersection.has_value())
BOOST_AUTO_TEST_SUITE_END()
IbisParser parser & reporter
KIBIS_MODEL * model
VECTOR3I expected(15, 30, 45)
BOOST_TEST_CONTEXT("Test Clearance")
BOOST_TEST_MESSAGE("Polyline has "<< chain.PointCount()<< " points")
int actual
BOOST_CHECK_EQUAL(result, "25.4")
int delta
@ PCB_SHAPE_T
class PCB_SHAPE, a segment not on copper layers
Definition typeinfo.h:80
@ PCB_VIA_T
class PCB_VIA, a via (like a track segment on a copper layer)
Definition typeinfo.h:89
@ PCB_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:708