Merge branch 'develop' into enhancement/houdini_resdshift_allow_using_get_aov_from_other_node

This commit is contained in:
Kayla Man 2024-02-23 22:43:12 +08:00 committed by GitHub
commit 93d71ff4f7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
41 changed files with 934 additions and 640 deletions

View file

@ -54,25 +54,36 @@ class LoadClip(phiero.SequenceLoader):
plugin_name = cls.__name__
plugin_settings = None
# Look for plugin settings in host specific settings
if plugin_name in plugin_type_settings:
plugin_settings = plugin_type_settings[plugin_name]
plugin_settings = plugin_type_settings.get(plugin_name)
if not plugin_settings:
return
print(">>> We have preset for {}".format(plugin_name))
for option, value in plugin_settings.items():
if option == "representations":
continue
if option == "product_types":
# TODO remove the key conversion when loaders can filter by
# product types
# convert 'product_types' to 'families'
option = "families"
elif option == "clip_name_template":
# TODO remove the formatting replacement
value = (
value
.replace("{folder[name]}", "{asset}")
.replace("{product[name]}", "{subset}")
)
if option == "enabled" and value is False:
print(" - is disabled by preset")
elif option == "representations":
continue
else:
print(" - setting `{}`: `{}`".format(option, value))
setattr(cls, option, value)
def load(self, context, name, namespace, options):
# add clip name template to options
options.update({

View file

@ -0,0 +1,42 @@
from pymxs import runtime as rt
import pyblish.api
from ayon_core.pipeline.publish import get_errored_instances_from_context
class SelectInvalidAction(pyblish.api.Action):
"""Select invalid objects in Blender when a publish plug-in failed."""
label = "Select Invalid"
on = "failed"
icon = "search"
def process(self, context, plugin):
errored_instances = get_errored_instances_from_context(context,
plugin=plugin)
# Get the invalid nodes for the plug-ins
self.log.info("Finding invalid nodes...")
invalid = list()
for instance in errored_instances:
invalid_nodes = plugin.get_invalid(instance)
if invalid_nodes:
if isinstance(invalid_nodes, (list, tuple)):
invalid.extend(invalid_nodes)
else:
self.log.warning(
"Failed plug-in doesn't have any selectable objects."
)
if not invalid:
self.log.info("No invalid nodes found.")
return
invalid_names = [obj.name for obj in invalid if not isinstance(obj, tuple)]
if not invalid_names:
invalid_names = [obj.name for obj, _ in invalid]
invalid = [obj for obj, _ in invalid]
self.log.info(
"Selecting invalid objects: %s", ", ".join(invalid_names)
)
rt.Select(invalid)

View file

@ -245,3 +245,27 @@ def get_previous_loaded_object(container: str):
if str(obj) in sel_list:
node_list.append(obj)
return node_list
def remove_container_data(container_node: str):
"""Function to remove container data after updating, switching or deleting it.
Args:
container_node (str): container node
"""
if container_node.modifiers[0].name == "OP Data":
all_set_members_names = [
member.node for member
in container_node.modifiers[0].openPypeData.all_handles]
# clean up the children of alembic dummy objects
for current_set_member in all_set_members_names:
shape_list = [members for members in current_set_member.Children
if rt.ClassOf(members) == rt.AlembicObject
or rt.isValidNode(members)]
if shape_list: # noqa
rt.Delete(shape_list)
rt.Delete(current_set_member)
rt.deleteModifier(container_node, container_node.modifiers[0])
rt.Delete(container_node)
rt.redrawViews()

View file

@ -1,6 +1,6 @@
import os
from ayon_core.hosts.max.api import lib, maintained_selection
from ayon_core.hosts.max.api import lib
from ayon_core.hosts.max.api.lib import (
unique_namespace,
get_namespace,
@ -9,7 +9,8 @@ from ayon_core.hosts.max.api.lib import (
from ayon_core.hosts.max.api.pipeline import (
containerise,
get_previous_loaded_object,
update_custom_attribute_data
update_custom_attribute_data,
remove_container_data
)
from ayon_core.pipeline import get_representation_path, load
@ -96,4 +97,4 @@ class FbxLoader(load.LoaderPlugin):
from pymxs import runtime as rt
node = rt.GetNodeByName(container["instance_node"])
rt.Delete(node)
remove_container_data(node)

View file

@ -8,7 +8,8 @@ from ayon_core.hosts.max.api.lib import (
)
from ayon_core.hosts.max.api.pipeline import (
containerise, get_previous_loaded_object,
update_custom_attribute_data
update_custom_attribute_data,
remove_container_data
)
from ayon_core.pipeline import get_representation_path, load
@ -93,6 +94,5 @@ class MaxSceneLoader(load.LoaderPlugin):
def remove(self, container):
from pymxs import runtime as rt
node = rt.GetNodeByName(container["instance_node"])
rt.Delete(node)
remove_container_data(node)

View file

@ -2,7 +2,8 @@ import os
from ayon_core.pipeline import load, get_representation_path
from ayon_core.hosts.max.api.pipeline import (
containerise,
get_previous_loaded_object
get_previous_loaded_object,
remove_container_data
)
from ayon_core.hosts.max.api import lib
from ayon_core.hosts.max.api.lib import (
@ -97,9 +98,9 @@ class ModelAbcLoader(load.LoaderPlugin):
def remove(self, container):
from pymxs import runtime as rt
node = rt.GetNodeByName(container["instance_node"])
rt.Delete(node)
remove_container_data(node)
@staticmethod
def get_container_children(parent, type_name):

View file

@ -2,7 +2,8 @@ import os
from ayon_core.pipeline import load, get_representation_path
from ayon_core.hosts.max.api.pipeline import (
containerise, get_previous_loaded_object,
update_custom_attribute_data
update_custom_attribute_data,
remove_container_data
)
from ayon_core.hosts.max.api import lib
from ayon_core.hosts.max.api.lib import (
@ -92,6 +93,5 @@ class FbxModelLoader(load.LoaderPlugin):
def remove(self, container):
from pymxs import runtime as rt
node = rt.GetNodeByName(container["instance_node"])
rt.Delete(node)
remove_container_data(node)

View file

@ -11,7 +11,8 @@ from ayon_core.hosts.max.api.lib import maintained_selection
from ayon_core.hosts.max.api.pipeline import (
containerise,
get_previous_loaded_object,
update_custom_attribute_data
update_custom_attribute_data,
remove_container_data
)
from ayon_core.pipeline import get_representation_path, load
@ -84,6 +85,5 @@ class ObjLoader(load.LoaderPlugin):
def remove(self, container):
from pymxs import runtime as rt
node = rt.GetNodeByName(container["instance_node"])
rt.Delete(node)
remove_container_data(node)

View file

@ -13,7 +13,8 @@ from ayon_core.hosts.max.api.lib import maintained_selection
from ayon_core.hosts.max.api.pipeline import (
containerise,
get_previous_loaded_object,
update_custom_attribute_data
update_custom_attribute_data,
remove_container_data
)
from ayon_core.pipeline import get_representation_path, load
@ -113,5 +114,6 @@ class ModelUSDLoader(load.LoaderPlugin):
self.update(container, representation)
def remove(self, container):
from pymxs import runtime as rt
node = rt.GetNodeByName(container["instance_node"])
rt.Delete(node)
remove_container_data(node)

View file

@ -10,7 +10,8 @@ from ayon_core.hosts.max.api import lib, maintained_selection
from ayon_core.hosts.max.api.lib import unique_namespace
from ayon_core.hosts.max.api.pipeline import (
containerise,
get_previous_loaded_object
get_previous_loaded_object,
remove_container_data
)
@ -103,9 +104,9 @@ class AbcLoader(load.LoaderPlugin):
def remove(self, container):
from pymxs import runtime as rt
node = rt.GetNodeByName(container["instance_node"])
rt.Delete(node)
remove_container_data(node)
@staticmethod
def get_container_children(parent, type_name):

View file

@ -4,7 +4,8 @@ from ayon_core.pipeline.load import LoadError
from ayon_core.hosts.max.api.pipeline import (
containerise,
get_previous_loaded_object,
update_custom_attribute_data
update_custom_attribute_data,
remove_container_data
)
from ayon_core.hosts.max.api.lib import (
@ -104,5 +105,6 @@ class OxAbcLoader(load.LoaderPlugin):
self.update(container, representation)
def remove(self, container):
from pymxs import runtime as rt
node = rt.GetNodeByName(container["instance_node"])
rt.Delete(node)
remove_container_data(node)

View file

@ -8,7 +8,8 @@ from ayon_core.hosts.max.api.lib import (
from ayon_core.hosts.max.api.pipeline import (
containerise,
get_previous_loaded_object,
update_custom_attribute_data
update_custom_attribute_data,
remove_container_data
)
from ayon_core.pipeline import get_representation_path, load
@ -63,6 +64,5 @@ class PointCloudLoader(load.LoaderPlugin):
def remove(self, container):
"""remove the container"""
from pymxs import runtime as rt
node = rt.GetNodeByName(container["instance_node"])
rt.Delete(node)
remove_container_data(node)

View file

@ -9,7 +9,8 @@ from ayon_core.pipeline.load import LoadError
from ayon_core.hosts.max.api.pipeline import (
containerise,
update_custom_attribute_data,
get_previous_loaded_object
get_previous_loaded_object,
remove_container_data
)
from ayon_core.hosts.max.api import lib
from ayon_core.hosts.max.api.lib import (
@ -72,6 +73,5 @@ class RedshiftProxyLoader(load.LoaderPlugin):
def remove(self, container):
from pymxs import runtime as rt
node = rt.getNodeByName(container["instance_node"])
rt.delete(node)
node = rt.GetNodeByName(container["instance_node"])
remove_container_data(node)

View file

@ -7,7 +7,8 @@ from ayon_core.hosts.max.api.lib import (
from ayon_core.hosts.max.api.pipeline import (
containerise,
get_previous_loaded_object,
update_custom_attribute_data
update_custom_attribute_data,
remove_container_data
)
from ayon_core.pipeline import get_representation_path, load
@ -59,6 +60,5 @@ class TyCacheLoader(load.LoaderPlugin):
def remove(self, container):
"""remove the container"""
from pymxs import runtime as rt
node = rt.GetNodeByName(container["instance_node"])
rt.Delete(node)
remove_container_data(node)

View file

@ -0,0 +1,88 @@
import pyblish.api
from pymxs import runtime as rt
from ayon_core.pipeline.publish import (
RepairAction,
OptionalPyblishPluginMixin,
PublishValidationError
)
from ayon_core.hosts.max.api.action import SelectInvalidAction
class ValidateCameraAttributes(OptionalPyblishPluginMixin,
pyblish.api.InstancePlugin):
"""Validates Camera has no invalid attribute properties
or values.(For 3dsMax Cameras only)
"""
order = pyblish.api.ValidatorOrder
families = ['camera']
hosts = ['max']
label = 'Validate Camera Attributes'
actions = [SelectInvalidAction, RepairAction]
optional = True
DEFAULTS = ["fov", "nearrange", "farrange",
"nearclip", "farclip"]
CAM_TYPE = ["Freecamera", "Targetcamera",
"Physical"]
@classmethod
def get_invalid(cls, instance):
invalid = []
if rt.units.DisplayType != rt.Name("Generic"):
cls.log.warning(
"Generic Type is not used as a scene unit\n\n"
"sure you tweak the settings with your own values\n\n"
"before validation.")
cameras = instance.data["members"]
project_settings = instance.context.data["project_settings"].get("max")
cam_attr_settings = (
project_settings["publish"]["ValidateCameraAttributes"]
)
for camera in cameras:
if str(rt.ClassOf(camera)) not in cls.CAM_TYPE:
cls.log.debug(
"Skipping camera created from external plugin..")
continue
for attr in cls.DEFAULTS:
default_value = cam_attr_settings.get(attr)
if default_value == float(0):
cls.log.debug(
f"the value of {attr} in setting set to"
" zero. Skipping the check.")
continue
if round(rt.getProperty(camera, attr), 1) != default_value:
cls.log.error(
f"Invalid attribute value for {camera.name}:{attr} "
f"(should be: {default_value}))")
invalid.append(camera)
return invalid
def process(self, instance):
if not self.is_active(instance.data):
self.log.debug("Skipping Validate Camera Attributes.")
return
invalid = self.get_invalid(instance)
if invalid:
raise PublishValidationError(
"Invalid camera attributes found. See log.")
@classmethod
def repair(cls, instance):
invalid_cameras = cls.get_invalid(instance)
project_settings = instance.context.data["project_settings"].get("max")
cam_attr_settings = (
project_settings["publish"]["ValidateCameraAttributes"]
)
for camera in invalid_cameras:
for attr in cls.DEFAULTS:
expected_value = cam_attr_settings.get(attr)
if expected_value == float(0):
cls.log.debug(
f"the value of {attr} in setting set to zero.")
continue
rt.setProperty(camera, attr, expected_value)

View file

@ -720,17 +720,17 @@ def get_created_node_imageio_setting_legacy(nodeclass, creator, subset):
"`{}`: Missing mandatory kwargs `host`, `cls`".format(__file__))
imageio_nodes = get_nuke_imageio_settings()["nodes"]
required_nodes = imageio_nodes["requiredNodes"]
required_nodes = imageio_nodes["required_nodes"]
# HACK: for backward compatibility this needs to be optional
override_nodes = imageio_nodes.get("overrideNodes", [])
override_nodes = imageio_nodes.get("override_nodes", [])
imageio_node = None
for node in required_nodes:
log.info(node)
if (
nodeclass in node["nukeNodeClass"]
and creator in node["plugins"]
nodeclass in node["nuke_node_class"]
and creator in node["plugins"]
):
imageio_node = node
break
@ -741,10 +741,10 @@ def get_created_node_imageio_setting_legacy(nodeclass, creator, subset):
override_imageio_node = None
for onode in override_nodes:
log.info(onode)
if nodeclass not in node["nukeNodeClass"]:
if nodeclass not in onode["nuke_node_class"]:
continue
if creator not in node["plugins"]:
if creator not in onode["plugins"]:
continue
if (
@ -766,26 +766,33 @@ def get_created_node_imageio_setting_legacy(nodeclass, creator, subset):
knob_names = [k["name"] for k in imageio_node["knobs"]]
for oknob in override_imageio_node["knobs"]:
oknob_name = oknob["name"]
oknob_type = oknob["type"]
oknob_value = oknob[oknob_type]
for knob in imageio_node["knobs"]:
# override matching knob name
if oknob["name"] == knob["name"]:
log.debug(
"_ overriding knob: `{}` > `{}`".format(
knob, oknob
))
if not oknob["value"]:
# remove original knob if no value found in oknob
imageio_node["knobs"].remove(knob)
else:
# override knob value with oknob's
knob["value"] = oknob["value"]
knob_name = knob["name"]
# add missing knobs into imageio_node
if oknob["name"] not in knob_names:
if oknob_name not in knob_names:
log.debug(
"_ adding knob: `{}`".format(oknob))
imageio_node["knobs"].append(oknob)
knob_names.append(oknob["name"])
knob_names.append(oknob_name)
continue
# override matching knob name
if oknob_name != knob_name:
continue
knob_type = knob["type"]
log.debug(
"_ overriding knob: `{}` > `{}`".format(knob, oknob)
)
if not oknob_value:
# remove original knob if no value found in oknob
imageio_node["knobs"].remove(knob)
else:
# override knob value with oknob's
knob[knob_type] = oknob_value
log.info("ImageIO node: {}".format(imageio_node))
return imageio_node
@ -795,14 +802,14 @@ def get_imageio_node_setting(node_class, plugin_name, subset):
''' Get preset data for dataflow (fileType, compression, bitDepth)
'''
imageio_nodes = get_nuke_imageio_settings()["nodes"]
required_nodes = imageio_nodes["requiredNodes"]
required_nodes = imageio_nodes["required_nodes"]
imageio_node = None
for node in required_nodes:
log.info(node)
if (
node_class in node["nukeNodeClass"]
and plugin_name in node["plugins"]
node_class in node["nuke_node_class"]
and plugin_name in node["plugins"]
):
imageio_node = node
break
@ -830,14 +837,14 @@ def get_imageio_node_override_setting(
''' Get imageio node overrides from settings
'''
imageio_nodes = get_nuke_imageio_settings()["nodes"]
override_nodes = imageio_nodes["overrideNodes"]
override_nodes = imageio_nodes["override_nodes"]
# find matching override node
override_imageio_node = None
for onode in override_nodes:
log.debug("__ onode: {}".format(onode))
log.debug("__ subset: {}".format(subset))
if node_class not in onode["nukeNodeClass"]:
if node_class not in onode["nuke_node_class"]:
continue
if plugin_name not in onode["plugins"]:
@ -862,26 +869,31 @@ def get_imageio_node_override_setting(
knob_names = [k["name"] for k in knobs_settings]
for oknob in override_imageio_node["knobs"]:
oknob_name = oknob["name"]
oknob_type = oknob["type"]
oknob_value = oknob[oknob_type]
for knob in knobs_settings:
# override matching knob name
if oknob["name"] == knob["name"]:
log.debug(
"_ overriding knob: `{}` > `{}`".format(
knob, oknob
))
if not oknob["value"]:
# remove original knob if no value found in oknob
knobs_settings.remove(knob)
else:
# override knob value with oknob's
knob["value"] = oknob["value"]
# add missing knobs into imageio_node
if oknob["name"] not in knob_names:
log.debug(
"_ adding knob: `{}`".format(oknob))
if oknob_name not in knob_names:
log.debug("_ adding knob: `{}`".format(oknob))
knobs_settings.append(oknob)
knob_names.append(oknob["name"])
knob_names.append(oknob_name)
continue
if oknob_name != knob["name"]:
continue
knob_type = knob["type"]
# override matching knob name
log.debug(
"_ overriding knob: `{}` > `{}`".format(knob, oknob)
)
if not oknob_value:
# remove original knob if no value found in oknob
knobs_settings.remove(knob)
else:
# override knob value with oknob's
knob[knob_type] = oknob_value
return knobs_settings
@ -890,7 +902,7 @@ def get_imageio_input_colorspace(filename):
''' Get input file colorspace based on regex in settings.
'''
imageio_regex_inputs = (
get_nuke_imageio_settings()["regexInputs"]["inputs"])
get_nuke_imageio_settings()["regex_inputs"]["inputs"])
preset_clrsp = None
for regexInput in imageio_regex_inputs:
@ -1177,8 +1189,9 @@ def create_prenodes(
):
last_node = None
for_dependency = {}
for name, node in nodes_setting.items():
for node in nodes_setting:
# get attributes
name = node["name"]
nodeclass = node["nodeclass"]
knobs = node["knobs"]
@ -1240,8 +1253,8 @@ def create_write_node(
name (str): name of node
data (dict): creator write instance data
input (node)[optional]: selected node to connect to
prenodes (dict)[optional]:
nodes to be created before write with dependency
prenodes (Optional[list[dict]]): nodes to be created before write
with dependency
review (bool)[optional]: adding review knob
farm (bool)[optional]: rendering workflow target
kwargs (dict)[optional]: additional key arguments for formatting
@ -1270,7 +1283,7 @@ def create_write_node(
Return:
node (obj): group node with avalon data as Knobs
'''
prenodes = prenodes or {}
prenodes = prenodes or []
# filtering variables
plugin_name = data["creator"]
@ -1285,7 +1298,8 @@ def create_write_node(
for knob in imageio_writes["knobs"]:
if knob["name"] == "file_type":
ext = knob["value"]
knot_type = knob["type"]
ext = knob[knot_type]
data.update({
"imageio_writes": imageio_writes,
@ -1400,12 +1414,17 @@ def create_write_node(
# set tile color
tile_color = next(
iter(
k["value"] for k in imageio_writes["knobs"]
k[k["type"]] for k in imageio_writes["knobs"]
if "tile_color" in k["name"]
), [255, 0, 0, 255]
)
new_tile_color = []
for c in tile_color:
if isinstance(c, float):
c = int(c * 255)
new_tile_color.append(c)
GN["tile_color"].setValue(
color_gui_to_int(tile_color))
color_gui_to_int(new_tile_color))
return GN
@ -1701,42 +1720,32 @@ def set_node_knobs_from_settings(node, knob_settings, **kwargs):
"""
for knob in knob_settings:
log.debug("__ knob: {}".format(pformat(knob)))
knob_type = knob["type"]
knob_name = knob["name"]
if knob_name not in node.knobs():
continue
knob_type = knob["type"]
knob_value = knob[knob_type]
if knob_type == "expression":
knob_expression = knob["expression"]
node[knob_name].setExpression(
knob_expression
)
node[knob_name].setExpression(knob_value)
continue
# first deal with formattable knob settings
if knob_type == "formatable":
template = knob["template"]
to_type = knob["to_type"]
template = knob_value["template"]
to_type = knob_value["to_type"]
try:
_knob_value = template.format(
**kwargs
)
knob_value = template.format(**kwargs)
except KeyError as msg:
raise KeyError(
"Not able to format expression: {}".format(msg))
# convert value to correct type
if to_type == "2d_vector":
knob_value = _knob_value.split(";").split(",")
else:
knob_value = _knob_value
knob_value = knob_value.split(";").split(",")
knob_type = to_type
else:
knob_value = knob["value"]
if not knob_value:
continue
@ -1747,29 +1756,46 @@ def set_node_knobs_from_settings(node, knob_settings, **kwargs):
def convert_knob_value_to_correct_type(knob_type, knob_value):
# first convert string types to string
# just to ditch unicode
if isinstance(knob_value, six.text_type):
knob_value = str(knob_value)
# Convert 'text' to string to avoid unicode
if knob_type == "text":
return str(knob_value)
# set correctly knob types
if knob_type == "bool":
knob_value = bool(knob_value)
elif knob_type == "decimal_number":
knob_value = float(knob_value)
elif knob_type == "number":
knob_value = int(knob_value)
elif knob_type == "text":
knob_value = knob_value
elif knob_type == "color_gui":
knob_value = color_gui_to_int(knob_value)
elif knob_type in ["2d_vector", "3d_vector", "color", "box"]:
knob_value = [float(val_) for val_ in knob_value]
if knob_type == "boolean":
return bool(knob_value)
if knob_type == "decimal_number":
return float(knob_value)
if knob_type == "number":
return int(knob_value)
if knob_type == "color_gui":
new_color = []
for value in knob_value:
if isinstance(value, float):
value = int(value * 255)
new_color.append(value)
return color_gui_to_int(new_color)
if knob_type == "box":
return [
knob_value["x"], knob_value["y"],
knob_value["r"], knob_value["t"]
]
if knob_type == "vector_2d":
return [knob_value["x"], knob_value["y"]]
if knob_type == "vector_3d":
return [knob_value["x"], knob_value["y"], knob_value["z"]]
return knob_value
def color_gui_to_int(color_gui):
# Append alpha channel if not present
if len(color_gui) == 3:
color_gui = list(color_gui) + [255]
hex_value = (
"0x{0:0>2x}{1:0>2x}{2:0>2x}{3:0>2x}").format(*color_gui)
return int(hex_value, 16)
@ -2016,41 +2042,21 @@ class WorkfileSettings(object):
host_name="nuke"
)
workfile_settings = imageio_host["workfile"]
viewer_process_settings = imageio_host["viewer"]["viewerProcess"]
workfile_settings = imageio_host["workfile"]
color_management = workfile_settings["color_management"]
native_ocio_config = workfile_settings["native_ocio_config"]
if not config_data:
# TODO: backward compatibility for old projects - remove later
# perhaps old project overrides is having it set to older version
# with use of `customOCIOConfigPath`
resolved_path = None
if workfile_settings.get("customOCIOConfigPath"):
unresolved_path = workfile_settings["customOCIOConfigPath"]
ocio_paths = unresolved_path[platform.system().lower()]
# no ocio config found and no custom path used
if self._root_node["colorManagement"].value() \
not in color_management:
self._root_node["colorManagement"].setValue(color_management)
for ocio_p in ocio_paths:
resolved_path = str(ocio_p).format(**os.environ)
if not os.path.exists(resolved_path):
continue
if resolved_path:
# set values to root
self._root_node["colorManagement"].setValue("OCIO")
self._root_node["OCIO_config"].setValue("custom")
self._root_node["customOCIOConfigPath"].setValue(
resolved_path)
else:
# no ocio config found and no custom path used
if self._root_node["colorManagement"].value() \
not in str(workfile_settings["colorManagement"]):
self._root_node["colorManagement"].setValue(
str(workfile_settings["colorManagement"]))
# second set ocio version
if self._root_node["OCIO_config"].value() \
not in str(workfile_settings["OCIO_config"]):
self._root_node["OCIO_config"].setValue(
str(workfile_settings["OCIO_config"]))
# second set ocio version
if self._root_node["OCIO_config"].value() \
not in native_ocio_config:
self._root_node["OCIO_config"].setValue(native_ocio_config)
else:
# OCIO config path is defined from prelaunch hook
@ -2063,22 +2069,17 @@ class WorkfileSettings(object):
residual_path
))
# we dont need the key anymore
workfile_settings.pop("customOCIOConfigPath", None)
workfile_settings.pop("colorManagement", None)
workfile_settings.pop("OCIO_config", None)
# get monitor lut from settings respecting Nuke version differences
monitor_lut = workfile_settings.pop("monitorLut", None)
monitor_lut = workfile_settings["thumbnail_space"]
monitor_lut_data = self._get_monitor_settings(
viewer_process_settings, monitor_lut)
# set monitor related knobs luts (MonitorOut, Thumbnails)
for knob, value_ in monitor_lut_data.items():
workfile_settings[knob] = value_
viewer_process_settings, monitor_lut
)
monitor_lut_data["workingSpaceLUT"] = (
workfile_settings["working_space"]
)
# then set the rest
for knob, value_ in workfile_settings.items():
for knob, value_ in monitor_lut_data.items():
# skip unfilled ocio config path
# it will be dict in value
if isinstance(value_, dict):
@ -2360,25 +2361,8 @@ Reopening Nuke should synchronize these paths and resolve any discrepancies.
if not write_node:
return
try:
# write all knobs to node
for knob in nuke_imageio_writes["knobs"]:
value = knob["value"]
if isinstance(value, six.text_type):
value = str(value)
if str(value).startswith("0x"):
value = int(value, 16)
log.debug("knob: {}| value: {}".format(
knob["name"], value
))
write_node[knob["name"]].setValue(value)
except TypeError:
log.warning(
"Legacy workflow didn't work, switching to current")
set_node_knobs_from_settings(
write_node, nuke_imageio_writes["knobs"])
set_node_knobs_from_settings(
write_node, nuke_imageio_writes["knobs"])
def set_reads_colorspace(self, read_clrs_inputs):
""" Setting colorspace to Read nodes
@ -2456,7 +2440,7 @@ Reopening Nuke should synchronize these paths and resolve any discrepancies.
log.error(_error)
log.info("Setting colorspace to read nodes...")
read_clrs_inputs = nuke_colorspace["regexInputs"].get("inputs", [])
read_clrs_inputs = nuke_colorspace["regex_inputs"].get("inputs", [])
if read_clrs_inputs:
self.set_reads_colorspace(read_clrs_inputs)

View file

@ -396,17 +396,25 @@ class NukeWriteCreator(NukeCreator):
# plugin settings
plugin_settings = self.get_creator_settings(project_settings)
temp_rendering_path_template = (
plugin_settings.get("temp_rendering_path_template")
or self.temp_rendering_path_template
)
# TODO remove template key replacements
temp_rendering_path_template = (
temp_rendering_path_template
.replace("{product[name]}", "{subset}")
.replace("{product[type]}", "{family}")
.replace("{task[name]}", "{task}")
.replace("{folder[name]}", "{asset}")
)
# individual attributes
self.instance_attributes = plugin_settings.get(
"instance_attributes") or self.instance_attributes
self.prenodes = plugin_settings["prenodes"]
self.default_variants = plugin_settings.get(
"default_variants") or self.default_variants
self.temp_rendering_path_template = (
plugin_settings.get("temp_rendering_path_template")
or self.temp_rendering_path_template
)
self.temp_rendering_path_template = temp_rendering_path_template
class OpenPypeCreator(LegacyCreator):
@ -1061,7 +1069,7 @@ class AbstractWriteRender(OpenPypeCreator):
icon = "sign-out"
defaults = ["Main", "Mask"]
knobs = []
prenodes = {}
prenodes = []
def __init__(self, *args, **kwargs):
super(AbstractWriteRender, self).__init__(*args, **kwargs)
@ -1167,7 +1175,7 @@ class AbstractWriteRender(OpenPypeCreator):
bool: True if legacy
"""
imageio_nodes = get_nuke_imageio_settings()["nodes"]
node = imageio_nodes["requiredNodes"][0]
node = imageio_nodes["required_nodes"][0]
if "type" not in node["knobs"][0]:
# if type is not yet in project anatomy
return True

View file

@ -53,7 +53,7 @@ class LoadClip(plugin.NukeLoader):
color = "white"
# Loaded from settings
_representations = []
representations_include = []
script_start = int(nuke.root()["first_frame"].value())
@ -82,7 +82,7 @@ class LoadClip(plugin.NukeLoader):
@classmethod
def get_representations(cls):
return cls._representations or cls.representations
return cls.representations_include or cls.representations
def load(self, context, name, namespace, options):
"""Load asset via database
@ -457,7 +457,7 @@ class LoadClip(plugin.NukeLoader):
colorspace = repre_data.get("colorspace")
colorspace = colorspace or version_data.get("colorspace")
# colorspace from `project_settings/nuke/imageio/regexInputs`
# colorspace from `project_settings/nuke/imageio/regex_inputs`
iio_colorspace = get_imageio_input_colorspace(path)
# Set colorspace defined in version data

View file

@ -47,7 +47,7 @@ class LoadImage(load.LoaderPlugin):
color = "white"
# Loaded from settings
_representations = []
representations_include = []
node_name_template = "{class_name}_{ext}"
@ -64,7 +64,7 @@ class LoadImage(load.LoaderPlugin):
@classmethod
def get_representations(cls):
return cls._representations or cls.representations
return cls.representations_include or cls.representations
def load(self, context, name, namespace, options):
self.log.info("__ options: `{}`".format(options))

View file

@ -12,7 +12,7 @@ class CollectInstanceData(pyblish.api.InstancePlugin):
hosts = ["nuke", "nukeassist"]
# presets
sync_workfile_version_on_families = []
sync_workfile_version_on_product_types = []
def process(self, instance):
family = instance.data["family"]
@ -25,7 +25,7 @@ class CollectInstanceData(pyblish.api.InstancePlugin):
pixel_aspect = format_.pixelAspect()
# sync workfile version
if family in self.sync_workfile_version_on_families:
if family in self.sync_workfile_version_on_product_types:
self.log.debug(
"Syncing version with workfile for '{}'".format(
family

View file

@ -50,6 +50,7 @@ class ExtractReviewIntermediates(publish.Extractor):
cls.outputs = current_setting["outputs"]
def process(self, instance):
# TODO 'families' should not be included for filtering of outputs
families = set(instance.data["families"])
# add main family to make sure all families are compared
@ -75,29 +76,33 @@ class ExtractReviewIntermediates(publish.Extractor):
# generate data
with maintained_selection():
generated_repres = []
for o_name, o_data in self.outputs.items():
for o_data in self.outputs:
o_name = o_data["name"]
self.log.debug(
"o_name: {}, o_data: {}".format(o_name, pformat(o_data)))
f_families = o_data["filter"]["families"]
f_product_types = o_data["filter"]["product_types"]
f_task_types = o_data["filter"]["task_types"]
f_subsets = o_data["filter"]["subsets"]
product_names = o_data["filter"]["product_names"]
self.log.debug(
"f_families `{}` > families: {}".format(
f_families, families))
"f_product_types `{}` > families: {}".format(
f_product_types, families))
self.log.debug(
"f_task_types `{}` > task_type: {}".format(
f_task_types, task_type))
self.log.debug(
"f_subsets `{}` > subset: {}".format(
f_subsets, subset))
"product_names `{}` > subset: {}".format(
product_names, subset))
# test if family found in context
# using intersection to make sure all defined
# families are present in combination
if f_families and not families.intersection(f_families):
if (
f_product_types
and not families.intersection(f_product_types)
):
continue
# test task types from filter
@ -105,8 +110,9 @@ class ExtractReviewIntermediates(publish.Extractor):
continue
# test subsets from filter
if f_subsets and not any(
re.search(s, subset) for s in f_subsets):
if product_names and not any(
re.search(p, subset) for p in product_names
):
continue
self.log.debug(
@ -117,7 +123,7 @@ class ExtractReviewIntermediates(publish.Extractor):
# check if settings have more then one preset
# so we dont need to add outputName to representation
# in case there is only one preset
multiple_presets = len(self.outputs.keys()) > 1
multiple_presets = len(self.outputs) > 1
# adding bake presets to instance data for other plugins
if not instance.data.get("bakePresets"):

View file

@ -29,9 +29,15 @@ class ExtractSlateFrame(publish.Extractor):
# Settings values
key_value_mapping = {
"f_submission_note": [True, "{comment}"],
"f_submitting_for": [True, "{intent[value]}"],
"f_vfx_scope_of_work": [False, ""]
"f_submission_note": {
"enabled": True, "template": "{comment}"
},
"f_submitting_for": {
"enabled": True, "template": "{intent[value]}"
},
"f_vfx_scope_of_work": {
"enabled": False, "template": ""
}
}
def process(self, instance):
@ -316,11 +322,11 @@ class ExtractSlateFrame(publish.Extractor):
})
for key, _values in self.key_value_mapping.items():
enabled, template = _values
if not enabled:
if not _values["enabled"]:
self.log.debug("Key \"{}\" is disabled".format(key))
continue
template = _values["template"]
try:
value = template.format(**fill_data)

View file

@ -25,7 +25,7 @@
### How to repair?
Contact your supervisor or fix it in project settings at
'project_settings/nuke/imageio/nodes/requiredNodes' at knobs.
'project_settings/nuke/imageio/nodes/required_nodes' at knobs.
Each '__legacy__' type has to be defined accordingly to its type.
</description>
</error>

View file

@ -30,6 +30,8 @@ class ValidateKnobs(pyblish.api.ContextPlugin):
actions = [RepairContextAction]
optional = True
knobs = "{}"
def process(self, context):
invalid = self.get_invalid(context, compute=True)
if invalid:
@ -61,6 +63,8 @@ class ValidateKnobs(pyblish.api.ContextPlugin):
invalid_knobs = []
for instance in context:
# Load fresh knobs data for each instance
settings_knobs = json.loads(cls.knobs)
# Filter families.
families = [instance.data["family"]]
@ -74,12 +78,12 @@ class ValidateKnobs(pyblish.api.ContextPlugin):
family = family.split(".")[0]
# avoid families not in settings
if family not in cls.knobs:
if family not in settings_knobs:
continue
# get presets of knobs
for preset in cls.knobs[family]:
knobs[preset] = cls.knobs[family][preset]
for preset in settings_knobs[family]:
knobs[preset] = settings_knobs[family][preset]
# Get invalid knobs.
nodes = []

View file

@ -86,7 +86,10 @@ class ValidateNukeWriteNode(
# Collect key values of same type in a list.
values_by_name = defaultdict(list)
for knob_data in correct_data["knobs"]:
values_by_name[knob_data["name"]].append(knob_data["value"])
knob_type = knob_data["type"]
knob_value = knob_data[knob_type]
values_by_name[knob_data["name"]].append(knob_value)
for knob_data in correct_data["knobs"]:
knob_type = knob_data["type"]
@ -97,7 +100,7 @@ class ValidateNukeWriteNode(
raise PublishXmlValidationError(
self, (
"Please update data in settings 'project_settings"
"/nuke/imageio/nodes/requiredNodes'"
"/nuke/imageio/nodes/required_nodes'"
),
key="legacy"
)

View file

@ -127,8 +127,8 @@ class WriteNodeKnobSettingPanel(nukescripts.PythonPanel):
knobs_nodes = []
settings = [
node_settings for node_settings
in get_nuke_imageio_settings()["nodes"]["overrideNodes"]
if node_settings["nukeNodeClass"] == "Write"
in get_nuke_imageio_settings()["nodes"]["override_nodes"]
if node_settings["nuke_node_class"] == "Write"
and node_settings["subsets"]
]
if not settings:

View file

@ -1,6 +1,6 @@
from .module import JobQueueModule
from .addon import JobQueueAddon
__all__ = (
"JobQueueModule",
"JobQueueAddon",
)

View file

@ -41,29 +41,24 @@ import json
import copy
import platform
from ayon_core.addon import click_wrap
from ayon_core.modules import OpenPypeModule
from ayon_core.addon import AYONAddon, click_wrap
from ayon_core.settings import get_system_settings
class JobQueueModule(OpenPypeModule):
class JobQueueAddon(AYONAddon):
name = "job_queue"
def initialize(self, modules_settings):
module_settings = modules_settings.get(self.name) or {}
server_url = module_settings.get("server_url") or ""
def initialize(self, studio_settings):
addon_settings = studio_settings.get(self.name) or {}
server_url = addon_settings.get("server_url") or ""
self._server_url = self.url_conversion(server_url)
jobs_root_mapping = self._roots_mapping_conversion(
module_settings.get("jobs_root")
addon_settings.get("jobs_root")
)
self._jobs_root_mapping = jobs_root_mapping
# Is always enabled
# - the module does nothing until is used
self.enabled = True
@classmethod
def _root_conversion(cls, root_path):
"""Make sure root path does not end with slash."""
@ -127,8 +122,8 @@ class JobQueueModule(OpenPypeModule):
@classmethod
def get_jobs_root_from_settings(cls):
module_settings = get_system_settings()["modules"]
jobs_root_mapping = module_settings.get(cls.name, {}).get("jobs_root")
studio_settings = get_system_settings()
jobs_root_mapping = studio_settings.get(cls.name, {}).get("jobs_root")
converted_mapping = cls._roots_mapping_conversion(jobs_root_mapping)
return converted_mapping[platform.system().lower()]
@ -157,9 +152,9 @@ class JobQueueModule(OpenPypeModule):
@classmethod
def get_server_url_from_settings(cls):
module_settings = get_system_settings()["modules"]
studio_settings = get_system_settings()
return cls.url_conversion(
module_settings
studio_settings
.get(cls.name, {})
.get("server_url")
)
@ -214,7 +209,7 @@ class JobQueueModule(OpenPypeModule):
@click_wrap.group(
JobQueueModule.name,
JobQueueAddon.name,
help="Application job server. Can be used as render farm."
)
def cli_main():
@ -228,7 +223,7 @@ def cli_main():
@click_wrap.option("--port", help="Server port")
@click_wrap.option("--host", help="Server host (ip address)")
def cli_start_server(port, host):
JobQueueModule.start_server(port, host)
JobQueueAddon.start_server(port, host)
@cli_main.command(
@ -241,4 +236,4 @@ def cli_start_server(port, host):
"--server_url",
help="Server url which handle workers and jobs.")
def cli_start_worker(app_name, server_url):
JobQueueModule.start_worker(app_name, server_url)
JobQueueAddon.start_worker(app_name, server_url)

View file

@ -108,7 +108,7 @@ class BaseCreateRoyalRenderJob(pyblish.api.InstancePlugin,
context = instance.context
self._rr_root = instance.data.get("rrPathName")
self._rr_root = instance.data.get("rr_root")
if not self._rr_root:
raise KnownPublishError(
("Missing RoyalRender root. "

View file

@ -1,41 +1,52 @@
# -*- coding: utf-8 -*-
"""
Requires:
instance.context.data["project_settings"]
Provides:
instance.data["rr_root"] (str) - root folder of RoyalRender server
"""
import os.path
import pyblish.api
from ayon_core.modules.royalrender.rr_job import get_rr_platform
class CollectRRPathFromInstance(pyblish.api.InstancePlugin):
"""Collect RR Path from instance."""
"""Collect RR Path from instance.
All RoyalRender server roots are set in `Studio Settings`, each project
uses only key pointing to that part to limit typos inside of Project
settings.
Eventually could be possible to add dropdown with these keys to the
Creators to allow artists to select which RR server they would like to use.
"""
order = pyblish.api.CollectorOrder
label = "Collect Royal Render path name from the Instance"
families = ["render", "prerender", "renderlayer"]
def process(self, instance):
instance.data["rrPathName"] = self._collect_rr_path_name(instance)
instance.data["rr_root"] = self._collect_root(instance)
self.log.info(
"Using '{}' for submission.".format(instance.data["rrPathName"]))
"Using '{}' for submission.".format(instance.data["rr_root"]))
@staticmethod
def _collect_rr_path_name(instance):
def _collect_root(self, instance):
# type: (pyblish.api.Instance) -> str
"""Get Royal Render pat name from render instance."""
# TODO there are no "rrPaths" on instance, if Publisher should expose
# this (eg. artist could select specific server) it must be added
# to publisher
instance_rr_paths = instance.data.get("rrPaths")
if instance_rr_paths is None:
return "default"
"""Get Royal Render pat name from render instance.
If artist should be able to select specific RR server it must be added
to creator. It is not there yet.
"""
rr_settings = instance.context.data["project_settings"]["royalrender"]
rr_paths = rr_settings["rr_paths"]
selected_paths = rr_settings["selected_rr_paths"]
selected_keys = rr_settings["selected_rr_paths"]
rr_servers = {
path_key
for path_key in selected_paths
if path_key in rr_paths
platform = get_rr_platform()
key_to_path = {
item["name"]: item["value"][platform]
for item in rr_paths
}
for instance_rr_path in instance_rr_paths:
if instance_rr_path in rr_servers:
return instance_rr_path
return "default"
for selected_key in selected_keys:
rr_root = key_to_path[selected_key]
if os.path.exists(rr_root):
return rr_root

View file

@ -37,8 +37,8 @@ class SubmitJobsToRoyalRender(pyblish.api.ContextPlugin):
isinstance(job, RRJob)
for job in instance.data.get("rrJobs")):
jobs += instance.data.get("rrJobs")
if instance.data.get("rrPathName"):
instance_rr_path = instance.data["rrPathName"]
if instance.data.get("rr_root"):
instance_rr_path = instance.data["rr_root"]
if jobs:
self._rr_root = instance_rr_path

View file

@ -133,242 +133,9 @@ def convert_system_settings(ayon_settings, default_settings, addon_versions):
# --------- Project settings ---------
def _convert_nuke_knobs(knobs):
new_knobs = []
for knob in knobs:
knob_type = knob["type"]
if knob_type == "boolean":
knob_type = "bool"
if knob_type != "bool":
value = knob[knob_type]
elif knob_type in knob:
value = knob[knob_type]
else:
value = knob["boolean"]
new_knob = {
"type": knob_type,
"name": knob["name"],
}
new_knobs.append(new_knob)
if knob_type == "formatable":
new_knob["template"] = value["template"]
new_knob["to_type"] = value["to_type"]
continue
value_key = "value"
if knob_type == "expression":
value_key = "expression"
elif knob_type == "color_gui":
value = _convert_color(value)
elif knob_type == "vector_2d":
value = [value["x"], value["y"]]
elif knob_type == "vector_3d":
value = [value["x"], value["y"], value["z"]]
elif knob_type == "box":
value = [value["x"], value["y"], value["r"], value["t"]]
new_knob[value_key] = value
return new_knobs
def _convert_nuke_project_settings(ayon_settings, output):
if "nuke" not in ayon_settings:
return
ayon_nuke = ayon_settings["nuke"]
# --- Load ---
ayon_load = ayon_nuke["load"]
ayon_load["LoadClip"]["_representations"] = (
ayon_load["LoadClip"].pop("representations_include")
)
ayon_load["LoadImage"]["_representations"] = (
ayon_load["LoadImage"].pop("representations_include")
)
# --- Create ---
ayon_create = ayon_nuke["create"]
for creator_name in (
"CreateWritePrerender",
"CreateWriteImage",
"CreateWriteRender",
):
create_plugin_settings = ayon_create[creator_name]
create_plugin_settings["temp_rendering_path_template"] = (
create_plugin_settings["temp_rendering_path_template"]
.replace("{product[name]}", "{subset}")
.replace("{product[type]}", "{family}")
.replace("{task[name]}", "{task}")
.replace("{folder[name]}", "{asset}")
)
new_prenodes = {}
for prenode in create_plugin_settings["prenodes"]:
name = prenode.pop("name")
prenode["knobs"] = _convert_nuke_knobs(prenode["knobs"])
new_prenodes[name] = prenode
create_plugin_settings["prenodes"] = new_prenodes
# --- Publish ---
ayon_publish = ayon_nuke["publish"]
slate_mapping = ayon_publish["ExtractSlateFrame"]["key_value_mapping"]
for key in tuple(slate_mapping.keys()):
value = slate_mapping[key]
slate_mapping[key] = [value["enabled"], value["template"]]
ayon_publish["ValidateKnobs"]["knobs"] = json.loads(
ayon_publish["ValidateKnobs"]["knobs"]
)
new_review_data_outputs = {}
outputs_settings = []
# Check deprecated ExtractReviewDataMov
# settings for backwards compatibility
deprecrated_review_settings = ayon_publish["ExtractReviewDataMov"]
current_review_settings = (
ayon_publish.get("ExtractReviewIntermediates")
)
if deprecrated_review_settings["enabled"]:
outputs_settings = deprecrated_review_settings["outputs"]
elif current_review_settings is None:
pass
elif current_review_settings["enabled"]:
outputs_settings = current_review_settings["outputs"]
for item in outputs_settings:
item_filter = item["filter"]
if "product_names" in item_filter:
item_filter["subsets"] = item_filter.pop("product_names")
item_filter["families"] = item_filter.pop("product_types")
reformat_nodes_config = item.get("reformat_nodes_config") or {}
reposition_nodes = reformat_nodes_config.get(
"reposition_nodes") or []
for reposition_node in reposition_nodes:
if "knobs" not in reposition_node:
continue
reposition_node["knobs"] = _convert_nuke_knobs(
reposition_node["knobs"]
)
name = item.pop("name")
new_review_data_outputs[name] = item
if deprecrated_review_settings["enabled"]:
deprecrated_review_settings["outputs"] = new_review_data_outputs
elif current_review_settings["enabled"]:
current_review_settings["outputs"] = new_review_data_outputs
collect_instance_data = ayon_publish["CollectInstanceData"]
if "sync_workfile_version_on_product_types" in collect_instance_data:
collect_instance_data["sync_workfile_version_on_families"] = (
collect_instance_data.pop(
"sync_workfile_version_on_product_types"))
# --- ImageIO ---
# NOTE 'monitorOutLut' is maybe not yet in v3 (ut should be)
ayon_imageio = ayon_nuke["imageio"]
# workfile
imageio_workfile = ayon_imageio["workfile"]
workfile_keys_mapping = (
("color_management", "colorManagement"),
("native_ocio_config", "OCIO_config"),
("working_space", "workingSpaceLUT"),
("thumbnail_space", "monitorLut"),
)
for src, dst in workfile_keys_mapping:
if (
src in imageio_workfile
and dst not in imageio_workfile
):
imageio_workfile[dst] = imageio_workfile.pop(src)
# regex inputs
if "regex_inputs" in ayon_imageio:
ayon_imageio["regexInputs"] = ayon_imageio.pop("regex_inputs")
# nodes
ayon_imageio_nodes = ayon_imageio["nodes"]
if "required_nodes" in ayon_imageio_nodes:
ayon_imageio_nodes["requiredNodes"] = (
ayon_imageio_nodes.pop("required_nodes"))
if "override_nodes" in ayon_imageio_nodes:
ayon_imageio_nodes["overrideNodes"] = (
ayon_imageio_nodes.pop("override_nodes"))
for item in ayon_imageio_nodes["requiredNodes"]:
if "nuke_node_class" in item:
item["nukeNodeClass"] = item.pop("nuke_node_class")
item["knobs"] = _convert_nuke_knobs(item["knobs"])
for item in ayon_imageio_nodes["overrideNodes"]:
if "nuke_node_class" in item:
item["nukeNodeClass"] = item.pop("nuke_node_class")
item["knobs"] = _convert_nuke_knobs(item["knobs"])
output["nuke"] = ayon_nuke
def _convert_hiero_project_settings(ayon_settings, output):
if "hiero" not in ayon_settings:
return
ayon_hiero = ayon_settings["hiero"]
new_gui_filters = {}
for item in ayon_hiero.pop("filters", []):
subvalue = {}
key = item["name"]
for subitem in item["value"]:
subvalue[subitem["name"]] = subitem["value"]
new_gui_filters[key] = subvalue
ayon_hiero["filters"] = new_gui_filters
ayon_load_clip = ayon_hiero["load"]["LoadClip"]
if "product_types" in ayon_load_clip:
ayon_load_clip["families"] = ayon_load_clip.pop("product_types")
ayon_load_clip = ayon_hiero["load"]["LoadClip"]
ayon_load_clip["clip_name_template"] = (
ayon_load_clip["clip_name_template"]
.replace("{folder[name]}", "{asset}")
.replace("{product[name]}", "{subset}")
)
output["hiero"] = ayon_hiero
def _convert_royalrender_project_settings(ayon_settings, output):
if "royalrender" not in ayon_settings:
return
ayon_royalrender = ayon_settings["royalrender"]
rr_paths = ayon_royalrender.get("selected_rr_paths", [])
output["royalrender"] = {
"publish": ayon_royalrender["publish"],
"rr_paths": rr_paths,
}
def convert_project_settings(ayon_settings, default_settings):
default_settings = copy.deepcopy(default_settings)
output = {}
_convert_nuke_project_settings(ayon_settings, output)
_convert_hiero_project_settings(ayon_settings, output)
_convert_royalrender_project_settings(ayon_settings, output)
for key, value in ayon_settings.items():
if key not in output:
output[key] = value

View file

@ -56,6 +56,16 @@
"enabled": false,
"attributes": {}
},
"ValidateCameraAttributes": {
"enabled": true,
"optional": true,
"active": false,
"fov": 45.0,
"nearrange": 0.0,
"farrange": 1000.0,
"nearclip": 1.0,
"farclip": 1000.0
},
"ValidateLoadedPlugin": {
"enabled": false,
"optional": true,

View file

@ -202,19 +202,39 @@ class Controller(QtCore.QObject):
def current_state(self):
return self._current_state
@staticmethod
def _convert_filter_presets(filter_presets):
"""Convert AYON settings presets to dictionary.
Returns:
dict[str, dict[str, Any]]: Filter presets converted to dictionary.
"""
if not isinstance(filter_presets, list):
return filter_presets
return {
filter_preset["name"]: {
item["name"]: item["value"]
for item in filter_preset["value"]
}
for filter_preset in filter_presets
}
def presets_by_hosts(self):
# Get global filters as base
presets = get_current_project_settings()
if not presets:
return {}
result = presets.get("core", {}).get("filters", {})
result = {}
hosts = pyblish.api.registered_hosts()
for host in hosts:
host_presets = presets.get(host, {}).get("filters")
if not host_presets:
continue
host_presets = self._convert_filter_presets(host_presets)
for key, value in host_presets.items():
if value is None:
if key in result:

View file

@ -1,5 +1,6 @@
from .tray import main
__all__ = (
"main",
)

View file

@ -0,0 +1,155 @@
import os
from qtpy import QtWidgets, QtCore, QtGui
from ayon_core import resources, style
from ayon_core.tools.utils import paint_image_with_color
class PixmapLabel(QtWidgets.QLabel):
"""Label resizing image to height of font."""
def __init__(self, pixmap, parent):
super(PixmapLabel, self).__init__(parent)
self._empty_pixmap = QtGui.QPixmap(0, 0)
self._source_pixmap = pixmap
def set_source_pixmap(self, pixmap):
"""Change source image."""
self._source_pixmap = pixmap
self._set_resized_pix()
def _get_pix_size(self):
size = self.fontMetrics().height() * 3
return size, size
def _set_resized_pix(self):
if self._source_pixmap is None:
self.setPixmap(self._empty_pixmap)
return
width, height = self._get_pix_size()
self.setPixmap(
self._source_pixmap.scaled(
width,
height,
QtCore.Qt.KeepAspectRatio,
QtCore.Qt.SmoothTransformation
)
)
def resizeEvent(self, event):
self._set_resized_pix()
super(PixmapLabel, self).resizeEvent(event)
class UpdateDialog(QtWidgets.QDialog):
restart_requested = QtCore.Signal()
ignore_requested = QtCore.Signal()
_min_width = 400
_min_height = 130
def __init__(self, parent=None):
super(UpdateDialog, self).__init__(parent)
icon = QtGui.QIcon(resources.get_ayon_icon_filepath())
self.setWindowIcon(icon)
self.setWindowTitle("AYON update")
self.setWindowFlags(
self.windowFlags()
| QtCore.Qt.WindowStaysOnTopHint
)
self.setMinimumWidth(self._min_width)
self.setMinimumHeight(self._min_height)
top_widget = QtWidgets.QWidget(self)
gift_pixmap = self._get_gift_pixmap()
gift_icon_label = PixmapLabel(gift_pixmap, top_widget)
label_widget = QtWidgets.QLabel(
(
"Your AYON needs to update."
"<br/><br/>Please restart AYON launcher and all running"
" applications as soon as possible."
),
top_widget
)
label_widget.setWordWrap(True)
top_layout = QtWidgets.QHBoxLayout(top_widget)
top_layout.setSpacing(10)
top_layout.addWidget(gift_icon_label, 0, QtCore.Qt.AlignCenter)
top_layout.addWidget(label_widget, 1)
ignore_btn = QtWidgets.QPushButton("Ignore", self)
restart_btn = QtWidgets.QPushButton("Restart && Change", self)
restart_btn.setObjectName("TrayRestartButton")
btns_layout = QtWidgets.QHBoxLayout()
btns_layout.addStretch(1)
btns_layout.addWidget(ignore_btn, 0)
btns_layout.addWidget(restart_btn, 0)
layout = QtWidgets.QVBoxLayout(self)
layout.addWidget(top_widget, 0)
layout.addStretch(1)
layout.addLayout(btns_layout, 0)
ignore_btn.clicked.connect(self._on_ignore)
restart_btn.clicked.connect(self._on_reset)
self._label_widget = label_widget
self._gift_icon_label = gift_icon_label
self._ignore_btn = ignore_btn
self._restart_btn = restart_btn
self._restart_accepted = False
self._current_is_higher = False
self._close_silently = False
self.setStyleSheet(style.load_stylesheet())
def close_silently(self):
self._close_silently = True
self.close()
def showEvent(self, event):
super(UpdateDialog, self).showEvent(event)
self._close_silently = False
self._restart_accepted = False
def closeEvent(self, event):
super(UpdateDialog, self).closeEvent(event)
if self._restart_accepted or self._current_is_higher:
return
if self._close_silently:
return
# Trigger ignore requested only if restart was not clicked and current
# version is lower
self.ignore_requested.emit()
def _on_ignore(self):
self.reject()
def _on_reset(self):
self._restart_accepted = True
self.restart_requested.emit()
self.accept()
def _get_gift_pixmap(self):
image_path = os.path.join(
os.path.dirname(os.path.abspath(__file__)),
"images",
"gifts.png"
)
src_image = QtGui.QImage(image_path)
color_value = style.get_objected_colors("font")
return paint_image_with_color(
src_image,
color_value.get_qcolor()
)

View file

@ -1,10 +1,11 @@
import collections
import os
import sys
import collections
import atexit
import platform
import ayon_api
from qtpy import QtCore, QtGui, QtWidgets
from ayon_core import resources, style
@ -12,57 +13,25 @@ from ayon_core.lib import (
Logger,
get_ayon_launcher_args,
run_detached_process,
is_dev_mode_enabled,
is_staging_enabled,
is_running_from_build,
)
from ayon_core.lib import is_running_from_build
from ayon_core.settings import get_ayon_settings
from ayon_core.addon import (
ITrayAction,
ITrayService,
TrayAddonsManager,
)
from ayon_core.settings import get_system_settings
from ayon_core.tools.utils import (
WrappedCallbackItem,
get_ayon_qt_app,
)
from .info_widget import InfoWidget
# TODO PixmapLabel should be moved to 'utils' in other future PR so should be
# imported from there
class PixmapLabel(QtWidgets.QLabel):
"""Label resizing image to height of font."""
def __init__(self, pixmap, parent):
super(PixmapLabel, self).__init__(parent)
self._empty_pixmap = QtGui.QPixmap(0, 0)
self._source_pixmap = pixmap
def set_source_pixmap(self, pixmap):
"""Change source image."""
self._source_pixmap = pixmap
self._set_resized_pix()
def _get_pix_size(self):
size = self.fontMetrics().height() * 3
return size, size
def _set_resized_pix(self):
if self._source_pixmap is None:
self.setPixmap(self._empty_pixmap)
return
width, height = self._get_pix_size()
self.setPixmap(
self._source_pixmap.scaled(
width,
height,
QtCore.Qt.KeepAspectRatio,
QtCore.Qt.SmoothTransformation
)
)
def resizeEvent(self, event):
self._set_resized_pix()
super(PixmapLabel, self).resizeEvent(event)
from .dialogs import (
UpdateDialog,
)
class TrayManager:
@ -78,22 +47,26 @@ class TrayManager:
self.log = Logger.get_logger(self.__class__.__name__)
system_settings = get_system_settings()
studio_settings = get_ayon_settings()
version_check_interval = system_settings["general"].get(
"version_check_interval"
update_check_interval = studio_settings["core"].get(
"update_check_interval"
)
if version_check_interval is None:
version_check_interval = 5
self._version_check_interval = version_check_interval * 60 * 1000
if update_check_interval is None:
update_check_interval = 5
self._update_check_interval = update_check_interval * 60 * 1000
self._addons_manager = TrayAddonsManager()
self.errors = []
self.main_thread_timer = None
self._update_check_timer = None
self._outdated_dialog = None
self._main_thread_timer = None
self._main_thread_callbacks = collections.deque()
self._execution_in_progress = None
self._closing = False
@property
def doubleclick_callback(self):
@ -107,29 +80,25 @@ class TrayManager:
if callback:
self.execute_in_main_thread(callback)
def _restart_and_install(self):
self.restart(use_expected_version=True)
def show_tray_message(self, title, message, icon=None, msecs=None):
"""Show tray message.
def execute_in_main_thread(self, callback, *args, **kwargs):
if isinstance(callback, WrappedCallbackItem):
item = callback
else:
item = WrappedCallbackItem(callback, *args, **kwargs)
Args:
title (str): Title of message.
message (str): Content of message.
icon (QSystemTrayIcon.MessageIcon): Message's icon. Default is
Information icon, may differ by Qt version.
msecs (int): Duration of message visibility in milliseconds.
Default is 10000 msecs, may differ by Qt version.
"""
args = [title, message]
kwargs = {}
if icon:
kwargs["icon"] = icon
if msecs:
kwargs["msecs"] = msecs
self._main_thread_callbacks.append(item)
return item
def _main_thread_execution(self):
if self._execution_in_progress:
return
self._execution_in_progress = True
for _ in range(len(self._main_thread_callbacks)):
if self._main_thread_callbacks:
item = self._main_thread_callbacks.popleft()
item.execute()
self._execution_in_progress = False
self.tray_widget.showMessage(*args, **kwargs)
def initialize_addons(self):
"""Add addons to tray."""
@ -140,7 +109,9 @@ class TrayManager:
self.tray_widget.menu.addMenu(admin_submenu)
# Add services if they are
services_submenu = ITrayService.services_submenu(self.tray_widget.menu)
services_submenu = ITrayService.services_submenu(
self.tray_widget.menu
)
self.tray_widget.menu.addMenu(services_submenu)
# Add separator
@ -165,39 +136,172 @@ class TrayManager:
main_thread_timer.timeout.connect(self._main_thread_execution)
main_thread_timer.start()
self.main_thread_timer = main_thread_timer
self._main_thread_timer = main_thread_timer
update_check_timer = QtCore.QTimer()
if self._update_check_interval > 0:
update_check_timer.timeout.connect(self._on_update_check_timer)
update_check_timer.setInterval(self._update_check_interval)
update_check_timer.start()
self._update_check_timer = update_check_timer
self.execute_in_main_thread(self._startup_validations)
def restart(self):
"""Restart Tray tool.
First creates new process with same argument and close current tray.
"""
self._closing = True
args = get_ayon_launcher_args()
# Create a copy of sys.argv
additional_args = list(sys.argv)
# Remove first argument from 'sys.argv'
# - when running from code the first argument is 'start.py'
# - when running from build the first argument is executable
additional_args.pop(0)
additional_args = [
arg
for arg in additional_args
if arg not in {"--use-staging", "--use-dev"}
]
if is_dev_mode_enabled():
additional_args.append("--use-dev")
elif is_staging_enabled():
additional_args.append("--use-staging")
args.extend(additional_args)
envs = dict(os.environ.items())
for key in {
"AYON_BUNDLE_NAME",
}:
envs.pop(key, None)
run_detached_process(args, env=envs)
self.exit()
def exit(self):
self._closing = True
self.tray_widget.exit()
def on_exit(self):
self._addons_manager.on_exit()
def execute_in_main_thread(self, callback, *args, **kwargs):
if isinstance(callback, WrappedCallbackItem):
item = callback
else:
item = WrappedCallbackItem(callback, *args, **kwargs)
self._main_thread_callbacks.append(item)
return item
def _on_update_check_timer(self):
try:
bundles = ayon_api.get_bundles()
user = ayon_api.get_user()
# This is a workaround for bug in ayon-python-api
if user.get("code") == 401:
raise Exception("Unauthorized")
except Exception:
self._revalidate_ayon_auth()
if self._closing:
return
try:
bundles = ayon_api.get_bundles()
except Exception:
return
if is_dev_mode_enabled():
return
bundle_type = (
"stagingBundle"
if is_staging_enabled()
else "productionBundle"
)
expected_bundle = bundles.get(bundle_type)
current_bundle = os.environ.get("AYON_BUNDLE_NAME")
is_expected = expected_bundle == current_bundle
if is_expected or expected_bundle is None:
self._restart_action.setVisible(False)
if (
self._outdated_dialog is not None
and self._outdated_dialog.isVisible()
):
self._outdated_dialog.close_silently()
return
self._restart_action.setVisible(True)
if self._outdated_dialog is None:
self._outdated_dialog = UpdateDialog()
self._outdated_dialog.restart_requested.connect(
self._restart_and_install
)
self._outdated_dialog.ignore_requested.connect(
self._outdated_bundle_ignored
)
self._outdated_dialog.show()
self._outdated_dialog.raise_()
self._outdated_dialog.activateWindow()
def _revalidate_ayon_auth(self):
result = self._show_ayon_login(restart_on_token_change=False)
if self._closing:
return False
if not result.new_token:
self.exit()
return False
return True
def _restart_and_install(self):
self.restart()
def _outdated_bundle_ignored(self):
self.show_tray_message(
"AYON update ignored",
(
"Please restart AYON launcher as soon as possible"
" to propagate updates."
)
)
def _main_thread_execution(self):
if self._execution_in_progress:
return
self._execution_in_progress = True
for _ in range(len(self._main_thread_callbacks)):
if self._main_thread_callbacks:
item = self._main_thread_callbacks.popleft()
try:
item.execute()
except BaseException:
self.log.erorr(
"Main thread execution failed", exc_info=True
)
self._execution_in_progress = False
def _startup_validations(self):
"""Run possible startup validations."""
pass
def show_tray_message(self, title, message, icon=None, msecs=None):
"""Show tray message.
Args:
title (str): Title of message.
message (str): Content of message.
icon (QSystemTrayIcon.MessageIcon): Message's icon. Default is
Information icon, may differ by Qt version.
msecs (int): Duration of message visibility in milliseconds.
Default is 10000 msecs, may differ by Qt version.
"""
args = [title, message]
kwargs = {}
if icon:
kwargs["icon"] = icon
if msecs:
kwargs["msecs"] = msecs
self.tray_widget.showMessage(*args, **kwargs)
# Trigger bundle validation on start
self._update_check_timer.timeout.emit()
def _add_version_item(self):
login_action = QtWidgets.QAction("Login", self.tray_widget)
login_action.triggered.connect(self._on_ayon_login)
self.tray_widget.menu.addAction(login_action)
version_string = os.getenv("AYON_VERSION", "AYON Info")
version_action = QtWidgets.QAction(version_string, self.tray_widget)
@ -216,16 +320,24 @@ class TrayManager:
self._restart_action = restart_action
def _on_ayon_login(self):
self.execute_in_main_thread(self._show_ayon_login)
self.execute_in_main_thread(
self._show_ayon_login,
restart_on_token_change=True
)
def _show_ayon_login(self):
def _show_ayon_login(self, restart_on_token_change):
from ayon_common.connection.credentials import change_user_ui
result = change_user_ui()
if result.shutdown:
self.exit()
return result
elif result.restart or result.token_changed:
restart = result.restart
if restart_on_token_change and result.token_changed:
restart = True
if restart:
# Remove environment variables from current connection
# - keep develop, staging, headless values
for key in {
@ -235,23 +347,13 @@ class TrayManager:
}:
os.environ.pop(key, None)
self.restart()
return result
def _on_restart_action(self):
self.restart(use_expected_version=True)
self.restart()
def restart(self, use_expected_version=False, reset_version=False):
"""Restart Tray tool.
First creates new process with same argument and close current tray.
Args:
use_expected_version(bool): OpenPype version is set to expected
version.
reset_version(bool): OpenPype version is cleaned up so igniters
logic will decide which version will be used.
"""
def _restart_ayon(self):
args = get_ayon_launcher_args()
envs = dict(os.environ.items())
# Create a copy of sys.argv
additional_args = list(sys.argv)
@ -259,35 +361,28 @@ class TrayManager:
# - when running from code the first argument is 'start.py'
# - when running from build the first argument is executable
additional_args.pop(0)
additional_args = [
arg
for arg in additional_args
if arg not in {"--use-staging", "--use-dev"}
]
cleanup_additional_args = False
if use_expected_version:
cleanup_additional_args = True
reset_version = True
# Pop OPENPYPE_VERSION
if reset_version:
cleanup_additional_args = True
envs.pop("OPENPYPE_VERSION", None)
if cleanup_additional_args:
_additional_args = []
for arg in additional_args:
if arg == "--use-staging" or arg.startswith("--use-version"):
continue
_additional_args.append(arg)
additional_args = _additional_args
if is_dev_mode_enabled():
additional_args.append("--use-dev")
elif is_staging_enabled():
additional_args.append("--use-staging")
args.extend(additional_args)
envs = dict(os.environ.items())
for key in {
"AYON_BUNDLE_NAME",
}:
envs.pop(key, None)
run_detached_process(args, env=envs)
self.exit()
def exit(self):
self.tray_widget.exit()
def on_exit(self):
self._addons_manager.on_exit()
def _on_version_action(self):
if self._info_widget is None:
self._info_widget = InfoWidget()

View file

@ -279,7 +279,8 @@ def create_server_package(
def main(
output_dir: Optional[str]=None,
skip_zip: bool=False,
keep_sources: bool=False
keep_sources: bool=False,
clear_output_dir: bool=False
):
log = logging.getLogger("create_package")
log.info("Start creating package")
@ -292,7 +293,8 @@ def main(
new_created_version_dir = os.path.join(
output_dir, ADDON_NAME, ADDON_VERSION
)
if os.path.isdir(new_created_version_dir):
if os.path.isdir(new_created_version_dir) and clear_output_dir:
log.info(f"Purging {new_created_version_dir}")
shutil.rmtree(output_dir)
@ -339,6 +341,15 @@ if __name__ == "__main__":
"Keep folder structure when server package is created."
)
)
parser.add_argument(
"-c", "--clear-output-dir",
dest="clear_output_dir",
action="store_true",
help=(
"Clear output directory before package creation."
)
)
parser.add_argument(
"-o", "--output",
dest="output_dir",
@ -350,4 +361,9 @@ if __name__ == "__main__":
)
args = parser.parse_args(sys.argv[1:])
main(args.output_dir, args.skip_zip, args.keep_sources)
main(
args.output_dir,
args.skip_zip,
args.keep_sources,
args.clear_output_dir
)

View file

@ -121,6 +121,11 @@ class CoreSettings(BaseSettingsModel):
widget="textarea",
scope=["studio"],
)
update_check_interval: int = SettingsField(
5,
title="Update check interval (minutes)",
ge=0
)
disk_mapping: DiskMappingModel = SettingsField(
default_factory=DiskMappingModel,
title="Disk mapping",

View file

@ -287,6 +287,13 @@ class ProcessSubmittedJobOnFarmModel(BaseSettingsModel):
default_factory=list,
title="Skip integration of representation with ext"
)
families_transfer: list[str] = SettingsField(
default_factory=list,
title=(
"List of family names to transfer\n"
"to generated instances (AOVs for example)."
)
)
aov_filter: list[AOVFilterSubmodel] = SettingsField(
default_factory=list,
title="Reviewable products filter",
@ -470,6 +477,7 @@ DEFAULT_DEADLINE_PLUGINS_SETTINGS = {
"deadline_priority": 50,
"publishing_script": "",
"skip_integration_repre_list": [],
"families_transfer": ["render3d", "render2d", "ftrack", "slate"],
"aov_filter": [
{
"name": "maya",

View file

@ -27,6 +27,17 @@ class ValidateAttributesModel(BaseSettingsModel):
return value
class ValidateCameraAttributesModel(BaseSettingsModel):
enabled: bool = SettingsField(title="Enabled")
optional: bool = SettingsField(title="Optional")
active: bool = SettingsField(title="Active")
fov: float = SettingsField(0.0, title="Focal Length")
nearrange: float = SettingsField(0.0, title="Near Range")
farrange: float = SettingsField(0.0, title="Far Range")
nearclip: float = SettingsField(0.0, title="Near Clip")
farclip: float = SettingsField(0.0, title="Far Clip")
class FamilyMappingItemModel(BaseSettingsModel):
families: list[str] = SettingsField(
default_factory=list,
@ -63,7 +74,14 @@ class PublishersModel(BaseSettingsModel):
default_factory=ValidateAttributesModel,
title="Validate Attributes"
)
ValidateCameraAttributes: ValidateCameraAttributesModel = SettingsField(
default_factory=ValidateCameraAttributesModel,
title="Validate Camera Attributes",
description=(
"If the value of the camera attributes set to 0, "
"the system automatically skips checking it"
)
)
ValidateLoadedPlugin: ValidateLoadedPluginModel = SettingsField(
default_factory=ValidateLoadedPluginModel,
title="Validate Loaded Plugin"
@ -101,6 +119,16 @@ DEFAULT_PUBLISH_SETTINGS = {
"enabled": False,
"attributes": "{}"
},
"ValidateCameraAttributes": {
"enabled": True,
"optional": True,
"active": False,
"fov": 45.0,
"nearrange": 0.0,
"farrange": 1000.0,
"nearclip": 1.0,
"farclip": 1000.0
},
"ValidateLoadedPlugin": {
"enabled": False,
"optional": True,