diff --git a/crates/engine/src/cmd/flatten/apply.rs b/crates/engine/src/cmd/flatten/apply.rs
index cccd82ecc..e4113b47b 100644
--- a/crates/engine/src/cmd/flatten/apply.rs
+++ b/crates/engine/src/cmd/flatten/apply.rs
@@ -26,6 +26,40 @@ pub(super) struct Flat {
/// Where its image goes, and the image (once rendered).
pub(super) area: Option,
raster: Option,
+ /// When the group is one block of type over several lines: its lines, to group the regions by.
+ lines: Option,
+}
+
+/// The lines of a block of type: document space → its line space, where each line spans a band
+/// across the lines (its ascent above the baseline to its descent below).
+struct Lines {
+ to_lines: Affine,
+ bands: Vec<(f64, f64)>,
+}
+
+impl Lines {
+ /// The lines of `n` when it is type laid out in two lines or more (not on a path).
+ fn of(n: &Node) -> Option {
+ let NodeKind::Text(t) = &n.kind else { return None };
+ if t.xf.determinant().abs() < 1e-12 {
+ return None;
+ }
+ let layout = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), t);
+ if layout.on_path {
+ return None;
+ }
+ let bands: Vec<(f64, f64)> = layout.lines.iter().filter(|l| l.x1 > l.x0).map(|l| (l.baseline - l.ascent, l.baseline + l.descent)).collect();
+ let to_lines = if layout.vertical { layout.line_xf.inverse() * t.xf.inverse() } else { t.xf.inverse() };
+ (bands.len() > 1).then_some(Lines { to_lines, bands })
+ }
+
+ /// The line nearest to the middle of `path`.
+ fn line_of(&self, path: &PathData) -> usize {
+ let Some(b) = path.bounds() else { return 0 };
+ let y = (self.to_lines * b.center()).y;
+ let away = |&(lo, hi): &(f64, f64)| if y < lo { lo - y } else { (y - hi).max(0.0) };
+ self.bands.iter().enumerate().min_by(|a, b| away(a.1).total_cmp(&away(b.1))).map_or(0, |(i, _)| i)
+ }
}
/// Where a group's image goes.
@@ -97,7 +131,11 @@ pub(super) fn plan(src: &Document, roots: &[NodeId], o: &FlattenOptions, render:
}
None => None,
};
- out.flat.push(Flat { roots: ids, regions, area, raster });
+ let lines = match g.as_slice() {
+ [i] => Lines::of(&art[*i]),
+ _ => None,
+ };
+ out.flat.push(Flat { roots: ids, regions, area, raster, lines });
}
Ok(out)
}
@@ -366,7 +404,10 @@ pub(super) fn apply(d: &mut Document, sel: &mut vectorcraft_doc::Selection, plan
children.push(Arc::new(raster_node(d, r)));
rasterized += 1;
}
+ // Type over several lines gives a group per line.
+ let mut by_line: Vec>> = f.lines.as_ref().map_or(vec![], |l| vec![vec![]; l.bands.len()]);
for (path, fill) in f.regions {
+ let line = f.lines.as_ref().map(|l| l.line_of(&path));
let mut n = shape_node(d, path, None);
n.appearance = Appearance {
items: vec![AppearanceItem::Fill(FillLayer { overprint: fill.overprint, ..FillLayer::new(fill.paint) })],
@@ -374,9 +415,20 @@ pub(super) fn apply(d: &mut Document, sel: &mut vectorcraft_doc::Selection, plan
}
.into();
n.opacity = fill.alpha.min(1.0);
- children.push(Arc::new(n));
+ match line.and_then(|i| by_line.get_mut(i)) {
+ Some(l) => l.push(Arc::new(n)),
+ None => children.push(Arc::new(n)),
+ }
vector += 1;
}
+ by_line.retain(|l| !l.is_empty());
+ if by_line.len() > 1 {
+ for l in by_line {
+ children.push(Arc::new(Node::group(d.alloc_id(), l)));
+ }
+ } else {
+ children.extend(by_line.into_iter().flatten());
+ }
let g = Node::group(d.alloc_id(), children);
ids.push(g.id);
if one_pass {
diff --git a/crates/engine/src/tests/flatten.rs b/crates/engine/src/tests/flatten.rs
index e2b9b4f6a..d4671c11e 100644
--- a/crates/engine/src/tests/flatten.rs
+++ b/crates/engine/src/tests/flatten.rs
@@ -381,6 +381,33 @@ fn outlined_type_keeps_its_underline() {
assert!(b.y1 > 151.0, "{b:?}");
}
+/// Flattening transparent type that runs over several lines gives a group per line, each holding
+/// that line's regions (#5252).
+#[test]
+fn flattening_multiline_transparent_text_gives_one_group_per_line() {
+ let _settings = settings_lock();
+ let mut s = session();
+ let text =
+ run(&mut s, "text.create", json!({"x": 10, "y": 40, "text": "First line\nSecond line\nThird line", "size": 24}))["id"].as_u64().unwrap();
+ run(&mut s, "transparency.set", json!({"ids": [text], "opacity": 50}));
+ let NodeKind::Text(t) = &node(&s, text).kind else { panic!("type") };
+ let layout = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), t);
+ let baselines: Vec = layout.baselines().iter().map(|(a, _)| (t.xf * *a).y).collect();
+ assert_eq!(baselines.len(), 3, "{baselines:?}");
+ let before = picture(&s);
+ let (ids, vector, _) = flatten(&mut s, json!({"ids": [text]}));
+ assert_eq!(ids.len(), 1);
+ assert!(vector > 0);
+ let g = node(&s, ids[0]);
+ let lines: Vec<&std::sync::Arc> = g.children().unwrap().iter().filter(|c| matches!(c.kind, NodeKind::Group { .. })).collect();
+ assert_eq!(lines.len(), 3, "expected one group per line, got {} children", g.children().unwrap().len());
+ for (line, y) in lines.iter().zip(&baselines) {
+ let b = line.geometric_bounds().unwrap();
+ assert!(b.y0 > y - 24.0 && b.y1 < y + 10.0, "line at baseline {y} has bounds {b:?}");
+ assert_regions_match(&s, &before, line.id.0, 2, "line");
+ }
+}
+
#[test]
fn outlined_type_keeps_its_gradient_on_the_outlines_its_transform_effect_moves() {
let _settings = settings_lock();