mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
Pen: the new-path asterisk leaves the pointer once a path is started
The Pen's pointer showed the new-path asterisk before the first anchor and also while a path was being drawn, where a click adds the path's next anchor. A new Cursor::PenDrawing, the pen nib without a badge, now shows there. The close, convert and join badges still show over the first anchor, the last anchor and another open path's end. The asterisk shows only where a click starts a new path. The Continue, Add and Delete badges now also show after another path was selected while the Pen was still drawing (for example with a Cmd-click). A click there already continued the path or added or deleted an anchor, but the pointer showed the asterisk.
This commit is contained in:
@@ -1051,7 +1051,7 @@ fn pen_with_cmd_borrows_direct_selection_and_goes_on_drawing() {
|
||||
gesture(&mut s, &[(100.0, 300.0)], none);
|
||||
gesture(&mut s, &[(200.0, 300.0), (250.0, 300.0)], none);
|
||||
let id = s.doc().unwrap().selection.objects[0];
|
||||
assert_eq!(s.cursor(Point::new(250.0, 300.0), none, v), vectorcraft_tools::Cursor::Pen);
|
||||
assert_eq!(s.cursor(Point::new(250.0, 300.0), none, v), vectorcraft_tools::Cursor::PenDrawing);
|
||||
assert_eq!(s.cursor(Point::new(250.0, 300.0), cmd, v), vectorcraft_tools::Cursor::ArrowHollow, "Direct Selection's");
|
||||
// The new anchor's outgoing handle: it moves, its incoming one turning with it.
|
||||
s.pointer(&PointerEvent::new(PointerKind::Down, 250.0, 300.0).with_mods(cmd), v).unwrap();
|
||||
@@ -1089,6 +1089,31 @@ fn pen_with_cmd_borrows_direct_selection_and_goes_on_drawing() {
|
||||
assert_eq!(s.tool_id(), "rectangle");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_pen_drops_the_new_path_asterisk_once_a_path_is_started() {
|
||||
// The Pen's asterisk means a click starts a new path: it shows until the first anchor is
|
||||
// placed, the pen has no badge while the path is drawn, and the asterisk comes back once the
|
||||
// path is finished (Enter, or a click on the first anchor that closes it).
|
||||
use vectorcraft_tools::Cursor;
|
||||
let (none, v) = (Mods::default(), view());
|
||||
let empty = Point::new(500.0, 500.0);
|
||||
let mut s = session();
|
||||
s.select_tool("pen", v).unwrap();
|
||||
assert_eq!(s.cursor(empty, none, v), Cursor::Pen);
|
||||
gesture(&mut s, &[(100.0, 300.0)], none);
|
||||
assert_eq!(s.cursor(empty, none, v), Cursor::PenDrawing, "after the first anchor");
|
||||
gesture(&mut s, &[(200.0, 300.0)], none);
|
||||
assert_eq!(s.cursor(empty, none, v), Cursor::PenDrawing);
|
||||
s.tool_key(ToolKey::Enter, none, v).unwrap();
|
||||
assert_eq!(s.cursor(empty, none, v), Cursor::Pen, "Enter finished the path");
|
||||
gesture(&mut s, &[(100.0, 400.0)], none);
|
||||
gesture(&mut s, &[(200.0, 400.0)], none);
|
||||
gesture(&mut s, &[(150.0, 450.0)], none);
|
||||
assert_eq!(s.cursor(Point::new(100.0, 400.0), none, v), Cursor::PenClose);
|
||||
gesture(&mut s, &[(100.0, 400.0)], none);
|
||||
assert_eq!(s.cursor(empty, none, v), Cursor::Pen, "closing finished the path");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reshaping_a_segment_keeps_smooth_anchors_smooth() {
|
||||
// #494: the anchors at the ends of a dragged segment keep their kind: a smooth one's other
|
||||
@@ -1176,7 +1201,7 @@ fn the_pen_continues_any_open_path_and_joins_another() {
|
||||
gesture(&mut s, &[(200.0, 100.0)], none);
|
||||
assert_eq!(s.doc().unwrap().selection.objects, vec![a]);
|
||||
assert_eq!(s.cursor(Point::new(300.0, 200.0), none, v), Cursor::PenJoin);
|
||||
assert_eq!(s.cursor(Point::new(350.0, 200.0), none, v), Cursor::Pen, "the middle of the other path");
|
||||
assert_eq!(s.cursor(Point::new(350.0, 200.0), none, v), Cursor::PenDrawing, "the middle of the other path");
|
||||
// #1009: Use Precise Cursors makes the join pointer a crosshair too.
|
||||
s.execute("prefs.set", &json!({"key": "usePreciseCursors", "value": true})).unwrap();
|
||||
assert_eq!(s.cursor(Point::new(300.0, 200.0), none, v), Cursor::Crosshair);
|
||||
|
||||
@@ -453,6 +453,7 @@ fn precise_cursors_replace_every_pen_state() {
|
||||
use vectorcraft_tools::Cursor;
|
||||
for c in [
|
||||
Cursor::Pen,
|
||||
Cursor::PenDrawing,
|
||||
Cursor::PenAdd,
|
||||
Cursor::PenDelete,
|
||||
Cursor::PenClose,
|
||||
|
||||
@@ -299,7 +299,7 @@ fn overlapping_other_endpoints_join_nearest_raw_end_before_grid_snapping() {
|
||||
#[test]
|
||||
fn another_path_join_keeps_snapped_target_when_raw_pointer_misses() {
|
||||
// Upstream also joins through snapping when raw hits no endpoint. Keep that fallback.
|
||||
grid_join((100.0, 300.0), (150.0, 300.0), (63.0, 303.0), true, Cursor::Pen);
|
||||
grid_join((100.0, 300.0), (150.0, 300.0), (63.0, 303.0), true, Cursor::PenDrawing);
|
||||
}
|
||||
|
||||
fn snapped_only_continue(preselected: bool) {
|
||||
|
||||
@@ -435,7 +435,10 @@ pub enum Cursor {
|
||||
RotateToward(u8),
|
||||
/// Over a Live Corners widget (drag to round the corners).
|
||||
CornerRadius,
|
||||
/// The Pen with the new-path asterisk: a click starts a new path.
|
||||
Pen,
|
||||
/// The Pen without a badge, while a path is being drawn: a click adds its next anchor.
|
||||
PenDrawing,
|
||||
PenAdd,
|
||||
PenDelete,
|
||||
PenClose,
|
||||
@@ -501,6 +504,7 @@ impl Cursor {
|
||||
pub fn precise(self) -> Self {
|
||||
match self {
|
||||
Cursor::Pen
|
||||
| Cursor::PenDrawing
|
||||
| Cursor::PenAdd
|
||||
| Cursor::PenDelete
|
||||
| Cursor::PenClose
|
||||
|
||||
+35
-5
@@ -17,6 +17,9 @@
|
||||
//! segment of a selected path adds an anchor there and a click on one of its anchors deletes it
|
||||
//! (Shift held or General → Disable Auto Add/Delete starts a new path instead). On a selected
|
||||
//! blend's spine a click adds a point (on a point no key object sits on: deletes it).
|
||||
//! The pointer shows the new-path asterisk where a click starts a new path. While a path is being
|
||||
//! drawn it shows no badge, except where a click closes the path, joins another open path or
|
||||
//! converts an anchor.
|
||||
//! Each anchor placed snaps to Smart Guides ([`DrawSnap`]): onto the anchors, centres and paths of
|
||||
//! the other art and of the path being drawn, into line with them, onto the construction guides
|
||||
//! through the last anchor; with Shift held it slides along its 45° step into line. Hovering shows
|
||||
@@ -388,7 +391,8 @@ impl Tool for PenTool {
|
||||
}
|
||||
fn cursor(&self, cx: &ToolContext, p: Point, m: Mods) -> Cursor {
|
||||
let tol = cx.tol(5.0).max(cx.point_tol());
|
||||
if let Some((id, first, last, _)) = self.active(cx) {
|
||||
let active = self.active(cx);
|
||||
if let Some((id, first, last, _)) = active {
|
||||
let closing_end = last_anchor(cx, id).filter(|(_, ai)| *ai > 0).map(|_| first);
|
||||
match pick_endpoint(closing_end, last, p, tol, Endpoint::First) {
|
||||
Some(Endpoint::First) => return Cursor::PenClose,
|
||||
@@ -402,7 +406,9 @@ impl Tool for PenTool {
|
||||
if alt_converts(cx, p, m) {
|
||||
return Cursor::PenConvert;
|
||||
}
|
||||
if !self.drawing {
|
||||
// These show whenever no path is being drawn, also after another path was selected while
|
||||
// the Pen was drawing.
|
||||
if active.is_none() {
|
||||
match spine_click(cx, p, tol).as_deref() {
|
||||
Some([Action::Exec(c, _)]) if c == "object.blend.spine.removeAnchor" => return Cursor::PenDelete,
|
||||
Some([_]) => return Cursor::PenAdd,
|
||||
@@ -419,7 +425,8 @@ impl Tool for PenTool {
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
Cursor::Pen
|
||||
// The new-path asterisk shows only while a click starts a new path.
|
||||
if active.is_some() { Cursor::PenDrawing } else { Cursor::Pen }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -977,7 +984,7 @@ mod tests {
|
||||
click(&mut narrow_pen, &narrow, near.x, near.y, Mods::default()),
|
||||
vec![Action::Begin("Convert Anchor Point".into()), set_out(id, 1, 60.0, 300.0)]
|
||||
);
|
||||
assert_eq!(narrow_pen.cursor(&narrow, Point::new(60.0, 300.0 + 6.0 / zoom), Mods::default()), Cursor::Pen);
|
||||
assert_eq!(narrow_pen.cursor(&narrow, Point::new(60.0, 300.0 + 6.0 / zoom), Mods::default()), Cursor::PenDrawing);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1243,7 +1250,7 @@ mod tests {
|
||||
let alt = Mods { alt: true, ..Mods::default() };
|
||||
let mut t = PenTool { drawing: true, ..PenTool::default() };
|
||||
assert_eq!(t.cursor(&cx, Point::new(150.0, 302.0), alt), Cursor::PenConvert);
|
||||
assert_eq!(t.cursor(&cx, Point::new(150.0, 302.0), Mods::default()), Cursor::Pen);
|
||||
assert_eq!(t.cursor(&cx, Point::new(150.0, 302.0), Mods::default()), Cursor::PenDrawing);
|
||||
assert_eq!(t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 150.0, 302.0).with_mods(alt)), vec![]);
|
||||
let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 150.0, 262.0).with_mods(alt));
|
||||
assert_eq!(a[0], Action::Begin("Reshape".into()));
|
||||
@@ -1278,7 +1285,30 @@ mod tests {
|
||||
sb.set([b]);
|
||||
let cx_b = cx(&d, &sb, &p);
|
||||
assert_eq!(t.cursor(&cx_b, Point::new(150.0, 350.0), Mods::default()), Cursor::Pen);
|
||||
// An end of B shows what a click there does: it goes on drawing B.
|
||||
assert_eq!(t.cursor(&cx_b, Point::new(60.0, 400.0), Mods::default()), Cursor::PenContinue);
|
||||
let acts = click(&mut t, &cx_b, 150.0, 350.0, Mods::default());
|
||||
assert!(matches!(&acts[..], [Action::Begin(_), Action::Preview(c, _)] if c == "path.create"), "{acts:?}");
|
||||
}
|
||||
|
||||
/// The new-path asterisk shows until a path is started; while one is being drawn the pen has no
|
||||
/// badge, and the asterisk comes back once the path is finished.
|
||||
#[test]
|
||||
fn the_new_path_asterisk_shows_only_before_a_path_is_started() {
|
||||
let (d, _, s) = drawing(vectorcraft_geom::SubPath::polyline(&[Point::new(10.0, 300.0)], false));
|
||||
let p = paint();
|
||||
let cx = cx(&d, &s, &p);
|
||||
let empty = Point::new(250.0, 450.0);
|
||||
let mut t = PenTool::default();
|
||||
assert_eq!(t.cursor(&cx, empty, Mods::default()), Cursor::Pen, "no path started");
|
||||
t.drawing = true;
|
||||
assert_eq!(t.cursor(&cx, empty, Mods::default()), Cursor::PenDrawing, "after the first anchor");
|
||||
let a = click(&mut t, &cx, empty.x, empty.y, Mods::default());
|
||||
assert!(matches!(&a[..], [Action::Begin(_), Action::Preview(c, _)] if c == "path.appendAnchor"), "{a:?}");
|
||||
assert_eq!(t.cursor(&cx, empty, Mods::default()), Cursor::PenDrawing);
|
||||
assert_eq!(t.cursor(&cx, empty, Mods { shift: true, ..Mods::default() }), Cursor::PenDrawing, "Shift constrains the next anchor");
|
||||
t.key(&cx, ToolKey::Enter, Mods::default());
|
||||
assert_eq!(t.cursor(&cx, empty, Mods::default()), Cursor::Pen, "the path is finished");
|
||||
assert_eq!(Cursor::PenDrawing.precise(), Cursor::Crosshair);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -553,9 +553,14 @@ fn cursor_icon(c: Cursor) -> egui::CursorIcon {
|
||||
Cursor::ResizeNwSe => C::ResizeNwSe,
|
||||
Cursor::ResizeNeSw => C::ResizeNeSw,
|
||||
Cursor::Rotate | Cursor::RotateToward(_) => C::Alias,
|
||||
Cursor::Pen | Cursor::PenAdd | Cursor::PenDelete | Cursor::PenClose | Cursor::PenContinue | Cursor::PenJoin | Cursor::PenConvert => {
|
||||
C::Crosshair
|
||||
}
|
||||
Cursor::Pen
|
||||
| Cursor::PenDrawing
|
||||
| Cursor::PenAdd
|
||||
| Cursor::PenDelete
|
||||
| Cursor::PenClose
|
||||
| Cursor::PenContinue
|
||||
| Cursor::PenJoin
|
||||
| Cursor::PenConvert => C::Crosshair,
|
||||
Cursor::Text => C::Text,
|
||||
Cursor::Hand => C::Grab,
|
||||
Cursor::HandGrab => C::Grabbing,
|
||||
@@ -2348,6 +2353,7 @@ mod tests {
|
||||
fn system_cursors_show_the_same_crosshair_with_precise_cursors_on_or_off() {
|
||||
for c in [
|
||||
Cursor::Pen,
|
||||
Cursor::PenDrawing,
|
||||
Cursor::PenAdd,
|
||||
Cursor::PenDelete,
|
||||
Cursor::PenClose,
|
||||
|
||||
@@ -335,6 +335,7 @@ fn glyph(c: Cursor, p: Pos2) -> Option<Vec<Shape>> {
|
||||
Cursor::RotateToward(octant) => rotate(ink, p, octant),
|
||||
Cursor::CornerRadius => corner_radius(ink, p),
|
||||
Cursor::Pen => pen(ink, p, "*"),
|
||||
Cursor::PenDrawing => pen(ink, p, ""),
|
||||
Cursor::PenAdd => pen(ink, p, "+"),
|
||||
Cursor::PenDelete => pen(ink, p, "-"),
|
||||
Cursor::PenClose => pen(ink, p, "o"),
|
||||
@@ -507,7 +508,7 @@ mod tests {
|
||||
use crate::VectorcraftApp;
|
||||
|
||||
/// Every cursor with a glyph.
|
||||
const GLYPHS: [Cursor; 38] = [
|
||||
const GLYPHS: [Cursor; 39] = [
|
||||
Cursor::Hand,
|
||||
Cursor::HandGrab,
|
||||
Cursor::ZoomIn,
|
||||
@@ -524,6 +525,7 @@ mod tests {
|
||||
Cursor::RotateToward(1),
|
||||
Cursor::CornerRadius,
|
||||
Cursor::Pen,
|
||||
Cursor::PenDrawing,
|
||||
Cursor::PenAdd,
|
||||
Cursor::PenDelete,
|
||||
Cursor::PenClose,
|
||||
@@ -603,6 +605,27 @@ mod tests {
|
||||
assert!(body[3] == 255 && body[..3].iter().all(|&c| c < 40), "{body:?}");
|
||||
}
|
||||
|
||||
/// The Pen drawing a path is the same nib as the new-path Pen, without the asterisk.
|
||||
#[test]
|
||||
fn the_drawing_pen_has_no_new_path_asterisk() {
|
||||
for ppp in [1.0, 2.0] {
|
||||
let (new, drawing) = (rasterize(Cursor::Pen, ppp).unwrap(), rasterize(Cursor::PenDrawing, ppp).unwrap());
|
||||
assert_eq!(new.hotspot, drawing.hotspot, "at {ppp}");
|
||||
// The asterisk's center, 19 × 17 points from the hotspot.
|
||||
let at = |img: &CustomCursorImage| {
|
||||
let [hx, hy] = img.hotspot.map(usize::from);
|
||||
let (x, y) = (hx + (19.0 * ppp) as usize, hy + (17.0 * ppp) as usize);
|
||||
if x < img.size[0] as usize && y < img.size[1] as usize { pixel(img, x, y)[3] } else { 0 }
|
||||
};
|
||||
assert_eq!(at(&new), 255, "the new-path Pen has its asterisk at {ppp}");
|
||||
assert_eq!(at(&drawing), 0, "the drawing Pen has no badge at {ppp}");
|
||||
// The nib is the same.
|
||||
let [hx, hy] = drawing.hotspot.map(usize::from);
|
||||
let nib = (hx + (8.0 * ppp) as usize, hy + (8.0 * ppp) as usize);
|
||||
assert_eq!(pixel(&new, nib.0, nib.1), pixel(&drawing, nib.0, nib.1), "at {ppp}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn each_bitmap_is_made_once_so_the_os_cursor_is_kept() {
|
||||
let mut images = Images::default();
|
||||
|
||||
@@ -551,7 +551,9 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- **Tools, panels and type, later 2026-10-14 merges:** the Transform panel's, Properties panel's and Control bar's X, Y, W and H show and edit the bounds of the anchor points selected with Direct Selection, and a whole-object selection is unchanged (#1839, fixes #1824). With Snap to Grid on, anchors and objects snap to the grid even when Snap to Pixel is also ticked: the grid wins (#1840; #1816 stays open until the reporter confirms). A marquee with the Selection or Direct Selection tool also selects the unlocked ruler guides it crosses (#1836). The toolbar shows no group labels by default (a preference brings them back) and runs on in one or two columns; the All Tools drawer lists the tools by flyout group with their shortcuts, has a menu (Reset, Basic, Advanced) and a Show section for the Fill/Stroke, Coloring, Draw Mode and Screen Mode controls (`toolbar.show`, #1751). An action clicked in the Actions panel stays selected, so Play Current Selection and Delete Selection act on it (#1853, part of #1629; editing actions and action shortcuts remain). Redo pressed during a drag keeps the drag as a step instead of losing the redone edit (#1856, #1719). Preferences are written as soon as OK is pressed, so they survive a session that doesn't end with a normal quit (#1857; #1747 stays open until the reporter confirms). New and Redefine Character Style take the format of the selected text range, also while typing (#1634). Type inside symbols, pattern tiles, graph designs and opacity masks gets its layout bounds back when a native file opens, so warped type in a symbol looks the same after save and reopen (#1833, #1738). Editing, renaming, deleting or merging a global swatch reaches the art of opacity masks, and a swatch used only in a mask is no longer listed as unused (#1830, #1735). On macOS, Save As, Save a Copy and the first Save show their sheet again; the Format menu on the sheet (#866) is withdrawn until the format can be set before the sheet opens, and #866 is open again (#1855; #1800, #1831 and #1518 stay open until confirmed on a Mac).
|
||||
- **Files and untrusted input, later 2026-10-14 merges:** SVG import applies style sheet rules with `:not()` selectors, with their nesting bounded (#1799, #1557), no longer stalls on a style sheet with many stacked descendant selectors over deep nesting (#1834, #1703), and reads an SVG's linked files within the automation roots in force again (#1870). DXF export writes the VPORT record's view direction once (group code 36 was written twice, which strict readers reject, #1789). A deeply nested file no longer overflows the stack: nesting is capped at 32 objects (deeper groups are merged with a warning; SVG past 256 elements and PDF past 256 nested arrays are refused), symbols and patterns that use themselves open without the offending instance or paint, and a document nested more than about 40 deep can no longer be saved to a file that won't open again (#1852). PDF and `.ai` files nested deeply inside compressed streams, page trees holding themselves and functions or colour spaces built on themselves no longer crash the app (hayro 0.8; the import runs on a 16 MB thread, #1871); a decompression bomb or impossible predictor parameters are refused (#1873, #1700, #1701); a brush whose art draws the brush again through a symbol or pattern no longer crashes the app, and graph designs, Affinity, DXF, metafiles and plug-ins were probed with hostile input (#1872). Hostile EPS, Affinity and native files can no longer run memory or CPU away through ASCII85 data, PostScript objects holding themselves, flat-colour pixel layers or far and overlapping data offsets (#1863: #1715, #1714, #1704, #1699), and the save fallback for folders that refuse a rename keeps the old file's content until the new one is in (#1695). A file whose ids sit at their largest value no longer breaks new objects (#1718); a failed `command.batch` puts each document back whole, its file, history and a drag in progress included (#1697); a control request that timed out before the app took it never runs later, and the reply says which case it was (#1725, [control-protocol.md](control-protocol.md)); the Eyedropper and recolouring refuse an image too large as RGBA before decoding it (#1717) (all #1862). A save or export that fails is reported in a dialog that names the file and stays until closed, with the document left modified and the old file untouched (#1685); a browser download that can't start fails the save instead of marking it saved (#1693); a file that finishes opening waits while a dialog is open, so Save As options can't save the wrong document (#1694) (all #1865). krilla is built without its text shaping, so the unmaintained rustybuzz and ttf-parser don't ship (#1874, #1730); the CI gate compiles the web app for wasm, and the web's SVG nesting cap (34) follows from its 1 MiB stack (#1875).
|
||||
- **Rendering, later 2026-10-14 merges:** copies, symbol instances and repeats that share an appearance keep their stroke outlines between frames (#1667). Wide drop shadows and glows at high zoom render within the raster budget (#1706); stacked Transform effects are bounded (#1705); 3D effects keep the area a nonzero compound path fills (#1724); pattern tiles redraw when a symbol, pattern or image their art shows changes (#1723); a template layer dims its images by its Dim Images percentage instead of fading the whole layer (#1683) (all #1864).
|
||||
- **Tool cursors:** the Eyedropper has an original pipette glyph with its tip at the hotspot;
|
||||
- **Tool cursors:** the Pen's pointer shows the new-path asterisk where a click starts a new path. While a
|
||||
path is being drawn it shows the pen without a badge, except over the first anchor (close), over the last
|
||||
anchor or with Alt held over a path (convert) and over another open path's end (join). The Eyedropper has an original pipette glyph with its tip at the hotspot;
|
||||
Use Precise Cursors restores the crosshair. The same glyph feeds OS bitmaps and painted
|
||||
cursors. DPI/hotspot and preference checks in `cursors.rs` and `tests_prefs.rs` cover #1269;
|
||||
actual OS display and Caps Lock remain unverified. Behaviour reference: [public tool-pointer documentation](https://helpx.adobe.com/illustrator/using/tools.html#change_tool_pointers). Tool cursors are painted into the window on Wayland (#1652; not run on Linux, #1645 stays open until confirmed). A Precise Cursors command toggles the preference and can take a shortcut (#1663).
|
||||
|
||||
Reference in New Issue
Block a user