Merge branch 'develop' into bugfix/nuke-intermediate-steams-bugs

This commit is contained in:
Jakub Jezek 2024-05-28 12:40:30 +02:00
commit 633aa0ca7d
No known key found for this signature in database
GPG key ID: 06DBD609ADF27FD9
302 changed files with 1476 additions and 336 deletions

View file

@ -51,8 +51,13 @@ IGNORED_MODULES_IN_AYON = set()
# - this is used to log the missing addon # - this is used to log the missing addon
MOVED_ADDON_MILESTONE_VERSIONS = { MOVED_ADDON_MILESTONE_VERSIONS = {
"applications": VersionInfo(0, 2, 0), "applications": VersionInfo(0, 2, 0),
"clockify": VersionInfo(0, 2, 0),
"traypublisher": VersionInfo(0, 2, 0),
"tvpaint": VersionInfo(0, 2, 0),
"nuke": VersionInfo(0, 2, 0),
} }
# Inherit from `object` for Python 2 hosts # Inherit from `object` for Python 2 hosts
class _ModuleClass(object): class _ModuleClass(object):
"""Fake module class for storing AYON addons. """Fake module class for storing AYON addons.

View file

@ -365,3 +365,62 @@ def maintained_time():
yield yield
finally: finally:
bpy.context.scene.frame_current = current_time bpy.context.scene.frame_current = current_time
def get_all_parents(obj):
"""Get all recursive parents of object.
Arguments:
obj (bpy.types.Object): Object to get all parents for.
Returns:
List[bpy.types.Object]: All parents of object
"""
result = []
while True:
obj = obj.parent
if not obj:
break
result.append(obj)
return result
def get_highest_root(objects):
"""Get the highest object (the least parents) among the objects.
If multiple objects have the same amount of parents (or no parents) the
first object found in the input iterable will be returned.
Note that this will *not* return objects outside of the input list, as
such it will not return the root of node from a child node. It is purely
intended to find the highest object among a list of objects. To instead
get the root from one object use, e.g. `get_all_parents(obj)[-1]`
Arguments:
objects (List[bpy.types.Object]): Objects to find the highest root in.
Returns:
Optional[bpy.types.Object]: First highest root found or None if no
`bpy.types.Object` found in input list.
"""
included_objects = {obj.name_full for obj in objects}
num_parents_to_obj = {}
for obj in objects:
if isinstance(obj, bpy.types.Object):
parents = get_all_parents(obj)
# included parents
parents = [parent for parent in parents if
parent.name_full in included_objects]
if not parents:
# A node without parents must be a highest root
return obj
num_parents_to_obj.setdefault(len(parents), obj)
if not num_parents_to_obj:
return
minimum_parent = min(num_parents_to_obj)
return num_parents_to_obj[minimum_parent]

View file

@ -26,7 +26,8 @@ from .ops import (
) )
from .lib import imprint from .lib import imprint
VALID_EXTENSIONS = [".blend", ".json", ".abc", ".fbx"] VALID_EXTENSIONS = [".blend", ".json", ".abc", ".fbx",
".usd", ".usdc", ".usda"]
def prepare_scene_name( def prepare_scene_name(

View file

@ -0,0 +1,30 @@
"""Create a USD Export."""
from ayon_core.hosts.blender.api import plugin, lib
class CreateUSD(plugin.BaseCreator):
"""Create USD Export"""
identifier = "io.openpype.creators.blender.usd"
name = "usdMain"
label = "USD"
product_type = "usd"
icon = "gears"
def create(
self, product_name: str, instance_data: dict, pre_create_data: dict
):
# Run parent create method
collection = super().create(
product_name, instance_data, pre_create_data
)
if pre_create_data.get("use_selection"):
objects = lib.get_selection()
for obj in objects:
collection.objects.link(obj)
if obj.type == 'EMPTY':
objects.extend(obj.children)
return collection

View file

@ -26,10 +26,10 @@ class CacheModelLoader(plugin.AssetLoader):
Note: Note:
At least for now it only supports Alembic files. At least for now it only supports Alembic files.
""" """
product_types = {"model", "pointcache", "animation"} product_types = {"model", "pointcache", "animation", "usd"}
representations = {"abc"} representations = {"abc", "usd"}
label = "Load Alembic" label = "Load Cache"
icon = "code-fork" icon = "code-fork"
color = "orange" color = "orange"
@ -53,10 +53,21 @@ class CacheModelLoader(plugin.AssetLoader):
plugin.deselect_all() plugin.deselect_all()
relative = bpy.context.preferences.filepaths.use_relative_paths relative = bpy.context.preferences.filepaths.use_relative_paths
bpy.ops.wm.alembic_import(
filepath=libpath, if any(libpath.lower().endswith(ext)
relative_path=relative for ext in [".usd", ".usda", ".usdc"]):
) # USD
bpy.ops.wm.usd_import(
filepath=libpath,
relative_path=relative
)
else:
# Alembic
bpy.ops.wm.alembic_import(
filepath=libpath,
relative_path=relative
)
imported = lib.get_selection() imported = lib.get_selection()

View file

@ -12,7 +12,7 @@ class CollectBlenderInstanceData(pyblish.api.InstancePlugin):
order = pyblish.api.CollectorOrder order = pyblish.api.CollectorOrder
hosts = ["blender"] hosts = ["blender"]
families = ["model", "pointcache", "animation", "rig", "camera", "layout", families = ["model", "pointcache", "animation", "rig", "camera", "layout",
"blendScene"] "blendScene", "usd"]
label = "Collect Instance" label = "Collect Instance"
def process(self, instance): def process(self, instance):

View file

@ -0,0 +1,90 @@
import os
import bpy
from ayon_core.pipeline import publish
from ayon_core.hosts.blender.api import plugin, lib
class ExtractUSD(publish.Extractor):
"""Extract as USD."""
label = "Extract USD"
hosts = ["blender"]
families = ["usd"]
def process(self, instance):
# Ignore runtime instances (e.g. USD layers)
# TODO: This is better done via more specific `families`
if not instance.data.get("transientData", {}).get("instance_node"):
return
# Define extract output file path
stagingdir = self.staging_dir(instance)
filename = f"{instance.name}.usd"
filepath = os.path.join(stagingdir, filename)
# Perform extraction
self.log.debug("Performing extraction..")
# Select all members to "export selected"
plugin.deselect_all()
selected = []
for obj in instance:
if isinstance(obj, bpy.types.Object):
obj.select_set(True)
selected.append(obj)
root = lib.get_highest_root(objects=instance[:])
if not root:
instance_node = instance.data["transientData"]["instance_node"]
raise publish.KnownPublishError(
f"No root object found in instance: {instance_node.name}"
)
self.log.debug(f"Exporting using active root: {root.name}")
context = plugin.create_blender_context(
active=root, selected=selected)
# Export USD
with bpy.context.temp_override(**context):
bpy.ops.wm.usd_export(
filepath=filepath,
selected_objects_only=True,
export_textures=False,
relative_paths=False,
export_animation=False,
export_hair=False,
export_uvmaps=True,
# TODO: add for new version of Blender (4+?)
# export_mesh_colors=True,
export_normals=True,
export_materials=True,
use_instancing=True
)
plugin.deselect_all()
# Add representation
representation = {
'name': 'usd',
'ext': 'usd',
'files': filename,
"stagingDir": stagingdir,
}
instance.data.setdefault("representations", []).append(representation)
self.log.debug("Extracted instance '%s' to: %s",
instance.name, representation)
class ExtractModelUSD(ExtractUSD):
"""Extract model as USD."""
label = "Extract USD (Model)"
hosts = ["blender"]
families = ["model"]
# Driven by settings
optional = True

View file

@ -0,0 +1,141 @@
# -*- coding: utf-8 -*-
"""Creator plugin for creating Model product type.
Note:
Currently, This creator plugin is the same as 'create_pointcache.py'
But renaming the product type to 'model'.
It's purpose to support
Maya (load/publish model from maya to/from houdini).
It's considered to support multiple representations in the future.
"""
from ayon_core.hosts.houdini.api import plugin
from ayon_core.lib import BoolDef
import hou
class CreateModel(plugin.HoudiniCreator):
"""Create Model"""
identifier = "io.openpype.creators.houdini.model"
label = "Model"
product_type = "model"
icon = "cube"
def create(self, product_name, instance_data, pre_create_data):
instance_data.pop("active", None)
instance_data.update({"node_type": "alembic"})
creator_attributes = instance_data.setdefault(
"creator_attributes", dict())
creator_attributes["farm"] = pre_create_data["farm"]
instance = super(CreateModel, self).create(
product_name,
instance_data,
pre_create_data)
instance_node = hou.node(instance.get("instance_node"))
parms = {
"use_sop_path": True,
"build_from_path": True,
"path_attrib": "path",
"prim_to_detail_pattern": "cbId",
"format": 2,
"facesets": 0,
"filename": hou.text.expandString(
"$HIP/pyblish/{}.abc".format(product_name))
}
if self.selected_nodes:
selected_node = self.selected_nodes[0]
# Although Houdini allows ObjNode path on `sop_path` for the
# the ROP node we prefer it set to the SopNode path explicitly
# Allow sop level paths (e.g. /obj/geo1/box1)
if isinstance(selected_node, hou.SopNode):
parms["sop_path"] = selected_node.path()
self.log.debug(
"Valid SopNode selection, 'SOP Path' in ROP will be set to '%s'."
% selected_node.path()
)
# Allow object level paths to Geometry nodes (e.g. /obj/geo1)
# but do not allow other object level nodes types like cameras, etc.
elif isinstance(selected_node, hou.ObjNode) and \
selected_node.type().name() in ["geo"]:
# get the output node with the minimum
# 'outputidx' or the node with display flag
sop_path = self.get_obj_output(selected_node)
if sop_path:
parms["sop_path"] = sop_path.path()
self.log.debug(
"Valid ObjNode selection, 'SOP Path' in ROP will be set to "
"the child path '%s'."
% sop_path.path()
)
if not parms.get("sop_path", None):
self.log.debug(
"Selection isn't valid. 'SOP Path' in ROP will be empty."
)
else:
self.log.debug(
"No Selection. 'SOP Path' in ROP will be empty."
)
instance_node.setParms(parms)
instance_node.parm("trange").set(1)
# Explicitly set f1 and f2 to frame start.
# Which forces the rop node to export one frame.
instance_node.parmTuple('f').deleteAllKeyframes()
fstart = int(hou.hscriptExpression("$FSTART"))
instance_node.parmTuple('f').set((fstart, fstart, 1))
# Lock any parameters in this list
to_lock = ["prim_to_detail_pattern"]
self.lock_parameters(instance_node, to_lock)
def get_network_categories(self):
return [
hou.ropNodeTypeCategory(),
hou.sopNodeTypeCategory()
]
def get_obj_output(self, obj_node):
"""Find output node with the smallest 'outputidx'."""
outputs = obj_node.subnetOutputs()
# if obj_node is empty
if not outputs:
return
# if obj_node has one output child whether its
# sop output node or a node with the render flag
elif len(outputs) == 1:
return outputs[0]
# if there are more than one, then it have multiple output nodes
# return the one with the minimum 'outputidx'
else:
return min(outputs,
key=lambda node: node.evalParm('outputidx'))
def get_instance_attr_defs(self):
return [
BoolDef("farm",
label="Submitting to Farm",
default=False)
]
def get_pre_create_attr_defs(self):
attrs = super().get_pre_create_attr_defs()
# Use same attributes as for instance attributes
return attrs + self.get_instance_attr_defs()

View file

@ -11,7 +11,7 @@ class CollectDataforCache(pyblish.api.InstancePlugin):
order = pyblish.api.CollectorOrder + 0.11 order = pyblish.api.CollectorOrder + 0.11
families = ["ass", "pointcache", families = ["ass", "pointcache",
"mantraifd", "redshiftproxy", "mantraifd", "redshiftproxy",
"vdbcache"] "vdbcache", "model"]
hosts = ["houdini"] hosts = ["houdini"]
targets = ["local", "remote"] targets = ["local", "remote"]
label = "Collect Data for Cache" label = "Collect Data for Cache"
@ -43,10 +43,7 @@ class CollectDataforCache(pyblish.api.InstancePlugin):
cache_files = {"_": instance.data["files"]} cache_files = {"_": instance.data["files"]}
# Convert instance family to pointcache if it is bgeo or abc # Convert instance family to pointcache if it is bgeo or abc
# because ??? # because ???
for family in instance.data["families"]: self.log.debug(instance.data["families"])
if family == "bgeo" or "abc":
instance.data["productType"] = "pointcache"
break
instance.data.update({ instance.data.update({
"plugin": "Houdini", "plugin": "Houdini",
"publish": True "publish": True

View file

@ -10,7 +10,7 @@ class CollectChunkSize(pyblish.api.InstancePlugin,
order = pyblish.api.CollectorOrder + 0.05 order = pyblish.api.CollectorOrder + 0.05
families = ["ass", "pointcache", families = ["ass", "pointcache",
"vdbcache", "mantraifd", "vdbcache", "mantraifd",
"redshiftproxy"] "redshiftproxy", "model"]
hosts = ["houdini"] hosts = ["houdini"]
targets = ["local", "remote"] targets = ["local", "remote"]
label = "Collect Chunk Size" label = "Collect Chunk Size"

View file

@ -1,21 +1,24 @@
"""Collector for pointcache types. """Collector for different types.
This will add additional family to pointcache instance based on This will add additional families to different instance based on
the creator_identifier parameter. the creator_identifier parameter.
""" """
import pyblish.api import pyblish.api
class CollectPointcacheType(pyblish.api.InstancePlugin): class CollectPointcacheType(pyblish.api.InstancePlugin):
"""Collect data type for pointcache instance.""" """Collect data type for different instances."""
order = pyblish.api.CollectorOrder order = pyblish.api.CollectorOrder
hosts = ["houdini"] hosts = ["houdini"]
families = ["pointcache"] families = ["pointcache", "model"]
label = "Collect type of pointcache" label = "Collect instances types"
def process(self, instance): def process(self, instance):
if instance.data["creator_identifier"] == "io.openpype.creators.houdini.bgeo": # noqa: E501 if instance.data["creator_identifier"] == "io.openpype.creators.houdini.bgeo": # noqa: E501
instance.data["families"] += ["bgeo"] instance.data["families"] += ["bgeo"]
elif instance.data["creator_identifier"] == "io.openpype.creators.houdini.pointcache": # noqa: E501 elif instance.data["creator_identifier"] in {
"io.openpype.creators.houdini.pointcache",
"io.openpype.creators.houdini.model"
}:
instance.data["families"] += ["abc"] instance.data["families"] += ["abc"]

View file

@ -132,6 +132,6 @@ class CollectLocalRenderInstances(pyblish.api.InstancePlugin):
] ]
}) })
# Remove original render instance # Skip integrating original render instance.
# I can't remove it here as I still need it to trigger the render. # We are not removing it because it's used to trigger the render.
# context.remove(instance) instance.data["integrate"] = False

