Files
vectorcraft/crates/tools/src/catalog.rs
T
bflatasticandClaude Opus 5.5 aefd961bbd Fix #494: Pen + Cmd edits the path being drawn; Pen + Alt reshapes segments; handle drags fixed and fast
Reported: with the Pen there was no way to edit curves in place as Ctrl does in Illustrator on
Windows (Cmd on the Mac), segments couldn't be grabbed, and the curve-editing tools felt slow and
sometimes didn't take a handle.

- Cmd/Ctrl held at a press with any tool but a selection tool now borrows the selection tool in the
  engine (Session::pointer), not in the canvas: the selection tool chosen last, or Direct Selection
  for the Pen, Curvature and anchor tools until one is chosen. The tool lent to isn't deactivated,
  so the Pen goes on drawing its path after the release while that path stays selected; it no
  longer draws on another open path selected meanwhile. Agents reach it with pointer_gesture
  mods.cmd. With Cmd held the pointer is the borrowed tool's. Choosing a tool or switching
  documents mid-gesture gives the tool back first.
- Pen + Alt over a segment of a selected path reshapes it, as the Anchor Point tool does.
- The handles shown are the handles that drag (one helper for Direct Selection, the Anchor Point
  tool and Pen + Alt): Direct Selection now drags the handles of a path selected as a whole (as
  the Pen leaves it, so Cmd + drag of a handle while drawing works), the Anchor Point tool no
  longer grabs handles that Show handles when multiple anchors are selected hides, the nearest
  handle wins, and a handle shorter than the tolerance leaves its anchor to be picked. The anchor
  tools pick within Selection & Anchor Display > Tolerance instead of a fixed 4 px.
- path.reshapeSegment keeps a smooth anchor at either end smooth (its other handle turns with the
  moved one) instead of kinking it.
- A dragged handle gathered its smart guide targets from the whole document on every pointer move
  (21 ms per move on 20,000 paths): HandleSnap now gathers them once per drag (1 ms).
  vectorcraft-cli perf measures handle and segment drags on a 5,000-anchor path.
- catalog::is_selection_tool and catalog::edits_anchors replace the UI's own lists.
- Tests in tools, engine and MCP (headless pointer_gesture); docs/mcp.md, control-protocol.md,
  ROADMAP.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 22:00:13 +07:00

196 lines
8.9 KiB
Rust

