Skip to content

Commit

Permalink
Merge pull request ynput#121 from ynput/enhancement/generic_load_test
Browse files Browse the repository at this point in the history
Generic Loader OTLs
  • Loading branch information
BigRoy authored Oct 31, 2024
2 parents 5d7e452 + 1b06f3d commit 952ded4
Show file tree
Hide file tree
Showing 44 changed files with 1,700 additions and 120 deletions.
123 changes: 118 additions & 5 deletions client/ayon_houdini/api/hda_utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Heper functions for load HDA"""

import os
import re
import uuid
from typing import List

Expand All @@ -15,8 +16,11 @@
get_folder_by_path,
get_product_by_name,
get_version_by_name,
get_representation_by_name
get_representation_by_name,
get_representations
)
from ayon_core.pipeline import Anatomy
from ayon_core.lib import StringTemplate
from ayon_core.pipeline.context_tools import (
get_current_project_name,
get_current_folder_path
Expand Down Expand Up @@ -145,14 +149,63 @@ def get_representation_path(
if use_ayon_entity_uri:
path = get_ayon_entity_uri_from_representation_context(context)
else:
path = get_representation_path_from_context(context)
path = _get_filepath_from_context(context)
# Load fails on UNC paths with backslashes and also
# fails to resolve @sourcename var with backslashed
# paths correctly. So we force forward slashes
path = path.replace("\\", "/")
return path


def _remove_format_spec(template: str, key: str) -> str:
"""Remove format specifier from a format token in formatting string.
For example, change `{frame:0>4d}` into `{frame}`
Examples:
>>> remove_format_spec("{frame:0>4d}", "frame")
'{frame}'
>>> remove_format_spec("{digit:04d}/{frame:0>4d}", "frame")
'{digit:04d}/{udim}_{frame}'
>>> remove_format_spec("{a: >4}/{aa: >4}", "a")
'{a}/{aa: >4}'
"""
# Find all {key:foobar} and remove the `:foobar`
# Pattern will be like `({key):[^}]+(})` where we use the captured groups
# to keep those parts in the resulting string
pattern = f"({{{key}):[^}}]+(}})"
return re.sub(pattern, r"\1\2", template)


def _get_filepath_from_context(context: dict):
"""Format file path for sequence with $F or <UDIM>."""
# The path is either a single file or sequence in a folder.
# Format frame as $F and udim as <UDIM>
representation = context["representation"]
frame = representation["context"].get("frame")
udim = representation["context"].get("udim")
if frame is not None or udim is not None:
template: str = representation["attrib"]["template"]
repre_context: dict = representation["context"]
if udim is not None:
repre_context["udim"] = "<UDIM>"
template = _remove_format_spec(template, "udim")
if frame is not None:
# Substitute frame number in sequence with $F with padding
repre_context["frame"] = "$F{}".format(len(frame)) # e.g. $F4
template = _remove_format_spec(template, "frame")

project_name: str = repre_context["project"]["name"]
anatomy = Anatomy(project_name, project_entity=context["project"])
repre_context["root"] = anatomy.roots
path = StringTemplate(template).format(repre_context)
else:
path = get_representation_path_from_context(context)

# Load fails on UNC paths with backslashes and also
# fails to resolve @sourcename var with backslashed
# paths correctly. So we force forward slashes
return os.path.normpath(path).replace("\\", "/")


def _get_thumbnail(project_name: str, version_id: str, thumbnail_dir: str):
folder = hou.text.expandString(thumbnail_dir)
path = os.path.join(folder, "{}_thumbnail.jpg".format(version_id))
Expand Down Expand Up @@ -357,7 +410,10 @@ def on_flag_changed(node, **kwargs):
if not images:
return

brightness = 0.3 if node.isBypassed() else 1.0
# This may trigger on a node that can't be bypassed, like `ObjNode` so
# consider those never bypassed
is_bypassed = hasattr(node, "isBypassed") and node.isBypassed()
brightness = 0.3 if is_bypassed else 1.0
has_changes = False
node_path = node.path()
for image in images:
Expand Down Expand Up @@ -653,11 +709,64 @@ def select_product_name(node):
product_parm.pressButton() # allow any callbacks to trigger


def get_available_representations(node):
"""Return the representation list for node.
Args:
node (hou.Node): Node to query selected version's representations for.
Returns:
list[str]: representation names for the product version.
"""

project_name = node.evalParm("project_name") or get_current_project_name()
folder_path = node.evalParm("folder_path")
product_name = node.evalParm("product_name")
version = node.evalParm("version")

if not all([
project_name, folder_path, product_name, version
]):
return []

try:
version = int(version.strip())
except ValueError:
load_message_parm = node.parm("load_message")
load_message_parm.set(f"Invalid version format: '{version}'\n"
"Make sure to set a valid version number.")
return

folder_entity = get_folder_by_path(
project_name,
folder_path=folder_path,
fields={"id"}
)
product_entity = get_product_by_name(
project_name,
product_name=product_name,
folder_id=folder_entity["id"],
fields={"id"})
version_entity = get_version_by_name(
project_name,
version,
product_id=product_entity["id"],
fields={"id"})
representations = get_representations(
project_name,
version_ids={version_entity["id"]},
fields={"name"}
)
representations_names = [n["name"] for n in representations]
return representations_names


def set_to_latest_version(node):
"""Callback on product name change
Refresh version parameter value by setting its value to
the latest version of the selected product.
Refresh version and representation parameters value by setting
their value to the latest version and representation of
the selected product.
Args:
node (hou.OpNode): The HDA node.
Expand All @@ -667,6 +776,10 @@ def set_to_latest_version(node):
if versions:
node.parm("version").set(str(versions[0]))

