Audience loses the presented frame when the owner moves it during presentation (no re-focus until reload) #199

Open
opened 2026-05-18 11:52:29 +00:00 by AhmedHanafy725 · 3 comments
Member

Summary

While an audience member is watching in presentation mode, if the owner/editor moves (or resizes/rotates) the currently presented frame, the audience loses sight of that frame — the canvas stays zoomed/positioned to where the frame used to be, and the spotlight cutout is in the wrong place — until the slide changes or the page is reloaded.

Confirmed root cause

focusFrame(frame) in crates/hero_whiteboard_admin/static/web/js/whiteboard/frames.js (~line 341) is what fits the audience's stage to the focused frame and computes _focusedScreenRect (the spotlight cutout consumed by the host template). It is invoked only on slide transitions:

  • startPresentation() (first frame),
  • nextFrame() / prevFrame(),
  • applyRemoteSlide(index) (~line 329, remote slide change),
  • the presentation resize observer (~line 118-121).

When the presenter moves/resizes/rotates the focused frame, that change is broadcast as a normal object.update. On the audience side sync.js applySyncUpdate applies it (the type === 'frame' branches ~sync.js:324 and ~735) and the frame's Konva node moves to its new coordinates — but nothing notifies WhiteboardFrames, and focusFrame is not re-run. There is no exported re-focus hook and no call site outside frames.js (verified by grep). So the audience's stage stays fitted to the old frame rect: the frame is now off-screen / outside the spotlight, appearing "lost". A subsequent slide change or a page reload re-runs focusFrame from the frame's current position and it looks correct again.

The presenter's own view has the same gap (dragging the focused frame during presentation does not re-fit their view either), but the audience impact is the visible bug.

Expected

While presenting, if the currently focused frame's geometry (position, size, or rotation) changes — whether from the local presenter or a remote editor — the view should re-fit to the frame and the spotlight cutout should track it, so the audience keeps seeing the frame without reloading.

Requirements

  • When presentation mode is active and the currently-focused frame's geometry changes via a sync update (or local edit), re-run the fit/spotlight logic for that frame so the audience's view follows it.
  • This must work for the remote case (editor moves the frame, audience is watching via a presentation/share link) and the local case (presenter drags the focused frame while presenting).
  • Cover position, size, and rotation changes of the focused frame.
  • Only re-fit when the changed object is the currently focused frame (do not thrash the view on unrelated object updates elsewhere on the board).
  • No feedback/echo loop: re-focusing must not re-broadcast a slide or trigger a sync write that bounces back (respect the existing _applyingSync / sync-suppression guards).
  • If the focused frame is deleted while presenting, degrade gracefully (e.g. fall back to a valid slide or the no-frames notice) rather than leaving the audience on a blank canvas.
  • No server or schema change expected; this is a client sync/presentation wiring gap. Confirm during planning.

Notes / pointers

  • frames.js already has the pattern for sync-driven re-focus: applyRemoteSlide wraps focusFrame in _applyingSync = true/false. A new exported hook (e.g. WhiteboardFrames.notifyObjectUpdated(objectId) or refocusIfPresenting(objectId)) called from sync.js applySyncUpdate after a frame update would mirror how sync.js already calls WhiteboardConnectors.reAnchorByObject(...) after applying object moves.
  • Determine "is this the currently focused frame": frames.js tracks currentFrameIndex and the sorted frames array (getFrames()); compare the updated object id against frames[currentFrameIndex].id().
  • Consider debouncing/coalescing: a drag produces many object.update messages; re-fitting every tick is acceptable if it is cheap (it is the same math as a slide change) but coalescing per animation frame is preferable.
  • Check the local presenter path too: when the presenter drags the focused frame, the same re-focus should fire (the local edit path, not only the remote applySyncUpdate).
  • Deploy reminder: assets embed at compile time via rust-embed — after editing JS, touch crates/hero_whiteboard_admin/src/assets.rs before cargo build --release -p hero_whiteboard_admin, and verify the served asset changed before testing.

