From f1ea6b3965f8ae4f7aa52fb0eb4b760a54701bac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20T=C3=B6rcsv=C3=A1ri?= Date: Fri, 24 Jul 2026 14:30:08 +0200 Subject: [PATCH] plugins 0002: kicad_tools --ipc356 + --fab-components fab exporters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two generic (manufacturer-agnostic) pcbnew-side subcommands for platform manufacturing plugins: --ipc356 (IPC-D-356 netlist via IPC356D_WRITER) and --fab-components (board metrics + per-footprint placement/BOM JSON — absolute board coords, consumers apply their own conventions). Bumps the kicad submodule for the diet keep-back of export_d356.cpp. Also closes a CI cache gap: wasm-cache-hash never hashed wasm/**, so a pure wasm/cli change would cache-hit stale output — { dir: "wasm" } is now an input (busts the cache once on landing). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01Mr8PQ34yCfvEtEcwSUvLAV --- kicad | 2 +- scripts/deploy/wasm-cache-hash.mjs | 7 + wasm/cli/kicad_tools_main.cpp | 6 +- wasm/cli/pcb_convert_main.cpp | 217 ++++++++++++++++++++++++++++- 4 files changed, 229 insertions(+), 3 deletions(-) diff --git a/kicad b/kicad index d06f653..ca1dde0 160000 --- a/kicad +++ b/kicad @@ -1 +1 @@ -Subproject commit d06f653f0edc18b1f7cc6e52d0bb3be426a5cefa +Subproject commit ca1dde0e5ffdd9fa32062d816c59a301d1d0fc6c diff --git a/scripts/deploy/wasm-cache-hash.mjs b/scripts/deploy/wasm-cache-hash.mjs index 0969be5..3bd37c6 100644 --- a/scripts/deploy/wasm-cache-hash.mjs +++ b/scripts/deploy/wasm-cache-hash.mjs @@ -59,6 +59,13 @@ const INPUTS = [ // Per-tool compile recipes (compile flags / emcc link options). { dir: "scripts/kicad", match: /^build-.*\.sh$/ }, + // The pcbjam-repo C++/CMake/shim sources compiled INTO the tools (bindings, + // cli mains, kiplatform, gl1, stubs, editor glue). These live outside the + // kicad/wx submodules, so without this entry a pure wasm/** edit (e.g. a + // new kicad_tools subcommand) would cache-hit stale output — plugins 0002 + // shipped exactly that way before this entry existed. + { dir: "wasm" }, + // Dependency builds (boost/cairo/occ/... — the sysroot the wasm links against). { dir: "scripts/deps" }, diff --git a/wasm/cli/kicad_tools_main.cpp b/wasm/cli/kicad_tools_main.cpp index a752f00..51bce80 100644 --- a/wasm/cli/kicad_tools_main.cpp +++ b/wasm/cli/kicad_tools_main.cpp @@ -10,6 +10,8 @@ * kicad_tools --gerbers [] * kicad_tools --drill [] * kicad_tools --plot-board [--pdf] [--layers ] [] + * kicad_tools --ipc356 [] + * kicad_tools --fab-components [] * * eeschema side (sym_convert_main.cpp) — everything else: * kicad_tools --convert-lib @@ -47,7 +49,9 @@ int main( int argc, char** argv ) if( argc >= 2 && ( 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 ) ) + || std::strcmp( argv[1], "--plot-board" ) == 0 + || std::strcmp( argv[1], "--ipc356" ) == 0 + || std::strcmp( argv[1], "--fab-components" ) == 0 ) ) { rc = pcbConvertMain( argc, argv ); } diff --git a/wasm/cli/pcb_convert_main.cpp b/wasm/cli/pcb_convert_main.cpp index cd5866e..e82144a 100644 --- a/wasm/cli/pcb_convert_main.cpp +++ b/wasm/cli/pcb_convert_main.cpp @@ -32,6 +32,20 @@ * default to the board's enabled set, or a comma-separated list * of canonical/user layer names. Exit: 0 ok, 2 usage, 4 failure. * + * ipc356: pcb_convert --ipc356 [] + * IPC-D-356 netlist via IPC356D_WRITER, default settings (out + * defaults to -netlist.ipc). Exit: 0 ok, 2 usage, 4 load + * failure, 5 write failure. + * + * fab: pcb_convert --fab-components [] + * Manufacturer-agnostic fabrication data JSON (plugins 0002): + * board metrics (Edge.Cuts bbox mm, aux origin, copper layer + * count, thickness) + one entry per footprint (ref, fpid, side, + * absolute position mm in board coords — no Y flip, no + * aux-origin subtraction; consumers derive their own — rotation + * deg, smd/exclude/dnp attributes, all non-empty fields). + * Exit: 0 ok, 2 usage, 4 load failure, 5 write 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). @@ -43,6 +57,7 @@ #include #include #include +#include #include #include #include @@ -54,13 +69,16 @@ #include #include +#include #include #include #include #include #include +#include #include #include +#include #include #include #include @@ -70,6 +88,7 @@ #include #include #include +#include #include #include #include @@ -636,6 +655,147 @@ int runDrill( const char* aInPath, const char* aOutDir ) return ok ? 0 : 4; } +// ── IPC-D-356 netlist export ────────────────────────────────────────────────── +// Same IPC356D_WRITER call the pcbnew GUI export makes, default settings; net +// assignments come from the saved board (no connectivity rebuild needed). + +int runIpc356( const char* aInPath, const char* aOutPath ) +{ + wxFileName fn( wxString::FromUTF8( aInPath ) ); + fn.MakeAbsolute(); + + BOARD* brd = loadBoardHeadless( aInPath ); + + if( !brd ) + return 4; + + wxString outPath; + + if( aOutPath ) + { + outPath = wxString::FromUTF8( aOutPath ); + } + else + { + wxFileName out( fn ); + out.SetName( out.GetName() + wxS( "-netlist" ) ); + out.SetExt( wxS( "ipc" ) ); + outPath = out.GetFullPath(); + } + + trace( "runIpc356: IPC356D_WRITER::Write" ); + IPC356D_WRITER writer( brd ); + const bool ok = writer.Write( outPath ); + + std::fprintf( stderr, "%s: %s (ipc-356d) -> %s\n", aInPath, ok ? "OK" : "FAIL", + (const char*) outPath.ToUTF8() ); + + return ok ? 0 : 5; +} + +// ── fabrication components JSON ─────────────────────────────────────────────── +// Manufacturer-agnostic per-footprint placement/BOM data + board metrics +// (plugins 0002). Coordinates are absolute board coords in mm, as-is: no Y +// flip, no aux-origin subtraction — manufacturer-specific conventions (e.g. +// PCBWay's aux-relative negated-Y positions) are applied by the consumer. + +int runFabComponents( const char* aInPath, const char* aOutPath ) +{ + wxFileName fn( wxString::FromUTF8( aInPath ) ); + fn.MakeAbsolute(); + + BOARD* brd = loadBoardHeadless( aInPath ); + + if( !brd ) + return 4; + + wxString outPath; + + if( aOutPath ) + { + outPath = wxString::FromUTF8( aOutPath ); + } + else + { + wxFileName out( fn ); + out.SetName( out.GetName() + wxS( "-fab-components" ) ); + out.SetExt( wxS( "json" ) ); + outPath = out.GetFullPath(); + } + + trace( "runFabComponents: collecting" ); + + const BOX2I bbox = brd->GetBoardEdgesBoundingBox(); + const VECTOR2I aux = brd->GetDesignSettings().GetAuxOrigin(); + + nlohmann::json board; + board["widthMm"] = pcbIUScale.IUTomm( bbox.GetWidth() ); + board["heightMm"] = pcbIUScale.IUTomm( bbox.GetHeight() ); + board["bboxOriginMm"] = { { "x", pcbIUScale.IUTomm( bbox.GetX() ) }, + { "y", pcbIUScale.IUTomm( bbox.GetY() ) } }; + board["auxOriginMm"] = { { "x", pcbIUScale.IUTomm( aux.x ) }, + { "y", pcbIUScale.IUTomm( aux.y ) } }; + board["copperLayers"] = brd->GetCopperLayerCount(); + board["thicknessMm"] = pcbIUScale.IUTomm( brd->GetDesignSettings().GetBoardThickness() ); + + nlohmann::json footprints = nlohmann::json::array(); + + for( FOOTPRINT* fp : brd->Footprints() ) + { + const VECTOR2I pos = fp->GetPosition(); + const int attrs = fp->GetAttributes(); + + nlohmann::json f; + f["ref"] = std::string( fp->GetReference().ToUTF8() ); + f["fpid"] = std::string( fp->GetFPID().GetUniStringLibId().ToUTF8() ); + f["side"] = fp->GetLayer() == B_Cu ? "bottom" : "top"; + f["posMm"] = { { "x", pcbIUScale.IUTomm( pos.x ) }, + { "y", pcbIUScale.IUTomm( pos.y ) } }; + f["rotationDeg"] = fp->GetOrientationDegrees(); + f["smd"] = bool( attrs & FP_SMD ); + f["excludeFromPos"] = fp->IsExcludedFromPosFiles(); + f["excludeFromBom"] = fp->IsExcludedFromBOM(); + f["dnp"] = fp->IsDNP(); + + nlohmann::json fields = nlohmann::json::object(); + + std::vector fieldList; + fp->GetFields( fieldList, false ); + + for( PCB_FIELD* field : fieldList ) + { + const wxString text = field->GetShownText( false ); + + if( !text.IsEmpty() ) + fields[std::string( field->GetName().ToUTF8() )] = std::string( text.ToUTF8() ); + } + + f["fields"] = fields; + footprints.push_back( f ); + } + + nlohmann::json j; + j["board"] = board; + j["footprints"] = footprints; + + trace( "runFabComponents: writing" ); + std::ofstream out( outPath.fn_str() ); + out << j.dump( 2 ) << "\n"; + out.close(); + + if( !out ) + { + std::fprintf( stderr, "%s: error: unable to write %s\n", aInPath, + (const char*) outPath.ToUTF8() ); + return 5; + } + + std::fprintf( stderr, "%s: OK (%d footprints) -> %s\n", aInPath, + (int) brd->Footprints().size(), (const char*) outPath.ToUTF8() ); + + return 0; +} + } // namespace @@ -1035,10 +1195,65 @@ int pcbConvertMain( int argc, char** argv ) _exit( rc ); } + if( argc >= 2 && std::strcmp( argv[1], "--ipc356" ) == 0 ) + { + wxDisableAsserts(); + + if( argc < 3 ) + { + std::fprintf( stderr, "usage: kicad_tools --ipc356 []\n" ); + return 2; + } + + int rc = 4; + + try + { + rc = runIpc356( 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], "--fab-components" ) == 0 ) + { + wxDisableAsserts(); + + if( argc < 3 ) + { + std::fprintf( stderr, + "usage: kicad_tools --fab-components []\n" ); + return 2; + } + + int rc = 4; + + try + { + rc = runFabComponents( 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 ); + } + std::fprintf( stderr, "usage: kicad_tools --drc [--json] [--strict] []\n" " kicad_tools --gerbers []\n" " kicad_tools --drill []\n" - " kicad_tools --plot-board [--pdf] [--layers ] []\n" ); + " kicad_tools --plot-board [--pdf] [--layers ] []\n" + " kicad_tools --ipc356 []\n" + " kicad_tools --fab-components []\n" ); return 2; }