From c43e4a388edc04eb5e737878acff1bae70337166 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sat, 7 Nov 2026 23:24:09 -0800 Subject: [PATCH] Tests: placed gradient tests check the whole gradient after Transform and Free Distort The baked Transform and Free Distort tests now compare each placed gradient's start, end, aspect and focal point with the expected values within a tolerance, and the Transform test adds a turn with a scale. --- crates/effects/src/bake/tests.rs | 97 ++++++++++++++++++++++---------- 1 file changed, 66 insertions(+), 31 deletions(-) diff --git a/crates/effects/src/bake/tests.rs b/crates/effects/src/bake/tests.rs index 0484f9cbe..0ff1cdac0 100644 --- a/crates/effects/src/bake/tests.rs +++ b/crates/effects/src/bake/tests.rs @@ -17,6 +17,20 @@ fn fx(id: &str, params: serde_json::Value) -> Effect { Effect { id: id.into(), params, visible: true } } +/// Asserts that placed gradient `got` has `want`'s start, end, aspect and focal point, within 1e-6. +fn assert_geom_near(got: vectorcraft_color::GradientGeom, want: vectorcraft_color::GradientGeom, what: &str) { + let near = |a: Point, b: Point| a.distance(b) < 1e-6; + let focal = match (got.focal, want.focal) { + (None, None) => true, + (Some(a), Some(b)) => near(a, b), + _ => false, + }; + assert!( + near(got.start, want.start) && near(got.end, want.end) && (got.aspect - want.aspect).abs() < 1e-6 && focal, + "{what}: got {got:?}, want {want:?}" + ); +} + fn doc_with(n: Node) -> Document { let mut d = Document::new(400.0, 400.0); let l = d.layers[0].id; @@ -52,30 +66,43 @@ fn a_baked_transform_moves_a_placed_gradient_with_the_path() { let ramp = GradientPaint::new(Gradient::new(GradientKind::Linear, stops)); let geom = GradientGeom { start: Point::new(0.0, 50.0), end: Point::new(100.0, 50.0), aspect: 1.0, focal: None }; let placed = Paint::Gradient(Box::new(GradientPaint { geom: Some(geom), ..ramp })); - let start = |n: &Node| match n.appearance.fill_paint() { - Paint::Gradient(g) => g.geom.unwrap().start.x, + let placed_geom = |n: &Node| match n.appearance.fill_paint() { + Paint::Gradient(g) => g.geom.unwrap(), p => panic!("a gradient: {p:?}"), }; + // A move 30 pt right, and a turn of 90° with the width halved about the center (50, 50): the + // square becomes 100 pt wide and 50 pt tall, and the gradient runs from the bottom up. + let moves = [ + (json!({"moveH": 30.0}), Point::new(30.0, 0.0), GradientGeom { start: Point::new(30.0, 50.0), end: Point::new(130.0, 50.0), ..geom }), + ( + json!({"rotate": 90.0, "scaleH": 50.0}), + Point::new(0.0, 25.0), + GradientGeom { start: Point::new(50.0, 75.0), end: Point::new(50.0, 25.0), ..geom }, + ), + ]; // Alone, with a stroke whose own effect makes one path for each fill and stroke, and as the // fill's own effect. - for (split, own) in [(false, false), (true, false), (true, true)] { - let mut ap = Appearance::basic(placed.clone(), Paint::solid(Color::BLACK), if split { 1.0 } else { 0.0 }); - if split && let AppearanceItem::Stroke(s) = &mut ap.items[1] { - s.effects.push(fx("path.offsetPath", json!({"offset": 5.0}))); + for (params, corner, want) in moves { + for (split, own) in [(false, false), (true, false), (true, true)] { + let mut ap = Appearance::basic(placed.clone(), Paint::solid(Color::BLACK), if split { 1.0 } else { 0.0 }); + if split && let AppearanceItem::Stroke(s) = &mut ap.items[1] { + s.effects.push(fx("path.offsetPath", json!({"offset": 5.0}))); + } + let transform = fx("distort.transform", params.clone()); + if own { + ap.items[0].effects_mut().push(transform.clone()); + } + let mut n = Node::path(NodeId(50), shapes::rectangle(Rect::new(0.0, 0.0, 100.0, 100.0)), ap); + if !own { + n.appearance.effects.push(transform); + } + let d = bake_document(&doc_with(n)).unwrap(); + let m = d.node(NodeId(50)).unwrap(); + let fill: &Node = if split { &m.children().unwrap()[0] } else { m }; + let b = fill.geometric_bounds().unwrap(); + assert!(b.origin().distance(corner) < 1e-6, "{params}, split {split}, own {own}: {b:?}"); + assert_geom_near(placed_geom(fill), want, &format!("{params}, split {split}, own {own}")); } - let transform = fx("distort.transform", json!({"moveH": 30.0})); - if own { - ap.items[0].effects_mut().push(transform.clone()); - } - let mut n = Node::path(NodeId(50), shapes::rectangle(Rect::new(0.0, 0.0, 100.0, 100.0)), ap); - if !own { - n.appearance.effects.push(transform); - } - let d = bake_document(&doc_with(n)).unwrap(); - let m = d.node(NodeId(50)).unwrap(); - let fill: &Node = if split { &m.children().unwrap()[0] } else { m }; - assert!((fill.geometric_bounds().unwrap().x0 - 30.0).abs() < 1e-6); - assert!((start(fill) - 30.0).abs() < 1e-6, "split {split}, own {own}: the gradient starts at {}", start(fill)); } } @@ -98,17 +125,21 @@ fn a_strokes_own_transform_and_free_distort_move_its_placed_gradient() { stroke.effects_mut().push(e.clone()); let n = Node::path(NodeId(50), shapes::rectangle(Rect::new(0.0, 0.0, 100.0, 100.0)), ap); let d = bake_document(&doc_with(n)).unwrap(); - let mut starts = vec![]; + let mut geoms = vec![]; d.node(NodeId(50)).unwrap().walk(&mut |c: &Node| { for item in &c.appearance.items { if let AppearanceItem::Stroke(s) = item && let Paint::Gradient(g) = &s.paint { - starts.push(g.geom.unwrap().start.x); + geoms.push(g.geom.unwrap()); } } }); - assert!(!starts.is_empty() && starts.iter().all(|s| (s - moved).abs() < 1e-6), "{}: {starts:?}", e.id); + assert!(!geoms.is_empty(), "{}: the stroke keeps its gradient", e.id); + let want = GradientGeom { start: Point::new(moved, 50.0), end: Point::new(100.0 + moved, 50.0), ..geom }; + for g in geoms { + assert_geom_near(g, want, &e.id); + } } } @@ -127,13 +158,18 @@ fn a_fills_gradient_follows_the_effects_around_its_transform_with_options() { *ap.items[0].effects_mut() = fill_fx.clone(); let n = Node::path(NodeId(50), shapes::rectangle(Rect::new(0.0, 0.0, 100.0, 100.0)), ap); let d = bake_document(&doc_with(n)).unwrap(); - let mut starts = vec![]; + let mut found = vec![]; d.node(NodeId(50)).unwrap().walk(&mut |c: &Node| { if let Paint::Gradient(g) = c.appearance.fill_paint() { - starts.push((c.geometric_bounds().unwrap().x0, g.geom.unwrap().start.x)); + found.push((c.geometric_bounds().unwrap(), g.geom.unwrap())); } }); - assert!(!starts.is_empty() && starts.iter().all(|(x0, s)| (x0 - 30.0).abs() < 1e-6 && (s - 30.0).abs() < 1e-6), "{fill_fx:?}: {starts:?}"); + assert!(!found.is_empty(), "{fill_fx:?}: the fill keeps its gradient"); + let want = GradientGeom { start: Point::new(30.0, 50.0), end: Point::new(130.0, 50.0), ..geom }; + for (b, g) in found { + assert!(b.origin().distance(Point::new(30.0, 0.0)) < 1e-6, "{fill_fx:?}: {b:?}"); + assert_geom_near(g, want, &format!("{fill_fx:?}")); + } } } @@ -143,7 +179,7 @@ fn baked_free_distort_moves_placed_gradients_from_the_bounds_to_the_distorted_bo 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 = |kind| GradientPaint::new(Gradient::new(kind, stops.clone())); let linear = GradientGeom { start: Point::new(20.0, 50.0), end: Point::new(80.0, 50.0), aspect: 1.0, focal: None }; - let radial = GradientGeom { start: Point::new(40.0, 30.0), end: Point::new(60.0, 30.0), aspect: 0.5, focal: None }; + let radial = GradientGeom { start: Point::new(40.0, 30.0), end: Point::new(60.0, 30.0), aspect: 0.5, focal: Some(Point::new(45.0, 32.0)) }; let placed_linear = Paint::Gradient(Box::new(GradientPaint { geom: Some(linear), ..ramp(GradientKind::Linear) })); let placed_radial = Paint::Gradient(Box::new(GradientPaint { geom: Some(radial), ..ramp(GradientKind::Radial) })); let fitted = Paint::Gradient(Box::new(ramp(GradientKind::Linear))); @@ -165,12 +201,11 @@ fn baked_free_distort_moves_placed_gradients_from_the_bounds_to_the_distorted_bo assert!((b.x1 - 150.0).abs() < 1e-6, "{b:?}"); let geom = g.geom.unwrap(); assert!(near(geom.start, Point::new(30.0, 50.0)) && near(geom.end, Point::new(120.0, 50.0)), "{geom:?}"); - // A radial gradient's center and ellipse take the same stretch. + // A radial gradient's center, focal point and ellipse take the same stretch: the radius along + // the vector grows from 20 to 30 pt and the 10 pt radius across it stays. let (_, g) = baked(&placed_radial, distort()); - let Paint::Gradient(mut expected) = placed_radial.clone() else { panic!("a gradient") }; - expected.transform(vectorcraft_geom::Affine::new([1.5, 0.0, 0.0, 1.0, 0.0, 0.0])); - assert_eq!(g.geom, expected.geom); - assert!(near(g.geom.unwrap().start, Point::new(60.0, 30.0)), "{:?}", g.geom); + let want = GradientGeom { start: Point::new(60.0, 30.0), end: Point::new(90.0, 30.0), aspect: 1.0 / 3.0, focal: Some(Point::new(67.5, 32.0)) }; + assert_geom_near(g.geom.unwrap(), want, "radial"); // A fitted gradient stays fitted, and fits the distorted outline. assert_eq!(baked(&fitted, distort()).1.geom, None); }