Bring to Front / Send to Back z-order changes don't sync to other open windows on the same board #208

Closed
opened 2026-05-20 13:28:03 +00:00 by AhmedHanafy725 · 2 comments

Summary

When one user reorders an object on the canvas with Bring to Front or Send to Back (from the right-click context menu or the floating toolbar), the change is visible locally but the other browser windows / tabs viewing the same board don't update their stacking. Refreshing the other window picks up the new order (so the server persists it), but live sync misses the update.

The same goes for the Move Forward / Move Backward stepwise variants in the toolbar (_commitZOrder covers all four).

Reproduction

  1. Open the same board in two windows / tabs as two collaborators (or in one window in two browsers).
  2. In window A: select an object, click Bring to Front (or right-click → Bring to Front).
  3. Observe window B — the object's visible stacking does not change.
  4. Refresh window B — the new order appears.

Same behaviour with Send to Back, Move Forward, and Move Backward.

Where things look correct on paper

  • Local handlers in both paths call WhiteboardSync.onUpdate(node):
    • crates/hero_whiteboard_admin/static/web/js/whiteboard/tools.js _commitZOrder (line ~1004) after moveToTop/moveToBottom/moveUp/moveDown.
    • crates/hero_whiteboard_admin/static/web/js/whiteboard/contextmenu.js bringToFront/sendToBack (lines ~216/~230) after moveToTop/moveToBottom.
  • crates/hero_whiteboard_admin/static/web/js/whiteboard/sync.js serializeForServer (line ~266) includes z_index: node.zIndex() || 0 (line ~405). flushUpdates sends object.update over JSON-RPC with z_index (lines ~498/511) and broadcasts object.updated over WebSocket (lines ~506/522).
  • The server's object.update handler (crates/hero_whiteboard_server/src/handlers/object.rs line ~113) writes z_index into the row.
  • The receiver path applySyncUpdate (sync.js ~line 587) updates node.zIndex(obj.z_index) (lines ~655-657) and schedules a layer batchDraw via rAF (line ~899-904).

Likely culprits to investigate during the spec phase

  • Whether the remote ever reaches applySyncUpdate for these object.updated messages — check the dispatcher around sync.js:160 and any guard like "skip if sender is me" that might match.
  • Whether the broadcast-seq / markBroadcast machinery (H8) is filtering the update on either side.
  • Whether node.zIndex(N) is being called inside applySyncUpdate for the right node when the node is nested inside a parent frame group rather than a top-level child of the object layer; the index N is then relative to the frame's children, not the object layer.
  • Whether the obj.z_index !== node.zIndex() guard at sync.js:655 short-circuits when the remote's runtime zIndex coincidentally matches the broadcast value (e.g. multiple frames of children, repeated 0s).
  • Whether the issuing window debounces / coalesces the z-only update with later position-only updates that drop the z_index field somehow.
  • Whether Konva's node.zIndex(N) here actually triggers the reorder visually after the rAF batchDraw.

Scope

Make live z-order changes propagate to all open windows on the same board within the existing sync debounce / throttle window, for:

  • Right-click context menu: Bring to Front, Send to Back.
  • Floating toolbar / shortcut: Bring to Front, Send to Back, Move Forward, Move Backward.
  • Single-object and multi-object selections.

Apply the same code path uniformly — no new RPC needed (object.update already supports z_index).

Requirements

  • After a z-order change in window A, window B reflects the new stacking automatically without requiring a reload.
  • Multi-node z-order changes preserve the relative order on all peers.
  • A node inside a parent frame stacks correctly within the frame on all peers.
  • The change persists across reload (server-side z_index is already being written).
  • Webframe overlay layering (WhiteboardWebframe.refreshAllLayering) recomputes on the receiver after the remote's stacking update so the iframe overlay shows/hides as expected on the other window too.

