Merge pull request #269 from theironchef/fix/free-transform-modifiers
Free Transform: a handle drag follows the modifiers held during the drag
This commit is contained in:
commit
333a487dab
|
|
@ -106,7 +106,7 @@ grade by behaviour against `plan/illustrator/`, not by whether a menu item exist
|
|||
- **Drawing:** Pen, Curvature, anchor tools, Pencil, Paintbrush, Blob Brush, Smooth, Path Eraser, Join.
|
||||
- **Shapes:** all shape tools (including Flare) and the line, arc, spiral and grid tools.
|
||||
- **Cutting:** Eraser, Scissors, Knife, Mirror & Cut, Line Cut and Rectangle Cut (real geometry, compound paths keep their holes).
|
||||
- **Transform:** Rotate, Reflect, Scale, Shear (click or Alt-click snaps the reference point to anchors and centres), Reshape, Free Transform (distort/perspective).
|
||||
- **Transform:** Rotate, Reflect, Scale, Shear (click or Alt-click snaps the reference point to anchors and centres), Reshape, Free Transform (free and perspective distort and shear, switched by the keys held during a handle drag).
|
||||
- **Live Corners:** drag a live rectangle's corner widgets (Selection or Direct Selection) to round all corners, with a radius readout.
|
||||
- **Graphs:** Column, Stacked Column, Bar, Stacked Bar, Line, Area, Scatter, Pie and Radar graph tools with Graph Data and Graph Type.
|
||||
- **Other:** Eyedropper, Gradient annotator, Artboard, Measure, Type, Hand, Zoom, Rotate View.
|
||||
|
|
|
|||
|
|
@ -148,6 +148,27 @@ fn free_transform_distort_mode_via_pointer() {
|
|||
assert_eq!(s.journal.last().map(|j| j.0.as_str()), Some("object.distort"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn free_transform_cmd_held_once_a_corner_drag_started_distorts_in_one_step() {
|
||||
let mut s = session();
|
||||
let a = rect(&mut s, 100.0, 100.0, 100.0, 100.0);
|
||||
let v = ViewInfo::default();
|
||||
s.select_tool("freeTransform", v).unwrap();
|
||||
let before = undo_len(&s);
|
||||
let cmd = Mods { cmd: true, ..Default::default() };
|
||||
s.pointer(&PointerEvent::new(PointerKind::Down, 200.0, 200.0), v).unwrap();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Drag, 210.0, 210.0), v).unwrap();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Drag, 250.0, 260.0).with_mods(cmd), v).unwrap();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Up, 250.0, 260.0).with_mods(cmd), v).unwrap();
|
||||
let d = &s.doc().unwrap().doc;
|
||||
let pts: Vec<Point> = d.node(a).unwrap().path_data().unwrap().anchors().map(|(_, _, an)| an.p).collect();
|
||||
for p in [Point::new(250.0, 260.0), Point::new(100.0, 100.0), Point::new(200.0, 100.0), Point::new(100.0, 200.0)] {
|
||||
assert!(pts.iter().any(|q| q.distance(p) < 1e-6), "{p:?} not in {pts:?}");
|
||||
}
|
||||
assert_eq!(undo_len(&s), before + 1);
|
||||
assert_eq!(s.doc().unwrap().history.undo.last().map(|e| e.label.as_str()), Some("Distort"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn free_transform_side_handle_scales() {
|
||||
let mut s = session();
|
||||
|
|
|
|||
|
|
@ -44,6 +44,27 @@ enum Op {
|
|||
Corners(Handle, DistortMode),
|
||||
}
|
||||
|
||||
impl Op {
|
||||
/// The bounding-box handle being dragged, if any.
|
||||
fn handle(self) -> Option<Handle> {
|
||||
match self {
|
||||
Op::Scale(h) | Op::Shear(h) | Op::Corners(h, _) => Some(h),
|
||||
Op::Move | Op::Rotate => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The undo step's name.
|
||||
fn label(self) -> &'static str {
|
||||
match self {
|
||||
Op::Move => "Move",
|
||||
Op::Scale(_) => "Scale",
|
||||
Op::Shear(_) => "Shear",
|
||||
Op::Rotate => "Rotate",
|
||||
Op::Corners(..) => "Distort",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
struct Drag {
|
||||
op: Op,
|
||||
|
|
@ -125,9 +146,9 @@ pub fn distort_quad(r: Rect, h: Handle, d: Vec2, mode: DistortMode) -> [Point; 4
|
|||
}
|
||||
|
||||
impl FreeTransformTool {
|
||||
fn classify(&self, cx: &ToolContext, r: Rect, p: Point, m: Mods) -> Option<Op> {
|
||||
let tol = cx.tol(5.0);
|
||||
if let Some(h) = hit_handle(r, p, tol) {
|
||||
/// What dragging handle `h` with modifiers `m` does: Cmd on a corner distorts it freely,
|
||||
/// Cmd+Alt+Shift in perspective, Cmd on a side shears; otherwise the tool's mode.
|
||||
fn handle_op(&self, h: Handle, m: Mods) -> Op {
|
||||
let mode = if h.is_corner() && m.cmd && m.alt && m.shift {
|
||||
DistortMode::Perspective
|
||||
} else if h.is_corner() && m.cmd {
|
||||
|
|
@ -135,12 +156,18 @@ impl FreeTransformTool {
|
|||
} else {
|
||||
self.mode
|
||||
};
|
||||
return Some(match mode {
|
||||
match mode {
|
||||
DistortMode::Free if !h.is_corner() && m.cmd => Op::Shear(h),
|
||||
DistortMode::Free => Op::Scale(h),
|
||||
DistortMode::Perspective if !h.is_corner() => Op::Scale(h),
|
||||
mode => Op::Corners(h, mode),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn classify(&self, cx: &ToolContext, r: Rect, p: Point, m: Mods) -> Option<Op> {
|
||||
let tol = cx.tol(5.0);
|
||||
if let Some(h) = hit_handle(r, p, tol) {
|
||||
return Some(self.handle_op(h, m));
|
||||
}
|
||||
if self.mode == DistortMode::Free && in_rotate_zone(r, p, tol, cx.tol(18.0)).is_some() {
|
||||
return Some(Op::Rotate);
|
||||
|
|
@ -205,19 +232,24 @@ impl Tool for FreeTransformTool {
|
|||
PointerKind::Drag => {
|
||||
let Some(mut d) = self.drag else { return vec![] };
|
||||
let mut out = vec![];
|
||||
// A handle drag follows the modifiers held now: the gesture is to start dragging
|
||||
// and then hold Cmd (a Cmd press on the canvas is the temporary Selection tool).
|
||||
if let Some(h) = d.op.handle() {
|
||||
let op = self.handle_op(h, ev.mods);
|
||||
if d.began && op.label() != d.op.label() {
|
||||
// Another operation: it gets an undo step of its own.
|
||||
out.push(Action::Cancel);
|
||||
out.push(Action::Begin(op.label().into()));
|
||||
}
|
||||
d.op = op;
|
||||
}
|
||||
if !d.began {
|
||||
if p.distance(d.start) < cx.tol(2.0) {
|
||||
self.drag = Some(d);
|
||||
return out;
|
||||
}
|
||||
d.began = true;
|
||||
let label = match d.op {
|
||||
Op::Move => "Move",
|
||||
Op::Scale(_) => "Scale",
|
||||
Op::Shear(_) => "Shear",
|
||||
Op::Rotate => "Rotate",
|
||||
Op::Corners(..) => "Distort",
|
||||
};
|
||||
out.push(Action::Begin(label.into()));
|
||||
out.push(Action::Begin(d.op.label().into()));
|
||||
}
|
||||
self.drag = Some(d);
|
||||
out.push(self.preview(cx, d, p, ev.mods));
|
||||
|
|
@ -353,6 +385,37 @@ mod tests {
|
|||
assert!((m * Point::new(100.0, 100.0)).distance(Point::new(120.0, 100.0)) < 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn modifiers_pressed_during_a_handle_drag_change_what_it_does() {
|
||||
let (d, id) = doc_with_rect();
|
||||
let mut s = Selection::default();
|
||||
s.add(id);
|
||||
let p = paint();
|
||||
let cx = cx(&d, &s, &p);
|
||||
let mut t = FreeTransformTool::default();
|
||||
let cmd = Mods { cmd: true, ..Default::default() };
|
||||
let perspective = Mods { cmd: true, alt: true, shift: true, ..Default::default() };
|
||||
// Start dragging the bottom-right corner, then hold Cmd: Free Distort, a step of its own.
|
||||
t.pointer(&cx, &ev(PointerKind::Down, 200.0, 200.0));
|
||||
let a = t.pointer(&cx, &ev(PointerKind::Drag, 210.0, 210.0));
|
||||
assert_eq!(a[0], Action::Begin("Scale".into()));
|
||||
let a = t.pointer(&cx, &ev(PointerKind::Drag, 220.0, 230.0).with_mods(cmd));
|
||||
let corners = json!({"corners": [[100.0, 100.0], [200.0, 100.0], [220.0, 230.0], [100.0, 200.0]], "from": [100.0, 100.0, 200.0, 200.0]});
|
||||
assert_eq!(a, vec![Action::Cancel, Action::Begin("Distort".into()), Action::Preview("object.distort".into(), corners)]);
|
||||
// Cmd+Alt+Shift: Perspective Distort, still the same step.
|
||||
let a = t.pointer(&cx, &ev(PointerKind::Drag, 220.0, 230.0).with_mods(perspective));
|
||||
assert!(matches!(&a[..], [Action::Preview(c, _)] if c == "object.distort"), "{a:?}");
|
||||
// Letting go of the keys scales again.
|
||||
let a = t.pointer(&cx, &ev(PointerKind::Drag, 220.0, 230.0));
|
||||
assert_eq!(a[..2], [Action::Cancel, Action::Begin("Scale".into())]);
|
||||
assert_eq!(t.pointer(&cx, &ev(PointerKind::Up, 220.0, 230.0)), vec![Action::Commit]);
|
||||
// Cmd held during a side handle's drag shears.
|
||||
t.pointer(&cx, &ev(PointerKind::Down, 150.0, 100.0));
|
||||
t.pointer(&cx, &ev(PointerKind::Drag, 155.0, 100.0));
|
||||
let a = t.pointer(&cx, &ev(PointerKind::Drag, 170.0, 100.0).with_mods(cmd));
|
||||
assert_eq!(a[..2], [Action::Cancel, Action::Begin("Shear".into())]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn distort_modes_emit_corners() {
|
||||
let (d, id) = doc_with_rect();
|
||||
|
|
|
|||
Loading…
Reference in New Issue