Merge pull request #180 from storytold/m8-delete-in-tools
M8.1: Delete/Backspace remove the selected width points and Puppet Warp pins
This commit is contained in:
commit
fe7da442ca
|
|
@ -144,7 +144,7 @@ grade by behaviour against `plan/illustrator/`, not by whether a menu item exist
|
|||
| M5 | Performance | 🟡 background render + caches + MT done; `vectorcraft-cli bench` and `vectorcraft-cli perf` (budget suite); file format v2 opens 3× faster (50k paths: 722 → 244 ms); raster effects (glows, shadows, blur, feather) no longer force the whole frame single-threaded (filtered offscreen per effect, verified equal to the single-threaded reference); effect-heavy demos need a clean-machine benchmark; dirty-region rendering, GPU backend spike pending | 15–25 |
|
||||
| M6 | Path operations (Pathfinder, Shape Builder, offset…) | ✅ mostly done (Boolean precision on almost-horizontal edges fixed and the property tests made deterministic; Shape Builder edge erase, large-offset bug open) | 3–6 |
|
||||
| M7 | Type (point/area/path, editing, styles, OpenType, threading, glyphs) | 🟡 Character/Paragraph Styles, Area Type Options, threaded text, Fit Headline, Glyphs, OpenType panel, Find Font, installed system fonts in every font menu (searchable) and found by name on open/import, CJK names in the UI, Text Wrap (offset, invert, both sides of an object; follows edits), Type on a Path effects, tab stops + Tabs panel done; tab leaders, spell check, vertical type pending | 45–60 |
|
||||
| M8 | Transform & distort (Puppet Warp, Liquify tools, Envelopes, Blends, Perspective Grid) | 🟡 live Blends, Envelopes (warp/mesh/top object), Width tool, Liquify tools, Puppet Warp and Perspective Grid landed; fidelity pass under way (M8.2 tool options persist and Liquify tools share one brush) | 8–12 |
|
||||
| M8 | Transform & distort (Puppet Warp, Liquify tools, Envelopes, Blends, Perspective Grid) | 🟡 live Blends, Envelopes (warp/mesh/top object), Width tool, Liquify tools, Puppet Warp and Perspective Grid landed; fidelity pass under way (M8.2 tool options persist and Liquify tools share one brush, M8.1 Delete removes Width points and Puppet Warp pins, not the art) | 8–12 |
|
||||
| M9 | Live effects (+ 3D & Materials) | 🟡 2D effects done incl. Effect → Pathfinder and Color Adjustments; SVG Filters, Document Raster Effects Settings, 3D pending | 86–135 |
|
||||
| M10 | Brushes, symbols, patterns, Repeat | 🟡 pattern swatches (5 tile types, Pattern Options, editing mode, SVG `<pattern>`/PDF export) and live Repeat (radial/grid/mirror) done; brushes/symbols in progress | 23–37 |
|
||||
| M11 | Artboards & views (artboard panel/tool, Trim View, middle-button pan and print tiling done; multiple windows, presentation polish) | 🟡 | 15–25 |
|
||||
|
|
|
|||
|
|
@ -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