Acceptance Criteria

  • Bring-to-front / send-to-back on a single object propagates and is visible in another window viewing the same board.
  • Move-forward / move-backward propagates the same way.
  • Multi-object z-order changes (transformer with multiple selected) propagate and preserve relative order across windows.
  • A z-order change on an object inside a frame propagates correctly within the frame on the remote.
  • After the remote applies the z update, the webframe overlay re-evaluates its hide-when-occluded layering.
  • No new RPC methods or server endpoints introduced.

Notes

The whiteboard frontend is vanilla JS modules under crates/hero_whiteboard_admin/static/web/js/whiteboard/ embedded via rust-embed; rebuild the admin crate to pick up JS changes. Server-side z_index persistence already works (the SQL UPDATE in crates/hero_whiteboard_server/src/db/queries.rs includes z_index). The fix is most likely a small change in the sync send or receive path.

## Summary When one user reorders an object on the canvas with **Bring to Front** or **Send to Back** (from the right-click context menu or the floating toolbar), the change is visible locally but the other browser windows / tabs viewing the same board don't update their stacking. Refreshing the other window picks up the new order (so the server persists it), but live sync misses the update. The same goes for the **Move Forward** / **Move Backward** stepwise variants in the toolbar (`_commitZOrder` covers all four). ## Reproduction 1. Open the same board in two windows / tabs as two collaborators (or in one window in two browsers). 2. In window A: select an object, click Bring to Front (or right-click → Bring to Front). 3. Observe window B — the object's visible stacking does not change. 4. Refresh window B — the new order appears. Same behaviour with Send to Back, Move Forward, and Move Backward. ## Where things look correct on paper - Local handlers in both paths call `WhiteboardSync.onUpdate(node)`: - `crates/hero_whiteboard_admin/static/web/js/whiteboard/tools.js` `_commitZOrder` (line ~1004) after `moveToTop`/`moveToBottom`/`moveUp`/`moveDown`. - `crates/hero_whiteboard_admin/static/web/js/whiteboard/contextmenu.js` `bringToFront`/`sendToBack` (lines ~216/~230) after `moveToTop`/`moveToBottom`. - `crates/hero_whiteboard_admin/static/web/js/whiteboard/sync.js` `serializeForServer` (line ~266) includes `z_index: node.zIndex() || 0` (line ~405). `flushUpdates` sends `object.update` over JSON-RPC with `z_index` (lines ~498/511) and broadcasts `object.updated` over WebSocket (lines ~506/522). - The server's `object.update` handler (`crates/hero_whiteboard_server/src/handlers/object.rs` line ~113) writes `z_index` into the row. - The receiver path `applySyncUpdate` (sync.js ~line 587) updates `node.zIndex(obj.z_index)` (lines ~655-657) and schedules a layer `batchDraw` via rAF (line ~899-904). ## Likely culprits to investigate during the spec phase - Whether the remote ever reaches `applySyncUpdate` for these `object.updated` messages — check the dispatcher around `sync.js:160` and any guard like "skip if sender is me" that might match. - Whether the broadcast-seq / `markBroadcast` machinery (H8) is filtering the update on either side. - Whether `node.zIndex(N)` is being called inside `applySyncUpdate` for the right node when the node is nested inside a parent frame group rather than a top-level child of the object layer; the index `N` is then relative to the frame's children, not the object layer. - Whether the `obj.z_index !== node.zIndex()` guard at sync.js:655 short-circuits when the remote's runtime zIndex coincidentally matches the broadcast value (e.g. multiple frames of children, repeated 0s). - Whether the issuing window debounces / coalesces the z-only update with later position-only updates that drop the `z_index` field somehow. - Whether Konva's `node.zIndex(N)` here actually triggers the reorder visually after the rAF `batchDraw`. ## Scope Make live z-order changes propagate to all open windows on the same board within the existing sync debounce / throttle window, for: - Right-click context menu: Bring to Front, Send to Back. - Floating toolbar / shortcut: Bring to Front, Send to Back, Move Forward, Move Backward. - Single-object and multi-object selections. Apply the same code path uniformly — no new RPC needed (object.update already supports `z_index`). ## Requirements - After a z-order change in window A, window B reflects the new stacking automatically without requiring a reload. - Multi-node z-order changes preserve the relative order on all peers. - A node inside a parent frame stacks correctly within the frame on all peers. - The change persists across reload (server-side z_index is already being written). - Webframe overlay layering (`WhiteboardWebframe.refreshAllLayering`) recomputes on the receiver after the remote's stacking update so the iframe overlay shows/hides as expected on the other window too. ## Acceptance Criteria - Bring-to-front / send-to-back on a single object propagates and is visible in another window viewing the same board. - Move-forward / move-backward propagates the same way. - Multi-object z-order changes (transformer with multiple selected) propagate and preserve relative order across windows. - A z-order change on an object inside a frame propagates correctly within the frame on the remote. - After the remote applies the z update, the webframe overlay re-evaluates its hide-when-occluded layering. - No new RPC methods or server endpoints introduced. ## Notes The whiteboard frontend is vanilla JS modules under `crates/hero_whiteboard_admin/static/web/js/whiteboard/` embedded via rust-embed; rebuild the admin crate to pick up JS changes. Server-side z_index persistence already works (the SQL UPDATE in `crates/hero_whiteboard_server/src/db/queries.rs` includes `z_index`). The fix is most likely a small change in the sync send or receive path.
Author
Owner

