Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Load LOPs: Support update via context options (replace parm callbacks with expressions) #112

Open
wants to merge 11 commits into
base: develop
Choose a base branch
from
209 changes: 119 additions & 90 deletions client/ayon_houdini/api/hda_utils.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"""Helper functions for load HDA"""

import os
import contextlib
import uuid
from typing import List

Expand Down Expand Up @@ -33,6 +32,14 @@
import hou


def get_session_cache() -> dict:
"""Get a persistent `hou.session.ayon_cache` dict"""
cache = getattr(hou.session, "ayon_cache", None)
if cache is None:
hou.session.ayon_cache = cache = {}
return cache


def is_valid_uuid(value) -> bool:
"""Return whether value is a valid UUID"""
try:
Expand All @@ -42,16 +49,6 @@ def is_valid_uuid(value) -> bool:
return True


@contextlib.contextmanager
def _unlocked_parm(parm):
"""Unlock parm during context; will always lock after"""
try:
parm.lock(False)
yield
finally:
parm.lock(True)


def get_available_versions(node):
"""Return the versions list for node.

Expand Down Expand Up @@ -99,7 +96,7 @@ def get_available_versions(node):
return version_names


def update_info(node, context):
def set_node_representation_from_context(node, context):
"""Update project, folder, product, version, representation name parms.

Arguments:
Expand Down Expand Up @@ -128,11 +125,6 @@ def update_info(node, context):
}
parms = {key: value for key, value in parms.items()
if node.evalParm(key) != value}
parms["load_message"] = "" # clear any warnings/errors

# 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
node.setParms(parms)


Expand All @@ -155,36 +147,21 @@ def _get_thumbnail(project_name: str, version_id: str, thumbnail_dir: str):
return path


def set_representation(node, representation_id: str):
file_parm = node.parm("file")
if not representation_id:
# Clear filepath and thumbnail
with _unlocked_parm(file_parm):
file_parm.set("")
set_node_thumbnail(node, None)
return

project_name = (
node.evalParm("project_name")
or get_current_project_name()
)

def get_representation_path(
project_name: str,
representation_id: str,
use_ayon_entity_uri: bool
) -> str:
# Ignore invalid representation ids silently
# TODO remove - added for backwards compatibility with OpenPype scenes
if not is_valid_uuid(representation_id):
return
return ""

repre_entity = get_representation_by_id(project_name, representation_id)
if not repre_entity:
return
return ""

context = get_representation_context(project_name, repre_entity)
update_info(node, context)

if node.parm("use_ayon_entity_uri"):
use_ayon_entity_uri = node.evalParm("use_ayon_entity_uri")
else:
use_ayon_entity_uri = False
if use_ayon_entity_uri:
path = get_ayon_entity_uri_from_representation_context(context)
else:
Expand All @@ -193,9 +170,25 @@ def set_representation(node, representation_id: str):
# fails to resolve @sourcename var with backslashed
# paths correctly. So we force forward slashes
path = path.replace("\\", "/")
with _unlocked_parm(file_parm):
file_parm.set(path)
return path


def set_representation(node, representation_id: str):
# For now this only updates the thumbnail, but it may update more over time
_update_thumbnail(node, representation_id)


def _update_thumbnail(node, representation_id):
# TODO: Unused currently; add support again for thumbnail updates
if not representation_id:
set_node_thumbnail(node, None)
return

project_name = (
node.evalParm("project_name")
or get_current_project_name()
)
repre_entity = get_representation_by_id(project_name, representation_id)
if node.evalParm("show_thumbnail"):
# Update thumbnail
# TODO: Cache thumbnail path as well
Expand Down Expand Up @@ -264,35 +257,16 @@ def on_representation_id_changed(node):
set_representation(node, repre_id)