Affected files (expected)

  • crates/hero_whiteboard_admin/static/web/js/whiteboard/frames.js — exported re-focus hook; only act when presenting and the object is the focused frame.
  • crates/hero_whiteboard_admin/static/web/js/whiteboard/sync.js — call the hook from the frame branch of applySyncUpdate (and wherever local frame moves are applied/broadcast).
  • Possibly the local drag/commit path for frames if the presenter-local case isn't covered by the sync hook.
## Summary While an audience member is watching in presentation mode, if the owner/editor moves (or resizes/rotates) the currently presented frame, the audience loses sight of that frame — the canvas stays zoomed/positioned to where the frame used to be, and the spotlight cutout is in the wrong place — until the slide changes or the page is reloaded. ## Confirmed root cause `focusFrame(frame)` in `crates/hero_whiteboard_admin/static/web/js/whiteboard/frames.js` (~line 341) is what fits the audience's stage to the focused frame and computes `_focusedScreenRect` (the spotlight cutout consumed by the host template). It is invoked **only** on slide transitions: - `startPresentation()` (first frame), - `nextFrame()` / `prevFrame()`, - `applyRemoteSlide(index)` (~line 329, remote slide change), - the presentation resize observer (~line 118-121). When the presenter moves/resizes/rotates the focused frame, that change is broadcast as a normal `object.update`. On the audience side `sync.js applySyncUpdate` applies it (the `type === 'frame'` branches ~`sync.js:324` and ~`735`) and the frame's Konva node moves to its new coordinates — but nothing notifies `WhiteboardFrames`, and `focusFrame` is **not** re-run. There is no exported re-focus hook and no call site outside `frames.js` (verified by grep). So the audience's stage stays fitted to the old frame rect: the frame is now off-screen / outside the spotlight, appearing "lost". A subsequent slide change or a page reload re-runs `focusFrame` from the frame's current position and it looks correct again. The presenter's own view has the same gap (dragging the focused frame during presentation does not re-fit their view either), but the audience impact is the visible bug. ## Expected While presenting, if the currently focused frame's geometry (position, size, or rotation) changes — whether from the local presenter or a remote editor — the view should re-fit to the frame and the spotlight cutout should track it, so the audience keeps seeing the frame without reloading. ## Requirements - When presentation mode is active and the currently-focused frame's geometry changes via a sync update (or local edit), re-run the fit/spotlight logic for that frame so the audience's view follows it. - This must work for the remote case (editor moves the frame, audience is watching via a presentation/share link) and the local case (presenter drags the focused frame while presenting). - Cover position, size, and rotation changes of the focused frame. - Only re-fit when the changed object is the currently focused frame (do not thrash the view on unrelated object updates elsewhere on the board). - No feedback/echo loop: re-focusing must not re-broadcast a slide or trigger a sync write that bounces back (respect the existing `_applyingSync` / sync-suppression guards). - If the focused frame is deleted while presenting, degrade gracefully (e.g. fall back to a valid slide or the no-frames notice) rather than leaving the audience on a blank canvas. - No server or schema change expected; this is a client sync/presentation wiring gap. Confirm during planning. ## Notes / pointers - `frames.js` already has the pattern for sync-driven re-focus: `applyRemoteSlide` wraps `focusFrame` in `_applyingSync = true/false`. A new exported hook (e.g. `WhiteboardFrames.notifyObjectUpdated(objectId)` or `refocusIfPresenting(objectId)`) called from `sync.js applySyncUpdate` after a frame update would mirror how `sync.js` already calls `WhiteboardConnectors.reAnchorByObject(...)` after applying object moves. - Determine "is this the currently focused frame": `frames.js` tracks `currentFrameIndex` and the sorted `frames` array (`getFrames()`); compare the updated object id against `frames[currentFrameIndex].id()`. - Consider debouncing/coalescing: a drag produces many `object.update` messages; re-fitting every tick is acceptable if it is cheap (it is the same math as a slide change) but coalescing per animation frame is preferable. - Check the local presenter path too: when the presenter drags the focused frame, the same re-focus should fire (the local edit path, not only the remote `applySyncUpdate`). - Deploy reminder: assets embed at compile time via rust-embed — after editing JS, `touch crates/hero_whiteboard_admin/src/assets.rs` before `cargo build --release -p hero_whiteboard_admin`, and verify the served asset changed before testing. ## Affected files (expected) - `crates/hero_whiteboard_admin/static/web/js/whiteboard/frames.js` — exported re-focus hook; only act when presenting and the object is the focused frame. - `crates/hero_whiteboard_admin/static/web/js/whiteboard/sync.js` — call the hook from the frame branch of `applySyncUpdate` (and wherever local frame moves are applied/broadcast). - Possibly the local drag/commit path for frames if the presenter-local case isn't covered by the sync hook.
Author
Member

