import React, { useState, useEffect } from 'react'; import SimulationCanvas from './components/SimulationCanvas'; import SimulationControls from './components/SimulationControls'; import SimulationList from './components/SimulationList'; import ConfigurationPanel from './components/ConfigurationPanel'; import { Play, Pause, Square } from 'lucide-react'; export interface Body { name: string; mass: number; position: [number, number, number]; velocity: [number, number, number]; } export interface Config { bodies: Body[]; normalization?: { m_0: number; r_0: number; t_0: number; }; } export interface SimulationInfo { id: string; is_running: boolean; current_step: number; playback_step: number; total_steps: number; recorded_steps: number; bodies_count: number; elapsed_time: number; } export interface BodyState { name: string; position: [number, number, number]; velocity: [number, number, number]; mass: number; } export interface SimulationUpdate { step: number; time: number; bodies: BodyState[]; energy?: { kinetic: number; potential: number; total: number; }; } function App() { const [simulations, setSimulations] = useState([]); const [selectedSimulation, setSelectedSimulation] = useState(null); const [currentData, setCurrentData] = useState(null); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [isAutoPlaying, setIsAutoPlaying] = useState(true); // Auto-play timeline by default const [sessionId] = useState(() => { // Generate or retrieve session ID from localStorage let id = localStorage.getItem('orbital-sim-session'); if (!id) { id = crypto.randomUUID(); localStorage.setItem('orbital-sim-session', id); } return id; }); // Helper function to add session header to requests const getHeaders = () => ({ 'Content-Type': 'application/json', 'X-Session-ID': sessionId, }); // Fetch simulations list const fetchSimulations = async () => { try { const response = await fetch('/api/simulations', { headers: { 'X-Session-ID': sessionId }, }); if (response.ok) { const data = await response.json(); setSimulations(data); } } catch (err) { setError('Failed to fetch simulations'); } }; // Create new simulation const createSimulation = async (config: Config, stepSize: number, totalSteps: number) => { setIsLoading(true); setError(null); try { const response = await fetch('/api/simulations', { method: 'POST', headers: getHeaders(), body: JSON.stringify({ config, step_size: stepSize, total_steps: totalSteps, }), }); if (response.ok) { const newSim = await response.json(); setSelectedSimulation(newSim.id); await fetchSimulations(); } else { setError('Failed to create simulation'); } } catch (err) { setError('Failed to create simulation'); } finally { setIsLoading(false); } }; // Control simulation const controlSimulation = async (id: string, action: string) => { try { const response = await fetch(`/api/simulations/${id}/control?action=${action}`, { method: 'POST', headers: { 'X-Session-ID': sessionId }, }); if (response.ok) { await fetchSimulations(); } } catch (err) { setError(`Failed to ${action} simulation`); } }; // Delete simulation const deleteSimulation = async (id: string) => { try { const response = await fetch(`/api/simulations/${id}`, { method: 'DELETE', headers: { 'X-Session-ID': sessionId }, }); if (response.ok) { if (selectedSimulation === id) { setSelectedSimulation(null); setCurrentData(null); } await fetchSimulations(); } } catch (err) { setError('Failed to delete simulation'); } }; // Seek to specific simulation step const seekToStep = async (id: string, step: number) => { try { const response = await fetch(`/api/simulations/${id}/seek?step=${step}`, { method: 'POST', headers: { 'X-Session-ID': sessionId }, }); if (response.ok) { const data = await response.json(); setCurrentData(data); await fetchSimulations(); // Update playback_step in simulation info } else { setError('Failed to seek simulation'); } } catch (err) { setError('Failed to seek simulation'); } }; // Timeline control functions const handleSeek = (step: number) => { if (selectedSimulation) { seekToStep(selectedSimulation, step); } }; const handleToggleAutoPlay = () => { setIsAutoPlaying(!isAutoPlaying); }; const handleRestart = () => { if (selectedSimulation) { seekToStep(selectedSimulation, 0); } }; // Poll simulation data (only when auto-playing or simulation is running) useEffect(() => { if (!selectedSimulation) return; const interval = setInterval(async () => { const selectedSim = simulations.find(s => s.id === selectedSimulation); // Only poll if auto-playing is enabled or if simulation is running if (!isAutoPlaying && selectedSim && !selectedSim.is_running) { return; } try { const response = await fetch(`/api/simulations/${selectedSimulation}`, { headers: { 'X-Session-ID': sessionId }, }); if (response.ok) { const data = await response.json(); setCurrentData(data); } } catch (err) { // Silently fail for polling } }, 100); // 10 FPS updates return () => clearInterval(interval); }, [selectedSimulation, isAutoPlaying, simulations]); // Initial load useEffect(() => { fetchSimulations(); }, []); const selectedSim = simulations.find(s => s.id === selectedSimulation); return (
Orbital Simulator
{selectedSim && ( <> )}
{selectedSim && currentData && ( )}
{error && (
{error}
)} {currentData ? ( ) : selectedSimulation ? (
Loading simulation data...
) : (
Select or create a simulation to begin
)}
); } export default App;