Merge branch 'develop' into enhancement/AY-5270_Nuke-Enhance-Viewer-process-override-with-colorspace-display-view

This commit is contained in:
Jakub Ježek 2024-06-14 12:25:22 +02:00 committed by GitHub
commit 48aecf846d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 306 additions and 58 deletions

View file

@ -1,6 +1,80 @@
import ayon_api
import json
import collections
from ayon_core.lib import CacheItem
import ayon_api
from ayon_api.graphql import FIELD_VALUE, GraphQlQuery, fields_to_dict
from ayon_core.lib import NestedCacheItem
# --- Implementation that should be in ayon-python-api ---
# The implementation is not available in all versions of ayon-python-api.
def users_graphql_query(fields):
query = GraphQlQuery("Users")
names_var = query.add_variable("userNames", "[String!]")
project_name_var = query.add_variable("projectName", "String!")
users_field = query.add_field_with_edges("users")
users_field.set_filter("names", names_var)
users_field.set_filter("projectName", project_name_var)
nested_fields = fields_to_dict(set(fields))
query_queue = collections.deque()
for key, value in nested_fields.items():
query_queue.append((key, value, users_field))
while query_queue:
item = query_queue.popleft()
key, value, parent = item
field = parent.add_field(key)
if value is FIELD_VALUE:
continue
for k, v in value.items():
query_queue.append((k, v, field))
return query
def get_users(project_name=None, usernames=None, fields=None):
"""Get Users.
Only administrators and managers can fetch all users. For other users
it is required to pass in 'project_name' filter.
Args:
project_name (Optional[str]): Project name.
usernames (Optional[Iterable[str]]): Filter by usernames.
fields (Optional[Iterable[str]]): Fields to be queried
for users.
Returns:
Generator[dict[str, Any]]: Queried users.
"""
filters = {}
if usernames is not None:
usernames = set(usernames)
if not usernames:
return
filters["userNames"] = list(usernames)
if project_name is not None:
filters["projectName"] = project_name
con = ayon_api.get_server_api_connection()
if not fields:
fields = con.get_default_fields_for_type("user")
query = users_graphql_query(set(fields))
for attr, filter_value in filters.items():
query.set_variable_value(attr, filter_value)
for parsed_data in query.continuous_query(con):
for user in parsed_data["users"]:
user["accessGroups"] = json.loads(user["accessGroups"])
yield user
# --- END of ayon-python-api implementation ---
class UserItem:
@ -32,19 +106,19 @@ class UserItem:
class UsersModel:
def __init__(self, controller):
self._controller = controller
self._users_cache = CacheItem(default_factory=list)
self._users_cache = NestedCacheItem(default_factory=list)
def get_user_items(self):
def get_user_items(self, project_name):
"""Get user items.
Returns:
List[UserItem]: List of user items.
"""
self._invalidate_cache()
return self._users_cache.get_data()
self._invalidate_cache(project_name)
return self._users_cache[project_name].get_data()
def get_user_items_by_name(self):
def get_user_items_by_name(self, project_name):
"""Get user items by name.
Implemented as most of cases using this model will need to find
@ -56,10 +130,10 @@ class UsersModel:
"""
return {
user_item.username: user_item
for user_item in self.get_user_items()
for user_item in self.get_user_items(project_name)
}
def get_user_item_by_username(self, username):
def get_user_item_by_username(self, project_name, username):
"""Get user item by username.
Args:
@ -69,16 +143,22 @@ class UsersModel:
Union[UserItem, None]: User item or None if not found.
"""
self._invalidate_cache()
for user_item in self.get_user_items():
self._invalidate_cache(project_name)
for user_item in self.get_user_items(project_name):
if user_item.username == username:
return user_item
return None
def _invalidate_cache(self):
if self._users_cache.is_valid:
def _invalidate_cache(self, project_name):
cache = self._users_cache[project_name]
if cache.is_valid:
return
self._users_cache.update_data([
if project_name is None:
cache.update_data([])
return
self._users_cache[project_name].update_data([
UserItem.from_entity_data(user)
for user in ayon_api.get_users()
for user in get_users(project_name)
])

View file

@ -1,7 +1,8 @@
from .window import ContextDialog, main
from .window import ContextDialog, main, ask_for_context
__all__ = (
"ContextDialog",
"main",
"ask_for_context"
)

View file

@ -791,3 +791,12 @@ def main(
window.show()
app.exec_()
controller.store_output()
def ask_for_context(strict=True):
controller = ContextDialogController()
controller.set_strict(strict)
window = ContextDialog(controller=controller)
window.exec_()
return controller.get_selected_context()

View file

@ -834,12 +834,13 @@ class AbstractWorkfilesFrontend(AbstractWorkfilesCommon):
pass
@abstractmethod
def get_workarea_file_items(self, folder_id, task_id):
def get_workarea_file_items(self, folder_id, task_name, sender=None):
"""Get workarea file items.
Args:
folder_id (str): Folder id.
task_id (str): Task id.
task_name (str): Task name.
sender (Optional[str]): Who requested workarea file items.
Returns:
list[FileItem]: List of workarea file items.
@ -905,12 +906,12 @@ class AbstractWorkfilesFrontend(AbstractWorkfilesCommon):
pass
@abstractmethod
def get_workfile_info(self, folder_id, task_id, filepath):
def get_workfile_info(self, folder_id, task_name, filepath):
"""Workfile info from database.
Args:
folder_id (str): Folder id.
task_id (str): Task id.
task_name (str): Task id.
filepath (str): Workfile path.
Returns:
@ -921,7 +922,7 @@ class AbstractWorkfilesFrontend(AbstractWorkfilesCommon):
pass
@abstractmethod
def save_workfile_info(self, folder_id, task_id, filepath, note):
def save_workfile_info(self, folder_id, task_name, filepath, note):
"""Save workfile info to database.
At this moment the only information which can be saved about
@ -932,7 +933,7 @@ class AbstractWorkfilesFrontend(AbstractWorkfilesCommon):
Args:
folder_id (str): Folder id.
task_id (str): Task id.
task_name (str): Task id.
filepath (str): Workfile path.
note (Union[str, None]): Note.
"""

View file

@ -278,7 +278,8 @@ class BaseWorkfileController(
)
def get_user_items_by_name(self):
return self._users_model.get_user_items_by_name()
project_name = self.get_current_project_name()
return self._users_model.get_user_items_by_name(project_name)
# Host information
def get_workfile_extensions(self):
@ -410,9 +411,11 @@ class BaseWorkfileController(
return self._workfiles_model.get_workarea_dir_by_context(
folder_id, task_id)
def get_workarea_file_items(self, folder_id, task_id):
def get_workarea_file_items(self, folder_id, task_name, sender=None):
task_id = self._get_task_id(folder_id, task_name)
return self._workfiles_model.get_workarea_file_items(
folder_id, task_id)
folder_id, task_id, task_name
)
def get_workarea_save_as_data(self, folder_id, task_id):
return self._workfiles_model.get_workarea_save_as_data(
@ -447,12 +450,14 @@ class BaseWorkfileController(
return self._workfiles_model.get_published_file_items(
folder_id, task_name)
def get_workfile_info(self, folder_id, task_id, filepath):
def get_workfile_info(self, folder_id, task_name, filepath):
task_id = self._get_task_id(folder_id, task_name)
return self._workfiles_model.get_workfile_info(
folder_id, task_id, filepath
)
def save_workfile_info(self, folder_id, task_id, filepath, note):
def save_workfile_info(self, folder_id, task_name, filepath, note):
task_id = self._get_task_id(folder_id, task_name)
self._workfiles_model.save_workfile_info(
folder_id, task_id, filepath, note
)
@ -627,6 +632,17 @@ class BaseWorkfileController(
def _emit_event(self, topic, data=None):
self.emit_event(topic, data, "controller")
def _get_task_id(self, folder_id, task_name, sender=None):
task_item = self._hierarchy_model.get_task_item_by_name(
self.get_current_project_name(),
folder_id,
task_name,
sender
)
if not task_item:
return None
return task_item.id
# Expected selection
# - expected selection is used to restore selection after refresh
# or when current context should be used
@ -722,7 +738,7 @@ class BaseWorkfileController(
self._host_save_workfile(dst_filepath)
# Make sure workfile info exists
self.save_workfile_info(folder_id, task_id, dst_filepath, None)
self.save_workfile_info(folder_id, task_name, dst_filepath, None)
# Create extra folders
create_workdir_extra_folders(

View file

@ -1,6 +1,7 @@
import os
import re
import copy
import uuid
import arrow
import ayon_api
@ -173,7 +174,7 @@ class WorkareaModel:
folder_mapping[task_id] = workdir
return workdir
def get_file_items(self, folder_id, task_id):
def get_file_items(self, folder_id, task_id, task_name):
items = []
if not folder_id or not task_id:
return items
@ -192,7 +193,7 @@ class WorkareaModel:
continue
workfile_info = self._controller.get_workfile_info(
folder_id, task_id, filepath
folder_id, task_name, filepath
)
modified = os.path.getmtime(filepath)
items.append(FileItem(
@ -587,6 +588,7 @@ class WorkfileEntitiesModel:
username = self._get_current_username()
workfile_info = {
"id": uuid.uuid4().hex,
"path": rootless_path,
"taskId": task_id,
"attrib": {
@ -770,19 +772,21 @@ class WorkfilesModel:
return self._workarea_model.get_workarea_dir_by_context(
folder_id, task_id)
def get_workarea_file_items(self, folder_id, task_id):
def get_workarea_file_items(self, folder_id, task_id, task_name):
"""Workfile items for passed context from workarea.
Args:
folder_id (Union[str, None]): Folder id.
task_id (Union[str, None]): Task id.
task_name (Union[str, None]): Task name.
Returns:
list[FileItem]: List of file items matching workarea of passed
context.
"""
return self._workarea_model.get_file_items(folder_id, task_id)
return self._workarea_model.get_file_items(
folder_id, task_id, task_name
)
def get_workarea_save_as_data(self, folder_id, task_id):
return self._workarea_model.get_workarea_save_as_data(

View file

@ -66,7 +66,7 @@ class WorkAreaFilesModel(QtGui.QStandardItemModel):
self._empty_item_used = False
self._published_mode = False
self._selected_folder_id = None
self._selected_task_id = None
self._selected_task_name = None
self._add_missing_context_item()
@ -153,7 +153,7 @@ class WorkAreaFilesModel(QtGui.QStandardItemModel):
def _on_task_changed(self, event):
self._selected_folder_id = event["folder_id"]
self._selected_task_id = event["task_id"]
self._selected_task_name = event["task_name"]
if not self._published_mode:
self._fill_items()
@ -179,13 +179,13 @@ class WorkAreaFilesModel(QtGui.QStandardItemModel):
def _fill_items_impl(self):
folder_id = self._selected_folder_id
task_id = self._selected_task_id
if not folder_id or not task_id:
task_name = self._selected_task_name
if not folder_id or not task_name:
self._add_missing_context_item()
return
file_items = self._controller.get_workarea_file_items(
folder_id, task_id
folder_id, task_name
)
root_item = self.invisibleRootItem()
if not file_items:

View file

@ -75,7 +75,7 @@ class SidePanelWidget(QtWidgets.QWidget):
self._btn_note_save = btn_note_save
self._folder_id = None
self._task_id = None
self._task_name = None
self._filepath = None
self._orig_note = ""
self._controller = controller
@ -93,10 +93,10 @@ class SidePanelWidget(QtWidgets.QWidget):
def _on_selection_change(self, event):
folder_id = event["folder_id"]
task_id = event["task_id"]
task_name = event["task_name"]
filepath = event["path"]
self._set_context(folder_id, task_id, filepath)
self._set_context(folder_id, task_name, filepath)
def _on_note_change(self):
text = self._note_input.toPlainText()
@ -106,19 +106,19 @@ class SidePanelWidget(QtWidgets.QWidget):
note = self._note_input.toPlainText()
self._controller.save_workfile_info(
self._folder_id,
self._task_id,
self._task_name,
self._filepath,
note
)
self._orig_note = note
self._btn_note_save.setEnabled(False)
def _set_context(self, folder_id, task_id, filepath):
def _set_context(self, folder_id, task_name, filepath):
workfile_info = None
# Check if folder, task and file are selected
if bool(folder_id) and bool(task_id) and bool(filepath):
if bool(folder_id) and bool(task_name) and bool(filepath):
workfile_info = self._controller.get_workfile_info(
folder_id, task_id, filepath
folder_id, task_name, filepath
)
enabled = workfile_info is not None
@ -127,7 +127,7 @@ class SidePanelWidget(QtWidgets.QWidget):
self._btn_note_save.setEnabled(enabled)
self._folder_id = folder_id
self._task_id = task_id
self._task_name = task_name
self._filepath = filepath
# Disable inputs and remove texts if any required arguments are

View file

@ -2,7 +2,10 @@
"""Creator plugin for creating publishable Houdini Digital Assets."""
import ayon_api
from ayon_core.pipeline import CreatorError
from ayon_core.pipeline import (
CreatorError,
get_current_project_name
)
from ayon_houdini.api import plugin
import hou
@ -56,8 +59,18 @@ class CreateHDA(plugin.HoudiniCreator):
raise CreatorError(
"cannot create hda from node {}".format(to_hda))
# Pick a unique type name for HDA product per folder path per project.
type_name = (
"{project_name}{folder_path}_{node_name}".format(
project_name=get_current_project_name(),
folder_path=folder_path.replace("/","_"),
node_name=node_name
)
)
hda_node = to_hda.createDigitalAsset(
name=node_name,
name=type_name,
description=node_name,
hda_file_name="$HIP/{}.hda".format(node_name)
)
hda_node.layoutChildren()

View file

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
import os
from ayon_core.pipeline import get_representation_path
from ayon_core.pipeline.load import LoadError
from ayon_houdini.api import (
pipeline,
plugin
@ -28,14 +29,18 @@ class HdaLoader(plugin.HoudiniLoader):
# Get the root node
obj = hou.node("/obj")
# Create a unique name
counter = 1
namespace = namespace or context["folder"]["name"]
formatted = "{}_{}".format(namespace, name) if namespace else name
node_name = "{0}_{1:03d}".format(formatted, counter)
node_name = "{}_{}".format(namespace, name) if namespace else name
hou.hda.installFile(file_path)
hda_node = obj.createNode(name, node_name)
# Get the type name from the HDA definition.
hda_defs = hou.hda.definitionsInFile(file_path)
if not hda_defs:
raise LoadError(f"No HDA definitions found in file: {file_path}")
type_name = hda_defs[0].nodeTypeName()
hda_node = obj.createNode(type_name, node_name)
self[:] = [hda_node]

View file

@ -37,8 +37,6 @@ from .lib import (
INSTANCE_DATA_KNOB,
get_main_window,
WorkfileSettings,
# TODO: remove this once workfile builder will be removed
process_workfile_builder,
start_workfile_template_builder,
launch_workfiles_app,
check_inventory_versions,
@ -67,6 +65,7 @@ from .workio import (
current_file
)
from .constants import ASSIST
from . import push_to_project
log = Logger.get_logger(__name__)
@ -159,9 +158,6 @@ def add_nuke_callbacks():
# template builder callbacks
nuke.addOnCreate(start_workfile_template_builder, nodeClass="Root")
# TODO: remove this callback once workfile builder will be removed
nuke.addOnCreate(process_workfile_builder, nodeClass="Root")
# fix ffmpeg settings on script
nuke.addOnScriptLoad(on_script_load)
@ -332,6 +328,11 @@ def _install_menu():
lambda: update_placeholder()
)
menu.addCommand(
"Push to Project",
lambda: push_to_project.main()
)
menu.addSeparator()
menu.addCommand(
"Experimental tools...",

View file

@ -0,0 +1,118 @@
from collections import defaultdict
import shutil
import os
from ayon_api import get_project, get_folder_by_id, get_task_by_id
from ayon_core.settings import get_project_settings
from ayon_core.pipeline import Anatomy, registered_host
from ayon_core.pipeline.template_data import get_template_data
from ayon_core.pipeline.workfile import get_workdir_with_workdir_data
from ayon_core.tools import context_dialog
from .utils import bake_gizmos_recursively
from .lib import MENU_LABEL
import nuke
def bake_container(container):
"""Bake containers to read nodes."""
node = container["node"]
# Fetch knobs to remove in order.
knobs_to_remove = []
remove = False
for count in range(0, node.numKnobs()):
knob = node.knob(count)
# All knobs from "AYON" tab knob onwards.
if knob.name() == MENU_LABEL:
remove = True
if remove:
knobs_to_remove.append(knob)
# Dont remove knobs from "containerId" onwards.
if knob.name() == "containerId":
remove = False
# Knobs needs to be remove in reverse order, because child knobs needs to
# be remove first.
for knob in reversed(knobs_to_remove):
node.removeKnob(knob)
node["tile_color"].setValue(0)
def main():
context = context_dialog.ask_for_context()
if context is None:
return
# Get workfile path to save to.
project_name = context["project_name"]
project = get_project(project_name)
folder = get_folder_by_id(project_name, context["folder_id"])
task = get_task_by_id(project_name, context["task_id"])
host = registered_host()
project_settings = get_project_settings(project_name)
anatomy = Anatomy(project_name)
workdir_data = get_template_data(
project, folder, task, host.name, project_settings
)
workdir = get_workdir_with_workdir_data(
workdir_data,
project_name,
anatomy,
project_settings=project_settings
)
# Save current workfile.
current_file = host.current_file()
host.save_file(current_file)
for container in host.ls():
bake_container(container)
# Bake gizmos.
bake_gizmos_recursively()
# Copy all read node files to "resources" folder next to workfile and
# change file path.
first_frame = int(nuke.root()["first_frame"].value())
last_frame = int(nuke.root()["last_frame"].value())
files_by_node_name = defaultdict(set)
nodes_by_name = {}
for count in range(first_frame, last_frame + 1):
nuke.frame(count)
for node in nuke.allNodes(filter="Read"):
files_by_node_name[node.name()].add(
nuke.filename(node, nuke.REPLACE)
)
nodes_by_name[node.name()] = node
resources_dir = os.path.join(workdir, "resources")
for name, files in files_by_node_name.items():
dir = os.path.join(resources_dir, name)
if not os.path.exists(dir):
os.makedirs(dir)
for f in files:
shutil.copy(f, os.path.join(dir, os.path.basename(f)))
node = nodes_by_name[name]
path = node["file"].value().replace(os.path.dirname(f), dir)
node["file"].setValue(path.replace("\\", "/"))
# Save current workfile to new context.
pushed_workfile = os.path.join(
workdir, os.path.basename(current_file))
host.save_file(pushed_workfile)
# Open current context workfile.
host.open_file(current_file)
nuke.message(f"Pushed to project: \n{pushed_workfile}")