Files
soundcraft/crates/engine/tests/edit_modes.rs
T
grovebit 72288fc77e Make the Shuffle, Slip, Spot and Grid edit modes work like Pro Tools
The edit modes only partly worked. Shuffle changed Cut, Clear and Paste
but not moves or trims. Spot froze drags and had no dialog. Grid snapped
relative to whichever clip was under the pointer, and Relative Grid
acted like Absolute. Behaviour now follows the Edit Modes chapter of the
Pro Tools Reference Guide.

Shuffle
- Grabber drags lift the clips out (later clips close the gap) and land
  them at the nearest clip boundary, pushing later clips right; an empty
  track takes them at the session start. The drag outline shows where
  each track's clips will land.
- Trims move the later clips (a trimmed start stays put and the material
  slides under it). Duplicate, Duplicate and Extend, Repeat and Clip List
  drops make room, and Paste over a selection replaces it.
- Moves lift each clip by id and close the hole it leaves, so crossfade
  partners and unselected clips keep their audio, and a crossfaded pair
  moved together stays a crossfade. Ripples never push clips past the
  session start, never split a crossfade (refused with a reason), and
  drops never land inside a clip or crossfade.
- A time- or edit-locked clip and everything after it stay put when
  Shuffle closes or opens gaps. Edits that would push into one are
  refused ("not enough room before a locked clip"), and trims stop short
  of it. Insert Time and Cut Time are timeline operations and still move
  everything.
- With Automation Follows Edit, automation moves with every ripple, up to
  a locked clip, and travels with moved clips; with it off, Shuffle edits
  leave automation alone. Shuffle Clear used to move the clips but leave
  their automation behind (#232).
- Shuffle Lock: Cmd-click (Ctrl-click on Windows and Linux) the Shuffle
  button while in another mode. While it's on, the button shows a padlock
  and every way into Shuffle (keys, button, commands) is refused.
- A drag works out every lift and landing point once per target track
  (`lift_plan`, `shuffle_slots`, and a `DragPlan` kept for the drag). In a
  release build, dragging 100 clips on a track of 20,000 took 29 ms a
  frame and a 135 ms drop; now it's under a microsecond a frame and a 5 ms
  drop.

Slip, Spot and Grid
- Slip: trims stop at the neighbouring clips instead of overlapping them.
- Spot: a Grabber click or drag, a Trim click or drag and a Clip List drop
  open a Spot dialog: Start, Sync Point and End in any time scale, the
  Duration, and the clip's Original Time Stamp. Trim clicks pick the
  nearer edge.
- Grid: a drag snaps the dragged clip's start (its sync point, when it
  has one inside the clip) to the grid. Relative Grid moves by whole grid
  steps and keeps the clip's offset from the grid; a cut or copy pasted
  in Relative Grid lands as far after a grid line as the copied range
  started after one. Holding Cmd (Ctrl on Windows and Linux) while
  dragging or dropping from the Clip List swaps Grid and Slip. Grid lines
  no longer fall a step short at tempos where a line isn't a whole number
  of samples.
- Choosing Grid from another mode picks whichever of Absolute and
  Relative Grid was used last; F4 in Grid toggles them, like clicking the
  Grid button again. Options > Edit/Tool Mode Keyboard Lock, which did
  nothing, now stops F4 and Alt+4 from switching them (toolbar clicks
  still do).
- Snap to Grid in Shuffle, Slip and Spot: Shift+F4, Shift-click on the
  Grid button or on another mode's button, or hold F4 with F1-F3. The
  Edit cursor and selections snap to the grid while clip moves and trims
  follow the main mode; the Grid button is outlined while it's on.