View file

@ -15,7 +15,8 @@ class CollectOutputSOPPath(pyblish.api.InstancePlugin):
"usd", "usd",
"usdrender", "usdrender",
"redshiftproxy", "redshiftproxy",
"staticMesh" "staticMesh",
"model"
] ]
hosts = ["houdini"] hosts = ["houdini"]

View file

@ -0,0 +1,60 @@
# -*- coding: utf-8 -*-
"""Validator for checking that export is a single frame."""
import pyblish.api
from ayon_core.pipeline import (
PublishValidationError,
OptionalPyblishPluginMixin
)
from ayon_core.pipeline.publish import ValidateContentsOrder
from ayon_core.hosts.houdini.api.action import SelectInvalidAction
class ValidateSingleFrame(pyblish.api.InstancePlugin,
OptionalPyblishPluginMixin):
"""Validate Export is a Single Frame.
It checks if rop node is exporting one frame.
This is mainly for Model product type.
"""
families = ["model"]
hosts = ["houdini"]
label = "Validate Single Frame"
order = ValidateContentsOrder + 0.1
actions = [SelectInvalidAction]
def process(self, instance):
invalid = self.get_invalid(instance)
if invalid:
nodes = [n.path() for n in invalid]
raise PublishValidationError(
"See log for details. "
"Invalid nodes: {0}".format(nodes)
)
@classmethod
def get_invalid(cls, instance):
invalid = []
frame_start = instance.data.get("frameStartHandle")
frame_end = instance.data.get("frameEndHandle")
# This happens if instance node has no 'trange' parameter.
if frame_start is None or frame_end is None:
cls.log.debug(
"No frame data, skipping check.."
)
return
if frame_start != frame_end:
invalid.append(instance.data["instance_node"])
cls.log.error(
"Invalid frame range on '%s'."
"You should use the same frame number for 'f1' "
"and 'f2' parameters.",
instance.data["instance_node"].path()
)
return invalid

View file

@ -16,9 +16,13 @@ class ValidateMeshIsStatic(pyblish.api.InstancePlugin,
"""Validate mesh is static. """Validate mesh is static.
It checks if output node is time dependent. It checks if output node is time dependent.
this avoids getting different output from ROP node when extracted
from a different frame than the first frame.
(Might be overly restrictive though)
""" """
families = ["staticMesh"] families = ["staticMesh",
"model"]
hosts = ["houdini"] hosts = ["houdini"]
label = "Validate Mesh is Static" label = "Validate Mesh is Static"
order = ValidateContentsOrder + 0.1 order = ValidateContentsOrder + 0.1

View file

@ -7,7 +7,7 @@ class ValidateIntermediateDirectoriesChecked(pyblish.api.InstancePlugin):
"""Validate Create Intermediate Directories is enabled on ROP node.""" """Validate Create Intermediate Directories is enabled on ROP node."""
order = pyblish.api.ValidatorOrder order = pyblish.api.ValidatorOrder
families = ["pointcache", "camera", "vdbcache"] families = ["pointcache", "camera", "vdbcache", "model"]
hosts = ["houdini"] hosts = ["houdini"]
label = "Create Intermediate Directories Checked" label = "Create Intermediate Directories Checked"

View file

@ -22,7 +22,7 @@ class ValidateSopOutputNode(pyblish.api.InstancePlugin):
""" """
order = pyblish.api.ValidatorOrder order = pyblish.api.ValidatorOrder
families = ["pointcache", "vdbcache"] families = ["pointcache", "vdbcache", "model"]
hosts = ["houdini"] hosts = ["houdini"]
label = "Validate Output Node (SOP)" label = "Validate Output Node (SOP)"
actions = [SelectROPAction, SelectInvalidAction] actions = [SelectROPAction, SelectInvalidAction]

View file

@ -1,5 +0,0 @@
from .clockify_module import ClockifyModule
__all__ = (
"ClockifyModule",
)

View file

@ -26,27 +26,32 @@ class CollectDeadlinePools(pyblish.api.InstancePlugin,
order = pyblish.api.CollectorOrder + 0.420 order = pyblish.api.CollectorOrder + 0.420
label = "Collect Deadline Pools" label = "Collect Deadline Pools"
hosts = ["aftereffects", hosts = [
"fusion", "aftereffects",
"harmony" "fusion",
"nuke", "harmony",
"maya", "maya",
"max", "max",
"houdini"] "houdini",
"nuke",
]
families = ["render", families = [
"rendering", "render",
"render.farm", "prerender",
"renderFarm", "rendering",
"renderlayer", "render.farm",
"maxrender", "renderFarm",
"usdrender", "renderlayer",
"redshift_rop", "maxrender",
"arnold_rop", "usdrender",
"mantra_rop", "redshift_rop",
"karma_rop", "arnold_rop",
"vray_rop", "mantra_rop",
"publish.hou"] "karma_rop",
"vray_rop",
"publish.hou",
]
primary_pool = None primary_pool = None
secondary_pool = None secondary_pool = None

View file

@ -1,8 +1,10 @@
import pyblish.api import pyblish.api
from ayon_core.pipeline.publish import PublishValidationError from ayon_core.pipeline.publish import (
PublishValidationError, OptionalPyblishPluginMixin
)
class ValidateVersion(pyblish.api.InstancePlugin): class ValidateVersion(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin):
"""Validate instance version. """Validate instance version.
AYON does not allow overwriting previously published versions. AYON does not allow overwriting previously published versions.
@ -18,6 +20,9 @@ class ValidateVersion(pyblish.api.InstancePlugin):
active = True active = True
def process(self, instance): def process(self, instance):
if not self.is_active(instance.data):
return
version = instance.data.get("version") version = instance.data.get("version")
latest_version = instance.data.get("latestVersion") latest_version = instance.data.get("latestVersion")

View file

@ -5,7 +5,7 @@ import ayon_api
import six import six
from ayon_core.style import get_default_entity_icon_color from ayon_core.style import get_default_entity_icon_color
from ayon_core.lib import CacheItem from ayon_core.lib import CacheItem, NestedCacheItem
PROJECTS_MODEL_SENDER = "projects.model" PROJECTS_MODEL_SENDER = "projects.model"
@ -17,6 +17,49 @@ class AbstractHierarchyController:
pass pass
class StatusItem:
"""Item representing status of project.
Args:
name (str): Status name ("Not ready").
color (str): Status color in hex ("#434a56").
short (str): Short status name ("NRD").
icon (str): Icon name in MaterialIcons ("fiber_new").
state (Literal["not_started", "in_progress", "done", "blocked"]):
Status state.
"""
def __init__(self, name, color, short, icon, state):
self.name = name
self.color = color
self.short = short
self.icon = icon
self.state = state
def to_data(self):
return {
"name": self.name,
"color": self.color,
"short": self.short,
"icon": self.icon,
"state": self.state,
}
@classmethod
def from_data(cls, data):
return cls(**data)
@classmethod
def from_project_item(cls, status_data):
return cls(
name=status_data["name"],
color=status_data["color"],
short=status_data["shortName"],
icon=status_data["icon"],
state=status_data["state"],
)
class ProjectItem: class ProjectItem:
"""Item representing folder entity on a server. """Item representing folder entity on a server.
@ -40,6 +83,23 @@ class ProjectItem:
} }
self.icon = icon self.icon = icon
@classmethod
def from_entity(cls, project_entity):
"""Creates folder item from entity.
Args:
project_entity (dict[str, Any]): Project entity.
Returns:
ProjectItem: Project item.
"""
return cls(
project_entity["name"],
project_entity["active"],
project_entity["library"],
)
def to_data(self): def to_data(self):
"""Converts folder item to data. """Converts folder item to data.
@ -79,7 +139,7 @@ def _get_project_items_from_entitiy(projects):
""" """
return [ return [
ProjectItem(project["name"], project["active"], project["library"]) ProjectItem.from_entity(project)
for project in projects for project in projects
] ]
@ -87,18 +147,29 @@ def _get_project_items_from_entitiy(projects):
class ProjectsModel(object): class ProjectsModel(object):
def __init__(self, controller): def __init__(self, controller):
self._projects_cache = CacheItem(default_factory=list) self._projects_cache = CacheItem(default_factory=list)
self._project_items_by_name = {} self._project_statuses_cache = NestedCacheItem(
self._projects_by_name = {} levels=1, default_factory=list
)
self._projects_by_name = NestedCacheItem(
levels=1, default_factory=list
)
self._is_refreshing = False self._is_refreshing = False
self._controller = controller self._controller = controller
def reset(self): def reset(self):
self._projects_cache.reset() self._projects_cache.reset()
self._project_items_by_name = {} self._project_statuses_cache.reset()
self._projects_by_name = {} self._projects_by_name.reset()
def refresh(self): def refresh(self):
"""Refresh project items.
This method will requery list of ProjectItem returned by
'get_project_items'.
To reset all cached items use 'reset' method.
"""
self._refresh_projects_cache() self._refresh_projects_cache()
def get_project_items(self, sender): def get_project_items(self, sender):
@ -117,12 +188,51 @@ class ProjectsModel(object):
return self._projects_cache.get_data() return self._projects_cache.get_data()
def get_project_entity(self, project_name): def get_project_entity(self, project_name):
if project_name not in self._projects_by_name: """Get project entity.
Args:
project_name (str): Project name.
Returns:
Union[dict[str, Any], None]: Project entity or None if project
was not found by name.
"""
project_cache = self._projects_by_name[project_name]
if not project_cache.is_valid:
entity = None entity = None
if project_name: if project_name:
entity = ayon_api.get_project(project_name) entity = ayon_api.get_project(project_name)
self._projects_by_name[project_name] = entity project_cache.update_data(entity)
return self._projects_by_name[project_name] return project_cache.get_data()
def get_project_status_items(self, project_name, sender):
"""Get project status items.
Args:
project_name (str): Project name.
sender (Union[str, None]): Name of sender who asked for items.
Returns:
list[StatusItem]: Status items for project.
"""
statuses_cache = self._project_statuses_cache[project_name]
if not statuses_cache.is_valid:
with self._project_statuses_refresh_event_manager(
sender, project_name
):
project_entity = None
if project_name:
project_entity = self.get_project_entity(project_name)
statuses = []
if project_entity:
statuses = [
StatusItem.from_project_item(status)
for status in project_entity["statuses"]
]
statuses_cache.update_data(statuses)
return statuses_cache.get_data()
@contextlib.contextmanager @contextlib.contextmanager
def _project_refresh_event_manager(self, sender): def _project_refresh_event_manager(self, sender):
@ -143,6 +253,23 @@ class ProjectsModel(object):
) )
self._is_refreshing = False self._is_refreshing = False
@contextlib.contextmanager
def _project_statuses_refresh_event_manager(self, sender, project_name):
self._controller.emit_event(
"projects.statuses.refresh.started",
{"sender": sender, "project_name": project_name},
PROJECTS_MODEL_SENDER
)
try:
yield
finally:
self._controller.emit_event(
"projects.statuses.refresh.finished",
{"sender": sender, "project_name": project_name},
PROJECTS_MODEL_SENDER
)
def _refresh_projects_cache(self, sender=None): def _refresh_projects_cache(self, sender=None):
if self._is_refreshing: if self._is_refreshing:
return None return None

