M8.16: Envelopes bend images, symbols, gradients, patterns and appearance; warps keep their frame

- Images distort as a raster mesh warp: cut by Fidelity into triangles,
  each a clipped image under the affine map of its corners (pieces overlap
  slightly, no seams; the renderer paints them without clip layers; SVG
  writes the pixels once and <use>s them).
- Symbol instances distort as their symbol's art (the renderer's cache
  follows edits to the symbol).
- Distort Appearance bakes strokes (filled outlines) and geometry effects
  before bending; Distort Linear Gradients bends linear gradients; Distort
  Pattern Fills bends the tiles. Opacity masks of the content bend too.
- Bug fix: a rotated, sheared or reflected warp envelope re-warped along
  the page axes. Envelopes now keep their own frame (append-only model
  field), so the warp turns with the art; move and scale keep it square.
- Live evaluation shares one set of hooks (vectorcraft_doc::live::Hooks,
  vectorcraft_effects::{expand_live, expanded_live_group,
  expand_live_deep}) between the canvas, Expand and every exporter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bflatastic
2026-10-06 12:01:47 +07:00
co-authored by Claude Opus 5.5
parent cd99714b42
commit 04fa49413c
21 changed files with 1008 additions and 146 deletions
+25 -23
View File
@@ -10,7 +10,8 @@ use std::sync::Arc;
use serde_json::{Value, json};
use vectorcraft_color::{Color, Paint};
use vectorcraft_doc::live::{
self, BlendDefaults, BlendOrientation, BlendSpacing, BlendSpec, EnvelopeKind, EnvelopeOptions, GradientMesh, MeshAppearance, PreserveShape, Spine,
self, BlendDefaults, BlendOrientation, BlendSpacing, BlendSpec, EnvelopeKind, EnvelopeMap, EnvelopeOptions, GradientMesh, MeshAppearance,
PreserveShape, Spine,
};
use vectorcraft_doc::{Appearance, Document, Node, NodeId, NodeKind, Selection};
use vectorcraft_geom::{Affine, PathData, Point};
@@ -930,7 +931,7 @@ fn envelope_defaults(s: &Session) -> (EnvelopeOptions, f64) {
}
fn envelope(id: NodeId, content: Vec<Arc<Node>>, kind: EnvelopeKind, (options, fidelity): (EnvelopeOptions, f64)) -> Node {
Node::new(id, NodeKind::Envelope { content, kind, fidelity, editing: false, options })
Node::new(id, NodeKind::Envelope { content, kind, fidelity, editing: false, options, frame: Affine::IDENTITY })
}
/// `rows` and `cols` (or `columns`), 1..50 each, defaulting to `default`.
@@ -954,14 +955,11 @@ fn env_make_warp(s: &mut Session, p: &Value) -> Result<Value> {
Ok(json!({ "id": id.0 }))
}
/// A mesh envelope of `rows`×`cols` patches over `content`: following the surface of `shape` (an
/// envelope's current kind), or a flat grid over the content's bounds.
fn mesh_kind(content: &[Arc<Node>], shape: Option<&EnvelopeKind>, rows: u32, cols: u32) -> Option<EnvelopeKind> {
let points = match shape {
Some(k) => live::envelope_surface(content, k, rows, cols, Color::BLACK)?.points.into_iter().map(|m| m.p).collect(),
None => live::grid_points(live::nodes_bounds(content)?, rows, cols),
};
Some(EnvelopeKind::Mesh { rows, cols, points })
/// A mesh envelope of `rows`×`cols` patches for an envelope with map `map`: following its
/// current surface (`maintain`), or a flat grid over its content.
fn mesh_kind(map: &EnvelopeMap, maintain: bool, rows: u32, cols: u32) -> EnvelopeKind {
let points = if maintain { map.surface_mesh(rows, cols, Color::BLACK).points.into_iter().map(|m| m.p).collect() } else { map.grid(rows, cols) };
EnvelopeKind::Mesh { rows, cols, points }
}
fn env_make_mesh(s: &mut Session, p: &Value) -> Result<Value> {
@@ -970,7 +968,8 @@ fn env_make_mesh(s: &mut Session, p: &Value) -> Result<Value> {
let roots = roots_param(s, p)?;
let st = s.doc()?;
let nodes: Vec<Arc<Node>> = roots.iter().filter_map(|id| st.doc.node(*id).cloned()).map(Arc::new).collect();
let kind = mesh_kind(&nodes, None, rows, cols).ok_or_else(|| bad(C, "selection has no bounds"))?;
let src = live::nodes_bounds(&nodes).ok_or_else(|| bad(C, "selection has no bounds"))?;
let kind = EnvelopeKind::Mesh { rows, cols, points: live::grid_points(src, rows, cols) };
let defaults = envelope_defaults(s);
let id = s.edit("Make Envelope", |d, sel| wrap(d, sel, &roots, |id, content| envelope(id, content, kind, defaults)))?;
Ok(json!({ "id": id.0 }))
@@ -1005,13 +1004,15 @@ fn env_make_top(s: &mut Session, p: &Value) -> Result<Value> {
Ok(json!({ "id": id.0 }))
}
/// The kind `f` makes of each selected envelope (from its content and its current kind).
fn reshaped(s: &Session, f: impl Fn(&[Arc<Node>], &EnvelopeKind) -> Result<EnvelopeKind>) -> Result<Vec<(NodeId, EnvelopeKind)>> {
/// The kind `f` makes of each selected envelope (from the envelope and its current kind).
fn reshaped(s: &Session, f: impl Fn(&Node, &EnvelopeKind) -> Result<EnvelopeKind>) -> Result<Vec<(NodeId, EnvelopeKind)>> {
let doc = &s.doc()?.doc;
let mut out = vec![];
for e in selected_of(s, is_envelope) {
if let Some(NodeKind::Envelope { content, kind, .. }) = doc.node(e).map(|n| &n.kind) {
out.push((e, f(content, kind)?));
if let Some(n) = doc.node(e)
&& let NodeKind::Envelope { kind, .. } = &n.kind
{
out.push((e, f(n, kind)?));
}
}
Ok(out)
@@ -1039,13 +1040,14 @@ fn env_reset_warp(s: &mut Session, p: &Value) -> Result<Value> {
fn env_reset_mesh(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "object.envelope.resetWithMesh";
let maintain = bool_or(p, "maintainShape", true);
let kinds = reshaped(s, |content, kind| {
let kinds = reshaped(s, |env, kind| {
let own = match kind {
EnvelopeKind::Mesh { rows, cols, .. } => (*rows, *cols),
_ => (4, 4),
};
let (rows, cols) = rows_cols(p, C, own)?;
mesh_kind(content, maintain.then_some(kind), rows, cols).ok_or_else(|| bad(C, "the envelope's content has no bounds"))
let map = EnvelopeMap::of(env).ok_or_else(|| bad(C, "the envelope's content has no bounds"))?;
Ok(mesh_kind(&map, maintain, rows, cols))
})?;
set_kinds(s, "Reset with Mesh", kinds)
}
@@ -1096,9 +1098,9 @@ fn env_info(s: &mut Session, _: &Value) -> Result<Value> {
Ok(v)
}
/// What Release gives back as an envelope's shape: a top object's path, or the surface of a warp
/// or mesh envelope as a gradient mesh, painted grey (id 0).
fn envelope_shape(content: &[Arc<Node>], kind: &EnvelopeKind) -> Option<Node> {
/// What Release gives back as the shape of envelope `env` (of `kind`): a top object's path, or the
/// surface of a warp or mesh envelope as a gradient mesh, painted grey (id 0).
fn envelope_shape(env: &Node, kind: &EnvelopeKind) -> Option<Node> {
let grey = Color::gray(0.25);
let (rows, cols) = match kind {
EnvelopeKind::TopObject { path } => {
@@ -1107,7 +1109,7 @@ fn envelope_shape(content: &[Arc<Node>], kind: &EnvelopeKind) -> Option<Node> {
EnvelopeKind::Mesh { rows, cols, .. } => (*rows, *cols),
EnvelopeKind::Warp { .. } => (4, 4),
};
live::envelope_surface(content, kind, rows, cols, grey).map(|m| Node::new(NodeId(0), NodeKind::Mesh(m)))
EnvelopeMap::of(env).map(|map| Node::new(NodeId(0), NodeKind::Mesh(map.surface_mesh(rows, cols, grey))))
}
/// Released content keeps envelope `env`'s opacity, blend mode, isolation, knockout and opacity
@@ -1143,7 +1145,7 @@ fn env_release(s: &mut Session, _: &Value) -> Result<Value> {
let Some(n) = d.node(*e).cloned() else { continue };
let NodeKind::Envelope { content, kind, .. } = &n.kind else { continue };
let mut nodes = carry_transparency(d, &n, content.iter().map(|c| (**c).clone()).collect());
if let Some(mut shape) = envelope_shape(content, kind) {
if let Some(mut shape) = envelope_shape(&n, kind) {
shape.id = d.alloc_id();
nodes.push(shape);
}
@@ -1195,7 +1197,7 @@ fn env_options(s: &mut Session, p: &Value) -> Result<Value> {
/// transparency and opacity mask; the generated pieces get ids of their own.
fn expand_envelope(d: &mut Document, id: NodeId) -> Result<()> {
let n = d.node(id).cloned().ok_or(EngineError::NoNode(id))?;
let mut g = live::expanded_group(&n, vectorcraft_render::effects::text_outliner());
let mut g = vectorcraft_render::effects::expanded_live_group(Some(d), &n);
fix_ids(d, &mut g);
// Outlined type repeats its object's id on its pieces.
vectorcraft_render::effects::fresh_ids(d, &mut g, &mut Default::default());
+2
View File
@@ -1210,6 +1210,8 @@ mod tests_emptytype;
#[cfg(test)]
mod tests_envelope;
#[cfg(test)]
mod tests_envelope_distort;
#[cfg(test)]
mod tests_expand;
#[cfg(test)]
mod tests_eyedropper;
+1 -1
View File
@@ -96,7 +96,7 @@ fn reset_with_warp_and_mesh_switch_the_kind_and_keep_the_content() {
assert!(s.execute("object.envelope.resetWithWarp", &json!({"style": "nope"})).is_err());
// Back to a mesh that keeps the arch's shape: the top middle point is where the warp put it.
s.execute("object.envelope.resetWithWarp", &json!({"style": "arch", "bend": 50, "horizontal": true})).unwrap();
let top = live::envelope_surface(&node(&s, e).children().unwrap().clone(), &kind(&s, e), 2, 2, vectorcraft_color::Color::BLACK).unwrap();
let top = live::EnvelopeMap::of(&node(&s, e)).unwrap().surface_mesh(2, 2, vectorcraft_color::Color::BLACK);
s.execute("object.envelope.resetWithMesh", &json!({"rows": 2, "cols": 2})).unwrap();
let EnvelopeKind::Mesh { rows: 2, cols: 2, points } = kind(&s, e) else { panic!("{:?}", kind(&s, e)) };
assert!(points[1].distance(top.points[1].p) < 1e-9 && points[1].y < 99.0, "{:?}", points[1]);
+298
View File
@@ -0,0 +1,298 @@
//! What envelopes distort: the envelope's own frame under transforms, images (a raster mesh warp),
//! symbols, linear gradients, pattern fills, strokes and effects (Distort Appearance).
use std::sync::Arc;
use serde_json::{Value, json};
use vectorcraft_color::{Color, Gradient, GradientPaint, Paint};
use vectorcraft_doc::live::EnvelopeOptions;
use vectorcraft_doc::{AppearanceItem, Node, NodeKind, PatternDef, Symbol};
use vectorcraft_geom::{Affine, Point, Rect, shapes};
use vectorcraft_render::effects;
use super::*;
fn session() -> Session {
let mut s = Session::new();
s.execute("file.new", &json!({"width": 800, "height": 600})).unwrap();
s
}
fn id_of(v: &Value) -> NodeId {
NodeId(v["id"].as_u64().unwrap())
}
fn node(s: &Session, id: NodeId) -> Node {
s.doc().unwrap().doc.node(id).unwrap().clone()
}
fn sel(s: &mut Session, ids: &[NodeId]) {
s.execute("select.set", &json!({"ids": ids.iter().map(|i| i.0).collect::<Vec<_>>()})).unwrap();
}
/// A 200×100 rectangle at (100, 100), filled red, no stroke.
fn rect(s: &mut Session) -> NodeId {
let a = id_of(&s.execute("shape.rectangle", &json!({"x": 100, "y": 100, "width": 200, "height": 100})).unwrap());
s.edit("red", |d, _| {
d.node_mut(a).unwrap().appearance = vectorcraft_doc::Appearance::basic(Paint::solid(Color::rgb(1.0, 0.0, 0.0)), Paint::None, 0.0);
Ok(())
})
.unwrap();
a
}
/// Envelope the selection with an arch warp (`bend` %) and `options` → the envelope.
fn arch(s: &mut Session, bend: f64, options: Value) -> NodeId {
let e = id_of(&s.execute("object.envelope.makeWithWarp", &json!({"style": "arch", "bend": bend})).unwrap());
s.execute("object.envelope.options", &options).unwrap();
e
}
/// The envelope evaluated as the canvas and the exporters do.
fn evaluated(s: &Session, e: NodeId) -> Vec<Node> {
effects::expand_live(Some(&s.doc().unwrap().doc), &node(s, e))
}
fn anchors(nodes: &[Node]) -> Vec<Point> {
let mut v = vec![];
for n in nodes {
n.walk(&mut |c| v.extend(c.path_data().into_iter().flat_map(|p| p.anchors().map(|(_, _, a)| a.p))));
}
v
}
fn frame(s: &Session, e: NodeId) -> Affine {
match node(s, e).kind {
NodeKind::Envelope { frame, .. } => frame,
k => panic!("{k:?}"),
}
}
/// Regression: rotating a warp envelope rotated its content and re-warped it along the page axes.
#[test]
fn a_rotated_warp_envelope_turns_with_its_warp() {
let mut s = session();
let a = rect(&mut s);
sel(&mut s, &[a]);
let e = arch(&mut s, 50.0, json!({}));
let before = anchors(&evaluated(&s, e));
let c = Point::new(200.0, 150.0);
s.execute("object.rotate", &json!({"angle": 90, "origin": [c.x, c.y]})).unwrap();
let after = anchors(&evaluated(&s, e));
let turn = Affine::rotate_about(-std::f64::consts::FRAC_PI_2, c);
assert_eq!(before.len(), after.len());
for (p, q) in before.iter().zip(&after) {
assert!((turn * *p).distance(*q) < 1e-6, "{:?} vs {q:?}", turn * *p);
}
assert_ne!(frame(&s, e), Affine::IDENTITY);
// Turning back squares the frame with the page again; a plain move or scale never tilts it.
s.execute("object.rotate", &json!({"angle": -90, "origin": [c.x, c.y]})).unwrap();
assert_eq!(frame(&s, e), Affine::IDENTITY);
s.execute("object.scale", &json!({"sx": 150, "sy": 50})).unwrap();
assert_eq!(frame(&s, e), Affine::IDENTITY);
// A reflection keeps a frame: the arch flips with the art.
s.execute("object.reflect", &json!({"axis": "horizontal"})).unwrap();
let flipped = anchors(&evaluated(&s, e));
let b = node(&s, e).geometric_bounds().unwrap();
assert!(flipped.iter().any(|p| p.y > b.y1 - 1.0) && frame(&s, e) != Affine::IDENTITY);
}
#[test]
fn the_frame_round_trips_natively() {
let mut s = session();
let a = rect(&mut s);
sel(&mut s, &[a]);
let e = arch(&mut s, 50.0, json!({}));
s.execute("object.rotate", &json!({"angle": 30})).unwrap();
let d = &s.doc().unwrap().doc;
let back = vectorcraft_format::load(&vectorcraft_format::save(d, false)).unwrap();
assert_eq!(back.node(e), d.node(e));
let mut old = serde_json::to_value(node(&s, e)).unwrap();
old["kind"].as_object_mut().unwrap().remove("frame");
let n: Node = serde_json::from_value(old).unwrap();
assert!(matches!(n.kind, NodeKind::Envelope { frame, .. } if frame == Affine::IDENTITY), "older envelopes stand square to the page");
}
/// A selected embedded 40×40 pt red image at (100, 100).
fn red_image(s: &mut Session) -> NodeId {
let img = image::RgbaImage::from_pixel(4, 4, image::Rgba([255, 0, 0, 255]));
let mut png = vec![];
img.write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png).unwrap();
s.edit("Place", |d, sel| {
d.images.insert("pic".into(), vectorcraft_doc::ImageBlob::png(png));
let id = d.alloc_id();
let xf = Affine::translate((100.0, 100.0)) * Affine::scale(10.0);
let im = vectorcraft_doc::ImageObject { key: "pic".into(), width: 4, height: 4, xf, link: None, placement: Default::default() };
d.insert(d.default_layer(), 0, Node::new(id, NodeKind::Image(im)))?;
sel.set([id]);
Ok(id)
})
.unwrap()
}
#[test]
fn images_bend_as_a_raster_mesh_warp() {
let mut s = session();
let im = red_image(&mut s);
sel(&mut s, &[im]);
let e = arch(&mut s, 80.0, json!({}));
let out = evaluated(&s, e);
let mut pieces = 0;
out[0].walk(&mut |c| pieces += matches!(c.kind, NodeKind::Image(_)) as usize);
assert!(pieces >= 32, "{pieces} pieces");
// The arch lifts the image's middle above its top edge, not its corners.
let img = vectorcraft_render::Renderer::new().render_region(&s.doc().unwrap().doc, Rect::new(0.0, 0.0, 200.0, 200.0), 1.0, true);
let b = node(&s, e).geometric_bounds().unwrap();
assert!(b.y0 < 95.0, "{b:?}");
let mid = img.pixel(120, 97);
assert!(mid[0] > 200 && mid[1] < 60, "the middle rose: {mid:?}");
assert!(img.pixel(101, 95)[1] > 200, "the corner stayed: {:?}", img.pixel(101, 95));
// The pieces export as clipped images, bent in the SVG too.
let svg = s.execute("document.export", &json!({"format": "svg"})).unwrap()["dataBase64"].as_str().unwrap().to_string();
let svg = String::from_utf8(vectorcraft_format::base64_decode(&svg).unwrap()).unwrap();
assert!(svg.matches("<use").count() >= 32 && svg.contains("clip"), "{}", &svg[..svg.len().min(400)]);
assert_eq!(svg.matches("<image").count(), 1, "the pieces share the image's pixels");
}
#[test]
fn symbols_bend_with_the_envelope() {
let mut s = session();
let inst = s
.edit("symbol", |d, sel| {
// Symbol art is normalised to ±10 (its instance's transform sizes it).
let art = Node::path(
NodeId(0),
shapes::rectangle(Rect::new(-10.0, -10.0, 10.0, 10.0)),
vectorcraft_doc::Appearance::basic(Paint::solid(Color::BLACK), Paint::None, 0.0),
);
d.symbols.push(Symbol { name: "Box".into(), art: Arc::new(art) });
let id = d.alloc_id();
let xf = Affine::translate((200.0, 150.0)) * Affine::scale_non_uniform(10.0, 5.0);
d.insert(d.default_layer(), 0, Node::new(id, NodeKind::SymbolInstance { symbol: "Box".into(), xf }))?;
sel.set([id]);
Ok(id)
})
.unwrap();
sel(&mut s, &[inst]);
let e = arch(&mut s, 60.0, json!({}));
let pts = anchors(&evaluated(&s, e));
assert!(pts.iter().any(|p| p.y < 90.0), "the symbol's art rose in the middle");
// Editing the symbol redraws the envelope (the renderer's cache follows the symbol's art).
let mut r = vectorcraft_render::Renderer::new();
let region = Rect::new(0.0, 0.0, 400.0, 300.0);
assert!(r.render_region(&s.doc().unwrap().doc, region, 1.0, true).pixel(200, 150)[1] < 60);
s.edit("edit symbol", |d, _| {
let art = Node::path(NodeId(0), shapes::rectangle(Rect::new(-10.0, -10.0, -8.0, -8.0)), Default::default());
d.symbols[0].art = Arc::new(art);
Ok(())
})
.unwrap();
assert!(r.render_region(&s.doc().unwrap().doc, region, 1.0, true).pixel(200, 150)[1] > 200);
}
fn strokes(nodes: &[Node]) -> usize {
let mut n = 0;
for c in nodes {
c.walk(&mut |c| n += c.appearance.items.iter().filter(|i| matches!(i, AppearanceItem::Stroke(_))).count());
}
n
}
#[test]
fn distort_appearance_bends_strokes_and_effects_with_the_art() {
let mut s = session();
let a = rect(&mut s);
s.execute("paint.setStroke", &json!({"ids": [a.0], "color": "#000000"})).unwrap();
s.execute("stroke.set", &json!({"ids": [a.0], "weight": 8})).unwrap();
sel(&mut s, &[a]);
let e = arch(&mut s, 60.0, json!({"distortAppearance": true}));
let out = evaluated(&s, e);
assert_eq!(strokes(&out), 0, "the stroke became its bent outline");
// Off: the stroke paints along the bent path at its own weight.
s.execute("object.envelope.options", &json!({"distortAppearance": false})).unwrap();
assert_eq!(strokes(&evaluated(&s, e)), 1);
// Geometry effects bake into the art first too.
s.execute("object.envelope.editContents", &json!({"editing": true})).unwrap();
s.execute("effect.apply", &json!({"effect": "distort.zigZag", "params": {"size": 5, "ridges": 10}})).unwrap();
s.execute("object.envelope.editContents", &json!({"editing": false})).unwrap();
s.execute("object.envelope.options", &json!({"distortAppearance": true})).unwrap();
let out = evaluated(&s, e);
let mut effects_left = 0;
out[0].walk(&mut |c| effects_left += c.appearance.effects.len());
assert_eq!(effects_left, 0);
assert!(anchors(&out).len() > anchors(&[node(&s, a)]).len());
}
fn gradient_geom(nodes: &[Node]) -> Option<vectorcraft_color::GradientGeom> {
let mut g = None;
for n in nodes {
n.walk(&mut |c| {
if let Paint::Gradient(gp) = c.appearance.fill_paint() {
g = g.or(gp.geom);
}
});
}
g
}
#[test]
fn linear_gradients_bend_when_asked() {
let mut s = session();
let a = rect(&mut s);
s.edit("gradient", |d, _| {
d.node_mut(a).unwrap().appearance.set_fill(Paint::Gradient(Box::new(GradientPaint::new(Gradient::default()))));
Ok(())
})
.unwrap();
sel(&mut s, &[a]);
let e = arch(&mut s, 60.0, json!({"distortAppearance": true}));
assert!(gradient_geom(&evaluated(&s, e)).is_none(), "off: the gradient fits the bent shape");
s.execute("object.envelope.options", &json!({"distortLinearGradients": true})).unwrap();
let g = gradient_geom(&evaluated(&s, e)).expect("on: the gradient is placed and follows the warp");
// Arch lifts the middle: the vector (left to right through the middle) moved up.
assert!(g.start.midpoint(g.end).y < 150.0 - 1.0, "{g:?}");
// It needs Distort Appearance.
s.execute("object.envelope.options", &json!({"distortAppearance": false})).unwrap();
assert!(gradient_geom(&evaluated(&s, e)).is_none());
}
#[test]
fn pattern_fills_bend_when_asked() {
let mut s = session();
let a = rect(&mut s);
s.edit("pattern", |d, _| {
let dot = Node::path(
NodeId(0),
shapes::rectangle(Rect::new(0.0, 0.0, 5.0, 5.0)),
vectorcraft_doc::Appearance::basic(Paint::solid(Color::BLACK), Paint::None, 0.0),
);
let mut def = PatternDef::new("Dots", vec![Arc::new(dot)]);
def.tile = Rect::new(0.0, 0.0, 20.0, 20.0);
d.patterns.push(def);
d.node_mut(a).unwrap().appearance.set_fill(vectorcraft_doc::pattern::pattern_paint("Dots"));
Ok(())
})
.unwrap();
sel(&mut s, &[a]);
let e = arch(&mut s, 60.0, json!({"distortAppearance": true}));
let clip_groups = |out: &[Node]| {
let mut n = 0;
out[0].walk(&mut |c| n += matches!(c.kind, NodeKind::Group { clip: true, .. }) as usize);
n
};
assert_eq!(clip_groups(&evaluated(&s, e)), 0, "off: the pattern fills the bent shape unbent");
s.execute("object.envelope.options", &json!({"distortPatternFills": true})).unwrap();
let out = evaluated(&s, e);
assert_eq!(clip_groups(&out), 1);
assert!(anchors(&out).len() > 50 * 4, "the tiles are art");
assert!(!matches!(out[0].appearance.fill_paint(), Paint::Pattern { .. }));
}
#[test]
fn new_envelopes_distort_appearance_and_old_ones_keep_their_look() {
let mut s = session();
let a = rect(&mut s);
sel(&mut s, &[a]);
let e = arch(&mut s, 50.0, json!({}));
assert!(matches!(node(&s, e).kind, NodeKind::Envelope { options, .. } if options == EnvelopeOptions::NEW));
}
+4 -2
View File
@@ -399,8 +399,10 @@ fn envelope_release_and_expand() {
s.execute("object.envelope.expand", &json!({})).unwrap();
let g = node(&s, e);
assert!(matches!(g.kind, NodeKind::Group { .. }));
let p = g.children().unwrap()[0].path_data().unwrap().anchor_count();
assert!(p > 4);
// (Distort Appearance, on for new envelopes, expands the stroke too: the fill comes first.)
let mut fill = None;
g.walk(&mut |c| fill = fill.or_else(|| c.path_data().map(|p| p.anchor_count())));
assert!(fill.unwrap() > 4);
}
#[test]