From d347afb2cb0f92fde147a9cdec8e71d75000500a Mon Sep 17 00:00:00 2001 From: Nigel Ihza Firdausy <106657905+Ni-zav@users.noreply.github.com> Date: Sun, 11 Oct 2026 21:44:02 +0700 Subject: [PATCH 1/2] fix(script): preserve item and layer wrapper identity (#811) --- crates/script/src/prelude.js | 51 +++++++++++++++++++++++++----------- crates/script/src/tests.rs | 33 +++++++++++++++++++++++ 2 files changed, 68 insertions(+), 16 deletions(-) diff --git a/crates/script/src/prelude.js b/crates/script/src/prelude.js index f50a968c..6d34a75b 100644 --- a/crates/script/src/prelude.js +++ b/crates/script/src/prelude.js @@ -6,8 +6,16 @@ var __ecGlobal = this; +// The scripting host keeps wrappers stable for each project item and comp/layer id. +// Clear them when loading a different document, whose IDs may be reused. +var __itemCache = Object.create(null), __layerCache = Object.create(null); +function __clearIdentityCache() { + __itemCache = Object.create(null); + __layerCache = Object.create(null); +} function __call(id, p) { var r = __exec(id, JSON.stringify(p || {})); + if (id === "file.newProject" || id === "file.open" || id === "file.revert") __clearIdentityCache(); return r === undefined || r === "" ? null : JSON.parse(r); } function __get(kind, a) { @@ -965,14 +973,18 @@ MarkerProperty.prototype = { function __layer(compId, id) { if (id === null || id === undefined) return null; - var i = __get("layer", { comp: compId, layer: id }); + var key = compId + ":" + id; + if (Object.prototype.hasOwnProperty.call(__layerCache, key)) return __layerCache[key]; + var i = __get("layer", { comp: compId, layer: id }), layer; switch (i.kind) { - case "text": return new TextLayer(compId, id); - case "shape": return new ShapeLayer(compId, id); - case "camera": return new CameraLayer(compId, id); - case "light": return new LightLayer(compId, id); - default: return new AVLayer(compId, id); + case "text": layer = new TextLayer(compId, id); break; + case "shape": layer = new ShapeLayer(compId, id); break; + case "camera": layer = new CameraLayer(compId, id); break; + case "light": layer = new LightLayer(compId, id); break; + default: layer = new AVLayer(compId, id); } + __layerCache[key] = layer; + return layer; } function Layer() {} @@ -1008,7 +1020,7 @@ Layer.prototype = { set name(v) { __call("layer.rename", this.__p1({ name: String(v) })); }, get index() { return this.__info().index; }, get matchName() { return this.__info().matchName; }, - get containingComp() { return new CompItem(this.__comp); }, + get containingComp() { return __item(this.__comp); }, get propertyDepth() { return 0; }, get propertyType() { return PropertyType.INDEXED_GROUP; }, get parentProperty() { return null; }, @@ -1273,7 +1285,7 @@ LayerCollection.prototype = { var ids = this.__ids(), sel = []; for (var i = 0; i < indices.length; i++) sel.push(ids[indices[i] - 1]); var r = __call("layer.precompose", { comp: this.__comp, layers: sel, name: String(name), mode: moveAllAttributes === false ? "leave" : "move" }); - return new CompItem(r.comp); + return __item(r.comp); }, byName: function (name) { var ids = this.__ids(); @@ -1321,13 +1333,20 @@ ItemCollection.prototype = { function __item(id) { if (id === null || id === undefined) return null; - if (id === 0) return new FolderItem(0); - var i = __get("item", { id: id }); - switch (i.kind) { - case "comp": return new CompItem(id); - case "folder": return new FolderItem(id); - default: return new FootageItem(id); + if (Object.prototype.hasOwnProperty.call(__itemCache, id)) return __itemCache[id]; + var item; + if (id === 0) { + item = new FolderItem(0); + } else { + var i = __get("item", { id: id }); + switch (i.kind) { + case "comp": item = new CompItem(id); break; + case "folder": item = new FolderItem(id); break; + default: item = new FootageItem(id); + } } + __itemCache[id] = item; + return item; } function Item() {} @@ -1584,7 +1603,7 @@ RenderQueueItem.prototype = { for (var i = 0; i < l.length; i++) if (l[i].id === this.__id) return l[i]; throw __err("the render queue item no longer exists"); }, - get comp() { return new CompItem(this.__info().comp); }, + get comp() { return __item(this.__info().comp); }, get status() { var s = this.__info().statusLabel; for (var k in __rqStatus) if (s.indexOf(k) === 0) return __rqStatus[k]; @@ -1682,7 +1701,7 @@ Project.prototype = { __info: function () { return __get("project"); }, get items() { return new ItemCollection(0); }, get numItems() { return this.__info().items.length; }, - get rootFolder() { return new FolderItem(0); }, + get rootFolder() { return __item(0); }, get activeItem() { return __item(this.__info().active); }, get selection() { return this.__info().selection.map(__item); }, get file() { var p = this.__info().path; return p ? new File(p) : null; }, diff --git a/crates/script/src/tests.rs b/crates/script/src/tests.rs index b50a7bfb..a2bfb98d 100644 --- a/crates/script/src/tests.rs +++ b/crates/script/src/tests.rs @@ -730,3 +730,36 @@ fn import_options_sequence_picks_one_still_or_the_run() { assert_eq!(kind_of(&s, "shot_[0001-0003].png"), FootageKind::Sequence); let _ = std::fs::remove_dir_all(&dir); } + +#[test] +fn script_item_and_layer_identity_survives_repeated_access() { + let mut s = session(); + let o = ok( + &mut s, + r#" + var c = app.project.items.addComp("Identity", 120, 80, 1, 3, 25); + var parent = c.layers.addNull(), child = c.layers.addNull(); + child.parent = parent; + [ + parent.containingComp === c, + c.layer(1) === c.layer(1), + child.parent === parent, + app.project.item(1) === app.project.item(1), + app.project.item(1) === c, + app.project.itemByID(c.id) === c + ] + "#, + ); + assert_eq!(o.result, json!([true, true, true, true, true, true])); + // Reusing item and layer IDs in a new project must not alias old wrappers. + let o = ok( + &mut s, + r#" + var old = app.project.item(1); + app.newProject(); + var next = app.project.items.addComp("New", 120, 80, 1, 3, 25); + [old !== next, app.project.item(1) === next] + "#, + ); + assert_eq!(o.result, json!([true, true])); +} From 40e9e7786b11a866993d6886538fc1046eec7fe4 Mon Sep 17 00:00:00 2001 From: Nigel Ihza Firdausy <106657905+Ni-zav@users.noreply.github.com> Date: Sun, 11 Oct 2026 21:45:20 +0700 Subject: [PATCH 2/2] fix(script): route usedIn composition references through identity cache --- crates/script/src/prelude.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/script/src/prelude.js b/crates/script/src/prelude.js index 6d34a75b..f6a47bfa 100644 --- a/crates/script/src/prelude.js +++ b/crates/script/src/prelude.js @@ -1376,7 +1376,7 @@ Item.prototype = { __call("project.select", { items: selection.filter(function (item) { return item !== id; }) }); } }, - get usedIn() { return this.__info().usedIn.map(function (c) { return new CompItem(c); }); }, + get usedIn() { return this.__info().usedIn.map(function (c) { return __item(c); }); }, remove: function () { __call("project.delete", { items: [this.__id] }); }, };