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.
This commit is contained in:
dotCooCoo
2026-11-02 15:22:00 +07:00
committed by bflatastic
parent a1b8da43de
commit eb92a1e53a
2 changed files with 34 additions and 5 deletions
+11 -5
View File
@@ -642,7 +642,11 @@ fn transform_item_leaf(leaf: &mut Node, max: Budget, used: &mut Budget) {
*used = used.minus(cost);
let mut pieces: Vec<Arc<Node>> = vec![];
let mut plain: Vec<AppearanceItem> = 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<bool> = 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() {
+23
View File
@@ -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<Point> = (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:?}");
}