From 6160d0bd458cac86777c5bff197aaeccc4a66239 Mon Sep 17 00:00:00 2001 From: Don Omondi <282319807+mockydevs@users.noreply.github.com> Date: Fri, 6 Nov 2026 07:32:31 -0500 Subject: [PATCH] Snap to Glyph: glyph bounds give corners and turn with rotated type (#5112) --- crates/tools/src/guides/targets.rs | 29 +++++++++++++++--- crates/tools/src/guides/tests.rs | 48 ++++++++++++++++++++++++++++-- docs/mcp.md | 2 +- docs/target-app-parity.md | 5 ++-- docs/type-parity.md | 5 ++-- 5 files changed, 77 insertions(+), 12 deletions(-) diff --git a/crates/tools/src/guides/targets.rs b/crates/tools/src/guides/targets.rs index 6fed43337..709e1f53d 100644 --- a/crates/tools/src/guides/targets.rs +++ b/crates/tools/src/guides/targets.rs @@ -139,6 +139,23 @@ pub(super) fn object_boxes(doc: &Document, moving: &[NodeId]) -> Vec { out } +/// The corners of glyph `g`'s bounding box in text space, clockwise from its top left as drawn: +/// the box of its outline in its own frame (font units), through `g.xf`, so it turns, slants and +/// scales with the glyph. `bb` is the outline's box in text space, used as it is for a glyph bent +/// along a path or with a frame that can't be undone. +fn glyph_box(g: &vectorcraft_text::PositionedGlyph, bb: &Rect) -> [Point; 4] { + let corners = |r: Rect| [Point::new(r.x0, r.y0), Point::new(r.x1, r.y0), Point::new(r.x1, r.y1), Point::new(r.x0, r.y1)]; + let det = g.xf.determinant(); + if g.bend.is_some() || !(det.is_finite() && det.abs() > 1e-12) { + return corners(*bb); + } + let local = (g.xf.inverse() * g.outline.clone()).bounding_box(); + if !(local.is_finite() && local.width() > 0.0 && local.height() > 0.0) { + return corners(*bb); + } + corners(local).map(|p| g.xf * p) +} + impl Targets { pub fn collect(doc: &Document, exclude: &[NodeId], visible: Option) -> Self { Self::collect_inner(doc, exclude, None, visible, Gather::Bounds) @@ -337,10 +354,14 @@ impl Targets { } } if o.bounds { - let r = t.xf.transform_rect_bbox(bb); - for p in [Point::new(r.x0, r.y0), Point::new(r.x1, r.y0), Point::new(r.x1, r.y1), Point::new(r.x0, r.y1)] { - self.xs.push((p.x, p, kind)); - self.ys.push((p.y, p, kind)); + // The glyph's box in its own frame (font units), placed as the glyph is: its + // corners are points to land on, even where the outline curves (the right of + // `B`), and its edges guides, which turn with rotated or slanted type. + let corners = glyph_box(g, &bb).map(to_doc); + for (i, c) in corners.iter().enumerate() { + self.points.push((*c, kind)); + let next = corners.get((i + 1) % 4).copied().unwrap_or(*c); + self.push_glyph_line(*c, next - *c, o.angular); } } } diff --git a/crates/tools/src/guides/tests.rs b/crates/tools/src/guides/tests.rs index e82618fb3..499410465 100644 --- a/crates/tools/src/guides/tests.rs +++ b/crates/tools/src/guides/tests.rs @@ -837,7 +837,7 @@ fn each_snap_to_glyph_option_takes_away_only_its_own_guides() { for (off, less) in [ (GlyphSnap { baseline: false, ..all }, [1, 0, 0, 0]), (GlyphSnap { x_height: false, ..all }, [1, 0, 0, 0]), - (GlyphSnap { bounds: false, ..all }, [4, 4, 0, 0]), + (GlyphSnap { bounds: false, ..all }, [2, 2, 0, 0]), (GlyphSnap { proximity: false, ..all }, [0, 2, 0, 0]), (GlyphSnap { anchors: false, ..all }, [0, 0, 0, anchors]), (GlyphSnap { angular: false, ..all }, [0, 0, 0, 0]), @@ -851,8 +851,8 @@ fn each_snap_to_glyph_option_takes_away_only_its_own_guides() { let p = Point::new(400.0, x_height + 2.0); assert!((Targets::for_drawing(&d, &[], None, Some(all)).snap_point(p, 4.0).0.y - x_height).abs() < 1e-9); assert_eq!(Targets::for_drawing(&d, &[], None, Some(GlyphSnap { x_height: false, ..all })).snap_point(p, 4.0).0.y, p.y); - // Rotated type: its baseline, x-height and width edges and its strokes run at an angle, all - // Angular Guides; its glyph bounds still run across and down the page. + // Rotated type: its baseline, x-height, width edges, glyph bounds and strokes run at an angle, + // all Angular Guides (the corners of its glyph bounds stay points to land on). let (d, _) = glyph_doc("H", 20f64.to_radians()); let (on, off) = (count(&d, all), count(&d, GlyphSnap { angular: false, ..all })); assert!(on[2] > 0, "{on:?}"); @@ -891,3 +891,45 @@ fn a_shift_constrained_point_slides_onto_a_rotated_baseline() { let (q, _) = Targets::for_drawing(&d, &[], None, Some(GlyphSnap::default())).snap_from(crossing + Vec2::new(1.5, 0.3), Some(&from), 4.0); assert!(q.distance(crossing) < 1e-9, "{q:?} vs {crossing:?}"); } + +/// The corners of a glyph's bounds, from its outline in its own frame placed as the glyph is. +fn glyph_corners(t: &vectorcraft_doc::TextObject) -> [Point; 4] { + let lay = layout_text(FontDb::global(), t); + let g = lay.glyphs.iter().find(|g| !g.outline.elements().is_empty()).expect("a glyph with an outline"); + let r = (g.xf.inverse() * g.outline.clone()).bounding_box(); + [Point::new(r.x0, r.y0), Point::new(r.x1, r.y0), Point::new(r.x1, r.y1), Point::new(r.x0, r.y1)].map(|p| t.xf * g.xf * p) +} + +/// Snap to Glyph › Glyph Bounds (#5112): each corner of a glyph's bounds is a point a drawn point +/// lands on, also where its outline curves (the right of `B`, where no anchor is), and the corners +/// turn with rotated type. Glyph Bounds off, they are gone. +#[test] +fn a_drawn_point_lands_on_a_corner_of_a_glyphs_bounds() { + let style = vectorcraft_doc::CharStyle { size: 200.0, ..Default::default() }; + for angle in [0.0, 30f64.to_radians()] { + let (d, t) = glyph_doc_with("B", angle, style.clone()); + let corners = glyph_corners(&t); + for (i, c) in corners.into_iter().enumerate() { + let p = c + Vec2::new(2.0, -1.5); + let (q, ov) = Targets::for_drawing(&d, &[], None, Some(GlyphSnap::default())).snap_point(p, 4.0); + assert!(q.distance(c) < 1e-9, "angle {angle}: {q:?} vs corner {c:?}"); + // The left corners of `B` are anchors of its stem too; the right ones, on its curved + // side, come from its bounds alone. + if i == 0 || i == 3 { + continue; + } + assert!(ov.iter().any(|o| matches!(o, Overlay::Label { text, .. } if text == "glyph")), "{ov:?}"); + let off = GlyphSnap { bounds: false, ..GlyphSnap::default() }; + let (q, _) = Targets::for_drawing(&d, &[], None, Some(off)).snap_point(p, 4.0); + assert!(q.distance(c) > 1e-3, "Glyph Bounds off: {q:?} still on {c:?}"); + } + // The right edge of the rotated glyph runs at its angle: a point beside it, far from the + // corners, lands on it. + let (tr, br) = (corners[1], corners[2]); + let dir = (br - tr) / (br - tr).hypot(); + let p = tr.midpoint(br) + Vec2::new(-dir.y, dir.x) * 1.5; + let (q, _) = + Targets::for_drawing(&d, &[], None, Some(GlyphSnap { anchors: false, proximity: false, ..GlyphSnap::default() })).snap_point(p, 4.0); + assert!((q - tr).cross(dir).abs() < 1e-6, "angle {angle}: {q:?} is off the right edge"); + } +} diff --git a/docs/mcp.md b/docs/mcp.md index 7bce52793..91589c619 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -3576,7 +3576,7 @@ Guides change where a snapped point lands: The Snap to Glyph options (the desktop app's Character panel menu › Show Snap to Glyph Options; View › Snap to Glyph is the desktop app's): `view.snapToGlyph.options {baseline?, xHeight?, bounds?, proximity?, angular?, anchors?}` (or `prefs.get`/`prefs.set` with key `glyphSnap`, a partial object) reads and sets which guides Snap to Glyph gives, all on -by default: the baseline and the x-height of a single line of type, the glyph bounds (each glyph's edges), the +by default: the baseline and the x-height of a single line of type, the glyph bounds (each glyph's edges and corners, turning with rotated type), the proximity guides at both edges of each character's width, the angular guides (along a glyph's slanted strokes, and every guide of rotated type, which runs at its angle) and the glyphs' anchor points. `{}` reads them; a key it doesn't know or a value that isn't a boolean is refused and changes nothing. diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 544fea0df..4211e36a1 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1,6 +1,6 @@ # Parity with Adobe Illustrator -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-06 · **Change:** minor (Drop Shadow: Color's swatch opens the Color Picker, #5364; Drop Shadow and glow dialogs: spinner arrows, #5367, #5368; Stroke Weight Up and Down shortcuts, #5797; Hindi catalog and contributor loanword preferences; full wording review in progress; Properties in Edit Artboards: a Transform section with the reference point above the Artboards section, #5219; the Control bar's Stroke: and Opacity: links scrub their fields, #4088; an image zoomed in on is separated, colorized or proofed only where it shows (#4155); a document's first frame renders on the render worker, so opening a heavy file no longer freezes the window, #4155; Control bar Paragraph section with type selected, #5372; translating unchanged type reuses its glyph and character-stroke outlines; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-06 · **Change:** minor (Snap to Glyph's glyph bounds give corners and turn with rotated type, #5112; Drop Shadow and glow dialogs: spinner arrows, #5367, #5368; Stroke Weight Up and Down shortcuts, #5797; Hindi catalog and contributor loanword preferences; full wording review in progress; Properties in Edit Artboards: a Transform section with the reference point above the Artboards section, #5219; the Control bar's Stroke: and Opacity: links scrub their fields, #4088; an image zoomed in on is separated, colorized or proofed only where it shows (#4155); a document's first frame renders on the render worker, so opening a heavy file no longer freezes the window, #4155; Control bar Paragraph section with type selected, #5372; translating unchanged type reuses its glyph and character-stroke outlines; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026) ## Feature submission evidence @@ -892,7 +892,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur A move pressed on a path's stroke, or within 6 px of its outline away from its anchors and center, is held by the nearest point of that path, with the Selection, Group Selection or Direct Selection tool, and a straight segment Direct Selection drags is held by its own nearest point. That point is what snaps, so the path lands exactly on a ruler guide (with "align") instead of the pressed spot landing there and the path a few pixels beside it, an offset that showed only zoomed far in (#3701). A press in the middle of a fill still snaps the pressed spot: the public [Align objects with guides](https://helpx.adobe.com/illustrator/desktop/measure-and-align/grids-and-guides/align-graphic-objects-with-guides.html) page has Snap to Point align the point under the pointer, which is placed exactly where the object should meet the guide. A bounding-box handle dragged with the Selection or Free Transform tool snaps what it moves as a dragged point snaps (#2054). With View › Snap to Grid on, it snaps to the nearest gridline, and with Snap to Pixel on, it snaps to whole pixels. With Smart Guides off and Snap to Point on, it snaps to the other objects' anchors and the ruler guides within the Snap to Point distance. With Smart Guides and Snap to Point both on, it lines up with the ruler guides as well as the other objects. Shift still keeps the proportions, and Alt still scales about the center. A user slice's edge or corner handle dragged with the Slice Selection tool snaps as the Slice tool's drag does. With View › Snap to Grid on, it snaps to the nearest gridline, and with Snap to Pixel on, it snaps to whole pixels. Otherwise, with Smart Guides on, it lands on or lines up with the anchors, centers and edges of the art and the artboards, and with Smart Guides off and Snap to Point on, it snaps to the art's anchors and the ruler guides within the Snap to Point distance. Then a ruler guide within 5 px, shown and unlocked, moves the side or corner onto it. Shift still keeps the proportions, and Alt still resizes about the center. - View › Snap to Glyph: with Smart Guides on, a point drawn near a single line of live type lands on its glyphs' anchor points and lines up with its baseline, its x-height and the edges of each character's advance width (proximity guides), as well as the glyph bounds. In rotated type these guides run at the type's angle, and type of one glyph also has angular guides along its slanted straight segments. A paragraph shows glyph bounds only. + View › Snap to Glyph: with Smart Guides on, a point drawn near a single line of live type lands on its glyphs' anchor points and lines up with its baseline, its x-height and the edges of each character's advance width (proximity guides), as well as the glyph bounds: each glyph's box gives edge guides and four corners a point lands on, even where its outline curves (the right of `B`), and the box follows the glyph's size, scaling and rotation (#5112). In rotated type these guides run at the type's angle, and type of one glyph also has angular guides along its slanted straight segments. A paragraph shows glyph bounds only. Snap to Glyph options (#4232, [Snap to Glyph options](https://helpx.adobe.com/illustrator/desktop/design-with-text/special-characters-glyphs/snap-to-glyph-options.html)): the Character panel menu's Show Snap to Glyph Options (Hide Snap to Glyph Options) shows a Snap to Glyph section in the panel, with or without type selected, with a checkbox for each kind of guide: Baseline, x-height, Glyph Bounds, Proximity Guides (the edges of each character's width), Angular Guides and Anchor Point. Each off takes its guides away and leaves the others (`view.snapToGlyph.options`, the `glyphSnap` preference group, all on by default). Angular Guides covers a glyph's slanted strokes and every guide of rotated type, which runs at its angle; the boxes are dimmed while View › Snap to Glyph is off, and the section starts with a Snap to Glyph checkbox for it (VectorCraft choices; the page settles neither). Showing the section scrolls the panel to it, since it comes after every character attribute (#5273). Tested in `crates/tools/src/guides/tests.rs`, `crates/ui-egui/src/panels/character/tests.rs` and `crates/engine/src/tests/prefs.rs`. - **Artboard click-to-fit (#1227):** a plain click fits the topmost editable object or group to a new artboard, including its stroke like Fit to Selected Art, and makes the new artboard active. Art and its selection stay unchanged; creation is one undo step. Handles, modified clicks, drags, Escape and tool changes retain their meaning. Tested inside an artboard and on the pasteboard (`crates/engine/tests/it/artboard_click.rs`); the public [CS6 Printing Guide](https://helpx.adobe.com/content/dam/help/en/indesign/pdf/CS6_PrintGuide.pdf) documents the object-click gesture. This legacy gesture adds evidence without regrading the 2026 canvas controls. - **Rotated views:** with the view rotated, Fit Artboard in Window, Fit All in Window and the status bar's artboard navigator fit the artboard or the art as the view turns it, and a Zoom tool marquee (Animated Zoom or GPU Performance off) zooms to the area dragged across. Ruler guides on no artboard turn with the view. The canvas draws the selection marquee where the tools select, Show Hidden Characters draws the marks of type partly inside the canvas, the Contextual Task Bar stays under the selection, and the Navigator's red box outlines what the window shows. @@ -1703,6 +1703,7 @@ Edit › Copy and Cut put the selected ruler guides on the clipboard, with the s | Date | Change | Summary | |---|---|---| +| 2026-11-06 | minor | Snap to Glyph › Glyph Bounds: the corners of each glyph's box are points a drawn point lands on, also on a curved side (`B`), and the box turns with rotated type, its edges then Angular Guides (#5112; [Snap to Glyph options](https://helpx.adobe.com/illustrator/desktop/design-with-text/special-characters-glyphs/snap-to-glyph-options.html)); scores and estimates unchanged. | | 2026-11-06 | minor | Drop Shadow: a swatch beside Color opens the Color Picker over the dialog, as the other effect colour fields do (#5364); correction: the #5365 entry listed Drop Shadow, whose Color is drawn outside the shared grid and had no swatch; scores unchanged. | | 2026-11-06 | minor | Drop Shadow, Outer Glow and Inner Glow: their number fields have spinner arrows (#5367, #5368); evidence added; scores unchanged. | | 2026-11-06 | minor | Stroke Weight Up and Down (Cmd/Ctrl+Alt+. and ,) step each selected stroke's weight (#5797); evidence added; scores unchanged. | diff --git a/docs/type-parity.md b/docs/type-parity.md index c539baf2e..aea0d5300 100644 --- a/docs/type-parity.md +++ b/docs/type-parity.md @@ -1,6 +1,6 @@ # Type parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-05 · **Change:** minor (Show Snap to Glyph Options scrolls to the options, with a Snap to Glyph checkbox, #5273; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-06 · **Change:** minor (Snap to Glyph's glyph bounds give corners and turn with rotated type, #5112; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x) Type is Illustrator's largest single area by use (weight 13 of 105 across "Type core" and "Type advanced" in [target-app-parity.md](target-app-parity.md)). This checklist lists the features one by one. Status is from the @@ -52,7 +52,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 (`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`). 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 | | | 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` | @@ -77,6 +77,7 @@ Character Styles menu New/Redefine and footer New capture the first selected cha | Date | Change | Summary | |---|---|---| +| 2026-11-06 | minor | Snap to Glyph › Glyph Bounds: each glyph's box gives four corners a drawn point lands on, also on a curved side (`B`), and its box and edges turn with rotated type (#5112); scores and estimates unchanged | | 2026-11-05 | minor | Character panel › Show Snap to Glyph Options scrolls the panel down to the Snap to Glyph section, which comes after every character attribute and could sit out of sight, and the section starts with a Snap to Glyph checkbox (View › Snap to Glyph) so its dimmed options can be turned on there (#5273); scores and estimates unchanged | | 2026-11-05 | minor | Release Type on a Path joins the Type Tools add-on: type on a path splits into point type and its path (#5532, beyond Illustrator). Scores and estimates unchanged. | | 2026-11-04 | minor | A Type-range Copy or Cut publishes styled art alongside plain text, so a later canvas Paste keeps its character formatting after editing ends (#5636). Scores and estimates unchanged. |