mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-24 10:09:32 -06:00
fix(graph): speed graph Fit Selection frames the curve between the keys (fixes #1617)
This commit is contained in:
@@ -109,6 +109,15 @@ fn shown_dims(l: &Layer, pr: &Property) -> usize {
|
||||
if pr.shown_dims > 0 && !l.is_3d() { (pr.shown_dims as usize).min(n) } else { n.min(4) }
|
||||
}
|
||||
|
||||
/// The speed graph's value of dimension `d` at layer time `lt` (one curve for a spatial property).
|
||||
fn speed_at(pr: &Property, spatial: bool, d: usize, lt: Tick) -> f64 {
|
||||
if spatial {
|
||||
effectcraft_engine::keyframe::speed(&pr.keys, lt, true)
|
||||
} else {
|
||||
effectcraft_engine::keyframe::velocity(&pr.keys, lt, false).get(d).copied().unwrap_or(0.0).abs()
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the curves for the visible time span.
|
||||
fn curves<'a>(app: &EffectcraftApp, comp: &'a Comp, ectx: &EvalCtx, tm: TMap, plot: Rect, speed: bool) -> Vec<Curve<'a>> {
|
||||
let sel = &app.session.state.selected_keys;
|
||||
@@ -126,16 +135,7 @@ fn curves<'a>(app: &EffectcraftApp, comp: &'a Comp, ectx: &EvalCtx, tm: TMap, pl
|
||||
let x = plot.min.x + plot.width() * i as f32 / (n - 1) as f32;
|
||||
let ct = tm.t(x);
|
||||
let tt = Tick::from_seconds_f64(ct);
|
||||
let v = if speed {
|
||||
let lt = l.layer_time(tt);
|
||||
if spatial {
|
||||
effectcraft_engine::keyframe::speed(&pr.keys, lt, true)
|
||||
} else {
|
||||
effectcraft_engine::keyframe::velocity(&pr.keys, lt, false).get(d).copied().unwrap_or(0.0).abs()
|
||||
}
|
||||
} else {
|
||||
ectx.at(tt).value(l, pr).components().get(d).copied().unwrap_or(0.0)
|
||||
};
|
||||
let v = if speed { speed_at(pr, spatial, d, l.layer_time(tt)) } else { ectx.at(tt).value(l, pr).components().get(d).copied().unwrap_or(0.0) };
|
||||
pts.push((ct, v));
|
||||
}
|
||||
let mut keys = vec![];
|
||||
@@ -290,7 +290,7 @@ pub(crate) fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painte
|
||||
ctx.data_mut(|d| d.insert_temp(was_speed_id, speed));
|
||||
if ctx.data_mut(|d| d.remove_temp::<bool>(fit_pending_id())).unwrap_or(false) || type_changed {
|
||||
if cs.iter().any(|c| c.keys.iter().any(|k| k.sel)) {
|
||||
fit_selection(app, &cs, tm, plot);
|
||||
fit_selection(app, &cs, tm, plot, speed);
|
||||
} else {
|
||||
fit_all(app);
|
||||
}
|
||||
@@ -584,7 +584,7 @@ pub(crate) fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painte
|
||||
fit_all(app);
|
||||
}
|
||||
if text_button(app, ui, p, &mut buttons, y, "Fit Selection", false, "fitSelection", "Fit selection to view") {
|
||||
fit_selection(app, &cs, tm, plot);
|
||||
fit_selection(app, &cs, tm, plot, speed);
|
||||
}
|
||||
buttons.x += 6.0;
|
||||
let (snap, reference, tbox) = (app.ui.timeline.graph_snap, app.ui.timeline.graph_reference, app.ui.timeline.graph_transform_box);
|
||||
@@ -646,7 +646,7 @@ pub(crate) fn track_open(ctx: &egui::Context, on: bool) {
|
||||
}
|
||||
|
||||
/// Zoom time and value to the selected keys (and their handles).
|
||||
fn fit_selection(app: &mut EffectcraftApp, cs: &[Curve], tm: TMap, plot: Rect) {
|
||||
fn fit_selection(app: &mut EffectcraftApp, cs: &[Curve], tm: TMap, plot: Rect, speed: bool) {
|
||||
let mut t0 = f64::INFINITY;
|
||||
let mut t1 = f64::NEG_INFINITY;
|
||||
let mut v0 = f64::INFINITY;
|
||||
@@ -678,6 +678,26 @@ fn fit_selection(app: &mut EffectcraftApp, cs: &[Curve], tm: TMap, plot: Rect) {
|
||||
}
|
||||
}
|
||||
}
|
||||
// In the speed graph eased keys sit at speed 0 and the curve peaks between them (#1617):
|
||||
// frame the curve from the first to the last selected key. A lone key has its handles on
|
||||
// the speed graph, so the check above misses it: it frames its neighbouring segments here.
|
||||
if speed {
|
||||
for c in cs {
|
||||
let (Some(first), Some(last)) = (c.keys.iter().position(|k| k.sel), c.keys.iter().rposition(|k| k.sel)) else { continue };
|
||||
let (a, b) = if first == last { (first.saturating_sub(1), last + 1) } else { (first, last) };
|
||||
let (Some(ka), Some(kb)) = (c.keys.get(a), c.keys.get(b.min(c.keys.len().saturating_sub(1)))) else { continue };
|
||||
t0 = t0.min(ka.t);
|
||||
t1 = t1.max(kb.t);
|
||||
let (s0, s1) = (ka.time.seconds(), kb.time.seconds());
|
||||
const SAMPLES: usize = 128;
|
||||
for i in 0..=SAMPLES {
|
||||
let lt = Tick::from_seconds_f64(s0 + (s1 - s0) * i as f64 / SAMPLES as f64);
|
||||
let v = speed_at(c.prop, !c.editable, c.dim, lt);
|
||||
v0 = v0.min(v);
|
||||
v1 = v1.max(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
let span = (t1 - t0).max(0.2);
|
||||
let pps = plot.width() as f64 * 0.8 / span;
|
||||
// Through the timeline's zoom, so the fit never zooms out past the whole comp.
|
||||
|
||||
@@ -466,3 +466,39 @@ fn speed_graph_handle_drags_upward_past_the_top() {
|
||||
// The speed per pixel depends on the graph's fitted scale; only the proportion matters.
|
||||
assert!(short > 0.0 && long > 9.0 * short, "40 px up gave {short}, 400 px up gave {long}");
|
||||
}
|
||||
|
||||
/// #1617: with Easy Ease both keys sit at speed 0 in the speed graph, so fitting the selection to
|
||||
/// its keys and handles framed -1..1 while the curve between them peaks far above. Opening the
|
||||
/// Graph Editor on selected keys and Fit Selection both frame the curve's peak, and a lone
|
||||
/// selected key frames its neighbouring segments.
|
||||
#[test]
|
||||
fn speed_graph_fit_selection_frames_the_curve_between_the_keys() {
|
||||
let (mut h, id, uid) = harness();
|
||||
let s = &mut h.state_mut().session;
|
||||
s.execute("prop.select", json!({"layer": id.0, "prop": uid})).unwrap();
|
||||
s.execute("keys.selectAll", json!({})).unwrap();
|
||||
s.execute("keys.easyEase", json!({})).unwrap();
|
||||
let keys = h.state().session.active_comp().unwrap().layer(id).unwrap().props.prop("transform/opacity").unwrap().keys.clone();
|
||||
let peak = (0..=100)
|
||||
.map(|i| effectcraft_engine::keyframe::velocity(&keys, effectcraft_engine::time::Tick::from_seconds_f64(i as f64 / 100.0), false)[0].abs())
|
||||
.fold(0.0, f64::max);
|
||||
assert!(peak > 100.0, "the eased curve peaks above its average speed: {peak}");
|
||||
h.state_mut().ui.timeline.graph_mode = "speed".into();
|
||||
h.state_mut().ui.timeline.graph_editor = true;
|
||||
h.run_steps(4);
|
||||
let frames_peak = |h: &Harness<'_, EffectcraftApp>, what: &str| {
|
||||
let (lo, hi) = h.state().ui.timeline.graph_range.expect("a fitted range");
|
||||
assert!(lo <= 0.0 && hi > peak * 0.9, "{what}: range {lo}..{hi}, the curve peaks at {peak}");
|
||||
};
|
||||
frames_peak(&h, "on open");
|
||||
h.state_mut().ui.timeline.graph_range = Some((-1.0, 1.0));
|
||||
let fit = h.state().auto.find("timeline.graph.fitSelection").expect("the Fit Selection button").rect;
|
||||
click_with(&mut h, pos2(fit[0] + fit[2] / 2.0, fit[1] + fit[3] / 2.0), Default::default());
|
||||
frames_peak(&h, "Fit Selection");
|
||||
let last = keys[1].time.seconds();
|
||||
h.state_mut().session.execute("keys.select", json!({"keys": [{"layer": id.0, "prop": uid, "time": last}]})).unwrap();
|
||||
assert_eq!(h.state().session.state.selected_keys.len(), 1);
|
||||
h.state_mut().ui.timeline.graph_range = Some((-1.0, 1.0));
|
||||
click_with(&mut h, pos2(fit[0] + fit[2] / 2.0, fit[1] + fit[3] / 2.0), Default::default());
|
||||
frames_peak(&h, "lone key");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user