MISC_digital_assetAsset Utilities otl module

Perform tasks related to the management of Houdini Digital Assets. Functions include geting a list of all instanced assets, hardening of assets in the hip file, creating debug versions of assets, reloading all operators, rescanning HOUDINI_OTLSCAN_PATH, and generating a skeleton help card.

Date 02/21/2011
Compatibility Houdini 9.5
Change Log Fixed several problems with __processOTLFileContextHandler() where it was incorrectly comparing node types against generic Python objects, not hou.NodeTypeCategory objects, incorrectly testing for a pop node instead of a shop node, and added a dop entry.
# Produced by:
#       Graham Thompson
#       captainhammy@gmail.com
#       www.captainhammy.com
# Name:         assetutils.py
# Comments:     Perform tasks related to the management of Houdini 
#               Digital Assets.
# Version:      1.1
# Compatibility: Houdini 9.5
# Changes:
#       1.1:
#               Fixed several problems with __processOTLFileContextHandler() 
#               where it was incorrectly comparing node types against generic 
#               Python objects, not hou.NodeTypeCategory objects, incorrectly 
#               testing for a pop node instead of a shop node, and added a dop 
#               entry.
import hou
import glob
import os
import tempfile
# The Houdini install location.
HOUDINI_INSTALL_DIR = hou.expandString("$HFS")
def isDigitalAsset(node):
    """ Test whether a node is a digital asset or not. """
    return node.type().definition() is not None
def filterHoudiniInstallFiles(files):
    """ Filter a list of files and remove any files that are in 
        the Houdini install directory ($HFS).
    return tuple([file_path for file_path in files
                  if HOUDINI_INSTALL_DIR not in file_path])
def findInstancedAssetDefinitions(include_hfs_defs=False):
    """ Search through the list of installed digital assets to
	find any that are instanced and return a tuple of definitions
        for those instanced assets.
        include_hfs_defs: Include default Houdini operators in the
    # Get a list of installed otl files.
    installed_files = hou.hda.loadedFiles()
    # If we are filtering Houdini's default operations then call
    # filterHoudiniFiles to remove any files in $HFS.
    if include_hfs_defs:
        files_to_check = installed_files
    # If not then the filtered files are the installed files.
        files_to_check = filterHoudiniInstallFiles(installed_files)
    # List of HDADefinitions that have instances.
    instanced_assets = [asset_def for file_path in files_to_check
                        for asset_def in hou.hda.definitionsInFile(file_path)
                         if len(asset_def.nodeType().instances()) != 0]
    # Return the list of instanced assets as a tuple.
    return tuple(instanced_assets)
def hardenAssets(harden_houdini_operators=False, harden_path="Embedded"):
    """ Embed all definitions of external instanced assets in the hip 
	file and set them as preferred.
        harden_houdini_operators: Harden Houdini's default operators.
        When set to false, only operator types defined in otl files not
        located in $HFS are hardened.
    # Find all instanced digital assets.
    asset_definitions = findInstancedAssetDefinitions(harden_houdini_operators)
    # Iterate over all the asset definitions.
    for definition in asset_definitions:
        # If the asset definition is not already stored in Embedded,
        # copy the definition to the Embedded library.
	if definition.libraryFilePath() != harden_path:
    # Try to get all the definitions to be preferred.
        for definition in hou.hda.definitionsInFile(harden_path):
    # Catch an exception if there are no definitions in the harden_path.
    except hou.OperationFailed:
def debugAsset(asset_node):
    """ Create a temporary debug version of an asset for use when
        A copy of the asset definition is copied to a temporary file
        and the source node is changed to this new debug type.
    asset_definition = asset_node.type().definition()
    # Attempting to debug a non-asset.
    if asset_definition is None:
        raise hou.OperationFailed("Operator is not an asset.")
    type_name = asset_definition.nodeTypeName()
    type_label = asset_definition.nodeType().description()
    new_type_name = "%s_debug" % type_name
    # Get the temp directory.  If $TEMP is not defined, we use a path
    # from Python's tempfile module.
    temp_dir = os.getenv("TEMP")
    if temp_dir is None:
        temp_dir = tempfile.gettempdir()
    file_path = os.path.join(temp_dir, "%s.otl" % new_type_name)
    # If the file already exists we prompt to overwrite it.
    if os.path.exists(file_path):
        msg = "Debug file already detected in %s."
        msg += "Would you like to overwrite it?"
        choice = hou.ui.displayMessage( msg % temp_dir, buttons = ("Yes", "No"))
        # If we are going to overwrite it we will remove the existing
        # file just to be safe.
        if choice == 0:
        # If we selected no, we just return.
    # Create a new debug node type definition.
				   "%s Debug" % type_label)
    # Install the new definition.
    # Change the selected node to this new type.  We keep the network
    # contents if the node is unlocked.
                              not asset_node.matchesCurrentDefinition())
    return asset_node
