🔀 Merge remote-tracking branch 'origin/develop' into maya_new_publisher

This commit is contained in:
Ondrej Samohel 2023-06-30 14:24:33 +02:00
commit 4b12f49dd6
No known key found for this signature in database
GPG key ID: 02376E18990A97C6
58 changed files with 1612 additions and 298 deletions

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ExtensionManifest Version="8.0" ExtensionBundleId="com.openpype.AE.panel" ExtensionBundleVersion="1.0.25"
<ExtensionManifest Version="8.0" ExtensionBundleId="com.openpype.AE.panel" ExtensionBundleVersion="1.0.26"
ExtensionBundleName="com.openpype.AE.panel" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<ExtensionList>
<Extension Id="com.openpype.AE.panel" Version="1.0" />

View file

@ -104,6 +104,39 @@
});
</script>
<script type=text/javascript>
$(function() {
$("a#create-placeholder-button").bind("click", function() {
RPC.call('AfterEffects.create_placeholder_route').then(function (data) {
}, function (error) {
alert(error);
});
});
});
</script>
<script type=text/javascript>
$(function() {
$("a#update-placeholder-button").bind("click", function() {
RPC.call('AfterEffects.update_placeholder_route').then(function (data) {
}, function (error) {
alert(error);
});
});
});
</script>
<script type=text/javascript>
$(function() {
$("a#build-workfile-button").bind("click", function() {
RPC.call('AfterEffects.build_workfile_template_route').then(function (data) {
}, function (error) {
alert(error);
});
});
});
</script>
<script type=text/javascript>
$(function() {
$("a#experimental-button").bind("click", function() {
@ -127,9 +160,15 @@
<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=separator0><button class="hostFontSize">&nbsp;</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=separator1><button class="hostFontSize">&nbsp;</button></a></div>
<div><a href=# id=create-placeholder-button><button class="hostFontSize">Create placeholder</button></a></div>
<div><a href=# id=update-placeholder-button><button class="hostFontSize">Update placeholder</button></a></div>
<div><a href=# id=build-workfile-button><button class="hostFontSize">Build Workfile from template</button></a></div>
<div><a href=# id=separator3><button class="hostFontSize">&nbsp;</button></a></div>
<div><a href=# id=experimental-button><button class="hostFontSize">Experimental Tools...</button></a></div>
</div>

View file

@ -107,6 +107,17 @@ function main(websocket_url){
});
});
RPC.addRoute('AfterEffects.add_item', function (data) {
log.warn('Server called client route "add_item":', data);
var escapedName = EscapeStringForJSX(data.name);
return runEvalScript("addItem('" + escapedName +"', " +
"'" + data.item_type + "')")
.then(function(result){
log.warn("get_items: " + result);
return result;
});
});
RPC.addRoute('AfterEffects.get_items', function (data) {
log.warn('Server called client route "get_items":', data);
return runEvalScript("getItems(" + data.comps + "," +
@ -118,6 +129,15 @@ function main(websocket_url){
});
});
RPC.addRoute('AfterEffects.select_items', function (data) {
log.warn('Server called client route "select_items":', data);
return runEvalScript("selectItems(" + JSON.stringify(data.items) + ")")
.then(function(result){
log.warn("select_items: " + result);
return result;
});
});
RPC.addRoute('AfterEffects.get_selected_items', function (data) {
log.warn('Server called client route "get_selected_items":', data);
@ -280,7 +300,7 @@ function main(websocket_url){
RPC.addRoute('AfterEffects.add_item_as_layer', function (data) {
log.warn('Server called client route "add_item_as_layer":', data);
return runEvalScript("addItemAsLayerToComp(" + data.comp_id + ", " +
data.item_id + "," +
data.item_id + "," +
" null )")
.then(function(result){
log.warn("addItemAsLayerToComp: " + result);
@ -288,6 +308,16 @@ function main(websocket_url){
});
});
RPC.addRoute('AfterEffects.add_item_instead_placeholder', function (data) {
log.warn('Server called client route "add_item_instead_placeholder":', data);
return runEvalScript("addItemInstead(" + data.placeholder_item_id + ", " +
data.item_id + ")")
.then(function(result){
log.warn("add_item_instead_placeholder: " + result);
return result;
});
});
RPC.addRoute('AfterEffects.render', function (data) {
log.warn('Server called client route "render":', data);
var escapedPath = EscapeStringForJSX(data.folder_url);
@ -312,6 +342,20 @@ function main(websocket_url){
});
});
RPC.addRoute('AfterEffects.add_placeholder', function (data) {
log.warn('Server called client route "add_placeholder":', data);
var escapedName = EscapeStringForJSX(data.name);
return runEvalScript("addPlaceholder('" + escapedName +"',"+
data.width + ',' +
data.height + ',' +
data.fps + ',' +
data.duration + ")")
.then(function(result){
log.warn("add_placeholder: " + result);
return result;
});
});
RPC.addRoute('AfterEffects.close', function (data) {
log.warn('Server called client route "close":', data);
return runEvalScript("close()");

View file

@ -112,6 +112,32 @@ function getActiveDocumentFullName(){
return _prepareError("No file open currently");
}
function addItem(name, item_type){
/**
* Adds comp or folder to project items.
*
* Could be called when creating publishable instance to prepare
* composition (and render queue).
*
* Args:
* name (str): composition name
* item_type (str): COMP|FOLDER
* Returns:
* SingleItemValue: eg {"result": VALUE}
*/
if (item_type == "COMP"){
// dummy values, will be rewritten later
item = app.project.items.addComp(name, 1920, 1060, 1, 10, 25);
}else if (item_type == "FOLDER"){
item = app.project.items.addFolder(name);
}else{
return _prepareError("Only 'COMP' or 'FOLDER' can be created");
}
return _prepareSingleValue(item.id);
}
function getItems(comps, folders, footages){
/**
* Returns JSON representation of compositions and
@ -139,6 +165,24 @@ function getItems(comps, folders, footages){
}
function selectItems(items){
/**
* Select all items from `items`, deselect other.
*
* Args:
* items (list)
*/
for (i = 1; i <= app.project.items.length; ++i){
item = app.project.items[i];
if (items.indexOf(item.id) > -1){
item.selected = true;
}else{
item.selected = false;
}
}
}
function getSelectedItems(comps, folders, footages){
/**
* Returns list of selected items from Project menu
@ -280,12 +324,12 @@ function setLabelColor(comp_id, color_idx){
}
}
function replaceItem(comp_id, path, item_name){
function replaceItem(item_id, path, item_name){
/**
* Replaces loaded file with new file and updates name
*
* Args:
* comp_id (int): id of composition, not a index!
* item_id (int): id of composition, not a index!
* path (string): absolute path to new file
* item_name (string): new composition name
*/
@ -295,7 +339,7 @@ function replaceItem(comp_id, path, item_name){
if (!fp.exists){
return _prepareError("File " + path + " not found.");
}
var item = app.project.itemByID(comp_id);
var item = app.project.itemByID(item_id);
if (item){
try{
if (isFileSequence(item)) {
@ -311,7 +355,7 @@ function replaceItem(comp_id, path, item_name){
fp.close();
}
}else{
return _prepareError("There is no composition with "+ comp_id);
return _prepareError("There is no item with "+ item_id);
}
app.endUndoGroup();
}
@ -821,6 +865,67 @@ function printMsg(msg){
alert(msg);
}
function addPlaceholder(name, width, height, fps, duration){
/** Add AE PlaceholderItem to Project list.
*
* PlaceholderItem chosen as it doesn't require existing file and
* might potentially allow nice functionality in the future.
*
*/
app.beginUndoGroup('change comp properties');
try{
item = app.project.importPlaceholder(name, width, height,
fps, duration);
return _prepareSingleValue(item.id);
}catch (error) {
writeLn(_prepareError("Cannot add placeholder " + error.toString()));
}
app.endUndoGroup();
}
function addItemInstead(placeholder_item_id, item_id){
/** Add new loaded item in place of load placeholder.
*
* Each placeholder could be placed multiple times into multiple
* composition. This loops through all compositions and
* places loaded item under placeholder.
* Placeholder item gets deleted later separately according
* to configuration in Settings.
*
* Args:
* placeholder_item_id (int)
* item_id (int)
*/
var item = app.project.itemByID(item_id);
if (!item){
return _prepareError("There is no item with "+ item_id);
}
app.beginUndoGroup('Add loaded items');
for (i = 1; i <= app.project.items.length; ++i){
var comp = app.project.items[i];
if (!(comp instanceof CompItem)){
continue
}
var i = 1;
while (i <= comp.numLayers) {
var layer = comp.layer(i);
var layer_source = layer.source;
if (layer_source && layer_source.id == placeholder_item_id){
var new_layer = comp.layers.add(item);
new_layer.moveAfter(layer);
// copy all(?) properties to new layer
layer.property("ADBE Transform Group").copyToComp(new_layer);
i = i + 1;
}
i = i + 1;
}
}
app.endUndoGroup();
}
function _prepareSingleValue(value){
return JSON.stringify({"result": value})
}

View file

@ -357,3 +357,33 @@ class AfterEffectsRoute(WebSocketRoute):
# Required return statement.
return "nothing"
def create_placeholder_route(self):
from openpype.hosts.aftereffects.api.workfile_template_builder import \
create_placeholder
partial_method = functools.partial(create_placeholder)
ProcessLauncher.execute_in_main_thread(partial_method)
# Required return statement.
return "nothing"
def update_placeholder_route(self):
from openpype.hosts.aftereffects.api.workfile_template_builder import \
update_placeholder
partial_method = functools.partial(update_placeholder)
ProcessLauncher.execute_in_main_thread(partial_method)
# Required return statement.
return "nothing"
def build_workfile_template_route(self):
from openpype.hosts.aftereffects.api.workfile_template_builder import \
build_workfile_template
partial_method = functools.partial(build_workfile_template)
ProcessLauncher.execute_in_main_thread(partial_method)
# Required return statement.
return "nothing"

View file

@ -10,6 +10,10 @@ from openpype.pipeline import (
register_creator_plugin_path,
AVALON_CONTAINER_ID,
)
from openpype.hosts.aftereffects.api.workfile_template_builder import (
AEPlaceholderLoadPlugin,
AEPlaceholderCreatePlugin
)
from openpype.pipeline.load import any_outdated_containers
import openpype.hosts.aftereffects
@ -116,6 +120,12 @@ class AfterEffectsHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost):
item["id"] = "publish_context"
self.stub.imprint(item["id"], item)
def get_workfile_build_placeholder_plugins(self):
return [
AEPlaceholderLoadPlugin,
AEPlaceholderCreatePlugin
]
# created instances section
def list_instances(self):
"""List all created instances from current workfile which

View file

@ -1,7 +1,11 @@
import six
from abc import ABCMeta
from openpype.pipeline import LoaderPlugin
from .launch_logic import get_stub
@six.add_metaclass(ABCMeta)
class AfterEffectsLoader(LoaderPlugin):
@staticmethod
def get_stub():

View file

@ -0,0 +1,271 @@
import os.path
import uuid
import shutil
from openpype.pipeline import registered_host
from openpype.tools.workfile_template_build import (
WorkfileBuildPlaceholderDialog,
)
from openpype.pipeline.workfile.workfile_template_builder import (
AbstractTemplateBuilder,
PlaceholderPlugin,
LoadPlaceholderItem,
CreatePlaceholderItem,
PlaceholderLoadMixin,
PlaceholderCreateMixin
)
from openpype.hosts.aftereffects.api import get_stub
from openpype.hosts.aftereffects.api.lib import set_settings
PLACEHOLDER_SET = "PLACEHOLDERS_SET"
PLACEHOLDER_ID = "openpype.placeholder"
class AETemplateBuilder(AbstractTemplateBuilder):
"""Concrete implementation of AbstractTemplateBuilder for AE"""
def import_template(self, path):
"""Import template into current scene.
Block if a template is already loaded.
Args:
path (str): A path to current template (usually given by
get_template_preset implementation)
Returns:
bool: Whether the template was successfully imported or not
"""
stub = get_stub()
if not os.path.exists(path):
stub.print_msg(f"Template file on {path} doesn't exist.")
return
stub.save()
workfile_path = stub.get_active_document_full_name()
shutil.copy2(path, workfile_path)
stub.open(workfile_path)
return True
class AEPlaceholderPlugin(PlaceholderPlugin):
"""Contains generic methods for all PlaceholderPlugins."""
def collect_placeholders(self):
"""Collect info from file metadata about created placeholders.
Returns:
(list) (LoadPlaceholderItem)
"""
output = []
scene_placeholders = self._collect_scene_placeholders()
for item in scene_placeholders:
if item.get("plugin_identifier") != self.identifier:
continue
if isinstance(self, AEPlaceholderLoadPlugin):
item = LoadPlaceholderItem(item["uuid"],
item["data"],
self)
elif isinstance(self, AEPlaceholderCreatePlugin):
item = CreatePlaceholderItem(item["uuid"],
item["data"],
self)
else:
raise NotImplementedError(f"Not implemented for {type(self)}")
output.append(item)
return output
def update_placeholder(self, placeholder_item, placeholder_data):
"""Resave changed properties for placeholders"""
item_id, metadata_item = self._get_item(placeholder_item)
stub = get_stub()
if not item_id:
stub.print_msg("Cannot find item for "
f"{placeholder_item.scene_identifier}")
return
metadata_item["data"] = placeholder_data
stub.imprint(item_id, metadata_item)
def _get_item(self, placeholder_item):
"""Returns item id and item metadata for placeholder from file meta"""
stub = get_stub()
placeholder_uuid = placeholder_item.scene_identifier
for metadata_item in stub.get_metadata():
if not metadata_item.get("is_placeholder"):
continue
if placeholder_uuid in metadata_item.get("uuid"):
return metadata_item["members"][0], metadata_item
return None, None
def _collect_scene_placeholders(self):
"""" Cache placeholder data to shared data.
Returns:
(list) of dicts
"""
placeholder_items = self.builder.get_shared_populate_data(
"placeholder_items"
)
if not placeholder_items:
placeholder_items = []
for item in get_stub().get_metadata():
if not item.get("is_placeholder"):
continue
placeholder_items.append(item)
self.builder.set_shared_populate_data(
"placeholder_items", placeholder_items
)
return placeholder_items
def _imprint_item(self, item_id, name, placeholder_data, stub):
if not item_id:
raise ValueError("Couldn't create a placeholder")
container_data = {
"id": "openpype.placeholder",
"name": name,
"is_placeholder": True,
"plugin_identifier": self.identifier,
"uuid": str(uuid.uuid4()), # scene_identifier
"data": placeholder_data,
"members": [item_id]
}
stub.imprint(item_id, container_data)
class AEPlaceholderCreatePlugin(AEPlaceholderPlugin, PlaceholderCreateMixin):
"""Adds Create placeholder.
This adds composition and runs Create
"""
identifier = "aftereffects.create"
label = "AfterEffects create"
def create_placeholder(self, placeholder_data):
stub = get_stub()
name = "CREATEPLACEHOLDER"
item_id = stub.add_item(name, "COMP")
self._imprint_item(item_id, name, placeholder_data, stub)
def populate_placeholder(self, placeholder):
"""Replace 'placeholder' with publishable instance.
Renames prepared composition name, creates publishable instance, sets
frame/duration settings according to DB.
"""
pre_create_data = {"use_selection": True}
item_id, item = self._get_item(placeholder)
get_stub().select_items([item_id])
self.populate_create_placeholder(placeholder, pre_create_data)
# apply settings for populated composition
item_id, metadata_item = self._get_item(placeholder)
set_settings(True, True, [item_id])
def get_placeholder_options(self, options=None):
return self.get_create_plugin_options(options)
class AEPlaceholderLoadPlugin(AEPlaceholderPlugin, PlaceholderLoadMixin):
identifier = "aftereffects.load"
label = "AfterEffects load"
def create_placeholder(self, placeholder_data):
"""Creates AE's Placeholder item in Project items list.
Sets dummy resolution/duration/fps settings, will be replaced when
populated.
"""
stub = get_stub()
name = "LOADERPLACEHOLDER"
item_id = stub.add_placeholder(name, 1920, 1060, 25, 10)
self._imprint_item(item_id, name, placeholder_data, stub)
def populate_placeholder(self, placeholder):
"""Use Openpype Loader from `placeholder` to create new FootageItems
New FootageItems are created, files are imported.
"""
self.populate_load_placeholder(placeholder)
errors = placeholder.get_errors()
stub = get_stub()
if errors:
stub.print_msg("\n".join(errors))
else:
if not placeholder.data["keep_placeholder"]:
metadata = stub.get_metadata()
for item in metadata:
if not item.get("is_placeholder"):
continue
scene_identifier = item.get("uuid")
if (scene_identifier and
scene_identifier == placeholder.scene_identifier):
stub.delete_item(item["members"][0])
stub.remove_instance(placeholder.scene_identifier, metadata)
def get_placeholder_options(self, options=None):
return self.get_load_plugin_options(options)
def load_succeed(self, placeholder, container):
placeholder_item_id, _ = self._get_item(placeholder)
item_id = container.id
get_stub().add_item_instead_placeholder(placeholder_item_id, item_id)
def build_workfile_template(*args, **kwargs):
builder = AETemplateBuilder(registered_host())
builder.build_template(*args, **kwargs)
def update_workfile_template(*args):
builder = AETemplateBuilder(registered_host())
builder.rebuild_template()
def create_placeholder(*args):
"""Called when new workile placeholder should be created."""
host = registered_host()
builder = AETemplateBuilder(host)
window = WorkfileBuildPlaceholderDialog(host, builder)
window.exec_()
def update_placeholder(*args):
"""Called after placeholder item is selected to modify it."""
host = registered_host()
builder = AETemplateBuilder(host)
stub = get_stub()
selected_items = stub.get_selected_items(True, True, True)
if len(selected_items) != 1:
stub.print_msg("Please select just 1 placeholder")
return
selected_id = selected_items[0].id
placeholder_item = None
placeholder_items_by_id = {
placeholder_item.scene_identifier: placeholder_item
for placeholder_item in builder.get_placeholders()
}
for metadata_item in stub.get_metadata():
if not metadata_item.get("is_placeholder"):
continue
if selected_id in metadata_item.get("members"):
placeholder_item = placeholder_items_by_id.get(
metadata_item["uuid"])
break
if not placeholder_item:
stub.print_msg("Didn't find placeholder metadata. "
"Remove and re-create placeholder.")
return
window = WorkfileBuildPlaceholderDialog(host, builder)
window.set_update_mode(placeholder_item)
window.exec_()

View file

@ -35,6 +35,8 @@ class AEItem(object):
instance_id = attr.ib(default=None) # New Publisher
width = attr.ib(default=None)
height = attr.ib(default=None)
is_placeholder = attr.ib(default=False)
uuid = attr.ib(default=False)
class AfterEffectsServerStub():
@ -220,6 +222,16 @@ class AfterEffectsServerStub():
)
return self._to_records(self._handle_return(res))
def select_items(self, items):
"""
Select items in Project list
Args:
items (list): of int item ids
"""
self.websocketserver.call(
self.client.call('AfterEffects.select_items', items=items))
def get_selected_items(self, comps, folders=False, footages=False):
"""
Same as get_items but using selected items only
@ -240,6 +252,21 @@ class AfterEffectsServerStub():
)
return self._to_records(self._handle_return(res))
def add_item(self, name, item_type):
"""
Adds either composition or folder to project item list.
Args:
name (str)
item_type (str): COMP|FOLDER
"""
res = self.websocketserver.call(self.client.call
('AfterEffects.add_item',
name=name,
item_type=item_type))
return self._handle_return(res)
def get_item(self, item_id):
"""
Returns metadata for particular 'item_id' or None
@ -316,7 +343,7 @@ class AfterEffectsServerStub():
return self._handle_return(res)
def remove_instance(self, instance_id):
def remove_instance(self, instance_id, metadata=None):
"""
Removes instance with 'instance_id' from file's metadata and
saves them.
@ -328,7 +355,10 @@ class AfterEffectsServerStub():
"""
cleaned_data = []
for instance in self.get_metadata():
if metadata is None:
metadata = self.get_metadata()
for instance in metadata:
inst_id = instance.get("instance_id") or instance.get("uuid")
if inst_id != instance_id:
cleaned_data.append(instance)
@ -534,6 +564,47 @@ class AfterEffectsServerStub():
if records:
return records.pop()
def add_item_instead_placeholder(self, placeholder_item_id, item_id):
"""
Adds item_id to layers where plaeholder_item_id is present.
1 placeholder could result in multiple loaded containers (eg items)
Args:
placeholder_item_id (int): id of placeholder item
item_id (int): loaded FootageItem id
"""
res = self.websocketserver.call(self.client.call
('AfterEffects.add_item_instead_placeholder', # noqa
placeholder_item_id=placeholder_item_id, # noqa
item_id=item_id))
return self._handle_return(res)
def add_placeholder(self, name, width, height, fps, duration):
"""
Adds new FootageItem as a placeholder for workfile builder
Placeholder requires width etc, currently probably only hardcoded
values.
Args:
name (str)
width (int)
height (int)
fps (float)
duration (int)
"""
res = self.websocketserver.call(self.client.call
('AfterEffects.add_placeholder',
name=name,
width=width,
height=height,
fps=fps,
duration=duration))
return self._handle_return(res)
def render(self, folder_url, comp_id):
"""
Render all renderqueueitem to 'folder_url'
@ -632,7 +703,8 @@ class AfterEffectsServerStub():
d.get('file_name'),
d.get("instance_id"),
d.get("width"),
d.get("height"))
d.get("height"),
d.get("is_placeholder"))
ret.append(item)
return ret

View file

@ -1,17 +1,15 @@
import re
from openpype.pipeline import get_representation_path
from openpype.hosts.aftereffects.api import (
AfterEffectsLoader,
containerise
)
from openpype.hosts.aftereffects import api
from openpype.hosts.aftereffects.api.lib import (
get_background_layers,
get_unique_layer_name,
)
class BackgroundLoader(AfterEffectsLoader):
class BackgroundLoader(api.AfterEffectsLoader):
"""
Load images from Background family
Creates for each background separate folder with all imported images
@ -21,6 +19,7 @@ class BackgroundLoader(AfterEffectsLoader):
For each load container is created and stored in project (.aep)
metadata
"""
label = "Load JSON Background"
families = ["background"]
representations = ["json"]
@ -48,7 +47,7 @@ class BackgroundLoader(AfterEffectsLoader):
self[:] = [comp]
namespace = namespace or comp_name
return containerise(
return api.containerise(
name,
namespace,
comp,

View file

@ -1,14 +1,11 @@
import re
from openpype.pipeline import get_representation_path
from openpype.hosts.aftereffects.api import (
AfterEffectsLoader,
containerise
)
from openpype.hosts.aftereffects import api
from openpype.hosts.aftereffects.api.lib import get_unique_layer_name
class FileLoader(AfterEffectsLoader):
class FileLoader(api.AfterEffectsLoader):
"""Load images
Stores the imported asset in a container named after the asset.
@ -64,7 +61,7 @@ class FileLoader(AfterEffectsLoader):
self[:] = [comp]
namespace = namespace or comp_name
return containerise(
return api.containerise(
name,
namespace,
comp,

View file

@ -21,8 +21,13 @@ from .pipeline import (
reset_selection
)
from .constants import (
OPENPYPE_TAG_NAME,
DEFAULT_SEQUENCE_NAME,
DEFAULT_BIN_NAME
)
from .lib import (
pype_tag_name,
flatten,
get_track_items,
get_current_project,
@ -82,8 +87,12 @@ __all__ = [
"file_extensions",
"work_root",
# Constants
"OPENPYPE_TAG_NAME",
"DEFAULT_SEQUENCE_NAME",
"DEFAULT_BIN_NAME",
# Lib functions
"pype_tag_name",
"flatten",
"get_track_items",
"get_current_project",

View file

@ -0,0 +1,3 @@
OPENPYPE_TAG_NAME = "openpypeData"
DEFAULT_SEQUENCE_NAME = "openpypeSequence"
DEFAULT_BIN_NAME = "openpypeBin"

View file

@ -5,7 +5,6 @@ Host specific functions where host api is connected
from copy import deepcopy
import os
import re
import sys
import platform
import functools
import warnings
@ -29,12 +28,22 @@ from openpype.pipeline import (
from openpype.pipeline.load import filter_containers
from openpype.lib import Logger
from . import tags
from .constants import (
OPENPYPE_TAG_NAME,
DEFAULT_SEQUENCE_NAME,
DEFAULT_BIN_NAME
)
from openpype.pipeline.colorspace import (
get_imageio_config
)
class _CTX:
has_been_setup = False
has_menu = False
parent_gui = None
class DeprecatedWarning(DeprecationWarning):
pass
@ -82,23 +91,14 @@ def deprecated(new_destination):
log = Logger.get_logger(__name__)
self = sys.modules[__name__]
self._has_been_setup = False
self._has_menu = False
self._registered_gui = None
self._parent = None
self.pype_tag_name = "openpypeData"
self.default_sequence_name = "openpypeSequence"
self.default_bin_name = "openpypeBin"
def flatten(_list):
for item in _list:
if isinstance(item, (list, tuple)):
for sub_item in flatten(item):
def flatten(list_):
for item_ in list_:
if isinstance(item_, (list, tuple)):
for sub_item in flatten(item_):
yield sub_item
else:
yield item
yield item_
def get_current_project(remove_untitled=False):
@ -131,7 +131,7 @@ def get_current_sequence(name=None, new=False):
if new:
# create new
name = name or self.default_sequence_name
name = name or DEFAULT_SEQUENCE_NAME
sequence = hiero.core.Sequence(name)
root_bin.addItem(hiero.core.BinItem(sequence))
elif name:
@ -345,7 +345,7 @@ def get_track_item_tags(track_item):
# collect all tags which are not openpype tag
returning_tag_data.extend(
tag for tag in _tags
if tag.name() != self.pype_tag_name
if tag.name() != OPENPYPE_TAG_NAME
)
return returning_tag_data
@ -385,7 +385,7 @@ def set_track_openpype_tag(track, data=None):
# if pype tag available then update with input data
tag = tags.create_tag(
"{}_{}".format(
self.pype_tag_name,
OPENPYPE_TAG_NAME,
_get_tag_unique_hash()
),
tag_data
@ -412,7 +412,7 @@ def get_track_openpype_tag(track):
return None
for tag in _tags:
# return only correct tag defined by global name
if self.pype_tag_name in tag.name():
if OPENPYPE_TAG_NAME in tag.name():
return tag
@ -484,7 +484,7 @@ def get_trackitem_openpype_tag(track_item):
return None
for tag in _tags:
# return only correct tag defined by global name
if self.pype_tag_name in tag.name():
if OPENPYPE_TAG_NAME in tag.name():
return tag
@ -516,7 +516,7 @@ def set_trackitem_openpype_tag(track_item, data=None):
# if pype tag available then update with input data
tag = tags.create_tag(
"{}_{}".format(
self.pype_tag_name,
OPENPYPE_TAG_NAME,
_get_tag_unique_hash()
),
tag_data
@ -698,29 +698,29 @@ def setup(console=False, port=None, menu=True):
menu (bool, optional): Display file menu in Hiero.
"""
if self._has_been_setup:
if _CTX.has_been_setup:
teardown()
add_submission()
if menu:
add_to_filemenu()
self._has_menu = True
_CTX.has_menu = True
self._has_been_setup = True
_CTX.has_been_setup = True
log.debug("pyblish: Loaded successfully.")
def teardown():
"""Remove integration"""
if not self._has_been_setup:
if not _CTX.has_been_setup:
return
if self._has_menu:
if _CTX.has_menu:
remove_from_filemenu()
self._has_menu = False
_CTX.has_menu = False
self._has_been_setup = False
_CTX.has_been_setup = False
log.debug("pyblish: Integration torn down successfully")
@ -928,7 +928,7 @@ def create_bin(path=None, project=None):
# get the first loaded project
project = project or get_current_project()
path = path or self.default_bin_name
path = path or DEFAULT_BIN_NAME
path = path.replace("\\", "/").split("/")
@ -1311,11 +1311,11 @@ def before_project_save(event):
def get_main_window():
"""Acquire Nuke's main window"""
if self._parent is None:
if _CTX.parent_gui is None:
top_widgets = QtWidgets.QApplication.topLevelWidgets()
name = "Foundry::UI::DockMainWindow"
main_window = next(widget for widget in top_widgets if
widget.inherits("QMainWindow") and
widget.metaObject().className() == name)
self._parent = main_window
return self._parent
_CTX.parent_gui = main_window
return _CTX.parent_gui

View file

@ -3,20 +3,18 @@
import os
import re
import sys
import ast
import opentimelineio as otio
from . import utils
import hiero.core
import hiero.ui
self = sys.modules[__name__]
self.track_types = {
TRACK_TYPE_MAP = {
hiero.core.VideoTrack: otio.schema.TrackKind.Video,
hiero.core.AudioTrack: otio.schema.TrackKind.Audio
}
self.project_fps = None
self.marker_color_map = {
MARKER_COLOR_MAP = {
"magenta": otio.schema.MarkerColor.MAGENTA,
"red": otio.schema.MarkerColor.RED,
"yellow": otio.schema.MarkerColor.YELLOW,
@ -24,30 +22,21 @@ self.marker_color_map = {
"cyan": otio.schema.MarkerColor.CYAN,
"blue": otio.schema.MarkerColor.BLUE,
}
self.timeline = None
self.include_tags = True
def flatten(_list):
for item in _list:
if isinstance(item, (list, tuple)):
for sub_item in flatten(item):
class CTX:
project_fps = None
timeline = None
include_tags = True
def flatten(list_):
for item_ in list_:
if isinstance(item_, (list, tuple)):
for sub_item in flatten(item_):
yield sub_item
else:
yield item
def get_current_hiero_project(remove_untitled=False):
projects = flatten(hiero.core.projects())
if not remove_untitled:
return next(iter(projects))
# if remove_untitled
for proj in projects:
if "Untitled" in proj.name():
proj.close()
else:
return proj
yield item_
def create_otio_rational_time(frame, fps):
@ -152,7 +141,7 @@ def create_otio_reference(clip):
file_head = media_source.filenameHead()
is_sequence = not media_source.singleFile()
frame_duration = media_source.duration()
fps = utils.get_rate(clip) or self.project_fps
fps = utils.get_rate(clip) or CTX.project_fps
extension = os.path.splitext(path)[-1]
if is_sequence:
@ -217,8 +206,8 @@ def get_marker_color(tag):
res = re.search(pat, icon)
if res:
color = res.groupdict().get('color')
if color.lower() in self.marker_color_map:
return self.marker_color_map[color.lower()]
if color.lower() in MARKER_COLOR_MAP:
return MARKER_COLOR_MAP[color.lower()]
return otio.schema.MarkerColor.RED
@ -232,7 +221,7 @@ def create_otio_markers(otio_item, item):
# Hiero adds this tag to a lot of clips
continue
frame_rate = utils.get_rate(item) or self.project_fps
frame_rate = utils.get_rate(item) or CTX.project_fps
marked_range = otio.opentime.TimeRange(
start_time=otio.opentime.RationalTime(
@ -279,7 +268,7 @@ def create_otio_clip(track_item):
duration = int(track_item.duration())
fps = utils.get_rate(track_item) or self.project_fps
fps = utils.get_rate(track_item) or CTX.project_fps
name = track_item.name()
media_reference = create_otio_reference(clip)
@ -296,7 +285,7 @@ def create_otio_clip(track_item):
)
# Add tags as markers
if self.include_tags:
if CTX.include_tags:
create_otio_markers(otio_clip, track_item)
create_otio_markers(otio_clip, track_item.source())
@ -319,13 +308,13 @@ def create_otio_gap(gap_start, clip_start, tl_start_frame, fps):
def _create_otio_timeline():
project = get_current_hiero_project(remove_untitled=False)
metadata = _get_metadata(self.timeline)
project = CTX.timeline.project()
metadata = _get_metadata(CTX.timeline)
metadata.update({
"openpype.timeline.width": int(self.timeline.format().width()),
"openpype.timeline.height": int(self.timeline.format().height()),
"openpype.timeline.pixelAspect": int(self.timeline.format().pixelAspect()), # noqa
"openpype.timeline.width": int(CTX.timeline.format().width()),
"openpype.timeline.height": int(CTX.timeline.format().height()),
"openpype.timeline.pixelAspect": int(CTX.timeline.format().pixelAspect()), # noqa
"openpype.project.useOCIOEnvironmentOverride": project.useOCIOEnvironmentOverride(), # noqa
"openpype.project.lutSetting16Bit": project.lutSetting16Bit(),
"openpype.project.lutSetting8Bit": project.lutSetting8Bit(),
@ -339,10 +328,10 @@ def _create_otio_timeline():
})
start_time = create_otio_rational_time(
self.timeline.timecodeStart(), self.project_fps)
CTX.timeline.timecodeStart(), CTX.project_fps)
return otio.schema.Timeline(
name=self.timeline.name(),
name=CTX.timeline.name(),
global_start_time=start_time,
metadata=metadata
)
@ -351,7 +340,7 @@ def _create_otio_timeline():
def create_otio_track(track_type, track_name):
return otio.schema.Track(
name=track_name,
kind=self.track_types[track_type]
kind=TRACK_TYPE_MAP[track_type]
)
@ -363,7 +352,7 @@ def add_otio_gap(track_item, otio_track, prev_out):
gap = otio.opentime.TimeRange(
duration=otio.opentime.RationalTime(
gap_length,
self.project_fps
CTX.project_fps
)
)
otio_gap = otio.schema.Gap(source_range=gap)
@ -396,14 +385,14 @@ def create_otio_timeline():
return track_item.parent().items()[itemindex - 1]
# get current timeline
self.timeline = hiero.ui.activeSequence()
self.project_fps = self.timeline.framerate().toFloat()
CTX.timeline = hiero.ui.activeSequence()
CTX.project_fps = CTX.timeline.framerate().toFloat()
# convert timeline to otio
otio_timeline = _create_otio_timeline()
# loop all defined track types
for track in self.timeline.items():
for track in CTX.timeline.items():
# skip if track is disabled
if not track.isEnabled():
continue
@ -441,7 +430,7 @@ def create_otio_timeline():
otio_track.append(otio_clip)
# Add tags as markers
if self.include_tags:
if CTX.include_tags:
create_otio_markers(otio_track, track)
# add track to otio timeline

View file

@ -310,7 +310,7 @@ class PrecollectInstances(pyblish.api.ContextPlugin):
# add pypedata marker to otio_clip metadata
for marker in otio_clip.markers:
if phiero.pype_tag_name in marker.name:
if phiero.OPENPYPE_TAG_NAME in marker.name:
otio_clip.metadata.update(marker.metadata)
return {"otioClip": otio_clip}

View file

@ -8,7 +8,6 @@ from qtpy.QtGui import QPixmap
import hiero.ui
from openpype.pipeline import legacy_io
from openpype.hosts.hiero import api as phiero
from openpype.hosts.hiero.api.otio import hiero_export
@ -22,8 +21,8 @@ class PrecollectWorkfile(pyblish.api.ContextPlugin):
asset = legacy_io.Session["AVALON_ASSET"]
subset = "workfile"
project = phiero.get_current_project()
active_timeline = hiero.ui.activeSequence()
project = active_timeline.project()
fps = active_timeline.framerate().toFloat()
# adding otio timeline to context

View file

@ -3,12 +3,12 @@
import hou # noqa
from openpype.hosts.houdini.api import plugin
from openpype.pipeline import CreatedInstance
from openpype.lib import EnumDef
class CreateRedshiftROP(plugin.HoudiniCreator):
"""Redshift ROP"""
identifier = "io.openpype.creators.houdini.redshift_rop"
label = "Redshift ROP"
family = "redshift_rop"
@ -28,7 +28,7 @@ class CreateRedshiftROP(plugin.HoudiniCreator):
instance = super(CreateRedshiftROP, self).create(
subset_name,
instance_data,
pre_create_data) # type: CreatedInstance
pre_create_data)
instance_node = hou.node(instance.get("instance_node"))
@ -57,6 +57,8 @@ class CreateRedshiftROP(plugin.HoudiniCreator):
fmt="${aov}.$F4.{ext}".format(aov="AOV", ext=ext)
)
ext_format_index = {"exr": 0, "tif": 1, "jpg": 2, "png": 3}
parms = {
# Render frame range
"trange": 1,
@ -64,6 +66,7 @@ class CreateRedshiftROP(plugin.HoudiniCreator):
"RS_outputFileNamePrefix": filepath,
"RS_outputMultilayerMode": "1", # no multi-layered exr
"RS_outputBeautyAOVSuffix": "beauty",
"RS_outputFileFormat": ext_format_index[ext],
}
if self.selected_nodes:
@ -93,8 +96,7 @@ class CreateRedshiftROP(plugin.HoudiniCreator):
def get_pre_create_attr_defs(self):
attrs = super(CreateRedshiftROP, self).get_pre_create_attr_defs()
image_format_enum = [
"bmp", "cin", "exr", "jpg", "pic", "pic.gz", "png",
"rad", "rat", "rta", "sgi", "tga", "tif",
"exr", "tif", "jpg", "png",
]
return attrs + [

View file

@ -250,10 +250,7 @@ def reset_frame_range(fps: bool = True):
frame_range["handleStart"]
)
frame_end_handle = frame_range["frameEnd"] + int(frame_range["handleEnd"])
frange_cmd = (
f"animationRange = interval {frame_start_handle} {frame_end_handle}"
)
rt.Execute(frange_cmd)
set_timeline(frame_start_handle, frame_end_handle)
set_render_frame_range(frame_start_handle, frame_end_handle)
@ -285,3 +282,10 @@ def get_max_version():
"""
max_info = rt.MaxVersion()
return max_info[7]
def set_timeline(frameStart, frameEnd):
"""Set frame range for timeline editor in Max
"""
rt.animationRange = rt.interval(frameStart, frameEnd)
return rt.animationRange

View file

@ -2320,8 +2320,8 @@ def reset_frame_range(playback=True, render=True, fps=True):
cmds.currentTime(frame_start)
if render:
cmds.setAttr("defaultRenderGlobals.startFrame", frame_start)
cmds.setAttr("defaultRenderGlobals.endFrame", frame_end)
cmds.setAttr("defaultRenderGlobals.startFrame", animation_start)
cmds.setAttr("defaultRenderGlobals.endFrame", animation_end)
def reset_scene_resolution():
@ -3989,6 +3989,71 @@ def get_capture_preset(task_name, task_type, subset, project_settings, log):
return capture_preset or {}
def get_reference_node(members, log=None):
"""Get the reference node from the container members
Args:
members: list of node names
Returns:
str: Reference node name.
"""
# Collect the references without .placeHolderList[] attributes as
# unique entries (objects only) and skipping the sharedReferenceNode.
references = set()
for ref in cmds.ls(members, exactType="reference", objectsOnly=True):
# Ignore any `:sharedReferenceNode`
if ref.rsplit(":", 1)[-1].startswith("sharedReferenceNode"):
continue
# Ignore _UNKNOWN_REF_NODE_ (PLN-160)
if ref.rsplit(":", 1)[-1].startswith("_UNKNOWN_REF_NODE_"):
continue
references.add(ref)
assert references, "No reference node found in container"
# Get highest reference node (least parents)
highest = min(references,
key=lambda x: len(get_reference_node_parents(x)))
# Warn the user when we're taking the highest reference node
if len(references) > 1:
if not log:
log = logging.getLogger(__name__)
log.warning("More than one reference node found in "
"container, using highest reference node: "
"%s (in: %s)", highest, list(references))
return highest
def get_reference_node_parents(ref):
"""Return all parent reference nodes of reference node
Args:
ref (str): reference node.
Returns:
list: The upstream parent reference nodes.
"""
parent = cmds.referenceQuery(ref,
referenceNode=True,
parent=True)
parents = []
while parent:
parents.append(parent)
parent = cmds.referenceQuery(parent,
referenceNode=True,
parent=True)
return parents
def create_rig_animation_instance(
nodes, context, namespace, options=None, log=None
):

View file

@ -116,7 +116,7 @@ class MayaHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost):
register_event_callback("taskChanged", on_task_changed)
register_event_callback("workfile.open.before", before_workfile_open)
register_event_callback("workfile.save.before", before_workfile_save)
register_event_callback("workfile.save.before", after_workfile_save)
register_event_callback("workfile.save.after", after_workfile_save)
def open_workfile(self, filepath):
return open_file(filepath)

View file

@ -32,6 +32,9 @@ from .pipeline import containerise
from . import lib
log = Logger.get_logger()
def _get_attr(node, attr, default=None):
"""Helper to get attribute which allows attribute to not exist."""
if not cmds.attributeQuery(attr, node=node, exists=True):
@ -39,69 +42,28 @@ def _get_attr(node, attr, default=None):
return cmds.getAttr("{}.{}".format(node, attr))
def get_reference_node(members, log=None):
# Backwards compatibility: these functions has been moved to lib.
def get_reference_node(*args, **kwargs):
"""Get the reference node from the container members
Args:
members: list of node names
Returns:
str: Reference node name.
Deprecated:
This function was moved and will be removed in 3.16.x.
"""
# Collect the references without .placeHolderList[] attributes as
# unique entries (objects only) and skipping the sharedReferenceNode.
references = set()
for ref in cmds.ls(members, exactType="reference", objectsOnly=True):
# Ignore any `:sharedReferenceNode`
if ref.rsplit(":", 1)[-1].startswith("sharedReferenceNode"):
continue
# Ignore _UNKNOWN_REF_NODE_ (PLN-160)
if ref.rsplit(":", 1)[-1].startswith("_UNKNOWN_REF_NODE_"):
continue
references.add(ref)
assert references, "No reference node found in container"
# Get highest reference node (least parents)
highest = min(references,
key=lambda x: len(get_reference_node_parents(x)))
# Warn the user when we're taking the highest reference node
if len(references) > 1:
if not log:
log = Logger.get_logger(__name__)
log.warning("More than one reference node found in "
"container, using highest reference node: "
"%s (in: %s)", highest, list(references))
return highest
msg = "Function 'get_reference_node' has been moved."
log.warning(msg)
cmds.warning(msg)
return lib.get_reference_node(*args, **kwargs)
def get_reference_node_parents(ref):
"""Return all parent reference nodes of reference node
Args:
ref (str): reference node.
Returns:
list: The upstream parent reference nodes.
def get_reference_node_parents(*args, **kwargs):
"""
parent = cmds.referenceQuery(ref,
referenceNode=True,
parent=True)
parents = []
while parent:
parents.append(parent)
parent = cmds.referenceQuery(parent,
referenceNode=True,
parent=True)
return parents
Deprecated:
This function was moved and will be removed in 3.16.x.
"""
msg = "Function 'get_reference_node_parents' has been moved."
log.warning(msg)
cmds.warning(msg)
return lib.get_reference_node_parents(*args, **kwargs)
class Creator(LegacyCreator):
@ -598,7 +560,7 @@ class ReferenceLoader(Loader):
if not nodes:
return
ref_node = get_reference_node(nodes, self.log)
ref_node = lib.get_reference_node(nodes, self.log)
container = containerise(
name=name,
namespace=namespace,
@ -627,7 +589,7 @@ class ReferenceLoader(Loader):
# Get reference node from container members
members = get_container_members(node)
reference_node = get_reference_node(members, self.log)
reference_node = lib.get_reference_node(members, self.log)
namespace = cmds.referenceQuery(reference_node, namespace=True)
file_type = {
@ -775,7 +737,7 @@ class ReferenceLoader(Loader):
# Assume asset has been referenced
members = cmds.sets(node, query=True)
reference_node = get_reference_node(members, self.log)
reference_node = lib.get_reference_node(members, self.log)
assert reference_node, ("Imported container not supported; "
"container must be referenced.")

View file

@ -14,7 +14,7 @@ from openpype.tools.workfile_template_build import (
WorkfileBuildPlaceholderDialog,
)
from .lib import read, imprint, get_main_window
from .lib import read, imprint, get_reference_node, get_main_window
PLACEHOLDER_SET = "PLACEHOLDERS_SET"
@ -243,15 +243,19 @@ class MayaPlaceholderLoadPlugin(PlaceholderPlugin, PlaceholderLoadMixin):
def get_placeholder_options(self, options=None):
return self.get_load_plugin_options(options)
def cleanup_placeholder(self, placeholder, failed):
def post_placeholder_process(self, placeholder, failed):
"""Hide placeholder, add them to placeholder set
"""
node = placeholder._scene_identifier
node = placeholder.scene_identifier
cmds.sets(node, addElement=PLACEHOLDER_SET)
cmds.hide(node)
cmds.setAttr(node + ".hiddenInOutliner", True)
def delete_placeholder(self, placeholder):
"""Remove placeholder if building was successful"""
cmds.delete(placeholder.scene_identifier)
def load_succeed(self, placeholder, container):
self._parent_in_hierarchy(placeholder, container)
@ -268,9 +272,19 @@ class MayaPlaceholderLoadPlugin(PlaceholderPlugin, PlaceholderLoadMixin):
return
roots = cmds.sets(container, q=True)
ref_node = get_reference_node(roots)
nodes_to_parent = []
for root in roots:
if ref_node:
ref_root = cmds.referenceQuery(root, nodes=True)[0]
ref_root = (
cmds.listRelatives(ref_root, parent=True, path=True) or
[ref_root]
)
nodes_to_parent.extend(ref_root)
continue
if root.endswith("_RN"):
# Backwards compatibility for hardcoded reference names.
refRoot = cmds.referenceQuery(root, n=True)[0]
refRoot = cmds.listRelatives(refRoot, parent=True) or [refRoot]
nodes_to_parent.extend(refRoot)
@ -287,10 +301,17 @@ class MayaPlaceholderLoadPlugin(PlaceholderPlugin, PlaceholderLoadMixin):
matrix=True,
worldSpace=True
)
scene_parent = cmds.listRelatives(
placeholder.scene_identifier, parent=True, fullPath=True
)
for node in set(nodes_to_parent):
cmds.reorder(node, front=True)
cmds.reorder(node, relative=placeholder.data["index"])
cmds.xform(node, matrix=placeholder_form, ws=True)
if scene_parent:
cmds.parent(node, scene_parent)
else:
cmds.parent(node, world=True)
holding_sets = cmds.listSets(object=placeholder.scene_identifier)
if not holding_sets:

View file

@ -2250,16 +2250,15 @@ Reopening Nuke should synchronize these paths and resolve any discrepancies.
log.warning(msg)
nuke.message(msg)
return
data = self._asset_entity["data"]
log.debug("__ asset data: `{}`".format(data))
asset_data = self._asset_entity["data"]
missing_cols = []
check_cols = ["fps", "frameStart", "frameEnd",
"handleStart", "handleEnd"]
for col in check_cols:
if col not in data:
if col not in asset_data:
missing_cols.append(col)
if len(missing_cols) > 0:
@ -2271,12 +2270,12 @@ Reopening Nuke should synchronize these paths and resolve any discrepancies.
return
# get handles values
handle_start = data["handleStart"]
handle_end = data["handleEnd"]
handle_start = asset_data["handleStart"]
handle_end = asset_data["handleEnd"]
fps = float(data["fps"])
frame_start_handle = int(data["frameStart"]) - handle_start
frame_end_handle = int(data["frameEnd"]) + handle_end
fps = float(asset_data["fps"])
frame_start_handle = int(asset_data["frameStart"]) - handle_start
frame_end_handle = int(asset_data["frameEnd"]) + handle_end
self._root_node["lock_range"].setValue(False)
self._root_node["fps"].setValue(fps)
@ -2284,21 +2283,18 @@ Reopening Nuke should synchronize these paths and resolve any discrepancies.
self._root_node["last_frame"].setValue(frame_end_handle)
self._root_node["lock_range"].setValue(True)
# setting active viewers
try:
nuke.frame(int(data["frameStart"]))
except Exception as e:
log.warning("no viewer in scene: `{}`".format(e))
# update node graph so knobs are updated
update_node_graph()
range = '{0}-{1}'.format(
int(data["frameStart"]),
int(data["frameEnd"])
frame_range = '{0}-{1}'.format(
int(asset_data["frameStart"]),
int(asset_data["frameEnd"])
)
for node in nuke.allNodes(filter="Viewer"):
node['frame_range'].setValue(range)
node['frame_range'].setValue(frame_range)
node['frame_range_lock'].setValue(True)
node['frame_range'].setValue(range)
node['frame_range'].setValue(frame_range)
node['frame_range_lock'].setValue(True)
if not ASSIST:
@ -2320,12 +2316,9 @@ Reopening Nuke should synchronize these paths and resolve any discrepancies.
"""Set resolution to project resolution."""
log.info("Resetting resolution")
project_name = legacy_io.active_project()
project = get_project(project_name)
asset_name = legacy_io.Session["AVALON_ASSET"]
asset = get_asset_by_name(project_name, asset_name)
asset_data = asset.get('data', {})
asset_data = self._asset_entity["data"]
data = {
format_data = {
"width": int(asset_data.get(
'resolutionWidth',
asset_data.get('resolution_width'))),
@ -2335,37 +2328,40 @@ Reopening Nuke should synchronize these paths and resolve any discrepancies.
"pixel_aspect": asset_data.get(
'pixelAspect',
asset_data.get('pixel_aspect', 1)),
"name": project["name"]
"name": project_name
}
if any(x for x in data.values() if x is None):
if any(x_ for x_ in format_data.values() if x_ is None):
msg = ("Missing set shot attributes in DB."
"\nContact your supervisor!."
"\n\nWidth: `{width}`"
"\nHeight: `{height}`"
"\nPixel Asspect: `{pixel_aspect}`").format(**data)
"\nPixel Aspect: `{pixel_aspect}`").format(**format_data)
log.error(msg)
nuke.message(msg)
existing_format = None
for format in nuke.formats():
if data["name"] == format.name():
if format_data["name"] == format.name():
existing_format = format
break
if existing_format:
# Enforce existing format to be correct.
existing_format.setWidth(data["width"])
existing_format.setHeight(data["height"])
existing_format.setPixelAspect(data["pixel_aspect"])
existing_format.setWidth(format_data["width"])
existing_format.setHeight(format_data["height"])
existing_format.setPixelAspect(format_data["pixel_aspect"])
else:
format_string = self.make_format_string(**data)
format_string = self.make_format_string(**format_data)
log.info("Creating new format: {}".format(format_string))
nuke.addFormat(format_string)
nuke.root()["format"].setValue(data["name"])
nuke.root()["format"].setValue(format_data["name"])
log.info("Format is set.")
# update node graph so knobs are updated
update_node_graph()
def make_format_string(self, **kwargs):
if kwargs.get("r"):
return (
@ -2484,6 +2480,20 @@ def get_dependent_nodes(nodes):
return connections_in, connections_out
def update_node_graph():
# Resetting frame will update knob values
try:
root_node_lock = nuke.root()["lock_range"].value()
nuke.root()["lock_range"].setValue(not root_node_lock)
nuke.root()["lock_range"].setValue(root_node_lock)
current_frame = nuke.frame()
nuke.frame(1)
nuke.frame(int(current_frame))
except Exception as error:
log.warning(error)
def find_free_space_to_paste_nodes(
nodes,
group=nuke.root(),

View file

@ -2,7 +2,7 @@ import os
import nuke
from openpype import resources
from .lib import maintained_selection
from qtpy import QtWidgets
def set_context_favorites(favorites=None):
@ -55,6 +55,7 @@ def bake_gizmos_recursively(in_group=None):
Arguments:
is_group (nuke.Node)[optonal]: group node or all nodes
"""
from .lib import maintained_selection
if in_group is None:
in_group = nuke.Root()
# preserve selection after all is done
@ -129,3 +130,11 @@ def get_colorspace_list(colorspace_knob):
reduced_clrs.append(clrs)
return reduced_clrs
def is_headless():
"""
Returns:
bool: headless
"""
return QtWidgets.QApplication.instance() is None

View file

@ -190,7 +190,7 @@ class NukePlaceholderLoadPlugin(NukePlaceholderPlugin, PlaceholderLoadMixin):
def get_placeholder_options(self, options=None):
return self.get_load_plugin_options(options)
def cleanup_placeholder(self, placeholder, failed):
def post_placeholder_process(self, placeholder, failed):
# deselect all selected nodes
placeholder_node = nuke.toNode(placeholder.scene_identifier)
@ -604,7 +604,7 @@ class NukePlaceholderCreatePlugin(
def get_placeholder_options(self, options=None):
return self.get_create_plugin_options(options)
def cleanup_placeholder(self, placeholder, failed):
def post_placeholder_process(self, placeholder, failed):
# deselect all selected nodes
placeholder_node = nuke.toNode(placeholder.scene_identifier)

View file

@ -1,6 +1,7 @@
"""Host API required Work Files tool"""
import os
import nuke
from .utils import is_headless
def file_extensions():
@ -25,6 +26,12 @@ def open_file(filepath):
# To remain in the same window, we have to clear the script and read
# in the contents of the workfile.
nuke.scriptClear()
if not is_headless():
autosave = nuke.toNode("preferences")["AutoSaveName"].evaluate()
autosave_prmpt = "Autosave detected.\nWould you like to load the autosave file?" # noqa
if os.path.isfile(autosave) and nuke.ask(autosave_prmpt):
filepath = autosave
nuke.scriptReadFile(filepath)
nuke.Root()["name"].setValue(filepath)
nuke.Root()["project_directory"].setValue(os.path.dirname(filepath))

View file

@ -15,7 +15,6 @@ log = Logger.get_logger(__name__)
self = sys.modules[__name__]
self.project_manager = None
self.media_storage = None
self.current_project = None
# OpenPype sequential rename variables
self.rename_index = 0
@ -88,10 +87,7 @@ def get_media_storage():
def get_current_project():
"""Get current project object.
"""
if not self.current_project:
self.current_project = get_project_manager().GetCurrentProject()
return self.current_project
return get_project_manager().GetCurrentProject()
def get_current_timeline(new=False):

View file

@ -4,18 +4,15 @@ import os
from openpype.lib import Logger
from .lib import (
get_project_manager,
get_current_project,
set_project_manager_to_folder_name
get_current_project
)
log = Logger.get_logger(__name__)
exported_projet_ext = ".drp"
def file_extensions():
return [exported_projet_ext]
return [".drp"]
def has_unsaved_changes():
@ -30,13 +27,17 @@ def save_file(filepath):
project = get_current_project()
name = project.GetName()
if "Untitled Project" not in name:
log.info("Saving project: `{}` as '{}'".format(name, file))
pm.ExportProject(name, filepath)
else:
log.info("Creating new project...")
pm.CreateProject(fname)
pm.ExportProject(name, filepath)
response = False
if name == "Untitled Project":
response = pm.CreateProject(fname)
log.info("New project created: {}".format(response))
pm.SaveProject()
elif name != fname:
response = project.SetName(fname)
log.info("Project renamed: {}".format(response))
exported = pm.ExportProject(fname, filepath)
log.info("Project exported: {}".format(exported))
def open_file(filepath):
@ -57,10 +58,8 @@ def open_file(filepath):
file = os.path.basename(filepath)
fname, _ = os.path.splitext(file)
dname, _ = fname.split("_v")
try:
if not set_project_manager_to_folder_name(dname):
raise
# load project from input path
project = pm.LoadProject(fname)
log.info(f"Project {project.GetName()} opened...")
@ -79,14 +78,18 @@ def open_file(filepath):
def current_file():
pm = get_project_manager()
current_dir = os.getenv("AVALON_WORKDIR")
file_ext = file_extensions()[0]
workdir_path = os.getenv("AVALON_WORKDIR")
project = pm.GetCurrentProject()
name = project.GetName()
fname = name + exported_projet_ext
current_file = os.path.join(current_dir, fname)
if not current_file:
return None
return os.path.normpath(current_file)
project_name = project.GetName()
file_name = project_name + file_ext
# create current file path
current_file_path = os.path.join(workdir_path, file_name)
# return current file path if it exists
if os.path.exists(current_file_path):
return os.path.normpath(current_file_path)
def work_root(session):

View file

@ -1,17 +1,13 @@
import os
from openpype.lib import PreLaunchHook
import openpype.hosts.resolve
class ResolveLaunchLastWorkfile(PreLaunchHook):
class PreLaunchResolveLastWorkfile(PreLaunchHook):
"""Special hook to open last workfile for Resolve.
Checks 'start_last_workfile', if set to False, it will not open last
workfile. This property is set explicitly in Launcher.
"""
# Execute after workfile template copy
order = 10
app_groups = ["resolve"]
@ -30,16 +26,9 @@ class ResolveLaunchLastWorkfile(PreLaunchHook):
return
# Add path to launch environment for the startup script to pick up
self.log.info(f"Setting OPENPYPE_RESOLVE_OPEN_ON_LAUNCH to launch "
f"last workfile: {last_workfile}")
self.log.info(
"Setting OPENPYPE_RESOLVE_OPEN_ON_LAUNCH to launch "
f"last workfile: {last_workfile}"
)
key = "OPENPYPE_RESOLVE_OPEN_ON_LAUNCH"
self.launch_context.env[key] = last_workfile
# Set the openpype prelaunch startup script path for easy access
# in the LUA .scriptlib code
op_resolve_root = os.path.dirname(openpype.hosts.resolve.__file__)
script_path = os.path.join(op_resolve_root, "startup.py")
key = "OPENPYPE_RESOLVE_STARTUP_SCRIPT"
self.launch_context.env[key] = script_path
self.log.info("Setting OPENPYPE_RESOLVE_STARTUP_SCRIPT to: "
f"{script_path}")

View file

@ -5,7 +5,7 @@ from openpype.lib import PreLaunchHook
from openpype.hosts.resolve.utils import setup
class ResolvePrelaunch(PreLaunchHook):
class PreLaunchResolveSetup(PreLaunchHook):
"""
This hook will set up the Resolve scripting environment as described in
Resolve's documentation found with the installed application at

View file

@ -0,0 +1,24 @@
import os
from openpype.lib import PreLaunchHook
import openpype.hosts.resolve
class PreLaunchResolveStartup(PreLaunchHook):
"""Special hook to configure startup script.
"""
order = 11
app_groups = ["resolve"]
def execute(self):
# Set the openpype prelaunch startup script path for easy access
# in the LUA .scriptlib code
op_resolve_root = os.path.dirname(openpype.hosts.resolve.__file__)
script_path = os.path.join(op_resolve_root, "startup.py")
key = "OPENPYPE_RESOLVE_STARTUP_SCRIPT"
self.launch_context.env[key] = script_path
self.log.info(
f"Setting OPENPYPE_RESOLVE_STARTUP_SCRIPT to: {script_path}"
)

View file

@ -10,9 +10,11 @@ This code runs in a separate process to the main Resolve process.
"""
import os
from openpype.lib import Logger
import openpype.hosts.resolve.api
log = Logger.get_logger(__name__)
def ensure_installed_host():
"""Install resolve host with openpype and return the registered host.
@ -44,17 +46,22 @@ def open_file(path):
def main():
# Open last workfile
workfile_path = os.environ.get("OPENPYPE_RESOLVE_OPEN_ON_LAUNCH")
if workfile_path:
if workfile_path and os.path.exists(workfile_path):
log.info(f"Opening last workfile: {workfile_path}")
open_file(workfile_path)
else:
print("No last workfile set to open. Skipping..")
log.info("No last workfile set to open. Skipping..")
# Launch OpenPype menu
from openpype.settings import get_project_settings
from openpype.pipeline.context_tools import get_current_project_name
project_name = get_current_project_name()
log.info(f"Current project name in context: {project_name}")
settings = get_project_settings(project_name)
if settings.get("resolve", {}).get("launch_openpype_menu_on_start", True):
log.info("Launching OpenPype menu..")
launch_menu()

View file

@ -0,0 +1,5 @@
#! python3
from openpype.hosts.resolve.startup import main
if __name__ == "__main__":
main()

View file

@ -0,0 +1,66 @@
import pyblish.api
from openpype.pipeline import registered_host
from openpype.pipeline import publish
from openpype.lib import EnumDef
from openpype.pipeline import colorspace
class CollectColorspace(pyblish.api.InstancePlugin,
publish.OpenPypePyblishPluginMixin,
publish.ColormanagedPyblishPluginMixin):
"""Collect explicit user defined representation colorspaces"""
label = "Choose representation colorspace"
order = pyblish.api.CollectorOrder + 0.49
hosts = ["traypublisher"]
colorspace_items = [
(None, "Don't override")
]
colorspace_attr_show = False
def process(self, instance):
values = self.get_attr_values_from_data(instance.data)
colorspace = values.get("colorspace", None)
if colorspace is None:
return
self.log.debug("Explicit colorspace set to: {}".format(colorspace))
context = instance.context
for repre in instance.data.get("representations", {}):
self.set_representation_colorspace(
representation=repre,
context=context,
colorspace=colorspace
)
@classmethod
def apply_settings(cls, project_settings):
host = registered_host()
host_name = host.name
project_name = host.get_current_project_name()
config_data = colorspace.get_imageio_config(
project_name, host_name,
project_settings=project_settings
)
if config_data:
filepath = config_data["path"]
config_items = colorspace.get_ocio_config_colorspaces(filepath)
cls.colorspace_items.extend((
(name, name) for name in config_items.keys()
))
cls.colorspace_attr_show = True
@classmethod
def get_attribute_defs(cls):
return [
EnumDef(
"colorspace",
cls.colorspace_items,
default="Don't override",
label="Override Colorspace",
hidden=not cls.colorspace_attr_show
)
]

View file

@ -0,0 +1,53 @@
import pyblish.api
from openpype.pipeline import (
publish,
PublishValidationError
)
from openpype.pipeline.colorspace import (
get_ocio_config_colorspaces
)
class ValidateColorspace(pyblish.api.InstancePlugin,
publish.OpenPypePyblishPluginMixin,
publish.ColormanagedPyblishPluginMixin):
"""Validate representation colorspaces"""
label = "Validate representation colorspace"
order = pyblish.api.ValidatorOrder
hosts = ["traypublisher"]
def process(self, instance):
config_colorspaces = {} # cache of colorspaces per config path
for repre in instance.data.get("representations", {}):
colorspace_data = repre.get("colorspaceData", {})
if not colorspace_data:
# Nothing to validate
continue
config_path = colorspace_data["config"]["path"]
if config_path not in config_colorspaces:
colorspaces = get_ocio_config_colorspaces(config_path)
config_colorspaces[config_path] = set(colorspaces)
colorspace = colorspace_data["colorspace"]
self.log.debug(
f"Validating representation '{repre['name']}' "
f"colorspace '{colorspace}'"
)
if colorspace not in config_colorspaces[config_path]:
message = (
f"Representation '{repre['name']}' colorspace "
f"'{colorspace}' does not exist in OCIO config: "
f"{config_path}"
)
raise PublishValidationError(
title="Representation colorspace",
message=message,
description=message
)

View file

@ -167,7 +167,7 @@ def filter_profiles(profiles_data, key_values, keys_order=None, logger=None):
for item in key_values.items()
])
logger.info(
logger.debug(
"Looking for matching profile for: {}".format(log_parts)
)
@ -209,19 +209,19 @@ def filter_profiles(profiles_data, key_values, keys_order=None, logger=None):
matching_profiles.append((profile, profile_scores))
if not matching_profiles:
logger.info(
logger.debug(
"None of profiles match your setup. {}".format(log_parts)
)
return None
if len(matching_profiles) > 1:
logger.info(
logger.debug(
"More than one profile match your setup. {}".format(log_parts)
)
profile = _profile_exclusion(matching_profiles, logger)
if profile:
logger.info(
logger.debug(
"Profile selected: {}".format(profile)
)
return profile

View file

@ -71,7 +71,7 @@ class CollectFtrackFamily(pyblish.api.InstancePlugin):
if "ftrack" not in families:
families.append("ftrack")
self.log.info("{} 'ftrack' family for instance with '{}'".format(
self.log.debug("{} 'ftrack' family for instance with '{}'".format(
result_str, family
))

View file

@ -75,7 +75,7 @@ class CollectFtrackTaskStatuses(pyblish.api.ContextPlugin):
}
context.data["ftrackTaskStatuses"] = task_statuses_by_type_id
context.data["ftrackStatusByTaskId"] = {}
self.log.info("Collected ftrack task statuses.")
self.log.debug("Collected ftrack task statuses.")
class IntegrateFtrackStatusBase(pyblish.api.InstancePlugin):
@ -116,7 +116,7 @@ class IntegrateFtrackStatusBase(pyblish.api.InstancePlugin):
profiles = self.get_status_profiles()
if not profiles:
project_name = context.data["projectName"]
self.log.info((
self.log.debug((
"Status profiles are not filled for project \"{}\". Skipping"
).format(project_name))
return
@ -124,7 +124,7 @@ class IntegrateFtrackStatusBase(pyblish.api.InstancePlugin):
# Task statuses were not collected -> skip
task_statuses_by_type_id = context.data.get("ftrackTaskStatuses")
if not task_statuses_by_type_id:
self.log.info(
self.log.debug(
"Ftrack task statuses are not collected. Skipping.")
return
@ -364,12 +364,12 @@ class IntegrateFtrackTaskStatus(pyblish.api.ContextPlugin):
def process(self, context):
task_statuses_by_type_id = context.data.get("ftrackTaskStatuses")
if not task_statuses_by_type_id:
self.log.info("Ftrack task statuses are not collected. Skipping.")
self.log.debug("Ftrack task statuses are not collected. Skipping.")
return
status_by_task_id = self._get_status_by_task_id(context)
if not status_by_task_id:
self.log.info("No statuses to set. Skipping.")
self.log.debug("No statuses to set. Skipping.")
return
ftrack_session = context.data["ftrackSession"]

View file

@ -240,7 +240,7 @@ def get_data_subprocess(config_path, data_type):
def compatible_python():
"""Only 3.9 or higher can directly use PyOpenColorIO in ocio_wrapper"""
compatible = False
if sys.version[0] == 3 and sys.version[1] >= 9:
if sys.version_info.major == 3 and sys.version_info.minor >= 9:
compatible = True
return compatible

View file

@ -233,36 +233,40 @@ class RepairAction(pyblish.api.Action):
raise RuntimeError("Plug-in does not have repair method.")
# Get the errored instances
self.log.info("Finding failed instances..")
self.log.debug("Finding failed instances..")
errored_instances = get_errored_instances_from_context(context)
# Apply pyblish.logic to get the instances for the plug-in
instances = pyblish.api.instances_by_plugin(errored_instances, plugin)
for instance in instances:
self.log.debug(
"Attempting repair for instance: {} ...".format(instance)
)
plugin.repair(instance)
class RepairContextAction(pyblish.api.Action):
"""Repairs the action
To process the repairing this requires a static `repair(instance)` method
To process the repairing this requires a static `repair(context)` method
is available on the plugin.
"""
label = "Repair"
on = "failed" # This action is only available on a failed plug-in
icon = "wrench" # Icon from Awesome Icon
def process(self, context, plugin):
if not hasattr(plugin, "repair"):
raise RuntimeError("Plug-in does not have repair method.")
# Get the failed instances
self.log.info("Finding failed instances..")
self.log.debug("Finding failed plug-ins..")
failed_plugins = get_errored_plugins_from_context(context)
# Apply pyblish.logic to get the instances for the plug-in
if plugin in failed_plugins:
self.log.info("Attempting fix ...")
self.log.debug("Attempting repair ...")
plugin.repair(context)

View file

@ -478,7 +478,9 @@ class AbstractTemplateBuilder(object):
create_first_version = template_preset["create_first_version"]
# check if first version is created
created_version_workfile = self.create_first_workfile_version()
created_version_workfile = False
if create_first_version:
created_version_workfile = self.create_first_workfile_version()
# if first version is created, import template
# and populate placeholders
@ -1564,7 +1566,16 @@ class PlaceholderLoadMixin(object):
else:
failed = False
self.load_succeed(placeholder, container)
self.cleanup_placeholder(placeholder, failed)
self.post_placeholder_process(placeholder, failed)
if failed:
self.log.debug(
"Placeholder cleanup skipped due to failed placeholder "
"population."
)
return
if not placeholder.data.get("keep_placeholder", True):
self.delete_placeholder(placeholder)
def load_failed(self, placeholder, representation):
if hasattr(placeholder, "load_failed"):
@ -1574,7 +1585,7 @@ class PlaceholderLoadMixin(object):
if hasattr(placeholder, "load_succeed"):
placeholder.load_succeed(container)
def cleanup_placeholder(self, placeholder, failed):
def post_placeholder_process(self, placeholder, failed):
"""Cleanup placeholder after load of single representation.
Can be called multiple times during placeholder item populating and is
@ -1588,6 +1599,10 @@ class PlaceholderLoadMixin(object):
pass
def delete_placeholder(self, placeholder, failed):
"""Called when all item population is done."""
self.log.debug("Clean up of placeholder is not implemented.")
class PlaceholderCreateMixin(object):
"""Mixin prepared for creating placeholder plugins.
@ -1673,12 +1688,14 @@ class PlaceholderCreateMixin(object):
)
]
def populate_create_placeholder(self, placeholder):
def populate_create_placeholder(self, placeholder, pre_create_data=None):
"""Create placeholder is going to create matching publishabe instance.
Args:
placeholder (PlaceholderItem): Placeholder item with information
about requested publishable instance.
pre_create_data (dict): dictionary of configuration from Creator
configuration in UI
"""
legacy_create = self.builder.use_legacy_creators
@ -1736,7 +1753,8 @@ class PlaceholderCreateMixin(object):
creator_plugin.identifier,
create_variant,
asset_doc,
task_name=task_name
task_name=task_name,
pre_create_data=pre_create_data
)
except: # noqa: E722
@ -1747,7 +1765,7 @@ class PlaceholderCreateMixin(object):
failed = False
self.create_succeed(placeholder, creator_instance)
self.cleanup_placeholder(placeholder, failed)
self.post_placeholder_process(placeholder, failed)
def create_failed(self, placeholder, creator_data):
if hasattr(placeholder, "create_failed"):
@ -1757,7 +1775,7 @@ class PlaceholderCreateMixin(object):
if hasattr(placeholder, "create_succeed"):
placeholder.create_succeed(creator_instance)
def cleanup_placeholder(self, placeholder, failed):
def post_placeholder_process(self, placeholder, failed):
"""Cleanup placeholder after load of single representation.
Can be called multiple times during placeholder item populating and is

View file

@ -86,8 +86,8 @@ class CleanUp(pyblish.api.InstancePlugin):
return
if not os.path.normpath(staging_dir).startswith(temp_root):
self.log.info("Skipping cleanup. Staging directory is not in the "
"temp folder: %s" % staging_dir)
self.log.debug("Skipping cleanup. Staging directory is not in the "
"temp folder: %s" % staging_dir)
return
if not os.path.exists(staging_dir):

View file

@ -442,7 +442,7 @@ class ModifiedBurnins(ffmpeg_burnins.Burnins):
with tempfile.NamedTemporaryFile(mode="w", delete=False) as temp:
temp.write(filter_string)
filters_path = temp.name
filters = '-filter_script "{}"'.format(filters_path)
filters = '-filter_script:v "{}"'.format(filters_path)
print("Filters:", filter_string)
self.cleanup_paths.append(filters_path)

View file

@ -32,10 +32,18 @@
"skip_timelines_check": [
".*"
]
},
"ValidateContainers": {
"enabled": true,
"optional": true,
"active": true
}
},
"workfile_builder": {
"create_first_version": false,
"custom_templates": []
},
"templated_workfile_build": {
"profiles": []
}
}

View file

@ -107,6 +107,17 @@
"label": "Skip Timeline Check for Tasks"
}
]
},
{
"type": "schema_template",
"name": "template_publish_plugin",
"template_data": [
{
"docstring": "Check if loaded container in scene are latest versions.",
"key": "ValidateContainers",
"label": "ValidateContainers"
}
]
}
]
},
@ -117,6 +128,10 @@
"workfile_builder/builder_on_start",
"workfile_builder/profiles"
]
},
{
"type": "schema",
"name": "schema_templated_workfile_build"
}
]
}

View file

@ -443,29 +443,29 @@ class PublishPluginsProxy:
def __init__(self, plugins):
plugins_by_id = {}
actions_by_id = {}
actions_by_plugin_id = {}
action_ids_by_plugin_id = {}
for plugin in plugins:
plugin_id = plugin.id
plugins_by_id[plugin_id] = plugin
action_ids = []
actions_by_id = {}
action_ids_by_plugin_id[plugin_id] = action_ids
actions_by_plugin_id[plugin_id] = actions_by_id
actions = getattr(plugin, "actions", None) or []
for action in actions:
action_id = 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
self._actions_by_id = actions_by_id
self._actions_by_plugin_id = actions_by_plugin_id
self._action_ids_by_plugin_id = action_ids_by_plugin_id
def get_action(self, action_id):
return self._actions_by_id[action_id]
def get_action(self, plugin_id, action_id):
return self._actions_by_plugin_id[plugin_id][action_id]
def get_plugin(self, plugin_id):
return self._plugins_by_id[plugin_id]
@ -497,7 +497,9 @@ class PublishPluginsProxy:
"""
return [
self._create_action_item(self._actions_by_id[action_id], plugin_id)
self._create_action_item(
self.get_action(plugin_id, action_id), plugin_id
)
for action_id in self._action_ids_by_plugin_id[plugin_id]
]
@ -2308,7 +2310,7 @@ class PublisherController(BasePublisherController):
def run_action(self, plugin_id, action_id):
# TODO handle result in UI
plugin = self._publish_plugins_proxy.get_plugin(plugin_id)
action = self._publish_plugins_proxy.get_action(action_id)
action = self._publish_plugins_proxy.get_action(plugin_id, action_id)
result = pyblish.plugin.process(
plugin, self._publish_context, None, action.id

View file

@ -1,3 +1,3 @@
# -*- coding: utf-8 -*-
"""Package declaring Pype version."""
__version__ = "3.15.11-nightly.4"
__version__ = "3.15.12-nightly.1"