vectorcraft/crates/engine/src/tests_knockout.rs

178 lines
8.4 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.

//! Knockout groups, the knockout shape and the page group (M3.46–M3.48): commands, undo, files
//! (old bools load) and the PDF and SVG output.
use serde_json::{Value, json};
use vectorcraft_doc::Knockout;
use super::*;
fn session() -> Session {
let mut s = Session::new();
s.execute("file.new", &json!({"width": 200, "height": 200})).unwrap();
s
}
fn run(s: &mut Session, id: &str, p: Value) -> Value {
s.execute(id, &p).unwrap()
}
fn node(s: &Session, id: u64) -> vectorcraft_doc::Node {
s.doc().unwrap().doc.node(NodeId(id)).unwrap().clone()
}
fn steps(s: &Session) -> usize {
s.doc().unwrap().history.undo.len()
}
/// A group of two overlapping 50% rectangles, selected. Returns the group id.
fn knockout_group(s: &mut Session) -> u64 {
let a = run(s, "shape.rectangle", json!({"x": 10, "y": 10, "width": 100, "height": 100}))["id"].clone();
let b = run(s, "shape.rectangle", json!({"x": 60, "y": 60, "width": 100, "height": 100}))["id"].clone();
run(s, "transparency.set", json!({"ids": [a, b], "opacity": 50}));
run(s, "select.set", json!({"ids": [a, b]}));
run(s, "object.group", json!({}))["id"].as_u64().unwrap()
}
#[test]
fn knockout_takes_three_states_names_and_bools() {
let mut s = session();
let g = knockout_group(&mut s);
assert_eq!(node(&s, g).knockout, Knockout::Neutral, "new groups are neutral");
for (param, want) in [
(json!("on"), Knockout::On),
(json!("Off"), Knockout::Off),
(json!("neutral"), Knockout::Neutral),
(json!(true), Knockout::On),
(json!(false), Knockout::Neutral),
] {
let before = steps(&s);
run(&mut s, "transparency.set", json!({"knockout": param}));
assert_eq!((node(&s, g).knockout, steps(&s)), (want, before + 1), "{param}");
}
run(&mut s, "transparency.set", json!({"knockout": "off"}));
let info = run(&mut s, "transparency.info", json!({}));
assert_eq!((info["knockout"].clone(), info["knockoutShape"].clone()), (json!("off"), json!(false)));
// A bad state changes nothing, also next to an opacity (which would go to an appearance item).
let before = steps(&s);
assert!(s.execute("transparency.set", &json!({"knockout": "maybe"})).is_err());
assert!(s.execute("transparency.set", &json!({"knockout": 3, "opacity": 20})).is_err());
assert!(s.execute("object.setProps", &json!({"knockout": "maybe"})).is_err());
assert_eq!((node(&s, g).knockout, node(&s, g).opacity, steps(&s)), (Knockout::Off, 1.0, before));
// Undo is one step per change.
run(&mut s, "edit.undo", json!({}));
assert_eq!(node(&s, g).knockout, Knockout::Neutral);
// The panel's click order.
assert_eq!([Knockout::On, Knockout::Neutral, Knockout::Off].map(Knockout::cycle), [Knockout::Neutral, Knockout::Off, Knockout::On]);
}
#[test]
fn knockout_shape_is_an_object_option() {
let mut s = session();
let g = knockout_group(&mut s);
let child = node(&s, g).children().unwrap()[1].id.0;
run(&mut s, "transparency.set", json!({"ids": [child], "knockoutShape": true}));
assert!(node(&s, child).knockout_shape && !node(&s, child).has_default_transparency());
let info = run(&mut s, "transparency.info", json!({"id": child}));
assert_eq!(info["knockoutShape"], true);
let mixed = run(&mut s, "transparency.info", json!({"ids": [child, g]}));
assert_eq!(mixed["knockoutShape"], Value::Null);
run(&mut s, "edit.undo", json!({}));
assert!(!node(&s, child).knockout_shape);
}
#[test]
fn page_group_toggles_are_undoable_document_settings() {
let mut s = session();
let doc = |s: &Session| (s.doc().unwrap().doc.page_isolate, s.doc().unwrap().doc.page_knockout);
let before = steps(&s);
assert_eq!(run(&mut s, "transparency.togglePageIsolatedBlending", json!({})), json!({"value": true}));
assert_eq!(run(&mut s, "transparency.togglePageKnockoutGroup", json!({"value": true})), json!({"value": true}));
assert_eq!((doc(&s), steps(&s)), ((true, true), before + 2));
// Setting the current value is not a change.
run(&mut s, "transparency.togglePageKnockoutGroup", json!({"value": true}));
assert_eq!(steps(&s), before + 2);
let info = run(&mut s, "transparency.info", json!({}));
assert_eq!((info["pageIsolatedBlending"].clone(), info["pageKnockoutGroup"].clone()), (json!(true), json!(true)));
run(&mut s, "edit.undo", json!({}));
assert_eq!(doc(&s), (true, false));
run(&mut s, "transparency.togglePageIsolatedBlending", json!({}));
assert_eq!(doc(&s), (false, false));
}
#[test]
fn knockout_settings_round_trip_and_old_bools_load() {
let mut s = session();
let g = knockout_group(&mut s);
let child = node(&s, g).children().unwrap()[0].id.0;
run(&mut s, "transparency.set", json!({"ids": [g], "knockout": "off"}));
run(&mut s, "transparency.set", json!({"ids": [child], "knockout": "on", "knockoutShape": true}));
run(&mut s, "transparency.togglePageIsolatedBlending", json!({}));
run(&mut s, "transparency.togglePageKnockoutGroup", json!({}));
let bytes = vectorcraft_format::save(&s.doc().unwrap().doc, false);
let back = vectorcraft_format::load(&bytes).unwrap();
let (bg, bc) = (back.node(NodeId(g)).unwrap(), back.node(NodeId(child)).unwrap());
assert_eq!((bg.knockout, bc.knockout, bc.knockout_shape, back.page_isolate, back.page_knockout), (Knockout::Off, Knockout::On, true, true, true));
// Defaults are not written.
let text = String::from_utf8(vectorcraft_format::save(&session().doc().unwrap().doc, false)).unwrap();
assert!(!text.contains("knockout") && !text.contains("page_isolate"), "{text}");
// Files from before the three states stored a bool: true is On, false is Neutral.
let mut v: Value = serde_json::from_slice(&bytes).unwrap();
let mut patched = 0;
fn patch(v: &mut Value, n: &mut usize) {
match v {
Value::Object(m) => {
if let Some(k) = m.get_mut("knockout") {
*k = json!(*k == json!("on"));
*n += 1;
}
m.values_mut().for_each(|c| patch(c, n));
}
Value::Array(a) => a.iter_mut().for_each(|c| patch(c, n)),
_ => {}
}
}
patch(&mut v, &mut patched);
assert_eq!(patched, 2);
let old = vectorcraft_format::load(&serde_json::to_vec(&v).unwrap()).unwrap();
assert_eq!((old.node(NodeId(g)).unwrap().knockout, old.node(NodeId(child)).unwrap().knockout), (Knockout::Neutral, Knockout::On));
}
#[test]
fn pdf_and_svg_write_knockout_groups_as_masks() {
let mut s = session();
let g = knockout_group(&mut s);
let plain = s.doc().unwrap().doc.clone();
run(&mut s, "transparency.set", json!({"ids": [g], "knockout": "on"}));
let ko = s.doc().unwrap().doc.clone();
let pdf = |d: &Document| vectorcraft_pdf::export_with_report(d, &vectorcraft_pdf::PdfOptions::uncompressed()).unwrap();
let r = pdf(&plain);
assert!(!String::from_utf8_lossy(&r.bytes).contains("/SMask") && r.warnings.is_empty());
let r = pdf(&ko);
let text = String::from_utf8_lossy(&r.bytes);
// The lower rectangle is drawn through a luminosity mask made of the upper one's alpha.
assert!(text.contains("/Luminosity") && text.contains("/Alpha"), "soft masks");
assert!(r.warnings.iter().any(|w| w.contains("knockout")), "{:?}", r.warnings);
let svg = |d: &Document| vectorcraft_svg::export(d, &Default::default());
assert!(!svg(&plain).contains("knockout"));
let out = svg(&ko);
assert_eq!(out.matches("mask=\"url(#knockout-").count(), 1, "{out}");
assert!(out.contains("<feColorMatrix") && out.contains("isolation"), "{out}");
// A neutral group passes its children through: still one mask per element below the top one.
run(&mut s, "select.set", json!({"ids": [g]}));
let outer = run(&mut s, "object.group", json!({}))["id"].as_u64().unwrap();
run(&mut s, "transparency.set", json!({"ids": [g], "knockout": "neutral"}));
run(&mut s, "transparency.set", json!({"ids": [outer], "knockout": "on"}));
assert_eq!(svg(&s.doc().unwrap().doc).matches("mask=\"url(#knockout-").count(), 1);
// The page group: an isolated group around the page content.
let mut page = (*plain).clone();
page.page_isolate = true;
assert!(svg(&page).contains("isolation"));
page.page_knockout = true;
assert_eq!(svg(&page).matches("mask=\"url(#knockout-").count(), 1, "the layer is neutral: its objects knock each other out");
assert!(String::from_utf8_lossy(&pdf(&page).bytes).contains("/Luminosity"));
}