The footprint and symbol editors are no longer separate WASM bundles: the frontend loads the parent pcbnew/eeschema bundle and passes --frame=fpedit / --frame=symedit (TOOL_BUNDLE + TOOL_FRAME -> Module.arguments in boot). Drops the two duplicate build+deploy targets and their wrapper scripts + vestigial embind; adds low-level harnesses (footprint_editor.html, symbol_editor.html) and a runtime-frame spec. Bumps the kicad submodule to the runtime --frame launcher. The frame-runtime spec is listed in PCBNEW_FAMILY_SPECS so CI routes it to the chromium-ci (V8) project — its footprint case boots the pcbnew module, which OOMs SpiderMonkey on x86 CI. Includes the editor-unification dossier (research docs 01-04 + the as-built implementation record 05). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
8.7 KiB
04 — Part 2: fuse the editors into one app
Verdict: feasible and well-bounded, but moderate work — and a product decision, not a dedup win. Merging "the four editors" means linking the two engines (pcbnew kiface + eeschema kiface) into one WASM image and picking the view at runtime. The collision surface is small and concentrated; the real cost is bundle size. Prereqs:
02-kiface-architecture.md,03.
What "the four" actually is
Board + Footprint + Schematic + Symbol = FACE_PCB (pcbnew kiface) + FACE_SCH (eeschema
kiface) = two engines in one static image. Extending to all 7 GUI tools adds FACE_GERBVIEW,
FACE_PL_EDITOR, FACE_PCB_CALCULATOR = 5 kifaces — same mechanism, more faces.
This is fundamentally different from Part 1. Part 1 is "one engine, many frames" — already
supported, you just expose the selector. Part 2 is "many engines, one image," which has no
existing template: upstream gets multi-kiface only by loading DLLs at runtime, the WASM build
has no dlopen, the kicad project manager isn't built for WASM (kicad/CMakeLists.txt:113),
and the WASM KIWAY explicitly assumes exactly one kiface (kiway.cpp:223-258). So Part 2 is a
real porting task, not a flag flip.
The symbol-collision surface (when two kifaces link into one image)
From most to least trouble:
1. Kiface() — the load-bearing item (moderate)
Each kiface defines a global accessor KIFACE_BASE& Kiface() (declared
include/kiface_base.h:134; defined pcbnew.cpp:600, eeschema.cpp:452,
gerbview.cpp:152, pl_editor.cpp:141, pcb_calculator.cpp:76). Linking two kifaces gives two
definitions of Kiface() — a duplicate symbol.
Worse, it is not just a name clash; it is a semantic one. Kiface() is called ~97 times
(pcbnew 48 calls / 21 files; eeschema 49 / 20 files), including 5 files in shared common/
that are compiled once into libkicommon and linked into every app:
common/eda_base_frame.cpp(e.g.:1274 return Kiface().KifaceSettings();,:1280,:1286)common/gestfich.cppcommon/design_block_tree_model_adapter.cpp:49common/dialogs/dialog_color_picker.cppcommon/tool/common_control.cpp
That shared code calls Kiface() expecting "the one active editor's kiface" — but with two
kifaces linked, it can't statically know whether the active frame is PCB or schematic. So
Kiface() must become a runtime dispatch on the active editor (a "current active kiface"
pointer keyed off the active frame's FACE_T). It's mostly process/editor-level config —
KifaceSettings(), KifaceSearch(), GetHelpFileName(), IsSingle() — so a single dispatching
Kiface() backed by an active-editor pointer is feasible, but it is a behavioral change touching
~100 call sites including shared code. This is what lifts Part 2 from "small" to "moderate."
⚠️
--allow-multiple-definitionis not a fix here. The static kiface link already passesLINKER:--allow-multiple-definition(see../symbol-editor/0001-symbol-editor-port.md). That would let twoKiface()definitions link — by silently taking the first — which means sharedcommon/code would always resolve to (say) pcbnew's kiface even while the schematic editor is active. That is the bug, not the fix.Kiface()needs real per-active-editor dispatch.
2. KIFACE_GETTER / KIFACE_1 (trivial)
Every kiface exports the same extern "C" getter — KIFACE_GETTER is a macro expanding to the
unmangled symbol KIFACE_1 (include/kiway.h:110-115), defined identically in each kiface
(pcbnew.cpp:605, eeschema.cpp:457, …), all returning &kiface. Two in one image = duplicate
KIFACE_1.
Fix is mechanical: the macro is unconditional at kiway.h:111, so guard it with #ifndef KIFACE_GETTER and pass a per-target -DKIFACE_GETTER=<distinct> (the same COMPILE_DEFINITIONS
machinery that already sets TOP_FRAME). The extern "C" declaration at kiway.h:541/543
follows the macro automatically. A combined launcher then declares both distinct externs and
registers each face:
extern "C" KIFACE* pcbnew_kiface_getter(int*,int,PGM_BASE*);
extern "C" KIFACE* eeschema_kiface_getter(int*,int,PGM_BASE*);
Kiway.set_kiface( KIWAY::FACE_PCB, pcbnew_kiface_getter(&v, KIFACE_VERSION, this) );
Kiway.set_kiface( KIWAY::FACE_SCH, eeschema_kiface_getter(&v, KIFACE_VERSION, this) );
3. Relax the WASM "exactly one kiface" convention (small)
kiway.cpp:223-258 enforces single-kiface registration by convention, not storage —
set_kiface already writes into a per-FACE_T array (include/kiway.h:476-481) and KiFACE()
reads per slot. Registering several faces is just calling set_kiface N times. Update the comment
and the single-getter assumption to match the combined launcher above.
Non-issues (verified)
Pgm()/programsingleton (common/pgm_base.cpp:1051-1058;single_top.cpp:137,180) — there is exactly one shared instance, which is correct for a multi-kiface app. No collision.- The
static <X>::IFACE kiface(...)instances (pcbnew.cpp:592,eeschema.cpp:445, in named namespacesPCB/SCH) — file-static, internal linkage, no external symbol. No collision. - Beyond
KIFACE_1andKiface(), the kifaces emit no other global non-member symbols at file scope; the lower libs (libkicommon, libgal, …) are linked once and shared. A one-shot trial link is the definitive duplicate-symbol audit, but the by-design collisions are just those two.
The real cost: bundle size
This is the strategic consideration, and it cuts against our current delivery model.
- Today: 7 separate bundles, each user lazy-loads only the tool they open. Total deployed ≈ 572 MB raw — but massively duplicated (wx + common + boost replicated in all 7).
- Merged: a combined image pays for shared code once. A 4-editor (pcbnew + eeschema) binary ≈ pcbnew's 146 MB (already carries wx/common + the PCB-only OCC) + eeschema's unique code (~30–40 MB; OCC is PCB-only, wx/common/boost are shared) ≈ ~180–190 MB. All-7 ≈ ~200–230 MB. (Estimates — a trial link is the only way to know precisely.)
- The catch: that ~190 MB is downloaded by every user — even one who only wants the
calculator. It works against the per-tool lazy loading we do now, and a merged binary can't
be lazily sub-loaded:
wasm-split/dlopenare already assessed as a dead end for us (see../perf/bundle-size.mdand the bundle-composition research). So the tradeoff is "one shared binary, zero duplication, cross-probing possible" vs. "larger first load for single-tool users."
When Part 2 is worth it
- Worth it if the product goal is a unified, project-based "KiCad in the browser" where a
user has a project open and flips between schematic and PCB — and you want desktop-style
cross-probing (select a component in the schematic, highlight it on the board). A combined
image holds the board editor and schematic editor live under one
KIWAY, which is exactly how desktop KiCad's single process works; the inter-editorKIWAYexpress messaging would light up for free. This is essentially porting the upstream project-manager model to one static image. - Not worth it if the goal is the fastest possible first load for a single tool. Then keep the tools as separate lazy-loaded bundles and just take Part 1's free dedup.
Suggested sequencing if pursued
- Ship Part 1 first (free; builds the runtime-frame launcher this reuses).
- Spike a 2-kiface trial link (pcbnew + eeschema) with the getter renamed (§2) and
--allow-multiple-definitionoff, to enumerate the true duplicate-symbol set and confirm it's justKiface(). Measure the combined size. - Convert
Kiface()to active-editor dispatch (§1) — the bulk of the work; validate the sharedcommon/call sites resolve correctly with both editors live. - Combined launcher registers both faces (§2/§3); JS opens any
FRAME_Tagainst the one bundle (and optionally two frames at once for cross-probing). - Only then consider folding in gerbview / pl_editor / calculator (3 more faces, same recipe).
Bottom line
The engineering is bounded and the scary part (the getter) is trivial; the genuine work is one
well-understood refactor (Kiface() → runtime dispatch). But Part 2 trades per-tool download size
for a unified binary, so decide it on product grounds — do we want a single cross-probing
KiCad-in-the-browser? — not as a duplication cleanup. The duplication cleanup is
Part 1, and it's free.