fix(eeschema): wasm dialog positioning (web .window CSS) + collab SCH_SHAPE add via coroutine fiber stack

Thrust A — dialogs render top-left with OK clipped in the WEB app (not the
test harness): root cause was the React shell missing the .window /
.window-canvas CSS that wx.js relies on (it positions each dialog div via
inline left/top, which need position:absolute). Added the rules to
web/apps/frontend/src/index.css. Native draw-text now works end-to-end;
symbol/power choosers render (placing still blocked by absent libraries).

Thrust B — collab apply of a newly-added SCH_SHAPE trapped in KiCad core
(SCH_COMMIT::Push CHT_ADD -> GAL view->Add, an asyncify invoke_* mis-dispatch)
because doApply ran off a fiber stack. doApply now runs inside a COROUTINE so
it executes on a libcontext fiber, the same context native draws use; the add
dispatches correctly. Re-enabled the SCH_SHAPE converter (rect/circle). Added
thirdparty/libcontext to the embind include path (tool/coroutine.h needs it).
Verified two-tab: rectangle + circle drawn in tab A sync + render in tab B.
Extended eeschema-collab.spec.ts apply test with a SCH_SHAPE add.

All changes root-repo only; kicad and wxwidgets forks untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Gergő Törcsvári 2026-06-05 11:57:10 +02:00
commit b577c05fae
No known key found for this signature in database
GPG key ID: 8E75F2CDE64E5322
6 changed files with 373 additions and 16 deletions

View file

