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>
225 lines
5.6 KiB
C++
225 lines
5.6 KiB
C++
/*
|
|
* WASM shadow of the oce 3D plugin's flat-C surface (oce3d_*) — the editor
|
|
* build links this INSTEAD of s3d_plugin_oce (no OCC in pcbnew.wasm; see
|
|
* docs/features/occ-split/README.md). The static plugin registry
|
|
* (kicad/3d-viewer/3d_cache/pcbjam_static_3d_plugins.cpp) consumes exactly
|
|
* this surface, so the plugin manager, S3D_CACHE, and the scene build compile
|
|
* untouched — .step/.iges footprint models keep loading, their parse just runs
|
|
* in the occ_service worker.
|
|
*
|
|
* oce3d_Load: the model file is already in MEMFS (materialized by
|
|
* PCBJAM_3D::EnsureModelFile before the plugin dispatch). Ship its bytes to
|
|
* the worker (EM_ASYNC_JS suspend — legal here: EnsureModelFile already
|
|
* suspends on this same S3D_CACHE::load path), get back the SCENEGRAPH
|
|
* serialized in KiCad's own binary cache format, and rebuild it with
|
|
* S3D::ReadCache.
|
|
*
|
|
* Metadata (extensions/filters/versions) mirrors plugins/3d/oce/oce.cpp and
|
|
* include/plugins/3d/3d_plugin.h so the manager's handshake and extension map
|
|
* are identical to the real plugin's.
|
|
*/
|
|
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include <emscripten.h>
|
|
|
|
#include <wx/string.h>
|
|
#include <wx/filefn.h>
|
|
|
|
#include <plugins/3dapi/ifsg_api.h>
|
|
|
|
class SCENEGRAPH;
|
|
|
|
namespace
|
|
{
|
|
|
|
// Non-Windows extension/filter lists from plugins/3d/oce/oce.cpp (FILE_DATA).
|
|
const std::vector<std::string> k_extensions = {
|
|
"stp", "STP", "stpZ", "stpz", "STPZ", "step", "STEP", "stp.gz", "STP.GZ", "step.gz",
|
|
"STEP.GZ", "igs", "IGS", "iges", "IGES"
|
|
};
|
|
|
|
const std::vector<std::string> k_filters = {
|
|
"STEP (*.stp;*.STP;*.stpZ;*.stpz;*.STPZ;*.step;*.STEP;*.stp.gz;*.STP.GZ;*.step.gz;"
|
|
"*.STEP.GZ)|*.stp;*.STP;*.stpZ;*.stpz;*.STPZ;*.step;*.STEP;*.stp.gz;*.STP.GZ;"
|
|
"*.step.gz;*.STEP.GZ",
|
|
"IGES (*.igs;*.IGS;*.iges;*.IGES)|*.igs;*.IGS;*.iges;*.IGES"
|
|
};
|
|
|
|
bool acceptAnyCacheTag( const char*, void* )
|
|
{
|
|
// The cache blob just crossed the worker boundary from our own writer —
|
|
// there is no plugin-version drift to guard against.
|
|
return true;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
// Suspends pcbnew while the worker parses + tessellates the model. JS returns
|
|
// a malloc'd path string: the scenegraph-cache file it wrote into this
|
|
// module's MEMFS ("" on failure).
|
|
EM_ASYNC_JS( char*, js_occLoadModelRequest, ( const char* aModelPath ),
|
|
{
|
|
const modelPath = UTF8ToString( aModelPath );
|
|
let cachePath = '';
|
|
|
|
try
|
|
{
|
|
const hook = globalThis.occService;
|
|
|
|
if( !hook || typeof hook.request !== 'function' )
|
|
{
|
|
console.error( '[pcbjam-occ] loadModel: occ_service provider not installed' );
|
|
}
|
|
else
|
|
{
|
|
const bytes = FS.readFile( modelPath ); // Uint8Array copy — transferable
|
|
const dot = modelPath.lastIndexOf( '.' );
|
|
const ext = dot >= 0 ? modelPath.slice( dot + 1 ) : 'step';
|
|
const res = await hook.request( { kind: 'loadModel', bytes, ext } );
|
|
|
|
if( res && res.ok && res.bytes && res.bytes.length )
|
|
{
|
|
cachePath = '/tmp/pcbjam_occ_model_cache.3dc';
|
|
FS.writeFile( cachePath, res.bytes );
|
|
}
|
|
else if( res && res.report )
|
|
{
|
|
console.error( '[pcbjam-occ] loadModel failed:', res.report );
|
|
}
|
|
}
|
|
}
|
|
catch( e )
|
|
{
|
|
console.error( '[pcbjam-occ] loadModel request failed:', e );
|
|
cachePath = '';
|
|
}
|
|
|
|
const n = lengthBytesUTF8( cachePath ) + 1;
|
|
const p = _malloc( n );
|
|
stringToUTF8( cachePath, p, n );
|
|
return p;
|
|
} )
|
|
|
|
|
|
extern "C"
|
|
{
|
|
|
|
// --- class handshake (include/plugins/3d/3d_plugin.h semantics) -------------
|
|
|
|
char const* oce3d_GetKicadPluginClass( void )
|
|
{
|
|
return "PLUGIN_3D";
|
|
}
|
|
|
|
|
|
void oce3d_GetClassVersion( unsigned char* Major, unsigned char* Minor, unsigned char* Patch,
|
|
unsigned char* Revision )
|
|
{
|
|
if( Major )
|
|
*Major = 1;
|
|
|
|
if( Minor )
|
|
*Minor = 0;
|
|
|
|
if( Patch )
|
|
*Patch = 0;
|
|
|
|
if( Revision )
|
|
*Revision = 0;
|
|
}
|
|
|
|
|
|
bool oce3d_CheckClassVersion( unsigned char Major, unsigned char, unsigned char, unsigned char )
|
|
{
|
|
return Major == 1;
|
|
}
|
|
|
|
|
|
// --- plugin identity (plugins/3d/oce/oce.cpp values) -------------------------
|
|
|
|
const char* oce3d_GetKicadPluginName( void )
|
|
{
|
|
return "PLUGIN_3D_OCE";
|
|
}
|
|
|
|
|
|
void oce3d_GetPluginVersion( unsigned char* Major, unsigned char* Minor, unsigned char* Patch,
|
|
unsigned char* Revision )
|
|
{
|
|
if( Major )
|
|
*Major = 1;
|
|
|
|
if( Minor )
|
|
*Minor = 4;
|
|
|
|
if( Patch )
|
|
*Patch = 2;
|
|
|
|
if( Revision )
|
|
*Revision = 0;
|
|
}
|
|
|
|
|
|
int oce3d_GetNExtensions( void )
|
|
{
|
|
return (int) k_extensions.size();
|
|
}
|
|
|
|
|
|
char const* oce3d_GetModelExtension( int aIndex )
|
|
{
|
|
if( aIndex < 0 || aIndex >= (int) k_extensions.size() )
|
|
return nullptr;
|
|
|
|
return k_extensions[aIndex].c_str();
|
|
}
|
|
|
|
|
|
int oce3d_GetNFilters( void )
|
|
{
|
|
return (int) k_filters.size();
|
|
}
|
|
|
|
|
|
char const* oce3d_GetFileFilter( int aIndex )
|
|
{
|
|
if( aIndex < 0 || aIndex >= (int) k_filters.size() )
|
|
return nullptr;
|
|
|
|
return k_filters[aIndex].c_str();
|
|
}
|
|
|
|
|
|
bool oce3d_CanRender( void )
|
|
{
|
|
return true;
|
|
}
|
|
|
|
|
|
SCENEGRAPH* oce3d_Load( char const* aFileName )
|
|
{
|
|
if( !aFileName )
|
|
return nullptr;
|
|
|
|
char* cachePath = js_occLoadModelRequest( aFileName );
|
|
|
|
if( !cachePath || !*cachePath )
|
|
{
|
|
std::free( cachePath );
|
|
return nullptr;
|
|
}
|
|
|
|
// ReadCache returns the top-level SCENEGRAPH as its SGNODE base.
|
|
SGNODE* node = S3D::ReadCache( cachePath, nullptr, &acceptAnyCacheTag );
|
|
|
|
wxRemoveFile( wxString::FromUTF8( cachePath ) );
|
|
std::free( cachePath );
|
|
|
|
return reinterpret_cast<SCENEGRAPH*>( node );
|
|
}
|
|
|
|
} // extern "C"
|