Skip to content

Commit

Permalink
Merge branch 'develop' into enhancement/usd_load_ayon_uri
Browse files Browse the repository at this point in the history
  • Loading branch information
BigRoy authored Aug 27, 2024
2 parents fdfdddf + bfa6275 commit 53930f2
Show file tree
Hide file tree
Showing 64 changed files with 1,996 additions and 243 deletions.
68 changes: 32 additions & 36 deletions client/ayon_houdini/api/colorspace.py
Original file line number Diff line number Diff line change
@@ -1,59 +1,55 @@
from typing import List

import attr
import hou
from ayon_houdini.api.lib import get_color_management_preferences
from ayon_core.pipeline.colorspace import get_display_view_colorspace_name
from ayon_core.pipeline.colorspace import (
get_display_view_colorspace_name,
get_ocio_config_colorspaces
)


@attr.s
class LayerMetadata(object):
"""Data class for Render Layer metadata."""
frameStart = attr.ib()
frameEnd = attr.ib()
products: "List[RenderProduct]" = attr.ib()


@attr.s
class RenderProduct(object):
"""Getting Colorspace as
Specific Render Product Parameter for submitting
publish job.
"""
"""Specific Render Product Parameter for submitting."""
colorspace = attr.ib() # colorspace
view = attr.ib()
productName = attr.ib(default=None)


class ARenderProduct(object):
"""This is the minimal data structure required to get
`ayon_core.pipeline.farm.pyblish_functions.create_instances_for_aov` to
work with deadline addon's job submissions."""
# TODO: The exact data structure should actually be defined in core for all
# addons to align.
def __init__(self, aov_names: List[str]):
colorspace = get_scene_linear_colorspace()
products = [
RenderProduct(colorspace=colorspace, productName=aov_name)
for aov_name in aov_names
]
self.layer_data = LayerMetadata(products=products)

def __init__(self):
"""Constructor."""
# Initialize
self.layer_data = self._get_layer_data()
self.layer_data.products = self.get_colorspace_data()

def _get_layer_data(self):
return LayerMetadata(
frameStart=int(hou.playbar.frameRange()[0]),
frameEnd=int(hou.playbar.frameRange()[1]),
)

def get_colorspace_data(self):
"""To be implemented by renderer class.

This should return a list of RenderProducts.
def get_scene_linear_colorspace():
"""Return colorspace name for Houdini's OCIO config scene linear role.
Returns:
list: List of RenderProduct
By default, renderers in Houdini render output images in the scene linear
role colorspace.
"""
data = get_color_management_preferences()
colorspace_data = [
RenderProduct(
colorspace=data["display"],
view=data["view"],
productName=""
)
]
return colorspace_data
Returns:
Optional[str]: The colorspace name for the 'scene_linear' role in
the OCIO config Houdini is currently set to.
"""
ocio_config_path = hou.Color.ocio_configPath()
colorspaces = get_ocio_config_colorspaces(ocio_config_path)
return colorspaces["roles"].get("scene_linear", {}).get("colorspace")


def get_default_display_view_colorspace():
Expand Down
160 changes: 130 additions & 30 deletions client/ayon_houdini/api/hda_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@

from ayon_houdini.api import lib

from qtpy import QtCore, QtWidgets
from qtpy import QtCore, QtWidgets, QtGui
import hou


Expand Down Expand Up @@ -129,14 +129,6 @@ def update_info(node, context):
if node.evalParm(key) != value}
parms["load_message"] = "" # clear any warnings/errors

# Update the product type filter to match the type
current = node.evalParm("product_type")
product_type = context["product"]["productType"]
if current and current != product_type:
# If current is empty we consider no filtering applied and we allow
# that to be a state that needs no switching
parms["product_type"] = product_type

# Note that these never trigger any parm callbacks since we do not
# trigger the `parm.pressButton` and programmatically setting values
# in Houdini does not trigger callbacks automatically
Expand Down Expand Up @@ -454,7 +446,7 @@ def __init__(self, parent=None):

