import React, { useRef, useEffect, useState } from 'react'; import { Canvas, useFrame } from '@react-three/fiber'; import { OrbitControls, Text } from '@react-three/drei'; import * as THREE from 'three'; interface Body { name: string; position: [number, number, number]; velocity: [number, number, number]; mass: number; } interface SimulationUpdate { step: number; time: number; bodies: Body[]; } interface Props { data: SimulationUpdate; resetTrails?: boolean; // Signal to reset all trails } // Component for rendering a celestial body function CelestialBody({ body, index, scaleFactor, resetTrails }: { body: Body; index: number; scaleFactor: number; resetTrails?: boolean; }) { const meshRef = useRef(null); const trailRef = useRef(null); const trailPoints = useRef([]); const lastPosition = useRef(null); const lastScaleFactor = useRef(scaleFactor); // Color palette for different bodies const colors = [ '#FFD700', // Gold for Sun '#FFA500', // Orange for Mercury '#FF6347', // Tomato for Venus '#4169E1', // Royal Blue for Earth '#FF0000', // Red for Mars '#DAA520', // Goldenrod for Jupiter '#F4A460', // Sandy Brown for Saturn '#40E0D0', // Turquoise for Uranus '#0000FF', // Blue for Neptune '#DDA0DD', // Plum for other bodies ]; const color = colors[index % colors.length]; // Scale positions based on the scale factor // Default scale factor of 1 means 1 AU, scale factor of 0.1 means 0.1 AU, etc. const AU = 1.496e11; // 1 AU in meters const scaledPosition = [ body.position[0] / (AU * scaleFactor), body.position[1] / (AU * scaleFactor), body.position[2] / (AU * scaleFactor) ]; // Calculate sphere size with better scaling for visualization // Make the Sun larger and planets visible let size; if (body.name.toLowerCase().includes('sun')) { size = 0.2; // Sun gets a fixed, visible size } else { // Scale planet sizes logarithmically but make them visible const earthMass = 5.972e24; const massRatio = body.mass / earthMass; size = Math.max(0.03, Math.min(Math.pow(massRatio, 1/3) * 0.08, 0.15)); } // Update trail with AU-scaled positions useEffect(() => { const position = new THREE.Vector3(...scaledPosition); // Check if we need to reset trails const shouldReset = resetTrails || Math.abs(scaleFactor - lastScaleFactor.current) > 0.01 || // Scale factor changed (lastPosition.current && position.distanceTo(lastPosition.current) > 5); // Big position jump if (shouldReset) { // Reset trails for discontinuous changes trailPoints.current = [position.clone()]; } else { // Normal trail update trailPoints.current.push(position.clone()); // Keep trail length manageable if (trailPoints.current.length > 1000) { trailPoints.current.shift(); } } // Update references for next comparison lastPosition.current = position.clone(); lastScaleFactor.current = scaleFactor; // Update trail geometry if (trailRef.current && trailPoints.current.length > 1) { const geometry = new THREE.BufferGeometry().setFromPoints(trailPoints.current); trailRef.current.geometry.dispose(); trailRef.current.geometry = geometry; } }, [body.position, scaleFactor, resetTrails]); return ( {/* Trail */} {trailPoints.current.length > 1 && ( )} {/* Body */} {/* Body label */} {body.name} ); } // Component for the 3D scene function Scene({ data, scaleFactor, resetTrails }: Props & { scaleFactor: number }) { return ( <> {/* Lighting */} {/* Bodies */} {data.bodies.map((body, index) => ( ))} {/* Grid removed for cleaner space view */} {/* Controls */} ); } const SimulationCanvas: React.FC = ({ data, resetTrails }) => { const [scaleFactor, setScaleFactor] = useState(1.0); // 1.0 = 1 AU const [internalResetTrails, setInternalResetTrails] = useState(false); const lastStep = useRef(data.step); const lastScaleFactor = useRef(1.0); // Detect big timeline jumps useEffect(() => { const stepDifference = Math.abs(data.step - lastStep.current); const isBigJump = stepDifference > 100; // Consider jumps > 100 steps as "big" if (isBigJump) { setInternalResetTrails(true); // Reset the flag after a short delay to trigger the effect setTimeout(() => setInternalResetTrails(false), 50); } lastStep.current = data.step; }, [data.step]); // Handle external reset signals and scale changes useEffect(() => { const scaleChanged = Math.abs(scaleFactor - lastScaleFactor.current) > 0.01; if (resetTrails || scaleChanged) { setInternalResetTrails(true); setTimeout(() => setInternalResetTrails(false), 50); } lastScaleFactor.current = scaleFactor; }, [resetTrails, scaleFactor]); const scaleOptions = [ { value: 0.1, label: '0.1 AU', description: 'Inner planets' }, { value: 0.5, label: '0.5 AU', description: 'Close view' }, { value: 1.0, label: '1 AU', description: 'Default' }, { value: 2.0, label: '2 AU', description: 'Wide view' }, { value: 5.0, label: '5 AU', description: 'Outer system' }, { value: 10.0, label: '10 AU', description: 'Very wide' }, ]; const getCurrentScaleLabel = () => { const current = scaleOptions.find(opt => opt.value === scaleFactor); return current ? current.label : `${scaleFactor} AU`; }; return (
{/* Scale control slider */}

View Scale

Scale: {getCurrentScaleLabel()}
setScaleFactor(parseFloat(e.target.value))} style={{ width: '100%', marginBottom: '0.5rem', }} />
Close Far
{/* Overlay information */}
Step: {data.step.toLocaleString()}
Time: {(data.time / 86400).toFixed(2)} days
Bodies: {data.bodies.length}
Scale: 1 unit = {scaleFactor} AU ({(scaleFactor * 150).toFixed(0)}M km)
); }; export default SimulationCanvas;