summaryrefslogtreecommitdiff
path: root/rust/crates/commands/tests/freeze_frame.rs
diff options
context:
space:
mode:
authorcashmere <cashmere@autistici.org>2026-07-28 20:14:51 +0200
committercashmere <cashmere@autistici.org>2026-07-28 20:14:51 +0200
commit32438ce25f69de99d2523b98237f62cfa90890d7 (patch)
tree3a712c347dec0889bdea3328b4c817c2d9392b27 /rust/crates/commands/tests/freeze_frame.rs
parentf5a82152394fde4d8f740f664ab7737b82861618 (diff)
downloadopencut-32438ce25f69de99d2523b98237f62cfa90890d7.tar.gz
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)
Diffstat (limited to 'rust/crates/commands/tests/freeze_frame.rs')
-rw-r--r--rust/crates/commands/tests/freeze_frame.rs155
1 files changed, 155 insertions, 0 deletions
diff --git a/rust/crates/commands/tests/freeze_frame.rs b/rust/crates/commands/tests/freeze_frame.rs
new file mode 100644
index 00000000..614b73f7
--- /dev/null
+++ b/rust/crates/commands/tests/freeze_frame.rs
@@ -0,0 +1,155 @@
+mod common;
+
+use commands::{
+ BatchCommand, Command, InsertElementCommand, InsertElementPlacement, RetainSide,
+ ShiftSplitRemainderCommand, SplitElementsCommand,
+};
+use common::{
+ FREEZE_SECONDS, SPLIT_SECONDS, VIDEO_SECONDS, build_freeze_image_element, seconds,
+ seed_project_with_video,
+};
+use scene::{Element, ElementRef, Project};
+use time::MediaTime;
+
+fn build_freeze_batch(track_id: &str) -> BatchCommand {
+ let split = SplitElementsCommand::new(
+ vec![ElementRef {
+ track_id: track_id.to_string(),
+ element_id: "video-1".to_string(),
+ }],
+ seconds(SPLIT_SECONDS),
+ RetainSide::Both,
+ );
+ let right_side = split.right_side_handle();
+ let shift = ShiftSplitRemainderCommand::new(
+ right_side,
+ seconds(SPLIT_SECONDS) + seconds(FREEZE_SECONDS),
+ );
+ let insert = InsertElementCommand::new(
+ build_freeze_image_element(),
+ InsertElementPlacement::Explicit {
+ track_id: track_id.to_string(),
+ },
+ );
+ BatchCommand::new(vec![Box::new(split), Box::new(shift), Box::new(insert)])
+}
+
+fn sorted_main_elements(project: &Project) -> Vec<Element> {
+ let scene = project
+ .scenes
+ .iter()
+ .find(|scene| scene.id == project.current_scene_id)
+ .expect("active scene");
+ let mut elements = scene.tracks.main.elements().to_vec();
+ elements.sort_by_key(|element| element.base().start_time);
+ elements
+}
+
+fn element_type_name(element: &Element) -> &'static str {
+ match element {
+ Element::Video(_) => "video",
+ Element::Image(_) => "image",
+ Element::Text(_) => "text",
+ Element::Audio(_) => "audio",
+ Element::Sticker(_) => "sticker",
+ Element::Graphic(_) => "graphic",
+ Element::Effect(_) => "effect",
+ }
+}
+
+#[test]
+fn splits_shifts_right_side_and_inserts_into_gap() {
+ let (mut project, track_id) = seed_project_with_video();
+
+ build_freeze_batch(&track_id)
+ .execute(&mut project)
+ .expect("execute");
+
+ let elements = sorted_main_elements(&project);
+ let type_names: Vec<&str> = elements.iter().map(element_type_name).collect();
+ assert_eq!(type_names, ["video", "image", "video"]);
+
+ let [left, image, right] = &elements[..] else {
+ panic!("expected three elements");
+ };
+ assert_eq!(left.base().start_time, MediaTime::ZERO);
+ assert_eq!(left.base().duration, seconds(SPLIT_SECONDS));
+ assert_eq!(image.base().start_time, seconds(SPLIT_SECONDS));
+ assert_eq!(image.base().duration, seconds(FREEZE_SECONDS));
+ assert_eq!(
+ right.base().start_time,
+ seconds(SPLIT_SECONDS) + seconds(FREEZE_SECONDS)
+ );
+ assert_eq!(
+ right.base().duration,
+ seconds(VIDEO_SECONDS - SPLIT_SECONDS)
+ );
+}
+
+#[test]
+fn undo_restores_original_element_as_single_step() {
+ let (mut project, track_id) = seed_project_with_video();
+ let mut batch = build_freeze_batch(&track_id);
+
+ batch.execute(&mut project).expect("execute");
+ batch.undo(&mut project);
+
+ let scene = project
+ .scenes
+ .iter()
+ .find(|scene| scene.id == project.current_scene_id)
+ .expect("active scene");
+ let elements = scene.tracks.main.elements();
+ assert_eq!(elements.len(), 1);
+ assert_eq!(elements[0].base().id, "video-1");
+ assert_eq!(elements[0].base().start_time, MediaTime::ZERO);
+ assert_eq!(elements[0].base().duration, seconds(VIDEO_SECONDS));
+}
+
+#[test]
+fn redo_after_undo_reapplies_shift_to_resplit_elements() {
+ let (mut project, track_id) = seed_project_with_video();
+ let mut batch = build_freeze_batch(&track_id);
+
+ batch.execute(&mut project).expect("execute");
+ batch.undo(&mut project);
+ batch.redo(&mut project).expect("redo");
+
+ let elements = sorted_main_elements(&project);
+ let type_names: Vec<&str> = elements.iter().map(element_type_name).collect();
+ assert_eq!(type_names, ["video", "image", "video"]);
+
+ let right = elements.last().expect("right element");
+ assert!(matches!(right, Element::Video(_)));
+ assert_eq!(
+ right.base().start_time,
+ seconds(SPLIT_SECONDS) + seconds(FREEZE_SECONDS)
+ );
+}
+
+#[test]
+fn no_op_when_split_produces_no_right_side() {
+ let (mut project, track_id) = seed_project_with_video();
+
+ let split = SplitElementsCommand::new(
+ vec![ElementRef {
+ track_id: track_id.to_string(),
+ element_id: "video-1".to_string(),
+ }],
+ MediaTime::ZERO,
+ RetainSide::Both,
+ );
+ let right_side = split.right_side_handle();
+ let shift = ShiftSplitRemainderCommand::new(right_side, seconds(FREEZE_SECONDS));
+ let mut batch = BatchCommand::new(vec![Box::new(split), Box::new(shift)]);
+
+ batch.execute(&mut project).expect("execute");
+ batch.undo(&mut project);
+
+ let scene = project
+ .scenes
+ .iter()
+ .find(|scene| scene.id == project.current_scene_id)
+ .expect("active scene");
+ assert_eq!(scene.tracks.main.elements().len(), 1);
+}