From 32438ce25f69de99d2523b98237f62cfa90890d7 Mon Sep 17 00:00:00 2001 From: cashmere Date: Tue, 28 Jul 2026 20:14:51 +0200 Subject: feat: port domain crates to Rust (session 1 complete) All green: cargo test --workspace (~300 tests, 0 failures). - timeline (45): placement, resize/ripple, split, snap, retime, audio separation, scene helpers - animation (73): bezier solver, channel sampling, keyframe ops, split/clamp/clone, param definitions - commands (9): Command trait, BatchCommand, history, 19 concrete commands, freeze-frame port 1:1 - audio (22): audible collection, range mixing, mastering limiter, rms/peak buckets - text (19): cosmic-text layout/measure/raster, backgrounds, decorations - graphics (52): full CSS gradient parser, tiny-skia raster, 4 shape definitions - effect-defs (16): blur definition, registry, pass resolution - mask-defs (19): all 9 mask renderers via tiny-skia - renderer (6): node model + scene builder port - desktop: GPUI projects screen wired to ProjectStore - scene: factory (web defaults: v32, Main Track, 1920x1080, 30fps) --- rust/crates/renderer/tests/scene_builder.rs | 228 ++++++++++++++++++++++++++++ 1 file changed, 228 insertions(+) create mode 100644 rust/crates/renderer/tests/scene_builder.rs (limited to 'rust/crates/renderer/tests/scene_builder.rs') diff --git a/rust/crates/renderer/tests/scene_builder.rs b/rust/crates/renderer/tests/scene_builder.rs new file mode 100644 index 00000000..1d65c092 --- /dev/null +++ b/rust/crates/renderer/tests/scene_builder.rs @@ -0,0 +1,228 @@ +use std::path::PathBuf; + +use renderer::node::{MediaSource, MediaType, Node}; +use renderer::{BuildSceneParams, build_scene}; +use scene::{ + Background, BaseTimelineElement, CanvasSize, Element, ImageElement, ParamValues, SceneTracks, + TextElement, Track, VideoElement, +}; +use time::MediaTime; + +fn base(id: &str, start: i64) -> BaseTimelineElement { + BaseTimelineElement { + id: id.to_string(), + name: id.to_string(), + duration: MediaTime::from_ticks(1000), + start_time: MediaTime::from_ticks(start), + trim_start: MediaTime::ZERO, + trim_end: MediaTime::ZERO, + source_duration: None, + animations: None, + params: ParamValues::new(), + } +} + +fn video_element(id: &str, start: i64, media_id: &str) -> Element { + Element::Video(VideoElement { + base: base(id, start), + media_id: media_id.to_string(), + is_source_audio_enabled: None, + hidden: None, + retime: None, + effects: None, + masks: None, + }) +} + +fn image_element(id: &str, start: i64, media_id: &str) -> Element { + Element::Image(ImageElement { + base: base(id, start), + media_id: media_id.to_string(), + hidden: None, + effects: None, + masks: None, + }) +} + +fn text_element(id: &str, start: i64, hidden: Option) -> Element { + Element::Text(TextElement { + base: base(id, start), + hidden, + effects: None, + }) +} + +fn video_track(id: &str, hidden: bool, elements: Vec) -> Track { + Track::Video { + id: id.to_string(), + name: id.to_string(), + elements, + muted: false, + hidden, + } +} + +fn media(id: &str, media_type: MediaType) -> MediaSource { + MediaSource { + id: id.to_string(), + media_type, + path: PathBuf::from(format!("/media/{id}")), + } +} + +fn build(tracks: &SceneTracks, media: &[MediaSource], background: &Background) -> Vec { + let params = BuildSceneParams { + canvas_size: CanvasSize { + width: 1920, + height: 1080, + }, + tracks, + media, + duration: MediaTime::from_ticks(10_000), + background, + is_preview: false, + }; + match build_scene(¶ms) { + Node::Root { children, .. } => children, + _ => panic!("expected root"), + } +} + +fn transparent() -> Background { + Background::Color { + color: "transparent".into(), + } +} + +#[test] +fn skips_elements_without_media_and_mismatched_types() { + let tracks = SceneTracks { + overlay: vec![], + main: video_track( + "main", + false, + vec![ + video_element("v1", 0, "m1"), + video_element("v2", 0, "missing"), + image_element("i1", 0, "m1"), + ], + ), + audio: vec![], + }; + let assets = [media("m1", MediaType::Video)]; + let children = build(&tracks, &assets, &transparent()); + + assert_eq!(children.len(), 1); + assert!(matches!(&children[0], Node::Video { media_id, .. } if media_id == "m1")); +} + +#[test] +fn main_track_renders_below_overlay() { + let tracks = SceneTracks { + overlay: vec![video_track("overlay", false, vec![text_element("t1", 0, None)])], + main: video_track("main", false, vec![video_element("v1", 0, "m1")]), + audio: vec![], + }; + let assets = [media("m1", MediaType::Video)]; + let children = build(&tracks, &assets, &transparent()); + + assert_eq!(children.len(), 2); + assert!(matches!(&children[0], Node::Video { .. })); + assert!(matches!(&children[1], Node::Text(_))); +} + +#[test] +fn hidden_elements_and_tracks_are_skipped() { + let tracks = SceneTracks { + overlay: vec![video_track("hidden-track", true, vec![text_element("t2", 0, None)])], + main: video_track( + "main", + false, + vec![video_element("v1", 0, "m1"), text_element("t1", 0, Some(true))], + ), + audio: vec![], + }; + let assets = [media("m1", MediaType::Video)]; + let children = build(&tracks, &assets, &transparent()); + + assert_eq!(children.len(), 1); + assert!(matches!(&children[0], Node::Video { .. })); +} + +#[test] +fn elements_sort_by_start_time_then_id() { + let tracks = SceneTracks { + overlay: vec![], + main: video_track( + "main", + false, + vec![ + video_element("b", 100, "m1"), + video_element("a", 100, "m1"), + video_element("z", 50, "m1"), + ], + ), + audio: vec![], + }; + let assets = [media("m1", MediaType::Video)]; + let children = build(&tracks, &assets, &transparent()); + + let starts: Vec = children + .iter() + .map(|node| match node { + Node::Video { visual, .. } => visual.time_offset.as_ticks(), + _ => panic!("expected video"), + }) + .collect(); + assert_eq!(starts, [50, 100, 100]); +} + +#[test] +fn color_background_comes_first_unless_transparent() { + let tracks = SceneTracks { + overlay: vec![], + main: video_track("main", false, vec![video_element("v1", 0, "m1")]), + audio: vec![], + }; + let assets = [media("m1", MediaType::Video)]; + + let with_color = build( + &tracks, + &assets, + &Background::Color { + color: "#112233".into(), + }, + ); + assert!(matches!(&with_color[0], Node::Color { color } if color == "#112233")); + assert_eq!(with_color.len(), 2); + + let transparent_children = build(&tracks, &assets, &transparent()); + assert_eq!(transparent_children.len(), 1); +} + +#[test] +fn blur_background_emits_backdrop_nodes_for_main_media() { + let tracks = SceneTracks { + overlay: vec![], + main: video_track("main", false, vec![video_element("v1", 0, "m1")]), + audio: vec![], + }; + let assets = [media("m1", MediaType::Video)]; + let children = build( + &tracks, + &assets, + &Background::Blur { + blur_intensity: 12.0, + }, + ); + + assert_eq!(children.len(), 2); + match &children[0] { + Node::BlurBackground(params) => { + assert_eq!(params.blur_intensity, 12.0); + assert_eq!(params.media_id, "m1"); + } + _ => panic!("expected blur background first"), + } + assert!(matches!(&children[1], Node::Video { .. })); +} -- cgit v1.3.1