Merge pull request #5468 from dotCooCoo/fix/vertical-path-upright-hit

Type: the stop inside an upright ligature on a vertical path follows its drawn cell
This commit is contained in:
BFlat
2026-11-04 09:15:43 +07:00
committed by GitHub
3 changed files with 78 additions and 1 deletions
+15
View File
@@ -382,6 +382,21 @@ 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;
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));
if start.is_finite() && end > start { (end - start, start) } else { (width, width_start) }
}
_ => (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) });
}
+3 -1
View File
@@ -166,7 +166,9 @@ pub struct PositionedGlyph {
/// in text space: the font's placement of the glyph (`GPOS` x placement), less a leading space
/// 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.
/// 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
/// before the pen without the font's vertical metrics.
pub width_start: f64,
}
+60
View File
@@ -459,6 +459,66 @@ 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.
#[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)] {
let db = db_with(font);
let mut line = BezPath::new();
line.move_to((0.0, 0.0));
line.line_to((1000.0, 0.0));
let mut t = point("fi", family);
t.vertical = true;
t.runs[0].style.standard_vertical_roman = true;
t.kind = TextKind::OnPath { path: PathData::from_bezpath(&line), start: 0.0, end: None };
let l = layout(&db, &t);
assert!(l.on_path);
let Some(g) = l.glyphs.iter().find(|g| g.len == 2) else { panic!("f + i is one glyph: {:?}", l.glyphs) };
assert_eq!(g.carets, vec![(1, 0.5)], "{family}");
assert!((g.width - 100.0).abs() < 1e-9 && (g.width_start + 12.0).abs() < 1e-9, "{family}: {} {}", g.width, g.width_start);
let x = |b| caret_position(&l, b).0.x - g.origin.x;
assert!((x(1) - 38.0).abs() < 1e-6, "{family}: {}", x(1));
let q = selection_quads(&l, 0, 1);
assert_eq!(q.len(), 1, "{q:?}");
assert!(((q[0][1] - q[0][0]).hypot() - 38.0).abs() < 1e-6, "{family}: {q:?}");
// A baseline shift moves the turned glyph along the path, and its stop with it.
let ink = g.outline.bounding_box().x0;
t.runs[0].style.baseline_shift = 10.0;
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 moved = s.outline.bounding_box().x0 - ink;
assert!(moved.abs() > 1.0, "{family}: the shift moves the glyph along the path: {moved}");
let stop = caret_position(&l, 1).0.x - s.origin.x;
assert!((stop - 38.0 - moved).abs() < 1e-6, "{family}: {stop} against {}", 38.0 + moved);
// Horizontal Scale widens the glyph across the path; its stop stays inside the glyph.
t.runs[0].style.baseline_shift = 0.0;
t.runs[0].style.h_scale = 200.0;
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 bounds = s.outline.bounding_box();
let stop = caret_position(&l, 1).0.x - s.origin.x;
assert!(bounds.x0 < stop && stop < bounds.x1 && stop > 0.0, "{family}: {stop} outside {bounds:?}");
t.runs[0].style.h_scale = 100.0;
// Skew on a slanted path: the stop stays inside the drawn glyph along the path.
let mut slant = BezPath::new();
slant.move_to((0.0, 0.0));
slant.line_to((1000.0, 1000.0));
t.runs[0].style.baseline_shift = 0.0;
t.kind = TextKind::OnPath { path: PathData::from_bezpath(&slant), start: 0.0, end: None };
t.path_effect = vectorcraft_doc::PathEffect::Skew;
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 ink = s.outline.elements().iter().filter_map(|el| el.end_point()).map(|p| (p - s.origin).dot(tangent));
let (lo, hi) = ink.fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), d| (lo.min(d), hi.max(d)));
let (a, b) = caret_position(&l, 1);
let stop = (a.midpoint(b) - s.origin).dot(tangent);
assert!(lo < stop && stop < hi, "{family}: {stop} outside {lo}..{hi}");
}
}
/// 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.