folder_widget = SimpleFoldersWidget(parent=self)

accept_button = QtWidgets.QPushButton("Accept")
accept_button = QtWidgets.QPushButton("Set folder path")

main_layout = QtWidgets.QVBoxLayout(self)
main_layout.addWidget(project_widget, 0)
Expand Down Expand Up @@ -510,6 +502,8 @@ def select_folder_path(node):
Args:
node (hou.OpNode): The HDA node.
"""
cursor_pos = QtGui.QCursor.pos()

main_window = lib.get_main_window()

project_name = node.evalParm("project_name")
Expand All @@ -528,6 +522,12 @@ def _select_folder_path():

dialog.setStyleSheet(load_stylesheet())

# Make it appear like a pop-up near cursor
dialog.resize(300, 600)
dialog.setWindowFlags(QtCore.Qt.Popup)
pos = dialog.mapToGlobal(cursor_pos - QtCore.QPoint(300, 0))
dialog.move(pos)

result = dialog.exec_()
if result != QtWidgets.QDialog.Accepted:
return
Expand Down Expand Up @@ -555,38 +555,138 @@ def _select_folder_path():
folder_parm.pressButton() # allow any callbacks to trigger


def get_available_products(node):
"""Return products menu items
It gets a list of available products of the specified product types
within the specified folder path with in the specified project.
Users can specify those in the HDA parameters.
class SelectProductDialog(QtWidgets.QDialog):
"""Simple dialog to allow a user to select a product."""

Args:
node (hou.OpNode): The HDA node.
def __init__(self, project_name, folder_id, parent=None):
super(SelectProductDialog, self).__init__(parent)
self.setWindowTitle("Select a Product")
self.setStyleSheet(load_stylesheet())

self.project_name = project_name
self.folder_id = folder_id

# Create widgets and layout
product_types_widget = QtWidgets.QComboBox()
products_widget = QtWidgets.QListWidget()
accept_button = QtWidgets.QPushButton("Set product name")

main_layout = QtWidgets.QVBoxLayout(self)
main_layout.setContentsMargins(0, 0, 0, 0)
main_layout.addWidget(product_types_widget, 0)
main_layout.addWidget(products_widget, 1)
main_layout.addWidget(accept_button, 0)

self.product_types_widget = product_types_widget
self.products_widget = products_widget

# Connect Signals
product_types_widget.currentTextChanged.connect(self.on_product_type_changed)
products_widget.itemDoubleClicked.connect(self.accept)
accept_button.clicked.connect(self.accept)

# Initialize widgets contents
product_types_widget.addItems(self.get_product_types())
product_type = self.get_selected_product_type()
self.set_product_type(product_type)

def get_selected_product(self) -> str:
if self.products_widget.currentItem():
return self.products_widget.currentItem().text()
return ""

def get_selected_product_type(self) -> str:
return self.product_types_widget.currentText()

def get_product_types(self) -> List[str]:
"""return default product types.
"""

return [
"*",
"animation",
"camera",
"model",
"pointcache",
"usd",
]

def on_product_type_changed(self, product_type: str):
self.set_product_type(product_type)

def set_product_type(self, product_type: str):
self.product_types_widget.setCurrentText(product_type)

if self.product_types_widget.currentText() != product_type:
# Product type does not exist
return

# Populate products list
products = self.get_available_products(product_type)
self.products_widget.clear()
if products:
self.products_widget.addItems(products)

def set_selected_product_name(self, product_name: str):
matching_items = self.products_widget.findItems(
product_name, QtCore.Qt.MatchFixedString)
if matching_items:
self.products_widget.setCurrentItem(matching_items[0])

def get_available_products(self, product_type):

if product_type == "*":
product_type = ""

product_types = [product_type] if product_type else None