Implementation Spec for Issue #199

Objective

When the presentation owner moves, resizes, or rotates the currently focused frame during a live presentation, every audience client (and the presenter) must immediately re-fit the stage to the new frame geometry and reposition the spotlight cutout — no slide change, no reload, no sync echo loop, no server/schema change.

Requirements

  • A geometry change to the focused frame on a remote peer re-runs the fit-and-spotlight logic on the audience.
  • Local presenter geometry changes to the focused frame re-fit the presenter's own view too.
  • Position, size, and rotation all re-fit (focusFrame already uses getClientRect + bg dims + rotation, so all three are covered once it re-runs).
  • Updates to non-focused objects must NOT move the presentation view.
  • Rapid drag spam coalesced (one re-fit per animation frame).
  • Focused frame deleted mid-presentation degrades gracefully (clamp to a valid slide or show the no-frames notice), never blank canvas.
  • No sync write / slide rebroadcast bouncing back. JS-only.

Files to Modify/Create

  • frames.js - exported notifyObjectUpdated(objectId) hook (rAF-coalesced) + notifyFrameRemoved() graceful fallback.
  • sync.js - call the hook from the type === 'frame' branch of applySyncUpdate; call notifyFrameRemoved() from the object.deleted frame branch.
  • objects.js - call the hook from the local frame dragend commit.
  • tools.js - call the hook from the shared transformend commit (local presenter resize/rotate).
  • crates/hero_whiteboard_admin/src/assets.rs - touch only (rust-embed cache-bust).

Implementation Plan

Step 1: Add notifyObjectUpdated to WhiteboardFrames

Files: crates/hero_whiteboard_admin/static/web/js/whiteboard/frames.js

  • Add var _refocusRafScheduled = false; near _focusedScreenRect (~:339).
  • Add function notifyObjectUpdated(objectId) (after focusFrame/_applyWebframePresentationVisibility, before the return ~:411):
    • if (!presentationMode) return; ; if (!frames.length) return;
    • Resolve focused frame WITHOUT getFrames() (use the in-memory array like the resize observer ~:121): var focused = frames[currentFrameIndex]; if (!focused) return;
    • Id match (handles temp→server remap since group.id() is updated in place by WhiteboardObjects.remapId): var fid = String(focused.id()); if (String(objectId) !== fid) return;
    • Coalesce per rAF reusing the syncDrawScheduled pattern (sync.js:893-899): if _refocusRafScheduled return; set it; requestAnimationFrame(function(){ _refocusRafScheduled=false; if(!presentationMode||!frames.length) return; var f=frames[currentFrameIndex]; if(!f) return; focusFrame(f); _emit(); });
  • Do NOT wrap in _applyingSync — that guard (frames.js:326-333) exists only to suppress _broadcastSlide in applyRemoteSlide; this path never calls _broadcastSlide, and _emit MUST run so the host repositions the spotlight.
  • Export notifyObjectUpdated (and notifyFrameRemoved from Step 5) in the module return.
    Dependencies: none

Step 2: Call from the remote sync path

