Connector updates do not propagate to other open viewers — line style and endpoint-follow-on-move are not synced live #92

Open
opened 2026-04-28 12:49:00 +00:00 by eslamnawara · 3 comments
Member

Summary

Two related issues when the same board is open in two windows:

  1. Line style not synced. Changing a connector's Line Style (e.g. Straight → Curved / Elbow) in tab A does not update the connector in tab B. Tab B keeps rendering the previous line style until reload.
  2. Endpoint does not follow connected object on move. Moving an object that is attached to a connector (e.g. dragging the rectangle that the connector terminates at) repositions the object in both tabs, but the connector's endpoint only follows the object in tab A. In tab B, the connector stays anchored to the object's old position — its arrow tip ends up dangling in empty space, disconnected from where the object actually is.

Both changes reflects as soon as tab B is manually reloaded.

Steps to reproduce

  1. Open a board in tab A.
  2. Open the same board in tab B via the shared link.
  3. Confirm both tabs are connected (cursor presence visible).
  4. In tab A, draw two shapes and a connector between them.
  5. In tab A, select the connector and change Line Style in the
    Properties panel (e.g. set it to Curved).
  6. Then, in tab A, drag the rectangle to a new position on the canvas.

Expected

  • Tab B updates the connector's line style live to match tab A.
  • Tab B updates the connector's endpoint live so it stays attached to the rectangle as it moves, the same way tab A renders it.

Actual

  • Tab B keeps rendering the connector with the old line style indefinitely.
  • Tab B repositions the rectangle but the connector endpoint stays at the rectangle's old position, leaving the arrow disconnected from the object it was attached to.
  • A manual reload of tab B brings everything into sync.
## Summary Two related issues when the same board is open in two windows: 1. **Line style not synced.** Changing a connector's `Line Style` (e.g. Straight → Curved / Elbow) in tab A does not update the connector in tab B. Tab B keeps rendering the previous line style until reload. 2. **Endpoint does not follow connected object on move.** Moving an object that is attached to a connector (e.g. dragging the rectangle that the connector terminates at) repositions the object in both tabs, but the connector's endpoint only follows the object in tab A. In tab B, the connector stays anchored to the object's old position — its arrow tip ends up dangling in empty space, disconnected from where the object actually is. Both changes reflects as soon as tab B is manually reloaded. ## Steps to reproduce 1. Open a board in tab A. 2. Open the same board in tab B via the shared link. 3. Confirm both tabs are connected (cursor presence visible). 4. In tab A, draw two shapes and a connector between them. 5. In tab A, select the connector and change `Line Style` in the Properties panel (e.g. set it to `Curved`). 6. Then, in tab A, drag the rectangle to a new position on the canvas. ## Expected - Tab B updates the connector's line style live to match tab A. - Tab B updates the connector's endpoint live so it stays attached to the rectangle as it moves, the same way tab A renders it. ## Actual - Tab B keeps rendering the connector with the old line style indefinitely. - Tab B repositions the rectangle but the connector endpoint stays at the rectangle's old position, leaving the arrow disconnected from the object it was attached to. - A manual reload of tab B brings everything into sync.
Member

Implementation Spec — Issue #92: Connector updates must propagate live

Objective

Make connector updates propagate live across all open viewers of the same board, fixing two related sync gaps:

  1. Line-Style changes made via the Properties panel (Straight / Curved / Elbow) must update other tabs immediately.
  2. Connector endpoints must follow an attached object on the receiver side when the object is moved on the sender side, just as they already follow on the sender side via Konva dragmove.

Requirements

  • Use the existing realtime infra (the same WebSocket fan-out that already carries connector.created / connector.deleted). No new transport, no new RPC method, no new DB column.
  • Smallest possible diff. Match the prior issue #90 / #91 pattern: thread an opts._fromSync flag through the local mutation function so receivers can suppress re-broadcast and history.
  • No backward-compat shims. The new opts argument is optional and additive.

Root cause

