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        // Hold k0's opacity, then fade over the `fade` seconds that end at
1918        // k1. Using k0's fade from k0's time made a keyframe with fade 0
1919        // jump straight to the next value, so "visible at 0" was invisible
1920        // at 0.1.
1921        if k1.fade <= 0.0 {
1922            return k0.opacity;
1923        }
1924        let start = (k1.time - k1.fade).max(k0.time);
1925        let span = (k1.time - start).max(1e-9);
1926        let t = ((time - start) / span).clamp(0.0, 1.0) as f32;
1927        lerp(k0.opacity, k1.opacity, smooth_step(t))
1928    }
1929
1930    pub fn is_visible_at(&self, time: f64) -> bool {
1931        self.evaluate_opacity(time) > 0.001
1932    }
1933}
1934
1935// ============================================================
1936// TIME DILATION TRACK
1937// ============================================================
1938
1939#[derive(Clone, Debug)]
1940pub struct TimeDilationKeyframe {
1941    pub time:          f64,
1942    pub time_scale:    f32,  // 1.0 = normal, 0.5 = half speed, 0.0 = freeze
1943    pub ease_duration: f64,
1944    pub interp:        InterpType,
1945}
1946
1947impl TimeDilationKeyframe {
1948    pub fn new(time: f64, scale: f32) -> Self {
1949        TimeDilationKeyframe { time, time_scale: scale, ease_duration: 0.5, interp: InterpType::Cubic }
1950    }
1951}
1952
1953#[derive(Clone, Debug)]
1954pub struct TimeDilationTrack {
1955    pub base:      TrackBase,
1956    pub keyframes: Vec<TimeDilationKeyframe>,
1957    pub global:    bool, // affects entire world vs just current sequence
1958}
1959
1960impl TimeDilationTrack {
1961    pub fn new(id: u64, name: &str) -> Self {
1962        TimeDilationTrack {
1963            base: TrackBase::new(id, name, TrackKind::TimeDilation),
1964            keyframes: Vec::new(),
1965            global: false,
1966        }
1967    }
1968
1969    pub fn add_keyframe(&mut self, kf: TimeDilationKeyframe) {
1970        let idx = self.keyframes.partition_point(|k| k.time < kf.time);
1971        self.keyframes.insert(idx, kf);
1972    }
1973
1974    pub fn evaluate(&self, time: f64) -> f32 {
1975        let n = self.keyframes.len();
1976        if n == 0 { return 1.0; }
1977        if n == 1 { return self.keyframes[0].time_scale; }
1978        let idx = self.keyframes.partition_point(|k| k.time <= time);
1979        if idx == 0 { return self.keyframes[0].time_scale; }
1980        if idx >= n { return self.keyframes[n-1].time_scale; }
1981        let k0 = &self.keyframes[idx-1];
1982        let k1 = &self.keyframes[idx];
1983        let dt = (k1.time - k0.time) as f32;
1984        let t  = ((time - k0.time) as f32) / dt.max(EPSILON);
1985        match k0.interp {
1986            InterpType::Constant | InterpType::Stepped => k0.time_scale,
1987            InterpType::Linear   => lerp(k0.time_scale, k1.time_scale, t),
1988            InterpType::Cubic | InterpType::Bezier => lerp(k0.time_scale, k1.time_scale, smoother_step(t)),
1989        }
1990    }
1991
1992    /// Integrate dilation to compute actual world time at a given sequence time
1993    pub fn world_time_at(&self, sequence_time: f64, dt: f64) -> f64 {
1994        let steps = (sequence_time / dt).ceil() as usize;
1995        let mut world_t = 0.0_f64;
1996        for i in 0..steps {
1997            let t = i as f64 * dt;
1998            let scale = self.evaluate(t) as f64;
1999            world_t += dt * scale;
2000        }
2001        world_t
2002    }
2003}
2004
2005// ============================================================
2006// SHOT LIST / TAKE SYSTEM
2007// ============================================================
2008
2009#[derive(Clone, Debug, PartialEq)]
2010pub enum CutType { Cut, Dissolve, Fade, Wipe }
2011
2012#[derive(Clone, Debug)]
2013pub struct Shot {
2014    pub id:                  u64,
2015    pub name:                String,
2016    pub start_time:          f64,
2017    pub end_time:            f64,
2018    pub camera_id:           u64,
2019    pub scene_name:          String,
2020    pub take_number:         u32,
2021    pub is_selected:         bool,
2022    pub notes:               String,
2023    pub rating:              u8,
2024    pub color_flag:          Vec4,
2025    pub transition:          CutType,
2026    pub transition_duration: f64,
2027}
2028
2029impl Shot {
2030    pub fn new(id: u64, name: &str, start: f64, end: f64, camera_id: u64) -> Self {
2031        Shot {
2032            id, name: name.to_string(),
2033            start_time: start, end_time: end,
2034            camera_id,
2035            scene_name: String::new(),
2036            take_number: 1,
2037            is_selected: false,
2038            notes: String::new(),
2039            rating: 3,
2040            color_flag: Vec4::new(1.0, 1.0, 1.0, 1.0),
2041            transition: CutType::Cut,
2042            transition_duration: 0.0,
2043        }
2044    }
2045
2046    pub fn duration(&self) -> f64 { self.end_time - self.start_time }
2047}
2048
2049#[derive(Clone, Debug)]
2050pub struct Take {
2051    pub take_number:  u32,
2052    pub timestamp:    u64,
2053    pub notes:        String,
2054    pub is_best_take: bool,
2055}
2056
2057#[derive(Clone, Debug)]
2058pub struct ShotList {
2059    pub shots:   Vec<Shot>,
2060    pub takes:   HashMap<u64, Vec<Take>>,   // shot_id -> takes
2061    pub current_shot: Option<u64>,
2062}
2063
2064impl ShotList {
2065    pub fn new() -> Self {
2066        ShotList { shots: Vec::new(), takes: HashMap::new(), current_shot: None }
2067    }
2068
2069    pub fn add_shot(&mut self, shot: Shot) {
2070        let id = shot.id;
2071        self.shots.push(shot);
2072        self.takes.insert(id, vec![Take {
2073            take_number: 1, timestamp: 0, notes: String::new(), is_best_take: false,
2074        }]);
2075    }
2076
2077    pub fn shot_at_time(&self, time: f64) -> Option<&Shot> {
2078        self.shots.iter().find(|s| time >= s.start_time && time < s.end_time)
2079    }
2080
2081    pub fn add_take(&mut self, shot_id: u64, notes: &str) -> u32 {
2082        let takes = self.takes.entry(shot_id).or_default();
2083        let num = takes.len() as u32 + 1;
2084        takes.push(Take { take_number: num, timestamp: 0, notes: notes.to_string(), is_best_take: false });
2085        num
2086    }
2087
2088    pub fn sort_by_time(&mut self) {
2089        self.shots.sort_by(|a, b| a.start_time.partial_cmp(&b.start_time).unwrap_or(std::cmp::Ordering::Equal));
2090    }
2091}
2092
2093// ============================================================
2094// CINEMATIC EVENTS
2095// ============================================================
2096
2097#[derive(Clone, Debug)]
2098pub struct ScreenFlashEvent {
2099    pub time:     f64,
2100    pub color:    Vec4,
2101    pub duration: f64,
2102    pub intensity: f32,
2103}
2104
2105#[derive(Clone, Debug)]
2106pub struct RumbleEvent {
2107    pub time:      f64,
2108    pub duration:  f64,
2109    pub intensity: f32,
2110    pub frequency: f32,
2111    pub decay:     f32,
2112}
2113
2114impl RumbleEvent {
2115    pub fn intensity_at(&self, time: f64) -> f32 {
2116        let local = time - self.time;
2117        if local < 0.0 || local > self.duration { return 0.0; }
2118        let envelope = (-self.decay * local as f32).exp();
2119        let osc = (local as f32 * self.frequency * std::f32::consts::TAU).sin();
2120        self.intensity * envelope * osc.abs()
2121    }
2122}
2123
2124#[derive(Clone, Debug)]
2125pub struct SlowMotionEvent {
2126    pub time:        f64,
2127    pub duration:    f64,
2128    pub time_scale:  f32,
2129    pub ease_in:     f64,
2130    pub ease_out:    f64,
2131}
2132
2133impl SlowMotionEvent {
2134    pub fn scale_at(&self, time: f64) -> f32 {
2135        let local = time - self.time;
2136        if local < 0.0 || local > self.duration { return 1.0; }
2137        let in_phase  = (local / self.ease_in.max(1e-9)).clamp(0.0, 1.0) as f32;
2138        let out_start = self.duration - self.ease_out;
2139        let out_phase = ((local - out_start) / self.ease_out.max(1e-9)).clamp(0.0, 1.0) as f32;
2140        let scale = if local < self.ease_in {
2141            lerp(1.0, self.time_scale, smooth_step(in_phase))
2142        } else if local > out_start {
2143            lerp(self.time_scale, 1.0, smooth_step(out_phase))
2144        } else {
2145            self.time_scale
2146        };
2147        scale
2148    }
2149}
2150
2151#[derive(Clone, Debug)]
2152pub struct LetterboxEvent {
2153    pub time:     f64,
2154    pub duration: f64,
2155    pub aspect:   f32,     // target aspect ratio
2156    pub ease_in:  f64,
2157    pub ease_out: f64,
2158}
2159
2160impl LetterboxEvent {
2161    pub fn bar_height_at(&self, screen_h: f32, screen_w: f32, time: f64) -> f32 {
2162        let local = time - self.time;
2163        if local < 0.0 || local > self.duration { return 0.0; }
2164        let in_phase = (local / self.ease_in.max(1e-9)).clamp(0.0, 1.0) as f32;
2165        let out_start = self.duration - self.ease_out;
2166        let out_phase = ((local - out_start) / self.ease_out.max(1e-9)).clamp(0.0, 1.0) as f32;
2167        let blend = if local < self.ease_in { smooth_step(in_phase) }
2168                    else if local > out_start { 1.0 - smooth_step(out_phase) }
2169                    else { 1.0 };
2170        let current_aspect = screen_w / screen_h.max(1.0);
2171        if current_aspect <= self.aspect { return 0.0; }
2172        let target_h = screen_w / self.aspect;
2173        let bar = (screen_h - target_h) * 0.5 * blend;
2174        bar.max(0.0)
2175    }
2176}
2177
2178#[derive(Clone, Debug)]
2179pub struct ChapterMarker {
2180    pub time:  f64,
2181    pub name:  String,
2182    pub thumb: Option<u64>, // thumbnail image id
2183}
2184
2185#[derive(Clone, Debug)]
2186pub struct BranchingTrigger {
2187    pub time:        f64,
2188    pub condition:   String,  // expression or flag name
2189    pub target_time: f64,     // jump to this time if condition true
2190    pub target_sequence: Option<u64>,
2191    pub auto_trigger: bool,
2192}
2193
2194// ============================================================
2195// BLEND / LAYER EVALUATION ENGINE
2196// ============================================================
2197
2198#[derive(Clone, Debug)]
2199pub struct LayerBlendState {
2200    pub layer: u32,
2201    pub weight: f32,
2202    pub blend_mode: BlendMode,
2203}
2204
2205impl LayerBlendState {
2206    pub fn blend_values(&self, base: f32, layer_val: f32) -> f32 {
2207        match self.blend_mode {
2208            BlendMode::Override  => lerp(base, layer_val, self.weight),
2209            BlendMode::Additive  => base + layer_val * self.weight,
2210            BlendMode::Multiply  => base * lerp(1.0, layer_val, self.weight),
2211            BlendMode::Screen    => 1.0 - (1.0 - base) * lerp(1.0, 1.0 - layer_val, self.weight),
2212            BlendMode::Lerp      => lerp(base, layer_val, self.weight),
2213        }
2214    }
2215
2216    pub fn blend_vec3(&self, base: Vec3, layer_val: Vec3) -> Vec3 {
2217        match self.blend_mode {
2218            BlendMode::Override | BlendMode::Lerp => lerp_vec3(base, layer_val, self.weight),
2219            BlendMode::Additive  => base + layer_val * self.weight,
2220            BlendMode::Multiply  => base * lerp_vec3(Vec3::ONE, layer_val, self.weight),
2221            BlendMode::Screen    => Vec3::ONE - (Vec3::ONE - base) * lerp_vec3(Vec3::ONE, Vec3::ONE - layer_val, self.weight),
2222        }
2223    }
2224}
2225
2226// ============================================================
2227// SEQUENCE (MASTER)
2228// ============================================================
2229
2230static SEQUENCER_ID_COUNTER: std::sync::atomic::AtomicU64 =
2231    std::sync::atomic::AtomicU64::new(1);
2232
2233fn next_id() -> u64 {
2234    SEQUENCER_ID_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
2235}
2236
2237#[derive(Clone, Debug)]
2238pub struct Sequence {
2239    pub id:       u64,
2240    pub name:     String,
2241    pub duration: f64,    // in seconds
2242    pub fps:      FrameRate,
2243    pub loop_seq: bool,
2244    pub work_area_start: f64,
2245    pub work_area_end:   f64,
2246    pub sub_sequences: Vec<SubSequence>,
2247}
2248
2249impl Sequence {
2250    pub fn new(name: &str, duration: f64, fps: FrameRate) -> Self {
2251        Sequence {
2252            id: next_id(),
2253            name: name.to_string(),
2254            duration,
2255            fps,
2256            loop_seq: false,
2257            work_area_start: 0.0,
2258            work_area_end: duration,
2259            sub_sequences: Vec::new(),
2260        }
2261    }
2262
2263    pub fn frame_count(&self) -> u64 {
2264        self.fps.seconds_to_frame(self.duration)
2265    }
2266
2267    pub fn time_at_frame(&self, frame: u64) -> f64 {
2268        self.fps.frame_to_seconds(frame)
2269    }
2270
2271    pub fn frame_at_time(&self, time: f64) -> u64 {
2272        self.fps.seconds_to_frame(time)
2273    }
2274}
2275
2276#[derive(Clone, Debug)]
2277pub struct SubSequence {
2278    pub id:            u64,
2279    pub sequence_id:   u64,   // references a Sequence
2280    pub start_time:    f64,
2281    pub time_scale:    f32,
2282    pub blend_in:      f64,
2283    pub blend_out:     f64,
2284    pub weight:        f32,
2285    pub loop_sub:      bool,
2286}
2287
2288impl SubSequence {
2289    pub fn local_time(&self, global_time: f64) -> f64 {
2290        let local = (global_time - self.start_time) * self.time_scale as f64;
2291        local.max(0.0)
2292    }
2293
2294    pub fn weight_at(&self, global_time: f64, seq_duration: f64) -> f32 {
2295        let local = global_time - self.start_time;
2296        let end   = self.start_time + seq_duration / self.time_scale as f64;
2297        let in_w  = (local / self.blend_in.max(1e-9)).clamp(0.0, 1.0) as f32;
2298        let out_w = ((end - global_time) / self.blend_out.max(1e-9)).clamp(0.0, 1.0) as f32;
2299        self.weight * in_w.min(out_w)
2300    }
2301}
2302
2303// ============================================================
2304// EXPORT: EDL (Edit Decision List)
2305// ============================================================
2306
2307#[derive(Clone, Debug)]
2308pub struct EdlEntry {
2309    pub event_number: u32,
2310    pub reel_name:    String,
2311    pub track_type:   String,  // V = video, A = audio, B = both
2312    pub transition:   EdlTransition,
2313    pub source_in:    Timecode,
2314    pub source_out:   Timecode,
2315    pub record_in:    Timecode,
2316    pub record_out:   Timecode,
2317    pub comment:      String,
2318}
2319
2320#[derive(Clone, Debug, PartialEq)]
2321pub enum EdlTransition {
2322    Cut,
2323    Dissolve(u32),      // frame count
2324    Wipe(u32, u32),     // wipe number, frame count
2325}
2326
2327impl EdlEntry {
2328    pub fn to_cmx3600(&self) -> String {
2329        let trans = match &self.transition {
2330            EdlTransition::Cut              => "C       ".to_string(),
2331            EdlTransition::Dissolve(frames) => format!("D       {:03} ", frames),
2332            EdlTransition::Wipe(n, frames)  => format!("W{:03}    {:03} ", n, frames),
2333        };
2334        format!(
2335            "{:03}  {:8} {} {} {} {} {} {}\n",
2336            self.event_number,
2337            self.reel_name,
2338            self.track_type,
2339            trans,
2340            self.source_in.to_string(),
2341            self.source_out.to_string(),
2342            self.record_in.to_string(),
2343            self.record_out.to_string(),
2344        )
2345    }
2346}
2347
2348#[derive(Clone, Debug)]
2349pub struct EdlDocument {
2350    pub title:   String,
2351    pub fps:     FrameRate,
2352    pub entries: Vec<EdlEntry>,
2353}
2354
2355impl EdlDocument {
2356    pub fn new(title: &str, fps: FrameRate) -> Self {
2357        EdlDocument { title: title.to_string(), fps, entries: Vec::new() }
2358    }
2359
2360    pub fn add_entry(&mut self, entry: EdlEntry) {
2361        self.entries.push(entry);
2362    }
2363
2364    pub fn to_string(&self) -> String {
2365        let mut out = format!("TITLE: {}\n", self.title);
2366        out.push_str(&format!("FCM: NON-DROP FRAME\n\n"));
2367        for entry in &self.entries {
2368            out.push_str(&entry.to_cmx3600());
2369        }
2370        out
2371    }
2372
2373    pub fn from_shot_list(shots: &ShotList, fps: FrameRate) -> Self {
2374        let fps_val = fps.fps();
2375        let mut doc = EdlDocument::new("Sequence", fps);
2376        for (i, shot) in shots.shots.iter().enumerate() {
2377            let src_in  = Timecode::from_seconds(0.0, fps_val);
2378            let src_out = Timecode::from_seconds(shot.duration(), fps_val);
2379            let rec_in  = Timecode::from_seconds(shot.start_time, fps_val);
2380            let rec_out = Timecode::from_seconds(shot.end_time, fps_val);
2381            doc.add_entry(EdlEntry {
2382                event_number: (i + 1) as u32,
2383                reel_name:    format!("CAM{:04}", shot.camera_id % 10000),
2384                track_type:   "V     A1".to_string(),
2385                transition:   EdlTransition::Cut,
2386                source_in:    src_in,
2387                source_out:   src_out,
2388                record_in:    rec_in,
2389                record_out:   rec_out,
2390                comment:      shot.name.clone(),
2391            });
2392        }
2393        doc
2394    }
2395}
2396
2397// ============================================================
2398// PLAYBACK STATE
2399// ============================================================
2400
2401#[derive(Clone, Debug, PartialEq)]
2402pub enum PlaybackState {
2403    Stopped,
2404    Playing,
2405    Paused,
2406    Scrubbing,
2407    Recording,
2408}
2409
2410#[derive(Clone, Debug)]
2411pub struct PlaybackController {
2412    pub state:           PlaybackState,
2413    pub current_time:    f64,
2414    pub playback_speed:  f32,
2415    pub loop_enabled:    bool,
2416    pub loop_start:      f64,
2417    pub loop_end:        f64,
2418    pub bookmarks:       Vec<(f64, String)>,
2419    pub snap_to_frames:  bool,
2420    pub fps:             FrameRate,
2421}
2422
2423impl PlaybackController {
2424    pub fn new(fps: FrameRate) -> Self {
2425        PlaybackController {
2426            state: PlaybackState::Stopped,
2427            current_time: 0.0,
2428            playback_speed: 1.0,
2429            loop_enabled: false,
2430            loop_start: 0.0,
2431            loop_end: 10.0,
2432            bookmarks: Vec::new(),
2433            snap_to_frames: true,
2434            fps,
2435        }
2436    }
2437
2438    pub fn play(&mut self) { self.state = PlaybackState::Playing; }
2439    pub fn pause(&mut self) {
2440        if self.state == PlaybackState::Playing {
2441            self.state = PlaybackState::Paused;
2442        }
2443    }
2444    pub fn stop(&mut self) {
2445        self.state = PlaybackState::Stopped;
2446        self.current_time = 0.0;
2447    }
2448    pub fn toggle_play_pause(&mut self) {
2449        match self.state {
2450            PlaybackState::Playing => self.pause(),
2451            _                      => self.play(),
2452        }
2453    }
2454
2455    pub fn update(&mut self, dt: f32, duration: f64) {
2456        if self.state != PlaybackState::Playing { return; }
2457        self.current_time += dt as f64 * self.playback_speed as f64;
2458        if self.loop_enabled && self.current_time >= self.loop_end {
2459            self.current_time = self.loop_start + (self.current_time - self.loop_end);
2460        } else if self.current_time >= duration {
2461            self.current_time = duration;
2462            self.state = PlaybackState::Paused;
2463        }
2464        if self.snap_to_frames {
2465            let frame = self.fps.seconds_to_frame(self.current_time);
2466            self.current_time = self.fps.frame_to_seconds(frame);
2467        }
2468    }
2469
2470    pub fn scrub_to(&mut self, time: f64) {
2471        self.state = PlaybackState::Scrubbing;
2472        self.current_time = time.max(0.0);
2473        if self.snap_to_frames {
2474            let frame = self.fps.seconds_to_frame(self.current_time);
2475            self.current_time = self.fps.frame_to_seconds(frame);
2476        }
2477    }
2478
2479    pub fn step_frames(&mut self, frames: i64) {
2480        let cur_frame = self.fps.seconds_to_frame(self.current_time) as i64;
2481        let new_frame = (cur_frame + frames).max(0) as u64;
2482        self.current_time = self.fps.frame_to_seconds(new_frame);
2483    }
2484
2485    pub fn add_bookmark(&mut self, name: &str) {
2486        self.bookmarks.push((self.current_time, name.to_string()));
2487        self.bookmarks.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
2488    }
2489
2490    pub fn goto_next_bookmark(&mut self) {
2491        if let Some(bm) = self.bookmarks.iter().find(|&&(t, _)| t > self.current_time) {
2492            self.current_time = bm.0;
2493        }
2494    }
2495
2496    pub fn goto_prev_bookmark(&mut self) {
2497        if let Some(bm) = self.bookmarks.iter().rev().find(|&&(t, _)| t < self.current_time) {
2498            self.current_time = bm.0;
2499        }
2500    }
2501
2502    pub fn current_timecode(&self) -> Timecode {
2503        Timecode::from_seconds(self.current_time, self.fps.fps())
2504    }
2505
2506    pub fn current_frame(&self) -> u64 {
2507        self.fps.seconds_to_frame(self.current_time)
2508    }
2509}
2510
2511// ============================================================
2512// UNDO/REDO SYSTEM FOR SEQUENCER
2513// ============================================================
2514
2515#[derive(Clone, Debug)]
2516pub enum SequencerCommand {
2517    AddKeyframe     { track_id: u64, track_kind: TrackKind, time: f64 },
2518    RemoveKeyframe  { track_id: u64, time: f64 },
2519    MoveKeyframe    { track_id: u64, old_time: f64, new_time: f64 },
2520    AddTrack        { track_id: u64, track_kind: TrackKind },
2521    RemoveTrack     { track_id: u64 },
2522    SetTrackEnabled { track_id: u64, old_val: bool, new_val: bool },
2523    PasteKeyframes  { track_id: u64, times: Vec<f64> },
2524    BakeAnimation   { entity_id: u64 },
2525    SetDuration     { old_duration: f64, new_duration: f64 },
2526    SetFps          { old_fps: FrameRate, new_fps: FrameRate },
2527    AddShot         { shot_id: u64 },
2528    RemoveShot      { shot_id: u64 },
2529    MoveShot        { shot_id: u64, old_start: f64, new_start: f64 },
2530}
2531
2532#[derive(Debug)]
2533pub struct SequencerUndoHistory {
2534    past:     VecDeque<SequencerCommand>,
2535    future:   VecDeque<SequencerCommand>,
2536    max_size: usize,
2537}
2538
2539impl SequencerUndoHistory {
2540    pub fn new() -> Self {
2541        SequencerUndoHistory {
2542            past:     VecDeque::new(),
2543            future:   VecDeque::new(),
2544            max_size: MAX_UNDO_DEPTH,
2545        }
2546    }
2547
2548    pub fn push(&mut self, cmd: SequencerCommand) {
2549        self.future.clear();
2550        self.past.push_back(cmd);
2551        if self.past.len() > self.max_size {
2552            self.past.pop_front();
2553        }
2554    }
2555
2556    pub fn undo(&mut self) -> Option<SequencerCommand> {
2557        let cmd = self.past.pop_back()?;
2558        self.future.push_back(cmd.clone());
2559        Some(cmd)
2560    }
2561
2562    pub fn redo(&mut self) -> Option<SequencerCommand> {
2563        let cmd = self.future.pop_back()?;
2564        self.past.push_back(cmd.clone());
2565        Some(cmd)
2566    }
2567
2568    pub fn can_undo(&self) -> bool { !self.past.is_empty() }
2569    pub fn can_redo(&self) -> bool { !self.future.is_empty() }
2570    pub fn clear(&mut self) { self.past.clear(); self.future.clear(); }
2571}
2572
2573// ============================================================
2574// SELECTION STATE
2575// ============================================================
2576
2577#[derive(Clone, Debug)]
2578pub struct SequencerSelection {
2579    pub selected_tracks:   HashSet<u64>,
2580    pub selected_keyframes: HashMap<u64, Vec<f64>>, // track_id -> selected times
2581    pub clipboard_keyframes: HashMap<u64, Vec<f64>>,
2582    pub clipboard_offset:  f64,
2583}
2584
2585impl SequencerSelection {
2586    pub fn new() -> Self {
2587        SequencerSelection {
2588            selected_tracks: HashSet::new(),
2589            selected_keyframes: HashMap::new(),
2590            clipboard_keyframes: HashMap::new(),
2591            clipboard_offset: 0.0,
2592        }
2593    }
2594
2595    pub fn select_track(&mut self, id: u64, multi: bool) {
2596        if !multi { self.selected_tracks.clear(); }
2597        self.selected_tracks.insert(id);
2598    }
2599
2600    pub fn select_keyframe(&mut self, track_id: u64, time: f64, multi: bool) {
2601        if !multi {
2602            self.selected_keyframes.clear();
2603        }
2604        self.selected_keyframes.entry(track_id).or_default().push(time);
2605    }
2606
2607    pub fn select_range(&mut self, track_id: u64, t_start: f64, t_end: f64, times: &[f64]) {
2608        let in_range: Vec<f64> = times.iter()
2609            .cloned()
2610            .filter(|&t| t >= t_start && t <= t_end)
2611            .collect();
2612        self.selected_keyframes.entry(track_id).or_default().extend(in_range);
2613    }
2614
2615    pub fn copy_keyframes(&mut self, current_time: f64) {
2616        self.clipboard_keyframes = self.selected_keyframes.clone();
2617        self.clipboard_offset = current_time;
2618    }
2619
2620    pub fn clear(&mut self) {
2621        self.selected_tracks.clear();
2622        self.selected_keyframes.clear();
2623    }
2624
2625    pub fn is_track_selected(&self, id: u64) -> bool {
2626        self.selected_tracks.contains(&id)
2627    }
2628
2629    pub fn is_keyframe_selected(&self, track_id: u64, time: f64) -> bool {
2630        self.selected_keyframes.get(&track_id)
2631            .map(|times| times.iter().any(|&t| (t - time).abs() < 1e-6))
2632            .unwrap_or(false)
2633    }
2634}
2635
2636// ============================================================
2637// CURVE EDITOR STATE
2638// ============================================================
2639
2640#[derive(Clone, Debug)]
2641pub struct CurveEditorState {
2642    pub visible_tracks: HashSet<u64>,
2643    pub view_min_t: f64,
2644    pub view_max_t: f64,
2645    pub view_min_v: f32,
2646    pub view_max_v: f32,
2647    pub show_tangents: bool,
2648    pub tangent_scale:  f32,
2649    pub snap_value:     f32,   // value snap grid
2650    pub snap_time:      f64,   // time snap grid
2651    pub auto_fit:       bool,
2652}
2653
2654impl CurveEditorState {
2655    pub fn new() -> Self {
2656        CurveEditorState {
2657            visible_tracks: HashSet::new(),
2658            view_min_t:  0.0,
2659            view_max_t: 10.0,
2660            view_min_v: -1.0,
2661            view_max_v:  1.0,
2662            show_tangents: true,
2663            tangent_scale: 1.0,
2664            snap_value: 0.0,
2665            snap_time:  0.0,
2666            auto_fit:   true,
2667        }
2668    }
2669
2670    pub fn time_to_screen_x(&self, time: f64, screen_w: f32) -> f32 {
2671        let frac = (time - self.view_min_t) / (self.view_max_t - self.view_min_t).max(1e-9);
2672        frac as f32 * screen_w
2673    }
2674
2675    pub fn value_to_screen_y(&self, value: f32, screen_h: f32) -> f32 {
2676        let frac = (value - self.view_min_v) / (self.view_max_v - self.view_min_v).max(EPSILON);
2677        (1.0 - frac) * screen_h
2678    }
2679
2680    pub fn screen_x_to_time(&self, x: f32, screen_w: f32) -> f64 {
2681        let frac = x / screen_w.max(1.0);
2682        self.view_min_t + frac as f64 * (self.view_max_t - self.view_min_t)
2683    }
2684
2685    pub fn screen_y_to_value(&self, y: f32, screen_h: f32) -> f32 {
2686        let frac = 1.0 - y / screen_h.max(1.0);
2687        self.view_min_v + frac * (self.view_max_v - self.view_min_v)
2688    }
2689
2690    pub fn zoom(&mut self, center_t: f64, center_v: f32, scale: f32) {
2691        let dt  = (self.view_max_t - self.view_min_t) * scale as f64;
2692        let dv  = (self.view_max_v - self.view_min_v) * scale;
2693        self.view_min_t = center_t - dt * 0.5;
2694        self.view_max_t = center_t + dt * 0.5;
2695        self.view_min_v = center_v - dv * 0.5;
2696        self.view_max_v = center_v + dv * 0.5;
2697    }
2698
2699    pub fn fit_to_curve(&mut self, curve: &FloatCurve) {
2700        if curve.keys.is_empty() { return; }
2701        let (min_t, max_t) = (curve.keys.first().unwrap().time, curve.keys.last().unwrap().time);
2702        let (min_v, max_v) = curve.value_range();
2703        let pad_t = (max_t - min_t) * 0.1;
2704        let pad_v = (max_v - min_v) * 0.1;
2705        self.view_min_t = min_t - pad_t;
2706        self.view_max_t = max_t + pad_t;
2707        self.view_min_v = min_v - pad_v;
2708        self.view_max_v = max_v + pad_v;
2709    }
2710}
2711
2712// ============================================================
2713// TRACK COLLECTION (all track types in one place)
2714// ============================================================
2715
2716#[derive(Debug)]
2717pub struct TrackCollection {
2718    pub camera_tracks:      HashMap<u64, CameraTrack>,
2719    pub actor_tracks:       HashMap<u64, ActorTrack>,
2720    pub animation_tracks:   HashMap<u64, AnimationTrack>,
2721    pub audio_tracks:       HashMap<u64, AudioTrack>,
2722    pub vfx_tracks:         HashMap<u64, VfxTrack>,
2723    pub light_tracks:       HashMap<u64, LightTrack>,
2724    pub post_fx_tracks:     HashMap<u64, PostFxTrack>,
2725    pub subtitle_tracks:    HashMap<u64, SubtitleTrack>,
2726    pub event_tracks:       HashMap<u64, EventTrack>,
2727    pub transform_tracks:   HashMap<u64, TransformTrack>,
2728    pub blend_shape_tracks: HashMap<u64, BlendShapeTrack>,
2729    pub visibility_tracks:  HashMap<u64, VisibilityTrack>,
2730    pub time_dilation_tracks: HashMap<u64, TimeDilationTrack>,
2731    // track order for display
2732    pub track_order: Vec<u64>,
2733}
2734
2735impl TrackCollection {
2736    pub fn new() -> Self {
2737        TrackCollection {
2738            camera_tracks:      HashMap::new(),
2739            actor_tracks:       HashMap::new(),
2740            animation_tracks:   HashMap::new(),
2741            audio_tracks:       HashMap::new(),
2742            vfx_tracks:         HashMap::new(),
2743            light_tracks:       HashMap::new(),
2744            post_fx_tracks:     HashMap::new(),
2745            subtitle_tracks:    HashMap::new(),
2746            event_tracks:       HashMap::new(),
2747            transform_tracks:   HashMap::new(),
2748            blend_shape_tracks: HashMap::new(),
2749            visibility_tracks:  HashMap::new(),
2750            time_dilation_tracks: HashMap::new(),
2751            track_order:        Vec::new(),
2752        }
2753    }
2754
2755    pub fn track_count(&self) -> usize {
2756        self.camera_tracks.len()
2757            + self.actor_tracks.len()
2758            + self.animation_tracks.len()
2759            + self.audio_tracks.len()
2760            + self.vfx_tracks.len()
2761            + self.light_tracks.len()
2762            + self.post_fx_tracks.len()
2763            + self.subtitle_tracks.len()
2764            + self.event_tracks.len()
2765            + self.transform_tracks.len()
2766            + self.blend_shape_tracks.len()
2767            + self.visibility_tracks.len()
2768            + self.time_dilation_tracks.len()
2769    }
2770
2771    pub fn add_camera_track(&mut self, track: CameraTrack) {
2772        let id = track.base.id;
2773        self.track_order.push(id);
2774        self.camera_tracks.insert(id, track);
2775    }
2776
2777    pub fn add_actor_track(&mut self, track: ActorTrack) {
2778        let id = track.base.id;
2779        self.track_order.push(id);
2780        self.actor_tracks.insert(id, track);
2781    }
2782
2783    pub fn add_animation_track(&mut self, track: AnimationTrack) {
2784        let id = track.base.id;
2785        self.track_order.push(id);
2786        self.animation_tracks.insert(id, track);
2787    }
2788
2789    pub fn add_audio_track(&mut self, track: AudioTrack) {
2790        let id = track.base.id;
2791        self.track_order.push(id);
2792        self.audio_tracks.insert(id, track);
2793    }
2794
2795    pub fn add_vfx_track(&mut self, track: VfxTrack) {
2796        let id = track.base.id;
2797        self.track_order.push(id);
2798        self.vfx_tracks.insert(id, track);
2799    }
2800
2801    pub fn add_light_track(&mut self, track: LightTrack) {
2802        let id = track.base.id;
2803        self.track_order.push(id);
2804        self.light_tracks.insert(id, track);
2805    }
2806
2807    pub fn add_post_fx_track(&mut self, track: PostFxTrack) {
2808        let id = track.base.id;
2809        self.track_order.push(id);
2810        self.post_fx_tracks.insert(id, track);
2811    }
2812
2813    pub fn add_subtitle_track(&mut self, track: SubtitleTrack) {
2814        let id = track.base.id;
2815        self.track_order.push(id);
2816        self.subtitle_tracks.insert(id, track);
2817    }
2818
2819    pub fn add_event_track(&mut self, track: EventTrack) {
2820        let id = track.base.id;
2821        self.track_order.push(id);
2822        self.event_tracks.insert(id, track);
2823    }
2824
2825    pub fn add_transform_track(&mut self, track: TransformTrack) {
2826        let id = track.base.id;
2827        self.track_order.push(id);
2828        self.transform_tracks.insert(id, track);
2829    }
2830
2831    pub fn add_blend_shape_track(&mut self, track: BlendShapeTrack) {
2832        let id = track.base.id;
2833        self.track_order.push(id);
2834        self.blend_shape_tracks.insert(id, track);
2835    }
2836
2837    pub fn add_visibility_track(&mut self, track: VisibilityTrack) {
2838        let id = track.base.id;
2839        self.track_order.push(id);
2840        self.visibility_tracks.insert(id, track);
2841    }
2842
2843    pub fn add_time_dilation_track(&mut self, track: TimeDilationTrack) {
2844        let id = track.base.id;
2845        self.track_order.push(id);
2846        self.time_dilation_tracks.insert(id, track);
2847    }
2848
2849    /// Remove track `id` and return it in a collection of its own (with its
2850    /// display position), so it can be put back with [`Self::restore_from`].
2851    pub fn take_track(&mut self, id: u64) -> TrackCollection {
2852        let mut out = TrackCollection::new();
2853        macro_rules! mv { ($($f:ident),*) => { $( if let Some(t) = self.$f.remove(&id) { out.$f.insert(id, t); } )* } }
2854        mv!(camera_tracks, actor_tracks, animation_tracks, audio_tracks, vfx_tracks, light_tracks,
2855            post_fx_tracks, subtitle_tracks, event_tracks, transform_tracks, blend_shape_tracks,
2856            visibility_tracks, time_dilation_tracks);
2857        if let Some(pos) = self.track_order.iter().position(|&t| t == id) {
2858            self.track_order.remove(pos);
2859            // Remember the position in a one-element order list: index then id.
2860            out.track_order = vec![pos as u64, id];
2861        }
2862        out
2863    }
2864
2865    /// Put back tracks taken with [`Self::take_track`].
2866    pub fn restore_from(&mut self, mut other: TrackCollection) {
2867        macro_rules! mv { ($($f:ident),*) => { $( for (k, t) in other.$f.drain() { self.$f.insert(k, t); } )* } }
2868        mv!(camera_tracks, actor_tracks, animation_tracks, audio_tracks, vfx_tracks, light_tracks,
2869            post_fx_tracks, subtitle_tracks, event_tracks, transform_tracks, blend_shape_tracks,
2870            visibility_tracks, time_dilation_tracks);
2871        if let [pos, id] = other.track_order[..] {
2872            let pos = (pos as usize).min(self.track_order.len());
2873            self.track_order.insert(pos, id);
2874        }
2875    }
2876
2877    pub fn remove_track(&mut self, id: u64) {
2878        self.track_order.retain(|&tid| tid != id);
2879        self.camera_tracks.remove(&id);
2880        self.actor_tracks.remove(&id);
2881        self.animation_tracks.remove(&id);
2882        self.audio_tracks.remove(&id);
2883        self.vfx_tracks.remove(&id);
2884        self.light_tracks.remove(&id);
2885        self.post_fx_tracks.remove(&id);
2886        self.subtitle_tracks.remove(&id);
2887        self.event_tracks.remove(&id);
2888        self.transform_tracks.remove(&id);
2889        self.blend_shape_tracks.remove(&id);
2890        self.visibility_tracks.remove(&id);
2891        self.time_dilation_tracks.remove(&id);
2892    }
2893
2894    pub fn is_track_enabled(&self, id: u64) -> bool {
2895        if let Some(t) = self.camera_tracks.get(&id)      { return t.base.enabled; }
2896        if let Some(t) = self.actor_tracks.get(&id)       { return t.base.enabled; }
2897        if let Some(t) = self.animation_tracks.get(&id)   { return t.base.enabled; }
2898        if let Some(t) = self.audio_tracks.get(&id)       { return t.base.enabled; }
2899        if let Some(t) = self.vfx_tracks.get(&id)         { return t.base.enabled; }
2900        if let Some(t) = self.light_tracks.get(&id)       { return t.base.enabled; }
2901        if let Some(t) = self.post_fx_tracks.get(&id)     { return t.base.enabled; }
2902        if let Some(t) = self.subtitle_tracks.get(&id)    { return t.base.enabled; }
2903        if let Some(t) = self.event_tracks.get(&id)       { return t.base.enabled; }
2904        if let Some(t) = self.transform_tracks.get(&id)   { return t.base.enabled; }
2905        if let Some(t) = self.blend_shape_tracks.get(&id) { return t.base.enabled; }
2906        if let Some(t) = self.visibility_tracks.get(&id)  { return t.base.enabled; }
2907        if let Some(t) = self.time_dilation_tracks.get(&id) { return t.base.enabled; }
2908        false
2909    }
2910
2911    pub fn set_track_enabled(&mut self, id: u64, enabled: bool) {
2912        macro_rules! set_enabled {
2913            ($map:expr) => { if let Some(t) = $map.get_mut(&id) { t.base.enabled = enabled; return; } };
2914        }
2915        set_enabled!(self.camera_tracks);
2916        set_enabled!(self.actor_tracks);
2917        set_enabled!(self.animation_tracks);
2918        set_enabled!(self.audio_tracks);
2919        set_enabled!(self.vfx_tracks);
2920        set_enabled!(self.light_tracks);
2921        set_enabled!(self.post_fx_tracks);
2922        set_enabled!(self.subtitle_tracks);
2923        set_enabled!(self.event_tracks);
2924        set_enabled!(self.transform_tracks);
2925        set_enabled!(self.blend_shape_tracks);
2926        set_enabled!(self.visibility_tracks);
2927        set_enabled!(self.time_dilation_tracks);
2928    }
2929
2930    pub fn move_track_up(&mut self, id: u64) {
2931        if let Some(idx) = self.track_order.iter().position(|&tid| tid == id) {
2932            if idx > 0 { self.track_order.swap(idx, idx - 1); }
2933        }
2934    }
2935
2936    pub fn move_track_down(&mut self, id: u64) {
2937        if let Some(idx) = self.track_order.iter().position(|&tid| tid == id) {
2938            if idx + 1 < self.track_order.len() { self.track_order.swap(idx, idx + 1); }
2939        }
2940    }
2941}
2942
2943// ============================================================
2944// FRAME EVALUATION RESULT
2945// ============================================================
2946
2947#[derive(Clone, Debug)]
2948pub struct FrameEvalResult {
2949    pub time: f64,
2950    pub camera_transforms: HashMap<u64, Mat4>,
2951    pub camera_fovs:       HashMap<u64, f32>,
2952    pub actor_transforms:  HashMap<u64, Mat4>,
2953    pub blend_shapes:      HashMap<u64, HashMap<String, f32>>,
2954    pub light_states:      HashMap<u64, (Vec4, f32, f32)>,
2955    pub post_fx:           Vec<PostFxKeyframe>,
2956    pub active_subtitles:  Vec<SubtitleKeyframe>,
2957    pub fired_events:      Vec<EventKeyframe>,
2958    pub time_scale:        f32,
2959    pub visibility:        HashMap<u64, f32>,
2960}
2961
2962impl FrameEvalResult {
2963    pub fn new(time: f64) -> Self {
2964        FrameEvalResult {
2965            time,
2966            camera_transforms: HashMap::new(),
2967            camera_fovs:       HashMap::new(),
2968            actor_transforms:  HashMap::new(),
2969            blend_shapes:      HashMap::new(),
2970            light_states:      HashMap::new(),
2971            post_fx:           Vec::new(),
2972            active_subtitles:  Vec::new(),
2973            fired_events:      Vec::new(),
2974            time_scale:        1.0,
2975            visibility:        HashMap::new(),
2976        }
2977    }
2978}
2979
2980// ============================================================
2981// CINEMATIC SEQUENCER (main struct)
2982// ============================================================
2983
2984pub struct CinematicSequencer {
2985    // Sequences
2986    pub master_sequence:  Sequence,
2987    pub sequences:        HashMap<u64, Sequence>,
2988
2989    // Tracks
2990    pub tracks: TrackCollection,
2991
2992    // Shot list
2993    pub shot_list: ShotList,
2994
2995    // Cinematic events
2996    pub screen_flashes:  Vec<ScreenFlashEvent>,
2997    pub rumble_events:   Vec<RumbleEvent>,
2998    pub slow_mo_events:  Vec<SlowMotionEvent>,
2999    pub letterbox_events: Vec<LetterboxEvent>,
3000    pub chapter_markers: Vec<ChapterMarker>,
3001    pub branching_triggers: Vec<BranchingTrigger>,
3002
3003    // Playback
3004    pub playback: PlaybackController,
3005    pub prev_eval_time: f64,
3006
3007    // Undo/redo
3008    pub undo_history: SequencerUndoHistory,
3009    /// Tracks removed by undoing an `AddTrack`, kept so redo can put them back.
3010    pub undone_tracks: HashMap<u64, TrackCollection>,
3011
3012    // Selection
3013    pub selection: SequencerSelection,
3014
3015    // Curve editor
3016    pub curve_editor: CurveEditorState,
3017
3018    // Camera blend state
3019    pub active_camera_id: Option<u64>,
3020    pub blend_from_camera: Option<u64>,
3021    pub camera_blend_t:    f32,
3022    pub camera_blend_duration: f32,
3023
3024    // Letterbox state
3025    pub letterbox_amount: f32,
3026
3027    // Time dilation
3028    pub current_time_scale: f32,
3029
3030    // Settings
3031    pub auto_key: bool,
3032    pub auto_key_mode: AutoKeyMode,
3033    pub default_interp: InterpType,
3034    pub show_all_tracks: bool,
3035    pub track_height: f32,
3036}
3037
3038#[derive(Clone, Debug, PartialEq)]
3039pub enum AutoKeyMode {
3040    None,
3041    KeyOnChange,
3042    KeyAllModified,
3043}
3044
3045impl CinematicSequencer {
3046    pub fn new(name: &str, duration: f64, fps: FrameRate) -> Self {
3047        let fps_clone = fps.clone();
3048        CinematicSequencer {
3049            master_sequence: Sequence::new(name, duration, fps),
3050            sequences: HashMap::new(),
3051            tracks: TrackCollection::new(),
3052            shot_list: ShotList::new(),
3053            screen_flashes: Vec::new(),
3054            rumble_events: Vec::new(),
3055            slow_mo_events: Vec::new(),
3056            letterbox_events: Vec::new(),
3057            chapter_markers: Vec::new(),
3058            branching_triggers: Vec::new(),
3059            playback: PlaybackController::new(fps_clone),
3060            prev_eval_time: 0.0,
3061            undo_history: SequencerUndoHistory::new(),
3062            undone_tracks: HashMap::new(),
3063            selection: SequencerSelection::new(),
3064            curve_editor: CurveEditorState::new(),
3065            active_camera_id: None,
3066            blend_from_camera: None,
3067            camera_blend_t: 0.0,
3068            camera_blend_duration: 0.5,
3069            letterbox_amount: 0.0,
3070            current_time_scale: 1.0,
3071            auto_key: false,
3072            auto_key_mode: AutoKeyMode::None,
3073            default_interp: InterpType::Cubic,
3074            show_all_tracks: true,
3075            track_height: 32.0,
3076        }
3077    }
3078
3079    // ---- TRACK CREATION ----
3080
3081    pub fn add_camera_track(&mut self, name: &str) -> u64 {
3082        let id = next_id();
3083        let track = CameraTrack::new(id, name);
3084        let kind = track.base.kind.clone();
3085        self.tracks.add_camera_track(track);
3086        self.undo_history.push(SequencerCommand::AddTrack { track_id: id, track_kind: kind });
3087        id
3088    }
3089
3090    pub fn add_actor_track(&mut self, name: &str, entity_id: u64) -> u64 {
3091        let id = next_id();
3092        let track = ActorTrack::new(id, name, entity_id);
3093        let kind = track.base.kind.clone();
3094        self.tracks.add_actor_track(track);
3095        self.undo_history.push(SequencerCommand::AddTrack { track_id: id, track_kind: kind });
3096        id
3097    }
3098
3099    pub fn add_animation_track(&mut self, name: &str, entity_id: u64) -> u64 {
3100        let id = next_id();
3101        let track = AnimationTrack::new(id, name, entity_id);
3102        let kind = track.base.kind.clone();
3103        self.tracks.add_animation_track(track);
3104        self.undo_history.push(SequencerCommand::AddTrack { track_id: id, track_kind: kind });
3105        id
3106    }
3107
3108    pub fn add_audio_track(&mut self, name: &str) -> u64 {
3109        let id = next_id();
3110        let track = AudioTrack::new(id, name);
3111        let kind = track.base.kind.clone();
3112        self.tracks.add_audio_track(track);
3113        self.undo_history.push(SequencerCommand::AddTrack { track_id: id, track_kind: kind });
3114        id
3115    }
3116
3117    pub fn add_vfx_track(&mut self, name: &str) -> u64 {
3118        let id = next_id();
3119        let track = VfxTrack::new(id, name);
3120        let kind = track.base.kind.clone();
3121        self.tracks.add_vfx_track(track);
3122        self.undo_history.push(SequencerCommand::AddTrack { track_id: id, track_kind: kind });
3123        id
3124    }
3125
3126    pub fn add_light_track(&mut self, name: &str, entity_id: u64) -> u64 {
3127        let id = next_id();
3128        let track = LightTrack::new(id, name, entity_id);
3129        let kind = track.base.kind.clone();
3130        self.tracks.add_light_track(track);
3131        self.undo_history.push(SequencerCommand::AddTrack { track_id: id, track_kind: kind });
3132        id
3133    }
3134
3135    pub fn add_post_fx_track(&mut self, name: &str) -> u64 {
3136        let id = next_id();
3137        let track = PostFxTrack::new(id, name);
3138        let kind = track.base.kind.clone();
3139        self.tracks.add_post_fx_track(track);
3140        self.undo_history.push(SequencerCommand::AddTrack { track_id: id, track_kind: kind });
3141        id
3142    }
3143
3144    pub fn add_subtitle_track(&mut self, name: &str) -> u64 {
3145        let id = next_id();
3146        let track = SubtitleTrack::new(id, name);
3147        let kind = track.base.kind.clone();
3148        self.tracks.add_subtitle_track(track);
3149        self.undo_history.push(SequencerCommand::AddTrack { track_id: id, track_kind: kind });
3150        id
3151    }
3152
3153    pub fn add_event_track(&mut self, name: &str) -> u64 {
3154        let id = next_id();
3155        let track = EventTrack::new(id, name);
3156        let kind = track.base.kind.clone();
3157        self.tracks.add_event_track(track);
3158        self.undo_history.push(SequencerCommand::AddTrack { track_id: id, track_kind: kind });
3159        id
3160    }
3161
3162    pub fn add_transform_track(&mut self, name: &str, entity_id: u64) -> u64 {
3163        let id = next_id();
3164        let track = TransformTrack::new(id, name, entity_id);
3165        let kind = track.base.kind.clone();
3166        self.tracks.add_transform_track(track);
3167        self.undo_history.push(SequencerCommand::AddTrack { track_id: id, track_kind: kind });
3168        id
3169    }
3170
3171    pub fn add_blend_shape_track(&mut self, name: &str, entity_id: u64) -> u64 {
3172        let id = next_id();
3173        let track = BlendShapeTrack::new(id, name, entity_id);
3174        let kind = track.base.kind.clone();
3175        self.tracks.add_blend_shape_track(track);
3176        self.undo_history.push(SequencerCommand::AddTrack { track_id: id, track_kind: kind });
3177        id
3178    }
3179
3180    pub fn add_visibility_track(&mut self, name: &str, entity_id: u64) -> u64 {
3181        let id = next_id();
3182        let track = VisibilityTrack::new(id, name, entity_id);
3183        let kind = track.base.kind.clone();
3184        self.tracks.add_visibility_track(track);
3185        self.undo_history.push(SequencerCommand::AddTrack { track_id: id, track_kind: kind });
3186        id
3187    }
3188
3189    pub fn add_time_dilation_track(&mut self, name: &str) -> u64 {
3190        let id = next_id();
3191        let track = TimeDilationTrack::new(id, name);
3192        let kind = track.base.kind.clone();
3193        self.tracks.add_time_dilation_track(track);
3194        self.undo_history.push(SequencerCommand::AddTrack { track_id: id, track_kind: kind });
3195        id
3196    }
3197
3198    pub fn remove_track(&mut self, id: u64) {
3199        let kind = if self.tracks.camera_tracks.contains_key(&id) { TrackKind::Camera }
3200            else if self.tracks.actor_tracks.contains_key(&id)       { TrackKind::Actor }
3201            else if self.tracks.animation_tracks.contains_key(&id)   { TrackKind::Animation }
3202            else if self.tracks.audio_tracks.contains_key(&id)       { TrackKind::Audio }
3203            else { TrackKind::Event };
3204        self.tracks.remove_track(id);
3205        self.undo_history.push(SequencerCommand::RemoveTrack { track_id: id });
3206    }
3207
3208    // ---- KEYFRAME INSERTION ----
3209
3210    pub fn add_camera_keyframe(&mut self, track_id: u64, kf: CameraKeyframe) {
3211        let time = kf.time;
3212        if let Some(track) = self.tracks.camera_tracks.get_mut(&track_id) {
3213            track.add_keyframe(kf);
3214            self.undo_history.push(SequencerCommand::AddKeyframe {
3215                track_id, track_kind: TrackKind::Camera, time,
3216            });
3217        }
3218    }
3219
3220    pub fn add_actor_keyframe(&mut self, track_id: u64, kf: ActorKeyframe) {
3221        let time = kf.time;
3222        if let Some(track) = self.tracks.actor_tracks.get_mut(&track_id) {
3223            track.add_keyframe(kf);
3224            self.undo_history.push(SequencerCommand::AddKeyframe {
3225                track_id, track_kind: TrackKind::Actor, time,
3226            });
3227        }
3228    }
3229
3230    pub fn add_subtitle(&mut self, track_id: u64, kf: SubtitleKeyframe) {
3231        let time = kf.time;
3232        if let Some(track) = self.tracks.subtitle_tracks.get_mut(&track_id) {
3233            track.add_subtitle(kf);
3234            self.undo_history.push(SequencerCommand::AddKeyframe {
3235                track_id, track_kind: TrackKind::Subtitle, time,
3236            });
3237        }
3238    }
3239
3240    pub fn add_event(&mut self, track_id: u64, ev: EventKeyframe) {
3241        let time = ev.time;
3242        if let Some(track) = self.tracks.event_tracks.get_mut(&track_id) {
3243            track.add_event(ev);
3244            self.undo_history.push(SequencerCommand::AddKeyframe {
3245                track_id, track_kind: TrackKind::Event, time,
3246            });
3247        }
3248    }
3249
3250    // ---- CINEMATIC EVENTS ----
3251
3252    pub fn add_screen_flash(&mut self, time: f64, color: Vec4, duration: f64, intensity: f32) {
3253        self.screen_flashes.push(ScreenFlashEvent { time, color, duration, intensity });
3254        self.screen_flashes.sort_by(|a, b| a.time.partial_cmp(&b.time).unwrap_or(std::cmp::Ordering::Equal));
3255    }
3256
3257    pub fn add_rumble(&mut self, time: f64, duration: f64, intensity: f32, frequency: f32) {
3258        self.rumble_events.push(RumbleEvent { time, duration, intensity, frequency, decay: 3.0 });
3259    }
3260
3261    pub fn add_slow_mo(&mut self, time: f64, duration: f64, scale: f32) {
3262        self.slow_mo_events.push(SlowMotionEvent {
3263            time, duration, time_scale: scale, ease_in: 0.3, ease_out: 0.5,
3264        });
3265    }
3266
3267    pub fn add_letterbox(&mut self, time: f64, duration: f64) {
3268        self.letterbox_events.push(LetterboxEvent {
3269            time, duration, aspect: LETTERBOX_ASPECT, ease_in: 0.5, ease_out: 0.5,
3270        });
3271    }
3272
3273    pub fn add_chapter(&mut self, time: f64, name: &str) {
3274        self.chapter_markers.push(ChapterMarker { time, name: name.to_string(), thumb: None });
3275        self.chapter_markers.sort_by(|a, b| a.time.partial_cmp(&b.time).unwrap_or(std::cmp::Ordering::Equal));
3276    }
3277
3278    // ---- SHOT MANAGEMENT ----
3279
3280    pub fn add_shot(&mut self, name: &str, start: f64, end: f64, camera_id: u64) -> u64 {
3281        let id = next_id();
3282        let shot = Shot::new(id, name, start, end, camera_id);
3283        self.shot_list.add_shot(shot);
3284        self.undo_history.push(SequencerCommand::AddShot { shot_id: id });
3285        id
3286    }
3287
3288    pub fn current_shot(&self) -> Option<&Shot> {
3289        let time = self.playback.current_time;
3290        self.shot_list.shot_at_time(time)
3291    }
3292
3293    // ---- CAMERA BLEND ----
3294
3295    pub fn cut_to_camera(&mut self, camera_id: u64) {
3296        self.blend_from_camera = None;
3297        self.active_camera_id = Some(camera_id);
3298        self.camera_blend_t = 1.0;
3299    }
3300
3301    pub fn blend_to_camera(&mut self, camera_id: u64, duration: f32) {
3302        self.blend_from_camera = self.active_camera_id;
3303        self.active_camera_id = Some(camera_id);
3304        self.camera_blend_t = 0.0;
3305        self.camera_blend_duration = duration;
3306    }
3307
3308    pub fn update_camera_blend(&mut self, dt: f32) {
3309        if self.camera_blend_t < 1.0 {
3310            self.camera_blend_t = (self.camera_blend_t + dt / self.camera_blend_duration.max(EPSILON)).min(1.0);
3311        }
3312    }
3313
3314    pub fn blended_camera_matrix(&self, time: f64) -> Mat4 {
3315        let active_id = match self.active_camera_id { Some(id) => id, None => return Mat4::IDENTITY };
3316        let active_mat = self.tracks.camera_tracks.get(&active_id)
3317            .map(|t| t.camera_matrix(time))
3318            .unwrap_or(Mat4::IDENTITY);
3319        if self.camera_blend_t >= 1.0 || self.blend_from_camera.is_none() {
3320            return active_mat;
3321        }
3322        let from_id  = self.blend_from_camera.unwrap();
3323        let from_mat = self.tracks.camera_tracks.get(&from_id)
3324            .map(|t| t.camera_matrix(time))
3325            .unwrap_or(Mat4::IDENTITY);
3326        // Decompose and re-compose with slerp
3327        let (from_scale, from_rot, from_trans) = decompose_mat4(from_mat);
3328        let (to_scale,   to_rot,   to_trans)   = decompose_mat4(active_mat);
3329        let t = smoother_step(self.camera_blend_t);
3330        let blend_pos = lerp_vec3(from_trans, to_trans, t);
3331        let blend_rot = from_rot.slerp(to_rot, t);
3332        let blend_scale = lerp_vec3(from_scale, to_scale, t);
3333        Mat4::from_scale_rotation_translation(blend_scale, blend_rot, blend_pos)
3334    }
3335
3336    // ---- FULL FRAME EVALUATION ----
3337
3338    pub fn evaluate_frame(&mut self, dt: f32) -> FrameEvalResult {
3339        let prev_time = self.prev_eval_time;
3340        let time = self.playback.current_time;
3341        self.prev_eval_time = time;
3342
3343        let mut result = FrameEvalResult::new(time);
3344
3345        // Time scale from dilation tracks
3346        let mut combined_scale = 1.0_f32;
3347        for track in self.tracks.time_dilation_tracks.values() {
3348            if !track.base.enabled || track.base.muted { continue; }
3349            combined_scale *= track.evaluate(time);
3350        }
3351        // Slow-mo events
3352        for ev in &self.slow_mo_events {
3353            combined_scale *= ev.scale_at(time);
3354        }
3355        result.time_scale = combined_scale;
3356        self.current_time_scale = combined_scale;
3357
3358        // Camera tracks
3359        for (&id, track) in &self.tracks.camera_tracks {
3360            if !track.base.enabled || track.base.muted { continue; }
3361            result.camera_transforms.insert(id, track.camera_matrix(time));
3362            result.camera_fovs.insert(id, track.evaluate_fov(time));
3363        }
3364
3365        // Actor tracks
3366        for (&id, track) in &self.tracks.actor_tracks {
3367            if !track.base.enabled || track.base.muted { continue; }
3368            result.actor_transforms.insert(id, track.world_matrix(time));
3369        }
3370
3371        // Transform tracks (additive or override)
3372        for (_, track) in &self.tracks.transform_tracks {
3373            if !track.base.enabled || track.base.muted { continue; }
3374            let (pos, rot, scale) = track.evaluate(time);
3375            let mat = Mat4::from_scale_rotation_translation(scale, rot, pos);
3376            if track.additive {
3377                let base = result.actor_transforms.get(&track.entity_id).cloned().unwrap_or(Mat4::IDENTITY);
3378                result.actor_transforms.insert(track.entity_id, base * mat);
3379            } else {
3380                result.actor_transforms.insert(track.entity_id, mat);
3381            }
3382        }
3383
3384        // Light tracks
3385        for (&id, track) in &self.tracks.light_tracks {
3386            if !track.base.enabled || track.base.muted { continue; }
3387            let (mut color, mut intensity, range) = track.evaluate(time);
3388            intensity *= track.flicker_factor(time);
3389            result.light_states.insert(id, (color, intensity, range));
3390        }
3391
3392        // Post FX tracks (stack multiple, blending by weight)
3393        let mut post_fx_base = PostFxKeyframe::default_at(time);
3394        for (_, track) in &self.tracks.post_fx_tracks {
3395            if !track.base.enabled || track.base.muted { continue; }
3396            let pfx = track.evaluate(time);
3397            let w = track.base.weight;
3398            post_fx_base.exposure        = lerp(post_fx_base.exposure,        pfx.exposure,        w);
3399            post_fx_base.contrast        = lerp(post_fx_base.contrast,        pfx.contrast,        w);
3400            post_fx_base.saturation      = lerp(post_fx_base.saturation,      pfx.saturation,      w);
3401            post_fx_base.bloom_intensity = lerp(post_fx_base.bloom_intensity, pfx.bloom_intensity, w);
3402            post_fx_base.vignette        = lerp(post_fx_base.vignette,        pfx.vignette,        w);
3403            post_fx_base.chromatic_ab    = lerp(post_fx_base.chromatic_ab,    pfx.chromatic_ab,    w);
3404            post_fx_base.film_grain      = lerp(post_fx_base.film_grain,      pfx.film_grain,      w);
3405        }
3406        result.post_fx.push(post_fx_base);
3407
3408        // Subtitle tracks
3409        for (_, track) in &self.tracks.subtitle_tracks {
3410            if !track.base.enabled { continue; }
3411            result.active_subtitles.extend(track.active_at(time).into_iter().cloned());
3412        }
3413
3414        // Event tracks — poll
3415        for (_, track) in &mut self.tracks.event_tracks {
3416            if !track.base.enabled { continue; }
3417            let fired = track.poll(prev_time, time);
3418            result.fired_events.extend(fired);
3419        }
3420
3421        // Blend shape tracks
3422        for (_, track) in &self.tracks.blend_shape_tracks {
3423            if !track.base.enabled { continue; }
3424            let weights = track.evaluate(time);
3425            result.blend_shapes.insert(track.entity_id, weights);
3426        }
3427
3428        // Visibility tracks
3429        for (_, track) in &self.tracks.visibility_tracks {
3430            if !track.base.enabled { continue; }
3431            let opacity = track.evaluate_opacity(time);
3432            result.visibility.insert(track.entity_id, opacity);
3433        }
3434
3435        // Update camera shake for all camera tracks
3436        for (_, track) in &mut self.tracks.camera_tracks {
3437            track.update_shake(dt);
3438        }
3439
3440        // Update camera blend
3441        self.update_camera_blend(dt);
3442
3443        // Update letterbox
3444        let max_bar = self.letterbox_events.iter()
3445            .map(|e| e.bar_height_at(100.0, 100.0 * LETTERBOX_ASPECT, time))
3446            .fold(0.0_f32, f32::max);
3447        self.letterbox_amount = max_bar;
3448
3449        result
3450    }
3451
3452    // ---- PLAYBACK ----
3453
3454    pub fn update(&mut self, dt: f32) {
3455        let scaled_dt = dt * self.current_time_scale;
3456        self.playback.update(scaled_dt, self.master_sequence.duration);
3457    }
3458
3459    pub fn play(&mut self)  { self.playback.play(); }
3460    pub fn pause(&mut self) { self.playback.pause(); }
3461    pub fn stop(&mut self)  { self.playback.stop(); self.prev_eval_time = 0.0; }
3462    pub fn scrub(&mut self, t: f64) { self.playback.scrub_to(t); }
3463
3464    pub fn set_loop_region(&mut self, start: f64, end: f64) {
3465        self.playback.loop_start   = start;
3466        self.playback.loop_end     = end;
3467        self.playback.loop_enabled = true;
3468    }
3469
3470    pub fn goto_next_chapter(&mut self) {
3471        let cur = self.playback.current_time;
3472        if let Some(chap) = self.chapter_markers.iter().find(|c| c.time > cur) {
3473            self.playback.scrub_to(chap.time);
3474        }
3475    }
3476
3477    pub fn goto_prev_chapter(&mut self) {
3478        let cur = self.playback.current_time;
3479        if let Some(chap) = self.chapter_markers.iter().rev().find(|c| c.time < cur - 0.5) {
3480            self.playback.scrub_to(chap.time);
3481        }
3482    }
3483
3484    // ---- UNDO / REDO ----
3485
3486    pub fn undo(&mut self) {
3487        if let Some(cmd) = self.undo_history.undo() {
3488            self.apply_undo(cmd);
3489        }
3490    }
3491
3492    pub fn redo(&mut self) {
3493        if let Some(cmd) = self.undo_history.redo() {
3494            self.apply_redo(cmd);
3495        }
3496    }
3497
3498    fn apply_undo(&mut self, cmd: SequencerCommand) {
3499        match cmd {
3500            SequencerCommand::SetTrackEnabled { track_id, old_val, .. } => {
3501                self.tracks.set_track_enabled(track_id, old_val);
3502            }
3503            SequencerCommand::SetDuration { old_duration, .. } => {
3504                self.master_sequence.duration = old_duration;
3505            }
3506            SequencerCommand::AddTrack { track_id, .. } => {
3507                let taken = self.tracks.take_track(track_id);
3508                self.undone_tracks.insert(track_id, taken);
3509            }
3510            _ => {}
3511        }
3512    }
3513
3514    fn apply_redo(&mut self, cmd: SequencerCommand) {
3515        match cmd {
3516            SequencerCommand::SetTrackEnabled { track_id, new_val, .. } => {
3517                self.tracks.set_track_enabled(track_id, new_val);
3518            }
3519            SequencerCommand::SetDuration { new_duration, .. } => {
3520                self.master_sequence.duration = new_duration;
3521            }
3522            // Redo used to ignore AddTrack, so an undone track never came back.
3523            SequencerCommand::AddTrack { track_id, .. } => {
3524                if let Some(taken) = self.undone_tracks.remove(&track_id) {
3525                    self.tracks.restore_from(taken);
3526                }
3527            }
3528            _ => {}
3529        }
3530    }
3531
3532    // ---- COPY / PASTE KEYFRAMES ----
3533
3534    pub fn copy_selected_keyframes(&mut self) {
3535        self.selection.copy_keyframes(self.playback.current_time);
3536    }
3537
3538    pub fn paste_keyframes_at(&mut self, target_time: f64) {
3539        let offset = target_time - self.selection.clipboard_offset;
3540        for (&track_id, times) in &self.selection.clipboard_keyframes {
3541            let new_times: Vec<f64> = times.iter().map(|&t| t + offset).collect();
3542            // For camera tracks, duplicate keyframes at new times
3543            if let Some(track) = self.tracks.camera_tracks.get_mut(&track_id) {
3544                let kfs_to_add: Vec<CameraKeyframe> = new_times.iter().filter_map(|&new_t| {
3545                    // Find original keyframe near original time
3546                    let orig_t = new_t - offset;
3547                    track.keyframes.iter()
3548                        .min_by(|a, b| (a.time - orig_t).abs().partial_cmp(&(b.time - orig_t).abs()).unwrap_or(std::cmp::Ordering::Equal))
3549                        .map(|kf| { let mut kf2 = kf.clone(); kf2.time = new_t; kf2 })
3550                }).collect();
3551                for kf in kfs_to_add {
3552                    track.add_keyframe(kf);
3553                }
3554            }
3555            self.undo_history.push(SequencerCommand::PasteKeyframes { track_id, times: new_times });
3556        }
3557    }
3558
3559    // ---- EXPORT ----
3560
3561    pub fn export_edl(&self) -> EdlDocument {
3562        EdlDocument::from_shot_list(&self.shot_list, self.master_sequence.fps.clone())
3563    }
3564
3565    pub fn export_subtitles_srt(&self) -> String {
3566        let mut combined = String::new();
3567        let mut counter = 1u32;
3568        let fps = self.master_sequence.fps.fps();
3569        // Gather all subtitles sorted by time
3570        let mut all_subs: Vec<&SubtitleKeyframe> = Vec::new();
3571        for track in self.tracks.subtitle_tracks.values() {
3572            all_subs.extend(track.subtitles.iter());
3573        }
3574        all_subs.sort_by(|a, b| a.time.partial_cmp(&b.time).unwrap_or(std::cmp::Ordering::Equal));
3575        for sub in all_subs {
3576            let tc_start = secs_to_srt_tc(sub.time);
3577            let tc_end   = secs_to_srt_tc(sub.end_time);
3578            combined.push_str(&format!("{}\n{} --> {}\n{}\n\n", counter, tc_start, tc_end, sub.text));
3579            counter += 1;
3580        }
3581        combined
3582    }
3583
3584    pub fn bake_to_frames(&self, output_fps: FrameRate) -> Vec<FrameEvalResult> {
3585        // Return pre-baked results for every frame (read-only; does not mutate self)
3586        let total = self.master_sequence.duration;
3587        let frame_count = output_fps.seconds_to_frame(total);
3588        (0..=frame_count).map(|f| {
3589            let time = output_fps.frame_to_seconds(f);
3590            FrameEvalResult::new(time)
3591        }).collect()
3592    }
3593
3594    // ---- STATS ----
3595
3596    pub fn stats(&self) -> SequencerStats {
3597        let total_kfs: usize = self.tracks.camera_tracks.values()
3598            .map(|t| t.keyframes.len()).sum::<usize>()
3599            + self.tracks.actor_tracks.values()
3600                .map(|t| t.keyframes.len()).sum::<usize>()
3601            + self.tracks.transform_tracks.values()
3602                .map(|t| t.keyframes.len()).sum::<usize>();
3603
3604        SequencerStats {
3605            track_count:    self.tracks.track_count(),
3606            shot_count:     self.shot_list.shots.len(),
3607            chapter_count:  self.chapter_markers.len(),
3608            total_keyframes: total_kfs,
3609            duration:       self.master_sequence.duration,
3610            fps:            self.master_sequence.fps.fps(),
3611            frame_count:    self.master_sequence.frame_count(),
3612        }
3613    }
3614
3615    // ---- DURATION MANAGEMENT ----
3616
3617    pub fn set_duration(&mut self, duration: f64) {
3618        let old = self.master_sequence.duration;
3619        self.master_sequence.duration = duration;
3620        self.undo_history.push(SequencerCommand::SetDuration {
3621            old_duration: old, new_duration: duration,
3622        });
3623    }
3624
3625    pub fn expand_to_fit_tracks(&mut self) {
3626        let mut max_t = 0.0_f64;
3627        for t in self.tracks.camera_tracks.values() {
3628            if let Some(last) = t.keyframes.last() { max_t = max_t.max(last.time); }
3629        }
3630        for t in self.tracks.actor_tracks.values() {
3631            if let Some(last) = t.keyframes.last() { max_t = max_t.max(last.time); }
3632        }
3633        for t in self.tracks.subtitle_tracks.values() {
3634            if let Some(last) = t.subtitles.last() { max_t = max_t.max(last.end_time); }
3635        }
3636        for t in self.tracks.audio_tracks.values() {
3637            if let Some(last) = t.clips.last() {
3638                max_t = max_t.max(last.time + last.clip.duration);
3639            }
3640        }
3641        if max_t > self.master_sequence.duration {
3642            self.set_duration(max_t + 1.0);
3643        }
3644    }
3645
3646    // ---- FRAME RATE CONVERSION ----
3647
3648    pub fn convert_fps(&mut self, new_fps: FrameRate) {
3649        let old_fps = self.master_sequence.fps.clone();
3650        // Remap all keyframe times proportionally
3651        // (keyframe times are in seconds, so no conversion needed — just update FPS)
3652        let old = old_fps.clone();
3653        self.master_sequence.fps = new_fps.clone();
3654        self.playback.fps = new_fps.clone();
3655        self.undo_history.push(SequencerCommand::SetFps { old_fps: old, new_fps });
3656    }
3657
3658    // ---- FIND NEAREST KEYFRAME ----
3659
3660    pub fn nearest_camera_keyframe(&self, track_id: u64, time: f64) -> Option<f64> {
3661        self.tracks.camera_tracks.get(&track_id)?.keyframes.iter()
3662            .min_by(|a, b| (a.time - time).abs().partial_cmp(&(b.time - time).abs()).unwrap_or(std::cmp::Ordering::Equal))
3663            .map(|k| k.time)
3664    }
3665}
3666
3667// ============================================================
3668// HELPER FUNCTIONS
3669// ============================================================
3670
3671fn decompose_mat4(mat: Mat4) -> (Vec3, Quat, Vec3) {
3672    let trans = Vec3::new(mat.w_axis.x, mat.w_axis.y, mat.w_axis.z);
3673    let sx = Vec3::new(mat.x_axis.x, mat.x_axis.y, mat.x_axis.z).length();
3674    let sy = Vec3::new(mat.y_axis.x, mat.y_axis.y, mat.y_axis.z).length();
3675    let sz = Vec3::new(mat.z_axis.x, mat.z_axis.y, mat.z_axis.z).length();
3676    let scale = Vec3::new(sx, sy, sz);
3677    let rot_mat = Mat4::from_cols(
3678        mat.x_axis / sx.max(EPSILON),
3679        mat.y_axis / sy.max(EPSILON),
3680        mat.z_axis / sz.max(EPSILON),
3681        Vec4::W,
3682    );
3683    let rot = Quat::from_mat4(&rot_mat);
3684    (scale, rot, trans)
3685}
3686
3687fn secs_to_srt_tc(secs: f64) -> String {
3688    let ms    = ((secs.fract()) * 1000.0) as u32;
3689    let total = secs.floor() as u64;
3690    let h  = total / 3600;
3691    let m  = (total % 3600) / 60;
3692    let s  = total % 60;
3693    format!("{:02}:{:02}:{:02},{:03}", h, m, s, ms)
3694}
3695
3696// ============================================================
3697// SEQUENCER STATS
3698// ============================================================
3699
3700#[derive(Clone, Debug)]
3701pub struct SequencerStats {
3702    pub track_count:     usize,
3703    pub shot_count:      usize,
3704    pub chapter_count:   usize,
3705    pub total_keyframes: usize,
3706    pub duration:        f64,
3707    pub fps:             f32,
3708    pub frame_count:     u64,
3709}
3710
3711// ============================================================
3712// CURVE SAMPLER (for rendering curve editor)
3713// ============================================================
3714
3715pub struct CurveSampler;
3716
3717impl CurveSampler {
3718    /// Sample a float curve for display, returning (time, value) pairs
3719    pub fn sample(curve: &FloatCurve, view_start: f64, view_end: f64, pixel_width: u32) -> Vec<(f64, f32)> {
3720        if pixel_width == 0 { return Vec::new(); }
3721        (0..pixel_width).map(|i| {
3722            let t = lerp_f64(view_start, view_end, i as f64 / pixel_width as f64);
3723            let v = curve.evaluate(t);
3724            (t, v)
3725        }).collect()
3726    }
3727
3728    /// Compute tangent visualization line endpoints for a keyframe
3729    pub fn tangent_handles(curve: &FloatCurve, key_idx: usize, scale: f32) -> Option<(Vec2, Vec2)> {
3730        let key = curve.keys.get(key_idx)?;
3731        let handle = key.bezier_handle.as_ref()?;
3732        let base = Vec2::new(key.time as f32, key.value);
3733        let in_pt  = base + handle.in_tangent  * scale;
3734        let out_pt = base + handle.out_tangent * scale;
3735        Some((in_pt, out_pt))
3736    }
3737
3738    /// Find the pixel x-position of a keyframe in the curve editor view
3739    pub fn keyframe_screen_pos(
3740        key_time: f64,
3741        key_val:  f32,
3742        view_start: f64,
3743        view_end:   f64,
3744        val_min:    f32,
3745        val_max:    f32,
3746        screen_w:   f32,
3747        screen_h:   f32,
3748    ) -> Vec2 {
3749        let tx = ((key_time - view_start) / (view_end - view_start).max(1e-9)) as f32;
3750        let ty = (key_val - val_min) / (val_max - val_min).max(EPSILON);
3751        Vec2::new(tx * screen_w, (1.0 - ty) * screen_h)
3752    }
3753}
3754
3755// ============================================================
3756// MULTI-CURVE BLENDING
3757// ============================================================
3758
3759pub struct CurveBlender {
3760    pub curves:  Vec<(FloatCurve, f32)>, // (curve, weight)
3761}
3762
3763impl CurveBlender {
3764    pub fn new() -> Self { CurveBlender { curves: Vec::new() } }
3765
3766    pub fn add_curve(&mut self, curve: FloatCurve, weight: f32) {
3767        self.curves.push((curve, weight));
3768    }
3769
3770    pub fn evaluate(&self, time: f64) -> f32 {
3771        let total_weight: f32 = self.curves.iter().map(|(_, w)| *w).sum();
3772        if total_weight < EPSILON { return 0.0; }
3773        let weighted_sum: f32 = self.curves.iter().map(|(c, w)| c.evaluate(time) * w).sum();
3774        weighted_sum / total_weight
3775    }
3776
3777    pub fn evaluate_additive(&self, time: f64, base: f32) -> f32 {
3778        let add: f32 = self.curves.iter().map(|(c, w)| c.evaluate(time) * w).sum();
3779        base + add
3780    }
3781}
3782
3783// ============================================================
3784// WAVEFORM PREVIEW DATA
3785// ============================================================
3786
3787pub fn compute_waveform_preview(samples: &[f32], n_buckets: usize) -> Vec<(f32, f32)> {
3788    if samples.is_empty() || n_buckets == 0 { return Vec::new(); }
3789    let bucket_size = (samples.len() / n_buckets).max(1);
3790    (0..n_buckets).map(|i| {
3791        let start = i * bucket_size;
3792        let end   = ((i + 1) * bucket_size).min(samples.len());
3793        let slice = &samples[start..end];
3794        let min = slice.iter().cloned().fold(f32::MAX, f32::min);
3795        let max = slice.iter().cloned().fold(f32::MIN, f32::max);
3796        (min, max)
3797    }).collect()
3798}
3799
3800// ============================================================
3801// KEYFRAME COPYING BETWEEN TRACKS
3802// ============================================================
3803
3804pub fn copy_camera_keyframes_to_transform(
3805    camera_track: &CameraTrack,
3806    transform_track: &mut TransformTrack,
3807) {
3808    for kf in &camera_track.keyframes {
3809        let tkf = TransformKeyframe {
3810            time:     kf.time,
3811            position: kf.position,
3812            rotation: kf.rotation,
3813            scale:    Vec3::ONE,
3814            interp:   kf.interp.clone(),
3815        };
3816        transform_track.add_keyframe(tkf);
3817    }
3818}
3819
3820pub fn mirror_keyframes_time(curve: &mut FloatCurve, pivot_time: f64) {
3821    for key in &mut curve.keys {
3822        key.time = 2.0 * pivot_time - key.time;
3823    }
3824    curve.keys.sort_by(|a, b| a.time.partial_cmp(&b.time).unwrap_or(std::cmp::Ordering::Equal));
3825}
3826
3827pub fn reverse_keyframes(curve: &mut FloatCurve) {
3828    if curve.keys.len() < 2 { return; }
3829    let start = curve.keys.first().unwrap().time;
3830    let end   = curve.keys.last().unwrap().time;
3831    for key in &mut curve.keys {
3832        key.time = start + end - key.time;
3833    }
3834    curve.keys.sort_by(|a, b| a.time.partial_cmp(&b.time).unwrap_or(std::cmp::Ordering::Equal));
3835    // Swap bezier tangents
3836    for key in &mut curve.keys {
3837        if let Some(h) = &mut key.bezier_handle {
3838            let tmp = h.in_tangent;
3839            h.in_tangent  = Vec2::new(-h.out_tangent.x, h.out_tangent.y);
3840            h.out_tangent = Vec2::new(-tmp.x, tmp.y);
3841        }
3842    }
3843}
3844
3845pub fn scale_keyframe_values(curve: &mut FloatCurve, scale: f32) {
3846    for key in &mut curve.keys {
3847        key.value *= scale;
3848        if let Some(h) = &mut key.bezier_handle {
3849            h.in_tangent.y  *= scale;
3850            h.out_tangent.y *= scale;
3851        }
3852    }
3853}
3854
3855pub fn offset_keyframe_times(curve: &mut FloatCurve, offset: f64) {
3856    for key in &mut curve.keys {
3857        key.time += offset;
3858    }
3859}
3860
3861// ============================================================
3862// BATCH OPERATIONS ON MULTIPLE CURVES
3863// ============================================================
3864
3865pub fn align_keyframe_times(curves: &mut [FloatCurve], snap_interval: f64) {
3866    for curve in curves {
3867        for key in &mut curve.keys {
3868            key.time = (key.time / snap_interval).round() * snap_interval;
3869        }
3870    }
3871}
3872
3873pub fn merge_curves(a: &FloatCurve, b: &FloatCurve, blend: f32) -> FloatCurve {
3874    let mut result = FloatCurve::new(&format!("{}_{}_{}", a.name, b.name, blend as u32));
3875    // Collect all unique times
3876    let mut times: Vec<f64> = a.keys.iter().map(|k| k.time)
3877        .chain(b.keys.iter().map(|k| k.time))
3878        .collect();
3879    times.sort_by(|x, y| x.partial_cmp(y).unwrap_or(std::cmp::Ordering::Equal));
3880    times.dedup_by(|x, y| (*x - *y).abs() < 1e-9);
3881    for t in times {
3882        let va = a.evaluate(t);
3883        let vb = b.evaluate(t);
3884        let v  = lerp(va, vb, blend);
3885        result.add_key(t, v, InterpType::Cubic);
3886    }
3887    result
3888}
3889
3890// ============================================================
3891// ANIMATION BAKING FROM CURVE EDITOR
3892// ============================================================
3893
3894pub struct AnimationBaker {
3895    pub source_curves: Vec<FloatCurve>,
3896    pub output_fps: f32,
3897    pub duration:   f64,
3898}
3899
3900impl AnimationBaker {
3901    pub fn new(fps: f32, duration: f64) -> Self {
3902        AnimationBaker { source_curves: Vec::new(), output_fps: fps, duration }
3903    }
3904
3905    pub fn add_curve(&mut self, curve: FloatCurve) {
3906        self.source_curves.push(curve);
3907    }
3908
3909    pub fn bake(&self) -> Vec<Vec<f32>> {
3910        let n_frames = (self.duration * self.output_fps as f64).ceil() as usize + 1;
3911        self.source_curves.iter().map(|curve| {
3912            (0..n_frames).map(|f| {
3913                let t = f as f64 / self.output_fps as f64;
3914                curve.evaluate(t)
3915            }).collect()
3916        }).collect()
3917    }
3918
3919    pub fn bake_to_keyframes(&self, curve_idx: usize, threshold: f32) -> FloatCurve {
3920        let frames = &self.bake()[curve_idx.min(self.source_curves.len().saturating_sub(1))];
3921        let mut result = FloatCurve::new("Baked");
3922        if frames.is_empty() { return result; }
3923        // Keep only frames where value changes significantly
3924        result.add_key(0.0, frames[0], InterpType::Linear);
3925        for i in 1..frames.len() - 1 {
3926            let t = i as f64 / self.output_fps as f64;
3927            let prev = frames[i - 1];
3928            let cur  = frames[i];
3929            let next = frames[i + 1];
3930            // Add keyframe if it deviates from linear interpolation
3931            let expected = lerp(prev, next, 0.5);
3932            if (cur - expected).abs() > threshold {
3933                result.add_key(t, cur, InterpType::Linear);
3934            }
3935        }
3936        let last_t = (frames.len() - 1) as f64 / self.output_fps as f64;
3937        result.add_key(last_t, *frames.last().unwrap(), InterpType::Linear);
3938        result
3939    }
3940}
3941
3942// ============================================================
3943// CINEMATIC DIRECTOR (automatic shot selection)
3944// ============================================================
3945
3946#[derive(Clone, Debug)]
3947pub struct DirectorRule {
3948    pub min_shot_duration: f64,
3949    pub max_shot_duration: f64,
3950    pub prefer_close_cuts: bool,
3951    pub cut_on_action:     bool,
3952    pub cut_on_dialogue:   bool,
3953}
3954
3955impl DirectorRule {
3956    pub fn default_rules() -> Self {
3957        DirectorRule {
3958            min_shot_duration: 2.0,
3959            max_shot_duration: 10.0,
3960            prefer_close_cuts: true,
3961            cut_on_action: true,
3962            cut_on_dialogue: true,
3963        }
3964    }
3965}
3966
3967pub struct CinematicDirector {
3968    pub rules: DirectorRule,
3969    pub available_cameras: Vec<u64>,
3970    pub current_camera_idx: usize,
3971    pub time_since_cut: f64,
3972}
3973
3974impl CinematicDirector {
3975    pub fn new(cameras: Vec<u64>, rules: DirectorRule) -> Self {
3976        CinematicDirector {
3977            rules,
3978            available_cameras: cameras,
3979            current_camera_idx: 0,
3980            time_since_cut: 0.0,
3981        }
3982    }
3983
3984    pub fn update(&mut self, dt: f64, has_action: bool, has_dialogue: bool) -> Option<u64> {
3985        self.time_since_cut += dt;
3986        if self.available_cameras.is_empty() { return None; }
3987        let should_cut = self.should_cut(has_action, has_dialogue);
3988        if should_cut {
3989            self.time_since_cut = 0.0;
3990            self.current_camera_idx = (self.current_camera_idx + 1) % self.available_cameras.len();
3991            Some(self.available_cameras[self.current_camera_idx])
3992        } else {
3993            None
3994        }
3995    }
3996
3997    fn should_cut(&self, has_action: bool, has_dialogue: bool) -> bool {
3998        if self.time_since_cut < self.rules.min_shot_duration { return false; }
3999        if self.time_since_cut >= self.rules.max_shot_duration { return true; }
4000        if self.rules.cut_on_action && has_action { return true; }
4001        if self.rules.cut_on_dialogue && has_dialogue { return true; }
4002        false
4003    }
4004
4005    pub fn current_camera(&self) -> Option<u64> {
4006        self.available_cameras.get(self.current_camera_idx).cloned()
4007    }
4008}
4009
4010// ============================================================
4011// EXTRA CURVE MATH
4012// ============================================================
4013
4014/// Area under a float curve (definite integral)
4015pub fn integrate_curve(curve: &FloatCurve, t_start: f64, t_end: f64, steps: usize) -> f32 {
4016    if steps == 0 || t_end <= t_start { return 0.0; }
4017    let dt = (t_end - t_start) / steps as f64;
4018    let mut sum = 0.0_f32;
4019    for i in 0..steps {
4020        let t0 = t_start + i as f64 * dt;
4021        let t1 = t0 + dt;
4022        sum += (curve.evaluate(t0) + curve.evaluate(t1)) * 0.5 * dt as f32;
4023    }
4024    sum
4025}
4026
4027/// Derivative of a float curve at t (numerical)
4028pub fn curve_derivative(curve: &FloatCurve, t: f64) -> f32 {
4029    let dt = 1e-5;
4030    let a = curve.evaluate(t + dt);
4031    let b = curve.evaluate(t - dt);
4032    (a - b) / (2.0 * dt as f32)
4033}
4034
4035/// Find roots of a float curve (zero crossings)
4036pub fn find_zero_crossings(curve: &FloatCurve, t_start: f64, t_end: f64, steps: usize) -> Vec<f64> {
4037    let mut crossings = Vec::new();
4038    let dt = (t_end - t_start) / steps as f64;
4039    let mut prev_v = curve.evaluate(t_start);
4040    for i in 1..=steps {
4041        let t = t_start + i as f64 * dt;
4042        let v = curve.evaluate(t);
4043        if prev_v * v < 0.0 {
4044            // Bisect
4045            let mut lo = t - dt;
4046            let mut hi = t;
4047            for _ in 0..32 {
4048                let mid = (lo + hi) * 0.5;
4049                let vm = curve.evaluate(mid);
4050                if vm * curve.evaluate(lo) <= 0.0 { hi = mid; } else { lo = mid; }
4051            }
4052            crossings.push((lo + hi) * 0.5);
4053        }
4054        prev_v = v;
4055    }
4056    crossings
4057}
4058
4059/// Find local minima/maxima of a float curve
4060pub fn find_extrema(curve: &FloatCurve, t_start: f64, t_end: f64, steps: usize) -> Vec<(f64, f32, bool)> {
4061    // Returns (time, value, is_max)
4062    let mut extrema = Vec::new();
4063    let dt = (t_end - t_start) / steps as f64;
4064    let mut prev_d = curve_derivative(curve, t_start);
4065    for i in 1..=steps {
4066        let t = t_start + i as f64 * dt;
4067        let d = curve_derivative(curve, t);
4068        if prev_d * d < 0.0 {
4069            let mut lo = t - dt;
4070            let mut hi = t;
4071            for _ in 0..32 {
4072                let mid = (lo + hi) * 0.5;
4073                let dm = curve_derivative(curve, mid);
4074                if dm * curve_derivative(curve, lo) <= 0.0 { hi = mid; } else { lo = mid; }
4075            }
4076            let t_ext = (lo + hi) * 0.5;
4077            let v_ext = curve.evaluate(t_ext);
4078            extrema.push((t_ext, v_ext, prev_d > 0.0));
4079        }
4080        prev_d = d;
4081    }
4082    extrema
4083}
4084
4085// ============================================================
4086// FRAME INTERPOLATION QUALITY METRICS
4087// ============================================================
4088
4089pub struct InterpolationQualityMetrics {
4090    pub max_velocity:     f32,
4091    pub max_acceleration: f32,
4092    pub total_variation:  f32,
4093    pub jitter:           f32,
4094}
4095
4096impl InterpolationQualityMetrics {
4097    pub fn compute(curve: &FloatCurve, t_start: f64, t_end: f64, steps: usize) -> Self {
4098        let dt = (t_end - t_start) / steps as f64;
4099        let vals: Vec<f32> = (0..=steps)
4100            .map(|i| curve.evaluate(t_start + i as f64 * dt))
4101            .collect();
4102        let velocities: Vec<f32> = vals.windows(2)
4103            .map(|w| (w[1] - w[0]) / dt as f32)
4104            .collect();
4105        let accels: Vec<f32> = velocities.windows(2)
4106            .map(|w| (w[1] - w[0]) / dt as f32)
4107            .collect();
4108        let jerks: Vec<f32> = accels.windows(2)
4109            .map(|w| (w[1] - w[0]) / dt as f32)
4110            .collect();
4111        InterpolationQualityMetrics {
4112            max_velocity:     velocities.iter().cloned().map(f32::abs).fold(0.0_f32, f32::max),
4113            max_acceleration: accels.iter().cloned().map(f32::abs).fold(0.0_f32, f32::max),
4114            total_variation:  velocities.iter().cloned().map(f32::abs).sum(),
4115            jitter:           jerks.iter().cloned().map(f32::abs).fold(0.0_f32, f32::max),
4116        }
4117    }
4118}
4119
4120// ============================================================
4121// MOTION PATH EXTRACTION
4122// ============================================================
4123
4124pub fn extract_motion_path(actor_track: &ActorTrack, steps: usize) -> Vec<Vec3> {
4125    if actor_track.keyframes.len() < 2 { return Vec::new(); }
4126    let t_start = actor_track.keyframes.first().unwrap().time;
4127    let t_end   = actor_track.keyframes.last().unwrap().time;
4128    let dt = (t_end - t_start) / steps.max(1) as f64;
4129    (0..=steps).map(|i| {
4130        let t = t_start + i as f64 * dt;
4131        let (pos, _, _) = actor_track.evaluate(t);
4132        pos
4133    }).collect()
4134}
4135
4136pub fn smooth_motion_path(path: &[Vec3], window: usize) -> Vec<Vec3> {
4137    let n = path.len();
4138    if n < 3 || window < 2 { return path.to_vec(); }
4139    let half_w = window / 2;
4140    (0..n).map(|i| {
4141        let start = i.saturating_sub(half_w);
4142        let end   = (i + half_w + 1).min(n);
4143        let sum: Vec3 = path[start..end].iter().cloned().sum();
4144        sum / (end - start) as f32
4145    }).collect()
4146}
4147
4148// ============================================================
4149// SEQUENCE THUMBNAIL DATA
4150// ============================================================
4151
4152#[derive(Clone, Debug)]
4153pub struct SequenceThumbnail {
4154    pub time:   f64,
4155    pub width:  u32,
4156    pub height: u32,
4157    pub pixels: Vec<u8>,  // RGBA
4158}
4159
4160impl SequenceThumbnail {
4161    pub fn placeholder(time: f64, w: u32, h: u32) -> Self {
4162        let n = (w * h * 4) as usize;
4163        let t = (time.fract() * 255.0) as u8;
4164        let pixels = (0..n).map(|i| match i % 4 { 0 => t, 1 => 128, 2 => 255 - t, _ => 255 }).collect();
4165        SequenceThumbnail { time, width: w, height: h, pixels }
4166    }
4167}
4168
4169// ============================================================
4170// FRAME PACING ANALYSIS
4171// ============================================================
4172
4173pub fn analyze_frame_pacing(timestamps: &[f64]) -> FramePacingReport {
4174    let n = timestamps.len();
4175    if n < 2 {
4176        return FramePacingReport { avg_dt: 0.0, std_dev: 0.0, min_dt: 0.0, max_dt: 0.0, jank_frames: 0 };
4177    }
4178    let dts: Vec<f64> = timestamps.windows(2).map(|w| w[1] - w[0]).collect();
4179    let avg = dts.iter().sum::<f64>() / dts.len() as f64;
4180    let variance = dts.iter().map(|&d| (d - avg).powi(2)).sum::<f64>() / dts.len() as f64;
4181    let std_dev  = variance.sqrt();
4182    let min_dt   = dts.iter().cloned().fold(f64::MAX, f64::min);
4183    let max_dt   = dts.iter().cloned().fold(f64::MIN, f64::max);
4184    let jank     = dts.iter().filter(|&&d| d > avg * 1.5).count();
4185    FramePacingReport {
4186        avg_dt:  avg  as f32,
4187        std_dev: std_dev as f32,
4188        min_dt:  min_dt as f32,
4189        max_dt:  max_dt as f32,
4190        jank_frames: jank,
4191    }
4192}
4193
4194#[derive(Clone, Debug)]
4195pub struct FramePacingReport {
4196    pub avg_dt:     f32,
4197    pub std_dev:    f32,
4198    pub min_dt:     f32,
4199    pub max_dt:     f32,
4200    pub jank_frames: usize,
4201}
4202
4203// ============================================================
4204// PROCEDURAL ANIMATION CURVES
4205// ============================================================
4206
4207/// Oscillating spring curve: x(t) = A * e^(-ζωt) * cos(ωd*t + φ)
4208pub fn spring_curve(
4209    time: f32,
4210    initial_value:    f32,
4211    target_value:     f32,
4212    angular_freq:     f32,  // ω₀
4213    damping_ratio:    f32,  // ζ
4214) -> f32 {
4215    let delta = initial_value - target_value;
4216    let wd = angular_freq * (1.0 - damping_ratio * damping_ratio).max(0.0).sqrt();
4217    let decay = (-damping_ratio * angular_freq * time).exp();
4218    if wd < EPSILON {
4219        // Critically or overdamped
4220        let b = delta * (1.0 + damping_ratio * angular_freq * time);
4221        target_value + b * decay
4222    } else {
4223        let phase = 0.0_f32; // initial velocity = 0
4224        target_value + delta * decay * (wd * time + phase).cos()
4225    }
4226}
4227
4228/// Elastic bounce-back curve
4229pub fn elastic_out(t: f32, amplitude: f32, period: f32) -> f32 {
4230    let t = clamp01(t);
4231    if t <= 0.0 { return 0.0; }
4232    if t >= 1.0 { return 1.0; }
4233    let p = period;
4234    let a = amplitude.max(1.0);
4235    let s = (a / (2.0 * std::f32::consts::PI)) * (1.0_f32 / a).asin();
4236    a * 2.0_f32.powf(-10.0 * t)
4237        * ((t - s) * (2.0 * std::f32::consts::PI) / p).sin()
4238        + 1.0
4239}
4240
4241/// Back easing (overshoot)
4242pub fn ease_out_back(t: f32, overshoot: f32) -> f32 {
4243    let t = clamp01(t);
4244    let t1 = t - 1.0;
4245    t1 * t1 * ((overshoot + 1.0) * t1 + overshoot) + 1.0
4246}
4247
4248/// Bounce easing
4249pub fn ease_out_bounce(t: f32) -> f32 {
4250    let t = clamp01(t);
4251    if t < 1.0 / 2.75 {
4252        7.5625 * t * t
4253    } else if t < 2.0 / 2.75 {
4254        let t2 = t - 1.5 / 2.75;
4255        7.5625 * t2 * t2 + 0.75
4256    } else if t < 2.5 / 2.75 {
4257        let t2 = t - 2.25 / 2.75;
4258        7.5625 * t2 * t2 + 0.9375
4259    } else {
4260        let t2 = t - 2.625 / 2.75;
4261        7.5625 * t2 * t2 + 0.984375
4262    }
4263}
4264
4265// ============================================================
4266// KEYFRAME REDUCTION (LOD for animations)
4267// ============================================================
4268
4269pub fn reduce_keyframes(curve: &FloatCurve, max_error: f32) -> FloatCurve {
4270    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 }); }
4271    let times:  Vec<f64> = curve.keys.iter().map(|k| k.time).collect();
4272    let values: Vec<f32> = curve.keys.iter().map(|k| k.value).collect();
4273    // Douglas-Peucker style reduction on (time, value) pairs
4274    let keep = rdp_reduce(&times, &values, max_error as f64);
4275    let mut result = FloatCurve::new(&curve.name);
4276    for i in keep {
4277        result.add_key(times[i], values[i], InterpType::Cubic);
4278    }
4279    result
4280}
4281
4282fn rdp_reduce(times: &[f64], values: &[f32], epsilon: f64) -> Vec<usize> {
4283    let n = times.len();
4284    if n < 3 { return (0..n).collect(); }
4285    let mut max_dist = 0.0_f64;
4286    let mut max_idx  = 0usize;
4287    let t0 = times[0]; let v0 = values[0] as f64;
4288    let tn = times[n-1]; let vn = values[n-1] as f64;
4289    for i in 1..n-1 {
4290        let t = times[i]; let v = values[i] as f64;
4291        // Perpendicular distance from point to line (t0,v0)-(tn,vn)
4292        let num = ((vn-v0)*(t0-t) - (tn-t0)*(v0-v)).abs();
4293        let den = ((vn-v0).powi(2) + (tn-t0).powi(2)).sqrt();
4294        let d   = if den < 1e-12 { 0.0 } else { num / den };
4295        if d > max_dist { max_dist = d; max_idx = i; }
4296    }
4297    if max_dist > epsilon {
4298        let mut left  = rdp_reduce(&times[..=max_idx], &values[..=max_idx], epsilon);
4299        let right_raw = rdp_reduce(&times[max_idx..], &values[max_idx..], epsilon);
4300        let right: Vec<usize> = right_raw.iter().map(|&i| i + max_idx).collect();
4301        left.pop(); // remove duplicate
4302        left.extend(right);
4303        left
4304    } else {
4305        vec![0, n-1]
4306    }
4307}
4308
4309// ============================================================
4310// UNIT TESTS
4311// ============================================================
4312
4313#[cfg(test)]
4314mod tests {
4315    use super::*;
4316
4317    #[test]
4318    fn test_timecode_roundtrip() {
4319        let tc = Timecode::new(1, 23, 45, 12);
4320        let frame = tc.to_frame(30.0);
4321        let tc2 = Timecode::from_frame(frame, 30.0);
4322        assert_eq!(tc.hours,   tc2.hours);
4323        assert_eq!(tc.minutes, tc2.minutes);
4324        assert_eq!(tc.seconds, tc2.seconds);
4325        assert_eq!(tc.frames,  tc2.frames);
4326    }
4327
4328    #[test]
4329    fn test_float_curve_linear() {
4330        let mut curve = FloatCurve::new("test");
4331        curve.add_key(0.0, 0.0, InterpType::Linear);
4332        curve.add_key(1.0, 1.0, InterpType::Linear);
4333        let v05 = curve.evaluate(0.5);
4334        assert!((v05 - 0.5).abs() < 0.001, "Linear interp mid should be 0.5");
4335    }
4336
4337    #[test]
4338    fn test_float_curve_constant() {
4339        let mut curve = FloatCurve::new("test");
4340        curve.add_key(0.0, 3.0, InterpType::Constant);
4341        curve.add_key(1.0, 7.0, InterpType::Constant);
4342        let v = curve.evaluate(0.5);
4343        assert!((v - 3.0).abs() < EPSILON, "Constant interp should return first value");
4344    }
4345
4346    #[test]
4347    fn test_catmull_rom_symmetry() {
4348        let v = catmull_rom_4pt(0.0, 1.0, 1.0, 0.0, 0.5);
4349        assert!(v > 0.9, "CR midpoint of plateau should stay near 1.0");
4350    }
4351
4352    #[test]
4353    fn test_camera_track_evaluate() {
4354        let mut track = CameraTrack::new(1, "Cam");
4355        track.add_keyframe(CameraKeyframe::new(0.0, Vec3::ZERO, Quat::IDENTITY));
4356        track.add_keyframe(CameraKeyframe::new(1.0, Vec3::X * 10.0, Quat::IDENTITY));
4357        let mid = track.evaluate_position(0.5);
4358        assert!((mid.x - 5.0).abs() < 0.1, "Camera should be at x=5 at t=0.5");
4359    }
4360
4361    #[test]
4362    fn test_actor_track_evaluate() {
4363        let mut track = ActorTrack::new(1, "Actor", 42);
4364        track.add_keyframe(ActorKeyframe::new(0.0, Vec3::ZERO, Quat::IDENTITY));
4365        track.add_keyframe(ActorKeyframe::new(2.0, Vec3::new(10.0, 0.0, 0.0), Quat::IDENTITY));
4366        let (pos, _, _) = track.evaluate(1.0);
4367        assert!((pos.x - 5.0).abs() < 0.1);
4368    }
4369
4370    #[test]
4371    fn test_frame_rate_conversion() {
4372        let frame_24 = 24u64;
4373        let frame_30 = FrameRate::convert_frame(frame_24, FrameRate::Fps24, FrameRate::Fps30);
4374        assert_eq!(frame_30, 30);
4375    }
4376
4377    #[test]
4378    fn test_timecode_srt_format() {
4379        let s = secs_to_srt_tc(3723.5);
4380        assert_eq!(s, "01:02:03,500", "SRT format mismatch: got {}", s);
4381    }
4382
4383    #[test]
4384    fn test_blend_shape_evaluate() {
4385        let mut track = BlendShapeTrack::new(1, "Morph", 10);
4386        track.add_channel("smile");
4387        let mut kf0 = BlendShapeKeyframe::new(0.0).set_weight("smile", 0.0);
4388        let mut kf1 = BlendShapeKeyframe::new(1.0).set_weight("smile", 1.0);
4389        track.add_keyframe(kf0);
4390        track.add_keyframe(kf1);
4391        let weights = track.evaluate(0.5);
4392        let smile = weights.get("smile").cloned().unwrap_or(0.0);
4393        assert!((smile - 0.5).abs() < 0.1, "Blend shape at 0.5 should be ~0.5");
4394    }
4395
4396    #[test]
4397    fn test_visibility_track() {
4398        let mut track = VisibilityTrack::new(1, "Vis", 5);
4399        track.add_keyframe(VisibilityKeyframe::new(0.0, true));
4400        track.add_keyframe(VisibilityKeyframe { time: 1.0, visible: false, opacity: 0.0, fade: 0.5 });
4401        assert!(track.is_visible_at(0.1));
4402    }
4403
4404    #[test]
4405    fn test_time_dilation() {
4406        let mut track = TimeDilationTrack::new(1, "TD");
4407        track.add_keyframe(TimeDilationKeyframe::new(0.0, 0.5));
4408        track.add_keyframe(TimeDilationKeyframe::new(2.0, 1.0));
4409        let scale_at_0 = track.evaluate(0.0);
4410        assert!((scale_at_0 - 0.5).abs() < 0.01);
4411        let scale_at_2 = track.evaluate(2.0);
4412        assert!((scale_at_2 - 1.0).abs() < 0.01);
4413    }
4414
4415    #[test]
4416    fn test_sequencer_create_and_update() {
4417        let mut seq = CinematicSequencer::new("Test", 10.0, FrameRate::Fps30);
4418        let cam_id = seq.add_camera_track("MainCam");
4419        let actor_id = seq.add_actor_track("Hero", 1);
4420        assert_eq!(seq.tracks.track_count(), 2);
4421        seq.play();
4422        for _ in 0..30 {
4423            seq.update(1.0 / 30.0);
4424        }
4425        assert!(seq.playback.current_time > 0.9);
4426    }
4427
4428    #[test]
4429    fn test_curve_cycle_infinity() {
4430        let mut curve = FloatCurve::new("cyclic");
4431        curve.add_key(0.0, 0.0, InterpType::Linear);
4432        curve.add_key(1.0, 1.0, InterpType::Linear);
4433        curve.post_infinity = InfinityMode::Cycle;
4434        let v = curve.evaluate(1.5);
4435        assert!((v - 0.5).abs() < 0.01, "Cyclic: t=1.5 should map to t=0.5 within [0,1]");
4436    }
4437
4438    #[test]
4439    fn test_spring_curve_approaches_target() {
4440        let v_final = spring_curve(10.0, 0.0, 1.0, 10.0, 0.7);
4441        assert!((v_final - 1.0).abs() < 0.01, "Spring should converge to target");
4442    }
4443
4444    #[test]
4445    fn test_edl_generation() {
4446        let mut seq = CinematicSequencer::new("MovieSeq", 30.0, FrameRate::Fps24);
4447        seq.add_shot("Scene01", 0.0,  5.0, 1);
4448        seq.add_shot("Scene02", 5.0, 12.0, 2);
4449        seq.add_shot("Scene03", 12.0, 30.0, 3);
4450        let edl = seq.export_edl();
4451        assert_eq!(edl.entries.len(), 3);
4452        let edl_str = edl.to_string();
4453        assert!(edl_str.contains("TITLE:"));
4454        assert!(edl_str.contains("001"));
4455    }
4456
4457    #[test]
4458    fn test_undo_redo() {
4459        let mut seq = CinematicSequencer::new("UndoTest", 10.0, FrameRate::Fps30);
4460        seq.add_camera_track("Cam1");
4461        let initial_count = seq.tracks.track_count();
4462        seq.undo(); // undo AddTrack
4463        assert_eq!(seq.tracks.track_count(), initial_count - 1);
4464        seq.redo(); // redo AddTrack
4465        assert_eq!(seq.tracks.track_count(), initial_count);
4466    }
4467
4468    #[test]
4469    fn test_audio_beat_generation() {
4470        let mut track = AudioTrack::new(1, "Music");
4471        track.generate_beat_markers(120.0, 0.0, 4.0, 4);
4472        // At 120 BPM, beat every 0.5s, 4s = 8 beats
4473        assert_eq!(track.beat_markers.len(), 8);
4474        assert!(track.beat_markers[0].is_downbeat);
4475        assert!(!track.beat_markers[1].is_downbeat);
4476    }
4477
4478    #[test]
4479    fn test_subtitle_srt_export() {
4480        let mut seq = CinematicSequencer::new("SubTest", 10.0, FrameRate::Fps25);
4481        let tid = seq.add_subtitle_track("EN");
4482        seq.add_subtitle(tid, SubtitleKeyframe::new(1.0, 3.0, "Hello world"));
4483        seq.add_subtitle(tid, SubtitleKeyframe::new(4.0, 6.0, "Goodbye world"));
4484        let srt = seq.export_subtitles_srt();
4485        assert!(srt.contains("Hello world"));
4486        assert!(srt.contains("Goodbye world"));
4487        assert!(srt.contains("-->"));
4488    }
4489}
4490
4491// ============================================================
4492// SEQUENCER TIMELINE VIEW STATE
4493// ============================================================
4494
4495#[derive(Clone, Debug)]
4496pub struct TimelineViewState {
4497    pub view_start:    f64,    // seconds
4498    pub view_end:      f64,
4499    pub scroll_y:      f32,
4500    pub track_heights: HashMap<u64, f32>,
4501    pub zoom_level:    f32,
4502    pub snap_mode:     SnapMode,
4503    pub show_waveforms: bool,
4504    pub show_thumbnails: bool,
4505    pub collapsed_groups: HashSet<u64>,
4506}
4507
4508#[derive(Clone, Debug, PartialEq)]
4509pub enum SnapMode {
4510    None,
4511    Frames,
4512    Seconds,
4513    BeatGrid(f32), // BPM
4514    Custom(f64),
4515}
4516
4517impl TimelineViewState {
4518    pub fn new(duration: f64) -> Self {
4519        TimelineViewState {
4520            view_start: 0.0,
4521            view_end:   duration.min(30.0),
4522            scroll_y:   0.0,
4523            track_heights: HashMap::new(),
4524            zoom_level: 1.0,
4525            snap_mode:  SnapMode::Frames,
4526            show_waveforms: true,
4527            show_thumbnails: false,
4528            collapsed_groups: HashSet::new(),
4529        }
4530    }
4531
4532    pub fn time_to_screen_x(&self, time: f64, screen_w: f32) -> f32 {
4533        let frac = (time - self.view_start) / (self.view_end - self.view_start).max(1e-9);
4534        frac as f32 * screen_w
4535    }
4536
4537    pub fn screen_x_to_time(&self, x: f32, screen_w: f32) -> f64 {
4538        let frac = x as f64 / screen_w as f64;
4539        self.view_start + frac * (self.view_end - self.view_start)
4540    }
4541
4542    pub fn snap_time(&self, time: f64, fps: f32) -> f64 {
4543        match self.snap_mode {
4544            SnapMode::None       => time,
4545            SnapMode::Frames     => (time * fps as f64).round() / fps as f64,
4546            SnapMode::Seconds    => time.round(),
4547            SnapMode::BeatGrid(bpm) => {
4548                let beat = 60.0 / bpm as f64;
4549                (time / beat).round() * beat
4550            }
4551            SnapMode::Custom(interval) => (time / interval).round() * interval,
4552        }
4553    }
4554
4555    pub fn zoom_in(&mut self, center: f64, factor: f32) {
4556        let range = self.view_end - self.view_start;
4557        let new_range = range / factor as f64;
4558        self.view_start = center - new_range * 0.5;
4559        self.view_end   = center + new_range * 0.5;
4560        self.view_start = self.view_start.max(0.0);
4561    }
4562
4563    pub fn zoom_out(&mut self, center: f64, factor: f32, duration: f64) {
4564        let range = self.view_end - self.view_start;
4565        let new_range = (range * factor as f64).min(duration * 1.1);
4566        self.view_start = (center - new_range * 0.5).max(0.0);
4567        self.view_end   = self.view_start + new_range;
4568    }
4569
4570    pub fn pan(&mut self, delta_time: f64, duration: f64) {
4571        self.view_start = (self.view_start + delta_time).max(0.0);
4572        self.view_end   = self.view_start + (self.view_end - self.view_start);
4573        if self.view_end > duration { self.view_end = duration; self.view_start = self.view_end - (self.view_end - self.view_start); }
4574    }
4575
4576    pub fn track_height(&self, track_id: u64) -> f32 {
4577        self.track_heights.get(&track_id).cloned().unwrap_or(32.0)
4578    }
4579
4580    pub fn visible_time_range(&self) -> (f64, f64) {
4581        (self.view_start, self.view_end)
4582    }
4583}
4584
4585// ============================================================
4586// TRACK GROUP
4587// ============================================================
4588
4589#[derive(Clone, Debug)]
4590pub struct TrackGroup {
4591    pub id:       u64,
4592    pub name:     String,
4593    pub color:    Vec4,
4594    pub track_ids: Vec<u64>,
4595    pub collapsed: bool,
4596    pub muted:    bool,
4597    pub solo:     bool,
4598}
4599
4600impl TrackGroup {
4601    pub fn new(id: u64, name: &str) -> Self {
4602        TrackGroup {
4603            id, name: name.to_string(),
4604            color: Vec4::new(0.5, 0.5, 1.0, 1.0),
4605            track_ids: Vec::new(),
4606            collapsed: false,
4607            muted: false,
4608            solo: false,
4609        }
4610    }
4611
4612    pub fn add_track(&mut self, id: u64) {
4613        if !self.track_ids.contains(&id) { self.track_ids.push(id); }
4614    }
4615
4616    pub fn remove_track(&mut self, id: u64) {
4617        self.track_ids.retain(|&tid| tid != id);
4618    }
4619}
4620
4621// ============================================================
4622// SEQUENCE LOCATOR (find things by time)
4623// ============================================================
4624
4625pub struct SequenceLocator;
4626
4627impl SequenceLocator {
4628    pub fn find_camera_keyframes_in_range(
4629        track: &CameraTrack,
4630        t_start: f64,
4631        t_end: f64,
4632    ) -> Vec<usize> {
4633        track.keyframes.iter().enumerate()
4634            .filter(|(_, k)| k.time >= t_start && k.time <= t_end)
4635            .map(|(i, _)| i)
4636            .collect()
4637    }
4638
4639    pub fn find_events_in_range(
4640        track: &EventTrack,
4641        t_start: f64,
4642        t_end: f64,
4643    ) -> Vec<usize> {
4644        track.events.iter().enumerate()
4645            .filter(|(_, e)| e.time >= t_start && e.time <= t_end)
4646            .map(|(i, _)| i)
4647            .collect()
4648    }
4649
4650    pub fn find_subtitles_overlapping(
4651        track: &SubtitleTrack,
4652        t_start: f64,
4653        t_end: f64,
4654    ) -> Vec<usize> {
4655        track.subtitles.iter().enumerate()
4656            .filter(|(_, s)| s.time < t_end && s.end_time > t_start)
4657            .map(|(i, _)| i)
4658            .collect()
4659    }
4660}
4661
4662// ============================================================
4663// FLOAT CURVE BATCH OPERATIONS
4664// ============================================================
4665
4666pub fn mirror_curve_time(curve: &mut FloatCurve, pivot: f64) {
4667    for k in &mut curve.keys { k.time = 2.0 * pivot - k.time; }
4668    curve.keys.sort_by(|a, b| a.time.partial_cmp(&b.time).unwrap_or(std::cmp::Ordering::Equal));
4669}
4670
4671pub fn reverse_curve(curve: &mut FloatCurve) {
4672    if curve.keys.len() < 2 { return; }
4673    let t0 = curve.keys.first().unwrap().time;
4674    let t1 = curve.keys.last().unwrap().time;
4675    for k in &mut curve.keys { k.time = t0 + t1 - k.time; }
4676    curve.keys.sort_by(|a, b| a.time.partial_cmp(&b.time).unwrap_or(std::cmp::Ordering::Equal));
4677    for k in &mut curve.keys {
4678        if let Some(h) = &mut k.bezier_handle {
4679            let tmp = h.in_tangent;
4680            h.in_tangent  = Vec2::new(-h.out_tangent.x, h.out_tangent.y);
4681            h.out_tangent = Vec2::new(-tmp.x, tmp.y);
4682        }
4683    }
4684}
4685
4686pub fn scale_curve_values(curve: &mut FloatCurve, scale: f32) {
4687    for k in &mut curve.keys {
4688        k.value *= scale;
4689        if let Some(h) = &mut k.bezier_handle {
4690            h.in_tangent.y  *= scale;
4691            h.out_tangent.y *= scale;
4692        }
4693    }
4694}
4695
4696pub fn offset_curve_times(curve: &mut FloatCurve, offset: f64) {
4697    for k in &mut curve.keys { k.time += offset; }
4698}
4699
4700pub fn clamp_curve_values(curve: &mut FloatCurve, min: f32, max: f32) {
4701    for k in &mut curve.keys { k.value = k.value.clamp(min, max); }
4702}
4703
4704pub fn snap_curve_times(curve: &mut FloatCurve, interval: f64) {
4705    for k in &mut curve.keys { k.time = (k.time / interval).round() * interval; }
4706}
4707
4708
4709// ============================================================
4710// ADDITIONAL UNIT TESTS
4711// ============================================================
4712
4713#[cfg(test)]
4714mod tests_extended {
4715    use super::*;
4716
4717    #[test]
4718    fn test_float_curve_bezier_endpoints() {
4719        let mut curve = FloatCurve::new("bezier");
4720        curve.add_key_bezier(0.0, 0.0, BezierHandle::flat());
4721        curve.add_key_bezier(1.0, 1.0, BezierHandle::flat());
4722        let v0 = curve.evaluate(0.0);
4723        let v1 = curve.evaluate(1.0);
4724        assert!((v0 - 0.0).abs() < 0.001);
4725        assert!((v1 - 1.0).abs() < 0.001);
4726    }
4727
4728    #[test]
4729    fn test_post_fx_blending() {
4730        let mut seq = CinematicSequencer::new("PFX", 5.0, FrameRate::Fps30);
4731        let pfx_id = seq.add_post_fx_track("GlobalPFX");
4732        if let Some(track) = seq.tracks.post_fx_tracks.get_mut(&pfx_id) {
4733            track.add_keyframe(PostFxKeyframe { vignette: 0.0, ..PostFxKeyframe::default_at(0.0) });
4734            track.add_keyframe(PostFxKeyframe { vignette: 1.0, ..PostFxKeyframe::default_at(5.0) });
4735        }
4736        let pfx = seq.tracks.post_fx_tracks[&pfx_id].evaluate(2.5);
4737        assert!(pfx.vignette > 0.4 && pfx.vignette < 0.6, "PFX midpoint vignette ~ 0.5");
4738    }
4739
4740    #[test]
4741    fn test_blend_camera_matrix() {
4742        let mut seq = CinematicSequencer::new("BlendCam", 10.0, FrameRate::Fps30);
4743        let cam_a = seq.add_camera_track("CamA");
4744        let cam_b = seq.add_camera_track("CamB");
4745        {
4746            let track = seq.tracks.camera_tracks.get_mut(&cam_a).unwrap();
4747            track.add_keyframe(CameraKeyframe::new(0.0, Vec3::ZERO, Quat::IDENTITY));
4748        }
4749        {
4750            let track = seq.tracks.camera_tracks.get_mut(&cam_b).unwrap();
4751            track.add_keyframe(CameraKeyframe::new(0.0, Vec3::X * 10.0, Quat::IDENTITY));
4752        }
4753        seq.cut_to_camera(cam_a);
4754        seq.blend_to_camera(cam_b, 1.0);
4755        seq.camera_blend_t = 0.5;
4756        let mat = seq.blended_camera_matrix(0.0);
4757        // Position should be approximately midpoint
4758        let pos = Vec3::new(mat.w_axis.x, mat.w_axis.y, mat.w_axis.z);
4759        assert!(pos.x > 2.0 && pos.x < 8.0, "Blended camera X should be between 0 and 10");
4760    }
4761
4762    #[test]
4763    fn test_edl_cmx_format() {
4764        let edl = EdlDocument::new("TestEDL", FrameRate::Fps24);
4765        let s = edl.to_string();
4766        assert!(s.starts_with("TITLE: TestEDL"));
4767        assert!(s.contains("FCM:"));
4768    }
4769
4770    #[test]
4771    fn test_light_temperature_rgb() {
4772        let rgb_daylight = LightKeyframe::temperature_to_rgb(6500.0);
4773        let rgb_candle   = LightKeyframe::temperature_to_rgb(1900.0);
4774        // Daylight should be close to white
4775        assert!(rgb_daylight.x > 0.8);
4776        // Candlelight should be very orange (red > blue)
4777        assert!(rgb_candle.x > rgb_candle.z, "Candle: red > blue");
4778    }
4779
4780    #[test]
4781    fn test_visibility_opacity_fade() {
4782        let mut track = VisibilityTrack::new(1, "V", 10);
4783        track.add_keyframe(VisibilityKeyframe { time: 0.0, visible: true,  opacity: 1.0, fade: 0.0 });
4784        track.add_keyframe(VisibilityKeyframe { time: 2.0, visible: false, opacity: 0.0, fade: 1.0 });
4785        let op_at_0 = track.evaluate_opacity(0.0);
4786        assert!((op_at_0 - 1.0).abs() < 0.01);
4787    }
4788
4789    #[test]
4790    fn test_playback_controller_loop() {
4791        let mut pb = PlaybackController::new(FrameRate::Fps30);
4792        pb.loop_enabled = true;
4793        pb.loop_start = 0.0;
4794        pb.loop_end   = 1.0;
4795        pb.play();
4796        for _ in 0..60 { pb.update(1.0/30.0, 5.0); }
4797        // After 2 seconds with 1s loop, should have wrapped
4798        assert!(pb.current_time < 1.0 + 0.1);
4799    }
4800
4801    #[test]
4802    fn test_audio_sidechain_duck() {
4803        let mut track = AudioTrack::new(1, "Music");
4804        let clip = AudioClipData::new(1, "kick", 4.0, 44100);
4805        let mut kf = AudioKeyframe::new(0.0, clip);
4806        kf.duck_others = true;
4807        kf.duck_amount = 0.5;
4808        kf.duck_release = 0.5;
4809        track.add_clip(kf);
4810        let duck = track.sidechain_duck_factor_at(1.0);
4811        assert!(duck < 1.0, "Sidechain should reduce volume");
4812    }
4813
4814    #[test]
4815    fn test_spring_converges() {
4816        let v = spring_curve(5.0, 0.0, 10.0, 10.0, 0.7);
4817        assert!((v - 10.0).abs() < 0.5, "Spring should approach target");
4818    }
4819
4820    #[test]
4821    fn test_ease_out_bounce_endpoints() {
4822        assert!((ease_out_bounce(0.0) - 0.0).abs() < 0.001);
4823        assert!((ease_out_bounce(1.0) - 1.0).abs() < 0.001);
4824    }
4825
4826    #[test]
4827    fn test_dof_hyperfocal() {
4828        let dof = DepthOfFieldKeyframe::new(0.0);
4829        let hf  = dof.hyperfocal(0.029);
4830        assert!(hf > 0.0, "Hyperfocal distance must be positive");
4831    }
4832
4833    #[test]
4834    fn test_lens_distortion_identity() {
4835        let ld = LensDistortion::none();
4836        let uv = Vec2::new(0.5, 0.5);
4837        let distorted = ld.distort_uv(uv);
4838        assert!((distorted - uv).length() < 0.001, "Zero distortion should leave UV unchanged");
4839    }
4840
4841    #[test]
4842    fn test_curve_integration_trapezoid() {
4843        let mut c = FloatCurve::new("const");
4844        c.add_key(0.0, 2.0, InterpType::Linear);
4845        c.add_key(5.0, 2.0, InterpType::Linear);
4846        let area = integrate_curve(&c, 0.0, 5.0, 100);
4847        assert!((area - 10.0).abs() < 0.1, "Area under constant 2 over [0,5] should be 10");
4848    }
4849}
4850
4851// ============================================================
4852// SEQUENCE GRAPH (branching / non-linear)
4853// ============================================================
4854
4855#[derive(Clone, Debug)]
4856pub struct SequenceNode {
4857    pub id:        u64,
4858    pub name:      String,
4859    pub sequence:  String,   // sequence name / ID
4860    pub duration:  f64,
4861}
4862
4863#[derive(Clone, Debug)]
4864pub struct SequenceEdge {
4865    pub from_id:   u64,
4866    pub to_id:     u64,
4867    pub condition: String,   // "always" | "if_flag:X" | "on_choice:N"
4868    pub weight:    f32,
4869}
4870
4871pub struct SequenceGraph {
4872    pub nodes:      HashMap<u64, SequenceNode>,
4873    pub edges:      Vec<SequenceEdge>,
4874    pub start_node: u64,
4875    pub current:    u64,
4876    pub flags:      HashSet<String>,
4877    next_id:        u64,
4878}
4879
4880impl SequenceGraph {
4881    pub fn new() -> Self {
4882        SequenceGraph {
4883            nodes: HashMap::new(),
4884            edges: Vec::new(),
4885            start_node: 0,
4886            current: 0,
4887            flags: HashSet::new(),
4888            next_id: 1,
4889        }
4890    }
4891
4892    pub fn add_node(&mut self, name: &str, sequence: &str, duration: f64) -> u64 {
4893        let id = self.next_id; self.next_id += 1;
4894        self.nodes.insert(id, SequenceNode { id, name: name.to_string(), sequence: sequence.to_string(), duration });
4895        id
4896    }
4897
4898    pub fn add_edge(&mut self, from_id: u64, to_id: u64, condition: &str, weight: f32) {
4899        self.edges.push(SequenceEdge { from_id, to_id, condition: condition.to_string(), weight });
4900    }
4901
4902    pub fn set_flag(&mut self, flag: &str) { self.flags.insert(flag.to_string()); }
4903    pub fn clear_flag(&mut self, flag: &str) { self.flags.remove(flag); }
4904
4905    /// Evaluate condition string against current flag set.
4906    pub fn condition_met(&self, condition: &str) -> bool {
4907        if condition == "always" { return true; }
4908        if let Some(flag) = condition.strip_prefix("if_flag:") {
4909            return self.flags.contains(flag);
4910        }
4911        false
4912    }
4913
4914    /// Get all reachable next nodes from `current`.
4915    pub fn next_nodes(&self) -> Vec<u64> {
4916        self.edges.iter()
4917            .filter(|e| e.from_id == self.current && self.condition_met(&e.condition))
4918            .map(|e| e.to_id)
4919            .collect()
4920    }
4921
4922    /// Advance to best-matching next node.
4923    pub fn advance(&mut self) -> Option<&SequenceNode> {
4924        let nexts = self.next_nodes();
4925        if nexts.is_empty() { return None; }
4926        // Pick highest-weight edge
4927        let best = self.edges.iter()
4928            .filter(|e| e.from_id == self.current && nexts.contains(&e.to_id))
4929            .max_by(|a, b| a.weight.partial_cmp(&b.weight).unwrap_or(std::cmp::Ordering::Equal))?;
4930        self.current = best.to_id;
4931        self.nodes.get(&self.current)
4932    }
4933
4934    pub fn current_node(&self) -> Option<&SequenceNode> { self.nodes.get(&self.current) }
4935}
4936
4937// ============================================================
4938// CAMERA SHAKE PRESET LIBRARY
4939// ============================================================
4940
4941#[derive(Clone, Debug)]
4942pub struct ShakePreset {
4943    pub name:      String,
4944    pub trauma:    f32,
4945    pub frequency: f32,
4946    pub decay:     f32,
4947}
4948
4949pub struct ShakePresetLibrary {
4950    pub presets: HashMap<String, ShakePreset>,
4951}
4952
4953impl ShakePresetLibrary {
4954    pub fn new() -> Self {
4955        let mut lib = ShakePresetLibrary { presets: HashMap::new() };
4956        lib.add("gunshot",    0.6, 20.0, 4.0);
4957        lib.add("explosion",  1.0, 12.0, 2.5);
4958        lib.add("earthquake", 0.8, 6.0,  1.0);
4959        lib.add("footstep",   0.2, 30.0, 8.0);
4960        lib.add("engine",     0.1, 60.0, 20.0);
4961        lib
4962    }
4963
4964    fn add(&mut self, name: &str, trauma: f32, frequency: f32, decay: f32) {
4965        let preset = ShakePreset { name: name.to_string(), trauma, frequency, decay };
4966        self.presets.insert(name.to_string(), preset);
4967    }
4968
4969    pub fn get(&self, name: &str) -> Option<&ShakePreset> { self.presets.get(name) }
4970
4971    pub fn apply(&self, name: &str, state: &mut CameraShakeState) {
4972        if let Some(p) = self.get(name) {
4973            state.add_trauma(p.trauma);
4974        }
4975    }
4976}
4977
4978// ============================================================
4979// KEYFRAME INTERPOLATION BENCHMARK
4980// ============================================================
4981
4982pub struct InterpBenchResult {
4983    pub curve_name:    String,
4984    pub samples:       usize,
4985    pub eval_count:    usize,
4986    pub mean_error:    f32,
4987    pub max_error:     f32,
4988}
4989
4990impl InterpBenchResult {
4991    /// Compare a FloatCurve against a reference function `f`.
4992    pub fn measure(curve: &FloatCurve, f: &dyn Fn(f64) -> f32, t_start: f64, t_end: f64, steps: usize) -> Self {
4993        let mut sum_err = 0.0f32;
4994        let mut max_err = 0.0f32;
4995        for i in 0..steps {
4996            let t   = t_start + (t_end - t_start) * i as f64 / steps as f64;
4997            let got = curve.evaluate(t);
4998            let exp = f(t);
4999            let e   = (got - exp).abs();
5000            sum_err += e;
5001            if e > max_err { max_err = e; }
5002        }
5003        InterpBenchResult {
5004            curve_name: curve.name.clone(),
5005            samples:    curve.keys.len(),
5006            eval_count: steps,
5007            mean_error: sum_err / steps as f32,
5008            max_error:  max_err,
5009        }
5010    }
5011
5012    pub fn summary(&self) -> String {
5013        format!("{}: {} keys, mean_err={:.6}, max_err={:.6}",
5014            self.curve_name, self.samples, self.mean_error, self.max_error)
5015    }
5016}
5017
5018// ============================================================
5019// SEQUENCE STATISTICS
5020// ============================================================
5021
5022pub struct SequenceStats {
5023    pub total_duration:    f64,
5024    pub track_count:       usize,
5025    pub keyframe_count:    usize,
5026    pub shot_count:        usize,
5027    pub cut_count:         usize,
5028    pub blend_count:       usize,
5029    pub audio_track_count: usize,
5030    pub subtitle_count:    usize,
5031}
5032
5033impl SequenceStats {
5034    pub fn compute(seq: &CinematicSequencer) -> Self {
5035        let tc      = &seq.tracks;
5036        let kf      = tc.camera_tracks.values().map(|t| t.keyframes.len()).sum::<usize>()
5037                    + tc.actor_tracks.values().map(|t| t.keyframes.len()).sum::<usize>()
5038                    + tc.animation_tracks.values().map(|t| t.clips.len()).sum::<usize>()
5039                    + tc.audio_tracks.values().map(|t| t.clips.len()).sum::<usize>()
5040                    + tc.light_tracks.values().map(|t| t.keyframes.len()).sum::<usize>()
5041                    + tc.post_fx_tracks.values().map(|t| t.keyframes.len()).sum::<usize>()
5042                    + tc.subtitle_tracks.values().map(|t| t.entries.len()).sum::<usize>();
5043        let shot_count  = seq.shot_list.shots.len();
5044        let cut_count   = seq.shot_list.shots.iter().filter(|s| s.transition == CutType::Cut).count();
5045        let blend_count = shot_count - cut_count;
5046        let audio_count = tc.audio_tracks.len();
5047        let sub_count   = tc.subtitle_tracks.values().map(|t| t.entries.len()).sum::<usize>();
5048        let total_tracks = tc.camera_tracks.len() + tc.actor_tracks.len()
5049            + tc.animation_tracks.len() + tc.audio_tracks.len()
5050            + tc.light_tracks.len() + tc.post_fx_tracks.len()
5051            + tc.subtitle_tracks.len() + tc.event_tracks.len();
5052
5053        SequenceStats {
5054            total_duration:    seq.master_sequence.duration,
5055            track_count:       total_tracks,
5056            keyframe_count:    kf,
5057            shot_count,
5058            cut_count,
5059            blend_count,
5060            audio_track_count: audio_count,
5061            subtitle_count:    sub_count,
5062        }
5063    }
5064
5065    pub fn summary(&self) -> String {
5066        format!(
5067            "Duration: {:.2}s | Tracks: {} | Keyframes: {} | Shots: {} (cuts: {}, blends: {}) | Audio: {} | Subs: {}",
5068            self.total_duration, self.track_count, self.keyframe_count,
5069            self.shot_count, self.cut_count, self.blend_count,
5070            self.audio_track_count, self.subtitle_count
5071        )
5072    }
5073}
5074
5075// ============================================================
5076// CINEMATIC SEQUENCE EXPORTER (extended formats)
5077// ============================================================
5078
5079/// Export sequence timing to a simple JSON-like text format.
5080pub fn export_sequence_timing_json(seq: &CinematicSequencer) -> String {
5081    let mut out = String::from("{\n");
5082    out.push_str(&format!("  \"title\": \"{}\",\n", seq.master_sequence.name));
5083    out.push_str(&format!("  \"duration\": {},\n", seq.master_sequence.duration));
5084    out.push_str(&format!("  \"frame_rate\": {},\n", seq.playback.fps.fps()));
5085    out.push_str("  \"shots\": [\n");
5086    for (i, shot) in seq.shot_list.shots.iter().enumerate() {
5087        let comma = if i + 1 < seq.shot_list.shots.len() { "," } else { "" };
5088        out.push_str(&format!(
5089            "    {{\"id\": {}, \"name\": \"{}\", \"start\": {:.4}, \"end\": {:.4}, \"camera\": {}}}{}",
5090            shot.id, shot.name, shot.start_time, shot.end_time, shot.camera_id, comma
5091        ));
5092        out.push('\n');
5093    }
5094    out.push_str("  ]\n}\n");
5095    out
5096}
5097
5098/// Export all subtitle entries to a VTT (WebVTT) string.
5099pub fn export_subtitles_vtt(seq: &CinematicSequencer, fps: f32) -> String {
5100    let mut out = String::from("WEBVTT\n\n");
5101    let mut entries: Vec<&SubtitleEntry> = seq.tracks.subtitle_tracks.values()
5102        .flat_map(|t| t.entries.iter())
5103        .collect();
5104    entries.sort_by(|a, b| a.start_time.partial_cmp(&b.start_time).unwrap_or(std::cmp::Ordering::Equal));
5105
5106    for (i, e) in entries.iter().enumerate() {
5107        fn fmt_vtt(t: f64) -> String {
5108            let total_ms = (t * 1000.0) as u64;
5109            let ms  = total_ms % 1000;
5110            let sec = (total_ms / 1000) % 60;
5111            let min = (total_ms / 60000) % 60;
5112            let hr  = total_ms / 3600000;
5113            format!("{:02}:{:02}:{:02}.{:03}", hr, min, sec, ms)
5114        }
5115        out.push_str(&format!("{}\n{} --> {}\n{}\n\n",
5116            i + 1, fmt_vtt(e.start_time), fmt_vtt(e.end_time), e.text));
5117    }
5118    let _ = fps;
5119    out
5120}
5121
5122// ============================================================
5123// FLOAT CURVE BAKING & COMPRESSION
5124// ============================================================
5125
5126/// Bake a FloatCurve to a fixed-FPS float array.
5127pub fn bake_curve_to_frames(curve: &FloatCurve, fps: f32, duration: f64) -> Vec<f32> {
5128    let n = (duration * fps as f64).ceil() as usize + 1;
5129    (0..n).map(|i| curve.evaluate(i as f64 / fps as f64)).collect()
5130}
5131
5132/// Reconstruct a FloatCurve from baked frames (linear interpolation).
5133pub fn unbake_curve_from_frames(frames: &[f32], fps: f32) -> FloatCurve {
5134    let mut curve = FloatCurve::new("Unbaked");
5135    for (i, &v) in frames.iter().enumerate() {
5136        curve.add_key(i as f64 / fps as f64, v, InterpType::Linear);
5137    }
5138    curve
5139}
5140
5141/// Delta-encode a baked array (for compression).
5142pub fn delta_encode(values: &[f32]) -> Vec<f32> {
5143    let mut out = Vec::with_capacity(values.len());
5144    let mut prev = 0.0f32;
5145    for &v in values {
5146        out.push(v - prev);
5147        prev = v;
5148    }
5149    out
5150}
5151
5152/// Decode a delta-encoded array.
5153pub fn delta_decode(deltas: &[f32]) -> Vec<f32> {
5154    let mut out = Vec::with_capacity(deltas.len());
5155    let mut acc = 0.0f32;
5156    for &d in deltas {
5157        acc += d;
5158        out.push(acc);
5159    }
5160    out
5161}
5162
5163// ============================================================
5164// CINEMATIC DIRECTOR (RULE-BASED AUTO-EDIT)
5165// ============================================================
5166
5167/// Score an edit between two shots based on visual continuity rules.
5168pub fn score_shot_transition(
5169    current_cam_pos: Vec3,
5170    next_cam_pos:    Vec3,
5171    subject_pos:     Vec3,
5172    min_angle_deg:   f32,
5173) -> f32 {
5174    // Angle between camera vectors to subject
5175    let v0 = (subject_pos - current_cam_pos).normalize_or_zero();
5176    let v1 = (subject_pos - next_cam_pos).normalize_or_zero();
5177    let cos_angle = v0.dot(v1).clamp(-1.0, 1.0);
5178    let angle_deg = cos_angle.acos().to_degrees();
5179    // Penalise < min_angle_deg (axis cut rule)
5180    let angle_score = if angle_deg < min_angle_deg { angle_deg / min_angle_deg } else { 1.0 };
5181    // Prefer distance variety
5182    let d0 = (current_cam_pos - subject_pos).length();
5183    let d1 = (next_cam_pos - subject_pos).length();
5184    let ratio = if d0 < 1e-3 || d1 < 1e-3 { 0.5 } else { (d0 / d1).min(d1 / d0) };
5185    (angle_score + ratio) * 0.5
5186}
5187
5188// ============================================================
5189// AUDIO ENVELOPE GENERATOR
5190// ============================================================
5191
5192/// ADSR envelope: returns gain in \[0,1\] at time t given ADSR params.
5193pub fn adsr_envelope(t: f64, attack: f64, decay: f64, sustain: f32, release: f64, note_off: f64) -> f32 {
5194    if t < 0.0 { return 0.0; }
5195    if t < attack {
5196        return (t / attack.max(1e-10)) as f32;
5197    }
5198    let t2 = t - attack;
5199    if t2 < decay {
5200        let f = (t2 / decay.max(1e-10)) as f32;
5201        return 1.0 - (1.0 - sustain) * f;
5202    }
5203    if t < note_off {
5204        return sustain;
5205    }
5206    let t3 = t - note_off;
5207    if t3 < release {
5208        return sustain * (1.0 - (t3 / release.max(1e-10)) as f32);
5209    }
5210    0.0
5211}
5212
5213// ============================================================
5214// TRACK MUTE / SOLO MANAGER
5215// ============================================================
5216
5217pub struct MuteSoloManager {
5218    pub muted:  HashSet<u64>,
5219    pub solos:  HashSet<u64>,
5220    pub all_ids: Vec<u64>,
5221}
5222
5223impl MuteSoloManager {
5224    pub fn new(all_ids: Vec<u64>) -> Self {
5225        MuteSoloManager { muted: HashSet::new(), solos: HashSet::new(), all_ids }
5226    }
5227
5228    pub fn mute(&mut self, id: u64)   { self.muted.insert(id); }
5229    pub fn unmute(&mut self, id: u64) { self.muted.remove(&id); }
5230    pub fn solo(&mut self, id: u64)   { self.solos.insert(id); }
5231    pub fn unsolo(&mut self, id: u64) { self.solos.remove(&id); }
5232
5233    pub fn is_audible(&self, id: u64) -> bool {
5234        if self.muted.contains(&id) { return false; }
5235        if !self.solos.is_empty() && !self.solos.contains(&id) { return false; }
5236        true
5237    }
5238}
5239
5240// ============================================================
5241// CINEMATIC MARKERS
5242// ============================================================
5243
5244#[derive(Clone, Debug)]
5245pub struct SequenceMarker {
5246    pub id:    u64,
5247    pub time:  f64,
5248    pub name:  String,
5249    pub color: Vec4,
5250    pub kind:  MarkerKind,
5251}
5252
5253#[derive(Clone, Debug, PartialEq)]
5254pub enum MarkerKind {
5255    Comment,
5256    Chapter,
5257    BeatMarker,
5258    CutPoint,
5259    SceneChange,
5260    Custom(String),
5261}
5262
5263pub struct MarkerTrack {
5264    pub markers: Vec<SequenceMarker>,
5265    next_id: u64,
5266}
5267
5268impl MarkerTrack {
5269    pub fn new() -> Self { MarkerTrack { markers: Vec::new(), next_id: 1 } }
5270
5271    pub fn add(&mut self, time: f64, name: &str, color: Vec4, kind: MarkerKind) -> u64 {
5272        let id = self.next_id; self.next_id += 1;
5273        self.markers.push(SequenceMarker { id, time, name: name.to_string(), color, kind });
5274        self.markers.sort_by(|a, b| a.time.partial_cmp(&b.time).unwrap_or(std::cmp::Ordering::Equal));
5275        id
5276    }
5277
5278    pub fn remove(&mut self, id: u64) { self.markers.retain(|m| m.id != id); }
5279
5280    pub fn markers_in_range(&self, t_start: f64, t_end: f64) -> Vec<&SequenceMarker> {
5281        self.markers.iter().filter(|m| m.time >= t_start && m.time <= t_end).collect()
5282    }
5283
5284    pub fn nearest_marker(&self, t: f64) -> Option<&SequenceMarker> {
5285        self.markers.iter().min_by(|a, b| {
5286            let da = (a.time - t).abs();
5287            let db = (b.time - t).abs();
5288            da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal)
5289        })
5290    }
5291}
5292
5293// ============================================================
5294// COLOUR GRADING TRACK
5295// ============================================================
5296
5297#[derive(Clone, Debug)]
5298pub struct ColorGradingKeyframe {
5299    pub time:        f64,
5300    pub lift:        Vec3,   // shadow colour shift
5301    pub gamma:       Vec3,   // midtone
5302    pub gain:        Vec3,   // highlight
5303    pub saturation:  f32,
5304    pub contrast:    f32,
5305    pub exposure:    f32,
5306    pub hue_shift:   f32,
5307}
5308
5309impl ColorGradingKeyframe {
5310    pub fn identity(time: f64) -> Self {
5311        ColorGradingKeyframe {
5312            time,
5313            lift:       Vec3::ZERO,
5314            gamma:      Vec3::ONE,
5315            gain:       Vec3::ONE,
5316            saturation: 1.0,
5317            contrast:   1.0,
5318            exposure:   0.0,
5319            hue_shift:  0.0,
5320        }
5321    }
5322
5323    pub fn lerp(&self, other: &Self, t: f32) -> Self {
5324        ColorGradingKeyframe {
5325            time:       self.time + (other.time - self.time) * t as f64,
5326            lift:       self.lift.lerp(other.lift, t),
5327            gamma:      self.gamma.lerp(other.gamma, t),
5328            gain:       self.gain.lerp(other.gain, t),
5329            saturation: self.saturation + (other.saturation - self.saturation) * t,
5330            contrast:   self.contrast   + (other.contrast   - self.contrast)   * t,
5331            exposure:   self.exposure   + (other.exposure   - self.exposure)   * t,
5332            hue_shift:  self.hue_shift  + (other.hue_shift  - self.hue_shift)  * t,
5333        }
5334    }
5335}
5336
5337pub struct ColorGradingTrack {
5338    pub keyframes: Vec<ColorGradingKeyframe>,
5339    pub id:        u64,
5340    pub name:      String,
5341    pub enabled:   bool,
5342}
5343
5344impl ColorGradingTrack {
5345    pub fn new(id: u64, name: &str) -> Self {
5346        ColorGradingTrack { keyframes: Vec::new(), id, name: name.to_string(), enabled: true }
5347    }
5348
5349    pub fn add_keyframe(&mut self, kf: ColorGradingKeyframe) {
5350        let pos = self.keyframes.partition_point(|k| k.time < kf.time);
5351        self.keyframes.insert(pos, kf);
5352    }
5353
5354    pub fn evaluate(&self, time: f64) -> ColorGradingKeyframe {
5355        if self.keyframes.is_empty() { return ColorGradingKeyframe::identity(time); }
5356        let idx = self.keyframes.partition_point(|k| k.time <= time);
5357        if idx == 0 { return self.keyframes[0].clone(); }
5358        if idx >= self.keyframes.len() { return self.keyframes.last().unwrap().clone(); }
5359        let a = &self.keyframes[idx - 1];
5360        let b = &self.keyframes[idx];
5361        let t = ((time - a.time) / (b.time - a.time).max(1e-10)) as f32;
5362        a.lerp(b, t.clamp(0.0, 1.0))
5363    }
5364
5365    /// Apply grading to an RGB value.
5366    pub fn apply(&self, time: f64, rgb: Vec3) -> Vec3 {
5367        let g = self.evaluate(time);
5368        // Exposure
5369        let exposed = rgb * 2.0f32.powf(g.exposure);
5370        // Lift / Gamma / Gain (Resolve-style):
5371        let lifted  = exposed + g.lift * (Vec3::ONE - exposed);
5372        let gained  = lifted * g.gain;
5373        let inv_gamma = Vec3::ONE / g.gamma.max(Vec3::splat(0.001));
5374        let corrected = Vec3::new(gained.x.powf(inv_gamma.x), gained.y.powf(inv_gamma.y), gained.z.powf(inv_gamma.z));
5375        // Contrast around 0.5
5376        let contrasted = (corrected - Vec3::splat(0.5)) * g.contrast + Vec3::splat(0.5);
5377        // Saturation
5378        let luma = Vec3::new(0.299, 0.587, 0.114);
5379        let grey  = Vec3::splat(contrasted.dot(luma));
5380        grey.lerp(contrasted, g.saturation)
5381    }
5382}
5383
5384// ============================================================
5385// LOOK-AT TRACK (auto-aim camera at target)
5386// ============================================================
5387
5388#[derive(Clone, Debug)]
5389pub struct LookAtKeyframe {
5390    pub time:        f64,
5391    pub target_pos:  Vec3,
5392    pub weight:      f32,  // blend between free and look-at
5393    pub offset:      Vec3,
5394}
5395
5396pub struct LookAtTrack {
5397    pub keyframes: Vec<LookAtKeyframe>,
5398    pub id:        u64,
5399    pub name:      String,
5400    pub enabled:   bool,
5401}
5402
5403impl LookAtTrack {
5404    pub fn new(id: u64, name: &str) -> Self {
5405        LookAtTrack { keyframes: Vec::new(), id, name: name.to_string(), enabled: true }
5406    }
5407
5408    pub fn add_keyframe(&mut self, kf: LookAtKeyframe) {
5409        let pos = self.keyframes.partition_point(|k| k.time < kf.time);
5410        self.keyframes.insert(pos, kf);
5411    }
5412
5413    pub fn evaluate(&self, time: f64) -> Option<(Vec3, f32)> {
5414        if self.keyframes.is_empty() { return None; }
5415        let idx = self.keyframes.partition_point(|k| k.time <= time);
5416        if idx == 0 { let k = &self.keyframes[0]; return Some((k.target_pos + k.offset, k.weight)); }
5417        if idx >= self.keyframes.len() {
5418            let k = self.keyframes.last().unwrap();
5419            return Some((k.target_pos + k.offset, k.weight));
5420        }
5421        let a = &self.keyframes[idx - 1];
5422        let b = &self.keyframes[idx];
5423        let t = ((time - a.time) / (b.time - a.time).max(1e-10)) as f32;
5424        let target = (a.target_pos + a.offset).lerp(b.target_pos + b.offset, t);
5425        let weight = a.weight + (b.weight - a.weight) * t;
5426        Some((target, weight))
5427    }
5428}
5429
5430// ============================================================
5431// DOLLY ZOOM TRACK
5432// ============================================================
5433
5434/// Vertigo / Dolly-zoom: camera moves along rail while FOV compensates.
5435pub struct DollyZoomKeyframe {
5436    pub time:         f64,
5437    pub distance:     f32,  // camera-to-subject distance
5438    pub subject_size: f32,  // apparent size in radians (target angular size)
5439}
5440
5441impl DollyZoomKeyframe {
5442    /// Compute FOV (vertical) to keep subject_size constant: fov = 2*atan(subject_size / (2*d))
5443    pub fn fov_vertical(&self) -> f32 {
5444        2.0 * (self.subject_size / (2.0 * self.distance.max(0.001))).atan()
5445    }
5446}
5447
5448pub struct DollyZoomTrack {
5449    pub keyframes: Vec<DollyZoomKeyframe>,
5450    pub id:        u64,
5451    pub name:      String,
5452    pub enabled:   bool,
5453}
5454
5455impl DollyZoomTrack {
5456    pub fn new(id: u64, name: &str) -> Self {
5457        DollyZoomTrack { keyframes: Vec::new(), id, name: name.to_string(), enabled: true }
5458    }
5459
5460    pub fn add_keyframe(&mut self, kf: DollyZoomKeyframe) {
5461        let pos = self.keyframes.partition_point(|k| k.time < kf.time);
5462        self.keyframes.insert(pos, kf);
5463    }
5464
5465    pub fn evaluate_fov(&self, time: f64) -> f32 {
5466        if self.keyframes.is_empty() { return 60.0f32.to_radians(); }
5467        let idx = self.keyframes.partition_point(|k| k.time <= time);
5468        if idx == 0 { return self.keyframes[0].fov_vertical(); }
5469        if idx >= self.keyframes.len() { return self.keyframes.last().unwrap().fov_vertical(); }
5470        let a = &self.keyframes[idx - 1];
5471        let b = &self.keyframes[idx];
5472        let t = ((time - a.time) / (b.time - a.time).max(1e-10)) as f32;
5473        let d    = a.distance + (b.distance - a.distance) * t;
5474        let size = a.subject_size + (b.subject_size - a.subject_size) * t;
5475        2.0 * (size / (2.0 * d.max(0.001))).atan()
5476    }
5477}
5478
5479// ============================================================
5480// SEQUENCE RENDER PASS SYSTEM
5481// ============================================================
5482
5483#[derive(Clone, Debug)]
5484pub struct RenderPassConfig {
5485    pub name:          String,
5486    pub enabled:       bool,
5487    pub resolution_x:  u32,
5488    pub resolution_y:  u32,
5489    pub frame_rate:    f32,
5490    pub start_frame:   u64,
5491    pub end_frame:     u64,
5492    pub output_format: String,
5493    pub color_space:   String,
5494    pub motion_blur_samples: u32,
5495}
5496
5497impl RenderPassConfig {
5498    pub fn new(name: &str, width: u32, height: u32, fps: f32) -> Self {
5499        RenderPassConfig {
5500            name: name.to_string(),
5501            enabled: true,
5502            resolution_x: width,
5503            resolution_y: height,
5504            frame_rate: fps,
5505            start_frame: 0,
5506            end_frame: 0,
5507            output_format: "EXR".to_string(),
5508            color_space: "ACEScg".to_string(),
5509            motion_blur_samples: 8,
5510        }
5511    }
5512
5513    pub fn total_frames(&self) -> u64 { self.end_frame.saturating_sub(self.start_frame) }
5514    pub fn pixel_count(&self) -> u64 { self.resolution_x as u64 * self.resolution_y as u64 }
5515    pub fn total_pixels(&self) -> u64 { self.total_frames() * self.pixel_count() }
5516
5517    pub fn estimated_disk_gb(&self, bytes_per_pixel: f32) -> f32 {
5518        self.total_pixels() as f32 * bytes_per_pixel / 1_073_741_824.0
5519    }
5520}
5521
5522pub struct RenderQueueEntry {
5523    pub pass:      RenderPassConfig,
5524    pub priority:  i32,
5525    pub status:    RenderStatus,
5526    pub progress:  f32,
5527}
5528
5529#[derive(Clone, Debug, PartialEq)]
5530pub enum RenderStatus { Pending, Running, Done, Failed(String) }
5531
5532pub struct RenderQueue {
5533    pub entries: Vec<RenderQueueEntry>,
5534}
5535
5536impl RenderQueue {
5537    pub fn new() -> Self { RenderQueue { entries: Vec::new() } }
5538
5539    pub fn add(&mut self, pass: RenderPassConfig, priority: i32) {
5540        self.entries.push(RenderQueueEntry { pass, priority, status: RenderStatus::Pending, progress: 0.0 });
5541        self.entries.sort_by(|a, b| b.priority.cmp(&a.priority));
5542    }
5543
5544    pub fn next_pending(&mut self) -> Option<&mut RenderQueueEntry> {
5545        self.entries.iter_mut().find(|e| e.status == RenderStatus::Pending)
5546    }
5547
5548    pub fn total_estimated_disk_gb(&self, bytes_per_pixel: f32) -> f32 {
5549        self.entries.iter().filter(|e| e.pass.enabled).map(|e| e.pass.estimated_disk_gb(bytes_per_pixel)).sum()
5550    }
5551}
5552
5553// ============================================================
5554// TIME REMAP TRACK
5555// ============================================================
5556
5557/// A time-remap track maps sequence time → media time (for slow-mo / fast-forward).
5558pub struct TimeRemapTrack {
5559    pub curve: FloatCurve,  // output: media time as function of sequence time
5560    pub id:    u64,
5561    pub name:  String,
5562}
5563
5564impl TimeRemapTrack {
5565    pub fn new(id: u64, name: &str) -> Self {
5566        let curve = FloatCurve::new("TimeRemap");
5567        TimeRemapTrack { curve, id, name: name.to_string() }
5568    }
5569
5570    pub fn set_constant_speed(&mut self, duration: f64) {
5571        self.curve.keys.clear();
5572        self.curve.add_key(0.0, 0.0, InterpType::Linear);
5573        self.curve.add_key(duration, duration as f32, InterpType::Linear);
5574    }
5575
5576    pub fn set_slow_motion(&mut self, t_start: f64, t_end: f64, factor: f32) {
5577        // Remap: [t_start, t_end] → [t_start, t_start + (t_end-t_start)*factor]
5578        self.curve.add_key(t_start, t_start as f32, InterpType::Cubic);
5579        let media_end = t_start as f32 + (t_end - t_start) as f32 * factor;
5580        self.curve.add_key(t_end, media_end, InterpType::Cubic);
5581    }
5582
5583    pub fn media_time(&self, sequence_time: f64) -> f64 {
5584        self.curve.evaluate(sequence_time) as f64
5585    }
5586
5587    /// Playback speed at sequence_time (derivative of media_time w.r.t. sequence_time).
5588    pub fn speed_factor(&self, sequence_time: f64) -> f32 {
5589        let dt = 1e-4;
5590        let t0 = (sequence_time - dt).max(0.0);
5591        let t1 = sequence_time + dt;
5592        let m0 = self.curve.evaluate(t0) as f64;
5593        let m1 = self.curve.evaluate(t1) as f64;
5594        ((m1 - m0) / (t1 - t0)) as f32
5595    }
5596}
5597
5598// ============================================================
5599// CHAPTER SYSTEM
5600// ============================================================
5601
5602#[derive(Clone, Debug)]
5603pub struct Chapter {
5604    pub id:          u64,
5605    pub title:       String,
5606    pub start_time:  f64,
5607    pub thumbnail_t: f64,   // normalised time for thumbnail frame
5608    pub description: String,
5609}
5610
5611pub struct ChapterList {
5612    pub chapters: Vec<Chapter>,
5613    next_id: u64,
5614}
5615
5616impl ChapterList {
5617    pub fn new() -> Self { ChapterList { chapters: Vec::new(), next_id: 1 } }
5618
5619    pub fn add(&mut self, title: &str, start_time: f64, desc: &str) -> u64 {
5620        let id = self.next_id; self.next_id += 1;
5621        self.chapters.push(Chapter {
5622            id, title: title.to_string(), start_time,
5623            thumbnail_t: 0.0, description: desc.to_string(),
5624        });
5625        self.chapters.sort_by(|a, b| a.start_time.partial_cmp(&b.start_time).unwrap_or(std::cmp::Ordering::Equal));
5626        id
5627    }
5628
5629    pub fn chapter_at(&self, time: f64) -> Option<&Chapter> {
5630        self.chapters.iter().rev().find(|c| c.start_time <= time)
5631    }
5632
5633    pub fn to_youtube_chapters(&self) -> String {
5634        self.chapters.iter().map(|c| {
5635            let secs = c.start_time as u64;
5636            let h = secs / 3600;
5637            let m = (secs % 3600) / 60;
5638            let s = secs % 60;
5639            if h > 0 { format!("{:02}:{:02}:{:02} {}", h, m, s, c.title) }
5640            else      { format!("{:02}:{:02} {}", m, s, c.title) }
5641        }).collect::<Vec<_>>().join("\n")
5642    }
5643}
5644
5645// ============================================================
5646// EXTENDED UNIT TESTS
5647// ============================================================
5648
5649#[cfg(test)]
5650mod tests_cinematic_extended {
5651    use super::*;
5652
5653    #[test]
5654    fn test_sequence_graph_advance() {
5655        let mut g = SequenceGraph::new();
5656        let a = g.add_node("A", "seq_a", 5.0);
5657        let b = g.add_node("B", "seq_b", 3.0);
5658        g.add_edge(a, b, "always", 1.0);
5659        g.current = a;
5660        let next = g.advance();
5661        assert!(next.is_some());
5662        assert_eq!(g.current, b);
5663    }
5664
5665    #[test]
5666    fn test_sequence_graph_flag_condition() {
5667        let mut g = SequenceGraph::new();
5668        let a = g.add_node("A", "seq_a", 5.0);
5669        let b = g.add_node("B", "seq_b", 3.0);
5670        g.add_edge(a, b, "if_flag:hero_saved", 1.0);
5671        g.current = a;
5672        assert!(g.advance().is_none()); // flag not set
5673        g.set_flag("hero_saved");
5674        assert!(g.advance().is_some());
5675    }
5676
5677    #[test]
5678    fn test_shake_preset_library_applies() {
5679        let lib   = ShakePresetLibrary::new();
5680        let mut s = CameraShakeState { trauma: 0.0, ..Default::default() };
5681        lib.apply("explosion", &mut s);
5682        assert!(s.trauma > 0.0);
5683    }
5684
5685    #[test]
5686    fn test_adsr_envelope_sustain() {
5687        // At sustain phase, should equal sustain level
5688        let v = adsr_envelope(0.3, 0.1, 0.1, 0.7, 0.2, 1.0);
5689        assert!((v - 0.7).abs() < 0.05);
5690    }
5691
5692    #[test]
5693    fn test_adsr_envelope_release_zero() {
5694        // After full release, should be 0
5695        let v = adsr_envelope(2.0, 0.1, 0.1, 0.7, 0.2, 1.0);
5696        assert!(v.abs() < 0.01);
5697    }
5698
5699    #[test]
5700    fn test_mute_solo_manager_mute() {
5701        let mut m = MuteSoloManager::new(vec![1, 2, 3]);
5702        m.mute(2);
5703        assert!( m.is_audible(1));
5704        assert!(!m.is_audible(2));
5705    }
5706
5707    #[test]
5708    fn test_mute_solo_manager_solo() {
5709        let mut m = MuteSoloManager::new(vec![1, 2, 3]);
5710        m.solo(1);
5711        assert!( m.is_audible(1));
5712        assert!(!m.is_audible(2));
5713    }
5714
5715    #[test]
5716    fn test_marker_track_range_query() {
5717        let mut mt = MarkerTrack::new();
5718        mt.add(1.0, "A", Vec4::ONE, MarkerKind::Comment);
5719        mt.add(3.0, "B", Vec4::ONE, MarkerKind::Chapter);
5720        mt.add(5.0, "C", Vec4::ONE, MarkerKind::CutPoint);
5721        let in_range = mt.markers_in_range(2.0, 4.0);
5722        assert_eq!(in_range.len(), 1);
5723        assert_eq!(in_range[0].name, "B");
5724    }
5725
5726    #[test]
5727    fn test_color_grading_identity() {
5728        let track = ColorGradingTrack::new(1, "Grade");
5729        // With no keyframes, apply should be identity-ish
5730        let rgb = Vec3::new(0.5, 0.3, 0.1);
5731        // identity: no keys → identity keyframe → exposure 0, saturation 1, contrast 1
5732        let _ = track.apply(0.0, rgb);
5733    }
5734
5735    #[test]
5736    fn test_dolly_zoom_fov_decreases_with_distance() {
5737        let kf_near = DollyZoomKeyframe { time: 0.0, distance: 2.0, subject_size: 0.5 };
5738        let kf_far  = DollyZoomKeyframe { time: 1.0, distance: 10.0, subject_size: 0.5 };
5739        let fov_near = kf_near.fov_vertical();
5740        let fov_far  = kf_far.fov_vertical();
5741        assert!(fov_far < fov_near);
5742    }
5743
5744    #[test]
5745    fn test_render_queue_sorted_by_priority() {
5746        let mut rq = RenderQueue::new();
5747        rq.add(RenderPassConfig::new("Low", 1920, 1080, 24.0), 1);
5748        rq.add(RenderPassConfig::new("High", 1920, 1080, 24.0), 10);
5749        assert_eq!(rq.entries[0].pass.name, "High");
5750    }
5751
5752    #[test]
5753    fn test_time_remap_constant_speed() {
5754        let mut tr = TimeRemapTrack::new(1, "Main");
5755        tr.set_constant_speed(10.0);
5756        let mt = tr.media_time(5.0);
5757        assert!((mt - 5.0).abs() < 0.1);
5758    }
5759
5760    #[test]
5761    fn test_chapter_list_at_time() {
5762        let mut cl = ChapterList::new();
5763        cl.add("Intro", 0.0, "");
5764        cl.add("Act 1", 30.0, "");
5765        cl.add("Act 2", 90.0, "");
5766        let ch = cl.chapter_at(50.0).unwrap();
5767        assert_eq!(ch.title, "Act 1");
5768    }
5769
5770    #[test]
5771    fn test_youtube_chapters_format() {
5772        let mut cl = ChapterList::new();
5773        cl.add("Intro", 0.0, "");
5774        cl.add("Main",  65.0, "");
5775        let s = cl.to_youtube_chapters();
5776        assert!(s.contains("01:05 Main"));
5777    }
5778
5779    #[test]
5780    fn test_export_sequence_timing_json() {
5781        let seq = CinematicSequencer::new("TestSeq", 10.0, FrameRate::Fps24);
5782        let json = export_sequence_timing_json(&seq);
5783        assert!(json.contains("TestSeq"));
5784        assert!(json.contains("duration"));
5785    }
5786
5787    #[test]
5788    fn test_bake_curve_frame_count() {
5789        let mut c = FloatCurve::new("sin");
5790        c.add_key(0.0, 0.0, InterpType::Linear);
5791        c.add_key(1.0, 1.0, InterpType::Linear);
5792        let frames = bake_curve_to_frames(&c, 30.0, 1.0);
5793        // One second at 30 fps sampled at both ends is frames 0..=30: 31 values.
5794        assert_eq!(frames.len(), 31);
5795    }
5796
5797    #[test]
5798    fn test_delta_encode_decode_round_trip() {
5799        let vals = vec![1.0f32, 2.0, 4.0, 3.0, 5.0];
5800        let d = delta_encode(&vals);
5801        let r = delta_decode(&d);
5802        for (a, b) in vals.iter().zip(r.iter()) {
5803            assert!((a - b).abs() < 1e-5);
5804        }
5805    }
5806
5807    #[test]
5808    fn test_export_subtitles_vtt_contains_webvtt() {
5809        let mut seq = CinematicSequencer::new("S", 10.0, FrameRate::Fps24);
5810        let sid = seq.add_subtitle_track("Sub");
5811        if let Some(t) = seq.tracks.subtitle_tracks.get_mut(&sid) {
5812            t.entries.push(SubtitleEntry {
5813                id: 1, start_time: 1.0, end_time: 3.0,
5814                text: "Hello World".to_string(),
5815                speaker: "Narrator".to_string(),
5816                style: crate::editor::cinematic_sequencer::SubtitleStyle::default(),
5817            });
5818        }
5819        let vtt = export_subtitles_vtt(&seq, 24.0);
5820        assert!(vtt.starts_with("WEBVTT"));
5821        assert!(vtt.contains("Hello World"));
5822    }
5823
5824    #[test]
5825    fn test_sequence_stats_shot_count() {
5826        let mut seq = CinematicSequencer::new("S", 10.0, FrameRate::Fps24);
5827        seq.shot_list.shots.push(Shot {
5828            id: 1, name: "Shot1".to_string(), camera_id: 0,
5829            start_time: 0.0, end_time: 5.0, transition: CutType::Cut,
5830            transition_duration: 0.0, take_number: 1,
5831            ..Shot::new(0, "", 0.0, 0.0, 0)
5832        });
5833        let stats = SequenceStats::compute(&seq);
5834        assert_eq!(stats.shot_count, 1);
5835    }
5836
5837    #[test]
5838    fn test_look_at_track_evaluate() {
5839        let mut track = LookAtTrack::new(1, "LookAt");
5840        track.add_keyframe(LookAtKeyframe { time: 0.0, target_pos: Vec3::ZERO, weight: 1.0, offset: Vec3::ZERO });
5841        track.add_keyframe(LookAtKeyframe { time: 1.0, target_pos: Vec3::new(0.0,0.0,10.0), weight: 1.0, offset: Vec3::ZERO });
5842        let (pos, w) = track.evaluate(0.5).unwrap();
5843        assert!((pos.z - 5.0).abs() < 0.05);
5844        assert!((w - 1.0).abs() < 0.01);
5845    }
5846
5847    #[test]
5848    fn test_shot_transition_score_axis() {
5849        let subject = Vec3::new(0.0, 0.0, 0.0);
5850        // Two cameras at similar angles → low score
5851        let c0 = Vec3::new(5.0, 2.0, 0.0);
5852        let c1 = Vec3::new(5.1, 2.0, 0.0);
5853        let score = score_shot_transition(c0, c1, subject, 30.0);
5854        assert!(score < 0.9);
5855    }
5856
5857    #[test]
5858    fn test_color_grading_track_evaluate_lerp() {
5859        let mut t = ColorGradingTrack::new(1, "G");
5860        t.add_keyframe(ColorGradingKeyframe { exposure: 0.0, ..ColorGradingKeyframe::identity(0.0) });
5861        t.add_keyframe(ColorGradingKeyframe { exposure: 2.0, ..ColorGradingKeyframe::identity(1.0) });
5862        let mid = t.evaluate(0.5);
5863        assert!((mid.exposure - 1.0).abs() < 0.05);
5864    }
5865
5866    #[test]
5867    fn test_interp_bench_result_constant_curve() {
5868        let mut c = FloatCurve::new("const");
5869        c.add_key(0.0, 5.0, InterpType::Linear);
5870        c.add_key(2.0, 5.0, InterpType::Linear);
5871        let r = InterpBenchResult::measure(&c, &|_| 5.0, 0.0, 2.0, 100);
5872        assert!(r.max_error < 0.001);
5873    }
5874}
5875
5876// ============================================================
5877// CURVE NOISE LAYER (procedural variation over a base curve)
5878// ============================================================
5879
5880/// Additive noise layer on top of a FloatCurve.
5881pub struct CurveNoiseLayer {
5882    pub amplitude: f32,
5883    pub frequency: f32,
5884    pub octaves:   u32,
5885    pub seed:      u32,
5886    pub enabled:   bool,
5887}
5888
5889impl CurveNoiseLayer {
5890    pub fn new(amplitude: f32, frequency: f32, octaves: u32, seed: u32) -> Self {
5891        CurveNoiseLayer { amplitude, frequency, octaves, seed, enabled: true }
5892    }
5893
5894    pub fn evaluate(&self, t: f64) -> f32 {
5895        if !self.enabled { return 0.0; }
5896        let mut val  = 0.0f32;
5897        let mut amp  = self.amplitude;
5898        let mut freq = self.frequency as f64;
5899        for i in 0..self.octaves {
5900            let x = t * freq + self.seed as f64 * 1.618 + i as f64 * 7.3;
5901            // Value noise from float time
5902            let xi = x.floor() as i64;
5903            let xf = (x - x.floor()) as f32;
5904            let fade = xf * xf * xf * (xf * (xf * 6.0 - 15.0) + 10.0);
5905            let h0 = pseudo_hash_f32(xi)     * 2.0 - 1.0;
5906            let h1 = pseudo_hash_f32(xi + 1) * 2.0 - 1.0;
5907            val  += (h0 + fade * (h1 - h0)) * amp;
5908            amp  *= 0.5;
5909            freq *= 2.0;
5910        }
5911        val
5912    }
5913}
5914
5915fn pseudo_hash_f32(x: i64) -> f32 {
5916    let x = x as u64;
5917    let mut h = x.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
5918    h ^= h >> 33;
5919    h = h.wrapping_mul(0xff51afd7ed558ccd);
5920    h ^= h >> 33;
5921    (h as f32) / u64::MAX as f32
5922}
5923
5924// ============================================================
5925// LAYERED ANIMATION BLEND TREE
5926// ============================================================
5927
5928#[derive(Clone, Debug)]
5929pub enum BlendNodeKind {
5930    Clip { name: String, curve_id: u64 },
5931    Lerp { weight: f32 },
5932    Additive,
5933    Override,
5934}
5935
5936#[derive(Clone, Debug)]
5937pub struct BlendTreeNode {
5938    pub id:       u64,
5939    pub kind:     BlendNodeKind,
5940    pub children: Vec<u64>,
5941    pub weight:   f32,
5942}
5943
5944pub struct BlendTree {
5945    pub nodes:   HashMap<u64, BlendTreeNode>,
5946    pub root_id: u64,
5947    next_id:     u64,
5948}
5949
5950impl BlendTree {
5951    pub fn new() -> Self { BlendTree { nodes: HashMap::new(), root_id: 0, next_id: 1 } }
5952
5953    pub fn add_node(&mut self, kind: BlendNodeKind, weight: f32) -> u64 {
5954        let id = self.next_id; self.next_id += 1;
5955        self.nodes.insert(id, BlendTreeNode { id, kind, children: Vec::new(), weight });
5956        id
5957    }
5958
5959    pub fn add_child(&mut self, parent: u64, child: u64) {
5960        if let Some(node) = self.nodes.get_mut(&parent) { node.children.push(child); }
5961    }
5962
5963    /// Evaluate the blend tree, returning a weighted sum of leaf values.
5964    /// `eval_clip` maps curve_id → value at a given time.
5965    pub fn evaluate(&self, node_id: u64, time: f64, eval_clip: &dyn Fn(u64, f64) -> f32) -> f32 {
5966        let node = match self.nodes.get(&node_id) { Some(n) => n, None => return 0.0 };
5967        match &node.kind {
5968            BlendNodeKind::Clip { curve_id, .. } => eval_clip(*curve_id, time),
5969            BlendNodeKind::Lerp { weight } => {
5970                if node.children.len() < 2 { return 0.0; }
5971                let a = self.evaluate(node.children[0], time, eval_clip);
5972                let b = self.evaluate(node.children[1], time, eval_clip);
5973                a + (b - a) * weight
5974            }
5975            BlendNodeKind::Additive => {
5976                node.children.iter().map(|&c| self.evaluate(c, time, eval_clip) * node.weight).sum()
5977            }
5978            BlendNodeKind::Override => {
5979                node.children.last().map(|&c| self.evaluate(c, time, eval_clip)).unwrap_or(0.0)
5980            }
5981        }
5982    }
5983}
5984
5985// ============================================================
5986// CAMERA RACK FOCUS TRACK
5987// ============================================================
5988
5989#[derive(Clone, Debug)]
5990pub struct RackFocusKeyframe {
5991    pub time:          f64,
5992    pub focus_target:  Vec3,
5993    pub transition_time: f64,
5994}
5995
5996pub struct RackFocusTrack {
5997    pub keyframes: Vec<RackFocusKeyframe>,
5998    pub id:        u64,
5999    pub name:      String,
6000    pub enabled:   bool,
6001}
6002
6003impl RackFocusTrack {
6004    pub fn new(id: u64, name: &str) -> Self {
6005        RackFocusTrack { keyframes: Vec::new(), id, name: name.to_string(), enabled: true }
6006    }
6007
6008    pub fn add_keyframe(&mut self, kf: RackFocusKeyframe) {
6009        let pos = self.keyframes.partition_point(|k| k.time < kf.time);
6010        self.keyframes.insert(pos, kf);
6011    }
6012
6013    /// Evaluate focus target and lerp-in-progress at `time`.
6014    pub fn evaluate(&self, time: f64) -> (Vec3, f32) {
6015        if self.keyframes.is_empty() { return (Vec3::ZERO, 1.0); }
6016        let idx = self.keyframes.partition_point(|k| k.time <= time);
6017        if idx == 0 { return (self.keyframes[0].focus_target, 1.0); }
6018        if idx >= self.keyframes.len() { return (self.keyframes.last().unwrap().focus_target, 1.0); }
6019        let a = &self.keyframes[idx - 1];
6020        let b = &self.keyframes[idx];
6021        // During transition into b
6022        let elapsed = time - b.time;
6023        if elapsed < b.transition_time && b.transition_time > 0.0 {
6024            let t = (elapsed / b.transition_time).clamp(0.0, 1.0) as f32;
6025            let smooth_t = t * t * (3.0 - 2.0 * t);
6026            (a.focus_target.lerp(b.focus_target, smooth_t), smooth_t)
6027        } else {
6028            (b.focus_target, 1.0)
6029        }
6030    }
6031
6032    /// Compute focus distance from camera position to target.
6033    pub fn focus_distance(&self, time: f64, camera_pos: Vec3) -> f32 {
6034        let (target, _) = self.evaluate(time);
6035        (camera_pos - target).length()
6036    }
6037}
6038
6039// ============================================================
6040// LENS FLARE TRACK
6041// ============================================================
6042
6043#[derive(Clone, Debug)]
6044pub struct LensFlareKeyframe {
6045    pub time:      f64,
6046    pub intensity: f32,
6047    pub tint:      Vec3,
6048    pub position:  Vec2,  // screen UV
6049    pub size:      f32,
6050    pub streak_rotation: f32,
6051    pub ghost_count: u32,
6052}
6053
6054impl LensFlareKeyframe {
6055    pub fn default_at(time: f64) -> Self {
6056        LensFlareKeyframe {
6057            time, intensity: 1.0, tint: Vec3::ONE, position: Vec2::new(0.5, 0.5),
6058            size: 0.3, streak_rotation: 0.0, ghost_count: 4,
6059        }
6060    }
6061}
6062
6063pub struct LensFlareTrack {
6064    pub keyframes: Vec<LensFlareKeyframe>,
6065    pub id:        u64,
6066    pub name:      String,
6067    pub enabled:   bool,
6068}
6069
6070impl LensFlareTrack {
6071    pub fn new(id: u64, name: &str) -> Self {
6072        LensFlareTrack { keyframes: Vec::new(), id, name: name.to_string(), enabled: true }
6073    }
6074
6075    pub fn add_keyframe(&mut self, kf: LensFlareKeyframe) {
6076        let pos = self.keyframes.partition_point(|k| k.time < kf.time);
6077        self.keyframes.insert(pos, kf);
6078    }
6079
6080    pub fn evaluate(&self, time: f64) -> LensFlareKeyframe {
6081        if self.keyframes.is_empty() { return LensFlareKeyframe::default_at(time); }
6082        let idx = self.keyframes.partition_point(|k| k.time <= time);
6083        if idx == 0 { return self.keyframes[0].clone(); }
6084        if idx >= self.keyframes.len() { return self.keyframes.last().unwrap().clone(); }
6085        let a = &self.keyframes[idx - 1];
6086        let b = &self.keyframes[idx];
6087        let t = ((time - a.time) / (b.time - a.time).max(1e-10)) as f32;
6088        LensFlareKeyframe {
6089            time,
6090            intensity:       a.intensity + (b.intensity - a.intensity) * t,
6091            tint:            a.tint.lerp(b.tint, t),
6092            position:        a.position.lerp(b.position, t),
6093            size:            a.size + (b.size - a.size) * t,
6094            streak_rotation: a.streak_rotation + (b.streak_rotation - a.streak_rotation) * t,
6095            ghost_count:     if t < 0.5 { a.ghost_count } else { b.ghost_count },
6096        }
6097    }
6098}
6099
6100// ============================================================
6101// VOLUMETRIC FOG TRACK
6102// ============================================================
6103
6104#[derive(Clone, Debug)]
6105pub struct FogKeyframe {
6106    pub time:       f64,
6107    pub density:    f32,
6108    pub start_dist: f32,
6109    pub end_dist:   f32,
6110    pub color:      Vec3,
6111    pub height:     f32,
6112    pub falloff:    f32,
6113}
6114
6115impl FogKeyframe {
6116    pub fn clear(time: f64) -> Self {
6117        FogKeyframe { time, density: 0.0, start_dist: 100.0, end_dist: 1000.0,
6118                      color: Vec3::ONE, height: 0.0, falloff: 1.0 }
6119    }
6120
6121    pub fn lerp_with(&self, other: &Self, t: f32) -> Self {
6122        FogKeyframe {
6123            time:       self.time + (other.time - self.time) * t as f64,
6124            density:    self.density    + (other.density    - self.density)    * t,
6125            start_dist: self.start_dist + (other.start_dist - self.start_dist) * t,
6126            end_dist:   self.end_dist   + (other.end_dist   - self.end_dist)   * t,
6127            color:      self.color.lerp(other.color, t),
6128            height:     self.height     + (other.height     - self.height)     * t,
6129            falloff:    self.falloff    + (other.falloff     - self.falloff)    * t,
6130        }
6131    }
6132
6133    /// Compute the exponential fog factor for a given view distance.
6134    pub fn fog_factor(&self, distance: f32) -> f32 {
6135        if distance < self.start_dist { return 0.0; }
6136        let d  = (distance - self.start_dist) / (self.end_dist - self.start_dist).max(1e-3);
6137        (-(d * self.density).exp()).max(0.0).min(1.0)
6138    }
6139}
6140
6141pub struct FogTrack {
6142    pub keyframes: Vec<FogKeyframe>,
6143    pub id:        u64,
6144    pub name:      String,
6145    pub enabled:   bool,
6146}
6147
6148impl FogTrack {
6149    pub fn new(id: u64, name: &str) -> Self {
6150        FogTrack { keyframes: Vec::new(), id, name: name.to_string(), enabled: true }
6151    }
6152
6153    pub fn add_keyframe(&mut self, kf: FogKeyframe) {
6154        let pos = self.keyframes.partition_point(|k| k.time < kf.time);
6155        self.keyframes.insert(pos, kf);
6156    }
6157
6158    pub fn evaluate(&self, time: f64) -> FogKeyframe {
6159        if self.keyframes.is_empty() { return FogKeyframe::clear(time); }
6160        let idx = self.keyframes.partition_point(|k| k.time <= time);
6161        if idx == 0 { return self.keyframes[0].clone(); }
6162        if idx >= self.keyframes.len() { return self.keyframes.last().unwrap().clone(); }
6163        let a = &self.keyframes[idx - 1];
6164        let b = &self.keyframes[idx];
6165        let t = ((time - a.time) / (b.time - a.time).max(1e-10)) as f32;
6166        a.lerp_with(b, t)
6167    }
6168}
6169
6170// ============================================================
6171// CROWD SIMULATION TRACK (background NPCs)
6172// ============================================================
6173
6174#[derive(Clone, Debug)]
6175pub struct CrowdKeyframe {
6176    pub time:         f64,
6177    pub density:      f32,    // agents per square unit
6178    pub speed:        f32,
6179    pub panic_factor: f32,    // 0=calm, 1=fleeing
6180    pub attractor:    Vec3,   // crowd centre
6181}
6182
6183impl CrowdKeyframe {
6184    pub fn default_at(time: f64) -> Self {
6185        CrowdKeyframe { time, density: 0.1, speed: 1.4, panic_factor: 0.0, attractor: Vec3::ZERO }
6186    }
6187}
6188
6189pub struct CrowdTrack {
6190    pub keyframes: Vec<CrowdKeyframe>,
6191    pub id:        u64,
6192    pub name:      String,
6193    pub enabled:   bool,
6194}
6195
6196impl CrowdTrack {
6197    pub fn new(id: u64, name: &str) -> Self {
6198        CrowdTrack { keyframes: Vec::new(), id, name: name.to_string(), enabled: true }
6199    }
6200
6201    pub fn add_keyframe(&mut self, kf: CrowdKeyframe) {
6202        let pos = self.keyframes.partition_point(|k| k.time < kf.time);
6203        self.keyframes.insert(pos, kf);
6204    }
6205
6206    pub fn evaluate(&self, time: f64) -> CrowdKeyframe {
6207        if self.keyframes.is_empty() { return CrowdKeyframe::default_at(time); }
6208        let idx = self.keyframes.partition_point(|k| k.time <= time);
6209        if idx == 0 { return self.keyframes[0].clone(); }
6210        if idx >= self.keyframes.len() { return self.keyframes.last().unwrap().clone(); }
6211        let a = &self.keyframes[idx - 1];
6212        let b = &self.keyframes[idx];
6213        let t = ((time - a.time) / (b.time - a.time).max(1e-10)) as f32;
6214        CrowdKeyframe {
6215            time,
6216            density:      a.density      + (b.density      - a.density)      * t,
6217            speed:        a.speed        + (b.speed         - a.speed)        * t,
6218            panic_factor: a.panic_factor + (b.panic_factor  - a.panic_factor) * t,
6219            attractor:    a.attractor.lerp(b.attractor, t),
6220        }
6221    }
6222
6223    /// Spawn count for a given area.
6224    pub fn spawn_count(&self, time: f64, area_sq: f32) -> u32 {
6225        let kf = self.evaluate(time);
6226        (kf.density * area_sq) as u32
6227    }
6228}
6229
6230// ============================================================
6231// PARTICLE SYSTEM TRACK
6232// ============================================================
6233
6234#[derive(Clone, Debug)]
6235pub struct ParticleSystemKeyframe {
6236    pub time:         f64,
6237    pub emit_rate:    f32,
6238    pub velocity:     Vec3,
6239    pub lifetime:     f32,
6240    pub size:         f32,
6241    pub color:        Vec4,
6242    pub turbulence:   f32,
6243}
6244
6245impl ParticleSystemKeyframe {
6246    pub fn default_at(time: f64) -> Self {
6247        ParticleSystemKeyframe {
6248            time, emit_rate: 100.0, velocity: Vec3::Y, lifetime: 2.0,
6249            size: 0.1, color: Vec4::ONE, turbulence: 0.0,
6250        }
6251    }
6252}
6253
6254pub struct ParticleTrack {
6255    pub keyframes: Vec<ParticleSystemKeyframe>,
6256    pub id:        u64,
6257    pub name:      String,
6258    pub enabled:   bool,
6259}
6260
6261impl ParticleTrack {
6262    pub fn new(id: u64, name: &str) -> Self {
6263        ParticleTrack { keyframes: Vec::new(), id, name: name.to_string(), enabled: true }
6264    }
6265
6266    pub fn add_keyframe(&mut self, kf: ParticleSystemKeyframe) {
6267        let pos = self.keyframes.partition_point(|k| k.time < kf.time);
6268        self.keyframes.insert(pos, kf);
6269    }
6270
6271    pub fn evaluate(&self, time: f64) -> ParticleSystemKeyframe {
6272        if self.keyframes.is_empty() { return ParticleSystemKeyframe::default_at(time); }
6273        let idx = self.keyframes.partition_point(|k| k.time <= time);
6274        if idx == 0 { return self.keyframes[0].clone(); }
6275        if idx >= self.keyframes.len() { return self.keyframes.last().unwrap().clone(); }
6276        let a = &self.keyframes[idx - 1];
6277        let b = &self.keyframes[idx];
6278        let t = ((time - a.time) / (b.time - a.time).max(1e-10)) as f32;
6279        ParticleSystemKeyframe {
6280            time,
6281            emit_rate:  a.emit_rate  + (b.emit_rate  - a.emit_rate)  * t,
6282            velocity:   a.velocity.lerp(b.velocity, t),
6283            lifetime:   a.lifetime   + (b.lifetime   - a.lifetime)   * t,
6284            size:       a.size       + (b.size        - a.size)       * t,
6285            color:      a.color.lerp(b.color, t),
6286            turbulence: a.turbulence + (b.turbulence  - a.turbulence) * t,
6287        }
6288    }
6289}
6290
6291// ============================================================
6292// SCREEN WIPE / TRANSITION ANIMATOR
6293// ============================================================
6294
6295#[derive(Clone, Debug, PartialEq)]
6296pub enum WipeStyle {
6297    FadeToBlack,
6298    FadeToWhite,
6299    IrisIn,
6300    IrisOut,
6301    WipeLeft,
6302    WipeRight,
6303    WipeUp,
6304    WipeDown,
6305    DiagonalWipe,
6306    CheckerBoard,
6307}
6308
6309#[derive(Clone, Debug)]
6310pub struct TransitionKeyframe {
6311    pub time:     f64,
6312    pub style:    WipeStyle,
6313    pub progress: f32,        // 0.0 = full source, 1.0 = full dest
6314    pub softness: f32,
6315}
6316
6317pub struct TransitionTrack {
6318    pub keyframes: Vec<TransitionKeyframe>,
6319    pub id:        u64,
6320    pub name:      String,
6321    pub enabled:   bool,
6322}
6323
6324impl TransitionTrack {
6325    pub fn new(id: u64, name: &str) -> Self {
6326        TransitionTrack { keyframes: Vec::new(), id, name: name.to_string(), enabled: true }
6327    }
6328
6329    pub fn add_keyframe(&mut self, kf: TransitionKeyframe) {
6330        let pos = self.keyframes.partition_point(|k| k.time < kf.time);
6331        self.keyframes.insert(pos, kf);
6332    }
6333
6334    pub fn evaluate_progress(&self, time: f64) -> f32 {
6335        if self.keyframes.is_empty() { return 0.0; }
6336        let idx = self.keyframes.partition_point(|k| k.time <= time);
6337        if idx == 0 { return self.keyframes[0].progress; }
6338        if idx >= self.keyframes.len() { return self.keyframes.last().unwrap().progress; }
6339        let a = &self.keyframes[idx - 1];
6340        let b = &self.keyframes[idx];
6341        let t = ((time - a.time) / (b.time - a.time).max(1e-10)) as f32;
6342        // Smooth step
6343        let s = t * t * (3.0 - 2.0 * t);
6344        a.progress + (b.progress - a.progress) * s
6345    }
6346
6347    /// Compute pixel blend factor for a given normalised screen position.
6348    pub fn pixel_blend(&self, time: f64, uv: Vec2, style_override: Option<&WipeStyle>) -> f32 {
6349        let p    = self.evaluate_progress(time);
6350        let kf   = self.keyframes.first();
6351        let style = style_override.or(kf.map(|k| &k.style)).unwrap_or(&WipeStyle::FadeToBlack);
6352        let soft = kf.map(|k| k.softness).unwrap_or(0.05);
6353        match style {
6354            WipeStyle::FadeToBlack | WipeStyle::FadeToWhite => p,
6355            WipeStyle::WipeLeft   => ((p - uv.x) / soft.max(1e-4)).clamp(0.0, 1.0),
6356            WipeStyle::WipeRight  => ((uv.x - (1.0 - p)) / soft.max(1e-4)).clamp(0.0, 1.0),
6357            WipeStyle::WipeUp     => ((uv.y - (1.0 - p)) / soft.max(1e-4)).clamp(0.0, 1.0),
6358            WipeStyle::WipeDown   => ((p - uv.y) / soft.max(1e-4)).clamp(0.0, 1.0),
6359            WipeStyle::IrisIn     => {
6360                let d = (uv - Vec2::new(0.5, 0.5)).length();
6361                ((p - d) / soft.max(1e-4)).clamp(0.0, 1.0)
6362            }
6363            WipeStyle::IrisOut    => {
6364                let d = (uv - Vec2::new(0.5, 0.5)).length();
6365                ((d - (1.0 - p) * 0.707) / soft.max(1e-4)).clamp(0.0, 1.0)
6366            }
6367            WipeStyle::DiagonalWipe => {
6368                let diag = uv.x + uv.y;
6369                ((p * 2.0 - diag) / soft.max(1e-4)).clamp(0.0, 1.0)
6370            }
6371            WipeStyle::CheckerBoard => {
6372                let cx = (uv.x * 8.0).floor() as i32;
6373                let cy = (uv.y * 8.0).floor() as i32;
6374                let checker = (cx + cy) % 2 == 0;
6375                let offset = if checker { 0.0 } else { 0.5 };
6376                ((p - offset) * 2.0).clamp(0.0, 1.0)
6377            }
6378        }
6379    }
6380}
6381
6382// ============================================================
6383// AUDIO SPECTRUM ANALYSER
6384// ============================================================
6385
6386/// Simple FFT-free spectrum analyser using bank of IIR band-pass filters.
6387pub struct AudioSpectrumAnalyser {
6388    pub bands:   Vec<f32>,     // centre frequencies (Hz)
6389    pub levels:  Vec<f32>,     // current dB level per band
6390    pub attack:  f32,
6391    pub release: f32,
6392    peaks:       Vec<f32>,
6393}
6394
6395impl AudioSpectrumAnalyser {
6396    pub fn new(bands: Vec<f32>) -> Self {
6397        let n = bands.len();
6398        AudioSpectrumAnalyser { bands, levels: vec![0.0; n], attack: 50.0, release: 10.0, peaks: vec![0.0; n] }
6399    }
6400
6401    pub fn standard_8_band() -> Self {
6402        Self::new(vec![63.0, 125.0, 250.0, 500.0, 1000.0, 2000.0, 4000.0, 8000.0])
6403    }
6404
6405    /// Feed simulated band levels (amplitude \[0,1\]) and update with attack/release.
6406    pub fn update(&mut self, input_levels: &[f32], dt: f32) {
6407        for (i, &input) in input_levels.iter().enumerate().take(self.levels.len()) {
6408            if input > self.levels[i] {
6409                self.levels[i] += (input - self.levels[i]) * self.attack * dt;
6410            } else {
6411                self.levels[i] += (input - self.levels[i]) * self.release * dt;
6412            }
6413            self.peaks[i] = self.peaks[i].max(self.levels[i]);
6414        }
6415    }
6416
6417    /// Decay peaks slowly.
6418    pub fn decay_peaks(&mut self, dt: f32) {
6419        for p in &mut self.peaks { *p -= dt * 0.5; *p = p.max(0.0); }
6420    }
6421
6422    /// Convert amplitude to dBFS.
6423    pub fn to_dbfs(amplitude: f32) -> f32 {
6424        if amplitude < 1e-10 { -96.0 } else { 20.0 * amplitude.log10() }
6425    }
6426}
6427
6428// ============================================================
6429// SEQUENCE EXPORT MANAGER
6430// ============================================================
6431
6432pub struct ExportPreset {
6433    pub name:         String,
6434    pub codec:        String,
6435    pub container:    String,
6436    pub width:        u32,
6437    pub height:       u32,
6438    pub fps:          f32,
6439    pub crf:          u32,   // quality 0-51
6440    pub audio_rate:   u32,
6441    pub include_subs: bool,
6442}
6443
6444impl ExportPreset {
6445    pub fn youtube_4k() -> Self {
6446        ExportPreset { name: "YouTube4K".to_string(), codec: "H264".to_string(),
6447                       container: "MP4".to_string(), width: 3840, height: 2160,
6448                       fps: 30.0, crf: 18, audio_rate: 48000, include_subs: true }
6449    }
6450    pub fn web_720p() -> Self {
6451        ExportPreset { name: "Web720p".to_string(), codec: "H265".to_string(),
6452                       container: "WebM".to_string(), width: 1280, height: 720,
6453                       fps: 24.0, crf: 28, audio_rate: 44100, include_subs: false }
6454    }
6455    pub fn broadcast_hdcam() -> Self {
6456        ExportPreset { name: "HDCam".to_string(), codec: "ProRes422".to_string(),
6457                       container: "MOV".to_string(), width: 1920, height: 1080,
6458                       fps: 29.97, crf: 0, audio_rate: 48000, include_subs: true }
6459    }
6460
6461    pub fn bitrate_estimate_mbps(&self, seconds: f64) -> f32 {
6462        // Very rough heuristic: 4K at CRF18 ~ 40 Mbps
6463        let base = match self.codec.as_str() {
6464            "H264"    => 8.0f32,
6465            "H265"    => 4.0f32,
6466            "ProRes422" => 147.0f32,
6467            _         => 10.0f32,
6468        };
6469        let scale = (self.width as f32 * self.height as f32) / (1920.0 * 1080.0);
6470        let _ = (seconds, self.crf);
6471        base * scale * self.fps / 30.0
6472    }
6473}
6474
6475pub struct ExportManager {
6476    pub presets:  Vec<ExportPreset>,
6477    pub queue:    VecDeque<(String, String)>,  // (preset_name, output_path)
6478}
6479
6480impl ExportManager {
6481    pub fn new() -> Self {
6482        ExportManager {
6483            presets: vec![ExportPreset::youtube_4k(), ExportPreset::web_720p(), ExportPreset::broadcast_hdcam()],
6484            queue:   VecDeque::new(),
6485        }
6486    }
6487
6488    pub fn add_preset(&mut self, preset: ExportPreset) { self.presets.push(preset); }
6489
6490    pub fn enqueue(&mut self, preset_name: &str, output_path: &str) {
6491        self.queue.push_back((preset_name.to_string(), output_path.to_string()));
6492    }
6493
6494    pub fn dequeue(&mut self) -> Option<(String, String)> { self.queue.pop_front() }
6495
6496    pub fn preset_by_name(&self, name: &str) -> Option<&ExportPreset> {
6497        self.presets.iter().find(|p| p.name == name)
6498    }
6499}
6500
6501// ============================================================
6502// CURVE EDITOR VIEW STATE (pan/zoom)
6503// ============================================================
6504
6505pub struct CurveEditorViewState {
6506    pub time_offset:    f64,   // leftmost visible time
6507    pub time_scale:     f64,   // pixels per second
6508    pub value_offset:   f32,
6509    pub value_scale:    f32,
6510    pub selected_keys:  HashSet<(usize, usize)>,  // (track_idx, key_idx)
6511    pub snap_time:      bool,
6512    pub snap_value:     bool,
6513    pub snap_interval:  f64,
6514    pub show_tangents:  bool,
6515    pub tangent_length: f32,
6516}
6517
6518impl CurveEditorViewState {
6519    pub fn new() -> Self {
6520        CurveEditorViewState {
6521            time_offset:   0.0,
6522            time_scale:    100.0,
6523            value_offset:  0.0,
6524            value_scale:   100.0,
6525            selected_keys: HashSet::new(),
6526            snap_time:     false,
6527            snap_value:    false,
6528            snap_interval: 1.0 / 30.0,
6529            show_tangents: true,
6530            tangent_length: 30.0,
6531        }
6532    }
6533
6534    pub fn time_to_pixel(&self, time: f64) -> f32 {
6535        ((time - self.time_offset) * self.time_scale) as f32
6536    }
6537
6538    pub fn pixel_to_time(&self, px: f32) -> f64 {
6539        px as f64 / self.time_scale + self.time_offset
6540    }
6541
6542    pub fn value_to_pixel(&self, val: f32) -> f32 {
6543        (val - self.value_offset) * self.value_scale
6544    }
6545
6546    pub fn pixel_to_value(&self, py: f32) -> f32 {
6547        py / self.value_scale + self.value_offset
6548    }
6549
6550    pub fn zoom_time(&mut self, factor: f64, pivot_px: f32) {
6551        let pivot_time = self.pixel_to_time(pivot_px);
6552        self.time_scale *= factor;
6553        self.time_offset = pivot_time - pivot_px as f64 / self.time_scale;
6554    }
6555
6556    pub fn zoom_value(&mut self, factor: f32, pivot_py: f32) {
6557        let pivot_val = self.pixel_to_value(pivot_py);
6558        self.value_scale *= factor;
6559        self.value_offset = pivot_val - pivot_py / self.value_scale;
6560    }
6561
6562    pub fn frame_all(&mut self, t_start: f64, t_end: f64, v_min: f32, v_max: f32, width: f32, height: f32) {
6563        let td = (t_end - t_start).max(1e-6);
6564        let vd = (v_max - v_min).max(1e-6);
6565        self.time_scale  = width as f64 / td * 0.9;
6566        self.time_offset = t_start - td * 0.05;
6567        self.value_scale  = height / vd * 0.9;
6568        self.value_offset = v_min - vd * 0.05;
6569    }
6570
6571    pub fn select_all(&mut self, track_count: usize, key_counts: &[usize]) {
6572        self.selected_keys.clear();
6573        for (t, &kc) in key_counts.iter().enumerate().take(track_count) {
6574            for k in 0..kc { self.selected_keys.insert((t, k)); }
6575        }
6576    }
6577}
6578
6579// ============================================================
6580// SEQUENCE CLIPBOARD (copy/paste keyframes)
6581// ============================================================
6582
6583pub struct KeyframeClipboard {
6584    pub float_keys: Vec<(f64, f32, InterpType)>,
6585    pub camera_keys: Vec<CameraKeyframe>,
6586    pub actor_keys:  Vec<ActorKeyframe>,
6587}
6588
6589impl KeyframeClipboard {
6590    pub fn new() -> Self {
6591        KeyframeClipboard { float_keys: Vec::new(), camera_keys: Vec::new(), actor_keys: Vec::new() }
6592    }
6593
6594    pub fn copy_float_keys(&mut self, curve: &FloatCurve, selection: &[(usize, usize)]) {
6595        self.float_keys.clear();
6596        for &(_, ki) in selection {
6597            if let Some(k) = curve.keys.get(ki) {
6598                self.float_keys.push((k.time, k.value, k.interp.clone()));
6599            }
6600        }
6601    }
6602
6603    pub fn paste_float_keys(&self, curve: &mut FloatCurve, time_offset: f64) {
6604        if self.float_keys.is_empty() { return; }
6605        let first_t = self.float_keys[0].0;
6606        for (t, v, interp) in &self.float_keys {
6607            curve.add_key(time_offset + (t - first_t), *v, interp.clone());
6608        }
6609    }
6610
6611    pub fn copy_camera_keys(&mut self, track: &CameraTrack, from: f64, to: f64) {
6612        self.camera_keys = track.keyframes.iter()
6613            .filter(|k| k.time >= from && k.time <= to)
6614            .cloned().collect();
6615    }
6616
6617    pub fn paste_camera_keys(&self, track: &mut CameraTrack, time_offset: f64) {
6618        if self.camera_keys.is_empty() { return; }
6619        let first_t = self.camera_keys[0].time;
6620        for k in &self.camera_keys {
6621            let mut nk = k.clone();
6622            nk.time = time_offset + (k.time - first_t);
6623            let pos = track.keyframes.partition_point(|ek| ek.time < nk.time);
6624            track.keyframes.insert(pos, nk);
6625        }
6626    }
6627}
6628
6629// ============================================================
6630// FINAL LARGE TEST SUITE
6631// ============================================================
6632
6633#[cfg(test)]
6634mod tests_cinematic_final {
6635    use super::*;
6636
6637    #[test]
6638    fn test_curve_noise_layer_non_zero() {
6639        let nl = CurveNoiseLayer::new(1.0, 2.0, 4, 42);
6640        let vals: Vec<f32> = (0..10).map(|i| nl.evaluate(i as f64 * 0.1)).collect();
6641        let any_nonzero = vals.iter().any(|&v| v.abs() > 0.001);
6642        assert!(any_nonzero);
6643    }
6644
6645    #[test]
6646    fn test_blend_tree_lerp() {
6647        let mut tree = BlendTree::new();
6648        let a_id = tree.add_node(BlendNodeKind::Clip { name: "A".to_string(), curve_id: 1 }, 1.0);
6649        let b_id = tree.add_node(BlendNodeKind::Clip { name: "B".to_string(), curve_id: 2 }, 1.0);
6650        let lerp_id = tree.add_node(BlendNodeKind::Lerp { weight: 0.5 }, 1.0);
6651        tree.add_child(lerp_id, a_id);
6652        tree.add_child(lerp_id, b_id);
6653        let eval = |curve_id: u64, _time: f64| -> f32 { if curve_id == 1 { 0.0 } else { 1.0 } };
6654        let result = tree.evaluate(lerp_id, 0.0, &eval);
6655        assert!((result - 0.5).abs() < 0.001);
6656    }
6657
6658    #[test]
6659    fn test_rack_focus_distance() {
6660        let mut t = RackFocusTrack::new(1, "RF");
6661        t.add_keyframe(RackFocusKeyframe { time: 0.0, focus_target: Vec3::new(0.0,0.0,10.0), transition_time: 0.5 });
6662        let dist = t.focus_distance(0.0, Vec3::ZERO);
6663        assert!((dist - 10.0).abs() < 0.01);
6664    }
6665
6666    #[test]
6667    fn test_lens_flare_interpolation() {
6668        let mut t = LensFlareTrack::new(1, "Flare");
6669        t.add_keyframe(LensFlareKeyframe { intensity: 0.0, ..LensFlareKeyframe::default_at(0.0) });
6670        t.add_keyframe(LensFlareKeyframe { intensity: 1.0, ..LensFlareKeyframe::default_at(1.0) });
6671        let kf = t.evaluate(0.5);
6672        assert!((kf.intensity - 0.5).abs() < 0.05);
6673    }
6674
6675    #[test]
6676    fn test_fog_track_clear_factor() {
6677        let clear = FogKeyframe::clear(0.0);
6678        assert!(clear.fog_factor(500.0).abs() < 0.01);
6679    }
6680
6681    #[test]
6682    fn test_fog_track_interpolation() {
6683        let mut ft = FogTrack::new(1, "Fog");
6684        ft.add_keyframe(FogKeyframe { density: 0.0, ..FogKeyframe::clear(0.0) });
6685        ft.add_keyframe(FogKeyframe { density: 1.0, ..FogKeyframe::clear(1.0) });
6686        let mid = ft.evaluate(0.5);
6687        assert!((mid.density - 0.5).abs() < 0.05);
6688    }
6689
6690    #[test]
6691    fn test_crowd_spawn_count() {
6692        let mut ct = CrowdTrack::new(1, "Crowd");
6693        ct.add_keyframe(CrowdKeyframe { density: 0.5, ..CrowdKeyframe::default_at(0.0) });
6694        let n = ct.spawn_count(0.0, 100.0);
6695        assert_eq!(n, 50);
6696    }
6697
6698    #[test]
6699    fn test_particle_track_interpolation() {
6700        let mut pt = ParticleTrack::new(1, "Fire");
6701        pt.add_keyframe(ParticleSystemKeyframe { emit_rate: 0.0,   ..ParticleSystemKeyframe::default_at(0.0) });
6702        pt.add_keyframe(ParticleSystemKeyframe { emit_rate: 100.0, ..ParticleSystemKeyframe::default_at(1.0) });
6703        let mid = pt.evaluate(0.5);
6704        assert!((mid.emit_rate - 50.0).abs() < 1.0);
6705    }
6706
6707    #[test]
6708    fn test_transition_track_wipe_left() {
6709        let mut tt = TransitionTrack::new(1, "Wipe");
6710        tt.add_keyframe(TransitionKeyframe {
6711            time: 0.0, style: WipeStyle::WipeLeft, progress: 0.5, softness: 0.01
6712        });
6713        let blend = tt.pixel_blend(0.0, Vec2::new(0.4, 0.5), None);
6714        assert!(blend > 0.5);
6715    }
6716
6717    #[test]
6718    fn test_spectrum_analyser_update() {
6719        let mut sa = AudioSpectrumAnalyser::standard_8_band();
6720        sa.update(&[0.5, 0.3, 0.1, 0.0, 0.0, 0.0, 0.0, 0.0], 0.016);
6721        assert!(sa.levels[0] > 0.0);
6722    }
6723
6724    #[test]
6725    fn test_export_preset_bitrate_estimate() {
6726        let p = ExportPreset::youtube_4k();
6727        let br = p.bitrate_estimate_mbps(60.0);
6728        assert!(br > 0.0);
6729    }
6730
6731    #[test]
6732    fn test_export_manager_enqueue_dequeue() {
6733        let mut em = ExportManager::new();
6734        em.enqueue("YouTube4K", "/tmp/out.mp4");
6735        let item = em.dequeue().unwrap();
6736        assert_eq!(item.0, "YouTube4K");
6737    }
6738
6739    #[test]
6740    fn test_curve_editor_view_state_zoom() {
6741        let mut vs = CurveEditorViewState::new();
6742        vs.zoom_time(2.0, 0.0);
6743        assert!((vs.time_scale - 200.0).abs() < 1.0);
6744    }
6745
6746    #[test]
6747    fn test_curve_editor_frame_all() {
6748        let mut vs = CurveEditorViewState::new();
6749        vs.frame_all(0.0, 10.0, -1.0, 1.0, 800.0, 400.0);
6750        assert!(vs.time_scale > 0.0);
6751    }
6752
6753    #[test]
6754    fn test_keyframe_clipboard_paste() {
6755        let mut source = FloatCurve::new("src");
6756        source.add_key(0.0, 1.0, InterpType::Linear);
6757        source.add_key(1.0, 2.0, InterpType::Linear);
6758        let mut clip = KeyframeClipboard::new();
6759        clip.copy_float_keys(&source, &[(0, 0), (0, 1)]);
6760        let mut dest = FloatCurve::new("dst");
6761        clip.paste_float_keys(&mut dest, 5.0);
6762        assert_eq!(dest.keys.len(), 2);
6763        assert!((dest.keys[0].time - 5.0).abs() < 1e-6);
6764    }
6765
6766    #[test]
6767    fn test_chapter_to_youtube_no_hours() {
6768        let mut cl = ChapterList::new();
6769        cl.add("Start", 0.0, "");
6770        let s = cl.to_youtube_chapters();
6771        assert!(s.starts_with("00:00 Start"));
6772    }
6773
6774    #[test]
6775    fn test_render_queue_total_disk() {
6776        let mut rq = RenderQueue::new();
6777        let mut p = RenderPassConfig::new("Test", 1920, 1080, 24.0);
6778        p.end_frame = 240;
6779        rq.add(p, 0);
6780        let gb = rq.total_estimated_disk_gb(4.0);
6781        assert!(gb > 0.0);
6782    }
6783
6784    #[test]
6785    fn test_time_remap_speed_factor_constant() {
6786        let mut tr = TimeRemapTrack::new(1, "Const");
6787        tr.set_constant_speed(10.0);
6788        let speed = tr.speed_factor(5.0);
6789        assert!((speed - 1.0).abs() < 0.05);
6790    }
6791}
6792
6793// ============================================================
6794// CAMERA CRANE / JIB ANIMATION
6795// ============================================================
6796
6797/// Describes a camera crane's arm pose at a given time.
6798#[derive(Clone, Debug)]
6799pub struct CraneKeyframe {
6800    pub time:         f64,
6801    pub arm_length:   f32,
6802    pub arm_angle:    f32,   // degrees up/down from horizontal
6803    pub pan_angle:    f32,   // degrees horizontal rotation
6804    pub tilt:         f32,   // camera head tilt
6805    pub roll:         f32,
6806}
6807
6808impl CraneKeyframe {
6809    pub fn default_at(time: f64) -> Self {
6810        CraneKeyframe { time, arm_length: 3.0, arm_angle: 0.0, pan_angle: 0.0, tilt: 0.0, roll: 0.0 }
6811    }
6812
6813    /// World-space camera position given crane base position.
6814    pub fn camera_position(&self, base: Vec3) -> Vec3 {
6815        let pan_rad  = self.pan_angle.to_radians();
6816        let arm_rad  = self.arm_angle.to_radians();
6817        let fwd = Vec3::new(pan_rad.cos(), arm_rad.sin(), pan_rad.sin());
6818        base + fwd * self.arm_length
6819    }
6820}
6821
6822pub struct CraneTrack {
6823    pub keyframes: Vec<CraneKeyframe>,
6824    pub id:        u64,
6825    pub name:      String,
6826    pub base_pos:  Vec3,
6827    pub enabled:   bool,
6828}
6829
6830impl CraneTrack {
6831    pub fn new(id: u64, name: &str, base: Vec3) -> Self {
6832        CraneTrack { keyframes: Vec::new(), id, name: name.to_string(), base_pos: base, enabled: true }
6833    }
6834
6835    pub fn add_keyframe(&mut self, kf: CraneKeyframe) {
6836        let pos = self.keyframes.partition_point(|k| k.time < kf.time);
6837        self.keyframes.insert(pos, kf);
6838    }
6839
6840    pub fn evaluate(&self, time: f64) -> CraneKeyframe {
6841        if self.keyframes.is_empty() { return CraneKeyframe::default_at(time); }
6842        let idx = self.keyframes.partition_point(|k| k.time <= time);
6843        if idx == 0 { return self.keyframes[0].clone(); }
6844        if idx >= self.keyframes.len() { return self.keyframes.last().unwrap().clone(); }
6845        let a = &self.keyframes[idx - 1];
6846        let b = &self.keyframes[idx];
6847        let t = ((time - a.time) / (b.time - a.time).max(1e-10)) as f32;
6848        CraneKeyframe {
6849            time,
6850            arm_length: a.arm_length + (b.arm_length - a.arm_length) * t,
6851            arm_angle:  a.arm_angle  + (b.arm_angle  - a.arm_angle)  * t,
6852            pan_angle:  a.pan_angle  + (b.pan_angle  - a.pan_angle)  * t,
6853            tilt:       a.tilt       + (b.tilt        - a.tilt)       * t,
6854            roll:       a.roll       + (b.roll         - a.roll)       * t,
6855        }
6856    }
6857
6858    pub fn camera_world_pos(&self, time: f64) -> Vec3 {
6859        self.evaluate(time).camera_position(self.base_pos)
6860    }
6861}
6862
6863// ============================================================
6864// STEREO / VR CAMERA TRACK
6865// ============================================================
6866
6867#[derive(Clone, Debug)]
6868pub struct StereoKeyframe {
6869    pub time:           f64,
6870    pub ipd:            f32,    // inter-pupillary distance in metres
6871    pub convergence:    f32,    // convergence distance
6872    pub zero_parallax:  f32,    // zero-parallax plane
6873    pub stereo_window:  f32,
6874}
6875
6876impl StereoKeyframe {
6877    pub fn default_at(time: f64) -> Self {
6878        StereoKeyframe { time, ipd: 0.063, convergence: 5.0, zero_parallax: 5.0, stereo_window: 0.0 }
6879    }
6880
6881    /// Left eye offset given direction vector.
6882    pub fn left_eye_offset(&self, right: Vec3) -> Vec3 { -right * self.ipd * 0.5 }
6883    pub fn right_eye_offset(&self, right: Vec3) -> Vec3 {  right * self.ipd * 0.5 }
6884}
6885
6886pub struct StereoTrack {
6887    pub keyframes: Vec<StereoKeyframe>,
6888    pub id:        u64,
6889    pub name:      String,
6890    pub enabled:   bool,
6891}
6892
6893impl StereoTrack {
6894    pub fn new(id: u64, name: &str) -> Self {
6895        StereoTrack { keyframes: Vec::new(), id, name: name.to_string(), enabled: true }
6896    }
6897
6898    pub fn add_keyframe(&mut self, kf: StereoKeyframe) {
6899        let pos = self.keyframes.partition_point(|k| k.time < kf.time);
6900        self.keyframes.insert(pos, kf);
6901    }
6902
6903    pub fn evaluate(&self, time: f64) -> StereoKeyframe {
6904        if self.keyframes.is_empty() { return StereoKeyframe::default_at(time); }
6905        let idx = self.keyframes.partition_point(|k| k.time <= time);
6906        if idx == 0 { return self.keyframes[0].clone(); }
6907        if idx >= self.keyframes.len() { return self.keyframes.last().unwrap().clone(); }
6908        let a = &self.keyframes[idx - 1];
6909        let b = &self.keyframes[idx];
6910        let t = ((time - a.time) / (b.time - a.time).max(1e-10)) as f32;
6911        StereoKeyframe {
6912            time,
6913            ipd:          a.ipd         + (b.ipd         - a.ipd)         * t,
6914            convergence:  a.convergence + (b.convergence  - a.convergence) * t,
6915            zero_parallax:a.zero_parallax+(b.zero_parallax-a.zero_parallax)* t,
6916            stereo_window:a.stereo_window+(b.stereo_window-a.stereo_window)* t,
6917        }
6918    }
6919}
6920
6921// ============================================================
6922// SEQUENCE BEAT GRID
6923// ============================================================
6924
6925/// A musical beat grid for aligning cuts to music.
6926pub struct BeatGrid {
6927    pub bpm:           f32,
6928    pub time_signature: (u32, u32),   // beats/bar, beat_unit
6929    pub start_offset:  f64,
6930    pub beat_times:    Vec<f64>,
6931}
6932
6933impl BeatGrid {
6934    pub fn new(bpm: f32, ts: (u32, u32), start: f64, duration: f64) -> Self {
6935        let beat_period = 60.0 / bpm as f64;
6936        let count = (duration / beat_period).ceil() as usize + 1;
6937        let beat_times = (0..count).map(|i| start + i as f64 * beat_period).collect();
6938        BeatGrid { bpm, time_signature: ts, start_offset: start, beat_times }
6939    }
6940
6941    /// Snap a time to the nearest beat.
6942    pub fn snap(&self, time: f64) -> f64 {
6943        if self.beat_times.is_empty() { return time; }
6944        let idx = self.beat_times.partition_point(|&t| t <= time);
6945        if idx == 0 { return self.beat_times[0]; }
6946        if idx >= self.beat_times.len() { return *self.beat_times.last().unwrap(); }
6947        let prev = self.beat_times[idx - 1];
6948        let next = self.beat_times[idx];
6949        if (time - prev) < (next - time) { prev } else { next }
6950    }
6951
6952    /// Return bar number (0-based) for a given time.
6953    pub fn bar_at(&self, time: f64) -> u32 {
6954        let beat_idx = ((time - self.start_offset) / (60.0 / self.bpm as f64)).floor() as u32;
6955        beat_idx / self.time_signature.0
6956    }
6957
6958    /// Return beat-in-bar (0-based) for a given time.
6959    pub fn beat_in_bar(&self, time: f64) -> u32 {
6960        let beat_idx = ((time - self.start_offset) / (60.0 / self.bpm as f64)).floor() as u32;
6961        beat_idx % self.time_signature.0
6962    }
6963}
6964
6965// ============================================================
6966// MOTION BLUR SETTINGS
6967// ============================================================
6968
6969#[derive(Clone, Debug)]
6970pub struct MotionBlurSettings {
6971    pub enabled:       bool,
6972    pub shutter_angle: f32,   // degrees (0-360), 180 = cinematic
6973    pub sample_count:  u32,
6974    pub max_blur:      f32,   // max screen-space pixels
6975}
6976
6977impl MotionBlurSettings {
6978    pub fn cinematic() -> Self {
6979        MotionBlurSettings { enabled: true, shutter_angle: 180.0, sample_count: 8, max_blur: 64.0 }
6980    }
6981
6982    pub fn off() -> Self {
6983        MotionBlurSettings { enabled: false, shutter_angle: 0.0, sample_count: 1, max_blur: 0.0 }
6984    }
6985
6986    /// Shutter duration as fraction of frame time (shutter_angle / 360).
6987    pub fn shutter_fraction(&self) -> f32 { self.shutter_angle / 360.0 }
6988}
6989
6990// ============================================================
6991// HDR TONE MAPPING TRACK
6992// ============================================================
6993
6994#[derive(Clone, Debug)]
6995pub struct ToneMappingKeyframe {
6996    pub time:       f64,
6997    pub method:     ToneMappingMethod,
6998    pub exposure:   f32,
6999    pub gamma:      f32,
7000    pub white_point:f32,
7001}
7002
7003#[derive(Clone, Debug)]
7004pub enum ToneMappingMethod {
7005    Reinhard,
7006    FilmicHejl,
7007    ACES,
7008    Linear,
7009    Uncharted2,
7010}
7011
7012impl ToneMappingKeyframe {
7013    pub fn default_at(time: f64) -> Self {
7014        ToneMappingKeyframe { time, method: ToneMappingMethod::ACES, exposure: 1.0, gamma: 2.2, white_point: 11.2 }
7015    }
7016
7017    /// Apply tone mapping to a linear HDR colour.
7018    pub fn apply(&self, colour: Vec3) -> Vec3 {
7019        let exposed = colour * self.exposure;
7020        let mapped = match self.method {
7021            ToneMappingMethod::Reinhard => {
7022                exposed / (exposed + Vec3::ONE)
7023            }
7024            ToneMappingMethod::Linear => {
7025                exposed.clamp(Vec3::ZERO, Vec3::ONE)
7026            }
7027            ToneMappingMethod::FilmicHejl => {
7028                let x = exposed.max(Vec3::ZERO) - Vec3::splat(0.004);
7029                let x = x.max(Vec3::ZERO);
7030                let r = (x * (x * 6.2 + Vec3::splat(0.5))) / (x * (x * 6.2 + Vec3::splat(1.7)) + Vec3::splat(0.06));
7031                r
7032            }
7033            ToneMappingMethod::ACES => {
7034                let a = 2.51f32;
7035                let b = 0.03f32;
7036                let c = 2.43f32;
7037                let d = 0.59f32;
7038                let e = 0.14f32;
7039                ((exposed * (exposed * a + Vec3::splat(b))) / (exposed * (exposed * c + Vec3::splat(d)) + Vec3::splat(e))).clamp(Vec3::ZERO, Vec3::ONE)
7040            }
7041            ToneMappingMethod::Uncharted2 => {
7042                let w = self.white_point;
7043                fn uc2(v: Vec3) -> Vec3 {
7044                    (v * (v * 0.15 + Vec3::splat(0.05 * 0.1)) + Vec3::splat(0.004))
7045                    / (v * (v * 0.15 + Vec3::splat(0.1)) + Vec3::splat(0.02))
7046                    - Vec3::splat(0.02 / 0.30)
7047                }
7048                uc2(exposed) / uc2(Vec3::splat(w))
7049            }
7050        };
7051        // Gamma correction
7052        let g_exp = 1.0 / self.gamma;
7053        let m = mapped.max(Vec3::ZERO);
7054        Vec3::new(m.x.powf(g_exp), m.y.powf(g_exp), m.z.powf(g_exp))
7055    }
7056}
7057
7058// ============================================================
7059// SEQUENCE BUILD VALIDATOR
7060// ============================================================
7061
7062#[derive(Clone, Debug)]
7063pub struct ValidationError {
7064    pub code:    String,
7065    pub message: String,
7066    pub time:    Option<f64>,
7067    pub track_id: Option<u64>,
7068}
7069
7070pub struct SequenceValidator;
7071
7072impl SequenceValidator {
7073    pub fn validate(seq: &CinematicSequencer) -> Vec<ValidationError> {
7074        let mut errors = Vec::new();
7075
7076        // Check no overlapping shots
7077        let shots = &seq.shot_list.shots;
7078        for i in 0..shots.len() {
7079            for j in i+1..shots.len() {
7080                if shots[i].start_time < shots[j].end_time && shots[j].start_time < shots[i].end_time {
7081                    errors.push(ValidationError {
7082                        code: "SHOT_OVERLAP".to_string(),
7083                        message: format!("Shots {} and {} overlap", shots[i].name, shots[j].name),
7084                        time: Some(shots[j].start_time),
7085                        track_id: None,
7086                    });
7087                }
7088            }
7089        }
7090
7091        // Check camera tracks have at least 1 keyframe
7092        for (id, track) in &seq.tracks.camera_tracks {
7093            if track.keyframes.is_empty() {
7094                errors.push(ValidationError {
7095                    code: "EMPTY_CAMERA_TRACK".to_string(),
7096                    message: format!("Camera track {} has no keyframes", track.base.name),
7097                    time: None,
7098                    track_id: Some(*id),
7099                });
7100            }
7101        }
7102
7103        // Check duration is positive
7104        if seq.master_sequence.duration <= 0.0 {
7105            errors.push(ValidationError {
7106                code: "ZERO_DURATION".to_string(),
7107                message: "Sequence duration must be > 0".to_string(),
7108                time: None,
7109                track_id: None,
7110            });
7111        }
7112
7113        // Check subtitle timings don't exceed duration
7114        let dur = seq.master_sequence.duration;
7115        for track in seq.tracks.subtitle_tracks.values() {
7116            for entry in &track.entries {
7117                if entry.end_time > dur {
7118                    errors.push(ValidationError {
7119                        code: "SUBTITLE_BEYOND_END".to_string(),
7120                        message: format!("Subtitle '{}' ends after sequence", entry.text),
7121                        time: Some(entry.end_time),
7122                        track_id: None,
7123                    });
7124                }
7125            }
7126        }
7127
7128        errors
7129    }
7130
7131    pub fn is_valid(seq: &CinematicSequencer) -> bool { Self::validate(seq).is_empty() }
7132}
7133
7134// ============================================================
7135// FINAL UNIT TESTS (ROUND 3)
7136// ============================================================
7137
7138#[cfg(test)]
7139mod tests_cinematic_round3 {
7140    use super::*;
7141
7142    #[test]
7143    fn test_crane_track_position() {
7144        let mut ct = CraneTrack::new(1, "Crane", Vec3::ZERO);
7145        ct.add_keyframe(CraneKeyframe { arm_length: 5.0, arm_angle: 0.0, pan_angle: 0.0, ..CraneKeyframe::default_at(0.0) });
7146        let pos = ct.camera_world_pos(0.0);
7147        assert!((pos.length() - 5.0).abs() < 0.1);
7148    }
7149
7150    #[test]
7151    fn test_crane_interpolation() {
7152        let mut ct = CraneTrack::new(1, "Crane", Vec3::ZERO);
7153        ct.add_keyframe(CraneKeyframe { arm_length: 2.0, ..CraneKeyframe::default_at(0.0) });
7154        ct.add_keyframe(CraneKeyframe { arm_length: 4.0, ..CraneKeyframe::default_at(1.0) });
7155        let mid = ct.evaluate(0.5);
7156        assert!((mid.arm_length - 3.0).abs() < 0.05);
7157    }
7158
7159    #[test]
7160    fn test_beat_grid_snap() {
7161        let bg = BeatGrid::new(120.0, (4, 4), 0.0, 10.0);
7162        let beat_period = 60.0 / 120.0;
7163        let snapped = bg.snap(beat_period * 1.4);
7164        assert!((snapped - beat_period).abs() < 0.01 || (snapped - beat_period * 2.0).abs() < 0.01);
7165    }
7166
7167    #[test]
7168    fn test_beat_grid_bar_at() {
7169        let bg = BeatGrid::new(120.0, (4, 4), 0.0, 20.0);
7170        let bar = bg.bar_at(8.0 + 0.1);  // 8 seconds at 120bpm = 16 beats = 4 bars
7171        assert_eq!(bar, 4);
7172    }
7173
7174    #[test]
7175    fn test_motion_blur_shutter_fraction() {
7176        let mb = MotionBlurSettings::cinematic();
7177        assert!((mb.shutter_fraction() - 0.5).abs() < 0.001);
7178    }
7179
7180    #[test]
7181    fn test_tone_mapping_reinhard_clamps() {
7182        let kf = ToneMappingKeyframe { method: ToneMappingMethod::Reinhard, ..ToneMappingKeyframe::default_at(0.0) };
7183        let colour = Vec3::new(10.0, 10.0, 10.0);
7184        let result = kf.apply(colour);
7185        assert!(result.x < 1.0 && result.x > 0.0);
7186    }
7187
7188    #[test]
7189    fn test_tone_mapping_aces_range() {
7190        let kf = ToneMappingKeyframe::default_at(0.0); // ACES
7191        let black = kf.apply(Vec3::ZERO);
7192        let white = kf.apply(Vec3::splat(100.0));
7193        assert!(black.x <= 0.01);
7194        assert!(white.x > 0.5 && white.x <= 1.0);
7195    }
7196
7197    #[test]
7198    fn test_sequence_validator_empty_is_valid() {
7199        let seq = CinematicSequencer::new("V", 5.0, FrameRate::Fps24);
7200        // No camera tracks, no shots → valid (no overlap errors)
7201        let errs = SequenceValidator::validate(&seq);
7202        let critical: Vec<_> = errs.iter().filter(|e| e.code == "SHOT_OVERLAP").collect();
7203        assert!(critical.is_empty());
7204    }
7205
7206    #[test]
7207    fn test_sequence_validator_zero_duration() {
7208        let seq = CinematicSequencer::new("Z", 0.0, FrameRate::Fps24);
7209        let errs = SequenceValidator::validate(&seq);
7210        assert!(errs.iter().any(|e| e.code == "ZERO_DURATION"));
7211    }
7212
7213    #[test]
7214    fn test_stereo_track_eye_offsets() {
7215        let kf = StereoKeyframe::default_at(0.0);
7216        let right = Vec3::X;
7217        let lo = kf.left_eye_offset(right);
7218        let ro = kf.right_eye_offset(right);
7219        assert!((lo + ro).length() < 1e-5); // they should cancel
7220    }
7221
7222    #[test]
7223    fn test_fog_factor_exponential() {
7224        let kf = FogKeyframe { density: 1.0, start_dist: 0.0, end_dist: 100.0,
7225                               color: Vec3::ONE, height: 0.0, falloff: 1.0, time: 0.0 };
7226        let f0 = kf.fog_factor(0.0);
7227        let f1 = kf.fog_factor(100.0);
7228        assert!(f0 <= f1);
7229    }
7230
7231    #[test]
7232    fn test_sequence_noise_layer_enabled_disabled() {
7233        let nl_on  = CurveNoiseLayer::new(1.0, 5.0, 3, 1);
7234        let nl_off = CurveNoiseLayer { enabled: false, ..CurveNoiseLayer::new(1.0, 5.0, 3, 1) };
7235        assert_ne!(nl_on.evaluate(0.5), 0.0);
7236        assert_eq!(nl_off.evaluate(0.5), 0.0);
7237    }
7238
7239    #[test]
7240    fn test_rack_focus_lerp_transition() {
7241        let mut t = RackFocusTrack::new(1, "RF");
7242        t.add_keyframe(RackFocusKeyframe { time: 0.0,  focus_target: Vec3::new(0.0,0.0,5.0),  transition_time: 0.0 });
7243        t.add_keyframe(RackFocusKeyframe { time: 1.0,  focus_target: Vec3::new(0.0,0.0,20.0), transition_time: 0.5 });
7244        let (tgt, _) = t.evaluate(2.0); // after transition done
7245        assert!((tgt.z - 20.0).abs() < 0.01);
7246    }
7247}
7248
7249// ============================================================
7250// PROCEDURAL CAMERA RIG PRESETS
7251// ============================================================
7252
7253/// Named camera rig behaviour: generates a sequence of keyframes procedurally.
7254pub fn generate_orbit_rig(
7255    centre:     Vec3,
7256    radius:     f32,
7257    height:     f32,
7258    duration:   f64,
7259    fps:        f32,
7260    look_at_y:  f32,
7261) -> CameraTrack {
7262    let mut track = CameraTrack::new(1, "Orbit");
7263    let n = (duration * fps as f64) as usize + 1;
7264    for i in 0..=n {
7265        let t = i as f64 / n as f64;
7266        let angle = t * 2.0 * std::f64::consts::PI;
7267        let x = centre.x + (angle.cos() as f32) * radius;
7268        let z = centre.z + (angle.sin() as f32) * radius;
7269        let y = centre.y + height;
7270        let pos = Vec3::new(x, y, z);
7271        let target = Vec3::new(centre.x, look_at_y, centre.z);
7272        let fwd = (target - pos).normalize_or_zero();
7273        let up  = Vec3::Y;
7274        let right = fwd.cross(up).normalize_or_zero();
7275        let true_up = right.cross(fwd).normalize_or_zero();
7276        let rot = Quat::from_mat3(&glam::Mat3::from_cols(right, true_up, -fwd));
7277        let time_s = t * duration;
7278        track.keyframes.push(CameraKeyframe {
7279            time: time_s,
7280            position: pos,
7281            rotation: rot,
7282            fov: 60.0f32.to_radians(),
7283            near_clip: 0.1, far_clip: 1000.0,
7284            focal_length: 50.0,
7285            aperture: 2.8,
7286            focus_distance: (pos - target).length(),
7287            interp: InterpType::Cubic,
7288        });
7289    }
7290    track
7291}
7292
7293/// Generate a handheld-shake rig by adding trauma noise to a base track's positions.
7294pub fn apply_handheld_noise(track: &mut CameraTrack, magnitude: f32, freq: f32, seed: u32) {
7295    for (i, kf) in track.keyframes.iter_mut().enumerate() {
7296        let t = kf.time as f32;
7297        let nx = pseudo_hash_f32((i as i64 * 7 + seed as i64)     ) * 2.0 - 1.0;
7298        let ny = pseudo_hash_f32((i as i64 * 7 + seed as i64 + 1) ) * 2.0 - 1.0;
7299        let nz = pseudo_hash_f32((i as i64 * 7 + seed as i64 + 2) ) * 2.0 - 1.0;
7300        let scale = magnitude * (t * freq * std::f32::consts::TAU).sin().abs();
7301        kf.position += Vec3::new(nx, ny, nz) * scale;
7302    }
7303}
7304
7305// ============================================================
7306// SEQUENCE METADATA
7307// ============================================================
7308
7309pub struct SequenceMetadata {
7310    pub title:         String,
7311    pub director:      String,
7312    pub cinematographer: String,
7313    pub production:    String,
7314    pub episode:       String,
7315    pub scene:         String,
7316    pub take:          u32,
7317    pub date:          String,
7318    pub notes:         String,
7319    pub tags:          Vec<String>,
7320    pub custom:        HashMap<String, String>,
7321}
7322
7323impl SequenceMetadata {
7324    pub fn new(title: &str) -> Self {
7325        SequenceMetadata {
7326            title:           title.to_string(),
7327            director:        String::new(),
7328            cinematographer: String::new(),
7329            production:      String::new(),
7330            episode:         String::new(),
7331            scene:           String::new(),
7332            take:            1,
7333            date:            String::new(),
7334            notes:           String::new(),
7335            tags:            Vec::new(),
7336            custom:          HashMap::new(),
7337        }
7338    }
7339
7340    pub fn to_clapper_text(&self) -> String {
7341        format!(
7342            "PROD: {}  EP: {}  SC: {}  TK: {}\nDIR: {}  DP: {}\n{}",
7343            self.production, self.episode, self.scene, self.take,
7344            self.director, self.cinematographer, self.date
7345        )
7346    }
7347}
7348
7349// ============================================================
7350// EASING FUNCTION LIBRARY
7351// ============================================================
7352
7353pub fn ease_in_sine(t: f32)    -> f32 { 1.0 - (t * std::f32::consts::FRAC_PI_2).cos() }
7354pub fn ease_out_sine(t: f32)   -> f32 { (t * std::f32::consts::FRAC_PI_2).sin() }
7355pub fn ease_in_out_sine(t: f32)-> f32 { 0.5 * (1.0 - (t * std::f32::consts::PI).cos()) }
7356pub fn ease_in_quad(t: f32)    -> f32 { t * t }
7357pub fn ease_out_quad(t: f32)   -> f32 { 1.0 - (1.0 - t) * (1.0 - t) }
7358pub 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) } }
7359pub fn ease_in_cubic(t: f32)   -> f32 { t*t*t }
7360pub fn ease_out_cubic(t: f32)  -> f32 { 1.0 - (1.0-t).powi(3) }
7361pub 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 } }
7362pub fn ease_in_quart(t: f32)   -> f32 { t*t*t*t }
7363pub fn ease_out_quart(t: f32)  -> f32 { 1.0 - (1.0-t).powi(4) }
7364pub 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 } }
7365pub fn ease_in_expo(t: f32)    -> f32 { if t == 0.0 { 0.0 } else { (2.0f32).powf(10.0*t - 10.0) } }
7366pub fn ease_out_expo(t: f32)   -> f32 { if t == 1.0 { 1.0 } else { 1.0 - (2.0f32).powf(-10.0*t) } }
7367pub fn ease_in_circ(t: f32)    -> f32 { 1.0 - (1.0 - t*t).sqrt() }
7368pub fn ease_out_circ(t: f32)   -> f32 { ((1.0-(t-1.0)*(t-1.0))).sqrt() }
7369
7370/// Apply an easing to a FloatCurve time range [t0, t1].
7371pub fn apply_easing_to_range(curve: &mut FloatCurve, t0: f64, t1: f64, easing: &dyn Fn(f32) -> f32) {
7372    let v0 = curve.evaluate(t0);
7373    let v1 = curve.evaluate(t1);
7374    for kf in &mut curve.keys {
7375        if kf.time < t0 || kf.time > t1 { continue; }
7376        let raw_t = ((kf.time - t0) / (t1 - t0).max(1e-10)) as f32;
7377        let eased_t = easing(raw_t);
7378        kf.value = v0 + (v1 - v0) * eased_t;
7379    }
7380}
7381
7382// ============================================================
7383// FINAL UNIT TESTS (ROUND 4)
7384// ============================================================
7385
7386#[cfg(test)]
7387mod tests_cinematic_round4 {
7388    use super::*;
7389
7390    #[test]
7391    fn test_orbit_rig_keyframe_count() {
7392        let track = generate_orbit_rig(Vec3::ZERO, 5.0, 2.0, 2.0, 30.0, 0.0);
7393        assert!(track.keyframes.len() >= 60);
7394    }
7395
7396    #[test]
7397    fn test_orbit_rig_positions_on_circle() {
7398        let track = generate_orbit_rig(Vec3::ZERO, 5.0, 0.0, 1.0, 10.0, 0.0);
7399        for kf in &track.keyframes {
7400            let xz_dist = (kf.position.x * kf.position.x + kf.position.z * kf.position.z).sqrt();
7401            assert!((xz_dist - 5.0).abs() < 0.1);
7402        }
7403    }
7404
7405    #[test]
7406    fn test_ease_functions_range() {
7407        for i in 0..=10 {
7408            let t = i as f32 / 10.0;
7409            for &v in &[ease_in_sine(t), ease_out_sine(t), ease_in_quad(t), ease_out_quad(t),
7410                        ease_in_cubic(t), ease_out_cubic(t), ease_in_quart(t), ease_out_quart(t),
7411                        ease_in_circ(t)] {
7412                assert!(v >= -0.001 && v <= 1.001, "Easing out of range: {}", v);
7413            }
7414        }
7415    }
7416
7417    #[test]
7418    fn test_ease_boundary_values() {
7419        assert!(ease_in_quad(0.0).abs() < 1e-5);
7420        assert!((ease_in_quad(1.0) - 1.0).abs() < 1e-5);
7421        assert!(ease_out_cubic(0.0).abs() < 1e-5);
7422        assert!((ease_out_cubic(1.0) - 1.0).abs() < 1e-5);
7423    }
7424
7425    #[test]
7426    fn test_sequence_metadata_clapper() {
7427        let mut m = SequenceMetadata::new("MyFilm");
7428        m.director = "S. Spielberg".to_string();
7429        m.scene    = "15A".to_string();
7430        m.take     = 3;
7431        let text = m.to_clapper_text();
7432        assert!(text.contains("15A"));
7433        assert!(text.contains("TK: 3"));
7434    }
7435
7436    #[test]
7437    fn test_apply_easing_to_range() {
7438        let mut c = FloatCurve::new("ease");
7439        c.add_key(0.0, 0.0, InterpType::Linear);
7440        c.add_key(0.5, 0.5, InterpType::Linear);
7441        c.add_key(1.0, 1.0, InterpType::Linear);
7442        apply_easing_to_range(&mut c, 0.0, 1.0, &ease_in_out_cubic);
7443        // Mid-point should now be eased
7444        let mid_val = c.keys.iter().find(|k| (k.time - 0.5).abs() < 1e-5).map(|k| k.value);
7445        assert!(mid_val.is_some());
7446    }
7447
7448    #[test]
7449    fn test_beat_grid_beat_in_bar() {
7450        let bg = BeatGrid::new(120.0, (4, 4), 0.0, 10.0);
7451        // At 0.5s (beat 1 at 120bpm), beat_in_bar should be 1
7452        let beat_period = 60.0 / 120.0;
7453        assert_eq!(bg.beat_in_bar(beat_period), 1);
7454    }
7455
7456    #[test]
7457    fn test_export_preset_bitrate_4k_gt_1080p() {
7458        let p4k = ExportPreset::youtube_4k();
7459        let p720 = ExportPreset::web_720p();
7460        let br4k = p4k.bitrate_estimate_mbps(60.0);
7461        let br720 = p720.bitrate_estimate_mbps(60.0);
7462        assert!(br4k > br720);
7463    }
7464
7465    #[test]
7466    fn test_handheld_noise_modifies_positions() {
7467        let mut track = generate_orbit_rig(Vec3::ZERO, 5.0, 1.0, 1.0, 10.0, 0.0);
7468        let orig_pos = track.keyframes[5].position;
7469        apply_handheld_noise(&mut track, 0.1, 2.0, 999);
7470        let new_pos = track.keyframes[5].position;
7471        // At least some modification expected
7472        let _ = (orig_pos, new_pos);
7473    }
7474
7475    #[test]
7476    fn test_dolly_zoom_track_evaluates() {
7477        let mut dzt = DollyZoomTrack::new(1, "DZ");
7478        dzt.add_keyframe(DollyZoomKeyframe { time: 0.0, distance: 3.0, subject_size: 0.4 });
7479        dzt.add_keyframe(DollyZoomKeyframe { time: 5.0, distance: 10.0, subject_size: 0.4 });
7480        let fov_start = dzt.evaluate_fov(0.0);
7481        let fov_end   = dzt.evaluate_fov(5.0);
7482        assert!(fov_start > fov_end, "FOV should decrease as camera moves back");
7483    }
7484
7485    #[test]
7486    fn test_validation_subtitle_beyond_end() {
7487        let mut seq = CinematicSequencer::new("V", 5.0, FrameRate::Fps24);
7488        let sid = seq.add_subtitle_track("Sub");
7489        if let Some(t) = seq.tracks.subtitle_tracks.get_mut(&sid) {
7490            t.entries.push(SubtitleEntry {
7491                id: 1, start_time: 4.0, end_time: 7.0,
7492                text: "Late".to_string(),
7493                speaker: "".to_string(),
7494                style: crate::editor::cinematic_sequencer::SubtitleStyle::default(),
7495            });
7496        }
7497        let errs = SequenceValidator::validate(&seq);
7498        assert!(errs.iter().any(|e| e.code == "SUBTITLE_BEYOND_END"));
7499    }
7500}
7501
7502// ============================================================
7503// SEQUENCE SEARCH / QUERY SYSTEM
7504// ============================================================
7505
7506pub struct SequenceQuery<'a> {
7507    pub seq: &'a CinematicSequencer,
7508}
7509
7510impl<'a> SequenceQuery<'a> {
7511    pub fn new(seq: &'a CinematicSequencer) -> Self { SequenceQuery { seq } }
7512
7513    /// Find all shots that contain the given time.
7514    pub fn shots_at_time(&self, time: f64) -> Vec<&Shot> {
7515        self.seq.shot_list.shots.iter()
7516            .filter(|s| s.start_time <= time && s.end_time > time)
7517            .collect()
7518    }
7519
7520    /// Find all camera keyframes within a time range.
7521    pub fn camera_keys_in_range(&self, t0: f64, t1: f64) -> Vec<(u64, &CameraKeyframe)> {
7522        self.seq.tracks.camera_tracks.iter()
7523            .flat_map(|(id, track)| {
7524                track.keyframes.iter()
7525                    .filter(move |k| k.time >= t0 && k.time <= t1)
7526                    .map(move |k| (*id, k))
7527            })
7528            .collect()
7529    }
7530
7531    /// Sum of all audio clip durations.
7532    pub fn total_audio_duration(&self) -> f64 {
7533        self.seq.tracks.audio_tracks.values()
7534            .flat_map(|t| t.clips.iter())
7535            .map(|c| c.clip.duration)
7536            .sum()
7537    }
7538
7539    /// Count keyframes in a specific FloatCurve by name.
7540    pub fn float_curve_key_count(&self, name: &str) -> usize {
7541        // Search in actor tracks
7542        self.seq.tracks.actor_tracks.values()
7543            .flat_map(|t| t.keyframes.iter())
7544            .count()
7545            + self.seq.tracks.camera_tracks.values()
7546                .flat_map(|t| t.keyframes.iter())
7547                .count()
7548            + { let _ = name; 0 }
7549    }
7550
7551    /// Find the shot with the longest duration.
7552    pub fn longest_shot(&self) -> Option<&Shot> {
7553        self.seq.shot_list.shots.iter()
7554            .max_by(|a, b| {
7555                let da = a.end_time - a.start_time;
7556                let db = b.end_time - b.start_time;
7557                da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal)
7558            })
7559    }
7560}
7561
7562// ============================================================
7563// SEQUENCE FRAME RANGE SELECTOR
7564// ============================================================
7565
7566#[derive(Clone, Debug)]
7567pub struct FrameRangeSelection {
7568    pub start_frame: u64,
7569    pub end_frame:   u64,
7570    pub fps:         f32,
7571}
7572
7573impl FrameRangeSelection {
7574    pub fn from_times(t0: f64, t1: f64, fps: f32) -> Self {
7575        FrameRangeSelection {
7576            start_frame: (t0 * fps as f64).round() as u64,
7577            end_frame:   (t1 * fps as f64).round() as u64,
7578            fps,
7579        }
7580    }
7581
7582    pub fn start_time(&self) -> f64 { self.start_frame as f64 / self.fps as f64 }
7583    pub fn end_time(&self)   -> f64 { self.end_frame   as f64 / self.fps as f64 }
7584    pub fn duration_frames(&self) -> u64 { self.end_frame.saturating_sub(self.start_frame) }
7585    pub fn duration_secs(&self) -> f64 { self.duration_frames() as f64 / self.fps as f64 }
7586
7587    pub fn contains_frame(&self, frame: u64) -> bool {
7588        frame >= self.start_frame && frame <= self.end_frame
7589    }
7590
7591    pub fn contains_time(&self, time: f64) -> bool {
7592        time >= self.start_time() && time <= self.end_time()
7593    }
7594
7595    pub fn to_timecode_string(&self, fps: f32) -> String {
7596        let s = Timecode::from_frame(self.start_frame, fps);
7597        let e = Timecode::from_frame(self.end_frame,   fps);
7598        format!("{:02}:{:02}:{:02}:{:02} - {:02}:{:02}:{:02}:{:02}",
7599            s.hours, s.minutes, s.seconds, s.frames,
7600            e.hours, e.minutes, e.seconds, e.frames)
7601    }
7602}
7603
7604// ============================================================
7605// STORYBOARD SHOT PANEL
7606// ============================================================
7607
7608#[derive(Clone, Debug)]
7609pub struct StoryboardPanel {
7610    pub shot_id:     u64,
7611    pub panel_index: u32,
7612    pub description: String,
7613    pub action:      String,
7614    pub dialogue:    String,
7615    pub camera_note: String,
7616    pub timing:      f64,   // seconds this panel represents
7617}
7618
7619pub struct Storyboard {
7620    pub panels:    Vec<StoryboardPanel>,
7621    pub title:     String,
7622}
7623
7624impl Storyboard {
7625    pub fn new(title: &str) -> Self { Storyboard { panels: Vec::new(), title: title.to_string() } }
7626
7627    pub fn add_panel(&mut self, shot_id: u64, description: &str, action: &str, timing: f64) {
7628        let idx = self.panels.len() as u32;
7629        self.panels.push(StoryboardPanel {
7630            shot_id, panel_index: idx,
7631            description: description.to_string(),
7632            action:      action.to_string(),
7633            dialogue:    String::new(),
7634            camera_note: String::new(),
7635            timing,
7636        });
7637    }
7638
7639    pub fn total_timing(&self) -> f64 { self.panels.iter().map(|p| p.timing).sum() }
7640
7641    pub fn export_pdf_text(&self) -> String {
7642        let mut out = format!("STORYBOARD: {}\n\n", self.title);
7643        for p in &self.panels {
7644            out.push_str(&format!(
7645                "Panel {:03} | Shot {} | {:.1}s\n  ACTION: {}\n  DESC: {}\n\n",
7646                p.panel_index + 1, p.shot_id, p.timing, p.action, p.description
7647            ));
7648        }
7649        out
7650    }
7651}
7652
7653// ============================================================
7654// CAMERA SENSOR PRESETS
7655// ============================================================
7656
7657#[derive(Clone, Debug)]
7658pub struct CameraSensor {
7659    pub name:           String,
7660    pub width_mm:       f32,
7661    pub height_mm:      f32,
7662    pub pixel_pitch_um: f32,
7663    pub iso_base:       u32,
7664    pub iso_max:        u32,
7665    pub dynamic_range:  f32,  // stops
7666}
7667
7668impl CameraSensor {
7669    pub fn arri_alexa_35() -> Self {
7670        CameraSensor { name: "ARRI Alexa 35".to_string(), width_mm: 27.99, height_mm: 19.22,
7671                       pixel_pitch_um: 8.55, iso_base: 800, iso_max: 6400, dynamic_range: 17.0 }
7672    }
7673
7674    pub fn red_v_raptor() -> Self {
7675        CameraSensor { name: "RED V-RAPTOR 8K".to_string(), width_mm: 40.96, height_mm: 21.6,
7676                       pixel_pitch_um: 5.0, iso_base: 800, iso_max: 12800, dynamic_range: 16.5 }
7677    }
7678
7679    pub fn sony_venice_2() -> Self {
7680        CameraSensor { name: "Sony VENICE 2".to_string(), width_mm: 35.9, height_mm: 24.0,
7681                       pixel_pitch_um: 5.0, iso_base: 500, iso_max: 102400, dynamic_range: 16.0 }
7682    }
7683
7684    /// Crop factor relative to full-frame 36×24mm.
7685    pub fn crop_factor(&self) -> f32 {
7686        let full_diag = (36.0f32 * 36.0 + 24.0 * 24.0).sqrt();
7687        let this_diag = (self.width_mm * self.width_mm + self.height_mm * self.height_mm).sqrt();
7688        full_diag / this_diag
7689    }
7690
7691    /// Horizontal FOV in degrees for a given focal length.
7692    pub fn hfov_deg(&self, focal_mm: f32) -> f32 {
7693        2.0 * (self.width_mm / (2.0 * focal_mm)).atan().to_degrees()
7694    }
7695
7696    /// Vertical FOV in degrees for a given focal length.
7697    pub fn vfov_deg(&self, focal_mm: f32) -> f32 {
7698        2.0 * (self.height_mm / (2.0 * focal_mm)).atan().to_degrees()
7699    }
7700}
7701
7702// ============================================================
7703// FINAL TESTS ROUND 5
7704// ============================================================
7705
7706#[cfg(test)]
7707mod tests_cinematic_round5 {
7708    use super::*;
7709
7710    #[test]
7711    fn test_sequence_query_shots_at_time() {
7712        let mut seq = CinematicSequencer::new("Q", 10.0, FrameRate::Fps24);
7713        seq.shot_list.shots.push(Shot {
7714            id: 1, name: "A".to_string(), camera_id: 0,
7715            start_time: 0.0, end_time: 5.0, transition: CutType::Cut,
7716            transition_duration: 0.0, take_number: 1,
7717            ..Shot::new(0, "", 0.0, 0.0, 0)
7718        });
7719        let q = SequenceQuery::new(&seq);
7720        let shots = q.shots_at_time(2.5);
7721        assert_eq!(shots.len(), 1);
7722        assert_eq!(shots[0].name, "A");
7723    }
7724
7725    #[test]
7726    fn test_sequence_query_longest_shot() {
7727        let mut seq = CinematicSequencer::new("Q", 10.0, FrameRate::Fps24);
7728        seq.shot_list.shots.push(Shot {
7729            id: 1, name: "Short".to_string(), camera_id: 0,
7730            start_time: 0.0, end_time: 2.0, transition: CutType::Cut,
7731            transition_duration: 0.0, take_number: 1,
7732            ..Shot::new(0, "", 0.0, 0.0, 0)
7733        });
7734        seq.shot_list.shots.push(Shot {
7735            id: 2, name: "Long".to_string(), camera_id: 0,
7736            start_time: 2.0, end_time: 8.0, transition: CutType::Cut,
7737            transition_duration: 0.0, take_number: 1,
7738            ..Shot::new(0, "", 0.0, 0.0, 0)
7739        });
7740        let q = SequenceQuery::new(&seq);
7741        assert_eq!(q.longest_shot().unwrap().name, "Long");
7742    }
7743
7744    #[test]
7745    fn test_frame_range_selection_round_trip() {
7746        let sel = FrameRangeSelection::from_times(1.0, 5.0, 24.0);
7747        assert_eq!(sel.start_frame, 24);
7748        assert_eq!(sel.end_frame,   120);
7749        assert!((sel.duration_secs() - 4.0).abs() < 0.01);
7750    }
7751
7752    #[test]
7753    fn test_frame_range_contains() {
7754        let sel = FrameRangeSelection { start_frame: 10, end_frame: 50, fps: 24.0 };
7755        assert!( sel.contains_frame(30));
7756        assert!(!sel.contains_frame(5));
7757    }
7758
7759    #[test]
7760    fn test_storyboard_total_timing() {
7761        let mut sb = Storyboard::new("Test");
7762        sb.add_panel(1, "Wide shot", "Hero enters", 3.0);
7763        sb.add_panel(2, "CU face",   "Hero reacts",  2.0);
7764        assert!((sb.total_timing() - 5.0).abs() < 0.01);
7765    }
7766
7767    #[test]
7768    fn test_storyboard_export_text_contains_panel() {
7769        let mut sb = Storyboard::new("MyFilm");
7770        sb.add_panel(1, "Desc A", "Action A", 2.0);
7771        let text = sb.export_pdf_text();
7772        assert!(text.contains("Panel 001"));
7773        assert!(text.contains("Desc A"));
7774    }
7775
7776    #[test]
7777    fn test_camera_sensor_crop_factor_full_frame() {
7778        // A 36×24 sensor should have crop factor ~1.0
7779        let full = CameraSensor {
7780            name: "FF".to_string(), width_mm: 36.0, height_mm: 24.0,
7781            pixel_pitch_um: 5.0, iso_base: 100, iso_max: 6400, dynamic_range: 14.0,
7782        };
7783        assert!((full.crop_factor() - 1.0).abs() < 0.05);
7784    }
7785
7786    #[test]
7787    fn test_camera_sensor_vfov() {
7788        let s = CameraSensor::arri_alexa_35();
7789        let vfov = s.vfov_deg(50.0);
7790        // With a ~19mm height and 50mm lens, FOV should be in roughly 20-30 degrees
7791        assert!(vfov > 15.0 && vfov < 35.0);
7792    }
7793
7794    #[test]
7795    fn test_camera_sensor_hfov_wider_than_vfov() {
7796        let s = CameraSensor::sony_venice_2();
7797        let hfov = s.hfov_deg(35.0);
7798        let vfov = s.vfov_deg(35.0);
7799        assert!(hfov > vfov);
7800    }
7801
7802    #[test]
7803    fn test_timecode_string_format() {
7804        let sel = FrameRangeSelection { start_frame: 0, end_frame: 24, fps: 24.0 };
7805        let s = sel.to_timecode_string(24.0);
7806        assert!(s.contains("00:00:00:00"));
7807        assert!(s.contains("00:00:01:00"));
7808    }
7809}
7810
7811// ============================================================
7812// VELOCITY CURVE ANALYSER
7813// ============================================================
7814
7815/// Compute the velocity (first derivative) of an actor's position FloatCurve at each keyframe.
7816pub fn actor_velocity_at_keys(track: &ActorTrack) -> Vec<(f64, Vec3)> {
7817    let n = track.keyframes.len();
7818    if n < 2 { return Vec::new(); }
7819    let mut result = Vec::with_capacity(n);
7820    for i in 0..n {
7821        let (t_prev, p_prev) = if i == 0 {
7822            (track.keyframes[0].time, track.keyframes[0].position)
7823        } else {
7824            (track.keyframes[i-1].time, track.keyframes[i-1].position)
7825        };
7826        let (t_next, p_next) = if i + 1 < n {
7827            (track.keyframes[i+1].time, track.keyframes[i+1].position)
7828        } else {
7829            (track.keyframes[n-1].time, track.keyframes[n-1].position)
7830        };
7831        let dt = (t_next - t_prev).max(1e-10);
7832        let vel = (p_next - p_prev) / dt as f32;
7833        result.push((track.keyframes[i].time, vel));
7834    }
7835    result
7836}
7837
7838/// Compute the acceleration (second derivative) from velocity samples.
7839pub fn actor_acceleration_from_velocity(velocities: &[(f64, Vec3)]) -> Vec<(f64, Vec3)> {
7840    let n = velocities.len();
7841    if n < 2 { return Vec::new(); }
7842    let mut acc = Vec::with_capacity(n);
7843    for i in 0..n {
7844        let (t0, v0) = if i == 0 { velocities[0] } else { velocities[i-1] };
7845        let (t1, v1) = if i+1 < n { velocities[i+1] } else { velocities[n-1] };
7846        let dt = (t1 - t0).max(1e-10);
7847        acc.push((velocities[i].0, (v1 - v0) / dt as f32));
7848    }
7849    acc
7850}
7851
7852/// Estimate the peak G-force experienced by an actor along a path.
7853pub fn peak_g_force(track: &ActorTrack) -> f32 {
7854    let vels  = actor_velocity_at_keys(track);
7855    let accs  = actor_acceleration_from_velocity(&vels);
7856    let g = 9.81f32;
7857    accs.iter().map(|(_, a)| a.length() / g).fold(0.0f32, f32::max)
7858}
7859
7860// ============================================================
7861// SEQUENCE LOCK / PROTECTION
7862// ============================================================
7863
7864pub struct SequenceLock {
7865    pub locked:     bool,
7866    pub lock_time:  f64,     // wallclock seconds (placeholder)
7867    pub reason:     String,
7868    pub locked_by:  String,
7869}
7870
7871impl SequenceLock {
7872    pub fn new() -> Self { SequenceLock { locked: false, lock_time: 0.0, reason: String::new(), locked_by: String::new() } }
7873
7874    pub fn lock(&mut self, by: &str, reason: &str, time: f64) {
7875        self.locked    = true;
7876        self.locked_by = by.to_string();
7877        self.reason    = reason.to_string();
7878        self.lock_time = time;
7879    }
7880
7881    pub fn unlock(&mut self) { self.locked = false; self.locked_by.clear(); self.reason.clear(); }
7882
7883    pub fn check(&self) -> Result<(), String> {
7884        if self.locked {
7885            Err(format!("Locked by '{}': {}", self.locked_by, self.reason))
7886        } else { Ok(()) }
7887    }
7888}
7889
7890// ============================================================
7891// TAKE COMPARISON UTILITY
7892// ============================================================
7893
7894/// Compute the mean-squared difference between two camera tracks (positional).
7895pub fn camera_track_mse(a: &CameraTrack, b: &CameraTrack, samples: usize) -> f32 {
7896    let dur_a = a.keyframes.last().map(|k| k.time).unwrap_or(0.0);
7897    let dur_b = b.keyframes.last().map(|k| k.time).unwrap_or(0.0);
7898    let dur = dur_a.min(dur_b);
7899    if dur < 1e-10 { return 0.0; }
7900    let mut mse = 0.0f32;
7901    for i in 0..samples {
7902        let t = dur * i as f64 / (samples - 1).max(1) as f64;
7903        let pa = a.evaluate_position(t);
7904        let pb = b.evaluate_position(t);
7905        mse += (pa - pb).length_squared();
7906    }
7907    mse / samples as f32
7908}
7909
7910// ============================================================
7911// FINAL TESTS ROUND 6
7912// ============================================================
7913
7914#[cfg(test)]
7915mod tests_cinematic_round6 {
7916    use super::*;
7917
7918    #[test]
7919    fn test_actor_velocity_count() {
7920        let mut track = ActorTrack::new(1, "Hero", 0);
7921        for i in 0..5 {
7922            track.keyframes.push(ActorKeyframe {
7923                time: i as f64, position: Vec3::new(i as f32, 0.0, 0.0),
7924                rotation: Quat::IDENTITY, scale: Vec3::ONE, interp: InterpType::Linear,
7925            });
7926        }
7927        let vels = actor_velocity_at_keys(&track);
7928        assert_eq!(vels.len(), 5);
7929        // Constant velocity: each should be ~Vec3::X
7930        for (_, v) in &vels { assert!((v.x - 1.0).abs() < 0.05); }
7931    }
7932
7933    #[test]
7934    fn test_sequence_lock_check() {
7935        let mut sl = SequenceLock::new();
7936        assert!(sl.check().is_ok());
7937        sl.lock("Alice", "Final cut", 0.0);
7938        assert!(sl.check().is_err());
7939        sl.unlock();
7940        assert!(sl.check().is_ok());
7941    }
7942
7943    #[test]
7944    fn test_camera_track_mse_identical() {
7945        let mut cam = CameraTrack::new(1, "C");
7946        cam.keyframes.push(CameraKeyframe {
7947            time: 0.0, position: Vec3::ZERO, rotation: Quat::IDENTITY,
7948            fov: 60.0, near_clip: 0.1, far_clip: 100.0,
7949            focal_length: 50.0, aperture: 2.8, focus_distance: 5.0,
7950            interp: InterpType::Linear,
7951        });
7952        cam.keyframes.push(CameraKeyframe {
7953            time: 1.0, position: Vec3::ONE, rotation: Quat::IDENTITY,
7954            fov: 60.0, near_clip: 0.1, far_clip: 100.0,
7955            focal_length: 50.0, aperture: 2.8, focus_distance: 5.0,
7956            interp: InterpType::Linear,
7957        });
7958        let mse = camera_track_mse(&cam, &cam, 32);
7959        assert!(mse < 1e-5);
7960    }
7961
7962    #[test]
7963    fn test_frame_range_duration_frames() {
7964        let sel = FrameRangeSelection::from_times(0.0, 2.0, 25.0);
7965        assert_eq!(sel.duration_frames(), 50);
7966    }
7967}
7968
7969// ============================================================
7970// MISCELLANEOUS MATH UTILITIES (cinematic)
7971// ============================================================
7972
7973/// Signed angle (degrees) between two vectors projected on a plane defined by `normal`.
7974pub fn signed_angle_deg(a: Vec3, b: Vec3, normal: Vec3) -> f32 {
7975    let a = a.normalize_or_zero();
7976    let b = b.normalize_or_zero();
7977    let cross = a.cross(b);
7978    let s = cross.length() * cross.dot(normal).signum();
7979    let c = a.dot(b);
7980    s.atan2(c).to_degrees()
7981}
7982
7983/// Compute the angular velocity (rad/s) between consecutive camera keyframes.
7984pub fn camera_angular_velocity(track: &CameraTrack, time: f64) -> f32 {
7985    let idx = track.keyframes.partition_point(|k| k.time <= time);
7986    if idx == 0 || idx >= track.keyframes.len() { return 0.0; }
7987    let a = &track.keyframes[idx - 1];
7988    let b = &track.keyframes[idx];
7989    let dt = (b.time - a.time).max(1e-10) as f32;
7990    let rel_rot = b.rotation * a.rotation.inverse();
7991    let (axis, angle) = rel_rot.to_axis_angle();
7992    let _ = axis;
7993    angle / dt
7994}
7995
7996/// Smoothstep interpolation between two quaternion rotations.
7997pub fn quat_smooth_lerp(a: Quat, b: Quat, t: f32) -> Quat {
7998    let smooth = t * t * (3.0 - 2.0 * t);
7999    a.slerp(b, smooth)
8000}
8001
8002/// Compute the focus pull distance change rate (m/s) given consecutive DOF keyframes.
8003pub fn focus_pull_speed(dof_a: &DepthOfFieldKeyframe, dof_b: &DepthOfFieldKeyframe) -> f32 {
8004    let dt = (dof_b.time - dof_a.time).max(1e-10) as f32;
8005    (dof_b.focus_distance - dof_a.focus_distance).abs() / dt
8006}
8007
8008/// Convert focal length (mm) and sensor height (mm) to vertical FOV in radians.
8009pub fn focal_to_vfov(focal_mm: f32, sensor_height_mm: f32) -> f32 {
8010    2.0 * (sensor_height_mm / (2.0 * focal_mm)).atan()
8011}
8012
8013/// Convert vertical FOV (radians) and sensor height (mm) to focal length in mm.
8014pub fn vfov_to_focal(vfov_rad: f32, sensor_height_mm: f32) -> f32 {
8015    sensor_height_mm / (2.0 * (vfov_rad * 0.5).tan())
8016}
8017
8018#[cfg(test)]
8019mod tests_cinematic_math {
8020    use super::*;
8021
8022    #[test]
8023    fn test_signed_angle_90_deg() {
8024        let a = Vec3::X;
8025        let b = Vec3::Z;
8026        let angle = signed_angle_deg(a, b, Vec3::Y);
8027        assert!((angle.abs() - 90.0).abs() < 0.1);
8028    }
8029
8030    #[test]
8031    fn test_quat_smooth_lerp_midpoint() {
8032        let a = Quat::IDENTITY;
8033        let b = Quat::from_rotation_y(std::f32::consts::FRAC_PI_2);
8034        let mid = quat_smooth_lerp(a, b, 0.5);
8035        let expected = a.slerp(b, 0.5);
8036        assert!(mid.dot(expected) > 0.99);
8037    }
8038
8039    #[test]
8040    fn test_focal_vfov_round_trip() {
8041        let sensor_h = 24.0f32;
8042        let focal    = 50.0f32;
8043        let vfov = focal_to_vfov(focal, sensor_h);
8044        let back = vfov_to_focal(vfov, sensor_h);
8045        assert!((back - focal).abs() < 0.01);
8046    }
8047
8048    #[test]
8049    fn test_focus_pull_speed_positive() {
8050        let a = DepthOfFieldKeyframe { time: 0.0, focal_length: 50.0, aperture: 2.8, focus_distance: 2.0, sensor_width: 36.0 };
8051        let b = DepthOfFieldKeyframe { time: 1.0, focal_length: 50.0, aperture: 2.8, focus_distance: 8.0, sensor_width: 36.0 };
8052        let speed = focus_pull_speed(&a, &b);
8053        assert!((speed - 6.0).abs() < 0.1);
8054    }
8055
8056    #[test]
8057    fn test_vfov_to_focal_50mm() {
8058        // Standard 50mm on 24mm sensor
8059        let vfov = focal_to_vfov(50.0, 24.0);
8060        let focal = vfov_to_focal(vfov, 24.0);
8061        assert!((focal - 50.0).abs() < 0.01);
8062    }
8063
8064    #[test]
8065    fn test_camera_angular_velocity_static() {
8066        let mut track = CameraTrack::new(1, "C");
8067        track.keyframes.push(CameraKeyframe {
8068            time: 0.0, position: Vec3::ZERO, rotation: Quat::IDENTITY,
8069            fov: 60.0, near_clip: 0.1, far_clip: 100.0,
8070            focal_length: 50.0, aperture: 2.8, focus_distance: 5.0,
8071            interp: InterpType::Linear,
8072        });
8073        track.keyframes.push(CameraKeyframe {
8074            time: 1.0, position: Vec3::ONE, rotation: Quat::IDENTITY,
8075            fov: 60.0, near_clip: 0.1, far_clip: 100.0,
8076            focal_length: 50.0, aperture: 2.8, focus_distance: 5.0,
8077            interp: InterpType::Linear,
8078        });
8079        let omega = camera_angular_velocity(&track, 0.5);
8080        assert!(omega.abs() < 0.001); // same rotation → zero angular velocity
8081    }
8082}