167 lines
4.5 KiB
TypeScript
167 lines
4.5 KiB
TypeScript
/**
|
|
* 공간적 종속성 검증 유틸리티
|
|
*
|
|
* 하위 영역이 상위 영역 내부에 배치되는지 검증
|
|
*/
|
|
|
|
export interface SpatialObject {
|
|
id: number;
|
|
position: { x: number; y: number; z: number };
|
|
size: { x: number; y: number; z: number };
|
|
hierarchyLevel: number;
|
|
parentId?: number;
|
|
parentKey?: string; // 외부 DB 키 (데이터 바인딩용)
|
|
}
|
|
|
|
/**
|
|
* 객체 A가 객체 B 안에 포함되는지 확인 (AABB)
|
|
*/
|
|
export function isContainedIn(child: SpatialObject, parent: SpatialObject): boolean {
|
|
// AABB (Axis-Aligned Bounding Box) 계산
|
|
const childMin = {
|
|
x: child.position.x - child.size.x / 2,
|
|
z: child.position.z - child.size.z / 2,
|
|
};
|
|
const childMax = {
|
|
x: child.position.x + child.size.x / 2,
|
|
z: child.position.z + child.size.z / 2,
|
|
};
|
|
|
|
const parentMin = {
|
|
x: parent.position.x - parent.size.x / 2,
|
|
z: parent.position.z - parent.size.z / 2,
|
|
};
|
|
const parentMax = {
|
|
x: parent.position.x + parent.size.x / 2,
|
|
z: parent.position.z + parent.size.z / 2,
|
|
};
|
|
|
|
// 자식 객체의 모든 모서리가 부모 객체 내부에 있어야 함 (XZ 평면에서)
|
|
return (
|
|
childMin.x >= parentMin.x &&
|
|
childMax.x <= parentMax.x &&
|
|
childMin.z >= parentMin.z &&
|
|
childMax.z <= parentMax.z
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 드래그 시 부모 영역을 찾아서 검증
|
|
* @param child 드래그 중인 자식 객체
|
|
* @param allObjects 모든 배치된 객체들
|
|
* @param hierarchyLevels 계층 레벨 설정 (1, 2, 3, ...)
|
|
* @returns 유효한 부모 객체 또는 null
|
|
*/
|
|
export function findValidParent(
|
|
child: SpatialObject,
|
|
allObjects: SpatialObject[],
|
|
hierarchyLevels: number
|
|
): SpatialObject | null {
|
|
// 최상위 레벨(레벨 1)은 부모가 없음
|
|
if (child.hierarchyLevel === 1) {
|
|
return null;
|
|
}
|
|
|
|
// 부모 레벨 (자신보다 1단계 위)
|
|
const parentLevel = child.hierarchyLevel - 1;
|
|
|
|
// 부모 레벨의 모든 객체 중에서 포함하는 객체 찾기
|
|
const possibleParents = allObjects.filter(
|
|
(obj) => obj.hierarchyLevel === parentLevel
|
|
);
|
|
|
|
for (const parent of possibleParents) {
|
|
if (isContainedIn(child, parent)) {
|
|
return parent;
|
|
}
|
|
}
|
|
|
|
// 포함하는 부모가 없으면 null
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* 드래그 종료 시 공간적 종속성 검증
|
|
* @param child 드래그 종료된 자식 객체
|
|
* @param allObjects 모든 배치된 객체들
|
|
* @returns { valid: boolean, parent: SpatialObject | null }
|
|
*/
|
|
export function validateSpatialContainment(
|
|
child: SpatialObject,
|
|
allObjects: SpatialObject[]
|
|
): { valid: boolean; parent: SpatialObject | null } {
|
|
// 최상위 레벨은 항상 유효
|
|
if (child.hierarchyLevel === 1) {
|
|
return { valid: true, parent: null };
|
|
}
|
|
|
|
const parent = findValidParent(child, allObjects, child.hierarchyLevel);
|
|
|
|
return {
|
|
valid: parent !== null,
|
|
parent: parent,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 부모 객체 이동 시 모든 자식 객체의 위치 재계산
|
|
* @param parent 이동한 부모 객체
|
|
* @param oldPosition 이전 위치
|
|
* @param newPosition 새 위치
|
|
* @param allObjects 모든 배치된 객체들
|
|
* @returns 업데이트된 자식 객체 배열
|
|
*/
|
|
export function updateChildrenPositions(
|
|
parent: SpatialObject,
|
|
oldPosition: { x: number; y: number; z: number },
|
|
newPosition: { x: number; y: number; z: number },
|
|
allObjects: SpatialObject[]
|
|
): SpatialObject[] {
|
|
const delta = {
|
|
x: newPosition.x - oldPosition.x,
|
|
y: newPosition.y - oldPosition.y,
|
|
z: newPosition.z - oldPosition.z,
|
|
};
|
|
|
|
// 직계 자식 (부모 ID가 일치하는 객체)
|
|
const directChildren = allObjects.filter(
|
|
(obj) => obj.parentId === parent.id
|
|
);
|
|
|
|
// 자식들의 위치 업데이트
|
|
return directChildren.map((child) => ({
|
|
...child,
|
|
position: {
|
|
x: child.position.x + delta.x,
|
|
y: child.position.y + delta.y,
|
|
z: child.position.z + delta.z,
|
|
},
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* 특정 객체의 모든 하위 자손 찾기 (재귀)
|
|
* @param parentId 부모 객체 ID
|
|
* @param allObjects 모든 배치된 객체들
|
|
* @returns 모든 하위 자손 객체 배열
|
|
*/
|
|
export function getAllDescendants(
|
|
parentId: number,
|
|
allObjects: SpatialObject[]
|
|
): SpatialObject[] {
|
|
const directChildren = allObjects.filter((obj) => obj.parentId === parentId);
|
|
|
|
let descendants = [...directChildren];
|
|
|
|
// 재귀적으로 손자, 증손자... 찾기
|
|
for (const child of directChildren) {
|
|
const grandChildren = getAllDescendants(child.id, allObjects);
|
|
descendants = [...descendants, ...grandChildren];
|
|
}
|
|
|
|
return descendants;
|
|
}
|
|
|
|
|
|
|