Implementation Spec for Issue #208

Objective

Make Bring to Front / Send to Back / Move Forward / Move Backward propagate the new stacking order to every other open window on the same board in real time, instead of only after a reload. Fix must work in both single-node (context menu) and multi-node (selection toolbar) paths.

Root cause

The Konva Transformer lives on the same layer as real object groups (tools.js:112 adds it as a sibling), and both bringToFront paths move the transformer above the moved object (tools.js:1018, contextmenu.js:222). serializeForServer at sync.js:405 then uses the raw layer-child index: z_index: node.zIndex() || 0.

Concrete trace: board with three objects + transformer, layer children [T, A, B, C]. Sender does bringToFront(A):

  • A.moveToTop()[T, B, C, A], A.zIndex() == 3.
  • transformer.moveToTop()[B, C, A, T], A.zIndex() == 2.
  • Broadcast z_index: 2 for A. Receiver layer is still [T, A, B, C]. applySyncUpdate (sync.js:655-657) calls node.zIndex(2)[T, B, A, C]. C still above A — the broadcast value is meaningless across windows because each window's transformer occupies a different slot.

Additionally, only the moved node's z_index is persisted; displaced objects keep stale DB values, so reload (ORDER BY z_index ASC in queries.rs:366) returns inconsistent order when ranks collide.

Other parts of the pipeline are healthy: server-side object.update writes z_index (object.rs:113); WS relay forwards JSON verbatim; handleWsMessage doesn't filter peers; H8/markBroadcast doesn't block remote application.

Requirements

  1. Bring to Front / Send to Back / Move Forward / Move Backward by one window updates stacking in every other open window for the same board within the existing ~500 ms debounce.
  2. No regression to geometry/style sync, undo/redo, or the H2 dirty-during-sync reconcile.
  3. Persisted z_index remains monotonic per board so list_objects returns objects in the intended order after reload.
  4. No server schema or RPC contract changes.

Files to Modify/Create

  • crates/hero_whiteboard_admin/static/web/js/whiteboard/sync.js — change wire z_index semantics from "layer-child index" to "rank among tracked objects" on both serialize and apply.
  • crates/hero_whiteboard_admin/static/web/js/whiteboard/contextmenu.js — after bringToFront/sendToBack, fan out onUpdate to all tracked objects (debounce+batch coalesces).
  • crates/hero_whiteboard_admin/static/web/js/whiteboard/tools.js — same fan-out at the end of _commitZOrder for the multi-node toolbar path.

No new files.

Implementation Plan

Step 1: Rank helpers in sync.js

