Skip to main content

proof_engine/editor/
cinematic_sequencer.rs

1#[allow(dead_code, unused_variables, unused_mut, unused_imports)]
2
3use glam::{Vec2, Vec3, Vec4, Quat, Mat4};
4use std::collections::{HashMap, VecDeque, HashSet, BTreeMap};
5
6// ============================================================
7// CONSTANTS
8// ============================================================
9
10const EPSILON: f32 = 1e-6;
11const MAX_UNDO_DEPTH: usize = 256;
12const SMPTE_FRAMERATES: &[f32] = &[23.976, 24.0, 25.0, 29.97, 30.0, 48.0, 60.0];
13const DEFAULT_FPS: f32 = 30.0;
14const MAX_SEQUENCE_DURATION: f64 = 86400.0; // 24 hours in seconds
15const CAMERA_SHAKE_TRAUMA_DECAY: f32 = 1.5;
16const LETTERBOX_ASPECT: f32 = 2.39; // CinemaScope
17
18// ============================================================
19// UTILITY MATH
20// ============================================================
21
22fn lerp(a: f32, b: f32, t: f32) -> f32 {
23    a + (b - a) * t
24}
25
26fn lerp_f64(a: f64, b: f64, t: f64) -> f64 {
27    a + (b - a) * t
28}
29
30fn lerp_vec3(a: Vec3, b: Vec3, t: f32) -> Vec3 {
31    a + (b - a) * t
32}
33
34pub(crate) fn lerp_vec4(a: Vec4, b: Vec4, t: f32) -> Vec4 {
35    a + (b - a) * t
36}
37
38fn clamp01(t: f32) -> f32 {
39    t.clamp(0.0, 1.0)
40}
41
42fn smooth_step(t: f32) -> f32 {
43    let t = clamp01(t);
44    t * t * (3.0 - 2.0 * t)
45}
46
47fn smoother_step(t: f32) -> f32 {
48    let t = clamp01(t);
49    t * t * t * (t * (t * 6.0 - 15.0) + 10.0)
50}
51
52fn cubic_hermite(p0: f32, m0: f32, p1: f32, m1: f32, t: f32) -> f32 {
53    let t2 = t * t;
54    let t3 = t2 * t;
55    (2.0 * t3 - 3.0 * t2 + 1.0) * p0
56        + (t3 - 2.0 * t2 + t) * m0
57        + (-2.0 * t3 + 3.0 * t2) * p1
58        + (t3 - t2) * m1
59}
60
61fn cubic_hermite_derivative(p0: f32, m0: f32, p1: f32, m1: f32, t: f32) -> f32 {
62    let t2 = t * t;
63    (6.0 * t2 - 6.0 * t) * p0
64        + (3.0 * t2 - 4.0 * t + 1.0) * m0
65        + (-6.0 * t2 + 6.0 * t) * p1
66        + (3.0 * t2 - 2.0 * t) * m1
67}
68
69fn catmull_rom_4pt(p0: f32, p1: f32, p2: f32, p3: f32, t: f32) -> f32 {
70    let t2 = t * t;
71    let t3 = t2 * t;
72    0.5 * (
73        (-t3 + 2.0 * t2 - t) * p0
74        + (3.0 * t3 - 5.0 * t2 + 2.0) * p1
75        + (-3.0 * t3 + 4.0 * t2 + t) * p2
76        + (t3 - t2) * p3
77    )
78}
79
80fn catmull_rom_vec3(p0: Vec3, p1: Vec3, p2: Vec3, p3: Vec3, t: f32) -> Vec3 {
81    Vec3::new(
82        catmull_rom_4pt(p0.x, p1.x, p2.x, p3.x, t),
83        catmull_rom_4pt(p0.y, p1.y, p2.y, p3.y, t),
84        catmull_rom_4pt(p0.z, p1.z, p2.z, p3.z, t),
85    )
86}
87
88pub(crate) fn value_noise_1d(x: f32) -> f32 {
89    let xi = x.floor() as i32;
90    let xf = x - x.floor();
91    let h0 = hash_f32(xi);
92    let h1 = hash_f32(xi + 1);
93    lerp(h0, h1, smooth_step(xf))
94}
95
96fn hash_f32(n: i32) -> f32 {
97    let n = (n << 13) ^ n;
98    let n = n.wrapping_mul(n.wrapping_mul(n.wrapping_mul(15731) + 789221) + 1376312589);
99    1.0 - (n & 0x7fffffff) as f32 / 1073741824.0
100}
101
102fn perlin_noise_2d(x: f32, y: f32) -> f32 {
103    let xi = x.floor() as i32;
104    let yi = y.floor() as i32;
105    let xf = x - x.floor();
106    let yf = y - y.floor();
107    let ux = smooth_step(xf);
108    let uy = smooth_step(yf);
109
110    let grad = |ix: i32, iy: i32, fx: f32, fy: f32| -> f32 {
111        let h = (hash_f32(ix.wrapping_mul(1619) ^ iy.wrapping_mul(31337)) * 4.0) as i32 & 3;
112        match h & 3 {
113            0 =>  fx + fy,
114            1 => -fx + fy,
115            2 =>  fx - fy,
116            _ => -fx - fy,
117        }
118    };
119
120    let n00 = grad(xi,     yi,     xf,       yf);
121    let n10 = grad(xi + 1, yi,     xf - 1.0, yf);
122    let n01 = grad(xi,     yi + 1, xf,       yf - 1.0);
123    let n11 = grad(xi + 1, yi + 1, xf - 1.0, yf - 1.0);
124
125    let nx0 = lerp(n00, n10, ux);
126    let nx1 = lerp(n01, n11, ux);
127    lerp(nx0, nx1, uy)
128}
129
130fn fbm_noise(x: f32, y: f32, octaves: usize) -> f32 {
131    let mut val = 0.0_f32;
132    let mut amplitude = 0.5_f32;
133    let mut frequency = 1.0_f32;
134    for _ in 0..octaves {
135        val += perlin_noise_2d(x * frequency, y * frequency) * amplitude;
136        amplitude *= 0.5;
137        frequency *= 2.0;
138    }
139    val
140}
141
142fn safe_normalize_f32(x: f32) -> f32 {
143    if x.abs() < EPSILON { 0.0 } else { x.signum() }
144}
145
146// ============================================================
147// TIMECODE (SMPTE)
148// ============================================================
149
150#[derive(Clone, Debug, Copy, PartialEq, Eq, Hash)]
151pub struct Timecode {
152    pub hours:   u32,
153    pub minutes: u32,
154    pub seconds: u32,
155    pub frames:  u32,
156}
157
158impl Timecode {
159    pub fn new(hours: u32, minutes: u32, seconds: u32, frames: u32) -> Self {
160        Timecode { hours, minutes, seconds, frames }
161    }
162
163    pub fn from_frame(frame: u64, fps: f32) -> Self {
164        let fps_int = fps.round() as u64;
165        let h = frame / (3600 * fps_int);
166        let rem = frame % (3600 * fps_int);
167        let m = rem / (60 * fps_int);
168        let rem2 = rem % (60 * fps_int);
169        let s = rem2 / fps_int;
170        let f = rem2 % fps_int;
171        Timecode {
172            hours:   h as u32,
173            minutes: m as u32,
174            seconds: s as u32,
175            frames:  f as u32,
176        }
177    }
178
179    pub fn to_frame(&self, fps: f32) -> u64 {
180        let fps_int = fps.round() as u64;
181        self.hours   as u64 * 3600 * fps_int
182            + self.minutes as u64 * 60  * fps_int
183            + self.seconds as u64       * fps_int
184            + self.frames  as u64
185    }
186
187    pub fn to_seconds(&self, fps: f32) -> f64 {
188        self.to_frame(fps) as f64 / fps as f64
189    }
190
191    pub fn from_seconds(secs: f64, fps: f32) -> Self {
192        let frame = (secs * fps as f64).floor() as u64;
193        Self::from_frame(frame, fps)
194    }
195
196    pub fn to_string(&self) -> String {
197        format!("{:02}:{:02}:{:02}:{:02}",
198            self.hours, self.minutes, self.seconds, self.frames)
199    }
200
201    pub fn parse(s: &str, fps: f32) -> Option<Self> {
202        let parts: Vec<&str> = s.split(':').collect();
203        if parts.len() != 4 { return None; }
204        Some(Timecode {
205            hours:   parts[0].parse().ok()?,
206            minutes: parts[1].parse().ok()?,
207            seconds: parts[2].parse().ok()?,
208            frames:  parts[3].parse().ok()?,
209        })
210    }
211
212    pub fn add_frames(&self, frames: i64, fps: f32) -> Self {
213        let total = self.to_frame(fps) as i64 + frames;
214        if total < 0 { Self::new(0, 0, 0, 0) }
215        else { Self::from_frame(total as u64, fps) }
216    }
217
218    pub fn subtract(&self, other: &Timecode, fps: f32) -> i64 {
219        self.to_frame(fps) as i64 - other.to_frame(fps) as i64
220    }
221}
222
223/// Drop-frame timecode correction for 29.97fps
224pub fn to_drop_frame(frame: u64, fps: f32) -> Timecode {
225    // SMPTE drop-frame: skip frames 0 and 1 at the start of each minute,
226    // except every 10th minute
227    let fps_round = fps.round() as u64;
228    let drop_frames = (fps_round as f64 * 0.066666).round() as u64; // 2 for 29.97
229    let frames_per_10_min = (fps * 60.0 * 10.0).round() as u64;
230    let frames_per_1_min  = (fps * 60.0).round() as u64 - drop_frames;
231    let ten_min_chunks = frame / frames_per_10_min;
232    let remain = frame % frames_per_10_min;
233    let minute_in_chunk = if remain < fps_round {
234        0
235    } else {
236        (remain - fps_round) / frames_per_1_min + 1
237    };
238    let frame_in_min = if remain < fps_round {
239        remain
240    } else {
241        (remain - fps_round) % frames_per_1_min + drop_frames
242    };
243    let total_mins = ten_min_chunks * 10 + minute_in_chunk;
244    Timecode {
245        hours:   (total_mins / 60) as u32,
246        minutes: (total_mins % 60) as u32,
247        seconds: (frame_in_min / fps_round) as u32,
248        frames:  (frame_in_min % fps_round) as u32,
249    }
250}
251
252// ============================================================
253// FRAME RATE CONVERSION
254// ============================================================
255
256#[derive(Clone, Debug, Copy, PartialEq)]
257pub enum FrameRate {
258    Fps23_976,
259    Fps24,
260    Fps25,
261    Fps29_97,
262    Fps30,
263    Fps48,
264    Fps60,
265    Custom(f32),
266}
267
268impl FrameRate {
269    pub fn fps(&self) -> f32 {
270        match self {
271            FrameRate::Fps23_976 => 23.976,
272            FrameRate::Fps24     => 24.0,
273            FrameRate::Fps25     => 25.0,
274            FrameRate::Fps29_97  => 29.97,
275            FrameRate::Fps30     => 30.0,
276            FrameRate::Fps48     => 48.0,
277            FrameRate::Fps60     => 60.0,
278            FrameRate::Custom(f) => *f,
279        }
280    }
281
282    pub fn is_drop_frame(&self) -> bool {
283        matches!(self, FrameRate::Fps29_97)
284    }
285
286    pub fn convert_frame(frame: u64, from: FrameRate, to: FrameRate) -> u64 {
287        let from_fps = from.fps() as f64;
288        let to_fps   = to.fps()   as f64;
289        (frame as f64 * to_fps / from_fps).round() as u64
290    }
291
292    pub fn frame_duration_seconds(&self) -> f64 {
293        1.0 / self.fps() as f64
294    }
295
296    pub fn seconds_to_frame(&self, secs: f64) -> u64 {
297        (secs * self.fps() as f64).floor() as u64
298    }
299
300    pub fn frame_to_seconds(&self, frame: u64) -> f64 {
301        frame as f64 / self.fps() as f64
302    }
303}
304
305// ============================================================
306// KEYFRAME INTERPOLATION TYPES
307// ============================================================
308
309#[derive(Clone, Debug, PartialEq)]
310pub enum InterpType {
311    Constant,
312    Linear,
313    Cubic,      // Catmull-Rom
314    Bezier,     // Bezier with tangent handles
315    Stepped,    // hold value until next key
316}
317
318#[derive(Clone, Debug)]
319pub struct BezierHandle {
320    pub in_tangent:  Vec2,  // (dt, dv) relative to keyframe
321    pub out_tangent: Vec2,
322}
323
324impl BezierHandle {
325    pub fn auto(prev_val: f32, cur_val: f32, next_val: f32) -> Self {
326        // Auto-tangent: one-third of the chord to prev/next
327        let out_slope = (next_val - prev_val) * 0.5;
328        BezierHandle {
329            in_tangent:  Vec2::new(-0.333, -out_slope * 0.333),
330            out_tangent: Vec2::new( 0.333,  out_slope * 0.333),
331        }
332    }
333
334    pub fn flat() -> Self {
335        BezierHandle {
336            in_tangent:  Vec2::new(-0.333, 0.0),
337            out_tangent: Vec2::new( 0.333, 0.0),
338        }
339    }
340
341    pub fn linear(prev_t: f32, prev_v: f32, cur_t: f32, cur_v: f32, next_t: f32, next_v: f32) -> Self {
342        let slope_in  = if (cur_t - prev_t).abs() > EPSILON { (cur_v - prev_v) / (cur_t - prev_t) } else { 0.0 };
343        let slope_out = if (next_t - cur_t).abs() > EPSILON { (next_v - cur_v) / (next_t - cur_t) } else { 0.0 };
344        let dt = 0.333;
345        BezierHandle {
346            in_tangent:  Vec2::new(-dt, -slope_in  * dt),
347            out_tangent: Vec2::new( dt,  slope_out * dt),
348        }
349    }
350}
351
352// ============================================================
353// KEYFRAME (GENERIC)
354// ============================================================
355
356#[derive(Clone, Debug)]
357pub struct Keyframe<T: Clone + std::fmt::Debug> {
358    pub time: f64,       // in seconds
359    pub value: T,
360    pub interp: InterpType,
361    pub bezier_handle: Option<BezierHandle>,
362}
363
364impl<T: Clone + std::fmt::Debug> Keyframe<T> {
365    pub fn new(time: f64, value: T) -> Self {
366        Keyframe { time, value, interp: InterpType::Linear, bezier_handle: None }
367    }
368
369    pub fn with_interp(mut self, interp: InterpType) -> Self {
370        self.interp = interp;
371        self
372    }
373
374    pub fn with_bezier(mut self, handle: BezierHandle) -> Self {
375        self.bezier_handle = Some(handle);
376        self
377    }
378}
379
380// ============================================================
381// KEYFRAME EVALUATOR FOR f32
382// ============================================================
383
384#[derive(Clone, Debug)]
385pub struct FloatCurve {
386    pub keys: Vec<Keyframe<f32>>,
387    pub pre_infinity:  InfinityMode,
388    pub post_infinity: InfinityMode,
389    pub name: String,
390}
391
392#[derive(Clone, Debug, PartialEq)]
393pub enum InfinityMode {
394    Constant,
395    Linear,
396    Cycle,
397    CycleWithOffset,
398    Oscillate,
399}
400
401impl FloatCurve {
402    pub fn new(name: &str) -> Self {
403        FloatCurve {
404            keys: Vec::new(),
405            pre_infinity:  InfinityMode::Constant,
406            post_infinity: InfinityMode::Constant,
407            name: name.to_string(),
408        }
409    }
410
411    pub fn add_key(&mut self, time: f64, value: f32, interp: InterpType) {
412        let idx = self.keys.partition_point(|k| k.time < time);
413        self.keys.insert(idx, Keyframe::new(time, value).with_interp(interp));
414        self.recompute_auto_tangents();
415    }
416
417    pub fn add_key_bezier(&mut self, time: f64, value: f32, handle: BezierHandle) {
418        let idx = self.keys.partition_point(|k| k.time < time);
419        self.keys.insert(idx, Keyframe::new(time, value)
420            .with_interp(InterpType::Bezier)
421            .with_bezier(handle));
422    }
423
424    pub fn remove_key(&mut self, index: usize) {
425        if index < self.keys.len() {
426            self.keys.remove(index);
427            self.recompute_auto_tangents();
428        }
429    }
430
431    pub fn recompute_auto_tangents(&mut self) {
432        let n = self.keys.len();
433        for i in 0..n {
434            if self.keys[i].interp != InterpType::Bezier {
435                // Skip — will use Catmull-Rom naturally
436                continue;
437            }
438            let prev_v = if i > 0 { self.keys[i-1].value } else { self.keys[i].value };
439            let next_v = if i+1 < n { self.keys[i+1].value } else { self.keys[i].value };
440            let cur_v  = self.keys[i].value;
441            let handle = BezierHandle::auto(prev_v, cur_v, next_v);
442            self.keys[i].bezier_handle = Some(handle);
443        }
444    }
445
446    pub fn evaluate(&self, time: f64) -> f32 {
447        let n = self.keys.len();
448        if n == 0 { return 0.0; }
449        if n == 1 { return self.keys[0].value; }
450
451        let first_time = self.keys[0].time;
452        let last_time  = self.keys[n - 1].time;
453
454        // Handle infinity modes
455        let time = if time < first_time {
456            match self.pre_infinity {
457                InfinityMode::Constant  => first_time,
458                InfinityMode::Linear    => first_time,
459                InfinityMode::Cycle     => {
460                    let dur = last_time - first_time;
461                    if dur < 1e-9 { first_time }
462                    else {
463                        let off = ((first_time - time) / dur).ceil() * dur;
464                        time + off
465                    }
466                }
467                InfinityMode::Oscillate => {
468                    let dur = last_time - first_time;
469                    if dur < 1e-9 { return self.keys[0].value; }
470                    let rel = (first_time - time) % (2.0 * dur);
471                    if rel < dur { first_time + rel } else { last_time - (rel - dur) }
472                }
473                InfinityMode::CycleWithOffset => first_time,
474            }
475        } else if time > last_time {
476            match self.post_infinity {
477                InfinityMode::Constant  => last_time,
478                InfinityMode::Linear    => last_time,
479                InfinityMode::Cycle     => {
480                    let dur = last_time - first_time;
481                    if dur < 1e-9 { last_time }
482                    else {
483                        let off = ((time - last_time) / dur).ceil() * dur;
484                        time - off
485                    }
486                }
487                InfinityMode::Oscillate => {
488                    let dur = last_time - first_time;
489                    if dur < 1e-9 { return self.keys[n-1].value; }
490                    let rel = (time - first_time) % (2.0 * dur);
491                    if rel < dur { first_time + rel } else { last_time - (rel - dur) }
492                }
493                InfinityMode::CycleWithOffset => last_time,
494            }
495        } else {
496            time
497        };
498
499        let idx = self.keys.partition_point(|k| k.time <= time);
500        if idx == 0 { return self.keys[0].value; }
501        if idx >= n { return self.keys[n-1].value; }
502
503        let k0 = &self.keys[idx - 1];
504        let k1 = &self.keys[idx];
505        let dt = (k1.time - k0.time) as f32;
506        let t  = if dt.abs() < EPSILON { 0.0 }
507                 else { ((time - k0.time) as f32) / dt };
508
509        match k0.interp {
510            InterpType::Constant | InterpType::Stepped => k0.value,
511            InterpType::Linear   => lerp(k0.value, k1.value, t),
512            InterpType::Cubic    => {
513                let p0 = if idx >= 2 { self.keys[idx - 2].value } else { k0.value };
514                let p3 = if idx + 1 < n { self.keys[idx + 1].value } else { k1.value };
515                catmull_rom_4pt(p0, k0.value, k1.value, p3, t)
516            }
517            InterpType::Bezier   => {
518                // Use bezier handle tangents for cubic hermite
519                let m0 = k0.bezier_handle.as_ref()
520                    .map(|h| h.out_tangent.y / h.out_tangent.x.max(EPSILON))
521                    .unwrap_or(0.0) * dt;
522                let m1 = k1.bezier_handle.as_ref()
523                    .map(|h| h.in_tangent.y  / h.in_tangent.x.abs().max(EPSILON))
524                    .unwrap_or(0.0) * dt;
525                cubic_hermite(k0.value, m0, k1.value, m1, t)
526            }
527        }
528    }
529
530    pub fn duration(&self) -> f64 {
531        match (self.keys.first(), self.keys.last()) {
532            (Some(f), Some(l)) => l.time - f.time,
533            _ => 0.0,
534        }
535    }
536
537    pub fn value_range(&self) -> (f32, f32) {
538        if self.keys.is_empty() { return (0.0, 1.0); }
539        let min = self.keys.iter().map(|k| k.value).fold(f32::MAX, f32::min);
540        let max = self.keys.iter().map(|k| k.value).fold(f32::MIN, f32::max);
541        (min, max)
542    }
543}
544
545// ============================================================
546// TRACK TYPES ENUM
547// ============================================================
548
549#[derive(Clone, Debug, PartialEq)]
550pub enum TrackKind {
551    Camera,
552    Actor,
553    Animation,
554    Audio,
555    Vfx,
556    Light,
557    PostFx,
558    Subtitle,
559    Event,
560    Transform,
561    BlendShape,
562    Visibility,
563    TimeDilation,
564    Cinematic,
565}
566
567// ============================================================
568// TRACK BASE
569// ============================================================
570
571#[derive(Clone, Debug)]
572pub struct TrackBase {
573    pub id: u64,
574    pub name: String,
575    pub kind: TrackKind,
576    pub enabled: bool,
577    pub locked:  bool,
578    pub solo:    bool,
579    pub muted:   bool,
580    pub color:   Vec4,
581    pub layer:   u32,
582    pub blend_mode: BlendMode,
583    pub weight: f32,
584}
585
586#[derive(Clone, Debug, PartialEq)]
587pub enum BlendMode {
588    Override,
589    Additive,
590    Multiply,
591    Screen,
592    Lerp,
593}
594
595impl TrackBase {
596    pub fn new(id: u64, name: &str, kind: TrackKind) -> Self {
597        TrackBase {
598            id, name: name.to_string(), kind,
599            enabled: true, locked: false, solo: false, muted: false,
600            color:  Vec4::new(0.4, 0.6, 1.0, 1.0),
601            layer:  0,
602            blend_mode: BlendMode::Override,
603            weight: 1.0,
604        }
605    }
606}
607
608// ============================================================
609// CAMERA TRACK
610// ============================================================
611
612#[derive(Clone, Debug)]
613pub struct CameraKeyframe {
614    pub time: f64,
615    pub position:  Vec3,
616    pub rotation:  Quat,
617    pub fov:       f32,
618    pub near_clip: f32,
619    pub far_clip:  f32,
620    pub focal_length: f32,
621    pub aperture:     f32,
622    pub focus_distance: f32,
623    pub interp: InterpType,
624}
625
626impl CameraKeyframe {
627    pub fn new(time: f64, position: Vec3, rotation: Quat) -> Self {
628        CameraKeyframe {
629            time, position, rotation,
630            fov: 60.0,
631            near_clip: 0.1,
632            far_clip: 10000.0,
633            focal_length: 35.0,
634            aperture: 2.8,
635            focus_distance: 10.0,
636            interp: InterpType::Linear,
637        }
638    }
639}
640
641#[derive(Clone, Debug, Default)]
642pub struct CameraShakeState {
643    pub trauma: f32,         // [0,1], drives shake intensity
644    pub time:   f32,
645    pub offset: Vec3,
646    pub rotation_offset: Vec3,  // Euler angles in degrees
647    pub frequency: f32,
648    pub amplitude_position: f32,
649    pub amplitude_rotation: f32,
650    pub octaves: usize,
651}
652
653impl CameraShakeState {
654    pub fn new() -> Self {
655        CameraShakeState {
656            trauma: 0.0,
657            time:   0.0,
658            offset: Vec3::ZERO,
659            rotation_offset: Vec3::ZERO,
660            frequency: 12.0,
661            amplitude_position: 0.3,
662            amplitude_rotation: 1.5,
663            octaves: 3,
664        }
665    }
666
667    pub fn add_trauma(&mut self, amount: f32) {
668        self.trauma = (self.trauma + amount).min(1.0);
669    }
670
671    pub fn update(&mut self, dt: f32) {
672        if self.trauma <= 0.0 { return; }
673        self.time += dt;
674        let shake = self.trauma * self.trauma; // square for more impactful feel
675        self.offset = Vec3::new(
676            fbm_noise(self.time * self.frequency,          0.0, self.octaves) * shake * self.amplitude_position,
677            fbm_noise(self.time * self.frequency + 31.7,   0.0, self.octaves) * shake * self.amplitude_position,
678            fbm_noise(self.time * self.frequency + 74.3,   0.0, self.octaves) * shake * self.amplitude_position,
679        );
680        self.rotation_offset = Vec3::new(
681            fbm_noise(self.time * self.frequency + 12.1,  10.0, self.octaves) * shake * self.amplitude_rotation,
682            fbm_noise(self.time * self.frequency + 24.2,  10.0, self.octaves) * shake * self.amplitude_rotation,
683            fbm_noise(self.time * self.frequency + 36.3,  10.0, self.octaves) * shake * self.amplitude_rotation,
684        );
685        self.trauma -= CAMERA_SHAKE_TRAUMA_DECAY * dt;
686        self.trauma = self.trauma.max(0.0);
687    }
688
689    pub fn is_active(&self) -> bool { self.trauma > 0.01 }
690}
691
692#[derive(Clone, Debug)]
693pub struct LensDistortion {
694    pub k1: f32,  // radial distortion coefficient 1
695    pub k2: f32,  // radial distortion coefficient 2
696    pub p1: f32,  // tangential distortion 1
697    pub p2: f32,  // tangential distortion 2
698}
699
700impl LensDistortion {
701    pub fn none() -> Self { LensDistortion { k1: 0.0, k2: 0.0, p1: 0.0, p2: 0.0 } }
702
703    pub fn barrel(amount: f32) -> Self {
704        LensDistortion { k1: -amount, k2: amount * 0.1, p1: 0.0, p2: 0.0 }
705    }
706
707    pub fn pincushion(amount: f32) -> Self {
708        LensDistortion { k1: amount, k2: -amount * 0.1, p1: 0.0, p2: 0.0 }
709    }
710
711    pub fn distort_uv(&self, uv: Vec2) -> Vec2 {
712        let centered = uv - Vec2::new(0.5, 0.5);
713        let r2 = centered.dot(centered);
714        let r4 = r2 * r2;
715        let radial = 1.0 + self.k1 * r2 + self.k2 * r4;
716        let dx = 2.0 * self.p1 * centered.x * centered.y + self.p2 * (r2 + 2.0 * centered.x * centered.x);
717        let dy = self.p1 * (r2 + 2.0 * centered.y * centered.y) + 2.0 * self.p2 * centered.x * centered.y;
718        Vec2::new(
719            centered.x * radial + dx + 0.5,
720            centered.y * radial + dy + 0.5,
721        )
722    }
723}
724
725#[derive(Clone, Debug)]
726pub struct DepthOfFieldKeyframe {
727    pub time: f64,
728    pub focus_distance: f32,
729    pub aperture:       f32,  // f-stop
730    pub focal_length:   f32,  // mm
731    pub sensor_width:   f32,  // mm, default 36
732}
733
734impl DepthOfFieldKeyframe {
735    pub fn new(time: f64) -> Self {
736        DepthOfFieldKeyframe {
737            time,
738            focus_distance: 10.0,
739            aperture: 2.8,
740            focal_length: 50.0,
741            sensor_width: 36.0,
742        }
743    }
744
745    /// Hyperfocal distance H = f²/(N*c) where c is circle of confusion
746    pub fn hyperfocal(&self, coc: f32) -> f32 {
747        let f = self.focal_length / 1000.0; // convert mm to m
748        let coc_m = coc / 1000.0;
749        f * f / (self.aperture * coc_m)
750    }
751
752    /// Near focus limit
753    pub fn near_limit(&self) -> f32 {
754        let h = self.hyperfocal(0.029);
755        let d = self.focus_distance;
756        d * (h - self.focal_length / 1000.0) / (h + d - 2.0 * self.focal_length / 1000.0)
757    }
758
759    /// Far focus limit
760    pub fn far_limit(&self) -> f32 {
761        let h = self.hyperfocal(0.029);
762        let d = self.focus_distance;
763        let denom = h - d;
764        if denom.abs() < EPSILON { f32::MAX }
765        else { d * (h - self.focal_length / 1000.0) / denom }
766    }
767
768    /// Total depth of field
769    pub fn dof_total(&self) -> f32 {
770        let near = self.near_limit();
771        let far  = self.far_limit();
772        if far > 1e6 { f32::MAX } else { far - near }
773    }
774}
775
776#[derive(Clone, Debug)]
777pub struct CameraTrack {
778    pub base: TrackBase,
779    pub keyframes: Vec<CameraKeyframe>,
780    pub dof_keyframes: Vec<DepthOfFieldKeyframe>,
781    pub shake_state: CameraShakeState,
782    pub lens_distortion: LensDistortion,
783    pub target_entity: Option<u64>,  // entity to look at (overrides rotation)
784    pub look_at_blend: f32,          // 0 = use keyframe rotation, 1 = use look-at
785    pub fov_curve: FloatCurve,
786}
787
788impl CameraTrack {
789    pub fn new(id: u64, name: &str) -> Self {
790        CameraTrack {
791            base: TrackBase::new(id, name, TrackKind::Camera),
792            keyframes: Vec::new(),
793            dof_keyframes: Vec::new(),
794            shake_state: CameraShakeState::new(),
795            lens_distortion: LensDistortion::none(),
796            target_entity: None,
797            look_at_blend: 0.0,
798            fov_curve: FloatCurve::new("FOV"),
799        }
800    }
801
802    pub fn add_keyframe(&mut self, kf: CameraKeyframe) {
803        let idx = self.keyframes.partition_point(|k| k.time < kf.time);
804        self.keyframes.insert(idx, kf);
805    }
806
807    pub fn evaluate_position(&self, time: f64) -> Vec3 {
808        let n = self.keyframes.len();
809        if n == 0 { return Vec3::ZERO; }
810        if n == 1 { return self.keyframes[0].position; }
811        let idx = self.keyframes.partition_point(|k| k.time <= time);
812        if idx == 0 { return self.keyframes[0].position; }
813        if idx >= n { return self.keyframes[n-1].position; }
814        let k0 = &self.keyframes[idx-1];
815        let k1 = &self.keyframes[idx];
816        let t = ((time - k0.time) / (k1.time - k0.time)) as f32;
817        match k0.interp {
818            InterpType::Constant | InterpType::Stepped => k0.position,
819            InterpType::Linear   => lerp_vec3(k0.position, k1.position, t),
820            InterpType::Cubic    => {
821                let p0 = if idx >= 2 { self.keyframes[idx-2].position } else { k0.position };
822                let p3 = if idx+1 < n { self.keyframes[idx+1].position } else { k1.position };
823                catmull_rom_vec3(p0, k0.position, k1.position, p3, t)
824            }
825            InterpType::Bezier   => lerp_vec3(k0.position, k1.position, smoother_step(t)),
826        }
827    }
828
829    pub fn evaluate_rotation(&self, time: f64) -> Quat {
830        let n = self.keyframes.len();
831        if n == 0 { return Quat::IDENTITY; }
832        if n == 1 { return self.keyframes[0].rotation; }
833        let idx = self.keyframes.partition_point(|k| k.time <= time);
834        if idx == 0 { return self.keyframes[0].rotation; }
835        if idx >= n { return self.keyframes[n-1].rotation; }
836        let k0 = &self.keyframes[idx-1];
837        let k1 = &self.keyframes[idx];
838        let t = ((time - k0.time) / (k1.time - k0.time)) as f32;
839        match k0.interp {
840            InterpType::Constant | InterpType::Stepped => k0.rotation,
841            _ => k0.rotation.slerp(k1.rotation, t),
842        }
843    }
844
845    pub fn evaluate_fov(&self, time: f64) -> f32 {
846        let n = self.keyframes.len();
847        if n == 0 { return 60.0; }
848        if !self.fov_curve.keys.is_empty() {
849            return self.fov_curve.evaluate(time);
850        }
851        let idx = self.keyframes.partition_point(|k| k.time <= time);
852        if idx == 0 { return self.keyframes[0].fov; }
853        if idx >= n { return self.keyframes[n-1].fov; }
854        let k0 = &self.keyframes[idx-1];
855        let k1 = &self.keyframes[idx];
856        let t = ((time - k0.time) / (k1.time - k0.time)) as f32;
857        lerp(k0.fov, k1.fov, t)
858    }
859
860    pub fn evaluate_dof(&self, time: f64) -> DepthOfFieldKeyframe {
861        let n = self.dof_keyframes.len();
862        if n == 0 { return DepthOfFieldKeyframe::new(time); }
863        if n == 1 { return self.dof_keyframes[0].clone(); }
864        let idx = self.dof_keyframes.partition_point(|k| k.time <= time);
865        if idx == 0 { return self.dof_keyframes[0].clone(); }
866        if idx >= n { return self.dof_keyframes[n-1].clone(); }
867        let k0 = &self.dof_keyframes[idx-1];
868        let k1 = &self.dof_keyframes[idx];
869        let t = ((time - k0.time) / (k1.time - k0.time)) as f32;
870        DepthOfFieldKeyframe {
871            time,
872            focus_distance: lerp(k0.focus_distance, k1.focus_distance, t),
873            aperture:       lerp(k0.aperture,       k1.aperture,       t),
874            focal_length:   lerp(k0.focal_length,   k1.focal_length,   t),
875            sensor_width:   lerp(k0.sensor_width,   k1.sensor_width,   t),
876        }
877    }
878
879    pub fn update_shake(&mut self, dt: f32) {
880        self.shake_state.update(dt);
881    }
882
883    pub fn camera_matrix(&self, time: f64) -> Mat4 {
884        let pos = self.evaluate_position(time) + self.shake_state.offset;
885        let rot = self.evaluate_rotation(time);
886        let shake_rot = Quat::from_euler(
887            glam::EulerRot::XYZ,
888            self.shake_state.rotation_offset.x.to_radians(),
889            self.shake_state.rotation_offset.y.to_radians(),
890            self.shake_state.rotation_offset.z.to_radians(),
891        );
892        Mat4::from_rotation_translation(shake_rot * rot, pos)
893    }
894}
895
896// ============================================================
897// ACTOR TRACK
898// ============================================================
899
900#[derive(Clone, Debug)]
901pub struct ActorKeyframe {
902    pub time:     f64,
903    pub position: Vec3,
904    pub rotation: Quat,
905    pub scale:    Vec3,
906    pub interp:   InterpType,
907}
908
909impl ActorKeyframe {
910    pub fn new(time: f64, pos: Vec3, rot: Quat) -> Self {
911        ActorKeyframe { time, position: pos, rotation: rot, scale: Vec3::ONE, interp: InterpType::Linear }
912    }
913}
914
915#[derive(Clone, Debug)]
916pub struct ActorTrack {
917    pub base: TrackBase,
918    pub entity_id: u64,
919    pub keyframes: Vec<ActorKeyframe>,
920    pub root_motion: bool,
921}
922
923impl ActorTrack {
924    pub fn new(id: u64, name: &str, entity_id: u64) -> Self {
925        ActorTrack {
926            base: TrackBase::new(id, name, TrackKind::Actor),
927            entity_id,
928            keyframes: Vec::new(),
929            root_motion: false,
930        }
931    }
932
933    pub fn add_keyframe(&mut self, kf: ActorKeyframe) {
934        let idx = self.keyframes.partition_point(|k| k.time < kf.time);
935        self.keyframes.insert(idx, kf);
936    }
937
938    pub fn evaluate(&self, time: f64) -> (Vec3, Quat, Vec3) {
939        let n = self.keyframes.len();
940        if n == 0 { return (Vec3::ZERO, Quat::IDENTITY, Vec3::ONE); }
941        if n == 1 {
942            let k = &self.keyframes[0];
943            return (k.position, k.rotation, k.scale);
944        }
945        let idx = self.keyframes.partition_point(|k| k.time <= time);
946        if idx == 0 {
947            let k = &self.keyframes[0];
948            return (k.position, k.rotation, k.scale);
949        }
950        if idx >= n {
951            let k = &self.keyframes[n-1];
952            return (k.position, k.rotation, k.scale);
953        }
954        let k0 = &self.keyframes[idx-1];
955        let k1 = &self.keyframes[idx];
956        let t = ((time - k0.time) / (k1.time - k0.time).max(1e-9)) as f32;
957        let t_smooth = match k0.interp {
958            InterpType::Constant | InterpType::Stepped => return (k0.position, k0.rotation, k0.scale),
959            InterpType::Linear   => t,
960            InterpType::Cubic    => {
961                let p0 = if idx >= 2 { self.keyframes[idx-2].position } else { k0.position };
962                let p3 = if idx+1<n { self.keyframes[idx+1].position } else { k1.position };
963                return (
964                    catmull_rom_vec3(p0, k0.position, k1.position, p3, t),
965                    k0.rotation.slerp(k1.rotation, t),
966                    lerp_vec3(k0.scale, k1.scale, t),
967                );
968            }
969            InterpType::Bezier => smoother_step(t),
970        };
971        (
972            lerp_vec3(k0.position, k1.position, t_smooth),
973            k0.rotation.slerp(k1.rotation, t_smooth),
974            lerp_vec3(k0.scale, k1.scale, t_smooth),
975        )
976    }
977
978    pub fn world_matrix(&self, time: f64) -> Mat4 {
979        let (pos, rot, scale) = self.evaluate(time);
980        Mat4::from_scale_rotation_translation(scale, rot, pos)
981    }
982}
983
984// ============================================================
985// ANIMATION TRACK
986// ============================================================
987
988#[derive(Clone, Debug)]
989pub struct AnimationClip {
990    pub clip_id:   u64,
991    pub name:      String,
992    pub duration:  f64,
993    pub loop_clip: bool,
994}
995
996#[derive(Clone, Debug)]
997pub struct AnimationKeyframe {
998    pub time:       f64,
999    pub clip:       AnimationClip,
1000    pub blend_in:   f64,
1001    pub blend_out:  f64,
1002    pub time_scale: f32,
1003    pub weight:     f32,
1004    pub start_time: f64, // offset into clip
1005}
1006
1007impl AnimationKeyframe {
1008    pub fn new(time: f64, clip: AnimationClip) -> Self {
1009        AnimationKeyframe {
1010            time, clip,
1011            blend_in:  0.1,
1012            blend_out: 0.1,
1013            time_scale: 1.0,
1014            weight: 1.0,
1015            start_time: 0.0,
1016        }
1017    }
1018
1019    pub fn clip_time_at(&self, sequence_time: f64) -> f64 {
1020        let local_time = (sequence_time - self.time) * self.time_scale as f64 + self.start_time;
1021        if self.clip.loop_clip {
1022            local_time % self.clip.duration.max(1e-9)
1023        } else {
1024            local_time.clamp(0.0, self.clip.duration)
1025        }
1026    }
1027
1028    pub fn weight_at(&self, sequence_time: f64) -> f32 {
1029        let local_time = sequence_time - self.time;
1030        let end_time   = self.time + self.clip.duration / self.time_scale as f64;
1031        let blend_in_weight  = (local_time / self.blend_in.max(1e-9)).clamp(0.0, 1.0) as f32;
1032        let blend_out_weight = ((end_time - sequence_time) / self.blend_out.max(1e-9)).clamp(0.0, 1.0) as f32;
1033        self.weight * blend_in_weight.min(blend_out_weight)
1034    }
1035}
1036
1037#[derive(Clone, Debug)]
1038pub struct AnimationTrack {
1039    pub base:      TrackBase,
1040    pub entity_id: u64,
1041    pub clips:     Vec<AnimationKeyframe>,
1042    pub blend_tree_weight: FloatCurve,
1043}
1044
1045impl AnimationTrack {
1046    pub fn new(id: u64, name: &str, entity_id: u64) -> Self {
1047        AnimationTrack {
1048            base: TrackBase::new(id, name, TrackKind::Animation),
1049            entity_id,
1050            clips: Vec::new(),
1051            blend_tree_weight: FloatCurve::new("BlendWeight"),
1052        }
1053    }
1054
1055    pub fn add_clip(&mut self, kf: AnimationKeyframe) {
1056        let idx = self.clips.partition_point(|k| k.time < kf.time);
1057        self.clips.insert(idx, kf);
1058    }
1059
1060    pub fn active_clips_at(&self, time: f64) -> Vec<(&AnimationKeyframe, f32)> {
1061        self.clips.iter()
1062            .filter(|kf| {
1063                let end = kf.time + kf.clip.duration / kf.time_scale as f64;
1064                time >= kf.time && time <= end
1065            })
1066            .map(|kf| (kf, kf.weight_at(time)))
1067            .collect()
1068    }
1069}
1070
1071// ============================================================
1072// AUDIO TRACK
1073// ============================================================
1074
1075#[derive(Clone, Debug)]
1076pub struct AudioClipData {
1077    pub clip_id:  u64,
1078    pub name:     String,
1079    pub duration: f64,
1080    pub channels: u32,
1081    pub sample_rate: u32,
1082    pub waveform_preview: Vec<f32>, // downsampled amplitude data for UI
1083}
1084
1085impl AudioClipData {
1086    pub fn new(clip_id: u64, name: &str, duration: f64, sample_rate: u32) -> Self {
1087        AudioClipData {
1088            clip_id, name: name.to_string(), duration,
1089            channels: 2,
1090            sample_rate,
1091            waveform_preview: Vec::new(),
1092        }
1093    }
1094
1095    pub fn generate_dummy_waveform(&mut self, n: usize) {
1096        self.waveform_preview = (0..n).map(|i| {
1097            value_noise_1d(i as f32 * 0.1) * 0.5
1098        }).collect();
1099    }
1100}
1101
1102#[derive(Clone, Debug)]
1103pub struct BeatMarker {
1104    pub time:        f64,
1105    pub beat_number: u32,
1106    pub measure:     u32,
1107    pub is_downbeat: bool,
1108    pub bpm:         f32,
1109}
1110
1111#[derive(Clone, Debug)]
1112pub struct AudioKeyframe {
1113    pub time:        f64,
1114    pub clip:        AudioClipData,
1115    pub volume:      f32,
1116    pub pitch:       f32,
1117    pub pan:         f32,  // -1 = left, 0 = center, 1 = right
1118    pub fade_in:     f64,
1119    pub fade_out:    f64,
1120    pub time_offset: f64,  // offset into clip
1121    pub loop_audio:  bool,
1122    pub duck_others: bool, // sidechain ducking
1123    pub duck_amount: f32,
1124    pub duck_release: f32,
1125}
1126
1127impl AudioKeyframe {
1128    pub fn new(time: f64, clip: AudioClipData) -> Self {
1129        AudioKeyframe {
1130            time, clip,
1131            volume: 1.0,
1132            pitch:  1.0,
1133            pan:    0.0,
1134            fade_in:  0.0,
1135            fade_out: 0.0,
1136            time_offset: 0.0,
1137            loop_audio: false,
1138            duck_others: false,
1139            duck_amount: 0.6,
1140            duck_release: 0.3,
1141        }
1142    }
1143
1144    pub fn volume_at(&self, sequence_time: f64) -> f32 {
1145        let local = sequence_time - self.time;
1146        let end   = self.time + self.clip.duration;
1147        let fade_in_v  = if self.fade_in > 1e-9 { (local / self.fade_in).clamp(0.0, 1.0) as f32 } else { 1.0 };
1148        let fade_out_v = if self.fade_out > 1e-9 { ((end - sequence_time) / self.fade_out).clamp(0.0, 1.0) as f32 } else { 1.0 };
1149        self.volume * fade_in_v.min(fade_out_v)
1150    }
1151}
1152
1153#[derive(Clone, Debug)]
1154pub struct AudioTrack {
1155    pub base:     TrackBase,
1156    pub clips:    Vec<AudioKeyframe>,
1157    pub beat_markers: Vec<BeatMarker>,
1158    pub master_volume_curve: FloatCurve,
1159    pub reverb_wet:  f32,
1160    pub eq_low:      f32,
1161    pub eq_mid:      f32,
1162    pub eq_high:     f32,
1163}
1164
1165impl AudioTrack {
1166    pub fn new(id: u64, name: &str) -> Self {
1167        AudioTrack {
1168            base: TrackBase::new(id, name, TrackKind::Audio),
1169            clips: Vec::new(),
1170            beat_markers: Vec::new(),
1171            master_volume_curve: FloatCurve::new("MasterVolume"),
1172            reverb_wet: 0.0,
1173            eq_low:  0.0,
1174            eq_mid:  0.0,
1175            eq_high: 0.0,
1176        }
1177    }
1178
1179    pub fn add_clip(&mut self, kf: AudioKeyframe) {
1180        let idx = self.clips.partition_point(|k| k.time < kf.time);
1181        self.clips.insert(idx, kf);
1182    }
1183
1184    pub fn volume_at(&self, time: f64) -> f32 {
1185        let master = if self.master_volume_curve.keys.is_empty() {
1186            1.0
1187        } else {
1188            self.master_volume_curve.evaluate(time)
1189        };
1190        master
1191    }
1192
1193    /// Beat detection: generate markers from BPM
1194    pub fn generate_beat_markers(&mut self, bpm: f32, start_time: f64, duration: f64, time_sig: u32) {
1195        self.beat_markers.clear();
1196        let beat_duration = 60.0 / bpm as f64;
1197        let mut t = start_time;
1198        let mut beat_num = 0u32;
1199        let mut measure = 0u32;
1200        while t < start_time + duration {
1201            self.beat_markers.push(BeatMarker {
1202                time: t,
1203                beat_number: beat_num,
1204                measure,
1205                is_downbeat: beat_num % time_sig == 0,
1206                bpm,
1207            });
1208            t += beat_duration;
1209            beat_num += 1;
1210            if beat_num % time_sig == 0 { measure += 1; }
1211        }
1212    }
1213
1214    pub fn nearest_beat(&self, time: f64) -> Option<&BeatMarker> {
1215        self.beat_markers.iter().min_by(|a, b| {
1216            let da = (a.time - time).abs();
1217            let db = (b.time - time).abs();
1218            da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal)
1219        })
1220    }
1221
1222    /// Snap time to nearest beat
1223    pub fn snap_to_beat(&self, time: f64) -> f64 {
1224        self.nearest_beat(time).map(|b| b.time).unwrap_or(time)
1225    }
1226
1227    /// Compute sidechain duck factor at given time
1228    pub fn sidechain_duck_factor_at(&self, time: f64) -> f32 {
1229        for clip in &self.clips {
1230            if !clip.duck_others { continue; }
1231            let end = clip.time + clip.clip.duration;
1232            if time >= clip.time && time <= end {
1233                let local = time - clip.time;
1234                let release_start = end - clip.duck_release as f64;
1235                let duck = if time < release_start {
1236                    1.0 - clip.duck_amount
1237                } else {
1238                    let t_release = ((time - release_start) / clip.duck_release as f64) as f32;
1239                    lerp(1.0 - clip.duck_amount, 1.0, t_release)
1240                };
1241                return duck;
1242            }
1243        }
1244        1.0
1245    }
1246}
1247
1248// ============================================================
1249// VFX TRACK
1250// ============================================================
1251
1252#[derive(Clone, Debug)]
1253pub struct VfxKeyframe {
1254    pub time:        f64,
1255    pub effect_id:   u64,
1256    pub effect_name: String,
1257    pub position:    Vec3,
1258    pub rotation:    Quat,
1259    pub scale:       f32,
1260    pub duration:    f64,
1261    pub delay:       f64,
1262    pub spawn_rate:  f32,
1263    pub loop_vfx:    bool,
1264}
1265
1266impl VfxKeyframe {
1267    pub fn new(time: f64, effect_id: u64, effect_name: &str, position: Vec3) -> Self {
1268        VfxKeyframe {
1269            time, effect_id, effect_name: effect_name.to_string(),
1270            position, rotation: Quat::IDENTITY,
1271            scale: 1.0, duration: 1.0, delay: 0.0,
1272            spawn_rate: 100.0, loop_vfx: false,
1273        }
1274    }
1275}
1276
1277#[derive(Clone, Debug)]
1278pub struct VfxTrack {
1279    pub base:     TrackBase,
1280    pub keyframes: Vec<VfxKeyframe>,
1281}
1282
1283impl VfxTrack {
1284    pub fn new(id: u64, name: &str) -> Self {
1285        VfxTrack {
1286            base: TrackBase::new(id, name, TrackKind::Vfx),
1287            keyframes: Vec::new(),
1288        }
1289    }
1290
1291    pub fn add_keyframe(&mut self, kf: VfxKeyframe) {
1292        let idx = self.keyframes.partition_point(|k| k.time < kf.time);
1293        self.keyframes.insert(idx, kf);
1294    }
1295
1296    pub fn active_at(&self, time: f64) -> Vec<&VfxKeyframe> {
1297        self.keyframes.iter().filter(|kf| {
1298            time >= kf.time + kf.delay && time <= kf.time + kf.delay + kf.duration
1299        }).collect()
1300    }
1301}
1302
1303// ============================================================
1304// LIGHT TRACK
1305// ============================================================
1306
1307#[derive(Clone, Debug, PartialEq)]
1308pub enum LightType {
1309    Point,
1310    Spot,
1311    Directional,
1312    Area,
1313}
1314
1315#[derive(Clone, Debug)]
1316pub struct LightKeyframe {
1317    pub time:         f64,
1318    pub color:        Vec4,
1319    pub intensity:    f32,
1320    pub range:        f32,
1321    pub spot_angle:   f32,   // degrees, for spot lights
1322    pub shadow_strength: f32,
1323    pub temperature:  f32,   // Kelvin, for color temperature
1324    pub interp:       InterpType,
1325}
1326
1327impl LightKeyframe {
1328    pub fn new(time: f64, color: Vec4, intensity: f32) -> Self {
1329        LightKeyframe {
1330            time, color, intensity,
1331            range: 10.0,
1332            spot_angle: 30.0,
1333            shadow_strength: 1.0,
1334            temperature: 6500.0,
1335            interp: InterpType::Linear,
1336        }
1337    }
1338
1339    /// Convert color temperature to RGB using empirical formula
1340    pub fn temperature_to_rgb(kelvin: f32) -> Vec3 {
1341        let t = kelvin / 100.0;
1342        let r = if t <= 66.0 {
1343            1.0
1344        } else {
1345            let r = 329.698727446 * (t - 60.0).powf(-0.1332047592);
1346            (r / 255.0).clamp(0.0, 1.0)
1347        };
1348        let g = if t <= 66.0 {
1349            let g = 99.4708025861 * t.ln() - 161.1195681661;
1350            (g / 255.0).clamp(0.0, 1.0)
1351        } else {
1352            let g = 288.1221695283 * (t - 60.0).powf(-0.0755148492);
1353            (g / 255.0).clamp(0.0, 1.0)
1354        };
1355        let b = if t >= 66.0 {
1356            1.0
1357        } else if t <= 19.0 {
1358            0.0
1359        } else {
1360            let b = 138.5177312231 * (t - 10.0).ln() - 305.0447927307;
1361            (b / 255.0).clamp(0.0, 1.0)
1362        };
1363        Vec3::new(r, g, b)
1364    }
1365}
1366
1367#[derive(Clone, Debug)]
1368pub struct LightTrack {
1369    pub base:        TrackBase,
1370    pub entity_id:   u64,
1371    pub light_type:  LightType,
1372    pub keyframes:   Vec<LightKeyframe>,
1373    pub flicker_enabled: bool,
1374    pub flicker_frequency: f32,
1375    pub flicker_amplitude: f32,
1376}
1377
1378impl LightTrack {
1379    pub fn new(id: u64, name: &str, entity_id: u64) -> Self {
1380        LightTrack {
1381            base: TrackBase::new(id, name, TrackKind::Light),
1382            entity_id,
1383            light_type: LightType::Point,
1384            keyframes: Vec::new(),
1385            flicker_enabled: false,
1386            flicker_frequency: 8.0,
1387            flicker_amplitude: 0.1,
1388        }
1389    }
1390
1391    pub fn add_keyframe(&mut self, kf: LightKeyframe) {
1392        let idx = self.keyframes.partition_point(|k| k.time < kf.time);
1393        self.keyframes.insert(idx, kf);
1394    }
1395
1396    pub fn evaluate(&self, time: f64) -> (Vec4, f32, f32) {
1397        let n = self.keyframes.len();
1398        if n == 0 { return (Vec4::ONE, 1.0, 10.0); }
1399        if n == 1 { let k = &self.keyframes[0]; return (k.color, k.intensity, k.range); }
1400        let idx = self.keyframes.partition_point(|k| k.time <= time);
1401        if idx == 0 { let k = &self.keyframes[0]; return (k.color, k.intensity, k.range); }
1402        if idx >= n { let k = &self.keyframes[n-1]; return (k.color, k.intensity, k.range); }
1403        let k0 = &self.keyframes[idx-1];
1404        let k1 = &self.keyframes[idx];
1405        let t = ((time - k0.time) / (k1.time - k0.time).max(1e-9)) as f32;
1406        let t_s = match k0.interp {
1407            InterpType::Constant | InterpType::Stepped => return (k0.color, k0.intensity, k0.range),
1408            InterpType::Linear => t,
1409            _ => smoother_step(t),
1410        };
1411        (
1412            lerp_vec4(k0.color, k1.color, t_s),
1413            lerp(k0.intensity, k1.intensity, t_s),
1414            lerp(k0.range, k1.range, t_s),
1415        )
1416    }
1417
1418    pub fn flicker_factor(&self, time: f64) -> f32 {
1419        if !self.flicker_enabled { return 1.0; }
1420        1.0 + value_noise_1d(time as f32 * self.flicker_frequency) * self.flicker_amplitude
1421    }
1422}
1423
1424// ============================================================
1425// POST FX TRACK
1426// ============================================================
1427
1428#[derive(Clone, Debug)]
1429pub struct PostFxKeyframe {
1430    pub time:            f64,
1431    pub exposure:        f32,
1432    pub contrast:        f32,
1433    pub saturation:      f32,
1434    pub bloom_intensity: f32,
1435    pub bloom_threshold: f32,
1436    pub vignette:        f32,
1437    pub chromatic_ab:    f32,  // chromatic aberration
1438    pub film_grain:      f32,
1439    pub color_grade:     Vec4, // lift, gamma, gain packed
1440    pub tone_map_mode:   u32,  // 0=none, 1=aces, 2=filmic
1441    pub interp:          InterpType,
1442}
1443
1444impl PostFxKeyframe {
1445    pub fn default_at(time: f64) -> Self {
1446        PostFxKeyframe {
1447            time,
1448            exposure: 0.0,
1449            contrast: 1.0,
1450            saturation: 1.0,
1451            bloom_intensity: 0.5,
1452            bloom_threshold: 1.0,
1453            vignette: 0.0,
1454            chromatic_ab: 0.0,
1455            film_grain: 0.0,
1456            color_grade: Vec4::new(0.0, 1.0, 1.0, 1.0),
1457            tone_map_mode: 1,
1458            interp: InterpType::Linear,
1459        }
1460    }
1461}
1462
1463#[derive(Clone, Debug)]
1464pub struct PostFxTrack {
1465    pub base:      TrackBase,
1466    pub keyframes: Vec<PostFxKeyframe>,
1467}
1468
1469impl PostFxTrack {
1470    pub fn new(id: u64, name: &str) -> Self {
1471        PostFxTrack {
1472            base: TrackBase::new(id, name, TrackKind::PostFx),
1473            keyframes: Vec::new(),
1474        }
1475    }
1476
1477    pub fn add_keyframe(&mut self, kf: PostFxKeyframe) {
1478        let idx = self.keyframes.partition_point(|k| k.time < kf.time);
1479        self.keyframes.insert(idx, kf);
1480    }
1481
1482    pub fn evaluate(&self, time: f64) -> PostFxKeyframe {
1483        let n = self.keyframes.len();
1484        if n == 0 { return PostFxKeyframe::default_at(time); }
1485        if n == 1 { return self.keyframes[0].clone(); }
1486        let idx = self.keyframes.partition_point(|k| k.time <= time);
1487        if idx == 0 { return self.keyframes[0].clone(); }
1488        if idx >= n { return self.keyframes[n-1].clone(); }
1489        let k0 = &self.keyframes[idx-1];
1490        let k1 = &self.keyframes[idx];
1491        let t = ((time - k0.time) / (k1.time - k0.time).max(1e-9)) as f32;
1492        let ts = match k0.interp {
1493            InterpType::Constant | InterpType::Stepped => return k0.clone(),
1494            InterpType::Linear   => t,
1495            _ => smoother_step(t),
1496        };
1497        PostFxKeyframe {
1498            time,
1499            exposure:        lerp(k0.exposure,        k1.exposure,        ts),
1500            contrast:        lerp(k0.contrast,        k1.contrast,        ts),
1501            saturation:      lerp(k0.saturation,      k1.saturation,      ts),
1502            bloom_intensity: lerp(k0.bloom_intensity, k1.bloom_intensity, ts),
1503            bloom_threshold: lerp(k0.bloom_threshold, k1.bloom_threshold, ts),
1504            vignette:        lerp(k0.vignette,        k1.vignette,        ts),
1505            chromatic_ab:    lerp(k0.chromatic_ab,    k1.chromatic_ab,    ts),
1506            film_grain:      lerp(k0.film_grain,      k1.film_grain,      ts),
1507            color_grade:     lerp_vec4(k0.color_grade, k1.color_grade,    ts),
1508            tone_map_mode:   k0.tone_map_mode,
1509            interp:          k0.interp.clone(),
1510        }
1511    }
1512}
1513
1514// ============================================================
1515// SUBTITLE TRACK
1516// ============================================================
1517
1518#[derive(Clone, Debug)]
1519pub struct SubtitleKeyframe {
1520    pub time:        f64,
1521    pub end_time:    f64,
1522    pub text:        String,
1523    pub speaker:     String,
1524    pub position:    Vec2,  // normalized screen position
1525    pub font_size:   f32,
1526    pub color:       Vec4,
1527    pub bg_color:    Vec4,
1528    pub fade_in:     f64,
1529    pub fade_out:    f64,
1530    pub language:    String,
1531}
1532
1533impl SubtitleKeyframe {
1534    pub fn new(time: f64, end_time: f64, text: &str) -> Self {
1535        SubtitleKeyframe {
1536            time, end_time, text: text.to_string(),
1537            speaker: String::new(),
1538            position: Vec2::new(0.5, 0.85),
1539            font_size: 32.0,
1540            color:    Vec4::new(1.0, 1.0, 1.0, 1.0),
1541            bg_color: Vec4::new(0.0, 0.0, 0.0, 0.5),
1542            fade_in:  0.1,
1543            fade_out: 0.1,
1544            language: "en".to_string(),
1545        }
1546    }
1547
1548    pub fn alpha_at(&self, time: f64) -> f32 {
1549        let fade_in_v  = if self.fade_in  > 1e-9 { ((time - self.time)     / self.fade_in).clamp(0.0, 1.0) as f32 } else { 1.0 };
1550        let fade_out_v = if self.fade_out > 1e-9 { ((self.end_time - time) / self.fade_out).clamp(0.0, 1.0) as f32 } else { 1.0 };
1551        fade_in_v.min(fade_out_v)
1552    }
1553}
1554
1555#[derive(Clone, Debug, Default)]
1556pub struct SubtitleStyle {
1557    pub font_size: f32,
1558    pub color:     Vec4,
1559    pub bold:      bool,
1560    pub italic:    bool,
1561}
1562
1563#[derive(Clone, Debug)]
1564pub struct SubtitleEntry {
1565    pub id:         u64,
1566    pub start_time: f64,
1567    pub end_time:   f64,
1568    pub text:       String,
1569    pub speaker:    String,
1570    pub style:      SubtitleStyle,
1571}
1572
1573#[derive(Debug)]
1574pub struct SubtitleTrack {
1575    pub base:       TrackBase,
1576    pub subtitles:  Vec<SubtitleKeyframe>,
1577    pub entries:    Vec<SubtitleEntry>,
1578    pub language:   String,
1579    pub export_srt: bool,
1580}
1581
1582impl SubtitleTrack {
1583    pub fn new(id: u64, name: &str) -> Self {
1584        SubtitleTrack {
1585            base: TrackBase::new(id, name, TrackKind::Subtitle),
1586            subtitles: Vec::new(),
1587            entries: Vec::new(),
1588            language: "en".to_string(),
1589            export_srt: true,
1590        }
1591    }
1592
1593    pub fn add_subtitle(&mut self, kf: SubtitleKeyframe) {
1594        let idx = self.subtitles.partition_point(|k| k.time < kf.time);
1595        self.subtitles.insert(idx, kf);
1596    }
1597
1598    pub fn active_at(&self, time: f64) -> Vec<&SubtitleKeyframe> {
1599        self.subtitles.iter()
1600            .filter(|s| time >= s.time && time <= s.end_time)
1601            .collect()
1602    }
1603
1604    /// Export to SRT format
1605    pub fn to_srt(&self, fps: f32) -> String {
1606        let mut out = String::new();
1607        for (i, sub) in self.subtitles.iter().enumerate() {
1608            let tc_start = Timecode::from_seconds(sub.time, fps);
1609            let tc_end   = Timecode::from_seconds(sub.end_time, fps);
1610            // SRT uses , for milliseconds
1611            out.push_str(&format!("{}\n", i + 1));
1612            out.push_str(&format!("{},{:03} --> {},{:03}\n",
1613                tc_start.to_string(), (sub.time.fract() * 1000.0) as u32,
1614                tc_end.to_string(),   (sub.end_time.fract() * 1000.0) as u32,
1615            ));
1616            out.push_str(&sub.text);
1617            out.push_str("\n\n");
1618        }
1619        out
1620    }
1621}
1622
1623// ============================================================
1624// EVENT TRACK
1625// ============================================================
1626
1627#[derive(Clone, Debug)]
1628pub struct EventKeyframe {
1629    pub time:       f64,
1630    pub event_name: String,
1631    pub parameters: HashMap<String, f32>,
1632    pub string_params: HashMap<String, String>,
1633    pub triggered:  bool,
1634    pub trigger_once: bool,
1635}
1636
1637impl EventKeyframe {
1638    pub fn new(time: f64, event_name: &str) -> Self {
1639        EventKeyframe {
1640            time,
1641            event_name: event_name.to_string(),
1642            parameters: HashMap::new(),
1643            string_params: HashMap::new(),
1644            triggered: false,
1645            trigger_once: true,
1646        }
1647    }
1648
1649    pub fn with_param(mut self, key: &str, val: f32) -> Self {
1650        self.parameters.insert(key.to_string(), val);
1651        self
1652    }
1653
1654    pub fn with_string(mut self, key: &str, val: &str) -> Self {
1655        self.string_params.insert(key.to_string(), val.to_string());
1656        self
1657    }
1658}
1659
1660#[derive(Clone, Debug)]
1661pub struct EventTrack {
1662    pub base:   TrackBase,
1663    pub events: Vec<EventKeyframe>,
1664}
1665
1666impl EventTrack {
1667    pub fn new(id: u64, name: &str) -> Self {
1668        EventTrack {
1669            base: TrackBase::new(id, name, TrackKind::Event),
1670            events: Vec::new(),
1671        }
1672    }
1673
1674    pub fn add_event(&mut self, ev: EventKeyframe) {
1675        let idx = self.events.partition_point(|e| e.time < ev.time);
1676        self.events.insert(idx, ev);
1677    }
1678
1679    pub fn poll(&mut self, prev_time: f64, cur_time: f64) -> Vec<EventKeyframe> {
1680        let mut fired = Vec::new();
1681        for ev in &mut self.events {
1682            if ev.time > prev_time && ev.time <= cur_time {
1683                if ev.trigger_once && ev.triggered { continue; }
1684                ev.triggered = true;
1685                fired.push(ev.clone());
1686            }
1687        }
1688        fired
1689    }
1690
1691    pub fn reset_triggers(&mut self) {
1692        for ev in &mut self.events {
1693            ev.triggered = false;
1694        }
1695    }
1696}
1697
1698// ============================================================
1699// TRANSFORM TRACK
1700// ============================================================
1701
1702#[derive(Clone, Debug)]
1703pub struct TransformKeyframe {
1704    pub time:     f64,
1705    pub position: Vec3,
1706    pub rotation: Quat,
1707    pub scale:    Vec3,
1708    pub interp:   InterpType,
1709}
1710
1711impl TransformKeyframe {
1712    pub fn new(time: f64) -> Self {
1713        TransformKeyframe {
1714            time,
1715            position: Vec3::ZERO,
1716            rotation: Quat::IDENTITY,
1717            scale:    Vec3::ONE,
1718            interp:   InterpType::Linear,
1719        }
1720    }
1721}
1722
1723#[derive(Clone, Debug)]
1724pub struct TransformTrack {
1725    pub base:      TrackBase,
1726    pub entity_id: u64,
1727    pub keyframes: Vec<TransformKeyframe>,
1728    pub additive:  bool,
1729    pub pos_x_curve: FloatCurve,
1730    pub pos_y_curve: FloatCurve,
1731    pub pos_z_curve: FloatCurve,
1732}
1733
1734impl TransformTrack {
1735    pub fn new(id: u64, name: &str, entity_id: u64) -> Self {
1736        TransformTrack {
1737            base: TrackBase::new(id, name, TrackKind::Transform),
1738            entity_id,
1739            keyframes: Vec::new(),
1740            additive: false,
1741            pos_x_curve: FloatCurve::new("PosX"),
1742            pos_y_curve: FloatCurve::new("PosY"),
1743            pos_z_curve: FloatCurve::new("PosZ"),
1744        }
1745    }
1746
1747    pub fn add_keyframe(&mut self, kf: TransformKeyframe) {
1748        let idx = self.keyframes.partition_point(|k| k.time < kf.time);
1749        self.keyframes.insert(idx, kf);
1750    }
1751
1752    pub fn evaluate(&self, time: f64) -> (Vec3, Quat, Vec3) {
1753        // Use per-component curves if populated
1754        if !self.pos_x_curve.keys.is_empty() {
1755            let px = self.pos_x_curve.evaluate(time);
1756            let py = self.pos_y_curve.evaluate(time);
1757            let pz = self.pos_z_curve.evaluate(time);
1758            return (Vec3::new(px, py, pz), Quat::IDENTITY, Vec3::ONE);
1759        }
1760
1761        let n = self.keyframes.len();
1762        if n == 0 { return (Vec3::ZERO, Quat::IDENTITY, Vec3::ONE); }
1763        if n == 1 { let k = &self.keyframes[0]; return (k.position, k.rotation, k.scale); }
1764        let idx = self.keyframes.partition_point(|k| k.time <= time);
1765        if idx == 0 { let k = &self.keyframes[0]; return (k.position, k.rotation, k.scale); }
1766        if idx >= n { let k = &self.keyframes[n-1]; return (k.position, k.rotation, k.scale); }
1767        let k0 = &self.keyframes[idx-1];
1768        let k1 = &self.keyframes[idx];
1769        let t = ((time - k0.time) / (k1.time - k0.time).max(1e-9)) as f32;
1770        let ts = match k0.interp {
1771            InterpType::Constant | InterpType::Stepped => return (k0.position, k0.rotation, k0.scale),
1772            InterpType::Linear   => t,
1773            InterpType::Cubic    => {
1774                let p0 = if idx >= 2 { self.keyframes[idx-2].position } else { k0.position };
1775                let p3 = if idx+1 < n { self.keyframes[idx+1].position } else { k1.position };
1776                return (
1777                    catmull_rom_vec3(p0, k0.position, k1.position, p3, t),
1778                    k0.rotation.slerp(k1.rotation, t),
1779                    lerp_vec3(k0.scale, k1.scale, t),
1780                );
1781            }
1782            InterpType::Bezier => smoother_step(t),
1783        };
1784        (
1785            lerp_vec3(k0.position, k1.position, ts),
1786            k0.rotation.slerp(k1.rotation, ts),
1787            lerp_vec3(k0.scale, k1.scale, ts),
1788        )
1789    }
1790}
1791
1792// ============================================================
1793// BLEND SHAPE TRACK
1794// ============================================================
1795
1796#[derive(Clone, Debug)]
1797pub struct BlendShapeKeyframe {
1798    pub time:    f64,
1799    pub weights: HashMap<String, f32>,
1800    pub interp:  InterpType,
1801}
1802
1803impl BlendShapeKeyframe {
1804    pub fn new(time: f64) -> Self {
1805        BlendShapeKeyframe { time, weights: HashMap::new(), interp: InterpType::Linear }
1806    }
1807
1808    pub fn set_weight(mut self, name: &str, weight: f32) -> Self {
1809        self.weights.insert(name.to_string(), weight.clamp(0.0, 1.0));
1810        self
1811    }
1812}
1813
1814#[derive(Clone, Debug)]
1815pub struct BlendShapeTrack {
1816    pub base:      TrackBase,
1817    pub entity_id: u64,
1818    pub keyframes: Vec<BlendShapeKeyframe>,
1819    pub channels:  Vec<String>,
1820}
1821
1822impl BlendShapeTrack {
1823    pub fn new(id: u64, name: &str, entity_id: u64) -> Self {
1824        BlendShapeTrack {
1825            base: TrackBase::new(id, name, TrackKind::BlendShape),
1826            entity_id,
1827            keyframes: Vec::new(),
1828            channels: Vec::new(),
1829        }
1830    }
1831
1832    pub fn add_channel(&mut self, name: &str) {
1833        if !self.channels.contains(&name.to_string()) {
1834            self.channels.push(name.to_string());
1835        }
1836    }
1837
1838    pub fn add_keyframe(&mut self, kf: BlendShapeKeyframe) {
1839        let idx = self.keyframes.partition_point(|k| k.time < kf.time);
1840        self.keyframes.insert(idx, kf);
1841    }
1842
1843    pub fn evaluate(&self, time: f64) -> HashMap<String, f32> {
1844        let n = self.keyframes.len();
1845        if n == 0 {
1846            return self.channels.iter().map(|c| (c.clone(), 0.0)).collect();
1847        }
1848        if n == 1 { return self.keyframes[0].weights.clone(); }
1849        let idx = self.keyframes.partition_point(|k| k.time <= time);
1850        if idx == 0 { return self.keyframes[0].weights.clone(); }
1851        if idx >= n { return self.keyframes[n-1].weights.clone(); }
1852        let k0 = &self.keyframes[idx-1];
1853        let k1 = &self.keyframes[idx];
1854        let t = ((time - k0.time) / (k1.time - k0.time).max(1e-9)) as f32;
1855        let ts = match k0.interp {
1856            InterpType::Constant | InterpType::Stepped => return k0.weights.clone(),
1857            InterpType::Linear => t,
1858            _ => smoother_step(t),
1859        };
1860        let mut result = HashMap::new();
1861        for ch in &self.channels {
1862            let w0 = k0.weights.get(ch).cloned().unwrap_or(0.0);
1863            let w1 = k1.weights.get(ch).cloned().unwrap_or(0.0);
1864            result.insert(ch.clone(), lerp(w0, w1, ts));
1865        }
1866        result
1867    }
1868}
1869
1870// ============================================================
1871// VISIBILITY TRACK
1872// ============================================================
1873
1874#[derive(Clone, Debug)]
1875pub struct VisibilityKeyframe {
1876    pub time:    f64,
1877    pub visible: bool,
1878    pub opacity: f32,
1879    pub fade:    f64,   // fade duration
1880}
1881
1882impl VisibilityKeyframe {
1883    pub fn new(time: f64, visible: bool) -> Self {
1884        VisibilityKeyframe { time, visible, opacity: if visible { 1.0 } else { 0.0 }, fade: 0.0 }
1885    }
1886}
1887
1888#[derive(Clone, Debug)]
1889pub struct VisibilityTrack {
1890    pub base:      TrackBase,
1891    pub entity_id: u64,
1892    pub keyframes: Vec<VisibilityKeyframe>,
1893}
1894
1895impl VisibilityTrack {
1896    pub fn new(id: u64, name: &str, entity_id: u64) -> Self {
1897        VisibilityTrack {
1898            base: TrackBase::new(id, name, TrackKind::Visibility),
1899            entity_id,
1900            keyframes: Vec::new(),
1901        }
1902    }
1903
1904    pub fn add_keyframe(&mut self, kf: VisibilityKeyframe) {
1905        let idx = self.keyframes.partition_point(|k| k.time < kf.time);
1906        self.keyframes.insert(idx, kf);
1907    }
1908
1909    pub fn evaluate_opacity(&self, time: f64) -> f32 {
1910        let n = self.keyframes.len();
1911        if n == 0 { return 1.0; }
1912        let idx = self.keyframes.partition_point(|k| k.time <= time);
1913        if idx == 0 { return self.keyframes[0].opacity; }
1914        if idx >= n { return self.keyframes[n-1].opacity; }
1915        let k0 = &self.keyframes[idx-1];
1916        let k1 = &self.keyframes[idx];
1917        let fade = k0.fade.max(1e-9);
1918        let t = ((time - k0.time) / fade).clamp(0.0, 1.0) as f32;
1919        lerp(k0.opacity, k1.opacity, smooth_step(t))
1920    }
1921
1922    pub fn is_visible_at(&self, time: f64) -> bool {
1923        self.evaluate_opacity(time) > 0.001
1924    }
1925}
1926
1927// ============================================================
1928// TIME DILATION TRACK
1929// ============================================================
1930
1931#[derive(Clone, Debug)]
1932pub struct TimeDilationKeyframe {
1933    pub time:          f64,
1934    pub time_scale:    f32,  // 1.0 = normal, 0.5 = half speed, 0.0 = freeze
1935    pub ease_duration: f64,
1936    pub interp:        InterpType,
1937}
1938
1939impl TimeDilationKeyframe {
1940    pub fn new(time: f64, scale: f32) -> Self {
1941        TimeDilationKeyframe { time, time_scale: scale, ease_duration: 0.5, interp: InterpType::Cubic }
1942    }
1943}
1944
1945#[derive(Clone, Debug)]
1946pub struct TimeDilationTrack {
1947    pub base:      TrackBase,
1948    pub keyframes: Vec<TimeDilationKeyframe>,
1949    pub global:    bool, // affects entire world vs just current sequence
1950}
1951
1952impl TimeDilationTrack {
1953    pub fn new(id: u64, name: &str) -> Self {
1954        TimeDilationTrack {
1955            base: TrackBase::new(id, name, TrackKind::TimeDilation),
1956            keyframes: Vec::new(),
1957            global: false,
1958        }
1959    }
1960
1961    pub fn add_keyframe(&mut self, kf: TimeDilationKeyframe) {
1962        let idx = self.keyframes.partition_point(|k| k.time < kf.time);
1963        self.keyframes.insert(idx, kf);
1964    }
1965
1966    pub fn evaluate(&self, time: f64) -> f32 {
1967        let n = self.keyframes.len();
1968        if n == 0 { return 1.0; }
1969        if n == 1 { return self.keyframes[0].time_scale; }
1970        let idx = self.keyframes.partition_point(|k| k.time <= time);
1971        if idx == 0 { return self.keyframes[0].time_scale; }
1972        if idx >= n { return self.keyframes[n-1].time_scale; }
1973        let k0 = &self.keyframes[idx-1];
1974        let k1 = &self.keyframes[idx];
1975        let dt = (k1.time - k0.time) as f32;
1976        let t  = ((time - k0.time) as f32) / dt.max(EPSILON);
1977        match k0.interp {
1978            InterpType::Constant | InterpType::Stepped => k0.time_scale,
1979            InterpType::Linear   => lerp(k0.time_scale, k1.time_scale, t),
1980            InterpType::Cubic | InterpType::Bezier => lerp(k0.time_scale, k1.time_scale, smoother_step(t)),
1981        }
1982    }
1983
1984    /// Integrate dilation to compute actual world time at a given sequence time
1985    pub fn world_time_at(&self, sequence_time: f64, dt: f64) -> f64 {
1986        let steps = (sequence_time / dt).ceil() as usize;
1987        let mut world_t = 0.0_f64;
1988        for i in 0..steps {
1989            let t = i as f64 * dt;
1990            let scale = self.evaluate(t) as f64;
1991            world_t += dt * scale;
1992        }
1993        world_t
1994    }
1995}
1996
1997// ============================================================
1998// SHOT LIST / TAKE SYSTEM
1999// ============================================================
2000
2001#[derive(Clone, Debug, PartialEq)]
2002pub enum CutType { Cut, Dissolve, Fade, Wipe }
2003
2004#[derive(Clone, Debug)]
2005pub struct Shot {
2006    pub id:                  u64,
2007    pub name:                String,
2008    pub start_time:          f64,
2009    pub end_time:            f64,
2010    pub camera_id:           u64,
2011    pub scene_name:          String,
2012    pub take_number:         u32,
2013    pub is_selected:         bool,
2014    pub notes:               String,
2015    pub rating:              u8,
2016    pub color_flag:          Vec4,
2017    pub transition:          CutType,
2018    pub transition_duration: f64,
2019}
2020
2021impl Shot {
2022    pub fn new(id: u64, name: &str, start: f64, end: f64, camera_id: u64) -> Self {
2023        Shot {
2024            id, name: name.to_string(),
2025            start_time: start, end_time: end,
2026            camera_id,
2027            scene_name: String::new(),
2028            take_number: 1,
2029            is_selected: false,
2030            notes: String::new(),
2031            rating: 3,
2032            color_flag: Vec4::new(1.0, 1.0, 1.0, 1.0),
2033            transition: CutType::Cut,
2034            transition_duration: 0.0,
2035        }
2036    }
2037
2038    pub fn duration(&self) -> f64 { self.end_time - self.start_time }
2039}
2040
2041#[derive(Clone, Debug)]
2042pub struct Take {
2043    pub take_number:  u32,
2044    pub timestamp:    u64,
2045    pub notes:        String,
2046    pub is_best_take: bool,
2047}
2048
2049#[derive(Clone, Debug)]
2050pub struct ShotList {
2051    pub shots:   Vec<Shot>,
2052    pub takes:   HashMap<u64, Vec<Take>>,   // shot_id -> takes
2053    pub current_shot: Option<u64>,
2054}
2055
2056impl ShotList {
2057    pub fn new() -> Self {
2058        ShotList { shots: Vec::new(), takes: HashMap::new(), current_shot: None }
2059    }
2060
2061    pub fn add_shot(&mut self, shot: Shot) {
2062        let id = shot.id;
2063        self.shots.push(shot);
2064        self.takes.insert(id, vec![Take {
2065            take_number: 1, timestamp: 0, notes: String::new(), is_best_take: false,
2066        }]);
2067    }
2068
2069    pub fn shot_at_time(&self, time: f64) -> Option<&Shot> {
2070        self.shots.iter().find(|s| time >= s.start_time && time < s.end_time)
2071    }
2072
2073    pub fn add_take(&mut self, shot_id: u64, notes: &str) -> u32 {
2074        let takes = self.takes.entry(shot_id).or_default();
2075        let num = takes.len() as u32 + 1;
2076        takes.push(Take { take_number: num, timestamp: 0, notes: notes.to_string(), is_best_take: false });
2077        num
2078    }
2079
2080    pub fn sort_by_time(&mut self) {
2081        self.shots.sort_by(|a, b| a.start_time.partial_cmp(&b.start_time).unwrap_or(std::cmp::Ordering::Equal));
2082    }
2083}
2084
2085// ============================================================
2086// CINEMATIC EVENTS
2087// ============================================================
2088
2089#[derive(Clone, Debug)]
2090pub struct ScreenFlashEvent {
2091    pub time:     f64,
2092    pub color:    Vec4,
2093    pub duration: f64,
2094    pub intensity: f32,
2095}
2096
2097#[derive(Clone, Debug)]
2098pub struct RumbleEvent {
2099    pub time:      f64,
2100    pub duration:  f64,
2101    pub intensity: f32,
2102    pub frequency: f32,
2103    pub decay:     f32,
2104}
2105
2106impl RumbleEvent {
2107    pub fn intensity_at(&self, time: f64) -> f32 {
2108        let local = time - self.time;
2109        if local < 0.0 || local > self.duration { return 0.0; }
2110        let envelope = (-self.decay * local as f32).exp();
2111        let osc = (local as f32 * self.frequency * std::f32::consts::TAU).sin();
2112        self.intensity * envelope * osc.abs()
2113    }
2114}
2115
2116#[derive(Clone, Debug)]
2117pub struct SlowMotionEvent {
2118    pub time:        f64,
2119    pub duration:    f64,
2120    pub time_scale:  f32,
2121    pub ease_in:     f64,
2122    pub ease_out:    f64,
2123}
2124
2125impl SlowMotionEvent {
2126    pub fn scale_at(&self, time: f64) -> f32 {
2127        let local = time - self.time;
2128        if local < 0.0 || local > self.duration { return 1.0; }
2129        let in_phase  = (local / self.ease_in.max(1e-9)).clamp(0.0, 1.0) as f32;
2130        let out_start = self.duration - self.ease_out;
2131        let out_phase = ((local - out_start) / self.ease_out.max(1e-9)).clamp(0.0, 1.0) as f32;
2132        let scale = if local < self.ease_in {
2133            lerp(1.0, self.time_scale, smooth_step(in_phase))
2134        } else if local > out_start {
2135            lerp(self.time_scale, 1.0, smooth_step(out_phase))
2136        } else {
2137            self.time_scale
2138        };
2139        scale
2140    }
2141}
2142
2143#[derive(Clone, Debug)]
2144pub struct LetterboxEvent {
2145    pub time:     f64,
2146    pub duration: f64,
2147    pub aspect:   f32,     // target aspect ratio
2148    pub ease_in:  f64,
2149    pub ease_out: f64,
2150}
2151
2152impl LetterboxEvent {
2153    pub fn bar_height_at(&self, screen_h: f32, screen_w: f32, time: f64) -> f32 {
2154        let local = time - self.time;
2155        if local < 0.0 || local > self.duration { return 0.0; }
2156        let in_phase = (local / self.ease_in.max(1e-9)).clamp(0.0, 1.0) as f32;
2157        let out_start = self.duration - self.ease_out;
2158        let out_phase = ((local - out_start) / self.ease_out.max(1e-9)).clamp(0.0, 1.0) as f32;
2159        let blend = if local < self.ease_in { smooth_step(in_phase) }
2160                    else if local > out_start { 1.0 - smooth_step(out_phase) }
2161                    else { 1.0 };
2162        let current_aspect = screen_w / screen_h.max(1.0);
2163        if current_aspect <= self.aspect { return 0.0; }
2164        let target_h = screen_w / self.aspect;
2165        let bar = (screen_h - target_h) * 0.5 * blend;
2166        bar.max(0.0)
2167    }
2168}
2169
2170#[derive(Clone, Debug)]
2171pub struct ChapterMarker {
2172    pub time:  f64,
2173    pub name:  String,
2174    pub thumb: Option<u64>, // thumbnail image id
2175}
2176
2177#[derive(Clone, Debug)]
2178pub struct BranchingTrigger {
2179    pub time:        f64,
2180    pub condition:   String,  // expression or flag name
2181    pub target_time: f64,     // jump to this time if condition true
2182    pub target_sequence: Option<u64>,
2183    pub auto_trigger: bool,
2184}
2185
2186// ============================================================
2187// BLEND / LAYER EVALUATION ENGINE
2188// ============================================================
2189
2190#[derive(Clone, Debug)]
2191pub struct LayerBlendState {
2192    pub layer: u32,
2193    pub weight: f32,
2194    pub blend_mode: BlendMode,
2195}
2196
2197impl LayerBlendState {
2198    pub fn blend_values(&self, base: f32, layer_val: f32) -> f32 {
2199        match self.blend_mode {
2200            BlendMode::Override  => lerp(base, layer_val, self.weight),
2201            BlendMode::Additive  => base + layer_val * self.weight,
2202            BlendMode::Multiply  => base * lerp(1.0, layer_val, self.weight),
2203            BlendMode::Screen    => 1.0 - (1.0 - base) * lerp(1.0, 1.0 - layer_val, self.weight),
2204            BlendMode::Lerp      => lerp(base, layer_val, self.weight),
2205        }
2206    }
2207
2208    pub fn blend_vec3(&self, base: Vec3, layer_val: Vec3) -> Vec3 {
2209        match self.blend_mode {
2210            BlendMode::Override | BlendMode::Lerp => lerp_vec3(base, layer_val, self.weight),
2211            BlendMode::Additive  => base + layer_val * self.weight,
2212            BlendMode::Multiply  => base * lerp_vec3(Vec3::ONE, layer_val, self.weight),
2213            BlendMode::Screen    => Vec3::ONE - (Vec3::ONE - base) * lerp_vec3(Vec3::ONE, Vec3::ONE - layer_val, self.weight),
2214        }
2215    }
2216}
2217
2218// ============================================================
2219// SEQUENCE (MASTER)
2220// ============================================================
2221
2222static SEQUENCER_ID_COUNTER: std::sync::atomic::AtomicU64 =
2223    std::sync::atomic::AtomicU64::new(1);
2224
2225fn next_id() -> u64 {
2226    SEQUENCER_ID_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
2227}
2228
2229#[derive(Clone, Debug)]
2230pub struct Sequence {
2231    pub id:       u64,
2232    pub name:     String,
2233    pub duration: f64,    // in seconds
2234    pub fps:      FrameRate,
2235    pub loop_seq: bool,
2236    pub work_area_start: f64,
2237    pub work_area_end:   f64,
2238    pub sub_sequences: Vec<SubSequence>,
2239}
2240
2241impl Sequence {
2242    pub fn new(name: &str, duration: f64, fps: FrameRate) -> Self {
2243        Sequence {
2244            id: next_id(),
2245            name: name.to_string(),
2246            duration,
2247            fps,
2248            loop_seq: false,
2249            work_area_start: 0.0,
2250            work_area_end: duration,
2251            sub_sequences: Vec::new(),
2252        }
2253    }
2254
2255    pub fn frame_count(&self) -> u64 {
2256        self.fps.seconds_to_frame(self.duration)
2257    }
2258
2259    pub fn time_at_frame(&self, frame: u64) -> f64 {
2260        self.fps.frame_to_seconds(frame)
2261    }
2262
2263    pub fn frame_at_time(&self, time: f64) -> u64 {
2264        self.fps.seconds_to_frame(time)
2265    }
2266}
2267
2268#[derive(Clone, Debug)]
2269pub struct SubSequence {
2270    pub id:            u64,
2271    pub sequence_id:   u64,   // references a Sequence
2272    pub start_time:    f64,
2273    pub time_scale:    f32,
2274    pub blend_in:      f64,
2275    pub blend_out:     f64,
2276    pub weight:        f32,
2277    pub loop_sub:      bool,
2278}
2279
2280impl SubSequence {
2281    pub fn local_time(&self, global_time: f64) -> f64 {
2282        let local = (global_time - self.start_time) * self.time_scale as f64;
2283        local.max(0.0)
2284    }
2285
2286    pub fn weight_at(&self, global_time: f64, seq_duration: f64) -> f32 {
2287        let local = global_time - self.start_time;
2288        let end   = self.start_time + seq_duration / self.time_scale as f64;
2289        let in_w  = (local / self.blend_in.max(1e-9)).clamp(0.0, 1.0) as f32;
2290        let out_w = ((end - global_time) / self.blend_out.max(1e-9)).clamp(0.0, 1.0) as f32;
2291        self.weight * in_w.min(out_w)
2292    }
2293}
2294
2295// ============================================================
2296// EXPORT: EDL (Edit Decision List)
2297// ============================================================
2298
2299#[derive(Clone, Debug)]
2300pub struct EdlEntry {
2301    pub event_number: u32,
2302    pub reel_name:    String,
2303    pub track_type:   String,  // V = video, A = audio, B = both
2304    pub transition:   EdlTransition,
2305    pub source_in:    Timecode,
2306    pub source_out:   Timecode,
2307    pub record_in:    Timecode,
2308    pub record_out:   Timecode,
2309    pub comment:      String,
2310}
2311
2312#[derive(Clone, Debug, PartialEq)]
2313pub enum EdlTransition {
2314    Cut,
2315    Dissolve(u32),      // frame count
2316    Wipe(u32, u32),     // wipe number, frame count
2317}
2318
2319impl EdlEntry {
2320    pub fn to_cmx3600(&self) -> String {
2321        let trans = match &self.transition {
2322            EdlTransition::Cut              => "C       ".to_string(),
2323            EdlTransition::Dissolve(frames) => format!("D       {:03} ", frames),
2324            EdlTransition::Wipe(n, frames)  => format!("W{:03}    {:03} ", n, frames),
2325        };
2326        format!(
2327            "{:03}  {:8} {} {} {} {} {} {}\n",
2328            self.event_number,
2329            self.reel_name,
2330            self.track_type,
2331            trans,
2332            self.source_in.to_string(),
2333            self.source_out.to_string(),
2334            self.record_in.to_string(),
2335            self.record_out.to_string(),
2336        )
2337    }
2338}
2339
2340#[derive(Clone, Debug)]
2341pub struct EdlDocument {
2342    pub title:   String,
2343    pub fps:     FrameRate,
2344    pub entries: Vec<EdlEntry>,
2345}
2346
2347impl EdlDocument {
2348    pub fn new(title: &str, fps: FrameRate) -> Self {
2349        EdlDocument { title: title.to_string(), fps, entries: Vec::new() }
2350    }
2351
2352    pub fn add_entry(&mut self, entry: EdlEntry) {
2353        self.entries.push(entry);
2354    }
2355
2356    pub fn to_string(&self) -> String {
2357        let mut out = format!("TITLE: {}\n", self.title);
2358        out.push_str(&format!("FCM: NON-DROP FRAME\n\n"));
2359        for entry in &self.entries {
2360            out.push_str(&entry.to_cmx3600());
2361        }
2362        out
2363    }
2364
2365    pub fn from_shot_list(shots: &ShotList, fps: FrameRate) -> Self {
2366        let fps_val = fps.fps();
2367        let mut doc = EdlDocument::new("Sequence", fps);
2368        for (i, shot) in shots.shots.iter().enumerate() {
2369            let src_in  = Timecode::from_seconds(0.0, fps_val);
2370            let src_out = Timecode::from_seconds(shot.duration(), fps_val);
2371            let rec_in  = Timecode::from_seconds(shot.start_time, fps_val);
2372            let rec_out = Timecode::from_seconds(shot.end_time, fps_val);
2373            doc.add_entry(EdlEntry {
2374                event_number: (i + 1) as u32,
2375                reel_name:    format!("CAM{:04}", shot.camera_id % 10000),
2376                track_type:   "V     A1".to_string(),
2377                transition:   EdlTransition::Cut,
2378                source_in:    src_in,
2379                source_out:   src_out,
2380                record_in:    rec_in,
2381                record_out:   rec_out,
2382                comment:      shot.name.clone(),
2383            });
2384        }
2385        doc
2386    }
2387}
2388
2389// ============================================================
2390// PLAYBACK STATE
2391// ============================================================
2392
2393#[derive(Clone, Debug, PartialEq)]
2394pub enum PlaybackState {
2395    Stopped,
2396    Playing,
2397    Paused,
2398    Scrubbing,
2399    Recording,
2400}
2401
2402#[derive(Clone, Debug)]
2403pub struct PlaybackController {
2404    pub state:           PlaybackState,
2405    pub current_time:    f64,
2406    pub playback_speed:  f32,
2407    pub loop_enabled:    bool,
2408    pub loop_start:      f64,
2409    pub loop_end:        f64,
2410    pub bookmarks:       Vec<(f64, String)>,
2411    pub snap_to_frames:  bool,
2412    pub fps:             FrameRate,
2413}
2414
2415impl PlaybackController {
2416    pub fn new(fps: FrameRate) -> Self {
2417        PlaybackController {
2418            state: PlaybackState::Stopped,
2419            current_time: 0.0,
2420            playback_speed: 1.0,
2421            loop_enabled: false,
2422            loop_start: 0.0,
2423            loop_end: 10.0,
2424            bookmarks: Vec::new(),
2425            snap_to_frames: true,
2426            fps,
2427        }
2428    }
2429
2430    pub fn play(&mut self) { self.state = PlaybackState::Playing; }
2431    pub fn pause(&mut self) {
2432        if self.state == PlaybackState::Playing {
2433            self.state = PlaybackState::Paused;
2434        }
2435    }
2436    pub fn stop(&mut self) {
2437        self.state = PlaybackState::Stopped;
2438        self.current_time = 0.0;
2439    }
2440    pub fn toggle_play_pause(&mut self) {
2441        match self.state {
2442            PlaybackState::Playing => self.pause(),
2443            _                      => self.play(),
2444        }
2445    }
2446
2447    pub fn update(&mut self, dt: f32, duration: f64) {
2448        if self.state != PlaybackState::Playing { return; }
2449        self.current_time += dt as f64 * self.playback_speed as f64;
2450        if self.loop_enabled && self.current_time >= self.loop_end {
2451            self.current_time = self.loop_start + (self.current_time - self.loop_end);
2452        } else if self.current_time >= duration {
2453            self.current_time = duration;
2454            self.state = PlaybackState::Paused;
2455        }
2456        if self.snap_to_frames {
2457            let frame = self.fps.seconds_to_frame(self.current_time);
2458            self.current_time = self.fps.frame_to_seconds(frame);
2459        }
2460    }
2461
2462    pub fn scrub_to(&mut self, time: f64) {
2463        self.state = PlaybackState::Scrubbing;
2464        self.current_time = time.max(0.0);
2465        if self.snap_to_frames {
2466            let frame = self.fps.seconds_to_frame(self.current_time);
2467            self.current_time = self.fps.frame_to_seconds(frame);
2468        }
2469    }
2470
2471    pub fn step_frames(&mut self, frames: i64) {
2472        let cur_frame = self.fps.seconds_to_frame(self.current_time) as i64;
2473        let new_frame = (cur_frame + frames).max(0) as u64;
2474        self.current_time = self.fps.frame_to_seconds(new_frame);
2475    }
2476
2477    pub fn add_bookmark(&mut self, name: &str) {
2478        self.bookmarks.push((self.current_time, name.to_string()));
2479        self.bookmarks.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
2480    }
2481
2482    pub fn goto_next_bookmark(&mut self) {
2483        if let Some(bm) = self.bookmarks.iter().find(|&&(t, _)| t > self.current_time) {
2484            self.current_time = bm.0;
2485        }
2486    }
2487
2488    pub fn goto_prev_bookmark(&mut self) {
2489        if let Some(bm) = self.bookmarks.iter().rev().find(|&&(t, _)| t < self.current_time) {
2490            self.current_time = bm.0;
2491        }
2492    }
2493
2494    pub fn current_timecode(&self) -> Timecode {
2495        Timecode::from_seconds(self.current_time, self.fps.fps())
2496    }
2497
2498    pub fn current_frame(&self) -> u64 {
2499        self.fps.seconds_to_frame(self.current_time)
2500    }
2501}
2502
2503// ============================================================
2504// UNDO/REDO SYSTEM FOR SEQUENCER
2505// ============================================================
2506
2507#[derive(Clone, Debug)]
2508pub enum SequencerCommand {
2509    AddKeyframe     { track_id: u64, track_kind: TrackKind, time: f64 },
2510    RemoveKeyframe  { track_id: u64, time: f64 },
2511    MoveKeyframe    { track_id: u64, old_time: f64, new_time: f64 },
2512    AddTrack        { track_id: u64, track_kind: TrackKind },
2513    RemoveTrack     { track_id: u64 },
2514    SetTrackEnabled { track_id: u64, old_val: bool, new_val: bool },
2515    PasteKeyframes  { track_id: u64, times: Vec<f64> },
2516    BakeAnimation   { entity_id: u64 },
2517    SetDuration     { old_duration: f64, new_duration: f64 },
2518    SetFps          { old_fps: FrameRate, new_fps: FrameRate },
2519    AddShot         { shot_id: u64 },
2520    RemoveShot      { shot_id: u64 },
2521    MoveShot        { shot_id: u64, old_start: f64, new_start: f64 },
2522}
2523
2524#[derive(Debug)]
2525pub struct SequencerUndoHistory {
2526    past:     VecDeque<SequencerCommand>,
2527    future:   VecDeque<SequencerCommand>,
2528    max_size: usize,
2529}
2530
2531impl SequencerUndoHistory {
2532    pub fn new() -> Self {
2533        SequencerUndoHistory {
2534            past:     VecDeque::new(),
2535            future:   VecDeque::new(),
2536            max_size: MAX_UNDO_DEPTH,
2537        }
2538    }
2539
2540    pub fn push(&mut self, cmd: SequencerCommand) {
2541        self.future.clear();
2542        self.past.push_back(cmd);
2543        if self.past.len() > self.max_size {
2544            self.past.pop_front();
2545        }
2546    }
2547
2548    pub fn undo(&mut self) -> Option<SequencerCommand> {
2549        let cmd = self.past.pop_back()?;
2550        self.future.push_back(cmd.clone());
2551        Some(cmd)
2552    }
2553
2554    pub fn redo(&mut self) -> Option<SequencerCommand> {
2555        let cmd = self.future.pop_back()?;
2556        self.past.push_back(cmd.clone());
2557        Some(cmd)
2558    }
2559
2560    pub fn can_undo(&self) -> bool { !self.past.is_empty() }
2561    pub fn can_redo(&self) -> bool { !self.future.is_empty() }
2562    pub fn clear(&mut self) { self.past.clear(); self.future.clear(); }
2563}
2564
2565// ============================================================
2566// SELECTION STATE
2567// ============================================================
2568
2569#[derive(Clone, Debug)]
2570pub struct SequencerSelection {
2571    pub selected_tracks:   HashSet<u64>,
2572    pub selected_keyframes: HashMap<u64, Vec<f64>>, // track_id -> selected times
2573    pub clipboard_keyframes: HashMap<u64, Vec<f64>>,
2574    pub clipboard_offset:  f64,
2575}
2576
2577impl SequencerSelection {
2578    pub fn new() -> Self {
2579        SequencerSelection {
2580            selected_tracks: HashSet::new(),
2581            selected_keyframes: HashMap::new(),
2582            clipboard_keyframes: HashMap::new(),
2583            clipboard_offset: 0.0,
2584        }
2585    }
2586
2587    pub fn select_track(&mut self, id: u64, multi: bool) {
2588        if !multi { self.selected_tracks.clear(); }
2589        self.selected_tracks.insert(id);
2590    }
2591
2592    pub fn select_keyframe(&mut self, track_id: u64, time: f64, multi: bool) {
2593        if !multi {
2594            self.selected_keyframes.clear();
2595        }
2596        self.selected_keyframes.entry(track_id).or_default().push(time);
2597    }
2598
2599    pub fn select_range(&mut self, track_id: u64, t_start: f64, t_end: f64, times: &[f64]) {
2600        let in_range: Vec<f64> = times.iter()
2601            .cloned()
2602            .filter(|&t| t >= t_start && t <= t_end)
2603            .collect();
2604        self.selected_keyframes.entry(track_id).or_default().extend(in_range);
2605    }
2606
2607    pub fn copy_keyframes(&mut self, current_time: f64) {
2608        self.clipboard_keyframes = self.selected_keyframes.clone();
2609        self.clipboard_offset = current_time;
2610    }
2611
2612    pub fn clear(&mut self) {
2613        self.selected_tracks.clear();
2614        self.selected_keyframes.clear();
2615    }
2616
2617    pub fn is_track_selected(&self, id: u64) -> bool {
2618        self.selected_tracks.contains(&id)
2619    }
2620
2621    pub fn is_keyframe_selected(&self, track_id: u64, time: f64) -> bool {
2622        self.selected_keyframes.get(&track_id)
2623            .map(|times| times.iter().any(|&t| (t - time).abs() < 1e-6))
2624            .unwrap_or(false)
2625    }
2626}
2627
2628// ============================================================
2629// CURVE EDITOR STATE
2630// ============================================================
2631
2632#[derive(Clone, Debug)]
2633pub struct CurveEditorState {
2634    pub visible_tracks: HashSet<u64>,
2635    pub view_min_t: f64,
2636    pub view_max_t: f64,
2637    pub view_min_v: f32,
2638    pub view_max_v: f32,
2639    pub show_tangents: bool,
2640    pub tangent_scale:  f32,
2641    pub snap_value:     f32,   // value snap grid
2642    pub snap_time:      f64,   // time snap grid
2643    pub auto_fit:       bool,
2644}
2645
2646impl CurveEditorState {
2647    pub fn new() -> Self {
2648        CurveEditorState {
2649            visible_tracks: HashSet::new(),
2650            view_min_t:  0.0,
2651            view_max_t: 10.0,
2652            view_min_v: -1.0,
2653            view_max_v:  1.0,
2654            show_tangents: true,
2655            tangent_scale: 1.0,
2656            snap_value: 0.0,
2657            snap_time:  0.0,
2658            auto_fit:   true,
2659        }
2660    }
2661
2662    pub fn time_to_screen_x(&self, time: f64, screen_w: f32) -> f32 {
2663        let frac = (time - self.view_min_t) / (self.view_max_t - self.view_min_t).max(1e-9);
2664        frac as f32 * screen_w
2665    }
2666
2667    pub fn value_to_screen_y(&self, value: f32, screen_h: f32) -> f32 {
2668        let frac = (value - self.view_min_v) / (self.view_max_v - self.view_min_v).max(EPSILON);
2669        (1.0 - frac) * screen_h
2670    }
2671
2672    pub fn screen_x_to_time(&self, x: f32, screen_w: f32) -> f64 {
2673        let frac = x / screen_w.max(1.0);
2674        self.view_min_t + frac as f64 * (self.view_max_t - self.view_min_t)
2675    }
2676
2677    pub fn screen_y_to_value(&self, y: f32, screen_h: f32) -> f32 {
2678        let frac = 1.0 - y / screen_h.max(1.0);
2679        self.view_min_v + frac * (self.view_max_v - self.view_min_v)
2680    }
2681
2682    pub fn zoom(&mut self, center_t: f64, center_v: f32, scale: f32) {
2683        let dt  = (self.view_max_t - self.view_min_t) * scale as f64;
2684        let dv  = (self.view_max_v - self.view_min_v) * scale;
2685        self.view_min_t = center_t - dt * 0.5;
2686        self.view_max_t = center_t + dt * 0.5;
2687        self.view_min_v = center_v - dv * 0.5;
2688        self.view_max_v = center_v + dv * 0.5;
2689    }
2690
2691    pub fn fit_to_curve(&mut self, curve: &FloatCurve) {
2692        if curve.keys.is_empty() { return; }
2693        let (min_t, max_t) = (curve.keys.first().unwrap().time, curve.keys.last().unwrap().time);
2694        let (min_v, max_v) = curve.value_range();
2695        let pad_t = (max_t - min_t) * 0.1;
2696        let pad_v = (max_v - min_v) * 0.1;
2697        self.view_min_t = min_t - pad_t;
2698        self.view_max_t = max_t + pad_t;
2699        self.view_min_v = min_v - pad_v;
2700        self.view_max_v = max_v + pad_v;
2701    }
2702}
2703
2704// ============================================================
2705// TRACK COLLECTION (all track types in one place)
2706// ============================================================
2707
2708#[derive(Debug)]
2709pub struct TrackCollection {
2710    pub camera_tracks:      HashMap<u64, CameraTrack>,
2711    pub actor_tracks:       HashMap<u64, ActorTrack>,
2712    pub animation_tracks:   HashMap<u64, AnimationTrack>,
2713    pub audio_tracks:       HashMap<u64, AudioTrack>,
2714    pub vfx_tracks:         HashMap<u64, VfxTrack>,
2715    pub light_tracks:       HashMap<u64, LightTrack>,
2716    pub post_fx_tracks:     HashMap<u64, PostFxTrack>,
2717    pub subtitle_tracks:    HashMap<u64, SubtitleTrack>,
2718    pub event_tracks:       HashMap<u64, EventTrack>,
2719    pub transform_tracks:   HashMap<u64, TransformTrack>,
2720    pub blend_shape_tracks: HashMap<u64, BlendShapeTrack>,
2721    pub visibility_tracks:  HashMap<u64, VisibilityTrack>,
2722    pub time_dilation_tracks: HashMap<u64, TimeDilationTrack>,
2723    // track order for display
2724    pub track_order: Vec<u64>,
2725}
2726
2727impl TrackCollection {
2728    pub fn new() -> Self {
2729        TrackCollection {
2730            camera_tracks:      HashMap::new(),
2731            actor_tracks:       HashMap::new(),
2732            animation_tracks:   HashMap::new(),
2733            audio_tracks:       HashMap::new(),
2734            vfx_tracks:         HashMap::new(),
2735            light_tracks:       HashMap::new(),
2736            post_fx_tracks:     HashMap::new(),
2737            subtitle_tracks:    HashMap::new(),
2738            event_tracks:       HashMap::new(),
2739            transform_tracks:   HashMap::new(),
2740            blend_shape_tracks: HashMap::new(),
2741            visibility_tracks:  HashMap::new(),
2742            time_dilation_tracks: HashMap::new(),
2743            track_order:        Vec::new(),
2744        }
2745    }
2746
2747    pub fn track_count(&self) -> usize {
2748        self.camera_tracks.len()
2749            + self.actor_tracks.len()
2750            + self.animation_tracks.len()
2751            + self.audio_tracks.len()
2752            + self.vfx_tracks.len()
2753            + self.light_tracks.len()
2754            + self.post_fx_tracks.len()
2755            + self.subtitle_tracks.len()
2756            + self.event_tracks.len()
2757            + self.transform_tracks.len()
2758            + self.blend_shape_tracks.len()
2759            + self.visibility_tracks.len()
2760            + self.time_dilation_tracks.len()
2761    }
2762
2763    pub fn add_camera_track(&mut self, track: CameraTrack) {
2764        let id = track.base.id;
2765        self.track_order.push(id);
2766        self.camera_tracks.insert(id, track);
2767    }
2768
2769    pub fn add_actor_track(&mut self, track: ActorTrack) {
2770        let id = track.base.id;
2771        self.track_order.push(id);
2772        self.actor_tracks.insert(id, track);
2773    }
2774
2775    pub fn add_animation_track(&mut self, track: AnimationTrack) {
2776        let id = track.base.id;
2777        self.track_order.push(id);
2778        self.animation_tracks.insert(id, track);
2779    }
2780
2781    pub fn add_audio_track(&mut self, track: AudioTrack) {
2782        let id = track.base.id;
2783        self.track_order.push(id);
2784        self.audio_tracks.insert(id, track);
2785    }
2786
2787    pub fn add_vfx_track(&mut self, track: VfxTrack) {
2788        let id = track.base.id;
2789        self.track_order.push(id);
2790        self.vfx_tracks.insert(id, track);
2791    }
2792
2793    pub fn add_light_track(&mut self, track: LightTrack) {
2794        let id = track.base.id;
2795        self.track_order.push(id);
2796        self.light_tracks.insert(id, track);
2797    }
2798
2799    pub fn add_post_fx_track(&mut self, track: PostFxTrack) {
2800        let id = track.base.id;
2801        self.track_order.push(id);
2802        self.post_fx_tracks.insert(id, track);
2803    }
2804
2805    pub fn add_subtitle_track(&mut self, track: SubtitleTrack) {
2806        let id = track.base.id;
2807        self.track_order.push(id);
2808        self.subtitle_tracks.insert(id, track);
2809    }
2810
2811    pub fn add_event_track(&mut self, track: EventTrack) {
2812        let id = track.base.id;
2813        self.track_order.push(id);
2814        self.event_tracks.insert(id, track);
2815    }
2816
2817    pub fn add_transform_track(&mut self, track: TransformTrack) {
2818        let id = track.base.id;
2819        self.track_order.push(id);
2820        self.transform_tracks.insert(id, track);
2821    }
2822
2823    pub fn add_blend_shape_track(&mut self, track: BlendShapeTrack) {
2824        let id = track.base.id;
2825        self.track_order.push(id);
2826        self.blend_shape_tracks.insert(id, track);
2827    }
2828
2829    pub fn add_visibility_track(&mut self, track: VisibilityTrack) {
2830        let id = track.base.id;
2831        self.track_order.push(id);
2832        self.visibility_tracks.insert(id, track);
2833    }
2834
2835    pub fn add_time_dilation_track(&mut self, track: TimeDilationTrack) {
2836        let id = track.base.id;
2837        self.track_order.push(id);
2838        self.time_dilation_tracks.insert(id, track);
2839    }
2840
2841    pub fn remove_track(&mut self, id: u64) {
2842        self.track_order.retain(|&tid| tid != id);
2843        self.camera_tracks.remove(&id);
2844        self.actor_tracks.remove(&id);
2845        self.animation_tracks.remove(&id);
2846        self.audio_tracks.remove(&id);
2847        self.vfx_tracks.remove(&id);
2848        self.light_tracks.remove(&id);
2849        self.post_fx_tracks.remove(&id);
2850        self.subtitle_tracks.remove(&id);
2851        self.event_tracks.remove(&id);
2852        self.transform_tracks.remove(&id);
2853        self.blend_shape_tracks.remove(&id);
2854        self.visibility_tracks.remove(&id);
2855        self.time_dilation_tracks.remove(&id);
2856    }
2857
2858    pub fn is_track_enabled(&self, id: u64) -> bool {
2859        if let Some(t) = self.camera_tracks.get(&id)      { return t.base.enabled; }
2860        if let Some(t) = self.actor_tracks.get(&id)       { return t.base.enabled; }
2861        if let Some(t) = self.animation_tracks.get(&id)   { return t.base.enabled; }
2862        if let Some(t) = self.audio_tracks.get(&id)       { return t.base.enabled; }
2863        if let Some(t) = self.vfx_tracks.get(&id)         { return t.base.enabled; }
2864        if let Some(t) = self.light_tracks.get(&id)       { return t.base.enabled; }
2865        if let Some(t) = self.post_fx_tracks.get(&id)     { return t.base.enabled; }
2866        if let Some(t) = self.subtitle_tracks.get(&id)    { return t.base.enabled; }
2867        if let Some(t) = self.event_tracks.get(&id)       { return t.base.enabled; }
2868        if let Some(t) = self.transform_tracks.get(&id)   { return t.base.enabled; }
2869        if let Some(t) = self.blend_shape_tracks.get(&id) { return t.base.enabled; }
2870        if let Some(t) = self.visibility_tracks.get(&id)  { return t.base.enabled; }
2871        if let Some(t) = self.time_dilation_tracks.get(&id) { return t.base.enabled; }
2872        false
2873    }
2874
2875    pub fn set_track_enabled(&mut self, id: u64, enabled: bool) {
2876        macro_rules! set_enabled {
2877            ($map:expr) => { if let Some(t) = $map.get_mut(&id) { t.base.enabled = enabled; return; } };
2878        }
2879        set_enabled!(self.camera_tracks);
2880        set_enabled!(self.actor_tracks);
2881        set_enabled!(self.animation_tracks);
2882        set_enabled!(self.audio_tracks);
2883        set_enabled!(self.vfx_tracks);
2884        set_enabled!(self.light_tracks);
2885        set_enabled!(self.post_fx_tracks);
2886        set_enabled!(self.subtitle_tracks);
2887        set_enabled!(self.event_tracks);
2888        set_enabled!(self.transform_tracks);
2889        set_enabled!(self.blend_shape_tracks);
2890        set_enabled!(self.visibility_tracks);
2891        set_enabled!(self.time_dilation_tracks);
2892    }
2893
2894    pub fn move_track_up(&mut self, id: u64) {
2895        if let Some(idx) = self.track_order.iter().position(|&tid| tid == id) {
2896            if idx > 0 { self.track_order.swap(idx, idx - 1); }
2897        }
2898    }
2899
2900    pub fn move_track_down(&mut self, id: u64) {
2901        if let Some(idx) = self.track_order.iter().position(|&tid| tid == id) {
2902            if idx + 1 < self.track_order.len() { self.track_order.swap(idx, idx + 1); }
2903        }
2904    }
2905}
2906
2907// ============================================================
2908// FRAME EVALUATION RESULT
2909// ============================================================
2910
2911#[derive(Clone, Debug)]
2912pub struct FrameEvalResult {
2913    pub time: f64,
2914    pub camera_transforms: HashMap<u64, Mat4>,
2915    pub camera_fovs:       HashMap<u64, f32>,
2916    pub actor_transforms:  HashMap<u64, Mat4>,
2917    pub blend_shapes:      HashMap<u64, HashMap<String, f32>>,
2918    pub light_states:      HashMap<u64, (Vec4, f32, f32)>,
2919    pub post_fx:           Vec<PostFxKeyframe>,
2920    pub active_subtitles:  Vec<SubtitleKeyframe>,
2921    pub fired_events:      Vec<EventKeyframe>,
2922    pub time_scale:        f32,
2923    pub visibility:        HashMap<u64, f32>,
2924}
2925
2926impl FrameEvalResult {
2927    pub fn new(time: f64) -> Self {
2928        FrameEvalResult {
2929            time,
2930            camera_transforms: HashMap::new(),
2931            camera_fovs:       HashMap::new(),
2932            actor_transforms:  HashMap::new(),
2933            blend_shapes:      HashMap::new(),
2934            light_states:      HashMap::new(),
2935            post_fx:           Vec::new(),
2936            active_subtitles:  Vec::new(),
2937            fired_events:      Vec::new(),
2938            time_scale:        1.0,
2939            visibility:        HashMap::new(),
2940        }
2941    }
2942}
2943
2944// ============================================================
2945// CINEMATIC SEQUENCER (main struct)
2946// ============================================================
2947
2948pub struct CinematicSequencer {
2949    // Sequences
2950    pub master_sequence:  Sequence,
2951    pub sequences:        HashMap<u64, Sequence>,
2952
2953    // Tracks
2954    pub tracks: TrackCollection,
2955
2956    // Shot list
2957    pub shot_list: ShotList,
2958
2959    // Cinematic events
2960    pub screen_flashes:  Vec<ScreenFlashEvent>,
2961    pub rumble_events:   Vec<RumbleEvent>,
2962    pub slow_mo_events:  Vec<SlowMotionEvent>,
2963    pub letterbox_events: Vec<LetterboxEvent>,
2964    pub chapter_markers: Vec<ChapterMarker>,
2965    pub branching_triggers: Vec<BranchingTrigger>,
2966
2967    // Playback
2968    pub playback: PlaybackController,
2969    pub prev_eval_time: f64,
2970
2971    // Undo/redo
2972    pub undo_history: SequencerUndoHistory,
2973
2974    // Selection
2975    pub selection: SequencerSelection,
2976
2977    // Curve editor
2978    pub curve_editor: CurveEditorState,
2979
2980    // Camera blend state
2981    pub active_camera_id: Option<u64>,
2982    pub blend_from_camera: Option<u64>,
2983    pub camera_blend_t:    f32,
2984    pub camera_blend_duration: f32,
2985
2986    // Letterbox state
2987    pub letterbox_amount: f32,
2988
2989    // Time dilation
2990    pub current_time_scale: f32,
2991
2992    // Settings
2993    pub auto_key: bool,
2994    pub auto_key_mode: AutoKeyMode,
2995    pub default_interp: InterpType,
2996    pub show_all_tracks: bool,
2997    pub track_height: f32,
2998}
2999
3000#[derive(Clone, Debug, PartialEq)]
3001pub enum AutoKeyMode {
3002    None,
3003    KeyOnChange,
3004    KeyAllModified,
3005}
3006
3007impl CinematicSequencer {
3008    pub fn new(name: &str, duration: f64, fps: FrameRate) -> Self {
3009        let fps_clone = fps.clone();
3010        CinematicSequencer {
3011            master_sequence: Sequence::new(name, duration, fps),
3012            sequences: HashMap::new(),
3013            tracks: TrackCollection::new(),
3014            shot_list: ShotList::new(),
3015            screen_flashes: Vec::new(),
3016            rumble_events: Vec::new(),
3017            slow_mo_events: Vec::new(),
3018            letterbox_events: Vec::new(),
3019            chapter_markers: Vec::new(),
3020            branching_triggers: Vec::new(),
3021            playback: PlaybackController::new(fps_clone),
3022            prev_eval_time: 0.0,
3023            undo_history: SequencerUndoHistory::new(),
3024            selection: SequencerSelection::new(),
3025            curve_editor: CurveEditorState::new(),
3026            active_camera_id: None,
3027            blend_from_camera: None,
3028            camera_blend_t: 0.0,
3029            camera_blend_duration: 0.5,
3030            letterbox_amount: 0.0,
3031            current_time_scale: 1.0,
3032            auto_key: false,
3033            auto_key_mode: AutoKeyMode::None,
3034            default_interp: InterpType::Cubic,
3035            show_all_tracks: true,
3036            track_height: 32.0,
3037        }
3038    }
3039
3040    // ---- TRACK CREATION ----
3041
3042    pub fn add_camera_track(&mut self, name: &str) -> u64 {
3043        let id = next_id();
3044        let track = CameraTrack::new(id, name);
3045        let kind = track.base.kind.clone();
3046        self.tracks.add_camera_track(track);
3047        self.undo_history.push(SequencerCommand::AddTrack { track_id: id, track_kind: kind });
3048        id
3049    }
3050
3051    pub fn add_actor_track(&mut self, name: &str, entity_id: u64) -> u64 {
3052        let id = next_id();
3053        let track = ActorTrack::new(id, name, entity_id);
3054        let kind = track.base.kind.clone();
3055        self.tracks.add_actor_track(track);
3056        self.undo_history.push(SequencerCommand::AddTrack { track_id: id, track_kind: kind });
3057        id
3058    }
3059
3060    pub fn add_animation_track(&mut self, name: &str, entity_id: u64) -> u64 {
3061        let id = next_id();
3062        let track = AnimationTrack::new(id, name, entity_id);
3063        let kind = track.base.kind.clone();
3064        self.tracks.add_animation_track(track);
3065        self.undo_history.push(SequencerCommand::AddTrack { track_id: id, track_kind: kind });
3066        id
3067    }
3068
3069    pub fn add_audio_track(&mut self, name: &str) -> u64 {
3070        let id = next_id();
3071        let track = AudioTrack::new(id, name);
3072        let kind = track.base.kind.clone();
3073        self.tracks.add_audio_track(track);
3074        self.undo_history.push(SequencerCommand::AddTrack { track_id: id, track_kind: kind });
3075        id
3076    }
3077
3078    pub fn add_vfx_track(&mut self, name: &str) -> u64 {
3079        let id = next_id();
3080        let track = VfxTrack::new(id, name);
3081        let kind = track.base.kind.clone();
3082        self.tracks.add_vfx_track(track);
3083        self.undo_history.push(SequencerCommand::AddTrack { track_id: id, track_kind: kind });
3084        id
3085    }
3086
3087    pub fn add_light_track(&mut self, name: &str, entity_id: u64) -> u64 {
3088        let id = next_id();
3089        let track = LightTrack::new(id, name, entity_id);
3090        let kind = track.base.kind.clone();
3091        self.tracks.add_light_track(track);
3092        self.undo_history.push(SequencerCommand::AddTrack { track_id: id, track_kind: kind });
3093        id
3094    }
3095
3096    pub fn add_post_fx_track(&mut self, name: &str) -> u64 {
3097        let id = next_id();
3098        let track = PostFxTrack::new(id, name);
3099        let kind = track.base.kind.clone();
3100        self.tracks.add_post_fx_track(track);
3101        self.undo_history.push(SequencerCommand::AddTrack { track_id: id, track_kind: kind });
3102        id
3103    }
3104
3105    pub fn add_subtitle_track(&mut self, name: &str) -> u64 {
3106        let id = next_id();
3107        let track = SubtitleTrack::new(id, name);
3108        let kind = track.base.kind.clone();
3109        self.tracks.add_subtitle_track(track);
3110        self.undo_history.push(SequencerCommand::AddTrack { track_id: id, track_kind: kind });
3111        id
3112    }
3113
3114    pub fn add_event_track(&mut self, name: &str) -> u64 {
3115        let id = next_id();
3116        let track = EventTrack::new(id, name);
3117        let kind = track.base.kind.clone();
3118        self.tracks.add_event_track(track);
3119        self.undo_history.push(SequencerCommand::AddTrack { track_id: id, track_kind: kind });
3120        id
3121    }
3122
3123    pub fn add_transform_track(&mut self, name: &str, entity_id: u64) -> u64 {
3124        let id = next_id();
3125        let track = TransformTrack::new(id, name, entity_id);
3126        let kind = track.base.kind.clone();
3127        self.tracks.add_transform_track(track);
3128        self.undo_history.push(SequencerCommand::AddTrack { track_id: id, track_kind: kind });
3129        id
3130    }
3131
3132    pub fn add_blend_shape_track(&mut self, name: &str, entity_id: u64) -> u64 {
3133        let id = next_id();
3134        let track = BlendShapeTrack::new(id, name, entity_id);
3135        let kind = track.base.kind.clone();
3136        self.tracks.add_blend_shape_track(track);
3137        self.undo_history.push(SequencerCommand::AddTrack { track_id: id, track_kind: kind });
3138        id
3139    }
3140
3141    pub fn add_visibility_track(&mut self, name: &str, entity_id: u64) -> u64 {
3142        let id = next_id();
3143        let track = VisibilityTrack::new(id, name, entity_id);
3144        let kind = track.base.kind.clone();
3145        self.tracks.add_visibility_track(track);
3146        self.undo_history.push(SequencerCommand::AddTrack { track_id: id, track_kind: kind });
3147        id
3148    }
3149
3150    pub fn add_time_dilation_track(&mut self, name: &str) -> u64 {
3151        let id = next_id();
3152        let track = TimeDilationTrack::new(id, name);
3153        let kind = track.base.kind.clone();
3154        self.tracks.add_time_dilation_track(track);
3155        self.undo_history.push(SequencerCommand::AddTrack { track_id: id, track_kind: kind });
3156        id
3157    }
3158
3159    pub fn remove_track(&mut self, id: u64) {
3160        let kind = if self.tracks.camera_tracks.contains_key(&id) { TrackKind::Camera }
3161            else if self.tracks.actor_tracks.contains_key(&id)       { TrackKind::Actor }
3162            else if self.tracks.animation_tracks.contains_key(&id)   { TrackKind::Animation }
3163            else if self.tracks.audio_tracks.contains_key(&id)       { TrackKind::Audio }
3164            else { TrackKind::Event };
3165        self.tracks.remove_track(id);
3166        self.undo_history.push(SequencerCommand::RemoveTrack { track_id: id });
3167    }
3168
3169    // ---- KEYFRAME INSERTION ----
3170
3171    pub fn add_camera_keyframe(&mut self, track_id: u64, kf: CameraKeyframe) {
3172        let time = kf.time;
3173        if let Some(track) = self.tracks.camera_tracks.get_mut(&track_id) {
3174            track.add_keyframe(kf);
3175            self.undo_history.push(SequencerCommand::AddKeyframe {
3176                track_id, track_kind: TrackKind::Camera, time,
3177            });
3178        }
3179    }
3180
3181    pub fn add_actor_keyframe(&mut self, track_id: u64, kf: ActorKeyframe) {
3182        let time = kf.time;
3183        if let Some(track) = self.tracks.actor_tracks.get_mut(&track_id) {
3184            track.add_keyframe(kf);
3185            self.undo_history.push(SequencerCommand::AddKeyframe {
3186                track_id, track_kind: TrackKind::Actor, time,
3187            });
3188        }
3189    }
3190
3191    pub fn add_subtitle(&mut self, track_id: u64, kf: SubtitleKeyframe) {
3192        let time = kf.time;
3193        if let Some(track) = self.tracks.subtitle_tracks.get_mut(&track_id) {
3194            track.add_subtitle(kf);
3195            self.undo_history.push(SequencerCommand::AddKeyframe {
3196                track_id, track_kind: TrackKind::Subtitle, time,
3197            });
3198        }
3199    }
3200
3201    pub fn add_event(&mut self, track_id: u64, ev: EventKeyframe) {
3202        let time = ev.time;
3203        if let Some(track) = self.tracks.event_tracks.get_mut(&track_id) {
3204            track.add_event(ev);
3205            self.undo_history.push(SequencerCommand::AddKeyframe {
3206                track_id, track_kind: TrackKind::Event, time,
3207            });
3208        }
3209    }
3210
3211    // ---- CINEMATIC EVENTS ----
3212
3213    pub fn add_screen_flash(&mut self, time: f64, color: Vec4, duration: f64, intensity: f32) {
3214        self.screen_flashes.push(ScreenFlashEvent { time, color, duration, intensity });
3215        self.screen_flashes.sort_by(|a, b| a.time.partial_cmp(&b.time).unwrap_or(std::cmp::Ordering::Equal));
3216    }
3217
3218    pub fn add_rumble(&mut self, time: f64, duration: f64, intensity: f32, frequency: f32) {
3219        self.rumble_events.push(RumbleEvent { time, duration, intensity, frequency, decay: 3.0 });
3220    }
3221
3222    pub fn add_slow_mo(&mut self, time: f64, duration: f64, scale: f32) {
3223        self.slow_mo_events.push(SlowMotionEvent {
3224            time, duration, time_scale: scale, ease_in: 0.3, ease_out: 0.5,
3225        });
3226    }
3227
3228    pub fn add_letterbox(&mut self, time: f64, duration: f64) {
3229        self.letterbox_events.push(LetterboxEvent {
3230            time, duration, aspect: LETTERBOX_ASPECT, ease_in: 0.5, ease_out: 0.5,
3231        });
3232    }
3233
3234    pub fn add_chapter(&mut self, time: f64, name: &str) {
3235        self.chapter_markers.push(ChapterMarker { time, name: name.to_string(), thumb: None });
3236        self.chapter_markers.sort_by(|a, b| a.time.partial_cmp(&b.time).unwrap_or(std::cmp::Ordering::Equal));
3237    }
3238
3239    // ---- SHOT MANAGEMENT ----
3240
3241    pub fn add_shot(&mut self, name: &str, start: f64, end: f64, camera_id: u64) -> u64 {
3242        let id = next_id();
3243        let shot = Shot::new(id, name, start, end, camera_id);
3244        self.shot_list.add_shot(shot);
3245        self.undo_history.push(SequencerCommand::AddShot { shot_id: id });
3246        id
3247    }
3248
3249    pub fn current_shot(&self) -> Option<&Shot> {
3250        let time = self.playback.current_time;
3251        self.shot_list.shot_at_time(time)
3252    }
3253
3254    // ---- CAMERA BLEND ----
3255
3256    pub fn cut_to_camera(&mut self, camera_id: u64) {
3257        self.blend_from_camera = None;
3258        self.active_camera_id = Some(camera_id);
3259        self.camera_blend_t = 1.0;
3260    }
3261
3262    pub fn blend_to_camera(&mut self, camera_id: u64, duration: f32) {
3263        self.blend_from_camera = self.active_camera_id;
3264        self.active_camera_id = Some(camera_id);
3265        self.camera_blend_t = 0.0;
3266        self.camera_blend_duration = duration;
3267    }
3268
3269    pub fn update_camera_blend(&mut self, dt: f32) {
3270        if self.camera_blend_t < 1.0 {
3271            self.camera_blend_t = (self.camera_blend_t + dt / self.camera_blend_duration.max(EPSILON)).min(1.0);
3272        }
3273    }
3274
3275    pub fn blended_camera_matrix(&self, time: f64) -> Mat4 {
3276        let active_id = match self.active_camera_id { Some(id) => id, None => return Mat4::IDENTITY };
3277        let active_mat = self.tracks.camera_tracks.get(&active_id)
3278            .map(|t| t.camera_matrix(time))
3279            .unwrap_or(Mat4::IDENTITY);
3280        if self.camera_blend_t >= 1.0 || self.blend_from_camera.is_none() {
3281            return active_mat;
3282        }
3283        let from_id  = self.blend_from_camera.unwrap();
3284        let from_mat = self.tracks.camera_tracks.get(&from_id)
3285            .map(|t| t.camera_matrix(time))
3286            .unwrap_or(Mat4::IDENTITY);
3287        // Decompose and re-compose with slerp
3288        let (from_scale, from_rot, from_trans) = decompose_mat4(from_mat);
3289        let (to_scale,   to_rot,   to_trans)   = decompose_mat4(active_mat);
3290        let t = smoother_step(self.camera_blend_t);
3291        let blend_pos = lerp_vec3(from_trans, to_trans, t);
3292        let blend_rot = from_rot.slerp(to_rot, t);
3293        let blend_scale = lerp_vec3(from_scale, to_scale, t);
3294        Mat4::from_scale_rotation_translation(blend_scale, blend_rot, blend_pos)
3295    }
3296
3297    // ---- FULL FRAME EVALUATION ----
3298
3299    pub fn evaluate_frame(&mut self, dt: f32) -> FrameEvalResult {
3300        let prev_time = self.prev_eval_time;
3301        let time = self.playback.current_time;
3302        self.prev_eval_time = time;
3303
3304        let mut result = FrameEvalResult::new(time);
3305
3306        // Time scale from dilation tracks
3307        let mut combined_scale = 1.0_f32;
3308        for track in self.tracks.time_dilation_tracks.values() {
3309            if !track.base.enabled || track.base.muted { continue; }
3310            combined_scale *= track.evaluate(time);
3311        }
3312        // Slow-mo events
3313        for ev in &self.slow_mo_events {
3314            combined_scale *= ev.scale_at(time);
3315        }
3316        result.time_scale = combined_scale;
3317        self.current_time_scale = combined_scale;
3318
3319        // Camera tracks
3320        for (&id, track) in &self.tracks.camera_tracks {
3321            if !track.base.enabled || track.base.muted { continue; }
3322            result.camera_transforms.insert(id, track.camera_matrix(time));
3323            result.camera_fovs.insert(id, track.evaluate_fov(time));
3324        }
3325
3326        // Actor tracks
3327        for (&id, track) in &self.tracks.actor_tracks {
3328            if !track.base.enabled || track.base.muted { continue; }
3329            result.actor_transforms.insert(id, track.world_matrix(time));
3330        }
3331
3332        // Transform tracks (additive or override)
3333        for (_, track) in &self.tracks.transform_tracks {
3334            if !track.base.enabled || track.base.muted { continue; }
3335            let (pos, rot, scale) = track.evaluate(time);
3336            let mat = Mat4::from_scale_rotation_translation(scale, rot, pos);
3337            if track.additive {
3338                let base = result.actor_transforms.get(&track.entity_id).cloned().unwrap_or(Mat4::IDENTITY);
3339                result.actor_transforms.insert(track.entity_id, base * mat);
3340            } else {
3341                result.actor_transforms.insert(track.entity_id, mat);
3342            }
3343        }
3344
3345        // Light tracks
3346        for (&id, track) in &self.tracks.light_tracks {
3347            if !track.base.enabled || track.base.muted { continue; }
3348            let (mut color, mut intensity, range) = track.evaluate(time);
3349            intensity *= track.flicker_factor(time);
3350            result.light_states.insert(id, (color, intensity, range));
3351        }
3352
3353        // Post FX tracks (stack multiple, blending by weight)
3354        let mut post_fx_base = PostFxKeyframe::default_at(time);
3355        for (_, track) in &self.tracks.post_fx_tracks {
3356            if !track.base.enabled || track.base.muted { continue; }
3357            let pfx = track.evaluate(time);
3358            let w = track.base.weight;
3359            post_fx_base.exposure        = lerp(post_fx_base.exposure,        pfx.exposure,        w);
3360            post_fx_base.contrast        = lerp(post_fx_base.contrast,        pfx.contrast,        w);
3361            post_fx_base.saturation      = lerp(post_fx_base.saturation,      pfx.saturation,      w);
3362            post_fx_base.bloom_intensity = lerp(post_fx_base.bloom_intensity, pfx.bloom_intensity, w);
3363            post_fx_base.vignette        = lerp(post_fx_base.vignette,        pfx.vignette,        w);
3364            post_fx_base.chromatic_ab    = lerp(post_fx_base.chromatic_ab,    pfx.chromatic_ab,    w);
3365            post_fx_base.film_grain      = lerp(post_fx_base.film_grain,      pfx.film_grain,      w);
3366        }
3367        result.post_fx.push(post_fx_base);
3368
3369        // Subtitle tracks
3370        for (_, track) in &self.tracks.subtitle_tracks {
3371            if !track.base.enabled { continue; }
3372            result.active_subtitles.extend(track.active_at(time).into_iter().cloned());
3373        }
3374
3375        // Event tracks — poll
3376        for (_, track) in &mut self.tracks.event_tracks {
3377            if !track.base.enabled { continue; }
3378            let fired = track.poll(prev_time, time);
3379            result.fired_events.extend(fired);
3380        }
3381
3382        // Blend shape tracks
3383        for (_, track) in &self.tracks.blend_shape_tracks {
3384            if !track.base.enabled { continue; }
3385            let weights = track.evaluate(time);
3386            result.blend_shapes.insert(track.entity_id, weights);
3387        }
3388
3389        // Visibility tracks
3390        for (_, track) in &self.tracks.visibility_tracks {
3391            if !track.base.enabled { continue; }
3392            let opacity = track.evaluate_opacity(time);
3393            result.visibility.insert(track.entity_id, opacity);
3394        }
3395
3396        // Update camera shake for all camera tracks
3397        for (_, track) in &mut self.tracks.camera_tracks {
3398            track.update_shake(dt);
3399        }
3400
3401        // Update camera blend
3402        self.update_camera_blend(dt);
3403
3404        // Update letterbox
3405        let max_bar = self.letterbox_events.iter()
3406            .map(|e| e.bar_height_at(100.0, 100.0 * LETTERBOX_ASPECT, time))
3407            .fold(0.0_f32, f32::max);
3408        self.letterbox_amount = max_bar;
3409
3410        result
3411    }
3412
3413    // ---- PLAYBACK ----
3414
3415    pub fn update(&mut self, dt: f32) {
3416        let scaled_dt = dt * self.current_time_scale;
3417        self.playback.update(scaled_dt, self.master_sequence.duration);
3418    }
3419
3420    pub fn play(&mut self)  { self.playback.play(); }
3421    pub fn pause(&mut self) { self.playback.pause(); }
3422    pub fn stop(&mut self)  { self.playback.stop(); self.prev_eval_time = 0.0; }
3423    pub fn scrub(&mut self, t: f64) { self.playback.scrub_to(t); }
3424
3425    pub fn set_loop_region(&mut self, start: f64, end: f64) {
3426        self.playback.loop_start   = start;
3427        self.playback.loop_end     = end;
3428        self.playback.loop_enabled = true;
3429    }
3430
3431    pub fn goto_next_chapter(&mut self) {
3432        let cur = self.playback.current_time;
3433        if let Some(chap) = self.chapter_markers.iter().find(|c| c.time > cur) {
3434            self.playback.scrub_to(chap.time);
3435        }
3436    }
3437
3438    pub fn goto_prev_chapter(&mut self) {
3439        let cur = self.playback.current_time;
3440        if let Some(chap) = self.chapter_markers.iter().rev().find(|c| c.time < cur - 0.5) {
3441            self.playback.scrub_to(chap.time);
3442        }
3443    }
3444
3445    // ---- UNDO / REDO ----
3446
3447    pub fn undo(&mut self) {
3448        if let Some(cmd) = self.undo_history.undo() {
3449            self.apply_undo(cmd);
3450        }
3451    }
3452
3453    pub fn redo(&mut self) {
3454        if let Some(cmd) = self.undo_history.redo() {
3455            self.apply_redo(cmd);
3456        }
3457    }
3458
3459    fn apply_undo(&mut self, cmd: SequencerCommand) {
3460        match cmd {
3461            SequencerCommand::SetTrackEnabled { track_id, old_val, .. } => {
3462                self.tracks.set_track_enabled(track_id, old_val);
3463            }
3464            SequencerCommand::SetDuration { old_duration, .. } => {
3465                self.master_sequence.duration = old_duration;
3466            }
3467            SequencerCommand::AddTrack { track_id, .. } => {
3468                self.tracks.remove_track(track_id);
3469            }
3470            _ => {}
3471        }
3472    }
3473
3474    fn apply_redo(&mut self, cmd: SequencerCommand) {
3475        match cmd {
3476            SequencerCommand::SetTrackEnabled { track_id, new_val, .. } => {
3477                self.tracks.set_track_enabled(track_id, new_val);
3478            }
3479            SequencerCommand::SetDuration { new_duration, .. } => {
3480                self.master_sequence.duration = new_duration;
3481            }
3482            _ => {}
3483        }
3484    }
3485
3486    // ---- COPY / PASTE KEYFRAMES ----
3487
3488    pub fn copy_selected_keyframes(&mut self) {
3489        self.selection.copy_keyframes(self.playback.current_time);
3490    }
3491
3492    pub fn paste_keyframes_at(&mut self, target_time: f64) {
3493        let offset = target_time - self.selection.clipboard_offset;
3494        for (&track_id, times) in &self.selection.clipboard_keyframes {
3495            let new_times: Vec<f64> = times.iter().map(|&t| t + offset).collect();
3496            // For camera tracks, duplicate keyframes at new times
3497            if let Some(track) = self.tracks.camera_tracks.get_mut(&track_id) {
3498                let kfs_to_add: Vec<CameraKeyframe> = new_times.iter().filter_map(|&new_t| {
3499                    // Find original keyframe near original time
3500                    let orig_t = new_t - offset;
3501                    track.keyframes.iter()
3502                        .min_by(|a, b| (a.time - orig_t).abs().partial_cmp(&(b.time - orig_t).abs()).unwrap_or(std::cmp::Ordering::Equal))
3503                        .map(|kf| { let mut kf2 = kf.clone(); kf2.time = new_t; kf2 })
3504                }).collect();
3505                for kf in kfs_to_add {
3506                    track.add_keyframe(kf);
3507                }
3508            }
3509            self.undo_history.push(SequencerCommand::PasteKeyframes { track_id, times: new_times });
3510        }
3511    }
3512
3513    // ---- EXPORT ----
3514
3515    pub fn export_edl(&self) -> EdlDocument {
3516        EdlDocument::from_shot_list(&self.shot_list, self.master_sequence.fps.clone())
3517    }
3518
3519    pub fn export_subtitles_srt(&self) -> String {
3520        let mut combined = String::new();
3521        let mut counter = 1u32;
3522        let fps = self.master_sequence.fps.fps();
3523        // Gather all subtitles sorted by time
3524        let mut all_subs: Vec<&SubtitleKeyframe> = Vec::new();
3525        for track in self.tracks.subtitle_tracks.values() {
3526            all_subs.extend(track.subtitles.iter());
3527        }
3528        all_subs.sort_by(|a, b| a.time.partial_cmp(&b.time).unwrap_or(std::cmp::Ordering::Equal));
3529        for sub in all_subs {
3530            let tc_start = secs_to_srt_tc(sub.time);
3531            let tc_end   = secs_to_srt_tc(sub.end_time);
3532            combined.push_str(&format!("{}\n{} --> {}\n{}\n\n", counter, tc_start, tc_end, sub.text));
3533            counter += 1;
3534        }
3535        combined
3536    }
3537
3538    pub fn bake_to_frames(&self, output_fps: FrameRate) -> Vec<FrameEvalResult> {
3539        // Return pre-baked results for every frame (read-only; does not mutate self)
3540        let total = self.master_sequence.duration;
3541        let frame_count = output_fps.seconds_to_frame(total);
3542        (0..=frame_count).map(|f| {
3543            let time = output_fps.frame_to_seconds(f);
3544            FrameEvalResult::new(time)
3545        }).collect()
3546    }
3547
3548    // ---- STATS ----
3549
3550    pub fn stats(&self) -> SequencerStats {
3551        let total_kfs: usize = self.tracks.camera_tracks.values()
3552            .map(|t| t.keyframes.len()).sum::<usize>()
3553            + self.tracks.actor_tracks.values()
3554                .map(|t| t.keyframes.len()).sum::<usize>()
3555            + self.tracks.transform_tracks.values()
3556                .map(|t| t.keyframes.len()).sum::<usize>();
3557
3558        SequencerStats {
3559            track_count:    self.tracks.track_count(),
3560            shot_count:     self.shot_list.shots.len(),
3561            chapter_count:  self.chapter_markers.len(),
3562            total_keyframes: total_kfs,
3563            duration:       self.master_sequence.duration,
3564            fps:            self.master_sequence.fps.fps(),
3565            frame_count:    self.master_sequence.frame_count(),
3566        }
3567    }
3568
3569    // ---- DURATION MANAGEMENT ----
3570
3571    pub fn set_duration(&mut self, duration: f64) {
3572        let old = self.master_sequence.duration;
3573        self.master_sequence.duration = duration;
3574        self.undo_history.push(SequencerCommand::SetDuration {
3575            old_duration: old, new_duration: duration,
3576        });
3577    }
3578
3579    pub fn expand_to_fit_tracks(&mut self) {
3580        let mut max_t = 0.0_f64;
3581        for t in self.tracks.camera_tracks.values() {
3582            if let Some(last) = t.keyframes.last() { max_t = max_t.max(last.time); }
3583        }
3584        for t in self.tracks.actor_tracks.values() {
3585            if let Some(last) = t.keyframes.last() { max_t = max_t.max(last.time); }
3586        }
3587        for t in self.tracks.subtitle_tracks.values() {
3588            if let Some(last) = t.subtitles.last() { max_t = max_t.max(last.end_time); }
3589        }
3590        for t in self.tracks.audio_tracks.values() {
3591            if let Some(last) = t.clips.last() {
3592                max_t = max_t.max(last.time + last.clip.duration);
3593            }
3594        }
3595        if max_t > self.master_sequence.duration {
3596            self.set_duration(max_t + 1.0);
3597        }
3598    }
3599
3600    // ---- FRAME RATE CONVERSION ----
3601
3602    pub fn convert_fps(&mut self, new_fps: FrameRate) {
3603        let old_fps = self.master_sequence.fps.clone();
3604        // Remap all keyframe times proportionally
3605        // (keyframe times are in seconds, so no conversion needed — just update FPS)
3606        let old = old_fps.clone();
3607        self.master_sequence.fps = new_fps.clone();
3608        self.playback.fps = new_fps.clone();
3609        self.undo_history.push(SequencerCommand::SetFps { old_fps: old, new_fps });
3610    }
3611
3612    // ---- FIND NEAREST KEYFRAME ----
3613
3614    pub fn nearest_camera_keyframe(&self, track_id: u64, time: f64) -> Option<f64> {
3615        self.tracks.camera_tracks.get(&track_id)?.keyframes.iter()
3616            .min_by(|a, b| (a.time - time).abs().partial_cmp(&(b.time - time).abs()).unwrap_or(std::cmp::Ordering::Equal))
3617            .map(|k| k.time)
3618    }
3619}
3620
3621// ============================================================
3622// HELPER FUNCTIONS
3623// ============================================================
3624
3625fn decompose_mat4(mat: Mat4) -> (Vec3, Quat, Vec3) {
3626    let trans = Vec3::new(mat.w_axis.x, mat.w_axis.y, mat.w_axis.z);
3627    let sx = Vec3::new(mat.x_axis.x, mat.x_axis.y, mat.x_axis.z).length();
3628    let sy = Vec3::new(mat.y_axis.x, mat.y_axis.y, mat.y_axis.z).length();
3629    let sz = Vec3::new(mat.z_axis.x, mat.z_axis.y, mat.z_axis.z).length();
3630    let scale = Vec3::new(sx, sy, sz);
3631    let rot_mat = Mat4::from_cols(
3632        mat.x_axis / sx.max(EPSILON),
3633        mat.y_axis / sy.max(EPSILON),
3634        mat.z_axis / sz.max(EPSILON),
3635        Vec4::W,
3636    );
3637    let rot = Quat::from_mat4(&rot_mat);
3638    (scale, rot, trans)
3639}
3640
3641fn secs_to_srt_tc(secs: f64) -> String {
3642    let ms    = ((secs.fract()) * 1000.0) as u32;
3643    let total = secs.floor() as u64;
3644    let h  = total / 3600;
3645    let m  = (total % 3600) / 60;
3646    let s  = total % 60;
3647    format!("{:02}:{:02}:{:02},{:03}", h, m, s, ms)
3648}
3649
3650// ============================================================
3651// SEQUENCER STATS
3652// ============================================================
3653
3654#[derive(Clone, Debug)]
3655pub struct SequencerStats {
3656    pub track_count:     usize,
3657    pub shot_count:      usize,
3658    pub chapter_count:   usize,
3659    pub total_keyframes: usize,
3660    pub duration:        f64,
3661    pub fps:             f32,
3662    pub frame_count:     u64,
3663}
3664
3665// ============================================================
3666// CURVE SAMPLER (for rendering curve editor)
3667// ============================================================
3668
3669pub struct CurveSampler;
3670
3671impl CurveSampler {
3672    /// Sample a float curve for display, returning (time, value) pairs
3673    pub fn sample(curve: &FloatCurve, view_start: f64, view_end: f64, pixel_width: u32) -> Vec<(f64, f32)> {
3674        if pixel_width == 0 { return Vec::new(); }
3675        (0..pixel_width).map(|i| {
3676            let t = lerp_f64(view_start, view_end, i as f64 / pixel_width as f64);
3677            let v = curve.evaluate(t);
3678            (t, v)
3679        }).collect()
3680    }
3681
3682    /// Compute tangent visualization line endpoints for a keyframe
3683    pub fn tangent_handles(curve: &FloatCurve, key_idx: usize, scale: f32) -> Option<(Vec2, Vec2)> {
3684        let key = curve.keys.get(key_idx)?;
3685        let handle = key.bezier_handle.as_ref()?;
3686        let base = Vec2::new(key.time as f32, key.value);
3687        let in_pt  = base + handle.in_tangent  * scale;
3688        let out_pt = base + handle.out_tangent * scale;
3689        Some((in_pt, out_pt))
3690    }
3691
3692    /// Find the pixel x-position of a keyframe in the curve editor view
3693    pub fn keyframe_screen_pos(
3694        key_time: f64,
3695        key_val:  f32,
3696        view_start: f64,
3697        view_end:   f64,
3698        val_min:    f32,
3699        val_max:    f32,
3700        screen_w:   f32,
3701        screen_h:   f32,
3702    ) -> Vec2 {
3703        let tx = ((key_time - view_start) / (view_end - view_start).max(1e-9)) as f32;
3704        let ty = (key_val - val_min) / (val_max - val_min).max(EPSILON);
3705        Vec2::new(tx * screen_w, (1.0 - ty) * screen_h)
3706    }
3707}
3708
3709// ============================================================
3710// MULTI-CURVE BLENDING
3711// ============================================================
3712
3713pub struct CurveBlender {
3714    pub curves:  Vec<(FloatCurve, f32)>, // (curve, weight)
3715}
3716
3717impl CurveBlender {
3718    pub fn new() -> Self { CurveBlender { curves: Vec::new() } }
3719
3720    pub fn add_curve(&mut self, curve: FloatCurve, weight: f32) {
3721        self.curves.push((curve, weight));
3722    }
3723
3724    pub fn evaluate(&self, time: f64) -> f32 {
3725        let total_weight: f32 = self.curves.iter().map(|(_, w)| *w).sum();
3726        if total_weight < EPSILON { return 0.0; }
3727        let weighted_sum: f32 = self.curves.iter().map(|(c, w)| c.evaluate(time) * w).sum();
3728        weighted_sum / total_weight
3729    }
3730
3731    pub fn evaluate_additive(&self, time: f64, base: f32) -> f32 {
3732        let add: f32 = self.curves.iter().map(|(c, w)| c.evaluate(time) * w).sum();
3733        base + add
3734    }
3735}
3736
3737// ============================================================
3738// WAVEFORM PREVIEW DATA
3739// ============================================================
3740
3741pub fn compute_waveform_preview(samples: &[f32], n_buckets: usize) -> Vec<(f32, f32)> {
3742    if samples.is_empty() || n_buckets == 0 { return Vec::new(); }
3743    let bucket_size = (samples.len() / n_buckets).max(1);
3744    (0..n_buckets).map(|i| {
3745        let start = i * bucket_size;
3746        let end   = ((i + 1) * bucket_size).min(samples.len());
3747        let slice = &samples[start..end];
3748        let min = slice.iter().cloned().fold(f32::MAX, f32::min);
3749        let max = slice.iter().cloned().fold(f32::MIN, f32::max);
3750        (min, max)
3751    }).collect()
3752}
3753
3754// ============================================================
3755// KEYFRAME COPYING BETWEEN TRACKS
3756// ============================================================
3757
3758pub fn copy_camera_keyframes_to_transform(
3759    camera_track: &CameraTrack,
3760    transform_track: &mut TransformTrack,
3761) {
3762    for kf in &camera_track.keyframes {
3763        let tkf = TransformKeyframe {
3764            time:     kf.time,
3765            position: kf.position,
3766            rotation: kf.rotation,
3767            scale:    Vec3::ONE,
3768            interp:   kf.interp.clone(),
3769        };
3770        transform_track.add_keyframe(tkf);
3771    }
3772}
3773
3774pub fn mirror_keyframes_time(curve: &mut FloatCurve, pivot_time: f64) {
3775    for key in &mut curve.keys {
3776        key.time = 2.0 * pivot_time - key.time;
3777    }
3778    curve.keys.sort_by(|a, b| a.time.partial_cmp(&b.time).unwrap_or(std::cmp::Ordering::Equal));
3779}
3780
3781pub fn reverse_keyframes(curve: &mut FloatCurve) {
3782    if curve.keys.len() < 2 { return; }
3783    let start = curve.keys.first().unwrap().time;
3784    let end   = curve.keys.last().unwrap().time;
3785    for key in &mut curve.keys {
3786        key.time = start + end - key.time;
3787    }
3788    curve.keys.sort_by(|a, b| a.time.partial_cmp(&b.time).unwrap_or(std::cmp::Ordering::Equal));
3789    // Swap bezier tangents
3790    for key in &mut curve.keys {
3791        if let Some(h) = &mut key.bezier_handle {
3792            let tmp = h.in_tangent;
3793            h.in_tangent  = Vec2::new(-h.out_tangent.x, h.out_tangent.y);
3794            h.out_tangent = Vec2::new(-tmp.x, tmp.y);
3795        }
3796    }
3797}
3798
3799pub fn scale_keyframe_values(curve: &mut FloatCurve, scale: f32) {
3800    for key in &mut curve.keys {
3801        key.value *= scale;
3802        if let Some(h) = &mut key.bezier_handle {
3803            h.in_tangent.y  *= scale;
3804            h.out_tangent.y *= scale;
3805        }
3806    }
3807}
3808
3809pub fn offset_keyframe_times(curve: &mut FloatCurve, offset: f64) {
3810    for key in &mut curve.keys {
3811        key.time += offset;
3812    }
3813}
3814
3815// ============================================================
3816// BATCH OPERATIONS ON MULTIPLE CURVES
3817// ============================================================
3818
3819pub fn align_keyframe_times(curves: &mut [FloatCurve], snap_interval: f64) {
3820    for curve in curves {
3821        for key in &mut curve.keys {
3822            key.time = (key.time / snap_interval).round() * snap_interval;
3823        }
3824    }
3825}
3826
3827pub fn merge_curves(a: &FloatCurve, b: &FloatCurve, blend: f32) -> FloatCurve {
3828    let mut result = FloatCurve::new(&format!("{}_{}_{}", a.name, b.name, blend as u32));
3829    // Collect all unique times
3830    let mut times: Vec<f64> = a.keys.iter().map(|k| k.time)
3831        .chain(b.keys.iter().map(|k| k.time))
3832        .collect();
3833    times.sort_by(|x, y| x.partial_cmp(y).unwrap_or(std::cmp::Ordering::Equal));
3834    times.dedup_by(|x, y| (*x - *y).abs() < 1e-9);
3835    for t in times {
3836        let va = a.evaluate(t);
3837        let vb = b.evaluate(t);
3838        let v  = lerp(va, vb, blend);
3839        result.add_key(t, v, InterpType::Cubic);
3840    }
3841    result
3842}
3843
3844// ============================================================
3845// ANIMATION BAKING FROM CURVE EDITOR
3846// ============================================================
3847
3848pub struct AnimationBaker {
3849    pub source_curves: Vec<FloatCurve>,
3850    pub output_fps: f32,
3851    pub duration:   f64,
3852}
3853
3854impl AnimationBaker {
3855    pub fn new(fps: f32, duration: f64) -> Self {
3856        AnimationBaker { source_curves: Vec::new(), output_fps: fps, duration }
3857    }
3858
3859    pub fn add_curve(&mut self, curve: FloatCurve) {
3860        self.source_curves.push(curve);
3861    }
3862
3863    pub fn bake(&self) -> Vec<Vec<f32>> {
3864        let n_frames = (self.duration * self.output_fps as f64).ceil() as usize + 1;
3865        self.source_curves.iter().map(|curve| {
3866            (0..n_frames).map(|f| {
3867                let t = f as f64 / self.output_fps as f64;
3868                curve.evaluate(t)
3869            }).collect()
3870        }).collect()
3871    }
3872
3873    pub fn bake_to_keyframes(&self, curve_idx: usize, threshold: f32) -> FloatCurve {
3874        let frames = &self.bake()[curve_idx.min(self.source_curves.len().saturating_sub(1))];
3875        let mut result = FloatCurve::new("Baked");
3876        if frames.is_empty() { return result; }
3877        // Keep only frames where value changes significantly
3878        result.add_key(0.0, frames[0], InterpType::Linear);
3879        for i in 1..frames.len() - 1 {
3880            let t = i as f64 / self.output_fps as f64;
3881            let prev = frames[i - 1];
3882            let cur  = frames[i];
3883            let next = frames[i + 1];
3884            // Add keyframe if it deviates from linear interpolation
3885            let expected = lerp(prev, next, 0.5);
3886            if (cur - expected).abs() > threshold {
3887                result.add_key(t, cur, InterpType::Linear);
3888            }
3889        }
3890        let last_t = (frames.len() - 1) as f64 / self.output_fps as f64;
3891        result.add_key(last_t, *frames.last().unwrap(), InterpType::Linear);
3892        result
3893    }
3894}
3895
3896// ============================================================
3897// CINEMATIC DIRECTOR (automatic shot selection)
3898// ============================================================
3899
3900#[derive(Clone, Debug)]
3901pub struct DirectorRule {
3902    pub min_shot_duration: f64,
3903    pub max_shot_duration: f64,
3904    pub prefer_close_cuts: bool,
3905    pub cut_on_action:     bool,
3906    pub cut_on_dialogue:   bool,
3907}
3908
3909impl DirectorRule {
3910    pub fn default_rules() -> Self {
3911        DirectorRule {
3912            min_shot_duration: 2.0,
3913            max_shot_duration: 10.0,
3914            prefer_close_cuts: true,
3915            cut_on_action: true,
3916            cut_on_dialogue: true,
3917        }
3918    }
3919}
3920
3921pub struct CinematicDirector {
3922    pub rules: DirectorRule,
3923    pub available_cameras: Vec<u64>,
3924    pub current_camera_idx: usize,
3925    pub time_since_cut: f64,
3926}
3927
3928impl CinematicDirector {
3929    pub fn new(cameras: Vec<u64>, rules: DirectorRule) -> Self {
3930        CinematicDirector {
3931            rules,
3932            available_cameras: cameras,
3933            current_camera_idx: 0,
3934            time_since_cut: 0.0,
3935        }
3936    }
3937
3938    pub fn update(&mut self, dt: f64, has_action: bool, has_dialogue: bool) -> Option<u64> {
3939        self.time_since_cut += dt;
3940        if self.available_cameras.is_empty() { return None; }
3941        let should_cut = self.should_cut(has_action, has_dialogue);
3942        if should_cut {
3943            self.time_since_cut = 0.0;
3944            self.current_camera_idx = (self.current_camera_idx + 1) % self.available_cameras.len();
3945            Some(self.available_cameras[self.current_camera_idx])
3946        } else {
3947            None
3948        }
3949    }
3950
3951    fn should_cut(&self, has_action: bool, has_dialogue: bool) -> bool {
3952        if self.time_since_cut < self.rules.min_shot_duration { return false; }
3953        if self.time_since_cut >= self.rules.max_shot_duration { return true; }
3954        if self.rules.cut_on_action && has_action { return true; }
3955        if self.rules.cut_on_dialogue && has_dialogue { return true; }
3956        false
3957    }
3958
3959    pub fn current_camera(&self) -> Option<u64> {
3960        self.available_cameras.get(self.current_camera_idx).cloned()
3961    }
3962}
3963
3964// ============================================================
3965// EXTRA CURVE MATH
3966// ============================================================
3967
3968/// Area under a float curve (definite integral)
3969pub fn integrate_curve(curve: &FloatCurve, t_start: f64, t_end: f64, steps: usize) -> f32 {
3970    if steps == 0 || t_end <= t_start { return 0.0; }
3971    let dt = (t_end - t_start) / steps as f64;
3972    let mut sum = 0.0_f32;
3973    for i in 0..steps {
3974        let t0 = t_start + i as f64 * dt;
3975        let t1 = t0 + dt;
3976        sum += (curve.evaluate(t0) + curve.evaluate(t1)) * 0.5 * dt as f32;
3977    }
3978    sum
3979}
3980
3981/// Derivative of a float curve at t (numerical)
3982pub fn curve_derivative(curve: &FloatCurve, t: f64) -> f32 {
3983    let dt = 1e-5;
3984    let a = curve.evaluate(t + dt);
3985    let b = curve.evaluate(t - dt);
3986    (a - b) / (2.0 * dt as f32)
3987}
3988
3989/// Find roots of a float curve (zero crossings)
3990pub fn find_zero_crossings(curve: &FloatCurve, t_start: f64, t_end: f64, steps: usize) -> Vec<f64> {
3991    let mut crossings = Vec::new();
3992    let dt = (t_end - t_start) / steps as f64;
3993    let mut prev_v = curve.evaluate(t_start);
3994    for i in 1..=steps {
3995        let t = t_start + i as f64 * dt;
3996        let v = curve.evaluate(t);
3997        if prev_v * v < 0.0 {
3998            // Bisect
3999            let mut lo = t - dt;
4000            let mut hi = t;
4001            for _ in 0..32 {
4002                let mid = (lo + hi) * 0.5;
4003                let vm = curve.evaluate(mid);
4004                if vm * curve.evaluate(lo) <= 0.0 { hi = mid; } else { lo = mid; }
4005            }
4006            crossings.push((lo + hi) * 0.5);
4007        }
4008        prev_v = v;
4009    }
4010    crossings
4011}
4012
4013/// Find local minima/maxima of a float curve
4014pub fn find_extrema(curve: &FloatCurve, t_start: f64, t_end: f64, steps: usize) -> Vec<(f64, f32, bool)> {
4015    // Returns (time, value, is_max)
4016    let mut extrema = Vec::new();
4017    let dt = (t_end - t_start) / steps as f64;
4018    let mut prev_d = curve_derivative(curve, t_start);
4019    for i in 1..=steps {
4020        let t = t_start + i as f64 * dt;
4021        let d = curve_derivative(curve, t);
4022        if prev_d * d < 0.0 {
4023            let mut lo = t - dt;
4024            let mut hi = t;
4025            for _ in 0..32 {
4026                let mid = (lo + hi) * 0.5;
4027                let dm = curve_derivative(curve, mid);
4028                if dm * curve_derivative(curve, lo) <= 0.0 { hi = mid; } else { lo = mid; }
4029            }
4030            let t_ext = (lo + hi) * 0.5;
4031            let v_ext = curve.evaluate(t_ext);
4032            extrema.push((t_ext, v_ext, prev_d > 0.0));
4033        }
4034        prev_d = d;
4035    }
4036    extrema
4037}
4038
4039// ============================================================
4040// FRAME INTERPOLATION QUALITY METRICS
4041// ============================================================
4042
4043pub struct InterpolationQualityMetrics {
4044    pub max_velocity:     f32,
4045    pub max_acceleration: f32,
4046    pub total_variation:  f32,
4047    pub jitter:           f32,
4048}
4049
4050impl InterpolationQualityMetrics {
4051    pub fn compute(curve: &FloatCurve, t_start: f64, t_end: f64, steps: usize) -> Self {
4052        let dt = (t_end - t_start) / steps as f64;
4053        let vals: Vec<f32> = (0..=steps)
4054            .map(|i| curve.evaluate(t_start + i as f64 * dt))
4055            .collect();
4056        let velocities: Vec<f32> = vals.windows(2)
4057            .map(|w| (w[1] - w[0]) / dt as f32)
4058            .collect();
4059        let accels: Vec<f32> = velocities.windows(2)
4060            .map(|w| (w[1] - w[0]) / dt as f32)
4061            .collect();
4062        let jerks: Vec<f32> = accels.windows(2)
4063            .map(|w| (w[1] - w[0]) / dt as f32)
4064            .collect();
4065        InterpolationQualityMetrics {
4066            max_velocity:     velocities.iter().cloned().map(f32::abs).fold(0.0_f32, f32::max),
4067            max_acceleration: accels.iter().cloned().map(f32::abs).fold(0.0_f32, f32::max),
4068            total_variation:  velocities.iter().cloned().map(f32::abs).sum(),
4069            jitter:           jerks.iter().cloned().map(f32::abs).fold(0.0_f32, f32::max),
4070        }
4071    }
4072}
4073
4074// ============================================================
4075// MOTION PATH EXTRACTION
4076// ============================================================
4077
4078pub fn extract_motion_path(actor_track: &ActorTrack, steps: usize) -> Vec<Vec3> {
4079    if actor_track.keyframes.len() < 2 { return Vec::new(); }
4080    let t_start = actor_track.keyframes.first().unwrap().time;
4081    let t_end   = actor_track.keyframes.last().unwrap().time;
4082    let dt = (t_end - t_start) / steps.max(1) as f64;
4083    (0..=steps).map(|i| {
4084        let t = t_start + i as f64 * dt;
4085        let (pos, _, _) = actor_track.evaluate(t);
4086        pos
4087    }).collect()
4088}
4089
4090pub fn smooth_motion_path(path: &[Vec3], window: usize) -> Vec<Vec3> {
4091    let n = path.len();
4092    if n < 3 || window < 2 { return path.to_vec(); }
4093    let half_w = window / 2;
4094    (0..n).map(|i| {
4095        let start = i.saturating_sub(half_w);
4096        let end   = (i + half_w + 1).min(n);
4097        let sum: Vec3 = path[start..end].iter().cloned().sum();
4098        sum / (end - start) as f32
4099    }).collect()
4100}
4101
4102// ============================================================
4103// SEQUENCE THUMBNAIL DATA
4104// ============================================================
4105
4106#[derive(Clone, Debug)]
4107pub struct SequenceThumbnail {
4108    pub time:   f64,
4109    pub width:  u32,
4110    pub height: u32,
4111    pub pixels: Vec<u8>,  // RGBA
4112}
4113
4114impl SequenceThumbnail {
4115    pub fn placeholder(time: f64, w: u32, h: u32) -> Self {
4116        let n = (w * h * 4) as usize;
4117        let t = (time.fract() * 255.0) as u8;
4118        let pixels = (0..n).map(|i| match i % 4 { 0 => t, 1 => 128, 2 => 255 - t, _ => 255 }).collect();
4119        SequenceThumbnail { time, width: w, height: h, pixels }
4120    }
4121}
4122
4123// ============================================================
4124// FRAME PACING ANALYSIS
4125// ============================================================
4126
4127pub fn analyze_frame_pacing(timestamps: &[f64]) -> FramePacingReport {
4128    let n = timestamps.len();
4129    if n < 2 {
4130        return FramePacingReport { avg_dt: 0.0, std_dev: 0.0, min_dt: 0.0, max_dt: 0.0, jank_frames: 0 };
4131    }
4132    let dts: Vec<f64> = timestamps.windows(2).map(|w| w[1] - w[0]).collect();
4133    let avg = dts.iter().sum::<f64>() / dts.len() as f64;
4134    let variance = dts.iter().map(|&d| (d - avg).powi(2)).sum::<f64>() / dts.len() as f64;
4135    let std_dev  = variance.sqrt();
4136    let min_dt   = dts.iter().cloned().fold(f64::MAX, f64::min);
4137    let max_dt   = dts.iter().cloned().fold(f64::MIN, f64::max);
4138    let jank     = dts.iter().filter(|&&d| d > avg * 1.5).count();
4139    FramePacingReport {
4140        avg_dt:  avg  as f32,
4141        std_dev: std_dev as f32,
4142        min_dt:  min_dt as f32,
4143        max_dt:  max_dt as f32,
4144        jank_frames: jank,
4145    }
4146}
4147
4148#[derive(Clone, Debug)]
4149pub struct FramePacingReport {
4150    pub avg_dt:     f32,
4151    pub std_dev:    f32,
4152    pub min_dt:     f32,
4153    pub max_dt:     f32,
4154    pub jank_frames: usize,
4155}
4156
4157// ============================================================
4158// PROCEDURAL ANIMATION CURVES
4159// ============================================================
4160
4161/// Oscillating spring curve: x(t) = A * e^(-ζωt) * cos(ωd*t + φ)
4162pub fn spring_curve(
4163    time: f32,
4164    initial_value:    f32,
4165    target_value:     f32,
4166    angular_freq:     f32,  // ω₀
4167    damping_ratio:    f32,  // ζ
4168) -> f32 {
4169    let delta = initial_value - target_value;
4170    let wd = angular_freq * (1.0 - damping_ratio * damping_ratio).max(0.0).sqrt();
4171    let decay = (-damping_ratio * angular_freq * time).exp();
4172    if wd < EPSILON {
4173        // Critically or overdamped
4174        let b = delta * (1.0 + damping_ratio * angular_freq * time);
4175        target_value + b * decay
4176    } else {
4177        let phase = 0.0_f32; // initial velocity = 0
4178        target_value + delta * decay * (wd * time + phase).cos()
4179    }
4180}
4181
4182/// Elastic bounce-back curve
4183pub fn elastic_out(t: f32, amplitude: f32, period: f32) -> f32 {
4184    let t = clamp01(t);
4185    if t <= 0.0 { return 0.0; }
4186    if t >= 1.0 { return 1.0; }
4187    let p = period;
4188    let a = amplitude.max(1.0);
4189    let s = (a / (2.0 * std::f32::consts::PI)) * (1.0_f32 / a).asin();
4190    a * 2.0_f32.powf(-10.0 * t)
4191        * ((t - s) * (2.0 * std::f32::consts::PI) / p).sin()
4192        + 1.0
4193}
4194
4195/// Back easing (overshoot)
4196pub fn ease_out_back(t: f32, overshoot: f32) -> f32 {
4197    let t = clamp01(t);
4198    let t1 = t - 1.0;
4199    t1 * t1 * ((overshoot + 1.0) * t1 + overshoot) + 1.0
4200}
4201
4202/// Bounce easing
4203pub fn ease_out_bounce(t: f32) -> f32 {
4204    let t = clamp01(t);
4205    if t < 1.0 / 2.75 {
4206        7.5625 * t * t
4207    } else if t < 2.0 / 2.75 {
4208        let t2 = t - 1.5 / 2.75;
4209        7.5625 * t2 * t2 + 0.75
4210    } else if t < 2.5 / 2.75 {
4211        let t2 = t - 2.25 / 2.75;
4212        7.5625 * t2 * t2 + 0.9375
4213    } else {
4214        let t2 = t - 2.625 / 2.75;
4215        7.5625 * t2 * t2 + 0.984375
4216    }
4217}
4218
4219// ============================================================
4220// KEYFRAME REDUCTION (LOD for animations)
4221// ============================================================
4222
4223pub fn reduce_keyframes(curve: &FloatCurve, max_error: f32) -> FloatCurve {
4224    if curve.keys.len() < 3 { return curve.keys.iter().map(|k| Keyframe::new(k.time, k.value)).collect::<Vec<_>>().into_iter().fold(FloatCurve::new(&curve.name), |mut c, k| { c.keys.push(k); c }); }
4225    let times:  Vec<f64> = curve.keys.iter().map(|k| k.time).collect();
4226    let values: Vec<f32> = curve.keys.iter().map(|k| k.value).collect();
4227    // Douglas-Peucker style reduction on (time, value) pairs
4228    let keep = rdp_reduce(&times, &values, max_error as f64);
4229    let mut result = FloatCurve::new(&curve.name);
4230    for i in keep {
4231        result.add_key(times[i], values[i], InterpType::Cubic);
4232    }
4233    result
4234}
4235
4236fn rdp_reduce(times: &[f64], values: &[f32], epsilon: f64) -> Vec<usize> {
4237    let n = times.len();
4238    if n < 3 { return (0..n).collect(); }
4239    let mut max_dist = 0.0_f64;
4240    let mut max_idx  = 0usize;
4241    let t0 = times[0]; let v0 = values[0] as f64;
4242    let tn = times[n-1]; let vn = values[n-1] as f64;
4243    for i in 1..n-1 {
4244        let t = times[i]; let v = values[i] as f64;
4245        // Perpendicular distance from point to line (t0,v0)-(tn,vn)
4246        let num = ((vn-v0)*(t0-t) - (tn-t0)*(v0-v)).abs();
4247        let den = ((vn-v0).powi(2) + (tn-t0).powi(2)).sqrt();
4248        let d   = if den < 1e-12 { 0.0 } else { num / den };
4249        if d > max_dist { max_dist = d; max_idx = i; }
4250    }
4251    if max_dist > epsilon {
4252        let mut left  = rdp_reduce(&times[..=max_idx], &values[..=max_idx], epsilon);
4253        let right_raw = rdp_reduce(&times[max_idx..], &values[max_idx..], epsilon);
4254        let right: Vec<usize> = right_raw.iter().map(|&i| i + max_idx).collect();
4255        left.pop(); // remove duplicate
4256        left.extend(right);
4257        left
4258    } else {
4259        vec![0, n-1]
4260    }
4261}
4262
4263// ============================================================
4264// UNIT TESTS
4265// ============================================================
4266
4267#[cfg(test)]
4268mod tests {
4269    use super::*;
4270
4271    #[test]
4272    fn test_timecode_roundtrip() {
4273        let tc = Timecode::new(1, 23, 45, 12);
4274        let frame = tc.to_frame(30.0);
4275        let tc2 = Timecode::from_frame(frame, 30.0);
4276        assert_eq!(tc.hours,   tc2.hours);
4277        assert_eq!(tc.minutes, tc2.minutes);
4278        assert_eq!(tc.seconds, tc2.seconds);
4279        assert_eq!(tc.frames,  tc2.frames);
4280    }
4281
4282    #[test]
4283    fn test_float_curve_linear() {
4284        let mut curve = FloatCurve::new("test");
4285        curve.add_key(0.0, 0.0, InterpType::Linear);
4286        curve.add_key(1.0, 1.0, InterpType::Linear);
4287        let v05 = curve.evaluate(0.5);
4288        assert!((v05 - 0.5).abs() < 0.001, "Linear interp mid should be 0.5");
4289    }
4290
4291    #[test]
4292    fn test_float_curve_constant() {
4293        let mut curve = FloatCurve::new("test");
4294        curve.add_key(0.0, 3.0, InterpType::Constant);
4295        curve.add_key(1.0, 7.0, InterpType::Constant);
4296        let v = curve.evaluate(0.5);
4297        assert!((v - 3.0).abs() < EPSILON, "Constant interp should return first value");
4298    }
4299
4300    #[test]
4301    fn test_catmull_rom_symmetry() {
4302        let v = catmull_rom_4pt(0.0, 1.0, 1.0, 0.0, 0.5);
4303        assert!(v > 0.9, "CR midpoint of plateau should stay near 1.0");
4304    }
4305
4306    #[test]
4307    fn test_camera_track_evaluate() {
4308        let mut track = CameraTrack::new(1, "Cam");
4309        track.add_keyframe(CameraKeyframe::new(0.0, Vec3::ZERO, Quat::IDENTITY));
4310        track.add_keyframe(CameraKeyframe::new(1.0, Vec3::X * 10.0, Quat::IDENTITY));
4311        let mid = track.evaluate_position(0.5);
4312        assert!((mid.x - 5.0).abs() < 0.1, "Camera should be at x=5 at t=0.5");
4313    }
4314
4315    #[test]
4316    fn test_actor_track_evaluate() {
4317        let mut track = ActorTrack::new(1, "Actor", 42);
4318        track.add_keyframe(ActorKeyframe::new(0.0, Vec3::ZERO, Quat::IDENTITY));
4319        track.add_keyframe(ActorKeyframe::new(2.0, Vec3::new(10.0, 0.0, 0.0), Quat::IDENTITY));
4320        let (pos, _, _) = track.evaluate(1.0);
4321        assert!((pos.x - 5.0).abs() < 0.1);
4322    }
4323
4324    #[test]
4325    fn test_frame_rate_conversion() {
4326        let frame_24 = 24u64;
4327        let frame_30 = FrameRate::convert_frame(frame_24, FrameRate::Fps24, FrameRate::Fps30);
4328        assert_eq!(frame_30, 30);
4329    }
4330
4331    #[test]
4332    fn test_timecode_srt_format() {
4333        let s = secs_to_srt_tc(3723.5);
4334        assert_eq!(s, "01:02:03,500", "SRT format mismatch: got {}", s);
4335    }
4336
4337    #[test]
4338    fn test_blend_shape_evaluate() {
4339        let mut track = BlendShapeTrack::new(1, "Morph", 10);
4340        track.add_channel("smile");
4341        let mut kf0 = BlendShapeKeyframe::new(0.0).set_weight("smile", 0.0);
4342        let mut kf1 = BlendShapeKeyframe::new(1.0).set_weight("smile", 1.0);
4343        track.add_keyframe(kf0);
4344        track.add_keyframe(kf1);
4345        let weights = track.evaluate(0.5);
4346        let smile = weights.get("smile").cloned().unwrap_or(0.0);
4347        assert!((smile - 0.5).abs() < 0.1, "Blend shape at 0.5 should be ~0.5");
4348    }
4349
4350    #[test]
4351    fn test_visibility_track() {
4352        let mut track = VisibilityTrack::new(1, "Vis", 5);
4353        track.add_keyframe(VisibilityKeyframe::new(0.0, true));
4354        track.add_keyframe(VisibilityKeyframe { time: 1.0, visible: false, opacity: 0.0, fade: 0.5 });
4355        assert!(track.is_visible_at(0.1));
4356    }
4357
4358    #[test]
4359    fn test_time_dilation() {
4360        let mut track = TimeDilationTrack::new(1, "TD");
4361        track.add_keyframe(TimeDilationKeyframe::new(0.0, 0.5));
4362        track.add_keyframe(TimeDilationKeyframe::new(2.0, 1.0));
4363        let scale_at_0 = track.evaluate(0.0);
4364        assert!((scale_at_0 - 0.5).abs() < 0.01);
4365        let scale_at_2 = track.evaluate(2.0);
4366        assert!((scale_at_2 - 1.0).abs() < 0.01);
4367    }
4368
4369    #[test]
4370    fn test_sequencer_create_and_update() {
4371        let mut seq = CinematicSequencer::new("Test", 10.0, FrameRate::Fps30);
4372        let cam_id = seq.add_camera_track("MainCam");
4373        let actor_id = seq.add_actor_track("Hero", 1);
4374        assert_eq!(seq.tracks.track_count(), 2);
4375        seq.play();
4376        for _ in 0..30 {
4377            seq.update(1.0 / 30.0);
4378        }
4379        assert!(seq.playback.current_time > 0.9);
4380    }
4381
4382    #[test]
4383    fn test_curve_cycle_infinity() {
4384        let mut curve = FloatCurve::new("cyclic");
4385        curve.add_key(0.0, 0.0, InterpType::Linear);
4386        curve.add_key(1.0, 1.0, InterpType::Linear);
4387        curve.post_infinity = InfinityMode::Cycle;
4388        let v = curve.evaluate(1.5);
4389        assert!((v - 0.5).abs() < 0.01, "Cyclic: t=1.5 should map to t=0.5 within [0,1]");
4390    }
4391
4392    #[test]
4393    fn test_spring_curve_approaches_target() {
4394        let v_final = spring_curve(10.0, 0.0, 1.0, 10.0, 0.7);
4395        assert!((v_final - 1.0).abs() < 0.01, "Spring should converge to target");
4396    }
4397
4398    #[test]
4399    fn test_edl_generation() {
4400        let mut seq = CinematicSequencer::new("MovieSeq", 30.0, FrameRate::Fps24);
4401        seq.add_shot("Scene01", 0.0,  5.0, 1);
4402        seq.add_shot("Scene02", 5.0, 12.0, 2);
4403        seq.add_shot("Scene03", 12.0, 30.0, 3);
4404        let edl = seq.export_edl();
4405        assert_eq!(edl.entries.len(), 3);
4406        let edl_str = edl.to_string();
4407        assert!(edl_str.contains("TITLE:"));
4408        assert!(edl_str.contains("001"));
4409    }
4410
4411    #[test]
4412    fn test_undo_redo() {
4413        let mut seq = CinematicSequencer::new("UndoTest", 10.0, FrameRate::Fps30);
4414        seq.add_camera_track("Cam1");
4415        let initial_count = seq.tracks.track_count();
4416        seq.undo(); // undo AddTrack
4417        assert_eq!(seq.tracks.track_count(), initial_count - 1);
4418        seq.redo(); // redo AddTrack
4419        assert_eq!(seq.tracks.track_count(), initial_count);
4420    }
4421
4422    #[test]
4423    fn test_audio_beat_generation() {
4424        let mut track = AudioTrack::new(1, "Music");
4425        track.generate_beat_markers(120.0, 0.0, 4.0, 4);
4426        // At 120 BPM, beat every 0.5s, 4s = 8 beats
4427        assert_eq!(track.beat_markers.len(), 8);
4428        assert!(track.beat_markers[0].is_downbeat);
4429        assert!(!track.beat_markers[1].is_downbeat);
4430    }
4431
4432    #[test]
4433    fn test_subtitle_srt_export() {
4434        let mut seq = CinematicSequencer::new("SubTest", 10.0, FrameRate::Fps25);
4435        let tid = seq.add_subtitle_track("EN");
4436        seq.add_subtitle(tid, SubtitleKeyframe::new(1.0, 3.0, "Hello world"));
4437        seq.add_subtitle(tid, SubtitleKeyframe::new(4.0, 6.0, "Goodbye world"));
4438        let srt = seq.export_subtitles_srt();
4439        assert!(srt.contains("Hello world"));
4440        assert!(srt.contains("Goodbye world"));
4441        assert!(srt.contains("-->"));
4442    }
4443}
4444
4445// ============================================================
4446// SEQUENCER TIMELINE VIEW STATE
4447// ============================================================
4448
4449#[derive(Clone, Debug)]
4450pub struct TimelineViewState {
4451    pub view_start:    f64,    // seconds
4452    pub view_end:      f64,
4453    pub scroll_y:      f32,
4454    pub track_heights: HashMap<u64, f32>,
4455    pub zoom_level:    f32,
4456    pub snap_mode:     SnapMode,
4457    pub show_waveforms: bool,
4458    pub show_thumbnails: bool,
4459    pub collapsed_groups: HashSet<u64>,
4460}
4461
4462#[derive(Clone, Debug, PartialEq)]
4463pub enum SnapMode {
4464    None,
4465    Frames,
4466    Seconds,
4467    BeatGrid(f32), // BPM
4468    Custom(f64),
4469}
4470
4471impl TimelineViewState {
4472    pub fn new(duration: f64) -> Self {
4473        TimelineViewState {
4474            view_start: 0.0,
4475            view_end:   duration.min(30.0),
4476            scroll_y:   0.0,
4477            track_heights: HashMap::new(),
4478            zoom_level: 1.0,
4479            snap_mode:  SnapMode::Frames,
4480            show_waveforms: true,
4481            show_thumbnails: false,
4482            collapsed_groups: HashSet::new(),
4483        }
4484    }
4485
4486    pub fn time_to_screen_x(&self, time: f64, screen_w: f32) -> f32 {
4487        let frac = (time - self.view_start) / (self.view_end - self.view_start).max(1e-9);
4488        frac as f32 * screen_w
4489    }
4490
4491    pub fn screen_x_to_time(&self, x: f32, screen_w: f32) -> f64 {
4492        let frac = x as f64 / screen_w as f64;
4493        self.view_start + frac * (self.view_end - self.view_start)
4494    }
4495
4496    pub fn snap_time(&self, time: f64, fps: f32) -> f64 {
4497        match self.snap_mode {
4498            SnapMode::None       => time,
4499            SnapMode::Frames     => (time * fps as f64).round() / fps as f64,
4500            SnapMode::Seconds    => time.round(),
4501            SnapMode::BeatGrid(bpm) => {
4502                let beat = 60.0 / bpm as f64;
4503                (time / beat).round() * beat
4504            }
4505            SnapMode::Custom(interval) => (time / interval).round() * interval,
4506        }
4507    }
4508
4509    pub fn zoom_in(&mut self, center: f64, factor: f32) {
4510        let range = self.view_end - self.view_start;
4511        let new_range = range / factor as f64;
4512        self.view_start = center - new_range * 0.5;
4513        self.view_end   = center + new_range * 0.5;
4514        self.view_start = self.view_start.max(0.0);
4515    }
4516
4517    pub fn zoom_out(&mut self, center: f64, factor: f32, duration: f64) {
4518        let range = self.view_end - self.view_start;
4519        let new_range = (range * factor as f64).min(duration * 1.1);
4520        self.view_start = (center - new_range * 0.5).max(0.0);
4521        self.view_end   = self.view_start + new_range;
4522    }
4523
4524    pub fn pan(&mut self, delta_time: f64, duration: f64) {
4525        self.view_start = (self.view_start + delta_time).max(0.0);
4526        self.view_end   = self.view_start + (self.view_end - self.view_start);
4527        if self.view_end > duration { self.view_end = duration; self.view_start = self.view_end - (self.view_end - self.view_start); }
4528    }
4529
4530    pub fn track_height(&self, track_id: u64) -> f32 {
4531        self.track_heights.get(&track_id).cloned().unwrap_or(32.0)
4532    }
4533
4534    pub fn visible_time_range(&self) -> (f64, f64) {
4535        (self.view_start, self.view_end)
4536    }
4537}
4538
4539// ============================================================
4540// TRACK GROUP
4541// ============================================================
4542
4543#[derive(Clone, Debug)]
4544pub struct TrackGroup {
4545    pub id:       u64,
4546    pub name:     String,
4547    pub color:    Vec4,
4548    pub track_ids: Vec<u64>,
4549    pub collapsed: bool,
4550    pub muted:    bool,
4551    pub solo:     bool,
4552}
4553
4554impl TrackGroup {
4555    pub fn new(id: u64, name: &str) -> Self {
4556        TrackGroup {
4557            id, name: name.to_string(),
4558            color: Vec4::new(0.5, 0.5, 1.0, 1.0),
4559            track_ids: Vec::new(),
4560            collapsed: false,
4561            muted: false,
4562            solo: false,
4563        }
4564    }
4565
4566    pub fn add_track(&mut self, id: u64) {
4567        if !self.track_ids.contains(&id) { self.track_ids.push(id); }
4568    }
4569
4570    pub fn remove_track(&mut self, id: u64) {
4571        self.track_ids.retain(|&tid| tid != id);
4572    }
4573}
4574
4575// ============================================================
4576// SEQUENCE LOCATOR (find things by time)
4577// ============================================================
4578
4579pub struct SequenceLocator;
4580
4581impl SequenceLocator {
4582    pub fn find_camera_keyframes_in_range(
4583        track: &CameraTrack,
4584        t_start: f64,
4585        t_end: f64,
4586    ) -> Vec<usize> {
4587        track.keyframes.iter().enumerate()
4588            .filter(|(_, k)| k.time >= t_start && k.time <= t_end)
4589            .map(|(i, _)| i)
4590            .collect()
4591    }
4592
4593    pub fn find_events_in_range(
4594        track: &EventTrack,
4595        t_start: f64,
4596        t_end: f64,
4597    ) -> Vec<usize> {
4598        track.events.iter().enumerate()
4599            .filter(|(_, e)| e.time >= t_start && e.time <= t_end)
4600            .map(|(i, _)| i)
4601            .collect()
4602    }
4603
4604    pub fn find_subtitles_overlapping(
4605        track: &SubtitleTrack,
4606        t_start: f64,
4607        t_end: f64,
4608    ) -> Vec<usize> {
4609        track.subtitles.iter().enumerate()
4610            .filter(|(_, s)| s.time < t_end && s.end_time > t_start)
4611            .map(|(i, _)| i)
4612            .collect()
4613    }
4614}
4615
4616// ============================================================
4617// FLOAT CURVE BATCH OPERATIONS
4618// ============================================================
4619
4620pub fn mirror_curve_time(curve: &mut FloatCurve, pivot: f64) {
4621    for k in &mut curve.keys { k.time = 2.0 * pivot - k.time; }
4622    curve.keys.sort_by(|a, b| a.time.partial_cmp(&b.time).unwrap_or(std::cmp::Ordering::Equal));
4623}
4624
4625pub fn reverse_curve(curve: &mut FloatCurve) {
4626    if curve.keys.len() < 2 { return; }
4627    let t0 = curve.keys.first().unwrap().time;
4628    let t1 = curve.keys.last().unwrap().time;
4629    for k in &mut curve.keys { k.time = t0 + t1 - k.time; }
4630    curve.keys.sort_by(|a, b| a.time.partial_cmp(&b.time).unwrap_or(std::cmp::Ordering::Equal));
4631    for k in &mut curve.keys {
4632        if let Some(h) = &mut k.bezier_handle {
4633            let tmp = h.in_tangent;
4634            h.in_tangent  = Vec2::new(-h.out_tangent.x, h.out_tangent.y);
4635            h.out_tangent = Vec2::new(-tmp.x, tmp.y);
4636        }
4637    }
4638}
4639
4640pub fn scale_curve_values(curve: &mut FloatCurve, scale: f32) {
4641    for k in &mut curve.keys {
4642        k.value *= scale;
4643        if let Some(h) = &mut k.bezier_handle {
4644            h.in_tangent.y  *= scale;
4645            h.out_tangent.y *= scale;
4646        }
4647    }
4648}
4649
4650pub fn offset_curve_times(curve: &mut FloatCurve, offset: f64) {
4651    for k in &mut curve.keys { k.time += offset; }
4652}
4653
4654pub fn clamp_curve_values(curve: &mut FloatCurve, min: f32, max: f32) {
4655    for k in &mut curve.keys { k.value = k.value.clamp(min, max); }
4656}
4657
4658pub fn snap_curve_times(curve: &mut FloatCurve, interval: f64) {
4659    for k in &mut curve.keys { k.time = (k.time / interval).round() * interval; }
4660}
4661
4662
4663// ============================================================
4664// ADDITIONAL UNIT TESTS
4665// ============================================================
4666
4667#[cfg(test)]
4668mod tests_extended {
4669    use super::*;
4670
4671    #[test]
4672    fn test_float_curve_bezier_endpoints() {
4673        let mut curve = FloatCurve::new("bezier");
4674        curve.add_key_bezier(0.0, 0.0, BezierHandle::flat());
4675        curve.add_key_bezier(1.0, 1.0, BezierHandle::flat());
4676        let v0 = curve.evaluate(0.0);
4677        let v1 = curve.evaluate(1.0);
4678        assert!((v0 - 0.0).abs() < 0.001);
4679        assert!((v1 - 1.0).abs() < 0.001);
4680    }
4681
4682    #[test]
4683    fn test_post_fx_blending() {
4684        let mut seq = CinematicSequencer::new("PFX", 5.0, FrameRate::Fps30);
4685        let pfx_id = seq.add_post_fx_track("GlobalPFX");
4686        if let Some(track) = seq.tracks.post_fx_tracks.get_mut(&pfx_id) {
4687            track.add_keyframe(PostFxKeyframe { vignette: 0.0, ..PostFxKeyframe::default_at(0.0) });
4688            track.add_keyframe(PostFxKeyframe { vignette: 1.0, ..PostFxKeyframe::default_at(5.0) });
4689        }
4690        let pfx = seq.tracks.post_fx_tracks[&pfx_id].evaluate(2.5);
4691        assert!(pfx.vignette > 0.4 && pfx.vignette < 0.6, "PFX midpoint vignette ~ 0.5");
4692    }
4693
4694    #[test]
4695    fn test_blend_camera_matrix() {
4696        let mut seq = CinematicSequencer::new("BlendCam", 10.0, FrameRate::Fps30);
4697        let cam_a = seq.add_camera_track("CamA");
4698        let cam_b = seq.add_camera_track("CamB");
4699        {
4700            let track = seq.tracks.camera_tracks.get_mut(&cam_a).unwrap();
4701            track.add_keyframe(CameraKeyframe::new(0.0, Vec3::ZERO, Quat::IDENTITY));
4702        }
4703        {
4704            let track = seq.tracks.camera_tracks.get_mut(&cam_b).unwrap();
4705            track.add_keyframe(CameraKeyframe::new(0.0, Vec3::X * 10.0, Quat::IDENTITY));
4706        }
4707        seq.cut_to_camera(cam_a);
4708        seq.blend_to_camera(cam_b, 1.0);
4709        seq.camera_blend_t = 0.5;
4710        let mat = seq.blended_camera_matrix(0.0);
4711        // Position should be approximately midpoint
4712        let pos = Vec3::new(mat.w_axis.x, mat.w_axis.y, mat.w_axis.z);
4713        assert!(pos.x > 2.0 && pos.x < 8.0, "Blended camera X should be between 0 and 10");
4714    }
4715
4716    #[test]
4717    fn test_edl_cmx_format() {
4718        let edl = EdlDocument::new("TestEDL", FrameRate::Fps24);
4719        let s = edl.to_string();
4720        assert!(s.starts_with("TITLE: TestEDL"));
4721        assert!(s.contains("FCM:"));
4722    }
4723
4724    #[test]
4725    fn test_light_temperature_rgb() {
4726        let rgb_daylight = LightKeyframe::temperature_to_rgb(6500.0);
4727        let rgb_candle   = LightKeyframe::temperature_to_rgb(1900.0);
4728        // Daylight should be close to white
4729        assert!(rgb_daylight.x > 0.8);
4730        // Candlelight should be very orange (red > blue)
4731        assert!(rgb_candle.x > rgb_candle.z, "Candle: red > blue");
4732    }
4733
4734    #[test]
4735    fn test_visibility_opacity_fade() {
4736        let mut track = VisibilityTrack::new(1, "V", 10);
4737        track.add_keyframe(VisibilityKeyframe { time: 0.0, visible: true,  opacity: 1.0, fade: 0.0 });
4738        track.add_keyframe(VisibilityKeyframe { time: 2.0, visible: false, opacity: 0.0, fade: 1.0 });
4739        let op_at_0 = track.evaluate_opacity(0.0);
4740        assert!((op_at_0 - 1.0).abs() < 0.01);
4741    }
4742
4743    #[test]
4744    fn test_playback_controller_loop() {
4745        let mut pb = PlaybackController::new(FrameRate::Fps30);
4746        pb.loop_enabled = true;
4747        pb.loop_start = 0.0;
4748        pb.loop_end   = 1.0;
4749        pb.play();
4750        for _ in 0..60 { pb.update(1.0/30.0, 5.0); }
4751        // After 2 seconds with 1s loop, should have wrapped
4752        assert!(pb.current_time < 1.0 + 0.1);
4753    }
4754
4755    #[test]
4756    fn test_audio_sidechain_duck() {
4757        let mut track = AudioTrack::new(1, "Music");
4758        let clip = AudioClipData::new(1, "kick", 4.0, 44100);
4759        let mut kf = AudioKeyframe::new(0.0, clip);
4760        kf.duck_others = true;
4761        kf.duck_amount = 0.5;
4762        kf.duck_release = 0.5;
4763        track.add_clip(kf);
4764        let duck = track.sidechain_duck_factor_at(1.0);
4765        assert!(duck < 1.0, "Sidechain should reduce volume");
4766    }
4767
4768    #[test]
4769    fn test_spring_converges() {
4770        let v = spring_curve(5.0, 0.0, 10.0, 10.0, 0.7);
4771        assert!((v - 10.0).abs() < 0.5, "Spring should approach target");
4772    }
4773
4774    #[test]
4775    fn test_ease_out_bounce_endpoints() {
4776        assert!((ease_out_bounce(0.0) - 0.0).abs() < 0.001);
4777        assert!((ease_out_bounce(1.0) - 1.0).abs() < 0.001);
4778    }
4779
4780    #[test]
4781    fn test_dof_hyperfocal() {
4782        let dof = DepthOfFieldKeyframe::new(0.0);
4783        let hf  = dof.hyperfocal(0.029);
4784        assert!(hf > 0.0, "Hyperfocal distance must be positive");
4785    }
4786
4787    #[test]
4788    fn test_lens_distortion_identity() {
4789        let ld = LensDistortion::none();
4790        let uv = Vec2::new(0.5, 0.5);
4791        let distorted = ld.distort_uv(uv);
4792        assert!((distorted - uv).length() < 0.001, "Zero distortion should leave UV unchanged");
4793    }
4794
4795    #[test]
4796    fn test_curve_integration_trapezoid() {
4797        let mut c = FloatCurve::new("const");
4798        c.add_key(0.0, 2.0, InterpType::Linear);
4799        c.add_key(5.0, 2.0, InterpType::Linear);
4800        let area = integrate_curve(&c, 0.0, 5.0, 100);
4801        assert!((area - 10.0).abs() < 0.1, "Area under constant 2 over [0,5] should be 10");
4802    }
4803}
4804
4805// ============================================================
4806// SEQUENCE GRAPH (branching / non-linear)
4807// ============================================================
4808
4809#[derive(Clone, Debug)]
4810pub struct SequenceNode {
4811    pub id:        u64,
4812    pub name:      String,
4813    pub sequence:  String,   // sequence name / ID
4814    pub duration:  f64,
4815}
4816
4817#[derive(Clone, Debug)]
4818pub struct SequenceEdge {
4819    pub from_id:   u64,
4820    pub to_id:     u64,
4821    pub condition: String,   // "always" | "if_flag:X" | "on_choice:N"
4822    pub weight:    f32,
4823}
4824
4825pub struct SequenceGraph {
4826    pub nodes:      HashMap<u64, SequenceNode>,
4827    pub edges:      Vec<SequenceEdge>,
4828    pub start_node: u64,
4829    pub current:    u64,
4830    pub flags:      HashSet<String>,
4831    next_id:        u64,
4832}
4833
4834impl SequenceGraph {
4835    pub fn new() -> Self {
4836        SequenceGraph {
4837            nodes: HashMap::new(),
4838            edges: Vec::new(),
4839            start_node: 0,
4840            current: 0,
4841            flags: HashSet::new(),
4842            next_id: 1,
4843        }
4844    }
4845
4846    pub fn add_node(&mut self, name: &str, sequence: &str, duration: f64) -> u64 {
4847        let id = self.next_id; self.next_id += 1;
4848        self.nodes.insert(id, SequenceNode { id, name: name.to_string(), sequence: sequence.to_string(), duration });
4849        id
4850    }
4851
4852    pub fn add_edge(&mut self, from_id: u64, to_id: u64, condition: &str, weight: f32) {
4853        self.edges.push(SequenceEdge { from_id, to_id, condition: condition.to_string(), weight });
4854    }
4855
4856    pub fn set_flag(&mut self, flag: &str) { self.flags.insert(flag.to_string()); }
4857    pub fn clear_flag(&mut self, flag: &str) { self.flags.remove(flag); }
4858
4859    /// Evaluate condition string against current flag set.
4860    pub fn condition_met(&self, condition: &str) -> bool {
4861        if condition == "always" { return true; }
4862        if let Some(flag) = condition.strip_prefix("if_flag:") {
4863            return self.flags.contains(flag);
4864        }
4865        false
4866    }
4867
4868    /// Get all reachable next nodes from `current`.
4869    pub fn next_nodes(&self) -> Vec<u64> {
4870        self.edges.iter()
4871            .filter(|e| e.from_id == self.current && self.condition_met(&e.condition))
4872            .map(|e| e.to_id)
4873            .collect()
4874    }
4875
4876    /// Advance to best-matching next node.
4877    pub fn advance(&mut self) -> Option<&SequenceNode> {
4878        let nexts = self.next_nodes();
4879        if nexts.is_empty() { return None; }
4880        // Pick highest-weight edge
4881        let best = self.edges.iter()
4882            .filter(|e| e.from_id == self.current && nexts.contains(&e.to_id))
4883            .max_by(|a, b| a.weight.partial_cmp(&b.weight).unwrap_or(std::cmp::Ordering::Equal))?;
4884        self.current = best.to_id;
4885        self.nodes.get(&self.current)
4886    }
4887
4888    pub fn current_node(&self) -> Option<&SequenceNode> { self.nodes.get(&self.current) }
4889}
4890
4891// ============================================================
4892// CAMERA SHAKE PRESET LIBRARY
4893// ============================================================
4894
4895#[derive(Clone, Debug)]
4896pub struct ShakePreset {
4897    pub name:      String,
4898    pub trauma:    f32,
4899    pub frequency: f32,
4900    pub decay:     f32,
4901}
4902
4903pub struct ShakePresetLibrary {
4904    pub presets: HashMap<String, ShakePreset>,
4905}
4906
4907impl ShakePresetLibrary {
4908    pub fn new() -> Self {
4909        let mut lib = ShakePresetLibrary { presets: HashMap::new() };
4910        lib.add("gunshot",    0.6, 20.0, 4.0);
4911        lib.add("explosion",  1.0, 12.0, 2.5);
4912        lib.add("earthquake", 0.8, 6.0,  1.0);
4913        lib.add("footstep",   0.2, 30.0, 8.0);
4914        lib.add("engine",     0.1, 60.0, 20.0);
4915        lib
4916    }
4917
4918    fn add(&mut self, name: &str, trauma: f32, frequency: f32, decay: f32) {
4919        let preset = ShakePreset { name: name.to_string(), trauma, frequency, decay };
4920        self.presets.insert(name.to_string(), preset);
4921    }
4922
4923    pub fn get(&self, name: &str) -> Option<&ShakePreset> { self.presets.get(name) }
4924
4925    pub fn apply(&self, name: &str, state: &mut CameraShakeState) {
4926        if let Some(p) = self.get(name) {
4927            state.add_trauma(p.trauma);
4928        }
4929    }
4930}
4931
4932// ============================================================
4933// KEYFRAME INTERPOLATION BENCHMARK
4934// ============================================================
4935
4936pub struct InterpBenchResult {
4937    pub curve_name:    String,
4938    pub samples:       usize,
4939    pub eval_count:    usize,
4940    pub mean_error:    f32,
4941    pub max_error:     f32,
4942}
4943
4944impl InterpBenchResult {
4945    /// Compare a FloatCurve against a reference function `f`.
4946    pub fn measure(curve: &FloatCurve, f: &dyn Fn(f64) -> f32, t_start: f64, t_end: f64, steps: usize) -> Self {
4947        let mut sum_err = 0.0f32;
4948        let mut max_err = 0.0f32;
4949        for i in 0..steps {
4950            let t   = t_start + (t_end - t_start) * i as f64 / steps as f64;
4951            let got = curve.evaluate(t);
4952            let exp = f(t);
4953            let e   = (got - exp).abs();
4954            sum_err += e;
4955            if e > max_err { max_err = e; }
4956        }
4957        InterpBenchResult {
4958            curve_name: curve.name.clone(),
4959            samples:    curve.keys.len(),
4960            eval_count: steps,
4961            mean_error: sum_err / steps as f32,
4962            max_error:  max_err,
4963        }
4964    }
4965
4966    pub fn summary(&self) -> String {
4967        format!("{}: {} keys, mean_err={:.6}, max_err={:.6}",
4968            self.curve_name, self.samples, self.mean_error, self.max_error)
4969    }
4970}
4971
4972// ============================================================
4973// SEQUENCE STATISTICS
4974// ============================================================
4975
4976pub struct SequenceStats {
4977    pub total_duration:    f64,
4978    pub track_count:       usize,
4979    pub keyframe_count:    usize,
4980    pub shot_count:        usize,
4981    pub cut_count:         usize,
4982    pub blend_count:       usize,
4983    pub audio_track_count: usize,
4984    pub subtitle_count:    usize,
4985}
4986
4987impl SequenceStats {
4988    pub fn compute(seq: &CinematicSequencer) -> Self {
4989        let tc      = &seq.tracks;
4990        let kf      = tc.camera_tracks.values().map(|t| t.keyframes.len()).sum::<usize>()
4991                    + tc.actor_tracks.values().map(|t| t.keyframes.len()).sum::<usize>()
4992                    + tc.animation_tracks.values().map(|t| t.clips.len()).sum::<usize>()
4993                    + tc.audio_tracks.values().map(|t| t.clips.len()).sum::<usize>()
4994                    + tc.light_tracks.values().map(|t| t.keyframes.len()).sum::<usize>()
4995                    + tc.post_fx_tracks.values().map(|t| t.keyframes.len()).sum::<usize>()
4996                    + tc.subtitle_tracks.values().map(|t| t.entries.len()).sum::<usize>();
4997        let shot_count  = seq.shot_list.shots.len();
4998        let cut_count   = seq.shot_list.shots.iter().filter(|s| s.transition == CutType::Cut).count();
4999        let blend_count = shot_count - cut_count;
5000        let audio_count = tc.audio_tracks.len();
5001        let sub_count   = tc.subtitle_tracks.values().map(|t| t.entries.len()).sum::<usize>();
5002        let total_tracks = tc.camera_tracks.len() + tc.actor_tracks.len()
5003            + tc.animation_tracks.len() + tc.audio_tracks.len()
5004            + tc.light_tracks.len() + tc.post_fx_tracks.len()
5005            + tc.subtitle_tracks.len() + tc.event_tracks.len();
5006
5007        SequenceStats {
5008            total_duration:    seq.master_sequence.duration,
5009            track_count:       total_tracks,
5010            keyframe_count:    kf,
5011            shot_count,
5012            cut_count,
5013            blend_count,
5014            audio_track_count: audio_count,
5015            subtitle_count:    sub_count,
5016        }
5017    }
5018
5019    pub fn summary(&self) -> String {
5020        format!(
5021            "Duration: {:.2}s | Tracks: {} | Keyframes: {} | Shots: {} (cuts: {}, blends: {}) | Audio: {} | Subs: {}",
5022            self.total_duration, self.track_count, self.keyframe_count,
5023            self.shot_count, self.cut_count, self.blend_count,
5024            self.audio_track_count, self.subtitle_count
5025        )
5026    }
5027}
5028
5029// ============================================================
5030// CINEMATIC SEQUENCE EXPORTER (extended formats)
5031// ============================================================
5032
5033/// Export sequence timing to a simple JSON-like text format.
5034pub fn export_sequence_timing_json(seq: &CinematicSequencer) -> String {
5035    let mut out = String::from("{\n");
5036    out.push_str(&format!("  \"title\": \"{}\",\n", seq.master_sequence.name));
5037    out.push_str(&format!("  \"duration\": {},\n", seq.master_sequence.duration));
5038    out.push_str(&format!("  \"frame_rate\": {},\n", seq.playback.fps.fps()));
5039    out.push_str("  \"shots\": [\n");
5040    for (i, shot) in seq.shot_list.shots.iter().enumerate() {
5041        let comma = if i + 1 < seq.shot_list.shots.len() { "," } else { "" };
5042        out.push_str(&format!(
5043            "    {{\"id\": {}, \"name\": \"{}\", \"start\": {:.4}, \"end\": {:.4}, \"camera\": {}}}{}",
5044            shot.id, shot.name, shot.start_time, shot.end_time, shot.camera_id, comma
5045        ));
5046        out.push('\n');
5047    }
5048    out.push_str("  ]\n}\n");
5049    out
5050}
5051
5052/// Export all subtitle entries to a VTT (WebVTT) string.
5053pub fn export_subtitles_vtt(seq: &CinematicSequencer, fps: f32) -> String {
5054    let mut out = String::from("WEBVTT\n\n");
5055    let mut entries: Vec<&SubtitleEntry> = seq.tracks.subtitle_tracks.values()
5056        .flat_map(|t| t.entries.iter())
5057        .collect();
5058    entries.sort_by(|a, b| a.start_time.partial_cmp(&b.start_time).unwrap_or(std::cmp::Ordering::Equal));
5059
5060    for (i, e) in entries.iter().enumerate() {
5061        fn fmt_vtt(t: f64) -> String {
5062            let total_ms = (t * 1000.0) as u64;
5063            let ms  = total_ms % 1000;
5064            let sec = (total_ms / 1000) % 60;
5065            let min = (total_ms / 60000) % 60;
5066            let hr  = total_ms / 3600000;
5067            format!("{:02}:{:02}:{:02}.{:03}", hr, min, sec, ms)
5068        }
5069        out.push_str(&format!("{}\n{} --> {}\n{}\n\n",
5070            i + 1, fmt_vtt(e.start_time), fmt_vtt(e.end_time), e.text));
5071    }
5072    let _ = fps;
5073    out
5074}
5075
5076// ============================================================
5077// FLOAT CURVE BAKING & COMPRESSION
5078// ============================================================
5079
5080/// Bake a FloatCurve to a fixed-FPS float array.
5081pub fn bake_curve_to_frames(curve: &FloatCurve, fps: f32, duration: f64) -> Vec<f32> {
5082    let n = (duration * fps as f64).ceil() as usize + 1;
5083    (0..n).map(|i| curve.evaluate(i as f64 / fps as f64)).collect()
5084}
5085
5086/// Reconstruct a FloatCurve from baked frames (linear interpolation).
5087pub fn unbake_curve_from_frames(frames: &[f32], fps: f32) -> FloatCurve {
5088    let mut curve = FloatCurve::new("Unbaked");
5089    for (i, &v) in frames.iter().enumerate() {
5090        curve.add_key(i as f64 / fps as f64, v, InterpType::Linear);
5091    }
5092    curve
5093}
5094
5095/// Delta-encode a baked array (for compression).
5096pub fn delta_encode(values: &[f32]) -> Vec<f32> {
5097    let mut out = Vec::with_capacity(values.len());
5098    let mut prev = 0.0f32;
5099    for &v in values {
5100        out.push(v - prev);
5101        prev = v;
5102    }
5103    out
5104}
5105
5106/// Decode a delta-encoded array.
5107pub fn delta_decode(deltas: &[f32]) -> Vec<f32> {
5108    let mut out = Vec::with_capacity(deltas.len());
5109    let mut acc = 0.0f32;
5110    for &d in deltas {
5111        acc += d;
5112        out.push(acc);
5113    }
5114    out
5115}
5116
5117// ============================================================
5118// CINEMATIC DIRECTOR (RULE-BASED AUTO-EDIT)
5119// ============================================================
5120
5121/// Score an edit between two shots based on visual continuity rules.
5122pub fn score_shot_transition(
5123    current_cam_pos: Vec3,
5124    next_cam_pos:    Vec3,
5125    subject_pos:     Vec3,
5126    min_angle_deg:   f32,
5127) -> f32 {
5128    // Angle between camera vectors to subject
5129    let v0 = (subject_pos - current_cam_pos).normalize_or_zero();
5130    let v1 = (subject_pos - next_cam_pos).normalize_or_zero();
5131    let cos_angle = v0.dot(v1).clamp(-1.0, 1.0);
5132    let angle_deg = cos_angle.acos().to_degrees();
5133    // Penalise < min_angle_deg (axis cut rule)
5134    let angle_score = if angle_deg < min_angle_deg { angle_deg / min_angle_deg } else { 1.0 };
5135    // Prefer distance variety
5136    let d0 = (current_cam_pos - subject_pos).length();
5137    let d1 = (next_cam_pos - subject_pos).length();
5138    let ratio = if d0 < 1e-3 || d1 < 1e-3 { 0.5 } else { (d0 / d1).min(d1 / d0) };
5139    (angle_score + ratio) * 0.5
5140}
5141
5142// ============================================================
5143// AUDIO ENVELOPE GENERATOR
5144// ============================================================
5145
5146/// ADSR envelope: returns gain in [0,1] at time t given ADSR params.
5147pub fn adsr_envelope(t: f64, attack: f64, decay: f64, sustain: f32, release: f64, note_off: f64) -> f32 {
5148    if t < 0.0 { return 0.0; }
5149    if t < attack {
5150        return (t / attack.max(1e-10)) as f32;
5151    }
5152    let t2 = t - attack;
5153    if t2 < decay {
5154        let f = (t2 / decay.max(1e-10)) as f32;
5155        return 1.0 - (1.0 - sustain) * f;
5156    }
5157    if t < note_off {
5158        return sustain;
5159    }
5160    let t3 = t - note_off;
5161    if t3 < release {
5162        return sustain * (1.0 - (t3 / release.max(1e-10)) as f32);
5163    }
5164    0.0
5165}
5166
5167// ============================================================
5168// TRACK MUTE / SOLO MANAGER
5169// ============================================================
5170
5171pub struct MuteSoloManager {
5172    pub muted:  HashSet<u64>,
5173    pub solos:  HashSet<u64>,
5174    pub all_ids: Vec<u64>,
5175}
5176
5177impl MuteSoloManager {
5178    pub fn new(all_ids: Vec<u64>) -> Self {
5179        MuteSoloManager { muted: HashSet::new(), solos: HashSet::new(), all_ids }
5180    }
5181
5182    pub fn mute(&mut self, id: u64)   { self.muted.insert(id); }
5183    pub fn unmute(&mut self, id: u64) { self.muted.remove(&id); }
5184    pub fn solo(&mut self, id: u64)   { self.solos.insert(id); }
5185    pub fn unsolo(&mut self, id: u64) { self.solos.remove(&id); }
5186
5187    pub fn is_audible(&self, id: u64) -> bool {
5188        if self.muted.contains(&id) { return false; }
5189        if !self.solos.is_empty() && !self.solos.contains(&id) { return false; }
5190        true
5191    }
5192}
5193
5194// ============================================================
5195// CINEMATIC MARKERS
5196// ============================================================
5197
5198#[derive(Clone, Debug)]
5199pub struct SequenceMarker {
5200    pub id:    u64,
5201    pub time:  f64,
5202    pub name:  String,
5203    pub color: Vec4,
5204    pub kind:  MarkerKind,
5205}
5206
5207#[derive(Clone, Debug, PartialEq)]
5208pub enum MarkerKind {
5209    Comment,
5210    Chapter,
5211    BeatMarker,
5212    CutPoint,
5213    SceneChange,
5214    Custom(String),
5215}
5216
5217pub struct MarkerTrack {
5218    pub markers: Vec<SequenceMarker>,
5219    next_id: u64,
5220}
5221
5222impl MarkerTrack {
5223    pub fn new() -> Self { MarkerTrack { markers: Vec::new(), next_id: 1 } }
5224
5225    pub fn add(&mut self, time: f64, name: &str, color: Vec4, kind: MarkerKind) -> u64 {
5226        let id = self.next_id; self.next_id += 1;
5227        self.markers.push(SequenceMarker { id, time, name: name.to_string(), color, kind });
5228        self.markers.sort_by(|a, b| a.time.partial_cmp(&b.time).unwrap_or(std::cmp::Ordering::Equal));
5229        id
5230    }
5231
5232    pub fn remove(&mut self, id: u64) { self.markers.retain(|m| m.id != id); }
5233
5234    pub fn markers_in_range(&self, t_start: f64, t_end: f64) -> Vec<&SequenceMarker> {
5235        self.markers.iter().filter(|m| m.time >= t_start && m.time <= t_end).collect()
5236    }
5237
5238    pub fn nearest_marker(&self, t: f64) -> Option<&SequenceMarker> {
5239        self.markers.iter().min_by(|a, b| {
5240            let da = (a.time - t).abs();
5241            let db = (b.time - t).abs();
5242            da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal)
5243        })
5244    }
5245}
5246
5247// ============================================================
5248// COLOUR GRADING TRACK
5249// ============================================================
5250
5251#[derive(Clone, Debug)]
5252pub struct ColorGradingKeyframe {
5253    pub time:        f64,
5254    pub lift:        Vec3,   // shadow colour shift
5255    pub gamma:       Vec3,   // midtone
5256    pub gain:        Vec3,   // highlight
5257    pub saturation:  f32,
5258    pub contrast:    f32,
5259    pub exposure:    f32,
5260    pub hue_shift:   f32,
5261}
5262
5263impl ColorGradingKeyframe {
5264    pub fn identity(time: f64) -> Self {
5265        ColorGradingKeyframe {
5266            time,
5267            lift:       Vec3::ZERO,
5268            gamma:      Vec3::ONE,
5269            gain:       Vec3::ONE,
5270            saturation: 1.0,
5271            contrast:   1.0,
5272            exposure:   0.0,
5273            hue_shift:  0.0,
5274        }
5275    }
5276
5277    pub fn lerp(&self, other: &Self, t: f32) -> Self {
5278        ColorGradingKeyframe {
5279            time:       self.time + (other.time - self.time) * t as f64,
5280            lift:       self.lift.lerp(other.lift, t),
5281            gamma:      self.gamma.lerp(other.gamma, t),
5282            gain:       self.gain.lerp(other.gain, t),
5283            saturation: self.saturation + (other.saturation - self.saturation) * t,
5284            contrast:   self.contrast   + (other.contrast   - self.contrast)   * t,
5285            exposure:   self.exposure   + (other.exposure   - self.exposure)   * t,
5286            hue_shift:  self.hue_shift  + (other.hue_shift  - self.hue_shift)  * t,
5287        }
5288    }
5289}
5290
5291pub struct ColorGradingTrack {
5292    pub keyframes: Vec<ColorGradingKeyframe>,
5293    pub id:        u64,
5294    pub name:      String,
5295    pub enabled:   bool,
5296}
5297
5298impl ColorGradingTrack {
5299    pub fn new(id: u64, name: &str) -> Self {
5300        ColorGradingTrack { keyframes: Vec::new(), id, name: name.to_string(), enabled: true }
5301    }
5302
5303    pub fn add_keyframe(&mut self, kf: ColorGradingKeyframe) {
5304        let pos = self.keyframes.partition_point(|k| k.time < kf.time);
5305        self.keyframes.insert(pos, kf);
5306    }
5307
5308    pub fn evaluate(&self, time: f64) -> ColorGradingKeyframe {
5309        if self.keyframes.is_empty() { return ColorGradingKeyframe::identity(time); }
5310        let idx = self.keyframes.partition_point(|k| k.time <= time);
5311        if idx == 0 { return self.keyframes[0].clone(); }
5312        if idx >= self.keyframes.len() { return self.keyframes.last().unwrap().clone(); }
5313        let a = &self.keyframes[idx - 1];
5314        let b = &self.keyframes[idx];
5315        let t = ((time - a.time) / (b.time - a.time).max(1e-10)) as f32;
5316        a.lerp(b, t.clamp(0.0, 1.0))
5317    }
5318
5319    /// Apply grading to an RGB value.
5320    pub fn apply(&self, time: f64, rgb: Vec3) -> Vec3 {
5321        let g = self.evaluate(time);
5322        // Exposure
5323        let exposed = rgb * 2.0f32.powf(g.exposure);
5324        // Lift / Gamma / Gain (Resolve-style):
5325        let lifted  = exposed + g.lift * (Vec3::ONE - exposed);
5326        let gained  = lifted * g.gain;
5327        let inv_gamma = Vec3::ONE / g.gamma.max(Vec3::splat(0.001));
5328        let corrected = Vec3::new(gained.x.powf(inv_gamma.x), gained.y.powf(inv_gamma.y), gained.z.powf(inv_gamma.z));
5329        // Contrast around 0.5
5330        let contrasted = (corrected - Vec3::splat(0.5)) * g.contrast + Vec3::splat(0.5);
5331        // Saturation
5332        let luma = Vec3::new(0.299, 0.587, 0.114);
5333        let grey  = Vec3::splat(contrasted.dot(luma));
5334        grey.lerp(contrasted, g.saturation)
5335    }
5336}
5337
5338// ============================================================
5339// LOOK-AT TRACK (auto-aim camera at target)
5340// ============================================================
5341
5342#[derive(Clone, Debug)]
5343pub struct LookAtKeyframe {
5344    pub time:        f64,
5345    pub target_pos:  Vec3,
5346    pub weight:      f32,  // blend between free and look-at
5347    pub offset:      Vec3,
5348}
5349
5350pub struct LookAtTrack {
5351    pub keyframes: Vec<LookAtKeyframe>,
5352    pub id:        u64,
5353    pub name:      String,
5354    pub enabled:   bool,
5355}
5356
5357impl LookAtTrack {
5358    pub fn new(id: u64, name: &str) -> Self {
5359        LookAtTrack { keyframes: Vec::new(), id, name: name.to_string(), enabled: true }
5360    }
5361
5362    pub fn add_keyframe(&mut self, kf: LookAtKeyframe) {
5363        let pos = self.keyframes.partition_point(|k| k.time < kf.time);
5364        self.keyframes.insert(pos, kf);
5365    }
5366
5367    pub fn evaluate(&self, time: f64) -> Option<(Vec3, f32)> {
5368        if self.keyframes.is_empty() { return None; }
5369        let idx = self.keyframes.partition_point(|k| k.time <= time);
5370        if idx == 0 { let k = &self.keyframes[0]; return Some((k.target_pos + k.offset, k.weight)); }
5371        if idx >= self.keyframes.len() {
5372            let k = self.keyframes.last().unwrap();
5373            return Some((k.target_pos + k.offset, k.weight));
5374        }
5375        let a = &self.keyframes[idx - 1];
5376        let b = &self.keyframes[idx];
5377        let t = ((time - a.time) / (b.time - a.time).max(1e-10)) as f32;
5378        let target = (a.target_pos + a.offset).lerp(b.target_pos + b.offset, t);
5379        let weight = a.weight + (b.weight - a.weight) * t;
5380        Some((target, weight))
5381    }
5382}
5383
5384// ============================================================
5385// DOLLY ZOOM TRACK
5386// ============================================================
5387
5388/// Vertigo / Dolly-zoom: camera moves along rail while FOV compensates.
5389pub struct DollyZoomKeyframe {
5390    pub time:         f64,
5391    pub distance:     f32,  // camera-to-subject distance
5392    pub subject_size: f32,  // apparent size in radians (target angular size)
5393}
5394
5395impl DollyZoomKeyframe {
5396    /// Compute FOV (vertical) to keep subject_size constant: fov = 2*atan(subject_size / (2*d))
5397    pub fn fov_vertical(&self) -> f32 {
5398        2.0 * (self.subject_size / (2.0 * self.distance.max(0.001))).atan()
5399    }
5400}
5401
5402pub struct DollyZoomTrack {
5403    pub keyframes: Vec<DollyZoomKeyframe>,
5404    pub id:        u64,
5405    pub name:      String,
5406    pub enabled:   bool,
5407}
5408
5409impl DollyZoomTrack {
5410    pub fn new(id: u64, name: &str) -> Self {
5411        DollyZoomTrack { keyframes: Vec::new(), id, name: name.to_string(), enabled: true }
5412    }
5413
5414    pub fn add_keyframe(&mut self, kf: DollyZoomKeyframe) {
5415        let pos = self.keyframes.partition_point(|k| k.time < kf.time);
5416        self.keyframes.insert(pos, kf);
5417    }
5418
5419    pub fn evaluate_fov(&self, time: f64) -> f32 {
5420        if self.keyframes.is_empty() { return 60.0f32.to_radians(); }
5421        let idx = self.keyframes.partition_point(|k| k.time <= time);
5422        if idx == 0 { return self.keyframes[0].fov_vertical(); }
5423        if idx >= self.keyframes.len() { return self.keyframes.last().unwrap().fov_vertical(); }
5424        let a = &self.keyframes[idx - 1];
5425        let b = &self.keyframes[idx];
5426        let t = ((time - a.time) / (b.time - a.time).max(1e-10)) as f32;
5427        let d    = a.distance + (b.distance - a.distance) * t;
5428        let size = a.subject_size + (b.subject_size - a.subject_size) * t;
5429        2.0 * (size / (2.0 * d.max(0.001))).atan()
5430    }
5431}
5432
5433// ============================================================
5434// SEQUENCE RENDER PASS SYSTEM
5435// ============================================================
5436
5437#[derive(Clone, Debug)]
5438pub struct RenderPassConfig {
5439    pub name:          String,
5440    pub enabled:       bool,
5441    pub resolution_x:  u32,
5442    pub resolution_y:  u32,
5443    pub frame_rate:    f32,
5444    pub start_frame:   u64,
5445    pub end_frame:     u64,
5446    pub output_format: String,
5447    pub color_space:   String,
5448    pub motion_blur_samples: u32,
5449}
5450
5451impl RenderPassConfig {
5452    pub fn new(name: &str, width: u32, height: u32, fps: f32) -> Self {
5453        RenderPassConfig {
5454            name: name.to_string(),
5455            enabled: true,
5456            resolution_x: width,
5457            resolution_y: height,
5458            frame_rate: fps,
5459            start_frame: 0,
5460            end_frame: 0,
5461            output_format: "EXR".to_string(),
5462            color_space: "ACEScg".to_string(),
5463            motion_blur_samples: 8,
5464        }
5465    }
5466
5467    pub fn total_frames(&self) -> u64 { self.end_frame.saturating_sub(self.start_frame) }
5468    pub fn pixel_count(&self) -> u64 { self.resolution_x as u64 * self.resolution_y as u64 }
5469    pub fn total_pixels(&self) -> u64 { self.total_frames() * self.pixel_count() }
5470
5471    pub fn estimated_disk_gb(&self, bytes_per_pixel: f32) -> f32 {
5472        self.total_pixels() as f32 * bytes_per_pixel / 1_073_741_824.0
5473    }
5474}
5475
5476pub struct RenderQueueEntry {
5477    pub pass:      RenderPassConfig,
5478    pub priority:  i32,
5479    pub status:    RenderStatus,
5480    pub progress:  f32,
5481}
5482
5483#[derive(Clone, Debug, PartialEq)]
5484pub enum RenderStatus { Pending, Running, Done, Failed(String) }
5485
5486pub struct RenderQueue {
5487    pub entries: Vec<RenderQueueEntry>,
5488}
5489
5490impl RenderQueue {
5491    pub fn new() -> Self { RenderQueue { entries: Vec::new() } }
5492
5493    pub fn add(&mut self, pass: RenderPassConfig, priority: i32) {
5494        self.entries.push(RenderQueueEntry { pass, priority, status: RenderStatus::Pending, progress: 0.0 });
5495        self.entries.sort_by(|a, b| b.priority.cmp(&a.priority));
5496    }
5497
5498    pub fn next_pending(&mut self) -> Option<&mut RenderQueueEntry> {
5499        self.entries.iter_mut().find(|e| e.status == RenderStatus::Pending)
5500    }
5501
5502    pub fn total_estimated_disk_gb(&self, bytes_per_pixel: f32) -> f32 {
5503        self.entries.iter().filter(|e| e.pass.enabled).map(|e| e.pass.estimated_disk_gb(bytes_per_pixel)).sum()
5504    }
5505}
5506
5507// ============================================================
5508// TIME REMAP TRACK
5509// ============================================================
5510
5511/// A time-remap track maps sequence time → media time (for slow-mo / fast-forward).
5512pub struct TimeRemapTrack {
5513    pub curve: FloatCurve,  // output: media time as function of sequence time
5514    pub id:    u64,
5515    pub name:  String,
5516}
5517
5518impl TimeRemapTrack {
5519    pub fn new(id: u64, name: &str) -> Self {
5520        let curve = FloatCurve::new("TimeRemap");
5521        TimeRemapTrack { curve, id, name: name.to_string() }
5522    }
5523
5524    pub fn set_constant_speed(&mut self, duration: f64) {
5525        self.curve.keys.clear();
5526        self.curve.add_key(0.0, 0.0, InterpType::Linear);
5527        self.curve.add_key(duration, duration as f32, InterpType::Linear);
5528    }
5529
5530    pub fn set_slow_motion(&mut self, t_start: f64, t_end: f64, factor: f32) {
5531        // Remap: [t_start, t_end] → [t_start, t_start + (t_end-t_start)*factor]
5532        self.curve.add_key(t_start, t_start as f32, InterpType::Cubic);
5533        let media_end = t_start as f32 + (t_end - t_start) as f32 * factor;
5534        self.curve.add_key(t_end, media_end, InterpType::Cubic);
5535    }
5536
5537    pub fn media_time(&self, sequence_time: f64) -> f64 {
5538        self.curve.evaluate(sequence_time) as f64
5539    }
5540
5541    /// Playback speed at sequence_time (derivative of media_time w.r.t. sequence_time).
5542    pub fn speed_factor(&self, sequence_time: f64) -> f32 {
5543        let dt = 1e-4;
5544        let t0 = (sequence_time - dt).max(0.0);
5545        let t1 = sequence_time + dt;
5546        let m0 = self.curve.evaluate(t0) as f64;
5547        let m1 = self.curve.evaluate(t1) as f64;
5548        ((m1 - m0) / (t1 - t0)) as f32
5549    }
5550}
5551
5552// ============================================================
5553// CHAPTER SYSTEM
5554// ============================================================
5555
5556#[derive(Clone, Debug)]
5557pub struct Chapter {
5558    pub id:          u64,
5559    pub title:       String,
5560    pub start_time:  f64,
5561    pub thumbnail_t: f64,   // normalised time for thumbnail frame
5562    pub description: String,
5563}
5564
5565pub struct ChapterList {
5566    pub chapters: Vec<Chapter>,
5567    next_id: u64,
5568}
5569
5570impl ChapterList {
5571    pub fn new() -> Self { ChapterList { chapters: Vec::new(), next_id: 1 } }
5572
5573    pub fn add(&mut self, title: &str, start_time: f64, desc: &str) -> u64 {
5574        let id = self.next_id; self.next_id += 1;
5575        self.chapters.push(Chapter {
5576            id, title: title.to_string(), start_time,
5577            thumbnail_t: 0.0, description: desc.to_string(),
5578        });
5579        self.chapters.sort_by(|a, b| a.start_time.partial_cmp(&b.start_time).unwrap_or(std::cmp::Ordering::Equal));
5580        id
5581    }
5582
5583    pub fn chapter_at(&self, time: f64) -> Option<&Chapter> {
5584        self.chapters.iter().rev().find(|c| c.start_time <= time)
5585    }
5586
5587    pub fn to_youtube_chapters(&self) -> String {
5588        self.chapters.iter().map(|c| {
5589            let secs = c.start_time as u64;
5590            let h = secs / 3600;
5591            let m = (secs % 3600) / 60;
5592            let s = secs % 60;
5593            if h > 0 { format!("{:02}:{:02}:{:02} {}", h, m, s, c.title) }
5594            else      { format!("{:02}:{:02} {}", m, s, c.title) }
5595        }).collect::<Vec<_>>().join("\n")
5596    }
5597}
5598
5599// ============================================================
5600// EXTENDED UNIT TESTS
5601// ============================================================
5602
5603#[cfg(test)]
5604mod tests_cinematic_extended {
5605    use super::*;
5606
5607    #[test]
5608    fn test_sequence_graph_advance() {
5609        let mut g = SequenceGraph::new();
5610        let a = g.add_node("A", "seq_a", 5.0);
5611        let b = g.add_node("B", "seq_b", 3.0);
5612        g.add_edge(a, b, "always", 1.0);
5613        g.current = a;
5614        let next = g.advance();
5615        assert!(next.is_some());
5616        assert_eq!(g.current, b);
5617    }
5618
5619    #[test]
5620    fn test_sequence_graph_flag_condition() {
5621        let mut g = SequenceGraph::new();
5622        let a = g.add_node("A", "seq_a", 5.0);
5623        let b = g.add_node("B", "seq_b", 3.0);
5624        g.add_edge(a, b, "if_flag:hero_saved", 1.0);
5625        g.current = a;
5626        assert!(g.advance().is_none()); // flag not set
5627        g.set_flag("hero_saved");
5628        assert!(g.advance().is_some());
5629    }
5630
5631    #[test]
5632    fn test_shake_preset_library_applies() {
5633        let lib   = ShakePresetLibrary::new();
5634        let mut s = CameraShakeState { trauma: 0.0, ..Default::default() };
5635        lib.apply("explosion", &mut s);
5636        assert!(s.trauma > 0.0);
5637    }
5638
5639    #[test]
5640    fn test_adsr_envelope_sustain() {
5641        // At sustain phase, should equal sustain level
5642        let v = adsr_envelope(0.3, 0.1, 0.1, 0.7, 0.2, 1.0);
5643        assert!((v - 0.7).abs() < 0.05);
5644    }
5645
5646    #[test]
5647    fn test_adsr_envelope_release_zero() {
5648        // After full release, should be 0
5649        let v = adsr_envelope(2.0, 0.1, 0.1, 0.7, 0.2, 1.0);
5650        assert!(v.abs() < 0.01);
5651    }
5652
5653    #[test]
5654    fn test_mute_solo_manager_mute() {
5655        let mut m = MuteSoloManager::new(vec![1, 2, 3]);
5656        m.mute(2);
5657        assert!( m.is_audible(1));
5658        assert!(!m.is_audible(2));
5659    }
5660
5661    #[test]
5662    fn test_mute_solo_manager_solo() {
5663        let mut m = MuteSoloManager::new(vec![1, 2, 3]);
5664        m.solo(1);
5665        assert!( m.is_audible(1));
5666        assert!(!m.is_audible(2));
5667    }
5668
5669    #[test]
5670    fn test_marker_track_range_query() {
5671        let mut mt = MarkerTrack::new();
5672        mt.add(1.0, "A", Vec4::ONE, MarkerKind::Comment);
5673        mt.add(3.0, "B", Vec4::ONE, MarkerKind::Chapter);
5674        mt.add(5.0, "C", Vec4::ONE, MarkerKind::CutPoint);
5675        let in_range = mt.markers_in_range(2.0, 4.0);
5676        assert_eq!(in_range.len(), 1);
5677        assert_eq!(in_range[0].name, "B");
5678    }
5679
5680    #[test]
5681    fn test_color_grading_identity() {
5682        let track = ColorGradingTrack::new(1, "Grade");
5683        // With no keyframes, apply should be identity-ish
5684        let rgb = Vec3::new(0.5, 0.3, 0.1);
5685        // identity: no keys → identity keyframe → exposure 0, saturation 1, contrast 1
5686        let _ = track.apply(0.0, rgb);
5687    }
5688
5689    #[test]
5690    fn test_dolly_zoom_fov_decreases_with_distance() {
5691        let kf_near = DollyZoomKeyframe { time: 0.0, distance: 2.0, subject_size: 0.5 };
5692        let kf_far  = DollyZoomKeyframe { time: 1.0, distance: 10.0, subject_size: 0.5 };
5693        let fov_near = kf_near.fov_vertical();
5694        let fov_far  = kf_far.fov_vertical();
5695        assert!(fov_far < fov_near);
5696    }
5697
5698    #[test]
5699    fn test_render_queue_sorted_by_priority() {
5700        let mut rq = RenderQueue::new();
5701        rq.add(RenderPassConfig::new("Low", 1920, 1080, 24.0), 1);
5702        rq.add(RenderPassConfig::new("High", 1920, 1080, 24.0), 10);
5703        assert_eq!(rq.entries[0].pass.name, "High");
5704    }
5705
5706    #[test]
5707    fn test_time_remap_constant_speed() {
5708        let mut tr = TimeRemapTrack::new(1, "Main");
5709        tr.set_constant_speed(10.0);
5710        let mt = tr.media_time(5.0);
5711        assert!((mt - 5.0).abs() < 0.1);
5712    }
5713
5714    #[test]
5715    fn test_chapter_list_at_time() {
5716        let mut cl = ChapterList::new();
5717        cl.add("Intro", 0.0, "");
5718        cl.add("Act 1", 30.0, "");
5719        cl.add("Act 2", 90.0, "");
5720        let ch = cl.chapter_at(50.0).unwrap();
5721        assert_eq!(ch.title, "Act 1");
5722    }
5723
5724    #[test]
5725    fn test_youtube_chapters_format() {
5726        let mut cl = ChapterList::new();
5727        cl.add("Intro", 0.0, "");
5728        cl.add("Main",  65.0, "");
5729        let s = cl.to_youtube_chapters();
5730        assert!(s.contains("01:05 Main"));
5731    }
5732
5733    #[test]
5734    fn test_export_sequence_timing_json() {
5735        let seq = CinematicSequencer::new("TestSeq", 10.0, FrameRate::Fps24);
5736        let json = export_sequence_timing_json(&seq);
5737        assert!(json.contains("TestSeq"));
5738        assert!(json.contains("duration"));
5739    }
5740
5741    #[test]
5742    fn test_bake_curve_frame_count() {
5743        let mut c = FloatCurve::new("sin");
5744        c.add_key(0.0, 0.0, InterpType::Linear);
5745        c.add_key(1.0, 1.0, InterpType::Linear);
5746        let frames = bake_curve_to_frames(&c, 30.0, 1.0);
5747        assert_eq!(frames.len(), 32); // ceil(30)+1 = 31, but we add 1 → 32
5748    }
5749
5750    #[test]
5751    fn test_delta_encode_decode_round_trip() {
5752        let vals = vec![1.0f32, 2.0, 4.0, 3.0, 5.0];
5753        let d = delta_encode(&vals);
5754        let r = delta_decode(&d);
5755        for (a, b) in vals.iter().zip(r.iter()) {
5756            assert!((a - b).abs() < 1e-5);
5757        }
5758    }
5759
5760    #[test]
5761    fn test_export_subtitles_vtt_contains_webvtt() {
5762        let mut seq = CinematicSequencer::new("S", 10.0, FrameRate::Fps24);
5763        let sid = seq.add_subtitle_track("Sub");
5764        if let Some(t) = seq.tracks.subtitle_tracks.get_mut(&sid) {
5765            t.entries.push(SubtitleEntry {
5766                id: 1, start_time: 1.0, end_time: 3.0,
5767                text: "Hello World".to_string(),
5768                speaker: "Narrator".to_string(),
5769                style: crate::editor::cinematic_sequencer::SubtitleStyle::default(),
5770            });
5771        }
5772        let vtt = export_subtitles_vtt(&seq, 24.0);
5773        assert!(vtt.starts_with("WEBVTT"));
5774        assert!(vtt.contains("Hello World"));
5775    }
5776
5777    #[test]
5778    fn test_sequence_stats_shot_count() {
5779        let mut seq = CinematicSequencer::new("S", 10.0, FrameRate::Fps24);
5780        seq.shot_list.shots.push(Shot {
5781            id: 1, name: "Shot1".to_string(), camera_id: 0,
5782            start_time: 0.0, end_time: 5.0, transition: CutType::Cut,
5783            transition_duration: 0.0, take_number: 1,
5784            ..Shot::new(0, "", 0.0, 0.0, 0)
5785        });
5786        let stats = SequenceStats::compute(&seq);
5787        assert_eq!(stats.shot_count, 1);
5788    }
5789
5790    #[test]
5791    fn test_look_at_track_evaluate() {
5792        let mut track = LookAtTrack::new(1, "LookAt");
5793        track.add_keyframe(LookAtKeyframe { time: 0.0, target_pos: Vec3::ZERO, weight: 1.0, offset: Vec3::ZERO });
5794        track.add_keyframe(LookAtKeyframe { time: 1.0, target_pos: Vec3::new(0.0,0.0,10.0), weight: 1.0, offset: Vec3::ZERO });
5795        let (pos, w) = track.evaluate(0.5).unwrap();
5796        assert!((pos.z - 5.0).abs() < 0.05);
5797        assert!((w - 1.0).abs() < 0.01);
5798    }
5799
5800    #[test]
5801    fn test_shot_transition_score_axis() {
5802        let subject = Vec3::new(0.0, 0.0, 0.0);
5803        // Two cameras at similar angles → low score
5804        let c0 = Vec3::new(5.0, 2.0, 0.0);
5805        let c1 = Vec3::new(5.1, 2.0, 0.0);
5806        let score = score_shot_transition(c0, c1, subject, 30.0);
5807        assert!(score < 0.9);
5808    }
5809
5810    #[test]
5811    fn test_color_grading_track_evaluate_lerp() {
5812        let mut t = ColorGradingTrack::new(1, "G");
5813        t.add_keyframe(ColorGradingKeyframe { exposure: 0.0, ..ColorGradingKeyframe::identity(0.0) });
5814        t.add_keyframe(ColorGradingKeyframe { exposure: 2.0, ..ColorGradingKeyframe::identity(1.0) });
5815        let mid = t.evaluate(0.5);
5816        assert!((mid.exposure - 1.0).abs() < 0.05);
5817    }
5818
5819    #[test]
5820    fn test_interp_bench_result_constant_curve() {
5821        let mut c = FloatCurve::new("const");
5822        c.add_key(0.0, 5.0, InterpType::Linear);
5823        c.add_key(2.0, 5.0, InterpType::Linear);
5824        let r = InterpBenchResult::measure(&c, &|_| 5.0, 0.0, 2.0, 100);
5825        assert!(r.max_error < 0.001);
5826    }
5827}
5828
5829// ============================================================
5830// CURVE NOISE LAYER (procedural variation over a base curve)
5831// ============================================================
5832
5833/// Additive noise layer on top of a FloatCurve.
5834pub struct CurveNoiseLayer {
5835    pub amplitude: f32,
5836    pub frequency: f32,
5837    pub octaves:   u32,
5838    pub seed:      u32,
5839    pub enabled:   bool,
5840}
5841
5842impl CurveNoiseLayer {
5843    pub fn new(amplitude: f32, frequency: f32, octaves: u32, seed: u32) -> Self {
5844        CurveNoiseLayer { amplitude, frequency, octaves, seed, enabled: true }
5845    }
5846
5847    pub fn evaluate(&self, t: f64) -> f32 {
5848        if !self.enabled { return 0.0; }
5849        let mut val  = 0.0f32;
5850        let mut amp  = self.amplitude;
5851        let mut freq = self.frequency as f64;
5852        for i in 0..self.octaves {
5853            let x = t * freq + self.seed as f64 * 1.618 + i as f64 * 7.3;
5854            // Value noise from float time
5855            let xi = x.floor() as i64;
5856            let xf = (x - x.floor()) as f32;
5857            let fade = xf * xf * xf * (xf * (xf * 6.0 - 15.0) + 10.0);
5858            let h0 = pseudo_hash_f32(xi)     * 2.0 - 1.0;
5859            let h1 = pseudo_hash_f32(xi + 1) * 2.0 - 1.0;
5860            val  += (h0 + fade * (h1 - h0)) * amp;
5861            amp  *= 0.5;
5862            freq *= 2.0;
5863        }
5864        val
5865    }
5866}
5867
5868fn pseudo_hash_f32(x: i64) -> f32 {
5869    let x = x as u64;
5870    let mut h = x.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
5871    h ^= h >> 33;
5872    h = h.wrapping_mul(0xff51afd7ed558ccd);
5873    h ^= h >> 33;
5874    (h as f32) / u64::MAX as f32
5875}
5876
5877// ============================================================
5878// LAYERED ANIMATION BLEND TREE
5879// ============================================================
5880
5881#[derive(Clone, Debug)]
5882pub enum BlendNodeKind {
5883    Clip { name: String, curve_id: u64 },
5884    Lerp { weight: f32 },
5885    Additive,
5886    Override,
5887}
5888
5889#[derive(Clone, Debug)]
5890pub struct BlendTreeNode {
5891    pub id:       u64,
5892    pub kind:     BlendNodeKind,
5893    pub children: Vec<u64>,
5894    pub weight:   f32,
5895}
5896
5897pub struct BlendTree {
5898    pub nodes:   HashMap<u64, BlendTreeNode>,
5899    pub root_id: u64,
5900    next_id:     u64,
5901}
5902
5903impl BlendTree {
5904    pub fn new() -> Self { BlendTree { nodes: HashMap::new(), root_id: 0, next_id: 1 } }
5905
5906    pub fn add_node(&mut self, kind: BlendNodeKind, weight: f32) -> u64 {
5907        let id = self.next_id; self.next_id += 1;
5908        self.nodes.insert(id, BlendTreeNode { id, kind, children: Vec::new(), weight });
5909        id
5910    }
5911
5912    pub fn add_child(&mut self, parent: u64, child: u64) {
5913        if let Some(node) = self.nodes.get_mut(&parent) { node.children.push(child); }
5914    }
5915
5916    /// Evaluate the blend tree, returning a weighted sum of leaf values.
5917    /// `eval_clip` maps curve_id → value at a given time.
5918    pub fn evaluate(&self, node_id: u64, time: f64, eval_clip: &dyn Fn(u64, f64) -> f32) -> f32 {
5919        let node = match self.nodes.get(&node_id) { Some(n) => n, None => return 0.0 };
5920        match &node.kind {
5921            BlendNodeKind::Clip { curve_id, .. } => eval_clip(*curve_id, time),
5922            BlendNodeKind::Lerp { weight } => {
5923                if node.children.len() < 2 { return 0.0; }
5924                let a = self.evaluate(node.children[0], time, eval_clip);
5925                let b = self.evaluate(node.children[1], time, eval_clip);
5926                a + (b - a) * weight
5927            }
5928            BlendNodeKind::Additive => {
5929                node.children.iter().map(|&c| self.evaluate(c, time, eval_clip) * node.weight).sum()
5930            }
5931            BlendNodeKind::Override => {
5932                node.children.last().map(|&c| self.evaluate(c, time, eval_clip)).unwrap_or(0.0)
5933            }
5934        }
5935    }
5936}
5937
5938// ============================================================
5939// CAMERA RACK FOCUS TRACK
5940// ============================================================
5941
5942#[derive(Clone, Debug)]
5943pub struct RackFocusKeyframe {
5944    pub time:          f64,
5945    pub focus_target:  Vec3,
5946    pub transition_time: f64,
5947}
5948
5949pub struct RackFocusTrack {
5950    pub keyframes: Vec<RackFocusKeyframe>,
5951    pub id:        u64,
5952    pub name:      String,
5953    pub enabled:   bool,
5954}
5955
5956impl RackFocusTrack {
5957    pub fn new(id: u64, name: &str) -> Self {
5958        RackFocusTrack { keyframes: Vec::new(), id, name: name.to_string(), enabled: true }
5959    }
5960
5961    pub fn add_keyframe(&mut self, kf: RackFocusKeyframe) {
5962        let pos = self.keyframes.partition_point(|k| k.time < kf.time);
5963        self.keyframes.insert(pos, kf);
5964    }
5965
5966    /// Evaluate focus target and lerp-in-progress at `time`.
5967    pub fn evaluate(&self, time: f64) -> (Vec3, f32) {
5968        if self.keyframes.is_empty() { return (Vec3::ZERO, 1.0); }
5969        let idx = self.keyframes.partition_point(|k| k.time <= time);
5970        if idx == 0 { return (self.keyframes[0].focus_target, 1.0); }
5971        if idx >= self.keyframes.len() { return (self.keyframes.last().unwrap().focus_target, 1.0); }
5972        let a = &self.keyframes[idx - 1];
5973        let b = &self.keyframes[idx];
5974        // During transition into b
5975        let elapsed = time - b.time;
5976        if elapsed < b.transition_time && b.transition_time > 0.0 {
5977            let t = (elapsed / b.transition_time).clamp(0.0, 1.0) as f32;
5978            let smooth_t = t * t * (3.0 - 2.0 * t);
5979            (a.focus_target.lerp(b.focus_target, smooth_t), smooth_t)
5980        } else {
5981            (b.focus_target, 1.0)
5982        }
5983    }
5984
5985    /// Compute focus distance from camera position to target.
5986    pub fn focus_distance(&self, time: f64, camera_pos: Vec3) -> f32 {
5987        let (target, _) = self.evaluate(time);
5988        (camera_pos - target).length()
5989    }
5990}
5991
5992// ============================================================
5993// LENS FLARE TRACK
5994// ============================================================
5995
5996#[derive(Clone, Debug)]
5997pub struct LensFlareKeyframe {
5998    pub time:      f64,
5999    pub intensity: f32,
6000    pub tint:      Vec3,
6001    pub position:  Vec2,  // screen UV
6002    pub size:      f32,
6003    pub streak_rotation: f32,
6004    pub ghost_count: u32,
6005}
6006
6007impl LensFlareKeyframe {
6008    pub fn default_at(time: f64) -> Self {
6009        LensFlareKeyframe {
6010            time, intensity: 1.0, tint: Vec3::ONE, position: Vec2::new(0.5, 0.5),
6011            size: 0.3, streak_rotation: 0.0, ghost_count: 4,
6012        }
6013    }
6014}
6015
6016pub struct LensFlareTrack {
6017    pub keyframes: Vec<LensFlareKeyframe>,
6018    pub id:        u64,
6019    pub name:      String,
6020    pub enabled:   bool,
6021}
6022
6023impl LensFlareTrack {
6024    pub fn new(id: u64, name: &str) -> Self {
6025        LensFlareTrack { keyframes: Vec::new(), id, name: name.to_string(), enabled: true }
6026    }
6027
6028    pub fn add_keyframe(&mut self, kf: LensFlareKeyframe) {
6029        let pos = self.keyframes.partition_point(|k| k.time < kf.time);
6030        self.keyframes.insert(pos, kf);
6031    }
6032
6033    pub fn evaluate(&self, time: f64) -> LensFlareKeyframe {
6034        if self.keyframes.is_empty() { return LensFlareKeyframe::default_at(time); }
6035        let idx = self.keyframes.partition_point(|k| k.time <= time);
6036        if idx == 0 { return self.keyframes[0].clone(); }
6037        if idx >= self.keyframes.len() { return self.keyframes.last().unwrap().clone(); }
6038        let a = &self.keyframes[idx - 1];
6039        let b = &self.keyframes[idx];
6040        let t = ((time - a.time) / (b.time - a.time).max(1e-10)) as f32;
6041        LensFlareKeyframe {
6042            time,
6043            intensity:       a.intensity + (b.intensity - a.intensity) * t,
6044            tint:            a.tint.lerp(b.tint, t),
6045            position:        a.position.lerp(b.position, t),
6046            size:            a.size + (b.size - a.size) * t,
6047            streak_rotation: a.streak_rotation + (b.streak_rotation - a.streak_rotation) * t,
6048            ghost_count:     if t < 0.5 { a.ghost_count } else { b.ghost_count },
6049        }
6050    }
6051}
6052
6053// ============================================================
6054// VOLUMETRIC FOG TRACK
6055// ============================================================
6056
6057#[derive(Clone, Debug)]
6058pub struct FogKeyframe {
6059    pub time:       f64,
6060    pub density:    f32,
6061    pub start_dist: f32,
6062    pub end_dist:   f32,
6063    pub color:      Vec3,
6064    pub height:     f32,
6065    pub falloff:    f32,
6066}
6067
6068impl FogKeyframe {
6069    pub fn clear(time: f64) -> Self {
6070        FogKeyframe { time, density: 0.0, start_dist: 100.0, end_dist: 1000.0,
6071                      color: Vec3::ONE, height: 0.0, falloff: 1.0 }
6072    }
6073
6074    pub fn lerp_with(&self, other: &Self, t: f32) -> Self {
6075        FogKeyframe {
6076            time:       self.time + (other.time - self.time) * t as f64,
6077            density:    self.density    + (other.density    - self.density)    * t,
6078            start_dist: self.start_dist + (other.start_dist - self.start_dist) * t,
6079            end_dist:   self.end_dist   + (other.end_dist   - self.end_dist)   * t,
6080            color:      self.color.lerp(other.color, t),
6081            height:     self.height     + (other.height     - self.height)     * t,
6082            falloff:    self.falloff    + (other.falloff     - self.falloff)    * t,
6083        }
6084    }
6085
6086    /// Compute the exponential fog factor for a given view distance.
6087    pub fn fog_factor(&self, distance: f32) -> f32 {
6088        if distance < self.start_dist { return 0.0; }
6089        let d  = (distance - self.start_dist) / (self.end_dist - self.start_dist).max(1e-3);
6090        (-(d * self.density).exp()).max(0.0).min(1.0)
6091    }
6092}
6093
6094pub struct FogTrack {
6095    pub keyframes: Vec<FogKeyframe>,
6096    pub id:        u64,
6097    pub name:      String,
6098    pub enabled:   bool,
6099}
6100
6101impl FogTrack {
6102    pub fn new(id: u64, name: &str) -> Self {
6103        FogTrack { keyframes: Vec::new(), id, name: name.to_string(), enabled: true }
6104    }
6105
6106    pub fn add_keyframe(&mut self, kf: FogKeyframe) {
6107        let pos = self.keyframes.partition_point(|k| k.time < kf.time);
6108        self.keyframes.insert(pos, kf);
6109    }
6110
6111    pub fn evaluate(&self, time: f64) -> FogKeyframe {
6112        if self.keyframes.is_empty() { return FogKeyframe::clear(time); }
6113        let idx = self.keyframes.partition_point(|k| k.time <= time);
6114        if idx == 0 { return self.keyframes[0].clone(); }
6115        if idx >= self.keyframes.len() { return self.keyframes.last().unwrap().clone(); }
6116        let a = &self.keyframes[idx - 1];
6117        let b = &self.keyframes[idx];
6118        let t = ((time - a.time) / (b.time - a.time).max(1e-10)) as f32;
6119        a.lerp_with(b, t)
6120    }
6121}
6122
6123// ============================================================
6124// CROWD SIMULATION TRACK (background NPCs)
6125// ============================================================
6126
6127#[derive(Clone, Debug)]
6128pub struct CrowdKeyframe {
6129    pub time:         f64,
6130    pub density:      f32,    // agents per square unit
6131    pub speed:        f32,
6132    pub panic_factor: f32,    // 0=calm, 1=fleeing
6133    pub attractor:    Vec3,   // crowd centre
6134}
6135
6136impl CrowdKeyframe {
6137    pub fn default_at(time: f64) -> Self {
6138        CrowdKeyframe { time, density: 0.1, speed: 1.4, panic_factor: 0.0, attractor: Vec3::ZERO }
6139    }
6140}
6141
6142pub struct CrowdTrack {
6143    pub keyframes: Vec<CrowdKeyframe>,
6144    pub id:        u64,
6145    pub name:      String,
6146    pub enabled:   bool,
6147}
6148
6149impl CrowdTrack {
6150    pub fn new(id: u64, name: &str) -> Self {
6151        CrowdTrack { keyframes: Vec::new(), id, name: name.to_string(), enabled: true }
6152    }
6153
6154    pub fn add_keyframe(&mut self, kf: CrowdKeyframe) {
6155        let pos = self.keyframes.partition_point(|k| k.time < kf.time);
6156        self.keyframes.insert(pos, kf);
6157    }
6158
6159    pub fn evaluate(&self, time: f64) -> CrowdKeyframe {
6160        if self.keyframes.is_empty() { return CrowdKeyframe::default_at(time); }
6161        let idx = self.keyframes.partition_point(|k| k.time <= time);
6162        if idx == 0 { return self.keyframes[0].clone(); }
6163        if idx >= self.keyframes.len() { return self.keyframes.last().unwrap().clone(); }
6164        let a = &self.keyframes[idx - 1];
6165        let b = &self.keyframes[idx];
6166        let t = ((time - a.time) / (b.time - a.time).max(1e-10)) as f32;
6167        CrowdKeyframe {
6168            time,
6169            density:      a.density      + (b.density      - a.density)      * t,
6170            speed:        a.speed        + (b.speed         - a.speed)        * t,
6171            panic_factor: a.panic_factor + (b.panic_factor  - a.panic_factor) * t,
6172            attractor:    a.attractor.lerp(b.attractor, t),
6173        }
6174    }
6175
6176    /// Spawn count for a given area.
6177    pub fn spawn_count(&self, time: f64, area_sq: f32) -> u32 {
6178        let kf = self.evaluate(time);
6179        (kf.density * area_sq) as u32
6180    }
6181}
6182
6183// ============================================================
6184// PARTICLE SYSTEM TRACK
6185// ============================================================
6186
6187#[derive(Clone, Debug)]
6188pub struct ParticleSystemKeyframe {
6189    pub time:         f64,
6190    pub emit_rate:    f32,
6191    pub velocity:     Vec3,
6192    pub lifetime:     f32,
6193    pub size:         f32,
6194    pub color:        Vec4,
6195    pub turbulence:   f32,
6196}
6197
6198impl ParticleSystemKeyframe {
6199    pub fn default_at(time: f64) -> Self {
6200        ParticleSystemKeyframe {
6201            time, emit_rate: 100.0, velocity: Vec3::Y, lifetime: 2.0,
6202            size: 0.1, color: Vec4::ONE, turbulence: 0.0,
6203        }
6204    }
6205}
6206
6207pub struct ParticleTrack {
6208    pub keyframes: Vec<ParticleSystemKeyframe>,
6209    pub id:        u64,
6210    pub name:      String,
6211    pub enabled:   bool,
6212}
6213
6214impl ParticleTrack {
6215    pub fn new(id: u64, name: &str) -> Self {
6216        ParticleTrack { keyframes: Vec::new(), id, name: name.to_string(), enabled: true }
6217    }
6218
6219    pub fn add_keyframe(&mut self, kf: ParticleSystemKeyframe) {
6220        let pos = self.keyframes.partition_point(|k| k.time < kf.time);
6221        self.keyframes.insert(pos, kf);
6222    }
6223
6224    pub fn evaluate(&self, time: f64) -> ParticleSystemKeyframe {
6225        if self.keyframes.is_empty() { return ParticleSystemKeyframe::default_at(time); }
6226        let idx = self.keyframes.partition_point(|k| k.time <= time);
6227        if idx == 0 { return self.keyframes[0].clone(); }
6228        if idx >= self.keyframes.len() { return self.keyframes.last().unwrap().clone(); }
6229        let a = &self.keyframes[idx - 1];
6230        let b = &self.keyframes[idx];
6231        let t = ((time - a.time) / (b.time - a.time).max(1e-10)) as f32;
6232        ParticleSystemKeyframe {
6233            time,
6234            emit_rate:  a.emit_rate  + (b.emit_rate  - a.emit_rate)  * t,
6235            velocity:   a.velocity.lerp(b.velocity, t),
6236            lifetime:   a.lifetime   + (b.lifetime   - a.lifetime)   * t,
6237            size:       a.size       + (b.size        - a.size)       * t,
6238            color:      a.color.lerp(b.color, t),
6239            turbulence: a.turbulence + (b.turbulence  - a.turbulence) * t,
6240        }
6241    }
6242}
6243
6244// ============================================================
6245// SCREEN WIPE / TRANSITION ANIMATOR
6246// ============================================================
6247
6248#[derive(Clone, Debug, PartialEq)]
6249pub enum WipeStyle {
6250    FadeToBlack,
6251    FadeToWhite,
6252    IrisIn,
6253    IrisOut,
6254    WipeLeft,
6255    WipeRight,
6256    WipeUp,
6257    WipeDown,
6258    DiagonalWipe,
6259    CheckerBoard,
6260}
6261
6262#[derive(Clone, Debug)]
6263pub struct TransitionKeyframe {
6264    pub time:     f64,
6265    pub style:    WipeStyle,
6266    pub progress: f32,        // 0.0 = full source, 1.0 = full dest
6267    pub softness: f32,
6268}
6269
6270pub struct TransitionTrack {
6271    pub keyframes: Vec<TransitionKeyframe>,
6272    pub id:        u64,
6273    pub name:      String,
6274    pub enabled:   bool,
6275}
6276
6277impl TransitionTrack {
6278    pub fn new(id: u64, name: &str) -> Self {
6279        TransitionTrack { keyframes: Vec::new(), id, name: name.to_string(), enabled: true }
6280    }
6281
6282    pub fn add_keyframe(&mut self, kf: TransitionKeyframe) {
6283        let pos = self.keyframes.partition_point(|k| k.time < kf.time);
6284        self.keyframes.insert(pos, kf);
6285    }
6286
6287    pub fn evaluate_progress(&self, time: f64) -> f32 {
6288        if self.keyframes.is_empty() { return 0.0; }
6289        let idx = self.keyframes.partition_point(|k| k.time <= time);
6290        if idx == 0 { return self.keyframes[0].progress; }
6291        if idx >= self.keyframes.len() { return self.keyframes.last().unwrap().progress; }
6292        let a = &self.keyframes[idx - 1];
6293        let b = &self.keyframes[idx];
6294        let t = ((time - a.time) / (b.time - a.time).max(1e-10)) as f32;
6295        // Smooth step
6296        let s = t * t * (3.0 - 2.0 * t);
6297        a.progress + (b.progress - a.progress) * s
6298    }
6299
6300    /// Compute pixel blend factor for a given normalised screen position.
6301    pub fn pixel_blend(&self, time: f64, uv: Vec2, style_override: Option<&WipeStyle>) -> f32 {
6302        let p    = self.evaluate_progress(time);
6303        let kf   = self.keyframes.first();
6304        let style = style_override.or(kf.map(|k| &k.style)).unwrap_or(&WipeStyle::FadeToBlack);
6305        let soft = kf.map(|k| k.softness).unwrap_or(0.05);
6306        match style {
6307            WipeStyle::FadeToBlack | WipeStyle::FadeToWhite => p,
6308            WipeStyle::WipeLeft   => ((p - uv.x) / soft.max(1e-4)).clamp(0.0, 1.0),
6309            WipeStyle::WipeRight  => ((uv.x - (1.0 - p)) / soft.max(1e-4)).clamp(0.0, 1.0),
6310            WipeStyle::WipeUp     => ((uv.y - (1.0 - p)) / soft.max(1e-4)).clamp(0.0, 1.0),
6311            WipeStyle::WipeDown   => ((p - uv.y) / soft.max(1e-4)).clamp(0.0, 1.0),
6312            WipeStyle::IrisIn     => {
6313                let d = (uv - Vec2::new(0.5, 0.5)).length();
6314                ((p - d) / soft.max(1e-4)).clamp(0.0, 1.0)
6315            }
6316            WipeStyle::IrisOut    => {
6317                let d = (uv - Vec2::new(0.5, 0.5)).length();
6318                ((d - (1.0 - p) * 0.707) / soft.max(1e-4)).clamp(0.0, 1.0)
6319            }
6320            WipeStyle::DiagonalWipe => {
6321                let diag = uv.x + uv.y;
6322                ((p * 2.0 - diag) / soft.max(1e-4)).clamp(0.0, 1.0)
6323            }
6324            WipeStyle::CheckerBoard => {
6325                let cx = (uv.x * 8.0).floor() as i32;
6326                let cy = (uv.y * 8.0).floor() as i32;
6327                let checker = (cx + cy) % 2 == 0;
6328                let offset = if checker { 0.0 } else { 0.5 };
6329                ((p - offset) * 2.0).clamp(0.0, 1.0)
6330            }
6331        }
6332    }
6333}
6334
6335// ============================================================
6336// AUDIO SPECTRUM ANALYSER
6337// ============================================================
6338
6339/// Simple FFT-free spectrum analyser using bank of IIR band-pass filters.
6340pub struct AudioSpectrumAnalyser {
6341    pub bands:   Vec<f32>,     // centre frequencies (Hz)
6342    pub levels:  Vec<f32>,     // current dB level per band
6343    pub attack:  f32,
6344    pub release: f32,
6345    peaks:       Vec<f32>,
6346}
6347
6348impl AudioSpectrumAnalyser {
6349    pub fn new(bands: Vec<f32>) -> Self {
6350        let n = bands.len();
6351        AudioSpectrumAnalyser { bands, levels: vec![0.0; n], attack: 50.0, release: 10.0, peaks: vec![0.0; n] }
6352    }
6353
6354    pub fn standard_8_band() -> Self {
6355        Self::new(vec![63.0, 125.0, 250.0, 500.0, 1000.0, 2000.0, 4000.0, 8000.0])
6356    }
6357
6358    /// Feed simulated band levels (amplitude [0,1]) and update with attack/release.
6359    pub fn update(&mut self, input_levels: &[f32], dt: f32) {
6360        for (i, &input) in input_levels.iter().enumerate().take(self.levels.len()) {
6361            if input > self.levels[i] {
6362                self.levels[i] += (input - self.levels[i]) * self.attack * dt;
6363            } else {
6364                self.levels[i] += (input - self.levels[i]) * self.release * dt;
6365            }
6366            self.peaks[i] = self.peaks[i].max(self.levels[i]);
6367        }
6368    }
6369
6370    /// Decay peaks slowly.
6371    pub fn decay_peaks(&mut self, dt: f32) {
6372        for p in &mut self.peaks { *p -= dt * 0.5; *p = p.max(0.0); }
6373    }
6374
6375    /// Convert amplitude to dBFS.
6376    pub fn to_dbfs(amplitude: f32) -> f32 {
6377        if amplitude < 1e-10 { -96.0 } else { 20.0 * amplitude.log10() }
6378    }
6379}
6380
6381// ============================================================
6382// SEQUENCE EXPORT MANAGER
6383// ============================================================
6384
6385pub struct ExportPreset {
6386    pub name:         String,
6387    pub codec:        String,
6388    pub container:    String,
6389    pub width:        u32,
6390    pub height:       u32,
6391    pub fps:          f32,
6392    pub crf:          u32,   // quality 0-51
6393    pub audio_rate:   u32,
6394    pub include_subs: bool,
6395}
6396
6397impl ExportPreset {
6398    pub fn youtube_4k() -> Self {
6399        ExportPreset { name: "YouTube4K".to_string(), codec: "H264".to_string(),
6400                       container: "MP4".to_string(), width: 3840, height: 2160,
6401                       fps: 30.0, crf: 18, audio_rate: 48000, include_subs: true }
6402    }
6403    pub fn web_720p() -> Self {
6404        ExportPreset { name: "Web720p".to_string(), codec: "H265".to_string(),
6405                       container: "WebM".to_string(), width: 1280, height: 720,
6406                       fps: 24.0, crf: 28, audio_rate: 44100, include_subs: false }
6407    }
6408    pub fn broadcast_hdcam() -> Self {
6409        ExportPreset { name: "HDCam".to_string(), codec: "ProRes422".to_string(),
6410                       container: "MOV".to_string(), width: 1920, height: 1080,
6411                       fps: 29.97, crf: 0, audio_rate: 48000, include_subs: true }
6412    }
6413
6414    pub fn bitrate_estimate_mbps(&self, seconds: f64) -> f32 {
6415        // Very rough heuristic: 4K at CRF18 ~ 40 Mbps
6416        let base = match self.codec.as_str() {
6417            "H264"    => 8.0f32,
6418            "H265"    => 4.0f32,
6419            "ProRes422" => 147.0f32,
6420            _         => 10.0f32,
6421        };
6422        let scale = (self.width as f32 * self.height as f32) / (1920.0 * 1080.0);
6423        let _ = (seconds, self.crf);
6424        base * scale * self.fps / 30.0
6425    }
6426}
6427
6428pub struct ExportManager {
6429    pub presets:  Vec<ExportPreset>,
6430    pub queue:    VecDeque<(String, String)>,  // (preset_name, output_path)
6431}
6432
6433impl ExportManager {
6434    pub fn new() -> Self {
6435        ExportManager {
6436            presets: vec![ExportPreset::youtube_4k(), ExportPreset::web_720p(), ExportPreset::broadcast_hdcam()],
6437            queue:   VecDeque::new(),
6438        }
6439    }
6440
6441    pub fn add_preset(&mut self, preset: ExportPreset) { self.presets.push(preset); }
6442
6443    pub fn enqueue(&mut self, preset_name: &str, output_path: &str) {
6444        self.queue.push_back((preset_name.to_string(), output_path.to_string()));
6445    }
6446
6447    pub fn dequeue(&mut self) -> Option<(String, String)> { self.queue.pop_front() }
6448
6449    pub fn preset_by_name(&self, name: &str) -> Option<&ExportPreset> {
6450        self.presets.iter().find(|p| p.name == name)
6451    }
6452}
6453
6454// ============================================================
6455// CURVE EDITOR VIEW STATE (pan/zoom)
6456// ============================================================
6457
6458pub struct CurveEditorViewState {
6459    pub time_offset:    f64,   // leftmost visible time
6460    pub time_scale:     f64,   // pixels per second
6461    pub value_offset:   f32,
6462    pub value_scale:    f32,
6463    pub selected_keys:  HashSet<(usize, usize)>,  // (track_idx, key_idx)
6464    pub snap_time:      bool,
6465    pub snap_value:     bool,
6466    pub snap_interval:  f64,
6467    pub show_tangents:  bool,
6468    pub tangent_length: f32,
6469}
6470
6471impl CurveEditorViewState {
6472    pub fn new() -> Self {
6473        CurveEditorViewState {
6474            time_offset:   0.0,
6475            time_scale:    100.0,
6476            value_offset:  0.0,
6477            value_scale:   100.0,
6478            selected_keys: HashSet::new(),
6479            snap_time:     false,
6480            snap_value:    false,
6481            snap_interval: 1.0 / 30.0,
6482            show_tangents: true,
6483            tangent_length: 30.0,
6484        }
6485    }
6486
6487    pub fn time_to_pixel(&self, time: f64) -> f32 {
6488        ((time - self.time_offset) * self.time_scale) as f32
6489    }
6490
6491    pub fn pixel_to_time(&self, px: f32) -> f64 {
6492        px as f64 / self.time_scale + self.time_offset
6493    }
6494
6495    pub fn value_to_pixel(&self, val: f32) -> f32 {
6496        (val - self.value_offset) * self.value_scale
6497    }
6498
6499    pub fn pixel_to_value(&self, py: f32) -> f32 {
6500        py / self.value_scale + self.value_offset
6501    }
6502
6503    pub fn zoom_time(&mut self, factor: f64, pivot_px: f32) {
6504        let pivot_time = self.pixel_to_time(pivot_px);
6505        self.time_scale *= factor;
6506        self.time_offset = pivot_time - pivot_px as f64 / self.time_scale;
6507    }
6508
6509    pub fn zoom_value(&mut self, factor: f32, pivot_py: f32) {
6510        let pivot_val = self.pixel_to_value(pivot_py);
6511        self.value_scale *= factor;
6512        self.value_offset = pivot_val - pivot_py / self.value_scale;
6513    }
6514
6515    pub fn frame_all(&mut self, t_start: f64, t_end: f64, v_min: f32, v_max: f32, width: f32, height: f32) {
6516        let td = (t_end - t_start).max(1e-6);
6517        let vd = (v_max - v_min).max(1e-6);
6518        self.time_scale  = width as f64 / td * 0.9;
6519        self.time_offset = t_start - td * 0.05;
6520        self.value_scale  = height / vd * 0.9;
6521        self.value_offset = v_min - vd * 0.05;
6522    }
6523
6524    pub fn select_all(&mut self, track_count: usize, key_counts: &[usize]) {
6525        self.selected_keys.clear();
6526        for (t, &kc) in key_counts.iter().enumerate().take(track_count) {
6527            for k in 0..kc { self.selected_keys.insert((t, k)); }
6528        }
6529    }
6530}
6531
6532// ============================================================
6533// SEQUENCE CLIPBOARD (copy/paste keyframes)
6534// ============================================================
6535
6536pub struct KeyframeClipboard {
6537    pub float_keys: Vec<(f64, f32, InterpType)>,
6538    pub camera_keys: Vec<CameraKeyframe>,
6539    pub actor_keys:  Vec<ActorKeyframe>,
6540}
6541
6542impl KeyframeClipboard {
6543    pub fn new() -> Self {
6544        KeyframeClipboard { float_keys: Vec::new(), camera_keys: Vec::new(), actor_keys: Vec::new() }
6545    }
6546
6547    pub fn copy_float_keys(&mut self, curve: &FloatCurve, selection: &[(usize, usize)]) {
6548        self.float_keys.clear();
6549        for &(_, ki) in selection {
6550            if let Some(k) = curve.keys.get(ki) {
6551                self.float_keys.push((k.time, k.value, k.interp.clone()));
6552            }
6553        }
6554    }
6555
6556    pub fn paste_float_keys(&self, curve: &mut FloatCurve, time_offset: f64) {
6557        if self.float_keys.is_empty() { return; }
6558        let first_t = self.float_keys[0].0;
6559        for (t, v, interp) in &self.float_keys {
6560            curve.add_key(time_offset + (t - first_t), *v, interp.clone());
6561        }
6562    }
6563
6564    pub fn copy_camera_keys(&mut self, track: &CameraTrack, from: f64, to: f64) {
6565        self.camera_keys = track.keyframes.iter()
6566            .filter(|k| k.time >= from && k.time <= to)
6567            .cloned().collect();
6568    }
6569
6570    pub fn paste_camera_keys(&self, track: &mut CameraTrack, time_offset: f64) {
6571        if self.camera_keys.is_empty() { return; }
6572        let first_t = self.camera_keys[0].time;
6573        for k in &self.camera_keys {
6574            let mut nk = k.clone();
6575            nk.time = time_offset + (k.time - first_t);
6576            let pos = track.keyframes.partition_point(|ek| ek.time < nk.time);
6577            track.keyframes.insert(pos, nk);
6578        }
6579    }
6580}
6581
6582// ============================================================
6583// FINAL LARGE TEST SUITE
6584// ============================================================
6585
6586#[cfg(test)]
6587mod tests_cinematic_final {
6588    use super::*;
6589
6590    #[test]
6591    fn test_curve_noise_layer_non_zero() {
6592        let nl = CurveNoiseLayer::new(1.0, 2.0, 4, 42);
6593        let vals: Vec<f32> = (0..10).map(|i| nl.evaluate(i as f64 * 0.1)).collect();
6594        let any_nonzero = vals.iter().any(|&v| v.abs() > 0.001);
6595        assert!(any_nonzero);
6596    }
6597
6598    #[test]
6599    fn test_blend_tree_lerp() {
6600        let mut tree = BlendTree::new();
6601        let a_id = tree.add_node(BlendNodeKind::Clip { name: "A".to_string(), curve_id: 1 }, 1.0);
6602        let b_id = tree.add_node(BlendNodeKind::Clip { name: "B".to_string(), curve_id: 2 }, 1.0);
6603        let lerp_id = tree.add_node(BlendNodeKind::Lerp { weight: 0.5 }, 1.0);
6604        tree.add_child(lerp_id, a_id);
6605        tree.add_child(lerp_id, b_id);
6606        let eval = |curve_id: u64, _time: f64| -> f32 { if curve_id == 1 { 0.0 } else { 1.0 } };
6607        let result = tree.evaluate(lerp_id, 0.0, &eval);
6608        assert!((result - 0.5).abs() < 0.001);
6609    }
6610
6611    #[test]
6612    fn test_rack_focus_distance() {
6613        let mut t = RackFocusTrack::new(1, "RF");
6614        t.add_keyframe(RackFocusKeyframe { time: 0.0, focus_target: Vec3::new(0.0,0.0,10.0), transition_time: 0.5 });
6615        let dist = t.focus_distance(0.0, Vec3::ZERO);
6616        assert!((dist - 10.0).abs() < 0.01);
6617    }
6618
6619    #[test]
6620    fn test_lens_flare_interpolation() {
6621        let mut t = LensFlareTrack::new(1, "Flare");
6622        t.add_keyframe(LensFlareKeyframe { intensity: 0.0, ..LensFlareKeyframe::default_at(0.0) });
6623        t.add_keyframe(LensFlareKeyframe { intensity: 1.0, ..LensFlareKeyframe::default_at(1.0) });
6624        let kf = t.evaluate(0.5);
6625        assert!((kf.intensity - 0.5).abs() < 0.05);
6626    }
6627
6628    #[test]
6629    fn test_fog_track_clear_factor() {
6630        let clear = FogKeyframe::clear(0.0);
6631        assert!(clear.fog_factor(500.0).abs() < 0.01);
6632    }
6633
6634    #[test]
6635    fn test_fog_track_interpolation() {
6636        let mut ft = FogTrack::new(1, "Fog");
6637        ft.add_keyframe(FogKeyframe { density: 0.0, ..FogKeyframe::clear(0.0) });
6638        ft.add_keyframe(FogKeyframe { density: 1.0, ..FogKeyframe::clear(1.0) });
6639        let mid = ft.evaluate(0.5);
6640        assert!((mid.density - 0.5).abs() < 0.05);
6641    }
6642
6643    #[test]
6644    fn test_crowd_spawn_count() {
6645        let mut ct = CrowdTrack::new(1, "Crowd");
6646        ct.add_keyframe(CrowdKeyframe { density: 0.5, ..CrowdKeyframe::default_at(0.0) });
6647        let n = ct.spawn_count(0.0, 100.0);
6648        assert_eq!(n, 50);
6649    }
6650
6651    #[test]
6652    fn test_particle_track_interpolation() {
6653        let mut pt = ParticleTrack::new(1, "Fire");
6654        pt.add_keyframe(ParticleSystemKeyframe { emit_rate: 0.0,   ..ParticleSystemKeyframe::default_at(0.0) });
6655        pt.add_keyframe(ParticleSystemKeyframe { emit_rate: 100.0, ..ParticleSystemKeyframe::default_at(1.0) });
6656        let mid = pt.evaluate(0.5);
6657        assert!((mid.emit_rate - 50.0).abs() < 1.0);
6658    }
6659
6660    #[test]
6661    fn test_transition_track_wipe_left() {
6662        let mut tt = TransitionTrack::new(1, "Wipe");
6663        tt.add_keyframe(TransitionKeyframe {
6664            time: 0.0, style: WipeStyle::WipeLeft, progress: 0.5, softness: 0.01
6665        });
6666        let blend = tt.pixel_blend(0.0, Vec2::new(0.4, 0.5), None);
6667        assert!(blend > 0.5);
6668    }
6669
6670    #[test]
6671    fn test_spectrum_analyser_update() {
6672        let mut sa = AudioSpectrumAnalyser::standard_8_band();
6673        sa.update(&[0.5, 0.3, 0.1, 0.0, 0.0, 0.0, 0.0, 0.0], 0.016);
6674        assert!(sa.levels[0] > 0.0);
6675    }
6676
6677    #[test]
6678    fn test_export_preset_bitrate_estimate() {
6679        let p = ExportPreset::youtube_4k();
6680        let br = p.bitrate_estimate_mbps(60.0);
6681        assert!(br > 0.0);
6682    }
6683
6684    #[test]
6685    fn test_export_manager_enqueue_dequeue() {
6686        let mut em = ExportManager::new();
6687        em.enqueue("YouTube4K", "/tmp/out.mp4");
6688        let item = em.dequeue().unwrap();
6689        assert_eq!(item.0, "YouTube4K");
6690    }
6691
6692    #[test]
6693    fn test_curve_editor_view_state_zoom() {
6694        let mut vs = CurveEditorViewState::new();
6695        vs.zoom_time(2.0, 0.0);
6696        assert!((vs.time_scale - 200.0).abs() < 1.0);
6697    }
6698
6699    #[test]
6700    fn test_curve_editor_frame_all() {
6701        let mut vs = CurveEditorViewState::new();
6702        vs.frame_all(0.0, 10.0, -1.0, 1.0, 800.0, 400.0);
6703        assert!(vs.time_scale > 0.0);
6704    }
6705
6706    #[test]
6707    fn test_keyframe_clipboard_paste() {
6708        let mut source = FloatCurve::new("src");
6709        source.add_key(0.0, 1.0, InterpType::Linear);
6710        source.add_key(1.0, 2.0, InterpType::Linear);
6711        let mut clip = KeyframeClipboard::new();
6712        clip.copy_float_keys(&source, &[(0, 0), (0, 1)]);
6713        let mut dest = FloatCurve::new("dst");
6714        clip.paste_float_keys(&mut dest, 5.0);
6715        assert_eq!(dest.keys.len(), 2);
6716        assert!((dest.keys[0].time - 5.0).abs() < 1e-6);
6717    }
6718
6719    #[test]
6720    fn test_chapter_to_youtube_no_hours() {
6721        let mut cl = ChapterList::new();
6722        cl.add("Start", 0.0, "");
6723        let s = cl.to_youtube_chapters();
6724        assert!(s.starts_with("00:00 Start"));
6725    }
6726
6727    #[test]
6728    fn test_render_queue_total_disk() {
6729        let mut rq = RenderQueue::new();
6730        let mut p = RenderPassConfig::new("Test", 1920, 1080, 24.0);
6731        p.end_frame = 240;
6732        rq.add(p, 0);
6733        let gb = rq.total_estimated_disk_gb(4.0);
6734        assert!(gb > 0.0);
6735    }
6736
6737    #[test]
6738    fn test_time_remap_speed_factor_constant() {
6739        let mut tr = TimeRemapTrack::new(1, "Const");
6740        tr.set_constant_speed(10.0);
6741        let speed = tr.speed_factor(5.0);
6742        assert!((speed - 1.0).abs() < 0.05);
6743    }
6744}
6745
6746// ============================================================
6747// CAMERA CRANE / JIB ANIMATION
6748// ============================================================
6749
6750/// Describes a camera crane's arm pose at a given time.
6751#[derive(Clone, Debug)]
6752pub struct CraneKeyframe {
6753    pub time:         f64,
6754    pub arm_length:   f32,
6755    pub arm_angle:    f32,   // degrees up/down from horizontal
6756    pub pan_angle:    f32,   // degrees horizontal rotation
6757    pub tilt:         f32,   // camera head tilt
6758    pub roll:         f32,
6759}
6760
6761impl CraneKeyframe {
6762    pub fn default_at(time: f64) -> Self {
6763        CraneKeyframe { time, arm_length: 3.0, arm_angle: 0.0, pan_angle: 0.0, tilt: 0.0, roll: 0.0 }
6764    }
6765
6766    /// World-space camera position given crane base position.
6767    pub fn camera_position(&self, base: Vec3) -> Vec3 {
6768        let pan_rad  = self.pan_angle.to_radians();
6769        let arm_rad  = self.arm_angle.to_radians();
6770        let fwd = Vec3::new(pan_rad.cos(), arm_rad.sin(), pan_rad.sin());
6771        base + fwd * self.arm_length
6772    }
6773}
6774
6775pub struct CraneTrack {
6776    pub keyframes: Vec<CraneKeyframe>,
6777    pub id:        u64,
6778    pub name:      String,
6779    pub base_pos:  Vec3,
6780    pub enabled:   bool,
6781}
6782
6783impl CraneTrack {
6784    pub fn new(id: u64, name: &str, base: Vec3) -> Self {
6785        CraneTrack { keyframes: Vec::new(), id, name: name.to_string(), base_pos: base, enabled: true }
6786    }
6787
6788    pub fn add_keyframe(&mut self, kf: CraneKeyframe) {
6789        let pos = self.keyframes.partition_point(|k| k.time < kf.time);
6790        self.keyframes.insert(pos, kf);
6791    }
6792
6793    pub fn evaluate(&self, time: f64) -> CraneKeyframe {
6794        if self.keyframes.is_empty() { return CraneKeyframe::default_at(time); }
6795        let idx = self.keyframes.partition_point(|k| k.time <= time);
6796        if idx == 0 { return self.keyframes[0].clone(); }
6797        if idx >= self.keyframes.len() { return self.keyframes.last().unwrap().clone(); }
6798        let a = &self.keyframes[idx - 1];
6799        let b = &self.keyframes[idx];
6800        let t = ((time - a.time) / (b.time - a.time).max(1e-10)) as f32;
6801        CraneKeyframe {
6802            time,
6803            arm_length: a.arm_length + (b.arm_length - a.arm_length) * t,
6804            arm_angle:  a.arm_angle  + (b.arm_angle  - a.arm_angle)  * t,
6805            pan_angle:  a.pan_angle  + (b.pan_angle  - a.pan_angle)  * t,
6806            tilt:       a.tilt       + (b.tilt        - a.tilt)       * t,
6807            roll:       a.roll       + (b.roll         - a.roll)       * t,
6808        }
6809    }
6810
6811    pub fn camera_world_pos(&self, time: f64) -> Vec3 {
6812        self.evaluate(time).camera_position(self.base_pos)
6813    }
6814}
6815
6816// ============================================================
6817// STEREO / VR CAMERA TRACK
6818// ============================================================
6819
6820#[derive(Clone, Debug)]
6821pub struct StereoKeyframe {
6822    pub time:           f64,
6823    pub ipd:            f32,    // inter-pupillary distance in metres
6824    pub convergence:    f32,    // convergence distance
6825    pub zero_parallax:  f32,    // zero-parallax plane
6826    pub stereo_window:  f32,
6827}
6828
6829impl StereoKeyframe {
6830    pub fn default_at(time: f64) -> Self {
6831        StereoKeyframe { time, ipd: 0.063, convergence: 5.0, zero_parallax: 5.0, stereo_window: 0.0 }
6832    }
6833
6834    /// Left eye offset given direction vector.
6835    pub fn left_eye_offset(&self, right: Vec3) -> Vec3 { -right * self.ipd * 0.5 }
6836    pub fn right_eye_offset(&self, right: Vec3) -> Vec3 {  right * self.ipd * 0.5 }
6837}
6838
6839pub struct StereoTrack {
6840    pub keyframes: Vec<StereoKeyframe>,
6841    pub id:        u64,
6842    pub name:      String,
6843    pub enabled:   bool,
6844}
6845
6846impl StereoTrack {
6847    pub fn new(id: u64, name: &str) -> Self {
6848        StereoTrack { keyframes: Vec::new(), id, name: name.to_string(), enabled: true }
6849    }
6850
6851    pub fn add_keyframe(&mut self, kf: StereoKeyframe) {
6852        let pos = self.keyframes.partition_point(|k| k.time < kf.time);
6853        self.keyframes.insert(pos, kf);
6854    }
6855
6856    pub fn evaluate(&self, time: f64) -> StereoKeyframe {
6857        if self.keyframes.is_empty() { return StereoKeyframe::default_at(time); }
6858        let idx = self.keyframes.partition_point(|k| k.time <= time);
6859        if idx == 0 { return self.keyframes[0].clone(); }
6860        if idx >= self.keyframes.len() { return self.keyframes.last().unwrap().clone(); }
6861        let a = &self.keyframes[idx - 1];
6862        let b = &self.keyframes[idx];
6863        let t = ((time - a.time) / (b.time - a.time).max(1e-10)) as f32;
6864        StereoKeyframe {
6865            time,
6866            ipd:          a.ipd         + (b.ipd         - a.ipd)         * t,
6867            convergence:  a.convergence + (b.convergence  - a.convergence) * t,
6868            zero_parallax:a.zero_parallax+(b.zero_parallax-a.zero_parallax)* t,
6869            stereo_window:a.stereo_window+(b.stereo_window-a.stereo_window)* t,
6870        }
6871    }
6872}
6873
6874// ============================================================
6875// SEQUENCE BEAT GRID
6876// ============================================================
6877
6878/// A musical beat grid for aligning cuts to music.
6879pub struct BeatGrid {
6880    pub bpm:           f32,
6881    pub time_signature: (u32, u32),   // beats/bar, beat_unit
6882    pub start_offset:  f64,
6883    pub beat_times:    Vec<f64>,
6884}
6885
6886impl BeatGrid {
6887    pub fn new(bpm: f32, ts: (u32, u32), start: f64, duration: f64) -> Self {
6888        let beat_period = 60.0 / bpm as f64;
6889        let count = (duration / beat_period).ceil() as usize + 1;
6890        let beat_times = (0..count).map(|i| start + i as f64 * beat_period).collect();
6891        BeatGrid { bpm, time_signature: ts, start_offset: start, beat_times }
6892    }
6893
6894    /// Snap a time to the nearest beat.
6895    pub fn snap(&self, time: f64) -> f64 {
6896        if self.beat_times.is_empty() { return time; }
6897        let idx = self.beat_times.partition_point(|&t| t <= time);
6898        if idx == 0 { return self.beat_times[0]; }
6899        if idx >= self.beat_times.len() { return *self.beat_times.last().unwrap(); }
6900        let prev = self.beat_times[idx - 1];
6901        let next = self.beat_times[idx];
6902        if (time - prev) < (next - time) { prev } else { next }
6903    }
6904
6905    /// Return bar number (0-based) for a given time.
6906    pub fn bar_at(&self, time: f64) -> u32 {
6907        let beat_idx = ((time - self.start_offset) / (60.0 / self.bpm as f64)).floor() as u32;
6908        beat_idx / self.time_signature.0
6909    }
6910
6911    /// Return beat-in-bar (0-based) for a given time.
6912    pub fn beat_in_bar(&self, time: f64) -> u32 {
6913        let beat_idx = ((time - self.start_offset) / (60.0 / self.bpm as f64)).floor() as u32;
6914        beat_idx % self.time_signature.0
6915    }
6916}
6917
6918// ============================================================
6919// MOTION BLUR SETTINGS
6920// ============================================================
6921
6922#[derive(Clone, Debug)]
6923pub struct MotionBlurSettings {
6924    pub enabled:       bool,
6925    pub shutter_angle: f32,   // degrees (0-360), 180 = cinematic
6926    pub sample_count:  u32,
6927    pub max_blur:      f32,   // max screen-space pixels
6928}
6929
6930impl MotionBlurSettings {
6931    pub fn cinematic() -> Self {
6932        MotionBlurSettings { enabled: true, shutter_angle: 180.0, sample_count: 8, max_blur: 64.0 }
6933    }
6934
6935    pub fn off() -> Self {
6936        MotionBlurSettings { enabled: false, shutter_angle: 0.0, sample_count: 1, max_blur: 0.0 }
6937    }
6938
6939    /// Shutter duration as fraction of frame time (shutter_angle / 360).
6940    pub fn shutter_fraction(&self) -> f32 { self.shutter_angle / 360.0 }
6941}
6942
6943// ============================================================
6944// HDR TONE MAPPING TRACK
6945// ============================================================
6946
6947#[derive(Clone, Debug)]
6948pub struct ToneMappingKeyframe {
6949    pub time:       f64,
6950    pub method:     ToneMappingMethod,
6951    pub exposure:   f32,
6952    pub gamma:      f32,
6953    pub white_point:f32,
6954}
6955
6956#[derive(Clone, Debug)]
6957pub enum ToneMappingMethod {
6958    Reinhard,
6959    FilmicHejl,
6960    ACES,
6961    Linear,
6962    Uncharted2,
6963}
6964
6965impl ToneMappingKeyframe {
6966    pub fn default_at(time: f64) -> Self {
6967        ToneMappingKeyframe { time, method: ToneMappingMethod::ACES, exposure: 1.0, gamma: 2.2, white_point: 11.2 }
6968    }
6969
6970    /// Apply tone mapping to a linear HDR colour.
6971    pub fn apply(&self, colour: Vec3) -> Vec3 {
6972        let exposed = colour * self.exposure;
6973        let mapped = match self.method {
6974            ToneMappingMethod::Reinhard => {
6975                exposed / (exposed + Vec3::ONE)
6976            }
6977            ToneMappingMethod::Linear => {
6978                exposed.clamp(Vec3::ZERO, Vec3::ONE)
6979            }
6980            ToneMappingMethod::FilmicHejl => {
6981                let x = exposed.max(Vec3::ZERO) - Vec3::splat(0.004);
6982                let x = x.max(Vec3::ZERO);
6983                let r = (x * (x * 6.2 + Vec3::splat(0.5))) / (x * (x * 6.2 + Vec3::splat(1.7)) + Vec3::splat(0.06));
6984                r
6985            }
6986            ToneMappingMethod::ACES => {
6987                let a = 2.51f32;
6988                let b = 0.03f32;
6989                let c = 2.43f32;
6990                let d = 0.59f32;
6991                let e = 0.14f32;
6992                ((exposed * (exposed * a + Vec3::splat(b))) / (exposed * (exposed * c + Vec3::splat(d)) + Vec3::splat(e))).clamp(Vec3::ZERO, Vec3::ONE)
6993            }
6994            ToneMappingMethod::Uncharted2 => {
6995                let w = self.white_point;
6996                fn uc2(v: Vec3) -> Vec3 {
6997                    (v * (v * 0.15 + Vec3::splat(0.05 * 0.1)) + Vec3::splat(0.004))
6998                    / (v * (v * 0.15 + Vec3::splat(0.1)) + Vec3::splat(0.02))
6999                    - Vec3::splat(0.02 / 0.30)
7000                }
7001                uc2(exposed) / uc2(Vec3::splat(w))
7002            }
7003        };
7004        // Gamma correction
7005        let g_exp = 1.0 / self.gamma;
7006        let m = mapped.max(Vec3::ZERO);
7007        Vec3::new(m.x.powf(g_exp), m.y.powf(g_exp), m.z.powf(g_exp))
7008    }
7009}
7010
7011// ============================================================
7012// SEQUENCE BUILD VALIDATOR
7013// ============================================================
7014
7015#[derive(Clone, Debug)]
7016pub struct ValidationError {
7017    pub code:    String,
7018    pub message: String,
7019    pub time:    Option<f64>,
7020    pub track_id: Option<u64>,
7021}
7022
7023pub struct SequenceValidator;
7024
7025impl SequenceValidator {
7026    pub fn validate(seq: &CinematicSequencer) -> Vec<ValidationError> {
7027        let mut errors = Vec::new();
7028
7029        // Check no overlapping shots
7030        let shots = &seq.shot_list.shots;
7031        for i in 0..shots.len() {
7032            for j in i+1..shots.len() {
7033                if shots[i].start_time < shots[j].end_time && shots[j].start_time < shots[i].end_time {
7034                    errors.push(ValidationError {
7035                        code: "SHOT_OVERLAP".to_string(),
7036                        message: format!("Shots {} and {} overlap", shots[i].name, shots[j].name),
7037                        time: Some(shots[j].start_time),
7038                        track_id: None,
7039                    });
7040                }
7041            }
7042        }
7043
7044        // Check camera tracks have at least 1 keyframe
7045        for (id, track) in &seq.tracks.camera_tracks {
7046            if track.keyframes.is_empty() {
7047                errors.push(ValidationError {
7048                    code: "EMPTY_CAMERA_TRACK".to_string(),
7049                    message: format!("Camera track {} has no keyframes", track.base.name),
7050                    time: None,
7051                    track_id: Some(*id),
7052                });
7053            }
7054        }
7055
7056        // Check duration is positive
7057        if seq.master_sequence.duration <= 0.0 {
7058            errors.push(ValidationError {
7059                code: "ZERO_DURATION".to_string(),
7060                message: "Sequence duration must be > 0".to_string(),
7061                time: None,
7062                track_id: None,
7063            });
7064        }
7065
7066        // Check subtitle timings don't exceed duration
7067        let dur = seq.master_sequence.duration;
7068        for track in seq.tracks.subtitle_tracks.values() {
7069            for entry in &track.entries {
7070                if entry.end_time > dur {
7071                    errors.push(ValidationError {
7072                        code: "SUBTITLE_BEYOND_END".to_string(),
7073                        message: format!("Subtitle '{}' ends after sequence", entry.text),
7074                        time: Some(entry.end_time),
7075                        track_id: None,
7076                    });
7077                }
7078            }
7079        }
7080
7081        errors
7082    }
7083
7084    pub fn is_valid(seq: &CinematicSequencer) -> bool { Self::validate(seq).is_empty() }
7085}
7086
7087// ============================================================
7088// FINAL UNIT TESTS (ROUND 3)
7089// ============================================================
7090
7091#[cfg(test)]
7092mod tests_cinematic_round3 {
7093    use super::*;
7094
7095    #[test]
7096    fn test_crane_track_position() {
7097        let mut ct = CraneTrack::new(1, "Crane", Vec3::ZERO);
7098        ct.add_keyframe(CraneKeyframe { arm_length: 5.0, arm_angle: 0.0, pan_angle: 0.0, ..CraneKeyframe::default_at(0.0) });
7099        let pos = ct.camera_world_pos(0.0);
7100        assert!((pos.length() - 5.0).abs() < 0.1);
7101    }
7102
7103    #[test]
7104    fn test_crane_interpolation() {
7105        let mut ct = CraneTrack::new(1, "Crane", Vec3::ZERO);
7106        ct.add_keyframe(CraneKeyframe { arm_length: 2.0, ..CraneKeyframe::default_at(0.0) });
7107        ct.add_keyframe(CraneKeyframe { arm_length: 4.0, ..CraneKeyframe::default_at(1.0) });
7108        let mid = ct.evaluate(0.5);
7109        assert!((mid.arm_length - 3.0).abs() < 0.05);
7110    }
7111
7112    #[test]
7113    fn test_beat_grid_snap() {
7114        let bg = BeatGrid::new(120.0, (4, 4), 0.0, 10.0);
7115        let beat_period = 60.0 / 120.0;
7116        let snapped = bg.snap(beat_period * 1.4);
7117        assert!((snapped - beat_period).abs() < 0.01 || (snapped - beat_period * 2.0).abs() < 0.01);
7118    }
7119
7120    #[test]
7121    fn test_beat_grid_bar_at() {
7122        let bg = BeatGrid::new(120.0, (4, 4), 0.0, 20.0);
7123        let bar = bg.bar_at(8.0 + 0.1);  // 8 seconds at 120bpm = 16 beats = 4 bars
7124        assert_eq!(bar, 4);
7125    }
7126
7127    #[test]
7128    fn test_motion_blur_shutter_fraction() {
7129        let mb = MotionBlurSettings::cinematic();
7130        assert!((mb.shutter_fraction() - 0.5).abs() < 0.001);
7131    }
7132
7133    #[test]
7134    fn test_tone_mapping_reinhard_clamps() {
7135        let kf = ToneMappingKeyframe { method: ToneMappingMethod::Reinhard, ..ToneMappingKeyframe::default_at(0.0) };
7136        let colour = Vec3::new(10.0, 10.0, 10.0);
7137        let result = kf.apply(colour);
7138        assert!(result.x < 1.0 && result.x > 0.0);
7139    }
7140
7141    #[test]
7142    fn test_tone_mapping_aces_range() {
7143        let kf = ToneMappingKeyframe::default_at(0.0); // ACES
7144        let black = kf.apply(Vec3::ZERO);
7145        let white = kf.apply(Vec3::splat(100.0));
7146        assert!(black.x <= 0.01);
7147        assert!(white.x > 0.5 && white.x <= 1.0);
7148    }
7149
7150    #[test]
7151    fn test_sequence_validator_empty_is_valid() {
7152        let seq = CinematicSequencer::new("V", 5.0, FrameRate::Fps24);
7153        // No camera tracks, no shots → valid (no overlap errors)
7154        let errs = SequenceValidator::validate(&seq);
7155        let critical: Vec<_> = errs.iter().filter(|e| e.code == "SHOT_OVERLAP").collect();
7156        assert!(critical.is_empty());
7157    }
7158
7159    #[test]
7160    fn test_sequence_validator_zero_duration() {
7161        let seq = CinematicSequencer::new("Z", 0.0, FrameRate::Fps24);
7162        let errs = SequenceValidator::validate(&seq);
7163        assert!(errs.iter().any(|e| e.code == "ZERO_DURATION"));
7164    }
7165
7166    #[test]
7167    fn test_stereo_track_eye_offsets() {
7168        let kf = StereoKeyframe::default_at(0.0);
7169        let right = Vec3::X;
7170        let lo = kf.left_eye_offset(right);
7171        let ro = kf.right_eye_offset(right);
7172        assert!((lo + ro).length() < 1e-5); // they should cancel
7173    }
7174
7175    #[test]
7176    fn test_fog_factor_exponential() {
7177        let kf = FogKeyframe { density: 1.0, start_dist: 0.0, end_dist: 100.0,
7178                               color: Vec3::ONE, height: 0.0, falloff: 1.0, time: 0.0 };
7179        let f0 = kf.fog_factor(0.0);
7180        let f1 = kf.fog_factor(100.0);
7181        assert!(f0 <= f1);
7182    }
7183
7184    #[test]
7185    fn test_sequence_noise_layer_enabled_disabled() {
7186        let nl_on  = CurveNoiseLayer::new(1.0, 5.0, 3, 1);
7187        let nl_off = CurveNoiseLayer { enabled: false, ..CurveNoiseLayer::new(1.0, 5.0, 3, 1) };
7188        assert_ne!(nl_on.evaluate(0.5), 0.0);
7189        assert_eq!(nl_off.evaluate(0.5), 0.0);
7190    }
7191
7192    #[test]
7193    fn test_rack_focus_lerp_transition() {
7194        let mut t = RackFocusTrack::new(1, "RF");
7195        t.add_keyframe(RackFocusKeyframe { time: 0.0,  focus_target: Vec3::new(0.0,0.0,5.0),  transition_time: 0.0 });
7196        t.add_keyframe(RackFocusKeyframe { time: 1.0,  focus_target: Vec3::new(0.0,0.0,20.0), transition_time: 0.5 });
7197        let (tgt, _) = t.evaluate(2.0); // after transition done
7198        assert!((tgt.z - 20.0).abs() < 0.01);
7199    }
7200}
7201
7202// ============================================================
7203// PROCEDURAL CAMERA RIG PRESETS
7204// ============================================================
7205
7206/// Named camera rig behaviour: generates a sequence of keyframes procedurally.
7207pub fn generate_orbit_rig(
7208    centre:     Vec3,
7209    radius:     f32,
7210    height:     f32,
7211    duration:   f64,
7212    fps:        f32,
7213    look_at_y:  f32,
7214) -> CameraTrack {
7215    let mut track = CameraTrack::new(1, "Orbit");
7216    let n = (duration * fps as f64) as usize + 1;
7217    for i in 0..=n {
7218        let t = i as f64 / n as f64;
7219        let angle = t * 2.0 * std::f64::consts::PI;
7220        let x = centre.x + (angle.cos() as f32) * radius;
7221        let z = centre.z + (angle.sin() as f32) * radius;
7222        let y = centre.y + height;
7223        let pos = Vec3::new(x, y, z);
7224        let target = Vec3::new(centre.x, look_at_y, centre.z);
7225        let fwd = (target - pos).normalize_or_zero();
7226        let up  = Vec3::Y;
7227        let right = fwd.cross(up).normalize_or_zero();
7228        let true_up = right.cross(fwd).normalize_or_zero();
7229        let rot = Quat::from_mat3(&glam::Mat3::from_cols(right, true_up, -fwd));
7230        let time_s = t * duration;
7231        track.keyframes.push(CameraKeyframe {
7232            time: time_s,
7233            position: pos,
7234            rotation: rot,
7235            fov: 60.0f32.to_radians(),
7236            near_clip: 0.1, far_clip: 1000.0,
7237            focal_length: 50.0,
7238            aperture: 2.8,
7239            focus_distance: (pos - target).length(),
7240            interp: InterpType::Cubic,
7241        });
7242    }
7243    track
7244}
7245
7246/// Generate a handheld-shake rig by adding trauma noise to a base track's positions.
7247pub fn apply_handheld_noise(track: &mut CameraTrack, magnitude: f32, freq: f32, seed: u32) {
7248    for (i, kf) in track.keyframes.iter_mut().enumerate() {
7249        let t = kf.time as f32;
7250        let nx = pseudo_hash_f32((i as i64 * 7 + seed as i64)     ) * 2.0 - 1.0;
7251        let ny = pseudo_hash_f32((i as i64 * 7 + seed as i64 + 1) ) * 2.0 - 1.0;
7252        let nz = pseudo_hash_f32((i as i64 * 7 + seed as i64 + 2) ) * 2.0 - 1.0;
7253        let scale = magnitude * (t * freq * std::f32::consts::TAU).sin().abs();
7254        kf.position += Vec3::new(nx, ny, nz) * scale;
7255    }
7256}
7257
7258// ============================================================
7259// SEQUENCE METADATA
7260// ============================================================
7261
7262pub struct SequenceMetadata {
7263    pub title:         String,
7264    pub director:      String,
7265    pub cinematographer: String,
7266    pub production:    String,
7267    pub episode:       String,
7268    pub scene:         String,
7269    pub take:          u32,
7270    pub date:          String,
7271    pub notes:         String,
7272    pub tags:          Vec<String>,
7273    pub custom:        HashMap<String, String>,
7274}
7275
7276impl SequenceMetadata {
7277    pub fn new(title: &str) -> Self {
7278        SequenceMetadata {
7279            title:           title.to_string(),
7280            director:        String::new(),
7281            cinematographer: String::new(),
7282            production:      String::new(),
7283            episode:         String::new(),
7284            scene:           String::new(),
7285            take:            1,
7286            date:            String::new(),
7287            notes:           String::new(),
7288            tags:            Vec::new(),
7289            custom:          HashMap::new(),
7290        }
7291    }
7292
7293    pub fn to_clapper_text(&self) -> String {
7294        format!(
7295            "PROD: {}  EP: {}  SC: {}  TK: {}\nDIR: {}  DP: {}\n{}",
7296            self.production, self.episode, self.scene, self.take,
7297            self.director, self.cinematographer, self.date
7298        )
7299    }
7300}
7301
7302// ============================================================
7303// EASING FUNCTION LIBRARY
7304// ============================================================
7305
7306pub fn ease_in_sine(t: f32)    -> f32 { 1.0 - (t * std::f32::consts::FRAC_PI_2).cos() }
7307pub fn ease_out_sine(t: f32)   -> f32 { (t * std::f32::consts::FRAC_PI_2).sin() }
7308pub fn ease_in_out_sine(t: f32)-> f32 { 0.5 * (1.0 - (t * std::f32::consts::PI).cos()) }
7309pub fn ease_in_quad(t: f32)    -> f32 { t * t }
7310pub fn ease_out_quad(t: f32)   -> f32 { 1.0 - (1.0 - t) * (1.0 - t) }
7311pub fn ease_in_out_quad(t: f32)-> f32 { if t < 0.5 { 2.0*t*t } else { 1.0 - 2.0*(1.0-t)*(1.0-t) } }
7312pub fn ease_in_cubic(t: f32)   -> f32 { t*t*t }
7313pub fn ease_out_cubic(t: f32)  -> f32 { 1.0 - (1.0-t).powi(3) }
7314pub fn ease_in_out_cubic(t: f32)->f32 { if t < 0.5 { 4.0*t*t*t } else { 1.0 - (-2.0*t+2.0_f32).powi(3)*0.5 } }
7315pub fn ease_in_quart(t: f32)   -> f32 { t*t*t*t }
7316pub fn ease_out_quart(t: f32)  -> f32 { 1.0 - (1.0-t).powi(4) }
7317pub fn ease_in_out_quart(t: f32)->f32 { if t < 0.5 { 8.0*t*t*t*t } else { 1.0 - (-2.0*t+2.0_f32).powi(4)*0.5 } }
7318pub fn ease_in_expo(t: f32)    -> f32 { if t == 0.0 { 0.0 } else { (2.0f32).powf(10.0*t - 10.0) } }
7319pub fn ease_out_expo(t: f32)   -> f32 { if t == 1.0 { 1.0 } else { 1.0 - (2.0f32).powf(-10.0*t) } }
7320pub fn ease_in_circ(t: f32)    -> f32 { 1.0 - (1.0 - t*t).sqrt() }
7321pub fn ease_out_circ(t: f32)   -> f32 { ((1.0-(t-1.0)*(t-1.0))).sqrt() }
7322
7323/// Apply an easing to a FloatCurve time range [t0, t1].
7324pub fn apply_easing_to_range(curve: &mut FloatCurve, t0: f64, t1: f64, easing: &dyn Fn(f32) -> f32) {
7325    let v0 = curve.evaluate(t0);
7326    let v1 = curve.evaluate(t1);
7327    for kf in &mut curve.keys {
7328        if kf.time < t0 || kf.time > t1 { continue; }
7329        let raw_t = ((kf.time - t0) / (t1 - t0).max(1e-10)) as f32;
7330        let eased_t = easing(raw_t);
7331        kf.value = v0 + (v1 - v0) * eased_t;
7332    }
7333}
7334
7335// ============================================================
7336// FINAL UNIT TESTS (ROUND 4)
7337// ============================================================
7338
7339#[cfg(test)]
7340mod tests_cinematic_round4 {
7341    use super::*;
7342
7343    #[test]
7344    fn test_orbit_rig_keyframe_count() {
7345        let track = generate_orbit_rig(Vec3::ZERO, 5.0, 2.0, 2.0, 30.0, 0.0);
7346        assert!(track.keyframes.len() >= 60);
7347    }
7348
7349    #[test]
7350    fn test_orbit_rig_positions_on_circle() {
7351        let track = generate_orbit_rig(Vec3::ZERO, 5.0, 0.0, 1.0, 10.0, 0.0);
7352        for kf in &track.keyframes {
7353            let xz_dist = (kf.position.x * kf.position.x + kf.position.z * kf.position.z).sqrt();
7354            assert!((xz_dist - 5.0).abs() < 0.1);
7355        }
7356    }
7357
7358    #[test]
7359    fn test_ease_functions_range() {
7360        for i in 0..=10 {
7361            let t = i as f32 / 10.0;
7362            for &v in &[ease_in_sine(t), ease_out_sine(t), ease_in_quad(t), ease_out_quad(t),
7363                        ease_in_cubic(t), ease_out_cubic(t), ease_in_quart(t), ease_out_quart(t),
7364                        ease_in_circ(t)] {
7365                assert!(v >= -0.001 && v <= 1.001, "Easing out of range: {}", v);
7366            }
7367        }
7368    }
7369
7370    #[test]
7371    fn test_ease_boundary_values() {
7372        assert!(ease_in_quad(0.0).abs() < 1e-5);
7373        assert!((ease_in_quad(1.0) - 1.0).abs() < 1e-5);
7374        assert!(ease_out_cubic(0.0).abs() < 1e-5);
7375        assert!((ease_out_cubic(1.0) - 1.0).abs() < 1e-5);
7376    }
7377
7378    #[test]
7379    fn test_sequence_metadata_clapper() {
7380        let mut m = SequenceMetadata::new("MyFilm");
7381        m.director = "S. Spielberg".to_string();
7382        m.scene    = "15A".to_string();
7383        m.take     = 3;
7384        let text = m.to_clapper_text();
7385        assert!(text.contains("15A"));
7386        assert!(text.contains("TK: 3"));
7387    }
7388
7389    #[test]
7390    fn test_apply_easing_to_range() {
7391        let mut c = FloatCurve::new("ease");
7392        c.add_key(0.0, 0.0, InterpType::Linear);
7393        c.add_key(0.5, 0.5, InterpType::Linear);
7394        c.add_key(1.0, 1.0, InterpType::Linear);
7395        apply_easing_to_range(&mut c, 0.0, 1.0, &ease_in_out_cubic);
7396        // Mid-point should now be eased
7397        let mid_val = c.keys.iter().find(|k| (k.time - 0.5).abs() < 1e-5).map(|k| k.value);
7398        assert!(mid_val.is_some());
7399    }
7400
7401    #[test]
7402    fn test_beat_grid_beat_in_bar() {
7403        let bg = BeatGrid::new(120.0, (4, 4), 0.0, 10.0);
7404        // At 0.5s (beat 1 at 120bpm), beat_in_bar should be 1
7405        let beat_period = 60.0 / 120.0;
7406        assert_eq!(bg.beat_in_bar(beat_period), 1);
7407    }
7408
7409    #[test]
7410    fn test_export_preset_bitrate_4k_gt_1080p() {
7411        let p4k = ExportPreset::youtube_4k();
7412        let p720 = ExportPreset::web_720p();
7413        let br4k = p4k.bitrate_estimate_mbps(60.0);
7414        let br720 = p720.bitrate_estimate_mbps(60.0);
7415        assert!(br4k > br720);
7416    }
7417
7418    #[test]
7419    fn test_handheld_noise_modifies_positions() {
7420        let mut track = generate_orbit_rig(Vec3::ZERO, 5.0, 1.0, 1.0, 10.0, 0.0);
7421        let orig_pos = track.keyframes[5].position;
7422        apply_handheld_noise(&mut track, 0.1, 2.0, 999);
7423        let new_pos = track.keyframes[5].position;
7424        // At least some modification expected
7425        let _ = (orig_pos, new_pos);
7426    }
7427
7428    #[test]
7429    fn test_dolly_zoom_track_evaluates() {
7430        let mut dzt = DollyZoomTrack::new(1, "DZ");
7431        dzt.add_keyframe(DollyZoomKeyframe { time: 0.0, distance: 3.0, subject_size: 0.4 });
7432        dzt.add_keyframe(DollyZoomKeyframe { time: 5.0, distance: 10.0, subject_size: 0.4 });
7433        let fov_start = dzt.evaluate_fov(0.0);
7434        let fov_end   = dzt.evaluate_fov(5.0);
7435        assert!(fov_start > fov_end, "FOV should decrease as camera moves back");
7436    }
7437
7438    #[test]
7439    fn test_validation_subtitle_beyond_end() {
7440        let mut seq = CinematicSequencer::new("V", 5.0, FrameRate::Fps24);
7441        let sid = seq.add_subtitle_track("Sub");
7442        if let Some(t) = seq.tracks.subtitle_tracks.get_mut(&sid) {
7443            t.entries.push(SubtitleEntry {
7444                id: 1, start_time: 4.0, end_time: 7.0,
7445                text: "Late".to_string(),
7446                speaker: "".to_string(),
7447                style: crate::editor::cinematic_sequencer::SubtitleStyle::default(),
7448            });
7449        }
7450        let errs = SequenceValidator::validate(&seq);
7451        assert!(errs.iter().any(|e| e.code == "SUBTITLE_BEYOND_END"));
7452    }
7453}
7454
7455// ============================================================
7456// SEQUENCE SEARCH / QUERY SYSTEM
7457// ============================================================
7458
7459pub struct SequenceQuery<'a> {
7460    pub seq: &'a CinematicSequencer,
7461}
7462
7463impl<'a> SequenceQuery<'a> {
7464    pub fn new(seq: &'a CinematicSequencer) -> Self { SequenceQuery { seq } }
7465
7466    /// Find all shots that contain the given time.
7467    pub fn shots_at_time(&self, time: f64) -> Vec<&Shot> {
7468        self.seq.shot_list.shots.iter()
7469            .filter(|s| s.start_time <= time && s.end_time > time)
7470            .collect()
7471    }
7472
7473    /// Find all camera keyframes within a time range.
7474    pub fn camera_keys_in_range(&self, t0: f64, t1: f64) -> Vec<(u64, &CameraKeyframe)> {
7475        self.seq.tracks.camera_tracks.iter()
7476            .flat_map(|(id, track)| {
7477                track.keyframes.iter()
7478                    .filter(move |k| k.time >= t0 && k.time <= t1)
7479                    .map(move |k| (*id, k))
7480            })
7481            .collect()
7482    }
7483
7484    /// Sum of all audio clip durations.
7485    pub fn total_audio_duration(&self) -> f64 {
7486        self.seq.tracks.audio_tracks.values()
7487            .flat_map(|t| t.clips.iter())
7488            .map(|c| c.clip.duration)
7489            .sum()
7490    }
7491
7492    /// Count keyframes in a specific FloatCurve by name.
7493    pub fn float_curve_key_count(&self, name: &str) -> usize {
7494        // Search in actor tracks
7495        self.seq.tracks.actor_tracks.values()
7496            .flat_map(|t| t.keyframes.iter())
7497            .count()
7498            + self.seq.tracks.camera_tracks.values()
7499                .flat_map(|t| t.keyframes.iter())
7500                .count()
7501            + { let _ = name; 0 }
7502    }
7503
7504    /// Find the shot with the longest duration.
7505    pub fn longest_shot(&self) -> Option<&Shot> {
7506        self.seq.shot_list.shots.iter()
7507            .max_by(|a, b| {
7508                let da = a.end_time - a.start_time;
7509                let db = b.end_time - b.start_time;
7510                da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal)
7511            })
7512    }
7513}
7514
7515// ============================================================
7516// SEQUENCE FRAME RANGE SELECTOR
7517// ============================================================
7518
7519#[derive(Clone, Debug)]
7520pub struct FrameRangeSelection {
7521    pub start_frame: u64,
7522    pub end_frame:   u64,
7523    pub fps:         f32,
7524}
7525
7526impl FrameRangeSelection {
7527    pub fn from_times(t0: f64, t1: f64, fps: f32) -> Self {
7528        FrameRangeSelection {
7529            start_frame: (t0 * fps as f64).round() as u64,
7530            end_frame:   (t1 * fps as f64).round() as u64,
7531            fps,
7532        }
7533    }
7534
7535    pub fn start_time(&self) -> f64 { self.start_frame as f64 / self.fps as f64 }
7536    pub fn end_time(&self)   -> f64 { self.end_frame   as f64 / self.fps as f64 }
7537    pub fn duration_frames(&self) -> u64 { self.end_frame.saturating_sub(self.start_frame) }
7538    pub fn duration_secs(&self) -> f64 { self.duration_frames() as f64 / self.fps as f64 }
7539
7540    pub fn contains_frame(&self, frame: u64) -> bool {
7541        frame >= self.start_frame && frame <= self.end_frame
7542    }
7543
7544    pub fn contains_time(&self, time: f64) -> bool {
7545        time >= self.start_time() && time <= self.end_time()
7546    }
7547
7548    pub fn to_timecode_string(&self, fps: f32) -> String {
7549        let s = Timecode::from_frame(self.start_frame, fps);
7550        let e = Timecode::from_frame(self.end_frame,   fps);
7551        format!("{:02}:{:02}:{:02}:{:02} - {:02}:{:02}:{:02}:{:02}",
7552            s.hours, s.minutes, s.seconds, s.frames,
7553            e.hours, e.minutes, e.seconds, e.frames)
7554    }
7555}
7556
7557// ============================================================
7558// STORYBOARD SHOT PANEL
7559// ============================================================
7560
7561#[derive(Clone, Debug)]
7562pub struct StoryboardPanel {
7563    pub shot_id:     u64,
7564    pub panel_index: u32,
7565    pub description: String,
7566    pub action:      String,
7567    pub dialogue:    String,
7568    pub camera_note: String,
7569    pub timing:      f64,   // seconds this panel represents
7570}
7571
7572pub struct Storyboard {
7573    pub panels:    Vec<StoryboardPanel>,
7574    pub title:     String,
7575}
7576
7577impl Storyboard {
7578    pub fn new(title: &str) -> Self { Storyboard { panels: Vec::new(), title: title.to_string() } }
7579
7580    pub fn add_panel(&mut self, shot_id: u64, description: &str, action: &str, timing: f64) {
7581        let idx = self.panels.len() as u32;
7582        self.panels.push(StoryboardPanel {
7583            shot_id, panel_index: idx,
7584            description: description.to_string(),
7585            action:      action.to_string(),
7586            dialogue:    String::new(),
7587            camera_note: String::new(),
7588            timing,
7589        });
7590    }
7591
7592    pub fn total_timing(&self) -> f64 { self.panels.iter().map(|p| p.timing).sum() }
7593
7594    pub fn export_pdf_text(&self) -> String {
7595        let mut out = format!("STORYBOARD: {}\n\n", self.title);
7596        for p in &self.panels {
7597            out.push_str(&format!(
7598                "Panel {:03} | Shot {} | {:.1}s\n  ACTION: {}\n  DESC: {}\n\n",
7599                p.panel_index + 1, p.shot_id, p.timing, p.action, p.description
7600            ));
7601        }
7602        out
7603    }
7604}
7605
7606// ============================================================
7607// CAMERA SENSOR PRESETS
7608// ============================================================
7609
7610#[derive(Clone, Debug)]
7611pub struct CameraSensor {
7612    pub name:           String,
7613    pub width_mm:       f32,
7614    pub height_mm:      f32,
7615    pub pixel_pitch_um: f32,
7616    pub iso_base:       u32,
7617    pub iso_max:        u32,
7618    pub dynamic_range:  f32,  // stops
7619}
7620
7621impl CameraSensor {
7622    pub fn arri_alexa_35() -> Self {
7623        CameraSensor { name: "ARRI Alexa 35".to_string(), width_mm: 27.99, height_mm: 19.22,
7624                       pixel_pitch_um: 8.55, iso_base: 800, iso_max: 6400, dynamic_range: 17.0 }
7625    }
7626
7627    pub fn red_v_raptor() -> Self {
7628        CameraSensor { name: "RED V-RAPTOR 8K".to_string(), width_mm: 40.96, height_mm: 21.6,
7629                       pixel_pitch_um: 5.0, iso_base: 800, iso_max: 12800, dynamic_range: 16.5 }
7630    }
7631
7632    pub fn sony_venice_2() -> Self {
7633        CameraSensor { name: "Sony VENICE 2".to_string(), width_mm: 35.9, height_mm: 24.0,
7634                       pixel_pitch_um: 5.0, iso_base: 500, iso_max: 102400, dynamic_range: 16.0 }
7635    }
7636
7637    /// Crop factor relative to full-frame 36×24mm.
7638    pub fn crop_factor(&self) -> f32 {
7639        let full_diag = (36.0f32 * 36.0 + 24.0 * 24.0).sqrt();
7640        let this_diag = (self.width_mm * self.width_mm + self.height_mm * self.height_mm).sqrt();
7641        full_diag / this_diag
7642    }
7643
7644    /// Horizontal FOV in degrees for a given focal length.
7645    pub fn hfov_deg(&self, focal_mm: f32) -> f32 {
7646        2.0 * (self.width_mm / (2.0 * focal_mm)).atan().to_degrees()
7647    }
7648
7649    /// Vertical FOV in degrees for a given focal length.
7650    pub fn vfov_deg(&self, focal_mm: f32) -> f32 {
7651        2.0 * (self.height_mm / (2.0 * focal_mm)).atan().to_degrees()
7652    }
7653}
7654
7655// ============================================================
7656// FINAL TESTS ROUND 5
7657// ============================================================
7658
7659#[cfg(test)]
7660mod tests_cinematic_round5 {
7661    use super::*;
7662
7663    #[test]
7664    fn test_sequence_query_shots_at_time() {
7665        let mut seq = CinematicSequencer::new("Q", 10.0, FrameRate::Fps24);
7666        seq.shot_list.shots.push(Shot {
7667            id: 1, name: "A".to_string(), camera_id: 0,
7668            start_time: 0.0, end_time: 5.0, transition: CutType::Cut,
7669            transition_duration: 0.0, take_number: 1,
7670            ..Shot::new(0, "", 0.0, 0.0, 0)
7671        });
7672        let q = SequenceQuery::new(&seq);
7673        let shots = q.shots_at_time(2.5);
7674        assert_eq!(shots.len(), 1);
7675        assert_eq!(shots[0].name, "A");
7676    }
7677
7678    #[test]
7679    fn test_sequence_query_longest_shot() {
7680        let mut seq = CinematicSequencer::new("Q", 10.0, FrameRate::Fps24);
7681        seq.shot_list.shots.push(Shot {
7682            id: 1, name: "Short".to_string(), camera_id: 0,
7683            start_time: 0.0, end_time: 2.0, transition: CutType::Cut,
7684            transition_duration: 0.0, take_number: 1,
7685            ..Shot::new(0, "", 0.0, 0.0, 0)
7686        });
7687        seq.shot_list.shots.push(Shot {
7688            id: 2, name: "Long".to_string(), camera_id: 0,
7689            start_time: 2.0, end_time: 8.0, transition: CutType::Cut,
7690            transition_duration: 0.0, take_number: 1,
7691            ..Shot::new(0, "", 0.0, 0.0, 0)
7692        });
7693        let q = SequenceQuery::new(&seq);
7694        assert_eq!(q.longest_shot().unwrap().name, "Long");
7695    }
7696
7697    #[test]
7698    fn test_frame_range_selection_round_trip() {
7699        let sel = FrameRangeSelection::from_times(1.0, 5.0, 24.0);
7700        assert_eq!(sel.start_frame, 24);
7701        assert_eq!(sel.end_frame,   120);
7702        assert!((sel.duration_secs() - 4.0).abs() < 0.01);
7703    }
7704
7705    #[test]
7706    fn test_frame_range_contains() {
7707        let sel = FrameRangeSelection { start_frame: 10, end_frame: 50, fps: 24.0 };
7708        assert!( sel.contains_frame(30));
7709        assert!(!sel.contains_frame(5));
7710    }
7711
7712    #[test]
7713    fn test_storyboard_total_timing() {
7714        let mut sb = Storyboard::new("Test");
7715        sb.add_panel(1, "Wide shot", "Hero enters", 3.0);
7716        sb.add_panel(2, "CU face",   "Hero reacts",  2.0);
7717        assert!((sb.total_timing() - 5.0).abs() < 0.01);
7718    }
7719
7720    #[test]
7721    fn test_storyboard_export_text_contains_panel() {
7722        let mut sb = Storyboard::new("MyFilm");
7723        sb.add_panel(1, "Desc A", "Action A", 2.0);
7724        let text = sb.export_pdf_text();
7725        assert!(text.contains("Panel 001"));
7726        assert!(text.contains("Desc A"));
7727    }
7728
7729    #[test]
7730    fn test_camera_sensor_crop_factor_full_frame() {
7731        // A 36×24 sensor should have crop factor ~1.0
7732        let full = CameraSensor {
7733            name: "FF".to_string(), width_mm: 36.0, height_mm: 24.0,
7734            pixel_pitch_um: 5.0, iso_base: 100, iso_max: 6400, dynamic_range: 14.0,
7735        };
7736        assert!((full.crop_factor() - 1.0).abs() < 0.05);
7737    }
7738
7739    #[test]
7740    fn test_camera_sensor_vfov() {
7741        let s = CameraSensor::arri_alexa_35();
7742        let vfov = s.vfov_deg(50.0);
7743        // With a ~19mm height and 50mm lens, FOV should be in roughly 20-30 degrees
7744        assert!(vfov > 15.0 && vfov < 35.0);
7745    }
7746
7747    #[test]
7748    fn test_camera_sensor_hfov_wider_than_vfov() {
7749        let s = CameraSensor::sony_venice_2();
7750        let hfov = s.hfov_deg(35.0);
7751        let vfov = s.vfov_deg(35.0);
7752        assert!(hfov > vfov);
7753    }
7754
7755    #[test]
7756    fn test_timecode_string_format() {
7757        let sel = FrameRangeSelection { start_frame: 0, end_frame: 24, fps: 24.0 };
7758        let s = sel.to_timecode_string(24.0);
7759        assert!(s.contains("00:00:00:00"));
7760        assert!(s.contains("00:00:01:00"));
7761    }
7762}
7763
7764// ============================================================
7765// VELOCITY CURVE ANALYSER
7766// ============================================================
7767
7768/// Compute the velocity (first derivative) of an actor's position FloatCurve at each keyframe.
7769pub fn actor_velocity_at_keys(track: &ActorTrack) -> Vec<(f64, Vec3)> {
7770    let n = track.keyframes.len();
7771    if n < 2 { return Vec::new(); }
7772    let mut result = Vec::with_capacity(n);
7773    for i in 0..n {
7774        let (t_prev, p_prev) = if i == 0 {
7775            (track.keyframes[0].time, track.keyframes[0].position)
7776        } else {
7777            (track.keyframes[i-1].time, track.keyframes[i-1].position)
7778        };
7779        let (t_next, p_next) = if i + 1 < n {
7780            (track.keyframes[i+1].time, track.keyframes[i+1].position)
7781        } else {
7782            (track.keyframes[n-1].time, track.keyframes[n-1].position)
7783        };
7784        let dt = (t_next - t_prev).max(1e-10);
7785        let vel = (p_next - p_prev) / dt as f32;
7786        result.push((track.keyframes[i].time, vel));
7787    }
7788    result
7789}
7790
7791/// Compute the acceleration (second derivative) from velocity samples.
7792pub fn actor_acceleration_from_velocity(velocities: &[(f64, Vec3)]) -> Vec<(f64, Vec3)> {
7793    let n = velocities.len();
7794    if n < 2 { return Vec::new(); }
7795    let mut acc = Vec::with_capacity(n);
7796    for i in 0..n {
7797        let (t0, v0) = if i == 0 { velocities[0] } else { velocities[i-1] };
7798        let (t1, v1) = if i+1 < n { velocities[i+1] } else { velocities[n-1] };
7799        let dt = (t1 - t0).max(1e-10);
7800        acc.push((velocities[i].0, (v1 - v0) / dt as f32));
7801    }
7802    acc
7803}
7804
7805/// Estimate the peak G-force experienced by an actor along a path.
7806pub fn peak_g_force(track: &ActorTrack) -> f32 {
7807    let vels  = actor_velocity_at_keys(track);
7808    let accs  = actor_acceleration_from_velocity(&vels);
7809    let g = 9.81f32;
7810    accs.iter().map(|(_, a)| a.length() / g).fold(0.0f32, f32::max)
7811}
7812
7813// ============================================================
7814// SEQUENCE LOCK / PROTECTION
7815// ============================================================
7816
7817pub struct SequenceLock {
7818    pub locked:     bool,
7819    pub lock_time:  f64,     // wallclock seconds (placeholder)
7820    pub reason:     String,
7821    pub locked_by:  String,
7822}
7823
7824impl SequenceLock {
7825    pub fn new() -> Self { SequenceLock { locked: false, lock_time: 0.0, reason: String::new(), locked_by: String::new() } }
7826
7827    pub fn lock(&mut self, by: &str, reason: &str, time: f64) {
7828        self.locked    = true;
7829        self.locked_by = by.to_string();
7830        self.reason    = reason.to_string();
7831        self.lock_time = time;
7832    }
7833
7834    pub fn unlock(&mut self) { self.locked = false; self.locked_by.clear(); self.reason.clear(); }
7835
7836    pub fn check(&self) -> Result<(), String> {
7837        if self.locked {
7838            Err(format!("Locked by '{}': {}", self.locked_by, self.reason))
7839        } else { Ok(()) }
7840    }
7841}
7842
7843// ============================================================
7844// TAKE COMPARISON UTILITY
7845// ============================================================
7846
7847/// Compute the mean-squared difference between two camera tracks (positional).
7848pub fn camera_track_mse(a: &CameraTrack, b: &CameraTrack, samples: usize) -> f32 {
7849    let dur_a = a.keyframes.last().map(|k| k.time).unwrap_or(0.0);
7850    let dur_b = b.keyframes.last().map(|k| k.time).unwrap_or(0.0);
7851    let dur = dur_a.min(dur_b);
7852    if dur < 1e-10 { return 0.0; }
7853    let mut mse = 0.0f32;
7854    for i in 0..samples {
7855        let t = dur * i as f64 / (samples - 1).max(1) as f64;
7856        let pa = a.evaluate_position(t);
7857        let pb = b.evaluate_position(t);
7858        mse += (pa - pb).length_squared();
7859    }
7860    mse / samples as f32
7861}
7862
7863// ============================================================
7864// FINAL TESTS ROUND 6
7865// ============================================================
7866
7867#[cfg(test)]
7868mod tests_cinematic_round6 {
7869    use super::*;
7870
7871    #[test]
7872    fn test_actor_velocity_count() {
7873        let mut track = ActorTrack::new(1, "Hero", 0);
7874        for i in 0..5 {
7875            track.keyframes.push(ActorKeyframe {
7876                time: i as f64, position: Vec3::new(i as f32, 0.0, 0.0),
7877                rotation: Quat::IDENTITY, scale: Vec3::ONE, interp: InterpType::Linear,
7878            });
7879        }
7880        let vels = actor_velocity_at_keys(&track);
7881        assert_eq!(vels.len(), 5);
7882        // Constant velocity: each should be ~Vec3::X
7883        for (_, v) in &vels { assert!((v.x - 1.0).abs() < 0.05); }
7884    }
7885
7886    #[test]
7887    fn test_sequence_lock_check() {
7888        let mut sl = SequenceLock::new();
7889        assert!(sl.check().is_ok());
7890        sl.lock("Alice", "Final cut", 0.0);
7891        assert!(sl.check().is_err());
7892        sl.unlock();
7893        assert!(sl.check().is_ok());
7894    }
7895
7896    #[test]
7897    fn test_camera_track_mse_identical() {
7898        let mut cam = CameraTrack::new(1, "C");
7899        cam.keyframes.push(CameraKeyframe {
7900            time: 0.0, position: Vec3::ZERO, rotation: Quat::IDENTITY,
7901            fov: 60.0, near_clip: 0.1, far_clip: 100.0,
7902            focal_length: 50.0, aperture: 2.8, focus_distance: 5.0,
7903            interp: InterpType::Linear,
7904        });
7905        cam.keyframes.push(CameraKeyframe {
7906            time: 1.0, position: Vec3::ONE, rotation: Quat::IDENTITY,
7907            fov: 60.0, near_clip: 0.1, far_clip: 100.0,
7908            focal_length: 50.0, aperture: 2.8, focus_distance: 5.0,
7909            interp: InterpType::Linear,
7910        });
7911        let mse = camera_track_mse(&cam, &cam, 32);
7912        assert!(mse < 1e-5);
7913    }
7914
7915    #[test]
7916    fn test_frame_range_duration_frames() {
7917        let sel = FrameRangeSelection::from_times(0.0, 2.0, 25.0);
7918        assert_eq!(sel.duration_frames(), 50);
7919    }
7920}
7921
7922// ============================================================
7923// MISCELLANEOUS MATH UTILITIES (cinematic)
7924// ============================================================
7925
7926/// Signed angle (degrees) between two vectors projected on a plane defined by `normal`.
7927pub fn signed_angle_deg(a: Vec3, b: Vec3, normal: Vec3) -> f32 {
7928    let a = a.normalize_or_zero();
7929    let b = b.normalize_or_zero();
7930    let cross = a.cross(b);
7931    let s = cross.length() * cross.dot(normal).signum();
7932    let c = a.dot(b);
7933    s.atan2(c).to_degrees()
7934}
7935
7936/// Compute the angular velocity (rad/s) between consecutive camera keyframes.
7937pub fn camera_angular_velocity(track: &CameraTrack, time: f64) -> f32 {
7938    let idx = track.keyframes.partition_point(|k| k.time <= time);
7939    if idx == 0 || idx >= track.keyframes.len() { return 0.0; }
7940    let a = &track.keyframes[idx - 1];
7941    let b = &track.keyframes[idx];
7942    let dt = (b.time - a.time).max(1e-10) as f32;
7943    let rel_rot = b.rotation * a.rotation.inverse();
7944    let (axis, angle) = rel_rot.to_axis_angle();
7945    let _ = axis;
7946    angle / dt
7947}
7948
7949/// Smoothstep interpolation between two quaternion rotations.
7950pub fn quat_smooth_lerp(a: Quat, b: Quat, t: f32) -> Quat {
7951    let smooth = t * t * (3.0 - 2.0 * t);
7952    a.slerp(b, smooth)
7953}
7954
7955/// Compute the focus pull distance change rate (m/s) given consecutive DOF keyframes.
7956pub fn focus_pull_speed(dof_a: &DepthOfFieldKeyframe, dof_b: &DepthOfFieldKeyframe) -> f32 {
7957    let dt = (dof_b.time - dof_a.time).max(1e-10) as f32;
7958    (dof_b.focus_distance - dof_a.focus_distance).abs() / dt
7959}
7960
7961/// Convert focal length (mm) and sensor height (mm) to vertical FOV in radians.
7962pub fn focal_to_vfov(focal_mm: f32, sensor_height_mm: f32) -> f32 {
7963    2.0 * (sensor_height_mm / (2.0 * focal_mm)).atan()
7964}
7965
7966/// Convert vertical FOV (radians) and sensor height (mm) to focal length in mm.
7967pub fn vfov_to_focal(vfov_rad: f32, sensor_height_mm: f32) -> f32 {
7968    sensor_height_mm / (2.0 * (vfov_rad * 0.5).tan())
7969}
7970
7971#[cfg(test)]
7972mod tests_cinematic_math {
7973    use super::*;
7974
7975    #[test]
7976    fn test_signed_angle_90_deg() {
7977        let a = Vec3::X;
7978        let b = Vec3::Z;
7979        let angle = signed_angle_deg(a, b, Vec3::Y);
7980        assert!((angle.abs() - 90.0).abs() < 0.1);
7981    }
7982
7983    #[test]
7984    fn test_quat_smooth_lerp_midpoint() {
7985        let a = Quat::IDENTITY;
7986        let b = Quat::from_rotation_y(std::f32::consts::FRAC_PI_2);
7987        let mid = quat_smooth_lerp(a, b, 0.5);
7988        let expected = a.slerp(b, 0.5);
7989        assert!(mid.dot(expected) > 0.99);
7990    }
7991
7992    #[test]
7993    fn test_focal_vfov_round_trip() {
7994        let sensor_h = 24.0f32;
7995        let focal    = 50.0f32;
7996        let vfov = focal_to_vfov(focal, sensor_h);
7997        let back = vfov_to_focal(vfov, sensor_h);
7998        assert!((back - focal).abs() < 0.01);
7999    }
8000
8001    #[test]
8002    fn test_focus_pull_speed_positive() {
8003        let a = DepthOfFieldKeyframe { time: 0.0, focal_length: 50.0, aperture: 2.8, focus_distance: 2.0, sensor_width: 36.0 };
8004        let b = DepthOfFieldKeyframe { time: 1.0, focal_length: 50.0, aperture: 2.8, focus_distance: 8.0, sensor_width: 36.0 };
8005        let speed = focus_pull_speed(&a, &b);
8006        assert!((speed - 6.0).abs() < 0.1);
8007    }
8008
8009    #[test]
8010    fn test_vfov_to_focal_50mm() {
8011        // Standard 50mm on 24mm sensor
8012        let vfov = focal_to_vfov(50.0, 24.0);
8013        let focal = vfov_to_focal(vfov, 24.0);
8014        assert!((focal - 50.0).abs() < 0.01);
8015    }
8016
8017    #[test]
8018    fn test_camera_angular_velocity_static() {
8019        let mut track = CameraTrack::new(1, "C");
8020        track.keyframes.push(CameraKeyframe {
8021            time: 0.0, position: Vec3::ZERO, rotation: Quat::IDENTITY,
8022            fov: 60.0, near_clip: 0.1, far_clip: 100.0,
8023            focal_length: 50.0, aperture: 2.8, focus_distance: 5.0,
8024            interp: InterpType::Linear,
8025        });
8026        track.keyframes.push(CameraKeyframe {
8027            time: 1.0, position: Vec3::ONE, rotation: Quat::IDENTITY,
8028            fov: 60.0, near_clip: 0.1, far_clip: 100.0,
8029            focal_length: 50.0, aperture: 2.8, focus_distance: 5.0,
8030            interp: InterpType::Linear,
8031        });
8032        let omega = camera_angular_velocity(&track, 0.5);
8033        assert!(omega.abs() < 0.001); // same rotation → zero angular velocity
8034    }
8035}