mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-10 06:31:19 +00:00
Tests: placed gradients under Warp on strokes, symbol art and compound shapes
Covers the other places a Warp now moves placed gradients: a group's and a nested group's own strokes and a compound path's stroke (with the stroke-inflated bounds), the art of a symbol instance and the area of a compound shape with a Warp, and a stroke's own Warp on the canvas. A new test checks that Arc's no-bend threshold for gradients matches the one the warp uses, and the no-bend test now covers all fifteen styles. The render test's Fisheye case, which left the square's bounds as they were, is replaced by a vertical Bulge, and each style there is checked to move the outline's left or right edge. The Object > Expand test checks that it measures the ramp and not an edge of the outline. The params text and docs/mcp.md now state which bounds each gradient follows, and that a warp moves them with a bend or a distortion.
This commit is contained in:
@@ -284,7 +284,7 @@ fn a_warp_with_no_bend_leaves_a_placed_gradient_exactly_in_place() {
|
||||
let geom = GradientGeom { start: Point::new(13.7, 31.3), end: Point::new(91.1, 47.9), aspect: 1.0, focal: None };
|
||||
let placed = Paint::Gradient(Box::new(GradientPaint { geom: Some(geom), ..GradientPaint::new(Gradient::new(GradientKind::Linear, stops)) }));
|
||||
let mut drift = 0;
|
||||
let cases = ["arc", "bulge", "flag", "wave", "rise", "fisheye", "inflate", "squeeze", "twist"].map(|s| (s, 0.0));
|
||||
let cases = crate::WARP_STYLES.map(|(s, _)| (s, 0.0));
|
||||
for (k, (style, bend)) in cases.into_iter().chain([("arc", 1e-5), ("arc", -1e-5)]).enumerate() {
|
||||
let k = k as f64;
|
||||
let oval = Rect::new(10.1 + k * 0.37, 20.3 - k * 1.13, 97.7 + k * 3.31, 71.9 + k * 0.71);
|
||||
|
||||
@@ -109,7 +109,7 @@ fn lengths_of(id: &str) -> Lengths {
|
||||
}
|
||||
}
|
||||
|
||||
const WARP_DOC: &str = "{bend: % (-100..100, 50; placed gradients move and scale from each path's bounds to its warped bounds), horizontal: % distortion (0), vertical: % distortion (0), orientation: \"horizontal\"|\"vertical\"}";
|
||||
const WARP_DOC: &str = "{bend: % (-100..100, 50; with a bend or a distortion, placed gradients move and scale from the object's bounds to its warped bounds), horizontal: % distortion (0), vertical: % distortion (0), orientation: \"horizontal\"|\"vertical\"}";
|
||||
|
||||
const DT: &[&str] = &["Effect", "Distort & Transform"];
|
||||
|
||||
|
||||
@@ -349,6 +349,42 @@ fn shape_art_moves_the_gradient_of_a_symbol_instances_art_with_its_transform() {
|
||||
assert!((out.geometric_bounds().unwrap().x0 - 30.0).abs() < 1e-6, "the art moves");
|
||||
}
|
||||
|
||||
/// Under a Warp effect, the gradient placed on a symbol instance's art's own fill follows the art's
|
||||
/// bounds, and the gradient placed on a compound shape follows its area's bounds, each from before
|
||||
/// the warp to after it, in the art Pathfinder takes.
|
||||
#[test]
|
||||
fn shape_art_moves_the_gradients_of_a_symbol_instances_art_and_a_compound_shape_with_a_warp() {
|
||||
use vectorcraft_color::{Gradient, GradientGeom, GradientKind, GradientPaint, GradientStop};
|
||||
let stops = vec![GradientStop::new(0.0, Color::rgb(1.0, 0.0, 0.0)), GradientStop::new(1.0, Color::rgb(0.0, 0.0, 1.0))];
|
||||
let ramp = GradientPaint::new(Gradient::new(GradientKind::Linear, stops));
|
||||
let geom = GradientGeom { start: (2.0, 3.0).into(), end: (9.0, 3.0).into(), aspect: 1.0, focal: None };
|
||||
let placed = Paint::Gradient(Box::new(GradientPaint { geom: Some(geom), ..ramp }));
|
||||
let follow = |p: Point, from: Rect, to: Rect| {
|
||||
Point::new(to.x0 + (p.x - from.x0) / from.width() * to.width(), to.y0 + (p.y - from.y0) / from.height() * to.height())
|
||||
};
|
||||
let check = |out: &Node, from: Rect, what: &str| {
|
||||
let to = out.geometric_bounds().unwrap();
|
||||
assert_ne!(from, to, "{what}: the warp changes the bounds");
|
||||
let Paint::Gradient(g) = out.appearance.fill_paint() else { panic!("{what}: a gradient: {:?}", out.appearance) };
|
||||
let got = g.geom.unwrap();
|
||||
let (s, e) = (follow(geom.start, from, to), follow(geom.end, from, to));
|
||||
assert!(got.start.distance(s) < 1e-6 && got.end.distance(e) < 1e-6, "{what}: {got:?}, want {s:?} {e:?}");
|
||||
};
|
||||
for style in ["arc", "rise", "bulge"] {
|
||||
let warp = || Effect { id: format!("warp.{style}"), params: serde_json::json!({"bend": 40.0}), visible: true };
|
||||
let mut art = Node::group(NodeId(20), vec![sq(21, 0.0, Color::BLACK)]);
|
||||
art.appearance.items.push(AppearanceItem::Fill(vectorcraft_doc::FillLayer::new(placed.clone())));
|
||||
let hook = move |_: &Node| Some(art.clone());
|
||||
let mut instance = Node::new(NodeId(1), NodeKind::SymbolInstance { symbol: "s".into(), xf: vectorcraft_geom::Affine::IDENTITY });
|
||||
instance.appearance.effects.push(warp());
|
||||
check(&shape_art(&instance, Some(&hook)).unwrap(), Rect::new(0.0, 0.0, 10.0, 10.0), &format!("warp.{style} on a symbol instance"));
|
||||
let mut shape = compound_shape(ShapeMode::Add);
|
||||
shape.appearance = Appearance::basic(placed.clone(), Paint::None, 0.0).into();
|
||||
shape.appearance.effects.push(warp());
|
||||
check(&shape_art(&shape, None).unwrap(), Rect::new(0.0, 0.0, 15.0, 10.0), &format!("warp.{style} on a compound shape"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_groups_transform_and_free_distort_move_the_gradients_placed_on_its_paths() {
|
||||
use vectorcraft_color::{Gradient, GradientGeom, GradientKind, GradientPaint, GradientStop};
|
||||
@@ -412,9 +448,10 @@ fn a_groups_warp_moves_the_gradients_placed_on_its_paths_by_each_paths_bounds()
|
||||
}
|
||||
}
|
||||
|
||||
/// Under a group's Warp effect, the group's own placed gradient follows the group's bounds, a
|
||||
/// nested group's own placed gradient follows the nested group's bounds, and a compound path's
|
||||
/// placed gradient follows the compound path's bounds, each from before the warp to after it.
|
||||
/// Under a group's Warp effect, the group's own placed gradients follow the group's bounds, a
|
||||
/// nested group's own placed gradients follow the nested group's bounds, and a compound path's
|
||||
/// placed gradients follow the compound path's bounds, each from before the warp to after it (a
|
||||
/// stroke's: those bounds inflated by the stroke).
|
||||
#[test]
|
||||
fn a_groups_warp_moves_its_own_a_nested_groups_and_a_compound_paths_gradients_by_their_bounds() {
|
||||
use vectorcraft_color::{Gradient, GradientGeom, GradientKind, GradientPaint, GradientStop};
|
||||
@@ -422,7 +459,10 @@ fn a_groups_warp_moves_its_own_a_nested_groups_and_a_compound_paths_gradients_by
|
||||
let ramp = GradientPaint::new(Gradient::new(GradientKind::Linear, stops));
|
||||
let geom = GradientGeom { start: (12.0, 4.0).into(), end: (26.0, 4.0).into(), aspect: 1.0, focal: None };
|
||||
let placed = Paint::Gradient(Box::new(GradientPaint { geom: Some(geom), ..ramp }));
|
||||
let own_fill = |n: &mut Node| n.appearance.items.push(AppearanceItem::Fill(vectorcraft_doc::FillLayer::new(placed.clone())));
|
||||
let own_fill = |n: &mut Node| {
|
||||
n.appearance.items.push(AppearanceItem::Fill(vectorcraft_doc::FillLayer::new(placed.clone())));
|
||||
n.appearance.items.push(AppearanceItem::Stroke(vectorcraft_doc::StrokeLayer::new(placed.clone(), 3.0)));
|
||||
};
|
||||
let follow = |p: Point, from: Rect, to: Rect| {
|
||||
Point::new(to.x0 + (p.x - from.x0) / from.width() * to.width(), to.y0 + (p.y - from.y0) / from.height() * to.height())
|
||||
};
|
||||
@@ -434,7 +474,7 @@ fn a_groups_warp_moves_its_own_a_nested_groups_and_a_compound_paths_gradients_by
|
||||
Arc::new(Node::path(NodeId(7), shapes::rectangle(Rect::new(23.0, 3.0, 27.0, 7.0)), Appearance::default())),
|
||||
];
|
||||
let mut compound = Node::new(NodeId(3), NodeKind::Compound { children: ring, rule: FillRule::EvenOdd });
|
||||
compound.appearance = Appearance::basic(placed.clone(), Paint::None, 0.0).into();
|
||||
compound.appearance = Appearance::basic(placed.clone(), placed.clone(), 2.0).into();
|
||||
let mut g = Node::group(NodeId(10), vec![Arc::new(inner), Arc::new(compound), sq(2, 40.0, Color::BLACK)]);
|
||||
own_fill(&mut g);
|
||||
let mut before = std::collections::HashMap::new();
|
||||
@@ -460,19 +500,29 @@ fn a_groups_warp_moves_its_own_a_nested_groups_and_a_compound_paths_gradients_by
|
||||
let Paint::Gradient(gp) = item.paint() else { continue };
|
||||
let (from, to) = (before[&c.id], after[&c.id]);
|
||||
assert_ne!(from, to, "warp.{style} {:?}: the warp changes the bounds", c.id);
|
||||
let stroke = match item {
|
||||
AppearanceItem::Stroke(s) => Some(s),
|
||||
AppearanceItem::Fill(_) => None,
|
||||
};
|
||||
let (from, to) = match stroke {
|
||||
Some(s) => (s.paint_bounds(from), s.paint_bounds(to)),
|
||||
None => (from, to),
|
||||
};
|
||||
let got = gp.geom.unwrap();
|
||||
let (s, e) = (follow(geom.start, from, to), follow(geom.end, from, to));
|
||||
assert!(
|
||||
got.start.distance(s) < 1e-6 && got.end.distance(e) < 1e-6,
|
||||
"warp.{style} {:?}: {got:?}, want {s:?} {e:?} ({from:?} to {to:?})",
|
||||
c.id
|
||||
"warp.{style} {:?} (stroke: {}): {got:?}, want {s:?} {e:?} ({from:?} to {to:?})",
|
||||
c.id,
|
||||
stroke.is_some()
|
||||
);
|
||||
checked.push(c.id);
|
||||
checked.push((c.id, stroke.is_some()));
|
||||
}
|
||||
});
|
||||
checked.sort();
|
||||
checked.dedup();
|
||||
assert_eq!(checked, vec![NodeId(3), NodeId(5), NodeId(10)], "warp.{style}");
|
||||
let want: Vec<_> = [NodeId(3), NodeId(5), NodeId(10)].into_iter().flat_map(|id| [(id, false), (id, true)]).collect();
|
||||
assert_eq!(checked, want, "warp.{style}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -180,6 +180,24 @@ fn warp_zero_bend_is_identity_ish() {
|
||||
}
|
||||
}
|
||||
|
||||
/// `warp::bends` says a warp moves placed gradients exactly when the warp moves the outline:
|
||||
/// Arc leaves the outline as it is under a bend of 0.0001 % and moves it above that, and the other
|
||||
/// styles move it at any bend.
|
||||
#[test]
|
||||
fn warp_bends_when_the_warp_moves_the_outline() {
|
||||
// A star: Fisheye moves nothing on the edge of the box, where an ellipse or a rectangle lies.
|
||||
let oval = shapes::star(Point::new(51.7, 34.3), 40.9, 17.3, 5, 7.0);
|
||||
for (s, _) in WARP_STYLES {
|
||||
let id = format!("warp.{s}");
|
||||
let flat = run(&id, json!({"bend": 0}), &oval);
|
||||
for bend in [0.0, 1e-5, -1e-5, 0.9e-4, -0.9e-4, 1.1e-4, -1.1e-4] {
|
||||
let p = json!({ "bend": bend });
|
||||
let moved = run(&id, p.clone(), &oval) != flat;
|
||||
assert_eq!(crate::warp::bends(&id, &p), moved, "{id} at {bend} %");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn warp_arc_bends_the_top_edge() {
|
||||
let out = run("warp.arc", json!({"bend": 50}), &square());
|
||||
|
||||
@@ -34,7 +34,9 @@ pub fn warp(path: &PathData, b: Rect, style: WarpStyle, p: &Value, max: usize) -
|
||||
}
|
||||
|
||||
/// Does warp effect `id` with params `p` move any point? Every style leaves the box as it is with
|
||||
/// no bend and no distortion, and Arc (`warp.arc`) takes a bend under 0.0001 % as none.
|
||||
/// no bend and no distortion, and Arc (`warp.arc`) takes a bend under 0.0001 % as none: the
|
||||
/// threshold must match the one in [`warp_point`]'s Arc case (`warp_bends_when_the_warp_moves_the_outline`
|
||||
/// tests both sides of it).
|
||||
pub(crate) fn bends(id: &str, p: &Value) -> bool {
|
||||
let share = |key: &str, default: f64| num(p, key, default).clamp(-100.0, 100.0) / 100.0;
|
||||
let bend = share("bend", 50.0);
|
||||
|
||||
@@ -194,10 +194,14 @@ fn a_placed_gradient_moved_by_a_warp_expands_as_the_canvas_draws_it() {
|
||||
let want = b.x0 + (115.0 - 40.0) / 120.0 * b.width();
|
||||
assert!((want - 115.0).abs() > 2.0, "{effect} {params}: the warp moves the midpoint ({b:?})");
|
||||
let red = |x: usize| f64::from(after.pixels[(100 * 200 + x) * 4]);
|
||||
let half = (b.x0.ceil() as usize + 1..b.x1.floor() as usize - 1)
|
||||
.find(|&x| red(x) >= 127.5 && red(x + 1) < 127.5)
|
||||
.map(|x| x as f64 + 0.5 + (red(x) - 127.5) / (red(x) - red(x + 1)))
|
||||
.unwrap();
|
||||
// White turns dark once on row 100, and gradually, so the crossing is the ramp's midpoint
|
||||
// and not an edge of the outline.
|
||||
let crossings: Vec<usize> =
|
||||
(b.x0.ceil() as usize + 3..b.x1.floor() as usize - 4).filter(|&x| red(x) >= 127.5 && red(x + 1) < 127.5).collect();
|
||||
assert_eq!(crossings.len(), 1, "{effect} {params}: white turns dark once on row 100: {crossings:?}");
|
||||
let x = crossings[0];
|
||||
assert!(red(x - 2) - red(x + 3) < 100.0, "{effect} {params}: the ramp, not an edge: {} to {}", red(x - 2), red(x + 3));
|
||||
let half = x as f64 + 0.5 + (red(x) - 127.5) / (red(x) - red(x + 1));
|
||||
assert!((half - want).abs() < 1.0, "{effect} {params} on item {item}: the midpoint is at {half}, want {want}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -874,9 +874,20 @@ fn a_paths_gradient_moves_and_scales_with_its_warped_bounds() {
|
||||
geom: Some(GradientGeom { start: Point::new(30.0, 50.0), end: Point::new(60.0, 50.0), aspect: 1.0, focal: None }),
|
||||
..ramp.clone()
|
||||
};
|
||||
for (style, params) in [("arc", json!({"bend": 50})), ("fisheye", json!({"bend": 60})), ("rise", json!({"bend": 40, "horizontal": 50}))] {
|
||||
let e = fx(&format!("warp.{style}"), params);
|
||||
let styles = [
|
||||
("arc", json!({"bend": 50})),
|
||||
("bulge", json!({"bend": 60, "orientation": "vertical"})),
|
||||
("rise", json!({"bend": 40, "vertical": 50, "orientation": "vertical"})),
|
||||
];
|
||||
for (style, params) in styles.clone() {
|
||||
let e = fx(&format!("warp.{style}"), params.clone());
|
||||
let (got, want) = (draw(&placed, e.clone()), draw(&ramp, e));
|
||||
// The warp moves the outline's left or right edge, so a gradient left in place would not
|
||||
// run from edge to edge.
|
||||
let square = vectorcraft_geom::Rect::new(30.0, 35.0, 60.0, 65.0);
|
||||
let path = shapes::rectangle(square);
|
||||
let b = vectorcraft_effects::apply_geometry(&[fx(&format!("warp.{style}"), params.clone())], &path, square).bounds().unwrap();
|
||||
assert!((b.x0 - 30.0).abs() > 0.5 || (b.x1 - 60.0).abs() > 0.5, "warp.{style}: the outline spans {b:?}");
|
||||
let mut worst = 0;
|
||||
for y in 0..100 {
|
||||
for x in 0..100 {
|
||||
@@ -886,6 +897,52 @@ fn a_paths_gradient_moves_and_scales_with_its_warped_bounds() {
|
||||
}
|
||||
assert!(worst <= 3, "warp.{style}: worst channel difference {worst}");
|
||||
}
|
||||
// A stroke's own Warp: the canvas draws its gradient moved and scaled from the stroke-inflated
|
||||
// bounds before the warp to those after it, as on the warped path with that gradient.
|
||||
let geom = GradientGeom { start: Point::new(35.0, 50.0), end: Point::new(55.0, 50.0), aspect: 1.0, focal: None };
|
||||
let stroked = Paint::Gradient(Box::new(GradientPaint { geom: Some(geom), ..ramp.clone() }));
|
||||
let square = vectorcraft_geom::Rect::new(30.0, 35.0, 60.0, 65.0);
|
||||
let follow = |p: Point, from: vectorcraft_geom::Rect, to: vectorcraft_geom::Rect| {
|
||||
Point::new(to.x0 + (p.x - from.x0) / from.width() * to.width(), to.y0 + (p.y - from.y0) / from.height() * to.height())
|
||||
};
|
||||
for (style, params) in styles {
|
||||
let mut n = Node::path(NodeId(0), shapes::rectangle(square), Appearance::basic(Paint::None, stroked.clone(), 6.0));
|
||||
let Some(AppearanceItem::Stroke(layer)) = n.appearance.items.iter().find(|i| matches!(i, AppearanceItem::Stroke(_))).cloned() else {
|
||||
panic!("a stroke")
|
||||
};
|
||||
let Some(item) = n.appearance.items.iter_mut().find(|i| matches!(i, AppearanceItem::Stroke(_))) else { panic!("a stroke") };
|
||||
item.effects_mut().push(fx(&format!("warp.{style}"), params));
|
||||
let d = doc_with(Document::new(100.0, 100.0), n);
|
||||
let live = render(&d);
|
||||
let baked = vectorcraft_effects::bake_document(&d).unwrap();
|
||||
let mut warped = None;
|
||||
for l in &baked.layers {
|
||||
l.walk(&mut |c: &Node| {
|
||||
if let (Some(path), true) = (c.path_data(), c.appearance.items.iter().any(|i| matches!(i, AppearanceItem::Stroke(_)))) {
|
||||
warped.get_or_insert_with(|| path.clone());
|
||||
}
|
||||
});
|
||||
}
|
||||
let path = warped.unwrap();
|
||||
let b = vectorcraft_geom::Shape::bounding_box(&path.to_bezpath());
|
||||
assert_ne!(b, square, "warp.{style}: the warp changes the bounds");
|
||||
let (from, to) = (layer.paint_bounds(square), layer.paint_bounds(b));
|
||||
let moved = GradientGeom { start: follow(geom.start, from, to), end: follow(geom.end, from, to), ..geom };
|
||||
let want_node = Node::path(
|
||||
NodeId(0),
|
||||
path,
|
||||
Appearance::basic(Paint::None, Paint::Gradient(Box::new(GradientPaint { geom: Some(moved), ..ramp.clone() })), 6.0),
|
||||
);
|
||||
let want = render(&doc_with(Document::new(100.0, 100.0), want_node));
|
||||
let mut worst = 0;
|
||||
for y in 0..100 {
|
||||
for x in 0..100 {
|
||||
let (a, b) = (live.pixel(x, y), want.pixel(x, y));
|
||||
worst = worst.max((0..3).map(|i| a[i].abs_diff(b[i])).max().unwrap_or(0));
|
||||
}
|
||||
}
|
||||
assert!(worst <= 3, "warp.{style} on the stroke: worst channel difference {worst}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+4
-2
@@ -2283,8 +2283,10 @@ counts as a value left out except for `recolor.apply`'s `model` and a `map` row'
|
||||
|
||||
Groups and layers carry fills, strokes and effects of their own, as in the reference app: their fills and strokes
|
||||
paint every member's geometry, and their effects apply to the members as one piece (one combined drop shadow; a
|
||||
Transform or Warp moves or bends the whole group; under a Warp, the gradient placed on each path inside moves and scales
|
||||
from that path's bounds to its warped bounds). Their Contents row (type: Characters) is a slot in the stack:
|
||||
Transform or Warp moves or bends the whole group). Under a group's Warp, each placed gradient moves and scales from
|
||||
bounds before the warp to bounds after it: a path's or compound path's gradients use its own bounds, a nested group's
|
||||
own fills and strokes use the nested group's bounds, and the group's own fills and strokes use the group's bounds; a
|
||||
stroke uses those bounds inflated by the stroke. Their Contents row (type: Characters) is a slot in the stack:
|
||||
fills and strokes above it paint over the members (characters), those below under them; new items go above it
|
||||
(on an image, below it).
|
||||
`appearance.moveItem {"from": "contents", "to": n}` puts the row above the bottom `n` items, and an item move takes
|
||||
|
||||
@@ -537,9 +537,9 @@ since 2026-10-01.
|
||||
- **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.
|
||||
- **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.
|
||||
- **Distort Appearance with Transform effect options:** with Envelope Options › Distort Appearance on, a path whose Transform effect has Scale Strokes & Effects or Transform Patterns on, or copies it with a gradient, is bent as the same per-copy art it draws and expands as outside the envelope: each copy's own stroke weight, dashes, pattern placement and gradient (effects `bake::tests`). Scores and hours unchanged.
|
||||
- **Placed gradients follow Transform and Free Distort effects:** a gradient placed with the Gradient tool moves, turns and scales with the object when the object's Transform effect moves its outline, and each copy of the Transform effect takes it as many times as it takes the transform. Under Free Distort it moves and scales from the path's bounds before the effect to its bounds after it. This holds on the canvas, in export, in Expand Appearance, in Pathfinder and compound shapes, and in Object › Expand of the fill, for an object's own effects, a group's (the own fills of the group, of the groups inside it, of a symbol's art, and of the images and symbol instances it moves, included) and a fill's or stroke's own. A gradient on a path inside a group with a Transform effect now moves with the path (#2214 moved only the gradients of type), and so does a gradient on an image's or symbol instance's own fill. When a path keeps its shape because the effect would pass the geometry budget, the group's own gradients stay in place. How gradients follow each effect is VectorCraft's own rule; what each effect does to the object is described in [Distort and transform effects](https://helpx.adobe.com/illustrator/desktop/manage-objects/reshape-transform-objects/distort-and-transform-effects-in-illustrator.html) (effects `bake::tests::a_baked_transform_moves_a_placed_gradient_with_the_path`, `bake::tests::a_strokes_own_transform_and_free_distort_move_its_placed_gradient`, `bake::tests::a_fills_gradient_follows_the_effects_around_its_transform_with_options`, `bake::tests::baked_free_distort_moves_placed_gradients_from_the_bounds_to_the_distorted_bounds`, `group::tests::a_groups_transform_and_free_distort_move_the_gradients_placed_on_its_paths`, `group::tests::a_groups_transform_moves_the_gradient_of_its_own_fill`, `group::tests::a_nested_groups_own_gradient_follows_the_outer_groups_transform_and_free_distort`, `group::tests::a_groups_transform_moves_the_gradients_placed_on_an_images_own_fills`, `group::tests::shape_art_moves_gradients_with_a_transform_effect`, `group::tests::shape_art_moves_the_gradient_of_a_symbol_instances_art_with_its_transform`, `tests::art_past_the_budget_keeps_its_own_gradients_in_place`; render `objectfx::a_paths_gradient_moves_with_its_transform_and_free_distort`; engine `objexpand::a_placed_gradient_moved_by_a_transform_or_free_distort_expands_as_the_canvas_draws_it`). Scores and hours unchanged.
|
||||
- **Placed gradients follow the Warp effects:** under each of the fifteen Warp styles, a gradient placed with the Gradient tool moves and scales from the path's bounds before the warp to its bounds after it, as under Free Distort. This holds for an object's own Warp and for a fill's or stroke's own (a stroke uses its stroke-inflated bounds), on the canvas and in Object › Expand of the fill. In a group with a Warp effect, each path's and compound path's gradient follows its own bounds, a nested group's own fills follow the nested group's bounds, and the group's own fills follow the group's bounds. 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 `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`; 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.
|
||||
- **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 (<https://helpx.adobe.com/illustrator/desktop/manage-objects/arrange-objects/rotate-objects-by-specific-angles.html>). Tests: `tests_constrain_tools`, `tests_constrain_new` (engine), `geom` `constrain_axes` / `rotate_deg`, the shape, grid and type tool tests.
|
||||
|
||||
- **Clipping paths on their outline (G1):** the selection tools (Selection, Direct Selection, Group Selection), Object Highlighting and a right-click take a clipping path on its outline before the art it clips; Select Behind lists the same; the eyedropper, the tools that act on what is drawn and the selection tools' double-clicks, loaded text and guide release take the art. Doc and tool regression tests; score unchanged.
|
||||
|
||||
Reference in New Issue
Block a user