mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-26 18:56:55 -06:00
Merge pull request #3832 from Taha-Mahmoodi/feat/distance-guides
Smart Guides: Alt over another object or the artboard shows the selection's distances to it
This commit is contained in:
@@ -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)
|
||||
@@ -438,6 +439,17 @@ impl Session {
|
||||
self.with_tool_cx(view, |t, cx| t.ime_caret(cx))
|
||||
}
|
||||
|
||||
/// The pointer stopped hovering for the active tool ([`vectorcraft_tools::Tool::hover_ended`]).
|
||||
pub fn tool_hover_ended(&mut self, view: ViewInfo) {
|
||||
self.with_tool_cx(view, |t, cx| t.hover_ended(cx));
|
||||
}
|
||||
|
||||
/// Whether the active tool's hover depends on what lies under a still pointer
|
||||
/// ([`vectorcraft_tools::Tool::wants_rehover`]).
|
||||
pub fn tool_wants_rehover(&self) -> bool {
|
||||
self.tool.wants_rehover()
|
||||
}
|
||||
|
||||
pub fn tool_busy(&self) -> bool {
|
||||
self.tool.busy()
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
@@ -806,6 +813,17 @@ pub trait Tool: Send {
|
||||
None
|
||||
}
|
||||
fn notify(&mut self, _cx: &ToolContext, _what: &str) {}
|
||||
/// The pointer stopped hovering for the tool while it holds no press: it left the canvas (for a
|
||||
/// panel or out of the window), went under a floating panel, menu, popup or dialog, or a press
|
||||
/// started panning, zooming or turning the view. Drop what shows only for the hover (the
|
||||
/// Selection tool's Distance Guides).
|
||||
fn hover_ended(&mut self, _cx: &ToolContext) {}
|
||||
/// Whether what the tool shows for the hover depends on what lies under a still pointer: then
|
||||
/// the canvas hovers it again where the pointer is when the view, the document or the
|
||||
/// selection changes (the Selection tool while Alt shows Distance Guides).
|
||||
fn wants_rehover(&self) -> bool {
|
||||
false
|
||||
}
|
||||
/// Called when the user switches away (finish pending work).
|
||||
fn deactivate(&mut self, _cx: &ToolContext) -> Vec<Action> {
|
||||
vec![]
|
||||
@@ -1047,6 +1065,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,
|
||||
|
||||
+211
-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,43 @@ 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>,
|
||||
/// The last hover had Alt held: what it measures depends on what lies under the pointer
|
||||
/// ([`Tool::wants_rehover`]).
|
||||
alt_hover: bool,
|
||||
}
|
||||
|
||||
/// 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 →
|
||||
@@ -523,6 +563,17 @@ impl Tool for SelectionTool {
|
||||
}
|
||||
}
|
||||
|
||||
/// The pointer hovers nothing for the tool: its Smart Guides label and its Distance Guides go.
|
||||
fn hover_ended(&mut self, _cx: &ToolContext) {
|
||||
self.hover = None;
|
||||
self.distance = None;
|
||||
self.alt_hover = false;
|
||||
}
|
||||
|
||||
fn wants_rehover(&self) -> bool {
|
||||
self.alt_hover
|
||||
}
|
||||
|
||||
fn transforming(&self) -> bool {
|
||||
match &self.state {
|
||||
State::Moving { began: true, .. } | State::Scaling { .. } | State::Rotating { .. } => true,
|
||||
@@ -535,6 +586,9 @@ impl Tool for SelectionTool {
|
||||
}
|
||||
|
||||
fn pointer(&mut self, cx: &ToolContext, ev: &PointerEvent) -> Vec<Action> {
|
||||
let hovering = ev.kind == PointerKind::Move && matches!(self.state, State::Idle);
|
||||
self.alt_hover = hovering && ev.mods.alt;
|
||||
self.distance = hovering.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 +899,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 +1174,159 @@ 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() })
|
||||
}
|
||||
|
||||
/// A Distance Guides measured line: its ends and its label.
|
||||
type DistanceLine = (Point, Point, String);
|
||||
|
||||
/// The Distance Guides overlays: each measured line, and the dashed lines out to it, in order.
|
||||
fn distances(t: &SelectionTool, cx: &ToolContext) -> (Vec<DistanceLine>, 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) -> DistanceLine {
|
||||
(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());
|
||||
// So does the hover ending (the pointer leaving the canvas) with Alt still held.
|
||||
t.pointer(&cx, &alt(PointerKind::Move, 350.0, 150.0));
|
||||
assert!(!distances(&t, &cx).0.is_empty());
|
||||
assert!(t.wants_rehover(), "an Alt hover is hovered again when what lies under it changes");
|
||||
t.hover_ended(&cx);
|
||||
assert!(distances(&t, &cx).0.is_empty());
|
||||
assert!(!t.wants_rehover());
|
||||
t.pointer(&cx, &ev(PointerKind::Move, 350.0, 150.0));
|
||||
assert!(!t.wants_rehover(), "nor without Alt");
|
||||
// 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();
|
||||
|
||||
@@ -102,6 +102,27 @@ enum Drag {
|
||||
Present,
|
||||
}
|
||||
|
||||
/// Whether drag `d` is a press the tool holds. Every other drag (panning, zooming, turning the
|
||||
/// view, a touch gesture, Presentation Mode) never reaches the tool, so its hover ends with it.
|
||||
fn tool_holds(d: Drag) -> bool {
|
||||
matches!(d, Drag::Tool | Drag::Art)
|
||||
}
|
||||
|
||||
/// What a hover depends on besides the pointer's screen position.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
struct HoverContext {
|
||||
/// The active tool: a tool switched to under a still pointer is hovered at once.
|
||||
tool: &'static str,
|
||||
/// The canvas, and the view's centre, zoom and rotation.
|
||||
canvas: egui::Rect,
|
||||
center: Point,
|
||||
zoom: f64,
|
||||
rotation: f64,
|
||||
/// The active document's id and revision (which moves with every edit, undo and selection
|
||||
/// change).
|
||||
doc: Option<(u64, u64)>,
|
||||
}
|
||||
|
||||
/// Clear canvas interaction state before a tab transfer takes ownership of the artwork drag.
|
||||
pub(crate) fn end_document_drag(ctx: &egui::Context) {
|
||||
ctx.data_mut(|d| {
|
||||
@@ -955,6 +976,36 @@ fn handle_input(app: &mut VectorcraftApp, ui: &Ui, resp: &egui::Response, rect:
|
||||
d.insert_temp(id, m);
|
||||
prev.is_some_and(|prev| prev != m)
|
||||
});
|
||||
// Where the tool hovers: the pointer over the canvas itself, not over a floating panel, a menu,
|
||||
// a popup or a dialog drawn on top of it ([`on_canvas`]).
|
||||
let hover_tool = hover.filter(|p| on_canvas(ui, resp, *p));
|
||||
// Whether the tool hovered last frame: coming onto the canvas re-hovers it, even after a single
|
||||
// jump egui doesn't count as moving, and leaving it ends the tool's hover (below).
|
||||
let was_over = ui.data_mut(|d| {
|
||||
let id = egui::Id::new("canvas-hovered");
|
||||
let prev = d.get_temp::<bool>(id).unwrap_or(false);
|
||||
d.insert_temp(id, hover_tool.is_some());
|
||||
prev
|
||||
});
|
||||
// What a hover means besides where the pointer is on screen: the tool, the canvas, the view
|
||||
// (scrolled, zoomed or turned by the wheel, a gesture, keys or a command) and the document and
|
||||
// selection as they are (another tab, an edit, an undo). Under a still pointer, a tool switched
|
||||
// to is hovered at once, and when the rest changes, a tool that asks for it
|
||||
// ([`Session::tool_wants_rehover`]) is hovered again where the pointer is.
|
||||
let context = HoverContext {
|
||||
tool: app.session.tool_id(),
|
||||
canvas: rect,
|
||||
center: v.center,
|
||||
zoom: v.zoom,
|
||||
rotation: v.rotation,
|
||||
doc: app.session.active().map(|st| (st.uid, st.revision)),
|
||||
};
|
||||
let (tool_switched, context_changed) = ui.data_mut(|d| {
|
||||
let id = egui::Id::new("canvas-hover-context");
|
||||
let prev = d.get_temp::<HoverContext>(id);
|
||||
d.insert_temp(id, context);
|
||||
prev.map_or((false, false), |prev| (prev.tool != context.tool, prev != context))
|
||||
});
|
||||
|
||||
// In Presentation Mode a click anywhere goes to the next artboard. The tool receives no press,
|
||||
// drag or hover, and a drag does not pan the view. A drag that began before the mode was turned
|
||||
@@ -1022,6 +1073,10 @@ fn handle_input(app: &mut VectorcraftApp, ui: &Ui, resp: &egui::Response, rect:
|
||||
crate::document_drag::begin(app, ui.ctx(), ev.pos);
|
||||
Drag::Tool
|
||||
};
|
||||
// Panning, zooming or turning the view takes the pointer from the tool's hover.
|
||||
if !tool_holds(d) {
|
||||
app.session.tool_hover_ended(view);
|
||||
}
|
||||
ui.data_mut(|dd| {
|
||||
dd.insert_temp(drag_id(), d);
|
||||
dd.insert_temp(drag_pos_id(), p);
|
||||
@@ -1038,6 +1093,7 @@ fn handle_input(app: &mut VectorcraftApp, ui: &Ui, resp: &egui::Response, rect:
|
||||
{
|
||||
// Middle-button drag pans the view whatever the tool.
|
||||
ui.data_mut(|dd| dd.insert_temp(drag_id(), Drag::Pan { start, center: v.center, middle: true }));
|
||||
app.session.tool_hover_ended(view);
|
||||
} else if let Some(d) = drag {
|
||||
// The pointer gone (a pen lifted with its press), the drag ends where it was last seen.
|
||||
let p = pointer.interact_pos().or_else(|| ui.data(|dd| dd.get_temp(drag_pos_id()))).unwrap_or(rect.center());
|
||||
@@ -1189,12 +1245,19 @@ fn handle_input(app: &mut VectorcraftApp, ui: &Ui, resp: &egui::Response, rect:
|
||||
Drag::Pan { .. } | Drag::RotateView { .. } => {}
|
||||
}
|
||||
}
|
||||
} else if let Some(p) = hover
|
||||
&& (pointer.is_moving() || mods_changed)
|
||||
} else if let Some(p) = hover_tool
|
||||
&& (pointer.is_moving() || mods_changed || !was_over || tool_switched || (context_changed && app.session.tool_wants_rehover()))
|
||||
{
|
||||
let ev = PointerEvent { kind: PointerKind::Move, pos: xf.to_doc(p), mods: mods(m, space), pressure: 1.0 };
|
||||
dispatch(app, &ev, view);
|
||||
}
|
||||
// The pointer left the canvas (for a panel, or out of the window) or went under a floating
|
||||
// panel, menu, popup or dialog, with no press the tool holds: the tool's hover ends, which no
|
||||
// hover event would clear (Alt's Distance Guides).
|
||||
if was_over && hover_tool.is_none() && !drag.is_some_and(tool_holds) {
|
||||
app.session.tool_hover_ended(view);
|
||||
ui.ctx().request_repaint();
|
||||
}
|
||||
if pointer.primary_released()
|
||||
&& let Some(i) = ui.data_mut(|d| d.remove_temp::<usize>(name_press_id()))
|
||||
{
|
||||
@@ -3183,6 +3246,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 +3266,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 +9559,203 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// Distance Guides on a canvas at 100%: a 400 × 300 pt document with a 50 pt square at (50, 50),
|
||||
/// selected, and with `other` another 100 pt to its right, the Selection tool and Smart Guides
|
||||
/// on → the app, its context, the view, the screen point over the middle of where the other
|
||||
/// square is (or would be) and the selected square's id.
|
||||
fn distance_scene(other: bool) -> (VectorcraftApp, egui::Context, Xf, Pos2, serde_json::Value) {
|
||||
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();
|
||||
if other {
|
||||
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));
|
||||
(app, ctx, xf, over, a)
|
||||
}
|
||||
|
||||
/// One frame with `events` → the shapes drawn, flattened. The modifiers held stay from one
|
||||
/// frame to the next until an `Event::ModifiersChanged`.
|
||||
fn distance_frame(app: &mut VectorcraftApp, ctx: &egui::Context, events: Vec<egui::Event>) -> Vec<Shape> {
|
||||
distance_frame_covered(app, ctx, events, None)
|
||||
}
|
||||
|
||||
/// What opens over the canvas in [`distance_frame_covered`].
|
||||
#[derive(Clone, Copy)]
|
||||
enum Cover {
|
||||
/// A floating window (a floating panel, a menu, a popup) over this rectangle.
|
||||
Panel(egui::Rect),
|
||||
/// A modal dialog, with the backdrop that keeps the rest of the app from the pointer.
|
||||
Dialog,
|
||||
}
|
||||
|
||||
/// [`distance_frame`] with `cover` drawn over the canvas.
|
||||
fn distance_frame_covered(app: &mut VectorcraftApp, ctx: &egui::Context, events: Vec<egui::Event>, cover: Option<Cover>) -> Vec<Shape> {
|
||||
let raw = egui::RawInput { screen_rect: Some(WINDOW), events, ..Default::default() };
|
||||
let mut out = ctx.run_ui(raw, |ui| {
|
||||
show(app, ui);
|
||||
match cover {
|
||||
Some(Cover::Panel(r)) => {
|
||||
egui::Area::new(egui::Id::new("test-floating-panel")).order(egui::Order::Foreground).fixed_pos(r.min).show(ui.ctx(), |ui| {
|
||||
ui.allocate_space(r.size());
|
||||
});
|
||||
}
|
||||
Some(Cover::Dialog) => {
|
||||
crate::dialogs::modal::show(ui.ctx(), "Test", egui::Id::new("test-dialog"), 0.0, 8, |ui| ui.label("A dialog"));
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
});
|
||||
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 texts drawn in `s`.
|
||||
fn texts(s: &[Shape]) -> Vec<String> {
|
||||
s.iter().filter_map(|s| if let Shape::Text(t) = s { Some(t.galley.text().to_string()) } else { None }).collect()
|
||||
}
|
||||
|
||||
/// The label "100.00 pt" drawn in `s` and the pill of the smart guide colour it sits in, if any.
|
||||
fn gap_label(s: &[Shape]) -> Option<(egui::Rect, Option<egui::Rect>)> {
|
||||
let magenta = c32(vectorcraft_tools::guides::MAGENTA);
|
||||
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 == magenta && r.rect.contains(text.center())).then_some(r.rect) } else { None });
|
||||
Some((text, pill))
|
||||
}
|
||||
|
||||
/// 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};
|
||||
let (mut app, ctx, xf, over, _) = distance_scene(true);
|
||||
let mut run = |events| distance_frame(&mut app, &ctx, events);
|
||||
run(vec![Event::PointerMoved(over)]);
|
||||
assert_eq!(gap_label(&run(vec![])), None, "without Alt");
|
||||
run(vec![Event::ModifiersChanged(Modifiers::ALT)]);
|
||||
let s = run(vec![]);
|
||||
let (text, pill) = gap_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 magenta = c32(vectorcraft_tools::guides::MAGENTA);
|
||||
let measured = s.iter().any(|s| matches!(s, Shape::LineSegment { points, stroke } if *points == [from, to] && stroke.color == magenta));
|
||||
assert!(measured, "the measured line between the facing edges");
|
||||
run(vec![Event::ModifiersChanged(Modifiers::NONE)]);
|
||||
assert_eq!(gap_label(&run(vec![])), None, "Alt let go");
|
||||
}
|
||||
|
||||
/// The Distance Guides go when the pointer stops hovering for the tool with Alt still held: it
|
||||
/// leaves the canvas for a panel or out of the window, or a press pans (Alt+Space) and takes it
|
||||
/// over the panels; they come back when it hovers the other square again.
|
||||
#[test]
|
||||
fn distance_guides_go_when_the_hover_ends() {
|
||||
use egui::{Event, Modifiers};
|
||||
let (mut app, ctx, xf, over, _) = distance_scene(true);
|
||||
let outside = pos2(xf.rect.max.x + 10.0, xf.rect.center().y);
|
||||
assert!(WINDOW.contains(outside) && !xf.rect.contains(outside), "a panel beside the canvas");
|
||||
let mut run = |events| distance_frame(&mut app, &ctx, events);
|
||||
run(vec![Event::ModifiersChanged(Modifiers::ALT), Event::PointerMoved(over)]);
|
||||
assert!(gap_label(&run(vec![])).is_some(), "shown over the other square");
|
||||
run(vec![Event::PointerMoved(outside)]);
|
||||
assert_eq!(gap_label(&run(vec![])), None, "gone over a panel, Alt still held");
|
||||
run(vec![Event::PointerMoved(over)]);
|
||||
assert!(gap_label(&run(vec![])).is_some(), "back over the other square");
|
||||
run(vec![Event::PointerGone]);
|
||||
assert_eq!(gap_label(&run(vec![])), None, "gone out of the window");
|
||||
// Back over the square, Space held with Alt pans: they go at the press, and stay gone while
|
||||
// the pan takes the pointer out over a panel.
|
||||
let space = |pressed| Event::Key { key: egui::Key::Space, physical_key: None, pressed, repeat: false, modifiers: Modifiers::ALT };
|
||||
let button = |pos, pressed| Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed, modifiers: Modifiers::ALT };
|
||||
run(vec![Event::PointerMoved(over)]);
|
||||
run(vec![space(true)]);
|
||||
assert!(gap_label(&run(vec![])).is_some(), "Space held: still shown");
|
||||
run(vec![button(over, true)]);
|
||||
assert_eq!(gap_label(&run(vec![])), None, "gone at the pan's press");
|
||||
run(vec![Event::PointerMoved(over + vec2(20.0, 0.0))]);
|
||||
run(vec![Event::PointerMoved(outside)]);
|
||||
assert_eq!(gap_label(&run(vec![])), None, "still gone after panning out over a panel");
|
||||
run(vec![button(outside, false), space(false)]);
|
||||
}
|
||||
|
||||
/// With Alt held and the pointer still, the Distance Guides follow what lies under it: they go
|
||||
/// under a floating panel or a modal dialog and come back when it closes; scrolled so that
|
||||
/// empty artboard lies under the pointer, they measure to the artboard; when an edit puts an
|
||||
/// object under the pointer, they measure to it; and the Selection tool switched to shows them
|
||||
/// at once.
|
||||
#[test]
|
||||
fn distance_guides_follow_what_lies_under_a_still_pointer() {
|
||||
use egui::{Event, Modifiers};
|
||||
let (mut app, ctx, _, over, _) = distance_scene(true);
|
||||
let run = |app: &mut VectorcraftApp, events, cover| distance_frame_covered(app, &ctx, events, cover);
|
||||
run(&mut app, vec![Event::ModifiersChanged(Modifiers::ALT), Event::PointerMoved(over)], None);
|
||||
assert!(gap_label(&run(&mut app, vec![], None)).is_some(), "shown over the other square");
|
||||
// A floating panel, then a modal dialog, opens over the still pointer.
|
||||
for cover in [Cover::Panel(egui::Rect::from_center_size(over, vec2(120.0, 120.0))), Cover::Dialog] {
|
||||
run(&mut app, vec![], Some(cover));
|
||||
run(&mut app, vec![], Some(cover));
|
||||
assert_eq!(gap_label(&run(&mut app, vec![], Some(cover))), None, "gone under it");
|
||||
// A frame for egui to drop the closed window, one for the hover to come back.
|
||||
run(&mut app, vec![], None);
|
||||
run(&mut app, vec![], None);
|
||||
assert!(gap_label(&run(&mut app, vec![], None)).is_some(), "back once it closes");
|
||||
}
|
||||
// Another tool and back to the Selection tool (its shortcut, the toolbar): a new Selection
|
||||
// tool, hovered at once.
|
||||
app.select_tool("rectangle");
|
||||
run(&mut app, vec![], None);
|
||||
assert_eq!(gap_label(&run(&mut app, vec![], None)), None, "the Rectangle tool shows none");
|
||||
app.select_tool("selection");
|
||||
run(&mut app, vec![], None);
|
||||
assert!(gap_label(&run(&mut app, vec![], None)).is_some(), "the Selection tool again");
|
||||
// The view scrolls 150 pt (the wheel, a gesture, the keys): empty artboard is under the
|
||||
// pointer, and the selected square is 300 pt from the artboard's right edge.
|
||||
app.view_mut().unwrap().center.x += 150.0;
|
||||
run(&mut app, vec![], None);
|
||||
let s = run(&mut app, vec![], None);
|
||||
assert_eq!(gap_label(&s), None, "no longer over the other square");
|
||||
assert!(texts(&s).iter().any(|t| t == "300.00 pt"), "to the artboard: {:?}", texts(&s));
|
||||
|
||||
// A square made under the still pointer, the first selected again (an edit, an undo).
|
||||
let (mut app, ctx, _, over, a) = distance_scene(false);
|
||||
let run = |app: &mut VectorcraftApp, events| distance_frame(app, &ctx, events);
|
||||
run(&mut app, vec![Event::ModifiersChanged(Modifiers::ALT), Event::PointerMoved(over)]);
|
||||
let s = run(&mut app, vec![]);
|
||||
assert!(gap_label(&s).is_none() && texts(&s).iter().any(|t| t == "300.00 pt"), "to the artboard: {:?}", texts(&s));
|
||||
app.run("shape.rectangle", json!({"x": 200, "y": 50, "width": 50, "height": 50})).unwrap();
|
||||
app.run("select.set", json!({"ids": [a]})).unwrap();
|
||||
run(&mut app, vec![]);
|
||||
assert!(gap_label(&run(&mut app, vec![])).is_some(), "to the new square");
|
||||
// Another document (a new tab) under the still pointer: nothing selected there, none.
|
||||
app.run("file.new", json!({"width": 400, "height": 300})).unwrap();
|
||||
run(&mut app, vec![]);
|
||||
assert_eq!(gap_label(&run(&mut app, vec![])), None, "another document");
|
||||
}
|
||||
|
||||
/// 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