Align: Align to Glyph Bounds for point and area type (#4231)

This commit is contained in:
bflatastic
2026-10-30 02:01:58 +07:00
parent 2ad24732ae
commit 513b02233f
38 changed files with 352 additions and 59 deletions
+69 -31
View File
@@ -3,6 +3,7 @@ use super::{
Affine, Document, EngineError, NodeId, OrientedBox, Point, Rect, Result, Scaling, Session, Value, Vec2, bad, check_numbers, check_switches,
effects, has_multi, ok, selected_roots, str_param,
};
use vectorcraft_doc::{NodeKind, TextKind};
impl Session {
/// The bounds of `ids` the Transform panel, the bounding box, Align and Distribute measure:
@@ -18,12 +19,51 @@ impl Session {
}
}
/// Measure with preview (visual) bounds? The `bounds` param, else Use Preview Bounds.
fn preview_bounds(s: &Session, p: &Value, cmd: &str) -> Result<bool> {
match choice(p, cmd, "bounds", &["preview", "geometric"])? {
None => Ok(s.prefs.use_preview_bounds),
Some(b) => Ok(b == "preview"),
/// How Align and Distribute measure each object: by visual bounds as drawn (`preview`, see
/// [`effects::preview::bounds_of`]) or geometric bounds, and point type (`point`) or area type
/// (`area`) by the ink of its glyphs instead of its em box or frame (Align to Glyph Bounds).
#[derive(Clone, Copy, Default)]
struct Measure {
preview: bool,
point: bool,
area: bool,
}
impl Measure {
/// The bounds of `ids` together.
fn bounds(self, d: &Document, ids: &[NodeId]) -> Option<Rect> {
if !(self.point || self.area) {
return effects::preview::bounds_of(d, ids, self.preview);
}
ids.iter().filter_map(|id| self.of(d, *id)).reduce(|a, b| a.union(b))
}
/// The bounds of `id`: with Align to Glyph Bounds on for its kind of type, its glyphs' ink
/// (with the characters' strokes when measuring visual bounds). Type on a path, and type that
/// draws nothing, is measured as before.
fn of(self, d: &Document, id: NodeId) -> Option<Rect> {
if let Some(NodeKind::Text(t)) = d.node(id).map(|n| &n.kind)
&& match t.kind {
TextKind::Point => self.point,
TextKind::Area { .. } => self.area,
TextKind::OnPath { .. } => false,
}
&& let Some(ink) = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), t).ink_bounds()
{
let r = t.xf.transform_rect_bbox(ink);
return Some(if self.preview { r.inflate(t.char_stroke_outset(), t.char_stroke_outset()) } else { r });
}
effects::preview::bounds_of(d, &[id], self.preview)
}
}
/// How the Align commands measure: preview (visual) bounds from the `bounds` param, else Use
/// Preview Bounds, and type by its glyphs as Align to Glyph Bounds says.
fn measure(s: &Session, p: &Value, cmd: &str) -> Result<Measure> {
let preview = match choice(p, cmd, "bounds", &["preview", "geometric"])? {
None => s.prefs.use_preview_bounds,
Some(b) => b == "preview",
};
Ok(Measure { preview, point: s.prefs.align_glyph_bounds_point, area: s.prefs.align_glyph_bounds_area })
}
/// Param `key` of the Align commands, Shear, Unlock All, Show All and `text.setFormat`'s lists: one
@@ -50,9 +90,9 @@ fn spacing(p: &Value, cmd: &str) -> Result<Option<f64>> {
.transpose()
}
/// `ids` with their bounds (`preview`: visual bounds as drawn, see [`effects::preview::bounds_of`]).
fn items_bounds(d: &Document, ids: &[NodeId], preview: bool) -> Vec<(NodeId, Rect)> {
ids.iter().filter_map(|id| Some((*id, effects::preview::bounds_of(d, &[*id], preview)?))).collect()
/// `ids` with their bounds, measured by `m`.
fn items_bounds(d: &Document, ids: &[NodeId], m: Measure) -> Vec<(NodeId, Rect)> {
ids.iter().filter_map(|id| Some((*id, m.of(d, *id)?))).collect()
}
/// Move each object of `moves` by its offset and each ruler guide of `guides` (index, position)
@@ -101,13 +141,13 @@ fn key_guide(s: &Session, p: &Value) -> Option<(f64, bool)> {
/// Where Align puts the objects, `r`, and the axes it moves them along (x, y): a key guide gives
/// the line across its axis only (a vertical guide fixes x, a horizontal one y), so the other
/// axis stays as it is.
fn align_reference(s: &Session, p: &Value, ids: &[NodeId], preview: bool, own: Option<Rect>) -> Result<(Rect, bool, bool)> {
fn align_reference(s: &Session, p: &Value, ids: &[NodeId], m: Measure, own: Option<Rect>) -> Result<(Rect, bool, bool)> {
if let Some((pos, vertical)) = key_guide(s, p) {
return Ok((Rect::new(pos, pos, pos, pos), vertical, !vertical));
}
let r = match own {
Some(b) if align_to(s, p) != Some("artboard") => b,
_ => reference_rect(s, p, ids, preview)?,
_ => reference_rect(s, p, ids, m)?,
};
Ok((r, true, true))
}
@@ -121,13 +161,11 @@ fn selected_guides_bounds(st: &crate::DocState) -> Option<Rect> {
})
}
fn reference_rect(s: &Session, p: &Value, ids: &[NodeId], preview: bool) -> Result<Rect> {
fn reference_rect(s: &Session, p: &Value, ids: &[NodeId], m: Measure) -> Result<Rect> {
let st = s.doc()?;
match align_to(s, p) {
Some("artboard") => {
let b = effects::preview::bounds_of(&st.doc, ids, preview)
.or_else(|| ids.is_empty().then(|| selected_guides_bounds(st)).flatten())
.unwrap_or_default();
let b = m.bounds(&st.doc, ids).or_else(|| ids.is_empty().then(|| selected_guides_bounds(st)).flatten()).unwrap_or_default();
// Align to Artboard uses artboard `artboard` (the app passes the active one). Without
// it, or with an index the document doesn't have, it uses the artboard under the center
// of the selection, or the first one when no artboard is there.
@@ -137,13 +175,13 @@ fn reference_rect(s: &Session, p: &Value, ids: &[NodeId], preview: bool) -> Resu
}
Some("key") => {
let k = st.selection.key.ok_or_else(|| EngineError::Other("no key object".into()))?;
effects::preview::bounds_of(&st.doc, &[k], preview).ok_or(EngineError::NoNode(k))
m.of(&st.doc, k).ok_or(EngineError::NoNode(k))
}
_ => {
if ids.is_empty() {
selected_guides_bounds(st).ok_or_else(|| EngineError::Other("nothing to align".into()))
} else {
effects::preview::bounds_of(&st.doc, ids, preview).ok_or_else(|| EngineError::Other("nothing to align".into()))
m.bounds(&st.doc, ids).ok_or_else(|| EngineError::Other("nothing to align".into()))
}
}
}
@@ -153,9 +191,9 @@ fn reference_rect(s: &Session, p: &Value, ids: &[NodeId], preview: bool) -> Resu
/// (command `cmd`), the same artboard as `object.align` uses ([`reference_rect`]). It returns
/// `None` for the other choices. A `to` outside the choices is an error. Both commands call it
/// before they space selected anchor points, and they space those points without the artboard.
fn distribute_to(s: &Session, p: &Value, ids: &[NodeId], preview: bool, cmd: &str) -> Result<Option<Rect>> {
fn distribute_to(s: &Session, p: &Value, ids: &[NodeId], m: Measure, cmd: &str) -> Result<Option<Rect>> {
match choice(p, cmd, "to", &["selection", "artboard", "key"])? {
Some("artboard") => reference_rect(s, p, ids, preview).map(Some),
Some("artboard") => reference_rect(s, p, ids, m).map(Some),
_ => Ok(None),
}
}
@@ -171,7 +209,7 @@ fn key_root(s: &Session, ids: &[NodeId]) -> Option<NodeId> {
pub(super) fn align(s: &mut Session, p: &Value) -> Result<Value> {
let ids = selected_roots(s)?;
let preview = preview_bounds(s, p, "object.align")?;
let m = measure(s, p, "object.align")?;
let (h, v) = direction(p, "object.align")?;
choice(p, "object.align", "to", &["selection", "artboard", "key"])?;
check_switches(p, &["patterns"], "object.align")?;
@@ -183,12 +221,12 @@ pub(super) fn align(s: &mut Session, p: &Value) -> Result<Value> {
if !s.doc()?.selection.anchors.is_empty() {
return align_anchors(s, p, &ids, (h, v));
}
let (r, along_x, along_y) = align_reference(s, p, &ids, preview, None)?;
let (r, along_x, along_y) = align_reference(s, p, &ids, m, None)?;
let (h, v) = (h.filter(|_| along_x), v.filter(|_| along_y));
let key = key_root(s, &ids);
let moves: Vec<(NodeId, Vec2)> = {
let d = &s.doc()?.doc;
items_bounds(d, &ids, preview)
items_bounds(d, &ids, m)
.into_iter()
.filter(|(id, _)| Some(*id) != key || align_to(s, p) != Some("key"))
.map(|(id, b)| {
@@ -261,7 +299,7 @@ fn align_anchors(s: &mut Session, p: &Value, ids: &[NodeId], (h, v): (Option<&st
};
// shortcut: no key anchor yet, so Align To Key Object aligns to the points' bounds as Selection does
// (or to a key guide).
let (r, along_x, along_y) = align_reference(s, p, ids, false, Some(bounds))?;
let (r, along_x, along_y) = align_reference(s, p, ids, Measure::default(), Some(bounds))?;
let (h, v) = (h.filter(|_| along_x), v.filter(|_| along_y));
let target = |q: Point| {
let x = match h {
@@ -316,12 +354,12 @@ enum Spaced {
Guide(usize),
}
/// The selected objects `ids` with their bounds (`preview`: visual bounds), and the selected
/// The selected objects `ids` with their bounds (measured by `m`), and the selected
/// ruler guides across the axis (vertical ones when `horiz`) with their line as their bounds.
/// Guides along the axis are left out, so they stay where they are.
fn spaced_items(s: &Session, ids: &[NodeId], preview: bool, horiz: bool) -> Result<Vec<(Spaced, Rect)>> {
fn spaced_items(s: &Session, ids: &[NodeId], m: Measure, horiz: bool) -> Result<Vec<(Spaced, Rect)>> {
let st = s.doc()?;
let objects = items_bounds(&st.doc, ids, preview).into_iter().map(|(id, r)| (Spaced::Object(id), r));
let objects = items_bounds(&st.doc, ids, m).into_iter().map(|(id, r)| (Spaced::Object(id), r));
let guides = st.selection.guides.iter().filter_map(|&i| {
let g = st.doc.guides.get(i).filter(|g| g.vertical == horiz)?;
Some((Spaced::Guide(i), Rect::new(g.pos, g.pos, g.pos, g.pos)))
@@ -405,8 +443,8 @@ pub(super) fn distribute(s: &mut Session, p: &Value) -> Result<Value> {
check_switches(p, &["patterns"], "object.distribute")?;
check_numbers(p, &["artboard"], "object.distribute")?;
let ids = selected_roots(s)?;
let preview = preview_bounds(s, p, "object.distribute")?;
let board = distribute_to(s, p, &ids, preview, "object.distribute")?;
let m = measure(s, p, "object.distribute")?;
let board = distribute_to(s, p, &ids, m, "object.distribute")?;
let spacing = spacing(p, "object.distribute")?;
// Anchor points selected with Direct Selection are distributed themselves, as Align aligns them.
if !s.doc()?.selection.anchors.is_empty() {
@@ -436,7 +474,7 @@ pub(super) fn distribute(s: &mut Session, p: &Value) -> Result<Value> {
};
// The selected guides across the axis are spaced with the objects (#1597). A guide has no
// size, so its left edge, center and right edge are all its line.
let mut items = spaced_items(s, &ids, preview, horiz)?;
let mut items = spaced_items(s, &ids, m, horiz)?;
items.sort_by(|a, b| key(&a.1, horiz).total_cmp(&key(&b.1, horiz)));
let n = items.len();
if spacing.is_some() && key_guide_along(s, p, horiz) {
@@ -481,14 +519,14 @@ pub(super) fn distribute_spacing(s: &mut Session, p: &Value) -> Result<Value> {
check_numbers(p, &["artboard"], "object.distributeSpacing")?;
let ids = selected_roots(s)?;
let horiz = choice(p, "object.distributeSpacing", "axis", &["horizontal", "vertical"])? != Some("vertical");
let preview = preview_bounds(s, p, "object.distributeSpacing")?;
let board = distribute_to(s, p, &ids, preview, "object.distributeSpacing")?;
let m = measure(s, p, "object.distributeSpacing")?;
let board = distribute_to(s, p, &ids, m, "object.distributeSpacing")?;
let spacing = spacing(p, "object.distributeSpacing")?;
if !s.doc()?.selection.anchors.is_empty() {
return distribute_anchors(s, "Distribute Spacing", horiz, spacing);
}
// The selected guides across the axis are spaced with the objects, as items with no size (#1597).
let mut items = spaced_items(s, &ids, preview, horiz)?;
let mut items = spaced_items(s, &ids, m, horiz)?;
items.sort_by(|a, b| if horiz { a.1.x0.total_cmp(&b.1.x0) } else { a.1.y0.total_cmp(&b.1.y0) });
if spacing.is_some() && key_guide_along(s, p, horiz) {
return ok();
+3 -1
View File
@@ -376,7 +376,9 @@ pub const PREF_SPECS: &[PrefSpec] = &[
"Transform",
choice(&[("default", "Default"), ("object", "Transform Object Only"), ("pattern", "Transform Pattern Only"), ("both", "Transform Both")])
),
p!("useRegistrationPointForSymbol", "General", "Transform Panel", "Use Registration Point for Symbol", bool),
p!("useRegistrationPointForSymbol", "General", "Transform Panel", "Use Registration Point for Symbol", bool), // The Align panel menu: Align and Distribute measure type by its glyphs' ink
p!("alignGlyphBoundsPoint", "Type", "Align to Glyph Bounds", "Point Text", bool),
p!("alignGlyphBoundsArea", "Type", "Align to Glyph Bounds", "Area Text", bool),
];
pub fn spec(key: &str) -> Option<&'static PrefSpec> {
+8
View File
@@ -27,6 +27,12 @@ pub struct Prefs {
pub select_same_tint_percent: bool,
pub show_home_screen: bool,
pub use_preview_bounds: bool,
/// Align panel menu › Align to Glyph Bounds › Point Text: Align and Distribute measure point
/// type by the ink of its glyphs instead of its em box.
pub align_glyph_bounds_point: bool,
/// Align panel menu › Align to Glyph Bounds › Area Text: the same for area type, instead of
/// its frame.
pub align_glyph_bounds_area: bool,
pub display_print_size: bool,
pub double_click_to_isolate: bool,
pub transform_pattern_tiles: bool,
@@ -317,6 +323,8 @@ impl Default for Prefs {
select_same_tint_percent: false,
show_home_screen: true,
use_preview_bounds: false,
align_glyph_bounds_point: false,
align_glyph_bounds_area: false,
display_print_size: false,
double_click_to_isolate: true,
transform_pattern_tiles: false,
+1
View File
@@ -59,6 +59,7 @@ mod fontfiles;
mod fontlist;
mod freeform;
mod globaledit;
mod glyphbounds;
mod gradient;
mod gradpanel;
mod gradspan;
+110
View File
@@ -0,0 +1,110 @@
//! Align panel menu › Align to Glyph Bounds (#4231): with Point Text or Area Text on, Align and
//! Distribute measure that kind of type by the ink of its glyphs instead of its em box or frame.
use serde_json::{Value, json};
use vectorcraft_doc::{NodeId, NodeKind};
use vectorcraft_geom::Rect;
use crate::*;
fn run(s: &mut Session, id: &str, p: Value) -> Value {
s.execute(id, &p).unwrap_or_else(|e| panic!("{id}: {e}"))
}
fn id(v: Value) -> NodeId {
NodeId(v["id"].as_u64().unwrap())
}
/// A document with a 50 pt square at (10, 10), lowercase "ace" (no ascenders or descenders, so
/// its ink sits inside its em box) as point type with its baseline at (100, 300), and as area
/// type in a 200 × 100 pt frame at (100, 350); Align to Glyph Bounds set as `point` and `area`.
fn doc(point: bool, area: bool) -> (Session, NodeId, NodeId, NodeId) {
let mut s = Session::new();
run(&mut s, "file.new", json!({"width": 600, "height": 600}));
let square = id(run(&mut s, "shape.rectangle", json!({"x": 10, "y": 10, "width": 50, "height": 50})));
let pt = id(run(&mut s, "text.create", json!({"x": 100, "y": 300, "size": 48, "text": "ace"})));
let ar = id(run(&mut s, "text.create", json!({"x": 100, "y": 350, "size": 48, "area": {"width": 200, "height": 100}, "text": "ace"})));
run(&mut s, "prefs.set", json!({"values": {"alignGlyphBoundsPoint": point, "alignGlyphBoundsArea": area}}));
(s, square, pt, ar)
}
/// The em box (point type) or frame (area type): what Align measures by default.
fn em(s: &Session, id: NodeId) -> Rect {
s.doc().unwrap().doc.node(id).unwrap().geometric_bounds().unwrap()
}
/// The ink of the type's glyphs on the page.
fn ink(s: &Session, id: NodeId) -> Rect {
let NodeKind::Text(t) = &s.doc().unwrap().doc.node(id).unwrap().kind else { panic!("type") };
t.xf.transform_rect_bbox(vectorcraft_text::layout(vectorcraft_text::FontDb::global(), t).ink_bounds().unwrap())
}
fn close(a: f64, b: f64) -> bool {
(a - b).abs() < 1e-6
}
/// Aligns `ids` to the left and the top of the selection (the square is left and above them).
fn align_top_left(s: &mut Session, ids: &[NodeId]) {
run(s, "select.set", json!({"ids": ids.iter().map(|i| i.0).collect::<Vec<_>>()}));
run(s, "object.align", json!({"horizontal": "left"}));
run(s, "object.align", json!({"vertical": "top"}));
}
#[test]
fn glyph_ink_sits_inside_the_em_box_of_lowercase_type() {
let (s, _, pt, ar) = doc(false, false);
for t in [pt, ar] {
let (e, i) = (em(&s, t), ink(&s, t));
assert!(i.y0 > e.y0 + 1.0 && i.x0 > e.x0, "{t:?}: ink {i:?} inside {e:?}");
}
}
#[test]
fn point_text_aligns_by_its_glyphs_with_point_text_on() {
for on in [false, true] {
let (mut s, square, pt, _) = doc(on, false);
align_top_left(&mut s, &[square, pt]);
let r = if on { ink(&s, pt) } else { em(&s, pt) };
assert!(close(r.x0, 10.0) && close(r.y0, 10.0), "Point Text {on}: {r:?}");
// The square stays where it is: it is the selection's top-left.
assert_eq!(em(&s, square), Rect::new(10.0, 10.0, 60.0, 60.0));
}
}
#[test]
fn area_text_aligns_by_its_glyphs_with_area_text_on() {
for on in [false, true] {
let (mut s, square, _, ar) = doc(false, on);
align_top_left(&mut s, &[square, ar]);
let r = if on { ink(&s, ar) } else { em(&s, ar) };
assert!(close(r.x0, 10.0) && close(r.y0, 10.0), "Area Text {on}: {r:?}");
}
}
/// Point Text leaves area type measured by its frame, and Area Text leaves point type measured
/// by its em box.
#[test]
fn the_two_toggles_are_independent() {
let (mut s, square, _, ar) = doc(true, false);
align_top_left(&mut s, &[square, ar]);
let r = em(&s, ar);
assert!(close(r.x0, 10.0) && close(r.y0, 10.0), "area type by its frame: {r:?}");
let (mut s, square, pt, _) = doc(false, true);
align_top_left(&mut s, &[square, pt]);
let r = em(&s, pt);
assert!(close(r.x0, 10.0) && close(r.y0, 10.0), "point type by its em box: {r:?}");
}
/// Distribute measures type by its glyphs too: the point type's ink left edge lands halfway
/// between the two squares' left edges.
#[test]
fn distribute_spaces_point_text_by_its_glyphs() {
for on in [false, true] {
let (mut s, square, pt, _) = doc(on, false);
let far = id(run(&mut s, "shape.rectangle", json!({"x": 410, "y": 10, "width": 50, "height": 50})));
run(&mut s, "select.set", json!({"ids": [square.0, pt.0, far.0]}));
run(&mut s, "object.distribute", json!({"horizontal": "left"}));
let r = if on { ink(&s, pt) } else { em(&s, pt) };
assert!(close(r.x0, 210.0), "Point Text {on}: {r:?}");
}
}
+2 -22
View File
@@ -1,6 +1,6 @@
use super::flow::flow;
use super::region::Region;
use super::{Affine, Ctx, Punct, Range, Rect, SGlyph, Shape, Tcy, TextLayout, TextObject, VerticalAlign, is_cjk, punct};
use super::{Affine, Ctx, Punct, Range, Rect, SGlyph, Tcy, TextLayout, TextObject, VerticalAlign, is_cjk, punct};
/// System Layout on or Fractional Widths off: each character that takes space advances by a whole
/// number of points, at least one, so small type does not collapse onto itself. A zero advance (a
@@ -242,28 +242,8 @@ pub(super) fn stands_upright(g: &SGlyph) -> bool {
pub(super) fn finish_bounds(out: &mut TextLayout) {
let xf = out.line_xf;
let mut b: Option<Rect> = None;
let mut b: Option<Rect> = out.ink_bounds();
let mut add = |r: Rect| b = Some(b.map_or(r, |b| b.union(r)));
for g in &out.glyphs {
if !g.outline.elements().is_empty() {
add(g.outline.bounding_box());
}
if let Some((image, xf)) = g.placed_image() {
add(xf.transform_rect_bbox(image.rect()));
}
if let Some(b) = g.colour.as_ref().and_then(|c| c.geometric_bounds()) {
add(g.xf.transform_rect_bbox(b));
}
}
// Ruby: covered by its line's ascent, but its ink can overhang sideways past the line.
for g in &out.ruby {
if !g.outline.elements().is_empty() {
add(g.outline.bounding_box());
}
}
for i in &out.inlines {
add(i.bounds);
}
if !out.on_path {
for l in &out.lines {
add(xf.transform_rect_bbox(Rect::new(l.x0.min(l.x1), l.baseline - l.ascent, l.x0.max(l.x1), l.baseline + l.descent)));
+16 -1
View File
@@ -43,7 +43,7 @@ pub use hyphen::{hyphenation_exceptions, set_hyphenation_exceptions};
use std::sync::Arc;
pub use svg_glyph::{SvgGlyphReader, set_svg_glyph_reader};
use kurbo::{Affine, BezPath, Point, Rect, Vec2};
use kurbo::{Affine, BezPath, Point, Rect, Shape, Vec2};
pub use layout::{decorations, fit_first_line, layout, layout_with, scaled};
pub use vectorcraft_doc::TextObject;
@@ -341,6 +341,21 @@ impl TextLayout {
};
self.lines.iter().filter(|l| l.x1 > l.x0).map(line).collect()
}
/// The ink of the type in text space: its glyphs (and ruby) as drawn, colour glyphs, images and
/// inline graphics included, without the lines' em boxes that [`Self::bounds`] also covers.
/// What Align to Glyph Bounds aligns. `None` when nothing draws (only spaces, or no text).
pub fn ink_bounds(&self) -> Option<Rect> {
let outlined = |g: &PositionedGlyph| (!g.outline.elements().is_empty()).then(|| g.outline.bounding_box());
let glyphs = self.glyphs.iter().flat_map(|g| {
let image = g.placed_image().map(|(image, xf)| xf.transform_rect_bbox(image.rect()));
let colour = g.colour.as_ref().and_then(|c| c.geometric_bounds()).map(|b| g.xf.transform_rect_bbox(b));
[outlined(g), image, colour]
});
// Ruby is covered by its line's ascent, but its ink can overhang sideways past the line.
let ruby = self.ruby.iter().map(outlined);
let inlines = self.inlines.iter().map(|i| Some(i.bounds));
glyphs.chain(ruby).chain(inlines).flatten().reduce(|a, b| a.union(b))
}
/// All glyph outlines combined (e.g. for Create Outlines).
pub fn to_bezpath(&self) -> BezPath {
let mut p = BezPath::new();
+3
View File
@@ -5572,3 +5572,6 @@ axis Y Axis Y ос
# Object › Expand… dimmed until Expand Appearance (#3089).
Expand Appearance first (Object › Expand Appearance) Първо разширете външния вид (Обект › Разширяване на външния вид)
Live preview Преглед на живо
# Align panel menu › Align to Glyph Bounds (#4231).
Point Text Точков текст
Area Text Текст в зона
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -5673,3 +5673,6 @@ swatch White, Black Blanc, negre
swatch Yellow Groc
swatch Yellow Green Verd groc
theme Appearance Aparença
# Align panel menu › Align to Glyph Bounds (#4231).
Point Text Text de punt
Area Text Text d’àrea
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -5682,3 +5682,6 @@ axis Y Axis osa Y
# Object › Expand… dimmed until Expand Appearance (#3089).
Expand Appearance first (Object › Expand Appearance) Nejprve rozložte vzhled (Objekt › Rozložit vzhled)
Live preview Živý náhled
# Align panel menu › Align to Glyph Bounds (#4231).
Point Text Bodový text
Area Text Text v oblasti
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -5587,3 +5587,6 @@ axis Y Axis Y-akse
# Object › Expand… dimmed until Expand Appearance (#3089).
Expand Appearance first (Object › Expand Appearance) Udvid først udseendet (Objekt › Udvid udseende)
Live preview Direkte forhåndsvisning
# Align panel menu › Align to Glyph Bounds (#4231).
Point Text Punkttekst
Area Text Områdetekst
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -5264,3 +5264,6 @@ axis Y Axis Y-Achse
# Object › Expand… dimmed until Expand Appearance (#3089).
Expand Appearance first (Object › Expand Appearance) Wandeln Sie zuerst das Aussehen um (Objekt › Aussehen umwandeln)
Live preview Live-Vorschau
# Align panel menu › Align to Glyph Bounds (#4231).
Point Text Punkttext
Area Text Flächentext
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -5634,3 +5634,6 @@ axis Y Axis Άξονας Y
# Object › Expand… dimmed until Expand Appearance (#3089).
Expand Appearance first (Object › Expand Appearance) Επεκτείνετε πρώτα την εμφάνιση (Αντικείμενο › Επέκταση εμφάνισης)
Live preview Ζωντανή προεπισκόπηση
# Align panel menu › Align to Glyph Bounds (#4231).
Point Text Κείμενο σημείου
Area Text Κείμενο περιοχής
Can't render this file because it contains an unexpected character in line 164 and column 5.
+3
View File
@@ -5624,3 +5624,6 @@ axis Y Axis Eje Y
# Object › Expand… dimmed until Expand Appearance (#3089).
Expand Appearance first (Object › Expand Appearance) Primero expande la apariencia (Objeto › Expandir apariencia)
Live preview Vista previa en directo
# Align panel menu › Align to Glyph Bounds (#4231).
Point Text Texto de punto
Area Text Texto de área
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -5713,3 +5713,6 @@ axis Y Axis Y-akseli
# Object › Expand… dimmed until Expand Appearance (#3089).
Expand Appearance first (Object › Expand Appearance) Laajenna ensin ulkoasu (Objekti › Laajenna ulkoasu)
Live preview Reaaliaikainen esikatselu
# Align panel menu › Align to Glyph Bounds (#4231).
Point Text Pisteteksti
Area Text Alueteksti
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -5681,3 +5681,6 @@ axis Y Axis Axe Y
# Object › Expand… dimmed until Expand Appearance (#3089).
Expand Appearance first (Object › Expand Appearance) Décomposez d’abord l’aspect (Objet › Décomposer l’aspect)
Live preview Aperçu en direct
# Align panel menu › Align to Glyph Bounds (#4231).
Point Text Texte de point
Area Text Texte captif
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -5668,3 +5668,6 @@ axis Y Axis Y tengely
# Object › Expand… dimmed until Expand Appearance (#3089).
Expand Appearance first (Object › Expand Appearance) Előbb bontsd ki a megjelenést (Objektum › Megjelenés kibontása)
Live preview Élő előnézet
# Align panel menu › Align to Glyph Bounds (#4231).
Point Text Pontszöveg
Area Text Területszöveg
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -5587,3 +5587,6 @@ axis Y Axis Sumbu Y
# Object › Expand… dimmed until Expand Appearance (#3089).
Expand Appearance first (Object › Expand Appearance) Perluas tampilan terlebih dahulu (Objek › Perluas tampilan)
Live preview Pratinjau langsung
# Align panel menu › Align to Glyph Bounds (#4231).
Point Text Teks titik
Area Text Teks area
Can't render this file because it contains an unexpected character in line 2169 and column 5.
+3
View File
@@ -5298,3 +5298,6 @@ axis Y Axis Asse Y
# Object › Expand… dimmed until Expand Appearance (#3089).
Expand Appearance first (Object › Expand Appearance) Espandi prima l’aspetto (Oggetto › Espandi aspetto)
Live preview Anteprima dal vivo
# Align panel menu › Align to Glyph Bounds (#4231).
Point Text Testo per punti
Area Text Testo in area
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -5360,3 +5360,6 @@ axis Y Axis Y 軸
# Object › Expand… dimmed until Expand Appearance (#3089).
Expand Appearance first (Object › Expand Appearance) 先にアピアランスを分割してください(オブジェクト › アピアランスを分割)
Live preview ライブプレビュー
# Align panel menu › Align to Glyph Bounds (#4231).
Point Text ポイント文字
Area Text エリア内文字
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -5671,3 +5671,6 @@ swatch Bright Violet კაშკაშა იისფერი
Line Weight: ხაზის სისქე:
Position: პოზიცია:
Font Size: შრიფტის ზომა:
# Align panel menu › Align to Glyph Bounds (#4231).
Point Text წერტილოვანი ტექსტი
Area Text არის ტექსტი
Can't render this file because it is too large.
+3
View File
@@ -5261,3 +5261,6 @@ axis Y Axis Y축
# Object › Expand… dimmed until Expand Appearance (#3089).
Expand Appearance first (Object › Expand Appearance) 먼저 모양새를 확장하세요 (개체 › 모양새 확장)
Live preview 실시간 미리 보기
# Align panel menu › Align to Glyph Bounds (#4231).
Point Text 포인트 문자
Area Text 영역 문자
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -5631,3 +5631,6 @@ axis Y Axis Y-as
# Object › Expand… dimmed until Expand Appearance (#3089).
Expand Appearance first (Object › Expand Appearance) Breid eerst het uiterlijk uit (Object › Uiterlijk uitbreiden)
Live preview Live voorbeeld
# Align panel menu › Align to Glyph Bounds (#4231).
Point Text Punttekst
Area Text Vlaktekst
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -5536,3 +5536,6 @@ axis Y Axis Oś Y
# Object › Expand… dimmed until Expand Appearance (#3089).
Expand Appearance first (Object › Expand Appearance) Najpierw rozszerz wygląd (Obiekt › Rozszerz wygląd)
Live preview Podgląd na żywo
# Align panel menu › Align to Glyph Bounds (#4231).
Point Text Tekst punktowy
Area Text Tekst akapitowy
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -5599,3 +5599,6 @@ axis Y Axis Eixo Y
# Object › Expand… dimmed until Expand Appearance (#3089).
Expand Appearance first (Object › Expand Appearance) Expanda a aparência primeiro (Objeto › Expandir Aparência)
Live preview Visualização ao vivo
# Align panel menu › Align to Glyph Bounds (#4231).
Point Text Texto de Ponto
Area Text Texto de Área
Can't render this file because it contains an unexpected character in line 6 and column 47.
+3
View File
@@ -5660,3 +5660,6 @@ swatch Yellow Green Желто-зеленый
swatch [None] [Нет]
swatch [Registration] [Приводка]
theme Appearance Оформление
# Align panel menu › Align to Glyph Bounds (#4231).
Point Text Точечный текст
Area Text Текст в области
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -5706,3 +5706,6 @@ axis Y Axis Y-axel
# Object › Expand… dimmed until Expand Appearance (#3089).
Expand Appearance first (Object › Expand Appearance) Expandera utseendet först (Objekt › Expandera utseende)
Live preview Direkt förhandsgranskning
# Align panel menu › Align to Glyph Bounds (#4231).
Point Text Punkttext
Area Text Områdestext
Can't render this file because it contains an unexpected character in line 5 and column 25.
+3
View File
@@ -5640,3 +5640,6 @@ axis Y Axis Y ekseni
# Object › Expand… dimmed until Expand Appearance (#3089).
Expand Appearance first (Object › Expand Appearance) Önce görünümü genişletin (Nesne › Görünümü Genişlet)
Live preview Canlı önizleme
# Align panel menu › Align to Glyph Bounds (#4231).
Point Text Nokta Metni
Area Text Alan Metni
Can't render this file because it contains an unexpected character in line 5 and column 25.
+3
View File
@@ -5278,3 +5278,6 @@ axis Y Axis Вісь Y
# Object › Expand… dimmed until Expand Appearance (#3089).
Expand Appearance first (Object › Expand Appearance) Спочатку розберіть оформлення (Об’єкт › Розібрати оформлення)
Live preview Перегляд у реальному часі
# Align panel menu › Align to Glyph Bounds (#4231).
Point Text Точковий текст
Area Text Текст в області
Can't render this file because it contains an unexpected character in line 178 and column 5.
+3
View File
@@ -5280,3 +5280,6 @@ axis Y Axis Trục Y
# Object › Expand… dimmed until Expand Appearance (#3089).
Expand Appearance first (Object › Expand Appearance) Hãy mở rộng diện mạo trước (Đối tượng › Mở rộng diện mạo)
Live preview Xem trước trực tiếp
# Align panel menu › Align to Glyph Bounds (#4231).
Point Text Chữ điểm
Area Text Chữ vùng
Can't render this file because it contains an unexpected character in line 200 and column 5.
+3
View File
@@ -5521,3 +5521,6 @@ axis Y Axis Y 轴
# Object › Expand… dimmed until Expand Appearance (#3089).
Expand Appearance first (Object › Expand Appearance) 请先扩展外观(对象 › 扩展外观)
Live preview 实时预览
# Align panel menu › Align to Glyph Bounds (#4231).
Point Text 点文字
Area Text 区域文字
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -5317,3 +5317,6 @@ axis Y Axis Y 軸
# Object › Expand… dimmed until Expand Appearance (#3089).
Expand Appearance first (Object › Expand Appearance) 請先展開外觀(物件 › 展開外觀)
Live preview 即時預覽
# Align panel menu › Align to Glyph Bounds (#4231).
Point Text 點狀文字
Area Text 區域文字
Can't render this file because it contains an unexpected character in line 6 and column 47.
+15 -1
View File
@@ -337,7 +337,21 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) {
if menu_item(ui, tl!("Cancel Key Object"), align_to(app) == AlignTo::Key, false) {
app.run("select.key", json!({})).ok();
}
menu_item(ui, tl!("Align to Glyph Bounds"), false, false);
// Indented like the items beside it (their check column).
ui.menu_button(format!(" {}", tl!("Align to Glyph Bounds")), |ui| glyph_bounds_items(app, ui));
}
/// The Align to Glyph Bounds submenu: Point Text and Area Text, each checked while Align and
/// Distribute measure that kind of type by the ink of its glyphs.
fn glyph_bounds_items(app: &mut VectorcraftApp, ui: &mut Ui) {
for (label, key, on) in [
(tl!("Point Text"), "alignGlyphBoundsPoint", app.session.prefs.align_glyph_bounds_point),
(tl!("Area Text"), "alignGlyphBoundsArea", app.session.prefs.align_glyph_bounds_area),
] {
if menu_item(ui, label, true, on) {
super::transform::set_pref(app, key, !on);
}
}
}
#[cfg(test)]
+27
View File
@@ -692,3 +692,30 @@ fn distribute_buttons_space_the_anchor_points_of_one_path() {
let xs: Vec<f64> = node.path_data().unwrap().anchors().map(|(_, _, a)| a.p.x).collect();
assert_eq!(xs, [10.0, 55.0, 100.0], "{}", app.ui.status);
}
/// #4231: the panel menu's Align to Glyph Bounds is a submenu (no longer dimmed) whose Point Text
/// and Area Text each toggle their own preference and show a check while it is on.
#[test]
fn align_to_glyph_bounds_toggles_point_and_area_text() {
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default());
let ctx = egui::Context::default();
crate::theme::install_fonts(&ctx);
assert!(frame_events(&ctx, &mut app, vec![], menu).iter().any(|(t, _)| t.trim() == "Align to Glyph Bounds"));
let checked = |app: &mut VectorcraftApp| {
let texts = frame_events(&ctx, app, vec![], glyph_bounds_items);
texts.iter().filter_map(|(t, _)| t.strip_prefix("✓ ")).map(str::to_string).collect::<Vec<_>>()
};
let choose = |app: &mut VectorcraftApp, label: &str| {
let texts = frame_events(&ctx, app, vec![], glyph_bounds_items);
let r = texts.iter().find(|(t, _)| t.trim_start_matches('✓').trim() == label).unwrap_or_else(|| panic!("{label} in {texts:?}")).1;
click(&ctx, app, r.center(), glyph_bounds_items);
};
let prefs = |app: &VectorcraftApp| (app.session.prefs.align_glyph_bounds_point, app.session.prefs.align_glyph_bounds_area);
assert!(checked(&mut app).is_empty(), "both off by default");
choose(&mut app, "Point Text");
assert_eq!((prefs(&app), checked(&mut app)), ((true, false), vec!["Point Text".to_string()]));
choose(&mut app, "Area Text");
assert_eq!((prefs(&app), checked(&mut app)), ((true, true), vec!["Point Text".to_string(), "Area Text".to_string()]));
choose(&mut app, "Point Text");
assert_eq!((prefs(&app), checked(&mut app)), ((false, true), vec!["Area Text".to_string()]));
}
+3
View File
@@ -456,6 +456,9 @@ Use Preview Bounds: the Align panel flyout's Use Preview Bounds item shows and t
preference (`prefs.set`); while it is on, the bounding box and the Transform panel, Properties and Control bar
fields measure visual bounds.
Align to Glyph Bounds: the Align panel flyout's Align to Glyph Bounds submenu shows and toggles the
`alignGlyphBoundsPoint` (Point Text) and `alignGlyphBoundsArea` (Area Text) preferences (`prefs.set`), checked while on.
Align To: the Align panel's Align To choice is saved with the preferences, and `ui.inspect` reports it as
`ui.align_to` (`"selection"` or `"artboard"`; Align to Key Object stores no choice). The align buttons of the Align
panel, the Control bar and the Properties panel run `object.align` with `to: "artboard"` when one object is selected,
+16
View File
@@ -2938,6 +2938,22 @@ that index, the step gets that document's last artboard. An action recorded in t
{"name":"run_command","arguments":{"command":"select.allOnArtboard","params":{"artboard":1}}}
```
## Align to Glyph Bounds
Align and Distribute normally measure type by its em box (point type) or its frame (area type). With the preference
`alignGlyphBoundsPoint` on (`prefs.set {key: "alignGlyphBoundsPoint", value: true}`; also the Align panel flyout's
Align to Glyph Bounds › Point Text), `object.align`, `object.distribute` and `object.distributeSpacing` measure point
type by the ink of its glyphs instead: lowercase "ace" aligned to the top of a rectangle lines up the tops of its
letters, not the space above them that the em box keeps for capitals and ascenders. `alignGlyphBoundsArea` (Area Text)
does the same for area type. The two are independent and off by default; type on a path, type that draws nothing and
type inside a selected group are measured as before. With Use Preview Bounds on, the glyphs' ink takes in the
characters' strokes. [Align text](https://helpx.adobe.com/illustrator/desktop/design-with-text/edit-format-text/align-text.html).
```json
{"name":"run_command","arguments":{"command":"prefs.set","params":{"values":{"alignGlyphBoundsPoint":true,"alignGlyphBoundsArea":true}}}}
{"name":"run_command","arguments":{"command":"object.align","params":{"vertical":"top"}}}
```
## Use Preview Bounds
With the preference `usePreviewBounds` on (`prefs.set {key: "usePreviewBounds", value: true}`; also the Align panel
+4 -2
View File
@@ -1,6 +1,6 @@
# Parity with Adobe Illustrator
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (Blob Brush and Eraser Tool Options: Size follows the pen pressure, Fixed or Pressure with a Variation (#4590, #3961); scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (Selection, transform & align tools: Align panel menu › Align to Glyph Bounds › Point Text and Area Text align and distribute type by the ink of its glyphs (#4231); scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026)
## Feature submission evidence
@@ -108,7 +108,7 @@ dominate; 3D, graphs and automation are specialist). They sum to 105. Feature de
| Area | Weight | Done | Main gaps | One agent (h) | Kind |
|---|---:|---:|---|---:|---|
| Selection, transform & align tools | 6 | 85% | Direct Selection segment click/Delete corrected (#1278); open-path anchor Delete keeps gaps; Select › Start Global Edit finds the paths, compound paths and symbol instances of the selected object's shape (Appearance, Size and artboard options) and gives an edit of one to all of them in proportion, as one undo step (#3521); remaining: Free Transform/Scale/Reflect along a rotated box, Global Edit of similar groups and excluding a match with Shift-click, transform nuances (no-op anchor Align history regression-tested, #1180 follow-up) | 4–6 | estimated; Global Edit regression-tested (`crates/engine/src/tests/globaledit.rs`; Quick Action, menu label and Escape in `panels::properties::tests::start_global_edit_quick_action_and_escape`) |
| Selection, transform & align tools | 6 | 85% | Direct Selection segment click/Delete corrected (#1278); open-path anchor Delete keeps gaps; Select › Start Global Edit finds the paths, compound paths and symbol instances of the selected object's shape (Appearance, Size and artboard options) and gives an edit of one to all of them in proportion, as one undo step (#3521); the Align panel menu's Align to Glyph Bounds › Point Text and Area Text measure type by its glyphs' ink for Align and Distribute (#4231); remaining: Free Transform/Scale/Reflect along a rotated box, Global Edit of similar groups and excluding a match with Shift-click, transform nuances (no-op anchor Align history regression-tested, #1180 follow-up) | 4–6 | estimated; Global Edit regression-tested (`crates/engine/src/tests/globaledit.rs`; Quick Action, menu label and Escape in `panels::properties::tests::start_global_edit_quick_action_and_escape`) |
| Drawing tools (Pen, Curvature, Pencil, shapes, Shaper) | 7 | 85% | Shaper construction widget, Pencil/Pen nuances, Blob Brush options, Touch Type, Dimension tool; see [ui-parity.md](ui-parity.md) | 8–13 | estimated; Curvature point display regression-tested (#1236, in part); Polygon/Star exact-size count persistence regression-covered; Live Corners' Relative rounding done (#3091), score unchanged |
| Path operations, Pathfinder, Shape Builder, Live Paint | 5 | 80% | Shape Builder on the reporter's file (#937: art past 4,096 segments is cut by lines again; unconfirmed on the file itself), its gap detection and options; Live Paint Gap Options | 4–8 | estimated (lowered from 86%: open issues); compound attributes regression-tested (#1197); single-result source names survive SVG export/import (#1293), with [SVG ID semantics](https://www.w3.org/TR/SVG2/struct.html#IDAttribute) and original-shape tests; Simplify component selection and preview regression-tested (#1272); Simplify of a Direct Selection part of a path regression-tested (`tests/simplify_part.rs`); Divide open-cutting-path geometry and appearance regression-tested (#1307); the Shape Builder's edge-source grid is bounded in memory and files a segment once per cell (`planar.rs` test `rect_index_holds_a_bounded_number_of_references`); live compound shapes (Make, Alt-click, member Shape Modes, Release, Expand, saved and exported) regression-tested (#4522) |
| Colour, swatches, gradients, patterns, mesh, recolor | 7 | 92% | 30.0's gradient dither, perceptual blending and gradient presets; freeform and mixed spot/process gradients in SVG/PDF | 4–7 | estimated (lowered from 96%: target moved); unpainted zero-width stroke restoration fixed (#1253), score and hours unchanged; Eyedropper source reads regression-tested (#1251) |
@@ -653,6 +653,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
The Select menu's Same commands, its Object commands that select art, Next Object Above and Below, saved selections and the Magic Wand (#2655) select only objects that Select All selects and objects inside them: nothing hidden or locked, nothing in a hidden or locked group, layer or sublayer, nothing on a template layer, and in isolation mode nothing outside the isolated group or layer. In isolation mode Select All, All on Active Artboard and Inverse select nothing while the isolated group or layer, or a group or layer it is in, is hidden, locked or a template layer.
Select ▸ Object ▸ Bristle Brush Strokes selects the objects whose topmost stroke has a bristle brush, whatever the brush is named, and leaves out objects whose brush is of another type, also when its name has "Bristle" in it.
Align and Distribute Spacing keep a compound path or selected group that contains the key object in place, as they keep the key object itself.
Align to Glyph Bounds (#4231): the Align panel menu's Align to Glyph Bounds submenu has Point Text and Area Text, each checked while on (`alignGlyphBoundsPoint`, `alignGlyphBoundsArea`, off by default). With one on, Align, Distribute Objects and Distribute Spacing measure that kind of type by the ink of its glyphs instead of its em box (point type) or frame (area type), so lowercase type aligned to a rectangle's top lines up the tops of its letters; the other kind, type on a path and type in a selected group are measured as before, and Use Preview Bounds adds the characters' strokes ([Align text](https://helpx.adobe.com/illustrator/desktop/design-with-text/edit-format-text/align-text.html); `tests::glyphbounds`, `panels::align::tests::align_to_glyph_bounds_toggles_point_and_area_text`). The Character and Properties panels' controls for the same setting are not done.
Anchor Align moves the selected points to their bounds or the artboard (#1180). Repeating it, or moving by at most 1e-9 pt on both axes as whole-object Align does, leaves the document, Undo, Redo and saved state unchanged; actual moves still take one undo step. `crates/engine/src/tests.rs` covers repeated alignment, rounding-only alignment, unchanged handles, saved state, command recording and Undo/Redo.
A symbol instance is measured by its symbol's art, as its transform places it, for clicks, the marquee, the bounding box, the Transform panel, Align and Distribute (the art's strokes too with Use Preview Bounds), Fit to Artwork Bounds, exports of the art, thumbnails and canvas culling; one whose symbol is missing is measured as the 20 × 20 square around the origin.
A selected clip group shows the outline and anchors of its clipping path, not of the art the path clips, and its hover highlight and key object outline follow the clipping path too. That art shows its whole outline when it is selected on its own: with Edit Contents, Direct or Group Selection, or in isolation mode (#2422).
@@ -1201,6 +1202,7 @@ applies to fills). Tests: `color::gradient::tests` (perceptual midpoint, expansi
| Date | Change | Summary |
|---|---|---|
| 2026-10-30 | minor | Selection, transform & align tools: Align panel menu › Align to Glyph Bounds › Point Text and Area Text align and distribute type by the ink of its glyphs (#4231); evidence added; score and hours unchanged (estimated). |
| 2026-10-30 | minor | Blob Brush and Eraser Tool Options: Size follows the pen pressure, Fixed or Pressure with a Variation (#4590, #3961); scores and estimates unchanged |
| 2026-10-30 | minor | Blob Brush Tool Options: Size follows the pen pressure, Fixed or Pressure with a Variation (#4590); scores and estimates unchanged |
| 2026-10-29 | minor | Path operations: Pathfinder: live compound shapes (#4522): the panel menu's Make, Release and Expand Compound Shape, Alt-clicking a Shape Mode, a selected member's Shape Mode and the Expand button act; evidence added; score and hours unchanged (estimated). |
+3 -1
View File
@@ -1,6 +1,6 @@
# UI and interaction parity
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (Blob Brush and Eraser Tool Options: Size follows the pen pressure, Fixed or Pressure with a Variation (#4590, #3961); scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (Align panel menu › Align to Glyph Bounds › Point Text and Area Text align and distribute type by the ink of its glyphs (#4231); scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x)
## Feature submission evidence
@@ -115,6 +115,7 @@ tools, never checked side by side) isn't captured row by row.
| Distribute Objects and Distribute Spacing with a spacing from a key guide: the guide stays put and the other items are spaced the Distribute Spacing value from it, as from a key object with no size | done | Key guide from #3165; a vertical guide spaces the horizontal options, a horizontal one the vertical ones, and a guide along the axis leaves everything |
| Distribute ruler guides: the Distribute Objects and Distribute Spacing buttons space the selected guides across the axis with the selected objects, as items with no size; two or more guides selected alone are enough | done | #1597 in part; guides alone: select them, choose Align to Artboard and click Horizontal (Vertical) Distribute Center for equal columns (rows) ([public forum answer, 2017](https://community.adobe.com/questions-652/illustrator-equal-guides-822924)); with objects: a guide on each artboard edge, included in the selection, keeps the outermost lines there ([public forum answer, 2025](https://www.signs101.com/threads/is-there-a-way-to-distribute-shapes-like-this-in-illustrator.179556/)); a guide has no width, so the three options of an axis are the same for it; guides along the axis stay |
| Ruler guides align to a key object: a click on an object selected with guides makes it the key (one object is enough); Align moves the selected guides to its edge or centre (vertical guides to left, centre or right, horizontal ones to top, centre or bottom) and the key stays put | done | #2143; aligning to the selection or the artboard leaves guides alone |
| Align panel menu › Align to Glyph Bounds › Point Text and Area Text (checked while on): Align and Distribute measure point type or area type by the ink of its glyphs instead of its em box or frame | done | #4231; `alignGlyphBoundsPoint`, `alignGlyphBoundsArea` ([Align text](https://helpx.adobe.com/illustrator/desktop/design-with-text/edit-format-text/align-text.html)); `tests::glyphbounds`, `panels::align::tests::align_to_glyph_bounds_toggles_point_and_area_text`; the Character and Properties panels' Align to Glyph Bounds controls are not done |
| A marquee selects the objects whose paths it touches (anchors or segments): drawn inside a filled shape, over a background, it leaves that shape (Preview and Outline) | done | #1103 |
| Click a segment independently of its endpoint anchors; only its direction handles show | done | #1278: hollow endpoint squares, selected edge emphasized; headless egui checked |
| Drag a curved segment to bend it, a straight one to move its anchors | done | |
@@ -360,6 +361,7 @@ separate. Scores and hours unchanged. [Behavior and command details](development
| Date | Change | Summary |
|---|---|---|
| 2026-10-30 | minor | Align panel menu › Align to Glyph Bounds › Point Text and Area Text align and distribute type by the ink of its glyphs (#4231); the Character and Properties panels' controls for it are not done; scores and estimates unchanged |
| 2026-10-30 | minor | Blob Brush and Eraser Tool Options: Size follows the pen pressure, Fixed or Pressure with a Variation (#4590, #3961); scores and estimates unchanged |
| 2026-10-30 | minor | Blob Brush Tool Options: Size follows the pen pressure, Fixed or Pressure with a Variation (#4590); scores and estimates unchanged |
| 2026-10-29 | minor | Pathfinder: live compound shapes (#4522): the panel menu's Make, Release and Expand Compound Shape, Alt-clicking a Shape Mode, a selected member's Shape Mode and the Expand button act; scores and estimates unchanged |