mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-29 12:37:08 -06:00
Merge pull request #1485
This commit is contained in:
@@ -241,7 +241,9 @@ pub(crate) fn proportional_share(index: usize, count: usize) -> f64 {
|
||||
}
|
||||
|
||||
/// `prop.set`; with `selected`, also on the other selected layers: the same value, or with
|
||||
/// `offset` (a scrub) the same change from each layer's own value. With `offset` and
|
||||
/// `offset` (a scrub) the same change from each layer's own value. A typed value with `dims`
|
||||
/// (the components edited, e.g. `[2]` for Z) sets only those components on the other layers,
|
||||
/// which keep the rest of their own value (#1434). With `offset` and
|
||||
/// `proportional` (Proportional Scrubbing) each layer takes [`proportional_share`] of the change
|
||||
/// by its place in the selection order, the scrubbed layer too. One undo step (with `merge`,
|
||||
/// one that a continuing scrub keeps folding into).
|
||||
@@ -261,6 +263,23 @@ fn set_selected(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
.then(|| s.state.selected_layers.iter().copied().filter(|o| *o == lid || twins.iter().any(|(t, _)| t == o)).collect());
|
||||
let share = |l: LayerId| order.as_ref().map_or(1.0, |o| o.iter().position(|x| *x == l).map_or(1.0, |i| proportional_share(i, o.len())));
|
||||
let scalar = p.get("value").is_some_and(Value::is_number);
|
||||
// Typed into some components only: the others stay each layer's own.
|
||||
let dims: Option<Vec<usize>> = match (&delta, p.get("dims").and_then(Value::as_array), &new) {
|
||||
(None, Some(a), Some(_)) if !scalar => Some(a.iter().filter_map(Value::as_u64).filter_map(|d| usize::try_from(d).ok()).collect()),
|
||||
_ => None,
|
||||
};
|
||||
let typed = |s: &Session, q: &mut Value, olid: LayerId, ouid: Uid| {
|
||||
if let (Some(dims), Some(n)) = (&dims, &new)
|
||||
&& let Some(mut c) = current_components(s, cid, olid, ouid)
|
||||
{
|
||||
for &d in dims {
|
||||
if let (Some(slot), Some(v)) = (c.get_mut(d), n.get(d)) {
|
||||
*slot = *v;
|
||||
}
|
||||
}
|
||||
q["value"] = json!(c);
|
||||
}
|
||||
};
|
||||
// `p` aimed at (layer, prop) with its value moved by `k` × the change.
|
||||
let shifted = |s: &Session, q: &mut Value, olid: LayerId, ouid: Uid, k: f64| {
|
||||
if let Some(d) = &delta
|
||||
@@ -291,6 +310,7 @@ 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);
|
||||
// (a layer that can't take it, e.g. a clamped or static property, keeps its value)
|
||||
let _ = set(s, &q);
|
||||
}
|
||||
@@ -1229,7 +1249,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)}",
|
||||
"{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)}",
|
||||
has_layers,
|
||||
set_selected
|
||||
),
|
||||
|
||||
@@ -1343,3 +1343,45 @@ fn relative_trim_moves_every_selected_layer_by_the_same_delta() {
|
||||
s.execute("layer.timing", json!({"layers": [a], "inDelta": 0.1})).unwrap();
|
||||
assert!(near(io(&s, a).0, -0.9), "{:?}", io(&s, a));
|
||||
}
|
||||
|
||||
/// #1434: with several 3D layers selected, typing a value into one component (Z Position) sets
|
||||
/// that component on each of them; their other components stay their own. Before, every
|
||||
/// selected layer took the edited layer's whole value and they piled up on top of it.
|
||||
#[test]
|
||||
fn a_typed_component_applies_only_that_component_to_the_selected_layers() {
|
||||
let (mut s, a) = setup();
|
||||
let b = s.execute("layer.newSolid", json!({"color": "#ff0000", "width": 50, "height": 50})).unwrap()["layer"].as_u64().unwrap();
|
||||
let c = s.execute("layer.newSolid", json!({"color": "#00ff00", "width": 50, "height": 50})).unwrap()["layer"].as_u64().unwrap();
|
||||
for (l, xy) in [(a, [10.0, 20.0]), (b, [110.0, 120.0]), (c, [210.0, 220.0])] {
|
||||
s.execute("layer.setSwitch", json!({"layers": [l], "switch": "threeD", "value": true})).unwrap();
|
||||
s.execute("prop.set", json!({"layer": l, "path": "transform/position", "value": [xy[0], xy[1], 5.0]})).unwrap();
|
||||
}
|
||||
s.execute("layer.select", json!({"layers": [a, b, c]})).unwrap();
|
||||
let pos = |s: &Session, l: u64| prop(s, l, "transform/position").value.components();
|
||||
let uid = prop(&s, a, "transform/position").uid;
|
||||
// The panels send the edited layer's value with the typed component, and which one it was.
|
||||
let undo = s.history.undo.len();
|
||||
s.execute_checked("prop.set", json!({"layer": a, "prop": uid, "value": [10.0, 20.0, -300.0], "selected": true, "offset": false, "dims": [2]})).unwrap();
|
||||
assert_eq!(pos(&s, a), vec![10.0, 20.0, -300.0]);
|
||||
assert_eq!(pos(&s, b), vec![110.0, 120.0, -300.0], "B keeps its X and Y");
|
||||
assert_eq!(pos(&s, c), vec![210.0, 220.0, -300.0], "C keeps its X and Y");
|
||||
assert_eq!(s.history.undo.len(), undo + 1, "one undo step");
|
||||
s.execute("edit.undo", json!({})).unwrap();
|
||||
assert_eq!(pos(&s, b), vec![110.0, 120.0, 5.0]);
|
||||
|
||||
// Linked Scale: both edited components go to every layer, Z stays each layer's own.
|
||||
s.execute("prop.set", json!({"layer": c, "path": "transform/scale", "value": [50.0, 50.0, 30.0]})).unwrap();
|
||||
let scale_uid = prop(&s, a, "transform/scale").uid;
|
||||
s.execute("prop.set", json!({"layer": a, "prop": scale_uid, "value": [200.0, 200.0, 100.0], "selected": true, "dims": [0, 1]})).unwrap();
|
||||
assert_eq!(prop(&s, c, "transform/scale").value.components(), vec![200.0, 200.0, 30.0]);
|
||||
|
||||
// Position with Separate Dimensions on one of the other layers.
|
||||
s.execute("prop.separateDimensions", json!({"layer": b, "value": true})).unwrap();
|
||||
s.execute("prop.set", json!({"layer": a, "prop": uid, "value": [10.0, 20.0, 40.0], "selected": true, "dims": [2]})).unwrap();
|
||||
let axis = |m: &str| prop(&s, b, &format!("transform/{m}")).value.as_f64();
|
||||
assert_eq!((axis("positionX"), axis("positionY"), axis("positionZ")), (110.0, 120.0, 40.0));
|
||||
|
||||
// Without `dims` (agents setting a whole value) every selected layer takes the value as is.
|
||||
s.execute("prop.set", json!({"layer": a, "prop": uid, "value": [1.0, 2.0, 3.0], "selected": true})).unwrap();
|
||||
assert_eq!(pos(&s, c), vec![1.0, 2.0, 3.0]);
|
||||
}
|
||||
|
||||
@@ -377,6 +377,11 @@ fn prop_row(
|
||||
let mut nc = c.clone();
|
||||
nc[d] = nv;
|
||||
set(actions, json!(nc));
|
||||
// Typed into this component only: other selected layers keep the rest of
|
||||
// their own value (#1434).
|
||||
if let Some((_, a)) = actions.last_mut() {
|
||||
a["dims"] = json!([d]);
|
||||
}
|
||||
}
|
||||
x = vr.max.x + 8.0;
|
||||
}
|
||||
|
||||
@@ -317,6 +317,11 @@ fn transform_row(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painter,
|
||||
}
|
||||
}
|
||||
set(actions, json!(nc));
|
||||
// Typed into these components only: other selected layers keep the rest of
|
||||
// their own value (#1434).
|
||||
if let Some((_, a)) = actions.last_mut() {
|
||||
a["dims"] = json!((from..from + shown).collect::<Vec<_>>());
|
||||
}
|
||||
}
|
||||
x = vr.max.x + 10.0;
|
||||
}
|
||||
|
||||
@@ -4153,6 +4153,11 @@ fn value_editor(
|
||||
}
|
||||
}
|
||||
set(actions, json!(nc));
|
||||
// Typed into these components only: other selected layers keep the rest of
|
||||
// their own value (#1434).
|
||||
if let Some((_, a)) = actions.last_mut() {
|
||||
a["dims"] = json!((from..from + shown).collect::<Vec<_>>());
|
||||
}
|
||||
}
|
||||
x = r.max.x + if d + 1 < n { 2.0 } else { 0.0 };
|
||||
if d + 1 < n {
|
||||
|
||||
@@ -539,3 +539,48 @@ fn arrow_keys_move_the_caret_in_rename_fields() {
|
||||
let layer_name = h.state().session.active_comp().unwrap().layer(effectcraft_engine::project::LayerId(layer)).unwrap().name.clone();
|
||||
assert_eq!(layer_name, "Pla-te");
|
||||
}
|
||||
|
||||
/// #1434: with several 3D layers selected, typing a Z Position sets Z on each of them; their X
|
||||
/// and Y stay their own. Every selected layer used to take the typed layer's whole Position, so
|
||||
/// they stacked on top of it and seemed to disappear.
|
||||
#[test]
|
||||
fn typing_one_component_with_several_layers_selected_keeps_their_other_components() {
|
||||
let (mut h, _) = harness();
|
||||
let (a, b, transform) = {
|
||||
let s = &mut h.state_mut().session;
|
||||
let mut ids = vec![];
|
||||
for (name, xy) in [("A", [40.0, 50.0]), ("B", [200.0, 120.0])] {
|
||||
let l = s.execute("layer.newSolid", json!({"name": name, "color": "#406080", "width": 40, "height": 40})).unwrap()["layer"].as_u64().unwrap();
|
||||
s.execute("layer.setSwitch", json!({"layers": [l], "switch": "threeD", "value": true})).unwrap();
|
||||
s.execute("prop.set", json!({"layer": l, "path": "transform/position", "value": [xy[0], xy[1], 0.0]})).unwrap();
|
||||
ids.push(l);
|
||||
}
|
||||
s.execute("layer.select", json!({"layers": ids})).unwrap();
|
||||
let tr = s.active_comp().unwrap().layer(effectcraft_engine::project::LayerId(ids[0])).unwrap().props.sub("transform").unwrap().uid;
|
||||
(ids[0], ids[1], tr)
|
||||
};
|
||||
h.state_mut().ui.timeline.open_layers.insert(a);
|
||||
h.state_mut().ui.timeline.open_groups.insert(transform);
|
||||
h.run_steps(3);
|
||||
let pos = |h: &Harness<'_, EffectcraftApp>, l: u64| {
|
||||
h.state()
|
||||
.session
|
||||
.active_comp()
|
||||
.unwrap()
|
||||
.layer(effectcraft_engine::project::LayerId(l))
|
||||
.unwrap()
|
||||
.props
|
||||
.prop("transform/position")
|
||||
.unwrap()
|
||||
.value
|
||||
.components()
|
||||
};
|
||||
let position =
|
||||
h.state().session.active_comp().unwrap().layer(effectcraft_engine::project::LayerId(a)).unwrap().props.prop("transform/position").unwrap().uid;
|
||||
let p = center(&h, &format!("timeline.prop.{position}.value.2"));
|
||||
click(&mut h, p, 1);
|
||||
type_text(&mut h, "-300");
|
||||
key(&mut h, Key::Enter);
|
||||
assert_eq!(pos(&h, a), [40.0, 50.0, -300.0]);
|
||||
assert_eq!(pos(&h, b), [200.0, 120.0, -300.0], "B keeps its own X and Y");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user