A single SCH_COMMIT::Push fires OnItemsAdded/Removed/Changed separately and
synchronously, then RecalculateConnections once. COLLAB_LISTENER emitted each
category as its own delta, so the peer applied one atomic edit as three separate
commits, each with its own connectivity recompute. On a large connected drag the
junction at the wires' new crossing (added) was applied before the wires moved
(changed) -> dangling junction -> the peer's cleanup deleted it (lost segments).
COLLAB_LISTENER now buffers added/changed/removed (serialized in each synchronous
callback) and flushes one combined delta after Push returns, coalesced via
CallAfter. doApply applies it atomically removed->changed->added in a single
commit with one recompute, so the junction survives. Verified two-tab: a G-drag
of U1A that previously left the peer at 74 items / 7 junctions now emits one
delta {a:1,c:4,r:1} and both tabs converge at 75 / 8.
Root-repo only; kicad and wxwidgets untouched.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
7.8 KiB
eeschema essential ops — findings (2026-06-05)
Work on the five "it works" eeschema operations (place symbol / place power / draw text /
draw rectangle / draw circle), both natively and over collab. Ground truth gathered by
driving the real web app (localhost:3048, ?collab=1, two tabs) with Playwright and
by the headless e2e specs. All changes are root-repo only — the kicad and wxwidgets
submodules are untouched.
Result summary
| Op | Native editor | Collab sync |
|---|---|---|
| Draw text | ✅ works (dialog → OK → place) | ✅ (already worked) |
| Draw rectangle | ✅ works | ✅ fixed this session |
| Draw circle | ✅ works | ✅ fixed this session |
| Place symbol | ◐ dialog works, but no libraries (can't pick) | symbol-add deferred |
| Place power symbol | ◐ same — no libraries | symbol-add deferred |
3 of 5 fully work end-to-end. The 2 symbol ops are blocked by a separate, newly-found issue (no symbol libraries bundled), not by dialogs or asyncify anymore.
Thrust A — wasm dialogs (the #1 stated blocker): root cause was NOT what the prompt assumed
The prompt hypothesized a wxwidgets C++ dialog sizing/finishDialogSettings bug. Live
reproduction proved that wrong. The wx registry reported the text-properties dialog at the
correct centered position ((470,308) 660×384 in a 1600×1000 screen), but a screenshot
showed it jammed in the top-left with OK clipped. The mismatch is in the DOM/CSS layer:
wx.js(createWindow/setWindowRect) makes one.window<div>(+.window-canvas) per top-level window and positions it via inlinestyle.left/top, which requireposition: absolute.- The standalone test-harness HTML (
tests/apps/kicad/eeschema.html) ships.window { position: absolute }; the React web shell (WasmTool.tsx/src/index.css) never did → dialogs computedposition: static, ignored their left/top, and stacked at (0,0). This is exactly why the e2e specs (harness HTML) never caught it.
Fix: added .window / .window-canvas rules to web/apps/frontend/src/index.css
(mirrored from the harness; layers are pointer-events: none — input routes through the
main #canvas). Frontend-only, no wasm rebuild. Verified live: text-properties dialog
and symbol chooser now center with OK reachable; full native draw-text works
(dialog→OK→place→SCH_TEXT "HELLO").
New blocker found: no symbol libraries
After the dialog fix, the Choose Symbol / power chooser opens correctly but shows
"0 items loaded" — the wizard-skip config seeds an empty sym-lib-table and there
are no .kicad_sym standard libraries in the wasm MEMFS (/usr/share/kicad/symbols
absent; the repo doesn't vendor kicad-symbols, only kicad/demos/* and a fixture
kicad/qa/data/libraries/power.kicad_sym). So place-symbol/power can't pick a symbol.
Bundling libraries (provisioning in web/apps/frontend/src/wasm/boot.ts) is the follow-up;
draw text/rect/circle don't need it.
Thrust B — collab apply of SCH_SHAPE/SCH_SYMBOL add via a tool coroutine (the headline fix)
Confirmed the documented root cause: committing a newly-constructed SCH_SHAPE traps in
KiCad core (SCH_COMMIT::Push CHT_ADD → GAL view->Add → an asyncify invoke_viii
mis-dispatch, "memory access out of bounds") only from the programmatic CallAfter/apply
context — never from a native draw, because native draws run inside a tool coroutine
(a libcontext fiber stack). The dyncall shim can't catch it (it traps inside the wrong
function, not at the call boundary), and the bridge can't devirtualize core.
Fix (wasm/bindings/eeschema_embind.cpp, root repo only): run doApply inside a
COROUTINE fiber stack. kicadCollabApply's CallAfter lambda now constructs a
COROUTINE<int,int> whose body is doApply(...) and calls cor.Call(0). CallAfter runs
on the app main stack (ProcessPendingEvents), which is exactly where COROUTINE::Call
must be invoked from. On the fiber stack the GAL add-path virtuals dispatch correctly, like
a native draw. Re-enabled the SCH_SHAPE hand converter in makeItem (reconstructs from
the geometry itemToJson already emits; NB FILL_T::NO_FILL == 1). Kept the
devirtualization hacks (moveItemTo/moveFields) — harmless and proven.
Build gotcha: tool/coroutine.h does #include <libcontext.h>, which the embind
compile's include path lacked → added -I${KICAD_DIR}/thirdparty/libcontext to
scripts/kicad/build-kicad-target.sh.
Verified:
- Real app, two tabs: a rectangle (stype 1) AND a circle (stype 3) drawn in tab A sync to tab B with identical uuid + geometry and render (blue LAYER_NOTES shapes), no crash, 0 console errors.
- Headless e2e: extended
tests/kicad/eeschema-collab.spec.tsapply test with a SCH_SHAPE rectangle add — passes, no abort. Full eeschema-collab + eeschema-ui suites green (5 passed, 1 two-tab skipped as designed).
SCH_SYMBOL add — still deferred, but now unblocked
The coroutine fixes the context; symbol-add additionally needs the emit side to
produce the s-expr clipboard blob (SCH_IO_KICAD_SEXPR::Format →
LoadContent reconstruct — the path sch_editor_control.cpp doCopy/paste uses). It's moot
until symbol libraries are bundled (you can't place a symbol natively to sync). Symbol
move/position already syncs via the generic changed path.
Thrust C — collab wire/symbol-drag divergence (lost segments) — fixed
User-reported follow-up: dragging a wire OR symbol with multiple connections a large distance (small drags don't reroute) lost segments on the peer. Reproduced + root-caused in the real app, two tabs.
Root cause: one atomic edit (a single SCH_COMMIT::Push) calls OnItemsAdded,
OnItemsRemoved, OnItemsChanged separately and synchronously, then
RecalculateConnections once. The old COLLAB_LISTENER emitted each category as its own
delta, so the peer applied them as THREE separate commits, each with its own connectivity
recompute. A G-drag of U1A emitted {added:[junction]}, {removed:[wire]},
{changed:[3 wires, symbol]}; the junction (at the wires' new crossing) was applied
before the wires moved → a dangling junction on the peer → connectivity cleanup deleted
it. Result: tab A 75 items / 8 junctions, tab B 74 / 7 (the junction lost). Simple
translates (M tool) always converged — they touch only changed.
Fix (wasm/bindings/eeschema_embind.cpp): COLLAB_LISTENER now buffers the three
categories (serializing items in each synchronous callback) and flushes one combined
delta after Push returns, coalesced via CallAfter. The peer's doApply applies a
combined delta removed→changed→added in a single SCH_COMMIT with one recompute, so the
junction is added after its wires are in place and survives. Verified: the same G-drag
now emits 1 delta {a:1,c:4,r:1} and both tabs converge identically (75 items, 8 junctions,
zero wire/junction diff). Embind-only build; eeschema-collab + eeschema-ui suites green.
Files touched (all root repo)
web/apps/frontend/src/index.css—.window/.window-canvasCSS (Thrust A).wasm/bindings/eeschema_embind.cpp— COROUTINE-wrappeddoApply; re-enabled SCH_SHAPE converter; include<tool/coroutine.h>.scripts/kicad/build-kicad-target.sh— addedthirdparty/libcontextto the embind include path.tests/kicad/eeschema-collab.spec.ts— added a SCH_SHAPE rectangle apply case.
Suggested next steps
- Bundle symbol libraries (
power.kicad_symat minimum) into the wasm FS +sym-lib-tableso place-symbol/power work — the last gap for the 5 ops. - SCH_SYMBOL collab-add: add the s-expr blob to
itemToJsonand aLoadContentreconstruct inmakeItem(now that the coroutine context is in place). Do after (1).