feat(refboard): canvas polish — transform box sync, dot grid, snap tuning, perf fixes

- Fix transform box not updating after alignment/arrangement/normalize/flip operations
  (shortcuts, context menu, and selection toolbar all fixed with DRY _opUpdate helper)
- Replace 100ms polling loop with event-driven viewport updates (moved + wheel-scroll)
- Add adaptive dot grid background that responds to zoom/pan
- Reduce snap guide threshold from 8px to 4px for subtler snapping
- Remove PresenceOverlay (remote selection highlighting) — too heavy for minimal benefit
- Offset multiple dropped images so they don't overlap
- Add new canvas modules: SnapGuides, clipboard, grouping, FrameSprite, DrawingSprite,
  LaserPointer, context-menu-items, SelectionToolbar, Minimap
- Extract Editor hooks into dedicated files (useBoardLoader, useCanvasSetup,
  useShortcutHandler, useLayerPanel, useSaveManager, useFollowMode)
- Sync improvements: real-time transform broadcast, board rooms, viewport sync
This commit is contained in:
Hiren Kangad
2026-03-10 03:58:53 +05:30
parent cfd7adf73d
commit e9509ef7b0
41 changed files with 4171 additions and 705 deletions
+46
View File
@@ -79,6 +79,26 @@ function setupBoardRoom(io, socket) {
});
});
// ---- Incremental element sync (Excalidraw-style) ----
socket.on('element:update', (data) => {
if (!socket.currentBoardId) return;
const roomName = getRoomName(socket.currentBoardId);
socket.to(roomName).emit('element:update', {
...data,
userId: socket.userId,
});
});
socket.on('element:remove', (data) => {
if (!socket.currentBoardId) return;
const roomName = getRoomName(socket.currentBoardId);
socket.to(roomName).emit('element:remove', {
...data,
userId: socket.userId,
});
});
// ---- Lightweight transform (during drag/resize/rotate) ----
socket.on('object:transform', (data) => {
@@ -102,6 +122,32 @@ function setupBoardRoom(io, socket) {
});
});
// ---- Selection presence (broadcast what items a user has selected) ----
socket.on('selection:update', (data) => {
if (!socket.currentBoardId) return;
const roomName = getRoomName(socket.currentBoardId);
socket.volatile.to(roomName).emit('selection:update', {
...data,
userId: socket.userId,
displayName: socket.userDisplayName,
});
});
// ---- Laser pointer ----
socket.on('laser:move', (data) => {
if (!socket.currentBoardId) return;
const roomName = getRoomName(socket.currentBoardId);
socket.volatile.to(roomName).emit('laser:move', { ...data, userId: socket.userId });
});
socket.on('laser:stop', (data) => {
if (!socket.currentBoardId) return;
const roomName = getRoomName(socket.currentBoardId);
socket.to(roomName).emit('laser:stop', { ...data, userId: socket.userId });
});
// ---- Disconnect cleanup ----
socket.on('disconnect', () => {