mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
Smart Guides: Alt over another object or the artboard shows the selection's distances to it
Distance Guides, from the public "Use Distance Guides for accurate placement" and "Smart Guides options" (Invoke Distance Guides) pages: with Smart Guides on, something selected and the Selection tool, holding Alt/Option over another object shows the horizontal and vertical distances between the selection and it, and over an empty part of an artboard the selection's distances from the artboard's left, right, top and bottom edges. - guides::distance_guides measures the selection's bounds to a box: on an axis on which they lie apart, the gap between their facing edges; otherwise the offsets of both pairs of matching edges. Each line runs across the middle of the overlap on the other axis (else the selection's middle), with a dashed line out to the target's edge when it passes beside it. An artboard is always measured edge to edge. Labels are in the General unit, in the smart guide colour. - The Selection tool keeps what its Alt hover points at and draws the guides while idle; letting Alt go (the canvas re-hovers on a modifier change), a press or moving off clears them. - Overlay::Distance: the line with end ticks and its length in a pill; the canvas draws it. - Preferences › Smart Guides › Invoke Distance Guides (`distanceGuides`, on by default), with i18n rows; View › Smart Guides gates it too. - Hover only, as the pages describe: nothing while moving or nudging. Tests: select::tests::alt_over_* (gap and edges beside and below, artboard edges in pt and mm), distance_guides_need_alt_a_selection_and_ smart_guides, tests_prefs::invoke_distance_guides_measures_the_selection_ to_the_object_under_alt, canvas::tests::alt_over_another_object_draws_ the_distance_guide_label (frame test of the drawn label and line).
This commit is contained in:
committed by
bflatastic
parent
bfc2d6e439
commit
a301760aa6
@@ -234,6 +234,7 @@ pub const PREF_SPECS: &[PrefSpec] = &[
|
||||
p!("anchorPathLabels", "Smart Guides", "Display Options", "Anchor/Path Labels", bool),
|
||||
p!("measurementLabels", "Smart Guides", "Display Options", "Measurement Labels", bool),
|
||||
p!("spacingGuides", "Smart Guides", "Display Options", "Spacing Guides", bool),
|
||||
p!("distanceGuides", "Smart Guides", "Display Options", "Invoke Distance Guides", bool),
|
||||
p!("snappingTolerance", "Smart Guides", "", "Snapping Tolerance", num(0.0, 40.0, "pt")),
|
||||
p!("snapTangent", "Smart Guides", "Geometric Guides", "Snap to Tangent", bool),
|
||||
p!("snapPerpendicular", "Smart Guides", "Geometric Guides", "Snap to Perpendicular", bool),
|
||||
|
||||
@@ -970,6 +970,7 @@ pub struct Prefs {
|
||||
pub anchor_path_labels: bool,
|
||||
pub measurement_labels: bool,
|
||||
pub spacing_guides: bool,
|
||||
pub distance_guides: bool,
|
||||
pub snapping_tolerance: f64,
|
||||
pub snap_tangent: bool,
|
||||
pub snap_perpendicular: bool,
|
||||
@@ -1221,6 +1222,7 @@ impl Default for Prefs {
|
||||
anchor_path_labels: true,
|
||||
measurement_labels: true,
|
||||
spacing_guides: true,
|
||||
distance_guides: true,
|
||||
snapping_tolerance: 4.0,
|
||||
snap_tangent: true,
|
||||
snap_perpendicular: true,
|
||||
|
||||
@@ -616,6 +616,30 @@ fn spacing_guides_space_a_dragged_square_like_its_row() {
|
||||
assert_eq!(drag(&mut s), (161.0, 0), "off: where it was dragged");
|
||||
}
|
||||
|
||||
/// Smart Guides › Invoke Distance Guides: with a square selected, Alt held over another square to
|
||||
/// its right shows the gap between them in the document's General unit; with the preference or
|
||||
/// View › Smart Guides off, nothing.
|
||||
#[test]
|
||||
fn invoke_distance_guides_measures_the_selection_to_the_object_under_alt() {
|
||||
use vectorcraft_tools::{Mods, Overlay, PointerKind::Move};
|
||||
let mut s = new_doc();
|
||||
let a = square(&mut s, 100.0, 100.0);
|
||||
square(&mut s, 250.0, 100.0);
|
||||
s.execute("select.set", &json!({"ids": [a.0]})).unwrap();
|
||||
let v = ViewInfo::default();
|
||||
s.select_tool("selection", v).unwrap();
|
||||
let shown = |s: &mut Session, v: ViewInfo| {
|
||||
s.pointer(&PointerEvent::new(Move, 275.0, 125.0).with_mods(Mods { alt: true, ..Mods::default() }), v).unwrap();
|
||||
s.overlays(v).into_iter().filter_map(|o| if let Overlay::Distance { text, .. } = o { Some(text) } else { None }).collect::<Vec<_>>()
|
||||
};
|
||||
assert_eq!(shown(&mut s, v), ["100.00 pt"], "the 100 pt gap; the tops and bottoms are level");
|
||||
s.execute("document.setUnits", &json!({"units": "Millimeters"})).unwrap();
|
||||
assert_eq!(shown(&mut s, v), ["35.28 mm"]);
|
||||
assert!(shown(&mut s, ViewInfo { smart_guides: false, ..v }).is_empty(), "View › Smart Guides off");
|
||||
set_pref(&mut s, "distanceGuides", json!(false));
|
||||
assert!(shown(&mut s, v).is_empty(), "Invoke Distance Guides 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
|
||||
|
||||
@@ -282,6 +282,7 @@ impl Session {
|
||||
measurement_labels: self.prefs.measurement_labels,
|
||||
transform_tools_guides: self.prefs.transform_tools_guides,
|
||||
spacing_guides: self.prefs.spacing_guides,
|
||||
distance_guides: self.prefs.distance_guides,
|
||||
snapping_tolerance: self.prefs.snapping_tolerance,
|
||||
construction_angles: if self.prefs.construction_guides {
|
||||
vectorcraft_tools::guides::construction_angles(&self.prefs.construction_angles)
|
||||
|
||||
@@ -1574,6 +1574,49 @@ pub fn snap_pick(cx: &ToolContext, p: Point) -> (Point, Vec<Overlay>) {
|
||||
Targets::points_near(cx.doc, &roots, near).styled(cx).snap_point(p, tol)
|
||||
}
|
||||
|
||||
/// Distance Guides (Smart Guides → Invoke Distance Guides): how far the selection's bounds `from`
|
||||
/// lie from `to` along each axis, as measured lines ([`Overlay::Distance`]) labelled with their
|
||||
/// lengths in the General unit. Along an axis on which the two lie apart, one line spans the gap
|
||||
/// between their facing edges; along one on which they overlap, or one holds the other, one line
|
||||
/// runs between their left (top) edges and one between their right (bottom) edges. An artboard
|
||||
/// (`artboard`) is always measured edge to edge: the selection's distance from its left, right,
|
||||
/// top and bottom edges. Each line runs across the middle of where the two overlap on the other
|
||||
/// axis, else across the selection's middle, with a dashed line from `to`'s edge out to it when it
|
||||
/// passes beside `to`. Lines of no length are left out.
|
||||
pub fn distance_guides(cx: &ToolContext, from: Rect, to: Rect, artboard: bool) -> Vec<Overlay> {
|
||||
let finite = |r: &Rect| [r.x0, r.y0, r.x1, r.y1].iter().all(|v| v.is_finite());
|
||||
if !finite(&from) || !finite(&to) {
|
||||
return vec![];
|
||||
}
|
||||
let color = cx.smart_guide_color;
|
||||
let mut out = Vec::new();
|
||||
for x in [true, false] {
|
||||
let ((f0, f1), (t0, t1)) = (span(&from, x), span(&to, x));
|
||||
let ((g0, g1), (u0, u1)) = (span(&from, !x), span(&to, !x));
|
||||
let (lo, hi) = (g0.max(u0), g1.min(u1));
|
||||
let across = if lo <= hi { (lo + hi) / 2.0 } else { (g0 + g1) / 2.0 };
|
||||
let pt = |u: f64, c: f64| if x { Point::new(u, c) } else { Point::new(c, u) };
|
||||
// Each measured line: from the selection's edge to `to`'s.
|
||||
let lines: Vec<(f64, f64)> = match (artboard, f1 <= t0, t1 <= f0) {
|
||||
(false, true, _) => vec![(f1, t0)],
|
||||
(false, _, true) => vec![(f0, t1)],
|
||||
_ => vec![(f0, t0), (f1, t1)],
|
||||
};
|
||||
for (fu, tu) in lines {
|
||||
let length = (tu - fu).abs();
|
||||
if length <= 1e-9 {
|
||||
continue;
|
||||
}
|
||||
out.push(Overlay::Distance { a: pt(fu, across), b: pt(tu, across), text: cx.len(length), color });
|
||||
let on_edge = across.clamp(u0, u1);
|
||||
if on_edge != across {
|
||||
out.push(Overlay::Line { a: pt(tu, on_edge), b: pt(tu, across), color, dashed: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -352,6 +352,10 @@ pub struct ToolContext<'a> {
|
||||
/// two other objects in its row or column, or evenly between its two neighbours, and the
|
||||
/// equal gaps show.
|
||||
pub spacing_guides: bool,
|
||||
/// Smart Guides → Invoke Distance Guides: with the Selection tool, Alt/Option held over another
|
||||
/// object shows how far the selection is from it, and over an empty part of an artboard how
|
||||
/// far it is from the artboard's edges ([`guides::distance_guides`]).
|
||||
pub distance_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,
|
||||
@@ -455,6 +459,9 @@ pub enum Overlay {
|
||||
Label { p: Point, text: String, color: [u8; 3] },
|
||||
/// Measurement pill near the cursor (grey box with white text).
|
||||
Measure { p: Point, text: String },
|
||||
/// A Distance Guides measurement: a line from `a` to `b` with a tick across each end, and
|
||||
/// `text` (its length) in a pill of `color` on its middle.
|
||||
Distance { a: Point, b: Point, text: String, color: [u8; 3] },
|
||||
/// Translucent filled quad (text selection highlight), RGBA.
|
||||
Highlight { quad: [Point; 4], color: [u8; 4] },
|
||||
/// A colour chip of fixed screen size (a gradient stop), RGBA; ringed when selected.
|
||||
@@ -1047,6 +1054,7 @@ pub(crate) mod testutil {
|
||||
measurement_labels: true,
|
||||
transform_tools_guides: true,
|
||||
spacing_guides: true,
|
||||
distance_guides: true,
|
||||
snapping_tolerance: 4.0,
|
||||
construction_angles: guides::construction_angles(guides::DEFAULT_CONSTRUCTION_ANGLES),
|
||||
screen: None,
|
||||
|
||||
+184
-1
@@ -22,6 +22,9 @@
|
||||
//! path, area type or type on a path under the pointer, or into a new frame of the same kind, shape
|
||||
//! and size at the click on empty canvas,
|
||||
//! and a drag draws a rectangle for the new frame (`text.thread.add`). Escape drops the loaded text.
|
||||
//! With Smart Guides and Invoke Distance Guides on, Alt/Option held over another object shows how
|
||||
//! far the selection is from it, and over an empty part of an artboard how far it is from the
|
||||
//! artboard's edges ([`crate::guides::distance_guides`]).
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
@@ -33,7 +36,7 @@ use vectorcraft_geom::{Affine, Point, Rect, Vec2};
|
||||
|
||||
use crate::bbox::{Handle, hit_handle, in_rotate_zone, move_delta, rotate_for_drag, scale_for_drag};
|
||||
use crate::corners::{self, CornerDrag, over_widget};
|
||||
use crate::guides::{PointSnap, Targets, hold_on_path};
|
||||
use crate::guides::{PointSnap, Targets, distance_guides, hold_on_path};
|
||||
use crate::pathtype::{self, BracketDrag, over_bracket};
|
||||
use crate::pie::{self, PieDrag};
|
||||
use crate::rulerguide::GuideEdit;
|
||||
@@ -108,6 +111,40 @@ pub struct SelectionTool {
|
||||
guide: GuideEdit,
|
||||
/// Smart Guides: the centre or anchor under the pointer, and its label ([`Self::hover_point`]).
|
||||
hover: Option<(Point, &'static str)>,
|
||||
/// Distance Guides: what Alt held over it measures the selection to ([`distance_target`]).
|
||||
distance: Option<DistanceTo>,
|
||||
}
|
||||
|
||||
/// What Distance Guides measure the selection to.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
enum DistanceTo {
|
||||
Object(NodeId),
|
||||
/// The artboard at this index.
|
||||
Artboard(usize),
|
||||
}
|
||||
|
||||
/// Distance Guides: with Smart Guides and Smart Guides → Invoke Distance Guides on, something
|
||||
/// selected and Alt held (`m`), the object under `p` unless it is selected, or over no object the
|
||||
/// artboard `p` is on.
|
||||
fn distance_target(cx: &ToolContext, p: Point, m: Mods) -> Option<DistanceTo> {
|
||||
if !(cx.smart_guides && cx.distance_guides && m.alt) || cx.selection.objects.is_empty() {
|
||||
return None;
|
||||
}
|
||||
match hit_test(cx.doc, p, cx.selection_hit_options()).map(|h| h.top_object(cx.isolation)) {
|
||||
Some(id) => (!cx.selection.contains(id)).then_some(DistanceTo::Object(id)),
|
||||
None => cx.doc.artboard_at(p).map(DistanceTo::Artboard),
|
||||
}
|
||||
}
|
||||
|
||||
/// The Distance Guides from the selection to `to` ([`distance_guides`]); none once either is gone.
|
||||
fn distance_overlays(cx: &ToolContext, to: DistanceTo) -> Vec<Overlay> {
|
||||
let Some(from) = selection_bounds(cx) else { return vec![] };
|
||||
match to {
|
||||
DistanceTo::Object(id) if !cx.selection.contains(id) => object_bounds(cx, id).map(|r| distance_guides(cx, from, r, false)),
|
||||
DistanceTo::Object(_) => None,
|
||||
DistanceTo::Artboard(i) => cx.doc.artboards.get(i).map(|a| distance_guides(cx, from, a.rect, true)),
|
||||
}
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Snapping for the selection moved as a whole, its targets gathered when the move begins: View →
|
||||
@@ -535,6 +572,7 @@ impl Tool for SelectionTool {
|
||||
}
|
||||
|
||||
fn pointer(&mut self, cx: &ToolContext, ev: &PointerEvent) -> Vec<Action> {
|
||||
self.distance = (ev.kind == PointerKind::Move && matches!(self.state, State::Idle)).then(|| distance_target(cx, ev.pos, ev.mods)).flatten();
|
||||
if let State::Loaded { from, press } = self.state
|
||||
&& let Some(out) = self.loaded(cx, ev, from, press)
|
||||
{
|
||||
@@ -845,6 +883,9 @@ impl Tool for SelectionTool {
|
||||
o.push(Overlay::Label { p: q, text: label.into(), color: style.color });
|
||||
}
|
||||
}
|
||||
if let (Some(to), State::Idle, true, true) = (self.distance, &self.state, cx.smart_guides, cx.distance_guides) {
|
||||
o.extend(distance_overlays(cx, to));
|
||||
}
|
||||
if let Some((p, t)) = &self.measure {
|
||||
o.push(Overlay::Measure { p: *p, text: t.clone() });
|
||||
}
|
||||
@@ -1117,6 +1158,148 @@ mod tests {
|
||||
assert!(!t.overlays(&off).iter().any(|o| matches!(o, Overlay::Label { .. })));
|
||||
}
|
||||
|
||||
/// [`doc_with_rect`] (100, 100)–(200, 200), selected, and a second rectangle `other`.
|
||||
fn two_rects(other: Rect) -> (vectorcraft_doc::Document, NodeId, NodeId) {
|
||||
let (mut d, a) = doc_with_rect();
|
||||
let l = d.layers[0].id;
|
||||
let b = d.alloc_id();
|
||||
let path = vectorcraft_geom::shapes::rectangle(other);
|
||||
d.insert(Some(l), 1, vectorcraft_doc::Node::path(b, path, vectorcraft_doc::Appearance::default_art())).unwrap();
|
||||
(d, a, b)
|
||||
}
|
||||
|
||||
fn alt(kind: PointerKind, x: f64, y: f64) -> PointerEvent {
|
||||
ev(kind, x, y).with_mods(Mods { alt: true, ..Mods::default() })
|
||||
}
|
||||
|
||||
/// The Distance Guides overlays: each measured line (its ends and label), and the dashed
|
||||
/// lines out to it, in order.
|
||||
fn distances(t: &SelectionTool, cx: &ToolContext) -> (Vec<(Point, Point, String)>, Vec<(Point, Point)>) {
|
||||
let o = t.overlays(cx);
|
||||
let lines = o.iter().filter_map(|o| if let Overlay::Distance { a, b, text, .. } = o { Some((*a, *b, text.clone())) } else { None });
|
||||
let dashed = o.iter().filter_map(|o| if let Overlay::Line { a, b, dashed: true, .. } = o { Some((*a, *b)) } else { None });
|
||||
(lines.collect(), dashed.collect())
|
||||
}
|
||||
|
||||
fn line(a: (f64, f64), b: (f64, f64), text: &str) -> (Point, Point, String) {
|
||||
(Point::new(a.0, a.1), Point::new(b.0, b.1), text.to_string())
|
||||
}
|
||||
|
||||
/// Alt held over an object beside the selection shows the horizontal gap between their
|
||||
/// facing edges across the middle of where they overlap, and the vertical offsets of their
|
||||
/// top and bottom edges across the selection's middle, with dashed lines out to the other
|
||||
/// object's edges (helpx: Use Distance Guides for accurate placement).
|
||||
#[test]
|
||||
fn alt_over_an_object_beside_the_selection_measures_the_gap_and_the_edges() {
|
||||
let (d, a, _) = two_rects(Rect::new(300.0, 120.0, 400.0, 180.0));
|
||||
let s = Selection { objects: vec![a], ..Default::default() };
|
||||
let p = paint();
|
||||
let cx = cx(&d, &s, &p);
|
||||
let mut t = SelectionTool::default();
|
||||
t.pointer(&cx, &alt(PointerKind::Move, 350.0, 150.0));
|
||||
let (lines, dashed) = distances(&t, &cx);
|
||||
assert_eq!(
|
||||
lines,
|
||||
vec![
|
||||
line((200.0, 150.0), (300.0, 150.0), "100.00 pt"),
|
||||
line((150.0, 100.0), (150.0, 120.0), "20.00 pt"),
|
||||
line((150.0, 200.0), (150.0, 180.0), "20.00 pt"),
|
||||
]
|
||||
);
|
||||
let p = Point::new;
|
||||
assert_eq!(dashed, vec![(p(300.0, 120.0), p(150.0, 120.0)), (p(300.0, 180.0), p(150.0, 180.0))]);
|
||||
assert!(t.overlays(&cx).iter().all(|o| !matches!(o, Overlay::Distance { color, .. } if *color != cx.smart_guide_color)));
|
||||
}
|
||||
|
||||
/// Below the selection: the vertical gap, and the left and right edges' offsets.
|
||||
#[test]
|
||||
fn alt_over_an_object_below_the_selection_measures_the_vertical_gap() {
|
||||
let (d, a, _) = two_rects(Rect::new(120.0, 300.0, 180.0, 400.0));
|
||||
let s = Selection { objects: vec![a], ..Default::default() };
|
||||
let p = paint();
|
||||
let cx = cx(&d, &s, &p);
|
||||
let mut t = SelectionTool::default();
|
||||
t.pointer(&cx, &alt(PointerKind::Move, 150.0, 350.0));
|
||||
let (lines, dashed) = distances(&t, &cx);
|
||||
assert_eq!(
|
||||
lines,
|
||||
vec![
|
||||
line((100.0, 150.0), (120.0, 150.0), "20.00 pt"),
|
||||
line((200.0, 150.0), (180.0, 150.0), "20.00 pt"),
|
||||
line((150.0, 200.0), (150.0, 300.0), "100.00 pt"),
|
||||
]
|
||||
);
|
||||
assert_eq!(dashed.len(), 2, "out to the left and right edges of the object below");
|
||||
}
|
||||
|
||||
/// Alt held over an empty part of the artboard: the selection's distance from the artboard's
|
||||
/// left, right, top and bottom edges, in the General unit.
|
||||
#[test]
|
||||
fn alt_over_the_artboard_measures_to_its_four_edges_in_the_general_unit() {
|
||||
let (d, a, _) = two_rects(Rect::new(300.0, 120.0, 400.0, 180.0));
|
||||
let s = Selection { objects: vec![a], ..Default::default() };
|
||||
let p = paint();
|
||||
let cx = cx(&d, &s, &p);
|
||||
let mut t = SelectionTool::default();
|
||||
t.pointer(&cx, &alt(PointerKind::Move, 50.0, 450.0));
|
||||
let (lines, dashed) = distances(&t, &cx);
|
||||
assert_eq!(
|
||||
lines,
|
||||
vec![
|
||||
line((100.0, 150.0), (0.0, 150.0), "100.00 pt"),
|
||||
line((200.0, 150.0), (500.0, 150.0), "300.00 pt"),
|
||||
line((150.0, 100.0), (150.0, 0.0), "100.00 pt"),
|
||||
line((150.0, 200.0), (150.0, 500.0), "300.00 pt"),
|
||||
]
|
||||
);
|
||||
assert!(dashed.is_empty());
|
||||
let mm = ToolContext { unit: vectorcraft_doc::Unit::Millimeters, ..cx };
|
||||
t.pointer(&mm, &alt(PointerKind::Move, 50.0, 450.0));
|
||||
let texts: Vec<String> = distances(&t, &mm).0.into_iter().map(|l| l.2).collect();
|
||||
assert_eq!(texts, ["35.28 mm", "105.83 mm", "35.28 mm", "105.83 mm"]);
|
||||
// Off the artboard, over nothing: no distances.
|
||||
t.pointer(&cx, &alt(PointerKind::Move, 600.0, 600.0));
|
||||
assert!(distances(&t, &cx).0.is_empty());
|
||||
}
|
||||
|
||||
/// No Distance Guides without Alt, with nothing selected, over the selection itself, with
|
||||
/// Smart Guides or Invoke Distance Guides off, or once Alt is let go.
|
||||
#[test]
|
||||
fn distance_guides_need_alt_a_selection_and_smart_guides() {
|
||||
let (d, a, _) = two_rects(Rect::new(300.0, 120.0, 400.0, 180.0));
|
||||
let s = Selection { objects: vec![a], ..Default::default() };
|
||||
let none = Selection::default();
|
||||
let p = paint();
|
||||
let cx = cx(&d, &s, &p);
|
||||
let shown = |cx: &ToolContext, e: PointerEvent| {
|
||||
let mut t = SelectionTool::default();
|
||||
t.pointer(cx, &e);
|
||||
!distances(&t, cx).0.is_empty()
|
||||
};
|
||||
assert!(shown(&cx, alt(PointerKind::Move, 350.0, 150.0)));
|
||||
assert!(!shown(&cx, ev(PointerKind::Move, 350.0, 150.0)), "without Alt");
|
||||
assert!(!shown(&cx, ev(PointerKind::Move, 50.0, 450.0)), "without Alt, over the artboard");
|
||||
assert!(!shown(&crate::testutil::cx(&d, &none, &p), alt(PointerKind::Move, 350.0, 150.0)), "nothing selected");
|
||||
assert!(!shown(&cx, alt(PointerKind::Move, 150.0, 150.0)), "over the selection");
|
||||
assert!(
|
||||
!shown(&ToolContext { smart_guides: false, ..crate::testutil::cx(&d, &s, &p) }, alt(PointerKind::Move, 350.0, 150.0)),
|
||||
"View › Smart Guides off"
|
||||
);
|
||||
assert!(
|
||||
!shown(&ToolContext { distance_guides: false, ..crate::testutil::cx(&d, &s, &p) }, alt(PointerKind::Move, 350.0, 150.0)),
|
||||
"the preference off"
|
||||
);
|
||||
// Letting Alt go (the canvas sends a hover with the new modifiers) takes them away.
|
||||
let mut t = SelectionTool::default();
|
||||
t.pointer(&cx, &alt(PointerKind::Move, 350.0, 150.0));
|
||||
t.pointer(&cx, &ev(PointerKind::Move, 350.0, 150.0));
|
||||
assert!(distances(&t, &cx).0.is_empty());
|
||||
// A press ends them too.
|
||||
t.pointer(&cx, &alt(PointerKind::Move, 350.0, 150.0));
|
||||
t.pointer(&cx, &alt(PointerKind::Down, 350.0, 150.0));
|
||||
assert!(distances(&t, &cx).0.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn click_selects_object() {
|
||||
let (d, id) = doc_with_rect();
|
||||
|
||||
@@ -3183,6 +3183,7 @@ fn draw_overlays(p: &egui::Painter, xf: &Xf, overlays: &[Overlay], t: &Tokens, l
|
||||
p.rect_filled(r, CornerRadius::same(3), t.measure_bg);
|
||||
p.galley(sp + vec2(6.0, 4.0), galley, Color32::WHITE);
|
||||
}
|
||||
Overlay::Distance { a, b, text, color } => distance_guide(p, xf.to_screen(*a), xf.to_screen(*b), text, c32(*color)),
|
||||
Overlay::GridLine { a, b, color } => {
|
||||
let c = Color32::from_rgba_unmultiplied(color[0], color[1], color[2], color[3]);
|
||||
p.line_segment([xf.to_screen(*a), xf.to_screen(*b)], Stroke::new(1.0, c));
|
||||
@@ -3202,6 +3203,28 @@ fn draw_overlays(p: &egui::Painter, xf: &Xf, overlays: &[Overlay], t: &Tokens, l
|
||||
}
|
||||
}
|
||||
|
||||
/// A Distance Guides measurement on screen ([`Overlay::Distance`]): the line from `a` to `b` in the
|
||||
/// smart guide colour `color` with a 4 px tick across each end, and `text` in a pill of that colour
|
||||
/// on its middle, in white or black, whichever reads on it.
|
||||
fn distance_guide(p: &egui::Painter, a: Pos2, b: Pos2, text: &str, color: Color32) {
|
||||
let s = Stroke::new(1.0, color);
|
||||
p.line_segment([a, b], s);
|
||||
let along = b - a;
|
||||
let len = along.length();
|
||||
if len > 0.0 && len.is_finite() {
|
||||
let across = vec2(-along.y, along.x) / len * 4.0;
|
||||
for end in [a, b] {
|
||||
p.line_segment([end - across, end + across], s);
|
||||
}
|
||||
}
|
||||
let luma = 0.299 * f32::from(color.r()) + 0.587 * f32::from(color.g()) + 0.114 * f32::from(color.b());
|
||||
let ink = if luma > 150.0 { Color32::BLACK } else { Color32::WHITE };
|
||||
let galley = p.layout_no_wrap(text.to_owned(), egui::FontId::proportional(11.0), ink);
|
||||
let r = egui::Rect::from_center_size(a + along / 2.0, galley.size() + vec2(8.0, 4.0));
|
||||
p.rect_filled(r, CornerRadius::same(3), color);
|
||||
p.galley(r.min + vec2(4.0, 2.0), galley, ink);
|
||||
}
|
||||
|
||||
/// The Home screen (`app.home`; with no document open, unless its preference is off).
|
||||
fn home(app: &mut VectorcraftApp, ui: &mut Ui, rect: egui::Rect) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
@@ -9473,6 +9496,70 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// Distance Guides on screen: with a square selected and Alt held over another to its right,
|
||||
/// the frame draws the measured line between their facing edges with the gap's length in a
|
||||
/// pill of the smart guide colour on its middle; without Alt, or with Alt let go, nothing.
|
||||
#[test]
|
||||
fn alt_over_another_object_draws_the_distance_guide_label() {
|
||||
use egui::{Event, Modifiers};
|
||||
use vectorcraft_tools::guides::MAGENTA;
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 400, "height": 300})).unwrap();
|
||||
let a = app.run("shape.rectangle", json!({"x": 50, "y": 50, "width": 50, "height": 50})).unwrap()["id"].clone();
|
||||
app.run("shape.rectangle", json!({"x": 200, "y": 50, "width": 50, "height": 50})).unwrap();
|
||||
app.run("select.set", json!({"ids": [a]})).unwrap();
|
||||
app.ui.view.smart_guides = true;
|
||||
app.select_tool("selection");
|
||||
let ctx = egui::Context::default();
|
||||
frame(&mut app, &ctx, vec![]);
|
||||
let view = app.view_mut().unwrap();
|
||||
view.zoom = 1.0;
|
||||
view.rotation = 0.0;
|
||||
view.center = Point::new(200.0, 150.0);
|
||||
frame(&mut app, &ctx, vec![]);
|
||||
let xf = Xf::new(app.canvas_rect.unwrap(), app.view().unwrap());
|
||||
let over = xf.to_screen(Point::new(225.0, 75.0));
|
||||
// One frame with `events` → the shapes drawn, flattened. The modifiers held stay from one
|
||||
// frame to the next until an `Event::ModifiersChanged`.
|
||||
let run = |app: &mut VectorcraftApp, events: Vec<Event>| {
|
||||
let raw = egui::RawInput { screen_rect: Some(WINDOW), events, ..Default::default() };
|
||||
let mut out = ctx.run_ui(raw, |ui| show(app, ui));
|
||||
out.textures_delta.clear();
|
||||
fn flat(s: Shape, out: &mut Vec<Shape>) {
|
||||
match s {
|
||||
Shape::Vec(v) => v.into_iter().for_each(|s| flat(s, out)),
|
||||
s => out.push(s),
|
||||
}
|
||||
}
|
||||
let mut v = vec![];
|
||||
out.shapes.into_iter().for_each(|c| flat(c.shape, &mut v));
|
||||
v
|
||||
};
|
||||
// The label "100.00 pt" drawn and the pill it sits in, if any.
|
||||
let label = |s: &[Shape]| {
|
||||
let text = s
|
||||
.iter()
|
||||
.find_map(|s| if let Shape::Text(t) = s { (t.galley.text() == "100.00 pt").then(|| t.visual_bounding_rect()) } else { None })?;
|
||||
let pill = s.iter().find_map(|s| {
|
||||
if let Shape::Rect(r) = s { (r.fill == c32(MAGENTA) && r.rect.contains(text.center())).then_some(r.rect) } else { None }
|
||||
});
|
||||
Some((text, pill))
|
||||
};
|
||||
run(&mut app, vec![Event::PointerMoved(over)]);
|
||||
assert_eq!(label(&run(&mut app, vec![])), None, "without Alt");
|
||||
run(&mut app, vec![Event::ModifiersChanged(Modifiers::ALT)]);
|
||||
let s = run(&mut app, vec![]);
|
||||
let (text, pill) = label(&s).expect("the gap's length is drawn");
|
||||
let pill = pill.expect("in a pill of the smart guide colour");
|
||||
let mid = xf.to_screen(Point::new(150.0, 75.0));
|
||||
assert!(pill.center().distance(mid) < 1.0 && pill.contains_rect(text), "{pill:?} on {mid:?}");
|
||||
let (from, to) = (xf.to_screen(Point::new(100.0, 75.0)), xf.to_screen(Point::new(200.0, 75.0)));
|
||||
let measured = s.iter().any(|s| matches!(s, Shape::LineSegment { points, stroke } if *points == [from, to] && stroke.color == c32(MAGENTA)));
|
||||
assert!(measured, "the measured line between the facing edges");
|
||||
run(&mut app, vec![Event::ModifiersChanged(Modifiers::NONE)]);
|
||||
assert_eq!(label(&run(&mut app, vec![])), None, "Alt let go");
|
||||
}
|
||||
|
||||
/// The 800 × 600 window [`frame`] draws the canvas in.
|
||||
const WINDOW: egui::Rect = egui::Rect { min: Pos2::ZERO, max: Pos2 { x: 800.0, y: 600.0 } };
|
||||
|
||||
|
||||
@@ -3028,6 +3028,7 @@ font-technology Variable Променливи
|
||||
Space Before Paragraph Разстояние преди абзац
|
||||
Spaces Интервали
|
||||
Spacing Guides Водачи за разстояние
|
||||
Invoke Distance Guides Извикване на водачи за отстояния
|
||||
Spacing: Разстояние:
|
||||
Spanish Испански
|
||||
Specified Distance Упоменато разстояние
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3521,6 +3521,7 @@ theme Appearance Motiv
|
||||
Size/Leading Velikost/proklad
|
||||
Snapping Tolerance Tolerance přichytávání
|
||||
Spacing Guides Vodítka rozestupů
|
||||
Invoke Distance Guides Vyvolat vodítka vzdálenosti
|
||||
Spanish Španělština
|
||||
Subdivisions Dělení
|
||||
Templates Folder Složka šablon
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3270,6 +3270,7 @@ theme Appearance Udseende
|
||||
Size/Leading Størrelse/linjeafstand
|
||||
Snapping Tolerance Snap-tolerance
|
||||
Spacing Guides Afstandshjælpelinjer
|
||||
Invoke Distance Guides Aktivér hjælpelinjer for afstandsmåling
|
||||
Spanish Spansk
|
||||
Subdivisions Underinddelinger
|
||||
Templates Folder Skabelonmappe
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3066,6 +3066,7 @@ effect Points Punkte
|
||||
Space Before Paragraph Abstand vor Absatz
|
||||
Spaces Leerzeichen
|
||||
Spacing Guides Abstandshilfslinien
|
||||
Invoke Distance Guides Distanzhilfslinien aufrufen
|
||||
Spacing: Abstand:
|
||||
Spanish Spanisch
|
||||
Specified Distance Festgelegter Abstand
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3476,6 +3476,7 @@ theme Appearance Εμφάνιση
|
||||
Space Before Paragraph Διάστημα πριν από την παράγραφο
|
||||
Spaces Διαστήματα
|
||||
Spacing Guides Οδηγοί αποστάσεων
|
||||
Invoke Distance Guides Εμφάνιση οδηγών μέτρησης απόστασης
|
||||
Spanish Ισπανικά
|
||||
Specified Distance Καθορισμένη απόσταση
|
||||
Specified Steps Καθορισμένα βήματα
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2059 and column 19.
|
@@ -3096,6 +3096,7 @@ font-technology Variable Variables
|
||||
Space Before Paragraph Espacio antes del párrafo
|
||||
Spaces Espacios
|
||||
Spacing Guides Guías de espaciado
|
||||
Invoke Distance Guides Invocar guías de distancia
|
||||
Spacing: Espaciado:
|
||||
Spanish Español
|
||||
Specified Distance Distancia especificada
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3447,6 +3447,7 @@ theme Appearance Ulkoasu
|
||||
Size/Leading Koko/riviväli
|
||||
Snapping Tolerance Tartuntatoleranssi
|
||||
Spacing Guides Välistysapuviivat
|
||||
Invoke Distance Guides Käytä etäisyysapuviivoja
|
||||
Spanish Espanja
|
||||
Subdivisions Jaot
|
||||
Templates Folder Mallipohjakansio
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3093,6 +3093,7 @@ font-technology Variable Variables
|
||||
Space Before Paragraph Espace avant le paragraphe
|
||||
Spaces Espaces
|
||||
Spacing Guides Repères d’espacement
|
||||
Invoke Distance Guides Appeler les repères de distance
|
||||
Spacing: Espacement :
|
||||
Spanish Espagnol
|
||||
Specified Distance Distance définie
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3735,6 +3735,7 @@ mark-style Japanese Japán
|
||||
Spaces Szóközök
|
||||
Spacing Térköz
|
||||
Spacing Guides Térköz-segédvonalak
|
||||
Invoke Distance Guides Távolság-segédvonalak előhívása
|
||||
Spacing: Térköz:
|
||||
Spanish Spanyol
|
||||
Spatter Fröcskölés
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3270,6 +3270,7 @@ theme Appearance Tampilan
|
||||
Size/Leading Ukuran/Leading
|
||||
Snapping Tolerance Toleransi kancing
|
||||
Spacing Guides Panduan jarak
|
||||
Invoke Distance Guides Panggil panduan pengukuran jarak
|
||||
Spanish Spanyol
|
||||
Subdivisions Subdivisi
|
||||
Templates Folder Folder templat
|
||||
|
||||
|
@@ -3130,6 +3130,7 @@ mark-style Japanese Giapponese
|
||||
Space Before Paragraph Spazio prima del paragrafo
|
||||
Spaces Spazi
|
||||
Spacing Guides Guide di spaziatura
|
||||
Invoke Distance Guides Richiama guide di distanza
|
||||
Spacing: Spaziatura:
|
||||
Spanish Spagnolo
|
||||
Specified Distance Distanza specificata
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2896,6 +2896,7 @@ mark-style Japanese 日本式
|
||||
Space Before Paragraph 段落前のアキ
|
||||
Spaces スペース
|
||||
Spacing Guides 間隔ガイド
|
||||
Invoke Distance Guides 距離ガイドを呼び出す
|
||||
Spacing: 間隔:
|
||||
Spanish スペイン語
|
||||
Specify: 指定:
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3080,6 +3080,7 @@ theme Appearance იერსახე
|
||||
Size/Leading ზომა/სტრიქონთა დაშორება
|
||||
Snapping Tolerance მიბმის დაშვება
|
||||
Spacing Guides შუალედის დამხმარე ხაზები
|
||||
Invoke Distance Guides მანძილის დამხმარე ხაზების გამოძახება
|
||||
Spanish ესპანური
|
||||
Subdivisions ქვედანაყოფები
|
||||
Templates Folder შაბლონების საქაღალდე
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2890,6 +2890,7 @@ mark-style Japanese 일본식
|
||||
Space Before Paragraph 단락 앞 간격
|
||||
Spaces 공백
|
||||
Spacing Guides 간격 안내선
|
||||
Invoke Distance Guides 거리 안내선 호출
|
||||
Spacing: 간격:
|
||||
Spanish Español
|
||||
Specify: 지정:
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3284,6 +3284,7 @@ theme Appearance Vormgeving
|
||||
Size/Leading Tekengrootte/Regelafstand
|
||||
Snapping Tolerance Snapping-tolerantie
|
||||
Spacing Guides Hulplijnen voor tussenruimte
|
||||
Invoke Distance Guides Afstandshulplijnen oproepen
|
||||
Spanish Spaans
|
||||
Subdivisions Onderverdelingen
|
||||
Templates Folder Sjablonenmap
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3383,6 +3383,7 @@ theme Appearance Wygląd
|
||||
Size/Leading Rozmiar/interlinia
|
||||
Snapping Tolerance Tolerancja przyciągania
|
||||
Spacing Guides Linie odstępów
|
||||
Invoke Distance Guides Wywołuj linie odległości
|
||||
Spanish Hiszpański
|
||||
Subdivisions Podpodziały
|
||||
Templates Folder Folder szablonów
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2564,6 +2564,7 @@
|
||||
Space Before Paragraph Espaço Antes do Parágrafo
|
||||
Spaces Espaços
|
||||
Spacing Guides Guias de Espaçamento
|
||||
Invoke Distance Guides Invocar Guias de Distância
|
||||
Spacing: Espaçamento:
|
||||
Spanish Espanhol
|
||||
Specify: Especificar:
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -3126,6 +3126,7 @@ mark-style Japanese Японский
|
||||
Space Before Paragraph Интервал перед абзацем
|
||||
Spaces Пробелы
|
||||
Spacing Guides Направляющие интервалов
|
||||
Invoke Distance Guides Вызывать направляющие расстояний
|
||||
Spacing: Интервал:
|
||||
Spanish Испанский
|
||||
Specified Distance Заданное расстояние
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3028,6 +3028,7 @@ font-technology Variable Variabla
|
||||
Space Before Paragraph Avstånd före stycke
|
||||
Spaces Blanksteg
|
||||
Spacing Guides Avståndsstödlinjer
|
||||
Invoke Distance Guides Anropa mätstödlinjer för avstånd
|
||||
Spacing: Avstånd:
|
||||
Spanish Spanska
|
||||
Specified Distance Angivet avstånd
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 25.
|
@@ -2934,6 +2934,7 @@ font-technology Variable Değişken
|
||||
Space Before Paragraph Paragraftan Önce Boşluk
|
||||
Spaces Alanlar
|
||||
Spacing Guides Aralık Kılavuzları
|
||||
Invoke Distance Guides Mesafe Kılavuzlarını Çağır
|
||||
Spacing: Aralık:
|
||||
Spanish İspanyolca
|
||||
Specified Distance Belirtilen Mesafe
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 25.
|
@@ -3123,6 +3123,7 @@ effect Points Точки
|
||||
Space Before Paragraph Інтервал перед абзацом
|
||||
Spaces Пробіли
|
||||
Spacing Guides Напрямні інтервалів
|
||||
Invoke Distance Guides Викликати напрямні відстаней
|
||||
Spacing: Інтервал:
|
||||
Spanish Іспанська
|
||||
Specified Distance Задана відстань
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 740 and column 40.
|
@@ -3462,6 +3462,7 @@ mark-style Japanese Kiểu Nhật
|
||||
Spaces Khoảng trắng
|
||||
Spacing Khoảng cách
|
||||
Spacing Guides Đường căn khoảng cách
|
||||
Invoke Distance Guides Gọi đường căn đo khoảng cách
|
||||
Spacing: Khoảng cách:
|
||||
Spanish Tiếng Tây Ban Nha
|
||||
Spatter Bắn tung tóe
|
||||
|
||||
|
@@ -2962,6 +2962,7 @@ font-script Japanese 汉字
|
||||
Space Before Paragraph 段前间距
|
||||
Spaces 空格
|
||||
Spacing Guides 间距参考线
|
||||
Invoke Distance Guides 调用距离参考线
|
||||
Spacing: 间距:
|
||||
Spanish 西班牙语
|
||||
Specified Distance 指定距离
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3887,6 +3887,7 @@ font-script Japanese 漢字
|
||||
Space Before Paragraph 段前間距
|
||||
Spaces 空格
|
||||
Spacing Guides 間距參考線
|
||||
Invoke Distance Guides 叫用距離參考線
|
||||
Spacing: 間距:
|
||||
Spanish 西班牙文
|
||||
Specified Distance 指定距離
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
+4
-1
File diff suppressed because one or more lines are too long
@@ -2721,6 +2721,11 @@ Guides change where a snapped point lands:
|
||||
- `spacingGuides` (on by default): a moved selection (Selection tool / `pointer_gesture` with `selection`) snaps so
|
||||
the gap to a neighbour matches the equal spacing among other objects nearby; off, only alignment and construction
|
||||
guides pull.
|
||||
- `distanceGuides` (Invoke Distance Guides, on by default): with Smart Guides on, something selected and the Selection
|
||||
tool hovering with `mods.alt` (a `pointer` event of kind `move`), the canvas measures the selection's bounds to the
|
||||
object under the pointer (the gap between their facing edges on an axis on which they lie apart, else the offsets
|
||||
of their two edges on it) or, over no object, to the left, right, top and bottom edges of the artboard under the
|
||||
pointer: measured lines labelled in the General unit. Off, Alt-hovering shows none.
|
||||
|
||||
With Smart Guides on, a bounding-box handle dragged with the Selection or Free Transform tool (`mods.shift`
|
||||
proportional, `mods.alt` from the centre) lands on another object's anchor or centre (labelled "anchor" or
|
||||
|
||||
File diff suppressed because one or more lines are too long
+3
-1
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user