Merge branch 'pr/630' into batch1

This commit is contained in:
Brandon Thomas committed 2026-10-13 02:21:53 -04:00
commit 8a36af3af8
13 files changed
+368 -12

No files matched your search

+1
View File
@@ -135,6 +135,7 @@ Newest first.
- **2026-10-12** · Book numbering and style updates prepare every chapter before saving. Only closed native chapters are changed; imported IDML remains a read-only style/PDF source. If a later disk write fails, the error names the completed chapters; the batch is not a whole-book transaction. No readiness percentage change.
- **2026-10-12** · Desktop write callbacks and CLI raster exports use the atomic writer too; open readers retain the original bytes, while private Unix modes and symlink targets survive replacement. Permanent tests cover both actual callers. No readiness percentage change.
- **2026-10-12** · Save As, library edits and book collection edits persist prospective values before changing live state; failed writes preserve the document and existing collections. Save a Copy rebases data-source paths for its destination without changing the open document. No readiness percentage change.
- **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** · 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.
- **2026-10-12** · Native engine saves and exports replace files atomically; failed writes retain the original, private staging preserves Unix permissions, and valid long filenames work. Desktop Services and CLI raster exports remain separate follow-ups. 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",
@@ -1975,11 +2001,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;
@@ -1988,12 +2023,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;
@@ -2995,3 +3121,61 @@ mod swatch_name_tests {
}
}
}
#[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
@@ -335,6 +335,8 @@ pub enum Cursor {
ResizeNeSw,
Rotate,
Pen,
/// The Pen over an open path's end point: a click continues the path.
PenContinue,
Text,
Hand,
HandGrab,
+102 -6
View File
@@ -1,8 +1,9 @@
//! 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.
//! InDesign. Clicking an end point of an open path (the cursor shows it) continues that path from
//! there; dragging from it first reshapes its direction lines.
use designcraft_doc::SpreadRef;
use designcraft_doc::{ItemId, SpreadRef};
use designcraft_geom::{BezPath, Point};
use serde_json::{Value, json};
@@ -19,6 +20,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 {
@@ -32,9 +72,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() {
@@ -53,7 +119,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![],
}
}
@@ -93,12 +165,24 @@ impl Tool for PenTool {
&& (first.0 - sp).hypot() <= cx.tol(6.0)
{
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;
}
// On an open path's end point: continue that path.
if self.anchors.is_empty()
&& let Some((id, si, at_end)) = endpoint_at(cx, ev.pos)
{
return self.continue_from(cx, id, si, at_end);
}
let p = self.snap_click(cx, snap_spread, sp, ev.mods.shift);
if self.anchors.is_empty() {
self.spread = Some(sr);
@@ -108,6 +192,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;
@@ -120,6 +205,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![],
@@ -165,8 +261,8 @@ impl Tool for PenTool {
out
}
fn cursor(&self, _cx: &ToolContext, _p: Point, _m: Mods) -> Cursor {
Cursor::Pen
fn cursor(&self, cx: &ToolContext, p: Point, _m: Mods) -> Cursor {
if self.anchors.is_empty() && endpoint_at(cx, p).is_some() { Cursor::PenContinue } else { Cursor::Pen }
}
fn busy(&self) -> bool {
+59
View File
@@ -705,3 +705,62 @@ fn pen_click_snaps_to_a_guide() {
let x = p["anchors"][0]["p"][0].as_f64().unwrap();
assert!((x - 100.0).abs() < 1e-6, "anchor x {x}");
}
#[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);
}
+1 -1
View File
@@ -352,7 +352,7 @@ fn cursor_icon(c: Cursor) -> egui::CursorIcon {
Cursor::ResizeNwSe => C::ResizeNwSe,
Cursor::ResizeNeSw => C::ResizeNeSw,
Cursor::Rotate => C::Alias,
Cursor::Pen => C::Crosshair,
Cursor::Pen | Cursor::PenContinue => C::Crosshair,
Cursor::Text => C::Text,
Cursor::Hand => C::Grab,
Cursor::HandGrab => C::Grabbing,
+2
View File
@@ -55,6 +55,8 @@ const TABLE: &[(&str, [&str; 5])] = &[
("Cut", ["Ausschneiden", "Couper", "Cortar", "カット", "剪切"]),
("Copy", ["Kopieren", "Copier", "Copiar", "コピー", "复制"]),
("Paste", ["Einfügen", "Coller", "Pegar", "ペースト", "粘贴"]),
("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
@@ -30,6 +30,8 @@ pub(super) const TABLE: &[(&str, &str)] = &[
("Cut", "قص"),
("Copy", "نسخ"),
("Paste", "لصق"),
("Reverse Path", "عكس المسار"),
("Set Anchor", "تعيين نقطة الارتساء"),
("Clear", "مسح"),
("Delete", "حذف"),
("Duplicate", "تكرار"),
+2
View File
@@ -28,6 +28,8 @@ pub(super) const TABLE: &[(&str, &str)] = &[
("Cut", "Taglia"),
("Copy", "Copia"),
("Paste", "Incolla"),
("Reverse Path", "Inverti tracciato"),
("Set Anchor", "Imposta ancoraggio"),
("Clear", "Cancella"),
("Duplicate", "Duplica"),
("Select All", "Seleziona tutto"),
+2
View File
@@ -30,6 +30,8 @@ pub(super) const TABLE: &[(&str, &str)] = &[
("Cut", "カット"),
("Copy", "コピー"),
("Paste", "ペースト"),
("Reverse Path", "パスの方向反転"),
("Set Anchor", "アンカーポイントを設定"),
("Clear", "消去"),
("Duplicate", "複製"),
("Select All", "すべてを選択"),
+2
View File
@@ -31,6 +31,8 @@ pub(super) const TABLE: &[(&str, &str)] = &[
("Cut", "Recortar"),
("Copy", "Copiar"),
("Paste", "Colar"),
("Reverse Path", "Inverter demarcador"),
("Set Anchor", "Definir ponto de ancoragem"),
("Clear", "Limpar"),
("Delete", "Excluir"),
("Duplicate", "Duplicar"),
+2
View File
@@ -31,6 +31,8 @@ pub(super) const TABLE: &[(&str, &str)] = &[
("Cut", "Вирізати"),
("Copy", "Копіювати"),
("Paste", "Вставити"),
("Reverse Path", "Розвернути контур"),
("Set Anchor", "Задати прив’язку"),
("Clear", "Очистити"),
("Duplicate", "Дублювати"),
("Select All", "Виділити все"),
+2
View File
@@ -486,6 +486,8 @@ pub const MENUS: &[(&str, &[&str])] = &[
">Paths",
"cmd:object.makeCompoundPath",
"cmd:object.releaseCompoundPath",
"-",
"cmd:path.reverse",
"<",
">Pathfinder",
"cmd:object.pathfinder|Add|{\"op\": \"add\"}",