From 28fc436ef384e8c32a7d39cfdfad9fa73256d2f7 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Mon, 9 Nov 2026 16:45:04 -0800 Subject: [PATCH] Measure: both ends land on glyph guides with Snap to Glyph on MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- crates/tools/src/addons/measure_inspect.rs | 62 ++++- crates/tools/src/guides/snap.rs | 22 ++ crates/tools/src/guides/targets.rs | 11 + crates/tools/src/xform/measure.rs | 291 ++++++++++++++++++++- docs/mcp.md | 4 +- docs/target-app-parity.md | 1 + docs/type-parity.md | 2 +- docs/ui-parity.md | 2 +- 8 files changed, 377 insertions(+), 18 deletions(-) diff --git a/crates/tools/src/addons/measure_inspect.rs b/crates/tools/src/addons/measure_inspect.rs index 1ea8a9324..b7cb13a6e 100644 --- a/crates/tools/src/addons/measure_inspect.rs +++ b/crates/tools/src/addons/measure_inspect.rs @@ -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"); + } } diff --git a/crates/tools/src/guides/snap.rs b/crates/tools/src/guides/snap.rs index 0b117b95b..9d530f4db 100644 --- a/crates/tools/src/guides/snap.rs +++ b/crates/tools/src/guides/snap.rs @@ -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 { diff --git a/crates/tools/src/guides/targets.rs b/crates/tools/src/guides/targets.rs index 17b24ec16..e4039e6d9 100644 --- a/crates/tools/src/guides/targets.rs +++ b/crates/tools/src/guides/targets.rs @@ -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, glyphs: Option) -> 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 { diff --git a/crates/tools/src/xform/measure.rs b/crates/tools/src/xform/measure.rs index 62b207180..e3a009056 100644 --- a/crates/tools/src/xform/measure.rs +++ b/crates/tools/src/xform/measure.rs @@ -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 { 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 { - 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)); + } } diff --git a/docs/mcp.md b/docs/mcp.md index b23b9ed2a..990c26ea6 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -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 diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 9ba9a4036..641c84a3b 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -789,6 +789,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - Save / Don't Save / Cancel before closing or quitting with unsaved documents (tabs, Close, Close All, Quit, window close). - Four brightness themes, persistent preferences. - **Snapping quick access (#6164):** Control bar › Snapping includes Snap to Glyph (`view.snapToGlyph`) and Alignment Guides (`prefs.set`, `alignmentGuides`). The headless pointer/accessibility test `the_control_bar_snapping_menu_shares_glyph_and_alignment_settings` covers both commands, independent state, checked states after reopening, and all eight menu rows within a 1200 × 400-point window. Floating-panel parity remains unverified; scores and estimates unchanged. + - **Measure tools and Snap to Glyph:** with Snap to Glyph, Smart Guides and Alignment Guides on, and Snap to Grid and Snap to Pixel off, both ends of a Measure or Measure & Inspect drag land within the snapping tolerance on the guides of live type that the Character panel's options allow, except Anchor Point, and a Shift-drag's end slides along its 45° step to them. The guide shows during the drag. This is VectorCraft's own rule (`Targets::for_measure`, `xform::measure::tests::its_ends_land_on_glyph_guides_with_snap_to_glyph`); scores and estimates unchanged. - Preferences that act (#394, in part): Show Tool Tips, Double Click To Isolate, Zoom with Mouse Wheel (the wheel zooms about the pointer, Shift-wheel scrolls up and down, Cmd/Ctrl-wheel sideways; Alt/Option-wheel and a trackpad pinch zoom about the pointer either way), Use Precise Cursors; Selection & Anchor Display › Tolerance (objects, anchors and handles), Object Selection by Path Only, Command Click to Select Objects Behind (cycling down the stack), Zoom to Selection, anchor Size, Highlight anchors on mouse over; Type › Number of Recent Fonts (up to 15); Guides & Grid › guide and grid Color and Style (lines or dots) and Grids In Back. Then Snap to Point (its distance, for moves, drawn points and reference points), Move Locked and Hidden Artwork with Artboard, Enable Rubber Band for the Pen and Curvature tools, Handles style, Show handles when multiple anchors are selected, Hide Corner Widget for angles greater than; General › Transform Pattern Tiles (the transforms' and dialogs' Transform Patterns, Align and Distribute, art moved or duplicated with an artboard, and objects moved whole with direct-selected anchors) and Select Same Tint %; Type › Size/Leading, Tracking and Baseline Shift increments (`type.step`, Cmd+Shift+. and Cmd+Shift+, for the size, the Type tool's Alt+arrows), Enable Missing Glyph Protection and Fill New Type Objects With Placeholder Text. Then Smart Guides › Color, Alignment Guides, Anchor/Path Labels, Measurement Labels, Transform Tools, Object Highlighting (with Smart Guides on) and Snapping Tolerance (4 px by default, for every smart guide snap), then Construction Guides and their Angles (#506: a drawn point near a line at one of those angles through the point it leaves from lands on it). Then Type › Type Object Selection by Path Only (type is picked on the baseline of any of its lines, area type's frame or the path of type on a path, no longer anywhere in its bounds; the Type tools and the Eyedropper still take a click among the characters). Then General › Anti-aliased Artwork (off, the canvas draws the art with hard edges). Then Guides & Grid › Show Pixel Grid (Above 600% Zoom), a line at every document pixel over the art in Pixel Preview, turning with a rotated view as the document grid now does too. Then General › Show The Home Screen When No Documents Are Open (off, closing the last document leaves an empty window; the Home button still opens the screen). Then Units › Numbers Without Units Are Points (on by default: a number typed with no unit into a field in picas is read in points, a typed unit still wins; dimmed unless a unit is Picas). Then User Interface › Auto-Collapse Iconic Panels (off by default: on, a click away from a panel popped out of the icon column puts it away). Then Performance › Animated Zoom (on, with GPU Performance, which it needs and which dims it when off: the Zoom tool dragged sideways zooms about the press, right in and left out, and held still zooms on, Alt out; off, a drag zooms to the area dragged across, now drawn as a marquee), User Interface › Large Tabs (taller document tabs) and Scale Cursor Proportional to UI (off, the tool cursors keep their size whatever the UI Scaling), and File Handling › Display Bitmaps as Anti-aliased Images in Pixel Preview (off by default: Pixel Preview draws images with each pixel taking its nearest image pixel). Type › Auto Size New Area Type works too (new area type gets Auto Size). They reach agents too (`prefs.set`, then `pointer_gesture` or `press_key`; `ui.wheel` on the control channel). Then Smart Guides › Spacing Guides (moves space the selection evenly with its row or column, see Drawing aids). Then Hyphenation › Exceptions (comma-separated words that never break, or break only at their hyphens; `prefs.set` and restore push them into the hyphenator, #394). Then Type › Show Font Names in English (off, font menus label a family by its native-language name when the font has one; documents still use the English family, #394). Then General › Display Print Size at 100% Zoom (off by default: View › Actual Size is one document point per screen point; on, one document inch fills 96 screen points, an inch at the reference density display scaling is set against; `view.actualSize` / `prefs.set`, #394). Then Guides & Grid › Gridline every and Subdivisions (changing either sets the grid of every open document; new and opened documents take that grid too, except a native file and the native document an SVG, PDF, `.ai` or EPS file carries, which open with the grid they were saved with; `file.new`, `document.open`, `file.newFromTemplate`, `file.revert`, #394). Then Appearance of Black › On Screen (Display All Blacks Accurately by default: pure CMYK black shows through the CMYK profile, a dark grey; Display All Blacks as Rich Black shows it on the canvas as RGB black, also in CMYK documents drawn by ink planes, while rich blacks, black tints and images show as before; `RenderOptions::rich_black`, #394). Then Appearance of Black › Printing / Exporting (Output All Blacks Accurately by default; Output All Blacks as Rich Black draws pure CMYK black as RGB black in RGB raster exports and Print as Bitmap; CMYK ink exports and the document stay unchanged; `prefs.blackOutput` / `richBlack`, #394). Then Units › Identify Objects By (Object Name by default: Layers, Variables and Layer Options show and edit free names; XML ID shows and edits AI-style XML ids, `_` for a space and `_xHH_` escapes, and `layer.setProps` / rename refuse invalid ids; the document still stores the object name, #394). Then Type › Highlight Alternate Glyphs (off by default: on, pink behind OpenType substitutions on screen — ligatures, contextual forms; Document Setup's Highlight Substituted Glyphs stays separate; `prefs.set`, #394). About 14 stored options still do nothing yet (the rest of #394: Open Documents As Tabs, Real-time Drawing and Editing, GPU Performance beyond Animated Zoom, hyphenation language, scratch disks, the legacy clipboard, Touch Workspace). - Preferences that act (#394, in part): Show Tool Tips, Double Click To Isolate, Zoom with Mouse Wheel (the wheel zooms about the pointer, Shift-wheel scrolls up and down, Cmd/Ctrl-wheel sideways; Alt/Option-wheel and a trackpad pinch zoom about the pointer either way), Use Precise Cursors; Selection & Anchor Display › Tolerance (objects, anchors and handles), Object Selection by Path Only, Command Click to Select Objects Behind (cycling down the stack), Zoom to Selection, anchor Size, Highlight anchors on mouse over; Type › Number of Recent Fonts (up to 15); Guides & Grid › guide and grid Color and Style (lines or dots) and Grids In Back. Then Snap to Point (its distance, for moves, drawn points and reference points), Move Locked and Hidden Artwork with Artboard, Enable Rubber Band for the Pen and Curvature tools, Handles style, Show handles when multiple anchors are selected, Hide Corner Widget for angles greater than; General › Transform Pattern Tiles (the transforms' and dialogs' Transform Patterns, Align and Distribute, art moved or duplicated with an artboard, and objects moved whole with direct-selected anchors) and Select Same Tint %; Type › Size/Leading, Tracking and Baseline Shift increments (`type.step`, Cmd+Shift+. and Cmd+Shift+, for the size, the Type tool's Alt+arrows), Enable Missing Glyph Protection and Fill New Type Objects With Placeholder Text. Then Smart Guides › Color, Alignment Guides, Anchor/Path Labels, Measurement Labels, Transform Tools, Object Highlighting (with Smart Guides on) and Snapping Tolerance (4 px by default, for every smart guide snap), then Construction Guides and their Angles (#506: a drawn point near a line at one of those angles through the point it leaves from lands on it). Then Type › Type Object Selection by Path Only (type is picked on the baseline of any of its lines, area type's frame or the path of type on a path, no longer anywhere in its bounds; the Type tools and the Eyedropper still take a click among the characters). Then General › Anti-aliased Artwork (off, the canvas draws the art with hard edges). Then Guides & Grid › Show Pixel Grid (Above 600% Zoom), a line at every document pixel over the art in Pixel Preview, turning with a rotated view as the document grid now does too. Then General › Show The Home Screen When No Documents Are Open (off, closing the last document leaves an empty window; the Home button still opens the screen). Then Units › Numbers Without Units Are Points (on by default: a number typed with no unit into a field in picas is read in points, a typed unit still wins; dimmed unless a unit is Picas). Then User Interface › Auto-Collapse Iconic Panels (off by default: on, a click away from a panel popped out of the icon column puts it away). Then Performance › Animated Zoom (on, with GPU Performance, which it needs and which dims it when off: the Zoom tool dragged sideways zooms about the press, right in and left out, and held still zooms on, Alt out; off, a drag zooms to the area dragged across, now drawn as a marquee), User Interface › Large Tabs (taller document tabs) and Scale Cursor Proportional to UI (off, the tool cursors keep their size whatever the UI Scaling), and File Handling › Display Bitmaps as Anti-aliased Images in Pixel Preview (off by default: Pixel Preview draws images with each pixel taking its nearest image pixel). Type › Auto Size New Area Type works too (new area type gets Auto Size). They reach agents too (`prefs.set`, then `pointer_gesture` or `press_key`; `ui.wheel` on the control channel). Then Smart Guides › Spacing Guides (moves space the selection evenly with its row or column, see Drawing aids). Then Hyphenation › Exceptions (comma-separated words that never break, or break only at their hyphens; `prefs.set` and restore push them into the hyphenator, #394). Then Type › Show Font Names in English (off, font menus label a family by its native-language name when the font has one; documents still use the English family, #394). Then General › Display Print Size at 100% Zoom (off by default: View › Actual Size is one document point per screen point; on, one document inch fills 96 screen points, an inch at the reference density display scaling is set against; `view.actualSize` / `prefs.set`, #394). Then Guides & Grid › Gridline every and Subdivisions (changing either sets the grid of every open document; new and opened documents take that grid too, except a native file and the native document an SVG, PDF, `.ai` or EPS file carries, which open with the grid they were saved with; `file.new`, `document.open`, `file.newFromTemplate`, `file.revert`, #394). Then Appearance of Black › On Screen (Display All Blacks Accurately by default: pure CMYK black shows through the CMYK profile, a dark grey; Display All Blacks as Rich Black shows it on the canvas as RGB black, also in CMYK documents drawn by ink planes, while rich blacks, black tints and images show as before; `RenderOptions::rich_black`, #394). Then Appearance of Black › Printing / Exporting (Output All Blacks Accurately by default; Output All Blacks as Rich Black draws pure CMYK black as RGB black in RGB raster exports and Print as Bitmap; CMYK ink exports and the document stay unchanged; `prefs.blackOutput` / `richBlack`, #394). Then Units › Identify Objects By (Object Name by default: Layers, Variables and Layer Options show and edit free names; XML ID shows and edits AI-style XML ids, `_` for a space and `_xHH_` escapes, and `layer.setProps` / rename refuse invalid ids; the document still stores the object name, #394). Then Type › Highlight Alternate Glyphs (on by default: pink behind OpenType substitutions on screen — ligatures, contextual forms; Document Setup's Highlight Substituted Glyphs stays separate; `prefs.set`, #394). About 14 stored options still do nothing yet (the rest of #394: Open Documents As Tabs, Real-time Drawing and Editing, GPU Performance beyond Animated Zoom, hyphenation language, scratch disks, the legacy clipboard, Touch Workspace). - The desktop app keeps the Color panel's Show Options or Hide Options between sessions, and `ui.colorPanel` sets its mode and options as its panel menu does. diff --git a/docs/type-parity.md b/docs/type-parity.md index daec5c927..ea09866b4 100644 --- a/docs/type-parity.md +++ b/docs/type-parity.md @@ -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` | diff --git a/docs/ui-parity.md b/docs/ui-parity.md index 72f7502ee..9b386a82a 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -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 |