mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
Pencil: Shift held at the press draws a straight line at 45° steps (part of #3799)
This commit is contained in:
committed by
bflatastic
parent
b3673b7f32
commit
c39be04573
@@ -116,6 +116,31 @@ fn pencil_alt_closes_path() {
|
||||
assert!(ps[0].1.is_closed());
|
||||
}
|
||||
|
||||
/// #3799: with Shift held, a wavy Pencil drag draws one straight segment at 45° steps from the
|
||||
/// Constrain Angle (here 30°: a drag at about 60° comes out at 75°).
|
||||
#[test]
|
||||
fn pencil_with_shift_draws_a_straight_line() {
|
||||
let mut s = session();
|
||||
s.execute("prefs.set", &json!({"key": "constrainAngle", "value": 30})).unwrap();
|
||||
let v = view();
|
||||
let shift = Mods { shift: true, ..Default::default() };
|
||||
s.select_tool("pencil", v).unwrap();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Down, 100.0, 400.0).with_mods(shift), v).unwrap();
|
||||
for (x, y) in [(120.0, 380.0), (125.0, 330.0), (160.0, 320.0), (150.0, 313.4)] {
|
||||
s.pointer(&PointerEvent::new(PointerKind::Drag, x, y).with_mods(shift), v).unwrap();
|
||||
}
|
||||
s.pointer(&PointerEvent::new(PointerKind::Up, 150.0, 313.4).with_mods(shift), v).unwrap();
|
||||
let ps = paths(&s);
|
||||
assert_eq!(ps.len(), 1);
|
||||
let sp = &ps[0].1.subpaths[0];
|
||||
assert_eq!(sp.anchors.len(), 2, "{sp:?}");
|
||||
assert!(sp.anchors.iter().all(|a| a.h_in == a.p && a.h_out == a.p), "no handles: {sp:?}");
|
||||
let (a, b) = (sp.anchors[0].p, sp.anchors[1].p);
|
||||
assert!(a.distance(Point::new(100.0, 400.0)) < 1e-6, "{a:?}");
|
||||
let angle = (-(b.y - a.y)).atan2(b.x - a.x).to_degrees();
|
||||
assert!((angle - 75.0).abs() < 1e-6, "{angle}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pencil_continues_selected_open_path() {
|
||||
let mut s = session();
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
//!
|
||||
//! Pencil and Paintbrush preview the fitted path live (`path.freehand` re-applied on the snapshot)
|
||||
//! and commit on release; Alt on release (or ending near the start) closes the path, and starting
|
||||
//! near an end of a selected open path continues it. The Pencil's new path starts and ends on
|
||||
//! near an end of a selected open path continues it. Shift held at the press draws a straight
|
||||
//! Pencil line at 45° steps from the Constrain Angle instead. The Pencil's new path starts and ends on
|
||||
//! Smart Guides ([`DrawSnap`]: an anchor, a path, in line with the art), hovering too; the points
|
||||
//! between follow the hand. The Paintbrush records the pen pressure of each point (a stroke a pen
|
||||
//! pressed less than fully on sends `[x, y, pressure]` points) for pressure-sensitive brushes. The
|
||||
@@ -45,6 +46,9 @@ pub struct GestureTool {
|
||||
pub alt_smooth: bool,
|
||||
/// Active drag is smoothing with the Pencil tool.
|
||||
smoothing: bool,
|
||||
/// Active Pencil drag draws a straight line (Shift held at the press): the stroke is the
|
||||
/// press point and the pointer constrained to 45° steps from it.
|
||||
straight: bool,
|
||||
/// Where the pointer was for each Blob Brush point dragged since Shift went down: the last
|
||||
/// `shift_raw.len()` of `points`.
|
||||
shift_raw: Vec<Point>,
|
||||
@@ -88,6 +92,7 @@ impl GestureTool {
|
||||
// Off, as Pencil Tool Options ships it: Alt keeps drawing a closed path until it is on.
|
||||
alt_smooth: false,
|
||||
smoothing: false,
|
||||
straight: false,
|
||||
shift_raw: vec![],
|
||||
shift_next: None,
|
||||
merge_only_selection: false,
|
||||
@@ -128,6 +133,11 @@ impl GestureTool {
|
||||
/// line. Without Shift the stroke takes `p`.
|
||||
fn constrained(&mut self, cx: &ToolContext, p: Point, m: Mods) -> Point {
|
||||
self.shift_next = None;
|
||||
if self.straight
|
||||
&& let Some(&start) = self.points.first()
|
||||
{
|
||||
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 {
|
||||
self.shift_raw.clear();
|
||||
return p;
|
||||
@@ -176,8 +186,9 @@ impl GestureTool {
|
||||
}
|
||||
}
|
||||
|
||||
/// The selected open path whose end is near `p` (Edit Selected Paths, off at 0 pixels).
|
||||
fn find_extend(&self, cx: &ToolContext, p: Point) -> Option<(NodeId, bool)> {
|
||||
/// The selected open path whose end is near `p` (Edit Selected Paths, off at 0 pixels), and
|
||||
/// that end.
|
||||
fn find_extend(&self, cx: &ToolContext, p: Point) -> Option<(NodeId, bool, Point)> {
|
||||
if self.edit_within <= 0.0 {
|
||||
return None;
|
||||
}
|
||||
@@ -191,10 +202,10 @@ impl GestureTool {
|
||||
let Some(sp) = pd.subpaths.iter().rev().find(|s| !s.closed && !s.anchors.is_empty()) else { continue };
|
||||
let (first, last) = (sp.anchors[0].p, sp.anchors[sp.anchors.len() - 1].p);
|
||||
if p.distance(last) <= tol {
|
||||
return Some((*id, false));
|
||||
return Some((*id, false, last));
|
||||
}
|
||||
if p.distance(first) <= tol {
|
||||
return Some((*id, true));
|
||||
return Some((*id, true, first));
|
||||
}
|
||||
}
|
||||
None
|
||||
@@ -289,10 +300,15 @@ impl Tool for GestureTool {
|
||||
self.active = true;
|
||||
self.began = false;
|
||||
self.smoothing = self.id == "pencil" && self.alt_smooth && ev.mods.alt;
|
||||
self.extend = if self.draws_path() && !self.smoothing { self.find_extend(cx, p) } else { None };
|
||||
self.straight = self.id == "pencil" && !self.smoothing && ev.mods.shift;
|
||||
let extend = if self.draws_path() && !self.smoothing { self.find_extend(cx, p) } else { None };
|
||||
self.extend = extend.map(|(id, at_start, _)| (id, at_start));
|
||||
self.redraw = if self.draws_path() && !self.smoothing && self.extend.is_none() { self.find_redraw(cx, p) } else { None };
|
||||
// A path continued starts at its end, one redrawn on it; a new one where it snaps.
|
||||
let start = if self.snaps() && !self.smoothing && self.extend.is_none() && self.redraw.is_none() {
|
||||
// A straight line is constrained from the end it continues, so it starts there.
|
||||
let start = if let Some((_, _, end)) = extend.filter(|_| self.straight) {
|
||||
end
|
||||
} else if self.snaps() && !self.smoothing && self.extend.is_none() && self.redraw.is_none() {
|
||||
self.snap.press(cx, p, &[], None)
|
||||
} else {
|
||||
p
|
||||
@@ -309,11 +325,17 @@ impl Tool for GestureTool {
|
||||
if !self.began && self.id == "pencil" && self.alt_smooth && ev.mods.alt {
|
||||
self.smoothing = true;
|
||||
}
|
||||
if self.snaps() && !self.smoothing {
|
||||
if self.snaps() && !self.smoothing && !self.straight {
|
||||
// Where the stroke would end: shown, taken on release.
|
||||
self.snap.drag(cx, p, None);
|
||||
}
|
||||
if self.points.last().is_some_and(|l| l.distance(p) < cx.tol(1.0)) {
|
||||
if self.straight {
|
||||
// The line runs from the press point to the pointer, wherever the drag went.
|
||||
if self.points.first().is_some_and(|s| s.distance(p) < cx.tol(1.0)) {
|
||||
return vec![];
|
||||
}
|
||||
self.points.truncate(1);
|
||||
} else if self.points.last().is_some_and(|l| l.distance(p) < cx.tol(1.0)) {
|
||||
return vec![];
|
||||
}
|
||||
self.push(p, ev.pressure);
|
||||
@@ -333,9 +355,14 @@ impl Tool for GestureTool {
|
||||
return vec![];
|
||||
}
|
||||
let p = self.constrained(cx, p, ev.mods);
|
||||
let end = if self.snaps() && !self.smoothing { self.snap.drag(cx, p, None) } else { p };
|
||||
let end = if self.snaps() && !self.smoothing && !self.straight { self.snap.drag(cx, p, None) } else { p };
|
||||
self.snap.clear();
|
||||
if self.points.last().is_some_and(|l| l.distance(p) >= cx.tol(1.0)) {
|
||||
if self.straight {
|
||||
if self.points.first().is_some_and(|s| s.distance(p) >= cx.tol(1.0)) {
|
||||
self.points.truncate(1);
|
||||
self.push(end, ev.pressure);
|
||||
}
|
||||
} else if self.points.last().is_some_and(|l| l.distance(p) >= cx.tol(1.0)) {
|
||||
self.push(end, ev.pressure);
|
||||
} else if self.snaps()
|
||||
&& !self.smoothing
|
||||
@@ -349,6 +376,7 @@ impl Tool for GestureTool {
|
||||
self.active = false;
|
||||
self.began = false;
|
||||
self.smoothing = false;
|
||||
self.straight = false;
|
||||
self.clear();
|
||||
self.extend = None;
|
||||
self.redraw = None;
|
||||
@@ -369,6 +397,7 @@ impl Tool for GestureTool {
|
||||
self.active = false;
|
||||
self.began = false;
|
||||
self.smoothing = false;
|
||||
self.straight = false;
|
||||
self.clear();
|
||||
if began { vec![Action::Cancel] } else { vec![] }
|
||||
}
|
||||
@@ -388,6 +417,7 @@ impl Tool for GestureTool {
|
||||
self.snap.clear();
|
||||
self.active = false;
|
||||
self.began = false;
|
||||
self.straight = false;
|
||||
self.clear();
|
||||
if began { vec![Action::Commit] } else { vec![] }
|
||||
}
|
||||
@@ -694,6 +724,74 @@ mod tests {
|
||||
assert!(pts.len() == 3 && pts[2] == (0.0, 0.0) && (a - 30.0).abs() < 1e-9, "{pts:?}");
|
||||
}
|
||||
|
||||
/// #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.
|
||||
#[test]
|
||||
fn pencil_shift_draws_a_straight_line_at_45_degree_steps() {
|
||||
let (d, _) = doc_with_rect();
|
||||
let s = Selection::default();
|
||||
let p = paint();
|
||||
let mut cx = cx(&d, &s, &p);
|
||||
cx.smart_guides = false;
|
||||
let shift = Mods { shift: true, ..Default::default() };
|
||||
let pts = |acts: &[Vec<Action>]| -> Vec<Vec<(f64, f64)>> {
|
||||
acts.iter()
|
||||
.filter_map(|a| a.iter().find_map(|a| if let Action::Preview(_, v) = a { Some(v["points"].clone()) } else { None }))
|
||||
.map(|v| v.as_array().unwrap().iter().map(|q| (q[0].as_f64().unwrap(), q[1].as_f64().unwrap())).collect())
|
||||
.collect()
|
||||
};
|
||||
let angle = |q: (f64, f64), from: (f64, f64)| (-(q.1 - from.1)).atan2(q.0 - from.0).to_degrees();
|
||||
let mut t = GestureTool::new("pencil");
|
||||
// A wavy drag heading up and right at about 40°: a line at 45°, as long as the drag.
|
||||
let acts = drag_held(&mut t, &cx, &[(0.0, 0.0), (30.0, -10.0), (50.0, -60.0), (100.0, -84.0)], shift);
|
||||
let all = pts(&acts);
|
||||
assert!(all.iter().all(|v| v.len() == 2 && v[0] == (0.0, 0.0)), "{all:?}");
|
||||
let end = *all.last().unwrap().last().unwrap();
|
||||
assert!((angle(end, (0.0, 0.0)) - 45.0).abs() < 1e-9, "{end:?}");
|
||||
assert!((end.0.hypot(end.1) - 100f64.hypot(84.0)).abs() < 1e-9, "{end:?}");
|
||||
let last = acts.last().unwrap();
|
||||
assert!(matches!(&last[0], Action::Preview(_, v) if v["closed"] == false) && last[1] == Action::Commit, "{last:?}");
|
||||
// The steps turn with the Constrain Angle: at 30°, a drag at 40° gives 30°, one at 60° gives 75°.
|
||||
cx.constrain_angle = 30.0;
|
||||
for (to, want) in [((100.0, -84.0), 30.0), ((50.0, -86.6), 75.0)] {
|
||||
let all = pts(&drag_held(&mut t, &cx, &[(10.0, 10.0), (to.0 + 10.0, to.1 + 10.0)], shift));
|
||||
let end = *all.last().unwrap().last().unwrap();
|
||||
assert!((angle(end, (10.0, 10.0)) - want).abs() < 1e-9, "{to:?}: {all:?}");
|
||||
}
|
||||
// Without Shift, and with the Paintbrush, the stroke follows the hand.
|
||||
for (id, m) in [("pencil", Mods::default()), ("paintbrush", shift)] {
|
||||
let mut t = GestureTool::new(id);
|
||||
let all = pts(&drag_held(&mut t, &cx, &[(0.0, 0.0), (30.0, -10.0), (50.0, -60.0)], m));
|
||||
assert_eq!(all.last().unwrap().len(), 3, "{id}: {all:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// #3799: a Shift line started on an end of a selected open path continues the path from that
|
||||
/// end, constrained from the end itself.
|
||||
#[test]
|
||||
fn pencil_shift_line_continues_a_selected_path_from_its_end() {
|
||||
let (mut d, _) = doc_with_rect();
|
||||
let l = d.layers[0].id;
|
||||
let id = d.alloc_id();
|
||||
let line = vectorcraft_geom::PathData::from_bezpath(&vectorcraft_geom::BezPath::from_vec(vec![
|
||||
vectorcraft_geom::PathEl::MoveTo(Point::new(300.0, 300.0)),
|
||||
vectorcraft_geom::PathEl::LineTo(Point::new(400.0, 300.0)),
|
||||
]));
|
||||
d.insert(Some(l), 1, vectorcraft_doc::Node::path(id, line, vectorcraft_doc::Appearance::default_art())).unwrap();
|
||||
let mut s = Selection::default();
|
||||
s.set([id]);
|
||||
let p = paint();
|
||||
let mut cx = cx(&d, &s, &p);
|
||||
cx.smart_guides = false;
|
||||
let mut t = GestureTool::new("pencil");
|
||||
// Pressed 5 pt off the end, released 100 pt right and 4 pt above it.
|
||||
let acts = drag_held(&mut t, &cx, &[(405.0, 303.0), (450.0, 290.0), (500.0, 296.0)], Mods { shift: true, ..Default::default() });
|
||||
let Some(Action::Preview(_, v)) = acts.last().and_then(|a| a.first()) else { panic!("{acts:?}") };
|
||||
assert_eq!(v["extend"], json!({"id": id.0, "end": "end"}));
|
||||
assert_eq!(v["points"], json!([[400.0, 300.0], [400.0 + 100f64.hypot(4.0), 300.0]]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pencil_alt_closes_and_continues_selected_path() {
|
||||
let (d, id) = doc_with_rect();
|
||||
|
||||
File diff suppressed because one or more lines are too long
+3
-2
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user