diff --git a/crates/trace/src/centerline/mod.rs b/crates/trace/src/centerline/mod.rs
index 7999f655f..332c4499c 100644
--- a/crates/trace/src/centerline/mod.rs
+++ b/crates/trace/src/centerline/mod.rs
@@ -65,6 +65,9 @@ pub(crate) fn lines(
let mask = |i: usize| labels.get(i) == Some(&label);
let mut seen = vec![false; w * h];
let mut stack = vec![];
+ // Thin shapes nested in one another have overlapping boxes: the cells of all of them together
+ // are bounded, so the work is too (a component past it stays as it is, not a line).
+ let mut cells_left = n.saturating_mul(8).max(1 << 22);
for start in 0..w * h {
if !mask(start) || seen.get(start).copied().unwrap_or(false) {
continue;
@@ -86,6 +89,8 @@ pub(crate) fn lines(
}
}
let Some(g) = Grid::from_component(&comp, w) else { continue };
+ let Some(left) = cells_left.checked_sub(g.w.saturating_mul(g.h)) else { break };
+ cells_left = left;
let Some(dt) = distance::squared(&g.on, g.w, g.h) else { continue };
let widest = 2.0 * f64::from(dt.iter().copied().fold(0.0f32, f32::max)).sqrt() - 1.0;
if widest > max_width {
diff --git a/crates/trace/src/group.rs b/crates/trace/src/group.rs
index d5b615de6..50b165f1a 100644
--- a/crates/trace/src/group.rs
+++ b/crates/trace/src/group.rs
@@ -73,6 +73,9 @@ fn inside_or_near(p: Point, o: &Outer) -> bool {
inside
}
+/// The most paths grouped.
+const MAX_GROUPED_PATHS: usize = 20_000;
+
/// For each of `paths` (bottom-most first), the path it lies inside, if any: the smallest filled
/// path whose outer contour holds all of its own. The path that covers the whole `w` × `h` image
/// (the paper) is nobody's parent: everything would be inside it.
@@ -81,6 +84,10 @@ fn inside_or_near(p: Point, o: &Outer) -> bool {
/// `paths` paints in its own order: where the paths can overlap (`stacked`), a path only gets a parent below it, and only
/// if no path that would then come after it instead of before it shares any of its box.
pub(crate) fn parents(paths: &[TracedPath], w: usize, h: usize, stacked: bool) -> Vec