/** * 공간적 종속성 검증 유틸리티 * * 하위 영역이 상위 영역 내부에 배치되는지 검증 */ 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; }