Number fields: Mask Feather stays non-negative on the other selected layers (#3195)

A typed *-1 (the edited layer clamped at 0 by the panel) or a change
by the same amount could take another selected layer's Mask Feather
below 0. Clamp it at 0 there, as the panels do for the edited layer.
This commit is contained in:
vangel76 committed 2026-11-07 16:27:36 +01:00
1 parent 0cb5bfefb5
commit b2d6690fd2
2 files changed
+47

No files matched your search

+23
View File
@@ -436,6 +436,9 @@ fn set_selected(s: &mut Session, p: &Value) -> Result<Value> {
let mut q = twin_params(p, olid, ouid);
shifted(s, &mut q, olid, ouid, k);
typed(s, &mut q, olid, ouid);
if is_mask_feather(s, cid, olid, ouid) {
non_negative(&mut q["value"]);
}
// (a layer that can't take it, e.g. a clamped or static property, keeps its value)
let _ = set(s, &q);
}
@@ -444,6 +447,26 @@ fn set_selected(s: &mut Session, p: &Value) -> Result<Value> {
if merge_p(p).is_some() { run(s) } else { super::app_more::grouped(s, "Change Property", run) }
}
/// Whether property `uid` of `lid` is a mask's Mask Feather, which is never negative (After
/// Effects; the panels clamp a typed or scrubbed value at 0).
fn is_mask_feather(s: &Session, cid: ItemId, lid: LayerId, uid: Uid) -> bool {
let Some(l) = s.project.comp(cid).and_then(|c| c.layer(lid)) else { return false };
l.props.find(uid).is_some_and(|q| q.match_id == "feather") && l.props.parent_of(uid).is_some_and(|g| matches!(g.kind, GroupKind::Mask { .. }))
}
/// A JSON number, or each number of an array, raised to at least 0.
fn non_negative(v: &mut Value) {
match v {
Value::Number(n) => {
if let Some(x) = n.as_f64().filter(|x| *x < 0.0) {
*v = json!(x.max(0.0));
}
}
Value::Array(a) => a.iter_mut().for_each(non_negative),
_ => {}
}
}
/// `prop.toggleAnimation`; with `selected`, the other selected layers' same property follows.
fn toggle_anim_selected(s: &mut Session, p: &Value) -> Result<Value> {
let twins = selected_twins(s, p, "prop.toggleAnimation")?;
+24
View File
@@ -1554,6 +1554,30 @@ fn a_typed_factor_scales_each_selected_layers_own_value() {
assert_eq!(prop(&s, b, "transform/position").value.components()[..2], [50.0, 50.0], "B halves its own X, keeps its Y");
}
/// #3195: Mask Feather is never negative on the other selected layers either, when a typed `*-1`
/// (the edited layer clamped at 0 by the panel) or a change by the same amount takes theirs
/// below 0.
#[test]
fn a_selected_mask_feather_stays_non_negative() {
let (mut s, a) = setup();
let b = s.execute("layer.newSolid", json!({"color": "#ff0000", "width": 50, "height": 50})).unwrap()["layer"].as_u64().unwrap();
for (l, feather) in [(a, [10.0, 20.0]), (b, [4.0, 30.0])] {
s.execute("mask.new", json!({"layer": l, "vertices": [[0, 0], [20, 0], [20, 20]]})).unwrap();
s.execute("prop.set", json!({"layer": l, "path": "masks/#1/feather", "value": feather})).unwrap();
}
s.execute("layer.select", json!({"layers": [a, b]})).unwrap();
let feather = |s: &Session, l: u64| prop(s, l, "masks/#1/feather").value.components();
let uid = prop(&s, a, "masks/#1/feather").uid;
// `*-1` into the linked pair: the panel clamps the edited layer at [0, 0].
s.execute_checked("prop.set", json!({"layer": a, "prop": uid, "value": [0.0, 0.0], "selected": true, "dims": [0, 1], "factor": -1.0})).unwrap();
assert_eq!(feather(&s, a), [0.0, 0.0]);
assert_eq!(feather(&s, b), [0.0, 0.0], "B is clamped at 0, not [-4, -30]");
// A change by the same amount (a scrub, or a typed `+`): A by [-10, -15] from [10, 20].
s.execute("edit.undo", json!({})).unwrap();
s.execute_checked("prop.set", json!({"layer": a, "prop": uid, "value": [0.0, 5.0], "selected": true, "offset": true})).unwrap();
assert_eq!(feather(&s, b), [0.0, 15.0], "B's X is clamped at 0, its Y moves by -15");
}
/// #2485: linked Audio Levels typed with several layers selected: each takes the typed Left, and
/// its Right moves by its own change, so every layer keeps its own stereo balance.
#[test]