SVG: centred and right-aligned type on a path keeps its place

Squashed from #5977 onto main.
This commit is contained in:
Kyle Kanouse
2026-11-07 05:15:17 +07:00
committed by bflatastic
parent 5bb21cdddf
commit 86564528d3
3 changed files with 55 additions and 7 deletions
+5 -1
View File
@@ -208,7 +208,7 @@ sync_github_name = true
[people.sadkotheguest]
real_name = "Alexander Buraga"
display_name = "Alexander Buraga"
sync_github_name = false
sync_github_name = false
[people.adikshan11]
real_name = "Adithya Shankaran"
@@ -216,3 +216,7 @@ sync_github_name = true
[people.slafebre]
real_name = "Sven Lafebre"
[people.kylekanouse]
real_name = "Kyle Kanouse"
sync_github_name = true
+16 -6
View File
@@ -519,14 +519,24 @@ impl Writer<'_> {
let space = (lay.bounds, self.xf * t.xf);
let base = self.char_props(&t.first_style(), space);
let mut props = base.clone();
match t.para_at(0).justify {
Justify::Center | Justify::JustifyCenter => props.push(("text-anchor", "middle".into())),
Justify::Right | Justify::JustifyRight => props.push(("text-anchor", "end".into())),
_ => {}
}
let (from, to) = t.kind.path_span().unwrap_or((start, 1.0));
// The anchor sits where the layout aligns the line within its span.
let at = match t.para_at(0).justify {
Justify::Center | Justify::JustifyCenter => {
props.push(("text-anchor", "middle".into()));
(from + to) / 2.0
}
Justify::Right | Justify::JustifyRight => {
props.push(("text-anchor", "end".into()));
to
}
_ => from,
};
// A closed path's span runs past its start point; `startOffset` can't, so wrap it.
let at = if at > 1.0 { at - 1.0 } else { at };
props.extend(css::transparency(n));
let a = self.attrs(&props);
let offset = fmt_num(start * 100.0, 3);
let offset = fmt_num(at * 100.0, 3);
let mut s = format!("<text{id} xml:space=\"preserve\"{a}><textPath xlink:href=\"#{pid}\" startOffset=\"{offset}%\">");
for run in &t.runs {
let mut diff = run_diff(&base, self.char_props(&run.style, space));
+34
View File
@@ -444,6 +444,40 @@ fn text_on_path_roundtrip() {
assert!(close_rect(doc_path, PathData::from_bezpath(&curve).bounds().unwrap(), 0.01), "{doc_path:?}");
}
/// Centred and right-aligned type on a path anchors where the layout aligns it: the middle or the
/// end of its span, wrapped on a closed path. It reads back drawn where it was.
#[test]
fn aligned_text_on_path_anchors_within_its_span() {
use vectorcraft_doc::Justify;
use vectorcraft_geom::Shape;
let mut curve = vectorcraft_geom::BezPath::new();
curve.move_to((20.0, 150.0));
curve.curve_to((60.0, 40.0), (140.0, 40.0), (180.0, 150.0));
let circle = vectorcraft_geom::kurbo::Circle::new((100.0, 100.0), 60.0).to_path(0.01);
let drawn = |t: &TextObject| t.xf.transform_rect_bbox(vectorcraft_text::layout(vectorcraft_text::FontDb::global(), t).bounds);
for (path, start, justify, offset) in [
(&curve, 0.25, Justify::Center, "62.5%"),
(&curve, 0.25, Justify::Right, "100%"),
(&curve, 0.25, Justify::Left, "25%"),
(&circle, 0.7, Justify::Center, "20%"),
] {
let mut d = Document::new(200.0, 200.0);
let mut t = TextObject::point(Point::ZERO, "Aligned", CharStyle { size: 14.0, ..CharStyle::default() });
t.kind = TextKind::OnPath { path: PathData::from_bezpath(path), start, end: None };
t.para.justify = justify;
let before = drawn(&t);
let id = d.alloc_id();
let d = doc_with(vec![Node::new(id, NodeKind::Text(Box::new(t)))]);
let svg = export(&d, &ExportOptions::default());
assert!(svg.contains(&format!("startOffset=\"{offset}\"")), "{justify:?} at {start}: {svg}");
if path == &curve {
let r = import(&svg).unwrap();
let NodeKind::Text(t) = &art(&r)[0].kind else { panic!() };
assert!(close_rect(drawn(t), before, 0.5), "{justify:?}: {:?} {before:?}", drawn(t));
}
}
}
/// An end bracket, Align to Path or Spacing, which `<textPath>` can't express, keep their look as
/// outlines (#429).
#[test]