effectcraft/crates/engine/src/commands/styles.rs

245 lines
12 KiB
Rust

//! Layer ▸ Layer Styles: add / remove / show styles and the comp's Global Light.
//!
//! Styles are property groups under the layer's `layerStyles` group (see
//! [`effectcraft_project::styles`]); their values are edited with `prop.set` like any other
//! property (`layerStyles/dropShadow/distance`). Global Light is shared by every layer of a comp:
//! editing one layer's `layerStyles/blendingOptions/globalLightAngle` updates them all.
use effectcraft_project::build::Ids;
use effectcraft_project::styles::{self as st, GROUP, STYLES};
use effectcraft_project::{ItemId, LayerId, Uid};
use serde_json::{Value, json};
use super::{CommandSpec, b_p, bad, f_p, has_comp, has_layers, layer_mut, layer_p, layers_p, str_p, time_p};
use crate::{EngineError, Result, Session, cmd};
/// Selected (or given) layers that take layer styles.
fn style_layers(s: &Session, p: &Value) -> Result<(ItemId, Vec<LayerId>)> {
let (cid, ids) = layers_p(s, p)?;
let comp = s.project.comp(cid).ok_or(EngineError::NoComp)?;
Ok((cid, ids.into_iter().filter(|id| comp.layer(*id).is_some_and(|l| l.can_have_styles())).collect()))
}
fn add_styles(s: &mut Session, p: &Value, cmd: &str, styles: &[&str], enabled: bool, label: &str) -> Result<Value> {
let (cid, ids) = style_layers(s, p)?;
if ids.is_empty() {
return Err(bad(cmd, "no layer that takes layer styles (footage, solid, precomp, text or shape layer)"));
}
let uids = s.edit(label, None, |proj, state| {
let gl = proj.comp(cid).ok_or(EngineError::NoComp)?.global_light.clone();
let mut out = vec![];
let mut sel = vec![];
for lid in &ids {
let mut next = proj.next_id;
let l = layer_mut(proj, cid, *lid)?;
for id in styles {
let had = l.layer_styles().and_then(|g| g.sub(id)).is_some();
let uid = st::add_style(l, &mut Ids(&mut next), id, &gl, enabled).ok_or_else(|| bad(cmd, format!("unknown style `{id}`")))?;
// Choosing a style that is already there (e.g. after Show All) switches it on.
if had
&& enabled
&& let Some(g) = l.props.find_group_mut(uid)
{
g.enabled = true;
}
if let Some(g) = l.props.sub_mut(GROUP) {
g.enabled = true;
}
out.push(json!({"layer": lid.0, "style": id, "uid": uid}));
sel.push((*lid, uid));
}
proj.next_id = next;
}
state.selected_props = sel;
Ok(out)
})?;
Ok(json!({"styles": uids}))
}
fn add(s: &mut Session, p: &Value) -> Result<Value> {
let name = str_p(p, "style").ok_or_else(|| bad("layer.style.add", "missing `style` (e.g. dropShadow or \"Drop Shadow\")"))?;
let id = st::style_id(name)
.ok_or_else(|| bad("layer.style.add", format!("unknown style `{name}`; one of {}", STYLES.iter().map(|s| s.0).collect::<Vec<_>>().join(", "))))?;
let label = STYLES.iter().find(|s| s.0 == id).map(|s| s.1).unwrap_or(id);
add_styles(s, p, "layer.style.add", &[id], true, label)
}
macro_rules! style_cmd {
($fn:ident, $id:literal) => {
fn $fn(s: &mut Session, p: &Value) -> Result<Value> {
let label = STYLES.iter().find(|x| x.0 == $id).map(|x| x.1).unwrap_or($id);
add_styles(s, p, concat!("layer.style.", $id), &[$id], true, label)
}
};
}
style_cmd!(drop_shadow, "dropShadow");
style_cmd!(inner_shadow, "innerShadow");
style_cmd!(outer_glow, "outerGlow");
style_cmd!(inner_glow, "innerGlow");
style_cmd!(bevel_emboss, "bevelEmboss");
style_cmd!(satin, "satin");
style_cmd!(color_overlay, "colorOverlay");
style_cmd!(gradient_overlay, "gradientOverlay");
style_cmd!(stroke, "stroke");
/// Show All: every style is added (new ones switched off) so they can be revealed and edited.
fn show_all(s: &mut Session, p: &Value) -> Result<Value> {
let all: Vec<&str> = STYLES.iter().map(|s| s.0).collect();
add_styles(s, p, "layer.style.showAll", &all, false, "Show All Layer Styles")
}
fn remove_all(s: &mut Session, p: &Value) -> Result<Value> {
let (cid, ids) = style_layers(s, p)?;
let n = s.edit("Remove All Layer Styles", None, |proj, state| {
let mut n = 0;
for lid in &ids {
let l = layer_mut(proj, cid, *lid)?;
if let Some(g) = l.layer_styles() {
let gone: Vec<Uid> = std::iter::once(g.uid).chain(g.groups().map(|x| x.uid)).collect();
l.props.children.retain(|c| c.match_id() != GROUP);
state.selected_props.retain(|(_, u)| !gone.contains(u));
n += 1;
}
}
Ok(n)
})?;
Ok(json!({"layers": n}))
}
/// A style group of a layer by match id, display name, uid or the selection.
fn find_style(s: &Session, p: &Value, cmd: &str) -> Result<(ItemId, LayerId, Uid)> {
let (cid, lid) = layer_p(s, p, cmd)?;
let layer = s.project.comp(cid).and_then(|c| c.layer(lid)).ok_or(EngineError::NoComp)?;
let g = layer.layer_styles().ok_or_else(|| bad(cmd, "the layer has no layer styles"))?;
let found = match p.get("style") {
Some(Value::Number(n)) => {
let n = n.as_u64().unwrap_or(0);
if n == g.uid { Some(g.uid) } else { g.groups().find(|x| x.uid == n).map(|x| x.uid) }
}
Some(Value::String(name)) if name == GROUP || name.eq_ignore_ascii_case("Layer Styles") => Some(g.uid),
Some(Value::String(name)) => {
let id = st::style_id(name).unwrap_or(name.as_str());
g.groups().find(|x| x.match_id == id || x.name.eq_ignore_ascii_case(name)).map(|x| x.uid)
}
_ => s.state.selected_props.iter().find_map(|(l, u)| (*l == lid).then(|| g.groups().find(|x| x.uid == *u).map(|x| x.uid)).flatten()),
}
.ok_or_else(|| bad(cmd, "no such layer style"))?;
Ok((cid, lid, found))
}
fn remove(s: &mut Session, p: &Value) -> Result<Value> {
let (cid, lid, uid) = find_style(s, p, "layer.style.remove")?;
s.edit("Remove Layer Style", None, |proj, state| {
let l = layer_mut(proj, cid, lid)?;
let whole = l.layer_styles().is_some_and(|g| g.uid == uid);
if whole {
l.props.children.retain(|c| c.match_id() != GROUP);
} else if let Some(g) = l.props.sub_mut(GROUP) {
if g.sub(st::BLENDING).is_some_and(|b| b.uid == uid) {
return Err(bad("layer.style.remove", "Blending Options can't be removed"));
}
g.children.retain(|c| c.uid() != uid);
// The last style gone removes the Layer Styles group, as in AE.
if g.children.iter().all(|c| c.match_id() == st::BLENDING) {
l.props.children.retain(|c| c.match_id() != GROUP);
}
}
state.selected_props.retain(|(_, u)| *u != uid);
Ok(())
})?;
Ok(Value::Null)
}
/// Eye switch of a style (or of the whole Layer Styles group: `style: "layerStyles"`).
fn toggle(s: &mut Session, p: &Value) -> Result<Value> {
let (cid, lid, uid) = find_style(s, p, "layer.style.toggle")?;
let v = b_p(p, "value");
let r = s.edit("Toggle Layer Style", None, |proj, _| {
let g = layer_mut(proj, cid, lid)?.props.find_group_mut(uid).ok_or_else(|| bad("layer.style.toggle", "gone"))?;
g.enabled = v.unwrap_or(!g.enabled);
Ok(g.enabled)
})?;
Ok(json!(r))
}
fn global_light(s: &mut Session, p: &Value) -> Result<Value> {
let cid = super::comp_id(s, p)?;
let (angle, altitude) = (f_p(p, "angle"), f_p(p, "altitude"));
if angle.is_none() && altitude.is_none() {
let c = s.project.comp(cid).ok_or(EngineError::NoComp)?;
return Ok(json!({"angle": c.global_light.angle.value.as_f64(), "altitude": c.global_light.altitude.value.as_f64()}));
}
let t = time_p(s, p, s.project.comp(cid));
s.edit("Global Light", super::merge_p(p), |proj, _| {
let c = proj.comp_mut(cid).ok_or(EngineError::NoComp)?;
st::set_global_light(c, angle, altitude, t);
Ok(())
})?;
let c = s.project.comp(cid).ok_or(EngineError::NoComp)?;
Ok(json!({"angle": c.global_light.angle.value_at(t).as_f64(), "altitude": c.global_light.altitude.value_at(t).as_f64()}))
}
fn no_psd_styles(s: &Session) -> std::result::Result<(), String> {
has_layers(s)?;
Err("no Photoshop layer styles to convert (only layers imported from PSD files carry them)".into())
}
fn convert(_: &mut Session, _: &Value) -> Result<Value> {
Err(EngineError::Other("no Photoshop layer styles to convert".into()))
}
fn list(s: &mut Session, p: &Value) -> Result<Value> {
let (cid, lid) = layer_p(s, p, "layer.style.list")?;
let l = s.project.comp(cid).and_then(|c| c.layer(lid)).ok_or(EngineError::NoComp)?;
let styles: Vec<Value> = l
.layer_styles()
.map(|g| {
g.groups()
.filter(|x| x.match_id != st::BLENDING)
.map(|x| json!({"style": x.match_id, "name": x.name, "uid": x.uid, "enabled": x.enabled}))
.collect()
})
.unwrap_or_default();
Ok(
json!({"group": l.layer_styles().map(|g| g.uid), "enabled": l.layer_styles().map(|g| g.enabled), "styles": styles, "available": STYLES.iter().map(|s| json!({"style": s.0, "name": s.1})).collect::<Vec<_>>()}),
)
}
pub fn specs() -> Vec<CommandSpec> {
vec![
cmd!("layer.style.convertToEditable", "Convert to Editable Styles", ["Layer", "Layer Styles"], None, "{layers?}", no_psd_styles, convert),
cmd!("layer.style.showAll", "Show All", ["Layer", "Layer Styles"], None, "{layers?}", has_layers, show_all),
cmd!("layer.style.removeAll", "Remove All", ["Layer", "Layer Styles"], None, "{layers?}", has_layers, remove_all),
cmd!("layer.style.dropShadow", "Drop Shadow", ["Layer", "Layer Styles"], None, "{layers?}", has_layers, drop_shadow),
cmd!("layer.style.innerShadow", "Inner Shadow", ["Layer", "Layer Styles"], None, "{layers?}", has_layers, inner_shadow),
cmd!("layer.style.outerGlow", "Outer Glow", ["Layer", "Layer Styles"], None, "{layers?}", has_layers, outer_glow),
cmd!("layer.style.innerGlow", "Inner Glow", ["Layer", "Layer Styles"], None, "{layers?}", has_layers, inner_glow),
cmd!("layer.style.bevelEmboss", "Bevel and Emboss", ["Layer", "Layer Styles"], None, "{layers?}", has_layers, bevel_emboss),
cmd!("layer.style.satin", "Satin", ["Layer", "Layer Styles"], None, "{layers?}", has_layers, satin),
cmd!("layer.style.colorOverlay", "Color Overlay", ["Layer", "Layer Styles"], None, "{layers?}", has_layers, color_overlay),
cmd!("layer.style.gradientOverlay", "Gradient Overlay", ["Layer", "Layer Styles"], None, "{layers?}", has_layers, gradient_overlay),
cmd!("layer.style.stroke", "Stroke", ["Layer", "Layer Styles"], None, "{layers?}", has_layers, stroke),
cmd!(
"layer.style.add",
"Add Layer Style",
[],
None,
"{style: dropShadow|innerShadow|outerGlow|innerGlow|bevelEmboss|satin|colorOverlay|gradientOverlay|stroke (or display name), layers?}",
has_layers,
add
),
cmd!("layer.style.remove", "Remove Layer Style", [], None, "{layer?, style: id|name|uid|layerStyles}", has_layers, remove),
cmd!("layer.style.toggle", "Toggle Layer Style", [], None, "{layer?, style: id|name|uid|layerStyles, value?}", has_layers, toggle),
cmd!(
"layer.style.globalLight",
"Global Light",
[],
None,
"{comp?, angle? (deg), altitude? (deg, 0..90), time? (s)} — no values returns the current setting",
has_comp,
global_light
),
crate::query!("layer.style.list", "List Layer Styles", "{layer?}", list),
]
}