Timeline: Shift-dragging the work area snaps to the current time (#252)
Shift-dragging the work area's begin or end handle now snaps it to the current-time indicator, keyframes, layer in and out points and markers within 8 px, with the snap candidates the CTI and keyframe drags use. The work area bar itself drags too, as in After Effects: it moves the work area and keeps its length, and with Shift the nearer end snaps. Test: shift_dragging_the_work_area_snaps_to_the_current_time. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
41be200749
commit
0ac5f9fb9b
|
|
@ -330,6 +330,18 @@ fn snap_candidates(comp: &Comp, rows: &[Row]) -> Vec<f64> {
|
|||
v
|
||||
}
|
||||
|
||||
/// Shift-dragging the work area or one of its ends (#252): the correction (seconds) that puts
|
||||
/// the nearest of the `moving` times on the current time, a key, a layer in / out point or a
|
||||
/// marker within 8 px (the work area's own ends are no targets).
|
||||
fn work_area_snap(app: &EffectcraftApp, comp: &Comp, moving: &[f64], pps: f64) -> f64 {
|
||||
let (a, b) = (comp.work_area.0.seconds(), comp.work_area.1.seconds());
|
||||
let mut cands = snap_candidates(comp, &build_rows(app, comp));
|
||||
cands.retain(|c| (c - a).abs() > 1e-9 && (c - b).abs() > 1e-9);
|
||||
cands.push(app.session.time().seconds());
|
||||
let tol = 8.0 / pps.max(1e-6);
|
||||
moving.iter().map(|t| snap_time(&cands, *t, tol) - t).filter(|d| *d != 0.0).min_by(|x, y| x.abs().total_cmp(&y.abs())).unwrap_or(0.0)
|
||||
}
|
||||
|
||||
/// Timeline horizontal mapping.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub(crate) struct TMap {
|
||||
|
|
@ -1079,10 +1091,31 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
|||
p.rect_filled(ruler, 0.0, t.tl_ruler_bg);
|
||||
app.auto.add("timeline.ruler", ruler, &format!("{},{}", tm.start, tm.pps));
|
||||
// Work area bar: below the ruler, beside the column headers (After Effects' layout), with
|
||||
// blue begin / end handles; the cache bar runs under it.
|
||||
// blue begin / end handles; the cache bar runs under it. Dragging the bar moves the work
|
||||
// area, the handles move its ends; Shift snaps (#252).
|
||||
let wa_row = Rect::from_min_max(pos2(graph_x0, top + header_h), pos2(rect.max.x, top + header_h + colhdr_h));
|
||||
let wa = Rect::from_min_max(pos2(tm.x(comp.work_area.0.seconds()), wa_row.min.y + 3.0), pos2(tm.x(comp.work_area.1.seconds()), wa_row.max.y - 6.0));
|
||||
let (wa0, wa1) = (comp.work_area.0.seconds(), comp.work_area.1.seconds());
|
||||
let wa = Rect::from_min_max(pos2(tm.x(wa0), wa_row.min.y + 3.0), pos2(tm.x(wa1), wa_row.max.y - 6.0));
|
||||
p.with_clip_rect(wa_row).rect_filled(wa, 0.0, WORK_AREA_BAR);
|
||||
let shift = ui.input(|i| i.modifiers.shift);
|
||||
let bar = wa.intersect(wa_row);
|
||||
let bresp = ui.interact(bar, egui::Id::new("wa-bar"), Sense::drag());
|
||||
app.auto.add("timeline.workArea.bar", bar, "Work area");
|
||||
let bar_start = egui::Id::new("wa-bar-start");
|
||||
if bresp.drag_started() {
|
||||
ctx.data_mut(|d| d.insert_temp(bar_start, wa0));
|
||||
}
|
||||
if bresp.dragged()
|
||||
&& let (Some(a0), Some(d)) = (ctx.data(|d| d.get_temp::<f64>(bar_start)), bresp.total_drag_delta())
|
||||
{
|
||||
let len = wa1 - wa0;
|
||||
let mut a = a0 + d.x as f64 / pps.max(1e-6);
|
||||
if shift {
|
||||
a += work_area_snap(app, &comp, &[a, a + len], pps);
|
||||
}
|
||||
let a = fr.snap_nearest(Tick::from_seconds_f64(a.clamp(0.0, (comp.duration.seconds() - len).max(0.0)))).seconds();
|
||||
let _ = app.session.execute("comp.workArea", json!({"start": a, "end": a + len, "merge": "wa-drag"}));
|
||||
}
|
||||
for (hx, set) in [(wa.min.x, "begin"), (wa.max.x, "end")] {
|
||||
let hr = Rect::from_center_size(pos2(hx, wa.center().y), vec2(8.0, 14.0));
|
||||
let handle = if set == "begin" {
|
||||
|
|
@ -1096,7 +1129,11 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
|||
if resp.dragged()
|
||||
&& let Some(pt) = resp.interact_pointer_pos()
|
||||
{
|
||||
let secs = fr.snap_nearest(Tick::from_seconds_f64(tm.t(pt.x).max(0.0))).seconds();
|
||||
let mut secs = tm.t(pt.x).max(0.0);
|
||||
if shift {
|
||||
secs += work_area_snap(app, &comp, &[secs], pps);
|
||||
}
|
||||
let secs = fr.snap_nearest(Tick::from_seconds_f64(secs)).seconds();
|
||||
let key = if set == "begin" { "start" } else { "end" };
|
||||
let _ = app.session.execute("comp.workArea", json!({key: secs, "merge": "wa-drag"}));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,16 +64,24 @@ fn rect(h: &Harness<'_, EffectcraftApp>, id: &str) -> egui::Rect {
|
|||
}
|
||||
|
||||
fn drag(h: &mut Harness<'_, EffectcraftApp>, from: Pos2, to: Pos2) {
|
||||
drag_with(h, from, to, Modifiers::NONE);
|
||||
}
|
||||
|
||||
/// [`drag`] with `modifiers` held.
|
||||
fn drag_with(h: &mut Harness<'_, EffectcraftApp>, from: Pos2, to: Pos2, modifiers: Modifiers) {
|
||||
h.event(Event::ModifiersChanged(modifiers));
|
||||
h.event(Event::PointerMoved(from));
|
||||
h.step();
|
||||
h.event(Event::PointerButton { pos: from, button: egui::PointerButton::Primary, pressed: true, modifiers: Modifiers::NONE });
|
||||
h.event(Event::PointerButton { pos: from, button: egui::PointerButton::Primary, pressed: true, modifiers });
|
||||
h.step();
|
||||
for k in 1..=8 {
|
||||
h.event(Event::PointerMoved(from + (to - from) * (k as f32 / 8.0)));
|
||||
h.step();
|
||||
}
|
||||
h.event(Event::PointerButton { pos: to, button: egui::PointerButton::Primary, pressed: false, modifiers: Modifiers::NONE });
|
||||
h.event(Event::PointerButton { pos: to, button: egui::PointerButton::Primary, pressed: false, modifiers });
|
||||
h.run_steps(2);
|
||||
h.event(Event::ModifiersChanged(Modifiers::NONE));
|
||||
h.step();
|
||||
}
|
||||
|
||||
/// The visible span (seconds) from the ruler's automation label ("start,pps") and width.
|
||||
|
|
@ -161,3 +169,47 @@ fn spacebar_drag_scrolls_the_time_graph() {
|
|||
drag(&mut h, from, from - vec2(400.0, 0.0));
|
||||
assert!(in_point(&h) < -1.0, "{}", in_point(&h));
|
||||
}
|
||||
|
||||
/// #252: Shift-dragging a work area end, or the whole work area bar, snaps to the current-time
|
||||
/// indicator when it comes within reach; without Shift it lands where the pointer is.
|
||||
#[test]
|
||||
fn shift_dragging_the_work_area_snaps_to_the_current_time() {
|
||||
let mut s = Session::default();
|
||||
s.execute("comp.new", json!({"name": "Short", "width": 320, "height": 180, "frameRate": 30, "duration": 10})).unwrap();
|
||||
s.execute("layer.newSolid", json!({"name": "Plate", "color": "#406080"})).unwrap();
|
||||
let mut app = EffectcraftApp::new(s);
|
||||
app.ui.timeline.pps = Some(100.0);
|
||||
let mut h = Harness::builder().with_size(vec2(1600.0, 1000.0)).build_eframe(|_| app);
|
||||
let reset = |h: &mut Harness<'_, EffectcraftApp>| {
|
||||
h.state_mut().session.execute("comp.workArea", json!({"start": 1.0, "end": 4.0})).unwrap();
|
||||
h.state_mut().session.execute("time.set", json!({"time": 6.0})).unwrap();
|
||||
h.run_steps(3);
|
||||
};
|
||||
let work_area = |h: &Harness<'_, EffectcraftApp>| {
|
||||
let wa = h.state().session.active_comp().unwrap().work_area;
|
||||
(wa.0.seconds(), wa.1.seconds())
|
||||
};
|
||||
let near = |a: (f64, f64), b: (f64, f64)| (a.0 - b.0).abs() < 1e-6 && (a.1 - b.1).abs() < 1e-6;
|
||||
reset(&mut h);
|
||||
// Screen x of a time, from the handles at 1 s and 4 s.
|
||||
let (b, e) = (rect(&h, "timeline.workArea.begin").center(), rect(&h, "timeline.workArea.end").center());
|
||||
let pps = (e.x - b.x) / 3.0;
|
||||
let x = |t: f32| b.x + (t - 1.0) * pps;
|
||||
// The end handle to 5 px before the CTI (6 s).
|
||||
let to = pos2(x(6.0) - 5.0, e.y);
|
||||
drag(&mut h, e, to);
|
||||
assert!(work_area(&h).1 < 5.99, "no Shift: no snap {:?}", work_area(&h));
|
||||
reset(&mut h);
|
||||
drag_with(&mut h, e, to, Modifiers::SHIFT);
|
||||
assert!(near(work_area(&h), (1.0, 6.0)), "the end snaps to the CTI: {:?}", work_area(&h));
|
||||
// The bar moved so its end comes 5 px before the CTI: it moves as a whole and snaps.
|
||||
reset(&mut h);
|
||||
let mid = rect(&h, "timeline.workArea.bar").center();
|
||||
let to = mid + vec2(x(6.0) - 5.0 - e.x, 0.0);
|
||||
drag(&mut h, mid, to);
|
||||
let (a, z) = work_area(&h);
|
||||
assert!((z - a - 3.0).abs() < 1e-6 && z < 5.99 && a > 1.5, "no Shift: moved, no snap {a}..{z}");
|
||||
reset(&mut h);
|
||||
drag_with(&mut h, mid, to, Modifiers::SHIFT);
|
||||
assert!(near(work_area(&h), (3.0, 6.0)), "the bar snaps its end to the CTI: {:?}", work_area(&h));
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue