1. A small update to the UI so fewer input windows are used.
2. The "clear" action will now reset scoped parms to default. To avoid unexpected changes to parms there must be a method to identify which parms are used for animation. For that hou.parm(parm_path).isScoped() can be used. Then it is possible to easily control what parms or set of parms are cleared.
Extra: If you merely want to clear channels without limiting the range you can simply use shift+ctrl+LMB and ctrl+MMB on a group label for parms. At least one group is needed because the tool need to know what parms you are dealing with. A video is added to show this.
def commKeyframeManipulator():
"""Code for manipulating keyframed parms of one or many selected node(s).
Keyframes are expected to have been promoted to the selected node.
That is why this function only look for keyframes on the selected node(s) parms.
It utilizes the Houdini Object Model (HOM) included in SideFX Houdini.
It is made for the Houdini community.
Current version: 0.9.5 alpha.
Authors: SWest (v0.9.5 alpha).
License: https://creativecommons.org/licenses/by/4.0"""
# Code for some initial variables
actions = ('offset','clear','refit','cancel')
inputs_valid = True
def getNodes():
"""A function for getting all the user's desired nodes to adjust."""
selected_nodes = hou.selectedNodes()
inputs_valid = False
if selected_nodes == ():
message = 'Please select a node with keyframes and try again. '
hou.ui.displayMessage(message)
else:
inputs_valid = True
return inputs_valid, selected_nodes
def getAction(actions):
"""A function used to present the user with a set of tools related
to manipulating keyframes. The user's choice is returned."""
choice = hou.ui.displayMessage('Please choose desired action:',
buttons=actions,
default_choice=0, close_choice=0)
inputs_valid = True
action = actions[choice]
if action == 'cancel':
inputs_valid = False
return inputs_valid, action
def getOffset(action_dict):
"""A function used to get the necessary inputs for the offset action.
This include offset value and range.
Todo: extract snippets to reusable functions. """
title = 'Offset'
message = 'Offset settings'
temp_dict = {
'offset': '1.0',
'range_start': '1',
'range_end': '10000'
}
input_labels = ()
for k,v in temp_dict.items():
input_labels += (k,)
initial_contents = ()
for v in temp_dict.values():
initial_contents += (v,)
help = """
Help for offset:
offset
A positive or negative number for frame offset.
Example: -10 or 15.5 (float)
range
range_start: Start frame (float)
range_end: End frame (float)
"""
buttons = ('OK','Cancel')
severity = hou.severityType.Message
choice, user_contents = hou.ui.readMultiInput(message=message, input_labels=input_labels, password_input_indices=(),
buttons=buttons, severity=severity,
default_choice=0, close_choice=-1, help=help,
title=title, initial_contents=initial_contents)
inputs_valid = True
try:
action_dict['offset'] = float(user_contents[0])
except:
inputs_valid = False
message = 'The offset value is not valid. '
try:
action_dict['range_start'] = float(user_contents[1])
except:
inputs_valid = False
message = 'The range start value is not valid. '
try:
action_dict['range_end'] = float(user_contents[2])
message = 'The range end value is not valid. '
except:
inputs_valid = False
if not inputs_valid:
temp = hou.ui.displayMessage('Input error: '+message, title='Notification', buttons=('OK',))
if not choice == 0:
inputs_valid = False
# print the settings (debug)
# for k,v in action_dict.items():
# print(f'{k} {v} {type(v)}')
return inputs_valid, action_dict
def getClear(action_dict):
"""A function used to get the necessary inputs for the clear action.
This include range.
Todo: extract snippets to reusable functions. """
title = 'Clear'
message = 'Settings for the Clear action'
temp_dict = {
'range_start': '1',
'range_end': '10000'
}
input_labels = ()
for k,v in temp_dict.items():
input_labels += (k,)
initial_contents = ()
for v in temp_dict.values():
initial_contents += (v,)
help = """
Help for Clear:
Scoped parms or channels will be reset to default.
Parms or channels can be categorized with tabs (folders) in the properties UI.
To set parms as scoped shift+MMB on a UI folder or parm label.
To disable a parm as scoped shift+ctrl+MMB on a tab or a parm label.
range
range_start: Start frame (float)
range_end: End frame (float)
"""
buttons = ('OK','Cancel')
severity = hou.severityType.Message
choice, user_contents = hou.ui.readMultiInput(message=message, input_labels=input_labels, password_input_indices=(),
buttons=buttons, severity=severity,
default_choice=0, close_choice=-1, help=help,
title=title, initial_contents=initial_contents)
inputs_valid = True
try:
action_dict['range_start'] = float(user_contents[0])
except:
inputs_valid = False
message = 'The range start value is not valid. '
try:
action_dict['range_end'] = float(user_contents[1])
except:
inputs_valid = False
message = 'The range end value is not valid. '
if not inputs_valid:
temp = hou.ui.displayMessage('Input error: '+message, title='Notification', buttons=('OK',))
if not choice == 0:
inputs_valid = False
return inputs_valid, action_dict
def getRefit(action_dict):
"""A function used to get the necessary inputs for the refit action.
Todo: extract snippets to reusable functions. """
title = 'Refit'
message = 'Refit settings'
refit_dict = {
'refit': 'True',
'refit_tol': '1.0',
'refit_preserve_extrema': 'False',
'refit_bezier': 'True',
'resample': 'False',
'resample_rate': '1.0',
'resample_tol': '1.0',
'range': 'True',
'range_start': '1',
'range_end': '100',
'bake_chop': '1'
}
input_labels = ()
for k,v in refit_dict.items():
input_labels += (k,)
initial_contents = ()
for v in refit_dict.values():
initial_contents += (str(v),)
help = """
Help for refit:
refit
True: cubic refitting
False: only resampling or range but no refitting
refit_tol: refit tolerance in absolute value
refit_preserve_extrema: preserves keys that are local minima or maxima
refit_bezier
True: new keyframes use bezier()
False: new keyframes use cubic()
resample
True: Resampling is done before refitting
False: No resampling
resample_rate: 1.0 adds a keyframe for each frame.
resample_tol: 1.0 meaning no resampling for subframes.
range
True: use range_start and range_end
False: use first and last keyframe
range_start: Start frame
range_end: End frame
bake_chop: an enumeration value
"""
buttons = ('OK','Cancel')
severity = hou.severityType.Message
choice, user_contents = hou.ui.readMultiInput(message=message, input_labels=input_labels, password_input_indices=(),
buttons=buttons, severity=severity,
default_choice=0, close_choice=-1, help=help,
title=title, initial_contents=initial_contents)
# for i, (k,v) in enumerate(refit_dict.items()):
# refit_dict[k] = user_contents[i]
# validate inputs
inputs_valid = True
# something is wrong with the following
try:
action_dict['refit'] = bool(user_contents[0])
action_dict['refit_tol'] = float(user_contents[1])
action_dict['refit_preserve_extrema'] = bool(user_contents[2])
action_dict['refit_bezier'] = bool(user_contents[3])
action_dict['resample'] = bool(user_contents[4])
action_dict['resample_rate'] = float(user_contents[5])
action_dict['resample_tol'] = float(user_contents[6])
action_dict['range'] = bool(user_contents[7])
action_dict['range_start'] = float(user_contents[8])
action_dict['range_end'] = float(user_contents[9])
action_dict['bake_chop'] = float(user_contents[10])
except:
inputs_valid = False
else:
for k,v in action_dict.items():
print(f'{k} {v}')
# action_dict.update(refit_dict)
if not choice == 0:
inputs_valid = False
temp = hou.ui.displayMessage('refit is not yet fully implemented', title='Notification',
buttons=('OK',))
return inputs_valid, action_dict
def getParmsFromRange(action_dict, selected_nodes):
"""A function used for getting all parms with keyframes from the
user's selection. It will return a dict with the parm path and
keyframes as a list using json"""
parms_input_dict = {}
range_start = action_dict['range_start']
range_end = action_dict['range_end']
for node in selected_nodes:
for parm in node.allParms():
parm_is_scoped = parm.isScoped()
parm_keyframes_length = 0
try:
parm_keyframes = parm.keyframes()
except:
break
else:
parm_keyframes_length=len(parm_keyframes)
if(parm_keyframes_length > 0 and parm_is_scoped):
keyframes_input = []
for i in range(0,parm_keyframes_length):
keyframe_dict=parm.keyframes()[i].asJSON(save_keys_in_frames=True)
frame_in=keyframe_dict['frame']
if ( frame_in >= range_start) and (frame_in <= range_end):
keyframes_input.append(keyframe_dict.copy())
if len(keyframes_input) > 0:
parms_input_dict[parm.path()] = keyframes_input
return parms_input_dict
def getChanges(action_dict, parms_input_dict):
"""A function used to calculate the intended changes to keyframes.
It support different actions (tools). """
parms_output_dict = {}
keyframes_output_list = []
action = action_dict['action']
if action == 'offset':
offset = action_dict['offset']
range_start = action_dict['range_start']
range_end = action_dict['range_end']
for parm, keyframes in parms_input_dict.items():
keyframes_output_list = []
for keyframe in keyframes:
keyframe_dict = keyframe.copy()
frame_in=keyframe_dict['frame']
if ( frame_in >= range_start) and (frame_in <= range_end):
if action == 'offset':
frame_out=frame_in+offset
keyframe_dict['frame'] = frame_out
keyframes_output_list.append(keyframe_dict.copy())
if action == 'clear':
if keyframe_dict not in keyframes_output_list:
keyframes_output_list.append(keyframe_dict.copy())
parms_output_dict[parm] = keyframes_output_list
return parms_output_dict
def showIntentions(action_dict={}, parms_input_dict={}, parms_output_dict={}):
"""A function used to show the proposed changes based on the user input.
The user may proceed with OK or cancel the action. """
action = action_dict['action']
range_start = action_dict['range_start']
range_end = action_dict['range_end']
inputs_valid = True
report_str = ''
report_str += f'action: {action}\n\n'
report_str += f'selection range: {range_start}-{range_end}\n\n'
if len(parms_input_dict.items()) > 0:
is_keyframes = True
else:
is_keyframes = False
# distinguish report type
if is_keyframes:
parm_count = 1
if action == 'offset':
offset = action_dict['offset']
report_str += f'offset: {offset}\n\n'
report_str += f'Parms and keyframes: \n\n\n'
for i, (parm, keyframes) in enumerate(parms_input_dict.items()):
keyframe_count = 1
report_str += (f'parm {parm_count}: {parm}\n\n')
for k in range(0,len(keyframes)):
report_str += f'keyframe {keyframe_count}\n\n'
report_str += f'from: {keyframes[k]}\n\n'
report_str += f'to: {parms_output_dict[parm][k]}\n\n'
keyframe_count += 1
parm_count += 1
report_str += f'\n\n'
if action == 'clear':
report_str += f'Keyframes to be deleted: \n\n\n'
for i, (parm, keyframes) in enumerate(parms_input_dict.items()):
keyframe_count = 1
report_str += (f'parm {parm_count}: {parm}\n\n')
#if len(keyframes) > 1:
for k in range(0,len(keyframes)):
report_str += f'keyframe {keyframe_count}\n'
report_str += f'{parms_output_dict[parm][k]}\n\n'
keyframe_count += 1
parm_count += 1
report_str += f'\n\n'
choice = hou.ui.displayMessage(report_str, title='Proposed changes',
buttons=('OK','Cancel'))
if choice == 1:
inputs_valid = False
# no keyframes
else:
report_str += f'No keyframes are found in the selection. \n\n\n'
inputs_valid = False
choice = hou.ui.displayMessage(report_str, title='Proposed changes',
buttons=('OK','Cancel'))
if choice == 1:
inputs_valid = False
return inputs_valid
def setSingleKeyframe(parm_str, keyframe_dict):
"""A function used to set a new keyframe using a definition from a dict."""
# a new keyframe from template
keyframe_out = hou.Keyframe()
# configure the new keyframe from the dict
keyframe_out.fromJSON(keyframe_dict)
# set a new keyframe
hou.parm(parm_str).setKeyframe(keyframe_out)
def deleteKeyframes(input_dict):
"""A function used to delete keyframes from a dict."""
for parm_str, keyframes in input_dict.items():
for keyframe_dict in keyframes:
frame_out = keyframe_dict['frame']
hou.parm(parm_str).deleteKeyframeAtFrame(frame_out)
def setKeyframes(input_dict):
"""A function used to set keyframes from a dict."""
for parm_str, keyframes in input_dict.items():
for keyframe_dict in keyframes:
setSingleKeyframe(parm_str, keyframe_dict)
def setScopedParmsToDefault(selected_nodes):
"""A function used to set parms to default.
It is limited to scoped parms.
It must be so otherwise all parms are reset to default and that
might lead to undesired results."""
for node in selected_nodes:
for parm in node.allParms():
parm_is_scoped = parm.isScoped()
if parm.isScoped():
parm.revertToDefaults()
def makeChanges(action_dict, parms_input_dict, parms_output_dict):
"""A function used to make actual changes to keyframes specified
in the input dictionary. All inputs should already have been validated.
The proposed changes should be accepted by the user before this stage."""
action = action_dict['action']
range_start = action_dict['range_start']
range_end = action_dict['range_end']
if action == 'offset':
offset = action_dict['offset']
deleteKeyframes(parms_input_dict)
setKeyframes(parms_output_dict)
if action == 'clear':
deleteKeyframes(parms_output_dict)
if action == 'refit':
pass
#deleteKeyframes(parms_input_dict)
#setKeyframes(parms_output_dict)
# main:
# get user inputs start
inputs_valid, selected_nodes = getNodes()
action_dict = {}
# get user action
if inputs_valid:
inputs_valid, action = getAction(actions)
if inputs_valid:
action_dict['action'] = action
# get various inputs based on selected action
if inputs_valid:
if action == 'offset':
inputs_valid, action_dict = getOffset(action_dict)
elif action == 'clear':
inputs_valid, action_dict = getClear(action_dict)
setScopedParmsToDefault(selected_nodes)
elif action == 'refit':
inputs_valid, action_dict = getRefit(action_dict)
else:
inputs_valid = False
# get parms and keyframes from user selection
if inputs_valid:
parms_input_dict = getParmsFromRange(action_dict, selected_nodes)
# calculate intended changes into parms_output_dict
if inputs_valid:
parms_output_dict = getChanges(action_dict, parms_input_dict)
# present intended changes
if inputs_valid:
inputs_valid = showIntentions(action_dict, parms_input_dict,
parms_output_dict)
# perform changes
if inputs_valid:
makeChanges(action_dict, parms_input_dict, parms_output_dict)
commKeyframeManipulator()