mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-13 08:29:14 +00:00
Effects: a fill's or stroke's own Transform effect leaves the effects listed after it unscaled
Effects apply in the order they are listed, so an effect listed after a Transform effect applies to the art the Transform effect made, at its own values, as an object's later effects do after the object's Transform effect. With Scale Strokes & Effects on, a fill's or stroke's own Transform effect scaled the effects listed after it in that fill's or stroke's list by the mean scale: a 300% Transform turned a 5 pt Offset Path after it into a 15 pt one, and a Drop Shadow after it took three times its offset. With Transform Patterns or Scale Strokes & Effects on, an SVG filter after it also had the origin of its user space moved. - The effects after such a Transform effect now keep their values, on the transformed art and on each copy, on every fill and stroke a live-effect plug-in paints the piece with, and when the copies are counted against the effect budget. - With Transform Objects off, the effects before it keep their values too. - The Transform params text, docs/target-app-parity.md and docs/gaps.md describe the rule, and drop the note that listed an object's later effects keeping their distances as a shortfall.
This commit is contained in:
@@ -267,7 +267,7 @@ pub fn effect_catalog() -> Vec<EffectInfo> {
|
||||
"distort.transform",
|
||||
"Transform…",
|
||||
DT,
|
||||
"{scaleH: % (100), scaleV: % (100), moveH: pt (0; right = +), moveV: pt (0; down = +), rotate: deg (0; counter-clockwise; when the effect turns, reflects or scales the object unevenly, a gradient fitted to the object is fixed to its fit and turns with the object and with each copy), copies: 0..1000 (0; each copy transforms the last again and is painted below it, the object on top; a group's copies each hold all its members, and a group with a member that carries a geometry, Pathfinder or 3D effect copies each member in its place; placed gradients transform with the object and with each copy), reflectX: bool (false; flips left to right), reflectY: bool (false; flips top to bottom), reference: 0..8 (4; the point of the bounds' 9-point grid it scales, rotates and reflects about: 0 top left, 4 centre, 8 bottom right), random: bool (false; each scale goes a random share of the way from 100 % to its value, each move and the angle a random share of theirs, differently for each object and the same on every redraw), objects: bool (true; Transform Objects: off, the geometry stays where it is and makes no copies, and only the two options below apply, once; on, without copies or random, the raster effects listed before this effect (Rasterize and SVG filters excepted) transform with the art whatever strokes says: their sizes scale by the mean scale and a Drop Shadow's offset turns, reflects and scales, while the angles and light directions of pixel filters stay as they are), patterns: bool (false; Transform Patterns: pattern fills and strokes transform along; with copies the original keeps its pattern and copy k takes the transformation k times), strokes: bool (false; Scale Strokes & Effects: stroke weights and dashes, and the distances of the effects on the fills and strokes, scale by the mean scale √|scaleH·scaleV|, copy k by its k-th power)}",
|
||||
"{scaleH: % (100), scaleV: % (100), moveH: pt (0; right = +), moveV: pt (0; down = +), rotate: deg (0; counter-clockwise; when the effect turns, reflects or scales the object unevenly, a gradient fitted to the object is fixed to its fit and turns with the object and with each copy), copies: 0..1000 (0; each copy transforms the last again and is painted below it, the object on top; a group's copies each hold all its members, and a group with a member that carries a geometry, Pathfinder or 3D effect copies each member in its place; placed gradients transform with the object and with each copy), reflectX: bool (false; flips left to right), reflectY: bool (false; flips top to bottom), reference: 0..8 (4; the point of the bounds' 9-point grid it scales, rotates and reflects about: 0 top left, 4 centre, 8 bottom right), random: bool (false; each scale goes a random share of the way from 100 % to its value, each move and the angle a random share of theirs, differently for each object and the same on every redraw), objects: bool (true; Transform Objects: off, the geometry stays where it is and makes no copies, and only the two options below apply, once; on, without copies or random, the raster effects listed before this effect (Rasterize and SVG filters excepted) transform with the art whatever strokes says: their sizes scale by the mean scale and a Drop Shadow's offset turns, reflects and scales, while the angles and light directions of pixel filters stay as they are), patterns: bool (false; Transform Patterns: pattern fills and strokes transform along; with copies the original keeps its pattern and copy k takes the transformation k times), strokes: bool (false; Scale Strokes & Effects: stroke weights and dashes, and the distances of the effects on the fills and strokes, scale by the mean scale √|scaleH·scaleV|, copy k by its k-th power; on a fill or stroke, the effects listed after this effect in that fill's or stroke's own list keep their distances and apply to the transformed art, and with objects off so do the effects before it)}",
|
||||
json!({"scaleH": 100.0, "scaleV": 100.0, "moveH": 0.0, "moveV": 0.0, "rotate": 0.0, "copies": 0, "reflectX": false, "reflectY": false, "reference": 4, "random": false, "objects": true, "patterns": false, "strokes": false}),
|
||||
),
|
||||
g(
|
||||
|
||||
@@ -944,7 +944,7 @@ impl Step<'_> {
|
||||
// The copies of a path replace it by a group of them, the path included.
|
||||
let copied = |leaf: &Node| leaf.children().map_or(0, Vec::len);
|
||||
if options.reach_appearance() {
|
||||
let (made, moved) = transform_leaf(leaf, &path, map, copies, options, room);
|
||||
let (made, moved) = transform_leaf(leaf, &path, map, copies, options, room, None);
|
||||
*used = used.minus(before).plus(made);
|
||||
if moved {
|
||||
// With Transform Objects off the path stays, and its mask takes the
|
||||
@@ -960,7 +960,7 @@ impl Step<'_> {
|
||||
}
|
||||
if separate {
|
||||
let scaling = vectorcraft_doc::Scaling { keep_fit_when_flat: true, ..Default::default() };
|
||||
let made = copy_leaf(leaf, &path, m, copies, scaling, room);
|
||||
let made = copy_leaf(leaf, &path, m, copies, scaling, room, None);
|
||||
*used = used.minus(before).plus(made);
|
||||
moved_ids.insert(leaf.id);
|
||||
short |= counts(leaf.id) && copied(leaf) <= copies;
|
||||
@@ -1245,8 +1245,9 @@ fn copy_scaling(options: crate::distort::TransformOptions) -> vectorcraft_doc::S
|
||||
/// and the distances of the effects on its fills and strokes scale by `m`'s mean scale, and its
|
||||
/// pattern paints transform by `m`. With copies, the original keeps them and copy `k` takes them
|
||||
/// `k` times, each copy an object of its own ([`copy_leaf`], within `budget`). With Transform
|
||||
/// Objects off the geometry stays and the options apply once. Returns what the leaf costs after,
|
||||
/// and `false` when the path kept its shape because the moved path would pass `budget`.
|
||||
/// Objects off the geometry stays and the options apply once. The effects `kept` names keep their
|
||||
/// values on the leaf and on each copy ([`keep_listed`]). Returns what the leaf costs after, and
|
||||
/// `false` when the path kept its shape because the moved path would pass `budget`.
|
||||
fn transform_leaf(
|
||||
leaf: &mut Node,
|
||||
path: &PathData,
|
||||
@@ -1254,6 +1255,7 @@ fn transform_leaf(
|
||||
copies: usize,
|
||||
options: crate::distort::TransformOptions,
|
||||
budget: Budget,
|
||||
kept: Kept,
|
||||
) -> (Budget, bool) {
|
||||
let scaling = copy_scaling(options);
|
||||
if !options.objects {
|
||||
@@ -1266,6 +1268,7 @@ fn transform_leaf(
|
||||
if options.patterns {
|
||||
vectorcraft_doc::pattern::transform_pattern_paints(leaf, m);
|
||||
}
|
||||
keep_listed(leaf, kept);
|
||||
return (Budget::of(path), true);
|
||||
}
|
||||
if copies == 0 {
|
||||
@@ -1275,9 +1278,10 @@ fn transform_leaf(
|
||||
return (Budget::of(path), false);
|
||||
}
|
||||
leaf.transform(m, scaling);
|
||||
keep_listed(leaf, kept);
|
||||
return (Budget::of(&moved), true);
|
||||
}
|
||||
(copy_leaf(leaf, path, m, copies, scaling, budget), true)
|
||||
(copy_leaf(leaf, path, m, copies, scaling, budget, kept), true)
|
||||
}
|
||||
|
||||
/// Does a Transform effect's copies of `art` copy it whole ([`copy_art`])? They do when `art` is a
|
||||
@@ -1423,9 +1427,10 @@ fn copy_art(art: &mut Node, m: Affine, copies: usize, scaling: vectorcraft_doc::
|
||||
/// moves with it) rather than spanning every copy, and its strokes and patterns can differ from the
|
||||
/// others'. The copies stop where [`crate::distort::transform_within`]'s would, before they pass
|
||||
/// `budget` with what each copy's own geometry effects make ([`effects_cost`]), and before one's
|
||||
/// strokes would scale past [`MAX_STROKE_SCALE`]. Returns what the copies cost, the original
|
||||
/// included.
|
||||
fn copy_leaf(leaf: &mut Node, path: &PathData, m: Affine, copies: usize, scaling: vectorcraft_doc::Scaling, budget: Budget) -> Budget {
|
||||
/// strokes would scale past [`MAX_STROKE_SCALE`]. The effects `kept` names keep their values on
|
||||
/// each copy, before its cost is counted ([`keep_listed`]). Returns what the copies cost, the
|
||||
/// original included.
|
||||
fn copy_leaf(leaf: &mut Node, path: &PathData, m: Affine, copies: usize, scaling: vectorcraft_doc::Scaling, budget: Budget, kept: Kept) -> Budget {
|
||||
let mut made = Budget::of(path).plus(effects_cost(leaf, path));
|
||||
let mut placements: Vec<Arc<Node>> = vec![Arc::new(leaf.clone())];
|
||||
let mut xf = Affine::IDENTITY;
|
||||
@@ -1438,6 +1443,7 @@ fn copy_leaf(leaf: &mut Node, path: &PathData, m: Affine, copies: usize, scaling
|
||||
}
|
||||
let mut copy = leaf.clone();
|
||||
copy.transform(xf, scaling);
|
||||
keep_listed(&mut copy, kept);
|
||||
// Each copy's own geometry effects make a different amount, as the copy and its effects scale.
|
||||
let mut cost = Budget::of(&moved).plus(effects_cost(©, &moved));
|
||||
// Each copy carries the art of its linked masks, moved with it.
|
||||
@@ -1584,9 +1590,12 @@ fn item_makes_piece(item: &AppearanceItem) -> bool {
|
||||
/// effects. In paint order, each such fill or stroke is a path of its own: the leaf's geometry after
|
||||
/// the geometry effects of that fill or stroke before its first such Transform effect, transformed
|
||||
/// 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. The geometry effects after the Transform effect then
|
||||
/// stroke's weight and dashes, the distances of the effects before the Transform effect and its
|
||||
/// pattern follow the map, and the raster effects before it that follow it take their followed
|
||||
/// values ([`follow_piece`]). The effects after it keep their values and apply to what it made
|
||||
/// ([`keep_listed`]), and with Transform Objects off so do the effects before it. 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. 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`
|
||||
@@ -1699,13 +1708,15 @@ fn transform_item_leaf(leaf: &mut Node, max: Budget, used: &mut Budget, doc: Opt
|
||||
let map = crate::distort::transform_affine(tb, ¶ms, ctx.seed);
|
||||
let mut it = item.clone();
|
||||
let rest: Vec<Effect> = kept_before.iter().chain(&fx[at + 1..]).cloned().collect();
|
||||
*it.effects_mut() = rest;
|
||||
*it.effects_mut() = rest.clone();
|
||||
let mut p = piece(vec![it]);
|
||||
if carry.moves(&p.appearance) {
|
||||
carry.apply(&mut p.appearance);
|
||||
}
|
||||
set_geometry(&mut p, g.clone(), rule);
|
||||
let (made, moved) = transform_leaf(&mut p, &g, map, copies, options, room);
|
||||
let after = kept_before.len();
|
||||
let keep = |k: usize| k >= after || !options.objects;
|
||||
let (made, moved) = transform_leaf(&mut p, &g, map, copies, options, room, Some((&rest, &keep)));
|
||||
*used = used.plus(made);
|
||||
let from = crate::raster::held_back(before);
|
||||
if moved && !held_later {
|
||||
@@ -1728,8 +1739,10 @@ fn transform_item_leaf(leaf: &mut Node, max: Budget, used: &mut Budget, doc: Opt
|
||||
/// 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_carrying`]). 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 effect, at the effect's own distances. A Transform effect with Transform Patterns or Scale
|
||||
/// Strokes & Effects on transforms each path as [`transform_leaf`] does, and leaves the values of
|
||||
/// the effects after it, and with Transform Objects off those before it, as they are
|
||||
/// ([`keep_listed`]).
|
||||
/// A live-effect plug-in that paints what it returns ([`painted_plugin`]) makes each path one path
|
||||
/// for each piece it returns ([`paint_piece`]). The raster effects before a Transform effect
|
||||
/// follow it ([`follow_piece`]), except the first `held` effects, which a Transform effect before
|
||||
@@ -1781,7 +1794,8 @@ fn transform_piece(p: &mut Node, (seed, source, kept): (u64, usize, &[usize]), h
|
||||
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, moved) = transform_leaf(leaf, &path, map, copies, options, room);
|
||||
let keep = |k: usize| k > j || (k < j && !options.objects);
|
||||
let (made, moved) = transform_leaf(leaf, &path, map, copies, options, room, Some((item.effects(), &keep)));
|
||||
*used = used.minus(before).plus(made);
|
||||
if moved {
|
||||
follow_piece(leaf, was, from, &e);
|
||||
@@ -1815,6 +1829,30 @@ fn transform_piece(p: &mut Node, (seed, source, kept): (u64, usize, &[usize]), h
|
||||
});
|
||||
}
|
||||
|
||||
/// The effects a Transform effect in a fill's or stroke's own list leaves as they were
|
||||
/// ([`keep_listed`]): the list as it was before the Transform effect was applied, and which of its
|
||||
/// indices keep their values. `None` for an object's own Transform effect.
|
||||
type Kept<'a> = Option<(&'a [Effect], &'a dyn Fn(usize) -> bool)>;
|
||||
|
||||
/// Put back on the fills and strokes of each path under `p` the effect `kept`'s list holds at each
|
||||
/// index it takes, in place of what a Transform effect in their own list scaled or moved with its
|
||||
/// options ([`transform_leaf`]). The effects after the Transform effect apply to the art it made,
|
||||
/// and with Transform Objects off the effects before it keep their values too. A piece's fills and
|
||||
/// strokes share one list: a live-effect plug-in that paints what it returns gives each paint a copy
|
||||
/// of the fill or stroke ([`vectorcraft_plugins::painted::items`]).
|
||||
fn keep_listed(p: &mut Node, kept: Kept) {
|
||||
let Some((was, keep)) = kept else { return };
|
||||
for_each_leaf(p, &mut |leaf| {
|
||||
for item in &mut leaf.appearance.items {
|
||||
for (k, (e, old)) in item.effects_mut().iter_mut().zip(was).enumerate() {
|
||||
if keep(k) {
|
||||
e.clone_from(old);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Make the raster effects of each of `leaf`'s fills and strokes from index `from` on follow
|
||||
/// Transform effect `t`, listed after them, once `t` has moved the leaf
|
||||
/// ([`crate::raster::follow_one`]). The fills and strokes are copies of one fill or stroke (those a
|
||||
|
||||
@@ -198,16 +198,16 @@ fn a_fills_shadow_follows_the_fills_own_transform() {
|
||||
}
|
||||
|
||||
/// A Transform effect after a fill's own Transform effect with Scale Strokes & Effects on turns the
|
||||
/// shadow between them, which the first one scaled; a Transform effect that moves at random holds
|
||||
/// the shadow before it back from the Transform effects after it.
|
||||
/// shadow between them, which the first one leaves as it is; a Transform effect that moves at random
|
||||
/// holds the shadow before it back from the Transform effects after it.
|
||||
#[test]
|
||||
fn a_fills_later_transform_turns_the_shadow_its_options_scaled() {
|
||||
fn a_fills_later_transform_turns_the_shadow_after_its_first() {
|
||||
let n = fill_square(
|
||||
12,
|
||||
vec![transform(json!({"scaleH": 200, "scaleV": 200, "strokes": true})), shadow(10.0, 0.0, 0.0), transform(json!({"rotate": 90}))],
|
||||
);
|
||||
let art = crate::reshape(None, &n, None).unwrap();
|
||||
assert!(near(&shadows_in(&art), &[(0.0, -20.0)]), "{:?}", shadows_in(&art));
|
||||
assert!(near(&shadows_in(&art), &[(0.0, -10.0)]), "{:?}", shadows_in(&art));
|
||||
let held = fill_square(
|
||||
13,
|
||||
vec![
|
||||
@@ -221,6 +221,90 @@ fn a_fills_later_transform_turns_the_shadow_its_options_scaled() {
|
||||
assert!(drawn.len() == 1 && drawn[0].0 > 0.0 && drawn[0].1.abs() < 1e-9, "{drawn:?}");
|
||||
}
|
||||
|
||||
/// A fill's own Transform effect with Transform Patterns or Scale Strokes & Effects on leaves the
|
||||
/// fill's raster effects after it as they are: they paint around the transformed art, its copies
|
||||
/// included. With Transform Objects off it leaves the raster effects before it as they are too.
|
||||
/// Drawn, expanded and baked alike.
|
||||
#[test]
|
||||
fn a_fills_own_transform_leaves_the_shadow_after_it() {
|
||||
let cases = [
|
||||
vec![transform(json!({"scaleH": 200, "scaleV": 200, "strokes": true})), shadow(10.0, 0.0, 0.0)],
|
||||
vec![transform(json!({"scaleH": 200, "scaleV": 200, "patterns": true, "strokes": true})), shadow(10.0, 0.0, 0.0)],
|
||||
vec![transform(json!({"scaleH": 200, "scaleV": 200, "moveH": 120, "copies": 1, "strokes": true})), shadow(10.0, 0.0, 0.0)],
|
||||
vec![transform(json!({"scaleH": 200, "scaleV": 200, "objects": false, "strokes": true})), shadow(10.0, 0.0, 0.0)],
|
||||
vec![shadow(10.0, 0.0, 0.0), transform(json!({"scaleH": 200, "scaleV": 200, "objects": false, "strokes": true}))],
|
||||
// A later Transform effect with an option leaves the shadow after it as well.
|
||||
vec![
|
||||
transform(json!({"scaleH": 200, "scaleV": 200, "patterns": true})),
|
||||
transform(json!({"scaleH": 150, "scaleV": 150, "strokes": true})),
|
||||
shadow(10.0, 0.0, 0.0),
|
||||
],
|
||||
];
|
||||
for effects in cases {
|
||||
let n = fill_square(18, effects.clone());
|
||||
let art = crate::reshape(None, &n, None).unwrap();
|
||||
let drawn = shadows_in(&art);
|
||||
assert!(!drawn.is_empty() && near(&drawn, &vec![(10.0, 0.0); drawn.len()]), "{effects:?}: drawn {drawn:?}");
|
||||
let expanded = shadows_in(&expand_outlined(None, &n, None).unwrap());
|
||||
assert!(!expanded.is_empty() && near(&expanded, &vec![(10.0, 0.0); expanded.len()]), "{effects:?}: expanded {expanded:?}");
|
||||
let mut d = Document::new(800.0, 800.0);
|
||||
let layer = d.layers[0].id;
|
||||
d.insert(Some(layer), 0, n).unwrap();
|
||||
let baked = bake_document(&d).unwrap();
|
||||
let b = shadows_in(baked.node(NodeId(18)).unwrap());
|
||||
assert!(!b.is_empty() && near(&b, &vec![(10.0, 0.0); b.len()]), "{effects:?}: baked {b:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A geometry effect after a fill's own Transform effect with Scale Strokes & Effects on reshapes
|
||||
/// the transformed art by its own distance, as it does after an object's Transform effect.
|
||||
#[test]
|
||||
fn a_fills_own_transform_leaves_the_offset_after_it() {
|
||||
let offset = fx("path.offsetPath", json!({"offset": 5}));
|
||||
let scaled = transform(json!({"scaleH": 200, "scaleV": 200, "strokes": true}));
|
||||
let n = fill_square(19, vec![scaled.clone(), offset.clone()]);
|
||||
let on_fill = crate::reshape(None, &n, None).unwrap();
|
||||
let on_object = crate::reshape(None, &square(20, vec![scaled, offset]), None).unwrap();
|
||||
let (fill, object) = (on_fill.geometric_bounds().unwrap(), on_object.geometric_bounds().unwrap());
|
||||
// The 50 pt square scaled to 100 pt about its center, then offset by 5 pt on each side.
|
||||
assert!((fill.width() - 110.0).abs() < 1e-6 && (fill.height() - 110.0).abs() < 1e-6, "{fill:?}");
|
||||
assert!((fill.width() - object.width()).abs() < 1e-6 && (fill.x0 - object.x0).abs() < 1e-6, "{fill:?} {object:?}");
|
||||
let expanded = expand_outlined(None, &n, None).unwrap().geometric_bounds().unwrap();
|
||||
assert!((expanded.width() - 110.0).abs() < 1e-6 && (expanded.x0 - fill.x0).abs() < 1e-6, "expanded {expanded:?}");
|
||||
}
|
||||
|
||||
/// The SVG filters on `n` and the nodes under it, from the object's effects and from each fill's
|
||||
/// and stroke's.
|
||||
fn filters_in(n: &Node) -> Vec<Effect> {
|
||||
let mut out = vec![];
|
||||
n.walk(&mut |m| {
|
||||
let lists = std::iter::once(&m.appearance.effects).chain(m.appearance.items.iter().map(|i| i.effects()));
|
||||
out.extend(lists.flatten().filter(|e| e.id == crate::SVG_FILTER).cloned());
|
||||
});
|
||||
out
|
||||
}
|
||||
|
||||
/// An SVG filter after a fill's own Transform effect with Scale Strokes & Effects on keeps its
|
||||
/// scale and the origin of its user space, as it does after the same Transform effect without
|
||||
/// options and after an object's own Transform effect.
|
||||
#[test]
|
||||
fn a_fills_own_transform_leaves_the_svg_filter_after_it() {
|
||||
let filter = fx(crate::SVG_FILTER, json!({"scale": 1.0, "origin": [10.0, 10.0]}));
|
||||
for params in [
|
||||
json!({"scaleH": 200, "scaleV": 200, "moveH": 100, "strokes": true}),
|
||||
json!({"scaleH": 200, "scaleV": 200, "moveH": 100, "patterns": true}),
|
||||
json!({"scaleH": 200, "scaleV": 200, "moveH": 100, "copies": 1, "strokes": true}),
|
||||
json!({"scaleH": 200, "scaleV": 200, "objects": false, "strokes": true}),
|
||||
] {
|
||||
let art = crate::reshape(None, &fill_square(21, vec![transform(params.clone()), filter.clone()]), None).unwrap();
|
||||
let drawn = filters_in(&art);
|
||||
assert!(!drawn.is_empty() && drawn.iter().all(|e| *e == filter), "{params}: {drawn:?}");
|
||||
}
|
||||
let object = square(22, vec![transform(json!({"scaleH": 200, "scaleV": 200, "moveH": 100, "strokes": true})), filter.clone()]);
|
||||
let art = crate::reshape(None, &object, None).unwrap();
|
||||
assert!(!filters_in(&art).is_empty() && filters_in(&art).iter().all(|e| *e == filter), "{:?}", filters_in(&art));
|
||||
}
|
||||
|
||||
/// The 3D effects keep the shadow before a Transform effect that shapes the art they project, the
|
||||
/// object's and its fill's 3D effect alike.
|
||||
#[test]
|
||||
|
||||
@@ -222,7 +222,7 @@ fn each_copy_of_a_strokes_own_transform_scales_its_weight_once_more() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_fills_own_transform_with_scale_strokes_and_effects_scales_its_later_effects() {
|
||||
fn a_fills_own_transform_with_scale_strokes_and_effects_leaves_its_later_effects() {
|
||||
let mut n = stroke_with_effect(json!({}));
|
||||
if let Some(AppearanceItem::Fill(f)) = n.appearance.items.first_mut() {
|
||||
f.effects.push(transform(json!({"scaleH": 300, "scaleV": 300, "strokes": true})));
|
||||
@@ -231,10 +231,10 @@ 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") };
|
||||
// The Offset Path, scaled from 5 to 15, applies to the transformed fill's path.
|
||||
// The Offset Path applies to the transformed fill's path at its own 5 pt.
|
||||
assert!(f.effects.is_empty(), "{:?}", f.effects);
|
||||
let w = fill.geometric_bounds().unwrap().width();
|
||||
assert!(close(w, 330.0), "{w}");
|
||||
assert!(close(w, 310.0), "{w}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -476,6 +476,36 @@ fn a_shadow_on_each_fill_a_painted_effect_returns_follows_the_transform_after_it
|
||||
assert_eq!(far, near, "both shadows at 20 pt");
|
||||
}
|
||||
|
||||
/// A Transform effect with Scale Strokes & Effects on after a painted effect on a fill leaves the
|
||||
/// Drop Shadow after it as it is on every paint the piece is drawn with.
|
||||
#[test]
|
||||
fn a_transform_after_a_painted_fill_effect_leaves_the_later_shadow_on_every_paint() {
|
||||
let mut s = session();
|
||||
let id = rect(&mut s, 10.0, "#ff0000");
|
||||
let two = json!({"objects": [{"path": square_at(40.0), "fills": [solid("#00ff00"), solid("#0000ff")]}]}).to_string();
|
||||
install_wat(&mut s, &tpl::returning(&painting("test.engine.twopaints", true), &two));
|
||||
let fill = node(&s, id).appearance.items.iter().position(|i| i.is_fill()).unwrap();
|
||||
run(&mut s, "effect.apply", json!({"ids": [id.0], "effect": "plugin.test.engine.twopaints", "item": fill}));
|
||||
let mut n = node(&s, id);
|
||||
let later = [
|
||||
vectorcraft_doc::Effect { id: "distort.transform".into(), params: json!({"scaleH": 200, "scaleV": 200, "strokes": true}), visible: true },
|
||||
vectorcraft_doc::Effect { id: "stylize.dropShadow".into(), params: json!({"x": 10.0, "y": 0.0, "blur": 0.0}), visible: true },
|
||||
];
|
||||
n.appearance.items[fill].effects_mut().extend(later);
|
||||
let art = vectorcraft_render::effects::reshape(Some(&s.doc().unwrap().doc), &n, None).unwrap();
|
||||
let mut offsets = vec![];
|
||||
art.walk(&mut |m| {
|
||||
for item in &m.appearance.items {
|
||||
for r in vectorcraft_render::effects::raster_effects(item.effects()) {
|
||||
if let vectorcraft_render::effects::RasterFx::DropShadow { dx, dy, .. } = r {
|
||||
offsets.push((dx, dy));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
assert_eq!(offsets, [(10.0, 0.0), (10.0, 0.0)], "one shadow on each of the piece's two fills");
|
||||
}
|
||||
|
||||
/// A result refused for a swatch the document doesn't have stays cached: once the document has the
|
||||
/// swatch, the next check of that result clears the error.
|
||||
#[test]
|
||||
|
||||
+2
-2
@@ -357,8 +357,8 @@ macOS cursor delivery (#2292) is narrowed to a native host adapter using the exi
|
||||
Patterns and Scale Strokes & Effects: off, Transform Objects leaves the geometry in place; the other two scale
|
||||
stroke weights, dashes and the fills' and strokes' effect distances and move pattern tiles with the object, copy
|
||||
`k` by the transformation `k` times. Canvas, export and Expand Appearance agree. A Transform effect in a fill's or
|
||||
stroke's own effect list applies the two options to that fill or stroke and its later effects in the same way.
|
||||
Not yet: object effects after it keep their distances.
|
||||
stroke's own effect list applies the two options to that fill or stroke; the effects listed after it keep their
|
||||
distances and apply to the transformed art, as an object's later effects do.
|
||||
- **Done (#937, reproduced case):** Shape Builder regions on art of more than 4,096 segments (traced or type
|
||||
outlines): the planar map no longer gives up there, so lines cut such art instead of the tool offering whole
|
||||
shapes (the planar map's edge-source and face lookups were quadratic in the segments, now indexed, and the cap is 65,536; Divide with open paths shares it). The edge-source index files a long segment under the grid cells along it, so a diagonal hatch (every line's bounds hold most points) no longer costs a scan of the hatch per edge. The grid holds a budget of references (64 per item plus four rows' or columns' worth), the items past it scanned, so coincident segments can't make it quadratic in memory.
|
||||
|
||||
@@ -592,7 +592,7 @@ since 2026-10-01.
|
||||
- **`.ai`/EPS type laid out closer to the file's page (#6137):** type from the text document keeps auto leading as auto, each line taking the paragraph's Auto Leading share (key 7) of the largest type on it ([Line and character spacing](https://helpx.adobe.com/illustrator/using/line-character-spacing.html)); the paragraph's hyphenation options (keys 10 to 15) are read; area type of one frame whose last lines the file sets, but which don't fit here, may get a frame taller from its bottom, by its lines' deepest descent plus 0.05 pt and only when that shows more of those lines and no more than the file has (`vectorcraft_text::fit_source_lines`), with a warning when it does: the frame can stay as it is, or show only some of them; and a line a soft return ends is justified as a wrapped line is, a paragraph's last line keeping its alignment. Evidence: `ate::tests::auto_leading_follows_the_size_of_each_line`, `ate::tests::a_paragraph_keeps_its_auto_leading`, `ate::tests::a_paragraph_keeps_its_hyphenation_options`, `ate::tests::the_last_lines_the_file_sets_in_its_frame_show`, `tests::fit_source_lines_keeps_the_last_lines_the_source_sets`, `tests::typo::soft_return_in_justified_text_spreads_like_a_wrapped_line`. Limits: frames grow only for area type of one frame, horizontal, by its lines' deepest descent plus 0.05 pt; type in a font that isn't installed, and hyphenation in a language that isn't imported, still lay out differently. Scores unchanged.
|
||||
- **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: [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.
|
||||
- **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 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`). The effects listed after it in that fill's or stroke's list keep their own distances and apply to the transformed art and to each copy, as an object's later effects do after the object's Transform effect: a Drop Shadow after it keeps its offset, an Offset Path after it offsets by its own distance, and an SVG filter after it keeps its scale and the origin of its user space. With Transform Objects off, the effects before it keep their distances too. Before, Scale Strokes & Effects scaled a fill's or stroke's later effects by the mean scale, so existing documents with such effects draw differently. Effects apply in the order they are listed ([Appearance attributes](https://helpx.adobe.com/illustrator/desktop/special-effects-styles/apply-graphic-styles/appearance-attributes-in-illustrator.html)); how the two options treat the effects after a fill's or stroke's own Transform effect is VectorCraft's own rule, following that order (effects `tests::rasterfollow::a_fills_own_transform_leaves_the_shadow_after_it`, `tests::rasterfollow::a_fills_own_transform_leaves_the_offset_after_it`, `tests::rasterfollow::a_fills_own_transform_leaves_the_svg_filter_after_it`, `tests::transformfx::a_fills_own_transform_with_scale_strokes_and_effects_leaves_its_later_effects`; engine `plugins::a_transform_after_a_painted_fill_effect_leaves_the_later_shadow_on_every_paint`). 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.
|
||||
- **Raster effects before a Transform effect:** effects apply in the order they are listed ([Appearance attributes](https://helpx.adobe.com/illustrator/desktop/special-effects-styles/apply-graphic-styles/appearance-attributes-in-illustrator.html)). A Drop Shadow, glow, blur, Feather or pixel filter listed before a Transform effect in the same list (an object's effects, or a fill's or stroke's own) now follows that Transform effect: its sizes scale by the mean scale, and a Drop Shadow's offset turns, reflects and scales with the art. VectorCraft applies this rule with Scale Strokes & Effects on or off. Before, such an effect was drawn at its own distances around the transformed art. The canvas, export, Expand Appearance, Object › Expand, Outline Stroke and the 3D effects use the same values (effects `tests::rasterfollow`; render `a_transform_after_a_drop_shadow_scales_the_shadow_with_the_art`; engine `objexpand::a_shadow_before_a_transform_expands_as_the_canvas_draws_it`, `outlinestroke::a_shadow_before_a_transform_outlines_as_the_canvas_draws_it`). Not yet: a Transform effect that makes copies or moves at random leaves the raster effects before it as they are; a Rasterize effect before a Transform effect keeps its pixel grid and room around the art; the angles and light directions of pixel filters do not turn or reflect with the art. An object's or group's Transform effect changes the effects of its fills, strokes and members only through Scale Strokes & Effects, as before. 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.
|
||||
- **Placed gradients follow the Warp effects:** under each of the fifteen Warp styles with a bend or a distortion, a gradient placed with the Gradient tool moves and scales from the object's bounds before the warp to its bounds after it, as under Free Distort; a stroke's gradient uses those bounds inflated by the stroke. This holds for an object's own Warp and for a fill's or stroke's own, on the canvas and in Object › Expand of the fill. In a group with a Warp effect, each path's and compound path's gradients follow its own bounds, a nested group's own fills and strokes follow the nested group's bounds, and the group's own fills and strokes follow the group's bounds. In the art Pathfinder and compound shapes take, a symbol instance's Warp moves the gradients of its symbol's art by the art's bounds, and a compound shape's Warp moves its gradients by the bounds of its area. A gradient fitted to the object still fits the warped outline, and a warp with no bend and no distortion leaves the gradient exactly in place. Existing documents with a Warp effect on art with a placed gradient draw differently: before, the gradient stayed where it was while the outline changed. How gradients follow the warp is VectorCraft's own rule; what the Warp effects do to the object is described in [Reshaping with effects](https://helpx.adobe.com/illustrator/using/reshaping-objects-effects.html) (effects `tests::warp_bends_when_the_warp_moves_the_outline`, `bake::tests::baked_warps_move_placed_gradients_from_the_bounds_to_the_warped_bounds`, `bake::tests::a_warp_with_no_bend_leaves_a_placed_gradient_exactly_in_place`, `bake::tests::a_fills_and_a_strokes_own_warp_move_its_placed_gradient_with_the_bounds`, `group::tests::a_groups_warp_moves_the_gradients_placed_on_its_paths_by_each_paths_bounds`, `group::tests::a_groups_warp_moves_its_own_a_nested_groups_and_a_compound_paths_gradients_by_their_bounds`, `group::tests::shape_art_moves_the_gradients_of_a_symbol_instances_art_and_a_compound_shape_with_a_warp`; render `objectfx::a_paths_gradient_moves_and_scales_with_its_warped_bounds`; engine `objexpand::a_placed_gradient_moved_by_a_warp_expands_as_the_canvas_draws_it`). Scores and hours unchanged.
|
||||
|
||||
Reference in New Issue
Block a user