Bug 1 — Line Style not synced.
connectors.cycleLineStyle() (called from properties.js:1160) does the local destroy + recreate, persists via rpcCall('connector.update', ...), but never broadcasts a connector.updated message over the WebSocket. sync.js#handleWsMessage also has no branch for connector.updated. Tab B therefore never learns. Same issue applies to persistStyle() (stroke color / stroke width) — it persists but doesn't broadcast.

Bug 2 — Endpoint does not follow on receiver.
connectors.createConnector() re-anchors a connector via Konva listeners: fromGroup.on('dragmove.connector_<id>', updateFn). On the sender, the user drag fires dragmove. On the receiver, sync.js#applySyncUpdate repositions the attached object with node.x(obj.x); node.y(obj.y) — programmatic setters do not fire dragmove, so the connector's updateFn never runs.

Architecturally, the smaller and less risky fix is for the receiver to recompute connector endpoints after applying a position update, instead of broadcasting redundant waypoints — the geometry is fully derived from the from/to anchor points.

Files to Modify

  • crates/hero_whiteboard_ui/static/web/js/whiteboard/connectors.js
  • crates/hero_whiteboard_ui/static/web/js/whiteboard/sync.js

Files to Leave Alone

  • properties.js (its change / input handlers already call cycleLineStyle and persistStyle — once those broadcast, properties.js gets sync for free).
  • objects.js, tools.js (the local-drag re-anchor path already works via Konva events).
  • Any RPC/server code; connector.update already exists and is called.

Step-by-Step Plan

Step 1 — connectors.js: broadcast on style/line-style change + add receiver hooks

File: crates/hero_whiteboard_ui/static/web/js/whiteboard/connectors.js

  1. Introduce a private helper broadcastUpdate(id) that builds the same payload persistStyle / cycleLineStyle already build for rpcCall('connector.update', ...), then calls ws_sendConnector({ type: 'connector.updated', data: payload }) if ws_sendConnector is set and id is not a temp id (matches the temp-id guard already used in persistStyle and the delete path).

    • Payload mirrors the connector.create payload shape that loadFromSync already understands: id, board_id, from_id, to_id, line_type, style (JSON-stringified { stroke, strokeWidth }).
  2. Accept an optional opts argument on cycleLineStyle(id, opts) and persistStyle(id, opts) and skip both the RPC call and the WS broadcast when opts && opts._fromSync is true. Pattern matches the existing deleteConnector(id, opts) guard.

  3. After the existing rpcCall('connector.update', ...) call inside cycleLineStyle, call broadcastUpdate(id). Do the same after the existing rpcCall('connector.update', ...) inside persistStyle. Place the broadcast outside the promise chain so the WS message goes out immediately (matches persistStyle's fire-and-forget style).

  4. Add a public applyUpdate(connData, opts) on the module's return object. It looks up connectors[String(connData.id)]. If found, and connData.line_type differs from the current lineStyle, call cycleLineStyle(id, { _fromSync: true }) (or the equivalent — the spec accepts looping through up to 3 cycles to land on the requested type, matching what properties.js does today). Then, if connData.style carries new stroke / strokeWidth, set them on c.arrow and update c.baseStroke / c.baseStrokeWidth, then WhiteboardCanvas.getConnectorLayer().batchDraw(). If the connector is not present locally, fall back to loadFromSync(connData) (covers the case where the receiver missed the original connector.created).

  5. Add a public reAnchorByObject(objectId) on the module's return object. It iterates over connectors and, for any connector whose fromId or toId (compared as strings) equals objectId, looks up the from/to groups in WhiteboardCanvas.getObjectLayer() and runs the same body that updateFn already runs in createConnector (recompute closestAnchor / getLinePoints, including the curved special case, write c.arrow.points(pts), then batchDraw). This is the receiver-side equivalent of the Konva dragmove listener.

Dependencies: none.

Step 2 — sync.js: route connector.updated and re-anchor connectors after object moves

File: crates/hero_whiteboard_ui/static/web/js/whiteboard/sync.js

  1. In handleWsMessage, alongside connector.created / connector.deleted, add:

    } else if (msg.type === 'connector.updated') {
        if (msg.data && typeof WhiteboardConnectors !== 'undefined' && WhiteboardConnectors.applyUpdate) {
            WhiteboardConnectors.applyUpdate(msg.data, { _fromSync: true });
        }
    }
    
  2. In applySyncUpdate, after the position write node.x(obj.x); node.y(obj.y) and after rotation/dimension updates that could move the bounding box, call WhiteboardConnectors.reAnchorByObject(strId). Place it once, near the end of applySyncUpdate just before the redraw block, so it runs after every geometry mutation. Guard with typeof WhiteboardConnectors !== 'undefined' && WhiteboardConnectors.reAnchorByObject.

