ERP-node/frontend/components/admin/dashboard/collisionUtils.ts

163 lines
4.4 KiB
TypeScript
Raw Normal View History

/**
*
*/
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)),
};
}