View file

@ -290,6 +290,34 @@ class ActionDelegate(QtWidgets.QStyledItemDelegate):
painter.drawPixmap(extender_x, extender_y, pix) painter.drawPixmap(extender_x, extender_y, pix)
class ActionsProxyModel(QtCore.QSortFilterProxyModel):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive)
def lessThan(self, left, right):
# Sort by action order and then by label
left_value = left.data(ACTION_SORT_ROLE)
right_value = right.data(ACTION_SORT_ROLE)
# Values are same -> use super sorting
if left_value == right_value:
# Default behavior is using DisplayRole
return super().lessThan(left, right)
# Validate 'None' values
if right_value is None:
return True
if left_value is None:
return False
# Sort values and handle incompatible types
try:
return left_value < right_value
except TypeError:
return True
class ActionsWidget(QtWidgets.QWidget): class ActionsWidget(QtWidgets.QWidget):
def __init__(self, controller, parent): def __init__(self, controller, parent):
super(ActionsWidget, self).__init__(parent) super(ActionsWidget, self).__init__(parent)
@ -316,10 +344,7 @@ class ActionsWidget(QtWidgets.QWidget):
model = ActionsQtModel(controller) model = ActionsQtModel(controller)
proxy_model = QtCore.QSortFilterProxyModel() proxy_model = ActionsProxyModel()
proxy_model.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive)
proxy_model.setSortRole(ACTION_SORT_ROLE)
proxy_model.setSourceModel(model) proxy_model.setSourceModel(model)
view.setModel(proxy_model) view.setModel(proxy_model)
@ -359,7 +384,8 @@ class ActionsWidget(QtWidgets.QWidget):
def _on_model_refresh(self): def _on_model_refresh(self):
self._proxy_model.sort(0) self._proxy_model.sort(0)
# Force repaint all items # Force repaint all items
self._view.update() viewport = self._view.viewport()
viewport.update()
def _on_animation(self): def _on_animation(self):
time_now = time.time() time_now = time.time()

View file

@ -114,6 +114,7 @@ class VersionItem:
thumbnail_id (Union[str, None]): Thumbnail id. thumbnail_id (Union[str, None]): Thumbnail id.
published_time (Union[str, None]): Published time in format published_time (Union[str, None]): Published time in format
'%Y%m%dT%H%M%SZ'. '%Y%m%dT%H%M%SZ'.
status (Union[str, None]): Status name.
author (Union[str, None]): Author. author (Union[str, None]): Author.
frame_range (Union[str, None]): Frame range. frame_range (Union[str, None]): Frame range.
duration (Union[int, None]): Duration. duration (Union[int, None]): Duration.
@ -132,6 +133,7 @@ class VersionItem:
thumbnail_id, thumbnail_id,
published_time, published_time,
author, author,
status,
frame_range, frame_range,
duration, duration,
handles, handles,
@ -146,6 +148,7 @@ class VersionItem:
self.is_hero = is_hero self.is_hero = is_hero
self.published_time = published_time self.published_time = published_time
self.author = author self.author = author
self.status = status
self.frame_range = frame_range self.frame_range = frame_range
self.duration = duration self.duration = duration
self.handles = handles self.handles = handles
@ -185,6 +188,7 @@ class VersionItem:
"is_hero": self.is_hero, "is_hero": self.is_hero,
"published_time": self.published_time, "published_time": self.published_time,
"author": self.author, "author": self.author,
"status": self.status,
"frame_range": self.frame_range, "frame_range": self.frame_range,
"duration": self.duration, "duration": self.duration,
"handles": self.handles, "handles": self.handles,
@ -488,6 +492,27 @@ class FrontendLoaderController(_BaseLoaderController):
pass pass
@abstractmethod
def get_project_status_items(self, project_name, sender=None):
"""Items for all projects available on server.
Triggers event topics "projects.statuses.refresh.started" and
"projects.statuses.refresh.finished" with data:
{
"sender": sender,
"project_name": project_name
}
Args:
project_name (Union[str, None]): Project name.
sender (Optional[str]): Sender who requested the items.
Returns:
list[StatusItem]: List of status items.
"""
pass
@abstractmethod @abstractmethod
def get_product_items(self, project_name, folder_ids, sender=None): def get_product_items(self, project_name, folder_ids, sender=None):
"""Product items for folder ids. """Product items for folder ids.

View file

@ -180,6 +180,11 @@ class LoaderController(BackendLoaderController, FrontendLoaderController):
def get_project_items(self, sender=None): def get_project_items(self, sender=None):
return self._projects_model.get_project_items(sender) return self._projects_model.get_project_items(sender)
def get_project_status_items(self, project_name, sender=None):
return self._projects_model.get_project_status_items(
project_name, sender
)
def get_folder_items(self, project_name, sender=None): def get_folder_items(self, project_name, sender=None):
return self._hierarchy_model.get_folder_items(project_name, sender) return self._hierarchy_model.get_folder_items(project_name, sender)

View file

