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:
@@ -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', () => {
|
||||
|
||||
@@ -2,6 +2,7 @@ const { Server } = require('socket.io');
|
||||
const { verifyToken } = require('../auth');
|
||||
const { getUserById } = require('../db');
|
||||
const { setupBoardRoom } = require('./board-room');
|
||||
const { setupViewportSync } = require('./viewport-sync');
|
||||
|
||||
/**
|
||||
* Set up Socket.IO on the given HTTP server.
|
||||
@@ -49,6 +50,7 @@ function setupSocket(httpServer) {
|
||||
|
||||
// Set up board room handlers
|
||||
setupBoardRoom(io, socket);
|
||||
setupViewportSync(io, socket);
|
||||
|
||||
socket.on('disconnect', (reason) => {
|
||||
console.log(`[socket] User disconnected: ${socket.userDisplayName} (${reason})`);
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Viewport sync relay — broadcasts user viewport positions for follow mode.
|
||||
* Each user periodically emits their viewport state; this relays it to
|
||||
* everyone else in the same board room so followers can track in real-time.
|
||||
*
|
||||
* Kept separate from board-room.js to avoid conflicts.
|
||||
*/
|
||||
|
||||
function getRoomName(boardId) {
|
||||
return `board:${boardId}`;
|
||||
}
|
||||
|
||||
function setupViewportSync(io, socket) {
|
||||
// Relay viewport position to other users in the same board room
|
||||
socket.on('viewport:sync', (data) => {
|
||||
if (!socket.currentBoardId) return;
|
||||
const roomName = getRoomName(socket.currentBoardId);
|
||||
socket.volatile.to(roomName).emit('viewport:sync', {
|
||||
...data,
|
||||
userId: socket.userId,
|
||||
});
|
||||
});
|
||||
|
||||
// follow:start / follow:stop are informational — the server doesn't need
|
||||
// to gate viewport:sync relay (all users broadcast always, clients filter).
|
||||
// These events exist so the UI can show "X is following you" if desired.
|
||||
socket.on('follow:start', (data) => {
|
||||
if (!socket.currentBoardId) return;
|
||||
const roomName = getRoomName(socket.currentBoardId);
|
||||
socket.to(roomName).emit('follow:start', {
|
||||
boardId: data.boardId,
|
||||
followerId: socket.userId,
|
||||
followerName: socket.userDisplayName,
|
||||
targetUserId: data.targetUserId,
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('follow:stop', (data) => {
|
||||
if (!socket.currentBoardId) return;
|
||||
const roomName = getRoomName(socket.currentBoardId);
|
||||
socket.to(roomName).emit('follow:stop', {
|
||||
boardId: data.boardId,
|
||||
followerId: socket.userId,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { setupViewportSync };
|
||||
Reference in New Issue
Block a user