M8.1: Delete/Backspace remove the selected width points and Puppet Warp pins
The Width and Puppet Warp tools now claim Delete and Backspace while they have width points or pins selected, so the shortcut routing (desktop and MCP press_key) removes those instead of clearing the artwork. With nothing of the tool's selected the keys still clear the selected objects. Headless Backspace clears the selection like the desktop app does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
a05e251622
commit
5d21826fa8
|
|
@ -197,6 +197,11 @@ impl Headless {
|
|||
self.session.select_tool(t.id, self.view).map_err(|e| e.to_string())?;
|
||||
return Ok(json!({"handledBy": "tool.select", "tool": t.id}));
|
||||
}
|
||||
// Backspace clears the selection as Delete does (as in the desktop app).
|
||||
if tk == Some(ToolKey::Backspace) && mods == Mods::default() && self.session.active().is_some_and(|d| !d.selection.is_empty()) {
|
||||
let r = self.exec("edit.clear", &json!({}))?;
|
||||
return Ok(json!({"handledBy": "command", "command": "edit.clear", "result": r}));
|
||||
}
|
||||
if let Some(k) = tk {
|
||||
let mut out = vec![];
|
||||
let reqs = self.session.tool_key(k, mods, self.view).map_err(|e| e.to_string())?;
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ pub const DEFAULT_ADDR: &str = "127.0.0.1:7979";
|
|||
#[cfg(test)]
|
||||
mod tests;
|
||||
#[cfg(test)]
|
||||
mod tests_distortkeys;
|
||||
#[cfg(test)]
|
||||
mod tests_exportas;
|
||||
#[cfg(test)]
|
||||
mod tests_fileio;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,66 @@
|
|||
//! `press_key` with the Width and Puppet Warp tools: Delete and Backspace reach the selected width
|
||||
//! point or pin, and clear the selected art when the tool has nothing selected.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use crate::{Backend, Headless, call_tool};
|
||||
|
||||
fn ok(h: &mut Headless, name: &str, args: Value) -> Value {
|
||||
let r = call_tool(h, name, &args);
|
||||
assert!(!r.is_error, "{name} {args}: {r:?}");
|
||||
serde_json::from_str(r.content[0]["text"].as_str().unwrap()).unwrap_or(Value::Null)
|
||||
}
|
||||
|
||||
fn run(h: &mut Headless, command: &str, params: Value) -> Value {
|
||||
ok(h, "run_command", json!({"command": command, "params": params}))
|
||||
}
|
||||
|
||||
fn exists(h: &Headless, id: u64) -> bool {
|
||||
h.session.doc().unwrap().doc.node(vectorcraft_doc::NodeId(id)).is_some()
|
||||
}
|
||||
|
||||
fn click(h: &mut Headless, tool: &str, x: f64, y: f64) {
|
||||
ok(h, "pointer_gesture", json!({"tool": tool, "events": [{"kind": "down", "x": x, "y": y}, {"kind": "up", "x": x, "y": y}]}));
|
||||
}
|
||||
|
||||
fn session() -> Headless {
|
||||
let mut h = Headless::new();
|
||||
h.call("engine.execute", json!({"command": "file.new", "params": {"width": 400, "height": 400}})).unwrap();
|
||||
h
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_and_backspace_reach_the_width_tool() {
|
||||
for key in ["Delete", "Backspace"] {
|
||||
let mut h = session();
|
||||
let id = run(&mut h, "shape.line", json!({"x1": 100, "y1": 200, "x2": 300, "y2": 200}))["id"].as_u64().unwrap();
|
||||
run(&mut h, "stroke.set", json!({"weight": 10}));
|
||||
run(&mut h, "stroke.widthPoint.set", json!({"id": id, "t": 0.5, "left": 12, "right": 12}));
|
||||
click(&mut h, "width", 200.0, 200.0);
|
||||
let r = ok(&mut h, "press_key", json!({"key": key}));
|
||||
assert_eq!(r["handledBy"], "tool", "{key}: {r}");
|
||||
let n = h.session.doc().unwrap().doc.node(vectorcraft_doc::NodeId(id)).unwrap().appearance.stroke().unwrap().profile.clone().unwrap();
|
||||
assert_eq!(n.points.len(), 2, "{key} removed the width point");
|
||||
// With no width point selected the key clears the selected line.
|
||||
ok(&mut h, "press_key", json!({"key": key}));
|
||||
assert!(!exists(&h, id), "{key} cleared the line");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_and_backspace_reach_the_puppet_warp_tool() {
|
||||
for key in ["Delete", "Backspace"] {
|
||||
let mut h = session();
|
||||
let id = run(&mut h, "shape.rectangle", json!({"x": 100, "y": 100, "width": 200, "height": 100}))["id"].as_u64().unwrap();
|
||||
ok(&mut h, "select_tool", json!({"tool": "puppetWarp"}));
|
||||
let pins = |h: &Headless| h.session.tool_options()["pins"].as_array().map_or(0, Vec::len);
|
||||
click(&mut h, "puppetWarp", 112.0, 188.0);
|
||||
let n = pins(&h);
|
||||
let r = ok(&mut h, "press_key", json!({"key": key}));
|
||||
assert_eq!(r["handledBy"], "tool", "{key}: {r}");
|
||||
assert!(exists(&h, id), "{key} kept the art");
|
||||
assert_eq!(pins(&h), n - 1, "{key} removed the pin");
|
||||
ok(&mut h, "press_key", json!({"key": key}));
|
||||
assert!(!exists(&h, id), "{key} cleared the art");
|
||||
}
|
||||
}
|
||||
|
|
@ -131,6 +131,14 @@ impl Tool for PuppetWarpTool {
|
|||
_ => vec![],
|
||||
}
|
||||
}
|
||||
/// Delete/Backspace remove the selected pin; with none selected they are the shortcut's
|
||||
/// (Clear deletes the selected objects).
|
||||
fn claims_key(&self, cx: &ToolContext, key: ToolKey) -> bool {
|
||||
matches!(key, ToolKey::Delete | ToolKey::Backspace)
|
||||
&& self.drag.is_none()
|
||||
&& cx.selection.objects == self.ids
|
||||
&& self.selected.is_some_and(|i| i < self.pins.len())
|
||||
}
|
||||
fn key(&mut self, _cx: &ToolContext, key: ToolKey, _mods: Mods) -> Vec<Action> {
|
||||
if matches!(key, ToolKey::Delete | ToolKey::Backspace)
|
||||
&& let Some(i) = self.selected.take()
|
||||
|
|
|
|||
|
|
@ -349,6 +349,13 @@ impl Tool for WidthTool {
|
|||
},
|
||||
}
|
||||
}
|
||||
/// Delete/Backspace remove the selected width points; with none selected they are the
|
||||
/// shortcut's (Clear deletes the selected objects).
|
||||
fn claims_key(&self, cx: &ToolContext, key: ToolKey) -> bool {
|
||||
matches!(key, ToolKey::Delete | ToolKey::Backspace)
|
||||
&& self.drag.is_none()
|
||||
&& self.selected.as_ref().is_some_and(|(id, _)| !self.selected_indices(cx, *id).is_empty())
|
||||
}
|
||||
fn key(&mut self, cx: &ToolContext, key: ToolKey, _mods: Mods) -> Vec<Action> {
|
||||
if !matches!(key, ToolKey::Delete | ToolKey::Backspace) {
|
||||
return vec![];
|
||||
|
|
|
|||
|
|
@ -49,6 +49,8 @@ mod tests_clipboard;
|
|||
#[cfg(test)]
|
||||
mod tests_cut;
|
||||
#[cfg(test)]
|
||||
mod tests_distortkeys;
|
||||
#[cfg(test)]
|
||||
mod tests_docsetup;
|
||||
#[cfg(test)]
|
||||
mod tests_fonts;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,83 @@
|
|||
//! Delete and Backspace with the Width and Puppet Warp tools, through the shortcut routing: the
|
||||
//! selected width point or pin goes, and with nothing of the tool's selected the keys clear the
|
||||
//! selected art as usual.
|
||||
|
||||
use egui::{Key, Modifiers};
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_engine::Session;
|
||||
use vectorcraft_tools::{PointerEvent, PointerKind};
|
||||
|
||||
use crate::VectorcraftApp;
|
||||
|
||||
/// A 300×300 document with a selected 10 pt line from (100, 200) to (300, 200) → its id.
|
||||
fn app_with_line() -> (VectorcraftApp, u64) {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 400, "height": 400})).unwrap();
|
||||
let id = app.run("shape.line", json!({"x1": 100, "y1": 200, "x2": 300, "y2": 200})).unwrap()["id"].as_u64().unwrap();
|
||||
app.run("stroke.set", json!({"weight": 10})).unwrap();
|
||||
app.run("select.set", json!({"ids": [id]})).unwrap();
|
||||
(app, id)
|
||||
}
|
||||
|
||||
/// 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();
|
||||
}
|
||||
|
||||
fn click(app: &mut VectorcraftApp, x: f64, y: f64) {
|
||||
let v = app.view_info();
|
||||
for kind in [PointerKind::Down, PointerKind::Up] {
|
||||
app.session.pointer(&PointerEvent::new(kind, x, y), v).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn exists(app: &VectorcraftApp, id: u64) -> bool {
|
||||
app.session.doc().unwrap().doc.node(vectorcraft_doc::NodeId(id)).is_some()
|
||||
}
|
||||
|
||||
fn width_points(app: &VectorcraftApp, id: u64) -> Value {
|
||||
let n = app.session.doc().unwrap().doc.node(vectorcraft_doc::NodeId(id)).cloned().unwrap();
|
||||
json!(n.appearance.stroke().unwrap().profile.as_ref().map(|p| p.points.len()))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_and_backspace_remove_the_selected_width_point_then_clear() {
|
||||
for key in [Key::Delete, Key::Backspace] {
|
||||
let (mut app, id) = app_with_line();
|
||||
app.run("stroke.widthPoint.set", json!({"id": id, "t": 0.5, "left": 12, "right": 12})).unwrap();
|
||||
assert_eq!(width_points(&app, id), json!(3));
|
||||
app.select_tool("width");
|
||||
// A click on the width point's centre selects it; the key removes it, not the line.
|
||||
click(&mut app, 200.0, 200.0);
|
||||
press(&mut app, key);
|
||||
assert!(exists(&app, id), "{key:?} kept the line");
|
||||
assert_eq!(width_points(&app, id), json!(2), "{key:?} removed the width point");
|
||||
// Nothing of the tool's is selected now: the key clears the selected line.
|
||||
press(&mut app, key);
|
||||
assert!(!exists(&app, id), "{key:?} with no width point selected clears the selection");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_and_backspace_remove_the_selected_pin_then_clear() {
|
||||
for key in [Key::Delete, Key::Backspace] {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 400, "height": 400})).unwrap();
|
||||
let id = app.run("shape.rectangle", json!({"x": 100, "y": 100, "width": 200, "height": 100})).unwrap()["id"].as_u64().unwrap();
|
||||
app.run("select.set", json!({"ids": [id]})).unwrap();
|
||||
app.select_tool("puppetWarp");
|
||||
let pins = |app: &VectorcraftApp| app.session.tool_options()["pins"].as_array().map_or(0, Vec::len);
|
||||
// A click on the art away from the automatic pins adds a pin and selects it.
|
||||
click(&mut app, 112.0, 188.0);
|
||||
let n = pins(&app);
|
||||
assert!(n > 1, "{n} pins");
|
||||
press(&mut app, key);
|
||||
assert!(exists(&app, id), "{key:?} kept the art");
|
||||
assert_eq!(pins(&app), n - 1, "{key:?} removed the pin");
|
||||
press(&mut app, key);
|
||||
assert!(!exists(&app, id), "{key:?} with no pin selected clears the selection");
|
||||
}
|
||||
}
|
||||
|
|
@ -870,6 +870,9 @@ two points at the same `t` (the width before it, then after it), and the stroke'
|
|||
`document.inspect` reports a path's `strokeOptions.widthPoints` (`[t, left, right]`, factors of half the weight; null
|
||||
for a uniform stroke). `stroke.widthPoint.remove {id, index | indices}` deletes points; `stroke.widthProfile.set {ids,
|
||||
points}` replaces them all, as dragging several Shift-selected points with the Width tool does in one undo step.
|
||||
With the Width tool, `press_key` Delete or Backspace removes the selected width points (`handledBy: "tool"`); with none
|
||||
selected the key clears the selected objects as usual (`edit.clear`). The Puppet Warp tool's selected pins go the
|
||||
same way.
|
||||
|
||||
```json
|
||||
{"name":"run_command","arguments":{"command":"stroke.widthPoint.set","params":{"id":9,"t":0.5,"left":4,"right":4}}}
|
||||
|
|
|
|||
Loading…
Reference in New Issue