mirror of
https://github.com/aphrody-labs/effectcraft.git
synced 2026-10-14 03:29:02 +00:00
Merge pull request #921 from pravdomil/claude/alt-click-layer-toggles-b7a7a3
Timeline: Alt-click an A/V feature switches the other layers
This commit is contained in:
commit
e2db76e507
4 files changed
+111
-3
No files matched your search
@@ -382,6 +382,9 @@ fn set_switch(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let name = str_p(p, "switch").ok_or_else(|| bad("layer.setSwitch", "missing `switch`"))?.to_string();
|
||||
let v = b_p(p, "value");
|
||||
let label = format!("Layer Switch ({name})");
|
||||
if b_p(p, "others") == Some(true) {
|
||||
return set_switch_others(s, cid, &ids, &name, v, &label);
|
||||
}
|
||||
// `merge`: a switch dragged over several layers in the Timeline is one undo step.
|
||||
let r = s.edit(&label, merge_p(p), |proj, _| {
|
||||
let comp = proj.comp_mut(cid).ok_or(EngineError::NoComp)?;
|
||||
@@ -399,6 +402,38 @@ fn set_switch(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Ok(json!(r))
|
||||
}
|
||||
|
||||
/// `layer.setSwitch {others: true}` (Alt-click on an A/V feature in the Timeline): the named
|
||||
/// layers stand alone, shown / heard / soloed / unlocked while every other layer the switch
|
||||
/// applies to is the opposite; when they already do, the other layers come back. `value` sets
|
||||
/// the other layers directly.
|
||||
fn set_switch_others(s: &mut Session, cid: ItemId, ids: &[LayerId], name: &str, value: Option<bool>, label: &str) -> Result<Value> {
|
||||
let r = s.edit(label, None, |proj, _| {
|
||||
let comp = proj.comp_mut(cid).ok_or(EngineError::NoComp)?;
|
||||
// The state that sets a layer apart: unlocked for Lock, on for the rest.
|
||||
let active = !matches!(name, "lock" | "locked");
|
||||
// Track mattes keep their video off by design.
|
||||
let mattes: Vec<LayerId> =
|
||||
if matches!(name, "video" | "eye") { comp.layers.iter().filter_map(|l| l.track_matte.map(|m| m.layer)).collect() } else { Vec::new() };
|
||||
let applies = |l: &Layer| match name {
|
||||
"audio" => l.props.sub("audio").is_some(),
|
||||
"lock" | "locked" => true,
|
||||
_ => l.source.is_av() && !mattes.contains(&l.id),
|
||||
};
|
||||
let mut alone = true;
|
||||
for l in comp.layers.iter().filter(|l| applies(l)) {
|
||||
let Some(on) = switch_value(l, name) else { return Err(bad("layer.setSwitch", format!("unknown switch `{name}`"))) };
|
||||
alone &= on == (ids.contains(&l.id) == active);
|
||||
}
|
||||
let others = value.unwrap_or(alone == active);
|
||||
for l in comp.layers.iter_mut().filter(|l| applies(l)) {
|
||||
let v = if ids.contains(&l.id) { active } else { others };
|
||||
set_switch_value(l, name, v);
|
||||
}
|
||||
Ok(others)
|
||||
})?;
|
||||
Ok(json!(r))
|
||||
}
|
||||
|
||||
fn switch_value(l: &Layer, name: &str) -> Option<bool> {
|
||||
let sw = &l.switches;
|
||||
Some(match name {
|
||||
@@ -1538,7 +1573,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Layer Switch",
|
||||
[],
|
||||
None,
|
||||
"{layers?, switch: video|audio|solo|lock|shy|collapse|quality|fx|frameBlend|motionBlur|adjustment|threeD|guide|preserveTransparency, value?, merge?}",
|
||||
"{layers?, switch: video|audio|solo|lock|shy|collapse|quality|fx|frameBlend|motionBlur|adjustment|threeD|guide|preserveTransparency, value?, merge?, others? (the named layers alone: the other layers get the opposite state, or come back)}",
|
||||
has_layers,
|
||||
set_switch
|
||||
),
|
||||
|
||||
@@ -52,6 +52,41 @@ fn layer_quality_sampling_frame_blending_and_undo() {
|
||||
assert!(s.execute("layer.quality", json!({"quality": "fancy"})).is_err());
|
||||
}
|
||||
|
||||
/// `layer.setSwitch {others: true}` (Alt-click on an A/V feature): the layer stands alone, the
|
||||
/// other layers come back the next time.
|
||||
#[test]
|
||||
fn set_switch_others() {
|
||||
let mut s = comp();
|
||||
let a = solid(&mut s, "#ff0000");
|
||||
let b = solid(&mut s, "#00ff00");
|
||||
let c = solid(&mut s, "#0000ff");
|
||||
let get = |s: &Session, f: fn(&effectcraft_project::Layer) -> bool| [a, b, c].map(|l| f(&layer(s, l)));
|
||||
s.execute("layer.setSwitch", json!({"layers": [b], "switch": "video", "value": false})).unwrap();
|
||||
// B is hidden: it is shown, alone; then the others come back; one undo step each.
|
||||
s.execute("layer.setSwitch", json!({"layers": [b], "switch": "video", "others": true})).unwrap();
|
||||
assert_eq!(get(&s, |l| l.switches.video), [false, true, false]);
|
||||
s.execute("layer.setSwitch", json!({"layers": [b], "switch": "video", "others": true})).unwrap();
|
||||
assert_eq!(get(&s, |l| l.switches.video), [true; 3]);
|
||||
s.execute("edit.undo", json!({})).unwrap();
|
||||
assert_eq!(get(&s, |l| l.switches.video), [false, true, false]);
|
||||
// Solo.
|
||||
s.execute("layer.setSwitch", json!({"layers": [a], "switch": "solo", "value": true})).unwrap();
|
||||
s.execute("layer.setSwitch", json!({"layers": [c], "switch": "solo", "others": true})).unwrap();
|
||||
assert_eq!(get(&s, |l| l.switches.solo), [false, false, true]);
|
||||
// Lock: the layer stays unlocked and the others lock, then unlock.
|
||||
s.execute("layer.setSwitch", json!({"layers": [a], "switch": "lock", "others": true})).unwrap();
|
||||
assert_eq!(get(&s, |l| l.switches.locked), [false, true, true]);
|
||||
s.execute("layer.setSwitch", json!({"layers": [a], "switch": "lock", "others": true})).unwrap();
|
||||
assert_eq!(get(&s, |l| l.switches.locked), [false; 3]);
|
||||
// A track matte keeps its video off.
|
||||
s.execute("layer.setTrackMatte", json!({"layer": b, "matte": a})).unwrap();
|
||||
s.execute("layer.setSwitch", json!({"layers": [a], "switch": "video", "value": false})).unwrap();
|
||||
s.execute("layer.setSwitch", json!({"layers": [c], "switch": "video", "others": true})).unwrap();
|
||||
s.execute("layer.setSwitch", json!({"layers": [c], "switch": "video", "others": true})).unwrap();
|
||||
assert_eq!(get(&s, |l| l.switches.video), [false, true, true]);
|
||||
assert!(s.execute("layer.setSwitch", json!({"layers": [c], "switch": "nope", "others": true})).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn layer_switches_menu() {
|
||||
let mut s = comp();
|
||||
|
||||
@@ -1169,9 +1169,16 @@ fn layer_switch(
|
||||
id: egui::Id,
|
||||
layer: u64,
|
||||
name: &'static str,
|
||||
others: bool,
|
||||
actions: &mut Vec<(String, serde_json::Value)>,
|
||||
) {
|
||||
let resp = widgets::icon_toggle(ui, br, icon, on, t, id, Sense::click_and_drag());
|
||||
// Alt-click on an A/V feature sets the layer apart: the other layers go off (or locked),
|
||||
// and come back on the next Alt-click.
|
||||
if others && resp.is_pointer_button_down_on() && ui.input(|i| i.pointer.primary_pressed() && i.modifiers.alt) {
|
||||
actions.push(("layer.setSwitch".into(), json!({"layers": [layer], "switch": name, "others": true})));
|
||||
return;
|
||||
}
|
||||
if let Some((value, merge)) = switch_gesture(ui, &resp, row, name, on) {
|
||||
actions.push(("layer.setSwitch".into(), json!({"layers": [layer], "switch": name, "value": value, "merge": merge})));
|
||||
} else if resp.clicked() && !resp.clicked_by(egui::PointerButton::Primary) {
|
||||
@@ -1787,7 +1794,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
continue;
|
||||
}
|
||||
lp.rect_filled(br.shrink(0.5), 1.0, t.switch_well);
|
||||
layer_switch(ui, &t, r, br, icon, on, egui::Id::new(("av", layer.id.0, i)), layer.id.0, name, &mut actions);
|
||||
layer_switch(ui, &t, r, br, icon, on, egui::Id::new(("av", layer.id.0, i)), layer.id.0, name, true, &mut actions);
|
||||
app.auto.add(&format!("timeline.layer.{}.{name}", layer.id.0), br, name);
|
||||
}
|
||||
// Label swatch.
|
||||
@@ -1913,7 +1920,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
}
|
||||
let br = Rect::from_center_size(pos2(cw.switches + SW * i as f32 + SW / 2.0, cy), vec2(16.0, 16.0));
|
||||
lp.rect_filled(br.shrink(1.0), 1.0, t.switch_well);
|
||||
layer_switch(ui, &t, r, br, icon, on, egui::Id::new(("sw", layer.id.0, i)), layer.id.0, name, &mut actions);
|
||||
layer_switch(ui, &t, r, br, icon, on, egui::Id::new(("sw", layer.id.0, i)), layer.id.0, name, false, &mut actions);
|
||||
app.auto.add(&format!("timeline.layer.{}.switch.{name}", layer.id.0), br, name);
|
||||
}
|
||||
// Modes.
|
||||
|
||||
@@ -100,6 +100,37 @@ fn dragging_over_layer_switches_sets_them_all() {
|
||||
assert_eq!(states(&h, &ids, video), [false, true, false, true], "other switches untouched");
|
||||
}
|
||||
|
||||
/// Alt-click on an A/V feature sets the layer apart (the other layers hidden / locked), and the
|
||||
/// next Alt-click brings the others back; the Switches column is not affected by Alt.
|
||||
#[test]
|
||||
fn alt_click_on_av_feature_switches_the_other_layers() {
|
||||
let (mut h, ids) = harness();
|
||||
let alt_click = |h: &mut Harness<'_, EffectcraftApp>, id: String| {
|
||||
let p = rect(h, &id).center();
|
||||
let modifiers = Modifiers::ALT;
|
||||
h.event(Event::PointerMoved(p));
|
||||
h.event(Event::ModifiersChanged(modifiers));
|
||||
h.step();
|
||||
h.event(Event::PointerButton { pos: p, button: egui::PointerButton::Primary, pressed: true, modifiers });
|
||||
h.step();
|
||||
h.event(Event::PointerButton { pos: p, button: egui::PointerButton::Primary, pressed: false, modifiers });
|
||||
h.event(Event::ModifiersChanged(Modifiers::NONE));
|
||||
h.run_steps(3);
|
||||
};
|
||||
let steps = h.state().session.history.undo.len();
|
||||
alt_click(&mut h, format!("timeline.layer.{}.video", ids[1]));
|
||||
assert_eq!(states(&h, &ids, video), [false, true, false, false]);
|
||||
assert_eq!(h.state().session.history.undo.len(), steps + 1, "one undo step");
|
||||
alt_click(&mut h, format!("timeline.layer.{}.video", ids[1]));
|
||||
assert_eq!(states(&h, &ids, video), [true; 4]);
|
||||
alt_click(&mut h, format!("timeline.layer.{}.lock", ids[2]));
|
||||
assert_eq!(states(&h, &ids, |l| l.switches.locked), [true, true, false, true]);
|
||||
alt_click(&mut h, format!("timeline.layer.{}.solo", ids[0]));
|
||||
assert_eq!(states(&h, &ids, |l| l.switches.solo), [true, false, false, false]);
|
||||
alt_click(&mut h, format!("timeline.layer.{}.switch.motionBlur", ids[0]));
|
||||
assert_eq!(states(&h, &ids, motion_blur), [true, false, false, false]);
|
||||
}
|
||||
|
||||
/// #284: pressing a stopwatch and dragging over the stopwatches below gives every property the
|
||||
/// state the first one got (animated or not), in one undo step; Alt+click still adds an
|
||||
/// expression and a click toggles one property.
|
||||
|
||||
Reference in new issue
Block a user