121 lines
6.1 KiB
Rust
121 lines
6.1 KiB
Rust
//! Radial gradients' extent and focal point (M3.98): `paint.setGradientGeom {aspect, focal}`,
|
|
//! focal points through transforms, params and the native format, and the radial annotator.
|
|
|
|
use serde_json::json;
|
|
use vectorcraft_color::{GradientGeom, Paint};
|
|
use vectorcraft_geom::Point;
|
|
use vectorcraft_tools::{Mods, PointerEvent, PointerKind};
|
|
|
|
use super::*;
|
|
|
|
/// A 200 pt square at (100, 100) with a radial fill centred on it, radius 100.
|
|
fn radial() -> (Session, NodeId) {
|
|
let mut s = Session::new();
|
|
s.execute("file.new", &json!({"width": 800, "height": 600})).unwrap();
|
|
let r = s.execute("shape.rectangle", &json!({"x": 100, "y": 100, "width": 200, "height": 200})).unwrap();
|
|
let id = NodeId(r["id"].as_u64().unwrap());
|
|
s.execute("paint.setFill", &json!({"gradient": {"kind": "radial", "start": [200, 200], "end": [300, 200]}})).unwrap();
|
|
(s, id)
|
|
}
|
|
|
|
fn geom(s: &Session, id: NodeId) -> GradientGeom {
|
|
match s.doc().unwrap().doc.node(id).unwrap().appearance.fill_paint() {
|
|
Paint::Gradient(g) => g.geom.expect("placed"),
|
|
p => panic!("not a gradient: {p:?}"),
|
|
}
|
|
}
|
|
|
|
fn close(a: Point, b: Point) -> bool {
|
|
a.distance(b) < 1e-6
|
|
}
|
|
|
|
fn undo_len(s: &Session) -> usize {
|
|
s.doc().unwrap().history.undo.len()
|
|
}
|
|
|
|
#[test]
|
|
fn set_gradient_geom_sets_aspect_and_focal_in_one_step() {
|
|
let (mut s, id) = radial();
|
|
let n = undo_len(&s);
|
|
s.execute("paint.setGradientGeom", &json!({"focal": [160, 210], "aspect": 50})).unwrap();
|
|
let g = geom(&s, id);
|
|
assert_eq!((g.start, g.end, g.aspect, g.focal), (Point::new(200.0, 200.0), Point::new(300.0, 200.0), 0.5, Some(Point::new(160.0, 210.0))));
|
|
assert_eq!(undo_len(&s), n + 1);
|
|
// Out of the extent ellipse it is pulled in; null centres it.
|
|
s.execute("paint.setGradientGeom", &json!({"focal": [200, 400]})).unwrap();
|
|
assert!(close(geom(&s, id).focal.unwrap(), Point::new(200.0, 249.5)), "{:?}", geom(&s, id));
|
|
s.execute("paint.setGradientGeom", &json!({"focal": null})).unwrap();
|
|
assert_eq!(geom(&s, id).focal, None);
|
|
// One end without the other is an error; linear gradients have no focal point.
|
|
assert!(s.execute("paint.setGradientGeom", &json!({"end": [0, 0]})).is_err());
|
|
s.execute("paint.editGradient", &json!({"kind": "linear"})).unwrap();
|
|
s.execute("paint.setGradientGeom", &json!({"start": [100, 200], "end": [300, 200], "focal": [150, 200]})).unwrap();
|
|
assert_eq!(geom(&s, id).focal, None);
|
|
}
|
|
|
|
#[test]
|
|
fn the_focal_point_keeps_its_place_as_the_gradient_moves_and_reshapes() {
|
|
let (mut s, id) = radial();
|
|
s.execute("paint.setGradientGeom", &json!({"focal": [160, 210]})).unwrap();
|
|
// A new vector: moved 10 right and turned to point down.
|
|
s.execute("paint.setGradientGeom", &json!({"start": [210, 200], "end": [210, 300]})).unwrap();
|
|
assert!(close(geom(&s, id).focal.unwrap(), Point::new(200.0, 160.0)), "{:?}", geom(&s, id));
|
|
// The aspect ratio (Gradient panel) squashes it with the ellipse.
|
|
s.execute("paint.editGradient", &json!({"aspect": 50})).unwrap();
|
|
assert!(close(geom(&s, id).focal.unwrap(), Point::new(205.0, 160.0)), "{:?}", geom(&s, id));
|
|
// Transforming the object maps it.
|
|
s.execute("object.move", &json!({"dx": 5, "dy": 7})).unwrap();
|
|
assert!(close(geom(&s, id).focal.unwrap(), Point::new(210.0, 167.0)), "{:?}", geom(&s, id));
|
|
s.execute("object.transform", &json!({"matrix": [0, 1, -1, 0, 0, 0]})).unwrap();
|
|
assert!(close(geom(&s, id).focal.unwrap(), Point::new(-167.0, 210.0)), "{:?}", geom(&s, id));
|
|
}
|
|
|
|
#[test]
|
|
fn the_focal_point_round_trips_through_params_and_the_native_format() {
|
|
let (mut s, id) = radial();
|
|
s.execute("paint.setGradientGeom", &json!({"focal": [170, 190], "aspect": 80})).unwrap();
|
|
let want = geom(&s, id);
|
|
// Gradient params (what swatches and agents read) carry it, and give it back.
|
|
let Paint::Gradient(g) = s.doc().unwrap().doc.node(id).unwrap().appearance.fill_paint() else { panic!("a gradient fill") };
|
|
let params = vectorcraft_tools::params::gradient_params(&g);
|
|
assert_eq!(params["focal"], json!([170.0, 190.0]));
|
|
s.execute("paint.setFill", &json!({"gradient": {"kind": "linear"}})).unwrap();
|
|
s.execute("paint.setFill", &json!({ "gradient": params })).unwrap();
|
|
assert_eq!(geom(&s, id), want);
|
|
let back = vectorcraft_format::load(&vectorcraft_format::save(&s.doc().unwrap().doc, false)).unwrap();
|
|
let Paint::Gradient(g) = back.node(id).unwrap().appearance.fill_paint() else { panic!("gradient lost") };
|
|
assert_eq!(g.geom, Some(want));
|
|
}
|
|
|
|
#[test]
|
|
fn the_radial_annotator_drags_the_focal_dot_the_aspect_handle_and_the_ring() {
|
|
let (mut s, id) = radial();
|
|
let v = ViewInfo::default();
|
|
s.select_tool("gradient", v).unwrap();
|
|
let drag = |s: &mut Session, pts: &[(f64, f64)]| {
|
|
let n = undo_len(s);
|
|
for (i, (x, y)) in pts.iter().enumerate() {
|
|
let kind = [PointerKind::Down, PointerKind::Drag][i.min(1)];
|
|
s.pointer(&PointerEvent::new(kind, *x, *y).with_mods(Mods::default()), v).unwrap();
|
|
}
|
|
let (x, y) = pts[pts.len() - 1];
|
|
s.pointer(&PointerEvent::new(PointerKind::Up, x, y), v).unwrap();
|
|
assert_eq!(undo_len(s), n + 1, "one undo step");
|
|
};
|
|
// The focal dot sits on the centre: dragging it moves only the focal point.
|
|
drag(&mut s, &[(200.0, 200.0), (180.0, 195.0), (170.0, 190.0)]);
|
|
let g = geom(&s, id);
|
|
assert_eq!((g.start, g.focal), (Point::new(200.0, 200.0), Some(Point::new(170.0, 190.0))));
|
|
// Back onto the centre, it centres.
|
|
drag(&mut s, &[(170.0, 190.0), (190.0, 195.0), (200.5, 200.5)]);
|
|
assert_eq!(geom(&s, id).focal, None);
|
|
// The handle across the bar (100 pt above the centre) sets the aspect ratio.
|
|
drag(&mut s, &[(200.0, 100.0), (200.0, 120.0), (200.0, 150.0)]);
|
|
assert!((geom(&s, id).aspect - 0.5).abs() < 1e-9, "{:?}", geom(&s, id));
|
|
// The ellipse elsewhere rotates it, keeping its radius: from its bottom round to its left
|
|
// turns the vector a quarter clockwise, to point down.
|
|
drag(&mut s, &[(200.0, 250.0), (170.0, 240.0), (150.0, 200.0)]);
|
|
let g = geom(&s, id);
|
|
assert!(close(g.end, Point::new(200.0, 300.0)) && (g.aspect - 0.5).abs() < 1e-9, "{g:?}");
|
|
}
|