mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-28 12:07:48 -06:00
Timeline: trimming a selected layer's in/out point trims every selected layer (as in After Effects)
(cherry picked from commit 6af5aa81bf3b66d272f15f018f065f3324faa9fb) (cherry picked from commit 5423c78e4a58789c26df4f4db46e6f27fb458d51) (cherry picked from commit 8863abad1571815719e59715955c6a0402b5b67e)
This commit is contained in:
@@ -857,10 +857,21 @@ fn timing(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let fr = s.project.comp(cid).ok_or(EngineError::NoComp)?.frame_rate;
|
||||
let snap = |k: &str| f_p(p, k).map(|v| fr.snap_nearest(Tick::from_seconds_f64(v)));
|
||||
let (delta, start, inp, outp) = (snap("delta"), snap("start"), snap("in"), snap("out"));
|
||||
// `inDelta` / `outDelta`: move each layer's In / Out point by the same amount (the Timeline's
|
||||
// trim drag, which trims every selected layer as in After Effects), each layer within its own
|
||||
// limits: the comp's start / end (unless already past it) and its other end.
|
||||
let (in_delta, out_delta) = (snap("inDelta"), snap("outDelta"));
|
||||
s.edit("Layer Timing", merge_p(p), |proj, _| {
|
||||
let comp = proj.comp_mut(cid).ok_or(EngineError::NoComp)?;
|
||||
let fd = comp.frame_duration();
|
||||
let dur = comp.duration;
|
||||
for l in comp.layers.iter_mut().filter(|l| ids.contains(&l.id)) {
|
||||
if let Some(d) = in_delta {
|
||||
l.in_point = (l.in_point + d).max(Tick::ZERO.min(l.in_point)).min(l.out_point - fd);
|
||||
}
|
||||
if let Some(d) = out_delta {
|
||||
l.out_point = (l.out_point + d).min(dur.max(l.out_point)).max(l.in_point + fd);
|
||||
}
|
||||
let shift = |l: &mut Layer, d: Tick| {
|
||||
l.start_time += d;
|
||||
l.in_point += d;
|
||||
@@ -1530,7 +1541,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Layer Timing",
|
||||
[],
|
||||
None,
|
||||
"{layers?, op?: moveInToTime|moveOutToTime|trimInToTime|trimOutToTime, delta?, start?, in?, out?, merge?}",
|
||||
"{layers?, op?: moveInToTime|moveOutToTime|trimInToTime|trimOutToTime, delta?, start?, in?, out?, inDelta?, outDelta? (trim each layer's In / Out by seconds), merge?}",
|
||||
has_layers,
|
||||
timing
|
||||
),
|
||||
|
||||
@@ -883,3 +883,32 @@ fn property_edits_apply_to_every_selected_layer() {
|
||||
s.execute("prop.set", json!({"layer": a, "path": "transform/rotation", "value": 30})).unwrap();
|
||||
assert_eq!(prop(&s, b, "transform/rotation").value.as_f64(), 0.0);
|
||||
}
|
||||
|
||||
/// `inDelta` / `outDelta` trim every layer by the same amount (the Timeline's trim drag over
|
||||
/// several selected layers), each within its own limits, and a drag is one undo step.
|
||||
#[test]
|
||||
fn timing_deltas_trim_each_layer_within_its_limits() {
|
||||
let (mut s, a) = setup();
|
||||
let b = s.execute("layer.newSolid", json!({"color": "#ff0000", "width": 10, "height": 10})).unwrap()["layer"].as_u64().unwrap();
|
||||
s.execute("layer.timing", json!({"layers": [a], "in": 0.5, "out": 2.0})).unwrap();
|
||||
s.execute("layer.timing", json!({"layers": [b], "in": 1.0, "out": 3.5})).unwrap();
|
||||
let times = |s: &Session| -> Vec<(f64, f64)> {
|
||||
let c = s.active_comp().unwrap();
|
||||
[a, b].iter().map(|id| c.layer(effectcraft_project::LayerId(*id)).map(|l| (l.in_point.seconds(), l.out_point.seconds())).unwrap()).collect()
|
||||
};
|
||||
let before = times(&s);
|
||||
// A drag: two steps of half a second, one merge key; B stops at the comp's end (4 s).
|
||||
for _ in 0..2 {
|
||||
s.execute("layer.timing", json!({"layers": [a, b], "outDelta": 0.5, "merge": "bar-1"})).unwrap();
|
||||
}
|
||||
assert_eq!(times(&s), vec![(0.5, 3.0), (1.0, 4.0)]);
|
||||
assert!(s.undo());
|
||||
assert_eq!(times(&s), before, "one undo step restores every layer");
|
||||
// In points: A stops at the comp's start; neither crosses its Out point.
|
||||
s.execute("layer.timing", json!({"layers": [a, b], "inDelta": -1.0})).unwrap();
|
||||
assert_eq!(times(&s), vec![(0.0, 2.0), (0.0, 3.5)]);
|
||||
s.execute("layer.timing", json!({"layers": [a, b], "inDelta": 3.0})).unwrap();
|
||||
let fd = 1.0 / 30.0;
|
||||
let t = times(&s);
|
||||
assert!((t[0].0 - (2.0 - fd)).abs() < 1e-9 && (t[1].0 - 3.0).abs() < 1e-9, "{t:?}");
|
||||
}
|
||||
|
||||
@@ -2149,14 +2149,13 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
if frames != 0 {
|
||||
acc -= (frames as f64 * fd * pps) as f32;
|
||||
let d = frames as f64 * fd;
|
||||
// A selected layer's bar moves or trims every selected layer by the
|
||||
// same amount (each within its own limits), as in After Effects.
|
||||
let layers = if is_sel { json!(selected.iter().map(|l| l.0).collect::<Vec<_>>()) } else { json!([layer.id.0]) };
|
||||
let params = match kind {
|
||||
"move" => {
|
||||
json!({"layers": if is_sel { json!(selected.iter().map(|l| l.0).collect::<Vec<_>>()) } else { json!([layer.id.0]) }, "delta": d, "merge": drag_key})
|
||||
}
|
||||
"in" => json!({"layers": [layer.id.0], "in": (layer.in_point.seconds() + d).max(0.0), "merge": drag_key}),
|
||||
_ => {
|
||||
json!({"layers": [layer.id.0], "out": (layer.out_point.seconds() + d).min(comp.duration.seconds()), "merge": drag_key})
|
||||
}
|
||||
"move" => json!({"layers": layers, "delta": d, "merge": drag_key}),
|
||||
"in" => json!({"layers": layers, "inDelta": d, "merge": drag_key}),
|
||||
_ => json!({"layers": layers, "outDelta": d, "merge": drag_key}),
|
||||
};
|
||||
actions.push(("layer.timing".into(), params));
|
||||
}
|
||||
|
||||
@@ -119,3 +119,37 @@ fn dragging_bar_ends_trims_in_and_out() {
|
||||
let (i, o, s) = timing(&h, pre);
|
||||
assert!((i - 3.0).abs() < 1e-6 && (o - 8.0).abs() < 1.5 / 30.0 && s.abs() < 1e-6, "out extended to ~8 s, not slipped: in {i} out {o} start {s}");
|
||||
}
|
||||
|
||||
/// Trimming a selected layer's end trims every selected layer by the same amount (one undo
|
||||
/// step), as in After Effects; an unselected layer's end trims that layer alone.
|
||||
#[test]
|
||||
fn dragging_a_selected_bar_end_trims_every_selected_layer() {
|
||||
let (mut h, solid, pre) = harness();
|
||||
h.state_mut().session.execute("layer.select", json!({"layers": [solid, pre]})).unwrap();
|
||||
h.run_steps(2);
|
||||
let b = bar(&h, pre);
|
||||
let pps = bar(&h, solid).width() as f64 / 4.0;
|
||||
let y = b.center().y;
|
||||
drag(&mut h, pos2(b.max.x - 2.0, y), pos2(b.max.x - 2.0 + pps as f32, y));
|
||||
let ((si, so, _), (pi, po, _)) = (timing(&h, solid), timing(&h, pre));
|
||||
assert!((so - 7.0).abs() < 1.5 / 30.0 && (po - 8.0).abs() < 1.5 / 30.0, "both outs ~+1 s: solid {so}, precomp {po}");
|
||||
assert!((so - 6.0 - (po - 7.0)).abs() < 1e-9, "the same delta for both");
|
||||
assert!((si - 2.0).abs() < 1e-9 && (pi - 3.0).abs() < 1e-9, "in points untouched");
|
||||
// The In end too.
|
||||
let b = bar(&h, solid);
|
||||
drag(&mut h, pos2(b.min.x + 2.0, b.center().y), pos2(b.min.x + 2.0 + pps as f32 / 2.0, b.center().y));
|
||||
let ((si, _, _), (pi, _, _)) = (timing(&h, solid), timing(&h, pre));
|
||||
assert!((si - 2.5).abs() < 1.5 / 30.0 && (si - 2.0 - (pi - 3.0)).abs() < 1e-9, "both ins ~+0.5 s: solid {si}, precomp {pi}");
|
||||
// One undo step per drag.
|
||||
h.state_mut().session.undo();
|
||||
h.state_mut().session.undo();
|
||||
h.run_steps(2);
|
||||
assert_eq!((timing(&h, solid), timing(&h, pre)), ((2.0, 6.0, 0.0), (3.0, 7.0, 0.0)));
|
||||
// An unselected layer's end: only it.
|
||||
h.state_mut().session.execute("layer.select", json!({"layers": [solid]})).unwrap();
|
||||
h.run_steps(2);
|
||||
let b = bar(&h, pre);
|
||||
drag(&mut h, pos2(b.max.x - 2.0, y), pos2(b.max.x - 2.0 + pps as f32, y));
|
||||
assert_eq!(timing(&h, solid), (2.0, 6.0, 0.0), "the selected layer stays");
|
||||
assert!((timing(&h, pre).1 - 8.0).abs() < 1.5 / 30.0);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user