From dfb02274683c5841e10ebd79aa574487114a4833 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Mon, 5 Oct 2026 08:05:03 +0700 Subject: [PATCH] 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 --- crates/engine/src/commands/time.rs | 5 ++++- crates/engine/src/tests_menu_cmds.rs | 16 ++++++++++++++++ crates/ui-egui/src/panels/info.rs | 26 +++++++++++++++++++------- 3 files changed, 39 insertions(+), 8 deletions(-) diff --git a/crates/engine/src/commands/time.rs b/crates/engine/src/commands/time.rs index 0b6d19bc..1c4c1c64 100644 --- a/crates/engine/src/commands/time.rs +++ b/crates/engine/src/commands/time.rs @@ -79,7 +79,7 @@ fn go(s: &mut Session, p: &Value) -> Result { "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 = 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 { 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| { diff --git a/crates/engine/src/tests_menu_cmds.rs b/crates/engine/src/tests_menu_cmds.rs index abf79d70..7f1fce5f 100644 --- a/crates/engine/src/tests_menu_cmds.rs +++ b/crates/engine/src/tests_menu_cmds.rs @@ -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]); +} diff --git a/crates/ui-egui/src/panels/info.rs b/crates/ui-egui/src/panels/info.rs index 92df6f8c..4fad6666 100644 --- a/crates/ui-egui/src/panels/info.rs +++ b/crates/ui-egui/src/panels/info.rs @@ -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); } } }