2905 lines
95 KiB
React
2905 lines
95 KiB
React
/*
|
||
VFXReview Connector
|
||
Adobe After Effects 2024+ dockable ScriptUI panel
|
||
*/
|
||
|
||
var TOKEN = "am3O0PWUtqMJkAqsZ+bO7lho4cxQItxgukF6FHteAx4=";
|
||
var BASE_URL = "http://localhost:3000";
|
||
var PROJECT_CODE = "UNG_S1";
|
||
var EXPORT_ROOT = "V:/_EXPORTS/UNG";
|
||
var FOOTAGE_ROOT = "V:/_FOOTAGE/UNG";
|
||
var BANNER_IMAGE_PATH = "V:/VFXReviewConnector/banner.png";
|
||
// Optional: artist email for pipeline export attribution (Queue Export)
|
||
var ARTIST_EMAIL = "chris@twotalesanimation.com";
|
||
// Handle frames included in plates before the shot's source TC in-point.
|
||
// Used to derive the plate's own start timecode from the API's timecodeStart.
|
||
var HANDLE_FRAMES = 8;
|
||
|
||
(function VFXReviewConnector(thisObj) {
|
||
var uiState = {
|
||
episodeDropdown: null,
|
||
shotDropdown: null,
|
||
selectionText: null,
|
||
lastActionText: null,
|
||
pipelineStatusText: null,
|
||
urgentCheckbox: null,
|
||
submissionNoteField: null
|
||
};
|
||
|
||
var pipelineState = {
|
||
pollShotCode: null,
|
||
pollTaskId: 0,
|
||
prefilledShotCode: null
|
||
};
|
||
|
||
var buildState = {
|
||
lastStartSource: "",
|
||
lastStartFrames: 0
|
||
};
|
||
|
||
function parseJSON(text) {
|
||
if (!text) {
|
||
return null;
|
||
}
|
||
|
||
try {
|
||
if (typeof JSON !== "undefined" && JSON.parse) {
|
||
return JSON.parse(text);
|
||
}
|
||
} catch (jsonError) {
|
||
}
|
||
|
||
try {
|
||
return eval("(" + text + ")");
|
||
} catch (evalError) {
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
function getSelectedComps() {
|
||
var comps = [];
|
||
var selection;
|
||
var i;
|
||
|
||
if (!app.project) {
|
||
return comps;
|
||
}
|
||
|
||
selection = app.project.selection;
|
||
for (i = 0; i < selection.length; i += 1) {
|
||
if (selection[i] instanceof CompItem) {
|
||
comps.push(selection[i]);
|
||
}
|
||
}
|
||
|
||
return comps;
|
||
}
|
||
|
||
function getShotData(shotCode) {
|
||
var url;
|
||
var command;
|
||
var response;
|
||
var parsed;
|
||
|
||
try {
|
||
url = BASE_URL + "/api/ext/shots/lookup" +
|
||
"?shotCode=" + encodeURIComponent(shotCode) +
|
||
"&projectCode=" + PROJECT_CODE;
|
||
|
||
command = "curl -s " +
|
||
"-H \"Authorization: Bearer " + TOKEN + "\" " +
|
||
"-H \"Accept: application/json\" " +
|
||
"\"" + url + "\"";
|
||
|
||
response = system.callSystem(command);
|
||
} catch (callError) {
|
||
return null;
|
||
}
|
||
|
||
parsed = parseJSON(response);
|
||
if (!parsed) {
|
||
return null;
|
||
}
|
||
|
||
return parsed;
|
||
}
|
||
|
||
function getAPIData(url) {
|
||
var command;
|
||
var response;
|
||
var parsed;
|
||
|
||
try {
|
||
command = "curl -s " +
|
||
"-H \"Authorization: Bearer " + TOKEN + "\" " +
|
||
"-H \"Accept: application/json\" " +
|
||
"\"" + url + "\"";
|
||
|
||
response = system.callSystem(command);
|
||
} catch (callError) {
|
||
return null;
|
||
}
|
||
|
||
parsed = parseJSON(response);
|
||
if (!parsed) {
|
||
return null;
|
||
}
|
||
|
||
return parsed;
|
||
}
|
||
|
||
function getListFromResponse(data, keyName) {
|
||
if (!data) {
|
||
return [];
|
||
}
|
||
|
||
if (data.length !== undefined) {
|
||
return data;
|
||
}
|
||
|
||
if (keyName && data[keyName] && data[keyName].length !== undefined) {
|
||
return data[keyName];
|
||
}
|
||
|
||
if (data.data && data.data.length !== undefined) {
|
||
return data.data;
|
||
}
|
||
|
||
if (data.results && data.results.length !== undefined) {
|
||
return data.results;
|
||
}
|
||
|
||
if (data.items && data.items.length !== undefined) {
|
||
return data.items;
|
||
}
|
||
|
||
return [];
|
||
}
|
||
|
||
function padEpisodeNumber(value) {
|
||
var text = String(value);
|
||
|
||
while (text.length < 3) {
|
||
text = "0" + text;
|
||
}
|
||
|
||
return text;
|
||
}
|
||
|
||
function deriveEpisodeCode(value) {
|
||
var text = String(value);
|
||
|
||
if (/^[A-Z]{3}_\d{3}$/.test(text)) {
|
||
return text;
|
||
}
|
||
|
||
if (/^\d+$/.test(text)) {
|
||
return "UNG_" + padEpisodeNumber(text);
|
||
}
|
||
|
||
return text;
|
||
}
|
||
|
||
function deriveEpisodeAPIValue(value) {
|
||
var text = String(value);
|
||
var match = text.match(/[A-Z]{3}_(\d{3})/);
|
||
|
||
if (match && match.length > 1) {
|
||
return String(parseInt(match[1], 10));
|
||
}
|
||
|
||
return text;
|
||
}
|
||
|
||
function getEpisodeValue(item) {
|
||
if (item === null || item === undefined) {
|
||
return "";
|
||
}
|
||
|
||
if (typeof item !== "object") {
|
||
return deriveEpisodeAPIValue(item);
|
||
}
|
||
|
||
return deriveEpisodeAPIValue(item.episode || item.episodeNumber || item.number || item.id || item.code || item.episodeCode || item.name || "");
|
||
}
|
||
|
||
function getEpisodeLabel(item) {
|
||
if (item === null || item === undefined) {
|
||
return "";
|
||
}
|
||
|
||
if (typeof item !== "object") {
|
||
return deriveEpisodeCode(item);
|
||
}
|
||
|
||
return String(item.episodeCode || item.code || item.name || deriveEpisodeCode(getEpisodeValue(item)));
|
||
}
|
||
|
||
function getShotCodeValue(item) {
|
||
if (item === null || item === undefined) {
|
||
return "";
|
||
}
|
||
|
||
if (typeof item !== "object") {
|
||
return String(item);
|
||
}
|
||
|
||
return String(item.shotCode || item.code || item.name || "");
|
||
}
|
||
|
||
function getEpisodesFromAPI() {
|
||
var url = BASE_URL + "/api/ext/projects/" + PROJECT_CODE + "/episodes";
|
||
var data = getAPIData(url);
|
||
|
||
return getListFromResponse(data, "episodes");
|
||
}
|
||
|
||
function getShotsFromAPI(episodeValue) {
|
||
var url = BASE_URL + "/api/ext/projects/" + PROJECT_CODE + "/shots?episode=" + encodeURIComponent(episodeValue);
|
||
var data = getAPIData(url);
|
||
|
||
return getListFromResponse(data, "shots");
|
||
}
|
||
|
||
function normalizeShotData(data) {
|
||
if (!data) {
|
||
return null;
|
||
}
|
||
|
||
if (data.shotCode) {
|
||
return data;
|
||
}
|
||
|
||
if (data.shot && data.shot.shotCode) {
|
||
return data.shot;
|
||
}
|
||
|
||
if (data.data && data.data.shotCode) {
|
||
return data.data;
|
||
}
|
||
|
||
if (data.result && data.result.shotCode) {
|
||
return data.result;
|
||
}
|
||
|
||
if (data.results && data.results.length && data.results[0].shotCode) {
|
||
return data.results[0];
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
function ensureFolder(path) {
|
||
var folder = new Folder(path);
|
||
var parent;
|
||
|
||
if (folder.exists) {
|
||
return true;
|
||
}
|
||
|
||
parent = folder.parent;
|
||
if (parent && !parent.exists) {
|
||
if (!ensureFolder(parent.fsName)) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
try {
|
||
return folder.create();
|
||
} catch (folderError) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
function setCompStartFrame(comp, frameNumber) {
|
||
try {
|
||
comp.displayStartFrame = frameNumber;
|
||
return;
|
||
} catch (frameError) {
|
||
}
|
||
|
||
try {
|
||
comp.displayStartTime = frameNumber / comp.frameRate;
|
||
} catch (timeError) {
|
||
}
|
||
}
|
||
|
||
function deselectAllProjectItems() {
|
||
var i;
|
||
|
||
try {
|
||
for (i = 1; i <= app.project.numItems; i += 1) {
|
||
app.project.item(i).selected = false;
|
||
}
|
||
} catch (deselectError) {
|
||
}
|
||
}
|
||
|
||
// AE scripting exposes no direct read of a footage item's embedded start
|
||
// timecode. Recreate the UI behaviour instead: "New Comp from Selection"
|
||
// inherits the footage's start timecode from the file metadata, so build a
|
||
// throwaway comp, read the start time AE gave it, then delete it.
|
||
//
|
||
// Returns null when the probe could not run (menu command unavailable or
|
||
// disabled), so callers can fall back to the API timecode.
|
||
function probeFootageStartFrames(footageItem, fps) {
|
||
var commandId;
|
||
var projectPanelId;
|
||
var existingIds = {};
|
||
var probeComp = null;
|
||
var frames = null;
|
||
var i;
|
||
var item;
|
||
|
||
try {
|
||
commandId = app.findMenuCommandId("New Comp from Selection");
|
||
if (!commandId) {
|
||
return null;
|
||
}
|
||
|
||
for (i = 1; i <= app.project.numItems; i += 1) {
|
||
existingIds[app.project.item(i).id] = true;
|
||
}
|
||
|
||
// executeCommand mirrors the menu bar, so the Project panel has to
|
||
// be frontmost or "New Comp from Selection" is greyed out and the
|
||
// call silently does nothing.
|
||
try {
|
||
projectPanelId = app.findMenuCommandId("Project");
|
||
if (projectPanelId) {
|
||
app.executeCommand(projectPanelId);
|
||
}
|
||
} catch (focusError) {
|
||
}
|
||
|
||
deselectAllProjectItems();
|
||
footageItem.selected = true;
|
||
app.executeCommand(commandId);
|
||
|
||
// Only ever touch a comp that did not exist before the probe —
|
||
// never app.project.activeItem, which is an existing open comp
|
||
// when the command did not run.
|
||
for (i = app.project.numItems; i >= 1; i -= 1) {
|
||
item = app.project.item(i);
|
||
if (item instanceof CompItem && !existingIds[item.id]) {
|
||
probeComp = item;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (probeComp) {
|
||
frames = Math.round((probeComp.displayStartTime || 0) * (fps || probeComp.frameRate || 24));
|
||
}
|
||
} catch (probeError) {
|
||
frames = null;
|
||
}
|
||
|
||
try {
|
||
if (probeComp) {
|
||
probeComp.remove();
|
||
}
|
||
} catch (removeError) {
|
||
}
|
||
|
||
return frames;
|
||
}
|
||
|
||
// The plate's own start timecode: AE metadata when we can read it,
|
||
// otherwise the API's source clip timecode less the handle frames — the
|
||
// same convention the Set Start Timecode button uses.
|
||
function getFootageStartFrames(footageItem, apiShot, fps) {
|
||
var probed = probeFootageStartFrames(footageItem, fps);
|
||
var frames;
|
||
|
||
if (probed !== null && probed > 0) {
|
||
return { frames: probed, source: "footage metadata" };
|
||
}
|
||
|
||
if (apiShot && apiShot.timecodeStart) {
|
||
frames = parseTimecodeToFrames(apiShot.timecodeStart, fps) - HANDLE_FRAMES;
|
||
if (frames > 0) {
|
||
return { frames: frames, source: "API timecode " + apiShot.timecodeStart + " -" + HANDLE_FRAMES + "f" };
|
||
}
|
||
}
|
||
|
||
return { frames: 0, source: probed === 0 ? "footage starts at 00:00:00:00" : "no timecode found" };
|
||
}
|
||
|
||
function findComp(name) {
|
||
var i;
|
||
var item;
|
||
|
||
if (!app.project) {
|
||
return null;
|
||
}
|
||
|
||
for (i = 1; i <= app.project.numItems; i += 1) {
|
||
item = app.project.item(i);
|
||
if (item instanceof CompItem && item.name === name) {
|
||
return item;
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
function findFolder(name) {
|
||
var i;
|
||
var item;
|
||
|
||
if (!app.project) {
|
||
return null;
|
||
}
|
||
|
||
for (i = 1; i <= app.project.numItems; i += 1) {
|
||
item = app.project.item(i);
|
||
if (item instanceof FolderItem && item.name === name) {
|
||
return item;
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
function findEpisodeProjectFolder(episodeCode) {
|
||
var shotsRoot = findFolder("__SHOTS");
|
||
var i;
|
||
var item;
|
||
|
||
if (!shotsRoot) {
|
||
return findFolder(episodeCode);
|
||
}
|
||
|
||
for (i = 1; i <= app.project.numItems; i += 1) {
|
||
item = app.project.item(i);
|
||
if (item instanceof FolderItem && item.name === episodeCode && item.parentFolder === shotsRoot) {
|
||
return item;
|
||
}
|
||
}
|
||
|
||
return findFolder(episodeCode);
|
||
}
|
||
|
||
function getEpisodeRootFolder(episodeCode) {
|
||
return new Folder(FOOTAGE_ROOT + "/" + episodeCode);
|
||
}
|
||
|
||
function getEpisodeCodesFromDisk() {
|
||
var root = new Folder(FOOTAGE_ROOT);
|
||
var folders;
|
||
var codes = [];
|
||
var i;
|
||
|
||
if (!root.exists) {
|
||
return codes;
|
||
}
|
||
|
||
folders = root.getFiles();
|
||
for (i = 0; i < folders.length; i += 1) {
|
||
if (folders[i] instanceof Folder) {
|
||
codes.push(folders[i].name);
|
||
}
|
||
}
|
||
|
||
codes.sort();
|
||
return codes;
|
||
}
|
||
|
||
function addUniqueText(values, value) {
|
||
var i;
|
||
|
||
for (i = 0; i < values.length; i += 1) {
|
||
if (values[i] === value) {
|
||
return;
|
||
}
|
||
}
|
||
|
||
values.push(value);
|
||
}
|
||
|
||
function getShotCodesForEpisode(episodeCode) {
|
||
var root = getEpisodeRootFolder(episodeCode);
|
||
var folders;
|
||
var shotCodes = [];
|
||
var i;
|
||
var shotCode;
|
||
|
||
if (!root.exists) {
|
||
return shotCodes;
|
||
}
|
||
|
||
folders = root.getFiles();
|
||
for (i = 0; i < folders.length; i += 1) {
|
||
if (folders[i] instanceof Folder) {
|
||
shotCode = extractShotCode(folders[i].name);
|
||
if (shotCode && folders[i].name.indexOf(shotCode) === 0) {
|
||
addUniqueText(shotCodes, shotCode);
|
||
}
|
||
}
|
||
}
|
||
|
||
shotCodes.sort();
|
||
return shotCodes;
|
||
}
|
||
|
||
function findSequenceFolders(shotCode, episodeCode) {
|
||
var root = getEpisodeRootFolder(episodeCode);
|
||
var folders;
|
||
var matches = [];
|
||
var i;
|
||
|
||
if (!root.exists) {
|
||
return matches;
|
||
}
|
||
|
||
folders = root.getFiles();
|
||
for (i = 0; i < folders.length; i += 1) {
|
||
if (folders[i] instanceof Folder && folders[i].name.indexOf(shotCode + "_") === 0) {
|
||
matches.push(folders[i]);
|
||
}
|
||
}
|
||
|
||
matches.sort();
|
||
return matches;
|
||
}
|
||
|
||
function importEXRSequence(folder) {
|
||
var files = folder.getFiles("*.exr");
|
||
var importOptions;
|
||
var footage;
|
||
|
||
if (files.length < 1) {
|
||
return null;
|
||
}
|
||
|
||
files.sort();
|
||
importOptions = new ImportOptions(files[0]);
|
||
importOptions.sequence = true;
|
||
footage = app.project.importFile(importOptions);
|
||
footage.mainSource.conformFrameRate = 24;
|
||
return footage;
|
||
}
|
||
|
||
function addOCIOToLayer(layer) {
|
||
var ocio;
|
||
var outputProp;
|
||
|
||
try {
|
||
ocio = layer.property("Effects").addProperty("OCIO Color Space Transform");
|
||
ocio.enabled = false;
|
||
} catch (ocioError) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
outputProp = ocio.property("Output Color Space");
|
||
} catch (propNameError) {
|
||
outputProp = null;
|
||
}
|
||
if (!outputProp) {
|
||
try {
|
||
outputProp = ocio.property(2);
|
||
} catch (propIdxError) {
|
||
outputProp = null;
|
||
}
|
||
}
|
||
if (outputProp) {
|
||
try {
|
||
outputProp.setValue(93);
|
||
} catch (setValueError) {
|
||
}
|
||
}
|
||
}
|
||
|
||
function getDateString() {
|
||
var today = new Date();
|
||
|
||
return today.getFullYear() +
|
||
"/" +
|
||
("0" + (today.getMonth() + 1)).slice(-2) +
|
||
"/" +
|
||
("0" + today.getDate()).slice(-2);
|
||
}
|
||
|
||
function parseTimecodeToSeconds(tc, fps) {
|
||
var parts = String(tc).split(/[:;]/);
|
||
var hh;
|
||
var mm;
|
||
var ss;
|
||
var ff;
|
||
|
||
if (parts.length < 4) {
|
||
return 0;
|
||
}
|
||
|
||
hh = parseInt(parts[0], 10);
|
||
mm = parseInt(parts[1], 10);
|
||
ss = parseInt(parts[2], 10);
|
||
ff = parseInt(parts[3], 10);
|
||
|
||
return hh * 3600 + mm * 60 + ss + ff / (fps || 24);
|
||
}
|
||
|
||
function parseTimecodeToFrames(tc, fps) {
|
||
var parts = String(tc).split(/[:;]/);
|
||
var hh;
|
||
var mm;
|
||
var ss;
|
||
var ff;
|
||
|
||
if (parts.length < 4) {
|
||
return 0;
|
||
}
|
||
|
||
hh = parseInt(parts[0], 10);
|
||
mm = parseInt(parts[1], 10);
|
||
ss = parseInt(parts[2], 10);
|
||
ff = parseInt(parts[3], 10);
|
||
|
||
return (hh * 3600 + mm * 60 + ss) * Math.round(fps || 24) + ff;
|
||
}
|
||
|
||
function findOverlayLayer(comp) {
|
||
var i;
|
||
var layer;
|
||
|
||
for (i = 1; i <= comp.numLayers; i += 1) {
|
||
layer = comp.layer(i);
|
||
|
||
try {
|
||
if (layer.source && layer.source instanceof CompItem && layer.source.name === "UNG_VFX_OVERLAY") {
|
||
return layer;
|
||
}
|
||
} catch (layerError) {
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
function setEssentialProperty(layer, propName, value) {
|
||
try {
|
||
layer.property("Essential Properties").property(propName).setValue(String(value));
|
||
return true;
|
||
} catch (propertyError) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
function applyOutputModuleTemplate(outputModule, templateName) {
|
||
try {
|
||
outputModule.applyTemplate(templateName);
|
||
return true;
|
||
} catch (templateError) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
function queueCompWithOutput(comp, templateName, outputPath) {
|
||
var renderQueueItem;
|
||
var outputModule;
|
||
|
||
try {
|
||
renderQueueItem = app.project.renderQueue.items.add(comp);
|
||
outputModule = renderQueueItem.outputModule(1);
|
||
if (!applyOutputModuleTemplate(outputModule, templateName)) {
|
||
renderQueueItem.remove();
|
||
return false;
|
||
}
|
||
outputModule.file = new File(outputPath);
|
||
return true;
|
||
} catch (queueError) {
|
||
try {
|
||
if (renderQueueItem) {
|
||
renderQueueItem.remove();
|
||
}
|
||
} catch (removeError) {
|
||
}
|
||
return false;
|
||
}
|
||
}
|
||
|
||
function updateStatus(selectedCount, message) {
|
||
if (uiState.selectionText) {
|
||
uiState.selectionText.text = selectedCount + " comps selected";
|
||
}
|
||
if (uiState.lastActionText) {
|
||
uiState.lastActionText.text = message || "";
|
||
}
|
||
}
|
||
|
||
function refreshSelection() {
|
||
var comps = getSelectedComps();
|
||
var message = comps.length + " comps selected";
|
||
updateStatus(comps.length, message);
|
||
return comps;
|
||
}
|
||
|
||
function extractShotCode(name) {
|
||
var match = String(name).match(/([A-Z]{3}_\d{3}_\d{3}_\d{3})/);
|
||
if (match && match.length > 1) {
|
||
return match[1];
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function getShotCodeFromComp(comp) {
|
||
return extractShotCode(comp.name) || comp.name;
|
||
}
|
||
|
||
function isPreviewComp(comp) {
|
||
return /_PREVIEW$/.test(comp.name);
|
||
}
|
||
|
||
function getMainCompForSelection(comp) {
|
||
var shotCode = getShotCodeFromComp(comp);
|
||
|
||
if (isPreviewComp(comp)) {
|
||
return findComp(shotCode);
|
||
}
|
||
|
||
return comp;
|
||
}
|
||
|
||
function getPreviewCompName(shotCode) {
|
||
return shotCode + "_PREVIEW";
|
||
}
|
||
|
||
function getPreviewCompForShot(shotCode) {
|
||
return findComp(getPreviewCompName(shotCode));
|
||
}
|
||
|
||
function getTargetComps() {
|
||
var comps = getSelectedComps();
|
||
|
||
if (comps.length < 1 && app.project && app.project.activeItem instanceof CompItem) {
|
||
comps.push(app.project.activeItem);
|
||
}
|
||
|
||
return comps;
|
||
}
|
||
|
||
function disableLayerByName(comp, layerName) {
|
||
var i;
|
||
var layer;
|
||
|
||
for (i = 1; i <= comp.numLayers; i += 1) {
|
||
try {
|
||
layer = comp.layer(i);
|
||
if (layer.name === layerName || (layer.source && layer.source instanceof CompItem && layer.source.name === layerName)) {
|
||
layer.enabled = false;
|
||
}
|
||
} catch (disableError) {
|
||
}
|
||
}
|
||
}
|
||
|
||
function updateOverlayForComp(comp, shotCode, apiData) {
|
||
var overlayComp = findComp("UNG_VFX_OVERLAY");
|
||
var overlayLayer;
|
||
var essentialProperties;
|
||
var overlayShotCode;
|
||
|
||
if (!overlayComp || !apiData || !apiData.shot) {
|
||
return false;
|
||
}
|
||
|
||
try {
|
||
overlayShotCode = apiData.shot.shotCode || shotCode;
|
||
overlayLayer = findOverlayLayer(comp);
|
||
if (!overlayLayer) {
|
||
overlayLayer = comp.layers.add(overlayComp);
|
||
overlayLayer.moveToBeginning();
|
||
}
|
||
|
||
overlayLayer.enabled = true;
|
||
essentialProperties = overlayLayer.property("Essential Properties");
|
||
essentialProperties.property("DATE").setValue(getDateString());
|
||
essentialProperties.property("SHOT NAME").setValue(overlayShotCode + "_cmp_TT_" + (apiData.shot.shotVersion || "v001"));
|
||
return true;
|
||
} catch (overlayError) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
function makeBatchResult(total) {
|
||
return {
|
||
total: total,
|
||
queued: 0,
|
||
renamed: 0,
|
||
skipped: 0,
|
||
failures: []
|
||
};
|
||
}
|
||
|
||
function addFailure(result, compName, reason) {
|
||
result.skipped += 1;
|
||
result.failures.push(compName + ": " + reason);
|
||
}
|
||
|
||
function fixCompNames() {
|
||
var comps = getSelectedComps();
|
||
var renamed = 0;
|
||
var i;
|
||
var shotCode;
|
||
|
||
app.beginUndoGroup("VFXReview Connector - Fix Comp Names");
|
||
for (i = 0; i < comps.length; i += 1) {
|
||
shotCode = extractShotCode(comps[i].name);
|
||
if (shotCode && comps[i].name !== shotCode) {
|
||
comps[i].name = shotCode;
|
||
renamed += 1;
|
||
}
|
||
}
|
||
app.endUndoGroup();
|
||
|
||
updateStatus(comps.length, renamed + " comp(s) renamed.");
|
||
}
|
||
|
||
function queueEXR() {
|
||
var comps = getSelectedComps();
|
||
var queued = 0;
|
||
var i;
|
||
var comp;
|
||
var lookupCode;
|
||
var shot;
|
||
var result = makeBatchResult(comps.length);
|
||
var shotCode;
|
||
var exrOutput;
|
||
var folderPath;
|
||
var outputPath;
|
||
|
||
app.beginUndoGroup("VFXReview Connector - Queue EXR");
|
||
for (i = 0; i < comps.length; i += 1) {
|
||
comp = comps[i];
|
||
lookupCode = getShotCodeFromComp(comp);
|
||
updateStatus(comps.length, "Looking up " + lookupCode + " (" + (i + 1) + " of " + comps.length + ")");
|
||
shot = normalizeShotData(getShotData(lookupCode));
|
||
|
||
if (!shot || !shot.shotCode || !shot.exrOutput) {
|
||
addFailure(result, comp.name, "API lookup failed or missing EXR output");
|
||
continue;
|
||
}
|
||
|
||
shotCode = shot.shotCode;
|
||
exrOutput = shot.exrOutput;
|
||
disableLayerByName(comp, "UNG_VFX_OVERLAY");
|
||
disableLayerByName(comp, "_SHOW LUT");
|
||
|
||
folderPath = EXPORT_ROOT + "/" + shotCode;
|
||
if (!ensureFolder(folderPath)) {
|
||
addFailure(result, comp.name, "Could not create output folder");
|
||
continue;
|
||
}
|
||
|
||
outputPath = folderPath + "/" + exrOutput + ".[#####].exr";
|
||
if (queueCompWithOutput(comp, "EXR Sequence", outputPath)) {
|
||
queued += 1;
|
||
result.queued += 1;
|
||
} else {
|
||
addFailure(result, comp.name, "Could not queue EXR render");
|
||
}
|
||
}
|
||
app.endUndoGroup();
|
||
|
||
updateStatus(comps.length, queued + " EXR renders queued. " + result.skipped + " skipped.");
|
||
}
|
||
|
||
function addOverlay() {
|
||
var comps = getSelectedComps();
|
||
var overlayComp = findComp("UNG_VFX_OVERLAY");
|
||
var overlayLayer;
|
||
var added = 0;
|
||
var updated = 0;
|
||
var skipped = 0;
|
||
var i;
|
||
var shotCode;
|
||
var data;
|
||
var dateString;
|
||
var essentialProperties;
|
||
|
||
if (comps.length < 1 && app.project && app.project.activeItem instanceof CompItem) {
|
||
comps.push(app.project.activeItem);
|
||
}
|
||
|
||
if (comps.length < 1) {
|
||
updateStatus(0, "Please select a comp first.");
|
||
return;
|
||
}
|
||
|
||
if (!overlayComp) {
|
||
updateStatus(comps.length, "UNG_VFX_OVERLAY not found.");
|
||
return;
|
||
}
|
||
|
||
dateString = getDateString();
|
||
|
||
app.beginUndoGroup("VFXReview Connector - Add Overlay");
|
||
for (i = 0; i < comps.length; i += 1) {
|
||
shotCode = getShotCodeFromComp(comps[i]);
|
||
updateStatus(comps.length, "Updating overlay for " + shotCode + " (" + (i + 1) + " of " + comps.length + ")");
|
||
data = getShotData(shotCode);
|
||
|
||
if (!data || !data.shot) {
|
||
skipped += 1;
|
||
continue;
|
||
}
|
||
|
||
try {
|
||
overlayLayer = findOverlayLayer(comps[i]);
|
||
if (!overlayLayer) {
|
||
overlayLayer = comps[i].layers.add(overlayComp);
|
||
overlayLayer.moveToBeginning();
|
||
added += 1;
|
||
}
|
||
|
||
essentialProperties = overlayLayer.property("Essential Properties");
|
||
essentialProperties.property("DATE").setValue(dateString);
|
||
essentialProperties.property("SHOT NAME").setValue(shotCode + "_cmp_TT_" + (data.shot.shotVersion || "v001"));
|
||
updated += 1;
|
||
} catch (overlayError) {
|
||
skipped += 1;
|
||
}
|
||
}
|
||
app.endUndoGroup();
|
||
|
||
updateStatus(comps.length, updated + " overlay(s) updated. " + added + " added. " + skipped + " skipped.");
|
||
}
|
||
|
||
function buildPreviewForShot(shotComp, shotCode, apiData) {
|
||
var templateComp = findComp("UNG_EXPORT_TEMPLATE");
|
||
var previewsFolder = findFolder("_PREVIEWS");
|
||
var shot;
|
||
var previewComp;
|
||
var slateLayer;
|
||
var frameCount;
|
||
var essentialProperties;
|
||
var shotLayer;
|
||
|
||
if (!templateComp || !apiData || !apiData.shot) {
|
||
return null;
|
||
}
|
||
|
||
shot = apiData.shot;
|
||
previewComp = null;
|
||
|
||
try {
|
||
previewComp = templateComp.duplicate();
|
||
previewComp.name = getPreviewCompName(shotCode);
|
||
|
||
if (previewsFolder) {
|
||
previewComp.parentFolder = previewsFolder;
|
||
}
|
||
|
||
previewComp.layer("SHOT").replaceSource(shotComp, false);
|
||
previewComp.layer("THUMBNAIL").replaceSource(shotComp, false);
|
||
|
||
slateLayer = previewComp.layer("NETFLIX_SLATE");
|
||
frameCount = Math.round(shotComp.duration * shotComp.frameRate);
|
||
essentialProperties = slateLayer.property("Essential Properties");
|
||
|
||
essentialProperties.property(3).setValue(shotCode + "_cmp_TT_" + (shot.shotVersion || "v001"));
|
||
setEssentialProperty(slateLayer, "Date", getDateString());
|
||
setEssentialProperty(slateLayer, "Desc", shot.description);
|
||
setEssentialProperty(slateLayer, "Notes", shot.notes);
|
||
essentialProperties.property(10).setValue(shotCode);
|
||
setEssentialProperty(slateLayer, "Episode", shot.episode);
|
||
setEssentialProperty(slateLayer, "Scene", shot.scene);
|
||
setEssentialProperty(slateLayer, "Frames", frameCount);
|
||
|
||
shotLayer = previewComp.layer("SHOT");
|
||
var targetOutPoint = shotLayer.inPoint + shotComp.duration;
|
||
if (targetOutPoint > previewComp.duration) {
|
||
previewComp.duration = targetOutPoint;
|
||
}
|
||
shotLayer.outPoint = targetOutPoint;
|
||
previewComp.duration = targetOutPoint;
|
||
|
||
return previewComp;
|
||
} catch (previewError) {
|
||
try {
|
||
if (previewComp) {
|
||
previewComp.remove();
|
||
}
|
||
} catch (removePreviewError) {
|
||
}
|
||
return null;
|
||
}
|
||
}
|
||
|
||
function buildShotFromCode(episodeCode, shotCode) {
|
||
var existingShotComp = findComp(shotCode);
|
||
var sequenceFolders;
|
||
var footageItems = [];
|
||
var footageFolder = findFolder("_FOOTAGE_4K");
|
||
var precompFolder = findFolder("_PRECOMPS");
|
||
var episodeProjectFolder = findEpisodeProjectFolder(episodeCode);
|
||
var apiData;
|
||
var footage;
|
||
var footageComp;
|
||
var shotComp;
|
||
var footageLayer;
|
||
var showLut;
|
||
var lutLayer;
|
||
var maxDuration = 0;
|
||
var width = 1920;
|
||
var height = 1080;
|
||
var primaryFootage = null;
|
||
var startInfo;
|
||
var i;
|
||
|
||
apiData = getShotData(shotCode);
|
||
if (!apiData || !apiData.shot) {
|
||
return null;
|
||
}
|
||
|
||
if (existingShotComp) {
|
||
updateOverlayForComp(existingShotComp, shotCode, apiData);
|
||
return existingShotComp;
|
||
}
|
||
|
||
sequenceFolders = findSequenceFolders(shotCode, episodeCode);
|
||
if (sequenceFolders.length < 1) {
|
||
return null;
|
||
}
|
||
|
||
for (i = 0; i < sequenceFolders.length; i += 1) {
|
||
footage = importEXRSequence(sequenceFolders[i]);
|
||
if (footage) {
|
||
footage.name = sequenceFolders[i].name;
|
||
if (footageFolder) {
|
||
footage.parentFolder = footageFolder;
|
||
}
|
||
footageItems.push(footage);
|
||
|
||
if (footage.duration > maxDuration) {
|
||
maxDuration = footage.duration;
|
||
width = footage.width;
|
||
height = footage.height;
|
||
primaryFootage = footage;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (footageItems.length < 1) {
|
||
return null;
|
||
}
|
||
if (!primaryFootage) {
|
||
primaryFootage = footageItems[0];
|
||
}
|
||
|
||
// Both comps start at the plate's native start timecode (as AE would
|
||
// when creating a comp from the footage), not at frame 0.
|
||
startInfo = getFootageStartFrames(primaryFootage, apiData.shot, 24);
|
||
buildState.lastStartSource = startInfo.source;
|
||
buildState.lastStartFrames = startInfo.frames;
|
||
|
||
footageComp = app.project.items.addComp(shotCode + "_FOOTAGE", width, height, 1, maxDuration, 24);
|
||
setCompStartFrame(footageComp, startInfo.frames);
|
||
if (precompFolder) {
|
||
footageComp.parentFolder = precompFolder;
|
||
}
|
||
|
||
for (i = footageItems.length - 1; i >= 0; i -= 1) {
|
||
footageLayer = footageComp.layers.add(footageItems[i]);
|
||
addOCIOToLayer(footageLayer);
|
||
}
|
||
|
||
shotComp = app.project.items.addComp(shotCode, width, height, 1, maxDuration, 24);
|
||
setCompStartFrame(shotComp, startInfo.frames);
|
||
if (episodeProjectFolder) {
|
||
shotComp.parentFolder = episodeProjectFolder;
|
||
}
|
||
|
||
shotComp.layers.add(footageComp);
|
||
|
||
showLut = findComp("_SHOW LUT");
|
||
if (showLut) {
|
||
lutLayer = shotComp.layers.add(showLut);
|
||
lutLayer.collapseTransformation = true;
|
||
}
|
||
|
||
updateOverlayForComp(shotComp, shotCode, apiData);
|
||
return shotComp;
|
||
}
|
||
|
||
function buildShotFromDropdown() {
|
||
var episodeCode;
|
||
var shotCode;
|
||
var shotComp;
|
||
|
||
if (!uiState.episodeDropdown || !uiState.episodeDropdown.selection || !uiState.shotDropdown || !uiState.shotDropdown.selection) {
|
||
updateStatus(getSelectedComps().length, "Choose an episode and shot first.");
|
||
return;
|
||
}
|
||
|
||
episodeCode = uiState.episodeDropdown.selection.episodeCode || deriveEpisodeCode(uiState.episodeDropdown.selection.text);
|
||
shotCode = uiState.shotDropdown.selection.shotCode || uiState.shotDropdown.selection.text;
|
||
|
||
app.beginUndoGroup("VFXReview Connector - Build Shot");
|
||
updateStatus(getSelectedComps().length, "Building shot " + shotCode);
|
||
shotComp = buildShotFromCode(episodeCode, shotCode);
|
||
app.endUndoGroup();
|
||
|
||
if (shotComp) {
|
||
updateStatus(getSelectedComps().length, "Shot built: " + shotCode +
|
||
" (start frame " + buildState.lastStartFrames +
|
||
(buildState.lastStartSource ? " from " + buildState.lastStartSource : "") + ")");
|
||
} else {
|
||
updateStatus(getSelectedComps().length, "Shot build failed: " + shotCode);
|
||
}
|
||
}
|
||
|
||
function buildPreviews() {
|
||
var comps = getTargetComps();
|
||
var templateComp = findComp("UNG_EXPORT_TEMPLATE");
|
||
var created = 0;
|
||
var skipped = 0;
|
||
var i;
|
||
var shotComp;
|
||
var shotCode;
|
||
var apiData;
|
||
var previewComp;
|
||
|
||
if (comps.length < 1) {
|
||
updateStatus(0, "Please select/open a shot comp.");
|
||
return;
|
||
}
|
||
|
||
if (!templateComp) {
|
||
updateStatus(comps.length, "UNG_EXPORT_TEMPLATE not found.");
|
||
return;
|
||
}
|
||
|
||
app.beginUndoGroup("VFXReview Connector - Build Preview");
|
||
for (i = 0; i < comps.length; i += 1) {
|
||
shotCode = getShotCodeFromComp(comps[i]);
|
||
shotComp = getMainCompForSelection(comps[i]);
|
||
updateStatus(comps.length, "Building preview for " + shotCode + " (" + (i + 1) + " of " + comps.length + ")");
|
||
|
||
if (!shotComp) {
|
||
skipped += 1;
|
||
continue;
|
||
}
|
||
|
||
apiData = getShotData(shotCode);
|
||
if (!apiData || !apiData.shot) {
|
||
skipped += 1;
|
||
continue;
|
||
}
|
||
|
||
previewComp = buildPreviewForShot(shotComp, shotCode, apiData);
|
||
if (previewComp) {
|
||
created += 1;
|
||
} else {
|
||
skipped += 1;
|
||
}
|
||
}
|
||
app.endUndoGroup();
|
||
|
||
updateStatus(comps.length, created + " preview(s) built. " + skipped + " skipped.");
|
||
}
|
||
|
||
function queueReview(templateName, extension, label) {
|
||
var comps = getTargetComps();
|
||
var queued = 0;
|
||
var i;
|
||
var result = makeBatchResult(comps.length);
|
||
var shotCode;
|
||
var apiData;
|
||
var mainComp;
|
||
var previewComp;
|
||
var outputPath;
|
||
|
||
app.beginUndoGroup("VFXReview Connector - Queue " + label);
|
||
if (!ensureFolder(EXPORT_ROOT)) {
|
||
app.endUndoGroup();
|
||
updateStatus(comps.length, "Could not create export folder.");
|
||
return;
|
||
}
|
||
|
||
for (i = 0; i < comps.length; i += 1) {
|
||
shotCode = getShotCodeFromComp(comps[i]);
|
||
updateStatus(comps.length, "Preparing " + label + " preview for " + shotCode + " (" + (i + 1) + " of " + comps.length + ")");
|
||
apiData = getShotData(shotCode);
|
||
if (!apiData || !apiData.shot) {
|
||
addFailure(result, comps[i].name, "Shot lookup failed");
|
||
continue;
|
||
}
|
||
|
||
mainComp = getMainCompForSelection(comps[i]);
|
||
if (!mainComp) {
|
||
addFailure(result, comps[i].name, "Main shot comp not found");
|
||
continue;
|
||
}
|
||
|
||
updateOverlayForComp(mainComp, shotCode, apiData);
|
||
|
||
if (isPreviewComp(comps[i])) {
|
||
previewComp = comps[i];
|
||
} else {
|
||
previewComp = getPreviewCompForShot(shotCode);
|
||
if (!previewComp) {
|
||
previewComp = buildPreviewForShot(mainComp, shotCode, apiData);
|
||
}
|
||
}
|
||
|
||
if (!previewComp) {
|
||
addFailure(result, comps[i].name, "Preview comp not found or could not be created");
|
||
continue;
|
||
}
|
||
|
||
updateStatus(comps.length, "Queueing " + label + " for " + shotCode + " (" + (i + 1) + " of " + comps.length + ")");
|
||
outputPath = EXPORT_ROOT + "/" + shotCode + "_cmp_TT_" + (apiData.shot.shotVersion || "v001") + "." + extension;
|
||
if (queueCompWithOutput(previewComp, templateName, outputPath)) {
|
||
queued += 1;
|
||
result.queued += 1;
|
||
} else {
|
||
addFailure(result, comps[i].name, "Could not queue " + label + " render");
|
||
}
|
||
}
|
||
app.endUndoGroup();
|
||
|
||
updateStatus(comps.length, queued + " " + label + " renders queued. " + result.skipped + " skipped.");
|
||
}
|
||
|
||
function queueEXRPreview() {
|
||
var comps = getSelectedComps();
|
||
var queued = 0;
|
||
var i;
|
||
var comp;
|
||
var lookupCode;
|
||
var shot;
|
||
var result = makeBatchResult(comps.length);
|
||
var shotCode;
|
||
var folderPath;
|
||
var outputPath;
|
||
|
||
app.beginUndoGroup("VFXReview Connector - Queue EXR Preview");
|
||
for (i = 0; i < comps.length; i += 1) {
|
||
comp = comps[i];
|
||
lookupCode = getShotCodeFromComp(comp);
|
||
updateStatus(comps.length, "Looking up " + lookupCode + " (" + (i + 1) + " of " + comps.length + ")");
|
||
shot = normalizeShotData(getShotData(lookupCode));
|
||
|
||
if (!shot || !shot.shotCode) {
|
||
addFailure(result, comp.name, "API lookup failed");
|
||
continue;
|
||
}
|
||
|
||
shotCode = shot.shotCode;
|
||
disableLayerByName(comp, "UNG_VFX_OVERLAY");
|
||
disableLayerByName(comp, "_SHOW LUT");
|
||
|
||
folderPath = EXPORT_ROOT + "/" + shotCode;
|
||
if (!ensureFolder(folderPath)) {
|
||
addFailure(result, comp.name, "Could not create output folder");
|
||
continue;
|
||
}
|
||
|
||
outputPath = folderPath + "/" + shotCode + "_cmp_TT_" + (shot.shotVersion || "v001") + ".[#####].exr";
|
||
if (queueCompWithOutput(comp, "EXR Sequence", outputPath)) {
|
||
queued += 1;
|
||
result.queued += 1;
|
||
} else {
|
||
addFailure(result, comp.name, "Could not queue EXR render");
|
||
}
|
||
}
|
||
app.endUndoGroup();
|
||
|
||
updateStatus(comps.length, queued + " EXR renders queued. " + result.skipped + " skipped.");
|
||
}
|
||
|
||
function queueMP4() {
|
||
queueReview("REVIEW_PREVIEW", "mp4", "MP4");
|
||
}
|
||
|
||
function queueMOV() {
|
||
queueReview("4444 Tri", "mov", "MOV");
|
||
}
|
||
|
||
function importExportedEXR() {
|
||
var comps = getTargetComps();
|
||
var exportsProjectFolder;
|
||
var comp;
|
||
var shotCode;
|
||
var shot;
|
||
var exportFolder;
|
||
var files;
|
||
var importOptions;
|
||
var footage;
|
||
var layer;
|
||
var i;
|
||
var imported = 0;
|
||
var skipped = 0;
|
||
|
||
if (comps.length < 1) {
|
||
updateStatus(0, "Please select or open a comp first.");
|
||
return;
|
||
}
|
||
|
||
app.beginUndoGroup("VFXReview Connector - Import Exported EXR");
|
||
|
||
for (i = 0; i < comps.length; i += 1) {
|
||
comp = comps[i];
|
||
shotCode = getShotCodeFromComp(comp);
|
||
updateStatus(comps.length, "Looking up " + shotCode + " (" + (i + 1) + " of " + comps.length + ")");
|
||
|
||
shot = normalizeShotData(getShotData(shotCode));
|
||
if (!shot || !shot.shotCode || !shot.exrOutput) {
|
||
updateStatus(comps.length, "API lookup failed or missing EXR output for " + shotCode);
|
||
skipped += 1;
|
||
continue;
|
||
}
|
||
|
||
exportFolder = new Folder(EXPORT_ROOT + "/" + shot.shotCode);
|
||
if (!exportFolder.exists) {
|
||
updateStatus(comps.length, "Export folder not found: " + exportFolder.fsName);
|
||
skipped += 1;
|
||
continue;
|
||
}
|
||
|
||
// Prefer the current review-render version naming convention:
|
||
// shotCode_cmp_TT_vXXX.####.exr
|
||
var currentVersion = shot.shotVersion || "v001";
|
||
var reviewPrefix = shot.shotCode + "_cmp_TT_" + currentVersion + ".";
|
||
files = exportFolder.getFiles(function (f) {
|
||
return f instanceof File &&
|
||
/\.exr$/i.test(f.name) &&
|
||
f.name.indexOf(reviewPrefix) === 0;
|
||
});
|
||
|
||
// Fallback to the broader queue EXR naming pattern if needed
|
||
if (!files || files.length < 1) {
|
||
var cmpPrefix = shot.shotCode + "_cmp_TT_";
|
||
files = exportFolder.getFiles(function (f) {
|
||
return f instanceof File &&
|
||
/\.exr$/i.test(f.name) &&
|
||
f.name.indexOf(cmpPrefix) === 0;
|
||
});
|
||
}
|
||
|
||
if (!files || files.length < 1) {
|
||
// Last resort: any EXR in the folder
|
||
files = exportFolder.getFiles("*.exr");
|
||
}
|
||
|
||
if (!files || files.length < 1) {
|
||
updateStatus(comps.length, "No EXR files found in " + exportFolder.fsName);
|
||
skipped += 1;
|
||
continue;
|
||
}
|
||
|
||
files.sort();
|
||
try {
|
||
importOptions = new ImportOptions(files[0]);
|
||
importOptions.sequence = true;
|
||
footage = app.project.importFile(importOptions);
|
||
footage.mainSource.conformFrameRate = 24;
|
||
} catch (importErr) {
|
||
updateStatus(comps.length, "Import failed for " + shot.shotCode + ": " + importErr.message);
|
||
skipped += 1;
|
||
continue;
|
||
}
|
||
|
||
exportsProjectFolder = findOrCreateFolder("_EXPORTS", null);
|
||
if (exportsProjectFolder) {
|
||
footage.parentFolder = exportsProjectFolder;
|
||
}
|
||
|
||
layer = comp.layers.add(footage);
|
||
layer.moveToBeginning();
|
||
imported += 1;
|
||
}
|
||
|
||
app.endUndoGroup();
|
||
|
||
updateStatus(comps.length, imported + " EXR export(s) imported. " + skipped + " skipped.");
|
||
}
|
||
|
||
function importRenders() {
|
||
var comps = getTargetComps();
|
||
var comp;
|
||
var shotCode;
|
||
var rendersRoot;
|
||
var rendersProjectFolder;
|
||
var allFiles;
|
||
var subFolders;
|
||
var importedFootage = [];
|
||
var addedLayers = [];
|
||
var layerIndices = [];
|
||
var footage;
|
||
var layer;
|
||
var i;
|
||
|
||
if (comps.length < 1) {
|
||
updateStatus(0, "Please select or open a comp first.");
|
||
return;
|
||
}
|
||
|
||
comp = comps[0];
|
||
shotCode = getShotCodeFromComp(comp);
|
||
rendersRoot = new Folder("X:/shared_projects_2026/UNGO_VFX/production/renders/" + shotCode);
|
||
|
||
if (!rendersRoot.exists) {
|
||
updateStatus(getSelectedComps().length, "Renders folder not found: " + rendersRoot.fsName);
|
||
return;
|
||
}
|
||
|
||
rendersProjectFolder = findOrCreateFolder("_RENDERS", null);
|
||
|
||
app.beginUndoGroup("VFXReview Connector - Import Renders");
|
||
|
||
// Try subfolders first (each subfolder may be a separate pass/layer)
|
||
subFolders = rendersRoot.getFiles();
|
||
for (i = 0; i < subFolders.length; i += 1) {
|
||
if (subFolders[i] instanceof Folder) {
|
||
footage = importEXRSequence(subFolders[i]);
|
||
if (footage) {
|
||
footage.name = subFolders[i].name;
|
||
footage.mainSource.conformFrameRate = 24;
|
||
if (rendersProjectFolder) {
|
||
footage.parentFolder = rendersProjectFolder;
|
||
}
|
||
importedFootage.push(footage);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Fall back to sequences directly in the root renders folder
|
||
if (importedFootage.length < 1) {
|
||
footage = importEXRSequence(rendersRoot);
|
||
if (footage) {
|
||
footage.name = shotCode + "_render";
|
||
footage.mainSource.conformFrameRate = 24;
|
||
if (rendersProjectFolder) {
|
||
footage.parentFolder = rendersProjectFolder;
|
||
}
|
||
importedFootage.push(footage);
|
||
}
|
||
}
|
||
|
||
if (importedFootage.length < 1) {
|
||
app.endUndoGroup();
|
||
updateStatus(getSelectedComps().length, "No EXR sequences found in renders folder.");
|
||
return;
|
||
}
|
||
|
||
// Add footage layers to comp and apply Extractor effect
|
||
for (i = 0; i < importedFootage.length; i += 1) {
|
||
layer = comp.layers.add(importedFootage[i]);
|
||
try {
|
||
var extractor = layer.property("Effects").addProperty("Extractor");
|
||
try {
|
||
extractor.property("Layers").setValue(1);
|
||
} catch (layersPropError) {
|
||
// Property name may differ — try by index (property 1 is typically "Layers")
|
||
try {
|
||
extractor.property(1).setValue(1);
|
||
} catch (layersIdxError) {
|
||
}
|
||
}
|
||
} catch (fxError) {
|
||
}
|
||
addedLayers.push(layer);
|
||
}
|
||
|
||
// Collect current layer indices for pre-compose
|
||
for (i = 0; i < addedLayers.length; i += 1) {
|
||
layerIndices.push(addedLayers[i].index);
|
||
}
|
||
|
||
// Pre-compose all render layers into {shotCode}_RENDER
|
||
try {
|
||
comp.layers.precompose(layerIndices, shotCode + "_RENDER", true);
|
||
updateStatus(getSelectedComps().length, importedFootage.length + " render(s) imported into " + shotCode + "_RENDER.");
|
||
} catch (precompError) {
|
||
updateStatus(getSelectedComps().length, importedFootage.length + " render(s) imported. Pre-compose failed: " + precompError.message);
|
||
}
|
||
|
||
app.endUndoGroup();
|
||
}
|
||
|
||
function prepDelivery() {
|
||
var comps = getTargetComps();
|
||
var i;
|
||
var j;
|
||
var comp;
|
||
var shotCode;
|
||
var shot;
|
||
var exrOutputBase;
|
||
var sourceFolder;
|
||
var deliveryFolderPath;
|
||
var exrFiles;
|
||
var expectedBaseName;
|
||
var result = makeBatchResult(comps.length);
|
||
var srcFile;
|
||
var srcBaseName;
|
||
var frameMatch;
|
||
var frameNum;
|
||
var destFileName;
|
||
var destPath;
|
||
var psLines;
|
||
var scriptFile;
|
||
var vbsFile;
|
||
var launched = 0;
|
||
|
||
if (comps.length < 1) {
|
||
updateStatus(0, "Please select or open a comp first.");
|
||
return;
|
||
}
|
||
|
||
for (i = 0; i < comps.length; i += 1) {
|
||
comp = comps[i];
|
||
shotCode = getShotCodeFromComp(comp);
|
||
updateStatus(comps.length, "Preparing delivery for " + shotCode + " (" + (i + 1) + " of " + comps.length + ")");
|
||
|
||
shot = normalizeShotData(getShotData(shotCode));
|
||
if (!shot || !shot.shotCode) {
|
||
addFailure(result, comp.name, "API lookup failed");
|
||
continue;
|
||
}
|
||
|
||
shotCode = shot.shotCode;
|
||
expectedBaseName = shotCode + "_cmp_TT_" + (shot.shotVersion || "v001");
|
||
exrOutputBase = shot.exrOutput || "";
|
||
|
||
sourceFolder = new Folder(EXPORT_ROOT + "/" + shotCode);
|
||
if (!sourceFolder.exists) {
|
||
addFailure(result, comp.name, "Source export folder not found: " + sourceFolder.fsName);
|
||
continue;
|
||
}
|
||
|
||
// Prefer the current review-render version naming convention:
|
||
// shotCode_cmp_TT_vXXX.####.exr
|
||
exrFiles = null;
|
||
var currentVersion = shot.shotVersion || "v001";
|
||
var reviewPrefix = shotCode + "_cmp_TT_" + currentVersion + ".";
|
||
exrFiles = sourceFolder.getFiles(function (f) {
|
||
return f instanceof File &&
|
||
/\.exr$/i.test(f.name) &&
|
||
f.name.indexOf(reviewPrefix) === 0;
|
||
});
|
||
|
||
// Fallback to the broader queue EXR naming pattern if needed
|
||
if (!exrFiles || exrFiles.length < 1) {
|
||
var cmpPfx = shotCode + "_cmp_TT_";
|
||
exrFiles = sourceFolder.getFiles(function (f) {
|
||
return f instanceof File &&
|
||
/\.exr$/i.test(f.name) &&
|
||
f.name.indexOf(cmpPfx) === 0;
|
||
});
|
||
}
|
||
|
||
if (!exrFiles || exrFiles.length < 1) {
|
||
// Last resort: any EXR in the folder
|
||
exrFiles = sourceFolder.getFiles("*.exr");
|
||
}
|
||
|
||
if (!exrFiles || exrFiles.length < 1) {
|
||
addFailure(result, comp.name, "No EXR files found in " + sourceFolder.fsName);
|
||
continue;
|
||
}
|
||
|
||
exrFiles.sort();
|
||
|
||
var episodeCode = shotCode.substring(0, 7);
|
||
var deliveryDate = getDateString().replace(/\//g, "");
|
||
deliveryFolderPath = EXPORT_ROOT + "/DELIVERY/" + episodeCode + "/" + deliveryDate + "_Delivery/" + shotCode;
|
||
if (!ensureFolder(deliveryFolderPath)) {
|
||
addFailure(result, comp.name, "Could not create delivery folder");
|
||
continue;
|
||
}
|
||
|
||
// Build a list of PowerShell Copy-Item commands
|
||
psLines = [];
|
||
for (j = 0; j < exrFiles.length; j += 1) {
|
||
srcFile = exrFiles[j];
|
||
srcBaseName = srcFile.name.replace(/\.exr$/i, "");
|
||
|
||
if (srcBaseName.indexOf(expectedBaseName + ".") === 0) {
|
||
destFileName = srcFile.name;
|
||
} else {
|
||
// Extract frame number and rename to delivery convention
|
||
frameMatch = srcFile.name.match(/\.(\d+)\.exr$/i);
|
||
if (!frameMatch) {
|
||
continue;
|
||
}
|
||
frameNum = frameMatch[1];
|
||
destFileName = expectedBaseName + "." + frameNum + ".exr";
|
||
}
|
||
|
||
destPath = (new File(deliveryFolderPath + "/" + destFileName)).fsName;
|
||
// Single-quoted PS strings are fully literal — safe for Windows paths
|
||
psLines.push("Copy-Item -LiteralPath '" + srcFile.fsName + "' -Destination '" + destPath + "'");
|
||
}
|
||
|
||
if (psLines.length < 1) {
|
||
addFailure(result, comp.name, "No valid EXR files to copy");
|
||
continue;
|
||
}
|
||
|
||
// Write PS1 script to temp folder
|
||
scriptFile = new File(Folder.temp.fsName + "/vfxdelivery_" + shotCode + ".ps1");
|
||
try {
|
||
scriptFile.open("w");
|
||
scriptFile.write(psLines.join("\r\n"));
|
||
// Self-delete once all transfers finish
|
||
scriptFile.write("\r\nRemove-Item -LiteralPath '" + scriptFile.fsName + "' -Force -ErrorAction SilentlyContinue");
|
||
scriptFile.close();
|
||
} catch (writeError) {
|
||
addFailure(result, comp.name, "Could not write delivery script: " + writeError.message);
|
||
continue;
|
||
}
|
||
|
||
// VBScript launcher: WScript.Shell.Run with bWaitOnReturn=False creates a fully
|
||
// independent process with no inherited pipe handles, so system.callSystem returns
|
||
// immediately and AE is never blocked regardless of how long the copy takes.
|
||
vbsFile = new File(Folder.temp.fsName + "/vfxdelivery_launch_" + shotCode + ".vbs");
|
||
try {
|
||
vbsFile.open("w");
|
||
vbsFile.write("Set sh = CreateObject(\"WScript.Shell\")\r\n");
|
||
vbsFile.write("sh.Run \"powershell -WindowStyle Hidden -NoProfile -ExecutionPolicy Bypass -File \" & Chr(34) & \"" + scriptFile.fsName + "\" & Chr(34), 0, False\r\n");
|
||
vbsFile.write("Set fso = CreateObject(\"Scripting.FileSystemObject\")\r\n");
|
||
vbsFile.write("fso.DeleteFile WScript.ScriptFullName\r\n");
|
||
vbsFile.close();
|
||
} catch (vbsError) {
|
||
addFailure(result, comp.name, "Could not write launcher script: " + vbsError.message);
|
||
continue;
|
||
}
|
||
|
||
system.callSystem("wscript.exe /nologo \"" + vbsFile.fsName + "\"");
|
||
launched += 1;
|
||
}
|
||
|
||
var statusMsg;
|
||
if (launched > 0) {
|
||
statusMsg = "Delivery transfer running in background for " + launched + " shot(s).";
|
||
} else {
|
||
statusMsg = "No shots queued for delivery.";
|
||
}
|
||
if (result.skipped > 0) {
|
||
statusMsg += " " + result.skipped + " skipped.";
|
||
}
|
||
updateStatus(comps.length, statusMsg);
|
||
}
|
||
|
||
function pullPictureLock() {
|
||
var comps = getTargetComps();
|
||
var comp;
|
||
var shotCode;
|
||
var episodePrefix;
|
||
var piclocksProjectFolder;
|
||
var piclockFootage;
|
||
var piclocksDir;
|
||
var files;
|
||
var foundFile;
|
||
var importOptions;
|
||
var apiData;
|
||
var shot;
|
||
var fps;
|
||
var handles;
|
||
var piclockStartTC;
|
||
var tcIn;
|
||
var tcOut;
|
||
var inOffset;
|
||
var outOffset;
|
||
var layer;
|
||
var i;
|
||
var item;
|
||
|
||
if (comps.length < 1) {
|
||
updateStatus(0, "Please select or open a comp first.");
|
||
return;
|
||
}
|
||
|
||
comp = comps[0];
|
||
shotCode = getShotCodeFromComp(comp);
|
||
episodePrefix = shotCode.substring(0, 7); // e.g. "UNG_106"
|
||
|
||
// Fetch API data for seq timecodes
|
||
updateStatus(getSelectedComps().length, "Looking up " + shotCode + "...");
|
||
apiData = getShotData(shotCode);
|
||
if (!apiData || !apiData.shot) {
|
||
updateStatus(getSelectedComps().length, "API lookup failed for " + shotCode);
|
||
return;
|
||
}
|
||
|
||
shot = apiData.shot;
|
||
if (!shot.seqTimecodeStart || !shot.seqTimecodeEnd) {
|
||
updateStatus(getSelectedComps().length, "No sequence timecodes for " + shotCode + ". Import picture tracker CSV first.");
|
||
return;
|
||
}
|
||
|
||
// Find or create _PICLOCKS project folder
|
||
piclocksProjectFolder = findOrCreateFolder("_PICLOCKS", null);
|
||
piclockFootage = null;
|
||
|
||
// Search only inside the _PICLOCKS project folder for matching footage
|
||
if (piclocksProjectFolder) {
|
||
for (i = 1; i <= app.project.numItems; i += 1) {
|
||
item = app.project.item(i);
|
||
if (item instanceof FootageItem &&
|
||
item.parentFolder === piclocksProjectFolder &&
|
||
item.name.substring(0, 7) === episodePrefix) {
|
||
piclockFootage = item;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Not in project — import from disk
|
||
if (!piclockFootage) {
|
||
piclocksDir = new Folder(FOOTAGE_ROOT + "/_PICLOCKS");
|
||
if (!piclocksDir.exists) {
|
||
updateStatus(getSelectedComps().length, "PICLOCKS folder not found: " + piclocksDir.fsName);
|
||
return;
|
||
}
|
||
|
||
files = piclocksDir.getFiles(episodePrefix + "_*");
|
||
foundFile = null;
|
||
if (files && files.length > 0) {
|
||
for (i = 0; i < files.length; i += 1) {
|
||
if (files[i] instanceof File) {
|
||
foundFile = files[i];
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!foundFile) {
|
||
updateStatus(getSelectedComps().length, "No piclock file matching " + episodePrefix + "_* found in " + piclocksDir.fsName);
|
||
return;
|
||
}
|
||
|
||
try {
|
||
importOptions = new ImportOptions(foundFile);
|
||
piclockFootage = app.project.importFile(importOptions);
|
||
if (piclocksProjectFolder) {
|
||
piclockFootage.parentFolder = piclocksProjectFolder;
|
||
}
|
||
} catch (importError) {
|
||
updateStatus(getSelectedComps().length, "Import failed: " + importError.message);
|
||
return;
|
||
}
|
||
}
|
||
|
||
// Calculate layer in/out offsets from seqTimecodes + handles
|
||
fps = comp.frameRate || 24;
|
||
handles = 8;
|
||
|
||
piclockStartTC = 3590;
|
||
try {
|
||
if (piclockFootage.mainSource && typeof piclockFootage.mainSource.startTimecode === "number") {
|
||
piclockStartTC = piclockFootage.mainSource.startTimecode;
|
||
}
|
||
} catch (tcReadError) {
|
||
}
|
||
|
||
tcIn = parseTimecodeToSeconds(shot.seqTimecodeStart, fps);
|
||
tcOut = parseTimecodeToSeconds(shot.seqTimecodeEnd, fps);
|
||
inOffset = tcIn - piclockStartTC - (handles / fps);
|
||
outOffset = tcOut - piclockStartTC + (handles / fps);
|
||
|
||
if (inOffset < 0) {
|
||
inOffset = 0;
|
||
}
|
||
if (outOffset > piclockFootage.duration) {
|
||
outOffset = piclockFootage.duration;
|
||
}
|
||
|
||
app.beginUndoGroup("VFXReview Connector - Pull Picture Lock");
|
||
|
||
layer = comp.layers.add(piclockFootage);
|
||
var scale =
|
||
Math.max(
|
||
comp.width / piclockFootage.width,
|
||
comp.height / piclockFootage.height
|
||
) * 100;
|
||
|
||
layer.property("Scale")
|
||
.setValue([
|
||
scale,
|
||
scale
|
||
]);
|
||
layer.moveToBeginning();
|
||
|
||
// Offset startTime so the correct footage frame plays at comp time 0
|
||
layer.startTime = -inOffset;
|
||
layer.inPoint = 0;
|
||
layer.outPoint = outOffset - inOffset;
|
||
|
||
app.endUndoGroup();
|
||
|
||
updateStatus(
|
||
getSelectedComps().length,
|
||
"Piclock added: " + shot.seqTimecodeStart + " \u2192 " + shot.seqTimecodeEnd + " (+" + handles + "fr handles)."
|
||
);
|
||
}
|
||
|
||
function applyOCIOColorSpace() {
|
||
var comps = getTargetComps();
|
||
var applied = 0;
|
||
var skipped = 0;
|
||
var i;
|
||
var j;
|
||
var comp;
|
||
var layer;
|
||
|
||
if (comps.length < 1) {
|
||
updateStatus(0, "Please select or open a comp first.");
|
||
return;
|
||
}
|
||
|
||
app.beginUndoGroup("VFXReview Connector - Apply OCIO");
|
||
for (i = 0; i < comps.length; i += 1) {
|
||
comp = comps[i];
|
||
for (j = 1; j <= comp.numLayers; j += 1) {
|
||
try {
|
||
layer = comp.layer(j);
|
||
// Only apply to footage layers (not comps, solids, etc.)
|
||
if (layer.source && layer.source instanceof FootageItem && layer.source.mainSource instanceof FileSource) {
|
||
addOCIOToLayer(layer);
|
||
applied += 1;
|
||
}
|
||
} catch (layerError) {
|
||
skipped += 1;
|
||
}
|
||
}
|
||
}
|
||
app.endUndoGroup();
|
||
|
||
updateStatus(comps.length, "OCIO applied to " + applied + " layer(s). " + skipped + " skipped.");
|
||
}
|
||
|
||
function findOrCreateFolder(name, parentFolder) {
|
||
var i;
|
||
var item;
|
||
|
||
if (!app.project) {
|
||
return null;
|
||
}
|
||
|
||
for (i = 1; i <= app.project.numItems; i += 1) {
|
||
item = app.project.item(i);
|
||
if (item instanceof FolderItem && item.name === name) {
|
||
if (!parentFolder || item.parentFolder === parentFolder) {
|
||
return item;
|
||
}
|
||
}
|
||
}
|
||
|
||
var newFolder = app.project.items.addFolder(name);
|
||
if (parentFolder) {
|
||
newFolder.parentFolder = parentFolder;
|
||
}
|
||
return newFolder;
|
||
}
|
||
|
||
function initialiseWorkspace() {
|
||
var topFolderNames = ["_PREVIEWS", "_PRECOMPS", "_PICLOCKS", "_FOOTAGE_4K", "_STOCK", "_RENDERS", "_UTILITIES"];
|
||
var templatePath = "X:/shared_projects_2026/UNGO_VFX/production/working_files/UNG_VFXOVERLAY_SLATE_TEMPLATE.aep";
|
||
var shotsFolder;
|
||
var epNum;
|
||
var epCode;
|
||
var templateFile;
|
||
var importOptions;
|
||
var importStatus;
|
||
var i;
|
||
|
||
if (!app.project) {
|
||
updateStatus(0, "No project open.");
|
||
return;
|
||
}
|
||
|
||
app.beginUndoGroup("VFXReview Connector - Initialise Workspace");
|
||
|
||
// Create __SHOTS with UNG_101 – UNG_117 subfolders
|
||
shotsFolder = findOrCreateFolder("__SHOTS", null);
|
||
for (epNum = 101; epNum <= 117; epNum += 1) {
|
||
epCode = "UNG_" + epNum;
|
||
findOrCreateFolder(epCode, shotsFolder);
|
||
}
|
||
|
||
// Create remaining top-level folders
|
||
for (i = 0; i < topFolderNames.length; i += 1) {
|
||
findOrCreateFolder(topFolderNames[i], null);
|
||
}
|
||
|
||
// Import AEP template
|
||
importStatus = "";
|
||
templateFile = new File(templatePath);
|
||
if (templateFile.exists) {
|
||
try {
|
||
importOptions = new ImportOptions(templateFile);
|
||
importOptions.importAs = ImportAsType.PROJECT;
|
||
app.project.importFile(importOptions);
|
||
importStatus = " Template imported.";
|
||
} catch (importError) {
|
||
importStatus = " Template import failed: " + importError.message;
|
||
}
|
||
} else {
|
||
importStatus = " Template file not found.";
|
||
}
|
||
|
||
app.endUndoGroup();
|
||
|
||
// ── Color settings (outside undo group — project-level settings) ──────
|
||
var colorStatus = "";
|
||
|
||
// 32-bit depth
|
||
try {
|
||
app.project.bitsPerChannel = 32;
|
||
colorStatus += " 32-bit depth set.";
|
||
} catch (bpcError) {
|
||
colorStatus += " Could not set 32-bit depth.";
|
||
}
|
||
|
||
// OCIO color management — AE 2022+ scripting API
|
||
try {
|
||
app.project.colorSettings.enabled = true;
|
||
colorStatus += " Color management enabled.";
|
||
} catch (csError) {
|
||
colorStatus += " Color management API unavailable (set manually).";
|
||
}
|
||
|
||
try {
|
||
// workingSpace index for OCIO ACES 1.2 varies by installation;
|
||
// attempt to match by name first, fall back to direct assignment.
|
||
var cs = app.project.colorSettings;
|
||
var found = false;
|
||
var wi;
|
||
if (cs.getWorkingSpaceList) {
|
||
var spaces = cs.getWorkingSpaceList();
|
||
for (wi = 0; wi < spaces.length; wi += 1) {
|
||
if (String(spaces[wi]).indexOf("ACES") !== -1 && String(spaces[wi]).indexOf("1.2") !== -1) {
|
||
cs.workingSpace = spaces[wi];
|
||
found = true;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
if (!found && cs.workingSpace !== undefined) {
|
||
cs.workingSpace = "ACES - ACES2065-1";
|
||
}
|
||
colorStatus += " OCIO ACES 1.2 applied.";
|
||
} catch (ocioError) {
|
||
colorStatus += " OCIO config must be set manually in Project Settings.";
|
||
}
|
||
|
||
updateStatus(getSelectedComps().length, "Workspace initialised." + importStatus + colorStatus);
|
||
}
|
||
|
||
function clearDropdown(dropdown) {
|
||
while (dropdown.items.length > 0) {
|
||
dropdown.remove(dropdown.items[0]);
|
||
}
|
||
}
|
||
|
||
function populateShotDropdown() {
|
||
var episodeValue;
|
||
var shots;
|
||
var i;
|
||
var shotCode;
|
||
var item;
|
||
|
||
if (!uiState.episodeDropdown || !uiState.episodeDropdown.selection || !uiState.shotDropdown) {
|
||
return;
|
||
}
|
||
|
||
episodeValue = uiState.episodeDropdown.selection.episodeValue || uiState.episodeDropdown.selection.text;
|
||
shots = getShotsFromAPI(episodeValue);
|
||
|
||
clearDropdown(uiState.shotDropdown);
|
||
for (i = 0; i < shots.length; i += 1) {
|
||
shotCode = getShotCodeValue(shots[i]);
|
||
if (shotCode) {
|
||
item = uiState.shotDropdown.add("item", shotCode);
|
||
item.shotCode = shotCode;
|
||
}
|
||
}
|
||
|
||
if (uiState.shotDropdown.items.length > 0) {
|
||
uiState.shotDropdown.selection = 0;
|
||
updateStatus(getSelectedComps().length, uiState.shotDropdown.items.length + " shot(s) found for " + uiState.episodeDropdown.selection.text);
|
||
} else {
|
||
updateStatus(getSelectedComps().length, "No shots found for " + uiState.episodeDropdown.selection.text);
|
||
}
|
||
}
|
||
|
||
function populateEpisodeDropdown() {
|
||
var episodes = getEpisodesFromAPI();
|
||
var i;
|
||
var label;
|
||
var episodeValue;
|
||
var item;
|
||
|
||
if (!uiState.episodeDropdown || !uiState.shotDropdown) {
|
||
return;
|
||
}
|
||
|
||
clearDropdown(uiState.episodeDropdown);
|
||
clearDropdown(uiState.shotDropdown);
|
||
|
||
for (i = 0; i < episodes.length; i += 1) {
|
||
label = getEpisodeLabel(episodes[i]);
|
||
episodeValue = getEpisodeValue(episodes[i]);
|
||
if (label && episodeValue) {
|
||
item = uiState.episodeDropdown.add("item", label);
|
||
item.episodeValue = episodeValue;
|
||
item.episodeCode = deriveEpisodeCode(label);
|
||
if (!/^[A-Z]{3}_\d{3}$/.test(item.episodeCode)) {
|
||
item.episodeCode = deriveEpisodeCode(episodeValue);
|
||
}
|
||
}
|
||
}
|
||
|
||
if (uiState.episodeDropdown.items.length > 0) {
|
||
uiState.episodeDropdown.selection = 0;
|
||
populateShotDropdown();
|
||
} else {
|
||
updateStatus(getSelectedComps().length, "No episodes found from API.");
|
||
}
|
||
}
|
||
|
||
// ── Shot Version Increment ────────────────────────────────────────────────
|
||
|
||
function incrementVersionString(ver) {
|
||
var match = String(ver).match(/^v(\d+)$/);
|
||
var num;
|
||
var str;
|
||
|
||
if (!match) {
|
||
return "v002";
|
||
}
|
||
|
||
num = parseInt(match[1], 10) + 1;
|
||
str = String(num);
|
||
while (str.length < 3) {
|
||
str = "0" + str;
|
||
}
|
||
return "v" + str;
|
||
}
|
||
|
||
function patchShotVersion(shotId, newVersion) {
|
||
var url = BASE_URL + "/api/ext/shots/" + shotId;
|
||
// Inner double-quotes escaped for Windows cmd.exe
|
||
var body = "{\\\"shotVersion\\\":\\\"" + newVersion + "\\\"}";
|
||
var command;
|
||
var response;
|
||
|
||
try {
|
||
command = "curl -s -X PATCH " +
|
||
"-H \"Authorization: Bearer " + TOKEN + "\" " +
|
||
"-H \"Content-Type: application/json\" " +
|
||
"-H \"Accept: application/json\" " +
|
||
"-d \"" + body + "\" " +
|
||
"\"" + url + "\"";
|
||
|
||
response = system.callSystem(command);
|
||
} catch (patchError) {
|
||
return null;
|
||
}
|
||
|
||
return parseJSON(response);
|
||
}
|
||
|
||
function incrementShotVersions() {
|
||
var comps = getTargetComps();
|
||
var i;
|
||
var shotCode;
|
||
var data;
|
||
var shot;
|
||
var newVersion;
|
||
var patchResult;
|
||
var versionName;
|
||
var previewComp;
|
||
var slateLayer;
|
||
var essentialProperties;
|
||
var updated = 0;
|
||
var skipped = 0;
|
||
var lastVersion = "";
|
||
|
||
if (comps.length < 1) {
|
||
updateStatus(0, "Please select or open a comp first.");
|
||
return;
|
||
}
|
||
|
||
app.beginUndoGroup("VFXReview Connector - Increment Shot Version");
|
||
for (i = 0; i < comps.length; i += 1) {
|
||
shotCode = getShotCodeFromComp(comps[i]);
|
||
updateStatus(comps.length, "Incrementing " + shotCode + " (" + (i + 1) + " of " + comps.length + ")");
|
||
|
||
data = getShotData(shotCode);
|
||
if (!data || !data.shot) {
|
||
skipped += 1;
|
||
continue;
|
||
}
|
||
|
||
shot = data.shot;
|
||
newVersion = incrementVersionString(shot.shotVersion || "v001");
|
||
|
||
patchResult = patchShotVersion(shot.id, newVersion);
|
||
if (!patchResult || !patchResult.success) {
|
||
skipped += 1;
|
||
continue;
|
||
}
|
||
|
||
data.shot.shotVersion = newVersion;
|
||
versionName = shotCode + "_cmp_TT_" + newVersion;
|
||
|
||
// 1. Update SHOT NAME on UNG_VFX_OVERLAY inside the selected comp
|
||
updateOverlayForComp(comps[i], shotCode, data);
|
||
|
||
// 2. Update {shotCode}_PREVIEW slate VersionName if the comp exists
|
||
previewComp = findComp(getPreviewCompName(shotCode));
|
||
if (previewComp) {
|
||
try {
|
||
slateLayer = previewComp.layer("NETFLIX_SLATE");
|
||
essentialProperties = slateLayer.property("Essential Properties");
|
||
try {
|
||
essentialProperties.property("VersionName").setValue(versionName);
|
||
} catch (byNameError) {
|
||
essentialProperties.property(3).setValue(versionName);
|
||
}
|
||
} catch (slateError) {
|
||
}
|
||
}
|
||
|
||
lastVersion = newVersion;
|
||
updated += 1;
|
||
}
|
||
app.endUndoGroup();
|
||
|
||
if (updated > 0) {
|
||
updateStatus(comps.length, updated + " shot(s) → " + lastVersion + ". " + skipped + " skipped.");
|
||
} else {
|
||
updateStatus(comps.length, "No shots incremented. " + skipped + " failed.");
|
||
}
|
||
}
|
||
|
||
function setStartFrameFromTimecode() {
|
||
var comps = getTargetComps();
|
||
var i;
|
||
var shotCode;
|
||
var data;
|
||
var shot;
|
||
var fps;
|
||
var frameNumber;
|
||
var updated = 0;
|
||
var skipped = 0;
|
||
|
||
if (comps.length < 1) {
|
||
updateStatus(0, "Please select or open a comp first.");
|
||
return;
|
||
}
|
||
|
||
app.beginUndoGroup("VFXReview Connector - Set Start Timecode");
|
||
for (i = 0; i < comps.length; i += 1) {
|
||
shotCode = getShotCodeFromComp(comps[i]);
|
||
updateStatus(comps.length, "Looking up " + shotCode + " (" + (i + 1) + " of " + comps.length + ")");
|
||
data = getShotData(shotCode);
|
||
|
||
if (!data || !data.shot || !data.shot.timecodeStart) {
|
||
skipped += 1;
|
||
continue;
|
||
}
|
||
|
||
shot = data.shot;
|
||
fps = shot.fps || comps[i].frameRate || 24;
|
||
frameNumber = parseTimecodeToFrames(shot.timecodeStart, fps) - HANDLE_FRAMES;
|
||
setCompStartFrame(comps[i], frameNumber);
|
||
updated += 1;
|
||
}
|
||
app.endUndoGroup();
|
||
|
||
updateStatus(comps.length, updated + " comp(s) start frame set. " + skipped + " skipped.");
|
||
}
|
||
|
||
// ── Render Pipeline (Queue Export → server render queue) ─────────────────
|
||
// Additive: the manual Queue EXR / MP4 / MOV / Prep Delivery buttons above
|
||
// stay as escape hatches. Queue Export submits a Render Manifest to
|
||
// POST /api/ext/exports; the SERVER increments the shot version and
|
||
// decides output paths, so no local render or Increment Version click is
|
||
// needed on this path.
|
||
|
||
function jsonEscapeString(value) {
|
||
var text = String(value);
|
||
var out = "";
|
||
var i;
|
||
var ch;
|
||
var code;
|
||
|
||
for (i = 0; i < text.length; i += 1) {
|
||
ch = text.charAt(i);
|
||
code = text.charCodeAt(i);
|
||
if (ch === "\"") {
|
||
out += "\\\"";
|
||
} else if (ch === "\\") {
|
||
out += "\\\\";
|
||
} else if (ch === "\n") {
|
||
out += "\\n";
|
||
} else if (ch === "\r") {
|
||
out += "\\r";
|
||
} else if (ch === "\t") {
|
||
out += "\\t";
|
||
} else if (code < 32) {
|
||
out += "\\u" + ("000" + code.toString(16)).slice(-4);
|
||
} else {
|
||
out += ch;
|
||
}
|
||
}
|
||
return out;
|
||
}
|
||
|
||
function jsonStringify(value) {
|
||
var parts;
|
||
var key;
|
||
var i;
|
||
|
||
if (value === null || value === undefined) {
|
||
return "null";
|
||
}
|
||
if (typeof value === "number" || typeof value === "boolean") {
|
||
return String(value);
|
||
}
|
||
if (typeof value === "string") {
|
||
return "\"" + jsonEscapeString(value) + "\"";
|
||
}
|
||
if (value instanceof Array) {
|
||
parts = [];
|
||
for (i = 0; i < value.length; i += 1) {
|
||
parts.push(jsonStringify(value[i]));
|
||
}
|
||
return "[" + parts.join(",") + "]";
|
||
}
|
||
parts = [];
|
||
for (key in value) {
|
||
if (value.hasOwnProperty(key) && value[key] !== undefined) {
|
||
parts.push("\"" + jsonEscapeString(key) + "\":" + jsonStringify(value[key]));
|
||
}
|
||
}
|
||
return "{" + parts.join(",") + "}";
|
||
}
|
||
|
||
// POST JSON via a temp file (-d @file) — avoids cmd.exe quote-escaping
|
||
// problems for large bodies. Returns { status: httpCode, data: parsed }.
|
||
function postJSON(url, bodyObject) {
|
||
var bodyFile;
|
||
var command;
|
||
var response = null;
|
||
var markerIndex;
|
||
var statusCode = 0;
|
||
var jsonText;
|
||
|
||
bodyFile = new File(Folder.temp.fsName + "/vfxr_post_body.json");
|
||
try {
|
||
bodyFile.encoding = "UTF-8";
|
||
if (!bodyFile.open("w")) {
|
||
return { status: 0, data: null };
|
||
}
|
||
bodyFile.write(jsonStringify(bodyObject));
|
||
bodyFile.close();
|
||
|
||
command = "curl -s -X POST " +
|
||
"-H \"Authorization: Bearer " + TOKEN + "\" " +
|
||
"-H \"Content-Type: application/json\" " +
|
||
"-H \"Accept: application/json\" " +
|
||
"-d \"@" + bodyFile.fsName + "\" " +
|
||
"-w \"__HTTP__:%{http_code}\" " +
|
||
"\"" + url + "\"";
|
||
|
||
response = system.callSystem(command);
|
||
} catch (postError) {
|
||
response = null;
|
||
}
|
||
try {
|
||
bodyFile.remove();
|
||
} catch (removeError) {
|
||
}
|
||
|
||
if (!response) {
|
||
return { status: 0, data: null };
|
||
}
|
||
|
||
markerIndex = response.lastIndexOf("__HTTP__:");
|
||
if (markerIndex >= 0) {
|
||
statusCode = parseInt(response.substring(markerIndex + 9), 10) || 0;
|
||
jsonText = response.substring(0, markerIndex);
|
||
} else {
|
||
jsonText = response;
|
||
}
|
||
|
||
return { status: statusCode, data: parseJSON(jsonText) };
|
||
}
|
||
|
||
function getLatestExportData(shotCode) {
|
||
var url = BASE_URL + "/api/ext/exports/latest" +
|
||
"?shotCode=" + encodeURIComponent(shotCode) +
|
||
"&projectCode=" + PROJECT_CODE;
|
||
return getAPIData(url);
|
||
}
|
||
|
||
function isActiveExportStatus(status) {
|
||
return status === "QUEUED" || status === "RENDERING" ||
|
||
status === "VALIDATING" || status === "GENERATING_PREVIEW";
|
||
}
|
||
|
||
function formatEtaShort(seconds) {
|
||
var mins;
|
||
var secs;
|
||
|
||
if (!seconds || seconds <= 0) {
|
||
return "";
|
||
}
|
||
mins = Math.floor(seconds / 60);
|
||
secs = Math.round(seconds % 60);
|
||
if (secs < 10) {
|
||
secs = "0" + secs;
|
||
}
|
||
return mins + ":" + secs;
|
||
}
|
||
|
||
function formatExportStatus(shotCode, exportInfo) {
|
||
var text;
|
||
var job;
|
||
var pct;
|
||
var eta;
|
||
|
||
if (!exportInfo) {
|
||
return shotCode + " — no exports yet";
|
||
}
|
||
|
||
text = shotCode + " " + (exportInfo.versionString || "") + " — " +
|
||
String(exportInfo.status || "").replace(/_/g, " ");
|
||
job = exportInfo.renderJob;
|
||
|
||
if (exportInfo.status === "RENDERING" && job) {
|
||
pct = Math.round((job.progress || 0) * 100);
|
||
text += " " + pct + "%";
|
||
eta = formatEtaShort(job.etaSeconds);
|
||
if (eta) {
|
||
text += " (ETA " + eta;
|
||
if (job.machineName) {
|
||
text += " on " + job.machineName;
|
||
}
|
||
text += ")";
|
||
} else if (job.machineName) {
|
||
text += " (" + job.machineName + ")";
|
||
}
|
||
} else if (job && job.errorMessage && /FAILED/.test(String(exportInfo.status))) {
|
||
text += ": " + String(job.errorMessage).substring(0, 60);
|
||
}
|
||
return text;
|
||
}
|
||
|
||
function setPipelineStatus(message) {
|
||
if (uiState.pipelineStatusText) {
|
||
uiState.pipelineStatusText.text = message || "";
|
||
}
|
||
}
|
||
|
||
function stopExportPolling() {
|
||
if (pipelineState.pollTaskId) {
|
||
try {
|
||
app.cancelTask(pipelineState.pollTaskId);
|
||
} catch (cancelError) {
|
||
}
|
||
pipelineState.pollTaskId = 0;
|
||
}
|
||
pipelineState.pollShotCode = null;
|
||
}
|
||
|
||
function pollExportTick() {
|
||
var shotCode = pipelineState.pollShotCode;
|
||
var data;
|
||
var exportInfo;
|
||
|
||
if (!shotCode) {
|
||
stopExportPolling();
|
||
return;
|
||
}
|
||
|
||
try {
|
||
data = getLatestExportData(shotCode);
|
||
if (data && data.export !== undefined) {
|
||
exportInfo = data.export;
|
||
setPipelineStatus(formatExportStatus(shotCode, exportInfo));
|
||
if (!exportInfo || !isActiveExportStatus(exportInfo.status)) {
|
||
stopExportPolling();
|
||
}
|
||
}
|
||
} catch (pollError) {
|
||
}
|
||
}
|
||
|
||
// While an export is active the panel polls every 15 s (spec §12.1); the
|
||
// task cancels itself once the export reaches a non-active state.
|
||
function startExportPolling(shotCode) {
|
||
stopExportPolling();
|
||
pipelineState.pollShotCode = shotCode;
|
||
try {
|
||
pipelineState.pollTaskId = app.scheduleTask("__vfxrExportPollTick()", 15000, true);
|
||
} catch (scheduleError) {
|
||
pipelineState.pollTaskId = 0;
|
||
}
|
||
}
|
||
|
||
function getStatusShotCode() {
|
||
var comps = getTargetComps();
|
||
var mainComp;
|
||
var code;
|
||
|
||
if (comps.length > 0) {
|
||
mainComp = getMainCompForSelection(comps[0]) || comps[0];
|
||
code = extractShotCode(mainComp.name);
|
||
if (code) {
|
||
return code;
|
||
}
|
||
}
|
||
if (uiState.shotDropdown && uiState.shotDropdown.selection) {
|
||
return String(uiState.shotDropdown.selection.text || "") || null;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
// Slate fields carry forward: pull the previous submission's values when the
|
||
// selected shot changes, but never clobber edits the artist has just typed
|
||
// for the same shot.
|
||
function prefillSlateFields(shotCode, exportInfo) {
|
||
if (pipelineState.prefilledShotCode === shotCode) {
|
||
return;
|
||
}
|
||
if (uiState.submissionNoteField) {
|
||
uiState.submissionNoteField.text = (exportInfo && exportInfo.submissionNote) ? exportInfo.submissionNote : "";
|
||
}
|
||
pipelineState.prefilledShotCode = shotCode;
|
||
}
|
||
|
||
function refreshExportStatus() {
|
||
var shotCode = getStatusShotCode();
|
||
var data;
|
||
|
||
if (!shotCode) {
|
||
setPipelineStatus("Select a comp or shot first");
|
||
return;
|
||
}
|
||
setPipelineStatus("Checking " + shotCode + "...");
|
||
data = getLatestExportData(shotCode);
|
||
if (!data || data.export === undefined) {
|
||
setPipelineStatus(shotCode + " — status check failed (server unreachable?)");
|
||
return;
|
||
}
|
||
prefillSlateFields(shotCode, data.export);
|
||
setPipelineStatus(formatExportStatus(shotCode, data.export));
|
||
if (data.export && isActiveExportStatus(data.export.status)) {
|
||
startExportPolling(shotCode);
|
||
}
|
||
}
|
||
|
||
function buildRenderManifest(comp, shot) {
|
||
var frameStart;
|
||
var frameEnd;
|
||
var frames;
|
||
var aepPath = app.project.file.fsName.replace(/\\/g, "/");
|
||
|
||
// Prefer the shot record's authoritative range (the server cross-checks
|
||
// it) — but only when it looks real: shots imported without ranges
|
||
// carry 0/0, which must never reach aerender. Fall back to the comp.
|
||
if (shot.frameStart !== null && shot.frameStart !== undefined &&
|
||
shot.frameEnd !== null && shot.frameEnd !== undefined &&
|
||
Number(shot.frameEnd) >= Number(shot.frameStart) &&
|
||
!(Number(shot.frameStart) === 0 && Number(shot.frameEnd) === 0)) {
|
||
frameStart = shot.frameStart;
|
||
frameEnd = shot.frameEnd;
|
||
} else {
|
||
frameStart = Math.round(comp.displayStartTime * comp.frameRate);
|
||
frames = Math.round(comp.duration * comp.frameRate);
|
||
frameEnd = frameStart + frames - 1;
|
||
}
|
||
|
||
return {
|
||
manifestVersion: 1,
|
||
projectCode: PROJECT_CODE,
|
||
shotCode: shot.shotCode,
|
||
shotId: shot.id,
|
||
aepPath: aepPath,
|
||
compName: comp.name,
|
||
rendererType: "aerender",
|
||
outputDir: "auto",
|
||
outputPattern: "auto",
|
||
outputModuleTemplate: "EXR Sequence",
|
||
frameStart: frameStart,
|
||
frameEnd: frameEnd,
|
||
fps: Math.round(comp.frameRate * 1000) / 1000,
|
||
width: comp.width,
|
||
height: comp.height
|
||
};
|
||
}
|
||
|
||
function submitExport(manifest, urgent, force) {
|
||
var body = {
|
||
manifest: manifest,
|
||
force: force ? true : false
|
||
};
|
||
|
||
if (ARTIST_EMAIL) {
|
||
body.submittedByEmail = ARTIST_EMAIL;
|
||
}
|
||
if (urgent) {
|
||
body.priority = 20;
|
||
}
|
||
// submissionNote is a per-export override; if blank the server falls back to the shot's slate default.
|
||
if (uiState.submissionNoteField) {
|
||
var note = uiState.submissionNoteField.text;
|
||
if (note && note.trim()) {
|
||
body.submissionNote = note;
|
||
}
|
||
}
|
||
return postJSON(BASE_URL + "/api/ext/exports", body);
|
||
}
|
||
|
||
function queueExport() {
|
||
var comps = getTargetComps();
|
||
var result = makeBatchResult(comps.length);
|
||
var urgent = uiState.urgentCheckbox ? uiState.urgentCheckbox.value : false;
|
||
var i;
|
||
var comp;
|
||
var lookupCode;
|
||
var data;
|
||
var manifest;
|
||
var response;
|
||
var errorText;
|
||
var lastQueued = null;
|
||
var lastShotCode = null;
|
||
|
||
if (comps.length < 1) {
|
||
updateStatus(0, "Select or open a comp first.");
|
||
return;
|
||
}
|
||
if (!app.project || !app.project.file) {
|
||
updateStatus(comps.length, "Save the project first — the render farm opens the AEP from disk.");
|
||
return;
|
||
}
|
||
|
||
// The worker renders the saved file; unsaved changes would silently
|
||
// render stale comps.
|
||
try {
|
||
app.project.save();
|
||
} catch (saveError) {
|
||
updateStatus(comps.length, "Could not save the project: " + saveError);
|
||
return;
|
||
}
|
||
|
||
for (i = 0; i < comps.length; i += 1) {
|
||
comp = getMainCompForSelection(comps[i]);
|
||
if (!comp) {
|
||
addFailure(result, comps[i].name, "Main comp not found for preview comp");
|
||
continue;
|
||
}
|
||
lookupCode = getShotCodeFromComp(comp);
|
||
updateStatus(comps.length, "Queuing " + lookupCode + " (" + (i + 1) + " of " + comps.length + ")...");
|
||
|
||
data = getShotData(lookupCode);
|
||
if (!data || !data.shot || !data.shot.id) {
|
||
addFailure(result, comp.name, "API lookup failed");
|
||
continue;
|
||
}
|
||
|
||
manifest = buildRenderManifest(comp, data.shot);
|
||
response = submitExport(manifest, urgent, false);
|
||
|
||
if (response.status === 409) {
|
||
errorText = (response.data && response.data.error) ?
|
||
response.data.error : "An active export already exists for this shot.";
|
||
if (confirm(lookupCode + ":\n" + errorText +
|
||
"\n\nForce supersede the active export and queue anyway?")) {
|
||
response = submitExport(manifest, urgent, true);
|
||
} else {
|
||
addFailure(result, comp.name, "Skipped (active export kept)");
|
||
continue;
|
||
}
|
||
}
|
||
|
||
if (response.status === 201 && response.data && response.data.export) {
|
||
result.queued += 1;
|
||
lastQueued = response.data.export;
|
||
lastShotCode = response.data.export.shotCode || lookupCode;
|
||
} else {
|
||
errorText = (response.data && response.data.error) ?
|
||
response.data.error : ("HTTP " + response.status);
|
||
addFailure(result, comp.name, errorText);
|
||
}
|
||
}
|
||
|
||
if (lastQueued) {
|
||
updateStatus(comps.length, "Queued " + lastQueued.versionString +
|
||
(urgent ? " (urgent)" : "") + " on render farm. " +
|
||
result.queued + " queued, " + result.skipped + " skipped.");
|
||
setPipelineStatus(formatExportStatus(lastShotCode, lastQueued));
|
||
startExportPolling(lastShotCode);
|
||
} else {
|
||
updateStatus(comps.length, "No exports queued. " +
|
||
(result.failures.length > 0 ? result.failures[0] : ""));
|
||
}
|
||
}
|
||
|
||
function retryLatestExport() {
|
||
var shotCode = getStatusShotCode();
|
||
var data;
|
||
var exportInfo;
|
||
var response;
|
||
|
||
if (!shotCode) {
|
||
setPipelineStatus("Select a comp or shot first");
|
||
return;
|
||
}
|
||
data = getLatestExportData(shotCode);
|
||
exportInfo = data ? data.export : null;
|
||
if (!exportInfo) {
|
||
setPipelineStatus(shotCode + " — no export to retry");
|
||
return;
|
||
}
|
||
if (!/FAILED$/.test(String(exportInfo.status))) {
|
||
setPipelineStatus(formatExportStatus(shotCode, exportInfo) + " — nothing to retry");
|
||
return;
|
||
}
|
||
|
||
response = postJSON(BASE_URL + "/api/ext/exports/" + exportInfo.id + "/retry",
|
||
{ note: "Retry from AE panel" });
|
||
if (response.status === 200) {
|
||
setPipelineStatus(shotCode + " " + exportInfo.versionString + " — requeued");
|
||
startExportPolling(shotCode);
|
||
} else {
|
||
setPipelineStatus("Retry failed: " +
|
||
((response.data && response.data.error) ? response.data.error : "HTTP " + response.status));
|
||
}
|
||
}
|
||
|
||
function buildUI(thisObjRef) {
|
||
var win = (thisObjRef instanceof Panel) ? thisObjRef : new Window("palette", "VFXReview Connector", undefined, { resizeable: true });
|
||
var bannerFile;
|
||
var bannerImage;
|
||
var title;
|
||
var shotBuilderPanel;
|
||
var shotBuilderGroup;
|
||
var shotBuilderButton;
|
||
var pipelinePanel;
|
||
var pipelineRow1;
|
||
var pipelineRow2;
|
||
var queueExportButton;
|
||
var refreshExportButton;
|
||
var retryExportButton;
|
||
var buttonsPanel;
|
||
var btnRow1;
|
||
var btnRow2;
|
||
var btnRow3;
|
||
var btnRow4;
|
||
var btnRow4b;
|
||
var btnRow4c;
|
||
var prepDeliveryButton;
|
||
var refreshButton;
|
||
var fixNamesButton;
|
||
var overlayButton;
|
||
var previewButton;
|
||
var exrButton;
|
||
var exrPreviewButton;
|
||
var importExrButton;
|
||
var mp4Button;
|
||
var movButton;
|
||
var btnRow5;
|
||
var importRendersButton;
|
||
var btnRow6;
|
||
var pictureLockButton;
|
||
var btnRow7;
|
||
var incrementVersionButton;
|
||
var btnRow6b;
|
||
var setStartTCButton;
|
||
var colorSpacePanel;
|
||
var ocioButton;
|
||
var workspacePanel;
|
||
var initWorkspaceButton;
|
||
var statusPanel;
|
||
|
||
win.orientation = "column";
|
||
win.alignChildren = ["fill", "top"];
|
||
win.spacing = 8;
|
||
win.margins = 12;
|
||
|
||
bannerFile = new File(BANNER_IMAGE_PATH);
|
||
if (bannerFile.exists) {
|
||
try {
|
||
bannerImage = win.add("image", undefined, bannerFile);
|
||
bannerImage.alignment = ["fit", "top"];
|
||
bannerImage.size = [360, 80];
|
||
} catch (bannerError) {
|
||
}
|
||
}
|
||
|
||
title = win.add("statictext", undefined, "VFXReview Connector");
|
||
title.alignment = ["fill", "top"];
|
||
|
||
shotBuilderPanel = win.add("panel", undefined, "Shot Builder");
|
||
shotBuilderPanel.orientation = "column";
|
||
shotBuilderPanel.alignChildren = ["fill", "top"];
|
||
shotBuilderPanel.margins = 10;
|
||
|
||
shotBuilderGroup = shotBuilderPanel.add("group");
|
||
shotBuilderGroup.orientation = "row";
|
||
shotBuilderGroup.alignChildren = ["fill", "center"];
|
||
shotBuilderGroup.spacing = 6;
|
||
|
||
uiState.episodeDropdown = shotBuilderGroup.add("dropdownlist", undefined, []);
|
||
uiState.episodeDropdown.preferredSize = [110, 24];
|
||
uiState.shotDropdown = shotBuilderGroup.add("dropdownlist", undefined, []);
|
||
uiState.shotDropdown.preferredSize = [150, 24];
|
||
shotBuilderButton = shotBuilderGroup.add("button", undefined, "Build Shot");
|
||
|
||
pipelinePanel = win.add("panel", undefined, "Render Pipeline");
|
||
pipelinePanel.orientation = "column";
|
||
pipelinePanel.alignChildren = ["fill", "top"];
|
||
pipelinePanel.margins = 8;
|
||
pipelinePanel.spacing = 4;
|
||
|
||
uiState.pipelineStatusText = pipelinePanel.add("statictext", undefined, "Export status: not checked");
|
||
|
||
pipelineRow1 = pipelinePanel.add("group");
|
||
pipelineRow1.orientation = "row";
|
||
pipelineRow1.alignChildren = ["fill", "center"];
|
||
pipelineRow1.spacing = 6;
|
||
queueExportButton = pipelineRow1.add("button", undefined, "Queue Export");
|
||
uiState.urgentCheckbox = pipelineRow1.add("checkbox", undefined, "Urgent");
|
||
|
||
var slateNoteGroup = pipelinePanel.add("group");
|
||
slateNoteGroup.orientation = "row";
|
||
slateNoteGroup.alignChildren = ["fill", "top"];
|
||
slateNoteGroup.spacing = 6;
|
||
var slateNoteLabel = slateNoteGroup.add("statictext", undefined, "Sub. Note:");
|
||
slateNoteLabel.preferredSize = [70, 20];
|
||
uiState.submissionNoteField = slateNoteGroup.add("edittext", undefined, "", { multiline: false });
|
||
uiState.submissionNoteField.preferredSize = [240, 22];
|
||
|
||
pipelineRow2 = pipelinePanel.add("group");
|
||
pipelineRow2.orientation = "row";
|
||
pipelineRow2.alignChildren = ["fill", "center"];
|
||
pipelineRow2.spacing = 6;
|
||
refreshExportButton = pipelineRow2.add("button", undefined, "Refresh Status");
|
||
retryExportButton = pipelineRow2.add("button", undefined, "Retry Export");
|
||
|
||
buttonsPanel = win.add("panel", undefined, "Actions");
|
||
buttonsPanel.orientation = "column";
|
||
buttonsPanel.alignChildren = ["fill", "top"];
|
||
buttonsPanel.margins = 8;
|
||
buttonsPanel.spacing = 4;
|
||
|
||
btnRow1 = buttonsPanel.add("group");
|
||
btnRow1.orientation = "row";
|
||
btnRow1.alignChildren = ["fill", "center"];
|
||
btnRow1.spacing = 6;
|
||
refreshButton = btnRow1.add("button", undefined, "Refresh");
|
||
fixNamesButton = btnRow1.add("button", undefined, "Fix Comp Names");
|
||
|
||
btnRow2 = buttonsPanel.add("group");
|
||
btnRow2.orientation = "row";
|
||
btnRow2.alignChildren = ["fill", "center"];
|
||
btnRow2.spacing = 6;
|
||
overlayButton = btnRow2.add("button", undefined, "Add Overlay");
|
||
previewButton = btnRow2.add("button", undefined, "Build Preview");
|
||
|
||
btnRow3 = buttonsPanel.add("group");
|
||
btnRow3.orientation = "row";
|
||
btnRow3.alignChildren = ["fill", "center"];
|
||
btnRow3.spacing = 6;
|
||
mp4Button = btnRow3.add("button", undefined, "Queue MP4");
|
||
movButton = btnRow3.add("button", undefined, "Queue MOV");
|
||
|
||
btnRow4 = buttonsPanel.add("group");
|
||
btnRow4.orientation = "row";
|
||
btnRow4.alignChildren = ["fill", "center"];
|
||
btnRow4.spacing = 6;
|
||
exrButton = btnRow4.add("button", undefined, "Queue EXR");
|
||
exrPreviewButton = btnRow4.add("button", undefined, "Queue EXR (Review)");
|
||
|
||
btnRow4b = buttonsPanel.add("group");
|
||
btnRow4b.orientation = "row";
|
||
btnRow4b.alignChildren = ["fill", "center"];
|
||
btnRow4b.spacing = 6;
|
||
importExrButton = btnRow4b.add("button", undefined, "Import EXR");
|
||
|
||
btnRow4c = buttonsPanel.add("group");
|
||
btnRow4c.orientation = "row";
|
||
btnRow4c.alignChildren = ["fill", "center"];
|
||
btnRow4c.spacing = 6;
|
||
prepDeliveryButton = btnRow4c.add("button", undefined, "Prep Delivery");
|
||
|
||
btnRow5 = buttonsPanel.add("group");
|
||
btnRow5.orientation = "row";
|
||
btnRow5.alignChildren = ["fill", "center"];
|
||
btnRow5.spacing = 6;
|
||
importRendersButton = btnRow5.add("button", undefined, "Import Renders");
|
||
|
||
btnRow6 = buttonsPanel.add("group");
|
||
btnRow6.orientation = "row";
|
||
btnRow6.alignChildren = ["fill", "center"];
|
||
btnRow6.spacing = 6;
|
||
pictureLockButton = btnRow6.add("button", undefined, "Pull Picture Lock");
|
||
|
||
btnRow6b = buttonsPanel.add("group");
|
||
btnRow6b.orientation = "row";
|
||
btnRow6b.alignChildren = ["fill", "center"];
|
||
btnRow6b.spacing = 6;
|
||
setStartTCButton = btnRow6b.add("button", undefined, "Set Start Timecode");
|
||
|
||
btnRow7 = buttonsPanel.add("group");
|
||
btnRow7.orientation = "row";
|
||
btnRow7.alignChildren = ["fill", "center"];
|
||
btnRow7.spacing = 6;
|
||
incrementVersionButton = btnRow7.add("button", undefined, "Increment Shot Version");
|
||
|
||
workspacePanel = win.add("panel", undefined, "Workspace");
|
||
workspacePanel.orientation = "column";
|
||
workspacePanel.alignChildren = ["fill", "top"];
|
||
workspacePanel.margins = 8;
|
||
workspacePanel.spacing = 4;
|
||
initWorkspaceButton = workspacePanel.add("button", undefined, "Initialise Workspace");
|
||
|
||
colorSpacePanel = win.add("panel", undefined, "Color Space");
|
||
colorSpacePanel.orientation = "column";
|
||
colorSpacePanel.alignChildren = ["fill", "top"];
|
||
colorSpacePanel.margins = 8;
|
||
colorSpacePanel.spacing = 4;
|
||
ocioButton = colorSpacePanel.add("button", undefined, "Add OCIO to Footage Layers");
|
||
|
||
statusPanel = win.add("panel", undefined, "Status");
|
||
statusPanel.orientation = "column";
|
||
statusPanel.alignChildren = ["fill", "top"];
|
||
statusPanel.margins = 10;
|
||
|
||
uiState.selectionText = statusPanel.add("statictext", undefined, "0 comps selected");
|
||
uiState.lastActionText = statusPanel.add("statictext", undefined, "Ready");
|
||
|
||
uiState.episodeDropdown.onChange = populateShotDropdown;
|
||
shotBuilderButton.onClick = buildShotFromDropdown;
|
||
queueExportButton.onClick = queueExport;
|
||
refreshExportButton.onClick = refreshExportStatus;
|
||
retryExportButton.onClick = retryLatestExport;
|
||
refreshButton.onClick = refreshSelection;
|
||
fixNamesButton.onClick = fixCompNames;
|
||
overlayButton.onClick = addOverlay;
|
||
previewButton.onClick = buildPreviews;
|
||
exrButton.onClick = queueEXR;
|
||
exrPreviewButton.onClick = queueEXRPreview;
|
||
importExrButton.onClick = importExportedEXR;
|
||
prepDeliveryButton.onClick = prepDelivery;
|
||
mp4Button.onClick = queueMP4;
|
||
movButton.onClick = queueMOV;
|
||
ocioButton.onClick = applyOCIOColorSpace;
|
||
initWorkspaceButton.onClick = initialiseWorkspace;
|
||
importRendersButton.onClick = importRenders;
|
||
pictureLockButton.onClick = pullPictureLock;
|
||
setStartTCButton.onClick = setStartFrameFromTimecode;
|
||
incrementVersionButton.onClick = incrementShotVersions;
|
||
|
||
win.layout.layout(true);
|
||
win.layout.resize();
|
||
win.onResizing = win.onResize = function () {
|
||
this.layout.resize();
|
||
};
|
||
|
||
return win;
|
||
}
|
||
|
||
// app.scheduleTask evaluates in global scope — expose the poll tick there
|
||
$.global.__vfxrExportPollTick = pollExportTick;
|
||
|
||
var panel = buildUI(thisObj);
|
||
refreshSelection();
|
||
populateEpisodeDropdown();
|
||
|
||
if (panel instanceof Window) {
|
||
panel.center();
|
||
panel.show();
|
||
}
|
||
}(this));
|