layer.slip reports the requested shift and whether it was clamped (#2371)

A slip on a layer with no source handles succeeded with `delta: 0` and nothing else, which
read like a no-op bug. The result now carries `requested` (seconds) and, per layer, `clamped`
when the source could not move that far.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bflatasticandClaude Opus 5.5 committed 2026-10-30 12:20:30 +07:00
1 parent b23ef3d9a8
commit f6f242d6dd
3 files changed
+20 -5

No files matched your search

+9 -1
View File
@@ -148,7 +148,15 @@ pub fn specs() -> Vec<CommandSpec> {
cmd!("layer.moveLater", "Move Layers 1 Frame Later", [], Some("Alt+PageDown"), "{layers?}", has_layers, |s, p| move_frames(s, p, 1)),
cmd!("layer.moveEarlier10", "Move Layers 10 Frames Earlier", [], Some("Alt+Shift+PageUp"), "{layers?}", has_layers, |s, p| move_frames(s, p, -10)),
cmd!("layer.moveLater10", "Move Layers 10 Frames Later", [], Some("Alt+Shift+PageDown"), "{layers?}", has_layers, |s, p| move_frames(s, p, 10)),
cmd!("layer.slip", "Slip Edit", [], None, "{layers?, delta? (seconds) | frames?, merge?}", has_layers, slip),
cmd!(
"layer.slip",
"Slip Edit",
[],
None,
"{layers?, delta? (seconds) | frames?, merge?} → {requested, slipped: [{layer, delta, clamped}]}",
has_layers,
slip
),
cmd!("layer.slipBack", "Slip Edit 1 Frame Earlier", [], None, "{layers?}", has_layers, |s, p| {
let mut p = p.clone();
p["frames"] = json!(-1);
+7 -3
View File
@@ -84,8 +84,9 @@ pub(super) fn move_frames(s: &mut Session, p: &Value, frames: i64) -> Result<Val
/// Slip edit: move a layer's source (footage or precomp) under its fixed in and out points, like
/// dragging the source-duration bar in After Effects. `delta` in seconds or `frames`; positive
/// moves the source later. Clamped so the source still covers the in–out span. Keyframes and
/// layer markers are in layer time, so they travel with the source.
/// moves the source later. Clamped so the source still covers the in–out span: the result gives
/// the `requested` shift and, per layer, the `delta` applied and whether it was `clamped` (#2371).
/// Keyframes and layer markers are in layer time, so they travel with the source.
pub(super) fn slip(s: &mut Session, p: &Value) -> Result<Value> {
let (cid, ids) = layers_p(s, p)?;
let comp = s.project.comp(cid).ok_or(EngineError::NoComp)?;
@@ -130,7 +131,10 @@ pub(super) fn slip(s: &mut Session, p: &Value) -> Result<Value> {
}
Ok(())
})?;
Ok(json!({"slipped": shifts.iter().map(|(id, d)| json!({"layer": id.0, "delta": d.seconds()})).collect::<Vec<_>>()}))
Ok(json!({
"requested": delta.seconds(),
"slipped": shifts.iter().map(|(id, d)| json!({"layer": id.0, "delta": d.seconds(), "clamped": *d != delta})).collect::<Vec<_>>(),
}))
}
pub(super) fn arrange(s: &mut Session, p: &Value) -> Result<Value> {
+4 -1
View File
@@ -58,7 +58,8 @@ fn slip_moves_the_source_under_fixed_in_and_out() {
let before = get(&s);
assert_eq!((before.start_time, before.in_point.seconds(), before.out_point.seconds()), (Tick::ZERO, 1.0, 3.0));
// Slip the source 0.5 s later: in/out stay, the source (start) moves.
s.execute("layer.slip", json!({"layers": [id], "delta": 0.5})).unwrap();
let r = s.execute("layer.slip", json!({"layers": [id], "delta": 0.5})).unwrap();
assert_eq!(r["slipped"][0]["clamped"], json!(false));
let l = get(&s);
assert_eq!((l.start_time.seconds(), l.in_point, l.out_point), (0.5, before.in_point, before.out_point));
// The source time at comp 2 s is now 1.5 s.
@@ -67,6 +68,8 @@ fn slip_moves_the_source_under_fixed_in_and_out() {
let r = s.execute("layer.slip", json!({"layers": [id], "delta": 5.0})).unwrap();
assert_eq!(get(&s).start_time.seconds(), 1.0);
assert_eq!(r["slipped"][0]["delta"], json!(0.5));
// The response says the 5 s asked for was cut short (#2371).
assert_eq!((r["requested"].clone(), r["slipped"][0]["clamped"].clone()), (json!(5.0), json!(true)));
// Slip by a frame.
s.execute("layer.slipBack", json!({"layers": [id]})).unwrap();
assert!((get(&s).start_time.seconds() - (1.0 - 1.0 / 30.0)).abs() < 1e-9);