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.