mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-10 09:59:47 -06:00
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:
@@ -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<Option<Rect>>) {
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user