/** * Neural Network Connection Visualization * Shows dense connections between layers like actual neural networks */ import { useMemo, useRef } from 'react'; import * as THREE from 'three'; import { useFrame } from '@react-three/fiber'; import type { Position3D } from '@/schema/types'; export interface NeuralConnectionProps { sourcePosition: Position3D; targetPosition: Position3D; sourceNeurons?: number; targetNeurons?: number; color?: string; highlighted?: boolean; animated?: boolean; connectionDensity?: number; // 0-1, how many connections to show style?: 'single' | 'dense' | 'bundle'; } /** * Single connection line between layers */ export function SingleConnection({ sourcePosition, targetPosition, color = '#ffffff', highlighted = false, }: NeuralConnectionProps) { const geometry = useMemo(() => { const points = [ new THREE.Vector3(sourcePosition.x, sourcePosition.y, sourcePosition.z), new THREE.Vector3(targetPosition.x, targetPosition.y, targetPosition.z), ]; return new THREE.BufferGeometry().setFromPoints(points); }, [sourcePosition, targetPosition]); return ( ); } /** * Dense connection bundle showing multiple lines */ export function DenseConnection({ sourcePosition, targetPosition, sourceNeurons = 16, targetNeurons = 16, color = '#4488ff', highlighted = false, animated = true, connectionDensity = 0.3, }: NeuralConnectionProps) { const groupRef = useRef(null); const materialRef = useRef(null); // Limit connections for performance const maxConnections = 100; const numConnections = Math.min( Math.floor(sourceNeurons * targetNeurons * connectionDensity), maxConnections ); // Generate connection lines const lines = useMemo(() => { const connections: { start: THREE.Vector3; end: THREE.Vector3 }[] = []; // Calculate source and target layer bounds const sourceSpread = Math.min(Math.sqrt(sourceNeurons) * 0.1, 0.4); const targetSpread = Math.min(Math.sqrt(targetNeurons) * 0.1, 0.4); for (let i = 0; i < numConnections; i++) { // Random positions within layer bounds const srcOffset = { y: (Math.random() - 0.5) * sourceSpread, z: (Math.random() - 0.5) * sourceSpread * 0.5, }; const tgtOffset = { y: (Math.random() - 0.5) * targetSpread, z: (Math.random() - 0.5) * targetSpread * 0.5, }; connections.push({ start: new THREE.Vector3( sourcePosition.x + 0.3, // Offset from layer center sourcePosition.y + srcOffset.y, sourcePosition.z + srcOffset.z ), end: new THREE.Vector3( targetPosition.x - 0.3, // Offset from layer center targetPosition.y + tgtOffset.y, targetPosition.z + tgtOffset.z ), }); } return connections; }, [sourcePosition, targetPosition, sourceNeurons, targetNeurons, numConnections]); // Animate opacity useFrame((state) => { if (animated && materialRef.current) { const pulse = Math.sin(state.clock.elapsedTime * 2) * 0.1 + 0.3; materialRef.current.opacity = highlighted ? 0.8 : pulse; } }); // Create buffer geometry for all lines const geometry = useMemo(() => { const positions: number[] = []; lines.forEach(line => { positions.push(line.start.x, line.start.y, line.start.z); positions.push(line.end.x, line.end.y, line.end.z); }); const geo = new THREE.BufferGeometry(); geo.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3)); return geo; }, [lines]); return ( ); } /** * Bundled connection - shows as a tube/pipe */ export function BundledConnection({ sourcePosition, targetPosition, sourceNeurons = 16, targetNeurons = 16, color = '#4488ff', highlighted = false, animated = true, }: NeuralConnectionProps) { const meshRef = useRef(null); // Calculate bundle thickness based on connection count const connectionStrength = Math.log2(Math.min(sourceNeurons, targetNeurons) + 1) * 0.02; const thickness = Math.max(0.02, Math.min(connectionStrength, 0.1)); // Create tube path const curve = useMemo(() => { const start = new THREE.Vector3(sourcePosition.x, sourcePosition.y, sourcePosition.z); const end = new THREE.Vector3(targetPosition.x, targetPosition.y, targetPosition.z); // Bezier control points for smooth curve const midX = (start.x + end.x) / 2; const control1 = new THREE.Vector3(midX, start.y, start.z); const control2 = new THREE.Vector3(midX, end.y, end.z); return new THREE.CubicBezierCurve3(start, control1, control2, end); }, [sourcePosition, targetPosition]); const geometry = useMemo(() => { return new THREE.TubeGeometry(curve, 20, thickness, 8, false); }, [curve, thickness]); // Animate flow effect useFrame((state) => { if (animated && meshRef.current) { const material = meshRef.current.material as THREE.MeshStandardMaterial; if (material.emissiveIntensity !== undefined) { material.emissiveIntensity = Math.sin(state.clock.elapsedTime * 3) * 0.2 + 0.3; } } }); const baseColor = new THREE.Color(color); return ( ); } /** * Flow particles along connection */ export function FlowParticles({ sourcePosition, targetPosition, color = '#ffffff', particleCount = 5, speed = 1, }: { sourcePosition: Position3D; targetPosition: Position3D; color?: string; particleCount?: number; speed?: number; }) { const particlesRef = useRef(null); // Create particle positions const { positions, offsets } = useMemo(() => { const pos = new Float32Array(particleCount * 3); const off = new Float32Array(particleCount); for (let i = 0; i < particleCount; i++) { off[i] = i / particleCount; // Spread particles along path // Initial positions will be updated in useFrame pos[i * 3] = sourcePosition.x; pos[i * 3 + 1] = sourcePosition.y; pos[i * 3 + 2] = sourcePosition.z; } return { positions: pos, offsets: off }; }, [sourcePosition, particleCount]); const geometry = useMemo(() => { const geo = new THREE.BufferGeometry(); geo.setAttribute('position', new THREE.BufferAttribute(positions, 3)); return geo; }, [positions]); // Animate particles useFrame((state) => { if (particlesRef.current) { const posAttr = particlesRef.current.geometry.getAttribute('position') as THREE.BufferAttribute; for (let i = 0; i < particleCount; i++) { // Calculate t along path (0 to 1) const t = ((state.clock.elapsedTime * speed + offsets[i]) % 1); // Lerp position posAttr.setXYZ( i, sourcePosition.x + (targetPosition.x - sourcePosition.x) * t, sourcePosition.y + (targetPosition.y - sourcePosition.y) * t, sourcePosition.z + (targetPosition.z - sourcePosition.z) * t ); } posAttr.needsUpdate = true; } }); return ( ); } /** * Smart connection that chooses visualization based on layer sizes */ export function SmartConnection({ sourcePosition, targetPosition, sourceNeurons = 16, targetNeurons = 16, color = '#4488ff', highlighted = false, animated = true, style = 'bundle', }: NeuralConnectionProps) { const totalConnections = sourceNeurons * targetNeurons; // Choose visualization based on connection count if (style === 'single' || totalConnections < 50) { return ( ); } if (style === 'dense' || totalConnections < 500) { return ( <> {animated && ( )} ); } // For very dense connections, use bundled representation return ( <> {animated && ( )} ); }