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
This commit is contained in:
parent
9697de7b26
commit
1f85e13acf
16 changed files with 821 additions and 52 deletions
|
|
@ -425,6 +425,52 @@ test("cross-app ghost render: a pcbnew peer's xsel uuid resolves on the current
|
|||
expect(hasAbort(testLogger)).toBe(false);
|
||||
});
|
||||
|
||||
// Follow-user (0008): FitViewport applies a world rect with contain semantics —
|
||||
// verified through the GetViewport round trip (center lands, the contained
|
||||
// axis' half-extent matches the request within the fit's own tolerance).
|
||||
test("fitViewport applies a world rect (contain) — GetViewport round trip", async ({
|
||||
page,
|
||||
testLogger,
|
||||
}) => {
|
||||
await bootAndOpen(page);
|
||||
|
||||
const target = { cx: 120e6, cy: 90e6, halfW: 40e6, halfH: 30e6 };
|
||||
await page.evaluate((t) => {
|
||||
const m = (window as unknown as PresenceWindow).Module as unknown as {
|
||||
kicadCollabFitViewport(cx: number, cy: number, hw: number, hh: number): void;
|
||||
};
|
||||
m.kicadCollabFitViewport(t.cx, t.cy, t.halfW, t.halfH);
|
||||
}, target);
|
||||
|
||||
// The fit is CallAfter+fiber scheduled — poll the transform until it lands.
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const vp = await page.evaluate(() =>
|
||||
JSON.parse((window as unknown as PresenceWindow).Module.kicadCollabGetViewport()),
|
||||
);
|
||||
return Math.abs(vp.cx - target.cx) < 1e6 && Math.abs(vp.cy - target.cy) < 1e6;
|
||||
},
|
||||
{ timeout: 20000, intervals: [250] },
|
||||
)
|
||||
.toBe(true);
|
||||
|
||||
const vp = await page.evaluate(() =>
|
||||
JSON.parse((window as unknown as PresenceWindow).Module.kicadCollabGetViewport()),
|
||||
);
|
||||
// Contain: at least one axis' visible half-extent matches the request (the
|
||||
// other overshoots by the canvas aspect), and neither is SMALLER (cropping).
|
||||
const halfW = vp.w / 2 / vp.scale;
|
||||
const halfH = vp.h / 2 / vp.scale;
|
||||
expect(halfW).toBeGreaterThan(target.halfW * 0.95);
|
||||
expect(halfH).toBeGreaterThan(target.halfH * 0.95);
|
||||
const relW = Math.abs(halfW - target.halfW) / target.halfW;
|
||||
const relH = Math.abs(halfH - target.halfH) / target.halfH;
|
||||
expect(Math.min(relW, relH)).toBeLessThan(0.05);
|
||||
|
||||
expect(hasAbort(testLogger)).toBe(false);
|
||||
});
|
||||
|
||||
test("viewport export returns a sane world↔screen transform", async ({ page, testLogger }) => {
|
||||
await bootAndOpen(page);
|
||||
|
||||
|
|
|
|||
|
|
@ -518,6 +518,52 @@ test("cross-app ghost render: an eeschema peer's symbol uuid maps to the footpri
|
|||
expect(hasAbort(testLogger)).toBe(false);
|
||||
});
|
||||
|
||||
// Follow-user (0008): FitViewport applies a world rect with contain semantics —
|
||||
// verified through the GetViewport round trip (center lands, the contained
|
||||
// axis' half-extent matches the request within the fit's own tolerance).
|
||||
test("fitViewport applies a world rect (contain) — GetViewport round trip", async ({
|
||||
page,
|
||||
testLogger,
|
||||
}) => {
|
||||
await bootAndOpen(page);
|
||||
|
||||
const target = { cx: 120e6, cy: 90e6, halfW: 40e6, halfH: 30e6 };
|
||||
await page.evaluate((t) => {
|
||||
const m = (window as unknown as PresenceWindow).Module as unknown as {
|
||||
kicadCollabFitViewport(cx: number, cy: number, hw: number, hh: number): void;
|
||||
};
|
||||
m.kicadCollabFitViewport(t.cx, t.cy, t.halfW, t.halfH);
|
||||
}, target);
|
||||
|
||||
// The fit is CallAfter+fiber scheduled — poll the transform until it lands.
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const vp = await page.evaluate(() =>
|
||||
JSON.parse((window as unknown as PresenceWindow).Module.kicadCollabGetViewport()),
|
||||
);
|
||||
return Math.abs(vp.cx - target.cx) < 1e6 && Math.abs(vp.cy - target.cy) < 1e6;
|
||||
},
|
||||
{ timeout: 20000, intervals: [250] },
|
||||
)
|
||||
.toBe(true);
|
||||
|
||||
const vp = await page.evaluate(() =>
|
||||
JSON.parse((window as unknown as PresenceWindow).Module.kicadCollabGetViewport()),
|
||||
);
|
||||
// Contain: at least one axis' visible half-extent matches the request (the
|
||||
// other overshoots by the canvas aspect), and neither is SMALLER (cropping).
|
||||
const halfW = vp.w / 2 / vp.scale;
|
||||
const halfH = vp.h / 2 / vp.scale;
|
||||
expect(halfW).toBeGreaterThan(target.halfW * 0.95);
|
||||
expect(halfH).toBeGreaterThan(target.halfH * 0.95);
|
||||
const relW = Math.abs(halfW - target.halfW) / target.halfW;
|
||||
const relH = Math.abs(halfH - target.halfH) / target.halfH;
|
||||
expect(Math.min(relW, relH)).toBeLessThan(0.05);
|
||||
|
||||
expect(hasAbort(testLogger)).toBe(false);
|
||||
});
|
||||
|
||||
test("viewport export returns a sane world↔screen transform", async ({ page, testLogger }) => {
|
||||
await bootAndOpen(page);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue