|
| 1 | +import { useRef, useMemo, useLayoutEffect } from 'react'; |
| 2 | +import { useFrame } from '@react-three/fiber'; |
| 3 | +import { Vector3, Color, QuadraticBezierCurve3 } from 'three'; |
| 4 | +import { Line, QuadraticBezierLine, Instance, Instances, Html } from '@react-three/drei'; |
| 5 | +import * as THREE from 'three'; |
| 6 | + |
| 7 | +interface DataLayersProps { |
| 8 | + radius?: number; |
| 9 | +} |
| 10 | + |
| 11 | +// Helper: Lat/Lng -> Vector3 |
| 12 | +function latLngToVector3(lat: number, lng: number, radius: number): Vector3 { |
| 13 | + const phi = (90 - lat) * (Math.PI / 180); |
| 14 | + const theta = (lng + 180) * (Math.PI / 180); |
| 15 | + const x = -(radius * Math.sin(phi) * Math.cos(theta)); |
| 16 | + const z = radius * Math.sin(phi) * Math.sin(theta); |
| 17 | + const y = radius * Math.cos(phi); |
| 18 | + return new Vector3(x, y, z); |
| 19 | +} |
| 20 | + |
| 21 | +// Data Sets |
| 22 | +const EXCHANGES = [ |
| 23 | + // AMERICAS |
| 24 | + { name: 'NYSE (New York)', lat: 40.7128, lng: -74.0060, color: '#00ccff' }, |
| 25 | + { name: 'B3 (Sao Paulo)', lat: -23.5505, lng: -46.6333, color: '#00ccff' }, |
| 26 | + // EMEA |
| 27 | + { name: 'LSE (London)', lat: 51.5074, lng: -0.1278, color: '#00ccff' }, |
| 28 | + { name: 'FRA (Frankfurt)', lat: 50.1109, lng: 8.6821, color: '#00ccff' }, // Deutsche Borse |
| 29 | + { name: 'PAR (Paris)', lat: 48.8566, lng: 2.3522, color: '#00ccff' }, // Euronext |
| 30 | + // APAC |
| 31 | + { name: 'JPX (Tokyo)', lat: 35.6762, lng: 139.6503, color: '#ff0055' }, |
| 32 | + { name: 'SGX (Singapore)', lat: 1.3521, lng: 103.8198, color: '#00ccff' }, |
| 33 | + { name: 'HKEX (Hong Kong)', lat: 22.3193, lng: 114.1694, color: '#00ccff' }, |
| 34 | + { name: 'SSE (Shanghai)', lat: 31.2304, lng: 121.4737, color: '#00ccff' }, |
| 35 | + { name: 'NSE (Mumbai)', lat: 19.0760, lng: 72.8777, color: '#00ccff' }, |
| 36 | + { name: 'ASX (Sydney)', lat: -33.8688, lng: 151.2093, color: '#00ccff' }, |
| 37 | +]; |
| 38 | + |
| 39 | +const NODES = [ |
| 40 | + { name: 'AWS-Virgina', lat: 39.0438, lng: -77.4874, type: 'cloud' }, |
| 41 | + { name: 'AWS-Frankfurt', lat: 50.1109, lng: 8.6821, type: 'cloud' }, |
| 42 | + { name: 'GCP-Tokyo', lat: 35.6895, lng: 139.6917, type: 'cloud' }, |
| 43 | + { name: 'Azure-SouthCentral', lat: 29.4241, lng: -98.4936, type: 'cloud' }, |
| 44 | + { name: 'QuanuX-Edge-CHI', lat: 41.8781, lng: -87.6298, type: 'edge' }, // Chicago CME |
| 45 | + { name: 'QuanuX-Edge-LDN', lat: 51.5074, lng: -0.1278, type: 'edge' }, |
| 46 | +]; |
| 47 | + |
| 48 | +const CONNECTIONS = [ |
| 49 | + { from: 'NYSE (New York)', to: 'LSE (London)', latency: '29ms' }, |
| 50 | + { from: 'NYSE (New York)', to: 'QuanuX-Edge-CHI', latency: '4ms' }, |
| 51 | + { from: 'LSE (London)', to: 'FRA (Frankfurt)', latency: '8ms' }, |
| 52 | + { from: 'FRA (Frankfurt)', to: 'SGX (Singapore)', latency: '140ms' }, |
| 53 | + { from: 'JPX (Tokyo)', to: 'SGX (Singapore)', latency: '35ms' }, |
| 54 | + { from: 'JPX (Tokyo)', to: 'HKEX (Hong Kong)', latency: '20ms' }, |
| 55 | + { from: 'QuanuX-Edge-CHI', to: 'Azure-SouthCentral', latency: '12ms' }, |
| 56 | + { from: 'NYSE (New York)', to: 'B3 (Sao Paulo)', latency: '60ms' }, |
| 57 | + { from: 'SGX (Singapore)', to: 'ASX (Sydney)', latency: '90ms' }, |
| 58 | +]; |
| 59 | + |
| 60 | +export function DataLayers({ radius = 5 }: DataLayersProps) { |
| 61 | + return ( |
| 62 | + <group> |
| 63 | + <ExchangeMarkers radius={radius} /> |
| 64 | + <NodeMarkers radius={radius} /> |
| 65 | + <GlobalConnections radius={radius} /> |
| 66 | + <SatelliteSwarm radius={radius} count={1500} /> |
| 67 | + <StarlinkUplinks radius={radius} /> |
| 68 | + </group> |
| 69 | + ); |
| 70 | +} |
| 71 | + |
| 72 | +// --- SUB COMPONENTS --- |
| 73 | + |
| 74 | +function ExchangeMarkers({ radius }: { radius: number }) { |
| 75 | + return ( |
| 76 | + <group> |
| 77 | + {EXCHANGES.map((ex, i) => { |
| 78 | + const pos = latLngToVector3(ex.lat, ex.lng, radius).multiplyScalar(1.0); |
| 79 | + return ( |
| 80 | + <group key={i} position={pos} lookAt={new Vector3(0, 0, 0)}> |
| 81 | + {/* Vertical Pillar */} |
| 82 | + <mesh rotation={[Math.PI / 2, 0, 0]} position={[0, 0, 0.6]}> |
| 83 | + <cylinderGeometry args={[0.03, 0.03, 1.2, 6]} /> |
| 84 | + <meshBasicMaterial color={ex.color} opacity={0.6} transparent /> |
| 85 | + </mesh> |
| 86 | + {/* Label */} |
| 87 | + <group position={[0, 0, 1.3]} rotation={[Math.PI / 2, Math.PI, 0]}> {/* Adjusted for billboard logic implicitly via lookAt? No, Html handles it */} |
| 88 | + {/* Using simple meshes for markers, labels could be HTML but trying to keep it performance heavy 3D? Html is better */} |
| 89 | + </group> |
| 90 | + </group> |
| 91 | + ); |
| 92 | + })} |
| 93 | + </group> |
| 94 | + ) |
| 95 | +} |
| 96 | + |
| 97 | +function NodeMarkers({ radius }: { radius: number }) { |
| 98 | + return ( |
| 99 | + <group> |
| 100 | + {NODES.map((node, i) => { |
| 101 | + const pos = latLngToVector3(node.lat, node.lng, radius).multiplyScalar(1.0); |
| 102 | + const color = node.type === 'edge' ? '#00ff88' : '#aa00ff'; // Green Edge, Purple Cloud |
| 103 | + return ( |
| 104 | + <mesh key={i} position={pos}> |
| 105 | + <boxGeometry args={[0.12, 0.12, 0.12]} /> |
| 106 | + <meshStandardMaterial color={color} emissive={color} emissiveIntensity={3} toneMapped={false} /> |
| 107 | + </mesh> |
| 108 | + ); |
| 109 | + })} |
| 110 | + </group> |
| 111 | + ) |
| 112 | +} |
| 113 | + |
| 114 | +function GlobalConnections({ radius }: { radius: number }) { |
| 115 | + // Collect all points |
| 116 | + const pointsMap = useMemo(() => { |
| 117 | + const map = new Map<string, { lat: number, lng: number }>(); |
| 118 | + [...EXCHANGES, ...NODES].forEach(p => map.set(p.name, p)); |
| 119 | + return map; |
| 120 | + }, []); |
| 121 | + |
| 122 | + return ( |
| 123 | + <group> |
| 124 | + {CONNECTIONS.map((conn, i) => { |
| 125 | + const start = pointsMap.get(conn.from); |
| 126 | + const end = pointsMap.get(conn.to); |
| 127 | + if (!start || !end) return null; |
| 128 | + |
| 129 | + const startPos = latLngToVector3(start.lat, start.lng, radius); |
| 130 | + const endPos = latLngToVector3(end.lat, end.lng, radius); |
| 131 | + |
| 132 | + // Calculate control point (midpoint projected out) |
| 133 | + const mid = startPos.clone().add(endPos).multiplyScalar(0.5).normalize().multiplyScalar(radius * 1.3); |
| 134 | + |
| 135 | + return ( |
| 136 | + <group key={i}> |
| 137 | + <QuadraticBezierLine |
| 138 | + start={startPos} |
| 139 | + end={endPos} |
| 140 | + mid={mid} |
| 141 | + color="#ffffff" |
| 142 | + lineWidth={1} |
| 143 | + dashed |
| 144 | + dashScale={5} |
| 145 | + gapSize={3} |
| 146 | + opacity={0.2} |
| 147 | + transparent |
| 148 | + /> |
| 149 | + {conn.latency && ( |
| 150 | + <Html position={mid} center> |
| 151 | + <div style={{ color: 'white', fontSize: '0.2em', whiteSpace: 'nowrap', textShadow: '0 0 2px black' }}> |
| 152 | + {conn.latency} |
| 153 | + </div> |
| 154 | + </Html> |
| 155 | + )} |
| 156 | + </group> |
| 157 | + ); |
| 158 | + })} |
| 159 | + </group> |
| 160 | + ); |
| 161 | +} |
| 162 | + |
| 163 | +function SatelliteSwarm({ radius, count }: { radius: number, count: number }) { |
| 164 | + const meshRef = useRef<THREE.InstancedMesh>(null); |
| 165 | + const dummy = useMemo(() => new THREE.Object3D(), []); |
| 166 | + |
| 167 | + // Static Random Positions for now, animated rotation of the whole group |
| 168 | + const particles = useMemo(() => { |
| 169 | + const temp = []; |
| 170 | + for (let i = 0; i < count; i++) { |
| 171 | + const r = radius * (1.15 + Math.random() * 0.2); // Altitude 1.15x - 1.35x |
| 172 | + const theta = Math.random() * Math.PI * 2; |
| 173 | + const phi = Math.acos(2 * Math.random() - 1); |
| 174 | + |
| 175 | + const x = r * Math.sin(phi) * Math.cos(theta); |
| 176 | + const y = r * Math.sin(phi) * Math.sin(theta); |
| 177 | + const z = r * Math.cos(phi); |
| 178 | + temp.push({ x, y, z }); |
| 179 | + } |
| 180 | + return temp; |
| 181 | + }, [count, radius]); |
| 182 | + |
| 183 | + useLayoutEffect(() => { |
| 184 | + if (!meshRef.current) return; |
| 185 | + particles.forEach((p, i) => { |
| 186 | + dummy.position.set(p.x, p.y, p.z); |
| 187 | + dummy.lookAt(0, 0, 0); |
| 188 | + dummy.scale.setScalar(Math.random() > 0.9 ? 1.5 : 0.6); |
| 189 | + dummy.updateMatrix(); |
| 190 | + meshRef.current?.setMatrixAt(i, dummy.matrix); |
| 191 | + }); |
| 192 | + meshRef.current.instanceMatrix.needsUpdate = true; |
| 193 | + }, [particles, dummy]); |
| 194 | + |
| 195 | + useFrame((state) => { |
| 196 | + if (!meshRef.current) return; |
| 197 | + meshRef.current.rotation.y -= 0.0002; // Orbit |
| 198 | + }); |
| 199 | + |
| 200 | + return ( |
| 201 | + <instancedMesh ref={meshRef} args={[undefined, undefined, count]}> |
| 202 | + <dodecahedronGeometry args={[0.02, 0]} /> |
| 203 | + <meshBasicMaterial color="#ffcc00" transparent opacity={0.6} /> |
| 204 | + </instancedMesh> |
| 205 | + ); |
| 206 | +} |
| 207 | + |
| 208 | +// Visualize "Uplinks" from QuanuX Nodes to nearby Satellites |
| 209 | +function StarlinkUplinks({ radius }: { radius: number }) { |
| 210 | + // For each QuanuX node, find a "satellite" position directly above it at orbit altitude |
| 211 | + const links = useMemo(() => { |
| 212 | + return NODES.map(node => { |
| 213 | + const start = latLngToVector3(node.lat, node.lng, radius); |
| 214 | + // Simulate a satellite being roughly above |
| 215 | + const end = start.clone().multiplyScalar(1.25); |
| 216 | + return { start, end }; |
| 217 | + }); |
| 218 | + }, [radius]); |
| 219 | + |
| 220 | + return ( |
| 221 | + <group> |
| 222 | + {links.map((link, i) => ( |
| 223 | + <group key={i}> |
| 224 | + <Line |
| 225 | + points={[link.start, link.end]} |
| 226 | + color="#00ff88" |
| 227 | + opacity={0.4} |
| 228 | + transparent |
| 229 | + lineWidth={1} |
| 230 | + /> |
| 231 | + {/* Pulsing signal packet would be nice here, but simple line for now */} |
| 232 | + </group> |
| 233 | + ))} |
| 234 | + </group> |
| 235 | + ) |
| 236 | +} |
0 commit comments