Layer Styles: changed Blending Options outlive the last style and only pixel-changing styles block a clip export (#1389)

This commit is contained in:
Marco Arnulfo
2026-10-30 00:44:57 +01:00
parent cc94fccda5
commit 09a93b6d62
6 changed files with 73 additions and 4 deletions
+3 -2
View File
@@ -152,8 +152,9 @@ fn remove(s: &mut Session, p: &Value) -> Result<Value> {
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) {
// The last style gone removes the Layer Styles group, as in AE, unless its Blending
// Options were changed (they can be edited with no style since #1389).
if g.children.iter().all(|c| c.match_id() == st::BLENDING) && g.sub(st::BLENDING).is_none_or(st::blending_is_default) {
l.props.children.retain(|c| c.match_id() != GROUP);
}
}
+21
View File
@@ -144,3 +144,24 @@ fn blending_options_alone_create_the_group_in_one_undo_step() {
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());
}
+1 -1
View File
@@ -138,7 +138,7 @@ fn unsupported(project: &Project, comp: &Comp, l: &Layer, format: TimelineFormat
if l.is_3d() {
return Some("3D layer".into());
}
if l.props.sub("layerStyles").is_some_and(|g| !g.children.is_empty()) {
if l.styles_change_pixels() {
return Some("layer styles".into());
}
if l.props.get("timeRemap").is_some_and(|p| p.is_animated()) {
+30
View File
@@ -429,6 +429,36 @@ fn plan_prerenders_rendered_only_layers() {
assert!(plan_prerender(&p, cid, &ExportOptions { prerender: PrerenderMode::None, ..Default::default() }).is_empty());
}
/// A Layer Styles group that doesn't change the picture (Blending Options at their defaults, or
/// every style switched off) keeps a clip a clip; Fill Opacity or a style that is on pre-renders it.
#[test]
fn only_layer_styles_that_change_pixels_prerender() {
use effectcraft_project::styles::{self as st, GROUP};
let opts = ExportOptions::default();
let reason = |edit: &dyn Fn(&mut Project, &mut Layer)| {
let (mut p, cid) = ae_project();
let c = p.comp(cid).unwrap();
let gl = c.global_light.clone();
let mut l = c.layers.iter().find(|l| l.name == "clip a").unwrap().clone();
st::add_blending_options(&mut l, &mut effectcraft_project::build::Ids(&mut p.next_id), &gl).unwrap();
edit(&mut p, &mut l);
let lid = l.id;
*p.comp_mut(cid).unwrap().layers.iter_mut().find(|x| x.id == lid).unwrap() = l;
prerender_reason(&p, cid, lid, &opts)
};
let fill = |l: &mut Layer, v: f64| l.props.prop_mut(&format!("{GROUP}/blendingOptions/advancedBlending/fillOpacity")).unwrap().value = Value::Scalar(v);
assert_eq!(reason(&|_, _| {}), None, "Blending Options at their defaults");
assert_eq!(reason(&|_, l| fill(l, 40.0)).as_deref(), Some("layer styles"));
let style = |on: bool| {
move |p: &mut Project, l: &mut Layer| {
let gl = effectcraft_project::styles::GlobalLight::default();
st::add_style(l, &mut effectcraft_project::build::Ids(&mut p.next_id), "dropShadow", &gl, on).unwrap();
}
};
assert_eq!(reason(&style(false)), None, "a style switched off");
assert_eq!(reason(&style(true)).as_deref(), Some("layer styles"));
}
#[test]
fn export_then_reimport_keeps_the_timeline() {
let (p, cid) = ae_project();
+16
View File
@@ -312,6 +312,22 @@ impl Layer {
pub fn can_have_styles(&self) -> bool {
self.source.is_av() && !self.switches.adjustment
}
/// Do the layer's styles change its pixels: the group on, and a style on or Advanced
/// Blending away from its defaults (animated counts as changed).
pub fn styles_change_pixels(&self) -> bool {
self.layer_styles()
.filter(|g| g.enabled && self.can_have_styles())
.is_some_and(|g| g.groups().any(|s| s.enabled && s.match_id != BLENDING) || g.sub(BLENDING).is_some_and(|bo| !blending_is_default(bo)))
}
}
/// Is Advanced Blending of this Blending Options group at its defaults (no keys, no
/// expression)? Global Light is the comp's and doesn't count.
pub fn blending_is_default(bo: &PropGroup) -> bool {
let mut next = 0;
let fresh = blending_options(&mut Ids(&mut next), &GlobalLight::default());
let (Some(adv), Some(def)) = (bo.sub("advancedBlending"), fresh.sub("advancedBlending")) else { return true };
def.props().all(|d| adv.get(&d.match_id).is_none_or(|p| same_anim(p, d)))
}
/// Give `layer` its Layer Styles group (Blending Options only, after Transform) when it has none.
+2 -1
View File
@@ -245,7 +245,8 @@ opacity fades the whole stack; Knockout and the R/G/B channel switches apply at
reuses the cached source/masks/effects. Global Light is one setting per comp, mirrored into every
layer's Blending Options and kept in step after each edit. Blending Options can be edited in the Layer Style dialog
before any style is added: the first change creates the Layer Styles group with Blending Options
only (`layer.style.add` with `blendingOptions`), in the same undo step as the change.
only (`layer.style.add` with `blendingOptions`), in the same undo step as the change. Removing
the last style keeps the group when its Advanced Blending is not at the defaults.
**Paint and Puppet** are effects whose instances carry nested groups. Paint (`effects::paint`)
holds Brush / Clone / Eraser strokes (Path, Stroke Options, Transform, hidden Duration span);