mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-11 18:54:55 +00:00
Merge pull request #6530 from dotCooCoo/fix/vertical-path-sideways-glyphs
Type: on a vertical path, letters lie a quarter turn clockwise from upright glyphs, in a column centered on the path
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -40,7 +40,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Character",
|
||||
[],
|
||||
None,
|
||||
"{id? (default: the one selected type object), start?: byte, end?: byte (default: all text), story?: bool = false (all the text of every frame of the threaded story `id` is a frame of, in one undo step; no `start` or `end`), font?, style?, size?: pt, leading?: pt|\"auto\", tracking?, kerning?: 1/1000 em|\"auto\"|\"optical\", baselineShift?: pt, hScale?: %, vScale?: %, rotation?: deg, fill?: colour|\"none\", stroke?: colour|\"none\" (#rrggbbaa and #rgba are refused in both), keepModel?: bool (fill and stroke, as paint.setFill), strokeWidth?: pt, strokeOptions?: {weight?, cap?, join?, miterLimit?, align?, dash?, dashOffset?, alignDashes?} (as stroke.set: the character stroke), underline?, strikethrough?, allCaps?: bool, smallCaps?: bool, position?: \"normal\"|\"superscript\"|\"subscript\" (sizes from Document Setup), features?: [\"dlig\", \"-liga\", …], featureChanges?: [\"tnum\", \"-onum\", …] (OpenType features turned on, or with - off, on top of each character's own; its other features are kept), charAlign?: \"romanBaseline\"|\"emBoxTop\"|\"emBoxCenter\"|\"emBoxBottom\"|\"icfTop\"|\"icfBottom\", proportionalMetrics?: bool (full-width glyphs on the font's proportional widths: `palt` in horizontal type, `vpal` in vertical type), tateChuYoko?: bool (Character panel menu › Tate-chu-yoko, with Show East Asian Options: in vertical type those characters stand side by side across one em of the column; kept on the characters and ignored in horizontal type), tcyUpDown?, tcyLeftRight?: pt -1296..1296 (Character panel menu › Tate-chu-yoko Settings: in vertical type a block of tate-chu-yoko characters moves up by a positive Up/Down and down by a negative one, right by a positive Left/Right and left by a negative one, as one piece with the values of its first character; 0 by default; automatic digit blocks and horizontal type do not move; neither turns tateChuYoko on), standardVerticalRoman?: bool (Character panel menu › Standard Vertical Roman Alignment, with Show East Asian Options: in vertical type those half-width characters, such as Latin letters and digits, stand upright one below the other instead of being rotated; spaces and tate-chu-yoko blocks are not changed; kept on the characters and ignored in horizontal type; off by default), noBreak?: bool (Character panel menu › No Break: these characters stay on one line, including across a space inside the run), kashidas?: bool (Character panel menu › Kashidas: selected characters allow insertion only when both joined neighbors have it on; on by default), charDirection?: \"default\"|\"leftToRight\"|\"rightToLeft\" (Character panel menu › Character Direction: the characters are set left to right or right to left inside their paragraph, as a Unicode bidi embedding; default: as the bidi algorithm sets them), digits?: \"default\"|\"arabic\"|\"hindi\"|\"farsi\"|\"devanagari\"|\"bengali\"|\"gurmukhi\"|\"gujarati\"|\"odia\"|\"tamil\"|\"telugu\"|\"kannada\"|\"malayalam\" (Character panel menu › Digits: the numeral system every digit is drawn in, whatever was typed: Western, Arabic-Indic (hindi), Eastern Arabic-Indic (farsi) or an Indic script's own digits), fontHeightReference?: \"emBox\"|\"capHeight\"|\"xHeight\"|\"icfBox\" (Character panel › Set Font Height Reference: the height of the font that size measures, the em box by default; with a font that has no such height, or one outside 0.1 to 2 em, the em box), language?: name|tag (Character panel › Language: one of Document Setup's languages by name or BCP 47 tag, case-insensitive, such as \"Turkish\" or \"tr\"; the characters are shaped in it, so the font uses that language's forms (OpenType `locl`); saved as the tag, English: USA, the default, as none; new type takes the document's language), fontAxes?: {\"wght\": 650, …} (Character panel › Variable Font: the setting of each variation axis named, in the axis's units and clamped to its range; null: back to the style's own setting; other axes keep theirs; tags the font has no axis for are left out; a font or style change clears them; text.fontList {family, style} lists the axes)} style a character range (size/leading/baselineShift are document points and hScale includes the object transform; runs are split at the range ends) → {id, runs, warning?: text (the font or style set isn't available: its type is drawn in the fallback font or the family's closest style)}",
|
||||
"{id? (default: the one selected type object), start?: byte, end?: byte (default: all text), story?: bool = false (all the text of every frame of the threaded story `id` is a frame of, in one undo step; no `start` or `end`), font?, style?, size?: pt, leading?: pt|\"auto\", tracking?, kerning?: 1/1000 em|\"auto\"|\"optical\", baselineShift?: pt, hScale?: %, vScale?: %, rotation?: deg, fill?: colour|\"none\", stroke?: colour|\"none\" (#rrggbbaa and #rgba are refused in both), keepModel?: bool (fill and stroke, as paint.setFill), strokeWidth?: pt, strokeOptions?: {weight?, cap?, join?, miterLimit?, align?, dash?, dashOffset?, alignDashes?} (as stroke.set: the character stroke), underline?, strikethrough?, allCaps?: bool, smallCaps?: bool, position?: \"normal\"|\"superscript\"|\"subscript\" (sizes from Document Setup), features?: [\"dlig\", \"-liga\", …], featureChanges?: [\"tnum\", \"-onum\", …] (OpenType features turned on, or with - off, on top of each character's own; its other features are kept), charAlign?: \"romanBaseline\"|\"emBoxTop\"|\"emBoxCenter\"|\"emBoxBottom\"|\"icfTop\"|\"icfBottom\", proportionalMetrics?: bool (full-width glyphs on the font's proportional widths: `palt` in horizontal type, `vpal` in vertical type), tateChuYoko?: bool (Character panel menu › Tate-chu-yoko, with Show East Asian Options: in vertical type those characters stand side by side across one em of the column; kept on the characters and ignored in horizontal type), tcyUpDown?, tcyLeftRight?: pt -1296..1296 (Character panel menu › Tate-chu-yoko Settings: in vertical type a block of tate-chu-yoko characters moves up by a positive Up/Down and down by a negative one, right by a positive Left/Right and left by a negative one, as one piece with the values of its first character; on a vertical path a positive Up/Down moves it toward the path's start and a positive Left/Right to the path's left; 0 by default; automatic digit blocks and horizontal type do not move; neither turns tateChuYoko on), standardVerticalRoman?: bool (Character panel menu › Standard Vertical Roman Alignment, with Show East Asian Options: in vertical type those half-width characters, such as Latin letters and digits, stand upright one below the other instead of being rotated; spaces and tate-chu-yoko blocks are not changed; kept on the characters and ignored in horizontal type; off by default), noBreak?: bool (Character panel menu › No Break: these characters stay on one line, including across a space inside the run), kashidas?: bool (Character panel menu › Kashidas: selected characters allow insertion only when both joined neighbors have it on; on by default), charDirection?: \"default\"|\"leftToRight\"|\"rightToLeft\" (Character panel menu › Character Direction: the characters are set left to right or right to left inside their paragraph, as a Unicode bidi embedding; default: as the bidi algorithm sets them), digits?: \"default\"|\"arabic\"|\"hindi\"|\"farsi\"|\"devanagari\"|\"bengali\"|\"gurmukhi\"|\"gujarati\"|\"odia\"|\"tamil\"|\"telugu\"|\"kannada\"|\"malayalam\" (Character panel menu › Digits: the numeral system every digit is drawn in, whatever was typed: Western, Arabic-Indic (hindi), Eastern Arabic-Indic (farsi) or an Indic script's own digits), fontHeightReference?: \"emBox\"|\"capHeight\"|\"xHeight\"|\"icfBox\" (Character panel › Set Font Height Reference: the height of the font that size measures, the em box by default; with a font that has no such height, or one outside 0.1 to 2 em, the em box), language?: name|tag (Character panel › Language: one of Document Setup's languages by name or BCP 47 tag, case-insensitive, such as \"Turkish\" or \"tr\"; the characters are shaped in it, so the font uses that language's forms (OpenType `locl`); saved as the tag, English: USA, the default, as none; new type takes the document's language), fontAxes?: {\"wght\": 650, …} (Character panel › Variable Font: the setting of each variation axis named, in the axis's units and clamped to its range; null: back to the style's own setting; other axes keep theirs; tags the font has no axis for are left out; a font or style change clears them; text.fontList {family, style} lists the axes)} style a character range (size/leading/baselineShift are document points and hScale includes the object transform; runs are split at the range ends) → {id, runs, warning?: text (the font or style set isn't available: its type is drawn in the fallback font or the family's closest style)}",
|
||||
has_doc,
|
||||
set_range_style
|
||||
),
|
||||
|
||||
+99
-18
@@ -93,6 +93,34 @@ fn takes_cell(g: &SGlyph) -> bool {
|
||||
g.adv > 0.0 || g.face.advance(g.gid) > 0.0
|
||||
}
|
||||
|
||||
/// Vertical type on a path: the turn that takes upright glyph `g` (of size `em`, its glyph space
|
||||
/// mapped onto the path by `pre`) from its local transform `local` (`unturned` takes its Character
|
||||
/// Rotation off) to the path, and where the next glyph of its cluster goes in its glyph space.
|
||||
///
|
||||
/// It stands in glyph space as it stands in vertical point type's line space: a quarter turn
|
||||
/// counterclockwise from horizontal path type's pose about the center of its cell, a baseline shift
|
||||
/// kept across the column and a leading space taken off moving it along the path, then moved across
|
||||
/// the path as a glyph lying on its side is.
|
||||
fn upright_path_turn(g: &SGlyph, em: f64, pre: Affine, local: Affine, unturned: Affine, angle: f64, advance: f64) -> (Affine, f64) {
|
||||
let cell = if takes_cell(g) { upright_cell(g) } else { advance };
|
||||
let (shift, unshift) = (Affine::translate((0.0, -g.bshift)), Affine::translate((0.0, g.bshift)));
|
||||
let about = Point::new(cell * 0.5, -upright_centre(g, cell, em));
|
||||
let mut turn =
|
||||
Affine::translate((-g.lead, g.face.ideographic_centre() * em)) * shift * Affine::rotate_about(-std::f64::consts::FRAC_PI_2, about) * unshift;
|
||||
// The effects that keep the glyph's axes fixed (Stair Step) leave its top toward the path's end on
|
||||
// a path that runs to the left: it turns half a turn about its cell's center.
|
||||
let tangent = Vec2::new(angle.cos(), angle.sin());
|
||||
let placed = pre * turn * local * unturned;
|
||||
if (placed * Point::new(0.0, -1.0) - placed * Point::ZERO).dot(tangent) > 0.0 {
|
||||
let centre = turn * shift * about;
|
||||
turn = Affine::translate(centre.to_vec2() * 2.0) * Affine::scale(-1.0) * turn;
|
||||
}
|
||||
// Its own advance across, from where the cell centers it: where the shaper set the next glyph of
|
||||
// its cluster.
|
||||
let across = g.face.advance(g.gid) * g.sx;
|
||||
(pre * turn, (cell - across) * 0.5 + across)
|
||||
}
|
||||
|
||||
/// Clear the caret stops of each glyph that shares its cluster with another glyph: only a glyph
|
||||
/// that alone stands for its characters is a ligature.
|
||||
fn keep_lone_ligature_carets(glyphs: &mut [PositionedGlyph]) {
|
||||
@@ -137,6 +165,10 @@ struct Ctx<'a> {
|
||||
/// next glyph of its cluster would take, the turn it got, the line-space x that next glyph is
|
||||
/// set after), so that a combining mark shaped into the same cluster turns with it.
|
||||
upright_cluster: Option<(usize, usize, Affine, f64)>,
|
||||
/// Vertical type on a path: the tate-chu-yoko block [`Self::emit`] is placing, as (the
|
||||
/// block's transform from its first glyph's place on the path, the center of its em in the
|
||||
/// column before that transform), so that the block's other glyphs stand with it on a curve.
|
||||
tcy_block: Option<(Affine, Point)>,
|
||||
out: TextLayout,
|
||||
}
|
||||
|
||||
@@ -329,7 +361,7 @@ impl Ctx<'_> {
|
||||
};
|
||||
// A glyph whose leading space was taken off (mojikumi) is drawn that much earlier: an
|
||||
// upright one in vertical type by moving it up the column once it stands upright.
|
||||
let upright = self.vertical && !self.on_path && g.tcy.is_none() && stands_upright(g);
|
||||
let upright = self.vertical && g.tcy.is_none() && stands_upright(g) && !(self.on_path && g.inline.is_some());
|
||||
let lead = if upright { 0.0 } else { g.lead };
|
||||
let local = match &g.inline {
|
||||
// Inline graphics: art space to glyph space (no font outline), moved along the line by
|
||||
@@ -365,28 +397,75 @@ impl Ctx<'_> {
|
||||
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
|
||||
// center 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;
|
||||
// Vertical type on a path: the column is centered on the line Align to Path puts the baseline
|
||||
// on, so the em boxes of upright glyphs, tate-chu-yoko blocks and glyphs lying on their side
|
||||
// straddle the path. Glyph space here is horizontal path type's: x runs along the path and y
|
||||
// across it, to the right of the path's direction.
|
||||
if let (true, true, Some(t)) = (self.vertical, self.on_path, g.tcy) {
|
||||
// Tate-chu-yoko stands upright as one block: set across the column, centered on its em of
|
||||
// it and on the path, its top toward the path's start, then moved by
|
||||
// its Up/Down toward the path's start and by its Left/Right to the path's left, whatever
|
||||
// the effect does to the glyph's axes. The block's first glyph sets where the block
|
||||
// stands; the others take its em, its place on the path and its column, so the block
|
||||
// stays whole on a curve.
|
||||
let (block, centre) = match self.tcy_block.filter(|_| t.pen > 0.0) {
|
||||
Some(b) => b,
|
||||
None => {
|
||||
// A later glyph without its first glyph: placed back by its pen offset.
|
||||
let (em, back) =
|
||||
if t.pen > 0.0 { (t.pen, Affine::translate((-t.pen, 0.0))) } else { (self.style_at(g.byte).size, Affine::IDENTITY) };
|
||||
let centre = Point::new(em * 0.5, 0.0);
|
||||
let squeeze =
|
||||
Affine::translate((centre.x, 0.0)) * Affine::scale_non_uniform(t.squeeze, 1.0) * Affine::translate((-centre.x, 0.0));
|
||||
let on_path = pre * back;
|
||||
let mut block = on_path * Affine::rotate_about(-std::f64::consts::FRAC_PI_2, centre) * squeeze;
|
||||
// The effects that keep the glyph's axes fixed leave the block's top toward the
|
||||
// path's end on a path that runs to the left: it turns half a turn about its
|
||||
// center, as an upright glyph does.
|
||||
let tangent = Vec2::new(angle.cos(), angle.sin());
|
||||
if (block * Point::new(centre.x, centre.y - 1.0) - block * centre).dot(tangent) > 0.0 {
|
||||
block = Affine::translate((on_path * centre).to_vec2() * 2.0) * Affine::scale(-1.0) * block;
|
||||
}
|
||||
let left = Vec2::new(tangent.y, -tangent.x);
|
||||
block = Affine::translate(-tangent * t.up + left * t.right) * block;
|
||||
self.tcy_block = Some((block, centre));
|
||||
(block, centre)
|
||||
}
|
||||
};
|
||||
let em = centre.x * 2.0;
|
||||
let across = Affine::translate((centre.x - t.width * 0.5 + t.ink, centre.y + g.face.ideographic_centre() * em));
|
||||
m = block * across * local;
|
||||
} else if self.vertical && self.on_path && (g.inline.is_some() || !stands_upright(g)) {
|
||||
// A glyph lying on its side (and an inline graphic) is a quarter turn clockwise from an
|
||||
// upright glyph, as horizontal type on the path sets it: its baseline runs along the path
|
||||
// and its top faces the path's left. It moves across the path so that the center of its
|
||||
// ideographic em box lies on the path.
|
||||
// <https://www.w3.org/TR/css-writing-modes-3/#vertical-orientations>
|
||||
// <https://www.unicode.org/reports/tr50/>
|
||||
let centre = g.face.ideographic_centre() * g.face.units_per_em() * g.sy;
|
||||
m = pre * Affine::translate((0.0, centre)) * local;
|
||||
} else if self.vertical && self.on_path {
|
||||
// An upright glyph is turned in glyph space ([`upright_path_turn`]), so every effect bends it
|
||||
// as it bends glyphs lying on their side: its top faces the path's start and the characters
|
||||
// of an upright ligature read along the path in order.
|
||||
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);
|
||||
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, in its glyph space.
|
||||
m = turn * Affine::translate((after, 0.0)) * local;
|
||||
self.upright_cluster = Some((g.byte, next + 1, turn, after));
|
||||
} else {
|
||||
let (turn, after) = upright_path_turn(g, self.style_at(g.byte).size, pre, local, unturned, angle, advance);
|
||||
m = turn * local;
|
||||
self.upright_cluster = (g.len > 0).then_some((g.byte, next + 1, turn, after));
|
||||
}
|
||||
} 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
|
||||
// from right to left across it).
|
||||
let em = self.style_at(g.byte).size;
|
||||
// from right to left across it). Every glyph of the block takes the em of its first
|
||||
// glyph, the pen offset of the others.
|
||||
let em = if t.pen > 0.0 { t.pen } else { self.style_at(g.byte).size };
|
||||
let start = origin.x - t.pen;
|
||||
let centre = Point::new(start + em * 0.5, origin.y - g.face.ideographic_centre() * em);
|
||||
let across = Affine::translate((centre.x - t.width * 0.5 + t.ink - origin.x, 0.0));
|
||||
@@ -629,6 +708,7 @@ mod emit_kashida_mark_last {
|
||||
system_layout: false,
|
||||
fractional_widths: true,
|
||||
upright_cluster: None,
|
||||
tcy_block: None,
|
||||
out: TextLayout::default(),
|
||||
};
|
||||
// Last visual glyph of the cluster: a mark that wrongly keeps the mark run (run 1).
|
||||
@@ -692,6 +772,7 @@ mod emit_kashida_mark_last {
|
||||
system_layout: false,
|
||||
fractional_widths: true,
|
||||
upright_cluster: None,
|
||||
tcy_block: None,
|
||||
out: TextLayout::default(),
|
||||
};
|
||||
// Simulate the pre-fix path: clone run from the mark glyph only.
|
||||
|
||||
@@ -374,6 +374,7 @@ fn layout_runs(db: &FontDb, t: &TextObject, opts: &LayoutOptions, set: Option<&T
|
||||
system_layout: t.system_layout,
|
||||
fractional_widths: t.fractional_widths,
|
||||
upright_cluster: None,
|
||||
tcy_block: None,
|
||||
out: TextLayout::default(),
|
||||
};
|
||||
// Vertical type: line space turned a quarter turn clockwise (lines become columns, each next
|
||||
|
||||
@@ -174,6 +174,15 @@ pub(super) fn on_path(cx: &mut Ctx<'_>, paras: &[Range<usize>], t: &TextObject,
|
||||
let s = style_metrics(cx.face_at(0).as_deref(), cx.style_at(0));
|
||||
(s.0, s.1)
|
||||
});
|
||||
// Vertical type: the caret and the selection span the column, half its em on each side of the
|
||||
// line Align to Path puts the baseline on.
|
||||
let m = if cx.vertical {
|
||||
let half = sg.iter().map(|g| g.face.units_per_em() * g.sx * 0.5).fold(f64::NAN, f64::max);
|
||||
let half = if half.is_finite() { half } else { cx.style_at(0).size * 0.5 };
|
||||
(half, half)
|
||||
} else {
|
||||
m
|
||||
};
|
||||
if ap.is_empty() {
|
||||
cx.out.overflow = !sg.is_empty();
|
||||
cx.out.lines.push(LineInfo {
|
||||
|
||||
@@ -136,6 +136,7 @@ fn composition_trim_stays_along_the_line_when_a_character_rotates() {
|
||||
system_layout: false,
|
||||
fractional_widths: true,
|
||||
upright_cluster: None,
|
||||
tcy_block: None,
|
||||
out: TextLayout::default(),
|
||||
};
|
||||
cx.emit(&glyph, pre, origin, angle, 30.0 - lead, 0);
|
||||
|
||||
@@ -655,7 +655,7 @@ fn vertical_only_settings_keep_a_horizontal_ligature_whole() {
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// its cell runs one em from the pen, as in vertical point type, 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]
|
||||
@@ -673,21 +673,21 @@ fn an_upright_ligature_on_a_path_takes_its_stop_in_its_turned_cell() {
|
||||
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);
|
||||
assert!((g.width - 100.0).abs() < 1e-9 && g.width_start.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));
|
||||
assert!((x(1) - 50.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;
|
||||
assert!(((q[0][1] - q[0][0]).hypot() - 50.0).abs() < 1e-6, "{family}: {q:?}");
|
||||
// A baseline shift moves the turned glyph across the path, and leaves its stop in place.
|
||||
let ink = g.outline.bounding_box();
|
||||
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 moved = s.outline.bounding_box().center() - ink.center();
|
||||
assert!((moved.x.abs()) < 1e-6 && (moved.y + 10.0).abs() < 1e-6, "{family}: the shift moves the glyph to the path's left: {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);
|
||||
assert!((stop - 50.0).abs() < 1e-6, "{family}: {stop}");
|
||||
// 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;
|
||||
@@ -718,7 +718,7 @@ 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.
|
||||
/// on its side is a quarter turn clockwise from that, as horizontal type on the path sets it.
|
||||
#[test]
|
||||
fn an_upright_ligature_on_a_path_reads_along_it_in_order() {
|
||||
use vectorcraft_doc::PathEffect;
|
||||
@@ -763,8 +763,8 @@ fn an_upright_ligature_on_a_path_reads_along_it_in_order() {
|
||||
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.
|
||||
// Without Standard Vertical Roman Alignment the letter lies on its side: its top faces the
|
||||
// path's left and its advance runs along the path.
|
||||
let mut line = BezPath::new();
|
||||
line.move_to((0.0, 0.0));
|
||||
line.line_to((1000.0, 0.0));
|
||||
@@ -774,9 +774,9 @@ fn an_upright_ligature_on_a_path_reads_along_it_in_order() {
|
||||
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 left = 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:?}");
|
||||
assert!((up - pen).dot(left) > 1.0 && (ahead - pen).dot(tangent) > 1.0, "{pen:?} {up:?} {ahead:?}");
|
||||
}
|
||||
|
||||
/// On a path, an upright ligature whose cell Proportional Metrics shortens (`vpal`, from 1000 units
|
||||
|
||||
@@ -184,6 +184,304 @@ fn type_on_a_path_stays_horizontal() {
|
||||
assert!(!layout(FontDb::global(), &t).vertical);
|
||||
}
|
||||
|
||||
/// `text` in vertical type on a straight path from `from` to `to`, with path effect `effect`.
|
||||
fn on_vertical_path(text: &str, from: (f64, f64), to: (f64, f64), effect: vectorcraft_doc::PathEffect) -> TextObject {
|
||||
let mut t = vertical(text);
|
||||
t.kind = TextKind::OnPath { path: PathData::from_bezpath(&kurbo::Line::new(from, to).to_path(0.1)), start: 0.0, end: None };
|
||||
t.path_effect = effect;
|
||||
t
|
||||
}
|
||||
|
||||
/// Glyph `i`'s top (its ascender direction) and its own advance direction in text space, as unit
|
||||
/// vectors.
|
||||
fn top_and_ahead(l: &TextLayout, i: usize) -> (Vec2, Vec2) {
|
||||
let g = &l.glyphs[i];
|
||||
let pen = g.xf * Point::ZERO;
|
||||
let unit = |v: Vec2| v / v.hypot();
|
||||
(unit(g.xf * Point::new(0.0, -500.0) - pen), unit(g.xf * Point::new(500.0, 0.0) - pen))
|
||||
}
|
||||
|
||||
/// On a vertical path with Rainbow, an upright glyph's top faces the path's start, and a letter lying
|
||||
/// on its side is a quarter turn clockwise from it: its top faces the path's left and its baseline
|
||||
/// runs along the path. Both turn with the path when it runs the other way. The effects that keep
|
||||
/// the glyphs' axes fixed on the page keep that relation only on a path that runs to the right.
|
||||
#[test]
|
||||
fn on_a_vertical_path_letters_lie_a_quarter_turn_clockwise_from_upright_glyphs() {
|
||||
use vectorcraft_doc::PathEffect;
|
||||
for to in [(0.0, 1000.0), (0.0, -1000.0), (1000.0, 0.0), (-1000.0, 0.0), (700.0, 700.0)] {
|
||||
let l = layout(FontDb::global(), &on_vertical_path("§L", (0.0, 0.0), to, PathEffect::Rainbow));
|
||||
let tangent = Vec2::new(to.0, to.1) / Vec2::new(to.0, to.1).hypot();
|
||||
let left = Vec2::new(tangent.y, -tangent.x);
|
||||
let (upright, _) = top_and_ahead(&l, 0);
|
||||
let (side, ahead) = top_and_ahead(&l, 1);
|
||||
assert!((upright + tangent).hypot() < 1e-6, "{to:?}: the upright top faces the start: {upright:?}");
|
||||
// A clockwise quarter turn with y down takes (x, y) to (-y, x).
|
||||
assert!((side - Vec2::new(-upright.y, upright.x)).hypot() < 1e-6, "{to:?}: {side:?} against {upright:?}");
|
||||
assert!((side - left).hypot() < 1e-6 && (ahead - tangent).hypot() < 1e-6, "{to:?}: {side:?} {ahead:?}");
|
||||
}
|
||||
// Skew, 3D Ribbon and Stair Step keep axes of the glyphs fixed on the page: on a path that runs
|
||||
// to the right the relation holds. On one that runs to the left, Skew and 3D Ribbon mirror both
|
||||
// kinds, and Stair Step keeps letters level while upright glyphs still face the path's start, so
|
||||
// letters are a quarter turn counterclockwise from upright glyphs there.
|
||||
for effect in [PathEffect::Skew, PathEffect::Ribbon3d, PathEffect::StairStep] {
|
||||
for to in [(1000.0, 0.0), (700.0, 700.0), (0.0, 1000.0), (0.0, -1000.0), (-1000.0, 0.0), (-700.0, 300.0)] {
|
||||
// Skew and 3D Ribbon slant glyphs on a slanted path, where no turn relates them exactly.
|
||||
if to.1 != 0.0 && effect != PathEffect::StairStep {
|
||||
continue;
|
||||
}
|
||||
let l = layout(FontDb::global(), &on_vertical_path("§L", (0.0, 0.0), to, effect));
|
||||
let (upright, _) = top_and_ahead(&l, 0);
|
||||
let (side, _) = top_and_ahead(&l, 1);
|
||||
let clockwise = to.0 >= 0.0;
|
||||
let want = if clockwise { Vec2::new(-upright.y, upright.x) } else { Vec2::new(upright.y, -upright.x) };
|
||||
assert!((side - want).hypot() < 1e-6, "{effect:?} {to:?}: {side:?} against {upright:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// On a vertical path, letters lying on their side are set as horizontal type on the same path sets
|
||||
/// them, with every path effect and on a path running either way, and they follow one another along
|
||||
/// the path without overlapping where the effect keeps them square to it.
|
||||
#[test]
|
||||
fn on_a_vertical_path_letters_keep_the_horizontal_path_pose() {
|
||||
use vectorcraft_doc::PathEffect;
|
||||
for effect in [PathEffect::Rainbow, PathEffect::Skew, PathEffect::Ribbon3d, PathEffect::StairStep, PathEffect::Gravity] {
|
||||
for to in [(1000.0, 0.0), (-1000.0, 0.0), (700.0, 300.0)] {
|
||||
let mut vertical = on_vertical_path("LLl", (0.0, 0.0), to, effect);
|
||||
let v = layout(FontDb::global(), &vertical);
|
||||
vertical.vertical = false;
|
||||
let h = layout(FontDb::global(), &vertical);
|
||||
for i in 0..3 {
|
||||
let (a, b) = (v.glyphs[i].xf.as_coeffs(), h.glyphs[i].xf.as_coeffs());
|
||||
assert!(a.iter().zip(b).take(4).all(|(a, b)| (a - b).abs() < 1e-9), "{effect:?} {to:?} glyph {i}: {a:?} against {b:?}");
|
||||
}
|
||||
let tangent = Vec2::new(to.0, to.1) / Vec2::new(to.0, to.1).hypot();
|
||||
let span = |i: usize| {
|
||||
let along = v.glyphs[i].outline.elements().iter().filter_map(|el| el.end_point()).map(|p| p.to_vec2().dot(tangent));
|
||||
along.fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), d| (lo.min(d), hi.max(d)))
|
||||
};
|
||||
// Skew slants the letters, Stair Step keeps them level and Gravity fans them toward the
|
||||
// path's center, so their ink can reach past the next letter's.
|
||||
if matches!(effect, PathEffect::Rainbow | PathEffect::Ribbon3d) {
|
||||
assert!((0..2).all(|i| span(i).1 <= span(i + 1).0 + 1e-6), "{effect:?} {to:?}: {:?}", (0..3).map(span).collect::<Vec<_>>());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// On a vertical path, the column is centered on the path: the em boxes of upright glyphs, of letters
|
||||
/// lying on their side and of a tate-chu-yoko block straddle it, their centers on the path line, on
|
||||
/// a path running any way.
|
||||
#[test]
|
||||
fn on_a_vertical_path_the_column_is_centered_on_the_path() {
|
||||
use vectorcraft_doc::PathEffect;
|
||||
let Some(face) = FontDb::global().face("Source Sans 3", "Regular") else { panic!("the bundled face") };
|
||||
let (em, centre) = (face.units_per_em(), face.ideographic_centre());
|
||||
for to in [(0.0, 1000.0), (0.0, -1000.0), (1000.0, 0.0), (-700.0, 700.0)] {
|
||||
let l = layout(FontDb::global(), &on_vertical_path("§H12§", (0.0, 0.0), to, PathEffect::Rainbow));
|
||||
let tangent = Vec2::new(to.0, to.1) / Vec2::new(to.0, to.1).hypot();
|
||||
let across = |p: Point| p.to_vec2().dot(Vec2::new(tangent.y, -tangent.x));
|
||||
// An upright glyph (the shaper centers a narrow one in its em box, as off a path) has its
|
||||
// ink centered on the path.
|
||||
for i in [0, 4] {
|
||||
let mid = across(ink(&l, i).center());
|
||||
assert!(mid.abs() < 1.0, "{to:?}: upright glyph {i} is {mid} off the path");
|
||||
}
|
||||
// A letter lying on its side: its em box's center is `centre` em above its baseline.
|
||||
let mid = across(l.glyphs[1].xf * Point::new(0.0, -centre * em));
|
||||
assert!(mid.abs() < 1e-6, "{to:?}: the letter's em box is {mid} off the path");
|
||||
// The digits of the block stand side by side across the path, the block centered on it.
|
||||
let block = (across(ink(&l, 2).center()) + across(ink(&l, 3).center())) * 0.5;
|
||||
assert!(block.abs() < 1.5, "{to:?}: the block is {block} off the path");
|
||||
}
|
||||
}
|
||||
|
||||
/// On a vertical path, the caret and the selection span the column, half its em on each side of the
|
||||
/// path, as the glyphs do.
|
||||
#[test]
|
||||
fn on_a_vertical_path_the_caret_and_selection_span_the_column() {
|
||||
use vectorcraft_doc::PathEffect;
|
||||
for to in [(0.0, 1000.0), (1000.0, 0.0), (-700.0, 700.0)] {
|
||||
let l = layout(FontDb::global(), &on_vertical_path("§L§", (0.0, 0.0), to, PathEffect::Rainbow));
|
||||
let tangent = Vec2::new(to.0, to.1) / Vec2::new(to.0, to.1).hypot();
|
||||
let across = |p: Point| p.to_vec2().dot(Vec2::new(tangent.y, -tangent.x));
|
||||
let (top, bottom) = caret_position(&l, "§".len());
|
||||
assert!((across(top) - 10.0).abs() < 1e-6 && (across(bottom) + 10.0).abs() < 1e-6, "{to:?}: caret {top:?} {bottom:?}");
|
||||
for q in selection_quads(&l, 0, "§L§".len()) {
|
||||
let (lo, hi) = q.iter().map(|p| across(*p)).fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), a| (lo.min(a), hi.max(a)));
|
||||
assert!((lo + 10.0).abs() < 1e-6 && (hi - 10.0).abs() < 1e-6, "{to:?}: highlight from {lo} to {hi}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// On a straight path running down from the anchor, vertical type sets every glyph where vertical
|
||||
/// point type sets it: upright glyphs with a horizontal scale stay centered on the column, a baseline
|
||||
/// shift moves an upright glyph across the column, every upright glyph keeps its own cell along the
|
||||
/// path, and a combining mark stays on its upright letter.
|
||||
#[test]
|
||||
fn on_a_downward_vertical_path_glyphs_sit_where_vertical_point_type_sets_them() {
|
||||
use crate::test_fonts::{VERTICAL_FAMILY, vertical_font};
|
||||
let run = |text: &str, style: CharStyle| TextRun { text: text.into(), style, inline: None };
|
||||
let runs = |plain: CharStyle| {
|
||||
vec![
|
||||
run("§×", plain.clone()),
|
||||
run("§", CharStyle { h_scale: 150.0, ..plain.clone() }),
|
||||
run("§", CharStyle { baseline_shift: 3.0, ..plain.clone() }),
|
||||
run("§", CharStyle { rotation: 30.0, ..plain.clone() }),
|
||||
run("L", CharStyle { h_scale: 150.0, ..plain.clone() }),
|
||||
run("L", CharStyle { baseline_shift: 3.0, ..plain.clone() }),
|
||||
run("q\u{301}x", CharStyle { standard_vertical_roman: true, ..plain.clone() }),
|
||||
run("§", plain),
|
||||
]
|
||||
};
|
||||
let compare = |db: &FontDb, point: &TextObject, name: &str| {
|
||||
let mut on_path = point.clone();
|
||||
on_path.kind =
|
||||
TextKind::OnPath { path: PathData::from_bezpath(&kurbo::Line::new((0.0, 0.0), (0.0, 3000.0)).to_path(0.1)), start: 0.0, end: None };
|
||||
let (p, v) = (layout(db, point), layout(db, &on_path));
|
||||
assert_eq!(p.glyphs.len(), v.glyphs.len(), "{name}");
|
||||
let apart: Vec<_> = (p.glyphs.iter().zip(&v.glyphs).enumerate())
|
||||
.filter(|(_, (a, b))| a.xf.as_coeffs().iter().zip(b.xf.as_coeffs()).any(|(a, b)| (a - b).abs() > 1e-6))
|
||||
.map(|(i, (a, b))| format!("glyph {i}: {:?} on the path, {:?} in point type", b.xf.as_coeffs(), a.xf.as_coeffs()))
|
||||
.collect();
|
||||
assert!(apart.is_empty(), "{name}: {apart:#?}");
|
||||
};
|
||||
let mut point = vertical("");
|
||||
point.runs = runs(CharStyle { size: 20.0, ..CharStyle::default() });
|
||||
compare(FontDb::global(), &point, "the bundled face");
|
||||
// A face with vertical metrics, whose cells hang from their vertical origins.
|
||||
let db = FontDb::with_font_dirs(vec![]);
|
||||
db.add_font(vertical_font(true).unwrap());
|
||||
point.runs = runs(CharStyle { size: 20.0, font_family: VERTICAL_FAMILY.into(), ..CharStyle::default() });
|
||||
compare(&db, &point, "vertical metrics");
|
||||
// The horizontally scaled upright glyph is wider across the column, centered where the plain one is.
|
||||
point.kind = TextKind::OnPath { path: PathData::from_bezpath(&kurbo::Line::new((0.0, 0.0), (0.0, 3000.0)).to_path(0.1)), start: 0.0, end: None };
|
||||
let l = layout(&db, &point);
|
||||
let (plain, wide) = (ink(&l, 0), ink(&l, 2));
|
||||
assert!(wide.width() > plain.width() * 1.4 && (wide.center().x - plain.center().x).abs() < 1e-6, "{plain:?} {wide:?}");
|
||||
}
|
||||
|
||||
/// With Skew on a curved vertical path, every glyph's axis along the path follows the tangent and
|
||||
/// its axis across the path stays upright on the page: an upright glyph's top runs back along the
|
||||
/// tangent and its own x axis points up the page, and a letter lying on its side keeps its top up
|
||||
/// the page and its baseline along the tangent.
|
||||
#[test]
|
||||
fn on_a_curved_vertical_path_skew_keeps_the_axis_across_the_path_upright() {
|
||||
use vectorcraft_doc::PathEffect;
|
||||
let mut arc = kurbo::BezPath::new();
|
||||
arc.move_to((0.0, 0.0));
|
||||
arc.curve_to((40.0, 0.0), (80.0, 40.0), (80.0, 120.0));
|
||||
let mut t = vertical("§L§L§");
|
||||
t.kind = TextKind::OnPath { path: PathData::from_bezpath(&arc), start: 0.0, end: None };
|
||||
t.path_effect = PathEffect::Skew;
|
||||
let l = layout(FontDb::global(), &t);
|
||||
let page_up = Vec2::new(0.0, -1.0);
|
||||
for (i, g) in l.glyphs.iter().enumerate() {
|
||||
let tangent = Vec2::new(g.angle.cos(), g.angle.sin());
|
||||
let (top, ahead) = top_and_ahead(&l, i);
|
||||
if i % 2 == 0 {
|
||||
assert!((top + tangent).hypot() < 1e-6 && (ahead - page_up).hypot() < 1e-6, "upright glyph {i}: {top:?} {ahead:?} at {tangent:?}");
|
||||
} else {
|
||||
assert!((top - page_up).hypot() < 1e-6 && (ahead - tangent).hypot() < 1e-6, "letter {i}: {top:?} {ahead:?} at {tangent:?}");
|
||||
}
|
||||
}
|
||||
let turns = l.glyphs.iter().map(|g| g.angle).fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), a| (lo.min(a), hi.max(a)));
|
||||
assert!(turns.1 - turns.0 > 0.3, "the glyphs sit on the curve: {turns:?}");
|
||||
}
|
||||
|
||||
/// A tate-chu-yoko block whose characters differ in size stays one block: every character stands on
|
||||
/// the block's baseline in the em of its first character, in vertical type and on a vertical path.
|
||||
#[test]
|
||||
fn a_tate_chu_yoko_block_of_mixed_sizes_shares_its_first_characters_em() {
|
||||
let run = |text: &str, size: f64| TextRun { text: text.into(), style: CharStyle { size, ..CharStyle::default() }, inline: None };
|
||||
let mut t = vertical("");
|
||||
t.runs = vec![run("§", 20.0), run("1", 20.0), run("2", 10.0), run("§", 20.0)];
|
||||
let mut on_path = t.clone();
|
||||
on_path.kind =
|
||||
TextKind::OnPath { path: PathData::from_bezpath(&kurbo::Line::new((0.0, 0.0), (0.0, 1000.0)).to_path(0.1)), start: 0.0, end: None };
|
||||
for (name, t) in [("vertical", &t), ("on a path", &on_path)] {
|
||||
let l = layout(FontDb::global(), t);
|
||||
assert!(l.glyphs[2].advance == 0.0, "{name}: 12 is one block: {:?}", l.glyphs.iter().map(|g| g.advance).collect::<Vec<_>>());
|
||||
let (one, two) = (ink(&l, 1), ink(&l, 2));
|
||||
assert!((one.y1 - two.y1).abs() < 0.5 && two.center().x > one.center().x, "{name}: on one baseline: {one:?} {two:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// On a vertical path, Tate-chu-yoko Settings Up/Down move a marked block toward the path's start
|
||||
/// and Left/Right move it to the right of its upright characters (the path's left), with every
|
||||
/// effect on a path running any way.
|
||||
#[test]
|
||||
fn on_a_vertical_path_tate_chu_yoko_settings_keep_their_directions() {
|
||||
use vectorcraft_doc::PathEffect;
|
||||
for effect in [PathEffect::Rainbow, PathEffect::Skew, PathEffect::Ribbon3d, PathEffect::StairStep, PathEffect::Gravity] {
|
||||
for to in [(1000.0, 0.0), (-1000.0, 0.0), (0.0, 1000.0), (-700.0, -700.0)] {
|
||||
let at = |up: f64, right: f64| {
|
||||
let marked = CharStyle { size: 20.0, tate_chu_yoko: true, tcy_up_down: up, tcy_left_right: right, ..CharStyle::default() };
|
||||
let mut t = on_vertical_path("", (0.0, 0.0), to, effect);
|
||||
t.runs = vec![
|
||||
TextRun { text: "§".into(), style: CharStyle { size: 20.0, ..CharStyle::default() }, inline: None },
|
||||
TextRun { text: "12".into(), style: marked, inline: None },
|
||||
];
|
||||
ink(&layout(FontDb::global(), &t), 1).center().to_vec2()
|
||||
};
|
||||
let tangent = Vec2::new(to.0, to.1) / Vec2::new(to.0, to.1).hypot();
|
||||
let left = Vec2::new(tangent.y, -tangent.x);
|
||||
let (plain, up, right) = (at(0.0, 0.0), at(4.0, 0.0), at(0.0, 3.0));
|
||||
assert!((up - plain + tangent * 4.0).hypot() < 1e-6, "{effect:?} {to:?}: Up/Down moved it {:?}", up - plain);
|
||||
assert!((right - plain - left * 3.0).hypot() < 1e-6, "{effect:?} {to:?}: Left/Right moved it {:?}", right - plain);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// On a vertical path, a tate-chu-yoko block stands upright like the glyphs around it: its digits
|
||||
/// read across the column, their tops toward the path's start, centered on the column.
|
||||
#[test]
|
||||
fn on_a_vertical_path_tate_chu_yoko_stands_upright() {
|
||||
use vectorcraft_doc::PathEffect;
|
||||
for to in [(0.0, 1000.0), (0.0, -1000.0), (1000.0, 0.0)] {
|
||||
let l = layout(FontDb::global(), &on_vertical_path("§12§", (0.0, 0.0), to, PathEffect::Rainbow));
|
||||
assert!(l.glyphs[1].outline.elements().len() > 1, "{to:?}: the 1 draws");
|
||||
let tangent = Vec2::new(to.0, to.1) / Vec2::new(to.0, to.1).hypot();
|
||||
let left = Vec2::new(tangent.y, -tangent.x);
|
||||
let (upright, _) = top_and_ahead(&l, 0);
|
||||
for i in [1, 2] {
|
||||
let (top, ahead) = top_and_ahead(&l, i);
|
||||
assert!((top - upright).hypot() < 1e-6 && (ahead - left).hypot() < 1e-6, "{to:?} digit {i}: {top:?} {ahead:?}");
|
||||
}
|
||||
let centre = |i: usize| ink(&l, i).center().to_vec2();
|
||||
let (one, two) = (centre(1), centre(2));
|
||||
assert!((two - one).dot(left) > 3.0 && (two - one).dot(tangent).abs() < 1.0, "{to:?}: 12 side by side: {one:?} {two:?}");
|
||||
let across = |v: Vec2| v.dot(left);
|
||||
let block = (across(one) + across(two)) * 0.5;
|
||||
assert!((block - across(centre(0))).abs() < 2.0, "{to:?}: the block is centered on the column: {block} against {}", across(centre(0)));
|
||||
let (before, after) = ((centre(0)).dot(tangent), (centre(3)).dot(tangent));
|
||||
assert!(before < one.dot(tangent) && one.dot(tangent) < after, "{to:?}: the block stands between the marks");
|
||||
}
|
||||
// On a curve the block stays whole: both digits turn alike and stand on one baseline.
|
||||
let mut arc = kurbo::BezPath::new();
|
||||
arc.move_to((0.0, 0.0));
|
||||
arc.curve_to((0.0, 40.0), (20.0, 60.0), (60.0, 60.0));
|
||||
let mut t = vertical("§12§");
|
||||
t.kind = TextKind::OnPath { path: PathData::from_bezpath(&arc), start: 0.0, end: None };
|
||||
let l = layout(FontDb::global(), &t);
|
||||
let (a, b) = (l.glyphs[1].xf.as_coeffs(), l.glyphs[2].xf.as_coeffs());
|
||||
assert!(a.iter().zip(b).take(4).all(|(a, b)| (a - b).abs() < 1e-9), "{a:?} against {b:?}");
|
||||
let (top, ahead) = top_and_ahead(&l, 1);
|
||||
let (p1, p2) = (l.glyphs[1].xf * Point::ZERO, l.glyphs[2].xf * Point::ZERO);
|
||||
assert!((p2 - p1).dot(top).abs() < 1e-6 && (p2 - p1).dot(ahead) > 3.0, "{p1:?} {p2:?}");
|
||||
// With every effect, on a path running either way, the block's top faces the path's start.
|
||||
for effect in [PathEffect::Rainbow, PathEffect::Skew, PathEffect::Ribbon3d, PathEffect::StairStep, PathEffect::Gravity] {
|
||||
for to in [(1000.0, 0.0), (-1000.0, 0.0)] {
|
||||
let l = layout(FontDb::global(), &on_vertical_path("§12§", (0.0, 0.0), to, effect));
|
||||
let tangent = Vec2::new(to.0, to.1) / Vec2::new(to.0, to.1).hypot();
|
||||
for i in [1, 2] {
|
||||
let (top, _) = top_and_ahead(&l, i);
|
||||
assert!(top.dot(tangent) < -0.5, "{effect:?} {to:?} digit {i}: {top:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn three_digits_stand_across_the_column_squeezed_into_one_em() {
|
||||
let l = layout(FontDb::global(), &vertical("§100§"));
|
||||
|
||||
+3
-2
@@ -6487,8 +6487,9 @@ with binding punctuation (`Tg`) on, which opens as `forced`. `none` isn't saved.
|
||||
`text.setFormat` and `text.setRangeStyle {tcyUpDown?, tcyLeftRight?}` set the Character panel menu's Tate-chu-yoko
|
||||
Settings of the characters, in points from -1296 to 1296 (document points, as `baselineShift`). In vertical type a
|
||||
block of tate-chu-yoko characters (`tateChuYoko`) moves up by a positive Up/Down and down by a negative one, and right
|
||||
by a positive Left/Right and left by a negative one. The block moves as one piece with the values of its first
|
||||
character, and keeps its one em down the column, its caret positions and its hit region. Two or three digits set
|
||||
by a positive Left/Right and left by a negative one. On a vertical path, a positive Up/Down moves the block toward the
|
||||
path's start and a positive Left/Right moves it to the path's left, with every path effect. The block moves as one piece
|
||||
with the values of its first character, and keeps its one em down the column, its caret positions and its hit region. Two or three digits set
|
||||
across the column without the mark do not move, and horizontal type keeps the values without moving the characters.
|
||||
Neither value turns `tateChuYoko` on. Both are 0 by default and saved only when not 0, and character styles carry
|
||||
them. A value that is not a number is refused with `` `tcyUpDown` must be a number ``, and one outside the range with
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user