vectorcraft/crates/engine/src/tests_styles.rs

254 lines
12 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Graphic styles: transparency, capture from groups and type, add-apply, unique names, links
//! (redefine, break link, delete, rename), Select All Unused, Sort by Name and Select > Same.
use serde_json::{Value, json};
use vectorcraft_color::{BlendMode, Color, Paint};
use vectorcraft_doc::{GraphicStyle, Knockout, Node};
use super::*;
fn session() -> Session {
let mut s = Session::new();
s.execute("file.new", &json!({"width": 600, "height": 400})).unwrap();
s
}
fn run(s: &mut Session, id: &str, p: Value) -> Value {
s.execute(id, &p).unwrap_or_else(|e| panic!("{id} {p}: {e}"))
}
/// A selected 50×50 rectangle at `x` with the default appearance.
fn rect(s: &mut Session, x: f64) -> NodeId {
let id = NodeId(run(s, "shape.rectangle", json!({"x": x, "y": 10, "width": 50, "height": 50}))["id"].as_u64().unwrap());
run(s, "select.set", json!({"ids": [id.0]}));
id
}
fn node(s: &Session, id: NodeId) -> Node {
s.doc().unwrap().doc.node(id).unwrap().clone()
}
fn style(s: &Session, name: &str) -> GraphicStyle {
s.doc().unwrap().doc.graphic_style(name).unwrap_or_else(|| panic!("no style {name}")).clone()
}
fn fill(s: &Session, id: NodeId) -> Paint {
node(s, id).appearance.fill_paint()
}
fn set_fill(s: &mut Session, id: NodeId, hex: &str) {
run(s, "paint.setFill", json!({"color": hex, "ids": [id.0]}));
}
fn linked(s: &mut Session, name: &str) -> Vec<u64> {
let l = run(s, "graphicStyle.list", json!({}));
let st = l["styles"].as_array().unwrap().iter().find(|g| g["name"] == name).unwrap_or_else(|| panic!("{name} not listed")).clone();
st["linked"].as_array().unwrap().iter().map(|v| v.as_u64().unwrap()).collect()
}
#[test]
fn styles_carry_opacity_and_blend_through_a_save() {
let mut s = session();
let a = rect(&mut s, 10.0);
run(&mut s, "transparency.set", json!({"item": null, "opacity": 50, "blend": "multiply", "knockout": true}));
run(&mut s, "graphicStyle.new", json!({"name": "Half"}));
let g = style(&s, "Half");
assert_eq!((g.opacity, g.blend, g.knockout), (0.5, BlendMode::Multiply, Knockout::On));
let b = rect(&mut s, 100.0);
run(&mut s, "graphicStyle.apply", json!({"name": "Half"}));
let nb = node(&s, b);
assert_eq!((nb.opacity, nb.blend, nb.knockout), (0.5, BlendMode::Multiply, Knockout::On));
let l = run(&mut s, "graphicStyle.list", json!({}));
let half = l["styles"].as_array().unwrap().iter().find(|g| g["name"] == "Half").unwrap();
assert_eq!((half["opacity"].as_f64(), half["blend"].as_str()), (Some(50.0), Some("Multiply")));
assert_eq!(linked(&mut s, "Half"), vec![a.0, b.0]);
assert_eq!(l["selected"], "Half");
// Native round trip: the style's transparency, its id and the objects' links survive.
let doc = s.doc().unwrap().doc.clone();
let back = vectorcraft_format::load(&vectorcraft_format::save(&doc, false)).unwrap();
assert_eq!(back.graphic_styles, doc.graphic_styles);
assert_eq!(back.node(b).unwrap().graphic_style, Some(g.id));
// Styles and objects from files without links still load (and get an id when first linked).
let old: GraphicStyle = serde_json::from_value(json!({"name": "Old", "appearance": {"items": []}})).unwrap();
assert_eq!((old.id, old.opacity, old.blend, old.isolate), (0, 1.0, BlendMode::Normal, false));
let mut d = (*doc).clone();
d.graphic_styles.push(old);
let i = d.graphic_styles.len() - 1;
assert_eq!(d.graphic_style_id(i), d.graphic_styles.iter().map(|g| g.id).max().unwrap());
assert!(!serde_json::to_string(&GraphicStyle::new("Plain", Default::default())).unwrap().contains("opacity"));
}
#[test]
fn a_style_from_a_group_or_type_is_not_empty() {
let mut s = session();
let a = rect(&mut s, 10.0);
let b = rect(&mut s, 100.0);
run(&mut s, "select.set", json!({"ids": [a.0, b.0]}));
run(&mut s, "paint.setFill", json!({"color": "#ff0000"}));
let g = NodeId(run(&mut s, "object.group", json!({}))["id"].as_u64().unwrap());
assert!(node(&s, g).appearance.items.is_empty());
run(&mut s, "graphicStyle.new", json!({"name": "Group Look"}));
let gs = style(&s, "Group Look");
assert_eq!(gs.appearance.fill_paint(), Paint::solid(Color::from_hex("#ff0000").unwrap()));
// Both members share the look, so the group is linked.
assert_eq!(linked(&mut s, "Group Look"), vec![g.0]);
let t = NodeId(run(&mut s, "text.create", json!({"x": 10, "y": 200, "text": "Hi"}))["id"].as_u64().unwrap());
run(&mut s, "select.set", json!({"ids": [t.0]}));
run(&mut s, "graphicStyle.new", json!({"name": "Type Look"}));
assert!(!style(&s, "Type Look").appearance.fill_paint().is_none());
// Applying to a group styles its contents and links the group.
let c = rect(&mut s, 200.0);
run(&mut s, "graphicStyle.apply", json!({"name": "Black Outline", "ids": [g.0]}));
assert_eq!(node(&s, a).appearance, style(&s, "Black Outline").appearance);
assert_eq!(node(&s, g).graphic_style, Some(style(&s, "Black Outline").id));
assert_ne!(node(&s, c).appearance, style(&s, "Black Outline").appearance);
}
#[test]
fn alt_apply_adds_the_style_on_top() {
let mut s = session();
rect(&mut s, 10.0);
run(&mut s, "appearance.clear", json!({}));
run(&mut s, "paint.setFill", json!({"color": "#00ff00"}));
run(&mut s, "effect.apply", json!({"effect": "distort.roughen"}));
run(&mut s, "graphicStyle.new", json!({"name": "Rough"}));
let rough = style(&s, "Rough");
let b = rect(&mut s, 100.0);
let before = node(&s, b).appearance;
run(&mut s, "graphicStyle.apply", json!({"name": "Rough", "add": true}));
let after = node(&s, b).appearance;
assert_eq!(after.items.len(), before.items.len() + rough.appearance.items.len());
assert_eq!(after.items[..before.items.len()], before.items[..]);
assert_eq!(after.items.last(), rough.appearance.items.last());
assert_eq!(after.effects.len(), before.effects.len() + 1);
assert_eq!(node(&s, b).graphic_style, None);
// One undo step.
run(&mut s, "edit.undo", json!({}));
assert_eq!(node(&s, b).appearance, before);
}
#[test]
fn style_names_are_unique() {
let mut s = session();
rect(&mut s, 10.0);
assert_eq!(run(&mut s, "graphicStyle.new", json!({"name": "X"}))["name"], "X");
assert_eq!(run(&mut s, "graphicStyle.new", json!({"name": "X"}))["name"], "X 2");
let n = s.doc().unwrap().doc.graphic_styles.len();
assert_eq!(run(&mut s, "graphicStyle.new", json!({}))["name"], format!("Graphic Style {}", n + 1));
assert!(s.execute("graphicStyle.rename", &json!({"name": "X 2", "to": "X"})).is_err());
assert!(s.execute("graphicStyle.rename", &json!({"name": "X 2", "to": " "})).is_err());
assert_eq!(run(&mut s, "graphicStyle.rename", json!({"name": "X 2", "to": "Y"}))["name"], "Y");
assert_eq!(run(&mut s, "graphicStyle.duplicate", json!({"name": "Y"}))["name"], "Y copy");
assert_ne!(style(&s, "Y copy").id, style(&s, "Y").id);
}
/// Three rectangles: `a` (the source of style "S"), `b` and `c`, all linked to "S".
fn linked_three(s: &mut Session) -> (NodeId, NodeId, NodeId) {
let a = rect(s, 10.0);
run(s, "graphicStyle.new", json!({"name": "S"}));
let b = rect(s, 100.0);
let c = rect(s, 200.0);
run(s, "graphicStyle.apply", json!({"name": "S", "ids": [b.0, c.0]}));
assert_eq!(linked(s, "S"), vec![a.0, b.0, c.0]);
(a, b, c)
}
#[test]
fn redefine_updates_every_linked_object() {
let mut s = session();
let (a, b, c) = linked_three(&mut s);
set_fill(&mut s, a, "#0000ff");
// Editing the source broke its link, but it remembers the style Redefine replaces.
assert_eq!(linked(&mut s, "S"), vec![b.0, c.0]);
run(&mut s, "select.set", json!({"ids": [a.0]}));
assert_eq!(s.selection_graphic_style().map(|(g, linked)| (g.name.clone(), linked)), Some(("S".into(), false)));
run(&mut s, "graphicStyle.redefine", json!({}));
let blue = Paint::solid(Color::from_hex("#0000ff").unwrap());
assert_eq!((fill(&s, b), fill(&s, c)), (blue.clone(), blue));
assert_eq!(linked(&mut s, "S"), vec![a.0, b.0, c.0]);
run(&mut s, "edit.undo", json!({}));
assert_eq!(fill(&s, b), Paint::solid(Color::WHITE));
}
#[test]
fn redefine_leaves_unlinked_and_edited_objects_alone() {
let mut s = session();
let (a, b, c) = linked_three(&mut s);
run(&mut s, "graphicStyle.breakLink", json!({"ids": [c.0]}));
assert_eq!(node(&s, c).graphic_style, None);
let d = rect(&mut s, 300.0);
run(&mut s, "graphicStyle.apply", json!({"name": "S"}));
set_fill(&mut s, d, "#00ff00");
set_fill(&mut s, a, "#0000ff");
run(&mut s, "graphicStyle.redefine", json!({"name": "S", "id": a.0}));
assert_eq!(fill(&s, b), Paint::solid(Color::from_hex("#0000ff").unwrap()));
assert_eq!(fill(&s, c), Paint::solid(Color::WHITE));
// The edited object keeps its look and is unlinked.
assert_eq!(fill(&s, d), Paint::solid(Color::from_hex("#00ff00").unwrap()));
assert_eq!(node(&s, d).graphic_style, None);
assert!(s.execute("graphicStyle.redefine", &json!({"id": c.0})).is_err());
}
#[test]
fn deleting_a_style_unlinks_and_renaming_keeps_links() {
let mut s = session();
let (_, b, c) = linked_three(&mut s);
run(&mut s, "graphicStyle.rename", json!({"name": "S", "to": "T"}));
assert_eq!(linked(&mut s, "T").len(), 3);
run(&mut s, "select.set", json!({"ids": [b.0]}));
assert_eq!(run(&mut s, "select.same.graphicStyle", json!({}))["count"], 3);
let look = node(&s, c).appearance;
run(&mut s, "graphicStyle.delete", json!({"name": "T"}));
assert_eq!((node(&s, b).graphic_style, node(&s, c).graphic_style), (None, None));
assert_eq!(node(&s, c).appearance, look);
run(&mut s, "select.set", json!({"ids": [b.0]}));
assert!(s.execute("select.same.graphicStyle", &json!({})).is_err());
}
#[test]
fn unused_styles_and_sort_by_name() {
let mut s = session();
linked_three(&mut s);
let unused = run(&mut s, "graphicStyle.unused", json!({}));
let names: Vec<&str> = unused["names"].as_array().unwrap().iter().filter_map(Value::as_str).collect();
assert!(names.contains(&"Sunshine") && !names.contains(&"S"));
run(&mut s, "graphicStyle.delete", json!({"names": names}));
assert_eq!(s.doc().unwrap().doc.graphic_styles.len(), 1);
run(&mut s, "edit.undo", json!({}));
run(&mut s, "graphicStyle.sortByName", json!({}));
let names: Vec<String> = s.doc().unwrap().doc.graphic_styles.iter().map(|g| g.name.clone()).collect();
assert_eq!(names, ["Default Graphic Style", "Black Outline", "Heavy Ink", "S", "Sunshine"]);
}
#[test]
fn select_same_appearance_attribute() {
let mut s = session();
let a = rect(&mut s, 10.0);
run(&mut s, "effect.apply", json!({"effect": "distort.roughen"}));
let b = rect(&mut s, 100.0);
set_fill(&mut s, b, "#ff0000");
run(&mut s, "effect.apply", json!({"effect": "distort.roughen"}));
let c = rect(&mut s, 200.0);
// The shared effect.
run(&mut s, "select.set", json!({"ids": [a.0]}));
run(&mut s, "select.same.appearanceAttribute", json!({}));
assert_eq!(s.doc().unwrap().selection.objects, vec![a, b]);
// The shared stroke (item 1).
run(&mut s, "select.set", json!({"ids": [a.0]}));
run(&mut s, "select.same.appearanceAttribute", json!({"item": 1}));
assert_eq!(s.doc().unwrap().selection.objects, vec![a, b, c]);
}
#[test]
fn document_info_lists_style_names() {
let mut s = session();
let (a, ..) = linked_three(&mut s);
let i = run(&mut s, "document.info", json!({}));
assert_eq!(i["graphicStyleNames"].as_array().unwrap().len(), 5);
run(&mut s, "select.set", json!({"ids": [a.0]}));
assert_eq!(run(&mut s, "document.info", json!({"selectionOnly": true}))["graphicStyleNames"], json!(["S"]));
}