Measure: both ends land on glyph guides with Snap to Glyph on

The Measure and Measure & Inspect tools ignored View › Snap to Glyph:
their press and end points snapped only to targets that hold no glyph
guides.

With Snap to Glyph, Smart Guides and Alignment Guides on, and Snap to
Grid and Snap to Pixel off, a drag's press and end land within the
snapping tolerance on the guides of live type that the Character
panel's options allow, except Anchor Point. A Shift-drag's end slides
along its 45° step to them, and the guide shows during the drag. With
Snap to Glyph off, the tools snap to the same targets as before and
show no guide. The targets are kept while the document, the window and
the snapping options stay the same, so a drag lays out the type once.
This commit is contained in:
dotCooCoo
2026-11-09 16:59:54 -08:00
parent c7c6c12ca8
commit 28fc436ef3
8 changed files with 377 additions and 18 deletions
+59 -3
View File
@@ -11,6 +11,7 @@ use vectorcraft_doc::{NodeId, Unit};
use vectorcraft_geom::{Point, Vec2};
use vectorcraft_pathops::addons::pathtools::{Inspection, inspect};
use crate::guides::DrawSnap;
use crate::xform::measure;
use crate::{Action, Cursor, Mods, Overlay, PointerEvent, PointerKind, Tool, ToolContext, ToolKey};
@@ -27,6 +28,8 @@ pub struct MeasureInspectTool {
before: Option<(Point, Point)>,
/// The path under the pointer and what it reads there.
hover: Option<(NodeId, Inspection)>,
/// Snapping for both ends of a measurement.
snap: DrawSnap,
}
/// An area of `pt2` square points in `unit` (`12.5 mm²`).
@@ -87,7 +90,7 @@ impl Tool for MeasureInspectTool {
match ev.kind {
PointerKind::Move => self.hover = Self::inspect_at(cx, ev.pos),
PointerKind::Down => {
let (p, _) = crate::guides::snap_draw(cx, ev.pos, &[]);
let p = self.snap.measure(cx, ev.pos, None);
self.before = self.line;
self.line = Some((p, p));
self.dragging = true;
@@ -95,7 +98,7 @@ impl Tool for MeasureInspectTool {
}
PointerKind::Drag | PointerKind::Up if self.dragging => {
if let Some((a, _)) = self.line {
self.line = Some((a, crate::xform::measure_end_point(cx, a, ev.pos, ev.mods.shift)));
self.line = Some((a, crate::xform::measure_end_point(cx, &mut self.snap, a, ev.pos, ev.mods.shift)));
}
self.dragging = ev.kind == PointerKind::Drag;
}
@@ -138,6 +141,7 @@ impl Tool for MeasureInspectTool {
out.push(Overlay::Anchor { p: at, color: ORANGE, filled: i.anchor.is_some(), size: 6.0 });
out.push(Overlay::Measure { p: at + off, text: readout(cx, i) });
}
out.extend(self.snap.guides().iter().filter(|_| self.dragging).cloned());
out
}
@@ -161,7 +165,8 @@ impl Tool for MeasureInspectTool {
mod tests {
use super::*;
use crate::testutil::*;
use vectorcraft_doc::Selection;
use vectorcraft_doc::{CharStyle, Document, Node, NodeKind, Selection, TextObject};
use vectorcraft_text::{FontDb, layout as layout_text};
#[test]
fn a_drag_measures_and_hovering_a_path_reads_it() {
@@ -227,4 +232,55 @@ mod tests {
assert!((o["distance"].as_f64().unwrap() - 36.0).abs() < 1e-9, "on the 9 pt grid: {o}");
assert!(o["dy"].as_f64().unwrap().abs() < 1e-9, "kept horizontal: {o}");
}
/// With Snap to Glyph on, both ends and a Shift-drag's end land on the guides of live type.
#[test]
fn its_ends_land_on_glyph_guides_with_snap_to_glyph() {
let mut d = Document::new(500.0, 500.0);
let (l, id) = (d.layers[0].id, d.alloc_id());
let text = TextObject::point(Point::new(100.0, 300.0), "Hg", CharStyle { size: 60.0, ..Default::default() });
d.insert(Some(l), 0, Node::new(id, NodeKind::Text(Box::new(text.clone())))).unwrap();
let lay = layout_text(FontDb::global(), &text);
let base = (text.xf * lay.line_xf * Point::new(lay.lines[0].x0, lay.lines[0].baseline)).y;
let (s, p) = (Selection::default(), paint());
let on = ToolContext { snap_to_glyph: true, ..cx(&d, &s, &p) };
let run = |c: &ToolContext, from: Point, to: Point, shift: bool| {
let mods = Mods { shift, ..Mods::default() };
let mut t = MeasureInspectTool::default();
t.pointer(c, &PointerEvent::new(PointerKind::Down, from.x, from.y));
t.pointer(c, &PointerEvent::new(PointerKind::Up, to.x, to.y).with_mods(mods));
let o = t.options();
let a = Point::new(o["x"].as_f64().unwrap(), o["y"].as_f64().unwrap());
(a, a + Vec2::new(o["dx"].as_f64().unwrap(), o["dy"].as_f64().unwrap()))
};
let (from, to) = (Point::new(400.0, base + 2.0), Point::new(450.0, base - 40.0));
assert_eq!(run(&on, from, to, false).0, Point::new(400.0, base));
let (from, to) = (Point::new(300.0, base - 100.0), Point::new(401.0, base + 1.0));
let landed = run(&on, from, to, true).1;
assert!(landed.distance(Point::new(400.0, base)) < 1e-9, "{landed:?}");
let off = ToolContext { snap_to_glyph: false, ..on };
let stayed = run(&off, from, to, true).1;
assert!(stayed.distance(to) < 1e-9, "{stayed:?}");
}
/// The guide an end lands on shows while the drag lasts.
#[test]
fn a_drag_shows_the_glyph_guide_it_lands_on() {
let mut d = Document::new(500.0, 500.0);
let (l, id) = (d.layers[0].id, d.alloc_id());
let text = TextObject::point(Point::new(100.0, 300.0), "Hg", CharStyle { size: 60.0, ..Default::default() });
d.insert(Some(l), 0, Node::new(id, NodeKind::Text(Box::new(text.clone())))).unwrap();
let lay = layout_text(FontDb::global(), &text);
let base = (text.xf * lay.line_xf * Point::new(lay.lines[0].x0, lay.lines[0].baseline)).y;
let (s, p) = (Selection::default(), paint());
let on = ToolContext { snap_to_glyph: true, ..cx(&d, &s, &p) };
let mut t = MeasureInspectTool::default();
t.pointer(&on, &PointerEvent::new(PointerKind::Down, 400.0, 50.0));
let (drag, up) = (PointerKind::Drag, PointerKind::Up);
let count = |t: &MeasureInspectTool| t.overlays(&on).iter().filter(|o| !matches!(o, Overlay::Measure { .. })).count();
t.pointer(&on, &PointerEvent::new(drag, 450.0, base + 2.0));
assert!(count(&t) > 1, "the measured line and the guide");
t.pointer(&on, &PointerEvent::new(up, 450.0, base + 2.0));
assert_eq!(count(&t), 1, "the measured line alone");
}
}
+22
View File
@@ -288,6 +288,11 @@ pub fn square(d: Vec2) -> Vec2 {
/// state: hovering gathers them again only after the document, the selection, what is left out,
/// the window or the snapping options changed; a press always gathers them afresh, and the drag
/// it starts keeps them, the document then showing the drag's own previews.
///
/// The Measure and Measure & Inspect tools keep one too, for the ends of a drag only. They do not
/// ask it while hovering. [`Self::measure`] gathers [`Targets::for_measure`] instead, and a press
/// reuses those targets while nothing they depend on changed. It has guides to show only while
/// Snap to Glyph gives guides.
#[derive(Default)]
pub struct DrawSnap {
/// The targets, and what they were gathered for (none: for one drag only).
@@ -348,6 +353,23 @@ impl DrawSnap {
self.snap(cx, p, from)
}
/// Snap an end of a measurement at `p` ([`Targets::for_measure`]), reusing the targets while
/// nothing they depend on changed, so a drag lays out the type once. With Snap to Glyph giving
/// no guides, [`Self::guides`] stays empty.
pub fn measure(&mut self, cx: &ToolContext, p: Point, from: Option<&Leave>) -> Point {
let key = SnapKey::of(cx, &[]);
let glyphs = key.snap_to_glyph.is_some();
if self.targets.as_ref().is_none_or(|(k, _)| k.as_ref() != Some(&key)) {
let snap = PointSnap::new(cx, || Targets::for_measure(cx.doc, visible(cx), key.snap_to_glyph));
self.targets = Some((Some(key), snap));
}
let q = self.snap(cx, p, from);
if !glyphs {
self.guides.clear();
}
q
}
/// Snap the point pressed at `p` onto the art's anchors and paths alone, within the Smart
/// Guides snapping tolerance (the Knife's straight cut). The targets serve this press only.
pub fn press_on_paths(&mut self, cx: &ToolContext, p: Point) -> Point {
+11
View File
@@ -213,6 +213,17 @@ impl Targets {
t.index()
}
/// Targets for the ends of a measurement: [`Self::collect`]'s, and with `glyphs` (Snap to Glyph
/// on) the guides those options ask for from the live text in `visible` (all of it for None),
/// without the outlines' anchor points, which Anchor Point gives the Pen and moved objects.
pub fn for_measure(doc: &Document, visible: Option<Rect>, glyphs: Option<GlyphSnap>) -> Self {
let mut t = Self::collect(doc, &[], None);
if let Some(o) = glyphs {
t.push_glyphs(doc, &[], visible, GlyphSnap { anchors: false, ..o });
}
t.index()
}
/// Targets for a dragged direction handle of path `id`: the other art, and the anchors of `id`
/// (its bounds and the segments through the handle move with it, so they would chase it).
pub fn for_handle(doc: &Document, id: NodeId) -> Self {
+279 -12
View File
@@ -7,6 +7,7 @@ use serde_json::{Value, json};
use vectorcraft_geom::{Point, Vec2};
use super::CYAN;
use crate::guides::DrawSnap;
use crate::{Action, Cursor, Mods, Overlay, PointerEvent, PointerKind, Tool, ToolContext, ToolKey};
#[derive(Default)]
@@ -15,6 +16,8 @@ pub struct MeasureTool {
dragging: bool,
/// The line measured before the drag under way, which Escape puts back.
before: Option<(Point, Point)>,
/// Snapping for both ends, its targets kept while nothing they depend on changes.
snap: DrawSnap,
}
/// (distance, angle in degrees counter-clockwise, dx, dy)
@@ -29,10 +32,10 @@ pub fn measure(a: Point, b: Point) -> (f64, f64, f64, f64) {
/// tool snaps a Shift-drawn end. With Snap to Point alone, the point `p` snaps to (an anchor, or
/// in line with a ruler guide) is projected instead. When the snapped point lies nearer another
/// step (the grid rounds it past the halfway angle) or behind the start, it is turned back onto
/// the pointer's step.
pub(crate) fn end_point(cx: &ToolContext, a: Point, p: Point, shift: bool) -> Point {
/// the pointer's step. With View › Snap to Glyph on, the guides of live type are targets too.
pub(crate) fn end_point(cx: &ToolContext, snap: &mut DrawSnap, a: Point, p: Point, shift: bool) -> Point {
if !shift {
return crate::guides::snap_draw(cx, p, &[]).0;
return snap.measure(cx, p, None);
}
let d = vectorcraft_geom::constrain_angle_from(p - a, 45.0, cx.constrain_angle);
let len = d.hypot();
@@ -41,10 +44,10 @@ pub(crate) fn end_point(cx: &ToolContext, a: Point, p: Point, shift: bool) -> Po
}
let u = d / len;
let to_point = cx.snap_to_point && !(cx.smart_guides || cx.snap_to_grid || cx.snap_to_pixel);
let s = if to_point { crate::guides::snap_draw(cx, p, &[]).0 } else { p };
let s = if to_point { snap.measure(cx, p, None) } else { p };
let q = a + u * (s - a).dot(u);
let leave = crate::guides::Leave::segment(cx, a, true);
let r = crate::guides::PointSnap::new(cx, || crate::guides::Targets::collect(cx.doc, &[], None)).snap_from(cx, q, Some(&leave)).0;
let r = snap.measure(cx, q, Some(&leave));
let v = r - a;
if v.cross(u).abs() <= 1e-9 * v.hypot().max(1.0) && v.dot(u) >= 0.0 { r } else { a + u * v.hypot() }
}
@@ -61,14 +64,14 @@ impl Tool for MeasureTool {
fn pointer(&mut self, cx: &ToolContext, ev: &PointerEvent) -> Vec<Action> {
match ev.kind {
PointerKind::Down => {
let (p, _) = crate::guides::snap_draw(cx, ev.pos, &[]);
let p = self.snap.measure(cx, ev.pos, None);
self.before = self.line;
self.line = Some((p, p));
self.dragging = true;
}
PointerKind::Drag | PointerKind::Up if self.dragging => {
if let Some((a, _)) = self.line {
self.line = Some((a, end_point(cx, a, ev.pos, ev.mods.shift)));
self.line = Some((a, end_point(cx, &mut self.snap, a, ev.pos, ev.mods.shift)));
}
self.dragging = ev.kind == PointerKind::Drag;
}
@@ -86,10 +89,8 @@ impl Tool for MeasureTool {
}
fn overlays(&self, cx: &ToolContext) -> Vec<Overlay> {
let Some((a, b)) = self.line else { return vec![] };
if a == b {
return vec![];
}
let guides = self.snap.guides().iter().filter(|_| self.dragging).cloned();
let Some((a, b)) = self.line.filter(|(a, b)| a != b) else { return guides.collect() };
let mut out = vec![Overlay::Line { a, b, color: CYAN, dashed: false }];
// Measurement Labels decides the readout; the line the tool draws stays.
if cx.measurement_labels {
@@ -99,6 +100,7 @@ impl Tool for MeasureTool {
text: format!("D: {}\nAngle: {ang:.1}°\n{}", cx.len(dist), cx.offset_label(dx, dy)),
});
}
out.extend(guides);
out
}
@@ -121,7 +123,11 @@ impl Tool for MeasureTool {
mod tests {
use super::*;
use crate::testutil::*;
use vectorcraft_doc::Selection;
use crate::{GlyphSnap, ScreenFrame};
use vectorcraft_doc::{CharStyle, Document, Node, NodeKind, Selection, TextObject};
use vectorcraft_geom::kurbo::{ParamCurve, Shape};
use vectorcraft_geom::{Affine, Rect};
use vectorcraft_text::{FontDb, layout as layout_text};
#[test]
fn horizontal_drag_reports_positive_zero_angle() {
@@ -324,4 +330,265 @@ mod tests {
assert_eq!(t.options(), first);
assert!((t.options()["distance"].as_f64().unwrap() - 50.0).abs() < 1e-9);
}
/// A document holding point type `text` at (100, 300), 60 pt, turned `angle` radians about
/// where it starts, and the type.
fn type_doc(text: &str, angle: f64, visible: bool, locked: bool) -> (Document, TextObject) {
let mut d = Document::new(500.0, 500.0);
let (l, id) = (d.layers[0].id, d.alloc_id());
let at = Point::new(100.0, 300.0);
let mut t = TextObject::point(at, text, CharStyle { size: 60.0, ..Default::default() });
t.xf = Affine::rotate_about(angle, at) * t.xf;
let mut n = Node::new(id, NodeKind::Text(Box::new(t.clone())));
(n.visible, n.locked) = (visible, locked);
d.insert(Some(l), 0, n).unwrap();
(d, t)
}
/// Where the baseline of `t`'s first line starts on the page, and its unit direction.
fn baseline(t: &TextObject) -> (Point, Vec2) {
let lay = layout_text(FontDb::global(), t);
let (m, line) = (t.xf * lay.line_xf, &lay.lines[0]);
let (at, along) = (m * Point::new(line.x0, line.baseline), m * Point::new(line.x0 + 1.0, line.baseline));
(at, (along - at) / (along - at).hypot())
}
/// The y of the x-height of `t`'s first glyph, in type that isn't turned.
fn x_height(t: &TextObject) -> f64 {
let lay = layout_text(FontDb::global(), t);
let g = &lay.glyphs[0];
(t.xf * g.xf * Point::new(0.0, -FontDb::global().face_by_id(g.font_id).unwrap().x_height())).y
}
/// The bounds on the page of glyph `i` of `t`, which isn't turned.
fn glyph_bounds(t: &TextObject, i: usize) -> Rect {
t.xf.transform_rect_bbox(layout_text(FontDb::global(), t).glyphs[i].outline.bounding_box())
}
/// Where a measurement dragged from `from` to `to` starts and ends.
fn measured(tool: &mut dyn Tool, cx: &ToolContext, from: Point, to: Point, shift: bool) -> (Point, Point) {
let mods = Mods { shift, ..Mods::default() };
tool.pointer(cx, &PointerEvent::new(PointerKind::Down, from.x, from.y));
tool.pointer(cx, &PointerEvent::new(PointerKind::Drag, to.x, to.y).with_mods(mods));
tool.pointer(cx, &PointerEvent::new(PointerKind::Up, to.x, to.y).with_mods(mods));
let o = tool.options();
let a = Point::new(o["x"].as_f64().unwrap(), o["y"].as_f64().unwrap());
(a, a + Vec2::new(o["dx"].as_f64().unwrap(), o["dy"].as_f64().unwrap()))
}
/// Snap to Glyph: both ends land on a line of type's baseline and x-height and on its glyphs'
/// bounds, each only while its option is on. With Snap to Glyph off they stay at the pointer.
#[test]
fn its_ends_land_on_glyph_guides_with_snap_to_glyph() {
let (d, t) = type_doc("Hg", 0.0, true, false);
let (s, p) = (Selection::default(), paint());
let on = ToolContext { snap_to_glyph: true, ..cx(&d, &s, &p) };
let (base, x) = (baseline(&t).0.y, x_height(&t));
let (from, to) = (Point::new(400.0, base + 2.0), Point::new(450.0, x + 2.0));
let (a, b) = measured(&mut MeasureTool::default(), &on, from, to, false);
assert_eq!(a, Point::new(400.0, base), "the press lands on the baseline");
assert_eq!(b, Point::new(450.0, x), "the end lands on the x-height");
let off = ToolContext { snap_to_glyph: false, ..on };
assert_eq!(measured(&mut MeasureTool::default(), &off, from, to, false), (from, to));
// The Character panel's options: without Baseline and Glyph Bounds (the bottom of `H` is
// on the baseline) the press stays, and without x-height the end does.
let o = GlyphSnap { baseline: false, bounds: false, x_height: false, ..GlyphSnap::default() };
let without = ToolContext { glyph_snap: o, ..on };
assert_eq!(measured(&mut MeasureTool::default(), &without, from, to, false), (from, to));
// A corner of the box of `H`, which only Glyph Bounds gives there.
let corner = Point::new(glyph_bounds(&t, 0).x1, glyph_bounds(&t, 0).y0);
let near = corner + Vec2::new(1.5, -1.0);
let bounds = ToolContext { glyph_snap: GlyphSnap { anchors: false, ..GlyphSnap::default() }, ..on };
let landed = measured(&mut MeasureTool::default(), &bounds, Point::new(400.0, 50.0), near, false).1;
assert!(landed.distance(corner) < 1e-9, "{landed:?} vs {corner:?}");
let plain = measured(&mut MeasureTool::default(), &off, Point::new(400.0, 50.0), near, false).1;
assert!((plain.x - corner.x).abs() > 1e-3, "{plain:?}");
}
/// A Shift-drag's end stays on its 45° step and slides along it to where it meets a glyph guide.
#[test]
fn a_shift_drag_slides_along_its_step_to_a_glyph_guide() {
let (d, t) = type_doc("Hg", 0.0, true, false);
let (s, p) = (Selection::default(), paint());
let on = ToolContext { snap_to_glyph: true, ..cx(&d, &s, &p) };
let x = x_height(&t);
let (from, to) = (Point::new(300.0, x - 100.0), Point::new(401.0, x + 1.0));
let landed = measured(&mut MeasureTool::default(), &on, from, to, true).1;
assert!(landed.distance(Point::new(400.0, x)) < 1e-9, "{landed:?}");
let off = ToolContext { snap_to_glyph: false, ..on };
let stayed = measured(&mut MeasureTool::default(), &off, from, to, true).1;
assert!(stayed.distance(to) < 1e-9, "{stayed:?}");
}
/// Proximity Guides pull an end, with every other option off. Anchor Point does not.
#[test]
fn proximity_guides_pull_an_end_and_a_glyphs_anchor_points_do_not() {
let (d, t) = type_doc("HHHH", 0.0, true, false);
let (s, p) = (Selection::default(), paint());
let none = GlyphSnap { baseline: false, x_height: false, bounds: false, proximity: false, angular: false, anchors: false };
let on = ToolContext { snap_to_glyph: true, ..cx(&d, &s, &p) };
let off = ToolContext { snap_to_glyph: false, ..on };
let start = Point::new(400.0, 50.0);
let end = |c: &ToolContext, to: Point| measured(&mut MeasureTool::default(), c, start, to, false).1;
let lay = layout_text(FontDb::global(), &t);
let edge = (t.xf * lay.glyphs[1].origin).x;
let to = Point::new(edge + 2.0, 100.0);
assert_eq!(end(&ToolContext { glyph_snap: GlyphSnap { proximity: true, ..none }, ..on }, to).x, edge);
assert_eq!(end(&off, to).x, to.x);
let anchor = t.xf * lay.glyphs[1].outline.segments().next().unwrap().start();
let to = anchor + Vec2::new(1.5, -1.0);
assert_eq!(end(&ToolContext { glyph_snap: GlyphSnap { anchors: true, ..none }, ..on }, to), end(&off, to));
}
/// While a drag snaps with Snap to Glyph on, the guide shows; the drag's end hides it, and with
/// Snap to Glyph off there is only the measured line and its readout.
#[test]
fn a_drag_shows_the_glyph_guide_it_lands_on() {
let (d, t) = type_doc("Hg", 0.0, true, false);
let (s, p) = (Selection::default(), paint());
let on = ToolContext { snap_to_glyph: true, ..cx(&d, &s, &p) };
let to = Point::new(450.0, baseline(&t).0.y + 2.0);
let guides = |c: &ToolContext, to: Point, up: bool| {
let mut tool = MeasureTool::default();
tool.pointer(c, &PointerEvent::new(PointerKind::Down, 400.0, 50.0));
tool.pointer(c, &PointerEvent::new(if up { PointerKind::Up } else { PointerKind::Drag }, to.x, to.y));
tool.overlays(c).iter().filter(|o| !matches!(o, Overlay::Measure { .. })).count() - 1
};
assert!(guides(&on, to, false) > 0, "the guide shows during the drag");
assert_eq!(guides(&on, to, true), 0, "the guide goes when the drag ends");
// Off, an end still lines up with a rectangle's top edge, as it always has, with no guide.
let (rect, _) = doc_with_rect();
let off = ToolContext { snap_to_glyph: false, ..cx(&rect, &s, &p) };
let near = Point::new(450.0, 101.0);
assert_eq!(measured(&mut MeasureTool::default(), &off, Point::new(400.0, 50.0), near, false).1.y, 100.0);
assert_eq!(guides(&off, near, false), 0, "no guide with Snap to Glyph off");
}
/// In rotated type the guides run at its angle: the end lands on the baseline, only with
/// Angular Guides on.
#[test]
fn its_ends_land_on_the_guides_of_rotated_type() {
let (d, t) = type_doc("H", 30f64.to_radians(), true, false);
let (s, p) = (Selection::default(), paint());
let on = ToolContext { snap_to_glyph: true, ..cx(&d, &s, &p) };
let (at, dir) = baseline(&t);
let to = at + dir * 250.0 + Vec2::new(-dir.y, dir.x) * 2.0;
let off_line = |c: &ToolContext| (measured(&mut MeasureTool::default(), c, Point::new(20.0, 20.0), to, false).1 - at).cross(dir).abs();
assert!(off_line(&on) < 1e-6, "{}", off_line(&on));
let flat = ToolContext { glyph_snap: GlyphSnap { angular: false, ..GlyphSnap::default() }, ..on };
assert!(off_line(&flat) > 1.0, "{}", off_line(&flat));
}
/// A paragraph gives glyph bounds only: no x-height, and the box of a glyph on a later line.
#[test]
fn a_paragraph_gives_the_measure_tool_glyph_bounds_only() {
let (d, t) = type_doc("H\nH", 0.0, true, false);
let (s, p) = (Selection::default(), paint());
let on = ToolContext { snap_to_glyph: true, ..cx(&d, &s, &p) };
let start = Point::new(400.0, 50.0);
let to = Point::new(450.0, x_height(&t) + 2.0);
assert_eq!(measured(&mut MeasureTool::default(), &on, start, to, false).1, to, "no x-height guide");
let lay = layout_text(FontDb::global(), &t);
assert_eq!(lay.lines.len(), 2);
let top = glyph_bounds(&t, lay.glyphs.len() - 1).y0;
assert_eq!(measured(&mut MeasureTool::default(), &on, start, Point::new(450.0, top - 2.0), false).1, Point::new(450.0, top));
}
/// Hidden type gives no guides. Locked type does, as it does for the drawing tools.
#[test]
fn hidden_type_gives_no_glyph_guides_and_locked_type_does() {
let (s, p) = (Selection::default(), paint());
let drag = |d: &Document, base: f64| {
let c = ToolContext { snap_to_glyph: true, ..cx(d, &s, &p) };
measured(&mut MeasureTool::default(), &c, Point::new(400.0, 50.0), Point::new(450.0, base + 2.0), false).1
};
let (hidden, t) = type_doc("Hg", 0.0, false, false);
let base = baseline(&t).0.y;
assert_eq!(drag(&hidden, base).y, base + 2.0);
let (locked, _) = type_doc("Hg", 0.0, true, true);
assert_eq!(drag(&locked, base).y, base);
}
/// Grid and pixel snapping come first; Snap to Glyph needs Smart Guides and Alignment Guides.
#[test]
fn glyph_guides_keep_their_place_among_the_other_snaps() {
let (d, t) = type_doc("Hg", 0.0, true, false);
let (s, p) = (Selection::default(), paint());
let on = ToolContext { snap_to_glyph: true, ..cx(&d, &s, &p) };
let base = baseline(&t).0.y;
let (start, to) = (Point::new(400.0, 50.0), Point::new(450.0, base + 2.0));
let end = |c: &ToolContext| measured(&mut MeasureTool::default(), c, start, to, false).1;
assert_eq!(end(&on), Point::new(to.x, base));
let grid = ToolContext { snap_to_grid: true, ..on };
assert_eq!(end(&grid), vectorcraft_geom::snap::snap_point_to_grid(to, grid.grid_step()));
assert_eq!(end(&ToolContext { snap_to_pixel: true, ..on }), Point::new(to.x.round(), to.y.round()));
assert_eq!(end(&ToolContext { smart_guides: false, snap_to_point: true, ..on }), to);
assert_eq!(end(&ToolContext { alignment_guides: false, ..on }), to);
}
/// The snapping distance is in screen pixels, so a zoomed-out view reaches farther; type
/// outside the window gives no guides.
#[test]
fn glyph_guides_follow_the_view() {
let (d, t) = type_doc("Hg", 0.0, true, false);
let (s, p) = (Selection::default(), paint());
let on = ToolContext { snap_to_glyph: true, ..cx(&d, &s, &p) };
let base = baseline(&t).0.y;
let (start, to) = (Point::new(400.0, 50.0), Point::new(450.0, base - 5.0));
let end = |c: &ToolContext| measured(&mut MeasureTool::default(), c, start, to, false).1;
assert_eq!(end(&on), to, "5 pt is out of reach at 100%");
assert_eq!(end(&ToolContext { zoom: 0.25, ..on }), Point::new(to.x, base));
let frame = |origin: Point| ScreenFrame { origin, right: Vec2::new(1.0, 0.0), down: Vec2::new(0.0, 1.0), size: (800.0, 600.0) };
let near = Point::new(to.x, base + 2.0);
let end = |c: &ToolContext| measured(&mut MeasureTool::default(), c, start, near, false).1;
assert_eq!(end(&ToolContext { screen: Some(frame(Point::ZERO)), ..on }), Point::new(to.x, base));
assert_eq!(end(&ToolContext { screen: Some(frame(Point::new(0.0, 1000.0))), ..on }), near);
}
/// A drag lays out the type once: it keeps the guides its press gathered until the document
/// or a snapping option changes.
#[test]
fn a_drag_keeps_the_glyph_guides_its_press_gathered() {
let (d, t) = type_doc("Hg", 0.0, true, false);
let (empty, s, p) = (Document::new(500.0, 500.0), Selection::default(), paint());
let on = ToolContext { snap_to_glyph: true, ..cx(&d, &s, &p) };
let base = baseline(&t).0.y;
let to = Point::new(450.0, base + 2.0);
let end = |tool: &MeasureTool| {
let o = tool.options();
o["y"].as_f64().unwrap() + o["dy"].as_f64().unwrap()
};
let mut tool = MeasureTool::default();
tool.pointer(&on, &PointerEvent::new(PointerKind::Down, 400.0, 50.0));
// The same revision: the guides of the press still serve, though this document has no type.
let later = ToolContext { snap_to_glyph: true, ..cx(&empty, &s, &p) };
tool.pointer(&later, &PointerEvent::new(PointerKind::Drag, to.x, to.y));
assert_eq!(end(&tool), base);
// A new revision gathers them again.
let edited = ToolContext { revision: (1, 0), ..later };
tool.pointer(&edited, &PointerEvent::new(PointerKind::Drag, to.x, to.y));
assert_eq!(end(&tool), to.y);
// So does turning Snap to Glyph off during the drag.
let mut tool = MeasureTool::default();
tool.pointer(&on, &PointerEvent::new(PointerKind::Down, 400.0, 50.0));
tool.pointer(&ToolContext { snap_to_glyph: false, ..on }, &PointerEvent::new(PointerKind::Drag, to.x, to.y));
assert_eq!(end(&tool), to.y);
}
/// Escape brings back the last result, and the next drag still lands on the guides.
#[test]
fn escape_leaves_the_glyph_guides_for_the_next_drag() {
let (d, t) = type_doc("Hg", 0.0, true, false);
let (s, p) = (Selection::default(), paint());
let on = ToolContext { snap_to_glyph: true, ..cx(&d, &s, &p) };
let base = baseline(&t).0.y;
let mut tool = MeasureTool::default();
let to = Point::new(450.0, base + 2.0);
let first = measured(&mut tool, &on, Point::new(400.0, 50.0), to, false);
tool.pointer(&on, &PointerEvent::new(PointerKind::Down, 300.0, 60.0));
tool.pointer(&on, &PointerEvent::new(PointerKind::Drag, 320.0, base + 2.0));
tool.key(&on, ToolKey::Escape, Mods::default());
assert!((tool.options()["y"].as_f64().unwrap() - first.0.y).abs() < 1e-9);
assert_eq!(measured(&mut tool, &on, Point::new(300.0, 60.0), to, false).1, Point::new(450.0, base));
}
}
+3 -1
View File
@@ -7680,7 +7680,9 @@ Tools:
`bezier` or `arc`; E cycles it. Shift steps the length by the keyboard increment.
- **Measure & Inspect tool** (`measureInspect`): a drag measures distance, angle and offsets, as the Measure tool does
(`tool.options` reports the last one); hovering a path reads it, as `addon.pathtools.inspect` does. Nothing goes into
the document.
the document. With Snap to Glyph, Smart Guides and Alignment Guides on, and Snap to Grid and Snap to Pixel off, both ends
of either tool's drag land within the snapping tolerance on the guides of live type, except Anchor Point, and the guide
shows during the drag.
- **Parametric Shape Tool** (`parametricShape`): draws shapes that stay editable as numbers. A drag from the centre runs
`addon.pathtools.shape.create`: the drag's length is the shape's size (height 2 × the distance, width in the last
shape's proportions), its direction the rotation (Shift: 45° steps). While dragging, ↑/↓ change the kind's count or
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -53,7 +53,7 @@ against Illustrator's public documentation; **estimated** unless noted.
| Caret: the arrows and Delete step over whole clusters (Thai consonant with its marks, combining accents, emoji); Backspace removes one character | done | in text with right-to-left characters the arrows move in visual order and stop on both sides of a character drawn with no width, such as a zero width non-joiner |
| Touch Type tool | done | moves, scales and rotates one character through its kerning, baseline shift, scales and rotation (`text.touchType`); a character with a preceding logical character on the same line can move along the line in either reading direction; the first logical character moves only up and down. Positive `dx` follows reading order. These direction rules are VectorCraft's choices. |
| Retype (identify fonts in outlines/images) | missing | Illustrator's uses a cloud model |
| Snap to Glyph | done | View › Snap to Glyph (`view.snapToGlyph`): with Smart Guides on, new point type and drawing tools snap to live-text baselines and glyph bounds, the corners of each glyph's box included, turning with rotated type (#5112; `guides::tests::a_drawn_point_lands_on_a_corner_of_a_glyphs_bounds`, `Targets::for_drawing`, `tools::text_tests::snap_to_glyph_lines_up_new_point_type_with_a_baseline`). Character panel menu › Show Snap to Glyph Options: Baseline, x-height, Glyph Bounds, Proximity Guides, Angular Guides and Anchor Point each turn their guides on or off (`view.snapToGlyph.options`, all on by default; #4232), after a Snap to Glyph checkbox for View › Snap to Glyph; showing them scrolls the panel to them (#5273) |
| Snap to Glyph | done | View › Snap to Glyph (`view.snapToGlyph`): with Smart Guides on, new point type and drawing tools snap to live-text baselines and glyph bounds, the corners of each glyph's box included, turning with rotated type (#5112; `guides::tests::a_drawn_point_lands_on_a_corner_of_a_glyphs_bounds`, `Targets::for_drawing`, `tools::text_tests::snap_to_glyph_lines_up_new_point_type_with_a_baseline`). With Alignment Guides on as well, and Snap to Grid and Snap to Pixel off, both ends of a Measure or Measure & Inspect drag snap to those guides and show them during the drag (`Targets::for_measure`, `xform::measure::tests::its_ends_land_on_glyph_guides_with_snap_to_glyph`). That the Measure tools take the guides is VectorCraft's choice; they leave out Anchor Point, which [the help page](https://helpx.adobe.com/illustrator/desktop/design-with-text/special-characters-glyphs/snap-to-glyph.html) limits to the Pen tool and objects moved with the Selection tool. Character panel menu › Show Snap to Glyph Options: Baseline, x-height, Glyph Bounds, Proximity Guides, Angular Guides and Anchor Point each turn their guides on or off (`view.snapToGlyph.options`, all on by default; #4232), after a Snap to Glyph checkbox for View › Snap to Glyph; showing them scrolls the panel to them (#5273) |
| Thai, Lao, Khmer, Myanmar: shaping (marks, reordering), lines broken between words, word-by-word caret and double-click | done | words found by ICU4X's line segmenter: dictionaries on the desktop, LSTM models on the web |
| Middle Eastern: bidirectional layout, Paragraph Direction | done | PDF export keeps harakat and zero-advance Noto Arabic dot glyphs in one compatible batch per layout line, with per-glyph vertical offsets (#6309). `crates/pdf/src/tests/realtext.rs` (`mark_text_*`) checks decoded cluster mappings, mixed Arabic/Latin/digits, contour placement against outline export, vertical columns, differing path rotations and PDF/X-1a/3/4. Editable-text import still needs ActualText support (#6165); separately styled or differently rotated clusters and external viewer search/copy remain outside this export-only evidence. |
| Middle Eastern and Indic: digit types (Character panel menu › Digits: Default, Arabic, Hindi, Farsi; Devanagari, Bengali, Gurmukhi, Gujarati, Odia, Tamil, Telugu, Kannada, Malayalam) | done | `text.setFormat` / `text.setRangeStyle {digits}` (`"devanagari"`, `"bengali"`, `"gurmukhi"`, `"gujarati"`, `"odia"`, `"tamil"`, `"telugu"`, `"kannada"`, `"malayalam"` added, #4942); the text keeps the typed digits, the glyphs and SVG export follow the style; carried by character styles. Not yet: `.ai` export of the setting; Indic digits typed as such stay as typed under another system. Evidence: `text::tests::indic_digits_map_to_their_scripts_block`, `tests::bidi::indic_digits_draw_the_scripts_own_digits`, `typecmd::area_tests::indic_digits_round_trip_through_the_commands_and_the_file` |
+1 -1
View File
@@ -222,7 +222,7 @@ tools, never checked side by side) isn't captured row by row.
| Distance Guides: Alt/Option over another object, or an empty part of an artboard, shows the selection's distances to it (Smart Guides › Invoke Distance Guides) | done | Selection tool hover with Smart Guides on: the gap between facing edges, else the offsets of matching edges, to another object; the left, right, top and bottom distances to the artboard; labelled in the General unit. The selection is measured as a whole. Nothing while moving or nudging, which the public pages don't describe. [Use Distance Guides](https://helpx.adobe.com/illustrator/desktop/measure-and-align/grids-and-guides/use-distance-guides-for-accurate-placement.html), [Smart Guides options](https://helpx.adobe.com/illustrator/desktop/measure-and-align/grids-and-guides/smart-guides-options.html) |
| Snapping tolerance preference | done | |
| Snap to Point, Snap to Grid, Snap to Pixel; Pixel Preview | done | #1840: with Snap to Grid on, anchors and objects snap to the grid even when Snap to Pixel is also ticked (#1816 waits for the reporter); #3835: a straight segment dragged with Direct Selection lands its anchors on the grid, the box round them putting an edge on a gridline on each axis, as a moved object does |
| Snap to Glyph | done | View › Snap to Glyph (`view.snapToGlyph`); new type and drawing snap to live-text baselines and glyph bounds when Smart Guides are on (#3124). Character panel menu › Show Snap to Glyph Options: a Snap to Glyph checkbox (View › Snap to Glyph), then a checkbox for each guide (Baseline, x-height, Glyph Bounds, Proximity Guides, Angular Guides, Anchor Point), dimmed while Snap to Glyph is off (#4232); showing them scrolls the panel to them (#5273) |
| Snap to Glyph | done | View › Snap to Glyph (`view.snapToGlyph`); new type and drawing snap to live-text baselines and glyph bounds when Smart Guides are on (#3124). With Alignment Guides on as well, and Snap to Grid and Snap to Pixel off, both ends of a Measure or Measure & Inspect drag snap to them too; that is VectorCraft's choice. Character panel menu › Show Snap to Glyph Options: a Snap to Glyph checkbox (View › Snap to Glyph), then a checkbox for each guide (Baseline, x-height, Glyph Bounds, Proximity Guides, Angular Guides, Anchor Point), dimmed while Snap to Glyph is off (#4232); showing them scrolls the panel to them (#5273) |
| Tangent and perpendicular snapping for line endpoints (new in 30.0) | done | #2530: tangent snapping to curves and perpendicular snapping to straight segments for lines drawn with the Line Segment, Pen and Curvature tools, and for an existing line whose end anchor is dragged with Direct Selection (the only anchor selected, an open path's end on a straight segment; Shift keeps its angle step instead; Group Selection moves the path whole and does not snap so); mid-line anchors, curved segments' ends and curves as perpendicular targets are excluded, as the pages say. Saved Preferences › Smart Guides › Geometric Guides controls, on by default. [Perpendicular](https://helpx.adobe.com/illustrator/desktop/measure-and-align/plot-and-measure/snap-to-perpendicular.html) and [tangent](https://helpx.adobe.com/illustrator/desktop/measure-and-align/plot-and-measure/snap-to-tangent.html) pages; test `dragged_line_ends_snap_perpendicular_to_segments_and_tangent_to_curves` |
| Snapping panel/popover consolidating the snap options (30.0) | partial | Control bar › Snapping provides quick access through existing view commands and prefs.set, including Snap to Glyph and Alignment Guides (#6164); no View › Snapping submenu. Floating-panel parity remains unverified. |
| Customisable rotation snap angle (#973) | missing | Illustrator: Constrain Angle and construction angles only |