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
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
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