Dependencies: Step 1.

Acceptance Criteria

  • Open the same board in tab A and tab B. In tab A, select a connector and change Line Style from Straight to Curved via the Properties panel. Tab B re-renders the connector as Curved within ~1 frame, no reload. Verified for Straight ↔ Curved ↔ Elbow.
  • In tab A, change a connector's stroke color and stroke width via the Properties panel. Tab B picks up both within ~1 frame.
  • In tab A, drag an object that has a connector attached at one or both endpoints. In tab B, the object moves and the connector endpoint follows the object during/after the move.
  • After a tab-B reload, connector positions and styles match tab A — live state and persisted state agree.
  • Tab A unchanged: local drag, style change, line-style cycling, undo/redo behave exactly as before (no double history entries, no echo loops, no stale temp-id paths).
  • No new console errors in either tab.
  • Connector with a temp id (RPC create still pending) is not broadcast as connector.updated (existing numCId <= 0 / isNaN guard from persistStyle is reused).

Notes

  • We deliberately do NOT broadcast connector.updated from objects.js's dragmove path. The receiver derives endpoints from anchor positions, so emitting redundant waypoints would only widen the wire payload and create races with the position update itself.
  • cycleLineStyle currently destroys and recreates the local Konva arrow. When called with _fromSync: true, the recreate path inside it must not re-call rpcCall('connector.create', ...)createConnector already guards on _fromServer, so the recursive createConnector(... { _fromServer: true, ... }) call inside cycleLineStyle is preserved. Only the outer rpcCall('connector.update', ...) and the new broadcast are skipped under _fromSync.
  • reAnchorByObject does not fire Konva events — it directly recomputes endpoints. This avoids interfering with the sender path's listeners and is why the function lives in connectors.js (it owns the connectors map).
  • The persistStyle signature change adds an optional opts only — call sites in properties.js keep working unchanged.
  • Do not touch comments / themes / cursors / presence — out of scope.
