diff --git a/projects/framework-nuke/extensions/nuke.yaml b/projects/framework-nuke/extensions/nuke.yaml index 636867ecd0..46ab2fcef9 100644 --- a/projects/framework-nuke/extensions/nuke.yaml +++ b/projects/framework-nuke/extensions/nuke.yaml @@ -26,4 +26,17 @@ tools: options: tool_configs: - nuke-setup-scene + - name: load + action: true + menu: false # True by default + label: "Loader" + dialog_name: framework_standard_loader_dialog + icon: open + options: + tool_configs: + - nuke-image-loader + - nuke-sequence-loader + - nuke-movie-loader + docked: false + diff --git a/projects/framework-nuke/extensions/plugins/nuke_image_loader.py b/projects/framework-nuke/extensions/plugins/nuke_image_loader.py new file mode 100644 index 0000000000..ebc130bad3 --- /dev/null +++ b/projects/framework-nuke/extensions/plugins/nuke_image_loader.py @@ -0,0 +1,52 @@ +# :coding: utf-8 +# :copyright: Copyright (c) 2024 ftrack +import os + +import nuke + +from ftrack_utils.paths import check_image_sequence +from ftrack_framework_core.plugin import BasePlugin +from ftrack_framework_core.exceptions.plugin import PluginExecutionError + + +class NukeImageLoaderPlugin(BasePlugin): + '''Load an image or sequence into Nuke''' + + name = 'nuke_image_loader' + + def run(self, store): + ''' + Expects the image to load in the :obj:`self.options`, loads the image + ''' + image_path = store.get('component_path') + if not image_path: + raise PluginExecutionError(f'No image path provided in store!') + + n = nuke.nodes.Read() + + sequence_metadata = None + if store.get('is_sequence'): + # Expect path to be on the form folder/plate.%d.exr [1-35], convert to Nuke loadable + # format + sequence_metadata = check_image_sequence(image_path) + image_path = image_path[: image_path.rfind(' ')].replace( + '%d', '%0{}d'.format(sequence_metadata['padding']) + ) + else: + # Check that file exists + if not os.path.exists(image_path): + raise PluginExecutionError( + f'Image file does not exist: {image_path}' + ) + + n['file'].fromUserText(image_path) + + self.logger.debug(f'Created image read node, reading: {image_path}') + + if store.get('is_sequence'): + n['first'].setValue(sequence_metadata['start']) + n['last'].setValue(sequence_metadata['end']) + self.logger.debug( + 'Image sequence frame range set: ' + f'{sequence_metadata["start"]}-{sequence_metadata["end"]}' + ) diff --git a/projects/framework-nuke/extensions/plugins/nuke_movie_loader.py b/projects/framework-nuke/extensions/plugins/nuke_movie_loader.py new file mode 100644 index 0000000000..3fcf697afb --- /dev/null +++ b/projects/framework-nuke/extensions/plugins/nuke_movie_loader.py @@ -0,0 +1,33 @@ +# :coding: utf-8 +# :copyright: Copyright (c) 2024 ftrack +import os + +import nuke + +from ftrack_framework_core.plugin import BasePlugin +from ftrack_framework_core.exceptions.plugin import PluginExecutionError + + +class NukeMovieLoaderPlugin(BasePlugin): + '''Load a movie into Nuke''' + + name = 'nuke_movie_loader' + + def run(self, store): + ''' + Expects the movie to load in the :obj:`self.options`, loads the movie + ''' + movie_path = store.get('component_path') + if not movie_path: + raise PluginExecutionError(f'No movie path provided in store!') + + # Check that file exists + if not os.path.exists(movie_path): + raise PluginExecutionError( + f'Image file does not exist: {movie_path}' + ) + + n = nuke.nodes.Read() + n['file'].fromUserText(movie_path) + + self.logger.debug(f'Created movie read node, reading: {movie_path}') diff --git a/projects/framework-nuke/extensions/tool-configs/nuke-image-loader.yaml b/projects/framework-nuke/extensions/tool-configs/nuke-image-loader.yaml new file mode 100644 index 0000000000..b1276cae81 --- /dev/null +++ b/projects/framework-nuke/extensions/tool-configs/nuke-image-loader.yaml @@ -0,0 +1,33 @@ +type: tool_config +name: nuke-image-loader +config_type: loader +compatible: + entity_types: + - FileComponent + supported_file_extensions: + - ".png" + - ".jpg" + - ".jpeg" + - ".exr" + - ".tif" + - ".tiff" + - ".tga" + - ".bmp" + - ".hdr" + - ".dpx" + - ".cin" + - ".psd" + - ".tx" + +engine: + - type: plugin + tags: + - context + plugin: resolve_entity_path + ui: show_component + + - type: plugin + tags: + - loader + plugin: nuke_image_loader + diff --git a/projects/framework-nuke/extensions/tool-configs/nuke-movie-loader.yaml b/projects/framework-nuke/extensions/tool-configs/nuke-movie-loader.yaml new file mode 100644 index 0000000000..4bc91f375f --- /dev/null +++ b/projects/framework-nuke/extensions/tool-configs/nuke-movie-loader.yaml @@ -0,0 +1,32 @@ +type: tool_config +name: nuke-movie-loader +config_type: loader +compatible: + entity_types: + - FileComponent + supported_file_extensions: + - ".mov" + - ".mp4" + - ".avi" + - ".mpg" + - ".mpeg" + - ".m4v" + - ".mkv" + - ".webm" + - ".wmv" + - ".flv" + - ".vob" + - ".ogv" + +engine: + - type: plugin + tags: + - context + plugin: resolve_entity_path + ui: show_component + + - type: plugin + tags: + - loader + plugin: nuke_movie_loader + diff --git a/projects/framework-nuke/extensions/tool-configs/nuke-sequence-loader.yaml b/projects/framework-nuke/extensions/tool-configs/nuke-sequence-loader.yaml new file mode 100644 index 0000000000..95ff7ac79b --- /dev/null +++ b/projects/framework-nuke/extensions/tool-configs/nuke-sequence-loader.yaml @@ -0,0 +1,32 @@ +type: tool_config +name: nuke-sequence-loader +config_type: loader +compatible: + entity_types: + - SequenceComponent + supported_file_extensions: + - ".png" + - ".jpg" + - ".jpeg" + - ".exr" + - ".tif" + - ".tiff" + - ".tga" + - ".bmp" + - ".hdr" + - ".dpx" + - ".cin" + - ".psd" + - ".tx" +engine: + - type: plugin + tags: + - context + plugin: resolve_entity_path + ui: show_component + + - type: plugin + tags: + - loader + plugin: nuke_image_loader + diff --git a/projects/framework-nuke/release_notes.md b/projects/framework-nuke/release_notes.md index 1133af9086..ad0c777f52 100644 --- a/projects/framework-nuke/release_notes.md +++ b/projects/framework-nuke/release_notes.md @@ -3,6 +3,7 @@ ## upcoming +* [new] Studio asset load capability, covering single file images, movies and image sequences. * [changed] Host, Client instance; Pass run_in_main_thread argument. * [fix] Init; Fix on_run_tool_callback options argument. diff --git a/projects/framework-nuke/resource/bootstrap/menu.py b/projects/framework-nuke/resource/bootstrap/menu.py index cbcf2b4edb..56c29cf0f8 100644 --- a/projects/framework-nuke/resource/bootstrap/menu.py +++ b/projects/framework-nuke/resource/bootstrap/menu.py @@ -8,6 +8,7 @@ def deferred_execution(): ftrack_framework_nuke.execute_startup_tools() + ftrack_framework_nuke.subscribe_action_tools() nuke.addOnCreate(deferred_execution, nodeClass='Root') diff --git a/projects/framework-nuke/source/ftrack_framework_nuke/__init__.py b/projects/framework-nuke/source/ftrack_framework_nuke/__init__.py index f1281b38a6..701b1777bb 100644 --- a/projects/framework-nuke/source/ftrack_framework_nuke/__init__.py +++ b/projects/framework-nuke/source/ftrack_framework_nuke/__init__.py @@ -7,6 +7,11 @@ from functools import partial import platform +try: + from PySide6 import QtWidgets, QtCore +except ImportError: + from PySide2 import QtWidgets, QtCore + import nuke, nukescripts import ftrack_api @@ -25,6 +30,7 @@ from ftrack_utils.usage import set_usage_tracker, UsageTracker from ftrack_framework_nuke.utils import ( + get_nuke_session_identifier, dock_nuke_right, find_nodegraph_viewer, run_in_main_thread, @@ -70,6 +76,7 @@ def get_ftrack_menu(menu_name='ftrack', submenu_name=None): client_instance = None startup_tools = [] +action_tools = [] @run_in_main_thread @@ -78,13 +85,31 @@ def on_run_tool_callback(tool_name, dialog_name=None, options=None): tool_name, dialog_name, options, - dock_func=partial(dock_nuke_right) if dialog_name else None, + dock_func=dock_nuke_right if dialog_name else None, ) # Prevent bug in Nuke were curve editor is activated on docking a panel if options.get("docked"): find_nodegraph_viewer(activate=True) +@run_in_main_thread +def on_subscribe_action_tool_callback( + tool_name, label, dialog_name=None, options=None +): + client_instance.subscribe_action_tool( + tool_name, + label, + dialog_name, + options, + session_identifier_func=get_nuke_session_identifier, + ) + + +def on_exit(): + '''Nuke shutdown, tear down client''' + client_instance.close() + + def bootstrap_integration(framework_extensions_path): global client_instance @@ -171,8 +196,10 @@ def bootstrap_integration(framework_extensions_path): for tool in dcc_config['tools']: run_on = tool.get("run_on") + action = tool.get("action") on_menu = tool.get("menu", True) - name = tool['name'] + label = tool.get('label') or tool.get('name') + name = tool.get('name') dialog_name = tool.get('dialog_name') options = tool.get('options', {}) # TODO: In the future, we should probably emit an event so plugins can @@ -185,25 +212,27 @@ def bootstrap_integration(framework_extensions_path): tool['label'], f'{__name__}.onRunToolCallback("{name}","{dialog_name}", {options})', ) - - if run_on: - if run_on == "startup": - # Add all tools on a global variable as they can't be executed until - # root node is created. - startup_tools.append( - [ - name, - dialog_name, - options, - ] - ) - else: - logger.error( - f"Unsupported run_on value: {run_on} tool section of the " - f"tool {tool.get('name')} on the tool config file: " - f"{dcc_config['name']}. \n Currently supported values:" - f" [startup]" - ) + if run_on == "startup": + startup_tools.append( + [ + name, + dialog_name, + options, + ] + ) + if action: + action_tools.append( + [ + name, + label, + dialog_name, + options, + ] + ) + + # Add shutdown hook, for client to be properly closed when Nuke exists + app = QtWidgets.QApplication.instance() + app.aboutToQuit.connect(on_exit) def execute_startup_tools(): @@ -211,6 +240,11 @@ def execute_startup_tools(): on_run_tool_callback(*tool) +def subscribe_action_tools(): + for tool in action_tools: + on_subscribe_action_tool_callback(*tool) + + # Find and read DCC config try: bootstrap_integration(get_extensions_path_from_environment()) diff --git a/projects/framework-nuke/source/ftrack_framework_nuke/utils/__init__.py b/projects/framework-nuke/source/ftrack_framework_nuke/utils/__init__.py index a3d18e0a24..3b86d2ca4a 100644 --- a/projects/framework-nuke/source/ftrack_framework_nuke/utils/__init__.py +++ b/projects/framework-nuke/source/ftrack_framework_nuke/utils/__init__.py @@ -2,6 +2,7 @@ # :copyright: Copyright (c) 2024 ftrack from functools import wraps import threading +import socket try: from PySide6 import QtWidgets @@ -12,6 +13,14 @@ from nukescripts import panels +def get_nuke_session_identifier(): + computer_name = socket.gethostname() + # Get the Maya scene name + script_name = nuke.root().name().split('/')[-1] + identifier = f"{script_name}_Nuke_{computer_name}" + return identifier + + def dock_nuke_right(widget): '''Dock *widget*, to the right of the properties panel in Nuke''' class_name = widget.__class__.__name__