/** * Grid and snap-to-grid functionality for canvas * Provides visual grid and snapping behavior */ import Konva from 'konva'; import { writable } from 'svelte/store'; import type { Writable } from 'svelte/store'; export interface GridSettings { enabled: boolean; size: number; // Grid cell size in pixels visible: boolean; // Show visual grid snapEnabled: boolean; // Enable snap-to-grid color: string; // Grid line color opacity: number; // Grid line opacity } const DEFAULT_GRID: GridSettings = { enabled: true, size: 20, visible: false, snapEnabled: false, color: '#d1d5db', opacity: 0.5, }; /** * Create grid settings store */ function createGridStore() { const { subscribe, set, update }: Writable = writable(DEFAULT_GRID); return { subscribe, set, update, /** * Toggle grid visibility */ toggleVisible: () => { update((settings) => ({ ...settings, visible: !settings.visible, })); }, /** * Toggle snap-to-grid */ toggleSnap: () => { update((settings) => ({ ...settings, snapEnabled: !settings.snapEnabled, })); }, /** * Set grid size */ setSize: (size: number) => { update((settings) => ({ ...settings, size: Math.max(5, Math.min(200, size)), // Clamp to 5-200 })); }, /** * Enable/disable grid */ setEnabled: (enabled: boolean) => { update((settings) => ({ ...settings, enabled, })); }, /** * Reset to defaults */ reset: () => { set(DEFAULT_GRID); }, }; } export const grid = createGridStore(); /** * Snap position to grid */ export function snapToGrid(x: number, y: number, gridSize: number): { x: number; y: number } { return { x: Math.round(x / gridSize) * gridSize, y: Math.round(y / gridSize) * gridSize, }; } /** * Draw visual grid on layer */ export function drawGrid( layer: Konva.Layer, width: number, height: number, gridSize: number, color: string = '#d1d5db', opacity: number = 0.5 ): Konva.Group { const gridGroup = new Konva.Group({ listening: false, name: 'grid', }); // Draw vertical lines for (let x = 0; x <= width; x += gridSize) { const line = new Konva.Line({ points: [x, 0, x, height], stroke: color, strokeWidth: 1, opacity, listening: false, }); gridGroup.add(line); } // Draw horizontal lines for (let y = 0; y <= height; y += gridSize) { const line = new Konva.Line({ points: [0, y, width, y], stroke: color, strokeWidth: 1, opacity, listening: false, }); gridGroup.add(line); } layer.add(gridGroup); gridGroup.moveToBottom(); // Grid should be behind all images return gridGroup; } /** * Remove grid from layer */ export function removeGrid(layer: Konva.Layer): void { const grids = layer.find('.grid'); grids.forEach((grid) => grid.destroy()); layer.batchDraw(); } /** * Update grid visual */ export function updateGrid( layer: Konva.Layer, settings: GridSettings, viewportWidth: number, viewportHeight: number ): void { // Remove existing grid removeGrid(layer); // Draw new grid if visible if (settings.visible && settings.enabled) { drawGrid(layer, viewportWidth, viewportHeight, settings.size, settings.color, settings.opacity); layer.batchDraw(); } } /** * Setup drag with snap-to-grid */ export function setupSnapDrag( image: Konva.Image | Konva.Group, gridSettings: GridSettings ): () => void { function handleDragMove() { if (!gridSettings.snapEnabled || !gridSettings.enabled) return; const pos = image.position(); const snapped = snapToGrid(pos.x, pos.y, gridSettings.size); image.position(snapped); } image.on('dragmove', handleDragMove); return () => { image.off('dragmove', handleDragMove); }; }