From 00c33706343200284f36158f707149057ccea37f Mon Sep 17 00:00:00 2001 From: Hiren Date: Mon, 9 Mar 2026 18:31:55 +0530 Subject: [PATCH] feat: remember zoom and pan position per board across sessions Viewport transform (zoom level + pan offset) is saved to localStorage keyed by board ID. Restored automatically when reopening the same board. Debounced 300ms save on zoom/pan to avoid excessive writes. --- frontend/src/canvas/FabricCanvas.tsx | 54 +++++++++++++++++++++++++--- frontend/src/pages/Editor.tsx | 1 + 2 files changed, 50 insertions(+), 5 deletions(-) diff --git a/frontend/src/canvas/FabricCanvas.tsx b/frontend/src/canvas/FabricCanvas.tsx index e4eb1d8..5e6e4d4 100644 --- a/frontend/src/canvas/FabricCanvas.tsx +++ b/frontend/src/canvas/FabricCanvas.tsx @@ -12,14 +12,31 @@ export interface FabricCanvasHandle { interface FabricCanvasProps { canvasState?: string | null; currentTool: string; + boardId?: string; onChange?: () => void; } const MIN_ZOOM = 0.1; const MAX_ZOOM = 5.0; +function viewportKey(id: string) { return `refboard:viewport:${id}`; } + +function saveViewport(id: string, vpt: number[]) { + try { localStorage.setItem(viewportKey(id), JSON.stringify(vpt)); } catch {} +} + +function loadViewport(id: string): number[] | null { + try { + const raw = localStorage.getItem(viewportKey(id)); + if (!raw) return null; + const arr = JSON.parse(raw); + if (Array.isArray(arr) && arr.length === 6 && arr.every((v: any) => typeof v === 'number' && isFinite(v))) return arr; + } catch {} + return null; +} + const FabricCanvas = forwardRef( - ({ canvasState, currentTool, onChange }, ref) => { + ({ canvasState, currentTool, boardId, onChange }, ref) => { const containerRef = useRef(null); const canvasElRef = useRef(null); const fabricRef = useRef(null); @@ -49,6 +66,17 @@ const FabricCanvas = forwardRef( fabricRef.current = canvas; + // Debounced viewport persistence + let vpTimer: ReturnType | null = null; + const persistViewport = () => { + if (!boardId) return; + if (vpTimer) clearTimeout(vpTimer); + vpTimer = setTimeout(() => { + const vpt = canvas.viewportTransform; + if (vpt) saveViewport(boardId, [...vpt]); + }, 300); + }; + // Handle resize const observer = new ResizeObserver(() => { const w = container.clientWidth; @@ -76,6 +104,7 @@ const FabricCanvas = forwardRef( vpt[5] += e.e.clientY - lastPanPoint.current.y; lastPanPoint.current = { x: e.e.clientX, y: e.e.clientY }; canvas.requestRenderAll(); + persistViewport(); }); canvas.on('mouse:up', () => { @@ -113,6 +142,7 @@ const FabricCanvas = forwardRef( const point = canvas.getScenePoint(e); canvas.zoomToPoint(point, zoom); canvas.requestRenderAll(); + persistViewport(); }); // Space key for pan @@ -146,6 +176,7 @@ const FabricCanvas = forwardRef( changeEvents.forEach((evt) => canvas.on(evt as any, changeHandler)); return () => { + if (vpTimer) clearTimeout(vpTimer); observer.disconnect(); window.removeEventListener('keydown', onKeyDown); window.removeEventListener('keyup', onKeyUp); @@ -196,6 +227,13 @@ const FabricCanvas = forwardRef( // other clients with object:add events for objects they already have suppressBroadcasts(); canvas.loadFromJSON(parsed).then(() => { + // Restore saved viewport (zoom + pan) if available + if (boardId) { + const savedVpt = loadViewport(boardId); + if (savedVpt) { + canvas.setViewportTransform(savedVpt as any); + } + } canvas.requestRenderAll(); // Small delay to ensure all deferred events have fired before resuming setTimeout(() => resumeBroadcasts(), 100); @@ -240,13 +278,15 @@ const FabricCanvas = forwardRef( const cx = (minX + maxX) / 2; const cy = (minY + maxY) / 2; - canvas.setViewportTransform([ + const vpt: [number, number, number, number, number, number] = [ zoom, 0, 0, zoom, canvasW / 2 - cx * zoom, canvasH / 2 - cy * zoom, - ]); + ]; + canvas.setViewportTransform(vpt); canvas.requestRenderAll(); - }, []); + if (boardId) saveViewport(boardId, vpt); + }, [boardId]); useImperativeHandle(ref, () => ({ getCanvas: () => fabricRef.current, @@ -258,8 +298,12 @@ const FabricCanvas = forwardRef( const center = canvas.getCenterPoint(); canvas.zoomToPoint(center, Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, zoom))); canvas.requestRenderAll(); + if (boardId) { + const vpt = canvas.viewportTransform; + if (vpt) saveViewport(boardId, [...vpt]); + } }, - }), [fitAll]); + }), [fitAll, boardId]); return (