@@ -15,7 +15,7 @@
|
|||||||
and then quit it.
|
and then quit it.
|
||||||
|
|
||||||
.EXAMPLE
|
.EXAMPLE
|
||||||
.\test-preview-build.ps1 -ExrDir "V:\_EXPORTS\UNG\renders\UNG_S1\111\UNG_111_001_030\v002" `
|
.\test-preview-build.ps1 -ExrDir "V:\_EXPORTS\UNG_S1\111\UNG_111_001_030\v002" `
|
||||||
-ShotCode "UNG_111_001_030" -Version "v002"
|
-ShotCode "UNG_111_001_030" -Version "v002"
|
||||||
|
|
||||||
.EXAMPLE
|
.EXAMPLE
|
||||||
|
|||||||
+338
-85
@@ -3,22 +3,27 @@
|
|||||||
Adobe After Effects 2024+ dockable ScriptUI panel
|
Adobe After Effects 2024+ dockable ScriptUI panel
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var TOKEN = "am3O0PWUtqMJkAqsZ+bO7lho4cxQItxgukF6FHteAx4=";
|
// Default config — overridden by stored settings at runtime
|
||||||
var BASE_URL = "http://localhost:3000";
|
var CONFIG = {
|
||||||
var PROJECT_CODE = "UNG_S1";
|
baseUrl: "http://localhost:3000",
|
||||||
var EXPORT_ROOT = "V:/_EXPORTS/UNG";
|
token: "",
|
||||||
var FOOTAGE_ROOT = "V:/_FOOTAGE/UNG";
|
exportRoot: "",
|
||||||
var BANNER_IMAGE_PATH = "V:/VFXReviewConnector/banner.png";
|
footageRoot: "",
|
||||||
// Optional: artist email for pipeline export attribution (Queue Export)
|
bannerImagePath:"",
|
||||||
var ARTIST_EMAIL = "chris@twotalesanimation.com";
|
artistEmail: "",
|
||||||
// Handle frames included in plates before the shot's source TC in-point.
|
handleFrames: 8,
|
||||||
// Used to derive the plate's own start timecode from the API's timecodeStart.
|
lastProjectCode:""
|
||||||
var HANDLE_FRAMES = 8;
|
};
|
||||||
|
|
||||||
|
var SETTINGS_GROUP = "VFXReviewConnector";
|
||||||
|
var SETTINGS_KEY = "cfg";
|
||||||
|
|
||||||
(function VFXReviewConnector(thisObj) {
|
(function VFXReviewConnector(thisObj) {
|
||||||
var uiState = {
|
var uiState = {
|
||||||
|
projectDropdown: null,
|
||||||
episodeDropdown: null,
|
episodeDropdown: null,
|
||||||
shotDropdown: null,
|
shotDropdown: null,
|
||||||
|
episodeLabel: null,
|
||||||
selectionText: null,
|
selectionText: null,
|
||||||
lastActionText: null,
|
lastActionText: null,
|
||||||
pipelineStatusText: null,
|
pipelineStatusText: null,
|
||||||
@@ -26,6 +31,8 @@ var HANDLE_FRAMES = 8;
|
|||||||
submissionNoteField: null
|
submissionNoteField: null
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var CURRENT_PROJECT = null; // { code, name, showId, projectType }
|
||||||
|
|
||||||
var pipelineState = {
|
var pipelineState = {
|
||||||
pollShotCode: null,
|
pollShotCode: null,
|
||||||
pollTaskId: 0,
|
pollTaskId: 0,
|
||||||
@@ -57,6 +64,14 @@ var HANDLE_FRAMES = 8;
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ExtendScript is ES3 — String.prototype.trim does not exist.
|
||||||
|
function trimText(value) {
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return String(value).replace(/^\s+/, "").replace(/\s+$/, "");
|
||||||
|
}
|
||||||
|
|
||||||
function getSelectedComps() {
|
function getSelectedComps() {
|
||||||
var comps = [];
|
var comps = [];
|
||||||
var selection;
|
var selection;
|
||||||
@@ -83,12 +98,12 @@ var HANDLE_FRAMES = 8;
|
|||||||
var parsed;
|
var parsed;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
url = BASE_URL + "/api/ext/shots/lookup" +
|
url = CONFIG.baseUrl + "/api/ext/shots/lookup" +
|
||||||
"?shotCode=" + encodeURIComponent(shotCode) +
|
"?shotCode=" + encodeURIComponent(shotCode) +
|
||||||
"&projectCode=" + PROJECT_CODE;
|
"&projectCode=" + (CURRENT_PROJECT ? CURRENT_PROJECT.code : "");
|
||||||
|
|
||||||
command = "curl -s " +
|
command = "curl -s " +
|
||||||
"-H \"Authorization: Bearer " + TOKEN + "\" " +
|
"-H \"Authorization: Bearer " + CONFIG.token + "\" " +
|
||||||
"-H \"Accept: application/json\" " +
|
"-H \"Accept: application/json\" " +
|
||||||
"\"" + url + "\"";
|
"\"" + url + "\"";
|
||||||
|
|
||||||
@@ -112,7 +127,7 @@ var HANDLE_FRAMES = 8;
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
command = "curl -s " +
|
command = "curl -s " +
|
||||||
"-H \"Authorization: Bearer " + TOKEN + "\" " +
|
"-H \"Authorization: Bearer " + CONFIG.token + "\" " +
|
||||||
"-H \"Accept: application/json\" " +
|
"-H \"Accept: application/json\" " +
|
||||||
"\"" + url + "\"";
|
"\"" + url + "\"";
|
||||||
|
|
||||||
@@ -229,19 +244,39 @@ var HANDLE_FRAMES = 8;
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getEpisodesFromAPI() {
|
function getEpisodesFromAPI() {
|
||||||
var url = BASE_URL + "/api/ext/projects/" + PROJECT_CODE + "/episodes";
|
var url = CONFIG.baseUrl + "/api/ext/projects/" + (CURRENT_PROJECT ? CURRENT_PROJECT.code : "") + "/episodes";
|
||||||
var data = getAPIData(url);
|
var data = getAPIData(url);
|
||||||
|
|
||||||
return getListFromResponse(data, "episodes");
|
return getListFromResponse(data, "episodes");
|
||||||
}
|
}
|
||||||
|
|
||||||
function getShotsFromAPI(episodeValue) {
|
function getShotsFromAPI(value) {
|
||||||
var url = BASE_URL + "/api/ext/projects/" + PROJECT_CODE + "/shots?episode=" + encodeURIComponent(episodeValue);
|
var isEpisodic = CURRENT_PROJECT && CURRENT_PROJECT.projectType === "EPISODIC";
|
||||||
|
var param = isEpisodic ? "episode" : "scene";
|
||||||
|
var url = CONFIG.baseUrl + "/api/ext/projects/" + (CURRENT_PROJECT ? CURRENT_PROJECT.code : "") + "/shots?" + param + "=" + encodeURIComponent(value);
|
||||||
var data = getAPIData(url);
|
var data = getAPIData(url);
|
||||||
|
|
||||||
return getListFromResponse(data, "shots");
|
return getListFromResponse(data, "shots");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getScenesFromAPI() {
|
||||||
|
var url = CONFIG.baseUrl + "/api/ext/projects/" + (CURRENT_PROJECT ? CURRENT_PROJECT.code : "") + "/shots?limit=500";
|
||||||
|
var data = getAPIData(url);
|
||||||
|
var shots = getListFromResponse(data, "shots");
|
||||||
|
var seen = {};
|
||||||
|
var scenes = [];
|
||||||
|
for (var i = 0; i < shots.length; i += 1) {
|
||||||
|
var sc = shots[i].scene || "";
|
||||||
|
if (sc && !seen[sc]) { seen[sc] = true; scenes.push(sc); }
|
||||||
|
}
|
||||||
|
scenes.sort();
|
||||||
|
return scenes;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getProjectsFromAPI() {
|
||||||
|
var url = CONFIG.baseUrl + "/api/ext/projects";
|
||||||
|
var data = getAPIData(url);
|
||||||
|
return getListFromResponse(data, "projects");
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeShotData(data) {
|
function normalizeShotData(data) {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return null;
|
return null;
|
||||||
@@ -270,6 +305,16 @@ var HANDLE_FRAMES = 8;
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getShowExportRoot() {
|
||||||
|
var showId = CURRENT_PROJECT ? (CURRENT_PROJECT.showId || CURRENT_PROJECT.code) : "";
|
||||||
|
return CONFIG.exportRoot + (showId ? "/" + showId : "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function getShowFootageRoot() {
|
||||||
|
var showId = CURRENT_PROJECT ? (CURRENT_PROJECT.showId || CURRENT_PROJECT.code) : "";
|
||||||
|
return CONFIG.footageRoot + (showId ? "/" + showId : "");
|
||||||
|
}
|
||||||
|
|
||||||
function ensureFolder(path) {
|
function ensureFolder(path) {
|
||||||
var folder = new Folder(path);
|
var folder = new Folder(path);
|
||||||
var parent;
|
var parent;
|
||||||
@@ -397,9 +442,9 @@ var HANDLE_FRAMES = 8;
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (apiShot && apiShot.timecodeStart) {
|
if (apiShot && apiShot.timecodeStart) {
|
||||||
frames = parseTimecodeToFrames(apiShot.timecodeStart, fps) - HANDLE_FRAMES;
|
frames = parseTimecodeToFrames(apiShot.timecodeStart, fps) - CONFIG.handleFrames;
|
||||||
if (frames > 0) {
|
if (frames > 0) {
|
||||||
return { frames: frames, source: "API timecode " + apiShot.timecodeStart + " -" + HANDLE_FRAMES + "f" };
|
return { frames: frames, source: "API timecode " + apiShot.timecodeStart + " -" + CONFIG.handleFrames + "f" };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -462,11 +507,11 @@ var HANDLE_FRAMES = 8;
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getEpisodeRootFolder(episodeCode) {
|
function getEpisodeRootFolder(episodeCode) {
|
||||||
return new Folder(FOOTAGE_ROOT + "/" + episodeCode);
|
return new Folder(getShowFootageRoot() + "/" + episodeCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getEpisodeCodesFromDisk() {
|
function getEpisodeCodesFromDisk() {
|
||||||
var root = new Folder(FOOTAGE_ROOT);
|
var root = new Folder(getShowFootageRoot());
|
||||||
var folders;
|
var folders;
|
||||||
var codes = [];
|
var codes = [];
|
||||||
var i;
|
var i;
|
||||||
@@ -867,7 +912,7 @@ var HANDLE_FRAMES = 8;
|
|||||||
disableLayerByName(comp, "UNG_VFX_OVERLAY");
|
disableLayerByName(comp, "UNG_VFX_OVERLAY");
|
||||||
disableLayerByName(comp, "_SHOW LUT");
|
disableLayerByName(comp, "_SHOW LUT");
|
||||||
|
|
||||||
folderPath = EXPORT_ROOT + "/" + shotCode;
|
folderPath = getShowExportRoot() + "/" + shotCode;
|
||||||
if (!ensureFolder(folderPath)) {
|
if (!ensureFolder(folderPath)) {
|
||||||
addFailure(result, comp.name, "Could not create output folder");
|
addFailure(result, comp.name, "Could not create output folder");
|
||||||
continue;
|
continue;
|
||||||
@@ -1108,13 +1153,19 @@ var HANDLE_FRAMES = 8;
|
|||||||
var episodeCode;
|
var episodeCode;
|
||||||
var shotCode;
|
var shotCode;
|
||||||
var shotComp;
|
var shotComp;
|
||||||
|
var isEpisodic = CURRENT_PROJECT && CURRENT_PROJECT.projectType === "EPISODIC";
|
||||||
|
|
||||||
if (!uiState.episodeDropdown || !uiState.episodeDropdown.selection || !uiState.shotDropdown || !uiState.shotDropdown.selection) {
|
if (!uiState.episodeDropdown || !uiState.episodeDropdown.selection || !uiState.shotDropdown || !uiState.shotDropdown.selection) {
|
||||||
updateStatus(getSelectedComps().length, "Choose an episode and shot first.");
|
updateStatus(getSelectedComps().length, "Choose a " + (isEpisodic ? "episode" : "scene") + " and shot first.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
episodeCode = uiState.episodeDropdown.selection.episodeCode || deriveEpisodeCode(uiState.episodeDropdown.selection.text);
|
// For episodic, derive full episode code; for standard pass the selected scene value
|
||||||
|
if (isEpisodic) {
|
||||||
|
episodeCode = uiState.episodeDropdown.selection.episodeCode || deriveEpisodeCode(uiState.episodeDropdown.selection.text);
|
||||||
|
} else {
|
||||||
|
episodeCode = uiState.episodeDropdown.selection.filterValue || uiState.episodeDropdown.selection.text;
|
||||||
|
}
|
||||||
shotCode = uiState.shotDropdown.selection.shotCode || uiState.shotDropdown.selection.text;
|
shotCode = uiState.shotDropdown.selection.shotCode || uiState.shotDropdown.selection.text;
|
||||||
|
|
||||||
app.beginUndoGroup("VFXReview Connector - Build Shot");
|
app.beginUndoGroup("VFXReview Connector - Build Shot");
|
||||||
@@ -1193,7 +1244,7 @@ var HANDLE_FRAMES = 8;
|
|||||||
var outputPath;
|
var outputPath;
|
||||||
|
|
||||||
app.beginUndoGroup("VFXReview Connector - Queue " + label);
|
app.beginUndoGroup("VFXReview Connector - Queue " + label);
|
||||||
if (!ensureFolder(EXPORT_ROOT)) {
|
if (!ensureFolder(getShowExportRoot())) {
|
||||||
app.endUndoGroup();
|
app.endUndoGroup();
|
||||||
updateStatus(comps.length, "Could not create export folder.");
|
updateStatus(comps.length, "Could not create export folder.");
|
||||||
return;
|
return;
|
||||||
@@ -1231,7 +1282,7 @@ var HANDLE_FRAMES = 8;
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateStatus(comps.length, "Queueing " + label + " for " + shotCode + " (" + (i + 1) + " of " + comps.length + ")");
|
updateStatus(comps.length, "Queueing " + label + " for " + shotCode + " (" + (i + 1) + " of " + comps.length + ")");
|
||||||
outputPath = EXPORT_ROOT + "/" + shotCode + "_cmp_TT_" + (apiData.shot.shotVersion || "v001") + "." + extension;
|
outputPath = getShowExportRoot() + "/" + shotCode + "_cmp_TT_" + (apiData.shot.shotVersion || "v001") + "." + extension;
|
||||||
if (queueCompWithOutput(previewComp, templateName, outputPath)) {
|
if (queueCompWithOutput(previewComp, templateName, outputPath)) {
|
||||||
queued += 1;
|
queued += 1;
|
||||||
result.queued += 1;
|
result.queued += 1;
|
||||||
@@ -1272,7 +1323,7 @@ var HANDLE_FRAMES = 8;
|
|||||||
disableLayerByName(comp, "UNG_VFX_OVERLAY");
|
disableLayerByName(comp, "UNG_VFX_OVERLAY");
|
||||||
disableLayerByName(comp, "_SHOW LUT");
|
disableLayerByName(comp, "_SHOW LUT");
|
||||||
|
|
||||||
folderPath = EXPORT_ROOT + "/" + shotCode;
|
folderPath = getShowExportRoot() + "/" + shotCode;
|
||||||
if (!ensureFolder(folderPath)) {
|
if (!ensureFolder(folderPath)) {
|
||||||
addFailure(result, comp.name, "Could not create output folder");
|
addFailure(result, comp.name, "Could not create output folder");
|
||||||
continue;
|
continue;
|
||||||
@@ -1333,7 +1384,7 @@ var HANDLE_FRAMES = 8;
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
exportFolder = new Folder(EXPORT_ROOT + "/" + shot.shotCode);
|
exportFolder = new Folder(getShowExportRoot() + "/" + shot.shotCode);
|
||||||
if (!exportFolder.exists) {
|
if (!exportFolder.exists) {
|
||||||
updateStatus(comps.length, "Export folder not found: " + exportFolder.fsName);
|
updateStatus(comps.length, "Export folder not found: " + exportFolder.fsName);
|
||||||
skipped += 1;
|
skipped += 1;
|
||||||
@@ -1545,7 +1596,7 @@ var HANDLE_FRAMES = 8;
|
|||||||
expectedBaseName = shotCode + "_cmp_TT_" + (shot.shotVersion || "v001");
|
expectedBaseName = shotCode + "_cmp_TT_" + (shot.shotVersion || "v001");
|
||||||
exrOutputBase = shot.exrOutput || "";
|
exrOutputBase = shot.exrOutput || "";
|
||||||
|
|
||||||
sourceFolder = new Folder(EXPORT_ROOT + "/" + shotCode);
|
sourceFolder = new Folder(getShowExportRoot() + "/" + shotCode);
|
||||||
if (!sourceFolder.exists) {
|
if (!sourceFolder.exists) {
|
||||||
addFailure(result, comp.name, "Source export folder not found: " + sourceFolder.fsName);
|
addFailure(result, comp.name, "Source export folder not found: " + sourceFolder.fsName);
|
||||||
continue;
|
continue;
|
||||||
@@ -1586,7 +1637,7 @@ var HANDLE_FRAMES = 8;
|
|||||||
|
|
||||||
var episodeCode = shotCode.substring(0, 7);
|
var episodeCode = shotCode.substring(0, 7);
|
||||||
var deliveryDate = getDateString().replace(/\//g, "");
|
var deliveryDate = getDateString().replace(/\//g, "");
|
||||||
deliveryFolderPath = EXPORT_ROOT + "/DELIVERY/" + episodeCode + "/" + deliveryDate + "_Delivery/" + shotCode;
|
deliveryFolderPath = getShowExportRoot() + "/DELIVERY/" + episodeCode + "/" + deliveryDate + "_Delivery/" + shotCode;
|
||||||
if (!ensureFolder(deliveryFolderPath)) {
|
if (!ensureFolder(deliveryFolderPath)) {
|
||||||
addFailure(result, comp.name, "Could not create delivery folder");
|
addFailure(result, comp.name, "Could not create delivery folder");
|
||||||
continue;
|
continue;
|
||||||
@@ -1731,7 +1782,7 @@ var HANDLE_FRAMES = 8;
|
|||||||
|
|
||||||
// Not in project — import from disk
|
// Not in project — import from disk
|
||||||
if (!piclockFootage) {
|
if (!piclockFootage) {
|
||||||
piclocksDir = new Folder(FOOTAGE_ROOT + "/_PICLOCKS");
|
piclocksDir = new Folder(getShowFootageRoot() + "/_PICLOCKS");
|
||||||
if (!piclocksDir.exists) {
|
if (!piclocksDir.exists) {
|
||||||
updateStatus(getSelectedComps().length, "PICLOCKS folder not found: " + piclocksDir.fsName);
|
updateStatus(getSelectedComps().length, "PICLOCKS folder not found: " + piclocksDir.fsName);
|
||||||
return;
|
return;
|
||||||
@@ -1978,7 +2029,7 @@ var HANDLE_FRAMES = 8;
|
|||||||
}
|
}
|
||||||
|
|
||||||
function populateShotDropdown() {
|
function populateShotDropdown() {
|
||||||
var episodeValue;
|
var filterValue;
|
||||||
var shots;
|
var shots;
|
||||||
var i;
|
var i;
|
||||||
var shotCode;
|
var shotCode;
|
||||||
@@ -1988,8 +2039,8 @@ var HANDLE_FRAMES = 8;
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
episodeValue = uiState.episodeDropdown.selection.episodeValue || uiState.episodeDropdown.selection.text;
|
filterValue = uiState.episodeDropdown.selection.filterValue || uiState.episodeDropdown.selection.text;
|
||||||
shots = getShotsFromAPI(episodeValue);
|
shots = getShotsFromAPI(filterValue);
|
||||||
|
|
||||||
clearDropdown(uiState.shotDropdown);
|
clearDropdown(uiState.shotDropdown);
|
||||||
for (i = 0; i < shots.length; i += 1) {
|
for (i = 0; i < shots.length; i += 1) {
|
||||||
@@ -2008,38 +2059,132 @@ var HANDLE_FRAMES = 8;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function populateEpisodeDropdown() {
|
function populateEpisodeOrSceneDropdown() {
|
||||||
var episodes = getEpisodesFromAPI();
|
var isEpisodic = CURRENT_PROJECT && CURRENT_PROJECT.projectType === "EPISODIC";
|
||||||
var i;
|
var i;
|
||||||
var label;
|
|
||||||
var episodeValue;
|
|
||||||
var item;
|
var item;
|
||||||
|
|
||||||
if (!uiState.episodeDropdown || !uiState.shotDropdown) {
|
if (!uiState.episodeDropdown || !uiState.shotDropdown) { return; }
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
clearDropdown(uiState.episodeDropdown);
|
clearDropdown(uiState.episodeDropdown);
|
||||||
clearDropdown(uiState.shotDropdown);
|
clearDropdown(uiState.shotDropdown);
|
||||||
|
|
||||||
for (i = 0; i < episodes.length; i += 1) {
|
if (uiState.episodeLabel) {
|
||||||
label = getEpisodeLabel(episodes[i]);
|
uiState.episodeLabel.text = isEpisodic ? "Ep:" : "Scene:";
|
||||||
episodeValue = getEpisodeValue(episodes[i]);
|
}
|
||||||
if (label && episodeValue) {
|
|
||||||
item = uiState.episodeDropdown.add("item", label);
|
if (isEpisodic) {
|
||||||
item.episodeValue = episodeValue;
|
var episodes = getEpisodesFromAPI();
|
||||||
item.episodeCode = deriveEpisodeCode(label);
|
for (i = 0; i < episodes.length; i += 1) {
|
||||||
if (!/^[A-Z]{3}_\d{3}$/.test(item.episodeCode)) {
|
var label = getEpisodeLabel(episodes[i]);
|
||||||
item.episodeCode = deriveEpisodeCode(episodeValue);
|
var episodeValue = getEpisodeValue(episodes[i]);
|
||||||
|
if (label && episodeValue) {
|
||||||
|
item = uiState.episodeDropdown.add("item", label);
|
||||||
|
item.filterValue = episodeValue;
|
||||||
|
item.episodeCode = deriveEpisodeCode(label);
|
||||||
|
if (!/^[A-Z]{3}_\d{3}$/.test(item.episodeCode)) {
|
||||||
|
item.episodeCode = deriveEpisodeCode(episodeValue);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
var scenes = getScenesFromAPI();
|
||||||
|
for (i = 0; i < scenes.length; i += 1) {
|
||||||
|
item = uiState.episodeDropdown.add("item", scenes[i]);
|
||||||
|
item.filterValue = scenes[i];
|
||||||
|
item.episodeCode = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (uiState.episodeDropdown.items.length > 0) {
|
if (uiState.episodeDropdown.items.length > 0) {
|
||||||
uiState.episodeDropdown.selection = 0;
|
uiState.episodeDropdown.selection = 0;
|
||||||
populateShotDropdown();
|
populateShotDropdown();
|
||||||
} else {
|
} else {
|
||||||
updateStatus(getSelectedComps().length, "No episodes found from API.");
|
updateStatus(getSelectedComps().length, isEpisodic ? "No episodes found." : "No scenes found.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function populateEpisodeDropdown() {
|
||||||
|
populateEpisodeOrSceneDropdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
function populateProjectDropdown() {
|
||||||
|
var projects;
|
||||||
|
var i;
|
||||||
|
var p;
|
||||||
|
var item;
|
||||||
|
var selectIdx = 0;
|
||||||
|
|
||||||
|
if (!uiState.projectDropdown) { return; }
|
||||||
|
|
||||||
|
projects = getProjectsFromAPI();
|
||||||
|
clearDropdown(uiState.projectDropdown);
|
||||||
|
clearDropdown(uiState.episodeDropdown);
|
||||||
|
clearDropdown(uiState.shotDropdown);
|
||||||
|
|
||||||
|
for (i = 0; i < projects.length; i += 1) {
|
||||||
|
p = projects[i];
|
||||||
|
item = uiState.projectDropdown.add("item", p.code + " — " + p.name);
|
||||||
|
item.projectData = p;
|
||||||
|
if (p.code === CONFIG.lastProjectCode) { selectIdx = i; }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uiState.projectDropdown.items.length > 0) {
|
||||||
|
uiState.projectDropdown.selection = selectIdx;
|
||||||
|
CURRENT_PROJECT = uiState.projectDropdown.items[selectIdx].projectData;
|
||||||
|
populateEpisodeOrSceneDropdown();
|
||||||
|
} else {
|
||||||
|
updateStatus(0, "No projects found — check server URL and token in Settings.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openSettingsDialog() {
|
||||||
|
var dlg = new Window("dialog", "VFXReview Connector — Settings", undefined);
|
||||||
|
dlg.orientation = "column";
|
||||||
|
dlg.alignChildren = ["fill", "top"];
|
||||||
|
dlg.spacing = 6;
|
||||||
|
dlg.margins = 16;
|
||||||
|
|
||||||
|
function addField(parent, lbl, val) {
|
||||||
|
var grp = parent.add("group");
|
||||||
|
grp.orientation = "row";
|
||||||
|
grp.alignChildren = ["fill", "center"];
|
||||||
|
var label = grp.add("statictext", undefined, lbl);
|
||||||
|
label.preferredSize = [110, 20];
|
||||||
|
var field = grp.add("edittext", undefined, val || "");
|
||||||
|
field.preferredSize = [280, 22];
|
||||||
|
return field;
|
||||||
|
}
|
||||||
|
|
||||||
|
var fUrl = addField(dlg, "Server URL:", CONFIG.baseUrl);
|
||||||
|
var fToken = addField(dlg, "API Token:", CONFIG.token);
|
||||||
|
var fExport = addField(dlg, "Export Root:", CONFIG.exportRoot);
|
||||||
|
var fFootage= addField(dlg, "Footage Root:", CONFIG.footageRoot);
|
||||||
|
var fBanner = addField(dlg, "Banner Path:", CONFIG.bannerImagePath);
|
||||||
|
var fEmail = addField(dlg, "Artist Email:", CONFIG.artistEmail);
|
||||||
|
var fHandles= addField(dlg, "Handle Frames:", String(CONFIG.handleFrames));
|
||||||
|
|
||||||
|
var btnRow = dlg.add("group");
|
||||||
|
btnRow.orientation = "row";
|
||||||
|
btnRow.alignChildren = ["fill", "center"];
|
||||||
|
btnRow.spacing = 8;
|
||||||
|
// Using named "ok"/"cancel" — the only reliable way to capture dialog result in ScriptUI
|
||||||
|
btnRow.add("button", undefined, "Save", { name: "ok" });
|
||||||
|
btnRow.add("button", undefined, "Cancel", { name: "cancel" });
|
||||||
|
|
||||||
|
dlg.layout.layout(true);
|
||||||
|
dlg.center();
|
||||||
|
|
||||||
|
if (dlg.show() === 1) {
|
||||||
|
CONFIG.baseUrl = trimText(fUrl.text) || CONFIG.baseUrl;
|
||||||
|
CONFIG.token = trimText(fToken.text);
|
||||||
|
CONFIG.exportRoot = trimText(fExport.text);
|
||||||
|
CONFIG.footageRoot = trimText(fFootage.text);
|
||||||
|
CONFIG.bannerImagePath = trimText(fBanner.text);
|
||||||
|
CONFIG.artistEmail = trimText(fEmail.text);
|
||||||
|
CONFIG.handleFrames = parseInt(fHandles.text, 10) || 8;
|
||||||
|
saveConfig();
|
||||||
|
populateProjectDropdown();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2063,7 +2208,7 @@ var HANDLE_FRAMES = 8;
|
|||||||
}
|
}
|
||||||
|
|
||||||
function patchShotVersion(shotId, newVersion) {
|
function patchShotVersion(shotId, newVersion) {
|
||||||
var url = BASE_URL + "/api/ext/shots/" + shotId;
|
var url = CONFIG.baseUrl + "/api/ext/shots/" + shotId;
|
||||||
// Inner double-quotes escaped for Windows cmd.exe
|
// Inner double-quotes escaped for Windows cmd.exe
|
||||||
var body = "{\\\"shotVersion\\\":\\\"" + newVersion + "\\\"}";
|
var body = "{\\\"shotVersion\\\":\\\"" + newVersion + "\\\"}";
|
||||||
var command;
|
var command;
|
||||||
@@ -2071,7 +2216,7 @@ var HANDLE_FRAMES = 8;
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
command = "curl -s -X PATCH " +
|
command = "curl -s -X PATCH " +
|
||||||
"-H \"Authorization: Bearer " + TOKEN + "\" " +
|
"-H \"Authorization: Bearer " + CONFIG.token + "\" " +
|
||||||
"-H \"Content-Type: application/json\" " +
|
"-H \"Content-Type: application/json\" " +
|
||||||
"-H \"Accept: application/json\" " +
|
"-H \"Accept: application/json\" " +
|
||||||
"-d \"" + body + "\" " +
|
"-d \"" + body + "\" " +
|
||||||
@@ -2188,7 +2333,7 @@ var HANDLE_FRAMES = 8;
|
|||||||
|
|
||||||
shot = data.shot;
|
shot = data.shot;
|
||||||
fps = shot.fps || comps[i].frameRate || 24;
|
fps = shot.fps || comps[i].frameRate || 24;
|
||||||
frameNumber = parseTimecodeToFrames(shot.timecodeStart, fps) - HANDLE_FRAMES;
|
frameNumber = parseTimecodeToFrames(shot.timecodeStart, fps) - CONFIG.handleFrames;
|
||||||
setCompStartFrame(comps[i], frameNumber);
|
setCompStartFrame(comps[i], frameNumber);
|
||||||
updated += 1;
|
updated += 1;
|
||||||
}
|
}
|
||||||
@@ -2263,6 +2408,40 @@ var HANDLE_FRAMES = 8;
|
|||||||
return "{" + parts.join(",") + "}";
|
return "{" + parts.join(",") + "}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getConfigPath() {
|
||||||
|
return Folder.userData.fsName + "/VFXReviewConnector/config.json";
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadConfig() {
|
||||||
|
try {
|
||||||
|
var f = new File(getConfigPath());
|
||||||
|
if (f.exists) {
|
||||||
|
f.encoding = "UTF-8";
|
||||||
|
f.open("r");
|
||||||
|
var raw = f.read();
|
||||||
|
f.close();
|
||||||
|
var parsed = parseJSON(raw);
|
||||||
|
if (parsed) {
|
||||||
|
for (var k in parsed) {
|
||||||
|
if (parsed.hasOwnProperty(k)) CONFIG[k] = parsed[k];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveConfig() {
|
||||||
|
try {
|
||||||
|
var dir = new Folder(Folder.userData.fsName + "/VFXReviewConnector");
|
||||||
|
if (!dir.exists) { dir.create(); }
|
||||||
|
var f = new File(getConfigPath());
|
||||||
|
f.encoding = "UTF-8";
|
||||||
|
f.open("w");
|
||||||
|
f.write(jsonStringify(CONFIG));
|
||||||
|
f.close();
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
// POST JSON via a temp file (-d @file) — avoids cmd.exe quote-escaping
|
// POST JSON via a temp file (-d @file) — avoids cmd.exe quote-escaping
|
||||||
// problems for large bodies. Returns { status: httpCode, data: parsed }.
|
// problems for large bodies. Returns { status: httpCode, data: parsed }.
|
||||||
function postJSON(url, bodyObject) {
|
function postJSON(url, bodyObject) {
|
||||||
@@ -2283,7 +2462,7 @@ var HANDLE_FRAMES = 8;
|
|||||||
bodyFile.close();
|
bodyFile.close();
|
||||||
|
|
||||||
command = "curl -s -X POST " +
|
command = "curl -s -X POST " +
|
||||||
"-H \"Authorization: Bearer " + TOKEN + "\" " +
|
"-H \"Authorization: Bearer " + CONFIG.token + "\" " +
|
||||||
"-H \"Content-Type: application/json\" " +
|
"-H \"Content-Type: application/json\" " +
|
||||||
"-H \"Accept: application/json\" " +
|
"-H \"Accept: application/json\" " +
|
||||||
"-d \"@" + bodyFile.fsName + "\" " +
|
"-d \"@" + bodyFile.fsName + "\" " +
|
||||||
@@ -2315,9 +2494,9 @@ var HANDLE_FRAMES = 8;
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getLatestExportData(shotCode) {
|
function getLatestExportData(shotCode) {
|
||||||
var url = BASE_URL + "/api/ext/exports/latest" +
|
var url = CONFIG.baseUrl + "/api/ext/exports/latest" +
|
||||||
"?shotCode=" + encodeURIComponent(shotCode) +
|
"?shotCode=" + encodeURIComponent(shotCode) +
|
||||||
"&projectCode=" + PROJECT_CODE;
|
"&projectCode=" + (CURRENT_PROJECT ? CURRENT_PROJECT.code : "");
|
||||||
return getAPIData(url);
|
return getAPIData(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2501,7 +2680,7 @@ var HANDLE_FRAMES = 8;
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
manifestVersion: 1,
|
manifestVersion: 1,
|
||||||
projectCode: PROJECT_CODE,
|
projectCode: CURRENT_PROJECT ? CURRENT_PROJECT.code : "",
|
||||||
shotCode: shot.shotCode,
|
shotCode: shot.shotCode,
|
||||||
shotId: shot.id,
|
shotId: shot.id,
|
||||||
aepPath: aepPath,
|
aepPath: aepPath,
|
||||||
@@ -2524,20 +2703,20 @@ var HANDLE_FRAMES = 8;
|
|||||||
force: force ? true : false
|
force: force ? true : false
|
||||||
};
|
};
|
||||||
|
|
||||||
if (ARTIST_EMAIL) {
|
if (CONFIG.artistEmail) {
|
||||||
body.submittedByEmail = ARTIST_EMAIL;
|
body.submittedByEmail = CONFIG.artistEmail;
|
||||||
}
|
}
|
||||||
if (urgent) {
|
if (urgent) {
|
||||||
body.priority = 20;
|
body.priority = 20;
|
||||||
}
|
}
|
||||||
// submissionNote is a per-export override; if blank the server falls back to the shot's slate default.
|
// submissionNote is a per-export override; if blank the server falls back to the shot's slate default.
|
||||||
if (uiState.submissionNoteField) {
|
if (uiState.submissionNoteField) {
|
||||||
var note = uiState.submissionNoteField.text;
|
var note = trimText(uiState.submissionNoteField.text);
|
||||||
if (note && note.trim()) {
|
if (note) {
|
||||||
body.submissionNote = note;
|
body.submissionNote = note;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return postJSON(BASE_URL + "/api/ext/exports", body);
|
return postJSON(CONFIG.baseUrl + "/api/ext/exports", body);
|
||||||
}
|
}
|
||||||
|
|
||||||
function queueExport() {
|
function queueExport() {
|
||||||
@@ -2646,7 +2825,7 @@ var HANDLE_FRAMES = 8;
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
response = postJSON(BASE_URL + "/api/ext/exports/" + exportInfo.id + "/retry",
|
response = postJSON(CONFIG.baseUrl + "/api/ext/exports/" + exportInfo.id + "/retry",
|
||||||
{ note: "Retry from AE panel" });
|
{ note: "Retry from AE panel" });
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
setPipelineStatus(shotCode + " " + exportInfo.versionString + " — requeued");
|
setPipelineStatus(shotCode + " " + exportInfo.versionString + " — requeued");
|
||||||
@@ -2703,24 +2882,42 @@ var HANDLE_FRAMES = 8;
|
|||||||
var statusPanel;
|
var statusPanel;
|
||||||
|
|
||||||
win.orientation = "column";
|
win.orientation = "column";
|
||||||
win.alignChildren = ["fill", "top"];
|
win.alignChildren = ["fill", "fill"];
|
||||||
win.spacing = 8;
|
win.spacing = 0;
|
||||||
win.margins = 12;
|
win.margins = 0;
|
||||||
|
|
||||||
bannerFile = new File(BANNER_IMAGE_PATH);
|
var scrollRow = win.add("group");
|
||||||
|
scrollRow.orientation = "row";
|
||||||
|
scrollRow.alignChildren = ["fill", "fill"];
|
||||||
|
scrollRow.spacing = 0;
|
||||||
|
scrollRow.margins = 0;
|
||||||
|
|
||||||
|
var scrollContent = scrollRow.add("group");
|
||||||
|
scrollContent.orientation = "column";
|
||||||
|
scrollContent.alignChildren = ["fill", "top"];
|
||||||
|
scrollContent.spacing = 8;
|
||||||
|
scrollContent.margins = 12;
|
||||||
|
|
||||||
|
var vScrollbar = scrollRow.add("scrollbar");
|
||||||
|
vScrollbar.orientation = "vertical";
|
||||||
|
vScrollbar.preferredSize = [14, -1];
|
||||||
|
vScrollbar.minvalue = 0;
|
||||||
|
vScrollbar.value = 0;
|
||||||
|
|
||||||
|
bannerFile = new File(CONFIG.bannerImagePath);
|
||||||
if (bannerFile.exists) {
|
if (bannerFile.exists) {
|
||||||
try {
|
try {
|
||||||
bannerImage = win.add("image", undefined, bannerFile);
|
bannerImage = scrollContent.add("image", undefined, bannerFile);
|
||||||
bannerImage.alignment = ["fit", "top"];
|
bannerImage.alignment = ["fit", "top"];
|
||||||
bannerImage.size = [360, 80];
|
bannerImage.size = [360, 80];
|
||||||
} catch (bannerError) {
|
} catch (bannerError) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
title = win.add("statictext", undefined, "VFXReview Connector");
|
title = scrollContent.add("statictext", undefined, "VFXReview Connector");
|
||||||
title.alignment = ["fill", "top"];
|
title.alignment = ["fill", "top"];
|
||||||
|
|
||||||
shotBuilderPanel = win.add("panel", undefined, "Shot Builder");
|
shotBuilderPanel = scrollContent.add("panel", undefined, "Shot Builder");
|
||||||
shotBuilderPanel.orientation = "column";
|
shotBuilderPanel.orientation = "column";
|
||||||
shotBuilderPanel.alignChildren = ["fill", "top"];
|
shotBuilderPanel.alignChildren = ["fill", "top"];
|
||||||
shotBuilderPanel.margins = 10;
|
shotBuilderPanel.margins = 10;
|
||||||
@@ -2730,13 +2927,33 @@ var HANDLE_FRAMES = 8;
|
|||||||
shotBuilderGroup.alignChildren = ["fill", "center"];
|
shotBuilderGroup.alignChildren = ["fill", "center"];
|
||||||
shotBuilderGroup.spacing = 6;
|
shotBuilderGroup.spacing = 6;
|
||||||
|
|
||||||
uiState.episodeDropdown = shotBuilderGroup.add("dropdownlist", undefined, []);
|
// Project row
|
||||||
uiState.episodeDropdown.preferredSize = [110, 24];
|
var projectRow = shotBuilderPanel.add("group");
|
||||||
uiState.shotDropdown = shotBuilderGroup.add("dropdownlist", undefined, []);
|
projectRow.orientation = "row";
|
||||||
uiState.shotDropdown.preferredSize = [150, 24];
|
projectRow.alignChildren = ["fill", "center"];
|
||||||
shotBuilderButton = shotBuilderGroup.add("button", undefined, "Build Shot");
|
projectRow.spacing = 6;
|
||||||
|
var projectLabel = projectRow.add("statictext", undefined, "Project:");
|
||||||
|
projectLabel.preferredSize = [42, 20];
|
||||||
|
uiState.projectDropdown = projectRow.add("dropdownlist", undefined, []);
|
||||||
|
uiState.projectDropdown.preferredSize = [220, 24];
|
||||||
|
var settingsBtn = projectRow.add("button", undefined, "\u2699");
|
||||||
|
settingsBtn.preferredSize = [26, 24];
|
||||||
|
settingsBtn.helpTip = "Settings";
|
||||||
|
|
||||||
pipelinePanel = win.add("panel", undefined, "Render Pipeline");
|
// Episode / Scene row
|
||||||
|
var episodeRow = shotBuilderPanel.add("group");
|
||||||
|
episodeRow.orientation = "row";
|
||||||
|
episodeRow.alignChildren = ["fill", "center"];
|
||||||
|
episodeRow.spacing = 6;
|
||||||
|
uiState.episodeLabel = episodeRow.add("statictext", undefined, "Ep:");
|
||||||
|
uiState.episodeLabel.preferredSize = [42, 20];
|
||||||
|
uiState.episodeDropdown = episodeRow.add("dropdownlist", undefined, []);
|
||||||
|
uiState.episodeDropdown.preferredSize = [110, 24];
|
||||||
|
uiState.shotDropdown = episodeRow.add("dropdownlist", undefined, []);
|
||||||
|
uiState.shotDropdown.preferredSize = [130, 24];
|
||||||
|
shotBuilderButton = episodeRow.add("button", undefined, "Build Shot");
|
||||||
|
|
||||||
|
pipelinePanel = scrollContent.add("panel", undefined, "Render Pipeline");
|
||||||
pipelinePanel.orientation = "column";
|
pipelinePanel.orientation = "column";
|
||||||
pipelinePanel.alignChildren = ["fill", "top"];
|
pipelinePanel.alignChildren = ["fill", "top"];
|
||||||
pipelinePanel.margins = 8;
|
pipelinePanel.margins = 8;
|
||||||
@@ -2767,7 +2984,7 @@ var HANDLE_FRAMES = 8;
|
|||||||
refreshExportButton = pipelineRow2.add("button", undefined, "Refresh Status");
|
refreshExportButton = pipelineRow2.add("button", undefined, "Refresh Status");
|
||||||
retryExportButton = pipelineRow2.add("button", undefined, "Retry Export");
|
retryExportButton = pipelineRow2.add("button", undefined, "Retry Export");
|
||||||
|
|
||||||
buttonsPanel = win.add("panel", undefined, "Actions");
|
buttonsPanel = scrollContent.add("panel", undefined, "Actions");
|
||||||
buttonsPanel.orientation = "column";
|
buttonsPanel.orientation = "column";
|
||||||
buttonsPanel.alignChildren = ["fill", "top"];
|
buttonsPanel.alignChildren = ["fill", "top"];
|
||||||
buttonsPanel.margins = 8;
|
buttonsPanel.margins = 8;
|
||||||
@@ -2837,21 +3054,21 @@ var HANDLE_FRAMES = 8;
|
|||||||
btnRow7.spacing = 6;
|
btnRow7.spacing = 6;
|
||||||
incrementVersionButton = btnRow7.add("button", undefined, "Increment Shot Version");
|
incrementVersionButton = btnRow7.add("button", undefined, "Increment Shot Version");
|
||||||
|
|
||||||
workspacePanel = win.add("panel", undefined, "Workspace");
|
workspacePanel = scrollContent.add("panel", undefined, "Workspace");
|
||||||
workspacePanel.orientation = "column";
|
workspacePanel.orientation = "column";
|
||||||
workspacePanel.alignChildren = ["fill", "top"];
|
workspacePanel.alignChildren = ["fill", "top"];
|
||||||
workspacePanel.margins = 8;
|
workspacePanel.margins = 8;
|
||||||
workspacePanel.spacing = 4;
|
workspacePanel.spacing = 4;
|
||||||
initWorkspaceButton = workspacePanel.add("button", undefined, "Initialise Workspace");
|
initWorkspaceButton = workspacePanel.add("button", undefined, "Initialise Workspace");
|
||||||
|
|
||||||
colorSpacePanel = win.add("panel", undefined, "Color Space");
|
colorSpacePanel = scrollContent.add("panel", undefined, "Color Space");
|
||||||
colorSpacePanel.orientation = "column";
|
colorSpacePanel.orientation = "column";
|
||||||
colorSpacePanel.alignChildren = ["fill", "top"];
|
colorSpacePanel.alignChildren = ["fill", "top"];
|
||||||
colorSpacePanel.margins = 8;
|
colorSpacePanel.margins = 8;
|
||||||
colorSpacePanel.spacing = 4;
|
colorSpacePanel.spacing = 4;
|
||||||
ocioButton = colorSpacePanel.add("button", undefined, "Add OCIO to Footage Layers");
|
ocioButton = colorSpacePanel.add("button", undefined, "Add OCIO to Footage Layers");
|
||||||
|
|
||||||
statusPanel = win.add("panel", undefined, "Status");
|
statusPanel = scrollContent.add("panel", undefined, "Status");
|
||||||
statusPanel.orientation = "column";
|
statusPanel.orientation = "column";
|
||||||
statusPanel.alignChildren = ["fill", "top"];
|
statusPanel.alignChildren = ["fill", "top"];
|
||||||
statusPanel.margins = 10;
|
statusPanel.margins = 10;
|
||||||
@@ -2859,7 +3076,26 @@ var HANDLE_FRAMES = 8;
|
|||||||
uiState.selectionText = statusPanel.add("statictext", undefined, "0 comps selected");
|
uiState.selectionText = statusPanel.add("statictext", undefined, "0 comps selected");
|
||||||
uiState.lastActionText = statusPanel.add("statictext", undefined, "Ready");
|
uiState.lastActionText = statusPanel.add("statictext", undefined, "Ready");
|
||||||
|
|
||||||
|
uiState.projectDropdown.onChange = function () {
|
||||||
|
if (uiState.projectDropdown.selection) {
|
||||||
|
CURRENT_PROJECT = uiState.projectDropdown.selection.projectData;
|
||||||
|
if (CURRENT_PROJECT) {
|
||||||
|
CONFIG.lastProjectCode = CURRENT_PROJECT.code;
|
||||||
|
saveConfig();
|
||||||
|
}
|
||||||
|
populateEpisodeOrSceneDropdown();
|
||||||
|
}
|
||||||
|
};
|
||||||
uiState.episodeDropdown.onChange = populateShotDropdown;
|
uiState.episodeDropdown.onChange = populateShotDropdown;
|
||||||
|
// ScriptUI silently swallows exceptions thrown inside onClick handlers.
|
||||||
|
settingsBtn.onClick = function () {
|
||||||
|
try {
|
||||||
|
openSettingsDialog();
|
||||||
|
} catch (settingsError) {
|
||||||
|
alert("VFXReview Connector \u2014 Settings error:\n" + settingsError.toString() +
|
||||||
|
(settingsError.line ? "\nLine " + settingsError.line : ""));
|
||||||
|
}
|
||||||
|
};
|
||||||
shotBuilderButton.onClick = buildShotFromDropdown;
|
shotBuilderButton.onClick = buildShotFromDropdown;
|
||||||
queueExportButton.onClick = queueExport;
|
queueExportButton.onClick = queueExport;
|
||||||
refreshExportButton.onClick = refreshExportStatus;
|
refreshExportButton.onClick = refreshExportStatus;
|
||||||
@@ -2883,8 +3119,24 @@ var HANDLE_FRAMES = 8;
|
|||||||
|
|
||||||
win.layout.layout(true);
|
win.layout.layout(true);
|
||||||
win.layout.resize();
|
win.layout.resize();
|
||||||
|
|
||||||
|
var _contentH = scrollContent.size[1];
|
||||||
|
vScrollbar.maxvalue = Math.max(0, _contentH - scrollRow.size[1]);
|
||||||
|
vScrollbar.stepdelta = 20;
|
||||||
|
vScrollbar.jumpdelta = 80;
|
||||||
|
|
||||||
|
vScrollbar.onChanging = function () {
|
||||||
|
scrollContent.location.y = -Math.round(vScrollbar.value);
|
||||||
|
};
|
||||||
|
|
||||||
win.onResizing = win.onResize = function () {
|
win.onResizing = win.onResize = function () {
|
||||||
this.layout.resize();
|
this.layout.resize();
|
||||||
|
var visible = scrollRow.size[1];
|
||||||
|
vScrollbar.maxvalue = Math.max(0, _contentH - visible);
|
||||||
|
if (vScrollbar.value > vScrollbar.maxvalue) {
|
||||||
|
vScrollbar.value = vScrollbar.maxvalue;
|
||||||
|
}
|
||||||
|
scrollContent.location.y = -Math.round(vScrollbar.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
return win;
|
return win;
|
||||||
@@ -2893,9 +3145,10 @@ var HANDLE_FRAMES = 8;
|
|||||||
// app.scheduleTask evaluates in global scope — expose the poll tick there
|
// app.scheduleTask evaluates in global scope — expose the poll tick there
|
||||||
$.global.__vfxrExportPollTick = pollExportTick;
|
$.global.__vfxrExportPollTick = pollExportTick;
|
||||||
|
|
||||||
|
loadConfig();
|
||||||
var panel = buildUI(thisObj);
|
var panel = buildUI(thisObj);
|
||||||
refreshSelection();
|
refreshSelection();
|
||||||
populateEpisodeDropdown();
|
populateProjectDropdown();
|
||||||
|
|
||||||
if (panel instanceof Window) {
|
if (panel instanceof Window) {
|
||||||
panel.center();
|
panel.center();
|
||||||
|
|||||||
+95
-1
@@ -6,7 +6,101 @@ A dockable ScriptUI panel for Adobe After Effects 2024+ that integrates with the
|
|||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
At the top of the script, four global variables control how the panel connects to your project:
|
Settings are stored per-machine via the **⚙ Settings** button in the panel (persisted in After Effects' built-in preferences — no need to edit the JSX file).
|
||||||
|
|
||||||
|
| Setting | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| **Server URL** | Base URL of the VFXReview server (e.g. `https://review.twotalesvfx.com`) |
|
||||||
|
| **API Token** | Bearer token matching `API_SECRET_KEY` in the server `.env` |
|
||||||
|
| **Export Root** | Local path where renders and deliverables are written |
|
||||||
|
| **Footage Root** | Local path where EXR plate folders are read from |
|
||||||
|
| **Banner Path** | Path to the panel banner image |
|
||||||
|
| **Artist Email** | Used for render attribution in the pipeline |
|
||||||
|
| **Handle Frames** | Plate handle frames prepended before the shot's source TC in-point (default: 8) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pipeline Folder Structure
|
||||||
|
|
||||||
|
### Export Root
|
||||||
|
|
||||||
|
All outputs from the connector are written under **`<Export Root>/{SHOW_ID}/`** (e.g. `V:/_EXPORTS`). The show ID is appended automatically from the selected project — configure `exportRoot` as the bare root without a show ID.
|
||||||
|
|
||||||
|
```
|
||||||
|
<Export Root>/
|
||||||
|
└── {SHOW_ID}/ # e.g. UNG/
|
||||||
|
├── {SHOT_CODE}/ # e.g. UNG_108_001_010/
|
||||||
|
│ ├── {shot}_cmp_TT_{ver}.[#####].exr # Queue EXR / Queue EXR (Review)
|
||||||
|
│ └── {shot}_cmp_TT_{ver}.mp4 / .mov # Queue MP4 / Queue MOV
|
||||||
|
│
|
||||||
|
├── DELIVERY/
|
||||||
|
│ └── {EPISODE_OR_SCENE}/ # e.g. UNG_108/
|
||||||
|
│ └── {YYYYMMDD}_Delivery/
|
||||||
|
│ └── {SHOT_CODE}/
|
||||||
|
│ └── {shot}_cmp_TT_{ver}.[#####].exr
|
||||||
|
│
|
||||||
|
└── (render farm outputs — managed by the server)
|
||||||
|
└── {PROJECT_CODE}/{EPISODE}/{SHOT_CODE}/{ver}/
|
||||||
|
└── {shot}_cmp_TT_{ver}.[#####].exr
|
||||||
|
```
|
||||||
|
|
||||||
|
**Version naming:** `{SHOT_CODE}_cmp_TT_{version}` — e.g. `UNG_108_001_010_cmp_TT_v003.[01001].exr`
|
||||||
|
|
||||||
|
Versions are set manually via **Increment Shot Version** or the shot detail page. Queuing a render does **not** auto-increment the version.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Footage Root
|
||||||
|
|
||||||
|
Source EXR plates are read from **`<Footage Root>/{SHOW_ID}/`**. Configure `footageRoot` as the bare root (e.g. `V:/_FOOTAGE`).
|
||||||
|
|
||||||
|
```
|
||||||
|
<Footage Root>/
|
||||||
|
└── {SHOW_ID}/ # e.g. UNG/
|
||||||
|
├── {EPISODE_CODE}/ # episodic: e.g. UNG_108/
|
||||||
|
│ └── {SHOT_CODE}_{CLIP}/ # e.g. UNG_108_001_010_A315C001/
|
||||||
|
│ └── *.exr # EXR sequence
|
||||||
|
│
|
||||||
|
├── {SCENE}/ # standard: e.g. 001/
|
||||||
|
│ └── {SHOT_CODE}_{CLIP}/
|
||||||
|
│ └── *.exr
|
||||||
|
│
|
||||||
|
└── _PICLOCKS/
|
||||||
|
└── {EPISODE_CODE}_*.mov # matched by episode prefix
|
||||||
|
```
|
||||||
|
|
||||||
|
Sequence folders must start with the shot code followed by an underscore (`{SHOT_CODE}_*`). The connector imports all matching folders as separate footage layers.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Episodic vs. Standard productions
|
||||||
|
|
||||||
|
| | Episodic | Standard |
|
||||||
|
|---|---|---|
|
||||||
|
| Shot code format | `{SHOW}_{EP}_{SCENE}_{NUM}` | `{SHOW}_{SCENE}_{NUM}` |
|
||||||
|
| Shot Builder | Episode dropdown → Shot dropdown | Scene dropdown → Shot dropdown |
|
||||||
|
| Footage path | `{footageRoot}/{showId}/{EPISODE}/` | `{footageRoot}/{showId}/{SCENE}/` |
|
||||||
|
| Export path | `{exportRoot}/{showId}/{SHOT_CODE}/` | same |
|
||||||
|
| Delivery subfolder | `DELIVERY/{EPISODE}/…` | `DELIVERY/{SCENE}/…` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Migrating an existing project (e.g. UNG)
|
||||||
|
|
||||||
|
If you previously set `exportRoot = V:/_EXPORTS/UNG`, update it to `V:/_EXPORTS`. The code now appends `/{showId}/` automatically, so `V:/_EXPORTS/UNG/…` is still produced — no files need to move.
|
||||||
|
|
||||||
|
Same applies to `footageRoot`: change `V:/_FOOTAGE/UNG` → `V:/_FOOTAGE`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## New production checklist
|
||||||
|
|
||||||
|
1. Create `<Footage Root>/{SHOW_ID}/{EPISODE or SCENE}/` and place EXR sequences inside, named `{SHOT_CODE}_{clip}`.
|
||||||
|
2. `<Export Root>/{SHOW_ID}/` is created automatically on first render.
|
||||||
|
3. Add the project to the VFXReview server with the correct **Show ID** set.
|
||||||
|
4. Open the panel in After Effects, click **⚙**, enter the Server URL, API Token, bare `exportRoot`, and bare `footageRoot`, then click **Save**.
|
||||||
|
5. The **Project** dropdown will populate automatically. Select the production and the episode/scene list will load.
|
||||||
|
|
||||||
|
|
||||||
| Variable | Default | Description |
|
| Variable | Default | Description |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ export async function GET(
|
|||||||
const { searchParams } = new URL(req.url);
|
const { searchParams } = new URL(req.url);
|
||||||
|
|
||||||
const episode = searchParams.get("episode")?.trim() || undefined;
|
const episode = searchParams.get("episode")?.trim() || undefined;
|
||||||
|
const scene = searchParams.get("scene")?.trim() || undefined;
|
||||||
const sequence = searchParams.get("sequence")?.trim() || undefined;
|
const sequence = searchParams.get("sequence")?.trim() || undefined;
|
||||||
const status = searchParams.get("status")?.trim() || undefined;
|
const status = searchParams.get("status")?.trim() || undefined;
|
||||||
const page = Math.max(1, parseInt(searchParams.get("page") ?? "1", 10));
|
const page = Math.max(1, parseInt(searchParams.get("page") ?? "1", 10));
|
||||||
@@ -98,6 +99,7 @@ export async function GET(
|
|||||||
where: {
|
where: {
|
||||||
projectId: project.id,
|
projectId: project.id,
|
||||||
...(episode ? { episode } : {}),
|
...(episode ? { episode } : {}),
|
||||||
|
...(scene ? { scene } : {}),
|
||||||
...(sequence ? { sequence } : {}),
|
...(sequence ? { sequence } : {}),
|
||||||
...(status ? { status: status as never } : {}),
|
...(status ? { status: status as never } : {}),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export const PIPELINE_CONFIG_DEFAULTS = {
|
|||||||
"render.maxAttempts": 3,
|
"render.maxAttempts": 3,
|
||||||
"render.stallTimeoutSeconds": 600,
|
"render.stallTimeoutSeconds": 600,
|
||||||
"render.urgentPriorityThreshold": 20,
|
"render.urgentPriorityThreshold": 20,
|
||||||
"render.outputRoot": "//SAN/renders",
|
"render.outputRoot": "//SAN",
|
||||||
"validation.sampleEvery": 25,
|
"validation.sampleEvery": 25,
|
||||||
"preview.maxWidth": 1920,
|
"preview.maxWidth": 1920,
|
||||||
// Preview stage (§9): headless AE rebuild of the render with slate/burn-ins
|
// Preview stage (§9): headless AE rebuild of the render with slate/burn-ins
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ export async function createExport(input: CreateExportInput) {
|
|||||||
const shot = await db.shot.findUnique({
|
const shot = await db.shot.findUnique({
|
||||||
where: { id: manifest.shotId },
|
where: { id: manifest.shotId },
|
||||||
include: {
|
include: {
|
||||||
project: { select: { id: true, code: true, showId: true } },
|
project: { select: { id: true, code: true, showId: true, projectType: true } },
|
||||||
tasks: { where: { type: "COMP" }, orderBy: { sortOrder: "asc" }, take: 1, select: { id: true } },
|
tasks: { where: { type: "COMP" }, orderBy: { sortOrder: "asc" }, take: 1, select: { id: true } },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -150,9 +150,15 @@ export async function createExport(input: CreateExportInput) {
|
|||||||
const versionString = freshShot.shotVersion ?? formatVersionString(versionNumber);
|
const versionString = freshShot.shotVersion ?? formatVersionString(versionNumber);
|
||||||
const exrBase = nextExrOutputBase(freshShot.exrOutput, shot.shotCode, versionString);
|
const exrBase = nextExrOutputBase(freshShot.exrOutput, shot.shotCode, versionString);
|
||||||
|
|
||||||
|
// {root}/{showId}/[{episode} for episodic]/{shotCode}/{version}
|
||||||
|
const showSegment = shot.project.showId || shot.project.code;
|
||||||
|
const episodeSegment =
|
||||||
|
shot.project.projectType === "EPISODIC" && shot.episode?.trim() ? shot.episode.trim() : null;
|
||||||
const outputDir =
|
const outputDir =
|
||||||
manifest.outputDir === "auto"
|
manifest.outputDir === "auto"
|
||||||
? [outputRoot, shot.project.code, ...(shot.episode ? [shot.episode] : []), shot.shotCode, versionString].join("/")
|
? [outputRoot, showSegment, episodeSegment, shot.shotCode, versionString]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join("/")
|
||||||
: manifest.outputDir;
|
: manifest.outputDir;
|
||||||
const outputPattern =
|
const outputPattern =
|
||||||
manifest.outputPattern === "auto"
|
manifest.outputPattern === "auto"
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
-- Export paths are {outputRoot}/{showId}/[{episode}]/{shotCode}/{version}.
|
||||||
|
-- A stored "render.outputRoot" of "//SAN/renders" injected a stray "renders/"
|
||||||
|
-- segment; strip it so the root is just the SAN mount point.
|
||||||
|
UPDATE "system_config"
|
||||||
|
SET "value" = regexp_replace("value", '/+renders/*$', '')
|
||||||
|
WHERE "key" = 'render.outputRoot'
|
||||||
|
AND "value" ~ '/+renders/*$';
|
||||||
Reference in New Issue
Block a user