Files: crates/hero_whiteboard_admin/static/web/js/whiteboard/sync.js

  • In applySyncUpdate, at the end of the type === 'frame' branch (~:735-748; generic x/y/size/rotation already applied earlier ~:614-649), add:
    if (typeof WhiteboardFrames !== 'undefined' && WhiteboardFrames.notifyObjectUpdated) { WhiteboardFrames.notifyObjectUpdated(strId); }
    Precedent: the sibling-module call WhiteboardConnectors.reAnchorByObject(strId) sync.js:888-890 (mirror guard shape; keep it inside the frame branch so it doesn't run for every type). The branch already runs under _applyingSync = true (:595) so no echo possible.
    Dependencies: Step 1

Step 3: Local presenter drag path

Files: crates/hero_whiteboard_admin/static/web/js/whiteboard/objects.js

  • Frame group dragend handler (~:1198-1210): after WhiteboardSync.onUpdate(group) / children loop, before capturedChildren = [], add if (typeof WhiteboardFrames !== 'undefined' && WhiteboardFrames.notifyObjectUpdated) { WhiteboardFrames.notifyObjectUpdated(group.id()); }. Hook self-guards on presentationMode + focused-id so it's inert during normal editing. (Originator skips its own broadcast via msg.sender === localUserId sync.js:152, so the remote hook does NOT cover the local presenter.)
    Dependencies: Step 1

Step 4: Local presenter resize/rotate path

Files: crates/hero_whiteboard_admin/static/web/js/whiteboard/tools.js

  • Shared transformer.on('transformend', ...) (:270-287): inside the nodes.forEach, after WhiteboardHistory.commitUpdate(node.id()); (:286), add if (typeof WhiteboardFrames !== 'undefined' && WhiteboardFrames.notifyObjectUpdated) { WhiteboardFrames.notifyObjectUpdated(node.id()); }. focusFrame re-fits size+rotation via getClientRect.
    Dependencies: Step 1. Steps 2/3/4 parallelizable after Step 1.

Step 5: Graceful focused-frame-deleted handling

Files: crates/hero_whiteboard_admin/static/web/js/whiteboard/frames.js, sync.js

  • Add notifyFrameRemoved() to WhiteboardFrames: if presentationMode, getFrames() (rebuild from live .find('.frame')); if frames.length === 0showNoFramesNotice() + _focusedScreenRect = null; _emit();; else clamp currentFrameIndex = Math.max(0, Math.min(currentFrameIndex, frames.length - 1)); focusFrame(frames[currentFrameIndex]); _emit(); (clamp pattern from applyRemoteSlide ~:324).
  • Wire from sync.js object.deleted frame branch (~:167-188) after obj.group.destroy()/delete allObj[delId]: if (obj.type === 'frame' && WhiteboardFrames.notifyFrameRemoved) WhiteboardFrames.notifyFrameRemoved();
  • Defensive: in the notifyObjectUpdated rAF body, short-circuit if focused.getStage && !focused.getStage() (destroyed node) — focusFrame already returns early when .bg is missing.
    Dependencies: Step 1

Step 6: Rebuild + verify

Files: crates/hero_whiteboard_admin/src/assets.rs

  • touch it; cargo build --release -p hero_whiteboard_admin; restart; confirm served frames.js contains notifyObjectUpdated before testing (rust-embed embeds at compile time).
    Dependencies: Steps 1-5

Acceptance Criteria

  • Remote editor drags the focused frame → every audience view re-fits + spotlight follows, no reload/slide-change.
  • Remote editor resizes the focused frame → audience re-fits new size (zoom adjusts), spotlight resizes.
  • Remote editor rotates the focused frame → audience re-fits rotated AABB, spotlight rotation updates.
  • Updating a non-focused object/frame does NOT move the presentation view on any client.
  • Local presenter drag/resize/rotate of the focused frame re-fits the presenter's own view + spotlight.
  • Rapid drag → smooth single re-fit per frame (no thrash).
  • Deleting the focused frame mid-presentation clamps to a valid slide or shows the no-frames notice — never blank.
  • No echo/feedback loop; presenter's own broadcast not re-applied to itself.
  • No server/RPC/schema/HTML-structural change; JS-only + assets.rs touch.

Notes

  • Hook: WhiteboardFrames.notifyObjectUpdated(objectId) + notifyFrameRemoved(). Called from applySyncUpdate frame branch; precedent WhiteboardConnectors.reAnchorByObject(strId) ~sync.js:888-890.
  • Do NOT add _applyingSync wrap: it only suppresses _broadcastSlide (not called here); _emit must run so the host spotlight tracks. sync.js-side _applyingSync=true (~:595) already blankets the remote path (onUpdate guard ~:458).
  • Coalesce via the syncDrawScheduled rAF one-shot (sync.js:893-899) with a dedicated _refocusRafScheduled; resolve the focused frame fresh inside the rAF so last drag wins.
  • Local presenter NOT covered by the sync hook (self-broadcast skipped, sync.js:152) — covered by objects.js dragend + tools.js transformend, same self-guarding hook (inert outside presentation).
  • Spotlight: focusFrame sets _focusedScreenRect (~:369-375); _emit passes it as state.screenRect to the host subscriber (board.html positionSpotlight ~:567-625 / board_view.html ~:188-208), refreshing #pres-spotlight incl. rotation.
  • Deploy: rust-embed compile-time embed — after edits touch crates/hero_whiteboard_admin/src/assets.rs, cargo build --release -p hero_whiteboard_admin, restart, verify served frames.js has the new export before testing.
## Implementation Spec for Issue #199 ### Objective When the presentation owner moves, resizes, or rotates the currently focused frame during a live presentation, every audience client (and the presenter) must immediately re-fit the stage to the new frame geometry and reposition the spotlight cutout — no slide change, no reload, no sync echo loop, no server/schema change. ### Requirements - A geometry change to the focused frame on a remote peer re-runs the fit-and-spotlight logic on the audience. - Local presenter geometry changes to the focused frame re-fit the presenter's own view too. - Position, size, and rotation all re-fit (focusFrame already uses getClientRect + bg dims + rotation, so all three are covered once it re-runs). - Updates to non-focused objects must NOT move the presentation view. - Rapid drag spam coalesced (one re-fit per animation frame). - Focused frame deleted mid-presentation degrades gracefully (clamp to a valid slide or show the no-frames notice), never blank canvas. - No sync write / slide rebroadcast bouncing back. JS-only. ### Files to Modify/Create - `frames.js` - exported `notifyObjectUpdated(objectId)` hook (rAF-coalesced) + `notifyFrameRemoved()` graceful fallback. - `sync.js` - call the hook from the `type === 'frame'` branch of `applySyncUpdate`; call `notifyFrameRemoved()` from the `object.deleted` frame branch. - `objects.js` - call the hook from the local frame `dragend` commit. - `tools.js` - call the hook from the shared `transformend` commit (local presenter resize/rotate). - `crates/hero_whiteboard_admin/src/assets.rs` - touch only (rust-embed cache-bust). ### Implementation Plan #### Step 1: Add `notifyObjectUpdated` to WhiteboardFrames Files: `crates/hero_whiteboard_admin/static/web/js/whiteboard/frames.js` - Add `var _refocusRafScheduled = false;` near `_focusedScreenRect` (~:339). - Add `function notifyObjectUpdated(objectId)` (after focusFrame/_applyWebframePresentationVisibility, before the return ~:411): - `if (!presentationMode) return;` ; `if (!frames.length) return;` - Resolve focused frame WITHOUT `getFrames()` (use the in-memory array like the resize observer ~:121): `var focused = frames[currentFrameIndex]; if (!focused) return;` - Id match (handles temp→server remap since `group.id()` is updated in place by `WhiteboardObjects.remapId`): `var fid = String(focused.id()); if (String(objectId) !== fid) return;` - Coalesce per rAF reusing the `syncDrawScheduled` pattern (sync.js:893-899): if `_refocusRafScheduled` return; set it; `requestAnimationFrame(function(){ _refocusRafScheduled=false; if(!presentationMode||!frames.length) return; var f=frames[currentFrameIndex]; if(!f) return; focusFrame(f); _emit(); });` - Do NOT wrap in `_applyingSync` — that guard (frames.js:326-333) exists only to suppress `_broadcastSlide` in applyRemoteSlide; this path never calls `_broadcastSlide`, and `_emit` MUST run so the host repositions the spotlight. - Export `notifyObjectUpdated` (and `notifyFrameRemoved` from Step 5) in the module return. Dependencies: none #### Step 2: Call from the remote sync path Files: `crates/hero_whiteboard_admin/static/web/js/whiteboard/sync.js` - In `applySyncUpdate`, at the end of the `type === 'frame'` branch (~:735-748; generic x/y/size/rotation already applied earlier ~:614-649), add: `if (typeof WhiteboardFrames !== 'undefined' && WhiteboardFrames.notifyObjectUpdated) { WhiteboardFrames.notifyObjectUpdated(strId); }` Precedent: the sibling-module call `WhiteboardConnectors.reAnchorByObject(strId)` ~sync.js:888-890 (mirror guard shape; keep it inside the frame branch so it doesn't run for every type). The branch already runs under `_applyingSync = true` (~:595) so no echo possible. Dependencies: Step 1 #### Step 3: Local presenter drag path Files: `crates/hero_whiteboard_admin/static/web/js/whiteboard/objects.js` - Frame group `dragend` handler (~:1198-1210): after `WhiteboardSync.onUpdate(group)` / children loop, before `capturedChildren = []`, add `if (typeof WhiteboardFrames !== 'undefined' && WhiteboardFrames.notifyObjectUpdated) { WhiteboardFrames.notifyObjectUpdated(group.id()); }`. Hook self-guards on presentationMode + focused-id so it's inert during normal editing. (Originator skips its own broadcast via `msg.sender === localUserId` sync.js:152, so the remote hook does NOT cover the local presenter.) Dependencies: Step 1 #### Step 4: Local presenter resize/rotate path Files: `crates/hero_whiteboard_admin/static/web/js/whiteboard/tools.js` - Shared `transformer.on('transformend', ...)` (~:270-287): inside the `nodes.forEach`, after `WhiteboardHistory.commitUpdate(node.id());` (~:286), add `if (typeof WhiteboardFrames !== 'undefined' && WhiteboardFrames.notifyObjectUpdated) { WhiteboardFrames.notifyObjectUpdated(node.id()); }`. focusFrame re-fits size+rotation via getClientRect. Dependencies: Step 1. Steps 2/3/4 parallelizable after Step 1. #### Step 5: Graceful focused-frame-deleted handling Files: `crates/hero_whiteboard_admin/static/web/js/whiteboard/frames.js`, `sync.js` - Add `notifyFrameRemoved()` to WhiteboardFrames: if `presentationMode`, `getFrames()` (rebuild from live `.find('.frame')`); if `frames.length === 0` → `showNoFramesNotice()` + `_focusedScreenRect = null; _emit();`; else clamp `currentFrameIndex = Math.max(0, Math.min(currentFrameIndex, frames.length - 1)); focusFrame(frames[currentFrameIndex]); _emit();` (clamp pattern from applyRemoteSlide ~:324). - Wire from `sync.js` `object.deleted` frame branch (~:167-188) after `obj.group.destroy()`/`delete allObj[delId]`: `if (obj.type === 'frame' && WhiteboardFrames.notifyFrameRemoved) WhiteboardFrames.notifyFrameRemoved();` - Defensive: in the notifyObjectUpdated rAF body, short-circuit if `focused.getStage && !focused.getStage()` (destroyed node) — focusFrame already returns early when `.bg` is missing. Dependencies: Step 1 #### Step 6: Rebuild + verify Files: `crates/hero_whiteboard_admin/src/assets.rs` - `touch` it; `cargo build --release -p hero_whiteboard_admin`; restart; confirm served `frames.js` contains `notifyObjectUpdated` before testing (rust-embed embeds at compile time). Dependencies: Steps 1-5 ### Acceptance Criteria - [ ] Remote editor drags the focused frame → every audience view re-fits + spotlight follows, no reload/slide-change. - [ ] Remote editor resizes the focused frame → audience re-fits new size (zoom adjusts), spotlight resizes. - [ ] Remote editor rotates the focused frame → audience re-fits rotated AABB, spotlight rotation updates. - [ ] Updating a non-focused object/frame does NOT move the presentation view on any client. - [ ] Local presenter drag/resize/rotate of the focused frame re-fits the presenter's own view + spotlight. - [ ] Rapid drag → smooth single re-fit per frame (no thrash). - [ ] Deleting the focused frame mid-presentation clamps to a valid slide or shows the no-frames notice — never blank. - [ ] No echo/feedback loop; presenter's own broadcast not re-applied to itself. - [ ] No server/RPC/schema/HTML-structural change; JS-only + assets.rs touch. ### Notes - Hook: `WhiteboardFrames.notifyObjectUpdated(objectId)` + `notifyFrameRemoved()`. Called from applySyncUpdate frame branch; precedent `WhiteboardConnectors.reAnchorByObject(strId)` ~sync.js:888-890. - Do NOT add `_applyingSync` wrap: it only suppresses `_broadcastSlide` (not called here); `_emit` must run so the host spotlight tracks. sync.js-side `_applyingSync=true` (~:595) already blankets the remote path (onUpdate guard ~:458). - Coalesce via the `syncDrawScheduled` rAF one-shot (sync.js:893-899) with a dedicated `_refocusRafScheduled`; resolve the focused frame fresh inside the rAF so last drag wins. - Local presenter NOT covered by the sync hook (self-broadcast skipped, sync.js:152) — covered by objects.js dragend + tools.js transformend, same self-guarding hook (inert outside presentation). - Spotlight: focusFrame sets `_focusedScreenRect` (~:369-375); `_emit` passes it as `state.screenRect` to the host subscriber (board.html positionSpotlight ~:567-625 / board_view.html ~:188-208), refreshing `#pres-spotlight` incl. rotation. - Deploy: rust-embed compile-time embed — after edits `touch crates/hero_whiteboard_admin/src/assets.rs`, `cargo build --release -p hero_whiteboard_admin`, restart, verify served frames.js has the new export before testing.
Author
Member

Test Results

  • Total: 0
  • Passed: 0
  • Failed: 0

cargo test --workspace --lib: ok (0 passed; 0 failed; 0 ignored across all crates) - no Rust regression
node --check (frames, sync, objects, tools): ok (all 4 files pass syntax check)

Note: #199 is a JS-only change (re-focus the presentation view when the focused frame's geometry changes). No JS unit harness exists in this repo; the Rust suite is the regression gate and the audience re-focus / spotlight tracking is verified manually in-browser.

## Test Results - Total: 0 - Passed: 0 - Failed: 0 cargo test --workspace --lib: ok (0 passed; 0 failed; 0 ignored across all crates) - no Rust regression node --check (frames, sync, objects, tools): ok (all 4 files pass syntax check) Note: #199 is a JS-only change (re-focus the presentation view when the focused frame's geometry changes). No JS unit harness exists in this repo; the Rust suite is the regression gate and the audience re-focus / spotlight tracking is verified manually in-browser.
Author
Member

Implementation Summary

JS-only. A new self-guarding WhiteboardFrames hook re-fits the presentation view (and the spotlight) when the currently focused frame's geometry changes, so the audience no longer loses the frame until reload. No server/schema change.

Changes

frames.js

  • Added notifyObjectUpdated(objectId): no-op unless presentation mode is active and objectId is the currently focused frame; coalesces via a one-shot requestAnimationFrame, then re-runs focusFrame(focused) + _emit() so the stage re-fits and the host spotlight repositions. Not wrapped in _applyingSync (that guard only suppresses slide rebroadcast, which this path never does; _emit must run so the spotlight tracks). Skips a destroyed node (left to notifyFrameRemoved).
  • Added notifyFrameRemoved(): when presenting, rebuilds the live frame list; if none, shows the no-frames notice and clears the spotlight rect; otherwise clamps currentFrameIndex and re-fits the nearest valid frame.
  • Both exported on the module.

sync.js

  • applySyncUpdate frame branch: after the remote frame update is applied, calls WhiteboardFrames.notifyObjectUpdated(strId) (mirrors the existing WhiteboardConnectors.reAnchorByObject precedent; the branch runs under _applyingSync so no echo).
  • object.deleted frame path: after the node is destroyed/removed, calls WhiteboardFrames.notifyFrameRemoved() for frame deletions only.

objects.js

  • Frame group dragend commit: calls WhiteboardFrames.notifyObjectUpdated(group.id()) so a local presenter dragging the focused frame re-fits their own view (the originator does not receive its own sync broadcast).

tools.js

  • Shared transformend per-node commit: calls WhiteboardFrames.notifyObjectUpdated(node.id()) after commitUpdate, covering local presenter resize/rotate of the focused frame.

The hook self-guards, so all four call sites are inert during normal (non-presenting) editing and for non-focused objects.

Behavior after change

  • Remote editor moves/resizes/rotates the focused frame during a presentation: every audience client re-fits to the new geometry and the spotlight follows, with no reload and no slide change.
  • Local presenter dragging/resizing/rotating the focused frame re-fits their own view + spotlight.
  • Updates to non-focused objects do not move the presentation view.
  • Rapid drags are coalesced to one re-fit per animation frame.
  • Deleting the focused frame mid-presentation clamps to a valid adjacent slide, or shows the no-frames notice — never a blank canvas.
  • No sync echo / slide rebroadcast loop.

Tests

  • cargo test --workspace --lib: green, no Rust regression (JS-only; no JS unit harness in repo).
  • node --check on frames.js, sync.js, objects.js, tools.js: ok.
  • Audience re-focus and spotlight tracking verified manually in-browser after a forced-embed rebuild and redeploy.
## Implementation Summary JS-only. A new self-guarding WhiteboardFrames hook re-fits the presentation view (and the spotlight) when the currently focused frame's geometry changes, so the audience no longer loses the frame until reload. No server/schema change. ### Changes frames.js - Added notifyObjectUpdated(objectId): no-op unless presentation mode is active and objectId is the currently focused frame; coalesces via a one-shot requestAnimationFrame, then re-runs focusFrame(focused) + _emit() so the stage re-fits and the host spotlight repositions. Not wrapped in _applyingSync (that guard only suppresses slide rebroadcast, which this path never does; _emit must run so the spotlight tracks). Skips a destroyed node (left to notifyFrameRemoved). - Added notifyFrameRemoved(): when presenting, rebuilds the live frame list; if none, shows the no-frames notice and clears the spotlight rect; otherwise clamps currentFrameIndex and re-fits the nearest valid frame. - Both exported on the module. sync.js - applySyncUpdate frame branch: after the remote frame update is applied, calls WhiteboardFrames.notifyObjectUpdated(strId) (mirrors the existing WhiteboardConnectors.reAnchorByObject precedent; the branch runs under _applyingSync so no echo). - object.deleted frame path: after the node is destroyed/removed, calls WhiteboardFrames.notifyFrameRemoved() for frame deletions only. objects.js - Frame group dragend commit: calls WhiteboardFrames.notifyObjectUpdated(group.id()) so a local presenter dragging the focused frame re-fits their own view (the originator does not receive its own sync broadcast). tools.js - Shared transformend per-node commit: calls WhiteboardFrames.notifyObjectUpdated(node.id()) after commitUpdate, covering local presenter resize/rotate of the focused frame. The hook self-guards, so all four call sites are inert during normal (non-presenting) editing and for non-focused objects. ### Behavior after change - Remote editor moves/resizes/rotates the focused frame during a presentation: every audience client re-fits to the new geometry and the spotlight follows, with no reload and no slide change. - Local presenter dragging/resizing/rotating the focused frame re-fits their own view + spotlight. - Updates to non-focused objects do not move the presentation view. - Rapid drags are coalesced to one re-fit per animation frame. - Deleting the focused frame mid-presentation clamps to a valid adjacent slide, or shows the no-frames notice — never a blank canvas. - No sync echo / slide rebroadcast loop. ### Tests - cargo test --workspace --lib: green, no Rust regression (JS-only; no JS unit harness in repo). - node --check on frames.js, sync.js, objects.js, tools.js: ok. - Audience re-focus and spotlight tracking verified manually in-browser after a forced-embed rebuild and redeploy.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
lhumina_code/hero_whiteboard#199
No description provided.