# Implementation Spec — Issue #92: Connector updates must propagate live ## Objective Make connector updates propagate live across all open viewers of the same board, fixing two related sync gaps: 1. Line-Style changes made via the Properties panel (Straight / Curved / Elbow) must update other tabs immediately. 2. Connector endpoints must follow an attached object on the receiver side when the object is moved on the sender side, just as they already follow on the sender side via Konva `dragmove`. ## Requirements - Use the existing realtime infra (the same WebSocket fan-out that already carries `connector.created` / `connector.deleted`). No new transport, no new RPC method, no new DB column. - Smallest possible diff. Match the prior issue #90 / #91 pattern: thread an `opts._fromSync` flag through the local mutation function so receivers can suppress re-broadcast and history. - No backward-compat shims. The new `opts` argument is optional and additive. ## Root cause **Bug 1 — Line Style not synced.** `connectors.cycleLineStyle()` (called from `properties.js:1160`) does the local destroy + recreate, persists via `rpcCall('connector.update', ...)`, but never broadcasts a `connector.updated` message over the WebSocket. `sync.js#handleWsMessage` also has no branch for `connector.updated`. Tab B therefore never learns. Same issue applies to `persistStyle()` (stroke color / stroke width) — it persists but doesn't broadcast. **Bug 2 — Endpoint does not follow on receiver.** `connectors.createConnector()` re-anchors a connector via Konva listeners: `fromGroup.on('dragmove.connector_<id>', updateFn)`. On the sender, the user drag fires `dragmove`. On the receiver, `sync.js#applySyncUpdate` repositions the attached object with `node.x(obj.x); node.y(obj.y)` — programmatic setters do not fire `dragmove`, so the connector's `updateFn` never runs. Architecturally, the smaller and less risky fix is for the receiver to recompute connector endpoints after applying a position update, instead of broadcasting redundant waypoints — the geometry is fully derived from the from/to anchor points. ## Files to Modify - `crates/hero_whiteboard_ui/static/web/js/whiteboard/connectors.js` - `crates/hero_whiteboard_ui/static/web/js/whiteboard/sync.js` ## Files to Leave Alone - `properties.js` (its `change` / `input` handlers already call `cycleLineStyle` and `persistStyle` — once those broadcast, properties.js gets sync for free). - `objects.js`, `tools.js` (the local-drag re-anchor path already works via Konva events). - Any RPC/server code; `connector.update` already exists and is called. ## Step-by-Step Plan ### Step 1 — `connectors.js`: broadcast on style/line-style change + add receiver hooks File: `crates/hero_whiteboard_ui/static/web/js/whiteboard/connectors.js` 1. Introduce a private helper `broadcastUpdate(id)` that builds the same payload `persistStyle` / `cycleLineStyle` already build for `rpcCall('connector.update', ...)`, then calls `ws_sendConnector({ type: 'connector.updated', data: payload })` if `ws_sendConnector` is set and `id` is not a temp id (matches the temp-id guard already used in `persistStyle` and the delete path). - Payload mirrors the `connector.create` payload shape that `loadFromSync` already understands: `id`, `board_id`, `from_id`, `to_id`, `line_type`, `style` (JSON-stringified `{ stroke, strokeWidth }`). 2. Accept an optional `opts` argument on `cycleLineStyle(id, opts)` and `persistStyle(id, opts)` and skip both the RPC call and the WS broadcast when `opts && opts._fromSync` is true. Pattern matches the existing `deleteConnector(id, opts)` guard. 3. After the existing `rpcCall('connector.update', ...)` call inside `cycleLineStyle`, call `broadcastUpdate(id)`. Do the same after the existing `rpcCall('connector.update', ...)` inside `persistStyle`. Place the broadcast outside the promise chain so the WS message goes out immediately (matches `persistStyle`'s fire-and-forget style). 4. Add a public `applyUpdate(connData, opts)` on the module's return object. It looks up `connectors[String(connData.id)]`. If found, and `connData.line_type` differs from the current `lineStyle`, call `cycleLineStyle(id, { _fromSync: true })` (or the equivalent — the spec accepts looping through up to 3 cycles to land on the requested type, matching what `properties.js` does today). Then, if `connData.style` carries new `stroke` / `strokeWidth`, set them on `c.arrow` and update `c.baseStroke` / `c.baseStrokeWidth`, then `WhiteboardCanvas.getConnectorLayer().batchDraw()`. If the connector is not present locally, fall back to `loadFromSync(connData)` (covers the case where the receiver missed the original `connector.created`). 5. Add a public `reAnchorByObject(objectId)` on the module's return object. It iterates over `connectors` and, for any connector whose `fromId` or `toId` (compared as strings) equals `objectId`, looks up the from/to groups in `WhiteboardCanvas.getObjectLayer()` and runs the same body that `updateFn` already runs in `createConnector` (recompute `closestAnchor` / `getLinePoints`, including the curved special case, write `c.arrow.points(pts)`, then `batchDraw`). This is the receiver-side equivalent of the Konva `dragmove` listener. Dependencies: none. ### Step 2 — `sync.js`: route `connector.updated` and re-anchor connectors after object moves File: `crates/hero_whiteboard_ui/static/web/js/whiteboard/sync.js` 1. In `handleWsMessage`, alongside `connector.created` / `connector.deleted`, add: ``` } else if (msg.type === 'connector.updated') { if (msg.data && typeof WhiteboardConnectors !== 'undefined' && WhiteboardConnectors.applyUpdate) { WhiteboardConnectors.applyUpdate(msg.data, { _fromSync: true }); } } ``` 2. In `applySyncUpdate`, after the position write `node.x(obj.x); node.y(obj.y)` and after rotation/dimension updates that could move the bounding box, call `WhiteboardConnectors.reAnchorByObject(strId)`. Place it once, near the end of `applySyncUpdate` just before the redraw block, so it runs after every geometry mutation. Guard with `typeof WhiteboardConnectors !== 'undefined' && WhiteboardConnectors.reAnchorByObject`. Dependencies: Step 1. ## Acceptance Criteria - [ ] Open the same board in tab A and tab B. In tab A, select a connector and change Line Style from Straight to Curved via the Properties panel. Tab B re-renders the connector as Curved within ~1 frame, no reload. Verified for Straight ↔ Curved ↔ Elbow. - [ ] In tab A, change a connector's stroke color and stroke width via the Properties panel. Tab B picks up both within ~1 frame. - [ ] In tab A, drag an object that has a connector attached at one or both endpoints. In tab B, the object moves *and* the connector endpoint follows the object during/after the move. - [ ] After a tab-B reload, connector positions and styles match tab A — live state and persisted state agree. - [ ] Tab A unchanged: local drag, style change, line-style cycling, undo/redo behave exactly as before (no double history entries, no echo loops, no stale temp-id paths). - [ ] No new console errors in either tab. - [ ] Connector with a temp id (RPC create still pending) is not broadcast as `connector.updated` (existing `numCId <= 0` / `isNaN` guard from `persistStyle` is reused). ## Notes - We deliberately do NOT broadcast `connector.updated` from `objects.js`'s `dragmove` path. The receiver derives endpoints from anchor positions, so emitting redundant waypoints would only widen the wire payload and create races with the position update itself. - `cycleLineStyle` currently destroys and recreates the local Konva arrow. When called with `_fromSync: true`, the recreate path inside it must not re-call `rpcCall('connector.create', ...)` — `createConnector` already guards on `_fromServer`, so the recursive `createConnector(... { _fromServer: true, ... })` call inside `cycleLineStyle` is preserved. Only the outer `rpcCall('connector.update', ...)` and the new broadcast are skipped under `_fromSync`. - `reAnchorByObject` does not fire Konva events — it directly recomputes endpoints. This avoids interfering with the sender path's listeners and is why the function lives in `connectors.js` (it owns the `connectors` map). - The `persistStyle` signature change adds an optional `opts` only — call sites in `properties.js` keep working unchanged. - Do not touch comments / themes / cursors / presence — out of scope.
Member

