Merge pr/630: Tools: the Pen continues an open path from either end point (#630)

# Conflicts:
#	ROADMAP.md
#	crates/engine/src/cmd/object.rs
#	crates/tools/src/lib.rs
#	crates/tools/src/pen.rs
#	crates/tools/src/tests.rs
#	crates/ui-egui/src/canvas.rs
#	crates/ui-egui/src/i18n.rs
#	crates/ui-egui/src/i18n/ar.rs
#	crates/ui-egui/src/i18n/it.rs
#	crates/ui-egui/src/i18n/ja.rs
#	crates/ui-egui/src/i18n/pt_br.rs
#	crates/ui-egui/src/i18n/uk.rs
This commit is contained in:
aiongg
2026-10-13 00:02:51 +00:00
13 changed files with 380 additions and 11 deletions
+1
View File
@@ -128,6 +128,7 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m
Newest first.
- **2026-10-12** · The Pen tool continues an open path from either end point, as in InDesign: the cursor shows it over an end point, a click there carries on drawing that path, a drag first reshapes the end point's direction lines, and the other end closes it. Object › Paths › Reverse Path (`path.reverse`); `path.setAnchor`; `path.appendAnchor` and `path.close` take a subpath. No readiness percentage change.
- **2026-10-12** · The Eyedropper puts a colour sampled from an image on the stroke when the toolbox's Stroke box is in front, as in InDesign (a line takes it on its stroke). No readiness percentage change.
- **2026-10-12** · Window close and application/menu Quit ask about unsaved documents; later Cancel keeps every tab open, including earlier Don't Save choices. Approvals are rechecked after edits. Cancelable macOS Dock Quit remains open (#544). No readiness percentage change.
- **2026-10-12** · Each running app keeps recovery data in its own locked folder; current folder and lock ownership are checked before writes and cleanup. Failed isolation disables recovery with a warning instead of sharing document IDs. No readiness percentage change.
+189 -5
View File
@@ -595,8 +595,34 @@ pub fn specs() -> Vec<CommandSpec> {
step_and_repeat
),
cmd!("path.create", "Create Path", [], None, "{spread?, anchors: [{p:[x,y], in?:[x,y], out?:[x,y]}], closed?: bool}", has_doc, path_create),
cmd!("path.appendAnchor", "Add Anchor", [], None, "{id, anchor: {p, in?, out?}} (spread coords)", has_doc, path_append),
cmd!("path.close", "Close Path", ["Object", "Paths"], None, "{id}", has_doc, path_close),
cmd!(
"path.appendAnchor",
"Add Anchor",
[],
None,
"{id, anchor: {p, in?, out?} (spread coords), subpath?: index (default the last)} — after the subpath's last anchor",
has_doc,
path_append
),
cmd!(
"path.setAnchor",
"Set Anchor",
[],
None,
"{id, subpath?: index (default the last), anchor: index|\"first\"|\"last\", p?, in?, out? (spread coords)} — moves an anchor or its direction lines; with both lines given and collinear it becomes smooth",
has_doc,
path_set_anchor
),
cmd!(
"path.reverse",
"Reverse Path",
["Object", "Paths"],
None,
"{ids?, subpath?: index (only that subpath of the first target)} — the anchors run the other way (the end point becomes the start)",
has_selection,
path_reverse
),
cmd!("path.close", "Close Path", ["Object", "Paths"], None, "{id, subpath?: index (default every subpath)}", has_doc, path_close),
cmd!("path.moveAnchors", "Move Anchors", [], None, "{id, anchors: [[subpath, index]], dx, dy, handle?: in|out}", has_doc, path_move_anchors),
cmd!(
"object.align",
@@ -2687,11 +2713,20 @@ fn path_create(s: &mut Session, p: &Value) -> Result<Value> {
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);
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();
let it = d.item_mut(id).ok_or(designcraft_doc::DocError::NoItem(id))?;
let sp = it.path.subpaths.last_mut().ok_or_else(|| bad("path.appendAnchor", "empty path"))?;
let sp = match subpath {
Some(i) => {
usize::try_from(i).ok().and_then(|i| it.path.subpaths.get_mut(i)).ok_or_else(|| bad("path.appendAnchor", "no such subpath"))?
}
None => it.path.subpaths.last_mut().ok_or_else(|| bad("path.appendAnchor", "empty path"))?,
};
if sp.closed {
return Err(bad("path.appendAnchor", "the subpath is closed"));
}
sp.anchors.push(a.transform(inv));
if it.shape == Shape::GraphicLine {
it.shape = Shape::Path;
@@ -2700,12 +2735,103 @@ fn path_append(s: &mut Session, p: &Value) -> Result<Value> {
})
}
fn path_set_anchor(s: &mut Session, p: &Value) -> Result<Value> {
const ID: &str = "path.setAnchor";
let id = super::id_param(p, "id").ok_or_else(|| bad(ID, "missing id"))?;
let pt = |k: &str| -> Option<Point> {
let a = p.get(k)?.as_array()?;
let q = Point::new(a.first()?.as_f64()?, a.get(1)?.as_f64()?);
(q.x.is_finite() && q.y.is_finite()).then_some(q)
};
let (new_p, new_in, new_out) = (pt("p"), pt("in"), pt("out"));
if new_p.is_none() && new_in.is_none() && new_out.is_none() {
return Err(bad(ID, "give p, in or out"));
}
let subpath = p.get("subpath").and_then(Value::as_u64);
let which = p.get("anchor").cloned().unwrap_or(Value::Null);
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();
let it = d.item_mut(id).ok_or(designcraft_doc::DocError::NoItem(id))?;
if it.locked {
return Err(bad(ID, "the path is locked"));
}
let sp = match subpath {
Some(i) => usize::try_from(i).ok().and_then(|i| it.path.subpaths.get_mut(i)),
None => it.path.subpaths.last_mut(),
}
.ok_or_else(|| bad(ID, "no such subpath"))?;
let n = sp.anchors.len();
let ai = match &which {
Value::String(w) if w == "first" => 0,
Value::String(w) if w == "last" => n.saturating_sub(1),
v => v.as_u64().and_then(|i| usize::try_from(i).ok()).ok_or_else(|| bad(ID, "anchor: an index, \"first\" or \"last\""))?,
};
let a = sp.anchors.get_mut(ai).ok_or_else(|| bad(ID, "no such anchor"))?;
if let Some(q) = new_p {
let v = inv * q - a.p;
a.p += v;
a.h_in += v;
a.h_out += v;
}
if let Some(q) = new_in {
a.h_in = inv * q;
}
if let Some(q) = new_out {
a.h_out = inv * q;
}
if new_in.is_some() && new_out.is_some() {
let (i, o) = (a.h_in - a.p, a.h_out - a.p);
let collinear = (i.x * o.y - i.y * o.x).abs() <= 1e-6 * i.hypot().max(1.0) * o.hypot().max(1.0) && i.dot(o) <= 0.0;
a.kind = if collinear { designcraft_geom::AnchorKind::Smooth } else { designcraft_geom::AnchorKind::Corner };
}
ok()
})
}
fn path_reverse(s: &mut Session, p: &Value) -> Result<Value> {
let ids = targets(s, p)?;
let only = p.get("subpath").and_then(Value::as_u64).map(|i| usize::try_from(i).unwrap_or(usize::MAX));
if let (Some(i), Some(id)) = (only, ids.first())
&& s.doc()?.doc.item(*id).is_none_or(|it| it.path.subpaths.get(i).is_none())
{
return Err(bad("path.reverse", "no such subpath"));
}
let ids = if only.is_some() { ids.into_iter().take(1).collect() } else { ids };
s.edit(|d, _| {
let mut reversed = 0;
for id in &ids {
let Some(it) = d.item_mut(*id) else { continue };
if it.locked || it.is_group() {
continue;
}
for (i, sp) in it.path.subpaths.iter_mut().enumerate() {
if only.is_some_and(|o| o != i) {
continue;
}
sp.anchors.reverse();
for a in &mut sp.anchors {
std::mem::swap(&mut a.h_in, &mut a.h_out);
}
}
reversed += 1;
}
Ok(json!({"reversed": reversed}))
})
}
fn path_close(s: &mut Session, p: &Value) -> Result<Value> {
let id = super::id_param(p, "id").ok_or_else(|| bad("path.close", "missing id"))?;
let only = p.get("subpath").and_then(Value::as_u64).map(|i| usize::try_from(i).unwrap_or(usize::MAX));
s.edit(|d, _| {
let it = d.item_mut(id).ok_or(designcraft_doc::DocError::NoItem(id))?;
for sp in &mut it.path.subpaths {
sp.closed = true;
if only.is_some_and(|i| i >= it.path.subpaths.len()) {
return Err(bad("path.close", "no such subpath"));
}
for (i, sp) in it.path.subpaths.iter_mut().enumerate() {
if only.is_none_or(|o| o == i) {
sp.closed = true;
}
}
if it.shape == Shape::GraphicLine {
it.shape = Shape::Path;
@@ -4213,3 +4339,61 @@ mod spread_target_tests {
assert!(s.doc().unwrap().doc.spreads[0].items.is_empty());
}
}
#[cfg(test)]
mod path_edit_tests {
use designcraft_doc::ItemId;
use designcraft_geom::Point;
use serde_json::json;
fn session_with_path() -> (crate::Session, u64) {
let mut s = crate::Session::new();
s.execute("file.new", &json!({})).unwrap();
let id = s
.execute(
"path.create",
&json!({"anchors": [{"p": [100, 100]}, {"p": [200, 100], "in": [170, 80], "out": [230, 120]}, {"p": [300, 200]}]}),
)
.unwrap()["id"]
.as_u64()
.unwrap();
(s, id)
}
fn anchors(s: &crate::Session, id: u64) -> Vec<designcraft_geom::Anchor> {
let d = &s.doc().unwrap().doc;
let it = d.item(ItemId(id)).unwrap();
let xf = d.parent_xf(&d.find(ItemId(id)).unwrap()) * it.xf;
it.path.subpaths[0].anchors.iter().map(|a| a.transform(xf)).collect()
}
#[test]
fn reverse_path_swaps_ends_and_direction_lines() {
let (mut s, id) = session_with_path();
s.execute("path.reverse", &json!({"ids": [id]})).unwrap();
let a = anchors(&s, id);
assert_eq!(a.iter().map(|a| a.p).collect::<Vec<_>>(), [Point::new(300.0, 200.0), Point::new(200.0, 100.0), Point::new(100.0, 100.0)]);
assert_eq!((a[1].h_in, a[1].h_out), (Point::new(230.0, 120.0), Point::new(170.0, 80.0)));
assert!(s.execute("path.reverse", &json!({"ids": [id], "subpath": 3})).is_err());
}
#[test]
fn anchors_go_on_the_named_open_subpath_and_set_anchor_reshapes_one() {
let (mut s, id) = session_with_path();
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());
// 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();
assert_eq!((last.h_in, last.h_out, last.kind), (Point::new(380.0, 230.0), Point::new(420.0, 270.0), designcraft_geom::AnchorKind::Smooth));
s.execute("path.setAnchor", &json!({"id": id, "anchor": 0, "p": [90, 110]})).unwrap();
assert_eq!(anchors(&s, id)[0].p, Point::new(90.0, 110.0));
assert!(s.execute("path.setAnchor", &json!({"id": id, "anchor": 9, "p": [0, 0]})).is_err());
assert!(s.execute("path.setAnchor", &json!({"id": id, "anchor": "last"})).is_err(), "nothing to set");
// Closing one subpath; then it takes no more anchors.
s.execute("path.close", &json!({"id": id, "subpath": 0})).unwrap();
assert!(s.execute("path.appendAnchor", &json!({"id": id, "anchor": {"p": [0, 0]}})).is_err());
assert!(s.execute("path.close", &json!({"id": id, "subpath": 4})).is_err());
}
}
+2
View File
@@ -369,6 +369,8 @@ pub enum Cursor {
PenClose,
/// Convert Direction Point.
PenConvert,
/// The Pen over an open path's end point: a click continues the path.
PenContinue,
Text,
Hand,
HandGrab,
+100 -3
View File
@@ -1,7 +1,8 @@
//! Pen tool (P): click for corner points, drag for smooth points, click the first point to close,
//! Enter/Esc (or switching tools) to finish an open path. Each anchor is its own undo step, as in
//! InDesign. Between paths, a click on a selected path's anchor deletes it and a click on its
//! outline adds an anchor; the cursor shows which.
//! outline adds an anchor; clicking an end point of an open path continues that path from there
//! (dragging from it first reshapes its direction lines). The cursor shows which.
use designcraft_doc::{ItemId, SpreadRef};
use designcraft_geom::{BezPath, Point};
@@ -22,6 +23,45 @@ pub struct PenTool {
hover: Point,
/// Smart guides in canvas coordinates, as `snap_point` returned them.
guides: Vec<Overlay>,
/// Continuing an existing path: the subpath the new anchors go on.
subpath: Option<usize>,
/// The press was on the continued path's end point: a drag reshapes its direction lines
/// instead of adding an anchor.
editing_end: bool,
/// The press on the end point has been dragged.
end_moved: bool,
}
/// An end point of an open path under canvas point `p`: (item, subpath, whether it is the last
/// anchor). The selection is checked first, then the frontmost item under the pointer.
fn endpoint_at(cx: &ToolContext, p: Point) -> Option<(ItemId, usize, bool)> {
let tol = cx.tol(6.0);
let mut ids = cx.selection.items.clone();
if let Some((_, id)) = cx.hit(p)
&& !ids.contains(&id)
{
ids.push(id);
}
for id in ids {
let Some(it) = cx.doc.item(id) else { continue };
if it.locked || it.is_group() || !matches!(it.content, designcraft_doc::Content::Unassigned) {
continue;
}
let Some(xf) = cx.item_canvas_xf(id).map(|m| m * it.xf) else { continue };
for (si, sp) in it.path.subpaths.iter().enumerate() {
if sp.closed {
continue;
}
let (Some(first), Some(last)) = (sp.anchors.first(), sp.anchors.last()) else { continue };
if (xf * last.p - p).hypot() <= tol {
return Some((id, si, true));
}
if (xf * first.p - p).hypot() <= tol {
return Some((id, si, false));
}
}
}
None
}
fn anchor_json(a: &(Point, Point, Point)) -> Value {
@@ -38,6 +78,8 @@ enum Hover {
Delete(u64, usize, usize),
/// Add an anchor on a selected path's outline.
Add(ItemId),
/// Continue subpath `.1` of open path `.0` from its last (`.2`) or first anchor.
Continue(ItemId, usize, bool),
}
impl PenTool {
@@ -55,6 +97,9 @@ impl PenTool {
if !self.anchors.is_empty() {
return Hover::Place;
}
if let Some((id, si, at_end)) = endpoint_at(cx, p) {
return Hover::Continue(id, si, at_end);
}
let ids = &cx.selection.items;
if let Some((id, si, ai, None)) = anchor_at_in(cx, ids, p) {
return Hover::Delete(id, si, ai);
@@ -68,9 +113,35 @@ impl PenTool {
self.anchors.clear();
self.dragging = false;
self.guides.clear();
self.subpath = None;
self.editing_end = false;
self.end_moved = false;
vec![]
}
/// Start continuing subpath `si` of `id` from its last (`at_end`) or first anchor: the anchors
/// so far are its other end and the end point, in spread coordinates.
fn continue_from(&mut self, cx: &ToolContext, id: ItemId, si: usize, at_end: bool) -> Vec<Action> {
let (Some(it), Some(loc)) = (cx.doc.item(id), cx.doc.find(id)) else { return vec![] };
let Some(sp) = it.path.subpaths.get(si) else { return vec![] };
let (Some(first), Some(last)) = (sp.anchors.first(), sp.anchors.last()) else { return vec![] };
let xf = cx.doc.parent_xf(&loc) * it.xf;
let at = |a: &designcraft_geom::Anchor, flip: bool| {
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.
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);
self.spread = Some(loc.spread);
self.anchors = vec![other, end];
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}))] }
}
/// Shift constrains first. The stored point is the snapped spread point. Guides are already canvas.
fn snap_click(&mut self, cx: &ToolContext, spread: SpreadRef, mut p: Point, shift: bool) -> Point {
if shift && let Some(last) = self.anchors.last() {
@@ -89,7 +160,13 @@ impl PenTool {
let anchors: Vec<Value> = self.anchors.iter().map(anchor_json).collect();
vec![Action::Exec("path.create".into(), json!({"spread": spread_json(sr), "anchors": anchors, "closed": false, "notifyPen": true}))]
}
Some(id) => vec![Action::Exec("path.appendAnchor".into(), json!({"id": id, "anchor": anchor_json(last)}))],
Some(id) => {
let mut p = json!({"id": id, "anchor": anchor_json(last)});
if let Some(si) = self.subpath {
p["subpath"] = json!(si);
}
vec![Action::Exec("path.appendAnchor".into(), p)]
}
None => vec![],
}
}
@@ -126,12 +203,19 @@ impl Tool for PenTool {
match self.hover_at(cx, ev.pos) {
Hover::Close => {
let out = match self.path {
Some(id) => vec![Action::Exec("path.close".into(), json!({"id": id}))],
Some(id) => {
let mut p = json!({"id": id});
if let Some(si) = self.subpath {
p["subpath"] = json!(si);
}
vec![Action::Exec("path.close".into(), p)]
}
None => vec![],
};
self.finish();
return out;
}
Hover::Continue(id, si, at_end) => return self.continue_from(cx, id, si, at_end),
Hover::Delete(id, si, ai) => {
return vec![Action::Exec("path.deleteAnchor".into(), json!({"id": id, "subpath": si, "anchor": ai}))];
}
@@ -149,6 +233,7 @@ impl Tool for PenTool {
vec![]
}
PointerKind::Drag => {
self.end_moved |= self.editing_end;
if let Some(a) = self.anchors.last_mut() {
// Smooth point: out handle follows the pointer, in handle mirrors it.
a.2 = sp;
@@ -161,6 +246,17 @@ impl Tool for PenTool {
return vec![];
}
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 {
return vec![];
}
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]}),
)];
}
self.commit_anchor()
}
_ => vec![],
@@ -210,6 +306,7 @@ impl Tool for PenTool {
match self.hover_at(cx, p) {
Hover::Place => Cursor::Pen,
Hover::Close => Cursor::PenClose,
Hover::Continue(..) => Cursor::PenContinue,
Hover::Delete(..) => Cursor::PenDelete,
Hover::Add(_) => Cursor::PenAdd,
}
+59
View File
@@ -1520,3 +1520,62 @@ fn fitting_handle_axes_follow_spread_and_frame_rotation() {
}
}
}
#[test]
fn pen_continues_an_open_path_from_either_end_point() {
let mut d = Document::new(&NewDocument::default());
let lid = d.default_layer();
let id = ItemId(d.alloc());
let anchors = vec![
designcraft_geom::Anchor::corner(Point::new(100.0, 100.0)),
designcraft_geom::Anchor {
p: Point::new(200.0, 100.0),
h_in: Point::new(170.0, 80.0),
h_out: Point::new(230.0, 120.0),
kind: designcraft_geom::AnchorKind::Smooth,
},
];
let mut it = Item::new(id, lid, Shape::Path, designcraft_geom::PathData::single(designcraft_geom::SubPath::new(anchors, false)));
it.stroke = Stroke::default();
d.insert_item(SpreadRef::Doc(0), it, None).unwrap();
let (s, c, l) = (Selection::default(), Cache::new(), CanvasLayout::new(&d, false));
let cx = ctx(&d, &s, &c, &l);
let off = l.xf(SpreadRef::Doc(0)).translation();
let at = |x: f64, y: f64| (x + off.x, y + off.y);
let ev = |k: PointerKind, (x, y): (f64, f64)| PointerEvent::new(k, x, y);
let mut t = create("pen");
// The cursor says the end point continues the path; elsewhere it's the plain pen.
assert_eq!(t.cursor(&cx, Point::new(at(200.0, 100.0).0, at(200.0, 100.0).1), Mods::default()), Cursor::PenContinue);
assert_eq!(t.cursor(&cx, Point::new(at(150.0, 300.0).0, at(150.0, 300.0).1), Mods::default()), Cursor::Pen);
// Clicking the last anchor adds nothing yet; the next click appends to that path.
assert!(t.pointer(&cx, &ev(PointerKind::Down, at(200.0, 100.0))).is_empty());
assert!(t.pointer(&cx, &ev(PointerKind::Up, at(200.0, 100.0))).is_empty(), "a click on the end point changes nothing");
t.pointer(&cx, &ev(PointerKind::Down, at(300.0, 200.0)));
let a = t.pointer(&cx, &ev(PointerKind::Up, at(300.0, 200.0)));
let [Action::Exec(cmd, p)] = a.as_slice() else { panic!("{a:?}") };
assert_eq!((cmd.as_str(), p["id"].as_u64(), p["subpath"].as_u64()), ("path.appendAnchor", Some(id.0), Some(0)));
assert_eq!((p["anchor"]["p"][0].as_f64(), p["anchor"]["p"][1].as_f64()), (Some(300.0), Some(200.0)));
// Clicking the first anchor of the other end closes it.
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.
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}))]);
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]})
)]
);
// Closed paths and locked paths aren't continued.
let mut d2 = d.clone();
d2.item_mut(id).unwrap().locked = true;
let cx = ctx(&d2, &s, &c, &l);
let t = create("pen");
assert_eq!(t.cursor(&cx, Point::new(at(200.0, 100.0).0, at(200.0, 100.0).1), Mods::default()), Cursor::Pen);
}
+15 -3
View File
@@ -96,6 +96,7 @@ enum Badge {
Delete,
Close,
Convert,
Continue,
}
/// The pen nib, tip at `o` (top left), with a badge below right.
@@ -121,6 +122,7 @@ fn pen(ink: &mut Ink, o: Pos2, badge: Badge) {
ink.line(b + vec2(0.0, 6.0), b + vec2(3.0, 0.0));
ink.line(b + vec2(3.0, 0.0), b + vec2(6.0, 6.0));
}
Badge::Continue => ink.line(b + vec2(1.0, 6.0), b + vec2(5.0, 0.0)),
}
}
@@ -152,6 +154,7 @@ fn glyph(c: Cursor, p: Pos2) -> Option<Vec<Shape>> {
Cursor::PenDelete => pen(&mut ink, p, Badge::Delete),
Cursor::PenClose => pen(&mut ink, p, Badge::Close),
Cursor::PenConvert => pen(&mut ink, p, Badge::Convert),
Cursor::PenContinue => pen(&mut ink, p, Badge::Continue),
Cursor::Rotate => rotate(&mut ink, p),
_ => return None,
}
@@ -270,7 +273,7 @@ pub fn system(c: Cursor) -> egui::CursorIcon {
Cursor::ResizeNwSe => C::ResizeNwSe,
Cursor::ResizeNeSw => C::ResizeNeSw,
Cursor::Rotate => C::Alias,
Cursor::Pen | Cursor::PenAdd | Cursor::PenDelete | Cursor::PenClose | Cursor::PenConvert => C::Crosshair,
Cursor::Pen | Cursor::PenAdd | Cursor::PenDelete | Cursor::PenClose | Cursor::PenConvert | Cursor::PenContinue => C::Crosshair,
Cursor::Text => C::Text,
Cursor::Hand => C::Grab,
Cursor::HandGrab => C::Grabbing,
@@ -320,8 +323,17 @@ mod tests {
use super::*;
/// Every cursor with a glyph.
const GLYPHS: [Cursor; 8] =
[Cursor::Arrow, Cursor::ArrowHollow, Cursor::Pen, Cursor::PenAdd, Cursor::PenDelete, Cursor::PenClose, Cursor::PenConvert, Cursor::Rotate];
const GLYPHS: [Cursor; 9] = [
Cursor::Arrow,
Cursor::ArrowHollow,
Cursor::Pen,
Cursor::PenAdd,
Cursor::PenDelete,
Cursor::PenClose,
Cursor::PenConvert,
Cursor::PenContinue,
Cursor::Rotate,
];
fn alpha(img: &CustomCursorImage, x: usize, y: usize) -> u8 {
img.rgba.get((y * img.size[0] as usize + x) * 4 + 3).copied().unwrap_or(0)
+2
View File
@@ -65,6 +65,8 @@ const TABLE: &[(&str, [&str; 5])] = &[
"Paste Vector Art",
["Vektorgrafik einfügen", "Coller l’illustration vectorielle", "Pegar ilustración vectorial", "ベクトルアートをペースト", "粘贴矢量图稿"],
),
("Reverse Path", ["Pfadrichtung umkehren", "Inverser le tracé", "Invertir trazado", "パスの方向反転", "反转路径"]),
("Set Anchor", ["Ankerpunkt festlegen", "Définir le point d’ancrage", "Definir punto de ancla", "アンカーポイントを設定", "设置锚点"]),
("Clear", ["Löschen", "Effacer", "Borrar", "消去", "清除"]),
("Duplicate", ["Duplizieren", "Dupliquer", "Duplicar", "複製", "重复"]),
("Select All", ["Alles auswählen", "Tout sélectionner", "Seleccionar todo", "すべてを選択", "全选"]),
+2
View File
@@ -31,6 +31,8 @@ pub(super) const TABLE: &[(&str, &str)] = &[
("Copy", "نسخ"),
("Paste", "لصق"),
("Paste Vector Art", "لصق رسم متجه"),
("Reverse Path", "عكس المسار"),
("Set Anchor", "تعيين نقطة الارتساء"),
("Clear", "مسح"),
("Delete", "حذف"),
("Duplicate", "تكرار"),
+2
View File
@@ -29,6 +29,8 @@ pub(super) const TABLE: &[(&str, &str)] = &[
("Copy", "Copia"),
("Paste", "Incolla"),
("Paste Vector Art", "Incolla grafica vettoriale"),
("Reverse Path", "Inverti tracciato"),
("Set Anchor", "Imposta ancoraggio"),
("Clear", "Cancella"),
("Duplicate", "Duplica"),
("Select All", "Seleziona tutto"),
+2
View File
@@ -31,6 +31,8 @@ pub(super) const TABLE: &[(&str, &str)] = &[
("Copy", "コピー"),
("Paste", "ペースト"),
("Paste Vector Art", "ベクトルアートをペースト"),
("Reverse Path", "パスの方向反転"),
("Set Anchor", "アンカーポイントを設定"),
("Clear", "消去"),
("Duplicate", "複製"),
("Select All", "すべてを選択"),
+2
View File
@@ -32,6 +32,8 @@ pub(super) const TABLE: &[(&str, &str)] = &[
("Copy", "Copiar"),
("Paste", "Colar"),
("Paste Vector Art", "Colar arte vetorial"),
("Reverse Path", "Inverter demarcador"),
("Set Anchor", "Definir ponto de ancoragem"),
("Clear", "Limpar"),
("Delete", "Excluir"),
("Duplicate", "Duplicar"),
+2
View File
@@ -30,6 +30,8 @@ pub(super) const TABLE: &[(&str, &str)] = &[
("Copy", "Копіювати"),
("Paste", "Вставити"),
("Paste Vector Art", "Вставити векторну графіку"),
("Reverse Path", "Розвернути контур"),
("Set Anchor", "Задати прив’язку"),
("Clear", "Очистити"),
("Duplicate", "Дублювати"),
("Select All", "Виділити все"),
+2
View File
@@ -530,6 +530,8 @@ pub const MENUS: &[(&str, &[&str])] = &[
">Paths",
"cmd:object.makeCompoundPath",
"cmd:object.releaseCompoundPath",
"-",
"cmd:path.reverse",
"<",
">Pathfinder",
"cmd:object.pathfinder|Add|{\"op\": \"add\"}",