mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-31 21:27:00 -06:00
Merge pull request #4703 from storytold/feat/smart-pen-marquee
Smart Pen: an Alt-drag away from paths selects the points inside a marquee
This commit is contained in:
@@ -16,12 +16,14 @@
|
||||
//! A selected point's handle drags (Shift: 45°, Alt: keeps its angle); a double-click on it toggles
|
||||
//! its point. A segment drags to reshape it (Shift: handles square to its chord, Alt: handles keep
|
||||
//! their angles), Alt-click adds a point on it; a click on a path that isn't selected selects it.
|
||||
//! With no path being drawn, an Alt-drag away from paths draws a marquee that selects the points
|
||||
//! inside it (Shift: toggles them).
|
||||
//!
|
||||
//! Each gesture is one undo step (`addon.pen.*`, and `path.*` where the standard command does it).
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_doc::{NodeId, NodeKind};
|
||||
use vectorcraft_geom::{Anchor, BezPath, Point, SubPath, Vec2, constrain_angle_from};
|
||||
use vectorcraft_geom::{Anchor, BezPath, Point, Rect, SubPath, Vec2, constrain_angle_from};
|
||||
use vectorcraft_pathops::addons::pathtools::SMOOTH_RATIO;
|
||||
use vectorcraft_pathops::addons::pen::{leave_handle, straighten_segment};
|
||||
|
||||
@@ -50,6 +52,8 @@ pub struct SmartPenTool {
|
||||
/// The pointer at the last press or drag (Space moves an anchor being placed by its moves).
|
||||
last: Point,
|
||||
snap: DrawSnap,
|
||||
/// An Alt-drag's marquee: where it started, where it is, and whether it toggles (Shift).
|
||||
marquee: Option<(Point, Point, bool)>,
|
||||
}
|
||||
|
||||
/// A press: where it went down, what it is on and whether it has become a drag (and started its
|
||||
@@ -140,6 +144,50 @@ fn selected_alone(cx: &ToolContext, id: NodeId) -> bool {
|
||||
}
|
||||
|
||||
impl SmartPenTool {
|
||||
/// The marquee's part of a pointer event: an Alt-press away from paths, with no path being
|
||||
/// drawn, starts it; drags move its corner; the release selects the points of the editable
|
||||
/// paths inside it (`select.anchorsMany`; Shift: toggled). `None`: the event isn't its.
|
||||
fn marquee_event(&mut self, cx: &ToolContext, ev: &PointerEvent) -> Option<Vec<Action>> {
|
||||
match (ev.kind, self.marquee) {
|
||||
(PointerKind::Down, None) if ev.mods.alt && self.active(cx).is_none() && self.hit(cx, ev.pos) == Hit::Nothing => {
|
||||
self.marquee = Some((ev.pos, ev.pos, ev.mods.shift));
|
||||
Some(vec![])
|
||||
}
|
||||
(PointerKind::Drag | PointerKind::Move, Some((a, _, toggle))) => {
|
||||
self.marquee = Some((a, ev.pos, toggle));
|
||||
Some(vec![])
|
||||
}
|
||||
(PointerKind::Up, Some((a, _, toggle))) => {
|
||||
self.marquee = None;
|
||||
let r = Rect::from_points(a, ev.pos);
|
||||
// A click, not a drag: nothing to select.
|
||||
if r.width() < cx.tol(DRAG_THRESHOLD) && r.height() < cx.tol(DRAG_THRESHOLD) {
|
||||
return Some(vec![]);
|
||||
}
|
||||
let editable = cx.doc.editable();
|
||||
let mut items = vec![];
|
||||
crate::draw2::walk_pickable(cx, |n| {
|
||||
if let NodeKind::Path { path, .. } = &n.kind
|
||||
&& editable(n.id)
|
||||
{
|
||||
let inside: Vec<Value> = path.anchors().filter(|(_, _, a)| r.contains(a.p)).map(|(s, i, _)| json!([s, i])).collect();
|
||||
if !inside.is_empty() {
|
||||
items.push(json!({"id": n.id.0, "anchors": inside}));
|
||||
}
|
||||
}
|
||||
});
|
||||
Some(match (items.is_empty(), toggle) {
|
||||
(true, true) => vec![],
|
||||
(true, false) => vec![Action::Exec("select.none".into(), json!({}))],
|
||||
(false, _) => {
|
||||
vec![Action::Exec("select.anchorsMany".into(), json!({"items": items, "mode": if toggle { "toggle" } else { "set" }}))]
|
||||
}
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The open subpath being drawn: (path, subpath, its data), while it is selected and editable.
|
||||
fn active<'a>(&self, cx: &'a ToolContext) -> Option<(NodeId, usize, &'a SubPath)> {
|
||||
let (id, si) = self.path.filter(|_| self.drawing)?;
|
||||
@@ -438,7 +486,7 @@ impl Tool for SmartPenTool {
|
||||
}
|
||||
|
||||
fn busy(&self) -> bool {
|
||||
self.press.is_some()
|
||||
self.press.is_some() || self.marquee.is_some()
|
||||
}
|
||||
|
||||
fn pointer(&mut self, cx: &ToolContext, ev: &PointerEvent) -> Vec<Action> {
|
||||
@@ -453,6 +501,9 @@ impl Tool for SmartPenTool {
|
||||
let leave = Leave::segment(cx, a.p, ev.mods.shift);
|
||||
if a.has_out() { leave.without_contact() } else { leave }
|
||||
});
|
||||
if let Some(acts) = self.marquee_event(cx, ev) {
|
||||
return acts;
|
||||
}
|
||||
match ev.kind {
|
||||
PointerKind::Move => {
|
||||
self.hover = Some(self.snap.hover(cx, ev.pos, drawn.as_slice(), from.as_ref()));
|
||||
@@ -489,6 +540,7 @@ impl Tool for SmartPenTool {
|
||||
|
||||
fn deactivate(&mut self, _cx: &ToolContext) -> Vec<Action> {
|
||||
self.stop();
|
||||
self.marquee = None;
|
||||
self.snap.clear();
|
||||
match self.press.take() {
|
||||
Some(pr) if pr.begun => vec![Action::Commit],
|
||||
@@ -516,6 +568,9 @@ impl Tool for SmartPenTool {
|
||||
}
|
||||
}
|
||||
o.extend_from_slice(self.snap.guides());
|
||||
if let Some((a, b, _)) = self.marquee {
|
||||
o.push(Overlay::Marquee(Rect::from_points(a, b)));
|
||||
}
|
||||
o
|
||||
}
|
||||
|
||||
|
||||
@@ -308,3 +308,24 @@ fn the_rubber_band_shows_the_next_segment_and_ending_the_path() {
|
||||
assert!(t.active(&cx).is_none());
|
||||
assert!(t.overlays(&cx).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_alt_drag_away_from_paths_selects_the_points_inside_a_marquee() {
|
||||
let (d, id, s) = doc(SubPath::polyline(&[Point::new(100.0, 100.0), Point::new(200.0, 100.0), Point::new(300.0, 100.0)], false));
|
||||
let p = paint();
|
||||
let cx = plain(&d, &s, &p);
|
||||
let mut t = SmartPenTool::default();
|
||||
assert!(t.pointer(&cx, &ev(PointerKind::Down, 50.0, 50.0, ALT)).is_empty());
|
||||
assert!(t.busy());
|
||||
t.pointer(&cx, &ev(PointerKind::Drag, 250.0, 150.0, ALT));
|
||||
assert_eq!(t.overlays(&cx).last(), Some(&Overlay::Marquee(Rect::from_points(Point::new(50.0, 50.0), Point::new(250.0, 150.0)))));
|
||||
let acts = t.pointer(&cx, &ev(PointerKind::Up, 250.0, 150.0, ALT));
|
||||
assert_eq!(acts, [Action::Exec("select.anchorsMany".into(), json!({"items": [{"id": id.0, "anchors": [[0, 0], [0, 1]]}], "mode": "set"}))]);
|
||||
assert!(!t.busy(), "the marquee is over");
|
||||
// A click is no marquee; Shift toggles the points inside it.
|
||||
assert!(click(&mut t, &cx, 50.0, 50.0, ALT).is_empty());
|
||||
t.pointer(&cx, &ev(PointerKind::Down, 280.0, 50.0, Mods { shift: true, ..ALT }));
|
||||
t.pointer(&cx, &ev(PointerKind::Drag, 320.0, 150.0, Mods { shift: true, ..ALT }));
|
||||
let acts = t.pointer(&cx, &ev(PointerKind::Up, 320.0, 150.0, Mods { shift: true, ..ALT }));
|
||||
assert_eq!(acts, [Action::Exec("select.anchorsMany".into(), json!({"items": [{"id": id.0, "anchors": [[0, 2]]}], "mode": "toggle"}))]);
|
||||
}
|
||||
|
||||
@@ -5794,6 +5794,8 @@ The tool (`smartPen`), option `next`: `"straight"` (default) or `"curved"`, the
|
||||
one toggles its anchor.
|
||||
- Segments drag to reshape (`addon.pen.reshape`; Shift: `symmetric`, Alt: `keepAngles`); Alt-click adds an anchor
|
||||
(`path.insertAnchor`); a click on a path that isn't selected selects it.
|
||||
- With no path being drawn, an Alt-drag away from paths draws a marquee that selects the points of the editable paths
|
||||
inside it (`select.anchorsMany`; with Shift, toggles them).
|
||||
|
||||
Commands:
|
||||
|
||||
|
||||
+3
-2
@@ -1,6 +1,6 @@
|
||||
# UI and interaction parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (Color Effects add-on: Select by Color Kind; scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (Smart Pen add-on: an Alt-drag away from paths selects points with a marquee; scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
|
||||
## Feature submission evidence
|
||||
|
||||
@@ -323,7 +323,7 @@ next unit in the order a right-click on a ruler lists them, from Feet back to Po
|
||||
| Preferences › General › Constrain Angle | done | arrow keys and Shift drags along the turned axes; the Rectangle, Rounded Rectangle, Ellipse and grid tools, point and area type and the shape size dialogs draw along them with the bounding box turned; Shift-drawn polygons and stars keep the turned base orientation; the Line Segment tool's Shift steps count from it (#394, <https://helpx.adobe.com/illustrator/desktop/manage-objects/arrange-objects/rotate-objects-by-specific-angles.html>); arcs, spirals, flares and Pen paths are not turned |
|
||||
| Tabs panel: a stop drags from where the press landed, a stop just added included | done | #1458 |
|
||||
| Outline view keeps template layers in Preview, dimmed | done | #1486; a picture placed to be traced stays visible under the outlines |
|
||||
| Smart Pen add-on (VectorCraft addition): Object › Plug-ins › Smart Pen selects the Smart Pen Tool, which the All Tools drawer lists under its own heading but neither toolbar shows as shipped (no shortcut), and opens the Smart Pen panel (next segment straight or curved; Remove Points Keep Shape, Make Smooth, Make Corner, Retract Handles, Close or Join). The tool draws (click: straight or curved segment, a curve after a straight one tangent; drag: smooth anchor, Alt breaks the handle, Shift 45°, Space moves it; Tab switches a segment; rubber band; close, join, continue) and edits without switching tools (click selects a point, drag moves it, Alt-click removes it keeping the shape, Shift+Alt-click deletes it, double-click toggles smooth/corner; handle drags with Shift and Alt; segment drags reshape, Alt-click adds a point) | done | `addon.pen.*`, `crates/tools/src/addons/smart_pen.rs`, `crates/ui-egui/src/addons/pen.rs` and their tests; not an Illustrator feature, so it is not scored |
|
||||
| Smart Pen add-on (VectorCraft addition): Object › Plug-ins › Smart Pen selects the Smart Pen Tool, which the All Tools drawer lists under its own heading but neither toolbar shows as shipped (no shortcut), and opens the Smart Pen panel (next segment straight or curved; Remove Points Keep Shape, Make Smooth, Make Corner, Retract Handles, Close or Join). The tool draws (click: straight or curved segment, a curve after a straight one tangent; drag: smooth anchor, Alt breaks the handle, Shift 45°, Space moves it; Tab switches a segment; rubber band; close, join, continue) and edits without switching tools (click selects a point, drag moves it, Alt-click removes it keeping the shape, Shift+Alt-click deletes it, double-click toggles smooth/corner; handle drags with Shift and Alt; segment drags reshape, Alt-click adds a point); an Alt-drag away from paths selects the points inside a marquee | done | `addon.pen.*`, `crates/tools/src/addons/smart_pen.rs`, `crates/ui-egui/src/addons/pen.rs` and their tests; not an Illustrator feature, so it is not scored |
|
||||
| Path Precision Tools add-on (VectorCraft addition): Object › Plug-ins › Path Precision Tools selects its three tools (Anchor Eraser Brush, Extend Path, Measure & Inspect), which the All Tools drawer lists under their own heading but neither toolbar shows as shipped, opens the Anchor Inspector panel (the selected anchor's X/Y and handle lengths and angles as fields; Make Smooth, Make Corner, Equalize Handles, Remove Points Keep Shape, Add Points, the three ways to close) and runs Add Points at / Split at Crossings; a double-click on the brush or Extend Path tool opens its options | done | `addon.pathtools.*`, `crates/tools/src/addons/`, `crates/ui-egui/src/addons/pathtools.rs` and their tests; not an Illustrator feature, so it is not scored |
|
||||
| Artwork Cleanup add-on (VectorCraft addition): Object › Plug-ins › Artwork Cleanup opens its panel, which checks the selection or the document for 16 kinds of issue, lists them with counts, and shows or fixes the picked rows or all of them, each fix one undo step; Simplify Anchors (Keep Shape), Rejoin Path Pieces and Deep Ungroup open their options first | done | `addon.cleanup.*`, `crates/ui-egui/src/addons/cleanup.rs` and its tests; not an Illustrator feature, so it is not scored |
|
||||
| Color Effects add-on (VectorCraft addition): live Exposure, Desaturate and Invert Colors in Effect › Color Effects and Object › Plug-ins › Color Effects; its panel picks a colour adjustment and its values and adds it as a live effect or applies it as a filter, into the colours, as one undo step; Select by Color Kind selects the objects that use spot colours, carry colour effects, or are RGB, CMYK or grayscale images | done | `addon.color.*`, `crates/ui-egui/src/addons/color.rs` and its tests; not an Illustrator feature, so it is not scored |
|
||||
@@ -367,6 +367,7 @@ separate. Scores and hours unchanged. [Behavior and command details](development
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-30 | minor | Smart Pen add-on: an Alt-drag away from paths selects points with a marquee; scores and estimates unchanged |
|
||||
| 2026-10-30 | minor | Color Effects add-on: Select by Color Kind; scores and estimates unchanged |
|
||||
| 2026-10-30 | minor | View › Hide Artboards hides their edges and shows the canvas as paper (#4038); scores and estimates unchanged |
|
||||
| 2026-10-30 | minor | Contextual task bar: stays up through a click on another object, hides only for a drag (#4681); scores and estimates unchanged |
|
||||
|
||||
Reference in New Issue
Block a user