File size: 3,332 Bytes
8a01471
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
import { useRef, useEffect } from 'react';
import { useThree, useFrame } from '@react-three/fiber';
import { OrbitControls as DreiOrbitControls } from '@react-three/drei';
import * as THREE from 'three';
import { useVisualizerStore } from '@/core/store';

/**
 * Camera controls component with orbit, zoom, and pan
 */
export function CameraControls() {
  const controlsRef = useRef<any>(null);
  const model = useVisualizerStore(state => state.model);
  const setCameraPosition = useVisualizerStore(state => state.setCameraPosition);
  const setCameraTarget = useVisualizerStore(state => state.setCameraTarget);
  
  const { camera: threeCamera } = useThree();
  
  // Update store when camera moves
  useFrame(() => {
    if (controlsRef.current) {
      const pos = threeCamera.position;
      const target = controlsRef.current.target;
      
      // Debounce updates
      setCameraPosition({ x: pos.x, y: pos.y, z: pos.z });
      setCameraTarget({ x: target.x, y: target.y, z: target.z });
    }
  });
  
  // Reset camera when model changes
  useEffect(() => {
    if (model && controlsRef.current) {
      // Compute camera position to frame the model
      const nodeCount = model.graph.nodes.length;
      const distance = Math.max(nodeCount * 1.5, 15);
      
      threeCamera.position.set(0, distance * 0.3, distance);
      controlsRef.current.target.set(0, -nodeCount * 0.5, 0);
      controlsRef.current.update();
    }
  }, [model, threeCamera]);
  
  return (
    <DreiOrbitControls
      ref={controlsRef}
      enablePan={true}
      enableZoom={true}
      enableRotate={true}
      minDistance={2}
      maxDistance={100}
      minPolarAngle={0}
      maxPolarAngle={Math.PI}
      dampingFactor={0.1}
      rotateSpeed={0.5}
      panSpeed={0.5}
      zoomSpeed={0.8}
    />
  );
}

/**
 * Camera animation for transitions
 */
export function useCameraAnimation() {
  const { camera } = useThree();
  const targetRef = useRef<THREE.Vector3 | null>(null);
  const lookAtRef = useRef<THREE.Vector3 | null>(null);
  const progressRef = useRef(0);
  
  useFrame((_, delta) => {
    if (targetRef.current && progressRef.current < 1) {
      progressRef.current += delta * 2;
      const t = Math.min(progressRef.current, 1);
      const eased = 1 - Math.pow(1 - t, 3); // Ease out cubic
      
      camera.position.lerp(targetRef.current, eased);
      
      if (lookAtRef.current) {
        camera.lookAt(lookAtRef.current);
      }
      
      if (t >= 1) {
        targetRef.current = null;
        lookAtRef.current = null;
      }
    }
  });
  
  const animateTo = (position: THREE.Vector3, lookAt?: THREE.Vector3) => {
    targetRef.current = position;
    lookAtRef.current = lookAt || null;
    progressRef.current = 0;
  };
  
  return { animateTo };
}

/**
 * Focus camera on a specific node
 */
export function useFocusNode() {
  const computedNodes = useVisualizerStore(state => state.computedNodes);
  const { animateTo } = useCameraAnimation();
  
  const focusNode = (nodeId: string) => {
    const node = computedNodes.get(nodeId);
    if (!node) return;
    
    const { x, y, z } = node.computedPosition;
    const targetPos = new THREE.Vector3(x, y + 5, z + 10);
    const lookAtPos = new THREE.Vector3(x, y, z);
    
    animateTo(targetPos, lookAtPos);
  };
  
  return { focusNode };
}