mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-31 21:48:20 -06:00
Layer Styles: changed Blending Options outlive the last style and only pixel-changing styles block a clip export (#1389)
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user