mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-27 11:37:02 -06:00
Shortcuts: layer selection, timing, masks and Casts Shadows
- layer.extendSelectionNext / Previous (Cmd+Shift+Down / Up). - layer.moveInToStart / moveOutToEnd (Alt+Home / End). - layer.moveEarlier / moveLater (Alt+PageUp / PageDown) and layer.moveEarlier10 / moveLater10 (with Shift). Alt+PageUp / PageDown used to slip the source one frame; layer.slipBack / slipForward keep working but have no default key. - layer.stretchInToTime / stretchOutToTime (Cmd+Shift+, / Cmd+Alt+,): layer.timeStretch takes to: in|out. - layer.mask.selectNext / selectPrevious (Alt+` / Alt+Shift+`). - layer.castsShadows (Alt+Shift+C) for the selected 3D layers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
cf912d5049
commit
bf53ae6094
@@ -919,6 +919,14 @@ fn timing(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let d = t - l.out_point;
|
||||
shift(l, d);
|
||||
}
|
||||
"moveInToStart" => {
|
||||
let d = Tick::ZERO - l.in_point;
|
||||
shift(l, d);
|
||||
}
|
||||
"moveOutToEnd" => {
|
||||
let d = comp_dur - l.out_point;
|
||||
shift(l, d);
|
||||
}
|
||||
"trimInToTime" => l.in_point = t.min(l.out_point - fd),
|
||||
"trimOutToTime" => l.out_point = t.max(l.in_point + fd),
|
||||
_ => {
|
||||
@@ -951,6 +959,20 @@ fn timing(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Ok(Value::Null)
|
||||
}
|
||||
|
||||
/// `p` with `key` set.
|
||||
fn with(p: &Value, key: &str, v: Value) -> Value {
|
||||
let mut p = if p.is_object() { p.clone() } else { json!({}) };
|
||||
p[key] = v;
|
||||
p
|
||||
}
|
||||
|
||||
/// Move the layers `frames` frames in time (Alt+Page Up / Page Down, with Shift ten frames).
|
||||
fn move_frames(s: &mut Session, p: &Value, frames: i64) -> Result<Value> {
|
||||
let (cid, _) = layers_p(s, p)?;
|
||||
let delta = s.project.comp(cid).ok_or(EngineError::NoComp)?.frame_rate.tick_of(frames).seconds();
|
||||
timing(s, &with(p, "delta", json!(delta)))
|
||||
}
|
||||
|
||||
/// Slip edit: move a layer's source (footage or precomp) under its fixed in and out points, like
|
||||
/// dragging the source-duration bar in After Effects. `delta` in seconds or `frames`; positive
|
||||
/// moves the source later. Clamped so the source still covers the in–out span. Keyframes and
|
||||
@@ -1580,6 +1602,16 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
cmd!("layer.select", "Select Layers", [], None, "{layers: [id|name|#n], add?, toggle?}", has_comp, select),
|
||||
cmd!("layer.selectNext", "Select Next Layer", [], Some("Cmd+ArrowDown"), "{add?}", has_comp, select_next),
|
||||
cmd!("layer.selectPrevious", "Select Previous Layer", [], Some("Cmd+ArrowUp"), "{add?}", has_comp, select_prev),
|
||||
cmd!("layer.extendSelectionNext", "Extend Selection to Next Layer", [], Some("Cmd+Shift+ArrowDown"), "{}", has_comp, |s, _| select_step(
|
||||
s,
|
||||
&json!({"add": true}),
|
||||
1
|
||||
)),
|
||||
cmd!("layer.extendSelectionPrevious", "Extend Selection to Previous Layer", [], Some("Cmd+Shift+ArrowUp"), "{}", has_comp, |s, _| select_step(
|
||||
s,
|
||||
&json!({"add": true}),
|
||||
-1
|
||||
)),
|
||||
cmd!(
|
||||
"layer.setSwitch",
|
||||
"Layer Switch",
|
||||
@@ -1607,17 +1639,29 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Layer Timing",
|
||||
[],
|
||||
None,
|
||||
"{layers?, op?: moveInToTime|moveOutToTime|trimInToTime|trimOutToTime, delta?, start?, in?, out?, inDelta?, outDelta? (seconds, per layer), merge?}",
|
||||
"{layers?, op?: moveInToTime|moveOutToTime|moveInToStart|moveOutToEnd|trimInToTime|trimOutToTime, delta?, start?, in?, out?, inDelta?, outDelta? (seconds, per layer), merge?}",
|
||||
has_layers,
|
||||
timing
|
||||
),
|
||||
cmd!("layer.moveInToStart", "Move Layer In Point to Beginning of Comp", [], Some("Alt+Home"), "{layers?}", has_layers, |s, p| timing(
|
||||
s,
|
||||
&with(p, "op", json!("moveInToStart"))
|
||||
)),
|
||||
cmd!("layer.moveOutToEnd", "Move Layer Out Point to End of Comp", [], Some("Alt+End"), "{layers?}", has_layers, |s, p| timing(
|
||||
s,
|
||||
&with(p, "op", json!("moveOutToEnd"))
|
||||
)),
|
||||
cmd!("layer.moveEarlier", "Move Layers 1 Frame Earlier", [], Some("Alt+PageUp"), "{layers?}", has_layers, |s, p| move_frames(s, p, -1)),
|
||||
cmd!("layer.moveLater", "Move Layers 1 Frame Later", [], Some("Alt+PageDown"), "{layers?}", has_layers, |s, p| move_frames(s, p, 1)),
|
||||
cmd!("layer.moveEarlier10", "Move Layers 10 Frames Earlier", [], Some("Alt+Shift+PageUp"), "{layers?}", has_layers, |s, p| move_frames(s, p, -10)),
|
||||
cmd!("layer.moveLater10", "Move Layers 10 Frames Later", [], Some("Alt+Shift+PageDown"), "{layers?}", has_layers, |s, p| move_frames(s, p, 10)),
|
||||
cmd!("layer.slip", "Slip Edit", [], None, "{layers?, delta? (seconds) | frames?, merge?}", has_layers, slip),
|
||||
cmd!("layer.slipBack", "Slip Edit 1 Frame Earlier", [], Some("Alt+PageUp"), "{layers?}", has_layers, |s, p| {
|
||||
cmd!("layer.slipBack", "Slip Edit 1 Frame Earlier", [], None, "{layers?}", has_layers, |s, p| {
|
||||
let mut p = p.clone();
|
||||
p["frames"] = json!(-1);
|
||||
slip(s, &p)
|
||||
}),
|
||||
cmd!("layer.slipForward", "Slip Edit 1 Frame Later", [], Some("Alt+PageDown"), "{layers?}", has_layers, |s, p| {
|
||||
cmd!("layer.slipForward", "Slip Edit 1 Frame Later", [], None, "{layers?}", has_layers, |s, p| {
|
||||
let mut p = p.clone();
|
||||
p["frames"] = json!(1);
|
||||
slip(s, &p)
|
||||
|
||||
@@ -24,6 +24,11 @@ fn time_stretch(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let pct = f_p(p, "percent");
|
||||
let dur = f_p(p, "duration");
|
||||
let hold = str_p(p, "hold").unwrap_or("in").to_string();
|
||||
// `to`: stretch so the In (or Out) point lands on the current time, the other point held.
|
||||
let to = str_p(p, "to").map(str::to_string);
|
||||
if to.as_deref().is_some_and(|t| t != "in" && t != "out") {
|
||||
return Err(bad("layer.timeStretch", "to: in|out"));
|
||||
}
|
||||
let cti = s.time();
|
||||
let comp_rate = s.project.comp(cid).ok_or(EngineError::NoComp)?.frame_rate;
|
||||
let fd = comp_rate.frame_duration();
|
||||
@@ -39,6 +44,16 @@ fn time_stretch(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
continue;
|
||||
}
|
||||
let span = (l.out_point - l.in_point).seconds();
|
||||
let (dur, hold) = match to.as_deref() {
|
||||
Some("in") => (Some((l.out_point - cti).seconds()), "out"),
|
||||
Some(_) => (Some((cti - l.in_point).seconds()), "in"),
|
||||
None => (dur, hold.as_str()),
|
||||
};
|
||||
// The current time is on the wrong side of the held point: nothing to stretch to.
|
||||
if to.is_some() && dur.is_some_and(|d| d < fd.seconds()) {
|
||||
continue;
|
||||
}
|
||||
let pct = if to.is_some() { None } else { pct };
|
||||
let new = match (pct, dur) {
|
||||
(Some(v), _) => v,
|
||||
(None, Some(d)) if span > 0.0 => old * d / span,
|
||||
@@ -48,7 +63,7 @@ fn time_stretch(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
return Err(bad("layer.timeStretch", "the stretch factor must be at least 1 %"));
|
||||
}
|
||||
// The hold point keeps showing the same layer time.
|
||||
let h = match hold.as_str() {
|
||||
let h = match hold {
|
||||
"current" => cti,
|
||||
"out" => l.out_point,
|
||||
_ => l.in_point,
|
||||
@@ -220,6 +235,16 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
has_layers,
|
||||
time_stretch
|
||||
),
|
||||
cmd!("layer.stretchInToTime", "Stretch Layer In Point to Current Time", [], Some("Cmd+Shift+,"), "{layers?}", has_layers, |s, p| {
|
||||
let mut p = if p.is_object() { p.clone() } else { json!({}) };
|
||||
p["to"] = json!("in");
|
||||
time_stretch(s, &p)
|
||||
}),
|
||||
cmd!("layer.stretchOutToTime", "Stretch Layer Out Point to Current Time", [], Some("Cmd+Alt+,"), "{layers?}", has_layers, |s, p| {
|
||||
let mut p = if p.is_object() { p.clone() } else { json!({}) };
|
||||
p["to"] = json!("out");
|
||||
time_stretch(s, &p)
|
||||
}),
|
||||
cmd!("layer.timeReverse", "Time-Reverse Layer", ["Layer", "Time"], Some("Cmd+Alt+R"), "{layers?}", has_layers, |s, p| {
|
||||
let mut p = p.clone();
|
||||
p["op"] = json!("reverse");
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use effectcraft_keyframe::Value as KV;
|
||||
use effectcraft_project::{ItemId, LayerSource, Project};
|
||||
use effectcraft_time::Tick;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::{CommandSpec, always, comp_id, has_comp, layers_p};
|
||||
use super::{CommandSpec, always, bad, comp_id, has_comp, has_layers, layers_p};
|
||||
use crate::{EngineError, Result, Session, cmd};
|
||||
|
||||
/// Ctrl+Alt+B: the work area spans the selected layers (their earliest In to their latest Out
|
||||
@@ -38,6 +39,52 @@ fn deselect_keys(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
Ok(Value::Null)
|
||||
}
|
||||
|
||||
/// Alt+` / Alt+Shift+`: select the next (previous) mask of the layer, wrapping around; the first
|
||||
/// (last) one when none is selected.
|
||||
fn step_mask(s: &mut Session, p: &Value, dir: i64) -> Result<Value> {
|
||||
let cmd = if dir > 0 { "layer.mask.selectNext" } else { "layer.mask.selectPrevious" };
|
||||
let (cid, ids) = layers_p(s, p)?;
|
||||
let comp = s.project.comp(cid).ok_or(EngineError::NoComp)?;
|
||||
let (lid, masks) = ids
|
||||
.iter()
|
||||
.filter_map(|id| comp.layer(*id))
|
||||
.find_map(|l| l.masks().filter(|m| m.groups().next().is_some()).map(|m| (l.id, m)))
|
||||
.ok_or_else(|| bad(cmd, "the selected layers have no masks"))?;
|
||||
let groups: Vec<_> = masks.groups().collect();
|
||||
let n = groups.len() as i64;
|
||||
// The selected mask: its group, or a property inside it.
|
||||
let selected = |g: &effectcraft_project::PropGroup| {
|
||||
s.state.selected_props.iter().any(|(l, uid)| *l == lid && (g.find_group(*uid).is_some() || g.find(*uid).is_some()))
|
||||
};
|
||||
let i = match groups.iter().position(|g| selected(g)) {
|
||||
Some(i) => (i as i64 + dir).rem_euclid(n.max(1)),
|
||||
None if dir > 0 => 0,
|
||||
None => n - 1,
|
||||
};
|
||||
let uid = groups.get(i as usize).map(|g| g.uid).ok_or_else(|| bad(cmd, "the selected layers have no masks"))?;
|
||||
s.state.selected_props = vec![(lid, uid)];
|
||||
s.state.selected_keys.clear();
|
||||
s.state.selected_vertices.clear();
|
||||
Ok(json!({"layer": lid.0, "mask": uid, "index": i + 1}))
|
||||
}
|
||||
|
||||
/// Alt+Shift+C: Casts Shadows on or off for the selected 3D layers (all follow the first one).
|
||||
fn casts_shadows(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const PATH: &str = "materialOptions/castsShadows";
|
||||
let (cid, ids) = layers_p(s, p)?;
|
||||
let comp = s.project.comp(cid).ok_or(EngineError::NoComp)?;
|
||||
let with: Vec<_> = ids.iter().filter_map(|id| comp.layer(*id)).filter(|l| l.switches.three_d && l.props.prop(PATH).is_some()).collect();
|
||||
let first = with.first().ok_or_else(|| bad("layer.castsShadows", "select a 3D layer"))?;
|
||||
let on = match p.get("value").and_then(Value::as_bool) {
|
||||
Some(v) => v,
|
||||
None => first.props.prop(PATH).is_none_or(|pr| pr.value == KV::Enum(0)),
|
||||
};
|
||||
let layers: Vec<u64> = with.iter().map(|l| l.id.0).collect();
|
||||
let set = super::find("material.set").ok_or_else(|| bad("layer.castsShadows", "Material Options are not available"))?;
|
||||
(set.run)(s, &json!({"comp": cid.0, "layers": layers, "castsShadows": if on { "on" } else { "off" }}))?;
|
||||
Ok(json!({"castsShadows": on, "layers": layers}))
|
||||
}
|
||||
|
||||
/// The comps connected to `from` by nesting, in either direction (its network of nested
|
||||
/// compositions), `from` included.
|
||||
fn hierarchy(proj: &Project, from: ItemId) -> BTreeSet<ItemId> {
|
||||
@@ -93,5 +140,8 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
),
|
||||
cmd!("comp.activateRecent", "Activate Most Recent Composition in Hierarchy", [], Some("Shift+Escape"), "{comp?}", has_comp, activate_recent),
|
||||
cmd!("keys.deselectAll", "Deselect All Keyframes and Properties", [], ["Shift+F2", "Cmd+Alt+Shift+A"], "{}", always, deselect_keys),
|
||||
cmd!("layer.mask.selectNext", "Select Next Mask", [], Some("Alt+`"), "{layer?}", has_layers, |s, p| step_mask(s, p, 1)),
|
||||
cmd!("layer.mask.selectPrevious", "Select Previous Mask", [], Some("Alt+Shift+`"), "{layer?}", has_layers, |s, p| step_mask(s, p, -1)),
|
||||
cmd!("layer.castsShadows", "Casts Shadows", [], Some("Alt+Shift+C"), "{layers?, value?}", has_layers, casts_shadows),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ fn slip_moves_the_source_under_fixed_in_and_out() {
|
||||
let r = s.execute("layer.slip", json!({"layers": [id], "delta": 5.0})).unwrap();
|
||||
assert_eq!(get(&s).start_time.seconds(), 1.0);
|
||||
assert_eq!(r["slipped"][0]["delta"], json!(0.5));
|
||||
// Alt+PageUp / Alt+PageDown nudge by a frame.
|
||||
// Slip by a frame.
|
||||
s.execute("layer.slipBack", json!({"layers": [id]})).unwrap();
|
||||
assert!((get(&s).start_time.seconds() - (1.0 - 1.0 / 30.0)).abs() < 1e-9);
|
||||
s.execute("layer.slipForward", json!({"layers": [id]})).unwrap();
|
||||
@@ -75,8 +75,9 @@ fn slip_moves_the_source_under_fixed_in_and_out() {
|
||||
// Undo walks back through the slips.
|
||||
assert!(s.undo() && s.undo() && s.undo() && s.undo());
|
||||
assert_eq!(get(&s).start_time, Tick::ZERO);
|
||||
assert_eq!(crate::find_command("layer.slipBack").and_then(|c| c.shortcut), Some("Alt+PageUp"));
|
||||
assert_eq!(crate::find_command("layer.slipForward").and_then(|c| c.shortcut), Some("Alt+PageDown"));
|
||||
// Alt+PageUp / Alt+PageDown move the layer; the one-frame slips have no default key.
|
||||
assert_eq!(crate::find_command("layer.slipBack").and_then(|c| c.shortcut), None);
|
||||
assert_eq!(crate::find_command("layer.slipForward").and_then(|c| c.shortcut), None);
|
||||
// Layers without a source duration can't slip.
|
||||
s.execute("layer.newSolid", json!({"color": "#ff0000"})).unwrap();
|
||||
assert!(s.execute("layer.slip", json!({"delta": 0.5})).is_err());
|
||||
|
||||
@@ -23,6 +23,12 @@ fn secs(s: &Session) -> f64 {
|
||||
s.time().seconds()
|
||||
}
|
||||
|
||||
fn span(s: &Session, id: u64) -> (f64, f64) {
|
||||
let c = s.active_comp().unwrap();
|
||||
let l = c.layers.iter().find(|l| l.id.0 == id).unwrap();
|
||||
(l.in_point.seconds(), l.out_point.seconds())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn time_goes_to_the_work_area_and_to_in_and_out_points() {
|
||||
let (mut s, ids) = session([(1.0, 3.0), (2.0, 6.0), (0.0, 10.0)]);
|
||||
@@ -107,3 +113,106 @@ fn keyframes_and_properties_deselect_but_layers_stay() {
|
||||
assert!(s.state.selected_keys.is_empty() && s.state.selected_props.is_empty());
|
||||
assert_eq!(s.state.selected_layers.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selection_extends_through_the_stack() {
|
||||
let (mut s, ids) = session([(0.0, 10.0); 3]);
|
||||
s.execute("layer.select", json!({"layers": [ids[0]]})).unwrap();
|
||||
s.execute("layer.extendSelectionNext", json!({})).unwrap();
|
||||
s.execute("layer.extendSelectionNext", json!({})).unwrap();
|
||||
let mut sel: Vec<u64> = s.state.selected_layers.iter().map(|l| l.0).collect();
|
||||
sel.sort();
|
||||
let mut all = ids.clone();
|
||||
all.sort();
|
||||
assert_eq!(sel, all);
|
||||
// At the end of the stack the selection stays.
|
||||
s.execute("layer.extendSelectionNext", json!({})).unwrap();
|
||||
assert_eq!(s.state.selected_layers.len(), 3);
|
||||
s.execute("layer.select", json!({"layers": [ids[2]]})).unwrap();
|
||||
s.execute("layer.extendSelectionPrevious", json!({})).unwrap();
|
||||
assert_eq!(s.state.selected_layers.len(), 2);
|
||||
assert!(s.state.selected_layers.iter().any(|l| l.0 == ids[1]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn layers_move_to_the_comp_ends_and_by_ten_frames() {
|
||||
let (mut s, ids) = session([(1.0, 3.0), (2.0, 6.0), (0.0, 10.0)]);
|
||||
s.execute("layer.select", json!({"layers": [ids[0], ids[1]]})).unwrap();
|
||||
s.execute("layer.moveInToStart", json!({})).unwrap();
|
||||
assert_eq!((span(&s, ids[0]), span(&s, ids[1])), ((0.0, 2.0), (0.0, 4.0)));
|
||||
s.execute("layer.moveOutToEnd", json!({})).unwrap();
|
||||
assert_eq!((span(&s, ids[0]), span(&s, ids[1])), ((8.0, 10.0), (6.0, 10.0)));
|
||||
s.execute("layer.moveEarlier10", json!({})).unwrap();
|
||||
let (a, b) = span(&s, ids[0]);
|
||||
assert!((a - (8.0 - 10.0 / 30.0)).abs() < 1e-9 && (b - (10.0 - 10.0 / 30.0)).abs() < 1e-9);
|
||||
s.execute("layer.moveLater10", json!({})).unwrap();
|
||||
assert_eq!(span(&s, ids[0]), (8.0, 10.0));
|
||||
s.execute("layer.moveLater", json!({})).unwrap();
|
||||
s.execute("layer.moveEarlier", json!({})).unwrap();
|
||||
assert_eq!(span(&s, ids[0]), (8.0, 10.0));
|
||||
// The unselected layer never moved.
|
||||
assert_eq!(span(&s, ids[2]), (0.0, 10.0));
|
||||
for (id, keys) in [
|
||||
("layer.moveEarlier", "Alt+PageUp"),
|
||||
("layer.moveLater", "Alt+PageDown"),
|
||||
("layer.moveEarlier10", "Alt+Shift+PageUp"),
|
||||
("layer.moveLater10", "Alt+Shift+PageDown"),
|
||||
] {
|
||||
assert_eq!(crate::find_command(id).and_then(|c| c.shortcut), Some(keys));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn in_and_out_points_stretch_to_the_current_time() {
|
||||
let (mut s, ids) = session([(2.0, 4.0), (0.0, 10.0), (0.0, 10.0)]);
|
||||
let stretch = |s: &Session| s.active_comp().unwrap().layers.iter().find(|l| l.id.0 == ids[0]).unwrap().stretch;
|
||||
s.execute("layer.select", json!({"layers": [ids[0]]})).unwrap();
|
||||
// In point to 1 s: the Out point holds, the layer is 3 s long at 150 %.
|
||||
s.execute("time.set", json!({"time": 1.0})).unwrap();
|
||||
s.execute("layer.stretchInToTime", json!({})).unwrap();
|
||||
assert_eq!(span(&s, ids[0]), (1.0, 4.0));
|
||||
assert!((stretch(&s) - 150.0).abs() < 1e-6);
|
||||
// Out point to 7 s: the In point holds, 6 s at 300 %.
|
||||
s.execute("time.set", json!({"time": 7.0})).unwrap();
|
||||
s.execute("layer.stretchOutToTime", json!({})).unwrap();
|
||||
assert_eq!(span(&s, ids[0]), (1.0, 7.0));
|
||||
assert!((stretch(&s) - 300.0).abs() < 1e-6);
|
||||
// A current time before the In point can't be the Out point: the layer is left alone.
|
||||
s.execute("time.set", json!({"time": 0.5})).unwrap();
|
||||
s.execute("layer.stretchOutToTime", json!({})).unwrap();
|
||||
assert_eq!(span(&s, ids[0]), (1.0, 7.0));
|
||||
assert!(s.execute("layer.timeStretch", json!({"to": "middle"})).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn masks_are_stepped_through_and_wrap() {
|
||||
let (mut s, ids) = session([(0.0, 10.0); 3]);
|
||||
s.execute("layer.select", json!({"layers": [ids[0]]})).unwrap();
|
||||
assert!(s.execute("layer.mask.selectNext", json!({})).is_err());
|
||||
for _ in 0..3 {
|
||||
s.execute("layer.addMask", json!({"shape": "rect"})).unwrap();
|
||||
}
|
||||
s.execute("keys.deselectAll", json!({})).unwrap();
|
||||
let step = |s: &mut Session, cmd: &str| s.execute(cmd, json!({})).unwrap()["index"].as_u64().unwrap();
|
||||
assert_eq!(step(&mut s, "layer.mask.selectNext"), 1);
|
||||
assert_eq!(step(&mut s, "layer.mask.selectNext"), 2);
|
||||
assert_eq!(step(&mut s, "layer.mask.selectNext"), 3);
|
||||
assert_eq!(step(&mut s, "layer.mask.selectNext"), 1);
|
||||
assert_eq!(step(&mut s, "layer.mask.selectPrevious"), 3);
|
||||
assert_eq!(s.state.selected_props.len(), 1);
|
||||
s.execute("keys.deselectAll", json!({})).unwrap();
|
||||
assert_eq!(step(&mut s, "layer.mask.selectPrevious"), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn casts_shadows_toggles_on_3d_layers_only() {
|
||||
let (mut s, ids) = session([(0.0, 10.0); 3]);
|
||||
s.execute("layer.select", json!({"layers": [ids[0], ids[1]]})).unwrap();
|
||||
assert!(s.execute("layer.castsShadows", json!({})).is_err());
|
||||
s.execute("layer.setSwitch", json!({"layers": [ids[0]], "switch": "threeD", "value": true})).unwrap();
|
||||
let before = s.execute("layer.castsShadows", json!({})).unwrap();
|
||||
assert_eq!(before["layers"], json!([ids[0]]));
|
||||
let after = s.execute("layer.castsShadows", json!({})).unwrap();
|
||||
assert_ne!(before["castsShadows"], after["castsShadows"]);
|
||||
assert_eq!(s.execute("layer.castsShadows", json!({"value": true})).unwrap()["castsShadows"], json!(true));
|
||||
}
|
||||
|
||||
@@ -2131,6 +2131,15 @@ mod tests {
|
||||
("comp.activateRecent", "Shift+Escape"),
|
||||
("keys.deselectAll", "Shift+F2"),
|
||||
("keys.deselectAll", "Cmd+Alt+Shift+A"),
|
||||
("layer.extendSelectionNext", "Cmd+Shift+ArrowDown"),
|
||||
("layer.moveInToStart", "Alt+Home"),
|
||||
("layer.moveEarlier", "Alt+PageUp"),
|
||||
("layer.moveLater10", "Alt+Shift+PageDown"),
|
||||
("layer.stretchInToTime", "Cmd+Shift+,"),
|
||||
("layer.stretchOutToTime", "Cmd+Alt+,"),
|
||||
("layer.mask.selectNext", "Alt+`"),
|
||||
("layer.mask.selectPrevious", "Alt+Shift+`"),
|
||||
("layer.castsShadows", "Alt+Shift+C"),
|
||||
] {
|
||||
assert!(bindings(&app.session).into_iter().any(|x| x.2 == id && Some((x.0, x.1)) == parse_shortcut(keys)), "{id} on {keys}");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user