vectorcraft/crates/tools/src/lib.rs

739 lines
30 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! VectorCraft tools: pointer events in → commands and overlays out.
//!
//! Tools never mutate the document directly. They emit [`Action`]s that the engine executes:
//! `Begin` snapshots the document, each `Preview` re-applies one command on top of that snapshot
//! (replacing the previous preview), and `Commit` records the last preview as a single undo step
//! and journal entry. One-shot `Exec` actions run immediately. This makes every gesture replayable
//! by the control channel and MCP, and keeps tools testable without a UI.
#![forbid(unsafe_code)]
pub mod bbox;
pub mod builder;
pub mod catalog;
pub mod corners;
pub mod cropimage;
pub mod cut;
pub mod direct;
pub mod distort;
pub mod draw2;
pub mod extra;
pub mod guides;
pub mod meshblend;
pub mod meshedit;
pub mod params;
pub mod pathtype;
pub mod pen;
pub mod place;
pub mod printtiling;
pub mod rulerguide;
pub mod select;
pub mod settings;
pub mod shape;
pub mod slice;
pub mod symbolism;
pub mod text;
pub mod typewidget;
pub mod xform;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use vectorcraft_color::Paint;
use vectorcraft_doc::{Document, NodeId, Selection, Unit};
use vectorcraft_geom::{BezPath, Point, Rect, Vec2};
pub use catalog::{TOOL_GROUPS, ToolInfo, tool_info};
/// Modifier keys.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Mods {
#[serde(default)]
pub shift: bool,
/// Option on macOS.
#[serde(default)]
pub alt: bool,
/// Command on macOS, Ctrl elsewhere.
#[serde(default)]
pub cmd: bool,
#[serde(default)]
pub ctrl: bool,
#[serde(default)]
pub space: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum PointerKind {
/// Button pressed.
Down,
/// Moved with the button held.
Drag,
/// Button released.
Up,
/// Moved with no button (hover).
Move,
DoubleClick,
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub struct PointerEvent {
pub kind: PointerKind,
/// Document coordinates.
pub pos: Point,
#[serde(default)]
pub mods: Mods,
#[serde(default = "one")]
pub pressure: f32,
}
fn one() -> f32 {
1.0
}
impl PointerEvent {
pub fn new(kind: PointerKind, x: f64, y: f64) -> Self {
Self { kind, pos: Point::new(x, y), mods: Mods::default(), pressure: 1.0 }
}
pub fn with_mods(mut self, m: Mods) -> Self {
self.mods = m;
self
}
/// The pen `pressure` of a pointer event given as JSON (control channel, MCP): 0..1, default 1.
pub fn json_pressure(e: &Value) -> f32 {
e.get("pressure").and_then(Value::as_f64).filter(|f| f.is_finite()).map_or(1.0, |f| f.clamp(0.0, 1.0) as f32)
}
/// How long the pointer then holds still, in seconds, from a JSON pointer event's `holdMs`
/// (0..60000; default 0): the time [`Tool::tick`] gets.
pub fn json_hold(e: &Value) -> f64 {
e.get("holdMs").and_then(Value::as_f64).filter(|f| f.is_finite()).map_or(0.0, |ms| ms.clamp(0.0, 60_000.0) / 1000.0)
}
}
/// Keys tools care about.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum ToolKey {
Enter,
Escape,
Backspace,
Delete,
Up,
Down,
Left,
Right,
/// Illustrator: ↑/↓ while drawing a polygon/star change sides/points; `[`/`]` change sizes.
BracketLeft,
BracketRight,
Tab,
Home,
End,
/// A digit key 0–9 (5 while dragging with the Perspective Selection tool moves perpendicular
/// to the plane).
Digit(u8),
}
/// What a tool asks the engine to do.
#[derive(Clone, Debug, PartialEq)]
pub enum Action {
/// Start an interaction (snapshot the document). The label becomes the undo name.
Begin(String),
/// Replace the current preview with this command, applied to the snapshot.
Preview(String, Value),
/// Finish the interaction: keep the last preview as one undo step.
Commit,
/// Abort the interaction: restore the snapshot.
Cancel,
/// Execute a command immediately (its own undo step, if it edits).
Exec(String, Value),
/// Ask the UI to open a dialog (e.g. click with the Rectangle tool → size dialog).
Dialog(String, Value),
/// Switch to another tool (e.g. after placing a text frame).
SwitchTool(String),
/// Call the tool's `notify` after the preceding actions ran (e.g. to pick up a created object).
Notify(String),
}
/// Paint defaults for new art (the fill/stroke proxy) and the rest of the new-art template.
#[derive(Clone, Debug, PartialEq)]
pub struct PaintDefaults {
pub fill: Paint,
pub stroke: Paint,
pub stroke_width: f64,
/// The appearance new art takes (stroke options, more fills and strokes, effects), its top
/// fill and stroke repainted with `fill`, `stroke` and `stroke_width`; `None`: one plain fill
/// and stroke. Its placed gradients are relative to the unit box.
pub appearance: Option<vectorcraft_doc::Appearance>,
/// New art's opacity and blend mode.
pub opacity: f32,
pub blend: vectorcraft_color::BlendMode,
/// The graphic style the template came from (by name): new art is linked to it.
pub style: Option<String>,
/// The template is the last selection's appearance (New Art Has Basic Appearance off): with
/// the option on again, new art takes only its basic fill and stroke.
pub inherited: bool,
}
impl Default for PaintDefaults {
/// White fill, 1 pt black stroke, nothing else.
fn default() -> Self {
Self {
fill: Paint::solid(vectorcraft_color::Color::WHITE),
stroke: Paint::solid(vectorcraft_color::Color::BLACK),
stroke_width: 1.0,
appearance: None,
opacity: 1.0,
blend: Default::default(),
style: None,
inherited: false,
}
}
}
/// The document window on screen, in document coordinates: widgets that stay put on screen (the
/// Plane Switching Widget) are placed with it.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ScreenFrame {
/// The window's top-left corner.
pub origin: Point,
/// One screen pixel to the right and one down.
pub right: Vec2,
pub down: Vec2,
/// The window's size in screen pixels.
pub size: (f64, f64),
}
impl ScreenFrame {
/// The document point `x`, `y` screen pixels from the window's top-left corner.
pub fn at(&self, x: f64, y: f64) -> Point {
self.origin + self.right * x + self.down * y
}
/// Where the document point `p` is, in screen pixels from the window's top-left corner (the
/// inverse of [`Self::at`]); none for a degenerate frame.
pub fn to_screen(&self, p: Point) -> Option<(f64, f64)> {
let (r, d, v) = (self.right, self.down, p - self.origin);
let det = r.x * d.y - d.x * r.y;
(det.is_finite() && det.abs() > 1e-12).then(|| ((v.x * d.y - d.x * v.y) / det, (r.x * v.y - v.x * r.y) / det))
}
}
/// Read-only context a tool sees.
pub struct ToolContext<'a> {
pub doc: &'a Document,
/// Which open document `doc` is (its process-unique id) and its revision, which moves with
/// every change of the document or the selection: what a tool keeps between pointer events
/// for the document as it is (the snap targets of hovering) is keyed on it.
pub revision: (u64, u64),
pub selection: &'a Selection,
/// Screen pixels per document point.
pub zoom: f64,
pub isolation: Option<NodeId>,
pub paint: &'a PaintDefaults,
pub outline: bool,
pub smart_guides: bool,
/// View → Show Guides with Lock Guides off: the selection tools pick and drag ruler guides.
pub guides: bool,
pub snap_to_grid: bool,
/// Bounding box shown (View → Show/Hide Bounding Box).
pub show_bbox: bool,
/// View → Snap to Pixel.
pub snap_to_pixel: bool,
/// View → Snap to Point: with Smart Guides off, dragged selections, drawn points and picked
/// points (a transform's reference point) land on anchors and ruler guides within
/// [`Self::snap_tolerance`].
pub snap_to_point: bool,
/// View → Show Corner Widget: paths show draggable Live Corners widgets in their corners.
pub corner_widgets: bool,
/// Which paint proxy is in front (true = Fill): the one the Gradient tool edits.
pub fill_active: bool,
/// The selected gradient stop (`gradient.selectStop`), marked on the gradient annotator.
pub gradient_stop: Option<usize>,
/// The Appearance panel's active fill/stroke (paint-order index into the first selected
/// object's stack); paint tools show and edit that item.
pub appearance_item: Option<usize>,
/// General → Constrain Angle (degrees): Shift constrains drags to 45° steps from it.
pub constrain_angle: f64,
/// The selected freeform gradient point (`paint.freeform.selectPoint`), marked on the
/// freeform annotator.
pub freeform_point: Option<usize>,
/// Eyedropper Options' raster sample size: the pixels square averaged when the Eyedropper
/// samples an image (1, 3 or 5).
pub raster_sample: u32,
/// General → Use Preview Bounds: the bounding box measures visual bounds (strokes included).
pub preview_bounds: bool,
/// Units ▸ General: the unit measurement labels show lengths in.
pub unit: Unit,
/// Units ▸ Stroke: the unit measurement labels show stroke widths in.
pub stroke_unit: Unit,
/// Clipboard Handling → When pasting text: Keep Plain Text. Text pasted while typing takes
/// the style at the caret, even the text the Type tool copied with its formatting.
pub paste_plain_text: bool,
/// View → Hide Slices: the Slice Selection tool can't pick hidden slices.
pub slices_hidden: bool,
/// View → Lock Slices: the Slice Selection tool leaves locked slices alone.
pub slices_locked: bool,
/// General → Disable Auto Add/Delete is off: the Pen adds an anchor on a selected path's
/// segment and deletes one of its anchors.
pub auto_add_delete: bool,
/// Selection & Anchor Display → Tolerance: how near (screen pixels) a click must be to a path
/// or an anchor to pick it.
pub selection_tolerance: f64,
/// Selection & Anchor Display → Size (1–7, default 3): how big anchors and handles are drawn.
pub anchor_size: u32,
/// Selection & Anchor Display → Object Selection by Path Only: a click inside a filled path
/// doesn't pick it, only one on its path does.
pub path_only: bool,
/// Type → Type Object Selection by Path Only: type is picked on its type path only (point
/// type's baseline, area type's frame, type on a path's path), not anywhere in its bounds.
pub type_path_only: bool,
/// General → Double Click To Isolate: a double-click on a group with the Selection tool
/// isolates it.
pub double_click_isolate: bool,
/// Selection & Anchor Display → Command Click to Select Objects Behind: Cmd/Ctrl-click with the
/// Selection tool selects the object under the selected one, the next click the one under
/// that.
pub select_behind: bool,
/// Selection & Anchor Display → Highlight anchors on mouse over: Direct Selection marks the
/// anchor under the pointer.
pub highlight_anchors: bool,
/// Selection & Anchor Display → Snap to Point (screen pixels): how near an anchor or a ruler
/// guide pulls the pointer while View → Snap to Point is on.
pub snap_tolerance: f64,
/// Selection & Anchor Display → Show handles when multiple anchors are selected: off, Direct
/// Selection shows and drags handles only while a single anchor is selected.
pub handles_multiple: bool,
/// Selection & Anchor Display → Hide Corner Widget for angles greater than (degrees): corners
/// wider than this show no Live Corners widget.
pub corner_widget_max_angle: f64,
/// Selection & Anchor Display → Move Locked and Hidden Artwork with Artboard.
pub move_locked_with_artboard: bool,
/// Selection & Anchor Display → Enable Rubber Band for Pen Tool: the Pen previews the next
/// segment to the pointer.
pub pen_rubber_band: bool,
/// Selection & Anchor Display → Enable Rubber Band for Curvature Tool.
pub curvature_rubber_band: bool,
/// Type → Fill New Type Objects With Placeholder Text: type the Type tools place starts with
/// placeholder text, selected.
pub placeholder_text: bool,
/// Smart Guides → Color: the smart guides' lines and labels (RGB).
pub smart_guide_color: [u8; 3],
/// Smart Guides → Alignment Guides: the lines along the edges and centres the art lines up
/// with show. Off, the art still snaps into line.
pub alignment_guides: bool,
/// Smart Guides → Anchor/Path Labels: the "anchor", "center", "path"… labels show.
pub anchor_path_labels: bool,
/// Smart Guides → Measurement Labels: the size and offset readouts while drawing and moving.
pub measurement_labels: bool,
/// Smart Guides → Transform Tools: the readouts while scaling, rotating and shearing.
pub transform_tools_guides: bool,
/// Smart Guides → Spacing Guides: a moved selection snaps to spacing equal to the gap between
/// two other objects in its row or column, or evenly between its two neighbours, and the
/// equal gaps show.
pub spacing_guides: bool,
/// Smart Guides → Snapping Tolerance (screen pixels): how near a smart guide target pulls the
/// pointer, a dragged edge or a drawn point.
pub snapping_tolerance: f64,
/// Smart Guides → Construction Guides: the angles (degrees, counter-clockwise from the x axis
/// as seen on the page) of the lines through the point a drawn point leaves from, onto which a
/// point near one lands ([`guides::construction_angles`]); none with the option off.
pub construction_angles: &'static [f64],
/// The document window (none headless): screen-fixed widgets sit in it.
pub screen: Option<ScreenFrame>,
/// Where the Plane Switching Widget sits (Perspective Grid Options); None while it's hidden.
pub plane_widget: Option<distort::perspective::widget::WidgetCorner>,
}
impl ToolContext<'_> {
/// Tolerance in document units for `px` screen pixels.
pub fn tol(&self, px: f64) -> f64 {
px / self.zoom.max(1e-9)
}
/// How near (document units) a smart guide target pulls: Smart Guides → Snapping Tolerance.
pub fn snap_tol(&self) -> f64 {
self.tol(self.snapping_tolerance)
}
/// A length as measurement labels show it, in the General unit (`12.50 mm`).
pub fn len(&self, v: f64) -> String {
self.unit.readout(v)
}
/// A size label: `W: …` over `H: …`.
pub fn size_label(&self, w: f64, h: f64) -> String {
format!("W: {}\nH: {}", self.len(w), self.len(h))
}
/// A move label: `dX: …` over `dY: …`.
pub fn offset_label(&self, dx: f64, dy: f64) -> String {
format!("dX: {}\ndY: {}", self.len(dx), self.len(dy))
}
/// What Snap to Grid snaps to: the grid's subdivisions (or its lines without any).
pub fn grid_step(&self) -> f64 {
self.doc.grid.spacing / self.doc.grid.subdivisions.max(1) as f64
}
/// The selection tolerance in document units ([`Self::selection_tolerance`]).
pub fn pick_tol(&self) -> f64 {
self.tol(self.selection_tolerance)
}
/// How near (document units) a press picks an anchor or a direction handle's end: the
/// selection tolerance, but at least 2 px past the drawn point, so a point is easy to catch
/// and wins over the segments through it, which are tested after it (#593).
pub fn point_tol(&self) -> f64 {
// Anchors are drawn 5 px wide at the default Size 3, a pixel more or less per step.
let half = (5.0 + f64::from(self.anchor_size.clamp(1, 7)) - 3.0) / 2.0;
self.tol(self.selection_tolerance.max(half + 2.0))
}
pub fn hit_options(&self) -> vectorcraft_doc::hit::HitOptions {
vectorcraft_doc::hit::HitOptions {
tol: self.pick_tol(),
outline: self.outline,
path_only: self.path_only,
type_path_only: self.type_path_only,
scope: self.isolation,
}
}
}
/// Visual feedback drawn by the UI in screen space (coordinates here are document space).
#[derive(Clone, Debug, PartialEq)]
pub enum Overlay {
/// Marquee / rubber band rectangle.
Marquee(Rect),
/// A path preview (e.g. pen rubber band) in a colour (RGB).
Path { path: BezPath, color: [u8; 3], width: f32, dashed: bool },
/// A line segment (smart guide, handle line).
Line { a: Point, b: Point, color: [u8; 3], dashed: bool },
/// An anchor square: filled = selected.
Anchor { p: Point, color: [u8; 3], filled: bool, size: f32 },
/// A Curvature tool point, drawn as a circle; filled = the last touched point.
CurvePoint { p: Point, color: [u8; 3], filled: bool, size: f32 },
/// A handle end circle.
Handle { p: Point, color: [u8; 3] },
/// Smart-guide style label (e.g. "anchor", "W: 100 pt").
Label { p: Point, text: String, color: [u8; 3] },
/// Measurement pill near the cursor (grey box with white text).
Measure { p: Point, text: String },
/// Translucent filled quad (text selection highlight), RGBA.
Highlight { quad: [Point; 4], color: [u8; 4] },
/// A colour chip of fixed screen size (a gradient stop), RGBA; ringed when selected.
Swatch { p: Point, color: [u8; 4], selected: bool },
/// A hairline in a translucent colour (perspective gridlines), RGBA.
GridLine { a: Point, b: Point, color: [u8; 4] },
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub enum Cursor {
#[default]
Arrow,
ArrowHollow,
Move,
Crosshair,
ResizeH,
ResizeV,
ResizeNwSe,
ResizeNeSw,
Rotate,
/// The rotate cursor beside a bounding box, bowed toward the pointer's side of the box: off
/// its centre in direction `0..8` (0 straight up, then clockwise in 45° steps). Its arc wraps
/// the corner it is at, so at the top right it opens toward the bottom left. Make it with
/// [`Cursor::rotate_about`].
RotateToward(u8),
/// Over a Live Corners widget (drag to round the corners).
CornerRadius,
Pen,
PenAdd,
PenDelete,
PenClose,
PenContinue,
/// While drawing, over an end of another open path (a click joins the two).
PenJoin,
/// Over the last anchor of the path being drawn (a click retracts its outgoing handle), or with
/// Alt held over a selected path's handle or anchor (the Anchor Point tool's gesture).
PenConvert,
Text,
Hand,
HandGrab,
ZoomIn,
ZoomOut,
Eyedropper,
NotAllowed,
/// Over the gradient annotator's bar: a click adds a stop.
AddStop,
/// A gradient stop dragged off the bar: releasing deletes it.
RemoveStop,
/// The Slice tool: a crosshair with a blade.
Slice,
/// The Slice Selection tool: the arrow with a slice badge.
SliceSelect,
/// The Width tool away from strokes.
Width,
/// The Width tool over a stroke: a drag adds a width point.
WidthAdd,
/// The Width tool over a width point or a handle end: a drag moves or widens it.
WidthPoint,
/// The Blend tool away from art: a crosshair with a hollow square.
Blend,
/// The Blend tool over an object it can blend: a crosshair with a filled square.
BlendObject,
/// The Blend tool over an anchor point (the blend starts there): a crosshair with a target.
BlendAnchor,
/// Over a bracket of selected type on a path (a drag moves it): the arrow with a bracket.
PathBracket,
/// Over the type widget of selected type (a double-click converts point type to area type and
/// back): the arrow with a type badge.
TypeWidget,
/// The Shape Builder: a crosshair with a plus (merge mode)...
ShapeBuilder,
/// ...or, with Alt held, a minus (erase mode).
ShapeBuilderErase,
}
impl Cursor {
/// The rotate cursor for the pointer at `p` turning art about `center`: [`Cursor::RotateToward`]
/// the pointer's side of it, or the plain [`Cursor::Rotate`] on the centre itself.
pub fn rotate_about(center: Point, p: Point) -> Self {
let d = p - center;
if !(d.x.is_finite() && d.y.is_finite()) || d.hypot() < 1e-9 {
return Cursor::Rotate;
}
// y points down the page: atan2(x, -y) is 0 straight up and grows clockwise.
let octant = (d.x.atan2(-d.y) / std::f64::consts::FRAC_PI_4).round() as i64;
Cursor::RotateToward(octant.rem_euclid(8) as u8)
}
/// General › Use Precise Cursors: the drawing tools' pointers (the Pen's in every state, the
/// Eyedropper's, the Slice and Blend tools') become a plain crosshair at the hotspot.
pub fn precise(self) -> Self {
match self {
Cursor::Pen
| Cursor::PenAdd
| Cursor::PenDelete
| Cursor::PenClose
| Cursor::PenContinue
| Cursor::PenJoin
| Cursor::PenConvert
| Cursor::Eyedropper
| Cursor::Slice
| Cursor::Blend
| Cursor::BlendObject
| Cursor::BlendAnchor
| Cursor::ShapeBuilder
| Cursor::ShapeBuilderErase => Cursor::Crosshair,
c => c,
}
}
}
/// A tool state machine.
pub trait Tool: Send {
fn id(&self) -> &'static str;
fn pointer(&mut self, cx: &ToolContext, ev: &PointerEvent) -> Vec<Action>;
fn key(&mut self, _cx: &ToolContext, _key: ToolKey, _mods: Mods) -> Vec<Action> {
vec![]
}
fn overlays(&self, _cx: &ToolContext) -> Vec<Overlay> {
vec![]
}
fn cursor(&self, _cx: &ToolContext, _p: Point, _mods: Mods) -> Cursor {
Cursor::Arrow
}
/// Tool options as JSON (shown by the Control bar / tool options dialog).
fn options(&self) -> Value {
Value::Null
}
fn set_option(&mut self, _key: &str, _value: &Value) {}
/// Is an interaction in progress (drag, open pen path)?
fn busy(&self) -> bool {
false
}
/// Is a drag moving, scaling or rotating the selection (the canvas then hides the bounding box,
/// so only the art is seen going with the pointer)?
fn transforming(&self) -> bool {
false
}
/// Does the tool take `key` now, ahead of the command shortcuts bound to it (the Gradient tool
/// with a stop selected takes Delete and the arrows)?
fn claims_key(&self, _cx: &ToolContext, _key: ToolKey) -> bool {
false
}
/// Does the tool want typed text (Type tool editing)? Single-key shortcuts are suppressed.
fn wants_text(&self) -> bool {
false
}
fn text_input(&mut self, _cx: &ToolContext, _s: &str) -> Vec<Action> {
vec![]
}
/// IME composition (marked text) for the tool that wants text: `text` replaces the previous
/// marked text (or the selection); an empty `text` ends the composition. `active_chars` is the
/// clause being converted, in characters of `text`. The committed result arrives through
/// [`Tool::text_input`].
fn ime_preedit(&mut self, _cx: &ToolContext, _text: &str, _active_chars: Option<std::ops::Range<usize>>) -> Vec<Action> {
vec![]
}
/// Is uncommitted IME text being shown? Keys, shortcuts and Undo wait while it is.
fn composing(&self) -> bool {
false
}
/// Where the IME candidate window goes: the caret line (top, bottom) in document space.
fn ime_caret(&self, _cx: &ToolContext) -> Option<(Point, Point)> {
None
}
fn notify(&mut self, _cx: &ToolContext, _what: &str) {}
/// Called when the user switches away (finish pending work).
fn deactivate(&mut self, _cx: &ToolContext) -> Vec<Action> {
vec![]
}
/// A command from outside the tool ran (a menu, a panel, an agent): finish work it ended (the
/// Type tool stops editing text the command took out of the selection).
fn after_command(&mut self, _cx: &ToolContext) -> Vec<Action> {
vec![]
}
/// Time passes while the pointer button is held (`dt` seconds since the last tick, whether or
/// not the pointer moved): tools that keep working while the brush holds still (Twirl, Pucker,
/// Bloat) act on it. The host supplies the time, so tests and agents drive it exactly.
fn tick(&mut self, _cx: &ToolContext, _dt: f64) -> Vec<Action> {
vec![]
}
/// Does the tool want [`Tool::tick`]s now (the host keeps time only then)?
fn wants_ticks(&self) -> bool {
false
}
}
/// Create a tool by id. Unknown or not-yet-implemented tools fall back to a no-op tool that keeps the id.
pub fn create(id: &str) -> Box<dyn Tool> {
match id {
"selection" => Box::new(select::SelectionTool::default()),
"directSelection" => Box::new(direct::DirectSelectionTool::new(false)),
"groupSelection" => Box::new(direct::DirectSelectionTool::new(true)),
"pen" => Box::new(pen::PenTool::default()),
"type" | "areaType" | "typeOnPath" | "verticalType" | "verticalAreaType" | "verticalTypeOnPath" => Box::new(text::TypeTool::new(id)),
"rectangle" | "roundedRectangle" | "ellipse" | "polygon" | "star" | "lineSegment" => Box::new(shape::ShapeTool::new(id)),
// Not in the toolbar: `file.place.queue` loads it.
"place" => Box::new(place::PlaceTool::default()),
other => symbolism::create(other)
.or_else(|| builder::create(other))
.or_else(|| draw2::create(other))
.or_else(|| xform::create(other))
.or_else(|| meshblend::create(other))
.or_else(|| distort::create(other))
.or_else(|| extra::create(other))
.or_else(|| slice::create(other))
.or_else(|| printtiling::create(other))
.or_else(|| cropimage::create(other))
.or_else(|| cut::create(other))
.unwrap_or_else(|| Box::new(NoopTool(tool_info(other).map(|t| t.id).unwrap_or("selection")))),
}
}
/// Placeholder for tools whose behaviour hasn't landed yet (selecting them still works).
pub struct NoopTool(&'static str);
impl Tool for NoopTool {
fn id(&self) -> &'static str {
self.0
}
fn pointer(&mut self, _cx: &ToolContext, _ev: &PointerEvent) -> Vec<Action> {
vec![]
}
}
pub(crate) fn json_ids(ids: &[NodeId]) -> Value {
Value::Array(ids.iter().map(|i| Value::from(i.0)).collect())
}
#[cfg(test)]
pub(crate) mod testutil {
use super::*;
use vectorcraft_doc::{Appearance, Node};
use vectorcraft_geom::shapes;
pub fn doc_with_rect() -> (Document, NodeId) {
let mut d = Document::new(500.0, 500.0);
let l = d.layers[0].id;
let id = d.alloc_id();
d.insert(Some(l), 0, Node::path(id, shapes::rectangle(Rect::new(100.0, 100.0, 200.0, 200.0)), Appearance::default_art())).unwrap();
(d, id)
}
/// [`doc_with_rect`] plus 120 × 40 area type at (300, 300).
pub fn doc_with_area_type() -> (Document, NodeId) {
let (mut d, _) = doc_with_rect();
let l = d.layers[0].id;
let id = d.alloc_id();
let mut t = vectorcraft_doc::TextObject::point(Point::new(300.0, 300.0), "Some words", Default::default());
t.kind = vectorcraft_doc::TextKind::Area { frame: shapes::rectangle(Rect::new(0.0, 0.0, 120.0, 40.0)) };
d.insert(Some(l), 1, Node::new(id, vectorcraft_doc::NodeKind::Text(Box::new(t)))).unwrap();
(d, id)
}
/// [`doc_with_rect`] plus type on a 300 pt path from (100, 300) to the right, from 20 % on.
pub fn doc_with_path_type() -> (Document, NodeId) {
let (mut d, _) = doc_with_rect();
let l = d.layers[0].id;
let id = d.alloc_id();
let mut t = vectorcraft_doc::TextObject::point(Point::ZERO, "Path type", Default::default());
let path = vectorcraft_geom::PathData::from_bezpath(&vectorcraft_geom::BezPath::from_vec(vec![
vectorcraft_geom::PathEl::MoveTo(Point::new(100.0, 300.0)),
vectorcraft_geom::PathEl::LineTo(Point::new(400.0, 300.0)),
]));
t.kind = vectorcraft_doc::TextKind::OnPath { path, start: 0.2, end: None };
d.insert(Some(l), 1, Node::new(id, vectorcraft_doc::NodeKind::Text(Box::new(t)))).unwrap();
(d, id)
}
pub fn paint() -> PaintDefaults {
PaintDefaults::default()
}
pub fn cx<'a>(d: &'a Document, s: &'a Selection, p: &'a PaintDefaults) -> ToolContext<'a> {
ToolContext {
doc: d,
revision: (0, 0),
selection: s,
zoom: 1.0,
isolation: None,
paint: p,
outline: false,
smart_guides: true,
guides: true,
snap_to_grid: false,
show_bbox: true,
snap_to_pixel: false,
snap_to_point: true,
corner_widgets: true,
fill_active: true,
gradient_stop: None,
appearance_item: None,
constrain_angle: 0.0,
freeform_point: None,
raster_sample: 1,
preview_bounds: false,
unit: Unit::Points,
stroke_unit: Unit::Points,
paste_plain_text: false,
slices_hidden: false,
slices_locked: false,
auto_add_delete: true,
selection_tolerance: 3.0,
anchor_size: 3,
path_only: false,
type_path_only: false,
double_click_isolate: true,
select_behind: true,
highlight_anchors: true,
snap_tolerance: 2.0,
handles_multiple: true,
corner_widget_max_angle: 177.0,
move_locked_with_artboard: false,
pen_rubber_band: true,
curvature_rubber_band: true,
placeholder_text: false,
smart_guide_color: guides::MAGENTA,
alignment_guides: true,
anchor_path_labels: true,
measurement_labels: true,
transform_tools_guides: true,
spacing_guides: true,
snapping_tolerance: 4.0,
construction_angles: guides::construction_angles(guides::DEFAULT_CONSTRUCTION_ANGLES),
screen: None,
plane_widget: Some(Default::default()),
}
}
}