Merge pull request #851 from Ni-zav/fix/811-script-object-identity-20261011

fix(script): preserve item and layer wrapper identity

# Conflicts:
#	crates/script/src/tests.rs
This commit is contained in:
Brandon Thomas committed 2026-10-12 03:23:26 -04:00
commit 6caa67971b
2 files changed
+69 -17

No files matched your search

+36 -17
View File
@@ -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) {
@@ -1056,14 +1064,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() {}
@@ -1099,7 +1111,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; },
@@ -1370,7 +1382,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();
@@ -1418,13 +1430,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() {}
@@ -1454,7 +1473,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] }); },
};
@@ -1681,7 +1700,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];
@@ -1783,7 +1802,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; },
+33
View File
@@ -930,3 +930,36 @@ fn import_options_preserve_layered_psd_compositions() {
assert_eq!(bad.result, json!(false));
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]));
}