Files
rocketry/src/components/trajectory/TimelineBar.jsx
2026-03-04 20:29:19 +00:00

142 lines
4.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
)
}