pcbjam/wasm/bindings/kicad_editor_embind.cpp
Viktor Vaczi 63ed1f3c1f e2e/CI: dual-engine suites, per-engine screenshots, SwiftShader retired, prod web suite, CI-coverage gate
Squash of experiment/ff-big-modules vs main.

Big-module routing removed: native-EH shrank kicad_editor below
SpiderMonkey's x86-64 code budget (runs 29355049705/29356152413 green on
stock Firefox), so BIG_MODULE_SPECS routing and the baseline-only-JIT
crutch are gone — kicad-firefox and kicad-chromium both run the full
suite, with the module compiled the way real users' browsers compile it.

Per-engine screenshots end to end: stableShot/shotPath write
test-results/<engine>/<name>.png; baselines move to
baseline-screenshots/{chromium,firefox}/ and the whole tools/screenshots
pipeline (compare/promote/manifest/spec-map/changelog/Discord) keys on
<engine>/<name>. Previously Firefox and Chromium renders of one spec
overwrote each other and Firefox renders were never actually gated.
Seeded from CI run 29421380806 (92 new firefox baselines, +24 chromium
web-suite shots); manifest generated from the baseline tree.

One merged playwright.config.ts (kicad/asyncify/coroutine/perf as
projects); ~25 dead npm scripts dropped. The web suite is gated in CI for
the first time ever (4 rotted specs fixed, 5 broken lib-bridge specs
triaged as fixme in docs/features/web-e2e-rot/); cheap lint step after
npm ci; last 26 blind-sleep violations fixed.

SwiftShader retired: CI Chromium renders WebGL on ANGLE → Mesa llvmpipe
(--use-gl=angle --use-angle=gl --ignore-gpu-blocklist; the blocklist flag
is mandatory — llvmpipe is blocklisted and WebGL is silently unavailable
without it) in BOTH configs. Under WORKERS=4 congestion SwiftShader
transiently failed the first post-board-load draw and the recovery
cascade ended in a silent permanent Cairo fallback — that engine flip was
the "~1.2% changedRatio both directions" occ-export baseline flake.
Validated 160/160 across two 80-repeat rigs; full analysis in
docs/features/wx-parity-bugs/occ-export-context-eviction.md. Chromium
baselines shift slightly on llvmpipe — promote once from the first green
run. Deflakes the new coverage exposed: presence baselines settle before
capture; presence fixtures declare current file formats; perf gets its
own outputDir so CI evidence survives; occ-export settles the board paint
before the export dialog; menu-item waits (waitForRenderedByLabel before
clickMenuItem) in 4 specs + the TESTING.md rule.

Web suite runs the PROD build, in parallel: webServer becomes backend
`start` + the standalone's e2e:preview (build-preview.mjs: link-wasm →
stash the public/wasm symlink aside during vite build, build-demo.mjs's
move — then vite preview as the persistent server). The wasm middleware
serves /wasm/* in preview and emits COOP/COEP/CORP itself (a pthread
worker script's own response must carry COEP or Chrome kills it with
ERR_BLOCKED_BY_RESPONSE). VITE_* flags bake at build time;
VITE_ALLOW_USER_OVERRIDE joins turbo globalEnv. fullyParallel + default
workers: 5.2m → 1.4m. Determinism fixes the parallel run exposed:
shared-page specs become serial groups; locks.spec grabs alice's exact
item via the new kicadCollabTestSelectByUuid hook (cross-tab "first
footprint" order is not a ysync invariant); quit specs poll page.url()
(quit supersedes its own navigation — NS_BINDING_ABORTED on Firefox).
Suite: 51 passed / 12 skipped / 0 failed in 1.6m.

CI-coverage gate (lint:ci-coverage): every tests/**/*.spec.ts must be
reachable from the npm scripts the workflows invoke — scraped from
.github/workflows/, resolved through package.json, coverage asked from
playwright --list itself. Rules: uncovered-spec + orphan-project (with a
documented LOCAL_ONLY_PROJECTS allowlist). Gating next to
lint:determinism; 138 spec files / 13 projects accounted for.

