Part of #394: Smart Guides display options and Snapping Tolerance

Color, Alignment Guides, Anchor/Path Labels, Measurement Labels,
Transform Tools, Object Highlighting and Snapping Tolerance are read:

- tools: `guides::GuideStyle` on `Targets` (`styled(cx)`) colours the
  lines and labels and leaves them out when their option is off, the
  snap itself unchanged; `ToolContext` gains the six fields and
  `snap_tol()`, replacing the 5 px literal for every smart guide snap
  (drawn points, moves, bounding-box handles, ruler guides, artboards,
  reference points). The tools gate their `Overlay::Measure` readouts on
  Measurement Labels (drawing, moving, ruler guides, artboards) or
  Transform Tools (scaling, rotating, Free Transform, transform tools).
- engine: `with_tool_cx` fills them from `Prefs` (the colour parsed, magenta
  as fallback); the colour's default is `#ff3dfc`, what the tools drew.
- ui-egui: the hover outline needs Object Highlighting and Smart Guides.
- Tests in tools (guides, select), engine (tests_prefs) and the canvas;
  docs/mcp.md and ROADMAP.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
wstockandClaude Fable 5.1 authored and bflatastic committed 2026-10-08 15:46:15 +07:00
1 parent d92ade9117
commit 14d7b3ddce
16 files changed
+353 -47

No files matched your search

