From 488248814575cfb411a416f75680d54d2204dc35 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Wed, 4 Nov 2026 00:30:04 -0800 Subject: [PATCH] Type: upright glyphs on a vertical path face the path's start In vertical type on a path, every glyph was turned a quarter turn with its top toward the path's end. An upright glyph's characters then read backwards along the path, so a ligature's font caret other than its middle did not line up with the glyph as drawn. An upright glyph whose top faces the path's end now turns half a turn about the centre of its cell, so its top faces the path's start. It keeps the same stretch of the path. Glyphs lying on their side, and upright glyphs that the Skew, 3D Ribbon and Stair Step effects already turn this way on a path running to the left, are unchanged. The ligature stops follow the top of the cell, so the caret, a click and the selection meet the drawn caret line. --- crates/text/src/layout.rs | 39 +++++++++---- crates/text/src/lib.rs | 2 +- crates/text/src/tests/ligature_carets.rs | 71 +++++++++++++++++++++++- 3 files changed, 98 insertions(+), 14 deletions(-) diff --git a/crates/text/src/layout.rs b/crates/text/src/layout.rs index 1ff72bdd2..77a55236d 100644 --- a/crates/text/src/layout.rs +++ b/crates/text/src/layout.rs @@ -332,9 +332,31 @@ impl Ctx<'_> { Affine::IDENTITY }; let mut m = pre * local; + // Vertical type on a path: an upright glyph's cell in font units, as (its length with the + // Proportional Metrics change to it, the height of its top above the baseline). + let path_cell = (self.vertical && self.on_path && g.inline.is_none() && g.tcy.is_none() && takes_cell(g) && stands_upright(g) && g.sy != 0.0) + .then(|| { + let (cell, top) = upright_extent(g); + let proportional = g.vpal.filter(|_| g.face.vertical_glyph(g.gid).is_some()).map_or(0.0, |a| a.advance) / g.sy; + (cell + proportional, top) + }); if self.vertical && self.on_path { // Vertical path type keeps the baseline path and turns each glyph across it. m = Affine::rotate_about(std::f64::consts::FRAC_PI_2, origin) * m; + if let Some((cell, top)) = path_cell { + // An upright glyph whose top then faces the path's end turns half a turn about the + // centre of its cell (without its Character Rotation), so that its top faces the + // path's start and the characters of an upright ligature read along the path in + // order. The effects that keep the glyph's up direction fixed already turn it so on + // a path that runs to the left. + let turned = m * unturned; + let tangent = Vec2::new(angle.cos(), angle.sin()); + let along = |y: f64| (turned * Point::new(0.0, y) - origin).dot(tangent); + if along(-top) > along(cell - top) { + let centre = turned * Point::new(g.face.advance(g.gid) * 0.5, cell * 0.5 - top); + m = Affine::translate(centre.to_vec2() * 2.0) * Affine::scale(-1.0) * m; + } + } } else if let (true, Some(t)) = (self.vertical, g.tcy) { // Tate-chu-yoko: the block set across the column, centred on its em of it, then moved up // by its Up/Down and right by its Left/Right (line x runs down the column and line y @@ -408,20 +430,17 @@ impl Ctx<'_> { } else { 0.0 }; - // On a path, an upright glyph turns about its pen: its cell runs along the path between - // where the glyph's transform sets the cell's foot and its top, moved by the shaper's - // placement and a baseline shift, and slanted by the path's effect. - let (width, width_start) = match (upright && self.on_path, g.sy) { - (true, sy) if sy != 0.0 => { - // In font units, with the Proportional Metrics change to its length. - let (cell, top) = upright_extent(g); - let proportional = g.vpal.filter(|_| g.face.vertical_glyph(g.gid).is_some()).map_or(0.0, |a| a.advance) / sy; + // On a path, an upright glyph's cell runs along the path from where the glyph's transform + // sets the cell's top to its foot, moved by the shaper's placement and a baseline shift, and + // slanted by the path's effect. + let (width, width_start) = match path_cell { + Some((cell, top)) => { let tangent = Vec2::new(angle.cos(), angle.sin()); let along = |y: f64| (m * Point::new(0.0, y) - origin).dot(tangent); - let (start, end) = (along(cell + proportional - top), along(-top)); + let (start, end) = (along(-top), along(cell - top)); if start.is_finite() && end > start { (end - start, start) } else { (width, width_start) } } - _ => (width, width_start), + None => (width, width_start), }; if let Some(art) = g.inline.as_ref().and_then(|ib| ib.art) { self.out.inlines.push(InlineGlyph { run: g.run, byte: g.byte, glyph: self.out.glyphs.len(), xf: m, bounds: m.transform_rect_bbox(art) }); diff --git a/crates/text/src/lib.rs b/crates/text/src/lib.rs index d7bc1d444..ce315645e 100644 --- a/crates/text/src/lib.rs +++ b/crates/text/src/lib.rs @@ -170,7 +170,7 @@ pub struct PositionedGlyph { /// taken off, plus the manual kerning of right-to-left text, which moves the glyph right so that /// the space it adds lies on the glyph's left. 0 for a glyph standing upright or set across the /// column in vertical type, and for an inline graphic. An upright glyph in vertical type on a - /// path turns about its pen, and its cell starts one cell length before the cell's top: 0.12 em + /// path stands with its top toward the path's start, and its cell starts at its top: 0.12 em /// before the pen without the font's vertical metrics. pub width_start: f64, } diff --git a/crates/text/src/tests/ligature_carets.rs b/crates/text/src/tests/ligature_carets.rs index 929ba50bc..af2ebae63 100644 --- a/crates/text/src/tests/ligature_carets.rs +++ b/crates/text/src/tests/ligature_carets.rs @@ -628,9 +628,10 @@ fn an_upright_ligature_takes_its_caret_down_its_cell() { assert!((at(900) - 0.5).abs() < 1e-9, "{}", at(900)); } -/// On a path, an upright ligature in vertical type turns about its pen, so its cell runs from -/// 0.12 em before the pen to 0.88 em after it, with or without the font's vertical metrics. The -/// inner stop lies at the middle of that cell, and the first character's selection ends there. +/// On a path, an upright ligature in vertical type stands with its top toward the path's start, and +/// its cell runs from 0.12 em before the pen to 0.88 em after it, with or without the font's vertical +/// metrics. The inner stop lies at the middle of that cell, and the first character's selection ends +/// there. #[test] fn an_upright_ligature_on_a_path_takes_its_stop_in_its_turned_cell() { for (font, family) in [(ligature_caret_font(None), LIGATURE_CARET_FAMILY), (vertical_ligature_caret_font(900), VERTICAL_LIGATURE_CARET_FAMILY)] { @@ -688,6 +689,70 @@ fn an_upright_ligature_on_a_path_takes_its_stop_in_its_turned_cell() { } } +/// On a path, an upright ligature in vertical type reads along the path in order: the top of its +/// cell comes first, and its font caret (600 units down the cell) is drawn where the caret, a click +/// and the selection put the stop, on a path running either way with any path effect. A glyph lying +/// on its side keeps its top toward the path's end. +#[test] +fn an_upright_ligature_on_a_path_reads_along_it_in_order() { + use vectorcraft_doc::PathEffect; + let db = db_with(vertical_ligature_caret_font(280)); + let mut t = point("fi", VERTICAL_LIGATURE_CARET_FAMILY); + t.vertical = true; + t.runs[0].style.standard_vertical_roman = true; + for (to, effect) in [ + (1000.0, PathEffect::Rainbow), + (-1000.0, PathEffect::Rainbow), + (-1000.0, PathEffect::Skew), + (-1000.0, PathEffect::Ribbon3d), + (-1000.0, PathEffect::StairStep), + (-1000.0, PathEffect::Gravity), + ] { + let mut line = BezPath::new(); + line.move_to((0.0, 0.0)); + line.line_to((to, 0.0)); + t.kind = TextKind::OnPath { path: PathData::from_bezpath(&line), start: 0.0, end: None }; + t.path_effect = effect; + let l = layout(&db, &t); + let Some(g) = l.glyphs.iter().find(|g| g.len == 2) else { panic!("f + i is one glyph: {:?}", l.glyphs) }; + let &[(1, f)] = g.carets.as_slice() else { panic!("one inner stop: {:?}", g.carets) }; + assert!((f - 0.6).abs() < 1e-9, "{f}"); + let tangent = Vec2::new(g.angle.cos(), g.angle.sin()); + let along = |p: Point| (p - g.origin).dot(tangent); + // Font units, y down: the cell's top is 880 units above the baseline and its foot 120 below. + let (top, foot) = (along(g.xf * Point::new(0.0, -880.0)), along(g.xf * Point::new(0.0, 120.0))); + assert!(top < foot, "{to} {effect:?}: the top comes first along the path: {top} {foot}"); + let (start, width) = (g.width_start, g.width); + assert!((top - start).abs() < 1e-6 && (foot - top - width).abs() < 1e-6, "{to} {effect:?}: {top} {foot} {start} {width}"); + let drawn = along(g.xf * Point::new(0.0, -280.0)); + assert!((drawn - (top + (foot - top) * 0.6)).abs() < 1e-6, "{to} {effect:?}: {drawn} against {top} {foot}"); + let (a, b) = caret_position(&l, 1); + let caret = along(a.midpoint(b)); + assert!((caret - drawn).abs() < 1e-6, "{to} {effect:?}: the caret stands at the drawn stop: {caret} against {drawn}"); + assert_eq!(hit_byte(&l, g.origin + tangent * (drawn + 0.3)), 1, "{to} {effect:?}"); + assert_eq!(hit_byte(&l, g.origin + tangent * (drawn - 0.3)), 1, "{to} {effect:?}"); + assert_eq!(hit_byte(&l, g.origin + tangent * 0.3), 0, "{to} {effect:?}"); + let q = selection_quads(&l, 0, 1); + assert_eq!(q.len(), 1, "{q:?}"); + let (q0, q1) = (along(q[0][0]), along(q[0][1])); + assert!((q0.min(q1) - drawn.min(0.0)).abs() < 1e-6 && (q0.max(q1) - drawn.max(0.0)).abs() < 1e-6, "{to} {effect:?}: {q:?} against {drawn}"); + } + // Without Standard Vertical Roman Alignment the letter lies on its side: its top still faces + // the path's end and its advance runs across the path, to its right. + let mut line = BezPath::new(); + line.move_to((0.0, 0.0)); + line.line_to((1000.0, 0.0)); + t.kind = TextKind::OnPath { path: PathData::from_bezpath(&line), start: 0.0, end: None }; + t.path_effect = PathEffect::Rainbow; + t.runs[0].style.standard_vertical_roman = false; + let l = layout(&db, &t); + let Some(s) = l.glyphs.iter().find(|g| g.len == 2) else { panic!("f + i is one glyph: {:?}", l.glyphs) }; + let tangent = Vec2::new(s.angle.cos(), s.angle.sin()); + let right = Vec2::new(-tangent.y, tangent.x); + let (pen, up, ahead) = (s.xf * Point::ZERO, s.xf * Point::new(0.0, -500.0), s.xf * Point::new(500.0, 0.0)); + assert!((up - pen).dot(tangent) > 1.0 && (ahead - pen).dot(right) > 1.0, "{pen:?} {up:?} {ahead:?}"); +} + /// Tracking that makes an upright ligature's advance negative leaves it upright: it keeps its /// caret down its cell, and its cell starts where it does without tracking, with or without the /// font's vertical metrics.