1use glam::{Vec2, Vec3, Vec4, Quat};
31use std::collections::HashMap;
32
33pub type Time = f32;
39
40#[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
55pub struct KeyId(pub u32);
56
57#[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 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
116pub enum CurveInterp {
117 Step,
119 #[default]
120 Linear,
122 CubicHermite,
124 CatmullRom,
126 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#[derive(Debug, Clone)]
190pub struct Keyframe {
191 pub id: KeyId,
192 pub time: Time,
193 pub value: TrackValue,
194 pub tan_in: Option<f32>,
196 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#[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#[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 pub fn insert_key(&mut self, time: Time, value: TrackValue) -> KeyId {
295 let id = KeyId(self.next_key);
296 self.next_key += 1;
297 self.keys.retain(|k| (k.time - time).abs() > 1e-5);
299 let key = Keyframe::new(id, time, value);
300 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 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 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 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 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 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 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#[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 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#[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#[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#[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 pub snap_frames: bool,
499 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 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 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 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#[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#[derive(Debug)]
687pub struct TimelineEditor {
688 pub timeline: Timeline,
689 pub selection: KeySelection,
690 undo_stack: Vec<TimelineEdit>,
691 redo_stack: Vec<TimelineEdit>,
692 pub px_per_sec: f32,
694 pub scroll_y: f32,
696 pub curve_mode: bool,
698 pub auto_key: bool,
700 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 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 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 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 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 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#[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(); 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}