vectorcraft/crates/engine/src/tests_slices.rs

330 lines
16 KiB
Rust

//! Object → Slice, View → Hide/Lock Slices, Select → Object → Slices and `slice.list`.
use serde_json::{Value, json};
use vectorcraft_doc::SliceKind;
use super::*;
fn session() -> Session {
let mut s = Session::new();
s.execute("file.new", &json!({"width": 300, "height": 300})).unwrap();
// Unstroked art: visual bounds are the geometry.
s.paint.stroke = vectorcraft_color::Paint::None;
s
}
fn run(s: &mut Session, id: &str, p: Value) -> Value {
s.execute(id, &p).unwrap_or_else(|e| panic!("{id}: {e}"))
}
fn rect(s: &mut Session, x: f64, y: f64, w: f64, h: f64) -> u64 {
run(s, "shape.rectangle", json!({"x": x, "y": y, "width": w, "height": h}))["id"].as_u64().unwrap()
}
/// `slice.list`'s slices.
fn slices(s: &mut Session) -> Vec<Value> {
run(s, "slice.list", json!({}))["slices"].as_array().unwrap().clone()
}
/// The laid-out rectangle of slice `id` as [x, y, width, height].
fn rect_of(s: &mut Session, id: u64) -> [f64; 4] {
let v = slices(s).into_iter().find(|v| v["id"] == json!(id)).unwrap();
["x", "y", "width", "height"].map(|k| v[k].as_f64().unwrap())
}
fn user_count(s: &Session) -> usize {
s.doc().unwrap().doc.slices.len()
}
/// A user slice over `[x, y, w, h]`, made from a temporary rectangle.
fn user_slice(s: &mut Session, r: [f64; 4]) -> u64 {
let tmp = rect(s, r[0], r[1], r[2], r[3]);
let id = run(s, "object.slice.fromSelection", json!({"ids": [tmp]}))["id"].as_u64().unwrap();
run(s, "select.set", json!({"ids": [tmp]}));
run(s, "edit.clear", json!({}));
id
}
#[test]
fn an_object_slice_follows_its_object_and_releases_back_to_it() {
let mut s = session();
let a = rect(&mut s, 100.0, 100.0, 50.0, 40.0);
let made = run(&mut s, "object.slice.make", json!({}));
assert_eq!(made["ids"], json!([a]));
assert_eq!(rect_of(&mut s, a), [100.0, 100.0, 50.0, 40.0]);
let list = slices(&mut s);
assert!(list.iter().any(|v| v["source"] == "auto"), "auto slices fill the artboard");
let area: f64 = list.iter().map(|v| v["width"].as_f64().unwrap() * v["height"].as_f64().unwrap()).sum();
assert_eq!(area, 300.0 * 300.0);
// Moving the object moves its slice; one undo step brings it back.
run(&mut s, "object.move", json!({"dx": 20, "dy": 10}));
assert_eq!(rect_of(&mut s, a), [120.0, 110.0, 50.0, 40.0]);
run(&mut s, "edit.undo", json!({}));
assert_eq!(rect_of(&mut s, a), [100.0, 100.0, 50.0, 40.0]);
// Make again changes nothing; Release leaves the object.
assert_eq!(run(&mut s, "object.slice.make", json!({}))["ids"], json!([]));
assert_eq!(run(&mut s, "object.slice.release", json!({}))["ids"], json!([a]));
assert!(s.doc().unwrap().doc.node(NodeId(a)).unwrap().slice.is_none());
assert!(slices(&mut s).is_empty());
run(&mut s, "edit.undo", json!({}));
assert!(s.doc().unwrap().doc.node(NodeId(a)).unwrap().slice.is_some());
}
#[test]
fn releasing_a_user_slice_leaves_an_unpainted_rectangle() {
let mut s = session();
let id = user_slice(&mut s, [10.0, 20.0, 30.0, 40.0]);
run(&mut s, "select.object.slices", json!({}));
let r = run(&mut s, "object.slice.release", json!({}));
assert_eq!(s.doc().unwrap().doc.slice(NodeId(id)), None);
let rid = r["ids"][0].as_u64().unwrap();
let d = &s.doc().unwrap().doc;
assert!(d.slices.is_empty());
let n = d.node(NodeId(rid)).unwrap();
assert_eq!(n.geometric_bounds().unwrap(), vectorcraft_geom::Rect::new(10.0, 20.0, 40.0, 60.0));
assert!(matches!(n.appearance.fill_paint(), vectorcraft_color::Paint::None));
assert_eq!(s.doc().unwrap().selection.objects, vec![NodeId(rid)]);
}
#[test]
fn guides_give_nine_slices() {
let mut s = session();
for (vertical, pos) in [(true, 100.0), (true, 200.0), (false, 50.0), (false, 250.0), (true, 400.0)] {
run(&mut s, "guide.add", json!({"vertical": vertical, "pos": pos}));
}
user_slice(&mut s, [0.0, 0.0, 10.0, 10.0]);
let ids = run(&mut s, "object.slice.fromGuides", json!({}))["ids"].as_array().unwrap().len();
assert_eq!(ids, 9, "the guide off the artboard cuts nothing");
assert_eq!(user_count(&s), 9, "the earlier slice is replaced");
let list = slices(&mut s);
assert_eq!(list.len(), 9, "the grid covers the artboard: no auto slices");
assert_eq!(list[4]["x"], 100.0);
assert_eq!(list[4]["width"], 100.0);
assert_eq!(list[4]["height"], 200.0);
assert_eq!(list.iter().map(|v| v["number"].as_u64().unwrap()).collect::<Vec<_>>(), (1..=9).collect::<Vec<_>>());
// One undo step takes the grid back.
run(&mut s, "edit.undo", json!({}));
assert_eq!(user_count(&s), 1);
}
#[test]
fn divide_two_by_three_and_by_size() {
let mut s = session();
user_slice(&mut s, [0.0, 0.0, 90.0, 60.0]);
run(&mut s, "select.object.slices", json!({}));
run(&mut s, "object.slice.options", json!({"name": "hero", "url": "https://example.com"}));
let pieces = run(&mut s, "object.slice.divide", json!({"rows": 2, "columns": 3}))["ids"].as_array().unwrap().clone();
assert_eq!(pieces.len(), 6);
let d = &s.doc().unwrap().doc;
let rects: Vec<_> = d.slices.iter().map(|sl| (sl.rect.x0, sl.rect.y0, sl.rect.width(), sl.rect.height())).collect();
assert_eq!(
rects,
[
(0.0, 0.0, 30.0, 30.0),
(30.0, 0.0, 30.0, 30.0),
(60.0, 0.0, 30.0, 30.0),
(0.0, 30.0, 30.0, 30.0),
(30.0, 30.0, 30.0, 30.0),
(60.0, 30.0, 30.0, 30.0)
]
);
assert!(d.slices.iter().all(|sl| sl.options.url == "https://example.com"));
assert_eq!(d.slices.iter().filter(|sl| sl.options.name == "hero").count(), 1, "only the first piece keeps the name");
assert_eq!(s.doc().unwrap().selection.slices.len(), 6, "the pieces are selected");
run(&mut s, "edit.undo", json!({}));
assert_eq!(user_count(&s), 1);
// By size: 25 pt rows of a 60 pt slice: 25, 25 and 10.
run(&mut s, "select.object.slices", json!({}));
run(&mut s, "object.slice.divide", json!({"rowHeight": 25}));
let hs: Vec<f64> = s.doc().unwrap().doc.slices.iter().map(|sl| sl.rect.height()).collect();
assert_eq!(hs, [25.0, 25.0, 10.0]);
assert!(s.execute("object.slice.divide", &json!({"rows": 0})).is_err());
assert!(s.execute("object.slice.divide", &json!({"columns": 5000})).is_err());
assert!(s.execute("object.slice.divide", &json!({})).is_err(), "nothing to divide by");
}
#[test]
fn combine_and_duplicate() {
let mut s = session();
user_slice(&mut s, [0.0, 0.0, 20.0, 20.0]);
run(&mut s, "select.object.slices", json!({}));
assert!(s.execute("object.slice.combine", &json!({})).is_err(), "two or more");
let b = rect(&mut s, 50.0, 40.0, 10.0, 10.0);
run(&mut s, "object.slice.make", json!({}));
run(&mut s, "select.object.slices", json!({}));
let c = run(&mut s, "object.slice.combine", json!({}))["id"].as_u64().unwrap();
assert_eq!(rect_of(&mut s, c), [0.0, 0.0, 60.0, 50.0]);
assert_eq!(user_count(&s), 1);
assert!(s.doc().unwrap().doc.node(NodeId(b)).unwrap().slice.is_none(), "the object slice was released");
assert_eq!(s.doc().unwrap().selection.slices, vec![NodeId(c)]);
let dup = run(&mut s, "object.slice.duplicate", json!({}))["ids"][0].as_u64().unwrap();
assert_eq!(rect_of(&mut s, dup), [10.0, 10.0, 60.0, 50.0]);
assert_eq!(s.doc().unwrap().selection.slices, vec![NodeId(dup)]);
}
#[test]
fn slice_options_set_read_and_undo_in_one_step() {
let mut s = session();
user_slice(&mut s, [0.0, 0.0, 50.0, 50.0]);
run(&mut s, "select.object.slices", json!({}));
let o = run(&mut s, "object.slice.options", json!({}));
assert_eq!((o["kind"].clone(), o["htmlText"].clone()), (json!("image"), json!(false)));
let undo = s.doc().unwrap().history.undo.len();
let o = run(
&mut s,
"object.slice.options",
json!({"kind": "No Image", "name": " cell ", "text": " <b>Hi</b>", "background": "#FF0000", "hAlign": "center", "vAlign": "bottom"}),
);
assert_eq!(
o,
json!({"kind": "noImage", "name": "cell", "url": "", "target": "", "message": "", "alt": "", "text": " <b>Hi</b>", "background": "#ff0000", "hAlign": "center", "vAlign": "bottom", "htmlText": false})
);
assert_eq!(s.doc().unwrap().history.undo.len(), undo + 1);
assert_eq!(slices(&mut s)[0]["name"], "cell");
assert!(s.execute("object.slice.options", &json!({"kind": "htmlText"})).is_err(), "HTML Text is for type");
assert!(s.execute("object.slice.options", &json!({"background": "pink-ish"})).is_err());
run(&mut s, "edit.undo", json!({}));
assert_eq!(s.doc().unwrap().doc.slices[0].options, Default::default());
// An object slice of type can be HTML Text.
let t = run(&mut s, "text.create", json!({"x": 10, "y": 100, "text": "Hello"}))["id"].as_u64().unwrap();
run(&mut s, "object.slice.make", json!({"ids": [t]}));
run(&mut s, "select.set", json!({"ids": [t]}));
let o = run(&mut s, "object.slice.options", json!({"kind": "htmlText"}));
assert_eq!((o["kind"].clone(), o["htmlText"].clone()), (json!("htmlText"), json!(true)));
assert_eq!(s.doc().unwrap().doc.node(NodeId(t)).unwrap().slice.as_ref().unwrap().kind, SliceKind::HtmlText);
}
#[test]
fn clip_to_artboard_hide_lock_select_and_delete_all() {
let mut s = session();
let id = user_slice(&mut s, [250.0, 250.0, 100.0, 100.0]);
assert_eq!(rect_of(&mut s, id), [250.0, 250.0, 50.0, 50.0], "clipped to the artboard");
assert_eq!(run(&mut s, "object.slice.clipToArtboard", json!({}))["on"], false);
assert_eq!(rect_of(&mut s, id), [250.0, 250.0, 100.0, 100.0]);
run(&mut s, "edit.undo", json!({}));
assert!(s.doc().unwrap().doc.slices_clip_to_artboard);
assert_eq!(run(&mut s, "object.slice.clipToArtboard", json!({"on": true}))["on"], true);
assert_eq!(run(&mut s, "view.slices.hide", json!({}))["hidden"], true);
assert!(s.slices_hidden());
assert_eq!(run(&mut s, "view.slices.lock", json!({"locked": true}))["locked"], true);
let l = run(&mut s, "slice.list", json!({}));
assert_eq!((l["hidden"].clone(), l["locked"].clone(), l["clipToArtboard"].clone()), (json!(true), json!(true), json!(true)));
run(&mut s, "view.slices.hide", json!({"hidden": false}));
assert!(!s.slices_hidden());
let a = rect(&mut s, 10.0, 10.0, 10.0, 10.0);
run(&mut s, "object.slice.make", json!({}));
assert_eq!(run(&mut s, "select.object.slices", json!({}))["count"], 2);
let sel = &s.doc().unwrap().selection;
assert_eq!((sel.slices.clone(), sel.objects.is_empty()), (vec![NodeId(id), NodeId(a)], true));
assert!(slices(&mut s).iter().filter(|v| v["selected"] == true).count() == 2);
// Selecting objects drops the slice selection.
run(&mut s, "select.all", json!({}));
assert!(s.doc().unwrap().selection.slices.is_empty());
assert_eq!(run(&mut s, "object.slice.deleteAll", json!({}))["count"], 2);
assert_eq!(user_count(&s), 0);
assert!(s.doc().unwrap().doc.node(NodeId(a)).unwrap().slice.is_none());
assert!(s.execute("object.slice.deleteAll", &json!({})).is_err(), "nothing to delete");
}
#[test]
fn slices_survive_a_native_round_trip() {
let mut s = session();
user_slice(&mut s, [100.0, 100.0, 40.0, 40.0]);
run(&mut s, "select.object.slices", json!({}));
run(&mut s, "object.slice.options", json!({"kind": "noImage", "text": "x"}));
let a = rect(&mut s, 10.0, 10.0, 10.0, 10.0);
run(&mut s, "object.slice.make", json!({}));
run(&mut s, "object.slice.options", json!({"alt": "logo"}));
run(&mut s, "object.slice.clipToArtboard", json!({"on": false}));
let doc = s.doc().unwrap().doc.clone();
vectorcraft_testkit::invariants::check_native_roundtrip_exact(&doc).unwrap();
let back = vectorcraft_format::load(&vectorcraft_format::save(&doc, false)).unwrap();
assert_eq!(back.slices, doc.slices);
assert_eq!(back.node(NodeId(a)).unwrap().slice.as_ref().unwrap().alt, "logo");
assert!(!back.slices_clip_to_artboard);
// A copy of an object slice is an object slice too, under its own id.
run(&mut s, "select.set", json!({"ids": [a]}));
run(&mut s, "edit.duplicate", json!({}));
assert_eq!(s.doc().unwrap().doc.object_slices().len(), 2);
}
#[test]
fn slice_tool_gestures_are_one_undo_step_each() {
use vectorcraft_tools::{Mods, PointerEvent, PointerKind as K, ToolKey};
let mut s = session();
let view = ViewInfo { smart_guides: false, ..Default::default() };
let undo = |s: &Session| s.doc().unwrap().history.undo.len();
let gesture = |s: &mut Session, pts: &[(K, f64, f64)]| {
for (k, x, y) in pts {
s.pointer(&PointerEvent::new(*k, *x, *y), view).unwrap();
}
};
s.select_tool("slice", view).unwrap();
let n = undo(&s);
gesture(&mut s, &[(K::Down, 10.0, 10.0), (K::Drag, 50.0, 30.0), (K::Drag, 110.0, 60.0), (K::Up, 110.0, 60.0)]);
assert_eq!(undo(&s), n + 1);
let id = s.doc().unwrap().doc.slices[0].id;
assert_eq!(s.doc().unwrap().doc.slices.len(), 1, "the previews left one slice");
assert_eq!(rect_of(&mut s, id.0), [10.0, 10.0, 100.0, 50.0]);
assert_eq!(s.doc().unwrap().selection.slices, vec![id]);
// Slice Selection: drag the slice, then its bottom-right handle, then Delete.
s.select_tool("sliceSelection", view).unwrap();
gesture(&mut s, &[(K::Down, 20.0, 20.0), (K::Drag, 30.0, 40.0), (K::Drag, 40.0, 50.0), (K::Up, 40.0, 50.0)]);
assert_eq!(undo(&s), n + 2);
assert_eq!(rect_of(&mut s, id.0), [30.0, 40.0, 100.0, 50.0]);
gesture(&mut s, &[(K::Down, 130.0, 90.0), (K::Drag, 140.0, 95.0), (K::Drag, 150.0, 100.0), (K::Up, 150.0, 100.0)]);
assert_eq!(undo(&s), n + 3);
assert_eq!(rect_of(&mut s, id.0), [30.0, 40.0, 120.0, 60.0]);
assert!(s.tool_claims_key(ToolKey::Delete, view));
s.tool_key(ToolKey::Delete, Mods::default(), view).unwrap();
assert!(s.doc().unwrap().doc.slices.is_empty());
assert_eq!(undo(&s), n + 4);
// Undo walks the gestures back one at a time.
run(&mut s, "edit.undo", json!({}));
assert_eq!(rect_of(&mut s, id.0), [30.0, 40.0, 120.0, 60.0]);
run(&mut s, "edit.undo", json!({}));
assert_eq!(rect_of(&mut s, id.0), [30.0, 40.0, 100.0, 50.0]);
// An object slice moves its object; locked slices stay put.
let a = rect(&mut s, 200.0, 200.0, 40.0, 40.0);
run(&mut s, "object.slice.make", json!({}));
gesture(&mut s, &[(K::Down, 220.0, 220.0), (K::Drag, 230.0, 220.0), (K::Drag, 240.0, 230.0), (K::Up, 240.0, 230.0)]);
assert_eq!(s.doc().unwrap().doc.node(NodeId(a)).unwrap().geometric_bounds().unwrap().x0, 220.0);
run(&mut s, "view.slices.lock", json!({"locked": true}));
let before = undo(&s);
gesture(&mut s, &[(K::Down, 250.0, 240.0), (K::Drag, 280.0, 260.0), (K::Up, 280.0, 260.0)]);
assert_eq!(undo(&s), before);
// The journal replays the gestures as commands.
let journal: Vec<String> = s.journal.iter().map(|(c, _)| c.clone()).collect();
for c in ["object.slice.create", "object.slice.move", "object.slice.setRect", "object.slice.delete", "object.slice.select"] {
assert!(journal.iter().any(|j| j == c), "{c} journaled: {journal:?}");
}
}
#[test]
fn slice_commands_check_their_params() {
let mut s = session();
assert!(s.execute("object.slice.create", &json!({"x": 0, "y": 0, "width": 0, "height": 10})).is_err(), "no area");
assert!(s.execute("object.slice.create", &json!({"x": 1e9, "y": 0, "width": 10, "height": 10})).is_err(), "off the canvas");
let id = run(&mut s, "object.slice.create", json!({"x": 50, "y": 60, "width": -20, "height": 10}))["id"].as_u64().unwrap();
assert_eq!(rect_of(&mut s, id), [30.0, 60.0, 20.0, 10.0], "a negative size draws the other way");
run(&mut s, "object.slice.setRect", json!({"id": id, "x": 0, "y": 0, "width": 5, "height": 5}));
assert_eq!(rect_of(&mut s, id), [0.0, 0.0, 5.0, 5.0]);
let a = rect(&mut s, 100.0, 100.0, 10.0, 10.0);
run(&mut s, "object.slice.make", json!({}));
assert!(
s.execute("object.slice.setRect", &json!({"id": a, "x": 0, "y": 0, "width": 5, "height": 5})).is_err(),
"object slices follow their object"
);
assert!(s.execute("object.slice.move", &json!({"slices": [12345], "dx": 1, "dy": 1})).is_err(), "not a slice");
assert_eq!(run(&mut s, "object.slice.select", json!({"slices": [id, a]}))["selected"], json!([id, a]));
assert_eq!(run(&mut s, "object.slice.select", json!({"slices": [a], "toggle": true}))["selected"], json!([id]));
assert_eq!(run(&mut s, "object.slice.delete", json!({"slices": [a]}))["count"], 1);
assert!(s.doc().unwrap().doc.node(NodeId(a)).is_some(), "deleting an object slice keeps its object");
assert_eq!(run(&mut s, "object.slice.select", json!({}))["selected"], json!([]));
}