Files
effectcraft/crates/engine/src/tests/styles.rs
T

168 lines
10 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.
//! Layer Styles commands: add/remove/show, eye switches, Global Light linkage, undo, serde.
use serde_json::json;
use crate::Session;
fn session() -> (Session, u64, u64) {
let mut s = Session::default();
s.execute("comp.new", json!({"name": "Styles", "width": 200, "height": 100, "frameRate": 30, "duration": 2})).unwrap();
let a = s.execute("layer.newSolid", json!({"color": "#ffffff", "width": 40, "height": 40})).unwrap()["layer"].as_u64().unwrap();
let b = s.execute("layer.newSolid", json!({"color": "#808080", "width": 40, "height": 40})).unwrap()["layer"].as_u64().unwrap();
(s, a, b)
}
fn styles_of(s: &Session, layer: u64) -> Vec<(String, bool)> {
let v = s.execute_query("layer.style.list", json!({"layer": layer}));
v["styles"].as_array().unwrap().iter().map(|x| (x["style"].as_str().unwrap().to_string(), x["enabled"].as_bool().unwrap())).collect()
}
impl Session {
fn execute_query(&self, id: &str, p: serde_json::Value) -> serde_json::Value {
let mut c = Session { project: self.project.clone(), state: self.state.clone(), ..Default::default() };
c.execute(id, p).unwrap()
}
}
#[test]
fn add_remove_show_all_with_undo() {
let (mut s, a, _) = session();
s.execute("layer.select", json!({"layers": [a]})).unwrap();
s.execute("layer.style.stroke", json!({})).unwrap();
s.execute("layer.style.dropShadow", json!({})).unwrap();
s.execute("layer.style.add", json!({"style": "Bevel and Emboss"})).unwrap();
assert_eq!(styles_of(&s, a), vec![("dropShadow".into(), true), ("bevelEmboss".into(), true), ("stroke".into(), true)]);
// Values are ordinary properties.
s.execute("prop.set", json!({"layer": a, "path": "layerStyles/dropShadow/distance", "value": 12})).unwrap();
let v = s.execute("prop.get", json!({"layer": a, "path": "layerStyles/dropShadow/distance"})).unwrap();
assert!(v.to_string().contains("12"));
// Eye switch.
s.execute("layer.style.toggle", json!({"layer": a, "style": "stroke"})).unwrap();
assert!(styles_of(&s, a).contains(&("stroke".into(), false)));
s.execute("edit.undo", json!({})).unwrap();
assert!(styles_of(&s, a).contains(&("stroke".into(), true)));
// Remove one, then everything.
s.execute("layer.style.remove", json!({"layer": a, "style": "bevelEmboss"})).unwrap();
assert_eq!(styles_of(&s, a).len(), 2);
s.execute("layer.style.removeAll", json!({})).unwrap();
assert!(styles_of(&s, a).is_empty());
let layer = s.active_comp().unwrap().layer(effectcraft_project::LayerId(a)).unwrap().clone();
assert!(layer.layer_styles().is_none());
s.execute("edit.undo", json!({})).unwrap();
assert_eq!(styles_of(&s, a).len(), 2);
s.execute("edit.redo", json!({})).unwrap();
assert!(styles_of(&s, a).is_empty());
// Show All adds every style switched off; choosing one switches it on.
s.execute("layer.style.showAll", json!({})).unwrap();
let all = styles_of(&s, a);
assert_eq!(all.len(), 9);
assert!(all.iter().all(|(_, on)| !on));
s.execute("layer.style.satin", json!({})).unwrap();
assert!(styles_of(&s, a).contains(&("satin".into(), true)));
// Removing the last style removes the group.
s.execute("layer.style.removeAll", json!({})).unwrap();
s.execute("layer.style.colorOverlay", json!({})).unwrap();
s.execute("layer.style.remove", json!({"layer": a, "style": "Color Overlay"})).unwrap();
assert!(s.active_comp().unwrap().layer(effectcraft_project::LayerId(a)).unwrap().layer_styles().is_none());
// Convert to Editable Styles is unavailable without PSD styles.
assert!(!s.is_enabled("layer.style.convertToEditable"));
}
#[test]
fn global_light_is_shared_by_the_comp() {
let (mut s, a, b) = session();
s.execute("layer.style.dropShadow", json!({"layers": [a]})).unwrap();
s.execute("layer.style.bevelEmboss", json!({"layers": [b]})).unwrap();
// Editing one layer's Global Light Angle updates the other layer and the comp.
s.execute("prop.set", json!({"layer": a, "path": "layerStyles/blendingOptions/globalLightAngle", "value": 45})).unwrap();
let bv = s.execute("prop.get", json!({"layer": b, "path": "layerStyles/blendingOptions/globalLightAngle"})).unwrap();
assert!(bv.to_string().contains("45"), "{bv}");
let gl = s.execute("layer.style.globalLight", json!({})).unwrap();
assert_eq!(gl["angle"].as_f64(), Some(45.0));
// The command sets both and new style groups pick it up.
s.execute("layer.style.globalLight", json!({"angle": 10, "altitude": 60})).unwrap();
let c = s.execute("layer.newSolid", json!({"color": "#ff0000", "width": 10, "height": 10})).unwrap()["layer"].as_u64().unwrap();
s.execute("layer.style.innerShadow", json!({"layers": [c]})).unwrap();
for l in [a, b, c] {
let v = s.execute("prop.get", json!({"layer": l, "path": "layerStyles/blendingOptions/globalLightAltitude"})).unwrap();
assert!(v.to_string().contains("60"), "{l}: {v}");
}
// Undo restores the previous shared value on every layer.
s.execute("edit.undo", json!({})).unwrap();
s.execute("edit.undo", json!({})).unwrap();
s.execute("edit.undo", json!({})).unwrap();
let v = s.execute("prop.get", json!({"layer": b, "path": "layerStyles/blendingOptions/globalLightAngle"})).unwrap();
assert!(v.to_string().contains("45"), "{v}");
}
#[test]
fn styles_render_and_round_trip() {
let (mut s, a, _) = session();
s.execute("layer.style.stroke", json!({"layers": [a]})).unwrap();
s.execute("prop.set", json!({"layer": a, "path": "layerStyles/stroke/size", "value": 6})).unwrap();
let cid = s.active_comp_id().unwrap();
// Cached session render == uncached render, and the stroke shows outside the 40×40 square.
let img = s.render(cid, s.time(), Default::default());
let r = effectcraft_render::render_frame(&s.project, cid, s.time(), 1.0);
assert_eq!(img.data, r.data);
assert!(img.get(123, 50)[0] > 0.98 && img.get(123, 50)[1] < 0.02, "{:?}", img.get(123, 50));
// Change the colour through a command: the cache must not serve stale pixels.
s.execute("prop.set", json!({"layer": a, "path": "layerStyles/stroke/color", "value": [0, 0, 1, 1]})).unwrap();
let img2 = s.render(cid, s.time(), Default::default());
assert!(img2.get(123, 50)[2] > 0.98 && img2.get(123, 50)[0] < 0.02);
// Serde round trip keeps styles and the comp's Global Light.
s.execute("layer.style.globalLight", json!({"angle": 33})).unwrap();
let json = serde_json::to_string(&*s.project).unwrap();
let back: effectcraft_project::Project = serde_json::from_str(&json).unwrap();
assert_eq!(back, *s.project);
assert_eq!(back.comp(cid).unwrap().global_light.angle.value.as_f64(), 33.0);
}
/// #1389: Blending Options on a layer with no styles. `layer.style.add` with `blendingOptions`
/// creates the Layer Styles group with Blending Options only; with the same merge key as the
/// value set right after it, both are one undo step, and undoing it leaves no group behind.
#[test]
fn blending_options_alone_create_the_group_in_one_undo_step() {
let (mut s, a, b) = session();
let undo = s.history.undo.len();
let r = s.execute("layer.style.add", json!({"layers": [a], "style": "blendingOptions", "merge": "bo"})).unwrap();
assert_eq!(r["styles"][0]["style"], "blendingOptions", "{r}");
let layer = s.active_comp().unwrap().layer(effectcraft_project::LayerId(a)).unwrap().clone();
let g = layer.layer_styles().expect("the group is created");
assert!(g.sub("blendingOptions").is_some());
assert!(styles_of(&s, a).is_empty(), "no style is added with it");
s.execute("prop.set", json!({"layer": a, "path": "layerStyles/blendingOptions/advancedBlending/fillOpacity", "value": 40, "merge": "bo"})).unwrap();
s.execute("prop.set", json!({"layer": a, "path": "layerStyles/blendingOptions/advancedBlending/fillOpacity", "value": 30, "merge": "bo"})).unwrap();
assert_eq!(s.history.undo.len(), undo + 1, "creating the group and setting the value are one step");
// The other layer is untouched, and Global Light is the comp's.
assert!(s.active_comp().unwrap().layer(effectcraft_project::LayerId(b)).unwrap().layer_styles().is_none());
let gl = s.execute("prop.get", json!({"layer": a, "path": "layerStyles/blendingOptions/globalLightAngle"})).unwrap();
assert!(gl.to_string().contains("120"), "{gl}");
s.execute("edit.undo", json!({})).unwrap();
assert!(s.active_comp().unwrap().layer(effectcraft_project::LayerId(a)).unwrap().layer_styles().is_none(), "one undo removes the group");
// By display name too.
s.execute("layer.style.add", json!({"layers": [a], "style": "Blending Options"})).unwrap();
assert!(s.active_comp().unwrap().layer(effectcraft_project::LayerId(a)).unwrap().layer_styles().is_some());
}
/// Removing the last style keeps a Layer Styles group whose Blending Options were changed (a
/// group with Blending Options only is a state of its own since #1389); at their defaults the
/// group goes, as before.
#[test]
fn removing_the_last_style_keeps_changed_blending_options() {
let (mut s, a, b) = session();
let fill = "layerStyles/blendingOptions/advancedBlending/fillOpacity";
s.execute("layer.style.add", json!({"layers": [a], "style": "blendingOptions"})).unwrap();
s.execute("prop.set", json!({"layer": a, "path": fill, "value": 40})).unwrap();
s.execute("layer.style.dropShadow", json!({"layers": [a]})).unwrap();
s.execute("layer.style.remove", json!({"layer": a, "style": "dropShadow"})).unwrap();
let v = s.execute("prop.get", json!({"layer": a, "path": fill})).unwrap();
assert_eq!(v["value"].as_f64(), Some(40.0), "{v}");
assert!(styles_of(&s, a).is_empty());
// At the defaults (here on the other layer, through the same command) the group goes.
s.execute("layer.style.add", json!({"layers": [b], "style": "blendingOptions"})).unwrap();
s.execute("layer.style.dropShadow", json!({"layers": [b]})).unwrap();
s.execute("layer.style.remove", json!({"layer": b, "style": "dropShadow"})).unwrap();
assert!(s.active_comp().unwrap().layer(effectcraft_project::LayerId(b)).unwrap().layer_styles().is_none());
}