kicad/pcbnew/exporters/gendrill_file_writer_base.cpp

461 lines
16 KiB
C++

/*
* This program source code file is part of KiCad, a free EDA CAD application.
*
* Copyright (C) 2017 Jean_Pierre Charras <jp.charras at wanadoo.fr>
* Copyright (C) 2015 SoftPLC Corporation, Dick Hollenbeck <dick@softplc.com>
* Copyright (C) 1992-2024 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 2
* 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, you may find one here:
* http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
* or you may search the http://www.gnu.org website for the version 2 license,
* or you may write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*/
#include <board.h>
#include <footprint.h>
#include <pad.h>
#include <pcb_track.h>
#include <collectors.h>
#include <reporter.h>
#include <richio.h>
#include <gendrill_file_writer_base.h>
/* Helper function for sorting hole list.
* Compare function used for sorting holes type type:
* plated then not plated
* then by increasing diameter value
* then by attribute type (vias, pad, mechanical)
* then by X then Y position
*/
static bool cmpHoleSorting( const HOLE_INFO& a, const HOLE_INFO& b )
{
if( a.m_Hole_NotPlated != b.m_Hole_NotPlated )
return b.m_Hole_NotPlated;
if( a.m_Hole_Diameter != b.m_Hole_Diameter )
return a.m_Hole_Diameter < b.m_Hole_Diameter;
// At this point (same diameter, same plated type), group by attribute
// type (via, pad, mechanical, although currently only not plated pads are mechanical)
if( a.m_HoleAttribute != b.m_HoleAttribute )
return a.m_HoleAttribute < b.m_HoleAttribute;
// At this point (same diameter, same type), sort by X then Y position.
// This is optimal for drilling and make the file reproducible as long as holes
// have not changed, even if the data order has changed.
if( a.m_Hole_Pos.x != b.m_Hole_Pos.x )
return a.m_Hole_Pos.x < b.m_Hole_Pos.x;
return a.m_Hole_Pos.y < b.m_Hole_Pos.y;
}
void GENDRILL_WRITER_BASE::buildHolesList( DRILL_LAYER_PAIR aLayerPair,
bool aGenerateNPTH_list )
{
HOLE_INFO new_hole;
m_holeListBuffer.clear();
m_toolListBuffer.clear();
wxASSERT( aLayerPair.first < aLayerPair.second ); // fix the caller
// build hole list for vias
if( ! aGenerateNPTH_list ) // vias are always plated !
{
for( auto track : m_pcb->Tracks() )
{
if( track->Type() != PCB_VIA_T )
continue;
PCB_VIA* via = static_cast<PCB_VIA*>( track );
int hole_sz = via->GetDrillValue();
if( hole_sz == 0 ) // Should not occur.
continue;
new_hole.m_ItemParent = via;
if( aLayerPair == DRILL_LAYER_PAIR( F_Cu, B_Cu ) )
new_hole.m_HoleAttribute = HOLE_ATTRIBUTE::HOLE_VIA_THROUGH;
else
new_hole.m_HoleAttribute = HOLE_ATTRIBUTE::HOLE_VIA_BURIED;
new_hole.m_Tool_Reference = -1; // Flag value for Not initialized
new_hole.m_Hole_Orient = ANGLE_0;
new_hole.m_Hole_Diameter = hole_sz;
new_hole.m_Hole_NotPlated = false;
new_hole.m_Hole_Size.x = new_hole.m_Hole_Size.y = new_hole.m_Hole_Diameter;
new_hole.m_Hole_Shape = 0; // hole shape: round
new_hole.m_Hole_Pos = via->GetStart();
via->LayerPair( &new_hole.m_Hole_Top_Layer, &new_hole.m_Hole_Bottom_Layer );
// LayerPair() returns params with m_Hole_Bottom_Layer > m_Hole_Top_Layer
// Remember: top layer = 0 and bottom layer = 31 for through hole vias
// Any captured via should be from aLayerPair.first to aLayerPair.second exactly.
if( new_hole.m_Hole_Top_Layer != aLayerPair.first ||
new_hole.m_Hole_Bottom_Layer != aLayerPair.second )
continue;
m_holeListBuffer.push_back( new_hole );
}
}
if( aLayerPair == DRILL_LAYER_PAIR( F_Cu, B_Cu ) )
{
// add holes for thru hole pads
for( FOOTPRINT* footprint : m_pcb->Footprints() )
{
for( PAD* pad : footprint->Pads() )
{
if( !m_merge_PTH_NPTH )
{
if( !aGenerateNPTH_list && pad->GetAttribute() == PAD_ATTRIB::NPTH )
continue;
if( aGenerateNPTH_list && pad->GetAttribute() != PAD_ATTRIB::NPTH )
continue;
}
if( pad->GetDrillSize().x == 0 )
continue;
new_hole.m_ItemParent = pad;
new_hole.m_Hole_NotPlated = (pad->GetAttribute() == PAD_ATTRIB::NPTH);
new_hole.m_HoleAttribute = new_hole.m_Hole_NotPlated
? HOLE_ATTRIBUTE::HOLE_MECHANICAL
: HOLE_ATTRIBUTE::HOLE_PAD;
new_hole.m_Tool_Reference = -1; // Flag is: Not initialized
new_hole.m_Hole_Orient = pad->GetOrientation();
new_hole.m_Hole_Shape = 0; // hole shape: round
new_hole.m_Hole_Diameter = std::min( pad->GetDrillSize().x, pad->GetDrillSize().y );
new_hole.m_Hole_Size.x = new_hole.m_Hole_Size.y = new_hole.m_Hole_Diameter;
// Convert oblong holes that are actually circular into drill hits
if( pad->GetDrillShape() != PAD_DRILL_SHAPE::CIRCLE &&
pad->GetDrillSizeX() != pad->GetDrillSizeY() )
{
new_hole.m_Hole_Shape = 1; // oval flag set
}
new_hole.m_Hole_Size = pad->GetDrillSize();
new_hole.m_Hole_Pos = pad->GetPosition(); // hole position
new_hole.m_Hole_Bottom_Layer = B_Cu;
new_hole.m_Hole_Top_Layer = F_Cu; // pad holes are through holes
m_holeListBuffer.push_back( new_hole );
}
}
}
// Sort holes per increasing diameter value (and for each dimater, by position)
sort( m_holeListBuffer.begin(), m_holeListBuffer.end(), cmpHoleSorting );
// build the tool list
int last_hole = -1; // Set to not initialized (this is a value not used
// for m_holeListBuffer[ii].m_Hole_Diameter)
bool last_notplated_opt = false;
HOLE_ATTRIBUTE last_attribute = HOLE_ATTRIBUTE::HOLE_UNKNOWN;
DRILL_TOOL new_tool( 0, false );
unsigned jj;
for( unsigned ii = 0; ii < m_holeListBuffer.size(); ii++ )
{
if( m_holeListBuffer[ii].m_Hole_Diameter != last_hole
|| m_holeListBuffer[ii].m_Hole_NotPlated != last_notplated_opt
#if USE_ATTRIB_FOR_HOLES
|| m_holeListBuffer[ii].m_HoleAttribute != last_attribute
#endif
)
{
new_tool.m_Diameter = m_holeListBuffer[ii].m_Hole_Diameter;
new_tool.m_Hole_NotPlated = m_holeListBuffer[ii].m_Hole_NotPlated;
new_tool.m_HoleAttribute = m_holeListBuffer[ii].m_HoleAttribute;
m_toolListBuffer.push_back( new_tool );
last_hole = new_tool.m_Diameter;
last_notplated_opt = new_tool.m_Hole_NotPlated;
last_attribute = new_tool.m_HoleAttribute;
}
jj = m_toolListBuffer.size();
if( jj == 0 )
continue; // Should not occurs
m_holeListBuffer[ii].m_Tool_Reference = jj; // Tool value Initialized (value >= 1)
m_toolListBuffer.back().m_TotalCount++;
if( m_holeListBuffer[ii].m_Hole_Shape )
m_toolListBuffer.back().m_OvalCount++;
}
}
std::vector<DRILL_LAYER_PAIR> GENDRILL_WRITER_BASE::getUniqueLayerPairs() const
{
wxASSERT( m_pcb );
PCB_TYPE_COLLECTOR vias;
vias.Collect( m_pcb, { PCB_VIA_T } );
std::set<DRILL_LAYER_PAIR> unique;
DRILL_LAYER_PAIR layer_pair;
for( int i = 0; i < vias.GetCount(); ++i )
{
PCB_VIA* v = static_cast<PCB_VIA*>( vias[i] );
v->LayerPair( &layer_pair.first, &layer_pair.second );
// only make note of blind buried.
// thru hole is placed unconditionally as first in fetched list.
if( layer_pair != DRILL_LAYER_PAIR( F_Cu, B_Cu ) )
unique.insert( layer_pair );
}
std::vector<DRILL_LAYER_PAIR> ret;
ret.emplace_back( F_Cu, B_Cu ); // always first in returned list
for( const DRILL_LAYER_PAIR& pair : unique )
ret.push_back( pair );
return ret;
}
const std::string GENDRILL_WRITER_BASE::layerName( PCB_LAYER_ID aLayer ) const
{
// Generic names here.
switch( aLayer )
{
case F_Cu:
return "front";
case B_Cu:
return "back";
default:
{
// aLayer use even values, and the first internal layer (In1) is B_Cu + 2.
int ly_id = ( aLayer - B_Cu ) / 2;
return StrPrintf( "in%d", ly_id );
}
}
}
const std::string GENDRILL_WRITER_BASE::layerPairName( DRILL_LAYER_PAIR aPair ) const
{
std::string ret = layerName( aPair.first );
ret += '-';
ret += layerName( aPair.second );
return ret;
}
const wxString GENDRILL_WRITER_BASE::getDrillFileName( DRILL_LAYER_PAIR aPair, bool aNPTH,
bool aMerge_PTH_NPTH ) const
{
wxASSERT( m_pcb );
wxString extend;
if( aNPTH )
extend = wxT( "-NPTH" );
else if( aPair == DRILL_LAYER_PAIR( F_Cu, B_Cu ) )
{
if( !aMerge_PTH_NPTH )
extend = wxT( "-PTH" );
// if merged, extend with nothing
}
else
{
extend += '-';
extend += layerPairName( aPair );
}
wxFileName fn = m_pcb->GetFileName();
fn.SetName( fn.GetName() + extend );
fn.SetExt( m_drillFileExtension );
wxString ret = fn.GetFullName();
return ret;
}
bool GENDRILL_WRITER_BASE::CreateMapFilesSet( const wxString& aPlotDirectory,
REPORTER * aReporter )
{
wxFileName fn;
wxString msg;
std::vector<DRILL_LAYER_PAIR> hole_sets = getUniqueLayerPairs();
// append a pair representing the NPTH set of holes, for separate drill files.
if( !m_merge_PTH_NPTH )
hole_sets.emplace_back( F_Cu, B_Cu );
for( std::vector<DRILL_LAYER_PAIR>::const_iterator it = hole_sets.begin();
it != hole_sets.end(); ++it )
{
DRILL_LAYER_PAIR pair = *it;
// For separate drill files, the last layer pair is the NPTH drill file.
bool doing_npth = m_merge_PTH_NPTH ? false : ( it == hole_sets.end() - 1 );
buildHolesList( pair, doing_npth );
// The file is created if it has holes, or if it is the non plated drill file
// to be sure the NPTH file is up to date in separate files mode.
// Also a PTH drill file is always created, to be sure at least one plated hole drill file
// is created (do not create any PTH drill file can be seen as not working drill generator).
if( getHolesCount() > 0 || doing_npth || pair == DRILL_LAYER_PAIR( F_Cu, B_Cu ) )
{
fn = GENDRILL_WRITER_BASE::getDrillFileName( pair, doing_npth, m_merge_PTH_NPTH );
fn.SetPath( aPlotDirectory );
fn.SetExt( wxEmptyString ); // Will be added by GenDrillMap
wxString fullfilename = fn.GetFullPath() + wxT( "-drl_map" );
fullfilename << wxT(".") << GetDefaultPlotExtension( m_mapFileFmt );
bool success = genDrillMapFile( fullfilename, m_mapFileFmt );
if( ! success )
{
if( aReporter )
{
msg.Printf( _( "Failed to create file '%s'." ), fullfilename );
aReporter->Report( msg, RPT_SEVERITY_ERROR );
}
return false;
}
else
{
if( aReporter )
{
msg.Printf( _( "Created file '%s'." ), fullfilename );
aReporter->Report( msg, RPT_SEVERITY_ACTION );
}
}
}
}
return true;
}
const wxString GENDRILL_WRITER_BASE::BuildFileFunctionAttributeString(
DRILL_LAYER_PAIR aLayerPair, TYPE_FILE aHoleType,
bool aCompatNCdrill ) const
{
// Build a wxString containing the .FileFunction attribute for drill files.
// %TF.FileFunction,Plated[NonPlated],layer1num,layer2num,PTH[NPTH][Blind][Buried],Drill[Route][Mixed]*%
wxString text;
if( aCompatNCdrill )
text = wxT( "; #@! " );
else
text = wxT( "%" );
text << wxT( "TF.FileFunction," );
if( aHoleType == NPTH_FILE )
text << wxT( "NonPlated," );
else if( aHoleType == MIXED_FILE ) // only for Excellon format
text << wxT( "MixedPlating," );
else
text << wxT( "Plated," );
int layer1 = aLayerPair.first;
int layer2 = aLayerPair.second;
// In Gerber files, layers num are 1 to copper layer count instead of F_Cu to B_Cu
// (0 to copper layer count-1)
// Note also for a n copper layers board, gerber layers num are 1 ... n
//
// Copper layers use even values, so the layer id in file is
// (Copper layer id) /2 + 1 if layer is not B_Cu
if( layer1 == F_Cu )
layer1 = 1;
else
layer1 = ( ( layer1 - B_Cu ) / 2 ) + 1;
if( layer2 == B_Cu )
layer2 = m_pcb->GetCopperLayerCount();
else
layer2 = ( ( layer2 - B_Cu ) / 2) + 1;
text << layer1 << wxT( "," ) << layer2;
// Now add PTH or NPTH or Blind or Buried attribute
int toplayer = 1;
int bottomlayer = m_pcb->GetCopperLayerCount();
if( aHoleType == NPTH_FILE )
text << wxT( ",NPTH" );
else if( aHoleType == MIXED_FILE ) // only for Excellon format
{
// write nothing
}
else if( layer1 == toplayer && layer2 == bottomlayer )
text << wxT( ",PTH" );
else if( layer1 == toplayer || layer2 == bottomlayer )
text << wxT( ",Blind" );
else
text << wxT( ",Buried" );
// In NC drill file, these previous parameters should be enough:
if( aCompatNCdrill )
return text;
// Now add Drill or Route or Mixed:
// file containing only round holes have Drill attribute
// file containing only oblong holes have Routed attribute
// file containing both holes have Mixed attribute
bool hasOblong = false;
bool hasDrill = false;
for( unsigned ii = 0; ii < m_holeListBuffer.size(); ii++ )
{
const HOLE_INFO& hole_descr = m_holeListBuffer[ii];
if( hole_descr.m_Hole_Shape ) // m_Hole_Shape not 0 is an oblong hole)
hasOblong = true;
else
hasDrill = true;
}
if( hasOblong && hasDrill )
text << wxT( ",Mixed" );
else if( hasDrill )
text << wxT( ",Drill" );
else if( hasOblong )
text << wxT( ",Rout" );
// else: empty file.
// End of .FileFunction attribute:
text << wxT( "*%" );
return text;
}