Add kicadCollabSnapshotItems / kicadCollabApplyItems / window.kicadCollab.onItems
to pl_editor, eeschema, pcbnew — per-item native-blob payloads
({added/changed:[{sexpr,parent}], removed:[uuid]}) alongside the untouched scalar
wire (legacy collab specs stay green).
- pl_editor: itemBlob per item; apply = SetPageLayout(append) + replace-by-uuid
(pointer-snapshot safe); bare payloads get the kicad_wks envelope. Snapshot +
apply + emit verified headless.
- eeschema: clipboard Format per item (symbols carry their lib_symbols); apply
mirrors the native paste — LoadContent into a throwaway sheet → detach →
replace-by-uuid → symbol lib relink (blob's lib_symbols first, live screen's
second) → SCH_COMMIT, in the CallAfter+COROUTINE context. Snapshot + apply
verified headless (a "lost" lone junction turned out to be correct connection
cleanup — test uses text).
- pcbnew: blobForItem per ROOT item with child→footprint lifting in flushDiff;
apply = makeFromBlob + commit replace-by-uuid on the fiber; bare non-footprint
payloads get wrapInBoardEnvelope (live board layer table). Snapshot + footprint
replace/add WITH children (the 0004 containment gap, closed) + removal verified
headless. Track/via/zone/text blob-apply hits the documented asyncify-fragile
envelope parse (reconfirmed empirically — a verbatim SaveSelection segment
envelope dies silently in the commit) and stays on the legacy scalar apply;
tracked in ysync 0008 status.
- eeschema/pcbnew scheduleFlush now runs flushDiff inside a COROUTINE: the
per-item Format in the v2 emit needs the fiber stack (0007 lesson). Their emit
remains unverifiable headless (both legacy two-tab tests are test.skip:
"open=false → SCH_COMMIT no-ops" / "harness can't PAINT") — verify in the real
app at Stage D; pl_editor's emit IS verified.
- tests/kicad/items-bridge.spec.ts: per-tool suite (snapshot uuids → local-edit
emit (where drivable) → apply changed/added/removed via save-readback → no
apply echo). 3/3 pass; roundtrip + collab suites unaffected.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Block A save embind exports + the round-trip integration harness.
Save exports (wasm/bindings, GPL):
- pcbnew: kicadSaveBoard(path) via PCB_IO_KICAD_SEXPR::SaveBoard(GetBoard()).
- eeschema: kicadSaveSchematic(path) via SCH_IO_KICAD_SEXPR::SaveSchematicFile.
Saves GetCurrentSheet().Last() (NOT Schematic().Root()): the wasm open-flow
nests the opened doc under an auto-created project root, so Root()'s screen
holds only a child-sheet symbol, not the loaded items.
(pl_editor already had kicadSaveDrawingSheet.)
Round-trip harness (tests/kicad/roundtrip.spec.ts):
- load fixture → save (ORIG) → kicadCollabSnapshot → reload (fresh wasm) →
open empty → kicadCollabApply → save (REGEN) → assert sexprDiff(ORIG,REGEN).equal.
- Two separate pages (extract closed before rebuild opens) so the process-global
wasm heap frees between boots (pcbnew ~190MB). Open both sides from the same
filename (eeschema embeds it as the root Sheetfile property). pcbnew boots the
seeded pcbnew-collab.html (plain pcbnew.html's first-run wizard blocks boot).
- pl_editor + eeschema round trips PASS (lossless). pcbnew is test.fixme with
tracked apply-coverage findings (footprints/zones not reconstructed; segment
width + via size lost; fp_text→gr_text) — bridge gaps for follow-up, not test bugs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The batched-emit fix still lost segments on a large connected drag (peer
dropped the P3-C1 wire). Deeper cause: the SCHEMATIC_LISTENER fires in
pushSchEdit BEFORE RecalculateConnections (sch_commit.cpp ~402 vs ~430), so
every emit was pre-cleanup RAW geometry; the cleanup that follows (merge
collinear wires, drop/split junctions) was never broadcast. The peer rebuilt
the raw edit and ran its own cleanup over a different dirty scope, so the two
peers cleaned up differently and the peer lost segments.
Replace the listener-list emit with a post-settle full-model snapshot DIFF
(snapshotByUuid), flushed via CallAfter once Push (cleanup included) returns —
capturing tab A's final, already-clean geometry. The peer applies that and
re-cleaning already-clean geometry is idempotent, so they converge. The native
listener is now just a change trigger. g_baseline holds the last-broadcast
state; doApply and kicadCollabSnapshot rebaseline so applied/seed items aren't
re-broadcast (echo). Mirrors pl_editor's snapshot-differ; 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.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
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>
After the body-move devirtualization, moving a symbol synced the body to the peer but
left its reference/value text behind: SCH_SYMBOL::Move()/SCH_LABEL_BASE::Move() move the
child fields via an inner virtual field.Move() that also mis-dispatches in the apply
context. Move the fields explicitly with a devirtualized SCH_FIELD::Move (moveFields).
Verified: a moved symbol's text label now follows the body on the peer.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The changed-path used the virtual SCH_ITEM::Move(), which silently no-ops from the
apply/CallAfter context (asyncify call_indirect mis-dispatch) for every non-wire item —
so moving a symbol synced on the sender but not the peer. Devirtualize Move() with an
explicit class-qualified call (moveItemTo), which is statically bound (a plain call, not
call_indirect) and executes. Verified: a symbol move now propagates. Also bumps wxwidgets
to ea599f7 (toolbar clicks no longer steal canvas keyboard focus).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extend doApply added-item construction beyond wires: SCH_TEXT, SCH_LABEL /
SCH_GLOBALLABEL / SCH_HIERLABEL (position + text + label shape), and SCH_NO_CONNECT.
Serialize text (any EDA_TEXT) and label shape in itemToJson. Moving/deleting existing
items of any type already worked (generic changed->Move and removed->Remove); this adds
their reconstruction on add.
Still uncovered: SCH_SYMBOL (needs lib-symbol + fields/orientation) and graphic shapes
(SCH_SHAPE). Known issues to fix next: adding text freezes the app; circles (SCH_SHAPE)
don't sync; deletes don't apply; wire moves only partially converge.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two things, both verified in the real web app (two-tab eeschema collab).
1. dynCall crash fix (all apps) — scripts/common/shims/dyncall-binding.js.tmpl.
Programmatic editor edits trapped with 'indirect call signature mismatch': the
asyncify-instrumented wasmExports[dynCall_<sig>] trampoline does call_indirect with a
stale type for some table indices (post-asyncify+O2) even though the table entry is
valid. Proven by patching the built js: at the trap getWasmTableEntry(index) SUCCEEDS
where the trampoline fails. Fix: the shim now catches the 'signature mismatch'
RuntimeError and falls back to getWasmTableEntry; the Asyncify unwind sentinel and real
exceptions re-throw, so instrumentation/unwind is untouched for normal calls. This
unblocks ALL programmatic edits, not just collab (e.g. eeschema SCH_ITEM::Move).
2. eeschema collab apply converters (wasm/bindings/eeschema_embind.cpp).
doApply now handles added-item construction (build the SCH_ITEM with the delta's uuid
via const_cast — as the s-expr parser does — + commit.Add) and richer SCH_LINE
serialization (start/end/layer) so wire edits reconstruct on the peer. Implemented for
SCH_LINE (wires) + SCH_JUNCTION; other types log 'no converter for added type' and are
skipped (next batch). eeschema re-enabled in the web app collab gate.
Tests: eeschema-collab.spec snapshot (green); apply/two-tab skipped — they no-op headless
because the e2e harness's kicadOpenFile returns false (OpenProjectFiles bails before
building the connectivity graph), so SCH_COMMIT::Push doesn't persist. Verified in-app.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
eeschema's half of the Yjs collaborative bridge, reusing the generic reconciler /
BroadcastChannel transport unchanged. Zero kicad-fork change: native SCH_ITEM uuid +
native SCHEMATIC_LISTENER. All in the wasm layer (wasm/bindings/eeschema_embind.cpp).
Working (verified in the web app):
- kicadCollabSnapshot(): enumerate sch.Hierarchy() -> LastScreen()->Items() as
{id,type,x,y}; registers the listener on first call
- emit: SCHEMATIC_LISTENER subclass -> per-item delta via window.kicadCollab.onDelta;
fires on real SCH_COMMIT::Push (a real wire move broadcasts added/removed/changed)
Apply is a documented follow-up (gated off so a peer tab can't crash): SCH_ITEM::Move
traps with 'indirect call signature mismatch' when invoked outside a KiCad tool
coroutine (Asyncify+fiber+exception-trampoline). Modify/Clone/GetPosition all work;
only the virtual Move write traps. Fix direction: route apply through TOOL_MANAGER.
Also: build-kicad-target.sh now force-relinks when only <app>_embind.cpp changed (the
embind .o isn't a make dep, so new bindings silently vanished), and adds the
expected/rtree/fmt thirdparty includes the eeschema bindings need.
Tests: eeschema-collab.spec.ts covers snapshot (green); apply/two-tab skipped with the
blocker noted. WasmTool gates collab to pl_editor only until eeschema apply works.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>