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
+153
View File
@@ -0,0 +1,153 @@
import { useEffect } from 'react';
import type { PixiCanvasHandle } from '../canvas/PixiCanvas';
import type { SelectionManager } from '../canvas/SelectionManager';
import type { UndoManager } from '../canvas/history';
import type { SceneItem } from '../canvas/SceneManager';
import { getItemWorldBounds } from '../canvas/SceneManager';
import { ToolType, toolShortcuts } from '../canvas/tools';
import { ShortcutContext, matchesShortcut, sortBySpecificity } from '../canvas/shortcuts';
import { shortcuts as shortcutDefs } from '../canvas/shortcut-definitions';
import { writeCanvasToClipboard, pasteFromSystemClipboard } from '../canvas/clipboard';
// Pre-sort shortcuts by specificity (most modifiers first) for correct matching
const sortedShortcuts = sortBySpecificity(shortcutDefs);
interface ShortcutHandlerDeps {
canvasRef: React.RefObject<PixiCanvasHandle | null>;
selectionRef: React.RefObject<SelectionManager | null>;
undoRef: React.RefObject<UndoManager | null>;
clipboardRef: React.MutableRefObject<SceneItem[]>;
resolvedBoardId: string | undefined;
onCanvasChange: () => void;
showToast: (msg: string) => void;
refreshLayers: () => void;
handleGroup: () => void;
handleUngroup: () => void;
setActiveTool: (t: ToolType) => void;
setCanUndo: (v: boolean) => void;
setCanRedo: (v: boolean) => void;
setZoom: (z: number) => void;
setShowGrid: React.Dispatch<React.SetStateAction<boolean>>;
setShowHelp: React.Dispatch<React.SetStateAction<boolean>>;
setFocusMode: React.Dispatch<React.SetStateAction<boolean>>;
}
/**
* Keyboard shortcut handler — registry-based, delegates to shortcut-definitions.
*/
export function useShortcutHandler(deps: ShortcutHandlerDeps) {
const {
canvasRef, selectionRef, undoRef, clipboardRef, resolvedBoardId,
onCanvasChange, showToast, refreshLayers,
handleGroup, handleUngroup,
setActiveTool, setCanUndo, setCanRedo, setZoom,
setShowGrid, setShowHelp, setFocusMode,
} = deps;
useEffect(() => {
async function onKeyDown(e: KeyboardEvent) {
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return;
const scene = canvasRef.current?.getScene();
const selection = selectionRef.current;
const viewport = canvasRef.current?.getViewport();
if (!scene || !selection || !viewport) return;
// Tool shortcuts (bare keys 1-5, v/h/p/t/e -- no modifiers)
if (!e.ctrlKey && !e.metaKey && !e.altKey) {
const tool = toolShortcuts[e.key.toLowerCase()];
if (tool) {
setActiveTool(tool);
return;
}
}
// Build context for shortcut handlers
const ctx: ShortcutContext = {
scene,
selection,
history: undoRef.current!,
viewport,
onChange: onCanvasChange,
clipboardRef,
writeCanvasToClipboard: async (items?: SceneItem[]) => {
try {
const app = canvasRef.current?.getApp() ?? null;
await writeCanvasToClipboard(app, viewport, items);
} catch (err: any) {
showToast('Copy failed: ' + (err.message || 'clipboard not available'));
}
},
showToast,
fitAll: () => canvasRef.current?.fitAll(),
setActiveTool,
setCanUndo,
setCanRedo,
refreshLayers,
handleGroup,
handleUngroup,
toggleGrid: () => setShowGrid((v) => !v),
toggleShowHelp: () => setShowHelp((v) => !v),
toggleFocusMode: () => setFocusMode((v) => !v),
pasteFromSystemClipboard: async (): Promise<string> => {
if (!resolvedBoardId) return 'No board';
const msg = await pasteFromSystemClipboard(scene, viewport, resolvedBoardId, onCanvasChange);
if (msg !== 'No image in clipboard') showToast(msg);
return msg;
},
fitSelection: () => {
const vp = canvasRef.current?.getViewport();
if (!vp || !selection) return;
const items = selection.getSelectedItems();
if (items.length === 0) return;
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
for (const item of items) {
const { x: ix, y: iy, w: iw, h: ih } = getItemWorldBounds(item);
if (ix < minX) minX = ix;
if (iy < minY) minY = iy;
if (ix + iw > maxX) maxX = ix + iw;
if (iy + ih > maxY) maxY = iy + ih;
}
const padding = 60;
const contentW = maxX - minX + padding * 2;
const contentH = maxY - minY + padding * 2;
const scaleX = vp.screenWidth / contentW;
const scaleY = vp.screenHeight / contentH;
const targetScale = Math.min(scaleX, scaleY, 5);
vp.animate({
position: { x: (minX + maxX) / 2, y: (minY + maxY) / 2 },
scale: targetScale,
time: 300,
ease: 'easeOutQuad',
});
},
};
// Match against sorted registry (most-specific first)
for (const def of sortedShortcuts) {
if (!matchesShortcut(e, def)) continue;
// Check selection requirements
const activeCount = selection.selectedIds.size;
if (def.needsSelection && activeCount === 0) continue;
if (def.minSelection && activeCount < def.minSelection) continue;
e.preventDefault();
await def.handler(ctx);
// Update zoom display after any action
setZoom(canvasRef.current?.getZoom() ?? 1);
return;
}
}
window.addEventListener('keydown', onKeyDown);
return () => window.removeEventListener('keydown', onKeyDown);
}, [
canvasRef, selectionRef, undoRef, clipboardRef, resolvedBoardId,
onCanvasChange, showToast, refreshLayers,
handleGroup, handleUngroup,
setActiveTool, setCanUndo, setCanRedo, setZoom,
setShowGrid, setShowHelp, setFocusMode,
]);
}