Effects: later effects on a fill's or stroke's own Transform apply to all its copies as one piece

The geometry effects after a fill's or stroke's own Transform effect with
Scale Strokes & Effects or Transform Patterns now take the bounds of the
transformed path and all its copies as their reference box, as an
object's own effects do. A later Rotate turns the copies about their
combined center instead of turning each copy about its own, and a later
Offset Path or plain Transform applies to the paths themselves. Each copy
keeps its own scaled distances.

Part of #885
This commit is contained in:
dotCooCoo
2026-11-02 15:22:00 +07:00
committed by bflatastic
parent eb92a1e53a
commit ae23859002
2 changed files with 86 additions and 9 deletions
+57 -5
View File
@@ -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, &params, seed);
let options = crate::distort::TransformOptions::of(&params);
let map = crate::distort::transform_affine(bounds, &params, 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<Option<Rect>>) {
+29 -4
View File
@@ -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<Rect> = 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]