mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-15 01:15:25 +00:00
Merge pull request #7380 from ncic/pencil-cursors
Pencil: shows where a stroke continues, connects and closes
This commit is contained in:
@@ -11,7 +11,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Pencil",
|
||||
[],
|
||||
None,
|
||||
"{points: [[x,y]…] or [[x,y,pressure]…] or [[x,y,pressure,tiltX,tiltY,rotation]…] (pen pressure 0..1: the stroke keeps it, for Calligraphic and Scatter brushes with Pressure variation; a point without one presses fully. Pen tilt in degrees, each −90..90, x positive leaning right and y leaning down the page, and barrel rotation in degrees clockwise: the stroke keeps how far the pen leaned, which way and how far its barrel turned, for brushes with Tilt, Bearing and Rotation variation; null for what a point lacks), fidelity?: pt (1.5; from 0.000015625 (0.01 / 640) to 100 / 0.0313 (about 3194.89), the Pencil's and the Paintbrush's 0.01 to 100 px Fidelity at the highest and the lowest zoom; a value outside that range is an error. The tools send their Fidelity in pixels divided by the zoom, kept within the range), fit?: bool (true; false: the points are joined by straight segments, as the Pencil previews a stroke with Live curve fitting off), closed?, style?: \"pencil\"|\"brush\", fill?: bool, keepSelected?: bool (true: the drawn or continued path is selected; false: a new path leaves nothing selected, and a continued or redrawn one is deselected while the rest of the selection stays), extend?: {id, end: \"start\"|\"end\"}, connect?: {id, end: \"start\"|\"end\"}, redraw?: {id, within?: pt (12)}} fit a freehand stroke → {id}. extend continues the path from that end; with connect, the stroke ends on that end of another path's last open subpath and joins it on, in one path with the extended path's appearance (the other path is removed when nothing of it is left); redraw (Edit Selected Paths) replaces the section of the path between where the stroke starts and ends, each within `within` of one subpath (the shorter way round a closed one), and draws a new path when the stroke doesn't end on it",
|
||||
"{points: [[x,y]…] or [[x,y,pressure]…] or [[x,y,pressure,tiltX,tiltY,rotation]…] (pen pressure 0..1: the stroke keeps it, for Calligraphic and Scatter brushes with Pressure variation; a point without one presses fully. Pen tilt in degrees, each −90..90, x positive leaning right and y leaning down the page, and barrel rotation in degrees clockwise: the stroke keeps how far the pen leaned, which way and how far its barrel turned, for brushes with Tilt, Bearing and Rotation variation; null for what a point lacks), fidelity?: pt (1.5; from 0.000015625 (0.01 / 640) to 100 / 0.0313 (about 3194.89), the Pencil's and the Paintbrush's 0.01 to 100 px Fidelity at the highest and the lowest zoom; a value outside that range is an error. The tools send their Fidelity in pixels divided by the zoom, kept within the range), fit?: bool (true; false: the points are joined by straight segments, as the Pencil previews a stroke with Live curve fitting off), closed?, style?: \"pencil\"|\"brush\", fill?: bool, keepSelected?: bool (true: the drawn or continued path is selected; false: a new path leaves nothing selected, and a continued or redrawn one is deselected while the rest of the selection stays), extend?: {id, end: \"start\"|\"end\"}, connect?: {id, end: \"start\"|\"end\"}, redraw?: {id, within?: pt (12)}} fit a freehand stroke → {id}. extend continues the path from that end, and closes it when `closed` is true (left out, when the stroke ends within four Fidelities of the path's other end, 6 to 400 pt); with connect, the stroke ends on that end of another path's last open subpath and joins it on, in one path with the extended path's appearance (the other path is removed when nothing of it is left); redraw (Edit Selected Paths) replaces the section of the path between where the stroke starts and ends, each within `within` of one subpath (the shorter way round a closed one), and draws a new path when the stroke doesn't end on it",
|
||||
has_doc,
|
||||
freehand
|
||||
),
|
||||
|
||||
@@ -217,11 +217,18 @@ pub(super) fn freehand(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
// An extending stroke that ends within four Fidelities of the path's other end closes it:
|
||||
// at least 6 pt and at most 400 pt, whatever the Fidelity it is fitted at.
|
||||
// `closed`, when given, says whether the stroke closes the path: the Pencil and the
|
||||
// Paintbrush send it, by their Close Paths When Ends Are Within and Alt at the release.
|
||||
// Left out, an extending stroke that ends within four Fidelities of the path's other end
|
||||
// closes it: at least 6 pt and at most 400 pt, whatever the Fidelity it is fitted at.
|
||||
let close_tol = (tol * 4.0).clamp(6.0, 400.0);
|
||||
let closing = connect.is_none() && sp.anchors.len() >= 2 && pts.last().is_some_and(|p| p.distance(first) <= close_tol);
|
||||
if closing && let Some(l) = pts.last_mut() {
|
||||
let near_first = pts.last().is_some_and(|p| p.distance(first) <= close_tol);
|
||||
let wants_close = if p.get("closed").is_some_and(|v| !v.is_null()) { closed } else { near_first };
|
||||
let closing = connect.is_none() && sp.anchors.len() >= 2 && wants_close;
|
||||
// A stroke ending near the other end ends on it; one ending farther away closes with a
|
||||
// straight segment back to it.
|
||||
let ends_on_first = closing && near_first;
|
||||
if ends_on_first && let Some(l) = pts.last_mut() {
|
||||
*l = first;
|
||||
}
|
||||
if let Some((_, _, y, _)) = &connect
|
||||
@@ -238,13 +245,23 @@ pub(super) fn freehand(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
add.remove(0);
|
||||
}
|
||||
sp.anchors.extend(add);
|
||||
if closing
|
||||
if ends_on_first
|
||||
&& sp.anchors.len() > 2
|
||||
&& let Some(l) = sp.anchors.pop()
|
||||
&& let Some(a0) = sp.anchors.first_mut()
|
||||
{
|
||||
a0.h_in = l.h_in;
|
||||
sp.closed = true;
|
||||
} else if closing && !ends_on_first && sp.anchors.len() > 2 {
|
||||
// Closed from away from the other end: a straight segment back to it, whatever
|
||||
// handles the open ends carried.
|
||||
sp.closed = true;
|
||||
if let Some(l) = sp.anchors.last_mut() {
|
||||
l.h_out = l.p;
|
||||
}
|
||||
if let Some(f) = sp.anchors.first_mut() {
|
||||
f.h_in = f.p;
|
||||
}
|
||||
}
|
||||
for a in &mut sp.anchors {
|
||||
*a = Anchor::with_handles(a.p, a.h_in, a.h_out);
|
||||
|
||||
@@ -5269,6 +5269,47 @@ fn pencil_extend_closes_within_400_pt_at_any_fidelity() {
|
||||
}
|
||||
}
|
||||
|
||||
/// `closed`, when given, decides whether a stroke continuing an open path closes it (the tools
|
||||
/// send it by Close Paths When Ends Are Within): false leaves it open even ending on the other end;
|
||||
/// true closes one ending 50 pt away with a straight segment back, the stroke's end kept, and one
|
||||
/// ending 2 pt away on the other end itself.
|
||||
#[test]
|
||||
fn pencil_extend_closes_as_closed_says() {
|
||||
let mut s = session();
|
||||
for (end, closed, ends_on_start) in [([0i32, 0], false, false), ([0, 50], true, false), ([0, 2], true, true)] {
|
||||
let l = line(&mut s, 0.0, 0.0, 200.0, 0.0);
|
||||
let points = json!([[200, 0], [200, 100], [100, 150], end]);
|
||||
let p = json!({"points": points, "fidelity": 1.5, "closed": closed, "extend": {"id": l.0}, "keepSelected": false});
|
||||
s.execute("path.freehand", &p).unwrap();
|
||||
let pd = path(&s, l);
|
||||
let sp = &pd.subpaths[0];
|
||||
assert_eq!(sp.closed, closed, "ending at {end:?}");
|
||||
if closed {
|
||||
let stroke_end = Point::new(f64::from(end[0]), f64::from(end[1]));
|
||||
let kept = sp.anchors.iter().any(|a| a.p.distance(stroke_end) < 1e-6);
|
||||
assert_eq!(kept, !ends_on_start, "ending at {end:?}: {:?}", sp.anchors);
|
||||
let on_start = sp.anchors.iter().filter(|a| a.p.distance(Point::new(0.0, 0.0)) < 1e-6).count();
|
||||
assert_eq!((sp.anchors[0].p, on_start), (Point::new(0.0, 0.0), 1), "ending at {end:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A continued path closed from away from its other end closes with a straight segment, even when
|
||||
/// that end carries a handle the open path didn't use.
|
||||
#[test]
|
||||
fn pencil_extend_closes_far_away_with_a_straight_segment() {
|
||||
let mut s = session();
|
||||
let anchors = json!([{"x": 0, "y": 0, "in": [-40, 30]}, {"x": 200, "y": 0}]);
|
||||
let l = NodeId(s.execute("path.create", &json!({"anchors": anchors})).unwrap()["id"].as_u64().unwrap());
|
||||
let points = json!([[200, 0], [200, 100], [100, 150], [0, 50]]);
|
||||
let p = json!({"points": points, "fidelity": 1.5, "closed": true, "extend": {"id": l.0}, "keepSelected": false});
|
||||
s.execute("path.freehand", &p).unwrap();
|
||||
let sp = path(&s, l).subpaths[0].clone();
|
||||
assert!(sp.closed);
|
||||
let (first, last) = (sp.anchors[0], sp.anchors[sp.anchors.len() - 1]);
|
||||
assert_eq!((first.h_in, last.h_out), (first.p, last.p), "the closing segment has no handles: {:?}", sp.anchors);
|
||||
}
|
||||
|
||||
/// At Fidelity 0.01 short, one-point and repeated-point strokes still draw or are refused, never
|
||||
/// crash, and what they draw has finite anchors.
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user