Merge pull request #4119 from ham9zah/fix/joined-type-on-path

Type on a path: joined scripts bend along the path so their joins stay connected
This commit is contained in:
BFlat
2026-10-26 09:21:44 +07:00
committed by GitHub
7 changed files with 420 additions and 5 deletions
+35 -3
View File
@@ -1661,7 +1661,7 @@ impl Exporter<'_> {
// The outlines left to fill as paths: all of them, or those real text leaves.
let outlines = match plain.as_deref().filter(|_| !fill.is_none() && !ex.area_paint(fill, true)) {
Some(text) => {
let (mut left, unwritten) = ex.glyph_text(s, &layout, i, text, fill, true);
let (mut left, unwritten) = ex.glyph_text(s, &layout, i, text, fill, true, Some(false));
images = Some(unwritten);
left.extend(run_bars.iter());
Cow::Owned(left)
@@ -1701,7 +1701,17 @@ impl Exporter<'_> {
s.push_transform(&text_space);
// The paint only lets the writer write the text; the glyphs it can't write
// as text draw nothing, and colour bitmap glyphs are drawn already.
self.glyph_text(s, &layout, i, text, &Paint::solid(Color::gray(0.0)), false);
self.glyph_text(s, &layout, i, text, &Paint::solid(Color::gray(0.0)), false, None);
s.pop();
});
} else if let Some(text) = plain.as_deref().filter(|_| layout.glyphs.iter().any(|g| g.run == i && g.bend.is_some())) {
// Glyphs bent along a path were drawn as their outlines (text is set straight):
// they are written again as invisible text, straight where they stand, so they can
// still be selected, searched and copied.
self.invisible = true;
crate::forms::form(s, Mark::Invisible, |s| {
s.push_transform(&text_space);
self.glyph_text(s, &layout, i, text, &Paint::solid(Color::gray(0.0)), false, Some(true));
s.pop();
});
}
@@ -1771,7 +1781,21 @@ impl Exporter<'_> {
/// indices of those it can't write or draw (SVG glyphs) are returned with the outlines. The
/// hyphen the layout adds where it breaks a line inside a word ([`break_hyphen`]) is written
/// as the character its glyph stands for, on its own after the glyphs before it.
fn glyph_text(&mut self, s: &mut Surface, layout: &TextLayout, run: usize, text: &str, fill: &Paint, images: bool) -> (BezPath, Vec<usize>) {
///
/// `bent`: `Some(false)` writes the glyphs bent along a path ([`vectorcraft_text::GlyphBend`])
/// as outlines, not text (text is set straight); `Some(true)` writes only those, as text where
/// they stand straight; `None` writes all of them as text.
#[allow(clippy::too_many_arguments)]
fn glyph_text(
&mut self,
s: &mut Surface,
layout: &TextLayout,
run: usize,
text: &str,
fill: &Paint,
images: bool,
bent: Option<bool>,
) -> (BezPath, Vec<usize>) {
let (mut rest, mut unwritten) = (BezPath::new(), vec![]);
let mut line: Option<GlyphLine> = None;
// The characters the glyphs stand for: the plain text, then, when the run has glyphs that
@@ -1783,6 +1807,14 @@ impl Exporter<'_> {
if coloured && !images {
continue;
}
match (bent, g.bend.is_some()) {
(Some(false), true) => {
rest.extend(g.outline.iter());
continue;
}
(Some(true), false) => continue,
_ => {}
}
// A list marker's glyphs stand for no character of the text either; they are not
// line-break hyphens, whatever the marker.
let hyphen = if layout.marker_of(gi).is_some() { None } else { break_hyphen(g) };
+14 -1
View File
@@ -1939,7 +1939,14 @@ impl Writer<'_> {
}
let id = self.id_attr(n);
let a = self.attrs(&css::transparency(n));
self.line(&format!("<g{id}{a}>"));
// Glyphs bent along a path (joined scripts on a curve) can only be outlines: the group
// names its text for screen readers and search.
let label = if lay.glyphs.iter().any(|g| g.bend.is_some()) {
format!(" role=\"img\" aria-label=\"{}\"", xml_escape(&t.plain_text()))
} else {
String::new()
};
self.line(&format!("<g{id}{a}{label}>"));
self.depth += 1;
// The glyphs stay in text space; the text's transform joins the document's meanwhile.
let saved = self.xf;
@@ -1992,6 +1999,12 @@ impl Writer<'_> {
return self.text_outlines(n, t);
}
if let TextKind::OnPath { path, start, end } = &t.kind {
// A reader lays a `<textPath>`'s glyphs straight: joined glyphs bent along the path
// keep their look as outlines.
if lay.glyphs.iter().any(|g| g.bend.is_some()) {
self.warn("joined type bent along a path is outlined to keep its look");
return self.text_outlines(n, t);
}
// `<textPath>` has a start offset only: an end bracket, Align to Path and Spacing
// keep their look as outlines.
if end.is_some() || t.path_align != vectorcraft_doc::PathAlign::Baseline || t.path_spacing != 0.0 {
+119 -1
View File
@@ -2,7 +2,7 @@
use std::ops::Range;
use kurbo::{Affine, BezPath, PathEl, Point, Rect, Shape, Vec2};
use kurbo::{Affine, BezPath, ParamCurve, PathEl, Point, Rect, Shape, Vec2};
use unicode_bidi::{BidiInfo, Level};
use vectorcraft_doc::text::Kashida;
use vectorcraft_doc::{
@@ -11,6 +11,7 @@ use vectorcraft_doc::{
};
use vectorcraft_geom::{ArcPath, PathData};
use crate::GlyphBend;
use crate::composer::{Breakpoint, Params, compose};
use crate::fontdb::FontDb;
use crate::hyphen::hyphen_points_with;
@@ -253,6 +254,7 @@ impl Ctx<'_> {
scale: 1.0,
image,
colour,
bend: None,
});
}
@@ -1728,6 +1730,7 @@ mod wrapping_tests {
vpal: None,
level: unicode_bidi::Level::ltr(),
no_break: false,
cursive: false,
})
.collect()
}
@@ -2541,6 +2544,107 @@ fn path_steps(ap: &ArcPath, glyphs: &[SGlyph], from: f64, spacing: f64) -> Vec<f
.collect()
}
/// How closely a bent glyph's curves follow the path (text space units).
const BEND_TOLERANCE: f64 = 0.01;
/// The most pieces one segment of a glyph's outline is cut into to bend it.
const BEND_PIECES: usize = 32;
/// The most elements a bent glyph outline may have; beyond that the glyph stays straight.
const BEND_MAX_ELEMENTS: usize = 8192;
/// `outline` (text space, placed straight by `pre`, which takes the glyph's pen space there) bent
/// along `ap` as `bend` says ([`GlyphBend`]); `None` where the path doesn't turn under the glyph
/// (it stays as it is, exactly), or the bent outline would be too large or not finite.
fn bend_outline(outline: &BezPath, pre: Affine, ap: &ArcPath, bend: &GlyphBend) -> Option<BezPath> {
let GlyphBend { s, step, advance, rise } = *bend;
if outline.elements().is_empty() || !(advance > 1e-9 && step.is_finite() && step > 0.0 && s.is_finite() && rise.is_finite()) {
return None;
}
// How far the path turns under the glyph; none: it lies straight already.
let ((_, a), (_, b)) = (ap.at(s), ap.at(s + step));
let turn = a.cross(b).atan2(a.dot(b)).abs();
if !(turn.is_finite() && turn > 1e-9) {
return None;
}
let det = pre.determinant();
if !(det.is_finite() && det.abs() > 1e-12) {
return None;
}
let to_pen = pre.inverse();
let k = step / advance;
let map = |q: Point| {
let (p, d) = ap.at(s + q.x * k);
p + Vec2::new(-d.y, d.x) * (q.y + rise)
};
// Pieces short enough that mapping a piece's control points stays within the tolerance: the
// error of a piece of length L under curvature c, at height y, is about L² c (1 + |y| c) / 8.
let curvature = turn / step;
let pen = to_pen * outline.clone();
let height = pen.bounding_box().y0.abs().max(pen.bounding_box().y1.abs()) + rise.abs();
let piece = ((8.0 * BEND_TOLERANCE) / (curvature * (1.0 + height * curvature))).sqrt();
let pieces = |a: Point, b: Point| -> usize {
let n = ((b - a).hypot() / piece).ceil();
if n.is_finite() { (n as usize).clamp(1, BEND_PIECES) } else { BEND_PIECES }
};
let mut out = BezPath::new();
let (mut start, mut cur) = (Point::ZERO, Point::ZERO);
let line = |out: &mut BezPath, from: Point, to: Point| {
let n = pieces(from, to);
for i in 1..=n {
out.line_to(map(from.lerp(to, i as f64 / n as f64)));
}
};
for el in pen.elements() {
match *el {
PathEl::MoveTo(p) => {
out.move_to(map(p));
(start, cur) = (p, p);
}
PathEl::LineTo(p) => {
line(&mut out, cur, p);
cur = p;
}
PathEl::QuadTo(p1, p2) => {
let c = kurbo::QuadBez::new(cur, p1, p2).raise();
let n = pieces(cur, p1).max(pieces(p1, p2));
for i in 0..n {
let q = c.subsegment(i as f64 / n as f64..(i + 1) as f64 / n as f64);
out.curve_to(map(q.p1), map(q.p2), map(q.p3));
}
cur = p2;
}
PathEl::CurveTo(p1, p2, p3) => {
let c = kurbo::CubicBez::new(cur, p1, p2, p3);
let n = pieces(cur, p1).max(pieces(p1, p2)).max(pieces(p2, p3));
for i in 0..n {
let q = c.subsegment(i as f64 / n as f64..(i + 1) as f64 / n as f64);
out.curve_to(map(q.p1), map(q.p2), map(q.p3));
}
cur = p3;
}
PathEl::ClosePath => {
if (cur - start).hypot() > 1e-12 {
line(&mut out, cur, start);
}
out.close_path();
cur = start;
}
}
if out.elements().len() > BEND_MAX_ELEMENTS {
return None;
}
}
let finite = |p: Point| p.x.is_finite() && p.y.is_finite();
out.elements()
.iter()
.all(|el| match *el {
PathEl::MoveTo(p) | PathEl::LineTo(p) => finite(p),
PathEl::QuadTo(a, b) => finite(a) && finite(b),
PathEl::CurveTo(a, b, c) => finite(a) && finite(b) && finite(c),
PathEl::ClosePath => true,
})
.then_some(out)
}
/// Lay type on a path out along `path` (text space), between its brackets.
fn on_path(cx: &mut Ctx<'_>, paras: &[Range<usize>], t: &TextObject, path: &PathData) {
cx.out.on_path = true;
@@ -2786,6 +2890,19 @@ fn on_path(cx: &mut Ctx<'_>, paras: &[Range<usize>], t: &TextObject, path: &Path
}
}
cx.emit(g, pre, origin, angle, g.adv, 0);
// A glyph of a script whose letters join is bent along a curved path (Rainbow), so its
// joining strokes meet its neighbours' instead of standing off the curve.
if t.path_effect == PathEffect::Rainbow && g.cursive && !cx.vertical && g.inline.is_none() {
let bend = GlyphBend { s, step, advance: g.adv, rise };
if let Some(last) = cx.out.glyphs.last_mut()
&& last.image.is_none()
&& last.colour.is_none()
&& let Some(bent) = bend_outline(&last.outline, pre, &ap, &bend)
{
last.outline = bent;
last.bend = Some(bend);
}
}
x += step;
}
let (ps, _) = ap.at(s0);
@@ -3212,5 +3329,6 @@ fn ruby_glyph(g: &PositionedGlyph, m: Affine, run: usize, byte: usize, line: usi
// Ruby draws outlines only.
image: None,
colour: None,
bend: None,
}
}
+21
View File
@@ -132,6 +132,25 @@ pub struct PositionedGlyph {
/// A colour glyph's art (COLR layers, an SVG document's art), drawn through `xf` instead of an
/// outline: a group in font units (y down). Its `outline` is empty.
pub colour: Option<Arc<vectorcraft_doc::Node>>,
/// Type on a curved path in a script whose letters join: the glyph's `outline` is bent along
/// the path ([`GlyphBend`]) so it meets its neighbours, and `xf` is only the straight glyph
/// nearest it (for the caret, hit testing and tools). `None`: the glyph is drawn through `xf`.
pub bend: Option<GlyphBend>,
}
/// How a glyph on a path is bent along it: a point of its pen space (x along its advance from its
/// left end, y down from the baseline) goes to `P(s + x · step / advance) + (y + rise) · N` there,
/// `P` the path's point at a distance along it and `N` its normal on the glyph's lower side.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct GlyphBend {
/// Where the glyph's left end is along the path.
pub s: f64,
/// How far along the path the glyph runs.
pub step: f64,
/// The glyph's advance.
pub advance: f64,
/// How far down the type moves off the path (Align to Path).
pub rise: f64,
}
impl PositionedGlyph {
@@ -704,6 +723,8 @@ mod tests_joiners;
#[cfg(test)]
mod tests_justify;
#[cfg(test)]
mod tests_path_joins;
#[cfg(test)]
mod tests_ruby;
#[cfg(test)]
mod tests_scripts;
+26
View File
@@ -65,6 +65,28 @@ pub(crate) struct SGlyph {
/// No Break (Character panel menu): keep this glyph on the same line as the neighbouring
/// glyphs that have it.
pub no_break: bool,
/// The glyph is of a script whose letters join (Arabic, Syriac, N'Ko, Mongolian, Adlam…),
/// decided for the whole segment it was shaped in ([`joins`]): on a curved path it is bent
/// along the path so its joins meet.
pub cursive: bool,
}
/// Does `script` join its letters (a cursive script, whose neighbouring glyphs touch)?
pub(crate) fn joins(script: Script) -> bool {
matches!(
script,
Script::Arabic
| Script::Syriac
| Script::Nko
| Script::Mongolian
| Script::Adlam
| Script::Mandaic
| Script::Manichaean
| Script::Psalter_Pahlavi
| Script::Hanifi_Rohingya
| Script::Sogdian
| Script::Phags_Pa
)
}
/// How `vpal` and the vertical kerning with it move an upright glyph in vertical type and change
@@ -560,6 +582,7 @@ fn inline_glyph(face: &Arc<FontFace>, st: &CharStyle, art: &InlineArt, run: usiz
inline: Some(InlineBox { art: drawn, xf }),
word_end: false,
no_break: st.no_break,
cursive: false,
}
}
@@ -610,6 +633,8 @@ fn shape_segment(text: &str, context: &Range<usize>, seg: &Segment, feats: &OtFe
let last = span.partition_point(|(r, _)| r.start < range.end).max(first);
let source_runs = &(source_runs.start + first..source_runs.start + last);
let text_seg = &text[range.clone()];
// The segment's script: that of its first character of a script of its own.
let cursive = text_seg.chars().map(|c| c.script()).find(|s| !matches!(s, Script::Common | Script::Inherited)).is_some_and(joins);
let full = st.size.max(0.0);
// Superscript/subscript and small capitals shrink the glyphs; line metrics keep the full size.
let (pos_scale, pos_shift) = st.position.scale_shift(full);
@@ -820,6 +845,7 @@ fn shape_segment(text: &str, context: &Range<usize>, seg: &Segment, feats: &OtFe
inline: None,
word_end: false,
no_break: gst.no_break,
cursive,
});
}
}
+1
View File
@@ -162,6 +162,7 @@ fn lines_of_clusters(lines: &[&[(usize, usize, f64)]], rtl: bool) -> TextLayout
scale: 1.0,
image: None,
colour: None,
bend: None,
});
x += advance;
}
+204
View File
@@ -0,0 +1,204 @@
//! Joined (cursive) type on a curved path: neighbouring glyphs must meet where they join. The gap
//! between one glyph's edge and the next one's, at the baseline and at the height of the joining
//! stroke, is the measure. Fonts are installed ones (none is kept in the repository): the tests
//! that need an Arabic font say so and stop without one.
use super::*;
use kurbo::{Affine, BezPath, Point, Shape};
use std::sync::Arc;
use vectorcraft_doc::{CharStyle, TextKind};
fn arabic_face() -> Option<Arc<FontFace>> {
let db = FontDb::global();
["Noto Sans Arabic", "Arial", "Geeza Pro"]
.into_iter()
.filter_map(|name| db.face(name, "Regular"))
.find(|f| ['س', 'ل', 'ة', 'م', 'ت', 'ص'].iter().all(|c| f.covers(*c)))
}
/// A circle of radius `r` round the origin, one closed path.
fn circle(r: f64) -> vectorcraft_geom::PathData {
vectorcraft_geom::PathData::from_bezpath(&kurbo::Circle::new((0.0, 0.0), r).to_path(1e-3))
}
/// Type `s` in `style` on a circle of radius `r`, the whole loop for it.
fn on_circle(s: &str, style: CharStyle, r: f64) -> TextLayout {
let mut t = TextObject::point(Point::ZERO, s, style);
t.kind = TextKind::OnPath { path: circle(r), start: 0.25, end: None };
layout(FontDb::global(), &t)
}
/// For each pair of neighbouring glyphs on the line (in visual order, inside a word), how far
/// apart the right edge of the left one and the left edge of the right one are, at the baseline
/// and `h` above it.
fn gaps(l: &TextLayout, text: &str, h: f64) -> Vec<(f64, f64)> {
// Visual order: the glyphs as placed, sorted along the line by where their left edge is on it.
let edges: Vec<(Point, Point, Point, Point, usize)> = l
.glyphs
.iter()
.filter(|g| text.get(g.byte..g.byte + g.len).is_some_and(|c| !c.trim().is_empty()))
.map(|g| {
let [a, b, ..] = g.xf.as_coeffs();
let sx = a.hypot(b);
let w = g.advance / sx;
let up = h / sx;
(g.xf * Point::new(0.0, 0.0), g.xf * Point::new(w, 0.0), g.xf * Point::new(0.0, -up), g.xf * Point::new(w, -up), g.byte)
})
.collect();
// Which word a byte is in: the spaces before it.
let word = |b: usize| text.get(..b).map_or(0, |t| t.chars().filter(|c| c.is_whitespace()).count());
edges.windows(2).filter(|w| word(w[0].4) == word(w[1].4)).map(|w| ((w[0].1 - w[1].0).hypot(), (w[0].3 - w[1].2).hypot())).collect()
}
/// The largest gaps (baseline, joining height) of type `s` on a circle of radius `r` at `size`.
fn worst(face: &FontFace, s: &str, size: f64, r: f64) -> (f64, f64) {
let style = CharStyle { font_family: face.family.clone(), font_style: face.style.clone(), size, ..CharStyle::default() };
let l = on_circle(s, style, r);
gaps(&l, s, size * 0.25).into_iter().fold((0.0, 0.0), |(a, b), (x, y)| (f64::max(a, x), f64::max(b, y)))
}
/// The measure, printed: joined Arabic and separate Latin on circles at three sizes.
#[test]
fn joins_on_a_circle_are_measured() {
let Some(face) = arabic_face() else {
eprintln!("SKIPPED: joins_on_a_circle_are_measured; no installed font covers Arabic");
return;
};
eprintln!("ASSERTIONS RUN: joins_on_a_circle_are_measured; Arabic font {} {}", face.family, face.style);
for size in [12.0, 29.0, 72.0] {
for r in [60.0, 110.0, 300.0] {
let (b, j) = worst(&face, "سلسلة متصلة", size, r);
let (lb, lj) = worst(&face, "minimum", size, r);
eprintln!("size {size:>4} r {r:>5}: arabic baseline {b:.4} join {j:.4} | latin baseline {lb:.4} at {lj:.4}");
}
}
let _ = BezPath::new();
let _ = Affine::IDENTITY;
}
/// Chord placement, worked out here only (not in the layout): each glyph's baseline ends on the
/// circle, the glyph turned to the chord between them. Neighbours then meet at the baseline, and `h`
/// above it they stand apart by `h` times the angle between their chords, (a_i + a_{i+1}) / 2R.
#[test]
fn chord_placement_leaves_the_join_gap_above_the_baseline() {
let Some(face) = arabic_face() else {
eprintln!("SKIPPED: chord_placement_leaves_the_join_gap_above_the_baseline; no installed font covers Arabic");
return;
};
let (size, r) = (29.0, 110.0);
let style = CharStyle { font_family: face.family.clone(), font_style: face.style.clone(), size, ..CharStyle::default() };
let s = "سلسلة";
let l = on_circle(s, style, r);
let h = size * 0.25;
let advances: Vec<f64> = l.glyphs.iter().map(|g| g.advance).filter(|a| *a > 1e-6).collect();
let mut worst: f64 = 0.0;
for w in advances.windows(2) {
let (a, b) = (w[0], w[1]);
// The chords' directions differ by the angle between their middles on the circle.
let turn = (a + b) / (2.0 * r);
let gap = 2.0 * h * (turn * 0.5).sin();
let predicted = h * (a + b) / (2.0 * r);
assert!((gap - predicted).abs() < 0.01, "{gap} vs {predicted}");
worst = worst.max(gap);
}
eprintln!("chord placement at {size} pt on radius {r}: baseline gap 0, join-height gap {worst:.3}");
assert!(worst > 0.5, "chord placement alone leaves a visible gap: {worst}");
}
/// A circle of radius `r` run the other way (clockwise): type on it stands on the inside, its
/// tops towards the centre, as Flip puts it.
fn circle_reversed(r: f64) -> vectorcraft_geom::PathData {
let mut bp = kurbo::Circle::new((0.0, 0.0), r).to_path(1e-3);
bp = bp.reverse_subpaths();
vectorcraft_geom::PathData::from_bezpath(&bp)
}
fn on(path: vectorcraft_geom::PathData, s: &str, style: CharStyle) -> TextLayout {
let mut t = TextObject::point(Point::ZERO, s, style);
t.kind = TextKind::OnPath { path, start: 0.25, end: None };
layout(FontDb::global(), &t)
}
/// The largest gap between neighbouring bent glyphs of a word at the baseline and `h` above it,
/// where their bends put their edges; and how many glyphs were bent.
fn bent_gaps(l: &TextLayout, text: &str, path: &vectorcraft_geom::PathData, h: f64) -> (f64, f64, usize) {
let ap = vectorcraft_geom::ArcPath::new(path);
let at = |b: &GlyphBend, x: f64, y: f64| {
let (p, d) = ap.at(b.s + x * b.step / b.advance);
p + kurbo::Vec2::new(-d.y, d.x) * (y + b.rise)
};
let word = |byte: usize| text.get(..byte).map_or(0, |t| t.chars().filter(|c| c.is_whitespace()).count());
let bent: Vec<(&GlyphBend, usize)> = l.glyphs.iter().filter_map(|g| g.bend.as_ref().map(|b| (b, g.byte))).collect();
let (mut base, mut join) = (0.0f64, 0.0f64);
for w in bent.windows(2) {
let ((a, ab), (b, bb)) = (w[0], w[1]);
if word(ab) != word(bb) {
continue;
}
base = base.max((at(a, a.advance, 0.0) - at(b, 0.0, 0.0)).hypot());
join = join.max((at(a, a.advance, -h) - at(b, 0.0, -h)).hypot());
}
(base, join, bent.len())
}
/// Joined Arabic on circles of three radii at three sizes, on both sides of the path: neighbouring
/// glyphs meet, at the baseline and at the height of the joining stroke.
#[test]
fn joined_glyphs_meet_on_a_curved_path() {
let Some(face) = arabic_face() else {
eprintln!("SKIPPED: joined_glyphs_meet_on_a_curved_path; no installed font covers Arabic");
return;
};
eprintln!("ASSERTIONS RUN: joined_glyphs_meet_on_a_curved_path; Arabic font {} {}", face.family, face.style);
let s = "سلسلة متصلة";
for size in [12.0, 29.0, 72.0] {
let style = CharStyle { font_family: face.family.clone(), font_style: face.style.clone(), size, ..CharStyle::default() };
for r in [60.0, 110.0, 300.0] {
for path in [circle(r), circle_reversed(r)] {
let l = on(path.clone(), s, style.clone());
let (base, join, n) = bent_gaps(&l, s, &path, size * 0.25);
assert!(n >= 8, "the Arabic glyphs are bent ({n})");
assert!(base <= 0.01 && join <= 0.01, "size {size} r {r}: baseline {base}, join {join}");
for g in &l.glyphs {
let finite = g.outline.elements().iter().all(|e| e.end_point().is_none_or(|p| p.x.is_finite() && p.y.is_finite()));
assert!(finite, "a bent outline that isn't finite");
}
}
}
}
}
/// Latin type on the same path is placed as before: straight glyphs, not bent, each outline its
/// font's glyph through `xf`.
#[test]
fn latin_on_a_curved_path_stays_straight() {
let db = FontDb::global();
let style = CharStyle { size: 29.0, ..CharStyle::default() };
let l = on(circle(110.0), "minimum", style);
assert!(!l.glyphs.is_empty());
for g in &l.glyphs {
assert!(g.bend.is_none());
let face = db.face_by_id(g.font_id).unwrap();
let straight = g.xf * (*db.outline(&face, g.gid)).clone();
assert_eq!(g.outline, straight, "glyph {} drawn through its xf", g.gid);
}
}
/// On a straight path nothing turns: joined type is laid out exactly as before, straight.
#[test]
fn joined_type_on_a_straight_path_is_unchanged() {
let Some(face) = arabic_face() else {
eprintln!("SKIPPED: joined_type_on_a_straight_path_is_unchanged; no installed font covers Arabic");
return;
};
let db = FontDb::global();
let mut line = BezPath::new();
line.move_to((0.0, 0.0));
line.line_to((400.0, 0.0));
let style = CharStyle { font_family: face.family.clone(), font_style: face.style.clone(), size: 29.0, ..CharStyle::default() };
let l = on(vectorcraft_geom::PathData::from_bezpath(&line), "سلسلة متصلة", style);
for g in &l.glyphs {
assert!(g.bend.is_none());
let face = db.face_by_id(g.font_id).unwrap();
assert_eq!(g.outline, g.xf * (*db.outline(&face, g.gid)).clone());
}
}