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 are 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: list[int] | tuple[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 not isinstance(indices, (list, tuple)): 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)] # 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) if size == 2: # noqa: PLR2004 return hou.Vector2(value) if size == 3: # noqa: PLR2004 return hou.Vector3(value) return hou.Vector4(value)
[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): # type: ignore # 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 of 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 contain folders. containing_folders = get_multiparm_containing_folders(name, parm_template_group) # The containing folder list is ordered by 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: # type: ignore # 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 | list[int] | tuple[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 not isinstance(indices, (list, tuple)): indices = [indices] # Validate that there are at least enough indices for the number of tokens. _validate_multiparm_resolve_values(name, indices) # Clamp the number of indices to the number of tokens. indices = indices[: name.count("#")] 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: list[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 not isinstance(indices, (list, tuple)): 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)] # Validate that enough indices were passed. _validate_multiparm_resolve_values(name, indices) full_name = resolve_multiparm_tokens(name, indices) 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