diff --git a/contributors/people.toml b/contributors/people.toml index 6d8e37573..69c412f8b 100644 --- a/contributors/people.toml +++ b/contributors/people.toml @@ -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 diff --git a/crates/svg/src/export/text.rs b/crates/svg/src/export/text.rs index 01510cd0d..44af6c1ee 100644 --- a/crates/svg/src/export/text.rs +++ b/crates/svg/src/export/text.rs @@ -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!(""); for run in &t.runs { let mut diff = run_diff(&base, self.char_props(&run.style, space)); diff --git a/crates/svg/tests/it/roundtrip.rs b/crates/svg/tests/it/roundtrip.rs index fde9ff86f..25d0169c6 100644 --- a/crates/svg/tests/it/roundtrip.rs +++ b/crates/svg/tests/it/roundtrip.rs @@ -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 `` can't express, keep their look as /// outlines (#429). #[test]