refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
|
/*
|
|
|
|
|
|
* Collab presence core (collab-presence 0002/0003/0005/0006/0007) — the
|
|
|
|
|
|
* editor-agnostic half of the presence layer, shared by the pcbnew and
|
|
|
|
|
|
* eeschema binding TUs so the two implementations can't drift.
|
|
|
|
|
|
*
|
|
|
|
|
|
* Everything here talks only to the shared KiCad base classes
|
|
|
|
|
|
* (EDA_DRAW_FRAME, SELECTION_TOOL, KIGFX::VIEW) — the small per-editor
|
|
|
|
|
|
* remainder (how to resolve a KIID, how to draw one peer's selection/xsel,
|
|
|
|
|
|
* what the selection emit payload carries) plugs in through CORE's hooks.
|
|
|
|
|
|
* Each binding TU owns ONE CORE instance in its anonymous namespace, so the
|
|
|
|
|
|
* merged kicad_editor image keeps today's per-editor state separation.
|
|
|
|
|
|
*
|
|
|
|
|
|
* The drawing itself (shapes, labels, cursors, pins, style knobs) stays in
|
|
|
|
|
|
* collab_presence_style.h; this header owns the state + event/scheduling
|
|
|
|
|
|
* machinery around it.
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
#pragma once
|
|
|
|
|
|
|
|
|
|
|
|
#ifdef __EMSCRIPTEN__
|
|
|
|
|
|
|
|
|
|
|
|
#include <chrono>
|
|
|
|
|
|
#include <functional>
|
|
|
|
|
|
#include <map>
|
|
|
|
|
|
#include <memory>
|
|
|
|
|
|
#include <string>
|
|
|
|
|
|
#include <vector>
|
|
|
|
|
|
#include <wx/event.h>
|
|
|
|
|
|
#include <wx/string.h>
|
|
|
|
|
|
#include <nlohmann/json.hpp>
|
|
|
|
|
|
|
|
|
|
|
|
#include <class_draw_panel_gal.h>
|
|
|
|
|
|
#include <eda_draw_frame.h>
|
|
|
|
|
|
#include <eda_item.h>
|
|
|
|
|
|
#include <kiid.h>
|
|
|
|
|
|
#include <pcbjam_remote_lock.h>
|
|
|
|
|
|
#include <tool/selection_tool.h>
|
|
|
|
|
|
#include <tool/tool_manager.h>
|
|
|
|
|
|
#include <view/view.h>
|
|
|
|
|
|
#include <view/view_overlay.h>
|
|
|
|
|
|
|
|
|
|
|
|
#include "collab_common.h"
|
|
|
|
|
|
#include "collab_presence_style.h"
|
|
|
|
|
|
|
|
|
|
|
|
namespace pcbjam_presence {
|
|
|
|
|
|
|
|
|
|
|
|
struct PEER
|
|
|
|
|
|
{
|
2026-08-28 16:14:44 +02:00
|
|
|
|
std::string id; // awareness identity (cursor-only updates key on it)
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
|
std::string name;
|
|
|
|
|
|
KIGFX::COLOR4D color;
|
|
|
|
|
|
bool hasCursor = false;
|
|
|
|
|
|
VECTOR2D cursor; // world coords (IU)
|
|
|
|
|
|
std::vector<KIID> selection;
|
|
|
|
|
|
// Cross-app selection (0006): the peer's selection in the OTHER editor,
|
|
|
|
|
|
// as SYMBOL uuids (pcbnew footprint paths are stripped to their tail by
|
|
|
|
|
|
// the TS side). Ghost-rendered by the per-editor drawPeerShapes hook.
|
|
|
|
|
|
std::vector<KIID> xsel;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Comment pin dot (collab-presence 0005): the GAL half of the hybrid pin —
|
|
|
|
|
|
// zero pan/zoom lag; the clickable hit target + thread popover are DOM.
|
|
|
|
|
|
struct PIN
|
|
|
|
|
|
{
|
|
|
|
|
|
std::string id;
|
|
|
|
|
|
std::string name; // author (palette-override rehash key)
|
|
|
|
|
|
VECTOR2D pos; // world coords (IU)
|
|
|
|
|
|
KIGFX::COLOR4D color;
|
|
|
|
|
|
bool resolved = false;
|
comments-ux: figma bubble pins, floating panel, seen/reactions/mentions UI, theme follow (0001 A–E + 0002)
- GAL pin = one closed polygon: round body, squared-off bottom-left corner
ON the anchor; PIN gains unread (accent ring); tuner knobs; shipped
defaults r9/ring4/alpha.9. DOM hit/highlight sized+offset from a LIVE
pin-geometry radius store the tuner feeds.
- Floating comments panel: draggable (shared useDraggablePanel with
always-onscreen restore; overlay FAB retrofitted), collapsible to header,
header carries add/show-hide/mark-all; unread badges (rose on mention).
- Reactions (emoji-mart lazy, quick-row) + @-mention autocomplete
(MentionInput; backend roster with presence/author fallback).
- Theme: ?theme= > storage > OS, no-flash boot, toggles (HomePage + overlay
View row), boot-seeded pcbjam-dark schematic colors + kicadSetColorTheme /
kicadSetDarkChrome bridges (canvas + wx chrome live flip), light/dark
variants across all overlay surfaces.
- e2e: panel/seen/reactions/mentions/theme specs + resize-spec geometry;
bumps pcbjam-shared (flat-key seen/reactions + listCollaborators) and
wxwidgets (dark chrome) pointers.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HLwn1toiNKi1MgxGKnZTes
2026-07-24 13:21:22 +02:00
|
|
|
|
bool unread = false; // comments-ux 0001 C: accent ring
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
inline long long nowMs()
|
|
|
|
|
|
{
|
|
|
|
|
|
return std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
|
|
|
|
std::chrono::steady_clock::now().time_since_epoch() )
|
|
|
|
|
|
.count();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-28 19:42:31 +02:00
|
|
|
|
/** Findings W-1/W-4: `json::value( key, default )` only defaults on a MISSING
|
|
|
|
|
|
* key — a present `null` (what JSON.stringify makes of ±Infinity/NaN) throws
|
|
|
|
|
|
* nlohmann type_error.302 across embind. Read numbers permissively. */
|
|
|
|
|
|
inline double numOr( const nlohmann::json& aObj, const char* aKey, double aDefault )
|
|
|
|
|
|
{
|
|
|
|
|
|
if( !aObj.is_object() )
|
|
|
|
|
|
return aDefault;
|
|
|
|
|
|
|
|
|
|
|
|
auto it = aObj.find( aKey );
|
|
|
|
|
|
return it != aObj.end() && it->is_number() ? it->get<double>() : aDefault;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
|
inline KIGFX::COLOR4D parsePeerColor( const std::string& aHex )
|
|
|
|
|
|
{
|
|
|
|
|
|
if( aHex.size() == 7 && aHex[0] == '#' )
|
|
|
|
|
|
{
|
|
|
|
|
|
long v = strtol( aHex.c_str() + 1, nullptr, 16 );
|
|
|
|
|
|
return KIGFX::COLOR4D( ( ( v >> 16 ) & 0xff ) / 255.0, ( ( v >> 8 ) & 0xff ) / 255.0,
|
|
|
|
|
|
( v & 0xff ) / 255.0, 0.9 );
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return KIGFX::COLOR4D( 0.23, 0.51, 0.96, 0.9 ); // palette blue fallback
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Per-editor presence state + machinery. One instance per binding TU; the
|
|
|
|
|
|
* hooks below are set once at construction (see presenceCore() in each TU).
|
|
|
|
|
|
*/
|
|
|
|
|
|
struct CORE
|
|
|
|
|
|
{
|
|
|
|
|
|
using json = nlohmann::json;
|
|
|
|
|
|
|
|
|
|
|
|
// ── per-editor hooks ──────────────────────────────────────────────────
|
|
|
|
|
|
/** The live editor frame, or nullptr (the usual dynamic_cast probe). */
|
|
|
|
|
|
std::function<EDA_DRAW_FRAME*()> frame;
|
|
|
|
|
|
|
|
|
|
|
|
/** The editor's selection tool (GetTool<> needs the concrete type). */
|
|
|
|
|
|
std::function<SELECTION_TOOL*( EDA_DRAW_FRAME* )> selectionTool;
|
|
|
|
|
|
|
|
|
|
|
|
/** What checkSelection hands to JS: a bare uuid array (eeschema) or the
|
|
|
|
|
|
* 0006 {uuids, fpPaths} payload (pcbnew). */
|
|
|
|
|
|
std::function<json( EDA_DRAW_FRAME* )> selectionEmitPayload;
|
|
|
|
|
|
|
|
|
|
|
|
/** Resolve a KIID to a live item, or nullptr (releaseSelection). */
|
|
|
|
|
|
std::function<EDA_ITEM*( EDA_DRAW_FRAME*, const KIID& )> resolveItem;
|
|
|
|
|
|
|
|
|
|
|
|
/** Draw ONE peer's selection boxes + cross-app ghosts into the overlays
|
|
|
|
|
|
* (item resolution and exact-outline rendering are editor-specific).
|
|
|
|
|
|
* Cursors and pins are drawn by the shared redraw loop. */
|
|
|
|
|
|
std::function<void( CORE&, EDA_DRAW_FRAME*, const PEER&, const KIGFX::COLOR4D&, double )>
|
|
|
|
|
|
drawPeerShapes;
|
|
|
|
|
|
|
|
|
|
|
|
// ── state ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
std::vector<PEER> peers;
|
|
|
|
|
|
std::vector<PIN> pins;
|
|
|
|
|
|
// Remote soft-locks (0007): uuid → holding peer's display name, derived
|
|
|
|
|
|
// by the TS side from ALL other clients' live selections (own user's
|
|
|
|
|
|
// other tabs included). Consulted by the fork's PCBJAM_REMOTE_LOCK query
|
|
|
|
|
|
// from the selection/move tools. Ephemeral — replaced on every setRemote.
|
|
|
|
|
|
std::map<KIID, std::string> locks;
|
|
|
|
|
|
// Every visual knob — see collab_presence_style.h; live-patched by
|
|
|
|
|
|
// kicadCollabSetStyle (tuner). Editors seed their own defaults.
|
|
|
|
|
|
STYLE style;
|
|
|
|
|
|
|
|
|
|
|
|
std::shared_ptr<KIGFX::VIEW_OVERLAY> overlay;
|
feat(collab): follow-user (collab-presence 0008) + chip depth-layer fix
Follow-user: click a peer's roster avatar to mirror their viewport until
local input breaks it.
- collab_presence_core.h: CORE::fitViewport(cx, cy, halfW, halfH) — fit the
leader's world rect with CONTAIN semantics (zoom derived from the
follower's own canvas via the ToScreen ratio; GetScale is the zoom, not
px/IU). Exported as kicadCollabFitViewport from both editor TUs + the
merged dispatcher.
- presence-kicad.ts: publish the visible world rect (viewportRect) into
awareness, 100 ms trailing throttle; guarded for pre-0008 handles.
- follow-user.ts: createFollow — follows an awareness CLIENT (a tab, not a
user); applies leader rect changes via FitViewport, dedupes unchanged
republishes; break-on-interact compares local onViewport echoes against
the last applied rect (2% rel tolerance, echo-grace before the first fit
lands); unfollows on leader-left; pauses on eeschema sheet mismatch.
- PresenceRoster: avatars are follow toggles (ring on the followed peer);
WasmTool renders the "Following <name> — move to stop" banner.
- tests: 7 controller units (85/85 collab), fitViewport round-trip e2e in
both kicad presence specs (20/20), two-tab tests/web/follow.spec.ts
(converge → track → wheel-zoom breaks → subsequent moves ignored).
Chip depth-layer fix (user-reported): name chips washed out inside
low-alpha selection fills — chip rects shared the shapes overlay's single
depth, and same-depth fragments drawn LATER lose the depth test, so an
earlier-painted fill rejected the chip's pixels. Now three layers via the
fork's VIEW_OVERLAY::SetDepthOffset: text (0) < chips + pin dots (1) <
selection shapes (2). drawLabel/drawCursor/drawSelectionBox take the chip
overlay explicitly; comment-pin dots move to the chip layer too (the 0005
"drawn last so pins sit above" comment had the rule backwards). Verified
with a chip-inside-30%-fill pixel repro + the full presence suite.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-09 09:30:40 +02:00
|
|
|
|
// Name chips + pin dots render one depth unit ABOVE the selection shapes
|
|
|
|
|
|
// (else an earlier-painted low-alpha fill rejects the chip's fragments and
|
|
|
|
|
|
// the tag washes out to the fill's alpha), and the text one unit above the
|
|
|
|
|
|
// chips — see the depth-layering note in collab_presence_style.h.
|
|
|
|
|
|
std::shared_ptr<KIGFX::VIEW_OVERLAY> chipOverlay;
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
|
std::shared_ptr<KIGFX::VIEW_OVERLAY> textOverlay;
|
2026-08-28 16:14:44 +02:00
|
|
|
|
// Cursors live on their own overlay trio (findings Y-4): a peer's 20 Hz
|
|
|
|
|
|
// cursor tick repaints these only; the selection/xsel/pin shapes above
|
|
|
|
|
|
// are repainted only when a selection, lock, pin or zoom changes.
|
|
|
|
|
|
std::shared_ptr<KIGFX::VIEW_OVERLAY> cursorOverlay;
|
|
|
|
|
|
std::shared_ptr<KIGFX::VIEW_OVERLAY> cursorChipOverlay;
|
|
|
|
|
|
std::shared_ptr<KIGFX::VIEW_OVERLAY> cursorTextOverlay;
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
|
|
|
|
|
|
|
bool started = false;
|
|
|
|
|
|
bool redrawScheduled = false;
|
2026-08-28 16:14:44 +02:00
|
|
|
|
bool shapesDirty = false;
|
|
|
|
|
|
bool cursorsDirty = false;
|
|
|
|
|
|
bool docChangeScheduled = false;
|
|
|
|
|
|
std::string lastShapeSig; // setRemote dedupe: shapes vs cursor-only change
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
|
bool selCheckScheduled = false;
|
|
|
|
|
|
std::string lastSelectionJson; // dedupe: emit only when the payload changed
|
|
|
|
|
|
long long lastCursorEmitMs = 0;
|
|
|
|
|
|
double lastVpScale = 0.0;
|
|
|
|
|
|
VECTOR2D lastVpCenter;
|
2026-07-09 16:31:41 +02:00
|
|
|
|
VECTOR2I lastVpSize;
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
|
|
|
|
|
|
|
// ── local state emit (selection / cursor / viewport) ──────────────────
|
|
|
|
|
|
|
|
|
|
|
|
json selectionUuids( EDA_DRAW_FRAME* aFrame )
|
|
|
|
|
|
{
|
|
|
|
|
|
json uuids = json::array();
|
|
|
|
|
|
|
|
|
|
|
|
SELECTION_TOOL* selTool = selectionTool( aFrame );
|
|
|
|
|
|
|
|
|
|
|
|
if( !selTool )
|
|
|
|
|
|
return uuids;
|
|
|
|
|
|
|
|
|
|
|
|
for( EDA_ITEM* item : selTool->GetSelection() )
|
|
|
|
|
|
uuids.push_back( pcbjam_collab::toUtf8( item->m_Uuid.AsString() ) );
|
|
|
|
|
|
|
|
|
|
|
|
return uuids;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Post-settle selection emit: read the selection AFTER the triggering event
|
|
|
|
|
|
// finished (CallAfter), dedupe against the last emitted set, hand to JS.
|
|
|
|
|
|
void checkSelection()
|
|
|
|
|
|
{
|
|
|
|
|
|
selCheckScheduled = false;
|
|
|
|
|
|
|
|
|
|
|
|
EDA_DRAW_FRAME* fr = frame();
|
|
|
|
|
|
|
|
|
|
|
|
if( !fr )
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
|
|
std::string s = selectionEmitPayload( fr ).dump();
|
|
|
|
|
|
|
|
|
|
|
|
if( s == lastSelectionJson )
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
|
|
lastSelectionJson = s;
|
|
|
|
|
|
pcbjam_collab::emitSelection( s );
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void scheduleSelCheck()
|
|
|
|
|
|
{
|
|
|
|
|
|
if( selCheckScheduled )
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
|
|
EDA_DRAW_FRAME* fr = frame();
|
|
|
|
|
|
|
|
|
|
|
|
if( !fr )
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
|
|
selCheckScheduled = true;
|
|
|
|
|
|
fr->CallAfter( [this]() { checkSelection(); } );
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Viewport push (world↔screen mapping for the DOM layers, 0005). Zoom also
|
|
|
|
|
|
// invalidates the overlay's screen-constant sizes → schedule a redraw.
|
|
|
|
|
|
void emitViewportIfChanged()
|
|
|
|
|
|
{
|
|
|
|
|
|
EDA_DRAW_FRAME* fr = frame();
|
|
|
|
|
|
|
|
|
|
|
|
if( !fr )
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
2026-07-09 16:31:41 +02:00
|
|
|
|
KIGFX::VIEW* view = fr->GetCanvas()->GetView();
|
|
|
|
|
|
double scale = view->GetScale(); // zoom — cheap change detector only
|
|
|
|
|
|
VECTOR2D c = view->GetCenter();
|
|
|
|
|
|
const VECTOR2I& sz = view->GetScreenPixelSize();
|
|
|
|
|
|
|
|
|
|
|
|
// Size participates in the dedupe: the JS worldToScreen maps through
|
|
|
|
|
|
// w/2,h/2, so a canvas resize (or the boot layout settling after the
|
|
|
|
|
|
// bind-time seed) with an unchanged scale/center must still re-push —
|
|
|
|
|
|
// else every DOM pin target is vertically offset until the next
|
|
|
|
|
|
// pan/zoom.
|
|
|
|
|
|
if( scale == lastVpScale && c == lastVpCenter && sz == lastVpSize )
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
|
|
lastVpScale = scale;
|
|
|
|
|
|
lastVpCenter = c;
|
2026-07-09 16:31:41 +02:00
|
|
|
|
lastVpSize = sz;
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
|
|
|
|
|
|
|
// px per IU via the GAL matrix — GetScale() is the zoom, not px/IU.
|
|
|
|
|
|
pcbjam_collab::emitViewport( c.x, c.y, view->ToScreen( 1.0 ), sz.x, sz.y );
|
|
|
|
|
|
|
|
|
|
|
|
if( !peers.empty() )
|
|
|
|
|
|
scheduleRedraw();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** kicadCollabGetViewport: `{cx,cy,scale,w,h}`, scale = px per IU via the
|
|
|
|
|
|
* GAL matrix (GetScale() is the zoom, not px/IU — pcbnew 0002 lesson). */
|
|
|
|
|
|
std::string viewportJson()
|
|
|
|
|
|
{
|
|
|
|
|
|
EDA_DRAW_FRAME* fr = frame();
|
|
|
|
|
|
|
|
|
|
|
|
if( !fr )
|
|
|
|
|
|
return "";
|
|
|
|
|
|
|
|
|
|
|
|
KIGFX::VIEW* view = fr->GetCanvas()->GetView();
|
|
|
|
|
|
VECTOR2D c = view->GetCenter();
|
|
|
|
|
|
const VECTOR2I& sz = view->GetScreenPixelSize();
|
|
|
|
|
|
|
|
|
|
|
|
return json{ { "cx", c.x }, { "cy", c.y }, { "scale", view->ToScreen( 1.0 ) },
|
|
|
|
|
|
{ "w", sz.x }, { "h", sz.y } }.dump();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat(collab): follow-user (collab-presence 0008) + chip depth-layer fix
Follow-user: click a peer's roster avatar to mirror their viewport until
local input breaks it.
- collab_presence_core.h: CORE::fitViewport(cx, cy, halfW, halfH) — fit the
leader's world rect with CONTAIN semantics (zoom derived from the
follower's own canvas via the ToScreen ratio; GetScale is the zoom, not
px/IU). Exported as kicadCollabFitViewport from both editor TUs + the
merged dispatcher.
- presence-kicad.ts: publish the visible world rect (viewportRect) into
awareness, 100 ms trailing throttle; guarded for pre-0008 handles.
- follow-user.ts: createFollow — follows an awareness CLIENT (a tab, not a
user); applies leader rect changes via FitViewport, dedupes unchanged
republishes; break-on-interact compares local onViewport echoes against
the last applied rect (2% rel tolerance, echo-grace before the first fit
lands); unfollows on leader-left; pauses on eeschema sheet mismatch.
- PresenceRoster: avatars are follow toggles (ring on the followed peer);
WasmTool renders the "Following <name> — move to stop" banner.
- tests: 7 controller units (85/85 collab), fitViewport round-trip e2e in
both kicad presence specs (20/20), two-tab tests/web/follow.spec.ts
(converge → track → wheel-zoom breaks → subsequent moves ignored).
Chip depth-layer fix (user-reported): name chips washed out inside
low-alpha selection fills — chip rects shared the shapes overlay's single
depth, and same-depth fragments drawn LATER lose the depth test, so an
earlier-painted fill rejected the chip's pixels. Now three layers via the
fork's VIEW_OVERLAY::SetDepthOffset: text (0) < chips + pin dots (1) <
selection shapes (2). drawLabel/drawCursor/drawSelectionBox take the chip
overlay explicitly; comment-pin dots move to the chip layer too (the 0005
"drawn last so pins sit above" comment had the rule backwards). Verified
with a chip-inside-30%-fill pixel repro + the full presence suite.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-09 09:30:40 +02:00
|
|
|
|
/** kicadCollabFitViewport (0008 follow-user): fit the given world RECT
|
|
|
|
|
|
* (center + half-extents, IU) into this canvas — contain, never crop:
|
|
|
|
|
|
* the follower's zoom is derived from ITS OWN canvas size, so leaders
|
|
|
|
|
|
* and followers on different monitors see the same world region.
|
jspi cleanup: remove the asyncify-era residue — dead code, conditionals, pipeline scaffolding, stale prose
The runtime is JSPI-only; this removes everything that still pretended
otherwise. Three exhaustive sweeps (C++/JS+build+CI/tests+docs) drove
the inventory; every deletion verified by grep closure + full gates.
Broken-right-now fixes:
- deploy-staging.yml passed the retired opt_level input — the workflow
could not even start. Removed.
- env.sh carried dead exports with a live -sASYNCIFY=1 inside
(WASM_LDFLAGS/PTHREAD_LDFLAGS, zero consumers). Removed; the
WASM_LEGACY_EXCEPTIONS rationale rewritten to the real reason.
- docker/build.sh exported PCBJAM_ASYNC_BACKEND (read nowhere). Gone.
Dead weight removed:
- binaryen submodule (nothing builds or invokes it), wasm-opt-bench
workflow + scripts/bench/, get-wasm-opt.sh, diagnostics.js (242 lines
of Asyncify-API-only code), the KICAD_PIPELINE background-postprocess
scaffolding (existed to parallelize the deleted wasm-opt phase; the
postprocess is a seconds-long node script and now runs inline),
build-monitor's dead asyncify rows, sched-context orphan build
output, dead .gitignore entries, the .jspi-assets spike dir (the two
wf-result research JSONs moved to docs/features/async/migration-evidence/).
- bindings: fiber_park.h + its 12 embind registrations (broken-if-
called under JSPI), the kicadOpenFileStart/OPEN_JOB starter route,
main_stack_runner.h + 5 includes, the always-null context-sleep weak
hook in nanosleep_yield.c.
- shim: the backend field (installed-flag idempotency instead),
noteContextWait (dead both sides), the __wxAsyncifyDump alias (+ the
WasmTool fallback and string-dump normalize branch).
- web: the emscripten-6-ignored mainScriptUrlOrBlob option in boot.ts
(gerber-demo keeps it: it loads the deployed CDN release, which
predates emscripten 6 — noted inline).
Conditionals: all 'backend === jspi' checks reduced to scheduler-
presence checks; races_quiescent re-keyed from Asyncify.state (vacuous)
to real backlog quiescence (resumeReady/mutatorQueue — NOT _windowLive,
which is the probing activation's own window by definition).
Renames (identifiers only, no file renames): ASYNC_LINK_FLAGS→
JSPI_LINK_FLAGS and Makefile ASYNC_LDFLAGS→JSPI_LDFLAGS,
kicadCollabFiberBusy→kicadCollabBusy (embind + web + tests),
collab_common.h fiber*→apply*/coroutine naming, asyncifySignatures→
wasmTrapSignatures (lists byte-identical).
Tests: the two remaining vacuous [wx-asyncify]/fiber-resume-refused
asserts re-keyed to live JSPI beacons; eeschema-load's failure message
no longer sends the developer to a deleted script; wait-beacons' dead
families/parser deleted; lane-0 legacy-glue guards removed (lane 0 is
unconstructible); the embind test.fail re-gated with the JSPI reason
(plain embind invokers cannot suspend — verified still failing);
lint-determinism now scans tests/jspi (166 files clean);
eeschema-collab local-move gated to chromium (~50% flaky on FF even
solo; pcbnew twin covers both engines).
Docs: DEBUG.md rewritten as the JSPI debugging guide; build.md
describes the single-phase build; docs/features/async/README.md
banner-marked historical and repointed at the NEW
23-jspi-runtime.md (current architecture: export census, turnstile,
libcontext ownership + refusal contract, embind call shapes, the
em-pthread service-wrapper trick, exception policy, known gaps).
Gates on the cleaned tree: test:e2e 725 passed / 0 failed (after the
quiescence-probe fix; the 3 other reds were verified contention flakes
solo-green or the documented FF gate), web 76/0, jspi 18/18 both
engines, vitest 295/295 + 17/17, all lints green, live-app census
clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016X9eh1s5sTx1o9Em9KBuwR
2026-08-14 09:25:32 +02:00
|
|
|
|
* Apply coroutine like every other view mutation from JS. */
|
feat(collab): follow-user (collab-presence 0008) + chip depth-layer fix
Follow-user: click a peer's roster avatar to mirror their viewport until
local input breaks it.
- collab_presence_core.h: CORE::fitViewport(cx, cy, halfW, halfH) — fit the
leader's world rect with CONTAIN semantics (zoom derived from the
follower's own canvas via the ToScreen ratio; GetScale is the zoom, not
px/IU). Exported as kicadCollabFitViewport from both editor TUs + the
merged dispatcher.
- presence-kicad.ts: publish the visible world rect (viewportRect) into
awareness, 100 ms trailing throttle; guarded for pre-0008 handles.
- follow-user.ts: createFollow — follows an awareness CLIENT (a tab, not a
user); applies leader rect changes via FitViewport, dedupes unchanged
republishes; break-on-interact compares local onViewport echoes against
the last applied rect (2% rel tolerance, echo-grace before the first fit
lands); unfollows on leader-left; pauses on eeschema sheet mismatch.
- PresenceRoster: avatars are follow toggles (ring on the followed peer);
WasmTool renders the "Following <name> — move to stop" banner.
- tests: 7 controller units (85/85 collab), fitViewport round-trip e2e in
both kicad presence specs (20/20), two-tab tests/web/follow.spec.ts
(converge → track → wheel-zoom breaks → subsequent moves ignored).
Chip depth-layer fix (user-reported): name chips washed out inside
low-alpha selection fills — chip rects shared the shapes overlay's single
depth, and same-depth fragments drawn LATER lose the depth test, so an
earlier-painted fill rejected the chip's pixels. Now three layers via the
fork's VIEW_OVERLAY::SetDepthOffset: text (0) < chips + pin dots (1) <
selection shapes (2). drawLabel/drawCursor/drawSelectionBox take the chip
overlay explicitly; comment-pin dots move to the chip layer too (the 0005
"drawn last so pins sit above" comment had the rule backwards). Verified
with a chip-inside-30%-fill pixel repro + the full presence suite.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-09 09:30:40 +02:00
|
|
|
|
void fitViewport( double aCx, double aCy, double aHalfW, double aHalfH )
|
|
|
|
|
|
{
|
|
|
|
|
|
EDA_DRAW_FRAME* fr = frame();
|
|
|
|
|
|
|
|
|
|
|
|
if( !fr || aHalfW <= 0 || aHalfH <= 0 )
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
jspi cleanup: remove the asyncify-era residue — dead code, conditionals, pipeline scaffolding, stale prose
The runtime is JSPI-only; this removes everything that still pretended
otherwise. Three exhaustive sweeps (C++/JS+build+CI/tests+docs) drove
the inventory; every deletion verified by grep closure + full gates.
Broken-right-now fixes:
- deploy-staging.yml passed the retired opt_level input — the workflow
could not even start. Removed.
- env.sh carried dead exports with a live -sASYNCIFY=1 inside
(WASM_LDFLAGS/PTHREAD_LDFLAGS, zero consumers). Removed; the
WASM_LEGACY_EXCEPTIONS rationale rewritten to the real reason.
- docker/build.sh exported PCBJAM_ASYNC_BACKEND (read nowhere). Gone.
Dead weight removed:
- binaryen submodule (nothing builds or invokes it), wasm-opt-bench
workflow + scripts/bench/, get-wasm-opt.sh, diagnostics.js (242 lines
of Asyncify-API-only code), the KICAD_PIPELINE background-postprocess
scaffolding (existed to parallelize the deleted wasm-opt phase; the
postprocess is a seconds-long node script and now runs inline),
build-monitor's dead asyncify rows, sched-context orphan build
output, dead .gitignore entries, the .jspi-assets spike dir (the two
wf-result research JSONs moved to docs/features/async/migration-evidence/).
- bindings: fiber_park.h + its 12 embind registrations (broken-if-
called under JSPI), the kicadOpenFileStart/OPEN_JOB starter route,
main_stack_runner.h + 5 includes, the always-null context-sleep weak
hook in nanosleep_yield.c.
- shim: the backend field (installed-flag idempotency instead),
noteContextWait (dead both sides), the __wxAsyncifyDump alias (+ the
WasmTool fallback and string-dump normalize branch).
- web: the emscripten-6-ignored mainScriptUrlOrBlob option in boot.ts
(gerber-demo keeps it: it loads the deployed CDN release, which
predates emscripten 6 — noted inline).
Conditionals: all 'backend === jspi' checks reduced to scheduler-
presence checks; races_quiescent re-keyed from Asyncify.state (vacuous)
to real backlog quiescence (resumeReady/mutatorQueue — NOT _windowLive,
which is the probing activation's own window by definition).
Renames (identifiers only, no file renames): ASYNC_LINK_FLAGS→
JSPI_LINK_FLAGS and Makefile ASYNC_LDFLAGS→JSPI_LDFLAGS,
kicadCollabFiberBusy→kicadCollabBusy (embind + web + tests),
collab_common.h fiber*→apply*/coroutine naming, asyncifySignatures→
wasmTrapSignatures (lists byte-identical).
Tests: the two remaining vacuous [wx-asyncify]/fiber-resume-refused
asserts re-keyed to live JSPI beacons; eeschema-load's failure message
no longer sends the developer to a deleted script; wait-beacons' dead
families/parser deleted; lane-0 legacy-glue guards removed (lane 0 is
unconstructible); the embind test.fail re-gated with the JSPI reason
(plain embind invokers cannot suspend — verified still failing);
lint-determinism now scans tests/jspi (166 files clean);
eeschema-collab local-move gated to chromium (~50% flaky on FF even
solo; pcbnew twin covers both engines).
Docs: DEBUG.md rewritten as the JSPI debugging guide; build.md
describes the single-phase build; docs/features/async/README.md
banner-marked historical and repointed at the NEW
23-jspi-runtime.md (current architecture: export census, turnstile,
libcontext ownership + refusal contract, embind call shapes, the
em-pthread service-wrapper trick, exception policy, known gaps).
Gates on the cleaned tree: test:e2e 725 passed / 0 failed (after the
quiescence-probe fix; the 3 other reds were verified contention flakes
solo-green or the documented FF gate), web 76/0, jspi 18/18 both
engines, vitest 295/295 + 17/17, all lints green, live-app census
clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016X9eh1s5sTx1o9Em9KBuwR
2026-08-14 09:25:32 +02:00
|
|
|
|
pcbjam_collab::runOnCoroutine( fr, [this, fr, aCx, aCy, aHalfW, aHalfH]() {
|
feat(collab): follow-user (collab-presence 0008) + chip depth-layer fix
Follow-user: click a peer's roster avatar to mirror their viewport until
local input breaks it.
- collab_presence_core.h: CORE::fitViewport(cx, cy, halfW, halfH) — fit the
leader's world rect with CONTAIN semantics (zoom derived from the
follower's own canvas via the ToScreen ratio; GetScale is the zoom, not
px/IU). Exported as kicadCollabFitViewport from both editor TUs + the
merged dispatcher.
- presence-kicad.ts: publish the visible world rect (viewportRect) into
awareness, 100 ms trailing throttle; guarded for pre-0008 handles.
- follow-user.ts: createFollow — follows an awareness CLIENT (a tab, not a
user); applies leader rect changes via FitViewport, dedupes unchanged
republishes; break-on-interact compares local onViewport echoes against
the last applied rect (2% rel tolerance, echo-grace before the first fit
lands); unfollows on leader-left; pauses on eeschema sheet mismatch.
- PresenceRoster: avatars are follow toggles (ring on the followed peer);
WasmTool renders the "Following <name> — move to stop" banner.
- tests: 7 controller units (85/85 collab), fitViewport round-trip e2e in
both kicad presence specs (20/20), two-tab tests/web/follow.spec.ts
(converge → track → wheel-zoom breaks → subsequent moves ignored).
Chip depth-layer fix (user-reported): name chips washed out inside
low-alpha selection fills — chip rects shared the shapes overlay's single
depth, and same-depth fragments drawn LATER lose the depth test, so an
earlier-painted fill rejected the chip's pixels. Now three layers via the
fork's VIEW_OVERLAY::SetDepthOffset: text (0) < chips + pin dots (1) <
selection shapes (2). drawLabel/drawCursor/drawSelectionBox take the chip
overlay explicitly; comment-pin dots move to the chip layer too (the 0005
"drawn last so pins sit above" comment had the rule backwards). Verified
with a chip-inside-30%-fill pixel repro + the full presence suite.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-09 09:30:40 +02:00
|
|
|
|
KIGFX::VIEW* view = fr->GetCanvas()->GetView();
|
|
|
|
|
|
const VECTOR2I& sz = view->GetScreenPixelSize();
|
|
|
|
|
|
|
|
|
|
|
|
// Pixels-per-IU that CONTAINS the rect in both axes. GetScale() is
|
|
|
|
|
|
// the zoom, not px/IU (0002 lesson) — convert via the GAL matrix:
|
|
|
|
|
|
// zoom scales linearly with px/IU, so target zoom = current zoom ×
|
|
|
|
|
|
// (target px/IU ÷ current px/IU).
|
|
|
|
|
|
double pxPerIuNow = view->ToScreen( 1.0 );
|
|
|
|
|
|
double pxPerIuFit = std::min( sz.x / ( 2.0 * aHalfW ), sz.y / ( 2.0 * aHalfH ) );
|
|
|
|
|
|
|
|
|
|
|
|
if( pxPerIuNow > 0 && pxPerIuFit > 0 )
|
|
|
|
|
|
view->SetScale( view->GetScale() * ( pxPerIuFit / pxPerIuNow ) );
|
|
|
|
|
|
|
|
|
|
|
|
view->SetCenter( VECTOR2D( aCx, aCy ) );
|
|
|
|
|
|
fr->GetCanvas()->ForceRefresh();
|
|
|
|
|
|
emitViewportIfChanged();
|
|
|
|
|
|
} );
|
|
|
|
|
|
}
|
|
|
|
|
|
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
|
/** kicadCollabSetViewport (0005): pan to a world position (comment panel
|
jspi cleanup: remove the asyncify-era residue — dead code, conditionals, pipeline scaffolding, stale prose
The runtime is JSPI-only; this removes everything that still pretended
otherwise. Three exhaustive sweeps (C++/JS+build+CI/tests+docs) drove
the inventory; every deletion verified by grep closure + full gates.
Broken-right-now fixes:
- deploy-staging.yml passed the retired opt_level input — the workflow
could not even start. Removed.
- env.sh carried dead exports with a live -sASYNCIFY=1 inside
(WASM_LDFLAGS/PTHREAD_LDFLAGS, zero consumers). Removed; the
WASM_LEGACY_EXCEPTIONS rationale rewritten to the real reason.
- docker/build.sh exported PCBJAM_ASYNC_BACKEND (read nowhere). Gone.
Dead weight removed:
- binaryen submodule (nothing builds or invokes it), wasm-opt-bench
workflow + scripts/bench/, get-wasm-opt.sh, diagnostics.js (242 lines
of Asyncify-API-only code), the KICAD_PIPELINE background-postprocess
scaffolding (existed to parallelize the deleted wasm-opt phase; the
postprocess is a seconds-long node script and now runs inline),
build-monitor's dead asyncify rows, sched-context orphan build
output, dead .gitignore entries, the .jspi-assets spike dir (the two
wf-result research JSONs moved to docs/features/async/migration-evidence/).
- bindings: fiber_park.h + its 12 embind registrations (broken-if-
called under JSPI), the kicadOpenFileStart/OPEN_JOB starter route,
main_stack_runner.h + 5 includes, the always-null context-sleep weak
hook in nanosleep_yield.c.
- shim: the backend field (installed-flag idempotency instead),
noteContextWait (dead both sides), the __wxAsyncifyDump alias (+ the
WasmTool fallback and string-dump normalize branch).
- web: the emscripten-6-ignored mainScriptUrlOrBlob option in boot.ts
(gerber-demo keeps it: it loads the deployed CDN release, which
predates emscripten 6 — noted inline).
Conditionals: all 'backend === jspi' checks reduced to scheduler-
presence checks; races_quiescent re-keyed from Asyncify.state (vacuous)
to real backlog quiescence (resumeReady/mutatorQueue — NOT _windowLive,
which is the probing activation's own window by definition).
Renames (identifiers only, no file renames): ASYNC_LINK_FLAGS→
JSPI_LINK_FLAGS and Makefile ASYNC_LDFLAGS→JSPI_LDFLAGS,
kicadCollabFiberBusy→kicadCollabBusy (embind + web + tests),
collab_common.h fiber*→apply*/coroutine naming, asyncifySignatures→
wasmTrapSignatures (lists byte-identical).
Tests: the two remaining vacuous [wx-asyncify]/fiber-resume-refused
asserts re-keyed to live JSPI beacons; eeschema-load's failure message
no longer sends the developer to a deleted script; wait-beacons' dead
families/parser deleted; lane-0 legacy-glue guards removed (lane 0 is
unconstructible); the embind test.fail re-gated with the JSPI reason
(plain embind invokers cannot suspend — verified still failing);
lint-determinism now scans tests/jspi (166 files clean);
eeschema-collab local-move gated to chromium (~50% flaky on FF even
solo; pcbnew twin covers both engines).
Docs: DEBUG.md rewritten as the JSPI debugging guide; build.md
describes the single-phase build; docs/features/async/README.md
banner-marked historical and repointed at the NEW
23-jspi-runtime.md (current architecture: export census, turnstile,
libcontext ownership + refusal contract, embind call shapes, the
em-pthread service-wrapper trick, exception policy, known gaps).
Gates on the cleaned tree: test:e2e 725 passed / 0 failed (after the
quiescence-probe fix; the 3 other reds were verified contention flakes
solo-green or the documented FF gate), web 76/0, jspi 18/18 both
engines, vitest 295/295 + 17/17, all lints green, live-app census
clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016X9eh1s5sTx1o9Em9KBuwR
2026-08-14 09:25:32 +02:00
|
|
|
|
* "jump to pin"). Apply coroutine like every other view mutation from JS. */
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
|
void panTo( double aCx, double aCy )
|
|
|
|
|
|
{
|
|
|
|
|
|
EDA_DRAW_FRAME* fr = frame();
|
|
|
|
|
|
|
|
|
|
|
|
if( !fr )
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
jspi cleanup: remove the asyncify-era residue — dead code, conditionals, pipeline scaffolding, stale prose
The runtime is JSPI-only; this removes everything that still pretended
otherwise. Three exhaustive sweeps (C++/JS+build+CI/tests+docs) drove
the inventory; every deletion verified by grep closure + full gates.
Broken-right-now fixes:
- deploy-staging.yml passed the retired opt_level input — the workflow
could not even start. Removed.
- env.sh carried dead exports with a live -sASYNCIFY=1 inside
(WASM_LDFLAGS/PTHREAD_LDFLAGS, zero consumers). Removed; the
WASM_LEGACY_EXCEPTIONS rationale rewritten to the real reason.
- docker/build.sh exported PCBJAM_ASYNC_BACKEND (read nowhere). Gone.
Dead weight removed:
- binaryen submodule (nothing builds or invokes it), wasm-opt-bench
workflow + scripts/bench/, get-wasm-opt.sh, diagnostics.js (242 lines
of Asyncify-API-only code), the KICAD_PIPELINE background-postprocess
scaffolding (existed to parallelize the deleted wasm-opt phase; the
postprocess is a seconds-long node script and now runs inline),
build-monitor's dead asyncify rows, sched-context orphan build
output, dead .gitignore entries, the .jspi-assets spike dir (the two
wf-result research JSONs moved to docs/features/async/migration-evidence/).
- bindings: fiber_park.h + its 12 embind registrations (broken-if-
called under JSPI), the kicadOpenFileStart/OPEN_JOB starter route,
main_stack_runner.h + 5 includes, the always-null context-sleep weak
hook in nanosleep_yield.c.
- shim: the backend field (installed-flag idempotency instead),
noteContextWait (dead both sides), the __wxAsyncifyDump alias (+ the
WasmTool fallback and string-dump normalize branch).
- web: the emscripten-6-ignored mainScriptUrlOrBlob option in boot.ts
(gerber-demo keeps it: it loads the deployed CDN release, which
predates emscripten 6 — noted inline).
Conditionals: all 'backend === jspi' checks reduced to scheduler-
presence checks; races_quiescent re-keyed from Asyncify.state (vacuous)
to real backlog quiescence (resumeReady/mutatorQueue — NOT _windowLive,
which is the probing activation's own window by definition).
Renames (identifiers only, no file renames): ASYNC_LINK_FLAGS→
JSPI_LINK_FLAGS and Makefile ASYNC_LDFLAGS→JSPI_LDFLAGS,
kicadCollabFiberBusy→kicadCollabBusy (embind + web + tests),
collab_common.h fiber*→apply*/coroutine naming, asyncifySignatures→
wasmTrapSignatures (lists byte-identical).
Tests: the two remaining vacuous [wx-asyncify]/fiber-resume-refused
asserts re-keyed to live JSPI beacons; eeschema-load's failure message
no longer sends the developer to a deleted script; wait-beacons' dead
families/parser deleted; lane-0 legacy-glue guards removed (lane 0 is
unconstructible); the embind test.fail re-gated with the JSPI reason
(plain embind invokers cannot suspend — verified still failing);
lint-determinism now scans tests/jspi (166 files clean);
eeschema-collab local-move gated to chromium (~50% flaky on FF even
solo; pcbnew twin covers both engines).
Docs: DEBUG.md rewritten as the JSPI debugging guide; build.md
describes the single-phase build; docs/features/async/README.md
banner-marked historical and repointed at the NEW
23-jspi-runtime.md (current architecture: export census, turnstile,
libcontext ownership + refusal contract, embind call shapes, the
em-pthread service-wrapper trick, exception policy, known gaps).
Gates on the cleaned tree: test:e2e 725 passed / 0 failed (after the
quiescence-probe fix; the 3 other reds were verified contention flakes
solo-green or the documented FF gate), web 76/0, jspi 18/18 both
engines, vitest 295/295 + 17/17, all lints green, live-app census
clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016X9eh1s5sTx1o9Em9KBuwR
2026-08-14 09:25:32 +02:00
|
|
|
|
pcbjam_collab::runOnCoroutine( fr, [this, fr, aCx, aCy]() {
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
|
fr->GetCanvas()->GetView()->SetCenter( VECTOR2D( aCx, aCy ) );
|
|
|
|
|
|
fr->GetCanvas()->ForceRefresh();
|
|
|
|
|
|
emitViewportIfChanged();
|
|
|
|
|
|
} );
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void onMotion( wxMouseEvent& aEvt )
|
|
|
|
|
|
{
|
|
|
|
|
|
aEvt.Skip();
|
|
|
|
|
|
|
|
|
|
|
|
long long now = nowMs();
|
|
|
|
|
|
|
|
|
|
|
|
if( now - lastCursorEmitMs < 50 ) // ≤20 emits/s, event-driven (no timers)
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
|
|
lastCursorEmitMs = now;
|
|
|
|
|
|
|
|
|
|
|
|
EDA_DRAW_FRAME* fr = frame();
|
|
|
|
|
|
|
|
|
|
|
|
if( !fr )
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
|
|
// Screen→world via the non-virtual VIEW::ToWorld (the virtual
|
jspi cleanup: remove the asyncify-era residue — dead code, conditionals, pipeline scaffolding, stale prose
The runtime is JSPI-only; this removes everything that still pretended
otherwise. Three exhaustive sweeps (C++/JS+build+CI/tests+docs) drove
the inventory; every deletion verified by grep closure + full gates.
Broken-right-now fixes:
- deploy-staging.yml passed the retired opt_level input — the workflow
could not even start. Removed.
- env.sh carried dead exports with a live -sASYNCIFY=1 inside
(WASM_LDFLAGS/PTHREAD_LDFLAGS, zero consumers). Removed; the
WASM_LEGACY_EXCEPTIONS rationale rewritten to the real reason.
- docker/build.sh exported PCBJAM_ASYNC_BACKEND (read nowhere). Gone.
Dead weight removed:
- binaryen submodule (nothing builds or invokes it), wasm-opt-bench
workflow + scripts/bench/, get-wasm-opt.sh, diagnostics.js (242 lines
of Asyncify-API-only code), the KICAD_PIPELINE background-postprocess
scaffolding (existed to parallelize the deleted wasm-opt phase; the
postprocess is a seconds-long node script and now runs inline),
build-monitor's dead asyncify rows, sched-context orphan build
output, dead .gitignore entries, the .jspi-assets spike dir (the two
wf-result research JSONs moved to docs/features/async/migration-evidence/).
- bindings: fiber_park.h + its 12 embind registrations (broken-if-
called under JSPI), the kicadOpenFileStart/OPEN_JOB starter route,
main_stack_runner.h + 5 includes, the always-null context-sleep weak
hook in nanosleep_yield.c.
- shim: the backend field (installed-flag idempotency instead),
noteContextWait (dead both sides), the __wxAsyncifyDump alias (+ the
WasmTool fallback and string-dump normalize branch).
- web: the emscripten-6-ignored mainScriptUrlOrBlob option in boot.ts
(gerber-demo keeps it: it loads the deployed CDN release, which
predates emscripten 6 — noted inline).
Conditionals: all 'backend === jspi' checks reduced to scheduler-
presence checks; races_quiescent re-keyed from Asyncify.state (vacuous)
to real backlog quiescence (resumeReady/mutatorQueue — NOT _windowLive,
which is the probing activation's own window by definition).
Renames (identifiers only, no file renames): ASYNC_LINK_FLAGS→
JSPI_LINK_FLAGS and Makefile ASYNC_LDFLAGS→JSPI_LDFLAGS,
kicadCollabFiberBusy→kicadCollabBusy (embind + web + tests),
collab_common.h fiber*→apply*/coroutine naming, asyncifySignatures→
wasmTrapSignatures (lists byte-identical).
Tests: the two remaining vacuous [wx-asyncify]/fiber-resume-refused
asserts re-keyed to live JSPI beacons; eeschema-load's failure message
no longer sends the developer to a deleted script; wait-beacons' dead
families/parser deleted; lane-0 legacy-glue guards removed (lane 0 is
unconstructible); the embind test.fail re-gated with the JSPI reason
(plain embind invokers cannot suspend — verified still failing);
lint-determinism now scans tests/jspi (166 files clean);
eeschema-collab local-move gated to chromium (~50% flaky on FF even
solo; pcbnew twin covers both engines).
Docs: DEBUG.md rewritten as the JSPI debugging guide; build.md
describes the single-phase build; docs/features/async/README.md
banner-marked historical and repointed at the NEW
23-jspi-runtime.md (current architecture: export census, turnstile,
libcontext ownership + refusal contract, embind call shapes, the
em-pthread service-wrapper trick, exception policy, known gaps).
Gates on the cleaned tree: test:e2e 725 passed / 0 failed (after the
quiescence-probe fix; the 3 other reds were verified contention flakes
solo-green or the documented FF gate), web 76/0, jspi 18/18 both
engines, vitest 295/295 + 17/17, all lints green, live-app census
clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016X9eh1s5sTx1o9Em9KBuwR
2026-08-14 09:25:32 +02:00
|
|
|
|
// VIEW_CONTROLS::GetMousePosition mis-dispatched here under the
|
|
|
|
|
|
// retired asyncify runtime; the direct call stays).
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
|
wxPoint p = aEvt.GetPosition();
|
|
|
|
|
|
VECTOR2D world = fr->GetCanvas()->GetView()->ToWorld( VECTOR2D( p.x, p.y ), true );
|
|
|
|
|
|
|
|
|
|
|
|
pcbjam_collab::emitCursor( world.x, world.y, true );
|
|
|
|
|
|
emitViewportIfChanged(); // catches drag-pan while moving
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void onLeave( wxMouseEvent& aEvt )
|
|
|
|
|
|
{
|
|
|
|
|
|
aEvt.Skip();
|
|
|
|
|
|
pcbjam_collab::emitCursor( 0, 0, false );
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── remote render ─────────────────────────────────────────────────────
|
|
|
|
|
|
|
2026-08-28 16:14:44 +02:00
|
|
|
|
// Repaint the remote-peers overlays. Runs in CallAfter + COROUTINE via the
|
jspi cleanup: remove the asyncify-era residue — dead code, conditionals, pipeline scaffolding, stale prose
The runtime is JSPI-only; this removes everything that still pretended
otherwise. Three exhaustive sweeps (C++/JS+build+CI/tests+docs) drove
the inventory; every deletion verified by grep closure + full gates.
Broken-right-now fixes:
- deploy-staging.yml passed the retired opt_level input — the workflow
could not even start. Removed.
- env.sh carried dead exports with a live -sASYNCIFY=1 inside
(WASM_LDFLAGS/PTHREAD_LDFLAGS, zero consumers). Removed; the
WASM_LEGACY_EXCEPTIONS rationale rewritten to the real reason.
- docker/build.sh exported PCBJAM_ASYNC_BACKEND (read nowhere). Gone.
Dead weight removed:
- binaryen submodule (nothing builds or invokes it), wasm-opt-bench
workflow + scripts/bench/, get-wasm-opt.sh, diagnostics.js (242 lines
of Asyncify-API-only code), the KICAD_PIPELINE background-postprocess
scaffolding (existed to parallelize the deleted wasm-opt phase; the
postprocess is a seconds-long node script and now runs inline),
build-monitor's dead asyncify rows, sched-context orphan build
output, dead .gitignore entries, the .jspi-assets spike dir (the two
wf-result research JSONs moved to docs/features/async/migration-evidence/).
- bindings: fiber_park.h + its 12 embind registrations (broken-if-
called under JSPI), the kicadOpenFileStart/OPEN_JOB starter route,
main_stack_runner.h + 5 includes, the always-null context-sleep weak
hook in nanosleep_yield.c.
- shim: the backend field (installed-flag idempotency instead),
noteContextWait (dead both sides), the __wxAsyncifyDump alias (+ the
WasmTool fallback and string-dump normalize branch).
- web: the emscripten-6-ignored mainScriptUrlOrBlob option in boot.ts
(gerber-demo keeps it: it loads the deployed CDN release, which
predates emscripten 6 — noted inline).
Conditionals: all 'backend === jspi' checks reduced to scheduler-
presence checks; races_quiescent re-keyed from Asyncify.state (vacuous)
to real backlog quiescence (resumeReady/mutatorQueue — NOT _windowLive,
which is the probing activation's own window by definition).
Renames (identifiers only, no file renames): ASYNC_LINK_FLAGS→
JSPI_LINK_FLAGS and Makefile ASYNC_LDFLAGS→JSPI_LDFLAGS,
kicadCollabFiberBusy→kicadCollabBusy (embind + web + tests),
collab_common.h fiber*→apply*/coroutine naming, asyncifySignatures→
wasmTrapSignatures (lists byte-identical).
Tests: the two remaining vacuous [wx-asyncify]/fiber-resume-refused
asserts re-keyed to live JSPI beacons; eeschema-load's failure message
no longer sends the developer to a deleted script; wait-beacons' dead
families/parser deleted; lane-0 legacy-glue guards removed (lane 0 is
unconstructible); the embind test.fail re-gated with the JSPI reason
(plain embind invokers cannot suspend — verified still failing);
lint-determinism now scans tests/jspi (166 files clean);
eeschema-collab local-move gated to chromium (~50% flaky on FF even
solo; pcbnew twin covers both engines).
Docs: DEBUG.md rewritten as the JSPI debugging guide; build.md
describes the single-phase build; docs/features/async/README.md
banner-marked historical and repointed at the NEW
23-jspi-runtime.md (current architecture: export census, turnstile,
libcontext ownership + refusal contract, embind call shapes, the
em-pthread service-wrapper trick, exception policy, known gaps).
Gates on the cleaned tree: test:e2e 725 passed / 0 failed (after the
quiescence-probe fix; the 3 other reds were verified contention flakes
solo-green or the documented FF gate), web 76/0, jspi 18/18 both
engines, vitest 295/295 + 17/17, all lints green, live-app census
clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016X9eh1s5sTx1o9Em9KBuwR
2026-08-14 09:25:32 +02:00
|
|
|
|
// apply queue — serialized with the applies, same constraint as every
|
2026-08-28 16:14:44 +02:00
|
|
|
|
// other view mutation from JS. Two groups, each repainted only when
|
|
|
|
|
|
// dirty: SHAPES (selection boxes, cross-app ghosts, comment pins — the
|
|
|
|
|
|
// expensive part: per-item resolve + outline geometry) and CURSORS.
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
|
void redrawOverlay()
|
|
|
|
|
|
{
|
|
|
|
|
|
redrawScheduled = false;
|
|
|
|
|
|
|
|
|
|
|
|
EDA_DRAW_FRAME* fr = frame();
|
|
|
|
|
|
|
|
|
|
|
|
if( !fr )
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
|
|
KIGFX::VIEW* view = fr->GetCanvas()->GetView();
|
|
|
|
|
|
|
|
|
|
|
|
if( !overlay )
|
|
|
|
|
|
{
|
feat(collab): follow-user (collab-presence 0008) + chip depth-layer fix
Follow-user: click a peer's roster avatar to mirror their viewport until
local input breaks it.
- collab_presence_core.h: CORE::fitViewport(cx, cy, halfW, halfH) — fit the
leader's world rect with CONTAIN semantics (zoom derived from the
follower's own canvas via the ToScreen ratio; GetScale is the zoom, not
px/IU). Exported as kicadCollabFitViewport from both editor TUs + the
merged dispatcher.
- presence-kicad.ts: publish the visible world rect (viewportRect) into
awareness, 100 ms trailing throttle; guarded for pre-0008 handles.
- follow-user.ts: createFollow — follows an awareness CLIENT (a tab, not a
user); applies leader rect changes via FitViewport, dedupes unchanged
republishes; break-on-interact compares local onViewport echoes against
the last applied rect (2% rel tolerance, echo-grace before the first fit
lands); unfollows on leader-left; pauses on eeschema sheet mismatch.
- PresenceRoster: avatars are follow toggles (ring on the followed peer);
WasmTool renders the "Following <name> — move to stop" banner.
- tests: 7 controller units (85/85 collab), fitViewport round-trip e2e in
both kicad presence specs (20/20), two-tab tests/web/follow.spec.ts
(converge → track → wheel-zoom breaks → subsequent moves ignored).
Chip depth-layer fix (user-reported): name chips washed out inside
low-alpha selection fills — chip rects shared the shapes overlay's single
depth, and same-depth fragments drawn LATER lose the depth test, so an
earlier-painted fill rejected the chip's pixels. Now three layers via the
fork's VIEW_OVERLAY::SetDepthOffset: text (0) < chips + pin dots (1) <
selection shapes (2). drawLabel/drawCursor/drawSelectionBox take the chip
overlay explicitly; comment-pin dots move to the chip layer too (the 0005
"drawn last so pins sit above" comment had the rule backwards). Verified
with a chip-inside-30%-fill pixel repro + the full presence suite.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-09 09:30:40 +02:00
|
|
|
|
// Depth layering near→deep: text (0) < chips (1) < shapes (2) —
|
|
|
|
|
|
// fork SetDepthOffset; see collab_presence_style.h.
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
|
overlay = view->MakeOverlay();
|
|
|
|
|
|
overlay->SetDepthOffset( PRESENCE_SHAPES_DEPTH_OFFSET );
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat(collab): follow-user (collab-presence 0008) + chip depth-layer fix
Follow-user: click a peer's roster avatar to mirror their viewport until
local input breaks it.
- collab_presence_core.h: CORE::fitViewport(cx, cy, halfW, halfH) — fit the
leader's world rect with CONTAIN semantics (zoom derived from the
follower's own canvas via the ToScreen ratio; GetScale is the zoom, not
px/IU). Exported as kicadCollabFitViewport from both editor TUs + the
merged dispatcher.
- presence-kicad.ts: publish the visible world rect (viewportRect) into
awareness, 100 ms trailing throttle; guarded for pre-0008 handles.
- follow-user.ts: createFollow — follows an awareness CLIENT (a tab, not a
user); applies leader rect changes via FitViewport, dedupes unchanged
republishes; break-on-interact compares local onViewport echoes against
the last applied rect (2% rel tolerance, echo-grace before the first fit
lands); unfollows on leader-left; pauses on eeschema sheet mismatch.
- PresenceRoster: avatars are follow toggles (ring on the followed peer);
WasmTool renders the "Following <name> — move to stop" banner.
- tests: 7 controller units (85/85 collab), fitViewport round-trip e2e in
both kicad presence specs (20/20), two-tab tests/web/follow.spec.ts
(converge → track → wheel-zoom breaks → subsequent moves ignored).
Chip depth-layer fix (user-reported): name chips washed out inside
low-alpha selection fills — chip rects shared the shapes overlay's single
depth, and same-depth fragments drawn LATER lose the depth test, so an
earlier-painted fill rejected the chip's pixels. Now three layers via the
fork's VIEW_OVERLAY::SetDepthOffset: text (0) < chips + pin dots (1) <
selection shapes (2). drawLabel/drawCursor/drawSelectionBox take the chip
overlay explicitly; comment-pin dots move to the chip layer too (the 0005
"drawn last so pins sit above" comment had the rule backwards). Verified
with a chip-inside-30%-fill pixel repro + the full presence suite.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-09 09:30:40 +02:00
|
|
|
|
if( !chipOverlay )
|
|
|
|
|
|
{
|
|
|
|
|
|
chipOverlay = view->MakeOverlay();
|
|
|
|
|
|
chipOverlay->SetDepthOffset( PRESENCE_CHIPS_DEPTH_OFFSET );
|
|
|
|
|
|
}
|
|
|
|
|
|
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
|
if( !textOverlay )
|
|
|
|
|
|
textOverlay = makePresenceTextOverlay( view );
|
|
|
|
|
|
|
2026-08-28 16:14:44 +02:00
|
|
|
|
if( !cursorOverlay )
|
|
|
|
|
|
{
|
|
|
|
|
|
cursorOverlay = view->MakeOverlay();
|
|
|
|
|
|
cursorOverlay->SetDepthOffset( PRESENCE_SHAPES_DEPTH_OFFSET );
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if( !cursorChipOverlay )
|
|
|
|
|
|
{
|
|
|
|
|
|
cursorChipOverlay = view->MakeOverlay();
|
|
|
|
|
|
cursorChipOverlay->SetDepthOffset( PRESENCE_CHIPS_DEPTH_OFFSET );
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if( !cursorTextOverlay )
|
|
|
|
|
|
cursorTextOverlay = makePresenceTextOverlay( view );
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
|
|
|
|
|
|
|
// Screen-constant sizing: px → world units, so cursors/outline widths
|
|
|
|
|
|
// don't scale with zoom. MUST go through the GAL matrix
|
|
|
|
|
|
// (ToWorld(double)) — the naive 1/GetScale() is the ZOOM factor, not
|
|
|
|
|
|
// px-per-IU, and under-sizes the drawing by ~7 orders of magnitude.
|
|
|
|
|
|
double px = view->ToWorld( 1.0 );
|
|
|
|
|
|
|
2026-08-28 16:14:44 +02:00
|
|
|
|
bool shapes = shapesDirty;
|
|
|
|
|
|
bool cursors = cursorsDirty;
|
|
|
|
|
|
shapesDirty = false;
|
|
|
|
|
|
cursorsDirty = false;
|
|
|
|
|
|
|
|
|
|
|
|
if( shapes )
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
|
{
|
2026-08-28 16:14:44 +02:00
|
|
|
|
overlay->Clear();
|
|
|
|
|
|
chipOverlay->Clear();
|
|
|
|
|
|
textOverlay->Clear();
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
|
|
2026-08-28 16:14:44 +02:00
|
|
|
|
for( const PEER& peer : peers )
|
|
|
|
|
|
{
|
|
|
|
|
|
KIGFX::COLOR4D color = peerColor( style, peer.name, peer.color );
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
|
|
2026-08-28 16:14:44 +02:00
|
|
|
|
// Selection boxes + cross-app ghosts: editor-specific resolution.
|
|
|
|
|
|
drawPeerShapes( *this, fr, peer, color, px );
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Comment pin dots (0005) — on the CHIPS layer so selection fills
|
|
|
|
|
|
// can't reject their fragments (see drawPin).
|
|
|
|
|
|
for( const PIN& pin : pins )
|
|
|
|
|
|
{
|
|
|
|
|
|
KIGFX::COLOR4D color = peerColor( style, pin.name, pin.color );
|
|
|
|
|
|
drawPin( chipOverlay.get(), pin.pos, color, pin.resolved, pin.unread, px, style );
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
view->Update( overlay.get() );
|
|
|
|
|
|
view->Update( chipOverlay.get() );
|
|
|
|
|
|
view->Update( textOverlay.get() );
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-28 16:14:44 +02:00
|
|
|
|
if( cursors )
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
|
{
|
2026-08-28 16:14:44 +02:00
|
|
|
|
cursorOverlay->Clear();
|
|
|
|
|
|
cursorChipOverlay->Clear();
|
|
|
|
|
|
cursorTextOverlay->Clear();
|
|
|
|
|
|
|
|
|
|
|
|
for( const PEER& peer : peers )
|
|
|
|
|
|
{
|
|
|
|
|
|
if( !peer.hasCursor )
|
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
|
|
KIGFX::COLOR4D color = peerColor( style, peer.name, peer.color );
|
|
|
|
|
|
drawCursor( cursorOverlay.get(), cursorChipOverlay.get(), cursorTextOverlay.get(),
|
|
|
|
|
|
peer.cursor, peer.name, color, px, style );
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
view->Update( cursorOverlay.get() );
|
|
|
|
|
|
view->Update( cursorChipOverlay.get() );
|
|
|
|
|
|
view->Update( cursorTextOverlay.get() );
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-28 16:14:44 +02:00
|
|
|
|
if( !shapes && !cursors )
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
|
// The canvas repaints on its own only with focus/input — force it,
|
|
|
|
|
|
// exactly as the cross-probe flash does.
|
|
|
|
|
|
fr->GetCanvas()->ForceRefresh();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-28 16:14:44 +02:00
|
|
|
|
void scheduleRedraw( bool aShapes = true, bool aCursors = true )
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
|
{
|
2026-08-28 16:14:44 +02:00
|
|
|
|
shapesDirty = shapesDirty || aShapes;
|
|
|
|
|
|
cursorsDirty = cursorsDirty || aCursors;
|
|
|
|
|
|
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
|
if( redrawScheduled )
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
|
|
EDA_DRAW_FRAME* fr = frame();
|
|
|
|
|
|
|
|
|
|
|
|
if( !fr )
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
|
|
redrawScheduled = true;
|
jspi cleanup: remove the asyncify-era residue — dead code, conditionals, pipeline scaffolding, stale prose
The runtime is JSPI-only; this removes everything that still pretended
otherwise. Three exhaustive sweeps (C++/JS+build+CI/tests+docs) drove
the inventory; every deletion verified by grep closure + full gates.
Broken-right-now fixes:
- deploy-staging.yml passed the retired opt_level input — the workflow
could not even start. Removed.
- env.sh carried dead exports with a live -sASYNCIFY=1 inside
(WASM_LDFLAGS/PTHREAD_LDFLAGS, zero consumers). Removed; the
WASM_LEGACY_EXCEPTIONS rationale rewritten to the real reason.
- docker/build.sh exported PCBJAM_ASYNC_BACKEND (read nowhere). Gone.
Dead weight removed:
- binaryen submodule (nothing builds or invokes it), wasm-opt-bench
workflow + scripts/bench/, get-wasm-opt.sh, diagnostics.js (242 lines
of Asyncify-API-only code), the KICAD_PIPELINE background-postprocess
scaffolding (existed to parallelize the deleted wasm-opt phase; the
postprocess is a seconds-long node script and now runs inline),
build-monitor's dead asyncify rows, sched-context orphan build
output, dead .gitignore entries, the .jspi-assets spike dir (the two
wf-result research JSONs moved to docs/features/async/migration-evidence/).
- bindings: fiber_park.h + its 12 embind registrations (broken-if-
called under JSPI), the kicadOpenFileStart/OPEN_JOB starter route,
main_stack_runner.h + 5 includes, the always-null context-sleep weak
hook in nanosleep_yield.c.
- shim: the backend field (installed-flag idempotency instead),
noteContextWait (dead both sides), the __wxAsyncifyDump alias (+ the
WasmTool fallback and string-dump normalize branch).
- web: the emscripten-6-ignored mainScriptUrlOrBlob option in boot.ts
(gerber-demo keeps it: it loads the deployed CDN release, which
predates emscripten 6 — noted inline).
Conditionals: all 'backend === jspi' checks reduced to scheduler-
presence checks; races_quiescent re-keyed from Asyncify.state (vacuous)
to real backlog quiescence (resumeReady/mutatorQueue — NOT _windowLive,
which is the probing activation's own window by definition).
Renames (identifiers only, no file renames): ASYNC_LINK_FLAGS→
JSPI_LINK_FLAGS and Makefile ASYNC_LDFLAGS→JSPI_LDFLAGS,
kicadCollabFiberBusy→kicadCollabBusy (embind + web + tests),
collab_common.h fiber*→apply*/coroutine naming, asyncifySignatures→
wasmTrapSignatures (lists byte-identical).
Tests: the two remaining vacuous [wx-asyncify]/fiber-resume-refused
asserts re-keyed to live JSPI beacons; eeschema-load's failure message
no longer sends the developer to a deleted script; wait-beacons' dead
families/parser deleted; lane-0 legacy-glue guards removed (lane 0 is
unconstructible); the embind test.fail re-gated with the JSPI reason
(plain embind invokers cannot suspend — verified still failing);
lint-determinism now scans tests/jspi (166 files clean);
eeschema-collab local-move gated to chromium (~50% flaky on FF even
solo; pcbnew twin covers both engines).
Docs: DEBUG.md rewritten as the JSPI debugging guide; build.md
describes the single-phase build; docs/features/async/README.md
banner-marked historical and repointed at the NEW
23-jspi-runtime.md (current architecture: export census, turnstile,
libcontext ownership + refusal contract, embind call shapes, the
em-pthread service-wrapper trick, exception policy, known gaps).
Gates on the cleaned tree: test:e2e 725 passed / 0 failed (after the
quiescence-probe fix; the 3 other reds were verified contention flakes
solo-green or the documented FF gate), web 76/0, jspi 18/18 both
engines, vitest 295/295 + 17/17, all lints green, live-app census
clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016X9eh1s5sTx1o9Em9KBuwR
2026-08-14 09:25:32 +02:00
|
|
|
|
pcbjam_collab::runOnCoroutine( fr, [this]() { redrawOverlay(); } );
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-28 16:14:44 +02:00
|
|
|
|
/** The document changed (local commit OR remote apply — findings Y-1/Y-3):
|
|
|
|
|
|
* peers' selection boxes may now sit on deleted/moved items, and the
|
|
|
|
|
|
* local selection may have lost items with no closing canvas event.
|
|
|
|
|
|
* Queued on the apply coroutine so it runs AFTER the commit/apply body
|
|
|
|
|
|
* that raised it: repaint the shapes from the live document and re-check
|
|
|
|
|
|
* the local selection. Coalesced per settle. */
|
|
|
|
|
|
void onDocChanged()
|
|
|
|
|
|
{
|
|
|
|
|
|
if( docChangeScheduled )
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
|
|
EDA_DRAW_FRAME* fr = frame();
|
|
|
|
|
|
|
|
|
|
|
|
if( !fr )
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
|
|
docChangeScheduled = true;
|
|
|
|
|
|
|
|
|
|
|
|
pcbjam_collab::runOnCoroutine( fr, [this]()
|
|
|
|
|
|
{
|
|
|
|
|
|
docChangeScheduled = false;
|
|
|
|
|
|
|
|
|
|
|
|
if( !peers.empty() || !pins.empty() )
|
|
|
|
|
|
{
|
|
|
|
|
|
shapesDirty = true;
|
|
|
|
|
|
cursorsDirty = true;
|
|
|
|
|
|
redrawOverlay();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
checkSelection();
|
|
|
|
|
|
} );
|
|
|
|
|
|
}
|
|
|
|
|
|
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
|
// ── JS entry-point bodies ─────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
/** kicadCollabPresenceStart: install the input hooks on the GAL canvas
|
|
|
|
|
|
* (idempotent — the canvas is the same window for the whole session) and
|
|
|
|
|
|
* the fork's remote soft-lock query (0007). */
|
|
|
|
|
|
void start()
|
|
|
|
|
|
{
|
|
|
|
|
|
EDA_DRAW_FRAME* fr = frame();
|
|
|
|
|
|
|
|
|
|
|
|
if( !fr || started )
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
|
|
started = true;
|
|
|
|
|
|
|
|
|
|
|
|
// Remote soft-locks (0007): let the selection/move tools consult the
|
|
|
|
|
|
// peers' live selections through the fork's process-global query.
|
|
|
|
|
|
PCBJAM_REMOTE_LOCK::SetQuery(
|
|
|
|
|
|
[this]( const KIID& aId, wxString* aHolder ) -> bool
|
|
|
|
|
|
{
|
|
|
|
|
|
auto it = locks.find( aId );
|
|
|
|
|
|
|
|
|
|
|
|
if( it == locks.end() )
|
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
|
|
if( aHolder )
|
|
|
|
|
|
*aHolder = wxString::FromUTF8( it->second.c_str() );
|
|
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
} );
|
|
|
|
|
|
|
|
|
|
|
|
wxWindow* canvas = fr->GetCanvas();
|
|
|
|
|
|
|
|
|
|
|
|
canvas->Bind( wxEVT_MOTION, [this]( wxMouseEvent& e ) { onMotion( e ); } );
|
|
|
|
|
|
canvas->Bind( wxEVT_LEAVE_WINDOW, [this]( wxMouseEvent& e ) { onLeave( e ); } );
|
|
|
|
|
|
|
|
|
|
|
|
// Handlers Skip() so the view controls' own processing is untouched;
|
|
|
|
|
|
// selection checks run POST-event via CallAfter (the selection tool
|
|
|
|
|
|
// acts on the same event after us).
|
|
|
|
|
|
auto selAndViewport = [this]( wxEvent& e )
|
|
|
|
|
|
{
|
|
|
|
|
|
e.Skip();
|
|
|
|
|
|
scheduleSelCheck();
|
|
|
|
|
|
|
|
|
|
|
|
if( EDA_DRAW_FRAME* f = frame() )
|
|
|
|
|
|
f->CallAfter( [this]() { emitViewportIfChanged(); } );
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
canvas->Bind( wxEVT_LEFT_UP, [selAndViewport]( wxMouseEvent& e ) { selAndViewport( e ); } );
|
|
|
|
|
|
canvas->Bind( wxEVT_RIGHT_UP, [selAndViewport]( wxMouseEvent& e ) { selAndViewport( e ); } );
|
|
|
|
|
|
canvas->Bind( wxEVT_KEY_UP, [selAndViewport]( wxKeyEvent& e ) { selAndViewport( e ); } );
|
|
|
|
|
|
canvas->Bind( wxEVT_MOUSEWHEEL, [selAndViewport]( wxMouseEvent& e ) { selAndViewport( e ); } );
|
2026-07-09 16:31:41 +02:00
|
|
|
|
|
|
|
|
|
|
// Canvas resizes change the w/h half of the world↔screen transform
|
|
|
|
|
|
// without touching scale/center — re-push post-layout (CallAfter runs
|
|
|
|
|
|
// after the GAL's own onSize updated the screen size).
|
|
|
|
|
|
canvas->Bind( wxEVT_SIZE, [this]( wxSizeEvent& e )
|
|
|
|
|
|
{
|
|
|
|
|
|
e.Skip();
|
|
|
|
|
|
|
|
|
|
|
|
if( EDA_DRAW_FRAME* f = frame() )
|
|
|
|
|
|
f->CallAfter( [this]() { emitViewportIfChanged(); } );
|
|
|
|
|
|
} );
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** kicadCollabSetRemote: full remote-peers snapshot — `{peers:[{id,name,
|
|
|
|
|
|
* color,cursor:{x,y}|null,selection:[uuid],xsel:[uuid]}],locks:[{uuid,
|
|
|
|
|
|
* name}]}`, trivially derived from awareness.getStates() and idempotent
|
|
|
|
|
|
* (the overlay is cleared + fully redrawn). Empty peers clears it. */
|
|
|
|
|
|
void setRemote( const std::string& aJson )
|
|
|
|
|
|
{
|
|
|
|
|
|
json j = json::parse( aJson, nullptr, /*allow_exceptions*/ false );
|
|
|
|
|
|
|
|
|
|
|
|
if( j.is_discarded() )
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
|
|
std::vector<PEER> parsed;
|
|
|
|
|
|
|
|
|
|
|
|
for( const json& p : j.value( "peers", json::array() ) )
|
|
|
|
|
|
{
|
|
|
|
|
|
PEER peer;
|
2026-08-28 16:14:44 +02:00
|
|
|
|
peer.id = p.value( "id", "" );
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
|
peer.name = p.value( "name", "" );
|
|
|
|
|
|
peer.color = parsePeerColor( p.value( "color", "" ) );
|
|
|
|
|
|
|
|
|
|
|
|
if( p.contains( "cursor" ) && p["cursor"].is_object() )
|
|
|
|
|
|
{
|
|
|
|
|
|
peer.hasCursor = true;
|
2026-08-28 19:42:31 +02:00
|
|
|
|
peer.cursor = VECTOR2D( numOr( p["cursor"], "x", 0.0 ), numOr( p["cursor"], "y", 0.0 ) );
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for( const json& u : p.value( "selection", json::array() ) )
|
|
|
|
|
|
{
|
|
|
|
|
|
if( u.is_string() )
|
|
|
|
|
|
peer.selection.emplace_back( wxString::FromUTF8( u.get<std::string>().c_str() ) );
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Cross-app selection (0006): symbol uuids from the OTHER editor.
|
|
|
|
|
|
for( const json& u : p.value( "xsel", json::array() ) )
|
|
|
|
|
|
{
|
|
|
|
|
|
if( u.is_string() )
|
|
|
|
|
|
peer.xsel.emplace_back( wxString::FromUTF8( u.get<std::string>().c_str() ) );
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
parsed.push_back( std::move( peer ) );
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Remote soft-locks (0007): every other client's held uuids with the
|
|
|
|
|
|
// holder's display name for the infobar.
|
|
|
|
|
|
std::map<KIID, std::string> parsedLocks;
|
|
|
|
|
|
|
|
|
|
|
|
for( const json& l : j.value( "locks", json::array() ) )
|
|
|
|
|
|
{
|
|
|
|
|
|
std::string uuid = l.is_object() ? l.value( "uuid", "" ) : "";
|
|
|
|
|
|
|
|
|
|
|
|
if( !uuid.empty() )
|
|
|
|
|
|
parsedLocks[ KIID( wxString::FromUTF8( uuid.c_str() ) ) ] = l.value( "name", "" );
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-28 16:14:44 +02:00
|
|
|
|
// Shapes signature (everything but cursors): an unchanged one means
|
|
|
|
|
|
// this push is a cursor tick — repaint cursors only.
|
|
|
|
|
|
std::string sig;
|
|
|
|
|
|
|
|
|
|
|
|
for( const PEER& peer : parsed )
|
|
|
|
|
|
{
|
|
|
|
|
|
sig += peer.id + '\x1f' + peer.name + '\x1f' + peer.color.ToCSSString().ToStdString() + '\x1e';
|
|
|
|
|
|
|
|
|
|
|
|
for( const KIID& k : peer.selection )
|
|
|
|
|
|
sig += pcbjam_collab::toUtf8( k.AsString() ) + ',';
|
|
|
|
|
|
|
|
|
|
|
|
sig += '\x1e';
|
|
|
|
|
|
|
|
|
|
|
|
for( const KIID& k : peer.xsel )
|
|
|
|
|
|
sig += pcbjam_collab::toUtf8( k.AsString() ) + ',';
|
|
|
|
|
|
|
|
|
|
|
|
sig += '\x1d';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for( const auto& [id, name] : parsedLocks )
|
|
|
|
|
|
sig += pcbjam_collab::toUtf8( id.AsString() ) + '=' + name + ';';
|
|
|
|
|
|
|
|
|
|
|
|
bool shapesChanged = sig != lastShapeSig;
|
|
|
|
|
|
lastShapeSig = std::move( sig );
|
|
|
|
|
|
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
|
peers = std::move( parsed );
|
|
|
|
|
|
locks = std::move( parsedLocks );
|
|
|
|
|
|
start();
|
2026-08-28 16:14:44 +02:00
|
|
|
|
scheduleRedraw( shapesChanged, true );
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** kicadCollabSetRemoteCursors (findings Y-4): `{cursors:[{id,cursor:{x,y}|null}]}`
|
|
|
|
|
|
* for peers of the last full snapshot — updates their cursors and repaints
|
|
|
|
|
|
* the cursor overlays only. Unknown ids are ignored (the next full
|
|
|
|
|
|
* snapshot introduces them). */
|
|
|
|
|
|
void setRemoteCursors( const std::string& aJson )
|
|
|
|
|
|
{
|
|
|
|
|
|
json j = json::parse( aJson, nullptr, /*allow_exceptions*/ false );
|
|
|
|
|
|
|
|
|
|
|
|
if( j.is_discarded() )
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
|
|
bool changed = false;
|
|
|
|
|
|
|
|
|
|
|
|
for( const json& c : j.value( "cursors", json::array() ) )
|
|
|
|
|
|
{
|
|
|
|
|
|
std::string id = c.is_object() ? c.value( "id", "" ) : "";
|
|
|
|
|
|
|
|
|
|
|
|
for( PEER& peer : peers )
|
|
|
|
|
|
{
|
|
|
|
|
|
if( peer.id != id )
|
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
|
|
if( c.contains( "cursor" ) && c["cursor"].is_object() )
|
|
|
|
|
|
{
|
|
|
|
|
|
peer.hasCursor = true;
|
2026-08-28 19:42:31 +02:00
|
|
|
|
peer.cursor = VECTOR2D( numOr( c["cursor"], "x", 0.0 ), numOr( c["cursor"], "y", 0.0 ) );
|
2026-08-28 16:14:44 +02:00
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
peer.hasCursor = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
changed = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if( changed )
|
|
|
|
|
|
scheduleRedraw( false, true );
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
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
|
|
|
|
/** kicadCollabSetPins (0005): comment pins — `{pins:[{id,name,x,y,
|
|
|
|
|
|
* color,resolved,unread}]}`, world IU coords resolved by the TS side
|
|
|
|
|
|
* from the ydoc anchors. Snapshot semantics like setRemote. */
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
|
void setPins( const std::string& aJson )
|
|
|
|
|
|
{
|
|
|
|
|
|
json j = json::parse( aJson, nullptr, /*allow_exceptions*/ false );
|
|
|
|
|
|
|
|
|
|
|
|
if( j.is_discarded() )
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
|
|
std::vector<PIN> parsed;
|
|
|
|
|
|
|
|
|
|
|
|
for( const json& p : j.value( "pins", json::array() ) )
|
|
|
|
|
|
{
|
|
|
|
|
|
PIN pin;
|
|
|
|
|
|
pin.id = p.value( "id", "" );
|
|
|
|
|
|
pin.name = p.value( "name", "" );
|
2026-08-28 19:42:31 +02:00
|
|
|
|
pin.pos = VECTOR2D( numOr( p, "x", 0.0 ), numOr( p, "y", 0.0 ) );
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
|
pin.color = parsePeerColor( p.value( "color", "" ) );
|
|
|
|
|
|
pin.resolved = p.value( "resolved", false );
|
comments-ux: figma bubble pins, floating panel, seen/reactions/mentions UI, theme follow (0001 A–E + 0002)
- GAL pin = one closed polygon: round body, squared-off bottom-left corner
ON the anchor; PIN gains unread (accent ring); tuner knobs; shipped
defaults r9/ring4/alpha.9. DOM hit/highlight sized+offset from a LIVE
pin-geometry radius store the tuner feeds.
- Floating comments panel: draggable (shared useDraggablePanel with
always-onscreen restore; overlay FAB retrofitted), collapsible to header,
header carries add/show-hide/mark-all; unread badges (rose on mention).
- Reactions (emoji-mart lazy, quick-row) + @-mention autocomplete
(MentionInput; backend roster with presence/author fallback).
- Theme: ?theme= > storage > OS, no-flash boot, toggles (HomePage + overlay
View row), boot-seeded pcbjam-dark schematic colors + kicadSetColorTheme /
kicadSetDarkChrome bridges (canvas + wx chrome live flip), light/dark
variants across all overlay surfaces.
- e2e: panel/seen/reactions/mentions/theme specs + resize-spec geometry;
bumps pcbjam-shared (flat-key seen/reactions + listCollaborators) and
wxwidgets (dark chrome) pointers.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HLwn1toiNKi1MgxGKnZTes
2026-07-24 13:21:22 +02:00
|
|
|
|
pin.unread = p.value( "unread", false );
|
refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code.
Factored into two header-only shared files (zero build-script changes — the
collab_presence_style.h precedent):
- collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE
fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters
(onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test
hooks.
- collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence
state and machinery (start/canvas binds/lock query, setRemote/setPins/
setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull,
releaseSelection, locks probe), written against the EDA_DRAW_FRAME +
SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool,
selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU
(anonymous-namespace presenceCore()) so the merged image keeps per-editor
state separation.
- NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by
the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was
duplicated within each TU, letting the test probe drift from the pixels.
Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/
flushDiff/doApply*) — structurally parallel but the bodies encode per-editor
sync semantics and editor-specific asyncify devirtualization workarounds that
must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing
KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed
(ClearSelection is not on the SELECTION_TOOL base).
eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names,
signatures and the kicad_editor_embind.cpp dispatcher are unchanged.
Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18
(incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-08 15:18:58 +02:00
|
|
|
|
parsed.push_back( std::move( pin ) );
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pins = std::move( parsed );
|
|
|
|
|
|
start();
|
|
|
|
|
|
scheduleRedraw();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** kicadCollabSetStyle (presence tuner): live-patch the overlay STYLE
|
|
|
|
|
|
* (partial JSON — see collab_presence_style.h) and repaint. */
|
|
|
|
|
|
void setStyle( const std::string& aJson )
|
|
|
|
|
|
{
|
|
|
|
|
|
json j = json::parse( aJson, nullptr, /*allow_exceptions*/ false );
|
|
|
|
|
|
|
|
|
|
|
|
if( j.is_discarded() )
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
|
|
patchStyle( style, j );
|
|
|
|
|
|
scheduleRedraw();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** kicadCollabReleaseSelection (0007): the local client LOST the selection
|
|
|
|
|
|
* tiebreak — release the contested items (only those) from the live
|
|
|
|
|
|
* selection. If an interactive tool (move/drag) holds them, cancel it
|
|
|
|
|
|
* first (ESC semantics — the preview reverts); a bare cancel is NOT sent
|
|
|
|
|
|
* when idle, since ESC on the base selection tool would clear the whole
|
|
|
|
|
|
* selection. Ends with a forced selection re-emit (programmatic changes
|
|
|
|
|
|
* close no canvas event). */
|
|
|
|
|
|
void releaseSelection( const std::string& aUuidsJson, const std::string& aHolder )
|
|
|
|
|
|
{
|
|
|
|
|
|
json j = json::parse( aUuidsJson, nullptr, /*allow_exceptions*/ false );
|
|
|
|
|
|
|
|
|
|
|
|
if( j.is_discarded() || !j.is_array() )
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
|
|
EDA_DRAW_FRAME* fr = frame();
|
|
|
|
|
|
|
|
|
|
|
|
if( !fr )
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
|
|
std::vector<KIID> ids;
|
|
|
|
|
|
|
|
|
|
|
|
for( const json& u : j )
|
|
|
|
|
|
{
|
|
|
|
|
|
if( u.is_string() )
|
|
|
|
|
|
ids.emplace_back( wxString::FromUTF8( u.get<std::string>().c_str() ) );
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if( ids.empty() )
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
|
|
wxString holder = wxString::FromUTF8( aHolder.c_str() );
|
|
|
|
|
|
|
|
|
|
|
|
fr->CallAfter( [this, fr, ids, holder]() {
|
|
|
|
|
|
if( !fr->ToolStackIsEmpty() )
|
|
|
|
|
|
fr->GetToolManager()->RunAction( ACTIONS::cancelInteractive );
|
|
|
|
|
|
|
|
|
|
|
|
SELECTION_TOOL* st = selectionTool( fr );
|
|
|
|
|
|
|
|
|
|
|
|
if( !st )
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
|
|
bool released = false;
|
|
|
|
|
|
|
|
|
|
|
|
for( const KIID& id : ids )
|
|
|
|
|
|
{
|
|
|
|
|
|
EDA_ITEM* item = resolveItem( fr, id );
|
|
|
|
|
|
|
|
|
|
|
|
if( item && item->IsSelected() )
|
|
|
|
|
|
{
|
|
|
|
|
|
st->RemoveItemFromSel( item );
|
|
|
|
|
|
released = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if( released )
|
|
|
|
|
|
{
|
|
|
|
|
|
fr->ShowInfoBarWarning( wxString::Format( _( "%s is editing this — released from "
|
|
|
|
|
|
"your selection." ),
|
|
|
|
|
|
holder ),
|
|
|
|
|
|
true );
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
scheduleSelCheck();
|
|
|
|
|
|
} );
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** Test helper: REALLY select an item through the selection tool, then run
|
|
|
|
|
|
* the presence check (programmatic selects close no canvas event). */
|
|
|
|
|
|
void selectItem( EDA_ITEM* aItem )
|
|
|
|
|
|
{
|
|
|
|
|
|
EDA_DRAW_FRAME* fr = frame();
|
|
|
|
|
|
|
|
|
|
|
|
if( !fr )
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
|
|
fr->CallAfter( [this, fr, aItem]() {
|
|
|
|
|
|
if( SELECTION_TOOL* st = selectionTool( fr ) )
|
|
|
|
|
|
{
|
|
|
|
|
|
st->AddItemToSel( aItem );
|
|
|
|
|
|
scheduleSelCheck();
|
|
|
|
|
|
}
|
|
|
|
|
|
} );
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** Test probe (0007): the current remote soft-lock set as `[{uuid, name}]`. */
|
|
|
|
|
|
std::string locksJson()
|
|
|
|
|
|
{
|
|
|
|
|
|
json arr = json::array();
|
|
|
|
|
|
|
|
|
|
|
|
for( const auto& [id, name] : locks )
|
|
|
|
|
|
arr.push_back( { { "uuid", pcbjam_collab::toUtf8( id.AsString() ) }, { "name", name } } );
|
|
|
|
|
|
|
|
|
|
|
|
return arr.dump();
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
} // namespace pcbjam_presence
|
|
|
|
|
|
|
|
|
|
|
|
#endif // __EMSCRIPTEN__
|