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
- 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
- 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
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
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
Each remote selection rectangle now carries the selector's name above its
top-left corner (9px glyph, peer color) and the outline width goes 1.5→2.5px.
Verified live two-tab + presence-pcbnew.spec.ts 5/5.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
Zero kicad-fork changes — all in the embind layer:
- pcbnew_embind.cpp: presence section — canvas wx Bind() triggers (motion/
leave/up/key/wheel) + COLLAB_LISTENER piggyback → post-settle selection
check (dedupe) → onSelection; throttled cursor emit via VIEW::ToWorld;
viewport push/pull (px-per-IU via the GAL matrix — GetScale() is the zoom
and sized the first cut's overlay nm-small); kicadCollabSetRemote renders
peers' cursors (cross + name) and selection bbox outlines into one
per-user-colored VIEW_OVERLAY (CallAfter+COROUTINE), never touching local
selection; PresenceStart/GetSelection/TestSelectFirst/TestClearSelection.
- kicad_editor_embind.cpp: merged-image dispatch (pcb-only until 0003).
- presence-kicad.ts: bindKicadPresence — routes emits into awareness (0001)
and pushes trailing-throttled peer snapshots into the wasm; wired from
WasmTool.startPresence (pcbnew-gated). +5 unit tests.
- tests/kicad/presence-pcbnew.spec.ts: 5 e2e — programmatic + real box-select
emit, throttled cursor, remote render with no-leak + pixel restore,
viewport unit band. Existing pcbnew-collab/items-bridge suites stay green.
Verified live: two tabs over partykit — peer cursor cross + label + selection
outline visible on the other tab's canvas.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
- provider.ts: expose awareness on YjsProvider (partykit/hocuspocus surface
their own; BroadcastChannel gets a sibling-channel relay via awareness-bc.ts)
- presence.ts: createPresence — publish local PresenceState, subscribable
deduped peers() roster, setCursor/setSelection for P2, pagehide fast removal
- PresenceRoster.tsx + WasmTool wiring: facepile chip next to SourceChip;
eeschema rebinds per active sheet (sheet-manager ActiveSheet now carries the
room's provider); config presenceUser() = slug + colorForUser
- new dep y-protocols; unit tests (presence.test.ts) + e2e
tests/web/presence-roster.spec.ts (two tabs, verified vs real partykit)
- bump pcbjam-shared: presence-wire schema (MIT)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
The merged kicad_editor bundle made the chooser's footprint side reachable
from a schematic session; this wires up the data:
- boot.ts/constants.ts: every kicad_editor frame seeds BOTH sym-lib-table and
fp-lib-table (+ placeholder files; a created user lib joins both lists) —
the eeschema frame used to write fp-lib-table empty, leaving the selector
dead. TOOL_LIB_KIND remains only the presync/primary-kind lever.
- publish-libs.ts + kicad-pretty.ts: publish fp-index.json per tag —
[name, uniquePadCount] per footprint (countUniquePads mirrors KiCad's
GetUniquePadCount(DO_NOT_INCLUDE_NPTH)); index-only top-up mode for
already-published immutable tags.
- source.ts/cdn-source.ts: new bridge op "index" (source-global, dispatched
before the lib-id parse) + LibsSource.getFpIndex; the CDN source fetches
<tag>/fp-index.json once (404 ⇒ null ⇒ C++ default-only fallback).
- dev-demo.mjs: --libs-local serves a local publish-libs layout same-origin
at /libs-cdn (mirrors --models-local).
- tests/web/eeschema-fp-selector.spec.ts: e2e — chooser opens in --frame=sch,
selector fills from ONE index crossing, clicking a row per-item-gets the
body and the cross-face GAL preview renders; adaptive for index-less
sources (asserts crash-free default-only selector).
Submodule bumps: kicad (index-backed filterFootprints + preview AsyncLoad
fix + modal-pump crash guard), wxwidgets (modal pump logs e.stack).
Doc: pcbjam-private docs/features/libs/0014-eeschema-footprint-selector.md
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SCWVaCRM9T847PdPwYYajX
The closed API now membership-gates /parties/sync-room/* per request via
the session cookie and no longer puts a bearer token in sync-stack layer
descriptors. Pass a credentials:include fetch into SyncStack so live-layer
HTTP ops (manifest/bodies/put) carry the cookie; the realtime WebSocket
already gets it automatically (same-site handshake).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014dasZuqo6FStgT3rkC85im
The example backend's reflected-origin + allow-credentials pairing is safe
solely because this server holds no ambient credentials by design (default
origin is the explicit :3048; '*' is an operator opt-in). Make that a stated
invariant so adding any auth here forces revisiting the wildcard mode.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016TxciQ5VkNYUZesgMSEMUr
Backends with real auth (session cookies) need credentials:'include' on the
editor's cross-origin fetches — ts-rest clients, project file bytes/upload,
drift keepalive, lib item GET/PUT, sync-stack resolve. Cookie-less setups are
unaffected (the thin identity headers still ride along and same-site Lax
cookies simply don't exist). CDN/static-gallery fetches stay credential-less
(wildcard CORS rejects credentialed requests). The example backend's CORS now
sends allow-credentials — origin reflection was already exact, so this only
lets browsers accept those responses.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016TxciQ5VkNYUZesgMSEMUr
waitForRenderStable now runs two phases (still entirely in-page, still just the
canvas hash — no CDP screenshots):
1. Fast per-rAF convergence — hash every animation frame until stableFrames are
identical (~48ms). Catches high-frequency motion, cheap.
2. Wide confirmation — then re-hash confirmFrames times, each `interval` ms apart
(default 2 × 250ms ≈ 500ms). A slow async repaint (e.g. a file list arriving
after an asyncify readdir) that a few 16ms frames would sail past now forces a
reset back to phase 1.
This closes the hole where three ~16ms samples could declare "settled" ~200ms
before a delayed paint lands. Genuinely-animating states still never converge and
are captured at the timeout, as before.
Cost (local, 4 workers): e2e 62s->103s, kicad 155s->212s — pure waiting, no extra
CPU. Both suites green (e2e 340, kicad 92). Tunable via confirmFrames/interval.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVX1pHMvRPYHdp6ZfEawrk
The determinism conversion silently dropped/renamed screenshots vs the committed
baselines, so CI flagged ~35 baselines as "removed". Restore them:
- pl_editor-*: re-add the "pl_editor-" prefix the conversion dropped (5).
- Buffer screenshots that lost their path: (eeschema draw-wires/loaded-css,
pcbnew draw-lines/loaded-css, pcbnew-move, dark-mode, eeschema-crosshair,
aui-resize) re-add path:'test-results/<name>' so they SAVE and still feed the
in-test pixel-diff analysis (17).
- Dropped animating/loading captures re-added via stableShot / raw screenshot:
timer-03/05/06, dialogs-timer-running/started, wxgrid-02/03, 01-loading (7).
Delete 5 obsolete wizard baselines (eeschema/gerbview/pl_editor/symbol_editor/
generic -wizard-00-initial): every app is now seeded to bypass the first-run
setup wizard (that was the point of removing the wizard for/if loop), so those
states are no longer produced — pl_editor.spec asserts "no wizard appears"
instead.
Net: every committed baseline has a producer again (0 "removed"); determinism
guard green; affected specs pass (e2e 40, kicad 12) and emit the restored PNGs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVX1pHMvRPYHdp6ZfEawrk
Make the Playwright e2e + kicad suites deterministic so screenshot flake stops
tracing to timing races.
- Blind page.waitForTimeout -> condition waits (expect.poll, web-first
assertions, waitUntil) + readiness helpers (waitForWxApp, waitForCanvasApp).
Remaining sleeps are documented interaction dwells (annotated).
- Defensive "if element exists" branches -> loud asserts; label-fallback chains
-> normalized clickMenuItemByText. First-run wizard for/if loops removed by
seeding calculator/gerbview/pcbnew HTMLs.
- Screenshots: new stableShot(page, name) settles the render in-page (canvas
hash over rAF) then writes a raw PNG to test-results/ for the existing offline
gate (tools/screenshots vs baseline-screenshots). Replaces toHaveScreenshot,
which did inline compare + its own baselines and had decoupled the specs from
the real gate. scale:'css' pinned.
- retries: 0 in both configs.
- Guard: tests/tools/lint-determinism.ts (npm run lint:determinism) bans blind
sleeps / toHaveScreenshot / inline retries / swallowed catches in specs;
documented exceptions carry a marker. Rules in tests/TESTING.md.
Assertions, coverage, and renders unchanged (semantic-equivalence reviewed;
captures pixel-identical modulo inherent timer/timestamp/3d-raytrace variance).
Both suites green at retries:0 (e2e 340, kicad 92); ~35-61% faster.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVX1pHMvRPYHdp6ZfEawrk
Miss 08A: the binding stores wire-carried (lib_symbols …) definitions in
kdoc_libsymbols and prefixes them on apply wires — a joiner that never saw a
symbol adopts it WITH its definition (new e2e ysync-libsymbols.spec.ts).
Miss 08B: registerSaveHook gains onSavedText; WasmTool routes saved-file text
to syncLayoutToY (per sheet room via the manager's syncLayoutFromSave, or the
single-room doc) so title block / paper / setup edits converge instead of
drifting. Opt 12 (TS half): zod off the observer hot path (yToItemUnchecked),
children index built once per conversion. Opt 13: seed()'s adopt diffs the
editor snapshot against the doc view and applies only the doc-authoritative
difference — clean rebinds apply nothing, the adopt undo entry shrinks to the
real changed set. Opt 14 deliberately deferred (doc 18). All TS-side; no wasm
rebuild (the C++ blob/findLib sides already carried definitions).
Verified: shared 107, standalone 79 (+2 known pre-existing wasm-assets),
ysync e2e 21/21 chromium, collab regression 21/3-skip firefox.
Bumps: web/pcbjam-shared (lib_symbols channel + syncLayoutToY + opts).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JgThWXtdvrYLK47EDFoGdq
All seven ysync-review bugs fixed and verified; every repro's expected-fail
marker removed (they now run as regression tests). Also fixes two bugs found
while verifying (doc 17 F5/F6): file-seeded Y bodies are re-upserted in the
editor's serialization (doc-16 F4 was an artifact of bug 01), and the
0008-era "asyncify-fragile envelope parse" was really wrapInBoardEnvelope
emitting display layer names — canonical LSET::Name() fixes track/via/zone
v2 applies; makeFromBlob now logs parse errors instead of swallowing them.
Verified: shared 98, standalone collab 37, ysync e2e 20/20 (chromium),
collab regression set 21 passed / 3 pre-existing skips (firefox).
Bumps: kicad (board_commit child-removal listener notification),
web/pcbjam-shared (slot prune + arbitrated seed).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JgThWXtdvrYLK47EDFoGdq
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
New doc 16: the full repro-suite map (per-bug unit/e2e paths with verified
failure sites), the phase-C probe outcome, and four findings only the RUNNING
system revealed:
- F1: bug 03's sending half emits NOTHING — a child-only delete commit never
triggers a flush at all (worse than the doc's predicted bare removed-wire);
the GetWidth-assert tracer evidence and the fix implication.
- F2: Firefox cannot host two kicad_editor tabs in one context (per-process
wasm budget) — bug-01 two-tab repros are Chromium-only.
- F3: headless emit WORKS on both pcbnew and eeschema — the legacy two-tab
skip rationale and items-bridge localEdit omissions are stale.
- F4: drift-detect is strictly ITEM-silent on the green path (no writer-
formatting false positives).
Cross-updates: 00 index + verdict note; 01/04/05/06/07 Verification sections
gain their repro paths; 02 upgraded to runtime-CONFIRMED; 03 gains the F1
empirical correction; 11 (no v2 e2e coverage) CLOSED with a point-by-point
status update — only the legacy retirement remains.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DPfrVhfYgPPgtawjSssZfn
Now that demo.pcbjam.com is live, the landing page leads with it instead
of "coming soon":
- Hero: "Open the live demo" is the primary CTA; the coming-soon panel is
replaced by a DemoLaunchCard (screenshot + play overlay, new tab — the
editor needs COOP/COEP so it can never be iframed here); waitlist demoted
to a ghost button under a "Want early access to multiplayer?" lead-in.
- Removed the Gerber demo and positioning-map sections, then restored the
positioning map per review — recolored for light theme and without the
multiplayer badge/legend ("Everyone picks one. We do both.").
- Light theme: flipped the :root ink tokens, darkened accent/signal for
contrast on white; fixed two dark-assumption bugs it exposed (header CTA
text losing to .nav>a specificity, DemoVideo play scrim derived from --bg).
- Copy: positioned around what's live (real KiCad in a tab) with
multiplayer explicitly "coming — join the waitlist"; cut untrue claims
(GitHub import, self-host, offline PWA -> roadmap); cut the jam metaphor
outside the product name; TrustBar fake-stat grid -> plain claims strip;
footer tagline + fallback meta description aligned.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01REwWZGkAz1AZUJw8yqxkCN
Trim the waitlist microcopy on the standalone demo and the marketing site's
CTA/hero forms to just the value prop, removing the "no spam, unsubscribe
anytime" reassurance line.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01REwWZGkAz1AZUJw8yqxkCN
The three 3D-viewer shots changed because the specs now screenshot the
COMPLETED render (pixel-gated waitForThreeDRender) instead of a fixed-sleep
mid-render frame. The rest is the accumulated unpromoted backlog: eeschema/
symbol-editor drift, and first-ever baselines for the models-spec renders
(tail re-enabled in 561d050), frame-runtime, occ-export/probe and
xface-preferences from earlier merges.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The 3d-webgl merge (kicad eb13ff3bdc: the viewer now defaults to the real
OpenGL renderer via wasm/gl1, and occ-split moves STEP parsing into the
occ_service worker) made the raytracer-era orchestration on this branch moot —
main's chromium-ci phase is green at 15-way parallelism (28666407570 /
28698861536). Drop what no longer earns its complexity, keep the diagnostics,
fix main's live flake, and make the deadlock spec test what it was written for.
- REVERT the chromium-ci-3d serial project, the two-phase test:kicad:ci, the
SwiftShader GPU-process flags, and the resize-drag/models skips: config and
package.json are byte-for-byte back to main's shape. The raytracer contention
they guarded is no longer on the CI path.
- FIX main's live flake: run 28698861536 is green only via retry
(3d-viewer.spec:26 flaky) and 28666407570's deadlock red sampled an ALL-ZERO
pixel signature — the viewer's first frame lags the canvas's creation on
software WebGL under parallel load, and sampling too early reads an all-black
backbuffer. New waitForThreeDRender() gates render assertions on actual
pixels (1s-interval full-frame CPU reads) instead of fixed sleeps, used by
3d-viewer.spec:26 and the models render tail.
- KEEP the storm-proofed samplers (one full-frame getImageData on a
willReadFrequently canvas replacing 256 per-pixel GPU round-trips per sample
— the "GPU stall due to ReadPixels" trigger) and the logThreeDDiag
instrumentation: engine-independent, and they de-risk every remaining
software-GL pixel read.
- models spec: bridge assertions stay front-loaded (the protocol regression
signal is independent of the render); the occ_service parse verdict is now
POLLED — it lands async relative to the bridge ensures, so asserting it
immediately raced the worker; the render tail runs again everywhere. (The
pre-webgl raytracer+models renderer-death documented in a17f3be does not
affect the OpenGL default path — the raytracer-toggle+models combination
remains untested product surface, tracked outside this branch.)
- deadlock spec: the deadlock it guards is raytracer-specific and the viewer
now defaults to OpenGL — on the GL engine it either passes vacuously (fast
renders make every liveness assertion trivial, 28698861536) or fails on the
black first frame (28666407570). It now flips the engine via the "Use
raytracing" toolbar toggle (loud assert if the toggle moved) and
cross-checks engagement by requiring the canvas pixels to CHANGE after the
flip with no input in between (the raytraced frame is lit differently; a GL
re-render reproduces identical pixels; heap growth is unusable — mimalloc
satisfies the raytracer from freed arena pages). That guard immediately
caught a REAL defect: on the webgl-era wasm build the toggle is INERT (the
click lands and "Reload time" updates, but the canvas never changes —
suspects: DoRePaint's silent catch(runtime_error) freezing the canvas after
a raytracer Redraw throw, or ToggleRaytracing writing m_boardAdapter.m_Cfg
while RenderEngineChanged() reads GetAppSettings<…>(), possibly different
instances in the merged bundle). The spec is therefore test.skip-annotated
as a KNOWN ISSUE with the full engine-force machinery in place — unskipping
it self-validates the product fix. The CI-skip also stays (raytracer
liveness needs real-GPU pacing; the Worker-boot deadlock mechanism is
covered on CI by the standalone wx harnesses).
- 180s viewer-open waits kept as pure CI headroom (never slow a passing run).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The headed Mesa-llvmpipe switch (ce44636) was built on a misdiagnosis. With
--use-gl=angle --use-angle=gl under Xvfb, Chromium gets NO WebGL AT ALL on the
GPU-less CI VM: runs 28652367347 + 28664038296 both log
`glcanvas count before opening 3D viewer: 0` (even the pcbnew board's own GAL
canvas fails to create) and every failure is the FIRST viewer-open wait timing
out. "Timeouts instead of crashes" meant no GL context existed to crash, not
that llvmpipe was stable-but-slow. The Mesa/GLX path works for the Firefox
project, but not for Chromium on this image.
Headless SwiftShader is the environment where GL provably works — the
serialized run 28649537489 opened the viewer and rendered/sampled 56 colours
(viewer:26/130/209 all passed single-tab). Its remaining failures split into
two families, each addressed on its own evidence:
1) SwiftShader GPU-process kills (deadlock spec's lost canvas/black frame):
- chromium-ci-3d back to headless + --enable-unsafe-swiftshader, plus
--disable-gpu-watchdog and --disable-gpu-process-crash-limit: heavy churn
stalls the software-GL GPU process until the watchdog shoots it
(CONTEXT_LOST -> removed GL canvas -> "Target crashed"); with the watchdog
off a slow op completes, and without the crash limit a lost GPU process
keeps restarting for later tests. Same args locally (no-ops on a healthy
real GPU).
- Storm-proof every canvas sampler (3d-viewer, models, deadlock specs): one
full-frame getImageData on a willReadFrequently (CPU-backed) 2D canvas +
JS grid sampling, replacing 256 per-pixel getImageData GPU round-trips per
sample — the "GPU stall due to ReadPixels" trigger, worst in the deadlock
spec's 1.5s-interval settle polling. Sampled pixels and sig order are
unchanged.
- edge-resize spec: keep the open + 5-resize-handles assertions on CI, then
test.skip the drag — each resize step re-raytraces synchronously on the
wasm main thread and the 12-step drag blocked mouse.move past the 240s
budget even single-tab. Real-GPU-paced; still runs locally (deadlock
spec's CI-skip from d9900fe unchanged, same rationale).
2) KNOWN PRODUCT BUG uncovered in the models spec (NOT SwiftShader, NOT CI):
raytracing a scene WITH component models kills the Chromium renderer
process outright ~6s after scene build — silent process death with a FLAT
wasm heap (531-637 MB watched at 2s intervals; not OOM), no console error,
no wasm abort, no crash report. Deterministic on a real GPU (5/5),
fixture-independent (700 KB USB-C and 61 KB USON-8 STEP crash identically);
the same death hits CI SwiftShader at ~66s. Board-only raytraces complete
and stay alive, so the bug is specific to the model path. The spec's one
historical green ended at raytrace-age ~6s — inside the death window by
luck; every CI run of it ever died. Fix here: front-load the bridge
assertions (ensure requests fire during scene BUILD, before the raytrace),
so protocol regressions still fail the suite everywhere, and test.skip the
render tail unconditionally with the bug documented at the skip site. The
raytrace->blit pipeline stays gated on CI by 3d-viewer.spec's board-only
render. Bug tracked for a dedicated fix session.
Correct the now-wrong llvmpipe comments (config, open-wait rationale, deadlock
skip reason). Expected CI matrix: viewer open/render/stacking/titlebar +
models-bridge on SwiftShader serial; edge-resize drag + camera-move deadlock
on real GPUs locally; the Worker-boot deadlock mechanism stays covered on CI
by the standalone wx harnesses.
Locally validated on a real GPU (--project=chromium): 5 passed + 1 skipped
(the documented models render tail).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follow-up to the headed-llvmpipe switch. That run (28652367347) stopped CRASHING —
every failure became a plain timeout, confirming llvmpipe gives a stable WebGL context
(no more CONTEXT_LOST / "Target crashed") but software raytracing is just slow:
- The first board raytrace takes ~60s on llvmpipe, right at openThreeDViewer's 60s
wait → raise it to 180s (shared helper + the models spec's inline copy). Real GPU
returns in ~2s, so it is only CI headroom.
- The deadlock spec asserts the wasm main thread stays responsive within ~15s DURING a
raytrace — a premise only true on a fast (real) GPU. Under slow software rendering a
legitimately-slow raytrace is indistinguishable from a deadlock, so skip it on CI
(test.skip on process.env.CI). The Worker-boot deadlock MECHANISM stays covered on CI
by the standalone coroutine-pthread-ondemand / raytrace-threads wx harnesses; the spec
still runs locally on a real GPU.
- Gate the headed + llvmpipe launch flags on CI so chromium-ci-3d behaves like a normal
bundled-Chromium project locally.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Serializing the 3D specs (prior commit) proved the crashes are NOT concurrency: with
--workers=1 (one tab at a time) the same three heavy-interaction specs still failed
(run 28649537489). The [DIAG] instrumentation + trace console pinned the real cause —
under HEADLESS SwiftShader on the GPU-less CI VM, the CPU raytracer's WebGL blit plus
the tests' drawImage(glCanvas) pixel reads trigger "GPU stall due to ReadPixels", and
Chromium's GPU watchdog then resets the context (CONTEXT_LOST_WEBGL) or crashes the
renderer ("Target crashed") — even single-tab.
Run chromium-ci-3d HEADED under the CI's Xvfb with Mesa llvmpipe (--use-gl=angle
--use-angle=gl) — the same software-WebGL stack the Firefox CI project already relies
on — and disable the GPU watchdog / crash-limit so a slow ReadPixels completes instead
of being killed. --enable-unsafe-swiftshader kept only as a last-ditch fallback. The
serial --workers=1 pass + phase-1 worker cap stay (cheap, and they removed concurrency
as a variable).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The three heavy 3D-viewer specs (3d-viewer, 3d-viewer-deadlock, 3d-viewer-models)
crashed on CI (run 28604015154): "Target crashed", "browser has been closed", and a
black frozen canvas. Each boots the 3D-enabled pcbnew build, which pre-warms
~hardwareConcurrency*2+8 (~68 on the 30-core VM) Web Workers AND runs a multi-threaded
CPU raytracer over SwiftShader software-WebGL. Under fullyParallel the three spec FILES
run in separate browser processes concurrently, and the simultaneous raytrace tabs
exhaust the shared GPU process's ~16 live-WebGL-context limit / renderer memory. The
pool pre-warm (the deadlock fix, 7630c7e) is correct and present in the tested binary;
per-file isolation prevented in-process Worker accumulation but NOT this cross-process
concurrency — the gap these specs' first CI exposure hit.
Fix (test-orchestration only; no wasm/submodule changes):
- Route the three specs to a dedicated `chromium-ci-3d` Playwright project (own
outputDir=pw-artifacts/kicad-3d so its failure traces survive the later perf run's
wipe; the existing tests/pw-artifacts/** upload already captures it).
- test:kicad:ci runs it as a SECOND `playwright test` pass with --workers=1, so at most
one raytrace tab is alive at a time (the environment in which a single 3D load is
reliably green — docs/features/3d-raytracer/README.md §7). An exit-code-aggregating
sequencer runs both phases and fails if either fails (no silent skip of the 3D pass).
- Cap phase-1 CI workers to 12 as headroom under the WebGL-context cap.
- Log a resource-diagnostic snapshot (hardwareConcurrency, pthread pool, GL-canvas
count, wasm/JS heap) before each 3D interaction so any recurrence is attributable.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The demo (demo.pcbjam.com) cross-posts the waitlist to the marketing site's
serverless endpoint. It targeted the apex pcbjam.com, which 308-redirects to
www on Vercel; a CORS preflight can't follow redirects, so the OPTIONS failed
("Redirect is not allowed for a preflight request") and the POST never landed.
Point the waitlist target at the canonical www host. www.pcbjam.com already
returns 204 with access-control-allow-origin: https://demo.pcbjam.com, so the
preflight passes. Landing/version-badge link stays on the apex.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01REwWZGkAz1AZUJw8yqxkCN
Demo (web/standalone): retitle "KiCad Web" -> "PCBJam - KiCad demo", add
description + OG/Twitter tags (absolute URLs pinned to demo.pcbjam.com),
ship favicons (page had none), add the og-demo.png share card, and show an
"Early-access alpha" badge next to the home-page heading.
Site: add the missing og/og-home.png (BaseLayout referenced it but the
asset never existed -> og:image 404ed in prod) and set astro `site` so
canonical/OG URLs resolve absolutely (prerendered pages shipped
canonical=http://localhost:4321/ to production).
Cards are 1200x630, generated from the favicon.svg logo mark on a pure
white background.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The rebase-landing of 3d-webgl-port rewrote the kicad submodule branch
(979e4d1ef7 -> eb13ff3b, same content on the occ-split base) and
force-pushed it, but the rebased root commits kept the gitlink at the
now-orphaned pre-rebase SHA — every checkout of main (CI actions/checkout
included) failed with 'upload-pack: not our ref 979e4d1ef7'.
Point the gitlink at eb13ff3b, which both kicad wasm-port and
3d-webgl-port already reference. Lesson: when a landing rebases a
submodule too, bump the root gitlinks to the rewritten SHAs and push the
submodule first (scripts/git-workflow/repo-status.sh catches this).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reads the compare-dirs report.json of the parity + webgl-self levels (when
present — inert for builds without the 3D harness) and appends one line:
'✅ 3D renderer: parity 47/47 · webgl-self 47/47', or on drift the changed
count + worst offender with a pointer to the run-artifact triptychs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- build 3d-webgl test app after gal-webgl (same recipe: wx build + docker
sysroot headers); the capture spec already lives in the default e2e suite
and self-skips when the app is missing, so it now runs
- report-only parity + webgl-self step after the e2e suites
(continue-on-error: the committed baseline-webgl set is Mac-Chromium-
sourced — promote CI renders and flip gating if SwiftShader drifts past
the 0.005 floor)
- exclude tests/apps/3d-webgl from the test-apps cache (rebuilt every run,
like gal-webgl) and upload tests/3d-regression/output/** (renders, diffs,
the 3d:review triptych gallery) with the e2e artifacts
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
compare-dirs gains --artifacts changed|always: 'always' also writes the
side-by-side triptych + heatmap for pairs that PASS the floor (blue-grey
UNCHANGED caption), so a human can eyeball old-vs-new without a failing
gate. The gating scripts keep the default (empty diff dir = green);
3d:review writes the full 47-pair set to output/diff/parity-review/.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
kicad pointer bump: the __EMSCRIPTEN__ raytracer force-pins are removed, so
the viewer defaults to RENDER_3D_OPENGL running on the wasm/gl1 GL1->WebGL2
emulation layer (the raytracer stays available as the RAYTRACING engine).
Verified: tests/kicad 3d-viewer + 3d-viewer-models e2e green against the
kicad_editor build; 3d-regression parity 47/47.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- baseline-webgl/: first population from the passing local Chromium renders
(47/47 under the parity floor) — activates the webgl-self regression gate
(floor 0.005). CI may re-promote its own renders if the Linux/swiftshader
rasterization differs beyond the floor (the README's CI-promote flow).
- compare-dirs.ts: recreate the output dir on every run — triptychs/heatmaps
are only written for CHANGED pairs, so artifacts from earlier (redder) runs
lingered and misreported scenarios that had since gone green (a stale
red-state gen-cylinder triptych survived three green runs).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- gl1_glu: gluCylinder/gluDisk/gluSphere per the SGI GLU reference
tessellation (sin/cos phase, strip emission order, pole fans — Apple's
GLU derives from the same source, so vertices match the goldens);
emission goes through the public glBegin/glVertex entry points so
quadrics record into display lists
- glLineWidth>1 one-time warning (browsers clamp to 1; affected coverage
stays under the parity floor — zero floors.json overrides needed)
- production link site: build-kicad-target.sh now compiles wasm/gl1 from
sources.txt and applies wrapped_symbols.txt --wrap flags (same manifests
as the test harness); kicad_editor's RENDER_3D_OPENGL links against the
real emulation layer instead of no-ops
- wasm/stubs/gl_ffp_stub.c deleted (role fully superseded; history in git)
- docs: 3d-regression README port-status section (floor-blind small
geometry + line-width caveats), harness comments de-red-stated
Parity: 47/47 under the 0.02 floor, zero overrides. Native golden path
byte-identical to bb0e238 (whose gate ran green) — scenarios/, native/,
baseline/, manifest, floors, spec all untouched by the port.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- display lists: literal command replay through the shim state machine
(state leaks + between-list material changes get GL semantics for free);
recorded glDrawArrays EAGERLY snapshots enabled client-memory arrays at
compile time (the renderer frees them right after glEndList) and records
buffer+offset for VBO-backed pointers
- client-array draws: shared AttribSource path packs client-memory arrays
into one scratch-VBO upload and binds user VBOs with original offsets
(GL_BYTE normals / GL_UNSIGNED_BYTE colors normalized per GL1)
- glDrawElements over user VBO/IBO passes indices through untouched
(ELEMENT_ARRAY_BUFFER never touched by the shim)
- state mutators (enable/bindTexture/blendFunc/lineWidth/alphaFunc) shared
between the __wrap interceptors and list replay
Parity: 44/47 under the 0.02 floor (was 20). Verified visually: mini-board
Tier-3 composite (mask translucency + stencil-punched TH holes), stencil
hole subtraction, alpha-tested seg-end discs, COMBINE const-alpha model
transparency, and post-machining reproducing the upstream countersink bug
(walls absent, matching the native golden). Remaining 3: camera-* trio
needing the M4 GLU quadrics (+5 small-geometry false-greens they share).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
wasm/gl1: the GL1.x fixed-function emulation layer replacing the
gl_ffp_stub.c no-ops in the 3d-regression harness link. This batch:
- symbol split: 52 FFP-only entry points implemented; 10 Emscripten-owned
names intercepted via wasm-ld --wrap (sources.txt/wrapped_symbols.txt are
the shared manifests for both link sites; production hookup lands in M7)
- matrix stacks (MODELVIEW/PROJECTION, glGetFloatv readback), immediate
mode with all 8 GL1 primitive conversions, GL1-default state mirror
- full GL 1.5 Gouraud lighting uber-shader (eye-space light capture at
glLightfv time, color-material, two-side, COMBINE evaluator + alpha test
wired but inert until M3/M5)
- draw routing: FFP traffic identified by GL_VERTEX_ARRAY client state;
blit/2D-GAL draws pass through untouched
- display lists: correct glGenLists/glIsList existing-empty semantics;
recorder itself is M3 (recorded commands drop with a one-time warning)
Parity: 20/47 under the 0.02 floor, of which 12 verified genuinely
rendering (bg-gradient x2, bounding-box, half-open-cylinder, segment x2,
material x3, light x3 — eyeballed against baselines); the other 8 are
small-geometry scenarios whose missing GLU/display-list content sits under
the floor (become real in M3/M4).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A corrupted homebrew python3 (self-referencing exec wrapper) made the
config-load-time free-port helper hang forever, stalling every playwright
run on the machine. With a timeout the existing catch falls back to the
random-port path instead.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
tests/3d-regression mirrors the gal-regression pattern at renderer scale:
shared C++ scenarios call real KiCad 3D-viewer code (opengl_utils, display
lists + DrawCulled stencil subtraction, MODEL_3D VBOs, private generators via
a rob-template accessor, and full reload()+Redraw() composites over a
synthetic BOARD_ADAPTER). A native macOS harness renders them on real OpenGL
into 47 committed goldens (bit-deterministic, FBO capture); the wasm harness
compiles the same TUs against wasm/stubs/gl_ffp_stub.c no-ops so every
scenario renders blank — the TDD red state (parity meter: 47/47 changed).
Comparisons use the CI pixelmatch engine via the new generic compare-dirs.ts
(floors.json levels; manifest.json cmp-guards registry drift).
Documents an upstream bug: appendPostMachiningGeometry's countersink path
adds middle quads without normals, silently erasing the walls of any
display list it is batched into (3d-post-machining.png keeps the lists
separate to record it).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
An idle builder keeps the Docker Desktop VM ballooned: each per-worktree
compose project carries its own ${KICAD_DOCKER_MEM:-32G} cap, build.sh left
the container running afterwards ("interactive use"), and two forgotten
builders plus the VM's page-cache/high-water behavior read as ~56 GB of host
RAM during a ~20 GB build.
stop_builder() now runs from the EXIT trap on every path (success, failure,
Ctrl-C — both the sequential and the pipeline-mode trap). The container is
pure scaffolding — all caches live in the named volumes and `up -d` restarts
it in seconds — so stopping costs nothing. KICAD_KEEP_CONTAINER=1 restores
the old keep-running behavior for interactive debugging; docker/shell.sh is
unaffected (it does its own `up -d`).
Verified live: the full `all --build-deps` run on main left zero containers
running at exit.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Move OpenCASCADE out of the merged editor image into occ_service: a separate
emscripten module (-sASYNCIFY=0, MODULARIZE, in-container -Oz finalize, 2N+8
pre-warmed pthread pool) booted lazily in a dedicated Web Worker on the first
STEP export or STEP/IGES model parse. kicad_editor.wasm ~190 MB -> 130 MB;
sessions that never touch OCC never fetch its 57 MB. STEP export works in the
browser for the first time: the unchanged desktop dialog runs EXPORTER_STEP,
whose wasm shadow suspends into globalThis.occService and the export bytes go
straight to a browser download (never entering the editor heap). STEP/IGES 3D
models parse in the worker via the oce shadow (S3D WriteCache/ReadCache wire).
- wasm/occ-service/: service CMake target (hooked from the kicad fork's
top-level CMakeLists, wasm/editor pattern), embind entry
(occExport/occLoadModel), wxConfig pre-js.
- wasm/stubs/{exporter_step,oce_plugin}_stub.cpp: EM_ASYNC_JS worker bridges
(callee-shadowing; no caller #ifdefs).
- web/standalone: provider installed whenever the kicad_editor bundle boots
(cross-face safe); ONE shared worker-boot source occ-worker.js (vite ?raw;
the e2e stub reads the same file) — blob worker with locateFile absolutized
against the glue URL; export download-name guard.
- deps: OCC builds with RapidJSON so its glTF/GLB writer exists — pinned to
the vcpkg master snapshot 2025-02-26 (24b5e7a8b27f), the same code official
KiCad consumes via vcpkg.json's opencascade[rapidjson]; rapidjson's latest
tag (v1.1.0, 2016) is ill-formed under modern clang.
- tests: occ-export dialog e2e (lazy-fetch boundary + STEP download bytes),
occ-probe incl. a 9-format matrix (step/stpz/brep/xao/ply/stl/glb/u3d/pdf),
3d-viewer-models hard-asserts the worker parse; occ provider stub installed
ambiently by the kicad fixtures.
Validated against desktop kicad-cli 10.0.4: geometric exact equality (bbox
delta 0 um, volume delta 0.0000%) for STEP/GLB/STL/BREP/STPZ across three
boards and option sweeps — with desktop OCC 7.9 vs wasm OCC 7.8; PLY/XAO/PDF
structurally equal; U3D same-size (quantizer float LSBs differ). Full kicad
e2e green on Firefox and Chromium; standalone verified end to end (lazy fetch
only on the Export click; export.step 60,628 B ISO-10303-21; loadModel 700 KB
STEP -> 569 KB scenegraph cache).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ensureModelInMemfs memoized materialized refs in a Set<string> and, on a cache
hit, returned `${MODELS_3D_ROOT}/${ref}` — the ref's OWN path. But the wrl→step
fallback writes the body under the .step filename and returns that. So the first
ensure (prescan) wrote M.step and returned it, while the second ensure (the C++
viewer's PCBJAM_3D::EnsureModelFile lazy fallback) hit the early-return and got
back M.wrl — a file never written. KiCad then stat'd the missing .wrl
(S3D_CACHE::checkCache → GetModificationTime), so every component model on a
KiCad-6-vintage (all-.wrl) board failed with "Failed to retrieve file times for
'….wrl' (error 44)" and nothing rendered.
Fix: memoize ref → the ABSOLUTE path actually written (Set<string> →
Map<string,string>) and return that; doEnsure reuses an on-disk body via
analyzePath and records the real target. Regression test: a second ensure of a
.wrl ref returns the .step path, not the .wrl.
JS-only, no pcbnew rebuild. Only bit boards that reference .wrl (the CDN is
STEP-only from 10.x) AND double-ensure via prescan + lazy fallback — which is
why .step-ref boards worked.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
All four editors (PCB / Footprint / Schematic / Symbol) are now runtime --frame
choices of a single kicad_editor.wasm (178 MB at -O1 vs 147+82 separate; shared
wx/common/boost linked once). One editor per page load, as before; frames pcb /
fpedit / sch / symedit.
- wasm/editor/: the merged executable target (single_top + both kiface library sets,
whole-archive pcbcommon) + the safety-net focus-walk Kiface() dispatch TU. Gated by
KICAD_WASM_MERGED_EDITOR (kicad submodule bump carries the fork side: per-engine
Kiface/getter binding + ODR renames + dual-kiface launcher).
- wasm/bindings/: per-editor collab entries renamed pcbCollab*/schCollab* (JS names
unchanged); duplicate kicadOpenFile/kicadCollabOnSave + shared-name registrations
guarded behind KICAD_MERGED_EMBIND; new kicad_editor_embind.cpp registers each
shared JS name once, dispatching on the live frame.
- Build: kicad_editor app (build wrapper, target case arms, 3-object embind compile
with the ABI-critical flags, STUB_APP=pcbnew); docker/build.sh "all" =
kicad_editor calculator pl_editor gerbview (pcbnew/eeschema stay as explicit debug
apps); scripts/kicad/audit-merged-symbols.sh = repeatable ODR-collision audit (run
on kicad bumps).
- Frontend: Bundle type (bundle ≠ tool); TOOL_BUNDLE maps all four editors to
kicad_editor; explicit --frame tokens for pcbnew (pcb) and eeschema (sch); publish
list = the 4 real bundles.
- Tests/CI: five harnesses load kicad_editor.js with explicit frame tokens;
PCBNEW_FAMILY_SPECS renamed BIG_MODULE_SPECS + the 8 eeschema-family specs (they
now boot the merged module — SpiderMonkey x86 CI OOM routing); frame-runtime spec
covers all four frames from the one bundle.
Validated so far: frame-runtime 4/4 (each frame boots with the right title, no
aborts, no duplicate embind registration); 24-spec merged-module regression green;
3D raytracer renders. Known pre-existing failure: 3d-viewer title-bar drag deadlock,
fixed on main by 7630c7e (2N+8 pthread pre-warm) — picked up by the follow-up rebase.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The footprint and symbol editors are no longer separate WASM bundles: the frontend loads the parent pcbnew/eeschema bundle and passes --frame=fpedit / --frame=symedit (TOOL_BUNDLE + TOOL_FRAME -> Module.arguments in boot). Drops the two duplicate build+deploy targets and their wrapper scripts + vestigial embind; adds low-level harnesses (footprint_editor.html, symbol_editor.html) and a runtime-frame spec. Bumps the kicad submodule to the runtime --frame launcher.
The frame-runtime spec is listed in PCBNEW_FAMILY_SPECS so CI routes it to the chromium-ci (V8) project — its footprint case boots the pcbnew module, which OOMs SpiderMonkey on x86 CI. Includes the editor-unification dossier (research docs 01-04 + the as-built implementation record 05).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>