mirror of
https://github.com/storytold/designcraft.git
synced 2026-11-11 16:30:16 -06:00
Integration (#630): the Pen continuing from a path's first anchor adds anchors before it (path.appendAnchor at: start) instead of reversing the path, so arrowheads stay on their ends and a press then Esc leaves no undo step
This commit is contained in:
@@ -600,7 +600,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Add Anchor",
|
||||
[],
|
||||
None,
|
||||
"{id, anchor: {p, in?, out?} (spread coords), subpath?: index (default the last)} — after the subpath's last anchor",
|
||||
"{id, anchor: {p, in?, out?} (spread coords), subpath?: index (default the last), at?: end|start (default end)} — after the subpath's last anchor, or before its first with `at: start`",
|
||||
has_doc,
|
||||
path_append
|
||||
),
|
||||
@@ -2714,6 +2714,12 @@ fn path_append(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let id = super::id_param(p, "id").ok_or_else(|| bad("path.appendAnchor", "missing id"))?;
|
||||
let a = p.get("anchor").and_then(anchor_from).ok_or_else(|| bad("path.appendAnchor", "missing anchor"))?;
|
||||
let subpath = p.get("subpath").and_then(Value::as_u64);
|
||||
let at_start = match p.get("at") {
|
||||
None => false,
|
||||
Some(v) if v.as_str() == Some("end") => false,
|
||||
Some(v) if v.as_str() == Some("start") => true,
|
||||
Some(_) => return Err(bad("path.appendAnchor", "`at` must be `start` or `end`")),
|
||||
};
|
||||
s.edit(|d, _| {
|
||||
let loc = d.find(id).ok_or(designcraft_doc::DocError::NoItem(id))?;
|
||||
let inv = (d.parent_xf(&loc) * d.item_at(&loc).map(|i| i.xf).unwrap_or_default()).inverse();
|
||||
@@ -2727,7 +2733,11 @@ fn path_append(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if sp.closed {
|
||||
return Err(bad("path.appendAnchor", "the subpath is closed"));
|
||||
}
|
||||
sp.anchors.push(a.transform(inv));
|
||||
if at_start {
|
||||
sp.anchors.insert(0, a.transform(inv));
|
||||
} else {
|
||||
sp.anchors.push(a.transform(inv));
|
||||
}
|
||||
if it.shape == Shape::GraphicLine {
|
||||
it.shape = Shape::Path;
|
||||
}
|
||||
@@ -4383,6 +4393,10 @@ mod path_edit_tests {
|
||||
s.execute("path.appendAnchor", &json!({"id": id, "subpath": 0, "anchor": {"p": [400, 250]}})).unwrap();
|
||||
assert_eq!(anchors(&s, id).last().unwrap().p, Point::new(400.0, 250.0));
|
||||
assert!(s.execute("path.appendAnchor", &json!({"id": id, "subpath": 1, "anchor": {"p": [0, 0]}})).is_err());
|
||||
// `at: start` goes before the first anchor (the Pen continuing from a path's start).
|
||||
s.execute("path.appendAnchor", &json!({"id": id, "anchor": {"p": [50, 60]}, "at": "start"})).unwrap();
|
||||
assert_eq!((anchors(&s, id)[0].p, anchors(&s, id).len()), (Point::new(50.0, 60.0), 5));
|
||||
assert!(s.execute("path.appendAnchor", &json!({"id": id, "anchor": {"p": [0, 0]}, "at": "middle"})).is_err());
|
||||
// Direction lines on the last anchor: collinear and opposite make it smooth.
|
||||
s.execute("path.setAnchor", &json!({"id": id, "anchor": "last", "in": [380, 230], "out": [420, 270]})).unwrap();
|
||||
let last = *anchors(&s, id).last().unwrap();
|
||||
|
||||
+24
-6
@@ -30,6 +30,9 @@ pub struct PenTool {
|
||||
editing_end: bool,
|
||||
/// The press on the end point has been dragged.
|
||||
end_moved: bool,
|
||||
/// Continuing from the subpath's first anchor: `anchors` run in drawing order, the reverse
|
||||
/// of the path's, and new anchors go before its first one.
|
||||
from_start: bool,
|
||||
}
|
||||
|
||||
/// An end point of an open path under canvas point `p`: (item, subpath, whether it is the last
|
||||
@@ -116,6 +119,7 @@ impl PenTool {
|
||||
self.subpath = None;
|
||||
self.editing_end = false;
|
||||
self.end_moved = false;
|
||||
self.from_start = false;
|
||||
vec![]
|
||||
}
|
||||
|
||||
@@ -130,7 +134,8 @@ impl PenTool {
|
||||
let (i, o) = if flip { (a.h_out, a.h_in) } else { (a.h_in, a.h_out) };
|
||||
(xf * a.p, xf * i, xf * o)
|
||||
};
|
||||
// From the first anchor the path is reversed first, so the new anchors follow on.
|
||||
// From the first anchor the anchors run backwards (handles swapped); new ones are added
|
||||
// before the first, so the path keeps its direction (and its arrowheads their ends).
|
||||
let (other, end) = if at_end { (at(first, false), at(last, false)) } else { (at(last, true), at(first, true)) };
|
||||
self.path = Some(id.0);
|
||||
self.subpath = Some(si);
|
||||
@@ -139,7 +144,13 @@ impl PenTool {
|
||||
self.editing_end = true;
|
||||
self.end_moved = false;
|
||||
self.dragging = true;
|
||||
if at_end { vec![] } else { vec![Action::Exec("path.reverse".into(), json!({"ids": [id.0], "subpath": si}))] }
|
||||
self.from_start = !at_end;
|
||||
vec![]
|
||||
}
|
||||
|
||||
/// Anchor `a` (drawing order) in the path's order.
|
||||
fn path_anchor(&self, a: &(Point, Point, Point)) -> (Point, Point, Point) {
|
||||
if self.from_start { (a.0, a.2, a.1) } else { *a }
|
||||
}
|
||||
|
||||
/// Shift constrains first. The stored point is the snapped spread point. Guides are already canvas.
|
||||
@@ -161,10 +172,13 @@ impl PenTool {
|
||||
vec![Action::Exec("path.create".into(), json!({"spread": spread_json(sr), "anchors": anchors, "closed": false, "notifyPen": true}))]
|
||||
}
|
||||
Some(id) => {
|
||||
let mut p = json!({"id": id, "anchor": anchor_json(last)});
|
||||
let mut p = json!({"id": id, "anchor": anchor_json(&self.path_anchor(last))});
|
||||
if let Some(si) = self.subpath {
|
||||
p["subpath"] = json!(si);
|
||||
}
|
||||
if self.from_start {
|
||||
p["at"] = json!("start");
|
||||
}
|
||||
vec![Action::Exec("path.appendAnchor".into(), p)]
|
||||
}
|
||||
None => vec![],
|
||||
@@ -248,13 +262,17 @@ impl Tool for PenTool {
|
||||
self.dragging = false;
|
||||
// The continued path's end point: a drag set its direction lines.
|
||||
if std::mem::take(&mut self.editing_end) {
|
||||
let (Some(id), Some(si), Some(&(p, h_in, h_out))) = (self.path, self.subpath, self.anchors.last()) else { return vec![] };
|
||||
if !std::mem::take(&mut self.end_moved) || (h_out - p).hypot() < 1e-9 {
|
||||
let (Some(id), Some(si), Some(&(p, drawn_in, drawn_out))) = (self.path, self.subpath, self.anchors.last()) else {
|
||||
return vec![];
|
||||
};
|
||||
if !std::mem::take(&mut self.end_moved) || (drawn_out - p).hypot() < 1e-9 {
|
||||
return vec![];
|
||||
}
|
||||
let (_, h_in, h_out) = self.path_anchor(&(p, drawn_in, drawn_out));
|
||||
let anchor = if self.from_start { "first" } else { "last" };
|
||||
return vec![Action::Exec(
|
||||
"path.setAnchor".into(),
|
||||
json!({"id": id, "subpath": si, "anchor": "last", "in": [h_in.x, h_in.y], "out": [h_out.x, h_out.y]}),
|
||||
json!({"id": id, "subpath": si, "anchor": anchor, "in": [h_in.x, h_in.y], "out": [h_out.x, h_out.y]}),
|
||||
)];
|
||||
}
|
||||
self.commit_anchor()
|
||||
|
||||
@@ -1559,19 +1559,30 @@ fn pen_continues_an_open_path_from_either_end_point() {
|
||||
let a = t.pointer(&cx, &ev(PointerKind::Down, at(100.0, 100.0)));
|
||||
assert_eq!(a, vec![Action::Exec("path.close".into(), serde_json::json!({"id": id.0, "subpath": 0}))]);
|
||||
assert!(t.pointer(&cx, &ev(PointerKind::Up, at(100.0, 100.0))).is_empty());
|
||||
// From the first anchor, the path is reversed first; a drag there sets its direction lines.
|
||||
// From the first anchor the path keeps its direction (arrowheads stay on their ends, and a
|
||||
// press then Esc leaves nothing to undo): a drag there sets the first anchor's direction
|
||||
// lines, and new anchors go before it.
|
||||
let mut t = create("pen");
|
||||
let a = t.pointer(&cx, &ev(PointerKind::Down, at(100.0, 100.0)));
|
||||
assert_eq!(a, vec![Action::Exec("path.reverse".into(), serde_json::json!({"ids": [id.0], "subpath": 0}))]);
|
||||
assert!(t.pointer(&cx, &ev(PointerKind::Down, at(100.0, 100.0))).is_empty());
|
||||
t.pointer(&cx, &ev(PointerKind::Drag, at(80.0, 140.0)));
|
||||
let a = t.pointer(&cx, &ev(PointerKind::Up, at(80.0, 140.0)));
|
||||
assert_eq!(
|
||||
a,
|
||||
vec![Action::Exec(
|
||||
"path.setAnchor".into(),
|
||||
serde_json::json!({"id": id.0, "subpath": 0, "anchor": "last", "in": [120.0, 60.0], "out": [80.0, 140.0]})
|
||||
serde_json::json!({"id": id.0, "subpath": 0, "anchor": "first", "in": [80.0, 140.0], "out": [120.0, 60.0]})
|
||||
)]
|
||||
);
|
||||
t.pointer(&cx, &ev(PointerKind::Down, at(20.0, 200.0)));
|
||||
t.pointer(&cx, &ev(PointerKind::Drag, at(40.0, 220.0)));
|
||||
let a = t.pointer(&cx, &ev(PointerKind::Up, at(40.0, 220.0)));
|
||||
let [Action::Exec(cmd, p)] = a.as_slice() else { panic!("{a:?}") };
|
||||
assert_eq!((cmd.as_str(), p["at"].as_str(), p["subpath"].as_u64()), ("path.appendAnchor", Some("start"), Some(0)));
|
||||
assert_eq!(p["anchor"], serde_json::json!({"p": [20.0, 200.0], "in": [40.0, 220.0], "out": [0.0, 180.0]}));
|
||||
let mut t = create("pen");
|
||||
assert!(t.pointer(&cx, &ev(PointerKind::Down, at(100.0, 100.0))).is_empty());
|
||||
assert!(t.pointer(&cx, &ev(PointerKind::Up, at(100.0, 100.0))).is_empty());
|
||||
assert!(t.key(&cx, ToolKey::Escape, Mods::default()).iter().all(|a| !matches!(a, Action::Exec(c, _) if c.starts_with("path."))));
|
||||
// Closed paths and locked paths aren't continued.
|
||||
let mut d2 = d.clone();
|
||||
d2.item_mut(id).unwrap().locked = true;
|
||||
|
||||
Reference in New Issue
Block a user