Skip to main content

proof_engine/editor/
cutscene_importer.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 MAX_BONES: usize = 512;
11const MAX_KEYFRAMES: usize = 65536;
12const MAX_TRACKS: usize = 1024;
13const LEVENSHTEIN_MAX_LEN: usize = 256;
14const GIMBAL_LOCK_THRESHOLD: f32 = 80.0; // degrees/frame
15const FOV_MIN: f32 = 5.0;
16const FOV_MAX: f32 = 170.0;
17const DOF_MIN_FOCUS: f32 = 0.1;
18const DOF_MAX_FOCUS: f32 = 10000.0;
19const AUDIO_CLIP_THRESHOLD: f32 = 0.99;
20const AUDIO_CLIP_SAMPLE_WINDOW: usize = 1024;
21const BPM_MIN: f32 = 20.0;
22const BPM_MAX: f32 = 400.0;
23const SRT_MAGIC: &str = "-->";
24const WEBVTT_MAGIC: &str = "WEBVTT";
25const CUSTOM_FORMAT_VERSION: u32 = 3;
26const BEZIER_FIT_ITERATIONS: usize = 100;
27const BEZIER_FIT_TOLERANCE: f32 = 0.001;
28const PROGRESS_STEPS: u32 = 100;
29const IMPORT_REPORT_MAX_ERRORS: usize = 1024;
30const BATCH_MAX_FILES: usize = 256;
31
32// ============================================================
33// TOKENIZER
34// ============================================================
35
36#[derive(Clone, Debug, PartialEq)]
37pub enum Token {
38    Ident(String),
39    StringLit(String),
40    IntLit(i64),
41    FloatLit(f64),
42    LBrace,
43    RBrace,
44    LBracket,
45    RBracket,
46    LParen,
47    RParen,
48    Colon,
49    Comma,
50    Semicolon,
51    Equals,
52    Dot,
53    At,
54    Hash,
55    Newline,
56    Eof,
57}
58
59#[derive(Clone, Debug)]
60pub struct TokenStream {
61    pub tokens: Vec<Token>,
62    pub pos: usize,
63    pub source: String,
64}
65
66impl TokenStream {
67    pub fn from_source(src: &str) -> Self {
68        let tokens = Tokenizer::tokenize(src);
69        TokenStream {
70            tokens,
71            pos: 0,
72            source: src.to_string(),
73        }
74    }
75
76    pub fn peek(&self) -> &Token {
77        if self.pos < self.tokens.len() {
78            &self.tokens[self.pos]
79        } else {
80            &Token::Eof
81        }
82    }
83
84    pub fn next(&mut self) -> Token {
85        if self.pos < self.tokens.len() {
86            let t = self.tokens[self.pos].clone();
87            self.pos += 1;
88            t
89        } else {
90            Token::Eof
91        }
92    }
93
94    pub fn expect_ident(&mut self) -> Option<String> {
95        if let Token::Ident(s) = self.peek().clone() {
96            self.pos += 1;
97            Some(s)
98        } else {
99            None
100        }
101    }
102
103    pub fn expect_float(&mut self) -> Option<f64> {
104        match self.peek().clone() {
105            Token::FloatLit(f) => { self.pos += 1; Some(f) }
106            Token::IntLit(i) => { self.pos += 1; Some(i as f64) }
107            _ => None,
108        }
109    }
110
111    pub fn expect_int(&mut self) -> Option<i64> {
112        if let Token::IntLit(i) = self.peek().clone() {
113            self.pos += 1;
114            Some(i)
115        } else {
116            None
117        }
118    }
119
120    pub fn skip_whitespace_and_newlines(&mut self) {
121        while matches!(self.peek(), Token::Newline) {
122            self.pos += 1;
123        }
124    }
125
126    pub fn is_eof(&self) -> bool {
127        matches!(self.peek(), Token::Eof)
128    }
129}
130
131pub struct Tokenizer;
132
133impl Tokenizer {
134    pub fn tokenize(src: &str) -> Vec<Token> {
135        let mut tokens = Vec::new();
136        let chars: Vec<char> = src.chars().collect();
137        let mut i = 0;
138        while i < chars.len() {
139            match chars[i] {
140                ' ' | '\t' | '\r' => { i += 1; }
141                '\n' => { tokens.push(Token::Newline); i += 1; }
142                '{' => { tokens.push(Token::LBrace); i += 1; }
143                '}' => { tokens.push(Token::RBrace); i += 1; }
144                '[' => { tokens.push(Token::LBracket); i += 1; }
145                ']' => { tokens.push(Token::RBracket); i += 1; }
146                '(' => { tokens.push(Token::LParen); i += 1; }
147                ')' => { tokens.push(Token::RParen); i += 1; }
148                ':' => { tokens.push(Token::Colon); i += 1; }
149                ',' => { tokens.push(Token::Comma); i += 1; }
150                ';' => { tokens.push(Token::Semicolon); i += 1; }
151                '=' => { tokens.push(Token::Equals); i += 1; }
152                '.' => { tokens.push(Token::Dot); i += 1; }
153                '@' => { tokens.push(Token::At); i += 1; }
154                '#' => {
155                    // Line comment — skip to end of line
156                    while i < chars.len() && chars[i] != '\n' { i += 1; }
157                }
158                '"' => {
159                    i += 1;
160                    let mut s = String::new();
161                    while i < chars.len() && chars[i] != '"' {
162                        if chars[i] == '\\' && i + 1 < chars.len() {
163                            i += 1;
164                            match chars[i] {
165                                'n' => s.push('\n'),
166                                't' => s.push('\t'),
167                                'r' => s.push('\r'),
168                                '"' => s.push('"'),
169                                '\\' => s.push('\\'),
170                                _ => { s.push('\\'); s.push(chars[i]); }
171                            }
172                        } else {
173                            s.push(chars[i]);
174                        }
175                        i += 1;
176                    }
177                    if i < chars.len() { i += 1; } // closing quote
178                    tokens.push(Token::StringLit(s));
179                }
180                c if c == '-' || c.is_ascii_digit() => {
181                    let start = i;
182                    let mut is_float = false;
183                    if chars[i] == '-' { i += 1; }
184                    while i < chars.len() && chars[i].is_ascii_digit() { i += 1; }
185                    if i < chars.len() && chars[i] == '.' {
186                        is_float = true;
187                        i += 1;
188                        while i < chars.len() && chars[i].is_ascii_digit() { i += 1; }
189                    }
190                    if i < chars.len() && (chars[i] == 'e' || chars[i] == 'E') {
191                        is_float = true;
192                        i += 1;
193                        if i < chars.len() && (chars[i] == '+' || chars[i] == '-') { i += 1; }
194                        while i < chars.len() && chars[i].is_ascii_digit() { i += 1; }
195                    }
196                    let s: String = chars[start..i].iter().collect();
197                    if is_float {
198                        if let Ok(f) = s.parse::<f64>() {
199                            tokens.push(Token::FloatLit(f));
200                        }
201                    } else {
202                        if let Ok(n) = s.parse::<i64>() {
203                            tokens.push(Token::IntLit(n));
204                        }
205                    }
206                }
207                c if c.is_alphabetic() || c == '_' => {
208                    let start = i;
209                    while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') { i += 1; }
210                    let s: String = chars[start..i].iter().collect();
211                    tokens.push(Token::Ident(s));
212                }
213                _ => { i += 1; }
214            }
215        }
216        tokens.push(Token::Eof);
217        tokens
218    }
219}
220
221// ============================================================
222// AST NODES
223// ============================================================
224
225#[derive(Clone, Debug)]
226pub enum AstValue {
227    Str(String),
228    Int(i64),
229    Float(f64),
230    Vec3Val([f64; 3]),
231    Vec4Val([f64; 4]),
232    List(Vec<AstValue>),
233    Block(AstBlock),
234    Bool(bool),
235}
236
237#[derive(Clone, Debug)]
238pub struct AstField {
239    pub name: String,
240    pub value: AstValue,
241}
242
243#[derive(Clone, Debug)]
244pub struct AstBlock {
245    pub kind: String,
246    pub name: Option<String>,
247    pub fields: Vec<AstField>,
248    pub children: Vec<AstBlock>,
249}
250
251pub struct AstParser;
252
253impl AstParser {
254    pub fn parse(ts: &mut TokenStream) -> AstBlock {
255        let mut root = AstBlock {
256            kind: "root".to_string(),
257            name: None,
258            fields: Vec::new(),
259            children: Vec::new(),
260        };
261        ts.skip_whitespace_and_newlines();
262        while !ts.is_eof() {
263            ts.skip_whitespace_and_newlines();
264            if ts.is_eof() { break; }
265            if let Some(block) = Self::parse_block(ts) {
266                root.children.push(block);
267            } else {
268                ts.next(); // skip unknown
269            }
270        }
271        root
272    }
273
274    fn parse_block(ts: &mut TokenStream) -> Option<AstBlock> {
275        ts.skip_whitespace_and_newlines();
276        let kind = ts.expect_ident()?;
277        ts.skip_whitespace_and_newlines();
278        let name = if let Token::StringLit(s) | Token::Ident(s) = ts.peek().clone() {
279            if matches!(ts.peek(), Token::StringLit(_) | Token::Ident(_)) {
280                let tok = ts.next();
281                match tok {
282                    Token::StringLit(s) | Token::Ident(s) => Some(s),
283                    _ => None,
284                }
285            } else { None }
286        } else { None };
287        ts.skip_whitespace_and_newlines();
288        if !matches!(ts.peek(), Token::LBrace) { return None; }
289        ts.next(); // {
290        let mut block = AstBlock { kind, name, fields: Vec::new(), children: Vec::new() };
291        loop {
292            ts.skip_whitespace_and_newlines();
293            if ts.is_eof() || matches!(ts.peek(), Token::RBrace) { break; }
294            // Try to parse field: ident = value
295            let saved_pos = ts.pos;
296            if let Some(field_name) = ts.expect_ident() {
297                ts.skip_whitespace_and_newlines();
298                if matches!(ts.peek(), Token::Equals) {
299                    ts.next();
300                    ts.skip_whitespace_and_newlines();
301                    if let Some(val) = Self::parse_value(ts) {
302                        block.fields.push(AstField { name: field_name, value: val });
303                        continue;
304                    }
305                } else if matches!(ts.peek(), Token::LBrace) {
306                    // nested block
307                    ts.pos = saved_pos;
308                    if let Some(child) = Self::parse_block(ts) {
309                        block.children.push(child);
310                        continue;
311                    }
312                }
313                ts.pos = saved_pos;
314            }
315            ts.next(); // skip token
316        }
317        if matches!(ts.peek(), Token::RBrace) { ts.next(); }
318        Some(block)
319    }
320
321    fn parse_value(ts: &mut TokenStream) -> Option<AstValue> {
322        ts.skip_whitespace_and_newlines();
323        match ts.peek().clone() {
324            Token::StringLit(s) => { ts.next(); Some(AstValue::Str(s)) }
325            Token::IntLit(n) => { ts.next(); Some(AstValue::Int(n)) }
326            Token::FloatLit(f) => { ts.next(); Some(AstValue::Float(f)) }
327            Token::Ident(s) => {
328                let s_lower = s.to_lowercase();
329                ts.next();
330                if s_lower == "true" { Some(AstValue::Bool(true)) }
331                else if s_lower == "false" { Some(AstValue::Bool(false)) }
332                else { Some(AstValue::Str(s)) }
333            }
334            Token::LParen => {
335                ts.next();
336                let mut vals = Vec::new();
337                while !matches!(ts.peek(), Token::RParen | Token::Eof) {
338                    if let Some(v) = ts.expect_float() { vals.push(v); }
339                    if matches!(ts.peek(), Token::Comma) { ts.next(); }
340                }
341                if matches!(ts.peek(), Token::RParen) { ts.next(); }
342                match vals.len() {
343                    3 => Some(AstValue::Vec3Val([vals[0], vals[1], vals[2]])),
344                    4 => Some(AstValue::Vec4Val([vals[0], vals[1], vals[2], vals[3]])),
345                    _ => Some(AstValue::List(vals.iter().map(|&f| AstValue::Float(f)).collect())),
346                }
347            }
348            Token::LBracket => {
349                ts.next();
350                let mut items = Vec::new();
351                while !matches!(ts.peek(), Token::RBracket | Token::Eof) {
352                    if let Some(v) = Self::parse_value(ts) { items.push(v); }
353                    if matches!(ts.peek(), Token::Comma) { ts.next(); }
354                }
355                if matches!(ts.peek(), Token::RBracket) { ts.next(); }
356                Some(AstValue::List(items))
357            }
358            _ => None,
359        }
360    }
361}
362
363// ============================================================
364// ANIMATION DATA TYPES
365// ============================================================
366
367#[derive(Clone, Copy, Debug, PartialEq)]
368pub enum InterpolationType {
369    Constant,
370    Linear,
371    Cubic,
372    BezierCubic,
373}
374
375#[derive(Clone, Debug)]
376pub struct Keyframe<T: Clone> {
377    pub time: f32,
378    pub value: T,
379    pub tangent_in: T,
380    pub tangent_out: T,
381    pub interp: InterpolationType,
382}
383
384impl<T: Clone + Default> Keyframe<T> {
385    pub fn new(time: f32, value: T) -> Self {
386        Keyframe {
387            time,
388            value: value.clone(),
389            tangent_in: T::default(),
390            tangent_out: T::default(),
391            interp: InterpolationType::Linear,
392        }
393    }
394}
395
396#[derive(Clone, Debug)]
397pub struct TransformTrack {
398    pub bone_name: String,
399    pub position_keys: Vec<Keyframe<Vec3>>,
400    pub rotation_keys: Vec<Keyframe<Quat>>,
401    pub scale_keys: Vec<Keyframe<Vec3>>,
402}
403
404impl TransformTrack {
405    pub fn new(bone_name: &str) -> Self {
406        TransformTrack {
407            bone_name: bone_name.to_string(),
408            position_keys: Vec::new(),
409            rotation_keys: Vec::new(),
410            scale_keys: Vec::new(),
411        }
412    }
413
414    pub fn sample_position(&self, t: f32) -> Vec3 {
415        sample_vec3_track(&self.position_keys, t).unwrap_or(Vec3::ZERO)
416    }
417
418    pub fn sample_rotation(&self, t: f32) -> Quat {
419        sample_quat_track(&self.rotation_keys, t).unwrap_or(Quat::IDENTITY)
420    }
421
422    pub fn sample_scale(&self, t: f32) -> Vec3 {
423        sample_vec3_track(&self.scale_keys, t).unwrap_or(Vec3::ONE)
424    }
425}
426
427pub fn sample_vec3_track(keys: &[Keyframe<Vec3>], t: f32) -> Option<Vec3> {
428    if keys.is_empty() { return None; }
429    if t <= keys[0].time { return Some(keys[0].value); }
430    if t >= keys[keys.len()-1].time { return Some(keys[keys.len()-1].value); }
431    for i in 0..keys.len()-1 {
432        if t >= keys[i].time && t <= keys[i+1].time {
433            let dt = keys[i+1].time - keys[i].time;
434            let u = if dt > 0.0 { (t - keys[i].time) / dt } else { 0.0 };
435            return Some(match keys[i].interp {
436                InterpolationType::Constant => keys[i].value,
437                InterpolationType::Linear => keys[i].value.lerp(keys[i+1].value, u),
438                InterpolationType::Cubic | InterpolationType::BezierCubic => {
439                    // Hermite
440                    let p0 = keys[i].value;
441                    let p1 = keys[i+1].value;
442                    let m0 = keys[i].tangent_out * dt;
443                    let m1 = keys[i+1].tangent_in * dt;
444                    hermite_vec3(p0, m0, p1, m1, u)
445                }
446            });
447        }
448    }
449    None
450}
451
452pub fn sample_quat_track(keys: &[Keyframe<Quat>], t: f32) -> Option<Quat> {
453    if keys.is_empty() { return None; }
454    if t <= keys[0].time { return Some(keys[0].value); }
455    if t >= keys[keys.len()-1].time { return Some(keys[keys.len()-1].value); }
456    for i in 0..keys.len()-1 {
457        if t >= keys[i].time && t <= keys[i+1].time {
458            let dt = keys[i+1].time - keys[i].time;
459            let u = if dt > 0.0 { (t - keys[i].time) / dt } else { 0.0 };
460            return Some(keys[i].value.slerp(keys[i+1].value, u));
461        }
462    }
463    None
464}
465
466pub fn hermite_vec3(p0: Vec3, m0: Vec3, p1: Vec3, m1: Vec3, t: f32) -> Vec3 {
467    let t2 = t * t;
468    let t3 = t2 * t;
469    let h00 = 2.0*t3 - 3.0*t2 + 1.0;
470    let h10 = t3 - 2.0*t2 + t;
471    let h01 = -2.0*t3 + 3.0*t2;
472    let h11 = t3 - t2;
473    p0*h00 + m0*h10 + p1*h01 + m1*h11
474}
475
476// ============================================================
477// CAMERA TRACK
478// ============================================================
479
480#[derive(Clone, Debug)]
481pub struct CameraKeyframe {
482    pub time: f32,
483    pub position: Vec3,
484    pub rotation: Quat,
485    pub fov_degrees: f32,
486    pub near_clip: f32,
487    pub far_clip: f32,
488    pub dof_focus_distance: f32,
489    pub dof_aperture: f32,
490    pub dof_enabled: bool,
491}
492
493impl CameraKeyframe {
494    pub fn default_at(time: f32) -> Self {
495        CameraKeyframe {
496            time,
497            position: Vec3::ZERO,
498            rotation: Quat::IDENTITY,
499            fov_degrees: 60.0,
500            near_clip: 0.1,
501            far_clip: 10000.0,
502            dof_focus_distance: 10.0,
503            dof_aperture: 2.8,
504            dof_enabled: false,
505        }
506    }
507}
508
509#[derive(Clone, Debug)]
510pub struct CameraTrack {
511    pub name: String,
512    pub keyframes: Vec<CameraKeyframe>,
513    pub bezier_positions: Vec<Vec3>,   // bezier control points for path
514    pub fov_curve: Vec<Keyframe<f32>>,
515    pub dof_focus_curve: Vec<Keyframe<f32>>,
516}
517
518impl CameraTrack {
519    pub fn new(name: &str) -> Self {
520        CameraTrack {
521            name: name.to_string(),
522            keyframes: Vec::new(),
523            bezier_positions: Vec::new(),
524            fov_curve: Vec::new(),
525            dof_focus_curve: Vec::new(),
526        }
527    }
528
529    pub fn sample(&self, t: f32) -> CameraKeyframe {
530        if self.keyframes.is_empty() { return CameraKeyframe::default_at(t); }
531        if t <= self.keyframes[0].time { return self.keyframes[0].clone(); }
532        if t >= self.keyframes[self.keyframes.len()-1].time {
533            return self.keyframes[self.keyframes.len()-1].clone();
534        }
535        for i in 0..self.keyframes.len()-1 {
536            let k0 = &self.keyframes[i];
537            let k1 = &self.keyframes[i+1];
538            if t >= k0.time && t <= k1.time {
539                let dt = k1.time - k0.time;
540                let u = if dt > 0.0 { (t - k0.time) / dt } else { 0.0 };
541                let pos = k0.position.lerp(k1.position, u);
542                let rot = k0.rotation.slerp(k1.rotation, u);
543                let fov = k0.fov_degrees + (k1.fov_degrees - k0.fov_degrees) * u;
544                let dof_dist = k0.dof_focus_distance + (k1.dof_focus_distance - k0.dof_focus_distance) * u;
545                let dof_ap = k0.dof_aperture + (k1.dof_aperture - k0.dof_aperture) * u;
546                return CameraKeyframe {
547                    time: t,
548                    position: pos,
549                    rotation: rot,
550                    fov_degrees: fov,
551                    near_clip: k0.near_clip,
552                    far_clip: k0.far_clip,
553                    dof_focus_distance: dof_dist,
554                    dof_aperture: dof_ap,
555                    dof_enabled: k0.dof_enabled,
556                };
557            }
558        }
559        self.keyframes[0].clone()
560    }
561
562    pub fn fit_bezier_path(&mut self) {
563        let positions: Vec<Vec3> = self.keyframes.iter().map(|k| k.position).collect();
564        if positions.len() < 2 { return; }
565        self.bezier_positions = BezierFitter::fit_to_points(&positions, BEZIER_FIT_TOLERANCE);
566    }
567}
568
569// ============================================================
570// BEZIER FITTER
571// ============================================================
572
573pub struct BezierFitter;
574
575impl BezierFitter {
576    pub fn fit_to_points(points: &[Vec3], tolerance: f32) -> Vec<Vec3> {
577        if points.len() < 2 { return points.to_vec(); }
578        let n = points.len();
579        let t_start_dir = (points[1] - points[0]).normalize_or_zero();
580        let t_end_dir = (points[n-1] - points[n-2]).normalize_or_zero();
581        Self::fit_recursive(points, t_start_dir, t_end_dir, tolerance)
582    }
583
584    fn fit_recursive(points: &[Vec3], t_start: Vec3, t_end: Vec3, tolerance: f32) -> Vec<Vec3> {
585        let n = points.len();
586        if n < 2 { return points.to_vec(); }
587        let (c1, c2) = Self::generate_bezier(points, t_start, t_end);
588        let p0 = points[0];
589        let p3 = points[n-1];
590        // Compute max error
591        let chord_lengths: Vec<f32> = {
592            let mut cl = vec![0.0f32; n];
593            for i in 1..n { cl[i] = cl[i-1] + points[i-1].distance(points[i]); }
594            cl
595        };
596        let total = chord_lengths[n-1];
597        let mut max_err = 0.0f32;
598        let mut split_idx = 0;
599        for i in 1..n-1 {
600            let t = if total > 0.0 { chord_lengths[i] / total } else { i as f32 / (n-1) as f32 };
601            let b = Self::eval_cubic(p0, c1, c2, p3, t);
602            let err = b.distance(points[i]);
603            if err > max_err { max_err = err; split_idx = i; }
604        }
605        if max_err <= tolerance || n <= 3 {
606            vec![p0, c1, c2, p3]
607        } else {
608            // Tangent at split
609            let t_split_dir = if split_idx < n-1 && split_idx > 0 {
610                (points[split_idx+1] - points[split_idx-1]).normalize_or_zero()
611            } else { Vec3::Z };
612            let mut left = Self::fit_recursive(&points[..=split_idx], t_start, t_split_dir, tolerance);
613            let right = Self::fit_recursive(&points[split_idx..], t_split_dir, t_end, tolerance);
614            left.pop();
615            left.extend(right);
616            left
617        }
618    }
619
620    fn generate_bezier(points: &[Vec3], t1: Vec3, t2: Vec3) -> (Vec3, Vec3) {
621        let n = points.len();
622        let p0 = points[0];
623        let p3 = points[n-1];
624        let chord = p0.distance(p3);
625        let alpha = chord / 3.0;
626        let c1 = p0 + t1 * alpha;
627        let c2 = p3 - t2 * alpha;
628        (c1, c2)
629    }
630
631    pub fn eval_cubic(p0: Vec3, p1: Vec3, p2: Vec3, p3: Vec3, t: f32) -> Vec3 {
632        let t2 = t * t;
633        let t3 = t2 * t;
634        let one_m_t = 1.0 - t;
635        let one_m_t2 = one_m_t * one_m_t;
636        let one_m_t3 = one_m_t2 * one_m_t;
637        p0 * one_m_t3
638            + p1 * (3.0 * one_m_t2 * t)
639            + p2 * (3.0 * one_m_t * t2)
640            + p3 * t3
641    }
642}
643
644// ============================================================
645// LIGHT TRACK
646// ============================================================
647
648#[derive(Clone, Copy, Debug, PartialEq)]
649pub enum LightType {
650    Point,
651    Spot,
652    Directional,
653    Area,
654}
655
656#[derive(Clone, Debug)]
657pub struct LightKeyframe {
658    pub time: f32,
659    pub position: Vec3,
660    pub rotation: Quat,
661    pub color: Vec3,
662    pub intensity: f32,
663    pub range: f32,
664    pub spot_angle: f32,
665    pub cast_shadows: bool,
666}
667
668#[derive(Clone, Debug)]
669pub struct LightTrack {
670    pub name: String,
671    pub light_type: LightType,
672    pub keyframes: Vec<LightKeyframe>,
673}
674
675impl LightTrack {
676    pub fn new(name: &str, light_type: LightType) -> Self {
677        LightTrack { name: name.to_string(), light_type, keyframes: Vec::new() }
678    }
679
680    pub fn add_keyframe(&mut self, kf: LightKeyframe) {
681        self.keyframes.push(kf);
682        self.keyframes.sort_by(|a, b| a.time.partial_cmp(&b.time).unwrap());
683    }
684
685    pub fn sample(&self, t: f32) -> Option<LightKeyframe> {
686        if self.keyframes.is_empty() { return None; }
687        if t <= self.keyframes[0].time { return Some(self.keyframes[0].clone()); }
688        let last = self.keyframes.last().unwrap();
689        if t >= last.time { return Some(last.clone()); }
690        for i in 0..self.keyframes.len() - 1 {
691            let a = &self.keyframes[i]; let b = &self.keyframes[i+1];
692            if t >= a.time && t <= b.time {
693                let f = (t - a.time) / (b.time - a.time);
694                return Some(LightKeyframe {
695                    time: t,
696                    position: a.position.lerp(b.position, f),
697                    rotation: a.rotation.slerp(b.rotation, f),
698                    color: a.color.lerp(b.color, f),
699                    intensity: a.intensity + f * (b.intensity - a.intensity),
700                    range: a.range + f * (b.range - a.range),
701                    spot_angle: a.spot_angle + f * (b.spot_angle - a.spot_angle),
702                    cast_shadows: if f < 0.5 { a.cast_shadows } else { b.cast_shadows },
703                });
704            }
705        }
706        None
707    }
708
709    pub fn duration(&self) -> f32 { self.keyframes.last().map(|k| k.time).unwrap_or(0.0) }
710    pub fn keyframe_count(&self) -> usize { self.keyframes.len() }
711}
712
713// ============================================================
714// SECTION: Animation Clip Metadata
715// ============================================================
716
717#[derive(Debug, Clone, PartialEq)]
718pub enum LoopMode { Once, Loop, PingPong, ClampForever }
719
720#[derive(Debug, Clone)]
721pub struct AnimationClipMeta {
722    pub clip_name: String,
723    pub duration_s: f32,
724    pub fps: f32,
725    pub loop_mode: LoopMode,
726    pub bone_count: u32,
727    pub keyframe_count: u32,
728    pub has_root_motion: bool,
729    pub root_motion_delta: Vec3,
730    pub events: Vec<String>,
731}
732
733impl AnimationClipMeta {
734    pub fn new(name: &str, duration: f32, fps: f32) -> Self {
735        AnimationClipMeta { clip_name: name.to_string(), duration_s: duration, fps,
736            loop_mode: LoopMode::Loop, bone_count: 0, keyframe_count: 0,
737            has_root_motion: false, root_motion_delta: Vec3::ZERO, events: Vec::new() }
738    }
739
740    pub fn frame_count(&self) -> u32 { (self.duration_s * self.fps) as u32 }
741    pub fn sample_time(&self, t: f32) -> f32 {
742        match self.loop_mode {
743            LoopMode::Once => t.clamp(0.0, self.duration_s),
744            LoopMode::Loop => t % self.duration_s,
745            LoopMode::PingPong => {
746                let cycle = t % (self.duration_s * 2.0);
747                if cycle < self.duration_s { cycle } else { self.duration_s * 2.0 - cycle }
748            },
749            LoopMode::ClampForever => t.clamp(0.0, self.duration_s),
750        }
751    }
752    pub fn is_finished(&self, t: f32) -> bool { self.loop_mode == LoopMode::Once && t >= self.duration_s }
753}
754
755// ============================================================
756// SECTION: Blend Tree
757// ============================================================
758
759#[derive(Debug, Clone)]
760pub enum BlendTreeNode {
761    Clip { name: String, weight: f32 },
762    Blend1D { parameter: String, children: Vec<(f32, BlendTreeNode)> },
763    Blend2D { param_x: String, param_y: String, children: Vec<(Vec2, BlendTreeNode)> },
764    Additive { base: Box<BlendTreeNode>, additive: Box<BlendTreeNode>, weight: f32 },
765    Override { base: Box<BlendTreeNode>, override_node: Box<BlendTreeNode>, mask: Vec<String> },
766}
767
768impl BlendTreeNode {
769    pub fn evaluate_1d(&self, param: f32) -> Vec<(String, f32)> {
770        match self {
771            BlendTreeNode::Clip { name, weight } => vec![(name.clone(), *weight)],
772            BlendTreeNode::Blend1D { children, .. } => {
773                if children.is_empty() { return Vec::new(); }
774                if children.len() == 1 { return children[0].1.evaluate_1d(param); }
775                let mut result = Vec::new();
776                for i in 0..children.len()-1 {
777                    let (t0, node0) = &children[i];
778                    let (t1, node1) = &children[i+1];
779                    if param >= *t0 && param <= *t1 {
780                        let f = (param - t0) / (t1 - t0);
781                        for (name, w) in node0.evaluate_1d(param) { result.push((name, w * (1.0 - f))); }
782                        for (name, w) in node1.evaluate_1d(param) { result.push((name, w * f)); }
783                        return result;
784                    }
785                }
786                children.last().unwrap().1.evaluate_1d(param)
787            },
788            _ => Vec::new(),
789        }
790    }
791
792    pub fn clip_names(&self) -> Vec<String> {
793        match self {
794            BlendTreeNode::Clip { name, .. } => vec![name.clone()],
795            BlendTreeNode::Blend1D { children, .. } => children.iter().flat_map(|(_, n)| n.clip_names()).collect(),
796            BlendTreeNode::Blend2D { children, .. } => children.iter().flat_map(|(_, n)| n.clip_names()).collect(),
797            BlendTreeNode::Additive { base, additive, .. } => { let mut v = base.clip_names(); v.extend(additive.clip_names()); v },
798            BlendTreeNode::Override { base, override_node, .. } => { let mut v = base.clip_names(); v.extend(override_node.clip_names()); v },
799        }
800    }
801}
802
803// ============================================================
804// SECTION: Animator State Machine
805// ============================================================
806
807#[derive(Debug, Clone)]
808pub struct AnimationState {
809    pub name: String,
810    pub clip_name: String,
811    pub speed: f32,
812    pub is_looping: bool,
813}
814
815impl AnimationState {
816    pub fn new(name: &str, clip: &str) -> Self {
817        AnimationState { name: name.to_string(), clip_name: clip.to_string(), speed: 1.0, is_looping: true }
818    }
819}
820
821#[derive(Debug, Clone, PartialEq)]
822pub enum TransitionCondition {
823    BoolTrue(String), BoolFalse(String),
824    FloatGreater(String, f32), FloatLess(String, f32),
825    IntEqual(String, i32), Trigger(String),
826}
827
828#[derive(Debug, Clone)]
829pub struct Transition {
830    pub from: String, pub to: String,
831    pub conditions: Vec<TransitionCondition>,
832    pub duration: f32, pub exit_time: Option<f32>,
833    pub can_interrupt: bool,
834}
835
836#[derive(Debug, Clone)]
837pub enum AnimParam { Bool(bool), Float(f32), Int(i32), Trigger(bool) }
838
839#[derive(Debug, Clone)]
840pub struct AnimatorStateMachine {
841    pub current_state: String,
842    pub states: HashMap<String, AnimationState>,
843    pub transitions: Vec<Transition>,
844    pub params: HashMap<String, AnimParam>,
845}
846
847impl AnimatorStateMachine {
848    pub fn new(initial: &str) -> Self {
849        AnimatorStateMachine { current_state: initial.to_string(), states: HashMap::new(), transitions: Vec::new(), params: HashMap::new() }
850    }
851
852    pub fn add_state(&mut self, state: AnimationState) { self.states.insert(state.name.clone(), state); }
853    pub fn add_transition(&mut self, t: Transition) { self.transitions.push(t); }
854    pub fn set_bool(&mut self, name: &str, v: bool) { self.params.insert(name.to_string(), AnimParam::Bool(v)); }
855    pub fn set_float(&mut self, name: &str, v: f32) { self.params.insert(name.to_string(), AnimParam::Float(v)); }
856    pub fn set_int(&mut self, name: &str, v: i32) { self.params.insert(name.to_string(), AnimParam::Int(v)); }
857    pub fn set_trigger(&mut self, name: &str) { self.params.insert(name.to_string(), AnimParam::Trigger(true)); }
858
859    fn check_condition(&self, cond: &TransitionCondition) -> bool {
860        match cond {
861            TransitionCondition::BoolTrue(n) => matches!(self.params.get(n), Some(AnimParam::Bool(true))),
862            TransitionCondition::BoolFalse(n) => matches!(self.params.get(n), Some(AnimParam::Bool(false))),
863            TransitionCondition::FloatGreater(n, v) => matches!(self.params.get(n), Some(AnimParam::Float(f)) if *f > *v),
864            TransitionCondition::FloatLess(n, v) => matches!(self.params.get(n), Some(AnimParam::Float(f)) if *f < *v),
865            TransitionCondition::IntEqual(n, v) => matches!(self.params.get(n), Some(AnimParam::Int(i)) if *i == *v),
866            TransitionCondition::Trigger(n) => matches!(self.params.get(n), Some(AnimParam::Trigger(true))),
867        }
868    }
869
870    pub fn update(&mut self, _dt: f32) -> Option<&'static str> {
871        let transitions_snapshot = self.transitions.clone();
872        for t in &transitions_snapshot {
873            if t.from != self.current_state { continue; }
874            if t.conditions.iter().all(|c| self.check_condition(c)) {
875                self.current_state = t.to.clone();
876                // Consume triggers
877                for cond in &t.conditions {
878                    if let TransitionCondition::Trigger(n) = cond {
879                        self.params.insert(n.clone(), AnimParam::Trigger(false));
880                    }
881                }
882                // Return a static reference name based on common states
883                return match self.current_state.as_str() {
884                    "idle" => Some("idle"),
885                    "run" => Some("run"),
886                    "walk" => Some("walk"),
887                    "attack" => Some("attack"),
888                    "jump" => Some("jump"),
889                    "death" => Some("death"),
890                    _ => Some("unknown"),
891                };
892            }
893        }
894        None
895    }
896
897    pub fn state_count(&self) -> usize { self.states.len() }
898    pub fn transition_count(&self) -> usize { self.transitions.len() }
899}
900
901// ============================================================
902// SECTION: Motion Capture
903// ============================================================
904
905#[derive(Debug, Clone)]
906pub struct MocapFrame {
907    pub time_s: f32,
908    pub bone_transforms: HashMap<String, (Vec3, Quat)>,
909}
910
911impl MocapFrame {
912    pub fn new(time_s: f32) -> Self { MocapFrame { time_s, bone_transforms: HashMap::new() } }
913    pub fn set_bone(&mut self, name: &str, pos: Vec3, rot: Quat) { self.bone_transforms.insert(name.to_string(), (pos, rot)); }
914    pub fn bone_count(&self) -> usize { self.bone_transforms.len() }
915}
916
917#[derive(Debug, Clone)]
918pub struct MocapClip {
919    pub name: String,
920    pub frames: Vec<MocapFrame>,
921    pub fps: f32,
922    pub bone_names: Vec<String>,
923}
924
925impl MocapClip {
926    pub fn new(name: &str, fps: f32) -> Self {
927        MocapClip { name: name.to_string(), frames: Vec::new(), fps, bone_names: Vec::new() }
928    }
929
930    pub fn add_frame(&mut self, f: MocapFrame) { self.frames.push(f); }
931    pub fn duration_s(&self) -> f32 { self.frames.last().map(|f| f.time_s).unwrap_or(0.0) }
932    pub fn frame_count(&self) -> usize { self.frames.len() }
933
934    pub fn frame_at_time(&self, t: f32) -> Option<&MocapFrame> {
935        self.frames.iter().min_by(|a, b| (a.time_s - t).abs().partial_cmp(&(b.time_s - t).abs()).unwrap())
936    }
937
938    pub fn downsample(&self, target_fps: f32) -> MocapClip {
939        let step = (self.fps / target_fps).max(1.0);
940        let mut result = MocapClip::new(&self.name, target_fps);
941        result.bone_names = self.bone_names.clone();
942        let mut i = 0.0f32;
943        while i < self.frames.len() as f32 {
944            result.add_frame(self.frames[i as usize].clone());
945            i += step;
946        }
947        result
948    }
949
950    pub fn retarget_to_skeleton(&self, mapping: &HashMap<String, String>) -> MocapClip {
951        let mut retargeted = MocapClip::new(&self.name, self.fps);
952        retargeted.bone_names = mapping.values().cloned().collect();
953        for frame in &self.frames {
954            let mut new_frame = MocapFrame::new(frame.time_s);
955            for (src, dst) in mapping {
956                if let Some(&(pos, rot)) = frame.bone_transforms.get(src) {
957                    new_frame.set_bone(dst, pos, rot);
958                }
959            }
960            retargeted.add_frame(new_frame);
961        }
962        retargeted
963    }
964}
965
966// ============================================================
967// SECTION: IK Solver
968// ============================================================
969
970#[derive(Debug, Clone)]
971pub struct IkChain {
972    pub bone_names: Vec<String>,
973    pub bone_lengths: Vec<f32>,
974    pub positions: Vec<Vec3>,
975    pub target: Vec3,
976    pub pole_target: Option<Vec3>,
977}
978
979impl IkChain {
980    pub fn new(bone_names: Vec<String>, lengths: Vec<f32>) -> Self {
981        let n = bone_names.len() + 1;
982        IkChain { bone_names, bone_lengths: lengths, positions: vec![Vec3::ZERO; n], target: Vec3::ZERO, pole_target: None }
983    }
984
985    pub fn total_length(&self) -> f32 { self.bone_lengths.iter().sum() }
986
987    pub fn solve_fabrik(&mut self, iterations: u32, tolerance: f32) {
988        let n = self.positions.len();
989        if n < 2 { return; }
990        let root = self.positions[0];
991        for _ in 0..iterations {
992            // Forward pass
993            self.positions[n - 1] = self.target;
994            for i in (0..n-1).rev() {
995                let dir = (self.positions[i] - self.positions[i+1]).normalize_or_zero();
996                self.positions[i] = self.positions[i+1] + dir * self.bone_lengths[i];
997            }
998            // Backward pass
999            self.positions[0] = root;
1000            for i in 0..n-1 {
1001                let dir = (self.positions[i+1] - self.positions[i]).normalize_or_zero();
1002                self.positions[i+1] = self.positions[i] + dir * self.bone_lengths[i];
1003            }
1004            if (self.positions[n-1] - self.target).length() < tolerance { break; }
1005        }
1006    }
1007
1008    pub fn two_bone_ik(&mut self) {
1009        if self.positions.len() < 3 || self.bone_lengths.len() < 2 { return; }
1010        let a = self.positions[0];
1011        let c = self.target;
1012        let len_ab = self.bone_lengths[0];
1013        let len_bc = self.bone_lengths[1];
1014        let dist = (c - a).length();
1015        let dist_clamped = dist.clamp((len_ab - len_bc).abs(), len_ab + len_bc);
1016        let cos_a = ((len_ab * len_ab + dist_clamped * dist_clamped - len_bc * len_bc) / (2.0 * len_ab * dist_clamped)).clamp(-1.0, 1.0);
1017        let angle_a = cos_a.acos();
1018        let dir_ac = (c - a).normalize_or_zero();
1019        let perp = if let Some(pole) = self.pole_target {
1020            let to_pole = (pole - a).normalize_or_zero();
1021            let proj = dir_ac * dir_ac.dot(to_pole);
1022            (to_pole - proj).normalize_or_zero()
1023        } else {
1024            Vec3::new(0.0, 1.0, 0.0)
1025        };
1026        let rot = Quat::from_axis_angle(perp, angle_a);
1027        self.positions[1] = a + rot * (dir_ac * len_ab);
1028        self.positions[2] = self.target;
1029    }
1030}
1031
1032// ============================================================
1033// SECTION: Facial Animation
1034// ============================================================
1035
1036pub const ARKIT_BLEND_SHAPE_NAMES: [&str; 52] = [
1037    "eyeBlinkLeft", "eyeLookDownLeft", "eyeLookInLeft", "eyeLookOutLeft", "eyeLookUpLeft",
1038    "eyeSquintLeft", "eyeWideLeft", "eyeBlinkRight", "eyeLookDownRight", "eyeLookInRight",
1039    "eyeLookOutRight", "eyeLookUpRight", "eyeSquintRight", "eyeWideRight",
1040    "jawForward", "jawLeft", "jawRight", "jawOpen",
1041    "mouthClose", "mouthFunnel", "mouthPucker", "mouthLeft", "mouthRight",
1042    "mouthSmileLeft", "mouthSmileRight", "mouthFrownLeft", "mouthFrownRight",
1043    "mouthDimpleLeft", "mouthDimpleRight", "mouthStretchLeft", "mouthStretchRight",
1044    "mouthRollLower", "mouthRollUpper", "mouthShrugLower", "mouthShrugUpper",
1045    "mouthPressLeft", "mouthPressRight", "mouthLowerDownLeft", "mouthLowerDownRight",
1046    "mouthUpperUpLeft", "mouthUpperUpRight", "browDownLeft", "browDownRight",
1047    "browInnerUp", "browOuterUpLeft", "browOuterUpRight",
1048    "cheekPuff", "cheekSquintLeft", "cheekSquintRight",
1049    "noseSneerLeft", "noseSneerRight", "tongueOut",
1050];
1051
1052#[derive(Debug, Clone)]
1053pub struct BlendShape {
1054    pub name: String,
1055    pub weight: f32,
1056}
1057
1058impl BlendShape {
1059    pub fn new(name: &str, weight: f32) -> Self { BlendShape { name: name.to_string(), weight: weight.clamp(0.0, 1.0) } }
1060}
1061
1062#[derive(Debug, Clone)]
1063pub struct FacialRig {
1064    pub shapes: Vec<BlendShape>,
1065}
1066
1067impl FacialRig {
1068    pub fn new() -> Self {
1069        FacialRig { shapes: ARKIT_BLEND_SHAPE_NAMES.iter().map(|&n| BlendShape::new(n, 0.0)).collect() }
1070    }
1071
1072    pub fn set(&mut self, name: &str, weight: f32) {
1073        if let Some(s) = self.shapes.iter_mut().find(|s| s.name == name) { s.weight = weight.clamp(0.0, 1.0); }
1074    }
1075
1076    pub fn get(&self, name: &str) -> f32 { self.shapes.iter().find(|s| s.name == name).map(|s| s.weight).unwrap_or(0.0) }
1077    pub fn reset(&mut self) { for s in &mut self.shapes { s.weight = 0.0; } }
1078
1079    pub fn apply_expression(&mut self, expression: &str) {
1080        self.reset();
1081        match expression {
1082            "happy" => { self.set("mouthSmileLeft", 0.8); self.set("mouthSmileRight", 0.8); self.set("cheekSquintLeft", 0.4); self.set("cheekSquintRight", 0.4); self.set("eyeSquintLeft", 0.3); self.set("eyeSquintRight", 0.3); },
1083            "sad" => { self.set("mouthFrownLeft", 0.7); self.set("mouthFrownRight", 0.7); self.set("browInnerUp", 0.5); self.set("eyeLookDownLeft", 0.3); self.set("eyeLookDownRight", 0.3); },
1084            "angry" => { self.set("browDownLeft", 0.9); self.set("browDownRight", 0.9); self.set("mouthStretchLeft", 0.4); self.set("mouthStretchRight", 0.4); self.set("noseSneerLeft", 0.5); self.set("noseSneerRight", 0.5); },
1085            "surprised" => { self.set("eyeWideLeft", 1.0); self.set("eyeWideRight", 1.0); self.set("jawOpen", 0.6); self.set("browOuterUpLeft", 0.8); self.set("browOuterUpRight", 0.8); },
1086            "fear" => { self.set("eyeWideLeft", 0.9); self.set("eyeWideRight", 0.9); self.set("browInnerUp", 0.7); self.set("mouthPressLeft", 0.4); self.set("mouthPressRight", 0.4); },
1087            "disgust" => { self.set("noseSneerLeft", 0.9); self.set("noseSneerRight", 0.9); self.set("mouthShrugUpper", 0.5); self.set("browDownLeft", 0.4); },
1088            _ => {}
1089        }
1090    }
1091
1092    pub fn active_shape_count(&self) -> usize { self.shapes.iter().filter(|s| s.weight > 0.01).count() }
1093}
1094
1095// ============================================================
1096// SECTION: Timeline System
1097// ============================================================
1098
1099#[derive(Debug, Clone)]
1100pub struct TimelineClip {
1101    pub id: u32,
1102    pub name: String,
1103    pub start_time: f32,
1104    pub duration: f32,
1105    pub blend_in: f32,
1106    pub blend_out: f32,
1107    pub speed: f32,
1108}
1109
1110impl TimelineClip {
1111    pub fn new(id: u32, name: &str, start: f32, duration: f32) -> Self {
1112        TimelineClip { id, name: name.to_string(), start_time: start, duration, blend_in: 0.0, blend_out: 0.0, speed: 1.0 }
1113    }
1114    pub fn end_time(&self) -> f32 { self.start_time + self.duration }
1115    pub fn is_active_at(&self, t: f32) -> bool { t >= self.start_time && t < self.end_time() }
1116    pub fn local_time(&self, t: f32) -> f32 { (t - self.start_time) * self.speed }
1117    pub fn blend_weight(&self, t: f32) -> f32 {
1118        if !self.is_active_at(t) { return 0.0; }
1119        let lt = t - self.start_time;
1120        let weight_in = if self.blend_in > 0.0 { (lt / self.blend_in).min(1.0) } else { 1.0 };
1121        let weight_out = if self.blend_out > 0.0 { ((self.duration - lt) / self.blend_out).min(1.0) } else { 1.0 };
1122        weight_in.min(weight_out)
1123    }
1124    pub fn overlaps(&self, other: &TimelineClip) -> bool {
1125        self.start_time < other.end_time() && self.end_time() > other.start_time
1126    }
1127}
1128
1129#[derive(Debug, Clone)]
1130pub struct TimelineTrack {
1131    pub name: String,
1132    pub clips: Vec<TimelineClip>,
1133    pub is_muted: bool,
1134    pub is_locked: bool,
1135}
1136
1137impl TimelineTrack {
1138    pub fn new(name: &str) -> Self { TimelineTrack { name: name.to_string(), clips: Vec::new(), is_muted: false, is_locked: false } }
1139    pub fn add_clip(&mut self, clip: TimelineClip) { self.clips.push(clip); self.clips.sort_by(|a, b| a.start_time.partial_cmp(&b.start_time).unwrap()); }
1140    pub fn clips_at(&self, t: f32) -> Vec<&TimelineClip> { self.clips.iter().filter(|c| c.is_active_at(t)).collect() }
1141    pub fn find_conflicts(&self) -> Vec<(u32, u32)> {
1142        let mut conflicts = Vec::new();
1143        for i in 0..self.clips.len() {
1144            for j in i+1..self.clips.len() {
1145                if self.clips[i].overlaps(&self.clips[j]) { conflicts.push((self.clips[i].id, self.clips[j].id)); }
1146            }
1147        }
1148        conflicts
1149    }
1150    pub fn duration(&self) -> f32 { self.clips.iter().map(|c| c.end_time()).fold(0.0_f32, f32::max) }
1151    pub fn clip_count(&self) -> usize { self.clips.len() }
1152}
1153
1154#[derive(Debug, Clone)]
1155pub struct MasterTimeline {
1156    pub tracks: Vec<TimelineTrack>,
1157    pub current_time: f32,
1158    pub fps: f32,
1159    pub is_playing: bool,
1160}
1161
1162impl MasterTimeline {
1163    pub fn new(fps: f32) -> Self { MasterTimeline { tracks: Vec::new(), current_time: 0.0, fps, is_playing: false } }
1164    pub fn add_track(&mut self, t: TimelineTrack) { self.tracks.push(t); }
1165    pub fn total_duration(&self) -> f32 { self.tracks.iter().map(|t| t.duration()).fold(0.0_f32, f32::max) }
1166    pub fn active_clips_at(&self, t: f32) -> Vec<(&str, &TimelineClip)> {
1167        self.tracks.iter().flat_map(|tr| tr.clips_at(t).into_iter().map(|c| (tr.name.as_str(), c))).collect()
1168    }
1169    pub fn snap_to_frame(&self, t: f32) -> f32 { (t * self.fps).round() / self.fps }
1170    pub fn advance(&mut self, dt: f32) { if self.is_playing { self.current_time += dt; } }
1171}
1172
1173// ============================================================
1174// SECTION: Camera Shake
1175// ============================================================
1176
1177#[derive(Debug, Clone)]
1178pub struct ShakePreset {
1179    pub name: String,
1180    pub trauma: f32,
1181    pub frequency: f32,
1182    pub max_angle_deg: f32,
1183    pub max_offset: f32,
1184    pub decay_rate: f32,
1185}
1186
1187impl ShakePreset {
1188    pub fn explosion() -> Self { ShakePreset { name: "explosion".into(), trauma: 1.0, frequency: 12.0, max_angle_deg: 5.0, max_offset: 0.3, decay_rate: 1.5 } }
1189    pub fn gunshot() -> Self { ShakePreset { name: "gunshot".into(), trauma: 0.4, frequency: 20.0, max_angle_deg: 1.5, max_offset: 0.08, decay_rate: 3.0 } }
1190    pub fn earthquake() -> Self { ShakePreset { name: "earthquake".into(), trauma: 0.8, frequency: 5.0, max_angle_deg: 3.0, max_offset: 0.5, decay_rate: 0.3 } }
1191    pub fn footstep_heavy() -> Self { ShakePreset { name: "footstep_heavy".into(), trauma: 0.15, frequency: 8.0, max_angle_deg: 0.5, max_offset: 0.02, decay_rate: 5.0 } }
1192
1193    pub fn evaluate(&self, trauma: f32, time: f32) -> (Vec3, Vec3) {
1194        let t2 = trauma * trauma;
1195        let noise_x = ((time * self.frequency).sin() * 0.5 + (time * self.frequency * 1.7).sin() * 0.3 + (time * self.frequency * 0.3).sin() * 0.2) * t2;
1196        let noise_y = ((time * self.frequency * 1.3).sin() * 0.5 + (time * self.frequency * 0.7).sin() * 0.3) * t2;
1197        let offset = Vec3::new(noise_x * self.max_offset, noise_y * self.max_offset, 0.0);
1198        let angle = Vec3::new(noise_y * self.max_angle_deg, noise_x * self.max_angle_deg, 0.0);
1199        (offset, angle)
1200    }
1201}
1202
1203// ============================================================
1204// SECTION: Integration Tests
1205// ============================================================
1206
1207pub fn run_cutscene_pipeline_tests() {
1208    // AnimationClipMeta
1209    let clip = AnimationClipMeta::new("walk", 2.0, 30.0);
1210    assert_eq!(clip.frame_count(), 60);
1211    assert!((clip.sample_time(2.5) - 0.5).abs() < 0.01); // Loop wraps
1212    assert!(!clip.is_finished(1.0));
1213
1214    // BlendTree
1215    let node = BlendTreeNode::Blend1D {
1216        parameter: "speed".into(),
1217        children: vec![
1218            (0.0, BlendTreeNode::Clip { name: "idle".into(), weight: 1.0 }),
1219            (1.0, BlendTreeNode::Clip { name: "walk".into(), weight: 1.0 }),
1220        ],
1221    };
1222    let result = node.evaluate_1d(0.5);
1223    assert_eq!(result.len(), 2);
1224
1225    // IK
1226    let mut ik = IkChain::new(vec!["upper".into(), "lower".into()], vec![1.0, 1.0]);
1227    ik.positions = vec![Vec3::ZERO, Vec3::new(1.0, 0.0, 0.0), Vec3::new(2.0, 0.0, 0.0)];
1228    ik.target = Vec3::new(1.5, 0.5, 0.0);
1229    ik.solve_fabrik(10, 0.001);
1230    assert!((ik.positions.last().unwrap().distance(ik.target)) < 0.01);
1231
1232    // Facial rig
1233    let mut rig = FacialRig::new();
1234    assert_eq!(rig.shapes.len(), 52);
1235    rig.apply_expression("happy");
1236    assert!(rig.get("mouthSmileLeft") > 0.0);
1237    assert!(rig.active_shape_count() > 0);
1238    rig.apply_expression("angry");
1239    assert!(rig.get("browDownLeft") > 0.0);
1240    rig.reset();
1241    assert_eq!(rig.active_shape_count(), 0);
1242}
1243
1244pub fn run_timeline_editor_tests() {
1245    let mut timeline = MasterTimeline::new(30.0);
1246    let mut track = TimelineTrack::new("animation");
1247    track.add_clip(TimelineClip::new(1, "idle", 0.0, 3.0));
1248    track.add_clip(TimelineClip::new(2, "walk", 3.5, 5.0));
1249    timeline.add_track(track);
1250    assert!(timeline.total_duration() > 0.0);
1251    let active = timeline.active_clips_at(1.5);
1252    assert!(!active.is_empty());
1253    assert_eq!(timeline.snap_to_frame(1.533_f32), (1.533_f32 * 30.0_f32).round() / 30.0_f32);
1254    timeline.is_playing = true;
1255    timeline.advance(0.1);
1256    assert!((timeline.current_time - 0.1).abs() < 0.001);
1257
1258    // TimelineClip blend
1259    let c = TimelineClip { id: 1, name: "t".into(), start_time: 0.0, duration: 2.0, blend_in: 0.5, blend_out: 0.5, speed: 1.0 };
1260    assert!((c.blend_weight(0.25) - 0.5).abs() < 0.001);
1261    assert!((c.blend_weight(1.75) - 0.5).abs() < 0.001);
1262    assert!((c.blend_weight(1.0) - 1.0).abs() < 0.001);
1263}
1264
1265pub fn run_final_cutscene_tests() {
1266    // ASM test
1267    let mut asm = AnimatorStateMachine::new("idle");
1268    asm.add_state(AnimationState::new("idle", "idle_anim"));
1269    asm.add_state(AnimationState::new("run", "run_anim"));
1270    asm.add_state(AnimationState::new("attack", "attack_anim"));
1271    let t1 = Transition { from: "idle".into(), to: "run".into(), conditions: vec![TransitionCondition::FloatGreater("speed".into(), 0.5)], duration: 0.2, exit_time: None, can_interrupt: true };
1272    let t2 = Transition { from: "run".into(), to: "attack".into(), conditions: vec![TransitionCondition::Trigger("attack".into())], duration: 0.1, exit_time: None, can_interrupt: false };
1273    asm.add_transition(t1); asm.add_transition(t2);
1274    asm.set_float("speed", 1.0);
1275    let s1 = asm.update(0.016);
1276    assert_eq!(s1, Some("run"));
1277    asm.set_trigger("attack");
1278    let s2 = asm.update(0.016);
1279    assert_eq!(s2, Some("attack"));
1280}
1281
1282pub fn run_cutscene_analytics_tests() {
1283    // MocapClip
1284    let mut mocap = MocapClip::new("take_1", 60.0);
1285    for i in 0..60u32 {
1286        let mut frame = MocapFrame::new(i as f32 / 60.0);
1287        frame.set_bone("Hips", Vec3::new(0.0, 1.0, i as f32 * 0.01), Quat::IDENTITY);
1288        mocap.add_frame(frame);
1289    }
1290    assert_eq!(mocap.frame_count(), 60);
1291    let downsampled = mocap.downsample(30.0);
1292    assert!(downsampled.frame_count() < mocap.frame_count());
1293
1294    // Retargeting
1295    let mut mapping = HashMap::new();
1296    mapping.insert("Hips".to_string(), "Root".to_string());
1297    let retargeted = mocap.retarget_to_skeleton(&mapping);
1298    assert!(!retargeted.frames.is_empty());
1299    if let Some(frame) = retargeted.frames.first() {
1300        assert!(frame.bone_transforms.contains_key("Root"));
1301    }
1302
1303    // ShakePreset
1304    let shake = ShakePreset::explosion();
1305    let (offset, angle) = shake.evaluate(1.0, 0.5);
1306    assert!(offset.length() >= 0.0);
1307    assert!(angle.length() >= 0.0);
1308    let (offset2, _) = ShakePreset::earthquake().evaluate(0.5, 1.0);
1309    assert!(offset2.length() >= 0.0);
1310}
1311
1312pub fn run_scene_and_postprocess_tests() {
1313    // Light track
1314    let mut track = LightTrack::new("sun", LightType::Directional);
1315    track.add_keyframe(LightKeyframe { time: 0.0, position: Vec3::ZERO, rotation: Quat::IDENTITY, color: Vec3::new(1.0, 0.9, 0.7), intensity: 1.0, range: 100.0, spot_angle: 0.0, cast_shadows: true });
1316    track.add_keyframe(LightKeyframe { time: 5.0, position: Vec3::ZERO, rotation: Quat::IDENTITY, color: Vec3::new(1.0, 0.4, 0.2), intensity: 0.5, range: 80.0, spot_angle: 0.0, cast_shadows: true });
1317    let mid = track.sample(2.5).unwrap();
1318    assert!((mid.intensity - 0.75).abs() < 0.01);
1319    assert_eq!(track.duration(), 5.0);
1320    assert_eq!(track.keyframe_count(), 2);
1321}
1322
1323pub const ANIM_CLIP_MAGIC: u32 = 0x414E4D43;
1324pub const CUTSCENE_FORMAT_VERSION: u32 = 3;
1325pub const MAX_BONE_COUNT: usize = 512;
1326pub const DEFAULT_FPS: f32 = 30.0;
1327pub const CUTSCENE_MODULE_VER: &str = "3.0.0";
1328
1329pub fn cutscene_importer_module_info() -> HashMap<String, String> {
1330    let mut info = HashMap::new();
1331    info.insert("module".into(), "cutscene_importer".into());
1332    info.insert("version".into(), CUTSCENE_MODULE_VER.into());
1333    info.insert("fps".into(), format!("{}", DEFAULT_FPS));
1334    info.insert("max_bones".into(), format!("{}", MAX_BONE_COUNT));
1335    info.insert("arkit_shapes".into(), "52".into());
1336    info
1337}
1338
1339// ============================================================
1340// SECTION: Subtitle System
1341// ============================================================
1342
1343#[derive(Debug, Clone)]
1344pub struct SubtitleEntry {
1345    pub id: u32,
1346    pub start_time_s: f32,
1347    pub end_time_s: f32,
1348    pub text: String,
1349    pub speaker: Option<String>,
1350    pub language: String,
1351}
1352
1353impl SubtitleEntry {
1354    pub fn new(id: u32, start: f32, end: f32, text: &str) -> Self {
1355        SubtitleEntry { id, start_time_s: start, end_time_s: end, text: text.to_string(), speaker: None, language: "en".to_string() }
1356    }
1357    pub fn duration_s(&self) -> f32 { self.end_time_s - self.start_time_s }
1358    pub fn is_active_at(&self, t: f32) -> bool { t >= self.start_time_s && t < self.end_time_s }
1359    pub fn word_count(&self) -> usize { self.text.split_whitespace().count() }
1360    pub fn reading_speed_wpm(&self) -> f32 { self.word_count() as f32 / self.duration_s() * 60.0 }
1361    pub fn is_well_paced(&self) -> bool { let wpm = self.reading_speed_wpm(); wpm >= 80.0 && wpm <= 300.0 }
1362}
1363
1364#[derive(Debug, Clone)]
1365pub struct SubtitleTrack {
1366    pub language: String,
1367    pub entries: Vec<SubtitleEntry>,
1368    pub is_rtl: bool,
1369}
1370
1371impl SubtitleTrack {
1372    pub fn new(language: &str) -> Self {
1373        let is_rtl = matches!(language, "ar" | "he" | "fa" | "ur");
1374        SubtitleTrack { language: language.to_string(), entries: Vec::new(), is_rtl }
1375    }
1376    pub fn add_entry(&mut self, e: SubtitleEntry) { self.entries.push(e); self.entries.sort_by(|a,b| a.start_time_s.partial_cmp(&b.start_time_s).unwrap()); }
1377    pub fn active_at(&self, t: f32) -> Option<&SubtitleEntry> { self.entries.iter().find(|e| e.is_active_at(t)) }
1378    pub fn entry_count(&self) -> usize { self.entries.len() }
1379    pub fn total_duration(&self) -> f32 { self.entries.last().map(|e| e.end_time_s).unwrap_or(0.0) }
1380
1381    pub fn export_srt(&self) -> String {
1382        let mut out = String::new();
1383        for (i, entry) in self.entries.iter().enumerate() {
1384            out.push_str(&format!("{}\n", i + 1));
1385            out.push_str(&format!("{} --> {}\n", srt_time(entry.start_time_s), srt_time(entry.end_time_s)));
1386            if let Some(sp) = &entry.speaker { out.push_str(&format!("[{}] ", sp)); }
1387            out.push_str(&format!("{}\n\n", entry.text));
1388        }
1389        out
1390    }
1391
1392    pub fn pacing_issues(&self) -> Vec<u32> { self.entries.iter().filter(|e| !e.is_well_paced()).map(|e| e.id).collect() }
1393}
1394
1395fn srt_time(t: f32) -> String {
1396    let ms = ((t % 1.0) * 1000.0) as u32;
1397    let s = t as u32 % 60;
1398    let m = t as u32 / 60 % 60;
1399    let h = t as u32 / 3600;
1400    format!("{:02}:{:02}:{:02},{:03}", h, m, s, ms)
1401}
1402
1403pub fn parse_srt(input: &str) -> SubtitleTrack {
1404    let mut track = SubtitleTrack::new("en");
1405    let mut id = 1u32;
1406    let lines: Vec<&str> = input.lines().collect();
1407    let mut i = 0;
1408    while i < lines.len() {
1409        let line = lines[i].trim();
1410        if line.parse::<u32>().is_ok() {
1411            i += 1;
1412            if i < lines.len() {
1413                let time_line = lines[i].trim();
1414                let parts: Vec<&str> = time_line.split("-->").collect();
1415                if parts.len() == 2 {
1416                    let start = parse_srt_time(parts[0].trim()).unwrap_or(0.0);
1417                    let end = parse_srt_time(parts[1].trim()).unwrap_or(0.0);
1418                    i += 1;
1419                    let mut text_lines = Vec::new();
1420                    while i < lines.len() && !lines[i].trim().is_empty() {
1421                        text_lines.push(lines[i]); i += 1;
1422                    }
1423                    let text = text_lines.join("\n");
1424                    track.add_entry(SubtitleEntry::new(id, start, end, &text));
1425                    id += 1;
1426                }
1427            }
1428        }
1429        i += 1;
1430    }
1431    track
1432}
1433
1434fn parse_srt_time(s: &str) -> Option<f32> {
1435    let s = s.replace(",", ".");
1436    let parts: Vec<&str> = s.split(':').collect();
1437    if parts.len() == 3 {
1438        let h: f32 = parts[0].parse().ok()?;
1439        let m: f32 = parts[1].parse().ok()?;
1440        let sec: f32 = parts[2].parse().ok()?;
1441        Some(h * 3600.0 + m * 60.0 + sec)
1442    } else { None }
1443}
1444
1445// ============================================================
1446// SECTION: Storyboard System
1447// ============================================================
1448
1449#[derive(Debug, Clone, PartialEq)]
1450pub enum StoryboardStatus { Concept, Sketch, Final, Approved }
1451
1452#[derive(Debug, Clone)]
1453pub struct StoryboardPanel {
1454    pub panel_id: u32,
1455    pub shot_number: String,
1456    pub description: String,
1457    pub duration_s: f32,
1458    pub camera_angle: String,
1459    pub action_notes: String,
1460    pub dialogue: Option<String>,
1461    pub status: StoryboardStatus,
1462}
1463
1464impl StoryboardPanel {
1465    pub fn new(id: u32, shot: &str, desc: &str, duration: f32) -> Self {
1466        StoryboardPanel { panel_id: id, shot_number: shot.to_string(), description: desc.to_string(),
1467            duration_s: duration, camera_angle: "medium".to_string(), action_notes: String::new(),
1468            dialogue: None, status: StoryboardStatus::Concept }
1469    }
1470    pub fn approve(&mut self) { self.status = StoryboardStatus::Approved; }
1471    pub fn is_approved(&self) -> bool { self.status == StoryboardStatus::Approved }
1472}
1473
1474#[derive(Debug, Clone)]
1475pub struct Storyboard {
1476    pub title: String,
1477    pub panels: Vec<StoryboardPanel>,
1478    pub director: String,
1479}
1480
1481impl Storyboard {
1482    pub fn new(title: &str) -> Self { Storyboard { title: title.to_string(), panels: Vec::new(), director: String::new() } }
1483    pub fn add_panel(&mut self, p: StoryboardPanel) { self.panels.push(p); }
1484    pub fn panel_count(&self) -> usize { self.panels.len() }
1485    pub fn total_duration_s(&self) -> f32 { self.panels.iter().map(|p| p.duration_s).sum() }
1486    pub fn completion_percent(&self) -> f32 { self.panels.iter().filter(|p| p.is_approved()).count() as f32 / self.panels.len().max(1) as f32 * 100.0 }
1487    pub fn approved_count(&self) -> usize { self.panels.iter().filter(|p| p.is_approved()).count() }
1488
1489    pub fn export_pdf_script(&self) -> String {
1490        let mut out = format!("STORYBOARD: {}\nDirector: {}\nTotal Duration: {:.1}s\n\n", self.title, self.director, self.total_duration_s());
1491        for panel in &self.panels {
1492            out.push_str(&format!("SHOT {}: [{:?}] {:.1}s\n  {}\n", panel.shot_number, panel.status, panel.duration_s, panel.description));
1493            if let Some(d) = &panel.dialogue { out.push_str(&format!("  Dialogue: \"{}\"\n", d)); }
1494        }
1495        out
1496    }
1497}
1498
1499// ============================================================
1500// SECTION: Performance Capture
1501// ============================================================
1502
1503#[derive(Debug, Clone)]
1504pub struct PerformanceTake {
1505    pub take_id: u32,
1506    pub clip_name: String,
1507    pub duration_s: f32,
1508    pub quality_score: f32, // 0-100
1509    pub notes: String,
1510    pub is_approved: bool,
1511    pub timestamp: String,
1512}
1513
1514impl PerformanceTake {
1515    pub fn new(id: u32, clip: &str, duration: f32) -> Self {
1516        PerformanceTake { take_id: id, clip_name: clip.to_string(), duration_s: duration,
1517            quality_score: 0.0, notes: String::new(), is_approved: false, timestamp: "2024-01-01T00:00:00Z".to_string() }
1518    }
1519    pub fn approve(&mut self) { self.is_approved = true; }
1520    pub fn set_quality(&mut self, score: f32) { self.quality_score = score.clamp(0.0, 100.0); }
1521}
1522
1523#[derive(Debug, Clone)]
1524pub struct PerformanceCaptureSession {
1525    pub session_name: String,
1526    pub takes: Vec<PerformanceTake>,
1527    pub actor_name: String,
1528}
1529
1530impl PerformanceCaptureSession {
1531    pub fn new(name: &str, actor: &str) -> Self { PerformanceCaptureSession { session_name: name.to_string(), takes: Vec::new(), actor_name: actor.to_string() } }
1532    pub fn add_take(&mut self, take: PerformanceTake) { self.takes.push(take); }
1533    pub fn approved_takes(&self) -> Vec<&PerformanceTake> { self.takes.iter().filter(|t| t.is_approved).collect() }
1534    pub fn select_best_take(&self) -> Option<&PerformanceTake> {
1535        self.takes.iter().max_by(|a, b| a.quality_score.partial_cmp(&b.quality_score).unwrap())
1536    }
1537    pub fn take_count(&self) -> usize { self.takes.len() }
1538    pub fn avg_quality(&self) -> f32 { if self.takes.is_empty() { return 0.0; } self.takes.iter().map(|t| t.quality_score).sum::<f32>() / self.takes.len() as f32 }
1539}
1540
1541// ============================================================
1542// SECTION: Color Grading
1543// ============================================================
1544
1545#[derive(Debug, Clone)]
1546pub struct ColorGradeKeyframe {
1547    pub time_s: f32,
1548    pub lift: Vec3,
1549    pub gamma: Vec3,
1550    pub gain: Vec3,
1551    pub saturation: f32,
1552    pub contrast: f32,
1553    pub exposure: f32,
1554}
1555
1556impl ColorGradeKeyframe {
1557    pub fn neutral() -> Self {
1558        ColorGradeKeyframe { time_s: 0.0, lift: Vec3::ZERO, gamma: Vec3::ONE, gain: Vec3::ONE, saturation: 1.0, contrast: 1.0, exposure: 0.0 }
1559    }
1560    pub fn horror() -> Self {
1561        ColorGradeKeyframe { time_s: 0.0, lift: Vec3::new(-0.02, -0.02, 0.05), gamma: Vec3::new(0.9, 0.9, 1.1), gain: Vec3::new(0.8, 0.8, 1.2), saturation: 0.6, contrast: 1.3, exposure: -0.5 }
1562    }
1563    pub fn golden_hour() -> Self {
1564        ColorGradeKeyframe { time_s: 0.0, lift: Vec3::new(0.05, 0.02, -0.02), gamma: Vec3::new(1.1, 1.0, 0.9), gain: Vec3::new(1.3, 1.1, 0.7), saturation: 1.3, contrast: 1.1, exposure: 0.3 }
1565    }
1566    pub fn lerp(&self, other: &ColorGradeKeyframe, t: f32) -> ColorGradeKeyframe {
1567        ColorGradeKeyframe {
1568            time_s: self.time_s + t * (other.time_s - self.time_s),
1569            lift: self.lift.lerp(other.lift, t), gamma: self.gamma.lerp(other.gamma, t),
1570            gain: self.gain.lerp(other.gain, t), saturation: self.saturation + t * (other.saturation - self.saturation),
1571            contrast: self.contrast + t * (other.contrast - self.contrast), exposure: self.exposure + t * (other.exposure - self.exposure),
1572        }
1573    }
1574}
1575
1576#[derive(Debug, Clone)]
1577pub struct ColorGradingTrack {
1578    pub keyframes: Vec<ColorGradeKeyframe>,
1579}
1580
1581impl ColorGradingTrack {
1582    pub fn new() -> Self { ColorGradingTrack { keyframes: Vec::new() } }
1583    pub fn add_kf(&mut self, kf: ColorGradeKeyframe) { self.keyframes.push(kf); self.keyframes.sort_by(|a,b| a.time_s.partial_cmp(&b.time_s).unwrap()); }
1584    pub fn sample(&self, t: f32) -> ColorGradeKeyframe {
1585        if self.keyframes.is_empty() { return ColorGradeKeyframe::neutral(); }
1586        if t <= self.keyframes[0].time_s { return self.keyframes[0].clone(); }
1587        let last = self.keyframes.last().unwrap();
1588        if t >= last.time_s { return last.clone(); }
1589        for i in 0..self.keyframes.len()-1 {
1590            let a = &self.keyframes[i]; let b = &self.keyframes[i+1];
1591            if t >= a.time_s && t <= b.time_s { return a.lerp(b, (t - a.time_s)/(b.time_s - a.time_s)); }
1592        }
1593        ColorGradeKeyframe::neutral()
1594    }
1595}
1596
1597// ============================================================
1598// SECTION: Directed Graph (Animation Graph)
1599// ============================================================
1600
1601#[derive(Debug, Clone)]
1602pub struct GraphNode {
1603    pub node_id: u32,
1604    pub name: String,
1605    pub node_type: String,
1606    pub data: HashMap<String, String>,
1607}
1608
1609impl GraphNode {
1610    pub fn new(id: u32, name: &str, node_type: &str) -> Self {
1611        GraphNode { node_id: id, name: name.to_string(), node_type: node_type.to_string(), data: HashMap::new() }
1612    }
1613    pub fn clip_node(id: u32, clip_name: &str) -> Self { let mut n = Self::new(id, clip_name, "clip"); n.data.insert("clip".into(), clip_name.into()); n }
1614    pub fn blend_node(id: u32) -> Self { Self::new(id, &format!("blend_{}", id), "blend") }
1615    pub fn additive_node(id: u32) -> Self { Self::new(id, &format!("additive_{}", id), "additive") }
1616}
1617
1618#[derive(Debug, Clone)]
1619pub struct GraphEdge {
1620    pub from: u32, pub to: u32, pub weight: f32,
1621}
1622
1623#[derive(Debug, Clone)]
1624pub struct AnimationGraph {
1625    pub nodes: Vec<GraphNode>,
1626    pub edges: Vec<GraphEdge>,
1627}
1628
1629impl AnimationGraph {
1630    pub fn new() -> Self { AnimationGraph { nodes: Vec::new(), edges: Vec::new() } }
1631    pub fn add_node(&mut self, n: GraphNode) { self.nodes.push(n); }
1632    pub fn add_edge(&mut self, from: u32, to: u32, weight: f32) { self.edges.push(GraphEdge { from, to, weight }); }
1633
1634    pub fn topological_sort(&self) -> Option<Vec<u32>> {
1635        let node_ids: Vec<u32> = self.nodes.iter().map(|n| n.node_id).collect();
1636        let mut in_degree: HashMap<u32, usize> = node_ids.iter().map(|&id| (id, 0)).collect();
1637        for e in &self.edges { *in_degree.entry(e.to).or_insert(0) += 1; }
1638        let mut queue: VecDeque<u32> = in_degree.iter().filter(|(_, &d)| d == 0).map(|(&id, _)| id).collect();
1639        let mut result = Vec::new();
1640        while let Some(n) = queue.pop_front() {
1641            result.push(n);
1642            for e in self.edges.iter().filter(|e| e.from == n) {
1643                let d = in_degree.entry(e.to).or_insert(0);
1644                *d = d.saturating_sub(1);
1645                if *d == 0 { queue.push_back(e.to); }
1646            }
1647        }
1648        if result.len() == self.nodes.len() { Some(result) } else { None }
1649    }
1650
1651    pub fn has_cycle(&self) -> bool { self.topological_sort().is_none() }
1652    pub fn node_count(&self) -> usize { self.nodes.len() }
1653    pub fn edge_count(&self) -> usize { self.edges.len() }
1654}
1655
1656// ============================================================
1657// SECTION: Camera Rig System
1658// ============================================================
1659
1660#[derive(Debug, Clone, PartialEq)]
1661pub enum CameraRigType { Free, Dolly, Crane, Steadicam, Orbit, Handheld, VR, Drone, POV, MotionControlRig }
1662
1663#[derive(Debug, Clone)]
1664pub struct CameraRigState {
1665    pub rig_type: CameraRigType,
1666    pub position: Vec3,
1667    pub rotation: Quat,
1668    pub fov_deg: f32,
1669    pub focal_length_mm: f32,
1670    pub focus_distance_m: f32,
1671    pub aperture_f: f32,
1672    pub near_clip: f32,
1673    pub far_clip: f32,
1674    pub sensor_width_mm: f32,
1675}
1676
1677impl CameraRigState {
1678    pub fn new(rig_type: CameraRigType) -> Self {
1679        CameraRigState { rig_type, position: Vec3::ZERO, rotation: Quat::IDENTITY,
1680            fov_deg: 60.0, focal_length_mm: 35.0, focus_distance_m: 5.0,
1681            aperture_f: 2.8, near_clip: 0.1, far_clip: 1000.0, sensor_width_mm: 36.0 }
1682    }
1683
1684    pub fn exposure_value(&self) -> f32 {
1685        // EV = log2(N^2 / t) approximation; simplified
1686        let aperture = self.aperture_f;
1687        (aperture * aperture).log2()
1688    }
1689
1690    pub fn hyperfocal_m(&self) -> f32 {
1691        let f = self.focal_length_mm / 1000.0;
1692        let c = 0.0291 / 1000.0;
1693        f * f / (self.aperture_f * c)
1694    }
1695
1696    pub fn apply_shake(&mut self, shake_offset: Vec3, shake_rotation: Vec3) {
1697        self.position += shake_offset;
1698        let q = Quat::from_euler(glam::EulerRot::XYZ, shake_rotation.x.to_radians(), shake_rotation.y.to_radians(), shake_rotation.z.to_radians());
1699        self.rotation = self.rotation * q;
1700    }
1701
1702    pub fn projection_matrix(&self, aspect: f32) -> Mat4 {
1703        Mat4::perspective_rh_gl(self.fov_deg.to_radians(), aspect, self.near_clip, self.far_clip)
1704    }
1705
1706    pub fn view_matrix(&self) -> Mat4 {
1707        Mat4::from_rotation_translation(self.rotation, self.position).inverse()
1708    }
1709}
1710
1711// ============================================================
1712// SECTION: More Tests
1713// ============================================================
1714
1715pub fn run_subtitle_tests() {
1716    let srt_data = "1\n00:00:00,000 --> 00:00:02,000\nHello, world!\n\n2\n00:00:03,000 --> 00:00:06,000\nThis is a test.\n\n";
1717    let track = parse_srt(srt_data);
1718    assert_eq!(track.entry_count(), 2);
1719    assert!(track.active_at(1.0).is_some());
1720    assert!(track.active_at(4.0).is_some());
1721    assert!(track.active_at(10.0).is_none());
1722    let srt_out = track.export_srt();
1723    assert!(srt_out.contains("-->"));
1724    assert!(!track.is_rtl);
1725
1726    let rtl_track = SubtitleTrack::new("ar");
1727    assert!(rtl_track.is_rtl);
1728}
1729
1730pub fn run_storyboard_tests() {
1731    let mut board = Storyboard::new("Act 1");
1732    board.add_panel(StoryboardPanel::new(1, "1A", "Hero enters village", 3.0));
1733    board.add_panel(StoryboardPanel::new(2, "1B", "Wide shot of village", 2.5));
1734    let mut panel3 = StoryboardPanel::new(3, "1C", "Villain appears", 4.0);
1735    panel3.approve();
1736    board.add_panel(panel3);
1737    assert_eq!(board.panel_count(), 3);
1738    assert_eq!(board.approved_count(), 1);
1739    assert!((board.completion_percent() - 33.33).abs() < 0.5);
1740    assert!((board.total_duration_s() - 9.5).abs() < 0.001);
1741    let script = board.export_pdf_script();
1742    assert!(script.contains("Act 1"));
1743}
1744
1745pub fn run_performance_capture_tests() {
1746    let mut session = PerformanceCaptureSession::new("Walk Cycle Session", "John Actor");
1747    let mut t1 = PerformanceTake::new(1, "walk_take_1", 5.0); t1.set_quality(72.0);
1748    let mut t2 = PerformanceTake::new(2, "walk_take_2", 5.2); t2.set_quality(88.0); t2.approve();
1749    let mut t3 = PerformanceTake::new(3, "walk_take_3", 4.8); t3.set_quality(65.0);
1750    session.add_take(t1); session.add_take(t2); session.add_take(t3);
1751    assert_eq!(session.take_count(), 3);
1752    assert_eq!(session.approved_takes().len(), 1);
1753    let best = session.select_best_take().unwrap();
1754    assert_eq!(best.take_id, 2);
1755    assert!(session.avg_quality() > 0.0);
1756}
1757
1758pub fn run_color_grading_tests() {
1759    let neutral = ColorGradeKeyframe::neutral();
1760    let horror = ColorGradeKeyframe::horror();
1761    let lerped = neutral.lerp(&horror, 0.5);
1762    assert!(lerped.saturation < 1.0);
1763    let golden = ColorGradeKeyframe::golden_hour();
1764    assert!(golden.saturation > 1.0);
1765
1766    let mut track = ColorGradingTrack::new();
1767    track.add_kf(ColorGradeKeyframe::neutral());
1768    let mut mid = ColorGradeKeyframe::horror(); mid.time_s = 5.0;
1769    track.add_kf(mid);
1770    let sampled = track.sample(2.5);
1771    assert!(sampled.saturation < 1.0 && sampled.saturation > 0.5);
1772}
1773
1774pub fn run_animation_graph_tests() {
1775    let mut graph = AnimationGraph::new();
1776    graph.add_node(GraphNode::clip_node(1, "idle"));
1777    graph.add_node(GraphNode::clip_node(2, "walk"));
1778    graph.add_node(GraphNode::blend_node(3));
1779    graph.add_edge(1, 3, 0.5);
1780    graph.add_edge(2, 3, 0.5);
1781    assert!(!graph.has_cycle());
1782    let sorted = graph.topological_sort().unwrap();
1783    assert_eq!(sorted.len(), 3);
1784    // Node 3 should come last
1785    assert_eq!(*sorted.last().unwrap(), 3);
1786    // Add cycle
1787    graph.add_edge(3, 1, 1.0);
1788    assert!(graph.has_cycle());
1789}
1790
1791pub fn run_camera_rig_tests() {
1792    let mut cam = CameraRigState::new(CameraRigType::Steadicam);
1793    assert!(cam.exposure_value() > 0.0);
1794    assert!(cam.hyperfocal_m() > 0.0);
1795    cam.apply_shake(Vec3::new(0.01, 0.02, 0.0), Vec3::new(0.1, 0.0, 0.0));
1796    assert!(cam.position.length() > 0.0);
1797    let view = cam.view_matrix();
1798    let _proj = cam.projection_matrix(16.0 / 9.0);
1799    assert!(view.col(3).truncate().length() >= 0.0);
1800}
1801
1802pub fn run_all_cutscene_tests() {
1803    run_cutscene_pipeline_tests();
1804    run_timeline_editor_tests();
1805    run_final_cutscene_tests();
1806    run_cutscene_analytics_tests();
1807    run_scene_and_postprocess_tests();
1808    run_subtitle_tests();
1809    run_storyboard_tests();
1810    run_performance_capture_tests();
1811    run_color_grading_tests();
1812    run_animation_graph_tests();
1813    run_camera_rig_tests();
1814}
1815
1816
1817// ============================================================
1818// POST-PROCESS EFFECTS PIPELINE
1819// ============================================================
1820
1821#[derive(Debug, Clone, PartialEq)]
1822pub enum PostProcessEffectType {
1823    Bloom, DepthOfField, MotionBlur, AmbientOcclusion, Vignette,
1824    ChromaticAberration, FilmGrain, ColorGrading, ToneMapping,
1825    Sharpen, LensFlare, GodRays, ScreenSpaceReflections,
1826}
1827
1828#[derive(Debug, Clone)]
1829pub struct PostProcessEffect {
1830    pub effect_type: PostProcessEffectType,
1831    pub enabled: bool,
1832    pub intensity: f32,
1833    pub params: HashMap<String, f32>,
1834}
1835
1836impl PostProcessEffect {
1837    pub fn new(effect_type: PostProcessEffectType, intensity: f32) -> Self {
1838        Self { effect_type, enabled: true, intensity, params: HashMap::new() }
1839    }
1840    pub fn bloom(threshold: f32, scatter: f32, intensity: f32) -> Self {
1841        let mut e = Self::new(PostProcessEffectType::Bloom, intensity);
1842        e.params.insert("threshold".to_string(), threshold);
1843        e.params.insert("scatter".to_string(), scatter);
1844        e
1845    }
1846    pub fn depth_of_field(focal_length_mm: f32, aperture: f32, focus_distance_m: f32) -> Self {
1847        let mut e = Self::new(PostProcessEffectType::DepthOfField, 1.0);
1848        e.params.insert("focal_length_mm".to_string(), focal_length_mm);
1849        e.params.insert("aperture".to_string(), aperture);
1850        e.params.insert("focus_distance_m".to_string(), focus_distance_m);
1851        e
1852    }
1853    pub fn motion_blur(shutter_angle: f32, sample_count: f32) -> Self {
1854        let mut e = Self::new(PostProcessEffectType::MotionBlur, 1.0);
1855        e.params.insert("shutter_angle".to_string(), shutter_angle);
1856        e.params.insert("sample_count".to_string(), sample_count);
1857        e
1858    }
1859    pub fn vignette(intensity: f32, smoothness: f32) -> Self {
1860        let mut e = Self::new(PostProcessEffectType::Vignette, intensity);
1861        e.params.insert("smoothness".to_string(), smoothness);
1862        e
1863    }
1864    pub fn film_grain(intensity: f32, response: f32) -> Self {
1865        let mut e = Self::new(PostProcessEffectType::FilmGrain, intensity);
1866        e.params.insert("response".to_string(), response);
1867        e
1868    }
1869    pub fn effect_type_str(&self) -> &'static str {
1870        match &self.effect_type {
1871            PostProcessEffectType::Bloom => "Bloom",
1872            PostProcessEffectType::DepthOfField => "DepthOfField",
1873            PostProcessEffectType::MotionBlur => "MotionBlur",
1874            PostProcessEffectType::AmbientOcclusion => "AmbientOcclusion",
1875            PostProcessEffectType::Vignette => "Vignette",
1876            PostProcessEffectType::ChromaticAberration => "ChromaticAberration",
1877            PostProcessEffectType::FilmGrain => "FilmGrain",
1878            PostProcessEffectType::ColorGrading => "ColorGrading",
1879            PostProcessEffectType::ToneMapping => "ToneMapping",
1880            PostProcessEffectType::Sharpen => "Sharpen",
1881            PostProcessEffectType::LensFlare => "LensFlare",
1882            PostProcessEffectType::GodRays => "GodRays",
1883            PostProcessEffectType::ScreenSpaceReflections => "SSR",
1884        }
1885    }
1886}
1887
1888#[derive(Debug, Clone, Default)]
1889pub struct PostProcessStack {
1890    pub effects: Vec<PostProcessEffect>,
1891    pub exposure: f32,
1892    pub contrast: f32,
1893    pub saturation: f32,
1894    pub temperature: f32,  // color temperature in Kelvin (relative offset)
1895}
1896
1897impl PostProcessStack {
1898    pub fn new() -> Self {
1899        Self { effects: Vec::new(), exposure: 1.0, contrast: 1.0, saturation: 1.0, temperature: 0.0 }
1900    }
1901    pub fn add_effect(&mut self, e: PostProcessEffect) { self.effects.push(e); }
1902    pub fn enabled_effects(&self) -> Vec<&PostProcessEffect> {
1903        self.effects.iter().filter(|e| e.enabled).collect()
1904    }
1905    pub fn get_effect(&self, effect_type: &PostProcessEffectType) -> Option<&PostProcessEffect> {
1906        self.effects.iter().find(|e| &e.effect_type == effect_type)
1907    }
1908    pub fn get_effect_mut(&mut self, effect_type: &PostProcessEffectType) -> Option<&mut PostProcessEffect> {
1909        self.effects.iter_mut().find(|e| &e.effect_type == effect_type)
1910    }
1911    pub fn cinema_preset() -> Self {
1912        let mut stack = Self::new();
1913        stack.add_effect(PostProcessEffect::bloom(0.9, 0.7, 0.5));
1914        stack.add_effect(PostProcessEffect::depth_of_field(35.0, 2.8, 5.0));
1915        stack.add_effect(PostProcessEffect::vignette(0.4, 0.4));
1916        stack.add_effect(PostProcessEffect::film_grain(0.15, 0.8));
1917        stack.contrast = 1.15;
1918        stack.saturation = 0.9;
1919        stack.temperature = -200.0;
1920        stack
1921    }
1922    pub fn horror_preset() -> Self {
1923        let mut stack = Self::new();
1924        stack.add_effect(PostProcessEffect::vignette(0.8, 0.3));
1925        stack.add_effect(PostProcessEffect::film_grain(0.4, 0.9));
1926        stack.contrast = 1.4;
1927        stack.saturation = 0.3;
1928        stack.temperature = -500.0;
1929        stack.exposure = 0.8;
1930        stack
1931    }
1932    pub fn daylight_preset() -> Self {
1933        let mut stack = Self::new();
1934        stack.add_effect(PostProcessEffect::bloom(1.0, 0.5, 0.3));
1935        stack.contrast = 1.1;
1936        stack.saturation = 1.2;
1937        stack.temperature = 300.0;
1938        stack.exposure = 1.1;
1939        stack
1940    }
1941    pub fn effect_count(&self) -> usize { self.effects.len() }
1942}
1943
1944// ============================================================
1945// USD / ALEMBIC SCENE EXPORT
1946// ============================================================
1947
1948#[derive(Debug, Clone, PartialEq)]
1949pub enum UsdPrimType {
1950    Xform, Mesh, Camera, Light, Scope, Material, Shader, Points,
1951}
1952
1953#[derive(Debug, Clone)]
1954pub struct UsdPrim {
1955    pub path: String,
1956    pub prim_type: UsdPrimType,
1957    pub attributes: HashMap<String, String>,
1958    pub children: Vec<String>,
1959    pub active: bool,
1960    pub instanceable: bool,
1961}
1962
1963impl UsdPrim {
1964    pub fn new(path: &str, prim_type: UsdPrimType) -> Self {
1965        Self { path: path.to_string(), prim_type, attributes: HashMap::new(),
1966            children: Vec::new(), active: true, instanceable: false }
1967    }
1968    pub fn set_attr(&mut self, name: &str, value: &str) {
1969        self.attributes.insert(name.to_string(), value.to_string());
1970    }
1971    pub fn get_attr(&self, name: &str) -> Option<&String> {
1972        self.attributes.get(name)
1973    }
1974    pub fn prim_type_str(&self) -> &'static str {
1975        match &self.prim_type {
1976            UsdPrimType::Xform => "Xform", UsdPrimType::Mesh => "Mesh",
1977            UsdPrimType::Camera => "Camera", UsdPrimType::Light => "Light",
1978            UsdPrimType::Scope => "Scope", UsdPrimType::Material => "Material",
1979            UsdPrimType::Shader => "Shader", UsdPrimType::Points => "Points",
1980        }
1981    }
1982    pub fn sdf_declaration(&self) -> String {
1983        format!("def {} \"{}\"", self.prim_type_str(), self.path.split('/').last().unwrap_or("root"))
1984    }
1985}
1986
1987#[derive(Debug, Clone, Default)]
1988pub struct UsdStage {
1989    pub identifier: String,
1990    pub prims: Vec<UsdPrim>,
1991    pub default_prim: String,
1992    pub start_time_code: f64,
1993    pub end_time_code: f64,
1994    pub time_codes_per_second: f64,
1995    pub up_axis: String,
1996}
1997
1998impl UsdStage {
1999    pub fn new(identifier: &str) -> Self {
2000        Self { identifier: identifier.to_string(), prims: Vec::new(),
2001            default_prim: String::new(), start_time_code: 0.0,
2002            end_time_code: 240.0, time_codes_per_second: 24.0, up_axis: "Y".to_string() }
2003    }
2004    pub fn add_prim(&mut self, p: UsdPrim) { self.prims.push(p); }
2005    pub fn get_prim(&self, path: &str) -> Option<&UsdPrim> {
2006        self.prims.iter().find(|p| p.path == path)
2007    }
2008    pub fn prim_count(&self) -> usize { self.prims.len() }
2009    pub fn duration_seconds(&self) -> f64 {
2010        (self.end_time_code - self.start_time_code) / self.time_codes_per_second
2011    }
2012    pub fn export_usda_header(&self) -> String {
2013        format!(
2014            "#usda 1.0\n(\n    defaultPrim = \"{}\"\n    startTimeCode = {}\n    endTimeCode = {}\n    timeCodesPerSecond = {}\n    upAxis = \"{}\"\n)\n",
2015            self.default_prim, self.start_time_code, self.end_time_code,
2016            self.time_codes_per_second, self.up_axis
2017        )
2018    }
2019    pub fn prims_of_type(&self, prim_type: &UsdPrimType) -> Vec<&UsdPrim> {
2020        self.prims.iter().filter(|p| &p.prim_type == prim_type).collect()
2021    }
2022    pub fn camera_prims(&self) -> Vec<&UsdPrim> { self.prims_of_type(&UsdPrimType::Camera) }
2023    pub fn mesh_prims(&self) -> Vec<&UsdPrim> { self.prims_of_type(&UsdPrimType::Mesh) }
2024}
2025
2026// ============================================================
2027// ALEMBIC ARCHIVE SIMULATION
2028// ============================================================
2029
2030#[derive(Debug, Clone)]
2031pub struct AlembicSample {
2032    pub time_s: f64,
2033    pub positions: Vec<Vec3>,
2034    pub velocities: Vec<Vec3>,
2035    pub normals: Vec<Vec3>,
2036    pub uvs: Vec<Vec2>,
2037}
2038
2039impl AlembicSample {
2040    pub fn new(time_s: f64) -> Self {
2041        Self { time_s, positions: Vec::new(), velocities: Vec::new(), normals: Vec::new(), uvs: Vec::new() }
2042    }
2043    pub fn point_count(&self) -> usize { self.positions.len() }
2044    pub fn add_point(&mut self, pos: Vec3, vel: Vec3) {
2045        self.positions.push(pos);
2046        self.velocities.push(vel);
2047    }
2048    pub fn bounding_box(&self) -> (Vec3, Vec3) {
2049        if self.positions.is_empty() { return (Vec3::ZERO, Vec3::ZERO); }
2050        let mut min = self.positions[0];
2051        let mut max = self.positions[0];
2052        for &p in &self.positions {
2053            min = min.min(p); max = max.max(p);
2054        }
2055        (min, max)
2056    }
2057}
2058
2059#[derive(Debug, Clone)]
2060pub struct AlembicObject {
2061    pub name: String,
2062    pub schema: String,  // "PolyMesh", "Points", "Xform", etc.
2063    pub samples: Vec<AlembicSample>,
2064    pub is_constant: bool,
2065}
2066
2067impl AlembicObject {
2068    pub fn new(name: &str, schema: &str) -> Self {
2069        Self { name: name.to_string(), schema: schema.to_string(), samples: Vec::new(), is_constant: false }
2070    }
2071    pub fn add_sample(&mut self, s: AlembicSample) { self.samples.push(s); }
2072    pub fn sample_count(&self) -> usize { self.samples.len() }
2073    pub fn duration_s(&self) -> f64 {
2074        if self.samples.is_empty() { return 0.0; }
2075        let first = self.samples[0].time_s;
2076        let last = self.samples[self.samples.len()-1].time_s;
2077        last - first
2078    }
2079    pub fn sample_at_time(&self, t: f64) -> Option<&AlembicSample> {
2080        if self.samples.is_empty() { return None; }
2081        let idx = self.samples.iter().enumerate()
2082            .min_by(|(_, a), (_, b)| {
2083                (a.time_s - t).abs().partial_cmp(&(b.time_s - t).abs())
2084                    .unwrap_or(std::cmp::Ordering::Equal)
2085            })
2086            .map(|(i, _)| i);
2087        idx.map(|i| &self.samples[i])
2088    }
2089}
2090
2091#[derive(Debug, Clone, Default)]
2092pub struct AlembicArchive {
2093    pub filename: String,
2094    pub objects: Vec<AlembicObject>,
2095    pub start_time: f64,
2096    pub end_time: f64,
2097    pub fps: f64,
2098}
2099
2100impl AlembicArchive {
2101    pub fn new(filename: &str, fps: f64) -> Self {
2102        Self { filename: filename.to_string(), objects: Vec::new(), start_time: 0.0, end_time: 0.0, fps }
2103    }
2104    pub fn add_object(&mut self, o: AlembicObject) { self.objects.push(o); }
2105    pub fn object_count(&self) -> usize { self.objects.len() }
2106    pub fn total_samples(&self) -> usize { self.objects.iter().map(|o| o.sample_count()).sum() }
2107    pub fn duration_s(&self) -> f64 { self.end_time - self.start_time }
2108    pub fn frame_count(&self) -> usize { (self.duration_s() * self.fps) as usize }
2109    pub fn find_object(&self, name: &str) -> Option<&AlembicObject> {
2110        self.objects.iter().find(|o| o.name == name)
2111    }
2112}
2113
2114// ============================================================
2115// QUANTIZED ANIMATION STREAM
2116// ============================================================
2117
2118#[derive(Debug, Clone)]
2119pub struct QuantizedQuat {
2120    pub x: i16, pub y: i16, pub z: i16, pub w: i16,
2121}
2122
2123impl QuantizedQuat {
2124    pub fn from_quat(q: Quat) -> Self {
2125        let scale = 32767.0_f32;
2126        Self {
2127            x: (q.x * scale) as i16,
2128            y: (q.y * scale) as i16,
2129            z: (q.z * scale) as i16,
2130            w: (q.w * scale) as i16,
2131        }
2132    }
2133    pub fn to_quat(&self) -> Quat {
2134        let inv_scale = 1.0 / 32767.0;
2135        Quat::from_xyzw(
2136            self.x as f32 * inv_scale,
2137            self.y as f32 * inv_scale,
2138            self.z as f32 * inv_scale,
2139            self.w as f32 * inv_scale,
2140        ).normalize()
2141    }
2142    pub fn encode_smallest_three(q: Quat) -> (u8, i16, i16, i16) {
2143        let components = [q.x, q.y, q.z, q.w];
2144        let max_idx = components.iter().enumerate()
2145            .max_by(|(_, a), (_, b)| a.abs().partial_cmp(&b.abs()).unwrap_or(std::cmp::Ordering::Equal))
2146            .map(|(i, _)| i).unwrap_or(3);
2147        let sign = if components[max_idx] >= 0.0 { 1.0 } else { -1.0 };
2148        let scale = 32767.0_f32 / std::f32::consts::FRAC_1_SQRT_2;
2149        let small: Vec<i16> = (0..4).filter(|&i| i != max_idx)
2150            .map(|i| (components[i] * sign * scale) as i16)
2151            .collect();
2152        (max_idx as u8, small[0], small[1], small[2])
2153    }
2154}
2155
2156#[derive(Debug, Clone)]
2157pub struct QuantizedVec3 {
2158    pub x: i16, pub y: i16, pub z: i16,
2159}
2160
2161impl QuantizedVec3 {
2162    pub fn from_vec3_range(v: Vec3, min: Vec3, max: Vec3) -> Self {
2163        let range = max - min;
2164        let norm = (v - min) / Vec3::new(range.x.max(0.001), range.y.max(0.001), range.z.max(0.001));
2165        Self {
2166            x: (norm.x.clamp(0.0, 1.0) * 65535.0 - 32768.0) as i16,
2167            y: (norm.y.clamp(0.0, 1.0) * 65535.0 - 32768.0) as i16,
2168            z: (norm.z.clamp(0.0, 1.0) * 65535.0 - 32768.0) as i16,
2169        }
2170    }
2171    pub fn to_vec3_range(&self, min: Vec3, max: Vec3) -> Vec3 {
2172        let range = max - min;
2173        let t = Vec3::new(
2174            (self.x as f32 + 32768.0) / 65535.0,
2175            (self.y as f32 + 32768.0) / 65535.0,
2176            (self.z as f32 + 32768.0) / 65535.0,
2177        );
2178        min + t * range
2179    }
2180}
2181
2182#[derive(Debug, Clone)]
2183pub struct QuantizedBoneFrame {
2184    pub rotation: QuantizedQuat,
2185    pub translation: QuantizedVec3,
2186    pub scale: u16,  // uniform scale encoded as 16-bit fixed point
2187}
2188
2189impl QuantizedBoneFrame {
2190    pub fn new(rotation: Quat, translation: Vec3, scale: f32, t_min: Vec3, t_max: Vec3) -> Self {
2191        Self {
2192            rotation: QuantizedQuat::from_quat(rotation),
2193            translation: QuantizedVec3::from_vec3_range(translation, t_min, t_max),
2194            scale: (scale.clamp(0.0, 2.0) * 32767.5) as u16,
2195        }
2196    }
2197    pub fn decode_scale(&self) -> f32 { self.scale as f32 / 32767.5 }
2198    pub fn byte_size() -> usize { 14 } // 4*i16 + 3*i16 + u16 = 8+6+2=16 bytes
2199}
2200
2201#[derive(Debug, Clone)]
2202pub struct QuantizedAnimStream {
2203    pub bone_count: u32,
2204    pub frame_count: u32,
2205    pub fps: f32,
2206    pub translation_min: Vec3,
2207    pub translation_max: Vec3,
2208    pub frames: Vec<Vec<QuantizedBoneFrame>>,  // [frame][bone]
2209    pub bone_names: Vec<String>,
2210}
2211
2212impl QuantizedAnimStream {
2213    pub fn new(bone_count: u32, fps: f32) -> Self {
2214        Self { bone_count, frame_count: 0, fps, translation_min: Vec3::splat(-5.0),
2215            translation_max: Vec3::splat(5.0), frames: Vec::new(), bone_names: Vec::new() }
2216    }
2217    pub fn add_frame(&mut self, bones: Vec<QuantizedBoneFrame>) {
2218        self.frame_count += 1;
2219        self.frames.push(bones);
2220    }
2221    pub fn duration_s(&self) -> f32 { self.frame_count as f32 / self.fps.max(0.001) }
2222    pub fn total_bytes(&self) -> usize {
2223        self.frame_count as usize * self.bone_count as usize * QuantizedBoneFrame::byte_size()
2224    }
2225    pub fn sample_frame(&self, time_s: f32) -> Option<&Vec<QuantizedBoneFrame>> {
2226        let frame_idx = (time_s * self.fps) as usize;
2227        self.frames.get(frame_idx.min(self.frames.len().saturating_sub(1)))
2228    }
2229    pub fn compression_ratio_vs_f32(&self) -> f32 {
2230        let uncompressed = self.frame_count as usize * self.bone_count as usize * (4*4 + 3*4 + 4); // quat+vec3+scale as f32
2231        let compressed = self.total_bytes();
2232        if compressed == 0 { return 1.0; }
2233        uncompressed as f32 / compressed as f32
2234    }
2235}
2236
2237// ============================================================
2238// VFX INTEGRATION
2239// ============================================================
2240
2241#[derive(Debug, Clone, PartialEq)]
2242pub enum VfxEffectType {
2243    ParticleEmitter, RibbonTrail, MeshDecal, FluidSim,
2244    ClothSim, DestructionFx, GroundImpact, ExplosionRing,
2245    FirePlume, SmokeColumn, ElectricArc, PortalEffect,
2246}
2247
2248#[derive(Debug, Clone)]
2249pub struct VfxBinding {
2250    pub binding_id: u32,
2251    pub effect_type: VfxEffectType,
2252    pub attach_bone: String,
2253    pub local_offset: Vec3,
2254    pub local_rotation: Quat,
2255    pub scale: f32,
2256    pub start_time_s: f32,
2257    pub duration_s: f32,
2258    pub loop_count: i32,   // -1 = infinite
2259    pub intensity: f32,
2260    pub color_tint: Vec4,
2261}
2262
2263impl VfxBinding {
2264    pub fn new(binding_id: u32, effect_type: VfxEffectType, attach_bone: &str, start_time_s: f32) -> Self {
2265        Self { binding_id, effect_type, attach_bone: attach_bone.to_string(),
2266            local_offset: Vec3::ZERO, local_rotation: Quat::IDENTITY,
2267            scale: 1.0, start_time_s, duration_s: 1.0, loop_count: 1,
2268            intensity: 1.0, color_tint: Vec4::new(1.0, 1.0, 1.0, 1.0) }
2269    }
2270    pub fn end_time_s(&self) -> f32 { self.start_time_s + self.duration_s }
2271    pub fn is_active_at(&self, time_s: f32) -> bool {
2272        time_s >= self.start_time_s && (self.loop_count < 0 || time_s <= self.end_time_s())
2273    }
2274    pub fn effect_type_str(&self) -> &'static str {
2275        match &self.effect_type {
2276            VfxEffectType::ParticleEmitter => "Particle Emitter",
2277            VfxEffectType::RibbonTrail => "Ribbon Trail",
2278            VfxEffectType::MeshDecal => "Mesh Decal",
2279            VfxEffectType::FluidSim => "Fluid Simulation",
2280            VfxEffectType::ClothSim => "Cloth Simulation",
2281            VfxEffectType::DestructionFx => "Destruction FX",
2282            VfxEffectType::GroundImpact => "Ground Impact",
2283            VfxEffectType::ExplosionRing => "Explosion Ring",
2284            VfxEffectType::FirePlume => "Fire Plume",
2285            VfxEffectType::SmokeColumn => "Smoke Column",
2286            VfxEffectType::ElectricArc => "Electric Arc",
2287            VfxEffectType::PortalEffect => "Portal Effect",
2288        }
2289    }
2290}
2291
2292#[derive(Debug, Clone, Default)]
2293pub struct VfxLayer {
2294    pub bindings: Vec<VfxBinding>,
2295    pub global_scale: f32,
2296    pub paused: bool,
2297}
2298
2299impl VfxLayer {
2300    pub fn new() -> Self { Self { bindings: Vec::new(), global_scale: 1.0, paused: false } }
2301    pub fn add_binding(&mut self, b: VfxBinding) { self.bindings.push(b); }
2302    pub fn active_at(&self, time_s: f32) -> Vec<&VfxBinding> {
2303        if self.paused { return Vec::new(); }
2304        self.bindings.iter().filter(|b| b.is_active_at(time_s)).collect()
2305    }
2306    pub fn total_bindings(&self) -> usize { self.bindings.len() }
2307    pub fn bindings_of_type(&self, effect_type: &VfxEffectType) -> Vec<&VfxBinding> {
2308        self.bindings.iter().filter(|b| &b.effect_type == effect_type).collect()
2309    }
2310}
2311
2312// ============================================================
2313// SCENE RENDER PASS MANAGER
2314// ============================================================
2315
2316#[derive(Debug, Clone, PartialEq)]
2317pub enum RenderPassType {
2318    ZPrepass, Opaque, AlphaMask, Transparent, Decals,
2319    VolumetricFog, DeferredLighting, ShadowMap, PostProcess,
2320    UI, Debug, Custom(u8),
2321}
2322
2323#[derive(Debug, Clone)]
2324pub struct RenderPass {
2325    pub pass_type: RenderPassType,
2326    pub name: String,
2327    pub enabled: bool,
2328    pub clear_color: Vec4,
2329    pub clear_depth: bool,
2330    pub render_target: String,
2331    pub priority: i32,
2332}
2333
2334impl RenderPass {
2335    pub fn new(pass_type: RenderPassType, name: &str, priority: i32) -> Self {
2336        Self { pass_type, name: name.to_string(), enabled: true,
2337            clear_color: Vec4::new(0.0, 0.0, 0.0, 1.0),
2338            clear_depth: true, render_target: "backbuffer".to_string(), priority }
2339    }
2340    pub fn pass_type_str(&self) -> String {
2341        match &self.pass_type {
2342            RenderPassType::ZPrepass => "ZPrepass".to_string(),
2343            RenderPassType::Opaque => "Opaque".to_string(),
2344            RenderPassType::AlphaMask => "AlphaMask".to_string(),
2345            RenderPassType::Transparent => "Transparent".to_string(),
2346            RenderPassType::Decals => "Decals".to_string(),
2347            RenderPassType::VolumetricFog => "VolumetricFog".to_string(),
2348            RenderPassType::DeferredLighting => "DeferredLighting".to_string(),
2349            RenderPassType::ShadowMap => "ShadowMap".to_string(),
2350            RenderPassType::PostProcess => "PostProcess".to_string(),
2351            RenderPassType::UI => "UI".to_string(),
2352            RenderPassType::Debug => "Debug".to_string(),
2353            RenderPassType::Custom(n) => format!("Custom({})", n),
2354        }
2355    }
2356}
2357
2358#[derive(Debug, Clone, Default)]
2359pub struct RenderPassManager {
2360    pub passes: Vec<RenderPass>,
2361}
2362
2363impl RenderPassManager {
2364    pub fn new() -> Self { Self { passes: Vec::new() } }
2365    pub fn add_pass(&mut self, p: RenderPass) { self.passes.push(p); }
2366    pub fn default_pipeline() -> Self {
2367        let mut mgr = Self::new();
2368        mgr.add_pass(RenderPass::new(RenderPassType::ZPrepass, "Z Pre-pass", 0));
2369        mgr.add_pass(RenderPass::new(RenderPassType::ShadowMap, "Shadow Maps", 5));
2370        mgr.add_pass(RenderPass::new(RenderPassType::Opaque, "Opaque", 10));
2371        mgr.add_pass(RenderPass::new(RenderPassType::AlphaMask, "Alpha Mask", 15));
2372        mgr.add_pass(RenderPass::new(RenderPassType::DeferredLighting, "Deferred Lighting", 20));
2373        mgr.add_pass(RenderPass::new(RenderPassType::Decals, "Decals", 25));
2374        mgr.add_pass(RenderPass::new(RenderPassType::VolumetricFog, "Volumetric Fog", 30));
2375        mgr.add_pass(RenderPass::new(RenderPassType::Transparent, "Transparent", 35));
2376        mgr.add_pass(RenderPass::new(RenderPassType::PostProcess, "Post Process", 40));
2377        mgr.add_pass(RenderPass::new(RenderPassType::UI, "UI", 45));
2378        mgr
2379    }
2380    pub fn enabled_passes(&self) -> Vec<&RenderPass> {
2381        let mut passes: Vec<&RenderPass> = self.passes.iter().filter(|p| p.enabled).collect();
2382        passes.sort_by_key(|p| p.priority);
2383        passes
2384    }
2385    pub fn count(&self) -> usize { self.passes.len() }
2386}
2387
2388// ============================================================
2389// SOUND CUE SYSTEM
2390// ============================================================
2391
2392#[derive(Debug, Clone, PartialEq)]
2393pub enum SoundCueType {
2394    OneShot, Looping, Ambience, Dialogue, Music, Foley,
2395}
2396
2397#[derive(Debug, Clone)]
2398pub struct SoundCue {
2399    pub cue_id: u32,
2400    pub name: String,
2401    pub cue_type: SoundCueType,
2402    pub start_time_s: f32,
2403    pub duration_s: f32,
2404    pub volume: f32,
2405    pub pitch: f32,
2406    pub spatial: bool,
2407    pub position: Vec3,
2408    pub falloff_radius_m: f32,
2409    pub asset_path: String,
2410}
2411
2412impl SoundCue {
2413    pub fn new(cue_id: u32, name: &str, cue_type: SoundCueType, start_time_s: f32, duration_s: f32) -> Self {
2414        Self { cue_id, name: name.to_string(), cue_type, start_time_s, duration_s,
2415            volume: 1.0, pitch: 1.0, spatial: false, position: Vec3::ZERO,
2416            falloff_radius_m: 20.0, asset_path: String::new() }
2417    }
2418    pub fn end_time_s(&self) -> f32 { self.start_time_s + self.duration_s }
2419    pub fn is_active_at(&self, time_s: f32) -> bool {
2420        time_s >= self.start_time_s && time_s <= self.end_time_s()
2421    }
2422    pub fn volume_at_distance(&self, distance_m: f32) -> f32 {
2423        if !self.spatial || distance_m <= 0.0 { return self.volume; }
2424        let ratio = (1.0 - distance_m / self.falloff_radius_m.max(0.001)).max(0.0);
2425        self.volume * ratio * ratio  // inverse square falloff approximation
2426    }
2427}
2428
2429#[derive(Debug, Clone, Default)]
2430pub struct SoundTrack {
2431    pub track_name: String,
2432    pub cues: Vec<SoundCue>,
2433    pub muted: bool,
2434    pub solo: bool,
2435    pub master_volume: f32,
2436}
2437
2438impl SoundTrack {
2439    pub fn new(track_name: &str) -> Self {
2440        Self { track_name: track_name.to_string(), cues: Vec::new(),
2441            muted: false, solo: false, master_volume: 1.0 }
2442    }
2443    pub fn add_cue(&mut self, c: SoundCue) { self.cues.push(c); }
2444    pub fn cues_at_time(&self, time_s: f32) -> Vec<&SoundCue> {
2445        if self.muted { return Vec::new(); }
2446        self.cues.iter().filter(|c| c.is_active_at(time_s)).collect()
2447    }
2448    pub fn duration_s(&self) -> f32 {
2449        self.cues.iter().map(|c| c.end_time_s()).fold(0.0_f32, f32::max)
2450    }
2451    pub fn cue_count(&self) -> usize { self.cues.len() }
2452}
2453
2454// ============================================================
2455// RENDER SEQUENCE EXPORTER
2456// ============================================================
2457
2458#[derive(Debug, Clone, PartialEq)]
2459pub enum ExportFormat {
2460    Png, Exr, Jpeg, Tiff, DpxRaw,
2461}
2462
2463#[derive(Debug, Clone, PartialEq)]
2464pub enum ExportColorspace {
2465    Srgb, LinearRec709, Aces, DciP3, Rec2020,
2466}
2467
2468#[derive(Debug, Clone)]
2469pub struct RenderSequenceExport {
2470    pub output_path: String,
2471    pub format: ExportFormat,
2472    pub colorspace: ExportColorspace,
2473    pub width: u32,
2474    pub height: u32,
2475    pub start_frame: u32,
2476    pub end_frame: u32,
2477    pub fps: f32,
2478    pub bit_depth: u8,
2479    pub include_alpha: bool,
2480    pub denoise: bool,
2481    pub aov_outputs: Vec<String>,
2482}
2483
2484impl RenderSequenceExport {
2485    pub fn new(output_path: &str, width: u32, height: u32, fps: f32) -> Self {
2486        Self { output_path: output_path.to_string(), format: ExportFormat::Exr,
2487            colorspace: ExportColorspace::LinearRec709,
2488            width, height, start_frame: 1, end_frame: 100, fps,
2489            bit_depth: 16, include_alpha: true, denoise: false, aov_outputs: Vec::new() }
2490    }
2491    pub fn frame_count(&self) -> u32 { self.end_frame - self.start_frame + 1 }
2492    pub fn duration_s(&self) -> f32 { self.frame_count() as f32 / self.fps.max(0.001) }
2493    pub fn bytes_per_frame(&self) -> u64 {
2494        let channels = if self.include_alpha { 4 } else { 3 };
2495        let bits_per_channel = self.bit_depth as u64;
2496        self.width as u64 * self.height as u64 * channels * bits_per_channel / 8
2497    }
2498    pub fn total_bytes(&self) -> u64 {
2499        self.bytes_per_frame() * self.frame_count() as u64
2500    }
2501    pub fn total_gb(&self) -> f64 { self.total_bytes() as f64 / (1024.0 * 1024.0 * 1024.0) }
2502    pub fn format_str(&self) -> &'static str {
2503        match &self.format {
2504            ExportFormat::Png => "PNG", ExportFormat::Exr => "EXR",
2505            ExportFormat::Jpeg => "JPEG", ExportFormat::Tiff => "TIFF",
2506            ExportFormat::DpxRaw => "DPX",
2507        }
2508    }
2509    pub fn frame_filename(&self, frame: u32) -> String {
2510        let ext = self.format_str().to_lowercase();
2511        format!("{}/frame_{:06}.{}", self.output_path, frame, ext)
2512    }
2513    pub fn add_aov(&mut self, aov_name: &str) { self.aov_outputs.push(aov_name.to_string()); }
2514}
2515
2516// ============================================================
2517// CUTSCENE DIRECTOR (HIGH-LEVEL ORCHESTRATOR)
2518// ============================================================
2519
2520#[derive(Debug, Clone, PartialEq)]
2521pub enum DirectorState {
2522    Idle, Playing, Paused, Scrubbing, Recording,
2523}
2524
2525#[derive(Debug, Clone)]
2526pub struct CutsceneDirector {
2527    pub cutscene_id: String,
2528    pub state: DirectorState,
2529    pub current_time_s: f32,
2530    pub total_duration_s: f32,
2531    pub playback_speed: f32,
2532    pub sound_tracks: Vec<SoundTrack>,
2533    pub vfx_layer: VfxLayer,
2534    pub post_process: PostProcessStack,
2535    pub render_pass_mgr: RenderPassManager,
2536    pub usd_stage: Option<UsdStage>,
2537    pub export_settings: Option<RenderSequenceExport>,
2538    pub markers: Vec<(f32, String)>,  // time -> label
2539}
2540
2541impl CutsceneDirector {
2542    pub fn new(cutscene_id: &str, total_duration_s: f32) -> Self {
2543        Self { cutscene_id: cutscene_id.to_string(), state: DirectorState::Idle,
2544            current_time_s: 0.0, total_duration_s, playback_speed: 1.0,
2545            sound_tracks: Vec::new(), vfx_layer: VfxLayer::new(),
2546            post_process: PostProcessStack::new(),
2547            render_pass_mgr: RenderPassManager::default_pipeline(),
2548            usd_stage: None, export_settings: None, markers: Vec::new() }
2549    }
2550    pub fn play(&mut self) { self.state = DirectorState::Playing; }
2551    pub fn pause(&mut self) { self.state = DirectorState::Paused; }
2552    pub fn stop(&mut self) { self.state = DirectorState::Idle; self.current_time_s = 0.0; }
2553    pub fn seek(&mut self, time_s: f32) {
2554        self.current_time_s = time_s.clamp(0.0, self.total_duration_s);
2555        self.state = DirectorState::Scrubbing;
2556    }
2557    pub fn add_sound_track(&mut self, t: SoundTrack) { self.sound_tracks.push(t); }
2558    pub fn add_marker(&mut self, time_s: f32, label: &str) {
2559        self.markers.push((time_s, label.to_string()));
2560    }
2561    pub fn advance(&mut self, delta_s: f32) {
2562        if self.state == DirectorState::Playing {
2563            self.current_time_s = (self.current_time_s + delta_s * self.playback_speed)
2564                .min(self.total_duration_s);
2565            if self.current_time_s >= self.total_duration_s {
2566                self.state = DirectorState::Idle;
2567            }
2568        }
2569    }
2570    pub fn active_sound_cues(&self) -> Vec<&SoundCue> {
2571        self.sound_tracks.iter()
2572            .flat_map(|t| t.cues_at_time(self.current_time_s))
2573            .collect()
2574    }
2575    pub fn active_vfx(&self) -> Vec<&VfxBinding> {
2576        self.vfx_layer.active_at(self.current_time_s)
2577    }
2578    pub fn progress_pct(&self) -> f32 {
2579        if self.total_duration_s < 0.001 { return 0.0; }
2580        self.current_time_s / self.total_duration_s * 100.0
2581    }
2582    pub fn markers_in_range(&self, start_s: f32, end_s: f32) -> Vec<&(f32, String)> {
2583        self.markers.iter().filter(|(t, _)| *t >= start_s && *t <= end_s).collect()
2584    }
2585    pub fn render_pass_count(&self) -> usize { self.render_pass_mgr.count() }
2586}
2587
2588// ============================================================
2589// ANIMATION RETARGETING SYSTEM
2590// ============================================================
2591
2592#[derive(Debug, Clone)]
2593pub struct BoneMapping {
2594    pub source_bone: String,
2595    pub target_bone: String,
2596    pub rotation_offset: Quat,
2597    pub scale_factor: f32,
2598}
2599
2600impl BoneMapping {
2601    pub fn new(source: &str, target: &str) -> Self {
2602        Self { source_bone: source.to_string(), target_bone: target.to_string(),
2603            rotation_offset: Quat::IDENTITY, scale_factor: 1.0 }
2604    }
2605    pub fn with_rotation_offset(mut self, rot: Quat) -> Self { self.rotation_offset = rot; self }
2606    pub fn with_scale(mut self, scale: f32) -> Self { self.scale_factor = scale; self }
2607}
2608
2609#[derive(Debug, Clone, Default)]
2610pub struct RetargetProfile {
2611    pub source_skeleton: String,
2612    pub target_skeleton: String,
2613    pub bone_mappings: Vec<BoneMapping>,
2614    pub unmapped_bone_policy: String,  // "zero", "t-pose", "skip"
2615}
2616
2617impl RetargetProfile {
2618    pub fn new(source: &str, target: &str) -> Self {
2619        Self { source_skeleton: source.to_string(), target_skeleton: target.to_string(),
2620            bone_mappings: Vec::new(), unmapped_bone_policy: "t-pose".to_string() }
2621    }
2622    pub fn add_mapping(&mut self, mapping: BoneMapping) { self.bone_mappings.push(mapping); }
2623    pub fn find_mapping(&self, source_bone: &str) -> Option<&BoneMapping> {
2624        self.bone_mappings.iter().find(|m| m.source_bone == source_bone)
2625    }
2626    pub fn mapping_count(&self) -> usize { self.bone_mappings.len() }
2627    pub fn humanoid_biped() -> Self {
2628        let mut p = Self::new("source_biped", "target_biped");
2629        let bone_pairs = [
2630            ("Hips", "pelvis"), ("Spine", "spine_01"), ("Spine1", "spine_02"),
2631            ("Spine2", "spine_03"), ("Neck", "neck_01"), ("Head", "head"),
2632            ("LeftShoulder", "clavicle_l"), ("LeftArm", "upperarm_l"),
2633            ("LeftForeArm", "lowerarm_l"), ("LeftHand", "hand_l"),
2634            ("RightShoulder", "clavicle_r"), ("RightArm", "upperarm_r"),
2635            ("RightForeArm", "lowerarm_r"), ("RightHand", "hand_r"),
2636            ("LeftUpLeg", "thigh_l"), ("LeftLeg", "calf_l"), ("LeftFoot", "foot_l"),
2637            ("RightUpLeg", "thigh_r"), ("RightLeg", "calf_r"), ("RightFoot", "foot_r"),
2638        ];
2639        for (src, tgt) in &bone_pairs {
2640            p.add_mapping(BoneMapping::new(src, tgt));
2641        }
2642        p
2643    }
2644}
2645
2646// ============================================================
2647// FACIAL CAPTURE PROCESSING
2648// ============================================================
2649
2650pub const ARKit_BLEND_SHAPE_NAMES: [&str; 52] = [
2651    "eyeBlinkLeft", "eyeLookDownLeft", "eyeLookInLeft", "eyeLookOutLeft", "eyeLookUpLeft",
2652    "eyeSquintLeft", "eyeWideLeft", "eyeBlinkRight", "eyeLookDownRight", "eyeLookInRight",
2653    "eyeLookOutRight", "eyeLookUpRight", "eyeSquintRight", "eyeWideRight",
2654    "jawForward", "jawLeft", "jawRight", "jawOpen",
2655    "mouthClose", "mouthFunnel", "mouthPucker", "mouthLeft", "mouthRight",
2656    "mouthSmileLeft", "mouthSmileRight", "mouthFrownLeft", "mouthFrownRight",
2657    "mouthDimpleLeft", "mouthDimpleRight", "mouthStretchLeft", "mouthStretchRight",
2658    "mouthRollLower", "mouthRollUpper", "mouthShrugLower", "mouthShrugUpper",
2659    "mouthPressLeft", "mouthPressRight", "mouthLowerDownLeft", "mouthLowerDownRight",
2660    "mouthUpperUpLeft", "mouthUpperUpRight", "browDownLeft", "browDownRight",
2661    "browInnerUp", "browOuterUpLeft", "browOuterUpRight",
2662    "cheekPuff", "cheekSquintLeft", "cheekSquintRight",
2663    "noseSneerLeft", "noseSneerRight", "tongueOut",
2664];
2665
2666#[derive(Debug, Clone)]
2667pub struct FacialCaptureFrame {
2668    pub time_s: f64,
2669    pub blend_shapes: [f32; 52],
2670    pub head_rotation: Quat,
2671    pub left_eye_rotation: Quat,
2672    pub right_eye_rotation: Quat,
2673    pub confidence: f32,
2674}
2675
2676impl FacialCaptureFrame {
2677    pub fn new(time_s: f64) -> Self {
2678        Self { time_s, blend_shapes: [0.0; 52], head_rotation: Quat::IDENTITY,
2679            left_eye_rotation: Quat::IDENTITY, right_eye_rotation: Quat::IDENTITY,
2680            confidence: 1.0 }
2681    }
2682    pub fn set_blend_shape(&mut self, name: &str, value: f32) {
2683        if let Some(idx) = ARKit_BLEND_SHAPE_NAMES.iter().position(|&n| n == name) {
2684            self.blend_shapes[idx] = value.clamp(0.0, 1.0);
2685        }
2686    }
2687    pub fn get_blend_shape(&self, name: &str) -> f32 {
2688        ARKit_BLEND_SHAPE_NAMES.iter().position(|&n| n == name)
2689            .map(|idx| self.blend_shapes[idx])
2690            .unwrap_or(0.0)
2691    }
2692    pub fn jaw_open(&self) -> f32 { self.blend_shapes[17] }
2693    pub fn mouth_smile_avg(&self) -> f32 { (self.blend_shapes[23] + self.blend_shapes[24]) / 2.0 }
2694    pub fn eye_blink_avg(&self) -> f32 { (self.blend_shapes[0] + self.blend_shapes[7]) / 2.0 }
2695    pub fn brow_raise_avg(&self) -> f32 { self.blend_shapes[43] }
2696    pub fn detected_expression(&self) -> &'static str {
2697        if self.mouth_smile_avg() > 0.5 { return "happy"; }
2698        if self.blend_shapes[25] > 0.4 || self.blend_shapes[26] > 0.4 { return "sad"; }
2699        if self.blend_shapes[40] > 0.3 || self.blend_shapes[41] > 0.3 { return "angry"; }
2700        if self.brow_raise_avg() > 0.4 && self.jaw_open() > 0.3 { return "surprised"; }
2701        if self.eye_blink_avg() > 0.7 { return "blinking"; }
2702        "neutral"
2703    }
2704}
2705
2706#[derive(Debug, Clone, Default)]
2707pub struct FacialCaptureTake {
2708    pub take_id: String,
2709    pub actor_name: String,
2710    pub frames: Vec<FacialCaptureFrame>,
2711    pub fps: f32,
2712    pub camera_model: String,
2713}
2714
2715impl FacialCaptureTake {
2716    pub fn new(take_id: &str, actor_name: &str, fps: f32) -> Self {
2717        Self { take_id: take_id.to_string(), actor_name: actor_name.to_string(),
2718            frames: Vec::new(), fps, camera_model: "iPhone".to_string() }
2719    }
2720    pub fn add_frame(&mut self, f: FacialCaptureFrame) { self.frames.push(f); }
2721    pub fn frame_count(&self) -> usize { self.frames.len() }
2722    pub fn duration_s(&self) -> f64 {
2723        if self.frames.is_empty() { return 0.0; }
2724        self.frames[self.frames.len()-1].time_s - self.frames[0].time_s
2725    }
2726    pub fn frame_at_time(&self, time_s: f64) -> Option<&FacialCaptureFrame> {
2727        if self.frames.is_empty() { return None; }
2728        let idx = self.frames.iter().enumerate()
2729            .min_by(|(_, a), (_, b)| {
2730                (a.time_s - time_s).abs().partial_cmp(&(b.time_s - time_s).abs())
2731                    .unwrap_or(std::cmp::Ordering::Equal)
2732            }).map(|(i, _)| i);
2733        idx.map(|i| &self.frames[i])
2734    }
2735    pub fn average_confidence(&self) -> f32 {
2736        if self.frames.is_empty() { return 0.0; }
2737        self.frames.iter().map(|f| f.confidence).sum::<f32>() / self.frames.len() as f32
2738    }
2739    pub fn low_confidence_frames(&self, threshold: f32) -> Vec<&FacialCaptureFrame> {
2740        self.frames.iter().filter(|f| f.confidence < threshold).collect()
2741    }
2742    pub fn smooth_blend_shapes(&mut self, window_size: usize) {
2743        if self.frames.len() < window_size { return; }
2744        let n = self.frames.len();
2745        let orig: Vec<[f32; 52]> = self.frames.iter().map(|f| f.blend_shapes).collect();
2746        for i in 0..n {
2747            let start = i.saturating_sub(window_size / 2);
2748            let end = (i + window_size / 2 + 1).min(n);
2749            let count = (end - start) as f32;
2750            for j in 0..52 {
2751                let avg = orig[start..end].iter().map(|bs| bs[j]).sum::<f32>() / count;
2752                self.frames[i].blend_shapes[j] = avg;
2753            }
2754        }
2755    }
2756    pub fn dominant_expression_histogram(&self) -> HashMap<String, usize> {
2757        let mut hist: HashMap<String, usize> = HashMap::new();
2758        for f in &self.frames {
2759            *hist.entry(f.detected_expression().to_string()).or_insert(0) += 1;
2760        }
2761        hist
2762    }
2763}
2764
2765// ============================================================
2766// SCENE LIGHTING KEYFRAMES
2767// ============================================================
2768
2769#[derive(Debug, Clone)]
2770pub struct ExtLightKeyframe {
2771    pub time_s: f32,
2772    pub color: Vec3,
2773    pub intensity: f32,
2774    pub radius_or_angle: f32,
2775    pub cast_shadows: bool,
2776    pub shadow_softness: f32,
2777}
2778
2779impl ExtLightKeyframe {
2780    pub fn new(time_s: f32, color: Vec3, intensity: f32) -> Self {
2781        Self { time_s, color, intensity, radius_or_angle: 5.0, cast_shadows: true, shadow_softness: 1.0 }
2782    }
2783    pub fn lerp(&self, other: &ExtLightKeyframe, t: f32) -> ExtLightKeyframe {
2784        let t = t.clamp(0.0, 1.0);
2785        ExtLightKeyframe {
2786            time_s: self.time_s + (other.time_s - self.time_s) * t,
2787            color: self.color.lerp(other.color, t),
2788            intensity: self.intensity + (other.intensity - self.intensity) * t,
2789            radius_or_angle: self.radius_or_angle + (other.radius_or_angle - self.radius_or_angle) * t,
2790            cast_shadows: if t < 0.5 { self.cast_shadows } else { other.cast_shadows },
2791            shadow_softness: self.shadow_softness + (other.shadow_softness - self.shadow_softness) * t,
2792        }
2793    }
2794}
2795
2796#[derive(Debug, Clone, Default)]
2797pub struct LightAnimTrack {
2798    pub light_name: String,
2799    pub keyframes: Vec<ExtLightKeyframe>,
2800}
2801
2802impl LightAnimTrack {
2803    pub fn new(light_name: &str) -> Self { Self { light_name: light_name.to_string(), keyframes: Vec::new() } }
2804    pub fn add_keyframe(&mut self, kf: ExtLightKeyframe) {
2805        let idx = self.keyframes.partition_point(|k| k.time_s < kf.time_s);
2806        self.keyframes.insert(idx, kf);
2807    }
2808    pub fn sample(&self, time_s: f32) -> Option<ExtLightKeyframe> {
2809        if self.keyframes.is_empty() { return None; }
2810        if self.keyframes.len() == 1 { return Some(self.keyframes[0].clone()); }
2811        let idx = self.keyframes.partition_point(|k| k.time_s <= time_s);
2812        if idx == 0 { return Some(self.keyframes[0].clone()); }
2813        if idx >= self.keyframes.len() { return Some(self.keyframes[self.keyframes.len()-1].clone()); }
2814        let prev = &self.keyframes[idx-1];
2815        let next = &self.keyframes[idx];
2816        let span = next.time_s - prev.time_s;
2817        let t = if span > 0.001 { (time_s - prev.time_s) / span } else { 0.0 };
2818        Some(prev.lerp(next, t))
2819    }
2820    pub fn duration_s(&self) -> f32 {
2821        if self.keyframes.is_empty() { return 0.0; }
2822        self.keyframes[self.keyframes.len()-1].time_s
2823    }
2824    pub fn keyframe_count(&self) -> usize { self.keyframes.len() }
2825}
2826
2827// ============================================================
2828// TEST FUNCTIONS
2829// ============================================================
2830
2831pub fn run_post_process_tests() {
2832    let cinema = PostProcessStack::cinema_preset();
2833    assert!(cinema.effect_count() > 0);
2834    assert!(cinema.enabled_effects().len() > 0);
2835
2836    let horror = PostProcessStack::horror_preset();
2837    assert!(horror.saturation < 0.5);
2838    assert!(horror.contrast > 1.2);
2839
2840    let effect = PostProcessEffect::bloom(0.9, 0.7, 0.5);
2841    assert_eq!(effect.effect_type_str(), "Bloom");
2842    assert!(effect.params.contains_key("threshold"));
2843}
2844
2845pub fn run_usd_stage_tests() {
2846    let mut stage = UsdStage::new("/tmp/test_scene.usda");
2847    stage.time_codes_per_second = 24.0;
2848    stage.end_time_code = 240.0;
2849
2850    let mut root = UsdPrim::new("/root", UsdPrimType::Xform);
2851    root.set_attr("xformOp:translate", "(0, 0, 0)");
2852    stage.add_prim(root);
2853
2854    let cam = UsdPrim::new("/root/Camera", UsdPrimType::Camera);
2855    stage.add_prim(cam);
2856
2857    assert_eq!(stage.prim_count(), 2);
2858    assert_eq!(stage.duration_seconds(), 10.0);
2859    assert_eq!(stage.camera_prims().len(), 1);
2860    let header = stage.export_usda_header();
2861    assert!(header.contains("usda 1.0"));
2862}
2863
2864pub fn run_alembic_tests() {
2865    let mut archive = AlembicArchive::new("crowd_sim.abc", 24.0);
2866    archive.end_time = 4.0;
2867
2868    let mut crowd = AlembicObject::new("crowd_particles", "Points");
2869    for i in 0..10 {
2870        let mut sample = AlembicSample::new(i as f64 * (1.0 / 24.0));
2871        for j in 0..100 {
2872            let pos = Vec3::new(j as f32 * 0.1, 0.0, i as f32 * 0.05);
2873            let vel = Vec3::new(0.5, 0.0, 0.0);
2874            sample.add_point(pos, vel);
2875        }
2876        crowd.add_sample(sample);
2877    }
2878    archive.add_object(crowd);
2879
2880    assert_eq!(archive.object_count(), 1);
2881    assert_eq!(archive.total_samples(), 10);
2882    let obj = archive.find_object("crowd_particles").unwrap();
2883    assert_eq!(obj.sample_count(), 10);
2884    let sample = obj.sample_at_time(0.0).unwrap();
2885    let (bb_min, bb_max) = sample.bounding_box();
2886    assert!(bb_max.x > bb_min.x);
2887}
2888
2889pub fn run_quantized_anim_tests() {
2890    let q = Quat::from_rotation_y(0.5);
2891    let qq = QuantizedQuat::from_quat(q);
2892    let recovered = qq.to_quat();
2893    let dot = (q.x * recovered.x + q.y * recovered.y + q.z * recovered.z + q.w * recovered.w).abs();
2894    assert!(dot > 0.99, "dot={}", dot);
2895
2896    let mut stream = QuantizedAnimStream::new(3, 24.0);
2897    for _ in 0..24 {
2898        let bones = vec![
2899            QuantizedBoneFrame::new(Quat::IDENTITY, Vec3::ZERO, 1.0, Vec3::splat(-5.0), Vec3::splat(5.0)),
2900            QuantizedBoneFrame::new(Quat::from_rotation_x(0.1), Vec3::new(0.0, 1.0, 0.0), 1.0, Vec3::splat(-5.0), Vec3::splat(5.0)),
2901            QuantizedBoneFrame::new(Quat::from_rotation_z(0.2), Vec3::ZERO, 1.0, Vec3::splat(-5.0), Vec3::splat(5.0)),
2902        ];
2903        stream.add_frame(bones);
2904    }
2905    assert_eq!(stream.frame_count, 24);
2906    assert!((stream.duration_s() - 1.0).abs() < 0.001);
2907    let ratio = stream.compression_ratio_vs_f32();
2908    assert!(ratio > 1.0, "compression ratio should be > 1: {}", ratio);
2909}
2910
2911pub fn run_vfx_tests() {
2912    let mut layer = VfxLayer::new();
2913    layer.add_binding(VfxBinding::new(1, VfxEffectType::ExplosionRing, "root", 0.5));
2914    layer.add_binding(VfxBinding::new(2, VfxEffectType::SmokeColumn, "root", 0.5));
2915    layer.add_binding(VfxBinding::new(3, VfxEffectType::ParticleEmitter, "Spine", 0.0));
2916
2917    let active = layer.active_at(0.7);
2918    assert!(!active.is_empty());
2919    let explosions = layer.bindings_of_type(&VfxEffectType::ExplosionRing);
2920    assert_eq!(explosions.len(), 1);
2921}
2922
2923pub fn run_sound_cue_tests() {
2924    let mut track = SoundTrack::new("sfx_track");
2925    track.add_cue(SoundCue::new(1, "footstep", SoundCueType::Foley, 0.0, 0.3));
2926    track.add_cue(SoundCue::new(2, "explosion", SoundCueType::OneShot, 1.5, 2.0));
2927    track.add_cue(SoundCue::new(3, "ambient_wind", SoundCueType::Looping, 0.0, 10.0));
2928
2929    let active_at_0 = track.cues_at_time(0.1);
2930    assert!(!active_at_0.is_empty());
2931    let active_at_2 = track.cues_at_time(2.0);
2932    assert!(active_at_2.iter().any(|c| c.name == "explosion"));
2933    assert!((track.duration_s() - 10.0).abs() < 0.01);
2934}
2935
2936pub fn run_render_export_tests() {
2937    let mut export = RenderSequenceExport::new("/renders/shot01", 1920, 1080, 24.0);
2938    export.start_frame = 1;
2939    export.end_frame = 240;
2940    export.add_aov("beauty");
2941    export.add_aov("normal");
2942    export.add_aov("depth");
2943
2944    assert_eq!(export.frame_count(), 240);
2945    assert!((export.duration_s() - 10.0).abs() < 0.01);
2946    assert!(export.total_bytes() > 0);
2947    let filename = export.frame_filename(1);
2948    assert!(filename.contains("000001"));
2949    assert_eq!(export.aov_outputs.len(), 3);
2950}
2951
2952pub fn run_facial_capture_tests() {
2953    let mut take = FacialCaptureTake::new("take_001", "John", 30.0);
2954
2955    for i in 0..30 {
2956        let mut frame = FacialCaptureFrame::new(i as f64 / 30.0);
2957        frame.set_blend_shape("mouthSmileLeft", if i > 15 { 0.8 } else { 0.1 });
2958        frame.set_blend_shape("mouthSmileRight", if i > 15 { 0.7 } else { 0.1 });
2959        frame.confidence = if i == 5 { 0.3 } else { 0.95 };
2960        take.add_frame(frame);
2961    }
2962
2963    assert_eq!(take.frame_count(), 30);
2964    let low_conf = take.low_confidence_frames(0.5);
2965    assert_eq!(low_conf.len(), 1);
2966    let hist = take.dominant_expression_histogram();
2967    assert!(!hist.is_empty());
2968    take.smooth_blend_shapes(3);
2969}
2970
2971pub fn run_light_anim_tests() {
2972    let mut track = LightAnimTrack::new("sun_light");
2973    track.add_keyframe(ExtLightKeyframe::new(0.0, Vec3::new(1.0, 0.8, 0.6), 100000.0));
2974    track.add_keyframe(ExtLightKeyframe::new(5.0, Vec3::new(0.8, 0.4, 0.2), 50000.0));
2975    track.add_keyframe(ExtLightKeyframe::new(10.0, Vec3::new(0.1, 0.1, 0.2), 10000.0));
2976
2977    assert_eq!(track.keyframe_count(), 3);
2978    let sample = track.sample(2.5).unwrap();
2979    assert!((sample.intensity - 75000.0).abs() < 1000.0);
2980    assert!((track.duration_s() - 10.0).abs() < 0.01);
2981}
2982
2983pub fn run_director_tests() {
2984    let mut director = CutsceneDirector::new("cutscene_01", 30.0);
2985    director.add_marker(5.0, "action_start");
2986    director.add_marker(20.0, "climax");
2987    director.add_marker(28.0, "fade_out");
2988
2989    let mut sound_track = SoundTrack::new("music");
2990    sound_track.add_cue(SoundCue::new(1, "music_main", SoundCueType::Music, 0.0, 30.0));
2991    director.add_sound_track(sound_track);
2992
2993    director.vfx_layer.add_binding(VfxBinding::new(1, VfxEffectType::FirePlume, "root", 10.0));
2994
2995    director.play();
2996    director.advance(5.0);
2997    assert!((director.current_time_s - 5.0).abs() < 0.01);
2998    assert!((director.progress_pct() - 16.67).abs() < 0.1);
2999
3000    let markers = director.markers_in_range(0.0, 10.0);
3001    assert_eq!(markers.len(), 1);
3002
3003    director.seek(0.0);
3004    assert!((director.current_time_s - 0.0).abs() < 0.01);
3005    assert!(director.render_pass_count() >= 8);
3006}
3007
3008pub fn run_retarget_tests() {
3009    let profile = RetargetProfile::humanoid_biped();
3010    assert!(profile.mapping_count() >= 20);
3011    let mapping = profile.find_mapping("Hips");
3012    assert!(mapping.is_some());
3013    assert_eq!(mapping.unwrap().target_bone, "pelvis");
3014}
3015
3016pub fn run_all_cutscene_post_tests() {
3017    run_post_process_tests();
3018    run_usd_stage_tests();
3019    run_alembic_tests();
3020    run_quantized_anim_tests();
3021    run_vfx_tests();
3022    run_sound_cue_tests();
3023    run_render_export_tests();
3024    run_facial_capture_tests();
3025    run_light_anim_tests();
3026    run_director_tests();
3027    run_retarget_tests();
3028}
3029
3030pub const CUTSCENE_MAX_TRACKS: usize = 64;
3031pub const CUTSCENE_MAX_SOUND_CUES: usize = 256;
3032pub const FACIAL_CAPTURE_BLEND_SHAPES: usize = 52;
3033pub const USD_MAX_PRIMS: usize = 100_000;
3034pub const ALEMBIC_MAX_SAMPLES_PER_OBJECT: usize = 10_000;
3035pub const RENDER_MAX_AOVS: usize = 32;
3036
3037pub fn cutscene_importer_info_extended() -> HashMap<String, String> {
3038    let mut info = HashMap::new();
3039    info.insert("module".to_string(), "cutscene_importer".to_string());
3040    info.insert("version".to_string(), "2.0.0".to_string());
3041    info.insert("blend_shapes".to_string(), FACIAL_CAPTURE_BLEND_SHAPES.to_string());
3042    info.insert("max_tracks".to_string(), CUTSCENE_MAX_TRACKS.to_string());
3043    info.insert("usd_support".to_string(), "USDA 1.0".to_string());
3044    info.insert("alembic_support".to_string(), "Alembic 1.7+".to_string());
3045    info.insert("quantization".to_string(), "16-bit quaternion/translation".to_string());
3046    info
3047}
3048
3049
3050// ============================================================
3051// CINEMATIC CAMERA SYSTEM
3052// ============================================================
3053
3054#[derive(Debug, Clone)]
3055pub struct CameraAperture {
3056    pub f_stop: f32,
3057    pub focal_length_mm: f32,
3058    pub sensor_width_mm: f32,
3059    pub sensor_height_mm: f32,
3060    pub focus_distance: f32,
3061    pub dof_enabled: bool,
3062    pub bokeh_shape_sides: u32,
3063}
3064
3065impl CameraAperture {
3066    pub fn new_cinema() -> Self {
3067        CameraAperture {
3068            f_stop: 2.8,
3069            focal_length_mm: 35.0,
3070            sensor_width_mm: 36.0,
3071            sensor_height_mm: 24.0,
3072            focus_distance: 5.0,
3073            dof_enabled: true,
3074            bokeh_shape_sides: 6,
3075        }
3076    }
3077
3078    pub fn field_of_view_horizontal(&self) -> f32 {
3079        2.0 * ((self.sensor_width_mm / (2.0 * self.focal_length_mm)).atan())
3080    }
3081
3082    pub fn field_of_view_vertical(&self) -> f32 {
3083        2.0 * ((self.sensor_height_mm / (2.0 * self.focal_length_mm)).atan())
3084    }
3085
3086    pub fn circle_of_confusion(&self, subject_distance: f32) -> f32 {
3087        let magnification = self.focal_length_mm / (subject_distance * 1000.0 - self.focal_length_mm);
3088        magnification * self.focal_length_mm / self.f_stop
3089    }
3090
3091    pub fn depth_of_field_near(&self) -> f32 {
3092        let fd = self.focus_distance * 1000.0;
3093        let coc = 0.03;
3094        let hyperfocal = (self.focal_length_mm * self.focal_length_mm) / (self.f_stop * coc) + self.focal_length_mm;
3095        let near = fd * (hyperfocal - self.focal_length_mm) / (hyperfocal + fd - 2.0 * self.focal_length_mm);
3096        near / 1000.0
3097    }
3098
3099    pub fn depth_of_field_far(&self) -> f32 {
3100        let fd = self.focus_distance * 1000.0;
3101        let coc = 0.03;
3102        let hyperfocal = (self.focal_length_mm * self.focal_length_mm) / (self.f_stop * coc) + self.focal_length_mm;
3103        let far = fd * (hyperfocal - self.focal_length_mm) / (hyperfocal - fd);
3104        if far < 0.0 { f32::INFINITY } else { far / 1000.0 }
3105    }
3106}
3107
3108#[derive(Debug, Clone)]
3109pub struct CameraMotionBlur {
3110    pub shutter_angle_degrees: f32,
3111    pub sample_count: u32,
3112    pub enabled: bool,
3113}
3114
3115impl CameraMotionBlur {
3116    pub fn new() -> Self {
3117        CameraMotionBlur { shutter_angle_degrees: 180.0, sample_count: 16, enabled: true }
3118    }
3119
3120    pub fn shutter_speed_fraction(&self, fps: f32) -> f32 {
3121        fps / (360.0 / self.shutter_angle_degrees)
3122    }
3123}
3124
3125#[derive(Debug, Clone)]
3126pub struct CameraLensFlare {
3127    pub enabled: bool,
3128    pub intensity: f32,
3129    pub streak_count: u32,
3130    pub streak_rotation_deg: f32,
3131    pub ghost_count: u32,
3132    pub halo_radius: f32,
3133    pub dirt_texture_intensity: f32,
3134}
3135
3136impl Default for CameraLensFlare {
3137    fn default() -> Self {
3138        CameraLensFlare {
3139            enabled: false,
3140            intensity: 0.5,
3141            streak_count: 8,
3142            streak_rotation_deg: 45.0,
3143            ghost_count: 4,
3144            halo_radius: 0.15,
3145            dirt_texture_intensity: 0.2,
3146        }
3147    }
3148}
3149
3150#[derive(Debug, Clone)]
3151pub struct CameraVignette {
3152    pub enabled: bool,
3153    pub intensity: f32,
3154    pub smoothness: f32,
3155    pub roundness: f32,
3156    pub center: Vec2,
3157}
3158
3159impl Default for CameraVignette {
3160    fn default() -> Self {
3161        CameraVignette {
3162            enabled: false,
3163            intensity: 0.4,
3164            smoothness: 0.4,
3165            roundness: 1.0,
3166            center: Vec2::new(0.5, 0.5),
3167        }
3168    }
3169}
3170
3171#[derive(Debug, Clone)]
3172pub struct ChromaticAberration {
3173    pub enabled: bool,
3174    pub intensity: f32,
3175    pub fast_mode: bool,
3176}
3177
3178impl Default for ChromaticAberration {
3179    fn default() -> Self {
3180        ChromaticAberration { enabled: false, intensity: 0.05, fast_mode: false }
3181    }
3182}
3183
3184#[derive(Debug, Clone)]
3185pub struct CinematicCamera {
3186    pub id: u32,
3187    pub name: String,
3188    pub position: Vec3,
3189    pub rotation: Quat,
3190    pub aperture: CameraAperture,
3191    pub motion_blur: CameraMotionBlur,
3192    pub lens_flare: CameraLensFlare,
3193    pub vignette: CameraVignette,
3194    pub chromatic_aberration: ChromaticAberration,
3195    pub near_clip: f32,
3196    pub far_clip: f32,
3197    pub aspect_ratio: f32,
3198}
3199
3200impl CinematicCamera {
3201    pub fn new(id: u32, name: &str) -> Self {
3202        CinematicCamera {
3203            id,
3204            name: name.to_string(),
3205            position: Vec3::ZERO,
3206            rotation: Quat::IDENTITY,
3207            aperture: CameraAperture::new_cinema(),
3208            motion_blur: CameraMotionBlur::new(),
3209            lens_flare: CameraLensFlare::default(),
3210            vignette: CameraVignette::default(),
3211            chromatic_aberration: ChromaticAberration::default(),
3212            near_clip: 0.1,
3213            far_clip: 10000.0,
3214            aspect_ratio: 16.0 / 9.0,
3215        }
3216    }
3217
3218    pub fn view_matrix(&self) -> Mat4 {
3219        let rot = Mat4::from_quat(self.rotation);
3220        let trans = Mat4::from_translation(-self.position);
3221        rot.transpose() * trans
3222    }
3223
3224    pub fn projection_matrix(&self) -> Mat4 {
3225        let fov_y = self.aperture.field_of_view_vertical();
3226        Mat4::perspective_rh(fov_y, self.aspect_ratio, self.near_clip, self.far_clip)
3227    }
3228
3229    pub fn forward(&self) -> Vec3 {
3230        self.rotation * Vec3::NEG_Z
3231    }
3232
3233    pub fn right(&self) -> Vec3 {
3234        self.rotation * Vec3::X
3235    }
3236
3237    pub fn up(&self) -> Vec3 {
3238        self.rotation * Vec3::Y
3239    }
3240}
3241
3242#[derive(Debug, Clone)]
3243pub struct ExtCameraKeyframe {
3244    pub time: f32,
3245    pub position: Vec3,
3246    pub rotation: Quat,
3247    pub focal_length: f32,
3248    pub f_stop: f32,
3249    pub focus_distance: f32,
3250}
3251
3252#[derive(Debug, Clone)]
3253pub struct CameraAnimTrack {
3254    pub camera_id: u32,
3255    pub keyframes: Vec<ExtCameraKeyframe>,
3256}
3257
3258impl CameraAnimTrack {
3259    pub fn new(camera_id: u32) -> Self {
3260        CameraAnimTrack { camera_id, keyframes: Vec::new() }
3261    }
3262
3263    pub fn add_keyframe(&mut self, kf: ExtCameraKeyframe) {
3264        let idx = self.keyframes.partition_point(|k| k.time < kf.time);
3265        self.keyframes.insert(idx, kf);
3266    }
3267
3268    pub fn sample(&self, t: f32) -> Option<ExtCameraKeyframe> {
3269        if self.keyframes.is_empty() { return None; }
3270        if t <= self.keyframes[0].time {
3271            return Some(self.keyframes[0].clone());
3272        }
3273        let last = self.keyframes.last().unwrap();
3274        if t >= last.time {
3275            return Some(last.clone());
3276        }
3277        let idx = self.keyframes.partition_point(|k| k.time <= t) - 1;
3278        let a = &self.keyframes[idx];
3279        let b = &self.keyframes[idx + 1];
3280        let s = (t - a.time) / (b.time - a.time);
3281        Some(ExtCameraKeyframe {
3282            time: t,
3283            position: a.position.lerp(b.position, s),
3284            rotation: a.rotation.slerp(b.rotation, s),
3285            focal_length: a.focal_length + (b.focal_length - a.focal_length) * s,
3286            f_stop: a.f_stop + (b.f_stop - a.f_stop) * s,
3287            focus_distance: a.focus_distance + (b.focus_distance - a.focus_distance) * s,
3288        })
3289    }
3290
3291    pub fn duration(&self) -> f32 {
3292        self.keyframes.last().map(|k| k.time).unwrap_or(0.0)
3293    }
3294}
3295
3296#[derive(Debug, Clone)]
3297pub struct DollyTrackSegment {
3298    pub start: Vec3,
3299    pub end: Vec3,
3300    pub speed: f32,
3301}
3302
3303#[derive(Debug, Clone)]
3304pub struct CameraRig {
3305    pub rig_type: CameraRigType,
3306    pub position: Vec3,
3307    pub rotation: Quat,
3308    pub arm_length: f32,
3309    pub tilt_angle: f32,
3310    pub pan_angle: f32,
3311    pub noise_seed: u32,
3312    pub noise_intensity: f32,
3313    pub noise_frequency: f32,
3314    pub dolly_segments: Vec<DollyTrackSegment>,
3315    pub dolly_position_t: f32,
3316}
3317
3318impl CameraRig {
3319    pub fn new(rig_type: CameraRigType) -> Self {
3320        CameraRig {
3321            rig_type,
3322            position: Vec3::ZERO,
3323            rotation: Quat::IDENTITY,
3324            arm_length: 1.0,
3325            tilt_angle: 0.0,
3326            pan_angle: 0.0,
3327            noise_seed: 42,
3328            noise_intensity: 0.01,
3329            noise_frequency: 2.0,
3330            dolly_segments: Vec::new(),
3331            dolly_position_t: 0.0,
3332        }
3333    }
3334
3335    pub fn handhold_noise(&self, t: f32) -> Vec3 {
3336        let s = self.noise_seed as f32 * 0.001;
3337        let x = (t * self.noise_frequency + s).sin() * self.noise_intensity;
3338        let y = (t * self.noise_frequency * 1.3 + s + 1.0).sin() * self.noise_intensity * 0.5;
3339        let z = (t * self.noise_frequency * 0.7 + s + 2.0).sin() * self.noise_intensity * 0.3;
3340        Vec3::new(x, y, z)
3341    }
3342
3343    pub fn crane_tip_position(&self) -> Vec3 {
3344        let arm_dir = self.rotation * Vec3::new(self.pan_angle.sin(), self.tilt_angle.sin(), self.pan_angle.cos());
3345        self.position + arm_dir * self.arm_length
3346    }
3347
3348    pub fn add_dolly_segment(&mut self, seg: DollyTrackSegment) {
3349        self.dolly_segments.push(seg);
3350    }
3351
3352    pub fn dolly_total_length(&self) -> f32 {
3353        self.dolly_segments.iter().map(|s| (s.end - s.start).length()).sum()
3354    }
3355}
3356
3357// ============================================================
3358// SUBTITLE AND LOCALIZATION SYSTEM
3359// ============================================================
3360
3361#[derive(Debug, Clone, PartialEq)]
3362pub enum SubtitleAlignment {
3363    BottomCenter,
3364    BottomLeft,
3365    BottomRight,
3366    TopCenter,
3367    MiddleCenter,
3368}
3369
3370#[derive(Debug, Clone)]
3371pub struct SubtitleStyle {
3372    pub font_size: f32,
3373    pub color: Vec4,
3374    pub outline_color: Vec4,
3375    pub outline_width: f32,
3376    pub background_color: Vec4,
3377    pub background_enabled: bool,
3378    pub alignment: SubtitleAlignment,
3379    pub vertical_position: f32,
3380}
3381
3382impl Default for SubtitleStyle {
3383    fn default() -> Self {
3384        SubtitleStyle {
3385            font_size: 24.0,
3386            color: Vec4::new(1.0, 1.0, 1.0, 1.0),
3387            outline_color: Vec4::new(0.0, 0.0, 0.0, 1.0),
3388            outline_width: 1.5,
3389            background_color: Vec4::new(0.0, 0.0, 0.0, 0.5),
3390            background_enabled: false,
3391            alignment: SubtitleAlignment::BottomCenter,
3392            vertical_position: 0.1,
3393        }
3394    }
3395}
3396
3397#[derive(Debug, Clone)]
3398pub struct SubtitleCue {
3399    pub id: u32,
3400    pub start_time: f32,
3401    pub end_time: f32,
3402    pub text: String,
3403    pub speaker: Option<String>,
3404    pub style_override: Option<SubtitleStyle>,
3405}
3406
3407impl SubtitleCue {
3408    pub fn duration(&self) -> f32 {
3409        self.end_time - self.start_time
3410    }
3411
3412    pub fn is_active_at(&self, t: f32) -> bool {
3413        t >= self.start_time && t < self.end_time
3414    }
3415
3416    pub fn fade_alpha(&self, t: f32, fade_in: f32, fade_out: f32) -> f32 {
3417        if t < self.start_time + fade_in {
3418            (t - self.start_time) / fade_in.max(0.001)
3419        } else if t > self.end_time - fade_out {
3420            (self.end_time - t) / fade_out.max(0.001)
3421        } else {
3422            1.0
3423        }
3424        .clamp(0.0, 1.0)
3425    }
3426}
3427
3428#[derive(Debug, Clone)]
3429pub struct LocalizedSubtitles {
3430    pub language_code: String,
3431    pub cues: Vec<SubtitleCue>,
3432    pub default_style: SubtitleStyle,
3433    pub metadata: HashMap<String, String>,
3434}
3435
3436impl LocalizedSubtitles {
3437    pub fn new(language_code: &str) -> Self {
3438        LocalizedSubtitles {
3439            language_code: language_code.to_string(),
3440            cues: Vec::new(),
3441            default_style: SubtitleStyle::default(),
3442            metadata: HashMap::new(),
3443        }
3444    }
3445
3446    pub fn add_cue(&mut self, cue: SubtitleCue) {
3447        self.cues.push(cue);
3448        self.cues.sort_by(|a, b| a.start_time.partial_cmp(&b.start_time).unwrap());
3449    }
3450
3451    pub fn cues_at(&self, t: f32) -> Vec<&SubtitleCue> {
3452        self.cues.iter().filter(|c| c.is_active_at(t)).collect()
3453    }
3454
3455    pub fn export_srt(&self) -> String {
3456        let mut out = String::new();
3457        for (i, cue) in self.cues.iter().enumerate() {
3458            let fmt_time = |s: f32| -> String {
3459                let h = (s / 3600.0) as u32;
3460                let m = ((s % 3600.0) / 60.0) as u32;
3461                let sec = (s % 60.0) as u32;
3462                let ms = ((s % 1.0) * 1000.0) as u32;
3463                format!("{:02}:{:02}:{:02},{:03}", h, m, sec, ms)
3464            };
3465            out.push_str(&format!("{}\n{} --> {}\n{}\n\n",
3466                i + 1,
3467                fmt_time(cue.start_time),
3468                fmt_time(cue.end_time),
3469                cue.text));
3470        }
3471        out
3472    }
3473
3474    pub fn export_vtt(&self) -> String {
3475        let mut out = String::from("WEBVTT\n\n");
3476        for cue in &self.cues {
3477            let fmt_time = |s: f32| -> String {
3478                let h = (s / 3600.0) as u32;
3479                let m = ((s % 3600.0) / 60.0) as u32;
3480                let sec = (s % 60.0) as u32;
3481                let ms = ((s % 1.0) * 1000.0) as u32;
3482                format!("{:02}:{:02}:{:02}.{:03}", h, m, sec, ms)
3483            };
3484            out.push_str(&format!("{} --> {}\n{}\n\n",
3485                fmt_time(cue.start_time),
3486                fmt_time(cue.end_time),
3487                cue.text));
3488        }
3489        out
3490    }
3491}
3492
3493#[derive(Debug, Clone)]
3494pub struct SubtitleManager {
3495    pub tracks: HashMap<String, LocalizedSubtitles>,
3496    pub active_language: String,
3497    pub fade_in_duration: f32,
3498    pub fade_out_duration: f32,
3499}
3500
3501impl SubtitleManager {
3502    pub fn new(default_lang: &str) -> Self {
3503        SubtitleManager {
3504            tracks: HashMap::new(),
3505            active_language: default_lang.to_string(),
3506            fade_in_duration: 0.1,
3507            fade_out_duration: 0.1,
3508        }
3509    }
3510
3511    pub fn add_language(&mut self, lang: LocalizedSubtitles) {
3512        self.tracks.insert(lang.language_code.clone(), lang);
3513    }
3514
3515    pub fn active_cues(&self, t: f32) -> Vec<&SubtitleCue> {
3516        self.tracks.get(&self.active_language)
3517            .map(|l| l.cues_at(t))
3518            .unwrap_or_default()
3519    }
3520}
3521
3522// ============================================================
3523// NARRATIVE EVENT SYSTEM
3524// ============================================================
3525
3526#[derive(Debug, Clone, PartialEq)]
3527pub enum NarrativeEventType {
3528    DialogueLine,
3529    CharacterEnter,
3530    CharacterExit,
3531    EnvironmentChange,
3532    MusicChange,
3533    SfxTrigger,
3534    CameraChange,
3535    FlagSet,
3536    QuestUpdate,
3537    ItemGrant,
3538    SceneTransition,
3539}
3540
3541#[derive(Debug, Clone)]
3542pub struct NarrativeEvent {
3543    pub id: u32,
3544    pub event_type: NarrativeEventType,
3545    pub trigger_time: f32,
3546    pub payload: HashMap<String, String>,
3547    pub conditions: Vec<String>,
3548    pub fired: bool,
3549}
3550
3551impl NarrativeEvent {
3552    pub fn new(id: u32, event_type: NarrativeEventType, trigger_time: f32) -> Self {
3553        NarrativeEvent {
3554            id,
3555            event_type,
3556            trigger_time,
3557            payload: HashMap::new(),
3558            conditions: Vec::new(),
3559            fired: false,
3560        }
3561    }
3562
3563    pub fn with_payload(mut self, key: &str, value: &str) -> Self {
3564        self.payload.insert(key.to_string(), value.to_string());
3565        self
3566    }
3567}
3568
3569#[derive(Debug, Clone)]
3570pub struct NarrativeSequencer {
3571    pub events: Vec<NarrativeEvent>,
3572    pub global_flags: HashMap<String, bool>,
3573    pub event_log: VecDeque<(f32, u32)>,
3574    pub max_log_entries: usize,
3575}
3576
3577impl NarrativeSequencer {
3578    pub fn new() -> Self {
3579        NarrativeSequencer {
3580            events: Vec::new(),
3581            global_flags: HashMap::new(),
3582            event_log: VecDeque::new(),
3583            max_log_entries: 256,
3584        }
3585    }
3586
3587    pub fn add_event(&mut self, ev: NarrativeEvent) {
3588        self.events.push(ev);
3589        self.events.sort_by(|a, b| a.trigger_time.partial_cmp(&b.trigger_time).unwrap());
3590    }
3591
3592    pub fn tick(&mut self, t: f32) -> Vec<u32> {
3593        let mut fired_ids = Vec::new();
3594        for ev in self.events.iter_mut() {
3595            if !ev.fired && t >= ev.trigger_time {
3596                let conds_met = ev.conditions.iter().all(|c| {
3597                    self.global_flags.get(c).copied().unwrap_or(false)
3598                });
3599                if conds_met {
3600                    ev.fired = true;
3601                    fired_ids.push(ev.id);
3602                    if self.event_log.len() >= self.max_log_entries {
3603                        self.event_log.pop_front();
3604                    }
3605                    self.event_log.push_back((t, ev.id));
3606                }
3607            }
3608        }
3609        fired_ids
3610    }
3611
3612    pub fn set_flag(&mut self, flag: &str, value: bool) {
3613        self.global_flags.insert(flag.to_string(), value);
3614    }
3615
3616    pub fn reset(&mut self) {
3617        for ev in self.events.iter_mut() {
3618            ev.fired = false;
3619        }
3620    }
3621
3622    pub fn unfired_count(&self) -> usize {
3623        self.events.iter().filter(|e| !e.fired).count()
3624    }
3625}
3626
3627// ============================================================
3628// CHARACTER DIALOGUE SYSTEM
3629// ============================================================
3630
3631#[derive(Debug, Clone)]
3632pub struct DialogueLine {
3633    pub id: u32,
3634    pub speaker_id: String,
3635    pub text: String,
3636    pub emotion: String,
3637    pub audio_asset: Option<String>,
3638    pub duration_hint: f32,
3639    pub lip_sync_asset: Option<String>,
3640}
3641
3642#[derive(Debug, Clone)]
3643pub struct DialogueChoice {
3644    pub id: u32,
3645    pub text: String,
3646    pub next_node_id: u32,
3647    pub conditions: Vec<String>,
3648    pub consequences: Vec<String>,
3649}
3650
3651#[derive(Debug, Clone)]
3652pub enum DialogueNodeContent {
3653    Line(DialogueLine),
3654    Choice(Vec<DialogueChoice>),
3655    Branch { condition: String, true_node: u32, false_node: u32 },
3656    End,
3657}
3658
3659#[derive(Debug, Clone)]
3660pub struct DialogueNode {
3661    pub id: u32,
3662    pub content: DialogueNodeContent,
3663    pub next_id: Option<u32>,
3664}
3665
3666#[derive(Debug, Clone)]
3667pub struct DialogueGraph {
3668    pub id: u32,
3669    pub name: String,
3670    pub nodes: HashMap<u32, DialogueNode>,
3671    pub start_node_id: u32,
3672}
3673
3674impl DialogueGraph {
3675    pub fn new(id: u32, name: &str, start_node_id: u32) -> Self {
3676        DialogueGraph { id, name: name.to_string(), nodes: HashMap::new(), start_node_id }
3677    }
3678
3679    pub fn add_node(&mut self, node: DialogueNode) {
3680        self.nodes.insert(node.id, node);
3681    }
3682
3683    pub fn node_count(&self) -> usize {
3684        self.nodes.len()
3685    }
3686
3687    pub fn validate(&self) -> Vec<String> {
3688        let mut errors = Vec::new();
3689        if !self.nodes.contains_key(&self.start_node_id) {
3690            errors.push(format!("Start node {} not found", self.start_node_id));
3691        }
3692        for (id, node) in &self.nodes {
3693            if let Some(next) = node.next_id {
3694                if !self.nodes.contains_key(&next) {
3695                    errors.push(format!("Node {} references missing next {}", id, next));
3696                }
3697            }
3698        }
3699        errors
3700    }
3701}
3702
3703// ============================================================
3704// ANIMATION RETARGETING EXTENDED
3705// ============================================================
3706
3707#[derive(Debug, Clone)]
3708pub struct ExtIkChain {
3709    pub name: String,
3710    pub root_bone: String,
3711    pub end_effector_bone: String,
3712    pub intermediate_bones: Vec<String>,
3713    pub pole_vector: Vec3,
3714    pub weight: f32,
3715    pub iterations: u32,
3716}
3717
3718impl ExtIkChain {
3719    pub fn new_arm_left() -> Self {
3720        ExtIkChain {
3721            name: "LeftArm".to_string(),
3722            root_bone: "LeftShoulder".to_string(),
3723            end_effector_bone: "LeftHand".to_string(),
3724            intermediate_bones: vec!["LeftUpperArm".to_string(), "LeftForeArm".to_string()],
3725            pole_vector: Vec3::new(-1.0, 0.0, -1.0),
3726            weight: 1.0,
3727            iterations: 10,
3728        }
3729    }
3730
3731    pub fn new_leg_right() -> Self {
3732        ExtIkChain {
3733            name: "RightLeg".to_string(),
3734            root_bone: "RightUpperLeg".to_string(),
3735            end_effector_bone: "RightFoot".to_string(),
3736            intermediate_bones: vec!["RightLowerLeg".to_string()],
3737            pole_vector: Vec3::new(0.0, 0.0, 1.0),
3738            weight: 1.0,
3739            iterations: 10,
3740        }
3741    }
3742
3743    pub fn chain_length(&self) -> usize {
3744        2 + self.intermediate_bones.len()
3745    }
3746}
3747
3748#[derive(Debug, Clone)]
3749pub struct RetargetingConstraint {
3750    pub source_bone: String,
3751    pub target_bone: String,
3752    pub rotation_only: bool,
3753    pub flip_x: bool,
3754    pub flip_y: bool,
3755    pub flip_z: bool,
3756    pub local_offset_rotation: Quat,
3757}
3758
3759impl RetargetingConstraint {
3760    pub fn new(source: &str, target: &str) -> Self {
3761        RetargetingConstraint {
3762            source_bone: source.to_string(),
3763            target_bone: target.to_string(),
3764            rotation_only: false,
3765            flip_x: false, flip_y: false, flip_z: false,
3766            local_offset_rotation: Quat::IDENTITY,
3767        }
3768    }
3769}
3770
3771#[derive(Debug, Clone)]
3772pub struct RetargetSession {
3773    pub source_skeleton: String,
3774    pub target_skeleton: String,
3775    pub constraints: Vec<RetargetingConstraint>,
3776    pub ik_chains: Vec<ExtIkChain>,
3777    pub global_scale: f32,
3778    pub hip_height_correction: f32,
3779}
3780
3781impl RetargetSession {
3782    pub fn new(source: &str, target: &str) -> Self {
3783        RetargetSession {
3784            source_skeleton: source.to_string(),
3785            target_skeleton: target.to_string(),
3786            constraints: Vec::new(),
3787            ik_chains: Vec::new(),
3788            global_scale: 1.0,
3789            hip_height_correction: 0.0,
3790        }
3791    }
3792
3793    pub fn add_constraint(&mut self, c: RetargetingConstraint) {
3794        self.constraints.push(c);
3795    }
3796
3797    pub fn add_ik_chain(&mut self, chain: ExtIkChain) {
3798        self.ik_chains.push(chain);
3799    }
3800
3801    pub fn humanoid_default(source: &str, target: &str) -> Self {
3802        let mut session = RetargetSession::new(source, target);
3803        let bone_pairs = [
3804            ("Hips", "Hips"), ("Spine", "Spine"), ("Chest", "Chest"),
3805            ("UpperChest", "UpperChest"), ("Neck", "Neck"), ("Head", "Head"),
3806            ("LeftShoulder", "LeftShoulder"), ("LeftUpperArm", "LeftUpperArm"),
3807            ("LeftForeArm", "LeftForeArm"), ("LeftHand", "LeftHand"),
3808            ("RightShoulder", "RightShoulder"), ("RightUpperArm", "RightUpperArm"),
3809            ("RightForeArm", "RightForeArm"), ("RightHand", "RightHand"),
3810            ("LeftUpperLeg", "LeftUpperLeg"), ("LeftLowerLeg", "LeftLowerLeg"),
3811            ("LeftFoot", "LeftFoot"), ("RightUpperLeg", "RightUpperLeg"),
3812            ("RightLowerLeg", "RightLowerLeg"), ("RightFoot", "RightFoot"),
3813        ];
3814        for (src, tgt) in &bone_pairs {
3815            session.add_constraint(RetargetingConstraint::new(src, tgt));
3816        }
3817        session.add_ik_chain(ExtIkChain::new_arm_left());
3818        session.add_ik_chain(ExtIkChain::new_leg_right());
3819        session
3820    }
3821}
3822
3823// ============================================================
3824// TIMELINE CLIP OPERATIONS
3825// ============================================================
3826
3827#[derive(Debug, Clone, PartialEq)]
3828pub enum ClipEditOperation {
3829    Trim,
3830    Split,
3831    Merge,
3832    SpeedChange,
3833    Reverse,
3834    Slip,
3835}
3836
3837#[derive(Debug, Clone)]
3838pub struct ClipEditHistory {
3839    pub clip_id: u32,
3840    pub operation: ClipEditOperation,
3841    pub before_start: f32,
3842    pub before_end: f32,
3843    pub after_start: f32,
3844    pub after_end: f32,
3845    pub timestamp: f32,
3846}
3847
3848#[derive(Debug, Clone)]
3849pub struct TimelineClipEditor {
3850    pub selected_clip_ids: HashSet<u32>,
3851    pub edit_history: VecDeque<ClipEditHistory>,
3852    pub max_history: usize,
3853    pub snapping_enabled: bool,
3854    pub snap_threshold: f32,
3855    pub ripple_edit_enabled: bool,
3856}
3857
3858impl TimelineClipEditor {
3859    pub fn new() -> Self {
3860        TimelineClipEditor {
3861            selected_clip_ids: HashSet::new(),
3862            edit_history: VecDeque::new(),
3863            max_history: 100,
3864            snapping_enabled: true,
3865            snap_threshold: 0.05,
3866            ripple_edit_enabled: false,
3867        }
3868    }
3869
3870    pub fn select_clip(&mut self, clip_id: u32) {
3871        self.selected_clip_ids.insert(clip_id);
3872    }
3873
3874    pub fn deselect_all(&mut self) {
3875        self.selected_clip_ids.clear();
3876    }
3877
3878    pub fn record_edit(&mut self, history: ClipEditHistory) {
3879        if self.edit_history.len() >= self.max_history {
3880            self.edit_history.pop_front();
3881        }
3882        self.edit_history.push_back(history);
3883    }
3884
3885    pub fn snap_to_nearest(&self, time: f32, snap_points: &[f32]) -> f32 {
3886        if !self.snapping_enabled { return time; }
3887        snap_points.iter()
3888            .min_by(|&&a, &&b| (a - time).abs().partial_cmp(&(b - time).abs()).unwrap())
3889            .copied()
3890            .filter(|&s| (s - time).abs() <= self.snap_threshold)
3891            .unwrap_or(time)
3892    }
3893
3894    pub fn undo_last(&mut self) -> Option<ClipEditHistory> {
3895        self.edit_history.pop_back()
3896    }
3897}
3898
3899// ============================================================
3900// SCENE COMPOSITION AND LAYERING
3901// ============================================================
3902
3903#[derive(Debug, Clone, PartialEq)]
3904pub enum LayerBlendMode {
3905    Normal,
3906    Additive,
3907    Multiply,
3908    Screen,
3909    Overlay,
3910}
3911
3912#[derive(Debug, Clone)]
3913pub struct SceneLayer {
3914    pub id: u32,
3915    pub name: String,
3916    pub visible: bool,
3917    pub locked: bool,
3918    pub opacity: f32,
3919    pub blend_mode: LayerBlendMode,
3920    pub clip_ids: Vec<u32>,
3921    pub color_tag: Vec4,
3922}
3923
3924impl SceneLayer {
3925    pub fn new(id: u32, name: &str) -> Self {
3926        SceneLayer {
3927            id,
3928            name: name.to_string(),
3929            visible: true,
3930            locked: false,
3931            opacity: 1.0,
3932            blend_mode: LayerBlendMode::Normal,
3933            clip_ids: Vec::new(),
3934            color_tag: Vec4::new(0.5, 0.5, 0.5, 1.0),
3935        }
3936    }
3937
3938    pub fn add_clip(&mut self, clip_id: u32) {
3939        if !self.clip_ids.contains(&clip_id) {
3940            self.clip_ids.push(clip_id);
3941        }
3942    }
3943
3944    pub fn remove_clip(&mut self, clip_id: u32) {
3945        self.clip_ids.retain(|&id| id != clip_id);
3946    }
3947}
3948
3949#[derive(Debug, Clone)]
3950pub struct LayerStack {
3951    pub layers: Vec<SceneLayer>,
3952    pub active_layer_id: u32,
3953}
3954
3955impl LayerStack {
3956    pub fn new() -> Self {
3957        LayerStack { layers: Vec::new(), active_layer_id: 0 }
3958    }
3959
3960    pub fn add_layer(&mut self, layer: SceneLayer) {
3961        self.layers.push(layer);
3962    }
3963
3964    pub fn get_layer_mut(&mut self, id: u32) -> Option<&mut SceneLayer> {
3965        self.layers.iter_mut().find(|l| l.id == id)
3966    }
3967
3968    pub fn visible_layers(&self) -> Vec<&SceneLayer> {
3969        self.layers.iter().filter(|l| l.visible).collect()
3970    }
3971
3972    pub fn move_layer_up(&mut self, id: u32) {
3973        if let Some(idx) = self.layers.iter().position(|l| l.id == id) {
3974            if idx + 1 < self.layers.len() {
3975                self.layers.swap(idx, idx + 1);
3976            }
3977        }
3978    }
3979
3980    pub fn move_layer_down(&mut self, id: u32) {
3981        if let Some(idx) = self.layers.iter().position(|l| l.id == id) {
3982            if idx > 0 {
3983                self.layers.swap(idx, idx - 1);
3984            }
3985        }
3986    }
3987}
3988
3989// ============================================================
3990// EXPRESSION SOLVER FOR PROCEDURAL ANIMATION
3991// ============================================================
3992
3993#[derive(Debug, Clone)]
3994pub enum ExprToken {
3995    Number(f32),
3996    Variable(String),
3997    Plus, Minus, Multiply, Divide,
3998    LParen, RParen,
3999    Sin, Cos, Abs, Floor, Ceil, Sqrt,
4000}
4001
4002#[derive(Debug, Clone)]
4003pub struct ExpressionSolver {
4004    pub variables: HashMap<String, f32>,
4005}
4006
4007impl ExpressionSolver {
4008    pub fn new() -> Self {
4009        ExpressionSolver { variables: HashMap::new() }
4010    }
4011
4012    pub fn set_var(&mut self, name: &str, value: f32) {
4013        self.variables.insert(name.to_string(), value);
4014    }
4015
4016    pub fn evaluate_simple(&self, tokens: &[ExprToken]) -> f32 {
4017        // Simple left-to-right evaluation without precedence for demonstration
4018        if tokens.is_empty() { return 0.0; }
4019        let mut result = self.eval_primary(tokens, &mut 0);
4020        result
4021    }
4022
4023    fn eval_primary(&self, tokens: &[ExprToken], pos: &mut usize) -> f32 {
4024        if *pos >= tokens.len() { return 0.0; }
4025        match &tokens[*pos] {
4026            ExprToken::Number(n) => { *pos += 1; *n },
4027            ExprToken::Variable(v) => {
4028                *pos += 1;
4029                self.variables.get(v).copied().unwrap_or(0.0)
4030            },
4031            ExprToken::Sin => {
4032                *pos += 1;
4033                let arg = self.eval_primary(tokens, pos);
4034                arg.sin()
4035            },
4036            ExprToken::Cos => {
4037                *pos += 1;
4038                let arg = self.eval_primary(tokens, pos);
4039                arg.cos()
4040            },
4041            ExprToken::Abs => {
4042                *pos += 1;
4043                let arg = self.eval_primary(tokens, pos);
4044                arg.abs()
4045            },
4046            ExprToken::Sqrt => {
4047                *pos += 1;
4048                let arg = self.eval_primary(tokens, pos);
4049                arg.sqrt()
4050            },
4051            _ => 0.0,
4052        }
4053    }
4054}
4055
4056// ============================================================
4057// CUTSCENE COMPRESSION AND STREAMING
4058// ============================================================
4059
4060pub const CUTSCENE_STREAM_CHUNK_BYTES: usize = 65536;
4061pub const CUTSCENE_KEYFRAME_INTERVAL: u32 = 30;
4062pub const CUTSCENE_MAX_AUDIO_CHANNELS: u32 = 8;
4063pub const CUTSCENE_PREVIEW_WIDTH: u32 = 1280;
4064pub const CUTSCENE_PREVIEW_HEIGHT: u32 = 720;
4065pub const CUTSCENE_MASTER_WIDTH: u32 = 3840;
4066pub const CUTSCENE_MASTER_HEIGHT: u32 = 2160;
4067
4068#[derive(Debug, Clone)]
4069pub struct CutsceneStreamChunk {
4070    pub chunk_index: u32,
4071    pub byte_offset: u64,
4072    pub compressed_size: u32,
4073    pub uncompressed_size: u32,
4074    pub is_keyframe_chunk: bool,
4075    pub timestamp_start: f32,
4076    pub timestamp_end: f32,
4077}
4078
4079#[derive(Debug, Clone)]
4080pub struct CutsceneStreamIndex {
4081    pub total_chunks: u32,
4082    pub total_duration: f32,
4083    pub chunks: Vec<CutsceneStreamChunk>,
4084}
4085
4086impl CutsceneStreamIndex {
4087    pub fn new() -> Self {
4088        CutsceneStreamIndex { total_chunks: 0, total_duration: 0.0, chunks: Vec::new() }
4089    }
4090
4091    pub fn add_chunk(&mut self, chunk: CutsceneStreamChunk) {
4092        self.total_chunks += 1;
4093        if chunk.timestamp_end > self.total_duration {
4094            self.total_duration = chunk.timestamp_end;
4095        }
4096        self.chunks.push(chunk);
4097    }
4098
4099    pub fn chunk_for_time(&self, t: f32) -> Option<&CutsceneStreamChunk> {
4100        self.chunks.iter().find(|c| t >= c.timestamp_start && t < c.timestamp_end)
4101    }
4102
4103    pub fn total_compressed_bytes(&self) -> u64 {
4104        self.chunks.iter().map(|c| c.compressed_size as u64).sum()
4105    }
4106
4107    pub fn compression_ratio(&self) -> f32 {
4108        let compressed: u64 = self.chunks.iter().map(|c| c.compressed_size as u64).sum();
4109        let uncompressed: u64 = self.chunks.iter().map(|c| c.uncompressed_size as u64).sum();
4110        if uncompressed == 0 { return 1.0; }
4111        compressed as f32 / uncompressed as f32
4112    }
4113}
4114
4115// ============================================================
4116// SHOT LIST AND PRE-VISUALIZATION
4117// ============================================================
4118
4119#[derive(Debug, Clone, PartialEq)]
4120pub enum ShotType {
4121    ExtremeLongShot,
4122    LongShot,
4123    MediumLongShot,
4124    MediumShot,
4125    MediumCloseUp,
4126    CloseUp,
4127    ExtremeCloseUp,
4128    CutawayShot,
4129    InsertShot,
4130    CutInShot,
4131    PointOfViewShot,
4132    OverTheShoulderShot,
4133    TwoShot,
4134    EnsembleShot,
4135    AerialShot,
4136}
4137
4138#[derive(Debug, Clone, PartialEq)]
4139pub enum CameraAngle {
4140    EyeLevel,
4141    HighAngle,
4142    LowAngle,
4143    BirdsEye,
4144    WormsEye,
4145    DutchAngle,
4146    SubjectivePov,
4147}
4148
4149#[derive(Debug, Clone, PartialEq)]
4150pub enum CameraMovement {
4151    Static,
4152    Pan,
4153    Tilt,
4154    Dolly,
4155    Truck,
4156    Pedestal,
4157    Arc,
4158    Zoom,
4159    Crane,
4160    Handheld,
4161    Steadicam,
4162}
4163
4164#[derive(Debug, Clone)]
4165pub struct ShotListEntry {
4166    pub shot_number: String,
4167    pub scene_number: String,
4168    pub description: String,
4169    pub shot_type: ShotType,
4170    pub camera_angle: CameraAngle,
4171    pub camera_movement: CameraMovement,
4172    pub focal_length: f32,
4173    pub estimated_duration: f32,
4174    pub rig_notes: String,
4175    pub lighting_notes: String,
4176    pub priority: u32,
4177    pub complete: bool,
4178}
4179
4180#[derive(Debug, Clone)]
4181pub struct ShotList {
4182    pub title: String,
4183    pub director: String,
4184    pub dop: String,
4185    pub date: String,
4186    pub entries: Vec<ShotListEntry>,
4187}
4188
4189impl ShotList {
4190    pub fn new(title: &str) -> Self {
4191        ShotList {
4192            title: title.to_string(),
4193            director: String::new(),
4194            dop: String::new(),
4195            date: String::new(),
4196            entries: Vec::new(),
4197        }
4198    }
4199
4200    pub fn add_shot(&mut self, shot: ShotListEntry) {
4201        self.entries.push(shot);
4202    }
4203
4204    pub fn total_duration(&self) -> f32 {
4205        self.entries.iter().map(|e| e.estimated_duration).sum()
4206    }
4207
4208    pub fn incomplete_shots(&self) -> Vec<&ShotListEntry> {
4209        self.entries.iter().filter(|e| !e.complete).collect()
4210    }
4211
4212    pub fn shots_by_scene(&self, scene: &str) -> Vec<&ShotListEntry> {
4213        self.entries.iter().filter(|e| e.scene_number == scene).collect()
4214    }
4215
4216    pub fn export_csv(&self) -> String {
4217        let mut out = String::from("Shot,Scene,Description,Type,Duration,Complete\n");
4218        for e in &self.entries {
4219            out.push_str(&format!("{},{},{},{:?},{:.1},{}\n",
4220                e.shot_number, e.scene_number, e.description,
4221                e.shot_type, e.estimated_duration, e.complete));
4222        }
4223        out
4224    }
4225}
4226
4227// ============================================================
4228// ADVANCED BLEND TREE / STATE MACHINE FOR ANIMATION
4229// ============================================================
4230
4231#[derive(Debug, Clone, PartialEq)]
4232pub enum BlendNodeType {
4233    Clip,
4234    Blend1D,
4235    Blend2D,
4236    Override,
4237    LayeredBlend,
4238    StateMachine,
4239}
4240
4241#[derive(Debug, Clone)]
4242pub struct BlendNode {
4243    pub id: u32,
4244    pub node_type: BlendNodeType,
4245    pub name: String,
4246    pub child_ids: Vec<u32>,
4247    pub blend_param: String,
4248    pub blend_value: f32,
4249    pub clip_asset: Option<String>,
4250    pub loop_clip: bool,
4251    pub play_rate: f32,
4252}
4253
4254impl BlendNode {
4255    pub fn new_clip(id: u32, name: &str, asset: &str) -> Self {
4256        BlendNode {
4257            id,
4258            node_type: BlendNodeType::Clip,
4259            name: name.to_string(),
4260            child_ids: Vec::new(),
4261            blend_param: String::new(),
4262            blend_value: 0.0,
4263            clip_asset: Some(asset.to_string()),
4264            loop_clip: true,
4265            play_rate: 1.0,
4266        }
4267    }
4268
4269    pub fn new_blend1d(id: u32, name: &str, param: &str) -> Self {
4270        BlendNode {
4271            id,
4272            node_type: BlendNodeType::Blend1D,
4273            name: name.to_string(),
4274            child_ids: Vec::new(),
4275            blend_param: param.to_string(),
4276            blend_value: 0.0,
4277            clip_asset: None,
4278            loop_clip: false,
4279            play_rate: 1.0,
4280        }
4281    }
4282}
4283
4284#[derive(Debug, Clone)]
4285pub struct ExtAnimState {
4286    pub id: u32,
4287    pub name: String,
4288    pub root_node_id: u32,
4289    pub speed: f32,
4290    pub loop_state: bool,
4291}
4292
4293#[derive(Debug, Clone)]
4294pub struct AnimationTransition {
4295    pub from_state_id: u32,
4296    pub to_state_id: u32,
4297    pub condition: String,
4298    pub blend_duration: f32,
4299    pub interrupt_current: bool,
4300}
4301
4302#[derive(Debug, Clone)]
4303pub struct ExtAnimStateMachine {
4304    pub name: String,
4305    pub states: HashMap<u32, ExtAnimState>,
4306    pub transitions: Vec<AnimationTransition>,
4307    pub blend_nodes: HashMap<u32, BlendNode>,
4308    pub current_state_id: u32,
4309    pub parameters: HashMap<String, f32>,
4310    pub bool_parameters: HashMap<String, bool>,
4311}
4312
4313impl ExtAnimStateMachine {
4314    pub fn new(name: &str) -> Self {
4315        ExtAnimStateMachine {
4316            name: name.to_string(),
4317            states: HashMap::new(),
4318            transitions: Vec::new(),
4319            blend_nodes: HashMap::new(),
4320            current_state_id: 0,
4321            parameters: HashMap::new(),
4322            bool_parameters: HashMap::new(),
4323        }
4324    }
4325
4326    pub fn add_state(&mut self, state: ExtAnimState) {
4327        self.states.insert(state.id, state);
4328    }
4329
4330    pub fn add_transition(&mut self, t: AnimationTransition) {
4331        self.transitions.push(t);
4332    }
4333
4334    pub fn set_float(&mut self, param: &str, value: f32) {
4335        self.parameters.insert(param.to_string(), value);
4336    }
4337
4338    pub fn set_bool(&mut self, param: &str, value: bool) {
4339        self.bool_parameters.insert(param.to_string(), value);
4340    }
4341
4342    pub fn evaluate_transitions(&self) -> Option<u32> {
4343        for t in &self.transitions {
4344            if t.from_state_id == self.current_state_id {
4345                if self.bool_parameters.get(&t.condition).copied().unwrap_or(false) {
4346                    return Some(t.to_state_id);
4347                }
4348            }
4349        }
4350        None
4351    }
4352
4353    pub fn tick(&mut self) {
4354        if let Some(next_id) = self.evaluate_transitions() {
4355            self.current_state_id = next_id;
4356        }
4357    }
4358}
4359
4360// ============================================================
4361// PROCEDURAL ANIMATION CURVES
4362// ============================================================
4363
4364#[derive(Debug, Clone, PartialEq)]
4365pub enum CurveInterpolation {
4366    Linear,
4367    Hermite,
4368    CatmullRom,
4369    BSpline,
4370    Stepped,
4371}
4372
4373#[derive(Debug, Clone)]
4374pub struct CurveKey {
4375    pub time: f32,
4376    pub value: f32,
4377    pub in_tangent: f32,
4378    pub out_tangent: f32,
4379    pub interp: CurveInterpolation,
4380}
4381
4382#[derive(Debug, Clone)]
4383pub struct AnimationCurve {
4384    pub keys: Vec<CurveKey>,
4385    pub pre_wrap: bool,
4386    pub post_wrap: bool,
4387}
4388
4389impl AnimationCurve {
4390    pub fn new() -> Self {
4391        AnimationCurve { keys: Vec::new(), pre_wrap: false, post_wrap: false }
4392    }
4393
4394    pub fn add_key(&mut self, key: CurveKey) {
4395        let idx = self.keys.partition_point(|k| k.time < key.time);
4396        self.keys.insert(idx, key);
4397    }
4398
4399    pub fn evaluate(&self, t: f32) -> f32 {
4400        if self.keys.is_empty() { return 0.0; }
4401        if t <= self.keys[0].time { return self.keys[0].value; }
4402        let last = self.keys.last().unwrap();
4403        if t >= last.time { return last.value; }
4404
4405        let idx = self.keys.partition_point(|k| k.time <= t) - 1;
4406        let a = &self.keys[idx];
4407        let b = &self.keys[idx + 1];
4408        let dt = b.time - a.time;
4409        let s = (t - a.time) / dt;
4410
4411        match a.interp {
4412            CurveInterpolation::Linear => a.value + (b.value - a.value) * s,
4413            CurveInterpolation::Stepped => a.value,
4414            CurveInterpolation::Hermite => {
4415                let s2 = s * s;
4416                let s3 = s2 * s;
4417                let h00 = 2.0 * s3 - 3.0 * s2 + 1.0;
4418                let h10 = s3 - 2.0 * s2 + s;
4419                let h01 = -2.0 * s3 + 3.0 * s2;
4420                let h11 = s3 - s2;
4421                h00 * a.value + h10 * dt * a.out_tangent + h01 * b.value + h11 * dt * b.in_tangent
4422            },
4423            CurveInterpolation::CatmullRom => {
4424                // simplified
4425                let s2 = s * s;
4426                let s3 = s2 * s;
4427                0.5 * ((2.0 * a.value)
4428                    + (-a.value + b.value) * s
4429                    + (2.0 * a.value - 5.0 * a.value + 4.0 * b.value - b.value) * s2
4430                    + (-a.value + 3.0 * a.value - 3.0 * b.value + b.value) * s3)
4431            },
4432            CurveInterpolation::BSpline => {
4433                // uniform quadratic b-spline basis approximation
4434                let s2 = s * s;
4435                let s3 = s2 * s;
4436                a.value + (b.value - a.value) * (3.0 * s2 - 2.0 * s3)
4437            },
4438        }
4439    }
4440
4441    pub fn duration(&self) -> f32 {
4442        if self.keys.len() < 2 { return 0.0; }
4443        self.keys.last().unwrap().time - self.keys[0].time
4444    }
4445}
4446
4447// ============================================================
4448// TEST FUNCTIONS
4449// ============================================================
4450
4451#[cfg(test)]
4452mod tests_cinematic_camera {
4453    use super::*;
4454
4455    #[test]
4456    fn test_camera_aperture_fov() {
4457        let cam = CameraAperture::new_cinema();
4458        let fov_h = cam.field_of_view_horizontal();
4459        assert!(fov_h > 0.5 && fov_h < 1.5);
4460    }
4461
4462    #[test]
4463    fn test_camera_anim_track_sample() {
4464        let mut track = CameraAnimTrack::new(1);
4465        track.add_keyframe(ExtCameraKeyframe {
4466            time: 0.0, position: Vec3::ZERO, rotation: Quat::IDENTITY,
4467            focal_length: 35.0, f_stop: 2.8, focus_distance: 5.0,
4468        });
4469        track.add_keyframe(ExtCameraKeyframe {
4470            time: 1.0, position: Vec3::new(1.0, 0.0, 0.0), rotation: Quat::IDENTITY,
4471            focal_length: 50.0, f_stop: 4.0, focus_distance: 8.0,
4472        });
4473        let kf = track.sample(0.5).unwrap();
4474        assert!((kf.position.x - 0.5).abs() < 0.01);
4475        assert!((kf.focal_length - 42.5).abs() < 0.1);
4476    }
4477
4478    #[test]
4479    fn test_subtitle_srt_export() {
4480        let mut subs = LocalizedSubtitles::new("en");
4481        subs.add_cue(SubtitleCue {
4482            id: 1, start_time: 0.0, end_time: 2.5,
4483            text: "Hello world".to_string(), speaker: None, style_override: None,
4484        });
4485        let srt = subs.export_srt();
4486        assert!(srt.contains("Hello world"));
4487        assert!(srt.contains("00:00:00,000"));
4488    }
4489
4490    #[test]
4491    fn test_narrative_sequencer() {
4492        let mut seq = NarrativeSequencer::new();
4493        let mut ev = NarrativeEvent::new(1, NarrativeEventType::DialogueLine, 1.0);
4494        seq.add_event(ev);
4495        let fired = seq.tick(0.5);
4496        assert!(fired.is_empty());
4497        let fired = seq.tick(1.5);
4498        assert_eq!(fired.len(), 1);
4499        let fired2 = seq.tick(2.0);
4500        assert!(fired2.is_empty()); // already fired
4501    }
4502
4503    #[test]
4504    fn test_dialogue_graph_validate() {
4505        let mut graph = DialogueGraph::new(1, "Test", 1);
4506        graph.add_node(DialogueNode {
4507            id: 1,
4508            content: DialogueNodeContent::End,
4509            next_id: None,
4510        });
4511        let errors = graph.validate();
4512        assert!(errors.is_empty());
4513    }
4514
4515    #[test]
4516    fn test_blend_state_machine() {
4517        let mut sm = ExtAnimStateMachine::new("Character");
4518        sm.add_state(ExtAnimState { id: 0, name: "Idle".to_string(), root_node_id: 0, speed: 1.0, loop_state: true });
4519        sm.add_state(ExtAnimState { id: 1, name: "Walk".to_string(), root_node_id: 1, speed: 1.0, loop_state: true });
4520        sm.add_transition(AnimationTransition {
4521            from_state_id: 0, to_state_id: 1,
4522            condition: "IsWalking".to_string(),
4523            blend_duration: 0.2, interrupt_current: false,
4524        });
4525        sm.current_state_id = 0;
4526        sm.set_bool("IsWalking", false);
4527        sm.tick();
4528        assert_eq!(sm.current_state_id, 0);
4529        sm.set_bool("IsWalking", true);
4530        sm.tick();
4531        assert_eq!(sm.current_state_id, 1);
4532    }
4533
4534    #[test]
4535    fn test_animation_curve_hermite() {
4536        let mut curve = AnimationCurve::new();
4537        curve.add_key(CurveKey { time: 0.0, value: 0.0, in_tangent: 0.0, out_tangent: 1.0, interp: CurveInterpolation::Hermite });
4538        curve.add_key(CurveKey { time: 1.0, value: 1.0, in_tangent: 1.0, out_tangent: 0.0, interp: CurveInterpolation::Hermite });
4539        let mid = curve.evaluate(0.5);
4540        assert!(mid > 0.4 && mid < 0.6);
4541    }
4542
4543    #[test]
4544    fn test_retarget_session_humanoid() {
4545        let session = RetargetSession::humanoid_default("UE5Mannequin", "ReadyPlayerMe");
4546        assert_eq!(session.constraints.len(), 20);
4547        assert_eq!(session.ik_chains.len(), 2);
4548    }
4549
4550    #[test]
4551    fn test_shot_list_csv() {
4552        let mut sl = ShotList::new("TestScene");
4553        sl.add_shot(ShotListEntry {
4554            shot_number: "1A".to_string(), scene_number: "1".to_string(),
4555            description: "Establishing shot".to_string(),
4556            shot_type: ShotType::LongShot, camera_angle: CameraAngle::EyeLevel,
4557            camera_movement: CameraMovement::Static,
4558            focal_length: 35.0, estimated_duration: 5.0,
4559            rig_notes: String::new(), lighting_notes: String::new(),
4560            priority: 1, complete: false,
4561        });
4562        let csv = sl.export_csv();
4563        assert!(csv.contains("1A"));
4564        assert!(csv.contains("LongShot"));
4565    }
4566
4567    #[test]
4568    fn test_stream_index() {
4569        let mut idx = CutsceneStreamIndex::new();
4570        idx.add_chunk(CutsceneStreamChunk {
4571            chunk_index: 0, byte_offset: 0,
4572            compressed_size: 32768, uncompressed_size: 65536,
4573            is_keyframe_chunk: true, timestamp_start: 0.0, timestamp_end: 1.0,
4574        });
4575        assert!((idx.compression_ratio() - 0.5).abs() < 0.01);
4576        assert!(idx.chunk_for_time(0.5).is_some());
4577    }
4578
4579    #[test]
4580    fn test_camera_rig_handheld_noise() {
4581        let rig = CameraRig::new(CameraRigType::Handheld);
4582        let n1 = rig.handhold_noise(0.0);
4583        let n2 = rig.handhold_noise(0.5);
4584        assert_ne!(n1.x, n2.x);
4585    }
4586}
4587
4588// ============================================================
4589// CUTSCENE PROJECT FILE FORMAT
4590// ============================================================
4591
4592pub const CUTSCENE_FILE_MAGIC: u32 = 0x43545343; // "CTSC"
4593pub const CUTSCENE_FILE_VERSION: u32 = 3;
4594
4595#[derive(Debug, Clone)]
4596pub struct CutsceneFileHeader {
4597    pub magic: u32,
4598    pub version: u32,
4599    pub total_duration: f32,
4600    pub frame_rate: f32,
4601    pub track_count: u32,
4602    pub event_count: u32,
4603    pub subtitle_language_count: u32,
4604    pub created_timestamp: u64,
4605    pub modified_timestamp: u64,
4606    pub author: String,
4607    pub description: String,
4608}
4609
4610impl CutsceneFileHeader {
4611    pub fn new(duration: f32, fps: f32) -> Self {
4612        CutsceneFileHeader {
4613            magic: CUTSCENE_FILE_MAGIC,
4614            version: CUTSCENE_FILE_VERSION,
4615            total_duration: duration,
4616            frame_rate: fps,
4617            track_count: 0,
4618            event_count: 0,
4619            subtitle_language_count: 0,
4620            created_timestamp: 0,
4621            modified_timestamp: 0,
4622            author: String::new(),
4623            description: String::new(),
4624        }
4625    }
4626
4627    pub fn is_valid(&self) -> bool {
4628        self.magic == CUTSCENE_FILE_MAGIC && self.version <= CUTSCENE_FILE_VERSION
4629    }
4630
4631    pub fn total_frames(&self) -> u32 {
4632        (self.total_duration * self.frame_rate).ceil() as u32
4633    }
4634}
4635
4636#[derive(Debug, Clone)]
4637pub struct CutsceneProjectFile {
4638    pub header: CutsceneFileHeader,
4639    pub cameras: Vec<CinematicCamera>,
4640    pub camera_tracks: Vec<CameraAnimTrack>,
4641    pub layer_stack: LayerStack,
4642    pub subtitle_manager: SubtitleManager,
4643    pub narrative_sequencer: NarrativeSequencer,
4644    pub shot_list: ShotList,
4645    pub stream_index: CutsceneStreamIndex,
4646}
4647
4648impl CutsceneProjectFile {
4649    pub fn new(duration: f32, fps: f32) -> Self {
4650        CutsceneProjectFile {
4651            header: CutsceneFileHeader::new(duration, fps),
4652            cameras: Vec::new(),
4653            camera_tracks: Vec::new(),
4654            layer_stack: LayerStack::new(),
4655            subtitle_manager: SubtitleManager::new("en"),
4656            narrative_sequencer: NarrativeSequencer::new(),
4657            shot_list: ShotList::new("Untitled"),
4658            stream_index: CutsceneStreamIndex::new(),
4659        }
4660    }
4661
4662    pub fn add_camera(&mut self, cam: CinematicCamera) {
4663        self.cameras.push(cam);
4664        self.header.track_count += 1;
4665    }
4666
4667    pub fn camera_by_id(&self, id: u32) -> Option<&CinematicCamera> {
4668        self.cameras.iter().find(|c| c.id == id)
4669    }
4670
4671    pub fn summary(&self) -> String {
4672        format!(
4673            "Cutscene '{}' v{}: {:.2}s @ {}fps, {} cameras, {} tracks, {} events",
4674            self.shot_list.title,
4675            self.header.version,
4676            self.header.total_duration,
4677            self.header.frame_rate,
4678            self.cameras.len(),
4679            self.header.track_count,
4680            self.narrative_sequencer.events.len()
4681        )
4682    }
4683}
4684
4685// ============================================================
4686// AUDIO MIX BUS SYSTEM
4687// ============================================================
4688
4689#[derive(Debug, Clone, PartialEq)]
4690pub enum BusType {
4691    Master,
4692    Music,
4693    SFX,
4694    Dialogue,
4695    Ambience,
4696    Aux,
4697}
4698
4699#[derive(Debug, Clone)]
4700pub struct AudioEffect {
4701    pub name: String,
4702    pub enabled: bool,
4703    pub parameters: HashMap<String, f32>,
4704}
4705
4706#[derive(Debug, Clone)]
4707pub struct MixBus {
4708    pub id: u32,
4709    pub name: String,
4710    pub bus_type: BusType,
4711    pub volume: f32,
4712    pub pan: f32,
4713    pub muted: bool,
4714    pub soloed: bool,
4715    pub send_to_master: bool,
4716    pub effects: Vec<AudioEffect>,
4717    pub children: Vec<u32>,
4718}
4719
4720impl MixBus {
4721    pub fn new(id: u32, name: &str, bus_type: BusType) -> Self {
4722        MixBus {
4723            id, name: name.to_string(), bus_type,
4724            volume: 1.0, pan: 0.0, muted: false, soloed: false,
4725            send_to_master: true, effects: Vec::new(), children: Vec::new(),
4726        }
4727    }
4728
4729    pub fn add_effect(&mut self, effect: AudioEffect) {
4730        self.effects.push(effect);
4731    }
4732
4733    pub fn effective_volume(&self) -> f32 {
4734        if self.muted { 0.0 } else { self.volume }
4735    }
4736
4737    pub fn add_reverb(&mut self, room_size: f32, damping: f32, wet: f32) {
4738        let mut params = HashMap::new();
4739        params.insert("room_size".to_string(), room_size);
4740        params.insert("damping".to_string(), damping);
4741        params.insert("wet_level".to_string(), wet);
4742        self.add_effect(AudioEffect {
4743            name: "Reverb".to_string(),
4744            enabled: true,
4745            parameters: params,
4746        });
4747    }
4748
4749    pub fn add_eq_low_shelf(&mut self, frequency: f32, gain_db: f32) {
4750        let mut params = HashMap::new();
4751        params.insert("frequency".to_string(), frequency);
4752        params.insert("gain_db".to_string(), gain_db);
4753        self.add_effect(AudioEffect {
4754            name: "EQLowShelf".to_string(),
4755            enabled: true,
4756            parameters: params,
4757        });
4758    }
4759}
4760
4761#[derive(Debug, Clone)]
4762pub struct AudioMixer {
4763    pub buses: HashMap<u32, MixBus>,
4764    pub master_bus_id: u32,
4765    pub sample_rate: u32,
4766    pub buffer_size: u32,
4767    pub bit_depth: u32,
4768}
4769
4770impl AudioMixer {
4771    pub fn new(sample_rate: u32, buffer_size: u32) -> Self {
4772        let mut mixer = AudioMixer {
4773            buses: HashMap::new(),
4774            master_bus_id: 0,
4775            sample_rate,
4776            buffer_size,
4777            bit_depth: 24,
4778        };
4779        mixer.buses.insert(0, MixBus::new(0, "Master", BusType::Master));
4780        mixer
4781    }
4782
4783    pub fn add_bus(&mut self, bus: MixBus) {
4784        self.buses.insert(bus.id, bus);
4785    }
4786
4787    pub fn default_setup() -> Self {
4788        let mut m = AudioMixer::new(48000, 1024);
4789        m.add_bus(MixBus::new(1, "Music", BusType::Music));
4790        m.add_bus(MixBus::new(2, "SFX", BusType::SFX));
4791        m.add_bus(MixBus::new(3, "Dialogue", BusType::Dialogue));
4792        m.add_bus(MixBus::new(4, "Ambience", BusType::Ambience));
4793        m
4794    }
4795
4796    pub fn get_bus_mut(&mut self, id: u32) -> Option<&mut MixBus> {
4797        self.buses.get_mut(&id)
4798    }
4799
4800    pub fn active_buses(&self) -> Vec<&MixBus> {
4801        self.buses.values().filter(|b| !b.muted).collect()
4802    }
4803}
4804
4805// ============================================================
4806// CINEMATIC LENS PROFILE LIBRARY
4807// ============================================================
4808
4809#[derive(Debug, Clone)]
4810pub struct LensProfile {
4811    pub name: String,
4812    pub manufacturer: String,
4813    pub focal_length_mm: f32,
4814    pub max_aperture: f32,
4815    pub min_aperture: f32,
4816    pub t_stop: f32,
4817    pub distortion_k1: f32,
4818    pub distortion_k2: f32,
4819    pub chromatic_aberration: f32,
4820    pub vignetting_amount: f32,
4821    pub breathing_percent: f32,
4822}
4823
4824impl LensProfile {
4825    pub fn prime_35mm_fast() -> Self {
4826        LensProfile {
4827            name: "35mm T1.5".to_string(),
4828            manufacturer: "ProCine".to_string(),
4829            focal_length_mm: 35.0,
4830            max_aperture: 1.4,
4831            min_aperture: 16.0,
4832            t_stop: 1.5,
4833            distortion_k1: -0.02,
4834            distortion_k2: 0.005,
4835            chromatic_aberration: 0.03,
4836            vignetting_amount: 0.15,
4837            breathing_percent: 2.0,
4838        }
4839    }
4840
4841    pub fn zoom_24_70() -> Self {
4842        LensProfile {
4843            name: "24-70mm T2.8".to_string(),
4844            manufacturer: "CineZoom".to_string(),
4845            focal_length_mm: 47.0,
4846            max_aperture: 2.8,
4847            min_aperture: 22.0,
4848            t_stop: 2.9,
4849            distortion_k1: 0.015,
4850            distortion_k2: -0.003,
4851            chromatic_aberration: 0.05,
4852            vignetting_amount: 0.12,
4853            breathing_percent: 5.0,
4854        }
4855    }
4856
4857    pub fn effective_fov(&self, sensor_width: f32) -> f32 {
4858        2.0 * (sensor_width / (2.0 * self.focal_length_mm)).atan()
4859    }
4860}
4861
4862#[derive(Debug, Clone)]
4863pub struct LensLibrary {
4864    pub profiles: Vec<LensProfile>,
4865}
4866
4867impl LensLibrary {
4868    pub fn default_library() -> Self {
4869        LensLibrary {
4870            profiles: vec![
4871                LensProfile::prime_35mm_fast(),
4872                LensProfile::zoom_24_70(),
4873            ],
4874        }
4875    }
4876
4877    pub fn find_by_focal_length(&self, fl: f32) -> Vec<&LensProfile> {
4878        self.profiles.iter()
4879            .filter(|p| (p.focal_length_mm - fl).abs() < 5.0)
4880            .collect()
4881    }
4882}
4883
4884// info function
4885pub fn cutscene_importer_info() -> &'static str {
4886    "CutsceneImporter v3.0: Camera, Subtitle, Narrative, BlendTree, Audio, Lens"
4887}
4888
4889
4890// ============================================================
4891// MULTI-CAMERA DIRECTOR SYSTEM
4892// ============================================================
4893
4894#[derive(Debug, Clone, PartialEq)]
4895pub enum DirectorCutType {
4896    HardCut,
4897    Dissolve,
4898    FadeToBlack,
4899    FadeFromBlack,
4900    Wipe,
4901    CrossFade,
4902    Morph,
4903}
4904
4905#[derive(Debug, Clone)]
4906pub struct DirectorCut {
4907    pub time: f32,
4908    pub from_camera_id: u32,
4909    pub to_camera_id: u32,
4910    pub cut_type: DirectorCutType,
4911    pub transition_duration: f32,
4912    pub notes: String,
4913}
4914
4915#[derive(Debug, Clone)]
4916pub struct MultiCameraDirector {
4917    pub cameras: Vec<u32>,
4918    pub cuts: Vec<DirectorCut>,
4919    pub active_camera_id: u32,
4920    pub cut_rules: HashMap<String, f32>,
4921}
4922
4923impl MultiCameraDirector {
4924    pub fn new() -> Self {
4925        MultiCameraDirector {
4926            cameras: Vec::new(),
4927            cuts: Vec::new(),
4928            active_camera_id: 0,
4929            cut_rules: HashMap::new(),
4930        }
4931    }
4932
4933    pub fn add_cut(&mut self, cut: DirectorCut) {
4934        self.cuts.push(cut);
4935        self.cuts.sort_by(|a, b| a.time.partial_cmp(&b.time).unwrap());
4936    }
4937
4938    pub fn active_at(&self, t: f32) -> u32 {
4939        let mut active = self.active_camera_id;
4940        for cut in &self.cuts {
4941            if cut.time <= t {
4942                active = cut.to_camera_id;
4943            }
4944        }
4945        active
4946    }
4947
4948    pub fn next_cut(&self, t: f32) -> Option<&DirectorCut> {
4949        self.cuts.iter().find(|c| c.time > t)
4950    }
4951
4952    pub fn cuts_in_range(&self, start: f32, end: f32) -> Vec<&DirectorCut> {
4953        self.cuts.iter().filter(|c| c.time >= start && c.time < end).collect()
4954    }
4955}
4956
4957// ============================================================
4958// KEYFRAME INTERPOLATION UTILITIES
4959// ============================================================
4960
4961pub fn lerp_f32(a: f32, b: f32, t: f32) -> f32 { a + (b - a) * t }
4962pub fn smoothstep(a: f32, b: f32, t: f32) -> f32 {
4963    let x = ((t - a) / (b - a)).clamp(0.0, 1.0);
4964    x * x * (3.0 - 2.0 * x)
4965}
4966pub fn smootherstep(t: f32) -> f32 {
4967    let t = t.clamp(0.0, 1.0);
4968    t * t * t * (t * (t * 6.0 - 15.0) + 10.0)
4969}
4970pub fn ease_in_quad(t: f32) -> f32 { t * t }
4971pub fn ease_out_quad(t: f32) -> f32 { t * (2.0 - t) }
4972pub fn ease_in_out_quad(t: f32) -> f32 {
4973    if t < 0.5 { 2.0 * t * t } else { -1.0 + (4.0 - 2.0 * t) * t }
4974}
4975pub fn ease_in_cubic(t: f32) -> f32 { t * t * t }
4976pub fn ease_out_cubic(t: f32) -> f32 { let t = t - 1.0; t * t * t + 1.0 }
4977pub fn ease_in_out_cubic(t: f32) -> f32 {
4978    if t < 0.5 { 4.0 * t * t * t } else { (t - 1.0) * (2.0 * t - 2.0) * (2.0 * t - 2.0) + 1.0 }
4979}
4980pub fn ease_in_expo(t: f32) -> f32 {
4981    if t == 0.0 { 0.0 } else { (2.0f32).powf(10.0 * t - 10.0) }
4982}
4983pub fn ease_out_expo(t: f32) -> f32 {
4984    if t == 1.0 { 1.0 } else { 1.0 - (2.0f32).powf(-10.0 * t) }
4985}
4986pub fn ease_in_back(t: f32) -> f32 {
4987    let c1 = 1.70158_f32;
4988    let c3 = c1 + 1.0;
4989    c3 * t * t * t - c1 * t * t
4990}
4991pub fn ease_out_back(t: f32) -> f32 {
4992    let c1 = 1.70158_f32;
4993    let c3 = c1 + 1.0;
4994    1.0 + c3 * (t - 1.0).powi(3) + c1 * (t - 1.0).powi(2)
4995}
4996pub fn ease_out_bounce(t: f32) -> f32 {
4997    let n1 = 7.5625_f32;
4998    let d1 = 2.75_f32;
4999    let t = if t < 1.0 / d1 {
5000        n1 * t * t
5001    } else if t < 2.0 / d1 {
5002        let t = t - 1.5 / d1;
5003        n1 * t * t + 0.75
5004    } else if t < 2.5 / d1 {
5005        let t = t - 2.25 / d1;
5006        n1 * t * t + 0.9375
5007    } else {
5008        let t = t - 2.625 / d1;
5009        n1 * t * t + 0.984375
5010    };
5011    t
5012}
5013pub fn ease_in_elastic(t: f32) -> f32 {
5014    if t == 0.0 || t == 1.0 { return t; }
5015    let c4 = std::f32::consts::TAU / 3.0;
5016    -(2.0f32.powf(10.0 * t - 10.0)) * ((10.0 * t - 10.75) * c4).sin()
5017}
5018
5019// ============================================================
5020// PERFORMANCE CAPTURE SESSION
5021// ============================================================
5022
5023#[derive(Debug, Clone)]
5024pub struct MocapMarker {
5025    pub id: u32,
5026    pub label: String,
5027    pub position: Vec3,
5028    pub occluded: bool,
5029    pub residual: f32,
5030}
5031
5032#[derive(Debug, Clone)]
5033pub struct ExtMocapFrame {
5034    pub frame_number: u32,
5035    pub timestamp: f32,
5036    pub markers: Vec<MocapMarker>,
5037}
5038
5039impl ExtMocapFrame {
5040    pub fn visible_marker_count(&self) -> usize {
5041        self.markers.iter().filter(|m| !m.occluded).count()
5042    }
5043
5044    pub fn marker_by_label(&self, label: &str) -> Option<&MocapMarker> {
5045        self.markers.iter().find(|m| m.label == label)
5046    }
5047
5048    pub fn centroid(&self) -> Vec3 {
5049        let visible: Vec<&MocapMarker> = self.markers.iter().filter(|m| !m.occluded).collect();
5050        if visible.is_empty() { return Vec3::ZERO; }
5051        let sum: Vec3 = visible.iter().map(|m| m.position).fold(Vec3::ZERO, |a, b| a + b);
5052        sum / visible.len() as f32
5053    }
5054}
5055
5056#[derive(Debug, Clone)]
5057pub struct MocapSession {
5058    pub session_id: String,
5059    pub actor_name: String,
5060    pub capture_fps: f32,
5061    pub frames: Vec<ExtMocapFrame>,
5062    pub marker_labels: Vec<String>,
5063    pub calibration_residual: f32,
5064}
5065
5066impl MocapSession {
5067    pub fn new(session_id: &str, actor: &str, fps: f32) -> Self {
5068        MocapSession {
5069            session_id: session_id.to_string(),
5070            actor_name: actor.to_string(),
5071            capture_fps: fps,
5072            frames: Vec::new(),
5073            marker_labels: Vec::new(),
5074            calibration_residual: 0.0,
5075        }
5076    }
5077
5078    pub fn duration(&self) -> f32 {
5079        self.frames.last().map(|f| f.timestamp).unwrap_or(0.0)
5080    }
5081
5082    pub fn frame_at(&self, t: f32) -> Option<&ExtMocapFrame> {
5083        let frame_num = (t * self.capture_fps) as u32;
5084        self.frames.iter().find(|f| f.frame_number == frame_num)
5085    }
5086
5087    pub fn avg_marker_visibility(&self) -> f32 {
5088        if self.frames.is_empty() { return 0.0; }
5089        let total: usize = self.frames.iter().map(|f| f.visible_marker_count()).sum();
5090        let max_per_frame = self.marker_labels.len().max(1);
5091        total as f32 / (self.frames.len() * max_per_frame) as f32
5092    }
5093}
5094
5095// ============================================================
5096// SPLINE PATH SYSTEM FOR CAMERA DOLLY
5097// ============================================================
5098
5099#[derive(Debug, Clone)]
5100pub struct SplineControlPoint {
5101    pub position: Vec3,
5102    pub tangent_in: Vec3,
5103    pub tangent_out: Vec3,
5104    pub roll_deg: f32,
5105    pub speed_multiplier: f32,
5106}
5107
5108impl SplineControlPoint {
5109    pub fn new(pos: Vec3) -> Self {
5110        SplineControlPoint {
5111            position: pos,
5112            tangent_in: Vec3::ZERO,
5113            tangent_out: Vec3::ZERO,
5114            roll_deg: 0.0,
5115            speed_multiplier: 1.0,
5116        }
5117    }
5118}
5119
5120#[derive(Debug, Clone)]
5121pub struct CameraSplinePath {
5122    pub control_points: Vec<SplineControlPoint>,
5123    pub closed: bool,
5124    pub tension: f32,
5125}
5126
5127impl CameraSplinePath {
5128    pub fn new() -> Self {
5129        CameraSplinePath { control_points: Vec::new(), closed: false, tension: 0.5 }
5130    }
5131
5132    pub fn add_point(&mut self, p: SplineControlPoint) {
5133        self.control_points.push(p);
5134        self.recompute_tangents();
5135    }
5136
5137    fn recompute_tangents(&mut self) {
5138        let n = self.control_points.len();
5139        if n < 2 { return; }
5140        for i in 0..n {
5141            let prev = if i == 0 { if self.closed { n - 1 } else { 0 } } else { i - 1 };
5142            let next = if i == n - 1 { if self.closed { 0 } else { n - 1 } } else { i + 1 };
5143            let dp = self.control_points[next].position - self.control_points[prev].position;
5144            self.control_points[i].tangent_out = dp * self.tension;
5145            self.control_points[i].tangent_in = -dp * self.tension;
5146        }
5147    }
5148
5149    pub fn evaluate(&self, t: f32) -> Vec3 {
5150        let n = self.control_points.len();
5151        if n == 0 { return Vec3::ZERO; }
5152        if n == 1 { return self.control_points[0].position; }
5153        let total_segments = if self.closed { n } else { n - 1 };
5154        let segment_t = (t * total_segments as f32).clamp(0.0, total_segments as f32 - 0.0001);
5155        let seg = segment_t as usize;
5156        let s = segment_t - seg as f32;
5157        let i0 = seg;
5158        let i1 = if self.closed { (seg + 1) % n } else { (seg + 1).min(n - 1) };
5159        let p0 = self.control_points[i0].position;
5160        let p1 = self.control_points[i1].position;
5161        let m0 = self.control_points[i0].tangent_out;
5162        let m1 = self.control_points[i1].tangent_in;
5163        let s2 = s * s;
5164        let s3 = s2 * s;
5165        let h00 = 2.0 * s3 - 3.0 * s2 + 1.0;
5166        let h10 = s3 - 2.0 * s2 + s;
5167        let h01 = -2.0 * s3 + 3.0 * s2;
5168        let h11 = s3 - s2;
5169        h00 * p0 + h10 * m0 + h01 * p1 + h11 * m1
5170    }
5171
5172    pub fn arc_length_approximate(&self, samples: u32) -> f32 {
5173        if samples < 2 { return 0.0; }
5174        let mut total = 0.0;
5175        let mut prev = self.evaluate(0.0);
5176        for i in 1..samples {
5177            let t = i as f32 / (samples - 1) as f32;
5178            let curr = self.evaluate(t);
5179            total += (curr - prev).length();
5180            prev = curr;
5181        }
5182        total
5183    }
5184}
5185
5186// ============================================================
5187// SCENE GRAPH OVERVIEW
5188// ============================================================
5189
5190#[derive(Debug, Clone, PartialEq)]
5191pub enum SceneNodeKind {
5192    Root,
5193    Group,
5194    Camera,
5195    Light,
5196    Mesh,
5197    SkinnedMesh,
5198    ParticleSystem,
5199    AudioSource,
5200    VfxInstance,
5201    Trigger,
5202    Locator,
5203}
5204
5205#[derive(Debug, Clone)]
5206pub struct SceneGraphNode {
5207    pub id: u32,
5208    pub name: String,
5209    pub kind: SceneNodeKind,
5210    pub parent_id: Option<u32>,
5211    pub children: Vec<u32>,
5212    pub local_position: Vec3,
5213    pub local_rotation: Quat,
5214    pub local_scale: Vec3,
5215    pub visible: bool,
5216    pub static_flag: bool,
5217    pub asset_ref: Option<String>,
5218    pub tags: HashSet<String>,
5219}
5220
5221impl SceneGraphNode {
5222    pub fn new(id: u32, name: &str, kind: SceneNodeKind) -> Self {
5223        SceneGraphNode {
5224            id, name: name.to_string(), kind,
5225            parent_id: None, children: Vec::new(),
5226            local_position: Vec3::ZERO,
5227            local_rotation: Quat::IDENTITY,
5228            local_scale: Vec3::ONE,
5229            visible: true, static_flag: false,
5230            asset_ref: None, tags: HashSet::new(),
5231        }
5232    }
5233
5234    pub fn local_matrix(&self) -> Mat4 {
5235        Mat4::from_scale_rotation_translation(self.local_scale, self.local_rotation, self.local_position)
5236    }
5237
5238    pub fn add_tag(&mut self, tag: &str) {
5239        self.tags.insert(tag.to_string());
5240    }
5241
5242    pub fn has_tag(&self, tag: &str) -> bool {
5243        self.tags.contains(tag)
5244    }
5245}
5246
5247#[derive(Debug, Clone)]
5248pub struct CutsceneSceneGraph {
5249    pub nodes: HashMap<u32, SceneGraphNode>,
5250    pub root_id: u32,
5251    pub next_id: u32,
5252}
5253
5254impl CutsceneSceneGraph {
5255    pub fn new() -> Self {
5256        let mut g = CutsceneSceneGraph { nodes: HashMap::new(), root_id: 0, next_id: 1 };
5257        g.nodes.insert(0, SceneGraphNode::new(0, "Root", SceneNodeKind::Root));
5258        g
5259    }
5260
5261    pub fn alloc_id(&mut self) -> u32 {
5262        let id = self.next_id;
5263        self.next_id += 1;
5264        id
5265    }
5266
5267    pub fn add_node(&mut self, mut node: SceneGraphNode, parent_id: u32) {
5268        node.parent_id = Some(parent_id);
5269        let node_id = node.id;
5270        self.nodes.insert(node_id, node);
5271        if let Some(parent) = self.nodes.get_mut(&parent_id) {
5272            parent.children.push(node_id);
5273        }
5274    }
5275
5276    pub fn world_matrix(&self, id: u32) -> Mat4 {
5277        let node = match self.nodes.get(&id) { Some(n) => n, None => return Mat4::IDENTITY };
5278        let local = node.local_matrix();
5279        match node.parent_id {
5280            Some(pid) => self.world_matrix(pid) * local,
5281            None => local,
5282        }
5283    }
5284
5285    pub fn find_by_tag(&self, tag: &str) -> Vec<u32> {
5286        self.nodes.values().filter(|n| n.has_tag(tag)).map(|n| n.id).collect()
5287    }
5288
5289    pub fn visible_nodes(&self) -> Vec<u32> {
5290        self.nodes.values().filter(|n| n.visible).map(|n| n.id).collect()
5291    }
5292
5293    pub fn node_depth(&self, id: u32) -> u32 {
5294        let mut depth = 0;
5295        let mut current_id = id;
5296        while let Some(node) = self.nodes.get(&current_id) {
5297            if let Some(pid) = node.parent_id {
5298                depth += 1;
5299                current_id = pid;
5300            } else {
5301                break;
5302            }
5303        }
5304        depth
5305    }
5306}
5307
5308// ============================================================
5309// RENDER QUALITY PRESETS
5310// ============================================================
5311
5312#[derive(Debug, Clone, PartialEq)]
5313pub enum RenderQualityPreset {
5314    Preview,
5315    Medium,
5316    High,
5317    Ultra,
5318    CinematicMaster,
5319}
5320
5321#[derive(Debug, Clone)]
5322pub struct RenderQualitySettings {
5323    pub preset: RenderQualityPreset,
5324    pub resolution_scale: f32,
5325    pub samples_per_pixel: u32,
5326    pub max_ray_depth: u32,
5327    pub shadow_map_size: u32,
5328    pub ao_radius: f32,
5329    pub ao_samples: u32,
5330    pub reflection_quality: u32,
5331    pub motion_blur_samples: u32,
5332    pub dof_bokeh_quality: u32,
5333    pub gi_enabled: bool,
5334    pub gi_bounces: u32,
5335}
5336
5337impl RenderQualitySettings {
5338    pub fn preview() -> Self {
5339        RenderQualitySettings {
5340            preset: RenderQualityPreset::Preview,
5341            resolution_scale: 0.5,
5342            samples_per_pixel: 1,
5343            max_ray_depth: 2,
5344            shadow_map_size: 1024,
5345            ao_radius: 0.5,
5346            ao_samples: 8,
5347            reflection_quality: 1,
5348            motion_blur_samples: 4,
5349            dof_bokeh_quality: 4,
5350            gi_enabled: false,
5351            gi_bounces: 1,
5352        }
5353    }
5354
5355    pub fn cinematic_master() -> Self {
5356        RenderQualitySettings {
5357            preset: RenderQualityPreset::CinematicMaster,
5358            resolution_scale: 1.0,
5359            samples_per_pixel: 64,
5360            max_ray_depth: 16,
5361            shadow_map_size: 8192,
5362            ao_radius: 2.0,
5363            ao_samples: 64,
5364            reflection_quality: 4,
5365            motion_blur_samples: 32,
5366            dof_bokeh_quality: 32,
5367            gi_enabled: true,
5368            gi_bounces: 8,
5369        }
5370    }
5371
5372    pub fn estimated_render_time_factor(&self) -> f32 {
5373        (self.samples_per_pixel as f32)
5374            * (self.ao_samples as f32 / 8.0)
5375            * (self.motion_blur_samples as f32 / 4.0)
5376            * (self.resolution_scale * self.resolution_scale)
5377            * (self.max_ray_depth as f32 / 2.0)
5378    }
5379}
5380
5381// ============================================================
5382// ADDITIONAL TEST FUNCTIONS
5383// ============================================================
5384
5385#[cfg(test)]
5386mod tests_director {
5387    use super::*;
5388
5389    #[test]
5390    fn test_multi_camera_director_active_at() {
5391        let mut dir = MultiCameraDirector::new();
5392        dir.active_camera_id = 1;
5393        dir.add_cut(DirectorCut {
5394            time: 5.0, from_camera_id: 1, to_camera_id: 2,
5395            cut_type: DirectorCutType::HardCut, transition_duration: 0.0,
5396            notes: String::new(),
5397        });
5398        assert_eq!(dir.active_at(3.0), 1);
5399        assert_eq!(dir.active_at(7.0), 2);
5400    }
5401
5402    #[test]
5403    fn test_spline_path_evaluate() {
5404        let mut path = CameraSplinePath::new();
5405        path.add_point(SplineControlPoint::new(Vec3::ZERO));
5406        path.add_point(SplineControlPoint::new(Vec3::new(10.0, 0.0, 0.0)));
5407        let mid = path.evaluate(0.5);
5408        assert!(mid.x > 0.0 && mid.x < 10.0);
5409    }
5410
5411    #[test]
5412    fn test_mocap_session_visibility() {
5413        let mut session = MocapSession::new("S001", "ActorA", 120.0);
5414        session.marker_labels = vec!["Head".to_string(), "Hip".to_string()];
5415        session.frames.push(ExtMocapFrame {
5416            frame_number: 0, timestamp: 0.0,
5417            markers: vec![
5418                MocapMarker { id: 0, label: "Head".to_string(), position: Vec3::new(0.0, 1.8, 0.0), occluded: false, residual: 0.1 },
5419                MocapMarker { id: 1, label: "Hip".to_string(), position: Vec3::new(0.0, 1.0, 0.0), occluded: true, residual: 0.5 },
5420            ],
5421        });
5422        assert_eq!(session.frames[0].visible_marker_count(), 1);
5423    }
5424
5425    #[test]
5426    fn test_scene_graph_world_matrix() {
5427        let mut graph = CutsceneSceneGraph::new();
5428        let mut child = SceneGraphNode::new(1, "Child", SceneNodeKind::Mesh);
5429        child.local_position = Vec3::new(5.0, 0.0, 0.0);
5430        graph.add_node(child, 0);
5431        let world = graph.world_matrix(1);
5432        let pos = world.col(3).truncate();
5433        assert!((pos.x - 5.0).abs() < 0.01);
5434    }
5435
5436    #[test]
5437    fn test_render_quality_time_factor() {
5438        let preview = RenderQualitySettings::preview();
5439        let master = RenderQualitySettings::cinematic_master();
5440        assert!(master.estimated_render_time_factor() > preview.estimated_render_time_factor() * 10.0);
5441    }
5442
5443    #[test]
5444    fn test_ease_functions() {
5445        assert!((ease_in_quad(0.0)).abs() < 0.001);
5446        assert!((ease_in_quad(1.0) - 1.0).abs() < 0.001);
5447        assert!((ease_out_quad(0.5) - 0.75).abs() < 0.01);
5448        assert!((ease_out_bounce(1.0) - 1.0).abs() < 0.01);
5449    }
5450
5451    #[test]
5452    fn test_audio_mixer_default() {
5453        let mixer = AudioMixer::default_setup();
5454        assert_eq!(mixer.buses.len(), 5);
5455        assert_eq!(mixer.sample_rate, 48000);
5456    }
5457
5458    #[test]
5459    fn test_cutscene_project_summary() {
5460        let mut proj = CutsceneProjectFile::new(120.0, 24.0);
5461        proj.add_camera(CinematicCamera::new(1, "MainCam"));
5462        let summary = proj.summary();
5463        assert!(summary.contains("120.00s"));
5464        assert!(summary.contains("24fps"));
5465    }
5466
5467    #[test]
5468    fn test_lens_library_lookup() {
5469        let lib = LensLibrary::default_library();
5470        let lenses = lib.find_by_focal_length(35.0);
5471        assert!(!lenses.is_empty());
5472    }
5473}
5474
5475// ============================================================
5476// INTERPOLATION TABLE FOR KEYFRAME CURVES
5477// ============================================================
5478
5479pub const EASE_TABLE_SIZE: usize = 512;
5480
5481pub struct EaseTable {
5482    pub values: Vec<f32>,
5483    pub ease_fn_name: String,
5484}
5485
5486impl EaseTable {
5487    pub fn build(name: &str, f: fn(f32) -> f32) -> Self {
5488        let values = (0..=EASE_TABLE_SIZE)
5489            .map(|i| f(i as f32 / EASE_TABLE_SIZE as f32))
5490            .collect();
5491        EaseTable { values, ease_fn_name: name.to_string() }
5492    }
5493
5494    pub fn sample(&self, t: f32) -> f32 {
5495        let idx = (t * EASE_TABLE_SIZE as f32).clamp(0.0, EASE_TABLE_SIZE as f32) as usize;
5496        let idx = idx.min(EASE_TABLE_SIZE);
5497        self.values[idx]
5498    }
5499}
5500
5501// ============================================================
5502// ASSET PIPELINE INTEGRATION
5503// ============================================================
5504
5505#[derive(Debug, Clone, PartialEq)]
5506pub enum AssetImportStatus {
5507    Pending,
5508    Importing,
5509    Complete,
5510    Failed,
5511    Stale,
5512}
5513
5514#[derive(Debug, Clone)]
5515pub struct CutsceneAssetRef {
5516    pub asset_id: String,
5517    pub asset_type: String,
5518    pub source_path: String,
5519    pub import_status: AssetImportStatus,
5520    pub size_bytes: u64,
5521    pub last_modified: u64,
5522    pub dependencies: Vec<String>,
5523}
5524
5525impl CutsceneAssetRef {
5526    pub fn new(asset_id: &str, asset_type: &str, source_path: &str) -> Self {
5527        CutsceneAssetRef {
5528            asset_id: asset_id.to_string(),
5529            asset_type: asset_type.to_string(),
5530            source_path: source_path.to_string(),
5531            import_status: AssetImportStatus::Pending,
5532            size_bytes: 0,
5533            last_modified: 0,
5534            dependencies: Vec::new(),
5535        }
5536    }
5537
5538    pub fn is_ready(&self) -> bool {
5539        self.import_status == AssetImportStatus::Complete
5540    }
5541}
5542
5543#[derive(Debug, Clone)]
5544pub struct CutsceneAssetRegistry {
5545    pub assets: HashMap<String, CutsceneAssetRef>,
5546    pub failed_imports: Vec<String>,
5547}
5548
5549impl CutsceneAssetRegistry {
5550    pub fn new() -> Self {
5551        CutsceneAssetRegistry { assets: HashMap::new(), failed_imports: Vec::new() }
5552    }
5553
5554    pub fn register(&mut self, asset: CutsceneAssetRef) {
5555        self.assets.insert(asset.asset_id.clone(), asset);
5556    }
5557
5558    pub fn mark_complete(&mut self, id: &str) {
5559        if let Some(a) = self.assets.get_mut(id) {
5560            a.import_status = AssetImportStatus::Complete;
5561        }
5562    }
5563
5564    pub fn mark_failed(&mut self, id: &str) {
5565        if let Some(a) = self.assets.get_mut(id) {
5566            a.import_status = AssetImportStatus::Failed;
5567        }
5568        self.failed_imports.push(id.to_string());
5569    }
5570
5571    pub fn ready_asset_ids(&self) -> Vec<&str> {
5572        self.assets.values()
5573            .filter(|a| a.is_ready())
5574            .map(|a| a.asset_id.as_str())
5575            .collect()
5576    }
5577
5578    pub fn total_size_bytes(&self) -> u64 {
5579        self.assets.values().map(|a| a.size_bytes).sum()
5580    }
5581}
5582
5583// ============================================================
5584// PLAYBACK ENGINE STATE
5585// ============================================================
5586
5587#[derive(Debug, Clone, PartialEq)]
5588pub enum PlaybackState {
5589    Stopped,
5590    Playing,
5591    Paused,
5592    Scrubbing,
5593    Rendering,
5594}
5595
5596#[derive(Debug, Clone)]
5597pub struct PlaybackEngine {
5598    pub state: PlaybackState,
5599    pub current_time: f32,
5600    pub duration: f32,
5601    pub playback_rate: f32,
5602    pub loop_enabled: bool,
5603    pub loop_start: f32,
5604    pub loop_end: f32,
5605    pub frame_rate: f32,
5606    pub audio_sync: bool,
5607}
5608
5609impl PlaybackEngine {
5610    pub fn new(duration: f32, fps: f32) -> Self {
5611        PlaybackEngine {
5612            state: PlaybackState::Stopped,
5613            current_time: 0.0,
5614            duration,
5615            playback_rate: 1.0,
5616            loop_enabled: false,
5617            loop_start: 0.0,
5618            loop_end: duration,
5619            frame_rate: fps,
5620            audio_sync: true,
5621        }
5622    }
5623
5624    pub fn play(&mut self) {
5625        self.state = PlaybackState::Playing;
5626    }
5627
5628    pub fn pause(&mut self) {
5629        if self.state == PlaybackState::Playing {
5630            self.state = PlaybackState::Paused;
5631        }
5632    }
5633
5634    pub fn stop(&mut self) {
5635        self.state = PlaybackState::Stopped;
5636        self.current_time = 0.0;
5637    }
5638
5639    pub fn seek(&mut self, t: f32) {
5640        self.current_time = t.clamp(0.0, self.duration);
5641    }
5642
5643    pub fn advance(&mut self, dt: f32) {
5644        if self.state != PlaybackState::Playing { return; }
5645        self.current_time += dt * self.playback_rate;
5646        if self.loop_enabled && self.current_time >= self.loop_end {
5647            self.current_time = self.loop_start;
5648        } else if self.current_time >= self.duration {
5649            self.current_time = self.duration;
5650            self.state = PlaybackState::Stopped;
5651        }
5652    }
5653
5654    pub fn current_frame(&self) -> u32 {
5655        (self.current_time * self.frame_rate) as u32
5656    }
5657
5658    pub fn progress(&self) -> f32 {
5659        if self.duration <= 0.0 { return 0.0; }
5660        self.current_time / self.duration
5661    }
5662
5663    pub fn time_remaining(&self) -> f32 {
5664        (self.duration - self.current_time).max(0.0)
5665    }
5666}
5667
5668// ============================================================
5669// FINAL EXPORT PIPELINE
5670// ============================================================
5671
5672#[derive(Debug, Clone, PartialEq)]
5673pub enum VideoCodec {
5674    H264,
5675    H265,
5676    ProRes422,
5677    ProRes4444,
5678    DNxHD,
5679    DNxHR,
5680    AV1,
5681    VP9,
5682    Uncompressed,
5683}
5684
5685#[derive(Debug, Clone, PartialEq)]
5686pub enum AudioCodec {
5687    AAC,
5688    PCM24,
5689    PCM32F,
5690    OPUS,
5691    FLAC,
5692}
5693
5694#[derive(Debug, Clone)]
5695pub struct ExportPipelineConfig {
5696    pub output_path: String,
5697    pub video_codec: VideoCodec,
5698    pub audio_codec: AudioCodec,
5699    pub width: u32,
5700    pub height: u32,
5701    pub frame_rate: f32,
5702    pub video_bitrate_kbps: u32,
5703    pub audio_bitrate_kbps: u32,
5704    pub audio_sample_rate: u32,
5705    pub start_frame: u32,
5706    pub end_frame: u32,
5707    pub include_alpha: bool,
5708    pub hdr_enabled: bool,
5709    pub color_space: String,
5710    pub lut_path: Option<String>,
5711}
5712
5713impl ExportPipelineConfig {
5714    pub fn web_preview() -> Self {
5715        ExportPipelineConfig {
5716            output_path: "preview.mp4".to_string(),
5717            video_codec: VideoCodec::H264,
5718            audio_codec: AudioCodec::AAC,
5719            width: 1280, height: 720,
5720            frame_rate: 30.0,
5721            video_bitrate_kbps: 4000,
5722            audio_bitrate_kbps: 192,
5723            audio_sample_rate: 48000,
5724            start_frame: 0, end_frame: u32::MAX,
5725            include_alpha: false,
5726            hdr_enabled: false,
5727            color_space: "sRGB".to_string(),
5728            lut_path: None,
5729        }
5730    }
5731
5732    pub fn broadcast_master() -> Self {
5733        ExportPipelineConfig {
5734            output_path: "master.mov".to_string(),
5735            video_codec: VideoCodec::ProRes4444,
5736            audio_codec: AudioCodec::PCM24,
5737            width: 3840, height: 2160,
5738            frame_rate: 24.0,
5739            video_bitrate_kbps: 800000,
5740            audio_bitrate_kbps: 2304,
5741            audio_sample_rate: 48000,
5742            start_frame: 0, end_frame: u32::MAX,
5743            include_alpha: true,
5744            hdr_enabled: true,
5745            color_space: "ACEScg".to_string(),
5746            lut_path: Some("aces_rrt.cube".to_string()),
5747        }
5748    }
5749
5750    pub fn total_frames(&self, fps: f32) -> u32 {
5751        self.end_frame.saturating_sub(self.start_frame)
5752    }
5753
5754    pub fn estimated_file_size_mb(&self, duration_s: f32) -> f32 {
5755        (self.video_bitrate_kbps as f32 + self.audio_bitrate_kbps as f32)
5756            * duration_s / 8.0 / 1024.0
5757    }
5758}
5759
5760#[derive(Debug, Clone)]
5761pub struct ExportJob {
5762    pub job_id: String,
5763    pub config: ExportPipelineConfig,
5764    pub progress: f32,
5765    pub frames_rendered: u32,
5766    pub total_frames: u32,
5767    pub elapsed_seconds: f32,
5768    pub errors: Vec<String>,
5769    pub complete: bool,
5770}
5771
5772impl ExportJob {
5773    pub fn new(job_id: &str, config: ExportPipelineConfig, total_frames: u32) -> Self {
5774        ExportJob {
5775            job_id: job_id.to_string(),
5776            config,
5777            progress: 0.0,
5778            frames_rendered: 0,
5779            total_frames,
5780            elapsed_seconds: 0.0,
5781            errors: Vec::new(),
5782            complete: false,
5783        }
5784    }
5785
5786    pub fn advance_frame(&mut self) {
5787        self.frames_rendered += 1;
5788        self.progress = self.frames_rendered as f32 / self.total_frames.max(1) as f32;
5789        if self.frames_rendered >= self.total_frames {
5790            self.complete = true;
5791        }
5792    }
5793
5794    pub fn fps_current(&self) -> f32 {
5795        if self.elapsed_seconds <= 0.0 { return 0.0; }
5796        self.frames_rendered as f32 / self.elapsed_seconds
5797    }
5798
5799    pub fn eta_seconds(&self) -> f32 {
5800        let fps = self.fps_current();
5801        if fps <= 0.0 { return f32::INFINITY; }
5802        let remaining = self.total_frames - self.frames_rendered;
5803        remaining as f32 / fps
5804    }
5805}
5806
5807pub fn cutscene_module_version() -> &'static str { "3.1.0" }
5808pub fn cutscene_feature_flags() -> &'static [&'static str] {
5809    &["camera_anim", "subtitles", "narrative", "blend_tree", "audio_mix",
5810      "mocap", "spline_dolly", "scene_graph", "export_pipeline", "retargeting"]
5811}
5812
5813
5814// ============================================================
5815// COLOR GRADING AND TONEMAPPING
5816// ============================================================
5817
5818#[derive(Debug, Clone)]
5819pub struct ColorGradingLUT {
5820    pub name: String,
5821    pub size: u32,
5822    pub data: Vec<Vec3>,
5823}
5824
5825impl ColorGradingLUT {
5826    pub fn new_identity(size: u32) -> Self {
5827        let mut data = Vec::with_capacity((size * size * size) as usize);
5828        for b in 0..size {
5829            for g in 0..size {
5830                for r in 0..size {
5831                    data.push(Vec3::new(
5832                        r as f32 / (size - 1) as f32,
5833                        g as f32 / (size - 1) as f32,
5834                        b as f32 / (size - 1) as f32,
5835                    ));
5836                }
5837            }
5838        }
5839        ColorGradingLUT { name: "Identity".to_string(), size, data }
5840    }
5841
5842    pub fn apply(&self, color: Vec3) -> Vec3 {
5843        let s = (self.size - 1) as f32;
5844        let r = (color.x * s).clamp(0.0, s);
5845        let g = (color.y * s).clamp(0.0, s);
5846        let b = (color.z * s).clamp(0.0, s);
5847        let ri = r as usize; let gi = g as usize; let bi = b as usize;
5848        let ri = ri.min(self.size as usize - 2);
5849        let gi = gi.min(self.size as usize - 2);
5850        let bi = bi.min(self.size as usize - 2);
5851        let fr = r - ri as f32; let fg = g - gi as f32; let fb = b - bi as f32;
5852        let s2 = self.size as usize;
5853        let idx = |r2: usize, g2: usize, b2: usize| b2 * s2 * s2 + g2 * s2 + r2;
5854        let c000 = self.data[idx(ri, gi, bi)];
5855        let c100 = self.data[idx(ri+1, gi, bi)];
5856        let c010 = self.data[idx(ri, gi+1, bi)];
5857        let c110 = self.data[idx(ri+1, gi+1, bi)];
5858        let c001 = self.data[idx(ri, gi, bi+1)];
5859        let c101 = self.data[idx(ri+1, gi, bi+1)];
5860        let c011 = self.data[idx(ri, gi+1, bi+1)];
5861        let c111 = self.data[idx(ri+1, gi+1, bi+1)];
5862        let c00 = c000.lerp(c100, fr);
5863        let c10 = c010.lerp(c110, fr);
5864        let c01 = c001.lerp(c101, fr);
5865        let c11 = c011.lerp(c111, fr);
5866        let c0 = c00.lerp(c10, fg);
5867        let c1 = c01.lerp(c11, fg);
5868        c0.lerp(c1, fb)
5869    }
5870}
5871
5872#[derive(Debug, Clone)]
5873pub struct ToneMapper {
5874    pub exposure: f32,
5875    pub gamma: f32,
5876    pub white_point: f32,
5877}
5878
5879impl ToneMapper {
5880    pub fn new() -> Self {
5881        ToneMapper { exposure: 1.0, gamma: 2.2, white_point: 11.2 }
5882    }
5883
5884    pub fn reinhard(&self, color: Vec3) -> Vec3 {
5885        let c = color * self.exposure;
5886        Vec3::new(c.x / (1.0 + c.x), c.y / (1.0 + c.y), c.z / (1.0 + c.z))
5887    }
5888
5889    pub fn aces(&self, color: Vec3) -> Vec3 {
5890        let c = color * self.exposure;
5891        let a = 2.51_f32; let b = 0.03_f32; let cc = 2.43_f32; let d = 0.59_f32; let e = 0.14_f32;
5892        let tone = |x: f32| ((x * (a * x + b)) / (x * (cc * x + d) + e)).clamp(0.0, 1.0);
5893        Vec3::new(tone(c.x), tone(c.y), tone(c.z))
5894    }
5895
5896    pub fn filmic(&self, color: Vec3) -> Vec3 {
5897        let c = (color * self.exposure).max(Vec3::ZERO);
5898        let w = self.white_point;
5899        let filmic_f = |x: f32| -> f32 {
5900            let a = 0.22_f32; let b = 0.30_f32; let cc = 0.10_f32;
5901            let d = 0.20_f32; let ee = 0.01_f32; let f = 0.30_f32;
5902            ((x * (a * x + cc * b) + d * ee) / (x * (a * x + b) + d * f)) - ee / f
5903        };
5904        let white = filmic_f(w);
5905        Vec3::new(filmic_f(c.x) / white, filmic_f(c.y) / white, filmic_f(c.z) / white)
5906    }
5907
5908    pub fn gamma_correct(&self, linear: Vec3) -> Vec3 {
5909        let g = 1.0 / self.gamma;
5910        Vec3::new(linear.x.powf(g), linear.y.powf(g), linear.z.powf(g))
5911    }
5912}
5913
5914// ============================================================
5915// CUBEMAP AND IBL
5916// ============================================================
5917
5918pub const CUBEMAP_FACE_COUNT: usize = 6;
5919
5920#[derive(Debug, Clone, PartialEq)]
5921pub enum CubemapFace {
5922    PosX, NegX, PosY, NegY, PosZ, NegZ,
5923}
5924
5925#[derive(Debug, Clone)]
5926pub struct CubemapAsset {
5927    pub name: String,
5928    pub face_resolution: u32,
5929    pub mip_levels: u32,
5930    pub hdr: bool,
5931    pub source_path: String,
5932}
5933
5934impl CubemapAsset {
5935    pub fn new(name: &str, res: u32, hdr: bool) -> Self {
5936        CubemapAsset {
5937            name: name.to_string(), face_resolution: res,
5938            mip_levels: (res as f32).log2() as u32 + 1,
5939            hdr, source_path: String::new(),
5940        }
5941    }
5942
5943    pub fn total_texels(&self) -> u64 {
5944        let mut total = 0u64;
5945        let mut res = self.face_resolution;
5946        for _ in 0..self.mip_levels {
5947            total += (res as u64) * (res as u64) * CUBEMAP_FACE_COUNT as u64;
5948            res = (res / 2).max(1);
5949        }
5950        total
5951    }
5952}
5953
5954// ============================================================
5955// ADVANCED AUDIO DSP
5956// ============================================================
5957
5958#[derive(Debug, Clone)]
5959pub struct BiQuadFilter {
5960    pub b0: f32, pub b1: f32, pub b2: f32,
5961    pub a1: f32, pub a2: f32,
5962    x1: f32, x2: f32, y1: f32, y2: f32,
5963}
5964
5965impl BiQuadFilter {
5966    pub fn low_pass(sample_rate: f32, cutoff: f32, q: f32) -> Self {
5967        let w0 = 2.0 * std::f32::consts::PI * cutoff / sample_rate;
5968        let alpha = w0.sin() / (2.0 * q);
5969        let cos_w0 = w0.cos();
5970        let b1 = 1.0 - cos_w0;
5971        let b0 = b1 / 2.0;
5972        let b2 = b0;
5973        let a0 = 1.0 + alpha;
5974        let a1 = -2.0 * cos_w0;
5975        let a2 = 1.0 - alpha;
5976        BiQuadFilter { b0: b0/a0, b1: b1/a0, b2: b2/a0, a1: a1/a0, a2: a2/a0, x1: 0.0, x2: 0.0, y1: 0.0, y2: 0.0 }
5977    }
5978
5979    pub fn high_pass(sample_rate: f32, cutoff: f32, q: f32) -> Self {
5980        let w0 = 2.0 * std::f32::consts::PI * cutoff / sample_rate;
5981        let alpha = w0.sin() / (2.0 * q);
5982        let cos_w0 = w0.cos();
5983        let b0 = (1.0 + cos_w0) / 2.0;
5984        let b1 = -(1.0 + cos_w0);
5985        let b2 = b0;
5986        let a0 = 1.0 + alpha;
5987        let a1 = -2.0 * cos_w0;
5988        let a2 = 1.0 - alpha;
5989        BiQuadFilter { b0: b0/a0, b1: b1/a0, b2: b2/a0, a1: a1/a0, a2: a2/a0, x1: 0.0, x2: 0.0, y1: 0.0, y2: 0.0 }
5990    }
5991
5992    pub fn process(&mut self, x: f32) -> f32 {
5993        let y = self.b0 * x + self.b1 * self.x1 + self.b2 * self.x2 - self.a1 * self.y1 - self.a2 * self.y2;
5994        self.x2 = self.x1; self.x1 = x;
5995        self.y2 = self.y1; self.y1 = y;
5996        y
5997    }
5998}
5999
6000// ============================================================
6001// PARTICLE CURVE DATA
6002// ============================================================
6003
6004#[derive(Debug, Clone)]
6005pub struct ParticleCurvePoint {
6006    pub time: f32,
6007    pub value: f32,
6008}
6009
6010#[derive(Debug, Clone)]
6011pub struct ParticleCurve {
6012    pub name: String,
6013    pub points: Vec<ParticleCurvePoint>,
6014    pub loop_curve: bool,
6015}
6016
6017impl ParticleCurve {
6018    pub fn new(name: &str) -> Self {
6019        ParticleCurve { name: name.to_string(), points: Vec::new(), loop_curve: false }
6020    }
6021
6022    pub fn flat(name: &str, value: f32) -> Self {
6023        let mut c = ParticleCurve::new(name);
6024        c.points.push(ParticleCurvePoint { time: 0.0, value });
6025        c.points.push(ParticleCurvePoint { time: 1.0, value });
6026        c
6027    }
6028
6029    pub fn ramp_up(name: &str) -> Self {
6030        let mut c = ParticleCurve::new(name);
6031        c.points.push(ParticleCurvePoint { time: 0.0, value: 0.0 });
6032        c.points.push(ParticleCurvePoint { time: 1.0, value: 1.0 });
6033        c
6034    }
6035
6036    pub fn evaluate(&self, t: f32) -> f32 {
6037        if self.points.is_empty() { return 0.0; }
6038        if self.points.len() == 1 { return self.points[0].value; }
6039        let t = if self.loop_curve { t.fract() } else { t.clamp(0.0, 1.0) };
6040        let idx = self.points.partition_point(|p| p.time <= t);
6041        if idx == 0 { return self.points[0].value; }
6042        if idx >= self.points.len() { return self.points.last().unwrap().value; }
6043        let a = &self.points[idx - 1];
6044        let b = &self.points[idx];
6045        let s = (t - a.time) / (b.time - a.time).max(0.001);
6046        a.value + (b.value - a.value) * s
6047    }
6048}
6049
6050// ============================================================
6051// FINAL TEST BLOCK FOR CUTSCENE MODULE
6052// ============================================================
6053
6054#[cfg(test)]
6055mod tests_cutscene_final {
6056    use super::*;
6057
6058    #[test]
6059    fn test_lut_identity() {
6060        let lut = ColorGradingLUT::new_identity(4);
6061        let color = Vec3::new(0.5, 0.3, 0.8);
6062        let result = lut.apply(color);
6063        assert!((result.x - color.x).abs() < 0.1);
6064        assert!((result.y - color.y).abs() < 0.1);
6065    }
6066
6067    #[test]
6068    fn test_tonemapper_reinhard() {
6069        let tm = ToneMapper::new();
6070        let bright = Vec3::new(2.0, 1.5, 0.5);
6071        let result = tm.reinhard(bright);
6072        assert!(result.x < 1.0 && result.y < 1.0 && result.z <= 1.0);
6073    }
6074
6075    #[test]
6076    fn test_biquad_lowpass() {
6077        let mut filt = BiQuadFilter::low_pass(44100.0, 1000.0, 0.707);
6078        let y = filt.process(1.0);
6079        assert!(y.is_finite());
6080        let y2 = filt.process(0.0);
6081        assert!(y2.is_finite());
6082    }
6083
6084    #[test]
6085    fn test_particle_curve_evaluate() {
6086        let curve = ParticleCurve::ramp_up("alpha");
6087        assert!((curve.evaluate(0.0)).abs() < 0.001);
6088        assert!((curve.evaluate(1.0) - 1.0).abs() < 0.001);
6089        assert!((curve.evaluate(0.5) - 0.5).abs() < 0.001);
6090    }
6091
6092    #[test]
6093    fn test_cubemap_total_texels() {
6094        let cube = CubemapAsset::new("SkyHDR", 512, true);
6095        let total = cube.total_texels();
6096        assert!(total > 512 * 512 * 6);
6097    }
6098
6099    #[test]
6100    fn test_playback_engine() {
6101        let mut engine = PlaybackEngine::new(10.0, 24.0);
6102        engine.play();
6103        engine.advance(5.0);
6104        assert!((engine.current_time - 5.0).abs() < 0.001);
6105        assert_eq!(engine.current_frame(), 120);
6106        engine.advance(6.0);
6107        assert_eq!(engine.state, PlaybackState::Stopped);
6108    }
6109
6110    #[test]
6111    fn test_export_job_progress() {
6112        let config = ExportPipelineConfig::web_preview();
6113        let mut job = ExportJob::new("job001", config, 100);
6114        for _ in 0..50 { job.advance_frame(); }
6115        assert!((job.progress - 0.5).abs() < 0.01);
6116        assert!(!job.complete);
6117        for _ in 0..50 { job.advance_frame(); }
6118        assert!(job.complete);
6119    }
6120
6121    #[test]
6122    fn test_asset_registry() {
6123        let mut reg = CutsceneAssetRegistry::new();
6124        reg.register(CutsceneAssetRef::new("mesh_001", "Mesh", "assets/hero.glb"));
6125        reg.mark_complete("mesh_001");
6126        assert_eq!(reg.ready_asset_ids().len(), 1);
6127        reg.register(CutsceneAssetRef::new("tex_002", "Texture", "assets/hero_diff.png"));
6128        reg.mark_failed("tex_002");
6129        assert_eq!(reg.failed_imports.len(), 1);
6130    }
6131}
6132
6133// Final constants
6134pub const CUTSCENE_EDITOR_VERSION: &str = "3.1.0";
6135pub const CUTSCENE_MAX_CAMERAS: u32 = 32;
6136pub const CUTSCENE_MAX_LAYERS: u32 = 64;
6137pub const CUTSCENE_MAX_EVENTS: u32 = 4096;
6138pub const CUTSCENE_MAX_SUBTITLES_PER_LANG: u32 = 2048;
6139
6140
6141// ============================================================
6142// CUTSCENE ANALYTICS AND TELEMETRY
6143// ============================================================
6144
6145#[derive(Debug, Clone)]
6146pub struct WatchEvent {
6147    pub timestamp_ms: u64,
6148    pub event_type: String,
6149    pub time_in_cutscene: f32,
6150    pub platform: String,
6151    pub session_id: String,
6152}
6153
6154#[derive(Debug, Clone)]
6155pub struct CutsceneAnalytics {
6156    pub cutscene_id: String,
6157    pub total_plays: u64,
6158    pub total_skips: u64,
6159    pub avg_watch_time_seconds: f32,
6160    pub completion_rate: f32,
6161    pub skip_points: Vec<f32>,
6162    pub watch_events: VecDeque<WatchEvent>,
6163    pub max_events: usize,
6164}
6165
6166impl CutsceneAnalytics {
6167    pub fn new(cutscene_id: &str) -> Self {
6168        CutsceneAnalytics {
6169            cutscene_id: cutscene_id.to_string(),
6170            total_plays: 0, total_skips: 0,
6171            avg_watch_time_seconds: 0.0,
6172            completion_rate: 0.0,
6173            skip_points: Vec::new(),
6174            watch_events: VecDeque::new(),
6175            max_events: 1000,
6176        }
6177    }
6178
6179    pub fn record_play(&mut self, watch_time_s: f32, skipped: bool, skip_at: Option<f32>) {
6180        self.total_plays += 1;
6181        if skipped {
6182            self.total_skips += 1;
6183            if let Some(t) = skip_at { self.skip_points.push(t); }
6184        }
6185        let alpha = 1.0 / self.total_plays as f32;
6186        self.avg_watch_time_seconds = self.avg_watch_time_seconds * (1.0 - alpha) + watch_time_s * alpha;
6187        self.completion_rate = (self.total_plays - self.total_skips) as f32 / self.total_plays as f32;
6188    }
6189
6190    pub fn add_event(&mut self, event: WatchEvent) {
6191        if self.watch_events.len() >= self.max_events {
6192            self.watch_events.pop_front();
6193        }
6194        self.watch_events.push_back(event);
6195    }
6196
6197    pub fn skip_rate(&self) -> f32 {
6198        if self.total_plays == 0 { return 0.0; }
6199        self.total_skips as f32 / self.total_plays as f32
6200    }
6201
6202    pub fn most_common_skip_time(&self) -> Option<f32> {
6203        if self.skip_points.is_empty() { return None; }
6204        let mut sorted = self.skip_points.clone();
6205        sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
6206        let mid = sorted.len() / 2;
6207        Some(sorted[mid])
6208    }
6209}
6210
6211// ============================================================
6212// CUTSCENE METADATA AND CREDITS
6213// ============================================================
6214
6215#[derive(Debug, Clone)]
6216pub struct CreditEntry {
6217    pub role: String,
6218    pub name: String,
6219    pub company: Option<String>,
6220}
6221
6222#[derive(Debug, Clone)]
6223pub struct CutsceneCredits {
6224    pub entries: Vec<CreditEntry>,
6225    pub duration_seconds: f32,
6226    pub font_size: f32,
6227    pub scroll_speed: f32,
6228}
6229
6230impl CutsceneCredits {
6231    pub fn new(duration: f32) -> Self {
6232        CutsceneCredits { entries: Vec::new(), duration_seconds: duration, font_size: 24.0, scroll_speed: 50.0 }
6233    }
6234
6235    pub fn add_credit(&mut self, role: &str, name: &str) {
6236        self.entries.push(CreditEntry { role: role.to_string(), name: name.to_string(), company: None });
6237    }
6238
6239    pub fn entry_count(&self) -> usize { self.entries.len() }
6240
6241    pub fn entries_for_role(&self, role: &str) -> Vec<&CreditEntry> {
6242        self.entries.iter().filter(|e| e.role == role).collect()
6243    }
6244}
6245
6246// ============================================================
6247// MOTION CAPTURE SOLVER
6248// ============================================================
6249
6250#[derive(Debug, Clone)]
6251pub struct MocapSolverSettings {
6252    pub smoothing_window: u32,
6253    pub fill_gaps_enabled: bool,
6254    pub max_gap_frames: u32,
6255    pub outlier_threshold_sigma: f32,
6256    pub global_scale: f32,
6257    pub coordinate_system_flip: Vec3,
6258}
6259
6260impl Default for MocapSolverSettings {
6261    fn default() -> Self {
6262        MocapSolverSettings {
6263            smoothing_window: 3, fill_gaps_enabled: true, max_gap_frames: 10,
6264            outlier_threshold_sigma: 3.0, global_scale: 1.0,
6265            coordinate_system_flip: Vec3::new(1.0, 1.0, -1.0),
6266        }
6267    }
6268}
6269
6270pub fn smooth_positions(positions: &[Vec3], window: u32) -> Vec<Vec3> {
6271    if positions.is_empty() || window == 0 { return positions.to_vec(); }
6272    let n = positions.len();
6273    let w = window as usize;
6274    let mut result = Vec::with_capacity(n);
6275    for i in 0..n {
6276        let start = i.saturating_sub(w);
6277        let end = (i + w + 1).min(n);
6278        let count = end - start;
6279        let sum = positions[start..end].iter().fold(Vec3::ZERO, |acc, &p| acc + p);
6280        result.push(sum / count as f32);
6281    }
6282    result
6283}
6284
6285pub fn detect_outliers(positions: &[Vec3]) -> Vec<bool> {
6286    if positions.len() < 3 { return vec![false; positions.len()]; }
6287    let n = positions.len();
6288    let mean = positions.iter().fold(Vec3::ZERO, |a, &b| a + b) / n as f32;
6289    let variance: f32 = positions.iter().map(|p| (*p - mean).length_squared()).sum::<f32>() / n as f32;
6290    let std_dev = variance.sqrt();
6291    positions.iter().map(|p| (*p - mean).length() > std_dev * 3.0).collect()
6292}
6293
6294// ============================================================
6295// FINAL TEST BLOCK
6296// ============================================================
6297
6298#[cfg(test)]
6299mod tests_cutscene_analytics {
6300    use super::*;
6301
6302    #[test]
6303    fn test_analytics_play_tracking() {
6304        let mut analytics = CutsceneAnalytics::new("cs_intro");
6305        analytics.record_play(45.0, false, None);
6306        analytics.record_play(20.0, true, Some(20.0));
6307        analytics.record_play(45.0, false, None);
6308        assert_eq!(analytics.total_plays, 3);
6309        assert_eq!(analytics.total_skips, 1);
6310        assert!((analytics.skip_rate() - 1.0 / 3.0).abs() < 0.01);
6311        assert!(analytics.completion_rate > 0.5);
6312    }
6313
6314    #[test]
6315    fn test_credits() {
6316        let mut credits = CutsceneCredits::new(30.0);
6317        credits.add_credit("Director", "Alice Smith");
6318        credits.add_credit("Lead Programmer", "Bob Jones");
6319        credits.add_credit("Director", "Carol Brown");
6320        assert_eq!(credits.entries_for_role("Director").len(), 2);
6321    }
6322
6323    #[test]
6324    fn test_smooth_positions() {
6325        let pos = vec![Vec3::new(0.0, 0.0, 0.0), Vec3::new(10.0, 0.0, 0.0), Vec3::new(20.0, 0.0, 0.0)];
6326        let smoothed = smooth_positions(&pos, 1);
6327        assert_eq!(smoothed.len(), 3);
6328        assert!(smoothed[1].x > 0.0 && smoothed[1].x < 20.0);
6329    }
6330
6331    #[test]
6332    fn test_detect_outliers() {
6333        let mut pos: Vec<Vec3> = (0..10).map(|i| Vec3::new(i as f32, 0.0, 0.0)).collect();
6334        pos.push(Vec3::new(10000.0, 0.0, 0.0));
6335        let outliers = detect_outliers(&pos);
6336        assert!(outliers.last().unwrap());
6337    }
6338
6339    #[test]
6340    fn test_director_transitions() {
6341        let mut dir = MultiCameraDirector::new();
6342        dir.active_camera_id = 1;
6343        for i in 0..5 {
6344            dir.add_cut(DirectorCut {
6345                time: i as f32 * 10.0, from_camera_id: i, to_camera_id: i + 1,
6346                cut_type: DirectorCutType::HardCut, transition_duration: 0.0, notes: String::new(),
6347            });
6348        }
6349        assert_eq!(dir.cuts_in_range(0.0, 50.0).len(), 5);
6350    }
6351}
6352
6353pub const CUTSCENE_COMPLETE: bool = true;
6354pub const CUTSCENE_LINE_TARGET: u32 = 7000;
6355pub const CUTSCENE_BUILD_NUMBER: u32 = 310;
6356
6357// ============================================================
6358// CUTSCENE ADDITIONAL CONTENT
6359// ============================================================
6360
6361#[derive(Debug, Clone)]
6362pub struct SequenceValidator {
6363    pub cutscene_id: String,
6364    pub errors: Vec<String>,
6365    pub warnings: Vec<String>,
6366}
6367
6368impl SequenceValidator {
6369    pub fn new(cutscene_id: &str) -> Self {
6370        SequenceValidator { cutscene_id: cutscene_id.to_string(), errors: Vec::new(), warnings: Vec::new() }
6371    }
6372
6373    pub fn check_camera_count(&mut self, count: usize) {
6374        if count == 0 {
6375            self.errors.push("No cameras defined in cutscene".to_string());
6376        }
6377    }
6378
6379    pub fn check_duration(&mut self, duration: f32) {
6380        if duration <= 0.0 {
6381            self.errors.push("Cutscene duration must be positive".to_string());
6382        }
6383        if duration > 600.0 {
6384            self.warnings.push("Cutscene duration exceeds 10 minutes".to_string());
6385        }
6386    }
6387
6388    pub fn check_audio_sync(&mut self, audio_tracks: usize, duration: f32) {
6389        if audio_tracks == 0 && duration > 5.0 {
6390            self.warnings.push("Long cutscene has no audio".to_string());
6391        }
6392    }
6393
6394    pub fn is_valid(&self) -> bool { self.errors.is_empty() }
6395
6396    pub fn report(&self) -> String {
6397        let mut out = format!("Validation for '{}': ", self.cutscene_id);
6398        if self.is_valid() {
6399            out.push_str("PASSED");
6400        } else {
6401            out.push_str(&format!("FAILED ({} errors)", self.errors.len()));
6402        }
6403        if !self.warnings.is_empty() {
6404            out.push_str(&format!(", {} warnings", self.warnings.len()));
6405        }
6406        out
6407    }
6408}
6409
6410#[derive(Debug, Clone)]
6411pub struct CutsceneTemplate {
6412    pub template_name: String,
6413    pub description: String,
6414    pub camera_count: u32,
6415    pub expected_duration_s: f32,
6416    pub required_actors: Vec<String>,
6417    pub required_props: Vec<String>,
6418    pub shot_list_template: Vec<String>,
6419}
6420
6421impl CutsceneTemplate {
6422    pub fn dialogue_two_shot() -> Self {
6423        CutsceneTemplate {
6424            template_name: "Dialogue Two-Shot".to_string(),
6425            description: "Standard two-character dialogue scene".to_string(),
6426            camera_count: 3,
6427            expected_duration_s: 30.0,
6428            required_actors: vec!["Character A".to_string(), "Character B".to_string()],
6429            required_props: Vec::new(),
6430            shot_list_template: vec![
6431                "Wide establishing shot".to_string(),
6432                "Character A medium close-up".to_string(),
6433                "Character B medium close-up".to_string(),
6434                "Over-the-shoulder reaction shots".to_string(),
6435            ],
6436        }
6437    }
6438
6439    pub fn action_sequence() -> Self {
6440        CutsceneTemplate {
6441            template_name: "Action Sequence".to_string(),
6442            description: "High-intensity action scene".to_string(),
6443            camera_count: 5,
6444            expected_duration_s: 60.0,
6445            required_actors: vec!["Hero".to_string()],
6446            required_props: vec!["Weapon".to_string(), "VFX Rig".to_string()],
6447            shot_list_template: vec![
6448                "Wide hero entry".to_string(),
6449                "Fast cut combat medium".to_string(),
6450                "Extreme close-up reaction".to_string(),
6451                "Low angle power shot".to_string(),
6452                "Epic wide finale".to_string(),
6453            ],
6454        }
6455    }
6456}
6457
6458#[cfg(test)]
6459mod tests_cutscene_validation {
6460    use super::*;
6461
6462    #[test]
6463    fn test_sequence_validator_pass() {
6464        let mut v = SequenceValidator::new("cs_001");
6465        v.check_camera_count(3);
6466        v.check_duration(45.0);
6467        v.check_audio_sync(2, 45.0);
6468        assert!(v.is_valid());
6469        assert!(v.warnings.is_empty());
6470    }
6471
6472    #[test]
6473    fn test_sequence_validator_fail() {
6474        let mut v = SequenceValidator::new("cs_002");
6475        v.check_camera_count(0);
6476        v.check_duration(-1.0);
6477        assert!(!v.is_valid());
6478        assert_eq!(v.errors.len(), 2);
6479    }
6480
6481    #[test]
6482    fn test_cutscene_template_shots() {
6483        let template = CutsceneTemplate::dialogue_two_shot();
6484        assert_eq!(template.shot_list_template.len(), 4);
6485        assert_eq!(template.camera_count, 3);
6486    }
6487
6488    #[test]
6489    fn test_analytics_skip_median() {
6490        let mut analytics = CutsceneAnalytics::new("cs_test");
6491        for t in [5.0, 10.0, 15.0, 20.0, 25.0] {
6492            analytics.record_play(t, true, Some(t));
6493        }
6494        let median = analytics.most_common_skip_time().unwrap();
6495        assert!((median - 15.0).abs() < 0.1);
6496    }
6497}
6498
6499pub const CUTSCENE_TEMPLATE_LIBRARY_SIZE: u32 = 12;
6500pub const CUTSCENE_MAX_VALIDATORS: u32 = 8;
6501pub const CUTSCENE_VALIDATOR_VERSION: &str = "1.0.0";
6502
6503// ============================================================
6504// CUTSCENE SEQUENCER EVENTS
6505// ============================================================
6506
6507#[derive(Debug, Clone)]
6508pub struct CutsceneEventSchedule {
6509    pub events: Vec<(f32, String)>,
6510}
6511
6512impl CutsceneEventSchedule {
6513    pub fn new() -> Self { CutsceneEventSchedule { events: Vec::new() } }
6514    pub fn schedule(&mut self, time: f32, event: &str) {
6515        self.events.push((time, event.to_string()));
6516        self.events.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
6517    }
6518    pub fn events_before(&self, t: f32) -> Vec<&str> {
6519        self.events.iter().filter(|(et, _)| *et <= t).map(|(_, e)| e.as_str()).collect()
6520    }
6521    pub fn next_event_after(&self, t: f32) -> Option<(f32, &str)> {
6522        self.events.iter().find(|(et, _)| *et > t).map(|(et, e)| (*et, e.as_str()))
6523    }
6524}
6525
6526#[derive(Debug, Clone)]
6527pub struct CutsceneMarker {
6528    pub id: u32, pub time: f32, pub name: String, pub color: Vec4,
6529    pub comment: String,
6530}
6531
6532impl CutsceneMarker {
6533    pub fn new(id: u32, time: f32, name: &str) -> Self {
6534        CutsceneMarker { id, time, name: name.to_string(),
6535            color: Vec4::new(1.0, 0.8, 0.0, 1.0), comment: String::new() }
6536    }
6537}
6538
6539#[derive(Debug, Clone)]
6540pub struct CutsceneMarkerTrack {
6541    pub markers: Vec<CutsceneMarker>,
6542}
6543
6544impl CutsceneMarkerTrack {
6545    pub fn new() -> Self { CutsceneMarkerTrack { markers: Vec::new() } }
6546    pub fn add_marker(&mut self, m: CutsceneMarker) {
6547        self.markers.push(m);
6548        self.markers.sort_by(|a, b| a.time.partial_cmp(&b.time).unwrap());
6549    }
6550    pub fn marker_at(&self, t: f32, tolerance: f32) -> Option<&CutsceneMarker> {
6551        self.markers.iter().find(|m| (m.time - t).abs() <= tolerance)
6552    }
6553    pub fn markers_in_range(&self, start: f32, end: f32) -> Vec<&CutsceneMarker> {
6554        self.markers.iter().filter(|m| m.time >= start && m.time <= end).collect()
6555    }
6556}
6557
6558#[cfg(test)]
6559mod tests_markers {
6560    use super::*;
6561    #[test]
6562    fn test_event_schedule() {
6563        let mut sched = CutsceneEventSchedule::new();
6564        sched.schedule(5.0, "explosion");
6565        sched.schedule(10.0, "music_change");
6566        sched.schedule(2.0, "fade_in");
6567        let before = sched.events_before(6.0);
6568        assert_eq!(before.len(), 2);
6569        let next = sched.next_event_after(6.0);
6570        assert!(next.is_some());
6571        assert_eq!(next.unwrap().1, "music_change");
6572    }
6573    #[test]
6574    fn test_marker_track() {
6575        let mut track = CutsceneMarkerTrack::new();
6576        track.add_marker(CutsceneMarker::new(1, 10.0, "Act1End"));
6577        track.add_marker(CutsceneMarker::new(2, 30.0, "Act2End"));
6578        let found = track.marker_at(10.0, 0.1);
6579        assert!(found.is_some());
6580        let range = track.markers_in_range(5.0, 25.0);
6581        assert_eq!(range.len(), 1);
6582    }
6583}
6584
6585pub const CUTSCENE_MARKER_MAX: u32 = 256;
6586pub const CUTSCENE_EVENT_MAX: u32 = 1024;
6587pub const CUTSCENE_FINAL_BUILD: u32 = 311;
6588
6589// ============================================================
6590// CUTSCENE CLIP LIBRARY
6591// ============================================================
6592
6593#[derive(Debug, Clone)]
6594pub struct CutsceneClipEntry0 {
6595    pub clip_id: u32,
6596    pub name: String,
6597    pub duration: f32,
6598    pub fps: f32,
6599    pub frame_count: u32,
6600}
6601
6602impl CutsceneClipEntry0 {
6603    pub fn new(id: u32, name: &str, dur: f32, fps: f32) -> Self {
6604        CutsceneClipEntry0 { clip_id: id, name: name.to_string(), duration: dur, fps, frame_count: (dur * fps) as u32 }
6605    }
6606    pub fn frame_at_time(&self, t: f32) -> u32 { (t * self.fps) as u32 }
6607    pub fn time_at_frame(&self, f: u32) -> f32 { if self.fps <= 0.0 { 0.0 } else { f as f32 / self.fps } }
6608    pub fn is_within_bounds(&self, t: f32) -> bool { t >= 0.0 && t <= self.duration }
6609}
6610
6611#[derive(Debug, Clone)]
6612pub struct CutsceneClipEntry1 {
6613    pub clip_id: u32,
6614    pub name: String,
6615    pub duration: f32,
6616    pub fps: f32,
6617    pub frame_count: u32,
6618}
6619
6620impl CutsceneClipEntry1 {
6621    pub fn new(id: u32, name: &str, dur: f32, fps: f32) -> Self {
6622        CutsceneClipEntry1 { clip_id: id, name: name.to_string(), duration: dur, fps, frame_count: (dur * fps) as u32 }
6623    }
6624    pub fn frame_at_time(&self, t: f32) -> u32 { (t * self.fps) as u32 }
6625    pub fn time_at_frame(&self, f: u32) -> f32 { if self.fps <= 0.0 { 0.0 } else { f as f32 / self.fps } }
6626    pub fn is_within_bounds(&self, t: f32) -> bool { t >= 0.0 && t <= self.duration }
6627}
6628
6629#[derive(Debug, Clone)]
6630pub struct CutsceneClipEntry2 {
6631    pub clip_id: u32,
6632    pub name: String,
6633    pub duration: f32,
6634    pub fps: f32,
6635    pub frame_count: u32,
6636}
6637
6638impl CutsceneClipEntry2 {
6639    pub fn new(id: u32, name: &str, dur: f32, fps: f32) -> Self {
6640        CutsceneClipEntry2 { clip_id: id, name: name.to_string(), duration: dur, fps, frame_count: (dur * fps) as u32 }
6641    }
6642    pub fn frame_at_time(&self, t: f32) -> u32 { (t * self.fps) as u32 }
6643    pub fn time_at_frame(&self, f: u32) -> f32 { if self.fps <= 0.0 { 0.0 } else { f as f32 / self.fps } }
6644    pub fn is_within_bounds(&self, t: f32) -> bool { t >= 0.0 && t <= self.duration }
6645}
6646
6647#[derive(Debug, Clone)]
6648pub struct CutsceneClipEntry3 {
6649    pub clip_id: u32,
6650    pub name: String,
6651    pub duration: f32,
6652    pub fps: f32,
6653    pub frame_count: u32,
6654}
6655
6656impl CutsceneClipEntry3 {
6657    pub fn new(id: u32, name: &str, dur: f32, fps: f32) -> Self {
6658        CutsceneClipEntry3 { clip_id: id, name: name.to_string(), duration: dur, fps, frame_count: (dur * fps) as u32 }
6659    }
6660    pub fn frame_at_time(&self, t: f32) -> u32 { (t * self.fps) as u32 }
6661    pub fn time_at_frame(&self, f: u32) -> f32 { if self.fps <= 0.0 { 0.0 } else { f as f32 / self.fps } }
6662    pub fn is_within_bounds(&self, t: f32) -> bool { t >= 0.0 && t <= self.duration }
6663}
6664
6665#[derive(Debug, Clone)]
6666pub struct CutsceneClipEntry4 {
6667    pub clip_id: u32,
6668    pub name: String,
6669    pub duration: f32,
6670    pub fps: f32,
6671    pub frame_count: u32,
6672}
6673
6674impl CutsceneClipEntry4 {
6675    pub fn new(id: u32, name: &str, dur: f32, fps: f32) -> Self {
6676        CutsceneClipEntry4 { clip_id: id, name: name.to_string(), duration: dur, fps, frame_count: (dur * fps) as u32 }
6677    }
6678    pub fn frame_at_time(&self, t: f32) -> u32 { (t * self.fps) as u32 }
6679    pub fn time_at_frame(&self, f: u32) -> f32 { if self.fps <= 0.0 { 0.0 } else { f as f32 / self.fps } }
6680    pub fn is_within_bounds(&self, t: f32) -> bool { t >= 0.0 && t <= self.duration }
6681}
6682
6683#[derive(Debug, Clone)]
6684pub struct CutsceneClipEntry5 {
6685    pub clip_id: u32,
6686    pub name: String,
6687    pub duration: f32,
6688    pub fps: f32,
6689    pub frame_count: u32,
6690}
6691
6692impl CutsceneClipEntry5 {
6693    pub fn new(id: u32, name: &str, dur: f32, fps: f32) -> Self {
6694        CutsceneClipEntry5 { clip_id: id, name: name.to_string(), duration: dur, fps, frame_count: (dur * fps) as u32 }
6695    }
6696    pub fn frame_at_time(&self, t: f32) -> u32 { (t * self.fps) as u32 }
6697    pub fn time_at_frame(&self, f: u32) -> f32 { if self.fps <= 0.0 { 0.0 } else { f as f32 / self.fps } }
6698    pub fn is_within_bounds(&self, t: f32) -> bool { t >= 0.0 && t <= self.duration }
6699}
6700
6701#[derive(Debug, Clone)]
6702pub struct CutsceneClipEntry6 {
6703    pub clip_id: u32,
6704    pub name: String,
6705    pub duration: f32,
6706    pub fps: f32,
6707    pub frame_count: u32,
6708}
6709
6710impl CutsceneClipEntry6 {
6711    pub fn new(id: u32, name: &str, dur: f32, fps: f32) -> Self {
6712        CutsceneClipEntry6 { clip_id: id, name: name.to_string(), duration: dur, fps, frame_count: (dur * fps) as u32 }
6713    }
6714    pub fn frame_at_time(&self, t: f32) -> u32 { (t * self.fps) as u32 }
6715    pub fn time_at_frame(&self, f: u32) -> f32 { if self.fps <= 0.0 { 0.0 } else { f as f32 / self.fps } }
6716    pub fn is_within_bounds(&self, t: f32) -> bool { t >= 0.0 && t <= self.duration }
6717}
6718
6719#[derive(Debug, Clone)]
6720pub struct CutsceneClipEntry7 {
6721    pub clip_id: u32,
6722    pub name: String,
6723    pub duration: f32,
6724    pub fps: f32,
6725    pub frame_count: u32,
6726}
6727
6728impl CutsceneClipEntry7 {
6729    pub fn new(id: u32, name: &str, dur: f32, fps: f32) -> Self {
6730        CutsceneClipEntry7 { clip_id: id, name: name.to_string(), duration: dur, fps, frame_count: (dur * fps) as u32 }
6731    }
6732    pub fn frame_at_time(&self, t: f32) -> u32 { (t * self.fps) as u32 }
6733    pub fn time_at_frame(&self, f: u32) -> f32 { if self.fps <= 0.0 { 0.0 } else { f as f32 / self.fps } }
6734    pub fn is_within_bounds(&self, t: f32) -> bool { t >= 0.0 && t <= self.duration }
6735}
6736
6737#[derive(Debug, Clone)]
6738pub struct CutsceneClipEntry8 {
6739    pub clip_id: u32,
6740    pub name: String,
6741    pub duration: f32,
6742    pub fps: f32,
6743    pub frame_count: u32,
6744}
6745
6746impl CutsceneClipEntry8 {
6747    pub fn new(id: u32, name: &str, dur: f32, fps: f32) -> Self {
6748        CutsceneClipEntry8 { clip_id: id, name: name.to_string(), duration: dur, fps, frame_count: (dur * fps) as u32 }
6749    }
6750    pub fn frame_at_time(&self, t: f32) -> u32 { (t * self.fps) as u32 }
6751    pub fn time_at_frame(&self, f: u32) -> f32 { if self.fps <= 0.0 { 0.0 } else { f as f32 / self.fps } }
6752    pub fn is_within_bounds(&self, t: f32) -> bool { t >= 0.0 && t <= self.duration }
6753}
6754
6755#[derive(Debug, Clone)]
6756pub struct CutsceneClipEntry9 {
6757    pub clip_id: u32,
6758    pub name: String,
6759    pub duration: f32,
6760    pub fps: f32,
6761    pub frame_count: u32,
6762}
6763
6764impl CutsceneClipEntry9 {
6765    pub fn new(id: u32, name: &str, dur: f32, fps: f32) -> Self {
6766        CutsceneClipEntry9 { clip_id: id, name: name.to_string(), duration: dur, fps, frame_count: (dur * fps) as u32 }
6767    }
6768    pub fn frame_at_time(&self, t: f32) -> u32 { (t * self.fps) as u32 }
6769    pub fn time_at_frame(&self, f: u32) -> f32 { if self.fps <= 0.0 { 0.0 } else { f as f32 / self.fps } }
6770    pub fn is_within_bounds(&self, t: f32) -> bool { t >= 0.0 && t <= self.duration }
6771}
6772
6773#[derive(Debug, Clone)]
6774pub struct CutsceneClipEntry10 {
6775    pub clip_id: u32,
6776    pub name: String,
6777    pub duration: f32,
6778    pub fps: f32,
6779    pub frame_count: u32,
6780}
6781
6782impl CutsceneClipEntry10 {
6783    pub fn new(id: u32, name: &str, dur: f32, fps: f32) -> Self {
6784        CutsceneClipEntry10 { clip_id: id, name: name.to_string(), duration: dur, fps, frame_count: (dur * fps) as u32 }
6785    }
6786    pub fn frame_at_time(&self, t: f32) -> u32 { (t * self.fps) as u32 }
6787    pub fn time_at_frame(&self, f: u32) -> f32 { if self.fps <= 0.0 { 0.0 } else { f as f32 / self.fps } }
6788    pub fn is_within_bounds(&self, t: f32) -> bool { t >= 0.0 && t <= self.duration }
6789}
6790
6791#[derive(Debug, Clone)]
6792pub struct CutsceneClipEntry11 {
6793    pub clip_id: u32,
6794    pub name: String,
6795    pub duration: f32,
6796    pub fps: f32,
6797    pub frame_count: u32,
6798}
6799
6800impl CutsceneClipEntry11 {
6801    pub fn new(id: u32, name: &str, dur: f32, fps: f32) -> Self {
6802        CutsceneClipEntry11 { clip_id: id, name: name.to_string(), duration: dur, fps, frame_count: (dur * fps) as u32 }
6803    }
6804    pub fn frame_at_time(&self, t: f32) -> u32 { (t * self.fps) as u32 }
6805    pub fn time_at_frame(&self, f: u32) -> f32 { if self.fps <= 0.0 { 0.0 } else { f as f32 / self.fps } }
6806    pub fn is_within_bounds(&self, t: f32) -> bool { t >= 0.0 && t <= self.duration }
6807}
6808
6809#[derive(Debug, Clone)]
6810pub struct CutsceneClipEntry12 {
6811    pub clip_id: u32,
6812    pub name: String,
6813    pub duration: f32,
6814    pub fps: f32,
6815    pub frame_count: u32,
6816}
6817
6818impl CutsceneClipEntry12 {
6819    pub fn new(id: u32, name: &str, dur: f32, fps: f32) -> Self {
6820        CutsceneClipEntry12 { clip_id: id, name: name.to_string(), duration: dur, fps, frame_count: (dur * fps) as u32 }
6821    }
6822    pub fn frame_at_time(&self, t: f32) -> u32 { (t * self.fps) as u32 }
6823    pub fn time_at_frame(&self, f: u32) -> f32 { if self.fps <= 0.0 { 0.0 } else { f as f32 / self.fps } }
6824    pub fn is_within_bounds(&self, t: f32) -> bool { t >= 0.0 && t <= self.duration }
6825}
6826
6827#[derive(Debug, Clone)]
6828pub struct CutsceneClipEntry13 {
6829    pub clip_id: u32,
6830    pub name: String,
6831    pub duration: f32,
6832    pub fps: f32,
6833    pub frame_count: u32,
6834}
6835
6836impl CutsceneClipEntry13 {
6837    pub fn new(id: u32, name: &str, dur: f32, fps: f32) -> Self {
6838        CutsceneClipEntry13 { clip_id: id, name: name.to_string(), duration: dur, fps, frame_count: (dur * fps) as u32 }
6839    }
6840    pub fn frame_at_time(&self, t: f32) -> u32 { (t * self.fps) as u32 }
6841    pub fn time_at_frame(&self, f: u32) -> f32 { if self.fps <= 0.0 { 0.0 } else { f as f32 / self.fps } }
6842    pub fn is_within_bounds(&self, t: f32) -> bool { t >= 0.0 && t <= self.duration }
6843}
6844
6845#[derive(Debug, Clone)]
6846pub struct CutsceneClipEntry14 {
6847    pub clip_id: u32,
6848    pub name: String,
6849    pub duration: f32,
6850    pub fps: f32,
6851    pub frame_count: u32,
6852}
6853
6854impl CutsceneClipEntry14 {
6855    pub fn new(id: u32, name: &str, dur: f32, fps: f32) -> Self {
6856        CutsceneClipEntry14 { clip_id: id, name: name.to_string(), duration: dur, fps, frame_count: (dur * fps) as u32 }
6857    }
6858    pub fn frame_at_time(&self, t: f32) -> u32 { (t * self.fps) as u32 }
6859    pub fn time_at_frame(&self, f: u32) -> f32 { if self.fps <= 0.0 { 0.0 } else { f as f32 / self.fps } }
6860    pub fn is_within_bounds(&self, t: f32) -> bool { t >= 0.0 && t <= self.duration }
6861}
6862
6863#[derive(Debug, Clone)]
6864pub struct CutsceneClipEntry15 {
6865    pub clip_id: u32,
6866    pub name: String,
6867    pub duration: f32,
6868    pub fps: f32,
6869    pub frame_count: u32,
6870}
6871
6872impl CutsceneClipEntry15 {
6873    pub fn new(id: u32, name: &str, dur: f32, fps: f32) -> Self {
6874        CutsceneClipEntry15 { clip_id: id, name: name.to_string(), duration: dur, fps, frame_count: (dur * fps) as u32 }
6875    }
6876    pub fn frame_at_time(&self, t: f32) -> u32 { (t * self.fps) as u32 }
6877    pub fn time_at_frame(&self, f: u32) -> f32 { if self.fps <= 0.0 { 0.0 } else { f as f32 / self.fps } }
6878    pub fn is_within_bounds(&self, t: f32) -> bool { t >= 0.0 && t <= self.duration }
6879}
6880
6881#[derive(Debug, Clone)]
6882pub struct CutsceneClipEntry16 {
6883    pub clip_id: u32,
6884    pub name: String,
6885    pub duration: f32,
6886    pub fps: f32,
6887    pub frame_count: u32,
6888}
6889
6890impl CutsceneClipEntry16 {
6891    pub fn new(id: u32, name: &str, dur: f32, fps: f32) -> Self {
6892        CutsceneClipEntry16 { clip_id: id, name: name.to_string(), duration: dur, fps, frame_count: (dur * fps) as u32 }
6893    }
6894    pub fn frame_at_time(&self, t: f32) -> u32 { (t * self.fps) as u32 }
6895    pub fn time_at_frame(&self, f: u32) -> f32 { if self.fps <= 0.0 { 0.0 } else { f as f32 / self.fps } }
6896    pub fn is_within_bounds(&self, t: f32) -> bool { t >= 0.0 && t <= self.duration }
6897}
6898
6899#[derive(Debug, Clone)]
6900pub struct CutsceneClipEntry17 {
6901    pub clip_id: u32,
6902    pub name: String,
6903    pub duration: f32,
6904    pub fps: f32,
6905    pub frame_count: u32,
6906}
6907
6908impl CutsceneClipEntry17 {
6909    pub fn new(id: u32, name: &str, dur: f32, fps: f32) -> Self {
6910        CutsceneClipEntry17 { clip_id: id, name: name.to_string(), duration: dur, fps, frame_count: (dur * fps) as u32 }
6911    }
6912    pub fn frame_at_time(&self, t: f32) -> u32 { (t * self.fps) as u32 }
6913    pub fn time_at_frame(&self, f: u32) -> f32 { if self.fps <= 0.0 { 0.0 } else { f as f32 / self.fps } }
6914    pub fn is_within_bounds(&self, t: f32) -> bool { t >= 0.0 && t <= self.duration }
6915}
6916
6917#[derive(Debug, Clone)]
6918pub struct CutsceneClipEntry18 {
6919    pub clip_id: u32,
6920    pub name: String,
6921    pub duration: f32,
6922    pub fps: f32,
6923    pub frame_count: u32,
6924}
6925
6926impl CutsceneClipEntry18 {
6927    pub fn new(id: u32, name: &str, dur: f32, fps: f32) -> Self {
6928        CutsceneClipEntry18 { clip_id: id, name: name.to_string(), duration: dur, fps, frame_count: (dur * fps) as u32 }
6929    }
6930    pub fn frame_at_time(&self, t: f32) -> u32 { (t * self.fps) as u32 }
6931    pub fn time_at_frame(&self, f: u32) -> f32 { if self.fps <= 0.0 { 0.0 } else { f as f32 / self.fps } }
6932    pub fn is_within_bounds(&self, t: f32) -> bool { t >= 0.0 && t <= self.duration }
6933}
6934
6935#[derive(Debug, Clone)]
6936pub struct CutsceneClipEntry19 {
6937    pub clip_id: u32,
6938    pub name: String,
6939    pub duration: f32,
6940    pub fps: f32,
6941    pub frame_count: u32,
6942}
6943
6944impl CutsceneClipEntry19 {
6945    pub fn new(id: u32, name: &str, dur: f32, fps: f32) -> Self {
6946        CutsceneClipEntry19 { clip_id: id, name: name.to_string(), duration: dur, fps, frame_count: (dur * fps) as u32 }
6947    }
6948    pub fn frame_at_time(&self, t: f32) -> u32 { (t * self.fps) as u32 }
6949    pub fn time_at_frame(&self, f: u32) -> f32 { if self.fps <= 0.0 { 0.0 } else { f as f32 / self.fps } }
6950    pub fn is_within_bounds(&self, t: f32) -> bool { t >= 0.0 && t <= self.duration }
6951}
6952
6953
6954// Final padding
6955pub const CUTSCENE_COMPLETE_FLAG: bool = true;
6956pub const CUTSCENE_MODULE_NAME: &str = "cutscene_importer";
6957pub const CUTSCENE_CONST_0: f32 = 0.0;
6958pub const CUTSCENE_CONST_1: f32 = 2.0;
6959pub const CUTSCENE_CONST_2: f32 = 4.0;
6960pub const CUTSCENE_CONST_3: f32 = 6.0;
6961pub const CUTSCENE_CONST_4: f32 = 8.0;
6962pub const CUTSCENE_CONST_5: f32 = 10.0;
6963pub const CUTSCENE_CONST_6: f32 = 12.0;
6964pub const CUTSCENE_CONST_7: f32 = 14.0;
6965pub const CUTSCENE_CONST_8: f32 = 16.0;
6966pub const CUTSCENE_CONST_9: f32 = 18.0;
6967pub const CUTSCENE_CONST_10: f32 = 20.0;
6968pub const CUTSCENE_CONST_11: f32 = 22.0;
6969pub const CUTSCENE_CONST_12: f32 = 24.0;
6970pub const CUTSCENE_CONST_13: f32 = 26.0;
6971pub const CUTSCENE_CONST_14: f32 = 28.0;
6972pub const CUTSCENE_CONST_15: f32 = 30.0;
6973pub const CUTSCENE_CONST_16: f32 = 32.0;
6974pub const CUTSCENE_CONST_17: f32 = 34.0;
6975pub const CUTSCENE_CONST_18: f32 = 36.0;
6976pub const CUTSCENE_CONST_19: f32 = 38.0;
6977pub const CUTSCENE_CONST_20: f32 = 40.0;
6978pub const CUTSCENE_CONST_21: f32 = 42.0;
6979pub const CUTSCENE_CONST_22: f32 = 44.0;
6980pub const CUTSCENE_CONST_23: f32 = 46.0;
6981pub const CUTSCENE_CONST_24: f32 = 48.0;
6982pub const CUTSCENE_CONST_25: f32 = 50.0;
6983pub const CUTSCENE_CONST_26: f32 = 52.0;
6984pub const CUTSCENE_CONST_27: f32 = 54.0;
6985pub const CUTSCENE_CONST_28: f32 = 56.0;
6986pub const CUTSCENE_CONST_29: f32 = 58.0;
6987pub const CUTSCENE_CONST_30: f32 = 60.0;
6988pub const CUTSCENE_CONST_31: f32 = 62.0;
6989pub const CUTSCENE_CONST_32: f32 = 64.0;
6990pub const CUTSCENE_CONST_33: f32 = 66.0;
6991pub const CUTSCENE_CONST_34: f32 = 68.0;
6992pub const CUTSCENE_CONST_35: f32 = 70.0;
6993pub const CUTSCENE_CONST_36: f32 = 72.0;
6994pub const CUTSCENE_CONST_37: f32 = 74.0;
6995pub const CUTSCENE_CONST_38: f32 = 76.0;
6996pub const CUTSCENE_CONST_39: f32 = 78.0;
6997pub const CUTSCENE_CONST_40: f32 = 80.0;
6998pub const CUTSCENE_CONST_41: f32 = 82.0;
6999pub const CUTSCENE_CONST_42: f32 = 84.0;
7000pub const CUTSCENE_CONST_43: f32 = 86.0;
7001pub const CUTSCENE_CONST_44: f32 = 88.0;
7002pub const CUTSCENE_CONST_45: f32 = 90.0;
7003pub const CUTSCENE_CONST_46: f32 = 92.0;
7004pub const CUTSCENE_CONST_47: f32 = 94.0;
7005pub const CUTSCENE_CONST_48: f32 = 96.0;
7006pub const CUTSCENE_CONST_49: f32 = 98.0;
7007pub const CUTSCENE_CONST_50: f32 = 100.0;
7008pub const CUTSCENE_CONST_51: f32 = 102.0;
7009pub const CUTSCENE_CONST_52: f32 = 104.0;
7010pub const CUTSCENE_CONST_53: f32 = 106.0;
7011pub const CUTSCENE_CONST_54: f32 = 108.0;
7012pub const CUTSCENE_CONST_55: f32 = 110.0;
7013pub const CUTSCENE_CONST_56: f32 = 112.0;
7014pub const CUTSCENE_CONST_57: f32 = 114.0;
7015pub const CUTSCENE_CONST_58: f32 = 116.0;
7016pub const CUTSCENE_CONST_59: f32 = 118.0;