representations = get_available_representations(node)
if representations:
node.parm("representation_name").set(representations[0])


# region Parm Expressions
# Callbacks used for expression on HDAs (e.g. Load Asset or Load Shot LOP)
Expand Down
70 changes: 67 additions & 3 deletions client/ayon_houdini/api/lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
import six
import ayon_api

import hou

from ayon_core.lib import StringTemplate
from ayon_core.settings import get_current_project_settings
from ayon_core.pipeline import (
Expand All @@ -27,8 +29,6 @@
from ayon_core.tools.utils import PopupUpdateKeys, SimplePopup
from ayon_core.tools.utils.host_tools import get_tool_by_name

import hou


self = sys.modules[__name__]
self._parent = None
Expand Down Expand Up @@ -1400,7 +1400,7 @@ def find_active_network(category, default):
Arguments:
category (hou.NodeTypeCategory): The node network category type.
default (str): The default path to fallback to if no active pane
is found with the given category.
is found with the given category, e.g. "/obj"
Returns:
hou.Node: The node network to return.
Expand Down Expand Up @@ -1521,3 +1521,67 @@ def start_workfile_template_builder():
build_workfile_template(workfile_creation_enabled=True)
except TemplateProfileNotFound:
log.warning("Template profile not found. Skipping...")


def show_node_parmeditor(node):
"""Show Parameter Editor for the Node.
Args:
node (hou.Node): node instance
"""

# Check if there's a floating parameter editor pane with its node set to the specified node.
for tab in hou.ui.paneTabs():
if (
tab.type() == hou.paneTabType.Parm
and tab.isFloating()
and tab.currentNode() == node
):
tab.setIsCurrentTab()
return

# We are using the hscript to create and set the network path of the pane
# because hscript can set the node path without selecting the node.
# Create a floating pane and set its name to the node path.
hou.hscript(
f"pane -F -m parmeditor -n {node.path()}"
)
# Hide network controls, turn linking off and set operator node path.
hou.hscript(
f"pane -a 1 -l 0 -H {node.path()} {node.path()}"
)


def connect_file_parm_to_loader(file_parm: hou.Parm):
"""Connect the given file parm to a generic loader.
If the parm is already connected to a generic loader node, go to that node.
"""

from .pipeline import get_or_create_avalon_container

referenced_parm = file_parm.getReferencedParm()

# If the parm has reference
if file_parm != referenced_parm:
referenced_node = referenced_parm.getReferencedParm().node()
if referenced_node.type().name() == "ayon::generic_loader::1.0":
show_node_parmeditor(referenced_node)
return

# Create a generic loader node and reference its file parm
main_container = get_or_create_avalon_container()

node_name = f"{file_parm.node().name()}_{file_parm.name()}_loader"
load_node = main_container.createNode("ayon::generic_loader",
node_name=node_name)
load_node.moveToGoodPosition()

# Set relative reference via hscript. This avoids the issues of
# `setExpression` e.g. having a keyframe.
relative_path = file_parm.node().relativePathTo(load_node)
expression = rf'chs\(\"{relative_path}/file\"\)' # noqa
hou.hscript(
'opparm -r'
f' {file_parm.node().path()} {file_parm.name()} \`{expression}\`'
)
show_node_parmeditor(load_node)
Loading

0 comments on commit 952ded4

Please sign in to comment.