Source code for houdini_core_tools.parameters

"""Functions and tools to deal with Houdini parameters."""

# Future
from __future__ import annotations

# Standard Library
import contextlib
import itertools
import operator
import re
from typing import TYPE_CHECKING

# Houdini Core Tools
from houdini_core_tools import exceptions

# Houdini
import hou

if TYPE_CHECKING:
    from collections.abc import Callable, Sequence


# Non-Public Functions


def _find_parameters_with_value(target_value: str, check_func: Callable) -> tuple[hou.Parm, ...]:
    """Find parameters which contain the target value.

    Args:
        target_value: The string value to search for.
        check_func: A function to use for testing value matching.

    Returns:
        A tuple of parameters which contain the value.
    """
    # Use 'opfind' hscript command to find all the nodes which have parameters
    # containing our value.
    paths = hou.hscript(f"opfind '{target_value}'")[0].split()

    parms_with_value = []

    for path in paths:
        node = hou.node(path)

        for parm in node.parms():
            value = None

            # Check string parameters via unexpandedString()
            try:
                value = parm.unexpandedString()

            # Fails on non-string parameters.
            except hou.OperationFailed:
                # In that case, check for any expressions.
                with contextlib.suppress(hou.OperationFailed):
                    value = parm.expression()

            # If we got a value and the checking function detects a match, then
            # we'll return that parameter
            if value and check_func(value, target_value):
                parms_with_value.append(parm)

    return tuple(parms_with_value)


def _get_names_in_folder(parent_template: hou.FolderParmTemplate) -> tuple[str, ...]:
    """Get a list of template names inside a template folder.

    This should only really ever be called originally with a multiparm template.

    Args:
        parent_template: The parm template to get items for.

    Returns:
        A list of template names inside the template.
    """
    names: list[str] = []

    for parm_template in parent_template.parmTemplates():
        if isinstance(parm_template, hou.FolderParmTemplate):
            # If the template is a folder (but not a multiparm folder), then we
            # need to get the parms inside it as well since they are technically siblings.
            if not is_parm_template_multiparm_folder(parm_template):
                names.extend(_get_names_in_folder(parm_template))

            else:
                names.append(parm_template.name())

        else:
            names.append(parm_template.name())

    return tuple(str(name) for name in names)


def _validate_multiparm_resolve_values(name: str, indices: Sequence[int]) -> None:
    """Validate a multiparm token string and the indices to be resolved.

    This function will raise a ValueError if there are not enough indices
    supplied for the number of tokens.

    Args:
        name: The parameter name to validate.
        indices: The indices to format into the token string.

    Raises:
        NotEnoughMultiParmIndicesError: When not enough indices are supplied.
    """
    # Get the number of multiparm tokens in the name.
    token_count = name.count("#")

    # Ensure that there are enough indices for the name.  Houdini can handle too many
    # indices, but if there is not enough, it won't like that and return an unexpected value.
    if token_count > len(indices):
        raise exceptions.NotEnoughMultiParmIndicesError(name, token_count, len(indices))


# Functions