Files: sync.js

  • Add objectRankOf(node): walks node.getParent().children, returns the count of children with zIndex < node.zIndex() that are present in WhiteboardObjects.getAllObjects() (skips Transformer, selectionRect, drawing preview — none are in that map).
  • Add setObjectRank(node, rank): iterate parent.children, count tracked-object siblings excluding node; choose the absolute child index that puts node at the requested rank among tracked objects; call node.zIndex(absoluteIndex). Clamp rank to [0, trackedCount-1].
    Dependencies: none

Step 2: Serialize uses rank (depends on Step 1)

Files: sync.js

  • Replace sync.js:405 z_index: node.zIndex() || 0, with z_index: objectRankOf(node),. This affects all outbound writes (onCreate, flushUpdates single + batch, persistNow, saveAll).

Step 3: Apply uses rank (depends on Step 1; parallelizable with Step 2)

Files: sync.js

  • Replace sync.js:655-657:
    if (obj.z_index != null && obj.z_index !== node.zIndex()) {
        node.zIndex(obj.z_index);
    }
    
    with:
    if (obj.z_index != null) {
        setObjectRank(node, obj.z_index);
    }
    
  • Drop the !== node.zIndex() short-circuit (apples to oranges — setObjectRank is idempotent so an unnecessary call is a no-op).

Step 4: Fan-out in contextmenu.js (depends on Steps 2+3; parallelizable with Step 5)

Files: contextmenu.js

  • In bringToFront (line ~216) and sendToBack (line ~230), after the local moveToTop()/moveToBottom() + batchDraw(), replace the single WhiteboardSync.onUpdate(targetNode) call with a loop:
    var all = WhiteboardObjects.getAllObjects();
    Object.keys(all).forEach(function(id) {
        var e = all[id];
        if (e && e.group) WhiteboardSync.onUpdate(e.group);
    });
    
  • The 500ms debounce + object.batch_update coalesce this into one round-trip; serializeForServer for unchanged nodes emits identical geometry so it's idempotent.

Step 5: Fan-out in tools.js (depends on Steps 2+3; parallelizable with Step 4)

Files: tools.js

  • In _commitZOrder (line ~1004), after the existing per-node WhiteboardSync.onUpdate(n) loop, append the same fan-out so displaced (unmoved) tracked objects also persist their new ranks.

Step 6: Build + manual verify

  • touch crates/hero_whiteboard_admin/src/assets.rs && cargo build --release -p hero_whiteboard_admin.
  • Two tabs on the same board: bring-to-front / send-to-back / move-forward / move-backward in tab A reflects in tab B within ~500ms without reload; reload either tab → same stacking.

Acceptance Criteria

  • Tab A "Bring to Front" on an object moves it above every other object in tab B within ~500ms, no reload.
  • Works with tab B having a different object selected (its transformer in a different slot).
  • Send to Back / Move Forward / Move Backward propagate the same way.
  • Multi-node Bring to Front / Send to Back from the selection toolbar propagates correctly.
  • Reload after a z-order op shows the same stacking (DB z_index ranks are now monotonic).
  • No regression in geometry/style sync, undo/redo (single batch step still), H2 mid-sync reconcile.

Notes

  • Pure client-side change in static/web/js/whiteboard/. No Rust changes.
  • Wire/DB z_index semantics change from "layer-child index" to "rank among tracked objects". list_objects already orders ASC, so existing rows continue to load in correct relative order.
  • The fan-out is the minimal correct approach: every same-id update is coalesced by wsSendThrottled and the RPC layer batches into one object.batch_update per z-order action.
  • Alternative considered and rejected: move the transformer to its own layer. Structural fix but much bigger blast radius (selection rect, drawing preview, anchor hit padding, listening flags) — higher regression risk.