def on_representation_parms_changed(node, force=False):
"""
Usually used as callback to the project, folder, product, version and
representation parms which on change - would result in a different
representation id to be resolved.

Args:
node (hou.Node): Node to update.
force (Optional[bool]): Whether to force the callback to retrigger
even if the representation id already matches. For example, when
needing to resolve the filepath in a different way.
"""
project_name = node.evalParm("project_name") or get_current_project_name()
representation_id = get_representation_id(
project_name=project_name,
folder_path=node.evalParm("folder_path"),
product_name=node.evalParm("product_name"),
version=node.evalParm("version"),
representation_name=node.evalParm("representation_name"),
load_message_parm=node.parm("load_message")
def get_node_expected_representation_id(node) -> str:
project_name = node.evalParm(
"project_name") or get_current_project_name()
return get_representation_id(
project_name=project_name,
folder_path=node.evalParm("folder_path"),
product_name=node.evalParm("product_name"),
version=node.evalParm("version"),
representation_name=node.evalParm("representation_name"),
)
if representation_id is None:
representation_id = ""
else:
representation_id = str(representation_id)

if force or node.evalParm("representation") != representation_id:
node.parm("representation").set(representation_id)
node.parm("representation").pressButton() # trigger callback


def get_representation_id(
Expand All @@ -301,7 +275,6 @@ def get_representation_id(
product_name,
version,
representation_name,
load_message_parm,
):
"""Get representation id.

Expand All @@ -311,14 +284,14 @@ def get_representation_id(
product_name (str): Product name
version (str): Version name as string
representation_name (str): Representation name
load_message_parm (hou.Parm): A string message parm to report
any error messages to.

Returns:
Optional[str]: Representation id or None if not found.
str: Representation id or None if not found.

"""
Raises:
ValueError: If the entity could not be resolved with input values.

"""
if not all([
project_name, folder_path, product_name, version, representation_name
]):
Expand All @@ -330,15 +303,14 @@ def get_representation_id(
"representation": representation_name
}
missing = ", ".join(key for key, value in labels.items() if not value)
load_message_parm.set(f"Load info incomplete. Found empty: {missing}")
return
raise ValueError(f"Load info incomplete. Found empty: {missing}")

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

folder_entity = get_folder_by_path(project_name,
folder_path=folder_path,
Expand All @@ -347,36 +319,32 @@ def get_representation_id(
# This may be due to the project not existing - so let's validate
# that first
if not get_project(project_name):
load_message_parm.set(f"Project not found: '{project_name}'")
return
load_message_parm.set(f"Folder not found: '{folder_path}'")
return
raise ValueError(f"Project not found: '{project_name}'")
raise ValueError(f"Folder not found: '{folder_path}'")

product_entity = get_product_by_name(
project_name,
product_name=product_name,
folder_id=folder_entity["id"],
fields={"id"})
if not product_entity:
load_message_parm.set(f"Product not found: '{product_name}'")
return
raise ValueError(f"Product not found: '{product_name}'")

version_entity = get_version_by_name(
project_name,
version,
product_id=product_entity["id"],
fields={"id"})
if not version_entity:
load_message_parm.set(f"Version not found: '{version}'")
return
raise ValueError(f"Version not found: '{version}'")

representation_entity = get_representation_by_name(
project_name,
representation_name,
version_id=version_entity["id"],
fields={"id"})
if not representation_entity:
load_message_parm.set(
f"Representation not found: '{representation_name}'.")
return
raise ValueError(f"Representation not found: '{representation_name}'.")
return representation_entity["id"]


Expand Down Expand Up @@ -714,3 +682,64 @@ def set_to_latest_version(node):
versions = get_available_versions(node)
if versions:
node.parm("version").set(str(versions[0]))


# region Parm Expressions
# Callbacks used for expression on HDAs (e.g. Load Asset or Load Shot LOP)
# Note that these are called many times, sometimes even multiple times when
# the Parameters tab is open on the node. So some caching is performed to
# avoid expensive re-querying.
def expression_clear_cache(subkey=None) -> bool:
# Clear full cache if no subkey provided
if subkey is None:
if hasattr(hou.session, "ayon_cache"):
delattr(hou.session, "ayon_cache")
return True
return False

# Clear only key in cache if provided
cache = getattr(hou.session, "ayon_cache", {})
if subkey in cache:
cache.pop(subkey)
return True
return False


def expression_get_representation_id() -> str:
project_name = hou.evalParm("project_name")
folder_path = hou.evalParm("folder_path")
product_name = hou.evalParm("product_name")
version = hou.evalParm("version")
representation_name = hou.evalParm("representation_name")

node = hou.pwd()
hash_value = (project_name, folder_path, product_name, version,
representation_name)
cache = get_session_cache().setdefault("representation_ids", {})
if hash_value in cache:
return cache[hash_value]

try:
repre_id = get_node_expected_representation_id(node)
except ValueError:
# Ignore invalid parameters
repre_id = ""

cache[hash_value] = repre_id


def expression_get_representation_path() -> str:
cache = get_session_cache().setdefault("representation_path", {})
project_name = hou.evalParm("project_name")
repre_id = hou.evalParm("representation")
use_entity_uri = hou.evalParm("use_ayon_entity_uri")
hash_value = project_name, repre_id, use_entity_uri
if hash_value in cache:
return cache[hash_value]

path = get_representation_path(project_name, repre_id, use_entity_uri)
cache[hash_value] = path
return path

# endregion

Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"hdaroot/switch_load_warnings.def":1728298521,
"hdaroot/warn_no_representation_set.def":1708980551,
"hdaroot/reference.def":1698150558,
"hdaroot/output0.def":1698215383,
"hdaroot.def":1717451587
"hdaroot.def":1728285340,
"hdaroot/reference.def":1698150558
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
{
"values":["20.0.703"
"values":["20.5.370"
],
"indexes":{
"hdaroot/warn_no_representation_set.userdata":0,
"hdaroot/output0.userdata":0,
"hdaroot/switch_load_warnings.userdata":0,
"hdaroot/reference.userdata":0,
"hdaroot/output0.userdata":0
"hdaroot/warn_no_representation_set.userdata":0
}
}
Loading