Test Results

  • cargo fmt --all -- --check: pass
  • cargo check --workspace: pass
  • cargo clippy --workspace -- -D warnings: pass
  • cargo test --workspace --lib: pass
  • node --check connectors.js: pass
  • node --check sync.js: pass
## Test Results - cargo fmt --all -- --check: pass - cargo check --workspace: pass - cargo clippy --workspace -- -D warnings: pass - cargo test --workspace --lib: pass - node --check connectors.js: pass - node --check sync.js: pass
Member

Implementation Summary

Two files changed, JS-only — no server / SDK / OpenRPC / DB changes. Reuses the existing per-board WebSocket relay; the only new wire-level addition is a connector.updated message routed alongside the already-handled connector.created and connector.deleted.

Root cause

  1. Line-style / stroke not synced. connectors.cycleLineStyle and connectors.persistStyle persisted via rpcCall('connector.update', ...) but never broadcast over the WebSocket, and sync.js#handleWsMessage had no connector.updated branch.
  2. Endpoint did not follow remote object move. Local connectors re-anchor via Konva dragmove.connector_<id> listeners. On the receiver, applySyncUpdate repositions the attached object with node.x(...); node.y(...). Programmatic setters do not fire dragmove, so the receiver's connector listeners never ran.

crates/hero_whiteboard_ui/static/web/js/whiteboard/connectors.js

  • cycleLineStyle(id) and persistStyle(id) now accept an optional opts and skip both the rpcCall('connector.update', ...) and the new WebSocket broadcast when opts._fromSync is true. The recursive createConnector(..., { _fromServer: true, ... }) call inside cycleLineStyle is preserved unchanged so the local Konva arrow is still recreated correctly under sync-driven cycling.
  • New private helper broadcastUpdate(id) sends { type: 'connector.updated', data } via the existing ws_sendConnector callback. Payload mirrors the connector.update RPC field shape (id, board_id, from_id, to_id, line_type, JSON-stringified style { stroke, strokeWidth }). Uses the existing temp-id guard so connectors with not-yet-assigned ids are not broadcast.
  • Two new exported functions:
    • applyUpdate(connData, opts) — applies a remote connector.updated payload locally. Cycles lineStyle to the requested type via cycleLineStyle(id, { _fromSync: true }) (capped at 4 iterations to avoid infinite loops on unknown types), parses connData.style, applies stroke / strokeWidth to c.arrow, updates baseStroke / baseStrokeWidth, and redraws. Falls back to loadFromSync(connData) if the connector is not yet present locally.
    • reAnchorByObject(objectId) — iterates connectors and, for any whose fromId or toId matches, recomputes anchor + line points (including the curved-style midpoint offset block, copied from the local dragmove updateFn) and writes them onto c.arrow.points(pts). Single batchDraw after the loop.