def __processOTLFileContextHandler(asset_definition):
    """ Find the base operator location to create an instance of an
        VEX/VOPs are not supported.
    # Get the context the definition belongs to.
    type_category = asset_definition.nodeTypeCategory()
    if type_category == hou.objNodeTypeCategory():
        return hou.node("/obj")
    elif type_category == hou.sopNodeTypeCategory():
        return hou.node("/obj").createNode("geo")
    elif type_category == hou.chopNodeTypeCategory():
        return hou.node("/ch").createNode("ch")
    elif type_category == hou.cop2NodeTypeCategory():
        return hou.node("/img").createNode("img")
    elif type_category == hou.ropNodeTypeCategory():
        return hou.node("/out")
    elif type_category == hou.popNodeTypeCategory():
        return hou.node("/part").createNode("popnet")
    elif type_category == hou.dopNodeTypeCategory():
        return hou.node("/obj").createNode("dopnet")
    elif type_category == hou.shopNodeTypeCategory():
        return hou.node("/shop")
def processOTLFileDefinitions(file_path, function, args=()):
    """ For each asset definition in a file, perform a given function
        on it, with the given args and update the definition afterwards.
        VEX/VOP operators are currently not supported.
    for definition in hou.hda.definitionsInFile(file_path):
        # Get the context to create the asset it.
        context_node = __processOTLFileContextHandler(definition)
        # Create an instance of the definition and unlock it.
        asset = context_node.createNode(definition.nodeTypeName())
        # Call the function, passing in a reference to the instance and
        # the definition.
        function.__call__(*args + (asset, definition))
        # Update the definition and destroy the node.
        # Try to destroy the context node.  This will throw a 
        # hou.OperationFailed on manager nodes so try/except it.
        except hou.OperationFailed:
def setExternalPreferred():
    """ Set all external asset definitions to be preferred
        over those assets located in $HH/otls.
    # Get all the loaded otls.
    loaded_files = hou.hda.loadedFiles()
    for file_path in loaded_files:
        # Find the directory the asset is located in.
        directory = os.path.dirname(file_path)
        # If the directory is not in the Houdini install directory.
        if HOUDINI_INSTALL_DIR not in directory:
            # For each defintion in this file, make sure it is preferred.
            for definition in hou.hda.definitionsInFile(file_path):
def reloadAllOperators(reload_hfs_operators=False):
    """ Reload all assets installed in the current session. 
        reload_hfs_operators: Specify whether or not to reload operator
                             defintions from the Houdini install directory.
    loaded_files = hou.hda.loadedFiles()
    # Reload each file in the loaded files list
    for file_path in loaded_files:
        # If the asset is not in the Houdini install directory or we are
        # specifically reloading the default Houdini operators then reload
        # the file.
        if HOUDINI_INSTALL_DIR not in file_path or reload_hfs_operators:
def rescanOTLPath():
    """ Rescan the HOUDINI_OTLSCAN_PATH and install any asset definitions
        that are not already installed, returning a list of these file paths
        and a list of any files that could not be installed.
    # Get a list of directories in the HOUDINI_OTLSCAN_PATH.
    scanned_paths = os.environ.get("HOUDINI_OTLSCAN_PATH")
    installed_assets = []
    errors = []
    # If the path is set.
    if scanned_paths is not None:
        # Get the loaded files.
        loaded_files = hou.hda.loadedFiles()
        # Split the list based on the separating colon. Skip the first
        # entry since we won't want to check the current directory.
        scanned_paths = scanned_paths.split(':')[1:]
        for directory_path in scanned_paths:
            # Get all the .otl files in the directory.
            otl_files = glob.glob(os.path.join(directory_path, "*.otl"))
            for otl_path in otl_files:
                full_path = os.path.join(directory_path, otl_path)
                # If the file isn't already loaded, install it.
                if full_path not in loaded_files:
                    except hou.OperationFailed:
    return tuple(installed_assets), tuple(errors)
def getEmbeddedAssets():
    """ Return a tuple of all asset definitions that are Embedded. """
        return hou.hda.definitionsInFile("Embedded")
    except OperationFailed:
        return ()
def destroyAssetInstances(asset_definition):
    """ Destroy all instances of a hou.HDADefinition.
        The number of instances that were destroyed is returned.
    asset_instances = asset_definition.nodeType().instances()
    if len(asset_instances) != 0:
        for instance in asset_instances:
    return len(asset_instances)
