mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-13 22:21:59 +00:00
Copy now remembers the whole chain of layers around each object, not only the name of the nearest one. With Paste Remembers Layers on: - In the same document, art goes back into the sublayer it came from while that sublayer exists, even after it was moved, so two sublayers of the same name in different layers stay apart. A deleted sublayer is made again inside the layer that held it. - In another document, the sublayer and the layers around it are matched by name from the top level down; of repeated names, the topmost chain that matches the most names wins, a shown and unlocked one first. A missing sublayer is made inside its layer, and a missing layer on top with the sublayer inside it, in the paste's undo step. - When the matched layer, or a layer around it, is hidden or locked, the paste fails with "the target layer is locked or hidden" and nothing is pasted, instead of putting art in a hidden layer or making a second layer of the same name.
1481 lines
75 KiB
Rust
1481 lines
75 KiB
Rust
//! Copy and paste between documents (resources, swatch conflicts) and paste placement (M4.22,
|
||
//! M4.23).
|
||
|
||
use serde_json::{Value, json};
|
||
use vectorcraft_color::{Color, Paint};
|
||
use vectorcraft_doc::{Document, Node, NodeKind};
|
||
use vectorcraft_geom::{Point, Rect};
|
||
|
||
use crate::*;
|
||
|
||
fn session() -> Session {
|
||
let mut s = Session::new();
|
||
s.execute("file.new", &json!({"width": 400, "height": 300})).unwrap();
|
||
s
|
||
}
|
||
|
||
/// A second document in the same session (as when copying between windows); it becomes active.
|
||
fn new_doc(s: &mut Session) {
|
||
s.execute("file.new", &json!({"width": 400, "height": 300})).unwrap();
|
||
}
|
||
|
||
fn id_of(v: &Value) -> NodeId {
|
||
NodeId(v["id"].as_u64().unwrap())
|
||
}
|
||
|
||
fn ids_of(v: &Value) -> Vec<NodeId> {
|
||
v["ids"].as_array().unwrap().iter().map(|i| NodeId(i.as_u64().unwrap())).collect()
|
||
}
|
||
|
||
fn rect(s: &mut Session, x: f64, y: f64, w: f64, h: f64) -> NodeId {
|
||
id_of(&s.execute("shape.rectangle", &json!({"x": x, "y": y, "width": w, "height": h})).unwrap())
|
||
}
|
||
|
||
fn ellipse(s: &mut Session) -> NodeId {
|
||
id_of(&s.execute("shape.ellipse", &json!({"x": 300, "y": 200, "width": 40, "height": 20})).unwrap())
|
||
}
|
||
|
||
fn sel(s: &mut Session, ids: &[NodeId]) {
|
||
s.execute("select.set", &json!({"ids": ids.iter().map(|i| i.0).collect::<Vec<_>>()})).unwrap();
|
||
}
|
||
|
||
fn copy(s: &mut Session, ids: &[NodeId]) {
|
||
sel(s, ids);
|
||
s.execute("edit.copy", &json!({})).unwrap();
|
||
}
|
||
|
||
fn doc(s: &Session) -> &Document {
|
||
&s.doc().unwrap().doc
|
||
}
|
||
|
||
fn node(s: &Session, id: NodeId) -> Node {
|
||
doc(s).node(id).unwrap().clone()
|
||
}
|
||
|
||
fn fill(s: &Session, id: NodeId) -> Paint {
|
||
node(s, id).appearance.fill_paint()
|
||
}
|
||
|
||
fn linked(color: Color, swatch: &str, tint: f32) -> Paint {
|
||
Paint::Solid { color, swatch: Some(swatch.into()), tint }
|
||
}
|
||
|
||
fn hex(h: &str) -> Color {
|
||
Color::from_hex(h).unwrap()
|
||
}
|
||
|
||
fn render(s: &Session) -> Vec<u8> {
|
||
vectorcraft_render::Renderer::new().render_region(doc(s), Rect::new(0.0, 0.0, 400.0, 300.0), 1.0, true).pixels
|
||
}
|
||
|
||
/// A global swatch `name` of `hex` and a rectangle filled with it.
|
||
fn swatched_rect(s: &mut Session, name: &str, hex: &str) -> NodeId {
|
||
s.execute("swatch.new", &json!({"name": name, "color": hex, "global": true})).unwrap();
|
||
let r = rect(s, 20.0, 20.0, 40.0, 40.0);
|
||
s.execute("paint.setFill", &json!({"ids": [r.0], "swatch": name})).unwrap();
|
||
r
|
||
}
|
||
|
||
fn renamed(v: &Value) -> Vec<(String, String, String)> {
|
||
let s = |r: &Value, k: &str| r[k].as_str().unwrap().to_string();
|
||
v["renamed"].as_array().unwrap().iter().map(|r| (s(r, "kind"), s(r, "from"), s(r, "to"))).collect()
|
||
}
|
||
|
||
// ---------- M4.22: resources ----------
|
||
|
||
/// Verify ordinary paste retains image, symbol and pattern resources and their rendered appearance.
|
||
#[test]
|
||
fn an_image_symbol_and_pattern_rect_pasted_into_a_new_document_render_the_same() {
|
||
resource_transfer(false);
|
||
}
|
||
|
||
/// Verify a translated artwork drop preserves editable resources, pattern phase, opacity and clipboard contents.
|
||
#[test]
|
||
fn dropped_artwork_preserves_editable_images_symbols_patterns_and_clipboard() {
|
||
resource_transfer(true);
|
||
}
|
||
|
||
/// Exercise paste or tab-drop resource transfer and compare source and destination rendering.
|
||
fn resource_transfer(drag: bool) {
|
||
let mut s = session();
|
||
// Pattern "Dots" painting a rectangle.
|
||
let dot = rect(&mut s, 0.0, 0.0, 10.0, 10.0);
|
||
s.execute("paint.setFill", &json!({"ids": [dot.0], "color": "#ff0000"})).unwrap();
|
||
sel(&mut s, &[dot]);
|
||
s.execute("object.pattern.make", &json!({"name": "Dots", "width": 20, "height": 20})).unwrap();
|
||
s.execute("object.pattern.done", &json!({})).unwrap();
|
||
sel(&mut s, &[dot]);
|
||
s.execute("edit.clear", &json!({})).unwrap();
|
||
let patterned = rect(&mut s, 20.0, 20.0, 100.0, 80.0);
|
||
s.execute("paint.setFill", &json!({"ids": [patterned.0], "swatch": "Dots"})).unwrap();
|
||
// Symbol "Badge" (a green square) placed once.
|
||
let badge = rect(&mut s, 150.0, 20.0, 50.0, 50.0);
|
||
s.execute("paint.setFill", &json!({"ids": [badge.0], "color": "#00aa44"})).unwrap();
|
||
sel(&mut s, &[badge]);
|
||
s.execute("symbol.new", &json!({"name": "Badge"})).unwrap();
|
||
// An embedded image (a rasterized blue ellipse).
|
||
let e = id_of(&s.execute("shape.ellipse", &json!({"x": 240, "y": 40, "width": 90, "height": 60})).unwrap());
|
||
s.execute("paint.setFill", &json!({"ids": [e.0], "color": "#2040ff"})).unwrap();
|
||
sel(&mut s, &[e]);
|
||
s.execute("object.rasterize", &json!({"ppi": 72})).unwrap();
|
||
s.execute("select.all", &json!({})).unwrap();
|
||
s.execute("transparency.set", &json!({"opacity":45})).unwrap();
|
||
s.execute("edit.copy", &json!({})).unwrap();
|
||
assert_eq!(s.clipboard.nodes.len(), 3);
|
||
assert_eq!((s.clipboard.symbols.len(), s.clipboard.patterns.len(), s.clipboard.images.len()), (1, 1, 1));
|
||
let before = render(&s);
|
||
let source = s.doc().unwrap().clone();
|
||
let ids = source.selection.objects.clone();
|
||
new_doc(&mut s);
|
||
assert!(before != render(&s));
|
||
let r = if drag {
|
||
s.clipboard = Clipboard::default();
|
||
s.execute("edit.dropSelection", &json!({"sourceDocument": source.uid, "ids": ids, "grab": [0,0], "at": [13,17]})).unwrap()
|
||
} else {
|
||
s.execute("edit.pasteInPlace", &json!({})).unwrap()
|
||
};
|
||
if drag {
|
||
assert_eq!(s.clipboard, Clipboard::default());
|
||
}
|
||
assert_eq!(s.documents()[0].doc, source.doc);
|
||
// The pattern (its swatch comes along), the symbol and the image blob.
|
||
assert_eq!((r["added"].as_u64(), r["merged"].as_u64()), (Some(3), Some(0)), "{r}");
|
||
let d = doc(&s);
|
||
assert!(d.pattern("Dots").is_some() && d.swatch("Dots").is_some());
|
||
assert!(d.symbols.iter().any(|x| x.name == "Badge"));
|
||
assert_eq!(d.images.len(), 1);
|
||
let after = if drag {
|
||
vectorcraft_render::Renderer::new().render_region(doc(&s), Rect::new(13.0, 17.0, 413.0, 317.0), 1.0, true).pixels
|
||
} else {
|
||
render(&s)
|
||
};
|
||
assert!(before == after, "the transferred art, its transparency and pattern alignment render as in the source");
|
||
}
|
||
|
||
#[test]
|
||
fn a_spot_swatch_brings_its_plate() {
|
||
let mut s = session();
|
||
let r = swatched_rect(&mut s, "Spot Teal", "#008080");
|
||
s.execute("swatch.setSpot", &json!({"name": "Spot Teal"})).unwrap();
|
||
copy(&mut s, &[r]);
|
||
new_doc(&mut s);
|
||
let plates = |s: &Session| vectorcraft_render::proof::plates(doc(s)).iter().filter(|p| p.spot).map(|p| p.name.clone()).collect::<Vec<_>>();
|
||
assert!(plates(&s).is_empty());
|
||
s.execute("edit.paste", &json!({})).unwrap();
|
||
assert_eq!(plates(&s), ["Spot Teal"]);
|
||
let sw = doc(&s).swatch("Spot Teal").unwrap();
|
||
assert!(sw.spot && sw.global);
|
||
}
|
||
|
||
#[test]
|
||
fn swatch_conflicts_merge_relinks_and_add_renames() {
|
||
let mut s = session();
|
||
let r = swatched_rect(&mut s, "Brand", "#ff0000");
|
||
copy(&mut s, &[r]);
|
||
new_doc(&mut s);
|
||
s.execute("swatch.new", &json!({"name": "Brand", "color": "#0000ff", "global": true})).unwrap();
|
||
let c = s.execute("clipboard.conflicts", &json!({})).unwrap();
|
||
assert_eq!(c["swatches"], json!([{"name": "Brand", "document": "#0000ff", "clipboard": "#ff0000", "spot": false}]));
|
||
let swatches = doc(&s).swatches.len();
|
||
|
||
// Merge (the default): the pasted object takes the document's Brand.
|
||
let v = s.execute("edit.pasteInPlace", &json!({"swatchConflict": "merge"})).unwrap();
|
||
assert_eq!((v["merged"].as_u64(), v["added"].as_u64()), (Some(1), Some(0)));
|
||
assert_eq!(fill(&s, ids_of(&v)[0]), linked(hex("#0000ff"), "Brand", 1.0));
|
||
assert_eq!(doc(&s).swatches.len(), swatches);
|
||
s.execute("edit.undo", &json!({})).unwrap();
|
||
|
||
// Add: the pasted swatch joins under a new name and the object follows it.
|
||
let v = s.execute("edit.pasteInPlace", &json!({"swatchConflict": "add"})).unwrap();
|
||
assert_eq!(v["renamed"], json!([{"kind": "swatch", "from": "Brand", "to": "Brand 2"}]));
|
||
assert_eq!(fill(&s, ids_of(&v)[0]), linked(hex("#ff0000"), "Brand 2", 1.0));
|
||
let added = doc(&s).swatch("Brand 2").unwrap();
|
||
assert!(added.global && added.paint.color() == Some(hex("#ff0000")));
|
||
s.execute("edit.undo", &json!({})).unwrap();
|
||
|
||
// Per swatch name; the others merge.
|
||
let v = s.execute("edit.paste", &json!({"swatchConflict": {"Other": "add"}})).unwrap();
|
||
assert_eq!(v["merged"], 1);
|
||
assert!(s.execute("edit.paste", &json!({"swatchConflict": "keep"})).is_err());
|
||
assert!(s.execute("edit.paste", &json!({"swatchConflict": {"Brand": 1}})).is_err());
|
||
}
|
||
|
||
#[test]
|
||
fn tints_and_their_tint_swatches_follow_the_base() {
|
||
let mut s = session();
|
||
s.execute("swatch.new", &json!({"name": "Ink", "color": {"c": 0, "m": 100, "y": 100, "k": 0}, "spot": true})).unwrap();
|
||
let r = rect(&mut s, 0.0, 0.0, 10.0, 10.0);
|
||
s.execute("paint.setFill", &json!({"ids": [r.0], "swatch": "Ink", "tint": 40})).unwrap();
|
||
// The 40% tint swatch, and one of another tint the art doesn't use.
|
||
assert_eq!(s.execute("swatch.new", &json!({})).unwrap()["name"], "Ink 40%");
|
||
s.execute("paint.setFill", &json!({"ids": [r.0], "swatch": "Ink", "tint": 70})).unwrap();
|
||
s.execute("swatch.new", &json!({})).unwrap();
|
||
s.execute("paint.setFill", &json!({"ids": [r.0], "swatch": "Ink", "tint": 40})).unwrap();
|
||
copy(&mut s, &[r]);
|
||
assert_eq!(s.clipboard.swatches.iter().map(|w| w.name.as_str()).collect::<Vec<_>>(), ["Ink", "Ink 40%"]);
|
||
|
||
// Into a document whose Ink is cyan: merged, the tint is 40% of the document's Ink.
|
||
new_doc(&mut s);
|
||
s.execute("swatch.new", &json!({"name": "Ink", "color": {"c": 100, "m": 0, "y": 0, "k": 0}, "spot": true})).unwrap();
|
||
let v = s.execute("edit.pasteInPlace", &json!({})).unwrap();
|
||
let cyan = Color::cmyk(1.0, 0.0, 0.0, 0.0);
|
||
assert_eq!(fill(&s, ids_of(&v)[0]), linked(cyan.tinted(0.4), "Ink", 0.4));
|
||
let tint = doc(&s).swatch("Ink 40%").unwrap();
|
||
assert_eq!(tint.paint, linked(cyan.tinted(0.4), "Ink", 0.4), "the tint swatch links to the document's Ink");
|
||
assert!(doc(&s).swatch("Ink 70%").is_none(), "unused tints stay behind");
|
||
s.execute("edit.undo", &json!({})).unwrap();
|
||
|
||
// Added: the tint and its tint swatch follow the renamed base.
|
||
let v = s.execute("edit.pasteInPlace", &json!({"swatchConflict": "add"})).unwrap();
|
||
let magenta = Color::cmyk(0.0, 1.0, 1.0, 0.0);
|
||
assert_eq!(fill(&s, ids_of(&v)[0]), linked(magenta.tinted(0.4), "Ink 2", 0.4));
|
||
assert_eq!(doc(&s).swatch("Ink 40%").unwrap().paint, linked(magenta.tinted(0.4), "Ink 2", 0.4));
|
||
}
|
||
|
||
#[test]
|
||
fn gradient_stops_stay_linked_to_their_swatches() {
|
||
let mut s = session();
|
||
s.execute("swatch.new", &json!({"name": "Sky", "color": "#3080ff", "global": true})).unwrap();
|
||
let r = rect(&mut s, 0.0, 0.0, 50.0, 50.0);
|
||
s.execute("paint.setFill", &json!({"ids": [r.0], "gradient": {"kind": "linear"}})).unwrap();
|
||
s.edit("link stop", |d, _| {
|
||
let n = d.node_mut(r).unwrap();
|
||
let Paint::Gradient(mut g) = n.appearance.fill_paint() else { panic!() };
|
||
g.gradient.stops[0].set_color(Color::from_hex("#3080ff").unwrap(), Some(("Sky".into(), 1.0)));
|
||
n.appearance.set_fill(Paint::Gradient(g));
|
||
Ok(())
|
||
})
|
||
.unwrap();
|
||
copy(&mut s, &[r]);
|
||
assert!(s.clipboard.swatches.iter().any(|w| w.name == "Sky"));
|
||
new_doc(&mut s);
|
||
s.execute("swatch.new", &json!({"name": "Sky", "color": "#000080", "global": true})).unwrap();
|
||
let v = s.execute("edit.paste", &json!({"swatchConflict": "add"})).unwrap();
|
||
let Paint::Gradient(g) = fill(&s, ids_of(&v)[0]) else { panic!() };
|
||
assert_eq!((g.gradient.stops[0].swatch.as_deref(), g.gradient.stops[0].color), (Some("Sky 2"), hex("#3080ff")));
|
||
assert!(doc(&s).swatch("Sky 2").is_some());
|
||
}
|
||
|
||
#[test]
|
||
fn gradient_swatches_come_along_with_their_stop_links() {
|
||
let mut s = session();
|
||
s.execute("swatch.new", &json!({"name": "Sky", "color": "#3080ff", "global": true})).unwrap();
|
||
let glow = json!({"stops": [{"offset": 0, "swatch": "Sky"}, {"offset": 1, "color": "#ffffff"}]});
|
||
s.execute("swatch.new", &json!({"name": "Glow", "gradient": glow})).unwrap();
|
||
let r = rect(&mut s, 0.0, 0.0, 50.0, 50.0);
|
||
s.execute("paint.setFill", &json!({"ids": [r.0], "swatch": "Glow"})).unwrap();
|
||
copy(&mut s, &[r]);
|
||
assert_eq!(s.clipboard.swatches.iter().map(|w| w.name.as_str()).collect::<Vec<_>>(), ["Sky", "Glow"]);
|
||
// A document with another Glow and another Sky.
|
||
new_doc(&mut s);
|
||
s.execute("swatch.new", &json!({"name": "Sky", "color": "#000080", "global": true})).unwrap();
|
||
s.execute("swatch.new", &json!({"name": "Glow", "gradient": {"stops": [{"offset": 0, "color": "#ff0000"}, {"offset": 1, "color": "#000000"}]}}))
|
||
.unwrap();
|
||
let v = s.execute("edit.paste", &json!({"swatchConflict": "add"})).unwrap();
|
||
let Paint::Gradient(g) = fill(&s, ids_of(&v)[0]) else { panic!() };
|
||
assert_eq!((g.swatch.as_deref(), g.gradient.stops[0].swatch.as_deref()), (Some("Glow 2"), Some("Sky 2")));
|
||
let Paint::Gradient(sw) = doc(&s).swatch("Glow 2").unwrap().paint.clone() else { panic!() };
|
||
assert_eq!(sw.gradient, g.gradient, "the added gradient swatch links its stop to the added Sky too");
|
||
}
|
||
|
||
/// A gradient swatch made from another one (by name, from a fill linked to it, or from a gradient
|
||
/// that names it, placed or not) holds the gradient alone, so copying art that uses it brings only
|
||
/// that swatch. A placed gradient keeps its aspect, and the swatch still fits each object it is
|
||
/// applied to.
|
||
#[test]
|
||
fn a_gradient_swatch_made_from_another_holds_no_link_to_it() {
|
||
let mut s = session();
|
||
let glow = json!({"kind": "radial", "stops": [{"offset": 0, "color": "#3080ff"}, {"offset": 1, "color": "#ffffff"}]});
|
||
s.execute("swatch.new", &json!({"name": "Glow", "gradient": glow})).unwrap();
|
||
let r = rect(&mut s, 0.0, 0.0, 50.0, 50.0);
|
||
s.execute("paint.setFill", &json!({"ids": [r.0], "swatch": "Glow"})).unwrap();
|
||
let Paint::Gradient(applied) = fill(&s, r) else { panic!("a gradient fill") };
|
||
assert_eq!(applied.swatch.as_deref(), Some("Glow"));
|
||
let mut by_gradient = vectorcraft_tools::params::gradient_params(&applied);
|
||
by_gradient["swatch"] = json!("Glow");
|
||
let mut placed = by_gradient.clone();
|
||
placed["start"] = json!([25, 25]);
|
||
placed["end"] = json!([50, 25]);
|
||
placed["aspect"] = json!(50);
|
||
let made = [
|
||
("By Name", json!({"name": "By Name", "swatch": "Glow"})),
|
||
("From Fill", json!({"name": "From Fill"})),
|
||
("From Gradient", json!({"name": "From Gradient", "gradient": by_gradient})),
|
||
("Placed", json!({"name": "Placed", "gradient": placed})),
|
||
];
|
||
let mut links = vec![];
|
||
for (name, p) in &made {
|
||
s.execute("swatch.new", p).unwrap();
|
||
let Paint::Gradient(sw) = doc(&s).swatch(name).unwrap().paint.clone() else { panic!("{name}: a gradient swatch") };
|
||
assert_eq!(sw.gradient, applied.gradient, "{name}: Glow's gradient");
|
||
links.push(sw.swatch.clone());
|
||
}
|
||
assert_eq!(links, [None, None, None, None], "no swatch links to Glow");
|
||
let Paint::Gradient(sw) = doc(&s).swatch("Placed").unwrap().paint.clone() else { panic!("a gradient swatch") };
|
||
assert!(sw.geom.is_some_and(|g| (g.aspect - 0.5).abs() < 1e-9), "the placed gradient keeps its aspect: {:?}", sw.geom);
|
||
for (name, _) in &made {
|
||
let t = rect(&mut s, 100.0, 0.0, 100.0, 60.0);
|
||
s.execute("paint.setFill", &json!({"ids": [t.0], "swatch": name})).unwrap();
|
||
if *name == "Placed" {
|
||
let Paint::Gradient(g) = fill(&s, t) else { panic!("a gradient fill") };
|
||
let geom = g.geom.unwrap();
|
||
assert!(geom.start.distance(Point::new(150.0, 30.0)) < 1e-9 && (geom.aspect - 0.5).abs() < 1e-9, "{geom:?}");
|
||
}
|
||
copy(&mut s, &[t]);
|
||
assert_eq!(s.clipboard.swatches.iter().map(|w| w.name.as_str()).collect::<Vec<_>>(), [*name], "Glow stays behind");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn lab_spot_colours_show_as_the_target_documents_spot_option_says() {
|
||
let mut s = session();
|
||
let lab = Color::lab(60.0, 50.0, 20.0);
|
||
s.execute("swatch.new", &json!({"name": "Lab Ink", "color": {"l": 60, "a": 50, "b": 20}, "spot": true})).unwrap();
|
||
let r = rect(&mut s, 0.0, 0.0, 10.0, 10.0);
|
||
s.execute("paint.setFill", &json!({"ids": [r.0], "swatch": "Lab Ink"})).unwrap();
|
||
assert_eq!(fill(&s, r).color(), Some(lab));
|
||
copy(&mut s, &[r]);
|
||
new_doc(&mut s);
|
||
s.execute("swatch.spotOptions", &json!({"useLab": false})).unwrap();
|
||
let id = ids_of(&s.execute("edit.paste", &json!({})).unwrap())[0];
|
||
let cmyk = doc(&s).global_color("Lab Ink").unwrap();
|
||
assert!(matches!(cmyk, Color::Cmyk { .. }));
|
||
assert_eq!(fill(&s, id), linked(cmyk, "Lab Ink", 1.0), "the CMYK equivalent, still linked");
|
||
assert_eq!(doc(&s).swatch("Lab Ink").unwrap().paint.color(), Some(lab), "the swatch keeps its Lab definition");
|
||
}
|
||
|
||
#[test]
|
||
fn registration_is_never_copied_as_a_swatch() {
|
||
let mut s = session();
|
||
let r = rect(&mut s, 0.0, 0.0, 10.0, 10.0);
|
||
s.edit("registration", |d, _| {
|
||
d.node_mut(r).unwrap().appearance.set_fill(Paint::registration());
|
||
Ok(())
|
||
})
|
||
.unwrap();
|
||
copy(&mut s, &[r]);
|
||
assert!(s.clipboard.swatches.is_empty());
|
||
new_doc(&mut s);
|
||
let swatches = doc(&s).swatches.len();
|
||
let v = s.execute("edit.paste", &json!({})).unwrap();
|
||
assert_eq!(v["added"], 0);
|
||
assert_eq!(doc(&s).swatches.len(), swatches);
|
||
assert!(fill(&s, ids_of(&v)[0]).is_registration());
|
||
}
|
||
|
||
#[test]
|
||
fn graphic_style_links_follow_the_style_into_the_document() {
|
||
let mut s = session();
|
||
let a = rect(&mut s, 0.0, 0.0, 30.0, 30.0);
|
||
s.execute("paint.setFill", &json!({"ids": [a.0], "color": "#ffcc00"})).unwrap();
|
||
sel(&mut s, &[a]);
|
||
s.execute("graphicStyle.new", &json!({"name": "Sun"})).unwrap();
|
||
let src_id = node(&s, a).extras().graphic_style.unwrap();
|
||
copy(&mut s, &[a]);
|
||
assert_eq!(s.clipboard.graphic_styles.iter().map(|g| g.name.as_str()).collect::<Vec<_>>(), ["Sun"]);
|
||
|
||
// A document with another "Sun" and styles taking up the source id.
|
||
new_doc(&mut s);
|
||
let b = rect(&mut s, 0.0, 0.0, 10.0, 10.0);
|
||
s.execute("paint.setFill", &json!({"ids": [b.0], "color": "#0000ff"})).unwrap();
|
||
sel(&mut s, &[b]);
|
||
s.execute("graphicStyle.new", &json!({"name": "Sun"})).unwrap();
|
||
let v = s.execute("edit.paste", &json!({})).unwrap();
|
||
assert_eq!(renamed(&v), [("graphicStyle".to_string(), "Sun".to_string(), "Sun 2".to_string())]);
|
||
let d = doc(&s);
|
||
let linked_to = d.graphic_style_by_id(node(&s, ids_of(&v)[0]).extras().graphic_style.unwrap()).unwrap().clone();
|
||
assert_eq!(linked_to.name, "Sun 2");
|
||
assert_eq!(linked_to.appearance.fill_paint(), Paint::solid(hex("#ffcc00")));
|
||
assert!(d.graphic_styles.iter().filter(|g| g.id == linked_to.id).count() == 1, "a fresh id");
|
||
// Pasting again finds the same style.
|
||
let v = s.execute("edit.paste", &json!({})).unwrap();
|
||
assert_eq!((v["added"].as_u64(), node(&s, ids_of(&v)[0]).extras().graphic_style), (Some(0), Some(linked_to.id)));
|
||
// Back in the source document the link is kept as it was.
|
||
s.execute("document.activate", &json!({"index": 0})).unwrap();
|
||
let v = s.execute("edit.paste", &json!({})).unwrap();
|
||
assert_eq!(node(&s, ids_of(&v)[0]).extras().graphic_style, Some(src_id));
|
||
}
|
||
|
||
#[test]
|
||
fn undo_removes_everything_the_paste_added() {
|
||
let mut s = session();
|
||
let r = swatched_rect(&mut s, "Brand", "#ff0000");
|
||
sel(&mut s, &[r]);
|
||
s.execute("symbol.new", &json!({"name": "Mark"})).unwrap();
|
||
s.execute("select.all", &json!({})).unwrap();
|
||
s.execute("edit.copy", &json!({})).unwrap();
|
||
new_doc(&mut s);
|
||
let before = doc(&s).clone();
|
||
let v = s.execute("edit.paste", &json!({})).unwrap();
|
||
assert_eq!(v["added"], 2, "{v}");
|
||
assert!(doc(&s).swatch("Brand").is_some() && !doc(&s).symbols.is_empty());
|
||
s.execute("edit.undo", &json!({})).unwrap();
|
||
assert!(*doc(&s) == before);
|
||
}
|
||
|
||
#[test]
|
||
fn styles_symbols_and_images_of_the_same_name_are_renamed() {
|
||
let mut s = session();
|
||
// A character style, a symbol and an image named like the target's own.
|
||
let t = id_of(&s.execute("text.create", &json!({"x": 10, "y": 50, "text": "Hello"})).unwrap());
|
||
s.execute("charStyle.new", &json!({"name": "Emphasis", "attrs": {"size": 24.0}})).unwrap();
|
||
s.execute("charStyle.apply", &json!({"name": "Emphasis", "id": t.0})).unwrap();
|
||
let m = rect(&mut s, 200.0, 100.0, 30.0, 30.0);
|
||
sel(&mut s, &[m]);
|
||
let mark = id_of(&s.execute("symbol.new", &json!({"name": "Mark"})).unwrap());
|
||
let e = rect(&mut s, 300.0, 100.0, 20.0, 20.0);
|
||
sel(&mut s, &[e]);
|
||
let img = id_of(&s.execute("object.rasterize", &json!({"ppi": 72})).unwrap());
|
||
let key = |s: &Session, id: NodeId| match node(s, id).kind {
|
||
NodeKind::Image(im) => im.key,
|
||
_ => panic!("not an image"),
|
||
};
|
||
let src_key = key(&s, img);
|
||
copy(&mut s, &[t, mark, img]);
|
||
|
||
new_doc(&mut s);
|
||
s.execute("charStyle.new", &json!({"name": "Emphasis", "attrs": {"size": 8.0}})).unwrap();
|
||
let m2 = ellipse(&mut s);
|
||
sel(&mut s, &[m2]);
|
||
s.execute("symbol.new", &json!({"name": "Mark"})).unwrap();
|
||
// Another image under the same key.
|
||
let e2 = ellipse(&mut s);
|
||
sel(&mut s, &[e2]);
|
||
let img2 = id_of(&s.execute("object.rasterize", &json!({"ppi": 72})).unwrap());
|
||
let other_key = key(&s, img2);
|
||
s.edit("same key", |d, _| {
|
||
let blob = d.images.remove(&other_key).unwrap();
|
||
d.images.insert(src_key.clone(), blob);
|
||
let NodeKind::Image(im) = &mut d.node_mut(img2).unwrap().kind else { panic!() };
|
||
im.key = src_key.clone();
|
||
Ok(())
|
||
})
|
||
.unwrap();
|
||
|
||
let v = s.execute("edit.pasteInPlace", &json!({})).unwrap();
|
||
let kinds: Vec<(String, String)> = renamed(&v).into_iter().map(|(k, _, to)| (k, to)).collect();
|
||
for want in [("charStyle", "Emphasis 2"), ("symbol", "Mark 2"), ("image", &*format!("{src_key} 2"))] {
|
||
assert!(kinds.contains(&(want.0.to_string(), want.1.to_string())), "{want:?} in {kinds:?}");
|
||
}
|
||
let ids = ids_of(&v);
|
||
let NodeKind::Text(tx) = node(&s, ids[0]).kind else { panic!() };
|
||
assert_eq!(tx.runs[0].style.style_name.as_deref(), Some("Emphasis 2"));
|
||
assert!(matches!(node(&s, ids[1]).kind, NodeKind::SymbolInstance { ref symbol, .. } if symbol == "Mark 2"));
|
||
let new_key = key(&s, ids[2]);
|
||
assert_ne!(new_key, src_key, "a different image under the same key is re-keyed");
|
||
assert!(doc(&s).images[&new_key].bytes != doc(&s).images[&src_key].bytes);
|
||
// Their own objects are untouched.
|
||
assert_eq!(key(&s, img2), src_key);
|
||
}
|
||
|
||
#[test]
|
||
fn pasting_again_reuses_what_an_earlier_paste_added() {
|
||
let mut s = session();
|
||
let r = swatched_rect(&mut s, "Brand", "#ff0000");
|
||
// A character style whose fill links to Brand.
|
||
let t = id_of(&s.execute("text.create", &json!({"x": 10, "y": 50, "text": "Hi"})).unwrap());
|
||
let red = serde_json::to_value(linked(hex("#ff0000"), "Brand", 1.0)).unwrap();
|
||
s.execute("charStyle.new", &json!({"name": "Loud", "attrs": {"fill": red}})).unwrap();
|
||
s.execute("charStyle.apply", &json!({"name": "Loud", "id": t.0})).unwrap();
|
||
copy(&mut s, &[r, t]);
|
||
new_doc(&mut s);
|
||
s.execute("swatch.new", &json!({"name": "Brand", "color": "#0000ff", "global": true})).unwrap();
|
||
let v = s.execute("edit.paste", &json!({"swatchConflict": "add"})).unwrap();
|
||
assert_eq!(v["added"], 2, "Brand 2 and Loud: {v}");
|
||
let style_fill = |s: &Session| serde_json::from_value::<Paint>(doc(s).char_styles[0].attrs["fill"].clone()).unwrap();
|
||
assert_eq!(style_fill(&s), linked(hex("#ff0000"), "Brand 2", 1.0), "the style's colour follows the renamed swatch");
|
||
// Again: Brand 2 and Loud are the pasted ones, nothing piles up.
|
||
let v = s.execute("edit.paste", &json!({"swatchConflict": "add"})).unwrap();
|
||
assert_eq!(v["added"], 0, "{v}");
|
||
assert_eq!(renamed(&v), [("swatch".to_string(), "Brand".to_string(), "Brand 2".to_string())]);
|
||
assert_eq!(fill(&s, ids_of(&v)[0]), linked(hex("#ff0000"), "Brand 2", 1.0));
|
||
assert!(doc(&s).swatch("Brand 3").is_none() && doc(&s).char_styles.len() == 1);
|
||
}
|
||
|
||
/// The active document, to edit its definitions directly.
|
||
fn doc_mut(s: &mut Session) -> &mut Document {
|
||
std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().doc)
|
||
}
|
||
|
||
/// The pattern a paint paints with.
|
||
fn pattern_of(p: &Paint) -> Option<&str> {
|
||
match p {
|
||
Paint::Pattern { pattern, .. } => Some(pattern),
|
||
_ => None,
|
||
}
|
||
}
|
||
|
||
/// The pattern the fill of the active document's character style `name` paints with.
|
||
fn style_pattern(s: &Session, name: &str) -> Option<String> {
|
||
let attrs = &doc(s).char_styles.iter().find(|x| x.name == name)?.attrs;
|
||
pattern_of(&serde_json::from_value::<Paint>(attrs.get("fill")?.clone()).ok()?).map(str::to_string)
|
||
}
|
||
|
||
/// A pattern "Dots" made from a `size` square of `color` in the active document.
|
||
fn dots(s: &mut Session, size: f64, color: &str) {
|
||
let square = rect(s, 300.0, 200.0, size, size);
|
||
s.execute("paint.setFill", &json!({"ids": [square.0], "color": color})).unwrap();
|
||
s.execute("object.pattern.make", &json!({"name": "Dots", "edit": false, "ids": [square.0]})).unwrap();
|
||
}
|
||
|
||
/// Type "Hi" filled with the pattern "Dots" (a small red square), set in a character style
|
||
/// "Patterned" whose fill is that pattern.
|
||
fn patterned_type(s: &mut Session) -> NodeId {
|
||
dots(s, 10.0, "#ff0000");
|
||
let t = id_of(&s.execute("text.create", &json!({"x": 10, "y": 50, "text": "Hi"})).unwrap());
|
||
s.execute("paint.setFill", &json!({"ids": [t.0], "swatch": "Dots"})).unwrap();
|
||
s.execute("charStyle.new", &json!({"name": "Patterned", "id": t.0})).unwrap();
|
||
s.execute("charStyle.apply", &json!({"name": "Patterned", "id": t.0})).unwrap();
|
||
t
|
||
}
|
||
|
||
/// The character style name of a type object's first run, and the pattern that run paints with.
|
||
fn type_links(s: &Session, id: NodeId) -> (Option<String>, Option<String>) {
|
||
let NodeKind::Text(tx) = node(s, id).kind else { panic!("not type") };
|
||
(tx.runs[0].style.style_name.clone(), pattern_of(&tx.runs[0].style.fill).map(str::to_string))
|
||
}
|
||
|
||
/// Type set in a style that paints with a pattern, pasted into a document with another pattern of
|
||
/// that name: the pattern comes in as "Dots 2", and the style paints with "Dots 2" too, so applying
|
||
/// it later paints with the pasted pattern. Pasting again adds nothing.
|
||
#[test]
|
||
fn pasted_styles_paint_with_the_renamed_pattern_they_use() {
|
||
let mut s = session();
|
||
let t = patterned_type(&mut s);
|
||
copy(&mut s, &[t]);
|
||
new_doc(&mut s);
|
||
dots(&mut s, 30.0, "#0000ff");
|
||
let v = s.execute("edit.paste", &json!({})).unwrap();
|
||
assert!(doc(&s).pattern("Dots 2").is_some(), "{v}");
|
||
assert_eq!(style_pattern(&s, "Patterned").as_deref(), Some("Dots 2"), "{v}");
|
||
let named = |n: &str| Some(n.to_string());
|
||
assert_eq!(type_links(&s, ids_of(&v)[0]), (named("Patterned"), named("Dots 2")));
|
||
let other = id_of(&s.execute("text.create", &json!({"x": 10, "y": 150, "text": "Yo"})).unwrap());
|
||
s.execute("charStyle.apply", &json!({"name": "Patterned", "id": other.0})).unwrap();
|
||
assert_eq!(type_links(&s, other).1.as_deref(), Some("Dots 2"), "the applied style paints with the pasted pattern");
|
||
let counts = |s: &Session| (doc(s).patterns.len(), doc(s).char_styles.len());
|
||
let before = counts(&s);
|
||
let v = s.execute("edit.paste", &json!({})).unwrap();
|
||
assert_eq!(v["added"], 0, "{v}");
|
||
assert_eq!(counts(&s), before);
|
||
assert_eq!(type_links(&s, ids_of(&v)[0]), (named("Patterned"), named("Dots 2")));
|
||
}
|
||
|
||
/// A document whose "Patterned" style has the pasted style's attributes but whose "Dots" is
|
||
/// another pattern: that style paints with the document's "Dots", so the pasted one comes in as
|
||
/// "Patterned 2", painting with "Dots 2", and the document's own is left as it was. Pasting again
|
||
/// uses the numbered copies.
|
||
#[test]
|
||
fn pasted_styles_are_not_matched_to_styles_that_paint_with_another_pattern() {
|
||
let mut s = session();
|
||
let t = patterned_type(&mut s);
|
||
let style = doc(&s).char_styles.iter().find(|x| x.name == "Patterned").unwrap().clone();
|
||
copy(&mut s, &[t]);
|
||
new_doc(&mut s);
|
||
dots(&mut s, 30.0, "#0000ff");
|
||
doc_mut(&mut s).char_styles.push(style);
|
||
let own = (doc(&s).char_styles.clone(), doc(&s).pattern("Dots").cloned());
|
||
let v = s.execute("edit.paste", &json!({})).unwrap();
|
||
assert_eq!(style_pattern(&s, "Patterned 2").as_deref(), Some("Dots 2"), "{v}");
|
||
let d = doc(&s);
|
||
assert_eq!((&d.char_styles[..own.0.len()], d.pattern("Dots")), (&own.0[..], own.1.as_ref()), "the document's own are unchanged");
|
||
let named = |n: &str| Some(n.to_string());
|
||
let links = (named("Patterned 2"), named("Dots 2"));
|
||
assert_eq!(type_links(&s, ids_of(&v)[0]), links);
|
||
let counts = |s: &Session| (doc(s).patterns.len(), doc(s).char_styles.len());
|
||
let before = counts(&s);
|
||
let v = s.execute("edit.paste", &json!({})).unwrap();
|
||
assert_eq!(v["added"], 0, "{v}");
|
||
assert_eq!(counts(&s), before);
|
||
assert_eq!(type_links(&s, ids_of(&v)[0]), links);
|
||
}
|
||
|
||
/// A style that paints with a pattern whose tile sets type in that same style is pasted with the
|
||
/// pattern: when the pattern comes in as "Dots 2", the style paints with "Dots 2", and the tile's
|
||
/// type is set in the style. Pasting again adds nothing. The commands do not make that loop, so the
|
||
/// source is a document edited directly.
|
||
#[test]
|
||
fn a_pasted_style_and_the_pattern_whose_tile_uses_it_come_in_together() {
|
||
let mut s = session();
|
||
let t = id_of(&s.execute("text.create", &json!({"x": 10, "y": 50, "text": "Tile"})).unwrap());
|
||
s.execute("charStyle.new", &json!({"name": "Accent", "attrs": {"kerning": 40}})).unwrap();
|
||
s.execute("charStyle.apply", &json!({"name": "Accent", "id": t.0})).unwrap();
|
||
s.execute("object.pattern.make", &json!({"name": "Dots", "edit": false, "ids": [t.0]})).unwrap();
|
||
let other = id_of(&s.execute("text.create", &json!({"x": 10, "y": 90, "text": "B"})).unwrap());
|
||
s.execute("charStyle.apply", &json!({"name": "Accent", "id": other.0})).unwrap();
|
||
s.execute("paint.setFill", &json!({"swatch": "Dots", "ids": [other.0]})).unwrap();
|
||
s.execute("charStyle.new", &json!({"name": "Painted", "id": other.0})).unwrap();
|
||
let d = doc_mut(&mut s);
|
||
let painted = d.char_styles.iter().find(|x| x.name == "Painted").unwrap().attrs.clone();
|
||
d.char_styles.retain(|x| x.name != "Painted");
|
||
d.char_styles.iter_mut().find(|x| x.name == "Accent").unwrap().attrs = painted;
|
||
assert_eq!(style_pattern(&s, "Accent").as_deref(), Some("Dots"));
|
||
copy(&mut s, &[other]);
|
||
new_doc(&mut s);
|
||
dots(&mut s, 30.0, "#0000ff");
|
||
let v = s.execute("edit.paste", &json!({})).unwrap();
|
||
assert!(doc(&s).pattern("Dots 2").is_some(), "{v}");
|
||
assert_eq!(style_pattern(&s, "Accent").as_deref(), Some("Dots 2"), "{v}");
|
||
let NodeKind::Text(tile) = &doc(&s).pattern("Dots 2").unwrap().art[0].kind else { panic!("the tile is type") };
|
||
assert_eq!(tile.runs[0].style.style_name.as_deref(), Some("Accent"));
|
||
let after = doc(&s).clone();
|
||
let v = s.execute("edit.paste", &json!({})).unwrap();
|
||
assert_eq!(v["added"], 0, "{v}");
|
||
assert!(doc(&s).patterns == after.patterns && doc(&s).char_styles == after.char_styles, "nothing is added");
|
||
// Back in the source document, the loop is the document's own.
|
||
s.execute("document.activate", &json!({"index": 0})).unwrap();
|
||
let v = s.execute("edit.paste", &json!({})).unwrap();
|
||
assert_eq!((v["added"].as_u64(), v["renamed"].as_array().map(Vec::len)), (Some(0), Some(0)), "{v}");
|
||
}
|
||
|
||
/// A symbol whose art paints with a pattern is not matched to the document's symbol of that name
|
||
/// and art when the document's pattern of that name is another one: it comes in as "Mark 2",
|
||
/// painting with "Dots 2".
|
||
#[test]
|
||
fn pasted_symbols_are_not_matched_to_symbols_that_paint_with_another_pattern() {
|
||
let mut s = session();
|
||
dots(&mut s, 10.0, "#ff0000");
|
||
let r = rect(&mut s, 20.0, 20.0, 50.0, 50.0);
|
||
s.execute("paint.setFill", &json!({"ids": [r.0], "swatch": "Dots"})).unwrap();
|
||
sel(&mut s, &[r]);
|
||
let mark = id_of(&s.execute("symbol.new", &json!({"name": "Mark"})).unwrap());
|
||
copy(&mut s, &[mark]);
|
||
// The document gets "Mark" and "Dots" from a first paste; then its "Dots" gets another tile.
|
||
new_doc(&mut s);
|
||
let v = s.execute("edit.paste", &json!({})).unwrap();
|
||
assert_eq!(v["renamed"], json!([]), "{v}");
|
||
let tile = &mut doc_mut(&mut s).patterns.iter_mut().find(|p| p.name == "Dots").unwrap().tile;
|
||
*tile = Rect::new(tile.x0, tile.y0, tile.x1 + 30.0, tile.y1 + 30.0);
|
||
let v = s.execute("edit.paste", &json!({})).unwrap();
|
||
assert!(renamed(&v).contains(&("symbol".to_string(), "Mark".to_string(), "Mark 2".to_string())), "{v}");
|
||
let art = |s: &Session, name: &str| fill_of(&doc(s).symbols.iter().find(|x| x.name == name).unwrap().art);
|
||
assert_eq!((art(&s, "Mark").as_deref(), art(&s, "Mark 2").as_deref()), (Some("Dots"), Some("Dots 2")));
|
||
assert!(matches!(node(&s, ids_of(&v)[0]).kind, NodeKind::SymbolInstance { ref symbol, .. } if symbol == "Mark 2"));
|
||
let v = s.execute("edit.paste", &json!({})).unwrap();
|
||
assert_eq!(v["added"], 0, "{v}");
|
||
}
|
||
|
||
/// The pattern the first filled object in `n` paints with.
|
||
fn fill_of(n: &Node) -> Option<String> {
|
||
let mut found = None;
|
||
n.walk(&mut |m| {
|
||
if found.is_none() {
|
||
found = pattern_of(&m.appearance.fill_paint()).map(str::to_string);
|
||
}
|
||
});
|
||
found
|
||
}
|
||
|
||
/// A brush whose art sets type in a character style is not matched to the document's brush of
|
||
/// that name and art when the document's style of that name is another one: the style comes in as
|
||
/// "Accent 2", and the brush as a copy whose type is set in "Accent 2".
|
||
#[test]
|
||
fn pasted_brushes_are_not_matched_to_brushes_that_use_another_style() {
|
||
let mut s = session();
|
||
let t = id_of(&s.execute("text.create", &json!({"x": 10, "y": 50, "text": "Ink"})).unwrap());
|
||
s.execute("charStyle.new", &json!({"name": "Accent", "attrs": {"kerning": 40}})).unwrap();
|
||
s.execute("charStyle.apply", &json!({"name": "Accent", "id": t.0})).unwrap();
|
||
let name = s.execute("brush.new", &json!({"type": "art", "name": "Lettering", "ids": [t.0]})).unwrap()["name"].as_str().unwrap().to_string();
|
||
let path = rect(&mut s, 200.0, 200.0, 40.0, 10.0);
|
||
s.execute("brush.apply", &json!({"name": name, "ids": [path.0]})).unwrap();
|
||
let brush = vectorcraft_brush::find(doc(&s), &name).unwrap();
|
||
copy(&mut s, &[path]);
|
||
new_doc(&mut s);
|
||
s.execute("charStyle.new", &json!({"name": "Accent", "attrs": {"kerning": 10}})).unwrap();
|
||
let d = doc_mut(&mut s);
|
||
let mut lib = vectorcraft_brush::library(d);
|
||
lib.push(brush);
|
||
vectorcraft_brush::store(d, &lib);
|
||
let v = s.execute("edit.paste", &json!({})).unwrap();
|
||
let names = renamed(&v);
|
||
let copied = names.iter().find(|(k, from, _)| k == "brush" && *from == name).map(|(_, _, to)| to.clone());
|
||
assert!(names.contains(&("charStyle".to_string(), "Accent".to_string(), "Accent 2".to_string())), "{v}");
|
||
let copied = copied.unwrap_or_else(|| panic!("the brush is added under a free name: {v}"));
|
||
let style_of = |s: &Session, b: &str| {
|
||
let b = vectorcraft_brush::find(doc(s), b).unwrap();
|
||
let mut found = None;
|
||
for a in b.art() {
|
||
a.walk(&mut |m| {
|
||
if let NodeKind::Text(tx) = &m.kind {
|
||
found = found.take().or_else(|| tx.runs[0].style.style_name.clone());
|
||
}
|
||
});
|
||
}
|
||
found
|
||
};
|
||
assert_eq!((style_of(&s, &name).as_deref(), style_of(&s, &copied).as_deref()), (Some("Accent"), Some("Accent 2")));
|
||
let v = s.execute("edit.paste", &json!({})).unwrap();
|
||
assert_eq!(v["added"], 0, "{v}");
|
||
}
|
||
|
||
#[test]
|
||
fn brushes_travel_with_their_strokes() {
|
||
let mut s = session();
|
||
let name = s
|
||
.execute("brush.new", &json!({"type": "calligraphic", "name": "Wide Nib", "params": {"angle": 30, "roundness": 20, "size": 9}}))
|
||
.unwrap()["name"]
|
||
.as_str()
|
||
.unwrap()
|
||
.to_string();
|
||
let r = rect(&mut s, 10.0, 10.0, 50.0, 50.0);
|
||
s.execute("brush.apply", &json!({"name": name, "ids": [r.0]})).unwrap();
|
||
copy(&mut s, &[r]);
|
||
assert_eq!(s.clipboard.brushes.len(), 1);
|
||
new_doc(&mut s);
|
||
assert!(vectorcraft_brush::find(doc(&s), &name).is_none());
|
||
s.execute("edit.paste", &json!({})).unwrap();
|
||
assert!(vectorcraft_brush::find(doc(&s), &name).is_some());
|
||
}
|
||
|
||
#[test]
|
||
fn pasting_back_into_the_source_document_adds_nothing() {
|
||
let mut s = session();
|
||
let r = swatched_rect(&mut s, "Brand", "#ff0000");
|
||
copy(&mut s, &[r]);
|
||
// Redefining the swatch afterwards is no conflict in its own document.
|
||
s.execute("swatch.edit", &json!({"name": "Brand", "color": "#00ff00"})).unwrap();
|
||
assert_eq!(s.execute("clipboard.conflicts", &json!({})).unwrap()["swatches"], json!([]));
|
||
let v = s.execute("edit.paste", &json!({})).unwrap();
|
||
assert_eq!((v["added"].as_u64(), v["renamed"].as_array().map(Vec::len)), (Some(0), Some(0)));
|
||
assert_eq!(fill(&s, ids_of(&v)[0]), linked(hex("#00ff00"), "Brand", 1.0), "it shows the swatch as it is now");
|
||
}
|
||
|
||
#[test]
|
||
fn paste_without_formatting_leaves_text_styles_behind() {
|
||
let mut s = session();
|
||
let t = id_of(&s.execute("text.create", &json!({"x": 10, "y": 50, "text": "Hello"})).unwrap());
|
||
s.execute("charStyle.new", &json!({"name": "Emphasis", "attrs": {"size": 24.0}})).unwrap();
|
||
s.execute("charStyle.apply", &json!({"name": "Emphasis", "id": t.0})).unwrap();
|
||
copy(&mut s, &[t]);
|
||
new_doc(&mut s);
|
||
s.execute("edit.pasteWithoutFormatting", &json!({})).unwrap();
|
||
assert!(doc(&s).char_styles.is_empty());
|
||
s.execute("edit.paste", &json!({})).unwrap();
|
||
assert_eq!(doc(&s).char_styles.len(), 1);
|
||
}
|
||
|
||
#[test]
|
||
fn clipboard_svg_carries_its_own_patterns() {
|
||
let mut s = session();
|
||
let dot = rect(&mut s, 0.0, 0.0, 10.0, 10.0);
|
||
sel(&mut s, &[dot]);
|
||
s.execute("object.pattern.make", &json!({"name": "Dots", "width": 20, "height": 20})).unwrap();
|
||
s.execute("object.pattern.done", &json!({})).unwrap();
|
||
let r = rect(&mut s, 20.0, 20.0, 100.0, 80.0);
|
||
s.execute("paint.setFill", &json!({"ids": [r.0], "swatch": "Dots"})).unwrap();
|
||
copy(&mut s, &[r]);
|
||
// Another document is active when the clipboard is published.
|
||
new_doc(&mut s);
|
||
let svg = s.clipboard_svg().unwrap();
|
||
assert!(svg.contains("<pattern"), "{svg}");
|
||
}
|
||
|
||
#[test]
|
||
fn a_failed_batch_puts_the_clipboard_back() {
|
||
let mut s = session();
|
||
let a = rect(&mut s, 0.0, 0.0, 10.0, 10.0);
|
||
let b = rect(&mut s, 20.0, 0.0, 10.0, 10.0);
|
||
copy(&mut s, &[a]);
|
||
let steps = json!([
|
||
{"command": "select.set", "params": {"ids": [a.0, b.0]}},
|
||
{"command": "edit.copy"},
|
||
{"command": "no.such.command"},
|
||
]);
|
||
assert!(s.execute("command.batch", &json!({"commands": steps})).is_err());
|
||
assert_eq!(s.clipboard.nodes.len(), 1);
|
||
}
|
||
|
||
// ---------- M4.23: placement ----------
|
||
|
||
#[test]
|
||
fn paste_centres_on_the_given_point() {
|
||
let mut s = session();
|
||
let r = rect(&mut s, 0.0, 0.0, 40.0, 20.0);
|
||
copy(&mut s, &[r]);
|
||
let v = s.execute("edit.paste", &json!({"center": [300, 200]})).unwrap();
|
||
let b = doc(&s).bounds_of(&ids_of(&v), false).unwrap();
|
||
assert!((b.center() - Point::new(300.0, 200.0)).hypot() < 1e-9, "{b:?}");
|
||
// Without a centre, the offset applies.
|
||
let v = s.execute("edit.paste", &json!({"dx": 5, "dy": 7})).unwrap();
|
||
assert_eq!(doc(&s).bounds_of(&ids_of(&v), false).unwrap().origin(), Point::new(5.0, 7.0));
|
||
}
|
||
|
||
#[test]
|
||
fn front_and_back_without_a_selection_use_the_current_layers_ends() {
|
||
let mut s = session();
|
||
let a = rect(&mut s, 0.0, 0.0, 10.0, 10.0);
|
||
let b = rect(&mut s, 20.0, 0.0, 10.0, 10.0);
|
||
copy(&mut s, &[a, b]);
|
||
let layer = |s: &Session| doc(s).layers[0].children().unwrap().iter().map(|n| n.id).collect::<Vec<_>>();
|
||
s.execute("select.none", &json!({})).unwrap();
|
||
let back = ids_of(&s.execute("edit.pasteInBack", &json!({})).unwrap());
|
||
assert_eq!(&layer(&s)[..2], &back[..], "pasted at the bottom (index 0), in order");
|
||
s.execute("select.none", &json!({})).unwrap();
|
||
let front = ids_of(&s.execute("edit.pasteInFront", &json!({})).unwrap());
|
||
let l = layer(&s);
|
||
assert_eq!(&l[l.len() - 2..], &front[..], "pasted on top, in order");
|
||
// With a selection: right above / below it.
|
||
sel(&mut s, &[a]);
|
||
let above = ids_of(&s.execute("edit.pasteInFront", &json!({})).unwrap());
|
||
let l = layer(&s);
|
||
let i = l.iter().position(|id| *id == a).unwrap();
|
||
assert_eq!(&l[i + 1..i + 3], &above[..]);
|
||
sel(&mut s, &[a]);
|
||
let below = ids_of(&s.execute("edit.pasteInBack", &json!({})).unwrap());
|
||
let l = layer(&s);
|
||
let i = l.iter().position(|id| *id == a).unwrap();
|
||
assert_eq!(&l[i - 2..i], &below[..]);
|
||
}
|
||
|
||
#[test]
|
||
fn front_and_back_beside_a_compound_paths_member_go_beside_the_compound_path() {
|
||
let mut s = session();
|
||
let a = rect(&mut s, 0.0, 0.0, 10.0, 10.0);
|
||
let b = rect(&mut s, 20.0, 0.0, 10.0, 10.0);
|
||
sel(&mut s, &[a, b]);
|
||
let c = NodeId(s.execute("object.compoundPath.make", &json!({})).unwrap()["id"].as_u64().unwrap());
|
||
let members = |s: &Session| doc(s).node(c).unwrap().children().unwrap().iter().map(|n| n.id).collect::<Vec<_>>();
|
||
let before = members(&s);
|
||
let layer = |s: &Session| doc(s).layers[0].children().unwrap().iter().map(|n| n.id).collect::<Vec<_>>();
|
||
// A compound path and a group, each pasted with one of the compound path's members selected.
|
||
let other = rect(&mut s, 40.0, 0.0, 10.0, 10.0);
|
||
sel(&mut s, &[other]);
|
||
let group = NodeId(s.execute("object.group", &json!({})).unwrap()["id"].as_u64().unwrap());
|
||
for copied in [c, group] {
|
||
copy(&mut s, &[copied]);
|
||
sel(&mut s, &[before[0]]);
|
||
let above = ids_of(&s.execute("edit.pasteInFront", &json!({})).unwrap());
|
||
let l = layer(&s);
|
||
let i = l.iter().position(|id| *id == c).unwrap();
|
||
assert_eq!(&l[i + 1..i + 2], &above[..], "right above the compound path");
|
||
sel(&mut s, &[before[1]]);
|
||
let below = ids_of(&s.execute("edit.pasteInBack", &json!({})).unwrap());
|
||
let l = layer(&s);
|
||
let i = l.iter().position(|id| *id == c).unwrap();
|
||
assert_eq!(&l[i - 1..i], &below[..], "right below the compound path");
|
||
assert_eq!(members(&s), before, "the compound path keeps its own members");
|
||
}
|
||
// A path still goes in front of the member, inside the compound path.
|
||
let loose = rect(&mut s, 60.0, 0.0, 10.0, 10.0);
|
||
copy(&mut s, &[loose]);
|
||
sel(&mut s, &[before[0]]);
|
||
let path = ids_of(&s.execute("edit.pasteInFront", &json!({})).unwrap());
|
||
assert_eq!(members(&s), [before[0], path[0], before[1]]);
|
||
}
|
||
|
||
#[test]
|
||
fn paste_in_back_of_a_clipping_path_keeps_it_the_clipping_path() {
|
||
let mut s = session();
|
||
let children = |s: &Session, g: NodeId| doc(s).node(g).unwrap().children().unwrap().iter().map(|n| n.id).collect::<Vec<_>>();
|
||
let loose = rect(&mut s, 200.0, 0.0, 10.0, 10.0);
|
||
let other = rect(&mut s, 220.0, 0.0, 10.0, 10.0);
|
||
sel(&mut s, &[other]);
|
||
let group = id_of(&s.execute("object.group", &json!({})).unwrap());
|
||
// A path as the clipping path, and a compound path as the clipping path (a group pasted in back
|
||
// of a member goes beside the compound path).
|
||
let art = rect(&mut s, 0.0, 0.0, 100.0, 100.0);
|
||
let mask = rect(&mut s, 10.0, 10.0, 50.0, 50.0);
|
||
sel(&mut s, &[art, mask]);
|
||
let g = id_of(&s.execute("object.clippingMask.make", &json!({})).unwrap());
|
||
let art2 = rect(&mut s, 0.0, 120.0, 100.0, 100.0);
|
||
let (a, b) = (rect(&mut s, 10.0, 130.0, 20.0, 20.0), rect(&mut s, 40.0, 130.0, 20.0, 20.0));
|
||
sel(&mut s, &[a, b]);
|
||
let c = id_of(&s.execute("object.compoundPath.make", &json!({})).unwrap());
|
||
sel(&mut s, &[art2, c]);
|
||
let g2 = id_of(&s.execute("object.clippingMask.make", &json!({})).unwrap());
|
||
for (g, clipping, target, copies) in [(g, mask, mask, vec![loose, group]), (g2, c, a, vec![group])] {
|
||
assert_eq!(children(&s, g)[0], clipping);
|
||
for copied in copies {
|
||
copy(&mut s, &[copied]);
|
||
sel(&mut s, &[target]);
|
||
let pasted = ids_of(&s.execute("edit.pasteInBack", &json!({})).unwrap());
|
||
let ch = children(&s, g);
|
||
assert_eq!(ch[0], clipping, "the clipping path stays the bottom child");
|
||
assert_eq!(&ch[1..2], &pasted[..], "right above the clipping path");
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn paste_on_all_artboards_keeps_the_offset_to_the_source_artboard() {
|
||
let mut s = Session::new();
|
||
s.execute("file.new", &json!({"width": 200, "height": 100, "artboards": 3})).unwrap();
|
||
let boards: Vec<Rect> = doc(&s).artboards.iter().map(|a| a.rect).collect();
|
||
// On artboard 2, 10 pt right and 15 pt down from its corner.
|
||
let r = rect(&mut s, boards[1].x0 + 10.0, boards[1].y0 + 15.0, 20.0, 20.0);
|
||
copy(&mut s, &[r]);
|
||
assert_eq!(s.clipboard.source_artboard, Some(boards[1]));
|
||
let ids = ids_of(&s.execute("edit.pasteOnAllArtboards", &json!({})).unwrap());
|
||
assert_eq!(ids.len(), 3);
|
||
for (id, b) in ids.iter().zip(&boards) {
|
||
assert_eq!(doc(&s).bounds_of(&[*id], false).unwrap().origin(), Point::new(b.x0 + 10.0, b.y0 + 15.0));
|
||
}
|
||
}
|
||
|
||
/// Paste in Place, in Front and in Back onto the active artboard (#693): where the objects were on
|
||
/// the artboard they were copied from, on the one named; without it, where they were.
|
||
#[test]
|
||
fn in_place_pastes_go_onto_the_artboard_named() {
|
||
let mut s = Session::new();
|
||
s.execute("file.new", &json!({"width": 200, "height": 100, "artboards": 3})).unwrap();
|
||
let boards: Vec<Rect> = doc(&s).artboards.iter().map(|a| a.rect).collect();
|
||
let r = rect(&mut s, boards[0].x0 + 10.0, boards[0].y0 + 15.0, 20.0, 20.0);
|
||
copy(&mut s, &[r]);
|
||
for cmd in ["edit.pasteInPlace", "edit.pasteInFront", "edit.pasteInBack"] {
|
||
let ids = ids_of(&s.execute(cmd, &json!({"artboard": 2})).unwrap());
|
||
assert_eq!(doc(&s).bounds_of(&ids, false).unwrap().origin(), Point::new(boards[2].x0 + 10.0, boards[2].y0 + 15.0), "{cmd}");
|
||
let ids = ids_of(&s.execute(cmd, &json!({})).unwrap());
|
||
assert_eq!(doc(&s).bounds_of(&ids, false).unwrap().origin(), Point::new(boards[0].x0 + 10.0, boards[0].y0 + 15.0), "{cmd} without one");
|
||
// An artboard that isn't there, or a value that isn't an index: an error, and nothing pasted.
|
||
let before = doc(&s).clone();
|
||
for bad in [json!(9), json!(-1), json!(1.5), json!("1")] {
|
||
assert!(s.execute(cmd, &json!({"artboard": bad})).is_err(), "{cmd} with artboard {bad}");
|
||
}
|
||
assert_eq!(doc(&s), &before, "{cmd}");
|
||
}
|
||
}
|
||
|
||
/// Paste in Place, in Front and in Back onto several artboards (the Artboard tool's selection, which
|
||
/// the app passes as `artboards`): a copy on each, where the objects were on the artboard they were
|
||
/// copied from, the copies in artboard order and in their own paint order, all selected, in one undo
|
||
/// step.
|
||
#[test]
|
||
fn in_place_pastes_put_a_copy_on_each_artboard_named() {
|
||
let mut s = Session::new();
|
||
s.execute("file.new", &json!({"width": 200, "height": 100, "artboards": 4})).unwrap();
|
||
let boards: Vec<Rect> = doc(&s).artboards.iter().map(|a| a.rect).collect();
|
||
let a = rect(&mut s, boards[1].x0 + 10.0, boards[1].y0 + 15.0, 20.0, 20.0);
|
||
let b = rect(&mut s, boards[1].x0 + 50.0, boards[1].y0 + 5.0, 10.0, 10.0);
|
||
copy(&mut s, &[a, b]);
|
||
for cmd in ["edit.pasteInPlace", "edit.pasteInFront", "edit.pasteInBack"] {
|
||
let undo = s.doc().unwrap().history.undo.len();
|
||
let before = doc(&s).clone();
|
||
let ids = ids_of(&s.execute(cmd, &json!({"artboards": [3, 0]})).unwrap());
|
||
assert_eq!(ids.len(), 4, "{cmd}: two objects on each of two artboards");
|
||
for (pair, board) in ids.chunks(2).zip([boards[0], boards[3]]) {
|
||
assert_eq!(doc(&s).bounds_of(&pair[..1], false).unwrap().origin(), Point::new(board.x0 + 10.0, board.y0 + 15.0), "{cmd}");
|
||
assert_eq!(doc(&s).bounds_of(&pair[1..], false).unwrap().origin(), Point::new(board.x0 + 50.0, board.y0 + 5.0), "{cmd}");
|
||
}
|
||
let layer: Vec<NodeId> = doc(&s).layers[0].children().into_iter().flatten().map(|n| n.id).collect();
|
||
let at: Vec<usize> = ids.iter().map(|id| layer.iter().position(|n| n == id).unwrap()).collect();
|
||
assert!(at.windows(2).all(|w| w[0] < w[1]), "{cmd}: in paint order, artboard by artboard: {at:?}");
|
||
assert_eq!(s.doc().unwrap().selection.in_paint_order(doc(&s)), ids, "{cmd}: the copies are selected");
|
||
assert_eq!(s.doc().unwrap().history.undo.len(), undo + 1, "{cmd}: one undo step");
|
||
s.execute("edit.undo", &json!({})).unwrap();
|
||
assert_eq!(doc(&s), &before, "{cmd}: undo takes every copy away");
|
||
s.execute("edit.redo", &json!({})).unwrap();
|
||
assert_eq!(doc(&s).layers[0].children().into_iter().flatten().count(), layer.len(), "{cmd}: redo");
|
||
// `artboard` wins over `artboards`.
|
||
let ids = ids_of(&s.execute(cmd, &json!({"artboard": 2, "artboards": [0, 3]})).unwrap());
|
||
assert_eq!(doc(&s).bounds_of(&ids, false).unwrap().origin(), Point::new(boards[2].x0 + 10.0, boards[2].y0 + 5.0), "{cmd}");
|
||
// A list that is empty, names an artboard the document lacks or names one twice, or isn't a
|
||
// list: an error, and nothing pasted.
|
||
let before = doc(&s).clone();
|
||
for bad in [json!([]), json!([0, 4]), json!([1, 1]), json!([0, -1]), json!(2), json!("0,1")] {
|
||
assert!(s.execute(cmd, &json!({"artboards": bad})).is_err(), "{cmd} with artboards {bad}");
|
||
}
|
||
assert!(s.execute(cmd, &json!({"artboard": 1, "artboards": [7]})).is_err(), "{cmd}: a bad list with `artboard`");
|
||
assert_eq!(doc(&s), &before, "{cmd}");
|
||
}
|
||
}
|
||
|
||
/// With the Artboard tool chosen and several artboards selected, Paste in Place, in Front and in
|
||
/// Back put a copy on each without naming them; the journal notes them, so a replay pastes on the
|
||
/// same artboards, and a recorded action leaves them out. With one selected, or another tool, the
|
||
/// objects go where they were.
|
||
#[test]
|
||
fn in_place_pastes_follow_the_artboard_tool_selection() {
|
||
let mut s = Session::new();
|
||
s.execute("file.new", &json!({"width": 200, "height": 100, "artboards": 3})).unwrap();
|
||
let boards: Vec<Rect> = doc(&s).artboards.iter().map(|a| a.rect).collect();
|
||
let r = rect(&mut s, boards[0].x0 + 10.0, boards[0].y0 + 15.0, 20.0, 20.0);
|
||
copy(&mut s, &[r]);
|
||
let origins = |s: &Session, ids: &[NodeId]| ids.iter().map(|id| doc(s).bounds_of(&[*id], false).unwrap().origin()).collect::<Vec<_>>();
|
||
let on = |b: &[usize]| b.iter().map(|i| Point::new(boards[*i].x0 + 10.0, boards[*i].y0 + 15.0)).collect::<Vec<_>>();
|
||
s.select_tool("artboard", ViewInfo::default()).unwrap();
|
||
for cmd in ["edit.pasteInPlace", "edit.pasteInFront", "edit.pasteInBack"] {
|
||
s.set_tool_option("selected", &json!([2, 1]));
|
||
let start = s.journal.len();
|
||
let ids = ids_of(&s.execute(cmd, &json!({})).unwrap());
|
||
assert_eq!(origins(&s, &ids), on(&[1, 2]), "{cmd}");
|
||
assert_eq!(s.journal.since(start).last().map(|(_, p)| p["artboards"].clone()), Some(json!([1, 2])), "{cmd}: journaled");
|
||
assert!(s.journal_for_action(start).iter().all(|(_, p)| p.get("artboards").is_none()), "{cmd}: not in an action");
|
||
// A stale index in the tool's selection is passed over.
|
||
s.set_tool_option("selected", &json!([0, 9]));
|
||
let ids = ids_of(&s.execute(cmd, &json!({})).unwrap());
|
||
assert_eq!(origins(&s, &ids), on(&[0]), "{cmd}: one left");
|
||
s.set_tool_option("selected", &json!([1]));
|
||
let ids = ids_of(&s.execute(cmd, &json!({})).unwrap());
|
||
assert_eq!(origins(&s, &ids), on(&[0]), "{cmd}: one selected, where it was");
|
||
}
|
||
s.set_tool_option("selected", &json!([1, 2]));
|
||
s.select_tool("selection", ViewInfo::default()).unwrap();
|
||
let ids = ids_of(&s.execute("edit.pasteInPlace", &json!({})).unwrap());
|
||
assert_eq!(origins(&s, &ids), on(&[0]), "another tool: where it was");
|
||
}
|
||
|
||
/// Art copied from no artboard onto several: it keeps its place on the first and its offset to the
|
||
/// first on the others. Guides alone go on each artboard named.
|
||
#[test]
|
||
fn in_place_pastes_onto_several_artboards_of_art_off_them_and_guides() {
|
||
let mut s = Session::new();
|
||
s.execute("file.new", &json!({"width": 200, "height": 100, "artboards": 3})).unwrap();
|
||
let boards: Vec<Rect> = doc(&s).artboards.iter().map(|a| a.rect).collect();
|
||
let r = rect(&mut s, boards[0].x0 - 100.0, boards[0].y0 + 15.0, 20.0, 20.0);
|
||
copy(&mut s, &[r]);
|
||
assert_eq!(s.clipboard.source_artboard, None);
|
||
let ids = ids_of(&s.execute("edit.pasteInPlace", &json!({"artboards": [1, 2]})).unwrap());
|
||
let at: Vec<Point> = ids.iter().map(|id| doc(&s).bounds_of(&[*id], false).unwrap().origin()).collect();
|
||
let first = Point::new(boards[0].x0 - 100.0, boards[0].y0 + 15.0);
|
||
assert_eq!(at, [first, first + (boards[2].origin() - boards[1].origin())]);
|
||
// Guides alone, copied from artboard 1: one on each artboard named, 30 pt into it.
|
||
s.execute("guide.add", &json!({"vertical": true, "pos": boards[0].x0 + 30.0})).unwrap();
|
||
s.execute("select.none", &json!({})).unwrap();
|
||
s.execute("guide.select", &json!({"indexes": [0]})).unwrap();
|
||
s.execute("edit.copy", &json!({})).unwrap();
|
||
let r = s.execute("edit.pasteInFront", &json!({"artboards": [1, 2]})).unwrap();
|
||
assert_eq!(r["guides"], 2);
|
||
let mut guides: Vec<f64> = doc(&s).ruler_guides().iter().map(|g| g.pos).collect();
|
||
guides.sort_by(f64::total_cmp);
|
||
assert_eq!(guides, [boards[0].x0 + 30.0, boards[1].x0 + 30.0, boards[2].x0 + 30.0]);
|
||
}
|
||
|
||
/// Artboards on the clipboard come back once, where they were, whichever artboards are named.
|
||
#[test]
|
||
fn in_place_pastes_of_copied_artboards_ignore_the_artboards_named() {
|
||
let mut s = Session::new();
|
||
s.execute("file.new", &json!({"width": 200, "height": 100, "artboards": 3})).unwrap();
|
||
rect(&mut s, 10.0, 10.0, 20.0, 20.0);
|
||
s.execute("artboard.copy", &json!({"index": 0, "art": true})).unwrap();
|
||
let r = s.execute("edit.pasteInPlace", &json!({"artboards": [1, 2]})).unwrap();
|
||
assert_eq!(r["artboards"], json!([3]));
|
||
assert_eq!(ids_of(&r).len(), 1);
|
||
assert_eq!(doc(&s).artboards[3].rect, doc(&s).artboards[0].rect);
|
||
}
|
||
|
||
/// Art copied from beside the artboards goes back where it was, whichever artboard is active (#1924).
|
||
#[test]
|
||
fn in_place_pastes_of_art_off_the_artboards_stay_where_it_was() {
|
||
let mut s = Session::new();
|
||
s.execute("file.new", &json!({"width": 200, "height": 100, "artboards": 2})).unwrap();
|
||
let boards: Vec<Rect> = doc(&s).artboards.iter().map(|a| a.rect).collect();
|
||
let r = rect(&mut s, boards[0].x0 - 100.0, boards[0].y0 + 15.0, 20.0, 20.0);
|
||
copy(&mut s, &[r]);
|
||
for cmd in ["edit.pasteInPlace", "edit.pasteInFront", "edit.pasteInBack"] {
|
||
let ids = ids_of(&s.execute(cmd, &json!({"artboard": 1})).unwrap());
|
||
assert_eq!(doc(&s).bounds_of(&ids, false).unwrap().origin(), Point::new(boards[0].x0 - 100.0, boards[0].y0 + 15.0), "{cmd}");
|
||
}
|
||
}
|
||
|
||
/// Art centered on an artboard's right or bottom edge is copied from that artboard: Paste in Place
|
||
/// puts it on the active artboard where it was on that one.
|
||
#[test]
|
||
fn in_place_pastes_of_art_on_an_artboard_edge_go_onto_the_artboard_named() {
|
||
let mut s = Session::new();
|
||
s.execute("file.new", &json!({"width": 200, "height": 100, "artboards": 2})).unwrap();
|
||
let boards: Vec<Rect> = doc(&s).artboards.iter().map(|a| a.rect).collect();
|
||
let b = boards[1];
|
||
// A line along the right edge of artboard 2, then one along its bottom edge.
|
||
for (x1, y1, x2, y2) in [(b.x1, b.y0 + 10.0, b.x1, b.y0 + 40.0), (b.x0 + 10.0, b.y1, b.x0 + 40.0, b.y1)] {
|
||
let line = id_of(&s.execute("shape.line", &json!({"x1": x1, "y1": y1, "x2": x2, "y2": y2})).unwrap());
|
||
copy(&mut s, &[line]);
|
||
assert_eq!(s.clipboard.source_artboard, Some(b));
|
||
let ids = ids_of(&s.execute("edit.pasteInPlace", &json!({"artboard": 0})).unwrap());
|
||
let moved = doc(&s).bounds_of(&ids, false).unwrap().origin() - doc(&s).bounds_of(&[line], false).unwrap().origin();
|
||
assert_eq!(moved, boards[0].origin() - b.origin());
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn paste_remembers_layers_restores_the_source_layers() {
|
||
let mut s = session();
|
||
let first = doc(&s).layers[0].id;
|
||
let a = rect(&mut s, 0.0, 0.0, 10.0, 10.0);
|
||
let art = id_of(&s.execute("layer.new", &json!({"name": "Art"})).unwrap());
|
||
let b = rect(&mut s, 20.0, 0.0, 10.0, 10.0);
|
||
copy(&mut s, &[a, b]);
|
||
let names: Vec<_> = s.clipboard.source_layers.iter().map(|l| l.as_ref().map(|l| l.names.clone())).collect();
|
||
assert_eq!(names, [Some(vec!["Layer 1".to_string()]), Some(vec!["Art".to_string()])]);
|
||
let other = id_of(&s.execute("layer.new", &json!({"name": "Other"})).unwrap());
|
||
assert_eq!(s.execute("layer.pasteRemembersLayers", &json!({})).unwrap()["on"], true);
|
||
assert!(doc(&s).paste_remembers_layers);
|
||
assert_eq!(s.execute("document.inspect", &json!({})).unwrap()["pasteRemembersLayers"], true);
|
||
let ids = ids_of(&s.execute("edit.paste", &json!({})).unwrap());
|
||
let parents: Vec<_> = ids.iter().map(|id| doc(&s).parent_of(*id)).collect();
|
||
assert_eq!(parents, [Some(first), Some(art)]);
|
||
// In a document without those layers they are made.
|
||
new_doc(&mut s);
|
||
s.execute("layer.pasteRemembersLayers", &json!({"on": true})).unwrap();
|
||
let ids = ids_of(&s.execute("edit.paste", &json!({})).unwrap());
|
||
let names: Vec<_> = ids.iter().map(|id| doc(&s).node(doc(&s).parent_of(*id).unwrap()).unwrap().name.clone().unwrap()).collect();
|
||
assert_eq!(names, ["Layer 1", "Art"]);
|
||
assert_eq!(doc(&s).layers.len(), 2, "Layer 1 exists; Art is new");
|
||
// The option is an undoable document setting.
|
||
s.execute("edit.undo", &json!({})).unwrap();
|
||
s.execute("edit.undo", &json!({})).unwrap();
|
||
assert!(!doc(&s).paste_remembers_layers);
|
||
// Off: everything goes to the current layer.
|
||
s.execute("document.activate", &json!({"index": 0})).unwrap();
|
||
assert_eq!(s.execute("layer.pasteRemembersLayers", &json!({"on": false})).unwrap()["on"], false);
|
||
s.execute("layer.setCurrent", &json!({"id": other.0})).unwrap();
|
||
let ids = ids_of(&s.execute("edit.paste", &json!({})).unwrap());
|
||
assert!(ids.iter().all(|id| doc(&s).parent_of(*id) == Some(other)));
|
||
}
|
||
|
||
/// A sublayer `name` made inside `parent`; it becomes the current layer.
|
||
fn sublayer(s: &mut Session, parent: NodeId, name: &str) -> NodeId {
|
||
id_of(&s.execute("layer.newSublayer", &json!({"parent": parent.0, "name": name})).unwrap())
|
||
}
|
||
|
||
/// The names of the layers around `id`, the top-level layer first.
|
||
fn layer_path(s: &Session, id: NodeId) -> Vec<String> {
|
||
let d = doc(s);
|
||
d.ancestry(id).unwrap().iter().filter_map(|a| d.node(*a).filter(|n| n.is_layer())).map(|l| l.name.clone().unwrap()).collect()
|
||
}
|
||
|
||
/// Art from two sublayers of the same name in different layers pastes back into each one; in
|
||
/// another document a missing sublayer is made inside its layer, and a missing layer on top with
|
||
/// the sublayer inside, in the paste's one undo step.
|
||
#[test]
|
||
fn paste_remembers_layers_keeps_same_named_sublayers_apart() {
|
||
let mut s = session();
|
||
let first = doc(&s).layers[0].id;
|
||
let sub1 = sublayer(&mut s, first, "Sub");
|
||
let a = rect(&mut s, 0.0, 0.0, 10.0, 10.0);
|
||
let second = id_of(&s.execute("layer.new", &json!({"name": "Layer 2", "top": true})).unwrap());
|
||
let sub2 = sublayer(&mut s, second, "Sub");
|
||
let b = rect(&mut s, 20.0, 0.0, 10.0, 10.0);
|
||
copy(&mut s, &[a, b]);
|
||
s.execute("layer.pasteRemembersLayers", &json!({"on": true})).unwrap();
|
||
s.execute("layer.setCurrent", &json!({"id": first.0})).unwrap();
|
||
let ids = ids_of(&s.execute("edit.paste", &json!({})).unwrap());
|
||
let parents: Vec<_> = ids.iter().map(|id| doc(&s).parent_of(*id)).collect();
|
||
assert_eq!(parents, [Some(sub1), Some(sub2)]);
|
||
new_doc(&mut s);
|
||
s.execute("layer.pasteRemembersLayers", &json!({"on": true})).unwrap();
|
||
let before = doc(&s).clone();
|
||
let ids = ids_of(&s.execute("edit.paste", &json!({})).unwrap());
|
||
let paths: Vec<_> = ids.iter().map(|id| layer_path(&s, *id)).collect();
|
||
assert_eq!(paths, [["Layer 1", "Sub"], ["Layer 2", "Sub"]]);
|
||
assert_eq!(doc(&s).layers.len(), 2, "Layer 1 is kept; Layer 2 is new");
|
||
assert_eq!(doc(&s).layers[0].children().unwrap().len(), 1, "Sub is made inside Layer 1");
|
||
let after = doc(&s).clone();
|
||
// Pasting again finds the layers made.
|
||
let again = ids_of(&s.execute("edit.paste", &json!({})).unwrap());
|
||
let parents: Vec<_> = again.iter().map(|id| doc(&s).parent_of(*id)).collect();
|
||
assert_eq!(parents, ids.iter().map(|id| doc(&s).parent_of(*id)).collect::<Vec<_>>());
|
||
s.execute("edit.undo", &json!({})).unwrap();
|
||
assert_eq!(*doc(&s), after);
|
||
s.execute("edit.undo", &json!({})).unwrap();
|
||
assert_eq!(*doc(&s), before, "one undo step takes the art and the layers made for it");
|
||
}
|
||
|
||
/// Art whose sublayer was deleted after the copy pastes into a new sublayer of that name inside
|
||
/// the same layer, not into a sublayer of that name in another layer.
|
||
#[test]
|
||
fn paste_remembers_layers_makes_a_deleted_sublayer_again_inside_its_layer() {
|
||
let mut s = session();
|
||
let first = doc(&s).layers[0].id;
|
||
let sub = sublayer(&mut s, first, "Sub");
|
||
let a = rect(&mut s, 0.0, 0.0, 10.0, 10.0);
|
||
let second = id_of(&s.execute("layer.new", &json!({"name": "Layer 2", "top": true})).unwrap());
|
||
let elsewhere = sublayer(&mut s, second, "Sub");
|
||
copy(&mut s, &[a]);
|
||
s.execute("layer.delete", &json!({"id": sub.0})).unwrap();
|
||
s.execute("layer.pasteRemembersLayers", &json!({"on": true})).unwrap();
|
||
let ids = ids_of(&s.execute("edit.paste", &json!({})).unwrap());
|
||
let made = doc(&s).parent_of(ids[0]).unwrap();
|
||
assert_ne!(made, elsewhere);
|
||
assert_eq!(doc(&s).parent_of(made), Some(first));
|
||
assert_eq!(layer_path(&s, ids[0]), ["Layer 1", "Sub"]);
|
||
}
|
||
|
||
/// Art whose sublayer was moved into another layer after the copy pastes into that sublayer where
|
||
/// it is now; no sublayer is made where it was.
|
||
#[test]
|
||
fn paste_remembers_layers_follows_a_moved_sublayer() {
|
||
let mut s = session();
|
||
let first = doc(&s).layers[0].id;
|
||
let sub = sublayer(&mut s, first, "Sub");
|
||
let a = rect(&mut s, 0.0, 0.0, 10.0, 10.0);
|
||
let second = id_of(&s.execute("layer.new", &json!({"name": "Layer 2", "top": true})).unwrap());
|
||
copy(&mut s, &[a]);
|
||
s.execute("layer.move", &json!({"id": sub.0, "target": second.0})).unwrap();
|
||
s.execute("layer.pasteRemembersLayers", &json!({"on": true})).unwrap();
|
||
let ids = ids_of(&s.execute("edit.paste", &json!({})).unwrap());
|
||
assert_eq!(doc(&s).parent_of(ids[0]), Some(sub));
|
||
assert_eq!(layer_path(&s, ids[0]), ["Layer 2", "Sub"]);
|
||
assert!(node(&s, first).children().unwrap().is_empty(), "no Sub is made in Layer 1");
|
||
}
|
||
|
||
/// In another document with layers of the same names, art goes into the topmost chain of layers
|
||
/// that matches the most names, a shown and unlocked one before a hidden or locked one.
|
||
#[test]
|
||
fn paste_remembers_layers_takes_the_topmost_open_match_of_repeated_names() {
|
||
let mut s = session();
|
||
let first = doc(&s).layers[0].id;
|
||
sublayer(&mut s, first, "Sub");
|
||
let a = rect(&mut s, 0.0, 0.0, 10.0, 10.0);
|
||
copy(&mut s, &[a]);
|
||
new_doc(&mut s);
|
||
s.execute("layer.pasteRemembersLayers", &json!({"on": true})).unwrap();
|
||
let lower = doc(&s).layers[0].id;
|
||
let lower_sub = sublayer(&mut s, lower, "Sub");
|
||
s.execute("layer.new", &json!({"name": "Sub", "top": true})).unwrap();
|
||
let upper = id_of(&s.execute("layer.new", &json!({"name": "Layer 1", "top": true})).unwrap());
|
||
// The upper Layer 1 has no Sub, and a top-level Sub is not inside a Layer 1: the lower chain
|
||
// matches both names.
|
||
let ids = ids_of(&s.execute("edit.paste", &json!({})).unwrap());
|
||
assert_eq!(doc(&s).parent_of(ids[0]), Some(lower_sub));
|
||
let upper_sub = sublayer(&mut s, upper, "Sub");
|
||
let ids = ids_of(&s.execute("edit.paste", &json!({})).unwrap());
|
||
assert_eq!(doc(&s).parent_of(ids[0]), Some(upper_sub), "the topmost of two full matches");
|
||
s.execute("layer.setProps", &json!({"id": upper_sub.0, "locked": true})).unwrap();
|
||
let ids = ids_of(&s.execute("edit.paste", &json!({})).unwrap());
|
||
assert_eq!(doc(&s).parent_of(ids[0]), Some(lower_sub), "an unlocked match before a locked one");
|
||
}
|
||
|
||
/// A remembered layer or sublayer that is hidden or locked, or inside a layer that is, refuses the
|
||
/// whole paste: the document, the selection and the undo history stay as they were.
|
||
#[test]
|
||
fn paste_remembers_layers_refuses_a_hidden_or_locked_layer() {
|
||
let (locked, hidden) = (json!({"locked": true}), json!({"visible": false}));
|
||
for (on_sub, props) in [(true, &locked), (true, &hidden), (false, &locked), (false, &hidden)] {
|
||
let mut s = session();
|
||
let first = doc(&s).layers[0].id;
|
||
let sub = sublayer(&mut s, first, "Sub");
|
||
let a = rect(&mut s, 0.0, 0.0, 10.0, 10.0);
|
||
let other = id_of(&s.execute("layer.new", &json!({"name": "Other", "top": true})).unwrap());
|
||
let b = rect(&mut s, 20.0, 0.0, 10.0, 10.0);
|
||
copy(&mut s, &[b, a]);
|
||
s.execute("layer.pasteRemembersLayers", &json!({"on": true})).unwrap();
|
||
let mut p = props.clone();
|
||
p["id"] = json!(if on_sub { sub.0 } else { first.0 });
|
||
s.execute("layer.setProps", &p).unwrap();
|
||
s.execute("layer.setCurrent", &json!({"id": other.0})).unwrap();
|
||
let before = s.doc().unwrap().clone();
|
||
assert!(s.execute("edit.paste", &json!({})).is_err(), "{p}");
|
||
assert_eq!(s.doc().unwrap().doc, before.doc, "{p}");
|
||
assert_eq!(s.doc().unwrap().selection, before.selection, "{p}");
|
||
assert_eq!(s.doc().unwrap().history.undo.len(), before.history.undo.len(), "{p}");
|
||
}
|
||
// In another document, a locked layer of the copied layer's name is refused too.
|
||
let mut s = session();
|
||
let a = rect(&mut s, 0.0, 0.0, 10.0, 10.0);
|
||
copy(&mut s, &[a]);
|
||
new_doc(&mut s);
|
||
s.execute("layer.pasteRemembersLayers", &json!({"on": true})).unwrap();
|
||
let locked = doc(&s).layers[0].id;
|
||
id_of(&s.execute("layer.new", &json!({"name": "Flyer"})).unwrap());
|
||
s.execute("layer.setProps", &json!({"id": locked.0, "locked": true})).unwrap();
|
||
let before = doc(&s).clone();
|
||
assert!(s.execute("edit.paste", &json!({})).is_err());
|
||
assert_eq!(*doc(&s), before, "no second Layer 1 is made");
|
||
}
|
||
|
||
#[test]
|
||
fn paste_remembers_layers_survives_save_and_old_files_load_without_it() {
|
||
let mut s = session();
|
||
s.execute("layer.pasteRemembersLayers", &json!({"on": true})).unwrap();
|
||
let bytes = vectorcraft_format::save(doc(&s), false);
|
||
assert!(vectorcraft_format::load(&bytes).unwrap().paste_remembers_layers);
|
||
let v: Value = serde_json::from_slice(&vectorcraft_format::save(&Document::new(10.0, 10.0), false)).unwrap();
|
||
assert!(v.get("paste_remembers_layers").is_none(), "off is not written");
|
||
assert!(!vectorcraft_format::load(v.to_string().as_bytes()).unwrap().paste_remembers_layers);
|
||
}
|
||
|
||
/// Verify document-scoped object IDs, grab-point placement and one destination Undo/Redo step.
|
||
#[test]
|
||
fn artwork_drop_uses_source_ids_hotspot_and_one_target_undo_step() {
|
||
let mut s = session();
|
||
let r = swatched_rect(&mut s, "Brand", "#ff0000");
|
||
let source = s.doc().unwrap().clone();
|
||
new_doc(&mut s);
|
||
let other = swatched_rect(&mut s, "Brand", "#0000ff");
|
||
assert_eq!(r, other, "node ids collide across documents");
|
||
copy(&mut s, &[other]);
|
||
let clip = s.clipboard.clone();
|
||
let target = s.doc().unwrap().clone();
|
||
let result = s.execute("edit.dropSelection", &json!({"sourceDocument": source.uid, "ids": [r, r], "grab": [25,30], "at": [175,130]})).unwrap();
|
||
let ids = ids_of(&result);
|
||
assert_eq!(ids.len(), 1);
|
||
assert_eq!(fill(&s, ids[0]), linked(hex("#ff0000"), "Brand 2", 1.0));
|
||
assert_eq!(fill(&s, other), linked(hex("#0000ff"), "Brand", 1.0));
|
||
assert_eq!(node(&s, ids[0]).geometric_bounds().unwrap(), Rect::new(170.0, 120.0, 210.0, 160.0));
|
||
assert_eq!(s.doc().unwrap().history.undo.len(), target.history.undo.len() + 1);
|
||
assert_eq!(s.clipboard, clip);
|
||
assert_eq!(s.documents()[0].doc, source.doc);
|
||
s.execute("edit.undo", &json!({})).unwrap();
|
||
assert_eq!(s.doc().unwrap().doc, target.doc);
|
||
s.execute("edit.redo", &json!({})).unwrap();
|
||
assert_eq!(s.doc().unwrap().selection.objects, ids);
|
||
}
|
||
|
||
/// Verify malformed, oversized, stale and overflowing drop requests leave document and clipboard state intact.
|
||
#[test]
|
||
fn invalid_artwork_drop_is_atomic() {
|
||
let mut s = session();
|
||
let r = rect(&mut s, 10.0, 10.0, 40.0, 40.0);
|
||
let uid = s.execute("document.inspect", &json!({})).unwrap()["uid"].as_u64().unwrap();
|
||
new_doc(&mut s);
|
||
let target = s.doc().unwrap().clone();
|
||
let clipboard = s.clipboard.clone();
|
||
for patch in [
|
||
json!({"sourceDocument": u64::MAX}),
|
||
json!({"sourceDocument": -1}),
|
||
json!({"sourceDocument": 1.5}),
|
||
json!({"sourceDocument": null}),
|
||
json!({"ids": vec![r;10001]}),
|
||
json!({"ids": null}),
|
||
json!({"ids": [r,-1]}),
|
||
json!({"ids": [r,1.5]}),
|
||
json!({"grab": [0,0,0]}),
|
||
json!({"grab": ["0",0]}),
|
||
json!({"ids": []}),
|
||
json!({"ids": [r, 9999999]}),
|
||
json!({"ids": ["1"]}),
|
||
json!({"grab": [0]}),
|
||
json!({"at": [null, 0]}),
|
||
json!({"at": [1e308, 0]}),
|
||
] {
|
||
let mut p = json!({"sourceDocument": uid, "ids": [r], "grab": [0,0], "at": [50,60]});
|
||
p.as_object_mut().unwrap().extend(patch.as_object().unwrap().clone());
|
||
assert!(s.execute("edit.dropSelection", &p).is_err(), "{p}");
|
||
assert_eq!(s.doc().unwrap().doc, target.doc);
|
||
assert_eq!(s.doc().unwrap().history.undo.len(), target.history.undo.len());
|
||
assert_eq!(s.doc().unwrap().selection, target.selection);
|
||
assert_eq!(s.clipboard, clipboard);
|
||
}
|
||
s.execute("file.close", &json!({"index":0})).unwrap();
|
||
assert!(s.execute("edit.dropSelection", &json!({"sourceDocument": uid,"ids":[r],"grab":[0,0],"at":[50,60]})).is_err());
|
||
assert_eq!(s.doc().unwrap().doc, target.doc);
|
||
}
|
||
|
||
/// Verify refusing a locked insertion context preserves the target document and its undo history.
|
||
#[test]
|
||
fn artwork_drop_into_a_locked_destination_is_atomic() {
|
||
let mut s = session();
|
||
let r = swatched_rect(&mut s, "Brand", "#ff0000");
|
||
let uid = s.doc().unwrap().uid;
|
||
new_doc(&mut s);
|
||
let layer = s.doc().unwrap().doc.layers[0].id;
|
||
s.execute("layer.setProps", &json!({"id":layer,"locked":true})).unwrap();
|
||
let target = s.doc().unwrap().clone();
|
||
assert!(s.execute("edit.dropSelection", &json!({"sourceDocument":uid,"ids":[r],"grab":[0,0],"at":[100,100]})).is_err());
|
||
assert_eq!(s.doc().unwrap().doc, target.doc);
|
||
assert_eq!(s.doc().unwrap().selection, target.selection);
|
||
assert_eq!(s.doc().unwrap().history.undo.len(), target.history.undo.len());
|
||
}
|
||
|
||
/// Verify tab drops use the editable current layer despite Paste Remembers Layers and source-layer name collisions.
|
||
#[test]
|
||
fn artwork_drop_uses_the_current_layer_even_when_paste_remembers_hidden_or_locked_source_layers() {
|
||
for hidden in [true, false] {
|
||
let mut s = session();
|
||
let r = rect(&mut s, 10.0, 10.0, 40.0, 40.0);
|
||
let uid = s.doc().unwrap().uid;
|
||
new_doc(&mut s);
|
||
let blocked = doc(&s).layers[0].id;
|
||
s.execute("layer.setProps", &json!({"id":blocked,"visible":!hidden,"locked":!hidden})).unwrap();
|
||
let current = id_of(&s.execute("layer.new", &json!({"name":"Flyer"})).unwrap());
|
||
s.execute("layer.pasteRemembersLayers", &json!({"on":true})).unwrap();
|
||
let blocked_before = node(&s, blocked);
|
||
let placed = ids_of(&s.execute("edit.dropSelection", &json!({"sourceDocument":uid,"ids":[r],"grab":[0,0],"at":[100,100]})).unwrap());
|
||
assert_eq!(doc(&s).parent_of(placed[0]), Some(current));
|
||
assert_eq!(node(&s, blocked), blocked_before);
|
||
assert_eq!(doc(&s).layers.len(), 2);
|
||
assert!(doc(&s).paste_remembers_layers, "the preference still applies to ordinary Paste");
|
||
}
|
||
}
|
||
|
||
/// Two touching rectangles made into 9-slice symbol `Audit`, its instance scaled 150% × 75% and
|
||
/// copied (#5902).
|
||
fn copied_nine_slice_instance(s: &mut Session) -> Document {
|
||
let red = rect(s, 30.0, 30.0, 80.0, 60.0);
|
||
s.execute("paint.setFill", &json!({"ids": [red.0], "color": "#ff0000"})).unwrap();
|
||
let blue = rect(s, 110.0, 90.0, 40.0, 30.0);
|
||
s.execute("paint.setFill", &json!({"ids": [blue.0], "color": "#0000ff"})).unwrap();
|
||
s.execute("select.all", &json!({})).unwrap();
|
||
s.execute("symbol.new", &json!({"name": "Audit", "nineSlice": true, "registration": 0, "exportType": "graphic"})).unwrap();
|
||
s.execute("object.scale", &json!({"sx": 150, "sy": 75})).unwrap();
|
||
s.execute("edit.copy", &json!({})).unwrap();
|
||
doc(s).clone()
|
||
}
|
||
|
||
/// The symbol options pasted symbol `to` should have: those of `from` in `src`.
|
||
fn assert_same_symbol_options(src: &Document, from: &str, d: &Document, to: &str) {
|
||
assert!(src.symbol_nine_slice(from).is_some());
|
||
assert_eq!(d.symbol_nine_slice(to), src.symbol_nine_slice(from));
|
||
assert_eq!(d.symbol_registration(to), src.symbol_registration(from));
|
||
assert_eq!(d.symbol_export_type(to), src.symbol_export_type(from));
|
||
assert_eq!(d.symbol_natural_xf(to), src.symbol_natural_xf(from));
|
||
}
|
||
|
||
#[test]
|
||
fn a_nine_slice_symbol_pasted_into_another_document_keeps_its_options_and_look() {
|
||
let mut s = session();
|
||
let src = copied_nine_slice_instance(&mut s);
|
||
let before = render(&s);
|
||
new_doc(&mut s);
|
||
s.execute("edit.pasteInPlace", &json!({})).unwrap();
|
||
assert_same_symbol_options(&src, "Audit", doc(&s), "Audit");
|
||
assert!(before == render(&s), "the pasted instance draws as in the source");
|
||
// Pasting again uses the symbol the first paste added.
|
||
let v = s.execute("edit.pasteInPlace", &json!({})).unwrap();
|
||
assert_eq!(v["added"], 0, "{v}");
|
||
}
|
||
|
||
#[test]
|
||
fn a_renamed_nine_slice_symbol_keeps_its_options_under_the_new_name() {
|
||
let mut s = session();
|
||
let src = copied_nine_slice_instance(&mut s);
|
||
new_doc(&mut s);
|
||
let other = ellipse(&mut s);
|
||
sel(&mut s, &[other]);
|
||
s.execute("symbol.new", &json!({"name": "Audit"})).unwrap();
|
||
let own = doc(&s).clone();
|
||
let v = s.execute("edit.pasteInPlace", &json!({})).unwrap();
|
||
assert!(renamed(&v).contains(&("symbol".into(), "Audit".into(), "Audit 2".into())), "{v}");
|
||
assert_same_symbol_options(&src, "Audit", doc(&s), "Audit 2");
|
||
// The document's own symbol keeps its options.
|
||
assert_eq!(doc(&s).symbol_nine_slice("Audit"), None);
|
||
assert_eq!(doc(&s).symbol_natural_xf("Audit"), own.symbol_natural_xf("Audit"));
|
||
}
|
||
|
||
#[test]
|
||
fn a_same_named_symbol_with_other_options_is_not_reused() {
|
||
let mut s = session();
|
||
let src = copied_nine_slice_instance(&mut s);
|
||
new_doc(&mut s);
|
||
s.execute("edit.pasteInPlace", &json!({})).unwrap();
|
||
// The document's `Audit` has the same art but 9-slice scaling off now.
|
||
s.execute("symbol.options", &json!({"name": "Audit", "nineSlice": false})).unwrap();
|
||
let v = s.execute("edit.pasteInPlace", &json!({})).unwrap();
|
||
assert!(renamed(&v).contains(&("symbol".into(), "Audit".into(), "Audit 2".into())), "{v}");
|
||
assert_same_symbol_options(&src, "Audit", doc(&s), "Audit 2");
|
||
assert_eq!(doc(&s).symbol_nine_slice("Audit"), None, "the document's own symbol keeps its options");
|
||
}
|
||
|
||
#[test]
|
||
fn a_symbol_without_a_recorded_natural_size_is_pasted_without_one() {
|
||
let mut s = session();
|
||
let r = rect(&mut s, 30.0, 30.0, 80.0, 60.0);
|
||
sel(&mut s, &[r]);
|
||
s.execute("symbol.new", &json!({"name": "Plain"})).unwrap();
|
||
s.edit("Forget size", |d, _| {
|
||
if let Some(m) = d.unknown.get_mut(vectorcraft_doc::SYMBOL_SIZES).and_then(Value::as_object_mut) {
|
||
m.remove("Plain");
|
||
}
|
||
Ok(())
|
||
})
|
||
.unwrap();
|
||
assert_eq!(doc(&s).unknown.get(vectorcraft_doc::SYMBOL_SIZES).and_then(|m| m.get("Plain")), None);
|
||
s.execute("edit.copy", &json!({})).unwrap();
|
||
let before = render(&s);
|
||
new_doc(&mut s);
|
||
s.execute("edit.pasteInPlace", &json!({})).unwrap();
|
||
assert!(doc(&s).symbols.iter().any(|x| x.name == "Plain"));
|
||
assert_eq!(doc(&s).unknown.get(vectorcraft_doc::SYMBOL_SIZES).and_then(|m| m.get("Plain")), None, "no size is made up for it");
|
||
assert!(before == render(&s), "the pasted instance draws as in the source");
|
||
}
|