From d639508a9a867603a2618db946ce5e345078a153 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Wed, 4 Nov 2026 06:15:59 -0800 Subject: [PATCH] Type: baseline shift moves an upright glyph across the column in vertical type Baseline shift on an upright glyph in vertical type moved it up the column instead of across it, at any Character Rotation, because the shift was applied before the glyph was turned upright. The shift is now taken off before the turn and put back after it, so an upright glyph moves across the column by the shift as a letter lying on its side does, and still turns about its shifted pen position. --- crates/text/src/layout.rs | 8 +++++-- crates/text/src/tests/vertical.rs | 35 +++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/crates/text/src/layout.rs b/crates/text/src/layout.rs index 38688b8ce..b814506e6 100644 --- a/crates/text/src/layout.rs +++ b/crates/text/src/layout.rs @@ -327,10 +327,14 @@ impl Ctx<'_> { } else if self.vertical && stands_upright(g) { let next = self.out.glyphs.len(); let after_base = self.out.glyphs.last().is_some_and(|p| p.byte == g.byte && p.line == line && p.len > 0); + // Baseline shift moves it across the column at any Character Rotation: the shift is + // taken off before the turn and put back after it. + let shift = pre * Point::new(0.0, -g.bshift) - pre * Point::ZERO; + let (shift, unshift) = (Affine::translate(shift), Affine::translate(-shift)); if let Some((_, _, turn, after)) = cluster.filter(|c| c.0 == g.byte && c.1 == next && after_base && g.len > 0) { // A later glyph of an upright glyph's cluster: set after it as the shaper placed // it, and turned with it. - m = turn * Affine::translate((after - origin.x, 0.0)) * m; + m = shift * turn * Affine::translate((after - origin.x, 0.0)) * unshift * m; self.upright_cluster = Some((g.byte, next + 1, turn, after)); } else { // Turned about the centre of its cell, which the column's centre line runs through: @@ -342,7 +346,7 @@ impl Ctx<'_> { // turning point instead would move it across the column too). let turn = Affine::translate((-g.lead, 0.0)) * Affine::rotate_about(-std::f64::consts::FRAC_PI_2, Point::new(origin.x + cell * 0.5, origin.y - upright_centre(g, cell, em))); - m = turn * m; + m = shift * turn * unshift * m; // Its own advance across, from where the cell centres it: where the shaper set the // next glyph of its cluster. let across = g.face.advance(g.gid) * g.sx; diff --git a/crates/text/src/tests/vertical.rs b/crates/text/src/tests/vertical.rs index 9c9d61681..c0a2b5e13 100644 --- a/crates/text/src/tests/vertical.rs +++ b/crates/text/src/tests/vertical.rs @@ -343,6 +343,41 @@ fn tracking_spaces_upright_marks_without_moving_them_off_the_centre_line() { assert!((step - 24.0).abs() < 1e-6, "one em plus 200/1000 em of tracking: {step}"); } +/// Baseline shift moves an upright glyph across the column, as it moves a letter lying on its +/// side, at any Character Rotation, and leaves its neighbors where they are. The glyph turns about +/// its shifted pen position (`xf * pivot`), and `xf * unturned` places the shifted glyph without +/// its rotation. +#[test] +fn baseline_shift_moves_an_upright_glyph_across_the_column() { + let at = |byte: usize, rotation: f64, shift: f64| { + let mut t = vertical("§l§"); + edit::style_range(&mut t.runs, byte, byte + "§l§"[byte..].chars().next().map_or(1, char::len_utf8), |s| { + s.rotation = rotation; + s.baseline_shift = shift; + }); + layout(FontDb::global(), &t) + }; + let centre = |l: &TextLayout, byte: usize| l.glyphs.iter().find(|g| g.byte == byte).map(|g| g.outline.bounding_box().center()).unwrap(); + // The letter lying on its side moves across the column by the shift. + let across = centre(&at(2, 0.0, 10.0), 2) - centre(&at(2, 0.0, 0.0), 2); + assert!((across.x.abs() - 10.0).abs() < 1e-6 && across.y.abs() < 1e-6, "{across:?}"); + for rotation in [0.0, 30.0, 90.0, -90.0] { + let (plain, shifted) = (at(3, rotation, 0.0), at(3, rotation, 10.0)); + let moved = centre(&shifted, 3) - centre(&plain, 3); + assert!((moved - across).hypot() < 1e-6, "{rotation}°: {moved:?}, across {across:?}"); + for byte in [0, 2] { + assert!((centre(&shifted, byte) - centre(&plain, byte)).hypot() < 1e-6, "{rotation}°: byte {byte} moved"); + } + let (p, s) = (plain.glyphs.iter().find(|g| g.byte == 3).unwrap(), shifted.glyphs.iter().find(|g| g.byte == 3).unwrap()); + let turn_at = s.xf * s.pivot - p.xf * p.pivot; + assert!((turn_at - across).hypot() < 1e-6, "{rotation}°: turns about a point moved by {turn_at:?}"); + let unrotated = at(3, 0.0, 10.0); + let u = unrotated.glyphs.iter().find(|g| g.byte == 3).unwrap(); + let (a, b) = ((s.xf * s.unturned).as_coeffs(), u.xf.as_coeffs()); + assert!(a.iter().zip(b).all(|(a, b)| (a - b).abs() < 1e-6), "{rotation}°: {a:?} vs {b:?}"); + } +} + /// Character panel menu › Tate-chu-yoko: a marked run stands across one em of the column, including /// a number longer than three digits and Latin letters. Unmarked text keeps the automatic rule /// (two or three digits only). Horizontal type stores the mark and does not move the letters.