mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-15 06:42:31 +00:00
Number fields: typed arithmetic (960/2, (3+4)*2) and a leading *, / or + applied to the value (#3195)
Number fields read + - * /, brackets and signs through a small bounded parser (crates/ui-egui/src/num_math.rs): no code is run, length and nesting are capped, every step must stay finite and a division by zero keeps the old value with no undo step. Hot-text values (Timeline, Effect Controls, Properties and the other panels) also take a leading *, / or + applied to the value shown, as the reporter describes for After Effects (not confirmed in public docs). A leading - stays a negative number. Hot text used to read +50 as 50; it now adds 50. On an angle the step goes from the signed remainder (+10 on 0x-30 is 0x-20). With several layers selected each layer takes the step from its own value: + through the offset path, * and / through a new prop.set factor. Typed results in whole-number hot text round instead of truncating. Dialog fields (the shared drag_value wrapper, Essential Graphics, Gradient Editor, colour picker) re-read their text per keystroke, so they take absolute arithmetic only. Fixes #3195
This commit is contained in:
1 parent
bb2d2005ce
commit
0cb5bfefb5
14 files changed
+658
-36
No files matched your search
@@ -371,20 +371,35 @@ 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` or `/2` (#3195): each layer's own value times it, not the edited layer's result.
|
||||
let factor = p.get("factor").and_then(Value::as_f64).filter(|k| k.is_finite() && delta.is_none());
|
||||
// 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| {
|
||||
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 at = snapped_key_time(s, cid, olid, q, "prop.set").ok().flatten();
|
||||
if let (true, Some(k)) = (scalar, factor)
|
||||
&& let Some(own) = components_at(s, cid, olid, ouid, at).and_then(|c| c.first().copied())
|
||||
&& (own * k).is_finite()
|
||||
{
|
||||
let change = dims.first().and_then(|&d| Some(n.get(d)? - c.get(d)?)).unwrap_or(0.0);
|
||||
q["value"] = json!(own * k);
|
||||
}
|
||||
if let (Some(dims), Some(n)) = (&dims, &new)
|
||||
&& let Some(mut c) = components_at(s, cid, olid, ouid, at)
|
||||
{
|
||||
// The typed components' new values on this layer.
|
||||
let typed_value = |c: &[f64], d: usize| match factor {
|
||||
Some(k) => c.get(d).map(|v| v * k).filter(|v| v.is_finite()),
|
||||
None => 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);
|
||||
@@ -1421,7 +1436,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)}",
|
||||
has_layers,
|
||||
set_selected
|
||||
),
|
||||
|
||||
@@ -1531,6 +1531,29 @@ 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");
|
||||
}
|
||||
|
||||
/// #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