vectorcraft/crates/engine/src/tests_focal.rs

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:?}");
}