From beea693ee132b740400466392f64a1d0085bf785 Mon Sep 17 00:00:00 2001 From: wstock <1394687+wstock@users.noreply.github.com> Date: Thu, 8 Oct 2026 10:52:03 +0000 Subject: [PATCH] Part of #394: Type Object Selection by Path Only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Preferences › Type › Type Object Selection by Path Only was stored and never read. On, the Selection tools pick type on its type path only — point type's baseline (the line the selection marks under it, across its layout's width), area type's frame, type on a path's path — within the selection tolerance; a click among the glyphs or inside the frame selects nothing. Off (the default), anywhere in the type's bounds selects it, as before. `HitOptions` gains `type_path_only`, set by the tools' context (`hit_options()`) and the canvas's `hit_at` from the preference, so the Selection tools, Cmd-click behind, object highlighting and the context menu agree. Tests at the hit-test level (point, area and on-path type) and through the Selection tool with `prefs.set`. Co-Authored-By: Claude Fable 5.1 --- ROADMAP.md | 6 +-- crates/doc/src/hit.rs | 64 +++++++++++++++++++++++++++++++- crates/engine/src/tests_live.rs | 7 +++- crates/engine/src/tests_prefs.rs | 32 ++++++++++++++++ crates/engine/src/tooling.rs | 1 + crates/tools/src/lib.rs | 11 +++++- crates/tools/src/text.rs | 2 +- crates/ui-egui/src/canvas.rs | 4 +- docs/mcp.md | 4 ++ 9 files changed, 122 insertions(+), 9 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index a608bc65..28d489cc 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -4,7 +4,7 @@ VectorCraft is a clean-room, open-source, pure-Rust reimplementation of the Adob This file tracks **how far we are and what's left**. Time estimates are wall-clock hours of continuous Claude Opus 5.5 agent work (including builds and the CI gate), given both for **one agent** and for **4–6 parallel agents** on disjoint crates. They are counted from the remaining work (see [Parity estimate](#parity-estimate)), calibrated against measured throughput, and updated as work lands. -_Last updated: 2026-10-08 (after M4.14–M4.98, M8.1–M8.20, M14.4–M14.7, the MCP prompts, resource templates, completions and logging, the 2026-10-07 issue fixes, the Layers panel rework and the 2026-10-08 community contributions: Pen and shape-tool modifiers, path editing, Layers, Artboards, Swatches/Brushes and dialogs, Japanese composition and PDF text, Hebrew/Arabic type, interface languages (Spanish among them), the font menu, saved selections, MCP and EPS, the missing-font export warning (#319), open paths in the Shape Builder (#374), PDF/`.ai` import keeping strokes live and hidden and nested layers (#372), per-corner Live Corners (#381), SVG type opening at the size it draws at (#396), the Wayland clipboard with copied-file paste (#398), ruler guides that can be selected, moved and deleted (#414), two batches of Preferences options that were stored but unused (#394), skeleton reads with `document.find` for large documents over MCP (#416) and anchors dragged with Direct Selection snapping to points, segments and smart guides, clipped SVG layers opening as clipping layers (#420), PDF/`.ai` layers read on pages of tens of millions of operators (#419), Document Color Mode putting RGB greys on the black plate on request and converting pattern tiles (#421) and movable dialogs with Offset Path's Joins dropdown (#423) and numeric fields scrubbed by dragging their label (#400), stroke weight presets in the stroke unit (#426), the Appearance panel's Opacity popup (#430), type on a path brackets, flip and Type on a Path Options that keep the type in place (#429), Layers rows dragged with a ghost and dropped on the trash or New Layer (#435), tool flyouts opening on a right or corner press (#436), tool flyouts torn off into floating toolbars (#437), the Shape Builder's erase mode deleting edges too (#439), installed fonts registered outside the font folders and fonts installed while the app runs listed in the font menus (#443), alike circular corners on rectangles that aren't square (#442), tool cursors as OS cursors so the pointer no longer trails over the canvas (#444), artboards copied and pasted with their art (#446), guides dragged out of the rulers snapping to the art's side midpoints and the artboards, artboard guides (#451), the toolbar's double arrow and Window › Toolbars › Double Column switching between one and two columns in any window height (#455), the Smart Guides display options and Snapping Tolerance (#394), Font Size and Leading combo boxes with preset lists in the Character and Properties panels and the Control bar (#458), screenshots pasted from the clipboard on macOS, Windows and the web (#457), the Properties panel's Fill and Stroke matching the Control bar's, Illustrator's Window menu panel keys (Shift+F7 for Align…) and Alt-wheel zoom whatever the wheel preference (Discord feedback), the point/area type widget (Discord feedback), groups in legacy Illustrator .ai files (Discord feedback), a rotating desktop log file (#465), image controls in the Control bar and Properties panel: Image Trace with its presets, Mask, Crop Image, Embed/Unembed, and an Image Trace object's preset, Expand and Release (Discord feedback), the Pen working as the Anchor Point tool with Alt held over a selected path's handles and anchors, and anchor buttons that convert, connect and cut paths at the selected anchors (Discord feedback), Japanese hanging punctuation (burasagari, #463), Escape leaving Presentation Mode with no screen mode saved across restarts, documents dropped on the desktop app opening as tabs of their own and a note that an `.ai` file's art off its artboards isn't in the part read (#472), Add/Delete Anchor Point tool icons drawn from the Pen tool's (#479), EPS files from Illustrator 2026, Inkscape/cairo, matplotlib, CorelDRAW, Affinity, LibreOffice and Ghostscript opening as vectors, and a preview that stands in saying why (Discord feedback, #466, #469, #474), chords with modifiers recorded in Edit › Keyboard Shortcuts (#487); see [Honest assessment](#honest-assessment-2026-10-05))._ +_Last updated: 2026-10-08 (after M4.14–M4.98, M8.1–M8.20, M14.4–M14.7, the MCP prompts, resource templates, completions and logging, the 2026-10-07 issue fixes, the Layers panel rework and the 2026-10-08 community contributions: Pen and shape-tool modifiers, path editing, Layers, Artboards, Swatches/Brushes and dialogs, Japanese composition and PDF text, Hebrew/Arabic type, interface languages (Spanish among them), the font menu, saved selections, MCP and EPS, the missing-font export warning (#319), open paths in the Shape Builder (#374), PDF/`.ai` import keeping strokes live and hidden and nested layers (#372), per-corner Live Corners (#381), SVG type opening at the size it draws at (#396), the Wayland clipboard with copied-file paste (#398), ruler guides that can be selected, moved and deleted (#414), two batches of Preferences options that were stored but unused (#394), skeleton reads with `document.find` for large documents over MCP (#416) and anchors dragged with Direct Selection snapping to points, segments and smart guides, clipped SVG layers opening as clipping layers (#420), PDF/`.ai` layers read on pages of tens of millions of operators (#419), Document Color Mode putting RGB greys on the black plate on request and converting pattern tiles (#421) and movable dialogs with Offset Path's Joins dropdown (#423) and numeric fields scrubbed by dragging their label (#400), stroke weight presets in the stroke unit (#426), the Appearance panel's Opacity popup (#430), type on a path brackets, flip and Type on a Path Options that keep the type in place (#429), Layers rows dragged with a ghost and dropped on the trash or New Layer (#435), tool flyouts opening on a right or corner press (#436), tool flyouts torn off into floating toolbars (#437), the Shape Builder's erase mode deleting edges too (#439), installed fonts registered outside the font folders and fonts installed while the app runs listed in the font menus (#443), alike circular corners on rectangles that aren't square (#442), tool cursors as OS cursors so the pointer no longer trails over the canvas (#444), artboards copied and pasted with their art (#446), guides dragged out of the rulers snapping to the art's side midpoints and the artboards, artboard guides (#451), the toolbar's double arrow and Window › Toolbars › Double Column switching between one and two columns in any window height (#455), the Smart Guides display options and Snapping Tolerance (#394), Font Size and Leading combo boxes with preset lists in the Character and Properties panels and the Control bar (#458), screenshots pasted from the clipboard on macOS, Windows and the web (#457), the Properties panel's Fill and Stroke matching the Control bar's, Illustrator's Window menu panel keys (Shift+F7 for Align…) and Alt-wheel zoom whatever the wheel preference (Discord feedback), the point/area type widget (Discord feedback), groups in legacy Illustrator .ai files (Discord feedback), a rotating desktop log file (#465), image controls in the Control bar and Properties panel: Image Trace with its presets, Mask, Crop Image, Embed/Unembed, and an Image Trace object's preset, Expand and Release (Discord feedback), the Pen working as the Anchor Point tool with Alt held over a selected path's handles and anchors, and anchor buttons that convert, connect and cut paths at the selected anchors (Discord feedback), Japanese hanging punctuation (burasagari, #463), Escape leaving Presentation Mode with no screen mode saved across restarts, documents dropped on the desktop app opening as tabs of their own and a note that an `.ai` file's art off its artboards isn't in the part read (#472), Add/Delete Anchor Point tool icons drawn from the Pen tool's (#479), EPS files from Illustrator 2026, Inkscape/cairo, matplotlib, CorelDRAW, Affinity, LibreOffice and Ghostscript opening as vectors, and a preview that stands in saying why (Discord feedback, #466, #469, #474), chords with modifiers recorded in Edit › Keyboard Shortcuts (#487), Type Object Selection by Path Only (#394); see [Honest assessment](#honest-assessment-2026-10-05))._ ## Where we are @@ -105,7 +105,7 @@ grade by behaviour against `plan/illustrator/`, not by whether a menu item exist - A canvas context menu: right-click selects the object under the pointer and lists what applies to the selection (Undo/Redo, clipboard, group, isolation, join, Remove Anchor Points when anchors are selected, masks, compound paths, guides, Transform, Arrange, Select, Export Selection), or the view commands on empty canvas. - Save / Don't Save / Cancel before closing or quitting with unsaved documents (tabs, Close, Close All, Quit, window close). - Four brightness themes, persistent preferences. - - Preferences that act (#394, in part): Show Tool Tips, Double Click To Isolate, Zoom with Mouse Wheel (the wheel zooms about the pointer, Shift-wheel scrolls up and down, Cmd/Ctrl-wheel sideways; Alt/Option-wheel and a trackpad pinch zoom about the pointer either way), Use Precise Cursors; Selection & Anchor Display › Tolerance (objects, anchors and handles), Object Selection by Path Only, Command Click to Select Objects Behind (cycling down the stack), Zoom to Selection, anchor Size, Highlight anchors on mouse over; Type › Number of Recent Fonts (up to 15); Guides & Grid › guide and grid Color and Style (lines or dots) and Grids In Back. Then Snap to Point (its distance, for moves, drawn points and reference points), Move Locked and Hidden Artwork with Artboard, Enable Rubber Band for the Pen and Curvature tools, Handles style, Show handles when multiple anchors are selected, Hide Corner Widget for angles greater than; General › Transform Pattern Tiles (the transforms' and dialogs' Transform Patterns) and Select Same Tint %; Type › Size/Leading, Tracking and Baseline Shift increments (`type.step`, Cmd+Shift+. and Cmd+Shift+, for the size, the Type tool's Alt+arrows), Enable Missing Glyph Protection and Fill New Type Objects With Placeholder Text. Then Smart Guides › Color, Alignment Guides, Anchor/Path Labels, Measurement Labels, Transform Tools, Object Highlighting (with Smart Guides on) and Snapping Tolerance (4 px by default, for every smart guide snap). They reach agents too (`prefs.set`, then `pointer_gesture` or `press_key`; `ui.wheel` on the control channel). About 38 stored options still do nothing yet (the rest of #394: Auto Size New Area Type, Show Font Names in English, Highlight Alternate Glyphs, Construction and Spacing Guides, performance, devices…). + - Preferences that act (#394, in part): Show Tool Tips, Double Click To Isolate, Zoom with Mouse Wheel (the wheel zooms about the pointer, Shift-wheel scrolls up and down, Cmd/Ctrl-wheel sideways; Alt/Option-wheel and a trackpad pinch zoom about the pointer either way), Use Precise Cursors; Selection & Anchor Display › Tolerance (objects, anchors and handles), Object Selection by Path Only, Command Click to Select Objects Behind (cycling down the stack), Zoom to Selection, anchor Size, Highlight anchors on mouse over; Type › Number of Recent Fonts (up to 15); Guides & Grid › guide and grid Color and Style (lines or dots) and Grids In Back. Then Snap to Point (its distance, for moves, drawn points and reference points), Move Locked and Hidden Artwork with Artboard, Enable Rubber Band for the Pen and Curvature tools, Handles style, Show handles when multiple anchors are selected, Hide Corner Widget for angles greater than; General › Transform Pattern Tiles (the transforms' and dialogs' Transform Patterns) and Select Same Tint %; Type › Size/Leading, Tracking and Baseline Shift increments (`type.step`, Cmd+Shift+. and Cmd+Shift+, for the size, the Type tool's Alt+arrows), Enable Missing Glyph Protection and Fill New Type Objects With Placeholder Text. Then Smart Guides › Color, Alignment Guides, Anchor/Path Labels, Measurement Labels, Transform Tools, Object Highlighting (with Smart Guides on) and Snapping Tolerance (4 px by default, for every smart guide snap). Then Type › Type Object Selection by Path Only (type is picked on its baseline, frame or path, no longer anywhere in its bounds). They reach agents too (`prefs.set`, then `pointer_gesture` or `press_key`; `ui.wheel` on the control channel). About 37 stored options still do nothing yet (the rest of #394: Auto Size New Area Type, Show Font Names in English, Highlight Alternate Glyphs, Construction and Spacing Guides, performance, devices…). - Dialogs move by dragging their heading (the band down from the top edge, as a title bar), stay on screen and keep where they were moved for the session; the form dialogs' choices are dropdowns (Offset Path's Joins, Average's and Shear's and Reflect's axis), and Offset Path lists Offset, Joins, Miter Limit and previews (#423). - Dialogs open with their first field focused and its value selected: type a value and press Enter (Move, Rotate, Scale…). Number fields in panels and dialogs step with Up/Down (Shift: ten, Ctrl/Cmd: a tenth of their unit), applied at once. - Scrubby labels (#400, beyond Illustrator, as in Photoshop): dragging a numeric field's label left or right (the ↔ cursor shows over it) steps the value one unit every 2 points (Shift: ten, Ctrl/Cmd: a tenth; counts stay whole), in the field's unit and within its limits, applied live as the art follows; the whole drag is one undo step and Escape puts the value back. A click on the label focuses the field; the text box itself never scrubs, and a field with no label doesn't. Text labels before a field (Transform, Properties, Control bar, Stroke, Transparency, form rows in dialogs) and the Character and Paragraph panels' icons act as labels. Preferences › General › Scrub Numeric Fields by Dragging (on by default) turns it off. @@ -215,7 +215,7 @@ about 1–1.5 agent-hours each, so the other rows (estimated on the older scale) | Export for Screens, Asset Export, slices, Save for Web | 3 | 85% | polish only (all four are done) | 2–4 | | Print, colour management, separations, flattener | 3 | 80% | print preview fidelity (flattener presets in PDF and print, overprint in PDF and composite print, Print dialog, presets, PostScript, marks, separations, Print Tiling, print overprint/flattener/bitmap options and printer profile are done) | 1–2 | | Automation | 3 | 60% | Variables (data merge), scripting surface, batch | 8–12 | -| UI chrome (panels, contextual Properties, workspaces, prefs) | 6 | 78% | Variables, SVG Interactivity, Properties per context, Consolidate All Windows, context menus beyond the canvas (Links, Asset Export, CSS Properties, Attributes and the canvas context menu are done), about 38 Preferences options stored but not yet acted on (#394: Construction and Spacing Guides, Auto Size New Area Type, Show Font Names in English, Highlight Alternate Glyphs, Show Pixel Grid, performance, devices…) | 14–22 | +| UI chrome (panels, contextual Properties, workspaces, prefs) | 6 | 78% | Variables, SVG Interactivity, Properties per context, Consolidate All Windows, context menus beyond the canvas (Links, Asset Export, CSS Properties, Attributes and the canvas context menu are done), about 37 Preferences options stored but not yet acted on (#394: Construction and Spacing Guides, Auto Size New Area Type, Show Font Names in English, Highlight Alternate Glyphs, Show Pixel Grid, performance, devices…) | 14–22 | | Libraries, Links, Package | 2 | 50% | a local Libraries panel (no cloud by design); Links and Package are done | 5–8 | | **Feature parity** | **103** | **~75%** | | **~210–330** | | Interaction-fidelity pass (side by side with Illustrator: every tool modifier, cursor, dialog, Properties context) | | | | 60–90 | diff --git a/crates/doc/src/hit.rs b/crates/doc/src/hit.rs index 58c26a87..29cde418 100644 --- a/crates/doc/src/hit.rs +++ b/crates/doc/src/hit.rs @@ -50,11 +50,15 @@ pub struct HitOptions { pub outline: bool, /// "Object Selection by Path Only" preference. pub path_only: bool, + /// "Type Object Selection by Path Only" preference: type is picked on its type path only + /// (point type's baseline, area type's frame, type on a path's path), not anywhere in its + /// bounds. + pub type_path_only: bool, } impl Default for HitOptions { fn default() -> Self { - Self { tol: 3.0, outline: false, path_only: false } + Self { tol: 3.0, outline: false, path_only: false, type_path_only: false } } } @@ -190,6 +194,7 @@ fn hit_leaf(n: &Node, p: Point, opt: HitOptions) -> Option { } (!opt.outline && !opt.path_only && fill_contains(&bp, *rule, p)).then_some(HitKind::Fill) } + NodeKind::Text(t) if opt.type_path_only => stroke_contains(&type_path(t), 0.0, opt.tol, p).then_some(HitKind::Outline), NodeKind::Text(_) | NodeKind::Image(_) | NodeKind::SymbolInstance { .. } | NodeKind::Blend { .. } | NodeKind::Envelope { .. } => { n.geometric_bounds().filter(|b| b.inflate(opt.tol, opt.tol).contains(p)).map(|_| HitKind::Bounds) } @@ -205,6 +210,24 @@ fn hit_leaf(n: &Node, p: Point, opt: HitOptions) -> Option { } } +/// The type path of `t` in document space, what "Type Object Selection by Path Only" picks type +/// by: area type's frame, type on a path's path, and for point type its first baseline (the line +/// the selection marks under it), across the width of its layout. +fn type_path(t: &crate::TextObject) -> vectorcraft_geom::BezPath { + use crate::TextKind; + match &t.kind { + TextKind::Area { frame } => frame.transformed(t.xf).to_bezpath(), + TextKind::OnPath { path, .. } => path.transformed(t.xf).to_bezpath(), + TextKind::Point => { + let b = t.cached_bounds.unwrap_or_else(|| t.estimate_bounds()); + let mut bp = vectorcraft_geom::BezPath::new(); + bp.move_to(t.xf * Point::new(b.x0, 0.0)); + bp.line_to(t.xf * Point::new(b.x1, 0.0)); + bp + } + } +} + /// Hit anywhere in an evaluated subtree (groups recurse; leaves use [`hit_leaf`]). fn hit_any(n: &Node, p: Point, opt: HitOptions) -> Option { match &n.kind { @@ -404,6 +427,45 @@ mod tests { } } + /// Type Object Selection by Path Only: point type is picked on its baseline, area type on its + /// frame and type on a path on its path, no longer anywhere in its bounds. + #[test] + fn type_path_only_picks_type_on_its_path() { + use crate::{CharStyle, TextKind, TextObject}; + let mut d = Document::new(400.0, 400.0); + let l = d.layers[0].id; + let mut add = |t: TextObject| { + let id = d.alloc_id(); + d.insert(Some(l), 9, Node::new(id, NodeKind::Text(Box::new(t)))).unwrap(); + id + }; + // Point type at (10, 100), 20 pt: its glyphs rise above the baseline, its layout is 60 wide. + let mut point = TextObject::point(Point::new(10.0, 100.0), "Hello", CharStyle { size: 20.0, ..CharStyle::default() }); + point.cached_bounds = Some(Rect::new(0.0, -16.0, 60.0, 4.0)); + let point = add(point); + let mut area = TextObject::point(Point::new(200.0, 200.0), "Area", CharStyle::default()); + area.kind = TextKind::Area { frame: shapes::rectangle(Rect::new(0.0, 0.0, 100.0, 50.0)) }; + let area = add(area); + let mut on_path = TextObject::point(Point::new(10.0, 300.0), "Path", CharStyle::default()); + on_path.kind = TextKind::OnPath { path: shapes::rectangle(Rect::new(0.0, 0.0, 100.0, 40.0)), start: 0.0, end: None }; + let on_path = add(on_path); + let path_only = HitOptions { type_path_only: true, ..Default::default() }; + let hit = |p: Point, opt: HitOptions| hit_test(&d, p, opt).map(|h| (h.leaf, h.kind)); + // Off: anywhere in the bounds. + assert_eq!(hit(Point::new(40.0, 90.0), HitOptions::default()), Some((point, HitKind::Bounds))); + assert_eq!(hit(Point::new(250.0, 225.0), HitOptions::default()), Some((area, HitKind::Bounds))); + assert_eq!(hit(Point::new(60.0, 320.0), HitOptions::default()), Some((on_path, HitKind::Bounds))); + // On: the glyphs and the inside of the frame or path no longer pick the type. + assert_eq!(hit(Point::new(40.0, 90.0), path_only), None, "point type: among its glyphs"); + assert_eq!(hit(Point::new(250.0, 225.0), path_only), None, "area type: inside its frame"); + assert_eq!(hit(Point::new(60.0, 320.0), path_only), None, "type on a path: inside its path"); + // The baseline (within the tolerance, across the layout's width), frame and path do. + assert_eq!(hit(Point::new(40.0, 101.5), path_only), Some((point, HitKind::Outline)), "point type: its baseline"); + assert_eq!(hit(Point::new(75.0, 100.0), path_only), None, "point type: past the end of its layout"); + assert_eq!(hit(Point::new(250.0, 200.5), path_only), Some((area, HitKind::Outline)), "area type: its frame"); + assert_eq!(hit(Point::new(60.0, 339.5), path_only), Some((on_path, HitKind::Outline)), "type on a path: its path"); + } + /// `objects_at`: every object under the point, topmost first; a group counts once. #[test] fn objects_at_lists_the_stack() { diff --git a/crates/engine/src/tests_live.rs b/crates/engine/src/tests_live.rs index 204afa4d..67c040c4 100644 --- a/crates/engine/src/tests_live.rs +++ b/crates/engine/src/tests_live.rs @@ -589,8 +589,11 @@ fn mesh_hit_test_inside() { sel(&mut s, &[a]); s.execute("object.mesh.create", &json!({"rows": 2, "cols": 2})).unwrap(); let d = &s.doc().unwrap().doc; - let h = - vectorcraft_doc::hit::hit_test(d, Point::new(50.0, 30.0), vectorcraft_doc::hit::HitOptions { tol: 1.0, outline: false, path_only: false }); + let h = vectorcraft_doc::hit::hit_test( + d, + Point::new(50.0, 30.0), + vectorcraft_doc::hit::HitOptions { tol: 1.0, outline: false, path_only: false, type_path_only: false }, + ); assert_eq!(h.map(|h| h.leaf), Some(a)); } diff --git a/crates/engine/src/tests_prefs.rs b/crates/engine/src/tests_prefs.rs index 4ebe5e0e..6e0cc7c7 100644 --- a/crates/engine/src/tests_prefs.rs +++ b/crates/engine/src/tests_prefs.rs @@ -561,3 +561,35 @@ fn missing_glyph_protection_keeps_glyphs_a_new_font_lacks() { s.execute("text.setStyle", &json!({"font": to})).unwrap(); assert_eq!(fonts(&s), [to, to, to]); } + +// ---------- #394, fourth batch ---------- + +/// Type › Type Object Selection by Path Only (#394): on, a click among point type's glyphs no +/// longer selects it with the Selection tool, a click on its baseline does; off (the default), +/// anywhere in its bounds selects it. +#[test] +fn type_selection_by_path_only_picks_type_on_its_baseline() { + use vectorcraft_tools::{PointerEvent, PointerKind}; + let mut s = new_doc(); + let id = NodeId(s.execute("text.create", &json!({"x": 100, "y": 100, "text": "Hello world", "size": 20})).unwrap()["id"].as_u64().unwrap()); + s.execute("select.none", &json!({})).unwrap(); + let b = s.doc().unwrap().doc.node(id).and_then(|n| n.geometric_bounds()).unwrap(); + assert!(b.y0 < 95.0 && b.x1 > 130.0, "the glyphs rise above the baseline at y = 100: {b:?}"); + s.select_tool("selection", ViewInfo::default()).unwrap(); + let click = |s: &mut Session, x: f64, y: f64| { + for k in [PointerKind::Down, PointerKind::Up] { + s.pointer(&PointerEvent::new(k, x, y), ViewInfo::default()).unwrap(); + } + s.doc().unwrap().selection.objects.clone() + }; + // Clear of the bounding box's handles: 15 px in from the left, halfway up the glyphs. + let (x, glyphs) = (b.x0 + 15.0, (b.y0 + 100.0) / 2.0); + assert_eq!(click(&mut s, x, glyphs), vec![id], "off: a click among the glyphs selects the type"); + assert!(click(&mut s, x, 300.0).is_empty(), "empty canvas deselects"); + set_pref(&mut s, "typeSelectionByPathOnly", json!(true)); + assert!(click(&mut s, x, glyphs).is_empty(), "on: a click among the glyphs selects nothing"); + assert_eq!(click(&mut s, x, 101.0), vec![id], "on: a click on the baseline selects it"); + assert!(click(&mut s, x, 300.0).is_empty()); + set_pref(&mut s, "typeSelectionByPathOnly", json!(false)); + assert_eq!(click(&mut s, x, glyphs), vec![id], "off again: the glyphs select it"); +} diff --git a/crates/engine/src/tooling.rs b/crates/engine/src/tooling.rs index dbc1d484..b149c31a 100644 --- a/crates/engine/src/tooling.rs +++ b/crates/engine/src/tooling.rs @@ -166,6 +166,7 @@ impl Session { auto_add_delete: !self.prefs.disable_auto_add_delete, selection_tolerance: self.prefs.selection_tolerance, path_only: self.prefs.object_selection_by_path_only, + type_path_only: self.prefs.type_selection_by_path_only, double_click_isolate: self.prefs.double_click_to_isolate, select_behind: self.prefs.ctrl_click_selects_behind, highlight_anchors: self.prefs.highlight_anchors_on_hover, diff --git a/crates/tools/src/lib.rs b/crates/tools/src/lib.rs index ddadd8ee..b143e8a4 100644 --- a/crates/tools/src/lib.rs +++ b/crates/tools/src/lib.rs @@ -273,6 +273,9 @@ pub struct ToolContext<'a> { /// Selection & Anchor Display → Object Selection by Path Only: a click inside a filled path /// doesn't pick it, only one on its path does. pub path_only: bool, + /// Type → Type Object Selection by Path Only: type is picked on its type path only (point + /// type's baseline, area type's frame, type on a path's path), not anywhere in its bounds. + pub type_path_only: bool, /// General → Double Click To Isolate: a double-click on a group with the Selection tool /// isolates it. pub double_click_isolate: bool, @@ -352,7 +355,12 @@ impl ToolContext<'_> { self.tol(self.selection_tolerance) } pub fn hit_options(&self) -> vectorcraft_doc::hit::HitOptions { - vectorcraft_doc::hit::HitOptions { tol: self.pick_tol(), outline: self.outline, path_only: self.path_only } + vectorcraft_doc::hit::HitOptions { + tol: self.pick_tol(), + outline: self.outline, + path_only: self.path_only, + type_path_only: self.type_path_only, + } } } @@ -650,6 +658,7 @@ pub(crate) mod testutil { auto_add_delete: true, selection_tolerance: 3.0, path_only: false, + type_path_only: false, double_click_isolate: true, select_behind: true, highlight_anchors: true, diff --git a/crates/tools/src/text.rs b/crates/tools/src/text.rs index f6e8d62a..ac986f48 100644 --- a/crates/tools/src/text.rs +++ b/crates/tools/src/text.rs @@ -230,7 +230,7 @@ impl TypeTool { /// Path under `p` for the Area Type / Type on a Path tools. fn path_at(cx: &ToolContext, p: Point, closed: bool) -> Option { - let h = hit_test(cx.doc, p, vectorcraft_doc::hit::HitOptions { tol: cx.tol(4.0), outline: true, path_only: false })?; + let h = hit_test(cx.doc, p, vectorcraft_doc::hit::HitOptions { tol: cx.tol(4.0), outline: true, path_only: false, type_path_only: false })?; match &cx.doc.node(h.leaf)?.kind { NodeKind::Path { path, .. } if !closed || path.is_closed() => Some(h.leaf), _ => None, diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index af203589..7a045ef2 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -1043,13 +1043,15 @@ fn walk_drawn<'a>(n: &'a Node, f: &mut impl FnMut(&'a Node)) { } /// The topmost editable object under document point `p` at `zoom`, as the selection tools pick it -/// (Selection & Anchor Display › Tolerance and Object Selection by Path Only). +/// (Selection & Anchor Display › Tolerance and Object Selection by Path Only, Type › Type Object +/// Selection by Path Only). fn hit_at(app: &VectorcraftApp, p: Point, zoom: f64) -> Option { let prefs = &app.session.prefs; let opt = vectorcraft_doc::hit::HitOptions { tol: prefs.selection_tolerance / zoom, outline: app.ui.view.outline, path_only: prefs.object_selection_by_path_only, + type_path_only: prefs.type_selection_by_path_only, }; vectorcraft_doc::hit::hit_test(&app.session.active()?.doc, p, opt) } diff --git a/docs/mcp.md b/docs/mcp.md index 27d0defd..cdeb1ea2 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -1211,6 +1211,10 @@ Not read yet: Construction Guides and their Angles, and Spacing Guides (the tool selection (←/→), leading (↑/↓) and baseline shift (Shift+↑/↓), five steps with Cmd/Ctrl too. - `missingGlyphProtection` (on by default): `text.setStyle` and `text.setRangeStyle` changing the font leave the characters the new font has no glyph for in the font that had one. +- `typeSelectionByPathOnly` (off by default): on, the Selection tools (`pointer_gesture` with `selection`, and the + mouse) pick type on its type path only — point type's baseline, area type's frame, type on a path's path — within + `selectionTolerance`; a click among the glyphs or inside the frame selects nothing. Off, anywhere in the type's + bounds selects it. ```json {"name":"run_command","arguments":{"command":"prefs.set","params":{"values":{"typeSizeIncrement":4,"trackingIncrement":50}}}}