Files
effectcraft/crates/ui-egui/tests/ui_graph_fit.rs
T
Elias ac442f8c61 ui: Graph Editor fits the selection when it opens
Opening the Graph Editor (header button, menu, Shift+F3, control channel)
frames the graphs on its first frame: Fit Selection to View when keys are
selected, else Fit All Graphs to View. Switching value/speed graph refits
too (different units). A selected key now shows its property's graph even
when its layer isn't selected, and Fit Selection on a lone key also frames
its neighbouring keys.

Co-Authored-By: Claude <[email protected]>

(cherry picked from commit a3c95f601e78e7a12f62aba94ee3515b2ade1670)

(cherry picked from commit d6309ffad79b91676d6409d1845de508e485873d)
(cherry picked from commit 78f557433742e443c9b7db34c1dac67e6a2b8e18)

(cherry picked from commit 7a4e160434b7bf1289cbf5900a86715814d1c791)
2026-10-12 19:21:41 +01:00

80 lines
4.0 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Opening the Graph Editor frames the graphs, as in After Effects: Fit Selection to View when
//! keys are selected (a key selected in the Timeline shows its property's graph even when the
//! layer isn't selected), else Fit All Graphs to View. It used to open on an empty 0–100 range
//! or with the curves off-view.
use effectcraft_engine::Session;
use effectcraft_ui_egui::EffectcraftApp;
use egui_kittest::Harness;
use serde_json::json;
/// A 20 s comp whose Box layer has Rotation keys 300° at 8 s and 720° at 12 s; the Timeline is
/// zoomed in on 0–4 s, so neither key is in view. Returns the harness and Rotation's uid.
fn harness(select_layer: bool) -> (Harness<'static, EffectcraftApp>, u64, u64) {
let mut s = Session::default();
s.execute("comp.new", json!({"name": "Fit", "width": 320, "height": 180, "frameRate": 30, "duration": 20})).unwrap();
s.execute("layer.newSolid", json!({"name": "Box", "color": "#e04020", "width": 80, "height": 80})).unwrap();
let id = s.active_comp().unwrap().layers[0].id;
for (t, v) in [(8.0, 300.0), (12.0, 720.0)] {
s.execute("prop.addKey", json!({"layer": id.0, "path": "transform/rotation", "time": t, "value": v})).unwrap();
}
let uid = s.active_comp().unwrap().layer(id).unwrap().props.prop("transform/rotation").unwrap().uid;
s.execute("layer.select", json!({"layers": if select_layer { vec![id.0] } else { vec![] }})).unwrap();
let mut h = Harness::builder().with_size(egui::vec2(1600.0, 1000.0)).build_eframe(|_| EffectcraftApp::new(s));
h.state_mut().ui.timeline.pps = Some(300.0);
h.state_mut().ui.timeline.start = 0.0;
h.run_steps(3);
(h, id.0, uid)
}
fn open_graph(h: &mut Harness<'_, EffectcraftApp>) {
let ctx = h.ctx.clone();
effectcraft_ui_egui::menus::invoke(h.state_mut(), &ctx, "timeline.graphEditor", json!({})).unwrap();
h.run_steps(4);
assert!(h.state().ui.timeline.graph_editor);
}
/// Every key of the property sits inside the plot, and the value range spans `lo..=hi`.
fn assert_framed(h: &Harness<'_, EffectcraftApp>, uid: u64, keys: &[usize], lo: f64, hi: f64) {
let plot = h.state().auto.find("timeline.graph").expect("graph plot").rect;
for i in keys {
let k = h.state().auto.find(&format!("timeline.graph.key.{uid}.0.{i}")).unwrap_or_else(|| panic!("graph key {i} not drawn")).rect;
let (cx, cy) = (k[0] + k[2] / 2.0, k[1] + k[3] / 2.0);
assert!(cx > plot[0] && cx < plot[0] + plot[2] && cy > plot[1] && cy < plot[1] + plot[3], "key {i} at ({cx}, {cy}) outside plot {plot:?}");
}
let (a, b) = h.state().ui.timeline.graph_range.expect("graph range");
assert!(a <= lo && b >= hi, "value range {a}..{b} doesn't contain {lo}..{hi}");
}
#[test]
fn opening_the_graph_editor_fits_the_selected_key() {
let (mut h, layer, uid) = harness(false);
h.state_mut().session.execute("keys.select", json!({"keys": [{"layer": layer, "prop": uid, "time": 12.0}]})).unwrap();
h.run_steps(2);
open_graph(&mut h);
// A lone key also frames its neighbour, so its curve shows.
assert_framed(&h, uid, &[0, 1], 300.0, 720.0);
assert!(!h.state().ui.timeline.graph_auto_zoom, "Fit Selection turns auto-zoom off");
}
#[test]
fn opening_the_graph_editor_with_no_keys_selected_fits_all_graphs() {
let (mut h, _, uid) = harness(true);
open_graph(&mut h);
assert_eq!(h.state().ui.timeline.pps, None, "the whole comp in time");
assert_framed(&h, uid, &[0, 1], 300.0, 720.0);
// Zooming away and toggling the Graph Editor off and on frames it again.
h.state_mut().ui.timeline.pps = Some(300.0);
h.state_mut().ui.timeline.start = 0.0;
open_graph_toggle_twice(&mut h);
assert_framed(&h, uid, &[0, 1], 300.0, 720.0);
}
fn open_graph_toggle_twice(h: &mut Harness<'_, EffectcraftApp>) {
let ctx = h.ctx.clone();
effectcraft_ui_egui::menus::invoke(h.state_mut(), &ctx, "timeline.graphEditor", json!({})).unwrap();
h.run_steps(2);
assert!(!h.state().ui.timeline.graph_editor);
open_graph(h);
}