mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-30 04:57:54 -06:00
Viewer: a mask vertex held at the panel edge pans the view (#999)
Dragging a mask or shape vertex, a tangent or a Pen point against the edge of the Composition panel now pans the view toward that edge, faster the further out the pointer is, and the point keeps moving with it, so zoomed-in rotoscoping no longer stops at the panel edge (as in After Effects). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
d26ab3b83e
commit
e2cf4477e7
@@ -1255,6 +1255,39 @@ fn phase_dragstart(app: &mut EffectcraftApp, ui: &mut egui::Ui, ectx: &EvalCtx,
|
||||
v.puppet_layers = puppet_layers;
|
||||
}
|
||||
|
||||
/// Auto-pan (#999): a mask vertex, tangent or Pen drag held at (or past) the edge of the view
|
||||
/// pans it, as in After Effects, so the point moves on past what was visible. Returns the map
|
||||
/// of the panned view.
|
||||
fn edge_pan(app: &mut EffectcraftApp, ctx: &egui::Context, g: &Gesture, map: ViewerMap, pos: Pos2) -> ViewerMap {
|
||||
if !matches!(g, Gesture::Vertices { .. } | Gesture::Tangent { .. } | Gesture::Pen { .. }) {
|
||||
return map;
|
||||
}
|
||||
let d = edge_pan_speed(map.area, pos) * ctx.input(|i| i.stable_dt).min(0.1);
|
||||
if d == egui::Vec2::ZERO {
|
||||
return map;
|
||||
}
|
||||
app.ui.viewer.pan = [app.ui.viewer.pan[0] + d.x, app.ui.viewer.pan[1] + d.y];
|
||||
ctx.request_repaint();
|
||||
ViewerMap { origin: map.origin + d, ..map }
|
||||
}
|
||||
|
||||
/// How fast (points per second, per axis) the view pans under a drag at `pos`: within 24 points
|
||||
/// of an edge of `area`, or past it, toward that edge and faster the further out.
|
||||
fn edge_pan_speed(area: Rect, pos: Pos2) -> egui::Vec2 {
|
||||
const ZONE: f32 = 24.0;
|
||||
let axis = |p: f32, lo: f32, hi: f32| {
|
||||
let depth = if p < lo + ZONE {
|
||||
lo + ZONE - p
|
||||
} else if p > hi - ZONE {
|
||||
hi - ZONE - p
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
(depth / ZONE).clamp(-3.0, 3.0) * 400.0
|
||||
};
|
||||
vec2(axis(pos.x, area.min.x, area.max.x), axis(pos.y, area.min.y, area.max.y))
|
||||
}
|
||||
|
||||
fn phase_drag(app: &mut EffectcraftApp, ui: &mut egui::Ui, ectx: &EvalCtx, resp: &egui::Response, v: &mut Vw<'_>) {
|
||||
let t = v.t;
|
||||
let zoom = v.zoom;
|
||||
@@ -1265,6 +1298,7 @@ fn phase_drag(app: &mut EffectcraftApp, ui: &mut egui::Ui, ectx: &EvalCtx, resp:
|
||||
let painter = v.painter.clone();
|
||||
let gesture: Option<Gesture> = ui.data(|d| d.get_temp(gid));
|
||||
if let (Some(g), Some(pos)) = (gesture.clone(), resp.interact_pointer_pos()) {
|
||||
let map = edge_pan(app, &ctx, &g, map, pos);
|
||||
let cpt = map.to_comp(pos);
|
||||
let merge = format!("viewer-drag-{}", ui.data(|d| d.get_temp::<u64>(egui::Id::unique("viewer-drag-n")).unwrap_or(0)));
|
||||
match g {
|
||||
|
||||
@@ -139,3 +139,15 @@ fn gizmo_segments_clip_at_the_near_plane() {
|
||||
assert!(s[1][0] > 10.0 && s[1][0].is_finite(), "toward the camera it grows, never flips: {s:?}");
|
||||
assert!(project_segment(&pc, vec3(0.0, 0.0, -200.0), vec3(0.0, 0.0, -300.0)).is_none());
|
||||
}
|
||||
|
||||
/// #999: a vertex drag pans the view only near or past the edge, toward it, faster further out.
|
||||
#[test]
|
||||
fn edge_pan_speed_moves_toward_the_edge() {
|
||||
let area = Rect::from_min_max(pos2(100.0, 50.0), pos2(900.0, 650.0));
|
||||
assert_eq!(edge_pan_speed(area, pos2(500.0, 350.0)), egui::Vec2::ZERO);
|
||||
let left = edge_pan_speed(area, pos2(105.0, 350.0));
|
||||
assert!(left.x > 0.0 && left.y == 0.0, "near the left edge the view moves right to show more: {left:?}");
|
||||
assert!(edge_pan_speed(area, pos2(20.0, 350.0)).x > left.x, "faster past the edge");
|
||||
let corner = edge_pan_speed(area, pos2(950.0, 700.0));
|
||||
assert!(corner.x < 0.0 && corner.y < 0.0, "{corner:?}");
|
||||
}
|
||||
|
||||
@@ -831,6 +831,48 @@ fn shift_drag_keeps_a_mask_vertex_to_one_axis() {
|
||||
assert!(free[0] > shifted[0] && free[1] > shifted[1], "without Shift both axes move: {shifted:?} -> {free:?}");
|
||||
}
|
||||
|
||||
/// #999: a mask vertex dragged against the edge of the Composition panel pans the view, and the
|
||||
/// vertex keeps moving while the pointer is held there, as in After Effects.
|
||||
#[test]
|
||||
fn a_vertex_held_at_the_viewer_edge_pans_the_view() {
|
||||
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 mask = {
|
||||
let s = &mut h.state_mut().session;
|
||||
s.execute("layer.select", json!({"layers": [plate]})).unwrap();
|
||||
let m = s.execute("mask.new", json!({"layer": plate, "vertices": corners, "closed": true})).unwrap()["mask"].as_u64().unwrap();
|
||||
s.execute("mask.selectVertices", json!({"vertices": [{"layer": plate, "mask": m, "index": 0}]})).unwrap();
|
||||
m
|
||||
};
|
||||
h.run_steps(2);
|
||||
let pan0 = h.state().ui.viewer.pan;
|
||||
let from = rect(&h, &format!("viewer.mask.{mask}.vertex.0")).center();
|
||||
let edge = pos2(2.0, from.y);
|
||||
h.input_mut().events.push(Event::PointerMoved(from));
|
||||
h.input_mut().events.push(Event::PointerButton { pos: from, button: egui::PointerButton::Primary, pressed: true, modifiers: Default::default() });
|
||||
h.step();
|
||||
for i in 1..=6 {
|
||||
h.input_mut().events.push(Event::PointerMoved(from + (edge - from) * (i as f32 / 6.0)));
|
||||
h.step();
|
||||
}
|
||||
let reached = mask_path(&h, "Plate").vertices[0];
|
||||
for _ in 0..20 {
|
||||
h.step();
|
||||
}
|
||||
h.input_mut().events.push(Event::PointerButton { pos: edge, button: egui::PointerButton::Primary, pressed: false, modifiers: Default::default() });
|
||||
h.run_steps(2);
|
||||
let pan = h.state().ui.viewer.pan;
|
||||
assert!(pan[0] > pan0[0] && pan[1] == pan0[1], "the view panned right to show more on the left: {pan0:?} -> {pan:?}");
|
||||
let after = mask_path(&h, "Plate").vertices[0];
|
||||
assert!(after[0] < reached[0], "the vertex kept moving left with the view: {reached:?} -> {after:?}");
|
||||
// Back inside the view, nothing pans.
|
||||
let still = h.state().ui.viewer.pan;
|
||||
let mid = rect(&h, "viewer.comp").center();
|
||||
drag(&mut h, mid, mid + vec2(5.0, 0.0));
|
||||
assert_eq!(h.state().ui.viewer.pan, still);
|
||||
}
|
||||
|
||||
/// #1769: a hidden masked layer keeps its mask path and vertices, and they remain editable.
|
||||
#[test]
|
||||
fn hidden_masked_layer_keeps_its_mask_path_editable() {
|
||||
|
||||
Reference in New Issue
Block a user