2025-06-21 23:29:14 -04:00

319 lines
8.7 KiB
TypeScript

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<SimulationInfo[]>([]);
const [selectedSimulation, setSelectedSimulation] = useState<string | null>(null);
const [currentData, setCurrentData] = useState<SimulationUpdate | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(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 (
<div className="app">
<header className="header">
<div className="logo">Orbital Simulator</div>
<div className="controls">
{selectedSim && (
<>
<button
className="button"
onClick={() => controlSimulation(selectedSim.id, selectedSim.is_running ? 'pause' : 'start')}
disabled={isLoading}
>
{selectedSim.is_running ? <Pause size={16} /> : <Play size={16} />}
{selectedSim.is_running ? 'Pause' : 'Start'}
</button>
<button
className="button secondary"
onClick={() => controlSimulation(selectedSim.id, 'step')}
disabled={isLoading || selectedSim.is_running}
>
Step
</button>
<button
className="button secondary"
onClick={() => controlSimulation(selectedSim.id, 'stop')}
disabled={isLoading}
>
<Square size={16} />
Stop
</button>
</>
)}
</div>
</header>
<div className="main-content">
<div className="sidebar">
<ConfigurationPanel
onCreateSimulation={createSimulation}
isLoading={isLoading}
/>
<SimulationList
simulations={simulations}
selectedSimulation={selectedSimulation}
currentData={currentData}
isAutoPlaying={isAutoPlaying}
onSelectSimulation={setSelectedSimulation}
onDeleteSimulation={deleteSimulation}
onControlSimulation={controlSimulation}
onSeek={handleSeek}
onToggleAutoPlay={handleToggleAutoPlay}
onRestart={handleRestart}
/>
{selectedSim && currentData && (
<SimulationControls
data={currentData}
/>
)}
</div>
<div className="simulation-view">
{error && (
<div className="error">
{error}
<button
className="button secondary"
onClick={() => setError(null)}
style={{ marginLeft: '1rem' }}
>
Dismiss
</button>
</div>
)}
{currentData ? (
<SimulationCanvas data={currentData} />
) : selectedSimulation ? (
<div className="loading">Loading simulation data...</div>
) : (
<div className="loading">Select or create a simulation to begin</div>
)}
</div>
</div>
</div>
);
}
export default App;