From f590cbd49c3bae97d79b86eb841f8876723fbc37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20T=C3=B6rcsv=C3=A1ri?= Date: Fri, 10 Jul 2026 18:10:17 +0200 Subject: [PATCH] feat: kicad_tools --drill + --plot-board --drill [outdir]: excellon drill files (kicad-cli JOB defaults, no map files). --plot-board [--pdf] [--layers a,b,...] [out]: single SVG/PDF document via PCB_PLOTTER; layers default to the board's enabled set, --layers takes canonical or user names. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01ADzCSeN3Q9DJL3YW2FZTXB --- kicad | 2 +- wasm/cli/kicad_tools_main.cpp | 6 +- wasm/cli/pcb_convert_main.cpp | 272 +++++++++++++++++++++++++++++++++- wasm/cli/sym_convert_main.cpp | 4 +- 4 files changed, 280 insertions(+), 4 deletions(-) diff --git a/kicad b/kicad index 7f123c0..8a25db2 160000 --- a/kicad +++ b/kicad @@ -1 +1 @@ -Subproject commit 7f123c0fa6cbae79d7ec803667d01d043dd44f14 +Subproject commit 8a25db244dec1362ed276deb2e06ef76ad196337 diff --git a/wasm/cli/kicad_tools_main.cpp b/wasm/cli/kicad_tools_main.cpp index fa65144..0900aed 100644 --- a/wasm/cli/kicad_tools_main.cpp +++ b/wasm/cli/kicad_tools_main.cpp @@ -8,6 +8,8 @@ * pcbnew side (pcb_convert_main.cpp): * kicad_tools --drc [--json] [--strict] [] * kicad_tools --gerbers [] + * kicad_tools --drill [] + * kicad_tools --plot-board [--pdf] [--layers ] [] * * eeschema side (sym_convert_main.cpp) — everything else: * kicad_tools --convert-lib @@ -37,7 +39,9 @@ int main( int argc, char** argv ) int rc; if( argc >= 2 - && ( std::strcmp( argv[1], "--drc" ) == 0 || std::strcmp( argv[1], "--gerbers" ) == 0 ) ) + && ( std::strcmp( argv[1], "--drc" ) == 0 || std::strcmp( argv[1], "--gerbers" ) == 0 + || std::strcmp( argv[1], "--drill" ) == 0 + || std::strcmp( argv[1], "--plot-board" ) == 0 ) ) { rc = pcbConvertMain( argc, argv ); } diff --git a/wasm/cli/pcb_convert_main.cpp b/wasm/cli/pcb_convert_main.cpp index 08fa8da..b7f1370 100644 --- a/wasm/cli/pcb_convert_main.cpp +++ b/wasm/cli/pcb_convert_main.cpp @@ -22,6 +22,16 @@ * (default: the input's directory). Zone fills plot as * saved (no re-check). Exit: 0 ok, 2 usage, 4 failure. * + * drill: pcb_convert --drill [] + * Excellon drill files, kicad-cli defaults (mm, decimal, absolute + * origin), no map files. Exit: 0 ok, 2 usage, 4 failure. + * + * plot: pcb_convert --plot-board [--pdf] [--layers ] + * [] + * Single SVG (default) or PDF document via PCB_PLOTTER; layers + * default to the board's enabled set, or a comma-separated list + * of canonical/user layer names. Exit: 0 ok, 2 usage, 4 failure. + * * No GUI, no renderer, no embind bindings. Wired into pcbnew/CMakeLists.txt * behind the KICAD_PCB_CONVERTER_WASM option (see * scripts/kicad/build-pcb_convert.sh). @@ -49,8 +59,12 @@ #include #include #include +#include #include +#include #include +#include +#include #include #include #include @@ -442,6 +456,183 @@ int runGerbers( const char* aInPath, const char* aOutDir ) return failed ? 4 : 0; } +// ── headless board plot (SVG / PDF) ─────────────────────────────────────────── +// Mirrors PCBNEW_JOBS_HANDLER::JobExportSvg/Pdf single-document mode via +// PCB_PLOTTER (already linked for --gerbers). Default layer set = the board's +// enabled layers in stackup plot order; --layers takes comma-separated +// canonical (F.Cu) or user layer names. + +LSEQ parseLayerList( BOARD* aBrd, const char* aArg, std::string& aError ) +{ + LSEQ seq; + wxString arg = wxString::FromUTF8( aArg ); + + for( const wxString& token : wxSplit( arg, ',' ) ) + { + bool found = false; + + for( PCB_LAYER_ID layer : LSET::AllLayersMask() ) + { + if( token == LSET::Name( layer ) || token == aBrd->GetLayerName( layer ) ) + { + seq.push_back( layer ); + found = true; + break; + } + } + + if( !found ) + { + aError = "unknown layer '" + std::string( token.ToUTF8() ) + "'"; + return LSEQ(); + } + } + + return seq; +} + + +int runPlotBoard( const char* aInPath, bool aPdf, const char* aLayersArg, const char* aOutPath ) +{ + wxFileName fn( wxString::FromUTF8( aInPath ) ); + fn.MakeAbsolute(); + + BOARD* brd = loadBoardHeadless( aInPath ); + + if( !brd ) + return 4; + + LSEQ layers; + + if( aLayersArg ) + { + std::string layerError; + layers = parseLayerList( brd, aLayersArg, layerError ); + + if( layers.empty() ) + { + std::fprintf( stderr, "%s: error: %s\n", aInPath, layerError.c_str() ); + return 2; + } + } + else + { + layers = brd->GetEnabledLayers().SeqStackupForPlotting(); + } + + wxString outPath; + + if( aOutPath ) + { + outPath = wxString::FromUTF8( aOutPath ); + } + else + { + wxFileName out( fn ); + out.SetExt( aPdf ? wxS( "pdf" ) : wxS( "svg" ) ); + outPath = out.GetFullPath(); + } + + REPORTER& reporter = CLI_REPORTER::GetInstance(); + + // kicad-cli plot defaults (colors, drawing sheet, margins) per format. + PCB_PLOT_PARAMS plotOpts; + + if( aPdf ) + { + JOB_EXPORT_PCB_PDF pdfJob; + PCB_PLOTTER::PlotJobToPlotOpts( plotOpts, &pdfJob, reporter ); + } + else + { + JOB_EXPORT_PCB_SVG svgJob; + PCB_PLOTTER::PlotJobToPlotOpts( plotOpts, &svgJob, reporter ); + } + + trace( "runPlotBoard: PCB_PLOTTER::Plot" ); + PCB_PLOTTER plotter( brd, &reporter, plotOpts ); + + const bool ok = plotter.Plot( outPath, layers, LSEQ(), false /*aUseGerberX2*/, + true /*single document*/, std::nullopt, std::nullopt, + std::nullopt ); + + std::fprintf( stderr, "%s: %s (%d layers) -> %s\n", aInPath, ok ? "OK" : "FAIL", + (int) layers.size(), (const char*) outPath.ToUTF8() ); + + return ok ? 0 : 4; +} + +// ── headless drill export (excellon) ────────────────────────────────────────── +// Mirrors PCBNEW_JOBS_HANDLER::JobExportDrill's excellon branch with the +// default JOB_EXPORT_PCB_DRILL options (kicad-cli defaults); no map files +// (the writer would drive a plotter for those — add on demand). + +int runDrill( const char* aInPath, const char* aOutDir ) +{ + wxFileName fn( wxString::FromUTF8( aInPath ) ); + fn.MakeAbsolute(); + + BOARD* brd = loadBoardHeadless( aInPath ); + + if( !brd ) + return 4; + + wxString outPath = aOutDir ? wxString::FromUTF8( aOutDir ) : fn.GetPath(); + + if( !outPath.EndsWith( wxS( "/" ) ) ) + outPath += wxS( "/" ); + + JOB_EXPORT_PCB_DRILL drillJob; // kicad-cli defaults + + VECTOR2I offset; + + if( drillJob.m_drillOrigin == JOB_EXPORT_PCB_DRILL::DRILL_ORIGIN::ABS ) + offset = VECTOR2I( 0, 0 ); + else + offset = brd->GetDesignSettings().GetAuxOrigin(); + + EXCELLON_WRITER::ZEROS_FMT zeroFmt; + + switch( drillJob.m_zeroFormat ) + { + case JOB_EXPORT_PCB_DRILL::ZEROS_FORMAT::KEEP_ZEROS: + zeroFmt = EXCELLON_WRITER::KEEP_ZEROS; + break; + case JOB_EXPORT_PCB_DRILL::ZEROS_FORMAT::SUPPRESS_LEADING: + zeroFmt = EXCELLON_WRITER::SUPPRESS_LEADING; + break; + case JOB_EXPORT_PCB_DRILL::ZEROS_FORMAT::SUPPRESS_TRAILING: + zeroFmt = EXCELLON_WRITER::SUPPRESS_TRAILING; + break; + case JOB_EXPORT_PCB_DRILL::ZEROS_FORMAT::DECIMAL: + default: + zeroFmt = EXCELLON_WRITER::DECIMAL_FORMAT; + break; + } + + // The upstream precision tables are statics in UI/handler TUs; the values + // are fixed (metric 3.3, inch 2.4). + const bool metric = drillJob.m_drillUnits == JOB_EXPORT_PCB_DRILL::DRILL_UNITS::MM; + DRILL_PRECISION precision = metric ? DRILL_PRECISION( 3, 3 ) : DRILL_PRECISION( 2, 4 ); + + trace( "runDrill: CreateDrillandMapFilesSet" ); + EXCELLON_WRITER writer( brd ); + writer.SetFormat( metric, zeroFmt, precision.m_Lhs, precision.m_Rhs ); + writer.SetOptions( drillJob.m_excellonMirrorY, drillJob.m_excellonMinimalHeader, offset, + drillJob.m_excellonCombinePTHNPTH ); + writer.SetRouteModeForOvalHoles( drillJob.m_excellonOvalDrillRoute ); + writer.SetMapFileFormat( PLOT_FORMAT::PDF ); // unused: maps off + + REPORTER& reporter = CLI_REPORTER::GetInstance(); + const bool ok = writer.CreateDrillandMapFilesSet( outPath, true /*drill*/, false /*map*/, + &reporter ); + + std::fprintf( stderr, "%s: %s (excellon) -> %s\n", aInPath, ok ? "OK" : "FAIL", + (const char*) outPath.ToUTF8() ); + + return ok ? 0 : 4; +} + } // namespace @@ -588,8 +779,87 @@ int pcbConvertMain( int argc, char** argv ) _exit( rc ); } + if( argc >= 2 && std::strcmp( argv[1], "--drill" ) == 0 ) + { + wxDisableAsserts(); + + if( argc < 3 ) + { + std::fprintf( stderr, "usage: kicad_tools --drill []\n" ); + return 2; + } + + int rc = 4; + + try + { + rc = runDrill( argv[2], argc >= 4 ? argv[3] : nullptr ); + } + catch( const std::exception& e ) + { + std::fprintf( stderr, "%s: error: %s\n", argv[2], e.what() ); + } + + // Same static-dtor rationale as --drc above. + std::fflush( nullptr ); + _exit( rc ); + } + + if( argc >= 2 && std::strcmp( argv[1], "--plot-board" ) == 0 ) + { + wxDisableAsserts(); + + bool pdf = false; + const char* layersArg = nullptr; + int arg = 2; + + while( arg < argc && std::strncmp( argv[arg], "--", 2 ) == 0 ) + { + if( std::strcmp( argv[arg], "--pdf" ) == 0 ) + { + pdf = true; + } + else if( std::strcmp( argv[arg], "--layers" ) == 0 && arg + 1 < argc ) + { + layersArg = argv[++arg]; + } + else + { + break; + } + + arg++; + } + + if( arg >= argc ) + { + std::fprintf( stderr, "usage: kicad_tools --plot-board [--pdf] [--layers ] " + " []\n" ); + return 2; + } + + const char* inPath = argv[arg++]; + const char* outPath = arg < argc ? argv[arg] : nullptr; + int rc = 4; + + try + { + rc = runPlotBoard( inPath, pdf, layersArg, outPath ); + } + catch( const std::exception& e ) + { + std::fprintf( stderr, "%s: error: %s\n", inPath, e.what() ); + } + + // Same static-dtor rationale as --drc above. + std::fflush( nullptr ); + _exit( rc ); + } + std::fprintf( stderr, "usage: kicad_tools --drc [--json] [--strict] []\n" - " kicad_tools --gerbers []\n" ); + " kicad_tools --gerbers []\n" + " kicad_tools --drill []\n" + " kicad_tools --plot-board [--pdf] [--layers ] []\n" ); return 2; } diff --git a/wasm/cli/sym_convert_main.cpp b/wasm/cli/sym_convert_main.cpp index 4e0164a..f323187 100644 --- a/wasm/cli/sym_convert_main.cpp +++ b/wasm/cli/sym_convert_main.cpp @@ -1114,7 +1114,9 @@ int symConvertMain( int argc, char** argv ) " kicad_tools --bom []\n" " kicad_tools --plot [--pdf] []\n" " kicad_tools --drc [--json] [--strict] []\n" - " kicad_tools --gerbers []\n" ); + " kicad_tools --gerbers []\n" + " kicad_tools --drill []\n" + " kicad_tools --plot-board [--pdf] [--layers ] []\n" ); return 2; }