Product fixes kept from the investigations (reachable on real GPUs too):
wx 7799fd1be5 — paint flags clear before dispatch + Invalidate always
propagates; kicad 3dcfea5e45 — SwiftShader pass-boundary flush +
per-instance font texture + first-frame GL-error drain (GAL recovery
recovers instead of falling back to Cairo) + the user-facing eeschema
switch navigates again under __EMSCRIPTEN__ (project-sync's
FaceRegistered gate had rerouted it into the hidden sync player; caught
by the newly-gated web suite).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018eUxiPApHgGiu9NFyQfhAq
2026-07-17 12:21:54 +02:00

509 lines
19 KiB
C++

/*
* Embind dispatcher for the merged kicad_editor WASM image (editor-unification Part 2).
*
* The pcbnew and eeschema binding TUs each implement the collab bridge for their own
* frame and, standalone, register the SAME JS-facing names. In the merged image both
* TUs are compiled with -DKICAD_MERGED_EMBIND, which
* - compiles out their duplicate frame-agnostic definitions (kicadOpenFile,
* extern "C" kicadCollabOnSave) — the single definitions live HERE, and
* - compiles out their shared-name EMSCRIPTEN_BINDINGS registrations — registered
* once HERE, dispatching to the renamed per-editor entries (pcbCollab… and
* schCollab…) on whichever editor frame is live. Per-editor unique names
* (kicadSaveBoard, kicadSaveSchematic, kicadCollabTestItemBlob, Board_…) keep
* flowing from the per-editor blocks unchanged.
*
* With the one-frame-per-page-load model exactly one of pcbEditorActive() /
* schEditorActive() is true — the same dynamic_cast probe every per-editor entry
* already starts with. JS-facing names and signatures are IDENTICAL to the standalone
* bundles, so the web app and tests need no per-bundle API differences.
*
* Deliberately header-light: no pcbnew/eeschema headers (avoids mixing both include
* roots in one TU); only the generic KIWAY_PLAYER surface + the extern declarations.
*/
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#include <emscripten/bind.h>
#include <algorithm>
#include <string>
#include <vector>
#include <wx/app.h>
#include <wx/string.h>
#include <wx/window.h>
#include <wx/frame.h>
#include <wx/menu.h>
#include <wx/statusbr.h>
#include <wx/aui/framemanager.h>
#include <kiway.h>
#include <kiway_player.h>
#include <pcbjam_read_only.h>
#include <project.h>
#include "pcbjam_libs_reload.h"
using namespace emscripten;
// Per-editor entry points and frame probes — defined (with external linkage) in
// pcbnew_embind.cpp / eeschema_embind.cpp.
bool pcbEditorActive();
void pcbCollabApply( std::string aJson );
void pcbCollabApplyItems( std::string aJson );
std::string pcbCollabSnapshot();
std::string pcbCollabSnapshotItems();
std::string pcbCollabTestMoveFirst( int aDx, int aDy );
std::string pcbCollabGetPos( std::string aId );
bool pcbCollabTestRemoveItem( std::string aId );
bool pcbCollabTestRotateItem( std::string aId, double aDeg );
// Collab-aware undo (ysync miss 09).
bool pcbCollabTestUndo();
int pcbCollabTestUndoDepth();
// Presence (collab-presence 0002) + comment pins/panning (0005).
void pcbCollabPresenceStart();
void pcbCollabSetRemote( std::string aJson );
void pcbCollabSetPins( std::string aJson );
void pcbCollabSetViewport( double aCx, double aCy );
// Follow-user (collab-presence 0008).
void pcbCollabFitViewport( double aCx, double aCy, double aHalfW, double aHalfH );
void pcbCollabSetStyle( std::string aJson );
std::string pcbCollabTestListItems( int aCount );
std::string pcbCollabTestDemoSet();
std::string pcbCollabGetViewport();
std::string pcbCollabGetSelection();
// Cross-app selection (collab-presence 0006).
std::string pcbCollabGetSelectionFull();
std::string pcbCollabTestGetCrossMapped();
std::string pcbCollabTestSelectComponent();
bool pcbCollabTestSelectByUuid( std::string aUuid );
// Selection soft-locks (collab-presence 0007).
void pcbCollabReleaseSelection( std::string aUuidsJson, std::string aHolder );
std::string pcbCollabTestGetLocked();
std::string pcbCollabTestSelectFirst();
bool pcbCollabTestClearSelection();
bool schEditorActive();
int schLibsSymbolUsage( std::string aLibNickname, std::string aSymbolName );
void schCollabApply( std::string aJson );
void schCollabApplyItems( std::string aJson );
std::string schCollabSnapshot();
std::string schCollabSnapshotItems();
std::string schCollabTestMoveFirst( int aDx, int aDy );
std::string schCollabGetPos( std::string aId );
bool schCollabTestRemoveItem( std::string aId );
bool schCollabTestRotateItem( std::string aId, double aDeg );
// Collab-aware undo (ysync miss 09).
bool schCollabTestUndo();
int schCollabTestUndoDepth();
// Presence (collab-presence 0003 — eeschema counterparts) + pins (0005).
void schCollabPresenceStart();
void schCollabSetRemote( std::string aJson );
void schCollabSetPins( std::string aJson );
void schCollabSetViewport( double aCx, double aCy );
// Follow-user (collab-presence 0008).
void schCollabFitViewport( double aCx, double aCy, double aHalfW, double aHalfH );
void schCollabSetStyle( std::string aJson );
std::string schCollabTestListItems( int aCount );
std::string schCollabTestDemoSet();
std::string schCollabGetViewport();
std::string schCollabGetSelection();
// Cross-app selection (collab-presence 0006).
std::string schCollabGetSelectionFull();
std::string schCollabTestGetCrossMapped();
std::string schCollabTestSelectComponent();
bool schCollabTestSelectByUuid( std::string aUuid );
// Selection soft-locks (collab-presence 0007).
void schCollabReleaseSelection( std::string aUuidsJson, std::string aHolder );
std::string schCollabTestGetLocked();
std::string schCollabTestSelectFirst();
bool schCollabTestClearSelection();
// Programmatically open a project file in the running editor frame, without UI
// automation. Frame-agnostic (any KIWAY_PLAYER); byte-identical to the definition the
// standalone bundles compile from their own binding TU.
static bool kicadOpenFile( std::string path )
{
KIWAY_PLAYER* frame =
wxTheApp ? static_cast<KIWAY_PLAYER*>( wxTheApp->GetTopWindow() ) : nullptr;
if( !frame )
return false;
if( wxWindow* blocking = frame->Kiway().GetBlockingDialog() )
blocking->Close( true );
return frame->OpenProjectFiles(
std::vector<wxString>( 1, wxString::FromUTF8( path.c_str() ) ) );
}
// Canvas-only chrome toggle (features/mobile): hide/show every AUI pane
// except the central draw canvas, plus the menubar and status bar, so the GAL
// canvas fills the frame. Generic wxFrame/wxAui surface only (keeps this TU
// header-light and serves both editor frames). Hidden bars release their space
// because the wasm port's frame client-area math skips !IsShown() bars (native
// parity, see wxwidgets/src/wasm/frame.cpp). Returns false until the editor
// frame exists — main() builds it after runtime init — so JS polls this.
// Hide-time visibility snapshot. KiCad keeps several panes hidden by default
// (Search, Properties, Net Inspector, …), so a blanket Show(true) on restore
// would surface panes the user never had open — restore only what the hide
// actually took away. Keyed to the frame so a snapshot never leaks onto a
// different frame's wxAuiManager.
static struct
{
wxFrame* frame = nullptr;
bool valid = false;
bool menuShown = false;
bool statusShown = false;
std::vector<wxString> paneNames;
} s_chromeSnap;
static bool chromeSkipsPane( const wxAuiPaneInfo& aPane )
{
// keep the central editor canvas (named "DrawFrame" in both editors)
return aPane.dock_direction == wxAUI_DOCK_CENTER || aPane.name == wxT( "DrawFrame" );
}
static bool kicadSetChrome( bool aShow )
{
wxFrame* frame =
wxTheApp ? dynamic_cast<wxFrame*>( wxTheApp->GetTopWindow() ) : nullptr;
if( !frame )
return false;
wxMenuBar* menuBar = frame->GetMenuBar();
wxStatusBar* statusBar = frame->GetStatusBar();
wxAuiManager* mgr = wxAuiManager::GetManager( frame );
if( !aShow )
{
// A repeated hide keeps the original snapshot (idempotent).
if( !s_chromeSnap.valid || s_chromeSnap.frame != frame )
{
s_chromeSnap.frame = frame;
s_chromeSnap.menuShown = menuBar && menuBar->IsShown();
s_chromeSnap.statusShown = statusBar && statusBar->IsShown();
s_chromeSnap.paneNames.clear();
if( mgr )
{
wxAuiPaneInfoArray& panes = mgr->GetAllPanes();
for( size_t i = 0; i < panes.GetCount(); ++i )
{
wxAuiPaneInfo& pane = panes.Item( i );
if( !chromeSkipsPane( pane ) && pane.IsShown() )
s_chromeSnap.paneNames.push_back( pane.name );
}
}
s_chromeSnap.valid = true;
}
}
// A show with no snapshot (or one taken on a different frame) falls back
// to revealing the standard chrome instead of obeying stale state.
const bool haveSnap = s_chromeSnap.valid && s_chromeSnap.frame == frame;
if( menuBar )
menuBar->Show( aShow && ( haveSnap ? s_chromeSnap.menuShown : true ) );
// Kept alive rather than detached: KiCad SetStatusText()s on every cursor
// move, and wxFrameBase wxCHECKs a null status bar.
if( statusBar )
statusBar->Show( aShow && ( haveSnap ? s_chromeSnap.statusShown : true ) );
if( mgr )
{
wxAuiPaneInfoArray& panes = mgr->GetAllPanes();
for( size_t i = 0; i < panes.GetCount(); ++i )
{
wxAuiPaneInfo& pane = panes.Item( i );
if( chromeSkipsPane( pane ) )
continue;
if( !aShow )
{
pane.Show( false );
}
else if( haveSnap )
{
if( std::find( s_chromeSnap.paneNames.begin(), s_chromeSnap.paneNames.end(),
pane.name )
!= s_chromeSnap.paneNames.end() )
{
pane.Show( true );
}
}
else if( pane.IsToolbar() )
{
// Show with no (or a stale, other-frame) snapshot: reveal the
// toolbars only — blanket-showing plain panels would surface
// the default-hidden ones.
pane.Show( true );
}
}
mgr->Update();
}
if( aShow )
s_chromeSnap.valid = false;
frame->SendSizeEvent();
return true;
}
// Read-only viewer lock (read-only-viewer): flips the process-global
// PCBJAM_READ_ONLY flag consumed by TOOL_MANAGER (view-only action allowlist)
// and the selection tools (nothing selectable), and mirrors it onto the
// project so the setup dialogs grey out. Zoom/pan stay live (mouse/touch
// bypass the tool system; keyboard zoom/pan is allowlisted). Returns false
// until the editor frame exists — main() builds it after runtime init — so
// JS polls this; the shell fails CLOSED if it never applies.
static bool kicadSetReadOnly( bool aReadOnly )
{
KIWAY_PLAYER* frame =
wxTheApp ? dynamic_cast<KIWAY_PLAYER*>( wxTheApp->GetTopWindow() ) : nullptr;
if( !frame )
return false;
PCBJAM_READ_ONLY::Set( aReadOnly );
frame->Prj().SetReadOnly( aReadOnly );
return true;
}
// C++ → JS save notification. Called from BOTH fork save chokepoints
// (PCB_EDIT_FRAME::SavePcbFile and SCH_EDIT_FRAME::saveSchematicFile) — one shared
// definition serves the merged image. No-op without a JS listener.
extern "C" void kicadCollabOnSave( const char* aPath )
{
EM_ASM( {
if( window.kicadCollab && window.kicadCollab.onSave )
window.kicadCollab.onSave( UTF8ToString( $0 ) );
}, aPath );
}
// Dispatch shims: route each shared JS name to the live editor's implementation.
// The sch path is the fallback arm so a JS call with NO frame up behaves like the
// standalone bundles (the per-editor impls no-op / return empty on a null frame).
static void collabApply( std::string aJson )
{
pcbEditorActive() ? pcbCollabApply( aJson ) : schCollabApply( aJson );
}
static void collabApplyItems( std::string aJson )
{
pcbEditorActive() ? pcbCollabApplyItems( aJson ) : schCollabApplyItems( aJson );
}
static std::string collabSnapshot()
{
return pcbEditorActive() ? pcbCollabSnapshot() : schCollabSnapshot();
}
static std::string collabSnapshotItems()
{
return pcbEditorActive() ? pcbCollabSnapshotItems() : schCollabSnapshotItems();
}
static std::string collabTestMoveFirst( int aDx, int aDy )
{
return pcbEditorActive() ? pcbCollabTestMoveFirst( aDx, aDy )
: schCollabTestMoveFirst( aDx, aDy );
}
static std::string collabGetPos( std::string aId )
{
return pcbEditorActive() ? pcbCollabGetPos( aId ) : schCollabGetPos( aId );
}
static bool collabTestRemoveItem( std::string aId )
{
return pcbEditorActive() ? pcbCollabTestRemoveItem( aId ) : schCollabTestRemoveItem( aId );
}
static bool collabTestRotateItem( std::string aId, double aDeg )
{
return pcbEditorActive() ? pcbCollabTestRotateItem( aId, aDeg )
: schCollabTestRotateItem( aId, aDeg );
}
static bool collabTestUndo()
{
return pcbEditorActive() ? pcbCollabTestUndo() : schCollabTestUndo();
}
static int collabTestUndoDepth()
{
return pcbEditorActive() ? pcbCollabTestUndoDepth() : schCollabTestUndoDepth();
}
// Placed-instance count for a library symbol — meaningful only with a schematic
// frame; every other editor answers 0 ("nothing placed here uses it").
static int libsSymbolUsage( std::string aLib, std::string aName )
{
return schEditorActive() ? schLibsSymbolUsage( aLib, aName ) : 0;
}
// Presence shims (collab-presence 0002 pcbnew / 0003 eeschema): route to the live
// editor's implementation, same pattern as the collab bridge shims above.
static void collabPresenceStart()
{
pcbEditorActive() ? pcbCollabPresenceStart() : schCollabPresenceStart();
}
static void collabSetRemote( std::string aJson )
{
pcbEditorActive() ? pcbCollabSetRemote( aJson ) : schCollabSetRemote( aJson );
}
static void collabSetPins( std::string aJson )
{
pcbEditorActive() ? pcbCollabSetPins( aJson ) : schCollabSetPins( aJson );
}
static void collabSetViewport( double aCx, double aCy )
{
pcbEditorActive() ? pcbCollabSetViewport( aCx, aCy ) : schCollabSetViewport( aCx, aCy );
}
// Follow-user (collab-presence 0008).
static void collabFitViewport( double aCx, double aCy, double aHalfW, double aHalfH )
{
pcbEditorActive() ? pcbCollabFitViewport( aCx, aCy, aHalfW, aHalfH )
: schCollabFitViewport( aCx, aCy, aHalfW, aHalfH );
}
static void collabSetStyle( std::string aJson )
{
pcbEditorActive() ? pcbCollabSetStyle( aJson ) : schCollabSetStyle( aJson );
}
static std::string collabTestListItems( int aCount )
{
return pcbEditorActive() ? pcbCollabTestListItems( aCount ) : schCollabTestListItems( aCount );
}
static std::string collabTestDemoSet()
{
return pcbEditorActive() ? pcbCollabTestDemoSet() : schCollabTestDemoSet();
}
static std::string collabGetViewport()
{
return pcbEditorActive() ? pcbCollabGetViewport() : schCollabGetViewport();
}
static std::string collabGetSelection()
{
return pcbEditorActive() ? pcbCollabGetSelection() : schCollabGetSelection();
}
static std::string collabGetSelectionFull()
{
return pcbEditorActive() ? pcbCollabGetSelectionFull() : schCollabGetSelectionFull();
}
static std::string collabTestGetCrossMapped()
{
return pcbEditorActive() ? pcbCollabTestGetCrossMapped() : schCollabTestGetCrossMapped();
}
static std::string collabTestSelectComponent()
{
return pcbEditorActive() ? pcbCollabTestSelectComponent() : schCollabTestSelectComponent();
}
static bool collabTestSelectByUuid( std::string aUuid )
{
return pcbEditorActive() ? pcbCollabTestSelectByUuid( aUuid )
: schCollabTestSelectByUuid( aUuid );
}
static void collabReleaseSelection( std::string aUuidsJson, std::string aHolder )
{
pcbEditorActive() ? pcbCollabReleaseSelection( aUuidsJson, aHolder )
: schCollabReleaseSelection( aUuidsJson, aHolder );
}
static std::string collabTestGetLocked()
{
return pcbEditorActive() ? pcbCollabTestGetLocked() : schCollabTestGetLocked();
}
static std::string collabTestSelectFirst()
{
return pcbEditorActive() ? pcbCollabTestSelectFirst() : schCollabTestSelectFirst();
}
static bool collabTestClearSelection()
{
return pcbEditorActive() ? pcbCollabTestClearSelection() : schCollabTestClearSelection();
}
EMSCRIPTEN_BINDINGS(kicad_editor) {
// Programmatic file open (preferred over UI automation from the web app).
function("kicadOpenFile", &kicadOpenFile);
// Canvas-only mobile mode (features/mobile).
function("kicadSetChrome", &kicadSetChrome);
// Read-only viewer lock (read-only-viewer).
function("kicadSetReadOnly", &kicadSetReadOnly);
// Yjs collaborative bridge entry points — same JS contract as the standalone
// bundles, dispatched on the active editor frame.
function("kicadCollabApply", &collabApply);
function("kicadCollabSnapshot", &collabSnapshot);
function("kicadCollabApplyItems", &collabApplyItems);
function("kicadCollabSnapshotItems", &collabSnapshotItems);
function("kicadCollabTestMoveFirst", &collabTestMoveFirst);
function("kicadCollabGetPos", &collabGetPos);
// ysync-review repro hooks (shared names; per-editor-only hooks — pad size,
// endpoint, field text — flow from the per-editor blocks unchanged).
function("kicadCollabTestRemoveItem", &collabTestRemoveItem);
function("kicadCollabTestRotateItem", &collabTestRotateItem);
// Collab-aware undo (ysync miss 09).
function("kicadCollabTestUndo", &collabTestUndo);
function("kicadCollabTestUndoDepth", &collabTestUndoDepth);
// Presence (collab-presence 0002/0003) + comment pins/panning (0005).
function("kicadCollabPresenceStart", &collabPresenceStart);
function("kicadCollabSetRemote", &collabSetRemote);
function("kicadCollabSetPins", &collabSetPins);
function("kicadCollabSetViewport", &collabSetViewport);
// Follow-user (collab-presence 0008).
function("kicadCollabFitViewport", &collabFitViewport);
function("kicadCollabSetStyle", &collabSetStyle);
function("kicadCollabTestListItems", &collabTestListItems);
function("kicadCollabTestDemoSet", &collabTestDemoSet);
function("kicadCollabGetViewport", &collabGetViewport);
function("kicadCollabGetSelection", &collabGetSelection);
// Cross-app selection (collab-presence 0006).
function("kicadCollabGetSelectionFull", &collabGetSelectionFull);
function("kicadCollabTestGetCrossMapped", &collabTestGetCrossMapped);
function("kicadCollabTestSelectComponent", &collabTestSelectComponent);
function("kicadCollabTestSelectByUuid", &collabTestSelectByUuid);
// Selection soft-locks (collab-presence 0007).
function("kicadCollabReleaseSelection", &collabReleaseSelection);
function("kicadCollabTestGetLocked", &collabTestGetLocked);
function("kicadCollabTestSelectFirst", &collabTestSelectFirst);
function("kicadCollabTestClearSelection", &collabTestClearSelection);
// Library reload after a remote (synced) lib edit — r2-idb-sync realtime.
function("kicadLibsReload", &pcbjam_libs::reloadLibrary);
// Placed-instance count for a library symbol (schematic sessions only —
// 0 from any other frame; drives the "symbol you are using was updated"
// toast after a remote lib edit).
function("kicadLibsSymbolUsage", &libsSymbolUsage);
}
#endif // __EMSCRIPTEN__