mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-13 20:08:42 +00:00
514 lines
27 KiB
Rust
514 lines
27 KiB
Rust
//! Panel-support commands (`cmd/{panelcmds,gradient,appearance,stroke,swatch,style}.rs`).
|
||
|
||
use serde_json::json;
|
||
use vectorcraft_color::{Color, GradientKind, Paint};
|
||
use vectorcraft_doc::{AppearanceItem, NodeKind};
|
||
|
||
use super::*;
|
||
|
||
fn session() -> Session {
|
||
let mut s = Session::new();
|
||
s.execute("file.new", &json!({"width": 800, "height": 600})).unwrap();
|
||
s
|
||
}
|
||
|
||
fn rect(s: &mut Session) -> NodeId {
|
||
let r = s.execute("shape.rectangle", &json!({"x": 10, "y": 10, "width": 100, "height": 50})).unwrap();
|
||
NodeId(r["id"].as_u64().unwrap())
|
||
}
|
||
|
||
fn node(s: &Session, id: NodeId) -> vectorcraft_doc::Node {
|
||
s.doc().unwrap().doc.node(id).unwrap().clone()
|
||
}
|
||
|
||
#[test]
|
||
fn edit_gradient_converts_and_keeps_stops() {
|
||
let mut s = session();
|
||
let id = rect(&mut s);
|
||
s.execute("select.set", &json!({"ids": [id.0]})).unwrap();
|
||
s.execute(
|
||
"paint.editGradient",
|
||
&json!({"stroke": false, "kind": "radial", "stops": [
|
||
{"offset": 1.0, "color": "#0000ff"},
|
||
{"offset": 0.0, "color": "#ff0000", "opacity": 50, "midpoint": 0.3},
|
||
{"offset": 0.5, "color": "#00ff00"}]}),
|
||
)
|
||
.unwrap();
|
||
let Paint::Gradient(g) = node(&s, id).appearance.fill_paint() else { panic!("not a gradient") };
|
||
assert_eq!(g.gradient.kind, GradientKind::Radial);
|
||
assert_eq!(g.gradient.stops.len(), 3);
|
||
assert_eq!(g.gradient.stops[0].color.to_hex(), "#ff0000");
|
||
assert!((g.gradient.stops[0].opacity - 0.5).abs() < 1e-6);
|
||
assert!((g.gradient.stops[0].midpoint - 0.3).abs() < 1e-6);
|
||
// One undo step.
|
||
s.execute("edit.undo", &json!({})).unwrap();
|
||
assert!(node(&s, id).appearance.fill_paint().color().is_some());
|
||
}
|
||
|
||
#[test]
|
||
fn edit_gradient_angle_reverse_and_errors() {
|
||
let mut s = session();
|
||
let id = rect(&mut s);
|
||
s.execute("select.set", &json!({"ids": [id.0]})).unwrap();
|
||
s.execute("paint.editGradient", &json!({"stroke": false, "angle": 450})).unwrap();
|
||
let Paint::Gradient(g) = node(&s, id).appearance.fill_paint() else { panic!() };
|
||
assert!((g.angle - 90.0).abs() < 1e-9);
|
||
s.execute("paint.editGradient", &json!({"stroke": false, "reverse": true})).unwrap();
|
||
let Paint::Gradient(g) = node(&s, id).appearance.fill_paint() else { panic!() };
|
||
assert_eq!(g.gradient.stops[0].color.to_hex(), "#000000");
|
||
// #1007: an off-centre midpoint is mirrored, not reset.
|
||
let stops = json!([{"offset": 0, "color": "#ff0000", "midpoint": 0.2}, {"offset": 1, "color": "#0000ff"}]);
|
||
s.execute("paint.editGradient", &json!({"stroke": false, "stops": stops})).unwrap();
|
||
s.execute("paint.editGradient", &json!({"stroke": false, "reverse": true})).unwrap();
|
||
let Paint::Gradient(g) = node(&s, id).appearance.fill_paint() else { panic!() };
|
||
assert_eq!(g.gradient.stops[0].color.to_hex(), "#0000ff");
|
||
assert!((g.gradient.stops[0].midpoint - 0.8).abs() < 1e-6, "{:?}", g.gradient.stops);
|
||
assert!(s.execute("paint.editGradient", &json!({"stops": [{"offset": 0, "color": "#fff"}]})).is_err());
|
||
assert!(s.execute("paint.editGradient", &json!({"kind": "conic"})).is_err());
|
||
}
|
||
|
||
#[test]
|
||
fn appearance_duplicate_and_move() {
|
||
let mut s = session();
|
||
let id = rect(&mut s);
|
||
s.execute("select.set", &json!({"ids": [id.0]})).unwrap();
|
||
let n0 = node(&s, id).appearance.items.len();
|
||
s.execute("appearance.duplicateItem", &json!({"index": 0})).unwrap();
|
||
assert_eq!(node(&s, id).appearance.items.len(), n0 + 1);
|
||
let last = node(&s, id).appearance.items.len() - 1;
|
||
s.execute("appearance.moveItem", &json!({"from": last, "to": 0})).unwrap();
|
||
assert!(matches!(node(&s, id).appearance.items[0], AppearanceItem::Stroke(_)));
|
||
assert!(s.execute("appearance.duplicateItem", &json!({"index": 99})).is_err());
|
||
}
|
||
|
||
#[test]
|
||
fn artboards_reorder_and_duplicate() {
|
||
let mut s = session();
|
||
s.execute("artboard.new", &json!({"name": "Second"})).unwrap();
|
||
let r = s.execute("artboard.duplicate", &json!({"index": 0})).unwrap();
|
||
assert_eq!(r["index"], 2);
|
||
let d = &s.doc().unwrap().doc;
|
||
assert_eq!(d.artboards.len(), 3);
|
||
assert!(d.artboards[2].rect.x0 >= d.artboards[1].rect.x1);
|
||
s.execute("artboard.reorder", &json!({"index": 1, "to": 0})).unwrap();
|
||
assert_eq!(s.doc().unwrap().doc.artboards[0].name, "Second");
|
||
}
|
||
|
||
/// An artboard's background color (Properties › Artboards › Fill): `artboard.setProps {color}` sets
|
||
/// it and `{color: null}` clears it, each in one undo step. A change without `color` leaves it, a
|
||
/// duplicate keeps it, `inspect_document` reports it, `artboard.new {color}` starts with it, an
|
||
/// `artboards` list gives each listed artboard its own color in one undo step, and a bad color is
|
||
/// refused.
|
||
#[test]
|
||
fn artboard_color_is_set_and_cleared_in_one_undo_step() {
|
||
let mut s = session();
|
||
s.execute("artboard.new", &json!({})).unwrap();
|
||
let color = |s: &Session, i: usize| s.doc().unwrap().doc.artboards[i].color;
|
||
let blue = Color::from_hex("#336699");
|
||
let black = Some(Color::cmyk(0.0, 0.0, 0.0, 1.0));
|
||
s.execute("artboard.setProps", &json!({"index": 1, "color": "#336699"})).unwrap();
|
||
assert_eq!((color(&s, 0), color(&s, 1)), (None, blue), "only that artboard");
|
||
s.execute("artboard.setProps", &json!({"index": 1, "name": "Dark"})).unwrap();
|
||
assert_eq!(color(&s, 1), blue, "a change without a color leaves it");
|
||
s.execute("artboard.setProps", &json!({"index": 1, "color": {"c": 0, "m": 0, "y": 0, "k": 100}})).unwrap();
|
||
assert_eq!(color(&s, 1), black);
|
||
s.execute("artboard.setProps", &json!({"index": 1, "color": null})).unwrap();
|
||
assert_eq!(color(&s, 1), None);
|
||
for was in [black, blue, blue, None] {
|
||
s.execute("edit.undo", &json!({})).unwrap();
|
||
assert_eq!(color(&s, 1), was);
|
||
}
|
||
s.execute("edit.redo", &json!({})).unwrap();
|
||
let doc = crate::inspect::document(&s);
|
||
assert_eq!(doc["artboards"][1]["color"], "#336699");
|
||
assert!(doc["artboards"][0].get("color").is_none(), "{doc}");
|
||
let copy = s.execute("artboard.duplicate", &json!({"index": 1})).unwrap()["index"].as_u64().unwrap() as usize;
|
||
assert_eq!(color(&s, copy), blue, "a duplicate keeps the color");
|
||
let made = s.execute("artboard.new", &json!({"color": "#336699"})).unwrap()["index"].as_u64().unwrap() as usize;
|
||
assert_eq!(color(&s, made), blue, "a new artboard takes `color` too");
|
||
let undo = s.doc().unwrap().history.undo.len();
|
||
s.execute("artboard.setProps", &json!({"artboards": [{"index": 0, "color": "#336699"}, {"index": 1, "color": null}]})).unwrap();
|
||
assert_eq!((color(&s, 0), color(&s, 1)), (blue, None), "each listed artboard takes its own color");
|
||
assert_eq!(s.doc().unwrap().history.undo.len(), undo + 1, "in one undo step");
|
||
let before = s.doc().unwrap().doc.clone();
|
||
assert!(s.execute("artboard.setProps", &json!({"artboards": [{"index": 0, "color": null}, {"index": 1, "color": "nope"}]})).is_err());
|
||
assert!(s.execute("artboard.setProps", &json!({"index": 1, "color": "nope"})).is_err());
|
||
assert!(s.execute("artboard.new", &json!({"color": "nope"})).is_err());
|
||
assert_eq!(*s.doc().unwrap().doc, *before, "a bad color changes nothing");
|
||
}
|
||
|
||
/// The native format writes an artboard's background color and reads it back. An artboard without
|
||
/// one is written as before, with no `color` key.
|
||
#[test]
|
||
fn artboard_color_round_trips_through_the_native_format() {
|
||
let mut s = session();
|
||
s.execute("artboard.new", &json!({})).unwrap();
|
||
s.execute("artboard.setProps", &json!({"index": 1, "color": {"c": 10, "m": 20, "y": 30, "k": 40}})).unwrap();
|
||
let bytes = vectorcraft_format::save(&s.doc().unwrap().doc, false);
|
||
let saved: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
|
||
let keys = |i: usize| saved["document"]["artboards"][i].as_object().unwrap().keys().cloned().collect::<Vec<_>>();
|
||
assert_eq!(keys(0), ["id", "name", "rect", "show_center_mark", "show_cross_hairs"]);
|
||
assert!(keys(1).contains(&"color".to_string()), "{:?}", keys(1));
|
||
let back = vectorcraft_format::load(&bytes).unwrap();
|
||
assert_eq!(back.artboards[0].color, None);
|
||
assert_eq!(back.artboards[1].color, Some(Color::cmyk(0.1, 0.2, 0.3, 0.4)));
|
||
assert_eq!(back.artboards, s.doc().unwrap().doc.artboards);
|
||
}
|
||
|
||
/// The artboards' left edges and names.
|
||
fn boards(s: &Session) -> Vec<(f64, String)> {
|
||
s.doc().unwrap().doc.artboards.iter().map(|a| (a.rect.x0, a.name.clone())).collect()
|
||
}
|
||
|
||
/// The left edge of object `id`.
|
||
fn left(s: &Session, id: u64) -> f64 {
|
||
s.doc().unwrap().doc.node(NodeId(id)).unwrap().geometric_bounds().unwrap().x0
|
||
}
|
||
|
||
fn ids(v: &serde_json::Value) -> Vec<u64> {
|
||
v.as_array().unwrap().iter().map(|v| v.as_u64().unwrap()).collect()
|
||
}
|
||
|
||
/// Duplicate Artboards (#446) copies the art on the artboard too, as the Artboard tool's Move/Copy
|
||
/// Artwork with Artboard says, in one undo step.
|
||
#[test]
|
||
fn duplicate_artboard_copies_its_art() {
|
||
let mut s = session();
|
||
let r = rect(&mut s);
|
||
let out = s.execute("artboard.duplicate", &json!({"index": 0})).unwrap();
|
||
assert_eq!(out["index"], 1);
|
||
let copies = ids(&out["ids"]);
|
||
assert_eq!(copies.len(), 1);
|
||
assert_ne!(copies[0], r.0);
|
||
assert_eq!(boards(&s), vec![(0.0, "Artboard 1".into()), (820.0, "Artboard 1 copy".into())]);
|
||
assert_eq!(left(&s, copies[0]), 830.0, "the copy sits on the new artboard as the original on its own");
|
||
s.execute("edit.undo", &json!({})).unwrap();
|
||
let d = &s.doc().unwrap().doc;
|
||
assert_eq!((d.artboards.len(), d.layers[0].children().unwrap().len()), (1, 1), "one undo step");
|
||
// Off (the tool's option or the param): the artboard alone.
|
||
s.set_tool_options(Some("artboard"), json!({"moveArt": false}).as_object().unwrap());
|
||
assert_eq!(s.execute("artboard.duplicate", &json!({})).unwrap()["ids"], json!([]));
|
||
assert_eq!(ids(&s.execute("artboard.duplicate", &json!({"index": 0, "art": true})).unwrap()["ids"]).len(), 1);
|
||
}
|
||
|
||
/// New Artboard and Artboard Options trim the name they are given, as the Artboards panel's rename
|
||
/// does, and a blank name gives the artboard its default name, Artboard N for the Nth artboard.
|
||
#[test]
|
||
fn new_and_renamed_artboards_trim_their_names() {
|
||
let mut s = session();
|
||
let new = |s: &mut Session, name: &str| s.execute("artboard.new", &json!({"name": name})).unwrap();
|
||
new(&mut s, " Cover \t");
|
||
new(&mut s, " \t ");
|
||
new(&mut s, "");
|
||
let names = |s: &Session| boards(s).into_iter().map(|(_, n)| n).collect::<Vec<_>>();
|
||
assert_eq!(names(&s), ["Artboard 1", "Cover", "Artboard 3", "Artboard 4"]);
|
||
s.execute("artboard.setProps", &json!({"index": 0, "name": " Back\t"})).unwrap();
|
||
s.execute("artboard.setProps", &json!({"index": 1, "name": " "})).unwrap();
|
||
s.execute("artboard.setProps", &json!({"index": 3, "name": ""})).unwrap();
|
||
assert_eq!(names(&s), ["Back", "Artboard 2", "Artboard 3", "Artboard 4"]);
|
||
}
|
||
|
||
/// Duplicate, Copy and Cut Artboard and Move Artboard Up/Down fail with `no such artboard` on an
|
||
/// `index` that names no artboard (past the last, negative, fractional, a string or null), and the
|
||
/// artboards, the art and the clipboard stay as they were.
|
||
#[test]
|
||
fn artboard_copies_refuse_an_index_that_names_no_artboard() {
|
||
let mut s = session();
|
||
rect(&mut s);
|
||
s.execute("artboard.new", &json!({})).unwrap();
|
||
s.set_tool_options(Some("artboard"), json!({"active": 1}).as_object().unwrap());
|
||
let (was, n) = (boards(&s), s.doc().unwrap().history.undo.len());
|
||
for index in [json!(99), json!(-1), json!(1.5), json!("1"), serde_json::Value::Null] {
|
||
for cmd in ["artboard.duplicate", "artboard.copy", "artboard.cut", "artboard.reorder"] {
|
||
let p = if cmd == "artboard.reorder" { json!({"index": index, "to": 0}) } else { json!({"index": index}) };
|
||
assert_eq!(s.execute(cmd, &p).unwrap_err().to_string(), "no such artboard", "{cmd} {p}");
|
||
}
|
||
}
|
||
let st = s.doc().unwrap();
|
||
assert_eq!((boards(&s), st.doc.layers[0].children().unwrap().len(), st.history.undo.len()), (was, 1, n));
|
||
assert!(s.clipboard.nodes.is_empty() && s.clipboard.artboards.is_empty(), "nothing copied");
|
||
}
|
||
|
||
/// Artboards 1 and 2, each with a square on it, and an artboard 3 below artboard 1 with a square on
|
||
/// it, with the Artboard tool selecting artboards 1 and 2 → the squares' ids, artboard by artboard.
|
||
fn two_selected_artboards(s: &mut Session) -> [u64; 3] {
|
||
s.execute("artboard.new", &json!({"x": 1000, "y": 0, "width": 100, "height": 100})).unwrap();
|
||
s.execute("artboard.new", &json!({"x": 0, "y": 700, "width": 100, "height": 100, "name": "Small"})).unwrap();
|
||
let sq = |s: &mut Session, x: u64, y: u64| {
|
||
s.execute("shape.rectangle", &json!({"x": x, "y": y, "width": 10, "height": 10})).unwrap()["id"].as_u64().unwrap()
|
||
};
|
||
let ids = [sq(s, 10, 10), sq(s, 1010, 10), sq(s, 10, 710)];
|
||
s.execute("select.none", &json!({})).unwrap();
|
||
s.select_tool("artboard", ViewInfo::default()).unwrap();
|
||
s.set_tool_option("selected", &json!([0, 1]));
|
||
ids
|
||
}
|
||
|
||
/// The objects on the document's layers, in paint order.
|
||
fn objects(s: &Session) -> Vec<u64> {
|
||
s.doc().unwrap().doc.layers.iter().flat_map(|l| l.children().into_iter().flatten().map(|n| n.id.0)).collect()
|
||
}
|
||
|
||
/// Duplicate Artboards copies every selected artboard with its art, in one undo step (the public
|
||
/// Use the artboard context menu page). The copies keep their places relative to each other, right
|
||
/// of the last artboard.
|
||
#[test]
|
||
fn duplicate_artboards_copies_every_selected_artboard_in_one_step() {
|
||
let mut s = session();
|
||
let [a, b, _] = two_selected_artboards(&mut s);
|
||
let before = (boards(&s), objects(&s));
|
||
for p in [json!({"indices": [0, 1]}), json!({})] {
|
||
let out = s.execute("artboard.duplicate", &p).unwrap();
|
||
assert_eq!((out["index"].clone(), out["indices"].clone()), (json!(3), json!([3, 4])), "{p}");
|
||
let copies = ids(&out["ids"]);
|
||
assert_eq!(copies.len(), 2, "{p}");
|
||
assert_eq!(boards(&s)[3..], [(1120.0, "Artboard 1 copy".to_string()), (2120.0, "Artboard 2 copy".to_string())], "{p}");
|
||
assert_eq!((left(&s, copies[0]), left(&s, copies[1])), (1130.0, 2130.0), "{p}: each square on its artboard's copy");
|
||
assert!(left(&s, a) == 10.0 && left(&s, b) == 1010.0, "{p}: the originals stay");
|
||
s.execute("edit.undo", &json!({})).unwrap();
|
||
assert_eq!((boards(&s), objects(&s)), before, "{p}: one undo step");
|
||
}
|
||
}
|
||
|
||
/// With the Artboard tool, Copy takes every selected artboard and the art on each (the public Cut,
|
||
/// copy, and paste artboards page). Paste adds a copy of each with its art in one undo step,
|
||
/// together right of the last artboard; Paste in Place puts them where they were. The tool then
|
||
/// selects the pasted artboards.
|
||
#[test]
|
||
fn artboard_tool_copy_and_paste_bring_every_selected_artboard() {
|
||
let mut s = session();
|
||
two_selected_artboards(&mut s);
|
||
let before = (boards(&s), objects(&s));
|
||
assert_eq!(s.execute("edit.copy", &json!({})).unwrap()["copied"], 2, "the squares on artboards 1 and 2");
|
||
let out = s.execute("edit.paste", &json!({})).unwrap();
|
||
assert_eq!((out["artboard"].clone(), out["artboards"].clone()), (json!(3), json!([3, 4])));
|
||
assert_eq!(boards(&s)[3..], [(1120.0, "Artboard 1 copy".to_string()), (2120.0, "Artboard 2 copy".to_string())]);
|
||
let pasted = ids(&out["ids"]);
|
||
assert_eq!((left(&s, pasted[0]), left(&s, pasted[1])), (1130.0, 2130.0));
|
||
assert_eq!(s.tool_options()["selected"], json!([3, 4]), "the tool selects the pasted artboards");
|
||
s.execute("edit.undo", &json!({})).unwrap();
|
||
assert_eq!((boards(&s), objects(&s)), before, "one undo step");
|
||
let out = s.execute("edit.pasteInPlace", &json!({})).unwrap();
|
||
let d = &s.doc().unwrap().doc;
|
||
assert_eq!((d.artboards[3].rect, d.artboards[4].rect), (d.artboards[0].rect, d.artboards[1].rect));
|
||
let pasted = ids(&out["ids"]);
|
||
assert_eq!((left(&s, pasted[0]), left(&s, pasted[1])), (10.0, 1010.0));
|
||
}
|
||
|
||
/// With the Artboard tool, Cut takes every selected artboard and the art on each away in one undo
|
||
/// step, and leaves the other artboards and their art; it fails when every artboard is selected.
|
||
#[test]
|
||
fn artboard_tool_cut_removes_every_selected_artboard() {
|
||
let mut s = session();
|
||
let [a, b, c] = two_selected_artboards(&mut s);
|
||
let before = (boards(&s), objects(&s));
|
||
assert_eq!(s.execute("edit.cut", &json!({})).unwrap()["copied"], 2);
|
||
assert_eq!((boards(&s), objects(&s)), (vec![(0.0, "Small".into())], vec![c]));
|
||
s.execute("edit.pasteInPlace", &json!({})).unwrap();
|
||
assert_eq!(boards(&s), vec![(0.0, "Small".into()), (0.0, "Artboard 1".into()), (1000.0, "Artboard 2".into())]);
|
||
s.execute("edit.undo", &json!({})).unwrap();
|
||
s.execute("edit.undo", &json!({})).unwrap();
|
||
assert_eq!((boards(&s), objects(&s)), before, "the cut is one undo step");
|
||
assert!(objects(&s).contains(&a) && objects(&s).contains(&b));
|
||
s.set_tool_option("selected", &json!([0, 1, 2]));
|
||
assert!(s.execute("edit.cut", &json!({})).is_err(), "every artboard");
|
||
assert_eq!(boards(&s), before.0);
|
||
}
|
||
|
||
/// With the Artboard tool, Copy and Paste copy its artboard with the art on it (#446): Paste places
|
||
/// it right of the last artboard, Paste in Place where it was, also in another document; each paste
|
||
/// is one undo step, and the tool takes the pasted artboard.
|
||
#[test]
|
||
fn artboard_tool_copy_and_paste_artboards_with_their_art() {
|
||
let mut s = session();
|
||
let r = rect(&mut s);
|
||
s.execute("artboard.new", &json!({"x": 0, "y": 700, "width": 100, "height": 100, "name": "Small"})).unwrap();
|
||
s.execute("select.none", &json!({})).unwrap();
|
||
// Nothing selected: Copy is off, until the Artboard tool is chosen.
|
||
let copy_enabled = |s: &Session| (crate::find_command("edit.copy").unwrap().enabled)(s).is_ok();
|
||
assert!(!copy_enabled(&s));
|
||
s.select_tool("artboard", ViewInfo::default()).unwrap();
|
||
assert!(copy_enabled(&s));
|
||
assert_eq!(s.execute("edit.copy", &json!({})).unwrap()["copied"], 1, "artboard 1 and its rectangle");
|
||
let out = s.execute("edit.paste", &json!({"center": [5, 5]})).unwrap();
|
||
assert_eq!(out["artboard"], 2);
|
||
let pasted = ids(&out["ids"]);
|
||
assert_eq!(left(&s, pasted[0]), 830.0, "beside the artboards, not at the view centre");
|
||
assert_eq!(boards(&s)[2], (820.0, "Artboard 1 copy".into()));
|
||
assert_eq!(s.tool_options()["active"], 2);
|
||
let st = s.doc().unwrap();
|
||
assert_eq!(st.selection.in_paint_order(&st.doc), vec![NodeId(pasted[0])]);
|
||
s.execute("edit.undo", &json!({})).unwrap();
|
||
assert_eq!(boards(&s).len(), 2);
|
||
assert!(s.doc().unwrap().doc.node(NodeId(pasted[0])).is_none(), "one undo step");
|
||
// In place: on top of the original.
|
||
let out = s.execute("edit.pasteInPlace", &json!({})).unwrap();
|
||
assert_eq!(left(&s, ids(&out["ids"])[0]), 10.0);
|
||
let d = &s.doc().unwrap().doc;
|
||
assert_eq!(d.artboards[2].rect, d.artboards[0].rect);
|
||
// Into another document.
|
||
s.execute("file.new", &json!({"width": 300, "height": 300})).unwrap();
|
||
let out = s.execute("edit.paste", &json!({})).unwrap();
|
||
assert_eq!(boards(&s), vec![(0.0, "Artboard 1".into()), (320.0, "Artboard 1 copy".into())]);
|
||
let n = s.doc().unwrap().doc.node(NodeId(ids(&out["ids"])[0])).unwrap().geometric_bounds().unwrap();
|
||
assert_eq!((n.x0, n.width()), (330.0, 100.0));
|
||
// Paste on All Artboards pastes the art alone.
|
||
let out = s.execute("edit.pasteOnAllArtboards", &json!({})).unwrap();
|
||
assert_eq!((out["artboard"].clone(), ids(&out["ids"]).len(), boards(&s).len()), (json!(null), 2, 2));
|
||
// The clipboard still holds the artboard for the next document, and the original is untouched.
|
||
s.execute("document.activate", &json!({"index": 0})).unwrap();
|
||
assert!(s.doc().unwrap().doc.node(r).is_some());
|
||
}
|
||
|
||
/// With the Artboard tool, Cut takes its artboard and the art on it away in one undo step (#446),
|
||
/// never the only artboard; pasting it back keeps its name.
|
||
#[test]
|
||
fn artboard_tool_cut_removes_the_artboard_and_its_art() {
|
||
let mut s = session();
|
||
let r = rect(&mut s);
|
||
let outside = s.execute("shape.rectangle", &json!({"x": 2000, "y": 0, "width": 10, "height": 10})).unwrap()["id"].as_u64().unwrap();
|
||
s.select_tool("artboard", ViewInfo::default()).unwrap();
|
||
assert!(s.execute("edit.cut", &json!({})).is_err(), "the only artboard");
|
||
s.execute("artboard.new", &json!({"x": 1000, "y": 0, "width": 100, "height": 100})).unwrap();
|
||
s.set_tool_option("active", &json!(0));
|
||
s.execute("edit.cut", &json!({})).unwrap();
|
||
let d = &s.doc().unwrap().doc;
|
||
assert_eq!((d.artboards.len(), d.node(r).is_none(), d.node(NodeId(outside)).is_some()), (1, true, true));
|
||
s.execute("edit.pasteInPlace", &json!({})).unwrap();
|
||
assert_eq!(boards(&s), vec![(1000.0, "Artboard 2".into()), (0.0, "Artboard 1".into())]);
|
||
s.execute("edit.undo", &json!({})).unwrap();
|
||
s.execute("edit.undo", &json!({})).unwrap();
|
||
let d = &s.doc().unwrap().doc;
|
||
assert_eq!((d.artboards.len(), d.node(r).is_some()), (2, true), "the cut is one undo step");
|
||
}
|
||
|
||
/// A copy of an artboard whose id is the largest there is (only a hand-made file has one, #1718)
|
||
/// takes an id that no other artboard has, as a new artboard does, whether Duplicate Artboards, the
|
||
/// Artboard tool's Alt-drag (`artboard.move` with `copy`) or Paste makes it. Deleting either
|
||
/// artboard then leaves the other's guides.
|
||
#[test]
|
||
fn copied_artboards_take_a_free_id_and_keep_their_own_guides() {
|
||
let mut got = vec![];
|
||
for (cmd, p) in [
|
||
("artboard.duplicate", json!({"index": 0})),
|
||
("artboard.move", json!({"index": 0, "dx": 0, "dy": 900, "copy": true})),
|
||
("edit.paste", json!({})),
|
||
] {
|
||
let mut s = Session::new();
|
||
let mut d = vectorcraft_doc::Document::new(800.0, 600.0);
|
||
d.artboards[0].id = u32::MAX;
|
||
s.add_document(d, None);
|
||
s.execute("guide.add", &json!({"vertical": true, "pos": 100, "artboard": 0})).unwrap();
|
||
if cmd == "edit.paste" {
|
||
s.execute("artboard.copy", &json!({"index": 0})).unwrap();
|
||
}
|
||
s.execute(cmd, &p).unwrap();
|
||
// Each pass deletes one artboard (the copy, then the original), adds the artboard ids of
|
||
// the remaining guides to `kept`, and undoes the delete.
|
||
let mut kept = vec![];
|
||
for i in [1, 0] {
|
||
s.execute("artboard.delete", &json!({"index": i})).unwrap();
|
||
kept.push(s.doc().unwrap().doc.guides.iter().map(|g| g.artboard).collect::<Vec<_>>());
|
||
s.execute("edit.undo", &json!({})).unwrap();
|
||
}
|
||
got.push((cmd, s.doc().unwrap().doc.artboards.iter().map(|a| a.id).collect::<Vec<_>>(), kept));
|
||
}
|
||
let max = Some(u32::MAX);
|
||
assert_eq!(
|
||
got,
|
||
[
|
||
("artboard.duplicate", vec![u32::MAX, 1], vec![vec![max], vec![Some(1)]]),
|
||
("artboard.move", vec![u32::MAX, 1], vec![vec![max], vec![Some(1)]]),
|
||
("edit.paste", vec![u32::MAX, 1], vec![vec![max], vec![]]),
|
||
]
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn swatch_groups_duplicate_sort() {
|
||
let mut s = session();
|
||
s.execute("swatch.new", &json!({"name": "Zed", "color": "#123456"})).unwrap();
|
||
s.execute("swatch.new", &json!({"name": "Alpha", "color": "#654321"})).unwrap();
|
||
let r = s.execute("swatch.newGroup", &json!({"name": "Mine", "swatches": ["Zed"], "colors": ["#ff0000"]})).unwrap();
|
||
assert_eq!(r["name"], "Mine");
|
||
let d = &s.doc().unwrap().doc;
|
||
let g = d.swatch_groups.iter().find(|g| g.name == "Mine").unwrap();
|
||
assert_eq!(g.swatches.len(), 2);
|
||
assert!(!d.swatches.iter().any(|x| x.name == "Zed"));
|
||
let r = s.execute("swatch.duplicate", &json!({"name": "Alpha"})).unwrap();
|
||
assert_eq!(r["name"], "Alpha copy");
|
||
s.execute("swatch.sortByName", &json!({})).unwrap();
|
||
let d = &s.doc().unwrap().doc;
|
||
assert!(d.swatches[0].name.starts_with('['));
|
||
let names: Vec<_> = d.swatches.iter().skip(1).map(|x| x.name.to_lowercase()).collect();
|
||
let mut sorted = names.clone();
|
||
sorted.sort();
|
||
assert_eq!(names, sorted);
|
||
}
|
||
|
||
#[test]
|
||
fn graphic_style_delete_duplicate() {
|
||
let mut s = session();
|
||
let id = rect(&mut s);
|
||
s.execute("select.set", &json!({"ids": [id.0]})).unwrap();
|
||
s.execute("graphicStyle.new", &json!({"name": "S1"})).unwrap();
|
||
assert_eq!(s.execute("graphicStyle.duplicate", &json!({"name": "S1"})).unwrap()["name"], "S1 copy");
|
||
s.execute("graphicStyle.delete", &json!({"name": "S1"})).unwrap();
|
||
let d = &s.doc().unwrap().doc;
|
||
assert!(d.graphic_styles.iter().all(|g| g.name != "S1"));
|
||
assert!(s.execute("graphicStyle.delete", &json!({"name": "nope"})).is_err());
|
||
}
|
||
|
||
#[test]
|
||
fn text_set_format() {
|
||
let mut s = session();
|
||
let r = s.execute("text.create", &json!({"x": 10, "y": 100, "text": "Hello"})).unwrap();
|
||
let id = NodeId(r["id"].as_u64().unwrap());
|
||
s.execute(
|
||
"text.setFormat",
|
||
&json!({"ids": [id.0], "kerning": 20, "baselineShift": 3, "hScale": 120, "rotation": 370, "underline": true, "spaceBefore": 6, "hyphenate": true}),
|
||
)
|
||
.unwrap();
|
||
let NodeKind::Text(t) = node(&s, id).kind else { panic!() };
|
||
let st = t.first_style();
|
||
assert_eq!(st.kerning, Some(20.0));
|
||
assert_eq!(st.baseline_shift, 3.0);
|
||
assert_eq!(st.h_scale, 120.0);
|
||
assert!((st.rotation - 10.0).abs() < 1e-9);
|
||
assert!(st.underline);
|
||
assert_eq!(t.para.space_before, 6.0);
|
||
assert!(t.para.hyphenate);
|
||
s.execute("text.setFormat", &json!({"ids": [id.0], "kerning": "auto"})).unwrap();
|
||
let NodeKind::Text(t) = node(&s, id).kind else { panic!() };
|
||
assert_eq!(t.first_style().kerning, None);
|
||
assert!(s.execute("text.setFormat", &json!({"ids": [id.0]})).is_err());
|
||
// Paragraph composer: Every-line by default, undoable.
|
||
assert_eq!(t.para.composer, vectorcraft_doc::Composer::EveryLine);
|
||
s.execute("text.setFormat", &json!({"ids": [id.0], "composer": "singleLine"})).unwrap();
|
||
let NodeKind::Text(t) = node(&s, id).kind else { panic!() };
|
||
assert_eq!(t.para.composer, vectorcraft_doc::Composer::SingleLine);
|
||
assert!(s.execute("text.setFormat", &json!({"ids": [id.0], "composer": "best"})).is_err());
|
||
assert!(s.execute("text.setFormat", &json!({"ids": [id.0], "composer": 1})).is_err());
|
||
s.execute("edit.undo", &json!({})).unwrap();
|
||
let NodeKind::Text(t) = node(&s, id).kind else { panic!() };
|
||
assert_eq!(t.para.composer, vectorcraft_doc::Composer::EveryLine);
|
||
}
|
||
|
||
#[test]
|
||
fn stroke_advanced_scale_swap_flip() {
|
||
let mut s = session();
|
||
let id = rect(&mut s);
|
||
s.execute("select.set", &json!({"ids": [id.0]})).unwrap();
|
||
s.execute("stroke.set", &json!({"startArrow": "Triangle", "profile": "taperEnd"})).unwrap();
|
||
s.execute("stroke.setAdvanced", &json!({"arrowScale": [50, 200]})).unwrap();
|
||
s.execute("stroke.setAdvanced", &json!({"swapArrows": true, "flipProfile": "along"})).unwrap();
|
||
let n = node(&s, id);
|
||
let st = n.appearance.stroke().unwrap();
|
||
assert_eq!(st.start_arrow, None);
|
||
assert_eq!(st.end_arrow, Some(vectorcraft_doc::Arrowhead::Triangle));
|
||
assert_eq!(st.arrow_scale, (200.0, 50.0));
|
||
assert_eq!(st.profile.as_ref().unwrap().points, vectorcraft_doc::WidthProfile::taper_start().points);
|
||
assert!(s.execute("stroke.setAdvanced", &json!({})).is_err());
|
||
assert!(s.execute("stroke.setAdvanced", &json!({"flipProfile": "sideways"})).is_err());
|
||
}
|