Commit graph pcbjam/wasm
Author SHA1 Message Date
Gergő Törcsvári
d6c4efcf1f
Phase E complete: K4-K6 become token waits — the bridge set is converted, suite at baseline
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L2SU74acXyviwSxBhMunFe
2026-08-10 10:14:19 +02:00
Gergő Törcsvári
75209dc07e
Phase E retry: K1 lands — early-resolve retention, not the lossy guard; pthread-ondemand link fixed
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L2SU74acXyviwSxBhMunFe
2026-08-10 10:14:19 +02:00
Gergő Törcsvári
ab6e44e57f
docs 22 + wx bump: DOM entries on the dispatch context; canvas tools green at D-on
The increment doc 22 ordered last round. Every entry that can reach a tool
coroutine - the four DOM callbacks and the mailbox tick - now goes through the
scheduler, so a coroutine is never entered by a star transfer from one path and
a direct symmetric swap from another. At D-on all four canvas-tool specs are
GREEN and the KiCad suite is 136/3 (was 135/5 before the sleep work, with the
tools red throughout).

context_sleep's wake learned the mirror lesson: now that the mailbox runs ON a
context, it must NOT call drain_all from there (drain refuses re-entry, and
should) - it marks ready and lets the outer drain_all perform the entry, with
an armed pump as a backstop.

Recorded honestly, not papered over: two of the three remaining D-on failures
are the timer-park and quasimodal-strand levers, each failing ONE assertion -
"scheduler shim observed the concurrent-park window", expected >0, got 0 -
while fired/done/parked/errors all pass. That counter needs TWO concurrent
in-place Asyncify parks, and the lever stages "timer park x MAIN-LOOP YIELD
PARK"; D5 removed the main loop's Asyncify park, so the overlap cannot occur.
Re-pinning those levers to the post-migration invariant is a Phase F decision
alongside fiber-resume-park's red->green flip, NOT an assert to relax now.