crates/hero_whiteboard_ui/static/web/js/whiteboard/sync.js

  • New branch in handleWsMessage next to the existing connector branches:
    } else if (msg.type === 'connector.updated') {
        if (msg.data && typeof WhiteboardConnectors !== 'undefined' && WhiteboardConnectors.applyUpdate) {
            WhiteboardConnectors.applyUpdate(msg.data, { _fromSync: true });
        }
    }
    
  • In applySyncUpdate, a single call to WhiteboardConnectors.reAnchorByObject(strId) near the end of the function (after every position / rotation / dimension mutation, just before the requestAnimationFrame redraw block) re-runs the same anchor-recompute logic the local dragmove listener runs, but driven by sync state.

Verification

  • cargo fmt --all -- --check: clean
  • cargo check --workspace: clean
  • cargo clippy --workspace -- -D warnings: clean
  • cargo test --workspace --lib: pass
  • node --check on both modified JS modules: clean

Manual smoke test

  1. Open the same board in two windows.
  2. In tab A, draw two shapes and a connector between them.
  3. In tab A, change the connector's Line Style (Straight ↔ Curved ↔ Elbow). Tab B updates within ~1 frame.
  4. In tab A, change the connector's stroke color and stroke width. Tab B picks both up.
  5. In tab A, drag one of the attached shapes. Tab B repositions the shape AND the connector endpoint follows along.
  6. Reload tab B — live state and persisted state agree.
  7. Local undo/redo / line-style cycling behave as before — no double history entries, no echo.

Notes

  • We did not broadcast connector.updated from the object-drag path. Endpoint geometry is fully derived from the from/to anchors, so the receiver recomputes it locally via reAnchorByObject after applying the position update — same approach the sender already uses through Konva's local dragmove listener.
  • Connectors with a temp id (RPC create still pending) are not broadcast — the existing numCId <= 0 / isNaN guard from persistStyle is reused in broadcastUpdate.
  • The _fromSync guard pattern is consistent with the prior issue #90 / #91 fixes (themes + shape type) and with the existing deleteConnector(id, opts) guard.