def generateHelpCard(node, using=True, related=False, inputs=False):
    """ Generate a help card skeleton for a node and returns it as a
        using: Create a "Using xxx" block.
        related: Create a list for related nodes.
        inputs: Create an "Inputs" block to describe each input.
    # Parameter types to ignore.
    to_ignore = (hou.parmTemplateType.Separator,
    nodetype = node.type()
    description = nodetype.description()
    type_name = nodetype.name()
    # Start the list.
    result = ["#type: node"]
    context = "sop"
    # Create the header information
    result.append("#context: %s" % context)
    icon = nodetype.icon().replace("_", "/", 1)
    result.append("#icon: %s" % icon)
    result.append("#internal: %s" % type_name)
    # Create the title block.
    result.append("= %s =" % description)
    result.append("Enter Description Here")
    # Create the optional Using block.
    if using:
        result.append("== Using %s == (includeme)" % description)
        result.append("    # Click the [Icon:%s]%s tool on the __Some Tab Name__ tab." 
                      % (icon, description))
    # Parameters block.
    count = 0
    previous_folders = ()
    for parm_tuple in node.parmTuples():
        template = parm_tuple.parmTemplate()
        # If this parm is a type to ignore, skip to the next one.
        if template.type() in to_ignore:
        # Get the containing folders.
        folders = parm_tuple[0].containingFolders()
        folder_len = len(folders)
        prev_len = len(previous_folders)
        ignore = False 
        # If there are containing folders we need to generate folder entries.
        if folder_len > 0:
            # The folder is different than the folder of the previous parm.
            if folders != previous_folders:
                # If the number of folders is equal we need to find where the changes
                # are occuring.
                if folder_len == prev_len:
                    # Change index.
                    idx = 0
                    # Iterate through all the
                    for i in range(folder_len):
                        # If the current folder differs at this index, set the index
                        # and break.
                        if folders[i] != previous_folders[i]:
                            idx = i
                    # Start from where they began to differ and iterate forward to add
                    # in missing entries.
                    for i in range(idx, folder_len - 1):
                        result.append("%s== %s == " % ("    " * i, folders[i]))
                        result.append("%sEnter a Folder Description" % ("    " * (i+1)))
                # If the current folder is less deep than the previous one we need to check
                # to see if they still share hierarchy so as to not add extra folder entries.
                elif folder_len < prev_len:
                    diff = prev_len - folder_len
                    # If the current folder is equal to the folder in the previous list minus
                    # the difference in length, then we need to ignore adding the new entry.
                    if folders[-1] == previous_folders[-(diff + 1)]:
                        ignore = True
                if not ignore:
                    # Add an entry.
                    result.append("%s== %s ==" % ("    " * (folder_len - 1), folders[-1]))
                    result.append("%sEnter a folder Description" % ("    " * (folder_len)))
                    previous_folders = folders
        parm_template = parm_tuple.parmTemplate()
        if parm_template.isHidden():
        # Print the parm tuple name as a title for the parameter name.
               % ('    ' * folder_len, parm_tuple.description().replace("_", " ").title()))
        # Try and get the help text.
        help_txt = parm_tuple.parmTemplate().help()
        # If there is help text, use it as the description.
        if len(help_txt) > 0:
            result.append("%s%s" % ('    ' * (folder_len + 1), help_txt))
        # If not, insert the place holder.
            result.append("%sEnter a parameter description" % ('    ' * (folder_len + 1)))
        # If this parm is a menu parm we need to add entries for each label.
        if template.type() == hou.parmTemplateType.Menu:
            labels = template.menuLabels()
            for label in labels:
                result.append("%s%s:" % ('    ' * (folder_len + 1), label))
                result.append("%sEnter a parameter description" % ('    ' * (folder_len + 2)))
    # Create option "Inputs" block.
    if inputs:
        for n in range(nodetype.maxNumInputs()):
            result.append("    Input %s:" % n)
            result.append("        ")
    # Create optional "Related" nodes links.
    if related:
        result.append("- [Node:obj/null]")
    return '\n'.join(result)

Valid XHTML 1.0 Strict