302 lines
7.1 KiB
TypeScript
302 lines
7.1 KiB
TypeScript
|
|
import {
|
||
|
|
PopGridPosition,
|
||
|
|
GridMode,
|
||
|
|
GRID_BREAKPOINTS
|
||
|
|
} from "../types/pop-layout";
|
||
|
|
|
||
|
|
// ========================================
|
||
|
|
// 그리드 위치 변환
|
||
|
|
// ========================================
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 12칸 기준 위치를 다른 모드로 변환
|
||
|
|
*/
|
||
|
|
export function convertPositionToMode(
|
||
|
|
position: PopGridPosition,
|
||
|
|
targetMode: GridMode
|
||
|
|
): PopGridPosition {
|
||
|
|
const sourceColumns = 12;
|
||
|
|
const targetColumns = GRID_BREAKPOINTS[targetMode].columns;
|
||
|
|
|
||
|
|
// 같은 칸 수면 그대로 반환
|
||
|
|
if (sourceColumns === targetColumns) {
|
||
|
|
return position;
|
||
|
|
}
|
||
|
|
|
||
|
|
const ratio = targetColumns / sourceColumns;
|
||
|
|
|
||
|
|
// 열 위치 변환
|
||
|
|
let newCol = Math.max(1, Math.ceil((position.col - 1) * ratio) + 1);
|
||
|
|
let newColSpan = Math.max(1, Math.round(position.colSpan * ratio));
|
||
|
|
|
||
|
|
// 범위 초과 방지
|
||
|
|
if (newCol > targetColumns) {
|
||
|
|
newCol = 1;
|
||
|
|
}
|
||
|
|
if (newCol + newColSpan - 1 > targetColumns) {
|
||
|
|
newColSpan = targetColumns - newCol + 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
return {
|
||
|
|
col: newCol,
|
||
|
|
row: position.row,
|
||
|
|
colSpan: Math.max(1, newColSpan),
|
||
|
|
rowSpan: position.rowSpan,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
// ========================================
|
||
|
|
// 겹침 감지 및 해결
|
||
|
|
// ========================================
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 두 위치가 겹치는지 확인
|
||
|
|
*/
|
||
|
|
export function isOverlapping(a: PopGridPosition, b: PopGridPosition): boolean {
|
||
|
|
// 열 겹침 체크
|
||
|
|
const aColEnd = a.col + a.colSpan - 1;
|
||
|
|
const bColEnd = b.col + b.colSpan - 1;
|
||
|
|
const colOverlap = !(aColEnd < b.col || bColEnd < a.col);
|
||
|
|
|
||
|
|
// 행 겹침 체크
|
||
|
|
const aRowEnd = a.row + a.rowSpan - 1;
|
||
|
|
const bRowEnd = b.row + b.rowSpan - 1;
|
||
|
|
const rowOverlap = !(aRowEnd < b.row || bRowEnd < a.row);
|
||
|
|
|
||
|
|
return colOverlap && rowOverlap;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 겹침 해결 (아래로 밀기)
|
||
|
|
*/
|
||
|
|
export function resolveOverlaps(
|
||
|
|
positions: Array<{ id: string; position: PopGridPosition }>,
|
||
|
|
columns: number
|
||
|
|
): Array<{ id: string; position: PopGridPosition }> {
|
||
|
|
// row, col 순으로 정렬
|
||
|
|
const sorted = [...positions].sort((a, b) =>
|
||
|
|
a.position.row - b.position.row || a.position.col - b.position.col
|
||
|
|
);
|
||
|
|
|
||
|
|
const resolved: Array<{ id: string; position: PopGridPosition }> = [];
|
||
|
|
|
||
|
|
sorted.forEach((item) => {
|
||
|
|
let { row, col, colSpan, rowSpan } = item.position;
|
||
|
|
|
||
|
|
// 열이 범위를 초과하면 조정
|
||
|
|
if (col + colSpan - 1 > columns) {
|
||
|
|
colSpan = columns - col + 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 기존 배치와 겹치면 아래로 이동
|
||
|
|
let attempts = 0;
|
||
|
|
const maxAttempts = 100;
|
||
|
|
|
||
|
|
while (attempts < maxAttempts) {
|
||
|
|
const currentPos: PopGridPosition = { col, row, colSpan, rowSpan };
|
||
|
|
const hasOverlap = resolved.some(r => isOverlapping(currentPos, r.position));
|
||
|
|
|
||
|
|
if (!hasOverlap) break;
|
||
|
|
|
||
|
|
row++;
|
||
|
|
attempts++;
|
||
|
|
}
|
||
|
|
|
||
|
|
resolved.push({
|
||
|
|
id: item.id,
|
||
|
|
position: { col, row, colSpan, rowSpan },
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
return resolved;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ========================================
|
||
|
|
// 좌표 변환
|
||
|
|
// ========================================
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 마우스 좌표 → 그리드 좌표 변환
|
||
|
|
*/
|
||
|
|
export function mouseToGridPosition(
|
||
|
|
mouseX: number,
|
||
|
|
mouseY: number,
|
||
|
|
canvasRect: DOMRect,
|
||
|
|
columns: number,
|
||
|
|
rowHeight: number,
|
||
|
|
gap: number,
|
||
|
|
padding: number
|
||
|
|
): { col: number; row: number } {
|
||
|
|
// 캔버스 내 상대 위치
|
||
|
|
const relX = mouseX - canvasRect.left - padding;
|
||
|
|
const relY = mouseY - canvasRect.top - padding;
|
||
|
|
|
||
|
|
// 칸 너비 계산
|
||
|
|
const totalGap = gap * (columns - 1);
|
||
|
|
const colWidth = (canvasRect.width - padding * 2 - totalGap) / columns;
|
||
|
|
|
||
|
|
// 그리드 좌표 계산 (1부터 시작)
|
||
|
|
const col = Math.max(1, Math.min(columns, Math.floor(relX / (colWidth + gap)) + 1));
|
||
|
|
const row = Math.max(1, Math.floor(relY / (rowHeight + gap)) + 1);
|
||
|
|
|
||
|
|
return { col, row };
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 그리드 좌표 → 픽셀 좌표 변환
|
||
|
|
*/
|
||
|
|
export function gridToPixelPosition(
|
||
|
|
col: number,
|
||
|
|
row: number,
|
||
|
|
colSpan: number,
|
||
|
|
rowSpan: number,
|
||
|
|
canvasWidth: number,
|
||
|
|
columns: number,
|
||
|
|
rowHeight: number,
|
||
|
|
gap: number,
|
||
|
|
padding: number
|
||
|
|
): { x: number; y: number; width: number; height: number } {
|
||
|
|
const totalGap = gap * (columns - 1);
|
||
|
|
const colWidth = (canvasWidth - padding * 2 - totalGap) / columns;
|
||
|
|
|
||
|
|
return {
|
||
|
|
x: padding + (col - 1) * (colWidth + gap),
|
||
|
|
y: padding + (row - 1) * (rowHeight + gap),
|
||
|
|
width: colWidth * colSpan + gap * (colSpan - 1),
|
||
|
|
height: rowHeight * rowSpan + gap * (rowSpan - 1),
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
// ========================================
|
||
|
|
// 위치 검증
|
||
|
|
// ========================================
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 위치가 그리드 범위 내에 있는지 확인
|
||
|
|
*/
|
||
|
|
export function isValidPosition(
|
||
|
|
position: PopGridPosition,
|
||
|
|
columns: number
|
||
|
|
): boolean {
|
||
|
|
return (
|
||
|
|
position.col >= 1 &&
|
||
|
|
position.row >= 1 &&
|
||
|
|
position.colSpan >= 1 &&
|
||
|
|
position.rowSpan >= 1 &&
|
||
|
|
position.col + position.colSpan - 1 <= columns
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 위치를 그리드 범위 내로 조정
|
||
|
|
*/
|
||
|
|
export function clampPosition(
|
||
|
|
position: PopGridPosition,
|
||
|
|
columns: number
|
||
|
|
): PopGridPosition {
|
||
|
|
let { col, row, colSpan, rowSpan } = position;
|
||
|
|
|
||
|
|
// 최소값 보장
|
||
|
|
col = Math.max(1, col);
|
||
|
|
row = Math.max(1, row);
|
||
|
|
colSpan = Math.max(1, colSpan);
|
||
|
|
rowSpan = Math.max(1, rowSpan);
|
||
|
|
|
||
|
|
// 열 범위 초과 방지
|
||
|
|
if (col + colSpan - 1 > columns) {
|
||
|
|
if (col > columns) {
|
||
|
|
col = 1;
|
||
|
|
}
|
||
|
|
colSpan = columns - col + 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
return { col, row, colSpan, rowSpan };
|
||
|
|
}
|
||
|
|
|
||
|
|
// ========================================
|
||
|
|
// 자동 배치
|
||
|
|
// ========================================
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 다음 빈 위치 찾기
|
||
|
|
*/
|
||
|
|
export function findNextEmptyPosition(
|
||
|
|
existingPositions: PopGridPosition[],
|
||
|
|
colSpan: number,
|
||
|
|
rowSpan: number,
|
||
|
|
columns: number
|
||
|
|
): PopGridPosition {
|
||
|
|
let row = 1;
|
||
|
|
let col = 1;
|
||
|
|
|
||
|
|
const maxAttempts = 1000;
|
||
|
|
let attempts = 0;
|
||
|
|
|
||
|
|
while (attempts < maxAttempts) {
|
||
|
|
const candidatePos: PopGridPosition = { col, row, colSpan, rowSpan };
|
||
|
|
|
||
|
|
// 범위 체크
|
||
|
|
if (col + colSpan - 1 > columns) {
|
||
|
|
col = 1;
|
||
|
|
row++;
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 겹침 체크
|
||
|
|
const hasOverlap = existingPositions.some(pos =>
|
||
|
|
isOverlapping(candidatePos, pos)
|
||
|
|
);
|
||
|
|
|
||
|
|
if (!hasOverlap) {
|
||
|
|
return candidatePos;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 다음 위치로 이동
|
||
|
|
col++;
|
||
|
|
if (col + colSpan - 1 > columns) {
|
||
|
|
col = 1;
|
||
|
|
row++;
|
||
|
|
}
|
||
|
|
|
||
|
|
attempts++;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 실패 시 마지막 행에 배치
|
||
|
|
return { col: 1, row: row + 1, colSpan, rowSpan };
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 컴포넌트들을 자동으로 배치
|
||
|
|
*/
|
||
|
|
export function autoLayoutComponents(
|
||
|
|
components: Array<{ id: string; colSpan: number; rowSpan: number }>,
|
||
|
|
columns: number
|
||
|
|
): Array<{ id: string; position: PopGridPosition }> {
|
||
|
|
const result: Array<{ id: string; position: PopGridPosition }> = [];
|
||
|
|
|
||
|
|
let currentRow = 1;
|
||
|
|
let currentCol = 1;
|
||
|
|
|
||
|
|
components.forEach(comp => {
|
||
|
|
// 현재 행에 공간이 부족하면 다음 행으로
|
||
|
|
if (currentCol + comp.colSpan - 1 > columns) {
|
||
|
|
currentRow++;
|
||
|
|
currentCol = 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
result.push({
|
||
|
|
id: comp.id,
|
||
|
|
position: {
|
||
|
|
col: currentCol,
|
||
|
|
row: currentRow,
|
||
|
|
colSpan: comp.colSpan,
|
||
|
|
rowSpan: comp.rowSpan,
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
currentCol += comp.colSpan;
|
||
|
|
});
|
||
|
|
|
||
|
|
return result;
|
||
|
|
}
|