mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-14 19:27:05 -04:00
Scripting: footage proxies, replaceWithSequence and mainSource.conformFrameRate (#813)
FootageItem had no replaceWithSequence, setProxy, setProxyWithSequence or setProxyToNone, useProxy and proxySource were fixed stubs, and mainSource was a new object on every read with no conformFrameRate. They now run the existing commands (file.replaceFootage, file.setProxy, file.useProxy, file.setProxyNone, file.interpretFootage); the item info reports the proxy and the conform/native frame rates, and mainSource is one live FootageSource per FootageItem. replaceSource is not covered: it needs an engine command that changes a layer's source. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
470ea7a3af
commit
004984d853
3 files changed
+84
-5
No files matched your search
@@ -121,6 +121,11 @@ fn item(s: &Session, a: &J) -> R {
|
||||
});
|
||||
let used_in: Vec<u64> = s.project.comps().filter(|(_, c)| c.layers.iter().any(|l| l.source.item() == Some(id))).map(|(cid, _)| cid.0).collect();
|
||||
o["usedIn"] = json!(used_in);
|
||||
// The item's proxy (footage and comps): its file and whether Use Proxy is on (#813).
|
||||
o["proxy"] = match &it.proxy {
|
||||
Some(p) => json!({"file": p.footage.path, "isStill": p.footage.kind == effectcraft_project::FootageKind::Still, "enabled": p.enabled}),
|
||||
None => J::Null,
|
||||
};
|
||||
match &it.kind {
|
||||
ItemKind::Folder => {
|
||||
o["kind"] = json!("folder");
|
||||
@@ -171,6 +176,9 @@ fn item(s: &Session, a: &J) -> R {
|
||||
o["footageMissing"] = json!(fo.missing);
|
||||
o["isStill"] = json!(fo.kind == effectcraft_project::FootageKind::Still);
|
||||
o["loop"] = json!(fo.loop_count);
|
||||
// Interpret Footage ▸ Conform to frame rate: 0 when the file's own rate is used (#813).
|
||||
o["conformFrameRate"] = json!(if fo.native_rate.is_some() { fo.frame_rate.as_f64() } else { 0.0 });
|
||||
o["nativeFrameRate"] = json!(fo.native_rate.unwrap_or(fo.frame_rate).as_f64());
|
||||
}
|
||||
ItemKind::Solid(so) => {
|
||||
o["kind"] = json!("solid");
|
||||
|
||||
@@ -1490,8 +1490,14 @@ AVItem.prototype = Object.create(Item.prototype);
|
||||
def("hasVideo", function () { return this.__info().hasVideo; });
|
||||
def("hasAudio", function () { return this.__info().hasAudio; });
|
||||
def("footageMissing", function () { return !!this.__info().footageMissing; });
|
||||
def("useProxy", function () { return false; });
|
||||
def("proxySource", function () { return null; });
|
||||
def("useProxy", function () { var p = this.__info().proxy; return !!(p && p.enabled); }, function (v) {
|
||||
if (!this.__info().proxy) throw __err("\"" + this.name + "\" has no proxy");
|
||||
__call("file.useProxy", { item: this.__id, on: !!v });
|
||||
});
|
||||
def("proxySource", function () {
|
||||
var p = this.__info().proxy;
|
||||
return p ? { isStill: !!p.isStill, file: p.file ? new File(p.file) : null, hasAlpha: false, toString: function () { return "[object FileSource]"; } } : null;
|
||||
});
|
||||
def("isMediaReplacementCompatible", function () { return false; });
|
||||
def("time", function () { return 0; });
|
||||
})();
|
||||
@@ -1623,14 +1629,42 @@ FootageItem.prototype.constructor = FootageItem;
|
||||
Object.defineProperty(FootageItem.prototype, "file", {
|
||||
get: function () { var f = this.__info().file; return f ? new File(f) : null; },
|
||||
});
|
||||
// The item's FootageSource: one live object per FootageItem, as in After Effects (#813).
|
||||
function FootageSource(item) { this.__item = item; }
|
||||
FootageSource.prototype = {
|
||||
constructor: FootageSource,
|
||||
__info: function () { return this.__item.__info(); },
|
||||
get isStill() { return !!this.__info().isStill; },
|
||||
get color() { return this.__info().color; },
|
||||
get file() { var i = this.__info(); return i.kind !== "solid" && i.file ? new File(i.file) : null; },
|
||||
get missingFootagePath() { var i = this.__info(); return i.footageMissing ? i.file : ""; },
|
||||
get loop() { return this.__info().loop; },
|
||||
get hasAlpha() { return false; },
|
||||
get nativeFrameRate() { return this.__info().nativeFrameRate || 0; },
|
||||
get displayFrameRate() { return this.__info().frameRate; },
|
||||
// Interpret Footage ▸ Conform to frame rate; 0 uses the file's own rate.
|
||||
get conformFrameRate() { return this.__info().conformFrameRate || 0; },
|
||||
set conformFrameRate(v) {
|
||||
v = __num(v, "conformFrameRate");
|
||||
if (v < 0) throw __err("conformFrameRate must be 0 or a frame rate");
|
||||
__call("file.interpretFootage", { items: [this.__item.__id], frameRate: v === 0 ? "file" : v });
|
||||
},
|
||||
toString: function () { return this.__info().kind === "solid" ? "[object SolidSource]" : "[object FileSource]"; },
|
||||
};
|
||||
Object.defineProperty(FootageItem.prototype, "mainSource", {
|
||||
get: function () {
|
||||
var i = this.__info();
|
||||
if (i.kind === "solid") return { isStill: true, color: i.color, hasAlpha: false, toString: function () { return "[object SolidSource]"; } };
|
||||
return { isStill: !!i.isStill, file: i.file ? new File(i.file) : null, missingFootagePath: i.footageMissing ? i.file : "", loop: i.loop, hasAlpha: false, toString: function () { return "[object FileSource]"; } };
|
||||
if (!this.__mainSource) Object.defineProperty(this, "__mainSource", { value: new FootageSource(this) });
|
||||
return this.__mainSource;
|
||||
},
|
||||
});
|
||||
FootageItem.prototype.replace = function (f) { __call("file.replaceFootage", { item: this.__id, path: __str(f) }); };
|
||||
FootageItem.prototype.replaceWithSequence = function (f, forceAlphabetical) {
|
||||
__call("file.replaceFootage", { item: this.__id, path: __str(f), sequence: true, alphabetical: !!forceAlphabetical });
|
||||
};
|
||||
// Proxies (footage and comps). A numbered still brings its sequence, so the two setters agree.
|
||||
AVItem.prototype.setProxy = function (f) { __call("file.setProxy", { item: this.__id, path: __str(f) }); };
|
||||
AVItem.prototype.setProxyWithSequence = function (f) { __call("file.setProxy", { item: this.__id, path: __str(f) }); };
|
||||
AVItem.prototype.setProxyToNone = function () { __call("file.setProxyNone", { item: this.__id }); };
|
||||
FootageItem.prototype.replaceWithSolid = function (color, name, w, h) {
|
||||
__call("file.replaceWithSolid", { item: this.__id, color: [color[0], color[1], color[2]], width: w, height: h });
|
||||
if (name !== undefined) this.name = name;
|
||||
|
||||
@@ -731,6 +731,43 @@ fn import_options_sequence_picks_one_still_or_the_run() {
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// #813: FootageItem proxies, replaceWithSequence and mainSource.conformFrameRate.
|
||||
#[test]
|
||||
fn footage_proxies_sequences_and_conform_rate() {
|
||||
use effectcraft_engine::project::ItemKind;
|
||||
let dir = std::env::temp_dir().join(format!("ec-script-proxy-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
for n in ["shot_0001.png", "shot_0002.png", "take_0001.png", "take_0002.png", "proxy.png"] {
|
||||
std::fs::write(dir.join(n), b"").unwrap();
|
||||
}
|
||||
let p = |n: &str| dir.join(n).to_string_lossy().replace('\\', "/");
|
||||
let mut s = session();
|
||||
s.importer = Some(Arc::new(Stills));
|
||||
let o = ok(
|
||||
&mut s,
|
||||
&format!(
|
||||
"var io = new ImportOptions(new File('{}')); io.sequence = true; var item = app.project.importFile(io);\
|
||||
var same = item.mainSource === item.mainSource;\
|
||||
item.mainSource.conformFrameRate = 12; var c12 = item.mainSource.conformFrameRate, fr = item.frameRate;\
|
||||
item.mainSource.conformFrameRate = 0; var c0 = item.mainSource.conformFrameRate;\
|
||||
var before = [item.useProxy, item.proxySource];\
|
||||
item.setProxy(new File('{}')); var set = [item.useProxy, item.proxySource.file.name];\
|
||||
item.useProxy = false; var off = item.useProxy;\
|
||||
item.setProxyToNone(); var none = item.proxySource;\
|
||||
item.replaceWithSequence(new File('{}'), false);\
|
||||
[same, c12, fr, c0, before[0], before[1], set[0], set[1], off, none]",
|
||||
p("shot_0001.png"),
|
||||
p("proxy.png"),
|
||||
p("take_0001.png")
|
||||
),
|
||||
);
|
||||
assert_eq!(o.result, json!([true, 12, 12, 0, false, null, true, "proxy.png", false, null]));
|
||||
let paths: Vec<String> =
|
||||
s.project.items.values().filter_map(|i| if let ItemKind::Footage(f) = &i.kind { Some(f.path.replace('\\', "/")) } else { None }).collect();
|
||||
assert!(paths.iter().any(|f| f.contains("take_")), "replaced with the take sequence: {paths:?}");
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn binary_file_io_preserves_all_byte_values() {
|
||||
let dir = std::env::temp_dir().join(format!("ec-script-binary-{}", std::process::id()));
|
||||
|
||||
Reference in new issue
Block a user