163 lines
4.4 KiB
TypeScript
163 lines
4.4 KiB
TypeScript
/**
|
|
* 대시보드 위젯 충돌 감지 및 자동 재배치 유틸리티
|
|
*/
|
|
|
|
import { DashboardElement } from "./types";
|
|
|
|
export interface Rectangle {
|
|
x: number;
|
|
y: number;
|
|
width: number;
|
|
height: number;
|
|
}
|
|
|
|
/**
|
|
* 두 사각형이 겹치는지 확인 (여유있는 충돌 감지)
|
|
* @param rect1 첫 번째 사각형
|
|
* @param rect2 두 번째 사각형
|
|
* @param cellSize 한 그리드 칸의 크기 (기본: 130px)
|
|
*/
|
|
export function isColliding(rect1: Rectangle, rect2: Rectangle, cellSize: number = 130): boolean {
|
|
// 겹친 영역 계산
|
|
const overlapX = Math.max(
|
|
0,
|
|
Math.min(rect1.x + rect1.width, rect2.x + rect2.width) - Math.max(rect1.x, rect2.x)
|
|
);
|
|
const overlapY = Math.max(
|
|
0,
|
|
Math.min(rect1.y + rect1.height, rect2.y + rect2.height) - Math.max(rect1.y, rect2.y)
|
|
);
|
|
|
|
// 큰 그리드의 절반(cellSize/2 ≈ 65px) 이상 겹쳐야 충돌로 간주
|
|
const collisionThreshold = Math.floor(cellSize / 2);
|
|
return overlapX >= collisionThreshold && overlapY >= collisionThreshold;
|
|
}
|
|
|
|
/**
|
|
* 특정 위젯과 충돌하는 다른 위젯들을 찾기
|
|
*/
|
|
export function findCollisions(
|
|
element: DashboardElement,
|
|
allElements: DashboardElement[],
|
|
cellSize: number = 130,
|
|
excludeId?: string
|
|
): DashboardElement[] {
|
|
const elementRect: Rectangle = {
|
|
x: element.position.x,
|
|
y: element.position.y,
|
|
width: element.size.width,
|
|
height: element.size.height,
|
|
};
|
|
|
|
return allElements.filter((other) => {
|
|
if (other.id === element.id || other.id === excludeId) {
|
|
return false;
|
|
}
|
|
|
|
const otherRect: Rectangle = {
|
|
x: other.position.x,
|
|
y: other.position.y,
|
|
width: other.size.width,
|
|
height: other.size.height,
|
|
};
|
|
|
|
return isColliding(elementRect, otherRect, cellSize);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 충돌을 해결하기 위해 위젯을 아래로 이동
|
|
*/
|
|
export function resolveCollisionVertically(
|
|
movingElement: DashboardElement,
|
|
collidingElement: DashboardElement,
|
|
gridSize: number = 10
|
|
): { x: number; y: number } {
|
|
// 충돌하는 위젯 아래로 이동
|
|
const newY = collidingElement.position.y + collidingElement.size.height + gridSize;
|
|
|
|
return {
|
|
x: collidingElement.position.x,
|
|
y: Math.round(newY / gridSize) * gridSize, // 그리드에 스냅
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 여러 위젯의 충돌을 재귀적으로 해결
|
|
*/
|
|
export function resolveAllCollisions(
|
|
elements: DashboardElement[],
|
|
movedElementId: string,
|
|
subGridSize: number = 10,
|
|
canvasWidth: number = 1560,
|
|
cellSize: number = 130,
|
|
maxIterations: number = 50
|
|
): DashboardElement[] {
|
|
let result = [...elements];
|
|
let iterations = 0;
|
|
|
|
// 이동한 위젯부터 시작
|
|
const movedIndex = result.findIndex((el) => el.id === movedElementId);
|
|
if (movedIndex === -1) return result;
|
|
|
|
// Y 좌표로 정렬 (위에서 아래로 처리)
|
|
const sortedIndices = result
|
|
.map((el, idx) => ({ el, idx }))
|
|
.sort((a, b) => a.el.position.y - b.el.position.y)
|
|
.map((item) => item.idx);
|
|
|
|
while (iterations < maxIterations) {
|
|
let hasCollision = false;
|
|
|
|
for (const idx of sortedIndices) {
|
|
const element = result[idx];
|
|
const collisions = findCollisions(element, result, cellSize);
|
|
|
|
if (collisions.length > 0) {
|
|
hasCollision = true;
|
|
|
|
// 첫 번째 충돌만 처리 (가장 위에 있는 것)
|
|
const collision = collisions.sort((a, b) => a.position.y - b.position.y)[0];
|
|
|
|
// 충돌하는 위젯을 아래로 이동
|
|
const collisionIdx = result.findIndex((el) => el.id === collision.id);
|
|
if (collisionIdx !== -1) {
|
|
const newY = element.position.y + element.size.height + subGridSize;
|
|
|
|
result[collisionIdx] = {
|
|
...result[collisionIdx],
|
|
position: {
|
|
...result[collisionIdx].position,
|
|
y: Math.round(newY / subGridSize) * subGridSize,
|
|
},
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!hasCollision) break;
|
|
iterations++;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* 위젯이 캔버스 경계를 벗어나지 않도록 제한
|
|
*/
|
|
export function constrainToCanvas(
|
|
element: DashboardElement,
|
|
canvasWidth: number,
|
|
canvasHeight: number,
|
|
gridSize: number = 10
|
|
): { x: number; y: number } {
|
|
const maxX = canvasWidth - element.size.width;
|
|
const maxY = canvasHeight - element.size.height;
|
|
|
|
return {
|
|
x: Math.max(0, Math.min(Math.round(element.position.x / gridSize) * gridSize, maxX)),
|
|
y: Math.max(0, Math.min(Math.round(element.position.y / gridSize) * gridSize, maxY)),
|
|
};
|
|
}
|
|
|