KiCad PCB EDA Suite
Loading...
Searching...
No Matches
export_d356.cpp
Go to the documentation of this file.
1/*
2 * This program source code file is part of KiCad, a free EDA CAD application.
3 *
4 * Copyright (C) 2011-2013 Lorenzo Marcantonio <[email protected]>
5 * Copyright The KiCad Developers, see AUTHORS.txt for contributors.
6 *
7 * This program is free software; you can redistribute it and/or
8 * modify it under the terms of the GNU General Public License
9 * as published by the Free Software Foundation; either version 2
10 * of the License, or (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with this program. If not, see <https://www.gnu.org/licenses/>.
19 */
20
25#include "export_d356.h"
27
28#include <algorithm>
29#include <cctype>
30#include <map>
31#include <set>
32#include <vector>
33
34#include <wx/checkbox.h>
35#include <wx/filedlg.h>
36#include <wx/filedlgcustomize.h>
37#include <wx/msgdlg.h>
38#include <kiplatform/ui.h>
39
40#include <confirm.h>
41#include <gestfich.h>
42#include <kiface_base.h>
43#include <pcb_edit_frame.h>
44#include <trigo.h>
45#include <build_version.h>
46#include <macros.h>
48#include <locale_io.h>
49#include <board.h>
51#include <footprint.h>
52#include <layer_range.h>
53#include <pad.h>
54#include <pcb_track.h>
55#include <string_utils.h>
56
57#include <math/util.h> // for KiROUND
59
60
61// D-356 numbers copper layers physically from 1 (front) to the copper layer count (back)
62static int physical_layer( BOARD* aPcb, PCB_LAYER_ID aLayer )
63{
64 if( aLayer == B_Cu )
65 return aPcb->GetCopperLayerCount();
66
67 return static_cast<int>( CopperLayerToOrdinal( aLayer ) ) + 1;
68}
69
70
71// Compute the access code for a pad. Returns -1 if there is no copper
72static int compute_pad_access_code( BOARD *aPcb, LSET aLayerMask )
73{
74 // Non-copper is not interesting here
75 aLayerMask &= LSET::AllCuMask();
76
77 if( !aLayerMask.any() )
78 return -1;
79
80 // Traditional TH pad
81 if( aLayerMask[F_Cu] && aLayerMask[B_Cu] )
82 return 0;
83
84 // Front SMD pad
85 if( aLayerMask[F_Cu] )
86 return 1;
87
88 // Back SMD pad
89 if( aLayerMask[B_Cu] )
90 return aPcb->GetCopperLayerCount();
91
92 // OK, we have an inner-layer only pad (and I have no idea about
93 // what could be used for); anyway, find the first copper layer
94 // it's on
95 for( PCB_LAYER_ID layer : LAYER_RANGE( In1_Cu, B_Cu, aPcb->GetCopperLayerCount() ) )
96 {
97 if( aLayerMask[layer] )
98 return physical_layer( aPcb, layer );
99 }
100
101 // This shouldn't happen
102 return -1;
103}
104
105/* Convert and clamp a size from IU to decimils */
106static int iu_to_d356(int iu, int clamp)
107{
108 int val = KiROUND( static_cast<double>( iu ) / ( pcbIUScale.IU_PER_MILS / 10.0 ) );
109 if( val > clamp ) return clamp;
110 if( val < -clamp ) return -clamp;
111 return val;
112}
113
114/* Extract the D356 record from the footprints (pads) */
115void IPC356D_WRITER::appendPadRecords( BOARD* aPcb, const std::vector<FAB_TEST_POINT>& aPoints,
116 std::vector<D356_RECORD>& aRecords )
117{
118 VECTOR2I origin = aPcb->GetDesignSettings().GetAuxOrigin();
119
120 for( const FAB_TEST_POINT& point : aPoints )
121 {
122 if( !point.m_pad )
123 continue;
124
125 const PAD* pad = point.m_pad;
126 const FOOTPRINT* footprint = pad->GetParentFootprint();
127 D356_RECORD rk;
128 rk.access = compute_pad_access_code( aPcb, pad->GetLayerSet() );
129
130 // It could be a mask only pad, we only handle pads with copper here
131 if( rk.access == -1 )
132 continue;
133
135 continue;
136
137 rk.netname = pad->GetNetname();
138 rk.pin = pad->GetNumber();
139 rk.refdes = footprint->GetReference();
140 // A named pad is a component terminal (end-net point); an unnamed copper
141 // feature is only reachable mid-net, so it is a midpoint per IPC-D-356A
142 rk.midpoint = rk.pin.IsEmpty();
143 rk.drill = point.m_drill;
144 rk.hole = ( rk.drill != 0 );
145 rk.smd = pad->GetAttribute() == PAD_ATTRIB::SMD || pad->GetAttribute() == PAD_ATTRIB::CONN;
146 rk.mechanical = ( pad->GetAttribute() == PAD_ATTRIB::NPTH );
147 rk.x_location = point.m_position.x - origin.x;
148 rk.y_location = origin.y - point.m_position.y;
149
150 PCB_LAYER_ID accessLayer = footprint->IsFlipped() ? B_Cu : F_Cu;
151 rk.x_size = pad->GetSize( accessLayer ).x;
152
153 // Rule: round pads have y = 0
154 if( pad->GetShape( accessLayer ) == PAD_SHAPE::CIRCLE )
155 rk.y_size = 0;
156 else
157 rk.y_size = pad->GetSize( accessLayer ).y;
158
159 rk.rotation = -pad->GetOrientation().AsDegrees();
160
161 if( rk.rotation < 0 )
162 rk.rotation += 360;
163
164 // the value indicates which sides are *not* accessible
165 rk.soldermask = 3;
166
167 if( point.m_frontMask )
168 rk.soldermask &= ~1;
169
170 if( point.m_backMask )
171 rk.soldermask &= ~2;
172
173 aRecords.push_back( std::move( rk ) );
174 }
175}
176
177/* Compute the access code for a via. In D-356 layers are numbered from 1 up,
178 where '1' is the 'primary side' (usually the component side);
179 '0' means 'both sides', and other layers follows in an unspecified order */
180static int via_access_code( BOARD *aPcb, int top_layer, int bottom_layer )
181{
182 // Easy case for through vias: top_layer is component, bottom_layer is
183 // solder, access code is 0
184 if( (top_layer == F_Cu) && (bottom_layer == B_Cu) )
185 return 0;
186
187 // Blind via, reachable from front
188 if( top_layer == F_Cu )
189 return 1;
190
191 // Blind via, reachable from bottom
192 if( bottom_layer == B_Cu )
193 return aPcb->GetCopperLayerCount();
194
195 // It's a buried via, accessible from some inner layer
196 // (maybe could be used for testing before laminating? no idea)
197 return physical_layer( aPcb, static_cast<PCB_LAYER_ID>( top_layer ) );
198}
199
200/* Extract the D356 record from the vias */
201static void appendViaRecords( BOARD* aPcb, const std::vector<FAB_TEST_POINT>& aPoints,
202 std::vector<D356_RECORD>& aRecords )
203{
204 VECTOR2I origin = aPcb->GetDesignSettings().GetAuxOrigin();
205
206 for( const FAB_TEST_POINT& point : aPoints )
207 {
208 if( !point.m_via )
209 continue;
210
211 const PCB_VIA* via = point.m_via;
212 const NETINFO_ITEM* net = via->GetNet();
213
214 D356_RECORD rk;
215 rk.smd = false;
216 rk.hole = true;
217
218 if( net )
219 rk.netname = net->GetNetname();
220 else
221 rk.netname = wxEmptyString;
222
223 rk.refdes = wxT( "VIA" );
224 rk.pin = wxT( "" );
225 rk.midpoint = true; // Vias are always midpoints
226 rk.drill = point.m_drill;
227 rk.mechanical = false;
228
229 PCB_LAYER_ID top_layer, bottom_layer;
230
231 via->LayerPair( &top_layer, &bottom_layer );
232
233 rk.access = via_access_code( aPcb, top_layer, bottom_layer );
234
235 if( rk.access != 0 )
236 {
237 rk.start_layer = physical_layer( aPcb, top_layer );
238 rk.end_layer = physical_layer( aPcb, bottom_layer );
239 }
240
241 rk.x_location = point.m_position.x - origin.x;
242 rk.y_location = origin.y - point.m_position.y;
243
244 // The record has a single size for vias. A blind via's 027 record describes its one
245 // surface pad; otherwise take the smaller of the front and back
246 if( rk.access == 1 )
247 rk.x_size = via->GetWidth( F_Cu );
248 else if( rk.access == aPcb->GetCopperLayerCount() )
249 rk.x_size = via->GetWidth( B_Cu );
250 else if( via->Padstack().Mode() != PADSTACK::MODE::NORMAL )
251 rk.x_size = std::min( via->GetWidth( F_Cu ), via->GetWidth( B_Cu ) );
252 else
253 rk.x_size = via->GetWidth( F_Cu );
254
255 rk.y_size = 0; // Round so height = 0
256 rk.rotation = 0;
257
258 // the value indicates which sides are *not* accessible
259 rk.soldermask = 0;
260
261 if( via->IsTented( F_Mask ) )
262 rk.soldermask |= 1;
263
264 if( via->IsTented( B_Mask ) )
265 rk.soldermask |= 2;
266
267 aRecords.push_back( rk );
268 }
269}
270
271// Rule: we can only use the standard ASCII, control excluded
272// Case folding is ASCII only since wxString::Upper() follows the locale (Turkish i)
273static wxString sanitize_d356_name( const wxString& aNetname, bool aUpper )
274{
275 wxString out;
276
277 for( wxUniChar ch : aNetname )
278 {
279 if( ch > 126 || !std::isgraph( static_cast<unsigned char>( ch ) ) )
280 ch = '?';
281 else if( aUpper && ch >= 'a' && ch <= 'z' )
282 ch = static_cast<char>( ch.GetValue() - 'a' + 'A' );
283
284 out += ch;
285 }
286
287 return out;
288}
289
290
291// NNAME aliases fill columns 9-13, so base 36 keeps them to five characters
292static wxString d356_alias( int aIndex )
293{
294 static const char digits[] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
295 wxString alias( wxT( "M" ) );
296
297 for( int div = 36 * 36 * 36; div > 0; div /= 36 )
298 alias += digits[( aIndex / div ) % 36];
299
300 return alias;
301}
302
303
304/* Write all the accumuled data to the file in D356 format */
305void IPC356D_WRITER::write_D356_records( std::vector <D356_RECORD> &aRecords, FILE* aFile )
306{
307 const wxString nc( wxT( "N/C" ) );
308
309 // Source net name to the signal name written in columns 4-17
310 std::map<wxString, wxString> d356_net_map;
311 std::set<wxString> d356_net_set = { nc };
312 std::map<wxString, int> canonCount;
313
314 // Rule: only uppercase (unofficial, but known to give problems otherwise)
315 for( const D356_RECORD& rk : aRecords )
316 {
317 if( !rk.netname.empty() && d356_net_map.emplace( rk.netname, wxEmptyString ).second )
318 canonCount[sanitize_d356_name( rk.netname, true )]++;
319 }
320
321 // Every candidate is reserved so no alias can shadow a real net
322 for( const auto& [candidate, count] : canonCount )
323 d356_net_set.insert( candidate );
324
325 // Every member of a collision is aliased, otherwise its NNAME would name the net that kept it
326 for( auto& [netname, canon] : d356_net_map )
327 {
328 wxString candidate = sanitize_d356_name( netname, true );
329
330 if( candidate.size() <= 14 && candidate != nc && canonCount[candidate] == 1 )
331 canon = candidate;
332 }
333
334 int aliasIndex = 0;
335
336 for( auto& [netname, canon] : d356_net_map )
337 {
338 if( !canon.empty() )
339 continue;
340
341 do
342 {
343 canon = d356_alias( aliasIndex++ );
344 } while( !d356_net_set.insert( canon ).second );
345
346 // Columns 15-72 hold the source name, keeping the tail since hierarchical prefixes repeat
347 fprintf( aFile, "P NNAME%-5.5s %-66.66s\n", TO_UTF8( canon ),
348 TO_UTF8( sanitize_d356_name( netname, false ).Right( 58 ) ) );
349 }
350
351 auto signalName =
352 [&]( const D356_RECORD& aRecord ) -> const wxString&
353 {
354 return aRecord.netname.empty() ? nc : d356_net_map.at( aRecord.netname );
355 };
356
357 // 356A wants the netlist sorted by net; stable keeps each net's vias ahead of its pads
358 std::stable_sort( aRecords.begin(), aRecords.end(),
359 [&]( const D356_RECORD& aLhs, const D356_RECORD& aRhs )
360 {
361 return signalName( aLhs ) < signalName( aRhs );
362 } );
363
364 for( D356_RECORD& rk : aRecords )
365 {
366 const wxString& d356_net = signalName( rk );
367 const bool blindOrBuried = rk.start_layer != 0;
368
369 // Choose the best record type
370 int rktype;
371
372 if( rk.smd )
373 rktype = 327;
374 else if( rk.mechanical )
375 rktype = 367;
376 else if( blindOrBuried )
377 rktype = 307;
378 else
379 rktype = 317;
380
381 auto writeComponent =
382 [&]( int aOpCode, bool aMidpoint )
383 {
384 fprintf( aFile, "%03d%-14.14s %-6.6s%c%-4.4s%c", aOpCode, TO_UTF8( d356_net ),
385 TO_UTF8( rk.refdes ), rk.pin.empty() ? ' ' : '-', TO_UTF8( rk.pin ),
386 aMidpoint ? 'M' : ' ' );
387 };
388
389 auto writeAccessAndSize =
390 [&]()
391 {
392 fprintf( aFile, "A%02dX%+07dY%+07dX%04dY%04dR%03d", rk.access,
393 iu_to_d356( rk.x_location, 999999 ), iu_to_d356( rk.y_location, 999999 ),
394 iu_to_d356( rk.x_size, 9999 ), iu_to_d356( rk.y_size, 9999 ), rk.rotation );
395 };
396
397 writeComponent( rktype, rk.midpoint );
398
399 // Hole definition
400 if( rk.hole )
401 {
402 fprintf( aFile, "D%04d%c",
403 iu_to_d356( rk.drill, 9999 ),
404 rk.mechanical ? 'U':'P' );
405 }
406 else
407 {
408 fprintf( aFile, " " );
409 }
410
411 // A 307 record omits the surface pad size, which goes in its 027 continuation instead
412 if( blindOrBuried )
413 {
414 fprintf( aFile, "A%02dX%+07dY%+07d%14s", rk.access, iu_to_d356( rk.x_location, 999999 ),
415 iu_to_d356( rk.y_location, 999999 ), "" );
416 }
417 else
418 {
419 writeAccessAndSize();
420 }
421
422 // Soldermask lives in columns 73-74 with column 72 left blank
423 fprintf( aFile, " S%d", rk.soldermask );
424
425 // Every test record is a full 80 columns
426 if( blindOrBuried )
427 fprintf( aFile, "L%02dL%02d\n", rk.start_layer, rk.end_layer );
428 else
429 fprintf( aFile, "%6s\n", "" );
430
431 // Blind vias reach a surface, so the 027 continuation describes that pad
432 if( blindOrBuried
433 && ( rk.start_layer == 1 || rk.end_layer == m_pcb->GetCopperLayerCount() ) )
434 {
435 // The midpoint flag belongs to the parent record only
436 writeComponent( 27, false );
437 fprintf( aFile, " " );
438 writeAccessAndSize();
439 fprintf( aFile, " S%d%6s\n", rk.soldermask, "" );
440 }
441 }
442}
443
444
445bool IPC356D_WRITER::Write( const wxString& aFilename )
446{
447 FILE* file = nullptr;
448 LOCALE_IO toggle; // Switch the locale to standard C
449
450 if( ( file = wxFopen( aFilename, wxT( "wt" ) ) ) == nullptr )
451 {
452 return false;
453 }
454
455 // This will contain everything needed for the 356 file
456 std::vector<D356_RECORD> d356_records;
457
458 std::vector<FAB_TEST_POINT> points = FabTestPoints( *m_pcb );
459 appendViaRecords( m_pcb, points, d356_records );
460 appendPadRecords( m_pcb, points, d356_records );
461
462 // Code 00 AFAIK is ASCII, CUST 0 is decimils/degrees
463 // CUST 1 would be metric but gerbtool simply ignores it!
464 // Every record is 80 columns and parameter values must end by column 72
465 auto writeRecord =
466 [&]( const std::string& aRecord )
467 {
468 fprintf( file, "%-80.80s\n", aRecord.substr( 0, 72 ).c_str() );
469 };
470
471 // Parameter values start in column 10 and JOB must lead the header
472 writeRecord( "P JOB " + std::string( TO_UTF8( wxFileName( m_pcb->GetFileName() ).GetName() ) ) );
473 writeRecord( "P CODE 00" );
474 writeRecord( "P UNITS CUST 0" );
475 writeRecord( "P arrayDim N" );
476 writeRecord( "P VER IPC-D-356A" );
477 writeRecord( "P IMAGE PRIMARY" );
478 write_D356_records( d356_records, file );
479 writeRecord( "999" );
480
481 fclose( file );
482
483 return true;
484}
485
486
487
488
489// Subclass for wxFileDialogCustomizeHook to add the checkbox
490class D365_CUSTOMIZE_HOOK : public wxFileDialogCustomizeHook
491{
492public:
493
494public:
495 D365_CUSTOMIZE_HOOK( bool aDoNotExportUnconnectedPads = false ) :
496 m_doNotExportUnconnectedPads( aDoNotExportUnconnectedPads ),
497 m_cb(nullptr)
498 {};
499
500 virtual void AddCustomControls( wxFileDialogCustomize& customizer ) override
501 {
502#ifdef __WXMAC__
503 customizer.AddStaticText( wxT( "\n\n" ) ); // Increase height of static box
504#endif
505
506 m_cb = customizer.AddCheckBox( _( "Do not export unconnected pads" ) );
508 }
509
510 virtual void TransferDataFromCustomControls() override
511 {
513 }
514
516private:
517
519 wxFileDialogCheckBox* m_cb;
520
522};
523
524
526{
527 wxFileName fn = m_frame->GetBoard()->GetFileName();
528 wxString ext, wildcard;
529
531 wildcard = FILEEXT::IpcD356FileWildcard();
532 fn.SetExt( ext );
533
534 wxString pro_dir = wxPathOnly( m_frame->Prj().GetProjectFullName() );
535
536 wxFileDialog dlg( m_frame, _( "Generate IPC-D-356 netlist file" ), pro_dir, fn.GetFullName(),
537 wildcard, wxFD_SAVE | wxFD_OVERWRITE_PROMPT );
538 D365_CUSTOMIZE_HOOK customizeHook( m_frame->GetPcbNewSettings()->m_ExportD356.doNotExportUnconnectedPads );
539 dlg.SetCustomizeHook( customizeHook );
540
542
543 if( dlg.ShowModal() == wxID_CANCEL )
544 return 0;
545
546 IPC356D_WRITER writer( m_frame->GetBoard() );
547 bool doNotExportUnconnectedPads = customizeHook.GetDoNotExportUnconnectedPads();
548 writer.SetDoNotExportUnconnectedPads( doNotExportUnconnectedPads );
549 m_frame->GetPcbNewSettings()->m_ExportD356.doNotExportUnconnectedPads = doNotExportUnconnectedPads;
550
551 if( writer.Write( dlg.GetPath() ) )
552 {
553 DisplayInfoMessage( m_frame, wxString::Format( _( "IPC-D-356 netlist file created:\n'%s'." ),
554 dlg.GetPath() ) );
555 }
556 else
557 {
558 DisplayError( m_frame, wxString::Format( _( "Failed to create file '%s'." ),
559 dlg.GetPath() ) );
560 }
561
562 return 0;
563}
constexpr EDA_IU_SCALE pcbIUScale
Definition base_units.h:128
constexpr BOX2I KiROUND(const BOX2D &aBoxD)
Definition box2.h:982
const VECTOR2I & GetAuxOrigin() const
int GenD356File(const TOOL_EVENT &aEvent)
Information pertinent to a Pcbnew printed circuit board.
Definition board.h:410
int GetCopperLayerCount() const
Definition board.cpp:1138
BOARD_DESIGN_SETTINGS & GetDesignSettings() const
Definition board.cpp:1301
virtual void TransferDataFromCustomControls() override
D365_CUSTOMIZE_HOOK(bool aDoNotExportUnconnectedPads=false)
wxDECLARE_NO_COPY_CLASS(D365_CUSTOMIZE_HOOK)
wxFileDialogCheckBox * m_cb
bool GetDoNotExportUnconnectedPads() const
virtual void AddCustomControls(wxFileDialogCustomize &customizer) override
bool IsFlipped() const
Definition footprint.h:662
const wxString & GetReference() const
Definition footprint.h:909
Wrapper to expose an API for writing IPC-D356 files.
Definition export_d356.h:60
bool m_doNotExportUnconnectedPads
Definition export_d356.h:98
void write_D356_records(std::vector< D356_RECORD > &aRecords, FILE *aFile)
Writes a list of records to the given output stream.
void SetDoNotExportUnconnectedPads(bool aDoNotExportUnconnectedPads)
Sets whether unconnected pads should be exported.
Definition export_d356.h:84
void appendPadRecords(BOARD *aPcb, const std::vector< FAB_TEST_POINT > &aPoints, std::vector< D356_RECORD > &aRecords)
bool Write(const wxString &aFilename)
Generates and writes the netlist to a given path.
Instantiate the current locale within a scope in which you are expecting exceptions to be thrown.
Definition locale_io.h:37
LSET is a set of PCB_LAYER_IDs.
Definition lset.h:37
static const LSET & AllCuMask()
return AllCuMask( MAX_CU_LAYERS );
Definition lset.cpp:604
Handle the data for a net.
Definition netinfo.h:50
const wxString & GetNetname() const
Definition netinfo.h:110
static const int UNCONNECTED
Constant that holds the "unconnected net" number (typically 0) all items "connected" to this net are ...
Definition netinfo.h:281
@ NORMAL
Shape is the same on all layers.
Definition padstack.h:170
Definition pad.h:61
Generic, UI-independent tool event.
Definition tool_event.h:167
void DisplayInfoMessage(wxWindow *aParent, const wxString &aMessage, const wxString &aExtraInfo)
Display an informational message box with aMessage.
Definition confirm.cpp:245
void DisplayError(wxWindow *aParent, const wxString &aText)
Display an error or warning message box with aMessage.
Definition confirm.cpp:192
This file is part of the common library.
#define _(s)
static int physical_layer(BOARD *aPcb, PCB_LAYER_ID aLayer)
static void appendViaRecords(BOARD *aPcb, const std::vector< FAB_TEST_POINT > &aPoints, std::vector< D356_RECORD > &aRecords)
static wxString sanitize_d356_name(const wxString &aNetname, bool aUpper)
static wxString d356_alias(int aIndex)
static int iu_to_d356(int iu, int clamp)
static int via_access_code(BOARD *aPcb, int top_layer, int bottom_layer)
static int compute_pad_access_code(BOARD *aPcb, LSET aLayerMask)
std::vector< FAB_TEST_POINT > FabTestPoints(const BOARD &aBoard)
The caller must build board connectivity before enumerating net endpoints.
static const std::string IpcD356FileExtension
static wxString IpcD356FileWildcard()
#define TO_UTF8(wxstring)
Convert a wxString to a UTF8 encoded C string for all wxWidgets build modes.
Definition idf_helpers.h:37
size_t CopperLayerToOrdinal(PCB_LAYER_ID aLayer)
Converts KiCad copper layer enum to an ordinal between the front and back layers.
Definition layer_ids.h:945
PCB_LAYER_ID
A quick note on layer IDs:
Definition layer_ids.h:56
@ B_Mask
Definition layer_ids.h:94
@ B_Cu
Definition layer_ids.h:61
@ F_Mask
Definition layer_ids.h:93
@ In1_Cu
Definition layer_ids.h:62
@ F_Cu
Definition layer_ids.h:60
This file contains miscellaneous commonly used macros and functions.
void AllowNetworkFileSystems(wxDialog *aDialog)
Configure a file dialog to show network and virtual file systems.
Definition wxgtk/ui.cpp:533
@ NPTH
like PAD_PTH, but not plated mechanical use only, no connection allowed
Definition padstack.h:102
@ SMD
Smd pad, appears on the solder paste layer (default)
Definition padstack.h:98
@ CONN
Like smd, does not appear on the solder paste layer (default) Note: also has a special attribute in G...
Definition padstack.h:99
wxString refdes
Definition export_d356.h:37
bool mechanical
Definition export_d356.h:41
wxString pin
Definition export_d356.h:38
wxString netname
Definition export_d356.h:36
VECTOR2< int32_t > VECTOR2I
Definition vector2d.h:708
Definition of file extensions used in Kicad.