Merge PR #6805: Droplets: carry the actions that Conditional steps call (#6804)

This commit is contained in:
aiongg
2026-10-21 05:04:01 +00:00
3 changed files with 57 additions and 10 deletions
+24 -9
View File
@@ -151,18 +151,33 @@ pub fn playback_plan(s: &Session, p: &Value) -> Result<(Action, usize)> {
Ok((action, from))
}
/// The actions that `steps` call through Play Action steps, followed transitively, in list order,
/// so a droplet can run without the Actions panel that made it (#2794). Index references in
/// `steps` and in the copies become names, which resolve without the rest of the list. A target
/// that doesn't resolve is left alone: it fails at playback, as it would have before.
/// The actions that `steps` call through Play Action steps and the branches of Conditional steps,
/// followed transitively, in list order, so a droplet can run without the Actions panel that made
/// it (#2794, #6804). Index references in `steps` and in the copies become names, which resolve
/// without the rest of the list. A target that doesn't resolve is left alone: it fails at
/// playback, as it would have before.
pub fn called_actions(list: &[Action], steps: &mut [(String, Value)]) -> Vec<Action> {
fn targets(list: &[Action], steps: &mut [(String, Value)], depth: usize, queue: &mut Vec<(usize, usize)>) {
for (_, params) in steps.iter_mut().filter(|(id, _)| id == "actions.play") {
let Ok(i) = resolve(list, params, "actions.play") else { continue };
if let (Some(action), Some(obj)) = (list.get(i), params.as_object_mut()) {
obj.insert("action".into(), json!(action.name));
for (id, params) in steps.iter_mut() {
match id.as_str() {
"actions.play" => {
let Ok(i) = resolve(list, params, "actions.play") else { continue };
if let (Some(action), Some(obj)) = (list.get(i), params.as_object_mut()) {
obj.insert("action".into(), json!(action.name));
}
queue.push((i, depth));
}
// Conditional branches only take names, so there is nothing to rewrite.
"actions.conditional" => {
for key in ["then", "else"] {
let Some(name) = params.get(key).and_then(Value::as_str) else { continue };
if let Some(i) = list.iter().position(|a| a.name == name) {
queue.push((i, depth));
}
}
}
_ => {}
}
queue.push((i, depth));
}
}
let mut queue = Vec::new();
+1 -1
View File
@@ -558,7 +558,7 @@ pub fn specs() -> Vec<CommandSpec> {
"file.automate.createDroplet",
"Create Droplet…",
&["File", "Automate"],
r##"{"path":str (.pcdroplet),"steps":[[id,params]…] (the action),"name":str?,"output":folder?,"format":"same|png|jpg|…"?,"quality":0..12?,"shim":bool=true on Unix (writes <name>.command calling `photocraft-cli droplet`)} → {path, shim} (the actions its Play Action steps call are saved in the droplet)"##,
r##"{"path":str (.pcdroplet),"steps":[[id,params]…] (the action),"name":str?,"output":folder?,"format":"same|png|jpg|…"?,"quality":0..12?,"shim":bool=true on Unix (writes <name>.command calling `photocraft-cli droplet`)} → {path, shim} (the actions its Play Action and Conditional steps call are saved in the droplet)"##,
native,
create_droplet
),
+32
View File
@@ -252,6 +252,38 @@ fn droplets_carry_the_actions_their_play_steps_call() {
assert_eq!(r["files"].as_array().unwrap().len(), inputs.len(), "{r}");
}
#[test]
fn droplets_carry_the_actions_their_conditional_steps_call() {
use crate::actions_cmds::Action;
let dir = tmp("droplet-conditional");
let inputs = images(&dir, 2, 40, 20);
let mut s = Session::new();
let act = |name: &str, steps: Value| -> Action { serde_json::from_value(json!({"name": name, "steps": steps})).unwrap() };
s.actions.list = vec![
act("Unrelated", json!([["image.imageRotation.180", {}]])),
act("Rotate", json!([["image.imageRotation.90cw", {}]])),
// The then branch calls Rotate by index, as the panel records it.
act("Wide", json!([["actions.play", {"action": 1}]])),
act("Tall", json!([["image.imageRotation.180", {}]])),
];
let steps = json!([["actions.conditional", {"if": "landscape", "then": "Wide", "else": "Tall"}]]);
let out = format!("{dir}/out");
let r = s
.execute("file.automate.createDroplet", json!({"path": format!("{dir}/Cond"), "steps": steps, "format": "png", "output": out, "shim": false}))
.unwrap();
let dp = r["path"].as_str().unwrap().to_string();
let v: Value = serde_json::from_slice(&std::fs::read(&dp).unwrap()).unwrap();
let names: Vec<&str> = v["calledActions"].as_array().unwrap().iter().map(|a| a["name"].as_str().unwrap()).collect();
assert_eq!(names, ["Rotate", "Wide", "Tall"], "both branches and what they call");
// A fresh session, as `photocraft-cli droplet` and the droplet shim use.
let mut fresh = Session::new();
let r = fresh.execute("file.automate.runDroplet", json!({"droplet": dp, "input": [dir.clone()]})).unwrap();
assert_eq!(r["files"].as_array().unwrap().len(), inputs.len(), "{r}");
let img = photocraft_codecs::decode(&std::fs::read(format!("{out}/img0.png")).unwrap()).unwrap();
assert_eq!(img.dimensions(), (20, 40), "the landscape input took the then branch");
}
#[test]
fn batch_and_droplets_skip_recorded_view_steps() {
let dir = tmp("view-steps");