import Konva from 'konva'; const SNAP_THRESHOLD = 10; type SnapPoint = { position: number; type: 'edge' | 'center'; direction: 'horizontal' | 'vertical'; }; type SnapResult = { x: number; y: number; horizontalGuide?: number; verticalGuide?: number; }; type ResizeSnapResult = { x: number; y: number; width: number; height: number; horizontalGuides: number[]; verticalGuides: number[]; }; export function initializeSnapGuides(stage: Konva.Stage): Konva.Layer { // Remove any existing snap guide layers from this stage const existingSnapLayers = stage.find('.snap-guide-layer'); existingSnapLayers.forEach((layer) => layer.destroy()); const snapGuideLayer = new Konva.Layer({ name: 'snap-guide-layer', }); stage.add(snapGuideLayer); return snapGuideLayer; } export function calculateSnapPositions( stage: Konva.Stage, excludeId?: string, ): { horizontal: SnapPoint[]; vertical: SnapPoint[] } { const fieldGroups = stage .find('.field-group') .filter((node): node is Konva.Group => node instanceof Konva.Group); const horizontal: SnapPoint[] = []; const vertical: SnapPoint[] = []; fieldGroups.forEach((group) => { if (excludeId && group.id() === excludeId) { return; } const rect = group.getClientRect(); // Vertical snap points (for horizontal alignment) horizontal.push( { position: rect.y, type: 'edge', direction: 'horizontal' }, { position: rect.y + rect.height / 2, type: 'center', direction: 'horizontal' }, { position: rect.y + rect.height, type: 'edge', direction: 'horizontal' }, ); // Horizontal snap points (for vertical alignment) vertical.push( { position: rect.x, type: 'edge', direction: 'vertical' }, { position: rect.x + rect.width / 2, type: 'center', direction: 'vertical' }, { position: rect.x + rect.width, type: 'edge', direction: 'vertical' }, ); }); return { horizontal, vertical }; } export function calculateSnapSizes( stage: Konva.Stage, excludeId?: string, ): { widths: number[]; heights: number[] } { const fieldGroups = stage .find('.field-group') .filter((node): node is Konva.Group => node instanceof Konva.Group); const widths: number[] = []; const heights: number[] = []; fieldGroups.forEach((group) => { if (excludeId && group.id() === excludeId) { return; } const rect = group.getClientRect(); widths.push(rect.width); heights.push(rect.height); }); return { widths, heights }; } export function getSnappedPosition( stage: Konva.Stage, movingGroup: Konva.Group, newX: number, newY: number, ): SnapResult { const { horizontal, vertical } = calculateSnapPositions(stage, movingGroup.id()); const rect = movingGroup.getClientRect(); let snappedX = newX; let snappedY = newY; let horizontalGuide: number | undefined; let verticalGuide: number | undefined; // Calculate the moving field's snap points const movingTop = newY; const movingBottom = newY + rect.height; const movingCenterY = newY + rect.height / 2; const movingLeft = newX; const movingRight = newX + rect.width; const movingCenterX = newX + rect.width / 2; // Check horizontal snapping (Y position) for (const snapPoint of horizontal) { const distanceTop = Math.abs(movingTop - snapPoint.position); const distanceBottom = Math.abs(movingBottom - snapPoint.position); const distanceCenter = Math.abs(movingCenterY - snapPoint.position); if (distanceTop <= SNAP_THRESHOLD) { snappedY = snapPoint.position; horizontalGuide = snapPoint.position; break; } else if (distanceBottom <= SNAP_THRESHOLD) { snappedY = snapPoint.position - rect.height; horizontalGuide = snapPoint.position; break; } else if (distanceCenter <= SNAP_THRESHOLD) { snappedY = snapPoint.position - rect.height / 2; horizontalGuide = snapPoint.position; break; } } // Check vertical snapping (X position) for (const snapPoint of vertical) { const distanceLeft = Math.abs(movingLeft - snapPoint.position); const distanceRight = Math.abs(movingRight - snapPoint.position); const distanceCenter = Math.abs(movingCenterX - snapPoint.position); if (distanceLeft <= SNAP_THRESHOLD) { snappedX = snapPoint.position; verticalGuide = snapPoint.position; break; } else if (distanceRight <= SNAP_THRESHOLD) { snappedX = snapPoint.position - rect.width; verticalGuide = snapPoint.position; break; } else if (distanceCenter <= SNAP_THRESHOLD) { snappedX = snapPoint.position - rect.width / 2; verticalGuide = snapPoint.position; break; } } return { x: snappedX, y: snappedY, horizontalGuide, verticalGuide, }; } export function getSnappedResize( stage: Konva.Stage, resizingGroup: Konva.Group, newX: number, newY: number, newWidth: number, newHeight: number, ): ResizeSnapResult { const { horizontal, vertical } = calculateSnapPositions(stage, resizingGroup.id()); const { widths, heights } = calculateSnapSizes(stage, resizingGroup.id()); const snappedX = newX; const snappedY = newY; let snappedWidth = newWidth; let snappedHeight = newHeight; const horizontalGuides: number[] = []; const verticalGuides: number[] = []; // Snap width to other field widths for (const width of widths) { if (Math.abs(newWidth - width) <= SNAP_THRESHOLD) { snappedWidth = width; break; } } // Snap height to other field heights for (const height of heights) { if (Math.abs(newHeight - height) <= SNAP_THRESHOLD) { snappedHeight = height; break; } } // Calculate field edges with new snapped dimensions const movingTop = snappedY; const movingBottom = snappedY + snappedHeight; const movingLeft = snappedX; const movingRight = snappedX + snappedWidth; // Snap edges to alignment guides for (const snapPoint of horizontal) { if (Math.abs(movingTop - snapPoint.position) <= SNAP_THRESHOLD) { horizontalGuides.push(snapPoint.position); } else if (Math.abs(movingBottom - snapPoint.position) <= SNAP_THRESHOLD) { horizontalGuides.push(snapPoint.position); } } for (const snapPoint of vertical) { if (Math.abs(movingLeft - snapPoint.position) <= SNAP_THRESHOLD) { verticalGuides.push(snapPoint.position); } else if (Math.abs(movingRight - snapPoint.position) <= SNAP_THRESHOLD) { verticalGuides.push(snapPoint.position); } } return { x: snappedX, y: snappedY, width: snappedWidth, height: snappedHeight, horizontalGuides, verticalGuides, }; } export function showSnapGuides( snapGuideLayer: Konva.Layer, horizontalGuide?: number, verticalGuide?: number, stageWidth?: number, stageHeight?: number, ): void { if (!snapGuideLayer) { return; } hideSnapGuides(snapGuideLayer); if (horizontalGuide !== undefined && stageWidth) { const horizontalLine = new Konva.Line({ name: 'snap-guide-horizontal', points: [0, horizontalGuide, stageWidth, horizontalGuide], stroke: 'rgb(0, 161, 255)', strokeWidth: 1, dash: [5, 5], listening: false, }); snapGuideLayer.add(horizontalLine); } if (verticalGuide !== undefined && stageHeight) { const verticalLine = new Konva.Line({ name: 'snap-guide-vertical', points: [verticalGuide, 0, verticalGuide, stageHeight], stroke: 'rgb(0, 161, 255)', strokeWidth: 1, dash: [5, 5], listening: false, }); snapGuideLayer.add(verticalLine); } snapGuideLayer.batchDraw(); } export function showMultipleSnapGuides( snapGuideLayer: Konva.Layer, horizontalGuides: number[], verticalGuides: number[], stageWidth: number, stageHeight: number, ): void { if (!snapGuideLayer) { return; } hideSnapGuides(snapGuideLayer); // Show horizontal guides horizontalGuides.forEach((guide) => { const horizontalLine = new Konva.Line({ name: 'snap-guide-horizontal', points: [0, guide, stageWidth, guide], stroke: 'rgb(0, 161, 255)', strokeWidth: 1, dash: [5, 5], listening: false, }); snapGuideLayer.add(horizontalLine); }); // Show vertical guides verticalGuides.forEach((guide) => { const verticalLine = new Konva.Line({ name: 'snap-guide-vertical', points: [guide, 0, guide, stageHeight], stroke: 'rgb(0, 161, 255)', strokeWidth: 1, dash: [5, 5], listening: false, }); snapGuideLayer.add(verticalLine); }); snapGuideLayer.batchDraw(); } export function hideSnapGuides(snapGuideLayer: Konva.Layer): void { if (!snapGuideLayer) { return; } const guides = snapGuideLayer.find('.snap-guide-horizontal, .snap-guide-vertical'); guides.forEach((guide: Konva.Node) => guide.destroy()); snapGuideLayer.batchDraw(); }