@ -0,0 +1,104 @@
# 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 **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<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.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.
## 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 `<tool/coroutine.h>`.
- `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).

View file

@ -0,0 +1,141 @@
# Init prompt — eeschema essential ops (place symbol / power / text / rect / circle)
Paste the block below into a fresh session.
---
We're on branch `feature/yjs-bridge` (kicad-wasm, 3 repos: root + `kicad` + `wxwidgets`
submodules). The Yjs collaborative-editing bridge for eeschema mostly works, and several core
eeschema-in-wasm UI bugs were fixed. **This session's goal: make the five operations that
define an "it works" eeschema actually work — both natively in the editor AND synced over
collab:**
1. **Place symbol** (SCH_SYMBOL)
2. **Place power symbol** (SCH_SYMBOL with a power lib symbol)
3. **Draw text** (SCH_TEXT)
4. **Draw rectangle** (SCH_SHAPE)
5. **Draw circle** (SCH_SHAPE)
Collab can't be called "working" while adding shapes/symbols doesn't sync, and the editor
can't be called "working" while you can't place a symbol or finish a text dialog.
**First read these memory entries** (MEMORY.md): `yjs_bridge_feature`,
`eeschema_collab_asyncify_apply`, `wasm_eeschema_ui_modal_hotkey_fixes`,
`feedback_build_reuse_schematic_cache`, `wasm_embind_relink_gotcha`, `web_wizard_skip_config_seed`.
Also `features/yjs-bridge/0006-eeschema-apply-findings.md`.
## Current state (what works / what doesn't)
**Native editor (wasm):**
- ✅ Backspace deletes, Delete deletes (hotkey fix). Esc cancels the active tool (focus fix) —
but Esc also exits *browser* fullscreen (unpreventable browser behavior; use a maximized
window, not fullscreen).
- ✅ Quasi-modal dialogs no longer freeze the app (nested event loops now pump via Asyncify in
`wxwidgets/src/wasm/evtloop.cpp`; the text-properties dialog opens + closes).
- ◐ **Drawing rectangles/circles works natively** (user-confirmed) — but see collab below.
- ✗ **Dialogs open in the wrong position / are mis-sized so the OK button is clipped or
unclickable** (the text-properties dialog, and almost certainly the **symbol chooser** and
**power-symbol chooser**). This blocks native place-symbol / place-power / finishing draw-text.
Symptom: `winuniv.cpp(925) SetScrollbar: page size can't be greater than range` asserts; the
dialog content overflows its frame. This is the **#1 blocker for the native ops.**
**Collab apply (peer side):**
- ✅ Wire add/move, delete (any type), SCH_TEXT add, SCH_LABEL add, and **move** of
symbols/junctions/labels incl. their text fields (devirtualized `Move` + `moveFields`).
- ✗ **SCH_SHAPE add (rect/circle) does NOT sync** — reconstructing+committing a new shape traps
in **KiCad core** `SCH_COMMIT::Push` → `view->Add` of a new shape (`invoke_viii` →
"memory access out of bounds").
- ✗ **SCH_SYMBOL add does NOT sync** — the s-expr clipboard-blob reconstruction traps in the
parser `LoadContent` (`invoke_iii` → "table index is out of bounds").
## Root causes (read these — they drive the approach)
1. **Asyncify `call_indirect` mis-dispatch from the apply context.** Editor mutation code invoked
from our `CallAfter`/apply path (NOT inside a tool coroutine) mis-dispatches virtual calls
(`invoke_*`) — silently no-ops or traps. We fixed it for **our own** calls by *devirtualizing*
(explicit class-qualified calls → plain `call`, not `call_indirect`) — see
`moveItemTo`/`moveFields` in `wasm/bindings/eeschema_embind.cpp`. But shape-add and symbol-add
trap **inside KiCad core** (`view->Add`→`ViewGetLayers`; the s-expr parser), which the bridge
can't devirtualize.
- **Fix direction (the headline task): route the apply through a tool coroutine.** Native edits
don't trap because the TOOL_MANAGER runs tool actions as coroutines (the Asyncify root /
`emscripten_fiber_swap` context). Make `doApply` run there — e.g. register a custom
`TOOL_ACTION` whose handler does the SCH_COMMIT and `m_toolManager->RunAction(...)` it
(instead of `frame->CallAfter([]{ doApply… })`). This is **untested** but should fix BOTH
shape-add and symbol-add at once, and likely lets the devirtualization hacks be removed.
The dyncall shim (`scripts/common/shims/dyncall-binding.js.tmpl`) only catches the
"signature mismatch" variant — it can't catch these "out of bounds" traps (they fire inside
the wrong function), so a JS-shim fix is not viable here.
2. **Wasm dialog layout/positioning.** `DIALOG_SHIM::finishDialogSettings` calls `Fit()` +
`Centre()`, but in the wasm port the dialog ends up mis-sized/positioned (OK clipped). Likely
in `wxwidgets/src/wasm/toplevel.cpp` (window position/size, `Centre`, client-area origin —
note `GetScreenPositionOfClientOrigin` "no window" asserts) and/or the wasm dialog/sizer Fit.
Fixing this is high-leverage: it unblocks the symbol chooser, power chooser, and text dialog
all at once.
## Suggested plan
- **Thrust A — wasm dialogs usable (unblocks native place-symbol / place-power / draw-text).**
Reproduce the text-properties dialog and the symbol chooser in the real app; fix the
size/position so OK is reachable. Start in `wxwidgets/src/wasm/toplevel.cpp` +
`dialog.cpp`/`evtloop.cpp`; check how the dialog's size/position is computed vs the viewport.
- **Thrust B — collab apply via a tool coroutine (unblocks shape/symbol ADD sync).** Re-enable
the SCH_SHAPE hand converter (itemToJson already emits shape geometry: `stype`/`sx`/`sy`/
`ex`/`ey`/`cx`/`cy`/`width`/`fill`; NOTE `FILL_T::NO_FILL == 1`, not 0) and the SCH_SYMBOL
s-expr-blob converter, but run `doApply` inside a coroutine so `view->Add` and the parser stop
trapping. Verify a circle/rect/symbol added in tab A appears in tab B.
Do them in either order; A is more self-contained, B is the bigger unknown.
## Architecture / key files
- **Collab bridge (root repo, zero kicad-fork change):** `wasm/bindings/eeschema_embind.cpp` —
`doApply` (deferred via `frame->CallAfter`), `makeItem` (per-type construct; SCH_SHAPE add is
currently DEFERRED/logged, SCH_SYMBOL add too), `moveItemTo`/`moveFields` (devirtualized move),
`itemToJson`/`snapshotItems`/`COLLAB_LISTENER` (emit), `kicadCollabApply/Snapshot/GetPos/
TestMoveFirst`.
- **JS reconciler/transport (unchanged, generic):** `web/apps/frontend/src/wasm/collab/`. Collab
gated by `?collab=1` in `WasmTool.tsx`; channel = `kicad-collab:<slug>:<path>`.
- **Native tools:** `kicad/eeschema/tools/sch_drawing_tools.cpp` (createNewText → DIALOG_TEXT_
PROPERTIES::ShowQuasiModal; drawShapes → `new SCH_SHAPE(type, LAYER_NOTES, 0, fill)`;
placeSymbol). Symbol chooser dialog: `kicad/eeschema/dialogs/dialog_symbol_chooser*`.
- **Wasm dialog/event infra:** `wxwidgets/src/wasm/dialog.cpp` (ShowModal via Asyncify
`startModal`), `evtloop.cpp` (nested-loop Asyncify pump for ShowQuasiModal), `toplevel.cpp`
(TLW position/size/Centre/fullscreen), `app.cpp` (key + mouse routing, toolbar-focus fix),
`window.cpp` (ClientToScreen). `kicad/common/dialog_shim.cpp` (ShowQuasiModal, finishDialogSettings).
## Build / test / verify
- **Build (reuse the warm wxWidgets cache — a fresh per-branch container OOMs at exit 137):**
`COMPOSE_PROJECT_NAME=kicad-wasm-feature-schematic KICAD_NO_MONITOR=1 ./docker/build.sh eeschema`
(~8 min embind-only; ~12–15 min if you touch `kicad`/`wxwidgets` since it recompiles those
incrementally + relinks. Auto-relinks embind-only changes. Don't pipe output; it logs to
`logs/build/`. The host-side asyncify + `wasm-opt -O2` pass runs AFTER the docker "build
complete!" line — wait for the final ~104 MB `output/eeschema.wasm`, not the ~189 MB
pre-opt one.) Then `cd tests && npm run setup:kicad`.
- **Headless e2e:** `cd tests && npx playwright test --config=playwright-kicad.config.ts
--project=firefox kicad/eeschema-ui.spec.ts kicad/eeschema-collab.spec.ts`. NOTE: the apply
path DOES work headless (mutates the model) but the GL canvas can't paint headless (incomplete
window → "no window" assert), so dialog/visual checks need the real app. `kicadCollabSnapshot`
only exposes top-level items (not child field positions) — verify field/visual behavior with a
real-app screenshot (force a redraw first, e.g. resize the window — Playwright WebGL captures
go stale otherwise).
- **Real app:** `cd web && pnpm dev` (frontend :3048, server :3050; `pnpm db:up && pnpm
db:migrate` once). Demo project `demo` has `demo.kicad_sch` (the ECC83 push-pull, 26 symbols).
Two tabs: `http://localhost:3048/p/demo/eeschema/demo.kicad_sch?collab=1`. Devtools console
shows `[collab]` logs. (Tab navigation may pop a `beforeunload` dialog — clear with
`window.onbeforeunload = null` first.) `Module.kicadCollabApply(json)` /
`kicadCollabSnapshot()` / `kicadCollabGetPos(uuid)` are callable from the console for probing.
- **Fast-debug:** patch the built `tests/apps/kicad/eeschema.js` with `console.log` and run a
spec WITHOUT a rebuild; `tests/logs/kicad/<spec>/*.log` captures the console. Granular
`EM_ASM({ console.log(...) })` markers in the C++ to bisect a hang/trap.
## Recent commits (this branch)
root: `a00cbd9` (move carries symbol/label fields), `9fa475f` (symbol move devirtualize +
wxwidgets bump), `9b90be0` (quasi-modal no-freeze + Backspace delete), `8b26c64` (SCH_SHAPE emit
+ defer shape/symbol add). kicad: `4132395` (Backspace hotkey). wxwidgets: `ea599f7` (toolbar
focus), `c27fe8b` (quasi-modal Asyncify pump). Use `/git-feature-commit` to commit across the 3
repos. NOTE: `a00cbd9` is **unsigned** (gpg passphrase wasn't cached) — `git commit --amend -S`
it once gpg is unlocked if signed history matters.

View file

@ -405,6 +405,8 @@ if [ -f "${EMBIND_SRC}" ]; then
KICAD_INCLUDES+=" -I${KICAD_DIR}/thirdparty/nanodbc" KICAD_INCLUDES+=" -I${KICAD_DIR}/thirdparty/nanodbc"
KICAD_INCLUDES+=" -I${KICAD_DIR}/thirdparty/picosha2" KICAD_INCLUDES+=" -I${KICAD_DIR}/thirdparty/picosha2"
KICAD_INCLUDES+=" -I${KICAD_DIR}/thirdparty" KICAD_INCLUDES+=" -I${KICAD_DIR}/thirdparty"
# libcontext.h lives one level deeper; tool/coroutine.h does #include <libcontext.h>
KICAD_INCLUDES+=" -I${KICAD_DIR}/thirdparty/libcontext"
KICAD_INCLUDES+=" -I${SYSROOT}/include" KICAD_INCLUDES+=" -I${SYSROOT}/include"
# KiCad requires C++20 for concepts # KiCad requires C++20 for concepts
em++ -std=c++20 -c ${EXTRA_FLAGS} ${WX_CXXFLAGS} ${KICAD_INCLUDES} "${EMBIND_SRC}" -o "${EMBIND_OBJ}" em++ -std=c++20 -c ${EXTRA_FLAGS} ${WX_CXXFLAGS} ${KICAD_INCLUDES} "${EMBIND_SRC}" -o "${EMBIND_OBJ}"

View file

@ -112,6 +112,7 @@ test.describe("eeschema collab bridge — single page", () => {
// takes effect. (Earlier this was skipped on the belief Push no-ops headless; that predated // takes effect. (Earlier this was skipped on the belief Push no-ops headless; that predated
// the dyncall-shim fix — apply now works here. Rendering still needs the real app.) // the dyncall-shim fix — apply now works here. Rendering still needs the real app.)
const TEXT_ID = "33333333-0000-0000-0000-000000000001"; const TEXT_ID = "33333333-0000-0000-0000-000000000001";
const RECT_ID = "33333333-0000-0000-0000-000000000002";
test("apply moves/removes/adds by uuid, no echo", async ({ page, testLogger }) => { test("apply moves/removes/adds by uuid, no echo", async ({ page, testLogger }) => {
await bootAndOpen(page, "apply"); await bootAndOpen(page, "apply");
@ -161,9 +162,7 @@ test.describe("eeschema collab bridge — single page", () => {
}) })
.toBe(""); .toBe("");
// added: a graphic text reconstructs by uuid. (SCH_SHAPE / SCH_SYMBOL `added` are deferred — // added: a graphic text reconstructs by uuid.
// committing a newly-constructed shape/symbol traps via the asyncify invoke_* mis-dispatch
// in SCH_COMMIT::Push from the programmatic apply context; see features/yjs-bridge/0006.)
await page.evaluate( await page.evaluate(
(textId) => (textId) =>
window.Module.kicadCollabApply( window.Module.kicadCollabApply(
@ -182,6 +181,40 @@ test.describe("eeschema collab bridge — single page", () => {
) )
.toBe(true); .toBe(true);
// added: a SCH_SHAPE (rectangle). Committing a newly-constructed shape used to trap in
// SCH_COMMIT::Push's CHT_ADD (GAL view->Add of a new shape → asyncify invoke_viii
// mis-dispatch) when doApply ran off a fiber stack; doApply now runs inside a COROUTINE, so
// the add dispatches like a native draw. stype 1 = SHAPE_T::RECTANGLE, fill 1 = NO_FILL.
await page.evaluate(
(rectId) =>
window.Module.kicadCollabApply(
JSON.stringify({
changed: [],
removed: [],
added: [
{
id: rectId,
type: "SCH_SHAPE",
stype: 1,
sx: 700000,
sy: 700000,
ex: 900000,
ey: 850000,
width: 0,
fill: 1,
},
],
}),
),
RECT_ID,
);
await expect
.poll(
async () => (await page.evaluate(() => window.Module.kicadCollabSnapshot())).includes(RECT_ID),
{ timeout: 10000, intervals: [250] },
)
.toBe(true);
const echoes = await page.evaluate(() => (window as unknown as { __echo: string[] }).__echo); const echoes = await page.evaluate(() => (window as unknown as { __echo: string[] }).__echo);
expect(echoes, "apply() must not echo a local onDelta").toHaveLength(0); expect(echoes, "apply() must not echo a local onDelta").toHaveLength(0);
expect(hasAbort(testLogger), "no WASM abort").toBe(false); expect(hasAbort(testLogger), "no WASM abort").toBe(false);

View file

@ -37,6 +37,7 @@
#include <sch_screen.h> #include <sch_screen.h>
#include <sch_sheet_path.h> #include <sch_sheet_path.h>
#include <schematic_settings.h> #include <schematic_settings.h>
#include <tool/coroutine.h>
using namespace emscripten; using namespace emscripten;
using json = nlohmann::json; using json = nlohmann::json;
@ -202,17 +203,46 @@ SCH_ITEM* makeItem( const json& j )
item = lbl; item = lbl;
} }
// SCH_SHAPE `added` reconstruction is DEFERRED (returns nullptr → logged "no converter"). else if( type == "SCH_SHAPE" )
// A constructed shape builds fine, but committing a *new* one traps in SCH_COMMIT::Push's {
// CHT_ADD path (the GAL view->Add of a new SCH_SHAPE) with a wasm "memory access out of // Reconstruct from the geometry itemToJson emits. Committing a *new* SCH_SHAPE used to
// bounds". The identical view->Add succeeds when the shape is loaded from file and when an // trap in SCH_COMMIT::Push's CHT_ADD path (GAL view->Add → an asyncify invoke_viii
// existing shape is MOVED (CHT_MODIFY) — so this is the asyncify indirect-call (invoke_viii) // mis-dispatch, "memory access out of bounds") because doApply ran off a fiber stack;
// mis-dispatch class: a SCH_SHAPE add-path virtual that mis-dispatches only from the // doApply now runs inside a COROUTINE (kicadCollabApply) so the add dispatches correctly,
// programmatic CallAfter/apply context (same family as the original SCH_ITEM::Move trap, // exactly as a native draw does. (0006/0007.) NB FILL_T::NO_FILL == 1, not 0.
// 0003). The dyncall shim only catches the "signature mismatch" variant, and this trap SHAPE_T st = (SHAPE_T) j.value( "stype", (int) SHAPE_T::RECTANGLE );
// fires inside the wrong function so it can't be safely retried. itemToJson still emits the int layer = j.value( "layer", (int) LAYER_NOTES );
// shape geometry (forward-compatible + drives `changed`/move); only `added` is skipped. int width = j.value( "width", 0 );
// Same blocker as SCH_SYMBOL add — see features/yjs-bridge/0006. FILL_T fill = (FILL_T) j.value( "fill", (int) FILL_T::NO_FILL );
auto* shp = new SCH_SHAPE( st, (SCH_LAYER_ID) layer, width, fill );
// Rectangle: two corners. Circle: start = center, end = a point on the radius. Both are
// fully defined by start+end (what itemToJson emits via GetStart()/GetEnd()).
shp->SetStart( VECTOR2I( j.value( "sx", 0 ), j.value( "sy", 0 ) ) );
shp->SetEnd( VECTOR2I( j.value( "ex", 0 ), j.value( "ey", 0 ) ) );
if( st == SHAPE_T::ARC && j.contains( "cx" ) )
{
shp->SetCenterX( j["cx"].get<int>() );
shp->SetCenterY( j["cy"].get<int>() );
}
else if( st == SHAPE_T::BEZIER )
{
if( j.contains( "c1x" ) )
shp->SetBezierC1( VECTOR2I( j["c1x"].get<int>(), j["c1y"].get<int>() ) );
if( j.contains( "c2x" ) )
shp->SetBezierC2( VECTOR2I( j["c2x"].get<int>(), j["c2y"].get<int>() ) );
}
if( SCH_EDIT_FRAME* fr = schFrame() )
shp->SetParent( &fr->Schematic() );
item = shp;
}
// SCH_SYMBOL `added` is still deferred (needs the s-expr clipboard-blob emit + LoadContent
// reconstruction; symbol PLACEMENT is also natively blocked until symbol libraries are
// bundled — see features/yjs-bridge tasks). Symbol move/position already syncs via `changed`.
if( item ) if( item )
const_cast<KIID&>( item->m_Uuid ) = KIID( wxString::FromUTF8( j.value( "id", "" ).c_str() ) ); const_cast<KIID&>( item->m_Uuid ) = KIID( wxString::FromUTF8( j.value( "id", "" ).c_str() ) );
@ -493,8 +523,22 @@ void kicadCollabApply( std::string aJson )
if( !fr ) if( !fr )
return; return;
// Defer to the editor's main-loop context so SCH_COMMIT runs like a normal edit. // Defer to the editor's main-loop context so SCH_COMMIT runs like a normal edit, AND run the
fr->CallAfter( [fr, delta]() { doApply( fr, delta ); } ); // mutation inside a COROUTINE so it executes on a libcontext fiber stack — the exact context
// KiCad tool actions (native draws/edits) run in. SCH_COMMIT::Push's CHT_ADD of a *new*
// SCH_SHAPE/SCH_SYMBOL dispatches GAL virtuals (view->Add → ViewGetLayers) through asyncify-
// instrumented invoke_*; off the fiber stack those mis-dispatch and trap inside KiCad core
// ("memory access out of bounds" / "table index out of bounds"), which the bridge can't
// devirtualize. On the fiber stack they dispatch correctly. CallAfter runs on the app main
// stack (ProcessPendingEvents), which is where COROUTINE::Call must be invoked from. (0007.)
fr->CallAfter( [fr, delta]() {
COROUTINE<int, int> cor( [fr, delta]( int ) -> int
{
doApply( fr, delta );
return 0;
} );
cor.Call( 0 );
} );
} }

View file

@ -52,3 +52,36 @@
margin: 0; margin: 0;
} }
} }
/*
* wxWidgets WASM child-window styling.
*
* The wx.js runtime glue (createWindow/setWindowRect) creates a `.window`
* <div> (+ `.window-canvas`) per top-level window — every KiCad dialog
* (text properties, symbol chooser, power chooser, …) is one of these,
* appended to #window-container. setWindowRect positions it via inline
* left/top, which only take effect when the element is `position: absolute`.
*
* The standalone test harness HTML ships these rules; the React shell
* (WasmTool.tsx) did not — so dialogs computed `position: static`, ignored
* their left/top, and rendered jammed in the top-left corner with the OK
* button clipped off-screen. These rules (mirrored from the test harness)
* restore correct centered positioning. Input is routed through the main
* #canvas, so the window layers are pointer-events: none.
*/
.window {
position: absolute;
pointer-events: none;
z-index: 10;
background-color: black;
overflow: hidden;
width: 0;
height: 0;
}
.window-canvas {
position: absolute;
top: 0;
left: 0;
pointer-events: none;
}