phase 12
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
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
This commit is contained in:
195
frontend/src/lib/canvas/grid.ts
Normal file
195
frontend/src/lib/canvas/grid.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
/**
|
||||
* 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);
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user