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