diff --git a/2023/icons/ngSkinTools2.ico b/2023/icons/ngSkinTools2.ico deleted file mode 100644 index caf8aef..0000000 Binary files a/2023/icons/ngSkinTools2.ico and /dev/null differ diff --git a/2023/icons/ngSkinTools2ShelfIcon.png b/2023/icons/ngSkinTools2ShelfIcon.png deleted file mode 100644 index 08b27e1..0000000 Binary files a/2023/icons/ngSkinTools2ShelfIcon.png and /dev/null differ diff --git a/2023/icons/offsetkeys.png b/2023/icons/offsetkeys.png new file mode 100644 index 0000000..3d30380 Binary files /dev/null and b/2023/icons/offsetkeys.png differ diff --git a/2023/scripts/animation_tools/offsetkeys.py b/2023/scripts/animation_tools/offsetkeys.py new file mode 100644 index 0000000..4b64f97 --- /dev/null +++ b/2023/scripts/animation_tools/offsetkeys.py @@ -0,0 +1,178 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Key Offset Tool +Made by Martin Chang, 2021 +Modified for multi-version Maya compatibility, 2025 +""" + +import maya.cmds as cmds + +# Global variables +bakeLayer = True +keyOffsetField = None +startOffsetField = None +endOffsetField = None + +def offsetKeys(forward, *args): + """Offset keys for selected objects""" + global keyOffsetField + + objects = cmds.ls(orderedSelection=True, long=False) + + if not keyOffsetField or not cmds.floatField(keyOffsetField, exists=True): + cmds.warning("Key Offset Tool window not found. Please open the tool first.") + return + + try: + keyOffset = cmds.floatField(keyOffsetField, query=True, value=True) + except RuntimeError: + cmds.warning("Failed to get keyframe offset value.") + return + + if not objects: + cmds.confirmDialog(title='Error', message='Select one or more objects to convert to offset keys') + return + + if forward: + for i in range(len(objects)): + try: + cmds.keyframe(objects[i], edit=True, relative=True, timeChange=keyOffset*(i+1)) + except RuntimeError as e: + cmds.warning("Failed to offset keys for {}: {}".format(objects[i], str(e))) + else: + for i in range(len(objects)): + try: + startTime = cmds.playbackOptions(q=True, minTime=True) + firstKey = cmds.findKeyframe(objects[i], which='first') + cmds.keyframe(objects[i], edit=True, relative=True, timeChange=-(firstKey-startTime)) + except RuntimeError as e: + cmds.warning("Failed to revert offset for {}: {}".format(objects[i], str(e))) + +def scaleToStart(*args): + """Scale keys to start of time range""" + global startOffsetField + + objects = cmds.ls(orderedSelection=True, long=False) + + if not startOffsetField or not cmds.floatField(startOffsetField, exists=True): + cmds.warning("Key Offset Tool window not found. Please open the tool first.") + return + + try: + startOffset = cmds.floatField(startOffsetField, query=True, value=True) + startTime = cmds.playbackOptions(q=True, minTime=True) + except RuntimeError: + cmds.warning("Failed to get offset or time range values.") + return + + if not objects: + cmds.confirmDialog(title='Error', message='Select one or more objects to scale keys to start') + return + + for i in range(len(objects)): + try: + firstKey = cmds.findKeyframe(objects[i], which='first') + if startOffset == 0: + cmds.warning("Offset from Start cannot be zero for {}".format(objects[i])) + continue + cmds.selectKey(objects[i], time=(firstKey, firstKey+startOffset)) + cmds.scaleKey(timePivot=firstKey+startOffset, timeScale=(firstKey-startTime)/startOffset + 1) + if firstKey+startOffset <= startTime: + cmds.warning('The desired keyframe to scale from is in front of the beginning of the time range, which will cause scaling issues') + except (RuntimeError, ZeroDivisionError) as e: + cmds.warning("Failed to scale keys for {}: {}".format(objects[i], str(e))) + +def scaleToEnd(*args): + """Scale keys to end of time range""" + global endOffsetField + + objects = cmds.ls(orderedSelection=True, long=False) + + if not endOffsetField or not cmds.floatField(endOffsetField, exists=True): + cmds.warning("Key Offset Tool window not found. Please open the tool first.") + return + + try: + endOffset = cmds.floatField(endOffsetField, query=True, value=True) + endTime = cmds.playbackOptions(q=True, maxTime=True) + except RuntimeError: + cmds.warning("Failed to get offset or time range values.") + return + + if not objects: + cmds.confirmDialog(title='Error', message='Select one or more objects to scale keys to end') + return + + for i in range(len(objects)): + try: + lastKey = cmds.findKeyframe(objects[i], which='last') + if endOffset == 0: + cmds.warning("Offset from End cannot be zero for {}".format(objects[i])) + continue + cmds.selectKey(objects[i], time=(lastKey, lastKey-endOffset)) + cmds.scaleKey(timePivot=lastKey-endOffset, timeScale=1-((lastKey-endTime)/endOffset)) + if lastKey-endOffset >= endTime: + cmds.warning('The desired keyframe to scale from is behind the end of the time range, which will cause scaling issues') + except (RuntimeError, ZeroDivisionError) as e: + cmds.warning("Failed to scale keys for {}: {}".format(objects[i], str(e))) + + +def show(): + """Show the Key Offset Tool window""" + global keyOffsetField, startOffsetField, endOffsetField + + # Delete existing window if it exists + if cmds.window("keyWin", exists=True): + cmds.deleteUI("keyWin") + + # Create window + physWindow = cmds.window( + "keyWin", + title="Key Offset Tool", + resizeToFitChildren=True, + sizeable=False, + minimizeButton=False, + maximizeButton=False, + menuBar=False + ) + cmds.window(physWindow, edit=True, h=100, w=100) + + cmds.columnLayout(columnAttach=('both', 0), columnWidth=190) + + cmds.text(label='Offset Keys:', height=30) + + cmds.setParent('..') + cmds.rowLayout(numberOfColumns=2) + cmds.text(label='Keyframe Offset:', width=120) + keyOffsetField = cmds.floatField(value=0.5, precision=2) + + cmds.setParent('..') + cmds.rowLayout(numberOfColumns=2, height=30) + cmds.button(label='Offset', width=75, command=lambda *args: offsetKeys(1)) + cmds.button(label='RevertOffset', width=75, command=lambda *args: offsetKeys(0)) + + cmds.setParent('..') + cmds.text(label='Scale Keys to Time Range:', height=30) + + cmds.setParent('..') + cmds.rowLayout(numberOfColumns=2) + cmds.text(label='Offset from Start:', width=120) + startOffsetField = cmds.floatField(value=10, precision=1) + + cmds.setParent('..') + cmds.button(label='Scale To Start', width=75, command=scaleToStart) + + cmds.setParent('..') + cmds.rowLayout(numberOfColumns=2) + cmds.text(label='Offset from End:', width=120) + endOffsetField = cmds.floatField(value=10, precision=1) + + cmds.setParent('..') + cmds.button(label='Scale To End', width=75, command=scaleToEnd) + + cmds.showWindow(physWindow) + + +if __name__ == '__main__': + show() \ No newline at end of file diff --git a/2023/shelves/shelf_Nexus_Animation.mel b/2023/shelves/shelf_Nexus_Animation.mel index 48c2859..981e21f 100644 --- a/2023/shelves/shelf_Nexus_Animation.mel +++ b/2023/shelves/shelf_Nexus_Animation.mel @@ -214,5 +214,39 @@ global proc shelf_Nexus_Animation () { -commandRepeatable 1 -flat 1 ; + shelfButton + -enableCommandRepeat 1 + -flexibleWidthType 3 + -flexibleWidthValue 32 + -enable 1 + -width 35 + -height 34 + -manage 1 + -visible 1 + -preventOverride 0 + -annotation "Key Offset Tool - Offset and scale keyframes for multiple objects" + -enableBackground 0 + -backgroundColor 0 0 0 + -highlightColor 0.321569 0.521569 0.65098 + -align "center" + -label "Offset Keys" + -labelOffset 0 + -rotation 0 + -flipX 0 + -flipY 0 + -useAlpha 1 + -font "plainLabelFont" + -overlayLabelColor 0.8 0.8 0.8 + -overlayLabelBackColor 0 0 0 0.5 + -image "offsetkeys.png" + -image1 "offsetkeys.png" + -style "iconOnly" + -marginWidth 0 + -marginHeight 1 + -command "import animation_tools.offsetkeys\nanimation_tools.offsetkeys.show()" + -sourceType "python" + -commandRepeatable 1 + -flat 1 + ; }