From a1b8da43de4df007bfa54dcb8cf506588cb6a3b4 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sun, 1 Nov 2026 20:15:29 -0800 Subject: [PATCH] Effects: plain fills and strokes beside a fill's or stroke's own Transform count toward the effect budget Each fill or stroke without such a Transform effect is drawn on the object's geometry again, so its cost now counts toward the budget that limits the Transform effect's copies. The object's own geometry is no longer counted a second time next to the pieces that replace it. The Transform effect options entry in docs/target-app-parity.md links the current help pages. Part of #885 --- crates/effects/src/reshape.rs | 12 +++++++++--- crates/effects/src/tests/transformfx.rs | 22 ++++++++++++++++++++++ docs/target-app-parity.md | 2 +- 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/crates/effects/src/reshape.rs b/crates/effects/src/reshape.rs index fd5968a01..5f76e9607 100644 --- a/crates/effects/src/reshape.rs +++ b/crates/effects/src/reshape.rs @@ -636,6 +636,10 @@ fn transform_item_leaf(leaf: &mut Node, max: Budget, used: &mut Budget) { p.extras_mut().mask = None; p }; + // The pieces replace the leaf's own geometry (counted in `used`): each plain piece costs that + // geometry again, and each transformed fill or stroke what its paths cost. + let cost = Budget::of(&path); + *used = used.minus(cost); let mut pieces: Vec> = vec![]; let mut plain: Vec = vec![]; for item in &original.appearance.items { @@ -646,11 +650,12 @@ fn transform_item_leaf(leaf: &mut Node, max: Budget, used: &mut Budget) { }; if !plain.is_empty() { pieces.push(Arc::new(piece(std::mem::take(&mut plain)))); + *used = used.plus(cost); } let before = &fx[..at]; - // What the art's other paths leave of the budget, with the leaf's own geometry (counted in - // `used`) given back: the geometry before the Transform effect and its copies keep to it. - let room = max.minus(*used).plus(Budget::of(&path)); + // 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); let g = if crate::has_geometry(before) && !path.is_empty() { crate::geometry::apply_geometry_within(before, &path, bounds, &ctx.item(item), room) } else { @@ -673,6 +678,7 @@ fn transform_item_leaf(leaf: &mut Node, max: Budget, used: &mut Budget) { } if !plain.is_empty() { pieces.push(Arc::new(piece(plain))); + *used = used.plus(cost); } leaf.appearance.items.clear(); leaf.appearance.contents_index = None; diff --git a/crates/effects/src/tests/transformfx.rs b/crates/effects/src/tests/transformfx.rs index fa24809a9..adb75b2cb 100644 --- a/crates/effects/src/tests/transformfx.rs +++ b/crates/effects/src/tests/transformfx.rs @@ -314,3 +314,25 @@ fn strokes_with_their_own_transform_keep_to_one_effect_budget() { paths.iter().filter_map(|p| crate::reshape::leaf_geometry(p)).fold(Budget { anchors: 0, lines: 0 }, |c, (g, _)| c.plus(Budget::of(&g))); assert!(Budget::EFFECTS.fits(cost) && cost.lines > crate::MAX_EFFECT_LINES / 2, "{cost:?}"); } + +/// A path of 600,000 anchors with a fill below a stroke whose own Transform effect makes five +/// copies: the fill's path counts toward the effect budget, so the stroke's copies stop where the +/// whole art would pass it. +#[test] +fn a_fill_below_a_strokes_own_transform_counts_toward_the_effect_budget() { + 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(12), 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": 5, "strokes": true}))); + } + 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:?}"); +} diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 726b0b8fc..64a7f2767 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -476,7 +476,7 @@ since 2026-10-01. - **PDF editable import:** Place from bytes or a path with `link: false`, and `clipboard.importPdf` followed by Paste, preserve live type on the selected page. Place still honors `textAs: "outlines"`. Synthetic two-page regressions: `crates/engine/src/cmd/fileio/tests_pdffidelity.rs`. No change to the PDF reader's existing fallback rules. - **Selected Grid Repeat handles (#2691):** `tools/src/repeat_grid.rs` tests place the handles outside the grid at zoom 0.5, 1 and 4, preview rows, columns and spacing, leave no undo step for a drag that ends where it began, and give bounding-box handles priority; a headless canvas regression drags the row handle two tiles down in a zoomed view (3 → 5 rows, the source unchanged, one Undo back) and the column-spacing slider. Scores and hours unchanged. - **Selected Mirror Repeat handles:** a headless canvas regression creates the default mirror repeat and drags its Top bounding-box handle where the axis crosses it; the repeat grows vertically while its width stays fixed. Existing mirror-axis checks cover center movement and end-handle hit priority. The focused UI mirror tests pass. Scores and hours unchanged. -- **Transform effect options (#885):** Effect › Distort & Transform › Transform… has visible Transform Objects, Transform Patterns and Scale Strokes & Effects checkboxes (`distort.transform` params `objects`, `patterns`, `strokes`; files without them draw as before). With Transform Objects off the geometry stays and makes no copies; Scale Strokes & Effects scales stroke weights, dashes and the fills' and strokes' effect distances by the mean scale, and Transform Patterns moves pattern tiles, each copy `k` by the transformation `k` times. Such objects draw, export and expand through the same per-copy art, so the canvas and Expand Appearance agree (engine `tests_transformfx`, effects `tests_transformfx`, the dialog test). A Transform effect in a fill's or stroke's own effects applies the same options to that fill or stroke: its weight and dashes, the distances of its later effects and its pattern follow the transformation, and with copies each copy is an object of its own; the canvas, export, Expand Appearance and Use Preview Bounds use the same art (effects `tests_transformfx`). Not yet: object effects after a Transform effect keep their distances. Public reference: [Transform objects](https://helpx.adobe.com/illustrator/using/transforming-objects.html), [Summary of effects](https://helpx.adobe.com/illustrator/using/summary-of-effects.html). Scores and hours unchanged. +- **Transform effect options (#885):** Effect › Distort & Transform › Transform… has visible Transform Objects, Transform Patterns and Scale Strokes & Effects checkboxes (`distort.transform` params `objects`, `patterns`, `strokes`; files without them draw as before). With Transform Objects off the geometry stays and makes no copies; Scale Strokes & Effects scales stroke weights, dashes and the fills' and strokes' effect distances by the mean scale, and Transform Patterns moves pattern tiles, each copy `k` by the transformation `k` times. Such objects draw, export and expand through the same per-copy art, so the canvas and Expand Appearance agree (engine `tests_transformfx`, effects `tests_transformfx`, the dialog test). A Transform effect in a fill's or stroke's own effects applies the same options to that fill or stroke: its weight and dashes, the distances of its later effects and its pattern follow the transformation, and with copies each copy is an object of its own; the canvas, export, Expand Appearance and Use Preview Bounds use the same art (effects `tests_transformfx`). Not yet: object effects after a Transform effect keep their distances. Public reference: [Scale objects](https://helpx.adobe.com/illustrator/desktop/manage-objects/reshape-transform-objects/scale-objects.html), [How to apply effects](https://helpx.adobe.com/illustrator/using/effects.html), [Distort and transform effects](https://helpx.adobe.com/illustrator/desktop/manage-objects/reshape-transform-objects/distort-and-transform-effects-in-illustrator.html). Scores and hours unchanged. - **Warp Options (#1760):** Effect > Warp and existing Appearance effect links share the fifteen existing styles, original schematic previews, orientation choices and percent Bend/Distortion controls. Focused tests passed for real numeric entry and slider input, Preview/Cancel, immediate Enter/OK, mixed-style edits, target/slot/visibility preservation and Undo/Redo. Each applied style stays unchanged until an explicit Style choice. Original style-menu and live-preview captures were reviewed; their numeric values were supplied programmatically. Public references: [Reshaping with effects](https://helpx.adobe.com/illustrator/using/reshaping-objects-effects.html) and [Using effects](https://helpx.adobe.com/illustrator/using/effects.html). Warp geometry, optional Properties integration and remembered defaults are unchanged; no broader parity claim. - **Constrain Angle (#394):** General › Constrain Angle turns the arrow keys' axes (guides stay on the page's), holds Shift drags of the Selection, Direct Selection, Free Transform, Slice Selection, Reshape and Measure tools and the Pen's handles to the turned axes, and new rectangles, rounded rectangles, ellipses, grids, point and area type, Shift-drawn polygons and stars and the shape size dialogs are drawn along it with their bounding box turned; 0° and whole turns are exactly as before, quarter turns exact (). Tests: `tests_constrain_tools`, `tests_constrain_new` (engine), `geom` `constrain_axes` / `rotate_deg`, the shape, grid and type tool tests.