2026-07-02 14:48:11 +02:00
|
|
|
/*
|
|
|
|
|
* Embind dispatcher for the merged kicad_editor WASM image (editor-unification Part 2).
|
|
|
|
|
*
|
|
|
|
|
* The pcbnew and eeschema binding TUs each implement the collab bridge for their own
|
|
|
|
|
* frame and, standalone, register the SAME JS-facing names. In the merged image both
|
|
|
|
|
* TUs are compiled with -DKICAD_MERGED_EMBIND, which
|
|
|
|
|
* - compiles out their duplicate frame-agnostic definitions (kicadOpenFile,
|
|
|
|
|
* extern "C" kicadCollabOnSave) — the single definitions live HERE, and
|
|
|
|
|
* - compiles out their shared-name EMSCRIPTEN_BINDINGS registrations — registered
|
|
|
|
|
* once HERE, dispatching to the renamed per-editor entries (pcbCollab… and
|
|
|
|
|
* schCollab…) on whichever editor frame is live. Per-editor unique names
|
|
|
|
|
* (kicadSaveBoard, kicadSaveSchematic, kicadCollabTestItemBlob, Board_…) keep
|
|
|
|
|
* flowing from the per-editor blocks unchanged.
|
|
|
|
|
*
|
|
|
|
|
* With the one-frame-per-page-load model exactly one of pcbEditorActive() /
|
|
|
|
|
* schEditorActive() is true — the same dynamic_cast probe every per-editor entry
|
|
|
|
|
* already starts with. JS-facing names and signatures are IDENTICAL to the standalone
|
|
|
|
|
* bundles, so the web app and tests need no per-bundle API differences.
|
|
|
|
|
*
|
|
|
|
|
* Deliberately header-light: no pcbnew/eeschema headers (avoids mixing both include
|
|
|
|
|
* roots in one TU); only the generic KIWAY_PLAYER surface + the extern declarations.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
#ifdef __EMSCRIPTEN__
|
|
|
|
|
#include <emscripten.h>
|
|
|
|
|
#include <emscripten/bind.h>
|
feat(mobile): Figma-like hide-UI toggle — Cmd/Ctrl+\ hotkey + floating button, snapshot-exact chrome restore
Chrome visibility is now a runtime toggle on any device instead of being
device-wired: mobile defaults to canvas-only, desktop to full UI, and the
floating top-right button (or Figma's Cmd/Ctrl+\ chord — free in KiCad,
only bare \ is bound) flips between them live.
- chrome-visibility.ts: module-global store (default isMobileMode(),
session-only) + pure hotkey matcher (rejects AltGr backslash + repeats)
- WasmTool: capture-phase hotkey (stopped before the wx layer), floating
toggle pill (matches the comment FAB design), useLayoutEffect apply with
sync first call + retry; overlays follow the toggle, capability-gated on
the kicad_editor bundle's kicadSetChrome export
- boot.ts mobile opt now installs touch gestures only
- kicadSetChrome: frame-keyed hide-time snapshot so restore re-shows ONLY
what hide took away (blanket Show(true) surfaced KiCad's default-hidden
Search/Properties/Net-Inspector panes); toolbars-only fallback otherwise
- tests: chrome-toggle.spec.ts (desktop hide/restore + geometric
restore-exactness ±3px), mobile toggle round-trip, 12 new unit tests,
test:web:mobile npm script
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 16:11:19 +02:00
|
|
|
#include <algorithm>
|
2026-08-09 16:32:53 +02:00
|
|
|
#include <memory>
|
2026-07-02 14:48:11 +02:00
|
|
|
#include <string>
|
|
|
|
|
#include <vector>
|
|
|
|
|
#include <wx/app.h>
|
|
|
|
|
#include <wx/string.h>
|
|
|
|
|
#include <wx/window.h>
|
2026-07-06 16:10:52 +02:00
|
|
|
#include <wx/frame.h>
|
|
|
|
|
#include <wx/menu.h>
|
|
|
|
|
#include <wx/statusbr.h>
|
|
|
|
|
#include <wx/aui/framemanager.h>
|
2026-07-02 14:48:11 +02:00
|
|
|
#include <kiway.h>
|
|
|
|
|
#include <kiway_player.h>
|
2026-07-10 20:27:04 +02:00
|
|
|
#include <pcbjam_read_only.h>
|
|
|
|
|
#include <project.h>
|
2026-07-02 14:48:11 +02:00
|
|
|
|
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"
|
2026-07-30 14:17:48 +02:00
|
|
|
#include "open_gate.h"
|
2026-08-10 09:58:28 +02:00
|
|
|
#include "main_stack_runner.h"
|
2026-07-31 20:34:32 +02:00
|
|
|
#include "timer_park.h"
|
fix(async): fiber resume guard — the prod board-load trap, red/green
Companion to kicad f0ce20ef64 (libcontext swap_suspended guard), which this
pins. The v0.1.20 diagnostics decoded the crash that survived v0.1.13–19:
TOOL_MANAGER Resume()s a coroutine whose body is asyncify-parked inside
handleSleep, the swap rewinds the stale fiber suspension, and the runtime is
poisoned. Full chain of evidence in docs/features/async/16-fiber-resume-guard.md
(+ round-3 addendum in 15-timer-park-repro.md).
- wasm/bindings/fiber_park.h + kicadTestFiberPark{Start,Prime,Poke,State}
exports (pcbnew + merged kicad_editor): stages Call→yield→legitimate
resume→sleep park→mid-park Resume, the exact prod state machine. The
first yield matters: it primes a real (then stale) suspension, matching
long-lived tool loops rather than a first-slice park.
- tests/kicad/fiber-resume-park.spec.ts: asserts the healthy contract on
polled state only (embind returns across fiber swaps are unwind
placeholders). RED on the unguarded build — fiber/sleep buffer
cross-restores, a jump-ghost beacon, the parked body zombified. GREEN with
the guard: mid-park poke refused ([collab-fcontext] jump-refused beacon),
park completes, post-yield resume works, no trap signatures.
- Regression sweep green: timer-park-repro, collab-load-fuzz, load-pcb,
pcbnew-collab, collab-undo, eeschema-collab (19 passed).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SE4o46Lnq3hF574FFq8x4
2026-07-31 23:37:05 +02:00
|
|
|
#include "fiber_park.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
|
|
|
|
2026-07-02 14:48:11 +02:00
|
|
|
using namespace emscripten;
|
|
|
|
|
|
|
|
|
|
// Per-editor entry points and frame probes — defined (with external linkage) in
|
|
|
|
|
// pcbnew_embind.cpp / eeschema_embind.cpp.
|
|
|
|
|
bool pcbEditorActive();
|
|
|
|
|
void pcbCollabApply( std::string aJson );
|
|
|
|
|
void pcbCollabApplyItems( std::string aJson );
|
|
|
|
|
std::string pcbCollabSnapshot();
|
|
|
|
|
std::string pcbCollabSnapshotItems();
|
|
|
|
|
std::string pcbCollabTestMoveFirst( int aDx, int aDy );
|
|
|
|
|
std::string pcbCollabGetPos( std::string aId );
|
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
|
|
|
bool pcbCollabTestRemoveItem( std::string aId );
|
|
|
|
|
bool pcbCollabTestRotateItem( std::string aId, double aDeg );
|
2026-07-08 11:09:26 +02:00
|
|
|
// Collab-aware undo (ysync miss 09).
|
|
|
|
|
bool pcbCollabTestUndo();
|
|
|
|
|
int pcbCollabTestUndoDepth();
|
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
|
|
|
// Presence (collab-presence 0002) + comment pins/panning (0005).
|
2026-07-06 15:59:51 +02:00
|
|
|
void pcbCollabPresenceStart();
|
|
|
|
|
void pcbCollabSetRemote( std::string 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
|
|
|
void pcbCollabSetPins( std::string aJson );
|
|
|
|
|
void pcbCollabSetViewport( double aCx, double aCy );
|
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).
|
|
|
|
|
void pcbCollabFitViewport( double aCx, double aCy, double aHalfW, double aHalfH );
|
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
|
|
|
void pcbCollabSetStyle( std::string aJson );
|
|
|
|
|
std::string pcbCollabTestListItems( int aCount );
|
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
|
|
|
std::string pcbCollabTestDemoSet();
|
2026-07-06 15:59:51 +02:00
|
|
|
std::string pcbCollabGetViewport();
|
|
|
|
|
std::string pcbCollabGetSelection();
|
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 (collab-presence 0006).
|
|
|
|
|
std::string pcbCollabGetSelectionFull();
|
|
|
|
|
std::string pcbCollabTestGetCrossMapped();
|
|
|
|
|
std::string pcbCollabTestSelectComponent();
|
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
|
|
|
bool pcbCollabTestSelectByUuid( std::string aUuid );
|
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 (collab-presence 0007).
|
|
|
|
|
void pcbCollabReleaseSelection( std::string aUuidsJson, std::string aHolder );
|
|
|
|
|
std::string pcbCollabTestGetLocked();
|
2026-07-06 15:59:51 +02:00
|
|
|
std::string pcbCollabTestSelectFirst();
|
|
|
|
|
bool pcbCollabTestClearSelection();
|
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).
|
|
|
|
|
void pcbSetColorTheme( std::string aTheme );
|
|
|
|
|
void pcbSetDarkChrome( bool aDark );
|
2026-07-02 14:48:11 +02:00
|
|
|
|
2026-07-27 18:48:25 +02:00
|
|
|
// Post-theme-apply hook shared with pcbjam_theme.h — identical inline-variable
|
|
|
|
|
// definition instead of including that header (this TU stays header-light);
|
|
|
|
|
// keep the two declarations in sync.
|
|
|
|
|
namespace pcbjam_theme
|
|
|
|
|
{
|
|
|
|
|
inline void ( *g_afterThemeApplied )() = nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-02 14:48:11 +02:00
|
|
|
bool schEditorActive();
|
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 );
|
2026-07-02 14:48:11 +02:00
|
|
|
void schCollabApply( std::string aJson );
|
|
|
|
|
void schCollabApplyItems( std::string aJson );
|
|
|
|
|
std::string schCollabSnapshot();
|
|
|
|
|
std::string schCollabSnapshotItems();
|
|
|
|
|
std::string schCollabTestMoveFirst( int aDx, int aDy );
|
|
|
|
|
std::string schCollabGetPos( std::string aId );
|
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
|
|
|
bool schCollabTestRemoveItem( std::string aId );
|
|
|
|
|
bool schCollabTestRotateItem( std::string aId, double aDeg );
|
2026-07-08 11:09:26 +02:00
|
|
|
// Collab-aware undo (ysync miss 09).
|
|
|
|
|
bool schCollabTestUndo();
|
|
|
|
|
int schCollabTestUndoDepth();
|
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
|
|
|
// Presence (collab-presence 0003 — eeschema counterparts) + pins (0005).
|
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();
|
|
|
|
|
void schCollabSetRemote( std::string 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
|
|
|
void schCollabSetPins( std::string aJson );
|
|
|
|
|
void schCollabSetViewport( double aCx, double aCy );
|
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).
|
|
|
|
|
void schCollabFitViewport( double aCx, double aCy, double aHalfW, double aHalfH );
|
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
|
|
|
void schCollabSetStyle( std::string aJson );
|
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).
|
|
|
|
|
void schSetColorTheme( std::string aTheme );
|
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
|
|
|
std::string schCollabTestListItems( int aCount );
|
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
|
|
|
std::string 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
|
|
|
std::string schCollabGetViewport();
|
|
|
|
|
std::string 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 (collab-presence 0006).
|
|
|
|
|
std::string schCollabGetSelectionFull();
|
|
|
|
|
std::string schCollabTestGetCrossMapped();
|
|
|
|
|
std::string 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
|
|
|
bool schCollabTestSelectByUuid( std::string aUuid );
|
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 (collab-presence 0007).
|
|
|
|
|
void schCollabReleaseSelection( std::string aUuidsJson, std::string aHolder );
|
|
|
|
|
std::string 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
|
|
|
std::string schCollabTestSelectFirst();
|
|
|
|
|
bool schCollabTestClearSelection();
|
2026-07-02 14:48:11 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
// Programmatically open a project file in the running editor frame, without UI
|
|
|
|
|
// automation. Frame-agnostic (any KIWAY_PLAYER); byte-identical to the definition the
|
|
|
|
|
// standalone bundles compile from their own binding TU.
|
|
|
|
|
static bool kicadOpenFile( std::string path )
|
|
|
|
|
{
|
2026-07-30 14:17:48 +02:00
|
|
|
// Held across every Asyncify park of the load; see open_gate.h.
|
|
|
|
|
pcbjam_open::BusyGuard busy;
|
|
|
|
|
|
|
|
|
|
if( pcbjam_open::testParkMs() > 0 )
|
|
|
|
|
emscripten_sleep( pcbjam_open::testParkMs() );
|
|
|
|
|
|
2026-07-02 14:48:11 +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-07-02 14:48:11 +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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-09 16:32:53 +02:00
|
|
|
// Phase F (docs/features/async/22 §10, the awaited-ccall entry class): the
|
|
|
|
|
// open body above, driven from a DISPATCH CONTEXT instead of the main stack.
|
|
|
|
|
// Run there, every wait inside the load parks the context through the
|
|
|
|
|
// registry — the main stack never parks in place, which is the last
|
|
|
|
|
// production member of the overlapped-wake class the D-on beacon sweep named.
|
2026-08-09 17:44:33 +02:00
|
|
|
//
|
|
|
|
|
// THE TOKEN IS PASSED IN, NOT RETURNED. Running the job on a dispatch context
|
|
|
|
|
// Asyncify-suspends THIS embind frame while the load parks, so any return
|
|
|
|
|
// value is delivered as an unwind PLACEHOLDER (0) into a rewind JS discards —
|
|
|
|
|
// the same gotcha the fiber-park levers document. So the shim wrapper mints
|
|
|
|
|
// the wait token in pure JS (no swap), hands it in here, and awaits its
|
|
|
|
|
// promise; this starter returns void and its own placeholder return is
|
|
|
|
|
// harmless. The job resolves the token when the load completes.
|
2026-08-09 16:32:53 +02:00
|
|
|
extern "C" void wxWasmRunOnDispatchContext( void ( *fn )( void* ), void* arg );
|
|
|
|
|
extern "C" void wxWasmResolveWait( int aToken, int aResult );
|
|
|
|
|
|
|
|
|
|
namespace
|
|
|
|
|
{
|
|
|
|
|
struct OPEN_JOB
|
|
|
|
|
{
|
|
|
|
|
std::string path;
|
|
|
|
|
int token;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
void kicadOpenFileJob( void* aArg )
|
|
|
|
|
{
|
|
|
|
|
std::unique_ptr<OPEN_JOB> job( static_cast<OPEN_JOB*>( aArg ) );
|
|
|
|
|
const bool ok = kicadOpenFile( job->path );
|
|
|
|
|
wxWasmResolveWait( job->token, ok ? 1 : 0 );
|
|
|
|
|
}
|
|
|
|
|
} // namespace
|
|
|
|
|
|
2026-08-09 17:44:33 +02:00
|
|
|
static void kicadOpenFileStart( int token, std::string path )
|
2026-08-09 16:32:53 +02:00
|
|
|
{
|
|
|
|
|
wxWasmRunOnDispatchContext( &kicadOpenFileJob, new OPEN_JOB{ std::move( path ), token } );
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-30 14:17:48 +02:00
|
|
|
// JS-pollable open-in-flight probe (open_gate.h): the web shell defers the
|
|
|
|
|
// collab/presence attach until the open chain has truly completed.
|
|
|
|
|
static bool kicadOpenFileBusy()
|
|
|
|
|
{
|
|
|
|
|
return pcbjam_open::busy();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Test-only (collab-load-fuzz): arm the deterministic open parks.
|
|
|
|
|
static void kicadTestSetOpenPark( int aMs )
|
|
|
|
|
{
|
|
|
|
|
pcbjam_open::testParkMs() = aMs;
|
2026-07-02 14:48:11 +02:00
|
|
|
}
|
|
|
|
|
|
2026-07-31 20:34:32 +02:00
|
|
|
// Test-only (timer-park repro, timer_park.h): a one-shot wx timer whose
|
|
|
|
|
// Notify() Asyncify-parks — the deterministic concurrent-park window.
|
|
|
|
|
static bool kicadTestArmTimerPark( int aDelayMs, int aParkMs )
|
|
|
|
|
{
|
|
|
|
|
return pcbjam_timer_park::arm( aDelayMs, aParkMs );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static std::string kicadTestTimerParkState()
|
|
|
|
|
{
|
|
|
|
|
return pcbjam_timer_park::stateJson();
|
|
|
|
|
}
|
|
|
|
|
|
fix(async): fiber resume guard — the prod board-load trap, red/green
Companion to kicad f0ce20ef64 (libcontext swap_suspended guard), which this
pins. The v0.1.20 diagnostics decoded the crash that survived v0.1.13–19:
TOOL_MANAGER Resume()s a coroutine whose body is asyncify-parked inside
handleSleep, the swap rewinds the stale fiber suspension, and the runtime is
poisoned. Full chain of evidence in docs/features/async/16-fiber-resume-guard.md
(+ round-3 addendum in 15-timer-park-repro.md).
- wasm/bindings/fiber_park.h + kicadTestFiberPark{Start,Prime,Poke,State}
exports (pcbnew + merged kicad_editor): stages Call→yield→legitimate
resume→sleep park→mid-park Resume, the exact prod state machine. The
first yield matters: it primes a real (then stale) suspension, matching
long-lived tool loops rather than a first-slice park.
- tests/kicad/fiber-resume-park.spec.ts: asserts the healthy contract on
polled state only (embind returns across fiber swaps are unwind
placeholders). RED on the unguarded build — fiber/sleep buffer
cross-restores, a jump-ghost beacon, the parked body zombified. GREEN with
the guard: mid-park poke refused ([collab-fcontext] jump-refused beacon),
park completes, post-yield resume works, no trap signatures.
- Regression sweep green: timer-park-repro, collab-load-fuzz, load-pcb,
pcbnew-collab, collab-undo, eeschema-collab (19 passed).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SE4o46Lnq3hF574FFq8x4
2026-07-31 23:37:05 +02:00
|
|
|
// Test-only (fiber-resume-park repro, fiber_park.h): Resume() into an
|
|
|
|
|
// asyncify-parked coroutine — the decoded prod board-load trap.
|
|
|
|
|
static bool kicadTestFiberParkStart( int aParkMs )
|
|
|
|
|
{
|
|
|
|
|
return pcbjam_fiber_park::start( aParkMs );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static bool kicadTestFiberParkPrime()
|
|
|
|
|
{
|
|
|
|
|
return pcbjam_fiber_park::prime();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static bool kicadTestFiberParkPoke()
|
|
|
|
|
{
|
|
|
|
|
return pcbjam_fiber_park::poke();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static std::string kicadTestFiberParkState()
|
|
|
|
|
{
|
|
|
|
|
return pcbjam_fiber_park::stateJson();
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-01 10:05:42 +02:00
|
|
|
static bool kicadTestFiberParkStartSecond()
|
|
|
|
|
{
|
|
|
|
|
return pcbjam_fiber_park::startSecond();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static bool kicadTestFiberParkPokeSecond()
|
|
|
|
|
{
|
|
|
|
|
return pcbjam_fiber_park::pokeSecond();
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-02 14:48:11 +02:00
|
|
|
|
feat(mobile): Figma-like hide-UI toggle — Cmd/Ctrl+\ hotkey + floating button, snapshot-exact chrome restore
Chrome visibility is now a runtime toggle on any device instead of being
device-wired: mobile defaults to canvas-only, desktop to full UI, and the
floating top-right button (or Figma's Cmd/Ctrl+\ chord — free in KiCad,
only bare \ is bound) flips between them live.
- chrome-visibility.ts: module-global store (default isMobileMode(),
session-only) + pure hotkey matcher (rejects AltGr backslash + repeats)
- WasmTool: capture-phase hotkey (stopped before the wx layer), floating
toggle pill (matches the comment FAB design), useLayoutEffect apply with
sync first call + retry; overlays follow the toggle, capability-gated on
the kicad_editor bundle's kicadSetChrome export
- boot.ts mobile opt now installs touch gestures only
- kicadSetChrome: frame-keyed hide-time snapshot so restore re-shows ONLY
what hide took away (blanket Show(true) surfaced KiCad's default-hidden
Search/Properties/Net-Inspector panes); toolbars-only fallback otherwise
- tests: chrome-toggle.spec.ts (desktop hide/restore + geometric
restore-exactness ±3px), mobile toggle round-trip, 12 new unit tests,
test:web:mobile npm script
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 16:11:19 +02:00
|
|
|
// Canvas-only chrome toggle (features/mobile): hide/show every AUI pane
|
2026-07-06 16:10:52 +02:00
|
|
|
// except the central draw canvas, plus the menubar and status bar, so the GAL
|
|
|
|
|
// canvas fills the frame. Generic wxFrame/wxAui surface only (keeps this TU
|
|
|
|
|
// header-light and serves both editor frames). Hidden bars release their space
|
|
|
|
|
// because the wasm port's frame client-area math skips !IsShown() bars (native
|
|
|
|
|
// parity, see wxwidgets/src/wasm/frame.cpp). Returns false until the editor
|
|
|
|
|
// frame exists — main() builds it after runtime init — so JS polls this.
|
feat(mobile): Figma-like hide-UI toggle — Cmd/Ctrl+\ hotkey + floating button, snapshot-exact chrome restore
Chrome visibility is now a runtime toggle on any device instead of being
device-wired: mobile defaults to canvas-only, desktop to full UI, and the
floating top-right button (or Figma's Cmd/Ctrl+\ chord — free in KiCad,
only bare \ is bound) flips between them live.
- chrome-visibility.ts: module-global store (default isMobileMode(),
session-only) + pure hotkey matcher (rejects AltGr backslash + repeats)
- WasmTool: capture-phase hotkey (stopped before the wx layer), floating
toggle pill (matches the comment FAB design), useLayoutEffect apply with
sync first call + retry; overlays follow the toggle, capability-gated on
the kicad_editor bundle's kicadSetChrome export
- boot.ts mobile opt now installs touch gestures only
- kicadSetChrome: frame-keyed hide-time snapshot so restore re-shows ONLY
what hide took away (blanket Show(true) surfaced KiCad's default-hidden
Search/Properties/Net-Inspector panes); toolbars-only fallback otherwise
- tests: chrome-toggle.spec.ts (desktop hide/restore + geometric
restore-exactness ±3px), mobile toggle round-trip, 12 new unit tests,
test:web:mobile npm script
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 16:11:19 +02:00
|
|
|
|
|
|
|
|
// Hide-time visibility snapshot. KiCad keeps several panes hidden by default
|
|
|
|
|
// (Search, Properties, Net Inspector, …), so a blanket Show(true) on restore
|
|
|
|
|
// would surface panes the user never had open — restore only what the hide
|
|
|
|
|
// actually took away. Keyed to the frame so a snapshot never leaks onto a
|
|
|
|
|
// different frame's wxAuiManager.
|
|
|
|
|
static struct
|
|
|
|
|
{
|
|
|
|
|
wxFrame* frame = nullptr;
|
|
|
|
|
bool valid = false;
|
|
|
|
|
bool menuShown = false;
|
|
|
|
|
bool statusShown = false;
|
|
|
|
|
std::vector<wxString> paneNames;
|
|
|
|
|
} s_chromeSnap;
|
|
|
|
|
|
|
|
|
|
static bool chromeSkipsPane( const wxAuiPaneInfo& aPane )
|
|
|
|
|
{
|
|
|
|
|
// keep the central editor canvas (named "DrawFrame" in both editors)
|
|
|
|
|
return aPane.dock_direction == wxAUI_DOCK_CENTER || aPane.name == wxT( "DrawFrame" );
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-06 16:10:52 +02:00
|
|
|
static bool kicadSetChrome( bool aShow )
|
|
|
|
|
{
|
|
|
|
|
wxFrame* frame =
|
|
|
|
|
wxTheApp ? dynamic_cast<wxFrame*>( wxTheApp->GetTopWindow() ) : nullptr;
|
|
|
|
|
|
|
|
|
|
if( !frame )
|
|
|
|
|
return false;
|
|
|
|
|
|
feat(mobile): Figma-like hide-UI toggle — Cmd/Ctrl+\ hotkey + floating button, snapshot-exact chrome restore
Chrome visibility is now a runtime toggle on any device instead of being
device-wired: mobile defaults to canvas-only, desktop to full UI, and the
floating top-right button (or Figma's Cmd/Ctrl+\ chord — free in KiCad,
only bare \ is bound) flips between them live.
- chrome-visibility.ts: module-global store (default isMobileMode(),
session-only) + pure hotkey matcher (rejects AltGr backslash + repeats)
- WasmTool: capture-phase hotkey (stopped before the wx layer), floating
toggle pill (matches the comment FAB design), useLayoutEffect apply with
sync first call + retry; overlays follow the toggle, capability-gated on
the kicad_editor bundle's kicadSetChrome export
- boot.ts mobile opt now installs touch gestures only
- kicadSetChrome: frame-keyed hide-time snapshot so restore re-shows ONLY
what hide took away (blanket Show(true) surfaced KiCad's default-hidden
Search/Properties/Net-Inspector panes); toolbars-only fallback otherwise
- tests: chrome-toggle.spec.ts (desktop hide/restore + geometric
restore-exactness ±3px), mobile toggle round-trip, 12 new unit tests,
test:web:mobile npm script
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 16:11:19 +02:00
|
|
|
wxMenuBar* menuBar = frame->GetMenuBar();
|
|
|
|
|
wxStatusBar* statusBar = frame->GetStatusBar();
|
|
|
|
|
wxAuiManager* mgr = wxAuiManager::GetManager( frame );
|
|
|
|
|
|
|
|
|
|
if( !aShow )
|
|
|
|
|
{
|
|
|
|
|
// A repeated hide keeps the original snapshot (idempotent).
|
|
|
|
|
if( !s_chromeSnap.valid || s_chromeSnap.frame != frame )
|
|
|
|
|
{
|
|
|
|
|
s_chromeSnap.frame = frame;
|
|
|
|
|
s_chromeSnap.menuShown = menuBar && menuBar->IsShown();
|
|
|
|
|
s_chromeSnap.statusShown = statusBar && statusBar->IsShown();
|
|
|
|
|
s_chromeSnap.paneNames.clear();
|
|
|
|
|
|
|
|
|
|
if( mgr )
|
|
|
|
|
{
|
|
|
|
|
wxAuiPaneInfoArray& panes = mgr->GetAllPanes();
|
|
|
|
|
|
|
|
|
|
for( size_t i = 0; i < panes.GetCount(); ++i )
|
|
|
|
|
{
|
|
|
|
|
wxAuiPaneInfo& pane = panes.Item( i );
|
|
|
|
|
|
|
|
|
|
if( !chromeSkipsPane( pane ) && pane.IsShown() )
|
|
|
|
|
s_chromeSnap.paneNames.push_back( pane.name );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
s_chromeSnap.valid = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// A show with no snapshot (or one taken on a different frame) falls back
|
|
|
|
|
// to revealing the standard chrome instead of obeying stale state.
|
|
|
|
|
const bool haveSnap = s_chromeSnap.valid && s_chromeSnap.frame == frame;
|
|
|
|
|
|
|
|
|
|
if( menuBar )
|
|
|
|
|
menuBar->Show( aShow && ( haveSnap ? s_chromeSnap.menuShown : true ) );
|
2026-07-06 16:10:52 +02:00
|
|
|
|
|
|
|
|
// Kept alive rather than detached: KiCad SetStatusText()s on every cursor
|
|
|
|
|
// move, and wxFrameBase wxCHECKs a null status bar.
|
feat(mobile): Figma-like hide-UI toggle — Cmd/Ctrl+\ hotkey + floating button, snapshot-exact chrome restore
Chrome visibility is now a runtime toggle on any device instead of being
device-wired: mobile defaults to canvas-only, desktop to full UI, and the
floating top-right button (or Figma's Cmd/Ctrl+\ chord — free in KiCad,
only bare \ is bound) flips between them live.
- chrome-visibility.ts: module-global store (default isMobileMode(),
session-only) + pure hotkey matcher (rejects AltGr backslash + repeats)
- WasmTool: capture-phase hotkey (stopped before the wx layer), floating
toggle pill (matches the comment FAB design), useLayoutEffect apply with
sync first call + retry; overlays follow the toggle, capability-gated on
the kicad_editor bundle's kicadSetChrome export
- boot.ts mobile opt now installs touch gestures only
- kicadSetChrome: frame-keyed hide-time snapshot so restore re-shows ONLY
what hide took away (blanket Show(true) surfaced KiCad's default-hidden
Search/Properties/Net-Inspector panes); toolbars-only fallback otherwise
- tests: chrome-toggle.spec.ts (desktop hide/restore + geometric
restore-exactness ±3px), mobile toggle round-trip, 12 new unit tests,
test:web:mobile npm script
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 16:11:19 +02:00
|
|
|
if( statusBar )
|
|
|
|
|
statusBar->Show( aShow && ( haveSnap ? s_chromeSnap.statusShown : true ) );
|
2026-07-06 16:10:52 +02:00
|
|
|
|
feat(mobile): Figma-like hide-UI toggle — Cmd/Ctrl+\ hotkey + floating button, snapshot-exact chrome restore
Chrome visibility is now a runtime toggle on any device instead of being
device-wired: mobile defaults to canvas-only, desktop to full UI, and the
floating top-right button (or Figma's Cmd/Ctrl+\ chord — free in KiCad,
only bare \ is bound) flips between them live.
- chrome-visibility.ts: module-global store (default isMobileMode(),
session-only) + pure hotkey matcher (rejects AltGr backslash + repeats)
- WasmTool: capture-phase hotkey (stopped before the wx layer), floating
toggle pill (matches the comment FAB design), useLayoutEffect apply with
sync first call + retry; overlays follow the toggle, capability-gated on
the kicad_editor bundle's kicadSetChrome export
- boot.ts mobile opt now installs touch gestures only
- kicadSetChrome: frame-keyed hide-time snapshot so restore re-shows ONLY
what hide took away (blanket Show(true) surfaced KiCad's default-hidden
Search/Properties/Net-Inspector panes); toolbars-only fallback otherwise
- tests: chrome-toggle.spec.ts (desktop hide/restore + geometric
restore-exactness ±3px), mobile toggle round-trip, 12 new unit tests,
test:web:mobile npm script
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 16:11:19 +02:00
|
|
|
if( mgr )
|
2026-07-06 16:10:52 +02:00
|
|
|
{
|
|
|
|
|
wxAuiPaneInfoArray& panes = mgr->GetAllPanes();
|
|
|
|
|
|
|
|
|
|
for( size_t i = 0; i < panes.GetCount(); ++i )
|
|
|
|
|
{
|
|
|
|
|
wxAuiPaneInfo& pane = panes.Item( i );
|
|
|
|
|
|
feat(mobile): Figma-like hide-UI toggle — Cmd/Ctrl+\ hotkey + floating button, snapshot-exact chrome restore
Chrome visibility is now a runtime toggle on any device instead of being
device-wired: mobile defaults to canvas-only, desktop to full UI, and the
floating top-right button (or Figma's Cmd/Ctrl+\ chord — free in KiCad,
only bare \ is bound) flips between them live.
- chrome-visibility.ts: module-global store (default isMobileMode(),
session-only) + pure hotkey matcher (rejects AltGr backslash + repeats)
- WasmTool: capture-phase hotkey (stopped before the wx layer), floating
toggle pill (matches the comment FAB design), useLayoutEffect apply with
sync first call + retry; overlays follow the toggle, capability-gated on
the kicad_editor bundle's kicadSetChrome export
- boot.ts mobile opt now installs touch gestures only
- kicadSetChrome: frame-keyed hide-time snapshot so restore re-shows ONLY
what hide took away (blanket Show(true) surfaced KiCad's default-hidden
Search/Properties/Net-Inspector panes); toolbars-only fallback otherwise
- tests: chrome-toggle.spec.ts (desktop hide/restore + geometric
restore-exactness ±3px), mobile toggle round-trip, 12 new unit tests,
test:web:mobile npm script
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 16:11:19 +02:00
|
|
|
if( chromeSkipsPane( pane ) )
|
2026-07-06 16:10:52 +02:00
|
|
|
continue;
|
|
|
|
|
|
feat(mobile): Figma-like hide-UI toggle — Cmd/Ctrl+\ hotkey + floating button, snapshot-exact chrome restore
Chrome visibility is now a runtime toggle on any device instead of being
device-wired: mobile defaults to canvas-only, desktop to full UI, and the
floating top-right button (or Figma's Cmd/Ctrl+\ chord — free in KiCad,
only bare \ is bound) flips between them live.
- chrome-visibility.ts: module-global store (default isMobileMode(),
session-only) + pure hotkey matcher (rejects AltGr backslash + repeats)
- WasmTool: capture-phase hotkey (stopped before the wx layer), floating
toggle pill (matches the comment FAB design), useLayoutEffect apply with
sync first call + retry; overlays follow the toggle, capability-gated on
the kicad_editor bundle's kicadSetChrome export
- boot.ts mobile opt now installs touch gestures only
- kicadSetChrome: frame-keyed hide-time snapshot so restore re-shows ONLY
what hide took away (blanket Show(true) surfaced KiCad's default-hidden
Search/Properties/Net-Inspector panes); toolbars-only fallback otherwise
- tests: chrome-toggle.spec.ts (desktop hide/restore + geometric
restore-exactness ±3px), mobile toggle round-trip, 12 new unit tests,
test:web:mobile npm script
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 16:11:19 +02:00
|
|
|
if( !aShow )
|
|
|
|
|
{
|
|
|
|
|
pane.Show( false );
|
|
|
|
|
}
|
|
|
|
|
else if( haveSnap )
|
|
|
|
|
{
|
|
|
|
|
if( std::find( s_chromeSnap.paneNames.begin(), s_chromeSnap.paneNames.end(),
|
|
|
|
|
pane.name )
|
|
|
|
|
!= s_chromeSnap.paneNames.end() )
|
|
|
|
|
{
|
|
|
|
|
pane.Show( true );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else if( pane.IsToolbar() )
|
|
|
|
|
{
|
|
|
|
|
// Show with no (or a stale, other-frame) snapshot: reveal the
|
|
|
|
|
// toolbars only — blanket-showing plain panels would surface
|
|
|
|
|
// the default-hidden ones.
|
|
|
|
|
pane.Show( true );
|
|
|
|
|
}
|
2026-07-06 16:10:52 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
mgr->Update();
|
|
|
|
|
}
|
|
|
|
|
|
feat(mobile): Figma-like hide-UI toggle — Cmd/Ctrl+\ hotkey + floating button, snapshot-exact chrome restore
Chrome visibility is now a runtime toggle on any device instead of being
device-wired: mobile defaults to canvas-only, desktop to full UI, and the
floating top-right button (or Figma's Cmd/Ctrl+\ chord — free in KiCad,
only bare \ is bound) flips between them live.
- chrome-visibility.ts: module-global store (default isMobileMode(),
session-only) + pure hotkey matcher (rejects AltGr backslash + repeats)
- WasmTool: capture-phase hotkey (stopped before the wx layer), floating
toggle pill (matches the comment FAB design), useLayoutEffect apply with
sync first call + retry; overlays follow the toggle, capability-gated on
the kicad_editor bundle's kicadSetChrome export
- boot.ts mobile opt now installs touch gestures only
- kicadSetChrome: frame-keyed hide-time snapshot so restore re-shows ONLY
what hide took away (blanket Show(true) surfaced KiCad's default-hidden
Search/Properties/Net-Inspector panes); toolbars-only fallback otherwise
- tests: chrome-toggle.spec.ts (desktop hide/restore + geometric
restore-exactness ±3px), mobile toggle round-trip, 12 new unit tests,
test:web:mobile npm script
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 16:11:19 +02:00
|
|
|
if( aShow )
|
|
|
|
|
s_chromeSnap.valid = false;
|
|
|
|
|
|
2026-07-06 16:10:52 +02:00
|
|
|
frame->SendSizeEvent();
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
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. Zoom/pan stay live (mouse/touch
|
|
|
|
|
// bypass the tool system; keyboard zoom/pan is allowlisted). Returns false
|
|
|
|
|
// until the editor frame exists — main() builds it after runtime init — so
|
|
|
|
|
// JS polls this; the shell fails CLOSED if it never applies.
|
|
|
|
|
static 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
|
|
|
// C++ → JS save notification. Called from BOTH fork save chokepoints
|
|
|
|
|
// (PCB_EDIT_FRAME::SavePcbFile and SCH_EDIT_FRAME::saveSchematicFile) — one shared
|
|
|
|
|
// definition serves the merged image. No-op without a JS listener.
|
|
|
|
|
extern "C" void kicadCollabOnSave( const char* aPath )
|
|
|
|
|
{
|
|
|
|
|
EM_ASM( {
|
|
|
|
|
if( window.kicadCollab && window.kicadCollab.onSave )
|
|
|
|
|
window.kicadCollab.onSave( UTF8ToString( $0 ) );
|
|
|
|
|
}, aPath );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Dispatch shims: route each shared JS name to the live editor's implementation.
|
|
|
|
|
// The sch path is the fallback arm so a JS call with NO frame up behaves like the
|
|
|
|
|
// standalone bundles (the per-editor impls no-op / return empty on a null frame).
|
|
|
|
|
static void collabApply( std::string aJson )
|
|
|
|
|
{
|
|
|
|
|
pcbEditorActive() ? pcbCollabApply( aJson ) : schCollabApply( aJson );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void collabApplyItems( std::string aJson )
|
|
|
|
|
{
|
|
|
|
|
pcbEditorActive() ? pcbCollabApplyItems( aJson ) : schCollabApplyItems( aJson );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static std::string collabSnapshot()
|
|
|
|
|
{
|
|
|
|
|
return pcbEditorActive() ? pcbCollabSnapshot() : schCollabSnapshot();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static std::string collabSnapshotItems()
|
|
|
|
|
{
|
|
|
|
|
return pcbEditorActive() ? pcbCollabSnapshotItems() : schCollabSnapshotItems();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static std::string collabTestMoveFirst( int aDx, int aDy )
|
|
|
|
|
{
|
|
|
|
|
return pcbEditorActive() ? pcbCollabTestMoveFirst( aDx, aDy )
|
|
|
|
|
: schCollabTestMoveFirst( aDx, aDy );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static std::string collabGetPos( std::string aId )
|
|
|
|
|
{
|
|
|
|
|
return pcbEditorActive() ? pcbCollabGetPos( aId ) : schCollabGetPos( aId );
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
static bool collabTestRemoveItem( std::string aId )
|
|
|
|
|
{
|
|
|
|
|
return pcbEditorActive() ? pcbCollabTestRemoveItem( aId ) : schCollabTestRemoveItem( aId );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static bool collabTestRotateItem( std::string aId, double aDeg )
|
|
|
|
|
{
|
|
|
|
|
return pcbEditorActive() ? pcbCollabTestRotateItem( aId, aDeg )
|
|
|
|
|
: schCollabTestRotateItem( aId, aDeg );
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-08 11:09:26 +02:00
|
|
|
static bool collabTestUndo()
|
|
|
|
|
{
|
|
|
|
|
return pcbEditorActive() ? pcbCollabTestUndo() : schCollabTestUndo();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static int collabTestUndoDepth()
|
|
|
|
|
{
|
|
|
|
|
return pcbEditorActive() ? pcbCollabTestUndoDepth() : schCollabTestUndoDepth();
|
|
|
|
|
}
|
|
|
|
|
|
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 — meaningful only with a schematic
|
|
|
|
|
// frame; every other editor answers 0 ("nothing placed here uses it").
|
|
|
|
|
static int libsSymbolUsage( std::string aLib, std::string aName )
|
|
|
|
|
{
|
|
|
|
|
return schEditorActive() ? schLibsSymbolUsage( aLib, aName ) : 0;
|
|
|
|
|
}
|
|
|
|
|
|
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 shims (collab-presence 0002 pcbnew / 0003 eeschema): route to the live
|
|
|
|
|
// editor's implementation, same pattern as the collab bridge shims above.
|
2026-07-06 15:59:51 +02:00
|
|
|
static void collabPresenceStart()
|
|
|
|
|
{
|
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
|
|
|
pcbEditorActive() ? pcbCollabPresenceStart() : schCollabPresenceStart();
|
2026-07-06 15:59:51 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void collabSetRemote( std::string 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
|
|
|
pcbEditorActive() ? pcbCollabSetRemote( aJson ) : schCollabSetRemote( aJson );
|
2026-07-06 15:59:51 +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
|
|
|
static void collabSetPins( std::string aJson )
|
|
|
|
|
{
|
|
|
|
|
pcbEditorActive() ? pcbCollabSetPins( aJson ) : schCollabSetPins( aJson );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void collabSetViewport( double aCx, double aCy )
|
|
|
|
|
{
|
|
|
|
|
pcbEditorActive() ? pcbCollabSetViewport( aCx, aCy ) : schCollabSetViewport( aCx, aCy );
|
|
|
|
|
}
|
|
|
|
|
|
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).
|
|
|
|
|
static void collabFitViewport( double aCx, double aCy, double aHalfW, double aHalfH )
|
|
|
|
|
{
|
|
|
|
|
pcbEditorActive() ? pcbCollabFitViewport( aCx, aCy, aHalfW, aHalfH )
|
|
|
|
|
: schCollabFitViewport( aCx, aCy, aHalfW, aHalfH );
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
static void collabSetStyle( std::string aJson )
|
|
|
|
|
{
|
|
|
|
|
pcbEditorActive() ? pcbCollabSetStyle( aJson ) : schCollabSetStyle( aJson );
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
// Theme switch (comments-ux 0002 F4): BOTH editors, not just the active one —
|
|
|
|
|
// a later frame switch (eeschema-switch-nav) must come up already themed.
|
|
|
|
|
// Each side no-ops on a null frame.
|
|
|
|
|
static void setColorTheme( std::string aTheme )
|
|
|
|
|
{
|
2026-07-27 18:48:25 +02:00
|
|
|
// A real theme apply rebuilds the menubar/toolbars, which come back
|
|
|
|
|
// SHOWN — re-hide them when the chrome is supposed to be hidden
|
|
|
|
|
// (read-only viewer / mobile canvas-only). Installed here, not in
|
|
|
|
|
// pcbjam_theme.h, because the chrome snapshot is merged-image state.
|
|
|
|
|
pcbjam_theme::g_afterThemeApplied = []() {
|
|
|
|
|
if( s_chromeSnap.valid )
|
|
|
|
|
kicadSetChrome( false );
|
|
|
|
|
};
|
|
|
|
|
|
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
|
|
|
pcbSetColorTheme( aTheme );
|
|
|
|
|
schSetColorTheme( aTheme );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The chrome flag is process-global — one call suffices.
|
|
|
|
|
static void setDarkChrome( bool aDark )
|
|
|
|
|
{
|
|
|
|
|
pcbSetDarkChrome( aDark );
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
static std::string collabTestListItems( int aCount )
|
|
|
|
|
{
|
|
|
|
|
return pcbEditorActive() ? pcbCollabTestListItems( aCount ) : schCollabTestListItems( aCount );
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
static std::string collabTestDemoSet()
|
|
|
|
|
{
|
|
|
|
|
return pcbEditorActive() ? pcbCollabTestDemoSet() : schCollabTestDemoSet();
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-06 15:59:51 +02:00
|
|
|
static std::string collabGetViewport()
|
|
|
|
|
{
|
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 pcbEditorActive() ? pcbCollabGetViewport() : schCollabGetViewport();
|
2026-07-06 15:59:51 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static std::string collabGetSelection()
|
|
|
|
|
{
|
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 pcbEditorActive() ? pcbCollabGetSelection() : schCollabGetSelection();
|
2026-07-06 15:59:51 +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
|
|
|
static std::string collabGetSelectionFull()
|
|
|
|
|
{
|
|
|
|
|
return pcbEditorActive() ? pcbCollabGetSelectionFull() : schCollabGetSelectionFull();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static std::string collabTestGetCrossMapped()
|
|
|
|
|
{
|
|
|
|
|
return pcbEditorActive() ? pcbCollabTestGetCrossMapped() : schCollabTestGetCrossMapped();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static std::string collabTestSelectComponent()
|
|
|
|
|
{
|
|
|
|
|
return pcbEditorActive() ? pcbCollabTestSelectComponent() : 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
|
|
|
static bool collabTestSelectByUuid( std::string aUuid )
|
|
|
|
|
{
|
|
|
|
|
return pcbEditorActive() ? pcbCollabTestSelectByUuid( aUuid )
|
|
|
|
|
: schCollabTestSelectByUuid( aUuid );
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
static void collabReleaseSelection( std::string aUuidsJson, std::string aHolder )
|
|
|
|
|
{
|
|
|
|
|
pcbEditorActive() ? pcbCollabReleaseSelection( aUuidsJson, aHolder )
|
|
|
|
|
: schCollabReleaseSelection( aUuidsJson, aHolder );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static std::string collabTestGetLocked()
|
|
|
|
|
{
|
|
|
|
|
return pcbEditorActive() ? pcbCollabTestGetLocked() : schCollabTestGetLocked();
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-06 15:59:51 +02:00
|
|
|
static std::string collabTestSelectFirst()
|
|
|
|
|
{
|
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 pcbEditorActive() ? pcbCollabTestSelectFirst() : schCollabTestSelectFirst();
|
2026-07-06 15:59:51 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static bool collabTestClearSelection()
|
|
|
|
|
{
|
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 pcbEditorActive() ? pcbCollabTestClearSelection() : schCollabTestClearSelection();
|
2026-07-06 15:59:51 +02:00
|
|
|
}
|
|
|
|
|
|
2026-07-02 14:48:11 +02:00
|
|
|
|
2026-07-21 12:22:09 +02:00
|
|
|
static bool kicadCollabFiberBusyProbe()
|
|
|
|
|
{
|
|
|
|
|
return pcbjam_collab::fiberBusy() || !pcbjam_collab::fiberQueue().empty();
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-02 14:48:11 +02:00
|
|
|
EMSCRIPTEN_BINDINGS(kicad_editor) {
|
2026-07-21 12:22:09 +02:00
|
|
|
// Fiber-queue idle probe (drift-trio finding #10b): a bare-embind-stack
|
|
|
|
|
// save during a parked apply fiber mis-dispatches (table index OOB) — the
|
|
|
|
|
// JS side must defer scratch saves while collab fiber work is in flight.
|
|
|
|
|
function("kicadCollabFiberBusy", &kicadCollabFiberBusyProbe);
|
2026-07-02 14:48:11 +02:00
|
|
|
// Programmatic file open (preferred over UI automation from the web app).
|
|
|
|
|
function("kicadOpenFile", &kicadOpenFile);
|
2026-08-09 16:32:53 +02:00
|
|
|
function("kicadOpenFileStart", &kicadOpenFileStart);
|
2026-07-30 14:17:48 +02:00
|
|
|
function("kicadOpenFileBusy", &kicadOpenFileBusy);
|
|
|
|
|
function("kicadTestSetOpenPark", &kicadTestSetOpenPark);
|
2026-07-31 20:34:32 +02:00
|
|
|
function("kicadTestArmTimerPark", &kicadTestArmTimerPark);
|
|
|
|
|
function("kicadTestTimerParkState", &kicadTestTimerParkState);
|
fix(async): fiber resume guard — the prod board-load trap, red/green
Companion to kicad f0ce20ef64 (libcontext swap_suspended guard), which this
pins. The v0.1.20 diagnostics decoded the crash that survived v0.1.13–19:
TOOL_MANAGER Resume()s a coroutine whose body is asyncify-parked inside
handleSleep, the swap rewinds the stale fiber suspension, and the runtime is
poisoned. Full chain of evidence in docs/features/async/16-fiber-resume-guard.md
(+ round-3 addendum in 15-timer-park-repro.md).
- wasm/bindings/fiber_park.h + kicadTestFiberPark{Start,Prime,Poke,State}
exports (pcbnew + merged kicad_editor): stages Call→yield→legitimate
resume→sleep park→mid-park Resume, the exact prod state machine. The
first yield matters: it primes a real (then stale) suspension, matching
long-lived tool loops rather than a first-slice park.
- tests/kicad/fiber-resume-park.spec.ts: asserts the healthy contract on
polled state only (embind returns across fiber swaps are unwind
placeholders). RED on the unguarded build — fiber/sleep buffer
cross-restores, a jump-ghost beacon, the parked body zombified. GREEN with
the guard: mid-park poke refused ([collab-fcontext] jump-refused beacon),
park completes, post-yield resume works, no trap signatures.
- Regression sweep green: timer-park-repro, collab-load-fuzz, load-pcb,
pcbnew-collab, collab-undo, eeschema-collab (19 passed).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SE4o46Lnq3hF574FFq8x4
2026-07-31 23:37:05 +02:00
|
|
|
function("kicadTestFiberParkStart", &kicadTestFiberParkStart);
|
|
|
|
|
function("kicadTestFiberParkPrime", &kicadTestFiberParkPrime);
|
|
|
|
|
function("kicadTestFiberParkPoke", &kicadTestFiberParkPoke);
|
|
|
|
|
function("kicadTestFiberParkState", &kicadTestFiberParkState);
|
2026-08-01 10:05:42 +02:00
|
|
|
function("kicadTestFiberParkStartSecond", &kicadTestFiberParkStartSecond);
|
|
|
|
|
function("kicadTestFiberParkPokeSecond", &kicadTestFiberParkPokeSecond);
|
2026-07-02 14:48:11 +02:00
|
|
|
|
2026-07-06 16:10:52 +02:00
|
|
|
// Canvas-only mobile mode (features/mobile).
|
|
|
|
|
function("kicadSetChrome", &kicadSetChrome);
|
|
|
|
|
|
2026-07-10 20:27:04 +02:00
|
|
|
// Read-only viewer lock (read-only-viewer).
|
|
|
|
|
function("kicadSetReadOnly", &kicadSetReadOnly);
|
|
|
|
|
|
2026-07-02 14:48:11 +02:00
|
|
|
// Yjs collaborative bridge entry points — same JS contract as the standalone
|
|
|
|
|
// bundles, dispatched on the active editor frame.
|
|
|
|
|
function("kicadCollabApply", &collabApply);
|
|
|
|
|
function("kicadCollabSnapshot", &collabSnapshot);
|
|
|
|
|
function("kicadCollabApplyItems", &collabApplyItems);
|
|
|
|
|
function("kicadCollabSnapshotItems", &collabSnapshotItems);
|
|
|
|
|
function("kicadCollabTestMoveFirst", &collabTestMoveFirst);
|
|
|
|
|
function("kicadCollabGetPos", &collabGetPos);
|
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 names; per-editor-only hooks — pad size,
|
|
|
|
|
// endpoint, field text — flow from the per-editor blocks unchanged).
|
|
|
|
|
function("kicadCollabTestRemoveItem", &collabTestRemoveItem);
|
|
|
|
|
function("kicadCollabTestRotateItem", &collabTestRotateItem);
|
2026-07-08 11:09:26 +02:00
|
|
|
// Collab-aware undo (ysync miss 09).
|
|
|
|
|
function("kicadCollabTestUndo", &collabTestUndo);
|
|
|
|
|
function("kicadCollabTestUndoDepth", &collabTestUndoDepth);
|
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
|
|
|
// Presence (collab-presence 0002/0003) + comment pins/panning (0005).
|
2026-07-06 15:59:51 +02:00
|
|
|
function("kicadCollabPresenceStart", &collabPresenceStart);
|
|
|
|
|
function("kicadCollabSetRemote", &collabSetRemote);
|
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", &collabSetPins);
|
|
|
|
|
function("kicadCollabSetViewport", &collabSetViewport);
|
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", &setColorTheme);
|
|
|
|
|
function("kicadSetDarkChrome", &setDarkChrome);
|
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", &collabFitViewport);
|
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", &collabSetStyle);
|
|
|
|
|
function("kicadCollabTestListItems", &collabTestListItems);
|
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", &collabTestDemoSet);
|
2026-07-06 15:59:51 +02:00
|
|
|
function("kicadCollabGetViewport", &collabGetViewport);
|
|
|
|
|
function("kicadCollabGetSelection", &collabGetSelection);
|
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 (collab-presence 0006).
|
|
|
|
|
function("kicadCollabGetSelectionFull", &collabGetSelectionFull);
|
|
|
|
|
function("kicadCollabTestGetCrossMapped", &collabTestGetCrossMapped);
|
|
|
|
|
function("kicadCollabTestSelectComponent", &collabTestSelectComponent);
|
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", &collabTestSelectByUuid);
|
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 (collab-presence 0007).
|
|
|
|
|
function("kicadCollabReleaseSelection", &collabReleaseSelection);
|
|
|
|
|
function("kicadCollabTestGetLocked", &collabTestGetLocked);
|
2026-07-06 15:59:51 +02:00
|
|
|
function("kicadCollabTestSelectFirst", &collabTestSelectFirst);
|
|
|
|
|
function("kicadCollabTestClearSelection", &collabTestClearSelection);
|
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.
|
|
|
|
|
function("kicadLibsReload", &pcbjam_libs::reloadLibrary);
|
|
|
|
|
// Placed-instance count for a library symbol (schematic sessions only —
|
|
|
|
|
// 0 from any other frame; drives the "symbol you are using was updated"
|
|
|
|
|
// toast after a remote lib edit).
|
|
|
|
|
function("kicadLibsSymbolUsage", &libsSymbolUsage);
|
2026-07-02 14:48:11 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#endif // __EMSCRIPTEN__
|