중력 적용 및 요소 쌓기 구현
This commit is contained in:
parent
f0bb349c8c
commit
3b5f0b638f
|
|
@ -73,39 +73,50 @@ function MaterialBox({
|
|||
const mouseStartPos = useRef<{ x: number; y: number }>({ x: 0, y: 0 });
|
||||
const { camera, gl } = useThree();
|
||||
|
||||
// 특정 좌표에 다른 요소가 있는지 확인 (AABB 충돌 감지)
|
||||
const checkCollision = (x: number, z: number): boolean => {
|
||||
// 특정 좌표에 요소를 배치할 수 있는지 확인하고, 필요하면 Y 위치를 조정
|
||||
const checkCollisionAndAdjustY = (x: number, y: number, z: number): { hasCollision: boolean; adjustedY: number } => {
|
||||
const mySize = placement.size_x || gridSize; // 내 크기 (5)
|
||||
const myHalfSize = mySize / 2; // 2.5
|
||||
const mySizeY = placement.size_y || gridSize; // 내 높이 (5)
|
||||
|
||||
return allPlacements.some((p) => {
|
||||
// 자기 자신은 제외 (엄격한 비교)
|
||||
let maxYBelow = gridSize / 2; // 기본 바닥 높이 (2.5)
|
||||
|
||||
for (const p of allPlacements) {
|
||||
// 자기 자신은 제외
|
||||
if (Number(p.id) === Number(placement.id)) {
|
||||
return false;
|
||||
continue;
|
||||
}
|
||||
|
||||
const pSize = p.size_x || gridSize; // 상대방 크기 (5)
|
||||
const pHalfSize = pSize / 2; // 2.5
|
||||
const pSizeY = p.size_y || gridSize; // 상대방 높이 (5)
|
||||
|
||||
// AABB (Axis-Aligned Bounding Box) 충돌 감지
|
||||
// 두 박스가 겹치는지 확인
|
||||
const isOverlapping =
|
||||
// XZ 평면에서 겹치는지 확인
|
||||
const isXZOverlapping =
|
||||
Math.abs(x - p.position_x) < myHalfSize + pHalfSize && // X축 겹침
|
||||
Math.abs(z - p.position_z) < myHalfSize + pHalfSize; // Z축 겹침
|
||||
|
||||
if (isOverlapping) {
|
||||
console.log("🔴 충돌 감지! (AABB)", {
|
||||
current: `${placement.id} at (${x}, ${z}) size=${mySize}`,
|
||||
blocking: `${p.id} at (${p.position_x}, ${p.position_z}) size=${pSize}`,
|
||||
distance: {
|
||||
x: Math.abs(x - p.position_x),
|
||||
z: Math.abs(z - p.position_z),
|
||||
},
|
||||
});
|
||||
}
|
||||
if (isXZOverlapping) {
|
||||
// 같은 XZ 위치에 요소가 있음
|
||||
// 그 요소의 윗면 높이 계산 (중심 + 높이/2)
|
||||
const topOfOtherElement = p.position_y + pSizeY / 2;
|
||||
// 내가 올라갈 Y 위치는 윗면 + 내 높이/2
|
||||
const myYOnTop = topOfOtherElement + mySizeY / 2;
|
||||
|
||||
return isOverlapping;
|
||||
});
|
||||
// 가장 높은 위치 기록
|
||||
if (myYOnTop > maxYBelow) {
|
||||
maxYBelow = myYOnTop;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 요청한 Y와 조정된 Y가 다르면 충돌로 간주 (위로 올려야 함)
|
||||
const needsAdjustment = Math.abs(y - maxYBelow) > 0.1;
|
||||
|
||||
return {
|
||||
hasCollision: needsAdjustment,
|
||||
adjustedY: maxYBelow,
|
||||
};
|
||||
};
|
||||
|
||||
// 드래그 중이 아닐 때만 위치 동기화
|
||||
|
|
@ -159,8 +170,8 @@ function MaterialBox({
|
|||
.multiplyScalar(deltaY * scaleFactor);
|
||||
|
||||
// 최종 위치 계산
|
||||
let finalX = dragStartPos.current.x + moveRight.x + moveForward.x;
|
||||
let finalZ = dragStartPos.current.z + moveRight.z + moveForward.z;
|
||||
const finalX = dragStartPos.current.x + moveRight.x + moveForward.x;
|
||||
const finalZ = dragStartPos.current.z + moveRight.z + moveForward.z;
|
||||
|
||||
// NaN 검증
|
||||
if (isNaN(finalX) || isNaN(finalZ)) {
|
||||
|
|
@ -171,12 +182,14 @@ function MaterialBox({
|
|||
const snappedX = snapToGrid(finalX, gridSize);
|
||||
const snappedZ = snapToGrid(finalZ, gridSize);
|
||||
|
||||
// 충돌 체크 (시각 피드백용 - 실제 차단은 마우스 업 시)
|
||||
const hasCollision = checkCollision(snappedX, snappedZ);
|
||||
setIsValidPosition(!hasCollision);
|
||||
// 충돌 체크 및 Y 위치 조정 (시각 피드백용)
|
||||
const { adjustedY } = checkCollisionAndAdjustY(snappedX, dragStartPos.current.y, snappedZ);
|
||||
|
||||
// 즉시 mesh 위치 업데이트 (부드러운 드래그 - 스냅되기 전 위치)
|
||||
meshRef.current.position.set(finalX, dragStartPos.current.y, finalZ);
|
||||
// 시각 피드백: 항상 유효한 위치 (위로 올라가기 때문)
|
||||
setIsValidPosition(true);
|
||||
|
||||
// 즉시 mesh 위치 업데이트 (조정된 Y 위치로)
|
||||
meshRef.current.position.set(finalX, adjustedY, finalZ);
|
||||
|
||||
// ⚠️ 드래그 중에는 상태 업데이트 안 함 (미리보기만)
|
||||
// 실제 저장은 handleGlobalMouseUp에서만 수행
|
||||
|
|
@ -198,31 +211,20 @@ function MaterialBox({
|
|||
const snappedX = snapToGrid(currentPos.x, gridSize);
|
||||
const snappedZ = snapToGrid(currentPos.z, gridSize);
|
||||
|
||||
// 충돌 체크: 최종 위치에서만 체크 (AABB 방식)
|
||||
const hasCollision = checkCollision(snappedX, snappedZ);
|
||||
// Y 위치 조정 (마인크래프트처럼 쌓기)
|
||||
const { adjustedY } = checkCollisionAndAdjustY(snappedX, currentPos.y, snappedZ);
|
||||
|
||||
if (hasCollision) {
|
||||
// ⛔ 충돌 시: 원래 위치로 되돌리고 저장 안 함
|
||||
console.log("⛔ 충돌! 원래 위치로 복원:", dragStartPos.current);
|
||||
meshRef.current.position.set(dragStartPos.current.x, dragStartPos.current.y, dragStartPos.current.z);
|
||||
setIsValidPosition(true);
|
||||
// 충돌 감지 콜백 호출 (Toast 알림)
|
||||
if (onCollisionDetected) {
|
||||
onCollisionDetected();
|
||||
}
|
||||
// ⚠️ 중요: onDrag 호출하지 않음 (상태 업데이트 안 함)
|
||||
} else {
|
||||
// ✅ 충돌 없음: 스냅된 위치로 최종 설정하고 저장
|
||||
console.log("✅ 충돌 없음! 저장:", { x: snappedX, y: dragStartPos.current.y, z: snappedZ });
|
||||
meshRef.current.position.set(snappedX, dragStartPos.current.y, snappedZ);
|
||||
// 최종 위치 저장 (이것만 실제 상태 업데이트)
|
||||
if (onDrag) {
|
||||
onDrag({
|
||||
x: snappedX,
|
||||
y: dragStartPos.current.y,
|
||||
z: snappedZ,
|
||||
});
|
||||
}
|
||||
// ✅ 항상 배치 가능 (위로 올라가므로)
|
||||
console.log("✅ 배치 완료! 저장:", { x: snappedX, y: adjustedY, z: snappedZ });
|
||||
meshRef.current.position.set(snappedX, adjustedY, snappedZ);
|
||||
|
||||
// 최종 위치 저장 (조정된 Y 위치로)
|
||||
if (onDrag) {
|
||||
onDrag({
|
||||
x: snappedX,
|
||||
y: adjustedY,
|
||||
z: snappedZ,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// 클릭만 한 경우: 원래 위치 유지 (아무것도 안 함)
|
||||
|
|
@ -387,10 +389,14 @@ function Scene({
|
|||
enablePan={true}
|
||||
enableZoom={true}
|
||||
enableRotate={true}
|
||||
minDistance={10}
|
||||
minDistance={8}
|
||||
maxDistance={200}
|
||||
maxPolarAngle={Math.PI / 2}
|
||||
enabled={!isDraggingAny}
|
||||
reverseOrbit={true} // 드래그 방향 반전 (자연스러운 이동)
|
||||
screenSpacePanning={true} // 화면 공간 패닝
|
||||
panSpeed={0.8} // 패닝 속도 (기본값 1.0, 낮을수록 느림)
|
||||
rotateSpeed={0.5} // 회전 속도
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -180,12 +180,62 @@ export default function YardEditor({ layout, onBack }: YardEditorProps) {
|
|||
setDeleteConfirmDialog({ open: true, placementId });
|
||||
};
|
||||
|
||||
// 중력 적용: 삭제된 요소 위에 있던 요소들을 아래로 내림
|
||||
const applyGravity = (deletedPlacement: YardPlacement, remainingPlacements: YardPlacement[]) => {
|
||||
const gridSize = 5;
|
||||
const halfSize = gridSize / 2;
|
||||
|
||||
return remainingPlacements.map((p) => {
|
||||
// 삭제된 요소와 XZ가 겹치는지 확인
|
||||
const isXZOverlapping =
|
||||
Math.abs(p.position_x - deletedPlacement.position_x) < gridSize &&
|
||||
Math.abs(p.position_z - deletedPlacement.position_z) < gridSize;
|
||||
|
||||
// 삭제된 요소보다 위에 있는지 확인
|
||||
const isAbove = p.position_y > deletedPlacement.position_y;
|
||||
|
||||
if (isXZOverlapping && isAbove) {
|
||||
// 아래로 내림: 삭제된 요소의 크기만큼
|
||||
const fallDistance = deletedPlacement.size_y || gridSize;
|
||||
const newY = Math.max(halfSize, p.position_y - fallDistance); // 바닥(2.5) 아래로는 안 내려감
|
||||
|
||||
return {
|
||||
...p,
|
||||
position_y: newY,
|
||||
};
|
||||
}
|
||||
|
||||
return p;
|
||||
});
|
||||
};
|
||||
|
||||
// 요소 삭제 확정 (로컬 상태에서만 삭제, 저장 시 서버에 반영)
|
||||
const confirmDeletePlacement = () => {
|
||||
const { placementId } = deleteConfirmDialog;
|
||||
if (placementId === null) return;
|
||||
|
||||
setPlacements((prev) => prev.filter((p) => p.id !== placementId));
|
||||
setPlacements((prev) => {
|
||||
const deletedPlacement = prev.find((p) => p.id === placementId);
|
||||
if (!deletedPlacement) return prev;
|
||||
|
||||
// 삭제 후 남은 요소들
|
||||
const remaining = prev.filter((p) => p.id !== placementId);
|
||||
|
||||
// 중력 적용 (재귀적으로 계속 적용)
|
||||
let result = remaining;
|
||||
let hasChanges = true;
|
||||
|
||||
// 모든 요소가 안정될 때까지 반복
|
||||
while (hasChanges) {
|
||||
const before = JSON.stringify(result.map((p) => p.position_y));
|
||||
result = applyGravity(deletedPlacement, result);
|
||||
const after = JSON.stringify(result.map((p) => p.position_y));
|
||||
hasChanges = before !== after;
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
if (selectedPlacement?.id === placementId) {
|
||||
setSelectedPlacement(null);
|
||||
setShowConfigPanel(false);
|
||||
|
|
|
|||
Loading…
Reference in New Issue