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>
32#include <base_units.h>
33
34#include <map>
35#include <memory>
36
37#include <core/utf8.h>
38
39#include <wx/dir.h>
40#include <wx/ffile.h>
41#include <wx/filename.h>
42#include <wx/tokenzr.h>
43#include <wx/utils.h>
44
45
46namespace
47{
48wxFileName MakeTempDir()
49{
50 wxFileName tempDir( wxFileName::GetTempDir(), wxEmptyString );
51 tempDir.AppendDir( wxString::Format( "kicad-backdrill-%llu",
52 static_cast<unsigned long long>( wxGetUTCTime() ) ) );
53 BOOST_REQUIRE( tempDir.Mkdir( wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) );
54
55 return tempDir;
56}
57} // anonymous namespace
58
59
60BOOST_AUTO_TEST_CASE( BackdrillCamOutputs )
61{
62 wxFileName tempDir = MakeTempDir();
63 wxFileName boardFile( tempDir.GetFullPath(), wxT( "backdrill_board.kicad_pcb" ) );
64
65 BOARD board;
66 board.SetCopperLayerCount( 6 );
67 board.SetFileName( boardFile.GetFullPath() );
68
69 auto via = new PCB_VIA( &board );
70 via->SetPosition( VECTOR2I( 0, 0 ) );
71 via->SetLayerPair( F_Cu, B_Cu );
72 via->SetDrill( pcbIUScale.mmToIU( 0.30 ) );
73 via->SetWidth( pcbIUScale.mmToIU( 0.60 ) );
74 via->SetSecondaryDrillSize( pcbIUScale.mmToIU( 0.20 ) );
75 via->SetSecondaryDrillStartLayer( F_Cu );
76 via->SetSecondaryDrillEndLayer( In3_Cu );
77 via->SetFrontPostMachiningMode( PAD_DRILL_POST_MACHINING_MODE::COUNTERSINK );
78 via->SetFrontPostMachiningSize( pcbIUScale.mmToIU( 0.60 ) );
79 via->SetFrontPostMachiningDepth( pcbIUScale.mmToIU( 0.15 ) );
80 via->SetFrontPostMachiningAngle( 900 );
81 board.Add( via );
82
83 EXCELLON_WRITER excellon( &board );
84 excellon.SetOptions( false, false, VECTOR2I( 0, 0 ), false );
85 excellon.SetFormat( true );
86 BOOST_REQUIRE( excellon.CreateDrillandMapFilesSet( tempDir.GetFullPath(), true, false, nullptr ) );
87
88 wxFileName excellonFile( tempDir.GetFullPath(), wxT( "backdrill_board_Backdrills_Drill_1_4.drl" ) );
89 BOOST_REQUIRE( excellonFile.FileExists() );
90
91 wxFFile excellonStream( excellonFile.GetFullPath(), wxT( "rb" ) );
92 wxString excellonContents;
93 BOOST_REQUIRE( excellonStream.ReadAll( &excellonContents ) );
94 BOOST_CHECK( excellonContents.Contains( wxT( "TF.FileFunction,NonPlated,1,4,Blind" ) ) );
95 BOOST_CHECK( excellonContents.Contains( wxT( "; Backdrill" ) ) );
96 BOOST_CHECK( excellonContents.Contains( wxT( "post-machining" ) ) );
97
98 wxFileName layerPairFile( tempDir.GetFullPath(), wxT( "backdrill_board-front-in3-backdrill.drl" ) );
99 BOOST_REQUIRE( layerPairFile.FileExists() );
100
101 wxFFile layerPairStream( layerPairFile.GetFullPath(), wxT( "rb" ) );
102 wxString layerPairContents;
103 BOOST_REQUIRE( layerPairStream.ReadAll( &layerPairContents ) );
104 BOOST_CHECK( layerPairContents.Contains( wxT( "; backdrill" ) ) );
105
106 wxFileName pthFile( tempDir.GetFullPath(), wxT( "backdrill_board-PTH.drl" ) );
107 BOOST_REQUIRE( pthFile.FileExists() );
108
109 wxFFile pthStream( pthFile.GetFullPath(), wxT( "rb" ) );
110 wxString pthContents;
111 BOOST_REQUIRE( pthStream.ReadAll( &pthContents ) );
112 BOOST_CHECK( pthContents.Contains( wxT( "; Post-machining front countersink dia 0.600mm depth 0.150mm angle 90deg" ) ) );
113
114 GERBER_WRITER gerber( &board );
115 gerber.SetOptions( VECTOR2I( 0, 0 ) );
116 gerber.SetFormat( 6 );
117 BOOST_REQUIRE( gerber.CreateDrillandMapFilesSet( tempDir.GetFullPath(), true, false, false, nullptr ) );
118
119 wxFileName gerberFile( tempDir.GetFullPath(), wxT( "backdrill_board_Backdrills_Drill_1_4-drl.gbr" ) );
120 BOOST_REQUIRE( gerberFile.FileExists() );
121
122 wxFFile gerberStream( gerberFile.GetFullPath(), wxT( "rb" ) );
123 wxString gerberContents;
124 BOOST_REQUIRE( gerberStream.ReadAll( &gerberContents ) );
125 BOOST_CHECK( gerberContents.Contains( wxT( "%TA.AperFunction,BackDrill*%" ) ) );
126 BOOST_CHECK( gerberContents.Contains( wxT( "%TF.FileFunction,NonPlated,1,4,Blind,Drill*%" ) ) );
127
128 wxFileName gerberLayerPairFile( tempDir.GetFullPath(),
129 wxT( "backdrill_board-front-in3-backdrill-drl.gbr" ) );
130 BOOST_REQUIRE( gerberLayerPairFile.FileExists() );
131
132 wxFFile gerberLayerPairStream( gerberLayerPairFile.GetFullPath(), wxT( "rb" ) );
133 wxString gerberLayerPairContents;
134 BOOST_REQUIRE( gerberLayerPairStream.ReadAll( &gerberLayerPairContents ) );
135 BOOST_CHECK( gerberLayerPairContents.Contains( wxT( "%TF.FileFunction,NonPlated,1,4,Blind,Drill*%" ) ) );
136 BOOST_CHECK( gerberLayerPairContents.Contains( wxT( "%TA.AperFunction,BackDrill*%" ) ) );
137
138 wxFileName odbRoot( tempDir.GetFullPath(), wxEmptyString );
139 odbRoot.AppendDir( wxT( "odb_out" ) );
140 BOOST_REQUIRE( odbRoot.Mkdir( wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) );
141
142 PCB_IO_ODBPP odbExporter;
143 std::map<std::string, UTF8> props;
144 props["units"] = "mm";
145 props["sigfig"] = "4";
146 BOOST_REQUIRE_NO_THROW( odbExporter.SaveBoard( odbRoot.GetFullPath(), &board, &props ) );
147
148 wxFileName drill1Dir( odbRoot.GetFullPath(), wxEmptyString );
149 drill1Dir.AppendDir( wxT( "steps" ) );
150 drill1Dir.AppendDir( wxT( "pcb" ) );
151 drill1Dir.AppendDir( wxT( "layers" ) );
152 drill1Dir.AppendDir( wxT( "drill1" ) );
153 BOOST_REQUIRE( drill1Dir.DirExists() );
154
155 wxFileName toolsFile( drill1Dir.GetFullPath(), wxT( "tools" ) );
156 BOOST_REQUIRE( toolsFile.FileExists() );
157
158 wxFFile toolsStream( toolsFile.GetFullPath(), wxT( "rb" ) );
159 wxString toolsContents;
160 BOOST_REQUIRE( toolsStream.ReadAll( &toolsContents ) );
161 BOOST_CHECK( toolsContents.Contains( wxT( "TYPE=NON_PLATED" ) ) );
162 BOOST_CHECK( toolsContents.Contains( wxT( "TYPE2=BLIND" ) ) );
163
164 wxFileName matrixFile( odbRoot.GetFullPath(), wxEmptyString );
165 matrixFile.AppendDir( wxT( "matrix" ) );
166 matrixFile.SetFullName( wxT( "matrix" ) );
167 BOOST_REQUIRE( matrixFile.FileExists() );
168
169 wxFFile matrixStream( matrixFile.GetFullPath(), wxT( "rb" ) );
170 wxString matrixContents;
171 BOOST_REQUIRE( matrixStream.ReadAll( &matrixContents ) );
172 BOOST_CHECK( matrixContents.Contains( wxT( "ADD_TYPE=BACKDRILL" ) ) );
173
174 matrixStream.Close();
175 toolsStream.Close();
176 gerberStream.Close();
177 gerberLayerPairStream.Close();
178 excellonStream.Close();
179 layerPairStream.Close();
180 pthStream.Close();
181
182 wxFileName::Rmdir( odbRoot.GetFullPath(), wxPATH_RMDIR_RECURSIVE );
183 wxFileName::Rmdir( tempDir.GetFullPath(), wxPATH_RMDIR_RECURSIVE );
184}
185
186
187// Regression test for https://gitlab.com/kicad/code/kicad/-/issues/23914
188// Only front-side (secondary) backdrill holes were exported; back-side (tertiary)
189// backdrill operations never produced a drill file.
190BOOST_AUTO_TEST_CASE( FrontAndBackBackdrillCamOutputs )
191{
192 wxFileName tempDir = MakeTempDir();
193 wxFileName boardFile( tempDir.GetFullPath(), wxT( "backdrill_pair_board.kicad_pcb" ) );
194
195 BOARD board;
196 board.SetCopperLayerCount( 6 );
197 board.SetFileName( boardFile.GetFullPath() );
198
199 auto topVia = new PCB_VIA( &board );
200 topVia->SetPosition( VECTOR2I( 0, 0 ) );
201 topVia->SetLayerPair( F_Cu, B_Cu );
202 topVia->SetDrill( pcbIUScale.mmToIU( 0.30 ) );
203 topVia->SetWidth( pcbIUScale.mmToIU( 0.60 ) );
204 topVia->SetSecondaryDrillSize( pcbIUScale.mmToIU( 0.40 ) );
205 topVia->SetSecondaryDrillStartLayer( F_Cu );
206 topVia->SetSecondaryDrillEndLayer( In1_Cu );
207 board.Add( topVia );
208
209 auto bottomVia = new PCB_VIA( &board );
210 bottomVia->SetPosition( VECTOR2I( pcbIUScale.mmToIU( 5.0 ), 0 ) );
211 bottomVia->SetLayerPair( F_Cu, B_Cu );
212 bottomVia->SetDrill( pcbIUScale.mmToIU( 0.30 ) );
213 bottomVia->SetWidth( pcbIUScale.mmToIU( 0.60 ) );
214 bottomVia->SetTertiaryDrillSize( pcbIUScale.mmToIU( 0.40 ) );
215 bottomVia->SetTertiaryDrillStartLayer( B_Cu );
216 bottomVia->SetTertiaryDrillEndLayer( In3_Cu );
217 board.Add( bottomVia );
218
219 EXCELLON_WRITER excellon( &board );
220 excellon.SetOptions( false, false, VECTOR2I( 0, 0 ), false );
221 excellon.SetFormat( true );
222 BOOST_REQUIRE( excellon.CreateDrillandMapFilesSet( tempDir.GetFullPath(), true, false,
223 nullptr ) );
224
225 wxFileName topBackdrillFile( tempDir.GetFullPath(),
226 wxT( "backdrill_pair_board_Backdrills_Drill_1_2.drl" ) );
227 BOOST_CHECK_MESSAGE( topBackdrillFile.FileExists(),
228 "Front-side backdrill drill file should be produced" );
229
230 // Start=B_Cu (UI index 6) drilled toward In3_Cu (UI index 4) in a 6-layer board
231 wxFileName bottomBackdrillFile( tempDir.GetFullPath(),
232 wxT( "backdrill_pair_board_Backdrills_Drill_6_4.drl" ) );
233 BOOST_CHECK_MESSAGE( bottomBackdrillFile.FileExists(),
234 "Back-side (tertiary) backdrill drill file should be produced" );
235
236 if( bottomBackdrillFile.FileExists() )
237 {
238 wxFFile stream( bottomBackdrillFile.GetFullPath(), wxT( "rb" ) );
239 wxString contents;
240 BOOST_REQUIRE( stream.ReadAll( &contents ) );
241 BOOST_CHECK( contents.Contains( wxT( "; Backdrill" ) ) );
242 stream.Close();
243 }
244
245 wxFileName::Rmdir( tempDir.GetFullPath(), wxPATH_RMDIR_RECURSIVE );
246}
247
248
249// Stronger coverage for https://gitlab.com/kicad/code/kicad/-/issues/23914
250// A single via can carry both a front-side (secondary) and a back-side
251// (tertiary) backdrill. Both drill files must be produced.
252BOOST_AUTO_TEST_CASE( DualBackdrillSameViaCamOutputs )
253{
254 wxFileName tempDir = MakeTempDir();
255 wxFileName boardFile( tempDir.GetFullPath(), wxT( "dual_backdrill_board.kicad_pcb" ) );
256
257 BOARD board;
258 board.SetCopperLayerCount( 6 );
259 board.SetFileName( boardFile.GetFullPath() );
260
261 auto via = new PCB_VIA( &board );
262 via->SetPosition( VECTOR2I( 0, 0 ) );
263 via->SetLayerPair( F_Cu, B_Cu );
264 via->SetDrill( pcbIUScale.mmToIU( 0.30 ) );
265 via->SetWidth( pcbIUScale.mmToIU( 0.60 ) );
266 via->SetSecondaryDrillSize( pcbIUScale.mmToIU( 0.40 ) );
267 via->SetSecondaryDrillStartLayer( F_Cu );
268 via->SetSecondaryDrillEndLayer( In1_Cu );
269 via->SetTertiaryDrillSize( pcbIUScale.mmToIU( 0.40 ) );
270 via->SetTertiaryDrillStartLayer( B_Cu );
271 via->SetTertiaryDrillEndLayer( In3_Cu );
272 board.Add( via );
273
274 EXCELLON_WRITER excellon( &board );
275 excellon.SetOptions( false, false, VECTOR2I( 0, 0 ), false );
276 excellon.SetFormat( true );
277 BOOST_REQUIRE( excellon.CreateDrillandMapFilesSet( tempDir.GetFullPath(), true, false,
278 nullptr ) );
279
280 wxFileName topBackdrillFile( tempDir.GetFullPath(),
281 wxT( "dual_backdrill_board_Backdrills_Drill_1_2.drl" ) );
282 BOOST_CHECK( topBackdrillFile.FileExists() );
283
284 wxFileName bottomBackdrillFile( tempDir.GetFullPath(),
285 wxT( "dual_backdrill_board_Backdrills_Drill_6_4.drl" ) );
286 BOOST_CHECK( bottomBackdrillFile.FileExists() );
287
288 wxFileName::Rmdir( tempDir.GetFullPath(), wxPATH_RMDIR_RECURSIVE );
289}
290
291
292// Regression test for https://gitlab.com/kicad/code/kicad/-/issues/23451
293// GERBER_WRITER::SetFormat() precision was not passed to the plotter; output always used 4.6.
294BOOST_AUTO_TEST_CASE( GerberDrillPrecision )
295{
296 wxFileName tempDir = MakeTempDir();
297 wxFileName boardFile( tempDir.GetFullPath(), wxT( "precision_board.kicad_pcb" ) );
298
299 BOARD board;
300 board.SetCopperLayerCount( 2 );
301 board.SetFileName( boardFile.GetFullPath() );
302
303 auto via = new PCB_VIA( &board );
304 via->SetPosition( VECTOR2I( 0, 0 ) );
305 via->SetLayerPair( F_Cu, B_Cu );
306 via->SetDrill( pcbIUScale.mmToIU( 0.30 ) );
307 via->SetWidth( pcbIUScale.mmToIU( 0.60 ) );
308 board.Add( via );
309
310 // Verify precision 5 produces "Fmt 4.5" in the file header
311 GERBER_WRITER gerber5( &board );
312 gerber5.SetOptions( VECTOR2I( 0, 0 ) );
313 gerber5.SetFormat( 5 );
314 BOOST_REQUIRE( gerber5.CreateDrillandMapFilesSet( tempDir.GetFullPath(), true, false, false, nullptr ) );
315
316 wxFileName gerberFile5( tempDir.GetFullPath(), wxT( "precision_board-PTH-drl.gbr" ) );
317 BOOST_REQUIRE( gerberFile5.FileExists() );
318
319 wxFFile gerberStream5( gerberFile5.GetFullPath(), wxT( "rb" ) );
320 wxString gerberContents5;
321 BOOST_REQUIRE( gerberStream5.ReadAll( &gerberContents5 ) );
322 BOOST_CHECK_MESSAGE( gerberContents5.Contains( wxT( "Fmt 4.5" ) ),
323 "Expected 'Fmt 4.5' in gerber header with precision=5" );
324 BOOST_CHECK( !gerberContents5.Contains( wxT( "Fmt 4.6" ) ) );
325 gerberStream5.Close();
326
327 // Verify precision 6 produces "Fmt 4.6" in the file header
328 GERBER_WRITER gerber6( &board );
329 gerber6.SetOptions( VECTOR2I( 0, 0 ) );
330 gerber6.SetFormat( 6 );
331 BOOST_REQUIRE( gerber6.CreateDrillandMapFilesSet( tempDir.GetFullPath(), true, false, false, nullptr ) );
332
333 wxFFile gerberStream6( gerberFile5.GetFullPath(), wxT( "rb" ) );
334 wxString gerberContents6;
335 BOOST_REQUIRE( gerberStream6.ReadAll( &gerberContents6 ) );
336 BOOST_CHECK_MESSAGE( gerberContents6.Contains( wxT( "Fmt 4.6" ) ),
337 "Expected 'Fmt 4.6' in gerber header with precision=6" );
338 BOOST_CHECK( !gerberContents6.Contains( wxT( "Fmt 4.5" ) ) );
339 gerberStream6.Close();
340
341 wxFileName::Rmdir( tempDir.GetFullPath(), wxPATH_RMDIR_RECURSIVE );
342}
343
344
345// Regression test for https://gitlab.com/kicad/code/kicad/-/issues/23452
346// Drill files for vias spanning inner-to-back or back-to-inner layers produced
347// reversed layer order in the generated file name (e.g. "back-in2" instead of "in2-back").
348BOOST_AUTO_TEST_CASE( DrillFileLayerOrderInFilename )
349{
350 wxFileName tempDir = MakeTempDir();
351 wxFileName boardFile( tempDir.GetFullPath(), wxT( "layer_order_board.kicad_pcb" ) );
352
353 BOARD board;
354 board.SetCopperLayerCount( 4 );
355 board.SetFileName( boardFile.GetFullPath() );
356
357 // Via spanning In2 (bottom inner) to B_Cu: file must be named "in2-back", not "back-in2".
358 auto via = new PCB_VIA( &board );
359 via->SetPosition( VECTOR2I( 0, 0 ) );
360 via->SetViaType( VIATYPE::BURIED );
361 via->SetLayerPair( In2_Cu, B_Cu );
362 via->SetDrill( pcbIUScale.mmToIU( 0.30 ) );
363 via->SetWidth( pcbIUScale.mmToIU( 0.60 ) );
364 board.Add( via );
365
366 EXCELLON_WRITER excellon( &board );
367 excellon.SetOptions( false, false, VECTOR2I( 0, 0 ), false );
368 excellon.SetFormat( true );
369 BOOST_REQUIRE( excellon.CreateDrillandMapFilesSet( tempDir.GetFullPath(), true, false, nullptr ) );
370
371 // Correct order: top inner layer first, then back layer.
372 wxFileName correctFile( tempDir.GetFullPath(), wxT( "layer_order_board-in2-back.drl" ) );
373 wxFileName reversedFile( tempDir.GetFullPath(), wxT( "layer_order_board-back-in2.drl" ) );
374 BOOST_CHECK_MESSAGE( correctFile.FileExists(), "Expected drill file 'in2-back' not found" );
375 BOOST_CHECK_MESSAGE( !reversedFile.FileExists(), "Incorrectly named drill file 'back-in2' found" );
376
377 wxFileName::Rmdir( tempDir.GetFullPath(), wxPATH_RMDIR_RECURSIVE );
378}
379
380
381// Regression test for https://gitlab.com/kicad/code/kicad/-/issues/23005
382// GenDrillReportFile crashed when aReporter was null (the default)
383BOOST_AUTO_TEST_CASE( DrillReportNullReporter )
384{
385 wxFileName tempDir = MakeTempDir();
386 wxFileName boardFile( tempDir.GetFullPath(), wxT( "test_board.kicad_pcb" ) );
387
388 BOARD board;
389 board.SetCopperLayerCount( 2 );
390 board.SetFileName( boardFile.GetFullPath() );
391
392 wxFileName reportFile( tempDir.GetFullPath(), wxT( "test_board-drl.rpt" ) );
393
394 // Valid path with null reporter should succeed without crashing
395 EXCELLON_WRITER excellon( &board );
396 BOOST_CHECK( excellon.GenDrillReportFile( reportFile.GetFullPath() ) );
397 BOOST_CHECK( reportFile.FileExists() );
398
399 GERBER_WRITER gerber( &board );
400 BOOST_CHECK( gerber.GenDrillReportFile( reportFile.GetFullPath() ) );
401
402 // Invalid path with null reporter should return false without crashing
403 EXCELLON_WRITER excellon2( &board );
404 BOOST_CHECK( !excellon2.GenDrillReportFile( wxT( "/nonexistent/path/report.rpt" ) ) );
405
406 GERBER_WRITER gerber2( &board );
407 BOOST_CHECK( !gerber2.GenDrillReportFile( wxT( "/nonexistent/path/report.rpt" ) ) );
408
409 wxFileName::Rmdir( tempDir.GetFullPath(), wxPATH_RMDIR_RECURSIVE );
410}
411
412
413// Regression test for https://gitlab.com/kicad/code/kicad/-/issues/23289
414// GenDrillReportFile crashed with SIGSEGV when the board had drills because
415// printToolSummary() passed integer literal 0 instead of the FILE* to fmt::print()
416BOOST_AUTO_TEST_CASE( DrillReportWithTools )
417{
418 wxFileName tempDir = MakeTempDir();
419 wxFileName boardFile( tempDir.GetFullPath(), wxT( "test_board_with_drills.kicad_pcb" ) );
420
421 BOARD board;
422 board.SetCopperLayerCount( 2 );
423 board.SetFileName( boardFile.GetFullPath() );
424
425 auto via = new PCB_VIA( &board );
426 via->SetPosition( VECTOR2I( 0, 0 ) );
427 via->SetLayerPair( F_Cu, B_Cu );
428 via->SetDrill( pcbIUScale.mmToIU( 0.30 ) );
429 via->SetWidth( pcbIUScale.mmToIU( 0.60 ) );
430 board.Add( via );
431
432 wxFileName reportFile( tempDir.GetFullPath(), wxT( "test_board_with_drills-drl.rpt" ) );
433
434 EXCELLON_WRITER excellon( &board );
435 excellon.SetOptions( false, false, VECTOR2I( 0, 0 ), false );
436 excellon.SetFormat( true );
437 BOOST_CHECK_NO_THROW( excellon.GenDrillReportFile( reportFile.GetFullPath() ) );
438 BOOST_CHECK( reportFile.FileExists() );
439
440 wxFFile reportStream( reportFile.GetFullPath(), wxT( "rb" ) );
441 wxString reportContents;
442 BOOST_REQUIRE( reportStream.ReadAll( &reportContents ) );
443 BOOST_CHECK( reportContents.Contains( wxT( "T1" ) ) );
444 BOOST_CHECK( reportContents.Contains( wxT( "0.300mm" ) ) );
445 reportStream.Close();
446
447 wxFileName::Rmdir( tempDir.GetFullPath(), wxPATH_RMDIR_RECURSIVE );
448}
449
450
451// Regression test for https://gitlab.com/kicad/code/kicad/-/issues/24014
452// A non-filled PCB_SHAPE rectangle on F.SilkS was emitted as a donut_rc symbol with a
453// corner radius smaller than half the line width. Many ODB++ viewers reject the
454// resulting degenerate symbol, so the outline appeared to be missing. The exporter now
455// emits the four outline edges as line features, matching how a rectangle built from
456// individual line segments is exported.
457BOOST_AUTO_TEST_CASE( OdbPpUnfilledRectangleOnSilk )
458{
459 wxFileName tempDir = MakeTempDir();
460 wxFileName boardFile( tempDir.GetFullPath(), wxT( "silk_rect_board.kicad_pcb" ) );
461
462 BOARD board;
463 board.SetCopperLayerCount( 2 );
464 board.SetFileName( boardFile.GetFullPath() );
465
466 // Non-filled rectangle on F.SilkS
467 PCB_SHAPE* rect = new PCB_SHAPE( &board, SHAPE_T::RECTANGLE );
468 rect->SetStart( VECTOR2I( pcbIUScale.mmToIU( 10.0 ), pcbIUScale.mmToIU( 10.0 ) ) );
469 rect->SetEnd( VECTOR2I( pcbIUScale.mmToIU( 30.0 ), pcbIUScale.mmToIU( 20.0 ) ) );
470 rect->SetLayer( F_SilkS );
471 rect->SetFilled( false );
472 rect->SetWidth( pcbIUScale.mmToIU( 0.15 ) );
473 board.Add( rect );
474
475 wxFileName odbRoot( tempDir.GetFullPath(), wxEmptyString );
476 odbRoot.AppendDir( wxT( "odb_out" ) );
477 BOOST_REQUIRE( odbRoot.Mkdir( wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) );
478
479 PCB_IO_ODBPP odbExporter;
480 std::map<std::string, UTF8> props;
481 props["units"] = "mm";
482 props["sigfig"] = "4";
483 BOOST_REQUIRE_NO_THROW( odbExporter.SaveBoard( odbRoot.GetFullPath(), &board, &props ) );
484
485 wxFileName silkFeatures( odbRoot.GetFullPath(), wxT( "features" ) );
486 silkFeatures.AppendDir( wxT( "steps" ) );
487 silkFeatures.AppendDir( wxT( "pcb" ) );
488 silkFeatures.AppendDir( wxT( "layers" ) );
489 silkFeatures.AppendDir( wxT( "f.silkscreen" ) );
490 BOOST_REQUIRE( silkFeatures.FileExists() );
491
492 wxFFile silkStream( silkFeatures.GetFullPath(), wxT( "rb" ) );
493 wxString silkContents;
494 BOOST_REQUIRE( silkStream.ReadAll( &silkContents ) );
495 silkStream.Close();
496
497 // Four ODB++ line ("L ...") features describe the rectangle outline.
498 int lineCount = 0;
499 wxStringTokenizer lines( silkContents, wxT( "\n" ) );
500
501 while( lines.HasMoreTokens() )
502 {
503 if( lines.GetNextToken().StartsWith( wxT( "L " ) ) )
504 lineCount++;
505 }
506
507 BOOST_CHECK_EQUAL( lineCount, 4 );
508
509 // The degenerate donut_rc symbol should not appear anymore.
510 BOOST_CHECK( !silkContents.Contains( wxT( "donut_rc" ) ) );
511
512 wxFileName::Rmdir( odbRoot.GetFullPath(), wxPATH_RMDIR_RECURSIVE );
513 wxFileName::Rmdir( tempDir.GetFullPath(), wxPATH_RMDIR_RECURSIVE );
514}
515
516
517// Export aBoard to a fresh ODB++ tree below aTempDir and return the root of that tree. Symbol
518// dimensions come out in micrometres because the file units are millimetres.
519static wxFileName ExportOdbTree( BOARD* aBoard, const wxFileName& aTempDir )
520{
521 wxFileName odbRoot( aTempDir.GetFullPath(), wxEmptyString );
522 odbRoot.AppendDir( wxT( "odb_out" ) );
523 BOOST_REQUIRE( odbRoot.Mkdir( wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) );
524
525 PCB_IO_ODBPP odbExporter;
526 std::map<std::string, UTF8> props;
527 props["units"] = "mm";
528 props["sigfig"] = "4";
529 BOOST_REQUIRE_NO_THROW( odbExporter.SaveBoard( odbRoot.GetFullPath(), aBoard, &props ) );
530
531 return odbRoot;
532}
533
534
535static wxString ReadOdbLayerFeatures( const wxFileName& aOdbRoot, const wxString& aLayerDir )
536{
537 wxFileName features( aOdbRoot.GetFullPath(), wxT( "features" ) );
538 features.AppendDir( wxT( "steps" ) );
539 features.AppendDir( wxT( "pcb" ) );
540 features.AppendDir( wxT( "layers" ) );
541 features.AppendDir( aLayerDir );
542 BOOST_REQUIRE( features.FileExists() );
543
544 wxFFile stream( features.GetFullPath(), wxT( "rb" ) );
545 wxString contents;
546 BOOST_REQUIRE( stream.ReadAll( &contents ) );
547 stream.Close();
548
549 return contents;
550}
551
552
553// Regression test for https://gitlab.com/kicad/code/kicad/-/issues/25089
554// The inner diameter of the donut_r symbol standing in for an unfilled circle subtracted only
555// half the line width from the diameter, so the exported annulus was a quarter width too thin
556// and sat off-centre from the circle it came from. The board is the reporter's own project.
557BOOST_AUTO_TEST_CASE( OdbPpUnfilledCircleAnnulus )
558{
559 SETTINGS_MANAGER settingsManager;
560 std::unique_ptr<BOARD> board;
561
562 KI_TEST::LoadBoard( settingsManager, wxT( "issue25089/odb_circles" ), board );
563 BOOST_REQUIRE( board );
564
565 wxFileName tempDir = MakeTempDir();
566 wxFileName odbRoot = ExportOdbTree( board.get(), tempDir );
567
568 // Every circle in the project is 2 mm across with a 0.1 mm stroke, so the ring spans radius
569 // 0.95 mm to 1.05 mm
570 for( const wxString& layerDir : { wxString( wxT( "f.cu" ) ), wxString( wxT( "edge.cuts" ) ) } )
571 {
572 wxString contents = ReadOdbLayerFeatures( odbRoot, layerDir );
573
574 BOOST_CHECK_MESSAGE( contents.Contains( wxT( "donut_r2100.0x1900.0" ) ),
575 "Wrong annulus on " + layerDir + ", features file holds:\n"
576 + contents );
577 }
578
579 wxFileName::Rmdir( odbRoot.GetFullPath(), wxPATH_RMDIR_RECURSIVE );
580 wxFileName::Rmdir( tempDir.GetFullPath(), wxPATH_RMDIR_RECURSIVE );
581}
582
583
584// Companion to OdbPpUnfilledCircleAnnulus covering the two circles that have no donut_r spelling,
585// derived from the same project. On F.Cu the stroke is as wide as the circle and closes the hole;
586// on B.Cu the radius sits at the EDA_SHAPE clamp of INT_MAX / 2, where the doubled diameter used to
587// overflow a signed int and emit a negative dimension.
588BOOST_AUTO_TEST_CASE( OdbPpUnfilledCircleWithoutHole )
589{
590 SETTINGS_MANAGER settingsManager;
591 std::unique_ptr<BOARD> board;
592
593 KI_TEST::LoadBoard( settingsManager, wxT( "issue25089/odb_circle_edge_cases" ), board );
594 BOOST_REQUIRE( board );
595
596 wxFileName tempDir = MakeTempDir();
597 wxFileName odbRoot = ExportOdbTree( board.get(), tempDir );
598
599 wxString frontContents = ReadOdbLayerFeatures( odbRoot, wxT( "f.cu" ) );
600
601 BOOST_CHECK_MESSAGE( frontContents.Contains( wxT( "r400.0" ) )
602 && !frontContents.Contains( wxT( "donut" ) ),
603 "Circle with a hole-closing stroke should export as a solid pad, "
604 "features file holds:\n"
605 + frontContents );
606
607 // Feature records carry signed Y coordinates, so only the symbol definitions can be checked
608 wxString backContents = ReadOdbLayerFeatures( odbRoot, wxT( "b.cu" ) );
609 wxStringTokenizer backLines( backContents, wxT( "\n" ) );
610
611 while( backLines.HasMoreTokens() )
612 {
613 wxString line = backLines.GetNextToken();
614
615 if( line.StartsWith( wxT( "$" ) ) )
616 {
617 BOOST_CHECK_MESSAGE( !line.Contains( wxT( "-" ) ),
618 "Oversized circle overflowed to a negative symbol dimension: "
619 + line );
620 }
621 }
622
623 wxFileName::Rmdir( odbRoot.GetFullPath(), wxPATH_RMDIR_RECURSIVE );
624 wxFileName::Rmdir( tempDir.GetFullPath(), wxPATH_RMDIR_RECURSIVE );
625}
626
627
628namespace
629{
630PAD* AddSlotPad( FOOTPRINT* aFootprint, const VECTOR2I& aPos, PAD_ATTRIB aAttribute )
631{
632 PAD* pad = new PAD( aFootprint );
633 pad->SetAttribute( aAttribute );
634 pad->SetLayerSet( aAttribute == PAD_ATTRIB::NPTH ? PAD::UnplatedHoleMask()
635 : PAD::PTHMask() );
636 pad->SetPosition( aPos );
638 pad->SetSize( PADSTACK::ALL_LAYERS,
639 VECTOR2I( pcbIUScale.mmToIU( 2.0 ), pcbIUScale.mmToIU( 1.0 ) ) );
640 pad->SetDrillShape( PAD_DRILL_SHAPE::OBLONG );
641 pad->SetDrillSize( VECTOR2I( pcbIUScale.mmToIU( 1.7 ), pcbIUScale.mmToIU( 0.6 ) ) );
642 aFootprint->Add( pad );
643
644 return pad;
645}
646} // anonymous namespace
647
648
649// Regression test for https://gitlab.com/kicad/code/kicad/-/issues/24677
650// Plated through-hole pads with a slotted (oval) drill were dropped entirely from the
651// ODB++ drill output. Round holes and non-plated slots exported fine, but plated slots
652// were never written to the plated drill layer. The exporter now emits PTH slots on the
653// plated drill layer just like NPTH slots on the non-plated layer.
654BOOST_AUTO_TEST_CASE( OdbPpPlatedSlotDrill )
655{
656 wxFileName tempDir = MakeTempDir();
657 wxFileName boardFile( tempDir.GetFullPath(), wxT( "plated_slot_board.kicad_pcb" ) );
658
659 BOARD board;
660 board.SetCopperLayerCount( 2 );
661 board.SetFileName( boardFile.GetFullPath() );
662
663 FOOTPRINT* fp = new FOOTPRINT( &board );
664 fp->SetPosition( VECTOR2I( pcbIUScale.mmToIU( 50.0 ), pcbIUScale.mmToIU( 50.0 ) ) );
665 board.Add( fp );
666
667 // A plated slot and a non-plated slot in the same design.
668 AddSlotPad( fp, VECTOR2I( pcbIUScale.mmToIU( 50.0 ), pcbIUScale.mmToIU( 50.0 ) ),
670 AddSlotPad( fp, VECTOR2I( pcbIUScale.mmToIU( 60.0 ), pcbIUScale.mmToIU( 50.0 ) ),
672
673 wxFileName odbRoot( tempDir.GetFullPath(), wxEmptyString );
674 odbRoot.AppendDir( wxT( "odb_out" ) );
675 BOOST_REQUIRE( odbRoot.Mkdir( wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) );
676
677 PCB_IO_ODBPP odbExporter;
678 std::map<std::string, UTF8> props;
679 props["units"] = "mm";
680 props["sigfig"] = "4";
681 BOOST_REQUIRE_NO_THROW( odbExporter.SaveBoard( odbRoot.GetFullPath(), &board, &props ) );
682
683 auto layerDir = [&]( const wxString& aLayer )
684 {
685 wxFileName dir( odbRoot.GetFullPath(), wxEmptyString );
686 dir.AppendDir( wxT( "steps" ) );
687 dir.AppendDir( wxT( "pcb" ) );
688 dir.AppendDir( wxT( "layers" ) );
689 dir.AppendDir( aLayer );
690
691 return dir;
692 };
693
694 auto readFile = []( const wxFileName& aFile )
695 {
696 wxFFile stream( aFile.GetFullPath(), wxT( "rb" ) );
697 wxString contents;
698 BOOST_REQUIRE( stream.ReadAll( &contents ) );
699 stream.Close();
700
701 return contents;
702 };
703
704 // ODB++ array members (e.g. drill tools) are indented, so trim before matching.
705 auto countLinesStartingWith = []( const wxString& aContents, const wxString& aPrefix )
706 {
707 int count = 0;
708 wxStringTokenizer lines( aContents, wxT( "\n" ) );
709
710 while( lines.HasMoreTokens() )
711 {
712 wxString line = lines.GetNextToken();
713 line.Trim( false );
714
715 if( line.StartsWith( aPrefix ) )
716 count++;
717 }
718
719 return count;
720 };
721
722 auto containsOvalSymbol = []( const wxString& aContents )
723 {
724 wxStringTokenizer lines( aContents, wxT( "\n" ) );
725
726 while( lines.HasMoreTokens() )
727 {
728 wxString line = lines.GetNextToken();
729
730 // Symbol definition lines look like "$0 oval<w>x<h>".
731 if( line.StartsWith( wxT( "$" ) ) && line.Contains( wxT( "oval" ) ) )
732 return true;
733 }
734
735 return false;
736 };
737
738 // The plated slot must appear on the plated drill layer as exactly one oval pad feature,
739 // and the slot must NOT leak onto this layer as a non-plated hole.
740 wxFileName platedDir = layerDir( wxT( "drill_plated_f.cu-b.cu" ) );
741 BOOST_REQUIRE_MESSAGE( platedDir.DirExists(), "Plated drill layer should exist" );
742
743 wxFileName platedFeatures( platedDir.GetFullPath(), wxT( "features" ) );
744 BOOST_REQUIRE( platedFeatures.FileExists() );
745 wxString platedContents = readFile( platedFeatures );
746
747 BOOST_CHECK_EQUAL( countLinesStartingWith( platedContents, wxT( "P " ) ), 1 );
748 BOOST_CHECK_MESSAGE( containsOvalSymbol( platedContents ),
749 "Plated slot should use an oval symbol" );
750
751 wxFileName platedTools( platedDir.GetFullPath(), wxT( "tools" ) );
752 BOOST_REQUIRE( platedTools.FileExists() );
753 wxString platedToolsContents = readFile( platedTools );
754 BOOST_CHECK_EQUAL( countLinesStartingWith( platedToolsContents, wxT( "TYPE=PLATED" ) ), 1 );
755 BOOST_CHECK_EQUAL( countLinesStartingWith( platedToolsContents, wxT( "TYPE=NON_PLATED" ) ), 0 );
756
757 // The non-plated slot must still appear on the non-plated drill layer (unchanged), and
758 // the plated slot must NOT leak onto it.
759 wxFileName nonPlatedDir = layerDir( wxT( "drill_non-plated_f.cu-b.cu" ) );
760 BOOST_REQUIRE_MESSAGE( nonPlatedDir.DirExists(), "Non-plated drill layer should exist" );
761
762 wxFileName nonPlatedFeatures( nonPlatedDir.GetFullPath(), wxT( "features" ) );
763 BOOST_REQUIRE( nonPlatedFeatures.FileExists() );
764 wxString nonPlatedContents = readFile( nonPlatedFeatures );
765
766 BOOST_CHECK_EQUAL( countLinesStartingWith( nonPlatedContents, wxT( "P " ) ), 1 );
767 BOOST_CHECK_MESSAGE( containsOvalSymbol( nonPlatedContents ),
768 "Non-plated slot should still export an oval symbol" );
769
770 wxFileName nonPlatedTools( nonPlatedDir.GetFullPath(), wxT( "tools" ) );
771 BOOST_REQUIRE( nonPlatedTools.FileExists() );
772 wxString nonPlatedToolsContents = readFile( nonPlatedTools );
773 BOOST_CHECK_EQUAL( countLinesStartingWith( nonPlatedToolsContents, wxT( "TYPE=NON_PLATED" ) ),
774 1 );
775 BOOST_CHECK_EQUAL( countLinesStartingWith( nonPlatedToolsContents, wxT( "TYPE=PLATED" ) ), 0 );
776
777 wxFileName::Rmdir( odbRoot.GetFullPath(), wxPATH_RMDIR_RECURSIVE );
778 wxFileName::Rmdir( tempDir.GetFullPath(), wxPATH_RMDIR_RECURSIVE );
779}
780
781
782// Regression test for https://gitlab.com/kicad/code/kicad/-/issues/25021
783// Backdrill holes are recorded as non-plated, so the report's backdrill sections asked
784// printToolSummary() for plated tools only and always claimed zero holes, even though
785// the matching Excellon files carried them.
786BOOST_AUTO_TEST_CASE( DrillReportBackdrillHoleCount )
787{
788 SETTINGS_MANAGER settingsManager;
789 std::unique_ptr<BOARD> board;
790
791 KI_TEST::LoadBoard( settingsManager, wxT( "issue25021/backdrill" ), board );
792 BOOST_REQUIRE( board );
793
794 wxFileName tempDir = MakeTempDir();
795 wxFileName boardFile( tempDir.GetFullPath(), wxT( "backdrill.kicad_pcb" ) );
796
797 // Keep the derived drill file names out of the source tree
798 board->SetFileName( boardFile.GetFullPath() );
799
800 wxFileName reportFile( tempDir.GetFullPath(), wxT( "backdrill-drl.rpt" ) );
801
802 EXCELLON_WRITER excellon( board.get() );
803 excellon.SetOptions( false, false, VECTOR2I( 0, 0 ), false );
804 excellon.SetFormat( true );
805 BOOST_REQUIRE( excellon.GenDrillReportFile( reportFile.GetFullPath() ) );
806
807 wxFFile reportStream( reportFile.GetFullPath(), wxT( "rb" ) );
808 wxString reportContents;
809 BOOST_REQUIRE( reportStream.ReadAll( &reportContents ) );
810 reportStream.Close();
811
812 struct BACKDRILL_SECTION
813 {
814 long holes = -1;
815 int toolLines = 0;
816 };
817
818 std::map<wxString, BACKDRILL_SECTION> sections;
819 wxString currentFile;
820 wxStringTokenizer lines( reportContents, wxT( "\n" ) );
821
822 while( lines.HasMoreTokens() )
823 {
824 wxString line = lines.GetNextToken();
825 line.Trim( true ).Trim( false );
826
827 if( line.StartsWith( wxT( "Drill file '" ) ) )
828 currentFile = line.AfterFirst( '\'' ).BeforeFirst( '\'' );
829
830 if( line.StartsWith( wxT( "T" ) ) && line.Contains( wxT( "0.330mm" ) ) )
831 sections[currentFile].toolLines++;
832
833 wxString count;
834
835 if( line.StartsWith( wxT( "Total backdrilled holes count " ), &count ) )
836 {
837 long value = -1;
838 BOOST_REQUIRE( count.ToLong( &value ) );
839 sections[currentFile].holes = value;
840 }
841 }
842
843 // The board backdrills two vias from F.Cu down to In3.Cu, and one via each from
844 // B.Cu up to In3.Cu and to In6.Cu
845 const std::map<wxString, long> expected = {
846 { wxT( "backdrill_Backdrills_Drill_1_4.drl" ), 2 },
847 { wxT( "backdrill_Backdrills_Drill_10_4.drl" ), 1 },
848 { wxT( "backdrill_Backdrills_Drill_10_7.drl" ), 1 }
849 };
850
851 // No backdrill section beyond the three expected ones
852 BOOST_CHECK_EQUAL( sections.size(), expected.size() );
853
854 for( const auto& [file, holes] : expected )
855 {
856 auto section = sections.find( file );
857
858 if( section == sections.end() )
859 {
860 BOOST_ERROR( "No backdrill section for " << file );
861 continue;
862 }
863
864 BOOST_CHECK_EQUAL( section->second.holes, holes );
865
866 // The section must also list the 0.33mm backdrill tool it counted
867 BOOST_CHECK_EQUAL( section->second.toolLines, 1 );
868 }
869
870 // Plated through holes and the unplated summary must be unaffected
871 BOOST_CHECK( reportContents.Contains( wxT( "Total plated holes count 4" ) ) );
872 BOOST_CHECK( reportContents.Contains( wxT( "Total unplated holes count 0" ) ) );
873
874 wxFileName::Rmdir( tempDir.GetFullPath(), wxPATH_RMDIR_RECURSIVE );
875}
constexpr EDA_IU_SCALE pcbIUScale
Definition base_units.h:121
Information pertinent to a Pcbnew printed circuit board.
Definition board.h:373
void Add(BOARD_ITEM *aItem, ADD_MODE aMode=ADD_MODE::INSERT, bool aSkipConnectivity=false) override
Removes an item from the container.
Definition board.cpp:1355
void SetFileName(const wxString &aFileName)
Definition board.h:408
void SetCopperLayerCount(int aCount)
Definition board.cpp:1000
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
@ In2_Cu
Definition layer_ids.h:63
@ 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
void LoadBoard(SETTINGS_MANAGER &aSettingsManager, const wxString &aRelPath, std::unique_ptr< BOARD > &aBoard)
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.
static wxString ReadOdbLayerFeatures(const wxFileName &aOdbRoot, const wxString &aLayerDir)
static wxFileName ExportOdbTree(BOARD *aBoard, const wxFileName &aTempDir)
BOOST_AUTO_TEST_CASE(BackdrillCamOutputs)
BOOST_REQUIRE(intersection.has_value()==c.ExpectedIntersection.has_value())
VECTOR3I expected(15, 30, 45)
BOOST_CHECK_EQUAL(result, "25.4")
VECTOR2< int32_t > VECTOR2I
Definition vector2d.h:683