mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-15 06:42:31 +00:00
Merge pull request #3236 from vangel76/number-field-math-3195
Number fields: typed arithmetic (960/2, (3+4)*2) and a leading *, / or + applied to the value
This commit is contained in:
14 files changed
+744
-35
No files matched your search
@@ -371,20 +371,41 @@ fn set_selected(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
};
|
||||
let follow: Vec<usize> =
|
||||
p.get("follow").and_then(Value::as_array).into_iter().flatten().filter_map(Value::as_u64).filter_map(|d| usize::try_from(d).ok()).collect();
|
||||
// A typed `*2`, `/2` (`factor`) or `+10` (`addend`) (#3195): each layer's own value times it
|
||||
// or plus it, not the edited layer's result (which its range may have clamped).
|
||||
let factor = p.get("factor").and_then(Value::as_f64).filter(|k| k.is_finite() && delta.is_none());
|
||||
let addend = p.get("addend").and_then(Value::as_f64).filter(|n| n.is_finite() && delta.is_none());
|
||||
let own_step = |v: f64| -> Option<f64> {
|
||||
let r = match (factor, addend) {
|
||||
(Some(k), _) => v * k,
|
||||
(None, Some(n)) => v + n,
|
||||
(None, None) => return None,
|
||||
};
|
||||
r.is_finite().then_some(r)
|
||||
};
|
||||
// Each layer's own value where `set` writes it (a `time` other than the CTI keys there).
|
||||
let typed = |s: &Session, q: &mut Value, olid: LayerId, ouid: Uid| {
|
||||
let at = snapped_key_time(s, cid, olid, q, "prop.set").ok().flatten();
|
||||
if scalar && let Some(v) = components_at(s, cid, olid, ouid, at).and_then(|c| c.first().copied()).and_then(own_step) {
|
||||
q["value"] = json!(v);
|
||||
}
|
||||
if let (Some(dims), Some(n)) = (&dims, &new)
|
||||
&& let Some(mut c) = components_at(s, cid, olid, ouid, snapped_key_time(s, cid, olid, q, "prop.set").ok().flatten())
|
||||
&& let Some(mut c) = components_at(s, cid, olid, ouid, at)
|
||||
{
|
||||
let change = dims.first().and_then(|&d| Some(n.get(d)? - c.get(d)?)).unwrap_or(0.0);
|
||||
// The typed components' new values on this layer.
|
||||
let typed_value = |c: &[f64], d: usize| {
|
||||
if factor.is_some() || addend.is_some() { c.get(d).copied().and_then(own_step) } else { n.get(d).copied() }
|
||||
};
|
||||
let change = dims.first().and_then(|&d| Some(typed_value(&c, d)? - c.get(d)?)).unwrap_or(0.0);
|
||||
let values: Vec<Option<f64>> = dims.iter().map(|&d| typed_value(&c, d)).collect();
|
||||
for &d in &follow {
|
||||
if let Some(slot) = c.get_mut(d) {
|
||||
*slot += change;
|
||||
}
|
||||
}
|
||||
for &d in dims {
|
||||
if let (Some(slot), Some(v)) = (c.get_mut(d), n.get(d)) {
|
||||
*slot = *v;
|
||||
for (&d, v) in dims.iter().zip(values) {
|
||||
if let (Some(slot), Some(v)) = (c.get_mut(d), v) {
|
||||
*slot = v;
|
||||
}
|
||||
}
|
||||
q["value"] = json!(c);
|
||||
@@ -421,6 +442,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);
|
||||
}
|
||||
@@ -429,6 +453,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")?;
|
||||
@@ -1446,6 +1490,7 @@ fn convert_one_expr(s: &mut Session, cid: ItemId, lid: LayerId, uid: Uid) -> Res
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the command specs for the property commands (get, set, keyframes, expressions and the rest).
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
vec![
|
||||
query!("prop.get", "Get Property", "{layer?, path|prop, time? (comp s)}", get),
|
||||
@@ -1454,7 +1499,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Set Property Value",
|
||||
[],
|
||||
None,
|
||||
"{layer?, path|prop, value, time? (layer s by default), timeBase?: layer|comp, merge?, selected? (also the other selected layers), offset? (them by the same change), proportional? (with offset: Proportional Scrubbing, each layer by its share in selection order, the first none, the last all), dims? (with selected, a typed value: the components edited, e.g. [2] for Z; the other layers keep the rest of their own value), follow? (with dims: components that move on each other layer by the change its first typed component takes)}",
|
||||
"{layer?, path|prop, value, time? (layer s by default), timeBase?: layer|comp, merge?, selected? (also the other selected layers), offset? (them by the same change), proportional? (with offset: Proportional Scrubbing, each layer by its share in selection order, the first none, the last all), dims? (with selected, a typed value: the components edited, e.g. [2] for Z; the other layers keep the rest of their own value), follow? (with dims: components that move on each other layer by the change its first typed component takes), factor? (with selected, a typed `*` or `/`: the other layers take their own value times it, in the dims components of a vector), addend? (the same for a typed `+`: their own value plus it)}",
|
||||
has_layers,
|
||||
set_selected
|
||||
),
|
||||
|
||||
@@ -1556,6 +1556,64 @@ fn a_typed_component_applies_only_that_component_to_the_selected_layers() {
|
||||
assert_eq!(pos(&s, c), vec![1.0, 2.0, 3.0]);
|
||||
}
|
||||
|
||||
/// #3195: a typed `*2` or `/2` with several layers selected scales each layer's own value (in
|
||||
/// the typed components of a vector), not the edited layer's result.
|
||||
#[test]
|
||||
fn a_typed_factor_scales_each_selected_layers_own_value() {
|
||||
let (mut s, a) = setup();
|
||||
let b = s.execute("layer.newSolid", json!({"color": "#ff0000", "width": 50, "height": 50})).unwrap()["layer"].as_u64().unwrap();
|
||||
s.execute("prop.set", json!({"layer": a, "path": "transform/opacity", "value": 40.0})).unwrap();
|
||||
s.execute("prop.set", json!({"layer": b, "path": "transform/opacity", "value": 30.0})).unwrap();
|
||||
s.execute("prop.set", json!({"layer": a, "path": "transform/position", "value": [10.0, 20.0]})).unwrap();
|
||||
s.execute("prop.set", json!({"layer": b, "path": "transform/position", "value": [100.0, 50.0]})).unwrap();
|
||||
s.execute("layer.select", json!({"layers": [a, b]})).unwrap();
|
||||
let opacity = prop(&s, a, "transform/opacity").uid;
|
||||
let undo = s.history.undo.len();
|
||||
s.execute_checked("prop.set", json!({"layer": a, "prop": opacity, "value": 80.0, "selected": true, "factor": 2.0})).unwrap();
|
||||
assert_eq!(prop(&s, a, "transform/opacity").value.as_f64(), 80.0);
|
||||
assert_eq!(prop(&s, b, "transform/opacity").value.as_f64(), 60.0, "B doubles its own 30");
|
||||
assert_eq!(s.history.undo.len(), undo + 1, "one undo step");
|
||||
let position = prop(&s, a, "transform/position").uid;
|
||||
s.execute_checked("prop.set", json!({"layer": a, "prop": position, "value": [5.0, 20.0], "selected": true, "dims": [0], "factor": 0.5})).unwrap();
|
||||
assert_eq!(prop(&s, a, "transform/position").value.components()[..2], [5.0, 20.0]);
|
||||
assert_eq!(prop(&s, b, "transform/position").value.components()[..2], [50.0, 50.0], "B halves its own X, keeps its Y");
|
||||
// A typed `+10` (`addend`): each layer's own value plus 10, within its own range, even where
|
||||
// the edited layer's range cut its change short (95 + 10 stops at 100).
|
||||
s.execute("prop.set", json!({"layer": a, "path": "transform/opacity", "value": 95.0})).unwrap();
|
||||
s.execute("prop.set", json!({"layer": b, "path": "transform/opacity", "value": 30.0})).unwrap();
|
||||
s.execute_checked("prop.set", json!({"layer": a, "prop": opacity, "value": 100.0, "selected": true, "addend": 10.0})).unwrap();
|
||||
assert_eq!(prop(&s, b, "transform/opacity").value.as_f64(), 40.0, "B adds 10, not the 5 A moved");
|
||||
s.execute("prop.set", json!({"layer": b, "path": "transform/opacity", "value": 95.0})).unwrap();
|
||||
s.execute_checked("prop.set", json!({"layer": a, "prop": opacity, "value": 100.0, "selected": true, "addend": 10.0})).unwrap();
|
||||
assert_eq!(prop(&s, b, "transform/opacity").value.as_f64(), 100.0, "B's own range still applies");
|
||||
s.execute_checked("prop.set", json!({"layer": a, "prop": position, "value": [15.0, 20.0], "selected": true, "dims": [0], "addend": 10.0})).unwrap();
|
||||
assert_eq!(prop(&s, b, "transform/position").value.components()[..2], [60.0, 50.0], "B adds 10 to its own X");
|
||||
}
|
||||
|
||||
/// #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]
|
||||
|
||||
Reference in new issue
Block a user