import type { ComponentConfig, GridConfig } from "@/types/report"; /** * 픽셀 좌표를 그리드 좌표로 변환 */ export function pixelToGrid(pixel: number, cellSize: number): number { return Math.round(pixel / cellSize); } /** * 그리드 좌표를 픽셀 좌표로 변환 */ export function gridToPixel(grid: number, cellSize: number): number { return grid * cellSize; } /** * 컴포넌트 위치/크기를 그리드에 스냅 */ export function snapComponentToGrid(component: ComponentConfig, gridConfig: GridConfig): ComponentConfig { if (!gridConfig.snapToGrid) { return component; } // 픽셀 좌표를 그리드 좌표로 변환 const gridX = pixelToGrid(component.x, gridConfig.cellWidth); const gridY = pixelToGrid(component.y, gridConfig.cellHeight); const gridWidth = Math.max(1, pixelToGrid(component.width, gridConfig.cellWidth)); const gridHeight = Math.max(1, pixelToGrid(component.height, gridConfig.cellHeight)); // 그리드 좌표를 다시 픽셀로 변환 return { ...component, gridX, gridY, gridWidth, gridHeight, x: gridToPixel(gridX, gridConfig.cellWidth), y: gridToPixel(gridY, gridConfig.cellHeight), width: gridToPixel(gridWidth, gridConfig.cellWidth), height: gridToPixel(gridHeight, gridConfig.cellHeight), }; } /** * 그리드 충돌 감지 * 두 컴포넌트가 겹치는지 확인 */ export function detectGridCollision( component: ComponentConfig, otherComponents: ComponentConfig[], gridConfig: GridConfig, ): boolean { const comp1GridX = component.gridX ?? pixelToGrid(component.x, gridConfig.cellWidth); const comp1GridY = component.gridY ?? pixelToGrid(component.y, gridConfig.cellHeight); const comp1GridWidth = component.gridWidth ?? pixelToGrid(component.width, gridConfig.cellWidth); const comp1GridHeight = component.gridHeight ?? pixelToGrid(component.height, gridConfig.cellHeight); for (const other of otherComponents) { if (other.id === component.id) continue; const comp2GridX = other.gridX ?? pixelToGrid(other.x, gridConfig.cellWidth); const comp2GridY = other.gridY ?? pixelToGrid(other.y, gridConfig.cellHeight); const comp2GridWidth = other.gridWidth ?? pixelToGrid(other.width, gridConfig.cellWidth); const comp2GridHeight = other.gridHeight ?? pixelToGrid(other.height, gridConfig.cellHeight); // AABB (Axis-Aligned Bounding Box) 충돌 감지 const xOverlap = comp1GridX < comp2GridX + comp2GridWidth && comp1GridX + comp1GridWidth > comp2GridX; const yOverlap = comp1GridY < comp2GridY + comp2GridHeight && comp1GridY + comp1GridHeight > comp2GridY; if (xOverlap && yOverlap) { return true; } } return false; } /** * 페이지 크기 기반 그리드 행/열 계산 */ export function calculateGridDimensions( pageWidth: number, pageHeight: number, cellWidth: number, cellHeight: number, ): { rows: number; columns: number } { return { columns: Math.floor(pageWidth / cellWidth), rows: Math.floor(pageHeight / cellHeight), }; } /** * 기본 그리드 설정 생성 */ export function createDefaultGridConfig(pageWidth: number, pageHeight: number): GridConfig { const cellWidth = 20; const cellHeight = 20; const { rows, columns } = calculateGridDimensions(pageWidth, pageHeight, cellWidth, cellHeight); return { cellWidth, cellHeight, rows, columns, visible: true, snapToGrid: true, gridColor: "#e5e7eb", gridOpacity: 0.5, }; } /** * 위치가 페이지 경계 내에 있는지 확인 */ export function isWithinPageBounds( component: ComponentConfig, pageWidth: number, pageHeight: number, margins: { top: number; bottom: number; left: number; right: number }, ): boolean { const minX = margins.left; const minY = margins.top; const maxX = pageWidth - margins.right; const maxY = pageHeight - margins.bottom; return ( component.x >= minX && component.y >= minY && component.x + component.width <= maxX && component.y + component.height <= maxY ); } /** * 컴포넌트를 페이지 경계 내로 제한 */ export function constrainToPageBounds( component: ComponentConfig, pageWidth: number, pageHeight: number, margins: { top: number; bottom: number; left: number; right: number }, ): ComponentConfig { const minX = margins.left; const minY = margins.top; const maxX = pageWidth - margins.right - component.width; const maxY = pageHeight - margins.bottom - component.height; return { ...component, x: Math.max(minX, Math.min(maxX, component.x)), y: Math.max(minY, Math.min(maxY, component.y)), }; }