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:
BFlat
2026-10-24 03:12:28 +07:00
committed by GitHub
36 changed files with 636 additions and 6 deletions
+1
View File
@@ -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),
+2
View File
@@ -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,
+24
View File
@@ -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
+12
View File
@@ -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()
}
+43
View File
@@ -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::*;
+19
View File
@@ -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
View File
@@ -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();
+285 -2
View File
@@ -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 } };
+1
View File
@@ -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.
+1
View File
@@ -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.
+1
View File
@@ -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.
+1
View File
@@ -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.
+1
View File
@@ -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.
+1
View File
@@ -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.
+1
View File
@@ -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.
+1
View File
@@ -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.
+1
View File
@@ -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.
+1
View File
@@ -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
1 # Indonesian (id) catalog. Same format as zh-hant.tsv: context <TAB> source <TAB> translation
3270
3271
3272
3273
3274
3275
3276
+1
View File
@@ -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.
+1
View File
@@ -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.
+1
View File
@@ -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.
+1
View File
@@ -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.
+1
View File
@@ -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.
+1
View File
@@ -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.
+1
View File
@@ -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.
+1
View File
@@ -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.
+1
View File
@@ -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.
+1
View File
@@ -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.
+1
View File
@@ -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.
+1
View File
@@ -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
1 # Vietnamese (vi) catalog. Same format as zh-hant.tsv: context <TAB> source <TAB> translation
3462
3463
3464
3465
3466
3467
3468
+1
View File
@@ -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.
+1
View File
@@ -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
View File
File diff suppressed because one or more lines are too long
+5
View File
@@ -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
View File
File diff suppressed because one or more lines are too long