mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-14 19:27:05 -04:00
Integrate community PRs: relink's params doc and menu rows, pt-BR in the language list, MCP docs, test fixes
- file.relinkFootage (#461): close the params doc so checked execution (MCP, control channel) rejects unknown keys; translate its menu entry. - general.language lists Português (Brasil) (#217). - docs/agents.md: set_property / add_keyframe take timeBase (#522). - #299's MCP test no longer sends get_property a value. - The Automatically Reload Footage sequence test uses PNG frames. - h264_frames_at_reduced_sizes (#507): the bar depends on the fixture's encoder; 50 dB with ffmpeg 6.1's x264. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
This commit is contained in:
7 files changed
+35
-15
No files matched your search
@@ -42,7 +42,11 @@ fn property_group_error_points_to_layer_inspection() {
|
||||
let l = call_json(&mut s, "execute_command", json!({"command": "layer.newText", "params": {"text": "Hi"}}))["layer"].clone();
|
||||
call_json(&mut s, "execute_command", json!({"command": "comp.renderer", "params": {"renderer": "advanced3d"}}));
|
||||
for tool in ["get_property", "set_property", "add_keyframe"] {
|
||||
let (content, err) = call(&mut s, tool, json!({"layer": l, "path": "geometryOptions", "value": 0, "time": 0}));
|
||||
let mut args = json!({"layer": l, "path": "geometryOptions", "time": 0});
|
||||
if tool != "get_property" {
|
||||
args["value"] = json!(0);
|
||||
}
|
||||
let (content, err) = call(&mut s, tool, args);
|
||||
assert!(err, "{tool}: {content:?}");
|
||||
let text = content[0]["text"].as_str().unwrap();
|
||||
assert!(text.contains("is a property group"), "{tool}: {text}");
|
||||
@@ -161,11 +165,12 @@ fn headless_workflow() {
|
||||
json!({"layer": l, "path": "transform/scale", "keys": [{"time": 1.5, "value": [50, 50]}, {"time": 2.5, "value": [100, 100]}], "timeBase": "comp", "interpolation": "hold"}),
|
||||
);
|
||||
let times: Vec<f64> = p["keys"].as_array().unwrap().iter().map(|k| k["time"].as_f64().unwrap()).collect();
|
||||
assert!((times[0] - 0.5).abs() < 1e-6 && (times[1] - 1.5).abs() < 1e-6, "layer-time keys: {times:?}");
|
||||
// Comp times snap to the comp's frames (29.97 fps) before becoming layer time.
|
||||
assert!((times[0] - 0.5).abs() < 0.02 && (times[1] - 1.5).abs() < 0.02, "layer-time keys: {times:?}");
|
||||
assert_eq!(p["keys"][0]["out"], "Hold", "the comp-time keys were selected for the interpolation");
|
||||
call_json(&mut s, "set_property", json!({"layer": l, "path": "transform/scale", "value": [75, 75], "time": 3.0, "timeBase": "comp"}));
|
||||
let p = call_json(&mut s, "get_property", json!({"layer": l, "path": "transform/scale"}));
|
||||
assert!(p["keys"].as_array().unwrap().iter().any(|k| (k["time"].as_f64().unwrap() - 2.0).abs() < 1e-6), "{p}");
|
||||
assert!(p["keys"].as_array().unwrap().iter().any(|k| (k["time"].as_f64().unwrap() - 2.0).abs() < 0.02), "{p}");
|
||||
|
||||
// Expression via set_property.
|
||||
let p = call_json(&mut s, "set_property", json!({"layer": l, "path": "transform/rotation", "expression": "time * 90"}));
|
||||
|
||||
@@ -180,7 +180,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Relink Missing Footage...",
|
||||
["File", "Dependencies"],
|
||||
None,
|
||||
"{folder: string (recursive desktop search, no symlinks; max 100000 entries), dryRun?: bool (report only), wait?: bool (return results inline; otherwise jobs.list/jobs.wait). Relinks only unique exact filenames with compatible metadata; preserves interpretation and sequence ranges. Reports high/low confidence and ambiguous, incompatible, unsupported or notFound items.",
|
||||
"{folder: string (recursive desktop search, no symlinks; max 100000 entries), dryRun?: bool (report only), wait?: bool (return results inline; otherwise jobs.list/jobs.wait)} — relinks only unique exact filenames with compatible metadata; preserves interpretation and sequence ranges. Reports high/low confidence and ambiguous, incompatible, unsupported or notFound items.",
|
||||
enabled,
|
||||
run
|
||||
)]
|
||||
|
||||
@@ -31,6 +31,7 @@ pub const LANGUAGES: &[(&str, &str)] = &[
|
||||
("繁體中文", "zh-hant"),
|
||||
("Українська", "uk"),
|
||||
("Español", "es"),
|
||||
("Português (Brasil)", "pt-br"),
|
||||
];
|
||||
|
||||
/// Settings ▸ Appearance ▸ UI Scale: (label, `appearance.uiScale` percent).
|
||||
|
||||
@@ -171,9 +171,13 @@ fn background_scan_reloads_changed_footage() {
|
||||
|
||||
#[test]
|
||||
fn image_sequences_reload_by_themselves_only_with_all_footage() {
|
||||
let (a, b) = (tmp("seq_0001.psd"), tmp("seq_0002.psd"));
|
||||
std::fs::write(&a, flat(RED, false)).unwrap();
|
||||
std::fs::write(&b, flat(RED, false)).unwrap();
|
||||
let png = |path: &str, v: u8| {
|
||||
std::thread::sleep(std::time::Duration::from_millis(30));
|
||||
image::RgbaImage::from_pixel(8, 8, image::Rgba([v, 0, 0, 255])).save(path).unwrap();
|
||||
};
|
||||
let (a, b) = (tmp("seq_0001.png"), tmp("seq_0002.png"));
|
||||
png(&a, 255);
|
||||
png(&b, 255);
|
||||
let mut s = effectcraft_host::session();
|
||||
let r = s.execute_checked("file.import", json!({"paths": [&a], "sequence": true})).unwrap();
|
||||
let item = ItemId(r["items"][0].as_u64().unwrap());
|
||||
@@ -181,11 +185,12 @@ fn image_sequences_reload_by_themselves_only_with_all_footage() {
|
||||
// Non-Sequence Footage (the default, as in After Effects): sequences are never stamped.
|
||||
assert_eq!(s.prefs.import.auto_reload_footage, "nonSequence");
|
||||
assert_eq!(s.execute_checked("footage.reloadChanged", json!({})).unwrap(), json!({"files": 0, "reloaded": 0}));
|
||||
save(&a, &flat(BLUE, true));
|
||||
png(&a, 0);
|
||||
assert_eq!(s.execute_checked("footage.reloadChanged", json!({})).unwrap(), json!({"files": 0, "reloaded": 0}));
|
||||
// All Footage: every frame is watched (first seen now, so only remembered).
|
||||
s.prefs.import.auto_reload_footage = "all".into();
|
||||
assert_eq!(s.execute_checked("footage.reloadChanged", json!({})).unwrap(), json!({"files": 0, "reloaded": 0}));
|
||||
save(&b, &flat(GREEN, true));
|
||||
std::fs::write(&b, b"not yet a png").unwrap();
|
||||
png(&b, 128);
|
||||
assert_eq!(s.execute_checked("footage.reloadChanged", json!({})).unwrap()["files"], json!(1));
|
||||
}
|
||||
@@ -251,7 +251,10 @@ fn h264_frames_at_reduced_sizes() {
|
||||
assert_eq!((sized.width, sized.height), (w, h));
|
||||
let db = psnr(&sized, &effectcraft_raster::resample(&full, w, h));
|
||||
eprintln!("h264 frame {n} at {w}×{h}: {db:.2} dB against the resampled full frame");
|
||||
assert!(db > 55.0, "frame {n} at {w}×{h}: {db:.2} dB");
|
||||
// Converting at half size averages before the YUV→RGB clip, so saturated pixels differ
|
||||
// slightly; how much depends on the encoder that made the fixture (50 dB with x264 from
|
||||
// ffmpeg 6.1, above 55 with newer ones), all well past visible.
|
||||
assert!(db > 45.0, "frame {n} at {w}×{h}: {db:.2} dB");
|
||||
assert!(Arc::ptr_eq(&sized, &pool.frame_at_size(ItemId(1), &f, t, w, h).expect("frame")), "cached");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -297,6 +297,7 @@ const SIMPLIFIED_CHINESE: &[(&str, &str, &str)] = &[
|
||||
("file.findMissing", "Find Missing Effects", "查找缺失的效果"),
|
||||
("file.findMissing", "Find Missing Fonts", "查找缺失的字体"),
|
||||
("file.findMissing", "Find Missing Footage", "查找缺失的素材"),
|
||||
("file.relinkFootage", "Relink Missing Footage...", "重新链接缺失的素材..."),
|
||||
("file.watchFolder", "Watch Folder...", "监视文件夹..."),
|
||||
("", "Scripts", "脚本"),
|
||||
("file.installScript", "Install Script File...", "安装脚本文件..."),
|
||||
@@ -920,6 +921,7 @@ const TRADITIONAL_CHINESE: &[(&str, &str, &str)] = &[
|
||||
("file.findMissing", "Find Missing Effects", "尋找遺失的效果"),
|
||||
("file.findMissing", "Find Missing Fonts", "尋找遺失的字體"),
|
||||
("file.findMissing", "Find Missing Footage", "尋找遺失的素材"),
|
||||
("file.relinkFootage", "Relink Missing Footage...", "重新連結遺失的素材..."),
|
||||
("file.watchFolder", "Watch Folder...", "監視檔案夾..."),
|
||||
("", "Scripts", "指令碼"),
|
||||
("file.installScript", "Install Script File...", "安裝指令碼檔案..."),
|
||||
@@ -1540,6 +1542,7 @@ const JAPANESE: &[(&str, &str, &str)] = &[
|
||||
("file.findMissing", "Find Missing Effects", "見つからないエフェクトを検索"),
|
||||
("file.findMissing", "Find Missing Fonts", "見つからないフォントを検索"),
|
||||
("file.findMissing", "Find Missing Footage", "見つからないフッテージを検索"),
|
||||
("file.relinkFootage", "Relink Missing Footage...", "見つからないフッテージを再リンク..."),
|
||||
("file.watchFolder", "Watch Folder...", "監視フォルダー..."),
|
||||
("", "Scripts", "スクリプト"),
|
||||
("file.installScript", "Install Script File...", "スクリプトファイルをインストール..."),
|
||||
@@ -2163,6 +2166,7 @@ const UKRAINIAN: &[(&str, &str, &str)] = &[
|
||||
("file.findMissing", "Find Missing Effects", "Знайти відсутні ефекти"),
|
||||
("file.findMissing", "Find Missing Fonts", "Знайти відсутні шрифти"),
|
||||
("file.findMissing", "Find Missing Footage", "Знайти відсутні матеріали"),
|
||||
("file.relinkFootage", "Relink Missing Footage...", "Перепідключити відсутні матеріали..."),
|
||||
("file.watchFolder", "Watch Folder...", "Спостерігати за текою..."),
|
||||
("", "Scripts", "Скрипти"),
|
||||
("file.installScript", "Install Script File...", "Встановити файл скрипту..."),
|
||||
@@ -2787,6 +2791,7 @@ const SPANISH: &[(&str, &str, &str)] = &[
|
||||
("file.findMissing", "Find Missing Effects", "Buscar efectos que faltan"),
|
||||
("file.findMissing", "Find Missing Fonts", "Buscar fuentes que faltan"),
|
||||
("file.findMissing", "Find Missing Footage", "Buscar material de archivo que falta"),
|
||||
("file.relinkFootage", "Relink Missing Footage...", "Volver a vincular material de archivo que falta..."),
|
||||
("file.watchFolder", "Watch Folder...", "Observar carpeta..."),
|
||||
("", "Scripts", "Scripts"),
|
||||
("file.installScript", "Install Script File...", "Instalar archivo de script..."),
|
||||
@@ -3409,6 +3414,7 @@ const PORTUGUESE: &[(&str, &str, &str)] = &[
|
||||
("file.findMissing", "Find Missing Effects", "Localizar efeitos ausentes"),
|
||||
("file.findMissing", "Find Missing Fonts", "Localizar fontes ausentes"),
|
||||
("file.findMissing", "Find Missing Footage", "Localizar mídias ausentes"),
|
||||
("file.relinkFootage", "Relink Missing Footage...", "Revincular mídias ausentes..."),
|
||||
("file.watchFolder", "Watch Folder...", "Pasta monitorada..."),
|
||||
("", "Scripts", "Scripts"),
|
||||
("file.installScript", "Install Script File...", "Instalar arquivo de script..."),
|
||||
|
||||
+5
-5
@@ -104,8 +104,8 @@ See [MCP conventions](mcp.md) for core tools, resources, strict arguments, and r
|
||||
| `get_project` / `get_comp {comp?}` | Project items, comp settings and layers. |
|
||||
| `get_layer {layer, comp?, time?, depth?, flat?}` | A layer's property tree (`depth` limits how many group levels expand). Every node has a `path`. |
|
||||
| `get_property {layer, path, comp?, time?}` | A leaf property's value at a time, keyframes and expression. Group paths (for example `geometryOptions`) contain children: inspect them with `get_layer`, then query a leaf such as `geometryOptions/extrusionDepth`. |
|
||||
| `set_property {layer, path, value?, time?, expression?, comp?}` | Sets a static value. With `time` it sets a keyframe; with `expression` it sets an expression. |
|
||||
| `add_keyframe {layer, path, time+value \| keys:[...], interpolation?, comp?}` | Adds keys, then optionally applies linear/bezier/hold/easyEase. |
|
||||
| `set_property {layer, path, value?, time?, timeBase?, expression?, comp?}` | Sets a static value. With `time` it sets a keyframe (`timeBase: "comp"` for comp seconds); with `expression` it sets an expression. |
|
||||
| `add_keyframe {layer, path, time+value \| keys:[...], timeBase?, interpolation?, comp?}` | Adds keys (layer seconds, or comp seconds with `timeBase: "comp"`), then optionally applies linear/bezier/hold/easyEase. |
|
||||
| `list_effects {filter?}` | Effect ids, names, categories, GPU / 32-bpc support and parameters. |
|
||||
| `list_fonts {query?, rescan?}` | Font families text layers can use, bundled and installed, with their styles, origin and own-language name; `rescan` picks up fonts installed since launch. |
|
||||
| `add_effect {layer, effect, values?, comp?}` | Apply an effect and set its parameters in one call and one undo step (a failing value leaves nothing applied); returns the instance path (`effects/#n`) and its parameter paths. |
|
||||
@@ -130,7 +130,7 @@ stretched.
|
||||
|
||||
Use MCP `execute_command` for engine commands `prop.set`, `prop.addKey`, `keys.set` and
|
||||
`keys.select` with `timeBase: "layer" | "comp"`; omitted means `layer`. The convenience tools
|
||||
`set_property` and `add_keyframe` retain their layer-time interface. The input base applies to
|
||||
`set_property` and `add_keyframe` take the same `timeBase` (layer time when omitted). The input base applies to
|
||||
both `keys.set.time` and `newTime`, and to every `keys.select.keys[].time`.
|
||||
Omitted `prop.set` or `prop.addKey` times use the target comp's CTI. Supplied write times snap
|
||||
to its nearest comp frame before conversion to stored layer time, including stretched or
|
||||
@@ -189,8 +189,8 @@ the Progress panel, `jobs.list`); headless sessions run `footage.check` when the
|
||||
Things that tripped up a long agent-driven session (a multi-scene 3D piece built entirely over MCP).
|
||||
|
||||
- **Layer time vs comp time.** `add_keyframe` and `set_property` with `time` take layer time;
|
||||
`get_property` and `render_frame` take comp time. Engine keyframe commands default to layer
|
||||
time; use `execute_command` with `timeBase: "comp"` to supply comp seconds on offset or
|
||||
`get_property` and `render_frame` take comp time. Those tools and the engine keyframe commands
|
||||
default to layer time; pass `timeBase: "comp"` to supply comp seconds on offset or
|
||||
stretched layers. `run_script`'s `setValueAtTime` and `keyTime` also use comp time. Without an
|
||||
explicit engine time base, subtract the layer's start time before keying an unstretched layer
|
||||
([#257](https://github.com/storytold/effectcraft/issues/257)).
|
||||
|
||||
Reference in new issue
Block a user