## Implementation Spec for Issue #208 ### Objective Make Bring to Front / Send to Back / Move Forward / Move Backward propagate the new stacking order to every other open window on the same board in real time, instead of only after a reload. Fix must work in both single-node (context menu) and multi-node (selection toolbar) paths. ### Root cause The Konva Transformer lives on the **same layer** as real object groups (`tools.js:112` adds it as a sibling), and both bringToFront paths move the transformer above the moved object (`tools.js:1018`, `contextmenu.js:222`). `serializeForServer` at `sync.js:405` then uses the raw layer-child index: `z_index: node.zIndex() || 0`. Concrete trace: board with three objects + transformer, layer children `[T, A, B, C]`. Sender does `bringToFront(A)`: - `A.moveToTop()` → `[T, B, C, A]`, `A.zIndex() == 3`. - `transformer.moveToTop()` → `[B, C, A, T]`, `A.zIndex() == 2`. - Broadcast `z_index: 2` for A. Receiver layer is still `[T, A, B, C]`. `applySyncUpdate` (`sync.js:655-657`) calls `node.zIndex(2)` → `[T, B, A, C]`. **C still above A** — the broadcast value is meaningless across windows because each window's transformer occupies a different slot. Additionally, only the moved node's `z_index` is persisted; displaced objects keep stale DB values, so reload (`ORDER BY z_index ASC` in `queries.rs:366`) returns inconsistent order when ranks collide. Other parts of the pipeline are healthy: server-side `object.update` writes `z_index` (`object.rs:113`); WS relay forwards JSON verbatim; `handleWsMessage` doesn't filter peers; H8/markBroadcast doesn't block remote application. ### Requirements 1. Bring to Front / Send to Back / Move Forward / Move Backward by one window updates stacking in every other open window for the same board within the existing ~500 ms debounce. 2. No regression to geometry/style sync, undo/redo, or the H2 dirty-during-sync reconcile. 3. Persisted `z_index` remains monotonic per board so `list_objects` returns objects in the intended order after reload. 4. No server schema or RPC contract changes. ### Files to Modify/Create - `crates/hero_whiteboard_admin/static/web/js/whiteboard/sync.js` — change wire `z_index` semantics from "layer-child index" to "rank among tracked objects" on both serialize and apply. - `crates/hero_whiteboard_admin/static/web/js/whiteboard/contextmenu.js` — after bringToFront/sendToBack, fan out `onUpdate` to all tracked objects (debounce+batch coalesces). - `crates/hero_whiteboard_admin/static/web/js/whiteboard/tools.js` — same fan-out at the end of `_commitZOrder` for the multi-node toolbar path. No new files. ### Implementation Plan #### Step 1: Rank helpers in sync.js Files: `sync.js` - Add `objectRankOf(node)`: walks `node.getParent().children`, returns the count of children with `zIndex < node.zIndex()` that are present in `WhiteboardObjects.getAllObjects()` (skips Transformer, selectionRect, drawing preview — none are in that map). - Add `setObjectRank(node, rank)`: iterate `parent.children`, count tracked-object siblings excluding `node`; choose the absolute child index that puts `node` at the requested rank among tracked objects; call `node.zIndex(absoluteIndex)`. Clamp rank to `[0, trackedCount-1]`. Dependencies: none #### Step 2: Serialize uses rank (depends on Step 1) Files: `sync.js` - Replace `sync.js:405` `z_index: node.zIndex() || 0,` with `z_index: objectRankOf(node),`. This affects all outbound writes (`onCreate`, `flushUpdates` single + batch, `persistNow`, `saveAll`). #### Step 3: Apply uses rank (depends on Step 1; parallelizable with Step 2) Files: `sync.js` - Replace `sync.js:655-657`: ```js if (obj.z_index != null && obj.z_index !== node.zIndex()) { node.zIndex(obj.z_index); } ``` with: ```js if (obj.z_index != null) { setObjectRank(node, obj.z_index); } ``` - Drop the `!== node.zIndex()` short-circuit (apples to oranges — `setObjectRank` is idempotent so an unnecessary call is a no-op). #### Step 4: Fan-out in contextmenu.js (depends on Steps 2+3; parallelizable with Step 5) Files: `contextmenu.js` - In `bringToFront` (line ~216) and `sendToBack` (line ~230), after the local `moveToTop()`/`moveToBottom()` + `batchDraw()`, replace the single `WhiteboardSync.onUpdate(targetNode)` call with a loop: ```js var all = WhiteboardObjects.getAllObjects(); Object.keys(all).forEach(function(id) { var e = all[id]; if (e && e.group) WhiteboardSync.onUpdate(e.group); }); ``` - The 500ms debounce + `object.batch_update` coalesce this into one round-trip; serializeForServer for unchanged nodes emits identical geometry so it's idempotent. #### Step 5: Fan-out in tools.js (depends on Steps 2+3; parallelizable with Step 4) Files: `tools.js` - In `_commitZOrder` (line ~1004), after the existing per-node `WhiteboardSync.onUpdate(n)` loop, append the same fan-out so displaced (unmoved) tracked objects also persist their new ranks. #### Step 6: Build + manual verify - `touch crates/hero_whiteboard_admin/src/assets.rs && cargo build --release -p hero_whiteboard_admin`. - Two tabs on the same board: bring-to-front / send-to-back / move-forward / move-backward in tab A reflects in tab B within ~500ms without reload; reload either tab → same stacking. ### Acceptance Criteria - [ ] Tab A "Bring to Front" on an object moves it above every other object in tab B within ~500ms, no reload. - [ ] Works with tab B having a different object selected (its transformer in a different slot). - [ ] Send to Back / Move Forward / Move Backward propagate the same way. - [ ] Multi-node Bring to Front / Send to Back from the selection toolbar propagates correctly. - [ ] Reload after a z-order op shows the same stacking (DB `z_index` ranks are now monotonic). - [ ] No regression in geometry/style sync, undo/redo (single batch step still), H2 mid-sync reconcile. ### Notes - Pure client-side change in `static/web/js/whiteboard/`. No Rust changes. - Wire/DB `z_index` semantics change from "layer-child index" to "rank among tracked objects". `list_objects` already orders ASC, so existing rows continue to load in correct relative order. - The fan-out is the minimal correct approach: every same-id update is coalesced by `wsSendThrottled` and the RPC layer batches into one `object.batch_update` per z-order action. - Alternative considered and rejected: move the transformer to its own layer. Structural fix but much bigger blast radius (selection rect, drawing preview, anchor hit padding, listening flags) — higher regression risk.
Author
Owner

