M5.14: J / K stop at the work area; the Info panel shows a selected keyframe

- Go to Next / Previous Keyframe (K / J) also stops at the work area's start and end, as in
  After Effects.
- With one keyframe selected, the Info panel shows its property, time and value instead of
  "1 keyframes selected".

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
bflatasticandClaude Opus 5.5 committed 2026-10-05 08:05:03 +07:00
1 parent 43bf2906d3
commit dfb0227468
3 files changed
+39 -8

No files matched your search

+4 -1
View File
@@ -79,7 +79,7 @@ fn go(s: &mut Session, p: &Value) -> Result<Value> {
"layerIn" => sel_layer.map(|l| l.in_point).unwrap_or(t),
"layerOut" => sel_layer.map(|l| l.out_point - comp.frame_duration()).unwrap_or(t),
"nextKey" | "prevKey" => {
// Keyframes (of visible/selected layers) and markers, in comp time.
// Keyframes (of visible/selected layers), markers and the work area, in comp time.
let mut times: Vec<Tick> = comp.markers.iter().map(|m| m.time).collect();
let layers: Vec<_> = if s.state.selected_layers.is_empty() {
comp.layers.iter().collect()
@@ -90,6 +90,9 @@ fn go(s: &mut Session, p: &Value) -> Result<Value> {
let only = p.get("prop").and_then(Value::as_u64);
if only.is_some() {
times.clear();
} else {
// The work area's start and end too, as in After Effects.
times.extend([comp.work_area.0, comp.work_area.1 - comp.frame_duration()]);
}
for l in layers {
l.props.walk("", &mut |_, pr| {
+16
View File
@@ -1014,3 +1014,19 @@ fn keys_from_several_layers_paste_in_order_and_paste_reversed_respects_stretch()
assert!(ct.len() == 2 && (ct[1] - ct[0] - 1.0).abs() < 1e-6, "keys 1 s apart in comp time: {ct:?}");
assert_eq!(vals, vec![10.0, 0.0], "reversed");
}
#[test]
fn j_and_k_stop_at_keys_markers_and_the_work_area() {
let mut s = comp();
let a = solid(&mut s, "#ff0000");
opacity_keys(&mut s, a, &[2.0]);
s.execute("comp.workArea", json!({"start": 1.0, "end": 3.0})).unwrap();
s.execute("layer.select", json!({"layers": [a]})).unwrap();
let mut stops = vec![];
for _ in 0..4 {
s.execute("time.nextKey", json!({})).unwrap();
stops.push((s.time().seconds() * 30.0).round() / 30.0);
}
let last = (3.0 * 30.0 - 1.0) / 30.0;
assert_eq!(stops, vec![1.0, 2.0, (last * 30.0f64).round() / 30.0, (last * 30.0f64).round() / 30.0]);
}
+19 -7
View File
@@ -101,13 +101,25 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
p.text(pos2(x0, ly), Align2::LEFT_CENTER, format!("Render: {ms:.0} ms • UI {:.0} fps{rq}", app.fps), Tokens::ui(11.5), t.text_faint);
}
if !app.session.state.selected_keys.is_empty() {
p.text(
pos2(x0, ly + 18.0),
Align2::LEFT_CENTER,
format!("{} keyframes selected", app.session.state.selected_keys.len()),
Tokens::ui(11.5),
t.text_dim,
);
// One key: its property, time and value (as After Effects' Info panel shows them).
let sel = &app.session.state.selected_keys;
let one = match sel.as_slice() {
[k] => c.layer(k.layer).and_then(|l| {
let pr = l.props.find(k.prop)?;
let key = pr.keys.iter().find(|x| x.time == k.time)?;
let tc = crate::panels::timecode(&app.session, &c, l.comp_time(k.time));
let v = match &key.value {
effectcraft_engine::keyframe::Value::Scalar(v) => format!("{v:.2}"),
effectcraft_engine::keyframe::Value::Vec2(v) => format!("{:.1}, {:.1}", v[0], v[1]),
effectcraft_engine::keyframe::Value::Vec3(v) => format!("{:.1}, {:.1}, {:.1}", v[0], v[1], v[2]),
_ => String::new(),
};
Some(format!("{} {tc} {v}", pr.name).trim_end().to_string())
}),
_ => None,
};
let line = one.unwrap_or_else(|| format!("{} keyframes selected", sel.len()));
p.text(pos2(x0, ly + 18.0), Align2::LEFT_CENTER, line, Tokens::ui(11.5), t.text_dim);
}
}
}