Trajectories

This commit is contained in:
2026-03-04 20:29:19 +00:00
parent 6af56f478f
commit 1b15384c10
20 changed files with 4825 additions and 71 deletions

View File

@@ -0,0 +1,141 @@
import { useState, useRef, useEffect } from 'react'
export function TimelineBar({
simulationResults,
currentTime,
onTimeChange,
isPlaying,
onPlayPause,
playbackSpeed,
onPlaybackSpeedChange,
}) {
const [showEventForm, setShowEventForm] = useState(false)
const [newEventTime, setNewEventTime] = useState('')
const [newEventLabel, setNewEventLabel] = useState('')
const [newEventType, setNewEventType] = useState('marker')
const timelineRef = useRef(null)
if (!simulationResults) {
return (
<div className="bg-slate-800 border-t border-slate-700 p-4 h-32">
<p className="text-slate-400 text-sm">Run simulation to see timeline</p>
</div>
)
}
const { states, events, summary } = simulationResults
const maxTime = summary.t_max
// Handle timeline scrub
const handleTimelineClick = (e) => {
if (!timelineRef.current) return
const rect = timelineRef.current.getBoundingClientRect()
const percent = (e.clientX - rect.left) / rect.width
const t = Math.max(0, Math.min(maxTime, percent * maxTime))
onTimeChange(t)
}
// Event colors
const eventColors = {
engine: '#ef4444',
guidance: '#f59e0b',
marker: '#8b5cf6',
jettison: '#06b6d4',
}
return (
<div className="bg-slate-800 border-t border-slate-700">
{/* Playback controls */}
<div className="flex items-center gap-3 px-4 py-3 border-b border-slate-700">
<button
onClick={() => onTimeChange(0)}
className="px-2 py-1 bg-slate-700 hover:bg-slate-600 rounded text-xs text-slate-300"
title="Rewind"
>
</button>
<button
onClick={onPlayPause}
className={`px-3 py-1 rounded text-xs font-medium ${
isPlaying
? 'bg-red-600 hover:bg-red-500 text-white'
: 'bg-blue-600 hover:bg-blue-500 text-white'
}`}
>
{isPlaying ? '⏸ Pause' : '▶ Play'}
</button>
<div className="flex items-center gap-1">
<span className="text-xs text-slate-400">Speed:</span>
<select
value={playbackSpeed}
onChange={e => onPlaybackSpeedChange(parseFloat(e.target.value))}
className="px-2 py-0.5 bg-slate-700 border border-slate-600 rounded text-xs text-slate-300 focus:outline-none"
>
<option value={1}>1×</option>
<option value={5}>5×</option>
<option value={10}>10×</option>
</select>
</div>
<span className="text-sm text-slate-300 font-mono ml-auto">
t = {currentTime.toFixed(2)} s / {maxTime.toFixed(1)} s
</span>
</div>
{/* Timeline */}
<div className="relative px-4 py-4">
<div
ref={timelineRef}
onClick={handleTimelineClick}
className="relative h-12 bg-slate-900 border border-slate-700 rounded cursor-pointer hover:bg-slate-800 transition-colors"
>
{/* Event markers */}
{events.map(event => {
const percent = (event.t / maxTime) * 100
return (
<div
key={event.id}
style={{
left: `${percent}%`,
backgroundColor: event.color || eventColors[event.type] || '#64748b',
}}
className="absolute top-0 bottom-0 w-1 rounded-t hover:brightness-150"
title={`${event.label} @ t=${event.t.toFixed(2)}s`}
>
{/* Label above */}
<div className="absolute bottom-full mb-1 left-1/2 transform -translate-x-1/2 bg-slate-700 border border-slate-600 rounded px-2 py-1 text-xs text-white whitespace-nowrap opacity-0 hover:opacity-100 transition-opacity z-10">
{event.label}
</div>
</div>
)
})}
{/* Playhead */}
<div
style={{ left: `${(currentTime / maxTime) * 100}%` }}
className="absolute top-0 bottom-0 w-0.5 bg-emerald-400 pointer-events-none"
/>
</div>
{/* Event list below */}
<div className="mt-3 text-xs space-y-1">
{events.slice(0, 5).map(event => (
<div key={event.id} className="flex items-center gap-2 text-slate-400">
<div
className="w-2 h-2 rounded-full"
style={{ backgroundColor: event.color || eventColors[event.type] || '#64748b' }}
/>
<span className="w-20 text-slate-500">{event.t.toFixed(1)}s</span>
<span>{event.label}</span>
</div>
))}
{events.length > 5 && (
<div className="text-slate-500">+{events.length - 5} more events</div>
)}
</div>
</div>
</div>
)
}