T: Type on a Path — tool, options, screen and PDF

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas
2026-10-02 20:34:20 -04:00
co-authored by Claude Opus 5.5
parent bfe4220840
commit eb4fa9c64c
15 changed files with 454 additions and 6 deletions
+81 -1
View File
@@ -303,7 +303,37 @@ pub fn frame_specs(doc: &Document, sid: StoryId) -> Vec<FrameSpec> {
// Grid lines are at spread y = start + n·inc (page tops are y = 0); map into inner space (translation only).
let ty = xf.translation().y;
let grid = (inc > 0.0).then_some((start - ty, inc));
out.push(FrameSpec { id: fid, area: item.text_area(), opts: tf.options.clone(), exclusions, page_name, page, grid, left_page });
// Type on a path: one line as long as the path (from the start offset).
let (area, opts) = match &tf.options.path {
Some(pt) => {
let len = designcraft_geom::warp::PathWarp::new(&item.path.to_bezpath(), false).length();
let size = st
.paras
.first()
.map(|p| doc.styles.resolve_para(p).1)
.map(|base| st.runs().map(|(_, f)| doc.styles.resolve_char(&base, f).size).fold(base.size, f64::max))
.unwrap_or(12.0);
let opts = TextFrameOptions {
columns: 1,
inset: [0.0; 4],
first_baseline: designcraft_doc::FirstBaseline::Ascent,
path: None,
..tf.options.clone()
};
(Rect::new(0.0, 0.0, (len - pt.start).max(1.0), size * 1.6), opts)
}
None => (item.text_area(), tf.options.clone()),
};
out.push(FrameSpec {
id: fid,
area,
opts,
exclusions: if tf.options.path.is_some() { vec![] } else { exclusions },
page_name,
page,
grid,
left_page,
});
}
out
}
@@ -1593,3 +1623,53 @@ fn line_dist(l: &Line, y: f64) -> f64 {
#[cfg(test)]
mod tests;
/// Each visible glyph of a type-on-a-path frame placed on the path: its outline in frame space
/// (grouped by run style).
pub fn path_glyphs(
cs: &ComposedStory,
ft: &FrameText,
path: &designcraft_geom::BezPath,
pt: &designcraft_doc::PathType,
) -> Vec<(u32, designcraft_geom::BezPath)> {
let db = designcraft_fonts::FontDb::global();
let warp = designcraft_geom::warp::PathWarp::new(path, pt.flip);
let mut runs: Vec<(u32, designcraft_geom::BezPath)> = Vec::new();
for l in &ft.lines {
// The part of the type that sits on the path.
let on = match pt.align {
designcraft_doc::PathAlign::Baseline => l.baseline,
designcraft_doc::PathAlign::Ascender => l.baseline - l.ascent,
designcraft_doc::PathAlign::Descender => l.baseline + l.descent,
designcraft_doc::PathAlign::Center => l.baseline - (l.ascent - l.descent) / 2.0,
};
for g in l.glyphs.iter().filter(|g| g.visible) {
let outline = db.outline(&g.face, g.gid);
if outline.elements().is_empty() {
continue;
}
let style = &cs.styles[g.style as usize];
let mid = g.x + g.adv / 2.0;
let Some((p, a)) = warp.at(pt.start + mid) else { continue };
let place = designcraft_geom::Affine::translate(p.to_vec2())
* designcraft_geom::Affine::rotate(a)
* designcraft_geom::Affine::translate((-mid, -on));
let skew = if style.skew != 0.0 {
designcraft_geom::Affine::new([1.0, 0.0, -style.skew.to_radians().tan(), 1.0, 0.0, 0.0])
} else {
designcraft_geom::Affine::IDENTITY
};
let m =
place * designcraft_geom::Affine::translate((g.x, l.baseline + g.y)) * skew * designcraft_geom::Affine::scale_non_uniform(g.sx, g.sy);
let bp = match runs.iter_mut().find(|r| r.0 == g.style) {
Some(r) => &mut r.1,
None => {
runs.push((g.style, designcraft_geom::BezPath::new()));
&mut runs.last_mut().expect("pushed").1
}
};
bp.extend((m * outline.as_ref().clone()).elements().iter().copied());
}
}
runs
}