Skip to content

Commit

Permalink
Add OpenRV Management
Browse files Browse the repository at this point in the history
  • Loading branch information
ccaillot committed Oct 20, 2023
1 parent 9245dce commit a89a5d1
Show file tree
Hide file tree
Showing 40 changed files with 2,331 additions and 26 deletions.
1 change: 1 addition & 0 deletions openpype/hooks/pre_add_last_workfile_arg.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class AddLastWorkfileToLaunchArgs(PreLaunchHook):
"tvpaint",
"substancepainter",
"aftereffects",
"openrv"
}
launch_types = {LaunchTypes.local}

Expand Down
1 change: 1 addition & 0 deletions openpype/hooks/pre_ocio_hook.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class OCIOEnvHook(PreLaunchHook):
"nuke",
"hiero",
"resolve",
"openrv"
}
launch_types = set()

Expand Down
47 changes: 46 additions & 1 deletion openpype/hosts/nuke/api/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
)
from openpype.pipeline.workfile import BuildWorkfile
from openpype.tools.utils import host_tools

from openpype.lib.applications import ApplicationManager
from .command import viewer_update_and_undo_stop
from .lib import (
Context,
Expand Down Expand Up @@ -349,6 +349,51 @@ def _install_menu():
# adding shortcuts
add_shortcuts_from_presets()

# adding rv
add_rv_from_presets()

def add_rv_from_presets():
rv_settings = get_current_project_settings()["openrv"]['openrv_nuke_integration']
if not rv_settings['rvnuke_enabled']:
return

app_manager = ApplicationManager()
openrv_app = app_manager.find_latest_available_variant_for_group("openrv")
rv_exec_path = str(openrv_app.find_executable())
rv_root = os.path.dirname(os.path.dirname(rv_exec_path))
rv_nuke_path = os.path.join(rv_root, 'plugins', 'SupportFiles', 'rvnuke')
if os.path.exists(rv_nuke_path):
nuke.pluginAddPath(rv_nuke_path)
log.info("RV Nuke path added: {}".format(rv_nuke_path))
else:
log.warning("RV Nuke path not found: {}".format(rv_nuke_path))

if not rv_exec_path:
return
try:
import rvNuke
rv_pref_panel = rvNuke.RvPreferencesPanel()
rv_pref_panel.rvPrefs.prefs["rvExecPath"] = rv_exec_path
rv_pref_panel.rvPrefs.saveToDisk()
except ImportError:
log.warning("rvNuke not found")

nuke.addOnCreate(add_rv_shortcut, nodeClass="Root")
return


def add_rv_shortcut():
rv_global_settings = get_current_project_settings()["openrv"]
rv_settings = rv_global_settings['openrv_nuke_integration']
if rv_settings['rvnuke_open_in_rv_shortcut']:
menubar = nuke.menu("Nuke")
menu_item = menubar.findItem("RV/View in RV")
if menu_item:
menu_item.setShortcut("Alt+v")
menu_item.setShortcut(rv_settings['rvnuke_open_in_rv_shortcut'])
log.info("Adding Shortcut `{}` to `{}`".format(
'Open in RV',
rv_settings['rvnuke_open_in_rv_shortcut']))

def change_context_label():
menubar = nuke.menu("Nuke")
Expand Down
10 changes: 10 additions & 0 deletions openpype/hosts/openrv/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from .addon import (
OpenRVAddon,
OPENRV_ROOT_DIR
)


__all__ = (
"OpenRVAddon",
"OPENRV_ROOT_DIR"
)
33 changes: 33 additions & 0 deletions openpype/hosts/openrv/addon.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import os
from openpype.modules import OpenPypeModule
from openpype.modules.interfaces import IHostAddon

OPENRV_ROOT_DIR = os.path.dirname(os.path.abspath(__file__))


class OpenRVAddon(OpenPypeModule, IHostAddon):
name = "openrv"
host_name = "openrv"

def initialize(self, module_settings):
self.enabled = True

def add_implementation_envs(self, env, app):
"""Modify environments to contain all required for implementation."""
# Set default environments if are not set via settings
defaults = {
"OPENPYPE_LOG_NO_COLORS": "True"
}
for key, value in defaults.items():
if not env.get(key):
env[key] = value

def get_launch_hook_paths(self, app):
if app.host_name != self.host_name:
return []
return [
os.path.join(OPENRV_ROOT_DIR, "hooks")
]

def get_workfile_extensions(self):
return [".rv"]
10 changes: 10 additions & 0 deletions openpype/hosts/openrv/api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
"""OpenRV OpenPype host API."""

from .pipeline import (
OpenRVHost
)

__all__ = [
"OpenRVHost"
]
34 changes: 34 additions & 0 deletions openpype/hosts/openrv/api/commands.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import logging

import rv
from openpype.pipeline.context_tools import get_current_project_asset

log = logging.getLogger(__name__)


def reset_frame_range():
""" Set timeline frame range.
"""
asset_doc = get_current_project_asset()
asset_name = asset_doc["name"]
asset_data = asset_doc["data"]

frame_start = asset_data.get("frameStart")
frame_end = asset_data.get("frameEnd")

if frame_start is None or frame_end is None:
log.warning("No edit information found for {}".format(asset_name))
return

rv.commands.setFrameStart(frame_start)
rv.commands.setFrameEnd(frame_end)
rv.commands.setFrame(frame_start)


def set_session_fps():
""" Set session fps.
"""
asset_doc = get_current_project_asset()
asset_data = asset_doc["data"]
fps = float(asset_data.get("fps", 25))
rv.commands.setFPS(fps)
36 changes: 36 additions & 0 deletions openpype/hosts/openrv/api/lib.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import contextlib

import rv


@contextlib.contextmanager
def maintained_view():
"""Reset to original view node after context"""
original = rv.commands.viewNode()
try:
yield
finally:
rv.commands.setViewNode(original)


@contextlib.contextmanager
def active_view(node):
"""Set active view during context"""
with maintained_view():
rv.commands.setViewNode(node)
yield


def group_member_of_type(group_node, member_type):
"""Return first member of group that is of the given node type.
This is similar to `rv.extra_commands.nodesInGroupOfType` but only
returns the first entry directly if it has any match.
Args:
group_node (str): The group node to search in.
member_type (str): The node type to search for.
Returns:
str or None: The first member found of given type or None
"""
for node in rv.commands.nodesInGroup(group_node):
if rv.commands.nodeType(node) == member_type:
return node
106 changes: 106 additions & 0 deletions openpype/hosts/openrv/api/ocio.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
"""Helper functions to apply OCIO colorspace settings on groups.
This tries to set the relevant OCIO settings on the group's look and render
pipeline similar to what the OpenColorIO Basic Color Management package does in
OpenRV through its `ocio_source_setup` python file.
This assumes that the OpenColorIO Basic Color Management package of RV is both
installed and loaded.
"""
import rv.commands
import rv.qtutils

from .lib import (
group_member_of_type,
active_view
)


class OCIONotActiveForGroup(RuntimeError):
"""Error raised when OCIO is not enabled on the group node."""


def get_group_ocio_look_node(group):
"""Return OCIOLook node from source group"""
pipeline = group_member_of_type(group, "RVLookPipelineGroup")
if pipeline:
return group_member_of_type(pipeline, "OCIOLook")


def get_group_ocio_file_node(group):
"""Return OCIOFile node from source group"""
pipeline = group_member_of_type(group, "RVLinearizePipelineGroup")
if pipeline:
return group_member_of_type(pipeline, "OCIOFile")


def set_group_ocio_colorspace(group, colorspace):
"""Set the group's OCIOFile node ocio.inColorSpace property.
This only works if OCIO is already 'active' for the group. T
"""
import ocio_source_setup # noqa, RV OCIO package
node = get_group_ocio_file_node(group)

if not node:
raise OCIONotActiveForGroup(
"Unable to find OCIOFile node for {}".format(group)
)

rv.commands.setStringProperty(
f"{node}.ocio.inColorSpace", [colorspace], True
)


def set_current_ocio_active_state(state):
"""Set the OCIO state for the currently active source.
This is a hacky workaround to enable/disable the OCIO active state for
a source since it appears to be that there's no way to explicitly trigger
this callback from the `ocio_source_setup.OCIOSourceSetupMode` instance
which does these changes.
"""
# TODO: Make this logic less hacky
# See: https://community.shotgridsoftware.com/t/how-to-enable-disable-ocio-and-set-ocio-colorspace-for-group-using-python/17178 # noqa

group = rv.commands.viewNode()
ocio_node = get_group_ocio_file_node(group)
if state == bool(ocio_node):
# Already in correct state
return

window = rv.qtutils.sessionWindow()
menu_bar = window.menuBar()
for action in menu_bar.actions():
if action.text() != "OCIO" or action.toolTip() != "OCIO":
continue

ocio_menu = action.menu()

for ocio_action in ocio_menu.actions():
if ocio_action.toolTip() == "File Color Space":
# The first entry is for "current source" instead
# of all sources so we need to break the for loop
# The first action of the file color space menu
# is the "Active" action. So lets take that one
active_action = ocio_action.menu().actions()[0]

active_action.trigger()
return

raise RuntimeError(
"Unable to set active state for current source. Make "
"sure the OCIO package is installed and loaded."
)


def set_group_ocio_active_state(group, state):
"""Set the OCIO state for the 'currently active source'.
This is a hacky workaround to enable/disable the OCIO active state for
a source since it appears to be that there's no way to explicitly trigger
this callback from the `ocio_source_setup.OCIOSourceSetupMode` instance
which does these changes.
"""
ocio_node = get_group_ocio_file_node(group)
if state == bool(ocio_node):
# Already in correct state
return

with active_view(group):
set_current_ocio_active_state(state)
Loading

0 comments on commit a89a5d1

Please sign in to comment.