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