Implementation Summary

Live z-order changes (Bring to Front / Send to Back / Move Forward / Move Backward) now propagate to other open windows on the same board.

Root cause

The Konva Transformer is a sibling of real objects on the object layer. serializeForServer used the raw Konva child index (node.zIndex() || 0), which mixed the transformer's slot into the value. Two windows whose transformers sit in different slots therefore disagreed on what an index meant. On top of that, only the moved node's z_index was persisted; displaced objects kept stale DB values, so reload order could collide.

Changes

  • crates/hero_whiteboard_admin/static/web/js/whiteboard/sync.js:
    • Added objectRankOf(node) — rank of a node among its tracked-object siblings (the Transformer, marquee selection rect, and drawing-preview lines are not in WhiteboardObjects.getAllObjects() and are skipped).
    • Added setObjectRank(node, rank) — inverse: moves a node so it becomes the rank-th tracked-object sibling, clamp-safe, idempotent.
    • Serialize now sends z_index: objectRankOf(node); apply now calls setObjectRank(node, obj.z_index).
  • crates/hero_whiteboard_admin/static/web/js/whiteboard/contextmenu.js: after bringToFront/sendToBack do their local moveToTop/moveToBottom, the onUpdate call fans out to every tracked object so all displaced ranks persist (the existing 500ms debounce + object.batch_update coalesces them into one round-trip).
  • crates/hero_whiteboard_admin/static/web/js/whiteboard/tools.js: same fan-out in _commitZOrder (covers the multi-node toolbar path: bringToFront, sendToBack, moveForward, moveBackward).

