mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-26 19:18:14 -06:00
Timeline: Quick Offset layers and keyframes in selection order (#1012)
Ctrl/Cmd+Alt dragging a selected layer bar or a selected key now runs a Quick Offset (layer.quickOffset / keys.quickOffset with fromStart, one undo step). The mode is chosen when the drag starts and needs at least two unlocked layers; the last selected layer moves by whole frames with the pointer. A readout by the pointer shows the Total Offset and the offset Per Layer, translated. A plain drag still moves the selection together, Alt alone still scales a key group in time (it now leaves Ctrl/Cmd+Alt to Quick Offset), and Ctrl/Cmd+Alt+click still toggles Hold. The key drag's readout is drawn by a shared drag_tip. Keys also register timeline.keyAt.<uid>.<ticks>, exact where keys on subframes can share a frame. Tests (ui_quick_offset): staggering layer bars in selection order and keys per layer, one undo each; plain drags and Alt scaling unchanged; the readout while dragging.
This commit is contained in:
@@ -525,6 +525,8 @@ const JAPANESE: &[(&str, &str)] = &[
|
||||
("⌘/Ctrl-drag a pin to record its motion", "⌘/Ctrl+ドラッグでピンの動きを記録"),
|
||||
("▣ {}", "▣ {}"),
|
||||
("⚠ {} is used by:", "⚠ {}は次のコマンドで使用中:"),
|
||||
("Total Offset", "合計オフセット"),
|
||||
("Per Layer", "レイヤーごと"),
|
||||
];
|
||||
|
||||
const SIMPLIFIED_CHINESE: &[(&str, &str)] = &[
|
||||
@@ -1060,6 +1062,8 @@ const SIMPLIFIED_CHINESE: &[(&str, &str)] = &[
|
||||
("After Effects project", "After Effects 项目"),
|
||||
("Lottie animation", "Lottie 动画"),
|
||||
("Project", "项目"),
|
||||
("Total Offset", "总偏移"),
|
||||
("Per Layer", "每个图层"),
|
||||
];
|
||||
|
||||
const TRADITIONAL_CHINESE: &[(&str, &str)] = &[
|
||||
@@ -1590,6 +1594,8 @@ const TRADITIONAL_CHINESE: &[(&str, &str)] = &[
|
||||
("After Effects project", "After Effects 專案"),
|
||||
("Lottie animation", "Lottie 動畫"),
|
||||
("Project", "專案"),
|
||||
("Total Offset", "總偏移"),
|
||||
("Per Layer", "每個圖層"),
|
||||
];
|
||||
|
||||
/// Ukrainian (uk): original translations of the same interface rows as the Chinese catalogs.
|
||||
@@ -2146,6 +2152,8 @@ const UKRAINIAN: &[(&str, &str)] = &[
|
||||
("After Effects project", "Проєкт After Effects"),
|
||||
("Lottie animation", "Анімація Lottie"),
|
||||
("Project", "Проєкт"),
|
||||
("Total Offset", "Загальний зсув"),
|
||||
("Per Layer", "На шар"),
|
||||
];
|
||||
|
||||
// Brazilian Portuguese.
|
||||
@@ -2580,6 +2588,8 @@ const PORTUGUESE: &[(&str, &str)] = &[
|
||||
("⌘/Ctrl-drag a pin to record its motion", "⌘/Ctrl+arraste um pino para gravar seu movimento"),
|
||||
("▣ {}", "▣ {}"),
|
||||
("⚠ {} is used by:", "⚠ {} é usado por:"),
|
||||
("Total Offset", "Deslocamento total"),
|
||||
("Per Layer", "Por camada"),
|
||||
];
|
||||
|
||||
/// Czech (cs): original translations covering every row in the Chinese panel catalogs.
|
||||
@@ -3115,6 +3125,8 @@ const CZECH: &[(&str, &str)] = &[
|
||||
"Tyto sousední klíčové snímky nelze interpolovat, protože mají různý počet bodů přechodu.",
|
||||
),
|
||||
("Variable fonts only", "Jen proměnná písma"),
|
||||
("Total Offset", "Celkový posun"),
|
||||
("Per Layer", "Na vrstvu"),
|
||||
];
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -1127,18 +1127,61 @@ fn layer_drag_id() -> egui::Id {
|
||||
}
|
||||
|
||||
/// A keyframe drag in progress: where the pointer started, the grabbed key's comp time
|
||||
/// (seconds) and how many frames the selected keys have moved so far.
|
||||
/// (seconds), how many frames the selected keys have moved so far and, for a Quick Offset
|
||||
/// (Ctrl/Cmd+Alt), how many layers it staggers.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
struct KeyDrag {
|
||||
origin_x: f32,
|
||||
anchor: f64,
|
||||
applied: i64,
|
||||
quick: Option<usize>,
|
||||
}
|
||||
|
||||
fn key_drag_id() -> egui::Id {
|
||||
egui::Id::new("tl-key-drag")
|
||||
}
|
||||
|
||||
/// A Quick Offset drag of selected layer bars (Ctrl/Cmd+Alt, #1012): where the pointer started,
|
||||
/// how many layers it staggers and how many frames the last one has moved so far.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
struct QuickBar {
|
||||
origin_x: f32,
|
||||
layers: usize,
|
||||
applied: i64,
|
||||
}
|
||||
|
||||
fn quick_bar_id(layer: LayerId) -> egui::Id {
|
||||
egui::Id::new(("tl-quick-bar", layer.0))
|
||||
}
|
||||
|
||||
/// How many layers a Quick Offset of `layers` staggers (#1012): the unlocked ones, each once, if
|
||||
/// there are at least two.
|
||||
fn quick_offset_layers(comp: &Comp, layers: impl IntoIterator<Item = LayerId>) -> Option<usize> {
|
||||
let mut seen: Vec<LayerId> = vec![];
|
||||
for id in layers {
|
||||
if !seen.contains(&id) && comp.layer(id).is_some_and(|l| !l.switches.locked) {
|
||||
seen.push(id);
|
||||
}
|
||||
}
|
||||
(seen.len() >= 2).then_some(seen.len())
|
||||
}
|
||||
|
||||
/// A Quick Offset's feedback: the last layer's move and the step from one layer to the next.
|
||||
fn quick_offset_note(frames: i64, layers: usize) -> String {
|
||||
let per = frames as f64 / layers.saturating_sub(1).max(1) as f64;
|
||||
format!("{}: {:+} f {}: {:+.2} f", tr("Total Offset"), frames, tr("Per Layer"), per)
|
||||
}
|
||||
|
||||
/// A Timeline drag's readout by the pointer (After Effects shows it in the Info panel).
|
||||
fn drag_tip(ctx: &egui::Context, t: &Tokens, ptr: Pos2, note: String) {
|
||||
let fg = ctx.layer_painter(egui::LayerId::new(egui::Order::Tooltip, egui::Id::new("tl-key-drag-tip")));
|
||||
let g = fg.layout_no_wrap(note, Tokens::ui(11.5), t.text);
|
||||
let r = Rect::from_min_size(ptr + vec2(14.0, 14.0), g.size() + vec2(10.0, 6.0));
|
||||
fg.rect_filled(r, 3.0, t.panel_bg);
|
||||
fg.rect_stroke(r, 3.0, Stroke::new(1.0, t.field_border), StrokeKind::Inside);
|
||||
fg.galley(r.min + vec2(5.0, 3.0), g, t.text);
|
||||
}
|
||||
|
||||
/// A switch pressed and dragged over other rows: the switch, the state the press gave it, and
|
||||
/// the undo step the drag folds into.
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -2126,7 +2169,41 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
}
|
||||
layer_context_menu(&body, &comp, layer, &selected, vector_footage_layer(&app.session.project, layer), &mut actions);
|
||||
let drag_key = format!("bar-{}", layer.id.0);
|
||||
// Ctrl/Cmd+Alt dragging a selected bar: Quick Offset (#1012), chosen as the
|
||||
// drag starts. The last selected layer moves by whole frames with the pointer.
|
||||
let mods = ui.input(|i| i.modifiers);
|
||||
if body.drag_started()
|
||||
&& is_sel
|
||||
&& mods.command
|
||||
&& mods.alt
|
||||
&& let Some(n) = quick_offset_layers(&comp, selected.iter().copied())
|
||||
&& let Some(p) = ctx.input(|i| i.pointer.press_origin())
|
||||
{
|
||||
ctx.data_mut(|d| d.insert_temp(quick_bar_id(layer.id), QuickBar { origin_x: p.x, layers: n, applied: 0 }));
|
||||
}
|
||||
let quick = ctx.data(|d| d.get_temp::<QuickBar>(quick_bar_id(layer.id)));
|
||||
if let Some(mut q) = quick {
|
||||
if body.dragged()
|
||||
&& let Some(ptr) = ctx.input(|i| i.pointer.latest_pos())
|
||||
{
|
||||
let frames = (((ptr.x - q.origin_x) as f64 / pps) / fd).round() as i64;
|
||||
if frames != q.applied {
|
||||
q.applied = frames;
|
||||
ctx.data_mut(|d| d.insert_temp(quick_bar_id(layer.id), q));
|
||||
let merge = format!("bar-quick-{}", layer.id.0);
|
||||
actions.push(("layer.quickOffset".into(), json!({"delta": frames as f64 * fd, "merge": merge, "fromStart": true})));
|
||||
}
|
||||
drag_tip(&ctx, &t, ptr, quick_offset_note(q.applied, q.layers));
|
||||
}
|
||||
if body.drag_stopped() {
|
||||
ctx.data_mut(|d| d.remove::<QuickBar>(quick_bar_id(layer.id)));
|
||||
ui_actions.push(UiAct::EndMerge);
|
||||
}
|
||||
}
|
||||
for (resp, kind) in [(&body, "move"), (&lin, "in"), (&lout, "out")] {
|
||||
if kind == "move" && quick.is_some() {
|
||||
continue;
|
||||
}
|
||||
if resp.dragged() {
|
||||
let acc_id = egui::Id::new(("bar-acc", layer.id.0, kind));
|
||||
let mut acc: f32 = ctx.data(|d| d.get_temp(acc_id).unwrap_or(0.0));
|
||||
@@ -2564,6 +2641,8 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
let kr = Rect::from_center_size(c, vec2(12.0, 14.0));
|
||||
let kresp = ui.interact(kr, egui::Id::new(("key", uid, k.time.0)), Sense::click_and_drag());
|
||||
app.auto.add(&format!("timeline.key.{uid}.{}", fr.frame_at(ct)), kr, &format!("{} key", prop.name));
|
||||
// Exact (layer-time ticks): keys a Quick Offset puts on subframes can share a frame.
|
||||
app.auto.add(&format!("timeline.keyAt.{uid}.{}", k.time.0), kr, &format!("{} key", prop.name));
|
||||
let mods = ui.input(|i| i.modifiers);
|
||||
let this_key = json!({"keys": [{"layer": layer.id.0, "prop": uid, "time": k.time.seconds()}], "selectProperties": true});
|
||||
if kresp.clicked() && mods.command {
|
||||
@@ -2658,7 +2737,8 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
});
|
||||
// Alt-drag the first or last key of a selected group: scale the group in time.
|
||||
if kresp.drag_started()
|
||||
&& ui.input(|i| i.modifiers.alt)
|
||||
&& mods.alt
|
||||
&& !mods.command
|
||||
&& let Some(c) = app.session.active_comp_arc()
|
||||
{
|
||||
super::graph_tools::alt_scale_begin(app, &ctx, &c, ct.seconds());
|
||||
@@ -2671,7 +2751,9 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
&& !super::graph_tools::alt_scale_active(&ctx)
|
||||
&& let Some(p) = ctx.input(|i| i.pointer.press_origin())
|
||||
{
|
||||
ctx.data_mut(|d| d.insert_temp(key_drag_id(), KeyDrag { origin_x: p.x, anchor: ct.seconds(), applied: 0 }));
|
||||
// Ctrl/Cmd+Alt: Quick Offset of the selected keys' layers (#1012).
|
||||
let quick = (mods.command && mods.alt && ks).then(|| quick_offset_layers(&comp, sel_keys.iter().map(|k| k.layer))).flatten();
|
||||
ctx.data_mut(|d| d.insert_temp(key_drag_id(), KeyDrag { origin_x: p.x, anchor: ct.seconds(), applied: 0, quick }));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2875,7 +2957,10 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
}
|
||||
let total = ((target - kd.anchor) / fd).round() as i64;
|
||||
if total != kd.applied {
|
||||
actions.push(("keys.move".into(), json!({"delta": (total - kd.applied) as f64 * fd, "merge": "key-drag"})));
|
||||
actions.push(match kd.quick {
|
||||
Some(_) => ("keys.quickOffset".into(), json!({"delta": total as f64 * fd, "merge": "key-quick-offset", "fromStart": true})),
|
||||
None => ("keys.move".into(), json!({"delta": (total - kd.applied) as f64 * fd, "merge": "key-drag"})),
|
||||
});
|
||||
kd.applied = total;
|
||||
ctx.data_mut(|d| d.insert_temp(key_drag_id(), kd));
|
||||
}
|
||||
@@ -2883,13 +2968,11 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
// Where the grabbed key is and how far the keys moved (After Effects shows this
|
||||
// in the Info panel).
|
||||
let at = Tick::from_seconds_f64(kd.anchor + kd.applied as f64 * fd);
|
||||
let note = format!("{} {:+} f", super::timecode(&app.session, &comp, at), kd.applied);
|
||||
let fg = ctx.layer_painter(egui::LayerId::new(egui::Order::Tooltip, egui::Id::new("tl-key-drag-tip")));
|
||||
let g = fg.layout_no_wrap(note, Tokens::ui(11.5), t.text);
|
||||
let r = Rect::from_min_size(ptr + vec2(14.0, 14.0), g.size() + vec2(10.0, 6.0));
|
||||
fg.rect_filled(r, 3.0, t.panel_bg);
|
||||
fg.rect_stroke(r, 3.0, Stroke::new(1.0, t.field_border), StrokeKind::Inside);
|
||||
fg.galley(r.min + vec2(5.0, 3.0), g, t.text);
|
||||
let note = match kd.quick {
|
||||
Some(n) => quick_offset_note(kd.applied, n),
|
||||
None => format!("{} {:+} f", super::timecode(&app.session, &comp, at), kd.applied),
|
||||
};
|
||||
drag_tip(&ctx, &t, ptr, note);
|
||||
app.ui.status = format!("Keyframe at {}", super::timecode(&app.session, &comp, at));
|
||||
}
|
||||
None => {
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
//! Quick Offset in the Timeline (#1012): Ctrl/Cmd+Alt dragging selected layer bars or keys
|
||||
//! staggers the layers in selection order, onto subframes, as one undo step; a plain drag still
|
||||
//! moves them together and Alt alone still scales a key group.
|
||||
|
||||
use effectcraft_engine::Session;
|
||||
use effectcraft_engine::project::LayerId;
|
||||
use effectcraft_engine::time::Tick;
|
||||
use effectcraft_ui_egui::EffectcraftApp;
|
||||
use egui::{Event, Modifiers, Pos2, pos2, vec2};
|
||||
use egui_kittest::Harness;
|
||||
use serde_json::json;
|
||||
|
||||
/// One frame at 30 fps.
|
||||
const F: i64 = 8_467_200_000;
|
||||
|
||||
/// A 4 s, 30 fps comp with three solids, each with Opacity keys at 0 s and 1 s, revealed.
|
||||
fn harness() -> (Harness<'static, EffectcraftApp>, Vec<u64>) {
|
||||
let mut s = Session::default();
|
||||
s.execute("comp.new", json!({"name": "Stagger", "width": 320, "height": 180, "frameRate": 30, "duration": 4})).unwrap();
|
||||
let mut ids = vec![];
|
||||
for i in 0..3 {
|
||||
let id =
|
||||
s.execute("layer.newSolid", json!({"name": format!("L{i}"), "color": "#e04020", "width": 80, "height": 80})).unwrap()["layer"].as_u64().unwrap();
|
||||
for (t, v) in [(0.0, 0.0), (1.0, 100.0)] {
|
||||
s.execute("prop.addKey", json!({"layer": id, "path": "transform/opacity", "time": t, "value": v})).unwrap();
|
||||
}
|
||||
ids.push(id);
|
||||
}
|
||||
s.execute("layer.select", json!({"layers": ids})).unwrap();
|
||||
let mut h = Harness::builder().with_size(vec2(1600.0, 1000.0)).build_eframe(|_| EffectcraftApp::new(s));
|
||||
h.run_steps(3);
|
||||
let ctx = h.ctx.clone();
|
||||
effectcraft_ui_egui::menus::invoke(h.state_mut(), &ctx, "timeline.reveal.opacity", json!({})).unwrap();
|
||||
h.run_steps(4);
|
||||
(h, ids)
|
||||
}
|
||||
|
||||
fn center(h: &Harness<'_, EffectcraftApp>, id: &str) -> Pos2 {
|
||||
let e = h.state().auto.find(id).unwrap_or_else(|| panic!("no {id}"));
|
||||
pos2(e.rect[0] + e.rect[2] / 2.0, e.rect[1] + e.rect[3] / 2.0)
|
||||
}
|
||||
|
||||
/// Press at `from`, move to `to` in small steps with `modifiers` held (a real drag), release.
|
||||
fn drag(h: &mut Harness<'_, EffectcraftApp>, from: Pos2, to: Pos2, modifiers: Modifiers) {
|
||||
h.input_mut().events.push(Event::PointerMoved(from));
|
||||
h.step();
|
||||
h.input_mut().events.push(Event::ModifiersChanged(modifiers));
|
||||
h.input_mut().events.push(Event::PointerButton { pos: from, button: egui::PointerButton::Primary, pressed: true, modifiers });
|
||||
h.step();
|
||||
for i in 1..=24 {
|
||||
h.input_mut().events.push(Event::PointerMoved(from + (to - from) * (i as f32 / 24.0)));
|
||||
h.step();
|
||||
}
|
||||
h.input_mut().events.push(Event::PointerButton { pos: to, button: egui::PointerButton::Primary, pressed: false, modifiers });
|
||||
h.step();
|
||||
h.input_mut().events.push(Event::ModifiersChanged(Modifiers::default()));
|
||||
h.run_steps(2);
|
||||
}
|
||||
|
||||
fn ctrl_alt() -> Modifiers {
|
||||
Modifiers { alt: true, ..Modifiers::COMMAND }
|
||||
}
|
||||
|
||||
fn start(h: &Harness<'_, EffectcraftApp>, id: u64) -> i64 {
|
||||
h.state().session.active_comp().unwrap().layer(LayerId(id)).unwrap().start_time.0
|
||||
}
|
||||
|
||||
/// The comp times (ticks) of a layer's Opacity keys.
|
||||
fn key_times(h: &Harness<'_, EffectcraftApp>, id: u64) -> Vec<i64> {
|
||||
let l = h.state().session.active_comp().unwrap().layer(LayerId(id)).unwrap().clone();
|
||||
l.props.prop("transform/opacity").unwrap().keys.iter().map(|k| l.comp_time(k.time).0).collect()
|
||||
}
|
||||
|
||||
fn opacity_uid(h: &Harness<'_, EffectcraftApp>, id: u64) -> u64 {
|
||||
h.state().session.active_comp().unwrap().layer(LayerId(id)).unwrap().props.prop("transform/opacity").unwrap().uid
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ctrl_alt_dragging_layer_bars_staggers_them_in_selection_order() {
|
||||
let (mut h, ids) = harness();
|
||||
let order = [ids[2], ids[0], ids[1]];
|
||||
for (i, id) in order.iter().enumerate() {
|
||||
h.state_mut().session.execute("layer.select", json!({"layers": [id], "add": i > 0})).unwrap();
|
||||
}
|
||||
h.run_steps(2);
|
||||
let before = h.state().session.project.clone();
|
||||
// Drag the middle layer's bar: the drag moves the last selected layer.
|
||||
let from = center(&h, &format!("timeline.layer.{}.bar", ids[0]));
|
||||
drag(&mut h, from, from + vec2(160.0, 0.0), ctrl_alt());
|
||||
let d = start(&h, order[2]);
|
||||
assert!(d > 0 && d % F == 0, "the last selected layer moves by whole frames: {d}");
|
||||
assert_eq!([start(&h, order[0]), start(&h, order[1])], [0, d / 2], "the first stays, the middle one moves half");
|
||||
assert_eq!(key_times(&h, order[1]), vec![d / 2, Tick::from_seconds_f64(1.0).0 + d / 2], "its keys come along");
|
||||
h.state_mut().session.execute("edit.undo", json!({})).unwrap();
|
||||
assert!(h.state().session.project.same_content(&before), "one undo step");
|
||||
// A plain drag still moves every selected layer together.
|
||||
h.run_steps(2);
|
||||
drag(&mut h, from, from + vec2(160.0, 0.0), Modifiers::default());
|
||||
let moved: Vec<i64> = order.iter().map(|id| start(&h, *id)).collect();
|
||||
assert!(moved[0] > 0 && moved.iter().all(|m| *m == moved[0]), "{moved:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ctrl_alt_dragging_keys_staggers_each_layers_keys_and_alt_still_scales() {
|
||||
let (mut h, ids) = harness();
|
||||
// Layer 1's keys first, then layer 0's, then layer 2's.
|
||||
let mut picks = vec![];
|
||||
for id in [ids[1], ids[0], ids[2]] {
|
||||
for t in [0.0, 1.0] {
|
||||
picks.push(json!({"layer": id, "path": "transform/opacity", "time": t}));
|
||||
}
|
||||
}
|
||||
for (i, k) in picks.iter().enumerate() {
|
||||
h.state_mut().session.execute("keys.select", json!({"keys": [k], "add": i > 0})).unwrap();
|
||||
}
|
||||
h.run_steps(2);
|
||||
let before = h.state().session.project.clone();
|
||||
let one = Tick::from_seconds_f64(1.0).0;
|
||||
let grab = center(&h, &format!("timeline.keyAt.{}.{one}", opacity_uid(&h, ids[0])));
|
||||
drag(&mut h, grab, grab + vec2(160.0, 0.0), ctrl_alt());
|
||||
let d = key_times(&h, ids[2])[0];
|
||||
assert!(d > 0 && d % F == 0, "the last layer's keys move by whole frames: {d}");
|
||||
assert_eq!(key_times(&h, ids[1]), vec![0, one], "the first layer's keys stay");
|
||||
assert_eq!(key_times(&h, ids[0]), vec![d / 2, one + d / 2], "the middle layer's keys move half, together");
|
||||
assert_eq!(key_times(&h, ids[2]), vec![d, one + d]);
|
||||
let layers: Vec<u64> = h.state().session.state.selected_keys.iter().map(|k| k.layer.0).collect();
|
||||
assert_eq!(layers, vec![ids[1], ids[1], ids[0], ids[0], ids[2], ids[2]], "the selection keeps its order");
|
||||
h.state_mut().session.execute("edit.undo", json!({})).unwrap();
|
||||
assert!(h.state().session.project.same_content(&before), "one undo step");
|
||||
// Alt alone still scales a key group in time: the first key stays, the last follows.
|
||||
h.state_mut().session.execute("keys.select", json!({"keys": [picks[2], picks[3]]})).unwrap();
|
||||
h.run_steps(2);
|
||||
let last = center(&h, &format!("timeline.keyAt.{}.{one}", opacity_uid(&h, ids[0])));
|
||||
drag(&mut h, last, last + vec2(160.0, 0.0), Modifiers { alt: true, ..Default::default() });
|
||||
let scaled = key_times(&h, ids[0]);
|
||||
assert!(scaled[0] == 0 && scaled[1] > one, "Alt scales: {scaled:?}");
|
||||
assert_eq!(key_times(&h, ids[1]), vec![0, one], "and staggers nothing");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_quick_offset_drag_shows_the_total_and_per_layer_offset() {
|
||||
let (mut h, ids) = harness();
|
||||
let from = center(&h, &format!("timeline.layer.{}.bar", ids[1]));
|
||||
h.input_mut().events.push(Event::PointerMoved(from));
|
||||
h.step();
|
||||
h.input_mut().events.push(Event::ModifiersChanged(ctrl_alt()));
|
||||
h.input_mut().events.push(Event::PointerButton { pos: from, button: egui::PointerButton::Primary, pressed: true, modifiers: ctrl_alt() });
|
||||
h.step();
|
||||
for i in 1..=12 {
|
||||
h.input_mut().events.push(Event::PointerMoved(from + vec2(10.0 * i as f32, 0.0)));
|
||||
h.step();
|
||||
}
|
||||
let shapes = h.output().shapes.clone();
|
||||
let text: Vec<String> = shapes
|
||||
.iter()
|
||||
.filter_map(|c| match &c.shape {
|
||||
egui::Shape::Text(t) => Some(t.galley.text().to_string()),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
assert!(text.iter().any(|t| t.starts_with("Total Offset: +") && t.contains("Per Layer: +")), "{text:?}");
|
||||
}
|
||||
@@ -118,7 +118,8 @@ one), `panel.tab.Viewer(<n>)` and `viewers.<n>` (View ▸ New Viewer's other Com
|
||||
renames it), `viewer.comp`, `viewer.handle.<layer>.<i>`,
|
||||
`timeline.layer.<id>.bar`, `timeline.layer.<id>.nestedMarker.<i>` (a precomp's comp markers), `timeline.layer.<id>.twirl`, `timeline.layer.<id>.row` (click selects,
|
||||
Enter or a double-click renames, drag reorders), `timeline.prop.<uid>.stopwatch`,
|
||||
`timeline.key.<uid>.<frame>`, `timeline.cti`, `effectControls.prop.<uid>.value` (an angle's
|
||||
`timeline.key.<uid>.<frame>` (and `timeline.keyAt.<uid>.<ticks>`, exact in layer-time ticks, for
|
||||
keys on subframes), `timeline.cti`, `effectControls.prop.<uid>.value` (an angle's
|
||||
revolutions are `timeline.prop.<uid>.revolutions`, `effectControls.prop.<uid>.revolutions` and
|
||||
`properties.prop.<uid>.revolutions`) and `effects.item.<name>`. The Composition viewer adds `viewer.magnification`, `viewer.resolution`,
|
||||
`viewer.roi`, `viewer.grid`, `viewer.channel`, `viewer.exposure`, `viewer.snapshot`,
|
||||
|
||||
Reference in New Issue
Block a user