# 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` `
` (+ `.window-canvas`) per top-level window and positions it via **inline `style.left/top`**, which require `position: 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 computed `position: 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` 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 `, 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.ts` apply 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`. **First fix (batched emit) was insufficient.** Combining the three callbacks into one delta fixed the junction-add case, but the user could still break it: a *large* connected drag made the peer lose the P3↔C1 wire. **Deeper root cause:** the SCHEMATIC_LISTENER fires in `pushSchEdit` *before* `RecalculateConnections` (sch_commit.cpp ~402 vs ~430), so the emit was always **pre-cleanup raw geometry**; the connectivity cleanup that follows (merge collinear wires, drop/split junctions) was never broadcast. The peer reconstructed the raw edit and ran ITS OWN cleanup over a different "dirty" scope → the two peers cleaned up differently and the peer lost segments. **Final fix (`wasm/bindings/eeschema_embind.cpp`): emit a post-settle snapshot diff.** The native listener is now just a "something changed" trigger; the actual change set is a DIFF of the full model taken after the edit *settles* — a `CallAfter` flush, which runs once Push (cleanup included) returns — so it captures tab A's FINAL, already-clean geometry. The peer applies that and re-cleaning already-clean geometry is idempotent, so the two converge. (Mirrors pl_editor's snapshot-differ.) `g_baseline` holds the last-broadcast state; `doApply` and `kicadCollabSnapshot` rebaseline so applied/seed items aren't re-broadcast (echo). No kicad-fork change. **Verified two-tab, rigorously** (real edit: `tabA` state changed AND `tabA===tabB` byte-for-byte): a wire reroute, plus U1A/U1B/C2 symbol drags, all converge exactly; eeschema-collab + eeschema-ui suites green. Embind-only build. ## Files touched (all root repo) - `web/apps/frontend/src/index.css` — `.window` / `.window-canvas` CSS (Thrust A). - `wasm/bindings/eeschema_embind.cpp` — COROUTINE-wrapped `doApply`; re-enabled SCH_SHAPE converter; include ``. - `scripts/kicad/build-kicad-target.sh` — added `thirdparty/libcontext` to the embind include path. - `tests/kicad/eeschema-collab.spec.ts` — added a SCH_SHAPE rectangle apply case. ## Suggested next steps 1. **Bundle symbol libraries** (`power.kicad_sym` at minimum) into the wasm FS + `sym-lib-table` so place-symbol/power work — the last gap for the 5 ops. 2. **SCH_SYMBOL collab-add**: add the s-expr blob to `itemToJson` and a `LoadContent` reconstruct in `makeItem` (now that the coroutine context is in place). Do after (1).