From eb92a1e53a33a2ed6105dee780db64d1634bd29e Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sun, 1 Nov 2026 20:29:41 -0800 Subject: [PATCH] Effects: a fill's or stroke's own Transform copies leave room in the effect budget for the pieces painted after them transform_item_leaf now holds back the leaf's geometry for each plain or transformed piece that follows a transformed fill or stroke, so the copies of an earlier item no longer use up the budget that a later fill or stroke still needs. The nested pass over a transformed piece keeps to the same reduced limit. --- crates/effects/src/reshape.rs | 16 +++++++++++----- crates/effects/src/tests/transformfx.rs | 23 +++++++++++++++++++++++ 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/crates/effects/src/reshape.rs b/crates/effects/src/reshape.rs index 5f76e9607..920f3727a 100644 --- a/crates/effects/src/reshape.rs +++ b/crates/effects/src/reshape.rs @@ -642,7 +642,11 @@ fn transform_item_leaf(leaf: &mut Node, max: Budget, used: &mut Budget) { *used = used.minus(cost); let mut pieces: Vec> = vec![]; let mut plain: Vec = vec![]; - for item in &original.appearance.items { + // Which items become a transformed piece of their own; every other run of items is one plain piece. + let transformed: Vec = original.appearance.items.iter().map(|i| crate::paints(i) && i.effects().iter().any(reaches_appearance)).collect(); + // The number of pieces after item `i` in paint order: each costs at least the leaf's geometry. + let later = |i: usize| (i + 1..transformed.len()).filter(|&j| transformed[j] || j == i + 1 || transformed[j - 1]).count(); + for (i, item) in original.appearance.items.iter().enumerate() { let fx = item.effects(); let Some(at) = fx.iter().position(reaches_appearance).filter(|_| crate::paints(item)) else { plain.push(item.clone()); @@ -653,9 +657,11 @@ fn transform_item_leaf(leaf: &mut Node, max: Budget, used: &mut Budget) { *used = used.plus(cost); } let before = &fx[..at]; - // What the art's other paths leave of the budget: the geometry before the Transform effect - // and its copies keep to it. - let room = max.minus(*used); + // What the art's other paths leave of the budget, less the leaf's geometry for each piece + // still to come: the geometry before the Transform effect and its copies keep to it. + let reserved = (0..later(i)).fold(Budget { anchors: 0, lines: 0 }, |r, _| r.plus(cost)); + let limit = max.minus(reserved); + let room = limit.minus(*used); let g = if crate::has_geometry(before) && !path.is_empty() { crate::geometry::apply_geometry_within(before, &path, bounds, &ctx.item(item), room) } else { @@ -673,7 +679,7 @@ fn transform_item_leaf(leaf: &mut Node, max: Budget, used: &mut Budget) { set_geometry(&mut p, g.clone(), rule); let made = transform_leaf(&mut p, &g, map, copies, options, room); *used = used.plus(made); - for_each_leaf(&mut p, &mut |l| transform_item_leaf(l, max, used)); + for_each_leaf(&mut p, &mut |l| transform_item_leaf(l, limit, used)); pieces.push(Arc::new(p)); } if !plain.is_empty() { diff --git a/crates/effects/src/tests/transformfx.rs b/crates/effects/src/tests/transformfx.rs index adb75b2cb..4c67fbd50 100644 --- a/crates/effects/src/tests/transformfx.rs +++ b/crates/effects/src/tests/transformfx.rs @@ -336,3 +336,26 @@ fn a_fill_below_a_strokes_own_transform_counts_toward_the_effect_budget() { assert_eq!(stroked_leaves(&art).len(), 2, "{cost:?}"); assert!(Budget::EFFECTS.fits(cost), "{cost:?}"); } + +/// A path of 600,000 anchors with a stroke whose own Transform effect makes two copies, below a +/// fill: the stroke's copies leave room for the fill painted after them. +#[test] +fn a_strokes_own_transform_leaves_room_for_a_fill_above_it() { + use crate::geometry::Budget; + use vectorcraft_geom::{PathData, Point, SubPath}; + let points: Vec = (0..600_000).map(|i| Point::new(f64::from(i) * 1.0e-4, f64::from(i % 2))).collect(); + let gray = Paint::solid(Color::rgb(0.5, 0.5, 0.5)); + let mut n = Node::path(NodeId(13), PathData::single(SubPath::polyline(&points, false)), Appearance::basic(gray, Paint::solid(Color::BLACK), 1.0)); + if let Some(s) = n.appearance.stroke_mut() { + s.effects.push(transform(json!({"moveH": 100, "copies": 2, "strokes": true}))); + } + n.appearance.items.reverse(); + assert!(matches!(n.appearance.items.first(), Some(AppearanceItem::Stroke(_)))); + let art = reshape(None, &n, None).unwrap(); + let cost = leaves(&art) + .iter() + .filter_map(|p| crate::reshape::leaf_geometry(p)) + .fold(Budget { anchors: 0, lines: 0 }, |c, (g, _)| c.plus(Budget::of(&g))); + assert_eq!(stroked_leaves(&art).len(), 2, "{cost:?}"); + assert!(Budget::EFFECTS.fits(cost), "{cost:?}"); +}