330 lines
16 KiB
Rust
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!([]));
|
|
}
|