Landing state: STAR_DISPATCH=0, kicad 139 passed / 1 (pre-existing occ-probe).
Next: Phase E - the K1-K7 bridges are the only in-place parks left under a
context, and are exactly what the current()!=0 fallback still tolerates.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LBjomQfKyRa3jBdeAKpmTw
2026-08-10 10:14:19 +02:00
Gergő Törcsvári
80468e7c5b
Phase B: main-thread sleep parks its context; the real D blocker is DOM entries
wasm/shims/context_sleep.cpp: a main-thread nanosleep whose frame stands on a
scheduler context that OWNS the stack arms a mailbox wake and yield_parks that
context instead of suspending the stack in place. It lives in the sleep
primitive rather than in tool_manager.cpp on purpose - KiCad and the wx core
stay untouched (CLAUDE.md's fork rule) and the whole K7 class moves at once,
not just TOOL_MANAGER::RunSynchronousAction's spin loop.

MEASURED AT D-ON, and it is NOT what unblocks Phase D. The four canvas-tool
specs still fail, but the trace now names a different cause: the fatal swap is
old=<libcontext ROOT> new=<tool coroutine> with mouseEventHandlerFunc above it
- a DOM mouse handler entering wasm DIRECTLY on the main stack, bypassing the
tick. So one coroutine is entered two ways: by the tick through the dispatch
context as a STAR TRANSFER, and by DOM handlers as a DIRECT SYMMETRIC SWAP. A
capture written by one path cannot be rewound by the other -> index out of
bounds in doRewind. That is section 7 rule 5 (partial migration is worse than
none) in its purest measured form, and it is why the harness stays green: its
coroutines are only ever entered from one place.

So the next increment is the DOM event entries (mouse/key/wheel/resize must
hand their events to the dispatch context as the tick does), not another park
site. It subsumes the one-root work too: with no dispatch on the main stack,
resolve_root_identity() always answers "the running context".

Landing state: STAR_DISPATCH=0, kicad 139 passed / 1 (pre-existing occ-probe)
= baseline, with the sleep shim in and inert.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LBjomQfKyRa3jBdeAKpmTw
2026-08-10 10:14:19 +02:00
Gergő Törcsvári
48da418f34
design-b D3: fix the doc-19 hang — quasi-modals off the coroutine stack
THE BUG IS FIXED. tests/kicad/quasimodal-strand.spec.ts flips from a
test.fail() pin to a plain green regression test: 3/3 runs
closed=true dialogs=0 refused-resumes=0 (was closed=false dialogs=1
refused-resumes=1 on every run).

Mechanism: a quasi-modal's nested event loop parked on the TOOL
COROUTINE's stack, which suspends the fiber's body where the fiber layer
cannot see it — so the stale-fiber guard quarantined the fiber and then
refused its own resume, the dispatch guard was never released, and every
click after that was deferred forever. Bouncing the nested loop onto the
main stack leaves the coroutine suspended the legitimate way (a recorded
fiber swap), so nothing is quarantined and nothing is refused.

Layering, so this is not a pile of WASM ifdefs in KiCad:
- wx (3d37db3bf1) owns the POLICY and the hook; it must not know what a
  coroutine is.
- wasm/bindings/main_stack_runner.h is the only place that may know both
  sides: it finds the frame's TOOL_MANAGER and bounces via RunMainStack.
  Header-only and self-installing, so no build-script change; included by
  every editor's binding TU.
- KiCad gets ONE ifdef-free method (2c777efede), needed only because
  TOOL_STATE is opaque outside TOOL_MANAGER. libcontext and dialog_shim
  are untouched — an earlier draft edited both and was reverted.

This also reframes the remaining plan: the doc-19 class is closed WITHOUT
migrating tool coroutines onto scheduler contexts. Note it does not make
the wait a context yield — waits still park in place, just never on a
coroutine stack.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TEHGiiXMShNXbBr7gSJ7iz
2026-08-10 10:14:17 +02:00
Gergő Törcsvári
5a3dd44b23
design-b D2: attempted, reverted, resequenced behind D3
Bumps wxwidgets d220aae5 (D2a: sched_context.h moved into wx's port,
header-only so evtloop.cpp can see it) and 5ee60a81 (D1 fix: 16-align
context stacks — EM_ASM's arg buffer lives on the running stack and the
glue asserts buf % 16 == 0, so misaligned contexts trapped in
readEmAsmArgs; std::vector<char> only gives malloc's 8-byte alignment).

The dispatch switch itself is NOT landed. Running the tick's
ProcessEvents on a context took the battery from 363 green to 388/7, six
of them the coroutine-nested harness wedging at
fiber_create_run_destroy_inside_modal via aliased-wake-live ->
fiber-resume-refused — doc 19's mechanism. A quasi-modal opened from a
tick handler suspends the dispatch context INSIDE the still-in-place
wait, putting one more Asyncify layer under every libcontext fiber.
Pooling contexts (8 burned in 30 ms) and falling back to entry-stack
dispatch both failed to avoid it, because the layer exists as soon as
the context is suspended.

That is doc 20's own risk 2 arriving on schedule, so the plan is
corrected rather than the symptom patched: D3 (waits become context
yields) must come first, after which the dispatch context is released at
its tick boundary instead of suspended and the failure class is
structurally absent. Verified the revert: the nested battery is green
again at this baseline (5 passed / 1 failed, the 1 being the
environment-sensitive modal:125 that also fails without any of this).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TEHGiiXMShNXbBr7gSJ7iz
2026-08-10 10:14:16 +02:00
Gergő Törcsvári
98731cd30e
design-b D1a: scheduler context primitives on emscripten fibers
Doc 20 §6 D1, first slice: wasm/sched/context.{h,cpp} implements
create / yield_park / mark_ready / drain with the registry as truth.

The shape that matters — a STAR, not libcontext's symmetric swap:
contexts only ever swap OUT to the scheduler, and only the scheduler
swaps IN. So "is this target safe to enter?" stops being a guess
(libcontext's swap_suspended / parked / hot-main refusals) and becomes a
lookup: the registry says Parked/Ready and holds the buffer. Doc 19's
refused-resume is unrepresentable here because resume is not a decision
made at the swap site.

Enforced, not hoped for:
- at most one transition in flight (drain() refuses re-entry, so a
  context calling drain() cannot turn the star into a cycle);
- mark_ready() never resumes inline — it queues, and drain() resumes
  from a clean stack (doc 13 §1.4's deferred-wake law, per context);
- yield_park() off a context is REFUSED, which is the "nothing parks in
  place" rule made mechanical;
- destroy() on a non-Finished context is refused (freeing a parked
  stack strands whatever is on it);
- FIFO ready queue (no starvation);
- main-thread-only, per doc 21 §2's pthread finding.

Memory is accounted from the start (doc 20 risk 1): live/peak contexts,
bytes/peak bytes, and per-context asyncify high-water measured from
asyncify_data.stack_ptr — the same quantity the shim reports as `rem=`,
from the other end. Sizes deliberately start at 128 KB C stack + 128 KB
asyncify buffer rather than inheriting libcontext's 512 K, so the number
gets derived from evidence; a >75% buffer use beacons BUFFER-PRESSURE
because that overflow is silent corruption, not a crash.

No production path runs on this yet — dispatch moves at D2.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TEHGiiXMShNXbBr7gSJ7iz
2026-08-10 10:14:15 +02:00
Gergő Törcsvári
5333099810
test(kicad): poisoned-attribution lever — the laundering scenario, red/green
kicadTestFiberParkStartSecond/PokeSecond: a second coroutine started while
the first body is asyncify-parked reproduces the misattributed jump that
launders the parked fiber past the C++ guard (the v0.1.21 prod bypass).
Spec scenario 2 stages it and asserts the JS stale-rewind guard quarantines
the laundered resume (exactly one fiber-resume-refused beacon), the parked
body completes undisturbed, and both coroutines finish cleanly.

Doc: async/16 rounds 2 + WSOD section.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SE4o46Lnq3hF574FFq8x4
2026-08-01 10:05:42 +02:00
Gergő Törcsvári
e561507898
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
Gergő Törcsvári
f86ef9e433
test(kicad): timer-park repro lever — deterministic concurrent-Asyncify window
kicadTestArmTimerPark(delayMs, parkMs): a one-shot wxTimer whose Notify()
emscripten_sleep()s, entering through the exact GAL-refresh-timer path
(emscripten_async_call → TimerCallbackFunc::Run → dispatch guard → Notify) —
the fresh-entry-that-parks the prod board-load trap family needs. Pollable
kicadTestTimerParkState(); inert unless armed. Registered beside
kicadTestSetOpenPark in pcbnew + the merged kicad_editor image.

tests/kicad/timer-park-repro.spec.ts drives four escalating cycles (park
only, 2× + fiber hammering, + 256MB heap growth mid-park) and asserts the
runtime survives every rewind AND that the [wx-asyncify] diagnostics observed
the window — engagement is asserted, so a run where the lever never created
the overlap cannot pass vacuously.

Result so far (docs/features/async/15-timer-park-repro.md): GREEN through
both rounds — genuine double-parks, live currData cross-restores, fiber
swaps, and mid-park heap growth are all handled by the shim + runtime. The
prod trap needs an ingredient this window still lacks (ranked in the doc);
the spec stays as the regression gate for whatever the eventual fix is.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SE4o46Lnq3hF574FFq8x4
2026-07-31 20:34:32 +02:00
Gergő Törcsvári
d35cf4f4eb
fix(load): close the dispatch-interlock hole at open + open gerbers from a project route
- kicadOpenFile now holds wxWasmDispatchGuard (open_gate.h). It enters through
  embind, so the interlock read "nothing parked" for the whole load and wx timers
  dispatched into the half-built board — the residual prod "index out of bounds"
  that survived the settle gate.
- new wasm/bindings/gerbview_embind.cpp (the bundle had no embind surface at all):
  kicadOpenFile / kicadOpenFiles / kicadOpenFileBusy. Clicking one gerber opens the
  whole fabrication set in its folder, since a lone layer is not a useful view.
- cross-app presence rejoins in the boot fan-out (network-only; the wasm-bound half
  still waits for the open to settle) — it had been pushed behind the board load.
- tests: gerber-set selection units + a gerbview multi-file open e2e.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0137pGo8W7asomGUTRMB7RzM
2026-07-30 19:08:57 +02:00
Gergő Törcsvári
a26ef4ebeb
fix(load): open-settle gate — kicadOpenFileBusy probe + collab entry guards for the parked-open embind trap (indirect call signature mismatch) + deterministic collab-load-fuzz e2e
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0137pGo8W7asomGUTRMB7RzM
2026-07-30 14:17:48 +02:00
Gergő Törcsvári
64d2cdf259
Revert "fix(wasm): never swap to a fiber while an Asyncify context is parked"
This reverts commit 32218122c3.
2026-07-29 17:29:28 +02:00
Gergő Törcsvári
32218122c3
fix(wasm): never swap to a fiber while an Asyncify context is parked
Opening a large board could kill the editor runtime outright: "index out of
bounds" / "unreachable executed" / "indirect call signature mismatch", after
which every later focus/key event trapped. Reported in prod against a big
uploaded board, and correlated by the reporter with the moment the presence
WebSocket connects.

That correlation is the tell. Presence/collab work enters the wasm through
runOnFiber -> COROUTINE::Call, i.e. emscripten_fiber_swap, whose stop_unwind
corrupts Asyncify's single currData slot when ANOTHER context is already
parked there. docs/features/async/13 pins the invariant: exactly one
unwind/rewind transition in flight, and prescribes "a single shared is-a-
transition-in-flight guard the pumps consult before re-driving".

Normally the slot is free when fibers drain: the main loop's per-frame
wxWasmYieldToBrowser completes every frame, so drainFibers runs between
yields. It is NOT free when a nested/modal pump tick drives ProcessEvents
while the chain that opened the modal is parked deeper down — precisely a big
board open (progress dialog over a parked load). The wx dispatch interlock
does not cover this: the modal parks deliberately zero its count so their own
pump may dispatch.

So gate the swap itself: drainFibers defers (re-CallAfter) while
asyncifyInFlight(). Bodies are viewport/overlay/apply work, so waiting out the
park costs latency, never correctness.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H8jo7zz1ZwzYpjJ64UZKN4
2026-07-29 15:29:36 +02:00
Gergő Törcsvári
b0e84ff849
fix(theme): stop the boot theme re-send from resurrecting hidden chrome
Since comments-ux 0002, WasmTool re-sends the shell theme through
kicadSetColorTheme on every boot; setColorTheme ran CommonSettingsChanged
unconditionally, which recreates the menubar/toolbars — and they come back
SHOWN, undoing the kicadSetChrome(false) the read-only viewer and mobile
canvas-only mode applied moments earlier (CI: read-only-editor +
mobile-editor "9 visible menu titles").

Two-part fix in pcbjam_theme:
- early-out when the requested theme AND the wx dark-chrome flag are
  already applied — the every-boot re-send (still needed for warm
  relaunches with stale MEMFS settings) becomes a true no-op;
- after a REAL apply, re-assert the hidden chrome via a hook the merged
  image installs (kicadSetChrome(false) when the snapshot says hidden),
  queued with CallAfter from inside the fiber body so FIFO lands it
  behind the CallAfter-deferred ReCreateMenuBar. Covers viewers/mobile
  users toggling dark mode mid-session.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y32etYBCKV6t1qDoLoGmgF
2026-07-27 18:48:25 +02:00
Gergő Törcsvári
f1ea6b3965
plugins 0002: kicad_tools --ipc356 + --fab-components fab exporters
Two generic (manufacturer-agnostic) pcbnew-side subcommands for platform
manufacturing plugins: --ipc356 (IPC-D-356 netlist via IPC356D_WRITER) and
--fab-components (board metrics + per-footprint placement/BOM JSON —
absolute board coords, consumers apply their own conventions). Bumps the
kicad submodule for the diet keep-back of export_d356.cpp.

Also closes a CI cache gap: wasm-cache-hash never hashed wasm/**, so a
pure wasm/cli change would cache-hit stale output — { dir: "wasm" } is
now an input (busts the cache once on landing).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mr8PQ34yCfvEtEcwSUvLAV
2026-07-27 14:01:30 +02:00
Gergő Törcsvári
21a96b0440
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
Gergő Törcsvári
22cd32b7b2
fix(drift-trio): pin fiber slot across asyncify parks (#10b layer 1)
Symbolized (HOIST_KEEP_NAMES=1): the trap is on the asyncify REWIND re-entering
the fiber — stack-local COROUTINE+body were destroyed when Call() returned
early on a park, so the rewind called through freed objects (latent UB in the
ORIGINAL fire-and-forget runOnFiber too). Heap-pinned FiberSlot + explicit
done flag + fiber-tail re-drain. Layer 2 (rewind interplay) still open —
fuzz stays fixme'd; pageerror stacks now captured in fuzz artifacts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G5cAM9M6q34n5X4dbrfVvi
2026-07-21 12:45:51 +02:00
Gergő Törcsvári
3fc90e8fe2
fix(drift-trio): phase E — serialized fiber queue (#10a) + fiber-busy probes
runOnFiber now runs bodies strictly one-at-a-time through a park-safe FIFO
(collab_common.h): the per-body fire-and-forget coroutine interleaved under
load — an asyncify park inside commit.Push let the event loop start the next
body, so a local commit and a remote apply ran interleaved on shared state
(s_applyingRemote is one global), silently losing applies on the actively-
editing receiver (fuzz finding #10a; B now fuzzes clean; 39-test suite green).

kicadCollabFiberBusy embind probe (merged + standalone registrations): a
bare-embind-stack scratch save during a parked fiber mis-dispatches (table
index OOB) — trio.ts modelText/drift and production drift-detect now defer
while fiber work is in flight (#10b hardening; the trap's root cause is still
open and needs a symbolized stack — fuzz stays fixme'd).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G5cAM9M6q34n5X4dbrfVvi
2026-07-21 12:22:09 +02:00
Gergő Törcsvári
23b0f43e73
feat(drift-trio): phase C — scenarios S2–S8 + re-resolve-on-fiber hooks
drift-trio-scenarios.spec.ts: disjoint ping-pong, same-item interleave (no
settle between bursts), conflict pairs (move-vs-delete / value-vs-value /
move-vs-move — winner is CRDT policy, asserted only as convergence + drift
silence), undo storm, 12-edit burst churn, late-joiner adopt, and Ctrl+S mid
peer burst; per-tool adapters, marker-waits before every sweep (finding #7).
S4 exposed finding #9: mutation hooks resolved item pointers at call time and
committed later on the fiber — a remote remove in between frees the pointer
(doApplyItems) and the commit resurrects the deleted item. Phase-B mutation
hooks now re-resolve by uuid ON the fiber; a vanished item makes the mutation
lose silently. applyDeltaToY's concurrent update/delete verified coherent
(full resurrect or full remove) — no shared change needed. trio.ts: TabSet
oracles + exported startV2 for duo/late-join composition.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G5cAM9M6q34n5X4dbrfVvi
2026-07-21 11:02:41 +02:00
Gergő Törcsvári
71a575d2d8
feat(drift-trio): phase B — action-catalog hooks + duplicate/lock wire fixes
17 kicadCollabTest* action primitives (eeschema: wire/junction/no-connect/
label/symbol/move/mirror/duplicate; pcbnew: track/via/text/zone/flip/
footprint-field/lock/move/duplicate), each a real SCH_/BOARD_COMMIT on the
fiber so the listener → flushDiff emit runs as for UI edits; tool-unique
names, merged-image safe. Fixes surfaced by the catalogs (0008 §10 #4–#8):
eeschema adds SetParent before staging (Push silently skips listener
notifications for unparented items), and pcbnew blobForItem now Formats
non-footprints with the FILE writer + wrapInBoardEnvelope — SaveSelection's
transfer copy cleared the locked flag, so (locked yes) never reached the doc.
drift-trio.spec.ts gains full A/B-alternating catalogs with per-step landed
gate + oracle sweep. Bumps kicad for the Duplicate child-uuid re-roll.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G5cAM9M6q34n5X4dbrfVvi
2026-07-21 10:37:37 +02:00
Gergő Törcsvári
f92266fcee
fix(ysync): wire dialect == file dialect, uuid churn, drift noise
The Y.Doc is the source of truth for the FILE, but both live wires wrote
KiCad's CLIPBOARD dialect — a lossy, paste-oriented format. Every
difference was permanent, unfixable drift.

- pcbnew: serialize footprint blobs with CTL_FOR_BOARD, not CLIPBOARD_IO's
  CTL_FOR_CLIPBOARD, which emitted (version)(generator)(generator_version)
  inside every (footprint …). Keep (locked yes).
- eeschema: aForClipboard=false — clipboard mode collapsed every symbol's
  (instances … (path "/sheet")) to (path "").
- Re-supply (version) at PARSE time only (withFootprintVersion): the token
  is invalid file content but load-bearing on decode — without it the
  parser starts at m_requiredVersion=0 and stamps (hide yes) on every
  mandatory field.
- FOOTPRINT copy ctor: restore mandatory-field uuids (EDA_ITEM::operator=
  keeps the target's const m_Uuid, so Clone() rerolled all four).
- drift: classify order-only diffs as `reordered` — y-sexpr v2 reorders
  legitimately; excluded from counts, report-worthiness and dedupe hashes.
  Migration 0017.
- fpedit from eeschema: AsyncLoad()+BlockUntilLoaded() in initLibraryTree —
  FACE_PCB starts lazily there and never preloaded its libraries.

Guards: wire-vs-file round-trip tests (pcbnew + eeschema),
fpedit-from-eeschema (verified red without the fix), symedit-from-eeschema.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016p9kjdGBdcpwUSjJ3q5xg2
2026-07-20 18:32:04 +02:00
Viktor Vaczi
703cb010b7 eeschema simulator: lazy ngspice_service worker — static sharedspice (XSPICE registry + CIDER), init_dll ifdef, e2e both engines
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 15:59:21 +02:00
Gergő Törcsvári
44f9373a15
cvpcb-wasm: link CvPcb as third kiface — eeschema Assign Footprints opens in WASM; serial-inline footprint list load fixes pool-task-vs-JS-bridge deadlock
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y8of27UBjJmwY7JkALit2j
2026-07-17 18:00:13 +02:00
Viktor Vaczi
63ed1f3c1f 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:21:54 +02:00
Gergő Törcsvári
fb2aa2759d
kicad-validity B3+C+S: revert toast, corpus lint, .kicad_mod tier
B3 (editor): kicad-binding observes kdoc_meta.revertNonce (deduped, like
seedNonce) → DOC_REVERTED_EVENT → WasmTool doc-reverted-toast (15 s,
data-testid for the deferred e2e check).
C: tests/tools/corpus-lint.ts (npm run corpus:lint) — kicad_tools --lint
over the fixture corpora raw + shared-codec round-trips
(docToFile(fileToDoc(x))); the E3/wrapInBoardEnvelope-class gate. Skips
when output/kicad_tools.js isn't built; CI step lands when kicad_tools
joins the CI app set. 7 fixtures / 7 round-trips green.
S: --lint + --resave gain the .kicad_mod tier (pcbToolsLintFootprint /
pcbToolsResaveFootprint; writer needs CTL_FOR_LIBRARY or the board flags
strip the (version) header). Verified: qa footprint round-trip lints
clean, resave stamps 20260206, garbage exits 1. Also bumps pcbjam-shared
(mergeYUpdates for the bisect pass).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HLua64PCVwkQ1hpWdaf1Gm
2026-07-14 19:02:13 +02:00
Gergő Törcsvári
c51ccf82ba
kicad-validity A1: kicad_tools --resave — full-parse rewrite in current format
New --resave <file> <outdir> subcommand (root docs/features/kicad-validity/
0001): .kicad_sch one file per sheet mirroring the hierarchy layout,
.kicad_pcb via pcbToolsResaveBoard callback (mirror of the lint callback),
.kicad_sym/.lib via ConvertLibrary. Exit 0 ok / 2 usage / 4 input invalid /
5 write failed — only 4 marks the input invalid for the upload gate.
The s-expr schematic save stub is now #ifndef KICAD_TOOLS_COMBINED: the
merged image links the real writer (+65 KB), standalone sym_convert keeps
its diet. Board save needed no un-stubbing (pcb_io/ survives the CMake
prune).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HLua64PCVwkQ1hpWdaf1Gm
2026-07-14 19:01:34 +02:00
Istvan Matejcsok
bb01f5d9e6 feat(read-only-viewer): editor viewer mode + kicadSetReadOnly binding
Anonymous / non-member sessions open PUBLIC projects as read-only viewers.

- kicadSetReadOnly(bool) embind (merged kicad_editor + pcbnew/eeschema/
  pl_editor TUs): sets the PCBJAM_READ_ONLY flag + Prj().SetReadOnly (greys
  the setup dialogs). Polls until the frame exists.
- read-only-mode.ts: resolveReadOnly(access, win) — server `access:"read"` or
  ?readonly=1 (narrow-only; no ?readonly=0). ToolPage threads it in, omits
  saveBytes (MEMFS-only saves).
- WasmTool: chrome force-hidden with a "View only" pill (toggle + Cmd+\
  disabled), presence/cross-app/comments/drift skipped, save-driven room
  writers unregistered, wasm frame locked via kicadSetReadOnly failing CLOSED
  (stale bundle → boot error, never a writable frame).
- collab: bindKicadCollab {readOnly} — inert DOWN hook, never seeds a room;
  UP observer + adopt stay live so peer edits render. index.ts / sheet-manager
  thread readOnly + drop initial awareness (invisible observer).
- Reference backend emits access:"write".

Bumps kicad + web/pcbjam-shared to the read-only-viewer commits.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012DN9py5GuPdExaaFzE4k27
2026-07-13 12:48:35 +02:00
Gergő Törcsvári
f07b9970d5
fix: wrapInBoardEnvelope layer order
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADzCSeN3Q9DJL3YW2FZTXB
2026-07-13 09:13:27 +02:00
Gergő Törcsvári
f590cbd49c
feat: kicad_tools --drill + --plot-board
--drill <pcb> [outdir]: excellon drill files (kicad-cli JOB defaults, no
map files). --plot-board [--pdf] [--layers a,b,...] <pcb> [out]: single
SVG/PDF document via PCB_PLOTTER; layers default to the board's enabled
set, --layers takes canonical or user names.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADzCSeN3Q9DJL3YW2FZTXB
2026-07-10 19:09:02 +02:00
Gergő Törcsvári
19014ae113
feat: kicad_tools --gerbers
--gerbers <pcb> [outdir]: one .gbr per enabled layer (stackup plot order,
kicad-cli defaults via default JOB_EXPORT_PCB_GERBERS + PlotJobToPlotOpts)
plus the .gbrjob file — mirrors JobExportGerbers minus the zone re-check.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADzCSeN3Q9DJL3YW2FZTXB
2026-07-10 19:09:02 +02:00
Gergő Törcsvári
ad9c4675f9
feat: kicad_tools headless CLI
One merged node-WASM CLI (wasm/tools, 26.5MB) supersedes sym_convert +
pcb_convert: --convert-lib (absolutizes paths — fixes the legacy plugin's
silent empty output on relative paths), --lint (now full-parses .kicad_pcb
via the linked pcbnew parser), --erc, --netlist, --bom, --plot, --drc.
Registered as the only headless CLI app; ASYNCIFY=0 CLIs skip the
nanosleep→Asyncify yield shim and exit via _exit (dieted static dtors trap).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADzCSeN3Q9DJL3YW2FZTXB
2026-07-10 19:09:01 +02:00
Gergő Törcsvári
28388990ee
ysync 0009 phase 4: sym_convert --lint mode (kicad_lint folded into the converter)
sym_convert --lint [--strict] <file...>: full parse for .kicad_sch (restored
LoadSchematicFile + minimal headless PGM/SETTINGS_MANAGER runtime,
aSetActive=false) and .kicad_sym/.lib; structure-only for other s-expr files;
dup-uuid identity lints (walker) on every s-expr input; errors as
file:line:offset, exit 0/1/2. Convert mode byte-identical (qa gate).

Fixes en route: 64KB stack local overflowed the default wasm stack (mimalloc
corruption); pre-js now inherits process.env into getenv (KICAD_CONFIG_HOME /
SYM_CONVERT_TRACE); font abort stub retired (SCH_SCREEN::Append bbox resolves
draw fonts) -> 18.0MB; six GetMsgPanelInfo typeinfo stubs; BUILD_3D_VIEWER
defaults OFF for headless CLIs (gl1 shim needs glm).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0142yAmMGZ3Ejrq3wwVr2Yq2
2026-07-10 13:13:51 +02:00
Gergő Törcsvári
720cff54ba
feat(occ): ship board 3D model bodies with STEP/GLB exports (3d-models 0007)
The occ_service export worker has its own MEMFS — the editor's lazily-fetched
lib models were invisible there, so every export was a bare board (54
"Could not add 3D model" warnings on pic_programmer, 2 STEP products).

- models-bridge: collectBoardModelFiles(boardText) — scan refs, ensure via
  the 0004 sparse source (IDB/R2, wrl->step fallback), read staged bytes
  back, dedupe by real staged path.
- occ-service.ts: attach the collected models to every export request
  (best-effort — prefetch failure still exports, misses reported by the
  exporter); transfer the body buffers.
- occ-worker.js (shared app/harness): pass req.models through to occExport.
- occ_service_main.cpp: occExport(board, params, models) stages each entry
  under PCBJAM_3D::MODELS_MEMFS_ROOT (path-sanitized) for the exporter's
  staged-model probe (kicad 83645275ac), removed again after the export.
- tests: harness occ stub mirrors the prefetch against the page kicadLibs
  provider + captures report/productCount; new occ-export-models.spec.ts
  guards the delivery (green companion pins preconditions; guard asserts 0
  missing lib models + component PRODUCTs). pic_programmer: 17/17 staged,
  87 products @ 13.3 MB (was 2 @ 402 KB). models-bridge unit tests 13/13.

Known remainder (0007 step 4): project-local ${KIPRJMOD} refs still drop.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017UjpnviP3ZDxTM1Ap63Sqv
2026-07-10 09:26:07 +02:00
Gergő Törcsvári
9315f56760
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-10 09:26:07 +02:00
Gergő Törcsvári
c229c75ca6
fix(collab): re-push viewport on canvas resize — comment pins sat offset until pan/zoom
emitViewportIfChanged deduped on scale+center only, so a canvas SIZE change
(boot layout settling after the bind-time seed, window resize) never re-pushed
{w,h} to JS. CommentLayer's worldToScreen maps through h/2, so every DOM pin
hit target (and its hover ring) sat vertically offset from its GAL dot by
exactly delta-h/2 css-px until the next pan/zoom finally passed the dedupe.

Size now participates in the dedupe and wxEVT_SIZE re-pushes post-layout
(CallAfter, after the GAL's own onSize). New regression e2e
comments-viewport-resize.spec.ts asserts the DOM pin re-aligns with GAL truth
(fresh kicadCollabGetViewport) across a window resize with no pan/zoom.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011x3h5AzkWDbHmCkeVYAuzU
2026-07-10 09:25:14 +02:00
Gergő Törcsvári
f8da88472c
sym_convert wasm diet: 31.3->11.05 MB via kiface prune + link-order shims (ysync 0009)
New wasm/cli/sym_convert_stubs.cpp: first-definition-wins overrides
(--allow-multiple-definition) severing schematic load/save, the KIFONT
factory (drops newstroke + freetype/harfbuzz), and the four UI virtuals
whose bodies reference pruned typeinfo/data. New
wasm/bindings/sym_convert_embind.cpp: no-op kicadCollabOnSave so the
converter stops linking eeschema's embind object (and --bind) that rooted
the editor surface from .init_array. build-kicad-target.sh: per-app
EMBIND_LINK_FLAG + sym_convert now uses its own embind TU. Bumps kicad
(kiface prune, gated). Output byte-identical on the qa corpus.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H58sC87w12FotXEmxrJrQV
2026-07-10 09:25:14 +02:00
Gergő Törcsvári
1f85e13acf
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-10 09:25:13 +02:00
Gergő Törcsvári
855137c650
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):

- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
  fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
  (onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
  hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
  state and machinery (start/canvas binds/lock query, setRemote/setPins/
  setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
  releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
  SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
  selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
  (anonymous-namespace presenceCore()) so the merged image keeps per-editor
  state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
  the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
  duplicated within each TU, letting the test probe drift from the pixels.

Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).

eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.

Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-10 09:17:32 +02:00
Istvan Matejcsok
d44ba9bdfb 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:15:37 +02:00
Istvan Matejcsok
dc7c60f723 feat(mobile): canvas-only mobile mode — pinch-zoom/pan/tap gestures + chrome-less editors
On a mobile device (or ?mobile=1) the editors run canvas-only with touch
gestures driving the view:

- touch-gestures.ts: pure recognizer (unit-tested) + DOM shim installed in
  boot preRun — one-finger drag → synthetic middle-drag (pan), pinch →
  synthetic wheel at the centroid (zoom-to-cursor), tap → left click.
  preRun registration order is what lets stopImmediatePropagation suppress
  the wx layer's single-finger→LEFT-drag touch mapping.
- kicadSetChrome(bool) embind: hides all AUI panes except DrawFrame + the
  menubar/status bar via generic wx APIs (kicad fork untouched); boot polls
  it after runtime init. Pairs with the wxwidgets IsShown layout fix.
- mobile-mode.ts: ?mobile=1/0 override or UA-CH/coarse-pointer autodetect;
  shell hides its overlays and the inherent-to-mobile preflight warnings.
- e2e: mobile-chromium project (Pixel 7) + 4 specs (chrome-less, tap,
  pinch, pan) with screenshot-invertibility assertions; also fixes
  tool-switch.spec's stale pre-scope-refactor URLs (was broken on main).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 16:15:37 +02:00
Gergő Törcsvári
574284c486
feat(collab): local-ops-only undo — remote applies skip the undo stack (ysync miss 09)
Ctrl+Z after a peer's edit no longer reverts (and re-broadcasts) the
peer's work, and the adopt undo-bomb is gone:

- doApply/doApplyItems (both editors) Push with SKIP_UNDO; the emit path
  is unaffected (suppression keys off s_applyingRemote, not undo).
- With SKIP_UNDO no picker owns removed items — the bindings free them
  after Push (explicit removals + upsert's remove-before-re-add; fields
  excluded: CHT_REMOVE hides them, parent keeps ownership). Freeing stays
  out of the fork commit classes so DRC's SKIP_UNDO callers can't
  double-free.
- Test hooks kicadCollabTestUndo/UndoDepth, registered per-editor AND in
  the kicad_editor dispatcher (merged image compiles out per-app
  registrations).
- kicad pointer: eeschema UUID undo guard + SKIP_UNDO connectivity split
  + quiet stale-entry drop (ca8877324c).
- tests/kicad/collab-undo.spec.ts: 5 scenarios (no undo entry from remote
  applies; selective undo; stranded replaced/deleted entries) — 5/5, plus
  collab/ysync regression 30 pass.
- docs: ysync-review 20 fix record; 09 marked FIXED; overview indexed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ejJEvS7ogef2o9gVTXjmp
2026-07-08 11:09:26 +02:00
Gergő Törcsvári
c30bcd4531
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 21:09:24 +02:00
Gergő Törcsvári
3186986a0a
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 21:09:23 +02:00
Gergő Törcsvári
ef15c0d863
feat(collab): ship the tuned presence style as per-editor defaults
Defaults picked with the PresenceTuner (2026-07-07):
- pcbnew: exact-outline selection, 6px stroke @0.7, 46% fill, bottom-end
  chips @7.5px, cross cursor 8/3px @1.0 with chip label, 9px pins w/ 3px ring.
- eeschema (eeschemaDefaultStyle): same look softened for the schematic
  canvas — hairline 1px outline, 14% fill, 0.5 cursor alpha.
- New chipBgAlpha knob (default 0.7 = the border alpha) for label AND cursor
  badges, + tuner slider.
- Tuner: per-tool defaults + per-tool localStorage keys; Reset returns each
  editor to its own shipped look; tool prop threaded from WasmTool.
Verified: pure defaults (no style push) render the picked look in both
editors; presence suites green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
2026-07-07 21:09:23 +02:00
Gergő Törcsvári
c8c2c5f583
fix(collab): name badges render for real — text overlay above a depth-offset shapes overlay
The full root-cause chain of the empty/mangled badges:
- GAL text justify is painter-residue → PRESENCE_TEXT_OVERLAY pins TOP-LEFT.
- VIEW_OVERLAY::ViewDraw hard-sets EVERY overlay to GetMinDepth(), so the
  shapes and text overlays always collided at one depth, where later-drawn
  fragments lose (and bitmap glyphs are textured quads whose transparent
  cells also write depth — punch-through produced cell-shaped holes instead).
  Fix rides the new fork VIEW_OVERLAY::SetDepthOffset: shapes at min+1,
  labels at min — 'rect first, text on top' now holds regardless of paint
  order. Verified: chips contain crisp names (dark-on-light, white-on-dark)
  for selection tags and cursor labels; presence suites 10/10.
- kicad pointer bump (fork 24c5854d5b).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
2026-07-07 21:09:23 +02:00
Gergő Törcsvári
4c287c2a27
fix(collab): deterministic nameplates — pin GAL text justify + punch chip text through the depth test
Two root causes of the wandering/invisible badge text:
- A plain VIEW_OVERLAY draws with whatever text justify the LAST painter left
  in the GAL (CENTER is only the reset default) — anchoring was
  nondeterministic. PRESENCE_OVERLAY (VIEW_OVERLAY subclass, replaces
  MakeOverlay) pins TOP-LEFT justify before executing its commands; the label
  math is written against that.
- The whole overlay draws at ONE depth and same-depth fragments drawn later
  LOSE the depth test — a chip rect over its text erased the text. Draw the
  text FIRST, the chip rect AFTER: the rect is rejected exactly on the glyph
  pixels, punching the text through.
Verified live: chips contain their names on light (dark text) and dark
(white text) user colors, for both selection tags and cursor labels.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
2026-07-07 21:09:23 +02:00
Gergő Törcsvári
21e3df8fd0
feat(collab): nth-in-room colors, comment toolbar + drag + global hide, luminance chip text
- presence.ts: colors claimed by ARRIVAL ORDER — each client takes the lowest
  free palette slot (no birthday-problem hash collisions; the best palette
  colors go first; N ≤ palette-size editors always distinct). Coordination-
  free: claims ride awareness; simultaneous-join collisions converge (lower
  clientID keeps, the other re-claims), same-user tabs adopt one color,
  claims are sticky across eeschema sheet rebinds (skeletons reuse them).
  colorOf(userId) resolves live colors; comment pins/popovers follow it
  (offline authors fall back to the hash). +2 unit tests (verified live:
  alice=slot0, bob=slot1, roster/pins consistent).
- CommentLayer: single comment icon expanding into a horizontal bar — new
  comment · list · show/hide all (eye empties the GAL pin set + DOM targets;
  re-shown on new-comment/panel-jump). Pins are DRAGGABLE: live LWW anchor
  writes while dragging (peers + the GAL dot follow), nearest-item re-snap on
  drop, click-vs-drag by 4px threshold (verified live: drag synced to the
  peer tab exactly). Shared setThreadAnchor + controller moveThread/
  setPinsVisible/colorFor.
- collab_presence_style.h: chip text color by background luminance (dark on
  light chips, white on dark — BitmapText draws with the stroke color).
- comments/presence-roster e2e updated for the bar + passing; collab units
  56/56; shared 122/122; shared pointer bump.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
2026-07-07 21:09:23 +02:00
Gergő Törcsvári
0f19811602
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 21:09:22 +02:00
Gergő Törcsvári
ce99773af4
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-07 21:09:22 +02:00