Merge pull request #1663 from ham9zah/pen-drag-threshold

Pen: drag threshold, Precise Cursors command and tests
This commit is contained in:
BFlat 2026-10-14 04:28:59 +07:00 committed by GitHub
commit f8396dbc4f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 325 additions and 1 deletions

View File

@ -373,6 +373,134 @@ fn use_precise_cursors_makes_drawing_cursors_crosshairs() {
assert_eq!(cursor(&mut s, "eyedropper"), Cursor::Eyedropper); assert_eq!(cursor(&mut s, "eyedropper"), Cursor::Eyedropper);
} }
/// Use Precise Cursors changes the pointer of the tools that have one of their own: off, each shows its own
/// pointer; on, a crosshair. Every other drawing tool shows a crosshair either way, and the Type tool and the
/// Scissors over no path keep theirs.
#[test]
fn use_precise_cursors_swaps_the_tools_own_pointers_for_a_crosshair() {
use vectorcraft_tools::{Cursor, Mods};
let mut s = Session::new();
s.execute("file.new", &json!({"width": 400, "height": 400})).unwrap();
let at = vectorcraft_geom::Point::new(300.0, 300.0);
let cursor = |s: &mut Session, tool: &str| {
s.select_tool(tool, ViewInfo::default()).unwrap();
s.cursor(at, Mods::default(), ViewInfo::default())
};
// Off: the pointer of each tool whose own pointer the preference replaces.
let own = [
("pen", Cursor::Pen),
("addAnchor", Cursor::PenAdd),
("deleteAnchor", Cursor::PenDelete),
("anchorPoint", Cursor::Pen),
("curvature", Cursor::Pen),
("mesh", Cursor::PenAdd),
("eyedropper", Cursor::Eyedropper),
("slice", Cursor::Slice),
("blend", Cursor::Blend),
("shapeBuilder", Cursor::ShapeBuilder),
];
for (tool, c) in own {
assert_eq!(cursor(&mut s, tool), c, "{tool} off");
}
// The tools that show a crosshair whatever the preference says.
let always = [
"lineSegment",
"arc",
"spiral",
"rectangularGrid",
"polarGrid",
"rectangle",
"roundedRectangle",
"ellipse",
"polygon",
"star",
"flare",
"paintbrush",
"blobBrush",
"shaper",
"pencil",
"smooth",
"pathEraser",
"join",
"eraser",
"knife",
"mirrorCut",
"lineCut",
"rectCut",
"gradient",
];
for tool in always {
assert_eq!(cursor(&mut s, tool), Cursor::Crosshair, "{tool} off");
}
s.execute("prefs.set", &json!({"key": "usePreciseCursors", "value": true})).unwrap();
for (tool, _) in own {
assert_eq!(cursor(&mut s, tool), Cursor::Crosshair, "{tool} on");
}
for tool in always {
assert_eq!(cursor(&mut s, tool), Cursor::Crosshair, "{tool} on");
}
assert_eq!(cursor(&mut s, "type"), Cursor::Text);
assert_eq!(cursor(&mut s, "scissors"), Cursor::NotAllowed);
s.execute("prefs.set", &json!({"key": "usePreciseCursors", "value": false})).unwrap();
for (tool, c) in own {
assert_eq!(cursor(&mut s, tool), c, "{tool} off again");
}
}
/// Every state of the Pen's pointer, and the other drawing pointers, turn into a crosshair; the rest stay.
#[test]
fn precise_cursors_replace_every_pen_state() {
use vectorcraft_tools::Cursor;
for c in [
Cursor::Pen,
Cursor::PenAdd,
Cursor::PenDelete,
Cursor::PenClose,
Cursor::PenContinue,
Cursor::PenJoin,
Cursor::PenConvert,
Cursor::Eyedropper,
Cursor::Slice,
Cursor::Blend,
Cursor::BlendObject,
Cursor::BlendAnchor,
Cursor::ShapeBuilder,
Cursor::ShapeBuilderErase,
] {
assert_eq!(c.precise(), Cursor::Crosshair, "{c:?}");
}
for c in [Cursor::Arrow, Cursor::Move, Cursor::Text, Cursor::Hand, Cursor::NotAllowed, Cursor::SliceSelect] {
assert_eq!(c.precise(), c, "{c:?}");
}
}
/// Switching the preference is not an edit: no undo step, no unsaved changes.
#[test]
fn use_precise_cursors_is_not_an_edit() {
let mut s = Session::new();
s.execute("file.new", &json!({"width": 400, "height": 400})).unwrap();
let before = (s.active().unwrap().history.undo.len(), s.active().unwrap().history.redo.len());
assert!(!s.active().unwrap().is_dirty());
s.execute("prefs.set", &json!({"key": "usePreciseCursors", "value": true})).unwrap();
assert!(s.prefs.use_precise_cursors);
let st = s.active().unwrap();
assert_eq!((st.history.undo.len(), st.history.redo.len()), before, "no undo step");
assert!(!st.is_dirty(), "the document stays saved");
}
/// The preference is saved with the others and reads back as it was.
#[test]
fn use_precise_cursors_is_saved_and_restored() {
let mut s = Session::new();
assert!(!s.prefs.use_precise_cursors, "off by default");
s.execute("prefs.set", &json!({"key": "usePreciseCursors", "value": true})).unwrap();
let saved = s.prefs.to_json();
assert_eq!(saved["usePreciseCursors"], json!(true));
let mut t = Session::new();
t.apply_prefs(serde_json::from_value(saved).unwrap());
assert!(t.prefs.use_precise_cursors);
}
// ---------- #394, second batch ---------- // ---------- #394, second batch ----------
fn new_doc() -> Session { fn new_doc() -> Session {

View File

@ -0,0 +1,37 @@
//! A click after a smooth anchor: the new corner has no handles, so the segment curves from the previous anchor's side only.
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use serde_json::json;
use vectorcraft_doc::NodeKind;
use vectorcraft_engine::{Session, ViewInfo};
use vectorcraft_geom::Point;
use vectorcraft_tools::{PointerEvent, PointerKind};
#[test]
fn click_after_a_smooth_anchor_curves_from_its_side_only() {
let mut s = Session::new();
s.execute("file.new", &json!({"width": 800, "height": 600})).unwrap();
let view = ViewInfo { zoom: 1.0, smart_guides: false, snap_to_grid: false, snap_to_pixel: false, snap_to_point: false, ..Default::default() };
s.select_tool("pen", view).unwrap();
let mut at = |kind, x, y| s.pointer(&PointerEvent::new(kind, x, y), view).unwrap();
// Press and drag out a smooth anchor at (100, 100), then click a corner at (300, 100).
at(PointerKind::Down, 100.0, 100.0);
at(PointerKind::Drag, 150.0, 60.0);
at(PointerKind::Up, 150.0, 60.0);
at(PointerKind::Down, 300.0, 100.0);
at(PointerKind::Up, 300.0, 100.0);
let mut path = None;
s.doc().unwrap().doc.walk(|n| {
if let NodeKind::Path { path: p, .. } = &n.kind {
path = Some(p.clone());
}
});
let sp = &path.unwrap().subpaths[0];
let (a, b) = (&sp.anchors[0], &sp.anchors[1]);
assert_eq!(a.h_out, Point::new(150.0, 60.0), "the outgoing handle stays where the drag left it");
assert_eq!(a.h_in, Point::new(50.0, 140.0), "mirrored through the anchor");
assert!(!b.has_in() && !b.has_out(), "a click places a corner without handles");
assert!(!sp.segment_is_line(0), "the segment is a curve, shaped by the previous anchor's handle only");
assert_eq!(sp.segment(0).p2, b.p);
}

View File

@ -44,6 +44,11 @@ pub struct PenTool {
in_h: Point, in_h: Point,
/// The pointer at the last button-down or drag: Space held moves the anchor as far as it moves. /// The pointer at the last button-down or drag: Space held moves the anchor as far as it moves.
last: Point, last: Point,
/// Where the button went down: the drag threshold is measured from it.
pressed: Point,
/// The pointer has moved past the drag threshold since the press: a press released inside it
/// is a click, so the anchor stays a corner without handles.
dragged: bool,
/// The last anchor of the path being drawn, while a click retracts its outgoing handle or a /// The last anchor of the path being drawn, while a click retracts its outgoing handle or a
/// drag pulls a new one out: (path, subpath, anchor, its position). /// drag pulls a new one out: (path, subpath, anchor, its position).
handle: Option<(NodeId, usize, usize, Point)>, handle: Option<(NodeId, usize, usize, Point)>,
@ -53,6 +58,9 @@ pub struct PenTool {
snap: DrawSnap, snap: DrawSnap,
} }
/// Screen pixels the pointer must move after a press before it shapes handles instead of clicking.
const DRAG_THRESHOLD: f64 = 3.0;
/// An anchor being placed, as the press decided: where it is, the path it goes on (none: the /// An anchor being placed, as the press decided: where it is, the path it goes on (none: the
/// first anchor of a new path) and whether it closes that path (the press was on its first /// first anchor of a new path) and whether it closes that path (the press was on its first
/// anchor). The drag only shapes its handles, whatever the path looks like meanwhile: a closing /// anchor). The drag only shapes its handles, whatever the path looks like meanwhile: a closing
@ -181,6 +189,8 @@ impl Tool for PenTool {
} }
PointerKind::Down => { PointerKind::Down => {
self.last = ev.pos; self.last = ev.pos;
self.pressed = ev.pos;
self.dragged = false;
if let Some((id, first, last, _)) = active { if let Some((id, first, last, _)) = active {
let end = last_anchor(cx, id); let end = last_anchor(cx, id);
let closing_end = end.filter(|(_, ai)| *ai > 0).map(|_| first); let closing_end = end.filter(|(_, ai)| *ai > 0).map(|_| first);
@ -265,6 +275,16 @@ impl Tool for PenTool {
if let Some(t) = &mut self.convert { if let Some(t) = &mut self.convert {
return t.pointer(cx, ev); return t.pointer(cx, ev);
} }
// Illustrator treats a press as a drag only past a few screen pixels. `last` follows the
// pointer meanwhile, so Space pressed before the threshold moves the anchor by the
// next move only.
if !self.dragged {
if ev.pos.distance(self.pressed) < cx.tol(DRAG_THRESHOLD) {
self.last = ev.pos;
return vec![];
}
self.dragged = true;
}
if let Some((id, si, ai, a)) = self.handle { if let Some((id, si, ai, a)) = self.handle {
let mut h = ev.pos; let mut h = ev.pos;
if ev.mods.shift { if ev.mods.shift {
@ -466,6 +486,20 @@ mod tests {
use crate::testutil::*; use crate::testutil::*;
use vectorcraft_doc::Selection; use vectorcraft_doc::Selection;
#[test]
fn press_inside_drag_threshold_places_a_corner() {
let (d, s) = (vectorcraft_doc::Document::new(500.0, 500.0), Selection::default());
let paint = paint();
let cx =
ToolContext { zoom: 1.0, smart_guides: false, snap_to_point: false, snap_to_pixel: false, snap_to_grid: false, ..cx(&d, &s, &paint) };
let mut pen = PenTool::default();
pen.pointer(&cx, &PointerEvent::new(PointerKind::Down, 100.0, 100.0));
assert!(pen.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 101.0, 101.0)).is_empty());
assert_eq!(pen.pointer(&cx, &PointerEvent::new(PointerKind::Up, 101.0, 101.0)), vec![Action::Commit]);
pen.pointer(&cx, &PointerEvent::new(PointerKind::Down, 200.0, 200.0));
assert_eq!(pen.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 204.0, 200.0)).len(), 1);
}
#[test] #[test]
fn snapped_last_endpoint_converts_active_path_after_raw_miss() { fn snapped_last_endpoint_converts_active_path_after_raw_miss() {
let first = Point::new(100.0, 300.0); let first = Point::new(100.0, 300.0);
@ -677,6 +711,25 @@ mod tests {
assert_eq!(handles(t.pointer(&cx, &drag)), (json!([300.0, 440.0]), json!([340.0, 440.0]))); assert_eq!(handles(t.pointer(&cx, &drag)), (json!([300.0, 440.0]), json!([340.0, 440.0])));
} }
#[test]
fn space_pressed_inside_the_drag_threshold_moves_the_anchor_by_the_next_move_only() {
let (d, s) = (vectorcraft_doc::Document::new(500.0, 500.0), Selection::default());
let paint = paint();
let cx =
ToolContext { zoom: 1.0, smart_guides: false, snap_to_point: false, snap_to_pixel: false, snap_to_grid: false, ..cx(&d, &s, &paint) };
let space = Mods { space: true, ..Mods::default() };
let mut pen = PenTool::default();
pen.pointer(&cx, &PointerEvent::new(PointerKind::Down, 100.0, 100.0));
// Still inside the threshold, Space held: nothing happens yet.
assert!(pen.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 102.0, 100.0).with_mods(space)).is_empty());
// The move that crosses it moves the anchor by that move (2 px to 4 px), not by the 4 px since the press.
let acts = pen.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 104.0, 100.0).with_mods(space));
let Some(Action::Preview(c, v)) = acts.first() else { panic!("not a preview: {acts:?}") };
assert_eq!(c, "path.create");
assert_eq!(v["anchors"][0]["x"], json!(102.0), "moved by the delta");
assert_eq!(v["anchors"][0]["y"], json!(100.0));
}
#[test] #[test]
fn space_moves_the_anchor_being_placed() { fn space_moves_the_anchor_being_placed() {
let (mut d, _) = doc_with_rect(); let (mut d, _) = doc_with_rect();

View File

@ -484,7 +484,8 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
{ {
crate::place::paint_cursor(app, ui.ctx(), &painter, hp); crate::place::paint_cursor(app, ui.ctx(), &painter, hp);
} }
// Caps Lock gives precise (crosshair) cursors, like Illustrator; env opt-out for system cursors. // The glyph cursors are drawn here; General › Use Precise Cursors (the session turns the drawing tools' pointers
// into crosshairs, `Cursor::precise`) needs no handling. VECTORCRAFT_SYSTEM_CURSORS opts out of the glyphs.
let custom = std::env::var_os("VECTORCRAFT_SYSTEM_CURSORS").is_none(); let custom = std::env::var_os("VECTORCRAFT_SYSTEM_CURSORS").is_none();
// User Interface › Scale Cursor Proportional to UI: the glyphs grow with UI Scaling; // User Interface › Scale Cursor Proportional to UI: the glyphs grow with UI Scaling;
// off, they keep their size. // off, they keep their size.
@ -2209,6 +2210,32 @@ mod tests {
use vectorcraft_engine::Session; use vectorcraft_engine::Session;
use vectorcraft_geom::Shape as _; use vectorcraft_geom::Shape as _;
/// With VECTORCRAFT_SYSTEM_CURSORS set the glyphs aren't drawn and the system's cursors stand in: those of
/// the Pen and the other drawing tools are the system crosshair already, so Use Precise Cursors makes no
/// difference to them there.
#[test]
fn system_cursors_show_the_same_crosshair_with_precise_cursors_on_or_off() {
for c in [
Cursor::Pen,
Cursor::PenAdd,
Cursor::PenDelete,
Cursor::PenClose,
Cursor::PenContinue,
Cursor::PenJoin,
Cursor::PenConvert,
Cursor::Eyedropper,
Cursor::Slice,
Cursor::Blend,
Cursor::BlendObject,
Cursor::BlendAnchor,
Cursor::ShapeBuilder,
Cursor::ShapeBuilderErase,
] {
assert_eq!(cursor_icon(c), egui::CursorIcon::Crosshair, "{c:?}");
assert_eq!(cursor_icon(c), cursor_icon(c.precise()), "{c:?}");
}
}
/// #826: in a turned view each ruler still has ticks along its whole length; at 0° the top one /// #826: in a turned view each ruler still has ticks along its whole length; at 0° the top one
/// reads x where the canvas has it. /// reads x where the canvas has it.
#[test] #[test]

View File

@ -494,6 +494,7 @@
Three Point Perspective Tříbodová perspektiva Three Point Perspective Tříbodová perspektiva
Save Grid as Preset… Uložit mřížku jako přednastavení… Save Grid as Preset… Uložit mřížku jako přednastavení…
Snap to Pixel Přichytávat k obrazovým bodům Snap to Pixel Přichytávat k obrazovým bodům
Precise Cursors Přesné kurzory
Snap to Point Přichytávat k bodům Snap to Point Přichytávat k bodům
Snap to Glyph Přichytávat ke glyfům Snap to Glyph Přichytávat ke glyfům
New View… Nové zobrazení… New View… Nové zobrazení…

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -2724,6 +2724,7 @@ effect Points Punkte
Snap to Glyph An Glyphe ausrichten Snap to Glyph An Glyphe ausrichten
Snap to Grid Am Raster ausrichten Snap to Grid Am Raster ausrichten
Snap to Pixel An Pixel ausrichten Snap to Pixel An Pixel ausrichten
Precise Cursors Präzise Cursor
Snap to Point An Punkt ausrichten Snap to Point An Punkt ausrichten
Snapping Tolerance Einrasttoleranz Snapping Tolerance Einrasttoleranz
Social Square Post 1080×1080 Quadratischer Social-Media-Beitrag 1080×1080 Social Square Post 1080×1080 Quadratischer Social-Media-Beitrag 1080×1080

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -2774,6 +2774,7 @@
Snap to Glyph Ajustar a glifo Snap to Glyph Ajustar a glifo
Snap to Grid Ajustar a cuadrícula Snap to Grid Ajustar a cuadrícula
Snap to Pixel Ajustar a píxel Snap to Pixel Ajustar a píxel
Precise Cursors Cursores precisos
Snap to Point Ajustar a punto Snap to Point Ajustar a punto
Snapping Tolerance Tolerancia de ajuste Snapping Tolerance Tolerancia de ajuste
Social Square Post 1080×1080 Publicación cuadrada para redes 1080×1080 Social Square Post 1080×1080 Publicación cuadrada para redes 1080×1080

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -2771,6 +2771,7 @@
Snap to Glyph Magnétisme du glyphe Snap to Glyph Magnétisme du glyphe
Snap to Grid Magnétisme de la grille Snap to Grid Magnétisme de la grille
Snap to Pixel Magnétisme du pixel Snap to Pixel Magnétisme du pixel
Precise Cursors Pointeurs précis
Snap to Point Magnétisme du point Snap to Point Magnétisme du point
Snapping Tolerance Tolérance du magnétisme Snapping Tolerance Tolérance du magnétisme
Social Square Post 1080×1080 Publication carrée pour réseaux sociaux 1080×1080 Social Square Post 1080×1080 Publication carrée pour réseaux sociaux 1080×1080

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -3358,6 +3358,7 @@
Snap to Glyph Illesztés glifához Snap to Glyph Illesztés glifához
Snap to Grid Illesztés a rácshoz Snap to Grid Illesztés a rácshoz
Snap to Pixel Illesztés pixelhez Snap to Pixel Illesztés pixelhez
Precise Cursors Pontos kurzorok
Snap to Point Illesztés pontra Snap to Point Illesztés pontra
Snapping Tolerance Illesztési tűrés Snapping Tolerance Illesztési tűrés
Social Közösségi média Social Közösségi média

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -2774,6 +2774,7 @@
Snap to Glyph Aggancia a glifo Snap to Glyph Aggancia a glifo
Snap to Grid Aggancia alla griglia Snap to Grid Aggancia alla griglia
Snap to Pixel Aggancia ai pixel Snap to Pixel Aggancia ai pixel
Precise Cursors Cursori di precisione
Snap to Point Aggancia al punto Snap to Point Aggancia al punto
Snapping Tolerance Tolleranza di aggancio Snapping Tolerance Tolleranza di aggancio
Social Square Post 1080×1080 Post quadrato per social 1080×1080 Social Square Post 1080×1080 Post quadrato per social 1080×1080

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -514,6 +514,7 @@
Three Point Perspective 3 点遠近法 Three Point Perspective 3 点遠近法
Save Grid as Preset… グリッドをプリセットとして保存… Save Grid as Preset… グリッドをプリセットとして保存…
Snap to Pixel ピクセルにスナップ Snap to Pixel ピクセルにスナップ
Precise Cursors 精密カーソル
Snap to Point ポイントにスナップ Snap to Point ポイントにスナップ
Snap to Glyph グリフにスナップ Snap to Glyph グリフにスナップ
New View… 新規表示… New View… 新規表示…

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -512,6 +512,7 @@
Three Point Perspective 3점 투시 Three Point Perspective 3점 투시
Save Grid as Preset… 격자를 프리셋으로 저장… Save Grid as Preset… 격자를 프리셋으로 저장…
Snap to Pixel 픽셀에 스냅 Snap to Pixel 픽셀에 스냅
Precise Cursors 정밀 커서
Snap to Point 포인트에 스냅 Snap to Point 포인트에 스냅
Snap to Glyph 글리프에 스냅 Snap to Glyph 글리프에 스냅
New View… 새 보기… New View… 새 보기…

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -489,6 +489,7 @@
Three Point Perspective Perspektywa trzypunktowa Three Point Perspective Perspektywa trzypunktowa
Save Grid as Preset… Zapisz siatkę jako ustawienie… Save Grid as Preset… Zapisz siatkę jako ustawienie…
Snap to Pixel Przyciągaj do pikseli Snap to Pixel Przyciągaj do pikseli
Precise Cursors Precyzyjne kursory
Snap to Point Przyciągaj do punktów Snap to Point Przyciągaj do punktów
Snap to Glyph Przyciągaj do glifów Snap to Glyph Przyciągaj do glifów
New View… Nowy widok… New View… Nowy widok…

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -2306,6 +2306,7 @@
Snap to Glyph Ajustar ao Glifo Snap to Glyph Ajustar ao Glifo
Snap to Grid Ajustar à Grade Snap to Grid Ajustar à Grade
Snap to Pixel Ajustar ao Pixel Snap to Pixel Ajustar ao Pixel
Precise Cursors Cursores Precisos
Snap to Point Ajustar ao Ponto Snap to Point Ajustar ao Ponto
Snapping Tolerance Tolerância de Ajuste Snapping Tolerance Tolerância de Ajuste
Soft Light Luz Suave Soft Light Luz Suave

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -2770,6 +2770,7 @@
Snap to Glyph Привязать к глифу Snap to Glyph Привязать к глифу
Snap to Grid Привязать к сетке Snap to Grid Привязать к сетке
Snap to Pixel Привязать к пикселю Snap to Pixel Привязать к пикселю
Precise Cursors Точные курсоры
Snap to Point Привязать к точке Snap to Point Привязать к точке
Snapping Tolerance Допуск привязки Snapping Tolerance Допуск привязки
Social Square Post 1080×1080 Квадратный пост для соцсетей 1080×1080 Social Square Post 1080×1080 Квадратный пост для соцсетей 1080×1080

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -2767,6 +2767,7 @@ effect Points Точки
Snap to Glyph Прив’язати до гліфу Snap to Glyph Прив’язати до гліфу
Snap to Grid Прив’язати до сітки Snap to Grid Прив’язати до сітки
Snap to Pixel Прив’язати до пікселя Snap to Pixel Прив’язати до пікселя
Precise Cursors Точні курсори
Snap to Point Прив’язати до точки Snap to Point Прив’язати до точки
Snapping Tolerance Допуск прив’язки Snapping Tolerance Допуск прив’язки
Social Square Post 1080×1080 Квадратний пост для соцмереж 1080×1080 Social Square Post 1080×1080 Квадратний пост для соцмереж 1080×1080

Can't render this file because it has a wrong number of fields in line 13.

View File

@ -3075,6 +3075,7 @@
Snap to Glyph Hút vào glyph Snap to Glyph Hút vào glyph
Snap to Grid Hút vào lưới Snap to Grid Hút vào lưới
Snap to Pixel Hút vào pixel Snap to Pixel Hút vào pixel
Precise Cursors Con trỏ chính xác
Snap to Point Hút vào điểm Snap to Point Hút vào điểm
Snapping Tolerance Dung sai hút Snapping Tolerance Dung sai hút
Social Square Post 1080×1080 Bài đăng vuông mạng xã hội 1080×1080 Social Square Post 1080×1080 Bài đăng vuông mạng xã hội 1080×1080

Can't render this file because it has a wrong number of fields in line 11.

View File

@ -2661,6 +2661,7 @@
Snap to Glyph 靠齐字符 Snap to Glyph 靠齐字符
Snap to Grid 对齐网格 Snap to Grid 对齐网格
Snap to Pixel 对齐像素 Snap to Pixel 对齐像素
Precise Cursors 精确光标
Snap to Point 对齐点 Snap to Point 对齐点
Snapping Tolerance 吸附容差 Snapping Tolerance 吸附容差
Social Square Post 1080×1080 社交方形帖子 1080×1080 Social Square Post 1080×1080 社交方形帖子 1080×1080

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -2752,6 +2752,7 @@
Snap to Glyph 靠齊字符 Snap to Glyph 靠齊字符
Snap to Grid 靠齊格點 Snap to Grid 靠齊格點
Snap to Pixel 靠齊像素 Snap to Pixel 靠齊像素
Precise Cursors 精確游標
Snap to Point 靠齊控制點 Snap to Point 靠齊控制點
Snapping Tolerance 靠齊容許度 Snapping Tolerance 靠齊容許度
Social Square Post 1080×1080 社群正方形貼文 1080×1080 Social Square Post 1080×1080 社群正方形貼文 1080×1080

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -186,6 +186,12 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
("view.trimView", "Trim View", "", "{} toggle: hide everything outside the artboards"), ("view.trimView", "Trim View", "", "{} toggle: hide everything outside the artboards"),
("view.cornerWidget", "Hide Corner Widget", "", "{} toggle the live corner widgets"), ("view.cornerWidget", "Hide Corner Widget", "", "{} toggle the live corner widgets"),
("view.snapToPixel", "Snap to Pixel", "", "{} toggle: drawing and moving land on whole pixels"), ("view.snapToPixel", "Snap to Pixel", "", "{} toggle: drawing and moving land on whole pixels"),
(
"view.preciseCursors",
"Precise Cursors",
"",
"{} toggle General › Use Precise Cursors: the drawing tools' pointers become a crosshair → true when on",
),
("view.textThreads", "Hide Text Threads", "Cmd+Shift+Y", "{} toggle the thread lines between threaded text frames"), ("view.textThreads", "Hide Text Threads", "Cmd+Shift+Y", "{} toggle the thread lines between threaded text frames"),
("view.gradientAnnotator", "Hide Gradient Annotator", "Cmd+Alt+G", "{} toggle the Gradient tool's annotator"), ("view.gradientAnnotator", "Hide Gradient Annotator", "Cmd+Alt+G", "{} toggle the Gradient tool's annotator"),
("type.hiddenCharacters", "Show Hidden Characters", "Cmd+Alt+I", "{} toggle markers for spaces, paragraph ends and story ends"), ("type.hiddenCharacters", "Show Hidden Characters", "Cmd+Alt+I", "{} toggle markers for spaces, paragraph ends and story ends"),
@ -1053,6 +1059,15 @@ pub fn run_ui_command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<R
None => Err("no such saved selection".into()), None => Err("no such saved selection".into()),
}, },
"view.snapToPixel" => flag(&mut app.ui.view.snap_to_pixel), "view.snapToPixel" => flag(&mut app.ui.view.snap_to_pixel),
// The preference itself (General › Use Precise Cursors), so the command, the dialog and the saved
// preferences share one value.
"view.preciseCursors" => {
let on = !app.session.prefs.use_precise_cursors;
app.run("prefs.set", json!({"key": "usePreciseCursors", "value": on})).map(|_| {
crate::prefs_dialog::snapshot(app);
json!(on)
})
}
"view.textThreads" => flag(&mut app.ui.view.text_threads), "view.textThreads" => flag(&mut app.ui.view.text_threads),
"type.hiddenCharacters" => flag(&mut app.ui.view.hidden_chars), "type.hiddenCharacters" => flag(&mut app.ui.view.hidden_chars),
"type.bold" => crate::panels::character::toggle_face(app, Face::Bold), "type.bold" => crate::panels::character::toggle_face(app, Face::Bold),
@ -3993,6 +4008,55 @@ mod tests {
assert_eq!(dynamic_label(&app, "view.cornerWidget", ""), "Show Corner Widget"); assert_eq!(dynamic_label(&app, "view.cornerWidget", ""), "Show Corner Widget");
} }
/// View › Precise Cursors has no menu item or default key, but the command (assignable in Keyboard Shortcuts)
/// writes General › Use Precise Cursors, the value the dialog and the saved preferences read.
#[test]
fn precise_cursors_command_toggles_the_preference() {
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), crate::Services::default());
assert!(!app.session.prefs.use_precise_cursors);
assert_eq!(app.run("view.preciseCursors", json!({})).unwrap(), json!(true));
assert!(app.session.prefs.use_precise_cursors);
assert_eq!(app.ui.engine_prefs["usePreciseCursors"], json!(true), "saved with the UI state");
assert!(UI_COMMANDS.iter().any(|c| c.0 == "view.preciseCursors" && c.2.is_empty()), "assignable, no default key");
assert_eq!(app.run("view.preciseCursors", json!({})).unwrap(), json!(false));
assert!(!app.session.prefs.use_precise_cursors);
}
/// The command is listed in Edit › Keyboard Shortcuts without a key and takes one the user types; with the
/// key it is the command that runs.
#[test]
fn precise_cursors_command_is_listed_and_assignable_in_the_shortcut_editor() {
use crate::shortcut_editor as ed;
let e = ed::entry("view.preciseCursors").expect("listed in the editor");
assert_eq!((e.label.as_str(), e.group.as_str(), e.is_tool), ("Precise Cursors", "View", false));
assert_eq!(ed::default_of("view.preciseCursors"), None, "no default key");
let mut overrides = std::collections::BTreeMap::new();
assert_eq!(ed::effective_in(&overrides, "view.preciseCursors"), None);
assert_eq!(ed::assign(&mut overrides, "view.preciseCursors", Some("Cmd+Alt+Shift+U"), false), Ok(vec![]));
let chord = ed::effective_in(&overrides, "view.preciseCursors").expect("assigned");
assert_eq!(ed::normalize(&chord), ed::normalize("Cmd+Alt+Shift+U"));
// A key another command holds is refused, so the new command can't take it over silently.
assert!(ed::assign(&mut overrides, "view.preciseCursors", Some("Cmd+Alt+Shift+P"), false).is_err());
assert_eq!(ed::assign(&mut overrides, "view.preciseCursors", None, false), Ok(vec![]));
assert_eq!(ed::effective_in(&overrides, "view.preciseCursors"), None, "cleared again");
}
/// The command is a preference, not an edit: no undo step, no unsaved changes, with a document open.
#[test]
fn precise_cursors_command_is_not_an_edit() {
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), crate::Services::default());
app.run("file.new", json!({})).unwrap();
let steps = |app: &VectorcraftApp| {
let st = app.session.active().unwrap();
(st.history.undo.len(), st.history.redo.len(), st.is_dirty())
};
let before = steps(&app);
assert!(!before.2);
app.run("view.preciseCursors", json!({})).unwrap();
assert!(app.session.prefs.use_precise_cursors);
assert_eq!(steps(&app), before);
}
/// Selection & Anchor Display › Zoom to Selection (#394): Zoom In and Zoom Out bring the /// Selection & Anchor Display › Zoom to Selection (#394): Zoom In and Zoom Out bring the
/// selection to the middle; off, they keep the view's centre. /// selection to the middle; off, they keep the view's centre.
#[test] #[test]