1use std::collections::BTreeMap;
4
5use oxideav_core::{Rational, TimeBase};
6
7use crate::audio::AudioCue;
8use crate::duration::{SceneDuration, TimeStamp};
9use crate::object::{Canvas, SceneObject};
10use crate::page::Page;
11
12#[derive(Clone, Debug)]
33pub struct Scene {
34 pub canvas: Canvas,
35 pub duration: SceneDuration,
36 pub time_base: TimeBase,
39 pub framerate: Rational,
48 pub sample_rate: u32,
50 pub background: Background,
51 pub objects: Vec<SceneObject>,
54 pub audio: Vec<AudioCue>,
55 pub metadata: Metadata,
56 pub pages: Option<Vec<Page>>,
61}
62
63impl Default for Scene {
64 fn default() -> Self {
65 Scene {
66 canvas: Canvas::raster(1920, 1080),
67 duration: SceneDuration::Finite(0),
68 time_base: TimeBase::new(1, 1_000),
69 framerate: Rational::new(30, 1),
70 sample_rate: 48_000,
71 background: Background::default(),
72 objects: Vec::new(),
73 audio: Vec::new(),
74 metadata: Metadata::default(),
75 pages: None,
76 }
77 }
78}
79
80impl Scene {
81 pub fn sort_by_z_order(&mut self) {
85 self.objects.sort_by_key(|o| o.z_order);
86 }
87
88 pub fn frame_to_timestamp(&self, frame_index: u64) -> TimeStamp {
94 let tb = self.time_base.0;
95 let num = frame_index as i128 * self.framerate.den as i128 * tb.den as i128;
96 let den = self.framerate.num as i128 * tb.num as i128;
97 if den == 0 {
98 0
99 } else {
100 (num / den) as TimeStamp
101 }
102 }
103
104 pub fn frame_count(&self) -> Option<u64> {
108 let end = self.duration.end()?;
109 let tb = self.time_base.0;
110 if tb.num == 0 || self.framerate.den == 0 {
111 return Some(0);
112 }
113 let num = (end as i128) * self.framerate.num as i128 * tb.num as i128;
114 let den = self.framerate.den as i128 * tb.den as i128;
115 if den == 0 {
116 Some(0)
117 } else {
118 Some((num / den).max(0) as u64)
119 }
120 }
121
122 pub fn visible_at(&self, t: crate::duration::TimeStamp) -> Vec<&SceneObject> {
125 let mut refs: Vec<&SceneObject> = self
126 .objects
127 .iter()
128 .filter(|o| o.lifetime.is_live_at(t))
129 .collect();
130 refs.sort_by_key(|o| o.z_order);
131 refs
132 }
133
134 pub fn is_paged(&self) -> bool {
137 self.pages.as_ref().is_some_and(|p| !p.is_empty())
138 }
139
140 pub fn pages_to_timeline(&self, per_page_duration_ms: u64) -> Vec<(usize, TimeStamp)> {
153 let Some(ref pages) = self.pages else {
154 return Vec::new();
155 };
156 let tb = self.time_base.0;
159 let num = (per_page_duration_ms as i128) * (tb.den as i128);
160 let den = (tb.num as i128) * 1000;
161 let ticks_per_page: TimeStamp = if den == 0 {
162 0
163 } else {
164 (num / den) as TimeStamp
165 };
166 let mut out = Vec::with_capacity(pages.len());
167 let mut t: TimeStamp = 0;
168 for (i, _) in pages.iter().enumerate() {
169 out.push((i, t));
170 t = t.saturating_add(ticks_per_page);
171 }
172 out
173 }
174
175 pub fn timeline_to_pages(&self, at_pts: &[TimeStamp]) -> Vec<TimeStamp> {
186 at_pts
187 .iter()
188 .copied()
189 .filter(|&t| self.duration.contains(t))
190 .collect()
191 }
192}
193
194#[non_exhaustive]
196#[derive(Clone, Debug)]
197pub enum Background {
198 Transparent,
200 Solid(u32),
202 LinearGradient {
204 from: u32,
205 to: u32,
206 angle_deg: f32,
209 },
210 Image(String),
213}
214
215impl Default for Background {
216 fn default() -> Self {
217 Background::Solid(0x000000FF)
218 }
219}
220
221#[derive(Clone, Debug, Default)]
232pub struct Metadata {
233 pub title: Option<String>,
234 pub author: Option<String>,
235 pub subject: Option<String>,
236 pub keywords: Vec<String>,
237 pub creator: Option<String>,
242 pub producer: Option<String>,
245 pub created_at: Option<String>,
248 pub modified_at: Option<String>,
252 pub custom: BTreeMap<String, String>,
259}
260
261#[cfg(test)]
262mod tests {
263 use super::*;
264 use crate::{animation::Animation, duration::Lifetime, id::ObjectId, object::SceneObject};
265
266 #[test]
267 fn visible_at_respects_lifetime() {
268 let mut scene = Scene {
269 duration: SceneDuration::Finite(1000),
270 ..Scene::default()
271 };
272 scene.objects.push(SceneObject {
273 id: ObjectId::new(1),
274 lifetime: Lifetime {
275 start: 100,
276 end: Some(200),
277 },
278 ..SceneObject::default()
279 });
280 scene.objects.push(SceneObject {
281 id: ObjectId::new(2),
282 lifetime: Lifetime::default(),
283 ..SceneObject::default()
284 });
285 let v = scene.visible_at(50);
286 assert_eq!(v.len(), 1);
287 assert_eq!(v[0].id, ObjectId::new(2));
288
289 let v = scene.visible_at(150);
290 assert_eq!(v.len(), 2);
291 }
292
293 #[test]
294 fn frame_to_timestamp_30fps_1ms_tb() {
295 let scene = Scene::default(); assert_eq!(scene.frame_to_timestamp(0), 0);
297 assert_eq!(scene.frame_to_timestamp(1), 33);
299 assert_eq!(scene.frame_to_timestamp(30), 1000);
300 }
301
302 #[test]
303 fn frame_to_timestamp_23_976_fps() {
304 let scene = Scene {
305 time_base: TimeBase::new(1, 90_000),
306 framerate: Rational::new(24_000, 1001),
307 ..Scene::default()
308 };
309 assert_eq!(scene.frame_to_timestamp(24_000), 90_090_000);
311 }
312
313 #[test]
314 fn frame_count_finite() {
315 let scene = Scene {
316 duration: SceneDuration::Finite(1000), ..Scene::default()
318 };
319 assert_eq!(scene.frame_count(), Some(30));
320 }
321
322 #[test]
323 fn frame_count_indefinite_is_none() {
324 let scene = Scene {
325 duration: SceneDuration::Indefinite,
326 ..Scene::default()
327 };
328 assert_eq!(scene.frame_count(), None);
329 }
330
331 #[test]
332 fn default_scene_is_timeline_mode() {
333 let s = Scene::default();
334 assert!(!s.is_paged());
335 assert!(s.pages.is_none());
336 }
337
338 #[test]
339 fn scene_in_pages_mode_reports_paged() {
340 let s = Scene {
341 pages: Some(vec![Page::new(595.0, 842.0), Page::new(842.0, 595.0)]),
342 ..Scene::default()
343 };
344 assert!(s.is_paged());
345 assert_eq!(s.pages.as_ref().unwrap().len(), 2);
346 }
347
348 #[test]
349 fn empty_pages_vec_is_not_paged() {
350 let s = Scene {
351 pages: Some(Vec::new()),
352 ..Scene::default()
353 };
354 assert!(!s.is_paged());
355 }
356
357 #[test]
358 fn pages_to_timeline_advances_per_page() {
359 let s = Scene {
361 pages: Some(vec![
362 Page::new(595.0, 842.0),
363 Page::new(595.0, 842.0),
364 Page::new(595.0, 842.0),
365 ]),
366 ..Scene::default()
367 };
368 let tl = s.pages_to_timeline(100);
369 assert_eq!(tl, vec![(0, 0), (1, 100), (2, 200)]);
370 }
371
372 #[test]
373 fn pages_to_timeline_empty_for_timeline_scene() {
374 let s = Scene::default();
375 assert!(s.pages_to_timeline(100).is_empty());
376 }
377
378 #[test]
379 fn pages_to_timeline_scales_for_90khz_tb() {
380 let s = Scene {
382 time_base: TimeBase::new(1, 90_000),
383 pages: Some(vec![Page::new(100.0, 100.0); 2]),
384 ..Scene::default()
385 };
386 let tl = s.pages_to_timeline(100);
387 assert_eq!(tl, vec![(0, 0), (1, 9_000)]);
388 }
389
390 #[test]
391 fn timeline_to_pages_filters_out_of_range() {
392 let s = Scene {
393 duration: SceneDuration::Finite(1000),
394 ..Scene::default()
395 };
396 let pts = vec![-1, 0, 500, 999, 1000, 5000];
397 let kept = s.timeline_to_pages(&pts);
398 assert_eq!(kept, vec![0, 500, 999]);
399 }
400
401 #[test]
402 fn timeline_to_pages_indefinite_keeps_nonneg() {
403 let s = Scene {
404 duration: SceneDuration::Indefinite,
405 ..Scene::default()
406 };
407 let pts = vec![-1, 0, i64::MAX];
408 let kept = s.timeline_to_pages(&pts);
409 assert_eq!(kept, vec![0, i64::MAX]);
410 }
411
412 #[test]
413 fn metadata_default_is_empty() {
414 let m = Metadata::default();
415 assert!(m.title.is_none());
416 assert!(m.creator.is_none());
417 assert!(m.producer.is_none());
418 assert!(m.created_at.is_none());
419 assert!(m.modified_at.is_none());
420 assert!(m.custom.is_empty());
421 }
422
423 #[test]
424 fn metadata_custom_carries_extras() {
425 let mut m = Metadata {
426 creator: Some("MyDrawingApp 4.2".into()),
427 producer: Some("oxideav-pdf 0.1".into()),
428 modified_at: Some("2026-05-04T12:00:00Z".into()),
429 ..Metadata::default()
430 };
431 m.custom
432 .insert("dc:rights".into(), "(c) 2026 Karpeles Lab Inc.".into());
433 m.custom.insert("Trapped".into(), "False".into());
434 assert_eq!(m.creator.as_deref(), Some("MyDrawingApp 4.2"));
435 assert_eq!(m.producer.as_deref(), Some("oxideav-pdf 0.1"));
436 assert_eq!(m.modified_at.as_deref(), Some("2026-05-04T12:00:00Z"));
437 assert_eq!(m.custom.get("Trapped").map(String::as_str), Some("False"));
438 assert_eq!(m.custom.len(), 2);
439 }
440
441 #[test]
442 fn sort_by_z_order_stable_ties() {
443 let mut scene = Scene::default();
444 scene.objects.push(SceneObject {
445 id: ObjectId::new(1),
446 z_order: 5,
447 animations: vec![Animation::new(
448 crate::animation::AnimatedProperty::Opacity,
449 Vec::new(),
450 crate::animation::Easing::Linear,
451 crate::animation::Repeat::Once,
452 )],
453 ..SceneObject::default()
454 });
455 scene.objects.push(SceneObject {
456 id: ObjectId::new(2),
457 z_order: 5,
458 ..SceneObject::default()
459 });
460 scene.objects.push(SceneObject {
461 id: ObjectId::new(3),
462 z_order: 1,
463 ..SceneObject::default()
464 });
465 scene.sort_by_z_order();
466 assert_eq!(scene.objects[0].id, ObjectId::new(3));
467 assert_eq!(scene.objects[1].id, ObjectId::new(1));
468 assert_eq!(scene.objects[2].id, ObjectId::new(2));
469 }
470}