- Option+1-4 (Alt+1-4 on Windows and Linux) work like F1-F4, and the `
  key cycles Shuffle, Slip, Spot and Grid, skipping a locked Shuffle.
- Insert Silence pushes later material only in Shuffle; in the other
  modes it clears the selection, as in Pro Tools.

Also
- Dragging clips from several tracks keeps each track's distance from the
  grabbed clip's track. Drops past the last track, or onto a track that
  can't play the clips (audio onto MIDI, an aux or the master), are
  refused; clips moved onto an aux used to vanish into its unused
  playlist.
- Undo and Redo keep the Edit mode, Shuffle Lock, Snap to Grid and the
  Grid kind. Start trims keep a clip's sync point on its audio, or drop
  it. Imported BWF time stamps are converted from the file's rate (read
  from its header) to the session's, and the Original Time Stamp no
  longer adds the session start twice. Typed positions that overflow are
  ignored instead of panicking.

Commands: `edit.move_clips` follows the mode, and its `track` moves the
first clip's track there and keeps the others' distances. `edit.trim_clip`,
`edit.snap_to_grid` (Shift+F4) and `edit.shuffle_lock` are new, and
`edit.mode` takes `snap_to_grid`. Mode buttons expose their names to
accessibility tools.

Tests: crates/{engine,ui-egui}/tests/edit_modes.rs and the op tests in
edit.rs; the random-edits property test also toggles clip locks.
2026-10-12 21:49:31 +01:00

416 lines
19 KiB
Rust

use serde_json::{Value, json};
use soundcraft_engine::Engine;
use soundcraft_engine::edit::SHUFFLE_LOCK;
use soundcraft_model::{AutoParam, Clip, ClipId, EditMode, Source, SourceId, TrackId};
fn source(id: u64, frames: u64) -> Source {
Source {
id: SourceId(id),
name: "src".into(),
path: "src.wav".into(),
channels: 1,
frames,
sample_rate: 48_000,
format: soundcraft_audio_io::FileFormat::Wav,
time_reference: 0,
unsaved: false,
}
}
fn new_track(e: &mut Engine) -> TrackId {
TrackId(e.execute("track.new", &json!({"count": 1})).unwrap()["tracks"][0].as_u64().unwrap())
}
fn engine(mode: &str) -> (Engine, TrackId, [ClipId; 2]) {
let mut e = Engine::default();
let t = new_track(&mut e);
let s = e.session_mut();
s.sources.push(source(7, 10_000));
let ids = [s.new_clip_id(), s.new_clip_id()];
s.track_mut(t).and_then(|t| t.playlist_mut()).unwrap().clips =
vec![Clip::audio(ids[0], "a", SourceId(7), 0, 0, 1000), Clip::audio(ids[1], "b", SourceId(7), 1000, 2000, 1000)];
e.execute("edit.mode", &json!({"mode": mode})).unwrap();
(e, t, ids)
}
/// A track of clips (start, length, offset) on a long file, source 8.
fn long(mode: &str, spans: &[(i64, i64, i64)]) -> (Engine, TrackId, Vec<ClipId>) {
let mut e = Engine::default();
let t = new_track(&mut e);
let s = e.session_mut();
s.sources.push(source(8, 1_000_000));
let ids: Vec<ClipId> = spans.iter().map(|_| s.new_clip_id()).collect();
s.track_mut(t).and_then(|t| t.playlist_mut()).unwrap().clips =
spans.iter().zip(&ids).map(|(&(start, len, offset), id)| Clip::audio(*id, "c", SourceId(8), offset, start, len)).collect();
e.execute("edit.mode", &json!({"mode": mode})).unwrap();
(e, t, ids)
}
fn clips(e: &Engine, t: TrackId) -> Vec<(i64, i64)> {
e.session().track(t).unwrap().clips().iter().map(|c| (c.start, c.length)).collect()
}
/// Each clip's id, length and file offset, in id order: what a move must never change.
fn material(e: &Engine, t: TrackId) -> Vec<(u64, i64, i64)> {
let mut v: Vec<_> = e.session().track(t).unwrap().clips().iter().map(|c| (c.id.0, c.length, c.source_offset())).collect();
v.sort_unstable();
v
}
fn start(e: &Engine, id: ClipId) -> i64 {
e.session().find_clip(id).unwrap().1.start
}
fn points(e: &Engine, t: TrackId) -> Vec<i64> {
e.session().track(t).unwrap().lane(&AutoParam::Volume).map(|l| l.points.iter().map(|p| p.at).collect()).unwrap_or_default()
}
fn set_points(e: &mut Engine, t: TrackId, at: &[i64]) {
for a in at {
run(e, "automation.set_point", json!({"track": t.0, "param": "volume", "at": a, "value": -6.0}));
}
}
fn run(e: &mut Engine, id: &str, p: Value) {
e.execute(id, &p).unwrap();
}
#[test]
fn insert_silence_pushes_in_shuffle_and_clears_otherwise() {
for (mode, want) in [("shuffle", vec![(0, 500), (700, 500), (2200, 1000)]), ("slip", vec![(0, 500), (700, 300), (2000, 1000)])] {
let (mut e, t, _) = engine(mode);
run(&mut e, "edit.insert_silence", json!({"tracks": [t.0], "start": 500, "end": 700}));
assert_eq!(clips(&e, t), want, "{mode}");
}
}
#[test]
fn duplicate_and_repeat_make_room_in_shuffle() {
let (mut e, t, _) = engine("shuffle");
run(&mut e, "edit.duplicate", json!({"tracks": [t.0], "start": 0, "end": 1000}));
assert_eq!(clips(&e, t), vec![(0, 1000), (1000, 1000), (3000, 1000)]);
let (mut e, t, _) = engine("shuffle");
run(&mut e, "edit.repeat", json!({"tracks": [t.0], "start": 0, "end": 1000, "count": 2}));
assert_eq!(clips(&e, t), vec![(0, 1000), (1000, 1000), (2000, 1000), (4000, 1000)]);
let (mut e, t, _) = engine("slip");
run(&mut e, "edit.duplicate", json!({"tracks": [t.0], "start": 0, "end": 1000}));
assert_eq!(clips(&e, t), vec![(0, 1000), (1000, 1000), (2000, 1000)]);
}
#[test]
fn shuffle_paste_replaces_the_selection() {
let (mut e, t, _) = engine("shuffle");
run(&mut e, "edit.copy", json!({"tracks": [t.0], "start": 0, "end": 1000}));
run(&mut e, "edit.select", json!({"tracks": [t.0], "start": 2000, "end": 2500}));
run(&mut e, "edit.paste", json!({}));
assert_eq!(clips(&e, t), vec![(0, 1000), (2000, 1000), (3000, 500)]);
}
#[test]
fn clip_list_drops_land_on_a_clip_boundary_in_shuffle() {
let (mut e, t, [_, b]) = engine("shuffle");
run(&mut e, "clip.place_source", json!({"source": 7, "track": t.0, "at": 1900}));
assert_eq!(clips(&e, t), vec![(0, 1000), (2000, 10_000), (12_000, 1000)]);
e.session_mut().find_clip_mut(b).unwrap().time_locked = true;
assert!(e.execute("clip.place_source", &json!({"source": 7, "track": t.0, "at": 0})).is_err());
assert_eq!(clips(&e, t).len(), 3);
}
#[test]
fn grid_moves_to_a_position_snap_or_keep_their_offset() {
let (mut e, t, [_, b]) = engine("grid");
let s = e.session();
let step = s.edit.grid.step_samples(0, s.sample_rate, &s.tempo, s.frame_rate);
run(&mut e, "edit.move_clips", json!({"clips": [b.0], "to": step + 1000}));
assert_eq!(clips(&e, t)[1].0, step);
run(&mut e, "edit.mode", json!({"mode": "relative_grid"}));
run(&mut e, "edit.move_clips", json!({"clips": [b.0], "to": 2 * step + 2000}));
assert_eq!(clips(&e, t)[1].0, 2 * step);
run(&mut e, "edit.move_clips", json!({"clips": [b.0], "by": 200}));
run(&mut e, "edit.move_clips", json!({"clips": [b.0], "to": 3 * step}));
assert_eq!(clips(&e, t)[1].0, 3 * step + 200);
}
#[test]
fn clips_cannot_move_onto_tracks_without_a_playlist() {
let (mut e, t, [a, _]) = engine("slip");
let aux = e.execute("track.new", &json!({"kind": "aux"})).unwrap()["tracks"][0].clone();
assert!(e.execute("edit.move_clips", &json!({"clips": [a.0], "by": 0, "track": aux})).is_err());
assert_eq!(clips(&e, t).len(), 2);
}
#[test]
fn trim_clip_needs_an_edge_and_an_unlocked_clip() {
let (mut e, t, [a, b]) = engine("slip");
assert!(e.execute("edit.trim_clip", &json!({"clip": a.0})).is_err());
e.session_mut().find_clip_mut(b).unwrap().edit_locked = true;
assert!(e.execute("edit.trim_clip", &json!({"clip": b.0, "end": 2500})).is_err());
run(&mut e, "edit.trim_clip", json!({"clip": a.0, "end": 5000}));
assert_eq!(clips(&e, t), vec![(0, 2000), (2000, 1000)]);
}
#[test]
fn shuffle_lock_keeps_shuffle_mode_out() {
let (mut e, _, _) = engine("slip");
run(&mut e, "edit.shuffle_lock", json!({}));
assert!(e.execute("edit.mode", &json!({"mode": "shuffle"})).is_err());
run(&mut e, "edit.mode", json!({"mode": "grid"}));
run(&mut e, "edit.shuffle_lock", json!({"value": false}));
run(&mut e, "edit.mode", json!({"mode": "shuffle"}));
assert!(e.execute("edit.shuffle_lock", &json!({})).is_err());
}
#[test]
fn snap_to_grid_rides_along_with_the_other_modes() {
let (mut e, _, _) = engine("slip");
let snap = |e: &Engine| e.session().edit.flag(soundcraft_engine::edit::SNAP_TO_GRID);
run(&mut e, "edit.snap_to_grid", json!({}));
assert!(snap(&e));
run(&mut e, "edit.mode", json!({"mode": "spot", "snap_to_grid": true}));
assert!(snap(&e));
run(&mut e, "edit.mode", json!({"mode": "shuffle"}));
assert!(!snap(&e));
run(&mut e, "edit.mode", json!({"mode": "grid", "snap_to_grid": true}));
assert!(!snap(&e));
assert!(e.execute("edit.snap_to_grid", &json!({})).is_err());
}
#[test]
fn shuffle_edits_that_would_push_a_locked_clip_are_refused() {
let (mut e, t, [_, b]) = engine("shuffle");
e.session_mut().find_clip_mut(b).unwrap().time_locked = true;
for id in ["edit.insert_silence", "edit.duplicate"] {
let err = e.execute(id, &json!({"tracks": [t.0], "start": 0, "end": 1500})).unwrap_err().to_string();
assert!(err.contains("locked clip"), "{id}: {err}");
}
run(&mut e, "edit.insert_silence", json!({"tracks": [t.0], "start": 0, "end": 1000}));
assert_eq!(clips(&e, t), vec![(1000, 1000), (2000, 1000)]);
run(&mut e, "event.cut_time", json!({"start": 0, "end": 1000}));
assert_eq!(clips(&e, t), vec![(0, 1000), (1000, 1000)]);
}
#[test]
fn clips_dragged_from_several_tracks_keep_their_tracks_apart() {
let (mut e, t, [a, _]) = engine("slip");
let new = e.execute("track.new", &json!({"count": 2})).unwrap()["tracks"].clone();
let [t2, t3] = [0, 1].map(|i| TrackId(new[i].as_u64().unwrap()));
let s = e.session_mut();
let d = s.new_clip_id();
s.track_mut(t2).and_then(|t| t.playlist_mut()).unwrap().clips.push(Clip::audio(d, "d", SourceId(7), 0, 5000, 500));
run(&mut e, "edit.move_clips", json!({"clips": [a.0, d.0], "by": 0, "track": t2.0}));
assert_eq!(clips(&e, t), vec![(2000, 1000)]);
assert_eq!(clips(&e, t2), vec![(0, 1000)]);
assert_eq!(clips(&e, t3), vec![(5000, 500)]);
assert!(e.execute("edit.move_clips", &json!({"clips": [a.0, d.0], "by": 0, "track": t3.0})).is_err());
}
#[test]
fn relative_grid_pastes_keep_the_offset_from_the_grid() {
let (mut e, t, [_, b]) = engine("relative_grid");
let s = e.session();
let step = s.edit.grid.step_samples(0, s.sample_rate, &s.tempo, s.frame_rate);
run(&mut e, "edit.select", json!({"clips": [b.0]}));
run(&mut e, "edit.copy", json!({}));
run(&mut e, "edit.select", json!({"tracks": [t.0], "start": 3 * step}));
run(&mut e, "edit.paste", json!({}));
assert_eq!(clips(&e, t).last(), Some(&(3 * step + 2000, 1000)));
run(&mut e, "edit.mode", json!({"mode": "slip"}));
run(&mut e, "edit.select", json!({"tracks": [t.0], "start": 5 * step}));
run(&mut e, "edit.paste", json!({}));
assert_eq!(clips(&e, t).last(), Some(&(5 * step, 1000)));
}
#[test]
fn shuffle_moves_keep_crossfade_partners_whole() {
let (mut e, t, ids) = long("slip", &[(0, 1000, 10_000), (1000, 1000, 20_000), (2000, 1000, 30_000)]);
run(&mut e, "edit.fades_create", json!({"tracks": [t.0], "start": 900, "end": 1100}));
run(&mut e, "edit.mode", json!({"mode": "shuffle"}));
let whole = material(&e, t);
let [a, b, c] = [ids[0], ids[1], ids[2]];
run(&mut e, "edit.move_clips", json!({"clips": [a.0], "by": 5000}));
assert_eq!(material(&e, t), whole);
assert_eq!([start(&e, b), start(&e, c), start(&e, a)], [0, 1100, 2100]);
let (mut e, t, ids) = long("slip", &[(0, 1000, 10_000), (1000, 1000, 20_000), (2000, 1000, 30_000)]);
run(&mut e, "edit.fades_create", json!({"tracks": [t.0], "start": 900, "end": 1100}));
run(&mut e, "edit.mode", json!({"mode": "shuffle"}));
for by in [2500, -2500] {
run(&mut e, "edit.move_clips", json!({"clips": [ids[0].0, ids[1].0], "by": by}));
assert_eq!(material(&e, t), whole, "{by}");
assert_eq!(start(&e, ids[1]) - start(&e, ids[0]), 900, "{by}");
}
assert_eq!([start(&e, ids[0]), start(&e, ids[2])], [0, 2000]);
run(&mut e, "edit.mode", json!({"mode": "slip"}));
run(&mut e, "edit.move_clips", json!({"clips": [ids[0].0, ids[1].0], "by": 5000}));
assert_eq!(material(&e, t), whole);
}
#[test]
fn pasting_copied_automation_in_shuffle_leaves_the_clips_alone() {
let (mut e, t, ids) = long("slip", &[(0, 1000, 0), (1000, 1000, 0), (2000, 1000, 0)]);
set_points(&mut e, t, &[200]);
run(&mut e, "edit.copy_all_automation", json!({"tracks": [t.0], "start": 0, "end": 500}));
run(&mut e, "edit.mode", json!({"mode": "shuffle"}));
run(&mut e, "edit.select", json!({"clips": [ids[1].0]}));
run(&mut e, "edit.paste", json!({}));
assert_eq!(clips(&e, t), vec![(0, 1000), (1000, 1000), (2000, 1000)]);
}
#[test]
fn automation_follows_shuffle_moves_trims_and_drops() {
let setup = || {
let (mut e, t, ids) = long("shuffle", &[(0, 1000, 0), (1000, 1000, 0), (2000, 1000, 0)]);
set_points(&mut e, t, &[500, 1500, 2500]);
(e, t, ids)
};
let (mut e, t, ids) = setup();
run(&mut e, "edit.move_clips", json!({"clips": [ids[0].0], "by": 3000}));
assert_eq!(points(&e, t), [500, 1500, 2500]);
assert_eq!([start(&e, ids[1]), start(&e, ids[2]), start(&e, ids[0])], [0, 1000, 2000]);
let (mut e, t, ids) = setup();
run(&mut e, "edit.trim_clip", json!({"clip": ids[0].0, "end": 500}));
assert_eq!(points(&e, t), [1000, 2000]);
let (mut e, t, _) = setup();
run(&mut e, "clip.place_source", json!({"source": 8, "track": t.0, "at": 1000}));
assert_eq!(points(&e, t), [500, 1_001_500, 1_002_500]);
}
#[test]
fn shuffle_automation_stops_at_locked_clips_and_needs_automation_follows_edit() {
let (mut e, t, ids) = long("shuffle", &[(0, 1000, 0), (1000, 1000, 0), (3000, 1000, 0)]);
e.session_mut().find_clip_mut(ids[2]).unwrap().time_locked = true;
set_points(&mut e, t, &[3500]);
run(&mut e, "edit.insert_silence", json!({"tracks": [t.0], "start": 0, "end": 500}));
run(&mut e, "edit.cut", json!({"tracks": [t.0], "start": 0, "end": 500}));
assert_eq!((start(&e, ids[2]), points(&e, t)), (3000, vec![3500]));
let (mut e, t, _) = long("shuffle", &[(0, 1000, 0), (1000, 1000, 0)]);
set_points(&mut e, t, &[300, 1500, 5000]);
e.session_mut().edit.automation_follows_edit = false;
run(&mut e, "edit.duplicate", json!({"tracks": [t.0], "start": 0, "end": 1000}));
run(&mut e, "edit.insert_silence", json!({"tracks": [t.0], "start": 0, "end": 100}));
assert_eq!(points(&e, t), [300, 1500, 5000]);
}
#[test]
fn shuffle_clear_moves_automation_with_the_clips() {
let (mut e, t, _) = long("shuffle", &[(0, 10_000, 0), (20_000, 10_000, 0)]);
set_points(&mut e, t, &[0, 9_000, 20_000, 29_000]);
run(&mut e, "edit.clear", json!({"track": t.0, "start": 4_000, "end": 6_000}));
assert_eq!(clips(&e, t), [(0, 4_000), (4_000, 4_000), (18_000, 10_000)]);
assert_eq!(points(&e, t), [0, 7_000, 18_000, 27_000]);
}
#[test]
fn relative_grid_pastes_land_on_grid_lines_at_any_tempo() {
let (mut e, t, [a, _]) = engine("relative_grid");
run(&mut e, "event.tempo", json!({"bpm": 90, "at": 0}));
run(&mut e, "edit.select", json!({"clips": [a.0]}));
run(&mut e, "edit.copy", json!({}));
run(&mut e, "edit.select", json!({"tracks": [t.0], "start": 128_000}));
run(&mut e, "edit.paste", json!({}));
assert!(clips(&e, t).contains(&(128_000, 1000)), "{:?}", clips(&e, t));
run(&mut e, "edit.paste", json!({"tracks": [t.0], "at": 50_000}));
assert!(clips(&e, t).contains(&(50_000, 1000)), "{:?}", clips(&e, t));
}
#[test]
fn start_trims_keep_the_sync_point_on_its_audio() {
let (mut e, t, ids) = long("slip", &[(0, 10_000, 0)]);
let sync = |e: &Engine| e.session().find_clip(ids[0]).unwrap().1.sync_point;
e.session_mut().find_clip_mut(ids[0]).unwrap().sync_point = 5000;
run(&mut e, "edit.trim_clip", json!({"clip": ids[0].0, "start": 2000}));
assert_eq!(sync(&e), 3000);
run(&mut e, "edit.trim_clip", json!({"clip": ids[0].0, "start": 8000}));
assert_eq!(sync(&e), 0);
run(&mut e, "edit.mode", json!({"mode": "grid"}));
run(&mut e, "edit.move_clips", json!({"clips": [ids[0].0], "to": 30_000}));
assert_eq!(clips(&e, t), vec![(30_000, 2000)]);
}
#[test]
fn imported_time_stamps_count_at_the_session_rate() {
use soundcraft_audio_io::{AudioBuffer, BwfInfo, EncodeOptions, FileFormat};
let mut e = Engine::default();
let bwf = BwfInfo { time_reference: 1_587_600_000, ..BwfInfo::default() };
let opts = EncodeOptions { format: FileFormat::Wav, bwf: Some(bwf), ..EncodeOptions::default() };
let bytes = soundcraft_audio_io::encode(&AudioBuffer::new(44_100, 1, 441), &opts).unwrap();
soundcraft_engine::io::import_audio_bytes(&mut e, "stamped.wav", &bytes, None, None, 0).unwrap();
assert_eq!(e.session().sources[0].time_reference, 1_728_000_000);
}
#[test]
fn shuffle_trims_stop_at_a_locked_crossfade_and_stay_inside_the_file() {
let (mut e, t, ids) = long("slip", &[(0, 1000, 500), (1000, 1000, 5000)]);
run(&mut e, "edit.fades_create", json!({"tracks": [t.0], "start": 950, "end": 1050}));
e.session_mut().find_clip_mut(ids[1]).unwrap().time_locked = true;
run(&mut e, "edit.mode", json!({"mode": "shuffle"}));
let before = material(&e, t);
run(&mut e, "edit.trim_clip", json!({"clip": ids[0].0, "end": 1500}));
run(&mut e, "edit.trim_clip", json!({"clip": ids[0].0, "start": 0}));
assert_eq!(material(&e, t), before);
let (mut e, _, [_, b]) = engine("shuffle");
run(&mut e, "edit.trim_clip", json!({"clip": b.0, "start": 2500, "end": 20_000}));
let c = e.session().find_clip(b).unwrap().1;
assert_eq!((c.source_offset(), c.source_offset() + c.length), (1500, 10_000));
}
#[test]
fn duplicate_and_extend_makes_room_in_shuffle() {
let (mut e, t, _) = long("shuffle", &[(0, 1000, 0), (1000, 1000, 0)]);
run(&mut e, "edit.duplicate_extend", json!({"tracks": [t.0], "start": 0, "end": 1000}));
assert_eq!(clips(&e, t), vec![(0, 1000), (1000, 1000), (2000, 1000)]);
let (mut e, t, ids) = long("shuffle", &[(0, 1000, 0), (1000, 1000, 0)]);
e.session_mut().find_clip_mut(ids[1]).unwrap().time_locked = true;
assert!(e.execute("edit.duplicate_extend", &json!({"tracks": [t.0], "start": 0, "end": 1000})).is_err());
assert_eq!(clips(&e, t), vec![(0, 1000), (1000, 1000)]);
}
#[test]
fn undo_leaves_the_edit_mode_and_shuffle_lock_alone() {
let (mut e, t, _) = engine("shuffle");
run(&mut e, "edit.cut", json!({"tracks": [t.0], "start": 0, "end": 500}));
run(&mut e, "edit.mode", json!({"mode": "slip"}));
run(&mut e, "edit.shuffle_lock", json!({}));
for step in [Engine::undo, Engine::redo] {
assert!(step(&mut e));
assert_eq!((e.session().edit.edit_mode, e.session().edit.flag(SHUFFLE_LOCK)), (EditMode::Slip, true));
}
}
#[test]
fn shuffle_cuts_reaching_before_the_session_start_keep_the_spacing() {
let (mut e, t, _) = long("shuffle", &[(0, 1000, 0), (1000, 1000, 0), (2000, 1000, 0)]);
run(&mut e, "edit.cut", json!({"tracks": [t.0], "start": -5000, "end": 500}));
assert_eq!(clips(&e, t), vec![(0, 500), (500, 1000), (1500, 1000)]);
}
#[test]
fn shuffle_never_pulls_a_crossfade_apart() {
let (mut e, t, ids) = long("slip", &[(0, 9000, 0), (9000, 11_000, 100_000), (20_000, 5000, 200_000)]);
run(&mut e, "edit.fades_create", json!({"tracks": [t.0], "start": 8000, "end": 10_000}));
run(&mut e, "edit.mode", json!({"mode": "shuffle"}));
let err = e.execute("edit.duplicate", &json!({"tracks": [t.0], "start": 5000, "end": 9000})).unwrap_err().to_string();
assert!(err.contains("crossfade"), "{err}");
run(&mut e, "clip.place_source", json!({"source": 8, "track": t.0, "at": 7900}));
assert_eq!(clips(&e, t)[0], (0, 1_000_000));
assert_eq!([start(&e, ids[0]), start(&e, ids[1])], [1_000_000, 1_008_000]);
}
#[test]
fn clips_move_to_and_from_hidden_tracks() {
let (mut e, t, [a, _]) = engine("slip");
let hidden = new_track(&mut e);
run(&mut e, "track.hide", json!({"tracks": [hidden.0], "hidden": true}));
run(&mut e, "edit.move_clips", json!({"clips": [a.0], "by": 0, "track": hidden.0}));
assert_eq!(clips(&e, hidden), vec![(0, 1000)]);
run(&mut e, "edit.move_clips", json!({"clips": [a.0], "by": 0, "track": t.0}));
assert_eq!(clips(&e, t).len(), 2);
}
#[test]
fn shuffle_lock_and_snap_rules_hold_when_a_call_names_tracks() {
let (mut e, t, _) = engine("shuffle");
assert!(e.execute("edit.shuffle_lock", &json!({"value": true, "tracks": [t.0]})).is_err());
run(&mut e, "edit.mode", json!({"mode": "grid"}));
assert!(e.execute("edit.snap_to_grid", &json!({"tracks": [t.0]})).is_err());
}