pcbjam/wasm/stubs/exporter_step_stub.cpp
Viktor Vaczi db9d6ee04b feat(wasm): occ-split — lazy occ_service worker; kicad_editor drops OCC (−31%)
Move OpenCASCADE out of the merged editor image into occ_service: a separate
emscripten module (-sASYNCIFY=0, MODULARIZE, in-container -Oz finalize, 2N+8
pre-warmed pthread pool) booted lazily in a dedicated Web Worker on the first
STEP export or STEP/IGES model parse. kicad_editor.wasm ~190 MB -> 130 MB;
sessions that never touch OCC never fetch its 57 MB. STEP export works in the
browser for the first time: the unchanged desktop dialog runs EXPORTER_STEP,
whose wasm shadow suspends into globalThis.occService and the export bytes go
straight to a browser download (never entering the editor heap). STEP/IGES 3D
models parse in the worker via the oce shadow (S3D WriteCache/ReadCache wire).

- wasm/occ-service/: service CMake target (hooked from the kicad fork's
  top-level CMakeLists, wasm/editor pattern), embind entry
  (occExport/occLoadModel), wxConfig pre-js.
- wasm/stubs/{exporter_step,oce_plugin}_stub.cpp: EM_ASYNC_JS worker bridges
  (callee-shadowing; no caller #ifdefs).
- web/standalone: provider installed whenever the kicad_editor bundle boots
  (cross-face safe); ONE shared worker-boot source occ-worker.js (vite ?raw;
  the e2e stub reads the same file) — blob worker with locateFile absolutized
  against the glue URL; export download-name guard.
- deps: OCC builds with RapidJSON so its glTF/GLB writer exists — pinned to
  the vcpkg master snapshot 2025-02-26 (24b5e7a8b27f), the same code official
  KiCad consumes via vcpkg.json's opencascade[rapidjson]; rapidjson's latest
  tag (v1.1.0, 2016) is ill-formed under modern clang.
- tests: occ-export dialog e2e (lazy-fetch boundary + STEP download bytes),
  occ-probe incl. a 9-format matrix (step/stpz/brep/xao/ply/stl/glb/u3d/pdf),
  3d-viewer-models hard-asserts the worker parse; occ provider stub installed
  ambiently by the kicad fixtures.

Validated against desktop kicad-cli 10.0.4: geometric exact equality (bbox
delta 0 um, volume delta 0.0000%) for STEP/GLB/STL/BREP/STPZ across three
boards and option sweeps — with desktop OCC 7.9 vs wasm OCC 7.8; PLY/XAO/PDF
structurally equal; U3D same-size (quantizer float LSBs differ). Full kicad
e2e green on Firefox and Chromium; standalone verified end to end (lazy fetch
only on the Export click; export.step 60,628 B ISO-10303-21; loadModel 700 KB
STEP -> 569 KB scenegraph cache).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 12:39:58 +02:00

183 lines
5.5 KiB
C++

/*
* WASM shadow of EXPORTER_STEP — the editor build compiles this INSTEAD of
* exporters/step/exporter_step.cpp (no OCC in pcbnew.wasm; see
* docs/features/occ-split/README.md). The class layout comes from the real
* header; only the three symbols other TUs reference are defined here:
* constructor, destructor, Export().
*
* Export() bridges to the occ_service worker: stage the live BOARD as sexpr
* text in MEMFS, ship it + the official JOB_EXPORT_PCB_3D JSON through an
* EM_ASYNC_JS suspend (globalThis.occService.request, installed by the web
* app), and report the outcome. The exported file's bytes never enter this
* module — the JS provider hands them straight to the browser download path.
*
* Callers stay untouched: PCBNEW_JOBS_HANDLER::JobExportStep and the browser
* branch of DIALOG_EXPORT_STEP construct EXPORTER_STEP exactly as on desktop.
*/
#include <cstdlib>
#include <string>
#include <emscripten.h>
#include <wx/filename.h>
#include <wx/string.h>
#include <nlohmann/json.hpp>
#include <board.h>
#include <reporter.h>
#include <jobs/job_export_pcb_3d.h>
#include <pcb_io/kicad_sexpr/pcb_io_kicad_sexpr.h>
#include <exporters/step/exporter_step.h>
// Complete types for the unique_ptr members destroyed in ~EXPORTER_STEP (their
// headers still exist in the tree/sysroot; only the OCC *link* is gone).
#include <exporters/step/step_pcb_model.h>
#include <filename_resolver.h>
namespace
{
const char* const TMP_BOARD = "/tmp/pcbjam_occ_export_board.kicad_pcb";
// Optional side-channel: the dialog seam serializes the FULL job (including
// fields EXPORTER_STEP_PARAMS doesn't carry, e.g. the assembly variant) right
// before calling Export(). Consumed once. When unset (jobs-handler path), the
// job JSON is reconstructed from m_params.
std::string s_nextJobJson;
} // namespace
// One-shot job-JSON override for the next EXPORTER_STEP::Export() call.
extern "C" void Pcbjam_SetExportJobJson( const char* aJson )
{
s_nextJobJson = aJson ? aJson : "";
}
// Suspends pcbnew (Asyncify; the __asyncjs__* import is auto-covered by
// scripts/common/asyncify-imports.txt) while the worker exports. JS returns a
// malloc'd JSON string: { ok, report } — the download already happened there.
EM_ASYNC_JS( char*, js_occExportRequest,
( const char* aBoardPath, const char* aJobJson, const char* aFileName ),
{
const boardPath = UTF8ToString( aBoardPath );
const jobJson = UTF8ToString( aJobJson );
const fileName = UTF8ToString( aFileName );
let res;
try
{
const hook = globalThis.occService;
if( !hook || typeof hook.request !== 'function' )
{
res = { ok: false, report: 'occ_service provider not installed' };
}
else
{
const board = FS.readFile( boardPath ); // Uint8Array copy — transferable
res = await hook.request( { kind: 'export', board, jobJson, fileName } );
}
}
catch( e )
{
console.error( '[pcbjam-occ] export request failed:', e );
res = { ok: false, report: 'occ_service request failed: ' + e };
}
const s = JSON.stringify( res || { ok: false, report: 'occ_service: no response' } );
const n = lengthBytesUTF8( s ) + 1;
const p = _malloc( n );
stringToUTF8( s, p, n );
return p;
} )
EXPORTER_STEP::EXPORTER_STEP( BOARD* aBoard, const EXPORTER_STEP_PARAMS& aParams,
REPORTER* aReporter ) :
m_params( aParams ),
m_reporter( aReporter ),
m_board( aBoard ),
m_platingThickness( 0 )
{
}
EXPORTER_STEP::~EXPORTER_STEP()
{
}
bool EXPORTER_STEP::Export()
{
if( !m_board )
return false;
// Stage the LIVE board (unsaved edits included) as sexpr text in MEMFS.
try
{
PCB_IO_KICAD_SEXPR io;
io.SaveBoard( wxString::FromUTF8( TMP_BOARD ), m_board );
}
catch( const std::exception& e )
{
if( m_reporter )
{
m_reporter->Report( wxString::Format( wxT( "Failed to stage board for export: %s" ),
e.what() ),
RPT_SEVERITY_ERROR );
}
return false;
}
// The official job JSON. Prefer the seam-provided full job; otherwise
// rebuild one from the params we were constructed with.
std::string jobJson;
if( !s_nextJobJson.empty() )
{
jobJson = std::move( s_nextJobJson );
s_nextJobJson.clear();
}
else
{
JOB_EXPORT_PCB_3D job;
job.m_3dparams = m_params;
job.SetStepFormat( m_params.m_Format );
nlohmann::json j;
job.ToJson( j );
jobJson = j.dump();
}
const wxString downloadName = wxFileName( m_outputFile ).GetFullName();
char* response = js_occExportRequest( TMP_BOARD, jobJson.c_str(),
downloadName.utf8_string().c_str() );
bool ok = false;
try
{
nlohmann::json res = nlohmann::json::parse( response ? response : "{}" );
ok = res.value( "ok", false );
const std::string report = res.value( "report", std::string() );
if( m_reporter && !report.empty() )
{
m_reporter->Report( wxString::FromUTF8( report.c_str() ),
ok ? RPT_SEVERITY_INFO : RPT_SEVERITY_ERROR );
}
}
catch( ... )
{
if( m_reporter )
m_reporter->Report( wxT( "occ_service: malformed response" ), RPT_SEVERITY_ERROR );
}
std::free( response );
wxRemoveFile( wxString::FromUTF8( TMP_BOARD ) );
return ok;
}