+3 -3
View File
@@ -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), and the toolbar's double arrow and Window › Toolbars › Double Column switching between one and two columns in any window height (#455); 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); 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), 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. They reach agents too (`prefs.set`, then `pointer_gesture` or `press_key`; `ui.wheel` on the control channel). About 45 stored options still do nothing yet (the rest of #394: Auto Size New Area Type, Show Font Names in English, Highlight Alternate Glyphs, the smart guide display options, 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), 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…).
- 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 45 Preferences options stored but not yet acted on (#394: smart guide display options, Auto Size New Area Type, Show Font Names in English, Highlight Alternate Glyphs, Show Pixel Grid, performance, devices…) | 16–26 |
| 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 |
| 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 |
+1 -1
View File
@@ -624,7 +624,7 @@ impl Default for Prefs {
grid_subdivisions: 8,
grids_in_back: true,
show_pixel_grid: true,
smart_guide_color: s("#ff4af0"),
smart_guide_color: s("#ff3dfc"),
alignment_guides: true,
object_highlighting: true,
transform_tools_guides: true,
+76
View File
@@ -279,6 +279,82 @@ fn snap_to_point_lands_a_dragged_selection_on_anchors() {
assert_eq!(drag(&mut s, ViewInfo { snap_to_point: false, ..v }), (276.5, 76.0), "View › Snap to Point off");
}
/// Preferences › Smart Guides (#394): Color, Alignment Guides, Anchor/Path Labels, Measurement
/// Labels, Transform Tools and Snapping Tolerance change what the Selection tool shows while a
/// square is dragged into line with another, and how far the pull reaches; a hidden guide still
/// snaps.
#[test]
fn smart_guide_display_preferences_filter_the_overlays() {
use vectorcraft_tools::Overlay;
use vectorcraft_tools::PointerKind::{Down, Drag, Up};
let mut s = new_doc();
square(&mut s, 100.0, 100.0);
let b = square(&mut s, 200.0, 100.0);
let v = ViewInfo::default();
s.select_tool("selection", v).unwrap();
let count = |s: &mut Session, f: &dyn Fn(&Overlay) -> bool| s.overlays(v).iter().filter(|o| f(o)).count();
let line = |o: &Overlay| matches!(o, Overlay::Line { .. });
let label = |o: &Overlay| matches!(o, Overlay::Label { .. });
let measure = |o: &Overlay| matches!(o, Overlay::Measure { .. });
// `b` grabbed by its centre and dragged so its left edge comes `off` px from the first
// square's right edge (x = 150): the x of its left edge mid-drag, before the pointer is let go.
let drag_to = |s: &mut Session, off: f64| {
gesture(s, v, &[(Down, 225.0, 125.0), (Drag, 200.0, 125.0), (Drag, 175.0 + off, 125.0)]);
top_left(s, b).0
};
let release = |s: &mut Session| {
gesture(s, v, &[(Up, 175.0, 125.0)]);
s.execute("edit.undo", &json!({})).unwrap();
};
assert_eq!(drag_to(&mut s, 3.0), 150.0, "3 px off: into line");
assert!(count(&mut s, &line) > 0 && count(&mut s, &measure) == 1);
let magenta = s.overlays(v).iter().find_map(|o| if let Overlay::Line { color, .. } = o { Some(*color) } else { None });
assert_eq!(magenta, Some(vectorcraft_tools::guides::MAGENTA), "the default colour");
release(&mut s);
set_pref(&mut s, "smartGuideColor", json!("#00ff00"));
drag_to(&mut s, 3.0);
assert!(s.overlays(v).iter().all(|o| !matches!(o, Overlay::Line { color, .. } | Overlay::Label { color, .. } if *color != [0, 255, 0])));
release(&mut s);
set_pref(&mut s, "alignmentGuides", json!(false));
assert_eq!(drag_to(&mut s, 3.0), 150.0, "still into line");
assert_eq!(count(&mut s, &line), 0, "no line");
release(&mut s);
set_pref(&mut s, "measurementLabels", json!(false));
drag_to(&mut s, 3.0);
assert_eq!(count(&mut s, &measure), 0);
release(&mut s);
// Snapping Tolerance: 6 px is beyond the default 4, within 8.
assert_eq!(drag_to(&mut s, 6.0), 156.0);
release(&mut s);
set_pref(&mut s, "snappingTolerance", json!(8));
assert_eq!(drag_to(&mut s, 6.0), 150.0);
release(&mut s);
// Transform Tools: the size readout while a bounding-box handle is dragged.
let scale = |s: &mut Session| {
gesture(s, v, &[(Down, 225.0, 150.0), (Drag, 225.0, 170.0)]);
let n = count(s, &measure);
gesture(s, v, &[(Up, 225.0, 170.0)]);
s.execute("edit.undo", &json!({})).unwrap();
n
};
assert_eq!(scale(&mut s), 1);
set_pref(&mut s, "transformToolsGuides", json!(false));
assert_eq!(scale(&mut s), 0);
// Anchor/Path Labels: a drawn corner pulled onto an anchor says "anchor" (and still lands there).
s.select_tool("rectangle", v).unwrap();
let draw = |s: &mut Session| {
gesture(s, v, &[(Down, 300.0, 300.0), (Drag, 153.0, 151.0)]);
let n = count(s, &label);
gesture(s, v, &[(Up, 153.0, 151.0)]);
let at = top_left(s, s.doc().unwrap().selection.objects[0]);
s.execute("edit.undo", &json!({})).unwrap();
(n, at)
};
assert_eq!(draw(&mut s), (1, (150.0, 150.0)));
set_pref(&mut s, "anchorPathLabels", json!(false));
assert_eq!(draw(&mut s), (0, (150.0, 150.0)));
}
/// Enable Rubber Band for Pen Tool / Curvature Tool (#394): off, no segment follows the pointer.
#[test]
fn rubber_band_preferences_hide_the_preview_to_the_pointer() {
+14
View File
@@ -44,6 +44,14 @@ impl Default for ViewInfo {
}
}
/// The RGB of a colour preference (`#rrggbb`), or `fallback` when it doesn't parse.
fn rgb(hex: &str, fallback: [u8; 3]) -> [u8; 3] {
vectorcraft_color::Color::from_hex(hex).map_or(fallback, |c| {
let [r, g, b, _] = c.to_rgba8(1.0);
[r, g, b]
})
}
/// Requests from tools that only the frontend can fulfil.
#[derive(Clone, Debug, PartialEq)]
pub enum UiRequest {
@@ -168,6 +176,12 @@ impl Session {
pen_rubber_band: self.prefs.pen_rubber_band,
curvature_rubber_band: self.prefs.curvature_rubber_band,
placeholder_text: self.prefs.placeholder_text,
smart_guide_color: rgb(&self.prefs.smart_guide_color, vectorcraft_tools::guides::MAGENTA),
alignment_guides: self.prefs.alignment_guides,
anchor_path_labels: self.prefs.anchor_path_labels,
measurement_labels: self.prefs.measurement_labels,
transform_tools_guides: self.prefs.transform_tools_guides,
snapping_tolerance: self.prefs.snapping_tolerance,
screen: view.screen,
plane_widget: self.prefs.perspective_widget.show.then_some(self.prefs.perspective_widget.position),
};
+1 -1
View File
@@ -186,7 +186,7 @@ impl Tool for FamilyTool {
}
fn overlays(&self, cx: &ToolContext) -> Vec<Overlay> {
match self.start {
Some(s) if self.began => {
Some(s) if self.began && cx.measurement_labels => {
let d = self.last - s;
vec![Overlay::Measure { p: self.last, text: cx.size_label(d.x.abs(), d.y.abs()) }]
}
+104 -18
View File
@@ -1,6 +1,7 @@
//! Smart Guides: snapping to anchors, path segments (for dragged anchors), object bounds
//! (edges/centres), artboards, with the magenta construction lines and labels Illustrator users
//! expect.
//! expect. Preferences › Smart Guides sets their colour, which of them show ([`GuideStyle`]) and
//! how near a target pulls (`ToolContext::snapping_tolerance`).
use vectorcraft_doc::hit::{HitOptions, hit_test};
use vectorcraft_doc::{Document, NodeId, NodeKind, OrientedBox, Selection};
@@ -12,6 +13,30 @@ use crate::{Overlay, ToolContext};
pub const MAGENTA: [u8; 3] = [0xff, 0x3d, 0xfc];
/// How Smart Guides look (Preferences › Smart Guides › Display Options): their colour, and whether
/// the alignment lines and the anchor/path labels show. Snapping is the same either way.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct GuideStyle {
pub color: [u8; 3],
/// Alignment Guides: the lines along the edges and centres the art lines up with.
pub lines: bool,
/// Anchor/Path Labels: "anchor", "center", "path", "align"…
pub labels: bool,
}
impl Default for GuideStyle {
fn default() -> Self {
Self { color: MAGENTA, lines: true, labels: true }
}
}
impl GuideStyle {
/// The style the preferences in `cx` ask for.
pub fn of(cx: &ToolContext) -> Self {
Self { color: cx.smart_guide_color, lines: cx.alignment_guides, labels: cx.anchor_path_labels }
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
enum Kind {
Anchor,
@@ -46,6 +71,8 @@ pub struct Targets {
segments: Vec<(Rect, PathSeg)>,
/// Ruler guides, which pull a point into line where they run (Snap to Point).
rulers: Vec<Ruler>,
/// How the guides these targets produce look.
style: GuideStyle,
}
/// A ruler guide as a snap target.
@@ -80,6 +107,23 @@ impl Targets {
Self::collect_inner(doc, exclude, None, visible, false)
}
/// The same targets drawing their guides as the Smart Guides preferences in `cx` ask
/// ([`GuideStyle::of`]).
pub fn styled(mut self, cx: &ToolContext) -> Self {
self.style = GuideStyle::of(cx);
self
}
/// An alignment line from `a` to `b`, unless Alignment Guides are off.
fn line(&self, a: Point, b: Point) -> Option<Overlay> {
self.style.lines.then_some(Overlay::Line { a, b, color: self.style.color, dashed: false })
}
/// A label `text` at `p`, unless Anchor/Path Labels are off.
fn label(&self, p: Point, text: &str) -> Option<Overlay> {
self.style.labels.then(|| Overlay::Label { p, text: text.into(), color: self.style.color })
}
/// Targets for dragging artboard `index`: everything except that artboard and `exclude` (the
/// art that moves along with it).
pub fn for_artboard(doc: &Document, index: usize, exclude: &[NodeId]) -> Self {
@@ -199,7 +243,7 @@ impl Targets {
/// What a dragged selection's grabbed point snaps to with View → Snap to Point and Smart
/// Guides off: the anchors of the art but `exclude` and the ruler guides.
pub fn snap_to_point(cx: &ToolContext, exclude: &[NodeId]) -> Option<Self> {
(cx.snap_to_point && !cx.smart_guides).then(|| Self::collect(cx.doc, exclude, None).for_snap_to_point(cx))
(cx.snap_to_point && !cx.smart_guides).then(|| Self::collect(cx.doc, exclude, None).for_snap_to_point(cx).styled(cx))
}
/// Anchors and centres of the visible leaves under `roots` (the roots included) whose bounds
@@ -228,10 +272,10 @@ impl Targets {
/// into line with targets. Returns the snapped point and guide overlays.
pub fn snap_point(&self, p: Point, tol: f64) -> (Point, Vec<Overlay>) {
if let Some((q, k)) = self.points.iter().filter(|(q, _)| q.distance(p) <= tol).min_by(|a, b| a.0.distance(p).total_cmp(&b.0.distance(p))) {
return (*q, vec![Overlay::Label { p: *q, text: k.label().into(), color: MAGENTA }]);
return (*q, self.label(*q, k.label()).into_iter().collect());
}
if let Some(q) = self.on_segment(p, tol) {
return (q, vec![Overlay::Label { p: q, text: Kind::Edge.label().into(), color: MAGENTA }]);
return (q, self.label(q, Kind::Edge.label()).into_iter().collect());
}
let mut out = p;
let mut ov = vec![];
@@ -241,16 +285,19 @@ impl Targets {
let rulers = self.rulers.iter().filter(|r| r.vertical == vertical).filter_map(|r| r.line_at(p, tol));
lines.iter().copied().chain(rulers).filter(|(t, ..)| (t - v).abs() <= tol).min_by(|a, b| (a.0 - v).abs().total_cmp(&(b.0 - v).abs()))
};
let mut aligned = false;
if let Some((x, from, _)) = nearest(&self.xs, true, p.x) {
out.x = x;
ov.push(Overlay::Line { a: from, b: Point::new(x, p.y), color: MAGENTA, dashed: false });
aligned = true;
ov.extend(self.line(from, Point::new(x, p.y)));
}
if let Some((y, from, _)) = nearest(&self.ys, false, p.y) {
out.y = y;
ov.push(Overlay::Line { a: from, b: Point::new(p.x, y), color: MAGENTA, dashed: false });
aligned = true;
ov.extend(self.line(from, Point::new(p.x, y)));
}
if !ov.is_empty() {
ov.push(Overlay::Label { p: out, text: "align".into(), color: MAGENTA });
if aligned {
ov.extend(self.label(out, "align"));
}
(out, ov)
}
@@ -278,7 +325,7 @@ impl Targets {
.filter(|(t, ..)| (t - v).abs() <= tol)
.min_by(|a, b| (a.0 - v).abs().total_cmp(&(b.0 - v).abs()));
match best {
Some((t, from, k)) => (t, vec![Overlay::Label { p: from, text: k.label().into(), color: MAGENTA }]),
Some((t, from, k)) => (t, self.label(from, k.label()).into_iter().collect()),
None => (v, vec![]),
}
}
@@ -293,8 +340,8 @@ impl Targets {
let rects: Vec<Rect> = std::iter::once(Vec2::ZERO).chain(offsets.iter().copied()).map(|o| Rect::from_points(p + o, p + o)).collect();
let (adj, mut ov) = self.snap_rects(&rects, tol);
let out = p + adj;
if !ov.is_empty() {
ov.push(Overlay::Label { p: out, text: "align".into(), color: MAGENTA });
if adj != Vec2::ZERO {
ov.extend(self.label(out, "align"));
}
(out, ov)
}
@@ -323,13 +370,13 @@ impl Targets {
d.x = dx;
let x = x + dx;
let (y0, y1) = (from.y.min(r.y0), from.y.max(r.y1));
ov.push(Overlay::Line { a: Point::new(x, y0), b: Point::new(x, y1), color: MAGENTA, dashed: false });
ov.extend(self.line(Point::new(x, y0), Point::new(x, y1)));
}
if let Some((dy, from, y, r)) = best_y {
d.y = dy;
let y = y + dy;
let (x0, x1) = (from.x.min(r.x0), from.x.max(r.x1));
ov.push(Overlay::Line { a: Point::new(x0, y), b: Point::new(x1, y), color: MAGENTA, dashed: false });
ov.extend(self.line(Point::new(x0, y), Point::new(x1, y)));
}
(d, ov)
}
@@ -419,10 +466,10 @@ impl Targets {
let nr = (to_doc * out).transform_rect_bbox(r);
let mut ov = vec![];
if let Some((x, from)) = hit_x {
ov.push(Overlay::Line { a: Point::new(x, from.y.min(nr.y0)), b: Point::new(x, from.y.max(nr.y1)), color: MAGENTA, dashed: false });
ov.extend(self.line(Point::new(x, from.y.min(nr.y0)), Point::new(x, from.y.max(nr.y1))));
}
if let Some((y, from)) = hit_y {
ov.push(Overlay::Line { a: Point::new(from.x.min(nr.x0), y), b: Point::new(from.x.max(nr.x1), y), color: MAGENTA, dashed: false });
ov.extend(self.line(Point::new(from.x.min(nr.x0), y), Point::new(from.x.max(nr.x1), y)));
}
(out, ov)
}
@@ -452,7 +499,7 @@ impl PointSnap {
let targets = if cx.snap_to_pixel || cx.snap_to_grid {
None
} else if cx.smart_guides {
Some((targets(), 5.0))
Some((targets().styled(cx), cx.snapping_tolerance))
} else if cx.snap_to_point {
Some((targets().for_snap_to_point(cx), cx.snap_tolerance))
} else {
@@ -503,11 +550,11 @@ pub fn snap_pick(cx: &ToolContext, p: Point) -> (Point, Vec<Overlay>) {
if !(cx.snap_to_point || cx.smart_guides) {
return (p, vec![]);
}
let tol = cx.tol(if cx.smart_guides { 5.0 } else { cx.snap_tolerance });
let tol = if cx.smart_guides { cx.snap_tol() } else { cx.tol(cx.snap_tolerance) };
let mut roots = cx.selection.objects.clone();
roots.extend(hit_test(cx.doc, p, HitOptions { tol, ..cx.hit_options() }).map(|h| h.leaf));
let near = Rect::new(p.x - tol, p.y - tol, p.x + tol, p.y + tol);
Targets::points_near(cx.doc, &roots, near).snap_point(p, tol)
Targets::points_near(cx.doc, &roots, near).styled(cx).snap_point(p, tol)
}
#[cfg(test)]
@@ -580,6 +627,45 @@ mod tests {
assert!(ov.is_empty());
}
/// Preferences › Smart Guides › Display Options (#394): the colour, and Alignment Guides and
/// Anchor/Path Labels off, change what shows and never where a point lands.
#[test]
fn display_options_colour_and_hide_the_guides_but_keep_the_snap() {
let (d, _) = doc_with_rect();
let s = Selection::default();
let p = paint();
let green = [0x00, 0xff, 0x00];
let c = ToolContext { smart_guide_color: green, ..cx(&d, &s, &p) };
let t = Targets::collect(&d, &[], None).styled(&c);
let (q, ov) = t.snap_point(Point::new(102.0, 99.0), 4.0);
assert_eq!(q, Point::new(100.0, 100.0));
assert!(matches!(&ov[0], Overlay::Label { text, color, .. } if text == "anchor" && *color == green), "{ov:?}");
let (q, ov) = t.snap_point(Point::new(301.0, 199.0), 4.0);
assert_eq!(q.y, 200.0);
assert!(ov.iter().all(|o| matches!(o, Overlay::Line { color, .. } | Overlay::Label { color, .. } if *color == green)), "{ov:?}");
assert!(
ov.iter().any(|o| matches!(o, Overlay::Line { .. })) && ov.iter().any(|o| matches!(o, Overlay::Label { text, .. } if text == "align"))
);
// Alignment Guides off: the point still lines up, no line and no "align" label.
let c = ToolContext { alignment_guides: false, ..cx(&d, &s, &p) };
let t = Targets::collect(&d, &[], None).styled(&c);
let (q, ov) = t.snap_point(Point::new(301.0, 199.0), 4.0);
assert_eq!(q.y, 200.0);
assert!(!ov.iter().any(|o| matches!(o, Overlay::Line { .. })), "{ov:?}");
assert!(ov.iter().any(|o| matches!(o, Overlay::Label { text, .. } if text == "align")), "{ov:?}");
let (dv, ov) = t.snap_rect(Rect::new(2.0, 50.0, 52.0, 90.0), 4.0);
assert_eq!((dv.x, ov.len()), (-2.0, 0));
// Anchor/Path Labels off: on the anchor, without saying so.
let c = ToolContext { anchor_path_labels: false, ..cx(&d, &s, &p) };
let t = Targets::collect(&d, &[], None).styled(&c);
let (q, ov) = t.snap_point(Point::new(102.0, 99.0), 4.0);
assert_eq!((q, ov.len()), (Point::new(100.0, 100.0), 0));
let (q, ov) = t.snap_point(Point::new(301.0, 199.0), 4.0);
assert_eq!(q.y, 200.0);
assert!(ov.iter().all(|o| matches!(o, Overlay::Line { .. })) && !ov.is_empty(), "{ov:?}");
assert_eq!(t.snap_guide(false, 199.0, 4.0), (200.0, vec![]));
}
/// Dragging an anchor of a path: its other anchors and the segments not touching it stay
/// targets, but neither it nor the path's bounds (which move) are.
#[test]
+24
View File
@@ -301,6 +301,20 @@ pub struct ToolContext<'a> {
/// Type → Fill New Type Objects With Placeholder Text: type the Type tools place starts with
/// placeholder text, selected.
pub placeholder_text: bool,
/// Smart Guides → Color: the smart guides' lines and labels (RGB).
pub smart_guide_color: [u8; 3],
/// Smart Guides → Alignment Guides: the lines along the edges and centres the art lines up
/// with show. Off, the art still snaps into line.
pub alignment_guides: bool,
/// Smart Guides → Anchor/Path Labels: the "anchor", "center", "path"… labels show.
pub anchor_path_labels: bool,
/// Smart Guides → Measurement Labels: the size and offset readouts while drawing and moving.
pub measurement_labels: bool,
/// Smart Guides → Transform Tools: the readouts while scaling, rotating and shearing.
pub transform_tools_guides: bool,
/// Smart Guides → Snapping Tolerance (screen pixels): how near a smart guide target pulls the
/// pointer, a dragged edge or a drawn point.
pub snapping_tolerance: f64,
/// The document window (none headless): screen-fixed widgets sit in it.
pub screen: Option<ScreenFrame>,
/// Where the Plane Switching Widget sits (Perspective Grid Options); None while it's hidden.
@@ -312,6 +326,10 @@ impl ToolContext<'_> {
pub fn tol(&self, px: f64) -> f64 {
px / self.zoom.max(1e-9)
}
/// How near (document units) a smart guide target pulls: Smart Guides → Snapping Tolerance.
pub fn snap_tol(&self) -> f64 {
self.tol(self.snapping_tolerance)
}
/// A length as measurement labels show it, in the General unit (`12.50 mm`).
pub fn len(&self, v: f64) -> String {
self.unit.readout(v)
@@ -637,6 +655,12 @@ pub(crate) mod testutil {
pen_rubber_band: true,
curvature_rubber_band: true,
placeholder_text: false,
smart_guide_color: guides::MAGENTA,
alignment_guides: true,
anchor_path_labels: true,
measurement_labels: true,
transform_tools_guides: true,
snapping_tolerance: 4.0,
screen: None,
plane_widget: Some(Default::default()),
}
+5 -3
View File
@@ -67,7 +67,7 @@ impl GuideSnap {
let targets = if cx.snap_to_pixel || cx.snap_to_grid {
None
} else if cx.smart_guides {
Some((targets(), 5.0))
Some((targets().styled(cx), cx.snapping_tolerance))
} else if cx.snap_to_point {
Some((targets().anchors_only(), cx.snap_tolerance))
} else {
@@ -98,8 +98,10 @@ impl GuideSnap {
/// The target the guide at `at` snapped to, and its position by the `pointer`.
fn overlays(&self, cx: &ToolContext, vertical: bool, at: f64, pointer: Point) -> Vec<Overlay> {
let mut o = self.snapped.clone();
let axis = if vertical { "X" } else { "Y" };
o.push(Overlay::Measure { p: pointer, text: format!("{axis}: {}", cx.len(at)) });
if cx.measurement_labels {
let axis = if vertical { "X" } else { "Y" };
o.push(Overlay::Measure { p: pointer, text: format!("{axis}: {}", cx.len(at)) });
}
o
}
}
+55 -7
View File
@@ -80,7 +80,7 @@ impl MoveSnap {
pub(crate) fn new(cx: &ToolContext) -> Self {
Self {
bounds: selection_bounds(cx),
targets: cx.smart_guides.then(|| Targets::collect(cx.doc, &cx.selection.objects, None)),
targets: cx.smart_guides.then(|| Targets::collect(cx.doc, &cx.selection.objects, None).styled(cx)),
points: Targets::snap_to_point(cx, &cx.selection.objects),
}
}
@@ -94,7 +94,7 @@ impl MoveSnap {
guides = ov;
}
if let (Some(t), Some(b)) = (&self.targets, self.bounds) {
let (adj, ov) = t.snap_rect(b + d, cx.tol(5.0));
let (adj, ov) = t.snap_rect(b + d, cx.snap_tol());
d += adj;
guides = ov;
}
@@ -205,7 +205,7 @@ impl Tool for SelectionTool {
// Only area type: the step resizes type areas; anything else scales.
let label = if areas && cx.selection.objects.iter().all(is_area) { "Resize Type Area" } else { "Scale" };
// Smart Guides align what the handle moves with the other objects.
self.targets = cx.smart_guides.then(|| Targets::collect(cx.doc, &cx.selection.objects, None));
self.targets = cx.smart_guides.then(|| Targets::collect(cx.doc, &cx.selection.objects, None).styled(cx));
self.state = State::Scaling { handle, bx, areas };
return vec![Action::Begin(label.into())];
}
@@ -268,7 +268,7 @@ impl Tool for SelectionTool {
(d, self.guides) = snap.snap(cx, start, d);
}
self.state = State::Moving { start, began: true, deselect: None };
self.measure = Some((p, cx.offset_label(d.x, d.y)));
self.measure = cx.measurement_labels.then(|| (p, cx.offset_label(d.x, d.y)));
out.push(Action::Preview("object.transform".into(), json!({ "matrix": matrix_json(Affine::translate(d)), "copy": m.alt })));
out
}
@@ -277,10 +277,10 @@ impl Tool for SelectionTool {
let mut a = scale_for_drag(bx.rect, handle, bx.to_local(p), m.shift, m.alt);
self.guides.clear();
if let Some(t) = &self.targets {
(a, self.guides) = t.snap_scale(&bx, handle, a, m.shift, m.alt, cx.tol(5.0));
(a, self.guides) = t.snap_scale(&bx, handle, a, m.shift, m.alt, cx.snap_tol());
}
let nr = a.transform_rect_bbox(bx.rect);
self.measure = Some((p, cx.size_label(nr.width(), nr.height())));
self.measure = cx.transform_tools_guides.then(|| (p, cx.size_label(nr.width(), nr.height())));
let mut params = json!({ "matrix": matrix_json(bx.conjugate(a)), "copy": false });
if areas {
params["typeAreas"] = json!(true);
@@ -289,7 +289,7 @@ impl Tool for SelectionTool {
}
(PointerKind::Drag, State::Rotating { center, start, .. }) => {
let (a, deg) = rotate_for_drag(center, start, p, m.shift);
self.measure = Some((p, format!("{:.1}°", -deg)));
self.measure = cx.transform_tools_guides.then(|| (p, format!("{:.1}°", -deg)));
vec![Action::Preview("object.transform".into(), json!({ "matrix": matrix_json(a), "copy": false }))]
}
(PointerKind::Drag, State::Marquee { start, add, .. }) => {
@@ -592,6 +592,54 @@ mod tests {
assert!((height(&a) - 97.0).abs() < 1e-9, "{a:?}");
}
/// Smart Guides › Measurement Labels, Transform Tools and Snapping Tolerance (#394): the
/// readouts while moving and while scaling each follow their option; the tolerance sets how
/// far a dragged handle is pulled.
#[test]
fn smart_guide_display_preferences_drive_the_readouts_and_the_pull() {
use vectorcraft_doc::{Appearance, Node};
let (mut d, _) = doc_with_rect();
let l = d.layers[0].id;
let id = d.alloc_id();
d.insert(Some(l), 1, Node::path(id, vectorcraft_geom::shapes::rectangle(Rect::new(250.0, 100.0, 300.0, 150.0)), Appearance::default_art()))
.unwrap();
let mut s = Selection::default();
s.add(id);
let p = paint();
let measures = |t: &SelectionTool, c: &ToolContext| t.overlays(c).iter().filter(|o| matches!(o, Overlay::Measure { .. })).count();
let height = |a: &[Action]| {
let Action::Preview(_, v) = &a[0] else { panic!("{a:?}") };
v["matrix"][3].as_f64().unwrap() * 50.0
};
// Moving: the offset readout is a measurement label.
for on in [true, false] {
let c = ToolContext { measurement_labels: on, ..cx(&d, &s, &p) };
let mut t = SelectionTool::default();
t.pointer(&c, &ev(PointerKind::Down, 275.0, 125.0));
t.pointer(&c, &ev(PointerKind::Drag, 285.0, 135.0));
assert_eq!(measures(&t, &c), usize::from(on), "measurement labels {on}");
t.pointer(&c, &ev(PointerKind::Up, 285.0, 135.0));
}
// Scaling by a handle: the size readout belongs to Transform Tools.
for on in [true, false] {
let c = ToolContext { transform_tools_guides: on, ..cx(&d, &s, &p) };
let mut t = SelectionTool::default();
t.pointer(&c, &ev(PointerKind::Down, 275.0, 150.0));
t.pointer(&c, &ev(PointerKind::Drag, 275.0, 180.0));
assert_eq!(measures(&t, &c), usize::from(on), "transform tools {on}");
t.pointer(&c, &ev(PointerKind::Up, 275.0, 180.0));
}
// Snapping Tolerance: 6 px from the neighbour's bottom edge is beyond 4, within 8.
for (tol, want) in [(4.0, 94.0), (8.0, 100.0), (0.0, 94.0)] {
let c = ToolContext { snapping_tolerance: tol, ..cx(&d, &s, &p) };
let mut t = SelectionTool::default();
t.pointer(&c, &ev(PointerKind::Down, 275.0, 150.0));
let a = t.pointer(&c, &ev(PointerKind::Drag, 275.0, 194.0));
assert!((height(&a) - want).abs() < 1e-9, "tolerance {tol}: {a:?}");
t.pointer(&c, &ev(PointerKind::Up, 275.0, 194.0));
}
}
#[test]
fn handle_drag_of_a_turned_box_snaps_on_the_page() {
use vectorcraft_doc::{Appearance, Node};
+3 -1
View File
@@ -206,7 +206,9 @@ impl Tool for ShapeTool {
Some(s) if self.began => {
let d = self.last - s;
let mut o = self.guides.clone();
o.push(Overlay::Measure { p: self.last, text: cx.size_label(d.x.abs(), d.y.abs()) });
if cx.measurement_labels {
o.push(Overlay::Measure { p: self.last, text: cx.size_label(d.x.abs(), d.y.abs()) });
}
o
}
_ => vec![],
+3 -1
View File
@@ -116,7 +116,9 @@ impl Tool for SliceTool {
Some(s) if self.began => {
let d = self.last - s;
let mut o = self.guides.clone();
o.push(Overlay::Measure { p: self.last, text: cx.size_label(d.x.abs(), d.y.abs()) });
if cx.measurement_labels {
o.push(Overlay::Measure { p: self.last, text: cx.size_label(d.x.abs(), d.y.abs()) });
}
o
}
_ => vec![],
+9 -7
View File
@@ -83,10 +83,10 @@ impl ArtboardTool {
Some(a) if art_moves => cx.doc.art_on_artboard(a.rect, cx.move_locked_with_artboard),
_ => vec![],
};
Targets::for_artboard(cx.doc, index, &art)
Targets::for_artboard(cx.doc, index, &art).styled(cx)
});
// Nothing to skip: no artboard has this index.
self.copy_targets = cx.smart_guides.then(|| Targets::for_artboard(cx.doc, usize::MAX, &[]));
self.copy_targets = cx.smart_guides.then(|| Targets::for_artboard(cx.doc, usize::MAX, &[]).styled(cx));
}
/// Snap a dragged handle: to whole pixels, the grid, or Smart Guides (in that order, as when
@@ -101,7 +101,7 @@ impl ArtboardTool {
}
let Some(t) = &self.targets else { return p };
let offsets: &[Vec2] = if bleed == Vec2::ZERO { &[] } else { &[bleed] };
let (q, ov) = t.snap_point_with(p, offsets, cx.tol(5.0));
let (q, ov) = t.snap_point_with(p, offsets, cx.snap_tol());
self.guides = ov;
q
}
@@ -120,9 +120,9 @@ impl ArtboardTool {
let Some(t) = (if copy { &self.copy_targets } else { &self.targets }) else { return d };
let moved = rect + d;
let (adj, ov) = if cx.doc.setup.has_bleed() {
t.snap_rects(&[moved, cx.doc.setup.bleed_rect(moved)], cx.tol(5.0))
t.snap_rects(&[moved, cx.doc.setup.bleed_rect(moved)], cx.snap_tol())
} else {
t.snap_rect(moved, cx.tol(5.0))
t.snap_rect(moved, cx.snap_tol())
};
self.guides = ov;
d + adj
@@ -291,7 +291,9 @@ impl Tool for ArtboardTool {
if let Some(Drag::Create { start, cur }) = self.drag {
let r = Rect::from_points(start, cur);
o.push(Overlay::Marquee(r));
o.push(Overlay::Measure { p: cur + Vec2::new(cx.tol(12.0), cx.tol(12.0)), text: cx.size_label(r.width(), r.height()) });
if cx.measurement_labels {
o.push(Overlay::Measure { p: cur + Vec2::new(cx.tol(12.0), cx.tol(12.0)), text: cx.size_label(r.width(), r.height()) });
}
return o;
}
let Some(ab) = cx.doc.artboards.get(self.active) else { return o };
@@ -301,7 +303,7 @@ impl Tool for ArtboardTool {
o.push(Overlay::Anchor { p: h.pos(r), color: BLUE, filled: false, size: 7.0 });
}
o.push(Overlay::Label { p: Point::new(r.x0, r.y0 - cx.tol(14.0)), text: format!("{:02} - {}", self.active + 1, ab.name), color: BLUE });
if let Some(Drag::Resize { began: true, .. }) = self.drag {
if let (Some(Drag::Resize { began: true, .. }), true) = (self.drag, cx.measurement_labels) {
o.push(Overlay::Measure {
p: Point::new(r.x1, r.y1) + Vec2::new(cx.tol(12.0), cx.tol(12.0)),
text: cx.size_label(r.width(), r.height()),
+4 -4
View File
@@ -198,7 +198,7 @@ impl FreeTransformTool {
Op::Scale(h) => {
let mut a = scale_for_drag(d.rect, h, p, shift, m.alt);
if let Some(t) = &self.targets {
(a, self.guides) = t.snap_scale(&vectorcraft_doc::OrientedBox::aligned(d.rect), h, a, shift, m.alt, cx.tol(5.0));
(a, self.guides) = t.snap_scale(&vectorcraft_doc::OrientedBox::aligned(d.rect), h, a, shift, m.alt, cx.snap_tol());
}
let nr = a.transform_rect_bbox(d.rect);
self.measure = Some((p, cx.size_label(nr.width(), nr.height())));
@@ -232,8 +232,8 @@ impl Tool for FreeTransformTool {
self.drag = None;
let Some(r) = selection_bounds(cx) else { return vec![] };
if let Some(op) = self.classify(cx, r, p, ev.mods) {
self.targets =
(cx.smart_guides && op.handle().is_some()).then(|| crate::guides::Targets::collect(cx.doc, &cx.selection.objects, None));
self.targets = (cx.smart_guides && op.handle().is_some())
.then(|| crate::guides::Targets::collect(cx.doc, &cx.selection.objects, None).styled(cx));
self.drag = Some(Drag { op, rect: r, start: p, began: false });
}
vec![]
@@ -304,7 +304,7 @@ impl Tool for FreeTransformTool {
}
}
o.extend(self.guides.iter().cloned());
if let Some((p, t)) = &self.measure {
if let (Some((p, t)), true) = (&self.measure, cx.transform_tools_guides) {
o.push(Overlay::Measure { p: *p + Vec2::new(cx.tol(12.0), cx.tol(12.0)), text: t.clone() });
}
o
+1 -1
View File
@@ -248,7 +248,7 @@ impl Tool for TransformTool {
{
o.push(Overlay::Line { a: d.origin, b: *p, color: super::CYAN, dashed: true });
}
if let Some((p, t)) = &self.measure {
if let (Some((p, t)), true) = (&self.measure, cx.transform_tools_guides) {
o.push(Overlay::Measure { p: *p + Vec2::new(cx.tol(12.0), cx.tol(12.0)), text: t.clone() });
}
o
+30
View File
@@ -1118,6 +1118,10 @@ fn panel_drop(app: &mut VectorcraftApp, ui: &Ui, resp: &egui::Response, xf: &Xf)
fn hover_highlight(app: &VectorcraftApp, p: &egui::Painter, xf: &Xf) {
let Some(h) = app.hover_doc else { return };
// Smart Guides › Object Highlighting: a Smart Guides display option, so it needs them on.
if !app.ui.view.smart_guides || !app.session.prefs.object_highlighting {
return;
}
if app.session.tool_busy() || !matches!(app.session.tool_id(), "selection" | "directSelection" | "groupSelection") {
return;
}
@@ -2016,6 +2020,32 @@ mod tests {
assert_eq!(dots(&mut app).len(), 2, "one anchor still shows them");
}
/// Smart Guides › Object Highlighting (#394): the outline of the object under the pointer
/// shows with Smart Guides on and the option on, and not otherwise.
#[test]
fn object_highlighting_preference_hides_the_hover_outline() {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.session.execute("file.new", &json!({"width": 400, "height": 300})).unwrap();
let id = app.session.execute("shape.rectangle", &json!({"x": 100, "y": 100, "width": 100, "height": 100})).unwrap()["id"].as_u64().unwrap();
app.session.execute("select.none", &json!({})).unwrap();
let ctx = egui::Context::default();
frame(&mut app, &ctx, vec![]);
let rect = app.canvas_rect.unwrap();
let over = Xf::new(rect, app.view().unwrap()).to_screen(Point::new(150.0, 150.0));
let layer = egui::epaint::ColorMode::Solid(c32(app.session.active().unwrap().doc.layer_color(vectorcraft_doc::NodeId(id))));
// The hover outline: 1.5 px lines in the layer colour.
let outlines = |app: &mut VectorcraftApp| {
frame(app, &ctx, vec![egui::Event::PointerMoved(over)]);
shapes(app, &ctx).iter().filter(|s| matches!(s, Shape::Path(ps) if ps.stroke.width == 1.5 && ps.stroke.color == layer)).count()
};
assert!(outlines(&mut app) > 0, "highlighted by default");
app.session.execute("prefs.set", &json!({"key": "objectHighlighting", "value": false})).unwrap();
assert_eq!(outlines(&mut app), 0, "the option off");
app.session.execute("prefs.set", &json!({"key": "objectHighlighting", "value": true})).unwrap();
app.ui.view.smart_guides = false;
assert_eq!(outlines(&mut app), 0, "Smart Guides off");
}
/// Hide Corner Widget for angles greater than (#394): a rectangle's 90° corners lose their
/// widgets below 90°.
#[test]
+20
View File
@@ -1162,6 +1162,26 @@ wheel zooms about the pointer, Shift-wheel scrolls up and down, Cmd/Ctrl-wheel s
`scrubNumericFields` (on: a horizontal drag on a numeric field's label steps the field, one undo step per drag; the
control channel's `ui.drag` scrubs).
The Smart Guides preferences (Preferences › Smart Guides) apply to `pointer_gesture` with Smart Guides on (the
default view) and to the mouse; they change what the tools show and how far a target pulls, never where a snapped
point lands:
- `smartGuideColor` (`#ff3dfc` by default): the colour of the smart guides' lines and labels.
- `alignmentGuides` (on by default): off, no line is drawn along the edge or centre the art lines up with; the art
still snaps into line.
- `anchorPathLabels` (on by default): off, no "anchor", "center", "path" or "align" label.
- `measurementLabels` (on by default): off, no size or offset readout while drawing, moving, dragging a ruler guide or
drawing and resizing an artboard.
- `transformToolsGuides` (on by default): off, no readout while scaling or rotating with the Selection tool, the
Free Transform tool or the Rotate, Scale, Shear and Reflect tools.
- `objectHighlighting` (on by default; desktop app): off, the Selection tools no longer outline the object under the
pointer. The outline also needs View › Smart Guides on.
- `snappingTolerance` (0–40 px, 4 by default): how near an anchor, edge, centre or artboard edge pulls a drawn point,
a dragged selection, a bounding-box handle, a ruler guide or an artboard. With Smart Guides off, Snap to Point uses
`snapToPointTolerance` instead.
Not read yet: Construction Guides and their Angles, and Spacing Guides (the tools draw neither).
## Type preferences
- `placeholderText` (on by default): type the Type tools place (`pointer_gesture` with `type`, `areaType`…) starts with