All checks were successful
CI/CD Pipeline / VM Test - backend-integration (push) Successful in 8s
CI/CD Pipeline / VM Test - full-stack (push) Successful in 7s
CI/CD Pipeline / VM Test - performance (push) Successful in 7s
CI/CD Pipeline / VM Test - security (push) Successful in 7s
CI/CD Pipeline / Backend Linting (push) Successful in 2s
CI/CD Pipeline / Frontend Linting (push) Successful in 16s
CI/CD Pipeline / Nix Flake Check (push) Successful in 38s
CI/CD Pipeline / CI Summary (push) Successful in 0s
196 lines
3.9 KiB
TypeScript
196 lines
3.9 KiB
TypeScript
/**
|
|
* 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<GridSettings> = 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);
|
|
};
|
|
}
|