Behavior

  • After a z-order action in window A, window B reflects the new stacking within ~500ms without reload.
  • Works in single-node and multi-node code paths, with or without a different selection in window B (its transformer position no longer matters).
  • Persisted z_index ranks remain monotonic per board; reload renders the same order.
  • Snapshot/commit history is still scoped to the moved node, so undo/redo of a z-order action remains a single user-visible step.

Test results

  • cargo test --workspace --lib: compiled cleanly, no failures (change is JS-only; regression guard).
  • node --check passed for sync.js, contextmenu.js, tools.js. File encodings clean.
  • Rebuilt and redeployed; served sync.js contains the new objectRankOf/setObjectRank helpers and the updated serialize/apply sites.

Notes

  • The wire/DB z_index semantics change from "raw child index in the object layer" to "rank among tracked objects". list_objects already orders ASC, so existing rows continue to load in the correct relative order.
  • Pure client-side change. No Rust changes, no schema or RPC contract changes.
  • An alternative considered and rejected was moving the transformer to its own layer. Bigger blast radius (selection rect, drawing preview, anchor hit padding, listening flags). The rank-based approach is the minimal correct fix.
## Implementation Summary Live z-order changes (Bring to Front / Send to Back / Move Forward / Move Backward) now propagate to other open windows on the same board. ### Root cause The Konva Transformer is a sibling of real objects on the object layer. `serializeForServer` used the raw Konva child index (`node.zIndex() || 0`), which mixed the transformer's slot into the value. Two windows whose transformers sit in different slots therefore disagreed on what an index meant. On top of that, only the moved node's `z_index` was persisted; displaced objects kept stale DB values, so reload order could collide. ### Changes - `crates/hero_whiteboard_admin/static/web/js/whiteboard/sync.js`: - Added `objectRankOf(node)` — rank of a node among its tracked-object siblings (the Transformer, marquee selection rect, and drawing-preview lines are not in `WhiteboardObjects.getAllObjects()` and are skipped). - Added `setObjectRank(node, rank)` — inverse: moves a node so it becomes the rank-th tracked-object sibling, clamp-safe, idempotent. - Serialize now sends `z_index: objectRankOf(node)`; apply now calls `setObjectRank(node, obj.z_index)`. - `crates/hero_whiteboard_admin/static/web/js/whiteboard/contextmenu.js`: after `bringToFront`/`sendToBack` do their local `moveToTop`/`moveToBottom`, the `onUpdate` call fans out to every tracked object so all displaced ranks persist (the existing 500ms debounce + `object.batch_update` coalesces them into one round-trip). - `crates/hero_whiteboard_admin/static/web/js/whiteboard/tools.js`: same fan-out in `_commitZOrder` (covers the multi-node toolbar path: bringToFront, sendToBack, moveForward, moveBackward). ### Behavior - After a z-order action in window A, window B reflects the new stacking within ~500ms without reload. - Works in single-node and multi-node code paths, with or without a different selection in window B (its transformer position no longer matters). - Persisted `z_index` ranks remain monotonic per board; reload renders the same order. - Snapshot/commit history is still scoped to the moved node, so undo/redo of a z-order action remains a single user-visible step. ### Test results - `cargo test --workspace --lib`: compiled cleanly, no failures (change is JS-only; regression guard). - `node --check` passed for `sync.js`, `contextmenu.js`, `tools.js`. File encodings clean. - Rebuilt and redeployed; served `sync.js` contains the new `objectRankOf`/`setObjectRank` helpers and the updated serialize/apply sites. ### Notes - The wire/DB `z_index` semantics change from "raw child index in the object layer" to "rank among tracked objects". `list_objects` already orders ASC, so existing rows continue to load in the correct relative order. - Pure client-side change. No Rust changes, no schema or RPC contract changes. - An alternative considered and rejected was moving the transformer to its own layer. Bigger blast radius (selection rect, drawing preview, anchor hit padding, listening flags). The rank-based approach is the minimal correct fix.
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#208
No description provided.