Merge pull request #1476 from itrycodinganditsbad/fix-1393
Keep transform handles in reach when the picture is past the monitor edge
This commit is contained in:
commit
4678c68d8e
|
|
@ -43,6 +43,9 @@ const BADGE_ARM: f32 = 5.0;
|
|||
const ROTATE_REACH: f32 = 24.0;
|
||||
/// The `merge` key of a drag's changes: one undo step, whichever parameters the drag changes.
|
||||
const MERGE: &str = "programTransform";
|
||||
/// How far inside the monitor's clip (which reaches 12 points past the picture area) a handle
|
||||
/// that would be off the monitor stops, so that all of it shows and can be grabbed.
|
||||
const REACH_INSET: f32 = 12.0 + CORNER;
|
||||
|
||||
/// What a press on the box takes hold of.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
|
|
@ -64,6 +67,9 @@ struct Placement {
|
|||
to_screen: Affine,
|
||||
/// The picture's box: TL, TR, BR, BL.
|
||||
quad: [Pos2; 4],
|
||||
/// Where the eight handles (see [`handle_points`]) are drawn and grabbed: on the box, or at
|
||||
/// the monitor's edge for those beyond it (see [`within`]).
|
||||
handles: [Pos2; 8],
|
||||
anchor_at: Pos2,
|
||||
position: Vec2,
|
||||
anchor: Vec2,
|
||||
|
|
@ -117,9 +123,19 @@ fn inside(quad: &[Pos2; 4], p: Pos2) -> bool {
|
|||
pos != neg
|
||||
}
|
||||
|
||||
/// The handles at `points` pulled into `view`, the part of the monitor on screen: a picture moved
|
||||
/// past the monitor's edge keeps every handle in reach (#1393). The picture stays where it is;
|
||||
/// only the squares stop at the edge. A `view` with no room leaves them where they are.
|
||||
fn within(points: [Pos2; 8], view: Rect) -> [Pos2; 8] {
|
||||
if !(view.is_finite() && view.is_positive()) {
|
||||
return points;
|
||||
}
|
||||
points.map(|p| pos2(p.x.max(view.min.x).min(view.max.x), p.y.max(view.min.y).min(view.max.y)))
|
||||
}
|
||||
|
||||
/// The box of effect `e` (Motion or Transform) of clip `it` at media time `mt`, on the monitor
|
||||
/// picture `pic` showing a `frame`-sized sequence.
|
||||
fn placement(app: &FilmcraftApp, it: &TrackItem, e: &EffectInstance, mt: Tick, pic: Rect, frame: (u32, u32)) -> Option<Placement> {
|
||||
/// picture `pic` showing a `frame`-sized sequence, with its handles kept inside `reach`.
|
||||
fn placement(app: &FilmcraftApp, it: &TrackItem, e: &EffectInstance, mt: Tick, pic: Rect, reach: Rect, frame: (u32, u32)) -> Option<Placement> {
|
||||
let seq = app.session.active_sequence()?;
|
||||
let project = &app.session.project;
|
||||
let size = filmcraft_render::source_size(project, it.item).unwrap_or(frame);
|
||||
|
|
@ -156,6 +172,7 @@ fn placement(app: &FilmcraftApp, it: &TrackItem, e: &EffectInstance, mt: Tick, p
|
|||
outer,
|
||||
to_screen,
|
||||
quad,
|
||||
handles: within(handle_points(&quad), reach),
|
||||
anchor_at,
|
||||
position,
|
||||
anchor,
|
||||
|
|
@ -168,7 +185,7 @@ fn placement(app: &FilmcraftApp, it: &TrackItem, e: &EffectInstance, mt: Tick, p
|
|||
|
||||
/// The selected effect (its clip and index) and its box, while it is Motion or Transform of a
|
||||
/// selected clip under the playhead.
|
||||
fn selected_placement(app: &FilmcraftApp, pic: Rect, frame: (u32, u32)) -> Option<(Target, &'static EffectDef, Placement)> {
|
||||
fn selected_placement(app: &FilmcraftApp, pic: Rect, reach: Rect, frame: (u32, u32)) -> Option<(Target, &'static EffectDef, Placement)> {
|
||||
let sel = app.session.state.selected_effect.as_ref()?;
|
||||
if !app.session.state.selection.contains(&sel.clip) {
|
||||
return None;
|
||||
|
|
@ -182,7 +199,7 @@ fn selected_placement(app: &FilmcraftApp, pic: Rect, frame: (u32, u32)) -> Optio
|
|||
let index = sel.index(&it.effects)?;
|
||||
let e = it.effects.get(index).filter(|e| e.enabled)?;
|
||||
let def = e.def().filter(|d| d.id == "motion" || d.id == "transform")?;
|
||||
let pl = placement(app, it, e, it.source_time_at(ph), pic, frame)?;
|
||||
let pl = placement(app, it, e, it.source_time_at(ph), pic, reach, frame)?;
|
||||
Some(((sel.clip, index), def, pl))
|
||||
}
|
||||
|
||||
|
|
@ -190,7 +207,7 @@ fn grab_at(pl: &Placement, p: Pos2) -> Option<Grab> {
|
|||
if (pl.anchor_at - p).length() <= ANCHOR_GRAB {
|
||||
return Some(Grab::Anchor);
|
||||
}
|
||||
if let Some(n) = handle_points(&pl.quad).iter().position(|h| (*h - p).length() <= GRAB) {
|
||||
if let Some(n) = pl.handles.iter().position(|h| (*h - p).length() <= GRAB) {
|
||||
return Some(Grab::Handle(n));
|
||||
}
|
||||
if inside(&pl.quad, p) {
|
||||
|
|
@ -282,24 +299,26 @@ fn angle_deg(v: egui::Vec2) -> f64 {
|
|||
f64::from(v.y.atan2(v.x).to_degrees())
|
||||
}
|
||||
|
||||
fn draw(app: &mut FilmcraftApp, ui: &egui::Ui, pl: &Placement, pic: Rect, name: &str) {
|
||||
/// Draws the box over the whole monitor (the ui's clip), not just the picture, so its handles show
|
||||
/// beside the frame as well as on it.
|
||||
fn draw(app: &mut FilmcraftApp, ui: &egui::Ui, pl: &Placement, name: &str) {
|
||||
let accent = app.tokens.accent;
|
||||
let painter = ui.painter().with_clip_rect(pic.expand(12.0));
|
||||
let painter = ui.painter();
|
||||
painter.add(egui::Shape::closed_line(pl.quad.to_vec(), Stroke::new(1.0, accent)));
|
||||
app.auto.add("program.transform.box", Rect::from_points(&pl.quad), name);
|
||||
let mid = centre(&pl.quad);
|
||||
// automation rects: the square inside the circle a press grabs in
|
||||
let grab = vec2(GRAB, GRAB) * std::f32::consts::SQRT_2;
|
||||
for (n, c) in pl.quad.iter().enumerate() {
|
||||
for (n, (q, c)) in pl.quad.iter().zip(&pl.handles).enumerate() {
|
||||
let hr = Rect::from_center_size(*c, vec2(CORNER, CORNER));
|
||||
painter.rect_filled(hr, 0.0, Color32::WHITE);
|
||||
painter.rect_stroke(hr, 0.0, Stroke::new(1.0, accent), StrokeKind::Middle);
|
||||
app.auto.add(&format!("program.transform.handle.{n}"), Rect::from_center_size(*c, grab), "scale handle");
|
||||
// just outside the corner, away from the box's centre
|
||||
let out = (*c - mid).normalized();
|
||||
app.auto.add(&format!("program.transform.rotate.{n}"), Rect::from_center_size(*c + out * 14.0, vec2(10.0, 10.0)), "rotate");
|
||||
let out = (*q - mid).normalized();
|
||||
app.auto.add(&format!("program.transform.rotate.{n}"), Rect::from_center_size(*q + out * 14.0, vec2(10.0, 10.0)), "rotate");
|
||||
}
|
||||
for (n, m) in handle_points(&pl.quad).iter().enumerate().skip(4) {
|
||||
for (n, m) in pl.handles.iter().enumerate().skip(4) {
|
||||
painter.rect_filled(Rect::from_center_size(*m, vec2(EDGE, EDGE)), 0.0, Color32::WHITE);
|
||||
app.auto.add(&format!("program.transform.handle.{n}"), Rect::from_center_size(*m, grab), "scale handle");
|
||||
}
|
||||
|
|
@ -382,10 +401,14 @@ pub fn monitor_overlay(app: &mut FilmcraftApp, ui: &mut egui::Ui, pic: Rect, fra
|
|||
let id = egui::Id::new("program-transform");
|
||||
let drag_id = id.with("drag");
|
||||
let double = ui.input(|i| i.pointer.button_double_clicked(egui::PointerButton::Primary));
|
||||
if let Some((sel, def, pl)) = selected_placement(app, pic, frame) {
|
||||
draw(app, ui, &pl, pic, def.name);
|
||||
// (as far again beyond the rotate zone, where a click lets go of the box)
|
||||
let resp = ui.interact(Rect::from_points(&pl.quad).expand(ROTATE_REACH * 2.0), id, Sense::click_and_drag());
|
||||
// the monitor around the picture as well as on it (its clip reaches past the picture area)
|
||||
let reach = ui.clip_rect().shrink(REACH_INSET);
|
||||
if let Some((sel, def, pl)) = selected_placement(app, pic, reach, frame) {
|
||||
draw(app, ui, &pl, def.name);
|
||||
// (as far again beyond the rotate zone, where a click lets go of the box; and the handles
|
||||
// held at the monitor's edge)
|
||||
let area = Rect::from_points(&pl.quad).union(Rect::from_points(&pl.handles));
|
||||
let resp = ui.interact(area.expand(ROTATE_REACH * 2.0), id, Sense::click_and_drag());
|
||||
// the left button only: a middle or right drag leaves the picture alone
|
||||
let left = egui::PointerButton::Primary;
|
||||
if resp.drag_started_by(left) {
|
||||
|
|
@ -442,3 +465,27 @@ pub fn monitor_overlay(app: &mut FilmcraftApp, ui: &mut egui::Ui, pic: Rect, fra
|
|||
}
|
||||
crate::panels::effect_controls::run(app, ui.ctx(), actions);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn handles_past_the_monitor_stop_at_its_edge() {
|
||||
let view = Rect::from_min_max(pos2(0.0, 0.0), pos2(100.0, 50.0));
|
||||
// a box from the middle of the view to well past its bottom-right corner
|
||||
let quad = [pos2(50.0, 25.0), pos2(150.0, 25.0), pos2(150.0, 75.0), pos2(50.0, 75.0)];
|
||||
let h = within(handle_points(&quad), view);
|
||||
assert_eq!(h[0], pos2(50.0, 25.0), "a handle on the monitor stays on the box");
|
||||
assert_eq!(h[1], pos2(100.0, 25.0));
|
||||
assert_eq!(h[2], pos2(100.0, 50.0));
|
||||
assert_eq!(h[3], pos2(50.0, 50.0));
|
||||
assert_eq!(h[5], pos2(100.0, 50.0));
|
||||
assert!(h.iter().all(|p| view.contains(*p)));
|
||||
// no room to keep them in: left where they are, without crossed bounds
|
||||
let none = Rect::from_min_max(pos2(10.0, 10.0), pos2(0.0, 0.0));
|
||||
assert_eq!(within(handle_points(&quad), none), handle_points(&quad));
|
||||
let nan = Rect::from_min_max(pos2(f32::NAN, 0.0), pos2(100.0, 50.0));
|
||||
assert_eq!(within(handle_points(&quad), nan), handle_points(&quad));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -441,6 +441,30 @@ fn guides_masks_and_graphics_take_the_left_button_only() {
|
|||
d.drag_from(at, 40.0, 20.0);
|
||||
assert_ne!(project(&d), before, "the left button still moves the layer");
|
||||
}
|
||||
/// A picture moved past the edge of the frame (and the monitor) keeps its handles in reach: the
|
||||
/// ones that would be off the monitor stop at its edge, and dragging them still scales (#1393).
|
||||
#[test]
|
||||
fn handles_stay_in_reach_when_the_picture_is_past_the_edge() {
|
||||
let mut d = Driver::demo();
|
||||
let (clip, _) = d.first_clip();
|
||||
// centred on the frame's bottom-right corner: the right and bottom handles are beyond it
|
||||
d.exec("effects.setParam", json!({"clip": clip, "effect": "motion", "param": "position", "value": [1920.0, 1080.0]}));
|
||||
d.click("effectControls.effect.motion");
|
||||
let pic = d.rect("program.picture");
|
||||
let (cx, cy) = d.center("program.transform.handle.0");
|
||||
assert!(near((cx, cy), (pic[0] + pic[2] / 2.0, pic[1] + pic[3] / 2.0)), "the top-left corner is at the frame's centre");
|
||||
let (hx, hy) = d.center("program.transform.handle.2");
|
||||
assert!(hx < 1600.0 && hy < 980.0, "the bottom-right handle is on screen: ({hx}, {hy})");
|
||||
// (its true place is about 280 points right of the picture, past the monitor)
|
||||
assert!(hx <= pic[0] + pic[2] + 40.0 && hy <= pic[1] + pic[3] + 40.0, "and on the monitor: ({hx}, {hy}), picture {pic:?}");
|
||||
let p0 = d.point(clip, "motion", "position");
|
||||
d.drag("program.transform.handle.2", -30.0, -30.0);
|
||||
let s = d.num(clip, "motion", "scale");
|
||||
assert!(s < 95.0, "the bottom-right handle scales: {s}");
|
||||
assert_eq!(d.point(clip, "motion", "position"), p0, "the picture is not moved");
|
||||
d.shot("transform-past-the-edge");
|
||||
}
|
||||
|
||||
/// Anywhere just outside the box rotates, beside the middle of an edge as beside a corner.
|
||||
#[test]
|
||||
fn rotating_from_beside_an_edge() {
|
||||
|
|
|
|||
Loading…
Reference in New Issue