mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
Merge branch 'develop' into enhancement/AY-1052_Context-Validation-Repair-Action-enhancements
This commit is contained in:
commit
b4e93cade3
20 changed files with 413 additions and 141 deletions
|
|
@ -137,7 +137,7 @@ class CreateShotClip(phiero.Creator):
|
|||
"value": ["<track_name>", "main", "bg", "fg", "bg",
|
||||
"animatic"],
|
||||
"type": "QComboBox",
|
||||
"label": "pRODUCT Name",
|
||||
"label": "Product Name",
|
||||
"target": "ui",
|
||||
"toolTip": "chose product name pattern, if <track_name> is selected, name of track layer will be used", # noqa
|
||||
"order": 0},
|
||||
|
|
@ -159,7 +159,7 @@ class CreateShotClip(phiero.Creator):
|
|||
"type": "QCheckBox",
|
||||
"label": "Include audio",
|
||||
"target": "tag",
|
||||
"toolTip": "Process productS with corresponding audio", # noqa
|
||||
"toolTip": "Process products with corresponding audio", # noqa
|
||||
"order": 3},
|
||||
"sourceResolution": {
|
||||
"value": False,
|
||||
|
|
|
|||
|
|
@ -4,12 +4,12 @@ import pyblish.api
|
|||
from ayon_core.pipeline import publish
|
||||
|
||||
|
||||
class ExtractThumnail(publish.Extractor):
|
||||
class ExtractThumbnail(publish.Extractor):
|
||||
"""
|
||||
Extractor for track item's tumnails
|
||||
Extractor for track item's tumbnails
|
||||
"""
|
||||
|
||||
label = "Extract Thumnail"
|
||||
label = "Extract Thumbnail"
|
||||
order = pyblish.api.ExtractorOrder
|
||||
families = ["plate", "take"]
|
||||
hosts = ["hiero"]
|
||||
|
|
@ -48,7 +48,7 @@ class ExtractThumnail(publish.Extractor):
|
|||
self.log.debug(
|
||||
"__ thumb_path: `{}`, frame: `{}`".format(thumbnail, thumb_frame))
|
||||
|
||||
self.log.info("Thumnail was generated to: {}".format(thumb_path))
|
||||
self.log.info("Thumbnail was generated to: {}".format(thumb_path))
|
||||
thumb_representation = {
|
||||
'files': thumb_file,
|
||||
'stagingDir': staging_dir,
|
||||
|
|
|
|||
|
|
@ -1519,24 +1519,30 @@ def extract_alembic(file,
|
|||
|
||||
|
||||
# region ID
|
||||
def get_id_required_nodes(referenced_nodes=False, nodes=None):
|
||||
"""Filter out any node which are locked (reference) or readOnly
|
||||
def get_id_required_nodes(referenced_nodes=False,
|
||||
nodes=None,
|
||||
existing_ids=True):
|
||||
"""Return nodes that should receive a `cbId` attribute.
|
||||
|
||||
This includes only mesh and curve nodes, parent transforms of the shape
|
||||
nodes, file texture nodes and object sets (including shading engines).
|
||||
|
||||
This filters out any node which is locked, referenced, read-only,
|
||||
intermediate object.
|
||||
|
||||
Args:
|
||||
referenced_nodes (bool): set True to filter out reference nodes
|
||||
referenced_nodes (bool): set True to include referenced nodes
|
||||
nodes (list, Optional): nodes to consider
|
||||
existing_ids (bool): set True to include nodes with `cbId` attribute
|
||||
|
||||
Returns:
|
||||
nodes (set): list of filtered nodes
|
||||
"""
|
||||
|
||||
lookup = None
|
||||
if nodes is None:
|
||||
# Consider all nodes
|
||||
nodes = cmds.ls()
|
||||
else:
|
||||
# Build a lookup for the only allowed nodes in output based
|
||||
# on `nodes` input of the function (+ ensure long names)
|
||||
lookup = set(cmds.ls(nodes, long=True))
|
||||
if nodes is not None and not nodes:
|
||||
# User supplied an empty `nodes` list to check so all we can
|
||||
# do is return the empty result
|
||||
return set()
|
||||
|
||||
def _node_type_exists(node_type):
|
||||
try:
|
||||
|
|
@ -1545,63 +1551,142 @@ def get_id_required_nodes(referenced_nodes=False, nodes=None):
|
|||
except RuntimeError:
|
||||
return False
|
||||
|
||||
def iterate(maya_iterator):
|
||||
while not maya_iterator.isDone():
|
||||
yield maya_iterator.thisNode()
|
||||
maya_iterator.next()
|
||||
|
||||
# `readOnly` flag is obsolete as of Maya 2016 therefore we explicitly
|
||||
# remove default nodes and reference nodes
|
||||
camera_shapes = ["frontShape", "sideShape", "topShape", "perspShape"]
|
||||
default_camera_shapes = {
|
||||
"frontShape", "sideShape", "topShape", "perspShape"
|
||||
}
|
||||
|
||||
ignore = set()
|
||||
if not referenced_nodes:
|
||||
ignore |= set(cmds.ls(long=True, referencedNodes=True))
|
||||
|
||||
# list all defaultNodes to filter out from the rest
|
||||
ignore |= set(cmds.ls(long=True, defaultNodes=True))
|
||||
ignore |= set(cmds.ls(camera_shapes, long=True))
|
||||
|
||||
# Remove Turtle from the result of `cmds.ls` if Turtle is loaded
|
||||
# TODO: This should be a less specific check for a single plug-in.
|
||||
if _node_type_exists("ilrBakeLayer"):
|
||||
ignore |= set(cmds.ls(type="ilrBakeLayer", long=True))
|
||||
|
||||
# Establish set of nodes types to include
|
||||
types = ["objectSet", "file", "mesh", "nurbsCurve", "nurbsSurface"]
|
||||
# The filtered types do not include transforms because we only want the
|
||||
# parent transforms that have a child shape that we filtered to, so we
|
||||
# include the parents here
|
||||
types = ["mesh", "nurbsCurve", "nurbsSurface", "file", "objectSet"]
|
||||
|
||||
# Check if plugin nodes are available for Maya by checking if the plugin
|
||||
# is loaded
|
||||
if cmds.pluginInfo("pgYetiMaya", query=True, loaded=True):
|
||||
types.append("pgYetiMaya")
|
||||
|
||||
# We *always* ignore intermediate shapes, so we filter them out directly
|
||||
nodes = cmds.ls(nodes, type=types, long=True, noIntermediate=True)
|
||||
iterator_type = OpenMaya.MIteratorType()
|
||||
# This tries to be closest matching API equivalents of `types` variable
|
||||
iterator_type.filterList = [
|
||||
OpenMaya.MFn.kMesh, # mesh
|
||||
OpenMaya.MFn.kNurbsSurface, # nurbsSurface
|
||||
OpenMaya.MFn.kNurbsCurve, # nurbsCurve
|
||||
OpenMaya.MFn.kFileTexture, # file
|
||||
OpenMaya.MFn.kSet, # objectSet
|
||||
OpenMaya.MFn.kPluginShape # pgYetiMaya
|
||||
]
|
||||
it = OpenMaya.MItDependencyNodes(iterator_type)
|
||||
|
||||
# The items which need to pass the id to their parent
|
||||
# Add the collected transform to the nodes
|
||||
dag = cmds.ls(nodes, type="dagNode", long=True) # query only dag nodes
|
||||
transforms = cmds.listRelatives(dag,
|
||||
parent=True,
|
||||
fullPath=True) or []
|
||||
fn_dep = OpenMaya.MFnDependencyNode()
|
||||
fn_dag = OpenMaya.MFnDagNode()
|
||||
result = set()
|
||||
|
||||
nodes = set(nodes)
|
||||
nodes |= set(transforms)
|
||||
def _should_include_parents(obj):
|
||||
"""Whether to include parents of obj in output"""
|
||||
if not obj.hasFn(OpenMaya.MFn.kShape):
|
||||
return False
|
||||
|
||||
nodes -= ignore # Remove the ignored nodes
|
||||
if not nodes:
|
||||
return nodes
|
||||
fn_dag.setObject(obj)
|
||||
if fn_dag.isIntermediateObject:
|
||||
return False
|
||||
|
||||
# Ensure only nodes from the input `nodes` are returned when a
|
||||
# filter was applied on function call because we also iterated
|
||||
# to parents and alike
|
||||
if lookup is not None:
|
||||
nodes &= lookup
|
||||
# Skip default cameras
|
||||
if (
|
||||
obj.hasFn(OpenMaya.MFn.kCamera) and
|
||||
fn_dag.name() in default_camera_shapes
|
||||
):
|
||||
return False
|
||||
|
||||
# Avoid locked nodes
|
||||
nodes_list = list(nodes)
|
||||
locked = cmds.lockNode(nodes_list, query=True, lock=True)
|
||||
for node, lock in zip(nodes_list, locked):
|
||||
if lock:
|
||||
log.warning("Skipping locked node: %s" % node)
|
||||
nodes.remove(node)
|
||||
return True
|
||||
|
||||
return nodes
|
||||
def _add_to_result_if_valid(obj):
|
||||
"""Add to `result` if the object should be included"""
|
||||
fn_dep.setObject(obj)
|
||||
if not existing_ids and fn_dep.hasAttribute("cbId"):
|
||||
return
|
||||
|
||||
if not referenced_nodes and fn_dep.isFromReferencedFile:
|
||||
return
|
||||
|
||||
if fn_dep.isDefaultNode:
|
||||
return
|
||||
|
||||
if fn_dep.isLocked:
|
||||
return
|
||||
|
||||
# Skip default cameras
|
||||
if (
|
||||
obj.hasFn(OpenMaya.MFn.kCamera) and
|
||||
fn_dep.name() in default_camera_shapes
|
||||
):
|
||||
return
|
||||
|
||||
if obj.hasFn(OpenMaya.MFn.kDagNode):
|
||||
# DAG nodes
|
||||
fn_dag.setObject(obj)
|
||||
|
||||
# Skip intermediate objects
|
||||
if fn_dag.isIntermediateObject:
|
||||
return
|
||||
|
||||
# DAG nodes can be instanced and thus may have multiple paths.
|
||||
# We need to identify each path
|
||||
paths = OpenMaya.MDagPath.getAllPathsTo(obj)
|
||||
for dag in paths:
|
||||
path = dag.fullPathName()
|
||||
result.add(path)
|
||||
else:
|
||||
# Dependency node
|
||||
path = fn_dep.name()
|
||||
result.add(path)
|
||||
|
||||
for obj in iterate(it):
|
||||
# For any non-intermediate shape node always include the parent
|
||||
# even if we exclude the shape itself (e.g. when locked, default)
|
||||
if _should_include_parents(obj):
|
||||
fn_dag.setObject(obj)
|
||||
parents = [
|
||||
fn_dag.parent(index) for index in range(fn_dag.parentCount())
|
||||
]
|
||||
for parent_obj in parents:
|
||||
_add_to_result_if_valid(parent_obj)
|
||||
|
||||
_add_to_result_if_valid(obj)
|
||||
|
||||
if not result:
|
||||
return result
|
||||
|
||||
# Exclude some additional types
|
||||
exclude_types = []
|
||||
if _node_type_exists("ilrBakeLayer"):
|
||||
# Remove Turtle from the result if Turtle is loaded
|
||||
exclude_types.append("ilrBakeLayer")
|
||||
|
||||
if exclude_types:
|
||||
exclude_nodes = set(cmds.ls(nodes, long=True, type=exclude_types))
|
||||
if exclude_nodes:
|
||||
result -= exclude_nodes
|
||||
|
||||
# Filter to explicit input nodes if provided
|
||||
if nodes is not None:
|
||||
# The amount of input nodes to filter to can be large and querying
|
||||
# many nodes can be slow in Maya. As such we want to try and reduce
|
||||
# it as much as possible, so we include the type filter to try and
|
||||
# reduce the result of `maya.cmds.ls` here.
|
||||
nodes = set(cmds.ls(nodes, long=True, type=types + ["dagNode"]))
|
||||
if nodes:
|
||||
result &= nodes
|
||||
else:
|
||||
return set()
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def get_id(node):
|
||||
|
|
|
|||
|
|
@ -580,7 +580,8 @@ def on_save():
|
|||
_remove_workfile_lock()
|
||||
|
||||
# Generate ids of the current context on nodes in the scene
|
||||
nodes = lib.get_id_required_nodes(referenced_nodes=False)
|
||||
nodes = lib.get_id_required_nodes(referenced_nodes=False,
|
||||
existing_ids=False)
|
||||
for node, new_id in lib.generate_ids(nodes):
|
||||
lib.set_id(node, new_id, overwrite=False)
|
||||
|
||||
|
|
@ -653,10 +654,6 @@ def on_task_changed():
|
|||
"Can't set project for new context because path does not exist: {}"
|
||||
).format(workdir))
|
||||
|
||||
with lib.suspended_refresh():
|
||||
lib.set_context_settings()
|
||||
lib.update_content_on_context_change()
|
||||
|
||||
global _about_to_save
|
||||
if not lib.IS_HEADLESS and _about_to_save:
|
||||
# Let's prompt the user to update the context settings or not
|
||||
|
|
|
|||
|
|
@ -286,7 +286,7 @@ class MayaPlaceholderLoadPlugin(PlaceholderPlugin, PlaceholderLoadMixin):
|
|||
if not container:
|
||||
return
|
||||
|
||||
roots = cmds.sets(container, q=True)
|
||||
roots = cmds.sets(container, q=True) or []
|
||||
ref_node = None
|
||||
try:
|
||||
ref_node = get_reference_node(roots)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import json
|
||||
|
||||
from maya import cmds
|
||||
|
||||
import pyblish.api
|
||||
|
|
@ -11,18 +9,24 @@ class CollectFileDependencies(pyblish.api.ContextPlugin):
|
|||
label = "Collect File Dependencies"
|
||||
order = pyblish.api.CollectorOrder - 0.49
|
||||
hosts = ["maya"]
|
||||
families = ["renderlayer"]
|
||||
|
||||
@classmethod
|
||||
def apply_settings(cls, project_settings, system_settings):
|
||||
# Disable plug-in if not used for deadline submission anyway
|
||||
settings = project_settings["deadline"]["publish"]["MayaSubmitDeadline"] # noqa
|
||||
cls.enabled = settings.get("asset_dependencies", True)
|
||||
|
||||
def process(self, context):
|
||||
dependencies = []
|
||||
dependencies = set()
|
||||
for node in cmds.ls(type="file"):
|
||||
path = cmds.getAttr("{}.{}".format(node, "fileTextureName"))
|
||||
if path not in dependencies:
|
||||
dependencies.append(path)
|
||||
dependencies.add(path)
|
||||
|
||||
for node in cmds.ls(type="AlembicNode"):
|
||||
path = cmds.getAttr("{}.{}".format(node, "abc_File"))
|
||||
if path not in dependencies:
|
||||
dependencies.append(path)
|
||||
dependencies.add(path)
|
||||
|
||||
context.data["fileDependencies"] = dependencies
|
||||
self.log.debug(json.dumps(dependencies, indent=4))
|
||||
context.data["fileDependencies"] = list(dependencies)
|
||||
|
|
|
|||
|
|
@ -1,14 +1,18 @@
|
|||
import inspect
|
||||
|
||||
import pyblish.api
|
||||
|
||||
from maya import cmds
|
||||
from ayon_core.pipeline.publish import (
|
||||
context_plugin_should_run,
|
||||
PublishValidationError,
|
||||
OptionalPyblishPluginMixin
|
||||
)
|
||||
|
||||
|
||||
class ValidateCurrentRenderLayerIsRenderable(pyblish.api.ContextPlugin,
|
||||
OptionalPyblishPluginMixin):
|
||||
"""Validate if current render layer has a renderable camera
|
||||
"""Validate if current render layer has a renderable camera.
|
||||
|
||||
There is a bug in Redshift which occurs when the current render layer
|
||||
at file open has no renderable camera. The error raised is as follows:
|
||||
|
|
@ -32,8 +36,39 @@ class ValidateCurrentRenderLayerIsRenderable(pyblish.api.ContextPlugin,
|
|||
if not context_plugin_should_run(self, context):
|
||||
return
|
||||
|
||||
layer = cmds.editRenderLayerGlobals(query=True, currentRenderLayer=True)
|
||||
# This validator only makes sense when publishing renderlayer instances
|
||||
# with Redshift. We skip validation if there isn't any.
|
||||
if not any(self.is_active_redshift_render_instance(instance)
|
||||
for instance in context):
|
||||
return
|
||||
|
||||
cameras = cmds.ls(type="camera", long=True)
|
||||
renderable = any(c for c in cameras if cmds.getAttr(c + ".renderable"))
|
||||
assert renderable, ("Current render layer '%s' has no renderable "
|
||||
"camera" % layer)
|
||||
if not renderable:
|
||||
layer = cmds.editRenderLayerGlobals(query=True,
|
||||
currentRenderLayer=True)
|
||||
raise PublishValidationError(
|
||||
"Current render layer '{}' has no renderable camera".format(
|
||||
layer
|
||||
),
|
||||
description=inspect.getdoc(self)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def is_active_redshift_render_instance(instance) -> bool:
|
||||
"""Return whether instance is an active renderlayer instance set to
|
||||
render with Redshift renderer."""
|
||||
if not instance.data.get("active", True):
|
||||
return False
|
||||
|
||||
# Check this before families just because it's a faster check
|
||||
if not instance.data.get("renderer") == "redshift":
|
||||
return False
|
||||
|
||||
families = set()
|
||||
families.add(instance.data.get("family"))
|
||||
families.update(instance.data.get("families", []))
|
||||
if "renderlayer" not in families:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import inspect
|
||||
|
||||
from maya import cmds
|
||||
|
||||
import pyblish.api
|
||||
|
|
@ -14,8 +16,7 @@ class ValidateModelContent(pyblish.api.InstancePlugin,
|
|||
OptionalPyblishPluginMixin):
|
||||
"""Adheres to the content of 'model' product type
|
||||
|
||||
- Must have one top group. (configurable)
|
||||
- Must only contain: transforms, meshes and groups
|
||||
See `get_description` for more details.
|
||||
|
||||
"""
|
||||
|
||||
|
|
@ -28,13 +29,16 @@ class ValidateModelContent(pyblish.api.InstancePlugin,
|
|||
validate_top_group = True
|
||||
optional = False
|
||||
|
||||
allowed = ('mesh', 'transform', 'nurbsCurve', 'nurbsSurface', 'locator')
|
||||
|
||||
@classmethod
|
||||
def get_invalid(cls, instance):
|
||||
|
||||
content_instance = instance.data.get("setMembers", None)
|
||||
if not content_instance:
|
||||
cls.log.error("Instance has no nodes!")
|
||||
return [instance.data["name"]]
|
||||
cls.log.error("Model instance has no nodes. "
|
||||
"It is not allowed to be empty")
|
||||
return [instance.data["instance_node"]]
|
||||
|
||||
# All children will be included in the extracted export so we also
|
||||
# validate *all* descendents of the set members and we skip any
|
||||
|
|
@ -46,30 +50,42 @@ class ValidateModelContent(pyblish.api.InstancePlugin,
|
|||
content_instance = list(set(content_instance + descendants))
|
||||
|
||||
# Ensure only valid node types
|
||||
allowed = ('mesh', 'transform', 'nurbsCurve', 'nurbsSurface', 'locator')
|
||||
nodes = cmds.ls(content_instance, long=True)
|
||||
valid = cmds.ls(content_instance, long=True, type=allowed)
|
||||
valid = cmds.ls(content_instance, long=True, type=cls.allowed)
|
||||
invalid = set(nodes) - set(valid)
|
||||
|
||||
if invalid:
|
||||
cls.log.error("These nodes are not allowed: %s" % invalid)
|
||||
# List as bullet points
|
||||
invalid_bullets = "\n".join(f"- {node}" for node in invalid)
|
||||
|
||||
cls.log.error(
|
||||
"These nodes are not allowed:\n{}\n\n"
|
||||
"The valid node types are: {}".format(
|
||||
invalid_bullets, ", ".join(cls.allowed))
|
||||
)
|
||||
return list(invalid)
|
||||
|
||||
if not valid:
|
||||
cls.log.error("No valid nodes in the instance")
|
||||
return True
|
||||
cls.log.error(
|
||||
"No valid nodes in the model instance.\n"
|
||||
"The valid node types are: {}".format(", ".join(cls.allowed))
|
||||
)
|
||||
return [instance.data["instance_node"]]
|
||||
|
||||
# Ensure it has shapes
|
||||
shapes = cmds.ls(valid, long=True, shapes=True)
|
||||
if not shapes:
|
||||
cls.log.error("No shapes in the model instance")
|
||||
return True
|
||||
return [instance.data["instance_node"]]
|
||||
|
||||
# Top group
|
||||
top_parents = set([x.split("|")[1] for x in content_instance])
|
||||
# Ensure single top group
|
||||
top_parents = {x.split("|", 2)[1] for x in content_instance}
|
||||
if cls.validate_top_group and len(top_parents) != 1:
|
||||
cls.log.error("Must have exactly one top group")
|
||||
return top_parents
|
||||
cls.log.error(
|
||||
"A model instance must have exactly one top group. "
|
||||
"Found top groups: {}".format(", ".join(top_parents))
|
||||
)
|
||||
return list(top_parents)
|
||||
|
||||
def _is_visible(node):
|
||||
"""Return whether node is visible"""
|
||||
|
|
@ -101,5 +117,21 @@ class ValidateModelContent(pyblish.api.InstancePlugin,
|
|||
if invalid:
|
||||
raise PublishValidationError(
|
||||
title="Model content is invalid",
|
||||
message="See log for more details"
|
||||
message="Model content is invalid. See log for more details.",
|
||||
description=self.get_description()
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_description(cls):
|
||||
return inspect.cleandoc(f"""
|
||||
### Model content is invalid
|
||||
|
||||
Your model instance does not adhere to the rules of a
|
||||
model product type:
|
||||
|
||||
- Must have at least one visible shape in it, like a mesh.
|
||||
- Must have one root node. When exporting multiple meshes they
|
||||
must be inside a group.
|
||||
- May only contain the following node types:
|
||||
{", ".join(cls.allowed)}
|
||||
""")
|
||||
|
|
|
|||
|
|
@ -60,7 +60,8 @@ class ValidateNodeIDs(pyblish.api.InstancePlugin):
|
|||
# We do want to check the referenced nodes as it might be
|
||||
# part of the end product.
|
||||
id_nodes = lib.get_id_required_nodes(referenced_nodes=True,
|
||||
nodes=instance[:])
|
||||
invalid = [n for n in id_nodes if not lib.get_id(n)]
|
||||
|
||||
return invalid
|
||||
nodes=instance[:],
|
||||
# Exclude those with already
|
||||
# existing ids
|
||||
existing_ids=False)
|
||||
return id_nodes
|
||||
|
|
|
|||
|
|
@ -37,27 +37,27 @@ class ValidateNodeIdsInDatabase(pyblish.api.InstancePlugin):
|
|||
invalid = self.get_invalid(instance)
|
||||
if invalid:
|
||||
raise PublishValidationError(
|
||||
("Found folder ids which are not related to "
|
||||
"current project in instance: `{}`").format(instance.name))
|
||||
"Found folder ids which are not related to "
|
||||
"current project in instance: `{}`".format(instance.name))
|
||||
|
||||
@classmethod
|
||||
def get_invalid(cls, instance):
|
||||
|
||||
invalid = []
|
||||
nodes = instance[:]
|
||||
if not nodes:
|
||||
return
|
||||
|
||||
# Get all id required nodes
|
||||
id_required_nodes = lib.get_id_required_nodes(referenced_nodes=True,
|
||||
nodes=instance[:])
|
||||
id_required_nodes = lib.get_id_required_nodes(referenced_nodes=False,
|
||||
nodes=nodes)
|
||||
if not id_required_nodes:
|
||||
return
|
||||
|
||||
# check ids against database ids
|
||||
project_name = instance.context.data["projectName"]
|
||||
folder_entities = ayon_api.get_folders(project_name, fields={"id"})
|
||||
folder_ids = {
|
||||
folder_entity["id"]
|
||||
for folder_entity in folder_entities
|
||||
}
|
||||
folder_ids = cls.get_project_folder_ids(context=instance.context)
|
||||
|
||||
# Get all asset IDs
|
||||
invalid = []
|
||||
for node in id_required_nodes:
|
||||
cb_id = lib.get_id(node)
|
||||
|
||||
|
|
@ -71,3 +71,31 @@ class ValidateNodeIdsInDatabase(pyblish.api.InstancePlugin):
|
|||
invalid.append(node)
|
||||
|
||||
return invalid
|
||||
|
||||
@classmethod
|
||||
def get_project_folder_ids(cls, context):
|
||||
"""Return all folder ids in the current project.
|
||||
|
||||
Arguments:
|
||||
context (pyblish.api.Context): The publish context.
|
||||
|
||||
Returns:
|
||||
set[str]: All folder ids in the current project.
|
||||
|
||||
"""
|
||||
# We query the database only for the first instance instead of
|
||||
# per instance by storing a cache in the context
|
||||
key = "__cache_project_folder_ids"
|
||||
if key in context.data:
|
||||
return context.data[key]
|
||||
|
||||
# check ids against database
|
||||
project_name = context.data["projectName"]
|
||||
folder_entities = ayon_api.get_folders(project_name, fields={"id"})
|
||||
folder_ids = {
|
||||
folder_entity["id"]
|
||||
for folder_entity in folder_entities
|
||||
}
|
||||
|
||||
context.data[key] = folder_ids
|
||||
return folder_ids
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ from ayon_core.pipeline.publish import (
|
|||
import ayon_core.hosts.maya.api.action
|
||||
from ayon_core.hosts.maya.api import lib
|
||||
|
||||
from maya import cmds
|
||||
|
||||
|
||||
class ValidateNodeIdsUnique(pyblish.api.InstancePlugin):
|
||||
"""Validate the nodes in the instance have a unique Colorbleed Id
|
||||
|
|
@ -41,7 +43,7 @@ class ValidateNodeIdsUnique(pyblish.api.InstancePlugin):
|
|||
if invalid:
|
||||
label = "Nodes found with non-unique folder ids"
|
||||
raise PublishValidationError(
|
||||
message="{}: {}".format(label, invalid),
|
||||
message="{}, see log".format(label),
|
||||
title="Non-unique folder ids on nodes",
|
||||
description="{}\n- {}".format(label,
|
||||
"\n- ".join(sorted(invalid)))
|
||||
|
|
@ -54,7 +56,6 @@ class ValidateNodeIdsUnique(pyblish.api.InstancePlugin):
|
|||
# Check only non intermediate shapes
|
||||
# todo: must the instance itself ensure to have no intermediates?
|
||||
# todo: how come there are intermediates?
|
||||
from maya import cmds
|
||||
instance_members = cmds.ls(instance, noIntermediate=True, long=True)
|
||||
|
||||
# Collect each id with their members
|
||||
|
|
@ -67,10 +68,14 @@ class ValidateNodeIdsUnique(pyblish.api.InstancePlugin):
|
|||
|
||||
# Take only the ids with more than one member
|
||||
invalid = list()
|
||||
_iteritems = getattr(ids, "iteritems", ids.items)
|
||||
for _ids, members in _iteritems():
|
||||
for members in ids.values():
|
||||
if len(members) > 1:
|
||||
cls.log.error("ID found on multiple nodes: '%s'" % members)
|
||||
members_text = "\n".join(
|
||||
"- {}".format(member) for member in sorted(members)
|
||||
)
|
||||
cls.log.error(
|
||||
"ID found on multiple nodes:\n{}".format(members_text)
|
||||
)
|
||||
invalid.extend(members)
|
||||
|
||||
return invalid
|
||||
|
|
|
|||
|
|
@ -130,6 +130,18 @@ class LoadClip(plugin.NukeLoader):
|
|||
first = 1
|
||||
last = first + duration
|
||||
|
||||
# If a slate is present, the frame range is 1 frame longer for movies,
|
||||
# but file sequences its the first frame that is 1 frame lower.
|
||||
slate_frames = repre_entity["data"].get("slateFrames", 0)
|
||||
extension = "." + repre_entity["context"]["ext"]
|
||||
|
||||
if extension in VIDEO_EXTENSIONS:
|
||||
last += slate_frames
|
||||
|
||||
files_count = len(repre_entity["files"])
|
||||
if extension in IMAGE_EXTENSIONS and files_count != 1:
|
||||
first -= slate_frames
|
||||
|
||||
# Fallback to folder name when namespace is None
|
||||
if namespace is None:
|
||||
namespace = context["folder"]["name"]
|
||||
|
|
@ -167,7 +179,9 @@ class LoadClip(plugin.NukeLoader):
|
|||
repre_entity
|
||||
)
|
||||
|
||||
self._set_range_to_node(read_node, first, last, start_at_workfile)
|
||||
self._set_range_to_node(
|
||||
read_node, first, last, start_at_workfile, slate_frames
|
||||
)
|
||||
|
||||
version_name = version_entity["version"]
|
||||
if version_name < 0:
|
||||
|
|
@ -402,14 +416,21 @@ class LoadClip(plugin.NukeLoader):
|
|||
for member in members:
|
||||
nuke.delete(member)
|
||||
|
||||
def _set_range_to_node(self, read_node, first, last, start_at_workfile):
|
||||
def _set_range_to_node(
|
||||
self, read_node, first, last, start_at_workfile, slate_frames=0
|
||||
):
|
||||
read_node['origfirst'].setValue(int(first))
|
||||
read_node['first'].setValue(int(first))
|
||||
read_node['origlast'].setValue(int(last))
|
||||
read_node['last'].setValue(int(last))
|
||||
|
||||
# set start frame depending on workfile or version
|
||||
self._loader_shift(read_node, start_at_workfile)
|
||||
if start_at_workfile:
|
||||
read_node['frame_mode'].setValue("start at")
|
||||
|
||||
start_frame = self.script_start - slate_frames
|
||||
|
||||
read_node['frame'].setValue(str(start_frame))
|
||||
|
||||
def _make_retimes(self, parent_node, version_data):
|
||||
''' Create all retime and timewarping nodes with copied animation '''
|
||||
|
|
@ -466,18 +487,6 @@ class LoadClip(plugin.NukeLoader):
|
|||
for i, n in enumerate(dependent_nodes):
|
||||
last_node.setInput(i, n)
|
||||
|
||||
def _loader_shift(self, read_node, workfile_start=False):
|
||||
""" Set start frame of read node to a workfile start
|
||||
|
||||
Args:
|
||||
read_node (nuke.Node): The nuke's read node
|
||||
workfile_start (bool): set workfile start frame if true
|
||||
|
||||
"""
|
||||
if workfile_start:
|
||||
read_node['frame_mode'].setValue("start at")
|
||||
read_node['frame'].setValue(str(self.script_start))
|
||||
|
||||
def _get_node_name(self, context):
|
||||
folder_entity = context["folder"]
|
||||
product_name = context["product"]["name"]
|
||||
|
|
|
|||
|
|
@ -300,6 +300,10 @@ class ExtractSlateFrame(publish.Extractor):
|
|||
self.log.debug(
|
||||
"__ matching_repre: {}".format(pformat(matching_repre)))
|
||||
|
||||
data = matching_repre.get("data", {})
|
||||
data["slateFrames"] = 1
|
||||
matching_repre["data"] = data
|
||||
|
||||
self.log.info("Added slate frame to representation files")
|
||||
|
||||
def add_comment_slate_node(self, instance, node):
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ class ProcessSubmittedCacheJobOnFarm(pyblish.api.InstancePlugin,
|
|||
"FTRACK_SERVER",
|
||||
"AYON_APP_NAME",
|
||||
"AYON_USERNAME",
|
||||
"OPENPYPE_SG_USER",
|
||||
"AYON_SG_USERNAME",
|
||||
"KITSU_LOGIN",
|
||||
"KITSU_PWD"
|
||||
]
|
||||
|
|
|
|||
|
|
@ -130,7 +130,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin,
|
|||
"FTRACK_SERVER",
|
||||
"AYON_APP_NAME",
|
||||
"AYON_USERNAME",
|
||||
"OPENPYPE_SG_USER",
|
||||
"AYON_SG_USERNAME",
|
||||
"KITSU_LOGIN",
|
||||
"KITSU_PWD"
|
||||
]
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ class CreatePublishRoyalRenderJob(pyblish.api.InstancePlugin,
|
|||
"FTRACK_SERVER",
|
||||
"AYON_APP_NAME",
|
||||
"AYON_USERNAME",
|
||||
"OPENPYPE_SG_USER",
|
||||
"AYON_SG_USERNAME",
|
||||
]
|
||||
priority = 50
|
||||
|
||||
|
|
|
|||
|
|
@ -91,9 +91,15 @@ class DeliveryOptionsDialog(QtWidgets.QDialog):
|
|||
longest_key = max(self.templates.keys(), key=len)
|
||||
dropdown.setMinimumContentsLength(len(longest_key))
|
||||
|
||||
template_label = QtWidgets.QLabel()
|
||||
template_label.setCursor(QtGui.QCursor(QtCore.Qt.IBeamCursor))
|
||||
template_label.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse)
|
||||
template_dir_label = QtWidgets.QLabel()
|
||||
template_dir_label.setCursor(QtGui.QCursor(QtCore.Qt.IBeamCursor))
|
||||
template_dir_label.setTextInteractionFlags(
|
||||
QtCore.Qt.TextSelectableByMouse)
|
||||
|
||||
template_file_label = QtWidgets.QLabel()
|
||||
template_file_label.setCursor(QtGui.QCursor(QtCore.Qt.IBeamCursor))
|
||||
template_file_label.setTextInteractionFlags(
|
||||
QtCore.Qt.TextSelectableByMouse)
|
||||
|
||||
renumber_frame = QtWidgets.QCheckBox()
|
||||
|
||||
|
|
@ -123,7 +129,8 @@ class DeliveryOptionsDialog(QtWidgets.QDialog):
|
|||
|
||||
input_layout.addRow("Selected representations", selected_label)
|
||||
input_layout.addRow("Delivery template", dropdown)
|
||||
input_layout.addRow("Template value", template_label)
|
||||
input_layout.addRow("Directory template", template_dir_label)
|
||||
input_layout.addRow("File template", template_file_label)
|
||||
input_layout.addRow("Renumber Frame", renumber_frame)
|
||||
input_layout.addRow("Renumber start frame", first_frame_start)
|
||||
input_layout.addRow("Root", root_line_edit)
|
||||
|
|
@ -151,7 +158,8 @@ class DeliveryOptionsDialog(QtWidgets.QDialog):
|
|||
layout.addWidget(text_area)
|
||||
|
||||
self.selected_label = selected_label
|
||||
self.template_label = template_label
|
||||
self.template_dir_label = template_dir_label
|
||||
self.template_file_label = template_file_label
|
||||
self.dropdown = dropdown
|
||||
self.first_frame_start = first_frame_start
|
||||
self.renumber_frame = renumber_frame
|
||||
|
|
@ -282,11 +290,13 @@ class DeliveryOptionsDialog(QtWidgets.QDialog):
|
|||
"""Adds list of delivery templates from Anatomy to dropdown."""
|
||||
templates = {}
|
||||
for template_name, value in anatomy.templates["delivery"].items():
|
||||
path_template = value["path"]
|
||||
if (
|
||||
not isinstance(path_template, str)
|
||||
or not path_template.startswith('{root')
|
||||
):
|
||||
directory_template = value["directory"]
|
||||
if not directory_template.startswith("{root"):
|
||||
self.log.warning(
|
||||
"Skipping template '%s' because directory template does "
|
||||
"not start with `{root` in value: %s",
|
||||
template_name, directory_template
|
||||
)
|
||||
continue
|
||||
|
||||
templates[template_name] = value
|
||||
|
|
@ -350,7 +360,8 @@ class DeliveryOptionsDialog(QtWidgets.QDialog):
|
|||
name = self.dropdown.currentText()
|
||||
template_value = self.templates.get(name)
|
||||
if template_value:
|
||||
self.template_label.setText(template_value)
|
||||
self.template_dir_label.setText(template_value["directory"])
|
||||
self.template_file_label.setText(template_value["file"])
|
||||
self.btn_delivery.setEnabled(bool(self._get_selected_repres()))
|
||||
|
||||
def _update_progress(self, uploaded):
|
||||
|
|
|
|||
|
|
@ -32,6 +32,35 @@ from ayon_core.pipeline.publish import (
|
|||
from ayon_core.pipeline.publish.lib import add_repre_files_for_cleanup
|
||||
|
||||
|
||||
def frame_to_timecode(frame: int, fps: float) -> str:
|
||||
"""Convert a frame number and FPS to editorial timecode (HH:MM:SS:FF).
|
||||
|
||||
Unlike `ayon_core.pipeline.editorial.frames_to_timecode` this does not
|
||||
rely on the `opentimelineio` package, so it can be used across hosts that
|
||||
do not have it available.
|
||||
|
||||
Args:
|
||||
frame (int): The frame number to be converted.
|
||||
fps (float): The frames per second of the video.
|
||||
|
||||
Returns:
|
||||
str: The timecode in HH:MM:SS:FF format.
|
||||
"""
|
||||
# Calculate total seconds
|
||||
total_seconds = frame / fps
|
||||
|
||||
# Extract hours, minutes, and seconds
|
||||
hours = int(total_seconds // 3600)
|
||||
minutes = int((total_seconds % 3600) // 60)
|
||||
seconds = int(total_seconds % 60)
|
||||
|
||||
# Adjust for non-integer FPS by rounding the remaining frames appropriately
|
||||
remaining_frames = round((total_seconds - int(total_seconds)) * fps)
|
||||
|
||||
# Format and return the timecode
|
||||
return f"{hours:02d}:{minutes:02d}:{seconds:02d}:{remaining_frames:02d}"
|
||||
|
||||
|
||||
class ExtractReview(pyblish.api.InstancePlugin):
|
||||
"""Extracting Review mov file for Ftrack
|
||||
|
||||
|
|
@ -390,7 +419,16 @@ class ExtractReview(pyblish.api.InstancePlugin):
|
|||
# add outputName to anatomy format fill_data
|
||||
fill_data.update({
|
||||
"output": output_name,
|
||||
"ext": output_ext
|
||||
"ext": output_ext,
|
||||
|
||||
# By adding `timecode` as data we can use it
|
||||
# in the ffmpeg arguments for `--timecode` so that editorial
|
||||
# like Resolve or Premiere can detect the start frame for e.g.
|
||||
# review output files
|
||||
"timecode": frame_to_timecode(
|
||||
frame=temp_data["frame_start_handle"],
|
||||
fps=float(instance.data["fps"])
|
||||
)
|
||||
})
|
||||
|
||||
try: # temporary until oiiotool is supported cross platform
|
||||
|
|
|
|||
22
client/ayon_core/plugins/publish/extract_slate_data.py
Normal file
22
client/ayon_core/plugins/publish/extract_slate_data.py
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import pyblish.api
|
||||
|
||||
from ayon_core.pipeline import publish
|
||||
|
||||
|
||||
class ExtractSlateData(publish.Extractor):
|
||||
"""Add slate data for integration."""
|
||||
|
||||
label = "Slate Data"
|
||||
# Offset from ExtractReviewSlate and ExtractGenerateSlate.
|
||||
order = pyblish.api.ExtractorOrder + 0.49
|
||||
families = ["slate", "review"]
|
||||
hosts = ["nuke", "shell"]
|
||||
|
||||
def process(self, instance):
|
||||
for representation in instance.data.get("representations", []):
|
||||
if "slate-frame" not in representation.get("tags", []):
|
||||
continue
|
||||
|
||||
data = representation.get("data", {})
|
||||
data["slateFrames"] = 1
|
||||
representation["data"] = data
|
||||
|
|
@ -173,6 +173,7 @@ def _product_types_enum():
|
|||
"rig",
|
||||
"setdress",
|
||||
"take",
|
||||
"usd",
|
||||
"usdShade",
|
||||
"vdbcache",
|
||||
"vrayproxy",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue