142 lines
4.7 KiB
JavaScript
142 lines
4.7 KiB
JavaScript
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>
|
||
)
|
||
}
|