import { create } from 'zustand'; import { persist } from 'zustand/middleware'; import type { CanvasState, CanvasSelection, Point } from '@/types'; interface CanvasStore extends CanvasState { /** Selection state */ selection: CanvasSelection; /** Set canvas dimensions */ setDimensions: (width: number, height: number) => void; /** Set zoom level */ setZoom: (zoom: number) => void; /** Zoom in (1.2x) */ zoomIn: () => void; /** Zoom out (0.8x) */ zoomOut: () => void; /** Fit canvas to viewport */ zoomToFit: (viewportWidth: number, viewportHeight: number) => void; /** Zoom to actual size (100%) */ zoomToActual: () => void; /** Set pan offset */ setPanOffset: (x: number, y: number) => void; /** Pan by delta */ pan: (dx: number, dy: number) => void; /** Reset pan to center */ resetPan: () => void; /** Toggle grid visibility */ toggleGrid: () => void; /** Set grid size */ setGridSize: (size: number) => void; /** Toggle rulers */ toggleRulers: () => void; /** Toggle snap to grid */ toggleSnapToGrid: () => void; /** Set background color */ setBackgroundColor: (color: string) => void; /** Set selection */ setSelection: (selection: Partial) => void; /** Clear selection */ clearSelection: () => void; /** Convert screen coordinates to canvas coordinates */ screenToCanvas: (screenX: number, screenY: number, containerWidth?: number, containerHeight?: number) => Point; /** Convert canvas coordinates to screen coordinates */ canvasToScreen: (canvasX: number, canvasY: number) => Point; } const DEFAULT_CANVAS_WIDTH = 800; const DEFAULT_CANVAS_HEIGHT = 600; const MIN_ZOOM = 0.1; const MAX_ZOOM = 10; const ZOOM_STEP = 1.2; export const useCanvasStore = create()( persist( (set, get) => ({ width: DEFAULT_CANVAS_WIDTH, height: DEFAULT_CANVAS_HEIGHT, zoom: 1, offsetX: 0, offsetY: 0, backgroundColor: 'transparent', showGrid: false, gridSize: 20, showRulers: true, snapToGrid: false, selection: { active: false, x: 0, y: 0, width: 0, height: 0, }, setDimensions: (width, height) => { set({ width, height }); }, setZoom: (zoom) => { const clampedZoom = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, zoom)); set({ zoom: clampedZoom }); }, zoomIn: () => { const { zoom } = get(); get().setZoom(zoom * ZOOM_STEP); }, zoomOut: () => { const { zoom } = get(); get().setZoom(zoom / ZOOM_STEP); }, zoomToFit: (viewportWidth, viewportHeight) => { const { width, height } = get(); const padding = 40; const scaleX = (viewportWidth - padding * 2) / width; const scaleY = (viewportHeight - padding * 2) / height; const zoom = Math.min(scaleX, scaleY, 1); set({ zoom, offsetX: 0, offsetY: 0 }); }, zoomToActual: () => { set({ zoom: 1 }); }, setPanOffset: (x, y) => { set({ offsetX: x, offsetY: y }); }, pan: (dx, dy) => { set((state) => ({ offsetX: state.offsetX + dx, offsetY: state.offsetY + dy, })); }, resetPan: () => { set({ offsetX: 0, offsetY: 0 }); }, toggleGrid: () => { set((state) => ({ showGrid: !state.showGrid })); }, setGridSize: (size) => { set({ gridSize: Math.max(1, size) }); }, toggleRulers: () => { set((state) => ({ showRulers: !state.showRulers })); }, toggleSnapToGrid: () => { set((state) => ({ snapToGrid: !state.snapToGrid })); }, setBackgroundColor: (color) => { set({ backgroundColor: color }); }, setSelection: (selection) => { set((state) => ({ selection: { ...state.selection, ...selection }, })); }, clearSelection: () => { set({ selection: { active: false, x: 0, y: 0, width: 0, height: 0, }, }); }, screenToCanvas: (screenX, screenY, containerWidth = 0, containerHeight = 0) => { const { zoom, offsetX, offsetY, width, height } = get(); // The canvas is rendered with this transformation: // 1. translate(offsetX + containerWidth/2, offsetY + containerHeight/2) - center in viewport with offset // 2. scale(zoom) - apply zoom // 3. translate(-width/2, -height/2) - position canvas so (0,0) is at top-left // // To reverse: // 1. Subtract container center and offset // 2. Divide by zoom // 3. Add canvas center return { x: (screenX - containerWidth / 2 - offsetX) / zoom + width / 2, y: (screenY - containerHeight / 2 - offsetY) / zoom + height / 2, }; }, canvasToScreen: (canvasX, canvasY) => { const { zoom, offsetX, offsetY } = get(); return { x: canvasX * zoom + offsetX, y: canvasY * zoom + offsetY, }; }, }), { name: 'canvas-view-storage', partialize: (state) => ({ backgroundColor: state.backgroundColor, showGrid: state.showGrid, gridSize: state.gridSize, showRulers: state.showRulers, snapToGrid: state.snapToGrid, // Exclude: width, height, zoom, offsetX, offsetY, selection }), } ) );