Files
vectorcraft/crates/tools/src/addons.rs
T
bflatastic 1ca3db1b51 Add-ons: Smart Pen — draw and edit paths with one tool, panel
A built-in add-on under Object › Plug-ins › Smart Pen:
- The Smart Pen Tool draws and edits Bézier paths without switching
  tools: a click adds the next segment straight or curved (a panel
  toggle; Tab flips the segment being placed or the last one), a drag
  makes a smooth point (Alt breaks the handle, Shift snaps to 45°, Space
  moves the point), a curve after a straight segment joins it tangent.
- On existing paths it selects and moves points, removes a point keeping
  the shape (Alt-click), toggles smooth/corner (double-click), drags
  handles, reshapes a segment by dragging it and inserts a point on it
  (Alt-click); it closes, continues and joins open paths.
- The Smart Pen panel: next segment straight or curved, remove points
  keeping shape, make smooth, make corner, retract handles, close or
  join.

The tool stays out of both toolbars as shipped; the All Tools drawer
lists it under the add-on's heading. Commands are addon.pen.*; the
existing Pen tool, commands and menus are unchanged.
2026-10-30 16:02:27 +07:00

65 lines
3.0 KiB
Rust

//! The tools of VectorCraft's built-in add-ons. They aren't in the toolbars as they ship: the All
//! Tools drawer lists them (under their add-on's heading) so they can be added, and each add-on's
//! submenu selects them. Like every tool they only emit commands (`addon.<name>.*`).
pub mod anchor_eraser;
pub mod extend_path;
pub mod measure_inspect;
pub mod smart_pen;
use crate::{Tool, ToolInfo};
/// Path Precision Tools' tools, one flyout group.
pub const PATH_TOOLS: &[ToolInfo] = &[
ToolInfo { id: anchor_eraser::ID, label: "Anchor Eraser Brush", shortcut: None, icon: "dc-remove-brush" },
ToolInfo { id: extend_path::ID, label: "Extend Path Tool", shortcut: None, icon: "dc-arrow-extend" },
ToolInfo { id: measure_inspect::ID, label: "Measure & Inspect Tool", shortcut: None, icon: "ruler" },
];
/// Smart Pen's tool, a flyout group of its own.
pub const SMART_PEN: &[ToolInfo] = &[ToolInfo { id: smart_pen::ID, label: "Smart Pen Tool", shortcut: None, icon: "spline-pointer" }];
/// The add-ons' tool groups, after the toolbar's own ([`crate::TOOL_GROUPS`]), each with the
/// heading the All Tools drawer lists it under (its add-on's name).
const HEADED: &[(&[ToolInfo], &str)] = &[(PATH_TOOLS, "Path Precision Tools"), (SMART_PEN, "Smart Pen")];
/// The add-ons' tool groups, after the toolbar's own ([`crate::TOOL_GROUPS`]).
pub const GROUPS: &[&[ToolInfo]] = &[PATH_TOOLS, SMART_PEN];
/// The heading the All Tools drawer lists tool `id` under: its add-on's name.
pub fn heading(id: &str) -> Option<&'static str> {
HEADED.iter().find(|(g, _)| g.iter().any(|t| t.id == id)).map(|(_, h)| *h)
}
/// Create an add-on tool by id (None = not one of them).
pub fn create(id: &str) -> Option<Box<dyn Tool>> {
Some(match id {
anchor_eraser::ID => Box::new(anchor_eraser::AnchorEraserTool::default()),
extend_path::ID => Box::new(extend_path::ExtendPathTool::default()),
measure_inspect::ID => Box::new(measure_inspect::MeasureInspectTool::default()),
smart_pen::ID => Box::new(smart_pen::SmartPenTool::default()),
_ => return None,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_addon_tool_is_made_and_catalogued_but_not_in_the_toolbar() {
for t in GROUPS.iter().flat_map(|g| g.iter()) {
assert_eq!(create(t.id).map(|tool| tool.id()), Some(t.id));
assert_eq!(crate::tool_info(t.id).map(|i| i.label), Some(t.label));
assert!(crate::create(t.id).id() == t.id);
assert!(t.shortcut.is_none(), "no default shortcut");
assert!(!crate::TOOL_GROUPS.iter().any(|g| g.iter().any(|x| x.id == t.id)), "{} is not in the toolbar's groups", t.id);
}
assert!(PATH_TOOLS.iter().all(|t| heading(t.id) == Some("Path Precision Tools")));
assert_eq!(heading(smart_pen::ID), Some("Smart Pen"));
assert_eq!(GROUPS.len(), HEADED.len());
assert!(crate::catalog::all_tools().any(|t| t.id == extend_path::ID));
assert!(create("pen").is_none() && heading("pen").is_none());
}
}