mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
[Automated] Merged develop into main
This commit is contained in:
commit
f49eb5d9fb
66 changed files with 3695 additions and 1395 deletions
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
|
|
@ -35,6 +35,7 @@ body:
|
|||
label: Version
|
||||
description: What version are you running? Look to OpenPype Tray
|
||||
options:
|
||||
- 3.15.8-nightly.2
|
||||
- 3.15.8-nightly.1
|
||||
- 3.15.7
|
||||
- 3.15.7-nightly.3
|
||||
|
|
@ -134,7 +135,6 @@ body:
|
|||
- 3.14.2-nightly.2
|
||||
- 3.14.2-nightly.1
|
||||
- 3.14.1
|
||||
- 3.14.1-nightly.4
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ExtensionManifest Version="8.0" ExtensionBundleId="com.openpype.AE.panel" ExtensionBundleVersion="1.0.24"
|
||||
ExtensionBundleName="openpype" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<ExtensionManifest Version="8.0" ExtensionBundleId="com.openpype.AE.panel" ExtensionBundleVersion="1.0.25"
|
||||
ExtensionBundleName="com.openpype.AE.panel" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<ExtensionList>
|
||||
<Extension Id="com.openpype.AE.panel" Version="1.0" />
|
||||
</ExtensionList>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
|
||||
|
||||
<link rel="stylesheet" href="css/topcoat-desktop-dark.min.css"/>
|
||||
<link id="hostStyle" rel="stylesheet" href="css/styles.css"/>
|
||||
|
||||
|
|
@ -25,11 +25,11 @@
|
|||
|
||||
<title></title>
|
||||
<script src="js/libs/jquery-2.0.2.min.js"></script>
|
||||
|
||||
|
||||
<script type=text/javascript>
|
||||
$(function() {
|
||||
$("a#workfiles-button").bind("click", function() {
|
||||
|
||||
|
||||
RPC.call('AfterEffects.workfiles_route').then(function (data) {
|
||||
}, function (error) {
|
||||
alert(error);
|
||||
|
|
@ -37,7 +37,7 @@
|
|||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
<script type=text/javascript>
|
||||
$(function() {
|
||||
$("a#loader-button").bind("click", function() {
|
||||
|
|
@ -48,7 +48,7 @@
|
|||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
<script type=text/javascript>
|
||||
$(function() {
|
||||
$("a#publish-button").bind("click", function() {
|
||||
|
|
@ -59,7 +59,7 @@
|
|||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
<script type=text/javascript>
|
||||
$(function() {
|
||||
$("a#sceneinventory-button").bind("click", function() {
|
||||
|
|
@ -70,7 +70,40 @@
|
|||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
<script type=text/javascript>
|
||||
$(function() {
|
||||
$("a#setresolution-button").bind("click", function() {
|
||||
RPC.call('AfterEffects.setresolution_route').then(function (data) {
|
||||
}, function (error) {
|
||||
alert(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<script type=text/javascript>
|
||||
$(function() {
|
||||
$("a#setframes-button").bind("click", function() {
|
||||
RPC.call('AfterEffects.setframes_route').then(function (data) {
|
||||
}, function (error) {
|
||||
alert(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<script type=text/javascript>
|
||||
$(function() {
|
||||
$("a#setall-button").bind("click", function() {
|
||||
RPC.call('AfterEffects.setall_route').then(function (data) {
|
||||
}, function (error) {
|
||||
alert(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<script type=text/javascript>
|
||||
$(function() {
|
||||
$("a#experimental-button").bind("click", function() {
|
||||
|
|
@ -80,25 +113,28 @@
|
|||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
</head>
|
||||
|
||||
<body class="hostElt">
|
||||
|
||||
<div id="content">
|
||||
|
||||
<div>
|
||||
<div id="content">
|
||||
|
||||
<div>
|
||||
<div></div><a href=# id=workfiles-button><button class="hostFontSize">Workfiles...</button></a></div>
|
||||
<div><a href=# id=loader-button><button class="hostFontSize">Load...</button></a></div>
|
||||
<div><a href=# id=publish-button><button class="hostFontSize">Publish...</button></a></div>
|
||||
<div><a href=# id=sceneinventory-button><button class="hostFontSize">Manage...</button></a></div>
|
||||
<div><a href=# id=setresolution-button><button class="hostFontSize">Set Resolution</button></a></div>
|
||||
<div><a href=# id=setframes-button><button class="hostFontSize">Set Frame Range</button></a></div>
|
||||
<div><a href=# id=setall-button><button class="hostFontSize">Apply All Settings</button></a></div>
|
||||
<div><a href=# id=experimental-button><button class="hostFontSize">Experimental Tools...</button></a></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<!-- <script src="js/libs/PlayerDebugMode"></script> -->
|
||||
<script src="js/libs/wsrpc.js"></script>
|
||||
<script src="js/libs/loglevel.min.js"></script>
|
||||
|
|
@ -107,6 +143,6 @@
|
|||
<script src="js/themeManager.js"></script>
|
||||
<script src="js/main.js"></script>
|
||||
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
}());
|
||||
|
|
|
|||
|
|
@ -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})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: <PhotoshopClientStub> 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"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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__)
|
||||
|
||||
|
|
|
|||
|
|
@ -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: <PhotoshopClientStub> where functions could be called from
|
||||
"""
|
||||
ae_stub = AfterEffectsServerStub()
|
||||
if not ae_stub.client:
|
||||
raise ConnectionNotEstablishedYet("Connection is not created yet")
|
||||
|
||||
return ae_stub
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ from .lib import (
|
|||
update_frame_range,
|
||||
set_asset_framerange,
|
||||
get_current_comp,
|
||||
get_bmd_library,
|
||||
comp_lock_and_undo_chunk
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -309,6 +309,12 @@ def get_fusion_module():
|
|||
return fusion
|
||||
|
||||
|
||||
def get_bmd_library():
|
||||
"""Get bmd library"""
|
||||
bmd = getattr(sys.modules["__main__"], "bmd", None)
|
||||
return bmd
|
||||
|
||||
|
||||
def get_current_comp():
|
||||
"""Get current comp in this session"""
|
||||
fusion = get_fusion_module()
|
||||
|
|
|
|||
|
|
@ -40,6 +40,11 @@ class CreateSaver(NewCreator):
|
|||
"{workdir}/renders/fusion/{subset}/{subset}.{frame}.{ext}")
|
||||
|
||||
def create(self, subset_name, instance_data, pre_create_data):
|
||||
self.pass_pre_attributes_to_instance(
|
||||
instance_data,
|
||||
pre_create_data
|
||||
)
|
||||
|
||||
instance_data.update({
|
||||
"id": "pyblish.avalon.instance",
|
||||
"subset": subset_name
|
||||
|
|
@ -194,20 +199,25 @@ class CreateSaver(NewCreator):
|
|||
attr_defs = [
|
||||
self._get_render_target_enum(),
|
||||
self._get_reviewable_bool(),
|
||||
self._get_frame_range_enum()
|
||||
]
|
||||
return attr_defs
|
||||
|
||||
def get_instance_attr_defs(self):
|
||||
"""Settings for publish page"""
|
||||
attr_defs = [
|
||||
self._get_render_target_enum(),
|
||||
self._get_reviewable_bool(),
|
||||
]
|
||||
return attr_defs
|
||||
return self.get_pre_create_attr_defs()
|
||||
|
||||
def pass_pre_attributes_to_instance(
|
||||
self,
|
||||
instance_data,
|
||||
pre_create_data
|
||||
):
|
||||
creator_attrs = instance_data["creator_attributes"] = {}
|
||||
for pass_key in pre_create_data.keys():
|
||||
creator_attrs[pass_key] = pre_create_data[pass_key]
|
||||
|
||||
# These functions below should be moved to another file
|
||||
# so it can be used by other plugins. plugin.py ?
|
||||
|
||||
def _get_render_target_enum(self):
|
||||
rendering_targets = {
|
||||
"local": "Local machine rendering",
|
||||
|
|
@ -220,6 +230,19 @@ class CreateSaver(NewCreator):
|
|||
"render_target", items=rendering_targets, label="Render target"
|
||||
)
|
||||
|
||||
def _get_frame_range_enum(self):
|
||||
frame_range_options = {
|
||||
"asset_db": "Current asset context",
|
||||
"render_range": "From viewer render in/out",
|
||||
"comp_range": "From composition timeline"
|
||||
}
|
||||
|
||||
return EnumDef(
|
||||
"frame_range_source",
|
||||
items=frame_range_options,
|
||||
label="Frame range source"
|
||||
)
|
||||
|
||||
def _get_reviewable_bool(self):
|
||||
return BoolDef(
|
||||
"review",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
from openpype.pipeline import (
|
||||
load,
|
||||
get_representation_path,
|
||||
|
|
@ -6,7 +5,7 @@ from openpype.pipeline import (
|
|||
from openpype.hosts.fusion.api import (
|
||||
imprint_container,
|
||||
get_current_comp,
|
||||
comp_lock_and_undo_chunk
|
||||
comp_lock_and_undo_chunk,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -15,7 +14,21 @@ class FusionLoadFBXMesh(load.LoaderPlugin):
|
|||
|
||||
families = ["*"]
|
||||
representations = ["*"]
|
||||
extensions = {"fbx"}
|
||||
extensions = {
|
||||
"3ds",
|
||||
"amc",
|
||||
"aoa",
|
||||
"asf",
|
||||
"bvh",
|
||||
"c3d",
|
||||
"dae",
|
||||
"dxf",
|
||||
"fbx",
|
||||
"htr",
|
||||
"mcd",
|
||||
"obj",
|
||||
"trc",
|
||||
}
|
||||
|
||||
label = "Load FBX mesh"
|
||||
order = -10
|
||||
|
|
@ -27,23 +40,24 @@ class FusionLoadFBXMesh(load.LoaderPlugin):
|
|||
def load(self, context, name, namespace, data):
|
||||
# Fallback to asset name when namespace is None
|
||||
if namespace is None:
|
||||
namespace = context['asset']['name']
|
||||
namespace = context["asset"]["name"]
|
||||
|
||||
# Create the Loader with the filename path set
|
||||
comp = get_current_comp()
|
||||
with comp_lock_and_undo_chunk(comp, "Create tool"):
|
||||
|
||||
path = self.fname
|
||||
|
||||
args = (-32768, -32768)
|
||||
tool = comp.AddTool(self.tool_type, *args)
|
||||
tool["ImportFile"] = path
|
||||
|
||||
imprint_container(tool,
|
||||
name=name,
|
||||
namespace=namespace,
|
||||
context=context,
|
||||
loader=self.__class__.__name__)
|
||||
imprint_container(
|
||||
tool,
|
||||
name=name,
|
||||
namespace=namespace,
|
||||
context=context,
|
||||
loader=self.__class__.__name__,
|
||||
)
|
||||
|
||||
def switch(self, container, representation):
|
||||
self.update(container, representation)
|
||||
|
|
|
|||
|
|
@ -3,17 +3,14 @@ import contextlib
|
|||
import openpype.pipeline.load as load
|
||||
from openpype.pipeline.load import (
|
||||
get_representation_context,
|
||||
get_representation_path_from_context
|
||||
get_representation_path_from_context,
|
||||
)
|
||||
from openpype.hosts.fusion.api import (
|
||||
imprint_container,
|
||||
get_current_comp,
|
||||
comp_lock_and_undo_chunk
|
||||
)
|
||||
from openpype.lib.transcoding import (
|
||||
IMAGE_EXTENSIONS,
|
||||
VIDEO_EXTENSIONS
|
||||
comp_lock_and_undo_chunk,
|
||||
)
|
||||
from openpype.lib.transcoding import IMAGE_EXTENSIONS, VIDEO_EXTENSIONS
|
||||
|
||||
comp = get_current_comp()
|
||||
|
||||
|
|
@ -57,20 +54,23 @@ def preserve_trim(loader, log=None):
|
|||
try:
|
||||
yield
|
||||
finally:
|
||||
|
||||
length = loader.GetAttrs()["TOOLIT_Clip_Length"][1] - 1
|
||||
if trim_from_start > length:
|
||||
trim_from_start = length
|
||||
if log:
|
||||
log.warning("Reducing trim in to %d "
|
||||
"(because of less frames)" % trim_from_start)
|
||||
log.warning(
|
||||
"Reducing trim in to %d "
|
||||
"(because of less frames)" % trim_from_start
|
||||
)
|
||||
|
||||
remainder = length - trim_from_start
|
||||
if trim_from_end > remainder:
|
||||
trim_from_end = remainder
|
||||
if log:
|
||||
log.warning("Reducing trim in to %d "
|
||||
"(because of less frames)" % trim_from_end)
|
||||
log.warning(
|
||||
"Reducing trim in to %d "
|
||||
"(because of less frames)" % trim_from_end
|
||||
)
|
||||
|
||||
loader["ClipTimeStart"][time] = trim_from_start
|
||||
loader["ClipTimeEnd"][time] = length - trim_from_end
|
||||
|
|
@ -109,11 +109,15 @@ def loader_shift(loader, frame, relative=True):
|
|||
# Shifting global in will try to automatically compensate for the change
|
||||
# in the "ClipTimeStart" and "HoldFirstFrame" inputs, so we preserve those
|
||||
# input values to "just shift" the clip
|
||||
with preserve_inputs(loader, inputs=["ClipTimeStart",
|
||||
"ClipTimeEnd",
|
||||
"HoldFirstFrame",
|
||||
"HoldLastFrame"]):
|
||||
|
||||
with preserve_inputs(
|
||||
loader,
|
||||
inputs=[
|
||||
"ClipTimeStart",
|
||||
"ClipTimeEnd",
|
||||
"HoldFirstFrame",
|
||||
"HoldLastFrame",
|
||||
],
|
||||
):
|
||||
# GlobalIn cannot be set past GlobalOut or vice versa
|
||||
# so we must apply them in the order of the shift.
|
||||
if shift > 0:
|
||||
|
|
@ -129,7 +133,14 @@ def loader_shift(loader, frame, relative=True):
|
|||
class FusionLoadSequence(load.LoaderPlugin):
|
||||
"""Load image sequence into Fusion"""
|
||||
|
||||
families = ["imagesequence", "review", "render", "plate"]
|
||||
families = [
|
||||
"imagesequence",
|
||||
"review",
|
||||
"render",
|
||||
"plate",
|
||||
"image",
|
||||
"onilne",
|
||||
]
|
||||
representations = ["*"]
|
||||
extensions = set(
|
||||
ext.lstrip(".") for ext in IMAGE_EXTENSIONS.union(VIDEO_EXTENSIONS)
|
||||
|
|
@ -143,7 +154,7 @@ class FusionLoadSequence(load.LoaderPlugin):
|
|||
def load(self, context, name, namespace, data):
|
||||
# Fallback to asset name when namespace is None
|
||||
if namespace is None:
|
||||
namespace = context['asset']['name']
|
||||
namespace = context["asset"]["name"]
|
||||
|
||||
# Use the first file for now
|
||||
path = get_representation_path_from_context(context)
|
||||
|
|
@ -151,7 +162,6 @@ class FusionLoadSequence(load.LoaderPlugin):
|
|||
# Create the Loader with the filename path set
|
||||
comp = get_current_comp()
|
||||
with comp_lock_and_undo_chunk(comp, "Create Loader"):
|
||||
|
||||
args = (-32768, -32768)
|
||||
tool = comp.AddTool("Loader", *args)
|
||||
tool["Clip"] = path
|
||||
|
|
@ -160,11 +170,13 @@ class FusionLoadSequence(load.LoaderPlugin):
|
|||
start = self._get_start(context["version"], tool)
|
||||
loader_shift(tool, start, relative=False)
|
||||
|
||||
imprint_container(tool,
|
||||
name=name,
|
||||
namespace=namespace,
|
||||
context=context,
|
||||
loader=self.__class__.__name__)
|
||||
imprint_container(
|
||||
tool,
|
||||
name=name,
|
||||
namespace=namespace,
|
||||
context=context,
|
||||
loader=self.__class__.__name__,
|
||||
)
|
||||
|
||||
def switch(self, container, representation):
|
||||
self.update(container, representation)
|
||||
|
|
@ -222,24 +234,28 @@ class FusionLoadSequence(load.LoaderPlugin):
|
|||
start = self._get_start(context["version"], tool)
|
||||
|
||||
with comp_lock_and_undo_chunk(comp, "Update Loader"):
|
||||
|
||||
# Update the loader's path whilst preserving some values
|
||||
with preserve_trim(tool, log=self.log):
|
||||
with preserve_inputs(tool,
|
||||
inputs=("HoldFirstFrame",
|
||||
"HoldLastFrame",
|
||||
"Reverse",
|
||||
"Depth",
|
||||
"KeyCode",
|
||||
"TimeCodeOffset")):
|
||||
with preserve_inputs(
|
||||
tool,
|
||||
inputs=(
|
||||
"HoldFirstFrame",
|
||||
"HoldLastFrame",
|
||||
"Reverse",
|
||||
"Depth",
|
||||
"KeyCode",
|
||||
"TimeCodeOffset",
|
||||
),
|
||||
):
|
||||
tool["Clip"] = path
|
||||
|
||||
# Set the global in to the start frame of the sequence
|
||||
global_in_changed = loader_shift(tool, start, relative=False)
|
||||
if global_in_changed:
|
||||
# Log this change to the user
|
||||
self.log.debug("Changed '%s' global in: %d" % (tool.Name,
|
||||
start))
|
||||
self.log.debug(
|
||||
"Changed '%s' global in: %d" % (tool.Name, start)
|
||||
)
|
||||
|
||||
# Update the imprinted representation
|
||||
tool.SetData("avalon.representation", str(representation["_id"]))
|
||||
|
|
@ -264,9 +280,11 @@ class FusionLoadSequence(load.LoaderPlugin):
|
|||
# Get frame start without handles
|
||||
start = data.get("frameStart")
|
||||
if start is None:
|
||||
self.log.warning("Missing start frame for version "
|
||||
"assuming starts at frame 0 for: "
|
||||
"{}".format(tool.Name))
|
||||
self.log.warning(
|
||||
"Missing start frame for version "
|
||||
"assuming starts at frame 0 for: "
|
||||
"{}".format(tool.Name)
|
||||
)
|
||||
return 0
|
||||
|
||||
# Use `handleStart` if the data is available
|
||||
|
|
|
|||
32
openpype/hosts/fusion/plugins/load/load_workfile.py
Normal file
32
openpype/hosts/fusion/plugins/load/load_workfile.py
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
"""Import workfiles into your current comp.
|
||||
As all imported nodes are free floating and will probably be changed there
|
||||
is no update or reload function added for this plugin
|
||||
"""
|
||||
|
||||
from openpype.pipeline import load
|
||||
|
||||
from openpype.hosts.fusion.api import (
|
||||
get_current_comp,
|
||||
get_bmd_library,
|
||||
)
|
||||
|
||||
|
||||
class FusionLoadWorkfile(load.LoaderPlugin):
|
||||
"""Load the content of a workfile into Fusion"""
|
||||
|
||||
families = ["workfile"]
|
||||
representations = ["*"]
|
||||
extensions = {"comp"}
|
||||
|
||||
label = "Load Workfile"
|
||||
order = -10
|
||||
icon = "code-fork"
|
||||
color = "orange"
|
||||
|
||||
def load(self, context, name, namespace, data):
|
||||
# Get needed elements
|
||||
bmd = get_bmd_library()
|
||||
comp = get_current_comp()
|
||||
|
||||
# Paste the content of the file into the current comp
|
||||
comp.Paste(bmd.readfile(self.fname))
|
||||
|
|
@ -35,9 +35,10 @@ class CollectFusionCompFrameRanges(pyblish.api.ContextPlugin):
|
|||
|
||||
# Store comp render ranges
|
||||
start, end, global_start, global_end = get_comp_render_range(comp)
|
||||
context.data["frameStart"] = int(start)
|
||||
context.data["frameEnd"] = int(end)
|
||||
context.data["frameStartHandle"] = int(global_start)
|
||||
context.data["frameEndHandle"] = int(global_end)
|
||||
context.data["handleStart"] = int(start) - int(global_start)
|
||||
context.data["handleEnd"] = int(global_end) - int(end)
|
||||
|
||||
context.data.update({
|
||||
"renderFrameStart": int(start),
|
||||
"renderFrameEnd": int(end),
|
||||
"compFrameStart": int(global_start),
|
||||
"compFrameEnd": int(global_end)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,50 +0,0 @@
|
|||
import pyblish.api
|
||||
from openpype.pipeline import publish
|
||||
import os
|
||||
|
||||
|
||||
class CollectFusionExpectedFrames(
|
||||
pyblish.api.InstancePlugin, publish.ColormanagedPyblishPluginMixin
|
||||
):
|
||||
"""Collect all frames needed to publish expected frames"""
|
||||
|
||||
order = pyblish.api.CollectorOrder + 0.5
|
||||
label = "Collect Expected Frames"
|
||||
hosts = ["fusion"]
|
||||
families = ["render"]
|
||||
|
||||
def process(self, instance):
|
||||
context = instance.context
|
||||
|
||||
frame_start = context.data["frameStartHandle"]
|
||||
frame_end = context.data["frameEndHandle"]
|
||||
path = instance.data["path"]
|
||||
output_dir = instance.data["outputDir"]
|
||||
|
||||
basename = os.path.basename(path)
|
||||
head, ext = os.path.splitext(basename)
|
||||
files = [
|
||||
f"{head}{str(frame).zfill(4)}{ext}"
|
||||
for frame in range(frame_start, frame_end + 1)
|
||||
]
|
||||
repre = {
|
||||
"name": ext[1:],
|
||||
"ext": ext[1:],
|
||||
"frameStart": f"%0{len(str(frame_end))}d" % frame_start,
|
||||
"files": files,
|
||||
"stagingDir": output_dir,
|
||||
}
|
||||
|
||||
self.set_representation_colorspace(
|
||||
representation=repre,
|
||||
context=context,
|
||||
)
|
||||
|
||||
# review representation
|
||||
if instance.data.get("review", False):
|
||||
repre["tags"] = ["review"]
|
||||
|
||||
# add the repre to the instance
|
||||
if "representations" not in instance.data:
|
||||
instance.data["representations"] = []
|
||||
instance.data["representations"].append(repre)
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
import pyblish.api
|
||||
|
||||
|
||||
class CollectFusionVersion(pyblish.api.ContextPlugin):
|
||||
"""Collect current comp"""
|
||||
|
||||
order = pyblish.api.CollectorOrder
|
||||
label = "Collect Fusion Version"
|
||||
hosts = ["fusion"]
|
||||
|
||||
def process(self, context):
|
||||
"""Collect all image sequence tools"""
|
||||
|
||||
comp = context.data.get("currentComp")
|
||||
if not comp:
|
||||
raise RuntimeError("No comp previously collected, unable to "
|
||||
"retrieve Fusion version.")
|
||||
|
||||
version = comp.GetApp().Version
|
||||
context.data["fusionVersion"] = version
|
||||
|
||||
self.log.info("Fusion version: %s" % version)
|
||||
|
|
@ -1,5 +1,3 @@
|
|||
import os
|
||||
|
||||
import pyblish.api
|
||||
|
||||
|
||||
|
|
@ -24,23 +22,63 @@ class CollectInstanceData(pyblish.api.InstancePlugin):
|
|||
creator_attributes = instance.data["creator_attributes"]
|
||||
instance.data.update(creator_attributes)
|
||||
|
||||
# Include start and end render frame in label
|
||||
subset = instance.data["subset"]
|
||||
frame_range_source = creator_attributes.get("frame_range_source")
|
||||
instance.data["frame_range_source"] = frame_range_source
|
||||
|
||||
# get asset frame ranges to all instances
|
||||
# render family instances `asset_db` render target
|
||||
start = context.data["frameStart"]
|
||||
end = context.data["frameEnd"]
|
||||
label = "{subset} ({start}-{end})".format(subset=subset,
|
||||
start=int(start),
|
||||
end=int(end))
|
||||
handle_start = context.data["handleStart"]
|
||||
handle_end = context.data["handleEnd"]
|
||||
start_with_handle = start - handle_start
|
||||
end_with_handle = end + handle_end
|
||||
|
||||
# conditions for render family instances
|
||||
if frame_range_source == "render_range":
|
||||
# set comp render frame ranges
|
||||
start = context.data["renderFrameStart"]
|
||||
end = context.data["renderFrameEnd"]
|
||||
handle_start = 0
|
||||
handle_end = 0
|
||||
start_with_handle = start
|
||||
end_with_handle = end
|
||||
|
||||
if frame_range_source == "comp_range":
|
||||
comp_start = context.data["compFrameStart"]
|
||||
comp_end = context.data["compFrameEnd"]
|
||||
render_start = context.data["renderFrameStart"]
|
||||
render_end = context.data["renderFrameEnd"]
|
||||
# set comp frame ranges
|
||||
start = render_start
|
||||
end = render_end
|
||||
handle_start = render_start - comp_start
|
||||
handle_end = comp_end - render_end
|
||||
start_with_handle = comp_start
|
||||
end_with_handle = comp_end
|
||||
|
||||
# Include start and end render frame in label
|
||||
subset = instance.data["subset"]
|
||||
label = (
|
||||
"{subset} ({start}-{end}) [{handle_start}-{handle_end}]"
|
||||
).format(
|
||||
subset=subset,
|
||||
start=int(start),
|
||||
end=int(end),
|
||||
handle_start=int(handle_start),
|
||||
handle_end=int(handle_end)
|
||||
)
|
||||
|
||||
instance.data.update({
|
||||
"label": label,
|
||||
|
||||
# todo: Allow custom frame range per instance
|
||||
"frameStart": context.data["frameStart"],
|
||||
"frameEnd": context.data["frameEnd"],
|
||||
"frameStartHandle": context.data["frameStartHandle"],
|
||||
"frameEndHandle": context.data["frameStartHandle"],
|
||||
"handleStart": context.data["handleStart"],
|
||||
"handleEnd": context.data["handleEnd"],
|
||||
"frameStart": start,
|
||||
"frameEnd": end,
|
||||
"frameStartHandle": start_with_handle,
|
||||
"frameEndHandle": end_with_handle,
|
||||
"handleStart": handle_start,
|
||||
"handleEnd": handle_end,
|
||||
"fps": context.data["fps"],
|
||||
})
|
||||
|
||||
|
|
@ -49,31 +87,3 @@ class CollectInstanceData(pyblish.api.InstancePlugin):
|
|||
if instance.data.get("review", False):
|
||||
self.log.info("Adding review family..")
|
||||
instance.data["families"].append("review")
|
||||
|
||||
if instance.data["family"] == "render":
|
||||
# TODO: This should probably move into a collector of
|
||||
# its own for the "render" family
|
||||
from openpype.hosts.fusion.api.lib import get_frame_path
|
||||
comp = context.data["currentComp"]
|
||||
|
||||
# This is only the case for savers currently but not
|
||||
# for workfile instances. So we assume saver here.
|
||||
tool = instance.data["transientData"]["tool"]
|
||||
path = tool["Clip"][comp.TIME_UNDEFINED]
|
||||
|
||||
filename = os.path.basename(path)
|
||||
head, padding, tail = get_frame_path(filename)
|
||||
ext = os.path.splitext(path)[1]
|
||||
assert tail == ext, ("Tail does not match %s" % ext)
|
||||
|
||||
instance.data.update({
|
||||
"path": path,
|
||||
"outputDir": os.path.dirname(path),
|
||||
"ext": ext, # todo: should be redundant?
|
||||
|
||||
# Backwards compatibility: embed tool in instance.data
|
||||
"tool": tool
|
||||
})
|
||||
|
||||
# Add tool itself as member
|
||||
instance.append(tool)
|
||||
|
|
|
|||
207
openpype/hosts/fusion/plugins/publish/collect_render.py
Normal file
207
openpype/hosts/fusion/plugins/publish/collect_render.py
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
import os
|
||||
import attr
|
||||
import pyblish.api
|
||||
|
||||
from openpype.pipeline import publish
|
||||
from openpype.pipeline.publish import RenderInstance
|
||||
from openpype.hosts.fusion.api.lib import get_frame_path
|
||||
|
||||
|
||||
@attr.s
|
||||
class FusionRenderInstance(RenderInstance):
|
||||
# extend generic, composition name is needed
|
||||
fps = attr.ib(default=None)
|
||||
projectEntity = attr.ib(default=None)
|
||||
stagingDir = attr.ib(default=None)
|
||||
app_version = attr.ib(default=None)
|
||||
tool = attr.ib(default=None)
|
||||
workfileComp = attr.ib(default=None)
|
||||
publish_attributes = attr.ib(default={})
|
||||
|
||||
|
||||
class CollectFusionRender(
|
||||
publish.AbstractCollectRender,
|
||||
publish.ColormanagedPyblishPluginMixin
|
||||
):
|
||||
|
||||
order = pyblish.api.CollectorOrder + 0.09
|
||||
label = "Collect Fusion Render"
|
||||
hosts = ["fusion"]
|
||||
|
||||
def get_instances(self, context):
|
||||
|
||||
comp = context.data.get("currentComp")
|
||||
comp_frame_format_prefs = comp.GetPrefs("Comp.FrameFormat")
|
||||
aspect_x = comp_frame_format_prefs["AspectX"]
|
||||
aspect_y = comp_frame_format_prefs["AspectY"]
|
||||
|
||||
instances = []
|
||||
instances_to_remove = []
|
||||
|
||||
current_file = context.data["currentFile"]
|
||||
version = context.data["version"]
|
||||
|
||||
project_entity = context.data["projectEntity"]
|
||||
|
||||
for inst in context:
|
||||
if not inst.data.get("active", True):
|
||||
continue
|
||||
|
||||
family = inst.data["family"]
|
||||
if family != "render":
|
||||
continue
|
||||
|
||||
task_name = context.data["task"]
|
||||
tool = inst.data["transientData"]["tool"]
|
||||
|
||||
instance_families = inst.data.get("families", [])
|
||||
subset_name = inst.data["subset"]
|
||||
instance = FusionRenderInstance(
|
||||
family="render",
|
||||
tool=tool,
|
||||
workfileComp=comp,
|
||||
families=instance_families,
|
||||
version=version,
|
||||
time="",
|
||||
source=current_file,
|
||||
label=inst.data["label"],
|
||||
subset=subset_name,
|
||||
asset=inst.data["asset"],
|
||||
task=task_name,
|
||||
attachTo=False,
|
||||
setMembers='',
|
||||
publish=True,
|
||||
name=subset_name,
|
||||
resolutionWidth=comp_frame_format_prefs.get("Width"),
|
||||
resolutionHeight=comp_frame_format_prefs.get("Height"),
|
||||
pixelAspect=aspect_x / aspect_y,
|
||||
tileRendering=False,
|
||||
tilesX=0,
|
||||
tilesY=0,
|
||||
review="review" in instance_families,
|
||||
frameStart=inst.data["frameStart"],
|
||||
frameEnd=inst.data["frameEnd"],
|
||||
handleStart=inst.data["handleStart"],
|
||||
handleEnd=inst.data["handleEnd"],
|
||||
ignoreFrameHandleCheck=(
|
||||
inst.data["frame_range_source"] == "render_range"),
|
||||
frameStep=1,
|
||||
fps=comp_frame_format_prefs.get("Rate"),
|
||||
app_version=comp.GetApp().Version,
|
||||
publish_attributes=inst.data.get("publish_attributes", {})
|
||||
)
|
||||
|
||||
render_target = inst.data["creator_attributes"]["render_target"]
|
||||
|
||||
# Add render target family
|
||||
render_target_family = f"render.{render_target}"
|
||||
if render_target_family not in instance.families:
|
||||
instance.families.append(render_target_family)
|
||||
|
||||
# Add render target specific data
|
||||
if render_target in {"local", "frames"}:
|
||||
instance.projectEntity = project_entity
|
||||
|
||||
if render_target == "farm":
|
||||
fam = "render.farm"
|
||||
if fam not in instance.families:
|
||||
instance.families.append(fam)
|
||||
instance.toBeRenderedOn = "deadline"
|
||||
instance.farm = True # to skip integrate
|
||||
if "review" in instance.families:
|
||||
# to skip ExtractReview locally
|
||||
instance.families.remove("review")
|
||||
|
||||
# add new instance to the list and remove the original
|
||||
# instance since it is not needed anymore
|
||||
instances.append(instance)
|
||||
instances_to_remove.append(inst)
|
||||
|
||||
for instance in instances_to_remove:
|
||||
context.remove(instance)
|
||||
|
||||
return instances
|
||||
|
||||
def post_collecting_action(self):
|
||||
for instance in self._context:
|
||||
if "render.frames" in instance.data.get("families", []):
|
||||
# adding representation data to the instance
|
||||
self._update_for_frames(instance)
|
||||
|
||||
def get_expected_files(self, render_instance):
|
||||
"""
|
||||
Returns list of rendered files that should be created by
|
||||
Deadline. These are not published directly, they are source
|
||||
for later 'submit_publish_job'.
|
||||
|
||||
Args:
|
||||
render_instance (RenderInstance): to pull anatomy and parts used
|
||||
in url
|
||||
|
||||
Returns:
|
||||
(list) of absolute urls to rendered file
|
||||
"""
|
||||
start = render_instance.frameStart - render_instance.handleStart
|
||||
end = render_instance.frameEnd + render_instance.handleEnd
|
||||
|
||||
path = (
|
||||
render_instance.tool["Clip"]
|
||||
[render_instance.workfileComp.TIME_UNDEFINED]
|
||||
)
|
||||
output_dir = os.path.dirname(path)
|
||||
render_instance.outputDir = output_dir
|
||||
|
||||
basename = os.path.basename(path)
|
||||
|
||||
head, padding, ext = get_frame_path(basename)
|
||||
|
||||
expected_files = []
|
||||
for frame in range(start, end + 1):
|
||||
expected_files.append(
|
||||
os.path.join(
|
||||
output_dir,
|
||||
f"{head}{str(frame).zfill(padding)}{ext}"
|
||||
)
|
||||
)
|
||||
|
||||
return expected_files
|
||||
|
||||
def _update_for_frames(self, instance):
|
||||
"""Updating instance for render.frames family
|
||||
|
||||
Adding representation data to the instance. Also setting
|
||||
colorspaceData to the representation based on file rules.
|
||||
"""
|
||||
|
||||
expected_files = instance.data["expectedFiles"]
|
||||
|
||||
start = instance.data["frameStart"] - instance.data["handleStart"]
|
||||
|
||||
path = expected_files[0]
|
||||
basename = os.path.basename(path)
|
||||
staging_dir = os.path.dirname(path)
|
||||
_, padding, ext = get_frame_path(basename)
|
||||
|
||||
repre = {
|
||||
"name": ext[1:],
|
||||
"ext": ext[1:],
|
||||
"frameStart": f"%0{padding}d" % start,
|
||||
"files": [os.path.basename(f) for f in expected_files],
|
||||
"stagingDir": staging_dir,
|
||||
}
|
||||
|
||||
self.set_representation_colorspace(
|
||||
representation=repre,
|
||||
context=instance.context,
|
||||
)
|
||||
|
||||
# review representation
|
||||
if instance.data.get("review", False):
|
||||
repre["tags"] = ["review"]
|
||||
|
||||
# add the repre to the instance
|
||||
if "representations" not in instance.data:
|
||||
instance.data["representations"] = []
|
||||
instance.data["representations"].append(repre)
|
||||
|
||||
return instance
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
import pyblish.api
|
||||
|
||||
|
||||
class CollectFusionRenders(pyblish.api.InstancePlugin):
|
||||
"""Collect current saver node's render Mode
|
||||
|
||||
Options:
|
||||
local (Render locally)
|
||||
frames (Use existing frames)
|
||||
|
||||
"""
|
||||
|
||||
order = pyblish.api.CollectorOrder + 0.4
|
||||
label = "Collect Renders"
|
||||
hosts = ["fusion"]
|
||||
families = ["render"]
|
||||
|
||||
def process(self, instance):
|
||||
render_target = instance.data["render_target"]
|
||||
family = instance.data["family"]
|
||||
|
||||
# add targeted family to families
|
||||
instance.data["families"].append(
|
||||
"{}.{}".format(family, render_target)
|
||||
)
|
||||
|
|
@ -1,8 +1,11 @@
|
|||
import os
|
||||
import logging
|
||||
import contextlib
|
||||
import pyblish.api
|
||||
from openpype.hosts.fusion.api import comp_lock_and_undo_chunk
|
||||
|
||||
from openpype.pipeline import publish
|
||||
from openpype.hosts.fusion.api import comp_lock_and_undo_chunk
|
||||
from openpype.hosts.fusion.api.lib import get_frame_path
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -38,7 +41,10 @@ def enabled_savers(comp, savers):
|
|||
saver.SetAttrs({"TOOLB_PassThrough": original_state})
|
||||
|
||||
|
||||
class FusionRenderLocal(pyblish.api.InstancePlugin):
|
||||
class FusionRenderLocal(
|
||||
pyblish.api.InstancePlugin,
|
||||
publish.ColormanagedPyblishPluginMixin
|
||||
):
|
||||
"""Render the current Fusion composition locally."""
|
||||
|
||||
order = pyblish.api.ExtractorOrder - 0.2
|
||||
|
|
@ -52,6 +58,8 @@ class FusionRenderLocal(pyblish.api.InstancePlugin):
|
|||
# Start render
|
||||
self.render_once(context)
|
||||
|
||||
self._add_representation(instance)
|
||||
|
||||
# Log render status
|
||||
self.log.info(
|
||||
"Rendered '{nm}' for asset '{ast}' under the task '{tsk}'".format(
|
||||
|
|
@ -71,11 +79,11 @@ class FusionRenderLocal(pyblish.api.InstancePlugin):
|
|||
|
||||
savers_to_render = [
|
||||
# Get the saver tool from the instance
|
||||
instance[0] for instance in context if
|
||||
instance.data["tool"] for instance in context if
|
||||
# Only active instances
|
||||
instance.data.get("publish", True) and
|
||||
# Only render.local instances
|
||||
"render.local" in instance.data["families"]
|
||||
"render.local" in instance.data.get("families", [])
|
||||
]
|
||||
|
||||
if key not in context.data:
|
||||
|
|
@ -107,3 +115,39 @@ class FusionRenderLocal(pyblish.api.InstancePlugin):
|
|||
|
||||
if context.data[key] is False:
|
||||
raise RuntimeError("Comp render failed")
|
||||
|
||||
def _add_representation(self, instance):
|
||||
"""Add representation to instance"""
|
||||
|
||||
expected_files = instance.data["expectedFiles"]
|
||||
|
||||
start = instance.data["frameStart"] - instance.data["handleStart"]
|
||||
|
||||
path = expected_files[0]
|
||||
_, padding, ext = get_frame_path(path)
|
||||
|
||||
staging_dir = os.path.dirname(path)
|
||||
|
||||
repre = {
|
||||
"name": ext[1:],
|
||||
"ext": ext[1:],
|
||||
"frameStart": f"%0{padding}d" % start,
|
||||
"files": [os.path.basename(f) for f in expected_files],
|
||||
"stagingDir": staging_dir,
|
||||
}
|
||||
|
||||
self.set_representation_colorspace(
|
||||
representation=repre,
|
||||
context=instance.context,
|
||||
)
|
||||
|
||||
# review representation
|
||||
if instance.data.get("review", False):
|
||||
repre["tags"] = ["review"]
|
||||
|
||||
# add the repre to the instance
|
||||
if "representations" not in instance.data:
|
||||
instance.data["representations"] = []
|
||||
instance.data["representations"].append(repre)
|
||||
|
||||
return instance
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ class ValidateCreateFolderChecked(pyblish.api.InstancePlugin):
|
|||
|
||||
@classmethod
|
||||
def get_invalid(cls, instance):
|
||||
tool = instance[0]
|
||||
tool = instance.data["tool"]
|
||||
create_dir = tool.GetInput("CreateDir")
|
||||
if create_dir == 0.0:
|
||||
cls.log.error(
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ class ValidateLocalFramesExistence(pyblish.api.InstancePlugin):
|
|||
|
||||
order = pyblish.api.ValidatorOrder
|
||||
label = "Validate Expected Frames Exists"
|
||||
families = ["render"]
|
||||
families = ["render.frames"]
|
||||
hosts = ["fusion"]
|
||||
actions = [RepairAction, SelectInvalidAction]
|
||||
|
||||
|
|
@ -23,31 +23,20 @@ class ValidateLocalFramesExistence(pyblish.api.InstancePlugin):
|
|||
if non_existing_frames is None:
|
||||
non_existing_frames = []
|
||||
|
||||
if instance.data.get("render_target") == "frames":
|
||||
tool = instance[0]
|
||||
tool = instance.data["tool"]
|
||||
|
||||
frame_start = instance.data["frameStart"]
|
||||
frame_end = instance.data["frameEnd"]
|
||||
path = instance.data["path"]
|
||||
output_dir = instance.data["outputDir"]
|
||||
expected_files = instance.data["expectedFiles"]
|
||||
|
||||
basename = os.path.basename(path)
|
||||
head, ext = os.path.splitext(basename)
|
||||
files = [
|
||||
f"{head}{str(frame).zfill(4)}{ext}"
|
||||
for frame in range(frame_start, frame_end + 1)
|
||||
]
|
||||
for file in expected_files:
|
||||
if not os.path.exists(file):
|
||||
cls.log.error(
|
||||
f"Missing file: {file}"
|
||||
)
|
||||
non_existing_frames.append(file)
|
||||
|
||||
for file in files:
|
||||
if not os.path.exists(os.path.join(output_dir, file)):
|
||||
cls.log.error(
|
||||
f"Missing file: {os.path.join(output_dir, file)}"
|
||||
)
|
||||
non_existing_frames.append(file)
|
||||
|
||||
if len(non_existing_frames) > 0:
|
||||
cls.log.error(f"Some of {tool.Name}'s files does not exist")
|
||||
return [tool]
|
||||
if len(non_existing_frames) > 0:
|
||||
cls.log.error(f"Some of {tool.Name}'s files does not exist")
|
||||
return [tool]
|
||||
|
||||
def process(self, instance):
|
||||
non_existing_frames = []
|
||||
|
|
@ -67,8 +56,7 @@ class ValidateLocalFramesExistence(pyblish.api.InstancePlugin):
|
|||
def repair(cls, instance):
|
||||
invalid = cls.get_invalid(instance)
|
||||
if invalid:
|
||||
tool = invalid[0]
|
||||
|
||||
tool = instance.data["tool"]
|
||||
# Change render target to local to render locally
|
||||
tool.SetData("openpype.creator_attributes.render_target", "local")
|
||||
|
||||
|
|
|
|||
|
|
@ -30,11 +30,11 @@ class ValidateFilenameHasExtension(pyblish.api.InstancePlugin):
|
|||
@classmethod
|
||||
def get_invalid(cls, instance):
|
||||
|
||||
path = instance.data["path"]
|
||||
path = instance.data["expectedFiles"][0]
|
||||
fname, ext = os.path.splitext(path)
|
||||
|
||||
if not ext:
|
||||
tool = instance[0]
|
||||
tool = instance.data["tool"]
|
||||
cls.log.error("%s has no extension specified" % tool.Name)
|
||||
return [tool]
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ class ValidateSaverHasInput(pyblish.api.InstancePlugin):
|
|||
@classmethod
|
||||
def get_invalid(cls, instance):
|
||||
|
||||
saver = instance[0]
|
||||
saver = instance.data["tool"]
|
||||
if not saver.Input.GetConnectedOutput():
|
||||
return [saver]
|
||||
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ class ValidateSaverPassthrough(pyblish.api.ContextPlugin):
|
|||
|
||||
def is_invalid(self, instance):
|
||||
|
||||
saver = instance[0]
|
||||
saver = instance.data["tool"]
|
||||
attr = saver.GetAttrs()
|
||||
active = not attr["TOOLB_PassThrough"]
|
||||
|
||||
|
|
|
|||
|
|
@ -133,11 +133,11 @@ class CollectNukeWrites(pyblish.api.InstancePlugin,
|
|||
else:
|
||||
representation['files'] = collected_frames
|
||||
|
||||
# inject colorspace data
|
||||
self.set_representation_colorspace(
|
||||
representation, instance.context,
|
||||
colorspace=colorspace
|
||||
)
|
||||
# inject colorspace data
|
||||
self.set_representation_colorspace(
|
||||
representation, instance.context,
|
||||
colorspace=colorspace
|
||||
)
|
||||
|
||||
instance.data["representations"].append(representation)
|
||||
self.log.info("Publishing rendered frames ...")
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import os
|
||||
from pathlib import Path
|
||||
import platform
|
||||
from openpype.lib import PreLaunchHook
|
||||
from openpype.hosts.resolve.utils import setup
|
||||
|
|
@ -6,33 +7,57 @@ from openpype.hosts.resolve.utils import setup
|
|||
|
||||
class ResolvePrelaunch(PreLaunchHook):
|
||||
"""
|
||||
This hook will check if current workfile path has Resolve
|
||||
project inside. IF not, it initialize it and finally it pass
|
||||
path to the project by environment variable to Premiere launcher
|
||||
shell script.
|
||||
This hook will set up the Resolve scripting environment as described in
|
||||
Resolve's documentation found with the installed application at
|
||||
{resolve}/Support/Developer/Scripting/README.txt
|
||||
|
||||
Prepares the following environment variables:
|
||||
- `RESOLVE_SCRIPT_API`
|
||||
- `RESOLVE_SCRIPT_LIB`
|
||||
|
||||
It adds $RESOLVE_SCRIPT_API/Modules to PYTHONPATH.
|
||||
|
||||
Additionally it sets up the Python home for Python 3 based on the
|
||||
RESOLVE_PYTHON3_HOME in the environment (usually defined in OpenPype's
|
||||
Application environment for Resolve by the admin). For this it sets
|
||||
PYTHONHOME and PATH variables.
|
||||
|
||||
It also defines:
|
||||
- `RESOLVE_UTILITY_SCRIPTS_DIR`: Destination directory for OpenPype
|
||||
Fusion scripts to be copied to for Resolve to pick them up.
|
||||
- `OPENPYPE_LOG_NO_COLORS` to True to ensure OP doesn't try to
|
||||
use logging with terminal colors as it fails in Resolve.
|
||||
|
||||
"""
|
||||
|
||||
app_groups = ["resolve"]
|
||||
|
||||
def execute(self):
|
||||
current_platform = platform.system().lower()
|
||||
|
||||
PROGRAMDATA = self.launch_context.env.get("PROGRAMDATA", "")
|
||||
RESOLVE_SCRIPT_API_ = {
|
||||
programdata = self.launch_context.env.get("PROGRAMDATA", "")
|
||||
resolve_script_api_locations = {
|
||||
"windows": (
|
||||
f"{PROGRAMDATA}/Blackmagic Design/"
|
||||
f"{programdata}/Blackmagic Design/"
|
||||
"DaVinci Resolve/Support/Developer/Scripting"
|
||||
),
|
||||
"darwin": (
|
||||
"/Library/Application Support/Blackmagic Design"
|
||||
"/DaVinci Resolve/Developer/Scripting"
|
||||
),
|
||||
"linux": "/opt/resolve/Developer/Scripting"
|
||||
"linux": "/opt/resolve/Developer/Scripting",
|
||||
}
|
||||
RESOLVE_SCRIPT_API = os.path.normpath(
|
||||
RESOLVE_SCRIPT_API_[current_platform])
|
||||
self.launch_context.env["RESOLVE_SCRIPT_API"] = RESOLVE_SCRIPT_API
|
||||
resolve_script_api = Path(
|
||||
resolve_script_api_locations[current_platform]
|
||||
)
|
||||
self.log.info(
|
||||
f"setting RESOLVE_SCRIPT_API variable to {resolve_script_api}"
|
||||
)
|
||||
self.launch_context.env[
|
||||
"RESOLVE_SCRIPT_API"
|
||||
] = resolve_script_api.as_posix()
|
||||
|
||||
RESOLVE_SCRIPT_LIB_ = {
|
||||
resolve_script_lib_dirs = {
|
||||
"windows": (
|
||||
"C:/Program Files/Blackmagic Design"
|
||||
"/DaVinci Resolve/fusionscript.dll"
|
||||
|
|
@ -41,61 +66,69 @@ class ResolvePrelaunch(PreLaunchHook):
|
|||
"/Applications/DaVinci Resolve/DaVinci Resolve.app"
|
||||
"/Contents/Libraries/Fusion/fusionscript.so"
|
||||
),
|
||||
"linux": "/opt/resolve/libs/Fusion/fusionscript.so"
|
||||
"linux": "/opt/resolve/libs/Fusion/fusionscript.so",
|
||||
}
|
||||
RESOLVE_SCRIPT_LIB = os.path.normpath(
|
||||
RESOLVE_SCRIPT_LIB_[current_platform])
|
||||
self.launch_context.env["RESOLVE_SCRIPT_LIB"] = RESOLVE_SCRIPT_LIB
|
||||
resolve_script_lib = Path(resolve_script_lib_dirs[current_platform])
|
||||
self.launch_context.env[
|
||||
"RESOLVE_SCRIPT_LIB"
|
||||
] = resolve_script_lib.as_posix()
|
||||
self.log.info(
|
||||
f"setting RESOLVE_SCRIPT_LIB variable to {resolve_script_lib}"
|
||||
)
|
||||
|
||||
# TODO: add OTIO installation from `openpype/requirements.py`
|
||||
# TODO: add OTIO installation from `openpype/requirements.py`
|
||||
# making sure python <3.9.* is installed at provided path
|
||||
python3_home = os.path.normpath(
|
||||
self.launch_context.env.get("RESOLVE_PYTHON3_HOME", ""))
|
||||
python3_home = Path(
|
||||
self.launch_context.env.get("RESOLVE_PYTHON3_HOME", "")
|
||||
)
|
||||
|
||||
assert os.path.isdir(python3_home), (
|
||||
assert python3_home.is_dir(), (
|
||||
"Python 3 is not installed at the provided folder path. Either "
|
||||
"make sure the `environments\resolve.json` is having correctly "
|
||||
"set `RESOLVE_PYTHON3_HOME` or make sure Python 3 is installed "
|
||||
f"in given path. \nRESOLVE_PYTHON3_HOME: `{python3_home}`"
|
||||
)
|
||||
self.launch_context.env["PYTHONHOME"] = python3_home
|
||||
self.log.info(f"Path to Resolve Python folder: `{python3_home}`...")
|
||||
|
||||
# add to the python path to path
|
||||
env_path = self.launch_context.env["PATH"]
|
||||
self.launch_context.env["PATH"] = os.pathsep.join([
|
||||
python3_home,
|
||||
os.path.join(python3_home, "Scripts")
|
||||
] + env_path.split(os.pathsep))
|
||||
|
||||
self.log.debug(f"PATH: {self.launch_context.env['PATH']}")
|
||||
python3_home_str = python3_home.as_posix()
|
||||
self.launch_context.env["PYTHONHOME"] = python3_home_str
|
||||
self.log.info(f"Path to Resolve Python folder: `{python3_home_str}`")
|
||||
|
||||
# add to the PYTHONPATH
|
||||
env_pythonpath = self.launch_context.env["PYTHONPATH"]
|
||||
self.launch_context.env["PYTHONPATH"] = os.pathsep.join([
|
||||
os.path.join(python3_home, "Lib", "site-packages"),
|
||||
os.path.join(RESOLVE_SCRIPT_API, "Modules"),
|
||||
] + env_pythonpath.split(os.pathsep))
|
||||
modules_path = Path(resolve_script_api, "Modules").as_posix()
|
||||
self.launch_context.env[
|
||||
"PYTHONPATH"
|
||||
] = f"{modules_path}{os.pathsep}{env_pythonpath}"
|
||||
|
||||
self.log.debug(f"PYTHONPATH: {self.launch_context.env['PYTHONPATH']}")
|
||||
|
||||
RESOLVE_UTILITY_SCRIPTS_DIR_ = {
|
||||
# add the pythonhome folder to PATH because on Windows
|
||||
# this is needed for Py3 to be correctly detected within Resolve
|
||||
env_path = self.launch_context.env["PATH"]
|
||||
self.log.info(f"Adding `{python3_home_str}` to the PATH variable")
|
||||
self.launch_context.env[
|
||||
"PATH"
|
||||
] = f"{python3_home_str}{os.pathsep}{env_path}"
|
||||
|
||||
self.log.debug(f"PATH: {self.launch_context.env['PATH']}")
|
||||
|
||||
resolve_utility_scripts_dirs = {
|
||||
"windows": (
|
||||
f"{PROGRAMDATA}/Blackmagic Design"
|
||||
f"{programdata}/Blackmagic Design"
|
||||
"/DaVinci Resolve/Fusion/Scripts/Comp"
|
||||
),
|
||||
"darwin": (
|
||||
"/Library/Application Support/Blackmagic Design"
|
||||
"/DaVinci Resolve/Fusion/Scripts/Comp"
|
||||
),
|
||||
"linux": "/opt/resolve/Fusion/Scripts/Comp"
|
||||
"linux": "/opt/resolve/Fusion/Scripts/Comp",
|
||||
}
|
||||
RESOLVE_UTILITY_SCRIPTS_DIR = os.path.normpath(
|
||||
RESOLVE_UTILITY_SCRIPTS_DIR_[current_platform]
|
||||
resolve_utility_scripts_dir = Path(
|
||||
resolve_utility_scripts_dirs[current_platform]
|
||||
)
|
||||
# setting utility scripts dir for scripts syncing
|
||||
self.launch_context.env["RESOLVE_UTILITY_SCRIPTS_DIR"] = (
|
||||
RESOLVE_UTILITY_SCRIPTS_DIR)
|
||||
self.launch_context.env[
|
||||
"RESOLVE_UTILITY_SCRIPTS_DIR"
|
||||
] = resolve_utility_scripts_dir.as_posix()
|
||||
|
||||
# remove terminal coloring tags
|
||||
self.launch_context.env["OPENPYPE_LOG_NO_COLORS"] = "True"
|
||||
|
|
|
|||
|
|
@ -8,30 +8,30 @@ RESOLVE_ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
|
|||
def setup(env):
|
||||
log = Logger.get_logger("ResolveSetup")
|
||||
scripts = {}
|
||||
us_env = env.get("RESOLVE_UTILITY_SCRIPTS_SOURCE_DIR")
|
||||
us_dir = env["RESOLVE_UTILITY_SCRIPTS_DIR"]
|
||||
util_scripts_env = env.get("RESOLVE_UTILITY_SCRIPTS_SOURCE_DIR")
|
||||
util_scripts_dir = env["RESOLVE_UTILITY_SCRIPTS_DIR"]
|
||||
|
||||
us_paths = [os.path.join(
|
||||
util_scripts_paths = [os.path.join(
|
||||
RESOLVE_ROOT_DIR,
|
||||
"utility_scripts"
|
||||
)]
|
||||
|
||||
# collect script dirs
|
||||
if us_env:
|
||||
log.info("Utility Scripts Env: `{}`".format(us_env))
|
||||
us_paths = us_env.split(
|
||||
os.pathsep) + us_paths
|
||||
if util_scripts_env:
|
||||
log.info("Utility Scripts Env: `{}`".format(util_scripts_env))
|
||||
util_scripts_paths = util_scripts_env.split(
|
||||
os.pathsep) + util_scripts_paths
|
||||
|
||||
# collect scripts from dirs
|
||||
for path in us_paths:
|
||||
for path in util_scripts_paths:
|
||||
scripts.update({path: os.listdir(path)})
|
||||
|
||||
log.info("Utility Scripts Dir: `{}`".format(us_paths))
|
||||
log.info("Utility Scripts Dir: `{}`".format(util_scripts_paths))
|
||||
log.info("Utility Scripts: `{}`".format(scripts))
|
||||
|
||||
# make sure no script file is in folder
|
||||
for s in os.listdir(us_dir):
|
||||
path = os.path.join(us_dir, s)
|
||||
for script in os.listdir(util_scripts_dir):
|
||||
path = os.path.join(util_scripts_dir, script)
|
||||
log.info("Removing `{}`...".format(path))
|
||||
if os.path.isdir(path):
|
||||
shutil.rmtree(path, onerror=None)
|
||||
|
|
@ -39,12 +39,10 @@ def setup(env):
|
|||
os.remove(path)
|
||||
|
||||
# copy scripts into Resolve's utility scripts dir
|
||||
for d, sl in scripts.items():
|
||||
# directory and scripts list
|
||||
for s in sl:
|
||||
# script in script list
|
||||
src = os.path.join(d, s)
|
||||
dst = os.path.join(us_dir, s)
|
||||
for directory, scripts in scripts.items():
|
||||
for script in scripts:
|
||||
src = os.path.join(directory, script)
|
||||
dst = os.path.join(util_scripts_dir, script)
|
||||
log.info("Copying `{}` to `{}`...".format(src, dst))
|
||||
if os.path.isdir(src):
|
||||
shutil.copytree(
|
||||
|
|
|
|||
|
|
@ -286,6 +286,26 @@ class CameraLoader(plugin.Loader):
|
|||
self.fname
|
||||
)
|
||||
|
||||
# Set range of all sections
|
||||
# Changing the range of the section is not enough. We need to change
|
||||
# the frame of all the keys in the section.
|
||||
for possessable in cam_seq.get_possessables():
|
||||
for tracks in possessable.get_tracks():
|
||||
for section in tracks.get_sections():
|
||||
section.set_range(
|
||||
data.get('clipIn'),
|
||||
data.get('clipOut') + 1)
|
||||
for channel in section.get_all_channels():
|
||||
for key in channel.get_keys():
|
||||
old_time = key.get_time().get_editor_property(
|
||||
'frame_number')
|
||||
old_time_value = old_time.get_editor_property(
|
||||
'value')
|
||||
new_time = old_time_value + (
|
||||
data.get('clipIn') - data.get('frameStart')
|
||||
)
|
||||
key.set_time(unreal.FrameNumber(value=new_time))
|
||||
|
||||
# Create Asset Container
|
||||
unreal_pipeline.create_container(
|
||||
container=container_name, path=asset_dir)
|
||||
|
|
|
|||
|
|
@ -7,9 +7,19 @@ import requests
|
|||
import pyblish.api
|
||||
|
||||
from openpype.pipeline import legacy_io
|
||||
from openpype.pipeline.publish import (
|
||||
OpenPypePyblishPluginMixin
|
||||
)
|
||||
from openpype.lib import (
|
||||
BoolDef,
|
||||
NumberDef
|
||||
)
|
||||
|
||||
|
||||
class FusionSubmitDeadline(pyblish.api.InstancePlugin):
|
||||
class FusionSubmitDeadline(
|
||||
pyblish.api.InstancePlugin,
|
||||
OpenPypePyblishPluginMixin
|
||||
):
|
||||
"""Submit current Comp to Deadline
|
||||
|
||||
Renders are submitted to a Deadline Web Service as
|
||||
|
|
@ -17,12 +27,62 @@ class FusionSubmitDeadline(pyblish.api.InstancePlugin):
|
|||
|
||||
"""
|
||||
|
||||
label = "Submit to Deadline"
|
||||
label = "Submit Fusion to Deadline"
|
||||
order = pyblish.api.IntegratorOrder
|
||||
hosts = ["fusion"]
|
||||
families = ["render.farm"]
|
||||
families = ["render"]
|
||||
targets = ["local"]
|
||||
|
||||
# presets
|
||||
priority = 50
|
||||
chunk_size = 1
|
||||
concurrent_tasks = 1
|
||||
group = ""
|
||||
|
||||
@classmethod
|
||||
def get_attribute_defs(cls):
|
||||
return [
|
||||
NumberDef(
|
||||
"priority",
|
||||
label="Priority",
|
||||
default=cls.priority,
|
||||
decimals=0
|
||||
),
|
||||
NumberDef(
|
||||
"chunk",
|
||||
label="Frames Per Task",
|
||||
default=cls.chunk_size,
|
||||
decimals=0,
|
||||
minimum=1,
|
||||
maximum=1000
|
||||
),
|
||||
NumberDef(
|
||||
"concurrency",
|
||||
label="Concurrency",
|
||||
default=cls.concurrent_tasks,
|
||||
decimals=0,
|
||||
minimum=1,
|
||||
maximum=10
|
||||
),
|
||||
BoolDef(
|
||||
"suspend_publish",
|
||||
default=False,
|
||||
label="Suspend publish"
|
||||
)
|
||||
]
|
||||
|
||||
def process(self, instance):
|
||||
if not instance.data.get("farm"):
|
||||
self.log.info("Skipping local instance.")
|
||||
return
|
||||
|
||||
attribute_values = self.get_attr_values_from_data(
|
||||
instance.data)
|
||||
|
||||
# add suspend_publish attributeValue to instance data
|
||||
instance.data["suspend_publish"] = attribute_values[
|
||||
"suspend_publish"]
|
||||
|
||||
context = instance.context
|
||||
|
||||
key = "__hasRun{}".format(self.__class__.__name__)
|
||||
|
|
@ -33,36 +93,55 @@ class FusionSubmitDeadline(pyblish.api.InstancePlugin):
|
|||
|
||||
from openpype.hosts.fusion.api.lib import get_frame_path
|
||||
|
||||
deadline_url = (
|
||||
context.data["system_settings"]
|
||||
["modules"]
|
||||
["deadline"]
|
||||
["DEADLINE_REST_URL"]
|
||||
)
|
||||
assert deadline_url, "Requires DEADLINE_REST_URL"
|
||||
# get default deadline webservice url from deadline module
|
||||
deadline_url = instance.context.data["defaultDeadline"]
|
||||
# if custom one is set in instance, use that
|
||||
if instance.data.get("deadlineUrl"):
|
||||
deadline_url = instance.data.get("deadlineUrl")
|
||||
assert deadline_url, "Requires Deadline Webservice URL"
|
||||
|
||||
# Collect all saver instances in context that are to be rendered
|
||||
saver_instances = []
|
||||
for instance in context[:]:
|
||||
if not self.families[0] in instance.data.get("families"):
|
||||
for instance in context:
|
||||
if instance.data["family"] != "render":
|
||||
# Allow only saver family instances
|
||||
continue
|
||||
|
||||
if not instance.data.get("publish", True):
|
||||
# Skip inactive instances
|
||||
continue
|
||||
|
||||
self.log.debug(instance.data["name"])
|
||||
saver_instances.append(instance)
|
||||
|
||||
if not saver_instances:
|
||||
raise RuntimeError("No instances found for Deadline submittion")
|
||||
raise RuntimeError("No instances found for Deadline submission")
|
||||
|
||||
fusion_version = int(context.data["fusionVersion"])
|
||||
filepath = context.data["currentFile"]
|
||||
filename = os.path.basename(filepath)
|
||||
comment = context.data.get("comment", "")
|
||||
comment = instance.data.get("comment", "")
|
||||
deadline_user = context.data.get("deadlineUser", getpass.getuser())
|
||||
|
||||
script_path = context.data["currentFile"]
|
||||
|
||||
for item in context:
|
||||
if "workfile" in item.data["families"]:
|
||||
msg = "Workfile (scene) must be published along"
|
||||
assert item.data["publish"] is True, msg
|
||||
|
||||
template_data = item.data.get("anatomyData")
|
||||
rep = item.data.get("representations")[0].get("name")
|
||||
template_data["representation"] = rep
|
||||
template_data["ext"] = rep
|
||||
template_data["comment"] = None
|
||||
anatomy_filled = context.data["anatomy"].format(template_data)
|
||||
template_filled = anatomy_filled["publish"]["path"]
|
||||
script_path = os.path.normpath(template_filled)
|
||||
|
||||
self.log.info(
|
||||
"Using published scene for render {}".format(script_path)
|
||||
)
|
||||
|
||||
filename = os.path.basename(script_path)
|
||||
|
||||
# Documentation for keys available at:
|
||||
# https://docs.thinkboxsoftware.com
|
||||
# /products/deadline/8.0/1_User%20Manual/manual
|
||||
|
|
@ -73,31 +152,41 @@ class FusionSubmitDeadline(pyblish.api.InstancePlugin):
|
|||
"BatchName": filename,
|
||||
|
||||
# Asset dependency to wait for at least the scene file to sync.
|
||||
"AssetDependency0": filepath,
|
||||
"AssetDependency0": script_path,
|
||||
|
||||
# Job name, as seen in Monitor
|
||||
"Name": filename,
|
||||
|
||||
"Priority": attribute_values.get(
|
||||
"priority", self.priority),
|
||||
"ChunkSize": attribute_values.get(
|
||||
"chunk", self.chunk_size),
|
||||
"ConcurrentTasks": attribute_values.get(
|
||||
"concurrency",
|
||||
self.concurrent_tasks
|
||||
),
|
||||
|
||||
# User, as seen in Monitor
|
||||
"UserName": deadline_user,
|
||||
|
||||
# Use a default submission pool for Fusion
|
||||
"Pool": "fusion",
|
||||
"Pool": instance.data.get("primaryPool"),
|
||||
"SecondaryPool": instance.data.get("secondaryPool"),
|
||||
"Group": self.group,
|
||||
|
||||
"Plugin": "Fusion",
|
||||
"Frames": "{start}-{end}".format(
|
||||
start=int(context.data["frameStart"]),
|
||||
end=int(context.data["frameEnd"])
|
||||
start=int(instance.data["frameStartHandle"]),
|
||||
end=int(instance.data["frameEndHandle"])
|
||||
),
|
||||
|
||||
"Comment": comment,
|
||||
},
|
||||
"PluginInfo": {
|
||||
# Input
|
||||
"FlowFile": filepath,
|
||||
"FlowFile": script_path,
|
||||
|
||||
# Mandatory for Deadline
|
||||
"Version": str(fusion_version),
|
||||
"Version": str(instance.data["app_version"]),
|
||||
|
||||
# Render in high quality
|
||||
"HighQuality": True,
|
||||
|
|
@ -108,7 +197,7 @@ class FusionSubmitDeadline(pyblish.api.InstancePlugin):
|
|||
|
||||
# Proxy: higher numbers smaller images for faster test renders
|
||||
# 1 = no proxy quality
|
||||
"Proxy": 1,
|
||||
"Proxy": 1
|
||||
},
|
||||
|
||||
# Mandatory for Deadline, may be empty
|
||||
|
|
@ -117,7 +206,9 @@ class FusionSubmitDeadline(pyblish.api.InstancePlugin):
|
|||
|
||||
# Enable going to rendered frames from Deadline Monitor
|
||||
for index, instance in enumerate(saver_instances):
|
||||
head, padding, tail = get_frame_path(instance.data["path"])
|
||||
head, padding, tail = get_frame_path(
|
||||
instance.data["expectedFiles"][0]
|
||||
)
|
||||
path = "{}{}{}".format(head, "#" * padding, tail)
|
||||
folder, filename = os.path.split(path)
|
||||
payload["JobInfo"]["OutputDirectory%d" % index] = folder
|
||||
|
|
|
|||
|
|
@ -36,6 +36,9 @@ from .lib import (
|
|||
context_plugin_should_run,
|
||||
get_instance_staging_dir,
|
||||
get_publish_repre_path,
|
||||
|
||||
apply_plugin_settings_automatically,
|
||||
get_plugin_settings,
|
||||
)
|
||||
|
||||
from .abstract_expected_files import ExpectedFiles
|
||||
|
|
@ -80,6 +83,9 @@ __all__ = (
|
|||
"get_instance_staging_dir",
|
||||
"get_publish_repre_path",
|
||||
|
||||
"apply_plugin_settings_automatically",
|
||||
"get_plugin_settings",
|
||||
|
||||
"ExpectedFiles",
|
||||
|
||||
"RenderInstance",
|
||||
|
|
|
|||
|
|
@ -320,6 +320,14 @@ def publish_plugins_discover(paths=None):
|
|||
continue
|
||||
|
||||
for plugin in pyblish.plugin.plugins_from_module(module):
|
||||
# Ignore base plugin classes
|
||||
# NOTE 'pyblish.api.discover' does not ignore them!
|
||||
if (
|
||||
plugin is pyblish.api.Plugin
|
||||
or plugin is pyblish.api.ContextPlugin
|
||||
or plugin is pyblish.api.InstancePlugin
|
||||
):
|
||||
continue
|
||||
if not allow_duplicates and plugin.__name__ in plugin_names:
|
||||
result.duplicated_plugins.append(plugin)
|
||||
log.debug("Duplicate plug-in found: %s", plugin)
|
||||
|
|
@ -355,29 +363,55 @@ def publish_plugins_discover(paths=None):
|
|||
return result
|
||||
|
||||
|
||||
def _get_plugin_settings(host_name, project_settings, plugin, log):
|
||||
def get_plugin_settings(plugin, project_settings, log, category=None):
|
||||
"""Get plugin settings based on host name and plugin name.
|
||||
|
||||
Note:
|
||||
Default implementation of automated settings is passing host name
|
||||
into 'category'.
|
||||
|
||||
Args:
|
||||
host_name (str): Name of host.
|
||||
plugin (pyblish.Plugin): Plugin where settings are applied.
|
||||
project_settings (dict[str, Any]): Project settings.
|
||||
plugin (pyliblish.Plugin): Plugin where settings are applied.
|
||||
log (logging.Logger): Logger to log messages.
|
||||
category (Optional[str]): Settings category key where to look
|
||||
for plugin settings.
|
||||
|
||||
Returns:
|
||||
dict[str, Any]: Plugin settings {'attribute': 'value'}.
|
||||
"""
|
||||
|
||||
# Use project settings from host name category when available
|
||||
try:
|
||||
return (
|
||||
project_settings
|
||||
[host_name]
|
||||
["publish"]
|
||||
[plugin.__name__]
|
||||
)
|
||||
except KeyError:
|
||||
pass
|
||||
# Plugin can define settings category by class attribute
|
||||
# - it's impossible to set `settings_category` via settings because
|
||||
# obviously settings are not applied before it.
|
||||
# - if `settings_category` is set the fallback category method is ignored
|
||||
settings_category = getattr(plugin, "settings_category", None)
|
||||
if settings_category:
|
||||
try:
|
||||
return (
|
||||
project_settings
|
||||
[settings_category]
|
||||
["publish"]
|
||||
[plugin.__name__]
|
||||
)
|
||||
except KeyError:
|
||||
log.warning((
|
||||
"Couldn't find plugin '{}' settings"
|
||||
" under settings category '{}'"
|
||||
).format(plugin.__name__, settings_category))
|
||||
return {}
|
||||
|
||||
# Use project settings based on a category name
|
||||
if category:
|
||||
try:
|
||||
return (
|
||||
project_settings
|
||||
[category]
|
||||
["publish"]
|
||||
[plugin.__name__]
|
||||
)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
# Settings category determined from path
|
||||
# - usually path is './<category>/plugins/publish/<plugin file>'
|
||||
|
|
@ -386,9 +420,10 @@ def _get_plugin_settings(host_name, project_settings, plugin, log):
|
|||
|
||||
split_path = filepath.rsplit(os.path.sep, 5)
|
||||
if len(split_path) < 4:
|
||||
log.warning(
|
||||
'plugin path too short to extract host {}'.format(filepath)
|
||||
)
|
||||
log.debug((
|
||||
"Plugin path is too short to automatically"
|
||||
" extract settings category. {}"
|
||||
).format(filepath))
|
||||
return {}
|
||||
|
||||
category_from_file = split_path[-4]
|
||||
|
|
@ -410,6 +445,28 @@ def _get_plugin_settings(host_name, project_settings, plugin, log):
|
|||
return {}
|
||||
|
||||
|
||||
def apply_plugin_settings_automatically(plugin, settings, logger=None):
|
||||
"""Automatically apply plugin settings to a plugin object.
|
||||
|
||||
Note:
|
||||
This function was created to be able to use it in custom overrides of
|
||||
'apply_settings' class method.
|
||||
|
||||
Args:
|
||||
plugin (type[pyblish.api.Plugin]): Class of a plugin.
|
||||
settings (dict[str, Any]): Plugin specific settings.
|
||||
logger (Optional[logging.Logger]): Logger to log debug messages about
|
||||
applied settings values.
|
||||
"""
|
||||
|
||||
for option, value in settings.items():
|
||||
if logger:
|
||||
logger.debug("Plugin {} - Attr: {} -> {}".format(
|
||||
option, value, plugin.__name__
|
||||
))
|
||||
setattr(plugin, option, value)
|
||||
|
||||
|
||||
def filter_pyblish_plugins(plugins):
|
||||
"""Pyblish plugin filter which applies OpenPype settings.
|
||||
|
||||
|
|
@ -453,13 +510,10 @@ def filter_pyblish_plugins(plugins):
|
|||
)
|
||||
else:
|
||||
# Automated
|
||||
plugin_settins = _get_plugin_settings(
|
||||
host_name, project_settings, plugin, log
|
||||
plugin_settins = get_plugin_settings(
|
||||
plugin, project_settings, log, host_name
|
||||
)
|
||||
for option, value in plugin_settins.items():
|
||||
log.info("setting {}:{} on plugin {}".format(
|
||||
option, value, plugin.__name__))
|
||||
setattr(plugin, option, value)
|
||||
apply_plugin_settings_automatically(plugin, plugin_settins, log)
|
||||
|
||||
# Remove disabled plugins
|
||||
if getattr(plugin, "enabled", True) is False:
|
||||
|
|
|
|||
|
|
@ -19,12 +19,13 @@ class OpenInDJV(load.LoaderPlugin):
|
|||
|
||||
djv_list = existing_djv_path()
|
||||
families = ["*"] if djv_list else []
|
||||
representations = [
|
||||
representations = ["*"]
|
||||
extensions = {
|
||||
"cin", "dpx", "avi", "dv", "gif", "flv", "mkv", "mov", "mpg", "mpeg",
|
||||
"mp4", "m4v", "mxf", "iff", "z", "ifl", "jpeg", "jpg", "jfif", "lut",
|
||||
"1dl", "exr", "pic", "png", "ppm", "pnm", "pgm", "pbm", "rla", "rpf",
|
||||
"sgi", "rgba", "rgb", "bw", "tga", "tiff", "tif", "img", "h264",
|
||||
]
|
||||
}
|
||||
|
||||
label = "Open in DJV"
|
||||
order = -10
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -45,6 +45,15 @@
|
|||
"chunk_size": 10,
|
||||
"group": "none"
|
||||
},
|
||||
"FusionSubmitDeadline": {
|
||||
"enabled": true,
|
||||
"optional": false,
|
||||
"active": true,
|
||||
"priority": 50,
|
||||
"chunk_size": 10,
|
||||
"concurrent_tasks": 1,
|
||||
"group": ""
|
||||
},
|
||||
"NukeSubmitDeadline": {
|
||||
"enabled": true,
|
||||
"optional": false,
|
||||
|
|
@ -114,6 +123,9 @@
|
|||
],
|
||||
"max": [
|
||||
".*"
|
||||
],
|
||||
"fusion": [
|
||||
".*"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1069,8 +1069,8 @@
|
|||
"RESOLVE_UTILITY_SCRIPTS_SOURCE_DIR": [],
|
||||
"RESOLVE_PYTHON3_HOME": {
|
||||
"windows": "{LOCALAPPDATA}/Programs/Python/Python36",
|
||||
"darwin": "~/Library/Python/3.6/bin",
|
||||
"linux": "/opt/Python/3.6/bin"
|
||||
"darwin": "/Library/Frameworks/Python.framework/Versions/3.6",
|
||||
"linux": "/opt/Python/3.6"
|
||||
}
|
||||
},
|
||||
"variants": {
|
||||
|
|
|
|||
|
|
@ -248,6 +248,50 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "dict",
|
||||
"collapsible": true,
|
||||
"key": "FusionSubmitDeadline",
|
||||
"label": "Fusion submit to Deadline",
|
||||
"checkbox_key": "enabled",
|
||||
"children": [
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "enabled",
|
||||
"label": "Enabled"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "optional",
|
||||
"label": "Optional"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "active",
|
||||
"label": "Active"
|
||||
},
|
||||
{
|
||||
"type": "number",
|
||||
"key": "priority",
|
||||
"label": "Priority"
|
||||
},
|
||||
{
|
||||
"type": "number",
|
||||
"key": "chunk_size",
|
||||
"label": "Frame per Task"
|
||||
},
|
||||
{
|
||||
"type": "number",
|
||||
"key": "concurrent_tasks",
|
||||
"label": "Number of concurrent tasks"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"key": "group",
|
||||
"label": "Group Name"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "dict",
|
||||
"collapsible": true,
|
||||
|
|
|
|||
|
|
@ -26,8 +26,8 @@
|
|||
|
||||
"bg": "#2C313A",
|
||||
"bg-inputs": "#21252B",
|
||||
"bg-buttons": "#434a56",
|
||||
"bg-button-hover": "rgb(81, 86, 97)",
|
||||
"bg-buttons": "rgb(67, 74, 86)",
|
||||
"bg-buttons-hover": "rgb(81, 86, 97)",
|
||||
"bg-inputs-disabled": "#2C313A",
|
||||
"bg-buttons-disabled": "#434a56",
|
||||
|
||||
|
|
@ -66,7 +66,9 @@
|
|||
"bg-success": "#458056",
|
||||
"bg-success-hover": "#55a066",
|
||||
"bg-error": "#AD2E2E",
|
||||
"bg-error-hover": "#C93636"
|
||||
"bg-error-hover": "#C93636",
|
||||
"bg-info": "rgb(63, 98, 121)",
|
||||
"bg-info-hover": "rgb(81, 146, 181)"
|
||||
},
|
||||
"tab-widget": {
|
||||
"bg": "#21252B",
|
||||
|
|
@ -94,6 +96,7 @@
|
|||
"crash": "#FF6432",
|
||||
"success": "#458056",
|
||||
"warning": "#ffc671",
|
||||
"progress": "rgb(194, 226, 236)",
|
||||
"tab-bg": "#16191d",
|
||||
"list-view-group": {
|
||||
"bg": "#434a56",
|
||||
|
|
|
|||
|
|
@ -136,7 +136,7 @@ QPushButton {
|
|||
}
|
||||
|
||||
QPushButton:hover {
|
||||
background: {color:bg-button-hover};
|
||||
background: {color:bg-buttons-hover};
|
||||
color: {color:font-hover};
|
||||
}
|
||||
|
||||
|
|
@ -166,7 +166,7 @@ QToolButton {
|
|||
}
|
||||
|
||||
QToolButton:hover {
|
||||
background: {color:bg-button-hover};
|
||||
background: {color:bg-buttons-hover};
|
||||
color: {color:font-hover};
|
||||
}
|
||||
|
||||
|
|
@ -722,6 +722,13 @@ OverlayMessageWidget[type="error"]:hover {
|
|||
background: {color:overlay-messages:bg-error-hover};
|
||||
}
|
||||
|
||||
OverlayMessageWidget[type="info"] {
|
||||
background: {color:overlay-messages:bg-info};
|
||||
}
|
||||
OverlayMessageWidget[type="info"]:hover {
|
||||
background: {color:overlay-messages:bg-info-hover};
|
||||
}
|
||||
|
||||
OverlayMessageWidget QWidget {
|
||||
background: transparent;
|
||||
}
|
||||
|
|
@ -749,10 +756,11 @@ OverlayMessageWidget QWidget {
|
|||
}
|
||||
|
||||
#InfoText {
|
||||
padding-left: 30px;
|
||||
padding-top: 20px;
|
||||
padding-left: 0px;
|
||||
padding-top: 0px;
|
||||
padding-right: 20px;
|
||||
background: transparent;
|
||||
border: 1px solid {color:border};
|
||||
border: none;
|
||||
}
|
||||
|
||||
#TypeEditor, #ToolEditor, #NameEditor, #NumberEditor {
|
||||
|
|
@ -914,7 +922,7 @@ PixmapButton{
|
|||
background: {color:bg-buttons};
|
||||
}
|
||||
PixmapButton:hover {
|
||||
background: {color:bg-button-hover};
|
||||
background: {color:bg-buttons-hover};
|
||||
}
|
||||
PixmapButton:disabled {
|
||||
background: {color:bg-buttons-disabled};
|
||||
|
|
@ -925,7 +933,7 @@ PixmapButton:disabled {
|
|||
background: {color:bg-view};
|
||||
}
|
||||
#ThumbnailPixmapHoverButton:hover {
|
||||
background: {color:bg-button-hover};
|
||||
background: {color:bg-buttons-hover};
|
||||
}
|
||||
|
||||
#CreatorDetailedDescription {
|
||||
|
|
@ -946,7 +954,7 @@ PixmapButton:disabled {
|
|||
}
|
||||
|
||||
#CreateDialogHelpButton:hover {
|
||||
background: {color:bg-button-hover};
|
||||
background: {color:bg-buttons-hover};
|
||||
}
|
||||
#CreateDialogHelpButton QWidget {
|
||||
background: transparent;
|
||||
|
|
@ -1005,7 +1013,7 @@ PixmapButton:disabled {
|
|||
border-radius: 0.2em;
|
||||
}
|
||||
#CardViewWidget:hover {
|
||||
background: {color:bg-button-hover};
|
||||
background: {color:bg-buttons-hover};
|
||||
}
|
||||
#CardViewWidget[state="selected"] {
|
||||
background: {color:bg-view-selection};
|
||||
|
|
@ -1032,7 +1040,7 @@ PixmapButton:disabled {
|
|||
}
|
||||
|
||||
#PublishInfoFrame[state="3"], #PublishInfoFrame[state="4"] {
|
||||
background: rgb(194, 226, 236);
|
||||
background: {color:publisher:progress};
|
||||
}
|
||||
|
||||
#PublishInfoFrame QLabel {
|
||||
|
|
@ -1040,6 +1048,11 @@ PixmapButton:disabled {
|
|||
font-style: bold;
|
||||
}
|
||||
|
||||
#PublishReportHeader {
|
||||
font-size: 14pt;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#PublishInfoMainLabel {
|
||||
font-size: 12pt;
|
||||
}
|
||||
|
|
@ -1060,7 +1073,7 @@ ValidationArtistMessage QLabel {
|
|||
}
|
||||
|
||||
#ValidationActionButton:hover {
|
||||
background: {color:bg-button-hover};
|
||||
background: {color:bg-buttons-hover};
|
||||
color: {color:font-hover};
|
||||
}
|
||||
|
||||
|
|
@ -1090,6 +1103,35 @@ ValidationArtistMessage QLabel {
|
|||
border-left: 1px solid {color:border};
|
||||
}
|
||||
|
||||
#PublishInstancesDetails {
|
||||
border: 1px solid {color:border};
|
||||
border-radius: 0.3em;
|
||||
}
|
||||
|
||||
#InstancesLogsView {
|
||||
border: 1px solid {color:border};
|
||||
background: {color:bg-view};
|
||||
border-radius: 0.3em;
|
||||
}
|
||||
|
||||
#PublishLogMessage {
|
||||
font-family: "Noto Sans Mono";
|
||||
}
|
||||
|
||||
#PublishInstanceLogsLabel {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#PublishCrashMainLabel{
|
||||
font-weight: bold;
|
||||
font-size: 16pt;
|
||||
}
|
||||
|
||||
#PublishCrashReportLabel {
|
||||
font-weight: bold;
|
||||
font-size: 13pt;
|
||||
}
|
||||
|
||||
#AssetNameInputWidget {
|
||||
background: {color:bg-inputs};
|
||||
border: 1px solid {color:border};
|
||||
|
|
|
|||
|
|
@ -198,29 +198,33 @@ class DropEmpty(QtWidgets.QWidget):
|
|||
|
||||
def paintEvent(self, event):
|
||||
super(DropEmpty, self).paintEvent(event)
|
||||
painter = QtGui.QPainter(self)
|
||||
|
||||
pen = QtGui.QPen()
|
||||
pen.setWidth(1)
|
||||
pen.setBrush(QtCore.Qt.darkGray)
|
||||
pen.setStyle(QtCore.Qt.DashLine)
|
||||
painter.setPen(pen)
|
||||
content_margins = self.layout().contentsMargins()
|
||||
pen.setWidth(1)
|
||||
|
||||
left_m = content_margins.left()
|
||||
top_m = content_margins.top()
|
||||
rect = QtCore.QRect(
|
||||
content_margins = self.layout().contentsMargins()
|
||||
rect = self.rect()
|
||||
left_m = content_margins.left() + pen.width()
|
||||
top_m = content_margins.top() + pen.width()
|
||||
new_rect = QtCore.QRect(
|
||||
left_m,
|
||||
top_m,
|
||||
(
|
||||
self.rect().width()
|
||||
rect.width()
|
||||
- (left_m + content_margins.right() + pen.width())
|
||||
),
|
||||
(
|
||||
self.rect().height()
|
||||
rect.height()
|
||||
- (top_m + content_margins.bottom() + pen.width())
|
||||
)
|
||||
)
|
||||
painter.drawRect(rect)
|
||||
|
||||
painter = QtGui.QPainter(self)
|
||||
painter.setRenderHint(QtGui.QPainter.Antialiasing)
|
||||
painter.setPen(pen)
|
||||
painter.drawRect(new_rect)
|
||||
|
||||
|
||||
class FilesModel(QtGui.QStandardItemModel):
|
||||
|
|
|
|||
|
|
@ -35,9 +35,13 @@ ResetKeySequence = QtGui.QKeySequence(
|
|||
|
||||
__all__ = (
|
||||
"CONTEXT_ID",
|
||||
"CONTEXT_LABEL",
|
||||
|
||||
"VARIANT_TOOLTIP",
|
||||
|
||||
"INPUTS_LAYOUT_HSPACING",
|
||||
"INPUTS_LAYOUT_VSPACING",
|
||||
|
||||
"INSTANCE_ID_ROLE",
|
||||
"SORT_VALUE_ROLE",
|
||||
"IS_GROUP_ROLE",
|
||||
|
|
@ -47,4 +51,6 @@ __all__ = (
|
|||
"FAMILY_ROLE",
|
||||
"GROUP_ROLE",
|
||||
"CONVERTER_IDENTIFIER_ROLE",
|
||||
|
||||
"ResetKeySequence",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ PLUGIN_ORDER_OFFSET = 0.5
|
|||
|
||||
class CardMessageTypes:
|
||||
standard = None
|
||||
info = "info"
|
||||
error = "error"
|
||||
|
||||
|
||||
|
|
@ -220,7 +221,12 @@ class PublishReportMaker:
|
|||
|
||||
def _add_plugin_data_item(self, plugin):
|
||||
if plugin in self._stored_plugins:
|
||||
raise ValueError("Plugin is already stored")
|
||||
# A plugin would be processed more than once. What can cause it:
|
||||
# - there is a bug in controller
|
||||
# - plugin class is imported into multiple files
|
||||
# - this can happen even with base classes from 'pyblish'
|
||||
raise ValueError(
|
||||
"Plugin '{}' is already stored".format(str(plugin)))
|
||||
|
||||
self._stored_plugins.append(plugin)
|
||||
|
||||
|
|
@ -239,6 +245,7 @@ class PublishReportMaker:
|
|||
label = plugin.label
|
||||
|
||||
return {
|
||||
"id": plugin.id,
|
||||
"name": plugin.__name__,
|
||||
"label": label,
|
||||
"order": plugin.order,
|
||||
|
|
@ -324,7 +331,7 @@ class PublishReportMaker:
|
|||
"instances": instances_details,
|
||||
"context": self._extract_context_data(self._current_context),
|
||||
"crashed_file_paths": crashed_file_paths,
|
||||
"id": str(uuid.uuid4()),
|
||||
"id": uuid.uuid4().hex,
|
||||
"report_version": "1.0.0"
|
||||
}
|
||||
|
||||
|
|
@ -342,7 +349,9 @@ class PublishReportMaker:
|
|||
"label": instance.data.get("label"),
|
||||
"family": instance.data["family"],
|
||||
"families": instance.data.get("families") or [],
|
||||
"exists": exists
|
||||
"exists": exists,
|
||||
"creator_identifier": instance.data.get("creator_identifier"),
|
||||
"instance_id": instance.data.get("instance_id"),
|
||||
}
|
||||
|
||||
def _extract_instance_log_items(self, result):
|
||||
|
|
@ -388,8 +397,11 @@ class PublishReportMaker:
|
|||
exception = result.get("error")
|
||||
if exception:
|
||||
fname, line_no, func, exc = exception.traceback
|
||||
# Action result does not have 'is_validation_error'
|
||||
is_validation_error = result.get("is_validation_error", False)
|
||||
output.append({
|
||||
"type": "error",
|
||||
"is_validation_error": is_validation_error,
|
||||
"msg": str(exception),
|
||||
"filename": str(fname),
|
||||
"lineno": str(line_no),
|
||||
|
|
@ -426,13 +438,15 @@ class PublishPluginsProxy:
|
|||
plugin_id = plugin.id
|
||||
plugins_by_id[plugin_id] = plugin
|
||||
|
||||
action_ids = set()
|
||||
action_ids = []
|
||||
action_ids_by_plugin_id[plugin_id] = action_ids
|
||||
|
||||
actions = getattr(plugin, "actions", None) or []
|
||||
for action in actions:
|
||||
action_id = action.id
|
||||
action_ids.add(action_id)
|
||||
if action_id in actions_by_id:
|
||||
continue
|
||||
action_ids.append(action_id)
|
||||
actions_by_id[action_id] = action
|
||||
|
||||
self._plugins_by_id = plugins_by_id
|
||||
|
|
@ -461,7 +475,7 @@ class PublishPluginsProxy:
|
|||
return plugin.id
|
||||
|
||||
def get_plugin_action_items(self, plugin_id):
|
||||
"""Get plugin action items for plugin by it's id.
|
||||
"""Get plugin action items for plugin by its id.
|
||||
|
||||
Args:
|
||||
plugin_id (str): Publish plugin id.
|
||||
|
|
@ -568,7 +582,7 @@ class ValidationErrorItem:
|
|||
context_validation,
|
||||
title,
|
||||
description,
|
||||
detail,
|
||||
detail
|
||||
):
|
||||
self.instance_id = instance_id
|
||||
self.instance_label = instance_label
|
||||
|
|
@ -677,6 +691,8 @@ class PublishValidationErrorsReport:
|
|||
|
||||
for title in titles:
|
||||
grouped_error_items.append({
|
||||
"id": uuid.uuid4().hex,
|
||||
"plugin_id": plugin_id,
|
||||
"plugin_action_items": list(plugin_action_items),
|
||||
"error_items": error_items_by_title[title],
|
||||
"title": title
|
||||
|
|
@ -2379,7 +2395,8 @@ class PublisherController(BasePublisherController):
|
|||
yield MainThreadItem(self.stop_publish)
|
||||
|
||||
# Add plugin to publish report
|
||||
self._publish_report.add_plugin_iter(plugin, self._publish_context)
|
||||
self._publish_report.add_plugin_iter(
|
||||
plugin, self._publish_context)
|
||||
|
||||
# WARNING This is hack fix for optional plugins
|
||||
if not self._is_publish_plugin_active(plugin):
|
||||
|
|
@ -2461,14 +2478,14 @@ class PublisherController(BasePublisherController):
|
|||
plugin, self._publish_context, instance
|
||||
)
|
||||
|
||||
self._publish_report.add_result(result)
|
||||
|
||||
exception = result.get("error")
|
||||
if exception:
|
||||
has_validation_error = False
|
||||
if (
|
||||
isinstance(exception, PublishValidationError)
|
||||
and not self.publish_has_validated
|
||||
):
|
||||
has_validation_error = True
|
||||
self._add_validation_error(result)
|
||||
|
||||
else:
|
||||
|
|
@ -2482,6 +2499,10 @@ class PublisherController(BasePublisherController):
|
|||
self.publish_error_msg = msg
|
||||
self.publish_has_crashed = True
|
||||
|
||||
result["is_validation_error"] = has_validation_error
|
||||
|
||||
self._publish_report.add_result(result)
|
||||
|
||||
self._publish_next_process()
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -163,7 +163,11 @@ class ZoomPlainText(QtWidgets.QPlainTextEdit):
|
|||
super(ZoomPlainText, self).wheelEvent(event)
|
||||
return
|
||||
|
||||
degrees = float(event.delta()) / 8
|
||||
if hasattr(event, "angleDelta"):
|
||||
delta = event.angleDelta().y()
|
||||
else:
|
||||
delta = event.delta()
|
||||
degrees = float(delta) / 8
|
||||
steps = int(ceil(degrees / 5))
|
||||
self._scheduled_scalings += steps
|
||||
if (self._scheduled_scalings * steps < 0):
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ from .help_widget import (
|
|||
from .publish_frame import PublishFrame
|
||||
from .tabs_widget import PublisherTabsWidget
|
||||
from .overview_widget import OverviewWidget
|
||||
from .validations_widget import ValidationsWidget
|
||||
from .report_page import ReportPageWidget
|
||||
|
||||
|
||||
__all__ = (
|
||||
|
|
@ -40,5 +40,5 @@ __all__ = (
|
|||
|
||||
"PublisherTabsWidget",
|
||||
"OverviewWidget",
|
||||
"ValidationsWidget",
|
||||
"ReportPageWidget",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ class BaseGroupWidget(QtWidgets.QWidget):
|
|||
return self._group
|
||||
|
||||
def get_widget_by_item_id(self, item_id):
|
||||
"""Get instance widget by it's id."""
|
||||
"""Get instance widget by its id."""
|
||||
|
||||
return self._widgets_by_id.get(item_id)
|
||||
|
||||
|
|
@ -702,8 +702,8 @@ class InstanceCardView(AbstractInstanceView):
|
|||
|
||||
for group_name in sorted_group_names:
|
||||
group_icons = {
|
||||
idenfier: self._controller.get_creator_icon(idenfier)
|
||||
for idenfier in identifiers_by_group[group_name]
|
||||
identifier: self._controller.get_creator_icon(identifier)
|
||||
for identifier in identifiers_by_group[group_name]
|
||||
}
|
||||
if group_name in self._widgets_by_group:
|
||||
group_widget = self._widgets_by_group[group_name]
|
||||
|
|
|
|||
BIN
openpype/tools/publisher/widgets/images/error.png
Normal file
BIN
openpype/tools/publisher/widgets/images/error.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
BIN
openpype/tools/publisher/widgets/images/success.png
Normal file
BIN
openpype/tools/publisher/widgets/images/success.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 9.5 KiB After Width: | Height: | Size: 11 KiB |
|
|
@ -468,45 +468,14 @@ class PublishFrame(QtWidgets.QWidget):
|
|||
widget.setProperty("state", state)
|
||||
widget.style().polish(widget)
|
||||
|
||||
def _copy_report(self):
|
||||
logs = self._controller.get_publish_report()
|
||||
logs_string = json.dumps(logs, indent=4)
|
||||
|
||||
mime_data = QtCore.QMimeData()
|
||||
mime_data.setText(logs_string)
|
||||
QtWidgets.QApplication.instance().clipboard().setMimeData(
|
||||
mime_data
|
||||
)
|
||||
|
||||
def _export_report(self):
|
||||
default_filename = "publish-report-{}".format(
|
||||
time.strftime("%y%m%d-%H-%M")
|
||||
)
|
||||
default_filepath = os.path.join(
|
||||
os.path.expanduser("~"),
|
||||
default_filename
|
||||
)
|
||||
new_filepath, ext = QtWidgets.QFileDialog.getSaveFileName(
|
||||
self, "Save report", default_filepath, ".json"
|
||||
)
|
||||
if not ext or not new_filepath:
|
||||
return
|
||||
|
||||
logs = self._controller.get_publish_report()
|
||||
full_path = new_filepath + ext
|
||||
dir_path = os.path.dirname(full_path)
|
||||
if not os.path.exists(dir_path):
|
||||
os.makedirs(dir_path)
|
||||
|
||||
with open(full_path, "w") as file_stream:
|
||||
json.dump(logs, file_stream)
|
||||
|
||||
def _on_report_triggered(self, identifier):
|
||||
if identifier == "export_report":
|
||||
self._export_report()
|
||||
self._controller.event_system.emit(
|
||||
"export_report.request", {}, "publish_frame")
|
||||
|
||||
elif identifier == "copy_report":
|
||||
self._copy_report()
|
||||
self._controller.event_system.emit(
|
||||
"copy_report.request", {}, "publish_frame")
|
||||
|
||||
elif identifier == "go_to_report":
|
||||
self.details_page_requested.emit()
|
||||
|
|
|
|||
1876
openpype/tools/publisher/widgets/report_page.py
Normal file
1876
openpype/tools/publisher/widgets/report_page.py
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -75,6 +75,7 @@ class ThumbnailPainterWidget(QtWidgets.QWidget):
|
|||
|
||||
painter = QtGui.QPainter()
|
||||
painter.begin(self)
|
||||
painter.setRenderHint(QtGui.QPainter.Antialiasing)
|
||||
painter.drawPixmap(0, 0, self._cached_pix)
|
||||
painter.end()
|
||||
|
||||
|
|
@ -183,6 +184,18 @@ class ThumbnailPainterWidget(QtWidgets.QWidget):
|
|||
backgrounded_images.append(new_pix)
|
||||
return backgrounded_images
|
||||
|
||||
def _paint_dash_line(self, painter, rect):
|
||||
pen = QtGui.QPen()
|
||||
pen.setWidth(1)
|
||||
pen.setBrush(QtCore.Qt.darkGray)
|
||||
pen.setStyle(QtCore.Qt.DashLine)
|
||||
|
||||
new_rect = rect.adjusted(1, 1, -1, -1)
|
||||
painter.setPen(pen)
|
||||
painter.setBrush(QtCore.Qt.transparent)
|
||||
# painter.drawRect(rect)
|
||||
painter.drawRect(new_rect)
|
||||
|
||||
def _cache_pix(self):
|
||||
rect = self.rect()
|
||||
rect_width = rect.width()
|
||||
|
|
@ -264,13 +277,7 @@ class ThumbnailPainterWidget(QtWidgets.QWidget):
|
|||
|
||||
# Draw drop enabled dashes
|
||||
if used_default_pix:
|
||||
pen = QtGui.QPen()
|
||||
pen.setWidth(1)
|
||||
pen.setBrush(QtCore.Qt.darkGray)
|
||||
pen.setStyle(QtCore.Qt.DashLine)
|
||||
final_painter.setPen(pen)
|
||||
final_painter.setBrush(QtCore.Qt.transparent)
|
||||
final_painter.drawRect(rect)
|
||||
self._paint_dash_line(final_painter, rect)
|
||||
|
||||
final_painter.end()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,715 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
try:
|
||||
import commonmark
|
||||
except Exception:
|
||||
commonmark = None
|
||||
|
||||
from qtpy import QtWidgets, QtCore, QtGui
|
||||
|
||||
from openpype.tools.utils import BaseClickableFrame, ClickableFrame
|
||||
from .widgets import (
|
||||
IconValuePixmapLabel
|
||||
)
|
||||
from ..constants import (
|
||||
INSTANCE_ID_ROLE
|
||||
)
|
||||
|
||||
|
||||
class ValidationErrorInstanceList(QtWidgets.QListView):
|
||||
"""List of publish instances that caused a validation error.
|
||||
|
||||
Instances are collected per plugin's validation error title.
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ValidationErrorInstanceList, self).__init__(*args, **kwargs)
|
||||
|
||||
self.setObjectName("ValidationErrorInstanceList")
|
||||
|
||||
self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
|
||||
self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
|
||||
|
||||
def minimumSizeHint(self):
|
||||
return self.sizeHint()
|
||||
|
||||
def sizeHint(self):
|
||||
result = super(ValidationErrorInstanceList, self).sizeHint()
|
||||
row_count = self.model().rowCount()
|
||||
height = 0
|
||||
if row_count > 0:
|
||||
height = self.sizeHintForRow(0) * row_count
|
||||
result.setHeight(height)
|
||||
return result
|
||||
|
||||
|
||||
class ValidationErrorTitleWidget(QtWidgets.QWidget):
|
||||
"""Title of validation error.
|
||||
|
||||
Widget is used as radio button so requires clickable functionality and
|
||||
changing style on selection/deselection.
|
||||
|
||||
Has toggle button to show/hide instances on which validation error happened
|
||||
if there is a list (Valdation error may happen on context).
|
||||
"""
|
||||
|
||||
selected = QtCore.Signal(int)
|
||||
instance_changed = QtCore.Signal(int)
|
||||
|
||||
def __init__(self, index, error_info, parent):
|
||||
super(ValidationErrorTitleWidget, self).__init__(parent)
|
||||
|
||||
self._index = index
|
||||
self._error_info = error_info
|
||||
self._selected = False
|
||||
|
||||
title_frame = ClickableFrame(self)
|
||||
title_frame.setObjectName("ValidationErrorTitleFrame")
|
||||
|
||||
toggle_instance_btn = QtWidgets.QToolButton(title_frame)
|
||||
toggle_instance_btn.setObjectName("ArrowBtn")
|
||||
toggle_instance_btn.setArrowType(QtCore.Qt.RightArrow)
|
||||
toggle_instance_btn.setMaximumWidth(14)
|
||||
|
||||
label_widget = QtWidgets.QLabel(error_info["title"], title_frame)
|
||||
|
||||
title_frame_layout = QtWidgets.QHBoxLayout(title_frame)
|
||||
title_frame_layout.addWidget(label_widget, 1)
|
||||
title_frame_layout.addWidget(toggle_instance_btn, 0)
|
||||
|
||||
instances_model = QtGui.QStandardItemModel()
|
||||
|
||||
help_text_by_instance_id = {}
|
||||
|
||||
items = []
|
||||
context_validation = False
|
||||
for error_item in error_info["error_items"]:
|
||||
context_validation = error_item.context_validation
|
||||
if context_validation:
|
||||
toggle_instance_btn.setArrowType(QtCore.Qt.NoArrow)
|
||||
description = self._prepare_description(error_item)
|
||||
help_text_by_instance_id[None] = description
|
||||
# Add fake item to have minimum size hint of view widget
|
||||
items.append(QtGui.QStandardItem("Context"))
|
||||
continue
|
||||
|
||||
label = error_item.instance_label
|
||||
item = QtGui.QStandardItem(label)
|
||||
item.setFlags(
|
||||
QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable
|
||||
)
|
||||
item.setData(label, QtCore.Qt.ToolTipRole)
|
||||
item.setData(error_item.instance_id, INSTANCE_ID_ROLE)
|
||||
items.append(item)
|
||||
description = self._prepare_description(error_item)
|
||||
help_text_by_instance_id[error_item.instance_id] = description
|
||||
|
||||
if items:
|
||||
root_item = instances_model.invisibleRootItem()
|
||||
root_item.appendRows(items)
|
||||
|
||||
instances_view = ValidationErrorInstanceList(self)
|
||||
instances_view.setModel(instances_model)
|
||||
|
||||
self.setLayoutDirection(QtCore.Qt.LeftToRight)
|
||||
|
||||
view_widget = QtWidgets.QWidget(self)
|
||||
view_layout = QtWidgets.QHBoxLayout(view_widget)
|
||||
view_layout.setContentsMargins(0, 0, 0, 0)
|
||||
view_layout.setSpacing(0)
|
||||
view_layout.addSpacing(14)
|
||||
view_layout.addWidget(instances_view, 0)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
layout.setSpacing(0)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addWidget(title_frame, 0)
|
||||
layout.addWidget(view_widget, 0)
|
||||
view_widget.setVisible(False)
|
||||
|
||||
if not context_validation:
|
||||
toggle_instance_btn.clicked.connect(self._on_toggle_btn_click)
|
||||
|
||||
title_frame.clicked.connect(self._mouse_release_callback)
|
||||
instances_view.selectionModel().selectionChanged.connect(
|
||||
self._on_seleciton_change
|
||||
)
|
||||
|
||||
self._title_frame = title_frame
|
||||
|
||||
self._toggle_instance_btn = toggle_instance_btn
|
||||
|
||||
self._view_widget = view_widget
|
||||
|
||||
self._instances_model = instances_model
|
||||
self._instances_view = instances_view
|
||||
|
||||
self._context_validation = context_validation
|
||||
self._help_text_by_instance_id = help_text_by_instance_id
|
||||
|
||||
self._expanded = False
|
||||
|
||||
def sizeHint(self):
|
||||
result = super(ValidationErrorTitleWidget, self).sizeHint()
|
||||
expected_width = max(
|
||||
self._view_widget.minimumSizeHint().width(),
|
||||
self._view_widget.sizeHint().width()
|
||||
)
|
||||
|
||||
if expected_width < 200:
|
||||
expected_width = 200
|
||||
|
||||
if result.width() < expected_width:
|
||||
result.setWidth(expected_width)
|
||||
|
||||
return result
|
||||
|
||||
def minimumSizeHint(self):
|
||||
return self.sizeHint()
|
||||
|
||||
def _prepare_description(self, error_item):
|
||||
"""Prepare description text for detail intput.
|
||||
|
||||
Args:
|
||||
error_item (ValidationErrorItem): Item which hold information about
|
||||
validation error.
|
||||
|
||||
Returns:
|
||||
str: Prepared detailed description.
|
||||
"""
|
||||
|
||||
dsc = error_item.description
|
||||
detail = error_item.detail
|
||||
if detail:
|
||||
dsc += "<br/><br/>{}".format(detail)
|
||||
|
||||
description = dsc
|
||||
if commonmark:
|
||||
description = commonmark.commonmark(dsc)
|
||||
return description
|
||||
|
||||
def _mouse_release_callback(self):
|
||||
"""Mark this widget as selected on click."""
|
||||
|
||||
self.set_selected(True)
|
||||
|
||||
def current_description_text(self):
|
||||
if self._context_validation:
|
||||
return self._help_text_by_instance_id[None]
|
||||
index = self._instances_view.currentIndex()
|
||||
# TODO make sure instance is selected
|
||||
if not index.isValid():
|
||||
index = self._instances_model.index(0, 0)
|
||||
|
||||
indence_id = index.data(INSTANCE_ID_ROLE)
|
||||
return self._help_text_by_instance_id[indence_id]
|
||||
|
||||
@property
|
||||
def is_selected(self):
|
||||
"""Is widget marked a selected.
|
||||
|
||||
Returns:
|
||||
bool: Item is selected or not.
|
||||
"""
|
||||
|
||||
return self._selected
|
||||
|
||||
@property
|
||||
def index(self):
|
||||
"""Widget's index set by parent.
|
||||
|
||||
Returns:
|
||||
int: Index of widget.
|
||||
"""
|
||||
|
||||
return self._index
|
||||
|
||||
def set_index(self, index):
|
||||
"""Set index of widget (called by parent).
|
||||
|
||||
Args:
|
||||
int: New index of widget.
|
||||
"""
|
||||
|
||||
self._index = index
|
||||
|
||||
def _change_style_property(self, selected):
|
||||
"""Change style of widget based on selection."""
|
||||
|
||||
value = "1" if selected else ""
|
||||
self._title_frame.setProperty("selected", value)
|
||||
self._title_frame.style().polish(self._title_frame)
|
||||
|
||||
def set_selected(self, selected=None):
|
||||
"""Change selected state of widget."""
|
||||
|
||||
if selected is None:
|
||||
selected = not self._selected
|
||||
|
||||
# Clear instance view selection on deselect
|
||||
if not selected:
|
||||
self._instances_view.clearSelection()
|
||||
|
||||
# Skip if has same value
|
||||
if selected == self._selected:
|
||||
return
|
||||
|
||||
self._selected = selected
|
||||
self._change_style_property(selected)
|
||||
if selected:
|
||||
self.selected.emit(self._index)
|
||||
self._set_expanded(True)
|
||||
|
||||
def _on_toggle_btn_click(self):
|
||||
"""Show/hide instances list."""
|
||||
|
||||
self._set_expanded()
|
||||
|
||||
def _set_expanded(self, expanded=None):
|
||||
if expanded is None:
|
||||
expanded = not self._expanded
|
||||
|
||||
elif expanded is self._expanded:
|
||||
return
|
||||
|
||||
if expanded and self._context_validation:
|
||||
return
|
||||
|
||||
self._expanded = expanded
|
||||
self._view_widget.setVisible(expanded)
|
||||
if expanded:
|
||||
self._toggle_instance_btn.setArrowType(QtCore.Qt.DownArrow)
|
||||
else:
|
||||
self._toggle_instance_btn.setArrowType(QtCore.Qt.RightArrow)
|
||||
|
||||
def _on_seleciton_change(self):
|
||||
sel_model = self._instances_view.selectionModel()
|
||||
if sel_model.selectedIndexes():
|
||||
self.instance_changed.emit(self._index)
|
||||
|
||||
|
||||
class ActionButton(BaseClickableFrame):
|
||||
"""Plugin's action callback button.
|
||||
|
||||
Action may have label or icon or both.
|
||||
|
||||
Args:
|
||||
plugin_action_item (PublishPluginActionItem): Action item that can be
|
||||
triggered by it's id.
|
||||
"""
|
||||
|
||||
action_clicked = QtCore.Signal(str, str)
|
||||
|
||||
def __init__(self, plugin_action_item, parent):
|
||||
super(ActionButton, self).__init__(parent)
|
||||
|
||||
self.setObjectName("ValidationActionButton")
|
||||
|
||||
self.plugin_action_item = plugin_action_item
|
||||
|
||||
action_label = plugin_action_item.label
|
||||
action_icon = plugin_action_item.icon
|
||||
label_widget = QtWidgets.QLabel(action_label, self)
|
||||
icon_label = None
|
||||
if action_icon:
|
||||
icon_label = IconValuePixmapLabel(action_icon, self)
|
||||
|
||||
layout = QtWidgets.QHBoxLayout(self)
|
||||
layout.setContentsMargins(5, 0, 5, 0)
|
||||
layout.addWidget(label_widget, 1)
|
||||
if icon_label:
|
||||
layout.addWidget(icon_label, 0)
|
||||
|
||||
self.setSizePolicy(
|
||||
QtWidgets.QSizePolicy.Minimum,
|
||||
self.sizePolicy().verticalPolicy()
|
||||
)
|
||||
|
||||
def _mouse_release_callback(self):
|
||||
self.action_clicked.emit(
|
||||
self.plugin_action_item.plugin_id,
|
||||
self.plugin_action_item.action_id
|
||||
)
|
||||
|
||||
|
||||
class ValidateActionsWidget(QtWidgets.QFrame):
|
||||
"""Wrapper widget for plugin actions.
|
||||
|
||||
Change actions based on selected validation error.
|
||||
"""
|
||||
|
||||
def __init__(self, controller, parent):
|
||||
super(ValidateActionsWidget, self).__init__(parent)
|
||||
|
||||
self.setAttribute(QtCore.Qt.WA_TranslucentBackground)
|
||||
|
||||
content_widget = QtWidgets.QWidget(self)
|
||||
content_layout = QtWidgets.QVBoxLayout(content_widget)
|
||||
|
||||
layout = QtWidgets.QHBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addWidget(content_widget)
|
||||
|
||||
self._controller = controller
|
||||
self._content_widget = content_widget
|
||||
self._content_layout = content_layout
|
||||
self._actions_mapping = {}
|
||||
|
||||
def clear(self):
|
||||
"""Remove actions from widget."""
|
||||
while self._content_layout.count():
|
||||
item = self._content_layout.takeAt(0)
|
||||
widget = item.widget()
|
||||
if widget:
|
||||
widget.setVisible(False)
|
||||
widget.deleteLater()
|
||||
self._actions_mapping = {}
|
||||
|
||||
def set_error_item(self, error_item):
|
||||
"""Set selected plugin and show it's actions.
|
||||
|
||||
Clears current actions from widget and recreate them from the plugin.
|
||||
|
||||
Args:
|
||||
Dict[str, Any]: Object holding error items, title and possible
|
||||
actions to run.
|
||||
"""
|
||||
|
||||
self.clear()
|
||||
|
||||
if not error_item:
|
||||
self.setVisible(False)
|
||||
return
|
||||
|
||||
plugin_action_items = error_item["plugin_action_items"]
|
||||
for plugin_action_item in plugin_action_items:
|
||||
if not plugin_action_item.active:
|
||||
continue
|
||||
|
||||
if plugin_action_item.on_filter not in ("failed", "all"):
|
||||
continue
|
||||
|
||||
action_id = plugin_action_item.action_id
|
||||
self._actions_mapping[action_id] = plugin_action_item
|
||||
|
||||
action_btn = ActionButton(plugin_action_item, self._content_widget)
|
||||
action_btn.action_clicked.connect(self._on_action_click)
|
||||
self._content_layout.addWidget(action_btn)
|
||||
|
||||
if self._content_layout.count() > 0:
|
||||
self.setVisible(True)
|
||||
self._content_layout.addStretch(1)
|
||||
else:
|
||||
self.setVisible(False)
|
||||
|
||||
def _on_action_click(self, plugin_id, action_id):
|
||||
self._controller.run_action(plugin_id, action_id)
|
||||
|
||||
|
||||
class VerticallScrollArea(QtWidgets.QScrollArea):
|
||||
"""Scroll area for validation error titles.
|
||||
|
||||
The biggest difference is that the scroll area has scroll bar on left side
|
||||
and resize of content will also resize scrollarea itself.
|
||||
|
||||
Resize if deferred by 100ms because at the moment of resize are not yet
|
||||
propagated sizes and visibility of scroll bars.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(VerticallScrollArea, self).__init__(*args, **kwargs)
|
||||
|
||||
self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
|
||||
self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
|
||||
self.setLayoutDirection(QtCore.Qt.RightToLeft)
|
||||
|
||||
self.setAttribute(QtCore.Qt.WA_TranslucentBackground)
|
||||
# Background of scrollbar will be transparent
|
||||
scrollbar_bg = self.verticalScrollBar().parent()
|
||||
if scrollbar_bg:
|
||||
scrollbar_bg.setAttribute(QtCore.Qt.WA_TranslucentBackground)
|
||||
self.setViewportMargins(0, 0, 0, 0)
|
||||
|
||||
self.verticalScrollBar().installEventFilter(self)
|
||||
|
||||
# Timer with 100ms offset after changing size
|
||||
size_changed_timer = QtCore.QTimer()
|
||||
size_changed_timer.setInterval(100)
|
||||
size_changed_timer.setSingleShot(True)
|
||||
|
||||
size_changed_timer.timeout.connect(self._on_timer_timeout)
|
||||
self._size_changed_timer = size_changed_timer
|
||||
|
||||
def setVerticalScrollBar(self, widget):
|
||||
old_widget = self.verticalScrollBar()
|
||||
if old_widget:
|
||||
old_widget.removeEventFilter(self)
|
||||
|
||||
super(VerticallScrollArea, self).setVerticalScrollBar(widget)
|
||||
if widget:
|
||||
widget.installEventFilter(self)
|
||||
|
||||
def setWidget(self, widget):
|
||||
old_widget = self.widget()
|
||||
if old_widget:
|
||||
old_widget.removeEventFilter(self)
|
||||
|
||||
super(VerticallScrollArea, self).setWidget(widget)
|
||||
if widget:
|
||||
widget.installEventFilter(self)
|
||||
|
||||
def _on_timer_timeout(self):
|
||||
width = self.widget().width()
|
||||
if self.verticalScrollBar().isVisible():
|
||||
width += self.verticalScrollBar().width()
|
||||
self.setMinimumWidth(width)
|
||||
|
||||
def eventFilter(self, obj, event):
|
||||
if (
|
||||
event.type() == QtCore.QEvent.Resize
|
||||
and (obj is self.widget() or obj is self.verticalScrollBar())
|
||||
):
|
||||
self._size_changed_timer.start()
|
||||
return super(VerticallScrollArea, self).eventFilter(obj, event)
|
||||
|
||||
|
||||
class ValidationArtistMessage(QtWidgets.QWidget):
|
||||
def __init__(self, message, parent):
|
||||
super(ValidationArtistMessage, self).__init__(parent)
|
||||
|
||||
artist_msg_label = QtWidgets.QLabel(message, self)
|
||||
artist_msg_label.setAlignment(QtCore.Qt.AlignCenter)
|
||||
|
||||
main_layout = QtWidgets.QHBoxLayout(self)
|
||||
main_layout.setContentsMargins(0, 0, 0, 0)
|
||||
main_layout.addWidget(
|
||||
artist_msg_label, 1, QtCore.Qt.AlignCenter
|
||||
)
|
||||
|
||||
|
||||
class ValidationsWidget(QtWidgets.QFrame):
|
||||
"""Widgets showing validation error.
|
||||
|
||||
This widget is shown if validation error/s happened during validation part.
|
||||
|
||||
Shows validation error titles with instances on which happened and
|
||||
validation error detail with possible actions (repair).
|
||||
|
||||
┌──────┬────────────────┬───────┐
|
||||
│titles│ │actions│
|
||||
│ │ │ │
|
||||
│ │ Error detail │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
└──────┴────────────────┴───────┘
|
||||
"""
|
||||
|
||||
def __init__(self, controller, parent):
|
||||
super(ValidationsWidget, self).__init__(parent)
|
||||
|
||||
# Before publishing
|
||||
before_publish_widget = ValidationArtistMessage(
|
||||
"Nothing to report until you run publish", self
|
||||
)
|
||||
# After success publishing
|
||||
publish_started_widget = ValidationArtistMessage(
|
||||
"So far so good", self
|
||||
)
|
||||
# After success publishing
|
||||
publish_stop_ok_widget = ValidationArtistMessage(
|
||||
"Publishing finished successfully", self
|
||||
)
|
||||
# After failed publishing (not with validation error)
|
||||
publish_stop_fail_widget = ValidationArtistMessage(
|
||||
"This is not your fault...", self
|
||||
)
|
||||
|
||||
# Validation errors
|
||||
validations_widget = QtWidgets.QWidget(self)
|
||||
|
||||
content_widget = QtWidgets.QWidget(validations_widget)
|
||||
|
||||
errors_scroll = VerticallScrollArea(content_widget)
|
||||
errors_scroll.setWidgetResizable(True)
|
||||
|
||||
errors_widget = QtWidgets.QWidget(errors_scroll)
|
||||
errors_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground)
|
||||
errors_layout = QtWidgets.QVBoxLayout(errors_widget)
|
||||
errors_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
errors_scroll.setWidget(errors_widget)
|
||||
|
||||
error_details_frame = QtWidgets.QFrame(content_widget)
|
||||
error_details_input = QtWidgets.QTextEdit(error_details_frame)
|
||||
error_details_input.setObjectName("InfoText")
|
||||
error_details_input.setTextInteractionFlags(
|
||||
QtCore.Qt.TextBrowserInteraction
|
||||
)
|
||||
|
||||
actions_widget = ValidateActionsWidget(controller, content_widget)
|
||||
actions_widget.setMinimumWidth(140)
|
||||
|
||||
error_details_layout = QtWidgets.QHBoxLayout(error_details_frame)
|
||||
error_details_layout.addWidget(error_details_input, 1)
|
||||
error_details_layout.addWidget(actions_widget, 0)
|
||||
|
||||
content_layout = QtWidgets.QHBoxLayout(content_widget)
|
||||
content_layout.setSpacing(0)
|
||||
content_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
content_layout.addWidget(errors_scroll, 0)
|
||||
content_layout.addWidget(error_details_frame, 1)
|
||||
|
||||
top_label = QtWidgets.QLabel(
|
||||
"Publish validation report", content_widget
|
||||
)
|
||||
top_label.setObjectName("PublishInfoMainLabel")
|
||||
top_label.setAlignment(QtCore.Qt.AlignCenter)
|
||||
|
||||
validation_layout = QtWidgets.QVBoxLayout(validations_widget)
|
||||
validation_layout.setContentsMargins(0, 0, 0, 0)
|
||||
validation_layout.addWidget(top_label, 0)
|
||||
validation_layout.addWidget(content_widget, 1)
|
||||
|
||||
main_layout = QtWidgets.QStackedLayout(self)
|
||||
main_layout.addWidget(before_publish_widget)
|
||||
main_layout.addWidget(publish_started_widget)
|
||||
main_layout.addWidget(publish_stop_ok_widget)
|
||||
main_layout.addWidget(publish_stop_fail_widget)
|
||||
main_layout.addWidget(validations_widget)
|
||||
|
||||
main_layout.setCurrentWidget(before_publish_widget)
|
||||
|
||||
controller.event_system.add_callback(
|
||||
"publish.process.started", self._on_publish_start
|
||||
)
|
||||
controller.event_system.add_callback(
|
||||
"publish.reset.finished", self._on_publish_reset
|
||||
)
|
||||
controller.event_system.add_callback(
|
||||
"publish.process.stopped", self._on_publish_stop
|
||||
)
|
||||
|
||||
self._main_layout = main_layout
|
||||
|
||||
self._before_publish_widget = before_publish_widget
|
||||
self._publish_started_widget = publish_started_widget
|
||||
self._publish_stop_ok_widget = publish_stop_ok_widget
|
||||
self._publish_stop_fail_widget = publish_stop_fail_widget
|
||||
self._validations_widget = validations_widget
|
||||
|
||||
self._top_label = top_label
|
||||
self._errors_widget = errors_widget
|
||||
self._errors_layout = errors_layout
|
||||
self._error_details_frame = error_details_frame
|
||||
self._error_details_input = error_details_input
|
||||
self._actions_widget = actions_widget
|
||||
|
||||
self._title_widgets = {}
|
||||
self._error_info = {}
|
||||
self._previous_select = None
|
||||
|
||||
self._controller = controller
|
||||
|
||||
def clear(self):
|
||||
"""Delete all dynamic widgets and hide all wrappers."""
|
||||
self._title_widgets = {}
|
||||
self._error_info = {}
|
||||
self._previous_select = None
|
||||
while self._errors_layout.count():
|
||||
item = self._errors_layout.takeAt(0)
|
||||
widget = item.widget()
|
||||
if widget:
|
||||
widget.deleteLater()
|
||||
|
||||
self._top_label.setVisible(False)
|
||||
self._error_details_frame.setVisible(False)
|
||||
self._errors_widget.setVisible(False)
|
||||
self._actions_widget.setVisible(False)
|
||||
|
||||
def _set_errors(self, validation_error_report):
|
||||
"""Set errors into context and created titles.
|
||||
|
||||
Args:
|
||||
validation_error_report (PublishValidationErrorsReport): Report
|
||||
with information about validation errors and publish plugin
|
||||
actions.
|
||||
"""
|
||||
|
||||
self.clear()
|
||||
if not validation_error_report:
|
||||
return
|
||||
|
||||
self._top_label.setVisible(True)
|
||||
self._error_details_frame.setVisible(True)
|
||||
self._errors_widget.setVisible(True)
|
||||
|
||||
grouped_error_items = validation_error_report.group_items_by_title()
|
||||
for idx, error_info in enumerate(grouped_error_items):
|
||||
widget = ValidationErrorTitleWidget(idx, error_info, self)
|
||||
widget.selected.connect(self._on_select)
|
||||
widget.instance_changed.connect(self._on_instance_change)
|
||||
self._errors_layout.addWidget(widget)
|
||||
self._title_widgets[idx] = widget
|
||||
self._error_info[idx] = error_info
|
||||
|
||||
self._errors_layout.addStretch(1)
|
||||
|
||||
if self._title_widgets:
|
||||
self._title_widgets[0].set_selected(True)
|
||||
|
||||
self.updateGeometry()
|
||||
|
||||
def _set_current_widget(self, widget):
|
||||
self._main_layout.setCurrentWidget(widget)
|
||||
|
||||
def _on_publish_start(self):
|
||||
self._set_current_widget(self._publish_started_widget)
|
||||
|
||||
def _on_publish_reset(self):
|
||||
self._set_current_widget(self._before_publish_widget)
|
||||
|
||||
def _on_publish_stop(self):
|
||||
if self._controller.publish_has_crashed:
|
||||
self._set_current_widget(self._publish_stop_fail_widget)
|
||||
return
|
||||
|
||||
if self._controller.publish_has_validation_errors:
|
||||
validation_errors = self._controller.get_validation_errors()
|
||||
self._set_current_widget(self._validations_widget)
|
||||
self._set_errors(validation_errors)
|
||||
return
|
||||
|
||||
if self._controller.publish_has_finished:
|
||||
self._set_current_widget(self._publish_stop_ok_widget)
|
||||
return
|
||||
|
||||
self._set_current_widget(self._publish_started_widget)
|
||||
|
||||
def _on_select(self, index):
|
||||
if self._previous_select:
|
||||
if self._previous_select.index == index:
|
||||
return
|
||||
self._previous_select.set_selected(False)
|
||||
|
||||
self._previous_select = self._title_widgets[index]
|
||||
|
||||
error_item = self._error_info[index]
|
||||
|
||||
self._actions_widget.set_error_item(error_item)
|
||||
|
||||
self._update_description()
|
||||
|
||||
def _on_instance_change(self, index):
|
||||
if self._previous_select and self._previous_select.index != index:
|
||||
self._title_widgets[index].set_selected(True)
|
||||
else:
|
||||
self._update_description()
|
||||
|
||||
def _update_description(self):
|
||||
description = self._previous_select.current_description_text()
|
||||
if commonmark:
|
||||
html = commonmark.commonmark(description)
|
||||
self._error_details_input.setHtml(html)
|
||||
elif hasattr(self._error_details_input, "setMarkdown"):
|
||||
self._error_details_input.setMarkdown(description)
|
||||
else:
|
||||
self._error_details_input.setText(description)
|
||||
|
|
@ -40,6 +40,41 @@ from ..constants import (
|
|||
INPUTS_LAYOUT_VSPACING,
|
||||
)
|
||||
|
||||
FA_PREFIXES = ["", "fa.", "fa5.", "fa5b.", "fa5s.", "ei.", "mdi."]
|
||||
|
||||
|
||||
def parse_icon_def(
|
||||
icon_def, default_width=None, default_height=None, color=None
|
||||
):
|
||||
if not icon_def:
|
||||
return None
|
||||
|
||||
if isinstance(icon_def, QtGui.QPixmap):
|
||||
return icon_def
|
||||
|
||||
color = color or "white"
|
||||
default_width = default_width or 512
|
||||
default_height = default_height or 512
|
||||
|
||||
if isinstance(icon_def, QtGui.QIcon):
|
||||
return icon_def.pixmap(default_width, default_height)
|
||||
|
||||
try:
|
||||
if os.path.exists(icon_def):
|
||||
return QtGui.QPixmap(icon_def)
|
||||
except Exception:
|
||||
# TODO logging
|
||||
pass
|
||||
|
||||
for prefix in FA_PREFIXES:
|
||||
try:
|
||||
icon_name = "{}{}".format(prefix, icon_def)
|
||||
icon = qtawesome.icon(icon_name, color=color)
|
||||
return icon.pixmap(default_width, default_height)
|
||||
except Exception:
|
||||
# TODO logging
|
||||
continue
|
||||
|
||||
|
||||
class PublishPixmapLabel(PixmapLabel):
|
||||
def _get_pix_size(self):
|
||||
|
|
@ -54,7 +89,6 @@ class IconValuePixmapLabel(PublishPixmapLabel):
|
|||
Handle icon parsing from creators/instances. Using of QAwesome module
|
||||
of path to images.
|
||||
"""
|
||||
fa_prefixes = ["", "fa."]
|
||||
default_size = 200
|
||||
|
||||
def __init__(self, icon_def, parent):
|
||||
|
|
@ -77,31 +111,9 @@ class IconValuePixmapLabel(PublishPixmapLabel):
|
|||
return pix
|
||||
|
||||
def _parse_icon_def(self, icon_def):
|
||||
if not icon_def:
|
||||
return self._default_pixmap()
|
||||
|
||||
if isinstance(icon_def, QtGui.QPixmap):
|
||||
return icon_def
|
||||
|
||||
if isinstance(icon_def, QtGui.QIcon):
|
||||
return icon_def.pixmap(self.default_size, self.default_size)
|
||||
|
||||
try:
|
||||
if os.path.exists(icon_def):
|
||||
return QtGui.QPixmap(icon_def)
|
||||
except Exception:
|
||||
# TODO logging
|
||||
pass
|
||||
|
||||
for prefix in self.fa_prefixes:
|
||||
try:
|
||||
icon_name = "{}{}".format(prefix, icon_def)
|
||||
icon = qtawesome.icon(icon_name, color="white")
|
||||
return icon.pixmap(self.default_size, self.default_size)
|
||||
except Exception:
|
||||
# TODO logging
|
||||
continue
|
||||
|
||||
icon = parse_icon_def(icon_def, self.default_size, self.default_size)
|
||||
if icon:
|
||||
return icon
|
||||
return self._default_pixmap()
|
||||
|
||||
|
||||
|
|
@ -692,6 +704,7 @@ class TasksCombobox(QtWidgets.QComboBox):
|
|||
style.drawControl(
|
||||
QtWidgets.QStyle.CE_ComboBoxLabel, opt, painter, self
|
||||
)
|
||||
painter.end()
|
||||
|
||||
def is_valid(self):
|
||||
"""Are all selected items valid."""
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
import os
|
||||
import json
|
||||
import time
|
||||
import collections
|
||||
import copy
|
||||
from qtpy import QtWidgets, QtCore, QtGui
|
||||
|
|
@ -15,10 +18,11 @@ from openpype.tools.utils import (
|
|||
|
||||
from .constants import ResetKeySequence
|
||||
from .publish_report_viewer import PublishReportViewerWidget
|
||||
from .control import CardMessageTypes
|
||||
from .control_qt import QtPublisherController
|
||||
from .widgets import (
|
||||
OverviewWidget,
|
||||
ValidationsWidget,
|
||||
ReportPageWidget,
|
||||
PublishFrame,
|
||||
|
||||
PublisherTabsWidget,
|
||||
|
|
@ -182,7 +186,7 @@ class PublisherWindow(QtWidgets.QDialog):
|
|||
controller, content_stacked_widget
|
||||
)
|
||||
|
||||
report_widget = ValidationsWidget(controller, parent)
|
||||
report_widget = ReportPageWidget(controller, parent)
|
||||
|
||||
# Details - Publish details
|
||||
publish_details_widget = PublishReportViewerWidget(
|
||||
|
|
@ -313,6 +317,13 @@ class PublisherWindow(QtWidgets.QDialog):
|
|||
controller.event_system.add_callback(
|
||||
"convertors.find.failed", self._on_convertor_error
|
||||
)
|
||||
controller.event_system.add_callback(
|
||||
"export_report.request", self._export_report
|
||||
)
|
||||
controller.event_system.add_callback(
|
||||
"copy_report.request", self._copy_report
|
||||
)
|
||||
|
||||
|
||||
# Store extra header widget for TrayPublisher
|
||||
# - can be used to add additional widgets to header between context
|
||||
|
|
@ -825,6 +836,9 @@ class PublisherWindow(QtWidgets.QDialog):
|
|||
self._validate_btn.setEnabled(validate_enabled)
|
||||
self._publish_btn.setEnabled(publish_enabled)
|
||||
|
||||
if not publish_enabled:
|
||||
self._publish_frame.set_shrunk_state(True)
|
||||
|
||||
self._update_publish_details_widget()
|
||||
|
||||
def _validate_create_instances(self):
|
||||
|
|
@ -941,6 +955,46 @@ class PublisherWindow(QtWidgets.QDialog):
|
|||
under_mouse = widget_x < global_pos.x()
|
||||
self._create_overlay_button.set_under_mouse(under_mouse)
|
||||
|
||||
def _copy_report(self):
|
||||
logs = self._controller.get_publish_report()
|
||||
logs_string = json.dumps(logs, indent=4)
|
||||
|
||||
mime_data = QtCore.QMimeData()
|
||||
mime_data.setText(logs_string)
|
||||
QtWidgets.QApplication.instance().clipboard().setMimeData(
|
||||
mime_data
|
||||
)
|
||||
self._controller.emit_card_message(
|
||||
"Report added to clipboard",
|
||||
CardMessageTypes.info)
|
||||
|
||||
def _export_report(self):
|
||||
default_filename = "publish-report-{}".format(
|
||||
time.strftime("%y%m%d-%H-%M")
|
||||
)
|
||||
default_filepath = os.path.join(
|
||||
os.path.expanduser("~"),
|
||||
default_filename
|
||||
)
|
||||
new_filepath, ext = QtWidgets.QFileDialog.getSaveFileName(
|
||||
self, "Save report", default_filepath, ".json"
|
||||
)
|
||||
if not ext or not new_filepath:
|
||||
return
|
||||
|
||||
logs = self._controller.get_publish_report()
|
||||
full_path = new_filepath + ext
|
||||
dir_path = os.path.dirname(full_path)
|
||||
if not os.path.exists(dir_path):
|
||||
os.makedirs(dir_path)
|
||||
|
||||
with open(full_path, "w") as file_stream:
|
||||
json.dump(logs, file_stream)
|
||||
|
||||
self._controller.emit_card_message(
|
||||
"Report saved",
|
||||
CardMessageTypes.info)
|
||||
|
||||
|
||||
class ErrorsMessageBox(ErrorMessageBox):
|
||||
def __init__(self, error_title, failed_info, message_start, parent):
|
||||
|
|
|
|||
|
|
@ -1,13 +1,16 @@
|
|||
from .layouts import FlowLayout
|
||||
from .widgets import (
|
||||
FocusSpinBox,
|
||||
FocusDoubleSpinBox,
|
||||
ComboBox,
|
||||
CustomTextComboBox,
|
||||
PlaceholderLineEdit,
|
||||
ExpandingTextEdit,
|
||||
BaseClickableFrame,
|
||||
ClickableFrame,
|
||||
ClickableLabel,
|
||||
ExpandBtn,
|
||||
ClassicExpandBtn,
|
||||
PixmapLabel,
|
||||
IconButton,
|
||||
PixmapButton,
|
||||
|
|
@ -37,15 +40,19 @@ from .overlay_messages import (
|
|||
|
||||
|
||||
__all__ = (
|
||||
"FlowLayout",
|
||||
|
||||
"FocusSpinBox",
|
||||
"FocusDoubleSpinBox",
|
||||
"ComboBox",
|
||||
"CustomTextComboBox",
|
||||
"PlaceholderLineEdit",
|
||||
"ExpandingTextEdit",
|
||||
"BaseClickableFrame",
|
||||
"ClickableFrame",
|
||||
"ClickableLabel",
|
||||
"ExpandBtn",
|
||||
"ClassicExpandBtn",
|
||||
"PixmapLabel",
|
||||
"IconButton",
|
||||
"PixmapButton",
|
||||
|
|
|
|||
150
openpype/tools/utils/layouts.py
Normal file
150
openpype/tools/utils/layouts.py
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
from qtpy import QtWidgets, QtCore
|
||||
|
||||
|
||||
class FlowLayout(QtWidgets.QLayout):
|
||||
"""Layout that organize widgets by minimum size into a flow layout.
|
||||
|
||||
Layout is putting widget from left to right and top to bottom. When widget
|
||||
can't fit a row it is added to next line. Minimum size matches widget with
|
||||
biggest 'sizeHint' width and height using calculated geometry.
|
||||
|
||||
Content margins are part of calculations. It is possible to define
|
||||
horizontal and vertical spacing.
|
||||
|
||||
Layout does not support stretch and spacing items.
|
||||
|
||||
Todos:
|
||||
Unified width concept -> use width of largest item so all of them are
|
||||
same. This could allow to have minimum columns option too.
|
||||
"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(FlowLayout, self).__init__(parent)
|
||||
|
||||
# spaces between each item
|
||||
self._horizontal_spacing = 5
|
||||
self._vertical_spacing = 5
|
||||
|
||||
self._items = []
|
||||
|
||||
def __del__(self):
|
||||
while self.count():
|
||||
self.takeAt(0, False)
|
||||
|
||||
def isEmpty(self):
|
||||
for item in self._items:
|
||||
if not item.isEmpty():
|
||||
return False
|
||||
return True
|
||||
|
||||
def setSpacing(self, spacing):
|
||||
self._horizontal_spacing = spacing
|
||||
self._vertical_spacing = spacing
|
||||
self.invalidate()
|
||||
|
||||
def setHorizontalSpacing(self, spacing):
|
||||
self._horizontal_spacing = spacing
|
||||
self.invalidate()
|
||||
|
||||
def setVerticalSpacing(self, spacing):
|
||||
self._vertical_spacing = spacing
|
||||
self.invalidate()
|
||||
|
||||
def addItem(self, item):
|
||||
self._items.append(item)
|
||||
self.invalidate()
|
||||
|
||||
def count(self):
|
||||
return len(self._items)
|
||||
|
||||
def itemAt(self, index):
|
||||
if 0 <= index < len(self._items):
|
||||
return self._items[index]
|
||||
return None
|
||||
|
||||
def takeAt(self, index, invalidate=True):
|
||||
if 0 <= index < len(self._items):
|
||||
item = self._items.pop(index)
|
||||
if invalidate:
|
||||
self.invalidate()
|
||||
return item
|
||||
return None
|
||||
|
||||
def expandingDirections(self):
|
||||
return QtCore.Qt.Orientations(QtCore.Qt.Vertical)
|
||||
|
||||
def hasHeightForWidth(self):
|
||||
return True
|
||||
|
||||
def heightForWidth(self, width):
|
||||
return self._setup_geometry(QtCore.QRect(0, 0, width, 0), True)
|
||||
|
||||
def setGeometry(self, rect):
|
||||
super(FlowLayout, self).setGeometry(rect)
|
||||
self._setup_geometry(rect)
|
||||
|
||||
def sizeHint(self):
|
||||
return self.minimumSize()
|
||||
|
||||
def minimumSize(self):
|
||||
size = QtCore.QSize(0, 0)
|
||||
for item in self._items:
|
||||
widget = item.widget()
|
||||
if widget is not None:
|
||||
parent = widget.parent()
|
||||
if not widget.isVisibleTo(parent):
|
||||
continue
|
||||
size = size.expandedTo(item.minimumSize())
|
||||
|
||||
if size.width() < 1 or size.height() < 1:
|
||||
return size
|
||||
l_margin, t_margin, r_margin, b_margin = self.getContentsMargins()
|
||||
size += QtCore.QSize(l_margin + r_margin, t_margin + b_margin)
|
||||
return size
|
||||
|
||||
def _setup_geometry(self, rect, only_calculate=False):
|
||||
h_spacing = self._horizontal_spacing
|
||||
v_spacing = self._vertical_spacing
|
||||
l_margin, t_margin, r_margin, b_margin = self.getContentsMargins()
|
||||
|
||||
left_x = rect.x() + l_margin
|
||||
top_y = rect.y() + t_margin
|
||||
pos_x = left_x
|
||||
pos_y = top_y
|
||||
row_height = 0
|
||||
for item in self._items:
|
||||
item_hint = item.sizeHint()
|
||||
item_width = item_hint.width()
|
||||
item_height = item_hint.height()
|
||||
if item_width < 1 or item_height < 1:
|
||||
continue
|
||||
|
||||
end_x = pos_x + item_width
|
||||
|
||||
wrap = (
|
||||
row_height > 0
|
||||
and (
|
||||
end_x > rect.right()
|
||||
or (end_x + r_margin) > rect.right()
|
||||
)
|
||||
)
|
||||
if not wrap:
|
||||
next_pos_x = end_x + h_spacing
|
||||
else:
|
||||
pos_x = left_x
|
||||
pos_y += row_height + v_spacing
|
||||
next_pos_x = pos_x + item_width + h_spacing
|
||||
row_height = 0
|
||||
|
||||
if not only_calculate:
|
||||
item.setGeometry(
|
||||
QtCore.QRect(pos_x, pos_y, item_width, item_height)
|
||||
)
|
||||
|
||||
pos_x = next_pos_x
|
||||
row_height = max(row_height, item_height)
|
||||
|
||||
height = (pos_y - top_y) + row_height
|
||||
if height > 0:
|
||||
height += b_margin
|
||||
return height
|
||||
|
|
@ -101,6 +101,46 @@ class PlaceholderLineEdit(QtWidgets.QLineEdit):
|
|||
self.setPalette(filter_palette)
|
||||
|
||||
|
||||
class ExpandingTextEdit(QtWidgets.QTextEdit):
|
||||
"""QTextEdit which does not have sroll area but expands height."""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(ExpandingTextEdit, self).__init__(parent)
|
||||
|
||||
size_policy = self.sizePolicy()
|
||||
size_policy.setHeightForWidth(True)
|
||||
size_policy.setVerticalPolicy(QtWidgets.QSizePolicy.Preferred)
|
||||
self.setSizePolicy(size_policy)
|
||||
|
||||
self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
|
||||
self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
|
||||
|
||||
doc = self.document()
|
||||
doc.contentsChanged.connect(self._on_doc_change)
|
||||
|
||||
def _on_doc_change(self):
|
||||
self.updateGeometry()
|
||||
|
||||
def hasHeightForWidth(self):
|
||||
return True
|
||||
|
||||
def heightForWidth(self, width):
|
||||
margins = self.contentsMargins()
|
||||
|
||||
document_width = 0
|
||||
if width >= margins.left() + margins.right():
|
||||
document_width = width - margins.left() - margins.right()
|
||||
|
||||
document = self.document().clone()
|
||||
document.setTextWidth(document_width)
|
||||
|
||||
return margins.top() + document.size().height() + margins.bottom()
|
||||
|
||||
def sizeHint(self):
|
||||
width = super(ExpandingTextEdit, self).sizeHint().width()
|
||||
return QtCore.QSize(width, self.heightForWidth(width))
|
||||
|
||||
|
||||
class BaseClickableFrame(QtWidgets.QFrame):
|
||||
"""Widget that catch left mouse click and can trigger a callback.
|
||||
|
||||
|
|
@ -161,19 +201,34 @@ class ClickableLabel(QtWidgets.QLabel):
|
|||
|
||||
class ExpandBtnLabel(QtWidgets.QLabel):
|
||||
"""Label showing expand icon meant for ExpandBtn."""
|
||||
state_changed = QtCore.Signal()
|
||||
|
||||
|
||||
def __init__(self, parent):
|
||||
super(ExpandBtnLabel, self).__init__(parent)
|
||||
self._source_collapsed_pix = QtGui.QPixmap(
|
||||
get_style_image_path("branch_closed")
|
||||
)
|
||||
self._source_expanded_pix = QtGui.QPixmap(
|
||||
get_style_image_path("branch_open")
|
||||
)
|
||||
self._source_collapsed_pix = self._create_collapsed_pixmap()
|
||||
self._source_expanded_pix = self._create_expanded_pixmap()
|
||||
|
||||
self._current_image = self._source_collapsed_pix
|
||||
self._collapsed = True
|
||||
|
||||
def set_collapsed(self, collapsed):
|
||||
def _create_collapsed_pixmap(self):
|
||||
return QtGui.QPixmap(
|
||||
get_style_image_path("branch_closed")
|
||||
)
|
||||
|
||||
def _create_expanded_pixmap(self):
|
||||
return QtGui.QPixmap(
|
||||
get_style_image_path("branch_open")
|
||||
)
|
||||
|
||||
@property
|
||||
def collapsed(self):
|
||||
return self._collapsed
|
||||
|
||||
def set_collapsed(self, collapsed=None):
|
||||
if collapsed is None:
|
||||
collapsed = not self._collapsed
|
||||
if self._collapsed == collapsed:
|
||||
return
|
||||
self._collapsed = collapsed
|
||||
|
|
@ -182,6 +237,7 @@ class ExpandBtnLabel(QtWidgets.QLabel):
|
|||
else:
|
||||
self._current_image = self._source_expanded_pix
|
||||
self._set_resized_pix()
|
||||
self.state_changed.emit()
|
||||
|
||||
def resizeEvent(self, event):
|
||||
self._set_resized_pix()
|
||||
|
|
@ -203,21 +259,55 @@ class ExpandBtnLabel(QtWidgets.QLabel):
|
|||
|
||||
|
||||
class ExpandBtn(ClickableFrame):
|
||||
state_changed = QtCore.Signal()
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(ExpandBtn, self).__init__(parent)
|
||||
|
||||
pixmap_label = ExpandBtnLabel(self)
|
||||
pixmap_label = self._create_pix_widget(self)
|
||||
|
||||
layout = QtWidgets.QHBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addWidget(pixmap_label)
|
||||
|
||||
pixmap_label.state_changed.connect(self.state_changed)
|
||||
|
||||
self._pixmap_label = pixmap_label
|
||||
|
||||
def set_collapsed(self, collapsed):
|
||||
def _create_pix_widget(self, parent=None):
|
||||
if parent is None:
|
||||
parent = self
|
||||
return ExpandBtnLabel(parent)
|
||||
|
||||
@property
|
||||
def collapsed(self):
|
||||
return self._pixmap_label.collapsed
|
||||
|
||||
def set_collapsed(self, collapsed=None):
|
||||
self._pixmap_label.set_collapsed(collapsed)
|
||||
|
||||
|
||||
class ClassicExpandBtnLabel(ExpandBtnLabel):
|
||||
def _create_collapsed_pixmap(self):
|
||||
return QtGui.QPixmap(
|
||||
get_style_image_path("right_arrow")
|
||||
)
|
||||
|
||||
def _create_expanded_pixmap(self):
|
||||
return QtGui.QPixmap(
|
||||
get_style_image_path("down_arrow")
|
||||
)
|
||||
|
||||
|
||||
class ClassicExpandBtn(ExpandBtn):
|
||||
"""Same as 'ExpandBtn' but with arrow images."""
|
||||
|
||||
def _create_pix_widget(self, parent=None):
|
||||
if parent is None:
|
||||
parent = self
|
||||
return ClassicExpandBtnLabel(parent)
|
||||
|
||||
|
||||
class ImageButton(QtWidgets.QPushButton):
|
||||
"""PushButton with icon and size of font.
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
 <!-- same menu as in PS -->
|
||||

|
||||
|
||||
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.
|
|||
|
||||

|
||||

|
||||
|
||||
|
||||
### 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.
|
||||
|
|
|
|||
BIN
website/docs/assets/aftereffects_extension.png
Normal file
BIN
website/docs/assets/aftereffects_extension.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
|
|
@ -506,6 +506,67 @@ or the scene file was copy pasted from different context.
|
|||
#### *Known errors*
|
||||
When there is a known error that can't be fixed by the user (e.g. can't connect to deadline service, etc.) `KnownPublishError` should be raised. The only difference is that its message is shown in UI to the artist otherwise a neutral message without context is shown.
|
||||
|
||||
### Plugins
|
||||
Plugin is a single processing unit that can work with publish context and instances.
|
||||
|
||||
#### Plugin types
|
||||
There are 2 types of plugins - `InstancePlugin` and `ContextPlugin`. Be aware that inheritance of plugin from `InstancePlugin` or `ContextPlugin` actually does not affect if plugin is instance or context plugin, that is affected by argument name in `process` method.
|
||||
|
||||
```python
|
||||
import pyblish.api
|
||||
|
||||
|
||||
# Context plugin
|
||||
class MyContextPlugin(pyblish.api.ContextPlugin):
|
||||
def process(self, context):
|
||||
...
|
||||
|
||||
# Instance plugin
|
||||
class MyInstancePlugin(pyblish.api.InstancePlugin):
|
||||
def process(self, instance):
|
||||
...
|
||||
|
||||
# Still an instance plugin
|
||||
class MyOtherInstancePlugin(pyblish.api.ContextPlugin):
|
||||
def process(self, instance):
|
||||
...
|
||||
```
|
||||
|
||||
#### Plugin filtering
|
||||
By pyblish logic, plugins have predefined filtering class attributes `hosts`, `targets` and `families`. Filter by `hosts` and `targets` are filters that are applied for current publishing process. Both filters are registered in `pyblish` module, `hosts` filtering may not match OpenPype host name (e.g. farm publishing uses `shell` in pyblish). Filter `families` works only on instance plugins and is dynamic during publish process by changing families of an instance.
|
||||
|
||||
All filters are list of a strings `families = ["image"]`. Empty list is invalid filter and plugin will be skipped, to allow plugin for all values use a start `families = ["*"]`. For more detailed filtering options check [pyblish documentation](https://api.pyblish.com/pluginsystem).
|
||||
|
||||
Each plugin must have order, there are 4 order milestones - Collect, Validate, Extract, Integration. Any plugin below collection order won't be processed. for more details check [pyblish documentation](https://api.pyblish.com/ordering).
|
||||
|
||||
#### Plugin settings
|
||||
Pyblish plugins may have settings. There are 2 ways how settings are applied, first is automated, and it's logic is based on function `filter_pyblish_plugins` in `./openpype/pipeline/publish/lib.py`, second is explicit by implementing class method `apply_settings` on a plugin.
|
||||
|
||||
|
||||
Automated logic is expecting specific structure of project settings `project_settings[{category}]["plugins"]["publish"][{plugin class name}]`. The category is a key in root of project settings. There are currently 3 ways how the category key is received.
|
||||
1. Use `settings_category` class attribute value from plugin. If `settings_category` is not `None` there is not any fallback to other way.
|
||||
2. Use currently registered pyblish host. This will be probably deprecated soon.
|
||||
3. Use 3rd folder name from a plugin filepath. From path `./maya/plugins/publish/collect_render.py` is used `maya` as the key.
|
||||
|
||||
For any other use-case is recommended to use explicit approach by implementing `apply_settings` method. Must use `@classmethod` decorator and expect arguments for project settings and system settings. We're planning to support single argument with only project settings.
|
||||
```python
|
||||
import pyblish.api
|
||||
|
||||
|
||||
class MyPlugin(pyblish.api.InstancePlugin):
|
||||
profiles = []
|
||||
|
||||
@classmethod
|
||||
def apply_settings(cls, project_settings, system_settings):
|
||||
cls.profiles = (
|
||||
project_settings
|
||||
["addon"]
|
||||
["plugins"]
|
||||
["publish"]
|
||||
["vfx_profiles"]
|
||||
)
|
||||
```
|
||||
|
||||
### Plugin extension
|
||||
Publish plugins can be extended by additional logic when inheriting from `OpenPypePyblishPluginMixin` which can be used as mixin (additional inheritance of class). Publish plugins that inherit from this mixin can define attributes that will be shown in **CreatedInstance**. One of the most important usages is to be able turn on/off optional plugins.
|
||||
|
||||
|
|
@ -596,4 +657,4 @@ Publish attributes work the same way as create attributes but the source of attr
|
|||
|
||||
### Create dialog
|
||||

|
||||
Create dialog is used by artist to create new instances in a context. The context selection can be enabled/disabled by changing `create_allow_context_change` on [creator plugin](#creator). In the middle part the artist selects what will be created and what variant it is. On the right side is information about the selected creator and its pre-create attributes. There is also a question mark button which extends the window and displays more detailed information about the creator.
|
||||
Create dialog is used by artist to create new instances in a context. The context selection can be enabled/disabled by changing `create_allow_context_change` on [creator plugin](#creator). In the middle part the artist selects what will be created and what variant it is. On the right side is information about the selected creator and its pre-create attributes. There is also a question mark button which extends the window and displays more detailed information about the creator.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue