diff --git a/crates/effects/src/reshape.rs b/crates/effects/src/reshape.rs index 920f3727a..1ebfe8fc2 100644 --- a/crates/effects/src/reshape.rs +++ b/crates/effects/src/reshape.rs @@ -610,10 +610,11 @@ fn transform_items(art: &mut Node) { /// as [`transform_leaf`] transforms an object with that effect's map, copies and options, so the /// stroke's weight and dashes, the distances of its remaining effects and its pattern follow the /// map. The reference box is that geometry's bounds and the seed is the leaf's id, as the renderer -/// draws a fill's or stroke's own effects. Its raster effects and the effects after the Transform -/// effect stay on it (and on each copy), and a later such Transform effect applies to the result in -/// the same way. The other fills and strokes stay together on paths of the leaf's geometry between -/// them, with their own effects. `max` is the budget of the whole art and `used` what it has spent. +/// draws a fill's or stroke's own effects. The geometry effects after the Transform effect then +/// apply to the transformed path and its copies as one piece ([`transform_piece`]), and its other +/// effects stay on each of them. The other fills and strokes stay together on paths of the leaf's +/// geometry between them, with their own effects. `max` is the budget of the whole art and `used` +/// what it has spent. fn transform_item_leaf(leaf: &mut Node, max: Budget, used: &mut Budget) { if matches!(leaf.kind, NodeKind::Path { clipping: true, .. }) || !leaf.appearance.items.iter().any(item_transform_options) { return; @@ -679,7 +680,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, limit, used)); + transform_piece(&mut p, ctx.seed, limit, used); pieces.push(Arc::new(p)); } if !plain.is_empty() { @@ -692,6 +693,57 @@ fn transform_item_leaf(leaf: &mut Node, max: Budget, used: &mut Budget) { leaf.kind = NodeKind::Group { children: pieces, clip: false }; } +/// Apply the visible geometry effects left on the one fill or stroke of each path under `p` (a +/// transformed piece of [`transform_item_leaf`] and its copies) to all of `p`'s paths as one piece: +/// each effect takes the bounds of all the paths as its reference box and `seed`, as an object's +/// own effects reshape all its paths ([`reshape_leaves_within`]). Each path applies its own copy of +/// the effect, whose distances its Scale Strokes & Effects scaled. A Transform effect with +/// Transform Patterns or Scale Strokes & Effects on transforms each path as [`transform_leaf`] does. +/// The geometry effects then leave the fills and strokes; their other effects stay. The paths keep +/// to `max` together with the rest of the art (`used`). +fn transform_piece(p: &mut Node, seed: u64, max: Budget, used: &mut Budget) { + let mut count = 0; + for_each_leaf(p, &mut |l| { + if let Some(item) = l.appearance.items.first() { + count = count.max(item.effects().len()); + } + }); + for j in 0..count { + let Some(bounds) = p.geometric_bounds() else { break }; + for_each_leaf(p, &mut |leaf| { + let Some(item) = leaf.appearance.items.first().cloned() else { return }; + let Some(e) = item.effects().get(j).filter(|e| e.visible && is_geometry(&e.id)).cloned() else { return }; + let Some((path, rule)) = leaf_geometry(leaf) else { return }; + // What the other paths leave of the budget. + let before = Budget::of(&path); + let room = max.minus(used.minus(before)); + if reaches_appearance(&e) { + let params = merged_params(&e.id, &e.params); + let (_, copies) = crate::distort::transform_map(bounds, ¶ms, seed); + let options = crate::distort::TransformOptions::of(¶ms); + let map = crate::distort::transform_affine(bounds, ¶ms, seed); + let made = transform_leaf(leaf, &path, map, copies, options, room); + *used = used.minus(before).plus(made); + return; + } + let appearance = leaf.appearance.clone(); + let stroke = match &item { + AppearanceItem::Stroke(s) => Some(s), + AppearanceItem::Fill(_) => None, + }; + let ctx = GeomContext { stroke, rule, seed, appearance: Some(&appearance), item: Some(0) }; + let out = crate::geometry::apply_geometry_within(std::slice::from_ref(&e), &path, bounds, &ctx, room); + *used = used.minus(before).plus(Budget::of(&out)); + set_geometry(leaf, out, rule); + }); + } + for_each_leaf(p, &mut |leaf| { + for item in &mut leaf.appearance.items { + item.effects_mut().retain(|e| !(e.visible && is_geometry(&e.id))); + } + }); +} + /// The bounds of each group of type's outlines under `n` ([`outline_text`]), `n` included, in /// order. A group inside one of them is part of it. fn type_outline_bounds(n: &Node, out: &mut Vec>) { diff --git a/crates/effects/src/tests/transformfx.rs b/crates/effects/src/tests/transformfx.rs index 4c67fbd50..bdeb9d8cf 100644 --- a/crates/effects/src/tests/transformfx.rs +++ b/crates/effects/src/tests/transformfx.rs @@ -231,10 +231,35 @@ fn a_fills_own_transform_with_scale_strokes_and_effects_scales_its_later_effects let art = reshape(None, &n, None).unwrap(); let fill = leaves(&art).into_iter().find(|p| p.appearance.stroke().is_none()).unwrap(); let AppearanceItem::Fill(f) = &fill.appearance.items[0] else { panic!("not a fill") }; - let ids: Vec<&str> = f.effects.iter().map(|e| e.id.as_str()).collect(); - assert_eq!(ids, vec!["path.offsetPath"]); - assert!(close(f.effects[0].params["offset"].as_f64().unwrap(), 15.0)); - assert!(close(fill.geometric_bounds().unwrap().width(), 300.0)); + // The Offset Path, scaled from 5 to 15, applies to the transformed fill's path. + assert!(f.effects.is_empty(), "{:?}", f.effects); + let w = fill.geometric_bounds().unwrap().width(); + assert!(close(w, 330.0), "{w}"); +} + +#[test] +fn a_later_transform_on_a_stroke_turns_its_copies_about_their_combined_center() { + for later in [json!({"rotate": 90}), json!({"rotate": 90, "strokes": true})] { + let mut n = stroke_with_effect(json!({"moveH": 200, "copies": 1, "strokes": true})); + if let Some(s) = n.appearance.stroke_mut() { + s.effects.push(transform(later.clone())); + } + let art = reshape(None, &n, None).unwrap(); + let paths = stroked_leaves(&art); + assert_eq!(paths.len(), 2, "{later}"); + // The copies span 0 to 300 by 0 to 100 before the turn; about the center (150, 50) both + // go to x 100 to 200, one above the other. + let boxes: Vec = paths.iter().map(|p| p.geometric_bounds().unwrap()).collect(); + for b in &boxes { + assert!(close(b.x0, 100.0) && close(b.x1, 200.0), "{later}: {boxes:?}"); + } + let top = boxes.iter().map(|b| b.y0).fold(f64::INFINITY, f64::min); + let bottom = boxes.iter().map(|b| b.y1).fold(f64::NEG_INFINITY, f64::max); + assert!(close(top, -100.0) && close(bottom, 200.0), "{later}: {boxes:?}"); + for p in &paths { + assert!(p.appearance.stroke().unwrap().effects.is_empty(), "{later}"); + } + } } #[test]