mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-29 20:27:05 -06:00
Merge pull request #4380 from dotCooCoo/feat/blob-brush-alt-smooth
Blob Brush: Alt switches to the Smooth tool while it is held
This commit is contained in:
@@ -12,7 +12,8 @@
|
||||
//! pressed less than fully on sends `[x, y, pressure]` points) for pressure-sensitive brushes. The
|
||||
//! other tools collect the drag polyline (shown as an overlay) and run
|
||||
//! one command on release. With Shift held, the Blob Brush's points stay on the horizontal or
|
||||
//! vertical line through the press point.
|
||||
//! vertical line through the press point. Alt held at the press, or before the Blob Brush's drag
|
||||
//! has moved, switches the Blob Brush to the Smooth tool: the drag smooths the selected paths.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_doc::{NodeId, NodeKind};
|
||||
@@ -70,8 +71,11 @@ pub struct GestureTool {
|
||||
/// preview shows the points joined by straight segments and the release fits them (Pencil
|
||||
/// only, on by default).
|
||||
pub live_fit: bool,
|
||||
/// Active drag is smoothing with the Pencil tool.
|
||||
/// Active drag is smoothing with the Pencil or the Blob Brush (Alt held).
|
||||
smoothing: bool,
|
||||
/// The pointer has moved from the press point, by any distance (a move too short to add a
|
||||
/// point counts).
|
||||
moved: bool,
|
||||
/// Active Pencil drag draws a straight line (Shift or Alt held at the press): the stroke is
|
||||
/// the press point and the pointer.
|
||||
straight: bool,
|
||||
@@ -142,6 +146,7 @@ impl GestureTool {
|
||||
live_preview: true,
|
||||
live_fit: true,
|
||||
smoothing: false,
|
||||
moved: false,
|
||||
straight: false,
|
||||
unconstrained: false,
|
||||
shift_raw: vec![],
|
||||
@@ -189,12 +194,18 @@ impl GestureTool {
|
||||
self.id == "pencil" && !(self.alt_smooth && m.alt) && (m.shift || m.alt)
|
||||
}
|
||||
|
||||
/// Would Alt held now switch the Blob Brush to the Smooth tool? It does before a press, and
|
||||
/// during a drag that has not moved from the press point yet.
|
||||
fn blob_alt_smooths(&self, m: Mods) -> bool {
|
||||
self.id == "blobBrush" && m.alt && !self.moved
|
||||
}
|
||||
|
||||
/// Where the stroke takes a pointer at `p`. For the Blob Brush with Shift held, that is the
|
||||
/// horizontal or vertical line through the press point that is nearer in angle to `p` (90°
|
||||
/// steps from the Constrain Angle), at `p`'s distance from the press point. The points dragged
|
||||
/// since Shift went down move to that line too, each at its pointer position's distance and on
|
||||
/// that position's side of the press point; a pointer at the press point leaves them on their
|
||||
/// line. Without Shift the stroke takes `p`.
|
||||
/// line. Without Shift, or while the Blob Brush smooths, the stroke takes `p`.
|
||||
fn constrained(&mut self, cx: &ToolContext, p: Point, m: Mods) -> Point {
|
||||
self.shift_next = None;
|
||||
if self.straight && self.unconstrained {
|
||||
@@ -205,7 +216,7 @@ impl GestureTool {
|
||||
{
|
||||
return start + vectorcraft_geom::constrain_angle_from(p - start, 45.0, cx.constrain_angle);
|
||||
}
|
||||
let Some(&start) = self.points.first().filter(|_| self.id == "blobBrush" && m.shift) else {
|
||||
let Some(&start) = self.points.first().filter(|_| self.id == "blobBrush" && m.shift && !self.smoothing) else {
|
||||
self.shift_raw.clear();
|
||||
self.shift_line = None;
|
||||
return p;
|
||||
@@ -230,6 +241,7 @@ impl GestureTool {
|
||||
|
||||
/// Forget the stroke.
|
||||
fn clear(&mut self) {
|
||||
self.moved = false;
|
||||
self.points.clear();
|
||||
self.pressures.clear();
|
||||
self.previewed = 0;
|
||||
@@ -413,7 +425,9 @@ impl Tool for GestureTool {
|
||||
self.active = true;
|
||||
self.began = false;
|
||||
self.near_start = false;
|
||||
self.smoothing = self.id == "pencil" && self.alt_smooth && ev.mods.alt;
|
||||
// Alt switches the Pencil to the Smooth tool while Alt key toggles to Smooth Tool is on, and always
|
||||
// switches the Blob Brush.
|
||||
self.smoothing = ev.mods.alt && (self.id == "blobBrush" || (self.id == "pencil" && self.alt_smooth));
|
||||
// Alt draws a straight line only while it doesn't lend the Smooth tool; with Shift
|
||||
// too, the line keeps Shift's 45° steps.
|
||||
self.straight = self.id == "pencil" && !self.smoothing && (ev.mods.shift || ev.mods.alt);
|
||||
@@ -443,10 +457,11 @@ impl Tool for GestureTool {
|
||||
if !self.active {
|
||||
return vec![];
|
||||
}
|
||||
let p = self.constrained(cx, p, ev.mods);
|
||||
if !self.began && !self.straight && self.id == "pencil" && self.alt_smooth && ev.mods.alt {
|
||||
if (!self.began && !self.straight && self.id == "pencil" && self.alt_smooth && ev.mods.alt) || self.blob_alt_smooths(ev.mods) {
|
||||
self.smoothing = true;
|
||||
}
|
||||
self.moved |= self.points.first().is_some_and(|start| p.distance(*start) > vectorcraft_geom::EPS);
|
||||
let p = self.constrained(cx, p, ev.mods);
|
||||
let mut end = p;
|
||||
if self.snaps() && !self.smoothing && !self.straight {
|
||||
// Where the stroke would end: shown, taken on release.
|
||||
@@ -504,6 +519,12 @@ impl Tool for GestureTool {
|
||||
if !self.active {
|
||||
return vec![];
|
||||
}
|
||||
// A release away from the press point counts as movement.
|
||||
self.moved |= self.points.first().is_some_and(|start| p.distance(*start) > vectorcraft_geom::EPS);
|
||||
// Alt pressed while the pointer stood still at the press point switches the release too.
|
||||
if self.blob_alt_smooths(ev.mods) {
|
||||
self.smoothing = true;
|
||||
}
|
||||
let p = self.constrained(cx, p, ev.mods);
|
||||
let end = if self.snaps() && !self.smoothing && (!self.straight || self.unconstrained) { self.snap.drag(cx, p, None) } else { p };
|
||||
self.snap.clear();
|
||||
@@ -585,6 +606,7 @@ impl Tool for GestureTool {
|
||||
self.active = false;
|
||||
self.near_start = false;
|
||||
self.began = false;
|
||||
self.smoothing = false;
|
||||
self.straight = false;
|
||||
self.unconstrained = false;
|
||||
self.clear();
|
||||
@@ -596,8 +618,9 @@ impl Tool for GestureTool {
|
||||
if !self.active || self.points.len() < 2 || (self.began && self.previews()) {
|
||||
return o;
|
||||
}
|
||||
// A Blob Brush drag that smooths shows the Smooth tool's thin line.
|
||||
let width = match self.id {
|
||||
"blobBrush" | "eraser" => (self.size * cx.zoom) as f32,
|
||||
"blobBrush" | "eraser" if !self.smoothing => (self.size * cx.zoom) as f32,
|
||||
_ => 1.0,
|
||||
};
|
||||
let color = if self.id == "eraser" { [0x80, 0x80, 0x80] } else { FEEDBACK };
|
||||
@@ -608,7 +631,7 @@ impl Tool for GestureTool {
|
||||
return o;
|
||||
}
|
||||
o.push(Overlay::Path { path: polyline(&self.points), color, width, dashed: self.id == "knife" });
|
||||
if cx.measurement_labels && self.id == "blobBrush" {
|
||||
if cx.measurement_labels && self.id == "blobBrush" && !self.smoothing {
|
||||
let last = self.points[self.points.len() - 1];
|
||||
let length = self.points.windows(2).map(|w| w[0].distance(w[1])).sum();
|
||||
o.push(Overlay::Measure { p: last + vectorcraft_geom::Vec2::new(cx.tol(12.0), cx.tol(12.0)), text: format!("L: {}", cx.len(length)) });
|
||||
@@ -616,9 +639,9 @@ impl Tool for GestureTool {
|
||||
o
|
||||
}
|
||||
fn cursor(&self, cx: &ToolContext, p: Point, m: Mods) -> Cursor {
|
||||
// A drag that smooths, or Alt before a Pencil stroke has begun to draw (a stroke that
|
||||
// has begun keeps drawing with Alt).
|
||||
if self.smoothing || (self.id == "pencil" && self.alt_smooth && m.alt && !self.began) {
|
||||
// A drag that smooths, or Alt before a Pencil or Blob Brush stroke has begun to draw (a
|
||||
// stroke that has begun keeps drawing with Alt).
|
||||
if self.smoothing || (self.id == "pencil" && self.alt_smooth && m.alt && !self.began) || self.blob_alt_smooths(m) {
|
||||
return Cursor::Smooth;
|
||||
}
|
||||
// Before a press that would draw a straight line: over an end of a selected open path,
|
||||
|
||||
@@ -336,6 +336,90 @@ fn blob_brush_shift_keeps_the_stroke_horizontal_or_vertical() {
|
||||
assert!(pts.len() == 3 && pts[2] == (0.0, 0.0) && (a - 30.0).abs() < 1e-9, "{pts:?}");
|
||||
}
|
||||
|
||||
/// Alt switches the Blob Brush to the Smooth tool: held before the press, or pressed before the
|
||||
/// drag has moved, the cursor shows the Smooth tool, the drag shows a thin line and its release
|
||||
/// smooths the selected paths (`path.smoothRegion`) instead of painting. Shift does not
|
||||
/// constrain a smoothing drag, a stroke that has moved keeps painting when Alt goes down, and
|
||||
/// with nothing selected the release changes no path.
|
||||
#[test]
|
||||
fn blob_brush_alt_switches_to_the_smooth_tool() {
|
||||
let (d, id) = doc_with_rect();
|
||||
let mut s = Selection::default();
|
||||
s.set([id]);
|
||||
let p = paint();
|
||||
let cx = ToolContext { measurement_labels: true, ..cx(&d, &s, &p) };
|
||||
let alt = Mods { alt: true, ..Default::default() };
|
||||
let at = Point::new(50.0, 50.0);
|
||||
let smoothed = |acts: &[Vec<Action>]| -> Option<Value> {
|
||||
match acts.last().and_then(|a| a.first()) {
|
||||
Some(Action::Exec(c, v)) if c == "path.smoothRegion" => Some(v.clone()),
|
||||
_ => None,
|
||||
}
|
||||
};
|
||||
let mut t = GestureTool::new("blobBrush");
|
||||
assert_eq!(t.cursor(&cx, at, alt), Cursor::Smooth);
|
||||
assert_eq!(t.cursor(&cx, at, Mods::default()), Cursor::BlobBrush);
|
||||
let stroke = [(50.0, 150.0), (150.0, 150.0), (250.0, 160.0)];
|
||||
let acts = drag_held(&mut t, &cx, &stroke, alt);
|
||||
assert!(acts[..acts.len() - 1].iter().all(|a| a.is_empty()), "{acts:?}");
|
||||
let v = smoothed(&acts).unwrap_or_else(|| panic!("{acts:?}"));
|
||||
assert_eq!(v, json!({"points": [[50.0, 150.0], [150.0, 150.0], [250.0, 160.0]], "radius": cx.tol(12.0), "fidelity": 1.5}));
|
||||
// Mid-drag the cursor stays the Smooth tool's when Alt is let go, and the line is thin with
|
||||
// no length label.
|
||||
t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 50.0, 150.0).with_mods(alt));
|
||||
t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 150.0, 150.0).with_mods(alt));
|
||||
assert_eq!(t.cursor(&cx, at, Mods::default()), Cursor::Smooth);
|
||||
let o = t.overlays(&cx);
|
||||
assert!(o.iter().any(|o| matches!(o, Overlay::Path { width, .. } if *width == 1.0)), "{o:?}");
|
||||
assert!(!o.iter().any(|o| matches!(o, Overlay::Measure { .. })), "{o:?}");
|
||||
// After another tool is chosen mid-drag, the Blob Brush shows its own cursor when it is chosen
|
||||
// again, and so does the Pencil after a drag that Alt key toggles to Smooth Tool made smooth.
|
||||
assert!(t.deactivate(&cx).is_empty());
|
||||
assert_eq!(t.cursor(&cx, at, Mods::default()), Cursor::BlobBrush);
|
||||
let mut pencil = GestureTool::new("pencil");
|
||||
pencil.set_option("altSmooth", &json!(true));
|
||||
pencil.pointer(&cx, &PointerEvent::new(PointerKind::Down, 50.0, 150.0).with_mods(alt));
|
||||
pencil.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 150.0, 150.0).with_mods(alt));
|
||||
assert!(pencil.deactivate(&cx).is_empty());
|
||||
assert_eq!(pencil.cursor(&cx, at, Mods::default()), Cursor::PencilNew);
|
||||
// Alt pressed before the drag has moved smooths too, and Shift leaves the points where the
|
||||
// pointer went.
|
||||
let alt_shift = Mods { alt: true, shift: true, ..Default::default() };
|
||||
t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 50.0, 150.0));
|
||||
t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 150.0, 170.0).with_mods(alt_shift));
|
||||
let acts = [t.pointer(&cx, &PointerEvent::new(PointerKind::Up, 250.0, 160.0).with_mods(alt_shift))];
|
||||
let v = smoothed(&acts).unwrap_or_else(|| panic!("{acts:?}"));
|
||||
assert_eq!(v["points"], json!([[50.0, 150.0], [150.0, 170.0], [250.0, 160.0]]));
|
||||
// A stroke that has moved keeps painting with Alt down.
|
||||
t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 50.0, 150.0));
|
||||
t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 150.0, 150.0));
|
||||
assert_eq!(t.cursor(&cx, at, alt), Cursor::BlobBrush);
|
||||
t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 200.0, 150.0).with_mods(alt));
|
||||
let acts = t.pointer(&cx, &PointerEvent::new(PointerKind::Up, 250.0, 150.0).with_mods(alt));
|
||||
assert!(matches!(acts.as_slice(), [Action::Exec(c, v)] if c == "path.blob" && v["points"].as_array().unwrap().len() == 4), "{acts:?}");
|
||||
// So does a stroke that has moved too little to add a point.
|
||||
t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 50.0, 150.0));
|
||||
t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 50.25, 150.0));
|
||||
assert_eq!(t.cursor(&cx, at, alt), Cursor::BlobBrush);
|
||||
t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 150.0, 150.0).with_mods(alt));
|
||||
let acts = t.pointer(&cx, &PointerEvent::new(PointerKind::Up, 250.0, 150.0).with_mods(alt));
|
||||
assert!(matches!(acts.as_slice(), [Action::Exec(c, _)] if c == "path.blob"), "{acts:?}");
|
||||
// Alt pressed while the pointer stands still at the press point does not paint on the release.
|
||||
t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 50.0, 150.0));
|
||||
assert_eq!(t.cursor(&cx, at, alt), Cursor::Smooth);
|
||||
let acts = t.pointer(&cx, &PointerEvent::new(PointerKind::Up, 50.0, 150.0).with_mods(alt));
|
||||
assert!(acts.is_empty(), "{acts:?}");
|
||||
// A release away from the press point with no drag before it paints when Alt goes down.
|
||||
t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 50.0, 150.0));
|
||||
let acts = t.pointer(&cx, &PointerEvent::new(PointerKind::Up, 150.0, 150.0).with_mods(alt));
|
||||
assert!(matches!(acts.as_slice(), [Action::Exec(c, _)] if c == "path.blob"), "{acts:?}");
|
||||
// With nothing selected an Alt drag sends nothing.
|
||||
let none = Selection::default();
|
||||
let empty = crate::testutil::cx(&d, &none, &p);
|
||||
let acts = drag_held(&mut t, &empty, &stroke, alt);
|
||||
assert!(acts.iter().all(|a| a.is_empty()), "{acts:?}");
|
||||
}
|
||||
|
||||
/// #3799: Shift held at the press draws a straight Pencil line from the press point, at 45°
|
||||
/// steps from the Constrain Angle, previewed as it is dragged; the Paintbrush keeps following
|
||||
/// the hand.
|
||||
|
||||
@@ -642,6 +642,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- **Drawing modes:** in Draw Inside, a double-click with a selection tool outside the object being drawn into sets the drawing mode to Draw Normal, as Shift+D does. Pastes follow the drawing mode ([drawing modes](https://helpx.adobe.com/illustrator/desktop/draw-shapes-and-paths/learn-drawing-basics/drawing-modes-overview.html)) (#3705): in Draw Inside, Paste, Paste in Place and Paste on All Artboards put the copies inside the object drawn into, in one undo step, Paste Remembers Layers aside; in Draw Behind, Paste in Place and Paste on All Artboards put them just below the selection; Paste in Front and Paste in Back ignore the mode. Shift+D cycles Draw Normal → Draw Behind → Draw Inside → Draw Normal, skipping Draw Inside unless one path, compound path or type is selected. With the object drawn into isolated (a double-click on it with the Selection tool), art drawn or pasted inside it takes isolation mode into its clip group, so it can be clicked and isn't dimmed, as art added in isolation mode belongs with what is isolated ([isolation mode](https://helpx.adobe.com/illustrator/desktop/manage-objects/select-objects/isolate-objects.html)) (#3705); undo or redo that removes the isolated object ends isolation mode.
|
||||
- **Drawing:** Pen, Curvature, anchor tools, Pencil, Paintbrush, Blob Brush, Smooth, Path Eraser, Join. Direction handles dragged with Direct Selection or the Anchor Point tool snap to smart guides, and Shift keeps them at 45° steps from their anchor. The Pen continues any open path from the end clicked, and while drawing, a click on an end of another open path joins the two into one (#776). The Curvature tool edits any selected path, whatever drew it, keeping its shape except where edited (#798): a click on a point makes it current and a drag moves it, re-curving only its two segments; Alt-click or double-click toggles it smooth or corner and Delete removes it; a click on a segment adds a point without changing the shape; with an end of an open path current, clicks go on from it and its other end closes it (`path.curvatureEdit`). Every point a drawing tool places snaps to Smart Guides through one snapper (#506): the Pen's and Curvature tool's anchors, the Pencil's ends, the shape tools' start and dragged corner or radius (Rectangle, Rounded Rectangle, Ellipse, Polygon, Star, Line Segment, Arc, Spiral, the grids), the Type tool's click and frame, a new artboard's corners. A filled Pencil path keeps a stroke of None (#1804, part of #1679). While a Pencil or Paintbrush stroke is drawn back to within Close Paths When Ends Are Within of its start, the cursor shows the closing ring and a release there closes the path, as [Draw straight lines with the Pencil tool](https://helpx.adobe.com/illustrator/desktop/draw-shapes-and-paths/draw-shapes/draw-straight-lines-with-the-pencil-tool.html) describes for the Pencil. The [ and ] keys reach the Blob Brush, the Eraser and the Symbolism tools in the app (#1734, part of #1661).
|
||||
With the Blob Brush, Shift held during a drag keeps the stroke on the horizontal or vertical line through the press point, whichever is nearer to the pointer (90° steps from the Constrain Angle preference), and the points dragged since Shift went down move to the other line when the pointer crosses to it; the points dragged while Shift is up follow the pointer.
|
||||
With the Blob Brush, Alt held before the press, or pressed before the drag has moved, switches to the Smooth tool, as the reference app's [Default keyboard shortcuts](https://web.archive.org/web/20260726060015/https://helpx.adobe.com/illustrator/using/default-keyboard-shortcuts.html) page lists: the cursor shows the Smooth tool, the drag shows a thin line without the length label, Shift does not constrain it, and the release smooths the selected paths under the drag (`path.smoothRegion`) instead of painting; with nothing selected the release changes no path. A stroke that has moved keeps painting when Alt goes down.
|
||||
Curvature point markers are circular, with no direction handles. While drawing, its own point feedback replaces generic selection markers; existing paths use the selection overlay, including partial selections and Hide Edges. Anchor Size applies, and Direct Selection and Pen retain their handles. The real egui-frame regressions in `crates/ui-egui/src/canvas.rs` verify marker positions, counts and unchanged documents (#1236, in part); the existing-path interpolation request remains open. The curve through the points (`curvature_spline` in `crates/tools/src/draw2.rs`) makes each stretch of three points follow the circle through them: a smooth point's handles lie along that circle's tangent, each `chord / (3 cos²(α/2))` long (α between handle and chord, capped at 90°), and the ends of an open path mirror their neighbour's tangent in their chord, so a J or a U curves evenly, a turn tighter than 30° comes to a point, and uneven spacing doesn't loop; the rubber band, a dragged or inserted point and `path.convertAnchor` to smooth use the same rule, while the Pencil's fitting keeps its own; `tests/draw2.rs` checks that the rubber band is the path the click makes (#1236, in part). The Smooth tool smooths only the stretch of path under the drag (#1547, fixes #1512).
|
||||
While Alt is held, the Paintbrush (before a press and while drawing) shows a closing cursor (a crosshair with a ring), and so does the Pencil with Alt key toggles to Smooth Tool off when Alt is pressed after the drag starts; releasing the mouse button with Alt still held closes the path. A Pencil drag started with Shift or Alt held draws a straight line instead, which stays open.
|
||||
With Shift held at the press, the Pencil draws a straight line from the press point to the pointer at 45° steps from the Constrain Angle, as [Draw straight lines with the Pencil tool](https://helpx.adobe.com/illustrator/desktop/draw-shapes-and-paths/draw-shapes/draw-straight-lines-with-the-pencil-tool.html) describes; pressed on an end of a selected open path, the line continues the path from that end (part of #3799). With Option/Alt held at the press and Alt key toggles to Smooth Tool off, the Pencil draws a straight line at any angle, whose ends snap to Smart Guides, and it stays open when Alt is still held at the release; Shift and Alt together keep the 45° steps (#3799). While Shift or Alt is held before the press, the Pencil shows a straight-line cursor (a crosshair with a short line), and over an end of a selected open path, where the line continues the path, a path-continuation cursor (the same line leaving a small square), as the page describes.
|
||||
|
||||
Reference in New Issue
Block a user