mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-29 20:27:05 -06:00
Tools: Escape cancels an Anchor Point, Mesh, Shape Builder or Measure drag
Escape did not cancel a drag with the Anchor Point, Mesh, Shape Builder and Measure tools, or a Direct Selection drag of a mesh point, a corner widget or the brackets of type on a path: the drag went on until the mouse was released. Escape cancels every other Direct Selection drag and a drag with the Free Transform, Rotate, Reflect, Scale, Shear and Gradient tools. Escape now cancels an Anchor Point drag, a Mesh tool drag of a mesh point or handle, and a Direct Selection drag of a mesh point or handle, a corner widget or the brackets of type on a path. The art goes back to how the press found it, no undo step is added, and the rest of the drag and the release send nothing. Escape during a Shape Builder drag ends it, so the release merges and deletes nothing; the tool takes Escape only during the drag. Escape during a Measure tool drag ends it and brings back the measurement shown before the press. Escape after an Anchor Point or Direct Selection press that has not moved yet drops the press. Otherwise, outside a drag, Escape does what it did before. Docs: docs/mcp.md, docs/target-app-parity.md.
This commit is contained in:
@@ -142,8 +142,8 @@ fn step_swatch(list: &[&Swatch], fill: &Paint, last: Option<usize>, forward: boo
|
||||
/// red, and the gesture deletes the regions it touches, the pieces of open paths and the edges
|
||||
/// (outline pieces between the points where paths meet) it passes over. With Shift held at the
|
||||
/// press, the drag draws a rectangular marquee and touches the regions it reaches (see
|
||||
/// [`BuilderMap::regions_in_marquee`]): they merge, or with Alt they are deleted. Its options are
|
||||
/// [`ShapeBuilderOptions`].
|
||||
/// [`BuilderMap::regions_in_marquee`]): they merge, or with Alt they are deleted. Escape during a
|
||||
/// drag cancels it. Its options are [`ShapeBuilderOptions`].
|
||||
#[derive(Default)]
|
||||
pub struct ShapeBuilderTool {
|
||||
cache: RegionCache,
|
||||
@@ -461,15 +461,23 @@ impl Tool for ShapeBuilderTool {
|
||||
}
|
||||
|
||||
/// With Pick Color From Color Swatches, the Left and Right arrow keys pick the color swatch
|
||||
/// before or after the current fill.
|
||||
/// before or after the current fill. Escape during a drag cancels it: the release after it
|
||||
/// merges and deletes nothing.
|
||||
fn claims_key(&self, _cx: &ToolContext, key: ToolKey) -> bool {
|
||||
self.opts.swatches && !self.dragging && matches!(key, ToolKey::Left | ToolKey::Right)
|
||||
(self.opts.swatches && !self.dragging && matches!(key, ToolKey::Left | ToolKey::Right)) || (self.dragging && key == ToolKey::Escape)
|
||||
}
|
||||
|
||||
fn key(&mut self, cx: &ToolContext, key: ToolKey, _mods: Mods) -> Vec<Action> {
|
||||
if !self.claims_key(cx, key) {
|
||||
return vec![];
|
||||
}
|
||||
if key == ToolKey::Escape {
|
||||
self.dragging = false;
|
||||
self.points.clear();
|
||||
self.touched = Touched::default();
|
||||
(self.marquee, self.hover, self.far) = (None, None, false);
|
||||
return vec![];
|
||||
}
|
||||
let list = color_swatches(cx.doc);
|
||||
let Some((i, s)) = step_swatch(&list, &cx.paint.fill, self.picked, key == ToolKey::Right).and_then(|i| Some((i, list.get(i)?))) else {
|
||||
return vec![];
|
||||
|
||||
@@ -60,6 +60,40 @@ fn shape_builder_drag_emits_merge() {
|
||||
assert!(v["points"].as_array().unwrap().len() >= 2);
|
||||
}
|
||||
|
||||
/// Escape during a merge drag, an Alt erase drag or a Shift marquee cancels it: the tool takes
|
||||
/// the key, nothing stays highlighted, and the release merges and deletes nothing. Enter leaves
|
||||
/// the drag alone, and with no drag Escape is not the tool's.
|
||||
#[test]
|
||||
fn escape_cancels_a_shape_builder_drag() {
|
||||
let (d, a, b) = two_rects();
|
||||
let mut s = Selection::default();
|
||||
s.set([a, b]);
|
||||
let p = paint();
|
||||
let cx = cx(&d, &s, &p);
|
||||
let alt = Mods { alt: true, ..Mods::default() };
|
||||
let shift = Mods { shift: true, ..Mods::default() };
|
||||
for (what, mods) in [("merge", Mods::default()), ("erase", alt), ("marquee", shift)] {
|
||||
let mut t = ShapeBuilderTool::default();
|
||||
t.pointer(&cx, &ev(PointerKind::Down, 25.0, 50.0).with_mods(mods));
|
||||
t.pointer(&cx, &ev(PointerKind::Drag, 75.0, 60.0).with_mods(mods));
|
||||
assert!(!t.touched.is_empty(), "{what}");
|
||||
assert!(t.key(&cx, ToolKey::Enter, Mods::default()).is_empty(), "{what}: Enter leaves the drag alone");
|
||||
assert!(t.busy(), "{what}");
|
||||
assert!(t.claims_key(&cx, ToolKey::Escape), "{what}: Escape is the tool's during the drag");
|
||||
assert!(t.key(&cx, ToolKey::Escape, Mods::default()).is_empty(), "{what}: no interaction began, so none is canceled");
|
||||
assert!(!t.busy(), "{what}");
|
||||
assert!(t.overlays(&cx).is_empty(), "{what}: nothing stays highlighted");
|
||||
assert!(t.pointer(&cx, &ev(PointerKind::Drag, 125.0, 70.0).with_mods(mods)).is_empty(), "{what}");
|
||||
assert!(t.pointer(&cx, &ev(PointerKind::Up, 125.0, 70.0).with_mods(mods)).is_empty(), "{what}: the release merges nothing");
|
||||
assert!(!t.claims_key(&cx, ToolKey::Escape), "{what}: with no drag, Escape is not the tool's");
|
||||
// The next drag merges as before.
|
||||
t.pointer(&cx, &ev(PointerKind::Down, 25.0, 50.0));
|
||||
t.pointer(&cx, &ev(PointerKind::Drag, 75.0, 50.0));
|
||||
let acts = t.pointer(&cx, &ev(PointerKind::Up, 75.0, 50.0));
|
||||
assert!(matches!(acts.as_slice(), [Action::Exec(id, _)] if id == "shapeBuilder.merge"), "{what}: {acts:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shape_builder_rebuilds_its_map_for_a_new_revision_at_the_same_address() {
|
||||
let (mut d, a, b) = two_rects();
|
||||
|
||||
@@ -453,8 +453,9 @@ impl Tool for DirectSelectionTool {
|
||||
fn options(&self) -> Value {
|
||||
self.mesh.options()
|
||||
}
|
||||
/// Escape during a drag (anchors, handles, segments, objects, type areas, blend spines, a
|
||||
/// marquee) cancels it: what it moved goes back and the tool waits for the next press (#1743).
|
||||
/// Escape during a drag (anchors, handles, segments, objects, type areas, blend spines, mesh
|
||||
/// points, corner widgets, the brackets of type on a path, a marquee) cancels it: what it moved
|
||||
/// goes back and the tool waits for the next press (#1743).
|
||||
/// With Group Selection, E during a marquee drag makes the marquee select only the objects
|
||||
/// entirely inside it, whole, and E pressed again makes it select what it selects by default.
|
||||
fn key(&mut self, _cx: &ToolContext, key: ToolKey, _mods: Mods) -> Vec<Action> {
|
||||
@@ -467,9 +468,11 @@ impl Tool for DirectSelectionTool {
|
||||
if key == ToolKey::Escape && self.guide.busy() {
|
||||
return self.guide.cancel();
|
||||
}
|
||||
if key != ToolKey::Escape || matches!(self.state, State::Idle | State::Mesh | State::Corner(_) | State::Bracket(_)) {
|
||||
if key != ToolKey::Escape || matches!(self.state, State::Idle) {
|
||||
return vec![];
|
||||
}
|
||||
// A mesh point drag ends here too; the `Action::Cancel` below undoes it.
|
||||
self.mesh.cancel();
|
||||
self.state = State::Idle;
|
||||
self.guides.clear();
|
||||
(self.anchor_snap, self.copy_snap, self.move_snap) = (None, None, None);
|
||||
|
||||
@@ -661,6 +661,41 @@ fn escape_cancels_a_drag() {
|
||||
assert_eq!(t.key(&cx, ToolKey::Escape, Mods::default()), vec![], "nothing to cancel");
|
||||
}
|
||||
|
||||
/// Escape also cancels a corner widget drag and a drag of the brackets of type on a path, and
|
||||
/// the release after it does nothing. After a press on a widget that has not moved, Escape drops
|
||||
/// the press, so its release selects no corner.
|
||||
#[test]
|
||||
fn escape_cancels_a_corner_widget_or_bracket_drag() {
|
||||
let p = paint();
|
||||
let (d, rect) = doc_with_rect();
|
||||
let mut s = Selection::default();
|
||||
s.set([rect]);
|
||||
let corners = cx(&d, &s, &p);
|
||||
let (d2, text) = doc_with_path_type();
|
||||
let mut s2 = Selection::default();
|
||||
s2.add(text);
|
||||
let brackets = cx(&d2, &s2, &p);
|
||||
// (what, its context, the press, the drag's first step)
|
||||
for (what, c, from, to) in [
|
||||
("corner widget", &corners, Point::new(110.0, 110.0), Point::new(120.0, 120.0)),
|
||||
("bracket", &brackets, Point::new(400.0, 290.0), Point::new(340.0, 290.0)),
|
||||
] {
|
||||
let mut t = DirectSelectionTool::new(false);
|
||||
assert!(t.pointer(c, &PointerEvent::new(PointerKind::Down, from.x, from.y)).is_empty(), "{what}");
|
||||
let a = t.pointer(c, &PointerEvent::new(PointerKind::Drag, to.x, to.y));
|
||||
assert!(matches!(a.first(), Some(Action::Begin(_))), "{what}: {a:?}");
|
||||
assert!(t.busy(), "{what}");
|
||||
assert_eq!(t.key(c, ToolKey::Escape, Mods::default()), vec![Action::Cancel], "{what}: Escape cancels the drag");
|
||||
assert!(!t.busy(), "{what}");
|
||||
assert!(t.pointer(c, &PointerEvent::new(PointerKind::Drag, to.x - 5.0, to.y)).is_empty(), "{what}: the drag is over");
|
||||
assert!(t.pointer(c, &PointerEvent::new(PointerKind::Up, to.x - 5.0, to.y)).is_empty(), "{what}: the release does nothing");
|
||||
}
|
||||
let mut t = DirectSelectionTool::new(false);
|
||||
t.pointer(&corners, &PointerEvent::new(PointerKind::Down, 110.0, 110.0));
|
||||
assert_eq!(t.key(&corners, ToolKey::Escape, Mods::default()), vec![Action::Cancel]);
|
||||
assert!(t.pointer(&corners, &PointerEvent::new(PointerKind::Up, 110.0, 110.0)).is_empty(), "the click selects no corner");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dragging_a_curved_segment_reshapes_it_and_a_straight_one_moves_its_anchors() {
|
||||
let (d, arch) = arch_doc();
|
||||
|
||||
@@ -19,7 +19,7 @@ use vectorcraft_geom::Point;
|
||||
use super::{hit_anchor, hit_segment, insert_anchor, remove_anchor};
|
||||
use crate::direct::hit_handle;
|
||||
use crate::guides::HandleSnap;
|
||||
use crate::{Action, Cursor, Mods, Overlay, PointerEvent, PointerKind, Tool, ToolContext};
|
||||
use crate::{Action, Cursor, Mods, Overlay, PointerEvent, PointerKind, Tool, ToolContext, ToolKey};
|
||||
use vectorcraft_geom::constrain_angle_from;
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
@@ -296,6 +296,30 @@ impl Tool for AnchorTool {
|
||||
fn busy(&self) -> bool {
|
||||
!matches!(self.state, State::Idle)
|
||||
}
|
||||
/// Escape during an Anchor Point drag (an anchor pulled into a smooth point or moved, a
|
||||
/// handle, a segment, a blend spine's point, handle or segment) cancels it: what it moved goes
|
||||
/// back and the release does nothing. Escape after a press that has not moved yet drops the
|
||||
/// press, so its release converts nothing.
|
||||
fn key(&mut self, _cx: &ToolContext, key: ToolKey, _mods: Mods) -> Vec<Action> {
|
||||
if key != ToolKey::Escape || matches!(self.state, State::Idle) {
|
||||
return vec![];
|
||||
}
|
||||
let began = matches!(
|
||||
self.state,
|
||||
State::Convert { began: true, .. }
|
||||
| State::Move { began: true, .. }
|
||||
| State::SpineMove { began: true, .. }
|
||||
| State::Handle { began: true, .. }
|
||||
| State::SpineConvert { began: true, .. }
|
||||
| State::SpineHandle { began: true, .. }
|
||||
| State::Reshape { began: true, .. }
|
||||
);
|
||||
self.state = State::Idle;
|
||||
self.guides.clear();
|
||||
self.measure = None;
|
||||
self.space_resume = None;
|
||||
if began { vec![Action::Cancel] } else { vec![] }
|
||||
}
|
||||
fn pointer(&mut self, cx: &ToolContext, ev: &PointerEvent) -> Vec<Action> {
|
||||
let p = ev.pos;
|
||||
// Anchors and handle ends are picked from a little further than segments.
|
||||
|
||||
@@ -537,6 +537,58 @@ fn anchor_point_space_mid_drag_moves_the_anchor() {
|
||||
assert_eq!(t.pointer(&cx, &PointerEvent::new(PointerKind::Up, 260.0, 370.0)), vec![Action::Commit]);
|
||||
}
|
||||
|
||||
/// Escape during a drag cancels it, whatever the drag does, and the release after it does
|
||||
/// nothing; Enter leaves the drag alone. After a press that has not moved, Escape drops the press,
|
||||
/// so its release does not make the smooth anchor a corner.
|
||||
#[test]
|
||||
fn escape_cancels_an_anchor_point_drag() {
|
||||
let (mut d, _) = doc_with_rect();
|
||||
let l = d.layers[0].id;
|
||||
let id = d.alloc_id();
|
||||
let mut sp = vectorcraft_geom::SubPath::polyline(&[Point::new(100.0, 300.0), Point::new(200.0, 300.0), Point::new(300.0, 300.0)], false);
|
||||
sp.anchors[1] = vectorcraft_geom::Anchor::smooth(Point::new(200.0, 300.0), Point::new(240.0, 320.0));
|
||||
let path = vectorcraft_geom::PathData::single(sp);
|
||||
d.insert(Some(l), 1, vectorcraft_doc::Node::path(id, path, vectorcraft_doc::Appearance::default_art())).unwrap();
|
||||
let mut s = Selection::default();
|
||||
s.set([id]);
|
||||
let p = paint();
|
||||
let cx = cx(&d, &s, &p);
|
||||
let shift = Mods { shift: true, ..Mods::default() };
|
||||
let space = Mods { space: true, ..Mods::default() };
|
||||
// (what, the press, its modifiers, Space held during the second drag step)
|
||||
for (what, x, y, mods, spaced) in [
|
||||
("convert", 100.0, 300.0, Mods::default(), false),
|
||||
("handle", 240.0, 320.0, Mods::default(), false),
|
||||
("segment", 150.0, 200.0, Mods::default(), false),
|
||||
("shift move", 200.0, 300.0, shift, false),
|
||||
("space move", 100.0, 300.0, Mods::default(), true),
|
||||
] {
|
||||
let mut t = AnchorTool::new("anchorPoint");
|
||||
t.pointer(&cx, &PointerEvent::new(PointerKind::Down, x, y).with_mods(mods));
|
||||
let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, x + 20.0, y + 30.0));
|
||||
assert!(matches!(&a[..], [Action::Begin(_), Action::Preview(..)]), "{what}: {a:?}");
|
||||
let step = if spaced { space } else { Mods::default() };
|
||||
let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, x + 30.0, y + 40.0).with_mods(step));
|
||||
let moved = matches!(&a[..], [Action::Preview(c, _)] if c == "path.moveAnchor");
|
||||
assert_eq!(moved, spaced || what == "shift move", "{what}: {a:?}");
|
||||
assert!(t.key(&cx, ToolKey::Enter, Mods::default()).is_empty(), "{what}: Enter leaves the drag alone");
|
||||
assert!(t.busy(), "{what}");
|
||||
assert_eq!(t.key(&cx, ToolKey::Escape, Mods::default()), vec![Action::Cancel], "{what}: Escape cancels the drag");
|
||||
assert!(!t.busy(), "{what}");
|
||||
assert!(t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, x + 40.0, y + 50.0)).is_empty(), "{what}: the drag is over");
|
||||
assert!(t.pointer(&cx, &PointerEvent::new(PointerKind::Up, x + 40.0, y + 50.0)).is_empty(), "{what}: the release does nothing");
|
||||
assert!(t.overlays(&cx).iter().all(|o| !matches!(o, Overlay::Measure { .. })), "{what}: no readout stays");
|
||||
}
|
||||
// A click on the smooth anchor makes it a corner; Escape between press and release drops it.
|
||||
let mut t = AnchorTool::new("anchorPoint");
|
||||
t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 200.0, 300.0));
|
||||
assert!(t.key(&cx, ToolKey::Escape, Mods::default()).is_empty(), "nothing began, so nothing is canceled");
|
||||
assert!(!t.busy());
|
||||
assert!(t.pointer(&cx, &PointerEvent::new(PointerKind::Up, 200.0, 300.0)).is_empty());
|
||||
// With nothing pressed, Escape is not the tool's.
|
||||
assert!(t.key(&cx, ToolKey::Escape, Mods::default()).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn anchor_point_shift_drag_honors_measurement_labels() {
|
||||
let (mut d, _) = doc_with_rect();
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
//! new point takes the current fill color in its own model, unless Shift); drag a mesh point,
|
||||
//! or a handle of the point last clicked → `object.mesh.movePoint` previews ([`MeshEdit`]);
|
||||
//! Alt-click a mesh point → `object.mesh.deletePoint`. Clicking a gradient mesh's point selects
|
||||
//! the mesh, and the next solid fill color recolors just that point.
|
||||
//! the mesh, and the next solid fill color recolors just that point. Escape during a drag
|
||||
//! cancels it.
|
||||
|
||||
use serde_json::json;
|
||||
use vectorcraft_color::Paint;
|
||||
@@ -17,7 +18,7 @@ use vectorcraft_doc::{NodeId, NodeKind};
|
||||
use vectorcraft_geom::Point;
|
||||
|
||||
use crate::meshedit::{MeshEdit, grid_of};
|
||||
use crate::{Action, Cursor, Mods, Overlay, PointerEvent, PointerKind, Tool, ToolContext};
|
||||
use crate::{Action, Cursor, Mods, Overlay, PointerEvent, PointerKind, Tool, ToolContext, ToolKey};
|
||||
|
||||
const FEEDBACK: [u8; 3] = [0x4f, 0x9d, 0xff];
|
||||
|
||||
@@ -252,6 +253,14 @@ impl Tool for MeshTool {
|
||||
fn busy(&self) -> bool {
|
||||
self.edit.dragging()
|
||||
}
|
||||
/// Escape during a drag of a mesh point or handle cancels it: the point goes back and the
|
||||
/// release does nothing.
|
||||
fn key(&mut self, _cx: &ToolContext, key: ToolKey, _mods: Mods) -> Vec<Action> {
|
||||
if key != ToolKey::Escape {
|
||||
return vec![];
|
||||
}
|
||||
self.edit.cancel().unwrap_or_default()
|
||||
}
|
||||
fn deactivate(&mut self, _cx: &ToolContext) -> Vec<Action> {
|
||||
self.edit.unfocus();
|
||||
self.edit.release().unwrap_or_default()
|
||||
|
||||
@@ -102,6 +102,11 @@ impl MeshEdit {
|
||||
self.drag.take().map(|_| vec![Action::Commit])
|
||||
}
|
||||
|
||||
/// Cancel a drag: the point or handle goes back (`None` when nothing was being dragged).
|
||||
pub fn cancel(&mut self) -> Option<Vec<Action>> {
|
||||
self.drag.take().map(|_| vec![Action::Cancel])
|
||||
}
|
||||
|
||||
pub fn dragging(&self) -> bool {
|
||||
self.drag.is_some()
|
||||
}
|
||||
@@ -194,6 +199,34 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// Escape during a drag of a mesh point or handle, with the Mesh tool or Direct Selection,
|
||||
/// cancels it, and the release after it does nothing; the point stays the focused one.
|
||||
#[test]
|
||||
fn escape_cancels_a_mesh_point_drag() {
|
||||
let (d, e) = doc_with_envelope();
|
||||
let mut s = Selection::default();
|
||||
s.set([e]);
|
||||
let p = paint();
|
||||
let c = cx(&d, &s, &p);
|
||||
let tools: [(&str, Box<dyn Tool>); 2] =
|
||||
[("mesh", crate::meshblend::create("mesh").unwrap()), ("directSelection", Box::new(crate::direct::DirectSelectionTool::new(false)))];
|
||||
for (what, mut t) in tools {
|
||||
// A point, then (once the point is focused) its handle.
|
||||
for (x, y) in [(200.0, 100.0), (200.0, 133.33333)] {
|
||||
assert_eq!(t.pointer(&c, &PointerEvent::new(PointerKind::Down, x, y)), vec![Action::Begin("Move Mesh Point".into())], "{what}");
|
||||
t.pointer(&c, &PointerEvent::new(PointerKind::Drag, x + 10.0, y - 10.0));
|
||||
assert!(t.key(&c, crate::ToolKey::Enter, crate::Mods::default()).is_empty(), "{what}: Enter leaves the drag alone");
|
||||
assert!(t.busy(), "{what}");
|
||||
assert_eq!(t.key(&c, crate::ToolKey::Escape, crate::Mods::default()), vec![Action::Cancel], "{what}: Escape cancels the drag");
|
||||
assert!(!t.busy(), "{what}");
|
||||
assert!(t.pointer(&c, &PointerEvent::new(PointerKind::Drag, x + 20.0, y - 20.0)).is_empty(), "{what}: the drag is over");
|
||||
assert!(t.pointer(&c, &PointerEvent::new(PointerKind::Up, x + 20.0, y - 20.0)).is_empty(), "{what}: the release does nothing");
|
||||
assert!(t.overlays(&c).iter().any(|o| matches!(o, Overlay::Handle { .. })), "{what}: the point stays focused");
|
||||
}
|
||||
assert!(t.key(&c, crate::ToolKey::Escape, crate::Mods::default()).is_empty(), "{what}: nothing to cancel");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn direct_selection_drags_a_selected_envelopes_points() {
|
||||
let (d, e) = doc_with_envelope();
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
//! Measure tool: drag between two points to read distance, angle, dX and dY (Shift constrains to
|
||||
//! 45°). The result stays on screen (and in the tool options, for agents) until the next drag; the
|
||||
//! document is never changed.
|
||||
//! document is never changed. Escape during a drag cancels it: the result of the drag before
|
||||
//! comes back.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_geom::{Point, Vec2};
|
||||
|
||||
use super::CYAN;
|
||||
use crate::{Action, Cursor, Mods, Overlay, PointerEvent, PointerKind, Tool, ToolContext};
|
||||
use crate::{Action, Cursor, Mods, Overlay, PointerEvent, PointerKind, Tool, ToolContext, ToolKey};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct MeasureTool {
|
||||
line: Option<(Point, Point)>,
|
||||
dragging: bool,
|
||||
/// The line measured before the drag under way, which Escape puts back.
|
||||
before: Option<(Point, Point)>,
|
||||
}
|
||||
|
||||
/// (distance, angle in degrees counter-clockwise, dx, dy)
|
||||
@@ -33,6 +36,7 @@ impl Tool for MeasureTool {
|
||||
match ev.kind {
|
||||
PointerKind::Down => {
|
||||
let (p, _) = crate::guides::snap_draw(cx, ev.pos, &[]);
|
||||
self.before = self.line;
|
||||
self.line = Some((p, p));
|
||||
self.dragging = true;
|
||||
}
|
||||
@@ -52,6 +56,14 @@ impl Tool for MeasureTool {
|
||||
vec![]
|
||||
}
|
||||
|
||||
fn key(&mut self, _cx: &ToolContext, key: ToolKey, _mods: Mods) -> Vec<Action> {
|
||||
if key == ToolKey::Escape && self.dragging {
|
||||
self.dragging = false;
|
||||
self.line = self.before;
|
||||
}
|
||||
vec![]
|
||||
}
|
||||
|
||||
fn overlays(&self, cx: &ToolContext) -> Vec<Overlay> {
|
||||
let Some((a, b)) = self.line else { return vec![] };
|
||||
if a == b {
|
||||
@@ -128,4 +140,39 @@ mod tests {
|
||||
assert!(!ov.iter().any(|o| matches!(o, Overlay::Measure { .. })), "Measurement Labels off hides the readout");
|
||||
assert!(ov.iter().any(|o| matches!(o, Overlay::Line { .. })), "the measured line stays");
|
||||
}
|
||||
|
||||
/// Escape during a drag ends it and brings back the result of the drag before (none at
|
||||
/// first); the release after it measures nothing.
|
||||
#[test]
|
||||
fn escape_cancels_a_measure_drag() {
|
||||
let (d, _) = doc_with_rect();
|
||||
let s = Selection::default();
|
||||
let p = paint();
|
||||
let mut cx = cx(&d, &s, &p);
|
||||
cx.smart_guides = false;
|
||||
let mut t = MeasureTool::default();
|
||||
t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 10.0, 10.0));
|
||||
t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 40.0, -30.0));
|
||||
assert!(t.key(&cx, ToolKey::Escape, Mods::default()).is_empty());
|
||||
assert!(!t.busy());
|
||||
assert_eq!(t.options(), Value::Null, "no earlier result");
|
||||
assert!(t.overlays(&cx).is_empty());
|
||||
t.pointer(&cx, &PointerEvent::new(PointerKind::Up, 40.0, -30.0));
|
||||
assert_eq!(t.options(), Value::Null, "the release measures nothing");
|
||||
// A measurement, then a second drag canceled: the first one's result is back.
|
||||
for (kind, x, y) in [(PointerKind::Down, 10.0, 10.0), (PointerKind::Drag, 40.0, -30.0), (PointerKind::Up, 40.0, -30.0)] {
|
||||
t.pointer(&cx, &PointerEvent::new(kind, x, y));
|
||||
}
|
||||
let first = t.options();
|
||||
t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 100.0, 100.0));
|
||||
t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 130.0, 100.0));
|
||||
assert!(t.key(&cx, ToolKey::Enter, Mods::default()).is_empty());
|
||||
assert!(t.busy(), "Enter leaves the drag alone");
|
||||
t.key(&cx, ToolKey::Escape, Mods::default());
|
||||
assert!(!t.busy());
|
||||
assert_eq!(t.options(), first);
|
||||
t.pointer(&cx, &PointerEvent::new(PointerKind::Up, 130.0, 100.0));
|
||||
assert_eq!(t.options(), first);
|
||||
assert!((t.options()["distance"].as_f64().unwrap() - 50.0).abs() < 1e-9);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,5 +50,6 @@ mod sysclip_emf;
|
||||
mod sysclip_probe;
|
||||
mod tab_overflow;
|
||||
mod taskbar;
|
||||
mod toolescape;
|
||||
mod transparencygrid;
|
||||
mod widthtool;
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
//! Escape through shortcut routing during a drag with the Anchor Point, Mesh, Direct Selection,
|
||||
//! Shape Builder and Measure tools.
|
||||
|
||||
use egui::{Key, Modifiers};
|
||||
use serde_json::json;
|
||||
use vectorcraft_engine::Session;
|
||||
use vectorcraft_tools::{PointerEvent, PointerKind};
|
||||
|
||||
use crate::VectorcraftApp;
|
||||
|
||||
/// One frame pressing `key` through the shortcut handler.
|
||||
fn press(app: &mut VectorcraftApp, key: Key) {
|
||||
let ctx = egui::Context::default();
|
||||
let ev = egui::Event::Key { key, physical_key: None, pressed: true, repeat: false, modifiers: Modifiers::NONE };
|
||||
let mut out = ctx.run_ui(egui::RawInput { events: vec![ev], ..Default::default() }, |ui| crate::shortcuts::handle(app, ui.ctx()));
|
||||
out.textures_delta.clear();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn escape_cancels_an_anchor_point_mesh_shape_builder_or_measure_drag() {
|
||||
// (the tool, a gradient mesh first, where the drag starts)
|
||||
for (tool, mesh, from) in [
|
||||
("anchorPoint", false, (100.0, 100.0)),
|
||||
("mesh", true, (100.0, 100.0)),
|
||||
("directSelection", true, (100.0, 100.0)),
|
||||
("shapeBuilder", false, (150.0, 150.0)),
|
||||
("measure", false, (150.0, 150.0)),
|
||||
] {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 400, "height": 400})).unwrap();
|
||||
let a = app.run("shape.rectangle", json!({"x": 100, "y": 100, "width": 100, "height": 100})).unwrap()["id"].as_u64().unwrap();
|
||||
let b = app.run("shape.rectangle", json!({"x": 150, "y": 100, "width": 100, "height": 100})).unwrap()["id"].as_u64().unwrap();
|
||||
if mesh {
|
||||
app.run("object.mesh.create", json!({"ids": [a], "at": [150, 150]})).unwrap();
|
||||
}
|
||||
app.run("select.set", json!({"ids": if mesh { json!([a]) } else { json!([a, b]) }})).unwrap();
|
||||
app.select_tool(tool);
|
||||
let st = app.session.doc().unwrap();
|
||||
let (before, undo) = (st.doc.clone(), st.history.undo.len());
|
||||
let options = app.session.tool_options();
|
||||
let view = app.view_info();
|
||||
let (mid, to) = ((from.0 + 30.0, from.1 + 10.0), (from.0 + 60.0, from.1 + 20.0));
|
||||
for (kind, (x, y)) in [(PointerKind::Down, from), (PointerKind::Drag, mid), (PointerKind::Drag, to)] {
|
||||
app.session.pointer(&PointerEvent::new(kind, x, y), view).unwrap();
|
||||
}
|
||||
assert!(app.session.tool_busy(), "{tool}");
|
||||
match tool {
|
||||
"shapeBuilder" => {}
|
||||
"measure" => assert_ne!(app.session.tool_options(), options, "{tool}: the drag measures"),
|
||||
_ => assert_ne!(*app.session.doc().unwrap().doc, *before, "{tool}: the drag changes the preview"),
|
||||
}
|
||||
press(&mut app, Key::Escape);
|
||||
assert_eq!(*app.session.doc().unwrap().doc, *before, "{tool}: Escape cancels the drag");
|
||||
assert_eq!(app.session.doc().unwrap().history.undo.len(), undo, "{tool}: and adds no undo step");
|
||||
assert!(!app.session.tool_busy(), "{tool}");
|
||||
assert_eq!(app.session.tool_id(), tool, "{tool}: Escape keeps the tool");
|
||||
app.session.pointer(&PointerEvent::new(PointerKind::Up, to.0, to.1), view).unwrap();
|
||||
assert_eq!(*app.session.doc().unwrap().doc, *before, "{tool}: the release changes nothing");
|
||||
assert_eq!(app.session.doc().unwrap().history.undo.len(), undo, "{tool}");
|
||||
assert_eq!(app.session.tool_options()["distance"], options["distance"], "{tool}: no measurement stays");
|
||||
}
|
||||
}
|
||||
+6
-2
@@ -2449,7 +2449,8 @@ so `path.moveAnchors {dx, dy}` (or a Direct Selection drag) pulls the path apart
|
||||
Selected End Points) joins the ends again; `path.join {ids: [a, b], ends: ["last", "first"]}` joins the ends asked for
|
||||
(the Pen's join, #776: drawing on from one open path, a click on another's end makes them one path). `path.convertAnchor {id, subpath?, anchor, to, x?, y?}` and
|
||||
`path.split {id, subpath?, anchor}` do the same to one anchor (the Anchor Point and Scissors tools); the Pen with Alt
|
||||
held over a selected path's handle, anchor or segment works as the Anchor Point tool. `path.reshapeSegment {id,
|
||||
held over a selected path's handle, anchor or segment works as the Anchor Point tool. `press_key` Escape between two
|
||||
`pointer_gesture` calls of an Anchor Point drag cancels it: the path goes back and no undo step is added. `path.reshapeSegment {id,
|
||||
subpath?, segment, t, dx, dy, perpendicular?}` (a segment dragged with Direct Selection or the Anchor Point tool) moves the segment's
|
||||
point at `t` by `dx`, `dy`: a smooth anchor at either end stays smooth, its other handle turning with the moved one.
|
||||
With `perpendicular: true` (Shift held during the drag) both of the segment's handles stand at right angles to the
|
||||
@@ -5059,6 +5060,8 @@ under the opposite corner, else of the front-most selected object. A Shift-click
|
||||
as a click does, so with `clickStrokeSplitsPath` on, a Shift-click on a stroke splits it. A marquee splits no stroke,
|
||||
however little the pointer moves: the tool sends no `splitStroke` with `rect`, and `shapeBuilder.merge` does not split
|
||||
a stroke when it has `rect`. With both `rect` and `points`, the command also touches what the points touch.
|
||||
`press_key` Escape during a Shape Builder drag, a marquee included, cancels it: the release sends no
|
||||
`shapeBuilder.merge`.
|
||||
|
||||
```json
|
||||
{"name":"run_command","arguments":{"command":"shapeBuilder.merge","params":{"rect":[50,50,200,200]}}}
|
||||
@@ -5352,7 +5355,8 @@ right/left/down/up; none stored means the smooth mesh through the points, as old
|
||||
`object.envelope.setMeshPoint {id, index, x, y, handle?}` and `object.mesh.movePoint`, `object.mesh.addLine` and
|
||||
`object.mesh.deletePoint` edit them like a gradient mesh's (one undo step each). On the canvas a selected envelope
|
||||
shows its mesh; the Mesh tool and Direct Selection drag its points and the clicked point's handles, and the Mesh tool
|
||||
adds a row and a column where it clicks inside. The Control bar shows Edit Envelope / Edit Contents, the warp's
|
||||
adds a row and a column where it clicks inside. `press_key` Escape during a drag of a point or handle of a mesh
|
||||
envelope or a gradient mesh cancels it: the point goes back and no undo step is added. The Control bar shows Edit Envelope / Edit Contents, the warp's
|
||||
style, orientation, bend and distortions (or the mesh's rows and columns), Reset (an unbent warp, a flat mesh) and
|
||||
Envelope Options.
|
||||
|
||||
|
||||
@@ -472,6 +472,7 @@ since 2026-10-01.
|
||||
|
||||
- **Clipping paths on their outline (G1):** the selection tools (Selection, Direct Selection, Group Selection), Object Highlighting and a right-click take a clipping path on its outline before the art it clips; Select Behind lists the same; the eyedropper, the tools that act on what is drawn and the selection tools' double-clicks, loaded text and guide release take the art. Doc and tool regression tests; score unchanged.
|
||||
- **Existing-guide Escape cancellation (G1):** Escape clears a pending guide press or drag through Selection and Direct/Group Selection, cancelling a begun preview. Eight app-input regressions cover both axes, move/copy, temporary selection-tool borrowing from idle Pen, preselected-guide document/interaction-start selection and Undo/Redo/saved-state preservation, later on-canvas motion/release, Shift-press cancellation, and ordinary-release/Enter/idle controls. Historical focused checks on `8ab574b7` recorded five expected RED failures with three passing controls, then all eight GREEN tests and the Direct Selection Escape control passed. All nine also passed in current full CI. A full CI retry on the five-unit combined source at `a0671777` passed all seven stages with 6,613 test outcomes on 2026-10-14. An earlier full-CI run on the identical source panicked in the randomized `ai_mutated_save_never_panics` test; the cause remains unresolved. This five-fix batch does not claim to repair that failure. The original 1280×960 headless app capture from `b076025a` was reviewed for one vertical Selection guide after Escape; source review found its scene unchanged on `a0671777`. Scores and hours unchanged.
|
||||
- **Escape in more tool drags:** Escape during an Anchor Point drag, a Mesh tool drag, a Direct Selection drag of a mesh point, a corner widget or the brackets of type on a path, or a Shape Builder drag cancels it: the art goes back to how the press found it, no undo step is added, the release does nothing, and the tool stays chosen. Escape during a Measure tool drag brings back the measurement before it. Regression tests: `escape_cancels_*` in `draw2/anchor/tests.rs`, `meshedit.rs`, `direct/tests.rs`, `builder/tests.rs` and `xform/measure.rs`, and `escape_cancels_an_anchor_point_mesh_shape_builder_or_measure_drag` in `tests/toolescape.rs`.
|
||||
- **TIFF in screen and asset export:** both format menus expose the existing TIFF encoder and Format Settings. Headless UI regressions choose TIFF and check scaled dimensions, artboard selection, filenames, encoding options, saved settings and collected-asset output. Public reference: [Export for Screens](https://helpx.adobe.com/illustrator/desktop/save-and-export/export-files-to-different-formats/export-for-screens.html), updated 2026-09-28, lists TIFF for both artboards and assets. Adobe staff [confirmed support in 30.2.1](https://illustrator.uservoice.com/forums/333657-illustrator-desktop-feature-requests/suggestions/49081034-add-tiff-format-into-export-for-screens-assets-d) on 2026-02-19; the older 2025 Collect assets guide omits TIFF.
|
||||
- **Export for Screens of the selected artboards:** while the Artboard tool is in use, File › Export › Export for Screens… (`file.exportForScreens`) opens with the artboards the tool has selected checked on its Artboards tab, and Export Artboard writes only those. Shift-clicks on the canvas, or Shift- or Ctrl/Cmd-clicks on the Artboards panel's rows, select several. The other fields show the settings the document last exported with. With another tool in use, the dialog takes its artboard choice from those settings too. Public reference: [Export selected artboards](https://helpx.adobe.com/illustrator/desktop/create-manage-artboards/organize-manage-artboards/export-artboards.html).
|
||||
|
||||
|
||||
Reference in New Issue
Block a user