mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-24 10:09:32 -06:00
Merge pull request #1649
This commit is contained in:
@@ -255,7 +255,7 @@ fn reveal_targets(app: &EffectcraftApp) -> Vec<u64> {
|
||||
/// `down`, `left`, `right`), 10 with Shift (`up10`…), as After Effects does, so zoomed in
|
||||
/// they move by fractions of a comp pixel. From any panel except Project (which owns the
|
||||
/// arrow keys for its own keyboard navigation, #961); not while typing, with a dialog or
|
||||
/// menu open, or with mask points selected.
|
||||
/// menu open. With mask points selected the arrows move the points instead (#1288).
|
||||
fn nudge(app: &mut EffectcraftApp, ctx: &egui::Context, dir: &str) -> Result<Value, String> {
|
||||
let (side, n) = match dir.strip_suffix("10") {
|
||||
Some(side) => (side, 10.0),
|
||||
@@ -269,15 +269,34 @@ fn nudge(app: &mut EffectcraftApp, ctx: &egui::Context, dir: &str) -> Result<Val
|
||||
_ => return Err(format!("unknown nudge `{dir}`")),
|
||||
};
|
||||
let st = &app.session.state;
|
||||
if app.ui.focused == PanelKind::Project || st.selected_layers.is_empty() || st.text_edit.is_some() || !st.selected_vertices.is_empty() {
|
||||
if app.ui.focused == PanelKind::Project || (st.selected_layers.is_empty() && st.selected_vertices.is_empty()) || st.text_edit.is_some() {
|
||||
return Ok(Value::Null);
|
||||
}
|
||||
// Screen pixels per comp pixel.
|
||||
let zoom = (app.ui.viewer.zoom.unwrap_or_else(|| crate::panels::viewer::last_fit(ctx)) * ctx.pixels_per_point()) as f64;
|
||||
let k = if zoom.is_finite() && zoom > 1e-6 { 1.0 / zoom } else { 1.0 };
|
||||
if let Some(layer) = st.selected_vertices.first().map(|v| v.layer) {
|
||||
let d = vertex_delta(app, layer, [x * k, y * k]);
|
||||
return run_engine(app, ctx, "mask.moveVertices", json!({"delta": d}));
|
||||
}
|
||||
run_engine(app, ctx, "layer.nudge", json!({"x": x * k, "y": y * k}))
|
||||
}
|
||||
|
||||
/// A comp-space step `d` in the layer space of `layer`, where mask points live. Like a drag of
|
||||
/// the points in the viewer, every selected point takes the first one's layer space.
|
||||
fn vertex_delta(app: &EffectcraftApp, layer: effectcraft_engine::project::LayerId, d: [f64; 2]) -> [f64; 2] {
|
||||
use effectcraft_engine::geom::vec2;
|
||||
let s = &app.session;
|
||||
let (Some(cid), Some(comp)) = (s.active_comp_id(), s.active_comp()) else { return d };
|
||||
let Some(l) = comp.layer(layer) else { return d };
|
||||
let ectx = effectcraft_engine::render::EvalCtx { project: &s.project, comp_id: cid, comp, time: s.time(), expr: s.expr.as_deref(), footage: None };
|
||||
let (l2c, _) = crate::panels::viewer::l2c(&ectx, l);
|
||||
l2c.inverse().map_or(d, |inv| {
|
||||
let v = inv.apply_vec(vec2(d[0], d[1]));
|
||||
[v.x, v.y]
|
||||
})
|
||||
}
|
||||
|
||||
fn no_params(p: &Value) -> bool {
|
||||
p.as_object().is_none_or(|m| m.is_empty())
|
||||
}
|
||||
|
||||
@@ -2097,6 +2097,37 @@ fn arrow_keys_nudge_the_selected_layer_at_the_viewer_magnification() {
|
||||
assert_eq!(pos(&h), [100.5, 109.5, 0.0], "sub-pixel steps when zoomed in");
|
||||
}
|
||||
|
||||
/// #1288: with mask points selected the arrow keys move the points, not the layer: 1 pixel at the
|
||||
/// viewer's magnification in comp space (half a layer pixel on a layer scaled to 200 %), 10 with
|
||||
/// Shift, one undo step each.
|
||||
#[test]
|
||||
fn arrow_keys_nudge_the_selected_mask_points() {
|
||||
let mut h = harness();
|
||||
let plate = layer_id(&h, "Plate");
|
||||
let corners = [[100.0, 60.0], [500.0, 60.0], [500.0, 300.0], [100.0, 300.0]];
|
||||
let s = &mut h.state_mut().session;
|
||||
s.execute("layer.select", json!({"layers": [plate]})).unwrap();
|
||||
s.execute("prop.set", json!({"layer": plate, "path": "transform/scale", "value": [200, 200, 100]})).unwrap();
|
||||
s.execute("mask.new", json!({"layer": plate, "vertices": corners, "closed": true})).unwrap();
|
||||
let mask = s.active_comp().unwrap().layer(LayerId(plate)).unwrap().masks().unwrap().groups().next().unwrap().uid;
|
||||
s.execute("mask.selectVertices", json!({"vertices": [{"layer": plate, "mask": mask, "index": 1}, {"layer": plate, "mask": mask, "index": 2}]})).unwrap();
|
||||
let ppp = h.ctx.pixels_per_point();
|
||||
h.state_mut().ui.viewer.zoom = Some(1.0 / ppp);
|
||||
h.run_steps(2);
|
||||
let pos = |h: &Harness<'_, EffectcraftApp>| {
|
||||
h.state().session.active_comp().unwrap().layer(LayerId(plate)).unwrap().props.prop("transform/position").unwrap().value.as_vec3()
|
||||
};
|
||||
let (pos0, undo) = (pos(&h), h.state().session.history.undo.len());
|
||||
key(&mut h, egui::Key::ArrowRight, egui::Modifiers::NONE);
|
||||
key(&mut h, egui::Key::ArrowDown, egui::Modifiers::SHIFT);
|
||||
let path = mask_path(&h, "Plate");
|
||||
let near = |a: [f64; 2], b: [f64; 2]| (a[0] - b[0]).abs() < 1e-6 && (a[1] - b[1]).abs() < 1e-6;
|
||||
assert!(near(path.vertices[1], [500.5, 65.0]) && near(path.vertices[2], [500.5, 305.0]), "{:?}", path.vertices);
|
||||
assert_eq!(path.vertices[0], corners[0], "unselected points stay");
|
||||
assert_eq!(pos(&h), pos0, "the layer stays");
|
||||
assert_eq!(h.state().session.history.undo.len(), undo + 2, "one undo step per press");
|
||||
}
|
||||
|
||||
/// A comp wider than the GPU's texture limit at Full resolution shows its frame (averaged down
|
||||
/// into a texture the renderer accepts) instead of failing the upload (#201: 11000×2200).
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user