Merge pull request #6402 from dotCooCoo/feat/text-glyph-bounds

Type: an optional setting measures point type by its glyph outlines
This commit is contained in:
BFlat
2026-11-10 14:14:00 +07:00
committed by GitHub
61 changed files with 680 additions and 33 deletions
Generated
+1
View File
@@ -6499,6 +6499,7 @@ dependencies = [
"ruzstd",
"serde",
"serde_json",
"skrifa 0.47.0",
"thiserror 2.0.21",
"vectorcraft-affinity",
"vectorcraft-apischema",
+1
View File
@@ -750,6 +750,7 @@ fn lerp_text(o: &mut TextObject, x: &TextObject, y: &TextObject, t: f64, tc: f64
if resized {
o.cached_bounds = None;
o.cached_baselines.clear();
o.cached_ink = None;
}
}
+1
View File
@@ -347,6 +347,7 @@ impl Document {
t.first_para_continues = false;
t.cached_bounds = None;
t.cached_baselines.clear();
t.cached_ink = None;
}
let on_path = n.extras().objects_on_path.is_some();
let mut renamed = std::collections::HashMap::new();
+1
View File
@@ -71,6 +71,7 @@ impl Document {
}
if changed {
t.cached_bounds = None;
t.cached_ink = None;
}
changed
}
+1
View File
@@ -236,6 +236,7 @@ impl TypeInks {
text.xf = Affine::new([a, b, c, d, 0.0, 0.0]);
text.cached_bounds = None;
text.cached_baselines.clear();
text.cached_ink = None;
let content = content_hash(&text).map(|hash| (hash, by_node.1, inputs));
match content.and_then(|key| self.content.get(&key)) {
Some(ink) => {
+6
View File
@@ -121,6 +121,12 @@ pub struct TextObject {
/// vertical column's centre line), filled in with `cached_bounds` (not serialized).
#[serde(skip)]
pub cached_baselines: Vec<(Point, Point)>,
/// Cached ink of the glyphs in text space, underline and strikethrough bars included
/// (`vectorcraft_effects::preview::laid_out_ink`, `None` when nothing draws),
/// with the `cached_bounds` it was measured with: filled in with `cached_bounds` by the engine's
/// text edits, and stale once `cached_bounds` differs ([`Self::ink`]). Not serialized.
#[serde(skip)]
pub cached_ink: Option<(Rect, Option<Rect>)>,
}
/// Conversion from stored character attributes to document-space type controls.
+8
View File
@@ -71,6 +71,7 @@ impl TextObject {
self.xf *= Affine::scale_non_uniform(1.0 / sx, 1.0 / sy);
self.cached_bounds = None;
self.cached_baselines.clear();
self.cached_ink = None;
true
}
@@ -97,6 +98,7 @@ impl TextObject {
list_level_counts: Vec::new(),
cached_bounds: None,
cached_baselines: Vec::new(),
cached_ink: None,
}
}
/// The list counts the story's earlier frames carry into this frame at each level
@@ -288,6 +290,12 @@ impl TextObject {
TextKind::Point => Some(self.xf.transform_rect_bbox(self.cached_bounds.unwrap_or_else(|| self.estimate_bounds()))),
}
}
/// The cached ink of the glyphs in text space ([`Self::cached_ink`]): `Some(None)` when nothing
/// draws, and `None` when it was not measured with the current `cached_bounds`.
pub fn ink(&self) -> Option<Option<Rect>> {
let (bounds, ink) = self.cached_ink?;
(self.cached_bounds == Some(bounds)).then_some(ink)
}
/// How far the characters' strokes paint past their glyphs, in document space: they scale
/// with the type.
pub fn char_stroke_outset(&self) -> f64 {
+117
View File
@@ -17,6 +17,8 @@
//! [`Selection::anchor_bounds`] measure the objects themselves. Raster effects (shadows, glows,
//! blurs) are not measured. In one object, the art of at most 4,096 symbol instances is measured
//! with its geometry effects, and the other instances in it are measured without them.
//! [`bounds_of_with`], [`oriented_bounds_with`] and [`anchor_bounds_with`] measure as these do and,
//! with [`Measure::glyphs`], take point type by the ink of its glyphs (Measure Type by Glyph Bounds).
use std::cell::{Cell, RefCell};
use std::collections::HashMap;
@@ -74,6 +76,121 @@ pub fn anchor_bounds(selection: &Selection, doc: &Document, preview: bool) -> Op
union_opt(bounds_of(doc, &selection.whole(), preview), selection.anchors_box(doc))
}
/// How [`bounds_of_with`], [`oriented_bounds_with`] and [`anchor_bounds_with`] measure objects:
/// `preview` as in [`bounds_of`], and with `glyphs` (Preferences › Type › Measure Type by Glyph
/// Bounds), point type, horizontal or vertical, by the ink of its glyphs instead of its em box
/// ([`text_ink_in`]).
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Measure {
pub preview: bool,
pub glyphs: bool,
}
impl Measure {
/// See [`by_glyphs`].
fn by_glyphs(self, drawn: &Drawn, n: &Node) -> bool {
self.glyphs
&& matches!(&n.kind, NodeKind::Text(t) if matches!(t.kind, vectorcraft_doc::TextKind::Point))
&& n.projection().is_none()
&& !(self.preview && drawn.draws_geometry(n))
}
}
/// Whether `m` measures `n` by the ink of its glyphs: point type, not in perspective, and with
/// `preview` only when no geometry effect changes what it draws.
pub fn by_glyphs(symbols: &SymbolBoxes, n: &Node, m: Measure) -> bool {
m.by_glyphs(&Drawn::new(symbols), n)
}
/// [`bounds_of`] measured by `m`.
pub fn bounds_of_with(doc: &Document, ids: &[NodeId], m: Measure) -> Option<Rect> {
if !m.glyphs {
return bounds_of(doc, ids, m.preview);
}
let symbols = doc.symbol_boxes();
let drawn = Drawn::new(&symbols);
if !ids.iter().filter_map(|id| doc.node(*id)).any(|n| m.by_glyphs(&drawn, n)) {
return bounds_of(doc, ids, m.preview);
}
ids.iter().filter_map(|id| doc.node(*id)).fold(None, |acc, n| union_opt(acc, measured_in(&drawn, n, Affine::IDENTITY, m)))
}
/// [`oriented_bounds`] measured by `m`.
pub fn oriented_bounds_with(doc: &Document, ids: &[NodeId], m: Measure) -> Option<OrientedBox> {
if !m.glyphs {
return oriented_bounds(doc, ids, m.preview);
}
let symbols = doc.symbol_boxes();
let drawn = Drawn::new(&symbols);
if !ids.iter().filter_map(|id| doc.node(*id)).any(|n| m.by_glyphs(&drawn, n)) {
return oriented_bounds(doc, ids, m.preview);
}
let angle = doc.bbox_angle(ids);
let to_local = if angle == 0.0 { Affine::IDENTITY } else { OrientedBox { angle, rect: Rect::ZERO }.to_doc().inverse() };
let rect = ids.iter().filter_map(|id| doc.node(*id)).fold(None, |acc, n| union_opt(acc, measured_in(&drawn, n, to_local, m)))?;
Some(OrientedBox { angle, rect })
}
/// [`anchor_bounds`] with the objects selected whole measured by `m`.
pub fn anchor_bounds_with(selection: &Selection, doc: &Document, m: Measure) -> Option<Rect> {
if selection.anchors.is_empty() {
return None;
}
union_opt(bounds_of_with(doc, &selection.whole(), m), selection.anchors_box(doc))
}
/// The box of `n` as `a` would place it, without changing it, measured by `m`: [`bounds_in`] with
/// `preview`, else [`Node::bounds_in`] of its geometric bounds, and point type by its glyphs as
/// [`Measure`] says.
pub fn bounds_in_with(symbols: &SymbolBoxes, n: &Node, a: Affine, m: Measure) -> Option<Rect> {
measured_in(&Drawn::new(symbols), n, a, m)
}
/// See [`bounds_in_with`].
fn measured_in(drawn: &Drawn, n: &Node, a: Affine, m: Measure) -> Option<Rect> {
if m.by_glyphs(drawn, n)
&& let NodeKind::Text(t) = &n.kind
&& let Some(r) = text_ink_in(t, a)
{
// The fills' and characters' strokes paint around the glyphs, as in `Node::visual_bounds`.
let o = if m.preview { n.appearance.outset().max(t.char_stroke_outset()) } else { 0.0 };
return Some(r.inflate(o, o));
}
match (m.preview, a == Affine::IDENTITY) {
(true, _) => drawn.next().bounds_in(n, a),
(false, true) => n.geometric_bounds_with(drawn.symbols),
(false, false) => n.bounds_in(drawn.symbols, a, false),
}
}
/// The box of the ink of type `t`'s glyphs with its text space mapped by `a * t.xf`: the cached
/// ink ([`vectorcraft_doc::TextObject::ink`]) when that map keeps a rectangle square, else the
/// glyph outlines and the underline and strikethrough bars mapped point by point
/// ([`vectorcraft_text::envelope_ink`]). `None` when nothing draws (no text, only spaces without
/// underline or strikethrough, or glyphs without outlines).
pub fn text_ink_in(t: &vectorcraft_doc::TextObject, a: Affine) -> Option<Rect> {
let m = a * t.xf;
let [p, q, r, s, _, _] = m.as_coeffs();
let square = (q.abs() <= 1e-12 && r.abs() <= 1e-12) || (p.abs() <= 1e-12 && s.abs() <= 1e-12);
if square {
let ink = match t.ink() {
Some(ink) => ink,
None => vectorcraft_text::envelope_ink(t, Affine::IDENTITY, None),
};
return ink.map(|i| m.transform_rect_bbox(i));
}
vectorcraft_text::envelope_ink(t, m, None)
}
/// The ink of type `t` laid out as `layout`, in text space: its glyphs
/// ([`vectorcraft_text::TextLayout::ink_bounds`]) and its underline and strikethrough bars
/// ([`vectorcraft_text::decorations`]), what [`text_ink_in`] measures. `None` when nothing draws.
pub fn laid_out_ink(layout: &vectorcraft_text::TextLayout, t: &vectorcraft_doc::TextObject) -> Option<Rect> {
vectorcraft_text::decorations(layout, vectorcraft_text::FontDb::global(), t)
.into_iter()
.fold(layout.ink_bounds(), |acc, (_, bar)| union_opt(acc, Some(bar.bounding_box())))
}
/// The visual bounds of `n` as drawn: those of its [`drawn_art`], else
/// [`Node::visual_bounds_with`].
pub fn visual_bounds(symbols: &SymbolBoxes, n: &Node) -> Option<Rect> {
+2
View File
@@ -88,3 +88,5 @@ zune-jpegxl = { workspace = true }
zune-core = { workspace = true }
# The JavaScript parser's lexer, compared with the script checker's on the same scripts.
vectorcraft-js = { workspace = true }
# A bundled font's own glyph boxes, for the glyph bounds tests.
skrifa = { workspace = true }
@@ -275,6 +275,7 @@ fn same_art(a: &NodeKind, b: &NodeKind) -> bool {
moved.xf = a.xf;
moved.cached_bounds = a.cached_bounds;
moved.cached_baselines.clone_from(&a.cached_baselines);
moved.cached_ink = a.cached_ink;
**a == moved
}
}
+12 -5
View File
@@ -6,16 +6,23 @@ use super::{
use vectorcraft_doc::{NodeKind, TextKind};
impl Session {
/// The bounds of `ids` the Transform panel, the bounding box, Align and Distribute measure:
/// with Use Preview Bounds on, visual bounds as drawn (strokes and geometry effects included,
/// see [`effects::preview`]), else geometric bounds.
/// The bounds of `ids` the Transform panel and the bounding box measure: with Use Preview
/// Bounds on, visual bounds as drawn (strokes and geometry effects included, see
/// [`effects::preview`]), else geometric bounds; with Measure Type by Glyph Bounds on, point
/// type by the ink of its glyphs ([`Session::bounds_measure`]).
pub fn transform_bounds(&self, ids: &[NodeId]) -> Option<Rect> {
effects::preview::bounds_of(&self.doc().ok()?.doc, ids, self.prefs.use_preview_bounds)
effects::preview::bounds_of_with(&self.doc().ok()?.doc, ids, self.bounds_measure())
}
/// [`Session::transform_bounds`] square to the objects' own angle (their rotated bounding
/// box, see [`Document::oriented_bounds`]).
pub fn transform_box(&self, ids: &[NodeId]) -> Option<OrientedBox> {
effects::preview::oriented_bounds(&self.doc().ok()?.doc, ids, self.prefs.use_preview_bounds)
effects::preview::oriented_bounds_with(&self.doc().ok()?.doc, ids, self.bounds_measure())
}
/// How the bounding box, the Transform panel and `object.setBounds` measure objects: Use
/// Preview Bounds, and point type by its glyphs with Measure Type by Glyph Bounds
/// (`typeGlyphBounds`).
pub fn bounds_measure(&self) -> effects::preview::Measure {
effects::preview::Measure { preview: self.prefs.use_preview_bounds, glyphs: self.prefs.type_glyph_bounds }
}
}
+23 -12
View File
@@ -59,24 +59,31 @@ fn bounds_transform(s: &Session, p: &Value, ids: Vec<NodeId>, least: f64, anchor
let st = s.doc()?;
check_switches(p, &TRANSFORM_SWITCHES, "object.setBounds")?;
check_switches(p, &["proportional"], "object.setBounds")?;
let how = s.bounds_measure();
if anchors
&& transforms_objects(p)
&& !bool_or(p, "copy", false)
&& let Some(b) = effects::preview::anchor_bounds(&st.selection, &st.doc, s.prefs.use_preview_bounds)
&& let Some(b) = effects::preview::anchor_bounds_with(&st.selection, &st.doc, how)
{
return Ok(Bounds::Anchors(ids, b));
}
let d = &s.doc()?.doc;
// Sizes are measured square to the bounding box, which a rotation turns with the objects:
// the math below runs in its frame.
let frame = d.oriented_bounds(&ids, false).ok_or_else(|| EngineError::Other("no bounds".into()))?;
// the math below runs in its frame. With Measure Type by Glyph Bounds, point type is measured
// by its glyphs' ink, which a transform of the type moves and scales with it.
let geometric = effects::preview::Measure { preview: false, ..how };
let frame = effects::preview::oriented_bounds_with(d, &ids, geometric).ok_or_else(|| EngineError::Other("no bounds".into()))?;
let g = frame.rect;
let symbols = d.symbol_boxes();
let by_glyphs = how.glyphs && ids.iter().filter_map(|id| d.node(*id)).any(|n| effects::preview::by_glyphs(&symbols, n, how));
// With Use Preview Bounds the values measure the visual box as drawn, whose margins around the
// geometric box (left, top, right, bottom) scale with the strokes or stay. The margins of
// `vs`, the visual box without the strokes in symbols' art, scale; those strokes keep their
// weight when an instance scales, so the rest of the margins stay.
// weight when an instance scales, so the rest of the margins stay. Type measured by its glyphs
// is measured as drawn after each scale tried instead (below).
let (v, vs) = if s.prefs.use_preview_bounds {
(effects::preview::oriented_bounds(d, &ids, true).map_or(g, |b| b.rect), scaling_box(d, &ids, &frame).unwrap_or(g))
let v = effects::preview::oriented_bounds_with(d, &ids, how).map_or(g, |b| b.rect);
(v, if by_glyphs { v } else { scaling_box(d, &ids, &frame).unwrap_or(g) })
} else {
(g, g)
};
@@ -125,16 +132,20 @@ fn bounds_transform(s: &Session, p: &Value, ids: Vec<NodeId>, least: f64, anchor
// The scales and the point above can miss the size or the reference point when the margins of
// the objects differ, the members of groups and the objects in symbols' art included, and with
// strokes that scale, when the sizes are out of proportion. The objects are then measured one
// by one ([`Spans::measure`]), and the scales solved for. Objects with geometry effects, and
// by one ([`Spans::measure`]), and the scales solved for. Objects with geometry effects,
// objects with a butt-capped stroke along a segment at a slant, whose margins change with the
// segment's direction, are measured as drawn after each scale tried instead ([`refit_drawn`]).
let symbols = d.symbol_boxes();
// segment's direction, and type measured by its glyphs, are measured as drawn after each scale
// tried instead ([`refit_drawn`]).
let to_local = frame.to_doc().inverse();
let drawn = || {
ids.iter()
.filter_map(|id| d.node(*id))
.any(|n| effects::preview::draws_geometry(&symbols, n) || n.has_slanted_butt_stroke(&symbols, to_local))
by_glyphs
|| ids
.iter()
.filter_map(|id| d.node(*id))
.any(|n| effects::preview::draws_geometry(&symbols, n) || n.has_slanted_butt_stroke(&symbols, to_local))
};
// Without Use Preview Bounds the box has no margins (`v`, `vs` and `g` are one box, glyph ink
// included), so the scales above are exact and nothing is refitted.
let solved = if !(s.prefs.use_preview_bounds && w >= 0.0 && h >= 0.0) {
None
} else if transforms_objects(p) && !bool_or(p, "typeAreas", false) && drawn() {
@@ -145,7 +156,7 @@ fn bounds_transform(s: &Session, p: &Value, ids: Vec<NodeId>, least: f64, anchor
ids.iter().filter_map(|id| d.node(*id)).fold(None, |acc, n| {
let mut n = n.clone();
n.transform(xf, sc);
vectorcraft_geom::union_opt(acc, effects::preview::bounds_in(&symbols, &n, to_local))
vectorcraft_geom::union_opt(acc, effects::preview::bounds_in_with(&symbols, &n, to_local, how))
})
};
// The scales above keep the effects' reach as a margin that stays, and give 0 for a size
+1 -1
View File
@@ -287,7 +287,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Set Bounds",
[],
None,
"{x?, y?, width?, height?, reference?: 0..8 (9-point grid; a number past 8 is read as 8), proportional?, strokes?, corners?, copy?: bool, patterns?: bool (Transform Patterns), typeAreas?: bool (area type: the frame is reshaped and the text reflows at its size), objects?: bool (Transform Objects, as object.move)} (Transform panel; width/height and the reference point follow the bounding box, rotated with rotated objects; x, y are page coordinates; with Use Preview Bounds the values measure the visual bounds; strokes/corners as object.scale; with anchor points selected (Direct Selection) and no copy, the values are the box around those points, square to the page, and only they move or scale, as with object.move (#1824))",
"{x?, y?, width?, height?, reference?: 0..8 (9-point grid; a number past 8 is read as 8), proportional?, strokes?, corners?, copy?: bool, patterns?: bool (Transform Patterns), typeAreas?: bool (area type: the frame is reshaped and the text reflows at its size), objects?: bool (Transform Objects, as object.move)} (Transform panel; width/height and the reference point follow the bounding box, rotated with rotated objects; x, y are page coordinates; with Use Preview Bounds the values measure the visual bounds; with the typeGlyphBounds preference, each point type object selected as an object (not inside a selected group) is measured by its glyphs' ink (underline and strikethrough included), which the scale gives the size asked for; strokes/corners as object.scale; with anchor points selected (Direct Selection) and no copy, the values are the box around those points, square to the page, and only they move or scale, as with object.move (#1824))",
has_selection,
set_bounds
),
+1 -1
View File
@@ -12,7 +12,7 @@ fn origin_of(s: &Session, p: &Value, ids: &[NodeId]) -> Result<Point> {
// Direct-selected anchors transform alone (see [`apply_transform`]), about their own centre.
let st = s.doc()?;
if !bool_or(p, "copy", false)
&& let Some(b) = effects::preview::anchor_bounds(&st.selection, &st.doc, s.prefs.use_preview_bounds)
&& let Some(b) = effects::preview::anchor_bounds_with(&st.selection, &st.doc, s.bounds_measure())
{
return Ok(b.center());
}
+2
View File
@@ -435,6 +435,8 @@ pub const PREF_SPECS: &[PrefSpec] = &[
// The Align panel menu's Align to Glyph Bounds: 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),
// The bounding box and the Transform panel measure point type by its glyphs' ink (also in the Transform panel menu).
p!("typeGlyphBounds", "Type", "Glyph Bounds", "Measure Type by Glyph Bounds", bool),
// Artboard Options › Global: the canvas darkens the area outside the artboards while the Artboard tool is active.
p!("fadeRegionOutsideArtboard", "General", "Artboard Options", "Fade Region Outside Artboard", bool),
p!("artboardUpdateWhileDragging", "General", "Artboard Options", "Update While Dragging", bool),
+2
View File
@@ -1611,6 +1611,7 @@ fn create_in_path(s: &mut Session, p: &Value) -> Result<Value> {
list_level_counts: Vec::new(),
cached_bounds: None,
cached_baselines: Vec::new(),
cached_ink: None,
};
super::create::new_type_alignment(s, p, C, &mut t)?;
if !on_path {
@@ -1901,6 +1902,7 @@ fn headline_tracking(t: &TextObject, target: f64) -> Option<f64> {
list_level_counts: Vec::new(),
cached_bounds: None,
cached_baselines: Vec::new(),
cached_ink: None,
};
for r in &mut h.runs {
r.style.tracking = tr;
+2
View File
@@ -304,6 +304,7 @@ pub(crate) fn refresh_bounds(t: &mut TextObject) {
let lay = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), t);
t.cached_bounds = Some(lay.bounds);
t.cached_baselines = lay.baselines();
t.cached_ink = Some((lay.bounds, vectorcraft_render::effects::preview::laid_out_ink(&lay, t)));
}
/// [`refresh_bounds`] for a document that is opening: the first frame takes the layout
@@ -315,6 +316,7 @@ pub(crate) fn refresh_bounds_for_first_frame(t: &mut TextObject) {
let lay = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), t);
t.cached_bounds = Some(lay.bounds);
t.cached_baselines = lay.baselines();
t.cached_ink = Some((lay.bounds, vectorcraft_render::effects::preview::laid_out_ink(&lay, t)));
vectorcraft_text::keep_layout(t, stamp, lay);
}
+7
View File
@@ -41,6 +41,12 @@ pub struct Prefs {
/// Align panel menu › Align to Glyph Bounds › Area Text: the same for area type, instead of
/// its frame.
pub align_glyph_bounds_area: bool,
/// Preferences › Type › Glyph Bounds › Measure Type by Glyph Bounds (also in the Transform
/// panel menu): the bounding box, the Transform panel's, Control bar's and Properties panel's X,
/// Y, W and H, and `object.setBounds` measure each point type object selected as an object (not
/// inside a selected group) by the ink of its glyphs instead of its em box
/// ([`crate::Session::bounds_measure`]).
pub type_glyph_bounds: bool,
/// Artboard Options › Fade Region Outside Artboard: while the Artboard tool is active, the
/// canvas draws everything outside the artboards darker, the pasteboard and the art on it.
pub fade_region_outside_artboard: bool,
@@ -372,6 +378,7 @@ impl Default for Prefs {
use_preview_bounds: false,
align_glyph_bounds_point: false,
align_glyph_bounds_area: false,
type_glyph_bounds: false,
fade_region_outside_artboard: true,
artboard_update_while_dragging: true,
display_print_size: false,
+1
View File
@@ -186,6 +186,7 @@ mod transformfx;
mod transparencygrid;
mod trap;
mod typearea;
mod typeglyphbounds;
mod typescale;
mod typeundo;
mod units;
+325
View File
@@ -0,0 +1,325 @@
//! Preferences › Type › Measure Type by Glyph Bounds (`typeGlyphBounds`): the bounding box, the
//! Transform panel's values and `object.setBounds` measure each point type object selected as an
//! object (not inside a selected group) by the ink of its glyphs instead of its em box.
use serde_json::{Value, json};
use vectorcraft_doc::{NodeId, NodeKind, OrientedBox, TextObject};
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 new document holding point type made by `text.create` with `params`, selected.
fn with_type(params: Value) -> (Session, NodeId) {
let mut s = Session::new();
run(&mut s, "file.new", json!({"width": 800, "height": 800}));
let t = id(run(&mut s, "text.create", params));
run(&mut s, "select.set", json!({"ids": [t.0]}));
(s, t)
}
fn glyphs(s: &mut Session, on: bool) {
run(s, "prefs.set", json!({"values": {"typeGlyphBounds": on}}));
}
fn text(s: &Session, id: NodeId) -> TextObject {
let NodeKind::Text(t) = &s.doc().unwrap().doc.node(id).unwrap().kind else { panic!("{id:?} is not type") };
(**t).clone()
}
/// The ink of the type's glyphs on the page, laid out afresh (not from the cache).
fn ink(s: &Session, id: NodeId) -> Rect {
let t = text(s, id);
t.xf.transform_rect_bbox(vectorcraft_text::layout(vectorcraft_text::FontDb::global(), &t).ink_bounds().unwrap())
}
/// The em box (the line box) of point type: what the bounding box measures by default.
fn em(s: &Session, id: NodeId) -> Rect {
s.doc().unwrap().doc.node(id).unwrap().geometric_bounds().unwrap()
}
fn near(a: Rect, b: Rect, tol: f64) -> bool {
(a.x0 - b.x0).abs() <= tol && (a.y0 - b.y0).abs() <= tol && (a.x1 - b.x1).abs() <= tol && (a.y1 - b.y1).abs() <= tol
}
fn undo_len(s: &Session) -> usize {
s.doc().unwrap().history.undo.len()
}
/// The glyph box of `c` in Inter Regular, in font units (x min, y min, x max, y max), and the
/// font's units per em, read from the font file's own `head`, `cmap`, `loca` and `glyf` tables.
fn inter_glyph_box(c: char) -> ([f64; 4], f64) {
use skrifa::MetadataProvider;
use skrifa::raw::TableProvider;
let font = skrifa::FontRef::new(include_bytes!("../../../../assets/fonts/Inter-Regular.ttf")).unwrap();
let upem = f64::from(font.head().unwrap().units_per_em());
let gid = font.charmap().map(c).unwrap();
let glyph = font.loca(None).unwrap().get_glyf(gid, &font.glyf().unwrap()).unwrap().unwrap();
([glyph.x_min(), glyph.y_min(), glyph.x_max(), glyph.y_max()].map(f64::from), upem)
}
/// "H" in Inter at 100 pt is drawn with straight lines only, so the box of its outline is the glyph
/// box the font file stores: the bounding box takes its size and place from the font itself.
#[test]
fn point_type_measures_the_glyph_box_the_font_file_stores() {
let (mut s, t) = with_type(json!({"x": 100, "y": 300, "size": 100, "font": "Inter", "text": "H"}));
glyphs(&mut s, true);
let ([x0, y0, x1, y1], upem) = inter_glyph_box('H');
let k = 100.0 / upem;
let b = s.transform_box(&[t]).unwrap();
assert_eq!(b.angle, 0.0);
// Text space runs down the page from the baseline at y = 300.
let want = Rect::new(100.0 + x0 * k, 300.0 - y1 * k, 100.0 + x1 * k, 300.0 - y0 * k);
assert!(near(b.rect, want, 1e-6), "{:?} != {want:?}", b.rect);
assert!(near(s.transform_bounds(&[t]).unwrap(), want, 1e-6));
// The em box is taller than the cap height.
assert!(em(&s, t).height() > b.rect.height() + 10.0, "em box {:?}", em(&s, t));
}
#[test]
fn off_by_default_the_box_is_the_em_box() {
let (mut s, t) = with_type(json!({"x": 100, "y": 300, "size": 100, "text": "Hg"}));
assert!(!s.prefs.type_glyph_bounds);
assert_eq!(s.transform_box(&[t]).unwrap().rect, em(&s, t));
glyphs(&mut s, true);
assert!(near(s.transform_box(&[t]).unwrap().rect, ink(&s, t), 1e-9));
glyphs(&mut s, false);
assert_eq!(s.transform_box(&[t]).unwrap().rect, em(&s, t));
}
/// A typed height scales the type so its ink takes that height, in one undo step, and the type
/// stays type.
#[test]
fn set_bounds_gives_the_ink_the_typed_size() {
let (mut s, t) = with_type(json!({"x": 100, "y": 300, "size": 100, "text": "Hg"}));
glyphs(&mut s, true);
let xf = text(&s, t).xf;
let n = undo_len(&s);
run(&mut s, "object.setBounds", json!({"height": 50}));
assert_eq!(undo_len(&s), n + 1);
assert!((ink(&s, t).height() - 50.0).abs() < 1e-6, "{:?}", ink(&s, t));
assert!(near(s.transform_box(&[t]).unwrap().rect, ink(&s, t), 1e-6));
assert!(matches!(s.doc().unwrap().doc.node(t).unwrap().kind, NodeKind::Text(_)));
run(&mut s, "edit.undo", json!({}));
assert_eq!(text(&s, t).xf, xf);
}
/// A typed X moves the ink box's reference point there and keeps its size; a typed width gives
/// the ink that width.
#[test]
fn set_bounds_moves_and_sizes_the_ink_box() {
let (mut s, t) = with_type(json!({"x": 100, "y": 300, "size": 100, "text": "Hg"}));
glyphs(&mut s, true);
let before = ink(&s, t);
run(&mut s, "object.setBounds", json!({"x": 400, "reference": 0}));
let moved = ink(&s, t);
assert!((moved.x0 - 400.0).abs() < 1e-6 && (moved.y0 - before.y0).abs() < 1e-6, "{moved:?}");
assert!((moved.width() - before.width()).abs() < 1e-6 && (moved.height() - before.height()).abs() < 1e-6, "{moved:?}");
run(&mut s, "object.setBounds", json!({"width": 123}));
assert!((ink(&s, t).width() - 123.0).abs() < 1e-6, "{:?}", ink(&s, t));
assert!((ink(&s, t).height() - before.height()).abs() < 1e-6, "{:?}", ink(&s, t));
}
#[test]
fn set_bounds_with_the_current_values_records_no_undo_step() {
let (mut s, t) = with_type(json!({"x": 100, "y": 300, "size": 100, "text": "Hg"}));
glyphs(&mut s, true);
let b = s.transform_box(&[t]).unwrap().rect;
let n = undo_len(&s);
run(&mut s, "object.setBounds", json!({"x": b.center().x, "y": b.center().y, "width": b.width(), "height": b.height()}));
assert_eq!(undo_len(&s), n);
assert_eq!(s.transform_box(&[t]).unwrap().rect, b);
}
/// A proportional width scales both sides of the ink by the same factor, about the reference
/// point of the ink box (here its top-left corner).
#[test]
fn proportional_width_keeps_the_ink_in_proportion_about_the_reference_point() {
let (mut s, t) = with_type(json!({"x": 100, "y": 300, "size": 100, "text": "Hg"}));
glyphs(&mut s, true);
let before = ink(&s, t);
run(&mut s, "object.setBounds", json!({"width": before.width() * 2.0, "proportional": true, "reference": 0}));
let after = ink(&s, t);
assert!((after.width() - before.width() * 2.0).abs() < 1e-6, "{after:?}");
assert!((after.height() - before.height() * 2.0).abs() < 1e-6, "{after:?}");
assert!((after.x0 - before.x0).abs() < 1e-6 && (after.y0 - before.y0).abs() < 1e-6, "{before:?} -> {after:?}");
}
/// Rotated type keeps its own angle, and the box is the size of the ink before the rotation. A
/// skewed object's box is the box of its glyph outlines, not of its mapped ink rectangle.
#[test]
fn rotated_and_skewed_type_measure_their_outlines() {
let (mut s, flat) = with_type(json!({"x": 100, "y": 300, "size": 100, "text": "Hg"}));
glyphs(&mut s, true);
let size = s.transform_box(&[flat]).unwrap().rect.size();
let t = id(run(&mut s, "text.create", json!({"x": 300, "y": 300, "size": 100, "text": "Hg", "rotation": 30})));
let b = s.transform_box(&[t]).unwrap();
assert_eq!(b.angle, s.doc().unwrap().doc.oriented_bounds(&[t], false).unwrap().angle);
assert!(b.angle != 0.0);
assert!((b.rect.width() - size.width).abs() < 1e-6 && (b.rect.height() - size.height).abs() < 1e-6, "{b:?} vs {size:?}");
let o = id(run(&mut s, "text.create", json!({"x": 100, "y": 600, "size": 100, "text": "o"})));
run(&mut s, "select.set", json!({"ids": [o.0]}));
run(&mut s, "object.transform", json!({"matrix": [1, 0, 0.6, 1, -360, 0]}));
let b = s.transform_box(&[o]).unwrap();
let tx = text(&s, o);
let lay = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), &tx);
let to_local = OrientedBox { angle: b.angle, rect: Rect::ZERO }.to_doc().inverse();
let want = lay.ink_bounds_in(to_local * tx.xf).unwrap();
assert!(near(b.rect, want, 1e-9), "{:?} != {want:?}", b.rect);
let mapped = (to_local * tx.xf).transform_rect_bbox(lay.ink_bounds().unwrap());
assert!(mapped.width() > b.rect.width() + 1.0, "a round letter's outline is narrower than its slanted box");
}
/// Type that draws nothing is measured by its em box.
#[test]
fn empty_and_space_only_type_keep_the_em_box() {
for text in ["", " "] {
let (mut s, t) = with_type(json!({"x": 100, "y": 300, "size": 40, "text": text}));
glyphs(&mut s, true);
assert_eq!(s.transform_box(&[t]).unwrap().rect, em(&s, t), "{text:?}");
}
}
/// Area type keeps its frame, and type on a path its path.
#[test]
fn area_type_and_type_on_a_path_are_measured_as_before() {
let mut s = Session::new();
run(&mut s, "file.new", json!({"width": 800, "height": 800}));
let area = id(run(&mut s, "text.create", json!({"x": 100, "y": 100, "size": 24, "text": "ace", "area": {"width": 200, "height": 100}})));
let line = id(run(&mut s, "shape.line", json!({"x1": 100, "y1": 400, "x2": 500, "y2": 400})));
let path = id(run(&mut s, "text.createInPath", json!({"path": line.0, "mode": "onPath", "text": "ace", "size": 24})));
for t in [area, path] {
let off = s.transform_box(&[t]).unwrap();
glyphs(&mut s, true);
assert_eq!(s.transform_box(&[t]).unwrap(), off, "{t:?}");
glyphs(&mut s, false);
}
}
#[test]
fn vertical_point_type_measures_its_ink() {
let (mut s, t) = with_type(json!({"x": 100, "y": 100, "size": 60, "text": "ace", "vertical": true}));
glyphs(&mut s, true);
let b = s.transform_box(&[t]).unwrap().rect;
assert!(near(b, ink(&s, t), 1e-9), "{b:?} != {:?}", ink(&s, t));
assert!(em(&s, t).width() > b.width() + 1.0, "em box {:?}", em(&s, t));
}
/// With Use Preview Bounds, the characters' strokes paint around the glyphs.
#[test]
fn preview_bounds_add_the_character_strokes_to_the_ink() {
let (mut s, t) = with_type(json!({"x": 100, "y": 300, "size": 100, "text": "Hg"}));
run(&mut s, "text.setStyle", json!({"ids": [t.0], "stroke": "#000000", "strokeWidth": 6}));
run(&mut s, "prefs.set", json!({"values": {"typeGlyphBounds": true, "usePreviewBounds": true}}));
let o = text(&s, t).char_stroke_outset();
assert!(o > 0.0);
let b = s.transform_box(&[t]).unwrap().rect;
assert!(near(b, ink(&s, t).inflate(o, o), 1e-9), "{b:?}");
// A typed height is the height with the strokes.
run(&mut s, "object.setBounds", json!({"height": 80}));
assert!((s.transform_box(&[t]).unwrap().rect.height() - 80.0).abs() < 1e-6, "{:?}", s.transform_box(&[t]));
}
/// Type inside a selected group is measured as before: the group's box does not change.
#[test]
fn a_group_holding_type_keeps_its_box() {
let (mut s, _) = with_type(json!({"x": 100, "y": 300, "size": 100, "text": "Hg"}));
let g = id(run(&mut s, "object.group", json!({})));
let off = s.transform_box(&[g]).unwrap();
glyphs(&mut s, true);
assert_eq!(s.transform_box(&[g]).unwrap(), off);
}
/// Runs in two fonts: the box covers the ink of both, and follows an edit of the type.
#[test]
fn mixed_fonts_measure_the_ink_of_every_run() {
let (mut s, t) = with_type(json!({"x": 100, "y": 300, "size": 100, "text": "Hgxo"}));
glyphs(&mut s, true);
run(&mut s, "text.setRangeStyle", json!({"id": t.0, "font": "Source Serif 4", "start": 2, "end": 4}));
let tx = text(&s, t);
assert!(tx.runs.iter().any(|r| r.style.font_family == "Source Serif 4") && tx.runs.len() > 1, "{:?}", tx.runs);
assert!(tx.ink().is_some(), "the edit refreshed the cached ink");
assert!(near(s.transform_box(&[t]).unwrap().rect, ink(&s, t), 1e-9));
}
/// A selection of type and a shape is measured by the type's ink and the shape's bounds.
#[test]
fn a_mixed_selection_takes_the_ink_and_the_shapes_bounds() {
let (mut s, t) = with_type(json!({"x": 100, "y": 300, "size": 100, "text": "Hg"}));
let r = id(run(&mut s, "shape.rectangle", json!({"x": 500, "y": 500, "width": 50, "height": 50})));
glyphs(&mut s, true);
assert!(near(s.transform_bounds(&[t, r]).unwrap(), ink(&s, t).union(Rect::new(500.0, 500.0, 550.0, 550.0)), 1e-9));
}
#[test]
fn the_preference_is_checked_and_saved() {
let mut s = Session::new();
assert!(s.execute("prefs.set", &json!({"values": {"typeGlyphBounds": "yes"}})).is_err());
assert!(!s.prefs.type_glyph_bounds);
assert!(!serde_json::from_value::<Prefs>(json!({})).unwrap().type_glyph_bounds);
run(&mut s, "prefs.set", json!({"key": "typeGlyphBounds", "value": true}));
let saved = serde_json::to_value(&s.prefs).unwrap();
assert_eq!(saved["typeGlyphBounds"], json!(true));
assert!(serde_json::from_value::<Prefs>(saved).unwrap().type_glyph_bounds);
assert_eq!(run(&mut s, "prefs.get", json!({"key": "typeGlyphBounds"})), json!(true));
}
/// The preference is not part of the document: a saved document is the same with it on or off.
#[test]
fn saved_documents_do_not_depend_on_the_preference() {
let (mut s, _) = with_type(json!({"x": 100, "y": 300, "size": 100, "text": "Hg"}));
let off = serde_json::to_string(&s.doc().unwrap().doc).unwrap();
assert!(!off.contains("cached_ink") && !off.contains("cachedInk"), "the cached ink is not saved");
glyphs(&mut s, true);
assert_eq!(serde_json::to_string(&s.doc().unwrap().doc).unwrap(), off);
}
/// What the type draws on the page: its glyphs and its underline and strikethrough bars.
fn drawn(s: &Session, id: NodeId) -> Rect {
let t = text(s, id);
vectorcraft_text::envelope_ink(&t, t.xf, None).unwrap()
}
/// Underline and strikethrough bars are drawn with the type, so the box takes them in and a typed
/// height gives what is drawn that height. Underlined spaces are measured by their bar.
#[test]
fn underline_and_strikethrough_bars_count_as_ink() {
let (mut s, t) = with_type(json!({"x": 100, "y": 300, "size": 100, "text": "H"}));
run(&mut s, "text.setRangeStyle", json!({"id": t.0, "start": 0, "end": 1, "underline": true}));
glyphs(&mut s, true);
let b = s.transform_box(&[t]).unwrap().rect;
assert!(near(b, drawn(&s, t), 1e-9), "{b:?} != {:?}", drawn(&s, t));
assert!(b.y1 > ink(&s, t).y1 + 1.0, "the underline hangs below the glyph");
run(&mut s, "object.setBounds", json!({"height": 50}));
assert!((drawn(&s, t).height() - 50.0).abs() < 1e-6, "{:?}", drawn(&s, t));
assert!(near(s.transform_box(&[t]).unwrap().rect, drawn(&s, t), 1e-6));
let (mut s, t) = with_type(json!({"x": 100, "y": 300, "size": 40, "text": " "}));
run(&mut s, "text.setRangeStyle", json!({"id": t.0, "start": 0, "end": 3, "strikethrough": true}));
glyphs(&mut s, true);
let b = s.transform_box(&[t]).unwrap().rect;
assert!(near(b, drawn(&s, t), 1e-9), "{b:?} != {:?}", drawn(&s, t));
assert!(em(&s, t).height() > b.height() + 10.0, "em box {:?}", em(&s, t));
}
/// A transform of type keeps its cached ink, which is in text space.
#[test]
fn the_cached_ink_is_the_laid_out_ink() {
let (mut s, t) = with_type(json!({"x": 100, "y": 300, "size": 100, "text": "Hg"}));
let tx = text(&s, t);
assert_eq!(tx.ink(), Some(vectorcraft_text::layout(vectorcraft_text::FontDb::global(), &tx).ink_bounds()));
run(&mut s, "object.transform", json!({"matrix": [2, 0, 0, 3, 0, 0]}));
assert_eq!(text(&s, t).ink(), tx.ink());
// A cache measured with other bounds is not read.
let mut stale = text(&s, t);
stale.cached_bounds = Some(Rect::new(0.0, 0.0, 1.0, 1.0));
assert_eq!(stale.ink(), None);
}
+1
View File
@@ -365,6 +365,7 @@ impl Session {
freeform_point: self.selected_freeform_point(),
raster_sample: self.prefs.eyedropper.sample_size,
preview_bounds: self.prefs.use_preview_bounds,
glyph_bounds: self.prefs.type_glyph_bounds,
preview_memo: Some(&self.preview_memo),
unit,
stroke_unit,
+9
View File
@@ -178,6 +178,10 @@ pub struct ToolContext<'a> {
pub raster_sample: u32,
/// General → Use Preview Bounds: the bounding box measures visual bounds (strokes included).
pub preview_bounds: bool,
/// Type › Measure Type by Glyph Bounds: the bounding box measures each point type object
/// selected as an object (not inside a selected group) by the ink of its glyphs
/// ([`vectorcraft_effects::preview::Measure`]).
pub glyph_bounds: bool,
/// The session's memo of the selection tools' measurements for the document as it is
/// ([`select::PreviewMemo`]); without one they measure on every call.
pub preview_memo: Option<&'a select::PreviewMemo>,
@@ -284,6 +288,11 @@ pub struct ToolContext<'a> {
}
impl ToolContext<'_> {
/// How the bounding box measures the selection: [`Self::preview_bounds`] and
/// [`Self::glyph_bounds`].
pub fn bounds_measure(&self) -> vectorcraft_effects::preview::Measure {
vectorcraft_effects::preview::Measure { preview: self.preview_bounds, glyphs: self.glyph_bounds }
}
/// Tolerance in document units for `px` screen pixels.
pub fn tol(&self, px: f64) -> f64 {
px / self.zoom.max(1e-9)
+2 -2
View File
@@ -1437,7 +1437,7 @@ impl DirectSelectionTool {
// them, an edge on a gridline on each axis), not that point (#3835).
let grid = segment && (cx.snap_to_grid || cx.snap_to_pixel);
self.partial_grid =
grid.then(|| MoveSnap::grid_only(vectorcraft_effects::preview::anchor_bounds(cx.selection, cx.doc, cx.preview_bounds)));
grid.then(|| MoveSnap::grid_only(vectorcraft_effects::preview::anchor_bounds_with(cx.selection, cx.doc, cx.bounds_measure())));
}
let mut d = move_delta(start, p, ev.mods.shift, cx.constrain_angle);
let unsnapped = d;
@@ -1474,7 +1474,7 @@ impl DirectSelectionTool {
self.partial_snap = (!cx.selection.anchors.is_empty()).then(|| PointSnap::new(cx, || Targets::for_anchor_drag(cx.doc, cx.selection)));
let grid = self.partial_snap.is_some() && grab.is_none() && (cx.snap_to_grid || cx.snap_to_pixel);
self.partial_grid =
grid.then(|| MoveSnap::grid_only(vectorcraft_effects::preview::anchor_bounds(cx.selection, cx.doc, cx.preview_bounds)));
grid.then(|| MoveSnap::grid_only(vectorcraft_effects::preview::anchor_bounds_with(cx.selection, cx.doc, cx.bounds_measure())));
let copy = ev.mods.alt && self.partial_snap.is_none();
out.push(Action::Begin(if copy { "Copy".into() } else { "Move".into() }));
self.state = State::MoveObject { start, grab, hold, began: true, deselect: None };
+1
View File
@@ -536,6 +536,7 @@ pub(crate) mod testutil {
freeform_point: None,
raster_sample: 1,
preview_bounds: false,
glyph_bounds: false,
preview_memo: None,
unit: Unit::Points,
stroke_unit: Unit::Points,
+8 -7
View File
@@ -419,7 +419,7 @@ struct MemoState {
struct Measured {
revision: (u64, u64),
objects: Vec<NodeId>,
preview: bool,
measure: vectorcraft_effects::preview::Measure,
bounds: Option<Option<Rect>>,
oriented: Option<Option<OrientedBox>>,
by_object: HashMap<NodeId, Option<Option<Rect>>>,
@@ -427,7 +427,7 @@ struct Measured {
impl Measured {
fn is_for(&self, cx: &ToolContext) -> bool {
self.revision == cx.revision && self.preview == cx.preview_bounds && self.objects == cx.selection.objects
self.revision == cx.revision && self.measure == cx.bounds_measure() && self.objects == cx.selection.objects
}
}
@@ -450,7 +450,7 @@ impl PreviewMemo {
let fresh = || Measured {
revision: cx.revision,
objects: cx.selection.objects.clone(),
preview: cx.preview_bounds,
measure: cx.bounds_measure(),
bounds: None,
oriented: None,
by_object: HashMap::new(),
@@ -487,15 +487,16 @@ fn remembered<T: Clone>(cx: &ToolContext, slot: impl Fn(&mut Measured) -> &mut O
}
/// Selection bounds used for the bounding box: visual bounds as drawn with Use Preview Bounds
/// ([`vectorcraft_effects::preview`]), else geometric bounds.
/// ([`vectorcraft_effects::preview`]), else geometric bounds, with point type measured by its
/// glyphs with Measure Type by Glyph Bounds ([`ToolContext::bounds_measure`]).
pub fn selection_bounds(cx: &ToolContext) -> Option<Rect> {
remembered(cx, |m| &mut m.bounds, || vectorcraft_effects::preview::bounds_of(cx.doc, &cx.selection.objects, cx.preview_bounds))
remembered(cx, |m| &mut m.bounds, || vectorcraft_effects::preview::bounds_of_with(cx.doc, &cx.selection.objects, cx.bounds_measure()))
}
/// [`selection_bounds`] square to the selection's own angle: the bounding box the Selection tool
/// shows and drags (rotated after a rotation).
pub fn selection_box(cx: &ToolContext) -> Option<OrientedBox> {
remembered(cx, |m| &mut m.oriented, || vectorcraft_effects::preview::oriented_bounds(cx.doc, &cx.selection.objects, cx.preview_bounds))
remembered(cx, |m| &mut m.oriented, || vectorcraft_effects::preview::oriented_bounds_with(cx.doc, &cx.selection.objects, cx.bounds_measure()))
}
/// [`selection_box`] while the canvas shows it (View › Show Bounding Box, and no anchors selected
@@ -527,7 +528,7 @@ fn object_bounds(cx: &ToolContext, id: NodeId) -> Option<Rect> {
/// which compares the whole selection: a walk of every selected object checking it each time takes
/// time in the square of their number.
fn with_object_bounds<R>(cx: &ToolContext, walk: impl FnOnce(&mut dyn FnMut(NodeId) -> Option<Rect>) -> R) -> R {
let mut measure = |id: NodeId| vectorcraft_effects::preview::bounds_of(cx.doc, &[id], cx.preview_bounds);
let mut measure = |id: NodeId| vectorcraft_effects::preview::bounds_of_with(cx.doc, &[id], cx.bounds_measure());
let Some(mut st) = cx.preview_memo.and_then(|memo| memo.0.try_borrow_mut().ok()) else {
return walk(&mut measure);
};
+24
View File
@@ -2255,6 +2255,30 @@ fn the_preview_memo_measures_once_per_revision_selection_and_preview_bounds() {
assert_eq!(selection_bounds(&c).map(|r| r.x1), Some(250.0));
}
/// With Measure Type by Glyph Bounds, the bounding box of selected point type is the ink of its
/// glyphs; the memo measures again when only that setting changes.
#[test]
fn the_bounding_box_of_point_type_follows_measure_type_by_glyph_bounds() {
use vectorcraft_doc::{CharStyle, Document, Node, NodeKind, TextObject};
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() });
let ink = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), &text).ink_bounds().unwrap();
d.insert(Some(l), 0, Node::new(id, NodeKind::Text(Box::new(text.clone())))).unwrap();
let (mut s, p, memo) = (Selection::default(), paint(), PreviewMemo::default());
s.set([id]);
let at = |glyph_bounds| {
let c = ToolContext { revision: (1, 1), glyph_bounds, preview_memo: Some(&memo), ..cx(&d, &s, &p) };
(selection_box(&c).map(|b| b.rect), object_bounds(&c, id))
};
let em = d.node(id).and_then(|n| n.geometric_bounds());
assert_eq!(at(false), (em, em));
let glyphs = Some(text.xf.transform_rect_bbox(ink));
assert_eq!(at(true), (glyphs, glyphs));
assert_ne!(glyphs, em);
assert_eq!(memo.measurements(), 4, "the same revision measured again for the setting");
}
/// A double-click on a symbol instance edits its symbol in symbol editing mode (`symbol.edit`), and
/// one outside the symbol's art in that mode leaves it, as it leaves isolation mode. An instance
/// inside the art of the symbol being edited is not entered.
+1 -1
View File
@@ -118,7 +118,7 @@ impl TransformTool {
return Some(*p);
}
// Anchors selected with Direct Selection transform alone, about their own centre.
vectorcraft_effects::preview::anchor_bounds(cx.selection, cx.doc, cx.preview_bounds)
vectorcraft_effects::preview::anchor_bounds_with(cx.selection, cx.doc, cx.bounds_measure())
.map(|b| b.center())
.or_else(|| selection_box(cx).map(|b| b.center()))
}
+3 -2
View File
@@ -137,13 +137,14 @@ impl VectorcraftApp {
/// frame: it is measured once per revision.
pub fn selection_box(&mut self) -> Option<vectorcraft_doc::OrientedBox> {
let st = self.session.active()?;
let key = (st.uid, st.revision, self.session.prefs.use_preview_bounds);
let measure = self.session.bounds_measure();
let key = (st.uid, st.revision, measure);
if let Some((k, b)) = self.canvas.selection_box
&& k == key
{
return b;
}
let b = match vectorcraft_effects::preview::anchor_bounds(&st.selection, &st.doc, self.session.prefs.use_preview_bounds) {
let b = match vectorcraft_effects::preview::anchor_bounds_with(&st.selection, &st.doc, measure) {
Some(r) => Some(vectorcraft_doc::OrientedBox::aligned(r)),
None => self.session.transform_box(&st.selection.objects),
};
+3 -2
View File
@@ -51,8 +51,9 @@ pub struct CanvasCache {
/// The selected objects' outlines, anchors and handles as drawn in the last frame. A frame that
/// does not draw them drops them ([`canvas::show`]).
pub selection_marks: canvas::SelectionMarks,
/// [`VectorcraftApp::selection_box`] for (document uid, revision, Use Preview Bounds).
pub selection_box: Option<((u64, u64, bool), Option<vectorcraft_doc::OrientedBox>)>,
/// [`VectorcraftApp::selection_box`] for (document uid, revision, Use Preview Bounds and
/// Measure Type by Glyph Bounds).
pub selection_box: Option<((u64, u64, vectorcraft_effects::preview::Measure), Option<vectorcraft_doc::OrientedBox>)>,
/// [`VectorcraftApp::selection_bounds`] for (document uid, revision).
pub selection_bounds: Option<((u64, u64), Option<vectorcraft_geom::Rect>)>,
/// [`VectorcraftApp::content_bounds`] for (document uid, revision).
+2
View File
@@ -5895,6 +5895,8 @@ axis Y Axis Y ос
# Align panel menu › Align to Glyph Bounds (#4231).
Point Text Точков текст
Area Text Текст в зона
# Transform panel menu › Measure Type by Glyph Bounds.
Measure Type by Glyph Bounds Измерване на текста по границите на глифа
# Character panel menu › Show Snap to Glyph Options (#4232).
Show Snap to Glyph Options Показване на опциите за прилепване към глиф
Hide Snap to Glyph Options Скриване на опциите за прилепване към глиф
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -5951,6 +5951,8 @@ axis Y Axis Y osa
Live preview Pregled uživo
Point Text Tačkasti tekst
Area Text Tekst u oblasti
# Transform panel menu › Measure Type by Glyph Bounds.
Measure Type by Glyph Bounds Mjeri tekst po granicama glifa
Show Snap to Glyph Options Prikaži opcije prianjanja uz glif
Hide Snap to Glyph Options Sakrij opcije prianjanja uz glif
Snap to Glyph Options Opcije prianjanja uz glif
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -6067,6 +6067,8 @@ theme Appearance Aparença
# Align panel menu › Align to Glyph Bounds (#4231).
Point Text Text de punt
Area Text Text d’àrea
# Transform panel menu › Measure Type by Glyph Bounds.
Measure Type by Glyph Bounds Mesura el text pels límits del glif
# Character panel menu › Show Snap to Glyph Options (#4232).
Show Snap to Glyph Options Mostra les opcions d’ajust al glif
Hide Snap to Glyph Options Amaga les opcions d’ajust al glif
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -6104,6 +6104,8 @@ axis Y Axis osa Y
# Align panel menu › Align to Glyph Bounds (#4231).
Point Text Bodový text
Area Text Text v oblasti
# Transform panel menu › Measure Type by Glyph Bounds.
Measure Type by Glyph Bounds Měřit text podle hranic glyfů
# Character panel menu › Show Snap to Glyph Options (#4232).
Show Snap to Glyph Options Zobrazit možnosti přichytávání ke glyfům
Hide Snap to Glyph Options Skrýt možnosti přichytávání ke glyfům
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -5910,6 +5910,8 @@ axis Y Axis Y-akse
# Align panel menu › Align to Glyph Bounds (#4231).
Point Text Punkttekst
Area Text Områdetekst
# Transform panel menu › Measure Type by Glyph Bounds.
Measure Type by Glyph Bounds Mål tekst efter glyfafgrænsning
# Character panel menu › Show Snap to Glyph Options (#4232).
Show Snap to Glyph Options Vis indstillinger for fastgørelse til tegn
Hide Snap to Glyph Options Skjul indstillinger for fastgørelse til tegn
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -5687,6 +5687,8 @@ axis Y Axis Y-Achse
# Align panel menu › Align to Glyph Bounds (#4231).
Point Text Punkttext
Area Text Flächentext
# Transform panel menu › Measure Type by Glyph Bounds.
Measure Type by Glyph Bounds Text an Glyphenbegrenzung messen
# Character panel menu › Show Snap to Glyph Options (#4232).
Show Snap to Glyph Options Optionen für „An Glyphe ausrichten“ einblenden
Hide Snap to Glyph Options Optionen für „An Glyphe ausrichten“ ausblenden
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -6055,6 +6055,8 @@ axis Y Axis Άξονας Y
# Align panel menu › Align to Glyph Bounds (#4231).
Point Text Κείμενο σημείου
Area Text Κείμενο περιοχής
# Transform panel menu › Measure Type by Glyph Bounds.
Measure Type by Glyph Bounds Μέτρηση κειμένου με βάση τα όρια γλυφής
# Character panel menu › Show Snap to Glyph Options (#4232).
Show Snap to Glyph Options Εμφάνιση επιλογών προσκόλλησης σε γλυφή
Hide Snap to Glyph Options Απόκρυψη επιλογών προσκόλλησης σε γλυφή
Can't render this file because it is too large.
+2
View File
@@ -6046,6 +6046,8 @@ axis Y Axis Eje Y
# Align panel menu › Align to Glyph Bounds (#4231).
Point Text Texto de punto
Area Text Texto de área
# Transform panel menu › Measure Type by Glyph Bounds.
Measure Type by Glyph Bounds Medir el texto por los límites del glifo
# Character panel menu › Show Snap to Glyph Options (#4232).
Show Snap to Glyph Options Mostrar opciones de ajuste a glifo
Hide Snap to Glyph Options Ocultar opciones de ajuste a glifo
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -6134,6 +6134,8 @@ axis Y Axis Y-akseli
# Align panel menu › Align to Glyph Bounds (#4231).
Point Text Pisteteksti
Area Text Alueteksti
# Transform panel menu › Measure Type by Glyph Bounds.
Measure Type by Glyph Bounds Mittaa teksti merkin rajoista
# Character panel menu › Show Snap to Glyph Options (#4232).
Show Snap to Glyph Options Näytä merkkiin kohdistuksen asetukset
Hide Snap to Glyph Options Piilota merkkiin kohdistuksen asetukset
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -6099,6 +6099,8 @@ axis Y Axis Axe Y
# Align panel menu › Align to Glyph Bounds (#4231).
Point Text Texte de point
Area Text Texte captif
# Transform panel menu › Measure Type by Glyph Bounds.
Measure Type by Glyph Bounds Mesurer le texte d’après les limites du glyphe
# Character panel menu › Show Snap to Glyph Options (#4232).
Show Snap to Glyph Options Afficher les options de magnétisme du glyphe
Hide Snap to Glyph Options Masquer les options de magnétisme du glyphe
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -394,6 +394,7 @@ antiAlias Sharp शार्प
Area / Path Type एरिया / पाथ टाइप
Area Graph Tool एरिया ग्राफ़ टूल
Area Text एरिया टेक्स्ट
Measure Type by Glyph Bounds टाइप को ग्लिफ़ की सीमाओं से मापें
Area Text Objects एरिया टेक्स्ट ऑब्जेक्ट
Area Type एरिया टाइप
Area Type Options एरिया टाइप ऑप्शन्स
Can't render this file because it is too large.
+2
View File
@@ -6087,6 +6087,8 @@ axis Y Axis Y tengely
# Align panel menu › Align to Glyph Bounds (#4231).
Point Text Pontszöveg
Area Text Területszöveg
# Transform panel menu › Measure Type by Glyph Bounds.
Measure Type by Glyph Bounds Szöveg mérése a glifahatárok szerint
# Character panel menu › Show Snap to Glyph Options (#4232).
Show Snap to Glyph Options Glifához illesztés beállításainak megjelenítése
Hide Snap to Glyph Options Glifához illesztés beállításainak elrejtése
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -5910,6 +5910,8 @@ axis Y Axis Sumbu Y
# Align panel menu › Align to Glyph Bounds (#4231).
Point Text Teks titik
Area Text Teks area
# Transform panel menu › Measure Type by Glyph Bounds.
Measure Type by Glyph Bounds Ukur teks menurut batas glif
# Character panel menu › Show Snap to Glyph Options (#4232).
Show Snap to Glyph Options Tampilkan opsi snap ke glif
Hide Snap to Glyph Options Sembunyikan opsi snap ke glif
Can't render this file because it contains an unexpected character in line 2308 and column 5.
+2
View File
@@ -5721,6 +5721,8 @@ axis Y Axis Asse Y
# Align panel menu › Align to Glyph Bounds (#4231).
Point Text Testo per punti
Area Text Testo in area
# Transform panel menu › Measure Type by Glyph Bounds.
Measure Type by Glyph Bounds Misura il testo in base ai limiti del glifo
# Character panel menu › Show Snap to Glyph Options (#4232).
Show Snap to Glyph Options Mostra opzioni di aggancio a glifo
Hide Snap to Glyph Options Nascondi opzioni di aggancio a glifo
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -5786,6 +5786,8 @@ axis Y Axis Y 軸
# Align panel menu › Align to Glyph Bounds (#4231).
Point Text ポイント文字
Area Text エリア内文字
# Transform panel menu › Measure Type by Glyph Bounds.
Measure Type by Glyph Bounds 文字を字形の境界で測定
# Character panel menu › Show Snap to Glyph Options (#4232).
Show Snap to Glyph Options グリフにスナップのオプションを表示
Hide Snap to Glyph Options グリフにスナップのオプションを隠す
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -6100,6 +6100,8 @@ swatch Bright Violet კაშკაშა იისფერი
# Align panel menu › Align to Glyph Bounds (#4231).
Point Text წერტილოვანი ტექსტი
Area Text არის ტექსტი
# Transform panel menu › Measure Type by Glyph Bounds.
Measure Type by Glyph Bounds ტექსტის გაზომვა გლიფის საზღვრებით
# Character panel menu › Show Snap to Glyph Options (#4232).
Show Snap to Glyph Options გლიფზე მიბმის პარამეტრების ჩვენება
Hide Snap to Glyph Options გლიფზე მიბმის პარამეტრების დამალვა
Can't render this file because it is too large.
+2
View File
@@ -5682,6 +5682,8 @@ axis Y Axis Y축
# Align panel menu › Align to Glyph Bounds (#4231).
Point Text 포인트 문자
Area Text 영역 문자
# Transform panel menu › Measure Type by Glyph Bounds.
Measure Type by Glyph Bounds 글리프 경계로 문자 측정
# Character panel menu › Show Snap to Glyph Options (#4232).
Show Snap to Glyph Options 글리프에 스냅 옵션 표시
Hide Snap to Glyph Options 글리프에 스냅 옵션 숨기기
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -5961,6 +5961,8 @@ axis Y Axis Y-as
# Align panel menu › Align to Glyph Bounds (#4231).
Point Text Punttekst
Area Text Vlaktekst
# Transform panel menu › Measure Type by Glyph Bounds.
Measure Type by Glyph Bounds Tekst meten op glyphbegrenzing
# Character panel menu › Show Snap to Glyph Options (#4232).
Show Snap to Glyph Options Opties voor uitlijnen op glyph tonen
Hide Snap to Glyph Options Opties voor uitlijnen op glyph verbergen
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -5958,6 +5958,8 @@ axis Y Axis Oś Y
# Align panel menu › Align to Glyph Bounds (#4231).
Point Text Tekst punktowy
Area Text Tekst akapitowy
# Transform panel menu › Measure Type by Glyph Bounds.
Measure Type by Glyph Bounds Mierz tekst według granic glifów
# Character panel menu › Show Snap to Glyph Options (#4232).
Show Snap to Glyph Options Pokaż opcje przyciągania do glifów
Hide Snap to Glyph Options Ukryj opcje przyciągania do glifów
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -6019,6 +6019,8 @@ axis Y Axis Eixo Y
# Align panel menu › Align to Glyph Bounds (#4231).
Point Text Texto de Ponto
Area Text Texto de Área
# Transform panel menu › Measure Type by Glyph Bounds.
Measure Type by Glyph Bounds Medir Texto pelos Limites do Glifo
# Character panel menu › Show Snap to Glyph Options (#4232).
Show Snap to Glyph Options Mostrar Opções de Ajuste ao Glifo
Hide Snap to Glyph Options Ocultar Opções de Ajuste ao Glifo
Can't render this file because it contains an unexpected character in line 6 and column 47.
+2
View File
@@ -6081,6 +6081,8 @@ theme Appearance Оформление
# Align panel menu › Align to Glyph Bounds (#4231).
Point Text Точечный текст
Area Text Текст в области
# Transform panel menu › Measure Type by Glyph Bounds.
Measure Type by Glyph Bounds Измерять текст по границам глифа
# Character panel menu › Show Snap to Glyph Options (#4232).
Show Snap to Glyph Options Показать параметры привязки к глифу
Hide Snap to Glyph Options Скрыть параметры привязки к глифу
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -6127,6 +6127,8 @@ axis Y Axis Y-axel
# Align panel menu › Align to Glyph Bounds (#4231).
Point Text Punkttext
Area Text Områdestext
# Transform panel menu › Measure Type by Glyph Bounds.
Measure Type by Glyph Bounds Mät text efter glyfgränser
# Character panel menu › Show Snap to Glyph Options (#4232).
Show Snap to Glyph Options Visa alternativ för fäst mot glyf
Hide Snap to Glyph Options Dölj alternativ för fäst mot glyf
Can't render this file because it contains an unexpected character in line 5 and column 25.
+2
View File
@@ -6060,6 +6060,8 @@ axis Y Axis Y ekseni
# Align panel menu › Align to Glyph Bounds (#4231).
Point Text Nokta Metni
Area Text Alan Metni
# Transform panel menu › Measure Type by Glyph Bounds.
Measure Type by Glyph Bounds Yazıyı Glif Sınırlarına Göre Ölç
# Character panel menu › Show Snap to Glyph Options (#4232).
Show Snap to Glyph Options Glif'e Yapışma Seçeneklerini Göster
Hide Snap to Glyph Options Glif'e Yapışma Seçeneklerini Gizle
Can't render this file because it contains an unexpected character in line 5 and column 25.
+2
View File
@@ -5701,6 +5701,8 @@ axis Y Axis Вісь Y
# Align panel menu › Align to Glyph Bounds (#4231).
Point Text Точковий текст
Area Text Текст в області
# Transform panel menu › Measure Type by Glyph Bounds.
Measure Type by Glyph Bounds Вимірювати текст за межами гліфа
# Character panel menu › Show Snap to Glyph Options (#4232).
Show Snap to Glyph Options Показати параметри прив’язки до гліфа
Hide Snap to Glyph Options Приховати параметри прив’язки до гліфа
Can't render this file because it contains an unexpected character in line 182 and column 5.
+2
View File
@@ -5702,6 +5702,8 @@ axis Y Axis Trục Y
# Align panel menu › Align to Glyph Bounds (#4231).
Point Text Chữ điểm
Area Text Chữ vùng
# Transform panel menu › Measure Type by Glyph Bounds.
Measure Type by Glyph Bounds Đo chữ theo giới hạn glyph
# Character panel menu › Show Snap to Glyph Options (#4232).
Show Snap to Glyph Options Hiện tùy chọn hút vào glyph
Hide Snap to Glyph Options Ẩn tùy chọn hút vào glyph
Can't render this file because it contains an unexpected character in line 204 and column 5.
+2
View File
@@ -5942,6 +5942,8 @@ axis Y Axis Y 轴
# Align panel menu › Align to Glyph Bounds (#4231).
Point Text 点文字
Area Text 区域文字
# Transform panel menu › Measure Type by Glyph Bounds.
Measure Type by Glyph Bounds 按字形边界度量文字
# Character panel menu › Show Snap to Glyph Options (#4232).
Show Snap to Glyph Options 显示靠齐字符选项
Hide Snap to Glyph Options 隐藏靠齐字符选项
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -5739,6 +5739,8 @@ axis Y Axis Y 軸
# Align panel menu › Align to Glyph Bounds (#4231).
Point Text 點狀文字
Area Text 區域文字
# Transform panel menu › Measure Type by Glyph Bounds.
Measure Type by Glyph Bounds 依字符邊界測量文字
# Character panel menu › Show Snap to Glyph Options (#4232).
Show Snap to Glyph Options 顯示靠齊字符選項
Hide Snap to Glyph Options 隱藏靠齊字符選項
Can't render this file because it contains an unexpected character in line 6 and column 47.
+5
View File
@@ -576,6 +576,11 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) {
if menu_item(ui, tl!("Scale Strokes & Effects"), true, ss) {
set_pref(app, "scaleStrokes", !ss);
}
// The panel's values and the bounding box measure point type by its glyphs' ink.
let glyphs = app.session.prefs.type_glyph_bounds;
if menu_item(ui, tl!("Measure Type by Glyph Bounds"), true, glyphs) {
set_pref(app, "typeGlyphBounds", !glyphs);
}
ui.separator();
// What the panel transforms (Transform Object Only / Pattern Only / Both), one checked.
let current = pattern_mode(app);
@@ -32,6 +32,26 @@ fn use_preview_bounds_shows_the_visual_size_and_checks_the_align_flyout() {
assert!(texts(&mut app, crate::panels::align::menu).iter().any(|t| t == "✓ Use Preview Bounds"));
}
/// The panel menu's Measure Type by Glyph Bounds is checked while `typeGlyphBounds` is on, and the
/// bounding box of selected point type then measures its glyphs without a change to the document.
#[test]
fn measure_type_by_glyph_bounds_is_checked_and_resizes_the_box() {
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default());
app.run("file.new", json!({"width": 300, "height": 300})).unwrap();
app.run("text.create", json!({"x": 20, "y": 200, "size": 100, "text": "Hg"})).unwrap();
assert!(texts(&mut app, menu).iter().any(|t| t == " Measure Type by Glyph Bounds"));
let em = app.selection_box().unwrap().rect;
let revision = app.session.active().unwrap().revision;
set_pref(&mut app, "typeGlyphBounds", true);
assert!(app.session.prefs.type_glyph_bounds);
assert!(texts(&mut app, menu).iter().any(|t| t == "✓ Measure Type by Glyph Bounds"));
assert_eq!(app.session.active().unwrap().revision, revision);
let glyphs = app.selection_box().unwrap().rect;
assert!(glyphs.height() < em.height() - 10.0, "{glyphs:?} inside {em:?}");
set_pref(&mut app, "typeGlyphBounds", false);
assert_eq!(app.selection_box().unwrap().rect, em);
}
#[test]
fn use_preview_bounds_shows_the_size_after_geometry_effects() {
// A 100 × 50 rectangle without a stroke, with Offset Path 10: 120 × 70 as drawn.
+5
View File
@@ -529,6 +529,11 @@ 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.
Measure Type by Glyph Bounds: the Transform panel menu's Measure Type by Glyph Bounds item shows and toggles the
`typeGlyphBounds` preference (`prefs.set`), checked while on; while it is on, the bounding box and the Transform
panel, Properties and Control bar fields measure each point type object selected as an object (not inside a selected
group) by its glyphs' ink.
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,
+29
View File
@@ -3999,6 +3999,35 @@ an error that lists the choices of a `horizontal`, `vertical`, `to`, `axis` or `
(`object.reflect` also takes an angle and names the value given), and one for a `spacing` that is not a number; nothing
moves.
## Glyph bounds for type
The bounding box normally measures point type by its em box, which leaves room above and below the letters for
ascenders, descenders and capitals. With the preference `typeGlyphBounds` on (`prefs.set {key: "typeGlyphBounds",
value: true}`; also Preferences › Type › Glyph Bounds and the Transform panel menu's Measure Type by Glyph Bounds), the
bounding box and its handles, the Transform panel's, Control bar's and Properties panel's X, Y, W and H,
`object.setBounds`, the Scale, Rotate and Reflect tools' reference point, and Swap Objects, Replace with Top Object,
Set Size of Each and Sort Objects measure point type, horizontal or vertical, by the ink of its glyphs: its glyph
outlines and its underline and strikethrough bars. This applies to each point type object that is selected as an object
(not inside a selected group), in a selection with other objects too. The option is VectorCraft's own and is off by
default.
A width or height typed with the option on scales the type until its ink has that size. The type stays live type: its
font size changes by the scale, so `object.setBounds {height: 72}` on "H" gives the cap height 72 pt, not a 72 pt
font. With Use Preview Bounds on, the ink takes in the characters' strokes, and type with geometry effects is
measured as drawn. Rotated type keeps its own angle, and skewed type is measured by its glyph outlines. Type that
draws nothing (no text, or only spaces without underline or strikethrough), area type, type on a path and type inside
a selected group are measured as before. Arrange in Grid and Group Overlapping Objects measure visual bounds as
before, and Align and Distribute follow Align to Glyph Bounds.
When moved type snaps (Snap to Grid, Snap to Pixel, Smart Guides and ruler guides), the edges and center of its ink box
snap, and the Distance Guides measure from that box. The other objects that it snaps to keep their usual boxes, as
they do with Use Preview Bounds. The preference is not saved in documents.
```json
{"name":"run_command","arguments":{"command":"prefs.set","params":{"values":{"typeGlyphBounds":true}}}}
{"name":"run_command","arguments":{"command":"object.setBounds","params":{"height":72,"proportional":true}}}
```
## Selection preferences
The Selection & Anchor Display and General preferences apply to `pointer_gesture` as they do to the mouse:
+1
View File
@@ -1061,6 +1061,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
- A butt cap ends a stroke square at the end of an open path, and a projecting cap extends half the weight past it. VectorCraft's visual box of a butt-capped stroke on an open path does not grow past the path's ends: a straight segment's box comes from the corners of its stroked rectangle, and a curve's box comes from the stroke at its ends and where its tangent is square to an axis. Round and projecting caps and arrowheads still reach past an end. A curve that bends more tightly than half the weight can measure a little larger than it paints, and a butt-dashed path whose first or last dash is a gap is measured to the path's ends. Public reference: [Apply stroke on an object](https://helpx.adobe.com/illustrator/using/stroke-object.html).
- A butt-capped curve whose first or last handle sits on its end point also stops at that end: a straight 50 pt curve with a 2 pt stroke and its first handle on its start point measures 50 pt tall, not 51 pt (doc `reach::tests::a_butt_curve_with_a_handle_on_its_end_point_stops_near_that_end`).
- With Use Preview Bounds on, the bounding box, the Transform panel, Align, Distribute, Distribute Spacing and `object.setBounds` measure each object after its geometry effects. A path is measured by its effected shape, and a fill or stroke with geometry effects of its own is measured by the shape it paints. Type, images, live objects and symbol instances are measured by their reshaped art, and a symbol instance is also measured by its reshaped art when its symbol's art has geometry effects. A group or layer with geometry or Pathfinder effects, or with a fill or stroke of its own that has geometry effects or paints members with geometry effects, is measured by its evaluated art. A blend, envelope or repeat whose objects have geometry effects is measured by its evaluated art, with those objects measured after their effects. A 100 pt square without a stroke, with Offset Path 10, measures 120 pt, and type with Outline Object aligns and sizes by its glyphs. Raster effects are not measured. Public reference: [Create text](https://web.archive.org/web/20220127090642/https://helpx.adobe.com/illustrator/using/creating-text.html) (archived copy).
- VectorCraft's own Measure Type by Glyph Bounds (`typeGlyphBounds`, off by default; Preferences › Type and the Transform panel menu): the bounding box, the Transform panel's, Control bar's and Properties panel's X, Y, W and H, `object.setBounds`, Swap Objects, Replace with Top Object, Set Size of Each and Sort Objects measure each point type object selected as an object by the ink of its glyphs, underline and strikethrough included, instead of its em box. A typed W or H scales the live type until its ink has that size, and the font size changes by the scale. Moved type snaps by its ink box. Type that draws nothing, area type, type on a path and type in a selected group are measured as before, and Align and Distribute follow Align to Glyph Bounds (`tests::typeglyphbounds`, `panels::transform::tests::measure_type_by_glyph_bounds_is_checked_and_resizes_the_box`).
- Initial live Revolve (#605, #846): a portable geometry and shading crate (`three-d`), editable path and compound profiles, compact options with artboard rotation and light gizmos, SVG/PDF vector output and Expand Appearance that can trim covered faces. Cap is on by default with continuous curved vector rims; shading uses vector gradients automatically, with sharp profile corners, uniform caps and round seam joins. One light and approximate visibility remain ([revolve.md](revolve.md)).
- Effect › Path › Outline Object has no options. Type that carries it is drawn, exported (PDF and SVG as paths, with no font) and expanded as its glyph outlines while it stays editable, and the effects after it reshape those outlines. Paths and images draw as before. With Use Preview Bounds on, type is still measured by its layout box. Public reference: [Summary of effects](https://web.archive.org/web/20210901011839/https://helpx.adobe.com/illustrator/using/summary-effects.html) (archived copy).
- Initial live Revolve (#605, #846): a portable geometry and shading crate (`three-d`), editable path and compound profiles, compact options with artboard rotation and light gizmos, SVG/PDF vector output and Expand Appearance that can trim covered faces. Cap is on by default with continuous curved vector rims; shading uses vector gradients automatically, with sharp profile corners, uniform caps and round seam joins. The Lights list, Add Light and Delete Light manage one to ten independent colored lights; ambient is shared and visibility remains approximate ([revolve.md](revolve.md)).