120 lines
6.6 KiB
Rust
120 lines
6.6 KiB
Rust
//! 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);
|
||
}
|