Trajectories
This commit is contained in:
141
src/components/trajectory/TimelineBar.jsx
Normal file
141
src/components/trajectory/TimelineBar.jsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user