//! The tool catalogue: ids, labels, shortcuts and toolbar groups (Illustrator's Advanced toolbar order).
use serde::Serialize;
#[derive(Clone, Copy, Debug, Serialize)]
pub struct ToolInfo {
pub id: &'static str,
pub label: &'static str,
/// Single-key shortcut (Shift+key shown as "Shift+X").
pub shortcut: Option<&'static str>,
/// Icon name in the UI icon set.
pub icon: &'static str,
}
const fn t(id: &'static str, label: &'static str, shortcut: Option<&'static str>, icon: &'static str) -> ToolInfo {
ToolInfo { id, label, shortcut, icon }
}
/// Toolbar groups in order; the first tool of each group is the default visible one.
pub const TOOL_GROUPS: &[&[ToolInfo]] = &[
&[t("selection", "Selection Tool", Some("V"), "tool-selection")],
&[
t("directSelection", "Direct Selection Tool", Some("A"), "tool-direct"),
t("groupSelection", "Group Selection Tool", None, "tool-group-select"),
],
&[t("magicWand", "Magic Wand Tool", Some("Y"), "tool-magic-wand")],
&[t("lasso", "Lasso Tool", Some("Q"), "tool-lasso")],
&[
t("pen", "Pen Tool", Some("P"), "tool-pen"),
t("addAnchor", "Add Anchor Point Tool", Some("+"), "tool-pen-add"),
t("deleteAnchor", "Delete Anchor Point Tool", Some("-"), "tool-pen-delete"),
t("anchorPoint", "Anchor Point Tool", Some("Shift+C"), "tool-anchor"),
],
&[t("curvature", "Curvature Tool", Some("Shift+~"), "tool-curvature")],
&[
t("type", "Type Tool", Some("T"), "tool-type"),
t("areaType", "Area Type Tool", None, "tool-type-area"),
t("typeOnPath", "Type on a Path Tool", None, "tool-type-path"),
t("verticalType", "Vertical Type Tool", None, "tool-type-vertical"),
t("verticalAreaType", "Vertical Area Type Tool", None, "tool-type-vertical"),
t("verticalTypeOnPath", "Vertical Type on a Path Tool", None, "tool-type-path"),
t("touchType", "Touch Type Tool", Some("Shift+T"), "tool-touch-type"),
],
&[
t("lineSegment", "Line Segment Tool", Some("\\"), "tool-line"),
t("arc", "Arc Tool", None, "tool-arc"),
t("spiral", "Spiral Tool", None, "tool-spiral"),
t("rectangularGrid", "Rectangular Grid Tool", None, "tool-rect-grid"),
t("polarGrid", "Polar Grid Tool", None, "tool-polar-grid"),
],
&[
t("rectangle", "Rectangle Tool", Some("M"), "tool-rect"),
t("roundedRectangle", "Rounded Rectangle Tool", None, "tool-rounded-rect"),
t("ellipse", "Ellipse Tool", Some("L"), "tool-ellipse"),
t("polygon", "Polygon Tool", None, "tool-polygon"),
t("star", "Star Tool", None, "tool-star"),
t("flare", "Flare Tool", None, "tool-flare"),
],
&[t("paintbrush", "Paintbrush Tool", Some("B"), "tool-brush"), t("blobBrush", "Blob Brush Tool", Some("Shift+B"), "tool-blob-brush")],
&[
t("shaper", "Shaper Tool", Some("Shift+N"), "tool-shaper"),
t("pencil", "Pencil Tool", Some("N"), "tool-pencil"),
t("smooth", "Smooth Tool", None, "tool-smooth"),
t("pathEraser", "Path Eraser Tool", None, "tool-path-eraser"),
t("join", "Join Tool", None, "tool-join"),
],
&[
t("eraser", "Eraser Tool", Some("Shift+E"), "tool-eraser"),
t("scissors", "Scissors Tool", Some("C"), "tool-scissors"),
t("knife", "Knife", None, "tool-knife"),
t("mirrorCut", "Mirror & Cut Tool", None, "tool-mirror-cut"),
t("lineCut", "Line Cut Tool", None, "tool-line-cut"),
t("rectCut", "Rectangle Cut Tool", None, "tool-rect-cut"),
],
&[t("rotate", "Rotate Tool", Some("R"), "tool-rotate"), t("reflect", "Reflect Tool", Some("O"), "tool-reflect")],
&[
t("scale", "Scale Tool", Some("S"), "tool-scale"),
t("shear", "Shear Tool", None, "tool-shear"),
t("reshape", "Reshape Tool", None, "tool-reshape"),
],
&[
t("width", "Width Tool", Some("Shift+W"), "tool-width"),
t("warp", "Warp Tool", Some("Shift+R"), "tool-warp"),
t("twirl", "Twirl Tool", None, "tool-twirl"),
t("pucker", "Pucker Tool", None, "tool-pucker"),
t("bloat", "Bloat Tool", None, "tool-bloat"),
t("scallop", "Scallop Tool", None, "tool-scallop"),
t("crystallize", "Crystallize Tool", None, "tool-crystallize"),
t("wrinkle", "Wrinkle Tool", None, "tool-wrinkle"),
],
&[t("freeTransform", "Free Transform Tool", Some("E"), "tool-free-transform"), t("puppetWarp", "Puppet Warp Tool", None, "tool-puppet")],
&[
t("shapeBuilder", "Shape Builder Tool", Some("Shift+M"), "tool-shape-builder"),
t("livePaintBucket", "Live Paint Bucket", Some("K"), "tool-bucket"),
t("livePaintSelection", "Live Paint Selection Tool", Some("Shift+L"), "tool-live-select"),
],
&[
t("perspectiveGrid", "Perspective Grid Tool", Some("Shift+P"), "tool-perspective"),
t("perspectiveSelection", "Perspective Selection Tool", Some("Shift+V"), "tool-perspective-select"),
],
&[t("mesh", "Mesh Tool", Some("U"), "tool-mesh")],
&[t("gradient", "Gradient Tool", Some("G"), "tool-gradient")],
&[t("eyedropper", "Eyedropper Tool", Some("I"), "tool-eyedropper"), t("measure", "Measure Tool", None, "tool-measure")],
&[t("blend", "Blend Tool", Some("W"), "tool-blend")],
&[
t("symbolSprayer", "Symbol Sprayer Tool", Some("Shift+S"), "tool-symbol"),
t("symbolShifter", "Symbol Shifter Tool", None, "tool-symbol"),
t("symbolScruncher", "Symbol Scruncher Tool", None, "tool-symbol"),
t("symbolSizer", "Symbol Sizer Tool", None, "tool-symbol"),
t("symbolSpinner", "Symbol Spinner Tool", None, "tool-symbol"),
t("symbolStainer", "Symbol Stainer Tool", None, "tool-symbol"),
t("symbolScreener", "Symbol Screener Tool", None, "tool-symbol"),
t("symbolStyler", "Symbol Styler Tool", None, "tool-symbol"),
],
&[
t("columnGraph", "Column Graph Tool", Some("J"), "tool-graph"),
t("stackedColumnGraph", "Stacked Column Graph Tool", None, "tool-graph"),
t("barGraph", "Bar Graph Tool", None, "tool-graph"),
t("stackedBarGraph", "Stacked Bar Graph Tool", None, "tool-graph"),
t("lineGraph", "Line Graph Tool", None, "tool-graph"),
t("areaGraph", "Area Graph Tool", None, "tool-graph"),
t("scatterGraph", "Scatter Graph Tool", None, "tool-graph"),
t("pieGraph", "Pie Graph Tool", None, "tool-graph"),
t("radarGraph", "Radar Graph Tool", None, "tool-graph"),
],
&[
t("artboard", "Artboard Tool", Some("Shift+O"), "tool-artboard"),
t("slice", "Slice Tool", Some("Shift+K"), "tool-slice"),
t("sliceSelection", "Slice Selection Tool", None, "tool-slice"),
],
&[
t("hand", "Hand Tool", Some("H"), "tool-hand"),
t("rotateView", "Rotate View Tool", Some("Shift+H"), "tool-rotate-view"),
t("printTiling", "Print Tiling Tool", None, "tool-print-tiling"),
],
&[t("zoom", "Zoom Tool", Some("Z"), "tool-zoom")],
];
pub fn all_tools() -> impl Iterator<Item = &'static ToolInfo> {
TOOL_GROUPS.iter().flat_map(|g| g.iter())
}
pub fn tool_info(id: &str) -> Option<&'static ToolInfo> {
all_tools().find(|t| t.id == id)
}
/// Tool for a single-key shortcut such as "V" or "Shift+M".
pub fn tool_for_shortcut(s: &str) -> Option<&'static ToolInfo> {
all_tools().find(|t| t.shortcut == Some(s))
}
/// Index of the group containing `id`.
pub fn group_of(id: &str) -> Option<usize> {
TOOL_GROUPS.iter().position(|g| g.iter().any(|t| t.id == id))
}
/// Selection, Direct Selection and Group Selection: the tools Cmd held lends the others for a drag.
pub fn is_selection_tool(id: &str) -> bool {
matches!(id, "selection" | "directSelection" | "groupSelection")
}
/// The tools that show a selected path's anchors and handles as Direct Selection does, and drag
/// them: a Cmd drag with one of them is a Direct Selection drag until a selection tool is chosen.
pub fn edits_anchors(id: &str) -> bool {
matches!(id, "directSelection" | "pen" | "addAnchor" | "deleteAnchor" | "anchorPoint" | "curvature")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unique_ids_and_shortcuts() {
let mut ids: Vec<&str> = all_tools().map(|t| t.id).collect();
let n = ids.len();
ids.sort();
ids.dedup();
assert_eq!(ids.len(), n, "duplicate tool ids");
let mut sc: Vec<&str> = all_tools().filter_map(|t| t.shortcut).collect();
let n = sc.len();
sc.sort();
sc.dedup();
assert_eq!(sc.len(), n, "duplicate shortcuts");
assert!(n > 40);
}
#[test]
fn lookups() {
assert_eq!(tool_for_shortcut("V").unwrap().id, "selection");
assert_eq!(tool_for_shortcut("Shift+M").unwrap().id, "shapeBuilder");
assert_eq!(group_of("star"), group_of("rectangle"));
assert!(is_selection_tool("groupSelection") && !is_selection_tool("pen"));
assert!(edits_anchors("pen") && !edits_anchors("rectangle"));
}
}