KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_cam_backdrill.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 3
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
20#include <boost/test/unit_test.hpp>
21
22#include <board.h>
23#include <footprint.h>
24#include <pad.h>
25#include <pcb_shape.h>
29#include <pcbnew/pcb_track.h>
30#include <base_units.h>
31
32#include <map>
33
34#include <core/utf8.h>
35
36#include <wx/dir.h>
37#include <wx/ffile.h>
38#include <wx/filename.h>
39#include <wx/tokenzr.h>
40#include <wx/utils.h>
41
42
43namespace
44{
45wxFileName MakeTempDir()
46{
47 wxFileName tempDir( wxFileName::GetTempDir(), wxEmptyString );
48 tempDir.AppendDir( wxString::Format( "kicad-backdrill-%llu",
49 static_cast<unsigned long long>( wxGetUTCTime() ) ) );
50 BOOST_REQUIRE( tempDir.Mkdir( wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) );
51
52 return tempDir;
53}
54} // anonymous namespace
55
56
57BOOST_AUTO_TEST_CASE( BackdrillCamOutputs )
58{
59 wxFileName tempDir = MakeTempDir();
60 wxFileName boardFile( tempDir.GetFullPath(), wxT( "backdrill_board.kicad_pcb" ) );
61
62 BOARD board;
63 board.SetCopperLayerCount( 6 );
64 board.SetFileName( boardFile.GetFullPath() );
65
66 auto via = new PCB_VIA( &board );
67 via->SetPosition( VECTOR2I( 0, 0 ) );
68 via->SetLayerPair( F_Cu, B_Cu );
69 via->SetDrill( pcbIUScale.mmToIU( 0.30 ) );
70 via->SetWidth( pcbIUScale.mmToIU( 0.60 ) );
71 via->SetSecondaryDrillSize( pcbIUScale.mmToIU( 0.20 ) );
72 via->SetSecondaryDrillStartLayer( F_Cu );
73 via->SetSecondaryDrillEndLayer( In3_Cu );
74 via->SetFrontPostMachiningMode( PAD_DRILL_POST_MACHINING_MODE::COUNTERSINK );
75 via->SetFrontPostMachiningSize( pcbIUScale.mmToIU( 0.60 ) );
76 via->SetFrontPostMachiningDepth( pcbIUScale.mmToIU( 0.15 ) );
77 via->SetFrontPostMachiningAngle( 900 );
78 board.Add( via );
79
80 EXCELLON_WRITER excellon( &board );
81 excellon.SetOptions( false, false, VECTOR2I( 0, 0 ), false );
82 excellon.SetFormat( true );
83 BOOST_REQUIRE( excellon.CreateDrillandMapFilesSet( tempDir.GetFullPath(), true, false, nullptr ) );
84
85 wxFileName excellonFile( tempDir.GetFullPath(), wxT( "backdrill_board_Backdrills_Drill_1_4.drl" ) );
86 BOOST_REQUIRE( excellonFile.FileExists() );
87
88 wxFFile excellonStream( excellonFile.GetFullPath(), wxT( "rb" ) );
89 wxString excellonContents;
90 BOOST_REQUIRE( excellonStream.ReadAll( &excellonContents ) );
91 BOOST_CHECK( excellonContents.Contains( wxT( "TF.FileFunction,NonPlated,1,4,Blind" ) ) );
92 BOOST_CHECK( excellonContents.Contains( wxT( "; Backdrill" ) ) );
93 BOOST_CHECK( excellonContents.Contains( wxT( "post-machining" ) ) );
94
95 wxFileName layerPairFile( tempDir.GetFullPath(), wxT( "backdrill_board-front-in3-backdrill.drl" ) );
96 BOOST_REQUIRE( layerPairFile.FileExists() );
97
98 wxFFile layerPairStream( layerPairFile.GetFullPath(), wxT( "rb" ) );
99 wxString layerPairContents;
100 BOOST_REQUIRE( layerPairStream.ReadAll( &layerPairContents ) );
101 BOOST_CHECK( layerPairContents.Contains( wxT( "; backdrill" ) ) );
102
103 wxFileName pthFile( tempDir.GetFullPath(), wxT( "backdrill_board-PTH.drl" ) );
104 BOOST_REQUIRE( pthFile.FileExists() );
105
106 wxFFile pthStream( pthFile.GetFullPath(), wxT( "rb" ) );
107 wxString pthContents;
108 BOOST_REQUIRE( pthStream.ReadAll( &pthContents ) );
109 BOOST_CHECK( pthContents.Contains( wxT( "; Post-machining front countersink dia 0.600mm depth 0.150mm angle 90deg" ) ) );
110
111 GERBER_WRITER gerber( &board );
112 gerber.SetOptions( VECTOR2I( 0, 0 ) );
113 gerber.SetFormat( 6 );
114 BOOST_REQUIRE( gerber.CreateDrillandMapFilesSet( tempDir.GetFullPath(), true, false, false, nullptr ) );
115
116 wxFileName gerberFile( tempDir.GetFullPath(), wxT( "backdrill_board_Backdrills_Drill_1_4-drl.gbr" ) );
117 BOOST_REQUIRE( gerberFile.FileExists() );
118
119 wxFFile gerberStream( gerberFile.GetFullPath(), wxT( "rb" ) );
120 wxString gerberContents;
121 BOOST_REQUIRE( gerberStream.ReadAll( &gerberContents ) );
122 BOOST_CHECK( gerberContents.Contains( wxT( "%TA.AperFunction,BackDrill*%" ) ) );
123 BOOST_CHECK( gerberContents.Contains( wxT( "%TF.FileFunction,NonPlated,1,4,Blind,Drill*%" ) ) );
124
125 wxFileName gerberLayerPairFile( tempDir.GetFullPath(),
126 wxT( "backdrill_board-front-in3-backdrill-drl.gbr" ) );
127 BOOST_REQUIRE( gerberLayerPairFile.FileExists() );
128
129 wxFFile gerberLayerPairStream( gerberLayerPairFile.GetFullPath(), wxT( "rb" ) );
130 wxString gerberLayerPairContents;
131 BOOST_REQUIRE( gerberLayerPairStream.ReadAll( &gerberLayerPairContents ) );
132 BOOST_CHECK( gerberLayerPairContents.Contains( wxT( "%TF.FileFunction,NonPlated,1,4,Blind,Drill*%" ) ) );
133 BOOST_CHECK( gerberLayerPairContents.Contains( wxT( "%TA.AperFunction,BackDrill*%" ) ) );
134
135 wxFileName odbRoot( tempDir.GetFullPath(), wxEmptyString );
136 odbRoot.AppendDir( wxT( "odb_out" ) );
137 BOOST_REQUIRE( odbRoot.Mkdir( wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) );
138
139 PCB_IO_ODBPP odbExporter;
140 std::map<std::string, UTF8> props;
141 props["units"] = "mm";
142 props["sigfig"] = "4";
143 BOOST_REQUIRE_NO_THROW( odbExporter.SaveBoard( odbRoot.GetFullPath(), &board, &props ) );
144
145 wxFileName drill1Dir( odbRoot.GetFullPath(), wxEmptyString );
146 drill1Dir.AppendDir( wxT( "steps" ) );
147 drill1Dir.AppendDir( wxT( "pcb" ) );
148 drill1Dir.AppendDir( wxT( "layers" ) );
149 drill1Dir.AppendDir( wxT( "drill1" ) );
150 BOOST_REQUIRE( drill1Dir.DirExists() );
151
152 wxFileName toolsFile( drill1Dir.GetFullPath(), wxT( "tools" ) );
153 BOOST_REQUIRE( toolsFile.FileExists() );
154
155 wxFFile toolsStream( toolsFile.GetFullPath(), wxT( "rb" ) );
156 wxString toolsContents;
157 BOOST_REQUIRE( toolsStream.ReadAll( &toolsContents ) );
158 BOOST_CHECK( toolsContents.Contains( wxT( "TYPE=NON_PLATED" ) ) );
159 BOOST_CHECK( toolsContents.Contains( wxT( "TYPE2=BLIND" ) ) );
160
161 wxFileName matrixFile( odbRoot.GetFullPath(), wxEmptyString );
162 matrixFile.AppendDir( wxT( "matrix" ) );
163 matrixFile.SetFullName( wxT( "matrix" ) );
164 BOOST_REQUIRE( matrixFile.FileExists() );
165
166 wxFFile matrixStream( matrixFile.GetFullPath(), wxT( "rb" ) );
167 wxString matrixContents;
168 BOOST_REQUIRE( matrixStream.ReadAll( &matrixContents ) );
169 BOOST_CHECK( matrixContents.Contains( wxT( "ADD_TYPE=BACKDRILL" ) ) );
170
171 matrixStream.Close();
172 toolsStream.Close();
173 gerberStream.Close();
174 gerberLayerPairStream.Close();
175 excellonStream.Close();
176 layerPairStream.Close();
177 pthStream.Close();
178
179 wxFileName::Rmdir( odbRoot.GetFullPath(), wxPATH_RMDIR_RECURSIVE );
180 wxFileName::Rmdir( tempDir.GetFullPath(), wxPATH_RMDIR_RECURSIVE );
181}
182
183
184// Regression test for https://gitlab.com/kicad/code/kicad/-/issues/23914
185// Only front-side (secondary) backdrill holes were exported; back-side (tertiary)
186// backdrill operations never produced a drill file.
187BOOST_AUTO_TEST_CASE( FrontAndBackBackdrillCamOutputs )
188{
189 wxFileName tempDir = MakeTempDir();
190 wxFileName boardFile( tempDir.GetFullPath(), wxT( "backdrill_pair_board.kicad_pcb" ) );
191
192 BOARD board;
193 board.SetCopperLayerCount( 6 );
194 board.SetFileName( boardFile.GetFullPath() );
195
196 auto topVia = new PCB_VIA( &board );
197 topVia->SetPosition( VECTOR2I( 0, 0 ) );
198 topVia->SetLayerPair( F_Cu, B_Cu );
199 topVia->SetDrill( pcbIUScale.mmToIU( 0.30 ) );
200 topVia->SetWidth( pcbIUScale.mmToIU( 0.60 ) );
201 topVia->SetSecondaryDrillSize( pcbIUScale.mmToIU( 0.40 ) );
202 topVia->SetSecondaryDrillStartLayer( F_Cu );
203 topVia->SetSecondaryDrillEndLayer( In1_Cu );
204 board.Add( topVia );
205
206 auto bottomVia = new PCB_VIA( &board );
207 bottomVia->SetPosition( VECTOR2I( pcbIUScale.mmToIU( 5.0 ), 0 ) );
208 bottomVia->SetLayerPair( F_Cu, B_Cu );
209 bottomVia->SetDrill( pcbIUScale.mmToIU( 0.30 ) );
210 bottomVia->SetWidth( pcbIUScale.mmToIU( 0.60 ) );
211 bottomVia->SetTertiaryDrillSize( pcbIUScale.mmToIU( 0.40 ) );
212 bottomVia->SetTertiaryDrillStartLayer( B_Cu );
213 bottomVia->SetTertiaryDrillEndLayer( In3_Cu );
214 board.Add( bottomVia );
215
216 EXCELLON_WRITER excellon( &board );
217 excellon.SetOptions( false, false, VECTOR2I( 0, 0 ), false );
218 excellon.SetFormat( true );
219 BOOST_REQUIRE( excellon.CreateDrillandMapFilesSet( tempDir.GetFullPath(), true, false,
220 nullptr ) );
221
222 wxFileName topBackdrillFile( tempDir.GetFullPath(),
223 wxT( "backdrill_pair_board_Backdrills_Drill_1_2.drl" ) );
224 BOOST_CHECK_MESSAGE( topBackdrillFile.FileExists(),
225 "Front-side backdrill drill file should be produced" );
226
227 // Start=B_Cu (UI index 6) drilled toward In3_Cu (UI index 4) in a 6-layer board
228 wxFileName bottomBackdrillFile( tempDir.GetFullPath(),
229 wxT( "backdrill_pair_board_Backdrills_Drill_6_4.drl" ) );
230 BOOST_CHECK_MESSAGE( bottomBackdrillFile.FileExists(),
231 "Back-side (tertiary) backdrill drill file should be produced" );
232
233 if( bottomBackdrillFile.FileExists() )
234 {
235 wxFFile stream( bottomBackdrillFile.GetFullPath(), wxT( "rb" ) );
236 wxString contents;
237 BOOST_REQUIRE( stream.ReadAll( &contents ) );
238 BOOST_CHECK( contents.Contains( wxT( "; Backdrill" ) ) );
239 stream.Close();
240 }
241
242 wxFileName::Rmdir( tempDir.GetFullPath(), wxPATH_RMDIR_RECURSIVE );
243}
244
245
246// Stronger coverage for https://gitlab.com/kicad/code/kicad/-/issues/23914
247// A single via can carry both a front-side (secondary) and a back-side
248// (tertiary) backdrill. Both drill files must be produced.
249BOOST_AUTO_TEST_CASE( DualBackdrillSameViaCamOutputs )
250{
251 wxFileName tempDir = MakeTempDir();
252 wxFileName boardFile( tempDir.GetFullPath(), wxT( "dual_backdrill_board.kicad_pcb" ) );
253
254 BOARD board;
255 board.SetCopperLayerCount( 6 );
256 board.SetFileName( boardFile.GetFullPath() );
257
258 auto via = new PCB_VIA( &board );
259 via->SetPosition( VECTOR2I( 0, 0 ) );
260 via->SetLayerPair( F_Cu, B_Cu );
261 via->SetDrill( pcbIUScale.mmToIU( 0.30 ) );
262 via->SetWidth( pcbIUScale.mmToIU( 0.60 ) );
263 via->SetSecondaryDrillSize( pcbIUScale.mmToIU( 0.40 ) );
264 via->SetSecondaryDrillStartLayer( F_Cu );
265 via->SetSecondaryDrillEndLayer( In1_Cu );
266 via->SetTertiaryDrillSize( pcbIUScale.mmToIU( 0.40 ) );
267 via->SetTertiaryDrillStartLayer( B_Cu );
268 via->SetTertiaryDrillEndLayer( In3_Cu );
269 board.Add( via );
270
271 EXCELLON_WRITER excellon( &board );
272 excellon.SetOptions( false, false, VECTOR2I( 0, 0 ), false );
273 excellon.SetFormat( true );
274 BOOST_REQUIRE( excellon.CreateDrillandMapFilesSet( tempDir.GetFullPath(), true, false,
275 nullptr ) );
276
277 wxFileName topBackdrillFile( tempDir.GetFullPath(),
278 wxT( "dual_backdrill_board_Backdrills_Drill_1_2.drl" ) );
279 BOOST_CHECK( topBackdrillFile.FileExists() );
280
281 wxFileName bottomBackdrillFile( tempDir.GetFullPath(),
282 wxT( "dual_backdrill_board_Backdrills_Drill_6_4.drl" ) );
283 BOOST_CHECK( bottomBackdrillFile.FileExists() );
284
285 wxFileName::Rmdir( tempDir.GetFullPath(), wxPATH_RMDIR_RECURSIVE );
286}
287
288
289// Regression test for https://gitlab.com/kicad/code/kicad/-/issues/23451
290// GERBER_WRITER::SetFormat() precision was not passed to the plotter; output always used 4.6.
291BOOST_AUTO_TEST_CASE( GerberDrillPrecision )
292{
293 wxFileName tempDir = MakeTempDir();
294 wxFileName boardFile( tempDir.GetFullPath(), wxT( "precision_board.kicad_pcb" ) );
295
296 BOARD board;
297 board.SetCopperLayerCount( 2 );
298 board.SetFileName( boardFile.GetFullPath() );
299
300 auto via = new PCB_VIA( &board );
301 via->SetPosition( VECTOR2I( 0, 0 ) );
302 via->SetLayerPair( F_Cu, B_Cu );
303 via->SetDrill( pcbIUScale.mmToIU( 0.30 ) );
304 via->SetWidth( pcbIUScale.mmToIU( 0.60 ) );
305 board.Add( via );
306
307 // Verify precision 5 produces "Fmt 4.5" in the file header
308 GERBER_WRITER gerber5( &board );
309 gerber5.SetOptions( VECTOR2I( 0, 0 ) );
310 gerber5.SetFormat( 5 );
311 BOOST_REQUIRE( gerber5.CreateDrillandMapFilesSet( tempDir.GetFullPath(), true, false, false, nullptr ) );
312
313 wxFileName gerberFile5( tempDir.GetFullPath(), wxT( "precision_board-PTH-drl.gbr" ) );
314 BOOST_REQUIRE( gerberFile5.FileExists() );
315
316 wxFFile gerberStream5( gerberFile5.GetFullPath(), wxT( "rb" ) );
317 wxString gerberContents5;
318 BOOST_REQUIRE( gerberStream5.ReadAll( &gerberContents5 ) );
319 BOOST_CHECK_MESSAGE( gerberContents5.Contains( wxT( "Fmt 4.5" ) ),
320 "Expected 'Fmt 4.5' in gerber header with precision=5" );
321 BOOST_CHECK( !gerberContents5.Contains( wxT( "Fmt 4.6" ) ) );
322 gerberStream5.Close();
323
324 // Verify precision 6 produces "Fmt 4.6" in the file header
325 GERBER_WRITER gerber6( &board );
326 gerber6.SetOptions( VECTOR2I( 0, 0 ) );
327 gerber6.SetFormat( 6 );
328 BOOST_REQUIRE( gerber6.CreateDrillandMapFilesSet( tempDir.GetFullPath(), true, false, false, nullptr ) );
329
330 wxFFile gerberStream6( gerberFile5.GetFullPath(), wxT( "rb" ) );
331 wxString gerberContents6;
332 BOOST_REQUIRE( gerberStream6.ReadAll( &gerberContents6 ) );
333 BOOST_CHECK_MESSAGE( gerberContents6.Contains( wxT( "Fmt 4.6" ) ),
334 "Expected 'Fmt 4.6' in gerber header with precision=6" );
335 BOOST_CHECK( !gerberContents6.Contains( wxT( "Fmt 4.5" ) ) );
336 gerberStream6.Close();
337
338 wxFileName::Rmdir( tempDir.GetFullPath(), wxPATH_RMDIR_RECURSIVE );
339}
340
341
342// Regression test for https://gitlab.com/kicad/code/kicad/-/issues/23005
343// GenDrillReportFile crashed when aReporter was null (the default)
344BOOST_AUTO_TEST_CASE( DrillReportNullReporter )
345{
346 wxFileName tempDir = MakeTempDir();
347 wxFileName boardFile( tempDir.GetFullPath(), wxT( "test_board.kicad_pcb" ) );
348
349 BOARD board;
350 board.SetCopperLayerCount( 2 );
351 board.SetFileName( boardFile.GetFullPath() );
352
353 wxFileName reportFile( tempDir.GetFullPath(), wxT( "test_board-drl.rpt" ) );
354
355 // Valid path with null reporter should succeed without crashing
356 EXCELLON_WRITER excellon( &board );
357 BOOST_CHECK( excellon.GenDrillReportFile( reportFile.GetFullPath() ) );
358 BOOST_CHECK( reportFile.FileExists() );
359
360 GERBER_WRITER gerber( &board );
361 BOOST_CHECK( gerber.GenDrillReportFile( reportFile.GetFullPath() ) );
362
363 // Invalid path with null reporter should return false without crashing
364 EXCELLON_WRITER excellon2( &board );
365 BOOST_CHECK( !excellon2.GenDrillReportFile( wxT( "/nonexistent/path/report.rpt" ) ) );
366
367 GERBER_WRITER gerber2( &board );
368 BOOST_CHECK( !gerber2.GenDrillReportFile( wxT( "/nonexistent/path/report.rpt" ) ) );
369
370 wxFileName::Rmdir( tempDir.GetFullPath(), wxPATH_RMDIR_RECURSIVE );
371}
372
373
374// Regression test for https://gitlab.com/kicad/code/kicad/-/issues/23289
375// GenDrillReportFile crashed with SIGSEGV when the board had drills because
376// printToolSummary() passed integer literal 0 instead of the FILE* to fmt::print()
377BOOST_AUTO_TEST_CASE( DrillReportWithTools )
378{
379 wxFileName tempDir = MakeTempDir();
380 wxFileName boardFile( tempDir.GetFullPath(), wxT( "test_board_with_drills.kicad_pcb" ) );
381
382 BOARD board;
383 board.SetCopperLayerCount( 2 );
384 board.SetFileName( boardFile.GetFullPath() );
385
386 auto via = new PCB_VIA( &board );
387 via->SetPosition( VECTOR2I( 0, 0 ) );
388 via->SetLayerPair( F_Cu, B_Cu );
389 via->SetDrill( pcbIUScale.mmToIU( 0.30 ) );
390 via->SetWidth( pcbIUScale.mmToIU( 0.60 ) );
391 board.Add( via );
392
393 wxFileName reportFile( tempDir.GetFullPath(), wxT( "test_board_with_drills-drl.rpt" ) );
394
395 EXCELLON_WRITER excellon( &board );
396 excellon.SetOptions( false, false, VECTOR2I( 0, 0 ), false );
397 excellon.SetFormat( true );
398 BOOST_CHECK_NO_THROW( excellon.GenDrillReportFile( reportFile.GetFullPath() ) );
399 BOOST_CHECK( reportFile.FileExists() );
400
401 wxFFile reportStream( reportFile.GetFullPath(), wxT( "rb" ) );
402 wxString reportContents;
403 BOOST_REQUIRE( reportStream.ReadAll( &reportContents ) );
404 BOOST_CHECK( reportContents.Contains( wxT( "T1" ) ) );
405 BOOST_CHECK( reportContents.Contains( wxT( "0.300mm" ) ) );
406 reportStream.Close();
407
408 wxFileName::Rmdir( tempDir.GetFullPath(), wxPATH_RMDIR_RECURSIVE );
409}
410
411
412// Regression test for https://gitlab.com/kicad/code/kicad/-/issues/24014
413// A non-filled PCB_SHAPE rectangle on F.SilkS was emitted as a donut_rc symbol with a
414// corner radius smaller than half the line width. Many ODB++ viewers reject the
415// resulting degenerate symbol, so the outline appeared to be missing. The exporter now
416// emits the four outline edges as line features, matching how a rectangle built from
417// individual line segments is exported.
418BOOST_AUTO_TEST_CASE( OdbPpUnfilledRectangleOnSilk )
419{
420 wxFileName tempDir = MakeTempDir();
421 wxFileName boardFile( tempDir.GetFullPath(), wxT( "silk_rect_board.kicad_pcb" ) );
422
423 BOARD board;
424 board.SetCopperLayerCount( 2 );
425 board.SetFileName( boardFile.GetFullPath() );
426
427 // Non-filled rectangle on F.SilkS
428 PCB_SHAPE* rect = new PCB_SHAPE( &board, SHAPE_T::RECTANGLE );
429 rect->SetStart( VECTOR2I( pcbIUScale.mmToIU( 10.0 ), pcbIUScale.mmToIU( 10.0 ) ) );
430 rect->SetEnd( VECTOR2I( pcbIUScale.mmToIU( 30.0 ), pcbIUScale.mmToIU( 20.0 ) ) );
431 rect->SetLayer( F_SilkS );
432 rect->SetFilled( false );
433 rect->SetWidth( pcbIUScale.mmToIU( 0.15 ) );
434 board.Add( rect );
435
436 wxFileName odbRoot( tempDir.GetFullPath(), wxEmptyString );
437 odbRoot.AppendDir( wxT( "odb_out" ) );
438 BOOST_REQUIRE( odbRoot.Mkdir( wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) );
439
440 PCB_IO_ODBPP odbExporter;
441 std::map<std::string, UTF8> props;
442 props["units"] = "mm";
443 props["sigfig"] = "4";
444 BOOST_REQUIRE_NO_THROW( odbExporter.SaveBoard( odbRoot.GetFullPath(), &board, &props ) );
445
446 wxFileName silkFeatures( odbRoot.GetFullPath(), wxT( "features" ) );
447 silkFeatures.AppendDir( wxT( "steps" ) );
448 silkFeatures.AppendDir( wxT( "pcb" ) );
449 silkFeatures.AppendDir( wxT( "layers" ) );
450 silkFeatures.AppendDir( wxT( "f.silkscreen" ) );
451 BOOST_REQUIRE( silkFeatures.FileExists() );
452
453 wxFFile silkStream( silkFeatures.GetFullPath(), wxT( "rb" ) );
454 wxString silkContents;
455 BOOST_REQUIRE( silkStream.ReadAll( &silkContents ) );
456 silkStream.Close();
457
458 // Four ODB++ line ("L ...") features describe the rectangle outline.
459 int lineCount = 0;
460 wxStringTokenizer lines( silkContents, wxT( "\n" ) );
461
462 while( lines.HasMoreTokens() )
463 {
464 if( lines.GetNextToken().StartsWith( wxT( "L " ) ) )
465 lineCount++;
466 }
467
468 BOOST_CHECK_EQUAL( lineCount, 4 );
469
470 // The degenerate donut_rc symbol should not appear anymore.
471 BOOST_CHECK( !silkContents.Contains( wxT( "donut_rc" ) ) );
472
473 wxFileName::Rmdir( odbRoot.GetFullPath(), wxPATH_RMDIR_RECURSIVE );
474 wxFileName::Rmdir( tempDir.GetFullPath(), wxPATH_RMDIR_RECURSIVE );
475}
476
477
478namespace
479{
480PAD* AddSlotPad( FOOTPRINT* aFootprint, const VECTOR2I& aPos, PAD_ATTRIB aAttribute )
481{
482 PAD* pad = new PAD( aFootprint );
483 pad->SetAttribute( aAttribute );
484 pad->SetLayerSet( aAttribute == PAD_ATTRIB::NPTH ? PAD::UnplatedHoleMask()
485 : PAD::PTHMask() );
486 pad->SetPosition( aPos );
488 pad->SetSize( PADSTACK::ALL_LAYERS,
489 VECTOR2I( pcbIUScale.mmToIU( 2.0 ), pcbIUScale.mmToIU( 1.0 ) ) );
490 pad->SetDrillShape( PAD_DRILL_SHAPE::OBLONG );
491 pad->SetDrillSize( VECTOR2I( pcbIUScale.mmToIU( 1.7 ), pcbIUScale.mmToIU( 0.6 ) ) );
492 aFootprint->Add( pad );
493
494 return pad;
495}
496} // anonymous namespace
497
498
499// Regression test for https://gitlab.com/kicad/code/kicad/-/issues/24677
500// Plated through-hole pads with a slotted (oval) drill were dropped entirely from the
501// ODB++ drill output. Round holes and non-plated slots exported fine, but plated slots
502// were never written to the plated drill layer. The exporter now emits PTH slots on the
503// plated drill layer just like NPTH slots on the non-plated layer.
504BOOST_AUTO_TEST_CASE( OdbPpPlatedSlotDrill )
505{
506 wxFileName tempDir = MakeTempDir();
507 wxFileName boardFile( tempDir.GetFullPath(), wxT( "plated_slot_board.kicad_pcb" ) );
508
509 BOARD board;
510 board.SetCopperLayerCount( 2 );
511 board.SetFileName( boardFile.GetFullPath() );
512
513 FOOTPRINT* fp = new FOOTPRINT( &board );
514 fp->SetPosition( VECTOR2I( pcbIUScale.mmToIU( 50.0 ), pcbIUScale.mmToIU( 50.0 ) ) );
515 board.Add( fp );
516
517 // A plated slot and a non-plated slot in the same design.
518 AddSlotPad( fp, VECTOR2I( pcbIUScale.mmToIU( 50.0 ), pcbIUScale.mmToIU( 50.0 ) ),
520 AddSlotPad( fp, VECTOR2I( pcbIUScale.mmToIU( 60.0 ), pcbIUScale.mmToIU( 50.0 ) ),
522
523 wxFileName odbRoot( tempDir.GetFullPath(), wxEmptyString );
524 odbRoot.AppendDir( wxT( "odb_out" ) );
525 BOOST_REQUIRE( odbRoot.Mkdir( wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) );
526
527 PCB_IO_ODBPP odbExporter;
528 std::map<std::string, UTF8> props;
529 props["units"] = "mm";
530 props["sigfig"] = "4";
531 BOOST_REQUIRE_NO_THROW( odbExporter.SaveBoard( odbRoot.GetFullPath(), &board, &props ) );
532
533 auto layerDir = [&]( const wxString& aLayer )
534 {
535 wxFileName dir( odbRoot.GetFullPath(), wxEmptyString );
536 dir.AppendDir( wxT( "steps" ) );
537 dir.AppendDir( wxT( "pcb" ) );
538 dir.AppendDir( wxT( "layers" ) );
539 dir.AppendDir( aLayer );
540
541 return dir;
542 };
543
544 auto readFile = []( const wxFileName& aFile )
545 {
546 wxFFile stream( aFile.GetFullPath(), wxT( "rb" ) );
547 wxString contents;
548 BOOST_REQUIRE( stream.ReadAll( &contents ) );
549 stream.Close();
550
551 return contents;
552 };
553
554 // ODB++ array members (e.g. drill tools) are indented, so trim before matching.
555 auto countLinesStartingWith = []( const wxString& aContents, const wxString& aPrefix )
556 {
557 int count = 0;
558 wxStringTokenizer lines( aContents, wxT( "\n" ) );
559
560 while( lines.HasMoreTokens() )
561 {
562 wxString line = lines.GetNextToken();
563 line.Trim( false );
564
565 if( line.StartsWith( aPrefix ) )
566 count++;
567 }
568
569 return count;
570 };
571
572 auto containsOvalSymbol = []( const wxString& aContents )
573 {
574 wxStringTokenizer lines( aContents, wxT( "\n" ) );
575
576 while( lines.HasMoreTokens() )
577 {
578 wxString line = lines.GetNextToken();
579
580 // Symbol definition lines look like "$0 oval<w>x<h>".
581 if( line.StartsWith( wxT( "$" ) ) && line.Contains( wxT( "oval" ) ) )
582 return true;
583 }
584
585 return false;
586 };
587
588 // The plated slot must appear on the plated drill layer as exactly one oval pad feature,
589 // and the slot must NOT leak onto this layer as a non-plated hole.
590 wxFileName platedDir = layerDir( wxT( "drill_plated_f.cu-b.cu" ) );
591 BOOST_REQUIRE_MESSAGE( platedDir.DirExists(), "Plated drill layer should exist" );
592
593 wxFileName platedFeatures( platedDir.GetFullPath(), wxT( "features" ) );
594 BOOST_REQUIRE( platedFeatures.FileExists() );
595 wxString platedContents = readFile( platedFeatures );
596
597 BOOST_CHECK_EQUAL( countLinesStartingWith( platedContents, wxT( "P " ) ), 1 );
598 BOOST_CHECK_MESSAGE( containsOvalSymbol( platedContents ),
599 "Plated slot should use an oval symbol" );
600
601 wxFileName platedTools( platedDir.GetFullPath(), wxT( "tools" ) );
602 BOOST_REQUIRE( platedTools.FileExists() );
603 wxString platedToolsContents = readFile( platedTools );
604 BOOST_CHECK_EQUAL( countLinesStartingWith( platedToolsContents, wxT( "TYPE=PLATED" ) ), 1 );
605 BOOST_CHECK_EQUAL( countLinesStartingWith( platedToolsContents, wxT( "TYPE=NON_PLATED" ) ), 0 );
606
607 // The non-plated slot must still appear on the non-plated drill layer (unchanged), and
608 // the plated slot must NOT leak onto it.
609 wxFileName nonPlatedDir = layerDir( wxT( "drill_non-plated_f.cu-b.cu" ) );
610 BOOST_REQUIRE_MESSAGE( nonPlatedDir.DirExists(), "Non-plated drill layer should exist" );
611
612 wxFileName nonPlatedFeatures( nonPlatedDir.GetFullPath(), wxT( "features" ) );
613 BOOST_REQUIRE( nonPlatedFeatures.FileExists() );
614 wxString nonPlatedContents = readFile( nonPlatedFeatures );
615
616 BOOST_CHECK_EQUAL( countLinesStartingWith( nonPlatedContents, wxT( "P " ) ), 1 );
617 BOOST_CHECK_MESSAGE( containsOvalSymbol( nonPlatedContents ),
618 "Non-plated slot should still export an oval symbol" );
619
620 wxFileName nonPlatedTools( nonPlatedDir.GetFullPath(), wxT( "tools" ) );
621 BOOST_REQUIRE( nonPlatedTools.FileExists() );
622 wxString nonPlatedToolsContents = readFile( nonPlatedTools );
623 BOOST_CHECK_EQUAL( countLinesStartingWith( nonPlatedToolsContents, wxT( "TYPE=NON_PLATED" ) ),
624 1 );
625 BOOST_CHECK_EQUAL( countLinesStartingWith( nonPlatedToolsContents, wxT( "TYPE=PLATED" ) ), 0 );
626
627 wxFileName::Rmdir( odbRoot.GetFullPath(), wxPATH_RMDIR_RECURSIVE );
628 wxFileName::Rmdir( tempDir.GetFullPath(), wxPATH_RMDIR_RECURSIVE );
629}
constexpr EDA_IU_SCALE pcbIUScale
Definition base_units.h:121
Information pertinent to a Pcbnew printed circuit board.
Definition board.h:372
void Add(BOARD_ITEM *aItem, ADD_MODE aMode=ADD_MODE::INSERT, bool aSkipConnectivity=false) override
Removes an item from the container.
Definition board.cpp:1295
void SetFileName(const wxString &aFileName)
Definition board.h:407
void SetCopperLayerCount(int aCount)
Definition board.cpp:991
virtual void SetFilled(bool aFlag)
Definition eda_shape.h:152
Create Excellon drill, drill map, and drill report files.
void SetFormat(bool aMetric, ZEROS_FMT aZerosFmt=DECIMAL_FORMAT, int aLeftDigits=0, int aRightDigits=0)
Initialize internal parameters to match the given format.
bool CreateDrillandMapFilesSet(const wxString &aPlotDirectory, bool aGenDrill, bool aGenMap, REPORTER *aReporter=nullptr)
Create the full set of Excellon drill file for the board.
void SetOptions(bool aMirror, bool aMinimalHeader, const VECTOR2I &aOffset, bool aMerge_PTH_NPTH)
Initialize internal parameters to match drill options.
void SetPosition(const VECTOR2I &aPos) override
void Add(BOARD_ITEM *aItem, ADD_MODE aMode=ADD_MODE::INSERT, bool aSkipConnectivity=false) override
Removes an item from the container.
bool GenDrillReportFile(const wxString &aFullFileName, REPORTER *aReporter=nullptr)
Create a plain text report file giving a list of drill values and drill count for through holes,...
Used to create Gerber drill files.
bool CreateDrillandMapFilesSet(const wxString &aPlotDirectory, bool aGenDrill, bool aGenMap, bool aGenTenting, REPORTER *aReporter=nullptr)
Create the full set of Excellon drill file for the board filenames are computed from the board name,...
void SetOptions(const VECTOR2I &aOffset)
Initialize internal parameters to match drill options.
void SetFormat(int aRightDigits=6)
Initialize internal parameters to match the given format.
static constexpr PCB_LAYER_ID ALL_LAYERS
! Temporary layer identifier to identify code that is not padstack-aware
Definition padstack.h:177
Definition pad.h:61
static LSET PTHMask()
layer set for a through hole pad
Definition pad.cpp:579
static LSET UnplatedHoleMask()
layer set for a mechanical unplated through hole pad
Definition pad.cpp:600
void SaveBoard(const wxString &aFileName, BOARD *aBoard, const std::map< std::string, UTF8 > *aProperties=nullptr) override
Write aBoard to a storage file in a format that this PCB_IO implementation knows about or it can be u...
void SetWidth(int aWidth) override
void SetEnd(const VECTOR2I &aEnd) override
void SetLayer(PCB_LAYER_ID aLayer) override
Set the layer this item is on.
void SetStart(const VECTOR2I &aStart) override
@ RECTANGLE
Use RECTANGLE instead of RECT to avoid collision in a Windows header.
Definition eda_shape.h:47
Classes used in drill files, map files and report files generation.
Classes used in drill files, map files and report files generation.
@ B_Cu
Definition layer_ids.h:61
@ F_SilkS
Definition layer_ids.h:96
@ In1_Cu
Definition layer_ids.h:62
@ In3_Cu
Definition layer_ids.h:64
@ F_Cu
Definition layer_ids.h:60
PAD_ATTRIB
The set of pad shapes, used with PAD::{Set,Get}Attribute().
Definition padstack.h:97
@ NPTH
like PAD_PTH, but not plated mechanical use only, no connection allowed
Definition padstack.h:103
@ PTH
Plated through hole pad.
Definition padstack.h:98
static bool readFile(const wxString &aFileName, wxString &aOut, size_t aLimit=0)
Read a file into aOut.
BOOST_AUTO_TEST_CASE(BackdrillCamOutputs)
BOOST_REQUIRE(intersection.has_value()==c.ExpectedIntersection.has_value())
BOOST_CHECK_MESSAGE(totalMismatches==0, std::to_string(totalMismatches)+" board(s) with strategy disagreements")
BOOST_CHECK_EQUAL(result, "25.4")
VECTOR2< int32_t > VECTOR2I
Definition vector2d.h:683