chpark-sync #425
|
|
@ -31,6 +31,7 @@ interface Yard3DCanvasProps {
|
||||||
onPlacementDrag?: (id: number, position: { x: number; y: number; z: number }) => void;
|
onPlacementDrag?: (id: number, position: { x: number; y: number; z: number }) => void;
|
||||||
gridSize?: number; // 그리드 크기 (기본값: 5)
|
gridSize?: number; // 그리드 크기 (기본값: 5)
|
||||||
onCollisionDetected?: () => void; // 충돌 감지 시 콜백
|
onCollisionDetected?: () => void; // 충돌 감지 시 콜백
|
||||||
|
focusOnPlacementId?: number | null; // 카메라가 포커스할 요소 ID
|
||||||
}
|
}
|
||||||
|
|
||||||
// 좌표를 그리드 칸의 중심에 스냅 (마인크래프트 스타일)
|
// 좌표를 그리드 칸의 중심에 스냅 (마인크래프트 스타일)
|
||||||
|
|
@ -467,6 +468,75 @@ function MaterialBox({
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3D 씬 컴포넌트
|
// 3D 씬 컴포넌트
|
||||||
|
// 카메라 포커스 컨트롤러
|
||||||
|
function CameraFocusController({
|
||||||
|
focusOnPlacementId,
|
||||||
|
placements,
|
||||||
|
orbitControlsRef,
|
||||||
|
}: {
|
||||||
|
focusOnPlacementId?: number | null;
|
||||||
|
placements: YardPlacement[];
|
||||||
|
orbitControlsRef: React.RefObject<any>;
|
||||||
|
}) {
|
||||||
|
const { camera } = useThree();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log("🎥 CameraFocusController triggered");
|
||||||
|
console.log(" - focusOnPlacementId:", focusOnPlacementId);
|
||||||
|
console.log(" - orbitControlsRef.current:", orbitControlsRef.current);
|
||||||
|
console.log(" - placements count:", placements.length);
|
||||||
|
|
||||||
|
if (focusOnPlacementId && orbitControlsRef.current) {
|
||||||
|
const targetPlacement = placements.find((p) => p.id === focusOnPlacementId);
|
||||||
|
console.log(" - targetPlacement:", targetPlacement);
|
||||||
|
|
||||||
|
if (targetPlacement) {
|
||||||
|
console.log("✅ Starting camera animation to:", targetPlacement.material_name || targetPlacement.id);
|
||||||
|
|
||||||
|
const controls = orbitControlsRef.current;
|
||||||
|
const targetPosition = new THREE.Vector3(
|
||||||
|
targetPlacement.position_x,
|
||||||
|
targetPlacement.position_y,
|
||||||
|
targetPlacement.position_z,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 카메라 위치 계산 (요소 위에서 약간 비스듬히)
|
||||||
|
const cameraOffset = new THREE.Vector3(15, 15, 15);
|
||||||
|
const newCameraPosition = targetPosition.clone().add(cameraOffset);
|
||||||
|
|
||||||
|
// 부드러운 애니메이션으로 카메라 이동
|
||||||
|
const startPos = camera.position.clone();
|
||||||
|
const startTarget = controls.target.clone();
|
||||||
|
const duration = 1000; // 1초
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
const animate = () => {
|
||||||
|
const elapsed = Date.now() - startTime;
|
||||||
|
const progress = Math.min(elapsed / duration, 1);
|
||||||
|
|
||||||
|
// easeInOutCubic 이징 함수
|
||||||
|
const eased = progress < 0.5 ? 4 * progress * progress * progress : 1 - Math.pow(-2 * progress + 2, 3) / 2;
|
||||||
|
|
||||||
|
// 카메라 위치 보간
|
||||||
|
camera.position.lerpVectors(startPos, newCameraPosition, eased);
|
||||||
|
|
||||||
|
// 카메라 타겟 보간
|
||||||
|
controls.target.lerpVectors(startTarget, targetPosition, eased);
|
||||||
|
controls.update();
|
||||||
|
|
||||||
|
if (progress < 1) {
|
||||||
|
requestAnimationFrame(animate);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
animate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [focusOnPlacementId, placements, camera, orbitControlsRef]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function Scene({
|
function Scene({
|
||||||
placements,
|
placements,
|
||||||
selectedPlacementId,
|
selectedPlacementId,
|
||||||
|
|
@ -474,12 +544,20 @@ function Scene({
|
||||||
onPlacementDrag,
|
onPlacementDrag,
|
||||||
gridSize = 5,
|
gridSize = 5,
|
||||||
onCollisionDetected,
|
onCollisionDetected,
|
||||||
|
focusOnPlacementId,
|
||||||
}: Yard3DCanvasProps) {
|
}: Yard3DCanvasProps) {
|
||||||
const [isDraggingAny, setIsDraggingAny] = useState(false);
|
const [isDraggingAny, setIsDraggingAny] = useState(false);
|
||||||
const orbitControlsRef = useRef<any>(null);
|
const orbitControlsRef = useRef<any>(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{/* 카메라 포커스 컨트롤러 */}
|
||||||
|
<CameraFocusController
|
||||||
|
focusOnPlacementId={focusOnPlacementId}
|
||||||
|
placements={placements}
|
||||||
|
orbitControlsRef={orbitControlsRef}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* 조명 */}
|
{/* 조명 */}
|
||||||
<ambientLight intensity={0.5} />
|
<ambientLight intensity={0.5} />
|
||||||
<directionalLight position={[10, 10, 5]} intensity={1} />
|
<directionalLight position={[10, 10, 5]} intensity={1} />
|
||||||
|
|
@ -551,6 +629,7 @@ export default function Yard3DCanvas({
|
||||||
onPlacementDrag,
|
onPlacementDrag,
|
||||||
gridSize = 5,
|
gridSize = 5,
|
||||||
onCollisionDetected,
|
onCollisionDetected,
|
||||||
|
focusOnPlacementId,
|
||||||
}: Yard3DCanvasProps) {
|
}: Yard3DCanvasProps) {
|
||||||
const handleCanvasClick = (e: any) => {
|
const handleCanvasClick = (e: any) => {
|
||||||
// Canvas의 빈 공간을 클릭했을 때만 선택 해제
|
// Canvas의 빈 공간을 클릭했을 때만 선택 해제
|
||||||
|
|
@ -577,6 +656,7 @@ export default function Yard3DCanvas({
|
||||||
onPlacementDrag={onPlacementDrag}
|
onPlacementDrag={onPlacementDrag}
|
||||||
gridSize={gridSize}
|
gridSize={gridSize}
|
||||||
onCollisionDetected={onCollisionDetected}
|
onCollisionDetected={onCollisionDetected}
|
||||||
|
focusOnPlacementId={focusOnPlacementId}
|
||||||
/>
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</Canvas>
|
</Canvas>
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ export default function YardEditor({ layout, onBack }: YardEditorProps) {
|
||||||
const [placements, setPlacements] = useState<YardPlacement[]>([]);
|
const [placements, setPlacements] = useState<YardPlacement[]>([]);
|
||||||
const [originalPlacements, setOriginalPlacements] = useState<YardPlacement[]>([]); // 원본 데이터 보관
|
const [originalPlacements, setOriginalPlacements] = useState<YardPlacement[]>([]); // 원본 데이터 보관
|
||||||
const [selectedPlacement, setSelectedPlacement] = useState<YardPlacement | null>(null);
|
const [selectedPlacement, setSelectedPlacement] = useState<YardPlacement | null>(null);
|
||||||
|
const [focusPlacementId, setFocusPlacementId] = useState<number | null>(null); // 카메라 포커스할 요소 ID
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
const [showConfigPanel, setShowConfigPanel] = useState(false);
|
const [showConfigPanel, setShowConfigPanel] = useState(false);
|
||||||
|
|
@ -203,9 +204,30 @@ export default function YardEditor({ layout, onBack }: YardEditorProps) {
|
||||||
};
|
};
|
||||||
|
|
||||||
// 요소 선택 (3D 캔버스 또는 목록에서)
|
// 요소 선택 (3D 캔버스 또는 목록에서)
|
||||||
const handleSelectPlacement = (placement: YardPlacement) => {
|
const handleSelectPlacement = (placement: YardPlacement | null) => {
|
||||||
|
console.log("📍 handleSelectPlacement called with:", placement);
|
||||||
|
|
||||||
|
if (!placement) {
|
||||||
|
// 빈 공간 클릭 시 선택 해제
|
||||||
|
console.log(" → Deselecting (null placement)");
|
||||||
|
setSelectedPlacement(null);
|
||||||
|
setShowConfigPanel(false);
|
||||||
|
setFocusPlacementId(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(" → Selecting placement:", placement.id, placement.material_name);
|
||||||
setSelectedPlacement(placement);
|
setSelectedPlacement(placement);
|
||||||
setShowConfigPanel(false); // 선택 시에는 설정 패널 닫기
|
setShowConfigPanel(false); // 선택 시에는 설정 패널 닫기
|
||||||
|
|
||||||
|
console.log(" → Setting focusPlacementId to:", placement.id);
|
||||||
|
setFocusPlacementId(placement.id); // 카메라 포커스
|
||||||
|
|
||||||
|
// 카메라 애니메이션 완료 후 focusPlacementId 초기화 (재클릭 시 다시 포커스 가능)
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log(" → Clearing focusPlacementId");
|
||||||
|
setFocusPlacementId(null);
|
||||||
|
}, 1100); // 애니메이션 시간(1000ms)보다 약간 길게
|
||||||
};
|
};
|
||||||
|
|
||||||
// 설정 버튼 클릭
|
// 설정 버튼 클릭
|
||||||
|
|
@ -500,8 +522,9 @@ export default function YardEditor({ layout, onBack }: YardEditorProps) {
|
||||||
<Yard3DCanvas
|
<Yard3DCanvas
|
||||||
placements={placements}
|
placements={placements}
|
||||||
selectedPlacementId={selectedPlacement?.id || null}
|
selectedPlacementId={selectedPlacement?.id || null}
|
||||||
onPlacementClick={(placement) => handleSelectPlacement(placement as YardPlacement)}
|
onPlacementClick={(placement) => handleSelectPlacement(placement as YardPlacement | null)}
|
||||||
onPlacementDrag={handlePlacementDrag}
|
onPlacementDrag={handlePlacementDrag}
|
||||||
|
focusOnPlacementId={focusPlacementId}
|
||||||
onCollisionDetected={() => {
|
onCollisionDetected={() => {
|
||||||
toast({
|
toast({
|
||||||
title: "배치 불가",
|
title: "배치 불가",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue