2019-09-15 15:08:57 +00:00
|
|
|
/*
|
|
|
|
* This program source code file is part of KiCad, a free EDA CAD application.
|
|
|
|
*
|
|
|
|
* Copyright (C) 2019 Jean_Pierre Charras <jp.charras at wanadoo.fr>
|
|
|
|
* Copyright (C) 1992-2019 KiCad Developers, see AUTHORS.txt for contributors.
|
|
|
|
*
|
|
|
|
* This program is free software: you can redistribute it and/or modify it
|
|
|
|
* under the terms of the GNU General Public License as published by the
|
|
|
|
* Free Software Foundation, either version 3 of the License, or (at your
|
|
|
|
* option) any later version.
|
|
|
|
*
|
|
|
|
* This program is distributed in the hope that it will be useful, but
|
|
|
|
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
|
|
* General Public License for more details.
|
|
|
|
*
|
|
|
|
* You should have received a copy of the GNU General Public License along
|
|
|
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @file gerber_placefile_writer.cpp
|
|
|
|
* @brief Functions to create place files in gerber X2 format.
|
|
|
|
*/
|
|
|
|
|
|
|
|
#include "gerber_placefile_writer.h"
|
|
|
|
|
|
|
|
#include <vector>
|
|
|
|
|
|
|
|
#include <plotter.h>
|
|
|
|
#include <kicad_string.h>
|
2020-10-24 01:38:50 +00:00
|
|
|
#include <locale_io.h>
|
2019-09-15 15:08:57 +00:00
|
|
|
#include <pcb_edit_frame.h>
|
|
|
|
#include <pgm_base.h>
|
|
|
|
|
2020-11-12 20:19:22 +00:00
|
|
|
#include <board.h>
|
2019-09-15 15:08:57 +00:00
|
|
|
|
|
|
|
#include <pcbplot.h>
|
|
|
|
#include <wildcards_and_files_ext.h>
|
|
|
|
#include <reporter.h>
|
|
|
|
#include <gbr_metadata.h>
|
2020-11-12 20:19:22 +00:00
|
|
|
#include <footprint.h>
|
2019-09-15 15:08:57 +00:00
|
|
|
|
|
|
|
|
|
|
|
PLACEFILE_GERBER_WRITER::PLACEFILE_GERBER_WRITER( BOARD* aPcb )
|
|
|
|
{
|
2020-11-16 00:04:55 +00:00
|
|
|
m_pcb = aPcb;
|
2020-01-12 13:00:42 +00:00
|
|
|
m_plotPad1Marker = true; // Place a marker to pin 1 (or A1) position
|
|
|
|
m_plotOtherPadsMarker = true; // Place a marker to other pins position
|
|
|
|
m_layer = PCB_LAYER_ID::UNDEFINED_LAYER; // No layer set
|
2019-09-15 15:08:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2020-07-01 12:28:10 +00:00
|
|
|
int PLACEFILE_GERBER_WRITER::CreatePlaceFile( wxString& aFullFilename, PCB_LAYER_ID aLayer,
|
|
|
|
bool aIncludeBrdEdges )
|
2019-09-15 15:08:57 +00:00
|
|
|
{
|
|
|
|
m_layer = aLayer;
|
|
|
|
|
2019-11-17 09:11:29 +00:00
|
|
|
PCB_PLOT_PARAMS plotOpts = m_pcb->GetPlotOptions();
|
|
|
|
|
|
|
|
if( plotOpts.GetUseAuxOrigin() )
|
2020-07-01 12:28:10 +00:00
|
|
|
m_offset = m_pcb->GetDesignSettings().m_AuxOrigin;
|
2019-11-17 09:11:29 +00:00
|
|
|
|
2019-09-15 15:08:57 +00:00
|
|
|
// Collect footprints on the right layer
|
2020-11-13 15:15:52 +00:00
|
|
|
std::vector<FOOTPRINT*> fp_list;
|
2019-09-15 15:08:57 +00:00
|
|
|
|
2020-11-13 15:15:52 +00:00
|
|
|
for( FOOTPRINT* footprint : m_pcb->Footprints() )
|
2019-09-15 15:08:57 +00:00
|
|
|
{
|
2020-11-13 02:09:34 +00:00
|
|
|
if( footprint->GetAttributes() & FP_EXCLUDE_FROM_POS_FILES )
|
2019-09-15 15:08:57 +00:00
|
|
|
continue;
|
|
|
|
|
|
|
|
if( footprint->GetLayer() == aLayer )
|
|
|
|
fp_list.push_back( footprint );
|
|
|
|
}
|
|
|
|
|
2019-11-17 09:11:29 +00:00
|
|
|
LOCALE_IO dummy_io; // Use the standard notation for float numbers
|
2019-09-15 15:08:57 +00:00
|
|
|
|
|
|
|
GERBER_PLOTTER plotter;
|
|
|
|
|
|
|
|
// Gerber drill file imply X2 format:
|
|
|
|
plotter.UseX2format( true );
|
|
|
|
plotter.UseX2NetAttributes( true );
|
|
|
|
|
|
|
|
// Add the standard X2 header, without FileFunction
|
|
|
|
AddGerberX2Header( &plotter, m_pcb );
|
|
|
|
plotter.SetViewport( m_offset, IU_PER_MILS/10, /* scale */ 1.0, /* mirror */false );
|
|
|
|
// has meaning only for gerber plotter. Must be called only after SetViewport
|
|
|
|
plotter.SetGerberCoordinatesFormat( 6 );
|
|
|
|
plotter.SetCreator( wxT( "PCBNEW" ) );
|
|
|
|
|
|
|
|
// Add the standard X2 FileFunction for P&P files
|
|
|
|
// %TF.FileFunction,Component,Ln,[top][bottom]*%
|
|
|
|
wxString text;
|
|
|
|
text.Printf( "%%TF.FileFunction,Component,L%d,%s*%%",
|
|
|
|
aLayer == B_Cu ? m_pcb->GetCopperLayerCount() : 1,
|
2019-10-21 13:15:46 +00:00
|
|
|
aLayer == B_Cu ? "Bot" : "Top" );
|
2019-09-15 15:08:57 +00:00
|
|
|
plotter.AddLineToHeader( text );
|
|
|
|
|
|
|
|
// Add file polarity (positive)
|
|
|
|
text = "%TF.FilePolarity,Positive*%";
|
|
|
|
plotter.AddLineToHeader( text );
|
|
|
|
|
|
|
|
if( !plotter.OpenFile( aFullFilename ) )
|
|
|
|
return -1;
|
|
|
|
|
|
|
|
// We need a BRDITEMS_PLOTTER to plot pads
|
|
|
|
BRDITEMS_PLOTTER brd_plotter( &plotter, m_pcb, plotOpts );
|
|
|
|
|
|
|
|
plotter.StartPlot();
|
|
|
|
|
2019-11-03 19:23:31 +00:00
|
|
|
// Some tools in P&P files have the type and size defined.
|
|
|
|
// they are position flash (round), pad1 flash (diamond), other pads flash (round)
|
|
|
|
// and component outline thickness (polyline)
|
|
|
|
int flash_position_shape_diam = Millimeter2iu( 0.3 ); // defined size for position shape (circle)
|
|
|
|
int pad1_mark_size = Millimeter2iu( 0.36 ); // defined size for pad 1 position (diamond)
|
|
|
|
int other_pads_mark_size = 0; // defined size for position shape (circle)
|
|
|
|
int line_thickness = Millimeter2iu( 0.1 ); // defined size for component outlines
|
2019-11-01 12:35:42 +00:00
|
|
|
|
|
|
|
brd_plotter.SetLayerSet( LSET( aLayer ) );
|
2019-09-15 15:08:57 +00:00
|
|
|
int cmp_count = 0;
|
2019-10-24 16:50:51 +00:00
|
|
|
bool allowUtf8 = true;
|
2019-09-15 15:08:57 +00:00
|
|
|
|
2019-11-03 19:23:31 +00:00
|
|
|
// Plot components data: position, outlines, pad1 and other pads.
|
2020-11-13 15:15:52 +00:00
|
|
|
for( FOOTPRINT* footprint : fp_list )
|
2019-09-15 15:08:57 +00:00
|
|
|
{
|
2019-11-03 19:23:31 +00:00
|
|
|
// Manage the aperture attribute component position:
|
2019-09-15 15:08:57 +00:00
|
|
|
GBR_METADATA gbr_metadata;
|
|
|
|
gbr_metadata.SetApertureAttrib( GBR_APERTURE_METADATA::GBR_APERTURE_ATTRIB_CMP_POSITION );
|
|
|
|
|
|
|
|
// Add object attribute: component reference to flash (mainly usefull for users)
|
2019-10-24 16:50:51 +00:00
|
|
|
// using quoted UTF8 string
|
2020-08-22 21:22:54 +00:00
|
|
|
wxString ref = ConvertNotAllowedCharsInGerber( footprint->Reference().GetShownText(),
|
2019-10-24 16:50:51 +00:00
|
|
|
allowUtf8, true );
|
2019-09-15 15:08:57 +00:00
|
|
|
|
|
|
|
gbr_metadata.SetCmpReference( ref );
|
|
|
|
gbr_metadata.SetNetAttribType( GBR_NETLIST_METADATA::GBR_NETINFO_CMP );
|
|
|
|
|
|
|
|
// Add P&P specific attributes
|
|
|
|
GBR_CMP_PNP_METADATA pnpAttrib;
|
|
|
|
|
|
|
|
// Add rotation info (rotation is CCW, in degrees):
|
|
|
|
pnpAttrib.m_Orientation = mapRotationAngle( footprint->GetOrientationDegrees() );
|
|
|
|
|
2020-08-26 21:43:38 +00:00
|
|
|
pnpAttrib.m_MountType = GBR_CMP_PNP_METADATA::MOUNT_TYPE_UNSPECIFIED;
|
2019-09-15 15:08:57 +00:00
|
|
|
|
2020-11-13 02:09:34 +00:00
|
|
|
if( footprint->GetAttributes() & FP_THROUGH_HOLE )
|
2020-08-26 21:43:38 +00:00
|
|
|
pnpAttrib.m_MountType = GBR_CMP_PNP_METADATA::MOUNT_TYPE_TH;
|
2020-11-13 02:09:34 +00:00
|
|
|
else if( footprint->GetAttributes() & FP_SMD )
|
2020-08-26 21:43:38 +00:00
|
|
|
pnpAttrib.m_MountType = GBR_CMP_PNP_METADATA::MOUNT_TYPE_SMD;
|
2019-09-15 15:08:57 +00:00
|
|
|
|
|
|
|
// Add component value info:
|
2020-08-22 21:22:54 +00:00
|
|
|
pnpAttrib.m_Value = ConvertNotAllowedCharsInGerber( footprint->Value().GetShownText(),
|
|
|
|
allowUtf8, true );
|
2019-09-15 15:08:57 +00:00
|
|
|
|
|
|
|
// Add component footprint info:
|
2019-10-06 11:39:50 +00:00
|
|
|
wxString fp_info = FROM_UTF8( footprint->GetFPID().GetLibItemName().c_str() );
|
2019-10-24 16:50:51 +00:00
|
|
|
pnpAttrib.m_Footprint = ConvertNotAllowedCharsInGerber( fp_info, allowUtf8, true );
|
2019-10-06 11:39:50 +00:00
|
|
|
|
|
|
|
// Add footprint lib name:
|
|
|
|
fp_info = FROM_UTF8( footprint->GetFPID().GetLibNickname().c_str() );
|
2019-10-24 16:50:51 +00:00
|
|
|
pnpAttrib.m_LibraryName = ConvertNotAllowedCharsInGerber( fp_info, allowUtf8, true );
|
2019-09-15 15:08:57 +00:00
|
|
|
|
|
|
|
gbr_metadata.m_NetlistMetadata.SetExtraData( pnpAttrib.FormatCmpPnPMetadata() );
|
|
|
|
|
2019-11-17 09:11:29 +00:00
|
|
|
wxPoint flash_pos = footprint->GetPosition();
|
2019-09-15 15:08:57 +00:00
|
|
|
|
2019-11-03 19:23:31 +00:00
|
|
|
plotter.FlashPadCircle( flash_pos, flash_position_shape_diam, FILLED, &gbr_metadata );
|
2019-09-15 15:08:57 +00:00
|
|
|
gbr_metadata.m_NetlistMetadata.ClearExtraData();
|
|
|
|
|
2019-10-17 10:41:42 +00:00
|
|
|
// Now some extra metadata is output, avoid blindly clearing the full metadata list
|
|
|
|
gbr_metadata.m_NetlistMetadata.m_TryKeepPreviousAttributes = true;
|
|
|
|
|
2019-11-01 12:35:42 +00:00
|
|
|
// We plot the footprint courtyard when possible.
|
|
|
|
// If not, the pads bounding box will be used.
|
|
|
|
bool useFpPadsBbox = true;
|
2020-11-20 13:55:10 +00:00
|
|
|
bool onBack = aLayer == B_Cu;
|
2019-11-01 12:35:42 +00:00
|
|
|
|
2020-10-25 15:02:49 +00:00
|
|
|
footprint->BuildPolyCourtyards();
|
|
|
|
|
2020-11-20 13:55:10 +00:00
|
|
|
int checkFlag = onBack ? MALFORMED_B_COURTYARD : MALFORMED_F_COURTYARD;
|
|
|
|
|
|
|
|
if( ( footprint->GetFlags() & checkFlag ) == 0 )
|
2019-09-15 15:08:57 +00:00
|
|
|
{
|
|
|
|
gbr_metadata.SetApertureAttrib( GBR_APERTURE_METADATA::GBR_APERTURE_ATTRIB_CMP_COURTYARD );
|
|
|
|
|
2020-11-20 13:55:10 +00:00
|
|
|
SHAPE_POLY_SET& courtyard = onBack ? footprint->GetPolyCourtyardBack()
|
|
|
|
: footprint->GetPolyCourtyardFront();
|
2019-09-15 15:08:57 +00:00
|
|
|
|
|
|
|
for( int ii = 0; ii < courtyard.OutlineCount(); ii++ )
|
|
|
|
{
|
|
|
|
SHAPE_LINE_CHAIN poly = courtyard.Outline( ii );
|
2019-11-01 12:35:42 +00:00
|
|
|
|
|
|
|
if( !poly.PointCount() )
|
|
|
|
continue;
|
|
|
|
|
|
|
|
useFpPadsBbox = false;
|
2020-10-15 01:45:20 +00:00
|
|
|
plotter.PLOTTER::PlotPoly( poly, FILL_TYPE::NO_FILL, line_thickness, &gbr_metadata );
|
2019-09-15 15:08:57 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-11-01 12:35:42 +00:00
|
|
|
if( useFpPadsBbox )
|
|
|
|
{
|
|
|
|
gbr_metadata.SetApertureAttrib( GBR_APERTURE_METADATA::GBR_APERTURE_ATTRIB_CMP_FOOTPRINT );
|
|
|
|
|
|
|
|
// bbox of fp pads, pos 0, rot 0, non flipped
|
|
|
|
EDA_RECT bbox = footprint->GetFpPadsLocalBbox();
|
|
|
|
|
|
|
|
// negate bbox Y values if the fp is flipped (always flipped around X axis
|
|
|
|
// in Gerber P&P files).
|
|
|
|
int y_sign = aLayer == B_Cu ? -1 : 1;
|
|
|
|
|
|
|
|
SHAPE_LINE_CHAIN poly;
|
|
|
|
poly.Append( bbox.GetLeft(), y_sign*bbox.GetTop() );
|
|
|
|
poly.Append( bbox.GetLeft(), y_sign*bbox.GetBottom() );
|
|
|
|
poly.Append( bbox.GetRight(), y_sign*bbox.GetBottom() );
|
|
|
|
poly.Append( bbox.GetRight(), y_sign*bbox.GetTop() );
|
|
|
|
poly.SetClosed( true );
|
|
|
|
|
|
|
|
poly.Rotate( -footprint->GetOrientationRadians(), VECTOR2I( 0, 0 ) );
|
2019-11-17 09:11:29 +00:00
|
|
|
poly.Move( footprint->GetPosition() );
|
2020-10-15 01:45:20 +00:00
|
|
|
plotter.PLOTTER::PlotPoly( poly, FILL_TYPE::NO_FILL, line_thickness, &gbr_metadata );
|
2019-11-01 12:35:42 +00:00
|
|
|
}
|
|
|
|
|
2020-11-12 22:30:02 +00:00
|
|
|
std::vector<PAD*>pad_key_list;
|
2019-09-15 15:08:57 +00:00
|
|
|
|
|
|
|
if( m_plotPad1Marker )
|
|
|
|
{
|
2019-10-06 11:39:50 +00:00
|
|
|
findPads1( pad_key_list, footprint );
|
2019-09-15 15:08:57 +00:00
|
|
|
|
2020-11-12 22:30:02 +00:00
|
|
|
for( PAD* pad1 : pad_key_list )
|
2019-09-15 15:08:57 +00:00
|
|
|
{
|
|
|
|
gbr_metadata.SetApertureAttrib(
|
|
|
|
GBR_APERTURE_METADATA::GBR_APERTURE_ATTRIB_PAD1_POSITION );
|
|
|
|
|
2019-11-17 09:11:29 +00:00
|
|
|
gbr_metadata.SetPadName( pad1->GetName(), allowUtf8, true );
|
|
|
|
|
|
|
|
gbr_metadata.SetPadPinFunction( pad1->GetPinFunction(), allowUtf8, true );
|
|
|
|
|
2019-09-15 15:08:57 +00:00
|
|
|
gbr_metadata.SetNetAttribType( GBR_NETLIST_METADATA::GBR_NETINFO_PAD );
|
|
|
|
|
2019-11-03 19:23:31 +00:00
|
|
|
// Flashes a diamond at pad position:
|
2019-11-17 09:11:29 +00:00
|
|
|
plotter.FlashRegularPolygon( pad1->GetPosition(),
|
|
|
|
pad1_mark_size,
|
2019-11-03 19:23:31 +00:00
|
|
|
4, 0.0, FILLED, &gbr_metadata );
|
2019-09-15 15:08:57 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if( m_plotOtherPadsMarker )
|
|
|
|
{
|
|
|
|
|
|
|
|
gbr_metadata.SetApertureAttrib(
|
|
|
|
GBR_APERTURE_METADATA::GBR_APERTURE_ATTRIB_PADOTHER_POSITION );
|
|
|
|
gbr_metadata.SetNetAttribType( GBR_NETLIST_METADATA::GBR_NETINFO_PAD );
|
|
|
|
|
2020-11-12 22:30:02 +00:00
|
|
|
for( PAD* pad: footprint->Pads() )
|
2019-09-15 15:08:57 +00:00
|
|
|
{
|
2019-10-06 11:39:50 +00:00
|
|
|
bool skip_pad = false;
|
|
|
|
|
2020-11-12 22:30:02 +00:00
|
|
|
for( PAD* pad1 : pad_key_list )
|
2019-10-06 11:39:50 +00:00
|
|
|
{
|
|
|
|
if( pad == pad1 ) // Already plotted
|
|
|
|
{
|
|
|
|
skip_pad = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if( skip_pad )
|
2019-09-15 15:08:57 +00:00
|
|
|
continue;
|
|
|
|
|
|
|
|
// Skip also pads not on the current layer, like pads only
|
|
|
|
// on a tech layer
|
|
|
|
if( !pad->IsOnLayer( aLayer ) )
|
|
|
|
continue;
|
|
|
|
|
2019-11-17 09:11:29 +00:00
|
|
|
gbr_metadata.SetPadName( pad->GetName(), allowUtf8, true );
|
|
|
|
|
|
|
|
gbr_metadata.SetPadPinFunction( pad->GetPinFunction(), allowUtf8, true );
|
2019-09-15 15:08:57 +00:00
|
|
|
|
|
|
|
// Flashes a round, 0 sized round shape at pad position
|
2019-11-17 09:11:29 +00:00
|
|
|
plotter.FlashPadCircle( pad->GetPosition(),
|
|
|
|
other_pads_mark_size,
|
2019-10-06 11:39:50 +00:00
|
|
|
FILLED, &gbr_metadata );
|
2019-09-15 15:08:57 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-10-06 11:39:50 +00:00
|
|
|
plotter.ClearAllAttributes(); // Unconditionally close all .TO attributes
|
2019-09-15 15:08:57 +00:00
|
|
|
|
|
|
|
cmp_count++;
|
|
|
|
}
|
|
|
|
|
2019-11-03 19:23:31 +00:00
|
|
|
// Plot board outlines, if requested
|
|
|
|
if( aIncludeBrdEdges )
|
|
|
|
{
|
|
|
|
brd_plotter.SetLayerSet( LSET( Edge_Cuts ) );
|
|
|
|
|
|
|
|
// Plot edge layer and graphic items
|
|
|
|
brd_plotter.PlotBoardGraphicItems();
|
|
|
|
|
|
|
|
// Draw footprint other graphic items:
|
2020-11-13 15:15:52 +00:00
|
|
|
for( FOOTPRINT* footprint : fp_list )
|
2019-11-03 19:23:31 +00:00
|
|
|
{
|
2020-10-04 14:19:33 +00:00
|
|
|
for( BOARD_ITEM* item : footprint->GraphicalItems() )
|
2019-11-03 19:23:31 +00:00
|
|
|
{
|
2020-10-04 14:19:33 +00:00
|
|
|
if( item->Type() == PCB_FP_SHAPE_T && item->GetLayer() == Edge_Cuts )
|
2020-10-04 23:34:59 +00:00
|
|
|
brd_plotter.PlotFootprintGraphicItem( (FP_SHAPE*) item );
|
2019-11-03 19:23:31 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2019-09-15 15:08:57 +00:00
|
|
|
plotter.EndPlot();
|
|
|
|
|
|
|
|
return cmp_count;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
double PLACEFILE_GERBER_WRITER::mapRotationAngle( double aAngle )
|
|
|
|
{
|
|
|
|
// convert a kicad footprint orientation to gerber rotation, depending on the layer
|
|
|
|
// Currently, same notation as kicad
|
|
|
|
return aAngle;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2020-11-13 15:15:52 +00:00
|
|
|
void PLACEFILE_GERBER_WRITER::findPads1( std::vector<PAD*>& aPadList, FOOTPRINT* aFootprint ) const
|
2019-09-15 15:08:57 +00:00
|
|
|
{
|
|
|
|
// Fint the pad "1" or pad "A1"
|
|
|
|
// this is possible only if only one pad is found
|
|
|
|
// Usefull to place a marker in this position
|
|
|
|
|
2020-11-12 22:30:02 +00:00
|
|
|
for( PAD* pad : aFootprint->Pads() )
|
2019-09-15 15:08:57 +00:00
|
|
|
{
|
|
|
|
if( !pad->IsOnLayer( m_layer ) )
|
|
|
|
continue;
|
|
|
|
|
|
|
|
if( pad->GetName() == "1" || pad->GetName() == "A1")
|
2019-10-06 11:39:50 +00:00
|
|
|
aPadList.push_back( pad );
|
2019-09-15 15:08:57 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const wxString PLACEFILE_GERBER_WRITER::GetPlaceFileName( const wxString& aFullBaseFilename,
|
|
|
|
PCB_LAYER_ID aLayer ) const
|
|
|
|
{
|
|
|
|
// Gerber files extension is always .gbr.
|
|
|
|
// Therefore, to mark pnp files, add "-pnp" to the filename, and a layer id.
|
|
|
|
wxFileName fn = aFullBaseFilename;
|
|
|
|
|
|
|
|
wxString post_id = "-pnp_";
|
|
|
|
post_id += aLayer == B_Cu ? "bottom" : "top";
|
|
|
|
fn.SetName( fn.GetName() + post_id );
|
|
|
|
fn.SetExt( GerberFileExtension );
|
|
|
|
|
|
|
|
return fn.GetFullPath();
|
|
|
|
}
|