2026-06-01 16:30:29 +02:00
|
|
|
/*
|
|
|
|
|
* Embind bindings for KiCad eeschema WASM.
|
|
|
|
|
*
|
|
|
|
|
* Picked up automatically by scripts/kicad/build-kicad-target.sh when building
|
|
|
|
|
* the eeschema app (it compiles wasm/bindings/<app>_embind.cpp if present).
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
#ifdef __EMSCRIPTEN__
|
2026-06-03 17:49:11 +02:00
|
|
|
#include <emscripten.h>
|
2026-06-01 16:30:29 +02:00
|
|
|
#include <emscripten/bind.h>
|
|
|
|
|
#include <kiway_player.h>
|
|
|
|
|
#include <kiway.h>
|
2026-06-05 13:30:47 +02:00
|
|
|
#include <map>
|
2026-06-03 17:49:11 +02:00
|
|
|
#include <memory>
|
|
|
|
|
#include <set>
|
|
|
|
|
#include <string>
|
2026-06-01 16:30:29 +02:00
|
|
|
#include <vector>
|
|
|
|
|
#include <wx/app.h>
|
2026-06-17 17:53:38 +02:00
|
|
|
#include <wx/filename.h>
|
2026-06-01 16:30:29 +02:00
|
|
|
#include <wx/string.h>
|
|
|
|
|
#include <wx/window.h>
|
2026-06-03 17:49:11 +02:00
|
|
|
#include <nlohmann/json.hpp>
|
|
|
|
|
#include <kiid.h>
|
2026-06-03 19:37:15 +02:00
|
|
|
#include <layer_ids.h>
|
2026-07-10 20:27:04 +02:00
|
|
|
#include <pcbjam_read_only.h>
|
|
|
|
|
#include <project.h>
|
2026-06-03 17:49:11 +02:00
|
|
|
#include <schematic.h>
|
|
|
|
|
#include <sch_edit_frame.h>
|
2026-08-26 13:29:29 +02:00
|
|
|
#include <symbol_edit_frame.h>
|
2026-06-10 18:06:33 +02:00
|
|
|
#include <sch_io/kicad_sexpr/sch_io_kicad_sexpr.h>
|
|
|
|
|
#include <sch_sheet.h>
|
2026-06-11 13:55:15 +02:00
|
|
|
#include <richio.h>
|
|
|
|
|
#include <lib_symbol.h>
|
|
|
|
|
#include <tools/sch_selection.h>
|
feat(collab): presence P3 — eeschema port (collab-presence 0003)
- eeschema_embind.cpp: presence section (0002 pattern, zero fork changes) —
wx canvas triggers + SCHEMATIC_LISTENER piggyback → post-settle
SCH_SELECTION_TOOL emit; throttled cursor; remote VIEW_OVERLAY render
(SCHEMATIC::ResolveItem, name tags, screen-constant via GAL matrix);
schCollab{PresenceStart,SetRemote,GetViewport,GetSelection,TestSelectFirst,
TestClearSelection}; kicad_editor_embind dispatches by active frame.
- sheet-manager: parked rooms carry SKELETON awareness states
({user,tool,sheetPath=bound sheet}) via publishSkeletons on switch + late
warm-up — the bound room's awareness then holds every project peer, so no
multi-room aggregation; presence.ts publishSkeleton helper.
- PresenceRoster: sheet-aware — peers on another sheet render dimmed with
'on <sheet>' tooltip; WasmTool un-gates the kicad presence bridge for
eeschema and threads activeSheetPath.
- tests: presence-eeschema.spec.ts (5 e2e, mirrors pcbnew incl. the px/IU
band at eeschema's 1e4/mm IU); presence.test.ts +2 (skeleton visibility,
no ghost cursor after rebind). eeschema-collab/subschema stay green.
Verified live: two tabs on demo.kicad_sch — peer cursor cross + label,
selection box + name tag, roster avatar.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
2026-07-06 17:05:15 +02:00
|
|
|
#include <tools/sch_selection_tool.h>
|
2026-06-03 17:49:11 +02:00
|
|
|
#include <sch_commit.h>
|
feat(collab): presence P3 — eeschema port (collab-presence 0003)
- eeschema_embind.cpp: presence section (0002 pattern, zero fork changes) —
wx canvas triggers + SCHEMATIC_LISTENER piggyback → post-settle
SCH_SELECTION_TOOL emit; throttled cursor; remote VIEW_OVERLAY render
(SCHEMATIC::ResolveItem, name tags, screen-constant via GAL matrix);
schCollab{PresenceStart,SetRemote,GetViewport,GetSelection,TestSelectFirst,
TestClearSelection}; kicad_editor_embind dispatches by active frame.
- sheet-manager: parked rooms carry SKELETON awareness states
({user,tool,sheetPath=bound sheet}) via publishSkeletons on switch + late
warm-up — the bound room's awareness then holds every project peer, so no
multi-room aggregation; presence.ts publishSkeleton helper.
- PresenceRoster: sheet-aware — peers on another sheet render dimmed with
'on <sheet>' tooltip; WasmTool un-gates the kicad presence bridge for
eeschema and threads activeSheetPath.
- tests: presence-eeschema.spec.ts (5 e2e, mirrors pcbnew incl. the px/IU
band at eeschema's 1e4/mm IU); presence.test.ts +2 (skeleton visibility,
no ghost cursor after rebind). eeschema-collab/subschema stay green.
Verified live: two tabs on demo.kicad_sch — peer cursor cross + label,
selection box + name tag, roster avatar.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
2026-07-06 17:05:15 +02:00
|
|
|
#include <sch_draw_panel.h>
|
|
|
|
|
#include <geometry/eda_angle.h>
|
|
|
|
|
#include <math/util.h>
|
|
|
|
|
#include <tool/tool_manager.h>
|
|
|
|
|
#include <view/view.h>
|
|
|
|
|
#include <view/view_overlay.h>
|
|
|
|
|
#include <chrono>
|
|
|
|
|
#include <wx/event.h>
|
2026-06-03 17:49:11 +02:00
|
|
|
#include <sch_item.h>
|
2026-06-03 19:37:15 +02:00
|
|
|
#include <sch_line.h>
|
|
|
|
|
#include <sch_junction.h>
|
2026-06-04 09:40:13 +02:00
|
|
|
#include <sch_no_connect.h>
|
|
|
|
|
#include <sch_text.h>
|
|
|
|
|
#include <sch_label.h>
|
2026-06-04 15:18:12 +02:00
|
|
|
#include <sch_symbol.h>
|
2026-06-04 17:05:52 +02:00
|
|
|
#include <sch_field.h>
|
2026-06-04 12:37:05 +02:00
|
|
|
#include <sch_shape.h>
|
|
|
|
|
#include <eda_shape.h>
|
|
|
|
|
#include <stroke_params.h>
|
2026-06-03 17:49:11 +02:00
|
|
|
#include <sch_screen.h>
|
|
|
|
|
#include <sch_sheet_path.h>
|
2026-06-04 12:37:05 +02:00
|
|
|
#include <schematic_settings.h>
|
feat(collab): selection soft-locks — remote-selected items can't be dragged locally (collab-presence 0007)
While a peer has an item selected, local users can still select it for
inspection but move/drag/rotate/delete skip it with an infobar naming the
holder (native locked-item UX; enforced via the fork's PCBJAM_REMOTE_LOCK
query — kicad 81f9cd80fd, the epic's first fork-touching phase). Overlapping
holds (both grabbed inside the awareness propagation window) tie-break
deterministically: lowest (user.id, clientID) keeps the item, every losing
client auto-releases it.
- lock-tiebreak.ts: pure policy — beats(), remoteLocks() (union of ALL other
clients' selections incl. own user's other tabs, minus own-held-and-winning
uuids so the winner isn't blocked mid-release), contestedReleases()
- presence.ts: clients() (per-client view, no user dedupe) + self(); FIX for
a pre-existing flaky stack overflow — resolveCollision re-entered itself
synchronously via its own patch's awareness 'change' and could ping-pong on
stale same-user states (~1-in-3 unit runs); re-entrancy guard defers
re-resolution to the next genuine delivery
- presence-kicad.ts: locks ride the kicadCollabSetRemote snapshot
(`locks:[{uuid,name}]`); losing overlaps call kicadCollabReleaseSelection
- wasm bindings (both TUs + merged dispatch): g_locks map + fork query
install; kicadCollabReleaseSelection (cancelInteractive only when a tool
stack is live — bare ESC would clear the whole selection — then selective
RemoveItemFromSel + infobar + forced re-emit); kicadCollabTestGetLocked
- tests: lock-tiebreak unit suite; presence-locks e2e for both editors (real
move veto — pcbnew click+M hotkey since its default left-drag is
rubber-band select, eeschema real drag — each with an unlocked control);
two-tab tests/web/locks.spec.ts (lock propagation + deterministic tiebreak
release + unlock on clear, passing vs real partykit)
Spec: docs/features/collab-presence/0007 (closed repo).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lg5jwWuhFH5dL8hEcBDuP2
2026-07-07 20:35:26 +02:00
|
|
|
#include <tool/actions.h>
|
feat(standalone): floating sheet navigator panel backed by a C++ hierarchy export (sheet-panel)
Canvas-only eeschema sessions (read-only viewers, hide-UI editors) could not
reach the wx hierarchy pane, so a viewer of a hierarchical design was stuck
on the entry sheet. Adds the React stand-in (SheetPanel) + the bridge:
- eeschema_embind: kicadSheetsGetTree() (SCHEMATIC::Hierarchy(), page-number
ordered: instance KIID path / parent / Sheetname / file / page / depth) and
kicadSheetsEnter(path) (validated synchronously, applied on the coroutine
via SCH_ACTIONS::changeSheet — same route as the wx pane, history intact).
OnSchSheetChanged pushes window.kicadCollab.onSheetsState after EVERY
navigation, wx-driven included, so the panel is event-driven.
- kicad → 9ab93b838e: navigate actions allowlisted for read-only viewers.
- SheetPanel.tsx: LayerPanel shell conventions; rendered + menu row only when
the hierarchy has >1 sheet instance; viewers boot it collapsed.
- tests: fixtures/demo/hier (root → Power, IO → Sub) + web/sheet-panel.spec
(list/order/depth, navigate + collab rebind, menu toggle/close, flat
schematic has no panel, bridge-driven navigation updates the panel);
SheetPanel.test.ts for the wire parser.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CtN6ASBvMGNbjPqY5boycg
2026-08-29 16:24:57 +02:00
|
|
|
#include <tools/sch_actions.h>
|
2026-06-05 11:57:10 +02:00
|
|
|
#include <tool/coroutine.h>
|
feat(collab): selection soft-locks — remote-selected items can't be dragged locally (collab-presence 0007)
While a peer has an item selected, local users can still select it for
inspection but move/drag/rotate/delete skip it with an infobar naming the
holder (native locked-item UX; enforced via the fork's PCBJAM_REMOTE_LOCK
query — kicad 81f9cd80fd, the epic's first fork-touching phase). Overlapping
holds (both grabbed inside the awareness propagation window) tie-break
deterministically: lowest (user.id, clientID) keeps the item, every losing
client auto-releases it.
- lock-tiebreak.ts: pure policy — beats(), remoteLocks() (union of ALL other
clients' selections incl. own user's other tabs, minus own-held-and-winning
uuids so the winner isn't blocked mid-release), contestedReleases()
- presence.ts: clients() (per-client view, no user dedupe) + self(); FIX for
a pre-existing flaky stack overflow — resolveCollision re-entered itself
synchronously via its own patch's awareness 'change' and could ping-pong on
stale same-user states (~1-in-3 unit runs); re-entrancy guard defers
re-resolution to the next genuine delivery
- presence-kicad.ts: locks ride the kicadCollabSetRemote snapshot
(`locks:[{uuid,name}]`); losing overlaps call kicadCollabReleaseSelection
- wasm bindings (both TUs + merged dispatch): g_locks map + fork query
install; kicadCollabReleaseSelection (cancelInteractive only when a tool
stack is live — bare ESC would clear the whole selection — then selective
RemoveItemFromSel + infobar + forced re-emit); kicadCollabTestGetLocked
- tests: lock-tiebreak unit suite; presence-locks e2e for both editors (real
move veto — pcbnew click+M hotkey since its default left-drag is
rubber-band select, eeschema real drag — each with an unlocked control);
two-tab tests/web/locks.spec.ts (lock propagation + deterministic tiebreak
release + unlock on clear, passing vs real partykit)
Spec: docs/features/collab-presence/0007 (closed repo).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lg5jwWuhFH5dL8hEcBDuP2
2026-07-07 20:35:26 +02:00
|
|
|
#include <pcbjam_remote_lock.h>
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
#include "collab_common.h"
|
2026-07-30 14:17:48 +02:00
|
|
|
#include "open_gate.h"
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
#include "collab_presence_core.h"
|
feat(collab): dev-time presence style tuner (VITE_PRESENCE_TUNER=1)
Parametrizes every visual knob of the presence overlay so we can pick the
shipped look live, then wire the winners into the defaults:
- collab_presence_style.h: shared STYLE struct + drawing (now used by BOTH
editor TUs — no more duplicated overlay code): selection shape (rect /
corner brackets / underline / rounded rect / filled-only), border width +
alpha, infill alpha, padding, corner radius; name tag show/size/chip-
background/inside-outside/top-bottom/start-end-center/offset; cursor shape
(cross / pointer / circle+dot), size/width/alpha + label knobs; fixed-color
and palette-by-name-hash overrides (try palettes without changing what
senders publish); pin radius/ring/alphas. Defaults == shipped look.
- kicadCollabSetStyle(json) live-patch export + kicadCollabTestListItems(n)
(real KIIDs for synthetic previews); merged dispatch; pins now carry the
author name so palette overrides recolor them consistently.
- PresenceTuner.tsx: floating dev panel (env-gated, tree-shaken otherwise) —
grouped sliders/selects, demo peers+pins injection for SOLO tuning,
localStorage persistence across reloads, Copy JSON export, reset.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
2026-07-06 19:10:40 +02:00
|
|
|
#include "collab_presence_style.h"
|
comments-ux: figma bubble pins, floating panel, seen/reactions/mentions UI, theme follow (0001 A–E + 0002)
- GAL pin = one closed polygon: round body, squared-off bottom-left corner
ON the anchor; PIN gains unread (accent ring); tuner knobs; shipped
defaults r9/ring4/alpha.9. DOM hit/highlight sized+offset from a LIVE
pin-geometry radius store the tuner feeds.
- Floating comments panel: draggable (shared useDraggablePanel with
always-onscreen restore; overlay FAB retrofitted), collapsible to header,
header carries add/show-hide/mark-all; unread badges (rose on mention).
- Reactions (emoji-mart lazy, quick-row) + @-mention autocomplete
(MentionInput; backend roster with presence/author fallback).
- Theme: ?theme= > storage > OS, no-flash boot, toggles (HomePage + overlay
View row), boot-seeded pcbjam-dark schematic colors + kicadSetColorTheme /
kicadSetDarkChrome bridges (canvas + wx chrome live flip), light/dark
variants across all overlay surfaces.
- e2e: panel/seen/reactions/mentions/theme specs + resize-spec geometry;
bumps pcbjam-shared (flat-key seen/reactions + listCollaborators) and
wxwidgets (dark chrome) pointers.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HLwn1toiNKi1MgxGKnZTes
2026-07-24 13:21:22 +02:00
|
|
|
#include "pcbjam_theme.h"
|
libs: peer lib edits reach the running editor + "placed symbol updated" toast
synced-source subscribes to its SyncStack: remote changes (self-save echoes
consumed via a selfPushed flag) debounce per kind into kicadLibsReload — a
new embind export (pcbjam_libs_reload.h, all three TUs) that drops the lib's
plugin cache (LIBRARY_MANAGER::ReloadLibraryEntry), reloads it, and mails
MAIL_RELOAD_LIB with the nickname so the symbol tree force-refreshes (the
plugin's modify hash is a pinned constant, so a plain sync would skip it).
After the reload, kicadLibsSymbolUsage (new eeschema embind: placed
SCH_SYMBOL count across unique screens) gates LIB_ITEM_UPDATED_EVENT, and
WasmTool shows an amber toast when a PLACED symbol changed — placed copies
keep the previous version until updated from the library.
syncedScopeLibsSource gives PROJECT sessions the synced source under
VITE_LIBS_SOURCE=synced (remote contract for lib listing/createLib, lazy
per-lib SyncStacks for item ops/presync) so realtime reaches open
schematics; previously project sessions silently fell back to the per-item
remote source. Unit tests cover reload debounce, self-echo skip, per-kind
routing, usage-gated event, and the no-Module no-op.
Bumps kicad (MAIL_RELOAD_LIB force-refresh payload).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013QRWoXiM9uuo1enXGhAYku
2026-07-09 18:21:21 +02:00
|
|
|
#include "pcbjam_libs_reload.h"
|
jspi: migration phases 0-7 — build knob, scheduler shim, test successor suite
Toolchain: emsdk 6.0.6 (versions.sh; cache-hash keys on it). Build knob
PCBJAM_ASYNC_BACKEND=jspi|asyncify: build-kicad-target.sh links editors with
-sJSPI + -sJSPI_EXPORTS=@scripts/common/jspi-exports.txt + --pre-js
jspi-scheduler.js (no DYNCALLS, no post-link asyncify pipeline); wx build
stamps the backend and forces clean on flip or unknown provenance;
docker/build.sh passes the knob, seeds the emscripten ports cache from the
volume every launch, jspi postprocess = patch-env-shim only.
scripts/common/shims/jspi-scheduler.js: the JSPI successor scheduler —
token-wait registry, resume turnstile (one armed resume between engine
re-entries, SP swaps only at microtask boundaries), green-region spill
stacks (16-aligned tops), S1 embind mutator FIFO lane + parker wraps, S6
shutdown, libctx integration hooks (suspend/end/quarantine + g_current
arm/clear), SuspendError attributor, lost-wake + stuck-window watchdogs,
__wxWaitDump observability.
Embind: PARKER registrations get emscripten::async() under PCBJAM_JSPI
(wasm/bindings/pcbjam_async_policy.h). nanosleep yields route via the shim.
Tests: tests/asyncify -> tests/jspi successor suite (jspi-stack red/green
shadow-stack battery, jspi-coroutine MiniCoro harness, suspend-races
semantic scenarios + __wxWaitDump books coherence); projects jspi-firefox/
jspi-chrome (asyncify-webkit retired — no JSPI in WebKit); unconditional
Firefox JSPI pref; guard-beacons -> wait-beacons (+wxScheduler/libctxJspi
families); Makefile.wasm links test apps against JSPI with the shim as a
tracked link prerequisite.
Web: WasmTool setRo await + __wxWaitDump forensics, open-flow contained
promise, scheduler-shim.test.ts retargeted (8 green).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NDeBaKKhQztd8KiVtHuyXr
2026-08-13 07:06:24 +02:00
|
|
|
#include "pcbjam_async_policy.h"
|
feat(collab): tuner round 2 — center-anchored labels, exact outlines, varied demo set, clearer color modes
- collab_presence_style.h: GAL BitmapText CENTERS on its position (confirmed
in GAL::ResetTextAttributes — the mispositioned nameplates); labels/chips
now hand GAL the block center. New selection shape 5 'exact outline':
pcbnew hugs real geometry (footprint bounding hull, TransformShapeToPolygon
for the rest, padding inflates the polygon); eeschema falls back to rect.
- kicadCollabTestDemoSet (both TUs + merged): labeled demo groups — smallest
+ largest footprint and the two busiest nets' segments (symbols + wire
bundles on sch) — so the style preview covers the real range of shapes.
- PresenceTuner: Colors section rebuilt as explicit modes (per-user / fixed /
palette) with preset palettes (default, pastel, vivid, okabe-ito), buffered
hex editing + Apply (the old always-filtering textarea ate keystrokes), an
'overlay only' hint; demo injection consumes the varied demo set; 'exact
outline (pcb)' in the shape list.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
2026-07-07 10:41:15 +02:00
|
|
|
#include <algorithm>
|
2026-06-01 16:30:29 +02:00
|
|
|
|
|
|
|
|
using namespace emscripten;
|
2026-06-03 17:49:11 +02:00
|
|
|
using json = nlohmann::json;
|
2026-06-01 16:30:29 +02:00
|
|
|
|
|
|
|
|
// Programmatically open a project file (schematic) in the running editor frame,
|
|
|
|
|
// without UI automation. Mirrors single_top.cpp's MacOpenFile path: the editor
|
|
|
|
|
// frame is the app's top window and is a KIWAY_PLAYER. Returns the result of
|
|
|
|
|
// OpenProjectFiles, or false if no frame is available — letting the JS caller
|
|
|
|
|
// fall back to driving File→Open.
|
2026-07-02 14:48:11 +02:00
|
|
|
//
|
|
|
|
|
// KICAD_MERGED_EMBIND (kicad_editor, editor-unification Part 2): pcbnew_embind.cpp
|
|
|
|
|
// defines the identical function and registers the same JS names — in the merged image
|
|
|
|
|
// the frame-agnostic duplicates (this + kicadCollabOnSave) and the shared-name
|
|
|
|
|
// registrations live once in kicad_editor_embind.cpp, which dispatches the per-editor
|
|
|
|
|
// entries (renamed schCollab*/pcbCollab* below; JS-facing names are unchanged).
|
|
|
|
|
#ifndef KICAD_MERGED_EMBIND
|
2026-06-01 16:30:29 +02:00
|
|
|
bool kicadOpenFile( std::string path )
|
|
|
|
|
{
|
jspi cleanup: remove the asyncify-era residue — dead code, conditionals, pipeline scaffolding, stale prose
The runtime is JSPI-only; this removes everything that still pretended
otherwise. Three exhaustive sweeps (C++/JS+build+CI/tests+docs) drove
the inventory; every deletion verified by grep closure + full gates.
Broken-right-now fixes:
- deploy-staging.yml passed the retired opt_level input — the workflow
could not even start. Removed.
- env.sh carried dead exports with a live -sASYNCIFY=1 inside
(WASM_LDFLAGS/PTHREAD_LDFLAGS, zero consumers). Removed; the
WASM_LEGACY_EXCEPTIONS rationale rewritten to the real reason.
- docker/build.sh exported PCBJAM_ASYNC_BACKEND (read nowhere). Gone.
Dead weight removed:
- binaryen submodule (nothing builds or invokes it), wasm-opt-bench
workflow + scripts/bench/, get-wasm-opt.sh, diagnostics.js (242 lines
of Asyncify-API-only code), the KICAD_PIPELINE background-postprocess
scaffolding (existed to parallelize the deleted wasm-opt phase; the
postprocess is a seconds-long node script and now runs inline),
build-monitor's dead asyncify rows, sched-context orphan build
output, dead .gitignore entries, the .jspi-assets spike dir (the two
wf-result research JSONs moved to docs/features/async/migration-evidence/).
- bindings: fiber_park.h + its 12 embind registrations (broken-if-
called under JSPI), the kicadOpenFileStart/OPEN_JOB starter route,
main_stack_runner.h + 5 includes, the always-null context-sleep weak
hook in nanosleep_yield.c.
- shim: the backend field (installed-flag idempotency instead),
noteContextWait (dead both sides), the __wxAsyncifyDump alias (+ the
WasmTool fallback and string-dump normalize branch).
- web: the emscripten-6-ignored mainScriptUrlOrBlob option in boot.ts
(gerber-demo keeps it: it loads the deployed CDN release, which
predates emscripten 6 — noted inline).
Conditionals: all 'backend === jspi' checks reduced to scheduler-
presence checks; races_quiescent re-keyed from Asyncify.state (vacuous)
to real backlog quiescence (resumeReady/mutatorQueue — NOT _windowLive,
which is the probing activation's own window by definition).
Renames (identifiers only, no file renames): ASYNC_LINK_FLAGS→
JSPI_LINK_FLAGS and Makefile ASYNC_LDFLAGS→JSPI_LDFLAGS,
kicadCollabFiberBusy→kicadCollabBusy (embind + web + tests),
collab_common.h fiber*→apply*/coroutine naming, asyncifySignatures→
wasmTrapSignatures (lists byte-identical).
Tests: the two remaining vacuous [wx-asyncify]/fiber-resume-refused
asserts re-keyed to live JSPI beacons; eeschema-load's failure message
no longer sends the developer to a deleted script; wait-beacons' dead
families/parser deleted; lane-0 legacy-glue guards removed (lane 0 is
unconstructible); the embind test.fail re-gated with the JSPI reason
(plain embind invokers cannot suspend — verified still failing);
lint-determinism now scans tests/jspi (166 files clean);
eeschema-collab local-move gated to chromium (~50% flaky on FF even
solo; pcbnew twin covers both engines).
Docs: DEBUG.md rewritten as the JSPI debugging guide; build.md
describes the single-phase build; docs/features/async/README.md
banner-marked historical and repointed at the NEW
23-jspi-runtime.md (current architecture: export census, turnstile,
libcontext ownership + refusal contract, embind call shapes, the
em-pthread service-wrapper trick, exception policy, known gaps).
Gates on the cleaned tree: test:e2e 725 passed / 0 failed (after the
quiescence-probe fix; the 3 other reds were verified contention flakes
solo-green or the documented FF gate), web 76/0, jspi 18/18 both
engines, vitest 295/295 + 17/17, all lints green, live-app census
clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016X9eh1s5sTx1o9Em9KBuwR
2026-08-14 09:25:32 +02:00
|
|
|
// Held across every suspension of the load; see open_gate.h.
|
2026-07-30 14:17:48 +02:00
|
|
|
pcbjam_open::BusyGuard busy;
|
|
|
|
|
|
|
|
|
|
if( pcbjam_open::testParkMs() > 0 )
|
|
|
|
|
emscripten_sleep( pcbjam_open::testParkMs() );
|
|
|
|
|
|
2026-06-01 16:30:29 +02:00
|
|
|
KIWAY_PLAYER* frame =
|
|
|
|
|
wxTheApp ? static_cast<KIWAY_PLAYER*>( wxTheApp->GetTopWindow() ) : nullptr;
|
|
|
|
|
|
|
|
|
|
if( !frame )
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
if( wxWindow* blocking = frame->Kiway().GetBlockingDialog() )
|
|
|
|
|
blocking->Close( true );
|
|
|
|
|
|
2026-07-30 14:17:48 +02:00
|
|
|
bool ok = frame->OpenProjectFiles(
|
2026-06-01 16:30:29 +02:00
|
|
|
std::vector<wxString>( 1, wxString::FromUTF8( path.c_str() ) ) );
|
2026-07-30 14:17:48 +02:00
|
|
|
|
|
|
|
|
// Test-only post-load park (open_gate.h): model fully loaded, gate still
|
|
|
|
|
// closed — the deterministic window the collab-load-fuzz spec hammers.
|
|
|
|
|
if( pcbjam_open::testParkMs() > 0 )
|
|
|
|
|
emscripten_sleep( pcbjam_open::testParkMs() );
|
|
|
|
|
|
|
|
|
|
return ok;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// JS-pollable open-in-flight probe (open_gate.h): the web shell defers the
|
|
|
|
|
// collab/presence attach until the open chain has truly completed.
|
|
|
|
|
bool kicadOpenFileBusy()
|
|
|
|
|
{
|
|
|
|
|
return pcbjam_open::busy();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Test-only (collab-load-fuzz): arm the deterministic open parks.
|
|
|
|
|
void kicadTestSetOpenPark( int aMs )
|
|
|
|
|
{
|
|
|
|
|
pcbjam_open::testParkMs() = aMs;
|
2026-06-01 16:30:29 +02:00
|
|
|
}
|
2026-07-10 20:27:04 +02:00
|
|
|
|
|
|
|
|
// Read-only viewer lock (read-only-viewer): flips the process-global
|
|
|
|
|
// PCBJAM_READ_ONLY flag consumed by TOOL_MANAGER (view-only action allowlist)
|
|
|
|
|
// and the selection tools (nothing selectable), and mirrors it onto the
|
|
|
|
|
// project so the setup dialogs grey out. Returns false until the editor frame
|
|
|
|
|
// exists so JS polls; the shell fails CLOSED if it never applies.
|
|
|
|
|
bool kicadSetReadOnly( bool aReadOnly )
|
|
|
|
|
{
|
|
|
|
|
KIWAY_PLAYER* frame =
|
|
|
|
|
wxTheApp ? dynamic_cast<KIWAY_PLAYER*>( wxTheApp->GetTopWindow() ) : nullptr;
|
|
|
|
|
|
|
|
|
|
if( !frame )
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
PCBJAM_READ_ONLY::Set( aReadOnly );
|
|
|
|
|
frame->Prj().SetReadOnly( aReadOnly );
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2026-07-02 14:48:11 +02:00
|
|
|
#endif // !KICAD_MERGED_EMBIND
|
2026-06-01 16:30:29 +02:00
|
|
|
|
2026-06-03 17:49:11 +02:00
|
|
|
// ───────────────────────────── Yjs collaborative bridge ─────────────────────────────
|
|
|
|
|
//
|
|
|
|
|
// eeschema's half of the unified bridge contract (features/yjs-bridge/0001, 0003).
|
|
|
|
|
// Unlike pl_editor it needs NO kicad-fork change: SCH_ITEM already carries a stable
|
|
|
|
|
// KIID, and eeschema has native change machinery, so the adapter is a thin re-use:
|
|
|
|
|
// ChangeSource (emit) = a SCHEMATIC_LISTENER subclass (SCH_COMMIT::Push fires it)
|
|
|
|
|
// apply = SCH_COMMIT Modify/Remove + Push (drives connectivity recompute)
|
|
|
|
|
// The generic JS reconciler / transport / WasmTool wiring are reused unchanged.
|
|
|
|
|
//
|
|
|
|
|
// Scope of this first commit (0003 §"first PoC"): position-level sync of existing
|
|
|
|
|
// items — changed (move/edit) and removed. Decomposed field coverage via reflection,
|
|
|
|
|
// constructing arbitrary new item types on `added`, and symbol-instance / multi-sheet
|
|
|
|
|
// scoping are deferred (see TODOs). Items are resolved by globally-unique uuid, so
|
|
|
|
|
// changed/removed already work across the whole hierarchy without sheet scoping.
|
|
|
|
|
namespace {
|
|
|
|
|
|
|
|
|
|
// Guard so SCH_COMMIT::Push's listener callbacks during apply() aren't re-emitted.
|
|
|
|
|
bool s_applyingRemote = false;
|
|
|
|
|
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
using pcbjam_collab::toUtf8;
|
2026-06-03 17:49:11 +02:00
|
|
|
|
|
|
|
|
SCH_EDIT_FRAME* schFrame()
|
|
|
|
|
{
|
|
|
|
|
return wxTheApp ? dynamic_cast<SCH_EDIT_FRAME*>( wxTheApp->GetTopWindow() ) : nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-17 17:53:38 +02:00
|
|
|
// The screen of the sheet the editor is currently showing. Per-sheet collab keys a room
|
|
|
|
|
// to each .kicad_sch, so the snapshot/diff that feeds a room must cover ONLY this screen,
|
|
|
|
|
// never the whole Hierarchy(). GetCurrentSheet().LastScreen() is the active sheet's screen
|
|
|
|
|
// (GetScreen() tracks the same screen and is the fallback before a sheet path exists).
|
|
|
|
|
SCH_SCREEN* currentScreen( SCH_EDIT_FRAME* aFrame )
|
|
|
|
|
{
|
|
|
|
|
if( !aFrame )
|
|
|
|
|
return nullptr;
|
|
|
|
|
|
|
|
|
|
if( SCH_SCREEN* screen = aFrame->GetCurrentSheet().LastScreen() )
|
|
|
|
|
return screen;
|
|
|
|
|
|
|
|
|
|
return aFrame->GetScreen();
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-03 17:49:11 +02:00
|
|
|
json itemToJson( SCH_ITEM* aItem )
|
|
|
|
|
{
|
|
|
|
|
VECTOR2I p = aItem->GetPosition();
|
2026-06-03 19:37:15 +02:00
|
|
|
json j = {
|
2026-06-03 17:49:11 +02:00
|
|
|
{ "id", toUtf8( aItem->m_Uuid.AsString() ) },
|
|
|
|
|
{ "type", toUtf8( aItem->GetClass() ) },
|
2026-06-03 19:37:15 +02:00
|
|
|
{ "x", p.x }, // internal units; integral, so no quantization needed
|
2026-06-03 17:49:11 +02:00
|
|
|
{ "y", p.y },
|
|
|
|
|
};
|
2026-06-03 19:37:15 +02:00
|
|
|
|
|
|
|
|
// Per-type fields needed to reconstruct the item on `added` (0003 converters).
|
|
|
|
|
// Hand-mapped for the wire-editing types; other types sync position-only (move) and
|
|
|
|
|
// are skipped on `added` (logged) until their converters are added.
|
|
|
|
|
if( aItem->Type() == SCH_LINE_T )
|
|
|
|
|
{
|
|
|
|
|
auto* line = static_cast<SCH_LINE*>( aItem );
|
|
|
|
|
j["sx"] = line->GetStartPoint().x;
|
|
|
|
|
j["sy"] = line->GetStartPoint().y;
|
|
|
|
|
j["ex"] = line->GetEndPoint().x;
|
|
|
|
|
j["ey"] = line->GetEndPoint().y;
|
|
|
|
|
j["layer"] = (int) line->GetLayer();
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-04 09:40:13 +02:00
|
|
|
if( EDA_TEXT* txt = dynamic_cast<EDA_TEXT*>( aItem ) )
|
|
|
|
|
j["text"] = toUtf8( txt->GetText() );
|
|
|
|
|
|
|
|
|
|
if( SCH_LABEL_BASE* lbl = dynamic_cast<SCH_LABEL_BASE*>( aItem ) )
|
|
|
|
|
j["shape"] = (int) lbl->GetShape();
|
|
|
|
|
|
2026-06-04 12:37:05 +02:00
|
|
|
// Graphic shapes (circle / rectangle / arc / line / bezier): geometry is start/end plus,
|
|
|
|
|
// for an arc, the center, and for a bezier the two control points. Stroke + fill complete it.
|
|
|
|
|
if( aItem->Type() == SCH_SHAPE_T )
|
|
|
|
|
{
|
|
|
|
|
auto* shp = static_cast<SCH_SHAPE*>( aItem );
|
|
|
|
|
j["stype"] = (int) shp->GetShape(); // SHAPE_T
|
|
|
|
|
j["sx"] = shp->GetStart().x;
|
|
|
|
|
j["sy"] = shp->GetStart().y;
|
|
|
|
|
j["ex"] = shp->GetEnd().x;
|
|
|
|
|
j["ey"] = shp->GetEnd().y;
|
|
|
|
|
j["layer"] = (int) shp->GetLayer();
|
|
|
|
|
j["width"] = shp->GetStroke().GetWidth();
|
|
|
|
|
j["fill"] = (int) shp->GetFillMode();
|
|
|
|
|
|
|
|
|
|
if( shp->GetShape() == SHAPE_T::ARC )
|
|
|
|
|
{
|
|
|
|
|
VECTOR2I c = shp->GetCenter();
|
|
|
|
|
j["cx"] = c.x;
|
|
|
|
|
j["cy"] = c.y;
|
|
|
|
|
}
|
|
|
|
|
else if( shp->GetShape() == SHAPE_T::BEZIER )
|
|
|
|
|
{
|
|
|
|
|
j["c1x"] = shp->GetBezierC1().x;
|
|
|
|
|
j["c1y"] = shp->GetBezierC1().y;
|
|
|
|
|
j["c2x"] = shp->GetBezierC2().x;
|
|
|
|
|
j["c2y"] = shp->GetBezierC2().y;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-03 19:37:15 +02:00
|
|
|
return j;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Construct a new SCH_ITEM from a delta item (for `added`), with the delta's uuid
|
|
|
|
|
// (m_Uuid is const → const_cast, exactly as the s-expr parser does). Returns nullptr for
|
|
|
|
|
// types without a converter yet.
|
|
|
|
|
SCH_ITEM* makeItem( const json& j )
|
|
|
|
|
{
|
|
|
|
|
std::string type = j.value( "type", "" );
|
|
|
|
|
SCH_ITEM* item = nullptr;
|
|
|
|
|
|
|
|
|
|
if( type == "SCH_LINE" )
|
|
|
|
|
{
|
|
|
|
|
int layer = j.value( "layer", (int) LAYER_NOTES );
|
|
|
|
|
auto* line = new SCH_LINE( VECTOR2I( j.value( "sx", 0 ), j.value( "sy", 0 ) ), layer );
|
|
|
|
|
line->SetEndPoint( VECTOR2I( j.value( "ex", 0 ), j.value( "ey", 0 ) ) );
|
|
|
|
|
item = line;
|
|
|
|
|
}
|
|
|
|
|
else if( type == "SCH_JUNCTION" )
|
|
|
|
|
{
|
|
|
|
|
item = new SCH_JUNCTION( VECTOR2I( j.value( "x", 0 ), j.value( "y", 0 ) ) );
|
|
|
|
|
}
|
2026-06-04 09:40:13 +02:00
|
|
|
else if( type == "SCH_NO_CONNECT" )
|
|
|
|
|
{
|
|
|
|
|
item = new SCH_NO_CONNECT( VECTOR2I( j.value( "x", 0 ), j.value( "y", 0 ) ) );
|
|
|
|
|
}
|
|
|
|
|
else if( type == "SCH_TEXT" )
|
|
|
|
|
{
|
2026-06-04 12:37:05 +02:00
|
|
|
auto* txt = new SCH_TEXT( VECTOR2I( j.value( "x", 0 ), j.value( "y", 0 ) ),
|
|
|
|
|
wxString::FromUTF8( j.value( "text", "" ).c_str() ) );
|
|
|
|
|
|
|
|
|
|
// Mirror the interactive text tool (sch_drawing_tools.cpp createNewText): parent the
|
|
|
|
|
// item to the schematic and apply the project's default text size, so a remotely-added
|
|
|
|
|
// text resolves variables / renders identically to a locally-placed one.
|
|
|
|
|
if( SCH_EDIT_FRAME* fr = schFrame() )
|
|
|
|
|
{
|
|
|
|
|
txt->SetParent( &fr->Schematic() );
|
|
|
|
|
int sz = fr->Schematic().Settings().m_DefaultTextSize;
|
|
|
|
|
txt->SetTextSize( VECTOR2I( sz, sz ) );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
item = txt;
|
2026-06-04 09:40:13 +02:00
|
|
|
}
|
|
|
|
|
else if( type == "SCH_LABEL" || type == "SCH_GLOBALLABEL" || type == "SCH_HIERLABEL" )
|
|
|
|
|
{
|
|
|
|
|
VECTOR2I pos( j.value( "x", 0 ), j.value( "y", 0 ) );
|
|
|
|
|
wxString text = wxString::FromUTF8( j.value( "text", "" ).c_str() );
|
|
|
|
|
|
|
|
|
|
SCH_LABEL_BASE* lbl = ( type == "SCH_LABEL" ) ? (SCH_LABEL_BASE*) new SCH_LABEL( pos, text )
|
|
|
|
|
: ( type == "SCH_GLOBALLABEL" ) ? (SCH_LABEL_BASE*) new SCH_GLOBALLABEL( pos, text )
|
|
|
|
|
: (SCH_LABEL_BASE*) new SCH_HIERLABEL( pos, text );
|
|
|
|
|
|
|
|
|
|
if( j.contains( "shape" ) )
|
|
|
|
|
lbl->SetShape( (LABEL_FLAG_SHAPE) j["shape"].get<int>() );
|
|
|
|
|
|
|
|
|
|
item = lbl;
|
|
|
|
|
}
|
2026-06-05 11:57:10 +02:00
|
|
|
else if( type == "SCH_SHAPE" )
|
|
|
|
|
{
|
|
|
|
|
// Reconstruct from the geometry itemToJson emits. Committing a *new* SCH_SHAPE used to
|
jspi cleanup: remove the asyncify-era residue — dead code, conditionals, pipeline scaffolding, stale prose
The runtime is JSPI-only; this removes everything that still pretended
otherwise. Three exhaustive sweeps (C++/JS+build+CI/tests+docs) drove
the inventory; every deletion verified by grep closure + full gates.
Broken-right-now fixes:
- deploy-staging.yml passed the retired opt_level input — the workflow
could not even start. Removed.
- env.sh carried dead exports with a live -sASYNCIFY=1 inside
(WASM_LDFLAGS/PTHREAD_LDFLAGS, zero consumers). Removed; the
WASM_LEGACY_EXCEPTIONS rationale rewritten to the real reason.
- docker/build.sh exported PCBJAM_ASYNC_BACKEND (read nowhere). Gone.
Dead weight removed:
- binaryen submodule (nothing builds or invokes it), wasm-opt-bench
workflow + scripts/bench/, get-wasm-opt.sh, diagnostics.js (242 lines
of Asyncify-API-only code), the KICAD_PIPELINE background-postprocess
scaffolding (existed to parallelize the deleted wasm-opt phase; the
postprocess is a seconds-long node script and now runs inline),
build-monitor's dead asyncify rows, sched-context orphan build
output, dead .gitignore entries, the .jspi-assets spike dir (the two
wf-result research JSONs moved to docs/features/async/migration-evidence/).
- bindings: fiber_park.h + its 12 embind registrations (broken-if-
called under JSPI), the kicadOpenFileStart/OPEN_JOB starter route,
main_stack_runner.h + 5 includes, the always-null context-sleep weak
hook in nanosleep_yield.c.
- shim: the backend field (installed-flag idempotency instead),
noteContextWait (dead both sides), the __wxAsyncifyDump alias (+ the
WasmTool fallback and string-dump normalize branch).
- web: the emscripten-6-ignored mainScriptUrlOrBlob option in boot.ts
(gerber-demo keeps it: it loads the deployed CDN release, which
predates emscripten 6 — noted inline).
Conditionals: all 'backend === jspi' checks reduced to scheduler-
presence checks; races_quiescent re-keyed from Asyncify.state (vacuous)
to real backlog quiescence (resumeReady/mutatorQueue — NOT _windowLive,
which is the probing activation's own window by definition).
Renames (identifiers only, no file renames): ASYNC_LINK_FLAGS→
JSPI_LINK_FLAGS and Makefile ASYNC_LDFLAGS→JSPI_LDFLAGS,
kicadCollabFiberBusy→kicadCollabBusy (embind + web + tests),
collab_common.h fiber*→apply*/coroutine naming, asyncifySignatures→
wasmTrapSignatures (lists byte-identical).
Tests: the two remaining vacuous [wx-asyncify]/fiber-resume-refused
asserts re-keyed to live JSPI beacons; eeschema-load's failure message
no longer sends the developer to a deleted script; wait-beacons' dead
families/parser deleted; lane-0 legacy-glue guards removed (lane 0 is
unconstructible); the embind test.fail re-gated with the JSPI reason
(plain embind invokers cannot suspend — verified still failing);
lint-determinism now scans tests/jspi (166 files clean);
eeschema-collab local-move gated to chromium (~50% flaky on FF even
solo; pcbnew twin covers both engines).
Docs: DEBUG.md rewritten as the JSPI debugging guide; build.md
describes the single-phase build; docs/features/async/README.md
banner-marked historical and repointed at the NEW
23-jspi-runtime.md (current architecture: export census, turnstile,
libcontext ownership + refusal contract, embind call shapes, the
em-pthread service-wrapper trick, exception policy, known gaps).
Gates on the cleaned tree: test:e2e 725 passed / 0 failed (after the
quiescence-probe fix; the 3 other reds were verified contention flakes
solo-green or the documented FF gate), web 76/0, jspi 18/18 both
engines, vitest 295/295 + 17/17, all lints green, live-app census
clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016X9eh1s5sTx1o9Em9KBuwR
2026-08-14 09:25:32 +02:00
|
|
|
// trap in SCH_COMMIT::Push's CHT_ADD path under the retired asyncify runtime because
|
|
|
|
|
// doApply ran off the coroutine context; doApply runs inside a COROUTINE
|
|
|
|
|
// (kicadCollabApply), serialized with local edits, exactly as a native draw does.
|
|
|
|
|
// (0006/0007.) NB FILL_T::NO_FILL == 1, not 0.
|
2026-06-05 11:57:10 +02:00
|
|
|
SHAPE_T st = (SHAPE_T) j.value( "stype", (int) SHAPE_T::RECTANGLE );
|
|
|
|
|
int layer = j.value( "layer", (int) LAYER_NOTES );
|
|
|
|
|
int width = j.value( "width", 0 );
|
|
|
|
|
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`.
|
2026-06-03 19:37:15 +02:00
|
|
|
|
|
|
|
|
if( item )
|
|
|
|
|
const_cast<KIID&>( item->m_Uuid ) = KIID( wxString::FromUTF8( j.value( "id", "" ).c_str() ) );
|
|
|
|
|
|
|
|
|
|
return item;
|
2026-06-03 17:49:11 +02:00
|
|
|
}
|
|
|
|
|
|
2026-06-17 17:53:38 +02:00
|
|
|
// Current model of the ACTIVE screen as an array of item json. One collab room == one
|
|
|
|
|
// .kicad_sch screen, so we never fold in the rest of the hierarchy (uuids are unique
|
|
|
|
|
// within a screen, so no cross-sheet dedup is needed).
|
|
|
|
|
json snapshotItems( SCH_EDIT_FRAME* aFrame )
|
2026-06-03 17:49:11 +02:00
|
|
|
{
|
2026-06-17 17:53:38 +02:00
|
|
|
json arr = json::array();
|
2026-06-03 17:49:11 +02:00
|
|
|
|
2026-06-17 17:53:38 +02:00
|
|
|
if( SCH_SCREEN* screen = currentScreen( aFrame ) )
|
2026-06-03 17:49:11 +02:00
|
|
|
{
|
|
|
|
|
for( SCH_ITEM* item : screen->Items() )
|
2026-06-17 17:53:38 +02:00
|
|
|
arr.push_back( itemToJson( item ) );
|
2026-06-03 17:49:11 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return arr;
|
|
|
|
|
}
|
|
|
|
|
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
// Wire emitters (legacy scalar delta + v2 items): shared, collab_common.h.
|
|
|
|
|
using pcbjam_collab::emitDelta;
|
|
|
|
|
using pcbjam_collab::emitItemsWire;
|
2026-06-11 13:55:15 +02:00
|
|
|
|
2026-06-17 17:53:38 +02:00
|
|
|
// Notify the standalone that the editor switched to a different sheet — a different
|
|
|
|
|
// .kicad_sch == a different collab room (ysync subschemas). The path is the active
|
|
|
|
|
// screen's load path: the same absolute MEMFS form kicadCollabOnSave emits, so the JS
|
|
|
|
|
// side strips the project prefix the same way (relativeProjectPath). No-op without a
|
|
|
|
|
// JS listener.
|
|
|
|
|
void emitSheetChanged()
|
|
|
|
|
{
|
|
|
|
|
SCH_SCREEN* screen = currentScreen( schFrame() );
|
|
|
|
|
|
|
|
|
|
if( !screen )
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
std::string s = toUtf8( screen->GetFileName() );
|
|
|
|
|
EM_ASM( {
|
|
|
|
|
if( window.kicadCollab && window.kicadCollab.onSheetChanged )
|
staging CI green-up: P-1 apply-slot wedge under JSPI, popup P-4 regression, ysync double-seed header, spec fixes
- P-1 (findings-p.spec P-1c red on CI + locally): a JS exception thrown from a
window.kicadCollab wire callback does NOT reach the C++ catch(...) — under
JSPI it REJECTS the promising coroutine entry, slot.done never flips and the
apply slot is busy for the page's life. Three layers: libcontext flags the
rejected record dead (kicad → d0afbc4bd3),
drainApplies() reaps a !done && !CanResume() body, and every EM_ASM
window.kicadCollab.onX(...) call is try/catch-guarded (collab_common.h,
kicad_editor/eeschema/pl_editor embind).
- popup.spec 'dismisses on outside click': wx P-4 SetFocus delegation excluded
for wxPopupWindow (wxwidgets → a7436d1052).
- ysync-two-tab concurrent seed: repairLayoutY + seedNonce retraction deleted
both headers (pcbjam-shared → 8dabf2e).
- dark-mode.spec: stale light-mode reference (pre wx fa46f1822c DOM
re-projection) replaced with the CI chromium render.
- items-wire-batch-loss.spec: precondition obsolete after P-5 — poisoned entry
is now synthetic, plus a P-5 contract assertion (field blob == '').
- presence-ghost-peer.spec: skip early when no /parties/ socket (CI web e2e
runs the BroadcastChannel provider); findings-p1.spec: polled click-select
precondition.
Local: kicad-chromium findings/presence/ysync/collab 59/59, popup 7/7,
dark-mode 1/1, pcbjam-shared vitest 249/249.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LvvARNMxQrq17vogn3REMU
2026-08-28 22:25:21 +02:00
|
|
|
{
|
|
|
|
|
// A throwing listener must never unwind the wasm frame that called it: under
|
|
|
|
|
// JSPI that rejects the running coroutine's entry (findings P-1).
|
|
|
|
|
try { window.kicadCollab.onSheetChanged( UTF8ToString( $0 ) ); }
|
|
|
|
|
catch( e ) { console.error( '[pcbjam collab] onSheetChanged listener threw', e ); }
|
|
|
|
|
}
|
2026-06-17 17:53:38 +02:00
|
|
|
}, s.c_str() );
|
|
|
|
|
}
|
|
|
|
|
|
feat(standalone): floating sheet navigator panel backed by a C++ hierarchy export (sheet-panel)
Canvas-only eeschema sessions (read-only viewers, hide-UI editors) could not
reach the wx hierarchy pane, so a viewer of a hierarchical design was stuck
on the entry sheet. Adds the React stand-in (SheetPanel) + the bridge:
- eeschema_embind: kicadSheetsGetTree() (SCHEMATIC::Hierarchy(), page-number
ordered: instance KIID path / parent / Sheetname / file / page / depth) and
kicadSheetsEnter(path) (validated synchronously, applied on the coroutine
via SCH_ACTIONS::changeSheet — same route as the wx pane, history intact).
OnSchSheetChanged pushes window.kicadCollab.onSheetsState after EVERY
navigation, wx-driven included, so the panel is event-driven.
- kicad → 9ab93b838e: navigate actions allowlisted for read-only viewers.
- SheetPanel.tsx: LayerPanel shell conventions; rendered + menu row only when
the hierarchy has >1 sheet instance; viewers boot it collapsed.
- tests: fixtures/demo/hier (root → Power, IO → Sub) + web/sheet-panel.spec
(list/order/depth, navigate + collab rebind, menu toggle/close, flat
schematic has no panel, bridge-driven navigation updates the panel);
SheetPanel.test.ts for the wire parser.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CtN6ASBvMGNbjPqY5boycg
2026-08-29 16:24:57 +02:00
|
|
|
// ───────────────────────── sheet navigator bridge (sheet-panel) ─────────────────────────
|
|
|
|
|
//
|
|
|
|
|
// The React stand-in for the docked wx HIERARCHY_PANE that kicadSetChrome(false)
|
|
|
|
|
// hides: kicadSheetsGetTree() returns the loaded hierarchy (every SCH_SHEET_PATH
|
|
|
|
|
// instance — a sheet file placed twice is two rows, like the wx pane) plus the
|
|
|
|
|
// current path; kicadSheetsEnter(path) navigates. Same shape as pcbnew's layer
|
|
|
|
|
// bridge: validate synchronously, apply on the frame's coroutine, then push the
|
|
|
|
|
// fresh state to window.kicadCollab.onSheetsState so the panel is event-driven.
|
|
|
|
|
// Rows are page-number ordered (the wx pane's order); `parent` is the KIID path
|
|
|
|
|
// of the enclosing instance ("" for the root) so JS can rebuild the tree.
|
|
|
|
|
json sheetsStateJson( SCH_EDIT_FRAME* aFrame )
|
|
|
|
|
{
|
|
|
|
|
SCH_SHEET_LIST list = aFrame->Schematic().Hierarchy();
|
|
|
|
|
list.SortByPageNumbers( /* aUpdateVirtualPageNums */ false );
|
|
|
|
|
|
|
|
|
|
json sheets = json::array();
|
|
|
|
|
|
|
|
|
|
for( const SCH_SHEET_PATH& path : list )
|
|
|
|
|
{
|
|
|
|
|
SCH_SHEET_PATH parent = path;
|
|
|
|
|
|
|
|
|
|
if( parent.size() > 1 )
|
|
|
|
|
parent.pop_back();
|
|
|
|
|
|
|
|
|
|
SCH_SCREEN* screen = path.LastScreen();
|
|
|
|
|
SCH_SHEET* sheet = path.Last();
|
|
|
|
|
wxString name = sheet ? sheet->GetName() : wxString();
|
|
|
|
|
|
|
|
|
|
// The root sheet carries no Sheetname — show its file like the wx pane does.
|
|
|
|
|
if( name.IsEmpty() && screen )
|
|
|
|
|
name = wxFileName( screen->GetFileName() ).GetName();
|
|
|
|
|
|
|
|
|
|
sheets.push_back( {
|
|
|
|
|
{ "path", toUtf8( path.PathAsString() ) },
|
|
|
|
|
{ "parent", path.size() > 1 ? toUtf8( parent.PathAsString() ) : std::string() },
|
|
|
|
|
{ "name", toUtf8( name ) },
|
|
|
|
|
{ "file", screen ? toUtf8( screen->GetFileName() ) : std::string() },
|
|
|
|
|
{ "page", toUtf8( path.GetPageNumber() ) },
|
|
|
|
|
{ "depth", static_cast<int>( path.size() ) - 1 },
|
|
|
|
|
} );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return { { "current", toUtf8( aFrame->GetCurrentSheet().PathAsString() ) },
|
|
|
|
|
{ "sheets", sheets } };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void emitSheetsState( SCH_EDIT_FRAME* aFrame )
|
|
|
|
|
{
|
|
|
|
|
if( !aFrame )
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
std::string s = sheetsStateJson( aFrame ).dump();
|
|
|
|
|
EM_ASM( {
|
|
|
|
|
if( window.kicadCollab && window.kicadCollab.onSheetsState )
|
|
|
|
|
{
|
|
|
|
|
// Never let a throwing listener unwind the wasm frame (findings P-1).
|
|
|
|
|
try { window.kicadCollab.onSheetsState( UTF8ToString( $0 ) ); }
|
|
|
|
|
catch( e ) { console.error( '[pcbjam sheets] onSheetsState listener threw', e ); }
|
|
|
|
|
}
|
|
|
|
|
}, s.c_str() );
|
|
|
|
|
}
|
|
|
|
|
|
fix(ysync): wire dialect == file dialect, uuid churn, drift noise
The Y.Doc is the source of truth for the FILE, but both live wires wrote
KiCad's CLIPBOARD dialect — a lossy, paste-oriented format. Every
difference was permanent, unfixable drift.
- pcbnew: serialize footprint blobs with CTL_FOR_BOARD, not CLIPBOARD_IO's
CTL_FOR_CLIPBOARD, which emitted (version)(generator)(generator_version)
inside every (footprint …). Keep (locked yes).
- eeschema: aForClipboard=false — clipboard mode collapsed every symbol's
(instances … (path "/sheet")) to (path "").
- Re-supply (version) at PARSE time only (withFootprintVersion): the token
is invalid file content but load-bearing on decode — without it the
parser starts at m_requiredVersion=0 and stamps (hide yes) on every
mandatory field.
- FOOTPRINT copy ctor: restore mandatory-field uuids (EDA_ITEM::operator=
keeps the target's const m_Uuid, so Clone() rerolled all four).
- drift: classify order-only diffs as `reordered` — y-sexpr v2 reorders
legitimately; excluded from counts, report-worthiness and dedupe hashes.
Migration 0017.
- fpedit from eeschema: AsyncLoad()+BlockUntilLoaded() in initLibraryTree —
FACE_PCB starts lazily there and never preloaded its libraries.
Guards: wire-vs-file round-trip tests (pcbnew + eeschema),
fpedit-from-eeschema (verified red without the fix), symedit-from-eeschema.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016p9kjdGBdcpwUSjJ3q5xg2
2026-07-20 18:29:52 +02:00
|
|
|
// Serialize one live schematic item to its native s-expr via a one-item
|
|
|
|
|
// SCH_SELECTION through SCH_IO_KICAD_SEXPR::Format. For a symbol the output also
|
|
|
|
|
// carries its (lib_symbols …) definition (that prelude is emitted for any symbol
|
|
|
|
|
// in the selection, independent of aForClipboard).
|
|
|
|
|
//
|
|
|
|
|
// aForClipboard MUST be false. Clipboard mode is a LOSSY, paste-oriented dialect:
|
|
|
|
|
// it rewrites `(instances (project … (path …)))` relative to aRelativePath — so a
|
|
|
|
|
// symbol on the current sheet collapses to `(path "")` — takes the REFERENCE field
|
|
|
|
|
// from the per-sheet instance instead of the ordinal one, and keeps orphaned
|
|
|
|
|
// instance data (sch_io_kicad_sexpr.cpp saveSymbol: ~758-766, ~791-806, ~903).
|
|
|
|
|
// The Y.Doc is the source of truth for the FILE, so a wire blob must be byte-equal
|
|
|
|
|
// to that item's subtree in a full file save; clipboard form would silently strip
|
|
|
|
|
// every symbol's sheet path and unit/reference on materialize.
|
|
|
|
|
//
|
|
|
|
|
// aRelativePath is still required (Format wxCHECKs it non-null) but is unread on
|
|
|
|
|
// the aForClipboard=false path.
|
2026-06-11 13:55:15 +02:00
|
|
|
std::string itemBlob( SCH_EDIT_FRAME* aFrame, SCH_ITEM* aItem )
|
|
|
|
|
{
|
|
|
|
|
SCH_SELECTION sel;
|
|
|
|
|
sel.SetScreen( aFrame->GetScreen() );
|
|
|
|
|
sel.Add( aItem );
|
|
|
|
|
|
|
|
|
|
STRING_FORMATTER fmt;
|
|
|
|
|
SCH_IO_KICAD_SEXPR plugin;
|
|
|
|
|
plugin.Format( &sel, &aFrame->GetCurrentSheet(), aFrame->Schematic(), &fmt,
|
fix(ysync): wire dialect == file dialect, uuid churn, drift noise
The Y.Doc is the source of truth for the FILE, but both live wires wrote
KiCad's CLIPBOARD dialect — a lossy, paste-oriented format. Every
difference was permanent, unfixable drift.
- pcbnew: serialize footprint blobs with CTL_FOR_BOARD, not CLIPBOARD_IO's
CTL_FOR_CLIPBOARD, which emitted (version)(generator)(generator_version)
inside every (footprint …). Keep (locked yes).
- eeschema: aForClipboard=false — clipboard mode collapsed every symbol's
(instances … (path "/sheet")) to (path "").
- Re-supply (version) at PARSE time only (withFootprintVersion): the token
is invalid file content but load-bearing on decode — without it the
parser starts at m_requiredVersion=0 and stamps (hide yes) on every
mandatory field.
- FOOTPRINT copy ctor: restore mandatory-field uuids (EDA_ITEM::operator=
keeps the target's const m_Uuid, so Clone() rerolled all four).
- drift: classify order-only diffs as `reordered` — y-sexpr v2 reorders
legitimately; excluded from counts, report-worthiness and dedupe hashes.
Migration 0017.
- fpedit from eeschema: AsyncLoad()+BlockUntilLoaded() in initLibraryTree —
FACE_PCB starts lazily there and never preloaded its libraries.
Guards: wire-vs-file round-trip tests (pcbnew + eeschema),
fpedit-from-eeschema (verified red without the fix), symedit-from-eeschema.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016p9kjdGBdcpwUSjJ3q5xg2
2026-07-20 18:29:52 +02:00
|
|
|
/*aForClipboard*/ false );
|
2026-06-11 13:55:15 +02:00
|
|
|
return fmt.GetString();
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-05 13:30:47 +02:00
|
|
|
// ── Emit via post-settle snapshot diff ───────────────────────────────────────────────────
|
2026-06-05 12:42:58 +02:00
|
|
|
//
|
2026-06-05 13:30:47 +02:00
|
|
|
// A local edit is a single SCH_COMMIT::Push that fires OnItemsAdded/Removed/Changed
|
|
|
|
|
// synchronously and THEN runs RecalculateConnections (sch_commit.cpp ~402-430). So the native
|
|
|
|
|
// listener only ever sees the *pre-cleanup* (raw) geometry, while the connectivity cleanup
|
|
|
|
|
// that follows — merging collinear wires, dropping redundant junctions, splitting at new
|
|
|
|
|
// crossings — is never reported. Broadcasting those raw per-category lists made the peer
|
|
|
|
|
// reconstruct the edit from the raw state and run ITS OWN cleanup, over a different "dirty"
|
|
|
|
|
// scope, so on a big connected drag the two peers cleaned up differently and the peer lost
|
|
|
|
|
// segments/junctions.
|
|
|
|
|
//
|
|
|
|
|
// Instead, treat the listener purely as a "something changed" trigger and broadcast a DIFF of
|
|
|
|
|
// the full model taken AFTER the edit settles — a CallAfter, which runs once Push (cleanup
|
|
|
|
|
// included) has fully returned. That captures tab A's FINAL, already-clean geometry; the peer
|
|
|
|
|
// applies it and re-cleaning already-clean geometry is idempotent, so the two converge. (This
|
|
|
|
|
// mirrors pl_editor's snapshot-differ.) g_baseline is the last-broadcast state.
|
|
|
|
|
|
2026-06-17 17:53:38 +02:00
|
|
|
// Diff baseline of the ACTIVE screen only (per-sheet collab room scope), keyed by uuid.
|
|
|
|
|
std::map<std::string, json> snapshotByUuid( SCH_EDIT_FRAME* aFrame )
|
2026-06-03 17:49:11 +02:00
|
|
|
{
|
2026-06-05 13:30:47 +02:00
|
|
|
std::map<std::string, json> m;
|
2026-06-03 17:49:11 +02:00
|
|
|
|
2026-06-17 17:53:38 +02:00
|
|
|
if( SCH_SCREEN* screen = currentScreen( aFrame ) )
|
2026-06-03 17:49:11 +02:00
|
|
|
{
|
2026-06-05 13:30:47 +02:00
|
|
|
for( SCH_ITEM* item : screen->Items() )
|
2026-06-17 17:53:38 +02:00
|
|
|
m[toUtf8( item->m_Uuid.AsString() )] = itemToJson( item );
|
2026-06-03 17:49:11 +02:00
|
|
|
}
|
|
|
|
|
|
2026-06-05 13:30:47 +02:00
|
|
|
return m;
|
|
|
|
|
}
|
2026-06-03 17:49:11 +02:00
|
|
|
|
2026-06-05 13:30:47 +02:00
|
|
|
std::map<std::string, json> g_baseline;
|
|
|
|
|
bool g_flushScheduled = false;
|
2026-06-05 12:42:58 +02:00
|
|
|
|
fix(ysync): review bugs 01–07 — listener registration, pad nets, child removals, dirty-set emit, targeted rebaseline, seed arbitration, stale hook (doc 17)
All seven ysync-review bugs fixed and verified; every repro's expected-fail
marker removed (they now run as regression tests). Also fixes two bugs found
while verifying (doc 17 F5/F6): file-seeded Y bodies are re-upserted in the
editor's serialization (doc-16 F4 was an artifact of bug 01), and the
0008-era "asyncify-fragile envelope parse" was really wrapInBoardEnvelope
emitting display layer names — canonical LSET::Name() fixes track/via/zone
v2 applies; makeFromBlob now logs parse errors instead of swallowing them.
Verified: shared 98, standalone collab 37, ysync e2e 20/20 (chromium),
collab regression set 21 passed / 3 pre-existing skips (firefox).
Bumps: kicad (board_commit child-removal listener notification),
web/pcbjam-shared (slot prune + arbitrated seed).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JgThWXtdvrYLK47EDFoGdq
2026-07-03 18:08:37 +02:00
|
|
|
// Roots the listener saw change since the last flush (uuids, captured at callback
|
|
|
|
|
// time — removed items may be freed before the flush runs). The scalar snapshot
|
|
|
|
|
// diff is a LOSSY projection: rotations/mirrors (anchor unchanged), field text
|
|
|
|
|
// edits (the commit stages the SCH_FIELD, which is not a screen item), stroke
|
|
|
|
|
// properties etc. never move it (bug 04). Dirty roots emit their v2 blob
|
|
|
|
|
// unconditionally; the apply is an idempotent upsert and the TS layer drops
|
|
|
|
|
// no-op bodies, so a false positive costs one local serialization.
|
|
|
|
|
std::set<std::string> g_dirty;
|
|
|
|
|
|
|
|
|
|
// Lift a commit-staged item to the SCREEN item the differ tracks (a field/pin/cell
|
|
|
|
|
// lifts to its symbol/sheet/label/table — same promotion sch_commit's undo uses).
|
|
|
|
|
void noteDirty( SCH_ITEM* aItem )
|
|
|
|
|
{
|
|
|
|
|
if( !aItem )
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
while( EDA_ITEM* p = aItem->GetParent() )
|
|
|
|
|
{
|
|
|
|
|
if( !p->IsType( { SCH_SYMBOL_T, SCH_TABLE_T, SCH_SHEET_T, SCH_LABEL_LOCATE_ANY_T } ) )
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
aItem = static_cast<SCH_ITEM*>( p );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
g_dirty.insert( toUtf8( aItem->m_Uuid.AsString() ) );
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-05 13:30:47 +02:00
|
|
|
// Re-seed the diff baseline to the current model — after handing out a seed snapshot, or after
|
|
|
|
|
// applying a remote delta (so those items aren't re-broadcast as a spurious local diff/echo).
|
fix(ysync): review bugs 01–07 — listener registration, pad nets, child removals, dirty-set emit, targeted rebaseline, seed arbitration, stale hook (doc 17)
All seven ysync-review bugs fixed and verified; every repro's expected-fail
marker removed (they now run as regression tests). Also fixes two bugs found
while verifying (doc 17 F5/F6): file-seeded Y bodies are re-upserted in the
editor's serialization (doc-16 F4 was an artifact of bug 01), and the
0008-era "asyncify-fragile envelope parse" was really wrapInBoardEnvelope
emitting display layer names — canonical LSET::Name() fixes track/via/zone
v2 applies; makeFromBlob now logs parse errors instead of swallowing them.
Verified: shared 98, standalone collab 37, ysync e2e 20/20 (chromium),
collab regression set 21 passed / 3 pre-existing skips (firefox).
Bumps: kicad (board_commit child-removal listener notification),
web/pcbjam-shared (slot prune + arbitrated seed).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JgThWXtdvrYLK47EDFoGdq
2026-07-03 18:08:37 +02:00
|
|
|
// Declares "current model == broadcast state", so pending dirty marks are stale too — on a
|
|
|
|
|
// sheet switch they'd otherwise emit the OLD sheet's items into the new sheet's room.
|
2026-06-05 13:30:47 +02:00
|
|
|
void rebaseline()
|
|
|
|
|
{
|
|
|
|
|
if( SCH_EDIT_FRAME* fr = schFrame() )
|
2026-06-17 17:53:38 +02:00
|
|
|
g_baseline = snapshotByUuid( fr );
|
fix(ysync): review bugs 01–07 — listener registration, pad nets, child removals, dirty-set emit, targeted rebaseline, seed arbitration, stale hook (doc 17)
All seven ysync-review bugs fixed and verified; every repro's expected-fail
marker removed (they now run as regression tests). Also fixes two bugs found
while verifying (doc 17 F5/F6): file-seeded Y bodies are re-upserted in the
editor's serialization (doc-16 F4 was an artifact of bug 01), and the
0008-era "asyncify-fragile envelope parse" was really wrapInBoardEnvelope
emitting display layer names — canonical LSET::Name() fixes track/via/zone
v2 applies; makeFromBlob now logs parse errors instead of swallowing them.
Verified: shared 98, standalone collab 37, ysync e2e 20/20 (chromium),
collab regression set 21 passed / 3 pre-existing skips (firefox).
Bumps: kicad (board_commit child-removal listener notification),
web/pcbjam-shared (slot prune + arbitrated seed).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JgThWXtdvrYLK47EDFoGdq
2026-07-03 18:08:37 +02:00
|
|
|
|
|
|
|
|
g_dirty.clear();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TARGETED rebaseline (bug 05): refresh baseline entries ONLY for the uuids a remote
|
|
|
|
|
// apply touched. A global rebaseline() would fold a concurrently-committed local edit
|
|
|
|
|
// (its flush is queued BEHIND the apply on the same pending-event list) into the
|
|
|
|
|
// baseline and silently swallow it; targeted, the edit still diffs and emits. The
|
|
|
|
|
// connectivity cleanup the apply's Push produced likewise stays diffable — the
|
|
|
|
|
// post-apply flush broadcasts it (idempotent on the original sender).
|
|
|
|
|
void rebaselineTouched( SCH_EDIT_FRAME* aFrame, const std::vector<std::string>& aIds )
|
|
|
|
|
{
|
|
|
|
|
for( const std::string& id : aIds )
|
|
|
|
|
{
|
|
|
|
|
g_baseline.erase( id );
|
|
|
|
|
|
|
|
|
|
KIID kid( wxString::FromUTF8( id.c_str() ) );
|
|
|
|
|
|
|
|
|
|
if( SCH_ITEM* live = aFrame->Schematic().ResolveItem( kid, nullptr, /*allowNull*/ true ) )
|
|
|
|
|
g_baseline[id] = itemToJson( live );
|
|
|
|
|
}
|
2026-06-05 13:30:47 +02:00
|
|
|
}
|
2026-06-05 12:42:58 +02:00
|
|
|
|
2026-06-05 13:30:47 +02:00
|
|
|
// Diff the current (settled, post-cleanup) model against the baseline and broadcast the change.
|
|
|
|
|
void flushDiff()
|
|
|
|
|
{
|
|
|
|
|
g_flushScheduled = false;
|
|
|
|
|
|
|
|
|
|
SCH_EDIT_FRAME* fr = schFrame();
|
2026-06-03 17:49:11 +02:00
|
|
|
|
2026-06-05 13:30:47 +02:00
|
|
|
if( !fr )
|
|
|
|
|
return;
|
2026-06-05 12:42:58 +02:00
|
|
|
|
2026-06-17 17:53:38 +02:00
|
|
|
std::map<std::string, json> cur = snapshotByUuid( fr );
|
2026-06-05 13:30:47 +02:00
|
|
|
|
|
|
|
|
json added = json::array(), changed = json::array(), removed = json::array();
|
|
|
|
|
|
2026-06-11 13:55:15 +02:00
|
|
|
// v2 items wire (per-item s-expr blobs), built from the same diff. Screen items
|
|
|
|
|
// are already root-level (fields live inside their symbols), so no lifting.
|
fix(ysync): review bugs 01–07 — listener registration, pad nets, child removals, dirty-set emit, targeted rebaseline, seed arbitration, stale hook (doc 17)
All seven ysync-review bugs fixed and verified; every repro's expected-fail
marker removed (they now run as regression tests). Also fixes two bugs found
while verifying (doc 17 F5/F6): file-seeded Y bodies are re-upserted in the
editor's serialization (doc-16 F4 was an artifact of bug 01), and the
0008-era "asyncify-fragile envelope parse" was really wrapInBoardEnvelope
emitting display layer names — canonical LSET::Name() fixes track/via/zone
v2 applies; makeFromBlob now logs parse errors instead of swallowing them.
Verified: shared 98, standalone collab 37, ysync e2e 20/20 (chromium),
collab regression set 21 passed / 3 pre-existing skips (firefox).
Bumps: kicad (board_commit child-removal listener notification),
web/pcbjam-shared (slot prune + arbitrated seed).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JgThWXtdvrYLK47EDFoGdq
2026-07-03 18:08:37 +02:00
|
|
|
json wAdded = json::array(), wChanged = json::array();
|
|
|
|
|
std::set<std::string> wDone;
|
2026-06-11 13:55:15 +02:00
|
|
|
|
|
|
|
|
auto blobFor = [&]( const std::string& id, json& aArr )
|
|
|
|
|
{
|
fix(ysync): review bugs 01–07 — listener registration, pad nets, child removals, dirty-set emit, targeted rebaseline, seed arbitration, stale hook (doc 17)
All seven ysync-review bugs fixed and verified; every repro's expected-fail
marker removed (they now run as regression tests). Also fixes two bugs found
while verifying (doc 17 F5/F6): file-seeded Y bodies are re-upserted in the
editor's serialization (doc-16 F4 was an artifact of bug 01), and the
0008-era "asyncify-fragile envelope parse" was really wrapInBoardEnvelope
emitting display layer names — canonical LSET::Name() fixes track/via/zone
v2 applies; makeFromBlob now logs parse errors instead of swallowing them.
Verified: shared 98, standalone collab 37, ysync e2e 20/20 (chromium),
collab regression set 21 passed / 3 pre-existing skips (firefox).
Bumps: kicad (board_commit child-removal listener notification),
web/pcbjam-shared (slot prune + arbitrated seed).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JgThWXtdvrYLK47EDFoGdq
2026-07-03 18:08:37 +02:00
|
|
|
if( !wDone.insert( id ).second )
|
|
|
|
|
return;
|
|
|
|
|
|
2026-06-11 13:55:15 +02:00
|
|
|
KIID kid( wxString::FromUTF8( id.c_str() ) );
|
|
|
|
|
|
|
|
|
|
if( SCH_ITEM* item = fr->Schematic().ResolveItem( kid, nullptr, /*allowNull*/ true ) )
|
|
|
|
|
aArr.push_back( json{ { "sexpr", itemBlob( fr, item ) }, { "parent", nullptr } } );
|
|
|
|
|
};
|
|
|
|
|
|
2026-06-05 13:30:47 +02:00
|
|
|
for( const auto& [id, j] : cur )
|
|
|
|
|
{
|
|
|
|
|
auto it = g_baseline.find( id );
|
|
|
|
|
|
|
|
|
|
if( it == g_baseline.end() )
|
2026-06-11 13:55:15 +02:00
|
|
|
{
|
2026-06-05 13:30:47 +02:00
|
|
|
added.push_back( j );
|
2026-06-11 13:55:15 +02:00
|
|
|
blobFor( id, wAdded );
|
|
|
|
|
}
|
2026-06-05 13:30:47 +02:00
|
|
|
else if( it->second != j )
|
2026-06-11 13:55:15 +02:00
|
|
|
{
|
2026-06-05 13:30:47 +02:00
|
|
|
changed.push_back( j );
|
2026-06-11 13:55:15 +02:00
|
|
|
blobFor( id, wChanged );
|
|
|
|
|
}
|
2026-06-05 12:42:58 +02:00
|
|
|
}
|
|
|
|
|
|
2026-06-05 13:30:47 +02:00
|
|
|
for( const auto& [id, j] : g_baseline )
|
2026-06-05 12:42:58 +02:00
|
|
|
{
|
2026-06-05 13:30:47 +02:00
|
|
|
if( !cur.count( id ) )
|
|
|
|
|
removed.push_back( id );
|
|
|
|
|
}
|
2026-06-05 12:42:58 +02:00
|
|
|
|
fix(ysync): review bugs 01–07 — listener registration, pad nets, child removals, dirty-set emit, targeted rebaseline, seed arbitration, stale hook (doc 17)
All seven ysync-review bugs fixed and verified; every repro's expected-fail
marker removed (they now run as regression tests). Also fixes two bugs found
while verifying (doc 17 F5/F6): file-seeded Y bodies are re-upserted in the
editor's serialization (doc-16 F4 was an artifact of bug 01), and the
0008-era "asyncify-fragile envelope parse" was really wrapInBoardEnvelope
emitting display layer names — canonical LSET::Name() fixes track/via/zone
v2 applies; makeFromBlob now logs parse errors instead of swallowing them.
Verified: shared 98, standalone collab 37, ysync e2e 20/20 (chromium),
collab regression set 21 passed / 3 pre-existing skips (firefox).
Bumps: kicad (board_commit child-removal listener notification),
web/pcbjam-shared (slot prune + arbitrated seed).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JgThWXtdvrYLK47EDFoGdq
2026-07-03 18:08:37 +02:00
|
|
|
// Dirty roots (bug 04): whatever the listener saw commit emits its blob on the
|
|
|
|
|
// v2 wire even when the scalar projection didn't move (rotation, field text,
|
|
|
|
|
// stroke edits). wDone dedups against the scalar-diff emits; deleted ids
|
|
|
|
|
// resolve null inside blobFor and skip (the removal loop covered them).
|
|
|
|
|
for( const std::string& id : g_dirty )
|
|
|
|
|
blobFor( id, wChanged );
|
|
|
|
|
|
|
|
|
|
g_dirty.clear();
|
|
|
|
|
|
2026-06-05 13:30:47 +02:00
|
|
|
g_baseline = std::move( cur );
|
2026-06-03 17:49:11 +02:00
|
|
|
|
2026-06-05 13:30:47 +02:00
|
|
|
if( !added.empty() || !changed.empty() || !removed.empty() )
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
emitDelta( json{ { "added", added }, { "changed", changed }, { "removed", removed } } );
|
fix(ysync): review bugs 01–07 — listener registration, pad nets, child removals, dirty-set emit, targeted rebaseline, seed arbitration, stale hook (doc 17)
All seven ysync-review bugs fixed and verified; every repro's expected-fail
marker removed (they now run as regression tests). Also fixes two bugs found
while verifying (doc 17 F5/F6): file-seeded Y bodies are re-upserted in the
editor's serialization (doc-16 F4 was an artifact of bug 01), and the
0008-era "asyncify-fragile envelope parse" was really wrapInBoardEnvelope
emitting display layer names — canonical LSET::Name() fixes track/via/zone
v2 applies; makeFromBlob now logs parse errors instead of swallowing them.
Verified: shared 98, standalone collab 37, ysync e2e 20/20 (chromium),
collab regression set 21 passed / 3 pre-existing skips (firefox).
Bumps: kicad (board_commit child-removal listener notification),
web/pcbjam-shared (slot prune + arbitrated seed).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JgThWXtdvrYLK47EDFoGdq
2026-07-03 18:08:37 +02:00
|
|
|
|
|
|
|
|
if( !wAdded.empty() || !wChanged.empty() || !removed.empty() )
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
emitItemsWire( json{ { "added", wAdded }, { "changed", wChanged }, { "removed", removed } } );
|
2026-06-05 13:30:47 +02:00
|
|
|
}
|
2026-06-05 12:42:58 +02:00
|
|
|
|
2026-06-05 13:30:47 +02:00
|
|
|
// Coalesce all the listener callbacks of one commit (and any other edits in the same loop
|
jspi cleanup: remove the asyncify-era residue — dead code, conditionals, pipeline scaffolding, stale prose
The runtime is JSPI-only; this removes everything that still pretended
otherwise. Three exhaustive sweeps (C++/JS+build+CI/tests+docs) drove
the inventory; every deletion verified by grep closure + full gates.
Broken-right-now fixes:
- deploy-staging.yml passed the retired opt_level input — the workflow
could not even start. Removed.
- env.sh carried dead exports with a live -sASYNCIFY=1 inside
(WASM_LDFLAGS/PTHREAD_LDFLAGS, zero consumers). Removed; the
WASM_LEGACY_EXCEPTIONS rationale rewritten to the real reason.
- docker/build.sh exported PCBJAM_ASYNC_BACKEND (read nowhere). Gone.
Dead weight removed:
- binaryen submodule (nothing builds or invokes it), wasm-opt-bench
workflow + scripts/bench/, get-wasm-opt.sh, diagnostics.js (242 lines
of Asyncify-API-only code), the KICAD_PIPELINE background-postprocess
scaffolding (existed to parallelize the deleted wasm-opt phase; the
postprocess is a seconds-long node script and now runs inline),
build-monitor's dead asyncify rows, sched-context orphan build
output, dead .gitignore entries, the .jspi-assets spike dir (the two
wf-result research JSONs moved to docs/features/async/migration-evidence/).
- bindings: fiber_park.h + its 12 embind registrations (broken-if-
called under JSPI), the kicadOpenFileStart/OPEN_JOB starter route,
main_stack_runner.h + 5 includes, the always-null context-sleep weak
hook in nanosleep_yield.c.
- shim: the backend field (installed-flag idempotency instead),
noteContextWait (dead both sides), the __wxAsyncifyDump alias (+ the
WasmTool fallback and string-dump normalize branch).
- web: the emscripten-6-ignored mainScriptUrlOrBlob option in boot.ts
(gerber-demo keeps it: it loads the deployed CDN release, which
predates emscripten 6 — noted inline).
Conditionals: all 'backend === jspi' checks reduced to scheduler-
presence checks; races_quiescent re-keyed from Asyncify.state (vacuous)
to real backlog quiescence (resumeReady/mutatorQueue — NOT _windowLive,
which is the probing activation's own window by definition).
Renames (identifiers only, no file renames): ASYNC_LINK_FLAGS→
JSPI_LINK_FLAGS and Makefile ASYNC_LDFLAGS→JSPI_LDFLAGS,
kicadCollabFiberBusy→kicadCollabBusy (embind + web + tests),
collab_common.h fiber*→apply*/coroutine naming, asyncifySignatures→
wasmTrapSignatures (lists byte-identical).
Tests: the two remaining vacuous [wx-asyncify]/fiber-resume-refused
asserts re-keyed to live JSPI beacons; eeschema-load's failure message
no longer sends the developer to a deleted script; wait-beacons' dead
families/parser deleted; lane-0 legacy-glue guards removed (lane 0 is
unconstructible); the embind test.fail re-gated with the JSPI reason
(plain embind invokers cannot suspend — verified still failing);
lint-determinism now scans tests/jspi (166 files clean);
eeschema-collab local-move gated to chromium (~50% flaky on FF even
solo; pcbnew twin covers both engines).
Docs: DEBUG.md rewritten as the JSPI debugging guide; build.md
describes the single-phase build; docs/features/async/README.md
banner-marked historical and repointed at the NEW
23-jspi-runtime.md (current architecture: export census, turnstile,
libcontext ownership + refusal contract, embind call shapes, the
em-pthread service-wrapper trick, exception policy, known gaps).
Gates on the cleaned tree: test:e2e 725 passed / 0 failed (after the
quiescence-probe fix; the 3 other reds were verified contention flakes
solo-green or the documented FF gate), web 76/0, jspi 18/18 both
engines, vitest 295/295 + 17/17, all lints green, live-app census
clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016X9eh1s5sTx1o9Em9KBuwR
2026-08-14 09:25:32 +02:00
|
|
|
// turn) into a single post-settle diff. flushDiff runs inside a COROUTINE via the apply
|
|
|
|
|
// queue: its v2 items emit serializes items via SCH_IO_KICAD_SEXPR::Format (itemBlob), and
|
|
|
|
|
// queueing it with the applies keeps emits from interleaving with a suspended apply body
|
|
|
|
|
// (under the retired asyncify runtime the bare CallAfter stack additionally trapped here —
|
|
|
|
|
// same lesson as doApply, 0007).
|
2026-06-05 13:30:47 +02:00
|
|
|
void scheduleFlush()
|
|
|
|
|
{
|
|
|
|
|
if( g_flushScheduled )
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
g_flushScheduled = true;
|
|
|
|
|
|
|
|
|
|
if( SCH_EDIT_FRAME* fr = schFrame() )
|
jspi cleanup: remove the asyncify-era residue — dead code, conditionals, pipeline scaffolding, stale prose
The runtime is JSPI-only; this removes everything that still pretended
otherwise. Three exhaustive sweeps (C++/JS+build+CI/tests+docs) drove
the inventory; every deletion verified by grep closure + full gates.
Broken-right-now fixes:
- deploy-staging.yml passed the retired opt_level input — the workflow
could not even start. Removed.
- env.sh carried dead exports with a live -sASYNCIFY=1 inside
(WASM_LDFLAGS/PTHREAD_LDFLAGS, zero consumers). Removed; the
WASM_LEGACY_EXCEPTIONS rationale rewritten to the real reason.
- docker/build.sh exported PCBJAM_ASYNC_BACKEND (read nowhere). Gone.
Dead weight removed:
- binaryen submodule (nothing builds or invokes it), wasm-opt-bench
workflow + scripts/bench/, get-wasm-opt.sh, diagnostics.js (242 lines
of Asyncify-API-only code), the KICAD_PIPELINE background-postprocess
scaffolding (existed to parallelize the deleted wasm-opt phase; the
postprocess is a seconds-long node script and now runs inline),
build-monitor's dead asyncify rows, sched-context orphan build
output, dead .gitignore entries, the .jspi-assets spike dir (the two
wf-result research JSONs moved to docs/features/async/migration-evidence/).
- bindings: fiber_park.h + its 12 embind registrations (broken-if-
called under JSPI), the kicadOpenFileStart/OPEN_JOB starter route,
main_stack_runner.h + 5 includes, the always-null context-sleep weak
hook in nanosleep_yield.c.
- shim: the backend field (installed-flag idempotency instead),
noteContextWait (dead both sides), the __wxAsyncifyDump alias (+ the
WasmTool fallback and string-dump normalize branch).
- web: the emscripten-6-ignored mainScriptUrlOrBlob option in boot.ts
(gerber-demo keeps it: it loads the deployed CDN release, which
predates emscripten 6 — noted inline).
Conditionals: all 'backend === jspi' checks reduced to scheduler-
presence checks; races_quiescent re-keyed from Asyncify.state (vacuous)
to real backlog quiescence (resumeReady/mutatorQueue — NOT _windowLive,
which is the probing activation's own window by definition).
Renames (identifiers only, no file renames): ASYNC_LINK_FLAGS→
JSPI_LINK_FLAGS and Makefile ASYNC_LDFLAGS→JSPI_LDFLAGS,
kicadCollabFiberBusy→kicadCollabBusy (embind + web + tests),
collab_common.h fiber*→apply*/coroutine naming, asyncifySignatures→
wasmTrapSignatures (lists byte-identical).
Tests: the two remaining vacuous [wx-asyncify]/fiber-resume-refused
asserts re-keyed to live JSPI beacons; eeschema-load's failure message
no longer sends the developer to a deleted script; wait-beacons' dead
families/parser deleted; lane-0 legacy-glue guards removed (lane 0 is
unconstructible); the embind test.fail re-gated with the JSPI reason
(plain embind invokers cannot suspend — verified still failing);
lint-determinism now scans tests/jspi (166 files clean);
eeschema-collab local-move gated to chromium (~50% flaky on FF even
solo; pcbnew twin covers both engines).
Docs: DEBUG.md rewritten as the JSPI debugging guide; build.md
describes the single-phase build; docs/features/async/README.md
banner-marked historical and repointed at the NEW
23-jspi-runtime.md (current architecture: export census, turnstile,
libcontext ownership + refusal contract, embind call shapes, the
em-pthread service-wrapper trick, exception policy, known gaps).
Gates on the cleaned tree: test:e2e 725 passed / 0 failed (after the
quiescence-probe fix; the 3 other reds were verified contention flakes
solo-green or the documented FF gate), web 76/0, jspi 18/18 both
engines, vitest 295/295 + 17/17, all lints green, live-app census
clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016X9eh1s5sTx1o9Em9KBuwR
2026-08-14 09:25:32 +02:00
|
|
|
pcbjam_collab::runOnCoroutine( fr, []() { flushDiff(); } );
|
2026-06-05 13:30:47 +02:00
|
|
|
else
|
|
|
|
|
flushDiff();
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-17 17:53:38 +02:00
|
|
|
// A hierarchical sheet was just created locally ("Add Sheet"): write its new child screen
|
|
|
|
|
// to the child .kicad_sch file and tell the standalone (window.kicadCollab.onSheetCreated),
|
|
|
|
|
// so the child is persisted/registered the moment it's created — without waiting for the
|
|
|
|
|
// user to enter it or save the project. Otherwise the parent's `(sheet … child)` reference
|
jspi cleanup: remove the asyncify-era residue — dead code, conditionals, pipeline scaffolding, stale prose
The runtime is JSPI-only; this removes everything that still pretended
otherwise. Three exhaustive sweeps (C++/JS+build+CI/tests+docs) drove
the inventory; every deletion verified by grep closure + full gates.
Broken-right-now fixes:
- deploy-staging.yml passed the retired opt_level input — the workflow
could not even start. Removed.
- env.sh carried dead exports with a live -sASYNCIFY=1 inside
(WASM_LDFLAGS/PTHREAD_LDFLAGS, zero consumers). Removed; the
WASM_LEGACY_EXCEPTIONS rationale rewritten to the real reason.
- docker/build.sh exported PCBJAM_ASYNC_BACKEND (read nowhere). Gone.
Dead weight removed:
- binaryen submodule (nothing builds or invokes it), wasm-opt-bench
workflow + scripts/bench/, get-wasm-opt.sh, diagnostics.js (242 lines
of Asyncify-API-only code), the KICAD_PIPELINE background-postprocess
scaffolding (existed to parallelize the deleted wasm-opt phase; the
postprocess is a seconds-long node script and now runs inline),
build-monitor's dead asyncify rows, sched-context orphan build
output, dead .gitignore entries, the .jspi-assets spike dir (the two
wf-result research JSONs moved to docs/features/async/migration-evidence/).
- bindings: fiber_park.h + its 12 embind registrations (broken-if-
called under JSPI), the kicadOpenFileStart/OPEN_JOB starter route,
main_stack_runner.h + 5 includes, the always-null context-sleep weak
hook in nanosleep_yield.c.
- shim: the backend field (installed-flag idempotency instead),
noteContextWait (dead both sides), the __wxAsyncifyDump alias (+ the
WasmTool fallback and string-dump normalize branch).
- web: the emscripten-6-ignored mainScriptUrlOrBlob option in boot.ts
(gerber-demo keeps it: it loads the deployed CDN release, which
predates emscripten 6 — noted inline).
Conditionals: all 'backend === jspi' checks reduced to scheduler-
presence checks; races_quiescent re-keyed from Asyncify.state (vacuous)
to real backlog quiescence (resumeReady/mutatorQueue — NOT _windowLive,
which is the probing activation's own window by definition).
Renames (identifiers only, no file renames): ASYNC_LINK_FLAGS→
JSPI_LINK_FLAGS and Makefile ASYNC_LDFLAGS→JSPI_LDFLAGS,
kicadCollabFiberBusy→kicadCollabBusy (embind + web + tests),
collab_common.h fiber*→apply*/coroutine naming, asyncifySignatures→
wasmTrapSignatures (lists byte-identical).
Tests: the two remaining vacuous [wx-asyncify]/fiber-resume-refused
asserts re-keyed to live JSPI beacons; eeschema-load's failure message
no longer sends the developer to a deleted script; wait-beacons' dead
families/parser deleted; lane-0 legacy-glue guards removed (lane 0 is
unconstructible); the embind test.fail re-gated with the JSPI reason
(plain embind invokers cannot suspend — verified still failing);
lint-determinism now scans tests/jspi (166 files clean);
eeschema-collab local-move gated to chromium (~50% flaky on FF even
solo; pcbnew twin covers both engines).
Docs: DEBUG.md rewritten as the JSPI debugging guide; build.md
describes the single-phase build; docs/features/async/README.md
banner-marked historical and repointed at the NEW
23-jspi-runtime.md (current architecture: export census, turnstile,
libcontext ownership + refusal contract, embind call shapes, the
em-pthread service-wrapper trick, exception policy, known gaps).
Gates on the cleaned tree: test:e2e 725 passed / 0 failed (after the
quiescence-probe fix; the 3 other reds were verified contention flakes
solo-green or the documented FF gate), web 76/0, jspi 18/18 both
engines, vitest 295/295 + 17/17, all lints green, live-app census
clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016X9eh1s5sTx1o9Em9KBuwR
2026-08-14 09:25:32 +02:00
|
|
|
// dangles for peers / on reload. Deferred onto the apply coroutine (CallAfter + COROUTINE),
|
|
|
|
|
// same as flushDiff/doApply, so the save serializes with any in-flight apply.
|
|
|
|
|
// The sheet is re-resolved by uuid in the deferred body so a
|
2026-06-17 17:53:38 +02:00
|
|
|
// since-deleted sheet (e.g. an immediate undo) is a no-op rather than a dangling pointer.
|
|
|
|
|
void scheduleSheetSave( SCH_SHEET* aSheet )
|
|
|
|
|
{
|
|
|
|
|
SCH_EDIT_FRAME* fr = schFrame();
|
|
|
|
|
SCH_SCREEN* parent = currentScreen( fr );
|
|
|
|
|
|
|
|
|
|
if( !fr || !parent || !aSheet->GetScreen() )
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
wxFileName childFn( aSheet->GetFileName() ); // relative "Sheetfile"
|
|
|
|
|
childFn.MakeAbsolute( wxFileName( parent->GetFileName() ).GetPath() );
|
|
|
|
|
|
|
|
|
|
std::string childAbs = toUtf8( childFn.GetFullPath() );
|
|
|
|
|
std::string uuid = toUtf8( aSheet->m_Uuid.AsString() );
|
|
|
|
|
|
jspi cleanup: remove the asyncify-era residue — dead code, conditionals, pipeline scaffolding, stale prose
The runtime is JSPI-only; this removes everything that still pretended
otherwise. Three exhaustive sweeps (C++/JS+build+CI/tests+docs) drove
the inventory; every deletion verified by grep closure + full gates.
Broken-right-now fixes:
- deploy-staging.yml passed the retired opt_level input — the workflow
could not even start. Removed.
- env.sh carried dead exports with a live -sASYNCIFY=1 inside
(WASM_LDFLAGS/PTHREAD_LDFLAGS, zero consumers). Removed; the
WASM_LEGACY_EXCEPTIONS rationale rewritten to the real reason.
- docker/build.sh exported PCBJAM_ASYNC_BACKEND (read nowhere). Gone.
Dead weight removed:
- binaryen submodule (nothing builds or invokes it), wasm-opt-bench
workflow + scripts/bench/, get-wasm-opt.sh, diagnostics.js (242 lines
of Asyncify-API-only code), the KICAD_PIPELINE background-postprocess
scaffolding (existed to parallelize the deleted wasm-opt phase; the
postprocess is a seconds-long node script and now runs inline),
build-monitor's dead asyncify rows, sched-context orphan build
output, dead .gitignore entries, the .jspi-assets spike dir (the two
wf-result research JSONs moved to docs/features/async/migration-evidence/).
- bindings: fiber_park.h + its 12 embind registrations (broken-if-
called under JSPI), the kicadOpenFileStart/OPEN_JOB starter route,
main_stack_runner.h + 5 includes, the always-null context-sleep weak
hook in nanosleep_yield.c.
- shim: the backend field (installed-flag idempotency instead),
noteContextWait (dead both sides), the __wxAsyncifyDump alias (+ the
WasmTool fallback and string-dump normalize branch).
- web: the emscripten-6-ignored mainScriptUrlOrBlob option in boot.ts
(gerber-demo keeps it: it loads the deployed CDN release, which
predates emscripten 6 — noted inline).
Conditionals: all 'backend === jspi' checks reduced to scheduler-
presence checks; races_quiescent re-keyed from Asyncify.state (vacuous)
to real backlog quiescence (resumeReady/mutatorQueue — NOT _windowLive,
which is the probing activation's own window by definition).
Renames (identifiers only, no file renames): ASYNC_LINK_FLAGS→
JSPI_LINK_FLAGS and Makefile ASYNC_LDFLAGS→JSPI_LDFLAGS,
kicadCollabFiberBusy→kicadCollabBusy (embind + web + tests),
collab_common.h fiber*→apply*/coroutine naming, asyncifySignatures→
wasmTrapSignatures (lists byte-identical).
Tests: the two remaining vacuous [wx-asyncify]/fiber-resume-refused
asserts re-keyed to live JSPI beacons; eeschema-load's failure message
no longer sends the developer to a deleted script; wait-beacons' dead
families/parser deleted; lane-0 legacy-glue guards removed (lane 0 is
unconstructible); the embind test.fail re-gated with the JSPI reason
(plain embind invokers cannot suspend — verified still failing);
lint-determinism now scans tests/jspi (166 files clean);
eeschema-collab local-move gated to chromium (~50% flaky on FF even
solo; pcbnew twin covers both engines).
Docs: DEBUG.md rewritten as the JSPI debugging guide; build.md
describes the single-phase build; docs/features/async/README.md
banner-marked historical and repointed at the NEW
23-jspi-runtime.md (current architecture: export census, turnstile,
libcontext ownership + refusal contract, embind call shapes, the
em-pthread service-wrapper trick, exception policy, known gaps).
Gates on the cleaned tree: test:e2e 725 passed / 0 failed (after the
quiescence-probe fix; the 3 other reds were verified contention flakes
solo-green or the documented FF gate), web 76/0, jspi 18/18 both
engines, vitest 295/295 + 17/17, all lints green, live-app census
clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016X9eh1s5sTx1o9Em9KBuwR
2026-08-14 09:25:32 +02:00
|
|
|
pcbjam_collab::runOnCoroutine( fr, [fr, childAbs, uuid]() {
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
KIID kid( wxString::FromUTF8( uuid.c_str() ) );
|
|
|
|
|
SCH_ITEM* item = fr->Schematic().ResolveItem( kid, nullptr, /*allowNull*/ true );
|
2026-06-17 17:53:38 +02:00
|
|
|
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
if( !item || item->Type() != SCH_SHEET_T )
|
|
|
|
|
return;
|
2026-06-17 17:53:38 +02:00
|
|
|
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
SCH_IO_KICAD_SEXPR io;
|
|
|
|
|
io.SaveSchematicFile( wxString::FromUTF8( childAbs.c_str() ),
|
|
|
|
|
static_cast<SCH_SHEET*>( item ), &fr->Schematic() );
|
|
|
|
|
}
|
|
|
|
|
catch( ... )
|
|
|
|
|
{
|
|
|
|
|
return; // a write failure must not abort the runtime
|
|
|
|
|
}
|
2026-06-17 17:53:38 +02:00
|
|
|
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
EM_ASM( {
|
|
|
|
|
if( window.kicadCollab && window.kicadCollab.onSheetCreated )
|
staging CI green-up: P-1 apply-slot wedge under JSPI, popup P-4 regression, ysync double-seed header, spec fixes
- P-1 (findings-p.spec P-1c red on CI + locally): a JS exception thrown from a
window.kicadCollab wire callback does NOT reach the C++ catch(...) — under
JSPI it REJECTS the promising coroutine entry, slot.done never flips and the
apply slot is busy for the page's life. Three layers: libcontext flags the
rejected record dead (kicad → d0afbc4bd3),
drainApplies() reaps a !done && !CanResume() body, and every EM_ASM
window.kicadCollab.onX(...) call is try/catch-guarded (collab_common.h,
kicad_editor/eeschema/pl_editor embind).
- popup.spec 'dismisses on outside click': wx P-4 SetFocus delegation excluded
for wxPopupWindow (wxwidgets → a7436d1052).
- ysync-two-tab concurrent seed: repairLayoutY + seedNonce retraction deleted
both headers (pcbjam-shared → 8dabf2e).
- dark-mode.spec: stale light-mode reference (pre wx fa46f1822c DOM
re-projection) replaced with the CI chromium render.
- items-wire-batch-loss.spec: precondition obsolete after P-5 — poisoned entry
is now synthetic, plus a P-5 contract assertion (field blob == '').
- presence-ghost-peer.spec: skip early when no /parties/ socket (CI web e2e
runs the BroadcastChannel provider); findings-p1.spec: polled click-select
precondition.
Local: kicad-chromium findings/presence/ysync/collab 59/59, popup 7/7,
dark-mode 1/1, pcbjam-shared vitest 249/249.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LvvARNMxQrq17vogn3REMU
2026-08-28 22:25:21 +02:00
|
|
|
{
|
|
|
|
|
// A throwing listener must never unwind the wasm frame that called it: under
|
|
|
|
|
// JSPI that rejects the running coroutine's entry (findings P-1).
|
|
|
|
|
try { window.kicadCollab.onSheetCreated( UTF8ToString( $0 ) ); }
|
|
|
|
|
catch( e ) { console.error( '[pcbjam collab] onSheetCreated listener threw', e ); }
|
|
|
|
|
}
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
}, childAbs.c_str() );
|
2026-06-17 17:53:38 +02:00
|
|
|
} );
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-05 13:30:47 +02:00
|
|
|
// ChangeSource: the native SCHEMATIC_LISTENER is just a trigger — the actual change set comes
|
|
|
|
|
// from the post-settle snapshot diff above. Skipped while applying a remote delta (no echo);
|
|
|
|
|
// doApply rebaselines instead.
|
feat(collab): presence P3 — eeschema port (collab-presence 0003)
- eeschema_embind.cpp: presence section (0002 pattern, zero fork changes) —
wx canvas triggers + SCHEMATIC_LISTENER piggyback → post-settle
SCH_SELECTION_TOOL emit; throttled cursor; remote VIEW_OVERLAY render
(SCHEMATIC::ResolveItem, name tags, screen-constant via GAL matrix);
schCollab{PresenceStart,SetRemote,GetViewport,GetSelection,TestSelectFirst,
TestClearSelection}; kicad_editor_embind dispatches by active frame.
- sheet-manager: parked rooms carry SKELETON awareness states
({user,tool,sheetPath=bound sheet}) via publishSkeletons on switch + late
warm-up — the bound room's awareness then holds every project peer, so no
multi-room aggregation; presence.ts publishSkeleton helper.
- PresenceRoster: sheet-aware — peers on another sheet render dimmed with
'on <sheet>' tooltip; WasmTool un-gates the kicad presence bridge for
eeschema and threads activeSheetPath.
- tests: presence-eeschema.spec.ts (5 e2e, mirrors pcbnew incl. the px/IU
band at eeschema's 1e4/mm IU); presence.test.ts +2 (skeleton visibility,
no ghost cursor after rebind). eeschema-collab/subschema stay green.
Verified live: two tabs on demo.kicad_sch — peer cursor cross + label,
selection box + name tag, roster avatar.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
2026-07-06 17:05:15 +02:00
|
|
|
//
|
|
|
|
|
// Presence (collab-presence 0003): schematic changes often change the selection
|
|
|
|
|
// too (delete, paste) with no closing canvas event — the trigger below also
|
|
|
|
|
// piggybacks a selection re-check. Defined in the presence section further down.
|
|
|
|
|
void schedulePresenceSelCheck();
|
2026-08-28 16:14:44 +02:00
|
|
|
// Findings Y-1/Y-3: ANY document change (local commit or remote apply) repaints
|
|
|
|
|
// the peers' overlay from the live document and re-checks the local selection,
|
|
|
|
|
// post-settle on the apply coroutine. Runs BEFORE the applying-remote early
|
|
|
|
|
// return below — the remote apply is exactly the case that left peers' boxes on
|
|
|
|
|
// deleted items.
|
|
|
|
|
void schedulePresenceDocChanged();
|
feat(collab): presence P3 — eeschema port (collab-presence 0003)
- eeschema_embind.cpp: presence section (0002 pattern, zero fork changes) —
wx canvas triggers + SCHEMATIC_LISTENER piggyback → post-settle
SCH_SELECTION_TOOL emit; throttled cursor; remote VIEW_OVERLAY render
(SCHEMATIC::ResolveItem, name tags, screen-constant via GAL matrix);
schCollab{PresenceStart,SetRemote,GetViewport,GetSelection,TestSelectFirst,
TestClearSelection}; kicad_editor_embind dispatches by active frame.
- sheet-manager: parked rooms carry SKELETON awareness states
({user,tool,sheetPath=bound sheet}) via publishSkeletons on switch + late
warm-up — the bound room's awareness then holds every project peer, so no
multi-room aggregation; presence.ts publishSkeleton helper.
- PresenceRoster: sheet-aware — peers on another sheet render dimmed with
'on <sheet>' tooltip; WasmTool un-gates the kicad presence bridge for
eeschema and threads activeSheetPath.
- tests: presence-eeschema.spec.ts (5 e2e, mirrors pcbnew incl. the px/IU
band at eeschema's 1e4/mm IU); presence.test.ts +2 (skeleton visibility,
no ghost cursor after rebind). eeschema-collab/subschema stay green.
Verified live: two tabs on demo.kicad_sch — peer cursor cross + label,
selection box + name tag, roster avatar.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
2026-07-06 17:05:15 +02:00
|
|
|
|
2026-06-05 13:30:47 +02:00
|
|
|
class COLLAB_LISTENER : public SCHEMATIC_LISTENER
|
|
|
|
|
{
|
|
|
|
|
public:
|
2026-06-17 17:53:38 +02:00
|
|
|
void OnSchItemsAdded( SCHEMATIC&, std::vector<SCH_ITEM*>& aItems ) override
|
|
|
|
|
{
|
|
|
|
|
// A newly-added hierarchical sheet → persist + register its child file (above).
|
|
|
|
|
if( !s_applyingRemote )
|
|
|
|
|
{
|
|
|
|
|
for( SCH_ITEM* item : aItems )
|
|
|
|
|
if( item->Type() == SCH_SHEET_T )
|
|
|
|
|
scheduleSheetSave( static_cast<SCH_SHEET*>( item ) );
|
|
|
|
|
}
|
fix(ysync): review bugs 01–07 — listener registration, pad nets, child removals, dirty-set emit, targeted rebaseline, seed arbitration, stale hook (doc 17)
All seven ysync-review bugs fixed and verified; every repro's expected-fail
marker removed (they now run as regression tests). Also fixes two bugs found
while verifying (doc 17 F5/F6): file-seeded Y bodies are re-upserted in the
editor's serialization (doc-16 F4 was an artifact of bug 01), and the
0008-era "asyncify-fragile envelope parse" was really wrapInBoardEnvelope
emitting display layer names — canonical LSET::Name() fixes track/via/zone
v2 applies; makeFromBlob now logs parse errors instead of swallowing them.
Verified: shared 98, standalone collab 37, ysync e2e 20/20 (chromium),
collab regression set 21 passed / 3 pre-existing skips (firefox).
Bumps: kicad (board_commit child-removal listener notification),
web/pcbjam-shared (slot prune + arbitrated seed).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JgThWXtdvrYLK47EDFoGdq
2026-07-03 18:08:37 +02:00
|
|
|
trigger( aItems );
|
2026-06-17 17:53:38 +02:00
|
|
|
}
|
fix(ysync): review bugs 01–07 — listener registration, pad nets, child removals, dirty-set emit, targeted rebaseline, seed arbitration, stale hook (doc 17)
All seven ysync-review bugs fixed and verified; every repro's expected-fail
marker removed (they now run as regression tests). Also fixes two bugs found
while verifying (doc 17 F5/F6): file-seeded Y bodies are re-upserted in the
editor's serialization (doc-16 F4 was an artifact of bug 01), and the
0008-era "asyncify-fragile envelope parse" was really wrapInBoardEnvelope
emitting display layer names — canonical LSET::Name() fixes track/via/zone
v2 applies; makeFromBlob now logs parse errors instead of swallowing them.
Verified: shared 98, standalone collab 37, ysync e2e 20/20 (chromium),
collab regression set 21 passed / 3 pre-existing skips (firefox).
Bumps: kicad (board_commit child-removal listener notification),
web/pcbjam-shared (slot prune + arbitrated seed).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JgThWXtdvrYLK47EDFoGdq
2026-07-03 18:08:37 +02:00
|
|
|
void OnSchItemsChanged( SCHEMATIC&, std::vector<SCH_ITEM*>& v ) override { trigger( v ); }
|
|
|
|
|
void OnSchItemsRemoved( SCHEMATIC&, std::vector<SCH_ITEM*>& v ) override { trigger( v ); }
|
2026-06-05 12:42:58 +02:00
|
|
|
|
2026-06-17 17:53:38 +02:00
|
|
|
// The editor switched to a different sheet (a different .kicad_sch == a different
|
|
|
|
|
// collab room). Re-baseline so the first edit on the new sheet diffs against ITS
|
|
|
|
|
// screen, not the previous one, then tell the standalone to rebind its room to the
|
|
|
|
|
// now-active sheet file. Fires from SCH_EDIT_FRAME::DisplayCurrentSheet, by which
|
|
|
|
|
// point GetCurrentSheet()/GetScreen() already point at the new sheet.
|
|
|
|
|
void OnSchSheetChanged( SCHEMATIC& ) override
|
|
|
|
|
{
|
|
|
|
|
rebaseline();
|
|
|
|
|
emitSheetChanged();
|
feat(standalone): floating sheet navigator panel backed by a C++ hierarchy export (sheet-panel)
Canvas-only eeschema sessions (read-only viewers, hide-UI editors) could not
reach the wx hierarchy pane, so a viewer of a hierarchical design was stuck
on the entry sheet. Adds the React stand-in (SheetPanel) + the bridge:
- eeschema_embind: kicadSheetsGetTree() (SCHEMATIC::Hierarchy(), page-number
ordered: instance KIID path / parent / Sheetname / file / page / depth) and
kicadSheetsEnter(path) (validated synchronously, applied on the coroutine
via SCH_ACTIONS::changeSheet — same route as the wx pane, history intact).
OnSchSheetChanged pushes window.kicadCollab.onSheetsState after EVERY
navigation, wx-driven included, so the panel is event-driven.
- kicad → 9ab93b838e: navigate actions allowlisted for read-only viewers.
- SheetPanel.tsx: LayerPanel shell conventions; rendered + menu row only when
the hierarchy has >1 sheet instance; viewers boot it collapsed.
- tests: fixtures/demo/hier (root → Power, IO → Sub) + web/sheet-panel.spec
(list/order/depth, navigate + collab rebind, menu toggle/close, flat
schematic has no panel, bridge-driven navigation updates the panel);
SheetPanel.test.ts for the wire parser.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CtN6ASBvMGNbjPqY5boycg
2026-08-29 16:24:57 +02:00
|
|
|
// The floating sheet panel follows wx-driven navigation too (double-
|
|
|
|
|
// clicking a sheet symbol, Alt+Backspace, the toolbar arrows).
|
|
|
|
|
emitSheetsState( schFrame() );
|
2026-06-17 17:53:38 +02:00
|
|
|
}
|
|
|
|
|
|
2026-06-05 13:30:47 +02:00
|
|
|
private:
|
fix(ysync): review bugs 01–07 — listener registration, pad nets, child removals, dirty-set emit, targeted rebaseline, seed arbitration, stale hook (doc 17)
All seven ysync-review bugs fixed and verified; every repro's expected-fail
marker removed (they now run as regression tests). Also fixes two bugs found
while verifying (doc 17 F5/F6): file-seeded Y bodies are re-upserted in the
editor's serialization (doc-16 F4 was an artifact of bug 01), and the
0008-era "asyncify-fragile envelope parse" was really wrapInBoardEnvelope
emitting display layer names — canonical LSET::Name() fixes track/via/zone
v2 applies; makeFromBlob now logs parse errors instead of swallowing them.
Verified: shared 98, standalone collab 37, ysync e2e 20/20 (chromium),
collab regression set 21 passed / 3 pre-existing skips (firefox).
Bumps: kicad (board_commit child-removal listener notification),
web/pcbjam-shared (slot prune + arbitrated seed).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JgThWXtdvrYLK47EDFoGdq
2026-07-03 18:08:37 +02:00
|
|
|
// Capture the touched roots at callback time (fields/pins lift to their
|
|
|
|
|
// symbol — noteDirty), then coalesce into one post-settle flush.
|
|
|
|
|
void trigger( const std::vector<SCH_ITEM*>& aItems )
|
2026-06-05 13:30:47 +02:00
|
|
|
{
|
2026-08-28 16:14:44 +02:00
|
|
|
schedulePresenceDocChanged();
|
|
|
|
|
|
fix(ysync): review bugs 01–07 — listener registration, pad nets, child removals, dirty-set emit, targeted rebaseline, seed arbitration, stale hook (doc 17)
All seven ysync-review bugs fixed and verified; every repro's expected-fail
marker removed (they now run as regression tests). Also fixes two bugs found
while verifying (doc 17 F5/F6): file-seeded Y bodies are re-upserted in the
editor's serialization (doc-16 F4 was an artifact of bug 01), and the
0008-era "asyncify-fragile envelope parse" was really wrapInBoardEnvelope
emitting display layer names — canonical LSET::Name() fixes track/via/zone
v2 applies; makeFromBlob now logs parse errors instead of swallowing them.
Verified: shared 98, standalone collab 37, ysync e2e 20/20 (chromium),
collab regression set 21 passed / 3 pre-existing skips (firefox).
Bumps: kicad (board_commit child-removal listener notification),
web/pcbjam-shared (slot prune + arbitrated seed).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JgThWXtdvrYLK47EDFoGdq
2026-07-03 18:08:37 +02:00
|
|
|
if( s_applyingRemote )
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
for( SCH_ITEM* item : aItems )
|
|
|
|
|
noteDirty( item );
|
|
|
|
|
|
|
|
|
|
scheduleFlush();
|
feat(collab): presence P3 — eeschema port (collab-presence 0003)
- eeschema_embind.cpp: presence section (0002 pattern, zero fork changes) —
wx canvas triggers + SCHEMATIC_LISTENER piggyback → post-settle
SCH_SELECTION_TOOL emit; throttled cursor; remote VIEW_OVERLAY render
(SCHEMATIC::ResolveItem, name tags, screen-constant via GAL matrix);
schCollab{PresenceStart,SetRemote,GetViewport,GetSelection,TestSelectFirst,
TestClearSelection}; kicad_editor_embind dispatches by active frame.
- sheet-manager: parked rooms carry SKELETON awareness states
({user,tool,sheetPath=bound sheet}) via publishSkeletons on switch + late
warm-up — the bound room's awareness then holds every project peer, so no
multi-room aggregation; presence.ts publishSkeleton helper.
- PresenceRoster: sheet-aware — peers on another sheet render dimmed with
'on <sheet>' tooltip; WasmTool un-gates the kicad presence bridge for
eeschema and threads activeSheetPath.
- tests: presence-eeschema.spec.ts (5 e2e, mirrors pcbnew incl. the px/IU
band at eeschema's 1e4/mm IU); presence.test.ts +2 (skeleton visibility,
no ghost cursor after rebind). eeschema-collab/subschema stay green.
Verified live: two tabs on demo.kicad_sch — peer cursor cross + label,
selection box + name tag, roster avatar.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
2026-07-06 17:05:15 +02:00
|
|
|
schedulePresenceSelCheck();
|
2026-06-05 13:30:47 +02:00
|
|
|
}
|
2026-06-03 17:49:11 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
COLLAB_LISTENER* g_listener = nullptr;
|
|
|
|
|
|
|
|
|
|
// Get the live SCHEMATIC and ensure our listener is registered on it (idempotent).
|
|
|
|
|
SCHEMATIC* ensureBridge()
|
|
|
|
|
{
|
|
|
|
|
SCH_EDIT_FRAME* fr = schFrame();
|
|
|
|
|
|
|
|
|
|
if( !fr )
|
|
|
|
|
return nullptr;
|
|
|
|
|
|
|
|
|
|
SCHEMATIC& sch = fr->Schematic();
|
|
|
|
|
|
|
|
|
|
if( !g_listener )
|
|
|
|
|
{
|
|
|
|
|
g_listener = new COLLAB_LISTENER();
|
|
|
|
|
sch.AddListener( g_listener );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return &sch;
|
|
|
|
|
}
|
|
|
|
|
|
feat(collab): presence P3 — eeschema port (collab-presence 0003)
- eeschema_embind.cpp: presence section (0002 pattern, zero fork changes) —
wx canvas triggers + SCHEMATIC_LISTENER piggyback → post-settle
SCH_SELECTION_TOOL emit; throttled cursor; remote VIEW_OVERLAY render
(SCHEMATIC::ResolveItem, name tags, screen-constant via GAL matrix);
schCollab{PresenceStart,SetRemote,GetViewport,GetSelection,TestSelectFirst,
TestClearSelection}; kicad_editor_embind dispatches by active frame.
- sheet-manager: parked rooms carry SKELETON awareness states
({user,tool,sheetPath=bound sheet}) via publishSkeletons on switch + late
warm-up — the bound room's awareness then holds every project peer, so no
multi-room aggregation; presence.ts publishSkeleton helper.
- PresenceRoster: sheet-aware — peers on another sheet render dimmed with
'on <sheet>' tooltip; WasmTool un-gates the kicad presence bridge for
eeschema and threads activeSheetPath.
- tests: presence-eeschema.spec.ts (5 e2e, mirrors pcbnew incl. the px/IU
band at eeschema's 1e4/mm IU); presence.test.ts +2 (skeleton visibility,
no ghost cursor after rebind). eeschema-collab/subschema stay green.
Verified live: two tabs on demo.kicad_sch — peer cursor cross + label,
selection box + name tag, roster avatar.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
2026-07-06 17:05:15 +02:00
|
|
|
// ───────────────────────── collab presence (collab-presence 0003) ─────────────────────────
|
|
|
|
|
//
|
|
|
|
|
// eeschema port of pcbnew's presence layer (0002 — see pcbnew_embind.cpp for the full
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
// design rationale). The state + event/scheduling machinery live in the shared
|
|
|
|
|
// pcbjam_presence::CORE (collab_presence_core.h); this TU supplies only the
|
|
|
|
|
// eeschema-specific hooks: frame/tool lookup, KIID resolution via
|
|
|
|
|
// SCHEMATIC::ResolveItem, the bare-uuid selection payload, and the per-peer draw
|
|
|
|
|
// (cross-app ghosts gated to the CURRENT sheet). Zero kicad-fork changes: wx-layer
|
|
|
|
|
// Bind() triggers on the GAL canvas + the COLLAB_LISTENER piggyback, selection read
|
|
|
|
|
// post-settle from SCH_SELECTION_TOOL. Rooms are per-sheet (warm pool), so peers
|
|
|
|
|
// publishing cursor/selection in the bound room are BY CONSTRUCTION on this same
|
|
|
|
|
// sheet file — no sheet filtering is needed for same-room selections; the JS side
|
|
|
|
|
// rebinds the whole presence layer on sheet navigation (onSheetChanged) and clears
|
|
|
|
|
// the overlay in between.
|
|
|
|
|
|
|
|
|
|
// Cross-app selection (0006): the schematic items a peer's xsel (symbol uuids
|
|
|
|
|
// from a pcbnew peer) resolves to on the CURRENT sheet. Unlike same-room
|
|
|
|
|
// selections (per-sheet rooms scope those by construction) xsel arrives
|
|
|
|
|
// project-wide, so only items that resolve onto the sheet THIS canvas is
|
|
|
|
|
// showing count — a bbox from another sheet's screen would land at meaningless
|
|
|
|
|
// coordinates. ONE resolver shared by the ghost render and the test probe
|
|
|
|
|
// (kicadCollabTestGetCrossMapped), so the assertion can't drift from the pixels.
|
|
|
|
|
std::vector<SCH_ITEM*> resolveXsel( SCH_EDIT_FRAME* aFrame, const pcbjam_presence::PEER& aPeer )
|
|
|
|
|
{
|
|
|
|
|
std::vector<SCH_ITEM*> items;
|
|
|
|
|
|
|
|
|
|
for( const KIID& id : aPeer.xsel )
|
feat(collab): presence P3 — eeschema port (collab-presence 0003)
- eeschema_embind.cpp: presence section (0002 pattern, zero fork changes) —
wx canvas triggers + SCHEMATIC_LISTENER piggyback → post-settle
SCH_SELECTION_TOOL emit; throttled cursor; remote VIEW_OVERLAY render
(SCHEMATIC::ResolveItem, name tags, screen-constant via GAL matrix);
schCollab{PresenceStart,SetRemote,GetViewport,GetSelection,TestSelectFirst,
TestClearSelection}; kicad_editor_embind dispatches by active frame.
- sheet-manager: parked rooms carry SKELETON awareness states
({user,tool,sheetPath=bound sheet}) via publishSkeletons on switch + late
warm-up — the bound room's awareness then holds every project peer, so no
multi-room aggregation; presence.ts publishSkeleton helper.
- PresenceRoster: sheet-aware — peers on another sheet render dimmed with
'on <sheet>' tooltip; WasmTool un-gates the kicad presence bridge for
eeschema and threads activeSheetPath.
- tests: presence-eeschema.spec.ts (5 e2e, mirrors pcbnew incl. the px/IU
band at eeschema's 1e4/mm IU); presence.test.ts +2 (skeleton visibility,
no ghost cursor after rebind). eeschema-collab/subschema stay green.
Verified live: two tabs on demo.kicad_sch — peer cursor cross + label,
selection box + name tag, roster avatar.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
2026-07-06 17:05:15 +02:00
|
|
|
{
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
SCH_SHEET_PATH path;
|
|
|
|
|
SCH_ITEM* item = aFrame->Schematic().ResolveItem( id, &path, /*allowNull*/ true );
|
feat(collab): presence P3 — eeschema port (collab-presence 0003)
- eeschema_embind.cpp: presence section (0002 pattern, zero fork changes) —
wx canvas triggers + SCHEMATIC_LISTENER piggyback → post-settle
SCH_SELECTION_TOOL emit; throttled cursor; remote VIEW_OVERLAY render
(SCHEMATIC::ResolveItem, name tags, screen-constant via GAL matrix);
schCollab{PresenceStart,SetRemote,GetViewport,GetSelection,TestSelectFirst,
TestClearSelection}; kicad_editor_embind dispatches by active frame.
- sheet-manager: parked rooms carry SKELETON awareness states
({user,tool,sheetPath=bound sheet}) via publishSkeletons on switch + late
warm-up — the bound room's awareness then holds every project peer, so no
multi-room aggregation; presence.ts publishSkeleton helper.
- PresenceRoster: sheet-aware — peers on another sheet render dimmed with
'on <sheet>' tooltip; WasmTool un-gates the kicad presence bridge for
eeschema and threads activeSheetPath.
- tests: presence-eeschema.spec.ts (5 e2e, mirrors pcbnew incl. the px/IU
band at eeschema's 1e4/mm IU); presence.test.ts +2 (skeleton visibility,
no ghost cursor after rebind). eeschema-collab/subschema stay green.
Verified live: two tabs on demo.kicad_sch — peer cursor cross + label,
selection box + name tag, roster avatar.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
2026-07-06 17:05:15 +02:00
|
|
|
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
if( item && path.LastScreen() == aFrame->GetScreen() )
|
|
|
|
|
items.push_back( item );
|
|
|
|
|
}
|
feat(collab): presence P3 — eeschema port (collab-presence 0003)
- eeschema_embind.cpp: presence section (0002 pattern, zero fork changes) —
wx canvas triggers + SCHEMATIC_LISTENER piggyback → post-settle
SCH_SELECTION_TOOL emit; throttled cursor; remote VIEW_OVERLAY render
(SCHEMATIC::ResolveItem, name tags, screen-constant via GAL matrix);
schCollab{PresenceStart,SetRemote,GetViewport,GetSelection,TestSelectFirst,
TestClearSelection}; kicad_editor_embind dispatches by active frame.
- sheet-manager: parked rooms carry SKELETON awareness states
({user,tool,sheetPath=bound sheet}) via publishSkeletons on switch + late
warm-up — the bound room's awareness then holds every project peer, so no
multi-room aggregation; presence.ts publishSkeleton helper.
- PresenceRoster: sheet-aware — peers on another sheet render dimmed with
'on <sheet>' tooltip; WasmTool un-gates the kicad presence bridge for
eeschema and threads activeSheetPath.
- tests: presence-eeschema.spec.ts (5 e2e, mirrors pcbnew incl. the px/IU
band at eeschema's 1e4/mm IU); presence.test.ts +2 (skeleton visibility,
no ghost cursor after rebind). eeschema-collab/subschema stay green.
Verified live: two tabs on demo.kicad_sch — peer cursor cross + label,
selection box + name tag, roster avatar.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
2026-07-06 17:05:15 +02:00
|
|
|
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
return items;
|
feat(collab): presence P3 — eeschema port (collab-presence 0003)
- eeschema_embind.cpp: presence section (0002 pattern, zero fork changes) —
wx canvas triggers + SCHEMATIC_LISTENER piggyback → post-settle
SCH_SELECTION_TOOL emit; throttled cursor; remote VIEW_OVERLAY render
(SCHEMATIC::ResolveItem, name tags, screen-constant via GAL matrix);
schCollab{PresenceStart,SetRemote,GetViewport,GetSelection,TestSelectFirst,
TestClearSelection}; kicad_editor_embind dispatches by active frame.
- sheet-manager: parked rooms carry SKELETON awareness states
({user,tool,sheetPath=bound sheet}) via publishSkeletons on switch + late
warm-up — the bound room's awareness then holds every project peer, so no
multi-room aggregation; presence.ts publishSkeleton helper.
- PresenceRoster: sheet-aware — peers on another sheet render dimmed with
'on <sheet>' tooltip; WasmTool un-gates the kicad presence bridge for
eeschema and threads activeSheetPath.
- tests: presence-eeschema.spec.ts (5 e2e, mirrors pcbnew incl. the px/IU
band at eeschema's 1e4/mm IU); presence.test.ts +2 (skeleton visibility,
no ghost cursor after rebind). eeschema-collab/subschema stay green.
Verified live: two tabs on demo.kicad_sch — peer cursor cross + label,
selection box + name tag, roster avatar.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
2026-07-06 17:05:15 +02:00
|
|
|
}
|
|
|
|
|
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
pcbjam_presence::CORE& presenceCore()
|
feat(collab): presence P3 — eeschema port (collab-presence 0003)
- eeschema_embind.cpp: presence section (0002 pattern, zero fork changes) —
wx canvas triggers + SCHEMATIC_LISTENER piggyback → post-settle
SCH_SELECTION_TOOL emit; throttled cursor; remote VIEW_OVERLAY render
(SCHEMATIC::ResolveItem, name tags, screen-constant via GAL matrix);
schCollab{PresenceStart,SetRemote,GetViewport,GetSelection,TestSelectFirst,
TestClearSelection}; kicad_editor_embind dispatches by active frame.
- sheet-manager: parked rooms carry SKELETON awareness states
({user,tool,sheetPath=bound sheet}) via publishSkeletons on switch + late
warm-up — the bound room's awareness then holds every project peer, so no
multi-room aggregation; presence.ts publishSkeleton helper.
- PresenceRoster: sheet-aware — peers on another sheet render dimmed with
'on <sheet>' tooltip; WasmTool un-gates the kicad presence bridge for
eeschema and threads activeSheetPath.
- tests: presence-eeschema.spec.ts (5 e2e, mirrors pcbnew incl. the px/IU
band at eeschema's 1e4/mm IU); presence.test.ts +2 (skeleton visibility,
no ghost cursor after rebind). eeschema-collab/subschema stay green.
Verified live: two tabs on demo.kicad_sch — peer cursor cross + label,
selection box + name tag, roster avatar.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
2026-07-06 17:05:15 +02:00
|
|
|
{
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
static pcbjam_presence::CORE core = []()
|
2026-07-07 15:57:56 +02:00
|
|
|
{
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
pcbjam_presence::CORE c;
|
feat(collab): presence P3 — eeschema port (collab-presence 0003)
- eeschema_embind.cpp: presence section (0002 pattern, zero fork changes) —
wx canvas triggers + SCHEMATIC_LISTENER piggyback → post-settle
SCH_SELECTION_TOOL emit; throttled cursor; remote VIEW_OVERLAY render
(SCHEMATIC::ResolveItem, name tags, screen-constant via GAL matrix);
schCollab{PresenceStart,SetRemote,GetViewport,GetSelection,TestSelectFirst,
TestClearSelection}; kicad_editor_embind dispatches by active frame.
- sheet-manager: parked rooms carry SKELETON awareness states
({user,tool,sheetPath=bound sheet}) via publishSkeletons on switch + late
warm-up — the bound room's awareness then holds every project peer, so no
multi-room aggregation; presence.ts publishSkeleton helper.
- PresenceRoster: sheet-aware — peers on another sheet render dimmed with
'on <sheet>' tooltip; WasmTool un-gates the kicad presence bridge for
eeschema and threads activeSheetPath.
- tests: presence-eeschema.spec.ts (5 e2e, mirrors pcbnew incl. the px/IU
band at eeschema's 1e4/mm IU); presence.test.ts +2 (skeleton visibility,
no ghost cursor after rebind). eeschema-collab/subschema stay green.
Verified live: two tabs on demo.kicad_sch — peer cursor cross + label,
selection box + name tag, roster avatar.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
2026-07-06 17:05:15 +02:00
|
|
|
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
// eeschema ships its own defaults (hairline outline, subtler fill/cursor).
|
|
|
|
|
c.style = pcbjam_presence::eeschemaDefaultStyle();
|
feat(collab): presence P3 — eeschema port (collab-presence 0003)
- eeschema_embind.cpp: presence section (0002 pattern, zero fork changes) —
wx canvas triggers + SCHEMATIC_LISTENER piggyback → post-settle
SCH_SELECTION_TOOL emit; throttled cursor; remote VIEW_OVERLAY render
(SCHEMATIC::ResolveItem, name tags, screen-constant via GAL matrix);
schCollab{PresenceStart,SetRemote,GetViewport,GetSelection,TestSelectFirst,
TestClearSelection}; kicad_editor_embind dispatches by active frame.
- sheet-manager: parked rooms carry SKELETON awareness states
({user,tool,sheetPath=bound sheet}) via publishSkeletons on switch + late
warm-up — the bound room's awareness then holds every project peer, so no
multi-room aggregation; presence.ts publishSkeleton helper.
- PresenceRoster: sheet-aware — peers on another sheet render dimmed with
'on <sheet>' tooltip; WasmTool un-gates the kicad presence bridge for
eeschema and threads activeSheetPath.
- tests: presence-eeschema.spec.ts (5 e2e, mirrors pcbnew incl. the px/IU
band at eeschema's 1e4/mm IU); presence.test.ts +2 (skeleton visibility,
no ghost cursor after rebind). eeschema-collab/subschema stay green.
Verified live: two tabs on demo.kicad_sch — peer cursor cross + label,
selection box + name tag, roster avatar.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
2026-07-06 17:05:15 +02:00
|
|
|
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
c.frame = []() -> EDA_DRAW_FRAME* { return schFrame(); };
|
feat(collab): presence P3 — eeschema port (collab-presence 0003)
- eeschema_embind.cpp: presence section (0002 pattern, zero fork changes) —
wx canvas triggers + SCHEMATIC_LISTENER piggyback → post-settle
SCH_SELECTION_TOOL emit; throttled cursor; remote VIEW_OVERLAY render
(SCHEMATIC::ResolveItem, name tags, screen-constant via GAL matrix);
schCollab{PresenceStart,SetRemote,GetViewport,GetSelection,TestSelectFirst,
TestClearSelection}; kicad_editor_embind dispatches by active frame.
- sheet-manager: parked rooms carry SKELETON awareness states
({user,tool,sheetPath=bound sheet}) via publishSkeletons on switch + late
warm-up — the bound room's awareness then holds every project peer, so no
multi-room aggregation; presence.ts publishSkeleton helper.
- PresenceRoster: sheet-aware — peers on another sheet render dimmed with
'on <sheet>' tooltip; WasmTool un-gates the kicad presence bridge for
eeschema and threads activeSheetPath.
- tests: presence-eeschema.spec.ts (5 e2e, mirrors pcbnew incl. the px/IU
band at eeschema's 1e4/mm IU); presence.test.ts +2 (skeleton visibility,
no ghost cursor after rebind). eeschema-collab/subschema stay green.
Verified live: two tabs on demo.kicad_sch — peer cursor cross + label,
selection box + name tag, roster avatar.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
2026-07-06 17:05:15 +02:00
|
|
|
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
c.selectionTool = []( EDA_DRAW_FRAME* fr ) -> SELECTION_TOOL*
|
feat(collab): presence P3 — eeschema port (collab-presence 0003)
- eeschema_embind.cpp: presence section (0002 pattern, zero fork changes) —
wx canvas triggers + SCHEMATIC_LISTENER piggyback → post-settle
SCH_SELECTION_TOOL emit; throttled cursor; remote VIEW_OVERLAY render
(SCHEMATIC::ResolveItem, name tags, screen-constant via GAL matrix);
schCollab{PresenceStart,SetRemote,GetViewport,GetSelection,TestSelectFirst,
TestClearSelection}; kicad_editor_embind dispatches by active frame.
- sheet-manager: parked rooms carry SKELETON awareness states
({user,tool,sheetPath=bound sheet}) via publishSkeletons on switch + late
warm-up — the bound room's awareness then holds every project peer, so no
multi-room aggregation; presence.ts publishSkeleton helper.
- PresenceRoster: sheet-aware — peers on another sheet render dimmed with
'on <sheet>' tooltip; WasmTool un-gates the kicad presence bridge for
eeschema and threads activeSheetPath.
- tests: presence-eeschema.spec.ts (5 e2e, mirrors pcbnew incl. the px/IU
band at eeschema's 1e4/mm IU); presence.test.ts +2 (skeleton visibility,
no ghost cursor after rebind). eeschema-collab/subschema stay green.
Verified live: two tabs on demo.kicad_sch — peer cursor cross + label,
selection box + name tag, roster avatar.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
2026-07-06 17:05:15 +02:00
|
|
|
{
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
return fr->GetToolManager()->GetTool<SCH_SELECTION_TOOL>();
|
|
|
|
|
};
|
feat(collab): presence P3 — eeschema port (collab-presence 0003)
- eeschema_embind.cpp: presence section (0002 pattern, zero fork changes) —
wx canvas triggers + SCHEMATIC_LISTENER piggyback → post-settle
SCH_SELECTION_TOOL emit; throttled cursor; remote VIEW_OVERLAY render
(SCHEMATIC::ResolveItem, name tags, screen-constant via GAL matrix);
schCollab{PresenceStart,SetRemote,GetViewport,GetSelection,TestSelectFirst,
TestClearSelection}; kicad_editor_embind dispatches by active frame.
- sheet-manager: parked rooms carry SKELETON awareness states
({user,tool,sheetPath=bound sheet}) via publishSkeletons on switch + late
warm-up — the bound room's awareness then holds every project peer, so no
multi-room aggregation; presence.ts publishSkeleton helper.
- PresenceRoster: sheet-aware — peers on another sheet render dimmed with
'on <sheet>' tooltip; WasmTool un-gates the kicad presence bridge for
eeschema and threads activeSheetPath.
- tests: presence-eeschema.spec.ts (5 e2e, mirrors pcbnew incl. the px/IU
band at eeschema's 1e4/mm IU); presence.test.ts +2 (skeleton visibility,
no ghost cursor after rebind). eeschema-collab/subschema stay green.
Verified live: two tabs on demo.kicad_sch — peer cursor cross + label,
selection box + name tag, roster avatar.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
2026-07-06 17:05:15 +02:00
|
|
|
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
// Selection emit = the bare uuid array (0006: eeschema uuids ARE the
|
|
|
|
|
// symbol uuids; pcbnew's counterpart adds fpPaths).
|
|
|
|
|
c.selectionEmitPayload = []( EDA_DRAW_FRAME* fr ) -> json
|
|
|
|
|
{
|
|
|
|
|
return presenceCore().selectionUuids( fr );
|
|
|
|
|
};
|
feat(collab): presence P3 — eeschema port (collab-presence 0003)
- eeschema_embind.cpp: presence section (0002 pattern, zero fork changes) —
wx canvas triggers + SCHEMATIC_LISTENER piggyback → post-settle
SCH_SELECTION_TOOL emit; throttled cursor; remote VIEW_OVERLAY render
(SCHEMATIC::ResolveItem, name tags, screen-constant via GAL matrix);
schCollab{PresenceStart,SetRemote,GetViewport,GetSelection,TestSelectFirst,
TestClearSelection}; kicad_editor_embind dispatches by active frame.
- sheet-manager: parked rooms carry SKELETON awareness states
({user,tool,sheetPath=bound sheet}) via publishSkeletons on switch + late
warm-up — the bound room's awareness then holds every project peer, so no
multi-room aggregation; presence.ts publishSkeleton helper.
- PresenceRoster: sheet-aware — peers on another sheet render dimmed with
'on <sheet>' tooltip; WasmTool un-gates the kicad presence bridge for
eeschema and threads activeSheetPath.
- tests: presence-eeschema.spec.ts (5 e2e, mirrors pcbnew incl. the px/IU
band at eeschema's 1e4/mm IU); presence.test.ts +2 (skeleton visibility,
no ghost cursor after rebind). eeschema-collab/subschema stay green.
Verified live: two tabs on demo.kicad_sch — peer cursor cross + label,
selection box + name tag, roster avatar.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
2026-07-06 17:05:15 +02:00
|
|
|
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
c.resolveItem = []( EDA_DRAW_FRAME* fr, const KIID& id ) -> EDA_ITEM*
|
|
|
|
|
{
|
|
|
|
|
return static_cast<SCH_EDIT_FRAME*>( fr )->Schematic()
|
|
|
|
|
.ResolveItem( id, nullptr, /*allowNull*/ true );
|
|
|
|
|
};
|
feat(collab): presence P3 — eeschema port (collab-presence 0003)
- eeschema_embind.cpp: presence section (0002 pattern, zero fork changes) —
wx canvas triggers + SCHEMATIC_LISTENER piggyback → post-settle
SCH_SELECTION_TOOL emit; throttled cursor; remote VIEW_OVERLAY render
(SCHEMATIC::ResolveItem, name tags, screen-constant via GAL matrix);
schCollab{PresenceStart,SetRemote,GetViewport,GetSelection,TestSelectFirst,
TestClearSelection}; kicad_editor_embind dispatches by active frame.
- sheet-manager: parked rooms carry SKELETON awareness states
({user,tool,sheetPath=bound sheet}) via publishSkeletons on switch + late
warm-up — the bound room's awareness then holds every project peer, so no
multi-room aggregation; presence.ts publishSkeleton helper.
- PresenceRoster: sheet-aware — peers on another sheet render dimmed with
'on <sheet>' tooltip; WasmTool un-gates the kicad presence bridge for
eeschema and threads activeSheetPath.
- tests: presence-eeschema.spec.ts (5 e2e, mirrors pcbnew incl. the px/IU
band at eeschema's 1e4/mm IU); presence.test.ts +2 (skeleton visibility,
no ghost cursor after rebind). eeschema-collab/subschema stay green.
Verified live: two tabs on demo.kicad_sch — peer cursor cross + label,
selection box + name tag, roster avatar.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
2026-07-06 17:05:15 +02:00
|
|
|
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
c.drawPeerShapes = []( pcbjam_presence::CORE& aCore, EDA_DRAW_FRAME* aFrame,
|
|
|
|
|
const pcbjam_presence::PEER& peer, const KIGFX::COLOR4D& color,
|
|
|
|
|
double px )
|
feat(collab): cross-app selection — eeschema symbol ⇄ pcbnew footprint ghost highlight (collab-presence 0006)
Selecting a symbol in eeschema ghost-highlights the linked footprint(s) in
every pcbnew tab of the project, and vice versa — across users AND one
user's own two tabs. Native KIWAY cross-probe is inert in WASM (one frame
per page); this rides the presence layer instead.
- cross-app.ts: project-wide awareness-only room (presenceRoomId), publishes
full PresenceState at selection rate (cursor always null); peers() = other-
TOOL clients incl. own user's other tabs; window.__pcbjamCrossApp test handle
- presence-kicad.ts: parseSelectionEmit (bare array | {uuids,fpPaths}),
xselFromPeerState (pcbnew paths → symbol uuids; eeschema uuids verbatim),
cross peers appended to the kicadCollabSetRemote snapshot as
{id "<user>#x<client>", name "<user> · sch|pcb", xsel}
- C++ (zero fork changes): pcbnew emits {uuids, fpPaths} (FOOTPRINT::GetPath)
and ghost-renders xsel via path-tail suffix scan; eeschema resolves xsel via
ResolveItem gated to the CURRENT sheet (xsel arrives project-wide, unlike
room-scoped selections); ghostStyle = alphas × xselAlphaScale (0.55, tuner-
patchable); new exports kicadCollabGetSelectionFull / TestGetCrossMapped /
TestSelectComponent (skips power symbols — PWR_FLAG has no footprint) +
merged-image dispatch
- tests: presence suites extended (payload shape, ghost render pixel tests,
13/13) + new two-tab tests/web/cross-probe.spec.ts (passing vs real
partykit); eeschema pixel compares now target the #glcanvas-* GAL panel
(the whole-window #canvas compare flaked on the auto-dismissing version
infobar — also fixes the long-known presence-eeschema restore flake);
cross-app + presence-kicad vitest suites
Spec: docs/features/collab-presence/0006 (closed repo).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lg5jwWuhFH5dL8hEcBDuP2
2026-07-07 19:30:18 +02:00
|
|
|
{
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
SCH_EDIT_FRAME* fr = static_cast<SCH_EDIT_FRAME*>( aFrame );
|
feat(collab): cross-app selection — eeschema symbol ⇄ pcbnew footprint ghost highlight (collab-presence 0006)
Selecting a symbol in eeschema ghost-highlights the linked footprint(s) in
every pcbnew tab of the project, and vice versa — across users AND one
user's own two tabs. Native KIWAY cross-probe is inert in WASM (one frame
per page); this rides the presence layer instead.
- cross-app.ts: project-wide awareness-only room (presenceRoomId), publishes
full PresenceState at selection rate (cursor always null); peers() = other-
TOOL clients incl. own user's other tabs; window.__pcbjamCrossApp test handle
- presence-kicad.ts: parseSelectionEmit (bare array | {uuids,fpPaths}),
xselFromPeerState (pcbnew paths → symbol uuids; eeschema uuids verbatim),
cross peers appended to the kicadCollabSetRemote snapshot as
{id "<user>#x<client>", name "<user> · sch|pcb", xsel}
- C++ (zero fork changes): pcbnew emits {uuids, fpPaths} (FOOTPRINT::GetPath)
and ghost-renders xsel via path-tail suffix scan; eeschema resolves xsel via
ResolveItem gated to the CURRENT sheet (xsel arrives project-wide, unlike
room-scoped selections); ghostStyle = alphas × xselAlphaScale (0.55, tuner-
patchable); new exports kicadCollabGetSelectionFull / TestGetCrossMapped /
TestSelectComponent (skips power symbols — PWR_FLAG has no footprint) +
merged-image dispatch
- tests: presence suites extended (payload shape, ghost render pixel tests,
13/13) + new two-tab tests/web/cross-probe.spec.ts (passing vs real
partykit); eeschema pixel compares now target the #glcanvas-* GAL panel
(the whole-window #canvas compare flaked on the auto-dismissing version
infobar — also fixes the long-known presence-eeschema restore flake);
cross-app + presence-kicad vitest suites
Spec: docs/features/collab-presence/0006 (closed repo).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lg5jwWuhFH5dL8hEcBDuP2
2026-07-07 19:30:18 +02:00
|
|
|
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
for( const KIID& id : peer.selection )
|
feat(collab): cross-app selection — eeschema symbol ⇄ pcbnew footprint ghost highlight (collab-presence 0006)
Selecting a symbol in eeschema ghost-highlights the linked footprint(s) in
every pcbnew tab of the project, and vice versa — across users AND one
user's own two tabs. Native KIWAY cross-probe is inert in WASM (one frame
per page); this rides the presence layer instead.
- cross-app.ts: project-wide awareness-only room (presenceRoomId), publishes
full PresenceState at selection rate (cursor always null); peers() = other-
TOOL clients incl. own user's other tabs; window.__pcbjamCrossApp test handle
- presence-kicad.ts: parseSelectionEmit (bare array | {uuids,fpPaths}),
xselFromPeerState (pcbnew paths → symbol uuids; eeschema uuids verbatim),
cross peers appended to the kicadCollabSetRemote snapshot as
{id "<user>#x<client>", name "<user> · sch|pcb", xsel}
- C++ (zero fork changes): pcbnew emits {uuids, fpPaths} (FOOTPRINT::GetPath)
and ghost-renders xsel via path-tail suffix scan; eeschema resolves xsel via
ResolveItem gated to the CURRENT sheet (xsel arrives project-wide, unlike
room-scoped selections); ghostStyle = alphas × xselAlphaScale (0.55, tuner-
patchable); new exports kicadCollabGetSelectionFull / TestGetCrossMapped /
TestSelectComponent (skips power symbols — PWR_FLAG has no footprint) +
merged-image dispatch
- tests: presence suites extended (payload shape, ghost render pixel tests,
13/13) + new two-tab tests/web/cross-probe.spec.ts (passing vs real
partykit); eeschema pixel compares now target the #glcanvas-* GAL panel
(the whole-window #canvas compare flaked on the auto-dismissing version
infobar — also fixes the long-known presence-eeschema restore flake);
cross-app + presence-kicad vitest suites
Spec: docs/features/collab-presence/0006 (closed repo).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lg5jwWuhFH5dL8hEcBDuP2
2026-07-07 19:30:18 +02:00
|
|
|
{
|
|
|
|
|
SCH_SHEET_PATH path;
|
|
|
|
|
SCH_ITEM* item = fr->Schematic().ResolveItem( id, &path, /*allowNull*/ true );
|
|
|
|
|
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
if( !item )
|
|
|
|
|
continue; // not in this schematic (yet) — skip silently
|
feat(collab): cross-app selection — eeschema symbol ⇄ pcbnew footprint ghost highlight (collab-presence 0006)
Selecting a symbol in eeschema ghost-highlights the linked footprint(s) in
every pcbnew tab of the project, and vice versa — across users AND one
user's own two tabs. Native KIWAY cross-probe is inert in WASM (one frame
per page); this rides the presence layer instead.
- cross-app.ts: project-wide awareness-only room (presenceRoomId), publishes
full PresenceState at selection rate (cursor always null); peers() = other-
TOOL clients incl. own user's other tabs; window.__pcbjamCrossApp test handle
- presence-kicad.ts: parseSelectionEmit (bare array | {uuids,fpPaths}),
xselFromPeerState (pcbnew paths → symbol uuids; eeschema uuids verbatim),
cross peers appended to the kicadCollabSetRemote snapshot as
{id "<user>#x<client>", name "<user> · sch|pcb", xsel}
- C++ (zero fork changes): pcbnew emits {uuids, fpPaths} (FOOTPRINT::GetPath)
and ghost-renders xsel via path-tail suffix scan; eeschema resolves xsel via
ResolveItem gated to the CURRENT sheet (xsel arrives project-wide, unlike
room-scoped selections); ghostStyle = alphas × xselAlphaScale (0.55, tuner-
patchable); new exports kicadCollabGetSelectionFull / TestGetCrossMapped /
TestSelectComponent (skips power symbols — PWR_FLAG has no footprint) +
merged-image dispatch
- tests: presence suites extended (payload shape, ghost render pixel tests,
13/13) + new two-tab tests/web/cross-probe.spec.ts (passing vs real
partykit); eeschema pixel compares now target the #glcanvas-* GAL panel
(the whole-window #canvas compare flaked on the auto-dismissing version
infobar — also fixes the long-known presence-eeschema restore flake);
cross-app + presence-kicad vitest suites
Spec: docs/features/collab-presence/0006 (closed repo).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lg5jwWuhFH5dL8hEcBDuP2
2026-07-07 19:30:18 +02:00
|
|
|
|
feat(collab): follow-user (collab-presence 0008) + chip depth-layer fix
Follow-user: click a peer's roster avatar to mirror their viewport until
local input breaks it.
- collab_presence_core.h: CORE::fitViewport(cx, cy, halfW, halfH) — fit the
leader's world rect with CONTAIN semantics (zoom derived from the
follower's own canvas via the ToScreen ratio; GetScale is the zoom, not
px/IU). Exported as kicadCollabFitViewport from both editor TUs + the
merged dispatcher.
- presence-kicad.ts: publish the visible world rect (viewportRect) into
awareness, 100 ms trailing throttle; guarded for pre-0008 handles.
- follow-user.ts: createFollow — follows an awareness CLIENT (a tab, not a
user); applies leader rect changes via FitViewport, dedupes unchanged
republishes; break-on-interact compares local onViewport echoes against
the last applied rect (2% rel tolerance, echo-grace before the first fit
lands); unfollows on leader-left; pauses on eeschema sheet mismatch.
- PresenceRoster: avatars are follow toggles (ring on the followed peer);
WasmTool renders the "Following <name> — move to stop" banner.
- tests: 7 controller units (85/85 collab), fitViewport round-trip e2e in
both kicad presence specs (20/20), two-tab tests/web/follow.spec.ts
(converge → track → wheel-zoom breaks → subsequent moves ignored).
Chip depth-layer fix (user-reported): name chips washed out inside
low-alpha selection fills — chip rects shared the shapes overlay's single
depth, and same-depth fragments drawn LATER lose the depth test, so an
earlier-painted fill rejected the chip's pixels. Now three layers via the
fork's VIEW_OVERLAY::SetDepthOffset: text (0) < chips + pin dots (1) <
selection shapes (2). drawLabel/drawCursor/drawSelectionBox take the chip
overlay explicitly; comment-pin dots move to the chip layer too (the 0005
"drawn last so pins sit above" comment had the rule backwards). Verified
with a chip-inside-30%-fill pixel repro + the full presence suite.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-09 09:30:40 +02:00
|
|
|
pcbjam_presence::drawSelectionBox( aCore.overlay.get(), aCore.chipOverlay.get(),
|
|
|
|
|
aCore.textOverlay.get(),
|
feat(collab): cross-app selection — eeschema symbol ⇄ pcbnew footprint ghost highlight (collab-presence 0006)
Selecting a symbol in eeschema ghost-highlights the linked footprint(s) in
every pcbnew tab of the project, and vice versa — across users AND one
user's own two tabs. Native KIWAY cross-probe is inert in WASM (one frame
per page); this rides the presence layer instead.
- cross-app.ts: project-wide awareness-only room (presenceRoomId), publishes
full PresenceState at selection rate (cursor always null); peers() = other-
TOOL clients incl. own user's other tabs; window.__pcbjamCrossApp test handle
- presence-kicad.ts: parseSelectionEmit (bare array | {uuids,fpPaths}),
xselFromPeerState (pcbnew paths → symbol uuids; eeschema uuids verbatim),
cross peers appended to the kicadCollabSetRemote snapshot as
{id "<user>#x<client>", name "<user> · sch|pcb", xsel}
- C++ (zero fork changes): pcbnew emits {uuids, fpPaths} (FOOTPRINT::GetPath)
and ghost-renders xsel via path-tail suffix scan; eeschema resolves xsel via
ResolveItem gated to the CURRENT sheet (xsel arrives project-wide, unlike
room-scoped selections); ghostStyle = alphas × xselAlphaScale (0.55, tuner-
patchable); new exports kicadCollabGetSelectionFull / TestGetCrossMapped /
TestSelectComponent (skips power symbols — PWR_FLAG has no footprint) +
merged-image dispatch
- tests: presence suites extended (payload shape, ghost render pixel tests,
13/13) + new two-tab tests/web/cross-probe.spec.ts (passing vs real
partykit); eeschema pixel compares now target the #glcanvas-* GAL panel
(the whole-window #canvas compare flaked on the auto-dismissing version
infobar — also fixes the long-known presence-eeschema restore flake);
cross-app + presence-kicad vitest suites
Spec: docs/features/collab-presence/0006 (closed repo).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lg5jwWuhFH5dL8hEcBDuP2
2026-07-07 19:30:18 +02:00
|
|
|
item->ViewBBox(), peer.name, color, px,
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
aCore.style );
|
feat(collab): cross-app selection — eeschema symbol ⇄ pcbnew footprint ghost highlight (collab-presence 0006)
Selecting a symbol in eeschema ghost-highlights the linked footprint(s) in
every pcbnew tab of the project, and vice versa — across users AND one
user's own two tabs. Native KIWAY cross-probe is inert in WASM (one frame
per page); this rides the presence layer instead.
- cross-app.ts: project-wide awareness-only room (presenceRoomId), publishes
full PresenceState at selection rate (cursor always null); peers() = other-
TOOL clients incl. own user's other tabs; window.__pcbjamCrossApp test handle
- presence-kicad.ts: parseSelectionEmit (bare array | {uuids,fpPaths}),
xselFromPeerState (pcbnew paths → symbol uuids; eeschema uuids verbatim),
cross peers appended to the kicadCollabSetRemote snapshot as
{id "<user>#x<client>", name "<user> · sch|pcb", xsel}
- C++ (zero fork changes): pcbnew emits {uuids, fpPaths} (FOOTPRINT::GetPath)
and ghost-renders xsel via path-tail suffix scan; eeschema resolves xsel via
ResolveItem gated to the CURRENT sheet (xsel arrives project-wide, unlike
room-scoped selections); ghostStyle = alphas × xselAlphaScale (0.55, tuner-
patchable); new exports kicadCollabGetSelectionFull / TestGetCrossMapped /
TestSelectComponent (skips power symbols — PWR_FLAG has no footprint) +
merged-image dispatch
- tests: presence suites extended (payload shape, ghost render pixel tests,
13/13) + new two-tab tests/web/cross-probe.spec.ts (passing vs real
partykit); eeschema pixel compares now target the #glcanvas-* GAL panel
(the whole-window #canvas compare flaked on the auto-dismissing version
infobar — also fixes the long-known presence-eeschema restore flake);
cross-app + presence-kicad vitest suites
Spec: docs/features/collab-presence/0006 (closed repo).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lg5jwWuhFH5dL8hEcBDuP2
2026-07-07 19:30:18 +02:00
|
|
|
}
|
feat(comments): figma-like comment pins + threads (collab-presence 0005)
Hybrid pins: the wasm draws the dot (kicadCollabSetPins rides the presence
VIEW_OVERLAY, author color + white ring, drawn above selections; zero
kicad-fork changes), the DOM owns interaction —
- comments.ts: controller gluing the MIT kdoc_comments helpers to the editor:
anchor resolution per tool IU (pins track item moves via kdoc_items
observation), throttled pin snapshots, anchorAt nearest-item snap, jumpTo
via new kicadCollabSetViewport; rebinds per sheet like presence.
- CommentLayer.tsx: comment mode (click catcher + composer), pin hit targets
over the GAL dots, thread popover (reply/edit/delete own, resolve/reopen,
delete thread), panel with resolved filter + jump-to (popover centers when
the pin is off-screen). Resolved pins drop figma-style.
- WasmTool: controller lifecycle beside presence; live viewport feed;
window.__pcbjamComments test handle (threads persist in the room ydoc).
- e2e tests/web/comments.spec.ts: two-tab create → reply → resolve → panel
filter → delete, passing vs real partykit; presence suites + collab units
stay green; shared pointer bump (0004 model).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
2026-07-06 17:50:11 +02:00
|
|
|
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
// Cross-app ghosts (0006) — see resolveXsel for the sheet gating.
|
|
|
|
|
if( !peer.xsel.empty() )
|
|
|
|
|
{
|
|
|
|
|
pcbjam_presence::STYLE ghost = pcbjam_presence::ghostStyle( aCore.style );
|
feat(collab): presence P3 — eeschema port (collab-presence 0003)
- eeschema_embind.cpp: presence section (0002 pattern, zero fork changes) —
wx canvas triggers + SCHEMATIC_LISTENER piggyback → post-settle
SCH_SELECTION_TOOL emit; throttled cursor; remote VIEW_OVERLAY render
(SCHEMATIC::ResolveItem, name tags, screen-constant via GAL matrix);
schCollab{PresenceStart,SetRemote,GetViewport,GetSelection,TestSelectFirst,
TestClearSelection}; kicad_editor_embind dispatches by active frame.
- sheet-manager: parked rooms carry SKELETON awareness states
({user,tool,sheetPath=bound sheet}) via publishSkeletons on switch + late
warm-up — the bound room's awareness then holds every project peer, so no
multi-room aggregation; presence.ts publishSkeleton helper.
- PresenceRoster: sheet-aware — peers on another sheet render dimmed with
'on <sheet>' tooltip; WasmTool un-gates the kicad presence bridge for
eeschema and threads activeSheetPath.
- tests: presence-eeschema.spec.ts (5 e2e, mirrors pcbnew incl. the px/IU
band at eeschema's 1e4/mm IU); presence.test.ts +2 (skeleton visibility,
no ghost cursor after rebind). eeschema-collab/subschema stay green.
Verified live: two tabs on demo.kicad_sch — peer cursor cross + label,
selection box + name tag, roster avatar.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
2026-07-06 17:05:15 +02:00
|
|
|
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
for( SCH_ITEM* item : resolveXsel( fr, peer ) )
|
|
|
|
|
{
|
feat(collab): follow-user (collab-presence 0008) + chip depth-layer fix
Follow-user: click a peer's roster avatar to mirror their viewport until
local input breaks it.
- collab_presence_core.h: CORE::fitViewport(cx, cy, halfW, halfH) — fit the
leader's world rect with CONTAIN semantics (zoom derived from the
follower's own canvas via the ToScreen ratio; GetScale is the zoom, not
px/IU). Exported as kicadCollabFitViewport from both editor TUs + the
merged dispatcher.
- presence-kicad.ts: publish the visible world rect (viewportRect) into
awareness, 100 ms trailing throttle; guarded for pre-0008 handles.
- follow-user.ts: createFollow — follows an awareness CLIENT (a tab, not a
user); applies leader rect changes via FitViewport, dedupes unchanged
republishes; break-on-interact compares local onViewport echoes against
the last applied rect (2% rel tolerance, echo-grace before the first fit
lands); unfollows on leader-left; pauses on eeschema sheet mismatch.
- PresenceRoster: avatars are follow toggles (ring on the followed peer);
WasmTool renders the "Following <name> — move to stop" banner.
- tests: 7 controller units (85/85 collab), fitViewport round-trip e2e in
both kicad presence specs (20/20), two-tab tests/web/follow.spec.ts
(converge → track → wheel-zoom breaks → subsequent moves ignored).
Chip depth-layer fix (user-reported): name chips washed out inside
low-alpha selection fills — chip rects shared the shapes overlay's single
depth, and same-depth fragments drawn LATER lose the depth test, so an
earlier-painted fill rejected the chip's pixels. Now three layers via the
fork's VIEW_OVERLAY::SetDepthOffset: text (0) < chips + pin dots (1) <
selection shapes (2). drawLabel/drawCursor/drawSelectionBox take the chip
overlay explicitly; comment-pin dots move to the chip layer too (the 0005
"drawn last so pins sit above" comment had the rule backwards). Verified
with a chip-inside-30%-fill pixel repro + the full presence suite.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-09 09:30:40 +02:00
|
|
|
pcbjam_presence::drawSelectionBox( aCore.overlay.get(), aCore.chipOverlay.get(),
|
|
|
|
|
aCore.textOverlay.get(),
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
item->ViewBBox(), peer.name, color, px,
|
|
|
|
|
ghost );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
feat(collab): presence P3 — eeschema port (collab-presence 0003)
- eeschema_embind.cpp: presence section (0002 pattern, zero fork changes) —
wx canvas triggers + SCHEMATIC_LISTENER piggyback → post-settle
SCH_SELECTION_TOOL emit; throttled cursor; remote VIEW_OVERLAY render
(SCHEMATIC::ResolveItem, name tags, screen-constant via GAL matrix);
schCollab{PresenceStart,SetRemote,GetViewport,GetSelection,TestSelectFirst,
TestClearSelection}; kicad_editor_embind dispatches by active frame.
- sheet-manager: parked rooms carry SKELETON awareness states
({user,tool,sheetPath=bound sheet}) via publishSkeletons on switch + late
warm-up — the bound room's awareness then holds every project peer, so no
multi-room aggregation; presence.ts publishSkeleton helper.
- PresenceRoster: sheet-aware — peers on another sheet render dimmed with
'on <sheet>' tooltip; WasmTool un-gates the kicad presence bridge for
eeschema and threads activeSheetPath.
- tests: presence-eeschema.spec.ts (5 e2e, mirrors pcbnew incl. the px/IU
band at eeschema's 1e4/mm IU); presence.test.ts +2 (skeleton visibility,
no ghost cursor after rebind). eeschema-collab/subschema stay green.
Verified live: two tabs on demo.kicad_sch — peer cursor cross + label,
selection box + name tag, roster avatar.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
2026-07-06 17:05:15 +02:00
|
|
|
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
return c;
|
|
|
|
|
}();
|
feat(collab): presence P3 — eeschema port (collab-presence 0003)
- eeschema_embind.cpp: presence section (0002 pattern, zero fork changes) —
wx canvas triggers + SCHEMATIC_LISTENER piggyback → post-settle
SCH_SELECTION_TOOL emit; throttled cursor; remote VIEW_OVERLAY render
(SCHEMATIC::ResolveItem, name tags, screen-constant via GAL matrix);
schCollab{PresenceStart,SetRemote,GetViewport,GetSelection,TestSelectFirst,
TestClearSelection}; kicad_editor_embind dispatches by active frame.
- sheet-manager: parked rooms carry SKELETON awareness states
({user,tool,sheetPath=bound sheet}) via publishSkeletons on switch + late
warm-up — the bound room's awareness then holds every project peer, so no
multi-room aggregation; presence.ts publishSkeleton helper.
- PresenceRoster: sheet-aware — peers on another sheet render dimmed with
'on <sheet>' tooltip; WasmTool un-gates the kicad presence bridge for
eeschema and threads activeSheetPath.
- tests: presence-eeschema.spec.ts (5 e2e, mirrors pcbnew incl. the px/IU
band at eeschema's 1e4/mm IU); presence.test.ts +2 (skeleton visibility,
no ghost cursor after rebind). eeschema-collab/subschema stay green.
Verified live: two tabs on demo.kicad_sch — peer cursor cross + label,
selection box + name tag, roster avatar.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
2026-07-06 17:05:15 +02:00
|
|
|
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
return core;
|
feat(collab): presence P3 — eeschema port (collab-presence 0003)
- eeschema_embind.cpp: presence section (0002 pattern, zero fork changes) —
wx canvas triggers + SCHEMATIC_LISTENER piggyback → post-settle
SCH_SELECTION_TOOL emit; throttled cursor; remote VIEW_OVERLAY render
(SCHEMATIC::ResolveItem, name tags, screen-constant via GAL matrix);
schCollab{PresenceStart,SetRemote,GetViewport,GetSelection,TestSelectFirst,
TestClearSelection}; kicad_editor_embind dispatches by active frame.
- sheet-manager: parked rooms carry SKELETON awareness states
({user,tool,sheetPath=bound sheet}) via publishSkeletons on switch + late
warm-up — the bound room's awareness then holds every project peer, so no
multi-room aggregation; presence.ts publishSkeleton helper.
- PresenceRoster: sheet-aware — peers on another sheet render dimmed with
'on <sheet>' tooltip; WasmTool un-gates the kicad presence bridge for
eeschema and threads activeSheetPath.
- tests: presence-eeschema.spec.ts (5 e2e, mirrors pcbnew incl. the px/IU
band at eeschema's 1e4/mm IU); presence.test.ts +2 (skeleton visibility,
no ghost cursor after rebind). eeschema-collab/subschema stay green.
Verified live: two tabs on demo.kicad_sch — peer cursor cross + label,
selection box + name tag, roster avatar.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
2026-07-06 17:05:15 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void schedulePresenceSelCheck()
|
|
|
|
|
{
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
presenceCore().scheduleSelCheck();
|
feat(collab): presence P3 — eeschema port (collab-presence 0003)
- eeschema_embind.cpp: presence section (0002 pattern, zero fork changes) —
wx canvas triggers + SCHEMATIC_LISTENER piggyback → post-settle
SCH_SELECTION_TOOL emit; throttled cursor; remote VIEW_OVERLAY render
(SCHEMATIC::ResolveItem, name tags, screen-constant via GAL matrix);
schCollab{PresenceStart,SetRemote,GetViewport,GetSelection,TestSelectFirst,
TestClearSelection}; kicad_editor_embind dispatches by active frame.
- sheet-manager: parked rooms carry SKELETON awareness states
({user,tool,sheetPath=bound sheet}) via publishSkeletons on switch + late
warm-up — the bound room's awareness then holds every project peer, so no
multi-room aggregation; presence.ts publishSkeleton helper.
- PresenceRoster: sheet-aware — peers on another sheet render dimmed with
'on <sheet>' tooltip; WasmTool un-gates the kicad presence bridge for
eeschema and threads activeSheetPath.
- tests: presence-eeschema.spec.ts (5 e2e, mirrors pcbnew incl. the px/IU
band at eeschema's 1e4/mm IU); presence.test.ts +2 (skeleton visibility,
no ghost cursor after rebind). eeschema-collab/subschema stay green.
Verified live: two tabs on demo.kicad_sch — peer cursor cross + label,
selection box + name tag, roster avatar.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
2026-07-06 17:05:15 +02:00
|
|
|
}
|
|
|
|
|
|
2026-08-28 16:14:44 +02:00
|
|
|
void schedulePresenceDocChanged()
|
|
|
|
|
{
|
|
|
|
|
presenceCore().onDocChanged();
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-03 17:49:11 +02:00
|
|
|
} // namespace
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
namespace {
|
|
|
|
|
|
2026-06-04 17:05:52 +02:00
|
|
|
// SCH_SYMBOL::Move() / SCH_LABEL_BASE::Move() move their child fields (reference, value, …) via
|
jspi cleanup: remove the asyncify-era residue — dead code, conditionals, pipeline scaffolding, stale prose
The runtime is JSPI-only; this removes everything that still pretended
otherwise. Three exhaustive sweeps (C++/JS+build+CI/tests+docs) drove
the inventory; every deletion verified by grep closure + full gates.
Broken-right-now fixes:
- deploy-staging.yml passed the retired opt_level input — the workflow
could not even start. Removed.
- env.sh carried dead exports with a live -sASYNCIFY=1 inside
(WASM_LDFLAGS/PTHREAD_LDFLAGS, zero consumers). Removed; the
WASM_LEGACY_EXCEPTIONS rationale rewritten to the real reason.
- docker/build.sh exported PCBJAM_ASYNC_BACKEND (read nowhere). Gone.
Dead weight removed:
- binaryen submodule (nothing builds or invokes it), wasm-opt-bench
workflow + scripts/bench/, get-wasm-opt.sh, diagnostics.js (242 lines
of Asyncify-API-only code), the KICAD_PIPELINE background-postprocess
scaffolding (existed to parallelize the deleted wasm-opt phase; the
postprocess is a seconds-long node script and now runs inline),
build-monitor's dead asyncify rows, sched-context orphan build
output, dead .gitignore entries, the .jspi-assets spike dir (the two
wf-result research JSONs moved to docs/features/async/migration-evidence/).
- bindings: fiber_park.h + its 12 embind registrations (broken-if-
called under JSPI), the kicadOpenFileStart/OPEN_JOB starter route,
main_stack_runner.h + 5 includes, the always-null context-sleep weak
hook in nanosleep_yield.c.
- shim: the backend field (installed-flag idempotency instead),
noteContextWait (dead both sides), the __wxAsyncifyDump alias (+ the
WasmTool fallback and string-dump normalize branch).
- web: the emscripten-6-ignored mainScriptUrlOrBlob option in boot.ts
(gerber-demo keeps it: it loads the deployed CDN release, which
predates emscripten 6 — noted inline).
Conditionals: all 'backend === jspi' checks reduced to scheduler-
presence checks; races_quiescent re-keyed from Asyncify.state (vacuous)
to real backlog quiescence (resumeReady/mutatorQueue — NOT _windowLive,
which is the probing activation's own window by definition).
Renames (identifiers only, no file renames): ASYNC_LINK_FLAGS→
JSPI_LINK_FLAGS and Makefile ASYNC_LDFLAGS→JSPI_LDFLAGS,
kicadCollabFiberBusy→kicadCollabBusy (embind + web + tests),
collab_common.h fiber*→apply*/coroutine naming, asyncifySignatures→
wasmTrapSignatures (lists byte-identical).
Tests: the two remaining vacuous [wx-asyncify]/fiber-resume-refused
asserts re-keyed to live JSPI beacons; eeschema-load's failure message
no longer sends the developer to a deleted script; wait-beacons' dead
families/parser deleted; lane-0 legacy-glue guards removed (lane 0 is
unconstructible); the embind test.fail re-gated with the JSPI reason
(plain embind invokers cannot suspend — verified still failing);
lint-determinism now scans tests/jspi (166 files clean);
eeschema-collab local-move gated to chromium (~50% flaky on FF even
solo; pcbnew twin covers both engines).
Docs: DEBUG.md rewritten as the JSPI debugging guide; build.md
describes the single-phase build; docs/features/async/README.md
banner-marked historical and repointed at the NEW
23-jspi-runtime.md (current architecture: export census, turnstile,
libcontext ownership + refusal contract, embind call shapes, the
em-pthread service-wrapper trick, exception policy, known gaps).
Gates on the cleaned tree: test:e2e 725 passed / 0 failed (after the
quiescence-probe fix; the 3 other reds were verified contention flakes
solo-green or the documented FF gate), web 76/0, jspi 18/18 both
engines, vitest 295/295 + 17/17, all lints green, live-app census
clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016X9eh1s5sTx1o9Em9KBuwR
2026-08-14 09:25:32 +02:00
|
|
|
// an inner `field.Move()`. Under the retired asyncify runtime that inner call silently no-oped
|
|
|
|
|
// in the apply context — the field text was left behind at its old position while the body
|
|
|
|
|
// moved — so the fields are re-moved here with a devirtualized call. Kept as-is through the
|
|
|
|
|
// JSPI migration; the collab move/drift suites pin the peer's field positions.
|
2026-06-04 17:05:52 +02:00
|
|
|
void moveFields( std::vector<SCH_FIELD>& aFields, const VECTOR2I& aDelta )
|
|
|
|
|
{
|
|
|
|
|
for( SCH_FIELD& field : aFields )
|
|
|
|
|
field.SCH_FIELD::Move( aDelta );
|
|
|
|
|
}
|
|
|
|
|
|
jspi cleanup: remove the asyncify-era residue — dead code, conditionals, pipeline scaffolding, stale prose
The runtime is JSPI-only; this removes everything that still pretended
otherwise. Three exhaustive sweeps (C++/JS+build+CI/tests+docs) drove
the inventory; every deletion verified by grep closure + full gates.
Broken-right-now fixes:
- deploy-staging.yml passed the retired opt_level input — the workflow
could not even start. Removed.
- env.sh carried dead exports with a live -sASYNCIFY=1 inside
(WASM_LDFLAGS/PTHREAD_LDFLAGS, zero consumers). Removed; the
WASM_LEGACY_EXCEPTIONS rationale rewritten to the real reason.
- docker/build.sh exported PCBJAM_ASYNC_BACKEND (read nowhere). Gone.
Dead weight removed:
- binaryen submodule (nothing builds or invokes it), wasm-opt-bench
workflow + scripts/bench/, get-wasm-opt.sh, diagnostics.js (242 lines
of Asyncify-API-only code), the KICAD_PIPELINE background-postprocess
scaffolding (existed to parallelize the deleted wasm-opt phase; the
postprocess is a seconds-long node script and now runs inline),
build-monitor's dead asyncify rows, sched-context orphan build
output, dead .gitignore entries, the .jspi-assets spike dir (the two
wf-result research JSONs moved to docs/features/async/migration-evidence/).
- bindings: fiber_park.h + its 12 embind registrations (broken-if-
called under JSPI), the kicadOpenFileStart/OPEN_JOB starter route,
main_stack_runner.h + 5 includes, the always-null context-sleep weak
hook in nanosleep_yield.c.
- shim: the backend field (installed-flag idempotency instead),
noteContextWait (dead both sides), the __wxAsyncifyDump alias (+ the
WasmTool fallback and string-dump normalize branch).
- web: the emscripten-6-ignored mainScriptUrlOrBlob option in boot.ts
(gerber-demo keeps it: it loads the deployed CDN release, which
predates emscripten 6 — noted inline).
Conditionals: all 'backend === jspi' checks reduced to scheduler-
presence checks; races_quiescent re-keyed from Asyncify.state (vacuous)
to real backlog quiescence (resumeReady/mutatorQueue — NOT _windowLive,
which is the probing activation's own window by definition).
Renames (identifiers only, no file renames): ASYNC_LINK_FLAGS→
JSPI_LINK_FLAGS and Makefile ASYNC_LDFLAGS→JSPI_LDFLAGS,
kicadCollabFiberBusy→kicadCollabBusy (embind + web + tests),
collab_common.h fiber*→apply*/coroutine naming, asyncifySignatures→
wasmTrapSignatures (lists byte-identical).
Tests: the two remaining vacuous [wx-asyncify]/fiber-resume-refused
asserts re-keyed to live JSPI beacons; eeschema-load's failure message
no longer sends the developer to a deleted script; wait-beacons' dead
families/parser deleted; lane-0 legacy-glue guards removed (lane 0 is
unconstructible); the embind test.fail re-gated with the JSPI reason
(plain embind invokers cannot suspend — verified still failing);
lint-determinism now scans tests/jspi (166 files clean);
eeschema-collab local-move gated to chromium (~50% flaky on FF even
solo; pcbnew twin covers both engines).
Docs: DEBUG.md rewritten as the JSPI debugging guide; build.md
describes the single-phase build; docs/features/async/README.md
banner-marked historical and repointed at the NEW
23-jspi-runtime.md (current architecture: export census, turnstile,
libcontext ownership + refusal contract, embind call shapes, the
em-pthread service-wrapper trick, exception policy, known gaps).
Gates on the cleaned tree: test:e2e 725 passed / 0 failed (after the
quiescence-probe fix; the 3 other reds were verified contention flakes
solo-green or the documented FF gate), web 76/0, jspi 18/18 both
engines, vitest 295/295 + 17/17, all lints green, live-app census
clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016X9eh1s5sTx1o9Em9KBuwR
2026-08-14 09:25:32 +02:00
|
|
|
// Move an item to an absolute position for the `changed` path. Under the retired asyncify
|
|
|
|
|
// runtime the virtual SCH_ITEM::Move(), dispatched from the apply/CallAfter context, silently
|
|
|
|
|
// NO-OPED (symbols/junctions/labels never moved on the peer — only SCH_LINE worked, via its
|
|
|
|
|
// direct SetStart/EndPoint path; see 0003 / eeschema_collab_asyncify_apply). Move() is
|
|
|
|
|
// therefore devirtualized with explicit class-qualified calls — statically bound plain wasm
|
|
|
|
|
// calls, correct on any runtime, so they stay. Composite items additionally need their child
|
|
|
|
|
// fields moved (see moveFields).
|
2026-06-04 15:18:12 +02:00
|
|
|
void moveItemTo( SCH_ITEM* aItem, const VECTOR2I& aNewPos )
|
|
|
|
|
{
|
|
|
|
|
VECTOR2I delta = aNewPos - aItem->GetPosition();
|
|
|
|
|
|
|
|
|
|
if( delta == VECTOR2I( 0, 0 ) )
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
switch( aItem->Type() )
|
|
|
|
|
{
|
2026-06-04 17:05:52 +02:00
|
|
|
case SCH_SYMBOL_T:
|
|
|
|
|
{
|
|
|
|
|
auto* sym = static_cast<SCH_SYMBOL*>( aItem );
|
|
|
|
|
sym->SCH_SYMBOL::Move( delta );
|
|
|
|
|
moveFields( sym->GetFields(), delta );
|
|
|
|
|
break;
|
|
|
|
|
}
|
2026-06-04 15:18:12 +02:00
|
|
|
case SCH_JUNCTION_T: static_cast<SCH_JUNCTION*>( aItem )->SCH_JUNCTION::Move( delta ); break;
|
|
|
|
|
case SCH_NO_CONNECT_T: static_cast<SCH_NO_CONNECT*>( aItem )->SCH_NO_CONNECT::Move( delta ); break;
|
|
|
|
|
case SCH_TEXT_T: static_cast<SCH_TEXT*>( aItem )->SCH_TEXT::Move( delta ); break;
|
|
|
|
|
case SCH_LABEL_T:
|
|
|
|
|
case SCH_GLOBAL_LABEL_T:
|
|
|
|
|
case SCH_HIER_LABEL_T:
|
2026-06-04 17:05:52 +02:00
|
|
|
{
|
|
|
|
|
auto* lbl = static_cast<SCH_LABEL_BASE*>( aItem );
|
|
|
|
|
lbl->SCH_LABEL_BASE::Move( delta );
|
|
|
|
|
moveFields( lbl->GetFields(), delta );
|
2026-06-04 15:18:12 +02:00
|
|
|
break;
|
2026-06-04 17:05:52 +02:00
|
|
|
}
|
2026-06-04 15:18:12 +02:00
|
|
|
default:
|
|
|
|
|
aItem->Move( delta ); // virtual fallback (may no-op in the apply context)
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-03 19:37:15 +02:00
|
|
|
// The actual model mutation, via SCH_COMMIT so connectivity/ERC recompute as for a UI
|
jspi cleanup: remove the asyncify-era residue — dead code, conditionals, pipeline scaffolding, stale prose
The runtime is JSPI-only; this removes everything that still pretended
otherwise. Three exhaustive sweeps (C++/JS+build+CI/tests+docs) drove
the inventory; every deletion verified by grep closure + full gates.
Broken-right-now fixes:
- deploy-staging.yml passed the retired opt_level input — the workflow
could not even start. Removed.
- env.sh carried dead exports with a live -sASYNCIFY=1 inside
(WASM_LDFLAGS/PTHREAD_LDFLAGS, zero consumers). Removed; the
WASM_LEGACY_EXCEPTIONS rationale rewritten to the real reason.
- docker/build.sh exported PCBJAM_ASYNC_BACKEND (read nowhere). Gone.
Dead weight removed:
- binaryen submodule (nothing builds or invokes it), wasm-opt-bench
workflow + scripts/bench/, get-wasm-opt.sh, diagnostics.js (242 lines
of Asyncify-API-only code), the KICAD_PIPELINE background-postprocess
scaffolding (existed to parallelize the deleted wasm-opt phase; the
postprocess is a seconds-long node script and now runs inline),
build-monitor's dead asyncify rows, sched-context orphan build
output, dead .gitignore entries, the .jspi-assets spike dir (the two
wf-result research JSONs moved to docs/features/async/migration-evidence/).
- bindings: fiber_park.h + its 12 embind registrations (broken-if-
called under JSPI), the kicadOpenFileStart/OPEN_JOB starter route,
main_stack_runner.h + 5 includes, the always-null context-sleep weak
hook in nanosleep_yield.c.
- shim: the backend field (installed-flag idempotency instead),
noteContextWait (dead both sides), the __wxAsyncifyDump alias (+ the
WasmTool fallback and string-dump normalize branch).
- web: the emscripten-6-ignored mainScriptUrlOrBlob option in boot.ts
(gerber-demo keeps it: it loads the deployed CDN release, which
predates emscripten 6 — noted inline).
Conditionals: all 'backend === jspi' checks reduced to scheduler-
presence checks; races_quiescent re-keyed from Asyncify.state (vacuous)
to real backlog quiescence (resumeReady/mutatorQueue — NOT _windowLive,
which is the probing activation's own window by definition).
Renames (identifiers only, no file renames): ASYNC_LINK_FLAGS→
JSPI_LINK_FLAGS and Makefile ASYNC_LDFLAGS→JSPI_LDFLAGS,
kicadCollabFiberBusy→kicadCollabBusy (embind + web + tests),
collab_common.h fiber*→apply*/coroutine naming, asyncifySignatures→
wasmTrapSignatures (lists byte-identical).
Tests: the two remaining vacuous [wx-asyncify]/fiber-resume-refused
asserts re-keyed to live JSPI beacons; eeschema-load's failure message
no longer sends the developer to a deleted script; wait-beacons' dead
families/parser deleted; lane-0 legacy-glue guards removed (lane 0 is
unconstructible); the embind test.fail re-gated with the JSPI reason
(plain embind invokers cannot suspend — verified still failing);
lint-determinism now scans tests/jspi (166 files clean);
eeschema-collab local-move gated to chromium (~50% flaky on FF even
solo; pcbnew twin covers both engines).
Docs: DEBUG.md rewritten as the JSPI debugging guide; build.md
describes the single-phase build; docs/features/async/README.md
banner-marked historical and repointed at the NEW
23-jspi-runtime.md (current architecture: export census, turnstile,
libcontext ownership + refusal contract, embind call shapes, the
em-pthread service-wrapper trick, exception policy, known gaps).
Gates on the cleaned tree: test:e2e 725 passed / 0 failed (after the
quiescence-probe fix; the 3 other reds were verified contention flakes
solo-green or the documented FF gate), web 76/0, jspi 18/18 both
engines, vitest 295/295 + 17/17, all lints green, live-app census
clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016X9eh1s5sTx1o9Em9KBuwR
2026-08-14 09:25:32 +02:00
|
|
|
// edit. (0003 decoded an asyncify-era dynCall trap on this path; the shim that healed
|
|
|
|
|
// it retired with that runtime.)
|
2026-06-03 17:49:11 +02:00
|
|
|
void doApply( SCH_EDIT_FRAME* aFrame, const json& aDelta )
|
|
|
|
|
{
|
|
|
|
|
SCHEMATIC& sch = aFrame->Schematic();
|
|
|
|
|
|
|
|
|
|
s_applyingRemote = true;
|
|
|
|
|
|
|
|
|
|
SCH_COMMIT commit( aFrame );
|
|
|
|
|
bool staged = false;
|
|
|
|
|
|
2026-07-08 11:09:26 +02:00
|
|
|
// With SKIP_UNDO no undo picker takes ownership of removed items; the commit
|
|
|
|
|
// detaches them from the screen and we free them after Push. Fields are hidden
|
|
|
|
|
// by CHT_REMOVE, not detached (still owned by their parent), so never freed.
|
|
|
|
|
std::vector<SCH_ITEM*> removedItems;
|
|
|
|
|
|
2026-06-03 17:49:11 +02:00
|
|
|
for( const json& rid : aDelta.value( "removed", json::array() ) )
|
|
|
|
|
{
|
|
|
|
|
SCH_SHEET_PATH path;
|
|
|
|
|
KIID id( wxString::FromUTF8( rid.get<std::string>().c_str() ) );
|
|
|
|
|
|
|
|
|
|
if( SCH_ITEM* item = sch.ResolveItem( id, &path, /*allowNull*/ true ) )
|
|
|
|
|
{
|
|
|
|
|
commit.Remove( item, path.LastScreen() );
|
2026-07-08 11:09:26 +02:00
|
|
|
|
|
|
|
|
if( item->Type() != SCH_FIELD_T )
|
|
|
|
|
removedItems.push_back( item );
|
|
|
|
|
|
2026-06-03 17:49:11 +02:00
|
|
|
staged = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for( const json& j : aDelta.value( "changed", json::array() ) )
|
|
|
|
|
{
|
|
|
|
|
SCH_SHEET_PATH path;
|
|
|
|
|
KIID id( wxString::FromUTF8( j.value( "id", "" ).c_str() ) );
|
|
|
|
|
|
|
|
|
|
if( SCH_ITEM* item = sch.ResolveItem( id, &path, /*allowNull*/ true ) )
|
|
|
|
|
{
|
|
|
|
|
commit.Modify( item, path.LastScreen() );
|
|
|
|
|
|
2026-06-03 19:37:15 +02:00
|
|
|
if( item->Type() == SCH_LINE_T && j.contains( "sx" ) )
|
|
|
|
|
{
|
|
|
|
|
// Wires reshape (endpoints move independently), so set both points.
|
|
|
|
|
auto* line = static_cast<SCH_LINE*>( item );
|
|
|
|
|
line->SetStartPoint( VECTOR2I( j["sx"].get<int>(), j["sy"].get<int>() ) );
|
|
|
|
|
line->SetEndPoint( VECTOR2I( j["ex"].get<int>(), j["ey"].get<int>() ) );
|
|
|
|
|
}
|
|
|
|
|
else if( j.contains( "x" ) && j.contains( "y" ) )
|
2026-06-03 17:49:11 +02:00
|
|
|
{
|
2026-06-04 15:18:12 +02:00
|
|
|
moveItemTo( item, VECTOR2I( j["x"].get<int>(), j["y"].get<int>() ) );
|
2026-06-03 17:49:11 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
staged = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-03 19:37:15 +02:00
|
|
|
for( const json& j : aDelta.value( "added", json::array() ) )
|
|
|
|
|
{
|
|
|
|
|
KIID id( wxString::FromUTF8( j.value( "id", "" ).c_str() ) );
|
|
|
|
|
|
|
|
|
|
if( sch.ResolveItem( id, nullptr, /*allowNull*/ true ) )
|
|
|
|
|
continue; // already present (our own echo)
|
|
|
|
|
|
|
|
|
|
if( SCH_ITEM* item = makeItem( j ) )
|
|
|
|
|
{
|
|
|
|
|
commit.Add( item, aFrame->GetScreen() );
|
|
|
|
|
staged = true;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
EM_ASM( { console.log( "[collab] eeschema apply: no converter for added type " + UTF8ToString( $0 ) ); },
|
|
|
|
|
j.value( "type", "?" ).c_str() );
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-06-03 17:49:11 +02:00
|
|
|
|
2026-07-08 11:09:26 +02:00
|
|
|
// SKIP_UNDO: a peer's edit must never land on this editor's undo stack — Ctrl+Z
|
|
|
|
|
// would revert (and re-broadcast) the peer's work. Undo is local-ops-only; stale
|
|
|
|
|
// local undo entries are dropped/re-resolved by UUID at undo time (miss 09).
|
2026-06-03 17:49:11 +02:00
|
|
|
if( staged )
|
2026-07-08 11:09:26 +02:00
|
|
|
commit.Push( wxT( "Collaborative edit" ), SKIP_UNDO );
|
|
|
|
|
|
|
|
|
|
for( SCH_ITEM* item : removedItems )
|
|
|
|
|
delete item;
|
2026-06-03 17:49:11 +02:00
|
|
|
|
2026-06-05 13:30:47 +02:00
|
|
|
// The applied remote changes (and any connectivity cleanup they triggered) are now the
|
|
|
|
|
// shared state — fold them into the baseline so the post-apply listener flush doesn't
|
|
|
|
|
// re-broadcast them as a local diff (echo).
|
|
|
|
|
rebaseline();
|
2026-06-03 17:49:11 +02:00
|
|
|
s_applyingRemote = false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-11 13:55:15 +02:00
|
|
|
// v2 items apply: removed by uuid; added/changed are an idempotent per-item upsert.
|
|
|
|
|
// Each blob is parsed through the clipboard-paste path — LoadContent into a throwaway
|
|
|
|
|
// sheet (sch_editor_control.cpp Paste pattern) — then the loaded items are detached,
|
|
|
|
|
// matched by uuid against the live model (replace), lib-relinked for symbols, and
|
|
|
|
|
// committed. Runs inside the apply COROUTINE (see kicadCollabApplyItems).
|
2026-08-28 18:19:29 +02:00
|
|
|
// ysync bug 07 (UP side): an apply is queued for the room of ONE sheet but runs
|
|
|
|
|
// deferred on whatever screen is active when it runs. The JS binding stamps the
|
|
|
|
|
// envelope with its sheet's project-relative path; if that is not the screen
|
|
|
|
|
// shown now, adding the items here would put another sheet's items on this
|
|
|
|
|
// screen (8/28: the root's `(sheet …)` entries landed in a subsheet, which then
|
|
|
|
|
// referenced itself and failed to load). Drop it — the binding for the shown
|
|
|
|
|
// sheet reconciles on its own bind.
|
|
|
|
|
static bool applyTargetsShownSheet( SCH_EDIT_FRAME* aFrame, const json& aWire )
|
|
|
|
|
{
|
|
|
|
|
auto it = aWire.find( "sheet" );
|
|
|
|
|
|
|
|
|
|
if( it == aWire.end() || !it->is_string() )
|
|
|
|
|
return true; // untagged (single-file tools, older clients): legacy behaviour
|
|
|
|
|
|
|
|
|
|
const std::string want = it->get<std::string>();
|
|
|
|
|
SCH_SCREEN* cur = currentScreen( aFrame );
|
|
|
|
|
const std::string have = cur ? toUtf8( cur->GetFileName() ) : std::string();
|
|
|
|
|
|
|
|
|
|
if( have == want )
|
|
|
|
|
return true;
|
|
|
|
|
|
|
|
|
|
// `have` is absolute (MEMFS project dir + relative path); `want` is relative.
|
|
|
|
|
if( have.size() > want.size() && have[have.size() - want.size() - 1] == '/'
|
|
|
|
|
&& have.compare( have.size() - want.size(), want.size(), want ) == 0 )
|
|
|
|
|
return true;
|
|
|
|
|
|
|
|
|
|
EM_ASM( { console.warn( "[collab] applyItems dropped: envelope sheet " + UTF8ToString( $0 )
|
|
|
|
|
+ " != shown sheet " + UTF8ToString( $1 ) ); },
|
|
|
|
|
want.c_str(), have.c_str() );
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2026-06-11 13:55:15 +02:00
|
|
|
void doApplyItems( SCH_EDIT_FRAME* aFrame, const json& aWire )
|
|
|
|
|
{
|
2026-08-28 18:19:29 +02:00
|
|
|
if( !applyTargetsShownSheet( aFrame, aWire ) )
|
|
|
|
|
return;
|
|
|
|
|
|
2026-06-11 13:55:15 +02:00
|
|
|
SCHEMATIC& sch = aFrame->Schematic();
|
|
|
|
|
|
|
|
|
|
s_applyingRemote = true;
|
|
|
|
|
|
|
|
|
|
SCH_COMMIT commit( aFrame );
|
|
|
|
|
bool staged = false;
|
|
|
|
|
|
fix(ysync): review bugs 01–07 — listener registration, pad nets, child removals, dirty-set emit, targeted rebaseline, seed arbitration, stale hook (doc 17)
All seven ysync-review bugs fixed and verified; every repro's expected-fail
marker removed (they now run as regression tests). Also fixes two bugs found
while verifying (doc 17 F5/F6): file-seeded Y bodies are re-upserted in the
editor's serialization (doc-16 F4 was an artifact of bug 01), and the
0008-era "asyncify-fragile envelope parse" was really wrapInBoardEnvelope
emitting display layer names — canonical LSET::Name() fixes track/via/zone
v2 applies; makeFromBlob now logs parse errors instead of swallowing them.
Verified: shared 98, standalone collab 37, ysync e2e 20/20 (chromium),
collab regression set 21 passed / 3 pre-existing skips (firefox).
Bumps: kicad (board_commit child-removal listener notification),
web/pcbjam-shared (slot prune + arbitrated seed).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JgThWXtdvrYLK47EDFoGdq
2026-07-03 18:08:37 +02:00
|
|
|
std::vector<std::string> touched; // uuids this apply acts on (targeted rebaseline)
|
|
|
|
|
|
2026-07-08 11:09:26 +02:00
|
|
|
// Owned by nobody once the SKIP_UNDO commit detaches them — freed after Push
|
|
|
|
|
// (fields are hidden, not detached, so excluded). See doApply.
|
|
|
|
|
std::vector<SCH_ITEM*> removedItems;
|
|
|
|
|
|
2026-06-11 13:55:15 +02:00
|
|
|
for( const json& rid : aWire.value( "removed", json::array() ) )
|
|
|
|
|
{
|
|
|
|
|
SCH_SHEET_PATH path;
|
|
|
|
|
KIID id( wxString::FromUTF8( rid.get<std::string>().c_str() ) );
|
|
|
|
|
|
fix(ysync): review bugs 01–07 — listener registration, pad nets, child removals, dirty-set emit, targeted rebaseline, seed arbitration, stale hook (doc 17)
All seven ysync-review bugs fixed and verified; every repro's expected-fail
marker removed (they now run as regression tests). Also fixes two bugs found
while verifying (doc 17 F5/F6): file-seeded Y bodies are re-upserted in the
editor's serialization (doc-16 F4 was an artifact of bug 01), and the
0008-era "asyncify-fragile envelope parse" was really wrapInBoardEnvelope
emitting display layer names — canonical LSET::Name() fixes track/via/zone
v2 applies; makeFromBlob now logs parse errors instead of swallowing them.
Verified: shared 98, standalone collab 37, ysync e2e 20/20 (chromium),
collab regression set 21 passed / 3 pre-existing skips (firefox).
Bumps: kicad (board_commit child-removal listener notification),
web/pcbjam-shared (slot prune + arbitrated seed).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JgThWXtdvrYLK47EDFoGdq
2026-07-03 18:08:37 +02:00
|
|
|
touched.push_back( rid.get<std::string>() );
|
|
|
|
|
|
2026-06-11 13:55:15 +02:00
|
|
|
if( SCH_ITEM* item = sch.ResolveItem( id, &path, /*allowNull*/ true ) )
|
|
|
|
|
{
|
|
|
|
|
commit.Remove( item, path.LastScreen() );
|
2026-07-08 11:09:26 +02:00
|
|
|
|
|
|
|
|
if( item->Type() != SCH_FIELD_T )
|
|
|
|
|
removedItems.push_back( item );
|
|
|
|
|
|
2026-06-11 13:55:15 +02:00
|
|
|
staged = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto upsert = [&]( const json& w )
|
|
|
|
|
{
|
|
|
|
|
std::string sexpr = w.value( "sexpr", "" );
|
|
|
|
|
|
|
|
|
|
if( sexpr.find_first_not_of( " \t\r\n" ) == std::string::npos )
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
// Parse into a throwaway sheet exactly like clipboard paste does. The screen
|
|
|
|
|
// is heap-allocated and owned by the sheet (freed with it).
|
|
|
|
|
SCH_SHEET tempSheet;
|
|
|
|
|
SCH_SCREEN* tempScreen = new SCH_SCREEN( &sch );
|
|
|
|
|
tempSheet.SetScreen( tempScreen );
|
|
|
|
|
|
|
|
|
|
STRING_LINE_READER reader( sexpr, wxT( "collab-items" ) );
|
|
|
|
|
SCH_IO_KICAD_SEXPR plugin;
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
plugin.LoadContent( reader, &tempSheet );
|
|
|
|
|
}
|
|
|
|
|
catch( ... )
|
|
|
|
|
{
|
|
|
|
|
EM_ASM( { console.log( "[collab] eeschema applyItems: blob parse failed" ); } );
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Resolve a symbol's LIB_SYMBOL: prefer the blob's own (lib_symbols …) cache
|
|
|
|
|
// (a clipboard-style blob carries it), else the live screen's — peers share
|
|
|
|
|
// the same document so the definition is normally already present.
|
|
|
|
|
auto findLib = [&]( SCH_SYMBOL* aSym ) -> LIB_SYMBOL*
|
|
|
|
|
{
|
|
|
|
|
wxString lookup = aSym->GetLibId().Format().wx_str();
|
|
|
|
|
|
|
|
|
|
if( !aSym->UseLibIdLookup() )
|
|
|
|
|
lookup = aSym->GetSchSymbolLibraryName();
|
|
|
|
|
|
|
|
|
|
auto& tlibs = tempScreen->GetLibSymbols();
|
|
|
|
|
auto ti = tlibs.find( lookup );
|
|
|
|
|
|
|
|
|
|
if( ti != tlibs.end() )
|
|
|
|
|
return new LIB_SYMBOL( *ti->second );
|
|
|
|
|
|
|
|
|
|
auto& libs = aFrame->GetScreen()->GetLibSymbols();
|
|
|
|
|
auto li = libs.find( lookup );
|
|
|
|
|
|
|
|
|
|
if( li != libs.end() )
|
|
|
|
|
return new LIB_SYMBOL( *li->second );
|
|
|
|
|
|
|
|
|
|
return nullptr;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
std::vector<SCH_ITEM*> loaded;
|
|
|
|
|
|
|
|
|
|
for( SCH_ITEM* item : tempScreen->Items() )
|
|
|
|
|
loaded.push_back( item );
|
|
|
|
|
|
|
|
|
|
for( SCH_ITEM* item : loaded )
|
|
|
|
|
{
|
|
|
|
|
tempScreen->Remove( item ); // detach: tempSheet's dtor must not free it
|
|
|
|
|
|
|
|
|
|
SCH_SHEET_PATH path;
|
|
|
|
|
|
|
|
|
|
if( SCH_ITEM* existing = sch.ResolveItem( item->m_Uuid, &path, /*allowNull*/ true ) )
|
2026-07-08 11:09:26 +02:00
|
|
|
{
|
2026-06-11 13:55:15 +02:00
|
|
|
commit.Remove( existing, path.LastScreen() );
|
|
|
|
|
|
2026-07-08 11:09:26 +02:00
|
|
|
if( existing->Type() != SCH_FIELD_T )
|
|
|
|
|
removedItems.push_back( existing );
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-11 13:55:15 +02:00
|
|
|
if( item->Type() == SCH_SYMBOL_T )
|
|
|
|
|
{
|
|
|
|
|
auto* sym = static_cast<SCH_SYMBOL*>( item );
|
|
|
|
|
|
|
|
|
|
if( LIB_SYMBOL* lib = findLib( sym ) )
|
|
|
|
|
sym->SetLibSymbol( lib );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
item->SetParent( &sch );
|
fix(ysync): review bugs 01–07 — listener registration, pad nets, child removals, dirty-set emit, targeted rebaseline, seed arbitration, stale hook (doc 17)
All seven ysync-review bugs fixed and verified; every repro's expected-fail
marker removed (they now run as regression tests). Also fixes two bugs found
while verifying (doc 17 F5/F6): file-seeded Y bodies are re-upserted in the
editor's serialization (doc-16 F4 was an artifact of bug 01), and the
0008-era "asyncify-fragile envelope parse" was really wrapInBoardEnvelope
emitting display layer names — canonical LSET::Name() fixes track/via/zone
v2 applies; makeFromBlob now logs parse errors instead of swallowing them.
Verified: shared 98, standalone collab 37, ysync e2e 20/20 (chromium),
collab regression set 21 passed / 3 pre-existing skips (firefox).
Bumps: kicad (board_commit child-removal listener notification),
web/pcbjam-shared (slot prune + arbitrated seed).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JgThWXtdvrYLK47EDFoGdq
2026-07-03 18:08:37 +02:00
|
|
|
touched.push_back( toUtf8( item->m_Uuid.AsString() ) );
|
2026-06-11 13:55:15 +02:00
|
|
|
commit.Add( item, aFrame->GetScreen() );
|
|
|
|
|
staged = true;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
for( const json& w : aWire.value( "added", json::array() ) )
|
|
|
|
|
upsert( w );
|
|
|
|
|
for( const json& w : aWire.value( "changed", json::array() ) )
|
|
|
|
|
upsert( w );
|
|
|
|
|
|
2026-07-08 11:09:26 +02:00
|
|
|
// SKIP_UNDO: remote applies never land on the local undo stack (see doApply).
|
2026-06-11 13:55:15 +02:00
|
|
|
if( staged )
|
2026-07-08 11:09:26 +02:00
|
|
|
commit.Push( wxT( "Collaborative edit (items)" ), SKIP_UNDO );
|
|
|
|
|
|
|
|
|
|
for( SCH_ITEM* item : removedItems )
|
|
|
|
|
delete item;
|
2026-06-11 13:55:15 +02:00
|
|
|
|
fix(ysync): review bugs 01–07 — listener registration, pad nets, child removals, dirty-set emit, targeted rebaseline, seed arbitration, stale hook (doc 17)
All seven ysync-review bugs fixed and verified; every repro's expected-fail
marker removed (they now run as regression tests). Also fixes two bugs found
while verifying (doc 17 F5/F6): file-seeded Y bodies are re-upserted in the
editor's serialization (doc-16 F4 was an artifact of bug 01), and the
0008-era "asyncify-fragile envelope parse" was really wrapInBoardEnvelope
emitting display layer names — canonical LSET::Name() fixes track/via/zone
v2 applies; makeFromBlob now logs parse errors instead of swallowing them.
Verified: shared 98, standalone collab 37, ysync e2e 20/20 (chromium),
collab regression set 21 passed / 3 pre-existing skips (firefox).
Bumps: kicad (board_commit child-removal listener notification),
web/pcbjam-shared (slot prune + arbitrated seed).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JgThWXtdvrYLK47EDFoGdq
2026-07-03 18:08:37 +02:00
|
|
|
// Fold ONLY the applied uuids into the baseline (echo suppression), then flush:
|
|
|
|
|
// anything else that now differs — a concurrent local edit, the connectivity
|
|
|
|
|
// cleanup this apply's Push produced — broadcasts as a normal local diff
|
|
|
|
|
// instead of being swallowed (bug 05).
|
|
|
|
|
rebaselineTouched( aFrame, touched );
|
2026-06-11 13:55:15 +02:00
|
|
|
s_applyingRemote = false;
|
fix(ysync): review bugs 01–07 — listener registration, pad nets, child removals, dirty-set emit, targeted rebaseline, seed arbitration, stale hook (doc 17)
All seven ysync-review bugs fixed and verified; every repro's expected-fail
marker removed (they now run as regression tests). Also fixes two bugs found
while verifying (doc 17 F5/F6): file-seeded Y bodies are re-upserted in the
editor's serialization (doc-16 F4 was an artifact of bug 01), and the
0008-era "asyncify-fragile envelope parse" was really wrapInBoardEnvelope
emitting display layer names — canonical LSET::Name() fixes track/via/zone
v2 applies; makeFromBlob now logs parse errors instead of swallowing them.
Verified: shared 98, standalone collab 37, ysync e2e 20/20 (chromium),
collab regression set 21 passed / 3 pre-existing skips (firefox).
Bumps: kicad (board_commit child-removal listener notification),
web/pcbjam-shared (slot prune + arbitrated seed).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JgThWXtdvrYLK47EDFoGdq
2026-07-03 18:08:37 +02:00
|
|
|
scheduleFlush();
|
2026-06-11 13:55:15 +02:00
|
|
|
}
|
|
|
|
|
|
2026-06-03 19:37:15 +02:00
|
|
|
// Test/PoC move (the SCH_COMMIT body for kicadCollabTestMoveFirst, deferred via CallAfter).
|
|
|
|
|
void collabTestMove( SCH_EDIT_FRAME* aFrame, SCH_ITEM* aItem, SCH_SCREEN* aScreen, int aDx,
|
|
|
|
|
int aDy )
|
|
|
|
|
{
|
|
|
|
|
SCH_COMMIT commit( aFrame );
|
|
|
|
|
commit.Modify( aItem, aScreen );
|
|
|
|
|
aItem->Move( VECTOR2I( aDx, aDy ) );
|
|
|
|
|
commit.Push( wxT( "Collab test move" ) );
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-03 17:49:11 +02:00
|
|
|
} // namespace
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// JS → C++. Apply a remote per-item delta by uuid, through SCH_COMMIT so connectivity/
|
|
|
|
|
// ERC/hierarchy recompute the same way a UI edit would (0003 §apply).
|
|
|
|
|
//
|
jspi cleanup: remove the asyncify-era residue — dead code, conditionals, pipeline scaffolding, stale prose
The runtime is JSPI-only; this removes everything that still pretended
otherwise. Three exhaustive sweeps (C++/JS+build+CI/tests+docs) drove
the inventory; every deletion verified by grep closure + full gates.
Broken-right-now fixes:
- deploy-staging.yml passed the retired opt_level input — the workflow
could not even start. Removed.
- env.sh carried dead exports with a live -sASYNCIFY=1 inside
(WASM_LDFLAGS/PTHREAD_LDFLAGS, zero consumers). Removed; the
WASM_LEGACY_EXCEPTIONS rationale rewritten to the real reason.
- docker/build.sh exported PCBJAM_ASYNC_BACKEND (read nowhere). Gone.
Dead weight removed:
- binaryen submodule (nothing builds or invokes it), wasm-opt-bench
workflow + scripts/bench/, get-wasm-opt.sh, diagnostics.js (242 lines
of Asyncify-API-only code), the KICAD_PIPELINE background-postprocess
scaffolding (existed to parallelize the deleted wasm-opt phase; the
postprocess is a seconds-long node script and now runs inline),
build-monitor's dead asyncify rows, sched-context orphan build
output, dead .gitignore entries, the .jspi-assets spike dir (the two
wf-result research JSONs moved to docs/features/async/migration-evidence/).
- bindings: fiber_park.h + its 12 embind registrations (broken-if-
called under JSPI), the kicadOpenFileStart/OPEN_JOB starter route,
main_stack_runner.h + 5 includes, the always-null context-sleep weak
hook in nanosleep_yield.c.
- shim: the backend field (installed-flag idempotency instead),
noteContextWait (dead both sides), the __wxAsyncifyDump alias (+ the
WasmTool fallback and string-dump normalize branch).
- web: the emscripten-6-ignored mainScriptUrlOrBlob option in boot.ts
(gerber-demo keeps it: it loads the deployed CDN release, which
predates emscripten 6 — noted inline).
Conditionals: all 'backend === jspi' checks reduced to scheduler-
presence checks; races_quiescent re-keyed from Asyncify.state (vacuous)
to real backlog quiescence (resumeReady/mutatorQueue — NOT _windowLive,
which is the probing activation's own window by definition).
Renames (identifiers only, no file renames): ASYNC_LINK_FLAGS→
JSPI_LINK_FLAGS and Makefile ASYNC_LDFLAGS→JSPI_LDFLAGS,
kicadCollabFiberBusy→kicadCollabBusy (embind + web + tests),
collab_common.h fiber*→apply*/coroutine naming, asyncifySignatures→
wasmTrapSignatures (lists byte-identical).
Tests: the two remaining vacuous [wx-asyncify]/fiber-resume-refused
asserts re-keyed to live JSPI beacons; eeschema-load's failure message
no longer sends the developer to a deleted script; wait-beacons' dead
families/parser deleted; lane-0 legacy-glue guards removed (lane 0 is
unconstructible); the embind test.fail re-gated with the JSPI reason
(plain embind invokers cannot suspend — verified still failing);
lint-determinism now scans tests/jspi (166 files clean);
eeschema-collab local-move gated to chromium (~50% flaky on FF even
solo; pcbnew twin covers both engines).
Docs: DEBUG.md rewritten as the JSPI debugging guide; build.md
describes the single-phase build; docs/features/async/README.md
banner-marked historical and repointed at the NEW
23-jspi-runtime.md (current architecture: export census, turnstile,
libcontext ownership + refusal contract, embind call shapes, the
em-pthread service-wrapper trick, exception policy, known gaps).
Gates on the cleaned tree: test:e2e 725 passed / 0 failed (after the
quiescence-probe fix; the 3 other reds were verified contention flakes
solo-green or the documented FF gate), web 76/0, jspi 18/18 both
engines, vitest 295/295 + 17/17, all lints green, live-app census
clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016X9eh1s5sTx1o9Em9KBuwR
2026-08-14 09:25:32 +02:00
|
|
|
// SCH_COMMIT must run on the editor's main loop, not on this embind ccall or an
|
|
|
|
|
// emscripten_async_call/setTimeout callback: wxEvtHandler::CallAfter queues onto the
|
|
|
|
|
// app's pending-event list, which the wasm main loop drains every frame via
|
|
|
|
|
// ProcessPendingEvents() (src/wasm/evtloop.cpp) — i.e. the exact context real UI edits
|
|
|
|
|
// run in. So defer the whole mutation there.
|
2026-07-02 14:48:11 +02:00
|
|
|
void schCollabApply( std::string aJson )
|
2026-06-03 17:49:11 +02:00
|
|
|
{
|
2026-07-30 14:17:48 +02:00
|
|
|
// Open-in-flight guard (open_gate.h): never touch the model while a
|
jspi cleanup: remove the asyncify-era residue — dead code, conditionals, pipeline scaffolding, stale prose
The runtime is JSPI-only; this removes everything that still pretended
otherwise. Three exhaustive sweeps (C++/JS+build+CI/tests+docs) drove
the inventory; every deletion verified by grep closure + full gates.
Broken-right-now fixes:
- deploy-staging.yml passed the retired opt_level input — the workflow
could not even start. Removed.
- env.sh carried dead exports with a live -sASYNCIFY=1 inside
(WASM_LDFLAGS/PTHREAD_LDFLAGS, zero consumers). Removed; the
WASM_LEGACY_EXCEPTIONS rationale rewritten to the real reason.
- docker/build.sh exported PCBJAM_ASYNC_BACKEND (read nowhere). Gone.
Dead weight removed:
- binaryen submodule (nothing builds or invokes it), wasm-opt-bench
workflow + scripts/bench/, get-wasm-opt.sh, diagnostics.js (242 lines
of Asyncify-API-only code), the KICAD_PIPELINE background-postprocess
scaffolding (existed to parallelize the deleted wasm-opt phase; the
postprocess is a seconds-long node script and now runs inline),
build-monitor's dead asyncify rows, sched-context orphan build
output, dead .gitignore entries, the .jspi-assets spike dir (the two
wf-result research JSONs moved to docs/features/async/migration-evidence/).
- bindings: fiber_park.h + its 12 embind registrations (broken-if-
called under JSPI), the kicadOpenFileStart/OPEN_JOB starter route,
main_stack_runner.h + 5 includes, the always-null context-sleep weak
hook in nanosleep_yield.c.
- shim: the backend field (installed-flag idempotency instead),
noteContextWait (dead both sides), the __wxAsyncifyDump alias (+ the
WasmTool fallback and string-dump normalize branch).
- web: the emscripten-6-ignored mainScriptUrlOrBlob option in boot.ts
(gerber-demo keeps it: it loads the deployed CDN release, which
predates emscripten 6 — noted inline).
Conditionals: all 'backend === jspi' checks reduced to scheduler-
presence checks; races_quiescent re-keyed from Asyncify.state (vacuous)
to real backlog quiescence (resumeReady/mutatorQueue — NOT _windowLive,
which is the probing activation's own window by definition).
Renames (identifiers only, no file renames): ASYNC_LINK_FLAGS→
JSPI_LINK_FLAGS and Makefile ASYNC_LDFLAGS→JSPI_LDFLAGS,
kicadCollabFiberBusy→kicadCollabBusy (embind + web + tests),
collab_common.h fiber*→apply*/coroutine naming, asyncifySignatures→
wasmTrapSignatures (lists byte-identical).
Tests: the two remaining vacuous [wx-asyncify]/fiber-resume-refused
asserts re-keyed to live JSPI beacons; eeschema-load's failure message
no longer sends the developer to a deleted script; wait-beacons' dead
families/parser deleted; lane-0 legacy-glue guards removed (lane 0 is
unconstructible); the embind test.fail re-gated with the JSPI reason
(plain embind invokers cannot suspend — verified still failing);
lint-determinism now scans tests/jspi (166 files clean);
eeschema-collab local-move gated to chromium (~50% flaky on FF even
solo; pcbnew twin covers both engines).
Docs: DEBUG.md rewritten as the JSPI debugging guide; build.md
describes the single-phase build; docs/features/async/README.md
banner-marked historical and repointed at the NEW
23-jspi-runtime.md (current architecture: export census, turnstile,
libcontext ownership + refusal contract, embind call shapes, the
em-pthread service-wrapper trick, exception policy, known gaps).
Gates on the cleaned tree: test:e2e 725 passed / 0 failed (after the
quiescence-probe fix; the 3 other reds were verified contention flakes
solo-green or the documented FF gate), web 76/0, jspi 18/18 both
engines, vitest 295/295 + 17/17, all lints green, live-app census
clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016X9eh1s5sTx1o9Em9KBuwR
2026-08-14 09:25:32 +02:00
|
|
|
// kicadOpenFile chain is suspended mid-load — commits/virtuals would walk
|
|
|
|
|
// a half-built schematic mid-mutation.
|
2026-07-30 14:17:48 +02:00
|
|
|
// Callers gate on kicadOpenFileBusy; fuzzed by tests/kicad/collab-load-fuzz.spec.ts.
|
|
|
|
|
if( pcbjam_open::busy() )
|
|
|
|
|
return;
|
|
|
|
|
|
2026-06-03 17:49:11 +02:00
|
|
|
json delta = json::parse( aJson, nullptr, /*allow_exceptions*/ false );
|
|
|
|
|
|
|
|
|
|
if( delta.is_discarded() )
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
SCH_EDIT_FRAME* fr = schFrame();
|
|
|
|
|
|
|
|
|
|
if( !fr )
|
|
|
|
|
return;
|
|
|
|
|
|
jspi cleanup: remove the asyncify-era residue — dead code, conditionals, pipeline scaffolding, stale prose
The runtime is JSPI-only; this removes everything that still pretended
otherwise. Three exhaustive sweeps (C++/JS+build+CI/tests+docs) drove
the inventory; every deletion verified by grep closure + full gates.
Broken-right-now fixes:
- deploy-staging.yml passed the retired opt_level input — the workflow
could not even start. Removed.
- env.sh carried dead exports with a live -sASYNCIFY=1 inside
(WASM_LDFLAGS/PTHREAD_LDFLAGS, zero consumers). Removed; the
WASM_LEGACY_EXCEPTIONS rationale rewritten to the real reason.
- docker/build.sh exported PCBJAM_ASYNC_BACKEND (read nowhere). Gone.
Dead weight removed:
- binaryen submodule (nothing builds or invokes it), wasm-opt-bench
workflow + scripts/bench/, get-wasm-opt.sh, diagnostics.js (242 lines
of Asyncify-API-only code), the KICAD_PIPELINE background-postprocess
scaffolding (existed to parallelize the deleted wasm-opt phase; the
postprocess is a seconds-long node script and now runs inline),
build-monitor's dead asyncify rows, sched-context orphan build
output, dead .gitignore entries, the .jspi-assets spike dir (the two
wf-result research JSONs moved to docs/features/async/migration-evidence/).
- bindings: fiber_park.h + its 12 embind registrations (broken-if-
called under JSPI), the kicadOpenFileStart/OPEN_JOB starter route,
main_stack_runner.h + 5 includes, the always-null context-sleep weak
hook in nanosleep_yield.c.
- shim: the backend field (installed-flag idempotency instead),
noteContextWait (dead both sides), the __wxAsyncifyDump alias (+ the
WasmTool fallback and string-dump normalize branch).
- web: the emscripten-6-ignored mainScriptUrlOrBlob option in boot.ts
(gerber-demo keeps it: it loads the deployed CDN release, which
predates emscripten 6 — noted inline).
Conditionals: all 'backend === jspi' checks reduced to scheduler-
presence checks; races_quiescent re-keyed from Asyncify.state (vacuous)
to real backlog quiescence (resumeReady/mutatorQueue — NOT _windowLive,
which is the probing activation's own window by definition).
Renames (identifiers only, no file renames): ASYNC_LINK_FLAGS→
JSPI_LINK_FLAGS and Makefile ASYNC_LDFLAGS→JSPI_LDFLAGS,
kicadCollabFiberBusy→kicadCollabBusy (embind + web + tests),
collab_common.h fiber*→apply*/coroutine naming, asyncifySignatures→
wasmTrapSignatures (lists byte-identical).
Tests: the two remaining vacuous [wx-asyncify]/fiber-resume-refused
asserts re-keyed to live JSPI beacons; eeschema-load's failure message
no longer sends the developer to a deleted script; wait-beacons' dead
families/parser deleted; lane-0 legacy-glue guards removed (lane 0 is
unconstructible); the embind test.fail re-gated with the JSPI reason
(plain embind invokers cannot suspend — verified still failing);
lint-determinism now scans tests/jspi (166 files clean);
eeschema-collab local-move gated to chromium (~50% flaky on FF even
solo; pcbnew twin covers both engines).
Docs: DEBUG.md rewritten as the JSPI debugging guide; build.md
describes the single-phase build; docs/features/async/README.md
banner-marked historical and repointed at the NEW
23-jspi-runtime.md (current architecture: export census, turnstile,
libcontext ownership + refusal contract, embind call shapes, the
em-pthread service-wrapper trick, exception policy, known gaps).
Gates on the cleaned tree: test:e2e 725 passed / 0 failed (after the
quiescence-probe fix; the 3 other reds were verified contention flakes
solo-green or the documented FF gate), web 76/0, jspi 18/18 both
engines, vitest 295/295 + 17/17, all lints green, live-app census
clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016X9eh1s5sTx1o9Em9KBuwR
2026-08-14 09:25:32 +02:00
|
|
|
// Defer to the editor's main loop + apply coroutine (runOnCoroutine) so SCH_COMMIT runs
|
|
|
|
|
// like a normal edit: a commit body that suspends (connectivity/GAL work) returns early
|
|
|
|
|
// from COROUTINE::Call, so applies must serialize with each other and with local edits
|
|
|
|
|
// or they interleave on shared commit/listener state. (0007, drift-trio #10.)
|
|
|
|
|
pcbjam_collab::runOnCoroutine( fr, [fr, delta]() { doApply( fr, delta ); } );
|
2026-06-03 17:49:11 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// JS pull of the full current model as an all-"added" delta (seed/baseline). Also
|
|
|
|
|
// registers the change listener on first call.
|
2026-07-02 14:48:11 +02:00
|
|
|
std::string schCollabSnapshot()
|
2026-06-03 17:49:11 +02:00
|
|
|
{
|
2026-07-30 14:17:48 +02:00
|
|
|
if( pcbjam_open::busy() ) // open in flight (open_gate.h) — see schCollabApply
|
|
|
|
|
return json{ { "added", json::array() }, { "changed", json::array() },
|
|
|
|
|
{ "removed", json::array() } }.dump();
|
|
|
|
|
|
2026-06-17 17:53:38 +02:00
|
|
|
ensureBridge();
|
|
|
|
|
json added = snapshotItems( schFrame() );
|
2026-06-03 17:49:11 +02:00
|
|
|
|
2026-06-05 13:30:47 +02:00
|
|
|
// Seed the diff baseline to exactly the model we're handing out, so the first local edit
|
|
|
|
|
// diffs against this snapshot (and we don't re-broadcast the whole model).
|
|
|
|
|
rebaseline();
|
|
|
|
|
|
2026-06-03 17:49:11 +02:00
|
|
|
return json{ { "added", added }, { "changed", json::array() },
|
|
|
|
|
{ "removed", json::array() } }.dump();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2026-06-11 13:55:15 +02:00
|
|
|
// JS → C++, v2 items wire. Same CallAfter + COROUTINE context as kicadCollabApply
|
|
|
|
|
// (LoadContent + SCH_COMMIT must run where native edits run).
|
2026-07-02 14:48:11 +02:00
|
|
|
void schCollabApplyItems( std::string aJson )
|
2026-06-11 13:55:15 +02:00
|
|
|
{
|
2026-07-30 14:17:48 +02:00
|
|
|
if( pcbjam_open::busy() ) // open in flight (open_gate.h) — see schCollabApply
|
|
|
|
|
return;
|
|
|
|
|
|
2026-06-11 13:55:15 +02:00
|
|
|
json wire = json::parse( aJson, nullptr, /*allow_exceptions*/ false );
|
|
|
|
|
|
|
|
|
|
if( wire.is_discarded() )
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
SCH_EDIT_FRAME* fr = schFrame();
|
|
|
|
|
|
|
|
|
|
if( !fr )
|
|
|
|
|
return;
|
|
|
|
|
|
jspi cleanup: remove the asyncify-era residue — dead code, conditionals, pipeline scaffolding, stale prose
The runtime is JSPI-only; this removes everything that still pretended
otherwise. Three exhaustive sweeps (C++/JS+build+CI/tests+docs) drove
the inventory; every deletion verified by grep closure + full gates.
Broken-right-now fixes:
- deploy-staging.yml passed the retired opt_level input — the workflow
could not even start. Removed.
- env.sh carried dead exports with a live -sASYNCIFY=1 inside
(WASM_LDFLAGS/PTHREAD_LDFLAGS, zero consumers). Removed; the
WASM_LEGACY_EXCEPTIONS rationale rewritten to the real reason.
- docker/build.sh exported PCBJAM_ASYNC_BACKEND (read nowhere). Gone.
Dead weight removed:
- binaryen submodule (nothing builds or invokes it), wasm-opt-bench
workflow + scripts/bench/, get-wasm-opt.sh, diagnostics.js (242 lines
of Asyncify-API-only code), the KICAD_PIPELINE background-postprocess
scaffolding (existed to parallelize the deleted wasm-opt phase; the
postprocess is a seconds-long node script and now runs inline),
build-monitor's dead asyncify rows, sched-context orphan build
output, dead .gitignore entries, the .jspi-assets spike dir (the two
wf-result research JSONs moved to docs/features/async/migration-evidence/).
- bindings: fiber_park.h + its 12 embind registrations (broken-if-
called under JSPI), the kicadOpenFileStart/OPEN_JOB starter route,
main_stack_runner.h + 5 includes, the always-null context-sleep weak
hook in nanosleep_yield.c.
- shim: the backend field (installed-flag idempotency instead),
noteContextWait (dead both sides), the __wxAsyncifyDump alias (+ the
WasmTool fallback and string-dump normalize branch).
- web: the emscripten-6-ignored mainScriptUrlOrBlob option in boot.ts
(gerber-demo keeps it: it loads the deployed CDN release, which
predates emscripten 6 — noted inline).
Conditionals: all 'backend === jspi' checks reduced to scheduler-
presence checks; races_quiescent re-keyed from Asyncify.state (vacuous)
to real backlog quiescence (resumeReady/mutatorQueue — NOT _windowLive,
which is the probing activation's own window by definition).
Renames (identifiers only, no file renames): ASYNC_LINK_FLAGS→
JSPI_LINK_FLAGS and Makefile ASYNC_LDFLAGS→JSPI_LDFLAGS,
kicadCollabFiberBusy→kicadCollabBusy (embind + web + tests),
collab_common.h fiber*→apply*/coroutine naming, asyncifySignatures→
wasmTrapSignatures (lists byte-identical).
Tests: the two remaining vacuous [wx-asyncify]/fiber-resume-refused
asserts re-keyed to live JSPI beacons; eeschema-load's failure message
no longer sends the developer to a deleted script; wait-beacons' dead
families/parser deleted; lane-0 legacy-glue guards removed (lane 0 is
unconstructible); the embind test.fail re-gated with the JSPI reason
(plain embind invokers cannot suspend — verified still failing);
lint-determinism now scans tests/jspi (166 files clean);
eeschema-collab local-move gated to chromium (~50% flaky on FF even
solo; pcbnew twin covers both engines).
Docs: DEBUG.md rewritten as the JSPI debugging guide; build.md
describes the single-phase build; docs/features/async/README.md
banner-marked historical and repointed at the NEW
23-jspi-runtime.md (current architecture: export census, turnstile,
libcontext ownership + refusal contract, embind call shapes, the
em-pthread service-wrapper trick, exception policy, known gaps).
Gates on the cleaned tree: test:e2e 725 passed / 0 failed (after the
quiescence-probe fix; the 3 other reds were verified contention flakes
solo-green or the documented FF gate), web 76/0, jspi 18/18 both
engines, vitest 295/295 + 17/17, all lints green, live-app census
clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016X9eh1s5sTx1o9Em9KBuwR
2026-08-14 09:25:32 +02:00
|
|
|
pcbjam_collab::runOnCoroutine( fr, [fr, wire]() { doApplyItems( fr, wire ); } );
|
2026-06-11 13:55:15 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2026-06-17 17:53:38 +02:00
|
|
|
// JS pull of the ACTIVE screen's model as an all-"added" v2 items wire: one clipboard-
|
|
|
|
|
// style blob per item on the current sheet (one collab room == one .kicad_sch screen).
|
|
|
|
|
// Registers the listener + rebaselines exactly like kicadCollabSnapshot.
|
2026-07-02 14:48:11 +02:00
|
|
|
std::string schCollabSnapshotItems()
|
2026-06-11 13:55:15 +02:00
|
|
|
{
|
2026-07-30 14:17:48 +02:00
|
|
|
if( pcbjam_open::busy() ) // open in flight (open_gate.h) — see schCollabApply
|
|
|
|
|
return json{ { "added", json::array() }, { "changed", json::array() },
|
|
|
|
|
{ "removed", json::array() } }.dump();
|
|
|
|
|
|
2026-06-11 13:55:15 +02:00
|
|
|
SCH_EDIT_FRAME* fr = schFrame();
|
|
|
|
|
|
|
|
|
|
json added = json::array();
|
|
|
|
|
|
|
|
|
|
if( fr )
|
|
|
|
|
{
|
|
|
|
|
ensureBridge();
|
|
|
|
|
|
2026-06-17 17:53:38 +02:00
|
|
|
if( SCH_SCREEN* screen = currentScreen( fr ) )
|
2026-06-11 13:55:15 +02:00
|
|
|
{
|
|
|
|
|
for( SCH_ITEM* item : screen->Items() )
|
2026-06-17 17:53:38 +02:00
|
|
|
added.push_back( json{ { "sexpr", itemBlob( fr, item ) }, { "parent", nullptr } } );
|
2026-06-11 13:55:15 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
rebaseline();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return json{ { "added", added }, { "changed", json::array() },
|
|
|
|
|
{ "removed", json::array() } }.dump();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2026-06-03 17:49:11 +02:00
|
|
|
// Test/PoC helper: move the first schematic item by (dx,dy) IU via a real SCH_COMMIT,
|
|
|
|
|
// firing the listener — a deterministic local edit for the two-tab demo / e2e.
|
|
|
|
|
// Returns the moved item's uuid.
|
2026-07-02 14:48:11 +02:00
|
|
|
std::string schCollabTestMoveFirst( int aDx, int aDy )
|
2026-06-03 17:49:11 +02:00
|
|
|
{
|
|
|
|
|
SCH_EDIT_FRAME* fr = schFrame();
|
|
|
|
|
|
|
|
|
|
if( !fr )
|
|
|
|
|
return "";
|
|
|
|
|
|
|
|
|
|
SCHEMATIC& sch = fr->Schematic();
|
|
|
|
|
|
|
|
|
|
for( const SCH_SHEET_PATH& path : sch.Hierarchy() )
|
|
|
|
|
{
|
|
|
|
|
SCH_SCREEN* screen = const_cast<SCH_SHEET_PATH&>( path ).LastScreen();
|
|
|
|
|
|
|
|
|
|
if( !screen )
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
for( SCH_ITEM* item : screen->Items() )
|
|
|
|
|
{
|
2026-06-03 19:37:15 +02:00
|
|
|
fr->CallAfter( [fr, item, screen, aDx, aDy]() { collabTestMove( fr, item, screen, aDx, aDy ); } );
|
2026-06-03 17:49:11 +02:00
|
|
|
return toUtf8( item->m_Uuid.AsString() );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return "";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2026-08-25 17:58:54 +02:00
|
|
|
// Outcome of a deferred update-from-library run → window event
|
|
|
|
|
// `pcbjam:lib-update-done` {ok, updated, missing[]} (libs 0017 §2c).
|
|
|
|
|
static void emitLibUpdateDone( int aUpdated, const std::vector<std::string>& aMissing )
|
|
|
|
|
{
|
|
|
|
|
nlohmann::json j;
|
|
|
|
|
j["ok"] = true;
|
|
|
|
|
j["updated"] = aUpdated;
|
|
|
|
|
j["missing"] = aMissing;
|
|
|
|
|
std::string s = j.dump();
|
|
|
|
|
EM_ASM( {
|
|
|
|
|
if( typeof window !== 'undefined' )
|
|
|
|
|
window.dispatchEvent( new CustomEvent( 'pcbjam:lib-update-done',
|
|
|
|
|
{ detail: JSON.parse( UTF8ToString( $0 ) ) } ) );
|
|
|
|
|
}, s.c_str() );
|
|
|
|
|
}
|
|
|
|
|
|
libs: peer lib edits reach the running editor + "placed symbol updated" toast
synced-source subscribes to its SyncStack: remote changes (self-save echoes
consumed via a selfPushed flag) debounce per kind into kicadLibsReload — a
new embind export (pcbjam_libs_reload.h, all three TUs) that drops the lib's
plugin cache (LIBRARY_MANAGER::ReloadLibraryEntry), reloads it, and mails
MAIL_RELOAD_LIB with the nickname so the symbol tree force-refreshes (the
plugin's modify hash is a pinned constant, so a plain sync would skip it).
After the reload, kicadLibsSymbolUsage (new eeschema embind: placed
SCH_SYMBOL count across unique screens) gates LIB_ITEM_UPDATED_EVENT, and
WasmTool shows an amber toast when a PLACED symbol changed — placed copies
keep the previous version until updated from the library.
syncedScopeLibsSource gives PROJECT sessions the synced source under
VITE_LIBS_SOURCE=synced (remote contract for lib listing/createLib, lazy
per-lib SyncStacks for item ops/presync) so realtime reaches open
schematics; previously project sessions silently fell back to the per-item
remote source. Unit tests cover reload debounce, self-echo skip, per-kind
routing, usage-gated event, and the no-Module no-op.
Bumps kicad (MAIL_RELOAD_LIB force-refresh payload).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013QRWoXiM9uuo1enXGhAYku
2026-07-09 18:21:21 +02:00
|
|
|
// How many placed instances of a library symbol the open schematic holds —
|
|
|
|
|
// the JS lib-sync bridge asks after a remote lib update so the editor chrome
|
|
|
|
|
// can warn "a symbol you are using changed" (placed SCH_SYMBOLs keep their
|
|
|
|
|
// embedded copy across a lib reload, so the user must update explicitly).
|
|
|
|
|
// Counts across all unique screens of the hierarchy; 0 without a schematic
|
|
|
|
|
// frame (symbol editor / viewer sessions).
|
2026-08-25 17:58:54 +02:00
|
|
|
// Re-read the named library symbols into every placed instance — the headless
|
|
|
|
|
// core of Tools ▸ Update Symbols from Library… (DIALOG_CHANGE_SYMBOLS::
|
|
|
|
|
// processSymbols with the dialog's defaults: keep field text/positions, take
|
|
|
|
|
// the new body/pins/attributes), scoped to `aNamesJson` of `aLibNickname`.
|
|
|
|
|
// Runs as a normal SCH_COMMIT on the frame's coroutine so peers receive it as
|
|
|
|
|
// an ordinary edit. Returns {ok, updated, missing[]}. (libs 0017 §2c)
|
|
|
|
|
std::string schUpdateFromLibrary( std::string aLibNickname, std::string aNamesJson )
|
|
|
|
|
{
|
|
|
|
|
nlohmann::json out;
|
|
|
|
|
SCH_EDIT_FRAME* fr = schFrame();
|
|
|
|
|
|
|
|
|
|
if( !fr )
|
|
|
|
|
{
|
|
|
|
|
out["ok"] = false;
|
|
|
|
|
out["error"] = "no schematic frame";
|
|
|
|
|
return out.dump();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::set<wxString> names;
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
for( const auto& n : nlohmann::json::parse( aNamesJson ) )
|
|
|
|
|
names.insert( wxString::FromUTF8( n.get<std::string>().c_str() ) );
|
|
|
|
|
}
|
|
|
|
|
catch( ... )
|
|
|
|
|
{
|
|
|
|
|
out["ok"] = false;
|
|
|
|
|
out["error"] = "bad names";
|
|
|
|
|
return out.dump();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const wxString lib = wxString::FromUTF8( aLibNickname.c_str() );
|
|
|
|
|
pcbjam_collab::runOnCoroutine( fr, [fr, lib, names]()
|
|
|
|
|
{
|
|
|
|
|
int updated = 0;
|
|
|
|
|
std::vector<std::string> missing;
|
|
|
|
|
SCH_COMMIT commit( fr );
|
|
|
|
|
SCH_SCREENS screens( fr->Schematic().Root() );
|
|
|
|
|
|
|
|
|
|
for( SCH_SCREEN* screen = screens.GetFirst(); screen; screen = screens.GetNext() )
|
|
|
|
|
{
|
|
|
|
|
std::vector<SCH_SYMBOL*> targets;
|
|
|
|
|
|
|
|
|
|
for( SCH_ITEM* item : screen->Items().OfType( SCH_SYMBOL_T ) )
|
|
|
|
|
{
|
|
|
|
|
SCH_SYMBOL* sym = static_cast<SCH_SYMBOL*>( item );
|
|
|
|
|
const LIB_ID& id = sym->GetLibId();
|
|
|
|
|
|
|
|
|
|
if( wxString( id.GetLibNickname() ) == lib && names.count( wxString( id.GetLibItemName() ) ) )
|
|
|
|
|
targets.push_back( sym );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for( SCH_SYMBOL* sym : targets )
|
|
|
|
|
{
|
|
|
|
|
LIB_SYMBOL* libSymbol = fr->GetLibSymbol( sym->GetLibId() );
|
|
|
|
|
|
|
|
|
|
if( !libSymbol )
|
|
|
|
|
{
|
|
|
|
|
missing.push_back( std::string( sym->GetLibId().Format().c_str() ) );
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::unique_ptr<LIB_SYMBOL> flattened = libSymbol->Flatten();
|
|
|
|
|
|
|
|
|
|
if( flattened->GetUnitCount() < sym->GetUnit() )
|
|
|
|
|
{
|
|
|
|
|
missing.push_back( std::string( sym->GetLibId().Format().c_str() ) );
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Same order as the dialog: remove, record, swap the lib symbol,
|
|
|
|
|
// re-append (the screen's RTree + connectivity re-index).
|
|
|
|
|
screen->Remove( sym );
|
|
|
|
|
commit.Modified( sym, static_cast<SCH_SYMBOL*>( sym->Clone() ), screen );
|
|
|
|
|
sym->SetLibSymbol( flattened.release() );
|
|
|
|
|
sym->SetExcludedFromSim( sym->GetLibSymbolRef()->GetExcludedFromSim() );
|
|
|
|
|
sym->SetExcludedFromBOM( sym->GetLibSymbolRef()->GetExcludedFromBOM() );
|
|
|
|
|
sym->SetExcludedFromBoard( sym->GetLibSymbolRef()->GetExcludedFromBoard() );
|
|
|
|
|
sym->SetShowPinNames( sym->GetLibSymbolRef()->GetShowPinNames() );
|
|
|
|
|
sym->SetShowPinNumbers( sym->GetLibSymbolRef()->GetShowPinNumbers() );
|
|
|
|
|
sym->SetSchSymbolLibraryName( wxEmptyString );
|
|
|
|
|
screen->Append( sym );
|
|
|
|
|
updated++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
commit.Push( wxT( "Update symbols from library" ) );
|
|
|
|
|
fr->GetCanvas()->Refresh();
|
2026-08-26 13:29:29 +02:00
|
|
|
|
|
|
|
|
// The Symbol Editor's own copy (libs 0019 F3): re-open it from the
|
|
|
|
|
// fresh library when unmodified; report and leave it with local edits.
|
|
|
|
|
if( SYMBOL_EDIT_FRAME* se = dynamic_cast<SYMBOL_EDIT_FRAME*>(
|
|
|
|
|
fr->Kiway().Player( FRAME_SCH_SYMBOL_EDITOR, false ) ) )
|
|
|
|
|
{
|
|
|
|
|
if( LIB_SYMBOL* cur = se->GetCurSymbol() )
|
|
|
|
|
{
|
|
|
|
|
const wxString curName = cur->GetName();
|
|
|
|
|
|
|
|
|
|
if( se->GetCurLib() == lib && names.count( curName ) )
|
|
|
|
|
{
|
|
|
|
|
const LIB_ID id( lib, curName );
|
|
|
|
|
|
|
|
|
|
if( se->IsContentModified() )
|
|
|
|
|
missing.push_back( std::string( id.Format().c_str() )
|
|
|
|
|
+ " (open in the Symbol Editor with unsaved edits — save or revert it first)" );
|
|
|
|
|
else
|
|
|
|
|
se->LoadSymbol( id, 1, 1 );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-25 17:58:54 +02:00
|
|
|
emitLibUpdateDone( updated, missing );
|
|
|
|
|
} );
|
|
|
|
|
|
|
|
|
|
// The body runs deferred on the frame's coroutine (runOnCoroutine =
|
|
|
|
|
// CallAfter): the caller awaits the `pcbjam:lib-update-done` window event
|
|
|
|
|
// for the outcome.
|
|
|
|
|
out["ok"] = true;
|
|
|
|
|
out["queued"] = true;
|
|
|
|
|
return out.dump();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Standalone-eeschema shape of kicadUpdateFromLibrary(kind, lib, namesJson).
|
|
|
|
|
static std::string schUpdateFromLibraryShim( std::string aKind, std::string aLib, std::string aNames )
|
|
|
|
|
{
|
|
|
|
|
if( aKind != "symbol" )
|
|
|
|
|
return "{\"ok\":false,\"error\":\"kind not handled by this editor\"}";
|
|
|
|
|
|
|
|
|
|
return schUpdateFromLibrary( aLib, aNames );
|
|
|
|
|
}
|
|
|
|
|
|
libs: peer lib edits reach the running editor + "placed symbol updated" toast
synced-source subscribes to its SyncStack: remote changes (self-save echoes
consumed via a selfPushed flag) debounce per kind into kicadLibsReload — a
new embind export (pcbjam_libs_reload.h, all three TUs) that drops the lib's
plugin cache (LIBRARY_MANAGER::ReloadLibraryEntry), reloads it, and mails
MAIL_RELOAD_LIB with the nickname so the symbol tree force-refreshes (the
plugin's modify hash is a pinned constant, so a plain sync would skip it).
After the reload, kicadLibsSymbolUsage (new eeschema embind: placed
SCH_SYMBOL count across unique screens) gates LIB_ITEM_UPDATED_EVENT, and
WasmTool shows an amber toast when a PLACED symbol changed — placed copies
keep the previous version until updated from the library.
syncedScopeLibsSource gives PROJECT sessions the synced source under
VITE_LIBS_SOURCE=synced (remote contract for lib listing/createLib, lazy
per-lib SyncStacks for item ops/presync) so realtime reaches open
schematics; previously project sessions silently fell back to the per-item
remote source. Unit tests cover reload debounce, self-echo skip, per-kind
routing, usage-gated event, and the no-Module no-op.
Bumps kicad (MAIL_RELOAD_LIB force-refresh payload).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013QRWoXiM9uuo1enXGhAYku
2026-07-09 18:21:21 +02:00
|
|
|
int schLibsSymbolUsage( std::string aLibNickname, std::string aSymbolName )
|
|
|
|
|
{
|
|
|
|
|
SCH_EDIT_FRAME* fr = schFrame();
|
|
|
|
|
|
|
|
|
|
if( !fr )
|
|
|
|
|
return 0;
|
|
|
|
|
|
|
|
|
|
const LIB_ID target( wxString::FromUTF8( aLibNickname.c_str() ),
|
|
|
|
|
wxString::FromUTF8( aSymbolName.c_str() ) );
|
|
|
|
|
|
|
|
|
|
int count = 0;
|
|
|
|
|
SCH_SCREENS screens( fr->Schematic().Root() );
|
|
|
|
|
|
|
|
|
|
for( SCH_SCREEN* screen = screens.GetFirst(); screen; screen = screens.GetNext() )
|
|
|
|
|
{
|
|
|
|
|
for( SCH_ITEM* item : screen->Items().OfType( SCH_SYMBOL_T ) )
|
|
|
|
|
{
|
|
|
|
|
if( static_cast<SCH_SYMBOL*>( item )->GetLibId() == target )
|
|
|
|
|
count++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-26 13:29:29 +02:00
|
|
|
// The Symbol Editor's open copy counts as "used" too (libs 0019 F3).
|
|
|
|
|
if( SYMBOL_EDIT_FRAME* se = dynamic_cast<SYMBOL_EDIT_FRAME*>(
|
|
|
|
|
fr->Kiway().Player( FRAME_SCH_SYMBOL_EDITOR, false ) ) )
|
|
|
|
|
{
|
|
|
|
|
if( LIB_SYMBOL* cur = se->GetCurSymbol() )
|
|
|
|
|
{
|
|
|
|
|
if( se->GetCurLib() == wxString( target.GetLibNickname() )
|
|
|
|
|
&& cur->GetName() == wxString( target.GetLibItemName() ) )
|
|
|
|
|
count++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
libs: peer lib edits reach the running editor + "placed symbol updated" toast
synced-source subscribes to its SyncStack: remote changes (self-save echoes
consumed via a selfPushed flag) debounce per kind into kicadLibsReload — a
new embind export (pcbjam_libs_reload.h, all three TUs) that drops the lib's
plugin cache (LIBRARY_MANAGER::ReloadLibraryEntry), reloads it, and mails
MAIL_RELOAD_LIB with the nickname so the symbol tree force-refreshes (the
plugin's modify hash is a pinned constant, so a plain sync would skip it).
After the reload, kicadLibsSymbolUsage (new eeschema embind: placed
SCH_SYMBOL count across unique screens) gates LIB_ITEM_UPDATED_EVENT, and
WasmTool shows an amber toast when a PLACED symbol changed — placed copies
keep the previous version until updated from the library.
syncedScopeLibsSource gives PROJECT sessions the synced source under
VITE_LIBS_SOURCE=synced (remote contract for lib listing/createLib, lazy
per-lib SyncStacks for item ops/presync) so realtime reaches open
schematics; previously project sessions silently fell back to the per-item
remote source. Unit tests cover reload debounce, self-echo skip, per-kind
routing, usage-gated event, and the no-Module no-op.
Bumps kicad (MAIL_RELOAD_LIB force-refresh payload).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013QRWoXiM9uuo1enXGhAYku
2026-07-09 18:21:21 +02:00
|
|
|
return count;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2026-06-03 17:49:11 +02:00
|
|
|
// Test helper: read an item's position by uuid as "x,y" (internal units).
|
2026-07-02 14:48:11 +02:00
|
|
|
std::string schCollabGetPos( std::string aId )
|
2026-06-03 17:49:11 +02:00
|
|
|
{
|
|
|
|
|
SCH_EDIT_FRAME* fr = schFrame();
|
|
|
|
|
|
|
|
|
|
if( !fr )
|
|
|
|
|
return "";
|
|
|
|
|
|
|
|
|
|
KIID id( wxString::FromUTF8( aId.c_str() ) );
|
|
|
|
|
|
|
|
|
|
if( SCH_ITEM* item = fr->Schematic().ResolveItem( id, nullptr, /*allowNull*/ true ) )
|
|
|
|
|
{
|
|
|
|
|
VECTOR2I p = item->GetPosition();
|
|
|
|
|
return std::to_string( p.x ) + "," + std::to_string( p.y );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return "";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
test(ysync): repro tests for review bugs 01-07 + v2 items-wire e2e port (miss 11)
The 2026-07-02 sync review (docs/features/ysync-review, ysync-review branch)
found 7 bugs and that the two-tab e2e only exercised the DEAD legacy scalar
wire. This lands plan doc 15 in full; results + empirical findings in doc 16.
- tests/collab/browser-entry-v2.ts (+build.mjs): the PRODUCTION v2 stack
bundled for e2e (connectKicadDoc + attachKicadCollab, kdoc_* keys), with
in-page renderActiveDoc/singleSeedRender/driftReport helpers and yjs forced
to ONE copy (the two web pnpm workspaces otherwise bundle two
instanceof-incompatible instances).
- tests/kicad/ysync-two-tab.spec.ts: pl_editor green baseline (A↔B edits,
ITEM-level drift silence) + divergent-uuid adopt; bug-01 pcb/ee fresh-room
repros (Chromium-only: two kicad_editor tabs exceed Firefox's per-process
wasm budget); bug-06 concurrent-seed race; bug-03 Y-half.
- tests/kicad/ysync-repros-{pcbnew,eeschema}.spec.ts: bugs 02/03/05 + the
bug-04 matrix (anchor-centred fp rotation, pad resize, endpoint drag,
symbol rotation, Value-field edit), each with green landed-preconditions;
the "local move emits" controls double as headless-emit probes — GREEN on
both tools, so every emit-dependent repro is a live test.fail.
- wasm/bindings: 7 local-edit test hooks via real commits
(CallAfter+COROUTINE) — TestRemoveItem/TestRotateItem (both tools,
dispatched in the merged image), TestSetPadSize/TestMoveEndpoint (pcbnew),
TestSetFieldText (eeschema).
- web/standalone ysync-repros.test.ts: bug-01 units (C++-faithful fake gating
emit on ensureBridge) + bug-07a/b (stale DOWN hook, real sheet-manager gap).
- web/pcbjam-shared bump: bug-03/06 unit repros.
Convention: every repro asserts the CORRECT behavior and is expected-fail
(test.fail/it.fails) naming its bug doc; a fix flips it to "unexpected pass",
forcing marker removal — the repro becomes the regression test. Every
expected failure verified (JSON reporter) to fail at its documented assert.
Suite state: 39 passed / 0 failed / 0 flaky / 5 skipped (2 firefox guards,
2 pre-existing legacy two-tab skips, 1 pre-existing roundtrip fixme).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DPfrVhfYgPPgtawjSssZfn
2026-07-03 12:43:35 +02:00
|
|
|
// ── ysync-review repro hooks ─────────────────────────────────────────────────
|
|
|
|
|
// Local-edit test hooks for the ysync-review repro e2e (docs/features/
|
|
|
|
|
// ysync-review on the ysync-review branch): each drives a REAL SCH_COMMIT via
|
|
|
|
|
// CallAfter + COROUTINE (the doApply wrapping), so the SCHEMATIC_LISTENER →
|
|
|
|
|
// flushDiff emit path runs exactly as for a UI edit. Each returns false when
|
|
|
|
|
// the uuid doesn't resolve, letting the spec distinguish "hook missed the
|
|
|
|
|
// item" from "differ missed the edit" (bug 04).
|
|
|
|
|
|
|
|
|
|
// Delete an item by uuid via a real SCH_COMMIT.
|
|
|
|
|
bool schCollabTestRemoveItem( std::string aId )
|
|
|
|
|
{
|
|
|
|
|
SCH_EDIT_FRAME* fr = schFrame();
|
|
|
|
|
|
|
|
|
|
if( !fr )
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
SCH_SHEET_PATH path;
|
|
|
|
|
SCH_ITEM* item = fr->Schematic().ResolveItem( KIID( wxString::FromUTF8( aId.c_str() ) ),
|
|
|
|
|
&path, /*allowNull*/ true );
|
|
|
|
|
|
|
|
|
|
if( !item )
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
SCH_SCREEN* screen = path.LastScreen();
|
|
|
|
|
|
jspi cleanup: remove the asyncify-era residue — dead code, conditionals, pipeline scaffolding, stale prose
The runtime is JSPI-only; this removes everything that still pretended
otherwise. Three exhaustive sweeps (C++/JS+build+CI/tests+docs) drove
the inventory; every deletion verified by grep closure + full gates.
Broken-right-now fixes:
- deploy-staging.yml passed the retired opt_level input — the workflow
could not even start. Removed.
- env.sh carried dead exports with a live -sASYNCIFY=1 inside
(WASM_LDFLAGS/PTHREAD_LDFLAGS, zero consumers). Removed; the
WASM_LEGACY_EXCEPTIONS rationale rewritten to the real reason.
- docker/build.sh exported PCBJAM_ASYNC_BACKEND (read nowhere). Gone.
Dead weight removed:
- binaryen submodule (nothing builds or invokes it), wasm-opt-bench
workflow + scripts/bench/, get-wasm-opt.sh, diagnostics.js (242 lines
of Asyncify-API-only code), the KICAD_PIPELINE background-postprocess
scaffolding (existed to parallelize the deleted wasm-opt phase; the
postprocess is a seconds-long node script and now runs inline),
build-monitor's dead asyncify rows, sched-context orphan build
output, dead .gitignore entries, the .jspi-assets spike dir (the two
wf-result research JSONs moved to docs/features/async/migration-evidence/).
- bindings: fiber_park.h + its 12 embind registrations (broken-if-
called under JSPI), the kicadOpenFileStart/OPEN_JOB starter route,
main_stack_runner.h + 5 includes, the always-null context-sleep weak
hook in nanosleep_yield.c.
- shim: the backend field (installed-flag idempotency instead),
noteContextWait (dead both sides), the __wxAsyncifyDump alias (+ the
WasmTool fallback and string-dump normalize branch).
- web: the emscripten-6-ignored mainScriptUrlOrBlob option in boot.ts
(gerber-demo keeps it: it loads the deployed CDN release, which
predates emscripten 6 — noted inline).
Conditionals: all 'backend === jspi' checks reduced to scheduler-
presence checks; races_quiescent re-keyed from Asyncify.state (vacuous)
to real backlog quiescence (resumeReady/mutatorQueue — NOT _windowLive,
which is the probing activation's own window by definition).
Renames (identifiers only, no file renames): ASYNC_LINK_FLAGS→
JSPI_LINK_FLAGS and Makefile ASYNC_LDFLAGS→JSPI_LDFLAGS,
kicadCollabFiberBusy→kicadCollabBusy (embind + web + tests),
collab_common.h fiber*→apply*/coroutine naming, asyncifySignatures→
wasmTrapSignatures (lists byte-identical).
Tests: the two remaining vacuous [wx-asyncify]/fiber-resume-refused
asserts re-keyed to live JSPI beacons; eeschema-load's failure message
no longer sends the developer to a deleted script; wait-beacons' dead
families/parser deleted; lane-0 legacy-glue guards removed (lane 0 is
unconstructible); the embind test.fail re-gated with the JSPI reason
(plain embind invokers cannot suspend — verified still failing);
lint-determinism now scans tests/jspi (166 files clean);
eeschema-collab local-move gated to chromium (~50% flaky on FF even
solo; pcbnew twin covers both engines).
Docs: DEBUG.md rewritten as the JSPI debugging guide; build.md
describes the single-phase build; docs/features/async/README.md
banner-marked historical and repointed at the NEW
23-jspi-runtime.md (current architecture: export census, turnstile,
libcontext ownership + refusal contract, embind call shapes, the
em-pthread service-wrapper trick, exception policy, known gaps).
Gates on the cleaned tree: test:e2e 725 passed / 0 failed (after the
quiescence-probe fix; the 3 other reds were verified contention flakes
solo-green or the documented FF gate), web 76/0, jspi 18/18 both
engines, vitest 295/295 + 17/17, all lints green, live-app census
clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016X9eh1s5sTx1o9Em9KBuwR
2026-08-14 09:25:32 +02:00
|
|
|
pcbjam_collab::runOnCoroutine( fr, [fr, item, screen]() {
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
SCH_COMMIT commit( fr );
|
|
|
|
|
commit.Remove( item, screen );
|
|
|
|
|
commit.Push( wxT( "Collab test remove" ) );
|
test(ysync): repro tests for review bugs 01-07 + v2 items-wire e2e port (miss 11)
The 2026-07-02 sync review (docs/features/ysync-review, ysync-review branch)
found 7 bugs and that the two-tab e2e only exercised the DEAD legacy scalar
wire. This lands plan doc 15 in full; results + empirical findings in doc 16.
- tests/collab/browser-entry-v2.ts (+build.mjs): the PRODUCTION v2 stack
bundled for e2e (connectKicadDoc + attachKicadCollab, kdoc_* keys), with
in-page renderActiveDoc/singleSeedRender/driftReport helpers and yjs forced
to ONE copy (the two web pnpm workspaces otherwise bundle two
instanceof-incompatible instances).
- tests/kicad/ysync-two-tab.spec.ts: pl_editor green baseline (A↔B edits,
ITEM-level drift silence) + divergent-uuid adopt; bug-01 pcb/ee fresh-room
repros (Chromium-only: two kicad_editor tabs exceed Firefox's per-process
wasm budget); bug-06 concurrent-seed race; bug-03 Y-half.
- tests/kicad/ysync-repros-{pcbnew,eeschema}.spec.ts: bugs 02/03/05 + the
bug-04 matrix (anchor-centred fp rotation, pad resize, endpoint drag,
symbol rotation, Value-field edit), each with green landed-preconditions;
the "local move emits" controls double as headless-emit probes — GREEN on
both tools, so every emit-dependent repro is a live test.fail.
- wasm/bindings: 7 local-edit test hooks via real commits
(CallAfter+COROUTINE) — TestRemoveItem/TestRotateItem (both tools,
dispatched in the merged image), TestSetPadSize/TestMoveEndpoint (pcbnew),
TestSetFieldText (eeschema).
- web/standalone ysync-repros.test.ts: bug-01 units (C++-faithful fake gating
emit on ensureBridge) + bug-07a/b (stale DOWN hook, real sheet-manager gap).
- web/pcbjam-shared bump: bug-03/06 unit repros.
Convention: every repro asserts the CORRECT behavior and is expected-fail
(test.fail/it.fails) naming its bug doc; a fix flips it to "unexpected pass",
forcing marker removal — the repro becomes the regression test. Every
expected failure verified (JSON reporter) to fail at its documented assert.
Suite state: 39 passed / 0 failed / 0 flaky / 5 skipped (2 firefox guards,
2 pre-existing legacy two-tab skips, 1 pre-existing roundtrip fixme).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DPfrVhfYgPPgtawjSssZfn
2026-07-03 12:43:35 +02:00
|
|
|
} );
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Rotate an item in place (aDeg snapped to 90° CCW steps) — bug 04: a symbol's
|
|
|
|
|
// GetPosition() is unchanged by an in-place rotation and its json carries no
|
|
|
|
|
// orientation, so the rotation is invisible to the scalar differ.
|
|
|
|
|
bool schCollabTestRotateItem( std::string aId, double aDeg )
|
|
|
|
|
{
|
|
|
|
|
SCH_EDIT_FRAME* fr = schFrame();
|
|
|
|
|
|
|
|
|
|
if( !fr )
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
SCH_SHEET_PATH path;
|
|
|
|
|
SCH_ITEM* item = fr->Schematic().ResolveItem( KIID( wxString::FromUTF8( aId.c_str() ) ),
|
|
|
|
|
&path, /*allowNull*/ true );
|
|
|
|
|
|
|
|
|
|
if( !item )
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
SCH_SCREEN* screen = path.LastScreen();
|
|
|
|
|
int steps = ( (int) ( aDeg / 90.0 + ( aDeg >= 0 ? 0.5 : -0.5 ) ) % 4 + 4 ) % 4;
|
|
|
|
|
|
jspi cleanup: remove the asyncify-era residue — dead code, conditionals, pipeline scaffolding, stale prose
The runtime is JSPI-only; this removes everything that still pretended
otherwise. Three exhaustive sweeps (C++/JS+build+CI/tests+docs) drove
the inventory; every deletion verified by grep closure + full gates.
Broken-right-now fixes:
- deploy-staging.yml passed the retired opt_level input — the workflow
could not even start. Removed.
- env.sh carried dead exports with a live -sASYNCIFY=1 inside
(WASM_LDFLAGS/PTHREAD_LDFLAGS, zero consumers). Removed; the
WASM_LEGACY_EXCEPTIONS rationale rewritten to the real reason.
- docker/build.sh exported PCBJAM_ASYNC_BACKEND (read nowhere). Gone.
Dead weight removed:
- binaryen submodule (nothing builds or invokes it), wasm-opt-bench
workflow + scripts/bench/, get-wasm-opt.sh, diagnostics.js (242 lines
of Asyncify-API-only code), the KICAD_PIPELINE background-postprocess
scaffolding (existed to parallelize the deleted wasm-opt phase; the
postprocess is a seconds-long node script and now runs inline),
build-monitor's dead asyncify rows, sched-context orphan build
output, dead .gitignore entries, the .jspi-assets spike dir (the two
wf-result research JSONs moved to docs/features/async/migration-evidence/).
- bindings: fiber_park.h + its 12 embind registrations (broken-if-
called under JSPI), the kicadOpenFileStart/OPEN_JOB starter route,
main_stack_runner.h + 5 includes, the always-null context-sleep weak
hook in nanosleep_yield.c.
- shim: the backend field (installed-flag idempotency instead),
noteContextWait (dead both sides), the __wxAsyncifyDump alias (+ the
WasmTool fallback and string-dump normalize branch).
- web: the emscripten-6-ignored mainScriptUrlOrBlob option in boot.ts
(gerber-demo keeps it: it loads the deployed CDN release, which
predates emscripten 6 — noted inline).
Conditionals: all 'backend === jspi' checks reduced to scheduler-
presence checks; races_quiescent re-keyed from Asyncify.state (vacuous)
to real backlog quiescence (resumeReady/mutatorQueue — NOT _windowLive,
which is the probing activation's own window by definition).
Renames (identifiers only, no file renames): ASYNC_LINK_FLAGS→
JSPI_LINK_FLAGS and Makefile ASYNC_LDFLAGS→JSPI_LDFLAGS,
kicadCollabFiberBusy→kicadCollabBusy (embind + web + tests),
collab_common.h fiber*→apply*/coroutine naming, asyncifySignatures→
wasmTrapSignatures (lists byte-identical).
Tests: the two remaining vacuous [wx-asyncify]/fiber-resume-refused
asserts re-keyed to live JSPI beacons; eeschema-load's failure message
no longer sends the developer to a deleted script; wait-beacons' dead
families/parser deleted; lane-0 legacy-glue guards removed (lane 0 is
unconstructible); the embind test.fail re-gated with the JSPI reason
(plain embind invokers cannot suspend — verified still failing);
lint-determinism now scans tests/jspi (166 files clean);
eeschema-collab local-move gated to chromium (~50% flaky on FF even
solo; pcbnew twin covers both engines).
Docs: DEBUG.md rewritten as the JSPI debugging guide; build.md
describes the single-phase build; docs/features/async/README.md
banner-marked historical and repointed at the NEW
23-jspi-runtime.md (current architecture: export census, turnstile,
libcontext ownership + refusal contract, embind call shapes, the
em-pthread service-wrapper trick, exception policy, known gaps).
Gates on the cleaned tree: test:e2e 725 passed / 0 failed (after the
quiescence-probe fix; the 3 other reds were verified contention flakes
solo-green or the documented FF gate), web 76/0, jspi 18/18 both
engines, vitest 295/295 + 17/17, all lints green, live-app census
clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016X9eh1s5sTx1o9Em9KBuwR
2026-08-14 09:25:32 +02:00
|
|
|
pcbjam_collab::runOnCoroutine( fr, [fr, item, screen, steps]() {
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
SCH_COMMIT commit( fr );
|
|
|
|
|
commit.Modify( item, screen );
|
test(ysync): repro tests for review bugs 01-07 + v2 items-wire e2e port (miss 11)
The 2026-07-02 sync review (docs/features/ysync-review, ysync-review branch)
found 7 bugs and that the two-tab e2e only exercised the DEAD legacy scalar
wire. This lands plan doc 15 in full; results + empirical findings in doc 16.
- tests/collab/browser-entry-v2.ts (+build.mjs): the PRODUCTION v2 stack
bundled for e2e (connectKicadDoc + attachKicadCollab, kdoc_* keys), with
in-page renderActiveDoc/singleSeedRender/driftReport helpers and yjs forced
to ONE copy (the two web pnpm workspaces otherwise bundle two
instanceof-incompatible instances).
- tests/kicad/ysync-two-tab.spec.ts: pl_editor green baseline (A↔B edits,
ITEM-level drift silence) + divergent-uuid adopt; bug-01 pcb/ee fresh-room
repros (Chromium-only: two kicad_editor tabs exceed Firefox's per-process
wasm budget); bug-06 concurrent-seed race; bug-03 Y-half.
- tests/kicad/ysync-repros-{pcbnew,eeschema}.spec.ts: bugs 02/03/05 + the
bug-04 matrix (anchor-centred fp rotation, pad resize, endpoint drag,
symbol rotation, Value-field edit), each with green landed-preconditions;
the "local move emits" controls double as headless-emit probes — GREEN on
both tools, so every emit-dependent repro is a live test.fail.
- wasm/bindings: 7 local-edit test hooks via real commits
(CallAfter+COROUTINE) — TestRemoveItem/TestRotateItem (both tools,
dispatched in the merged image), TestSetPadSize/TestMoveEndpoint (pcbnew),
TestSetFieldText (eeschema).
- web/standalone ysync-repros.test.ts: bug-01 units (C++-faithful fake gating
emit on ensureBridge) + bug-07a/b (stale DOWN hook, real sheet-manager gap).
- web/pcbjam-shared bump: bug-03/06 unit repros.
Convention: every repro asserts the CORRECT behavior and is expected-fail
(test.fail/it.fails) naming its bug doc; a fix flips it to "unexpected pass",
forcing marker removal — the repro becomes the regression test. Every
expected failure verified (JSON reporter) to fail at its documented assert.
Suite state: 39 passed / 0 failed / 0 flaky / 5 skipped (2 firefox guards,
2 pre-existing legacy two-tab skips, 1 pre-existing roundtrip fixme).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DPfrVhfYgPPgtawjSssZfn
2026-07-03 12:43:35 +02:00
|
|
|
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
for( int i = 0; i < steps; ++i )
|
|
|
|
|
item->Rotate( item->GetPosition(), /*aRotateCCW*/ true );
|
test(ysync): repro tests for review bugs 01-07 + v2 items-wire e2e port (miss 11)
The 2026-07-02 sync review (docs/features/ysync-review, ysync-review branch)
found 7 bugs and that the two-tab e2e only exercised the DEAD legacy scalar
wire. This lands plan doc 15 in full; results + empirical findings in doc 16.
- tests/collab/browser-entry-v2.ts (+build.mjs): the PRODUCTION v2 stack
bundled for e2e (connectKicadDoc + attachKicadCollab, kdoc_* keys), with
in-page renderActiveDoc/singleSeedRender/driftReport helpers and yjs forced
to ONE copy (the two web pnpm workspaces otherwise bundle two
instanceof-incompatible instances).
- tests/kicad/ysync-two-tab.spec.ts: pl_editor green baseline (A↔B edits,
ITEM-level drift silence) + divergent-uuid adopt; bug-01 pcb/ee fresh-room
repros (Chromium-only: two kicad_editor tabs exceed Firefox's per-process
wasm budget); bug-06 concurrent-seed race; bug-03 Y-half.
- tests/kicad/ysync-repros-{pcbnew,eeschema}.spec.ts: bugs 02/03/05 + the
bug-04 matrix (anchor-centred fp rotation, pad resize, endpoint drag,
symbol rotation, Value-field edit), each with green landed-preconditions;
the "local move emits" controls double as headless-emit probes — GREEN on
both tools, so every emit-dependent repro is a live test.fail.
- wasm/bindings: 7 local-edit test hooks via real commits
(CallAfter+COROUTINE) — TestRemoveItem/TestRotateItem (both tools,
dispatched in the merged image), TestSetPadSize/TestMoveEndpoint (pcbnew),
TestSetFieldText (eeschema).
- web/standalone ysync-repros.test.ts: bug-01 units (C++-faithful fake gating
emit on ensureBridge) + bug-07a/b (stale DOWN hook, real sheet-manager gap).
- web/pcbjam-shared bump: bug-03/06 unit repros.
Convention: every repro asserts the CORRECT behavior and is expected-fail
(test.fail/it.fails) naming its bug doc; a fix flips it to "unexpected pass",
forcing marker removal — the repro becomes the regression test. Every
expected failure verified (JSON reporter) to fail at its documented assert.
Suite state: 39 passed / 0 failed / 0 flaky / 5 skipped (2 firefox guards,
2 pre-existing legacy two-tab skips, 1 pre-existing roundtrip fixme).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DPfrVhfYgPPgtawjSssZfn
2026-07-03 12:43:35 +02:00
|
|
|
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
commit.Push( wxT( "Collab test rotate" ) );
|
test(ysync): repro tests for review bugs 01-07 + v2 items-wire e2e port (miss 11)
The 2026-07-02 sync review (docs/features/ysync-review, ysync-review branch)
found 7 bugs and that the two-tab e2e only exercised the DEAD legacy scalar
wire. This lands plan doc 15 in full; results + empirical findings in doc 16.
- tests/collab/browser-entry-v2.ts (+build.mjs): the PRODUCTION v2 stack
bundled for e2e (connectKicadDoc + attachKicadCollab, kdoc_* keys), with
in-page renderActiveDoc/singleSeedRender/driftReport helpers and yjs forced
to ONE copy (the two web pnpm workspaces otherwise bundle two
instanceof-incompatible instances).
- tests/kicad/ysync-two-tab.spec.ts: pl_editor green baseline (A↔B edits,
ITEM-level drift silence) + divergent-uuid adopt; bug-01 pcb/ee fresh-room
repros (Chromium-only: two kicad_editor tabs exceed Firefox's per-process
wasm budget); bug-06 concurrent-seed race; bug-03 Y-half.
- tests/kicad/ysync-repros-{pcbnew,eeschema}.spec.ts: bugs 02/03/05 + the
bug-04 matrix (anchor-centred fp rotation, pad resize, endpoint drag,
symbol rotation, Value-field edit), each with green landed-preconditions;
the "local move emits" controls double as headless-emit probes — GREEN on
both tools, so every emit-dependent repro is a live test.fail.
- wasm/bindings: 7 local-edit test hooks via real commits
(CallAfter+COROUTINE) — TestRemoveItem/TestRotateItem (both tools,
dispatched in the merged image), TestSetPadSize/TestMoveEndpoint (pcbnew),
TestSetFieldText (eeschema).
- web/standalone ysync-repros.test.ts: bug-01 units (C++-faithful fake gating
emit on ensureBridge) + bug-07a/b (stale DOWN hook, real sheet-manager gap).
- web/pcbjam-shared bump: bug-03/06 unit repros.
Convention: every repro asserts the CORRECT behavior and is expected-fail
(test.fail/it.fails) naming its bug doc; a fix flips it to "unexpected pass",
forcing marker removal — the repro becomes the regression test. Every
expected failure verified (JSON reporter) to fail at its documented assert.
Suite state: 39 passed / 0 failed / 0 flaky / 5 skipped (2 firefox guards,
2 pre-existing legacy two-tab skips, 1 pre-existing roundtrip fixme).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DPfrVhfYgPPgtawjSssZfn
2026-07-03 12:43:35 +02:00
|
|
|
} );
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
// Run Edit>Undo / read the undo depth — miss 09; frame-generic, collab_common.h.
|
2026-07-08 11:09:26 +02:00
|
|
|
bool schCollabTestUndo()
|
|
|
|
|
{
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
return pcbjam_collab::testUndo( schFrame() );
|
2026-07-08 11:09:26 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int schCollabTestUndoDepth()
|
|
|
|
|
{
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
return pcbjam_collab::testUndoDepth( schFrame() );
|
2026-07-08 11:09:26 +02:00
|
|
|
}
|
|
|
|
|
|
test(ysync): repro tests for review bugs 01-07 + v2 items-wire e2e port (miss 11)
The 2026-07-02 sync review (docs/features/ysync-review, ysync-review branch)
found 7 bugs and that the two-tab e2e only exercised the DEAD legacy scalar
wire. This lands plan doc 15 in full; results + empirical findings in doc 16.
- tests/collab/browser-entry-v2.ts (+build.mjs): the PRODUCTION v2 stack
bundled for e2e (connectKicadDoc + attachKicadCollab, kdoc_* keys), with
in-page renderActiveDoc/singleSeedRender/driftReport helpers and yjs forced
to ONE copy (the two web pnpm workspaces otherwise bundle two
instanceof-incompatible instances).
- tests/kicad/ysync-two-tab.spec.ts: pl_editor green baseline (A↔B edits,
ITEM-level drift silence) + divergent-uuid adopt; bug-01 pcb/ee fresh-room
repros (Chromium-only: two kicad_editor tabs exceed Firefox's per-process
wasm budget); bug-06 concurrent-seed race; bug-03 Y-half.
- tests/kicad/ysync-repros-{pcbnew,eeschema}.spec.ts: bugs 02/03/05 + the
bug-04 matrix (anchor-centred fp rotation, pad resize, endpoint drag,
symbol rotation, Value-field edit), each with green landed-preconditions;
the "local move emits" controls double as headless-emit probes — GREEN on
both tools, so every emit-dependent repro is a live test.fail.
- wasm/bindings: 7 local-edit test hooks via real commits
(CallAfter+COROUTINE) — TestRemoveItem/TestRotateItem (both tools,
dispatched in the merged image), TestSetPadSize/TestMoveEndpoint (pcbnew),
TestSetFieldText (eeschema).
- web/standalone ysync-repros.test.ts: bug-01 units (C++-faithful fake gating
emit on ensureBridge) + bug-07a/b (stale DOWN hook, real sheet-manager gap).
- web/pcbjam-shared bump: bug-03/06 unit repros.
Convention: every repro asserts the CORRECT behavior and is expected-fail
(test.fail/it.fails) naming its bug doc; a fix flips it to "unexpected pass",
forcing marker removal — the repro becomes the regression test. Every
expected failure verified (JSON reporter) to fail at its documented assert.
Suite state: 39 passed / 0 failed / 0 flaky / 5 skipped (2 firefox guards,
2 pre-existing legacy two-tab skips, 1 pre-existing roundtrip fixme).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DPfrVhfYgPPgtawjSssZfn
2026-07-03 12:43:35 +02:00
|
|
|
// Set a symbol's Value field text — bug 04: fields live inside the symbol (not
|
|
|
|
|
// in screen->Items()) and the symbol json carries no field text, so the most
|
|
|
|
|
// common schematic edit after moving things never syncs.
|
|
|
|
|
bool schCollabTestSetFieldText( std::string aId, std::string aText )
|
|
|
|
|
{
|
|
|
|
|
SCH_EDIT_FRAME* fr = schFrame();
|
|
|
|
|
|
|
|
|
|
if( !fr )
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
SCH_SHEET_PATH path;
|
|
|
|
|
SCH_ITEM* item = fr->Schematic().ResolveItem( KIID( wxString::FromUTF8( aId.c_str() ) ),
|
|
|
|
|
&path, /*allowNull*/ true );
|
|
|
|
|
|
|
|
|
|
if( !item || item->Type() != SCH_SYMBOL_T )
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
SCH_SYMBOL* sym = static_cast<SCH_SYMBOL*>( item );
|
|
|
|
|
SCH_SCREEN* screen = path.LastScreen();
|
|
|
|
|
wxString text = wxString::FromUTF8( aText.c_str() );
|
|
|
|
|
|
jspi cleanup: remove the asyncify-era residue — dead code, conditionals, pipeline scaffolding, stale prose
The runtime is JSPI-only; this removes everything that still pretended
otherwise. Three exhaustive sweeps (C++/JS+build+CI/tests+docs) drove
the inventory; every deletion verified by grep closure + full gates.
Broken-right-now fixes:
- deploy-staging.yml passed the retired opt_level input — the workflow
could not even start. Removed.
- env.sh carried dead exports with a live -sASYNCIFY=1 inside
(WASM_LDFLAGS/PTHREAD_LDFLAGS, zero consumers). Removed; the
WASM_LEGACY_EXCEPTIONS rationale rewritten to the real reason.
- docker/build.sh exported PCBJAM_ASYNC_BACKEND (read nowhere). Gone.
Dead weight removed:
- binaryen submodule (nothing builds or invokes it), wasm-opt-bench
workflow + scripts/bench/, get-wasm-opt.sh, diagnostics.js (242 lines
of Asyncify-API-only code), the KICAD_PIPELINE background-postprocess
scaffolding (existed to parallelize the deleted wasm-opt phase; the
postprocess is a seconds-long node script and now runs inline),
build-monitor's dead asyncify rows, sched-context orphan build
output, dead .gitignore entries, the .jspi-assets spike dir (the two
wf-result research JSONs moved to docs/features/async/migration-evidence/).
- bindings: fiber_park.h + its 12 embind registrations (broken-if-
called under JSPI), the kicadOpenFileStart/OPEN_JOB starter route,
main_stack_runner.h + 5 includes, the always-null context-sleep weak
hook in nanosleep_yield.c.
- shim: the backend field (installed-flag idempotency instead),
noteContextWait (dead both sides), the __wxAsyncifyDump alias (+ the
WasmTool fallback and string-dump normalize branch).
- web: the emscripten-6-ignored mainScriptUrlOrBlob option in boot.ts
(gerber-demo keeps it: it loads the deployed CDN release, which
predates emscripten 6 — noted inline).
Conditionals: all 'backend === jspi' checks reduced to scheduler-
presence checks; races_quiescent re-keyed from Asyncify.state (vacuous)
to real backlog quiescence (resumeReady/mutatorQueue — NOT _windowLive,
which is the probing activation's own window by definition).
Renames (identifiers only, no file renames): ASYNC_LINK_FLAGS→
JSPI_LINK_FLAGS and Makefile ASYNC_LDFLAGS→JSPI_LDFLAGS,
kicadCollabFiberBusy→kicadCollabBusy (embind + web + tests),
collab_common.h fiber*→apply*/coroutine naming, asyncifySignatures→
wasmTrapSignatures (lists byte-identical).
Tests: the two remaining vacuous [wx-asyncify]/fiber-resume-refused
asserts re-keyed to live JSPI beacons; eeschema-load's failure message
no longer sends the developer to a deleted script; wait-beacons' dead
families/parser deleted; lane-0 legacy-glue guards removed (lane 0 is
unconstructible); the embind test.fail re-gated with the JSPI reason
(plain embind invokers cannot suspend — verified still failing);
lint-determinism now scans tests/jspi (166 files clean);
eeschema-collab local-move gated to chromium (~50% flaky on FF even
solo; pcbnew twin covers both engines).
Docs: DEBUG.md rewritten as the JSPI debugging guide; build.md
describes the single-phase build; docs/features/async/README.md
banner-marked historical and repointed at the NEW
23-jspi-runtime.md (current architecture: export census, turnstile,
libcontext ownership + refusal contract, embind call shapes, the
em-pthread service-wrapper trick, exception policy, known gaps).
Gates on the cleaned tree: test:e2e 725 passed / 0 failed (after the
quiescence-probe fix; the 3 other reds were verified contention flakes
solo-green or the documented FF gate), web 76/0, jspi 18/18 both
engines, vitest 295/295 + 17/17, all lints green, live-app census
clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016X9eh1s5sTx1o9Em9KBuwR
2026-08-14 09:25:32 +02:00
|
|
|
pcbjam_collab::runOnCoroutine( fr, [fr, sym, screen, text]() {
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
SCH_COMMIT commit( fr );
|
|
|
|
|
commit.Modify( sym, screen );
|
|
|
|
|
sym->SetValueFieldText( text );
|
|
|
|
|
commit.Push( wxT( "Collab test field text" ) );
|
test(ysync): repro tests for review bugs 01-07 + v2 items-wire e2e port (miss 11)
The 2026-07-02 sync review (docs/features/ysync-review, ysync-review branch)
found 7 bugs and that the two-tab e2e only exercised the DEAD legacy scalar
wire. This lands plan doc 15 in full; results + empirical findings in doc 16.
- tests/collab/browser-entry-v2.ts (+build.mjs): the PRODUCTION v2 stack
bundled for e2e (connectKicadDoc + attachKicadCollab, kdoc_* keys), with
in-page renderActiveDoc/singleSeedRender/driftReport helpers and yjs forced
to ONE copy (the two web pnpm workspaces otherwise bundle two
instanceof-incompatible instances).
- tests/kicad/ysync-two-tab.spec.ts: pl_editor green baseline (A↔B edits,
ITEM-level drift silence) + divergent-uuid adopt; bug-01 pcb/ee fresh-room
repros (Chromium-only: two kicad_editor tabs exceed Firefox's per-process
wasm budget); bug-06 concurrent-seed race; bug-03 Y-half.
- tests/kicad/ysync-repros-{pcbnew,eeschema}.spec.ts: bugs 02/03/05 + the
bug-04 matrix (anchor-centred fp rotation, pad resize, endpoint drag,
symbol rotation, Value-field edit), each with green landed-preconditions;
the "local move emits" controls double as headless-emit probes — GREEN on
both tools, so every emit-dependent repro is a live test.fail.
- wasm/bindings: 7 local-edit test hooks via real commits
(CallAfter+COROUTINE) — TestRemoveItem/TestRotateItem (both tools,
dispatched in the merged image), TestSetPadSize/TestMoveEndpoint (pcbnew),
TestSetFieldText (eeschema).
- web/standalone ysync-repros.test.ts: bug-01 units (C++-faithful fake gating
emit on ensureBridge) + bug-07a/b (stale DOWN hook, real sheet-manager gap).
- web/pcbjam-shared bump: bug-03/06 unit repros.
Convention: every repro asserts the CORRECT behavior and is expected-fail
(test.fail/it.fails) naming its bug doc; a fix flips it to "unexpected pass",
forcing marker removal — the repro becomes the regression test. Every
expected failure verified (JSON reporter) to fail at its documented assert.
Suite state: 39 passed / 0 failed / 0 flaky / 5 skipped (2 firefox guards,
2 pre-existing legacy two-tab skips, 1 pre-existing roundtrip fixme).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DPfrVhfYgPPgtawjSssZfn
2026-07-03 12:43:35 +02:00
|
|
|
} );
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2026-07-21 10:37:37 +02:00
|
|
|
// ── drift-trio phase B action hooks (standalone-hardening 0008 §5) ───────────
|
|
|
|
|
// Creation/mutation primitives for the trio harness's action catalog. Each
|
jspi cleanup: remove the asyncify-era residue — dead code, conditionals, pipeline scaffolding, stale prose
The runtime is JSPI-only; this removes everything that still pretended
otherwise. Three exhaustive sweeps (C++/JS+build+CI/tests+docs) drove
the inventory; every deletion verified by grep closure + full gates.
Broken-right-now fixes:
- deploy-staging.yml passed the retired opt_level input — the workflow
could not even start. Removed.
- env.sh carried dead exports with a live -sASYNCIFY=1 inside
(WASM_LDFLAGS/PTHREAD_LDFLAGS, zero consumers). Removed; the
WASM_LEGACY_EXCEPTIONS rationale rewritten to the real reason.
- docker/build.sh exported PCBJAM_ASYNC_BACKEND (read nowhere). Gone.
Dead weight removed:
- binaryen submodule (nothing builds or invokes it), wasm-opt-bench
workflow + scripts/bench/, get-wasm-opt.sh, diagnostics.js (242 lines
of Asyncify-API-only code), the KICAD_PIPELINE background-postprocess
scaffolding (existed to parallelize the deleted wasm-opt phase; the
postprocess is a seconds-long node script and now runs inline),
build-monitor's dead asyncify rows, sched-context orphan build
output, dead .gitignore entries, the .jspi-assets spike dir (the two
wf-result research JSONs moved to docs/features/async/migration-evidence/).
- bindings: fiber_park.h + its 12 embind registrations (broken-if-
called under JSPI), the kicadOpenFileStart/OPEN_JOB starter route,
main_stack_runner.h + 5 includes, the always-null context-sleep weak
hook in nanosleep_yield.c.
- shim: the backend field (installed-flag idempotency instead),
noteContextWait (dead both sides), the __wxAsyncifyDump alias (+ the
WasmTool fallback and string-dump normalize branch).
- web: the emscripten-6-ignored mainScriptUrlOrBlob option in boot.ts
(gerber-demo keeps it: it loads the deployed CDN release, which
predates emscripten 6 — noted inline).
Conditionals: all 'backend === jspi' checks reduced to scheduler-
presence checks; races_quiescent re-keyed from Asyncify.state (vacuous)
to real backlog quiescence (resumeReady/mutatorQueue — NOT _windowLive,
which is the probing activation's own window by definition).
Renames (identifiers only, no file renames): ASYNC_LINK_FLAGS→
JSPI_LINK_FLAGS and Makefile ASYNC_LDFLAGS→JSPI_LDFLAGS,
kicadCollabFiberBusy→kicadCollabBusy (embind + web + tests),
collab_common.h fiber*→apply*/coroutine naming, asyncifySignatures→
wasmTrapSignatures (lists byte-identical).
Tests: the two remaining vacuous [wx-asyncify]/fiber-resume-refused
asserts re-keyed to live JSPI beacons; eeschema-load's failure message
no longer sends the developer to a deleted script; wait-beacons' dead
families/parser deleted; lane-0 legacy-glue guards removed (lane 0 is
unconstructible); the embind test.fail re-gated with the JSPI reason
(plain embind invokers cannot suspend — verified still failing);
lint-determinism now scans tests/jspi (166 files clean);
eeschema-collab local-move gated to chromium (~50% flaky on FF even
solo; pcbnew twin covers both engines).
Docs: DEBUG.md rewritten as the JSPI debugging guide; build.md
describes the single-phase build; docs/features/async/README.md
banner-marked historical and repointed at the NEW
23-jspi-runtime.md (current architecture: export census, turnstile,
libcontext ownership + refusal contract, embind call shapes, the
em-pthread service-wrapper trick, exception policy, known gaps).
Gates on the cleaned tree: test:e2e 725 passed / 0 failed (after the
quiescence-probe fix; the 3 other reds were verified contention flakes
solo-green or the documented FF gate), web 76/0, jspi 18/18 both
engines, vitest 295/295 + 17/17, all lints green, live-app census
clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016X9eh1s5sTx1o9Em9KBuwR
2026-08-14 09:25:32 +02:00
|
|
|
// drives a REAL SCH_COMMIT on the apply coroutine, so the SCHEMATIC_LISTENER →
|
2026-07-21 10:37:37 +02:00
|
|
|
// flushDiff emit path runs exactly as for a UI edit. Names are tool-unique
|
|
|
|
|
// (registered outside the KICAD_MERGED_EMBIND guard — same convention as
|
|
|
|
|
// kicadCollabTestSetFieldText), so the merged image needs no dispatcher.
|
|
|
|
|
|
|
|
|
|
// Commit a freshly-built item onto the current screen; returns its uuid.
|
|
|
|
|
static std::string schCollabTestCommitAdd( SCH_ITEM* aItem, const wxChar* aMsg )
|
|
|
|
|
{
|
|
|
|
|
SCH_EDIT_FRAME* fr = schFrame();
|
|
|
|
|
|
|
|
|
|
if( !fr )
|
|
|
|
|
{
|
|
|
|
|
delete aItem;
|
|
|
|
|
return "";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
SCH_SCREEN* screen = fr->GetScreen();
|
|
|
|
|
|
|
|
|
|
// SCH_COMMIT::Push finds the SCHEMATIC via the ITEM's parent chain and
|
|
|
|
|
// silently skips every listener notification (OnItemsAdded → our emit)
|
|
|
|
|
// when it comes up null — a bare `new SCH_ITEM` has no parent yet, so
|
|
|
|
|
// parent it like the drawing tools do before staging.
|
|
|
|
|
aItem->SetParent( screen );
|
|
|
|
|
|
|
|
|
|
std::string id = toUtf8( aItem->m_Uuid.AsString() );
|
|
|
|
|
wxString msg( aMsg );
|
|
|
|
|
|
jspi cleanup: remove the asyncify-era residue — dead code, conditionals, pipeline scaffolding, stale prose
The runtime is JSPI-only; this removes everything that still pretended
otherwise. Three exhaustive sweeps (C++/JS+build+CI/tests+docs) drove
the inventory; every deletion verified by grep closure + full gates.
Broken-right-now fixes:
- deploy-staging.yml passed the retired opt_level input — the workflow
could not even start. Removed.
- env.sh carried dead exports with a live -sASYNCIFY=1 inside
(WASM_LDFLAGS/PTHREAD_LDFLAGS, zero consumers). Removed; the
WASM_LEGACY_EXCEPTIONS rationale rewritten to the real reason.
- docker/build.sh exported PCBJAM_ASYNC_BACKEND (read nowhere). Gone.
Dead weight removed:
- binaryen submodule (nothing builds or invokes it), wasm-opt-bench
workflow + scripts/bench/, get-wasm-opt.sh, diagnostics.js (242 lines
of Asyncify-API-only code), the KICAD_PIPELINE background-postprocess
scaffolding (existed to parallelize the deleted wasm-opt phase; the
postprocess is a seconds-long node script and now runs inline),
build-monitor's dead asyncify rows, sched-context orphan build
output, dead .gitignore entries, the .jspi-assets spike dir (the two
wf-result research JSONs moved to docs/features/async/migration-evidence/).
- bindings: fiber_park.h + its 12 embind registrations (broken-if-
called under JSPI), the kicadOpenFileStart/OPEN_JOB starter route,
main_stack_runner.h + 5 includes, the always-null context-sleep weak
hook in nanosleep_yield.c.
- shim: the backend field (installed-flag idempotency instead),
noteContextWait (dead both sides), the __wxAsyncifyDump alias (+ the
WasmTool fallback and string-dump normalize branch).
- web: the emscripten-6-ignored mainScriptUrlOrBlob option in boot.ts
(gerber-demo keeps it: it loads the deployed CDN release, which
predates emscripten 6 — noted inline).
Conditionals: all 'backend === jspi' checks reduced to scheduler-
presence checks; races_quiescent re-keyed from Asyncify.state (vacuous)
to real backlog quiescence (resumeReady/mutatorQueue — NOT _windowLive,
which is the probing activation's own window by definition).
Renames (identifiers only, no file renames): ASYNC_LINK_FLAGS→
JSPI_LINK_FLAGS and Makefile ASYNC_LDFLAGS→JSPI_LDFLAGS,
kicadCollabFiberBusy→kicadCollabBusy (embind + web + tests),
collab_common.h fiber*→apply*/coroutine naming, asyncifySignatures→
wasmTrapSignatures (lists byte-identical).
Tests: the two remaining vacuous [wx-asyncify]/fiber-resume-refused
asserts re-keyed to live JSPI beacons; eeschema-load's failure message
no longer sends the developer to a deleted script; wait-beacons' dead
families/parser deleted; lane-0 legacy-glue guards removed (lane 0 is
unconstructible); the embind test.fail re-gated with the JSPI reason
(plain embind invokers cannot suspend — verified still failing);
lint-determinism now scans tests/jspi (166 files clean);
eeschema-collab local-move gated to chromium (~50% flaky on FF even
solo; pcbnew twin covers both engines).
Docs: DEBUG.md rewritten as the JSPI debugging guide; build.md
describes the single-phase build; docs/features/async/README.md
banner-marked historical and repointed at the NEW
23-jspi-runtime.md (current architecture: export census, turnstile,
libcontext ownership + refusal contract, embind call shapes, the
em-pthread service-wrapper trick, exception policy, known gaps).
Gates on the cleaned tree: test:e2e 725 passed / 0 failed (after the
quiescence-probe fix; the 3 other reds were verified contention flakes
solo-green or the documented FF gate), web 76/0, jspi 18/18 both
engines, vitest 295/295 + 17/17, all lints green, live-app census
clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016X9eh1s5sTx1o9Em9KBuwR
2026-08-14 09:25:32 +02:00
|
|
|
pcbjam_collab::runOnCoroutine( fr, [fr, aItem, screen, msg]() {
|
2026-07-21 10:37:37 +02:00
|
|
|
SCH_COMMIT commit( fr );
|
|
|
|
|
commit.Add( aItem, screen );
|
|
|
|
|
commit.Push( msg );
|
|
|
|
|
} );
|
|
|
|
|
|
|
|
|
|
return id;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::string schCollabTestAddWire( int aX1, int aY1, int aX2, int aY2 )
|
|
|
|
|
{
|
|
|
|
|
SCH_LINE* line = new SCH_LINE( VECTOR2I( aX1, aY1 ), LAYER_WIRE );
|
|
|
|
|
line->SetEndPoint( VECTOR2I( aX2, aY2 ) );
|
|
|
|
|
return schCollabTestCommitAdd( line, wxT( "Collab test add wire" ) );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::string schCollabTestAddJunction( int aX, int aY )
|
|
|
|
|
{
|
|
|
|
|
return schCollabTestCommitAdd( new SCH_JUNCTION( VECTOR2I( aX, aY ) ),
|
|
|
|
|
wxT( "Collab test add junction" ) );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::string schCollabTestAddNoConnect( int aX, int aY )
|
|
|
|
|
{
|
|
|
|
|
return schCollabTestCommitAdd( new SCH_NO_CONNECT( VECTOR2I( aX, aY ) ),
|
|
|
|
|
wxT( "Collab test add no-connect" ) );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// aKind: "label" | "global" | "hier".
|
|
|
|
|
std::string schCollabTestAddLabel( std::string aKind, std::string aText, int aX, int aY )
|
|
|
|
|
{
|
|
|
|
|
wxString txt = wxString::FromUTF8( aText.c_str() );
|
|
|
|
|
VECTOR2I pos( aX, aY );
|
|
|
|
|
SCH_LABEL_BASE* label;
|
|
|
|
|
|
|
|
|
|
if( aKind == "global" )
|
|
|
|
|
label = new SCH_GLOBALLABEL( pos, txt );
|
|
|
|
|
else if( aKind == "hier" )
|
|
|
|
|
label = new SCH_HIERLABEL( pos, txt );
|
|
|
|
|
else
|
|
|
|
|
label = new SCH_LABEL( pos, txt );
|
|
|
|
|
|
|
|
|
|
return schCollabTestCommitAdd( label, wxT( "Collab test add label" ) );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Place another instance of a symbol ALREADY in the schematic's lib_symbols
|
|
|
|
|
// cache (the harness fixtures embed their defs — no external lib needed).
|
|
|
|
|
std::string schCollabTestAddSymbol( std::string aLibId, int aX, int aY, std::string aRef )
|
|
|
|
|
{
|
|
|
|
|
SCH_EDIT_FRAME* fr = schFrame();
|
|
|
|
|
|
|
|
|
|
if( !fr )
|
|
|
|
|
return "";
|
|
|
|
|
|
|
|
|
|
SCH_SCREEN* screen = fr->GetScreen();
|
|
|
|
|
wxString libIdStr = wxString::FromUTF8( aLibId.c_str() );
|
|
|
|
|
LIB_SYMBOL* libSym = nullptr;
|
|
|
|
|
|
|
|
|
|
for( const auto& [name, sym] : screen->GetLibSymbols() )
|
|
|
|
|
{
|
|
|
|
|
if( name == libIdStr )
|
|
|
|
|
{
|
|
|
|
|
libSym = sym;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if( !libSym )
|
|
|
|
|
return "";
|
|
|
|
|
|
|
|
|
|
LIB_ID libId;
|
|
|
|
|
|
|
|
|
|
if( libId.Parse( libIdStr ) >= 0 )
|
|
|
|
|
return "";
|
|
|
|
|
|
|
|
|
|
SCH_SHEET_PATH& sheet = fr->GetCurrentSheet();
|
|
|
|
|
SCH_SYMBOL* sym = new SCH_SYMBOL( *libSym, libId, &sheet, 1, 1, VECTOR2I( aX, aY ) );
|
|
|
|
|
sym->SetParent( screen ); // Push's SCHEMATIC lookup — see schCollabTestCommitAdd
|
|
|
|
|
sym->SetRef( &sheet, wxString::FromUTF8( aRef.c_str() ) );
|
|
|
|
|
|
|
|
|
|
std::string id = toUtf8( sym->m_Uuid.AsString() );
|
|
|
|
|
|
jspi cleanup: remove the asyncify-era residue — dead code, conditionals, pipeline scaffolding, stale prose
The runtime is JSPI-only; this removes everything that still pretended
otherwise. Three exhaustive sweeps (C++/JS+build+CI/tests+docs) drove
the inventory; every deletion verified by grep closure + full gates.
Broken-right-now fixes:
- deploy-staging.yml passed the retired opt_level input — the workflow
could not even start. Removed.
- env.sh carried dead exports with a live -sASYNCIFY=1 inside
(WASM_LDFLAGS/PTHREAD_LDFLAGS, zero consumers). Removed; the
WASM_LEGACY_EXCEPTIONS rationale rewritten to the real reason.
- docker/build.sh exported PCBJAM_ASYNC_BACKEND (read nowhere). Gone.
Dead weight removed:
- binaryen submodule (nothing builds or invokes it), wasm-opt-bench
workflow + scripts/bench/, get-wasm-opt.sh, diagnostics.js (242 lines
of Asyncify-API-only code), the KICAD_PIPELINE background-postprocess
scaffolding (existed to parallelize the deleted wasm-opt phase; the
postprocess is a seconds-long node script and now runs inline),
build-monitor's dead asyncify rows, sched-context orphan build
output, dead .gitignore entries, the .jspi-assets spike dir (the two
wf-result research JSONs moved to docs/features/async/migration-evidence/).
- bindings: fiber_park.h + its 12 embind registrations (broken-if-
called under JSPI), the kicadOpenFileStart/OPEN_JOB starter route,
main_stack_runner.h + 5 includes, the always-null context-sleep weak
hook in nanosleep_yield.c.
- shim: the backend field (installed-flag idempotency instead),
noteContextWait (dead both sides), the __wxAsyncifyDump alias (+ the
WasmTool fallback and string-dump normalize branch).
- web: the emscripten-6-ignored mainScriptUrlOrBlob option in boot.ts
(gerber-demo keeps it: it loads the deployed CDN release, which
predates emscripten 6 — noted inline).
Conditionals: all 'backend === jspi' checks reduced to scheduler-
presence checks; races_quiescent re-keyed from Asyncify.state (vacuous)
to real backlog quiescence (resumeReady/mutatorQueue — NOT _windowLive,
which is the probing activation's own window by definition).
Renames (identifiers only, no file renames): ASYNC_LINK_FLAGS→
JSPI_LINK_FLAGS and Makefile ASYNC_LDFLAGS→JSPI_LDFLAGS,
kicadCollabFiberBusy→kicadCollabBusy (embind + web + tests),
collab_common.h fiber*→apply*/coroutine naming, asyncifySignatures→
wasmTrapSignatures (lists byte-identical).
Tests: the two remaining vacuous [wx-asyncify]/fiber-resume-refused
asserts re-keyed to live JSPI beacons; eeschema-load's failure message
no longer sends the developer to a deleted script; wait-beacons' dead
families/parser deleted; lane-0 legacy-glue guards removed (lane 0 is
unconstructible); the embind test.fail re-gated with the JSPI reason
(plain embind invokers cannot suspend — verified still failing);
lint-determinism now scans tests/jspi (166 files clean);
eeschema-collab local-move gated to chromium (~50% flaky on FF even
solo; pcbnew twin covers both engines).
Docs: DEBUG.md rewritten as the JSPI debugging guide; build.md
describes the single-phase build; docs/features/async/README.md
banner-marked historical and repointed at the NEW
23-jspi-runtime.md (current architecture: export census, turnstile,
libcontext ownership + refusal contract, embind call shapes, the
em-pthread service-wrapper trick, exception policy, known gaps).
Gates on the cleaned tree: test:e2e 725 passed / 0 failed (after the
quiescence-probe fix; the 3 other reds were verified contention flakes
solo-green or the documented FF gate), web 76/0, jspi 18/18 both
engines, vitest 295/295 + 17/17, all lints green, live-app census
clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016X9eh1s5sTx1o9Em9KBuwR
2026-08-14 09:25:32 +02:00
|
|
|
pcbjam_collab::runOnCoroutine( fr, [fr, sym, screen]() {
|
2026-07-21 10:37:37 +02:00
|
|
|
SCH_COMMIT commit( fr );
|
|
|
|
|
commit.Add( sym, screen );
|
|
|
|
|
commit.Push( wxT( "Collab test add symbol" ) );
|
|
|
|
|
} );
|
|
|
|
|
|
|
|
|
|
return id;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// By-uuid variant of MoveFirst (same CallAfter + devirtualized move path).
|
|
|
|
|
bool schCollabTestMoveSchItem( std::string aId, int aDx, int aDy )
|
|
|
|
|
{
|
|
|
|
|
SCH_EDIT_FRAME* fr = schFrame();
|
|
|
|
|
|
|
|
|
|
if( !fr )
|
|
|
|
|
return false;
|
|
|
|
|
|
2026-07-21 11:02:41 +02:00
|
|
|
SCH_SHEET_PATH probe;
|
|
|
|
|
if( !fr->Schematic().ResolveItem( KIID( wxString::FromUTF8( aId.c_str() ) ), &probe,
|
|
|
|
|
/*allowNull*/ true ) )
|
2026-07-21 10:37:37 +02:00
|
|
|
return false;
|
|
|
|
|
|
2026-07-21 11:02:41 +02:00
|
|
|
// Re-resolve when the deferred body runs: a remote remove can apply in
|
|
|
|
|
// between and the captured pointer would be dangling — the commit would
|
|
|
|
|
// resurrect a deleted item (drift-trio S4 move-vs-delete). Vanished =>
|
|
|
|
|
// the move loses, silently.
|
|
|
|
|
fr->CallAfter( [fr, aId, aDx, aDy]() {
|
|
|
|
|
SCH_SHEET_PATH path;
|
|
|
|
|
SCH_ITEM* item = fr->Schematic().ResolveItem( KIID( wxString::FromUTF8( aId.c_str() ) ),
|
|
|
|
|
&path, /*allowNull*/ true );
|
|
|
|
|
|
|
|
|
|
if( item )
|
|
|
|
|
collabTestMove( fr, item, path.LastScreen(), aDx, aDy );
|
|
|
|
|
} );
|
2026-07-21 10:37:37 +02:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool schCollabTestMirrorSchItem( std::string aId, bool aHorizontal )
|
|
|
|
|
{
|
|
|
|
|
SCH_EDIT_FRAME* fr = schFrame();
|
|
|
|
|
|
|
|
|
|
if( !fr )
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
SCH_SHEET_PATH path;
|
|
|
|
|
SCH_ITEM* item = fr->Schematic().ResolveItem( KIID( wxString::FromUTF8( aId.c_str() ) ),
|
|
|
|
|
&path, /*allowNull*/ true );
|
|
|
|
|
|
|
|
|
|
if( !item )
|
|
|
|
|
return false;
|
|
|
|
|
|
jspi cleanup: remove the asyncify-era residue — dead code, conditionals, pipeline scaffolding, stale prose
The runtime is JSPI-only; this removes everything that still pretended
otherwise. Three exhaustive sweeps (C++/JS+build+CI/tests+docs) drove
the inventory; every deletion verified by grep closure + full gates.
Broken-right-now fixes:
- deploy-staging.yml passed the retired opt_level input — the workflow
could not even start. Removed.
- env.sh carried dead exports with a live -sASYNCIFY=1 inside
(WASM_LDFLAGS/PTHREAD_LDFLAGS, zero consumers). Removed; the
WASM_LEGACY_EXCEPTIONS rationale rewritten to the real reason.
- docker/build.sh exported PCBJAM_ASYNC_BACKEND (read nowhere). Gone.
Dead weight removed:
- binaryen submodule (nothing builds or invokes it), wasm-opt-bench
workflow + scripts/bench/, get-wasm-opt.sh, diagnostics.js (242 lines
of Asyncify-API-only code), the KICAD_PIPELINE background-postprocess
scaffolding (existed to parallelize the deleted wasm-opt phase; the
postprocess is a seconds-long node script and now runs inline),
build-monitor's dead asyncify rows, sched-context orphan build
output, dead .gitignore entries, the .jspi-assets spike dir (the two
wf-result research JSONs moved to docs/features/async/migration-evidence/).
- bindings: fiber_park.h + its 12 embind registrations (broken-if-
called under JSPI), the kicadOpenFileStart/OPEN_JOB starter route,
main_stack_runner.h + 5 includes, the always-null context-sleep weak
hook in nanosleep_yield.c.
- shim: the backend field (installed-flag idempotency instead),
noteContextWait (dead both sides), the __wxAsyncifyDump alias (+ the
WasmTool fallback and string-dump normalize branch).
- web: the emscripten-6-ignored mainScriptUrlOrBlob option in boot.ts
(gerber-demo keeps it: it loads the deployed CDN release, which
predates emscripten 6 — noted inline).
Conditionals: all 'backend === jspi' checks reduced to scheduler-
presence checks; races_quiescent re-keyed from Asyncify.state (vacuous)
to real backlog quiescence (resumeReady/mutatorQueue — NOT _windowLive,
which is the probing activation's own window by definition).
Renames (identifiers only, no file renames): ASYNC_LINK_FLAGS→
JSPI_LINK_FLAGS and Makefile ASYNC_LDFLAGS→JSPI_LDFLAGS,
kicadCollabFiberBusy→kicadCollabBusy (embind + web + tests),
collab_common.h fiber*→apply*/coroutine naming, asyncifySignatures→
wasmTrapSignatures (lists byte-identical).
Tests: the two remaining vacuous [wx-asyncify]/fiber-resume-refused
asserts re-keyed to live JSPI beacons; eeschema-load's failure message
no longer sends the developer to a deleted script; wait-beacons' dead
families/parser deleted; lane-0 legacy-glue guards removed (lane 0 is
unconstructible); the embind test.fail re-gated with the JSPI reason
(plain embind invokers cannot suspend — verified still failing);
lint-determinism now scans tests/jspi (166 files clean);
eeschema-collab local-move gated to chromium (~50% flaky on FF even
solo; pcbnew twin covers both engines).
Docs: DEBUG.md rewritten as the JSPI debugging guide; build.md
describes the single-phase build; docs/features/async/README.md
banner-marked historical and repointed at the NEW
23-jspi-runtime.md (current architecture: export census, turnstile,
libcontext ownership + refusal contract, embind call shapes, the
em-pthread service-wrapper trick, exception policy, known gaps).
Gates on the cleaned tree: test:e2e 725 passed / 0 failed (after the
quiescence-probe fix; the 3 other reds were verified contention flakes
solo-green or the documented FF gate), web 76/0, jspi 18/18 both
engines, vitest 295/295 + 17/17, all lints green, live-app census
clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016X9eh1s5sTx1o9Em9KBuwR
2026-08-14 09:25:32 +02:00
|
|
|
pcbjam_collab::runOnCoroutine( fr, [fr, aId, aHorizontal]() { // re-resolve on the coroutine (S4)
|
2026-07-21 11:02:41 +02:00
|
|
|
SCH_SHEET_PATH path;
|
|
|
|
|
SCH_ITEM* live = fr->Schematic().ResolveItem( KIID( wxString::FromUTF8( aId.c_str() ) ),
|
|
|
|
|
&path, /*allowNull*/ true );
|
|
|
|
|
|
|
|
|
|
if( !live )
|
|
|
|
|
return;
|
2026-07-21 10:37:37 +02:00
|
|
|
|
|
|
|
|
SCH_COMMIT commit( fr );
|
2026-07-21 11:02:41 +02:00
|
|
|
commit.Modify( live, path.LastScreen() );
|
2026-07-21 10:37:37 +02:00
|
|
|
|
|
|
|
|
if( aHorizontal )
|
2026-07-21 11:02:41 +02:00
|
|
|
live->MirrorHorizontally( live->GetPosition().x );
|
2026-07-21 10:37:37 +02:00
|
|
|
else
|
2026-07-21 11:02:41 +02:00
|
|
|
live->MirrorVertically( live->GetPosition().y );
|
2026-07-21 10:37:37 +02:00
|
|
|
|
|
|
|
|
commit.Push( wxT( "Collab test mirror" ) );
|
|
|
|
|
} );
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Duplicate (fresh uuids — exercises the Clone()/uuid-churn drift class).
|
|
|
|
|
std::string schCollabTestDuplicateSchItem( std::string aId, int aDx, int aDy )
|
|
|
|
|
{
|
|
|
|
|
SCH_EDIT_FRAME* fr = schFrame();
|
|
|
|
|
|
|
|
|
|
if( !fr )
|
|
|
|
|
return "";
|
|
|
|
|
|
|
|
|
|
SCH_SHEET_PATH path;
|
|
|
|
|
SCH_ITEM* item = fr->Schematic().ResolveItem( KIID( wxString::FromUTF8( aId.c_str() ) ),
|
|
|
|
|
&path, /*allowNull*/ true );
|
|
|
|
|
|
|
|
|
|
if( !item )
|
|
|
|
|
return "";
|
|
|
|
|
|
|
|
|
|
SCH_SCREEN* screen = path.LastScreen();
|
|
|
|
|
SCH_ITEM* dup = item->Duplicate( /*addToParentGroup*/ false );
|
|
|
|
|
dup->Move( VECTOR2I( aDx, aDy ) );
|
|
|
|
|
|
|
|
|
|
std::string id = toUtf8( dup->m_Uuid.AsString() );
|
|
|
|
|
|
jspi cleanup: remove the asyncify-era residue — dead code, conditionals, pipeline scaffolding, stale prose
The runtime is JSPI-only; this removes everything that still pretended
otherwise. Three exhaustive sweeps (C++/JS+build+CI/tests+docs) drove
the inventory; every deletion verified by grep closure + full gates.
Broken-right-now fixes:
- deploy-staging.yml passed the retired opt_level input — the workflow
could not even start. Removed.
- env.sh carried dead exports with a live -sASYNCIFY=1 inside
(WASM_LDFLAGS/PTHREAD_LDFLAGS, zero consumers). Removed; the
WASM_LEGACY_EXCEPTIONS rationale rewritten to the real reason.
- docker/build.sh exported PCBJAM_ASYNC_BACKEND (read nowhere). Gone.
Dead weight removed:
- binaryen submodule (nothing builds or invokes it), wasm-opt-bench
workflow + scripts/bench/, get-wasm-opt.sh, diagnostics.js (242 lines
of Asyncify-API-only code), the KICAD_PIPELINE background-postprocess
scaffolding (existed to parallelize the deleted wasm-opt phase; the
postprocess is a seconds-long node script and now runs inline),
build-monitor's dead asyncify rows, sched-context orphan build
output, dead .gitignore entries, the .jspi-assets spike dir (the two
wf-result research JSONs moved to docs/features/async/migration-evidence/).
- bindings: fiber_park.h + its 12 embind registrations (broken-if-
called under JSPI), the kicadOpenFileStart/OPEN_JOB starter route,
main_stack_runner.h + 5 includes, the always-null context-sleep weak
hook in nanosleep_yield.c.
- shim: the backend field (installed-flag idempotency instead),
noteContextWait (dead both sides), the __wxAsyncifyDump alias (+ the
WasmTool fallback and string-dump normalize branch).
- web: the emscripten-6-ignored mainScriptUrlOrBlob option in boot.ts
(gerber-demo keeps it: it loads the deployed CDN release, which
predates emscripten 6 — noted inline).
Conditionals: all 'backend === jspi' checks reduced to scheduler-
presence checks; races_quiescent re-keyed from Asyncify.state (vacuous)
to real backlog quiescence (resumeReady/mutatorQueue — NOT _windowLive,
which is the probing activation's own window by definition).
Renames (identifiers only, no file renames): ASYNC_LINK_FLAGS→
JSPI_LINK_FLAGS and Makefile ASYNC_LDFLAGS→JSPI_LDFLAGS,
kicadCollabFiberBusy→kicadCollabBusy (embind + web + tests),
collab_common.h fiber*→apply*/coroutine naming, asyncifySignatures→
wasmTrapSignatures (lists byte-identical).
Tests: the two remaining vacuous [wx-asyncify]/fiber-resume-refused
asserts re-keyed to live JSPI beacons; eeschema-load's failure message
no longer sends the developer to a deleted script; wait-beacons' dead
families/parser deleted; lane-0 legacy-glue guards removed (lane 0 is
unconstructible); the embind test.fail re-gated with the JSPI reason
(plain embind invokers cannot suspend — verified still failing);
lint-determinism now scans tests/jspi (166 files clean);
eeschema-collab local-move gated to chromium (~50% flaky on FF even
solo; pcbnew twin covers both engines).
Docs: DEBUG.md rewritten as the JSPI debugging guide; build.md
describes the single-phase build; docs/features/async/README.md
banner-marked historical and repointed at the NEW
23-jspi-runtime.md (current architecture: export census, turnstile,
libcontext ownership + refusal contract, embind call shapes, the
em-pthread service-wrapper trick, exception policy, known gaps).
Gates on the cleaned tree: test:e2e 725 passed / 0 failed (after the
quiescence-probe fix; the 3 other reds were verified contention flakes
solo-green or the documented FF gate), web 76/0, jspi 18/18 both
engines, vitest 295/295 + 17/17, all lints green, live-app census
clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016X9eh1s5sTx1o9Em9KBuwR
2026-08-14 09:25:32 +02:00
|
|
|
pcbjam_collab::runOnCoroutine( fr, [fr, dup, screen]() {
|
2026-07-21 10:37:37 +02:00
|
|
|
SCH_COMMIT commit( fr );
|
|
|
|
|
commit.Add( dup, screen );
|
|
|
|
|
commit.Push( wxT( "Collab test duplicate" ) );
|
|
|
|
|
} );
|
|
|
|
|
|
|
|
|
|
return id;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2026-06-10 18:06:33 +02:00
|
|
|
// Programmatically save the in-memory schematic to a .kicad_sch file, without
|
|
|
|
|
// driving the Save As dialog — eeschema's analogue of pl_editor's
|
|
|
|
|
// kicadSaveDrawingSheet. Serializes the root sheet via the same SCH_IO_KICAD_SEXPR
|
|
|
|
|
// writer eeschema uses, so a test can read the file back from MEMFS and assert the
|
|
|
|
|
// file ⇄ Y.Doc round trip (README §A; feature 0004). Single-sheet scope: the round-
|
|
|
|
|
// trip fixtures are flat schematics; saving the root sheet writes the whole model.
|
2026-06-12 10:16:02 +02:00
|
|
|
// C++ → JS save notification (standalone-hardening save routing). Called from the
|
|
|
|
|
// kicad fork's save chokepoint (SCH_EDIT_FRAME::saveSchematicFile) after a
|
|
|
|
|
// successful write to MEMFS, so the web app can route the saved bytes onward
|
|
|
|
|
// (API upload, local-disk write-back, download). No-op without a JS listener.
|
2026-07-02 14:48:11 +02:00
|
|
|
// KICAD_MERGED_EMBIND: identical definition in pcbnew_embind.cpp; the merged image
|
|
|
|
|
// gets the one in kicad_editor_embind.cpp (both fork save chokepoints call it).
|
|
|
|
|
#ifndef KICAD_MERGED_EMBIND
|
2026-06-12 10:16:02 +02:00
|
|
|
extern "C" void kicadCollabOnSave( const char* aPath )
|
|
|
|
|
{
|
|
|
|
|
EM_ASM( {
|
|
|
|
|
if( window.kicadCollab && window.kicadCollab.onSave )
|
staging CI green-up: P-1 apply-slot wedge under JSPI, popup P-4 regression, ysync double-seed header, spec fixes
- P-1 (findings-p.spec P-1c red on CI + locally): a JS exception thrown from a
window.kicadCollab wire callback does NOT reach the C++ catch(...) — under
JSPI it REJECTS the promising coroutine entry, slot.done never flips and the
apply slot is busy for the page's life. Three layers: libcontext flags the
rejected record dead (kicad → d0afbc4bd3),
drainApplies() reaps a !done && !CanResume() body, and every EM_ASM
window.kicadCollab.onX(...) call is try/catch-guarded (collab_common.h,
kicad_editor/eeschema/pl_editor embind).
- popup.spec 'dismisses on outside click': wx P-4 SetFocus delegation excluded
for wxPopupWindow (wxwidgets → a7436d1052).
- ysync-two-tab concurrent seed: repairLayoutY + seedNonce retraction deleted
both headers (pcbjam-shared → 8dabf2e).
- dark-mode.spec: stale light-mode reference (pre wx fa46f1822c DOM
re-projection) replaced with the CI chromium render.
- items-wire-batch-loss.spec: precondition obsolete after P-5 — poisoned entry
is now synthetic, plus a P-5 contract assertion (field blob == '').
- presence-ghost-peer.spec: skip early when no /parties/ socket (CI web e2e
runs the BroadcastChannel provider); findings-p1.spec: polled click-select
precondition.
Local: kicad-chromium findings/presence/ysync/collab 59/59, popup 7/7,
dark-mode 1/1, pcbjam-shared vitest 249/249.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LvvARNMxQrq17vogn3REMU
2026-08-28 22:25:21 +02:00
|
|
|
{
|
|
|
|
|
// A throwing listener must never unwind the wasm frame that called it: under
|
|
|
|
|
// JSPI that rejects the running coroutine's entry (findings P-1).
|
|
|
|
|
try { window.kicadCollab.onSave( UTF8ToString( $0 ) ); }
|
|
|
|
|
catch( e ) { console.error( '[pcbjam collab] onSave listener threw', e ); }
|
|
|
|
|
}
|
2026-06-12 10:16:02 +02:00
|
|
|
}, aPath );
|
|
|
|
|
}
|
2026-07-02 14:48:11 +02:00
|
|
|
#endif // !KICAD_MERGED_EMBIND
|
|
|
|
|
|
feat(collab): presence P3 — eeschema port (collab-presence 0003)
- eeschema_embind.cpp: presence section (0002 pattern, zero fork changes) —
wx canvas triggers + SCHEMATIC_LISTENER piggyback → post-settle
SCH_SELECTION_TOOL emit; throttled cursor; remote VIEW_OVERLAY render
(SCHEMATIC::ResolveItem, name tags, screen-constant via GAL matrix);
schCollab{PresenceStart,SetRemote,GetViewport,GetSelection,TestSelectFirst,
TestClearSelection}; kicad_editor_embind dispatches by active frame.
- sheet-manager: parked rooms carry SKELETON awareness states
({user,tool,sheetPath=bound sheet}) via publishSkeletons on switch + late
warm-up — the bound room's awareness then holds every project peer, so no
multi-room aggregation; presence.ts publishSkeleton helper.
- PresenceRoster: sheet-aware — peers on another sheet render dimmed with
'on <sheet>' tooltip; WasmTool un-gates the kicad presence bridge for
eeschema and threads activeSheetPath.
- tests: presence-eeschema.spec.ts (5 e2e, mirrors pcbnew incl. the px/IU
band at eeschema's 1e4/mm IU); presence.test.ts +2 (skeleton visibility,
no ghost cursor after rebind). eeschema-collab/subschema stay green.
Verified live: two tabs on demo.kicad_sch — peer cursor cross + label,
selection box + name tag, roster avatar.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
2026-07-06 17:05:15 +02:00
|
|
|
// ── presence entry points (collab-presence 0003 — eeschema port of the 0002 set) ────────────
|
|
|
|
|
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
// Install the presence input hooks on the GAL canvas + the fork's soft-lock
|
|
|
|
|
// query (idempotent) — shared core. The canvas is the same window across
|
|
|
|
|
// sheet navigation, so one install serves the whole session.
|
feat(collab): presence P3 — eeschema port (collab-presence 0003)
- eeschema_embind.cpp: presence section (0002 pattern, zero fork changes) —
wx canvas triggers + SCHEMATIC_LISTENER piggyback → post-settle
SCH_SELECTION_TOOL emit; throttled cursor; remote VIEW_OVERLAY render
(SCHEMATIC::ResolveItem, name tags, screen-constant via GAL matrix);
schCollab{PresenceStart,SetRemote,GetViewport,GetSelection,TestSelectFirst,
TestClearSelection}; kicad_editor_embind dispatches by active frame.
- sheet-manager: parked rooms carry SKELETON awareness states
({user,tool,sheetPath=bound sheet}) via publishSkeletons on switch + late
warm-up — the bound room's awareness then holds every project peer, so no
multi-room aggregation; presence.ts publishSkeleton helper.
- PresenceRoster: sheet-aware — peers on another sheet render dimmed with
'on <sheet>' tooltip; WasmTool un-gates the kicad presence bridge for
eeschema and threads activeSheetPath.
- tests: presence-eeschema.spec.ts (5 e2e, mirrors pcbnew incl. the px/IU
band at eeschema's 1e4/mm IU); presence.test.ts +2 (skeleton visibility,
no ghost cursor after rebind). eeschema-collab/subschema stay green.
Verified live: two tabs on demo.kicad_sch — peer cursor cross + label,
selection box + name tag, roster avatar.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
2026-07-06 17:05:15 +02:00
|
|
|
void schCollabPresenceStart()
|
|
|
|
|
{
|
2026-08-28 16:14:44 +02:00
|
|
|
// Presence needs the document listener too (findings Y-1/Y-3: doc changes
|
|
|
|
|
// repaint peers' shapes + re-check the local selection) — collab normally
|
|
|
|
|
// registers it first via snapshot/apply, but presence must not depend on
|
|
|
|
|
// that ordering (test harnesses, presence-only sessions).
|
|
|
|
|
ensureBridge();
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
presenceCore().start();
|
feat(collab): presence P3 — eeschema port (collab-presence 0003)
- eeschema_embind.cpp: presence section (0002 pattern, zero fork changes) —
wx canvas triggers + SCHEMATIC_LISTENER piggyback → post-settle
SCH_SELECTION_TOOL emit; throttled cursor; remote VIEW_OVERLAY render
(SCHEMATIC::ResolveItem, name tags, screen-constant via GAL matrix);
schCollab{PresenceStart,SetRemote,GetViewport,GetSelection,TestSelectFirst,
TestClearSelection}; kicad_editor_embind dispatches by active frame.
- sheet-manager: parked rooms carry SKELETON awareness states
({user,tool,sheetPath=bound sheet}) via publishSkeletons on switch + late
warm-up — the bound room's awareness then holds every project peer, so no
multi-room aggregation; presence.ts publishSkeleton helper.
- PresenceRoster: sheet-aware — peers on another sheet render dimmed with
'on <sheet>' tooltip; WasmTool un-gates the kicad presence bridge for
eeschema and threads activeSheetPath.
- tests: presence-eeschema.spec.ts (5 e2e, mirrors pcbnew incl. the px/IU
band at eeschema's 1e4/mm IU); presence.test.ts +2 (skeleton visibility,
no ghost cursor after rebind). eeschema-collab/subschema stay green.
Verified live: two tabs on demo.kicad_sch — peer cursor cross + label,
selection box + name tag, roster avatar.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
2026-07-06 17:05:15 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// JS → C++: full remote-peers snapshot (same wire as pcbnew's kicadCollabSetRemote).
|
|
|
|
|
// Rooms are per-sheet, so the JS rebind pushes a fresh (or empty) snapshot on every
|
|
|
|
|
// sheet switch — peers here are always this sheet's.
|
|
|
|
|
void schCollabSetRemote( std::string aJson )
|
|
|
|
|
{
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
presenceCore().setRemote( aJson );
|
feat(collab): presence P3 — eeschema port (collab-presence 0003)
- eeschema_embind.cpp: presence section (0002 pattern, zero fork changes) —
wx canvas triggers + SCHEMATIC_LISTENER piggyback → post-settle
SCH_SELECTION_TOOL emit; throttled cursor; remote VIEW_OVERLAY render
(SCHEMATIC::ResolveItem, name tags, screen-constant via GAL matrix);
schCollab{PresenceStart,SetRemote,GetViewport,GetSelection,TestSelectFirst,
TestClearSelection}; kicad_editor_embind dispatches by active frame.
- sheet-manager: parked rooms carry SKELETON awareness states
({user,tool,sheetPath=bound sheet}) via publishSkeletons on switch + late
warm-up — the bound room's awareness then holds every project peer, so no
multi-room aggregation; presence.ts publishSkeleton helper.
- PresenceRoster: sheet-aware — peers on another sheet render dimmed with
'on <sheet>' tooltip; WasmTool un-gates the kicad presence bridge for
eeschema and threads activeSheetPath.
- tests: presence-eeschema.spec.ts (5 e2e, mirrors pcbnew incl. the px/IU
band at eeschema's 1e4/mm IU); presence.test.ts +2 (skeleton visibility,
no ghost cursor after rebind). eeschema-collab/subschema stay green.
Verified live: two tabs on demo.kicad_sch — peer cursor cross + label,
selection box + name tag, roster avatar.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
2026-07-06 17:05:15 +02:00
|
|
|
}
|
|
|
|
|
|
2026-08-28 16:14:44 +02:00
|
|
|
// JS → C++: cursor-only update for the peers of the last snapshot (findings Y-4).
|
|
|
|
|
void schCollabSetRemoteCursors( std::string aJson )
|
|
|
|
|
{
|
|
|
|
|
presenceCore().setRemoteCursors( aJson );
|
|
|
|
|
}
|
|
|
|
|
|
feat(comments): figma-like comment pins + threads (collab-presence 0005)
Hybrid pins: the wasm draws the dot (kicadCollabSetPins rides the presence
VIEW_OVERLAY, author color + white ring, drawn above selections; zero
kicad-fork changes), the DOM owns interaction —
- comments.ts: controller gluing the MIT kdoc_comments helpers to the editor:
anchor resolution per tool IU (pins track item moves via kdoc_items
observation), throttled pin snapshots, anchorAt nearest-item snap, jumpTo
via new kicadCollabSetViewport; rebinds per sheet like presence.
- CommentLayer.tsx: comment mode (click catcher + composer), pin hit targets
over the GAL dots, thread popover (reply/edit/delete own, resolve/reopen,
delete thread), panel with resolved filter + jump-to (popover centers when
the pin is off-screen). Resolved pins drop figma-style.
- WasmTool: controller lifecycle beside presence; live viewport feed;
window.__pcbjamComments test handle (threads persist in the room ydoc).
- e2e tests/web/comments.spec.ts: two-tab create → reply → resolve → panel
filter → delete, passing vs real partykit; presence suites + collab units
stay green; shared pointer bump (0004 model).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
2026-07-06 17:50:11 +02:00
|
|
|
// JS → C++ (collab-presence 0005): comment pin dots (same wire as pcbnew).
|
|
|
|
|
void schCollabSetPins( std::string aJson )
|
|
|
|
|
{
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
presenceCore().setPins( aJson );
|
feat(comments): figma-like comment pins + threads (collab-presence 0005)
Hybrid pins: the wasm draws the dot (kicadCollabSetPins rides the presence
VIEW_OVERLAY, author color + white ring, drawn above selections; zero
kicad-fork changes), the DOM owns interaction —
- comments.ts: controller gluing the MIT kdoc_comments helpers to the editor:
anchor resolution per tool IU (pins track item moves via kdoc_items
observation), throttled pin snapshots, anchorAt nearest-item snap, jumpTo
via new kicadCollabSetViewport; rebinds per sheet like presence.
- CommentLayer.tsx: comment mode (click catcher + composer), pin hit targets
over the GAL dots, thread popover (reply/edit/delete own, resolve/reopen,
delete thread), panel with resolved filter + jump-to (popover centers when
the pin is off-screen). Resolved pins drop figma-style.
- WasmTool: controller lifecycle beside presence; live viewport feed;
window.__pcbjamComments test handle (threads persist in the room ydoc).
- e2e tests/web/comments.spec.ts: two-tab create → reply → resolve → panel
filter → delete, passing vs real partykit; presence suites + collab units
stay green; shared pointer bump (0004 model).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
2026-07-06 17:50:11 +02:00
|
|
|
}
|
|
|
|
|
|
feat(collab): dev-time presence style tuner (VITE_PRESENCE_TUNER=1)
Parametrizes every visual knob of the presence overlay so we can pick the
shipped look live, then wire the winners into the defaults:
- collab_presence_style.h: shared STYLE struct + drawing (now used by BOTH
editor TUs — no more duplicated overlay code): selection shape (rect /
corner brackets / underline / rounded rect / filled-only), border width +
alpha, infill alpha, padding, corner radius; name tag show/size/chip-
background/inside-outside/top-bottom/start-end-center/offset; cursor shape
(cross / pointer / circle+dot), size/width/alpha + label knobs; fixed-color
and palette-by-name-hash overrides (try palettes without changing what
senders publish); pin radius/ring/alphas. Defaults == shipped look.
- kicadCollabSetStyle(json) live-patch export + kicadCollabTestListItems(n)
(real KIIDs for synthetic previews); merged dispatch; pins now carry the
author name so palette overrides recolor them consistently.
- PresenceTuner.tsx: floating dev panel (env-gated, tree-shaken otherwise) —
grouped sliders/selects, demo peers+pins injection for SOLO tuning,
localStorage persistence across reloads, Copy JSON export, reset.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
2026-07-06 19:10:40 +02:00
|
|
|
// JS → C++ (presence tuner): live-patch the overlay STYLE and repaint —
|
|
|
|
|
// see collab_presence_style.h + pcbnew's counterpart.
|
|
|
|
|
void schCollabSetStyle( std::string aJson )
|
|
|
|
|
{
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
presenceCore().setStyle( aJson );
|
feat(collab): dev-time presence style tuner (VITE_PRESENCE_TUNER=1)
Parametrizes every visual knob of the presence overlay so we can pick the
shipped look live, then wire the winners into the defaults:
- collab_presence_style.h: shared STYLE struct + drawing (now used by BOTH
editor TUs — no more duplicated overlay code): selection shape (rect /
corner brackets / underline / rounded rect / filled-only), border width +
alpha, infill alpha, padding, corner radius; name tag show/size/chip-
background/inside-outside/top-bottom/start-end-center/offset; cursor shape
(cross / pointer / circle+dot), size/width/alpha + label knobs; fixed-color
and palette-by-name-hash overrides (try palettes without changing what
senders publish); pin radius/ring/alphas. Defaults == shipped look.
- kicadCollabSetStyle(json) live-patch export + kicadCollabTestListItems(n)
(real KIIDs for synthetic previews); merged dispatch; pins now carry the
author name so palette overrides recolor them consistently.
- PresenceTuner.tsx: floating dev panel (env-gated, tree-shaken otherwise) —
grouped sliders/selects, demo peers+pins injection for SOLO tuning,
localStorage persistence across reloads, Copy JSON export, reset.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
2026-07-06 19:10:40 +02:00
|
|
|
}
|
|
|
|
|
|
comments-ux: figma bubble pins, floating panel, seen/reactions/mentions UI, theme follow (0001 A–E + 0002)
- GAL pin = one closed polygon: round body, squared-off bottom-left corner
ON the anchor; PIN gains unread (accent ring); tuner knobs; shipped
defaults r9/ring4/alpha.9. DOM hit/highlight sized+offset from a LIVE
pin-geometry radius store the tuner feeds.
- Floating comments panel: draggable (shared useDraggablePanel with
always-onscreen restore; overlay FAB retrofitted), collapsible to header,
header carries add/show-hide/mark-all; unread badges (rose on mention).
- Reactions (emoji-mart lazy, quick-row) + @-mention autocomplete
(MentionInput; backend roster with presence/author fallback).
- Theme: ?theme= > storage > OS, no-flash boot, toggles (HomePage + overlay
View row), boot-seeded pcbjam-dark schematic colors + kicadSetColorTheme /
kicadSetDarkChrome bridges (canvas + wx chrome live flip), light/dark
variants across all overlay surfaces.
- e2e: panel/seen/reactions/mentions/theme specs + resize-spec geometry;
bumps pcbjam-shared (flat-key seen/reactions + listCollaborators) and
wxwidgets (dark chrome) pointers.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HLwn1toiNKi1MgxGKnZTes
2026-07-24 13:21:22 +02:00
|
|
|
// JS → C++ (comments-ux 0002 F4): live color-theme switch (see pcbjam_theme.h).
|
|
|
|
|
void schSetColorTheme( std::string aTheme )
|
|
|
|
|
{
|
|
|
|
|
pcbjam_theme::setColorTheme( schFrame(), aTheme );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Pre-main chrome appearance seed (called at onRuntimeInitialized).
|
|
|
|
|
void schSetDarkChrome( bool aDark )
|
|
|
|
|
{
|
|
|
|
|
pcbjam_theme::setDarkChromeFlag( aDark );
|
|
|
|
|
}
|
|
|
|
|
|
feat(collab): tuner round 2 — center-anchored labels, exact outlines, varied demo set, clearer color modes
- collab_presence_style.h: GAL BitmapText CENTERS on its position (confirmed
in GAL::ResetTextAttributes — the mispositioned nameplates); labels/chips
now hand GAL the block center. New selection shape 5 'exact outline':
pcbnew hugs real geometry (footprint bounding hull, TransformShapeToPolygon
for the rest, padding inflates the polygon); eeschema falls back to rect.
- kicadCollabTestDemoSet (both TUs + merged): labeled demo groups — smallest
+ largest footprint and the two busiest nets' segments (symbols + wire
bundles on sch) — so the style preview covers the real range of shapes.
- PresenceTuner: Colors section rebuilt as explicit modes (per-user / fixed /
palette) with preset palettes (default, pastel, vivid, okabe-ito), buffered
hex editing + Apply (the old always-filtering textarea ate keystrokes), an
'overlay only' hint; demo injection consumes the varied demo set; 'exact
outline (pcb)' in the shape list.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
2026-07-07 10:41:15 +02:00
|
|
|
// Tuner helper: a VARIED demo-selection set for the current sheet — smallest +
|
|
|
|
|
// largest symbol and two bundles of wires (net-ish), mirroring pcbnew's
|
|
|
|
|
// pcbCollabTestDemoSet so the style preview shows the range of shapes.
|
|
|
|
|
std::string schCollabTestDemoSet()
|
|
|
|
|
{
|
|
|
|
|
SCH_EDIT_FRAME* fr = schFrame();
|
|
|
|
|
|
|
|
|
|
json groups = json::array();
|
|
|
|
|
|
|
|
|
|
if( fr )
|
|
|
|
|
{
|
|
|
|
|
if( SCH_SCREEN* screen = currentScreen( fr ) )
|
|
|
|
|
{
|
|
|
|
|
SCH_SYMBOL* smallest = nullptr;
|
|
|
|
|
SCH_SYMBOL* largest = nullptr;
|
|
|
|
|
double minA = 0, maxA = 0;
|
|
|
|
|
std::vector<std::string> wires;
|
|
|
|
|
|
|
|
|
|
for( SCH_ITEM* item : screen->Items() )
|
|
|
|
|
{
|
|
|
|
|
if( item->Type() == SCH_SYMBOL_T )
|
|
|
|
|
{
|
|
|
|
|
BOX2I bb = item->GetBoundingBox();
|
|
|
|
|
double a = (double) bb.GetWidth() * bb.GetHeight();
|
|
|
|
|
|
|
|
|
|
auto* sym = static_cast<SCH_SYMBOL*>( item );
|
|
|
|
|
|
|
|
|
|
if( !smallest || a < minA ) { smallest = sym; minA = a; }
|
|
|
|
|
if( !largest || a > maxA ) { largest = sym; maxA = a; }
|
|
|
|
|
}
|
|
|
|
|
else if( item->Type() == SCH_LINE_T && wires.size() < 8 )
|
|
|
|
|
{
|
|
|
|
|
wires.push_back( toUtf8( item->m_Uuid.AsString() ) );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if( smallest )
|
|
|
|
|
groups.push_back( { { "label", "symbol small" },
|
|
|
|
|
{ "ids", { toUtf8( smallest->m_Uuid.AsString() ) } } } );
|
|
|
|
|
|
|
|
|
|
if( largest && largest != smallest )
|
|
|
|
|
groups.push_back( { { "label", "symbol large" },
|
|
|
|
|
{ "ids", { toUtf8( largest->m_Uuid.AsString() ) } } } );
|
|
|
|
|
|
|
|
|
|
if( wires.size() >= 2 )
|
|
|
|
|
{
|
|
|
|
|
size_t half = wires.size() / 2;
|
|
|
|
|
groups.push_back( { { "label", "wires A" },
|
|
|
|
|
{ "ids", std::vector<std::string>( wires.begin(),
|
|
|
|
|
wires.begin() + half ) } } );
|
|
|
|
|
groups.push_back( { { "label", "wires B" },
|
|
|
|
|
{ "ids", std::vector<std::string>( wires.begin() + half,
|
|
|
|
|
wires.end() ) } } );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return json{ { "groups", groups } }.dump();
|
|
|
|
|
}
|
|
|
|
|
|
feat(collab): dev-time presence style tuner (VITE_PRESENCE_TUNER=1)
Parametrizes every visual knob of the presence overlay so we can pick the
shipped look live, then wire the winners into the defaults:
- collab_presence_style.h: shared STYLE struct + drawing (now used by BOTH
editor TUs — no more duplicated overlay code): selection shape (rect /
corner brackets / underline / rounded rect / filled-only), border width +
alpha, infill alpha, padding, corner radius; name tag show/size/chip-
background/inside-outside/top-bottom/start-end-center/offset; cursor shape
(cross / pointer / circle+dot), size/width/alpha + label knobs; fixed-color
and palette-by-name-hash overrides (try palettes without changing what
senders publish); pin radius/ring/alphas. Defaults == shipped look.
- kicadCollabSetStyle(json) live-patch export + kicadCollabTestListItems(n)
(real KIIDs for synthetic previews); merged dispatch; pins now carry the
author name so palette overrides recolor them consistently.
- PresenceTuner.tsx: floating dev panel (env-gated, tree-shaken otherwise) —
grouped sliders/selects, demo peers+pins injection for SOLO tuning,
localStorage persistence across reloads, Copy JSON export, reset.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
2026-07-06 19:10:40 +02:00
|
|
|
// Test/tuner helper: the first N item uuids of the CURRENT sheet — real,
|
|
|
|
|
// resolvable KIIDs for synthetic remote-selection previews.
|
|
|
|
|
std::string schCollabTestListItems( int aCount )
|
|
|
|
|
{
|
|
|
|
|
SCH_EDIT_FRAME* fr = schFrame();
|
|
|
|
|
|
|
|
|
|
json out = json::array();
|
|
|
|
|
|
|
|
|
|
if( fr )
|
|
|
|
|
{
|
|
|
|
|
if( SCH_SCREEN* screen = currentScreen( fr ) )
|
|
|
|
|
{
|
|
|
|
|
for( SCH_ITEM* item : screen->Items() )
|
|
|
|
|
{
|
|
|
|
|
if( (int) out.size() >= aCount )
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
out.push_back( toUtf8( item->m_Uuid.AsString() ) );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return out.dump();
|
|
|
|
|
}
|
|
|
|
|
|
feat(comments): figma-like comment pins + threads (collab-presence 0005)
Hybrid pins: the wasm draws the dot (kicadCollabSetPins rides the presence
VIEW_OVERLAY, author color + white ring, drawn above selections; zero
kicad-fork changes), the DOM owns interaction —
- comments.ts: controller gluing the MIT kdoc_comments helpers to the editor:
anchor resolution per tool IU (pins track item moves via kdoc_items
observation), throttled pin snapshots, anchorAt nearest-item snap, jumpTo
via new kicadCollabSetViewport; rebinds per sheet like presence.
- CommentLayer.tsx: comment mode (click catcher + composer), pin hit targets
over the GAL dots, thread popover (reply/edit/delete own, resolve/reopen,
delete thread), panel with resolved filter + jump-to (popover centers when
the pin is off-screen). Resolved pins drop figma-style.
- WasmTool: controller lifecycle beside presence; live viewport feed;
window.__pcbjamComments test handle (threads persist in the room ydoc).
- e2e tests/web/comments.spec.ts: two-tab create → reply → resolve → panel
filter → delete, passing vs real partykit; presence suites + collab units
stay green; shared pointer bump (0004 model).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
2026-07-06 17:50:11 +02:00
|
|
|
// JS → C++ (0005): pan the view to a world position (comment panel "jump to pin").
|
|
|
|
|
void schCollabSetViewport( double aCx, double aCy )
|
|
|
|
|
{
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
presenceCore().panTo( aCx, aCy );
|
feat(comments): figma-like comment pins + threads (collab-presence 0005)
Hybrid pins: the wasm draws the dot (kicadCollabSetPins rides the presence
VIEW_OVERLAY, author color + white ring, drawn above selections; zero
kicad-fork changes), the DOM owns interaction —
- comments.ts: controller gluing the MIT kdoc_comments helpers to the editor:
anchor resolution per tool IU (pins track item moves via kdoc_items
observation), throttled pin snapshots, anchorAt nearest-item snap, jumpTo
via new kicadCollabSetViewport; rebinds per sheet like presence.
- CommentLayer.tsx: comment mode (click catcher + composer), pin hit targets
over the GAL dots, thread popover (reply/edit/delete own, resolve/reopen,
delete thread), panel with resolved filter + jump-to (popover centers when
the pin is off-screen). Resolved pins drop figma-style.
- WasmTool: controller lifecycle beside presence; live viewport feed;
window.__pcbjamComments test handle (threads persist in the room ydoc).
- e2e tests/web/comments.spec.ts: two-tab create → reply → resolve → panel
filter → delete, passing vs real partykit; presence suites + collab units
stay green; shared pointer bump (0004 model).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
2026-07-06 17:50:11 +02:00
|
|
|
}
|
|
|
|
|
|
feat(collab): follow-user (collab-presence 0008) + chip depth-layer fix
Follow-user: click a peer's roster avatar to mirror their viewport until
local input breaks it.
- collab_presence_core.h: CORE::fitViewport(cx, cy, halfW, halfH) — fit the
leader's world rect with CONTAIN semantics (zoom derived from the
follower's own canvas via the ToScreen ratio; GetScale is the zoom, not
px/IU). Exported as kicadCollabFitViewport from both editor TUs + the
merged dispatcher.
- presence-kicad.ts: publish the visible world rect (viewportRect) into
awareness, 100 ms trailing throttle; guarded for pre-0008 handles.
- follow-user.ts: createFollow — follows an awareness CLIENT (a tab, not a
user); applies leader rect changes via FitViewport, dedupes unchanged
republishes; break-on-interact compares local onViewport echoes against
the last applied rect (2% rel tolerance, echo-grace before the first fit
lands); unfollows on leader-left; pauses on eeschema sheet mismatch.
- PresenceRoster: avatars are follow toggles (ring on the followed peer);
WasmTool renders the "Following <name> — move to stop" banner.
- tests: 7 controller units (85/85 collab), fitViewport round-trip e2e in
both kicad presence specs (20/20), two-tab tests/web/follow.spec.ts
(converge → track → wheel-zoom breaks → subsequent moves ignored).
Chip depth-layer fix (user-reported): name chips washed out inside
low-alpha selection fills — chip rects shared the shapes overlay's single
depth, and same-depth fragments drawn LATER lose the depth test, so an
earlier-painted fill rejected the chip's pixels. Now three layers via the
fork's VIEW_OVERLAY::SetDepthOffset: text (0) < chips + pin dots (1) <
selection shapes (2). drawLabel/drawCursor/drawSelectionBox take the chip
overlay explicitly; comment-pin dots move to the chip layer too (the 0005
"drawn last so pins sit above" comment had the rule backwards). Verified
with a chip-inside-30%-fill pixel repro + the full presence suite.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-09 09:30:40 +02:00
|
|
|
// JS → C++ (0008 follow-user): fit a leader's world rect into this canvas.
|
|
|
|
|
void schCollabFitViewport( double aCx, double aCy, double aHalfW, double aHalfH )
|
|
|
|
|
{
|
|
|
|
|
presenceCore().fitViewport( aCx, aCy, aHalfW, aHalfH );
|
|
|
|
|
}
|
|
|
|
|
|
feat(collab): presence P3 — eeschema port (collab-presence 0003)
- eeschema_embind.cpp: presence section (0002 pattern, zero fork changes) —
wx canvas triggers + SCHEMATIC_LISTENER piggyback → post-settle
SCH_SELECTION_TOOL emit; throttled cursor; remote VIEW_OVERLAY render
(SCHEMATIC::ResolveItem, name tags, screen-constant via GAL matrix);
schCollab{PresenceStart,SetRemote,GetViewport,GetSelection,TestSelectFirst,
TestClearSelection}; kicad_editor_embind dispatches by active frame.
- sheet-manager: parked rooms carry SKELETON awareness states
({user,tool,sheetPath=bound sheet}) via publishSkeletons on switch + late
warm-up — the bound room's awareness then holds every project peer, so no
multi-room aggregation; presence.ts publishSkeleton helper.
- PresenceRoster: sheet-aware — peers on another sheet render dimmed with
'on <sheet>' tooltip; WasmTool un-gates the kicad presence bridge for
eeschema and threads activeSheetPath.
- tests: presence-eeschema.spec.ts (5 e2e, mirrors pcbnew incl. the px/IU
band at eeschema's 1e4/mm IU); presence.test.ts +2 (skeleton visibility,
no ghost cursor after rebind). eeschema-collab/subschema stay green.
Verified live: two tabs on demo.kicad_sch — peer cursor cross + label,
selection box + name tag, roster avatar.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
2026-07-06 17:05:15 +02:00
|
|
|
// JS pull of the current viewport transform: `{cx,cy,scale,w,h}` with scale = px per
|
|
|
|
|
// IU via the GAL matrix (GetScale() is the zoom, not px/IU — pcbnew 0002 lesson).
|
|
|
|
|
std::string schCollabGetViewport()
|
|
|
|
|
{
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
return presenceCore().viewportJson();
|
feat(collab): presence P3 — eeschema port (collab-presence 0003)
- eeschema_embind.cpp: presence section (0002 pattern, zero fork changes) —
wx canvas triggers + SCHEMATIC_LISTENER piggyback → post-settle
SCH_SELECTION_TOOL emit; throttled cursor; remote VIEW_OVERLAY render
(SCHEMATIC::ResolveItem, name tags, screen-constant via GAL matrix);
schCollab{PresenceStart,SetRemote,GetViewport,GetSelection,TestSelectFirst,
TestClearSelection}; kicad_editor_embind dispatches by active frame.
- sheet-manager: parked rooms carry SKELETON awareness states
({user,tool,sheetPath=bound sheet}) via publishSkeletons on switch + late
warm-up — the bound room's awareness then holds every project peer, so no
multi-room aggregation; presence.ts publishSkeleton helper.
- PresenceRoster: sheet-aware — peers on another sheet render dimmed with
'on <sheet>' tooltip; WasmTool un-gates the kicad presence bridge for
eeschema and threads activeSheetPath.
- tests: presence-eeschema.spec.ts (5 e2e, mirrors pcbnew incl. the px/IU
band at eeschema's 1e4/mm IU); presence.test.ts +2 (skeleton visibility,
no ghost cursor after rebind). eeschema-collab/subschema stay green.
Verified live: two tabs on demo.kicad_sch — peer cursor cross + label,
selection box + name tag, roster avatar.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
2026-07-06 17:05:15 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// JS pull of the CURRENT selection's uuids (presence seed + e2e no-leak probe).
|
|
|
|
|
std::string schCollabGetSelection()
|
|
|
|
|
{
|
|
|
|
|
SCH_EDIT_FRAME* fr = schFrame();
|
|
|
|
|
|
|
|
|
|
if( !fr )
|
|
|
|
|
return "[]";
|
|
|
|
|
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
return presenceCore().selectionUuids( fr ).dump();
|
feat(collab): presence P3 — eeschema port (collab-presence 0003)
- eeschema_embind.cpp: presence section (0002 pattern, zero fork changes) —
wx canvas triggers + SCHEMATIC_LISTENER piggyback → post-settle
SCH_SELECTION_TOOL emit; throttled cursor; remote VIEW_OVERLAY render
(SCHEMATIC::ResolveItem, name tags, screen-constant via GAL matrix);
schCollab{PresenceStart,SetRemote,GetViewport,GetSelection,TestSelectFirst,
TestClearSelection}; kicad_editor_embind dispatches by active frame.
- sheet-manager: parked rooms carry SKELETON awareness states
({user,tool,sheetPath=bound sheet}) via publishSkeletons on switch + late
warm-up — the bound room's awareness then holds every project peer, so no
multi-room aggregation; presence.ts publishSkeleton helper.
- PresenceRoster: sheet-aware — peers on another sheet render dimmed with
'on <sheet>' tooltip; WasmTool un-gates the kicad presence bridge for
eeschema and threads activeSheetPath.
- tests: presence-eeschema.spec.ts (5 e2e, mirrors pcbnew incl. the px/IU
band at eeschema's 1e4/mm IU); presence.test.ts +2 (skeleton visibility,
no ghost cursor after rebind). eeschema-collab/subschema stay green.
Verified live: two tabs on demo.kicad_sch — peer cursor cross + label,
selection box + name tag, roster avatar.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
2026-07-06 17:05:15 +02:00
|
|
|
}
|
|
|
|
|
|
feat(collab): cross-app selection — eeschema symbol ⇄ pcbnew footprint ghost highlight (collab-presence 0006)
Selecting a symbol in eeschema ghost-highlights the linked footprint(s) in
every pcbnew tab of the project, and vice versa — across users AND one
user's own two tabs. Native KIWAY cross-probe is inert in WASM (one frame
per page); this rides the presence layer instead.
- cross-app.ts: project-wide awareness-only room (presenceRoomId), publishes
full PresenceState at selection rate (cursor always null); peers() = other-
TOOL clients incl. own user's other tabs; window.__pcbjamCrossApp test handle
- presence-kicad.ts: parseSelectionEmit (bare array | {uuids,fpPaths}),
xselFromPeerState (pcbnew paths → symbol uuids; eeschema uuids verbatim),
cross peers appended to the kicadCollabSetRemote snapshot as
{id "<user>#x<client>", name "<user> · sch|pcb", xsel}
- C++ (zero fork changes): pcbnew emits {uuids, fpPaths} (FOOTPRINT::GetPath)
and ghost-renders xsel via path-tail suffix scan; eeschema resolves xsel via
ResolveItem gated to the CURRENT sheet (xsel arrives project-wide, unlike
room-scoped selections); ghostStyle = alphas × xselAlphaScale (0.55, tuner-
patchable); new exports kicadCollabGetSelectionFull / TestGetCrossMapped /
TestSelectComponent (skips power symbols — PWR_FLAG has no footprint) +
merged-image dispatch
- tests: presence suites extended (payload shape, ghost render pixel tests,
13/13) + new two-tab tests/web/cross-probe.spec.ts (passing vs real
partykit); eeschema pixel compares now target the #glcanvas-* GAL panel
(the whole-window #canvas compare flaked on the auto-dismissing version
infobar — also fixes the long-known presence-eeschema restore flake);
cross-app + presence-kicad vitest suites
Spec: docs/features/collab-presence/0006 (closed repo).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lg5jwWuhFH5dL8hEcBDuP2
2026-07-07 19:30:18 +02:00
|
|
|
// JS pull of the current selection in the 0006 payload shape. eeschema has no
|
|
|
|
|
// footprint paths — the uuids ARE the symbol uuids — but the export keeps the
|
|
|
|
|
// merged image's kicadCollabGetSelectionFull contract uniform across editors.
|
|
|
|
|
std::string schCollabGetSelectionFull()
|
|
|
|
|
{
|
|
|
|
|
SCH_EDIT_FRAME* fr = schFrame();
|
|
|
|
|
|
|
|
|
|
if( !fr )
|
|
|
|
|
return "{\"uuids\":[],\"fpPaths\":[]}";
|
|
|
|
|
|
|
|
|
|
json payload;
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
payload["uuids"] = presenceCore().selectionUuids( fr );
|
feat(collab): cross-app selection — eeschema symbol ⇄ pcbnew footprint ghost highlight (collab-presence 0006)
Selecting a symbol in eeschema ghost-highlights the linked footprint(s) in
every pcbnew tab of the project, and vice versa — across users AND one
user's own two tabs. Native KIWAY cross-probe is inert in WASM (one frame
per page); this rides the presence layer instead.
- cross-app.ts: project-wide awareness-only room (presenceRoomId), publishes
full PresenceState at selection rate (cursor always null); peers() = other-
TOOL clients incl. own user's other tabs; window.__pcbjamCrossApp test handle
- presence-kicad.ts: parseSelectionEmit (bare array | {uuids,fpPaths}),
xselFromPeerState (pcbnew paths → symbol uuids; eeschema uuids verbatim),
cross peers appended to the kicadCollabSetRemote snapshot as
{id "<user>#x<client>", name "<user> · sch|pcb", xsel}
- C++ (zero fork changes): pcbnew emits {uuids, fpPaths} (FOOTPRINT::GetPath)
and ghost-renders xsel via path-tail suffix scan; eeschema resolves xsel via
ResolveItem gated to the CURRENT sheet (xsel arrives project-wide, unlike
room-scoped selections); ghostStyle = alphas × xselAlphaScale (0.55, tuner-
patchable); new exports kicadCollabGetSelectionFull / TestGetCrossMapped /
TestSelectComponent (skips power symbols — PWR_FLAG has no footprint) +
merged-image dispatch
- tests: presence suites extended (payload shape, ghost render pixel tests,
13/13) + new two-tab tests/web/cross-probe.spec.ts (passing vs real
partykit); eeschema pixel compares now target the #glcanvas-* GAL panel
(the whole-window #canvas compare flaked on the auto-dismissing version
infobar — also fixes the long-known presence-eeschema restore flake);
cross-app + presence-kicad vitest suites
Spec: docs/features/collab-presence/0006 (closed repo).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lg5jwWuhFH5dL8hEcBDuP2
2026-07-07 19:30:18 +02:00
|
|
|
payload["fpPaths"] = json::array();
|
|
|
|
|
return payload.dump();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Test probe (0006): the schematic-item uuids the current peers' cross-app
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
// selections resolve to ON THE CURRENT SHEET — same resolveXsel the render uses.
|
feat(collab): cross-app selection — eeschema symbol ⇄ pcbnew footprint ghost highlight (collab-presence 0006)
Selecting a symbol in eeschema ghost-highlights the linked footprint(s) in
every pcbnew tab of the project, and vice versa — across users AND one
user's own two tabs. Native KIWAY cross-probe is inert in WASM (one frame
per page); this rides the presence layer instead.
- cross-app.ts: project-wide awareness-only room (presenceRoomId), publishes
full PresenceState at selection rate (cursor always null); peers() = other-
TOOL clients incl. own user's other tabs; window.__pcbjamCrossApp test handle
- presence-kicad.ts: parseSelectionEmit (bare array | {uuids,fpPaths}),
xselFromPeerState (pcbnew paths → symbol uuids; eeschema uuids verbatim),
cross peers appended to the kicadCollabSetRemote snapshot as
{id "<user>#x<client>", name "<user> · sch|pcb", xsel}
- C++ (zero fork changes): pcbnew emits {uuids, fpPaths} (FOOTPRINT::GetPath)
and ghost-renders xsel via path-tail suffix scan; eeschema resolves xsel via
ResolveItem gated to the CURRENT sheet (xsel arrives project-wide, unlike
room-scoped selections); ghostStyle = alphas × xselAlphaScale (0.55, tuner-
patchable); new exports kicadCollabGetSelectionFull / TestGetCrossMapped /
TestSelectComponent (skips power symbols — PWR_FLAG has no footprint) +
merged-image dispatch
- tests: presence suites extended (payload shape, ghost render pixel tests,
13/13) + new two-tab tests/web/cross-probe.spec.ts (passing vs real
partykit); eeschema pixel compares now target the #glcanvas-* GAL panel
(the whole-window #canvas compare flaked on the auto-dismissing version
infobar — also fixes the long-known presence-eeschema restore flake);
cross-app + presence-kicad vitest suites
Spec: docs/features/collab-presence/0006 (closed repo).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lg5jwWuhFH5dL8hEcBDuP2
2026-07-07 19:30:18 +02:00
|
|
|
std::string schCollabTestGetCrossMapped()
|
|
|
|
|
{
|
|
|
|
|
json arr = json::array();
|
|
|
|
|
|
|
|
|
|
SCH_EDIT_FRAME* fr = schFrame();
|
|
|
|
|
|
|
|
|
|
if( !fr )
|
|
|
|
|
return arr.dump();
|
|
|
|
|
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
for( const pcbjam_presence::PEER& peer : presenceCore().peers )
|
feat(collab): cross-app selection — eeschema symbol ⇄ pcbnew footprint ghost highlight (collab-presence 0006)
Selecting a symbol in eeschema ghost-highlights the linked footprint(s) in
every pcbnew tab of the project, and vice versa — across users AND one
user's own two tabs. Native KIWAY cross-probe is inert in WASM (one frame
per page); this rides the presence layer instead.
- cross-app.ts: project-wide awareness-only room (presenceRoomId), publishes
full PresenceState at selection rate (cursor always null); peers() = other-
TOOL clients incl. own user's other tabs; window.__pcbjamCrossApp test handle
- presence-kicad.ts: parseSelectionEmit (bare array | {uuids,fpPaths}),
xselFromPeerState (pcbnew paths → symbol uuids; eeschema uuids verbatim),
cross peers appended to the kicadCollabSetRemote snapshot as
{id "<user>#x<client>", name "<user> · sch|pcb", xsel}
- C++ (zero fork changes): pcbnew emits {uuids, fpPaths} (FOOTPRINT::GetPath)
and ghost-renders xsel via path-tail suffix scan; eeschema resolves xsel via
ResolveItem gated to the CURRENT sheet (xsel arrives project-wide, unlike
room-scoped selections); ghostStyle = alphas × xselAlphaScale (0.55, tuner-
patchable); new exports kicadCollabGetSelectionFull / TestGetCrossMapped /
TestSelectComponent (skips power symbols — PWR_FLAG has no footprint) +
merged-image dispatch
- tests: presence suites extended (payload shape, ghost render pixel tests,
13/13) + new two-tab tests/web/cross-probe.spec.ts (passing vs real
partykit); eeschema pixel compares now target the #glcanvas-* GAL panel
(the whole-window #canvas compare flaked on the auto-dismissing version
infobar — also fixes the long-known presence-eeschema restore flake);
cross-app + presence-kicad vitest suites
Spec: docs/features/collab-presence/0006 (closed repo).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lg5jwWuhFH5dL8hEcBDuP2
2026-07-07 19:30:18 +02:00
|
|
|
{
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
for( SCH_ITEM* item : resolveXsel( fr, peer ) )
|
|
|
|
|
arr.push_back( toUtf8( item->m_Uuid.AsString() ) );
|
feat(collab): cross-app selection — eeschema symbol ⇄ pcbnew footprint ghost highlight (collab-presence 0006)
Selecting a symbol in eeschema ghost-highlights the linked footprint(s) in
every pcbnew tab of the project, and vice versa — across users AND one
user's own two tabs. Native KIWAY cross-probe is inert in WASM (one frame
per page); this rides the presence layer instead.
- cross-app.ts: project-wide awareness-only room (presenceRoomId), publishes
full PresenceState at selection rate (cursor always null); peers() = other-
TOOL clients incl. own user's other tabs; window.__pcbjamCrossApp test handle
- presence-kicad.ts: parseSelectionEmit (bare array | {uuids,fpPaths}),
xselFromPeerState (pcbnew paths → symbol uuids; eeschema uuids verbatim),
cross peers appended to the kicadCollabSetRemote snapshot as
{id "<user>#x<client>", name "<user> · sch|pcb", xsel}
- C++ (zero fork changes): pcbnew emits {uuids, fpPaths} (FOOTPRINT::GetPath)
and ghost-renders xsel via path-tail suffix scan; eeschema resolves xsel via
ResolveItem gated to the CURRENT sheet (xsel arrives project-wide, unlike
room-scoped selections); ghostStyle = alphas × xselAlphaScale (0.55, tuner-
patchable); new exports kicadCollabGetSelectionFull / TestGetCrossMapped /
TestSelectComponent (skips power symbols — PWR_FLAG has no footprint) +
merged-image dispatch
- tests: presence suites extended (payload shape, ghost render pixel tests,
13/13) + new two-tab tests/web/cross-probe.spec.ts (passing vs real
partykit); eeschema pixel compares now target the #glcanvas-* GAL panel
(the whole-window #canvas compare flaked on the auto-dismissing version
infobar — also fixes the long-known presence-eeschema restore flake);
cross-app + presence-kicad vitest suites
Spec: docs/features/collab-presence/0006 (closed repo).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lg5jwWuhFH5dL8hEcBDuP2
2026-07-07 19:30:18 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return arr.dump();
|
|
|
|
|
}
|
|
|
|
|
|
feat(collab): presence P3 — eeschema port (collab-presence 0003)
- eeschema_embind.cpp: presence section (0002 pattern, zero fork changes) —
wx canvas triggers + SCHEMATIC_LISTENER piggyback → post-settle
SCH_SELECTION_TOOL emit; throttled cursor; remote VIEW_OVERLAY render
(SCHEMATIC::ResolveItem, name tags, screen-constant via GAL matrix);
schCollab{PresenceStart,SetRemote,GetViewport,GetSelection,TestSelectFirst,
TestClearSelection}; kicad_editor_embind dispatches by active frame.
- sheet-manager: parked rooms carry SKELETON awareness states
({user,tool,sheetPath=bound sheet}) via publishSkeletons on switch + late
warm-up — the bound room's awareness then holds every project peer, so no
multi-room aggregation; presence.ts publishSkeleton helper.
- PresenceRoster: sheet-aware — peers on another sheet render dimmed with
'on <sheet>' tooltip; WasmTool un-gates the kicad presence bridge for
eeschema and threads activeSheetPath.
- tests: presence-eeschema.spec.ts (5 e2e, mirrors pcbnew incl. the px/IU
band at eeschema's 1e4/mm IU); presence.test.ts +2 (skeleton visibility,
no ghost cursor after rebind). eeschema-collab/subschema stay green.
Verified live: two tabs on demo.kicad_sch — peer cursor cross + label,
selection box + name tag, roster avatar.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
2026-07-06 17:05:15 +02:00
|
|
|
// Test helper: REALLY select the current sheet's first item through the selection
|
|
|
|
|
// tool, then run the presence check (programmatic selects close no canvas event).
|
|
|
|
|
std::string schCollabTestSelectFirst()
|
|
|
|
|
{
|
|
|
|
|
SCH_EDIT_FRAME* fr = schFrame();
|
|
|
|
|
|
|
|
|
|
if( !fr )
|
|
|
|
|
return "";
|
|
|
|
|
|
|
|
|
|
SCH_SCREEN* screen = currentScreen( fr );
|
|
|
|
|
|
|
|
|
|
if( !screen )
|
|
|
|
|
return "";
|
|
|
|
|
|
|
|
|
|
SCH_ITEM* target = nullptr;
|
|
|
|
|
|
|
|
|
|
for( SCH_ITEM* item : screen->Items() )
|
|
|
|
|
{
|
|
|
|
|
target = item;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if( !target )
|
|
|
|
|
return "";
|
|
|
|
|
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
presenceCore().selectItem( target );
|
feat(collab): presence P3 — eeschema port (collab-presence 0003)
- eeschema_embind.cpp: presence section (0002 pattern, zero fork changes) —
wx canvas triggers + SCHEMATIC_LISTENER piggyback → post-settle
SCH_SELECTION_TOOL emit; throttled cursor; remote VIEW_OVERLAY render
(SCHEMATIC::ResolveItem, name tags, screen-constant via GAL matrix);
schCollab{PresenceStart,SetRemote,GetViewport,GetSelection,TestSelectFirst,
TestClearSelection}; kicad_editor_embind dispatches by active frame.
- sheet-manager: parked rooms carry SKELETON awareness states
({user,tool,sheetPath=bound sheet}) via publishSkeletons on switch + late
warm-up — the bound room's awareness then holds every project peer, so no
multi-room aggregation; presence.ts publishSkeleton helper.
- PresenceRoster: sheet-aware — peers on another sheet render dimmed with
'on <sheet>' tooltip; WasmTool un-gates the kicad presence bridge for
eeschema and threads activeSheetPath.
- tests: presence-eeschema.spec.ts (5 e2e, mirrors pcbnew incl. the px/IU
band at eeschema's 1e4/mm IU); presence.test.ts +2 (skeleton visibility,
no ghost cursor after rebind). eeschema-collab/subschema stay green.
Verified live: two tabs on demo.kicad_sch — peer cursor cross + label,
selection box + name tag, roster avatar.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
2026-07-06 17:05:15 +02:00
|
|
|
|
|
|
|
|
return toUtf8( target->m_Uuid.AsString() );
|
|
|
|
|
}
|
|
|
|
|
|
feat(collab): cross-app selection — eeschema symbol ⇄ pcbnew footprint ghost highlight (collab-presence 0006)
Selecting a symbol in eeschema ghost-highlights the linked footprint(s) in
every pcbnew tab of the project, and vice versa — across users AND one
user's own two tabs. Native KIWAY cross-probe is inert in WASM (one frame
per page); this rides the presence layer instead.
- cross-app.ts: project-wide awareness-only room (presenceRoomId), publishes
full PresenceState at selection rate (cursor always null); peers() = other-
TOOL clients incl. own user's other tabs; window.__pcbjamCrossApp test handle
- presence-kicad.ts: parseSelectionEmit (bare array | {uuids,fpPaths}),
xselFromPeerState (pcbnew paths → symbol uuids; eeschema uuids verbatim),
cross peers appended to the kicadCollabSetRemote snapshot as
{id "<user>#x<client>", name "<user> · sch|pcb", xsel}
- C++ (zero fork changes): pcbnew emits {uuids, fpPaths} (FOOTPRINT::GetPath)
and ghost-renders xsel via path-tail suffix scan; eeschema resolves xsel via
ResolveItem gated to the CURRENT sheet (xsel arrives project-wide, unlike
room-scoped selections); ghostStyle = alphas × xselAlphaScale (0.55, tuner-
patchable); new exports kicadCollabGetSelectionFull / TestGetCrossMapped /
TestSelectComponent (skips power symbols — PWR_FLAG has no footprint) +
merged-image dispatch
- tests: presence suites extended (payload shape, ghost render pixel tests,
13/13) + new two-tab tests/web/cross-probe.spec.ts (passing vs real
partykit); eeschema pixel compares now target the #glcanvas-* GAL panel
(the whole-window #canvas compare flaked on the auto-dismissing version
infobar — also fixes the long-known presence-eeschema restore flake);
cross-app + presence-kicad vitest suites
Spec: docs/features/collab-presence/0006 (closed repo).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lg5jwWuhFH5dL8hEcBDuP2
2026-07-07 19:30:18 +02:00
|
|
|
// Test helper (0006): REALLY select the first SYMBOL on the current sheet —
|
|
|
|
|
// the deterministic cross-app subject (TestSelectFirst may pick a wire, which
|
|
|
|
|
// legitimately maps to nothing in pcbnew). Returns the uuid, "" without one.
|
|
|
|
|
std::string schCollabTestSelectComponent()
|
|
|
|
|
{
|
|
|
|
|
SCH_EDIT_FRAME* fr = schFrame();
|
|
|
|
|
|
|
|
|
|
if( !fr )
|
|
|
|
|
return "";
|
|
|
|
|
|
|
|
|
|
SCH_SCREEN* screen = currentScreen( fr );
|
|
|
|
|
|
|
|
|
|
if( !screen )
|
|
|
|
|
return "";
|
|
|
|
|
|
|
|
|
|
SCH_ITEM* target = nullptr;
|
|
|
|
|
|
|
|
|
|
for( SCH_ITEM* item : screen->Items().OfType( SCH_SYMBOL_T ) )
|
|
|
|
|
{
|
|
|
|
|
// Power symbols (PWR_FLAG, GND, …) legitimately have no footprint —
|
|
|
|
|
// they'd make the cross-app subject map to nothing by construction.
|
|
|
|
|
if( static_cast<SCH_SYMBOL*>( item )->IsPower() )
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
target = item;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if( !target )
|
|
|
|
|
return "";
|
|
|
|
|
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
presenceCore().selectItem( target );
|
feat(collab): cross-app selection — eeschema symbol ⇄ pcbnew footprint ghost highlight (collab-presence 0006)
Selecting a symbol in eeschema ghost-highlights the linked footprint(s) in
every pcbnew tab of the project, and vice versa — across users AND one
user's own two tabs. Native KIWAY cross-probe is inert in WASM (one frame
per page); this rides the presence layer instead.
- cross-app.ts: project-wide awareness-only room (presenceRoomId), publishes
full PresenceState at selection rate (cursor always null); peers() = other-
TOOL clients incl. own user's other tabs; window.__pcbjamCrossApp test handle
- presence-kicad.ts: parseSelectionEmit (bare array | {uuids,fpPaths}),
xselFromPeerState (pcbnew paths → symbol uuids; eeschema uuids verbatim),
cross peers appended to the kicadCollabSetRemote snapshot as
{id "<user>#x<client>", name "<user> · sch|pcb", xsel}
- C++ (zero fork changes): pcbnew emits {uuids, fpPaths} (FOOTPRINT::GetPath)
and ghost-renders xsel via path-tail suffix scan; eeschema resolves xsel via
ResolveItem gated to the CURRENT sheet (xsel arrives project-wide, unlike
room-scoped selections); ghostStyle = alphas × xselAlphaScale (0.55, tuner-
patchable); new exports kicadCollabGetSelectionFull / TestGetCrossMapped /
TestSelectComponent (skips power symbols — PWR_FLAG has no footprint) +
merged-image dispatch
- tests: presence suites extended (payload shape, ghost render pixel tests,
13/13) + new two-tab tests/web/cross-probe.spec.ts (passing vs real
partykit); eeschema pixel compares now target the #glcanvas-* GAL panel
(the whole-window #canvas compare flaked on the auto-dismissing version
infobar — also fixes the long-known presence-eeschema restore flake);
cross-app + presence-kicad vitest suites
Spec: docs/features/collab-presence/0006 (closed repo).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lg5jwWuhFH5dL8hEcBDuP2
2026-07-07 19:30:18 +02:00
|
|
|
|
|
|
|
|
return toUtf8( target->m_Uuid.AsString() );
|
|
|
|
|
}
|
|
|
|
|
|
e2e/CI: dual-engine suites, per-engine screenshots, SwiftShader retired, prod web suite, CI-coverage gate
Squash of experiment/ff-big-modules vs main.
Big-module routing removed: native-EH shrank kicad_editor below
SpiderMonkey's x86-64 code budget (runs 29355049705/29356152413 green on
stock Firefox), so BIG_MODULE_SPECS routing and the baseline-only-JIT
crutch are gone — kicad-firefox and kicad-chromium both run the full
suite, with the module compiled the way real users' browsers compile it.
Per-engine screenshots end to end: stableShot/shotPath write
test-results/<engine>/<name>.png; baselines move to
baseline-screenshots/{chromium,firefox}/ and the whole tools/screenshots
pipeline (compare/promote/manifest/spec-map/changelog/Discord) keys on
<engine>/<name>. Previously Firefox and Chromium renders of one spec
overwrote each other and Firefox renders were never actually gated.
Seeded from CI run 29421380806 (92 new firefox baselines, +24 chromium
web-suite shots); manifest generated from the baseline tree.
One merged playwright.config.ts (kicad/asyncify/coroutine/perf as
projects); ~25 dead npm scripts dropped. The web suite is gated in CI for
the first time ever (4 rotted specs fixed, 5 broken lib-bridge specs
triaged as fixme in docs/features/web-e2e-rot/); cheap lint step after
npm ci; last 26 blind-sleep violations fixed.
SwiftShader retired: CI Chromium renders WebGL on ANGLE → Mesa llvmpipe
(--use-gl=angle --use-angle=gl --ignore-gpu-blocklist; the blocklist flag
is mandatory — llvmpipe is blocklisted and WebGL is silently unavailable
without it) in BOTH configs. Under WORKERS=4 congestion SwiftShader
transiently failed the first post-board-load draw and the recovery
cascade ended in a silent permanent Cairo fallback — that engine flip was
the "~1.2% changedRatio both directions" occ-export baseline flake.
Validated 160/160 across two 80-repeat rigs; full analysis in
docs/features/wx-parity-bugs/occ-export-context-eviction.md. Chromium
baselines shift slightly on llvmpipe — promote once from the first green
run. Deflakes the new coverage exposed: presence baselines settle before
capture; presence fixtures declare current file formats; perf gets its
own outputDir so CI evidence survives; occ-export settles the board paint
before the export dialog; menu-item waits (waitForRenderedByLabel before
clickMenuItem) in 4 specs + the TESTING.md rule.
Web suite runs the PROD build, in parallel: webServer becomes backend
`start` + the standalone's e2e:preview (build-preview.mjs: link-wasm →
stash the public/wasm symlink aside during vite build, build-demo.mjs's
move — then vite preview as the persistent server). The wasm middleware
serves /wasm/* in preview and emits COOP/COEP/CORP itself (a pthread
worker script's own response must carry COEP or Chrome kills it with
ERR_BLOCKED_BY_RESPONSE). VITE_* flags bake at build time;
VITE_ALLOW_USER_OVERRIDE joins turbo globalEnv. fullyParallel + default
workers: 5.2m → 1.4m. Determinism fixes the parallel run exposed:
shared-page specs become serial groups; locks.spec grabs alice's exact
item via the new kicadCollabTestSelectByUuid hook (cross-tab "first
footprint" order is not a ysync invariant); quit specs poll page.url()
(quit supersedes its own navigation — NS_BINDING_ABORTED on Firefox).
Suite: 51 passed / 12 skipped / 0 failed in 1.6m.
CI-coverage gate (lint:ci-coverage): every tests/**/*.spec.ts must be
reachable from the npm scripts the workflows invoke — scraped from
.github/workflows/, resolved through package.json, coverage asked from
playwright --list itself. Rules: uncovered-spec + orphan-project (with a
documented LOCAL_ONLY_PROJECTS allowlist). Gating next to
lint:determinism; 138 spec files / 13 projects accounted for.
Product fixes kept from the investigations (reachable on real GPUs too):
wx 7799fd1be5 — paint flags clear before dispatch + Invalidate always
propagates; kicad 3dcfea5e45 — SwiftShader pass-boundary flush +
per-instance font texture + first-frame GL-error drain (GAL recovery
recovers instead of falling back to Cairo) + the user-facing eeschema
switch navigates again under __EMSCRIPTEN__ (project-sync's
FaceRegistered gate had rerouted it into the hidden sync player; caught
by the newly-gated web suite).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018eUxiPApHgGiu9NFyQfhAq
2026-07-17 12:10:40 +02:00
|
|
|
// Test helper (0007): select a SPECIFIC item by uuid — see pcbnew_embind.cpp
|
|
|
|
|
// for why the tiebreak specs must not rely on cross-tab iteration order.
|
|
|
|
|
bool schCollabTestSelectByUuid( std::string aUuid )
|
|
|
|
|
{
|
|
|
|
|
SCH_EDIT_FRAME* fr = schFrame();
|
|
|
|
|
|
|
|
|
|
if( !fr )
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
SCH_SCREEN* screen = currentScreen( fr );
|
|
|
|
|
|
|
|
|
|
if( !screen )
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
const wxString want = wxString::FromUTF8( aUuid.c_str() );
|
|
|
|
|
|
|
|
|
|
for( SCH_ITEM* item : screen->Items() )
|
|
|
|
|
{
|
|
|
|
|
if( item->m_Uuid.AsString() == want )
|
|
|
|
|
{
|
|
|
|
|
presenceCore().selectItem( item );
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
feat(collab): selection soft-locks — remote-selected items can't be dragged locally (collab-presence 0007)
While a peer has an item selected, local users can still select it for
inspection but move/drag/rotate/delete skip it with an infobar naming the
holder (native locked-item UX; enforced via the fork's PCBJAM_REMOTE_LOCK
query — kicad 81f9cd80fd, the epic's first fork-touching phase). Overlapping
holds (both grabbed inside the awareness propagation window) tie-break
deterministically: lowest (user.id, clientID) keeps the item, every losing
client auto-releases it.
- lock-tiebreak.ts: pure policy — beats(), remoteLocks() (union of ALL other
clients' selections incl. own user's other tabs, minus own-held-and-winning
uuids so the winner isn't blocked mid-release), contestedReleases()
- presence.ts: clients() (per-client view, no user dedupe) + self(); FIX for
a pre-existing flaky stack overflow — resolveCollision re-entered itself
synchronously via its own patch's awareness 'change' and could ping-pong on
stale same-user states (~1-in-3 unit runs); re-entrancy guard defers
re-resolution to the next genuine delivery
- presence-kicad.ts: locks ride the kicadCollabSetRemote snapshot
(`locks:[{uuid,name}]`); losing overlaps call kicadCollabReleaseSelection
- wasm bindings (both TUs + merged dispatch): g_locks map + fork query
install; kicadCollabReleaseSelection (cancelInteractive only when a tool
stack is live — bare ESC would clear the whole selection — then selective
RemoveItemFromSel + infobar + forced re-emit); kicadCollabTestGetLocked
- tests: lock-tiebreak unit suite; presence-locks e2e for both editors (real
move veto — pcbnew click+M hotkey since its default left-drag is
rubber-band select, eeschema real drag — each with an unlocked control);
two-tab tests/web/locks.spec.ts (lock propagation + deterministic tiebreak
release + unlock on clear, passing vs real partykit)
Spec: docs/features/collab-presence/0007 (closed repo).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lg5jwWuhFH5dL8hEcBDuP2
2026-07-07 20:35:26 +02:00
|
|
|
// JS → C++ (0007): tiebreak release — see pcbnew_embind.cpp for the design
|
|
|
|
|
// (cancel-interactive-if-a-tool-holds-them → selective unselect → infobar →
|
|
|
|
|
// forced re-emit).
|
|
|
|
|
void schCollabReleaseSelection( std::string aUuidsJson, std::string aHolder )
|
|
|
|
|
{
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
presenceCore().releaseSelection( aUuidsJson, aHolder );
|
feat(collab): selection soft-locks — remote-selected items can't be dragged locally (collab-presence 0007)
While a peer has an item selected, local users can still select it for
inspection but move/drag/rotate/delete skip it with an infobar naming the
holder (native locked-item UX; enforced via the fork's PCBJAM_REMOTE_LOCK
query — kicad 81f9cd80fd, the epic's first fork-touching phase). Overlapping
holds (both grabbed inside the awareness propagation window) tie-break
deterministically: lowest (user.id, clientID) keeps the item, every losing
client auto-releases it.
- lock-tiebreak.ts: pure policy — beats(), remoteLocks() (union of ALL other
clients' selections incl. own user's other tabs, minus own-held-and-winning
uuids so the winner isn't blocked mid-release), contestedReleases()
- presence.ts: clients() (per-client view, no user dedupe) + self(); FIX for
a pre-existing flaky stack overflow — resolveCollision re-entered itself
synchronously via its own patch's awareness 'change' and could ping-pong on
stale same-user states (~1-in-3 unit runs); re-entrancy guard defers
re-resolution to the next genuine delivery
- presence-kicad.ts: locks ride the kicadCollabSetRemote snapshot
(`locks:[{uuid,name}]`); losing overlaps call kicadCollabReleaseSelection
- wasm bindings (both TUs + merged dispatch): g_locks map + fork query
install; kicadCollabReleaseSelection (cancelInteractive only when a tool
stack is live — bare ESC would clear the whole selection — then selective
RemoveItemFromSel + infobar + forced re-emit); kicadCollabTestGetLocked
- tests: lock-tiebreak unit suite; presence-locks e2e for both editors (real
move veto — pcbnew click+M hotkey since its default left-drag is
rubber-band select, eeschema real drag — each with an unlocked control);
two-tab tests/web/locks.spec.ts (lock propagation + deterministic tiebreak
release + unlock on clear, passing vs real partykit)
Spec: docs/features/collab-presence/0007 (closed repo).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lg5jwWuhFH5dL8hEcBDuP2
2026-07-07 20:35:26 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Test probe (0007): the current remote soft-lock set as `[{uuid, name}]`.
|
|
|
|
|
std::string schCollabTestGetLocked()
|
|
|
|
|
{
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
return presenceCore().locksJson();
|
feat(collab): selection soft-locks — remote-selected items can't be dragged locally (collab-presence 0007)
While a peer has an item selected, local users can still select it for
inspection but move/drag/rotate/delete skip it with an infobar naming the
holder (native locked-item UX; enforced via the fork's PCBJAM_REMOTE_LOCK
query — kicad 81f9cd80fd, the epic's first fork-touching phase). Overlapping
holds (both grabbed inside the awareness propagation window) tie-break
deterministically: lowest (user.id, clientID) keeps the item, every losing
client auto-releases it.
- lock-tiebreak.ts: pure policy — beats(), remoteLocks() (union of ALL other
clients' selections incl. own user's other tabs, minus own-held-and-winning
uuids so the winner isn't blocked mid-release), contestedReleases()
- presence.ts: clients() (per-client view, no user dedupe) + self(); FIX for
a pre-existing flaky stack overflow — resolveCollision re-entered itself
synchronously via its own patch's awareness 'change' and could ping-pong on
stale same-user states (~1-in-3 unit runs); re-entrancy guard defers
re-resolution to the next genuine delivery
- presence-kicad.ts: locks ride the kicadCollabSetRemote snapshot
(`locks:[{uuid,name}]`); losing overlaps call kicadCollabReleaseSelection
- wasm bindings (both TUs + merged dispatch): g_locks map + fork query
install; kicadCollabReleaseSelection (cancelInteractive only when a tool
stack is live — bare ESC would clear the whole selection — then selective
RemoveItemFromSel + infobar + forced re-emit); kicadCollabTestGetLocked
- tests: lock-tiebreak unit suite; presence-locks e2e for both editors (real
move veto — pcbnew click+M hotkey since its default left-drag is
rubber-band select, eeschema real drag — each with an unlocked control);
two-tab tests/web/locks.spec.ts (lock propagation + deterministic tiebreak
release + unlock on clear, passing vs real partykit)
Spec: docs/features/collab-presence/0007 (closed repo).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lg5jwWuhFH5dL8hEcBDuP2
2026-07-07 20:35:26 +02:00
|
|
|
}
|
|
|
|
|
|
feat(collab): presence P3 — eeschema port (collab-presence 0003)
- eeschema_embind.cpp: presence section (0002 pattern, zero fork changes) —
wx canvas triggers + SCHEMATIC_LISTENER piggyback → post-settle
SCH_SELECTION_TOOL emit; throttled cursor; remote VIEW_OVERLAY render
(SCHEMATIC::ResolveItem, name tags, screen-constant via GAL matrix);
schCollab{PresenceStart,SetRemote,GetViewport,GetSelection,TestSelectFirst,
TestClearSelection}; kicad_editor_embind dispatches by active frame.
- sheet-manager: parked rooms carry SKELETON awareness states
({user,tool,sheetPath=bound sheet}) via publishSkeletons on switch + late
warm-up — the bound room's awareness then holds every project peer, so no
multi-room aggregation; presence.ts publishSkeleton helper.
- PresenceRoster: sheet-aware — peers on another sheet render dimmed with
'on <sheet>' tooltip; WasmTool un-gates the kicad presence bridge for
eeschema and threads activeSheetPath.
- tests: presence-eeschema.spec.ts (5 e2e, mirrors pcbnew incl. the px/IU
band at eeschema's 1e4/mm IU); presence.test.ts +2 (skeleton visibility,
no ghost cursor after rebind). eeschema-collab/subschema stay green.
Verified live: two tabs on demo.kicad_sch — peer cursor cross + label,
selection box + name tag, roster avatar.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
2026-07-06 17:05:15 +02:00
|
|
|
// Test helper: clear the selection through the tool + run the presence check.
|
|
|
|
|
bool schCollabTestClearSelection()
|
|
|
|
|
{
|
|
|
|
|
SCH_EDIT_FRAME* fr = schFrame();
|
|
|
|
|
|
|
|
|
|
if( !fr )
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
fr->CallAfter( [fr]() {
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
// ClearSelection is not on the shared SELECTION_TOOL base — the one
|
|
|
|
|
// presence hook that stays editor-typed.
|
feat(collab): presence P3 — eeschema port (collab-presence 0003)
- eeschema_embind.cpp: presence section (0002 pattern, zero fork changes) —
wx canvas triggers + SCHEMATIC_LISTENER piggyback → post-settle
SCH_SELECTION_TOOL emit; throttled cursor; remote VIEW_OVERLAY render
(SCHEMATIC::ResolveItem, name tags, screen-constant via GAL matrix);
schCollab{PresenceStart,SetRemote,GetViewport,GetSelection,TestSelectFirst,
TestClearSelection}; kicad_editor_embind dispatches by active frame.
- sheet-manager: parked rooms carry SKELETON awareness states
({user,tool,sheetPath=bound sheet}) via publishSkeletons on switch + late
warm-up — the bound room's awareness then holds every project peer, so no
multi-room aggregation; presence.ts publishSkeleton helper.
- PresenceRoster: sheet-aware — peers on another sheet render dimmed with
'on <sheet>' tooltip; WasmTool un-gates the kicad presence bridge for
eeschema and threads activeSheetPath.
- tests: presence-eeschema.spec.ts (5 e2e, mirrors pcbnew incl. the px/IU
band at eeschema's 1e4/mm IU); presence.test.ts +2 (skeleton visibility,
no ghost cursor after rebind). eeschema-collab/subschema stay green.
Verified live: two tabs on demo.kicad_sch — peer cursor cross + label,
selection box + name tag, roster avatar.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
2026-07-06 17:05:15 +02:00
|
|
|
if( SCH_SELECTION_TOOL* st = fr->GetToolManager()->GetTool<SCH_SELECTION_TOOL>() )
|
|
|
|
|
{
|
|
|
|
|
st->ClearSelection();
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
presenceCore().scheduleSelCheck();
|
feat(collab): presence P3 — eeschema port (collab-presence 0003)
- eeschema_embind.cpp: presence section (0002 pattern, zero fork changes) —
wx canvas triggers + SCHEMATIC_LISTENER piggyback → post-settle
SCH_SELECTION_TOOL emit; throttled cursor; remote VIEW_OVERLAY render
(SCHEMATIC::ResolveItem, name tags, screen-constant via GAL matrix);
schCollab{PresenceStart,SetRemote,GetViewport,GetSelection,TestSelectFirst,
TestClearSelection}; kicad_editor_embind dispatches by active frame.
- sheet-manager: parked rooms carry SKELETON awareness states
({user,tool,sheetPath=bound sheet}) via publishSkeletons on switch + late
warm-up — the bound room's awareness then holds every project peer, so no
multi-room aggregation; presence.ts publishSkeleton helper.
- PresenceRoster: sheet-aware — peers on another sheet render dimmed with
'on <sheet>' tooltip; WasmTool un-gates the kicad presence bridge for
eeschema and threads activeSheetPath.
- tests: presence-eeschema.spec.ts (5 e2e, mirrors pcbnew incl. the px/IU
band at eeschema's 1e4/mm IU); presence.test.ts +2 (skeleton visibility,
no ghost cursor after rebind). eeschema-collab/subschema stay green.
Verified live: two tabs on demo.kicad_sch — peer cursor cross + label,
selection box + name tag, roster avatar.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
2026-07-06 17:05:15 +02:00
|
|
|
}
|
|
|
|
|
} );
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-02 14:48:11 +02:00
|
|
|
// Merged-image dispatch probe (kicad_editor_embind.cpp): is the active top window the
|
|
|
|
|
// schematic editor? Counterpart of pcbnew_embind.cpp's pcbEditorActive().
|
|
|
|
|
bool schEditorActive()
|
|
|
|
|
{
|
|
|
|
|
return schFrame() != nullptr;
|
|
|
|
|
}
|
2026-06-12 10:16:02 +02:00
|
|
|
|
|
|
|
|
|
2026-06-10 18:06:33 +02:00
|
|
|
void kicadSaveSchematic( std::string path )
|
|
|
|
|
{
|
|
|
|
|
SCH_EDIT_FRAME* fr = schFrame();
|
|
|
|
|
|
|
|
|
|
if( !fr )
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
SCHEMATIC& sch = fr->Schematic();
|
|
|
|
|
|
|
|
|
|
// Save the CURRENT sheet, not Schematic().Root(): in the wasm open flow the
|
|
|
|
|
// opened document is displayed as the current sheet but can sit under an
|
|
|
|
|
// auto-created project root, so Root()'s own screen holds only a child-sheet
|
|
|
|
|
// symbol (not the loaded items). GetCurrentSheet() is the screen the editor is
|
|
|
|
|
// actually showing — the one whose items the snapshot/round-trip care about.
|
|
|
|
|
SCH_SHEET* sheet = fr->GetCurrentSheet().Last();
|
|
|
|
|
|
|
|
|
|
if( !sheet )
|
|
|
|
|
sheet = &sch.Root();
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
SCH_IO_KICAD_SEXPR io;
|
|
|
|
|
io.SaveSchematicFile( wxString::FromUTF8( path.c_str() ), sheet, &sch );
|
|
|
|
|
}
|
|
|
|
|
catch( ... )
|
|
|
|
|
{
|
|
|
|
|
// Don't abort the wasm runtime on a save failure; the JS caller detects it
|
|
|
|
|
// by the file being absent / empty.
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
feat(standalone): floating sheet navigator panel backed by a C++ hierarchy export (sheet-panel)
Canvas-only eeschema sessions (read-only viewers, hide-UI editors) could not
reach the wx hierarchy pane, so a viewer of a hierarchical design was stuck
on the entry sheet. Adds the React stand-in (SheetPanel) + the bridge:
- eeschema_embind: kicadSheetsGetTree() (SCHEMATIC::Hierarchy(), page-number
ordered: instance KIID path / parent / Sheetname / file / page / depth) and
kicadSheetsEnter(path) (validated synchronously, applied on the coroutine
via SCH_ACTIONS::changeSheet — same route as the wx pane, history intact).
OnSchSheetChanged pushes window.kicadCollab.onSheetsState after EVERY
navigation, wx-driven included, so the panel is event-driven.
- kicad → 9ab93b838e: navigate actions allowlisted for read-only viewers.
- SheetPanel.tsx: LayerPanel shell conventions; rendered + menu row only when
the hierarchy has >1 sheet instance; viewers boot it collapsed.
- tests: fixtures/demo/hier (root → Power, IO → Sub) + web/sheet-panel.spec
(list/order/depth, navigate + collab rebind, menu toggle/close, flat
schematic has no panel, bridge-driven navigation updates the panel);
SheetPanel.test.ts for the wire parser.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CtN6ASBvMGNbjPqY5boycg
2026-08-29 16:24:57 +02:00
|
|
|
// Sheet navigator bridge (sheet-panel) — see sheetsStateJson. ensureBridge()
|
|
|
|
|
// so the OnSchSheetChanged push is wired even for sessions that never bind
|
|
|
|
|
// presence (read-only viewers).
|
|
|
|
|
std::string schSheetsGetTree()
|
|
|
|
|
{
|
|
|
|
|
SCH_EDIT_FRAME* fr = schFrame();
|
|
|
|
|
|
|
|
|
|
if( !fr )
|
|
|
|
|
return "";
|
|
|
|
|
|
|
|
|
|
ensureBridge();
|
|
|
|
|
return sheetsStateJson( fr ).dump();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Navigate to the sheet instance whose KIID path is aPath (as reported by
|
|
|
|
|
// kicadSheetsGetTree). Validated synchronously against the live hierarchy; the
|
|
|
|
|
// switch itself runs on the coroutine through the navigate tool's changeSheet
|
|
|
|
|
// action — the same route the wx hierarchy pane takes, so back/forward history
|
|
|
|
|
// stays consistent. DisplayCurrentSheet fires OnSchSheetChanged → the collab
|
|
|
|
|
// sheet-manager rebinds its room and the panel gets its onSheetsState push.
|
|
|
|
|
// The action is on the read-only allowlist (view-only), so viewers can navigate.
|
|
|
|
|
bool schSheetsEnter( const std::string& aPath )
|
|
|
|
|
{
|
|
|
|
|
SCH_EDIT_FRAME* fr = schFrame();
|
|
|
|
|
|
|
|
|
|
if( !fr )
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
wxString wanted = wxString::FromUTF8( aPath.c_str() );
|
|
|
|
|
SCH_SHEET_LIST list = fr->Schematic().Hierarchy();
|
|
|
|
|
auto it = std::find_if( list.begin(), list.end(),
|
|
|
|
|
[&]( const SCH_SHEET_PATH& p )
|
|
|
|
|
{
|
|
|
|
|
return p.PathAsString() == wanted;
|
|
|
|
|
} );
|
|
|
|
|
|
|
|
|
|
if( it == list.end() )
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
if( *it == fr->GetCurrentSheet() )
|
|
|
|
|
{
|
|
|
|
|
emitSheetsState( fr );
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
SCH_SHEET_PATH target = *it;
|
|
|
|
|
|
|
|
|
|
pcbjam_collab::runOnCoroutine( fr, [fr, target]() {
|
|
|
|
|
SCH_SHEET_PATH path = target;
|
|
|
|
|
fr->GetToolManager()->RunAction<SCH_SHEET_PATH*>( SCH_ACTIONS::changeSheet, &path );
|
|
|
|
|
// changeSheet already pushed via OnSchSheetChanged; a no-op path
|
|
|
|
|
// (already there / rejected) still leaves the panel consistent.
|
|
|
|
|
emitSheetsState( fr );
|
|
|
|
|
} );
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
jspi cleanup: remove the asyncify-era residue — dead code, conditionals, pipeline scaffolding, stale prose
The runtime is JSPI-only; this removes everything that still pretended
otherwise. Three exhaustive sweeps (C++/JS+build+CI/tests+docs) drove
the inventory; every deletion verified by grep closure + full gates.
Broken-right-now fixes:
- deploy-staging.yml passed the retired opt_level input — the workflow
could not even start. Removed.
- env.sh carried dead exports with a live -sASYNCIFY=1 inside
(WASM_LDFLAGS/PTHREAD_LDFLAGS, zero consumers). Removed; the
WASM_LEGACY_EXCEPTIONS rationale rewritten to the real reason.
- docker/build.sh exported PCBJAM_ASYNC_BACKEND (read nowhere). Gone.
Dead weight removed:
- binaryen submodule (nothing builds or invokes it), wasm-opt-bench
workflow + scripts/bench/, get-wasm-opt.sh, diagnostics.js (242 lines
of Asyncify-API-only code), the KICAD_PIPELINE background-postprocess
scaffolding (existed to parallelize the deleted wasm-opt phase; the
postprocess is a seconds-long node script and now runs inline),
build-monitor's dead asyncify rows, sched-context orphan build
output, dead .gitignore entries, the .jspi-assets spike dir (the two
wf-result research JSONs moved to docs/features/async/migration-evidence/).
- bindings: fiber_park.h + its 12 embind registrations (broken-if-
called under JSPI), the kicadOpenFileStart/OPEN_JOB starter route,
main_stack_runner.h + 5 includes, the always-null context-sleep weak
hook in nanosleep_yield.c.
- shim: the backend field (installed-flag idempotency instead),
noteContextWait (dead both sides), the __wxAsyncifyDump alias (+ the
WasmTool fallback and string-dump normalize branch).
- web: the emscripten-6-ignored mainScriptUrlOrBlob option in boot.ts
(gerber-demo keeps it: it loads the deployed CDN release, which
predates emscripten 6 — noted inline).
Conditionals: all 'backend === jspi' checks reduced to scheduler-
presence checks; races_quiescent re-keyed from Asyncify.state (vacuous)
to real backlog quiescence (resumeReady/mutatorQueue — NOT _windowLive,
which is the probing activation's own window by definition).
Renames (identifiers only, no file renames): ASYNC_LINK_FLAGS→
JSPI_LINK_FLAGS and Makefile ASYNC_LDFLAGS→JSPI_LDFLAGS,
kicadCollabFiberBusy→kicadCollabBusy (embind + web + tests),
collab_common.h fiber*→apply*/coroutine naming, asyncifySignatures→
wasmTrapSignatures (lists byte-identical).
Tests: the two remaining vacuous [wx-asyncify]/fiber-resume-refused
asserts re-keyed to live JSPI beacons; eeschema-load's failure message
no longer sends the developer to a deleted script; wait-beacons' dead
families/parser deleted; lane-0 legacy-glue guards removed (lane 0 is
unconstructible); the embind test.fail re-gated with the JSPI reason
(plain embind invokers cannot suspend — verified still failing);
lint-determinism now scans tests/jspi (166 files clean);
eeschema-collab local-move gated to chromium (~50% flaky on FF even
solo; pcbnew twin covers both engines).
Docs: DEBUG.md rewritten as the JSPI debugging guide; build.md
describes the single-phase build; docs/features/async/README.md
banner-marked historical and repointed at the NEW
23-jspi-runtime.md (current architecture: export census, turnstile,
libcontext ownership + refusal contract, embind call shapes, the
em-pthread service-wrapper trick, exception policy, known gaps).
Gates on the cleaned tree: test:e2e 725 passed / 0 failed (after the
quiescence-probe fix; the 3 other reds were verified contention flakes
solo-green or the documented FF gate), web 76/0, jspi 18/18 both
engines, vitest 295/295 + 17/17, all lints green, live-app census
clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016X9eh1s5sTx1o9Em9KBuwR
2026-08-14 09:25:32 +02:00
|
|
|
static bool kicadCollabBusyProbe()
|
2026-07-21 12:22:09 +02:00
|
|
|
{
|
jspi cleanup: remove the asyncify-era residue — dead code, conditionals, pipeline scaffolding, stale prose
The runtime is JSPI-only; this removes everything that still pretended
otherwise. Three exhaustive sweeps (C++/JS+build+CI/tests+docs) drove
the inventory; every deletion verified by grep closure + full gates.
Broken-right-now fixes:
- deploy-staging.yml passed the retired opt_level input — the workflow
could not even start. Removed.
- env.sh carried dead exports with a live -sASYNCIFY=1 inside
(WASM_LDFLAGS/PTHREAD_LDFLAGS, zero consumers). Removed; the
WASM_LEGACY_EXCEPTIONS rationale rewritten to the real reason.
- docker/build.sh exported PCBJAM_ASYNC_BACKEND (read nowhere). Gone.
Dead weight removed:
- binaryen submodule (nothing builds or invokes it), wasm-opt-bench
workflow + scripts/bench/, get-wasm-opt.sh, diagnostics.js (242 lines
of Asyncify-API-only code), the KICAD_PIPELINE background-postprocess
scaffolding (existed to parallelize the deleted wasm-opt phase; the
postprocess is a seconds-long node script and now runs inline),
build-monitor's dead asyncify rows, sched-context orphan build
output, dead .gitignore entries, the .jspi-assets spike dir (the two
wf-result research JSONs moved to docs/features/async/migration-evidence/).
- bindings: fiber_park.h + its 12 embind registrations (broken-if-
called under JSPI), the kicadOpenFileStart/OPEN_JOB starter route,
main_stack_runner.h + 5 includes, the always-null context-sleep weak
hook in nanosleep_yield.c.
- shim: the backend field (installed-flag idempotency instead),
noteContextWait (dead both sides), the __wxAsyncifyDump alias (+ the
WasmTool fallback and string-dump normalize branch).
- web: the emscripten-6-ignored mainScriptUrlOrBlob option in boot.ts
(gerber-demo keeps it: it loads the deployed CDN release, which
predates emscripten 6 — noted inline).
Conditionals: all 'backend === jspi' checks reduced to scheduler-
presence checks; races_quiescent re-keyed from Asyncify.state (vacuous)
to real backlog quiescence (resumeReady/mutatorQueue — NOT _windowLive,
which is the probing activation's own window by definition).
Renames (identifiers only, no file renames): ASYNC_LINK_FLAGS→
JSPI_LINK_FLAGS and Makefile ASYNC_LDFLAGS→JSPI_LDFLAGS,
kicadCollabFiberBusy→kicadCollabBusy (embind + web + tests),
collab_common.h fiber*→apply*/coroutine naming, asyncifySignatures→
wasmTrapSignatures (lists byte-identical).
Tests: the two remaining vacuous [wx-asyncify]/fiber-resume-refused
asserts re-keyed to live JSPI beacons; eeschema-load's failure message
no longer sends the developer to a deleted script; wait-beacons' dead
families/parser deleted; lane-0 legacy-glue guards removed (lane 0 is
unconstructible); the embind test.fail re-gated with the JSPI reason
(plain embind invokers cannot suspend — verified still failing);
lint-determinism now scans tests/jspi (166 files clean);
eeschema-collab local-move gated to chromium (~50% flaky on FF even
solo; pcbnew twin covers both engines).
Docs: DEBUG.md rewritten as the JSPI debugging guide; build.md
describes the single-phase build; docs/features/async/README.md
banner-marked historical and repointed at the NEW
23-jspi-runtime.md (current architecture: export census, turnstile,
libcontext ownership + refusal contract, embind call shapes, the
em-pthread service-wrapper trick, exception policy, known gaps).
Gates on the cleaned tree: test:e2e 725 passed / 0 failed (after the
quiescence-probe fix; the 3 other reds were verified contention flakes
solo-green or the documented FF gate), web 76/0, jspi 18/18 both
engines, vitest 295/295 + 17/17, all lints green, live-app census
clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016X9eh1s5sTx1o9Em9KBuwR
2026-08-14 09:25:32 +02:00
|
|
|
return pcbjam_collab::applyBusy() || !pcbjam_collab::applyQueue().empty();
|
2026-07-21 12:22:09 +02:00
|
|
|
}
|
|
|
|
|
|
2026-06-01 16:30:29 +02:00
|
|
|
EMSCRIPTEN_BINDINGS(eeschema) {
|
2026-06-10 18:06:33 +02:00
|
|
|
// Programmatic save of the in-memory schematic (round-trip tests, README §A).
|
|
|
|
|
function("kicadSaveSchematic", &kicadSaveSchematic);
|
test(ysync): repro tests for review bugs 01-07 + v2 items-wire e2e port (miss 11)
The 2026-07-02 sync review (docs/features/ysync-review, ysync-review branch)
found 7 bugs and that the two-tab e2e only exercised the DEAD legacy scalar
wire. This lands plan doc 15 in full; results + empirical findings in doc 16.
- tests/collab/browser-entry-v2.ts (+build.mjs): the PRODUCTION v2 stack
bundled for e2e (connectKicadDoc + attachKicadCollab, kdoc_* keys), with
in-page renderActiveDoc/singleSeedRender/driftReport helpers and yjs forced
to ONE copy (the two web pnpm workspaces otherwise bundle two
instanceof-incompatible instances).
- tests/kicad/ysync-two-tab.spec.ts: pl_editor green baseline (A↔B edits,
ITEM-level drift silence) + divergent-uuid adopt; bug-01 pcb/ee fresh-room
repros (Chromium-only: two kicad_editor tabs exceed Firefox's per-process
wasm budget); bug-06 concurrent-seed race; bug-03 Y-half.
- tests/kicad/ysync-repros-{pcbnew,eeschema}.spec.ts: bugs 02/03/05 + the
bug-04 matrix (anchor-centred fp rotation, pad resize, endpoint drag,
symbol rotation, Value-field edit), each with green landed-preconditions;
the "local move emits" controls double as headless-emit probes — GREEN on
both tools, so every emit-dependent repro is a live test.fail.
- wasm/bindings: 7 local-edit test hooks via real commits
(CallAfter+COROUTINE) — TestRemoveItem/TestRotateItem (both tools,
dispatched in the merged image), TestSetPadSize/TestMoveEndpoint (pcbnew),
TestSetFieldText (eeschema).
- web/standalone ysync-repros.test.ts: bug-01 units (C++-faithful fake gating
emit on ensureBridge) + bug-07a/b (stale DOWN hook, real sheet-manager gap).
- web/pcbjam-shared bump: bug-03/06 unit repros.
Convention: every repro asserts the CORRECT behavior and is expected-fail
(test.fail/it.fails) naming its bug doc; a fix flips it to "unexpected pass",
forcing marker removal — the repro becomes the regression test. Every
expected failure verified (JSON reporter) to fail at its documented assert.
Suite state: 39 passed / 0 failed / 0 flaky / 5 skipped (2 firefox guards,
2 pre-existing legacy two-tab skips, 1 pre-existing roundtrip fixme).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DPfrVhfYgPPgtawjSssZfn
2026-07-03 12:43:35 +02:00
|
|
|
// eeschema-only ysync-review repro hook (name not shared with pcbnew).
|
|
|
|
|
function("kicadCollabTestSetFieldText", &schCollabTestSetFieldText);
|
2026-07-21 10:37:37 +02:00
|
|
|
// drift-trio phase B action hooks (tool-unique names, merged-image safe).
|
|
|
|
|
function("kicadCollabTestAddWire", &schCollabTestAddWire);
|
|
|
|
|
function("kicadCollabTestAddJunction", &schCollabTestAddJunction);
|
|
|
|
|
function("kicadCollabTestAddNoConnect", &schCollabTestAddNoConnect);
|
|
|
|
|
function("kicadCollabTestAddLabel", &schCollabTestAddLabel);
|
|
|
|
|
function("kicadCollabTestAddSymbol", &schCollabTestAddSymbol);
|
|
|
|
|
function("kicadCollabTestMoveSchItem", &schCollabTestMoveSchItem);
|
|
|
|
|
function("kicadCollabTestMirrorSchItem", &schCollabTestMirrorSchItem);
|
|
|
|
|
function("kicadCollabTestDuplicateSchItem", &schCollabTestDuplicateSchItem);
|
feat(standalone): floating sheet navigator panel backed by a C++ hierarchy export (sheet-panel)
Canvas-only eeschema sessions (read-only viewers, hide-UI editors) could not
reach the wx hierarchy pane, so a viewer of a hierarchical design was stuck
on the entry sheet. Adds the React stand-in (SheetPanel) + the bridge:
- eeschema_embind: kicadSheetsGetTree() (SCHEMATIC::Hierarchy(), page-number
ordered: instance KIID path / parent / Sheetname / file / page / depth) and
kicadSheetsEnter(path) (validated synchronously, applied on the coroutine
via SCH_ACTIONS::changeSheet — same route as the wx pane, history intact).
OnSchSheetChanged pushes window.kicadCollab.onSheetsState after EVERY
navigation, wx-driven included, so the panel is event-driven.
- kicad → 9ab93b838e: navigate actions allowlisted for read-only viewers.
- SheetPanel.tsx: LayerPanel shell conventions; rendered + menu row only when
the hierarchy has >1 sheet instance; viewers boot it collapsed.
- tests: fixtures/demo/hier (root → Power, IO → Sub) + web/sheet-panel.spec
(list/order/depth, navigate + collab rebind, menu toggle/close, flat
schematic has no panel, bridge-driven navigation updates the panel);
SheetPanel.test.ts for the wire parser.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CtN6ASBvMGNbjPqY5boycg
2026-08-29 16:24:57 +02:00
|
|
|
// Sheet navigator bridge (sheet-panel): hierarchy tree + navigate.
|
|
|
|
|
function("kicadSheetsGetTree", &schSheetsGetTree);
|
|
|
|
|
function("kicadSheetsEnter", &schSheetsEnter);
|
2026-07-02 14:48:11 +02:00
|
|
|
|
|
|
|
|
#ifndef KICAD_MERGED_EMBIND
|
|
|
|
|
// JS names ALSO registered by pcbnew_embind.cpp — in the merged image these are
|
|
|
|
|
// registered once by kicad_editor_embind.cpp, dispatching on the active frame.
|
|
|
|
|
// Programmatic file open (preferred over UI automation from the web app).
|
jspi: migration phases 0-7 — build knob, scheduler shim, test successor suite
Toolchain: emsdk 6.0.6 (versions.sh; cache-hash keys on it). Build knob
PCBJAM_ASYNC_BACKEND=jspi|asyncify: build-kicad-target.sh links editors with
-sJSPI + -sJSPI_EXPORTS=@scripts/common/jspi-exports.txt + --pre-js
jspi-scheduler.js (no DYNCALLS, no post-link asyncify pipeline); wx build
stamps the backend and forces clean on flip or unknown provenance;
docker/build.sh passes the knob, seeds the emscripten ports cache from the
volume every launch, jspi postprocess = patch-env-shim only.
scripts/common/shims/jspi-scheduler.js: the JSPI successor scheduler —
token-wait registry, resume turnstile (one armed resume between engine
re-entries, SP swaps only at microtask boundaries), green-region spill
stacks (16-aligned tops), S1 embind mutator FIFO lane + parker wraps, S6
shutdown, libctx integration hooks (suspend/end/quarantine + g_current
arm/clear), SuspendError attributor, lost-wake + stuck-window watchdogs,
__wxWaitDump observability.
Embind: PARKER registrations get emscripten::async() under PCBJAM_JSPI
(wasm/bindings/pcbjam_async_policy.h). nanosleep yields route via the shim.
Tests: tests/asyncify -> tests/jspi successor suite (jspi-stack red/green
shadow-stack battery, jspi-coroutine MiniCoro harness, suspend-races
semantic scenarios + __wxWaitDump books coherence); projects jspi-firefox/
jspi-chrome (asyncify-webkit retired — no JSPI in WebKit); unconditional
Firefox JSPI pref; guard-beacons -> wait-beacons (+wxScheduler/libctxJspi
families); Makefile.wasm links test apps against JSPI with the shim as a
tracked link prerequisite.
Web: WasmTool setRo await + __wxWaitDump forensics, open-flow contained
promise, scheduler-shim.test.ts retargeted (8 green).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NDeBaKKhQztd8KiVtHuyXr
2026-08-13 07:06:24 +02:00
|
|
|
function("kicadOpenFile", &kicadOpenFile PCBJAM_PARKER_POLICY);
|
2026-07-30 14:17:48 +02:00
|
|
|
function("kicadOpenFileBusy", &kicadOpenFileBusy);
|
|
|
|
|
function("kicadTestSetOpenPark", &kicadTestSetOpenPark);
|
jspi cleanup: remove the asyncify-era residue — dead code, conditionals, pipeline scaffolding, stale prose
The runtime is JSPI-only; this removes everything that still pretended
otherwise. Three exhaustive sweeps (C++/JS+build+CI/tests+docs) drove
the inventory; every deletion verified by grep closure + full gates.
Broken-right-now fixes:
- deploy-staging.yml passed the retired opt_level input — the workflow
could not even start. Removed.
- env.sh carried dead exports with a live -sASYNCIFY=1 inside
(WASM_LDFLAGS/PTHREAD_LDFLAGS, zero consumers). Removed; the
WASM_LEGACY_EXCEPTIONS rationale rewritten to the real reason.
- docker/build.sh exported PCBJAM_ASYNC_BACKEND (read nowhere). Gone.
Dead weight removed:
- binaryen submodule (nothing builds or invokes it), wasm-opt-bench
workflow + scripts/bench/, get-wasm-opt.sh, diagnostics.js (242 lines
of Asyncify-API-only code), the KICAD_PIPELINE background-postprocess
scaffolding (existed to parallelize the deleted wasm-opt phase; the
postprocess is a seconds-long node script and now runs inline),
build-monitor's dead asyncify rows, sched-context orphan build
output, dead .gitignore entries, the .jspi-assets spike dir (the two
wf-result research JSONs moved to docs/features/async/migration-evidence/).
- bindings: fiber_park.h + its 12 embind registrations (broken-if-
called under JSPI), the kicadOpenFileStart/OPEN_JOB starter route,
main_stack_runner.h + 5 includes, the always-null context-sleep weak
hook in nanosleep_yield.c.
- shim: the backend field (installed-flag idempotency instead),
noteContextWait (dead both sides), the __wxAsyncifyDump alias (+ the
WasmTool fallback and string-dump normalize branch).
- web: the emscripten-6-ignored mainScriptUrlOrBlob option in boot.ts
(gerber-demo keeps it: it loads the deployed CDN release, which
predates emscripten 6 — noted inline).
Conditionals: all 'backend === jspi' checks reduced to scheduler-
presence checks; races_quiescent re-keyed from Asyncify.state (vacuous)
to real backlog quiescence (resumeReady/mutatorQueue — NOT _windowLive,
which is the probing activation's own window by definition).
Renames (identifiers only, no file renames): ASYNC_LINK_FLAGS→
JSPI_LINK_FLAGS and Makefile ASYNC_LDFLAGS→JSPI_LDFLAGS,
kicadCollabFiberBusy→kicadCollabBusy (embind + web + tests),
collab_common.h fiber*→apply*/coroutine naming, asyncifySignatures→
wasmTrapSignatures (lists byte-identical).
Tests: the two remaining vacuous [wx-asyncify]/fiber-resume-refused
asserts re-keyed to live JSPI beacons; eeschema-load's failure message
no longer sends the developer to a deleted script; wait-beacons' dead
families/parser deleted; lane-0 legacy-glue guards removed (lane 0 is
unconstructible); the embind test.fail re-gated with the JSPI reason
(plain embind invokers cannot suspend — verified still failing);
lint-determinism now scans tests/jspi (166 files clean);
eeschema-collab local-move gated to chromium (~50% flaky on FF even
solo; pcbnew twin covers both engines).
Docs: DEBUG.md rewritten as the JSPI debugging guide; build.md
describes the single-phase build; docs/features/async/README.md
banner-marked historical and repointed at the NEW
23-jspi-runtime.md (current architecture: export census, turnstile,
libcontext ownership + refusal contract, embind call shapes, the
em-pthread service-wrapper trick, exception policy, known gaps).
Gates on the cleaned tree: test:e2e 725 passed / 0 failed (after the
quiescence-probe fix; the 3 other reds were verified contention flakes
solo-green or the documented FF gate), web 76/0, jspi 18/18 both
engines, vitest 295/295 + 17/17, all lints green, live-app census
clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016X9eh1s5sTx1o9Em9KBuwR
2026-08-14 09:25:32 +02:00
|
|
|
function("kicadCollabBusy", &kicadCollabBusyProbe);
|
2026-07-10 20:27:04 +02:00
|
|
|
// Read-only viewer lock (read-only-viewer).
|
|
|
|
|
function("kicadSetReadOnly", &kicadSetReadOnly);
|
2026-06-03 17:49:11 +02:00
|
|
|
// Yjs collaborative bridge entry points (same contract as pl_editor).
|
2026-07-02 14:48:11 +02:00
|
|
|
function("kicadCollabApply", &schCollabApply);
|
|
|
|
|
function("kicadCollabSnapshot", &schCollabSnapshot);
|
2026-06-11 13:55:15 +02:00
|
|
|
// v2 items bridge: per-item s-expr payloads (ysync 0008).
|
2026-07-02 14:48:11 +02:00
|
|
|
function("kicadCollabApplyItems", &schCollabApplyItems);
|
|
|
|
|
function("kicadCollabSnapshotItems", &schCollabSnapshotItems);
|
|
|
|
|
function("kicadCollabTestMoveFirst", &schCollabTestMoveFirst);
|
|
|
|
|
function("kicadCollabGetPos", &schCollabGetPos);
|
test(ysync): repro tests for review bugs 01-07 + v2 items-wire e2e port (miss 11)
The 2026-07-02 sync review (docs/features/ysync-review, ysync-review branch)
found 7 bugs and that the two-tab e2e only exercised the DEAD legacy scalar
wire. This lands plan doc 15 in full; results + empirical findings in doc 16.
- tests/collab/browser-entry-v2.ts (+build.mjs): the PRODUCTION v2 stack
bundled for e2e (connectKicadDoc + attachKicadCollab, kdoc_* keys), with
in-page renderActiveDoc/singleSeedRender/driftReport helpers and yjs forced
to ONE copy (the two web pnpm workspaces otherwise bundle two
instanceof-incompatible instances).
- tests/kicad/ysync-two-tab.spec.ts: pl_editor green baseline (A↔B edits,
ITEM-level drift silence) + divergent-uuid adopt; bug-01 pcb/ee fresh-room
repros (Chromium-only: two kicad_editor tabs exceed Firefox's per-process
wasm budget); bug-06 concurrent-seed race; bug-03 Y-half.
- tests/kicad/ysync-repros-{pcbnew,eeschema}.spec.ts: bugs 02/03/05 + the
bug-04 matrix (anchor-centred fp rotation, pad resize, endpoint drag,
symbol rotation, Value-field edit), each with green landed-preconditions;
the "local move emits" controls double as headless-emit probes — GREEN on
both tools, so every emit-dependent repro is a live test.fail.
- wasm/bindings: 7 local-edit test hooks via real commits
(CallAfter+COROUTINE) — TestRemoveItem/TestRotateItem (both tools,
dispatched in the merged image), TestSetPadSize/TestMoveEndpoint (pcbnew),
TestSetFieldText (eeschema).
- web/standalone ysync-repros.test.ts: bug-01 units (C++-faithful fake gating
emit on ensureBridge) + bug-07a/b (stale DOWN hook, real sheet-manager gap).
- web/pcbjam-shared bump: bug-03/06 unit repros.
Convention: every repro asserts the CORRECT behavior and is expected-fail
(test.fail/it.fails) naming its bug doc; a fix flips it to "unexpected pass",
forcing marker removal — the repro becomes the regression test. Every
expected failure verified (JSON reporter) to fail at its documented assert.
Suite state: 39 passed / 0 failed / 0 flaky / 5 skipped (2 firefox guards,
2 pre-existing legacy two-tab skips, 1 pre-existing roundtrip fixme).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DPfrVhfYgPPgtawjSssZfn
2026-07-03 12:43:35 +02:00
|
|
|
// ysync-review repro hooks shared with pcbnew (dispatched when merged).
|
|
|
|
|
function("kicadCollabTestRemoveItem", &schCollabTestRemoveItem);
|
|
|
|
|
function("kicadCollabTestRotateItem", &schCollabTestRotateItem);
|
2026-07-08 11:09:26 +02:00
|
|
|
function("kicadCollabTestUndo", &schCollabTestUndo);
|
|
|
|
|
function("kicadCollabTestUndoDepth", &schCollabTestUndoDepth);
|
feat(collab): presence P3 — eeschema port (collab-presence 0003)
- eeschema_embind.cpp: presence section (0002 pattern, zero fork changes) —
wx canvas triggers + SCHEMATIC_LISTENER piggyback → post-settle
SCH_SELECTION_TOOL emit; throttled cursor; remote VIEW_OVERLAY render
(SCHEMATIC::ResolveItem, name tags, screen-constant via GAL matrix);
schCollab{PresenceStart,SetRemote,GetViewport,GetSelection,TestSelectFirst,
TestClearSelection}; kicad_editor_embind dispatches by active frame.
- sheet-manager: parked rooms carry SKELETON awareness states
({user,tool,sheetPath=bound sheet}) via publishSkeletons on switch + late
warm-up — the bound room's awareness then holds every project peer, so no
multi-room aggregation; presence.ts publishSkeleton helper.
- PresenceRoster: sheet-aware — peers on another sheet render dimmed with
'on <sheet>' tooltip; WasmTool un-gates the kicad presence bridge for
eeschema and threads activeSheetPath.
- tests: presence-eeschema.spec.ts (5 e2e, mirrors pcbnew incl. the px/IU
band at eeschema's 1e4/mm IU); presence.test.ts +2 (skeleton visibility,
no ghost cursor after rebind). eeschema-collab/subschema stay green.
Verified live: two tabs on demo.kicad_sch — peer cursor cross + label,
selection box + name tag, roster avatar.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
2026-07-06 17:05:15 +02:00
|
|
|
// Presence (collab-presence 0003) — shared names with pcbnew's 0002 set.
|
|
|
|
|
function("kicadCollabPresenceStart", &schCollabPresenceStart);
|
|
|
|
|
function("kicadCollabSetRemote", &schCollabSetRemote);
|
2026-08-28 16:14:44 +02:00
|
|
|
function("kicadCollabSetRemoteCursors", &schCollabSetRemoteCursors);
|
feat(comments): figma-like comment pins + threads (collab-presence 0005)
Hybrid pins: the wasm draws the dot (kicadCollabSetPins rides the presence
VIEW_OVERLAY, author color + white ring, drawn above selections; zero
kicad-fork changes), the DOM owns interaction —
- comments.ts: controller gluing the MIT kdoc_comments helpers to the editor:
anchor resolution per tool IU (pins track item moves via kdoc_items
observation), throttled pin snapshots, anchorAt nearest-item snap, jumpTo
via new kicadCollabSetViewport; rebinds per sheet like presence.
- CommentLayer.tsx: comment mode (click catcher + composer), pin hit targets
over the GAL dots, thread popover (reply/edit/delete own, resolve/reopen,
delete thread), panel with resolved filter + jump-to (popover centers when
the pin is off-screen). Resolved pins drop figma-style.
- WasmTool: controller lifecycle beside presence; live viewport feed;
window.__pcbjamComments test handle (threads persist in the room ydoc).
- e2e tests/web/comments.spec.ts: two-tab create → reply → resolve → panel
filter → delete, passing vs real partykit; presence suites + collab units
stay green; shared pointer bump (0004 model).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
2026-07-06 17:50:11 +02:00
|
|
|
function("kicadCollabSetPins", &schCollabSetPins);
|
|
|
|
|
function("kicadCollabSetViewport", &schCollabSetViewport);
|
feat(collab): follow-user (collab-presence 0008) + chip depth-layer fix
Follow-user: click a peer's roster avatar to mirror their viewport until
local input breaks it.
- collab_presence_core.h: CORE::fitViewport(cx, cy, halfW, halfH) — fit the
leader's world rect with CONTAIN semantics (zoom derived from the
follower's own canvas via the ToScreen ratio; GetScale is the zoom, not
px/IU). Exported as kicadCollabFitViewport from both editor TUs + the
merged dispatcher.
- presence-kicad.ts: publish the visible world rect (viewportRect) into
awareness, 100 ms trailing throttle; guarded for pre-0008 handles.
- follow-user.ts: createFollow — follows an awareness CLIENT (a tab, not a
user); applies leader rect changes via FitViewport, dedupes unchanged
republishes; break-on-interact compares local onViewport echoes against
the last applied rect (2% rel tolerance, echo-grace before the first fit
lands); unfollows on leader-left; pauses on eeschema sheet mismatch.
- PresenceRoster: avatars are follow toggles (ring on the followed peer);
WasmTool renders the "Following <name> — move to stop" banner.
- tests: 7 controller units (85/85 collab), fitViewport round-trip e2e in
both kicad presence specs (20/20), two-tab tests/web/follow.spec.ts
(converge → track → wheel-zoom breaks → subsequent moves ignored).
Chip depth-layer fix (user-reported): name chips washed out inside
low-alpha selection fills — chip rects shared the shapes overlay's single
depth, and same-depth fragments drawn LATER lose the depth test, so an
earlier-painted fill rejected the chip's pixels. Now three layers via the
fork's VIEW_OVERLAY::SetDepthOffset: text (0) < chips + pin dots (1) <
selection shapes (2). drawLabel/drawCursor/drawSelectionBox take the chip
overlay explicitly; comment-pin dots move to the chip layer too (the 0005
"drawn last so pins sit above" comment had the rule backwards). Verified
with a chip-inside-30%-fill pixel repro + the full presence suite.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-09 09:30:40 +02:00
|
|
|
// Follow-user (collab-presence 0008).
|
|
|
|
|
function("kicadCollabFitViewport", &schCollabFitViewport);
|
feat(collab): dev-time presence style tuner (VITE_PRESENCE_TUNER=1)
Parametrizes every visual knob of the presence overlay so we can pick the
shipped look live, then wire the winners into the defaults:
- collab_presence_style.h: shared STYLE struct + drawing (now used by BOTH
editor TUs — no more duplicated overlay code): selection shape (rect /
corner brackets / underline / rounded rect / filled-only), border width +
alpha, infill alpha, padding, corner radius; name tag show/size/chip-
background/inside-outside/top-bottom/start-end-center/offset; cursor shape
(cross / pointer / circle+dot), size/width/alpha + label knobs; fixed-color
and palette-by-name-hash overrides (try palettes without changing what
senders publish); pin radius/ring/alphas. Defaults == shipped look.
- kicadCollabSetStyle(json) live-patch export + kicadCollabTestListItems(n)
(real KIIDs for synthetic previews); merged dispatch; pins now carry the
author name so palette overrides recolor them consistently.
- PresenceTuner.tsx: floating dev panel (env-gated, tree-shaken otherwise) —
grouped sliders/selects, demo peers+pins injection for SOLO tuning,
localStorage persistence across reloads, Copy JSON export, reset.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
2026-07-06 19:10:40 +02:00
|
|
|
function("kicadCollabSetStyle", &schCollabSetStyle);
|
comments-ux: figma bubble pins, floating panel, seen/reactions/mentions UI, theme follow (0001 A–E + 0002)
- GAL pin = one closed polygon: round body, squared-off bottom-left corner
ON the anchor; PIN gains unread (accent ring); tuner knobs; shipped
defaults r9/ring4/alpha.9. DOM hit/highlight sized+offset from a LIVE
pin-geometry radius store the tuner feeds.
- Floating comments panel: draggable (shared useDraggablePanel with
always-onscreen restore; overlay FAB retrofitted), collapsible to header,
header carries add/show-hide/mark-all; unread badges (rose on mention).
- Reactions (emoji-mart lazy, quick-row) + @-mention autocomplete
(MentionInput; backend roster with presence/author fallback).
- Theme: ?theme= > storage > OS, no-flash boot, toggles (HomePage + overlay
View row), boot-seeded pcbjam-dark schematic colors + kicadSetColorTheme /
kicadSetDarkChrome bridges (canvas + wx chrome live flip), light/dark
variants across all overlay surfaces.
- e2e: panel/seen/reactions/mentions/theme specs + resize-spec geometry;
bumps pcbjam-shared (flat-key seen/reactions + listCollaborators) and
wxwidgets (dark chrome) pointers.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HLwn1toiNKi1MgxGKnZTes
2026-07-24 13:21:22 +02:00
|
|
|
// Live color-theme switch (comments-ux 0002 F4).
|
|
|
|
|
function("kicadSetColorTheme", &schSetColorTheme);
|
|
|
|
|
function("kicadSetDarkChrome", &schSetDarkChrome);
|
feat(collab): dev-time presence style tuner (VITE_PRESENCE_TUNER=1)
Parametrizes every visual knob of the presence overlay so we can pick the
shipped look live, then wire the winners into the defaults:
- collab_presence_style.h: shared STYLE struct + drawing (now used by BOTH
editor TUs — no more duplicated overlay code): selection shape (rect /
corner brackets / underline / rounded rect / filled-only), border width +
alpha, infill alpha, padding, corner radius; name tag show/size/chip-
background/inside-outside/top-bottom/start-end-center/offset; cursor shape
(cross / pointer / circle+dot), size/width/alpha + label knobs; fixed-color
and palette-by-name-hash overrides (try palettes without changing what
senders publish); pin radius/ring/alphas. Defaults == shipped look.
- kicadCollabSetStyle(json) live-patch export + kicadCollabTestListItems(n)
(real KIIDs for synthetic previews); merged dispatch; pins now carry the
author name so palette overrides recolor them consistently.
- PresenceTuner.tsx: floating dev panel (env-gated, tree-shaken otherwise) —
grouped sliders/selects, demo peers+pins injection for SOLO tuning,
localStorage persistence across reloads, Copy JSON export, reset.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
2026-07-06 19:10:40 +02:00
|
|
|
function("kicadCollabTestListItems", &schCollabTestListItems);
|
feat(collab): tuner round 2 — center-anchored labels, exact outlines, varied demo set, clearer color modes
- collab_presence_style.h: GAL BitmapText CENTERS on its position (confirmed
in GAL::ResetTextAttributes — the mispositioned nameplates); labels/chips
now hand GAL the block center. New selection shape 5 'exact outline':
pcbnew hugs real geometry (footprint bounding hull, TransformShapeToPolygon
for the rest, padding inflates the polygon); eeschema falls back to rect.
- kicadCollabTestDemoSet (both TUs + merged): labeled demo groups — smallest
+ largest footprint and the two busiest nets' segments (symbols + wire
bundles on sch) — so the style preview covers the real range of shapes.
- PresenceTuner: Colors section rebuilt as explicit modes (per-user / fixed /
palette) with preset palettes (default, pastel, vivid, okabe-ito), buffered
hex editing + Apply (the old always-filtering textarea ate keystrokes), an
'overlay only' hint; demo injection consumes the varied demo set; 'exact
outline (pcb)' in the shape list.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
2026-07-07 10:41:15 +02:00
|
|
|
function("kicadCollabTestDemoSet", &schCollabTestDemoSet);
|
feat(collab): presence P3 — eeschema port (collab-presence 0003)
- eeschema_embind.cpp: presence section (0002 pattern, zero fork changes) —
wx canvas triggers + SCHEMATIC_LISTENER piggyback → post-settle
SCH_SELECTION_TOOL emit; throttled cursor; remote VIEW_OVERLAY render
(SCHEMATIC::ResolveItem, name tags, screen-constant via GAL matrix);
schCollab{PresenceStart,SetRemote,GetViewport,GetSelection,TestSelectFirst,
TestClearSelection}; kicad_editor_embind dispatches by active frame.
- sheet-manager: parked rooms carry SKELETON awareness states
({user,tool,sheetPath=bound sheet}) via publishSkeletons on switch + late
warm-up — the bound room's awareness then holds every project peer, so no
multi-room aggregation; presence.ts publishSkeleton helper.
- PresenceRoster: sheet-aware — peers on another sheet render dimmed with
'on <sheet>' tooltip; WasmTool un-gates the kicad presence bridge for
eeschema and threads activeSheetPath.
- tests: presence-eeschema.spec.ts (5 e2e, mirrors pcbnew incl. the px/IU
band at eeschema's 1e4/mm IU); presence.test.ts +2 (skeleton visibility,
no ghost cursor after rebind). eeschema-collab/subschema stay green.
Verified live: two tabs on demo.kicad_sch — peer cursor cross + label,
selection box + name tag, roster avatar.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
2026-07-06 17:05:15 +02:00
|
|
|
function("kicadCollabGetViewport", &schCollabGetViewport);
|
|
|
|
|
function("kicadCollabGetSelection", &schCollabGetSelection);
|
feat(collab): cross-app selection — eeschema symbol ⇄ pcbnew footprint ghost highlight (collab-presence 0006)
Selecting a symbol in eeschema ghost-highlights the linked footprint(s) in
every pcbnew tab of the project, and vice versa — across users AND one
user's own two tabs. Native KIWAY cross-probe is inert in WASM (one frame
per page); this rides the presence layer instead.
- cross-app.ts: project-wide awareness-only room (presenceRoomId), publishes
full PresenceState at selection rate (cursor always null); peers() = other-
TOOL clients incl. own user's other tabs; window.__pcbjamCrossApp test handle
- presence-kicad.ts: parseSelectionEmit (bare array | {uuids,fpPaths}),
xselFromPeerState (pcbnew paths → symbol uuids; eeschema uuids verbatim),
cross peers appended to the kicadCollabSetRemote snapshot as
{id "<user>#x<client>", name "<user> · sch|pcb", xsel}
- C++ (zero fork changes): pcbnew emits {uuids, fpPaths} (FOOTPRINT::GetPath)
and ghost-renders xsel via path-tail suffix scan; eeschema resolves xsel via
ResolveItem gated to the CURRENT sheet (xsel arrives project-wide, unlike
room-scoped selections); ghostStyle = alphas × xselAlphaScale (0.55, tuner-
patchable); new exports kicadCollabGetSelectionFull / TestGetCrossMapped /
TestSelectComponent (skips power symbols — PWR_FLAG has no footprint) +
merged-image dispatch
- tests: presence suites extended (payload shape, ghost render pixel tests,
13/13) + new two-tab tests/web/cross-probe.spec.ts (passing vs real
partykit); eeschema pixel compares now target the #glcanvas-* GAL panel
(the whole-window #canvas compare flaked on the auto-dismissing version
infobar — also fixes the long-known presence-eeschema restore flake);
cross-app + presence-kicad vitest suites
Spec: docs/features/collab-presence/0006 (closed repo).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lg5jwWuhFH5dL8hEcBDuP2
2026-07-07 19:30:18 +02:00
|
|
|
// Cross-app selection (0006).
|
|
|
|
|
function("kicadCollabGetSelectionFull", &schCollabGetSelectionFull);
|
|
|
|
|
function("kicadCollabTestGetCrossMapped", &schCollabTestGetCrossMapped);
|
feat(collab): selection soft-locks — remote-selected items can't be dragged locally (collab-presence 0007)
While a peer has an item selected, local users can still select it for
inspection but move/drag/rotate/delete skip it with an infobar naming the
holder (native locked-item UX; enforced via the fork's PCBJAM_REMOTE_LOCK
query — kicad 81f9cd80fd, the epic's first fork-touching phase). Overlapping
holds (both grabbed inside the awareness propagation window) tie-break
deterministically: lowest (user.id, clientID) keeps the item, every losing
client auto-releases it.
- lock-tiebreak.ts: pure policy — beats(), remoteLocks() (union of ALL other
clients' selections incl. own user's other tabs, minus own-held-and-winning
uuids so the winner isn't blocked mid-release), contestedReleases()
- presence.ts: clients() (per-client view, no user dedupe) + self(); FIX for
a pre-existing flaky stack overflow — resolveCollision re-entered itself
synchronously via its own patch's awareness 'change' and could ping-pong on
stale same-user states (~1-in-3 unit runs); re-entrancy guard defers
re-resolution to the next genuine delivery
- presence-kicad.ts: locks ride the kicadCollabSetRemote snapshot
(`locks:[{uuid,name}]`); losing overlaps call kicadCollabReleaseSelection
- wasm bindings (both TUs + merged dispatch): g_locks map + fork query
install; kicadCollabReleaseSelection (cancelInteractive only when a tool
stack is live — bare ESC would clear the whole selection — then selective
RemoveItemFromSel + infobar + forced re-emit); kicadCollabTestGetLocked
- tests: lock-tiebreak unit suite; presence-locks e2e for both editors (real
move veto — pcbnew click+M hotkey since its default left-drag is
rubber-band select, eeschema real drag — each with an unlocked control);
two-tab tests/web/locks.spec.ts (lock propagation + deterministic tiebreak
release + unlock on clear, passing vs real partykit)
Spec: docs/features/collab-presence/0007 (closed repo).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lg5jwWuhFH5dL8hEcBDuP2
2026-07-07 20:35:26 +02:00
|
|
|
// Selection soft-locks (0007).
|
|
|
|
|
function("kicadCollabReleaseSelection", &schCollabReleaseSelection);
|
|
|
|
|
function("kicadCollabTestGetLocked", &schCollabTestGetLocked);
|
feat(collab): presence P3 — eeschema port (collab-presence 0003)
- eeschema_embind.cpp: presence section (0002 pattern, zero fork changes) —
wx canvas triggers + SCHEMATIC_LISTENER piggyback → post-settle
SCH_SELECTION_TOOL emit; throttled cursor; remote VIEW_OVERLAY render
(SCHEMATIC::ResolveItem, name tags, screen-constant via GAL matrix);
schCollab{PresenceStart,SetRemote,GetViewport,GetSelection,TestSelectFirst,
TestClearSelection}; kicad_editor_embind dispatches by active frame.
- sheet-manager: parked rooms carry SKELETON awareness states
({user,tool,sheetPath=bound sheet}) via publishSkeletons on switch + late
warm-up — the bound room's awareness then holds every project peer, so no
multi-room aggregation; presence.ts publishSkeleton helper.
- PresenceRoster: sheet-aware — peers on another sheet render dimmed with
'on <sheet>' tooltip; WasmTool un-gates the kicad presence bridge for
eeschema and threads activeSheetPath.
- tests: presence-eeschema.spec.ts (5 e2e, mirrors pcbnew incl. the px/IU
band at eeschema's 1e4/mm IU); presence.test.ts +2 (skeleton visibility,
no ghost cursor after rebind). eeschema-collab/subschema stay green.
Verified live: two tabs on demo.kicad_sch — peer cursor cross + label,
selection box + name tag, roster avatar.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
2026-07-06 17:05:15 +02:00
|
|
|
function("kicadCollabTestSelectFirst", &schCollabTestSelectFirst);
|
feat(collab): cross-app selection — eeschema symbol ⇄ pcbnew footprint ghost highlight (collab-presence 0006)
Selecting a symbol in eeschema ghost-highlights the linked footprint(s) in
every pcbnew tab of the project, and vice versa — across users AND one
user's own two tabs. Native KIWAY cross-probe is inert in WASM (one frame
per page); this rides the presence layer instead.
- cross-app.ts: project-wide awareness-only room (presenceRoomId), publishes
full PresenceState at selection rate (cursor always null); peers() = other-
TOOL clients incl. own user's other tabs; window.__pcbjamCrossApp test handle
- presence-kicad.ts: parseSelectionEmit (bare array | {uuids,fpPaths}),
xselFromPeerState (pcbnew paths → symbol uuids; eeschema uuids verbatim),
cross peers appended to the kicadCollabSetRemote snapshot as
{id "<user>#x<client>", name "<user> · sch|pcb", xsel}
- C++ (zero fork changes): pcbnew emits {uuids, fpPaths} (FOOTPRINT::GetPath)
and ghost-renders xsel via path-tail suffix scan; eeschema resolves xsel via
ResolveItem gated to the CURRENT sheet (xsel arrives project-wide, unlike
room-scoped selections); ghostStyle = alphas × xselAlphaScale (0.55, tuner-
patchable); new exports kicadCollabGetSelectionFull / TestGetCrossMapped /
TestSelectComponent (skips power symbols — PWR_FLAG has no footprint) +
merged-image dispatch
- tests: presence suites extended (payload shape, ghost render pixel tests,
13/13) + new two-tab tests/web/cross-probe.spec.ts (passing vs real
partykit); eeschema pixel compares now target the #glcanvas-* GAL panel
(the whole-window #canvas compare flaked on the auto-dismissing version
infobar — also fixes the long-known presence-eeschema restore flake);
cross-app + presence-kicad vitest suites
Spec: docs/features/collab-presence/0006 (closed repo).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lg5jwWuhFH5dL8hEcBDuP2
2026-07-07 19:30:18 +02:00
|
|
|
function("kicadCollabTestSelectComponent", &schCollabTestSelectComponent);
|
e2e/CI: dual-engine suites, per-engine screenshots, SwiftShader retired, prod web suite, CI-coverage gate
Squash of experiment/ff-big-modules vs main.
Big-module routing removed: native-EH shrank kicad_editor below
SpiderMonkey's x86-64 code budget (runs 29355049705/29356152413 green on
stock Firefox), so BIG_MODULE_SPECS routing and the baseline-only-JIT
crutch are gone — kicad-firefox and kicad-chromium both run the full
suite, with the module compiled the way real users' browsers compile it.
Per-engine screenshots end to end: stableShot/shotPath write
test-results/<engine>/<name>.png; baselines move to
baseline-screenshots/{chromium,firefox}/ and the whole tools/screenshots
pipeline (compare/promote/manifest/spec-map/changelog/Discord) keys on
<engine>/<name>. Previously Firefox and Chromium renders of one spec
overwrote each other and Firefox renders were never actually gated.
Seeded from CI run 29421380806 (92 new firefox baselines, +24 chromium
web-suite shots); manifest generated from the baseline tree.
One merged playwright.config.ts (kicad/asyncify/coroutine/perf as
projects); ~25 dead npm scripts dropped. The web suite is gated in CI for
the first time ever (4 rotted specs fixed, 5 broken lib-bridge specs
triaged as fixme in docs/features/web-e2e-rot/); cheap lint step after
npm ci; last 26 blind-sleep violations fixed.
SwiftShader retired: CI Chromium renders WebGL on ANGLE → Mesa llvmpipe
(--use-gl=angle --use-angle=gl --ignore-gpu-blocklist; the blocklist flag
is mandatory — llvmpipe is blocklisted and WebGL is silently unavailable
without it) in BOTH configs. Under WORKERS=4 congestion SwiftShader
transiently failed the first post-board-load draw and the recovery
cascade ended in a silent permanent Cairo fallback — that engine flip was
the "~1.2% changedRatio both directions" occ-export baseline flake.
Validated 160/160 across two 80-repeat rigs; full analysis in
docs/features/wx-parity-bugs/occ-export-context-eviction.md. Chromium
baselines shift slightly on llvmpipe — promote once from the first green
run. Deflakes the new coverage exposed: presence baselines settle before
capture; presence fixtures declare current file formats; perf gets its
own outputDir so CI evidence survives; occ-export settles the board paint
before the export dialog; menu-item waits (waitForRenderedByLabel before
clickMenuItem) in 4 specs + the TESTING.md rule.
Web suite runs the PROD build, in parallel: webServer becomes backend
`start` + the standalone's e2e:preview (build-preview.mjs: link-wasm →
stash the public/wasm symlink aside during vite build, build-demo.mjs's
move — then vite preview as the persistent server). The wasm middleware
serves /wasm/* in preview and emits COOP/COEP/CORP itself (a pthread
worker script's own response must carry COEP or Chrome kills it with
ERR_BLOCKED_BY_RESPONSE). VITE_* flags bake at build time;
VITE_ALLOW_USER_OVERRIDE joins turbo globalEnv. fullyParallel + default
workers: 5.2m → 1.4m. Determinism fixes the parallel run exposed:
shared-page specs become serial groups; locks.spec grabs alice's exact
item via the new kicadCollabTestSelectByUuid hook (cross-tab "first
footprint" order is not a ysync invariant); quit specs poll page.url()
(quit supersedes its own navigation — NS_BINDING_ABORTED on Firefox).
Suite: 51 passed / 12 skipped / 0 failed in 1.6m.
CI-coverage gate (lint:ci-coverage): every tests/**/*.spec.ts must be
reachable from the npm scripts the workflows invoke — scraped from
.github/workflows/, resolved through package.json, coverage asked from
playwright --list itself. Rules: uncovered-spec + orphan-project (with a
documented LOCAL_ONLY_PROJECTS allowlist). Gating next to
lint:determinism; 138 spec files / 13 projects accounted for.
Product fixes kept from the investigations (reachable on real GPUs too):
wx 7799fd1be5 — paint flags clear before dispatch + Invalidate always
propagates; kicad 3dcfea5e45 — SwiftShader pass-boundary flush +
per-instance font texture + first-frame GL-error drain (GAL recovery
recovers instead of falling back to Cairo) + the user-facing eeschema
switch navigates again under __EMSCRIPTEN__ (project-sync's
FaceRegistered gate had rerouted it into the hidden sync player; caught
by the newly-gated web suite).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018eUxiPApHgGiu9NFyQfhAq
2026-07-17 12:10:40 +02:00
|
|
|
function("kicadCollabTestSelectByUuid", &schCollabTestSelectByUuid);
|
feat(collab): presence P3 — eeschema port (collab-presence 0003)
- eeschema_embind.cpp: presence section (0002 pattern, zero fork changes) —
wx canvas triggers + SCHEMATIC_LISTENER piggyback → post-settle
SCH_SELECTION_TOOL emit; throttled cursor; remote VIEW_OVERLAY render
(SCHEMATIC::ResolveItem, name tags, screen-constant via GAL matrix);
schCollab{PresenceStart,SetRemote,GetViewport,GetSelection,TestSelectFirst,
TestClearSelection}; kicad_editor_embind dispatches by active frame.
- sheet-manager: parked rooms carry SKELETON awareness states
({user,tool,sheetPath=bound sheet}) via publishSkeletons on switch + late
warm-up — the bound room's awareness then holds every project peer, so no
multi-room aggregation; presence.ts publishSkeleton helper.
- PresenceRoster: sheet-aware — peers on another sheet render dimmed with
'on <sheet>' tooltip; WasmTool un-gates the kicad presence bridge for
eeschema and threads activeSheetPath.
- tests: presence-eeschema.spec.ts (5 e2e, mirrors pcbnew incl. the px/IU
band at eeschema's 1e4/mm IU); presence.test.ts +2 (skeleton visibility,
no ghost cursor after rebind). eeschema-collab/subschema stay green.
Verified live: two tabs on demo.kicad_sch — peer cursor cross + label,
selection box + name tag, roster avatar.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
2026-07-06 17:05:15 +02:00
|
|
|
function("kicadCollabTestClearSelection", &schCollabTestClearSelection);
|
libs: peer lib edits reach the running editor + "placed symbol updated" toast
synced-source subscribes to its SyncStack: remote changes (self-save echoes
consumed via a selfPushed flag) debounce per kind into kicadLibsReload — a
new embind export (pcbjam_libs_reload.h, all three TUs) that drops the lib's
plugin cache (LIBRARY_MANAGER::ReloadLibraryEntry), reloads it, and mails
MAIL_RELOAD_LIB with the nickname so the symbol tree force-refreshes (the
plugin's modify hash is a pinned constant, so a plain sync would skip it).
After the reload, kicadLibsSymbolUsage (new eeschema embind: placed
SCH_SYMBOL count across unique screens) gates LIB_ITEM_UPDATED_EVENT, and
WasmTool shows an amber toast when a PLACED symbol changed — placed copies
keep the previous version until updated from the library.
syncedScopeLibsSource gives PROJECT sessions the synced source under
VITE_LIBS_SOURCE=synced (remote contract for lib listing/createLib, lazy
per-lib SyncStacks for item ops/presync) so realtime reaches open
schematics; previously project sessions silently fell back to the per-item
remote source. Unit tests cover reload debounce, self-echo skip, per-kind
routing, usage-gated event, and the no-Module no-op.
Bumps kicad (MAIL_RELOAD_LIB force-refresh payload).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013QRWoXiM9uuo1enXGhAYku
2026-07-09 18:21:21 +02:00
|
|
|
// Library reload after a remote (synced) lib edit — r2-idb-sync realtime.
|
jspi: migration phases 0-7 — build knob, scheduler shim, test successor suite
Toolchain: emsdk 6.0.6 (versions.sh; cache-hash keys on it). Build knob
PCBJAM_ASYNC_BACKEND=jspi|asyncify: build-kicad-target.sh links editors with
-sJSPI + -sJSPI_EXPORTS=@scripts/common/jspi-exports.txt + --pre-js
jspi-scheduler.js (no DYNCALLS, no post-link asyncify pipeline); wx build
stamps the backend and forces clean on flip or unknown provenance;
docker/build.sh passes the knob, seeds the emscripten ports cache from the
volume every launch, jspi postprocess = patch-env-shim only.
scripts/common/shims/jspi-scheduler.js: the JSPI successor scheduler —
token-wait registry, resume turnstile (one armed resume between engine
re-entries, SP swaps only at microtask boundaries), green-region spill
stacks (16-aligned tops), S1 embind mutator FIFO lane + parker wraps, S6
shutdown, libctx integration hooks (suspend/end/quarantine + g_current
arm/clear), SuspendError attributor, lost-wake + stuck-window watchdogs,
__wxWaitDump observability.
Embind: PARKER registrations get emscripten::async() under PCBJAM_JSPI
(wasm/bindings/pcbjam_async_policy.h). nanosleep yields route via the shim.
Tests: tests/asyncify -> tests/jspi successor suite (jspi-stack red/green
shadow-stack battery, jspi-coroutine MiniCoro harness, suspend-races
semantic scenarios + __wxWaitDump books coherence); projects jspi-firefox/
jspi-chrome (asyncify-webkit retired — no JSPI in WebKit); unconditional
Firefox JSPI pref; guard-beacons -> wait-beacons (+wxScheduler/libctxJspi
families); Makefile.wasm links test apps against JSPI with the shim as a
tracked link prerequisite.
Web: WasmTool setRo await + __wxWaitDump forensics, open-flow contained
promise, scheduler-shim.test.ts retargeted (8 green).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NDeBaKKhQztd8KiVtHuyXr
2026-08-13 07:06:24 +02:00
|
|
|
function("kicadLibsReload", &pcbjam_libs::reloadLibrary PCBJAM_PARKER_POLICY);
|
2026-08-19 13:47:13 +02:00
|
|
|
// Runtime lib-table row insert + load (a new team library appeared
|
|
|
|
|
// mid-session; the lib set is otherwise frozen at boot).
|
|
|
|
|
function("kicadLibsAddEntry", &pcbjam_libs::addLibraryEntry PCBJAM_PARKER_POLICY);
|
libs: peer lib edits reach the running editor + "placed symbol updated" toast
synced-source subscribes to its SyncStack: remote changes (self-save echoes
consumed via a selfPushed flag) debounce per kind into kicadLibsReload — a
new embind export (pcbjam_libs_reload.h, all three TUs) that drops the lib's
plugin cache (LIBRARY_MANAGER::ReloadLibraryEntry), reloads it, and mails
MAIL_RELOAD_LIB with the nickname so the symbol tree force-refreshes (the
plugin's modify hash is a pinned constant, so a plain sync would skip it).
After the reload, kicadLibsSymbolUsage (new eeschema embind: placed
SCH_SYMBOL count across unique screens) gates LIB_ITEM_UPDATED_EVENT, and
WasmTool shows an amber toast when a PLACED symbol changed — placed copies
keep the previous version until updated from the library.
syncedScopeLibsSource gives PROJECT sessions the synced source under
VITE_LIBS_SOURCE=synced (remote contract for lib listing/createLib, lazy
per-lib SyncStacks for item ops/presync) so realtime reaches open
schematics; previously project sessions silently fell back to the per-item
remote source. Unit tests cover reload debounce, self-echo skip, per-kind
routing, usage-gated event, and the no-Module no-op.
Bumps kicad (MAIL_RELOAD_LIB force-refresh payload).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013QRWoXiM9uuo1enXGhAYku
2026-07-09 18:21:21 +02:00
|
|
|
// Placed-instance count for a library symbol (drives the "a symbol you are
|
|
|
|
|
// using was updated" toast after a remote lib edit).
|
|
|
|
|
function("kicadLibsSymbolUsage", &schLibsSymbolUsage);
|
2026-08-25 17:58:54 +02:00
|
|
|
// Update placed symbols from the library (libs 0017 §2c).
|
|
|
|
|
function("kicadUpdateFromLibrary", &schUpdateFromLibraryShim);
|
2026-07-02 14:48:11 +02:00
|
|
|
#endif // !KICAD_MERGED_EMBIND
|
2026-06-01 16:30:29 +02:00
|
|
|
}
|
|
|
|
|
#endif
|