Type: keep composition spacing along the line when characters rotate

This commit is contained in:
dotCooCoo
2026-11-03 21:38:02 +07:00
committed by bflatastic
parent 52703ec4ed
commit 2b63bf1b67
6 changed files with 83 additions and 6 deletions
+5 -5
View File
@@ -285,21 +285,21 @@ impl Ctx<'_> {
Some(ib) => Affine::translate((g.kern_shift - lead, -g.bshift)) * ib.xf,
// Baseline shift moves the glyph across its line, and Character Rotation then turns it
// about its shifted pen position, so the shift is across the line at any rotation.
// Right-to-left manual kerning moves that pen position along the line.
// Manual kerning and composition trim move that pen position along the line.
None => {
Affine::translate((g.kern_shift, -g.bshift))
Affine::translate((g.kern_shift - lead, -g.bshift))
* Affine::rotate(-g.rotation.to_radians())
* Affine::translate((g.dx - lead, g.dy))
* Affine::translate((g.dx, g.dy))
* Affine::scale_non_uniform(g.sx, g.sy)
}
};
// The turning point in font units, which later moves of the glyph's transform keep.
let pivot = match local.determinant() {
det if det != 0.0 && det.is_finite() => local.inverse() * Point::new(g.kern_shift, -g.bshift),
det if det != 0.0 && det.is_finite() => local.inverse() * Point::new(g.kern_shift - lead, -g.bshift),
_ => Point::ZERO,
};
let unturned = if g.inline.is_none() && g.rotation != 0.0 && g.sx != 0.0 && g.sy != 0.0 {
let placed = Affine::translate((g.dx - lead, g.dy)) * Affine::scale_non_uniform(g.sx, g.sy);
let placed = Affine::translate((g.dx, g.dy)) * Affine::scale_non_uniform(g.sx, g.sy);
placed.inverse() * Affine::rotate(g.rotation.to_radians()) * placed
} else {
Affine::IDENTITY
+50
View File
@@ -49,6 +49,56 @@ fn source_range(glyphs: &[SGlyph], start: usize, end: usize) -> Range<usize> {
first.byte..last.byte + last.len
}
#[test]
fn composition_trim_stays_along_the_line_when_a_character_rotates() {
let db = FontDb::with_font_dirs(vec![]);
let style = CharStyle::default();
let opts = LayoutOptions::default();
for angle in [0.0_f64, std::f64::consts::FRAC_PI_4, std::f64::consts::FRAC_PI_2] {
let origin = Point::new(100.0, 80.0);
let pre = Affine::translate(origin.to_vec2()) * Affine::rotate(angle);
let draw = |rotation: f64, lead: f64| {
let mut glyph = synthetic_glyphs(&[(0, 1, 30.0, 'A')]).remove(0);
glyph.sx = 0.02;
glyph.sy = 0.02;
glyph.dx = 2.0;
glyph.dy = -1.0;
glyph.bshift = 4.0;
glyph.kern_shift = 3.0;
glyph.rotation = rotation;
glyph.lead = lead;
let mut cx = Ctx {
on_path: angle != 0.0,
db: &db,
text: "A",
runs: vec![(0..1, &style)],
inlines: vec![None],
default: style.clone(),
opts: &opts,
vertical: false,
system_layout: false,
fractional_widths: true,
upright_cluster: None,
out: TextLayout::default(),
};
cx.emit(&glyph, pre, origin, angle, 30.0 - lead, 0);
cx.out.glyphs.remove(0)
};
let expected = Vec2::new(-angle.cos(), -angle.sin()) * 5.0;
for rotation in [0.0, 45.0, 90.0, -90.0] {
let (plain, trimmed) = (draw(rotation, 0.0), draw(rotation, 5.0));
assert!(!trimmed.outline.elements().is_empty(), "fixture has drawn geometry");
let shift = trimmed.xf * Point::ZERO - plain.xf * Point::ZERO;
assert!((shift - expected).hypot() < 1e-9, "line {angle}, character {rotation}: {shift:?} versus {expected:?}");
let pivot_shift = trimmed.xf * trimmed.pivot - plain.xf * plain.pivot;
assert!((pivot_shift - expected).hypot() < 1e-9, "composition moves the turning point along the line: {pivot_shift:?}");
let unrotated = draw(0.0, 5.0);
let actual = (trimmed.xf * trimmed.unturned).as_coeffs();
assert!(actual.iter().zip(unrotated.xf.as_coeffs()).all(|(a, b)| (a - b).abs() < 1e-9), "removing rotation keeps composition placement");
}
}
}
#[test]
fn greedy_wrap_keeps_an_oversized_first_cluster_and_its_source_range() {
let text = "بَت";
+1 -1
View File
@@ -147,7 +147,7 @@ pub struct PositionedGlyph {
/// its Character Rotation. The identity for a glyph without one.
pub unturned: Affine,
/// Font units: `xf * pivot` is the point its Character Rotation turns it about, its pen
/// position on the baseline moved by its baseline shift.
/// position after composition spacing adjustments, moved by its baseline shift.
pub pivot: Point,
/// A ligature (one glyph that alone stands for several characters, grapheme clusters, of the
/// text): the caret stop between each two of them, in logical order, as the byte offset of the
+22
View File
@@ -1729,6 +1729,28 @@ fn proportional_metrics_set_full_width_glyphs_on_their_proportional_widths() {
assert!((moved - 10.0).abs() < 0.01, "〙's empty half goes past the line's end: あ moved {moved}");
}
#[test]
fn mojikumi_trim_keeps_a_rotated_opening_bracket_on_its_line() {
use vectorcraft_doc::Mojikumi;
if db().face("Shippori Mincho", "Regular").is_none_or(|f| f.family != "Shippori Mincho") {
eprintln!("skipped: Shippori Mincho is not in CRAFT_FONTS_DIR");
return;
}
let lay = |rotation, mojikumi| {
let st = CharStyle { font_family: "Shippori Mincho".into(), size: 20.0, rotation, ..Default::default() };
let mut t = area("「あ", st, Rect::new(0.0, 0.0, 200.0, 200.0), Justify::Left);
t.para.mojikumi = mojikumi;
layout(db(), &t)
};
for rotation in [0.0, 45.0, 90.0, -90.0] {
let (plain, trimmed) = (lay(rotation, Mojikumi::None), lay(rotation, Mojikumi::LineEndHalf));
let (a, b) = (&plain.glyphs[0], &trimmed.glyphs[0]);
assert!((a.advance - 20.0).abs() < 0.01 && (b.advance - 10.0).abs() < 0.01, "full-width bracket fixture");
let shift = b.xf * Point::ZERO - a.xf * Point::ZERO;
assert!((shift.x + 10.0).abs() < 0.01 && shift.y.abs() < 0.01, "{rotation} degrees: {shift:?}");
}
}
/// The arrows pass a Thai consonant with its vowel and tone marks whole (น้ำ: no, mai tho, sara am),
/// as an accented letter written with a combining mark; plain text moves a character at a time.
#[test]
+2
View File
@@ -26,6 +26,7 @@ Artboard Options forwards the active Artboard tool's Scale Artwork with Artboard
| 2026-10-30 | minor | M4: File formats: `.ai`/EPS editing-data fidelity from an audit of a user's files: All Caps and Small Caps (#4708); placed files at their box's corner, kept where they draw closer to the page (#4664, #4723, #4742); pages cropped to the art, with `UserUnit` (#4658, #4727); gradient mesh corners with handle weights of 0 and the state written after a mesh (#4738, #4744, #4748); type on a path spaced as the file lays it out (#4743); synthetic regressions in `crates/eps`; scores and estimates unchanged. |
| 2026-11-03 | minor | M7: Find Font replacement sources Document, Recent and System are shipped (#5217); estimates unchanged. |
| 2026-10-30 | minor | Raster effects: bounded length fields in effect dialogs clamp on commit and reject invalid inputs visibly (#4638); estimates unchanged. |
| 2026-11-03 | minor | M7: composition spacing remains along the line when characters rotate; estimates unchanged. |
| 2026-10-22 | minor | Reconcile upstream documentation after review F-3; preserve every dated revision and the original scaleArt runtime evidence without repeating completed jobs. |
| 2026-10-22 | minor | Reconcile upstream documentation after review F-2; preserve every dated revision and the original scaleArt runtime evidence without repeating completed jobs. |
| 2026-10-22 | minor | Reconcile subsequent upstream progress updates without changing the runtime or repeating completed evidence; retain every dated upstream and feature revision. |
@@ -153,6 +154,7 @@ separate. Scores and hours unchanged. [Behavior and command details](development
| 2026-11-03 | minor | M7 records Find Font's shipped replacement-source chooser (#5217); estimates unchanged. |
| 2026-11-03 | minor | Symbol editing shows draggable 9-slice guides (#5280). Guides outside the artwork remain unsupported. Scores and hours remain unchanged. |
| 2026-10-30 | patch | M5: opaque solid-fill knockout paths avoid redundant erase/add layers in hard-edge frames and supersampled raster export; exact-pixel regression coverage retains analytical-AA rounding and translucent knockout behavior; scores and estimates unchanged |
| 2026-11-03 | minor | M7: retain composition trimming in rotated character placement and rotation metadata; estimates unchanged. |
| 2026-10-30 | minor | M4: G9: `.ai`/EPS editing data: a blend takes the transparency (opacity, blend mode, knockout) in its state block after its record, so its steps knock each other out as the page draws them; a user's file comes in 0.6% from its page instead of 1.3%. Scores and estimates unchanged. |
| 2026-10-30 | minor | M4: File formats: `.ai`/EPS editing-data fidelity from an audit of a user's files: All Caps and Small Caps (#4708); placed files at their box's corner, kept where they draw closer to the page (#4664, #4723, #4742); pages cropped to the art, with `UserUnit` (#4658, #4727); gradient mesh corners with handle weights of 0 and the state written after a mesh (#4738, #4744, #4748); type on a path spaced as the file lays it out (#4743); synthetic regressions in `crates/eps`; scores and estimates unchanged. |
| 2026-10-29 | minor | M6: Pathfinder: live compound shapes (#4522): the panel menu's Make, Release and Expand Compound Shape, Alt-clicking a Shape Mode, a selected member's Shape Mode and the Expand button act; scores unchanged |
+3
View File
@@ -29,6 +29,7 @@ Artboard Options forwards the active Artboard tool's Scale Artwork with Artboard
| 2026-10-31 | minor | Object › Path › Smooth smooths a path along its length before refitting it, so Auto-Smooth takes a shaky line to one smooth curve instead of leaving its jags, and with anchor points direct-selected it smooths only that section; scores unchanged. |
| 2026-10-31 | minor | New Document: More Settings has the Save Preset button beside the name too, saving its settings (artboard layout included) as a preset (#4893); scores unchanged. |
| 2026-10-31 | minor | Object › Rasterize in a CMYK document with its CMYK colour model makes a CMYK image of the art's ink amounts (as CMYK exports draw them), so the Properties panel says CMYK and CMYK exports keep its inks; a transparent background becomes the image's opacity mask (#4882); scores unchanged. |
| 2026-11-03 | minor | Composition spacing stays along the line under Character Rotation, including the turning point and unrotated placement; scores and estimates unchanged. |
| 2026-10-31 | minor | Export for Screens is laid out as Illustrator's: Artboards and Assets tabs with a grid or list of thumbnails, Clear Selection and Prefix under them, Create Sub-folders by Scale or Format (on by default), Export PDFs As Single File or Multiple Files (`pdfs`), iOS and Android rows, the format presets in a ☰ menu, and a Selected / files-to-export count; the naming pattern, Layers and Selection pages and folders list stay in `document.exportForScreens` for scripts and MCP; scores unchanged. |
| 2026-10-31 | minor | Export for Screens is laid out as the reference app's: Artboards and Assets tabs with a grid or list of thumbnails, Clear Selection and Prefix under them, Create Sub-folders by Scale or Format (on by default), Export PDFs As Single File or Multiple Files (`pdfs`), iOS and Android rows, the format presets in a ☰ menu, and a Selected / files-to-export count; the naming pattern, Layers and Selection pages and folders list stay in `document.exportForScreens` for scripts and MCP; scores unchanged. |
| 2026-10-31 | minor | Artwork Cleanup add-on: Join Point Text (`addon.cleanup.joinText`) joins stacked one-line point type into one object, a paragraph per line keeping its baseline (#4826); scores unchanged. |
@@ -774,6 +775,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
Inside a ligature (one glyph for several characters, such as fi), the Type tool's caret, clicks, arrow keys and selection stop between its characters, at the font's ligature carets ([OpenType GDEF ligature caret list](https://learn.microsoft.com/en-us/typography/opentype/spec/gdef)) or, when the font does not list one valid caret for each stop, at the glyph's width divided equally.
Duplicate Artboards, the Artboard tool's Alt-drag and Paste give the new artboard an id that no other artboard has, also when an artboard's id is the largest possible value (#1718). Deleting the copy or the original then leaves the other's guides.
- **Touch Type:** a click with the Touch Type tool (Shift+T) selects one character and shows a box around it. Dragging the box or its lower-left handle moves the character. The upper-left handle scales it vertically, the lower-right handle scales it horizontally, the upper-right handle scales it proportionally, and the upper-middle handle rotates it. Each drag is one undo step that sets the character's kerning, baseline shift, scales or rotation, and the type stays editable (`text.touchType`; behavior reference: [Touch Type tool](https://web.archive.org/web/20261007185617/https://helpx.adobe.com/illustrator/using/tool-techniques/touch-type-tool.html), [Add text with the Type, Area Type, and Touch Type tools](https://web.archive.org/web/20260926095922/https://helpx.adobe.com/illustrator/desktop/design-with-text/add-manage-text/add-text-illustrator-text-tools.html)).
Composition's leading-space reduction stays along the line when a character rotates. An opening bracket trimmed by Line-end Punctuation Half Width Mojikumi keeps that placement under Character Rotation; its turning point follows the composition adjustment, and removing the rotation keeps the trim. Scores and estimates are unchanged.
A character rotated near or at a quarter turn follows a Touch Type drag across its line as well as along it: baseline shift moves a character up from its line at any rotation, and the character turns about its shifted pen position.
- **Temporary tools:** Cmd with any other tool drags with the selection tool used last (Selection, Direct Selection or Group Selection). Held Space pans and Cmd+Space zooms (Cmd+Alt+Space out) whatever the tool.
Space held while the Zoom tool drags a marquee moves the marquee and keeps its size ([Default keyboard shortcuts](https://web.archive.org/web/20260726060015/https://helpx.adobe.com/illustrator/using/default-keyboard-shortcuts.html)). When Space is let go, the drag sizes the marquee again from where it moved to.
@@ -1563,6 +1565,7 @@ A double-click on a Repeat (Radial, Grid or Mirror) with the Selection tool ente
| 2026-11-03 | minor | SVG imports preserve exported ellipses as four-anchor paths (#5243). Inline graphics use manual kerning (#5245). Symbol editing shows draggable 9-slice guides (#5280). Recolor Artwork offers smooth and segmented wheels and color bars (#5249). Scores and hours remain unchanged. |
| 2026-11-01 | minor | Repeat: a double-click isolates it to edit its source art, text included (#5054); evidence added; scores unchanged. |
| 2026-11-01 | minor | Live Corners: Up/Down while dragging a corner widget step the corner kind (#2898); evidence added; scores unchanged. |
| 2026-11-03 | minor | Rotated characters retain line-axis composition trimming; scores and estimates unchanged. |
| 2026-11-01 | minor | Shortcuts for Effect menu items (#3296); evidence added; scores unchanged. |
| 2026-11-01 | minor | Color Effects add-on (a VectorCraft addition): Find Nearest Swatch (#4923); evidence added; scores unchanged. |
| 2026-11-01 | minor | Create Swatch Legend add-on under Object › Plug-ins › Color Effects (`addon.color.swatchLegend`, #4919); evidence added; scores unchanged. |