Skip to main content

proof_engine/editor/
timeline.rs

1//! Animation Timeline — keyframe curves for bone FK rotations, SDF morph
2//! targets, and kit parameter animation.
3//!
4//! # Architecture
5//!
6//! The timeline owns a list of `AnimTrack` values — one per animated property.
7//! A track holds a sorted list of `Keyframe` values and a `CurveInterp` mode.
8//! Evaluating a track at time `t` returns the interpolated `TrackValue`.
9//!
10//! # Track types
11//!
12//! - **BoneRotation**: quaternion rotation for a named bone.
13//! - **BoneTranslation**: Vec3 translation for a named bone.
14//! - **BoneScale**: Vec3 scale for a named bone.
15//! - **SdfMorph**: blend factor [0,1] between two SDF graphs by name.
16//! - **KitFloat**: any f32 kit parameter (bloom, AO strength, etc.).
17//! - **KitVec3**: any Vec3 kit parameter (light direction, etc.).
18//! - **KitColor**: RGBA colour parameter.
19//!
20//! # Playback
21//!
22//! `Timeline::step` advances the playhead by `dt` seconds and evaluates all
23//! tracks, returning a `FrameSnapshot` with all current animated values.
24//!
25//! # Editing
26//!
27//! Keys are inserted/moved/deleted through the `TimelineEditor` wrapper, which
28//! maintains an undo stack of `TimelineEdit` entries.
29
30use glam::{Vec2, Vec3, Vec4, Quat};
31use std::collections::HashMap;
32
33// ─────────────────────────────────────────────────────────────────────────────
34// Time
35// ─────────────────────────────────────────────────────────────────────────────
36
37/// Timeline time in seconds (f32 for GPU-side compatibility).
38pub type Time = f32;
39
40// ─────────────────────────────────────────────────────────────────────────────
41// TrackId
42// ─────────────────────────────────────────────────────────────────────────────
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
45pub struct TrackId(pub u32);
46
47impl TrackId { pub const NONE: TrackId = TrackId(u32::MAX); }
48impl Default for TrackId { fn default() -> Self { TrackId::NONE } }
49
50// ─────────────────────────────────────────────────────────────────────────────
51// KeyId
52// ─────────────────────────────────────────────────────────────────────────────
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
55pub struct KeyId(pub u32);
56
57// ─────────────────────────────────────────────────────────────────────────────
58// TrackValue
59// ─────────────────────────────────────────────────────────────────────────────
60
61/// The union of all possible track value types.
62#[derive(Debug, Clone, PartialEq)]
63pub enum TrackValue {
64    Float(f32),
65    Vec2(Vec2),
66    Vec3(Vec3),
67    Vec4(Vec4),
68    Quat(Quat),
69    Bool(bool),
70    Event(String),
71}
72
73impl TrackValue {
74    pub fn as_float(&self) -> Option<f32> {
75        if let TrackValue::Float(v) = self { Some(*v) } else { None }
76    }
77    pub fn as_vec3(&self) -> Option<Vec3> {
78        if let TrackValue::Vec3(v) = self { Some(*v) } else { None }
79    }
80    pub fn as_quat(&self) -> Option<Quat> {
81        if let TrackValue::Quat(q) = self { Some(*q) } else { None }
82    }
83
84    /// Linear interpolation between two values. Returns None if types differ.
85    pub fn lerp(&self, other: &Self, t: f32) -> Option<Self> {
86        match (self, other) {
87            (TrackValue::Float(a), TrackValue::Float(b)) => Some(TrackValue::Float(a + (b - a) * t)),
88            (TrackValue::Vec2(a),  TrackValue::Vec2(b))  => Some(TrackValue::Vec2(*a + (*b - *a) * t)),
89            (TrackValue::Vec3(a),  TrackValue::Vec3(b))  => Some(TrackValue::Vec3(*a + (*b - *a) * t)),
90            (TrackValue::Vec4(a),  TrackValue::Vec4(b))  => Some(TrackValue::Vec4(*a + (*b - *a) * t)),
91            (TrackValue::Quat(a),  TrackValue::Quat(b))  => Some(TrackValue::Quat(a.slerp(*b, t))),
92            (TrackValue::Bool(a),  TrackValue::Bool(_))  => Some(TrackValue::Bool(*a)),
93            _ => None,
94        }
95    }
96
97    pub fn type_name(&self) -> &'static str {
98        match self {
99            TrackValue::Float(_) => "Float",
100            TrackValue::Vec2(_)  => "Vec2",
101            TrackValue::Vec3(_)  => "Vec3",
102            TrackValue::Vec4(_)  => "Vec4",
103            TrackValue::Quat(_)  => "Quat",
104            TrackValue::Bool(_)  => "Bool",
105            TrackValue::Event(_) => "Event",
106        }
107    }
108}
109
110// ─────────────────────────────────────────────────────────────────────────────
111// CurveInterp
112// ─────────────────────────────────────────────────────────────────────────────
113
114/// How to interpolate between two keyframes.
115#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
116pub enum CurveInterp {
117    /// Step: hold the left key's value until the right key.
118    Step,
119    #[default]
120    /// Linear interpolation.
121    Linear,
122    /// Cubic Hermite spline using per-key tangents.
123    CubicHermite,
124    /// Automatic cubic smoothing (Catmull-Rom).
125    CatmullRom,
126    /// Custom easing function.
127    Ease(EaseKind),
128}
129
130#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
131pub enum EaseKind {
132    EaseIn, EaseOut, EaseInOut,
133    BackIn, BackOut, BackInOut,
134    BounceIn, BounceOut, BounceInOut,
135    ElasticIn, ElasticOut,
136}
137
138impl EaseKind {
139    pub fn apply(self, t: f32) -> f32 {
140        match self {
141            EaseKind::EaseIn    => t * t,
142            EaseKind::EaseOut   => 1.0 - (1.0 - t) * (1.0 - t),
143            EaseKind::EaseInOut => {
144                if t < 0.5 { 2.0 * t * t }
145                else { 1.0 - (-2.0 * t + 2.0).powi(2) / 2.0 }
146            }
147            EaseKind::BackIn    => { let c1 = 1.701_58; let c3 = c1 + 1.0; c3 * t * t * t - c1 * t * t }
148            EaseKind::BackOut   => { let c1 = 1.701_58; let c3 = c1 + 1.0; 1.0 + c3 * (t-1.0).powi(3) + c1 * (t-1.0).powi(2) }
149            EaseKind::BackInOut => {
150                let c2 = 1.701_58 * 1.525;
151                if t < 0.5 {
152                    ((2.0 * t).powi(2) * ((c2 + 1.0) * 2.0 * t - c2)) / 2.0
153                } else {
154                    ((2.0 * t - 2.0).powi(2) * ((c2 + 1.0) * (2.0 * t - 2.0) + c2) + 2.0) / 2.0
155                }
156            }
157            EaseKind::BounceOut => {
158                let n1 = 7.5625; let d1 = 2.75;
159                let t = t;
160                if t < 1.0 / d1 { n1 * t * t }
161                else if t < 2.0 / d1 { let t = t - 1.5 / d1; n1 * t * t + 0.75 }
162                else if t < 2.5 / d1 { let t = t - 2.25 / d1; n1 * t * t + 0.9375 }
163                else { let t = t - 2.625 / d1; n1 * t * t + 0.984_375 }
164            }
165            EaseKind::BounceIn     => 1.0 - EaseKind::BounceOut.apply(1.0 - t),
166            EaseKind::BounceInOut  => {
167                if t < 0.5 { (1.0 - EaseKind::BounceOut.apply(1.0 - 2.0 * t)) / 2.0 }
168                else { (1.0 + EaseKind::BounceOut.apply(2.0 * t - 1.0)) / 2.0 }
169            }
170            EaseKind::ElasticIn  => {
171                if t == 0.0 || t == 1.0 { return t; }
172                let c4 = (2.0 * std::f32::consts::PI) / 3.0;
173                -(2.0f32.powf(10.0 * t - 10.0)) * ((t * 10.0 - 10.75) * c4).sin()
174            }
175            EaseKind::ElasticOut => {
176                if t == 0.0 || t == 1.0 { return t; }
177                let c4 = (2.0 * std::f32::consts::PI) / 3.0;
178                2.0f32.powf(-10.0 * t) * ((t * 10.0 - 0.75) * c4).sin() + 1.0
179            }
180        }
181    }
182}
183
184// ─────────────────────────────────────────────────────────────────────────────
185// Keyframe
186// ─────────────────────────────────────────────────────────────────────────────
187
188/// A single key in an animation track.
189#[derive(Debug, Clone)]
190pub struct Keyframe {
191    pub id:       KeyId,
192    pub time:     Time,
193    pub value:    TrackValue,
194    /// Left tangent for cubic interpolation.
195    pub tan_in:   Option<f32>,
196    /// Right tangent for cubic interpolation.
197    pub tan_out:  Option<f32>,
198    pub interp:   CurveInterp,
199    pub selected: bool,
200}
201
202impl Keyframe {
203    pub fn new(id: KeyId, time: Time, value: TrackValue) -> Self {
204        Self {
205            id, time, value,
206            tan_in: None, tan_out: None,
207            interp: CurveInterp::Linear,
208            selected: false,
209        }
210    }
211}
212
213// ─────────────────────────────────────────────────────────────────────────────
214// TrackTarget — identifies what a track animates
215// ─────────────────────────────────────────────────────────────────────────────
216
217#[derive(Debug, Clone, PartialEq, Eq, Hash)]
218pub enum TrackTarget {
219    BoneRotation(String),
220    BoneTranslation(String),
221    BoneScale(String),
222    BoneEnvelopeRadius(String),
223    SdfMorph { from_graph: String, to_graph: String },
224    NodeTranslation { graph: String, node_id: u32 },
225    NodeScale       { graph: String, node_id: u32 },
226    NodePrimParam   { graph: String, node_id: u32, param: String },
227    KitFloat(String),
228    KitVec3(String),
229    KitColor(String),
230    CameraPos,
231    CameraFov,
232    CameraRoll,
233    SceneFloat(String),
234    Event(String),
235}
236
237impl TrackTarget {
238    pub fn label(&self) -> String {
239        match self {
240            TrackTarget::BoneRotation(n)    => format!("{n} Rotation"),
241            TrackTarget::BoneTranslation(n) => format!("{n} Translation"),
242            TrackTarget::BoneScale(n)       => format!("{n} Scale"),
243            TrackTarget::BoneEnvelopeRadius(n) => format!("{n} EnvRadius"),
244            TrackTarget::SdfMorph { from_graph, to_graph } => format!("Morph {from_graph}→{to_graph}"),
245            TrackTarget::NodeTranslation { node_id, .. } => format!("N{node_id} Translation"),
246            TrackTarget::NodeScale { node_id, .. }       => format!("N{node_id} Scale"),
247            TrackTarget::NodePrimParam { node_id, param, .. } => format!("N{node_id}.{param}"),
248            TrackTarget::KitFloat(p)  => format!("Kit:{p}"),
249            TrackTarget::KitVec3(p)   => format!("Kit:{p}"),
250            TrackTarget::KitColor(p)  => format!("Kit:{p}"),
251            TrackTarget::CameraPos    => "Camera.Pos".into(),
252            TrackTarget::CameraFov    => "Camera.Fov".into(),
253            TrackTarget::CameraRoll   => "Camera.Roll".into(),
254            TrackTarget::SceneFloat(n) => format!("Scene:{n}"),
255            TrackTarget::Event(n)     => format!("Event:{n}"),
256        }
257    }
258}
259
260// ─────────────────────────────────────────────────────────────────────────────
261// AnimTrack
262// ─────────────────────────────────────────────────────────────────────────────
263
264/// A single animated property track.
265#[derive(Debug, Clone)]
266pub struct AnimTrack {
267    pub id:       TrackId,
268    pub target:   TrackTarget,
269    pub label:    String,
270    pub visible:  bool,
271    pub locked:   bool,
272    pub muted:    bool,
273    pub color:    Vec4,
274    keys:         Vec<Keyframe>,
275    next_key:     u32,
276    pub default_interp: CurveInterp,
277}
278
279impl AnimTrack {
280    pub fn new(id: TrackId, target: TrackTarget) -> Self {
281        let label = target.label();
282        Self {
283            id, label, target,
284            visible: true, locked: false, muted: false,
285            color: Vec4::new(0.4, 0.8, 0.4, 1.0),
286            keys: Vec::new(),
287            next_key: 1,
288            default_interp: CurveInterp::Linear,
289        }
290    }
291
292    // ── Key management ────────────────────────────────────────────────────
293
294    pub fn insert_key(&mut self, time: Time, value: TrackValue) -> KeyId {
295        let id = KeyId(self.next_key);
296        self.next_key += 1;
297        // Remove any existing key at this exact time
298        self.keys.retain(|k| (k.time - time).abs() > 1e-5);
299        let key = Keyframe::new(id, time, value);
300        // Insert in sorted order
301        let pos = self.keys.partition_point(|k| k.time < time);
302        self.keys.insert(pos, key);
303        id
304    }
305
306    pub fn remove_key(&mut self, id: KeyId) -> Option<Keyframe> {
307        if let Some(pos) = self.keys.iter().position(|k| k.id == id) {
308            Some(self.keys.remove(pos))
309        } else { None }
310    }
311
312    pub fn remove_at_time(&mut self, time: Time) {
313        self.keys.retain(|k| (k.time - time).abs() > 1e-5);
314    }
315
316    pub fn keys(&self) -> &[Keyframe] { &self.keys }
317    pub fn key_count(&self) -> usize { self.keys.len() }
318
319    pub fn start_time(&self) -> Time {
320        self.keys.first().map(|k| k.time).unwrap_or(0.0)
321    }
322
323    pub fn end_time(&self) -> Time {
324        self.keys.last().map(|k| k.time).unwrap_or(0.0)
325    }
326
327    pub fn duration(&self) -> Time { self.end_time() - self.start_time() }
328
329    // ── Evaluation ────────────────────────────────────────────────────────
330
331    /// Sample the track at time `t`. Returns None if no keys.
332    pub fn sample(&self, t: Time) -> Option<TrackValue> {
333        if self.muted { return None; }
334        if self.keys.is_empty() { return None; }
335        if self.keys.len() == 1 { return Some(self.keys[0].value.clone()); }
336
337        // Clamp to range
338        if t <= self.keys[0].time { return Some(self.keys[0].value.clone()); }
339        if t >= self.keys.last().unwrap().time {
340            return Some(self.keys.last().unwrap().value.clone());
341        }
342
343        // Find bracketing keys
344        let right_idx = self.keys.partition_point(|k| k.time <= t);
345        let right = &self.keys[right_idx];
346        let left  = &self.keys[right_idx - 1];
347
348        let dt = right.time - left.time;
349        if dt < 1e-8 { return Some(left.value.clone()); }
350        let u = (t - left.time) / dt;
351
352        let interp = left.interp;
353        let t_eased = match interp {
354            CurveInterp::Step         => 0.0,
355            CurveInterp::Linear       => u,
356            CurveInterp::CatmullRom | CurveInterp::CubicHermite => {
357                // Smooth cubic
358                u * u * (3.0 - 2.0 * u)
359            }
360            CurveInterp::Ease(kind)   => kind.apply(u),
361        };
362
363        left.value.lerp(&right.value, t_eased)
364    }
365
366    /// Get the key immediately before or at `t`.
367    pub fn key_before(&self, t: Time) -> Option<&Keyframe> {
368        let idx = self.keys.partition_point(|k| k.time <= t);
369        if idx == 0 { None } else { Some(&self.keys[idx - 1]) }
370    }
371
372    /// Move a key in time.
373    pub fn move_key(&mut self, id: KeyId, new_time: Time) {
374        if let Some(pos) = self.keys.iter().position(|k| k.id == id) {
375            self.keys[pos].time = new_time;
376            self.keys.sort_by(|a, b| a.time.partial_cmp(&b.time).unwrap_or(std::cmp::Ordering::Equal));
377        }
378    }
379
380    pub fn set_key_value(&mut self, id: KeyId, value: TrackValue) {
381        if let Some(key) = self.keys.iter_mut().find(|k| k.id == id) {
382            key.value = value;
383        }
384    }
385
386    pub fn set_key_interp(&mut self, id: KeyId, interp: CurveInterp) {
387        if let Some(key) = self.keys.iter_mut().find(|k| k.id == id) {
388            key.interp = interp;
389        }
390    }
391}
392
393// ─────────────────────────────────────────────────────────────────────────────
394// AnimClip — a named set of tracks
395// ─────────────────────────────────────────────────────────────────────────────
396
397#[derive(Debug, Clone)]
398pub struct AnimClip {
399    pub name:   String,
400    pub tracks: Vec<AnimTrack>,
401    next_track: u32,
402}
403
404impl AnimClip {
405    pub fn new(name: impl Into<String>) -> Self {
406        Self { name: name.into(), tracks: Vec::new(), next_track: 1 }
407    }
408
409    pub fn add_track(&mut self, target: TrackTarget) -> TrackId {
410        let id = TrackId(self.next_track);
411        self.next_track += 1;
412        self.tracks.push(AnimTrack::new(id, target));
413        id
414    }
415
416    pub fn get_track(&self, id: TrackId) -> Option<&AnimTrack> {
417        self.tracks.iter().find(|t| t.id == id)
418    }
419
420    pub fn get_track_mut(&mut self, id: TrackId) -> Option<&mut AnimTrack> {
421        self.tracks.iter_mut().find(|t| t.id == id)
422    }
423
424    pub fn duration(&self) -> Time {
425        self.tracks.iter().map(|t| t.end_time()).fold(0.0_f32, f32::max)
426    }
427
428    /// Sample all tracks at time `t`, returning a map from TrackId → value.
429    pub fn sample_all(&self, t: Time) -> HashMap<TrackId, TrackValue> {
430        self.tracks.iter()
431            .filter_map(|track| track.sample(t).map(|v| (track.id, v)))
432            .collect()
433    }
434
435    pub fn track_count(&self) -> usize { self.tracks.len() }
436    pub fn total_keys(&self) -> usize { self.tracks.iter().map(|t| t.key_count()).sum() }
437}
438
439// ─────────────────────────────────────────────────────────────────────────────
440// PlaybackState
441// ─────────────────────────────────────────────────────────────────────────────
442
443#[derive(Debug, Clone, Copy, PartialEq, Eq)]
444pub enum PlaybackState {
445    Stopped,
446    Playing,
447    Paused,
448    Recording,
449}
450
451impl Default for PlaybackState { fn default() -> Self { PlaybackState::Stopped } }
452
453#[derive(Debug, Clone, Copy, PartialEq, Eq)]
454pub enum LoopMode {
455    Once,
456    Loop,
457    PingPong,
458    HoldFirst,
459    HoldLast,
460}
461
462impl Default for LoopMode { fn default() -> Self { LoopMode::Loop } }
463
464// ─────────────────────────────────────────────────────────────────────────────
465// FrameSnapshot — the evaluated animation state at a given time
466// ─────────────────────────────────────────────────────────────────────────────
467
468#[derive(Debug, Clone, Default)]
469pub struct FrameSnapshot {
470    pub time:           Time,
471    pub bone_rotations: HashMap<String, Quat>,
472    pub bone_translates: HashMap<String, Vec3>,
473    pub bone_scales:    HashMap<String, Vec3>,
474    pub kit_floats:     HashMap<String, f32>,
475    pub kit_colors:     HashMap<String, Vec4>,
476    pub kit_vec3s:      HashMap<String, Vec3>,
477    pub sdf_morphs:     Vec<(String, String, f32)>,
478    pub events:         Vec<String>,
479    pub camera_pos:     Option<Vec3>,
480    pub camera_fov:     Option<f32>,
481}
482
483// ─────────────────────────────────────────────────────────────────────────────
484// Timeline
485// ─────────────────────────────────────────────────────────────────────────────
486
487/// Master timeline — owns all animation clips plus playback state.
488#[derive(Debug)]
489pub struct Timeline {
490    pub clips:        Vec<AnimClip>,
491    pub active_clip:  usize,
492    pub playhead:     Time,
493    pub state:        PlaybackState,
494    pub loop_mode:    LoopMode,
495    pub speed:        f32,
496    pub fps:          f32,
497    /// Whether to snap the playhead to frame boundaries.
498    pub snap_frames:  bool,
499    /// In/out point for looping.
500    pub in_point:     Time,
501    pub out_point:    Time,
502}
503
504impl Timeline {
505    pub fn new() -> Self {
506        let clip = AnimClip::new("Default");
507        Self {
508            clips:       vec![clip],
509            active_clip: 0,
510            playhead:    0.0,
511            state:       PlaybackState::Stopped,
512            loop_mode:   LoopMode::Loop,
513            speed:       1.0,
514            fps:         60.0,
515            snap_frames: false,
516            in_point:    0.0,
517            out_point:   10.0,
518        }
519    }
520
521    pub fn clip(&self) -> &AnimClip { &self.clips[self.active_clip] }
522    pub fn clip_mut(&mut self) -> &mut AnimClip { &mut self.clips[self.active_clip] }
523
524    pub fn frame_duration(&self) -> Time { 1.0 / self.fps }
525
526    pub fn current_frame(&self) -> u32 { (self.playhead * self.fps) as u32 }
527
528    pub fn total_frames(&self) -> u32 { (self.clip().duration() * self.fps) as u32 }
529
530    // ── Playback control ──────────────────────────────────────────────────
531
532    pub fn play(&mut self) { self.state = PlaybackState::Playing; }
533    pub fn pause(&mut self) {
534        self.state = if self.state == PlaybackState::Playing {
535            PlaybackState::Paused
536        } else {
537            PlaybackState::Playing
538        };
539    }
540    pub fn stop(&mut self) { self.state = PlaybackState::Stopped; self.playhead = self.in_point; }
541    pub fn record(&mut self) { self.state = PlaybackState::Recording; }
542
543    pub fn seek(&mut self, t: Time) {
544        self.playhead = if self.snap_frames {
545            (t * self.fps).round() / self.fps
546        } else {
547            t
548        }.clamp(self.in_point, self.out_point);
549    }
550
551    pub fn seek_frame(&mut self, frame: i32) {
552        let t = frame as f32 / self.fps;
553        self.seek(t);
554    }
555
556    pub fn step_forward(&mut self) { self.seek(self.playhead + self.frame_duration()); }
557    pub fn step_backward(&mut self) { self.seek(self.playhead - self.frame_duration()); }
558    pub fn go_to_start(&mut self) { self.seek(self.in_point); }
559    pub fn go_to_end(&mut self) { self.seek(self.out_point); }
560
561    /// Advance by `dt` seconds of wall-clock time.
562    pub fn step(&mut self, dt: f32) -> FrameSnapshot {
563        if self.state == PlaybackState::Playing || self.state == PlaybackState::Recording {
564            let new_t = self.playhead + dt * self.speed;
565            match self.loop_mode {
566                LoopMode::Once => {
567                    self.playhead = new_t.min(self.out_point);
568                    if self.playhead >= self.out_point { self.state = PlaybackState::Stopped; }
569                }
570                LoopMode::Loop => {
571                    let range = self.out_point - self.in_point;
572                    if range > 1e-5 {
573                        self.playhead = self.in_point + (new_t - self.in_point).rem_euclid(range);
574                    }
575                }
576                LoopMode::PingPong => {
577                    let range = self.out_point - self.in_point;
578                    if range > 1e-5 {
579                        let phase = (new_t - self.in_point) / range;
580                        let cycle = phase.floor() as u32;
581                        let frac  = phase - phase.floor();
582                        self.playhead = if cycle % 2 == 0 {
583                            self.in_point + frac * range
584                        } else {
585                            self.out_point - frac * range
586                        };
587                    }
588                }
589                LoopMode::HoldFirst => {
590                    self.playhead = new_t.max(self.in_point);
591                }
592                LoopMode::HoldLast => {
593                    self.playhead = new_t.min(self.out_point);
594                }
595            }
596        }
597        self.evaluate(self.playhead)
598    }
599
600    /// Evaluate all tracks at the given time and build a FrameSnapshot.
601    pub fn evaluate(&self, t: Time) -> FrameSnapshot {
602        let mut snap = FrameSnapshot { time: t, ..Default::default() };
603        for track in &self.clip().tracks {
604            let Some(val) = track.sample(t) else { continue; };
605            match &track.target {
606                TrackTarget::BoneRotation(n) => {
607                    if let TrackValue::Quat(q) = val { snap.bone_rotations.insert(n.clone(), q); }
608                }
609                TrackTarget::BoneTranslation(n) => {
610                    if let TrackValue::Vec3(v) = val { snap.bone_translates.insert(n.clone(), v); }
611                }
612                TrackTarget::BoneScale(n) => {
613                    if let TrackValue::Vec3(v) = val { snap.bone_scales.insert(n.clone(), v); }
614                }
615                TrackTarget::KitFloat(p) => {
616                    if let TrackValue::Float(v) = val { snap.kit_floats.insert(p.clone(), v); }
617                }
618                TrackTarget::KitColor(p) => {
619                    if let TrackValue::Vec4(c) = val { snap.kit_colors.insert(p.clone(), c); }
620                }
621                TrackTarget::KitVec3(p) => {
622                    if let TrackValue::Vec3(v) = val { snap.kit_vec3s.insert(p.clone(), v); }
623                }
624                TrackTarget::SdfMorph { from_graph, to_graph } => {
625                    if let TrackValue::Float(f) = val {
626                        snap.sdf_morphs.push((from_graph.clone(), to_graph.clone(), f));
627                    }
628                }
629                TrackTarget::CameraPos => {
630                    if let TrackValue::Vec3(v) = val { snap.camera_pos = Some(v); }
631                }
632                TrackTarget::CameraFov => {
633                    if let TrackValue::Float(v) = val { snap.camera_fov = Some(v); }
634                }
635                TrackTarget::Event(n) => {
636                    snap.events.push(n.clone());
637                }
638                _ => {}
639            }
640        }
641        snap
642    }
643}
644
645impl Default for Timeline { fn default() -> Self { Self::new() } }
646
647// ─────────────────────────────────────────────────────────────────────────────
648// TimelineEditor — adds undo/redo and selection to Timeline
649// ─────────────────────────────────────────────────────────────────────────────
650
651/// Selected set of (track, key) pairs.
652#[derive(Debug, Clone, Default)]
653pub struct KeySelection {
654    pub selected: Vec<(TrackId, KeyId)>,
655}
656
657impl KeySelection {
658    pub fn is_selected(&self, track: TrackId, key: KeyId) -> bool {
659        self.selected.iter().any(|&(t, k)| t == track && k == key)
660    }
661    pub fn select_only(&mut self, track: TrackId, key: KeyId) {
662        self.selected.clear();
663        self.selected.push((track, key));
664    }
665    pub fn toggle(&mut self, track: TrackId, key: KeyId) {
666        if let Some(pos) = self.selected.iter().position(|&(t, k)| t == track && k == key) {
667            self.selected.remove(pos);
668        } else {
669            self.selected.push((track, key));
670        }
671    }
672    pub fn clear(&mut self) { self.selected.clear(); }
673}
674
675#[derive(Debug, Clone)]
676pub enum TimelineEdit {
677    InsertKey { track: TrackId, key: Keyframe },
678    RemoveKey { track: TrackId, key: Keyframe },
679    MoveKey   { track: TrackId, key: KeyId, old_time: Time, new_time: Time },
680    SetValue  { track: TrackId, key: KeyId, old_value: TrackValue, new_value: TrackValue },
681    AddTrack  { track_id: TrackId },
682    RemoveTrack { track_id: TrackId, track: AnimTrack },
683}
684
685/// Full editor wrapper around Timeline.
686#[derive(Debug)]
687pub struct TimelineEditor {
688    pub timeline:    Timeline,
689    pub selection:   KeySelection,
690    undo_stack:      Vec<TimelineEdit>,
691    redo_stack:      Vec<TimelineEdit>,
692    /// Pixels-per-second for the dopesheet/curve editor.
693    pub px_per_sec:  f32,
694    /// Vertical scroll in the track list.
695    pub scroll_y:    f32,
696    /// Whether to show the curve editor (false = dopesheet mode).
697    pub curve_mode:  bool,
698    /// Whether to auto-key: insert a key whenever a property changes.
699    pub auto_key:    bool,
700    /// Whether to ripple-edit (shift subsequent keys on insert/delete).
701    pub ripple:      bool,
702}
703
704impl TimelineEditor {
705    pub fn new() -> Self {
706        Self {
707            timeline:   Timeline::new(),
708            selection:  KeySelection::default(),
709            undo_stack: Vec::new(),
710            redo_stack: Vec::new(),
711            px_per_sec: 120.0,
712            scroll_y:   0.0,
713            curve_mode: false,
714            auto_key:   false,
715            ripple:     false,
716        }
717    }
718
719    // ── Time ↔ pixels ─────────────────────────────────────────────────────
720
721    pub fn time_to_px(&self, t: Time) -> f32 { t * self.px_per_sec }
722    pub fn px_to_time(&self, px: f32) -> Time { px / self.px_per_sec }
723    pub fn zoom_in(&mut self)  { self.px_per_sec = (self.px_per_sec * 1.25).min(2000.0); }
724    pub fn zoom_out(&mut self) { self.px_per_sec = (self.px_per_sec / 1.25).max(5.0); }
725
726    // ── Track operations ──────────────────────────────────────────────────
727
728    pub fn add_track(&mut self, target: TrackTarget) -> TrackId {
729        let id = self.timeline.clip_mut().add_track(target);
730        self.undo_stack.push(TimelineEdit::AddTrack { track_id: id });
731        self.redo_stack.clear();
732        id
733    }
734
735    // ── Key operations ────────────────────────────────────────────────────
736
737    pub fn insert_key(&mut self, track_id: TrackId, time: Time, value: TrackValue) -> Option<KeyId> {
738        let track = self.timeline.clip_mut().get_track_mut(track_id)?;
739        let id = track.insert_key(time, value.clone());
740        if let Some(key) = track.keys().iter().find(|k| k.id == id).cloned() {
741            self.undo_stack.push(TimelineEdit::InsertKey { track: track_id, key });
742        }
743        self.redo_stack.clear();
744        Some(id)
745    }
746
747    pub fn delete_selected(&mut self) {
748        for (track_id, key_id) in self.selection.selected.drain(..).collect::<Vec<_>>() {
749            if let Some(track) = self.timeline.clip_mut().get_track_mut(track_id) {
750                if let Some(key) = track.remove_key(key_id) {
751                    self.undo_stack.push(TimelineEdit::RemoveKey { track: track_id, key });
752                }
753            }
754        }
755        self.redo_stack.clear();
756    }
757
758    pub fn undo(&mut self) {
759        if let Some(edit) = self.undo_stack.pop() {
760            match &edit {
761                TimelineEdit::InsertKey { track, key } => {
762                    if let Some(t) = self.timeline.clip_mut().get_track_mut(*track) {
763                        t.remove_key(key.id);
764                    }
765                }
766                TimelineEdit::RemoveKey { track, key } => {
767                    if let Some(t) = self.timeline.clip_mut().get_track_mut(*track) {
768                        t.keys.push(key.clone());
769                        t.keys.sort_by(|a, b| a.time.partial_cmp(&b.time).unwrap_or(std::cmp::Ordering::Equal));
770                    }
771                }
772                TimelineEdit::MoveKey { track, key, old_time, .. } => {
773                    if let Some(t) = self.timeline.clip_mut().get_track_mut(*track) {
774                        t.move_key(*key, *old_time);
775                    }
776                }
777                TimelineEdit::SetValue { track, key, old_value, .. } => {
778                    if let Some(t) = self.timeline.clip_mut().get_track_mut(*track) {
779                        t.set_key_value(*key, old_value.clone());
780                    }
781                }
782                _ => {}
783            }
784            self.redo_stack.push(edit);
785        }
786    }
787
788    pub fn redo(&mut self) {
789        if let Some(edit) = self.redo_stack.pop() {
790            match &edit {
791                TimelineEdit::InsertKey { track, key } => {
792                    if let Some(t) = self.timeline.clip_mut().get_track_mut(*track) {
793                        t.insert_key(key.time, key.value.clone());
794                    }
795                }
796                TimelineEdit::MoveKey { track, key, new_time, .. } => {
797                    if let Some(t) = self.timeline.clip_mut().get_track_mut(*track) {
798                        t.move_key(*key, *new_time);
799                    }
800                }
801                _ => {}
802            }
803            self.undo_stack.push(edit);
804        }
805    }
806
807    // ── Auto-key ──────────────────────────────────────────────────────────
808
809    pub fn auto_key_float(&mut self, track_id: TrackId, value: f32) {
810        if self.auto_key && self.timeline.state == PlaybackState::Recording {
811            let t = self.timeline.playhead;
812            self.insert_key(track_id, t, TrackValue::Float(value));
813        }
814    }
815
816    pub fn auto_key_quat(&mut self, track_id: TrackId, q: Quat) {
817        if self.auto_key && self.timeline.state == PlaybackState::Recording {
818            let t = self.timeline.playhead;
819            self.insert_key(track_id, t, TrackValue::Quat(q));
820        }
821    }
822
823    // ── Display ───────────────────────────────────────────────────────────
824
825    pub fn status_line(&self) -> String {
826        let tl = &self.timeline;
827        let clip = tl.clip();
828        format!(
829            "Timeline [{:?}] {:?} — t={:.3}s frame={} | {} tracks {} keys | {:.1}px/s",
830            tl.state, tl.loop_mode,
831            tl.playhead, tl.current_frame(),
832            clip.track_count(), clip.total_keys(),
833            self.px_per_sec,
834        )
835    }
836}
837
838impl Default for TimelineEditor { fn default() -> Self { Self::new() } }
839
840// ─────────────────────────────────────────────────────────────────────────────
841// Tests
842// ─────────────────────────────────────────────────────────────────────────────
843
844#[cfg(test)]
845mod tests {
846    use super::*;
847
848    #[test]
849    fn linear_interpolation() {
850        let mut track = AnimTrack::new(TrackId(1), TrackTarget::KitFloat("bloom".into()));
851        track.insert_key(0.0, TrackValue::Float(0.0));
852        track.insert_key(1.0, TrackValue::Float(2.0));
853        let v = track.sample(0.5).unwrap().as_float().unwrap();
854        assert!((v - 1.0).abs() < 1e-5);
855    }
856
857    #[test]
858    fn step_holds_left() {
859        let mut track = AnimTrack::new(TrackId(1), TrackTarget::KitFloat("x".into()));
860        track.insert_key(0.0, TrackValue::Float(1.0));
861        track.insert_key(1.0, TrackValue::Float(3.0));
862        track.keys[0].interp = CurveInterp::Step;
863        let v = track.sample(0.5).unwrap().as_float().unwrap();
864        assert!((v - 1.0).abs() < 1e-5);
865    }
866
867    #[test]
868    fn clamps_before_first_key() {
869        let mut track = AnimTrack::new(TrackId(1), TrackTarget::KitFloat("x".into()));
870        track.insert_key(1.0, TrackValue::Float(5.0));
871        let v = track.sample(0.0).unwrap().as_float().unwrap();
872        assert!((v - 5.0).abs() < 1e-5);
873    }
874
875    #[test]
876    fn timeline_step_advances() {
877        let mut ed = TimelineEditor::new();
878        ed.timeline.play();
879        ed.timeline.seek(0.0);
880        let snap = ed.timeline.step(0.1);
881        assert!(snap.time >= 0.0);
882    }
883
884    #[test]
885    fn undo_insert() {
886        let mut ed = TimelineEditor::new();
887        let tid = ed.add_track(TrackTarget::KitFloat("bloom".into()));
888        ed.insert_key(tid, 0.5, TrackValue::Float(1.0));
889        let count_before = ed.timeline.clip().get_track(tid).unwrap().key_count();
890        assert_eq!(count_before, 1);
891        ed.undo(); // undo insert
892        let count_after = ed.timeline.clip().get_track(tid).unwrap().key_count();
893        assert_eq!(count_after, 0);
894    }
895
896    #[test]
897    fn bounce_easing() {
898        let v = EaseKind::BounceOut.apply(1.0);
899        assert!((v - 1.0).abs() < 1e-5);
900        let v = EaseKind::BounceOut.apply(0.0);
901        assert!(v.abs() < 1e-5);
902    }
903
904    #[test]
905    fn quat_slerp_track() {
906        let mut track = AnimTrack::new(TrackId(1), TrackTarget::BoneRotation("Head".into()));
907        track.insert_key(0.0, TrackValue::Quat(Quat::IDENTITY));
908        let target = Quat::from_rotation_y(std::f32::consts::FRAC_PI_2);
909        track.insert_key(1.0, TrackValue::Quat(target));
910        let mid = track.sample(0.5).unwrap().as_quat().unwrap();
911        let expected = Quat::IDENTITY.slerp(target, 0.5);
912        assert!((mid.dot(expected)).abs() > 0.999);
913    }
914}