diff --git a/crates/engine/src/cmd/event.rs b/crates/engine/src/cmd/event.rs index d9271210..8e965a70 100644 --- a/crates/engine/src/cmd/event.rs +++ b/crates/engine/src/cmd/event.rs @@ -154,6 +154,7 @@ fn insert_time(e: &mut Engine, p: &Value) -> Result { let s = e.session_mut(); let ids: Vec<_> = s.tracks.iter().map(|t| t.id).collect(); for t in ids { + crate::edit::separate_all_at(s, t, r.start); crate::edit::insert_silence(s, t, r); if let Some(tr) = s.track_mut(t) { for l in &mut tr.automation { @@ -185,6 +186,8 @@ fn cut_time(e: &mut Engine, p: &Value) -> Result { let s = e.session_mut(); let ids: Vec<_> = s.tracks.iter().map(|t| t.id).collect(); for t in ids { + crate::edit::separate_all_at(s, t, r.start); + crate::edit::separate_all_at(s, t, r.end); crate::edit::clear_range(s, t, r, false); crate::edit::shift_from(s, t, r.end, -r.len()); if let Some(tr) = s.track_mut(t) { diff --git a/crates/engine/src/edit.rs b/crates/engine/src/edit.rs index c5edcc6c..636c9c55 100644 --- a/crates/engine/src/edit.rs +++ b/crates/engine/src/edit.rs @@ -78,9 +78,9 @@ fn rebase_midi_start(clip: &mut Clip, old_start: Samples, t: TimeCtx<'_>) { } } -/// Split every clip on a track's active playlist that spans `at`. The piece before `at` keeps the -/// clip's name. The piece after it is a new clip, and takes the next name of the clip's Auto Rename -/// scheme when it has one. +/// Split every clip on a track's active playlist that spans `at`, except time- or edit-locked ones. +/// The piece before `at` keeps the clip's name. The piece after it is a new clip, and takes the next +/// name of the clip's Auto Rename scheme when it has one. pub fn separate_at(s: &mut Session, track: TrackId, at: Samples) -> usize { separate_at_if(s, track, at, |_| true) } @@ -88,11 +88,21 @@ pub fn separate_at(s: &mut Session, track: TrackId, at: Samples) -> usize { /// [`separate_at`], giving the piece after `at` the next Auto Rename name only when `auto_name` /// returns true for it. Any other piece keeps the clip's name, and its scheme does not count up. pub fn separate_at_if(s: &mut Session, track: TrackId, at: Samples, auto_name: impl Fn(&Clip) -> bool) -> usize { + split_at(s, track, at, auto_name, true) +} + +/// [`separate_at`], splitting time- and edit-locked clips too. For the Event time operations, which +/// move the whole timeline, locked clips included. +pub fn separate_all_at(s: &mut Session, track: TrackId, at: Samples) -> usize { + split_at(s, track, at, |_| true, false) +} + +fn split_at(s: &mut Session, track: TrackId, at: Samples, auto_name: impl Fn(&Clip) -> bool, skip_locked: bool) -> usize { let tc_tempo = s.tempo.clone(); let sr = s.sample_rate; let t = TimeCtx { tempo: &tc_tempo, sr }; let mut ids = Vec::new(); - let n_split = s.track(track).map_or(0, |tr| tr.clips().iter().filter(|c| at > c.start && at < c.end()).count()); + let n_split = s.track(track).map_or(0, |tr| tr.clips().iter().filter(|c| at > c.start && at < c.end() && !(skip_locked && locked(c))).count()); for _ in 0..n_split { ids.push(s.new_clip_id()); } @@ -106,6 +116,7 @@ pub fn separate_at_if(s: &mut Session, track: TrackId, at: Samples, auto_name: i for c in pl.clips.drain(..) { if at > c.start && at < c.end() + && !(skip_locked && locked(&c)) && let Some(id) = ids.pop() && let Some((l, mut r)) = split_clip(&c, at, id, t) { diff --git a/crates/engine/tests/it/edit_modes.rs b/crates/engine/tests/it/edit_modes.rs index 7da8608d..c5d24fe3 100644 --- a/crates/engine/tests/it/edit_modes.rs +++ b/crates/engine/tests/it/edit_modes.rs @@ -579,3 +579,48 @@ fn moving_a_group_left_past_zero_keeps_its_spacing() { run(&mut e, "edit.move_clips", json!({"clips": [ids[0].0, ids[1].0], "by": -1000})); assert_eq!(clips(&e, t), [(0, 100), (1500, 100)]); } + +#[test] +fn separate_and_range_edits_leave_locked_clips_whole() { + let edits = [ + ("edit.separate", json!({"at": 24_000})), + ("edit.separate", json!({"start": 12_000, "end": 36_000})), + ("edit.separate_on_grid", json!({"start": 12_000, "end": 36_000})), + ("edit.clear", json!({"start": 12_000, "end": 36_000})), + ("edit.insert_silence", json!({"start": 24_000, "end": 25_000})), + ]; + for lock in [None, Some("clip.edit_lock"), Some("clip.time_lock")] { + for (cmd, p) in &edits { + let (mut e, t, ids) = long("slip", &[(0, 48_000, 0)]); + if let Some(lock) = lock { + run(&mut e, lock, json!({"clips": [ids[0].0]})); + } + let mut p = p.clone(); + p["tracks"] = json!([t.0]); + run(&mut e, cmd, p); + if lock.is_some() { + assert_eq!(clips(&e, t), vec![(0, 48_000)], "{lock:?} {cmd}"); + } else { + assert_ne!(clips(&e, t), vec![(0, 48_000)], "unlocked control {cmd}"); + } + } + } +} + +/// Event time edits move the entire timeline, including clips protected from ordinary edits. +#[test] +fn event_time_edits_split_and_move_locked_clips() { + for lock in ["clip.edit_lock", "clip.time_lock"] { + for (command, expected) in [ + ("event.insert_time", vec![(0, 12_000, 0), (36_000, 36_000, 12_000)]), + ("event.cut_time", vec![(0, 12_000, 0), (12_000, 12_000, 36_000)]), + ] { + let (mut e, t, ids) = long("slip", &[(0, 48_000, 0)]); + run(&mut e, lock, json!({"clips": [ids[0].0]})); + run(&mut e, command, json!({"start": 12_000, "end": 36_000})); + let actual: Vec<_> = e.session().track(t).unwrap().clips().iter().map(|c| (c.start, c.length, c.source_offset())).collect(); + assert_eq!(actual, expected, "{lock} {command}"); + assert!(e.session().track(t).unwrap().clips().iter().all(soundcraft_engine::edit::locked)); + } + } +}