products = ayon_api.get_products(
self.project_name,
folder_ids=[self.folder_id],
product_types=product_types
)

return list(sorted(product["name"] for product in products))


def select_product_name(node):
"""Show a modal pop-up dialog to allow user to select a product name
under the current folder entity as defined on the node's parameters.
Applies the chosen value to the `product_name` parm on the node."""

cursor_pos = QtGui.QCursor.pos()

Returns:
list[str]: Product names for Products menu.
"""
project_name = node.evalParm("project_name")
folder_path = node.evalParm("folder_path")
product_type = node.evalParm("product_type")
product_parm = node.parm("product_name")

folder_entity = ayon_api.get_folder_by_path(project_name,
folder_path,
fields={"id"})
if not folder_entity:
return []

# Apply filter only if any value is set
product_types = [product_type] if product_type else None

products = ayon_api.get_products(
return

dialog = SelectProductDialog(
project_name,
folder_ids=[folder_entity["id"]],
product_types=product_types
folder_entity["id"],
parent=lib.get_main_window()
)
dialog.set_selected_product_name(product_parm.eval())

dialog.resize(300, 600)
dialog.setWindowFlags(QtCore.Qt.Popup)
pos = dialog.mapToGlobal(cursor_pos - QtCore.QPoint(300, 0))
dialog.move(pos)
result = dialog.exec_()

if result != QtWidgets.QDialog.Accepted:
return
selected_product = dialog.get_selected_product()

return list(sorted(product["name"] for product in products))
if selected_product:
product_parm.set(selected_product)
product_parm.pressButton() # allow any callbacks to trigger


def set_to_latest_version(node):
Expand Down
31 changes: 30 additions & 1 deletion client/ayon_houdini/api/lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -579,12 +579,41 @@ def replace(match):

def get_color_management_preferences():
"""Get default OCIO preferences"""
return {

preferences = {
"config": hou.Color.ocio_configPath(),
"display": hou.Color.ocio_defaultDisplay(),
"view": hou.Color.ocio_defaultView()
}

# Note: For whatever reason they are cases where `view` may be an empty
# string even though a valid default display is set where `PyOpenColorIO`
# does correctly return the values.
# Workaround to get the correct default view
if preferences["config"] and not preferences["view"]:
log.debug(
"Houdini `hou.Color.ocio_defaultView()` returned empty value."
" Falling back to `PyOpenColorIO` to get the default view.")
try:
import PyOpenColorIO
except ImportError:
log.warning(
"Unable to workaround empty return value of "
"`hou.Color.ocio_defaultView()` because `PyOpenColorIO` is "
"not available.")
return preferences

config_path = preferences["config"]
config = PyOpenColorIO.Config.CreateFromFile(config_path)
display = config.getDefaultDisplay()
assert display == preferences["display"], \
"Houdini default OCIO display must match config default display"
view = config.getDefaultView(display)
preferences["display"] = display
preferences["view"] = view

return preferences


def get_obj_node_output(obj_node):
"""Find output node.
Expand Down
4 changes: 3 additions & 1 deletion client/ayon_houdini/api/workfile_template_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,8 @@ def create_placeholder_node(self, node_name=None):
Feel free to override it in different workfile build plugins.
"""

node = hou.node("/out").createNode("null", node_name)
node = hou.node("/out").createNode(
"null", node_name, force_valid_node_name=True)
node.moveToGoodPosition()
parms = node.parmTemplateGroup()
for parm in {"execute", "renderdialog"}:
Expand Down Expand Up @@ -230,6 +231,7 @@ def update_placeholder(self, placeholder_item, placeholder_data):

# Update node name
node_name = self.get_placeholder_node_name(placeholder_data)
node_name = hou.text.variableName(node_name)
placeholder_node.setName(node_name, unique_name=True)

def delete_placeholder(self, placeholder):
Expand Down
Loading

0 comments on commit 53930f2

Please sign in to comment.