Blend: Escape drops the Blend tool's first pick

After a click picked the first object with the Blend tool, Escape did
nothing: the pick stayed, and the next click on another object blended
the two. After a blend was made, Escape did not stop the next click
adding to it either.

Escape now drops the object picked first and stops the next click
adding to the blend just made, as a click on empty canvas does, so the
next click picks a new first object. The document and the selection
stay as they are, and no undo step is added.

Docs: docs/mcp.md, docs/target-app-parity.md.
This commit is contained in:
dotCooCoo
2026-10-29 11:22:56 +07:00
committed by bflatastic
parent 1924ddd1dd
commit 3ea2bba6e3
4 changed files with 85 additions and 4 deletions
+52 -1
View File
@@ -3,7 +3,7 @@
//! - Blend: click object A, then object B → `object.blend.make {ids: [A, B]}`; clicking an anchor
//! point of a path blends from that point (`starts`). Each further click adds the object as one
//! more key of the new blend (as does clicking a blend first). Double-click or Alt-click opens
//! Blend Options (dialog `blendOptions`). Clicking empty canvas starts over.
//! Blend Options (dialog `blendOptions`). Clicking empty canvas or pressing Escape starts over.
//! - Mesh: click inside a filled path → `object.mesh.create {ids, at}` (1×1 mesh plus lines through
//! the click); click inside a mesh or a mesh envelope → `object.mesh.addLine` (a gradient mesh's
//! new point takes the current fill color in its own model, unless Shift); drag a mesh point,
@@ -136,6 +136,14 @@ impl Tool for BlendTool {
fn busy(&self) -> bool {
self.first.is_some()
}
/// Escape drops the object picked first and stops adding to the blend just made, so the next
/// click picks a new first object. The document and the selection stay as they are.
fn key(&mut self, _cx: &ToolContext, key: crate::ToolKey, _mods: Mods) -> Vec<Action> {
if key == crate::ToolKey::Escape {
(self.first, self.chained) = (None, false);
}
vec![]
}
fn deactivate(&mut self, _cx: &ToolContext) -> Vec<Action> {
(self.first, self.chained) = (None, false);
vec![]
@@ -340,6 +348,49 @@ mod tests {
assert!(t.first(&c).is_none());
}
/// Escape drops the object picked first, and stops adding to the blend just made: the next
/// click is a new first pick, and the click after it blends with that one.
#[test]
fn escape_drops_the_blend_tools_first_pick() {
let (mut d, a) = doc_with_rect();
let l = d.layers[0].id;
let (b, g) = (d.alloc_id(), d.alloc_id());
let r = vectorcraft_geom::shapes::rectangle(vectorcraft_geom::Rect::new(300.0, 100.0, 350.0, 150.0));
d.insert(Some(l), 1, vectorcraft_doc::Node::path(b, r, vectorcraft_doc::Appearance::default_art())).unwrap();
let (k1, k2) = (d.alloc_id(), d.alloc_id());
let key = |id: NodeId, x: f64| {
let r = vectorcraft_geom::shapes::rectangle(vectorcraft_geom::Rect::new(x, 300.0, x + 20.0, 320.0));
std::sync::Arc::new(vectorcraft_doc::Node::path(id, r, vectorcraft_doc::Appearance::default_art()))
};
let blend = vectorcraft_doc::Node::new(g, NodeKind::Blend { children: vec![key(k1, 0.0), key(k2, 100.0)], spec: Default::default() });
d.insert(Some(l), 2, blend).unwrap();
let (s, p) = (Selection::default(), paint());
let c = cx(&d, &s, &p);
let picks = |id: NodeId| vec![Action::Exec("select.set".into(), json!({"ids": [id.0]}))];
let down = |x: f64, y: f64| PointerEvent::new(PointerKind::Down, x, y);
let esc = |t: &mut BlendTool| t.key(&c, crate::ToolKey::Escape, Mods::default());
let mut t = BlendTool::default();
assert_eq!(t.pointer(&c, &down(150.0, 150.0)), picks(a));
assert!(t.busy() && !t.overlays(&c).is_empty());
assert!(t.key(&c, crate::ToolKey::Enter, Mods::default()).is_empty());
assert!(t.busy(), "Enter keeps the pick");
assert!(esc(&mut t).is_empty(), "Escape changes nothing in the document");
assert!(!t.busy() && t.first(&c).is_none() && t.overlays(&c).is_empty(), "Escape drops the pick");
// The next click picks b first instead of blending a with it.
assert_eq!(t.pointer(&c, &down(320.0, 120.0)), picks(b));
assert_eq!(t.pointer(&c, &down(150.0, 150.0)), vec![Action::Exec("object.blend.make".into(), json!({"ids": [b.0, a.0]}))]);
// After a blend is made, Escape stops the next click adding to it.
let mut s = Selection::default();
s.set([g]);
let c = cx(&d, &s, &p);
let mut t = BlendTool { first: None, chained: true };
assert!(t.first(&c).is_some());
assert!(t.key(&c, crate::ToolKey::Escape, Mods::default()).is_empty());
assert!(t.first(&c).is_none(), "Escape ends adding to the blend just made");
assert_eq!(t.pointer(&c, &down(150.0, 150.0)), picks(a));
assert!(esc(&mut BlendTool::default()).is_empty(), "nothing to drop");
}
#[test]
fn mesh_tool_click_path_creates_mesh() {
let (d, a) = doc_with_rect();