Merge branch 'worktree-agent-a45c0c688bf960993'

# Conflicts:
#	Cargo.lock
#	apps/effectcraft-cli/Cargo.toml
#	apps/effectcraft-cli/src/main.rs
#	crates/host/src/lib.rs
#	crates/ui-egui/src/panels/mod.rs
This commit is contained in:
Brandon Thomas committed 2026-10-01 22:29:58 -04:00
commit b168d902c7
26 files changed
+3648 -32

No files matched your search

+2
View File
@@ -16,4 +16,6 @@ path = "src/main.rs"
effectcraft-engine = { workspace = true }
effectcraft-host = { workspace = true }
effectcraft-automation = { workspace = true }
effectcraft-render = { workspace = true }
effectcraft-time = { workspace = true }
serde_json = { workspace = true }
+144 -3
View File
@@ -9,6 +9,10 @@
//! effectcraft-cli get <comp> <layer> <path> [--time S] read a property
//! effectcraft-cli set <comp> <layer> <path> <value> [--time S] [--expression E]
//! effectcraft-cli render-frame [--comp C] [--time S|--frame N] [--max-side PX|--scale K] [--out F.png]
//! effectcraft-cli render [--comp C] --out FILE [--format h264|prores|png|jpeg|tiff|exr|gif] [--start S] [--end S]
//! [--work-area] [--fps N] [--resolution full|half|third|quarter|K] [--quality best|draft] [--channels rgb|rgba]
//! [--jpeg-quality N] [--bitrate KBPS] [--prores proxy|lt|standard|hq|4444|4444xq] [--audio auto|on|off]
//! effectcraft-cli render F.ecproj --queue render the project's Render Queue
//! effectcraft-cli mcp [--bridge PORT] MCP server on stdio
//!
//! Project: --project F.ecproj (or a positional *.ecproj) | --demo | --empty (default: demo; mcp: empty)
@@ -24,7 +28,7 @@ use effectcraft_automation::tools::{self, Reply};
use effectcraft_automation::{Backend, McpServer};
use serde_json::{Value, json};
const USAGE: &str = "usage: effectcraft-cli <info|commands|exec|run|props|get|set|render-frame|mcp> [args] [--json]
const USAGE: &str = "usage: effectcraft-cli <info|commands|exec|run|props|get|set|render-frame|render|mcp> [args] [--json]
info project + engine summary
commands [--filter TEXT] [--enabled] list engine commands
exec <command-id> [--params JSON] run one engine command
@@ -33,13 +37,41 @@ const USAGE: &str = "usage: effectcraft-cli <info|commands|exec|run|props|get|se
get <comp> <layer> <path> [--time S] read a property
set <comp> <layer> <path> <value> [--time S] [--expression E]
render-frame [--comp C] [--time S | --frame N] [--max-side PX | --scale K] [--out F.png]
render [--comp C] --out FILE [--format F] [--start S] [--end S] [--work-area] [--fps N]
[--resolution full|half|third|quarter|K] [--quality best|draft] [--channels rgb|rgba]
[--jpeg-quality N] [--bitrate KBPS] [--prores PROFILE] [--audio auto|on|off] | --queue
mcp [--bridge PORT] MCP server (JSON-RPC over stdio)
options: --project F.ecproj | --demo | --empty --save | --save-as F --bridge PORT --json
<comp>: id or name, '-' = active comp; <layer>: id, '#n' or name; <value>: JSON or bare string";
/// Options that take a value.
const VALUED: &[&str] =
&["--params", "--project", "--bridge", "--save-as", "--filter", "--time", "--frame", "--comp", "--max-side", "--scale", "--out", "--expression", "--depth"];
const VALUED: &[&str] = &[
"--params",
"--project",
"--bridge",
"--save-as",
"--filter",
"--time",
"--frame",
"--comp",
"--max-side",
"--scale",
"--out",
"--expression",
"--depth",
// render
"--format",
"--start",
"--end",
"--fps",
"--resolution",
"--quality",
"--channels",
"--jpeg-quality",
"--bitrate",
"--prores",
"--audio",
];
struct Args {
pos: Vec<String>,
@@ -198,6 +230,7 @@ fn emit(v: &Value, json_out: bool) {
fn run(cmd: &str, args: &Args, json_out: bool) -> Result<(), Failure> {
match cmd {
"render" => render(args, json_out)?,
"info" => {
let mut b = backend(args, true)?;
let p = tool(&mut b, "get_project", json!({}))?;
@@ -389,3 +422,111 @@ fn with_saved(v: Value, saved: Option<String>) -> Value {
Some(p) => json!({"result": v, "saved": p}),
}
}
/// `render`: queue `--comp` (or the active comp) with the given settings unless `--queue`, then
/// render the queue with a progress line on stderr. Fails if any item fails.
fn render(args: &Args, json_out: bool) -> Result<(), Failure> {
use effectcraft_engine::project::render_queue::RenderStatus;
if args.opt("--bridge").is_some() {
return usage_err("render runs headless; use `exec renderQueue.add` / `renderQueue.render` with --bridge");
}
let mut s = effectcraft_host::session();
match &args.project {
Some(p) => s.execute("file.open", json!({"path": p})).map_err(|e| Failure::Error(e.to_string()))?,
None => s.execute("file.openDemoProject", json!({})).map_err(|e| Failure::Error(e.to_string()))?,
};
let err = |e: effectcraft_engine::EngineError| Failure::Error(e.to_string());
if !args.flag("--queue") {
let Some(out) = args.opt("--out") else { return usage_err("render: --out FILE is required (or --queue)") };
let mut p = json!({"output": out});
if let Some(c) = args.opt("--comp") {
p["comp"] = json!(c);
}
let ext = std::path::Path::new(out).extension().map(|e| e.to_string_lossy().to_string());
if let Some(f) = args.opt("--format").map(str::to_string).or(ext) {
p["format"] = json!(f);
}
match (args.num("--start")?, args.num("--end")?) {
(None, None) => p["timeSpan"] = json!(if args.flag("--work-area") { "workArea" } else { "comp" }),
(a, b) => {
p["start"] = json!(a.unwrap_or(0.0));
if let Some(b) = b {
p["end"] = json!(b);
}
}
}
if let Some(v) = args.num("--fps")? {
p["frameRate"] = json!(v);
}
if let Some(r) = args.opt("--resolution").or(args.opt("--scale")) {
p["resolution"] = r.parse::<f64>().map(|v| json!(v)).unwrap_or(json!(r));
}
for (flag, key) in [("--quality", "quality"), ("--channels", "channels"), ("--prores", "proresProfile"), ("--audio", "audio")] {
if let Some(v) = args.opt(flag) {
p[key] = json!(v);
}
}
if let Some(v) = args.num("--jpeg-quality")? {
p["quality"] = json!(v);
}
if let Some(v) = args.num("--bitrate")? {
p["bitrate"] = json!(v);
}
// Render exactly this item: unqueue whatever the project's queue already holds.
for k in 0..s.project.render_queue.len() {
let _ = s.execute("renderQueue.setRender", json!({"index": k + 1, "render": false}));
}
let r = s.execute("renderQueue.add", p).map_err(err)?;
eprintln!(
"{} → {} ({}×{}, {} frames, {})",
r["compName"].as_str().unwrap_or("?"),
r["outputPath"].as_str().unwrap_or("?"),
r["width"],
r["height"],
r["frames"],
r["outputModuleSummary"].as_str().unwrap_or("")
);
}
s.execute("renderQueue.render", json!({"wait": false})).map_err(err)?;
let tty = std::io::IsTerminal::is_terminal(&std::io::stderr());
while s.is_rendering() {
std::thread::sleep(std::time::Duration::from_millis(100));
if tty && let Some(p) = s.render_progress() {
let left = p.remaining.map(|r| format!(", ~{r:.1}s left")).unwrap_or_default();
eprint!(
"
[{}/{}] frame {}/{} {:.1}s{left}\x1b[K",
(p.items_done + 1).min(p.items_total),
p.items_total,
p.done,
p.total,
p.item_elapsed
);
}
s.poll_render();
}
s.poll_render();
if tty {
eprintln!();
}
let mut results = vec![];
let mut failed = vec![];
for it in s.project.render_queue.iter().filter(|i| i.render && i.started.is_some()) {
let name = s.project.item(it.comp).map(|i| i.name.clone()).unwrap_or_else(|| "?".into());
match &it.status {
RenderStatus::Done => {
eprintln!("done: {name} → {} in {:.2}s", it.last_output.as_deref().unwrap_or("?"), it.render_time.unwrap_or(0.0));
results.push(json!({"comp": name, "output": it.last_output, "seconds": it.render_time}));
}
RenderStatus::Failed(e) => failed.push(format!("{name}: {e}")),
other => failed.push(format!("{name}: {}", other.label())),
}
}
if !failed.is_empty() {
return Err(Failure::Error(failed.join("; ")));
}
if json_out {
emit(&json!({"rendered": results}), true);
}
Ok(())
}