1 /*
2  * This program source code file is part of KiCad, a free EDA CAD application.
3  *
4  * Copyright (C) 2017 Jean_Pierre Charras <jp.charras at wanadoo.fr>
5  * Copyright (C) 2015 SoftPLC Corporation, Dick Hollenbeck <dick@softplc.com>
6  * Copyright (C) 1992-2021 KiCad Developers, see AUTHORS.txt for contributors.
7  *
8  * This program is free software; you can redistribute it and/or
9  * modify it under the terms of the GNU General Public License
10  * as published by the Free Software Foundation; either version 2
11  * of the License, or (at your option) any later version.
12  *
13  * This program is distributed in the hope that it will be useful,
14  * but WITHOUT ANY WARRANTY; without even the implied warranty of
15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16  * GNU General Public License for more details.
17  *
18  * You should have received a copy of the GNU General Public License
19  * along with this program; if not, you may find one here:
20  * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
21  * or you may search the http://www.gnu.org website for the version 2 license,
22  * or you may write to the Free Software Foundation, Inc.,
23  * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
24  */
25 
26 #include <board.h>
27 #include <footprint.h>
28 #include <pad.h>
29 #include <pcb_track.h>
30 #include <collectors.h>
31 #include <reporter.h>
32 
33 #include <gendrill_file_writer_base.h>
34 
35 
36 /* Helper function for sorting hole list.
37  * Compare function used for sorting holes type type:
38  * plated then not plated
39  * then by increasing diameter value
40  * then by attribute type (vias, pad, mechanical)
41  * then by X then Y position
42  */
cmpHoleSorting(const HOLE_INFO & a,const HOLE_INFO & b)43 static bool cmpHoleSorting( const HOLE_INFO& a, const HOLE_INFO& b )
44 {
45     if( a.m_Hole_NotPlated != b.m_Hole_NotPlated )
46         return b.m_Hole_NotPlated;
47 
48     if( a.m_Hole_Diameter != b.m_Hole_Diameter )
49         return a.m_Hole_Diameter < b.m_Hole_Diameter;
50 
51     // At this point (same diameter, same plated type), group by attribute
52     // type (via, pad, mechanical, although currently only not plated pads are mechanical)
53     if( a.m_HoleAttribute != b.m_HoleAttribute )
54         return a.m_HoleAttribute < b.m_HoleAttribute;
55 
56     // At this point (same diameter, same type), sort by X then Y position.
57     // This is optimal for drilling and make the file reproducible as long as holes
58     // have not changed, even if the data order has changed.
59     if( a.m_Hole_Pos.x != b.m_Hole_Pos.x )
60         return a.m_Hole_Pos.x < b.m_Hole_Pos.x;
61 
62     return a.m_Hole_Pos.y < b.m_Hole_Pos.y;
63 }
64 
65 
buildHolesList(DRILL_LAYER_PAIR aLayerPair,bool aGenerateNPTH_list)66 void GENDRILL_WRITER_BASE::buildHolesList( DRILL_LAYER_PAIR aLayerPair,
67                                            bool aGenerateNPTH_list )
68 {
69     HOLE_INFO new_hole;
70 
71     m_holeListBuffer.clear();
72     m_toolListBuffer.clear();
73 
74     wxASSERT( aLayerPair.first < aLayerPair.second );  // fix the caller
75 
76     // build hole list for vias
77     if( ! aGenerateNPTH_list )  // vias are always plated !
78     {
79         for( auto track : m_pcb->Tracks() )
80         {
81             if( track->Type() != PCB_VIA_T )
82                 continue;
83 
84             PCB_VIA* via = static_cast<PCB_VIA*>( track );
85             int      hole_sz = via->GetDrillValue();
86 
87             if( hole_sz == 0 )   // Should not occur.
88                 continue;
89 
90             new_hole.m_ItemParent = via;
91 
92             if( aLayerPair == DRILL_LAYER_PAIR( F_Cu, B_Cu ) )
93                 new_hole.m_HoleAttribute = HOLE_ATTRIBUTE::HOLE_VIA_THROUGH;
94             else
95                 new_hole.m_HoleAttribute = HOLE_ATTRIBUTE::HOLE_VIA_BURIED;
96 
97             new_hole.m_Tool_Reference = -1;         // Flag value for Not initialized
98             new_hole.m_Hole_Orient    = 0;
99             new_hole.m_Hole_Diameter  = hole_sz;
100             new_hole.m_Hole_NotPlated = false;
101             new_hole.m_Hole_Size.x = new_hole.m_Hole_Size.y = new_hole.m_Hole_Diameter;
102 
103             new_hole.m_Hole_Shape = 0;              // hole shape: round
104             new_hole.m_Hole_Pos = via->GetStart();
105 
106             via->LayerPair( &new_hole.m_Hole_Top_Layer, &new_hole.m_Hole_Bottom_Layer );
107 
108             // LayerPair() returns params with m_Hole_Bottom_Layer > m_Hole_Top_Layer
109             // Remember: top layer = 0 and bottom layer = 31 for through hole vias
110             // Any captured via should be from aLayerPair.first to aLayerPair.second exactly.
111             if( new_hole.m_Hole_Top_Layer    != aLayerPair.first ||
112                 new_hole.m_Hole_Bottom_Layer != aLayerPair.second )
113                 continue;
114 
115             m_holeListBuffer.push_back( new_hole );
116         }
117     }
118 
119     if( aLayerPair == DRILL_LAYER_PAIR( F_Cu, B_Cu ) )
120     {
121         // add holes for thru hole pads
122         for( FOOTPRINT* footprint : m_pcb->Footprints() )
123         {
124             for( PAD* pad : footprint->Pads() )
125             {
126                 if( !m_merge_PTH_NPTH )
127                 {
128                     if( !aGenerateNPTH_list && pad->GetAttribute() == PAD_ATTRIB::NPTH )
129                         continue;
130 
131                     if( aGenerateNPTH_list && pad->GetAttribute() != PAD_ATTRIB::NPTH )
132                         continue;
133                 }
134 
135                 if( pad->GetDrillSize().x == 0 )
136                     continue;
137 
138                 new_hole.m_ItemParent     = pad;
139                 new_hole.m_Hole_NotPlated = (pad->GetAttribute() == PAD_ATTRIB::NPTH);
140                 new_hole.m_HoleAttribute  = new_hole.m_Hole_NotPlated
141                                                 ? HOLE_ATTRIBUTE::HOLE_MECHANICAL
142                                                 : HOLE_ATTRIBUTE::HOLE_PAD;
143                 new_hole.m_Tool_Reference = -1;         // Flag is: Not initialized
144                 new_hole.m_Hole_Orient    = pad->GetOrientation();
145                 new_hole.m_Hole_Shape     = 0;           // hole shape: round
146                 new_hole.m_Hole_Diameter  = std::min( pad->GetDrillSize().x, pad->GetDrillSize().y );
147                 new_hole.m_Hole_Size.x    = new_hole.m_Hole_Size.y = new_hole.m_Hole_Diameter;
148 
149                 if( pad->GetDrillShape() != PAD_DRILL_SHAPE_CIRCLE )
150                     new_hole.m_Hole_Shape = 1; // oval flag set
151 
152                 new_hole.m_Hole_Size         = pad->GetDrillSize();
153                 new_hole.m_Hole_Pos          = pad->GetPosition();  // hole position
154                 new_hole.m_Hole_Bottom_Layer = B_Cu;
155                 new_hole.m_Hole_Top_Layer    = F_Cu;    // pad holes are through holes
156                 m_holeListBuffer.push_back( new_hole );
157             }
158         }
159     }
160 
161     // Sort holes per increasing diameter value (and for each dimater, by position)
162     sort( m_holeListBuffer.begin(), m_holeListBuffer.end(), cmpHoleSorting );
163 
164     // build the tool list
165     int last_hole = -1;     // Set to not initialized (this is a value not used
166                             // for m_holeListBuffer[ii].m_Hole_Diameter)
167     bool last_notplated_opt = false;
168     HOLE_ATTRIBUTE last_attribute = HOLE_ATTRIBUTE::HOLE_UNKNOWN;
169 
170     DRILL_TOOL new_tool( 0, false );
171     unsigned   jj;
172 
173     for( unsigned ii = 0; ii < m_holeListBuffer.size(); ii++ )
174     {
175         if( m_holeListBuffer[ii].m_Hole_Diameter != last_hole
176             || m_holeListBuffer[ii].m_Hole_NotPlated != last_notplated_opt
177 #if USE_ATTRIB_FOR_HOLES
178             || m_holeListBuffer[ii].m_HoleAttribute != last_attribute
179 #endif
180             )
181         {
182             new_tool.m_Diameter = m_holeListBuffer[ii].m_Hole_Diameter;
183             new_tool.m_Hole_NotPlated = m_holeListBuffer[ii].m_Hole_NotPlated;
184             new_tool.m_HoleAttribute = m_holeListBuffer[ii].m_HoleAttribute;
185             m_toolListBuffer.push_back( new_tool );
186             last_hole = new_tool.m_Diameter;
187             last_notplated_opt = new_tool.m_Hole_NotPlated;
188             last_attribute = new_tool.m_HoleAttribute;
189         }
190 
191         jj = m_toolListBuffer.size();
192 
193         if( jj == 0 )
194             continue;                                        // Should not occurs
195 
196         m_holeListBuffer[ii].m_Tool_Reference = jj;          // Tool value Initialized (value >= 1)
197 
198         m_toolListBuffer.back().m_TotalCount++;
199 
200         if( m_holeListBuffer[ii].m_Hole_Shape )
201             m_toolListBuffer.back().m_OvalCount++;
202     }
203 }
204 
205 
getUniqueLayerPairs() const206 std::vector<DRILL_LAYER_PAIR> GENDRILL_WRITER_BASE::getUniqueLayerPairs() const
207 {
208     wxASSERT( m_pcb );
209 
210     static const KICAD_T interesting_stuff_to_collect[] = {
211         PCB_VIA_T,
212         EOT
213     };
214 
215     PCB_TYPE_COLLECTOR  vias;
216 
217     vias.Collect( m_pcb, interesting_stuff_to_collect );
218 
219     std::set< DRILL_LAYER_PAIR >  unique;
220 
221     DRILL_LAYER_PAIR  layer_pair;
222 
223     for( int i = 0; i < vias.GetCount(); ++i )
224     {
225         PCB_VIA*  v = static_cast<PCB_VIA*>( vias[i] );
226 
227         v->LayerPair( &layer_pair.first, &layer_pair.second );
228 
229         // only make note of blind buried.
230         // thru hole is placed unconditionally as first in fetched list.
231         if( layer_pair != DRILL_LAYER_PAIR( F_Cu, B_Cu ) )
232         {
233             unique.insert( layer_pair );
234         }
235     }
236 
237     std::vector<DRILL_LAYER_PAIR>    ret;
238 
239     ret.emplace_back( F_Cu, B_Cu );      // always first in returned list
240 
241     for( std::set<DRILL_LAYER_PAIR>::const_iterator it = unique.begin(); it != unique.end(); ++it )
242         ret.push_back( *it );
243 
244     return ret;
245 }
246 
247 
layerName(PCB_LAYER_ID aLayer) const248 const std::string GENDRILL_WRITER_BASE::layerName( PCB_LAYER_ID aLayer ) const
249 {
250     // Generic names here.
251     switch( aLayer )
252     {
253     case F_Cu:
254         return "front";
255     case B_Cu:
256         return "back";
257     default:
258         return StrPrintf( "in%d", aLayer );
259     }
260 }
261 
262 
layerPairName(DRILL_LAYER_PAIR aPair) const263 const std::string GENDRILL_WRITER_BASE::layerPairName( DRILL_LAYER_PAIR aPair ) const
264 {
265     std::string ret = layerName( aPair.first );
266     ret += '-';
267     ret += layerName( aPair.second );
268 
269     return ret;
270 }
271 
272 
getDrillFileName(DRILL_LAYER_PAIR aPair,bool aNPTH,bool aMerge_PTH_NPTH) const273 const wxString GENDRILL_WRITER_BASE::getDrillFileName( DRILL_LAYER_PAIR aPair, bool aNPTH,
274                                                        bool aMerge_PTH_NPTH ) const
275 {
276     wxASSERT( m_pcb );
277 
278     wxString    extend;
279 
280     if( aNPTH )
281         extend = "-NPTH";
282     else if( aPair == DRILL_LAYER_PAIR( F_Cu, B_Cu ) )
283     {
284         if( !aMerge_PTH_NPTH )
285             extend = "-PTH";
286         // if merged, extend with nothing
287     }
288     else
289     {
290         extend += '-';
291         extend += layerPairName( aPair );
292     }
293 
294     wxFileName  fn = m_pcb->GetFileName();
295 
296     fn.SetName( fn.GetName() + extend );
297     fn.SetExt( m_drillFileExtension );
298 
299     wxString ret = fn.GetFullName();
300 
301     return ret;
302 }
303 
CreateMapFilesSet(const wxString & aPlotDirectory,REPORTER * aReporter)304 void GENDRILL_WRITER_BASE::CreateMapFilesSet( const wxString& aPlotDirectory,
305                                               REPORTER * aReporter )
306 {
307     wxFileName  fn;
308     wxString    msg;
309 
310     std::vector<DRILL_LAYER_PAIR> hole_sets = getUniqueLayerPairs();
311 
312     // append a pair representing the NPTH set of holes, for separate drill files.
313     if( !m_merge_PTH_NPTH )
314         hole_sets.emplace_back( F_Cu, B_Cu );
315 
316     for( std::vector<DRILL_LAYER_PAIR>::const_iterator it = hole_sets.begin();
317          it != hole_sets.end();  ++it )
318     {
319         DRILL_LAYER_PAIR  pair = *it;
320         // For separate drill files, the last layer pair is the NPTH drill file.
321         bool doing_npth = m_merge_PTH_NPTH ? false : ( it == hole_sets.end() - 1 );
322 
323         buildHolesList( pair, doing_npth );
324 
325         // The file is created if it has holes, or if it is the non plated drill file
326         // to be sure the NPTH file is up to date in separate files mode.
327         // Also a PTH drill file is always created, to be sure at least one plated hole drill file
328         // is created (do not create any PTH drill file can be seen as not working drill generator).
329         if( getHolesCount() > 0 || doing_npth || pair == DRILL_LAYER_PAIR( F_Cu, B_Cu ) )
330         {
331             fn = getDrillFileName( pair, doing_npth, m_merge_PTH_NPTH );
332             fn.SetPath( aPlotDirectory );
333 
334             fn.SetExt( wxEmptyString ); // Will be added by GenDrillMap
335             wxString fullfilename = fn.GetFullPath() + wxT( "-drl_map" );
336             fullfilename << wxT(".") << GetDefaultPlotExtension( m_mapFileFmt );
337 
338             bool success = genDrillMapFile( fullfilename, m_mapFileFmt );
339 
340             if( ! success )
341             {
342                 if( aReporter )
343                 {
344                     msg.Printf( _( "Failed to create file '%s'." ), fullfilename );
345                     aReporter->Report( msg, RPT_SEVERITY_ERROR );
346                 }
347 
348                 return;
349             }
350             else
351             {
352                 if( aReporter )
353                 {
354                     msg.Printf( _( "Created file '%s'." ), fullfilename );
355                     aReporter->Report( msg, RPT_SEVERITY_ACTION );
356                 }
357             }
358         }
359     }
360 }
361 
362 
BuildFileFunctionAttributeString(DRILL_LAYER_PAIR aLayerPair,TYPE_FILE aHoleType,bool aCompatNCdrill) const363 const wxString GENDRILL_WRITER_BASE::BuildFileFunctionAttributeString(
364                         DRILL_LAYER_PAIR aLayerPair, TYPE_FILE aHoleType,
365                         bool aCompatNCdrill ) const
366 {
367 // Build a wxString containing the .FileFunction attribute for drill files.
368 // %TF.FileFunction,Plated[NonPlated],layer1num,layer2num,PTH[NPTH][Blind][Buried],Drill[Route][Mixed]*%
369     wxString text;
370 
371     if( aCompatNCdrill )
372         text = "; #@! ";
373     else
374         text = "%";
375 
376     text << "TF.FileFunction,";
377 
378     if( aHoleType == NPTH_FILE )
379         text << "NonPlated,";
380     else if( aHoleType == MIXED_FILE )  // only for Excellon format
381         text << "MixedPlating,";
382     else
383         text << "Plated,";
384 
385     int layer1 = aLayerPair.first;
386     int layer2 = aLayerPair.second;
387     // In Gerber files, layers num are 1 to copper layer count instead of F_Cu to B_Cu
388     // (0 to copper layer count-1)
389     // Note also for a n copper layers board, gerber layers num are 1 ... n
390     layer1 += 1;
391 
392     if( layer2 == B_Cu )
393         layer2 = m_pcb->GetCopperLayerCount();
394     else
395         layer2 += 1;
396 
397     text << layer1 << "," << layer2;
398 
399     // Now add PTH or NPTH or Blind or Buried attribute
400     int toplayer = 1;
401     int bottomlayer = m_pcb->GetCopperLayerCount();
402 
403     if( aHoleType == NPTH_FILE )
404         text << ",NPTH";
405     else if( aHoleType == MIXED_FILE )      // only for Excellon format
406     {
407         // write nothing
408     }
409     else if( layer1 == toplayer && layer2 == bottomlayer )
410         text << ",PTH";
411     else if( layer1 == toplayer || layer2 == bottomlayer )
412         text << ",Blind";
413     else
414         text << ",Buried";
415 
416     // In NC drill file, these previous parameters should be enough:
417     if( aCompatNCdrill )
418         return text;
419 
420 
421     // Now add Drill or Route or Mixed:
422     // file containing only round holes have Drill attribute
423     // file containing only oblong holes have Routed attribute
424     // file containing both holes have Mixed attribute
425     bool hasOblong = false;
426     bool hasDrill = false;
427 
428     for( unsigned ii = 0; ii < m_holeListBuffer.size(); ii++ )
429     {
430         const HOLE_INFO& hole_descr = m_holeListBuffer[ii];
431 
432         if( hole_descr.m_Hole_Shape )   // m_Hole_Shape not 0 is an oblong hole)
433             hasOblong = true;
434         else
435             hasDrill = true;
436     }
437 
438     if( hasOblong && hasDrill )
439         text << ",Mixed";
440     else if( hasDrill )
441         text << ",Drill";
442     else if( hasOblong )
443         text << ",Route";
444 
445     // else: empty file.
446 
447     // End of .FileFunction attribute:
448     text << "*%";
449 
450     return text;
451 }
452