@engine-room/after-effects-mcp 0.4.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/bin/server.js +1730 -671
- package/package.json +1 -1
- package/panel/CSXS/manifest.xml +2 -2
- package/panel/client/main.js +323 -20
- package/panel/jsx/bundle.jsx +797 -67
- package/panel/package.json +1 -1
package/panel/jsx/bundle.jsx
CHANGED
|
@@ -295,10 +295,68 @@ OPS.set_comp = function (args) {
|
|
|
295
295
|
return __compSummary(c);
|
|
296
296
|
};
|
|
297
297
|
|
|
298
|
+
// A solid layer's source is a FootageItem in the project's Solids folder, and
|
|
299
|
+
// removing the comp removes the layer, not the item (issue #83). The solids
|
|
300
|
+
// this comp used are collected from ITS OWN layers before the removal and
|
|
301
|
+
// nothing else — deleting comp A must never reach a solid that only some other
|
|
302
|
+
// comp uses — and after it, `usedIn` decides: an item some other comp still
|
|
303
|
+
// places is kept and reported with the comps using it. The helpers live in
|
|
304
|
+
// footage.jsx beside purge_unused_footage, the project-wide sweep.
|
|
298
305
|
OPS.delete_comp = function (args) {
|
|
299
306
|
var c = getCompById(args.compId);
|
|
307
|
+
var compId = c.id;
|
|
308
|
+
var compName = c.name;
|
|
309
|
+
var purge = (args.purgeUnusedSolids === true);
|
|
310
|
+
|
|
311
|
+
var solids = __solidItemsOf(c);
|
|
312
|
+
|
|
300
313
|
c.remove();
|
|
301
|
-
|
|
314
|
+
|
|
315
|
+
var out = { ok: true, compId: compId, name: compName };
|
|
316
|
+
var kept = [];
|
|
317
|
+
var toRemove = [];
|
|
318
|
+
var gone = [];
|
|
319
|
+
for (var i = 0; i < solids.length; i++) {
|
|
320
|
+
// Re-fetched by id after the removal rather than held across it — the
|
|
321
|
+
// handle staleness this repo has measured elsewhere is not worth risking
|
|
322
|
+
// for one lookup per solid.
|
|
323
|
+
var item = app.project.itemByID(solids[i].id);
|
|
324
|
+
if (!item) { gone.push(solids[i]); continue; }
|
|
325
|
+
var uses = __usedInList(item);
|
|
326
|
+
if (uses.length > 0) {
|
|
327
|
+
kept.push({ id: item.id, name: item.name, usedIn: uses });
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
toRemove.push({ item: item, id: item.id, name: item.name, kind: "solid" });
|
|
331
|
+
}
|
|
332
|
+
if (gone.length > 0) out.solidsAlreadyGone = gone;
|
|
333
|
+
|
|
334
|
+
if (!purge) {
|
|
335
|
+
out.unusedSolidsLeft = toRemove.length;
|
|
336
|
+
if (toRemove.length > 0) {
|
|
337
|
+
out.note = toRemove.length + " solid item(s) this comp used are now used by nothing and remain in the project's Solids folder. " +
|
|
338
|
+
"Pass purgeUnusedSolids:true to remove them with the comp, or purge_unused_footage to sweep the whole project.";
|
|
339
|
+
}
|
|
340
|
+
return out;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
var r = __removeItems(toRemove);
|
|
344
|
+
out.removedSolids = r.removed;
|
|
345
|
+
out.keptSolids = kept;
|
|
346
|
+
if (r.failed) {
|
|
347
|
+
var msg = "delete_comp removed comp \"" + compName + "\" (id " + compId + "), then failed purging its solids at \"" +
|
|
348
|
+
r.failed.name + "\" (id " + r.failed.id + "): " + r.error + ".";
|
|
349
|
+
if (r.removed.length > 0) msg += " Removed before it: " + __nameList(r.removed, 20) + ".";
|
|
350
|
+
else msg += " No solid had been removed yet.";
|
|
351
|
+
if (r.notAttempted.length > 0) msg += " Not attempted: " + __nameList(r.notAttempted, 20) + ".";
|
|
352
|
+
msg += " The comp is gone, so do not call delete_comp for it again. One Undo in After Effects restores the comp and " +
|
|
353
|
+
"these solids together; purge_unused_footage removes whatever is still left.";
|
|
354
|
+
throw new Error(msg);
|
|
355
|
+
}
|
|
356
|
+
if (kept.length > 0) {
|
|
357
|
+
out.note = kept.length + " solid item(s) were kept because another comp still uses them; see keptSolids.usedIn.";
|
|
358
|
+
}
|
|
359
|
+
return out;
|
|
302
360
|
};
|
|
303
361
|
|
|
304
362
|
// ---------------------------------------------------------------------------
|
|
@@ -377,20 +435,24 @@ function __dupRepoint(comp, opts, depth) {
|
|
|
377
435
|
}
|
|
378
436
|
}
|
|
379
437
|
|
|
438
|
+
// A `folderId` argument resolved to a FolderItem, or a thrown error naming the
|
|
439
|
+
// id and what it actually is. Shared by duplicate_comp and purge_unused_footage.
|
|
440
|
+
function __folderArg(id) {
|
|
441
|
+
var f = app.project.itemByID(id);
|
|
442
|
+
if (!f) throw new Error("No project item with id " + id + " to use as folderId");
|
|
443
|
+
if (!(f instanceof FolderItem)) {
|
|
444
|
+
throw new Error(
|
|
445
|
+
"folderId " + id + ' ("' + f.name + '") is a ' + __itemKind(f) +
|
|
446
|
+
", not a project folder. Pass the id of a folder from get_project_summary, or omit folderId."
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
return f;
|
|
450
|
+
}
|
|
451
|
+
|
|
380
452
|
OPS.duplicate_comp = function (args) {
|
|
381
453
|
var src = getCompById(args.compId);
|
|
382
454
|
var folder = null;
|
|
383
|
-
if (args.folderId !== undefined && args.folderId !== null)
|
|
384
|
-
var f = app.project.itemByID(args.folderId);
|
|
385
|
-
if (!f) throw new Error("No project item with id " + args.folderId + " to use as folderId");
|
|
386
|
-
if (!(f instanceof FolderItem)) {
|
|
387
|
-
throw new Error(
|
|
388
|
-
"folderId " + args.folderId + ' ("' + f.name + '") is a ' + __itemKind(f) +
|
|
389
|
-
", not a project folder. Pass the id of a folder from get_project_summary, or omit folderId."
|
|
390
|
-
);
|
|
391
|
-
}
|
|
392
|
-
folder = f;
|
|
393
|
-
}
|
|
455
|
+
if (args.folderId !== undefined && args.folderId !== null) folder = __folderArg(args.folderId);
|
|
394
456
|
|
|
395
457
|
var opts = { seen: {}, created: [], repointed: 0, nameSuffix: null };
|
|
396
458
|
if (args.nameSuffix) opts.nameSuffix = args.nameSuffix;
|
|
@@ -553,15 +615,33 @@ OPS.create_text_layer = function (args) {
|
|
|
553
615
|
var align = args.anchorAlign === undefined ? "left" : args.anchorAlign;
|
|
554
616
|
var wantJustify = align !== "none" && __JUSTIFICATION[align] !== undefined;
|
|
555
617
|
var wantTracking = args.tracking !== undefined || align !== "none";
|
|
618
|
+
var verified = null;
|
|
556
619
|
if (args.font || args.size || args.color || wantJustify || wantTracking) {
|
|
557
620
|
var srcText = l.property("Source Text");
|
|
558
621
|
var td = srcText.value;
|
|
622
|
+
// The same TextDocument round trip set_text does, so the same read-back
|
|
623
|
+
// (issue #93 / #94, see __verifyJustification in text.jsx): the requested
|
|
624
|
+
// justification if there is one, otherwise whatever addText() gave the
|
|
625
|
+
// layer — 'none' promised to leave it alone, and that is checked too.
|
|
626
|
+
var justificationWanted = wantJustify ? __JUSTIFICATION[align] : td.justification;
|
|
559
627
|
if (args.font) td.font = args.font;
|
|
560
628
|
if (args.size) td.fontSize = args.size;
|
|
561
629
|
if (args.color) { td.applyFill = true; td.fillColor = [args.color[0], args.color[1], args.color[2]]; }
|
|
562
630
|
if (wantTracking) td.tracking = (args.tracking !== undefined ? args.tracking : 0);
|
|
563
|
-
if (wantJustify) td.justification =
|
|
631
|
+
if (wantJustify) td.justification = justificationWanted;
|
|
564
632
|
srcText.setValue(td);
|
|
633
|
+
try {
|
|
634
|
+
verified = __verifyJustification(srcText, justificationWanted);
|
|
635
|
+
} catch (e) {
|
|
636
|
+
// A throw here would leave a layer the caller has no id for, and the
|
|
637
|
+
// natural next move — call again — makes two. Same rule as
|
|
638
|
+
// add_shape_content: take the node back, then say so. The message is
|
|
639
|
+
// amended on the same Error object so nothing about it is lost.
|
|
640
|
+
try { l.remove(); } catch (e2) {}
|
|
641
|
+
e.message = String(e.message) + " The new layer was removed, so nothing was created; " +
|
|
642
|
+
"create it with anchorAlign:\"none\" and set the alignment afterwards with set_text.";
|
|
643
|
+
throw e;
|
|
644
|
+
}
|
|
565
645
|
}
|
|
566
646
|
if (align !== "none") {
|
|
567
647
|
l.property("Transform").property("Anchor Point").setValue([0, 0, 0]);
|
|
@@ -570,7 +650,12 @@ OPS.create_text_layer = function (args) {
|
|
|
570
650
|
var p = args.position;
|
|
571
651
|
l.property("Transform").property("Position").setValue(p.length === 3 ? p : [p[0], p[1]]);
|
|
572
652
|
}
|
|
573
|
-
|
|
653
|
+
var out = __layerSummary(l);
|
|
654
|
+
if (verified) {
|
|
655
|
+
out.justification = __justificationName(verified.justification);
|
|
656
|
+
out.justificationReasserted = verified.reasserted;
|
|
657
|
+
}
|
|
658
|
+
return out;
|
|
574
659
|
};
|
|
575
660
|
|
|
576
661
|
OPS.create_solid_layer = function (args) {
|
|
@@ -1973,11 +2058,54 @@ OPS.set_spatial_tangents = function (args) {
|
|
|
1973
2058
|
|
|
1974
2059
|
// expressions.jsx — get/set/toggle/clear expressions on any property.
|
|
1975
2060
|
|
|
2061
|
+
// After Effects reports a broken expression on `Property.expressionError` and
|
|
2062
|
+
// nowhere a script can otherwise see: assigning `.expression` succeeds whatever
|
|
2063
|
+
// the text says, and the failure is a warning banner in the UI (issue #97).
|
|
2064
|
+
// The field is filled in when the expression is *evaluated*, which a bare
|
|
2065
|
+
// assignment does not necessarily do — so the value is read first, inside a
|
|
2066
|
+
// try, purely to force one evaluation. Whether AE would populate the field
|
|
2067
|
+
// without that nudge cannot be settled offline; forcing it costs one evaluation
|
|
2068
|
+
// and makes the answer independent of the question. `.value` throws on a
|
|
2069
|
+
// property that has none (a group), which is also not an expression error.
|
|
2070
|
+
function __expressionError(prop) {
|
|
2071
|
+
try { var forced = prop.value; } catch (e) {}
|
|
2072
|
+
var err = "";
|
|
2073
|
+
try { err = prop.expressionError; } catch (e2) { err = ""; }
|
|
2074
|
+
return err ? String(err) : "";
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
function __propertyPathLabel(path) {
|
|
2078
|
+
var parts = [];
|
|
2079
|
+
for (var i = 0; i < path.length; i++) parts.push(String(path[i]));
|
|
2080
|
+
return parts.join(" > ");
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
// Throws if the expression on `prop` does not evaluate, naming the property,
|
|
2084
|
+
// AE's own message and both ways forward. The expression stays written — the
|
|
2085
|
+
// message says so — because nothing here rolls back and a caller fixing a
|
|
2086
|
+
// typo wants the property it was aimed at, not a cleared one.
|
|
2087
|
+
function __assertExpressionEvaluates(prop, path, verb) {
|
|
2088
|
+
var err = __expressionError(prop);
|
|
2089
|
+
if (!err) return;
|
|
2090
|
+
var enabledNow;
|
|
2091
|
+
try { enabledNow = !!prop.expressionEnabled; } catch (e) { enabledNow = null; }
|
|
2092
|
+
throw new Error(
|
|
2093
|
+
"Expression on " + __propertyPathLabel(path) + " was " + verb +
|
|
2094
|
+
" but After Effects cannot evaluate it: " + err +
|
|
2095
|
+
(enabledNow === false ? " (After Effects has disabled it.)" : "") +
|
|
2096
|
+
" Fix the expression and call set_expression again, or clear_expression to remove it."
|
|
2097
|
+
);
|
|
2098
|
+
}
|
|
2099
|
+
|
|
1976
2100
|
OPS.get_expression = noUndo(function (args) {
|
|
1977
2101
|
var c = getCompById(args.compId);
|
|
1978
2102
|
var l = getLayerById(c, args.layerId);
|
|
1979
2103
|
var prop = walkProperty(l, args.propertyPath);
|
|
1980
|
-
return {
|
|
2104
|
+
return {
|
|
2105
|
+
expression: prop.expression || "",
|
|
2106
|
+
enabled: !!prop.expressionEnabled,
|
|
2107
|
+
expressionError: __expressionError(prop),
|
|
2108
|
+
};
|
|
1981
2109
|
});
|
|
1982
2110
|
|
|
1983
2111
|
OPS.set_expression = function (args) {
|
|
@@ -1986,6 +2114,7 @@ OPS.set_expression = function (args) {
|
|
|
1986
2114
|
var prop = walkProperty(l, args.propertyPath);
|
|
1987
2115
|
prop.expression = args.expression || "";
|
|
1988
2116
|
prop.expressionEnabled = true;
|
|
2117
|
+
__assertExpressionEvaluates(prop, args.propertyPath, "written");
|
|
1989
2118
|
return { ok: true };
|
|
1990
2119
|
};
|
|
1991
2120
|
|
|
@@ -1994,6 +2123,9 @@ OPS.toggle_expression = function (args) {
|
|
|
1994
2123
|
var l = getLayerById(c, args.layerId);
|
|
1995
2124
|
var prop = walkProperty(l, args.propertyPath);
|
|
1996
2125
|
prop.expressionEnabled = !!args.enabled;
|
|
2126
|
+
// Disabling cannot fail this way; enabling can surface an error that has
|
|
2127
|
+
// been sitting on the property since it was disabled.
|
|
2128
|
+
if (args.enabled) __assertExpressionEvaluates(prop, args.propertyPath, "enabled");
|
|
1997
2129
|
return { ok: true };
|
|
1998
2130
|
};
|
|
1999
2131
|
|
|
@@ -2147,12 +2279,63 @@ var __JUSTIFICATION = {
|
|
|
2147
2279
|
full: ParagraphJustification.FULL_JUSTIFY_LASTLINE_LEFT,
|
|
2148
2280
|
};
|
|
2149
2281
|
|
|
2282
|
+
// The name for an enum value, for results and errors. AE has more FULL_JUSTIFY
|
|
2283
|
+
// variants than the map offers, so a value the map does not know comes back as
|
|
2284
|
+
// its number rather than as the nearest name.
|
|
2285
|
+
function __justificationName(v) {
|
|
2286
|
+
for (var k in __JUSTIFICATION) {
|
|
2287
|
+
if (__JUSTIFICATION.hasOwnProperty(k) && __JUSTIFICATION[k] === v) return k;
|
|
2288
|
+
}
|
|
2289
|
+
return String(v);
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2292
|
+
// Issue #93: on AE 26.3 the TextDocument round trip — read `.value`, change
|
|
2293
|
+
// `.text`, `setValue()` — has come back with justification reset to CENTER
|
|
2294
|
+
// even though the document that went in still said LEFT; issue #94 reports the
|
|
2295
|
+
// setter storing RIGHT when handed CENTER from a script. Nothing raises in
|
|
2296
|
+
// either case and the mechanism is unknown, so the only guard that can be built
|
|
2297
|
+
// blind is the one this repo asks for anyway: read the property back and
|
|
2298
|
+
// compare with what should be there. A second write of the justification alone
|
|
2299
|
+
// has been seen to land (`set_text({justification})` on its own always did), so
|
|
2300
|
+
// one re-assert is tried before giving up. If that also disagrees, the caller
|
|
2301
|
+
// is told what was expected and what the layer actually shows — a text layer
|
|
2302
|
+
// that silently re-centres is the failure this codebase refuses everywhere.
|
|
2303
|
+
//
|
|
2304
|
+
// `expected` is an enum value: the requested one, or the one read before the
|
|
2305
|
+
// mutation. Returns {justification, reasserted}; throws when it cannot deliver.
|
|
2306
|
+
function __verifyJustification(src, expected) {
|
|
2307
|
+
var actual = src.value.justification;
|
|
2308
|
+
if (actual === expected) return { justification: actual, reasserted: false };
|
|
2309
|
+
var again = src.value;
|
|
2310
|
+
again.justification = expected;
|
|
2311
|
+
src.setValue(again);
|
|
2312
|
+
actual = src.value.justification;
|
|
2313
|
+
if (actual === expected) return { justification: actual, reasserted: true };
|
|
2314
|
+
throw new Error(
|
|
2315
|
+
"Source Text justification did not land: expected " + __justificationName(expected) +
|
|
2316
|
+
" (" + String(expected) + "), the layer reads " + __justificationName(actual) +
|
|
2317
|
+
" (" + String(actual) + ") after a second write. The other fields were written. " +
|
|
2318
|
+
"Read the layer back with get_layer_full include:[\"text\"] before relying on its alignment."
|
|
2319
|
+
);
|
|
2320
|
+
}
|
|
2321
|
+
|
|
2150
2322
|
OPS.set_text = function (args) {
|
|
2151
2323
|
var c = getCompById(args.compId);
|
|
2152
2324
|
var l = getLayerById(c, args.layerId);
|
|
2153
2325
|
if (!(l instanceof TextLayer)) throw new Error("Layer is not a TextLayer");
|
|
2154
2326
|
var src = l.property("Source Text");
|
|
2155
2327
|
var td = src.value;
|
|
2328
|
+
// Captured before anything is touched: "undefined fields unchanged" is a
|
|
2329
|
+
// claim about this value, and it is what the read-back is checked against
|
|
2330
|
+
// when no justification was asked for.
|
|
2331
|
+
var justificationBefore = td.justification;
|
|
2332
|
+
var justificationWanted = justificationBefore;
|
|
2333
|
+
if (args.justification !== undefined) {
|
|
2334
|
+
if (__JUSTIFICATION[args.justification] === undefined) {
|
|
2335
|
+
throw new Error("Unknown justification '" + String(args.justification) + "'; expected one of left, center, right, full. Nothing was changed.");
|
|
2336
|
+
}
|
|
2337
|
+
justificationWanted = __JUSTIFICATION[args.justification];
|
|
2338
|
+
}
|
|
2156
2339
|
if (args.text !== undefined) td.text = args.text;
|
|
2157
2340
|
if (args.font !== undefined) td.font = args.font;
|
|
2158
2341
|
if (args.size !== undefined) td.fontSize = args.size;
|
|
@@ -2161,9 +2344,7 @@ OPS.set_text = function (args) {
|
|
|
2161
2344
|
if (args.strokeWidth !== undefined) td.strokeWidth = args.strokeWidth;
|
|
2162
2345
|
if (args.tracking !== undefined) td.tracking = args.tracking;
|
|
2163
2346
|
if (args.leading !== undefined) td.leading = args.leading;
|
|
2164
|
-
if (args.justification !== undefined)
|
|
2165
|
-
if (__JUSTIFICATION[args.justification]) td.justification = __JUSTIFICATION[args.justification];
|
|
2166
|
-
}
|
|
2347
|
+
if (args.justification !== undefined) td.justification = justificationWanted;
|
|
2167
2348
|
if (args.applyFill !== undefined) td.applyFill = args.applyFill;
|
|
2168
2349
|
if (args.applyStroke !== undefined) td.applyStroke = args.applyStroke;
|
|
2169
2350
|
if (args.fauxBold !== undefined) td.fauxBold = args.fauxBold;
|
|
@@ -2172,7 +2353,12 @@ OPS.set_text = function (args) {
|
|
|
2172
2353
|
if (args.smallCaps !== undefined) td.smallCaps = args.smallCaps;
|
|
2173
2354
|
if (args.baselineShift !== undefined) td.baselineShift = args.baselineShift;
|
|
2174
2355
|
src.setValue(td);
|
|
2175
|
-
|
|
2356
|
+
var verified = __verifyJustification(src, justificationWanted);
|
|
2357
|
+
return {
|
|
2358
|
+
ok: true,
|
|
2359
|
+
justification: __justificationName(verified.justification),
|
|
2360
|
+
justificationReasserted: verified.reasserted,
|
|
2361
|
+
};
|
|
2176
2362
|
};
|
|
2177
2363
|
|
|
2178
2364
|
OPS.add_text_animator = function (args) {
|
|
@@ -2921,6 +3107,221 @@ OPS.create_footage_layer = function (args) {
|
|
|
2921
3107
|
};
|
|
2922
3108
|
};
|
|
2923
3109
|
|
|
3110
|
+
// ---------------------------------------------------------------------------
|
|
3111
|
+
// Unused footage, and the solids delete_comp leaves behind (issue #83)
|
|
3112
|
+
// ---------------------------------------------------------------------------
|
|
3113
|
+
// A solid layer's source is a FootageItem in the project's Solids folder, not
|
|
3114
|
+
// something the comp owns. Deleting the comp (or the layer) deletes the layer
|
|
3115
|
+
// and leaves the item: nothing in AE's DOM ties the two lifetimes together and
|
|
3116
|
+
// nothing warns, so a session that builds and discards comps grows the project
|
|
3117
|
+
// bin without bound — a verification pass had to sweep 1,863 of them out by
|
|
3118
|
+
// name. Once orphaned they are indistinguishable from solids still in use
|
|
3119
|
+
// unless something reads `usedIn`, which is what everything below does.
|
|
3120
|
+
//
|
|
3121
|
+
// Two ops share these helpers. `delete_comp` (comps.jsx) can purge the solids
|
|
3122
|
+
// the deleted comp referenced — only those, and only the ones nothing else
|
|
3123
|
+
// still uses — and `purge_unused_footage` is the project-wide sweep: AE's own
|
|
3124
|
+
// Remove Unused Footage, with a dryRun and a report of what went.
|
|
3125
|
+
|
|
3126
|
+
/** The item behind a solid or adjustment layer: a FootageItem whose source is a SolidSource. */
|
|
3127
|
+
function __isSolidItem(it) {
|
|
3128
|
+
if (!(it instanceof FootageItem)) return false;
|
|
3129
|
+
var src = null;
|
|
3130
|
+
try { src = it.mainSource; } catch (e) { return false; }
|
|
3131
|
+
if (!src) return false;
|
|
3132
|
+
return (src instanceof SolidSource);
|
|
3133
|
+
}
|
|
3134
|
+
|
|
3135
|
+
/** "solid" | "placeholder" | "footage". Only ever called on a FootageItem — a comp or folder is never footage. */
|
|
3136
|
+
function __footageKind(it) {
|
|
3137
|
+
if (__isSolidItem(it)) return "solid";
|
|
3138
|
+
try {
|
|
3139
|
+
if (typeof PlaceholderSource !== "undefined" && it.mainSource instanceof PlaceholderSource) return "placeholder";
|
|
3140
|
+
} catch (e) {}
|
|
3141
|
+
return "footage";
|
|
3142
|
+
}
|
|
3143
|
+
|
|
3144
|
+
/**
|
|
3145
|
+
* The comps an item is placed in, as {id, name}. Read from After Effects on
|
|
3146
|
+
* every call rather than inferred from anything this bundle remembers — the
|
|
3147
|
+
* whole safety of a purge is that the answer is AE's, at the moment of asking.
|
|
3148
|
+
*/
|
|
3149
|
+
function __usedInList(it) {
|
|
3150
|
+
var out = [];
|
|
3151
|
+
var comps = null;
|
|
3152
|
+
try { comps = it.usedIn; } catch (e) { comps = null; }
|
|
3153
|
+
if (!comps) return out;
|
|
3154
|
+
for (var i = 0; i < comps.length; i++) {
|
|
3155
|
+
var c = comps[i];
|
|
3156
|
+
if (!c) continue;
|
|
3157
|
+
out.push({ id: c.id, name: c.name });
|
|
3158
|
+
}
|
|
3159
|
+
return out;
|
|
3160
|
+
}
|
|
3161
|
+
|
|
3162
|
+
/** True when `it` sits in `folder` or in any folder nested under it. */
|
|
3163
|
+
function __isInFolder(it, folder) {
|
|
3164
|
+
var cur = null;
|
|
3165
|
+
try { cur = it.parentFolder; } catch (e) { return false; }
|
|
3166
|
+
for (var depth = 0; cur && depth < 64; depth++) {
|
|
3167
|
+
if (cur.id === folder.id) return true;
|
|
3168
|
+
var next = null;
|
|
3169
|
+
try { next = cur.parentFolder; } catch (e2) { next = null; }
|
|
3170
|
+
cur = next;
|
|
3171
|
+
}
|
|
3172
|
+
return false;
|
|
3173
|
+
}
|
|
3174
|
+
|
|
3175
|
+
/** The distinct solid items this comp's own layers point at, as {id, name}. Nothing from any other comp. */
|
|
3176
|
+
function __solidItemsOf(comp) {
|
|
3177
|
+
var out = [];
|
|
3178
|
+
var seen = {};
|
|
3179
|
+
for (var i = 1; i <= comp.numLayers; i++) {
|
|
3180
|
+
var l = comp.layer(i);
|
|
3181
|
+
if (!(l instanceof AVLayer)) continue;
|
|
3182
|
+
var src = null;
|
|
3183
|
+
try { src = l.source; } catch (e) { continue; }
|
|
3184
|
+
if (!src || !__isSolidItem(src)) continue;
|
|
3185
|
+
var key = "I" + src.id;
|
|
3186
|
+
if (seen.hasOwnProperty(key)) continue;
|
|
3187
|
+
seen[key] = true;
|
|
3188
|
+
out.push({ id: src.id, name: src.name });
|
|
3189
|
+
}
|
|
3190
|
+
return out;
|
|
3191
|
+
}
|
|
3192
|
+
|
|
3193
|
+
function __itemReport(entry) {
|
|
3194
|
+
var r = { id: entry.id, name: entry.name, kind: entry.kind };
|
|
3195
|
+
if (entry.footageMissing) r.footageMissing = true;
|
|
3196
|
+
return r;
|
|
3197
|
+
}
|
|
3198
|
+
|
|
3199
|
+
function __nameList(reports, cap) {
|
|
3200
|
+
var names = [];
|
|
3201
|
+
for (var i = 0; i < reports.length && i < cap; i++) names.push("\"" + reports[i].name + "\" (id " + reports[i].id + ")");
|
|
3202
|
+
var s = names.join(", ");
|
|
3203
|
+
if (reports.length > cap) s += " and " + (reports.length - cap) + " more";
|
|
3204
|
+
return s;
|
|
3205
|
+
}
|
|
3206
|
+
|
|
3207
|
+
/**
|
|
3208
|
+
* Remove a list of project items. Entries are {item, id, name, kind, index?}.
|
|
3209
|
+
*
|
|
3210
|
+
* Highest project index first when indices are known, so no removal shifts an
|
|
3211
|
+
* item still to be visited — the entries were collected in one pass over
|
|
3212
|
+
* app.project.item(i) and removed in a second, never removed mid-walk. It stops
|
|
3213
|
+
* at the first failure: nothing rolls back, so the caller is handed what went
|
|
3214
|
+
* before the failure and what was never attempted, and reports that, rather
|
|
3215
|
+
* than a sweep that claims to have finished.
|
|
3216
|
+
*/
|
|
3217
|
+
function __removeItems(entries) {
|
|
3218
|
+
var ordered = entries.slice(0);
|
|
3219
|
+
ordered.sort(function (a, b) { return (b.index || 0) - (a.index || 0); });
|
|
3220
|
+
var removed = [];
|
|
3221
|
+
for (var i = 0; i < ordered.length; i++) {
|
|
3222
|
+
var entry = ordered[i];
|
|
3223
|
+
try {
|
|
3224
|
+
entry.item.remove();
|
|
3225
|
+
} catch (e) {
|
|
3226
|
+
var pending = [];
|
|
3227
|
+
for (var j = i + 1; j < ordered.length; j++) pending.push(__itemReport(ordered[j]));
|
|
3228
|
+
return {
|
|
3229
|
+
removed: removed,
|
|
3230
|
+
failed: __itemReport(entry),
|
|
3231
|
+
error: (e && e.message) ? String(e.message) : String(e),
|
|
3232
|
+
notAttempted: pending
|
|
3233
|
+
};
|
|
3234
|
+
}
|
|
3235
|
+
removed.push(__itemReport(entry));
|
|
3236
|
+
}
|
|
3237
|
+
return { removed: removed, failed: null, error: null, notAttempted: [] };
|
|
3238
|
+
}
|
|
3239
|
+
|
|
3240
|
+
OPS.purge_unused_footage = noUndoWhen(
|
|
3241
|
+
// A dry run is not an undo step. A sweep that changed nothing appearing in
|
|
3242
|
+
// the user's undo history would make "this changed nothing" false in the one
|
|
3243
|
+
// place they can see it — the same stance place_audio_cues takes.
|
|
3244
|
+
function (args) { return !!(args && args.dryRun === true); },
|
|
3245
|
+
function (args) {
|
|
3246
|
+
args = args || {};
|
|
3247
|
+
var solidsOnly = (args.solidsOnly !== false);
|
|
3248
|
+
var dryRun = (args.dryRun === true);
|
|
3249
|
+
var folder = null;
|
|
3250
|
+
if (args.folderId !== undefined && args.folderId !== null) folder = __folderArg(args.folderId);
|
|
3251
|
+
var what = "footage item";
|
|
3252
|
+
if (solidsOnly) what = "solid";
|
|
3253
|
+
|
|
3254
|
+
// One pass to decide, a second to remove. Removing inside the walk would
|
|
3255
|
+
// shift every index above the removed item under the loop's feet.
|
|
3256
|
+
var candidates = [];
|
|
3257
|
+
var scanned = 0;
|
|
3258
|
+
var inUse = 0;
|
|
3259
|
+
var proj = app.project;
|
|
3260
|
+
for (var i = 1; i <= proj.numItems; i++) {
|
|
3261
|
+
var it = proj.item(i);
|
|
3262
|
+
// A comp is an AVItem with a usedIn of its own, and a nested comp nothing
|
|
3263
|
+
// uses any more is exactly what a naive sweep would delete. Only footage.
|
|
3264
|
+
if (!(it instanceof FootageItem)) continue;
|
|
3265
|
+
if (solidsOnly && !__isSolidItem(it)) continue;
|
|
3266
|
+
if (folder && !__isInFolder(it, folder)) continue;
|
|
3267
|
+
scanned += 1;
|
|
3268
|
+
if (__usedInList(it).length > 0) { inUse += 1; continue; }
|
|
3269
|
+
var missing = false;
|
|
3270
|
+
try { missing = !!it.footageMissing; } catch (eMissing) {}
|
|
3271
|
+
candidates.push({ item: it, id: it.id, name: it.name, kind: __footageKind(it), index: i, footageMissing: missing });
|
|
3272
|
+
}
|
|
3273
|
+
// Sorted here, once, so a dry run lists items in exactly the order a real
|
|
3274
|
+
// run removes them — the plan has to be the truth about order too.
|
|
3275
|
+
candidates.sort(function (a, b) { return b.index - a.index; });
|
|
3276
|
+
|
|
3277
|
+
var out = { solidsOnly: solidsOnly, scanned: scanned, inUse: inUse };
|
|
3278
|
+
var where = "in the project";
|
|
3279
|
+
if (folder) {
|
|
3280
|
+
out.folderId = folder.id;
|
|
3281
|
+
out.folderName = folder.name;
|
|
3282
|
+
where = "in folder \"" + folder.name + "\"";
|
|
3283
|
+
}
|
|
3284
|
+
var nothingWhy = "every " + what + " " + where + " is still used in a comp";
|
|
3285
|
+
if (scanned === 0) nothingWhy = "no " + what + "s " + where;
|
|
3286
|
+
|
|
3287
|
+
if (dryRun) {
|
|
3288
|
+
var would = [];
|
|
3289
|
+
for (var w = 0; w < candidates.length; w++) would.push(__itemReport(candidates[w]));
|
|
3290
|
+
out.dryRun = true;
|
|
3291
|
+
out.wouldRemove = would;
|
|
3292
|
+
out.wouldRemoveCount = would.length;
|
|
3293
|
+
if (would.length === 0) {
|
|
3294
|
+
out.note = "Nothing to remove: " + nothingWhy + ". Nothing was changed and this call is not an undo step.";
|
|
3295
|
+
} else {
|
|
3296
|
+
out.note = "Nothing was removed and this call is not an undo step. Call again without dryRun to remove these " +
|
|
3297
|
+
would.length + " " + what + (would.length === 1 ? "" : "s") + " " + where + ".";
|
|
3298
|
+
}
|
|
3299
|
+
return out;
|
|
3300
|
+
}
|
|
3301
|
+
|
|
3302
|
+
var r = __removeItems(candidates);
|
|
3303
|
+
out.removed = r.removed;
|
|
3304
|
+
out.removedCount = r.removed.length;
|
|
3305
|
+
if (r.failed) {
|
|
3306
|
+
var msg = "purge_unused_footage stopped at \"" + r.failed.name + "\" (id " + r.failed.id + ", " + r.failed.kind + "): " + r.error + ".";
|
|
3307
|
+
if (r.removed.length > 0) {
|
|
3308
|
+
msg += " " + r.removed.length + " item(s) were removed before it: " + __nameList(r.removed, 20) + ".";
|
|
3309
|
+
} else {
|
|
3310
|
+
msg += " Nothing had been removed yet.";
|
|
3311
|
+
}
|
|
3312
|
+
msg += " " + r.notAttempted.length + " unused item(s) were not attempted." +
|
|
3313
|
+
" Nothing rolls back; one Undo in After Effects restores what this call removed. Run again, or with dryRun:true, to see what is left.";
|
|
3314
|
+
throw new Error(msg);
|
|
3315
|
+
}
|
|
3316
|
+
if (r.removed.length === 0) {
|
|
3317
|
+
out.note = "Nothing to remove: " + nothingWhy + ".";
|
|
3318
|
+
} else {
|
|
3319
|
+
out.note = "Removed " + r.removed.length + " unused " + what + (r.removed.length === 1 ? "" : "s") + " " + where + ", as one undo step.";
|
|
3320
|
+
}
|
|
3321
|
+
return out;
|
|
3322
|
+
}
|
|
3323
|
+
);
|
|
3324
|
+
|
|
2924
3325
|
|
|
2925
3326
|
// ===== audio.jsx =====
|
|
2926
3327
|
|
|
@@ -2946,10 +3347,21 @@ OPS.create_footage_layer = function (args) {
|
|
|
2946
3347
|
// .wav are the normal shape of a cue list ("whoosh" nine times), and each
|
|
2947
3348
|
// one would otherwise add another project item. Anything already in the
|
|
2948
3349
|
// project from that path is reused rather than imported a second time.
|
|
3350
|
+
//
|
|
3351
|
+
// Three per-cue options (issue #85) — `loop`, `fadeIn`/`fadeOut`, `stretch` —
|
|
3352
|
+
// are the patterns every scoring pass used to re-implement in a run_jsx
|
|
3353
|
+
// follow-up. Each one moves something After Effects then quietly resets, so
|
|
3354
|
+
// the order inside __placeAudioCue is load-bearing; see the comment there.
|
|
2949
3355
|
|
|
2950
3356
|
var __MAX_AUDIO_CUES = 200;
|
|
2951
3357
|
var __CUE_TIME_EPS = 1e-6;
|
|
2952
3358
|
|
|
3359
|
+
// The level a fade starts from and ends at when the caller gives none. -48 dB
|
|
3360
|
+
// is the bottom of the range After Effects' own Audio Levels slider offers, and
|
|
3361
|
+
// 1/256 of the recorded amplitude — near-silence for a fade to begin from while
|
|
3362
|
+
// staying inside the range a user can drag the property through by hand.
|
|
3363
|
+
var __DEFAULT_FADE_FLOOR_DB = -48;
|
|
3364
|
+
|
|
2953
3365
|
// AE's layer label colours. Users can rename them in preferences, but the tool
|
|
2954
3366
|
// takes an index and the indices do not move, so the names are accepted as a
|
|
2955
3367
|
// convenience and translated here. 0 is "None".
|
|
@@ -2998,6 +3410,10 @@ function __stripExtension(name) {
|
|
|
2998
3410
|
return s;
|
|
2999
3411
|
}
|
|
3000
3412
|
|
|
3413
|
+
function __isFiniteNumber(v) {
|
|
3414
|
+
return typeof v === "number" && isFinite(v);
|
|
3415
|
+
}
|
|
3416
|
+
|
|
3001
3417
|
/**
|
|
3002
3418
|
* The Audio Levels property of a layer.
|
|
3003
3419
|
*
|
|
@@ -3019,6 +3435,21 @@ function __audioLevelsProperty(layer) {
|
|
|
3019
3435
|
return p;
|
|
3020
3436
|
}
|
|
3021
3437
|
|
|
3438
|
+
/**
|
|
3439
|
+
* The Time Remap property of a layer that has remapping enabled. Same shape of
|
|
3440
|
+
* trap as Audio Levels: `layer.property("ADBE Time Remap")` is null on an
|
|
3441
|
+
* audio layer even after `timeRemapEnabled = true`, and the shortcut
|
|
3442
|
+
* `layer.timeRemap` is the handle that answers. The match-name lookup is kept
|
|
3443
|
+
* only as a fallback; a null from both is reported, never worked around.
|
|
3444
|
+
*/
|
|
3445
|
+
function __timeRemapProperty(layer) {
|
|
3446
|
+
var p = null;
|
|
3447
|
+
try { p = layer.timeRemap; } catch (e) {}
|
|
3448
|
+
if (p) return p;
|
|
3449
|
+
try { p = layer.property("ADBE Time Remapping"); } catch (e2) {}
|
|
3450
|
+
return p;
|
|
3451
|
+
}
|
|
3452
|
+
|
|
3022
3453
|
/** null when the item can carry an audio cue, else the reason it cannot. */
|
|
3023
3454
|
function __audioItemProblem(item) {
|
|
3024
3455
|
if (item instanceof FolderItem) return "\"" + item.name + "\" is a folder, not footage";
|
|
@@ -3034,6 +3465,84 @@ function __sourceReport(item, path) {
|
|
|
3034
3465
|
return { itemId: item.id, name: item.name, path: path };
|
|
3035
3466
|
}
|
|
3036
3467
|
|
|
3468
|
+
/** The duration of a project item, or null when it does not say. */
|
|
3469
|
+
function __itemDuration(item) {
|
|
3470
|
+
if (!item) return null;
|
|
3471
|
+
var d = null;
|
|
3472
|
+
try { d = item.duration; } catch (e) {}
|
|
3473
|
+
if (__isFiniteNumber(d)) return d;
|
|
3474
|
+
return null;
|
|
3475
|
+
}
|
|
3476
|
+
|
|
3477
|
+
/**
|
|
3478
|
+
* Where a planned cue's layer will start and end, in comp time, given what is
|
|
3479
|
+
* known about its source. `end` is null when it cannot be known yet — an
|
|
3480
|
+
* unlooped cue on a file that has not been imported, with no outPoint to cap
|
|
3481
|
+
* it — and the caller decides what to do with that.
|
|
3482
|
+
*/
|
|
3483
|
+
function __cueExtent(p, itemDuration) {
|
|
3484
|
+
var start = p.time;
|
|
3485
|
+
if (p.inPoint !== null) start = p.inPoint;
|
|
3486
|
+
var end = null;
|
|
3487
|
+
if (p.loop) {
|
|
3488
|
+
// A loop has no natural end, so its end is the one the caller gave or the
|
|
3489
|
+
// comp's, whichever the planner settled on.
|
|
3490
|
+
end = p.loopEnd;
|
|
3491
|
+
} else {
|
|
3492
|
+
var natural = null;
|
|
3493
|
+
if (__isFiniteNumber(itemDuration)) natural = p.time + itemDuration * p.stretchFactor;
|
|
3494
|
+
end = p.outPoint;
|
|
3495
|
+
// AE clamps an out point to what the source can supply, so a requested
|
|
3496
|
+
// outPoint past the file's end is not where the layer will end.
|
|
3497
|
+
if (end === null || (natural !== null && natural < end)) end = natural;
|
|
3498
|
+
}
|
|
3499
|
+
return { start: start, end: end };
|
|
3500
|
+
}
|
|
3501
|
+
|
|
3502
|
+
function __fmtSeconds(n) {
|
|
3503
|
+
// Rounded for prose only; nothing downstream reads these numbers back.
|
|
3504
|
+
return String(Math.round(n * 1000) / 1000) + "s";
|
|
3505
|
+
}
|
|
3506
|
+
|
|
3507
|
+
/**
|
|
3508
|
+
* null when the cue's fades fit inside its extent; a reason string when they
|
|
3509
|
+
* do not; and the string "unknown" when the extent cannot be computed yet.
|
|
3510
|
+
* Pure — used at plan time and again after the imports, on the same plan.
|
|
3511
|
+
*/
|
|
3512
|
+
function __fadeFitProblem(p, extent) {
|
|
3513
|
+
var total = p.fadeIn + p.fadeOut;
|
|
3514
|
+
if (total <= 0 && !p.loop) return null;
|
|
3515
|
+
if (extent.end === null) {
|
|
3516
|
+
if (total <= 0) return null;
|
|
3517
|
+
return "unknown";
|
|
3518
|
+
}
|
|
3519
|
+
var dur = extent.end - extent.start;
|
|
3520
|
+
var span = " (from " + __fmtSeconds(extent.start) + " to " + __fmtSeconds(extent.end) + ")";
|
|
3521
|
+
if (dur <= __CUE_TIME_EPS) {
|
|
3522
|
+
if (p.loop) {
|
|
3523
|
+
var why = "the comp's end, since no outPoint was given";
|
|
3524
|
+
if (p.outPoint !== null) why = "its outPoint";
|
|
3525
|
+
return "inPoint " + __fmtSeconds(extent.start) + " is not before the end of the loop at " + __fmtSeconds(extent.end) + " (" + why + ")";
|
|
3526
|
+
}
|
|
3527
|
+
return "inPoint " + __fmtSeconds(extent.start) + " is past where the cue ends" + span;
|
|
3528
|
+
}
|
|
3529
|
+
if (total <= 0) return null;
|
|
3530
|
+
if (total > dur + __CUE_TIME_EPS) {
|
|
3531
|
+
var what;
|
|
3532
|
+
if (p.fadeIn > 0 && p.fadeOut > 0) {
|
|
3533
|
+
what = "fadeIn " + __fmtSeconds(p.fadeIn) + " + fadeOut " + __fmtSeconds(p.fadeOut) + " = " + __fmtSeconds(total);
|
|
3534
|
+
} else if (p.fadeIn > 0) {
|
|
3535
|
+
what = "fadeIn " + __fmtSeconds(p.fadeIn);
|
|
3536
|
+
} else {
|
|
3537
|
+
what = "fadeOut " + __fmtSeconds(p.fadeOut);
|
|
3538
|
+
}
|
|
3539
|
+
var tail = "";
|
|
3540
|
+
if (!p.loop && p.outPoint === null) tail = " — the file's own length, with no outPoint to extend it and no loop";
|
|
3541
|
+
return what + " is longer than the cue, which runs " + __fmtSeconds(dur) + span + tail;
|
|
3542
|
+
}
|
|
3543
|
+
return null;
|
|
3544
|
+
}
|
|
3545
|
+
|
|
3037
3546
|
/**
|
|
3038
3547
|
* Turn the cue list into a plan, touching nothing. Every problem found is
|
|
3039
3548
|
* collected with the index of the cue that caused it rather than thrown on the
|
|
@@ -3055,6 +3564,15 @@ function __planAudioCues(comp, args) {
|
|
|
3055
3564
|
var prefix = "SFX_";
|
|
3056
3565
|
if (typeof args.namePrefix === "string") prefix = args.namePrefix;
|
|
3057
3566
|
|
|
3567
|
+
// One floor for the whole list: it is a property of the mix, not of a cue.
|
|
3568
|
+
var floorDb = __DEFAULT_FADE_FLOOR_DB;
|
|
3569
|
+
if (args.fadeFloorDb !== undefined && args.fadeFloorDb !== null) {
|
|
3570
|
+
if (!__isFiniteNumber(args.fadeFloorDb)) {
|
|
3571
|
+
throw new Error("fadeFloorDb must be a number of decibels (the default is " + __DEFAULT_FADE_FLOOR_DB + "); got " + String(args.fadeFloorDb));
|
|
3572
|
+
}
|
|
3573
|
+
floorDb = args.fadeFloorDb;
|
|
3574
|
+
}
|
|
3575
|
+
|
|
3058
3576
|
var byPath = __itemPathMap();
|
|
3059
3577
|
var problems = [];
|
|
3060
3578
|
var planned = [];
|
|
@@ -3071,7 +3589,7 @@ function __planAudioCues(comp, args) {
|
|
|
3071
3589
|
if (!hasId && !hasPath) { problems.push({ cue: i, reason: "has neither footageId nor path — give exactly one" }); continue; }
|
|
3072
3590
|
|
|
3073
3591
|
var time = cue.time;
|
|
3074
|
-
if (
|
|
3592
|
+
if (!__isFiniteNumber(time)) {
|
|
3075
3593
|
problems.push({ cue: i, reason: "time must be a number of seconds; got " + String(time) });
|
|
3076
3594
|
continue;
|
|
3077
3595
|
}
|
|
@@ -3082,7 +3600,7 @@ function __planAudioCues(comp, args) {
|
|
|
3082
3600
|
|
|
3083
3601
|
var levelDb = 0;
|
|
3084
3602
|
if (cue.levelDb !== undefined && cue.levelDb !== null) {
|
|
3085
|
-
if (
|
|
3603
|
+
if (!__isFiniteNumber(cue.levelDb)) {
|
|
3086
3604
|
problems.push({ cue: i, reason: "levelDb must be a number of decibels (0 is unedited); got " + String(cue.levelDb) });
|
|
3087
3605
|
continue;
|
|
3088
3606
|
}
|
|
@@ -3093,7 +3611,7 @@ function __planAudioCues(comp, args) {
|
|
|
3093
3611
|
var outPoint = null;
|
|
3094
3612
|
var trimBad = false;
|
|
3095
3613
|
if (cue.inPoint !== undefined && cue.inPoint !== null) {
|
|
3096
|
-
if (
|
|
3614
|
+
if (!__isFiniteNumber(cue.inPoint)) {
|
|
3097
3615
|
problems.push({ cue: i, reason: "inPoint must be a comp time in seconds; got " + String(cue.inPoint) });
|
|
3098
3616
|
trimBad = true;
|
|
3099
3617
|
} else {
|
|
@@ -3101,7 +3619,7 @@ function __planAudioCues(comp, args) {
|
|
|
3101
3619
|
}
|
|
3102
3620
|
}
|
|
3103
3621
|
if (!trimBad && cue.outPoint !== undefined && cue.outPoint !== null) {
|
|
3104
|
-
if (
|
|
3622
|
+
if (!__isFiniteNumber(cue.outPoint)) {
|
|
3105
3623
|
problems.push({ cue: i, reason: "outPoint must be a comp time in seconds; got " + String(cue.outPoint) });
|
|
3106
3624
|
trimBad = true;
|
|
3107
3625
|
} else {
|
|
@@ -3132,6 +3650,53 @@ function __planAudioCues(comp, args) {
|
|
|
3132
3650
|
}
|
|
3133
3651
|
}
|
|
3134
3652
|
|
|
3653
|
+
// The #85 options. Type-checked here, before the source is resolved, so a
|
|
3654
|
+
// cue with a bad stretch AND a missing file reports the first of the two
|
|
3655
|
+
// it hits — every cue still gets a line, which is what matters.
|
|
3656
|
+
var loop = false;
|
|
3657
|
+
if (cue.loop !== undefined && cue.loop !== null) {
|
|
3658
|
+
if (cue.loop !== true && cue.loop !== false) {
|
|
3659
|
+
problems.push({ cue: i, reason: "loop must be true or false; got " + String(cue.loop) });
|
|
3660
|
+
continue;
|
|
3661
|
+
}
|
|
3662
|
+
loop = cue.loop;
|
|
3663
|
+
}
|
|
3664
|
+
var stretch = null;
|
|
3665
|
+
if (cue.stretch !== undefined && cue.stretch !== null) {
|
|
3666
|
+
if (!__isFiniteNumber(cue.stretch) || cue.stretch <= 0) {
|
|
3667
|
+
problems.push({ cue: i, reason: "stretch must be a percentage greater than 0 (100 is unchanged, 200 is half speed); got " + String(cue.stretch) });
|
|
3668
|
+
continue;
|
|
3669
|
+
}
|
|
3670
|
+
stretch = cue.stretch;
|
|
3671
|
+
}
|
|
3672
|
+
var fadeIn = 0;
|
|
3673
|
+
var fadeOut = 0;
|
|
3674
|
+
var fadeBad = false;
|
|
3675
|
+
if (cue.fadeIn !== undefined && cue.fadeIn !== null) {
|
|
3676
|
+
if (!__isFiniteNumber(cue.fadeIn) || cue.fadeIn < 0) {
|
|
3677
|
+
problems.push({ cue: i, reason: "fadeIn must be a number of seconds, 0 or more; got " + String(cue.fadeIn) });
|
|
3678
|
+
fadeBad = true;
|
|
3679
|
+
} else {
|
|
3680
|
+
fadeIn = cue.fadeIn;
|
|
3681
|
+
}
|
|
3682
|
+
}
|
|
3683
|
+
if (!fadeBad && cue.fadeOut !== undefined && cue.fadeOut !== null) {
|
|
3684
|
+
if (!__isFiniteNumber(cue.fadeOut) || cue.fadeOut < 0) {
|
|
3685
|
+
problems.push({ cue: i, reason: "fadeOut must be a number of seconds, 0 or more; got " + String(cue.fadeOut) });
|
|
3686
|
+
fadeBad = true;
|
|
3687
|
+
} else {
|
|
3688
|
+
fadeOut = cue.fadeOut;
|
|
3689
|
+
}
|
|
3690
|
+
}
|
|
3691
|
+
if (fadeBad) continue;
|
|
3692
|
+
if ((fadeIn > 0 || fadeOut > 0) && floorDb >= levelDb) {
|
|
3693
|
+
problems.push({
|
|
3694
|
+
cue: i,
|
|
3695
|
+
reason: "fadeFloorDb " + floorDb + " is not below the cue's level " + levelDb + " dB, so its fade would go nowhere — lower the floor or raise the level"
|
|
3696
|
+
});
|
|
3697
|
+
continue;
|
|
3698
|
+
}
|
|
3699
|
+
|
|
3135
3700
|
// Resolve the source. A footageId names an item that must already be
|
|
3136
3701
|
// usable; a path is either something the project already holds or an
|
|
3137
3702
|
// import this call will do exactly once.
|
|
@@ -3169,44 +3734,85 @@ function __planAudioCues(comp, args) {
|
|
|
3169
3734
|
var name = prefix + __stripExtension(defaultName);
|
|
3170
3735
|
if (typeof cue.name === "string" && cue.name.length > 0) name = cue.name;
|
|
3171
3736
|
|
|
3172
|
-
|
|
3737
|
+
var stretchFactor = 1;
|
|
3738
|
+
if (stretch !== null) stretchFactor = stretch / 100;
|
|
3739
|
+
// A looped cue needs an end, because a loop has none of its own.
|
|
3740
|
+
var loopEnd = null;
|
|
3741
|
+
if (loop) {
|
|
3742
|
+
loopEnd = comp.duration;
|
|
3743
|
+
if (outPoint !== null) loopEnd = outPoint;
|
|
3744
|
+
}
|
|
3745
|
+
|
|
3746
|
+
var p = {
|
|
3173
3747
|
index: i, name: name, time: time, levelDb: levelDb,
|
|
3174
|
-
inPoint: inPoint, outPoint: outPoint, label: label, source: source
|
|
3175
|
-
|
|
3748
|
+
inPoint: inPoint, outPoint: outPoint, label: label, source: source,
|
|
3749
|
+
loop: loop, loopEnd: loopEnd, stretch: stretch, stretchFactor: stretchFactor,
|
|
3750
|
+
fadeIn: fadeIn, fadeOut: fadeOut,
|
|
3751
|
+
// true when the fade could not be measured against the file yet
|
|
3752
|
+
fadeUnchecked: false
|
|
3753
|
+
};
|
|
3754
|
+
|
|
3755
|
+
// Does the cue fit? Known now for a loop (its end is chosen here), for a
|
|
3756
|
+
// footageId or reused item (After Effects reports its duration) and for a
|
|
3757
|
+
// capped outPoint; not yet for an unlooped cue on a file still to import.
|
|
3758
|
+
var fit = __fadeFitProblem(p, __cueExtent(p, __itemDuration(source.item)));
|
|
3759
|
+
if (fit === "unknown") {
|
|
3760
|
+
p.fadeUnchecked = true;
|
|
3761
|
+
} else if (fit !== null) {
|
|
3762
|
+
problems.push({ cue: i, reason: fit });
|
|
3763
|
+
continue;
|
|
3764
|
+
}
|
|
3765
|
+
|
|
3766
|
+
planned.push(p);
|
|
3176
3767
|
}
|
|
3177
3768
|
|
|
3178
|
-
return { prefix: prefix, planned: planned, problems: problems, toImport: toImport };
|
|
3769
|
+
return { prefix: prefix, planned: planned, problems: problems, toImport: toImport, floorDb: floorDb };
|
|
3179
3770
|
}
|
|
3180
3771
|
|
|
3181
|
-
function
|
|
3772
|
+
function __audioProblemLines(problems) {
|
|
3182
3773
|
var lines = [];
|
|
3183
3774
|
for (var i = 0; i < problems.length; i++) {
|
|
3184
3775
|
lines.push("cue " + problems[i].cue + ": " + problems[i].reason);
|
|
3185
3776
|
}
|
|
3777
|
+
return lines.join("; ");
|
|
3778
|
+
}
|
|
3779
|
+
|
|
3780
|
+
function __audioProblemMessage(problems, total) {
|
|
3186
3781
|
return (
|
|
3187
3782
|
"place_audio_cues placed nothing — " + problems.length + " of " + total + " cues cannot be placed. " +
|
|
3188
|
-
|
|
3783
|
+
__audioProblemLines(problems) + ". Every cue is checked before anything is created, so the comp and project are " +
|
|
3189
3784
|
"untouched. Fix these and call again; dryRun:true checks a list without placing it."
|
|
3190
3785
|
);
|
|
3191
3786
|
}
|
|
3192
3787
|
|
|
3788
|
+
/**
|
|
3789
|
+
* Counts and the two lists a caller cannot work out alone — what would be
|
|
3790
|
+
* imported and what is already there — plus the failing cues. Never the
|
|
3791
|
+
* resolved list: a 7-cue "all fine" used to cost ~1.5k tokens of echo (#88).
|
|
3792
|
+
*/
|
|
3193
3793
|
function __audioDryRunReport(comp, plan, total) {
|
|
3194
|
-
var
|
|
3794
|
+
var wouldImport = [];
|
|
3795
|
+
for (var j = 0; j < plan.toImport.length; j++) wouldImport.push(plan.toImport[j].path);
|
|
3796
|
+
|
|
3797
|
+
var wouldReuse = [];
|
|
3798
|
+
var reuseSeen = {};
|
|
3799
|
+
var looped = 0;
|
|
3800
|
+
var faded = 0;
|
|
3801
|
+
var stretched = 0;
|
|
3802
|
+
var fadesUnchecked = 0;
|
|
3195
3803
|
for (var i = 0; i < plan.planned.length; i++) {
|
|
3196
3804
|
var p = plan.planned[i];
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3805
|
+
if (p.source.item && !reuseSeen.hasOwnProperty(String(p.source.item.id))) {
|
|
3806
|
+
reuseSeen[String(p.source.item.id)] = true;
|
|
3807
|
+
var r = { itemId: p.source.item.id, name: p.source.item.name };
|
|
3808
|
+
if (p.source.path) r.path = p.source.path;
|
|
3809
|
+
wouldReuse.push(r);
|
|
3201
3810
|
}
|
|
3202
|
-
if (p.
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
});
|
|
3811
|
+
if (p.loop) looped++;
|
|
3812
|
+
if (p.fadeIn > 0 || p.fadeOut > 0) faded++;
|
|
3813
|
+
if (p.stretch !== null) stretched++;
|
|
3814
|
+
if (p.fadeUnchecked) fadesUnchecked++;
|
|
3207
3815
|
}
|
|
3208
|
-
var wouldImport = [];
|
|
3209
|
-
for (var j = 0; j < plan.toImport.length; j++) wouldImport.push(plan.toImport[j].path);
|
|
3210
3816
|
|
|
3211
3817
|
var out = {
|
|
3212
3818
|
dryRun: true,
|
|
@@ -3214,17 +3820,24 @@ function __audioDryRunReport(comp, plan, total) {
|
|
|
3214
3820
|
compId: comp.id,
|
|
3215
3821
|
compName: comp.name,
|
|
3216
3822
|
cueCount: total,
|
|
3217
|
-
wouldPlace:
|
|
3823
|
+
wouldPlace: plan.planned.length,
|
|
3218
3824
|
wouldImport: wouldImport,
|
|
3825
|
+
wouldReuse: wouldReuse,
|
|
3219
3826
|
problems: plan.problems,
|
|
3220
|
-
cues: cues,
|
|
3221
3827
|
note: "Nothing was imported, created or changed, and this call is not an undo step."
|
|
3222
3828
|
};
|
|
3829
|
+
if (looped > 0) out.looped = looped;
|
|
3830
|
+
if (faded > 0) out.faded = faded;
|
|
3831
|
+
if (stretched > 0) out.stretched = stretched;
|
|
3223
3832
|
if (wouldImport.length > 0) {
|
|
3833
|
+
var fadeNote = "";
|
|
3834
|
+
if (fadesUnchecked > 0) {
|
|
3835
|
+
fadeNote = " — and, for " + fadesUnchecked + " cue(s) with a fade, whether the fade fits the file's length —";
|
|
3836
|
+
}
|
|
3224
3837
|
out.unverified =
|
|
3225
3838
|
wouldImport.length + " of these files are not in the project yet. They exist on disk, but whether each " +
|
|
3226
|
-
"carries an audio track is only knowable once After Effects has imported it
|
|
3227
|
-
"refuses the whole call if one does not.";
|
|
3839
|
+
"carries an audio track" + fadeNote + " is only knowable once After Effects has imported it; a real run checks " +
|
|
3840
|
+
"that and refuses the whole call if one does not.";
|
|
3228
3841
|
}
|
|
3229
3842
|
return out;
|
|
3230
3843
|
}
|
|
@@ -3239,16 +3852,40 @@ function __rollbackAudioCues(layers, items) {
|
|
|
3239
3852
|
}
|
|
3240
3853
|
}
|
|
3241
3854
|
|
|
3855
|
+
/**
|
|
3856
|
+
* The Time Remap expression that loops a cue. It is the pattern measured in
|
|
3857
|
+
* issue #85 — comp time since the layer began, wrapped at the file's length —
|
|
3858
|
+
* with two substitutions: `startTime` rather than `inPoint`, so trimming the
|
|
3859
|
+
* in point hides the front of the file the way it does on every other layer
|
|
3860
|
+
* instead of delaying it; and the source's duration read live off the layer,
|
|
3861
|
+
* so a relinked file keeps looping. An expression cannot read a layer's
|
|
3862
|
+
* stretch, so a stretched cue has its factor baked in, and that constant goes
|
|
3863
|
+
* stale if the stretch is changed by hand afterwards.
|
|
3864
|
+
*/
|
|
3865
|
+
function __loopExpression(p) {
|
|
3866
|
+
var elapsed = "(time - startTime)";
|
|
3867
|
+
if (p.stretch !== null && p.stretch !== 100) elapsed = "((time - startTime) * 100 / " + p.stretch + ")";
|
|
3868
|
+
return elapsed + " % thisLayer.source.duration";
|
|
3869
|
+
}
|
|
3870
|
+
|
|
3242
3871
|
/**
|
|
3243
3872
|
* `created` is the rollback list, and the layer joins it the instant it exists
|
|
3244
3873
|
* rather than once it is fully configured. A cue that dies between add() and
|
|
3245
3874
|
* the last setValue is exactly the case rollback is for, and a layer that had
|
|
3246
3875
|
* not been registered yet would be the one thing left behind.
|
|
3876
|
+
*
|
|
3877
|
+
* The order after that is not free to change. Each of the #85 options moves
|
|
3878
|
+
* something After Effects then resets: `stretch` moves startTime, so startTime
|
|
3879
|
+
* is set after it; enabling time remap resets the out point, so the extent is
|
|
3880
|
+
* set after that; and the fade keys sit at the in/out the layer actually has,
|
|
3881
|
+
* so they go last. Out point before in point throughout — extending first is
|
|
3882
|
+
* what lets an in point past the file's natural end land on a looped layer.
|
|
3247
3883
|
*/
|
|
3248
|
-
function __placeAudioCue(comp, p, item, created) {
|
|
3884
|
+
function __placeAudioCue(comp, p, item, created, floorDb) {
|
|
3249
3885
|
var layer = comp.layers.add(item);
|
|
3250
3886
|
created.push(layer);
|
|
3251
3887
|
layer.name = p.name;
|
|
3888
|
+
if (p.stretch !== null) layer.stretch = p.stretch;
|
|
3252
3889
|
// startTime first: it slides the whole layer and would drag any trim with it.
|
|
3253
3890
|
layer.startTime = p.time;
|
|
3254
3891
|
var levels = __audioLevelsProperty(layer);
|
|
@@ -3257,14 +3894,81 @@ function __placeAudioCue(comp, p, item, created) {
|
|
|
3257
3894
|
"the layer created for \"" + item.name + "\" has no Audio Levels property, so its level could not be set"
|
|
3258
3895
|
);
|
|
3259
3896
|
}
|
|
3260
|
-
// AE's Audio Levels is itself in decibels, one entry per channel.
|
|
3261
|
-
levels.setValue([p.levelDb, p.levelDb]);
|
|
3262
3897
|
if (p.label !== null) layer.label = p.label;
|
|
3263
|
-
|
|
3264
|
-
if (p.
|
|
3898
|
+
|
|
3899
|
+
if (p.loop) {
|
|
3900
|
+
// `=== false`, as with hasAudio: a layer that does not answer the question
|
|
3901
|
+
// is not refused on the strength of a missing property.
|
|
3902
|
+
if (layer.canSetTimeRemapEnabled === false) {
|
|
3903
|
+
throw new Error(
|
|
3904
|
+
"After Effects reports that time remapping cannot be enabled on the layer created for \"" + item.name + "\", so it cannot loop"
|
|
3905
|
+
);
|
|
3906
|
+
}
|
|
3907
|
+
layer.timeRemapEnabled = true;
|
|
3908
|
+
var remap = __timeRemapProperty(layer);
|
|
3909
|
+
if (!remap) {
|
|
3910
|
+
throw new Error(
|
|
3911
|
+
"time remapping was enabled on the layer created for \"" + item.name + "\" but its Time Remap property could not be found, so the loop expression could not be set"
|
|
3912
|
+
);
|
|
3913
|
+
}
|
|
3914
|
+
// The two keyframes AE created when remapping was enabled stay exactly
|
|
3915
|
+
// where they are. Removing them hides the property, and the next write to
|
|
3916
|
+
// it throws (issue #86); the expression overrides them anyway.
|
|
3917
|
+
remap.expression = __loopExpression(p);
|
|
3918
|
+
layer.outPoint = p.loopEnd;
|
|
3919
|
+
if (p.inPoint !== null) layer.inPoint = p.inPoint;
|
|
3920
|
+
} else {
|
|
3921
|
+
if (p.outPoint !== null) layer.outPoint = p.outPoint;
|
|
3922
|
+
if (p.inPoint !== null) layer.inPoint = p.inPoint;
|
|
3923
|
+
}
|
|
3924
|
+
|
|
3925
|
+
// AE's Audio Levels is itself in decibels, one entry per channel.
|
|
3926
|
+
var level = [p.levelDb, p.levelDb];
|
|
3927
|
+
if (p.fadeIn > 0 || p.fadeOut > 0) {
|
|
3928
|
+
var inP = layer.inPoint;
|
|
3929
|
+
var outP = layer.outPoint;
|
|
3930
|
+
var dur = outP - inP;
|
|
3931
|
+
// The planner measured the fade against what it could know; this is the
|
|
3932
|
+
// extent After Effects actually gave the layer, and the last line.
|
|
3933
|
+
if (p.fadeIn + p.fadeOut > dur + __CUE_TIME_EPS) {
|
|
3934
|
+
throw new Error(
|
|
3935
|
+
"its fades (" + __fmtSeconds(p.fadeIn) + " in, " + __fmtSeconds(p.fadeOut) + " out) are longer than the " +
|
|
3936
|
+
__fmtSeconds(dur) + " After Effects gave the layer (" + __fmtSeconds(inP) + " to " + __fmtSeconds(outP) + ")"
|
|
3937
|
+
);
|
|
3938
|
+
}
|
|
3939
|
+
var floor = [floorDb, floorDb];
|
|
3940
|
+
if (p.fadeIn > 0) {
|
|
3941
|
+
levels.setValueAtTime(inP, floor);
|
|
3942
|
+
levels.setValueAtTime(inP + p.fadeIn, level);
|
|
3943
|
+
}
|
|
3944
|
+
if (p.fadeOut > 0) {
|
|
3945
|
+
levels.setValueAtTime(outP - p.fadeOut, level);
|
|
3946
|
+
levels.setValueAtTime(outP, floor);
|
|
3947
|
+
}
|
|
3948
|
+
} else {
|
|
3949
|
+
levels.setValue(level);
|
|
3950
|
+
}
|
|
3265
3951
|
return layer;
|
|
3266
3952
|
}
|
|
3267
3953
|
|
|
3954
|
+
/**
|
|
3955
|
+
* What the caller gets back per cue: the id it has to carry forward, the name,
|
|
3956
|
+
* the time, and — only where an option could have changed it — what After
|
|
3957
|
+
* Effects actually did. Never an echo of the request.
|
|
3958
|
+
*/
|
|
3959
|
+
function __placedEntry(layer, p) {
|
|
3960
|
+
var entry = { layerId: layer.id, name: layer.name, time: p.time };
|
|
3961
|
+
// Read the trim back: AE clamps an in/out point to what the source can
|
|
3962
|
+
// actually supply, and the caller should see what it got.
|
|
3963
|
+
if (p.inPoint !== null) entry.inPoint = layer.inPoint;
|
|
3964
|
+
if (p.outPoint !== null || p.loop || p.stretch !== null) entry.outPoint = layer.outPoint;
|
|
3965
|
+
if (p.loop) entry.looped = true;
|
|
3966
|
+
if (p.stretch !== null) entry.stretch = layer.stretch;
|
|
3967
|
+
if (p.fadeIn > 0) entry.fadeIn = p.fadeIn;
|
|
3968
|
+
if (p.fadeOut > 0) entry.fadeOut = p.fadeOut;
|
|
3969
|
+
return entry;
|
|
3970
|
+
}
|
|
3971
|
+
|
|
3268
3972
|
OPS.place_audio_cues = noUndoWhen(
|
|
3269
3973
|
// dryRun is not an undo step either. A plan that quietly appeared in the
|
|
3270
3974
|
// user's undo history would make "this changed nothing" false in the one
|
|
@@ -3285,6 +3989,7 @@ OPS.place_audio_cues = noUndoWhen(
|
|
|
3285
3989
|
var reusedReport = [];
|
|
3286
3990
|
var reusedSeen = {};
|
|
3287
3991
|
var placed = [];
|
|
3992
|
+
var anyFade = false;
|
|
3288
3993
|
|
|
3289
3994
|
try {
|
|
3290
3995
|
// One import per distinct file, before any layer exists, so a bad file
|
|
@@ -3300,6 +4005,26 @@ OPS.place_audio_cues = noUndoWhen(
|
|
|
3300
4005
|
importedReport.push(__sourceReport(newItem, spec.path));
|
|
3301
4006
|
}
|
|
3302
4007
|
|
|
4008
|
+
// The fades the planner could not measure — on files it had not seen —
|
|
4009
|
+
// are measured now, with every duration known and no layer yet made, so
|
|
4010
|
+
// a misfit still names every offending cue at once and costs only the
|
|
4011
|
+
// imports, which the rollback takes back.
|
|
4012
|
+
var lateProblems = [];
|
|
4013
|
+
for (var f = 0; f < plan.planned.length; f++) {
|
|
4014
|
+
var pf = plan.planned[f];
|
|
4015
|
+
if (!pf.fadeUnchecked) continue;
|
|
4016
|
+
var fit = __fadeFitProblem(pf, __cueExtent(pf, __itemDuration(imported[pf.source.fsName])));
|
|
4017
|
+
if (fit === "unknown") {
|
|
4018
|
+
fit = "After Effects did not report a duration for the imported " + pf.source.path + ", so its fade cannot be checked";
|
|
4019
|
+
}
|
|
4020
|
+
if (fit !== null) lateProblems.push({ cue: pf.index, reason: fit });
|
|
4021
|
+
}
|
|
4022
|
+
if (lateProblems.length > 0) {
|
|
4023
|
+
throw new Error(
|
|
4024
|
+
"the fades on " + lateProblems.length + " cue(s) do not fit the files once imported — " + __audioProblemLines(lateProblems)
|
|
4025
|
+
);
|
|
4026
|
+
}
|
|
4027
|
+
|
|
3303
4028
|
for (var k = 0; k < plan.planned.length; k++) {
|
|
3304
4029
|
var p = plan.planned[k];
|
|
3305
4030
|
var item = p.source.item;
|
|
@@ -3310,23 +4035,12 @@ OPS.place_audio_cues = noUndoWhen(
|
|
|
3310
4035
|
}
|
|
3311
4036
|
var layer;
|
|
3312
4037
|
try {
|
|
3313
|
-
layer = __placeAudioCue(comp, p, item, createdLayers);
|
|
4038
|
+
layer = __placeAudioCue(comp, p, item, createdLayers, plan.floorDb);
|
|
3314
4039
|
} catch (eCue) {
|
|
3315
4040
|
throw new Error("cue " + p.index + " (\"" + p.name + "\" at " + p.time + "s): " + eCue.message);
|
|
3316
4041
|
}
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
index: layer.index,
|
|
3320
|
-
name: layer.name,
|
|
3321
|
-
time: p.time,
|
|
3322
|
-
levelDb: p.levelDb,
|
|
3323
|
-
itemId: item.id,
|
|
3324
|
-
// Read the trim back: AE clamps an in/out point to what the source
|
|
3325
|
-
// can actually supply, and the caller should see what it got.
|
|
3326
|
-
inPoint: layer.inPoint,
|
|
3327
|
-
outPoint: layer.outPoint,
|
|
3328
|
-
label: layer.label
|
|
3329
|
-
});
|
|
4042
|
+
if (p.fadeIn > 0 || p.fadeOut > 0) anyFade = true;
|
|
4043
|
+
placed.push(__placedEntry(layer, p));
|
|
3330
4044
|
}
|
|
3331
4045
|
} catch (e) {
|
|
3332
4046
|
__rollbackAudioCues(createdLayers, importedItems);
|
|
@@ -3337,13 +4051,15 @@ OPS.place_audio_cues = noUndoWhen(
|
|
|
3337
4051
|
);
|
|
3338
4052
|
}
|
|
3339
4053
|
|
|
3340
|
-
|
|
4054
|
+
var out = {
|
|
3341
4055
|
compId: comp.id,
|
|
3342
4056
|
placed: placed,
|
|
3343
4057
|
count: placed.length,
|
|
3344
4058
|
sources: { imported: importedReport, reused: reusedReport },
|
|
3345
4059
|
levelUnit: "dB"
|
|
3346
4060
|
};
|
|
4061
|
+
if (anyFade) out.fadeFloorDb = plan.floorDb;
|
|
4062
|
+
return out;
|
|
3347
4063
|
}
|
|
3348
4064
|
);
|
|
3349
4065
|
|
|
@@ -3841,6 +4557,11 @@ OPS.set_house_style = noUndo(function (args) {
|
|
|
3841
4557
|
}
|
|
3842
4558
|
|
|
3843
4559
|
file.encoding = "UTF-8";
|
|
4560
|
+
// ExtendScript's default lineFeed on macOS is "Macintosh", which turns every
|
|
4561
|
+
// "\n" into a bare "\r" on disk (measured on 26.3: a guide written through
|
|
4562
|
+
// this op read back fine, because File.read normalises, but opened as one
|
|
4563
|
+
// long line in a text editor). The file is meant to be hand-edited.
|
|
4564
|
+
file.lineFeed = "Unix";
|
|
3844
4565
|
if (!file.open("w")) throw new Error("Could not open " + file.fsName + " for writing");
|
|
3845
4566
|
try {
|
|
3846
4567
|
if (!file.write(content)) throw new Error("Write failed for " + file.fsName);
|
|
@@ -4654,7 +5375,16 @@ OPS.get_project_summary = noUndo(function (args) {
|
|
|
4654
5375
|
};
|
|
4655
5376
|
});
|
|
4656
5377
|
|
|
5378
|
+
// A search, so the default is the opposite of list_layers: absent `include`
|
|
5379
|
+
// means the core alone — id/index/name/sourceType plus the comp — because the
|
|
5380
|
+
// reason to call this is to learn which layers exist and what to address them
|
|
5381
|
+
// by, not to read them (issue #87: nine matches cost ~2k tokens, re-sent on
|
|
5382
|
+
// every later request). `include: []` and omitting it are therefore the same
|
|
5383
|
+
// here, and the section names are list_layers' own, read through the same
|
|
5384
|
+
// __layerSummary. What was included is echoed, so a bounded answer is never
|
|
5385
|
+
// read as a full one.
|
|
4657
5386
|
OPS.find_layers = noUndo(function (args) {
|
|
5387
|
+
var sections = (args && args.include) ? args.include : [];
|
|
4658
5388
|
var out = [];
|
|
4659
5389
|
var comps = [];
|
|
4660
5390
|
if (args.compId) comps.push(getCompById(args.compId));
|
|
@@ -4681,13 +5411,13 @@ OPS.find_layers = noUndo(function (args) {
|
|
|
4681
5411
|
}
|
|
4682
5412
|
if (!hit) continue;
|
|
4683
5413
|
}
|
|
4684
|
-
var s = __layerSummary(l);
|
|
5414
|
+
var s = __layerSummary(l, sections);
|
|
4685
5415
|
s.compId = c.id;
|
|
4686
5416
|
s.compName = c.name;
|
|
4687
5417
|
out.push(s);
|
|
4688
5418
|
}
|
|
4689
5419
|
}
|
|
4690
|
-
return out;
|
|
5420
|
+
return { matches: out, count: out.length, compsSearched: comps.length, included: sections };
|
|
4691
5421
|
});
|
|
4692
5422
|
|
|
4693
5423
|
|