Difference between revisions of "Houdini UI Customization"
m (→Shelf Tools) |
|||
| Line 2: | Line 2: | ||
Quick reminder to myself. When testing out things, call '''menurefresh''' in Hscript textport to refresh different menus (notably PARMmenu.xml) | Quick reminder to myself. When testing out things, call '''menurefresh''' in Hscript textport to refresh different menus (notably PARMmenu.xml) | ||
== Shelf Tools == | ==== Shelf Tools ==== | ||
I find it odd that Sidefx makes you go through shelf items for various UI functions that have nothing to do with the shelf, but that's the way the cookie crumbles. Also maybe I'm nostalgic but despite it's flaws the maya shelves were kinda easier to understand. | I find it odd that Sidefx makes you go through shelf items for various UI functions that have nothing to do with the shelf, but that's the way the cookie crumbles. Also maybe I'm nostalgic but despite it's flaws the maya shelves were kinda easier to understand. | ||
As I understand it single shelf items in houdini can be stored in different files, which means if you fumble around you can easily have two items with the same name but saved in two different places (I think. I'm confused). | As I understand it single shelf items in houdini can be stored in different files, which means if you fumble around you can easily have two items with the same name but saved in two different places (I think. I'm confused). | ||
Just make sure you keep an eyeball on where your shelf tool is stored (often in Default.shelf or some random production folder if you use Rez like us) | Just make sure you keep an eyeball on where your shelf tool is stored (often in Default.shelf or some random production folder if you use Rez like us) | ||
== Parameters as network view comments == | |||
https://i.imgur.com/ZddFaRC.gif | https://i.imgur.com/ZddFaRC.gif | ||
| Line 288: | Line 288: | ||
== Selection to bounding box to delete == | |||
Very often I manually select prims to delete a few local faces, then change something upstream, then my blast is all mangled. This will create a bounding box of the currently selected geo, group them with it and then blast the group. It's never perfect because of how bounding boxes operate (points/prims/othogonal to world axes), but it's very useful in my daily work. | Very often I manually select prims to delete a few local faces, then change something upstream, then my blast is all mangled. This will create a bounding box of the currently selected geo, group them with it and then blast the group. It's never perfect because of how bounding boxes operate (points/prims/othogonal to world axes), but it's very useful in my daily work. | ||
| Line 352: | Line 352: | ||
</pre> | </pre> | ||
== Toggle Viewport Background Color == | |||
Bound it to F6 | Bound it to F6 | ||
<pre> | <pre> | ||
| Line 378: | Line 378: | ||
hou.ui.setStatusMessage("Cycled background to %s" % bgs[bg].upper() ) | hou.ui.setStatusMessage("Cycled background to %s" % bgs[bg].upper() ) | ||
</pre> | </pre> | ||
== Toggle Auto / Manual scene update == | |||
Bound to F11, I use it every day. | Bound to F11, I use it every day. | ||
<pre> | <pre> | ||
| Line 388: | Line 388: | ||
hou.setUpdateMode(hou.updateMode.AutoUpdate) | hou.setUpdateMode(hou.updateMode.AutoUpdate) | ||
</pre> | </pre> | ||
== Auto create a wrangle node == | |||
I'm pretty sure this exists already but why not reinvent the wheel. Creates an attr wrangle and keeps inputs/outputs, with the code box already in focus. I only use the 'P' parameter viewer (the one that pops up in your network view) so other desktops might not work. | I'm pretty sure this exists already but why not reinvent the wheel. Creates an attr wrangle and keeps inputs/outputs, with the code box already in focus. I only use the 'P' parameter viewer (the one that pops up in your network view) so other desktops might not work. | ||
| Line 421: | Line 421: | ||
</pre> | </pre> | ||
== Copy auto Paste-Merge == | |||
Copy from https://berniebernie.fr/wiki/Houdini_Python#Paste_clipboard_nodes_to_object_merges | Copy from https://berniebernie.fr/wiki/Houdini_Python#Paste_clipboard_nodes_to_object_merges | ||
| Line 457: | Line 457: | ||
</pre> | </pre> | ||
==Screenshot to background image== | |||
https://i.imgur.com/kuc1PnO.gif | https://i.imgur.com/kuc1PnO.gif | ||
Latest revision as of 15:26, 12 November 2025
Various things I do to my Houdini
Quick reminder to myself. When testing out things, call menurefresh in Hscript textport to refresh different menus (notably PARMmenu.xml)
Shelf Tools
I find it odd that Sidefx makes you go through shelf items for various UI functions that have nothing to do with the shelf, but that's the way the cookie crumbles. Also maybe I'm nostalgic but despite it's flaws the maya shelves were kinda easier to understand. As I understand it single shelf items in houdini can be stored in different files, which means if you fumble around you can easily have two items with the same name but saved in two different places (I think. I'm confused). Just make sure you keep an eyeball on where your shelf tool is stored (often in Default.shelf or some random production folder if you use Rez like us)
Parameters as network view comments
This tool will show specific node parameter values as node comments. Good for quick overview without having to open the parameter spreadsheet. I've bound it to F10 and use it often.
Each node type can have its own specific properties. I've added a right click menu item so that I can automatically add the parameters to a config file in Houdini.
This is 95% chatGPT code and it's absolutely great at it.
In my PARMmenu.xml
<scriptItem id="tool_attribute_as_comment">
<modifyItem><insertAfter>motion_effects_menu</insertAfter></modifyItem>
<label>Add Parameter as Attribute Comment</label>
<scriptCode><![CDATA[import tool_attribute_as_comment;tool_attribute_as_comment.edit_attribute_config_rclick(kwargs)]]></scriptCode>
</scriptItem>
In my shelf item
import tool_attribute_as_comment tool_attribute_as_comment.display_next_attribute_as_comment()
In my python3.11libs/tool_attribute_as_comment.py
import hou
import os
import json
# Path setup
prefs_dir = hou.getenv("HOUDINI_USER_PREF_DIR")
json_path = os.path.join(prefs_dir, "attribute_display_config.json")
# Persistent index store
if not hasattr(hou.session, "attr_display_index"):
hou.session.attr_display_index = {}
# Ensure config file exists
def ensure_config_file():
if not os.path.exists(json_path):
with open(json_path, "w") as f:
json.dump({}, f, indent=4)
# Helper: load config safely and normalize attribute entries
def load_config():
ensure_config_file()
try:
with open(json_path, "r") as f:
raw = json.load(f)
except Exception as e:
hou.ui.setStatusMessage(f"Failed to read config:\n{e}")
return {}
# Normalize format: ensure each node-type maps to a list of dicts {name, label}
normalized = {}
for node_type, attrs in raw.items():
if attrs is None:
normalized[node_type] = []
continue
# If single string -> make list
if isinstance(attrs, str):
attrs = [attrs]
if not isinstance(attrs, list):
# unexpected type, skip defensively
normalized[node_type] = []
continue
normalized_list = []
for entry in attrs:
if isinstance(entry, str):
normalized_list.append({"name": entry, "label": entry})
elif isinstance(entry, dict):
name = entry.get("name") or entry.get("parm") or entry.get("param")
label = entry.get("label") or name
if name:
normalized_list.append({"name": name, "label": label})
# if no name, ignore malformed entry
else:
# unknown entry type; ignore
continue
normalized[node_type] = normalized_list
return normalized
# Helper: save config (assumes caller provides well-formed dict)
def save_config(config):
try:
# Save exactly what the caller provided. If you prefer a stable format,
# you can re-serialize to the normalized format before saving.
with open(json_path, "w") as f:
json.dump(config, f, indent=4)
except Exception as e:
hou.ui.setStatusMessage(f"Failed to save config:\n{e}")
# Main display function
def display_next_attribute_as_comment():
attr_config = load_config()
selected_nodes = hou.selectedNodes()
if not selected_nodes:
hou.ui.setStatusMessage("No nodes selected.")
return
for node in selected_nodes:
node_type = node.type().name()
node_path = node.path()
attrs = attr_config.get(node_type)
# attrs should already be normalized by load_config -> list of dicts
if not attrs:
node.setComment("No configured attributes in 'attribute_display_config.json'")
node.setGenericFlag(hou.nodeFlag.DisplayComment, True)
# ensure index is reset for this node type
hou.session.attr_display_index[node_path] = 0
continue
# Get current index, default 0
current_index = hou.session.attr_display_index.get(node_path, 0)
total_attrs = len(attrs)
# If we've reached or passed the end, show empty comment and reset
if current_index >= total_attrs:
node.setComment("") # empty comment as requested
node.setGenericFlag(hou.nodeFlag.DisplayComment, True)
hou.session.attr_display_index[node_path] = 0
continue
# Get two attributes at a time
end_index = current_index + 2
selected_attrs = attrs[current_index:end_index]
comment_lines = []
for attr_entry in selected_attrs:
# attr_entry is a dict with keys 'name' and 'label'
attr_name = attr_entry.get("name")
attr_label = attr_entry.get("label", attr_name)
try:
parm = node.parm(attr_name)
if parm is not None:
# obtain a friendly label if none provided: try parm template label
if not attr_label:
try:
tmpl = parm.parmTemplate()
attr_label = tmpl.label() if tmpl is not None else attr_name
except Exception:
attr_label = attr_name
attr_value = parm.eval()
comment_lines.append(f"{attr_label}: {attr_value}")
else:
comment_lines.append(f"{attr_label}: [Parm not found]")
except Exception as e:
comment_lines.append(f"{attr_label}: Error - {str(e)}")
# Update the comment on the node
node.setComment("\n".join(comment_lines))
node.setGenericFlag(hou.nodeFlag.DisplayComment, True)
# Store the updated index for next cycle (advance by 2)
hou.session.attr_display_index[node_path] = end_index
def edit_attribute_config_rclick(kwargs):
"""
Prepare arguments for the next function this is accessed using Houdini's right click menu on a parm.
"""
action = "add"
if kwargs['altclick'] == True:
action = "remove"
for parm in kwargs['parms']:
type = parm.node().type().name()
name = parm.name()
label = parm.description()
edit_attribute_config(type, name, attr_label=label, action=action)
# Helper: edit attribute config file (add/remove)
def edit_attribute_config(node_type, attr_name, attr_label=None, action="add"):
"""
Edit attribute_display_config.json.
action: "add" or "remove"
Example:
edit_attribute_config("geo", "shop_materialpath", "Material", "add")
edit_attribute_config("geo", "shop_materialpath", action="remove")
"""
# Load raw file (not normalized) so we preserve user's format as much as possible
ensure_config_file()
try:
with open(json_path, "r") as f:
raw = json.load(f)
except Exception as e:
hou.ui.setStatusMessage(f"Failed to read config for editing:\n{e}")
return
# Ensure the node_type exists and is a list
node_list = raw.get(node_type)
if node_list is None:
node_list = []
elif isinstance(node_list, str):
node_list = [node_list]
elif not isinstance(node_list, list):
# Unexpected; replace with empty list
node_list = []
if action == "add":
# Avoid duplicates (check by name)
names = set()
for ent in node_list:
if isinstance(ent, str):
names.add(ent)
elif isinstance(ent, dict):
n = ent.get("name")
if n:
names.add(n)
if attr_name in names:
hou.ui.setStatusMessage(f"{attr_name} already present for {node_type}")
return
# Add as dict so label is preserved
entry = {"name": attr_name, "label": attr_label or attr_name}
node_list.append(entry)
elif action == "remove":
new_list = []
for ent in node_list:
if isinstance(ent, str):
if ent != attr_name:
new_list.append(ent)
elif isinstance(ent, dict):
if ent.get("name") != attr_name:
new_list.append(ent)
else:
# unknown type, keep it just in case
new_list.append(ent)
node_list = new_list
else:
hou.ui.setStatusMessage(f"Invalid action: {action}")
return
raw[node_type] = node_list
try:
with open(json_path, "w") as f:
json.dump(raw, f, indent=4)
except Exception as e:
hou.ui.setStatusMessage(f"Failed to write config:\n{e}")
return
hou.ui.setStatusMessage(f"Config updated for {node_type}: {action} {attr_name}")
# New: reset functions
def reset_all_indexes():
"""Reset index for all nodes: clears the session store."""
hou.session.attr_display_index = {}
hou.ui.setStatusMessage("Attribute display indexes reset for all nodes.")
def reset_selected_nodes_indexes():
"""Reset index for currently selected nodes only."""
sel = hou.selectedNodes()
if not sel:
hou.ui.setStatusMessage("No nodes selected to reset.")
return
for n in sel:
path = n.path()
if path in hou.session.attr_display_index:
del hou.session.attr_display_index[path]
hou.ui.setStatusMessage("Attribute display indexes reset for selected nodes.")
# Run it
#display_next_attribute_as_comment()
Selection to bounding box to delete
Very often I manually select prims to delete a few local faces, then change something upstream, then my blast is all mangled. This will create a bounding box of the currently selected geo, group them with it and then blast the group. It's never perfect because of how bounding boxes operate (points/prims/othogonal to world axes), but it's very useful in my daily work.
Also Gemini did 90% of the coding and actually does error catching. It's a lot better than me :0
import hou, toolutils
def create_group_from_bbox():
"""
Creates a 'group' node and sets its bounding region parameters
based on the bounding box of the current geo selection
"""
try:
selected_nodes = hou.selectedNodes()
geo = toolutils.sceneViewer().selectGeometry()
# Check if exactly one node is selected.
if len(selected_nodes) != 1:
print("Please select a single geometry node to get the bounding box from.")
return
selected_node = selected_nodes[0]
# Check if the selected node is a geometry node (SOP).
if selected_node.type().category() != hou.sopNodeTypeCategory():
print("The selected node is not a geometry node (SOP). Please select a valid node.")
return
hou.clearAllSelected()
bbox = geo.boundingBox()
parent_node = selected_node.parent()
selected_node.setSelected(0)
group_node = parent_node.createNode("groupcreate", "bbox_group")
pos = [selected_node.position()[0],selected_node.position()[1]-1]
group_node.setPosition(pos)
group_node.setInput(0, selected_node)
group_node.parm("groupbounding").set(1)
group_node.parmTuple("size").set(bbox.sizevec())
group_node.parmTuple("t").set(bbox.center())
blast = parent_node.createNode("blast", "bbox_blast")
blast.setInput(0, group_node)
pos[1] = pos[1] - 1
blast.setPosition(pos)
blast.parm('group').set(group_node.parm("groupname").eval())
group_node.setSelected(1)
blast.setDisplayFlag(1)
blast.setSelected(1)
blast.setRenderFlag(1)
group_node.setCurrent(True, clear_all_selected=True)
except hou.Error as e:
print(f"An error occurred: {e}")
create_group_from_bbox()
Toggle Viewport Background Color
Bound it to F6
import sys
import toolutils
bg = None
try:
# cycle next bg
if kwargs['ctrlclick']: raise
bgs = hou.session.bg[:]
bgs = bgs[1:]+bgs[:1]
if kwargs['shiftclick']: bgs = ['bw', 'light', 'wb']
bg = bgs[0]
hou.session.bg = bgs
except:
# set up default bg vars
hou.session.bg = ['wb', 'bw', 'light']
bg = hou.session.bg[0]
bgs = { 'wb':'dark', 'bw':'grey', 'light':'light' }
hou.hscript("viewdisplay -B %s *" % bg)
hou.ui.setStatusMessage("Cycled background to %s" % bgs[bg].upper() )
Toggle Auto / Manual scene update
Bound to F11, I use it every day.
import hou
mode = hou.updateModeSetting().name()
if mode == 'AutoUpdate':
hou.setUpdateMode(hou.updateMode.Manual)
if mode == 'Manual':
hou.setUpdateMode(hou.updateMode.AutoUpdate)
Auto create a wrangle node
I'm pretty sure this exists already but why not reinvent the wheel. Creates an attr wrangle and keeps inputs/outputs, with the code box already in focus. I only use the 'P' parameter viewer (the one that pops up in your network view) so other desktops might not work.
bound to ctrl-shift-w
# TODO fix some weird inputs/outputs configurations
import hou
sn = hou.selectedNodes()
if sn:
w = sn[-1].parent().createNode('attribwrangle')
outputs = sn[-1].outputs()
w.setInput(0,sn[-1])
w.setCurrent(True, clear_all_selected=True)
w.setSelected(True,clear_all_selected=True, show_asset_if_selected=True)
w.setDisplayFlag(True)
sn[-1].setDisplayFlag(False)
if outputs:
for output in outputs:
output.setInput(0,w)
else:
#should not error out if it's launched in a shelf above a network plane
w = hou.ui.curDesktop().paneTabUnderCursor().pwd().createNode('attribwrangle')
w.moveToGoodPosition(relative_to_inputs=True, move_inputs=False, move_outputs=True, move_unconnected=False)
networkview = hou.ui.curDesktop().paneTabUnderCursor()
networkview.setCurrentNode(w,True)
networkview.parmMoveFocusTo('snippet')
Copy auto Paste-Merge
Copy from https://berniebernie.fr/wiki/Houdini_Python#Paste_clipboard_nodes_to_object_merges
Allow to copy nodes elsewhere with an object merge. Bound it to alt-v (so ctrl-c, alt-v in another place)
# this snippet will paste nodes in clipboard to object merges
# use it with a shortcut (I overrode 'alt-v' in network pane context)
import hou
network = hou.ui.curDesktop().paneTabUnderCursor()
networkpath = network.pwd().path()
pos = network.cursorPosition()
clipboard = hou.ui.getTextFromClipboard()
n = 0
if clipboard:
list = clipboard.split()
for item in list:
if hou.node(item) != None:
merge = hou.node(networkpath).createNode('object_merge','merge_'+item.split('/')[-1])
merge.parm('objpath1').set(str(item))
merge.setPosition(pos)
merge.move([n*2,0])
if n == 0:
merge.setSelected(True,True)
else:
merge.setSelected(True,False)
n = n + 1
Screenshot to background image
Customize your capture command line, no error checking 'cause I'm not a professional programmer, aka it works on my machine.
Should work with windows (hardcoded path to minicap ) and linux centos with xfce-screenshoter
#python 3.5+
import hou
import os
import subprocess
import nodegraphutils as utils
from time import gmtime, strftime
from os.path import expanduser
home = expanduser("~")
from pathlib import Path
exe = home+"\Downloads\exe\MiniCap.exe"
widthRatio = 4 # change to make screenshot bigger or smaller, this is ~x4 node width
def takeScreenShot(savePath):
'''change to your preferred capture app /!\ no error checking for now '''
subprocess.check_call([exe,"-captureregselect","-save",savePath,"-exit"])
#following is older code for linux i'll update it if I linux again
'''
Path(os.path.dirname(Path(savePath))).mkdir(parents=True, exist_ok=True)
if platform == "linux" or platform == "linux2":
savepath = os.path.dirname(Path(savePath)) #screenshot dir
screenshotPNG = subprocess.check_output('mv "$(xfce4-screenshooter -ro ls)" -v '+savepath,shell=True)
screenshotPNG = screenshotPNG.decode().strip().split('\n')[0].split(' -> ')[1].replace("'","") #helluva nasty oneliner
os.chdir(savepath)
os.rename(os.path.basename(screenshotPNG),os.path.basename(savePath))
# linux
elif platform == "darwin":
exit
elif platform == "win32":
subprocess.check_call([r"C:\Users\bernie\Documents\houdini18.0\config\exe\MiniCap.exe","-captureregselect","-save",savePath,"-exit"])
'''
def removeBackgroundImage(**kwargs):
''' erases bg image from tuples of backgroundImages() if it can find it, updates bg '''
deletingNode = [x[1] for x in kwargs.items()][0]
image = deletingNode.parm('houdinipath').eval()
editor = hou.ui.paneTabOfType(hou.paneTabType.NetworkEditor)
backgroundImagesDic = editor.backgroundImages()
backgroundImagesDic = tuple(x for x in backgroundImagesDic if x.path() != image)
editor.setBackgroundImages(backgroundImagesDic)
utils.saveBackgroundImages(editor.pwd(), backgroundImagesDic)
def changeBackgroundImageBrightness(event_type,**kwargs):
''' changes brightness/visibility if template or bypass flags are checked -- its poorly written/thought but i was tired'''
nullNode = [x[1] for x in kwargs.items()][0]
image = nullNode.parm('houdinipath').eval()
brightness = 1.0
if nullNode.isBypassed():
brightness = 0.0
elif nullNode.isTemplateFlagSet():
brightness = 0.5
editor = hou.ui.paneTabOfType(hou.paneTabType.NetworkEditor)
backgroundImagesDic = editor.backgroundImages()
i = 0
for item in backgroundImagesDic:
if item.path() == image:
backgroundImagesDic[i].setBrightness(brightness)
break
i = i + 1
editor.setBackgroundImages(backgroundImagesDic)
utils.saveBackgroundImages(editor.pwd(), backgroundImagesDic)
#generate unique(ish) path for screenshot
timestamp = strftime('%Y%m%d_%H%M%S', gmtime())
hipname = str(hou.getenv('HIPNAME'))
hippath = str(hou.getenv('HIP')) + '/screenshots'
screenshotName = hipname + '.' + timestamp + '.png'
pythonpath = hippath+'/'+screenshotName
systempath = hippath + '\\' + screenshotName
systempath = Path(systempath)
print(systempath)
houdinipath = '$HIP/screenshots/'+screenshotName
#take screenshot with capture region
takeScreenShot(systempath)
#set up background image plane
editor = hou.ui.paneTabOfType(hou.paneTabType.NetworkEditor)
image = hou.NetworkImage()
image.setPath(houdinipath)
sel = hou.selectedNodes()
nullNode = ''
if sel:
lastSel = sel[-1]
nullNode = lastSel.parent().createNode('null','screenshot')
if lastSel.outputConnections():
nullNode.setInput(0,lastSel)
else:
nullNode = editor.pwd().createNode('null','screenshot')
nullNode.moveToGoodPosition()
lastSel = nullNode
#configure image plane placement
nullNode.setUserData('nodeshape','task')
nullNode.setPosition(lastSel.position())
nullNode.setColor(hou.Color(.3,.3,.3))
nullNode.move([lastSel.size()[0]*2,-lastSel.size()[1]*2])
rez = hou.imageResolution(pythonpath)
ratio = 1.0*rez[1]/rez[0]
rect = hou.BoundingRect(0,-lastSel.size()[1]*1.1,widthRatio,-widthRatio*ratio-lastSel.size()[1]*1.1)
image.setRelativeToPath(nullNode.path())
image.setRect(rect)
#following is adding a spare parm with image path to be able to know which node corresponds to which background image
#could have used a user attribute or relativeToPath() and smarter logic but it works and it helps me visualize filepath
hou_parm_template_group = hou.ParmTemplateGroup()
hou_parm_template = hou.LabelParmTemplate("houdinipath", "Label", column_labels=(['\\'+houdinipath]))
hou_parm_template.hideLabel(True)
hou_parm_template_group.append(hou_parm_template)
nullNode.setParmTemplateGroup(hou_parm_template_group)
#attach a function that deletes the background image plane if the corresponding node is deleted (faster than doing it by hand)
nullNode.addEventCallback((hou.nodeEventType.BeingDeleted,), removeBackgroundImage)
#attach a function to change visibility or opacity if corresponding node flags are changed
nullNode.addEventCallback((hou.nodeEventType.FlagChanged,), changeBackgroundImageBrightness)
#add image to network background
backgroundImagesDic = editor.backgroundImages()
backgroundImagesDic = backgroundImagesDic + (image,)
editor.setBackgroundImages(backgroundImagesDic)
utils.saveBackgroundImages(editor.pwd(), backgroundImagesDic)
Other UI
Previews
Detach parameter window à la maya
Auto add frame offset parm
Userdocs XMLs
This is for right click context menus. Use the hscript 'menurefresh' when developping so as not to relaunch H each time (like a reload(module) in python).
<?xml version="1.0" encoding="UTF-8"?> <menuDocument> <menu> <subMenu id="bernie_rclick"> <label>Bernie's</label> <modifyItem><insertAfter>motion_effects_menu</insertAfter></modifyItem> <scriptItem id="bernie_set_frameoffset_parm"> <label>Add a frame offset parm to $F#</label> <context> <expression> import bernie_tools return bernie_tools.validate_sequence_parm(kwargs) </expression> </context> <scriptCode><![CDATA[import bernie_tools;reload(bernie_tools);bernie_tools.add_sequence_offset_spareparm(kwargs)]]></scriptCode> </scriptItem> <scriptItem id="bernie_set_frameoffset_parm"> <label>Browse to...</label> <context> <expression> import bernie_tools reload(bernie_tools) return bernie_tools.validate_uri(kwargs) </expression> </context> <scriptCode><![CDATA[import bernie_tools;reload(bernie_tools);bernie_tools.browse_to(kwargs)]]></scriptCode> </scriptItem> <scriptItem id="bernie_open_parm_spreadsheet"> <label>Open/Add in Parameter Spreadsheet</label> <scriptCode><![CDATA[import bernie_tools;reload(bernie_tools);bernie_tools.open_parm_spreadsheet(kwargs)]]></scriptCode> </scriptItem> </subMenu> </menu> </menuDocument>
ParmGearMenu.xml
<?xml version="1.0" encoding="UTF-8"?>
<!--
(long sidefx text)
-->
<menuDocument>
<!-- menuDocument can only contain 1 menu element, whose id is
implicitly "root_menu"
-->
<menu>
<scriptItem id="detach_parameter_window">
<label>Detach Parameter Window...</label>
<scriptCode><![CDATA[import bernie_tools;reload(bernie_tools);bernie_tools.detach_parameter_window(kwargs)]]></scriptCode>
</scriptItem>
</menu>
</menuDocument>
bernie_tools.py
py3
Some updates
import hou
import traceback
import re
import os
def return_first_parm(allKwargs):
'''given a right click context with kwargs, return what we want, the parameter -- which can be locked or not'''
parm = lockedparm = normalparm = False
try:
lockedparm = allKwargs['locked_parms'][0]
except:
pass
try:
normalparm = allKwargs['parms'][0]
except:
pass
if lockedparm:
parm = lockedparm
elif normalparm:
parm = normalparm
else:
print('bug')
return parm
def validate_sequence_parm(kwargs):
'''Checks if the first parm contains a value with $F'''
parm = return_first_parm(kwargs)
returnvalue = False
try:
returnvalue = '$F' in parm.rawValue()
except:
print("ERROR: %s" % traceback.format_exc())
return returnvalue
def validate_uri(kwargs):
'''Checks if the given argument, or its parent or grandparent is either a file or folder'''
parm = return_first_parm(kwargs)
parm = parm.eval()
parm = str(parm)
returnvalue = False
if os.path.isdir(parm) or os.path.isdir(os.path.dirname(parm)) or os.path.isdir(os.path.dirname(os.path.dirname(parm))):
returnvalue = True
return returnvalue
def browse_to(kwargs):
'''opens a browser to this given path (if a file), or its parent if it doesn't exist'''
parm = return_first_parm(kwargs)
path = os.path.dirname(parm.eval())
dir = ''
if os.path.isdir(path):
dir = path
elif os.path.isdir(os.path.dirname(path)):
dir = os.path.dirname(path)
else:
print('No directory found')
if dir != '':
os.startfile(os.path.realpath(dir))
def add_sequence_offset_spareparm(kwargs):
'''Adds an int spare parm + file parm below a parm that has a file sequence expression of type '$F#' using hscript's padzero'''
parm = return_first_parm(kwargs)
path = os.path.dirname(parm.eval())
parmval = parm.rawValue()
matchObj = re.match( r'^(.*)\$F(\d*)(.*)', parmval, re.M|re.I)
if matchObj:
n = parm.node()
parmGrp = n.parmTemplateGroup()
existing_parm = parmGrp.find(parm.name())
label = existing_parm.label()
name = existing_parm.name()
#create an offset parm with a similar name. No error checking!
fileParmTemplate = hou.StringParmTemplate(name+'_file',label+'_file', 1, default_value=([parmval]),string_type=hou.stringParmType.FileReference)
offsetParmTemplate = hou.IntParmTemplate(name+'_offset',label+'_offset', 1, default_expression=(["$F + 0"]))
parmGrp.insertAfter(existing_parm, fileParmTemplate)
parmGrp.insertAfter(fileParmTemplate, offsetParmTemplate)
n.setParmTemplateGroup(parmGrp)
#display the expression as it's most like we want to mmb through the offset
n.parm(name+'_offset').showExpression(1)
expression = "path=parm('"+name+"_file').rawValue().split('$F')\nframe=parm('"+name+"_offset').eval()\npad = int(path[1][0]) if path[1][1] is '.' else 0\nreturn path[0]+str(frame).zfill(pad)+'.'+path[1].split('.')[-1];"
hou_keyframe = hou.StringKeyframe()
hou_keyframe.setTime(1) #arbitrary keyframe needed to enable expression i think
hou_keyframe.setExpression(expression, hou.exprLanguage.Python)
parm.setKeyframe(hou_keyframe)
def detach_parameter_window(kwargs):
'''Open a floating parameter pane for a particular node.'''
node = kwargs['node']
pane_tab = hou.ui.curDesktop().createFloatingPaneTab(hou.paneTabType.Parm)
pane_tab.setCurrentNode(node)
pane_tab.setPin(True)
return pane_tab
def open_parm_spreadsheet(kwargs):
'''Opens the parameter spreadsheet with the current node selection and the right-clicked parm, appends the parm if it is already opened'''
# todo: keep existing parm mask to append to it. Can't retrieve current value AFAIK.
#import pprint
#pprint.pprint(kwargs)
#selectedParms = return_first_parm(kwargs)
parms = []
for parm in kwargs['parms']:
parms.append(parm)
for parm in kwargs['locked_parms']:
parms.append(parm)
selectedParm = parms[0]
parms = [x.name() for x in parms]
nodepaths = " ".join([node.path() for node in hou.selectedNodes()])
if not hou.selectedNodes():
nodepaths = selectedParm.node().path()
parmsheetP = selectedParm.name()
parmsheetPaths = nodepaths
if kwargs['ctrlclick']:
rmi = hou.ui.readMultiInput('Edit the node path and parms.', ['Node(s)','Parm(s)'], buttons=('OK','Cancel'), severity=hou.severityType.Message, default_choice=0, close_choice=1, help='Node path can have wildcards', title='Parm Chooser', initial_contents=(nodepaths.split(' ', 1)[0] ,selectedParm.name()))
if rmi[0]==0:
parmsheetPaths = rmi[1][0]
parmsheetP = rmi[1][1]
ps = hou.ui.findPaneTab('parmsheet')
if not ps:
desktop = hou.ui.curDesktop()
ps = desktop.createFloatingPaneTab(hou.paneTabType.ParmSpreadsheet)
cmd = 'parmsheet -w 0 -p "'+parmsheetP+'" -o "'+parmsheetPaths+'" '+ps.name()
hou.hscript(cmd)
def test(var):
print(var)
py2
Need to update to py3 for H19
In my userdocs\houdini18.5\python2.7libs folder
#march2022 update: fuck putin and improved the add offset to sequence so that you can still choose a new file and keep the offset without having to delete the spare parms (for imageplane frame offset for instance)
import hou
import traceback
import re
import os
def return_first_parm(allKwargs):
'''given a right click context with kwargs, return what we want, the parameter -- which can be locked or not'''
parm = lockedparm = normalparm = False
try:
lockedparm = allKwargs['locked_parms'][0]
except:
pass
try:
normalparm = allKwargs['parms'][0]
except:
pass
if lockedparm:
parm = lockedparm
elif normalparm:
parm = normalparm
else:
print('bug')
return parm
def validate_sequence_parm(kwargs):
'''Checks if the first parm contains a value with $F'''
parm = return_first_parm(kwargs)
returnvalue = False
try:
returnvalue = '$F' in parm.rawValue()
except:
print("ERROR: %s" % traceback.format_exc())
return returnvalue
def validate_uri(kwargs):
'''Checks if the given argument, or its parent or grandparent is either a file or folder'''
parm = return_first_parm(kwargs)
parm = parm.eval()
parm = str(parm)
returnvalue = False
if os.path.isdir(parm) or os.path.isdir(os.path.dirname(parm)) or os.path.isdir(os.path.dirname(os.path.dirname(parm))):
returnvalue = True
return returnvalue
def browse_to(kwargs):
'''opens a browser to this given path (if a file), or its parent if it doesn't exist'''
parm = return_first_parm(kwargs)
path = os.path.dirname(parm.eval())
dir = ''
if os.path.isdir(path):
dir = path
elif os.path.isdir(os.path.dirname(path)):
dir = os.path.dirname(path)
else:
print('No directory found')
if dir != '':
os.startfile(os.path.realpath(dir))
def add_sequence_offset_spareparm(kwargs):
'''Adds an int spare parm + file parm below a parm that has a file sequence expression of type '$F#' using hscript's padzero'''
parm = return_first_parm(kwargs)
path = os.path.dirname(parm.eval())
parmval = parm.rawValue()
matchObj = re.match( r'^(.*)\$F(\d*)(.*)', parmval, re.M|re.I)
if matchObj:
n = parm.node()
parmGrp = n.parmTemplateGroup()
existing_parm = parmGrp.find(parm.name())
label = existing_parm.label()
#create an offset parm with a similar name. No error checking!
fileParmTemplate = hou.StringParmTemplate(label+'_file',label+'_file', 1, default_value=([parmval]),string_type=hou.stringParmType.FileReference)
offsetParmTemplate = hou.IntParmTemplate(label+'_offset',label+'_offset', 1, default_expression=(["$F + 0"]))
parmGrp.insertAfter(existing_parm, fileParmTemplate)
parmGrp.insertAfter(fileParmTemplate, offsetParmTemplate)
n.setParmTemplateGroup(parmGrp)
#display the expression as it's most like we want to mmb through the offset
n.parm(label+'_offset').showExpression(1)
expression = "path=parm('"+label+"_file').rawValue().split('$F')\nframe=parm('"+label+"_offset').eval()\npad = int(path[1][0]) if path[1][1] is '.' else 0\nreturn path[0]+str(frame).zfill(pad)+'.'+path[1].split('.')[-1];"
hou_keyframe = hou.StringKeyframe()
hou_keyframe.setTime(1) #arbitrary keyframe needed to enable expression i think
hou_keyframe.setExpression(expression, hou.exprLanguage.Python)
parm.setKeyframe(hou_keyframe)
def detach_parameter_window(kwargs):
'''Open a floating parameter pane for a particular node.'''
node = kwargs['node']
pane_tab = hou.ui.curDesktop().createFloatingPaneTab(hou.paneTabType.Parm)
pane_tab.setCurrentNode(node)
pane_tab.setPin(True)
return pane_tab
def open_parm_spreadsheet(kwargs):
'''Opens the parameter spreadsheet with the current node selection and the right-clicked parm, appends the parm if it is already opened'''
#import pprint
#pprint.pprint(kwargs)
selectedParm = return_first_parm(kwargs)
nodepaths = " ".join([node.path() for node in hou.selectedNodes()])
if not hou.selectedNodes():
nodepaths = selectedParm.node().path()
#print(nodepaths)
ps = hou.ui.findPaneTab('parmsheet')
if not ps:
desktop = hou.ui.curDesktop()
ps = desktop.createFloatingPaneTab(hou.paneTabType.ParmSpreadsheet)
cmd = 'parmsheet -w 1 -p "'+selectedParm.name()+'" -o "'+nodepaths+'" '+ps.name()
hou.hscript(cmd)
def test(var):
print(var)