From 136af34a7189e78fd8549ffe029285283b7997c8 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 22 May 2023 10:45:20 +0200 Subject: [PATCH] AfterEffects: set frame range and resolution (#4983) * OP-5660 - adding menu buttons to Set frame range in AE * OP-5660 - refactored location of scripts set_settings should be in lib as it is used elsewhere, but launch_logic and lib created circular dependency. Moved main to launch logic as it is actually handling launching. * OP-5660 - added set_settings to creator When instance gets created, set frame range and resolution from DB. * OP-5660 - minor fix * OP-5660 - updated extension zip * OP-5660 - updated documentation * OP-5660 - fixed missing exception * OP-5660 - fixed argument * OP-5560 - fix imports * OP-5660 - updated extension * OP-5660 - add js alert message for buttons * OP-5660 - repacked extension Without Anastasyi showed success, but extension wasn't loaded. * OP-5660 - make log message nicer * OP-5660 - added log if workfile not saved * OP-5660 - provide defaults to limit None exception * OP-5660 - updated error message --- openpype/hosts/aftereffects/api/__init__.py | 10 +- openpype/hosts/aftereffects/api/extension.zxp | Bin 101426 -> 101866 bytes .../api/extension/CSXS/manifest.xml | 4 +- .../aftereffects/api/extension/index.html | 72 +++++++--- .../aftereffects/api/extension/js/main.js | 56 +++++--- .../api/extension/jsx/hostscript.jsx | 134 ++++++++++++------ .../hosts/aftereffects/api/launch_logic.py | 86 ++++++++--- openpype/hosts/aftereffects/api/lib.py | 118 ++++++++------- openpype/hosts/aftereffects/api/pipeline.py | 6 +- openpype/hosts/aftereffects/api/ws_stub.py | 65 ++++++--- .../plugins/create/create_render.py | 19 ++- .../plugins/publish/collect_render.py | 16 +-- openpype/scripts/non_python_host_launch.py | 3 +- website/docs/artist_hosts_aftereffects.md | 37 ++++- .../docs/assets/aftereffects_extension.png | Bin 0 -> 12533 bytes 15 files changed, 414 insertions(+), 212 deletions(-) create mode 100644 website/docs/assets/aftereffects_extension.png diff --git a/openpype/hosts/aftereffects/api/__init__.py b/openpype/hosts/aftereffects/api/__init__.py index a7137ba8fb..28062cc35d 100644 --- a/openpype/hosts/aftereffects/api/__init__.py +++ b/openpype/hosts/aftereffects/api/__init__.py @@ -4,9 +4,8 @@ Anything that isn't defined here is INTERNAL and unreliable for external use. """ -from .launch_logic import ( +from .ws_stub import ( get_stub, - stub, ) from .pipeline import ( @@ -18,7 +17,8 @@ from .pipeline import ( from .lib import ( maintained_selection, get_extension_manifest_path, - get_asset_settings + get_asset_settings, + set_settings ) from .plugin import ( @@ -27,9 +27,8 @@ from .plugin import ( __all__ = [ - # launch_logic + # ws_stub "get_stub", - "stub", # pipeline "ls", @@ -39,6 +38,7 @@ __all__ = [ "maintained_selection", "get_extension_manifest_path", "get_asset_settings", + "set_settings", # plugin "AfterEffectsLoader" diff --git a/openpype/hosts/aftereffects/api/extension.zxp b/openpype/hosts/aftereffects/api/extension.zxp index b436f0ca0b67313cf4509d1ec5e7d4a1dfd2d106..50fda416f806515e8a6e90e745153e341f73c7b2 100644 GIT binary patch delta 14164 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 XY6T7RTV3K`-(5HW;4}pI>(~DQ$dj(< delta 13616 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 - + diff --git a/openpype/hosts/aftereffects/api/extension/index.html b/openpype/hosts/aftereffects/api/extension/index.html index 52a7c4964f..291965559f 100644 --- a/openpype/hosts/aftereffects/api/extension/index.html +++ b/openpype/hosts/aftereffects/api/extension/index.html @@ -2,7 +2,7 @@ - + @@ -25,11 +25,11 @@ - + - + - + - + - + + + + + + + - - + + + - - + @@ -107,6 +143,6 @@ - + - \ No newline at end of file + diff --git a/openpype/hosts/aftereffects/api/extension/js/main.js b/openpype/hosts/aftereffects/api/extension/js/main.js index bb0f3b1f0c..ffc41f0937 100644 --- a/openpype/hosts/aftereffects/api/extension/js/main.js +++ b/openpype/hosts/aftereffects/api/extension/js/main.js @@ -4,7 +4,7 @@ indent: 4, maxerr: 50 */ var csInterface = new CSInterface(); - + log.warn("script start"); WSRPC.DEBUG = false; @@ -14,7 +14,7 @@ WSRPC.TRACE = false; async function startUp(url){ promis = runEvalScript("getEnv('" + url + "')"); - var res = await promis; + var res = await promis; log.warn("res: " + res); promis = runEvalScript("getEnv('OPENPYPE_DEBUG')"); @@ -56,7 +56,7 @@ function get_extension_version(){ } function main(websocket_url){ - // creates connection to 'websocket_url', registers routes + // creates connection to 'websocket_url', registers routes var default_url = 'ws://localhost:8099/ws/'; if (websocket_url == ''){ @@ -66,7 +66,7 @@ function main(websocket_url){ RPC.connect(); - log.warn("connected"); + log.warn("connected"); RPC.addRoute('AfterEffects.open', function (data) { log.warn('Server called client route "open":', data); @@ -88,7 +88,7 @@ function main(websocket_url){ }); RPC.addRoute('AfterEffects.get_active_document_name', function (data) { - log.warn('Server called client route ' + + log.warn('Server called client route ' + '"get_active_document_name":', data); return runEvalScript("getActiveDocumentName()") .then(function(result){ @@ -98,7 +98,7 @@ function main(websocket_url){ }); RPC.addRoute('AfterEffects.get_active_document_full_name', function (data){ - log.warn('Server called client route ' + + log.warn('Server called client route ' + '"get_active_document_full_name":', data); return runEvalScript("getActiveDocumentFullName()") .then(function(result){ @@ -118,7 +118,7 @@ function main(websocket_url){ }); }); - + RPC.addRoute('AfterEffects.get_selected_items', function (data) { log.warn('Server called client route "get_selected_items":', data); return runEvalScript("getSelectedItems(" + data.comps + "," + @@ -194,23 +194,25 @@ function main(websocket_url){ }); }); - RPC.addRoute('AfterEffects.get_work_area', function (data) { - log.warn('Server called client route "get_work_area":', data); - return runEvalScript("getWorkArea(" + data.item_id + ")") + RPC.addRoute('AfterEffects.get_comp_properties', function (data) { + log.warn('Server called client route "get_comp_properties":', data); + return runEvalScript("getCompProperties(" + data.item_id + ")") .then(function(result){ - log.warn("getWorkArea: " + result); + log.warn("get_comp_properties: " + result); return result; }); }); - RPC.addRoute('AfterEffects.set_work_area', function (data) { + RPC.addRoute('AfterEffects.set_comp_properties', function (data) { log.warn('Server called client route "set_work_area":', data); - return runEvalScript("setWorkArea(" + data.item_id + ',' + + return runEvalScript("setCompProperties(" + data.item_id + ',' + data.start + ',' + data.duration + ',' + - data.frame_rate + ")") + data.frame_rate + ',' + + data.width + ',' + + data.height + ")") .then(function(result){ - log.warn("getWorkArea: " + result); + log.warn("set_comp_properties: " + result); return result; }); }); @@ -255,7 +257,7 @@ function main(websocket_url){ RPC.addRoute('AfterEffects.import_background', function (data) { log.warn('Server called client route "import_background":', data); - return runEvalScript("importBackground(" + data.comp_id + ", " + + return runEvalScript("importBackground(" + data.comp_id + ", " + "'" + data.comp_name + "', " + JSON.stringify(data.files) + ")") .then(function(result){ @@ -266,7 +268,7 @@ function main(websocket_url){ RPC.addRoute('AfterEffects.reload_background', function (data) { log.warn('Server called client route "reload_background":', data); - return runEvalScript("reloadBackground(" + data.comp_id + ", " + + return runEvalScript("reloadBackground(" + data.comp_id + ", " + "'" + data.comp_name + "', " + JSON.stringify(data.files) + ")") .then(function(result){ @@ -314,6 +316,16 @@ function main(websocket_url){ log.warn('Server called client route "close":', data); return runEvalScript("close()"); }); + + RPC.addRoute('AfterEffects.print_msg', function (data) { + log.warn('Server called client route "print_msg":', data); + var escaped_msg = EscapeStringForJSX(data.msg); + return runEvalScript("printMsg('" + escaped_msg +"')") + .then(function(result){ + log.warn("print_msg: " + result); + return result; + }); + }); } /** main entry point **/ @@ -323,17 +335,17 @@ startUp("WEBSOCKET_URL"); 'use strict'; var csInterface = new CSInterface(); - - + + function init() { - + themeManager.init(); - + $("#btn_test").click(function () { csInterface.evalScript('sayHello()'); }); } - + init(); }()); diff --git a/openpype/hosts/aftereffects/api/extension/jsx/hostscript.jsx b/openpype/hosts/aftereffects/api/extension/jsx/hostscript.jsx index 5c1d163439..7d0b20bbb4 100644 --- a/openpype/hosts/aftereffects/api/extension/jsx/hostscript.jsx +++ b/openpype/hosts/aftereffects/api/extension/jsx/hostscript.jsx @@ -1,7 +1,7 @@ /*jslint vars: true, plusplus: true, devel: true, nomen: true, regexp: true, indent: 4, maxerr: 50 */ /*global $, Folder*/ -#include "../js/libs/json.js"; +//@include "../js/libs/json.js" /* All public API function should return JSON! */ @@ -29,13 +29,13 @@ function getEnv(variable){ function getMetadata(){ /** * Returns payload in 'Label' field of project's metadata - * + * **/ if (ExternalObject.AdobeXMPScript === undefined){ ExternalObject.AdobeXMPScript = new ExternalObject('lib:AdobeXMPScript'); } - + var proj = app.project; var meta = new XMPMeta(app.project.xmpPacket); var schemaNS = XMPMeta.getNamespaceURI("xmp"); @@ -53,7 +53,7 @@ function getMetadata(){ function imprint(payload){ /** * Stores payload in 'Label' field of project's metadata - * + * * Args: * payload (string): json content */ @@ -61,14 +61,14 @@ function imprint(payload){ ExternalObject.AdobeXMPScript = new ExternalObject('lib:AdobeXMPScript'); } - + var proj = app.project; var meta = new XMPMeta(app.project.xmpPacket); var schemaNS = XMPMeta.getNamespaceURI("xmp"); var label = "xmp:Label"; meta.setProperty(schemaNS, label, payload); - + app.project.xmpPacket = meta.serialize(); } @@ -116,14 +116,14 @@ function getItems(comps, folders, footages){ /** * Returns JSON representation of compositions and * if 'collectLayers' then layers in comps too. - * + * * Args: * comps (bool): return selected compositions * folders (bool): return folders * footages (bool): return FootageItem * Returns: * (list) of JSON items - */ + */ var items = [] for (i = 1; i <= app.project.items.length; ++i){ var item = app.project.items[i]; @@ -142,14 +142,14 @@ function getItems(comps, folders, footages){ function getSelectedItems(comps, folders, footages){ /** * Returns list of selected items from Project menu - * + * * Args: * comps (bool): return selected compositions * folders (bool): return folders * footages (bool): return FootageItem * Returns: * (list) of JSON items - */ + */ var items = [] for (i = 0; i < app.project.selection.length; ++i){ var item = app.project.selection[i]; @@ -166,9 +166,9 @@ function getSelectedItems(comps, folders, footages){ function _getItem(item, comps, folders, footages){ /** - * Auxiliary function as project items and selections + * Auxiliary function as project items and selections * are indexed in different way :/ - * Refactor + * Refactor */ var item_type = ''; if (item instanceof FolderItem){ @@ -189,7 +189,7 @@ function _getItem(item, comps, folders, footages){ return "{}"; } } - + var item = {"name": item.name, "id": item.id, "type": item_type}; @@ -200,7 +200,7 @@ function importFile(path, item_name, import_options){ /** * Imports file (image tested for now) as a FootageItem. * Creates new composition - * + * * Args: * path (string): absolute path to image file * item_name (string): label for composition @@ -218,7 +218,7 @@ function importFile(path, item_name, import_options){ app.beginUndoGroup("Import File"); fp = new File(path); if (fp.exists){ - try { + try { im_opt = new ImportOptions(fp); importAsType = import_options["ImportAsType"]; @@ -234,18 +234,18 @@ function importFile(path, item_name, import_options){ } if (importAsType.indexOf('PROJECT') > 0){ im_opt.importAs = ImportAsType.PROJECT; - } - + } + } if ('sequence' in import_options){ im_opt.sequence = true; } - + comp = app.project.importFile(im_opt); if (app.project.selection.length == 2 && app.project.selection[0] instanceof FolderItem){ - comp.parentFolder = app.project.selection[0] + comp.parentFolder = app.project.selection[0] } } catch (error) { return _prepareError(error.toString() + importOptions.file.fsName); @@ -283,14 +283,14 @@ function setLabelColor(comp_id, color_idx){ function replaceItem(comp_id, path, item_name){ /** * Replaces loaded file with new file and updates name - * + * * Args: * comp_id (int): id of composition, not a index! * path (string): absolute path to new file * item_name (string): new composition name */ app.beginUndoGroup("Replace File"); - + fp = new File(path); if (!fp.exists){ return _prepareError("File " + path + " not found."); @@ -303,7 +303,7 @@ function replaceItem(comp_id, path, item_name){ }else{ item.replace(fp); } - + item.name = item_name; } catch (error) { return _prepareError(error.toString() + path); @@ -319,7 +319,7 @@ function replaceItem(comp_id, path, item_name){ function renameItem(item_id, new_name){ /** * Renames item with 'item_id' to 'new_name' - * + * * Args: * item_id (int): id to search item * new_name (str) @@ -335,7 +335,7 @@ function renameItem(item_id, new_name){ function deleteItem(item_id){ /** * Delete any 'item_id' - * + * * Not restricted only to comp, it could delete * any item with 'id' */ @@ -347,38 +347,76 @@ function deleteItem(item_id){ } } -function getWorkArea(comp_id){ +function getCompProperties(comp_id){ /** - * Returns information about workarea - are that will be - * rendered. All calculation will be done in OpenPype, - * easier to modify without redeploy of extension. - * + * Returns information about composition - are that will be + * rendered. + * * Returns * (dict) */ - var item = app.project.itemByID(comp_id); - if (item){ - return JSON.stringify({ - "workAreaStart": item.displayStartFrame, - "workAreaDuration": item.duration, - "frameRate": item.frameRate}); - }else{ + var comp = app.project.itemByID(comp_id); + if (!comp){ return _prepareError("There is no composition with "+ comp_id); } + + return JSON.stringify({ + "id": comp.id, + "name": comp.name, + "frameStart": comp.displayStartFrame, + "framesDuration": comp.duration * comp.frameRate, + "frameRate": comp.frameRate, + "width": comp.width, + "height": comp.height}); } -function setWorkArea(comp_id, workAreaStart, workAreaDuration, frameRate){ +function setCompProperties(comp_id, frameStart, framesCount, frameRate, + width, height){ /** * Sets work area info from outside (from Ftrack via OpenPype) */ - var item = app.project.itemByID(comp_id); - if (item){ - item.displayStartTime = workAreaStart; - item.duration = workAreaDuration; - item.frameRate = frameRate; - }else{ + var comp = app.project.itemByID(comp_id); + if (!comp){ return _prepareError("There is no composition with "+ comp_id); } + + app.beginUndoGroup('change comp properties'); + if (frameStart && framesCount && frameRate){ + comp.displayStartFrame = frameStart; + comp.duration = framesCount / frameRate; + comp.frameRate = frameRate; + } + if (width && height){ + var widthOld = comp.width; + var widthNew = width; + var widthDelta = widthNew - widthOld; + + var heightOld = comp.height; + var heightNew = height; + var heightDelta = heightNew - heightOld; + + var offset = [widthDelta / 2, heightDelta / 2]; + + comp.width = widthNew; + comp.height = heightNew; + + for (var i = 1, il = comp.numLayers; i <= il; i++) { + var layer = comp.layer(i); + var positionProperty = layer.property('ADBE Transform Group').property('ADBE Position'); + + if (positionProperty.numKeys > 0) { + for (var j = 1, jl = positionProperty.numKeys; j <= jl; j++) { + var keyValue = positionProperty.keyValue(j); + positionProperty.setValueAtKey(j, keyValue + offset); + } + } else { + var positionValue = positionProperty.value; + positionProperty.setValue(positionValue + offset); + } + } + } + + app.endUndoGroup(); } function save(){ @@ -504,7 +542,7 @@ function addItemAsLayerToComp(comp_id, item_id, found_comp){ * Args: * comp_id (int): id of target composition * item_id (int): FootageItem.id - * found_comp (CompItem, optional): to limit querying if + * found_comp (CompItem, optional): to limit quering if * comp already found previously */ var comp = found_comp || app.project.itemByID(comp_id); @@ -749,7 +787,7 @@ function render(target_folder, comp_id){ var om1 = app.project.renderQueue.item(i).outputModule(1); var file_name = File.decode( om1.file.name ).replace('℗', ''); // Name contains special character, space? - + var omItem1_settable_str = app.project.renderQueue.item(i).outputModule(1).getSettings( GetSettingsFormat.STRING_SETTABLE ); var targetFolder = new Folder(target_folder); @@ -763,7 +801,7 @@ function render(target_folder, comp_id){ render_item.render = false; } } - + } app.beginSuppressDialogs(); app.project.renderQueue.render(); @@ -779,6 +817,10 @@ function getAppVersion(){ return _prepareSingleValue(app.version); } +function printMsg(msg){ + alert(msg); +} + function _prepareSingleValue(value){ return JSON.stringify({"result": value}) } diff --git a/openpype/hosts/aftereffects/api/launch_logic.py b/openpype/hosts/aftereffects/api/launch_logic.py index c428043d99..77c2b0b6ca 100644 --- a/openpype/hosts/aftereffects/api/launch_logic.py +++ b/openpype/hosts/aftereffects/api/launch_logic.py @@ -1,49 +1,77 @@ import os +import sys import subprocess import collections import logging import asyncio import functools +import traceback + from wsrpc_aiohttp import ( WebSocketRoute, WebSocketAsync ) -from qtpy import QtCore +from qtpy import QtCore, QtWidgets from openpype.lib import Logger -from openpype.pipeline import legacy_io from openpype.tools.utils import host_tools +from openpype.tests.lib import is_in_tests +from openpype.pipeline import install_host, legacy_io +from openpype.modules import ModulesManager from openpype.tools.adobe_webserver.app import WebServerTool -from .ws_stub import AfterEffectsServerStub +from .ws_stub import get_stub +from .lib import set_settings log = logging.getLogger(__name__) log.setLevel(logging.DEBUG) -class ConnectionNotEstablishedYet(Exception): - pass +def safe_excepthook(*args): + traceback.print_exception(*args) -def get_stub(): - """ - Convenience function to get server RPC stub to call methods directed - for host (Photoshop). - It expects already created connection, started from client. - Currently created when panel is opened (PS: Window>Extensions>Avalon) - :return: where functions could be called from - """ - ae_stub = AfterEffectsServerStub() - if not ae_stub.client: - raise ConnectionNotEstablishedYet("Connection is not created yet") +def main(*subprocess_args): + """Main entrypoint to AE launching, called from pre hook.""" + sys.excepthook = safe_excepthook - return ae_stub + from openpype.hosts.aftereffects.api import AfterEffectsHost + host = AfterEffectsHost() + install_host(host) -def stub(): - return get_stub() + os.environ["OPENPYPE_LOG_NO_COLORS"] = "False" + app = QtWidgets.QApplication([]) + app.setQuitOnLastWindowClosed(False) + + launcher = ProcessLauncher(subprocess_args) + launcher.start() + + if os.environ.get("HEADLESS_PUBLISH"): + manager = ModulesManager() + webpublisher_addon = manager["webpublisher"] + + launcher.execute_in_main_thread( + functools.partial( + webpublisher_addon.headless_publish, + log, + "CloseAE", + is_in_tests() + ) + ) + + elif os.environ.get("AVALON_PHOTOSHOP_WORKFILES_ON_LAUNCH", True): + save = False + if os.getenv("WORKFILES_SAVE_AS"): + save = True + + launcher.execute_in_main_thread( + lambda: host_tools.show_tool_by_name("workfiles", save=save) + ) + + sys.exit(app.exec_()) def show_tool_by_name(tool_name): @@ -55,6 +83,7 @@ def show_tool_by_name(tool_name): class ProcessLauncher(QtCore.QObject): + """Launches webserver, connects to it, runs main thread.""" route_name = "AfterEffects" _main_thread_callbacks = collections.deque() @@ -296,6 +325,15 @@ class AfterEffectsRoute(WebSocketRoute): async def sceneinventory_route(self): self._tool_route("sceneinventory") + async def setresolution_route(self): + self._settings_route(False, True) + + async def setframes_route(self): + self._settings_route(True, False) + + async def setall_route(self): + self._settings_route(True, True) + async def experimental_tools_route(self): self._tool_route("experimental_tools") @@ -309,3 +347,13 @@ class AfterEffectsRoute(WebSocketRoute): # Required return statement. return "nothing" + + def _settings_route(self, frames, resolution): + partial_method = functools.partial(set_settings, + frames, + resolution) + + ProcessLauncher.execute_in_main_thread(partial_method) + + # Required return statement. + return "nothing" diff --git a/openpype/hosts/aftereffects/api/lib.py b/openpype/hosts/aftereffects/api/lib.py index a39af5c81f..e8352c382b 100644 --- a/openpype/hosts/aftereffects/api/lib.py +++ b/openpype/hosts/aftereffects/api/lib.py @@ -1,69 +1,17 @@ import os -import sys import re import json import contextlib -import traceback import logging -from functools import partial -from qtpy import QtWidgets - -from openpype.pipeline import install_host -from openpype.modules import ModulesManager - -from openpype.tools.utils import host_tools -from openpype.tests.lib import is_in_tests -from .launch_logic import ProcessLauncher, get_stub +from openpype.pipeline.context_tools import get_current_context +from openpype.client import get_asset_by_name +from .ws_stub import get_stub log = logging.getLogger(__name__) log.setLevel(logging.DEBUG) -def safe_excepthook(*args): - traceback.print_exception(*args) - - -def main(*subprocess_args): - sys.excepthook = safe_excepthook - - from openpype.hosts.aftereffects.api import AfterEffectsHost - - host = AfterEffectsHost() - install_host(host) - - os.environ["OPENPYPE_LOG_NO_COLORS"] = "False" - app = QtWidgets.QApplication([]) - app.setQuitOnLastWindowClosed(False) - - launcher = ProcessLauncher(subprocess_args) - launcher.start() - - if os.environ.get("HEADLESS_PUBLISH"): - manager = ModulesManager() - webpublisher_addon = manager["webpublisher"] - - launcher.execute_in_main_thread( - partial( - webpublisher_addon.headless_publish, - log, - "CloseAE", - is_in_tests() - ) - ) - - elif os.environ.get("AVALON_PHOTOSHOP_WORKFILES_ON_LAUNCH", True): - save = False - if os.getenv("WORKFILES_SAVE_AS"): - save = True - - launcher.execute_in_main_thread( - lambda: host_tools.show_tool_by_name("workfiles", save=save) - ) - - sys.exit(app.exec_()) - - @contextlib.contextmanager def maintained_selection(): """Maintain selection during context.""" @@ -145,13 +93,13 @@ def get_asset_settings(asset_doc): """ asset_data = asset_doc["data"] - fps = asset_data.get("fps") - frame_start = asset_data.get("frameStart") - frame_end = asset_data.get("frameEnd") - handle_start = asset_data.get("handleStart") - handle_end = asset_data.get("handleEnd") - resolution_width = asset_data.get("resolutionWidth") - resolution_height = asset_data.get("resolutionHeight") + fps = asset_data.get("fps", 0) + frame_start = asset_data.get("frameStart", 0) + frame_end = asset_data.get("frameEnd", 0) + handle_start = asset_data.get("handleStart", 0) + handle_end = asset_data.get("handleEnd", 0) + resolution_width = asset_data.get("resolutionWidth", 0) + resolution_height = asset_data.get("resolutionHeight", 0) duration = (frame_end - frame_start + 1) + handle_start + handle_end return { @@ -164,3 +112,49 @@ def get_asset_settings(asset_doc): "resolutionHeight": resolution_height, "duration": duration } + + +def set_settings(frames, resolution, comp_ids=None, print_msg=True): + """Sets number of frames and resolution to selected comps. + + Args: + frames (bool): True if set frame info + resolution (bool): True if set resolution + comp_ids (list): specific composition ids, if empty + it tries to look for currently selected + print_msg (bool): True throw JS alert with msg + """ + frame_start = frames_duration = fps = width = height = None + current_context = get_current_context() + + asset_doc = get_asset_by_name(current_context["project_name"], + current_context["asset_name"]) + settings = get_asset_settings(asset_doc) + + msg = '' + if frames: + frame_start = settings["frameStart"] - settings["handleStart"] + frames_duration = settings["duration"] + fps = settings["fps"] + msg += f"frame start:{frame_start}, duration:{frames_duration}, "\ + f"fps:{fps}" + if resolution: + width = settings["resolutionWidth"] + height = settings["resolutionHeight"] + msg += f"width:{width} and height:{height}" + + stub = get_stub() + if not comp_ids: + comps = stub.get_selected_items(True, False, False) + comp_ids = [comp.id for comp in comps] + if not comp_ids: + stub.print_msg("Select at least one composition to apply settings.") + return + + for comp_id in comp_ids: + msg = f"Setting for comp {comp_id} " + msg + log.debug(msg) + stub.set_comp_properties(comp_id, frame_start, frames_duration, + fps, width, height) + if print_msg: + stub.print_msg(msg) diff --git a/openpype/hosts/aftereffects/api/pipeline.py b/openpype/hosts/aftereffects/api/pipeline.py index 020022e263..27aee8c7ce 100644 --- a/openpype/hosts/aftereffects/api/pipeline.py +++ b/openpype/hosts/aftereffects/api/pipeline.py @@ -8,10 +8,7 @@ from openpype.lib import Logger, register_event_callback from openpype.pipeline import ( register_loader_plugin_path, register_creator_plugin_path, - deregister_loader_plugin_path, - deregister_creator_plugin_path, AVALON_CONTAINER_ID, - legacy_io, ) from openpype.pipeline.load import any_outdated_containers import openpype.hosts.aftereffects @@ -23,7 +20,8 @@ from openpype.host import ( IPublishHost ) -from .launch_logic import get_stub, ConnectionNotEstablishedYet +from .launch_logic import get_stub +from .ws_stub import ConnectionNotEstablishedYet log = Logger.get_logger(__name__) diff --git a/openpype/hosts/aftereffects/api/ws_stub.py b/openpype/hosts/aftereffects/api/ws_stub.py index f094c7fa2a..576c997f49 100644 --- a/openpype/hosts/aftereffects/api/ws_stub.py +++ b/openpype/hosts/aftereffects/api/ws_stub.py @@ -11,6 +11,10 @@ from wsrpc_aiohttp import WebSocketAsync from openpype.tools.adobe_webserver.app import WebServerTool +class ConnectionNotEstablishedYet(Exception): + pass + + @attr.s class AEItem(object): """ @@ -24,8 +28,8 @@ class AEItem(object): # all imported elements, single for # regular image, array for Backgrounds members = attr.ib(factory=list) - workAreaStart = attr.ib(default=None) - workAreaDuration = attr.ib(default=None) + frameStart = attr.ib(default=None) + framesDuration = attr.ib(default=None) frameRate = attr.ib(default=None) file_name = attr.ib(default=None) instance_id = attr.ib(default=None) # New Publisher @@ -355,42 +359,50 @@ class AfterEffectsServerStub(): return self._handle_return(res) - def get_work_area(self, item_id): - """ Get work are information for render purposes + def get_comp_properties(self, comp_id): + """ Get composition information for render purposes + + Returns startFrame, frameDuration, fps, width, height. + Args: - item_id (int): + comp_id (int): Returns: (AEItem) """ res = self.websocketserver.call(self.client.call - ('AfterEffects.get_work_area', - item_id=item_id + ('AfterEffects.get_comp_properties', + item_id=comp_id )) records = self._to_records(self._handle_return(res)) if records: return records.pop() - def set_work_area(self, item, start, duration, frame_rate): + def set_comp_properties(self, comp_id, start, duration, frame_rate, + width, height): """ Set work area to predefined values (from Ftrack). Work area directs what gets rendered. Beware of rounding, AE expects seconds, not frames directly. Args: - item (dict): - start (float): workAreaStart in seconds - duration (float): in seconds + comp_id (int): + start (int): workAreaStart in frames + duration (int): in frames frame_rate (float): frames in seconds + width (int): resolution width + height (int): resolution height """ res = self.websocketserver.call(self.client.call - ('AfterEffects.set_work_area', - item_id=item.id, + ('AfterEffects.set_comp_properties', + item_id=comp_id, start=start, duration=duration, - frame_rate=frame_rate)) + frame_rate=frame_rate, + width=width, + height=height)) return self._handle_return(res) def save(self): @@ -554,6 +566,12 @@ class AfterEffectsServerStub(): return self._handle_return(res) + def print_msg(self, msg): + """Triggers Javascript alert dialog.""" + self.websocketserver.call(self.client.call + ('AfterEffects.print_msg', + msg=msg)) + def _handle_return(self, res): """Wraps return, throws ValueError if 'error' key is present.""" if res and isinstance(res, str) and res != "undefined": @@ -608,8 +626,8 @@ class AfterEffectsServerStub(): d.get('name'), d.get('type'), d.get('members'), - d.get('workAreaStart'), - d.get('workAreaDuration'), + d.get('frameStart'), + d.get('framesDuration'), d.get('frameRate'), d.get('file_name'), d.get("instance_id"), @@ -618,3 +636,18 @@ class AfterEffectsServerStub(): ret.append(item) return ret + + +def get_stub(): + """ + Convenience function to get server RPC stub to call methods directed + for host (Photoshop). + It expects already created connection, started from client. + Currently created when panel is opened (PS: Window>Extensions>Avalon) + :return: where functions could be called from + """ + ae_stub = AfterEffectsServerStub() + if not ae_stub.client: + raise ConnectionNotEstablishedYet("Connection is not created yet") + + return ae_stub diff --git a/openpype/hosts/aftereffects/plugins/create/create_render.py b/openpype/hosts/aftereffects/plugins/create/create_render.py index 171d7053ce..fa79fac78f 100644 --- a/openpype/hosts/aftereffects/plugins/create/create_render.py +++ b/openpype/hosts/aftereffects/plugins/create/create_render.py @@ -9,6 +9,7 @@ from openpype.pipeline import ( CreatorError ) from openpype.hosts.aftereffects.api.pipeline import cache_and_get_instances +from openpype.hosts.aftereffects.api.lib import set_settings from openpype.lib import prepare_template_data from openpype.pipeline.create import SUBSET_NAME_ALLOWED_SYMBOLS @@ -32,6 +33,14 @@ class RenderCreator(Creator): def create(self, subset_name_from_ui, data, pre_create_data): stub = api.get_stub() # only after After Effects is up + + try: + _ = stub.get_active_document_full_name() + except ValueError: + raise CreatorError( + "Please save workfile via Workfile app first!" + ) + if pre_create_data.get("use_selection"): comps = stub.get_selected_items( comps=True, folders=False, footages=False @@ -41,8 +50,8 @@ class RenderCreator(Creator): if not comps: raise CreatorError( - "Nothing to create. Select composition " - "if 'useSelection' or create at least " + "Nothing to create. Select composition in Project Bin if " + "'Use selection' is toggled or create at least " "one composition." ) use_composition_name = (pre_create_data.get("use_composition_name") or @@ -87,10 +96,14 @@ class RenderCreator(Creator): self._add_instance_to_context(new_instance) stub.rename_item(comp.id, subset_name) + set_settings(True, True, [comp.id], print_msg=False) def get_pre_create_attr_defs(self): output = [ - BoolDef("use_selection", default=True, label="Use selection"), + BoolDef("use_selection", + tooltip="Composition for publishable instance should be " + "selected by default.", + default=True, label="Use selection"), BoolDef("use_composition_name", label="Use composition name in subset"), UISeparatorDef(), diff --git a/openpype/hosts/aftereffects/plugins/publish/collect_render.py b/openpype/hosts/aftereffects/plugins/publish/collect_render.py index b01b707246..aa46461915 100644 --- a/openpype/hosts/aftereffects/plugins/publish/collect_render.py +++ b/openpype/hosts/aftereffects/plugins/publish/collect_render.py @@ -66,19 +66,19 @@ class CollectAERender(publish.AbstractCollectRender): comp_id = int(inst.data["members"][0]) - work_area_info = CollectAERender.get_stub().get_work_area(comp_id) + comp_info = CollectAERender.get_stub().get_comp_properties( + comp_id) - if not work_area_info: + if not comp_info: self.log.warning("Orphaned instance, deleting metadata") - inst_id = inst.get("instance_id") or str(comp_id) + inst_id = inst.data.get("instance_id") or str(comp_id) CollectAERender.get_stub().remove_instance(inst_id) continue - frame_start = work_area_info.workAreaStart - frame_end = round(work_area_info.workAreaStart + - float(work_area_info.workAreaDuration) * - float(work_area_info.frameRate)) - 1 - fps = work_area_info.frameRate + frame_start = comp_info.frameStart + frame_end = round(comp_info.frameStart + + comp_info.framesDuration) - 1 + fps = comp_info.frameRate # TODO add resolution when supported by extension task_name = inst.data.get("task") # legacy diff --git a/openpype/scripts/non_python_host_launch.py b/openpype/scripts/non_python_host_launch.py index 79fb1cbb52..c95a9df314 100644 --- a/openpype/scripts/non_python_host_launch.py +++ b/openpype/scripts/non_python_host_launch.py @@ -81,9 +81,10 @@ def main(argv): host_name = os.environ["AVALON_APP"].lower() if host_name == "photoshop": + # TODO refactor launch logic according to AE from openpype.hosts.photoshop.api.lib import main elif host_name == "aftereffects": - from openpype.hosts.aftereffects.api.lib import main + from openpype.hosts.aftereffects.api.launch_logic import main elif host_name == "harmony": from openpype.hosts.harmony.api.lib import main else: diff --git a/website/docs/artist_hosts_aftereffects.md b/website/docs/artist_hosts_aftereffects.md index d9522d5765..d415a1d47d 100644 --- a/website/docs/artist_hosts_aftereffects.md +++ b/website/docs/artist_hosts_aftereffects.md @@ -15,18 +15,18 @@ sidebar_label: AfterEffects ## Setup -To install the extension, download, install [Anastasyi's Extension Manager](https://install.anastasiy.com/). Open Anastasyi's Extension Manager and select AfterEffects in menu. Then go to `{path to pype}hosts/aftereffects/api/extension.zxp`. +To install the extension, download, install [Anastasyi's Extension Manager](https://install.anastasiy.com/). Open Anastasyi's Extension Manager and select AfterEffects in menu. Then go to `{path to pype}hosts/aftereffects/api/extension.zxp`. -Drag extension.zxp and drop it to Anastasyi's Extension Manager. The extension will install itself. +Drag extension.zxp and drop it to Anastasyi's Extension Manager. The extension will install itself. ## Implemented functionality AfterEffects implementation currently allows you to import and add various media to composition (image plates, renders, audio files, video files etc.) -and send prepared composition for rendering to Deadline or render locally. +and send prepared composition for rendering to Deadline or render locally. ## Usage -When you launch AfterEffects you will be met with the Workfiles app. If don't +When you launch AfterEffects you will be met with the Workfiles app. If don't have any previous workfiles, you can just close this window. Workfiles tools takes care of saving your .AEP files in the correct location and under @@ -34,7 +34,7 @@ a correct name. You should use it instead of standard file saving dialog. In AfterEffects you'll find the tools in the `OpenPype` extension: -![Extension](assets/photoshop_extension.png) +![Extension](assets/photoshop_extension.png) You can show the extension panel by going to `Window` > `Extensions` > `OpenPype`. @@ -58,6 +58,9 @@ Name of publishable instance (eg. subset name) could be configured with a templa Trash icon under the list of instances allows to delete any selected `render` instance. +Frame information (frame start, duration, fps) and resolution (width and height) is applied to selected composition from Asset Management System (Ftrack or DB) automatically! +(Eg. number of rendered frames is controlled by settings inserted from supervisor. Artist can override this by disabling validation only in special cases.) + Workfile instance will be automatically recreated though. If you do not want to publish it, use pill toggle on the instance item. If you would like to modify publishable instance, click on `Publish` tab at the top. This would allow you to change name of publishable @@ -67,7 +70,7 @@ Publisher allows publishing into different context, just click on any instance, #### RenderQueue -AE's Render Queue is required for publishing locally or on a farm. Artist needs to configure expected result format (extension, resolution) in the Render Queue in an Output module. +AE's Render Queue is required for publishing locally or on a farm. Artist needs to configure expected result format (extension, resolution) in the Render Queue in an Output module. Currently its expected to have only single render item per composition in the Render Queue. @@ -151,3 +154,25 @@ You can switch to a previous version of the image or update to the latest. ![Loader](assets/photoshop_manage_switch.gif) ![Loader](assets/photoshop_manage_update.gif) + + +### Setting section + +Composition properties should be controlled by state in Asset Management System (Ftrack etc). Extension provides couple of buttons to trigger this propagation. + +#### Set Resolution + +Set width and height from AMS to composition. + +#### Set Frame Range + +Start frame and duration in workarea is set according to the settings in AMS. Handles are incorporated (not inclusive). +It is expected that composition(s) is selected first before pushing this button! + +#### Apply All Settings + +Both previous settings are triggered at same time. + +### Experimental tools + +Currently empty. Could contain special tools available only for specific hosts for early access testing. diff --git a/website/docs/assets/aftereffects_extension.png b/website/docs/assets/aftereffects_extension.png new file mode 100644 index 0000000000000000000000000000000000000000..b14992471a1e42cb07c16e93858f180240f3f2e3 GIT binary patch literal 12533 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