## Implementation Summary Two files changed, JS-only — no server / SDK / OpenRPC / DB changes. Reuses the existing per-board WebSocket relay; the only new wire-level addition is a `connector.updated` message routed alongside the already-handled `connector.created` and `connector.deleted`. ### Root cause 1. **Line-style / stroke not synced.** `connectors.cycleLineStyle` and `connectors.persistStyle` persisted via `rpcCall('connector.update', ...)` but never broadcast over the WebSocket, and `sync.js#handleWsMessage` had no `connector.updated` branch. 2. **Endpoint did not follow remote object move.** Local connectors re-anchor via Konva `dragmove.connector_<id>` listeners. On the receiver, `applySyncUpdate` repositions the attached object with `node.x(...); node.y(...)`. Programmatic setters do not fire `dragmove`, so the receiver's connector listeners never ran. ### `crates/hero_whiteboard_ui/static/web/js/whiteboard/connectors.js` - `cycleLineStyle(id)` and `persistStyle(id)` now accept an optional `opts` and skip both the `rpcCall('connector.update', ...)` and the new WebSocket broadcast when `opts._fromSync` is true. The recursive `createConnector(..., { _fromServer: true, ... })` call inside `cycleLineStyle` is preserved unchanged so the local Konva arrow is still recreated correctly under sync-driven cycling. - New private helper `broadcastUpdate(id)` sends `{ type: 'connector.updated', data }` via the existing `ws_sendConnector` callback. Payload mirrors the `connector.update` RPC field shape (`id`, `board_id`, `from_id`, `to_id`, `line_type`, JSON-stringified `style { stroke, strokeWidth }`). Uses the existing temp-id guard so connectors with not-yet-assigned ids are not broadcast. - Two new exported functions: - `applyUpdate(connData, opts)` — applies a remote `connector.updated` payload locally. Cycles `lineStyle` to the requested type via `cycleLineStyle(id, { _fromSync: true })` (capped at 4 iterations to avoid infinite loops on unknown types), parses `connData.style`, applies stroke / strokeWidth to `c.arrow`, updates `baseStroke` / `baseStrokeWidth`, and redraws. Falls back to `loadFromSync(connData)` if the connector is not yet present locally. - `reAnchorByObject(objectId)` — iterates connectors and, for any whose `fromId` or `toId` matches, recomputes anchor + line points (including the curved-style midpoint offset block, copied from the local `dragmove` `updateFn`) and writes them onto `c.arrow.points(pts)`. Single `batchDraw` after the loop. ### `crates/hero_whiteboard_ui/static/web/js/whiteboard/sync.js` - New branch in `handleWsMessage` next to the existing connector branches: ``` } else if (msg.type === 'connector.updated') { if (msg.data && typeof WhiteboardConnectors !== 'undefined' && WhiteboardConnectors.applyUpdate) { WhiteboardConnectors.applyUpdate(msg.data, { _fromSync: true }); } } ``` - In `applySyncUpdate`, a single call to `WhiteboardConnectors.reAnchorByObject(strId)` near the end of the function (after every position / rotation / dimension mutation, just before the `requestAnimationFrame` redraw block) re-runs the same anchor-recompute logic the local `dragmove` listener runs, but driven by sync state. ### Verification - `cargo fmt --all -- --check`: clean - `cargo check --workspace`: clean - `cargo clippy --workspace -- -D warnings`: clean - `cargo test --workspace --lib`: pass - `node --check` on both modified JS modules: clean ### Manual smoke test 1. Open the same board in two windows. 2. In tab A, draw two shapes and a connector between them. 3. In tab A, change the connector's Line Style (Straight ↔ Curved ↔ Elbow). Tab B updates within ~1 frame. 4. In tab A, change the connector's stroke color and stroke width. Tab B picks both up. 5. In tab A, drag one of the attached shapes. Tab B repositions the shape AND the connector endpoint follows along. 6. Reload tab B — live state and persisted state agree. 7. Local undo/redo / line-style cycling behave as before — no double history entries, no echo. ### Notes - We did not broadcast `connector.updated` from the object-drag path. Endpoint geometry is fully derived from the from/to anchors, so the receiver recomputes it locally via `reAnchorByObject` after applying the position update — same approach the sender already uses through Konva's local `dragmove` listener. - Connectors with a temp id (RPC create still pending) are not broadcast — the existing `numCId <= 0 / isNaN` guard from `persistStyle` is reused in `broadcastUpdate`. - The `_fromSync` guard pattern is consistent with the prior issue #90 / #91 fixes (themes + shape type) and with the existing `deleteConnector(id, opts)` guard.
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
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#92
No description provided.