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
+20 -7
View File
@@ -1,7 +1,6 @@
import { Graphics } from 'pixi.js';
import type { Viewport } from 'pixi-viewport';
import type { SceneManager } from './SceneManager';
import { VideoSprite } from './sprites/VideoSprite';
import { uploadImage, uploadImageFromUrl } from '../api';
type OnChange = () => void;
@@ -55,10 +54,7 @@ function handleUploadResult(
}
if (mediaType === 'video' && assetKey) {
const url = imgData.public_url;
const video = new VideoSprite(assetKey, finalW, finalH, url);
video.position.set(x, y);
viewport.addChild(video);
sceneManager.addVideoFromUpload(assetKey, finalW, finalH, x, y);
} else if (assetKey) {
sceneManager.addImageFromUpload(assetKey, finalW, finalH, x, y);
} else {
@@ -122,18 +118,23 @@ export function setupDragDrop(
return;
}
let dropIndex = 0;
for (let i = 0; i < files.length; i++) {
const file = files[i];
if (!file.type.startsWith('image/') && !file.type.startsWith('video/')) continue;
const rect = container.getBoundingClientRect();
const world = viewport.toWorld(e.clientX - rect.left, e.clientY - rect.top);
const placeholder = createPlaceholder(viewport, world.x, world.y);
// Offset each subsequent file so they don't overlap
const offsetX = dropIndex * 30;
const offsetY = dropIndex * 30;
const placeholder = createPlaceholder(viewport, world.x + offsetX, world.y + offsetY);
dropIndex++;
try {
const res = await uploadImage(boardId, file);
removePlaceholder(viewport, placeholder);
handleUploadResult(res, viewport, sceneManager, world.x, world.y, onChange);
handleUploadResult(res, viewport, sceneManager, world.x + offsetX, world.y + offsetY, onChange);
} catch (err) {
console.error('Image upload failed:', err);
removePlaceholder(viewport, placeholder);
@@ -141,10 +142,22 @@ export function setupDragDrop(
}
}
// Prevent browser default (opening file in new tab) on the whole document
function onDocDragOver(e: DragEvent) {
e.preventDefault();
}
function onDocDrop(e: DragEvent) {
e.preventDefault();
}
document.addEventListener('dragover', onDocDragOver);
document.addEventListener('drop', onDocDrop);
container.addEventListener('dragover', onDragOver);
container.addEventListener('drop', onDrop);
return () => {
document.removeEventListener('dragover', onDocDragOver);
document.removeEventListener('drop', onDocDrop);
container.removeEventListener('dragover', onDragOver);
container.removeEventListener('drop', onDrop);
};