[docs] def eval_multiparm_instance( node: hou.OpNode, name: str, indices: Sequence[int] | int, *, raw_indices: bool = False, ) -> tuple | float | str | hou.Ramp: """Evaluate a multiparm parameter by indices. The name should include the # value(s) which will be replaced by the indices. The index should be the multiparm index, not including any start offset. You cannot try to evaluate a single component of a tuple parameter, evaluate the entire tuple instead and get which values you need. # Float >>> eval_multiparm_instance(node, "float#", 1) 0.53 # Float 3 >>> eval_multiparm_instance(node, "vec#", 1) (0.53, 1.0, 2.5) Args: node: The node to evaluate the parameter on. name: The base parameter name. indices: The multiparm indices. raw_indices: Whether the indices are 'raw' and should not try and take the folder offset into account. Returns: The evaluated parameter value. Raises: MissingMultiParmTokenError: If the parameter name does not contain at least one '#'. NoMatchingParameterTemplate: If the parameter name does not exist. InvalidMultiParmIndicesError: If the multiparm indices are not valid. """ if "#" not in name: raise exceptions.MissingMultiParmTokenError(name) ptg = node.parmTemplateGroup() parm_template = ptg.find(name) if parm_template is None: raise exceptions.NoMatchingParameterTemplate(name, node) # Handle directly passing a single index. if isinstance(indices, int): indices = [indices] if not raw_indices: offsets = get_multiparm_container_offsets(name, ptg) # Adjust any supplied offsets with the multiparm offset. indices = [idx + offset for idx, offset in zip(indices, offsets, strict=True)] # Validate that enough indices were passed. _validate_multiparm_resolve_values(name, indices) # Resolve the name and indices to get the parameter name. full_name = resolve_multiparm_tokens(name, indices) parm_tuple = node.parmTuple(full_name) if parm_tuple is None: raise exceptions.InvalidMultiParmIndicesError(full_name) values = parm_tuple.eval() # Return single value for non-tuple parms. if len(values) == 1: return values[0] return tuple(values)
[docs] def eval_parm_as_strip(parm: hou.Parm) -> tuple[bool, ...]: """Evaluate the parameter as a Button/Icon Strip. Returns a tuple of True/False values indicated which buttons are pressed. Args: parm: The parm to eval. Returns: True/False values for the strip. Raises: ParameterNotAButtonStripError: If the parameter is not a button strip. """ parm_template = parm.parmTemplate() if not isinstance(parm_template, hou.MenuParmTemplate) or not parm_template.isButtonStrip(): raise exceptions.ParameterNotAButtonStripError(parm) # Get the value. This might be the selected index or a bit mask if we # can select more than one. value = parm.eval() # Initialize a list of False values for each item on the strip. num_items = len(parm_template.menuItems()) values = [False] * num_items # If our menu type is a Toggle, that means we can select more than one # item at the same time, so our value is really a bit mask. if parm_template.menuType() == hou.menuType.StringToggle: # Check which items are selected. for i in range(num_items): mask = 1 << i if value & mask: values[i] = True # Value is just the selected index, so set that one to True. else: values[value] = True return tuple(values)
[docs] def eval_parm_strip_as_string(parm: hou.Parm) -> tuple[str, ...]: """Evaluate the parameter as a Button Strip as strings. Returns a tuple of the string tokens which are enabled. Args: parm: The parm to eval. Returns: String token values. """ strip_results = eval_parm_as_strip(parm) menu_items = parm.parmTemplate().menuItems() enabled_values = [] for i, value in enumerate(strip_results): if value: enabled_values.append(menu_items[i]) return tuple(str(val) for val in enabled_values)
[docs] def eval_parm_tuple_as_color(parm_tuple: hou.ParmTuple) -> hou.Color: """Evaluate a color parameter and return a hou.Color object. Args: parm_tuple: The parm tuple to eval. Returns: The evaluated parameter as a hou.Vector* Raises: ParmTupleTypeError: If the parm tuple is not a color. """ if not is_parm_tuple_color(parm_tuple): raise exceptions.ParmTupleTypeError(parm_tuple, "color chooser") return hou.Color(parm_tuple.eval())
[docs] def eval_parm_tuple_as_vector( parm_tuple: hou.ParmTuple, ) -> hou.Vector2 | hou.Vector3 | hou.Vector4: """Return the parameter value as a hou.Vector of the appropriate size. Args: parm_tuple: The parm tuple to eval. Returns: The evaluated parameter as a hou.Vector* Raises: ParmTupleTypeError: If the parm tuple is not a vector type (XYZW). """ if not is_parm_tuple_vector(parm_tuple): raise exceptions.ParmTupleTypeError(parm_tuple, "vector") value = parm_tuple.eval() size = len(value) match size: case 2: result = hou.Vector2(value) case 3: result = hou.Vector3(value) case 4: # pragma: no branch result = hou.Vector4(value) return result
[docs] def find_matching_parent_parm(parm: hou.Parm, *, stop_at_locked_hda: bool = True) -> hou.Parm | None: """Look for a parameter of the same name on any parent node. Args: parm: The reference parameter whose name to search for. stop_at_locked_hda: Whether to stop if a parent is a locked HDA. Returns: The matching parent parameter, if any. """ node = parm.node() parent = node.parent() parm_name = parm.name() while parent is not None: parent_parm = parent.parm(parm_name) if parent_parm is not None: return parent_parm if parent.isLockedHDA() and stop_at_locked_hda: return None parent = parent.parent() return None
[docs] def find_parameters_using_variable(variable: str) -> tuple[hou.Parm, ...]: """Find parameters which contain a variable. This only works for string parameters The variable name can be supplied with or without a $. Variable usage that includes {} to help with disambiguation will also be automatically found. This will match only the exact usage. For example, if you search for $HIP, the result would not include any parameters using $HIPNAME or $HIPFILE. Args: variable: The variable name to search for. Returns: A tuple of parameters which contain the variable. """ search_variable = variable.replace("{", "").replace("}", "") # If the variable doesn't start with $, we need to add it. if not variable.startswith("$"): search_variable = "$" + search_variable disambiguated_variable = f"${{{search_variable[1:]}}}" def _checker(value, target_variable): # noqa: ANN001,ANN202 # We need to escape the $ since it's a special regex character. var = "\\" + target_variable # Regex to match the variable string but ensuring that it matches exactly. # For example, if you are looking for '$HIP', you want to ensure you don't also # match $HIPNAME or $HIPFILE return bool(re.search(f"(?=.*{var}(?![a-zA-Z]))", value)) results: list[hou.Parm] = [] for variable_to_test in (search_variable, disambiguated_variable): results.extend(_find_parameters_with_value(variable_to_test, _checker)) return tuple(results)
[docs] def find_parameters_with_value(target_value: str) -> tuple[hou.Parm, ...]: """Find parameters which contain the target value. This only works for string parameters. Args: target_value: The value to search for. Returns: A tuple of parameters which contain the value. """ return _find_parameters_with_value(target_value, operator.contains)
[docs] def get_multiparm_containing_folders( name: str, parm_template_group: hou.ParmTemplateGroup ) -> tuple[hou.FolderParmTemplate, ...]: """Given a parameter template name, return a list of containing multiparms. If the name is contained in one or more multiparm folders, the returned templates will be ordered from innermost to outermost outer inner# param#_# In a situation like above, querying for containing folders of param#_# would result in a tuple ordered as follows: (<hou.FolderParmTemplate inner#>, <hou.FolderParmTemplate outer>) Args: name: The name of the parameter to get the containing names for. parm_template_group: A parameter template group for a nde. Returns: A tuple of containing multiparm templates, if any. """ # A list of containing folders. containing_folders = [] # Get the folder the parameter is in. containing_folder = parm_template_group.containingFolder(name) # Keep looking for containing folders until there are no. while True: # Add a containing multiparm folder to the list. if is_parm_template_multiparm_folder(containing_folder): containing_folders.append(containing_folder) # Try to find the parent-containing folder. try: containing_folder = parm_template_group.containingFolder(containing_folder) # Not inside a folder so bail out. except hou.OperationFailed: break return tuple(containing_folders)
[docs] def get_multiparm_container_offsets(name: str, parm_template_group: hou.ParmTemplateGroup) -> tuple[int, ...]: """Given a parameter template name, return a list of containing multiparm folder offsets. If the name is contained in one or more multiparm folders, the returned offsets will be ordered outermost to innermost outer (starting offset 0) inner# (starting offset 1) param#_# In a situation like above, querying for containing offsets of param#_# would result in a tuple ordered as follows: (0, 1) Args: name: The name of the parameter to get the containing offsets for. parm_template_group: A parameter template group for a nde. Returns: A tuple of containing multiparm offsets, if any. """ # A list of containing folders. containing_folders = get_multiparm_containing_folders(name, parm_template_group) # The containing folder list is ordered by the folder closest to the base parameter. # We want to process that list in reverse, so the first offset item will be for the # outermost parameter and match the ordered provided by the user. return tuple(get_multiparm_start_offset(folder) for folder in reversed(containing_folders))
[docs] def get_multiparm_siblings(parm: hou.Parm | hou.ParmTuple) -> dict: """Get a tuple of any sibling parameters in the multiparm block. Args: parm: The parameter to get any siblings for. Returns: Any sibling parameters. Raises: ParameterIsNotAMultiParmInstanceError: If the parm is not a multiparm instance. """ if not parm.isMultiParmInstance(): raise exceptions.ParameterIsNotAMultiParmInstanceError(parm.name()) if isinstance(parm, hou.Parm): parm = parm.tuple() # Get the template name for the parameter. template_name = get_multiparm_template_name(parm) node = parm.node() ptg = node.parmTemplateGroup() # Find the most immediate containing multiparm folder. containing_template = get_multiparm_containing_folders(template_name, ptg)[0] # type: ignore # Get a list of template names in that folder. names = _get_names_in_folder(containing_template) # The instance indices of the parameter. # indices = get_multiparm_instance_indices(parm, instance_index=True) indices = parm.multiParmInstanceIndices() parms = {} for name in names: # Skip the parameter that was passed in. if name == template_name: continue # Resolve the tokens and get the parm tuple. parm_name = resolve_multiparm_tokens(name, indices) parm_tuple = node.parmTuple(parm_name) # If the parm tuple has a size of 1, then just get the parm. if len(parm_tuple) == 1: parm_tuple = parm_tuple[0] parms[name] = parm_tuple return parms
[docs] def get_multiparm_start_offset(parm_template: hou.ParmTemplate) -> int: """Get the start offset of items in the multiparm. Args: parm_template: A multiparm folder parm template Returns: The start offset of the multiparm. Raises: ParameterTemplateIsNotAMultiParmError: If the template is not a multiparm. """ if not is_parm_template_multiparm_folder(parm_template): raise exceptions.ParameterTemplateIsNotAMultiParmError return int(parm_template.tags().get("multistartoffset", 1))
[docs] def get_multiparm_template_name(parm: hou.Parm | hou.ParmTuple) -> str | None: # noqa: RET503 """Return a multiparm instance's parameter template name. Args: parm: The parm to get the multiparm instances values for. Returns: The parameter template name, or None """ # Return None if the parameter isn't a multiparm instance. if not parm.isMultiParmInstance(): return None if isinstance(parm, hou.Parm): parm = parm.tuple() parm_template = parm.parmTemplate() indices = parm.multiParmInstanceIndices() parent_multiparm = parm.parentMultiParm() parent_template = parent_multiparm.parmTemplate() for template in parent_template.parmTemplates(): # pragma: no branch resolved_name = resolve_multiparm_tokens(template.name(), indices) if resolved_name == parm_template.name(): return template.name()
[docs] def is_parm_multiparm(parm: hou.Parm | hou.ParmTuple) -> bool: """Check if this parameter is a multiparm. Args: parm: The parm or tuple to check for being a multiparm. Returns: Whether the parameter is a multiparm. """ # Get the parameter template for the parm/tuple. parm_template = parm.parmTemplate() return is_parm_template_multiparm_folder(parm_template)
[docs] def is_parm_template_multiparm_folder(parm_template: hou.ParmTemplate) -> bool: """Returns True if the parm template represents a multiparm folder type. Args: parm_template: The parameter template to check. Returns: Whether the template represents a multiparm folder. """ if not isinstance(parm_template, hou.FolderParmTemplate): return False return parm_template.folderType() in { hou.folderType.MultiparmBlock, hou.folderType.ScrollingMultiparmBlock, hou.folderType.TabbedMultiparmBlock, }
[docs] def is_parm_tuple_color(parm_tuple: hou.ParmTuple) -> bool: """Check if the parameter is a color parameter. Args: parm_tuple: The parm tuple to check. Returns: Whether the parameter tuple is a color. """ parm_template = parm_tuple.parmTemplate() return parm_template.look() == hou.parmLook.ColorSquare
[docs] def is_parm_tuple_vector(parm_tuple: hou.ParmTuple) -> bool: """Check if the tuple is a vector parameter. Args: parm_tuple: The parm tuple to check. Returns: Whether the parameter tuple is a vector. """ parm_template = parm_tuple.parmTemplate() return parm_template.namingScheme() == hou.parmNamingScheme.XYZW
[docs] def resolve_multiparm_tokens(name: str, indices: int | Sequence[int]) -> str: """Resolve a multiparm token string with the supplied indices. Args: name: The parameter name. indices: One or mode multiparm indices. Returns: The resolved string. """ # Support passing in just a single value. if isinstance(indices, int): indices = [indices] # Validate that there are at least enough indices for the number of tokens. _validate_multiparm_resolve_values(name, indices) num_tokens = name.count("#") # Clamp the number of indices to the number of tokens. indices = indices[:num_tokens] name_components = name.split("#") all_components = [] for i, j in itertools.zip_longest(name_components, indices, fillvalue=""): all_components.extend([i, str(j)]) return "".join(all_components)
[docs] def unexpanded_string_multiparm_instance( node: hou.OpNode, name: str, indices: Sequence[int] | int, *, raw_indices: bool = False ) -> tuple[str, ...] | str: """Get the unexpanded string of a multiparm parameter by index. The name should include the # value which will be replaced by the index. The index should be the multiparm index, not including any start offset. You cannot try to evaluate a single component of a tuple parameter, evaluate the entire tuple instead and get which values you need. # String >>> eval_multiparm_instance(node, "string#", 1) '$HIP' # String 2 >>> eval_multiparm_instance(node, "stringvec#", 1) ('$HIP', '$PI') Args: node: The node to evaluate the parameter on. name: The base parameter name. indices: The multiparm indices. raw_indices: Whether the indices are 'raw' and should not try and take the folder offset into account. Returns: The evaluated parameter value. Raises: MissingMultiParmTokenError: If the parameter name does not contain any '#'. NoMatchingParameterTemplate: If the parameter name does not exist. ParameterIsNotAStringError: If the parameter is not a string. InvalidMultiParmIndicesError: If the multiparm index is not valid. """ if "#" not in name: raise exceptions.MissingMultiParmTokenError(name) ptg = node.parmTemplateGroup() parm_template = ptg.find(name) if parm_template is None: raise exceptions.NoMatchingParameterTemplate(name, node) if parm_template.dataType() != hou.parmData.String: raise exceptions.ParameterIsNotAStringError(parm_template) # Handle directly passing a single index. if isinstance(indices, int): indices = [indices] if not raw_indices: offsets = get_multiparm_container_offsets(name, ptg) # Adjust any supplied offsets with the multiparm offset. things = [idx + offset for idx, offset in zip(indices, offsets, strict=True)] else: things = indices # Validate that enough indices were passed. _validate_multiparm_resolve_values(name, things) full_name = resolve_multiparm_tokens(name, things) parm_tuple = node.parmTuple(full_name) if parm_tuple is None: raise exceptions.InvalidMultiParmIndicesError(full_name) values = tuple(str(parm.unexpandedString()) for parm in parm_tuple) # Return single value for non-tuple parms. if len(values) == 1: return values[0] return values