diff --git a/crates/engine/src/commands.rs b/crates/engine/src/commands.rs index cdacd363..86a2dbc6 100644 --- a/crates/engine/src/commands.rs +++ b/crates/engine/src/commands.rs @@ -1793,7 +1793,21 @@ fn build() -> Vec { .map(Tick) .or_else(|| p.get("deltaFrames").and_then(Value::as_i64).map(|f| s.sequence_rate().tick_of(f))) .unwrap_or_default(); - s.edit_sequence("Slide", |q, ctx, _| Ok(edit::slide(q, c, d, ctx)?))?; + let clips = with_links(s, &[c]); + s.edit_sequence("Slide", |q, ctx, _| { + // clamp across all linked partners, then slide them by the common delta + let mut dd = d; + for c in &clips { + let x = edit::slide(&mut q.clone(), *c, dd, ctx)?; + if x.abs() < dd.abs() { + dd = x; + } + } + for c in &clips { + edit::slide(q, *c, dd, ctx)?; + } + Ok(()) + })?; Ok(Value::Null) }), cmd!("timeline.rateStretch", "Rate Stretch", [], None, r#"{"clip":id,"edge":"in|out","delta":ticks}"#, has_seq, |s, p| { diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 3c1e4657..742a7f6a 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -83,6 +83,18 @@ fn trim_linked_and_ripple_delete() { assert_eq!(q.video_tracks[0].items[0].start, s.sequence_rate().tick_of(12), "the trim gap remains"); } +#[test] +fn slide_moves_linked_audio_with_the_video() { + let mut s = demo(); + let v = s.active_sequence().unwrap().video_tracks[0].items[1].clone(); + s.execute("timeline.slide", json!({"clip": v.id.0, "deltaFrames": 5})).unwrap(); + let q = s.active_sequence().unwrap(); + let (_, nv) = q.find_item(v.id).unwrap(); + let a = q.audio_tracks[0].items.iter().find(|i| i.link == v.link).unwrap(); + assert_eq!(nv.start, v.start + s.sequence_rate().tick_of(5)); + assert_eq!((a.start, a.source_in), (nv.start, nv.source_in), "linked audio slides too and stays in sync"); +} + #[test] fn zero_fps_is_refused() { let mut s = demo();