USES
The Maya SnapKey command can't resolve cases where there is more than one keyframe in between two consecutive whole number frames and you'll get a warning message like this: "// Warning: line 0: Had to skip snapping on some keys since multiple keys existed between the snap multiple. Command result indicates number of skipped keys. // "
The Snap Tools UI provides tools to handle this situation,
INTERFACE
One button fix:
The Snap and Fix button will call the Maya snapKey command on the selected time range and then handle the leftover non-integer keyframes following these rules:
- if the previous whole number frame doesn't have keyframes, it moves the non-integer key to the previous whole number frame.
- if the previous whole number frame already has a keyframe, it moves the non-integer key to the next whole number frame, unless the next whole number already has a keyframe
- if both the previous whole frame and the next whole frame already have keyframes, it deletes the non-integer keyframe.
Manual fix:
If you want a different solution than the automatic "Snap and Fix" you can handle it manually using the following tools that work on the selected time range.
Update list: It populates and refreshes the "Select key:" drop down menu with all the non-integer keys found in the time range for the selected object. You will need to use this button to refresh the list if you alter the keys without using the Snap Tools UI (for example using Ctrl+Z or deleting or moving a key directly from the timeline or the graph editor)
Snap: It calls the Maya snapKey command and populates the "Select key:" drop down menu with all the non-integer keys found in the time range for the selected object.
The rest of the tools are pretty self explanatory:
Delete selected key
Move key to previous whole number
Move key to next whole number
Go to selected key
INSTALLATION
To get the Snap Tools UI, download the script file and save it in your maya/version/scripts folder:
Download snapTools.py
Then run the following code in the script editor (make sure to be in a python tab):
import snapTools
reload(snapTools)
You can save these lines in a shelf button for easy access.
If you don't want to download the file you can just copy the code below to the script editor and drag it onto your custom shelves so you can access it at any time (remember to save your shelves before quitting Maya)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import maya.cmds as mc | |
from functools import partial | |
def warning_msg(msg, arg=None): | |
mc.confirmDialog(title='Warning', message=msg, button='ok') | |
#check if something selected | |
def check_selection(arg=None): | |
if not mc.ls(sl=True): | |
warning_msg('No objects selected') | |
return False | |
else: | |
return True | |
def get_time_range(arg=None): | |
startTime = mc.intFieldGrp(timeRange, query=True, value1=True) | |
endTime = mc.intFieldGrp(timeRange, query=True, value2=True) | |
return (startTime, endTime) | |
def get_keyframes(arg=None): | |
if check_selection(): | |
keyframes = mc.keyframe(time=get_time_range(), query=True) #find keyframes in time range | |
keyframeSet = set(keyframes) #remove duplicates | |
keyframeList = list(keyframeSet) | |
return keyframeList | |
else: | |
return False | |
#find non int keys and add to drop menu | |
def list_nonInt(arg=None): | |
nonInt_list = [] | |
keyframeList = get_keyframes() | |
if keyframeList: | |
for number in keyframeList: | |
if number != int(number)and number not in nonInt_list: | |
nonInt_list.append(number) | |
nonInt_list.sort() | |
currentItems= mc.optionMenuGrp(floatKeys, q = True, ill = True) | |
if currentItems: | |
mc.deleteUI(currentItems)#empty menu | |
for item in nonInt_list: | |
mc.menuItem(parent=(floatKeys +'|OptionMenu'), label= str(item))#repopulate menu | |
def snap_keys(arg=None): | |
sel = check_selection() | |
if sel: | |
mc.snapKey(time=get_time_range()) | |
list_nonInt() | |
def get_selectedKey(arg=None): | |
#query value from drop down menu | |
selectedKey = mc.optionMenuGrp(floatKeys, query=True, value=True) | |
if selectedKey: | |
return selectedKey | |
else: | |
warning_msg('No key selected') | |
return False | |
def go_to_selectedKey(arg=None): | |
selectedKey = get_selectedKey() | |
if selectedKey: | |
mc.currentTime(selectedKey , edit=True ) | |
def delete_selectedKey(arg=None): | |
if check_selection(): | |
selectedKey = get_selectedKey() | |
mc.cutKey(t=(selectedKey, selectedKey)) | |
list_nonInt() #update menu | |
#move selected key to previous int frame (check if key already there, ask if replace) | |
def move_key_fwd(arg=None): | |
keyframeList = get_keyframes() | |
if keyframeList: | |
selectedKey = get_selectedKey() | |
roundUp = int(float(selectedKey))+1.0 | |
if roundUp in keyframeList: | |
replace = mc.confirmDialog( title='Replace keyframe', message='That frame already has a keyframe. Do you want to replace it?', | |
button=['Yes','No'], defaultButton='Yes', cancelButton='No', dismissString='No' ) | |
if replace == 'Yes': | |
mc.cutKey(time=(selectedKey, selectedKey)) | |
mc.pasteKey(time=(roundUp, roundUp)) | |
print ("moved key on frame "+ str(selectedKey)+" to frame "+ str(roundUp)) | |
list_nonInt() #update menu | |
else: | |
mc.cutKey(time=(selectedKey, selectedKey)) | |
mc.pasteKey(time=(roundUp, roundUp)) | |
print ("moved key on frame "+ str(selectedKey)+" to frame "+ str(roundUp)) | |
list_nonInt() #update menu | |
#move selected key to next int frame (check if key already there, ask if replace) | |
def move_key_back(arg=None): | |
keyframeList = get_keyframes() | |
if keyframeList: | |
selectedKey = get_selectedKey() | |
roundDown = int(float(selectedKey)) | |
if roundDown in keyframeList: | |
replace = mc.confirmDialog( title='Replace keyframe', message='That frame already has a keyframe. Do you want to replace it?', | |
button=['Yes','No'], defaultButton='Yes', cancelButton='No', dismissString='No' ) | |
if replace == 'Yes': | |
mc.cutKey(time=(selectedKey, selectedKey)) | |
mc.pasteKey(time=(roundDown, roundDown)) | |
print ("moved key on frame "+ str(selectedKey)+" to frame "+str(roundDown)) | |
list_nonInt() | |
else: | |
mc.cutKey(time=(selectedKey, selectedKey)) | |
mc.pasteKey(time=(roundDown, roundDown)) | |
print ("moved key on frame "+ str(selectedKey)+" to frame "+str(roundDown)) | |
list_nonInt() | |
def snapFix(arg=None): | |
keyframeList = get_keyframes() | |
if keyframeList: | |
for number in keyframeList: | |
if number != int(number): # non int numbers | |
mc.cutKey(time=(number, number)) | |
roundDown = float(int(number)) | |
roundUp = int(number)+1.0 | |
if roundDown in keyframeList: | |
if roundUp in keyframeList: | |
# delete key if the previous and next int frames already have keys | |
print("deleted key on frame " + str(number)) | |
else: | |
# move key to next int frame if previous int frame already has key | |
mc.pasteKey(time=(roundUp, roundUp)) | |
keyframeList.append(roundUp) | |
print ("moved key on frame "+ str(number)+" to frame "+ str(roundUp)) | |
else: | |
# move key to previous int frame if previous int doesn't have key | |
mc.pasteKey(time=(roundDown, roundDown)) | |
keyframeList.append(roundDown) | |
print ("moved key on frame "+ str(number)+" to frame "+str(roundDown)) | |
##GUI | |
#check if gui is already open | |
if mc.window('snapToolsUI', query=True, exists=True): | |
mc.deleteUI('snapToolsUI', window=True) | |
mc.window('snapToolsUI', title='Snap Tools') | |
mc.columnLayout( adjustableColumn=True ) | |
mc.text(label='Time range:', align='left') | |
#get timeline values | |
minTime = mc.playbackOptions(query=True, minTime=True) | |
maxTime = mc.playbackOptions(query=True, maxTime=True) | |
#Time range | |
timeRange = mc.intFieldGrp(numberOfFields=2, value1=minTime, value2=maxTime) | |
mc.button(label='Snap and Fix', command=partial(snapFix)) | |
mc.text(label='Handle manually:', align='left') | |
floatKeys = mc.optionMenuGrp(label='Select key:') | |
mc.button(label='Update list', command=partial(list_nonInt)) | |
mc.button(label='Snap', command=partial(snap_keys)) | |
mc.button(label='Delete selected key', command=partial(delete_selectedKey)) | |
mc.button(label='Move key to PREVIOUS whole number', command=partial(move_key_back)) | |
mc.button(label='Move key to NEXT whole number', command=partial(move_key_fwd)) | |
mc.button(label='Go to selected key', command=partial(go_to_selectedKey)) | |
mc.showWindow('snapToolsUI') |