@ -58,6 +58,7 @@ def version_item_from_entity(version):
thumbnail_id=version["thumbnailId"], thumbnail_id=version["thumbnailId"],
published_time=published_time, published_time=published_time,
author=author, author=author,
status=version["status"],
frame_range=frame_range, frame_range=frame_range,
duration=duration, duration=duration,
handles=handles, handles=handles,
@ -526,8 +527,11 @@ class ProductsModel:
products = list(ayon_api.get_products(project_name, **kwargs)) products = list(ayon_api.get_products(project_name, **kwargs))
product_ids = {product["id"] for product in products} product_ids = {product["id"] for product in products}
# Add 'status' to fields -> fixed in ayon-python-api 1.0.4
fields = ayon_api.get_default_fields_for_type("version")
fields.add("status")
versions = ayon_api.get_versions( versions = ayon_api.get_versions(
project_name, product_ids=product_ids project_name, product_ids=product_ids, fields=fields
) )
return self._create_product_items( return self._create_product_items(

View file

@ -6,6 +6,9 @@ from ayon_core.tools.utils.lib import format_version
from .products_model import ( from .products_model import (
PRODUCT_ID_ROLE, PRODUCT_ID_ROLE,
VERSION_NAME_EDIT_ROLE, VERSION_NAME_EDIT_ROLE,
VERSION_STATUS_NAME_ROLE,
VERSION_STATUS_SHORT_ROLE,
VERSION_STATUS_COLOR_ROLE,
VERSION_ID_ROLE, VERSION_ID_ROLE,
PRODUCT_IN_SCENE_ROLE, PRODUCT_IN_SCENE_ROLE,
ACTIVE_SITE_ICON_ROLE, ACTIVE_SITE_ICON_ROLE,
@ -104,7 +107,10 @@ class VersionDelegate(QtWidgets.QStyledItemDelegate):
style = QtWidgets.QApplication.style() style = QtWidgets.QApplication.style()
style.drawControl( style.drawControl(
style.CE_ItemViewItem, option, painter, option.widget QtWidgets.QCommonStyle.CE_ItemViewItem,
option,
painter,
option.widget
) )
painter.save() painter.save()
@ -116,9 +122,14 @@ class VersionDelegate(QtWidgets.QStyledItemDelegate):
pen.setColor(fg_color) pen.setColor(fg_color)
painter.setPen(pen) painter.setPen(pen)
text_rect = style.subElementRect(style.SE_ItemViewItemText, option) text_rect = style.subElementRect(
QtWidgets.QCommonStyle.SE_ItemViewItemText,
option
)
text_margin = style.proxy().pixelMetric( text_margin = style.proxy().pixelMetric(
style.PM_FocusFrameHMargin, option, option.widget QtWidgets.QCommonStyle.PM_FocusFrameHMargin,
option,
option.widget
) + 1 ) + 1
painter.drawText( painter.drawText(
@ -194,6 +205,57 @@ class LoadedInSceneDelegate(QtWidgets.QStyledItemDelegate):
option.palette.setBrush(QtGui.QPalette.Text, color) option.palette.setBrush(QtGui.QPalette.Text, color)
class StatusDelegate(QtWidgets.QStyledItemDelegate):
"""Delegate showing status name and short name."""
def paint(self, painter, option, index):
if option.widget:
style = option.widget.style()
else:
style = QtWidgets.QApplication.style()
style.drawControl(
QtWidgets.QCommonStyle.CE_ItemViewItem,
option,
painter,
option.widget
)
painter.save()
text_rect = style.subElementRect(
QtWidgets.QCommonStyle.SE_ItemViewItemText,
option
)
text_margin = style.proxy().pixelMetric(
QtWidgets.QCommonStyle.PM_FocusFrameHMargin,
option,
option.widget
) + 1
padded_text_rect = text_rect.adjusted(
text_margin, 0, - text_margin, 0
)
fm = QtGui.QFontMetrics(option.font)
text = index.data(VERSION_STATUS_NAME_ROLE)
if padded_text_rect.width() < fm.width(text):
text = index.data(VERSION_STATUS_SHORT_ROLE)
status_color = index.data(VERSION_STATUS_COLOR_ROLE)
fg_color = QtGui.QColor(status_color)
pen = painter.pen()
pen.setColor(fg_color)
painter.setPen(pen)
painter.drawText(
padded_text_rect,
option.displayAlignment,
text
)
painter.restore()
class SiteSyncDelegate(QtWidgets.QStyledItemDelegate): class SiteSyncDelegate(QtWidgets.QStyledItemDelegate):
"""Paints icons and downloaded representation ration for both sites.""" """Paints icons and downloaded representation ration for both sites."""

View file

@ -22,18 +22,21 @@ VERSION_HERO_ROLE = QtCore.Qt.UserRole + 11
VERSION_NAME_ROLE = QtCore.Qt.UserRole + 12 VERSION_NAME_ROLE = QtCore.Qt.UserRole + 12
VERSION_NAME_EDIT_ROLE = QtCore.Qt.UserRole + 13 VERSION_NAME_EDIT_ROLE = QtCore.Qt.UserRole + 13
VERSION_PUBLISH_TIME_ROLE = QtCore.Qt.UserRole + 14 VERSION_PUBLISH_TIME_ROLE = QtCore.Qt.UserRole + 14
VERSION_AUTHOR_ROLE = QtCore.Qt.UserRole + 15 VERSION_STATUS_NAME_ROLE = QtCore.Qt.UserRole + 15
VERSION_FRAME_RANGE_ROLE = QtCore.Qt.UserRole + 16 VERSION_STATUS_SHORT_ROLE = QtCore.Qt.UserRole + 16
VERSION_DURATION_ROLE = QtCore.Qt.UserRole + 17 VERSION_STATUS_COLOR_ROLE = QtCore.Qt.UserRole + 17
VERSION_HANDLES_ROLE = QtCore.Qt.UserRole + 18 VERSION_AUTHOR_ROLE = QtCore.Qt.UserRole + 18
VERSION_STEP_ROLE = QtCore.Qt.UserRole + 19 VERSION_FRAME_RANGE_ROLE = QtCore.Qt.UserRole + 19
VERSION_AVAILABLE_ROLE = QtCore.Qt.UserRole + 20 VERSION_DURATION_ROLE = QtCore.Qt.UserRole + 20
VERSION_THUMBNAIL_ID_ROLE = QtCore.Qt.UserRole + 21 VERSION_HANDLES_ROLE = QtCore.Qt.UserRole + 21
ACTIVE_SITE_ICON_ROLE = QtCore.Qt.UserRole + 22 VERSION_STEP_ROLE = QtCore.Qt.UserRole + 22
REMOTE_SITE_ICON_ROLE = QtCore.Qt.UserRole + 23 VERSION_AVAILABLE_ROLE = QtCore.Qt.UserRole + 23
REPRESENTATIONS_COUNT_ROLE = QtCore.Qt.UserRole + 24 VERSION_THUMBNAIL_ID_ROLE = QtCore.Qt.UserRole + 24
SYNC_ACTIVE_SITE_AVAILABILITY = QtCore.Qt.UserRole + 25 ACTIVE_SITE_ICON_ROLE = QtCore.Qt.UserRole + 25
SYNC_REMOTE_SITE_AVAILABILITY = QtCore.Qt.UserRole + 26 REMOTE_SITE_ICON_ROLE = QtCore.Qt.UserRole + 26
REPRESENTATIONS_COUNT_ROLE = QtCore.Qt.UserRole + 27
SYNC_ACTIVE_SITE_AVAILABILITY = QtCore.Qt.UserRole + 28
SYNC_REMOTE_SITE_AVAILABILITY = QtCore.Qt.UserRole + 29
class ProductsModel(QtGui.QStandardItemModel): class ProductsModel(QtGui.QStandardItemModel):
@ -44,6 +47,7 @@ class ProductsModel(QtGui.QStandardItemModel):
"Product type", "Product type",
"Folder", "Folder",
"Version", "Version",
"Status",
"Time", "Time",
"Author", "Author",
"Frames", "Frames",
@ -69,11 +73,35 @@ class ProductsModel(QtGui.QStandardItemModel):
] ]
] ]
version_col = column_labels.index("Version") product_name_col = column_labels.index("Product name")
published_time_col = column_labels.index("Time") product_type_col = column_labels.index("Product type")
folders_label_col = column_labels.index("Folder") folders_label_col = column_labels.index("Folder")
version_col = column_labels.index("Version")
status_col = column_labels.index("Status")
published_time_col = column_labels.index("Time")
author_col = column_labels.index("Author")
frame_range_col = column_labels.index("Frames")
duration_col = column_labels.index("Duration")
handles_col = column_labels.index("Handles")
step_col = column_labels.index("Step")
in_scene_col = column_labels.index("In scene") in_scene_col = column_labels.index("In scene")
sitesync_avail_col = column_labels.index("Availability") sitesync_avail_col = column_labels.index("Availability")
_display_role_mapping = {
product_name_col: QtCore.Qt.DisplayRole,
product_type_col: PRODUCT_TYPE_ROLE,
folders_label_col: FOLDER_LABEL_ROLE,
version_col: VERSION_NAME_ROLE,
status_col: VERSION_STATUS_NAME_ROLE,
published_time_col: VERSION_PUBLISH_TIME_ROLE,
author_col: VERSION_AUTHOR_ROLE,
frame_range_col: VERSION_FRAME_RANGE_ROLE,
duration_col: VERSION_DURATION_ROLE,
handles_col: VERSION_HANDLES_ROLE,
step_col: VERSION_STEP_ROLE,
in_scene_col: PRODUCT_IN_SCENE_ROLE,
sitesync_avail_col: VERSION_AVAILABLE_ROLE,
}
def __init__(self, controller): def __init__(self, controller):
super(ProductsModel, self).__init__() super(ProductsModel, self).__init__()
@ -96,6 +124,7 @@ class ProductsModel(QtGui.QStandardItemModel):
self._last_project_name = None self._last_project_name = None
self._last_folder_ids = [] self._last_folder_ids = []
self._last_project_statuses = {}
def get_product_item_indexes(self): def get_product_item_indexes(self):
return [ return [
@ -141,6 +170,15 @@ class ProductsModel(QtGui.QStandardItemModel):
if not index.isValid(): if not index.isValid():
return None return None
if role in (VERSION_STATUS_SHORT_ROLE, VERSION_STATUS_COLOR_ROLE):
status_name = self.data(index, VERSION_STATUS_NAME_ROLE)
status_item = self._last_project_statuses.get(status_name)
if status_item is None:
return ""
if role == VERSION_STATUS_SHORT_ROLE:
return status_item.short
return status_item.color
col = index.column() col = index.column()
if col == 0: if col == 0:
return super(ProductsModel, self).data(index, role) return super(ProductsModel, self).data(index, role)
@ -168,29 +206,8 @@ class ProductsModel(QtGui.QStandardItemModel):
if role == QtCore.Qt.DisplayRole: if role == QtCore.Qt.DisplayRole:
if not index.data(PRODUCT_ID_ROLE): if not index.data(PRODUCT_ID_ROLE):
return None return None
if col == self.version_col: role = self._display_role_mapping.get(col)
role = VERSION_NAME_ROLE if role is None:
elif col == 1:
role = PRODUCT_TYPE_ROLE
elif col == 2:
role = FOLDER_LABEL_ROLE
elif col == 4:
role = VERSION_PUBLISH_TIME_ROLE
elif col == 5:
role = VERSION_AUTHOR_ROLE
elif col == 6:
role = VERSION_FRAME_RANGE_ROLE
elif col == 7:
role = VERSION_DURATION_ROLE
elif col == 8:
role = VERSION_HANDLES_ROLE
elif col == 9:
role = VERSION_STEP_ROLE
elif col == 10:
role = PRODUCT_IN_SCENE_ROLE
elif col == 11:
role = VERSION_AVAILABLE_ROLE
else:
return None return None
index = self.index(index.row(), 0, index.parent()) index = self.index(index.row(), 0, index.parent())
@ -312,6 +329,7 @@ class ProductsModel(QtGui.QStandardItemModel):
version_item.published_time, VERSION_PUBLISH_TIME_ROLE version_item.published_time, VERSION_PUBLISH_TIME_ROLE
) )
model_item.setData(version_item.author, VERSION_AUTHOR_ROLE) model_item.setData(version_item.author, VERSION_AUTHOR_ROLE)
model_item.setData(version_item.status, VERSION_STATUS_NAME_ROLE)
model_item.setData(version_item.frame_range, VERSION_FRAME_RANGE_ROLE) model_item.setData(version_item.frame_range, VERSION_FRAME_RANGE_ROLE)
model_item.setData(version_item.duration, VERSION_DURATION_ROLE) model_item.setData(version_item.duration, VERSION_DURATION_ROLE)
model_item.setData(version_item.handles, VERSION_HANDLES_ROLE) model_item.setData(version_item.handles, VERSION_HANDLES_ROLE)
@ -393,6 +411,11 @@ class ProductsModel(QtGui.QStandardItemModel):
self._last_project_name = project_name self._last_project_name = project_name
self._last_folder_ids = folder_ids self._last_folder_ids = folder_ids
status_items = self._controller.get_project_status_items(project_name)
self._last_project_statuses = {
status_item.name: status_item
for status_item in status_items
}
active_site_icon_def = self._controller.get_active_site_icon_def( active_site_icon_def = self._controller.get_active_site_icon_def(
project_name project_name

View file

@ -22,7 +22,8 @@ from .products_model import (
from .products_delegates import ( from .products_delegates import (
VersionDelegate, VersionDelegate,
LoadedInSceneDelegate, LoadedInSceneDelegate,
SiteSyncDelegate StatusDelegate,
SiteSyncDelegate,
) )
from .actions_utils import show_actions_menu from .actions_utils import show_actions_menu
@ -89,6 +90,7 @@ class ProductsWidget(QtWidgets.QWidget):
90, # Product type 90, # Product type
130, # Folder label 130, # Folder label
60, # Version 60, # Version
100, # Status
125, # Time 125, # Time
75, # Author 75, # Author
75, # Frames 75, # Frames
@ -128,20 +130,19 @@ class ProductsWidget(QtWidgets.QWidget):
products_view.setColumnWidth(idx, width) products_view.setColumnWidth(idx, width)
version_delegate = VersionDelegate() version_delegate = VersionDelegate()
products_view.setItemDelegateForColumn(
products_model.version_col, version_delegate)
time_delegate = PrettyTimeDelegate() time_delegate = PrettyTimeDelegate()
products_view.setItemDelegateForColumn( status_delegate = StatusDelegate()
products_model.published_time_col, time_delegate)
in_scene_delegate = LoadedInSceneDelegate() in_scene_delegate = LoadedInSceneDelegate()
products_view.setItemDelegateForColumn(
products_model.in_scene_col, in_scene_delegate)
sitesync_delegate = SiteSyncDelegate() sitesync_delegate = SiteSyncDelegate()
products_view.setItemDelegateForColumn(
products_model.sitesync_avail_col, sitesync_delegate) for col, delegate in (
(products_model.version_col, version_delegate),
(products_model.published_time_col, time_delegate),
(products_model.status_col, status_delegate),
(products_model.in_scene_col, in_scene_delegate),
(products_model.sitesync_avail_col, sitesync_delegate),
):
products_view.setItemDelegateForColumn(col, delegate)
main_layout = QtWidgets.QHBoxLayout(self) main_layout = QtWidgets.QHBoxLayout(self)
main_layout.setContentsMargins(0, 0, 0, 0) main_layout.setContentsMargins(0, 0, 0, 0)
@ -175,6 +176,7 @@ class ProductsWidget(QtWidgets.QWidget):
self._version_delegate = version_delegate self._version_delegate = version_delegate
self._time_delegate = time_delegate self._time_delegate = time_delegate
self._status_delegate = status_delegate
self._in_scene_delegate = in_scene_delegate self._in_scene_delegate = in_scene_delegate
self._sitesync_delegate = sitesync_delegate self._sitesync_delegate = sitesync_delegate

View file

@ -1,3 +1,3 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""Package declaring AYON core addon version.""" """Package declaring AYON core addon version."""
__version__ = "0.3.2-dev.1" __version__ = "0.3.3-dev.1"

View file

@ -1,6 +1,6 @@
name = "core" name = "core"
title = "Core" title = "Core"
version = "0.3.2-dev.1" version = "0.3.3-dev.1"
client_dir = "ayon_core" client_dir = "ayon_core"

View file

@ -1,3 +1,3 @@
name = "blender" name = "blender"
title = "Blender" title = "Blender"
version = "0.1.8" version = "0.1.9"

View file

@ -151,6 +151,10 @@ class PublishPluginsModel(BaseSettingsModel):
default_factory=ExtractPlayblastModel, default_factory=ExtractPlayblastModel,
title="Extract Playblast" title="Extract Playblast"
) )
ExtractModelUSD: ValidatePluginModel = SettingsField(
default_factory=ValidatePluginModel,
title="Extract Model USD"
)
DEFAULT_BLENDER_PUBLISH_SETTINGS = { DEFAULT_BLENDER_PUBLISH_SETTINGS = {
@ -348,5 +352,10 @@ DEFAULT_BLENDER_PUBLISH_SETTINGS = {
}, },
indent=4 indent=4
) )
},
"ExtractModelUSD": {
"enabled": True,
"optional": True,
"active": True
} }
} }

View file

@ -0,0 +1,5 @@
from .addon import ClockifyAddon
__all__ = (
"ClockifyAddon",
)

View file

@ -2,12 +2,12 @@ import os
import threading import threading
import time import time
from ayon_core.modules import AYONAddon, ITrayModule, IPluginPaths from ayon_core.addon import AYONAddon, ITrayAddon, IPluginPaths
from .constants import CLOCKIFY_FTRACK_USER_PATH, CLOCKIFY_FTRACK_SERVER_PATH from .constants import CLOCKIFY_FTRACK_USER_PATH, CLOCKIFY_FTRACK_SERVER_PATH
class ClockifyModule(AYONAddon, ITrayModule, IPluginPaths): class ClockifyAddon(AYONAddon, ITrayAddon, IPluginPaths):
name = "clockify" name = "clockify"
def initialize(self, studio_settings): def initialize(self, studio_settings):
@ -31,7 +31,7 @@ class ClockifyModule(AYONAddon, ITrayModule, IPluginPaths):
# TimersManager attributes # TimersManager attributes
# - set `timers_manager_connector` only in `tray_init` # - set `timers_manager_connector` only in `tray_init`
self.timers_manager_connector = None self.timers_manager_connector = None
self._timers_manager_module = None self._timer_manager_addon = None
@property @property
def clockify_api(self): def clockify_api(self):
@ -87,7 +87,7 @@ class ClockifyModule(AYONAddon, ITrayModule, IPluginPaths):
return {"actions": [actions_path]} return {"actions": [actions_path]}
def get_ftrack_event_handler_paths(self): def get_ftrack_event_handler_paths(self):
"""Function for Ftrack module to add ftrack event handler paths.""" """Function for ftrack addon to add ftrack event handler paths."""
return { return {
"user": [CLOCKIFY_FTRACK_USER_PATH], "user": [CLOCKIFY_FTRACK_USER_PATH],
"server": [CLOCKIFY_FTRACK_SERVER_PATH], "server": [CLOCKIFY_FTRACK_SERVER_PATH],
@ -206,19 +206,19 @@ class ClockifyModule(AYONAddon, ITrayModule, IPluginPaths):
self.action_stop_timer.setVisible(self.bool_timer_run) self.action_stop_timer.setVisible(self.bool_timer_run)
# --- TimersManager connection methods --- # --- TimersManager connection methods ---
def register_timers_manager(self, timer_manager_module): def register_timers_manager(self, timer_manager_addon):
"""Store TimersManager for future use.""" """Store TimersManager for future use."""
self._timers_manager_module = timer_manager_module self._timer_manager_addon = timer_manager_addon
def timer_started(self, data): def timer_started(self, data):
"""Tell TimersManager that timer started.""" """Tell TimersManager that timer started."""
if self._timers_manager_module is not None: if self._timer_manager_addon is not None:
self._timers_manager_module.timer_started(self.id, data) self._timer_manager_addon.timer_started(self.id, data)
def timer_stopped(self): def timer_stopped(self):
"""Tell TimersManager that timer stopped.""" """Tell TimersManager that timer stopped."""
if self._timers_manager_module is not None: if self._timer_manager_addon is not None:
self._timers_manager_module.timer_stopped(self.id) self._timer_manager_addon.timer_stopped(self.id)
def stop_timer(self): def stop_timer(self):
"""Called from TimersManager to stop timer.""" """Called from TimersManager to stop timer."""

View file

@ -1,15 +1,17 @@
import os import os
import json import json
import datetime import datetime
import requests import requests
from ayon_core.lib.local_settings import AYONSecureRegistry
from ayon_core.lib import Logger
from .constants import ( from .constants import (
CLOCKIFY_ENDPOINT, CLOCKIFY_ENDPOINT,
ADMIN_PERMISSION_NAMES, ADMIN_PERMISSION_NAMES,
) )
from ayon_core.lib.local_settings import AYONSecureRegistry
from ayon_core.lib import Logger
class ClockifyAPI: class ClockifyAPI:
log = Logger.get_logger(__name__) log = Logger.get_logger(__name__)

View file

@ -1,7 +1,9 @@
import os import os
import json import json
from openpype_modules.ftrack.lib import ServerAction
from openpype_modules.clockify.clockify_api import ClockifyAPI from ayon_clockify.clockify_api import ClockifyAPI
from ayon_ftrack.lib import ServerAction
class SyncClockifyServer(ServerAction): class SyncClockifyServer(ServerAction):

View file

@ -1,25 +1,20 @@
import json import json
from openpype_modules.ftrack.lib import BaseAction, statics_icon from ayon_clockify.clockify_api import ClockifyAPI
from openpype_modules.clockify.clockify_api import ClockifyAPI from ayon_ftrack.lib import BaseAction, statics_icon
class SyncClockifyLocal(BaseAction): class SyncClockifyLocal(BaseAction):
'''Synchronise project names and task types.''' """Synchronise project names and task types."""
#: Action identifier. identifier = "clockify.sync.local"
identifier = 'clockify.sync.local' label = "Sync To Clockify"
#: Action label. description = "Synchronise data to Clockify workspace"
label = 'Sync To Clockify (local)'
#: Action description.
description = 'Synchronise data to Clockify workspace'
#: roles that are allowed to register this action
role_list = ["Administrator", "project Manager"] role_list = ["Administrator", "project Manager"]
#: icon
icon = statics_icon("app_icons", "clockify-white.png") icon = statics_icon("app_icons", "clockify-white.png")
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(SyncClockifyLocal, self).__init__(*args, **kwargs) super(SyncClockifyLocal, self).__init__(*args, **kwargs)
#: CLockifyApi
self.clockify_api = ClockifyAPI() self.clockify_api = ClockifyAPI()
def discover(self, session, entities, event): def discover(self, session, entities, event):
@ -56,7 +51,7 @@ class SyncClockifyLocal(BaseAction):
'user': user, 'user': user,
'status': 'running', 'status': 'running',
'data': json.dumps({ 'data': json.dumps({
'description': 'Sync Ftrack to Clockify' 'description': 'Sync ftrack to Clockify'
}) })
}) })
session.commit() session.commit()

View file

@ -1,7 +1,8 @@
import ayon_api import ayon_api
from ayon_clockify.clockify_api import ClockifyAPI
from ayon_core.pipeline import LauncherAction from ayon_core.pipeline import LauncherAction
from openpype_modules.clockify.clockify_api import ClockifyAPI
class ClockifyStart(LauncherAction): class ClockifyStart(LauncherAction):

View file

@ -1,6 +1,6 @@
import ayon_api import ayon_api
from openpype_modules.clockify.clockify_api import ClockifyAPI from ayon_clockify.clockify_api import ClockifyAPI
from ayon_core.pipeline import LauncherAction from ayon_core.pipeline import LauncherAction

View file

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
"""Package declaring AYON addon 'clockify' version."""
__version__ = "0.2.0"

View file

@ -1,3 +1,9 @@
name = "clockify" name = "clockify"
title = "Clockify" title = "Clockify"
version = "0.1.1" version = "0.2.0"
client_dir = "ayon_clockify"
ayon_required_addons = {
"core": ">0.3.2",
}
ayon_compatible_addons = {}

View file

@ -47,7 +47,7 @@ plugin_for = ["ayon_server"]
""" """
CLIENT_VERSION_CONTENT = '''# -*- coding: utf-8 -*- CLIENT_VERSION_CONTENT = '''# -*- coding: utf-8 -*-
"""Package declaring AYON core addon version.""" """Package declaring AYON addon '{}' version."""
__version__ = "{}" __version__ = "{}"
''' '''
@ -183,6 +183,7 @@ def create_addon_zip(
def prepare_client_code( def prepare_client_code(
addon_name: str,
addon_dir: Path, addon_dir: Path,
addon_output_dir: Path, addon_output_dir: Path,
addon_version: str addon_version: str
@ -211,7 +212,9 @@ def prepare_client_code(
version_path = subpath / "version.py" version_path = subpath / "version.py"
if version_path.exists(): if version_path.exists():
with open(version_path, "w") as stream: with open(version_path, "w") as stream:
stream.write(CLIENT_VERSION_CONTENT.format(addon_version)) stream.write(
CLIENT_VERSION_CONTENT.format(addon_name, addon_version)
)
zip_filepath = private_dir / "client.zip" zip_filepath = private_dir / "client.zip"
with ZipFileLongPaths(zip_filepath, "w", zipfile.ZIP_DEFLATED) as zipf: with ZipFileLongPaths(zip_filepath, "w", zipfile.ZIP_DEFLATED) as zipf:
@ -262,7 +265,9 @@ def create_addon_package(
server_dir, addon_output_dir / "server", dirs_exist_ok=True server_dir, addon_output_dir / "server", dirs_exist_ok=True
) )
prepare_client_code(addon_dir, addon_output_dir, addon_version) prepare_client_code(
package.name, addon_dir, addon_output_dir, addon_version
)
if create_zip: if create_zip:
create_addon_zip( create_addon_zip(

View file

@ -57,6 +57,9 @@ class CreatePluginsModel(BaseSettingsModel):
CreateMantraROP: CreatorModel = SettingsField( CreateMantraROP: CreatorModel = SettingsField(
default_factory=CreatorModel, default_factory=CreatorModel,
title="Create Mantra ROP") title="Create Mantra ROP")
CreateModel: CreatorModel = SettingsField(
default_factory=CreatorModel,
title="Create Model")
CreatePointCache: CreatorModel = SettingsField( CreatePointCache: CreatorModel = SettingsField(
default_factory=CreatorModel, default_factory=CreatorModel,
title="Create PointCache (Abc)") title="Create PointCache (Abc)")
@ -124,6 +127,10 @@ DEFAULT_HOUDINI_CREATE_SETTINGS = {
"enabled": True, "enabled": True,
"default_variants": ["Main"] "default_variants": ["Main"]
}, },
"CreateModel": {
"enabled": True,
"default_variants": ["Main"]
},
"CreatePointCache": { "CreatePointCache": {
"enabled": True, "enabled": True,
"default_variants": ["Main"] "default_variants": ["Main"]

View file

@ -354,7 +354,7 @@ def imprint(node, data, tab=None):
Examples: Examples:
``` ```
import nuke import nuke
from ayon_core.hosts.nuke.api import lib from ayon_nuke.api import lib
node = nuke.createNode("NoOp") node = nuke.createNode("NoOp")
data = { data = {
@ -419,7 +419,7 @@ def add_publish_knob(node):
return node return node
@deprecated("ayon_core.hosts.nuke.api.lib.set_node_data") @deprecated("ayon_nuke.api.lib.set_node_data")
def set_avalon_knob_data(node, data=None, prefix="avalon:"): def set_avalon_knob_data(node, data=None, prefix="avalon:"):
"""[DEPRECATED] Sets data into nodes's avalon knob """[DEPRECATED] Sets data into nodes's avalon knob
@ -485,7 +485,7 @@ def set_avalon_knob_data(node, data=None, prefix="avalon:"):
return node return node
@deprecated("ayon_core.hosts.nuke.api.lib.get_node_data") @deprecated("ayon_nuke.api.lib.get_node_data")
def get_avalon_knob_data(node, prefix="avalon:", create=True): def get_avalon_knob_data(node, prefix="avalon:", create=True):
"""[DEPRECATED] Gets a data from nodes's avalon knob """[DEPRECATED] Gets a data from nodes's avalon knob
@ -1024,6 +1024,18 @@ def script_name():
return nuke.root().knob("name").value() return nuke.root().knob("name").value()
def add_button_render_on_farm(node):
name = "renderOnFarm"
label = "Render On Farm"
value = (
"from ayon_nuke.api.utils import submit_render_on_farm;"
"submit_render_on_farm(nuke.thisNode())"
)
knob = nuke.PyScript_Knob(name, label, value)
knob.clearFlag(nuke.STARTLINE)
node.addKnob(knob)
def add_button_write_to_read(node): def add_button_write_to_read(node):
name = "createReadNode" name = "createReadNode"
label = "Read From Rendered" label = "Read From Rendered"
@ -1146,6 +1158,17 @@ def create_write_node(
Return: Return:
node (obj): group node with avalon data as Knobs node (obj): group node with avalon data as Knobs
''' '''
# Ensure name does not contain any invalid characters.
special_chars = re.escape("!@#$%^&*()=[]{}|\\;',.<>/?~+-")
special_chars_regex = re.compile(f"[{special_chars}]")
found_special_characters = list(special_chars_regex.findall(name))
msg = (
f"Special characters found in name \"{name}\": "
f"{' '.join(found_special_characters)}"
)
assert not found_special_characters, msg
prenodes = prenodes or [] prenodes = prenodes or []
# filtering variables # filtering variables
@ -1270,6 +1293,10 @@ def create_write_node(
link.setFlag(0x1000) link.setFlag(0x1000)
GN.addKnob(link) GN.addKnob(link)
# Adding render farm submission button.
if data.get("render_on_farm", False):
add_button_render_on_farm(GN)
# adding write to read button # adding write to read button
add_button_write_to_read(GN) add_button_write_to_read(GN)
@ -2442,7 +2469,7 @@ def _launch_workfile_app():
host_tools.show_workfiles(parent=None, on_top=True) host_tools.show_workfiles(parent=None, on_top=True)
@deprecated("ayon_core.hosts.nuke.api.lib.start_workfile_template_builder") @deprecated("ayon_nuke.api.lib.start_workfile_template_builder")
def process_workfile_builder(): def process_workfile_builder():
""" [DEPRECATED] Process workfile builder on nuke start """ [DEPRECATED] Process workfile builder on nuke start

View file

@ -28,7 +28,7 @@ from ayon_core.pipeline import (
) )
from ayon_core.pipeline.workfile import BuildWorkfile from ayon_core.pipeline.workfile import BuildWorkfile
from ayon_core.tools.utils import host_tools from ayon_core.tools.utils import host_tools
from ayon_core.hosts.nuke import NUKE_ROOT_DIR from ayon_nuke import NUKE_ROOT_DIR
from ayon_core.tools.workfile_template_build import open_template_ui from ayon_core.tools.workfile_template_build import open_template_ui
from .lib import ( from .lib import (
@ -188,10 +188,10 @@ def reload_config():
""" """
for module in ( for module in (
"ayon_core.hosts.nuke.api.actions", "ayon_nuke.api.actions",
"ayon_core.hosts.nuke.api.menu", "ayon_nuke.api.menu",
"ayon_core.hosts.nuke.api.plugin", "ayon_nuke.api.plugin",
"ayon_core.hosts.nuke.api.lib", "ayon_nuke.api.lib",
): ):
log.info("Reloading module: {}...".format(module)) log.info("Reloading module: {}...".format(module))

View file

@ -572,8 +572,11 @@ class ExporterReview(object):
self.fhead = self.fhead.replace("#", "")[:-1] self.fhead = self.fhead.replace("#", "")[:-1]
def get_representation_data( def get_representation_data(
self, tags=None, range=False, self,
custom_tags=None, colorspace=None tags=None,
range=False,
custom_tags=None,
colorspace=None,
): ):
""" Add representation data to self.data """ Add representation data to self.data
@ -584,6 +587,8 @@ class ExporterReview(object):
Defaults to False. Defaults to False.
custom_tags (list[str], optional): user inputted custom tags. custom_tags (list[str], optional): user inputted custom tags.
Defaults to None. Defaults to None.
colorspace (str, optional): colorspace name.
Defaults to None.
""" """
add_tags = tags or [] add_tags = tags or []
repre = { repre = {
@ -591,7 +596,13 @@ class ExporterReview(object):
"ext": self.ext, "ext": self.ext,
"files": self.file, "files": self.file,
"stagingDir": self.staging_dir, "stagingDir": self.staging_dir,
"tags": [self.name.replace("_", "-")] + add_tags "tags": [self.name.replace("_", "-")] + add_tags,
"data": {
# making sure that once intermediate file is published
# as representation, we will be able to then identify it
# from representation.data.isIntermediate
"isIntermediate": True
},
} }
if custom_tags: if custom_tags:
@ -1000,7 +1011,7 @@ class ExporterReviewMov(ExporterReview):
tags=tags + add_tags, tags=tags + add_tags,
custom_tags=add_custom_tags, custom_tags=add_custom_tags,
range=True, range=True,
colorspace=colorspace colorspace=colorspace,
) )
self.log.debug("Representation... `{}`".format(self.data)) self.log.debug("Representation... `{}`".format(self.data))
@ -1039,7 +1050,7 @@ def convert_to_valid_instaces():
} }
return mapping[product_type] return mapping[product_type]
from ayon_core.hosts.nuke.api import workio from ayon_nuke.api import workio
task_name = get_current_task_name() task_name = get_current_task_name()

View file

@ -3,9 +3,15 @@ import re
import nuke import nuke
from ayon_core import resources import pyblish.util
import pyblish.api
from qtpy import QtWidgets from qtpy import QtWidgets
from ayon_core import resources
from ayon_core.pipeline import registered_host
from ayon_core.tools.utils import show_message_dialog
from ayon_core.pipeline.create import CreateContext
def set_context_favorites(favorites=None): def set_context_favorites(favorites=None):
""" Adding favorite folders to nuke's browser """ Adding favorite folders to nuke's browser
@ -142,3 +148,77 @@ def is_headless():
bool: headless bool: headless
""" """
return QtWidgets.QApplication.instance() is None return QtWidgets.QApplication.instance() is None
def submit_render_on_farm(node):
# Ensure code is executed in root context.
if nuke.root() == nuke.thisNode():
_submit_render_on_farm(node)
else:
# If not in root context, move to the root context and then execute the
# code.
with nuke.root():
_submit_render_on_farm(node)
def _submit_render_on_farm(node):
"""Render on farm submission
This function prepares the context for farm submission, validates it,
extracts relevant data, copies the current workfile to a timestamped copy,
and submits the job to the farm.
Args:
node (Node): The node for which the farm submission is being made.
"""
host = registered_host()
create_context = CreateContext(host)
# Ensure CreateInstance is enabled.
for instance in create_context.instances:
if node.name() != instance.transient_data["node"].name():
continue
instance.data["active"] = True
context = pyblish.api.Context()
context.data["create_context"] = create_context
# Used in pyblish plugin to determine which instance to publish.
context.data["node_name"] = node.name()
# Used in pyblish plugins to determine whether to run or not.
context.data["render_on_farm"] = True
# Since we need to bypass version validation and incrementing, we need to
# remove the plugins from the list that are responsible for these tasks.
plugins = pyblish.api.discover()
blacklist = ["IncrementScriptVersion", "ValidateVersion"]
plugins = [
plugin
for plugin in plugins
if plugin.__name__ not in blacklist
]
context = pyblish.util.publish(context, plugins=plugins)
error_message = ""
success = True
for result in context.data["results"]:
if result["success"]:
continue
success = False
err = result["error"]
error_message += "\n"
error_message += err.formatted_traceback
if not success:
show_message_dialog(
"Publish Errors", error_message, level="critical"
)
return
show_message_dialog(
"Submission Successful", "Submission to the farm was successful."
)

View file

@ -1,12 +1,12 @@
from ayon_core.pipeline import AYON_INSTANCE_ID, AVALON_INSTANCE_ID from ayon_core.pipeline import AYON_INSTANCE_ID, AVALON_INSTANCE_ID
from ayon_core.pipeline.create.creator_plugins import ProductConvertorPlugin from ayon_core.pipeline.create.creator_plugins import ProductConvertorPlugin
from ayon_core.hosts.nuke.api.lib import ( from ayon_nuke.api.lib import (
INSTANCE_DATA_KNOB, INSTANCE_DATA_KNOB,
get_node_data, get_node_data,
get_avalon_knob_data, get_avalon_knob_data,
NODE_TAB_NAME, NODE_TAB_NAME,
) )
from ayon_core.hosts.nuke.api.plugin import convert_to_valid_instaces from ayon_nuke.api.plugin import convert_to_valid_instaces
import nuke import nuke

View file

@ -1,6 +1,6 @@
from nukescripts import autoBackdrop from nukescripts import autoBackdrop
from ayon_core.hosts.nuke.api import ( from ayon_nuke.api import (
NukeCreator, NukeCreator,
maintained_selection, maintained_selection,
select_nodes select_nodes
@ -10,6 +10,8 @@ from ayon_core.hosts.nuke.api import (
class CreateBackdrop(NukeCreator): class CreateBackdrop(NukeCreator):
"""Add Publishable Backdrop""" """Add Publishable Backdrop"""
settings_category = "nuke"
identifier = "create_backdrop" identifier = "create_backdrop"
label = "Nukenodes (backdrop)" label = "Nukenodes (backdrop)"
product_type = "nukenodes" product_type = "nukenodes"

View file

@ -1,10 +1,10 @@
import nuke import nuke
from ayon_core.hosts.nuke.api import ( from ayon_nuke.api import (
NukeCreator, NukeCreator,
NukeCreatorError, NukeCreatorError,
maintained_selection maintained_selection
) )
from ayon_core.hosts.nuke.api.lib import ( from ayon_nuke.api.lib import (
create_camera_node_by_version create_camera_node_by_version
) )
@ -12,6 +12,8 @@ from ayon_core.hosts.nuke.api.lib import (
class CreateCamera(NukeCreator): class CreateCamera(NukeCreator):
"""Add Publishable Camera""" """Add Publishable Camera"""
settings_category = "nuke"
identifier = "create_camera" identifier = "create_camera"
label = "Camera (3d)" label = "Camera (3d)"
product_type = "camera" product_type = "camera"

View file

@ -1,5 +1,5 @@
import nuke import nuke
from ayon_core.hosts.nuke.api import ( from ayon_nuke.api import (
NukeCreator, NukeCreator,
NukeCreatorError, NukeCreatorError,
maintained_selection maintained_selection
@ -9,6 +9,8 @@ from ayon_core.hosts.nuke.api import (
class CreateGizmo(NukeCreator): class CreateGizmo(NukeCreator):
"""Add Publishable Group as gizmo""" """Add Publishable Group as gizmo"""
settings_category = "nuke"
identifier = "create_gizmo" identifier = "create_gizmo"
label = "Gizmo (group)" label = "Gizmo (group)"
product_type = "gizmo" product_type = "gizmo"

View file

@ -1,5 +1,5 @@
import nuke import nuke
from ayon_core.hosts.nuke.api import ( from ayon_nuke.api import (
NukeCreator, NukeCreator,
NukeCreatorError, NukeCreatorError,
maintained_selection maintained_selection
@ -9,6 +9,8 @@ from ayon_core.hosts.nuke.api import (
class CreateModel(NukeCreator): class CreateModel(NukeCreator):
"""Add Publishable Camera""" """Add Publishable Camera"""
settings_category = "nuke"
identifier = "create_model" identifier = "create_model"
label = "Model (3d)" label = "Model (3d)"
product_type = "model" product_type = "model"

View file

@ -1,7 +1,7 @@
import nuke import nuke
import six import six
import sys import sys
from ayon_core.hosts.nuke.api import ( from ayon_nuke.api import (
INSTANCE_DATA_KNOB, INSTANCE_DATA_KNOB,
NukeCreator, NukeCreator,
NukeCreatorError, NukeCreatorError,
@ -15,6 +15,8 @@ from ayon_core.pipeline import (
class CreateSource(NukeCreator): class CreateSource(NukeCreator):
"""Add Publishable Read with source""" """Add Publishable Read with source"""
settings_category = "nuke"
identifier = "create_source" identifier = "create_source"
label = "Source (read)" label = "Source (read)"
product_type = "source" product_type = "source"

View file

@ -11,11 +11,14 @@ from ayon_core.lib import (
UISeparatorDef, UISeparatorDef,
EnumDef EnumDef
) )
from ayon_core.hosts.nuke import api as napi from ayon_nuke import api as napi
from ayon_core.hosts.nuke.api.plugin import exposed_write_knobs from ayon_nuke.api.plugin import exposed_write_knobs
class CreateWriteImage(napi.NukeWriteCreator): class CreateWriteImage(napi.NukeWriteCreator):
settings_category = "nuke"
identifier = "create_write_image" identifier = "create_write_image"
label = "Image (write)" label = "Image (write)"
product_type = "image" product_type = "image"
@ -65,12 +68,16 @@ class CreateWriteImage(napi.NukeWriteCreator):
) )
def create_instance_node(self, product_name, instance_data): def create_instance_node(self, product_name, instance_data):
settings = self.project_settings["nuke"]["create"]["CreateWriteImage"]
# add fpath_template # add fpath_template
write_data = { write_data = {
"creator": self.__class__.__name__, "creator": self.__class__.__name__,
"productName": product_name, "productName": product_name,
"fpath_template": self.temp_rendering_path_template "fpath_template": self.temp_rendering_path_template,
"render_on_farm": (
"render_on_farm" in settings["instance_attributes"]
)
} }
write_data.update(instance_data) write_data.update(instance_data)

View file

@ -8,11 +8,14 @@ from ayon_core.pipeline import (
from ayon_core.lib import ( from ayon_core.lib import (
BoolDef BoolDef
) )
from ayon_core.hosts.nuke import api as napi from ayon_nuke import api as napi
from ayon_core.hosts.nuke.api.plugin import exposed_write_knobs from ayon_nuke.api.plugin import exposed_write_knobs
class CreateWritePrerender(napi.NukeWriteCreator): class CreateWritePrerender(napi.NukeWriteCreator):
settings_category = "nuke"
identifier = "create_write_prerender" identifier = "create_write_prerender"
label = "Prerender (write)" label = "Prerender (write)"
product_type = "prerender" product_type = "prerender"
@ -46,11 +49,17 @@ class CreateWritePrerender(napi.NukeWriteCreator):
return attr_defs return attr_defs
def create_instance_node(self, product_name, instance_data): def create_instance_node(self, product_name, instance_data):
settings = self.project_settings["nuke"]["create"]
settings = settings["CreateWritePrerender"]
# add fpath_template # add fpath_template
write_data = { write_data = {
"creator": self.__class__.__name__, "creator": self.__class__.__name__,
"productName": product_name, "productName": product_name,
"fpath_template": self.temp_rendering_path_template "fpath_template": self.temp_rendering_path_template,
"render_on_farm": (
"render_on_farm" in settings["instance_attributes"]
)
} }
write_data.update(instance_data) write_data.update(instance_data)

View file

@ -8,11 +8,14 @@ from ayon_core.pipeline import (
from ayon_core.lib import ( from ayon_core.lib import (
BoolDef BoolDef
) )
from ayon_core.hosts.nuke import api as napi from ayon_nuke import api as napi
from ayon_core.hosts.nuke.api.plugin import exposed_write_knobs from ayon_nuke.api.plugin import exposed_write_knobs
class CreateWriteRender(napi.NukeWriteCreator): class CreateWriteRender(napi.NukeWriteCreator):
settings_category = "nuke"
identifier = "create_write_render" identifier = "create_write_render"
label = "Render (write)" label = "Render (write)"
product_type = "render" product_type = "render"
@ -40,11 +43,16 @@ class CreateWriteRender(napi.NukeWriteCreator):
return attr_defs return attr_defs
def create_instance_node(self, product_name, instance_data): def create_instance_node(self, product_name, instance_data):
settings = self.project_settings["nuke"]["create"]["CreateWriteRender"]
# add fpath_template # add fpath_template
write_data = { write_data = {
"creator": self.__class__.__name__, "creator": self.__class__.__name__,
"productName": product_name, "productName": product_name,
"fpath_template": self.temp_rendering_path_template "fpath_template": self.temp_rendering_path_template,
"render_on_farm": (
"render_on_farm" in settings["instance_attributes"]
)
} }
write_data.update(instance_data) write_data.update(instance_data)

View file

@ -1,11 +1,11 @@
import ayon_api import ayon_api
import ayon_core.hosts.nuke.api as api import ayon_nuke.api as api
from ayon_core.pipeline import ( from ayon_core.pipeline import (
AutoCreator, AutoCreator,
CreatedInstance, CreatedInstance,
) )
from ayon_core.hosts.nuke.api import ( from ayon_nuke.api import (
INSTANCE_DATA_KNOB, INSTANCE_DATA_KNOB,
set_node_data set_node_data
) )
@ -13,6 +13,9 @@ import nuke
class WorkfileCreator(AutoCreator): class WorkfileCreator(AutoCreator):
settings_category = "nuke"
identifier = "workfile" identifier = "workfile"
product_type = "workfile" product_type = "workfile"

View file

@ -1,6 +1,6 @@
from ayon_core.lib import Logger from ayon_core.lib import Logger
from ayon_core.pipeline import InventoryAction from ayon_core.pipeline import InventoryAction
from ayon_core.hosts.nuke.api.lib import set_avalon_knob_data from ayon_nuke.api.lib import set_avalon_knob_data
class RepairOldLoaders(InventoryAction): class RepairOldLoaders(InventoryAction):

View file

@ -1,5 +1,5 @@
from ayon_core.pipeline import InventoryAction from ayon_core.pipeline import InventoryAction
from ayon_core.hosts.nuke.api.command import viewer_update_and_undo_stop from ayon_nuke.api.command import viewer_update_and_undo_stop
class SelectContainers(InventoryAction): class SelectContainers(InventoryAction):

View file

@ -4,7 +4,7 @@
from ayon_core.lib import Logger from ayon_core.lib import Logger
from ayon_core.pipeline import load from ayon_core.pipeline import load
from ayon_core.hosts.nuke.api import lib from ayon_nuke.api import lib
log = Logger.get_logger(__name__) log = Logger.get_logger(__name__)

View file

@ -6,7 +6,7 @@ from ayon_core.pipeline import (
load, load,
get_representation_path, get_representation_path,
) )
from ayon_core.hosts.nuke.api.lib import ( from ayon_nuke.api.lib import (
find_free_space_to_paste_nodes, find_free_space_to_paste_nodes,
maintained_selection, maintained_selection,
reset_selection, reset_selection,
@ -14,8 +14,8 @@ from ayon_core.hosts.nuke.api.lib import (
get_avalon_knob_data, get_avalon_knob_data,
set_avalon_knob_data set_avalon_knob_data
) )
from ayon_core.hosts.nuke.api.command import viewer_update_and_undo_stop from ayon_nuke.api.command import viewer_update_and_undo_stop
from ayon_core.hosts.nuke.api import containerise, update_container from ayon_nuke.api import containerise, update_container
class LoadBackdropNodes(load.LoaderPlugin): class LoadBackdropNodes(load.LoaderPlugin):
@ -25,6 +25,8 @@ class LoadBackdropNodes(load.LoaderPlugin):
representations = {"*"} representations = {"*"}
extensions = {"nk"} extensions = {"nk"}
settings_category = "nuke"
label = "Import Nuke Nodes" label = "Import Nuke Nodes"
order = 0 order = 0
icon = "eye" icon = "eye"

View file

@ -5,12 +5,12 @@ from ayon_core.pipeline import (
load, load,
get_representation_path, get_representation_path,
) )
from ayon_core.hosts.nuke.api import ( from ayon_nuke.api import (
containerise, containerise,
update_container, update_container,
viewer_update_and_undo_stop viewer_update_and_undo_stop
) )
from ayon_core.hosts.nuke.api.lib import ( from ayon_nuke.api.lib import (
maintained_selection maintained_selection
) )
@ -24,6 +24,8 @@ class AlembicCameraLoader(load.LoaderPlugin):
representations = {"*"} representations = {"*"}
extensions = {"abc"} extensions = {"abc"}
settings_category = "nuke"
label = "Load Alembic Camera" label = "Load Alembic Camera"
icon = "camera" icon = "camera"
color = "orange" color = "orange"

View file

@ -12,11 +12,11 @@ from ayon_core.pipeline.colorspace import (
get_imageio_file_rules_colorspace_from_filepath, get_imageio_file_rules_colorspace_from_filepath,
get_current_context_imageio_config_preset, get_current_context_imageio_config_preset,
) )
from ayon_core.hosts.nuke.api.lib import ( from ayon_nuke.api.lib import (
get_imageio_input_colorspace, get_imageio_input_colorspace,
maintained_selection maintained_selection
) )
from ayon_core.hosts.nuke.api import ( from ayon_nuke.api import (
containerise, containerise,
update_container, update_container,
viewer_update_and_undo_stop, viewer_update_and_undo_stop,
@ -26,7 +26,7 @@ from ayon_core.lib.transcoding import (
VIDEO_EXTENSIONS, VIDEO_EXTENSIONS,
IMAGE_EXTENSIONS IMAGE_EXTENSIONS
) )
from ayon_core.hosts.nuke.api import plugin from ayon_nuke.api import plugin
class LoadClip(plugin.NukeLoader): class LoadClip(plugin.NukeLoader):
@ -48,6 +48,8 @@ class LoadClip(plugin.NukeLoader):
ext.lstrip(".") for ext in IMAGE_EXTENSIONS.union(VIDEO_EXTENSIONS) ext.lstrip(".") for ext in IMAGE_EXTENSIONS.union(VIDEO_EXTENSIONS)
) )
settings_category = "nuke"
label = "Load Clip" label = "Load Clip"
order = -20 order = -20
icon = "file-video-o" icon = "file-video-o"
@ -61,7 +63,8 @@ class LoadClip(plugin.NukeLoader):
# option gui # option gui
options_defaults = { options_defaults = {
"start_at_workfile": True, "start_at_workfile": True,
"add_retime": True "add_retime": True,
"deep_exr": False
} }
node_name_template = "{class_name}_{ext}" node_name_template = "{class_name}_{ext}"
@ -78,6 +81,11 @@ class LoadClip(plugin.NukeLoader):
"add_retime", "add_retime",
help="Load with retime", help="Load with retime",
default=cls.options_defaults["add_retime"] default=cls.options_defaults["add_retime"]
),
qargparse.Boolean(
"deep_exr",
help="Read with deep exr",
default=cls.options_defaults["deep_exr"]
) )
] ]
@ -113,6 +121,9 @@ class LoadClip(plugin.NukeLoader):
add_retime = options.get( add_retime = options.get(
"add_retime", self.options_defaults["add_retime"]) "add_retime", self.options_defaults["add_retime"])
deep_exr = options.get(
"deep_exr", self.options_defaults["deep_exr"])
repre_id = repre_entity["id"] repre_id = repre_entity["id"]
self.log.debug( self.log.debug(
@ -153,13 +164,21 @@ class LoadClip(plugin.NukeLoader):
return return
read_name = self._get_node_name(context) read_name = self._get_node_name(context)
read_node = None
# Create the Loader with the filename path set if deep_exr:
read_node = nuke.createNode( # Create the Loader with the filename path set
"Read", read_node = nuke.createNode(
"name {}".format(read_name), "DeepRead",
inpanel=False "name {}".format(read_name),
) inpanel=False
)
else:
# Create the Loader with the filename path set
read_node = nuke.createNode(
"Read",
"name {}".format(read_name),
inpanel=False
)
# get colorspace # get colorspace
colorspace = ( colorspace = (
@ -171,14 +190,14 @@ class LoadClip(plugin.NukeLoader):
# we will switch off undo-ing # we will switch off undo-ing
with viewer_update_and_undo_stop(): with viewer_update_and_undo_stop():
read_node["file"].setValue(filepath) read_node["file"].setValue(filepath)
if read_node.Class() == "Read":
self.set_colorspace_to_node( self.set_colorspace_to_node(
read_node, read_node,
filepath, filepath,
project_name, project_name,
version_entity, version_entity,
repre_entity repre_entity
) )
self._set_range_to_node( self._set_range_to_node(
read_node, first, last, start_at_workfile, slate_frames read_node, first, last, start_at_workfile, slate_frames
@ -328,13 +347,14 @@ class LoadClip(plugin.NukeLoader):
# to avoid multiple undo steps for rest of process # to avoid multiple undo steps for rest of process
# we will switch off undo-ing # we will switch off undo-ing
with viewer_update_and_undo_stop(): with viewer_update_and_undo_stop():
self.set_colorspace_to_node( if read_node.Class() == "Read":
read_node, self.set_colorspace_to_node(
filepath, read_node,
project_name, filepath,
version_entity, project_name,
repre_entity version_entity,
) 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)

View file

@ -8,7 +8,7 @@ from ayon_core.pipeline import (
load, load,
get_representation_path, get_representation_path,
) )
from ayon_core.hosts.nuke.api import ( from ayon_nuke.api import (
containerise, containerise,
update_container, update_container,
viewer_update_and_undo_stop viewer_update_and_undo_stop
@ -22,13 +22,14 @@ class LoadEffects(load.LoaderPlugin):
representations = {"*"} representations = {"*"}
extensions = {"json"} extensions = {"json"}
settings_category = "nuke"
label = "Load Effects - nodes" label = "Load Effects - nodes"
order = 0 order = 0
icon = "cc" icon = "cc"
color = "white" color = "white"
ignore_attr = ["useLifetime"] ignore_attr = ["useLifetime"]
def load(self, context, name, namespace, data): def load(self, context, name, namespace, data):
""" """
Loading function to get the soft effects to particular read node Loading function to get the soft effects to particular read node

View file

@ -8,8 +8,8 @@ from ayon_core.pipeline import (
load, load,
get_representation_path, get_representation_path,
) )
from ayon_core.hosts.nuke.api import lib from ayon_nuke.api import lib
from ayon_core.hosts.nuke.api import ( from ayon_nuke.api import (
containerise, containerise,
update_container, update_container,
viewer_update_and_undo_stop viewer_update_and_undo_stop
@ -23,6 +23,8 @@ class LoadEffectsInputProcess(load.LoaderPlugin):
representations = {"*"} representations = {"*"}
extensions = {"json"} extensions = {"json"}
settings_category = "nuke"
label = "Load Effects - Input Process" label = "Load Effects - Input Process"
order = 0 order = 0
icon = "eye" icon = "eye"

View file

@ -5,13 +5,13 @@ from ayon_core.pipeline import (
load, load,
get_representation_path, get_representation_path,
) )
from ayon_core.hosts.nuke.api.lib import ( from ayon_nuke.api.lib import (
maintained_selection, maintained_selection,
get_avalon_knob_data, get_avalon_knob_data,
set_avalon_knob_data, set_avalon_knob_data,
swap_node_with_dependency, swap_node_with_dependency,
) )
from ayon_core.hosts.nuke.api import ( from ayon_nuke.api import (
containerise, containerise,
update_container, update_container,
viewer_update_and_undo_stop viewer_update_and_undo_stop
@ -25,6 +25,8 @@ class LoadGizmo(load.LoaderPlugin):
representations = {"*"} representations = {"*"}
extensions = {"nk"} extensions = {"nk"}
settings_category = "nuke"
label = "Load Gizmo" label = "Load Gizmo"
order = 0 order = 0
icon = "dropbox" icon = "dropbox"

View file

@ -6,14 +6,14 @@ from ayon_core.pipeline import (
load, load,
get_representation_path, get_representation_path,
) )
from ayon_core.hosts.nuke.api.lib import ( from ayon_nuke.api.lib import (
maintained_selection, maintained_selection,
create_backdrop, create_backdrop,
get_avalon_knob_data, get_avalon_knob_data,
set_avalon_knob_data, set_avalon_knob_data,
swap_node_with_dependency, swap_node_with_dependency,
) )
from ayon_core.hosts.nuke.api import ( from ayon_nuke.api import (
containerise, containerise,
update_container, update_container,
viewer_update_and_undo_stop viewer_update_and_undo_stop
@ -27,6 +27,8 @@ class LoadGizmoInputProcess(load.LoaderPlugin):
representations = {"*"} representations = {"*"}
extensions = {"nk"} extensions = {"nk"}
settings_category = "nuke"
label = "Load Gizmo - Input Process" label = "Load Gizmo - Input Process"
order = 0 order = 0
icon = "eye" icon = "eye"

View file

@ -7,10 +7,10 @@ from ayon_core.pipeline import (
load, load,
get_representation_path, get_representation_path,
) )
from ayon_core.hosts.nuke.api.lib import ( from ayon_nuke.api.lib import (
get_imageio_input_colorspace get_imageio_input_colorspace
) )
from ayon_core.hosts.nuke.api import ( from ayon_nuke.api import (
containerise, containerise,
update_container, update_container,
viewer_update_and_undo_stop viewer_update_and_undo_stop
@ -33,9 +33,9 @@ class LoadImage(load.LoaderPlugin):
"image", "image",
} }
representations = {"*"} representations = {"*"}
extensions = set( extensions = set(ext.lstrip(".") for ext in IMAGE_EXTENSIONS)
ext.lstrip(".") for ext in IMAGE_EXTENSIONS
) settings_category = "nuke"
label = "Load Image" label = "Load Image"
order = -10 order = -10

View file

@ -11,6 +11,8 @@ class MatchmoveLoader(load.LoaderPlugin):
representations = {"*"} representations = {"*"}
extensions = {"py"} extensions = {"py"}
settings_category = "nuke"
defaults = ["Camera", "Object"] defaults = ["Camera", "Object"]
label = "Run matchmove script" label = "Run matchmove script"

View file

@ -5,8 +5,8 @@ from ayon_core.pipeline import (
load, load,
get_representation_path, get_representation_path,
) )
from ayon_core.hosts.nuke.api.lib import maintained_selection from ayon_nuke.api.lib import maintained_selection
from ayon_core.hosts.nuke.api import ( from ayon_nuke.api import (
containerise, containerise,
update_container, update_container,
viewer_update_and_undo_stop viewer_update_and_undo_stop
@ -22,6 +22,8 @@ class AlembicModelLoader(load.LoaderPlugin):
representations = {"*"} representations = {"*"}
extensions = {"abc"} extensions = {"abc"}
settings_category = "nuke"
label = "Load Alembic" label = "Load Alembic"
icon = "cube" icon = "cube"
color = "orange" color = "orange"

View file

@ -10,7 +10,7 @@ from ayon_core.pipeline import (
load, load,
get_representation_path, get_representation_path,
) )
from ayon_core.hosts.nuke.api import ( from ayon_nuke.api import (
containerise, containerise,
viewer_update_and_undo_stop, viewer_update_and_undo_stop,
update_container, update_container,
@ -24,6 +24,8 @@ class LoadOcioLookNodes(load.LoaderPlugin):
representations = {"*"} representations = {"*"}
extensions = {"json"} extensions = {"json"}
settings_category = "nuke"
label = "Load OcioLook [nodes]" label = "Load OcioLook [nodes]"
order = 0 order = 0
icon = "cc" icon = "cc"

View file

@ -5,8 +5,8 @@ from ayon_core.pipeline import (
load, load,
get_representation_path, get_representation_path,
) )
from ayon_core.hosts.nuke.api.lib import get_avalon_knob_data from ayon_nuke.api.lib import get_avalon_knob_data
from ayon_core.hosts.nuke.api import ( from ayon_nuke.api import (
containerise, containerise,
update_container, update_container,
viewer_update_and_undo_stop viewer_update_and_undo_stop
@ -20,6 +20,8 @@ class LinkAsGroup(load.LoaderPlugin):
representations = {"*"} representations = {"*"}
extensions = {"nk"} extensions = {"nk"}
settings_category = "nuke"
label = "Load Precomp" label = "Load Precomp"
order = 0 order = 0
icon = "file" icon = "file"

View file

@ -1,6 +1,6 @@
from pprint import pformat from pprint import pformat
import pyblish.api import pyblish.api
from ayon_core.hosts.nuke.api import lib as pnlib from ayon_nuke.api import lib as pnlib
import nuke import nuke
@ -13,6 +13,8 @@ class CollectBackdrops(pyblish.api.InstancePlugin):
hosts = ["nuke"] hosts = ["nuke"]
families = ["nukenodes"] families = ["nukenodes"]
settings_category = "nuke"
def process(self, instance): def process(self, instance):
self.log.debug(pformat(instance.data)) self.log.debug(pformat(instance.data))

View file

@ -2,7 +2,7 @@ import os
import nuke import nuke
import pyblish.api import pyblish.api
from ayon_core.lib import get_version_from_path from ayon_core.lib import get_version_from_path
import ayon_core.hosts.nuke.api as napi import ayon_nuke.api as napi
from ayon_core.pipeline import KnownPublishError from ayon_core.pipeline import KnownPublishError
@ -13,6 +13,8 @@ class CollectContextData(pyblish.api.ContextPlugin):
label = "Collect context data" label = "Collect context data"
hosts = ['nuke'] hosts = ['nuke']
settings_category = "nuke"
def process(self, context): # sourcery skip: avoid-builtin-shadow def process(self, context): # sourcery skip: avoid-builtin-shadow
root_node = nuke.root() root_node = nuke.root()

View file

@ -13,5 +13,7 @@ class CollectFramerate(pyblish.api.ContextPlugin):
"nukeassist" "nukeassist"
] ]
settings_category = "nuke"
def process(self, context): def process(self, context):
context.data["fps"] = nuke.root()["fps"].getValue() context.data["fps"] = nuke.root()["fps"].getValue()

View file

@ -11,6 +11,8 @@ class CollectGizmo(pyblish.api.InstancePlugin):
hosts = ["nuke"] hosts = ["nuke"]
families = ["gizmo"] families = ["gizmo"]
settings_category = "nuke"
def process(self, instance): def process(self, instance):
gizmo_node = instance.data["transientData"]["node"] gizmo_node = instance.data["transientData"]["node"]

View file

@ -0,0 +1,58 @@
import pyblish.api
from ayon_core.pipeline.publish import (
AYONPyblishPluginMixin
)
class CollectRenderOnFarm(pyblish.api.ContextPlugin):
"""Setup instances for render on farm submission."""
# Needs to be after CollectFromCreateContext
order = pyblish.api.CollectorOrder - 0.49
label = "Collect Render On Farm"
hosts = ["nuke"]
settings_category = "nuke"
def process(self, context):
if not context.data.get("render_on_farm", False):
return
for instance in context:
if instance.data["family"] == "workfile":
instance.data["active"] = False
continue
# Filter out all other instances.
node = instance.data["transientData"]["node"]
if node.name() != instance.context.data["node_name"]:
instance.data["active"] = False
continue
instance.data["families"].append("render_on_farm")
# Enable for farm publishing.
instance.data["farm"] = True
# Skip workfile version incremental save.
instance.context.data["increment_script_version"] = False
class SetupRenderOnFarm(pyblish.api.InstancePlugin, AYONPyblishPluginMixin):
"""Setup instance for render on farm submission."""
order = pyblish.api.CollectorOrder + 0.4999
label = "Setup Render On Farm"
hosts = ["nuke"]
families = ["render_on_farm"]
def process(self, instance):
# Clear the families as we only want the main family, ei. no review
# etc.
instance.data["families"] = ["render_on_farm"]
# Use the workfile instead of published.
publish_attributes = instance.data["publish_attributes"]
plugin_attributes = publish_attributes["NukeSubmitDeadline"]
plugin_attributes["use_published_workfile"] = False

View file

@ -11,6 +11,8 @@ class CollectModel(pyblish.api.InstancePlugin):
hosts = ["nuke"] hosts = ["nuke"]
families = ["model"] families = ["model"]
settings_category = "nuke"
def process(self, instance): def process(self, instance):
geo_node = instance.data["transientData"]["node"] geo_node = instance.data["transientData"]["node"]

View file

@ -11,6 +11,8 @@ class CollectInstanceData(pyblish.api.InstancePlugin):
label = "Collect Nuke Instance Data" label = "Collect Nuke Instance Data"
hosts = ["nuke", "nukeassist"] hosts = ["nuke", "nukeassist"]
settings_category = "nuke"
# presets # presets
sync_workfile_version_on_families = [] sync_workfile_version_on_families = []

View file

@ -12,6 +12,8 @@ class CollectNukeReads(pyblish.api.InstancePlugin):
hosts = ["nuke", "nukeassist"] hosts = ["nuke", "nukeassist"]
families = ["source"] families = ["source"]
settings_category = "nuke"
def process(self, instance): def process(self, instance):
self.log.debug("checking instance: {}".format(instance)) self.log.debug("checking instance: {}".format(instance))

View file

@ -10,6 +10,8 @@ class CollectSlate(pyblish.api.InstancePlugin):
hosts = ["nuke"] hosts = ["nuke"]
families = ["render"] families = ["render"]
settings_category = "nuke"
def process(self, instance): def process(self, instance):
node = instance.data["transientData"]["node"] node = instance.data["transientData"]["node"]

View file

@ -11,6 +11,8 @@ class CollectWorkfile(pyblish.api.InstancePlugin):
hosts = ['nuke'] hosts = ['nuke']
families = ["workfile"] families = ["workfile"]
settings_category = "nuke"
def process(self, instance): # sourcery skip: avoid-builtin-shadow def process(self, instance): # sourcery skip: avoid-builtin-shadow
script_data = instance.context.data["scriptData"] script_data = instance.context.data["scriptData"]

View file

@ -1,7 +1,7 @@
import os import os
import nuke import nuke
import pyblish.api import pyblish.api
from ayon_core.hosts.nuke import api as napi from ayon_nuke import api as napi
from ayon_core.pipeline import publish from ayon_core.pipeline import publish
@ -14,6 +14,8 @@ class CollectNukeWrites(pyblish.api.InstancePlugin,
hosts = ["nuke", "nukeassist"] hosts = ["nuke", "nukeassist"]
families = ["render", "prerender", "image"] families = ["render", "prerender", "image"]
settings_category = "nuke"
# cache # cache
_write_nodes = {} _write_nodes = {}
_frame_ranges = {} _frame_ranges = {}

View file

@ -5,7 +5,7 @@ import nuke
import pyblish.api import pyblish.api
from ayon_core.pipeline import publish from ayon_core.pipeline import publish
from ayon_core.hosts.nuke.api.lib import ( from ayon_nuke.api.lib import (
maintained_selection, maintained_selection,
reset_selection, reset_selection,
select_nodes select_nodes
@ -25,6 +25,8 @@ class ExtractBackdropNode(publish.Extractor):
hosts = ["nuke"] hosts = ["nuke"]
families = ["nukenodes"] families = ["nukenodes"]
settings_category = "nuke"
def process(self, instance): def process(self, instance):
tmp_nodes = [] tmp_nodes = []
child_nodes = instance.data["transientData"]["childNodes"] child_nodes = instance.data["transientData"]["childNodes"]

View file

@ -6,7 +6,7 @@ import nuke
import pyblish.api import pyblish.api
from ayon_core.pipeline import publish from ayon_core.pipeline import publish
from ayon_core.hosts.nuke.api.lib import maintained_selection from ayon_nuke.api.lib import maintained_selection
class ExtractCamera(publish.Extractor): class ExtractCamera(publish.Extractor):
@ -17,6 +17,8 @@ class ExtractCamera(publish.Extractor):
families = ["camera"] families = ["camera"]
hosts = ["nuke"] hosts = ["nuke"]
settings_category = "nuke"
# presets # presets
write_geo_knobs = [ write_geo_knobs = [
("file_type", "abc"), ("file_type", "abc"),

View file

@ -4,8 +4,8 @@ import nuke
import pyblish.api import pyblish.api
from ayon_core.pipeline import publish from ayon_core.pipeline import publish
from ayon_core.hosts.nuke.api import utils as pnutils from ayon_nuke.api import utils as pnutils
from ayon_core.hosts.nuke.api.lib import ( from ayon_nuke.api.lib import (
maintained_selection, maintained_selection,
reset_selection, reset_selection,
select_nodes select_nodes
@ -23,6 +23,8 @@ class ExtractGizmo(publish.Extractor):
hosts = ["nuke"] hosts = ["nuke"]
families = ["gizmo"] families = ["gizmo"]
settings_category = "nuke"
def process(self, instance): def process(self, instance):
tmp_nodes = [] tmp_nodes = []
orig_grpn = instance.data["transientData"]["node"] orig_grpn = instance.data["transientData"]["node"]

Some files were not shown because too many files have changed in this diff Show more