File size: 4,167 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
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
import { useCallback, useEffect } from 'react';
import { useThree } from '@react-three/fiber';
import * as THREE from 'three';
import { useVisualizerStore } from '@/core/store';

/**
 * Hook for raycasting and object picking
 */
export function useRaycast() {
  const { camera, scene, gl } = useThree();
  const raycaster = new THREE.Raycaster();
  const pointer = new THREE.Vector2();
  
  const getIntersections = useCallback((event: MouseEvent | PointerEvent) => {
    const rect = gl.domElement.getBoundingClientRect();
    pointer.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
    pointer.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
    
    raycaster.setFromCamera(pointer, camera);
    return raycaster.intersectObjects(scene.children, true);
  }, [camera, scene, gl]);
  
  return { getIntersections };
}

/**
 * Keyboard shortcuts handler
 */
export function useKeyboardShortcuts() {
  const resetCamera = useVisualizerStore(state => state.resetCamera);
  const selectNode = useVisualizerStore(state => state.selectNode);
  const selection = useVisualizerStore(state => state.selection);
  const computedNodes = useVisualizerStore(state => state.computedNodes);
  const updateConfig = useVisualizerStore(state => state.updateConfig);
  const config = useVisualizerStore(state => state.config);
  
  useEffect(() => {
    const handleKeyDown = (event: KeyboardEvent) => {
      switch (event.key) {
        case 'Escape':
          // Deselect current selection
          selectNode(null);
          break;
        
        case 'r':
        case 'R':
          // Reset camera
          if (!event.ctrlKey && !event.metaKey) {
            resetCamera();
          }
          break;
        
        case 'l':
        case 'L':
          // Toggle labels
          updateConfig({ showLabels: !config.showLabels });
          break;
        
        case 'e':
        case 'E':
          // Toggle edges
          updateConfig({ showEdges: !config.showEdges });
          break;
        
        case 'ArrowUp':
        case 'ArrowDown': {
          // Navigate between nodes
          if (selection.selectedNodeId) {
            const nodeIds = Array.from(computedNodes.keys());
            const currentIndex = nodeIds.indexOf(selection.selectedNodeId);
            const nextIndex = event.key === 'ArrowDown'
              ? Math.min(currentIndex + 1, nodeIds.length - 1)
              : Math.max(currentIndex - 1, 0);
            selectNode(nodeIds[nextIndex]);
          }
          break;
        }
        
        default:
          break;
      }
    };
    
    window.addEventListener('keydown', handleKeyDown);
    return () => window.removeEventListener('keydown', handleKeyDown);
  }, [resetCamera, selectNode, selection, computedNodes, updateConfig, config]);
}

/**
 * Touch gesture handler for mobile
 */
export function useTouchGestures() {
  // Placeholder for touch gesture handling
  // Can be expanded for pinch-to-zoom, two-finger rotate, etc.
}

/**
 * LOD (Level of Detail) manager based on camera distance
 */
export function useLODManager() {
  const { camera } = useThree();
  const computedNodes = useVisualizerStore(state => state.computedNodes);
  const updateNodeLOD = useVisualizerStore(state => state.updateNodeLOD);
  
  // LOD thresholds
  const LOD_DISTANCES = {
    HIGH: 20,    // LOD 0 (full detail) when closer than this
    MEDIUM: 40,  // LOD 1 (medium detail)
    LOW: 80,     // LOD 2 (low detail)
  };
  
  const updateLOD = useCallback(() => {
    const lodMap = new Map<string, number>();
    const cameraPos = camera.position;
    
    computedNodes.forEach((node, id) => {
      const nodePos = new THREE.Vector3(
        node.computedPosition.x,
        node.computedPosition.y,
        node.computedPosition.z
      );
      const distance = cameraPos.distanceTo(nodePos);
      
      let lod = 0;
      if (distance > LOD_DISTANCES.LOW) {
        lod = 2;
      } else if (distance > LOD_DISTANCES.MEDIUM) {
        lod = 1;
      }
      
      lodMap.set(id, lod);
    });
    
    updateNodeLOD(lodMap);
  }, [camera, computedNodes, updateNodeLOD]);
  
  return { updateLOD };
}