import { Position, Size } from "@/types/screen"; export interface GridSettings { columns: number; gap: number; padding: number; snapToGrid: boolean; } export interface GridInfo { columnWidth: number; totalWidth: number; totalHeight: number; } /** * 격자 정보 계산 */ export function calculateGridInfo( containerWidth: number, containerHeight: number, gridSettings: GridSettings, ): GridInfo { const { columns, gap, padding } = gridSettings; // 사용 가능한 너비 계산 (패딩 제외) const availableWidth = containerWidth - padding * 2; // 격자 간격을 고려한 컬럼 너비 계산 const totalGaps = (columns - 1) * gap; const columnWidth = (availableWidth - totalGaps) / columns; return { columnWidth: Math.max(columnWidth, 50), // 최소 50px totalWidth: containerWidth, totalHeight: containerHeight, }; } /** * 위치를 격자에 맞춤 */ export function snapToGrid(position: Position, gridInfo: GridInfo, gridSettings: GridSettings): Position { if (!gridSettings.snapToGrid) { return position; } const { columnWidth } = gridInfo; const { gap, padding } = gridSettings; // 격자 기준으로 위치 계산 const gridX = Math.round((position.x - padding) / (columnWidth + gap)); const gridY = Math.round((position.y - padding) / 20); // 20px 단위로 세로 스냅 // 실제 픽셀 위치로 변환 const snappedX = Math.max(padding, padding + gridX * (columnWidth + gap)); const snappedY = Math.max(padding, padding + gridY * 20); return { x: snappedX, y: snappedY, z: position.z, }; } /** * 크기를 격자에 맞춤 */ export function snapSizeToGrid(size: Size, gridInfo: GridInfo, gridSettings: GridSettings): Size { if (!gridSettings.snapToGrid) { return size; } const { columnWidth } = gridInfo; const { gap } = gridSettings; // 격자 단위로 너비 계산 // 컴포넌트가 차지하는 컬럼 수를 올바르게 계산 let gridColumns = 1; // 현재 너비에서 가장 가까운 격자 컬럼 수 찾기 for (let cols = 1; cols <= gridSettings.columns; cols++) { const targetWidth = cols * columnWidth + (cols - 1) * gap; if (size.width <= targetWidth + (columnWidth + gap) / 2) { gridColumns = cols; break; } gridColumns = cols; } const snappedWidth = gridColumns * columnWidth + (gridColumns - 1) * gap; // 높이는 20px 단위로 스냅 const snappedHeight = Math.max(40, Math.round(size.height / 20) * 20); console.log( `📏 크기 스냅: ${size.width}px → ${snappedWidth}px (${gridColumns}컬럼, 컬럼너비:${columnWidth}px, 간격:${gap}px)`, ); return { width: Math.max(columnWidth, snappedWidth), height: snappedHeight, }; } /** * 격자 컬럼 수로 너비 계산 */ export function calculateWidthFromColumns(columns: number, gridInfo: GridInfo, gridSettings: GridSettings): number { const { columnWidth } = gridInfo; const { gap } = gridSettings; return columns * columnWidth + (columns - 1) * gap; } /** * 너비에서 격자 컬럼 수 계산 */ export function calculateColumnsFromWidth(width: number, gridInfo: GridInfo, gridSettings: GridSettings): number { const { columnWidth } = gridInfo; const { gap } = gridSettings; return Math.max(1, Math.round((width + gap) / (columnWidth + gap))); } /** * 격자 가이드라인 생성 */ export function generateGridLines( containerWidth: number, containerHeight: number, gridSettings: GridSettings, ): { verticalLines: number[]; horizontalLines: number[]; } { const { columns, gap, padding } = gridSettings; const gridInfo = calculateGridInfo(containerWidth, containerHeight, gridSettings); const { columnWidth } = gridInfo; // 세로 격자선 (컬럼 경계) const verticalLines: number[] = []; for (let i = 0; i <= columns; i++) { const x = padding + i * (columnWidth + gap) - gap / 2; if (x >= padding && x <= containerWidth - padding) { verticalLines.push(x); } } // 가로 격자선 (20px 단위) const horizontalLines: number[] = []; for (let y = padding; y < containerHeight; y += 20) { horizontalLines.push(y); } return { verticalLines, horizontalLines, }; } /** * 컴포넌트가 격자 경계에 있는지 확인 */ export function isOnGridBoundary( position: Position, size: Size, gridInfo: GridInfo, gridSettings: GridSettings, tolerance: number = 5, ): boolean { const snappedPos = snapToGrid(position, gridInfo, gridSettings); const snappedSize = snapSizeToGrid(size, gridInfo, gridSettings); const positionMatch = Math.abs(position.x - snappedPos.x) <= tolerance && Math.abs(position.y - snappedPos.y) <= tolerance; const sizeMatch = Math.abs(size.width - snappedSize.width) <= tolerance && Math.abs(size.height - snappedSize.height) <= tolerance; return positionMatch && sizeMatch; }