ERP-node/frontend/lib/utils/gridUtils.ts

183 lines
4.8 KiB
TypeScript

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;
}