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:
parent
6bd17aeb26
commit
b577c05fae
6 changed files with 373 additions and 16 deletions
104
features/yjs-bridge/0007-eeschema-essential-ops-findings.md
Normal file
104
features/yjs-bridge/0007-eeschema-essential-ops-findings.md
Normal 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).
|
||||||
141
features/yjs-bridge/0007-eeschema-essential-ops-prompt.md
Normal file
141
features/yjs-bridge/0007-eeschema-essential-ops-prompt.md
Normal 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.
|
||||||
|
|
@ -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}"
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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 );
|
||||||
|
} );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue