Skip to content

Commit

Permalink
Merge pull request #112 from BigRoy/enhancement/load_lops_use_express…
Browse files Browse the repository at this point in the history
…ions

Load LOPs: Support update via context options (replace parm callbacks with expressions)
  • Loading branch information
BigRoy authored Oct 31, 2024
2 parents 769839f + 8fab9fe commit 5d7e452
Show file tree
Hide file tree
Showing 18 changed files with 342 additions and 299 deletions.
258 changes: 136 additions & 122 deletions client/ayon_houdini/api/hda_utils.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
"""Helper functions for load HDA"""
"""Heper functions for load HDA"""

import os
import contextlib
import uuid
from typing import List

import hou
from qtpy import QtCore, QtWidgets, QtGui

import ayon_api
from ayon_api import (
get_project,
Expand All @@ -15,22 +17,26 @@
get_version_by_name,
get_representation_by_name
)
from ayon_core.pipeline.load import (
get_representation_context,
get_representation_path_from_context
)
from ayon_core.pipeline.context_tools import (
get_current_project_name,
get_current_folder_path
)
from ayon_core.tools.utils import SimpleFoldersWidget
from ayon_core.pipeline.load import (
get_representation_context,
get_representation_path_from_context
)
from ayon_core.style import load_stylesheet

from ayon_core.tools.utils import SimpleFoldersWidget
from ayon_houdini.api import lib
from .usd import get_ayon_entity_uri_from_representation_context

from qtpy import QtCore, QtWidgets, QtGui
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:
Expand All @@ -42,16 +48,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 +95,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,14 +124,35 @@ 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)


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 ""

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

context = get_representation_context(project_name, repre_entity)
if use_ayon_entity_uri:
path = get_ayon_entity_uri_from_representation_context(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
path = path.replace("\\", "/")
return path


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 All @@ -155,47 +172,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")
def update_thumbnail(node):
if not node.evalParm("show_thumbnail"):
lib.remove_all_thumbnails(node)
return

representation_id = node.evalParm("representation")
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()
)

# Ignore invalid representation ids silently
# TODO remove - added for backwards compatibility with OpenPype scenes
if not is_valid_uuid(representation_id):
return

repre_entity = get_representation_by_id(project_name, representation_id)
if not repre_entity:
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:
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
path = path.replace("\\", "/")
with _unlocked_parm(file_parm):
file_parm.set(path)

if node.evalParm("show_thumbnail"):
# Update thumbnail
# TODO: Cache thumbnail path as well
Expand Down Expand Up @@ -238,11 +229,7 @@ def compute_thumbnail_rect(node):

def on_thumbnail_show_changed(node):
"""Callback on thumbnail show parm changed"""
if node.evalParm("show_thumbnail"):
# For now, update all
on_representation_id_changed(node)
else:
lib.remove_all_thumbnails(node)
update_thumbnail(node)


def on_thumbnail_size_changed(node):
Expand All @@ -254,45 +241,16 @@ def on_thumbnail_size_changed(node):
lib.set_node_thumbnail(node, thumbnail)


def on_representation_id_changed(node):
"""Callback on representation id changed
Args:
node (hou.Node): Node to update.
"""
repre_id = node.evalParm("representation")
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 +259,6 @@ def get_representation_id(
product_name,
version,
representation_name,
load_message_parm,
):
"""Get representation id.
Expand All @@ -311,14 +268,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 +287,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 +303,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 +666,65 @@ 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
return repre_id


def expression_get_representation_path() -> str:
cache = get_session_cache().setdefault("representation_path", {})
project_name: str = hou.evalParm("project_name")
repre_id: str = hou.evalParm("representation")
use_entity_uri = bool(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

Loading

0 comments on commit 5d7e452

Please sign in to comment.