diff --git a/crates/engine/src/cmd/textwrap.rs b/crates/engine/src/cmd/textwrap.rs index caeb1f8fcf..c7c9adb261 100644 --- a/crates/engine/src/cmd/textwrap.rs +++ b/crates/engine/src/cmd/textwrap.rs @@ -17,7 +17,7 @@ pub fn specs() -> Vec { "Make", ["Object", "Text Wrap"], None, - "{offset?: pt (6), invert?: bool, ids?} area type below the selected objects (same layer) wraps around them → {ids}", + "{offset?: pt (6), invert?: bool, ids?} area type below the selected objects (same layer) wraps around them; a negative offset (to -1000) lets the text that far into them → {ids}", has_selection, make ), @@ -27,7 +27,7 @@ pub fn specs() -> Vec { "Text Wrap Options…", ["Object", "Text Wrap"], None, - "{offset?: pt, invert?: bool, ids?} set the options of the selected wrap objects; no options → the current {offset, invert}", + "{offset?: pt, invert?: bool, ids?} set the options of the selected wrap objects (offset -1000 to 1000; a negative one lets text into the objects); no options → the current {offset, invert}", has_selection, options ), @@ -186,6 +186,33 @@ mod tests { assert_eq!(lines(&s, t), plain); } + /// A negative offset from Text Wrap Options lets the lines beside a wrap object run that far + /// into it, the options report it, and one undo puts back the offset before it. + #[test] + fn text_wrap_options_negative_offset_reflows_and_round_trips() { + let mut s = Session::new(); + s.execute("file.new", &json!({"width": 600, "height": 600})).unwrap(); + let t = s + .execute("text.create", &json!({"x": 0, "y": 0, "text": "word ".repeat(200), "size": 12, "area": {"width": 400, "height": 400}})) + .unwrap()["id"] + .as_u64() + .unwrap(); + let r = s.execute("shape.rectangle", &json!({"x": -50, "y": 50, "width": 250, "height": 100})).unwrap()["id"].as_u64().unwrap(); + s.execute("select.set", &json!({"ids": [r]})).unwrap(); + s.execute("object.textWrap.make", &json!({})).unwrap(); + let beside = |s: &Session| lines(s, t).into_iter().filter(|l| l.0 > 1.0).map(|l| l.0).collect::>(); + assert!(!beside(&s).is_empty() && beside(&s).iter().all(|x| (x - 206.0).abs() < 1e-6), "{:?}", beside(&s)); + assert_eq!(s.execute("object.textWrap.options", &json!({"offset": -20})).unwrap()["offset"], json!(-20.0)); + assert!(!beside(&s).is_empty() && beside(&s).iter().all(|x| (x - 180.0).abs() < 1e-6), "{:?}", beside(&s)); + assert_eq!(s.execute("object.textWrap.options", &json!({})).unwrap()["offset"], json!(-20.0)); + s.execute("edit.undo", &json!({})).unwrap(); + assert_eq!(s.execute("object.textWrap.options", &json!({})).unwrap()["offset"], json!(6.0)); + assert!(beside(&s).iter().all(|x| (x - 206.0).abs() < 1e-6), "{:?}", beside(&s)); + // An offset past -1000 is kept at -1000: the whole object lets text through. + assert_eq!(s.execute("object.textWrap.options", &json!({"offset": -5000})).unwrap()["offset"], json!(-1000.0)); + assert!(lines(&s, t).iter().all(|l| l.0 < 1.0 && l.1 > 300.0)); + } + #[test] fn text_flows_on_both_sides_of_a_centred_object() { let mut s = Session::new(); diff --git a/crates/mcp/src/tools/definitions.rs b/crates/mcp/src/tools/definitions.rs index d4d69c945f..e583753e4d 100644 --- a/crates/mcp/src/tools/definitions.rs +++ b/crates/mcp/src/tools/definitions.rs @@ -511,7 +511,7 @@ pub fn tool_definitions() -> Vec { obj( json!({ "ids": {"type": "array", "items": {"type": "integer"}}, - "offset": num("Gap between text and object (default 6 pt)"), + "offset": num("Gap between text and object in pt, -1000 to 1000 (default 6 pt); a negative gap lets text that far into the object"), "invert": {"type": "boolean"}, "release": {"type": "boolean"}, }), diff --git a/crates/text/src/layout/region.rs b/crates/text/src/layout/region.rs index d7293c1109..a4a342535d 100644 --- a/crates/text/src/layout/region.rs +++ b/crates/text/src/layout/region.rs @@ -368,6 +368,80 @@ fn band_boundaries(polys: &[Vec], top: f64, bottom: f64) -> (Vec<(f64, f6 (union_intervals(edges), paired) } +/// The largest magnitude a text wrap offset may have (points), the bound the Text Wrap commands +/// and `.ai` import keep too. +const MAX_WRAP_OFFSET: f64 = 1000.0; + +/// A wrap object's offset as layout uses it: within [`MAX_WRAP_OFFSET`] either way, and 0 when it +/// is not a number. +fn wrap_offset(offset: f64) -> f64 { + if offset.is_nan() { 0.0 } else { offset.clamp(-MAX_WRAP_OFFSET, MAX_WRAP_OFFSET) } +} + +/// The x at which some point of the band `top..bottom` (`top <= bottom`) is inside `polys` +/// (even-odd) and farther than `d` (> 0) from every edge of it, holes included: the cover of the +/// shape shrunk by `d`. The band is cut into pieces (at most [`INVERT_WRAP_PIECES`] even ones, and +/// at every anchor's height and `d` above and below it when there are at most that many of those); +/// in each piece, an x is left out when the shape is nowhere in the piece there, or when the whole +/// piece there lies within `d` of one edge. A piece where several edges together cover it keeps +/// its x, so the result can be wider than the shrunk shape, never narrower, and never wider than +/// the shape's own cover of the band. +fn eroded_cover(polys: &[Vec], top: f64, bottom: f64, d: f64) -> Vec<(f64, f64)> { + // A point `d` from an edge, up to rounding, counts as near it, so a band that only touches the + // shrunk shape is left whole. + let r = d * (1.0 + 1e-12); + let span = |p: &[Point]| p.iter().fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), q| (lo.min(q.y), hi.max(q.y))); + // The shrunk shape lies more than `d` below the shape's top and above its bottom, so a band + // outside that holds none of it; within it, only contours that come within `d` of the band + // count (the others neither cross it nor bound the shape near it). + let (lo, hi) = polys.iter().map(|p| span(p)).fold((f64::INFINITY, f64::NEG_INFINITY), |(a, b), (c, e)| (a.min(c), b.max(e))); + if bottom <= lo + r || top >= hi - r { + return vec![]; + } + let near_band: Vec> = polys + .iter() + .filter(|p| { + let (lo, hi) = span(p); + hi >= top - r && lo <= bottom + r + }) + .cloned() + .collect(); + let polys = near_band.as_slice(); + let pieces = ((bottom - top) * 4.0 / d).ceil().clamp(1.0, INVERT_WRAP_PIECES); + let mut cuts: Vec = (0..=pieces as usize).map(|k| top + (bottom - top) * k as f64 / pieces).collect(); + if bottom > top { + let mut anchors: Vec = polys.iter().flatten().flat_map(|p| [p.y - d, p.y, p.y + d]).filter(|&y| y > top && y < bottom).collect(); + anchors.sort_by(f64::total_cmp); + anchors.dedup(); + if anchors.len() <= INVERT_WRAP_PIECES as usize { + cuts.extend(anchors); + cuts.sort_by(f64::total_cmp); + cuts.dedup(); + } + } + let mut out = Vec::new(); + // A band of no height keeps its two equal cuts: one piece of no height, measured at its y. + for w in cuts.windows(2) { + let (a, b) = (w[0], w[1]); + let cover = band_cover(polys, a, b, false); + if cover.is_empty() { + continue; + } + // The points closer than `d` to an edge make a convex shape, so a vertical line within + // the piece lies in it when both its ends do. + let near = polys.iter().flat_map(|p| { + (0..p.len()).filter_map(move |i| { + let (s, t) = (p[i], p[(i + 1) % p.len()]); + let (u, v) = (near_segment(s, t, a, r)?, near_segment(s, t, b, r)?); + let (x, y) = (u.0.max(v.0), u.1.min(v.1)); + (y > x).then_some((x, y)) + }) + }); + out.extend(subtract_intervals(&cover, &union_intervals(near.collect()))); + } + union_intervals(out) +} + impl Region { pub(super) fn cells(path: &BezPath, opts: &LayoutOptions, wrap: &[vectorcraft_doc::WrapShape]) -> Vec { // A frame out of range has no cells, and a wrap shape out of range is left out. @@ -376,7 +450,7 @@ impl Region { let polys: std::sync::Arc<[Vec]> = polys.into(); let wraps: std::sync::Arc<[FlatWrap]> = wrap .iter() - .filter_map(|w| Some((polygons(&w.path.to_bezpath())?, w.wrap.offset.max(0.0), w.wrap.invert))) + .filter_map(|w| Some((polygons(&w.path.to_bezpath())?, wrap_offset(w.wrap.offset), w.wrap.invert))) .filter(|w| !w.0.is_empty()) .collect(); let bbox = path.bounding_box(); @@ -415,8 +489,10 @@ impl Region { } /// `iv` minus every x at which a wrap object lies somewhere in the band `top..bottom` (or the x - /// inside it over the whole band, for Invert Wrap). Offsets grow each wrap shape vertically - /// and horizontally. Along the line, the text also keeps the inset beside each wrap object + /// inside it over the whole band, for Invert Wrap). A positive offset grows each wrap shape + /// vertically and horizontally; a negative one shrinks it by that distance from its edges + /// (holes included), so text runs that far into the object, or stays that far inside it for + /// Invert Wrap. Along the line, the text also keeps the inset beside each wrap object /// (outside it, or inside it for Invert Wrap); above and below a wrap object only its offset /// applies. fn wrapped(&self, mut iv: Vec<(f64, f64)>, top: f64, bottom: f64) -> Vec<(f64, f64)> { @@ -428,7 +504,17 @@ impl Region { } let grow = |v: Vec<(f64, f64)>| union_intervals(v.into_iter().map(|(a, b)| (a - off, b + off)).collect()); let shrink = |v: Vec<(f64, f64)>| v.into_iter().map(|(a, b)| (a + inset, b - inset)).filter(|(a, b)| b > a).collect::>(); - iv = if *invert { + let pad = |v: Vec<(f64, f64)>| union_intervals(v.into_iter().map(|(a, b)| (a - inset, b + inset)).collect()); + iv = if *off < 0.0 { + // Invert Wrap keeps the x at which the shape shrunk by the offset holds the whole + // band; otherwise the text leaves out the x at which the shrunk shape lies + // somewhere in the band. + if *invert { + intersect_intervals(&iv, &shrink(band_inset_intervals(polys, top, bottom, -off))) + } else { + subtract_intervals(&iv, &pad(eroded_cover(polys, top, bottom, -off))) + } + } else if *invert { // The band is cut into pieces. Each height of a piece `y0..y1` is within the offset // of some height from `y0` plus the offset to `y1` less the offset, so an x the // shape holds at all of those heights is inside the grown shape over the piece. In @@ -469,8 +555,7 @@ impl Region { } iv } else { - let w = grow(band_cover(polys, top - off, bottom + off, false)); - subtract_intervals(&iv, &union_intervals(w.into_iter().map(|(a, b)| (a - inset, b + inset)).collect())) + subtract_intervals(&iv, &pad(grow(band_cover(polys, top - off, bottom + off, false)))) }; } iv @@ -624,6 +709,94 @@ fn a_line_stays_clear_of_a_wrap_object_that_reaches_into_its_band() { assert_spans(&Region::cells(&frame, &LayoutOptions::default(), &[wrap])[0].spans(50.0, 50.0), &[(15.0, 45.0)]); } +/// The spans of a 100 pt square frame over the band `top..bottom` with one wrap object through +/// `contours` (even-odd). +#[cfg(test)] +fn wrap_spans(contours: &[&[(f64, f64)]], offset: f64, invert: bool, top: f64, bottom: f64) -> Vec<(f64, f64)> { + let frame = Rect::new(0.0, 0.0, 100.0, 100.0).to_path(0.1); + let wrap = vectorcraft_doc::WrapShape { path: PathData::from_bezpath(&closed(contours)), wrap: vectorcraft_doc::TextWrap { offset, invert } }; + Region::cells(&frame, &LayoutOptions::default(), &[wrap])[0].spans(top, bottom) +} + +/// A negative offset lets text run into a wrap object by that distance, across and along the +/// line: beside a square, the line reaches 5 pt into it, and a line whose band reaches no more than +/// 5 pt into its top or bottom runs across it. +#[cfg(test)] +#[test] +fn negative_wrap_offset_insets_rectangle_on_both_axes() { + let square: &[(f64, f64)] = &[(40.0, 40.0), (60.0, 40.0), (60.0, 60.0), (40.0, 60.0)]; + assert_spans(&wrap_spans(&[square], -5.0, false, 44.0, 50.0), &[(0.0, 45.0), (55.0, 100.0)]); + assert_spans(&wrap_spans(&[square], -5.0, false, 30.0, 45.0), &[(0.0, 100.0)]); + assert_spans(&wrap_spans(&[square], -5.0, false, 55.0, 70.0), &[(0.0, 100.0)]); + assert_spans(&wrap_spans(&[square], -5.0, false, 30.0, 46.0), &[(0.0, 45.0), (55.0, 100.0)]); + // The most an offset may take away is 1000 pt, and an offset that is not a number is 0. + assert_spans(&wrap_spans(&[square], f64::NEG_INFINITY, false, 44.0, 50.0), &[(0.0, 100.0)]); + assert_spans(&wrap_spans(&[square], f64::NAN, false, 44.0, 50.0), &[(0.0, 40.0), (60.0, 100.0)]); +} + +/// A negative offset is measured from a sloped edge itself (10 pt across the edge of a triangle is +/// 10√2 pt along the line), and from the edges of a hole too. +#[cfg(test)] +#[test] +fn negative_wrap_offset_insets_sloped_shape_and_hole() { + let triangle: &[(f64, f64)] = &[(0.0, 0.0), (100.0, 100.0), (0.0, 100.0)]; + assert_spans(&wrap_spans(&[triangle], -10.0, false, 40.0, 60.0), &[(0.0, 10.0), (60.0 - 10.0 * std::f64::consts::SQRT_2, 100.0)]); + let outer: &[(f64, f64)] = &[(20.0, 20.0), (80.0, 20.0), (80.0, 80.0), (20.0, 80.0)]; + let hole: &[(f64, f64)] = &[(40.0, 40.0), (60.0, 40.0), (60.0, 60.0), (40.0, 60.0)]; + assert_spans(&wrap_spans(&[outer, hole], -5.0, false, 48.0, 52.0), &[(0.0, 25.0), (35.0, 65.0), (75.0, 100.0)]); +} + +/// With Invert Wrap, a negative offset keeps the text that far inside the object, along and across +/// the line. +#[cfg(test)] +#[test] +fn negative_invert_wrap_keeps_only_eroded_interior() { + let square: &[(f64, f64)] = &[(40.0, 40.0), (60.0, 40.0), (60.0, 60.0), (40.0, 60.0)]; + assert_spans(&wrap_spans(&[square], -5.0, true, 46.0, 54.0), &[(45.0, 55.0)]); + assert!(wrap_spans(&[square], -5.0, true, 42.0, 48.0).is_empty()); + let triangle: &[(f64, f64)] = &[(0.0, 0.0), (100.0, 100.0), (0.0, 100.0)]; + assert_spans(&wrap_spans(&[triangle], -10.0, true, 40.0, 60.0), &[(10.0, 40.0 - 10.0 * std::f64::consts::SQRT_2)]); +} + +/// A negative offset at least half as large as a wrap object is wide leaves nothing of it: text +/// runs across it, and with Invert Wrap no text fits inside it. +#[cfg(test)] +#[test] +fn negative_offset_erases_narrow_wrap_shape() { + let strip: &[(f64, f64)] = &[(40.0, 0.0), (48.0, 0.0), (48.0, 100.0), (40.0, 100.0)]; + for offset in [-4.0, -5.0, -1000.0] { + assert_spans(&wrap_spans(&[strip], offset, false, 40.0, 60.0), &[(0.0, 100.0)]); + assert!(wrap_spans(&[strip], offset, true, 40.0, 60.0).is_empty(), "offset {offset}"); + } + // A band of no height. + assert_spans(&wrap_spans(&[strip], -1.0, false, 50.0, 50.0), &[(0.0, 41.0), (47.0, 100.0)]); +} + +/// A negative offset never keeps text farther from a wrap object than an offset of 0 does, on a +/// shape whose edges meet at many angles. +#[cfg(test)] +#[test] +fn negative_offset_never_wraps_more_than_no_offset() { + let star: Vec<(f64, f64)> = (0..40) + .map(|k| { + let (r, a) = (if k % 2 == 0 { 40.0 } else { 15.0 }, f64::from(k) * std::f64::consts::PI / 20.0); + (50.0 + r * a.cos(), 50.0 + r * a.sin()) + }) + .collect(); + for (top, bottom) in [(10.0, 22.0), (44.0, 56.0), (30.0, 31.0), (70.0, 90.0)] { + for invert in [false, true] { + let zero = wrap_spans(&[star.as_slice()], 0.0, invert, top, bottom); + let neg = wrap_spans(&[star.as_slice()], -3.0, invert, top, bottom); + let width = |v: &[(f64, f64)]| v.iter().map(|(a, b)| b - a).sum::(); + if invert { + assert!(width(&neg) <= width(&zero) + 1e-9, "{top}..{bottom}: {neg:?} {zero:?}"); + } else { + assert!(width(&neg) >= width(&zero) - 1e-9, "{top}..{bottom}: {neg:?} {zero:?}"); + } + } + } +} + /// A hole that starts and ends inside a line's band splits the line, and a band below the hole, or /// one that only touches it, keeps the whole width. #[cfg(test)] diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 9a9b0d06ed..9ad6c941e0 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1269,6 +1269,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - **Type:** Character size, leading, baseline shift and horizontal-scale controls account for object/group transforms (#1034): exact document-point sizes, mixed-value display and keyboard increments, range edits, resettable horizontal stretch, undo/redo and native save/reopen (`tests_typescale`, Character panel tests). Point size is the transformed em-height axis, independent of glyph ink bounds; affine rotation, reflection and shear are retained. Group scaling still scales text; preserving font sizes is a separate enhancement. Behaviour reference: [Transform text](https://helpx.adobe.com/illustrator/desktop/design-with-text/edit-format-text/transform-text.html) (public documentation). Text Wrap, Type on a Path effects (Rainbow/Skew/3D Ribbon/Stair Step/Gravity) and Options (Flip, Align to Path, Spacing; OK keeps the type where it is), type on a path's start, centre and end brackets (Selection and Direct Selection drag them to set where the type runs, slide it along its path and, across the path, flip it: #429), Character and Paragraph Styles (override-preserving redefine), paragraph attributes per paragraph (alignment, indents, spacing, hyphenation, hanging punctuation, tab stops, paragraph style, direction, mojikumi and leading model; the Paragraph, Tabs and Paragraph Styles panels act on the paragraphs the Type tool's selection or caret touches, Return continues the paragraph's style, threads and SVG keep them), Area Type Options (rows/columns/inset/first baseline, Text Flow by rows or by columns (#4978), vertical Align Top/Center/Bottom/Justify per row/column, reflowing non-rectangular frames and text wrap), threaded text across any closed shapes, Fit Headline, the Every-line Composer for justified and ragged text (paragraph-wide breaks for an even rag, Single-line per object from the Paragraph panel menu or `text.setFormat {composer}`). Area type resizes like Illustrator's type areas: a bounding-box handle drag (Selection tool) or Area Type Options Width/Height size the frame and the text reflows at its size (threads too); Direct Selection drags a frame corner or edge (`text.reshapeArea`), and an anchor of the path of type on a path outside perspective, the type reflowing along it (`text.reshapePath`, #3980). Point type still scales. Auto Size: the frame height follows the text after every edit, in the same undo step (rectangular frames of horizontal type in one row; columns get the least height that holds them; a height set by hand or threading turns it off), and the "Auto Size New Area Type" preference gives it to new area type. Shrink Text to Fit (beyond Illustrator): overflowing area type scales its sizes, leading and baseline shifts down by the largest factor (in 0.1 pt steps of the first run, down to a minimum percentage) that fits, at layout time so every export sees it. `text.areaOptions` and `document.inspect` report overflow and the shrink factor. The Selection tool shows the type widget beside selected type's bounding box (hollow on point type, filled on area type); double-clicking it converts between them (Type › Convert To Area Type / Convert To Point Type), keeping the text and its styles in place. Variable fonts list every named instance as a style of their family (Figtree Light to Black and their italics, Bahnschrift, Segoe UI Variable), and the instance chosen is shaped, drawn, outlined and measured at its axis settings; PDF and SVG embed it as that instance. The Character panel's Variable Font sliders set each axis (see Fonts). Beyond Illustrator: inline graphics (`text.insertInline`, Type ▸ Insert Inline Symbol) set a document symbol in a run of type as one character that flows, wraps and justifies like a glyph (InDesign-style inline objects, e.g. mana symbols in card rules text): sized to a multiple of the font size, centred on the cap height, opening up tall lines, following Redefine Symbol; a character of its run's size for Character Alignment and Top-to-Top leading, placed by the bidi reordering in right-to-left text (a neutral U+FFFC); drawn on the canvas, kept as live text plus a `` in SVG and as vector art in PDF; Backspace removes it whole. Hebrew and Arabic are laid out right to left with the Unicode bidirectional algorithm (embedded Latin and numbers keep their direction, lines put in visual order after wrapping) in point, area and path type, with caret, selection and arrow keys in visual order; each paragraph's direction comes from its first strong character unless Paragraph Direction sets it (Paragraph panel with Show Indic Options, `text.setFormat {direction}`), and new type aligns to the start of its direction. SVG export outlines right-to-left text. Arabic typefaces: builds made with a craft-fonts checkout that holds its Arabic families (Noto Sans Arabic, the Arabic fallback, plus Cairo, Tajawal, Almarai, IBM Plex Sans Arabic, Noto Kufi Arabic, Amiri, Alexandria, Fustat, Reem Kufi, Aref Ruqaa and Gulzar once craft-fonts adds them, storytold/craft-fonts#11; releases get them when their craft-fonts pin moves) list them in the font menus on desktop; the web build fetches them from beside the wasm (Noto Sans Arabic before start, so Arabic never shows as boxes, the rest in the background). Font Size and Leading are combo boxes in the Character and Properties panels (Font Size in the Control bar too, beside the Character link, font family and style): type, step with ↑/↓ or the spinner, scrub the label, or pick a preset (6–72 pt, shown in the type unit; Leading also Auto), each pick one undo step; Kerning and Tracking list -100…200; a size, leading, kerning or tracking the selected text doesn't share shows blank (#458). Type › Bold and Italic (beyond Illustrator, as page layout apps have them; Cmd+Shift+B / Cmd+Shift+I while the Type tool edits text) switch the selected type to its family's own Bold or Italic face and back, keeping its width (a Narrow face stays Narrow); a family without that face says so (#724). The commands that make or restyle type (and the MCP `add_text` tool) say in their result when area type does not fit its frame, and `add_text` takes Auto Size and Shrink Text to Fit (#1500, fixes #1471). Cmd+Shift+C (Align center) works on Windows and Linux too (#1940). A double-click on area type's bottom middle handle turns on Auto Size (#1940). Print and Print Preview warn about the area type and type on a path that prints without the text that doesn't fit, and give their ids. A tab stop in the Tabs panel drags from where the press landed, a stop just added included (#1487, fixes #1458). Area type in a non-rectangular shape moves a word too wide for the top lines down to the first line that holds it instead of breaking it (#1617, fixes #1615). Stylistic sets (ss01–ss20), stylistic and titling alternates, proportional and lining figures, and superscript and subscript glyphs can be turned on as OpenType features of a character style (`text.setStyle {features}`), and the OpenType panel keeps them (#1611, fixes #1607). Escape while typing ends the edit, keeps the type selected and switches back to the tool that was active before type editing began (kept while clicks move editing to other text), or to the Selection tool when there was none (#1802, #1686, #1656). Select › Same matches type on its characters' fill and stroke (#1746, #1737). `text.create`, `text.createInPath` and the MCP `add_text` tool take `justify`, so point type can be centred on or end at its point (#1752, #1748). The Character panel menu's Show Font Height Options shows a Set Font Height Reference dropdown under Font Size (`text.setFormat` or `text.setRangeStyle` `{fontHeightReference}`). Each reference shows an icon of the height it measures before its name, in the open list and in the closed field (#6304): a frame around a letter for Em Box, a bar from the baseline to the top of a capital for Cap Height and of a lowercase x for x-Height, and a frame inset in the em square for ICF Box. With Em Box, the default, the font size is the size of the em square; with Cap Height, x-Height or ICF Box, the characters are set at the em size at which the font's OS/2 cap height, its x height or the height of its ideographic character face equals the font size. With a font that has no such value, or one outside 0.1 to 2 em, the size measures the em box. SVG, PDF and legacy `.ai` export and the CSS Properties rules write the em size the characters are drawn at, and DXF export writes 0.7 times that em size as each TEXT entity's height (group 40, the height of its capitals). Behavior reference: [Vary font height](https://web.archive.org/web/20251118174722/https://helpx.adobe.com/illustrator/desktop/design-with-text/edit-format-text/vary-font-height.html). The Every-line Composer composes a paragraph whose characters differ in line metrics (type size, font, leading or vertical scale, or an inline graphic taller than the text) in area type of any shape, rows, columns and text wrap included. In a frame that is not a rectangle, or with text wrap, each line it tries is measured where the lines above it put it, across the line's own height, so a taller line gets the room of the narrower part of the frame, or of the span beside a wrap object, that it reaches. Such a paragraph is broken with the Single-line Composer when the frame has no room for all of it or when the search passes VectorCraft's own limits. Behavior reference: [the help page](https://helpx.adobe.com/illustrator/desktop/design-with-text/edit-format-text/set-hyphenation-and-line-breaks.html). + A text wrap object's Offset may be negative as well as positive ([the help page](https://help.adobe.com/archive/en_US/illustrator/cs4/illustrator_cs4_help.pdf#page=307)). A negative Offset (to -1000 pt) shrinks the wrap object by that distance, measured from each edge itself and from the edges of its holes, so lines beside it, above it and below it run that far into it, and an object narrower than twice the distance no longer moves text aside. With Invert Wrap, a negative Offset keeps the text that far inside the object. These are VectorCraft's own rules for the shape of the shrunk object. Text Wrap Options, `object.textWrap.make` and `object.textWrap.options` and the MCP `text_wrap` tool take the negative value, and layout uses it (`crates/text/src/layout/region.rs` tests, `cmd::textwrap::tests::text_wrap_options_negative_offset_reflows_and_round_trips`). In area type of a shape other than a rectangle, each line's whole band, ascent to descent, stays inside the frame: where an edge pinches in within the band, the line ends at the narrowest point, and a line runs beside a hole that starts and ends within its band, not across it. In area type of any shape, a line also stays clear of a text wrap object that reaches into any part of its band, and inside an Invert Wrap object over its whole band (`crates/text/src/layout/region.rs` tests). The Character panel's Small Caps, Superscript and Subscript buttons and the panel menu's items of the same names turn small caps and the superscript or subscript position on and off for the selected type, or for the text the Type tool has selected (`text.setFormat` or `text.setRangeStyle` `{smallCaps, position}`), in Document Setup's proportions. Turning Superscript on turns Subscript off, and turning Subscript on turns Superscript off. A button or item shows as on when the first selected character (at a caret, the character before it) has its attribute. The Character panel's Anti-aliasing Method dropdown, under Character Rotation, sets how the edges of the selected type objects are drawn as pixels (`text.setFormat {antiAlias}`), for the whole object, with each frame of a thread keeping its own method and graph labels keeping it when the graph is drawn again, on the canvas, in raster exports and when the type is rasterized. VectorCraft defines the methods: Sharp, the default, draws edges as before; None paints a pixel only where the type covers at least half of it; Crisp shows a pixel covered by a share c at `2c - 0.5`, and Strong shows it at `2c`, each kept between 0 and 1. SVG, PDF and EPS export are unchanged. Scores and hours unchanged.