diff --git a/config/functions.json b/config/functions.json
index 1e40d2306..65e083228 100644
--- a/config/functions.json
+++ b/config/functions.json
@@ -742,6 +742,22 @@
"import mmSolver.tools.calibratecamera.tool as tool;",
"tool.update();"
]
+ },
+ "ml_convert_rotation_order": {
+ "name": "Convert Rotation Order UI",
+ "tooltip": "Change the rotation order of an object while preserving animation.",
+ "command": [
+ "import mmSolver.tools.mltools.ml_convertRotationOrder as ml_convertRotationOrder;",
+ "ml_convertRotationOrder.ui();"
+ ]
+ },
+ "ml_check_rotation_orders": {
+ "name": "Check Rotation Order for Selection",
+ "tooltip": "Checks Rotation order for all selected objects and prints result in Script Editor.",
+ "command": [
+ "import mmSolver.tools.mltools.tools;",
+ "mmSolver.tools.mltools.tools.loadTipsForAll();"
+ ]
}
}
}
diff --git a/config/menu.json b/config/menu.json
index 4b8f9f492..b357f65b5 100644
--- a/config/menu.json
+++ b/config/menu.json
@@ -73,6 +73,9 @@
"general_tools/---Controllers",
"general_tools/create_controller2",
"general_tools/remove_controller2",
+ "general_tools/---ML Tools",
+ "general_tools/ml_convert_rotation_order",
+ "general_tools/ml_check_rotation_orders",
"general_tools/---Settings & Preferences",
"general_tools/user_preferences_window",
"file_io_tools/---Input Output",
diff --git a/config/shelf.json b/config/shelf.json
index db57b4682..6e96a4763 100644
--- a/config/shelf.json
+++ b/config/shelf.json
@@ -74,6 +74,9 @@
"general_tools/gen_tools_popup/---Controllers",
"general_tools/gen_tools_popup/create_controller2",
"general_tools/gen_tools_popup/remove_controller2",
+ "general_tools/gen_tools_popup/---ML Tools",
+ "general_tools/gen_tools_popup/ml_convert_rotation_order",
+ "general_tools/gen_tools_popup/ml_check_rotation_orders",
"general_tools/gen_tools_popup/---Settings & Preferences",
"general_tools/gen_tools_popup/user_preferences_window",
"file_io_tools/file_io_popup/---Input Output",
diff --git a/config/shelf_minimal.json b/config/shelf_minimal.json
index 31a0403f5..91463ff3b 100644
--- a/config/shelf_minimal.json
+++ b/config/shelf_minimal.json
@@ -71,6 +71,9 @@
"general_tools/gen_tools_popup/---Controllers",
"general_tools/gen_tools_popup/create_controller2",
"general_tools/gen_tools_popup/remove_controller2",
+ "general_tools/gen_tools_popup/---ML Tools",
+ "general_tools/gen_tools_popup/ml_convert_rotation_order",
+ "general_tools/gen_tools_popup/ml_check_rotation_orders",
"general_tools/gen_tools_popup/---Settings & Preferences",
"general_tools/gen_tools_popup/user_preferences_window",
"file_io_tools/file_io_popup/---Input Output",
diff --git a/python/mmSolver/tools/mltools/__init__.py b/python/mmSolver/tools/mltools/__init__.py
new file mode 100644
index 000000000..d51fc620a
--- /dev/null
+++ b/python/mmSolver/tools/mltools/__init__.py
@@ -0,0 +1,27 @@
+# Copyright (C) 2019, 2021 David Cattermole.
+#
+# This file is part of mmSolver.
+#
+# mmSolver is free software: you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# mmSolver is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with mmSolver. If not, see .
+#
+"""
+These are direct ports for Morgan Loomis's Animation tools so that they
+can be utilized within mmSolver.
+
+Morgan Loomis's tools are under the MIT License making them compatible
+with mmSolver's GNU Lesser General Public License.
+
+The original files can be found here:
+http://morganloomis.com/
+"""
diff --git a/python/mmSolver/tools/mltools/constant.py b/python/mmSolver/tools/mltools/constant.py
new file mode 100644
index 000000000..d9f215a8c
--- /dev/null
+++ b/python/mmSolver/tools/mltools/constant.py
@@ -0,0 +1 @@
+ROTATE_ORDERS = ['xyz', 'yzx','zxy','xzy','yxz','zyx']
\ No newline at end of file
diff --git a/python/mmSolver/tools/mltools/ml_convertRotationOrder.py b/python/mmSolver/tools/mltools/ml_convertRotationOrder.py
new file mode 100644
index 000000000..d33a5aaf8
--- /dev/null
+++ b/python/mmSolver/tools/mltools/ml_convertRotationOrder.py
@@ -0,0 +1,350 @@
+# -= ml_convertRotationOrder.py =-
+# __ by Morgan Loomis
+# ____ ___ / / http://morganloomis.com
+# / __ `__ \/ / Revision 5
+# / / / / / / / 2018-02-17
+# /_/ /_/ /_/_/ _________
+# /_________/
+#
+# ______________
+# - -/__ License __/- - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+#
+# Copyright 2018 Morgan Loomis
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy of
+# this software and associated documentation files (the "Software"), to deal in
+# the Software without restriction, including without limitation the rights to use,
+# copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
+# Software, and to permit persons to whom the Software is furnished to do so,
+# subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+# ___________________
+# - -/__ Installation __/- - - - - - - - - - - - - - - - - - - - - - - - - -
+#
+# Copy this file into your maya scripts directory, for example:
+# C:/Documents and Settings/user/My Documents/maya/scripts/ml_convertRotationOrder.py
+#
+# Run the tool in a python shell or shelf button by importing the module,
+# and then calling the primary function:
+#
+# import ml_convertRotationOrder
+# ml_convertRotationOrder.ui()
+#
+#
+# __________________
+# - -/__ Description __/- - - - - - - - - - - - - - - - - - - - - - - - - - -
+#
+# Change the rotation order of an object while preserving animation. When you
+# want to change the rotation order of a control after you've already animated, or
+# don't want to alter the pose of an object.
+#
+# ____________
+# - -/__ Usage __/- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+#
+# Run the UI. Select the objects with rotation orders you want to change, and
+# press the button for the desired rotation order.
+#
+#
+# ___________________
+# - -/__ Requirements __/- - - - - - - - - - - - - - - - - - - - - - - - - -
+#
+# This script requires the ml_utilities module, which can be downloaded here:
+# https://raw.githubusercontent.com/morganloomis/ml_tools/master/ml_utilities.py
+#
+# __________
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /_ Enjoy! _/- - -
+
+__author__ = 'Morgan Loomis'
+__license__ = 'MIT'
+__revision__ = 5
+__category__ = 'animation'
+
+
+import maya.cmds as mc
+from maya import OpenMaya
+
+try:
+ import ml_utilities as utl
+ utl.upToDateCheck(32)
+except ImportError:
+ result = mc.confirmDialog( title='Module Not Found',
+ message='This tool requires the ml_utilities module. Once downloaded you will need to restart Maya.',
+ button=['Download Module','Cancel'],
+ defaultButton='Cancel', cancelButton='Cancel', dismissString='Cancel' )
+
+ if result == 'Download Module':
+ mc.showHelp('http://morganloomis.com/tool/ml_utilities/',absolute=True)
+
+ROTATE_ORDERS = ['xyz', 'yzx','zxy','xzy','yxz','zyx']
+_BUTTON = dict()
+
+def ui():
+ '''
+ User interface for convert rotation order
+ '''
+
+ with utl.MlUi('ml_convertRotationOrder', 'Convert Rotation Order', width=400, height=140, info='''Select objects to convert and press button for desired rotation order.
+Use the "Get Tips" button to see suggestions for a single object on the current frame.''') as win:
+
+
+ mc.button(label='Get tips for selection', command=loadTips, annotation='')
+ mc.scrollField('ml_convertRotationOrder_nodeInfo_scrollField', editable=False, wordWrap=True, height=60)
+
+ mc.rowColumnLayout(numberOfColumns=2, columnWidth=[(1,100), (2,400)], columnAttach=[2,'both',1])
+ for each in ROTATE_ORDERS:
+ _BUTTON[each] = win.buttonWithPopup(label=each, command=globals()[each], annotation='Convert selected object rotate order to '+each+'.', shelfLabel=each)
+ mc.textField('ml_convertRotationOrder_'+each+'_textField', editable=False)
+
+ resetTips()
+
+
+
+def loadTips(*args):
+
+ sel = mc.ls(sl=True)
+
+ if not len(sel) == 1:
+ OpenMaya.MGlobal.displayWarning('Please select a single object.')
+ return
+
+ resetTips()
+
+ ro = ROTATE_ORDERS[mc.getAttr(sel[0]+'.rotateOrder')]
+
+ nodeName = mc.ls(sl=True, shortNames=True)[0]
+
+ infoText = 'This object is '
+ tol = gimbalTolerence(sel[0])
+ if tol < 0.1:
+ infoText += 'not currently gimballing'
+ else:
+ if tol < 0.5:
+ infoText += 'only '
+ infoText += str(int(tol*100))
+ infoText+='% gimballed'
+
+
+ #test all rotation orders and find the lowest value
+ rotOrderTests = testAllRotateOrdersForGimbal(sel[0])
+ lowest = sorted(rotOrderTests)[0]
+ #find the lower of the two worldspace options
+ lowestWS = 1
+ for each in rotOrderTests[2:4]:
+ if each < lowestWS:
+ lowestWS = each
+
+ #determine if it's a worldspace control
+ ws = isWorldSpaceControl(sel[0])
+ if ws:
+ infoText += ", and it looks like it's a worldspace control."
+ else:
+ infoText+='.'
+
+ for t, r in zip(rotOrderTests, ROTATE_ORDERS):
+ if r == ro:
+ continue
+
+ text = str(int(t*100)) + '% Gimballed. '
+
+ if ws:
+ if r.endswith('y') and t == lowestWS: #lowest worldspace option is reccomended
+ text += '<-- [RECOMMENDED]'
+ elif lowest __revision__:
+ if prompt and mc.optionVar(query='ml_utilities_revision') < revision:
+ result = mc.confirmDialog( title='Module Out of Date',
+ message='Your version of ml_utilities may be out of date for this tool. Without the latest file you may encounter errors.',
+ button=['Download Latest Revision','Ignore', "Don't Ask Again"],
+ defaultButton='Download Latest Revision', cancelButton='Ignore', dismissString='Ignore' )
+
+ if result == 'Download Latest Revision':
+ mc.showHelp(GITHUB_ROOT_URL+'ml_utilities.py', absolute=True)
+ elif result == "Don't Ask Again":
+ mc.optionVar(intValue=('ml_utilities_revision', revision))
+ return False
+ return True
+
+
+def castToTime(time):
+ '''
+ Maya's keyframe commands are finnicky about how lists of times or indicies are formatted.
+ '''
+ if isinstance(time, (list, tuple)):
+ return [(x,) for x in time]
+ return (time,)
+
+
+def constrain(source, destination, translate=True, rotate=True, scale=False, maintainOffset=False):
+ '''
+ Constrain two objects, even if they have some locked attributes.
+ '''
+
+ transAttr = None
+ rotAttr = None
+ scaleAttr = None
+
+ if translate:
+ transAttr = mc.listAttr(destination, keyable=True, unlocked=True, string='translate*')
+ if rotate:
+ rotAttr = mc.listAttr(destination, keyable=True, unlocked=True, string='rotate*')
+ if scale:
+ scaleAttr = mc.listAttr(destination, keyable=True, unlocked=True, string='scale*')
+
+ rotSkip = list()
+ transSkip = list()
+
+ for axis in ['x','y','z']:
+ if transAttr and not 'translate'+axis.upper() in transAttr:
+ transSkip.append(axis)
+ if rotAttr and not 'rotate'+axis.upper() in rotAttr:
+ rotSkip.append(axis)
+
+ if not transSkip:
+ transSkip = 'none'
+ if not rotSkip:
+ rotSkip = 'none'
+
+ constraints = list()
+ if rotAttr and transAttr and rotSkip == 'none' and transSkip == 'none':
+ constraints.append(mc.parentConstraint(source, destination, maintainOffset=maintainOffset))
+ else:
+ if transAttr:
+ constraints.append(mc.pointConstraint(source, destination, skip=transSkip, maintainOffset=maintainOffset))
+ if rotAttr:
+ constraints.append(mc.orientConstraint(source, destination, skip=rotSkip, maintainOffset=maintainOffset))
+
+ return constraints
+
+
+def createAnimLayer(nodes=None, name=None, namePrefix='', override=True):
+ '''
+ Create an animation layer, add nodes, and select it.
+ '''
+
+ #if there's no layer name, generate one
+ if not name:
+ if namePrefix:
+ namePrefix+='_'
+ if nodes:
+ shortNodes = mc.ls(nodes, shortNames=True)
+ shortNodes = [x.rpartition(':')[-1] for x in shortNodes]
+ #if there's just one node, use it's name minus the namespace
+ if len(shortNodes) == 1:
+ name = namePrefix+shortNodes[0]
+ else:
+ #try to find the longest common substring
+ commonString = longestCommonSubstring(shortNodes)
+ if commonString:
+ name = commonString
+ elif ':' in nodes[0]:
+ #otherwise use the namespace if it has one
+ name = nodes[0].rpartition(':')[-1]
+ if not name:
+ if not namePrefix:
+ namePrefix = 'ml_'
+ name = namePrefix+'animLayer'
+
+ layer = mc.animLayer(name, override=override)
+
+ #add the nodes to the layer
+ if nodes:
+ sel = mc.ls(sl=True)
+ mc.select(nodes)
+ mc.animLayer(layer, edit=True, addSelectedObjects=True)
+ if sel:
+ mc.select(sel)
+ else:
+ mc.select(clear=True)
+
+ #select the layer
+ selectAnimLayer(layer)
+ return layer
+
+
+def selectAnimLayer(animLayer=None):
+ '''
+ Select only the specified animation layer
+ '''
+ #deselect all layers
+ for each in mc.ls(type='animLayer'):
+ mc.animLayer(each, edit=True, selected=False, preferred=False)
+ if animLayer:
+ mc.animLayer(animLayer, edit=True, selected=True, preferred=True)
+
+
+def getSelectedAnimLayers():
+ '''
+ Return the names of the layers which are selected
+ '''
+ layers = list()
+ for each in mc.ls(type='animLayer'):
+ if mc.animLayer(each, query=True, selected=True):
+ layers.append(each)
+ return layers
+
+
+def createHotkey(command, name, description='', python=True):
+ '''
+ Open up the hotkey editor to create a hotkey from the specified command
+ '''
+
+ if MAYA_VERSION > 2015:
+ print("Creating hotkeys currently doesn't work in the new hotkey editor.")
+ print("Here's the command, you'll have to make the hotkey yourself (sorry):")
+ print(command)
+ OpenMaya.MGlobal.displayWarning("Couldn't create hotkey, please see script editor for details...")
+ return
+
+ mm.eval('hotkeyEditor')
+ mc.textScrollList('HotkeyEditorCategoryTextScrollList', edit=True, selectItem='User')
+ mm.eval('hotkeyEditorCategoryTextScrollListSelect')
+ mm.eval('hotkeyEditorCreateCommand')
+
+ mc.textField('HotkeyEditorNameField', edit=True, text=name)
+ mc.textField('HotkeyEditorDescriptionField', edit=True, text=description)
+
+ if python:
+ if MAYA_VERSION < 2013:
+ command = 'python("'+command+'");'
+ else: #2013 or above
+ mc.radioButtonGrp('HotkeyEditorLanguageRadioGrp', edit=True, select=2)
+
+ mc.scrollField('HotkeyEditorCommandField', edit=True, text=command)
+
+
+def createShelfButton(command, label='', name=None, description='',
+ image=None, #the default image is a circle
+ labelColor=(1, 0.5, 0),
+ labelBackgroundColor=(0, 0, 0, 0.5),
+ backgroundColor=None
+ ):
+ '''
+ Create a shelf button for the command on the current shelf
+ '''
+ #some good default icons:
+ #menuIconConstraints - !
+ #render_useBackground - circle
+ #render_volumeShader - black dot
+ #menuIconShow - eye
+
+ gShelfTopLevel = mm.eval('$temp=$gShelfTopLevel')
+ if not mc.tabLayout(gShelfTopLevel, exists=True):
+ OpenMaya.MGlobal.displayWarning('Shelf not visible.')
+ return
+
+ if not name:
+ name = label
+
+ if not image:
+ image = getIcon(name)
+ if not image:
+ image = 'render_useBackground'
+
+ shelfTab = mc.shelfTabLayout(gShelfTopLevel, query=True, selectTab=True)
+ shelfTab = gShelfTopLevel+'|'+shelfTab
+
+ #add additional args depending on what version of maya we're in
+ kwargs = {}
+ if MAYA_VERSION >= 2009:
+ kwargs['commandRepeatable'] = True
+ if MAYA_VERSION >= 2011:
+ kwargs['overlayLabelColor'] = labelColor
+ kwargs['overlayLabelBackColor'] = labelBackgroundColor
+ if backgroundColor:
+ kwargs['enableBackground'] = bool(backgroundColor)
+ kwargs['backgroundColor'] = backgroundColor
+
+ return mc.shelfButton(parent=shelfTab, label=name, command=command,
+ imageOverlayLabel=label, image=image, annotation=description,
+ width=32, height=32, align='center', **kwargs)
+
+
+def deselectChannels():
+ '''
+ Deselect selected channels in the channelBox
+ by clearing selection and then re-selecting
+ '''
+
+ if not getSelectedChannels():
+ return
+ sel = mc.ls(sl=True)
+ mc.select(clear=True)
+ mc.evalDeferred(partial(mc.select,sel))
+
+
+def formLayoutGrid(form, controls, offset=1):
+ '''
+ Controls should be a list of lists, and this will arrange them in a grid
+ '''
+
+ kwargs = {'edit':True, 'attachPosition':[]}
+ rowInc = 100/len(controls)
+ colInc = 100/len(controls[0])
+ position = {'left':0,'right':100,'top':0,'bottom':100}
+
+ for r,row in enumerate(controls):
+ position['top'] = r*rowInc
+ position['bottom'] = (r+1)*rowInc
+ for c,ctrl in enumerate(row):
+ position['left'] = c*colInc
+ position['right'] = (c+1)*colInc
+ for k in list(position.keys()):
+ kwargs['attachPosition'].append((ctrl, k, offset, position[k]))
+
+ mc.formLayout(form, **kwargs)
+
+
+def frameRange(start=None, end=None):
+ '''
+ Returns the frame range based on the highlighted timeslider,
+ or otherwise the playback range.
+ '''
+
+ if not start and not end:
+ gPlayBackSlider = mm.eval('$temp=$gPlayBackSlider')
+ if mc.timeControl(gPlayBackSlider, query=True, rangeVisible=True):
+ frameRange = mc.timeControl(gPlayBackSlider, query=True, rangeArray=True)
+ start = frameRange[0]
+ end = frameRange[1]-1
+ else:
+ start = mc.playbackOptions(query=True, min=True)
+ end = mc.playbackOptions(query=True, max=True)
+
+ return start,end
+
+
+def getChannelFromAnimCurve(curve, plugs=True):
+ '''
+ Finding the channel associated with a curve has gotten really complicated since animation layers.
+ This is a recursive function which walks connections from a curve until an animated channel is found.
+ '''
+
+
+ #we need to save the attribute for later.
+ attr = ''
+ if '.' in curve:
+ curve, attr = curve.split('.')
+
+ nodeType = mc.nodeType(curve)
+ if nodeType.startswith('animCurveT') or nodeType.startswith('animBlendNode'):
+ source = mc.listConnections(curve+'.output', source=False, plugs=plugs)
+ if not source and nodeType=='animBlendNodeAdditiveRotation':
+ #if we haven't found a connection from .output, then it may be a node that uses outputX, outputY, etc.
+ #get the proper attribute by using the last letter of the input attribute, which should be X, Y, etc.
+ #if we're not returning plugs, then we wont have an attr suffix to use, so just use X.
+ attrSuffix = 'X'
+ if plugs:
+ attrSuffix = attr[-1]
+
+ source = mc.listConnections(curve+'.output'+attrSuffix, source=False, plugs=plugs)
+ if source:
+ nodeType = mc.nodeType(source[0])
+ if nodeType.startswith('animCurveT') or nodeType.startswith('animBlendNode'):
+ return getChannelFromAnimCurve(source[0], plugs=plugs)
+ return source[0]
+
+
+def getCurrentCamera():
+ '''
+ Returns the camera that you're currently looking through.
+ If the current highlighted panel isn't a modelPanel,
+ '''
+
+ panel = mc.getPanel(withFocus=True)
+
+ if mc.getPanel(typeOf=panel) != 'modelPanel':
+ #just get the first visible model panel we find, hopefully the correct one.
+ for p in mc.getPanel(visiblePanels=True):
+ if mc.getPanel(typeOf=p) == 'modelPanel':
+ panel = p
+ mc.setFocus(panel)
+ break
+
+ if mc.getPanel(typeOf=panel) != 'modelPanel':
+ OpenMaya.MGlobal.displayWarning('Please highlight a camera viewport.')
+ return False
+
+ camShape = mc.modelEditor(panel, query=True, camera=True)
+ if not camShape:
+ return False
+
+ camNodeType = mc.nodeType(camShape)
+ if mc.nodeType(camShape) == 'transform':
+ return camShape
+ elif mc.nodeType(camShape) in ['camera','stereoRigCamera']:
+ return mc.listRelatives(camShape, parent=True, path=True)[0]
+
+
+def getFrameRate():
+ '''
+ Return an int of the current frame rate
+ '''
+ currentUnit = mc.currentUnit(query=True, time=True)
+ if currentUnit == 'film':
+ return 24
+ if currentUnit == 'show':
+ return 48
+ if currentUnit == 'pal':
+ return 25
+ if currentUnit == 'ntsc':
+ return 30
+ if currentUnit == 'palf':
+ return 50
+ if currentUnit == 'ntscf':
+ return 60
+ if 'fps' in currentUnit:
+ return int(currentUnit.substitute('fps',''))
+
+ return 1
+
+
+def getFrameRateInSeconds():
+
+ return 1.0/getFrameRate()
+
+
+def getDistanceInMeters():
+
+ unit = mc.currentUnit(query=True, linear=True)
+
+ if unit == 'mm':
+ return 1000
+ elif unit == 'cm':
+ return 100
+ elif unit == 'km':
+ return 0.001
+ elif unit == 'in':
+ return 39.3701
+ elif unit == 'ft':
+ return 3.28084
+ elif unit == 'yd':
+ return 1.09361
+ elif unit == 'mi':
+ return 0.000621371
+
+ return 1
+
+
+def getHoldTangentType():
+ '''
+ Returns the best in and out tangent type for creating a hold with the current tangent settings.
+ '''
+ try:
+ tangentType = mc.keyTangent(query=True, g=True, ott=True)[0]
+ except:
+ return 'auto','auto'
+ if tangentType=='linear':
+ return 'linear','linear'
+ if tangentType=='step':
+ return 'linear','step'
+ if tangentType == 'plateau' or tangentType == 'spline' or MAYA_VERSION < 2012:
+ return 'plateau','plateau'
+ return 'auto','auto'
+
+
+def getIcon(name):
+ '''
+ Check if an icon name exists, and return with proper extension.
+ Otherwise return standard warning icon.
+ '''
+
+ ext = '.png'
+ if MAYA_VERSION < 2011:
+ ext = '.xpm'
+
+ if not name.endswith('.png') and not name.endswith('.xpm'):
+ name+=ext
+
+ for each in os.environ['XBMLANGPATH'].split(os.pathsep):
+ #on some linux systems each path ends with %B, for some reason
+ iconPath = os.path.abspath(each.replace('%B',''))
+ iconPath = os.path.join(iconPath,name)
+ if os.path.exists(iconPath):
+ return name
+
+ return None
+
+
+def getIconPath():
+ '''
+ Find the icon path
+ '''
+
+ appDir = os.environ['MAYA_APP_DIR']
+ for each in os.environ['XBMLANGPATH'].split(os.pathsep):
+ #on some linux systems each path ends with %B, for some reason
+ iconPath = each.replace('%B','')
+ if iconPath.startswith(appDir):
+ iconPath = os.path.abspath(iconPath)
+ if os.path.exists(iconPath):
+ return iconPath
+
+
+def getModelPanel():
+ '''Return the active or first visible model panel.'''
+
+ panel = mc.getPanel(withFocus=True)
+
+ if mc.getPanel(typeOf=panel) != 'modelPanel':
+ #just get the first visible model panel we find, hopefully the correct one.
+ panels = getModelPanels()
+ if panels:
+ panel = panels[0]
+ mc.setFocus(panel)
+
+ if mc.getPanel(typeOf=panel) != 'modelPanel':
+ OpenMaya.MGlobal.displayWarning('Please highlight a camera viewport.')
+ return None
+ return panel
+
+
+def getModelPanels():
+ '''Return all the model panels visible so you can operate on them.'''
+ panels = []
+ for p in mc.getPanel(visiblePanels=True):
+ if mc.getPanel(typeOf=p) == 'modelPanel':
+ panels.append(p)
+ return panels
+
+
+def getNamespace(node):
+ '''Returns the namespace of a node with simple string parsing.'''
+
+ if not ':' in node:
+ return ''
+ return node.rsplit('|',1)[-1].rsplit(':',1)[0] + ':'
+
+
+def getNucleusHistory(node):
+
+ history = mc.listHistory(node, levels=0)
+ if history:
+ dynamics = mc.ls(history, type='hairSystem')
+ if dynamics:
+ nucleus = mc.listConnections(dynamics[0]+'.startFrame', source=True, destination=False, type='nucleus')
+ if nucleus:
+ return nucleus[0]
+ return None
+
+
+def getRoots(nodes):
+
+ objs = mc.ls(nodes, int=True)
+ tops = []
+ namespaces = []
+ parent = None
+ for obj in objs:
+ namespace = getNamespace(obj)
+ if namespace in namespaces:
+ #we've already done this one
+ continue
+ parent = mc.listRelatives(obj, parent=True, pa=True)
+ top = obj
+ if not namespace:
+ while parent:
+ top = parent[0]
+ parent = mc.listRelatives(top, parent=True, pa=True)
+
+ tops.append(top)
+
+ else:
+ namespaces.append(namespace)
+ while parent and parent[0].rsplit('|',1)[-1].startswith(namespace):
+ top = parent[0]
+ parent = mc.listRelatives(top, parent=True, pa=True)
+
+ tops.append(top)
+ return tops
+
+
+def getSelectedChannels():
+ '''
+ Return channels that are selected in the channelbox
+ '''
+
+ if not mc.ls(sl=True):
+ return
+ gChannelBoxName = mm.eval('$temp=$gChannelBoxName')
+ sma = mc.channelBox(gChannelBoxName, query=True, sma=True)
+ ssa = mc.channelBox(gChannelBoxName, query=True, ssa=True)
+ sha = mc.channelBox(gChannelBoxName, query=True, sha=True)
+
+ channels = list()
+ if sma:
+ channels.extend(sma)
+ if ssa:
+ channels.extend(ssa)
+ if sha:
+ channels.extend(sha)
+
+ return channels
+
+
+def getSkinCluster(mesh):
+ '''
+ Return the first skinCluster affecting this mesh.
+ '''
+
+ if mc.nodeType(mesh) in ('mesh','nurbsSurface','nurbsCurve'):
+ shapes = [mesh]
+ else:
+ shapes = mc.listRelatives(mesh, shapes=True, path=True)
+
+ for shape in shapes:
+ history = mc.listHistory(shape, groupLevels=True, pruneDagObjects=True)
+ if not history:
+ continue
+ skins = mc.ls(history, type='skinCluster')
+ if skins:
+ return skins[0]
+ return None
+
+
+def listAnimCurves(objOrAttrs):
+ '''
+ This lists connections to all types of animNodes
+ '''
+
+ animNodes = list()
+
+ tl = mc.listConnections(objOrAttr, s=True, d=False, type='animCurveTL')
+ ta = mc.listConnections(objOrAttr, s=True, d=False, type='animCurveTA')
+ tu = mc.listConnections(objOrAttr, s=True, d=False, type='animCurveTU')
+
+ if tl:
+ animNodes.extend(tl)
+ if ta:
+ animNodes.extend(ta)
+ if tu:
+ animNodes.extend(tu)
+
+ return animNodes
+
+
+def longestCommonSubstring(data):
+ '''
+ Returns the longest string that is present in the list of strings.
+ '''
+ substr = ''
+ if len(data) > 1 and len(data[0]) > 0:
+ for i in range(len(data[0])):
+ for j in range(len(data[0])-i+1):
+ if j > len(substr):
+ find = data[0][i:i+j]
+ if len(data) < 1 and len(find) < 1:
+ continue
+ found = True
+ for k in range(len(data)):
+ if find not in data[k]:
+ found = False
+ if found:
+ substr = data[0][i:i+j]
+ return substr
+
+
+def matchBake(source=None, destination=None, bakeOnOnes=False, maintainOffset=False, preserveTangentWeight=True, translate=True, rotate=True, start=None, end=None):
+
+ if not source and not destination:
+ sel = mc.ls(sl=True)
+ if len(sel) != 2:
+ OpenMaya.MGlobal.displayWarning('Select exactly 2 objects')
+ return
+ source = [sel[0]]
+ destination = [sel[1]]
+
+ #save for reset:
+ resetTime = mc.currentTime(query=True)
+
+ #frame range
+ if start == None or end == None:
+ start, end = frameRange()
+
+ attributes = list()
+ if translate:
+ attributes.extend(['translateX','translateY','translateZ'])
+ if rotate:
+ attributes.extend(['rotateX','rotateY','rotateZ'])
+
+ if not attributes:
+ OpenMaya.MGlobal.displayWarning('No attributes to bake!')
+ return
+
+ duplicates = {}
+ keytimes = {}
+ constraint = list()
+ itt = {}
+ ott = {}
+ weighted = {}
+ itw = {}
+ otw = {}
+ #initialize allKeyTimes with start and end frames, since they may not be keyed
+ allKeyTimes = [start,end]
+ for s,d in zip(source,destination):
+
+ #duplicate the destination
+ dup = mc.duplicate(d, name='temp#', parentOnly=True)[0]
+ for a in attributes:
+ mc.setAttr(dup+'.'+a, lock=False, keyable=True)
+
+ constraint.append(mc.parentConstraint(s, dup, maintainOffset=maintainOffset))
+
+ #cut keys on destination
+ mc.cutKey(d, attribute=attributes, time=(start,end))
+
+ #set up our data dictionaries
+ duplicates[d] = dup
+ keytimes[d] = {}
+ itt[d] = {}
+ ott[d] = {}
+ weighted[d] = {}
+ itw[d] = {}
+ otw[d] = {}
+
+ #if we're baking on ones, we don't need keytimes
+ if not bakeOnOnes:
+ for a in attributes:
+ currKeytimes = mc.keyframe(s, attribute=a, time=(start,end), query=True, timeChange=True)
+ if not currKeytimes:
+ continue
+
+ keytimes[d][a] = currKeytimes
+ allKeyTimes.extend(currKeytimes)
+
+ #errors in maya 2016.5?
+ try:
+ itt[d][a] = mc.keyTangent(s, attribute=a, time=(start,end), query=True, inTangentType=True)
+ ott[d][a] = mc.keyTangent(s, attribute=a, time=(start,end), query=True, outTangentType=True)
+ except RuntimeError as err:
+ itt[d][a] = ['auto'] * len(currKeytimes)
+ ott[d][a] = ['auto'] * len(currKeytimes)
+
+ if preserveTangentWeight and mc.keyTangent(s, attribute=a, query=True, weightedTangents=True)[0]:
+ weighted[d][a] = True
+ itw[d][a] = mc.keyTangent(s, attribute=a, time=(start,end), query=True, inWeight=True)
+ otw[d][a] = mc.keyTangent(s, attribute=a, time=(start,end), query=True, outWeight=True)
+
+ #change fixed tangents to spline, because we can't set fixed tangents
+ for i, each in enumerate(itt[d][a]):
+ if each == 'fixed':
+ itt[d][a][i] = 'spline'
+
+ for i, each in enumerate(ott[d][a]):
+ if each == 'fixed':
+ ott[d][a][i] = 'spline'
+
+ #add the start and end frames and tangents if they're not keyed
+ if not start in keytimes[d][a]:
+ keytimes[d][a].insert(0,start)
+ itt[d][a].insert(0,'spline')
+ ott[d][a].insert(0,'spline')
+ if a in weighted[d]:
+ itw[d][a].insert(0, 1.0)
+ otw[d][a].insert(0, 1.0)
+ if not end in keytimes[d][a]:
+ keytimes[d][a].append(end)
+ itt[d][a].append('spline')
+ ott[d][a].append('spline')
+ if a in weighted[d]:
+ itw[d][a].append(1.0)
+ otw[d][a].append(1.0)
+
+ #reverse these, because we want to pop but start from the beginning
+ itt[d][a].reverse()
+ ott[d][a].reverse()
+ if a in weighted[d]:
+ itw[d][a].reverse()
+ otw[d][a].reverse()
+
+
+ if bakeOnOnes:
+ allKeyTimes = list(range(int(start), int(end)+1))
+ else:
+ allKeyTimes = list(set(allKeyTimes))
+ allKeyTimes.sort()
+
+ with UndoChunk():
+ #if
+ with IsolateViews():
+ for frame in allKeyTimes:
+ #cycle through all the frames
+ mc.currentTime(frame, edit=True)
+ for d in destination:
+ weightedSet = False
+ for a in attributes:
+ try:
+ v = mc.getAttr(duplicates[d]+'.'+a)
+ if bakeOnOnes:
+ mc.setKeyframe(d, attribute=a, time=frame,
+ value=v,
+ itt='spline',
+ ott='spline')
+ mc.setAttr(d+'.'+a, v)
+ elif a in keytimes[d] and frame in keytimes[d][a]:
+ #tangent types line up with keytimes
+ mc.setKeyframe(d, attribute=a, time=frame,
+ value=v,
+ itt=itt[d][a].pop(),
+ ott=ott[d][a].pop()
+ )
+ mc.setAttr(d+'.'+a, v)
+
+ except:
+ pass
+
+ #this was breaking the tangents inside the other loop, so run it after.
+ if not bakeOnOnes and preserveTangentWeight:
+ for d in destination:
+ for a in attributes:
+ if a in weighted[d]:
+ mc.keyTangent(d, attribute=a, edit=True, weightedTangents=True)
+ for frame in keytimes[d][a]:
+ mc.keyTangent(d, attribute=a, time=(frame,), edit=True, absolute=True, inWeight=itw[d][a].pop(), outWeight=otw[d][a].pop())
+
+ #reset time and selection
+ mc.currentTime(resetTime, edit=True)
+ mc.select(destination, replace=True)
+
+ mc.delete(list(duplicates.values()))
+ if rotate:
+ mc.filterCurve(mc.listConnections(destination,type='animCurve'))
+ if bakeOnOnes:
+ mc.keyTangent(destination, attribute=attributes, itt='spline', ott='spline')
+
+def message(msg, position='midCenterTop'):
+
+ OpenMaya.MGlobal.displayWarning(msg)
+ fadeTime = min(len(msg)*100, 2000)
+ mc.inViewMessage( amg=msg, pos=position, fade=True, fadeStayTime=fadeTime, dragKill=True)
+
+
+def minimizeRotationCurves(obj):
+ '''
+ Sets rotation animation to the value closest to zero.
+ '''
+
+ rotateCurves = mc.keyframe(obj, attribute=('rotateX','rotateY', 'rotateZ'), query=True, name=True)
+
+ if not rotateCurves or len(rotateCurves) < 3:
+ return
+
+ keyTimes = mc.keyframe(rotateCurves, query=True, timeChange=True)
+ tempFrame = sorted(keyTimes)[0] - 1
+
+ #set a temp frame
+ mc.setKeyframe(rotateCurves, time=(tempFrame,), value=0)
+
+ #euler filter
+ mc.filterCurve(rotateCurves)
+
+ #delete temp key
+ mc.cutKey(rotateCurves, time=(tempFrame,))
+
+
+def renderShelfIcon(name='tmp', width=32, height=32):
+ '''
+ This renders a shelf-sized icon and hopefully places it in your icon directory
+ '''
+ imageName=name
+
+ #getCamera
+ cam = getCurrentCamera()
+
+ #save these values for resetting
+ currentRenderer = mc.getAttr('defaultRenderGlobals.currentRenderer')
+ imageFormat = mc.getAttr('defaultRenderGlobals.imageFormat')
+
+ mc.setAttr('defaultRenderGlobals.currentRenderer', 'mayaSoftware', type='string')
+
+ imageFormat = 50 #XPM
+ if MAYA_VERSION >= 2011:
+ imageFormat = 32 #PNG
+
+ mc.setAttr('defaultRenderGlobals.imageFormat', imageFormat)
+ mc.setAttr('defaultRenderGlobals.imfkey', 'xpm', type='string')
+ #here's the imageName
+ mc.setAttr('defaultRenderGlobals.imageFilePrefix', imageName, type='string')
+
+ mc.setAttr(cam+'.backgroundColor', 0.8,0.8,0.8, type='double3')
+ #need to reset this afterward
+
+ image = mc.render(cam, xresolution=width, yresolution=height)
+ base = os.path.basename(image)
+
+ #here we attempt to move the rendered icon to a more generalized icon location
+ newPath = getIconPath()
+ if newPath:
+ newPath = os.path.join(newPath, base)
+ shutil.move(image, newPath)
+ image = newPath
+
+ #reset
+ mc.setAttr('defaultRenderGlobals.currentRenderer', currentRenderer, type='string')
+ mc.setAttr('defaultRenderGlobals.imageFormat', imageFormat)
+
+ return image
+
+
+def setAnimValue(plug, value, tangentType=None):
+ '''
+ Sets key if the channel is keyed, otherwise setAttr
+ '''
+
+ if mc.keyframe(plug, query=True, name=True):
+ mc.setKeyframe(plug, value=value)
+ if tangentType:
+ time = mc.currentTime(query=True)
+ itt = tangentType
+ ott = tangentType
+ if tangentType == 'step':
+ itt = 'linear'
+ mc.keyTangent(plug, time=(time,), edit=True, itt=itt, ott=ott)
+
+ mc.setAttr(plug, value)
+
+
+class Dragger(object):
+
+ def __init__(self,
+ name = 'mlDraggerContext',
+ title = 'Dragger',
+ defaultValue=0,
+ minValue=None,
+ maxValue=None,
+ multiplier=0.01,
+ cursor='hand'
+ ):
+
+ self.multiplier = multiplier
+ self.defaultValue = defaultValue
+ self.minValue = minValue
+ self.maxValue = maxValue
+ #self.cycleCheck = mc.cycleCheck(query=True, evaluation=True)
+
+ self.draggerContext = name
+ if not mc.draggerContext(self.draggerContext, exists=True):
+ self.draggerContext = mc.draggerContext(self.draggerContext)
+
+ mc.draggerContext(self.draggerContext, edit=True,
+ pressCommand=self.press,
+ dragCommand=self.drag,
+ releaseCommand=self.release,
+ cursor=cursor,
+ drawString=title,
+ undoMode='all'
+ )
+
+
+ def press(self, *args):
+ '''
+ Be careful overwriting the press method in child classes, because of the undoInfo openChunk
+ '''
+
+ self.anchorPoint = mc.draggerContext(self.draggerContext, query=True, anchorPoint=True)
+ self.button = mc.draggerContext(self.draggerContext, query=True, button=True)
+
+ # This turns off the undo queue until we're done dragging, so we can undo it.
+ mc.undoInfo(openChunk=True)
+
+
+ def drag(self, *args):
+ '''
+ This is what is actually run during the drag, updating the coordinates and calling the
+ placeholder drag functions depending on which button is pressed.
+ '''
+
+ self.dragPoint = mc.draggerContext(self.draggerContext, query=True, dragPoint=True)
+
+ #if this doesn't work, try getmodifier
+ self.modifier = mc.draggerContext(self.draggerContext, query=True, modifier=True)
+
+ self.x = ((self.dragPoint[0] - self.anchorPoint[0]) * self.multiplier) + self.defaultValue
+ self.y = ((self.dragPoint[1] - self.anchorPoint[1]) * self.multiplier) + self.defaultValue
+
+ if self.minValue is not None and self.x < self.minValue:
+ self.x = self.minValue
+ if self.maxValue is not None and self.x > self.maxValue:
+ self.x = self.maxValue
+
+ #dragString
+ if self.modifier == 'control':
+ if self.button == 1:
+ self.dragControlLeft(*args)
+ elif self.button == 2:
+ self.dragControlMiddle(*args)
+ elif self.modifier == 'shift':
+ if self.button == 1:
+ self.dragShiftLeft(*args)
+ elif self.button == 2:
+ self.dragShiftMiddle(*args)
+ else:
+ if self.button == 1:
+ self.dragLeft()
+ elif self.button == 2:
+ self.dragMiddle()
+
+ mc.refresh()
+
+ def release(self, *args):
+ '''
+ Be careful overwriting the release method in child classes. Not closing the undo chunk leaves maya in a sorry state.
+ '''
+ # close undo chunk and turn cycle check back on
+ mc.undoInfo(closeChunk=True)
+ #mc.cycleCheck(evaluation=self.cycleCheck)
+ mm.eval('SelectTool')
+
+ def drawString(self, message):
+ '''
+ Creates a string message at the position of the pointer.
+ '''
+ mc.draggerContext(self.draggerContext, edit=True, drawString=message)
+
+ def dragLeft(self,*args):
+ '''Placeholder for potential commands. This is meant to be overridden by a child class.'''
+ pass
+
+ def dragMiddle(self,*args):
+ '''Placeholder for potential commands. This is meant to be overridden by a child class.'''
+ pass
+
+ def dragControlLeft(self,*args):
+ '''Placeholder for potential commands. This is meant to be overridden by a child class.'''
+ pass
+
+ def dragControlMiddle(self,*args):
+ '''Placeholder for potential commands. This is meant to be overridden by a child class.'''
+ pass
+
+ def dragShiftLeft(self,*args):
+ '''Placeholder for potential commands. This is meant to be overridden by a child class.'''
+ pass
+
+ def dragShiftMiddle(self,*args):
+ '''Placeholder for potential commands. This is meant to be overridden by a child class.'''
+ pass
+
+ #no drag right, because that is monopolized by the right click menu
+ #no alt drag, because that is used for the camera
+
+ def setTool(self):
+ mc.setToolTo(self.draggerContext)
+
+
+class IsolateViews():
+ '''
+ Isolates selection with nothing selected for all viewports
+ This speeds up any process that causes the viewport to refresh,
+ such as baking or changing time.
+ '''
+
+ def __enter__(self):
+
+ if MAYA_VERSION >= 2016.5:
+ if not mc.ogs(query=True, pause=True):
+ mc.ogs(pause=True)
+ else:
+ self.sel = mc.ls(sl=True)
+ self.modelPanels = mc.getPanel(type='modelPanel')
+
+ #unfortunately there's no good way to know what's been isolated, so in this case if a view is isolated, skip it.
+ self.skip = []
+ for each in self.modelPanels:
+ if mc.isolateSelect(each, query=True, state=True):
+ self.skip.append(each)
+
+ self.isolate(True)
+
+ mc.select(clear=True)
+
+ self.resetAutoKey = mc.autoKeyframe(query=True, state=True)
+ mc.autoKeyframe(state=False)
+
+
+ def __exit__(self, *args):
+
+ #reset settings
+ mc.autoKeyframe(state=self.resetAutoKey)
+
+ if MAYA_VERSION >= 2016.5:
+ if mc.ogs(query=True, pause=True):
+ mc.ogs(pause=True)
+ else:
+ if self.sel:
+ mc.select(self.sel)
+
+ self.isolate(False)
+
+
+ def isolate(self, state):
+
+ mc.select(clear=True)
+ for each in self.modelPanels:
+ if not each in self.skip:
+ mc.isolateSelect(each, state=state)
+
+
+class KeySelection(object):
+ '''
+
+ '''
+
+ def __init__(self, *args):
+
+ #if args are passed in, this has been called from and out of date script. Warn and fail.
+ if args:
+ print('')
+ print("Because of an update to ml_utilities, the tool you're trying to run is deprecated and needs to be updated as well.")
+ print("Please visit http://morganloomis.com/tools and download the latest version of this tool.")
+ OpenMaya.MGlobal.displayError('Tool out of date. See script editor for details.')
+ return
+
+ self.shortestTime = getFrameRate()/6000.0
+
+ #node variables
+ self.nodeSelection = mc.ls(sl=True)
+ self._nodes = list()
+ self._curves = list()
+ self._channels = list()
+
+ #time variables
+ self.currentTime = mc.currentTime(query=True)
+ self._time = None
+ self._timeRangeStart = None
+ self._timeRangeEnd = None
+
+ #keyframe command variables
+ self.selected = False
+
+ #other housekeeping
+ self._curvesCulled = False
+
+
+ @property
+ def curves(self):
+ '''
+ The keySelections's animation curve list.
+ '''
+
+ # if self._curves is False or None, then it has been initialized and curves haven't been found.
+ if self._curves == []:
+
+ #find anim curves connected to channels or nodes
+ for each in (self._channels, self._nodes):
+ if not each:
+ continue
+ # this will only return time based keyframes, not driven keys
+ self._curves = mc.keyframe(each, time=(':',), query=True, name=True)
+
+ if self._curves:
+ self._curvesCulled = False
+ break
+ if not self._curves:
+ self._curves = False
+
+ # need to remove curves which are unkeyable
+ # supposedly referenced keys are keyable in 2013, I'll need to test that and update
+ if self._curves and not self._curvesCulled:
+ remove = list()
+ for c in self._curves:
+ if mc.referenceQuery(c, isNodeReferenced=True):
+ remove.append(c)
+ else:
+ plug = mc.listConnections('.'.join((c,'output')), source=False, plugs=True)
+ if plug:
+ if not mc.getAttr(plug, keyable=True) and not mc.getAttr(plug, settable=True):
+ remove.append(c)
+ if remove:
+ for r in remove:
+ self._curves.remove(r)
+ self._curvesCulled = True
+
+ return self._curves
+
+
+ @property
+ def channels(self):
+ '''
+ The keySelection's channels list.
+ '''
+
+ if not self._channels:
+
+ if self._curves:
+ for c in self._curves:
+ self._channels.append(getChannelFromAnimCurve(c))
+ elif self._nodes:
+ for obj in self._nodes:
+ keyable = mc.listAttr(obj, keyable=True, unlocked=True, hasData=True, settable=True)
+ if keyable:
+ for attr in keyable:
+ self._channels.append('.'.join((obj, attr)))
+
+ return self._channels
+
+ @property
+ def nodes(self):
+ '''
+ The keySelection's node list.
+ '''
+
+ if not self._nodes:
+
+ if self._curves:
+ self._nodes = list()
+ for c in self._curves:
+ n = getChannelFromAnimCurve(c, plugs=False)
+ if not n in self._nodes:
+ self._nodes.append(n)
+ elif self._channels:
+ for each in self._channels:
+ n = each.split('.')[0]
+ if not n in self._nodes:
+ self._nodes.append(n)
+
+ return self._nodes
+
+ @property
+ def args(self):
+ '''
+ This will return channels, curves or nodes in instances where we don't care which.
+ It wont waste time converting from one to the other.
+ '''
+ if self._channels:
+ return self._channels
+ if self._curves:
+ return self._curves
+ if self._nodes:
+ return self._nodes
+ return None
+
+
+ @property
+ def time(self):
+ '''
+ The keySelection's time, formatted for maya's various keyframe command arguments.
+ '''
+ if self._time:
+ if isinstance(self._time, list):
+ return tuple(self._time)
+ elif isinstance(self._time, float) or isinstance(self._time, int):
+ return (self._time,)
+ return self._time
+ elif self._timeRangeStart and self._timeRangeEnd:
+ return (self._timeRangeStart,self._timeRangeEnd)
+ elif self._timeRangeStart:
+ return (str(self._timeRangeStart)+':',)
+ elif self._timeRangeEnd:
+ return (':'+str(self._timeRangeEnd),)
+ elif self.selected:
+ #if keys are selected, get their times
+ timeList = self.keyframe(query=True, timeChange=True)
+ return tuple(set(timeList))
+ return (':',)
+
+
+ @property
+ def times(self):
+ '''
+ This returns an expanded list of times, which is synced with the curve list.
+ '''
+ timeList = list()
+ theTime = self.time
+ for c in self.curves:
+ curveTime = tuple(mc.keyframe(c, time=(theTime,), query=True, timeChange=True))
+ if len(curveTime) == 1:
+ curveTime = (curveTime[0],)
+ timeList.append(curveTime)
+ return timeList
+
+ @property
+ def values(self):
+ valueList = list()
+ for c in self.curves:
+ curveValues = mc.keyframe(c, query=True, valueChange=True)
+ if len(curveValues) == 1:
+ curveValues = (curveValues[0],)
+ valueList.append(curveValues)
+ return valueList
+
+
+ @property
+ def initialized(self):
+ '''
+ Basically just tells if the object has been sucessfully initialized.
+ '''
+ return bool(self.args)
+
+
+ def selectedObjects(self):
+ '''
+ Initializes the keySelection object with selected objects.
+ Returns True if successful.
+ '''
+
+ if not self.nodeSelection:
+ return False
+
+ self._nodes = self.nodeSelection
+ return True
+
+
+ def selectedChannels(self):
+ '''
+ Initializes the keySelection object with selected channels.
+ Returns True if successful.
+ '''
+
+ chanBoxChan = getSelectedChannels()
+
+ if not chanBoxChan:
+ return False
+
+ #channels may be on shapes, include shapes in the list
+ nodes = self.nodeSelection
+ shapes = mc.listRelatives(self.nodeSelection, shapes=True, path=True)
+ if shapes:
+ nodes.extend(shapes)
+ nodes = list(set(nodes))
+
+ for obj in nodes:
+ for attr in chanBoxChan:
+ if mc.attributeQuery(attr, node=obj, exists=True):
+ self._channels.append('.'.join((obj,attr)))
+
+ if not self._channels:
+ return False
+
+ return True
+
+
+ def selectedLayers(self, includeLayerWeight=True):
+ '''
+ This affects keys on all layers that the node belongs to.
+ If includeLayerWeight, the keys on the layer's weight attribute will be affected also.
+ '''
+ layers = getSelectedAnimLayers()
+ curves = list()
+ for layer in layers:
+ layerCurves = mc.animLayer(layer, query=True, animCurves=True)
+ if layerCurves:
+ curves.extend(layerCurves)
+ if includeLayerWeight:
+ weightCurve = mc.keyframe(layer+'.weight', query=True, name=True)
+ if weightCurve:
+ curves.append(weightCurve[0])
+ self._curves = curves
+ #we only want to use curves, since nodes or channels wont accurately represent all layers
+ self._nodes = None
+ self._channels = None
+
+
+ def visibleInGraphEditor(self):
+ '''
+ Initializes the keySelection object with curves visibile in graph editor.
+ Returns True if successful.
+ '''
+
+
+ if not 'graphEditor1' in mc.getPanel(visiblePanels=True):
+ return False
+
+ graphVis = mc.selectionConnection('graphEditor1FromOutliner', query=True, obj=True)
+
+ if not graphVis:
+ return False
+
+ for each in graphVis:
+ try:
+ self._curves.extend(mc.keyframe(each, query=True, name=True))
+ except Exception:
+ pass
+
+
+ if not self._curves:
+ return False
+
+ return True
+
+
+ def selectedKeys(self):
+ '''
+ Initializes the keySelection object with selected keyframes.
+ Returns True if successful.
+ '''
+
+ selectedCurves = mc.keyframe(query=True, name=True, selected=True)
+
+ if not selectedCurves:
+ return False
+ self._curves = selectedCurves
+ self.selected = True
+ return True
+
+
+ def keyedChannels(self, includeShapes=False):
+ '''
+ Initializes the keySelection object with keyed channels.
+ Returns True if successful.
+ '''
+
+ if not self.nodeSelection:
+ return False
+
+ self._nodes = self.nodeSelection
+ if includeShapes:
+ shapes = mc.listRelatives(self.nodeSelection, shapes=True, path=True)
+ if shapes:
+ self._nodes.extend(shapes)
+
+ #since self.curves is a property, it is actually finding curves from self._nodes
+ if not self.curves:
+ #if we don't find curves, reset nodes and fail
+ self._nodes = None
+ return False
+
+ #reset self._nodes, otherwise they'll take priority over curves.
+ self._nodes = None
+
+ return True
+
+
+ def keyedInHierarchy(self, includeRoot=True):
+ '''
+ Initializes the keySelection object with all the animation curves in the hierarchy.
+ Returns True if successful.
+ '''
+
+ if not self.nodeSelection:
+ return False
+
+ tops = getRoots(self.nodeSelection)
+
+ if not tops:
+ #if we haven't been sucessful, we're done
+ return False
+
+ nodes = mc.listRelatives(tops, pa=True, type='transform', ad=True)
+ if not nodes:
+ nodes = list()
+
+ if includeRoot:
+ nodes.extend(tops)
+
+ if not nodes:
+ return False
+
+ #now that we've determined the hierarchy, lets find keyed nodes
+ #for node in nodes:
+ # this will only return time based keyframes, not driven keys
+ self._curves = mc.keyframe(nodes, time=(':',), query=True, name=True)
+
+ #nodes or channels can be acessed by the node or channel property
+ if not self._curves:
+ return False
+
+ return True
+
+
+ def scene(self):
+ '''
+ Initializes the keySelection object with all animation curves in the scene.
+ Returns True if successful.
+ '''
+
+ tl = mc.ls(type='animCurveTL')
+ ta = mc.ls(type='animCurveTA')
+ tu = mc.ls(type='animCurveTU')
+
+ if tl:
+ self._curves.extend(tl)
+ if ta:
+ self._curves.extend(ta)
+ if tu:
+ self._curves.extend(tu)
+
+ if not self._curves:
+ return False
+
+ return True
+
+
+ def selectedFrameRange(self):
+ '''
+ Sets the keySelection time to the selected frame range, returns false if frame range not selected.
+ '''
+
+ gPlayBackSlider = mm.eval('$temp=$gPlayBackSlider')
+ if mc.timeControl(gPlayBackSlider, query=True, rangeVisible=True):
+ self._timeRangeStart, self._timeRangeEnd = mc.timeControl(gPlayBackSlider, query=True, rangeArray=True)
+ return True
+ return False
+
+
+ def frameRange(self):
+ '''
+ Sets the keySelection time to the selected frame range, or the current frame range.
+ '''
+ #this is selected range in the time slider
+ self._timeRangeStart, self._timeRangeEnd = frameRange()
+
+
+ def toEnd(self, includeCurrent=False):
+ '''
+ Sets the keySelection time to the range from the current frame to the last frame.
+ Option to include the current frame.
+ '''
+
+ t = self.currentTime
+ if not includeCurrent:
+ t+=self.shortestTime
+ self._timeRangeStart = t
+
+
+ def fromBeginning(self, includeCurrent=False):
+ '''
+ Sets the keySelection time to the range from the first frame to the current frame.
+ Option to include the current frame.
+ '''
+
+ t = self.currentTime
+ if not includeCurrent:
+ t-=self.shortestTime
+ self._timeRangeEnd = t
+
+
+ def keyRange(self):
+ '''
+ Sets the keySelection time range to the range of keys in the keySelection.
+ '''
+
+ keyTimes = self.getSortedKeyTimes()
+
+ if not keyTimes or keyTimes[0] == keyTimes[-1]:
+ return
+
+ self._timeRangeStart = keyTimes[0]
+ self._timeRangeEnd = keyTimes[-1]
+
+
+ def currentFrame(self):
+ '''
+ Sets the keySelection time to the current frame.
+ '''
+ self._time = self.currentTime
+
+
+ def previousKey(self):
+ '''
+ Sets the keySelection time to the previous key from the current frame.
+ '''
+ self._time = self.findKeyframe(which='previous')
+
+
+ def nextKey(self):
+ '''
+ Sets the keySelection time to the next key from the current frame.
+ '''
+ self._time = self.findKeyframe(which='next')
+
+
+ def setKeyframe(self, deleteSubFrames=False, **kwargs):
+ '''
+ Wrapper for the setKeyframe command. Curve and time arguments will be provided based on
+ how this object was intitialized, otherwise usage is the same as maya's setKeyframe command.
+ Option to delete sub-frames after keying.
+ '''
+
+ if not 'time' in kwargs:
+ #still not sure about how I want to do this, but we need a discrete time.
+ #if time is a string set to current time
+ if isinstance(self.time, tuple) and (isinstance(self.time[0], str) or len(self.time)>1):
+ kwargs['time'] = mc.currentTime(query=True)
+ else:
+ kwargs['time'] = self.time
+
+ if 'insert' in kwargs and kwargs['insert'] == True:
+ #setKeyframe fails if insert option is used but there's no keyframes on the channels.
+ #key any curves with insert, then key everything again without it
+
+ if self.curves:
+ mc.setKeyframe(self.curves, **kwargs)
+ kwargs['insert'] = False
+
+ #want to try setting keys on nodes first, since certain setKeyframe arguments wont work
+ #as expected with channels
+ if self._nodes:
+ mc.setKeyframe(self.nodes, **kwargs)
+ self._curves = mc.keyframe(self.nodes, query=True, name=True)
+ else:
+ mc.setKeyframe(self.channels, **kwargs)
+ self._curves = mc.keyframe(self.channels, query=True, name=True)
+
+ #there's a new selection of curves, so reset the member variables
+ self._channels = list()
+ self._nodes = list()
+ self._time = kwargs['time']
+
+ if deleteSubFrames:
+ #remove nearby sub-frames
+ #this breaks at higher frame ranges because maya doesn't keep enough digits
+ #this value is also different for different frame rates
+ if self.currentTime % 1 == 0 and -9999 < self.currentTime < 9999:
+ #the distance that keys can be is independent of frame rate, so we have to convert based on the frame rate.
+ tol = self.shortestTime
+ self.cutKey(time=(self.currentTime+tol, self.currentTime+0.5))
+ self.cutKey(time=(self.currentTime-0.5, self.currentTime-tol))
+
+
+ def keyframe(self,**kwargs):
+ '''
+ Wrapper for the keyframe command. Curve and time arguments will be provided based on
+ how this object was intitialized, otherwise usage is the same as maya's keyframe command.
+ '''
+ if self.selected:
+ #it's important that selection test first, becuase it's called by the time property
+ kwargs['sl'] = True
+ elif not 'time' in kwargs:
+ kwargs['time'] = self.time
+
+ return mc.keyframe(self.curves, **kwargs)
+
+
+ def cutKey(self, includeSubFrames=False, **kwargs):
+ '''
+ Wrapper for the cutKey command. Curve and time arguments will be provided based on
+ how this object was intitialized, otherwise usage is the same as maya's cutKey command.
+ Option to delete sub-frames.
+ '''
+
+ if not 'includeUpperBound' in kwargs:
+ kwargs['includeUpperBound'] = False
+
+ if self.selected:
+ mc.cutKey(sl=True, **kwargs)
+ return
+
+ if not 'time' in kwargs:
+ if includeSubFrames:
+ kwargs['time'] = (round(self.time[0])-0.5, round(self.time[-1])+0.5)
+ else:
+ kwargs['time'] = self.time
+ mc.cutKey(self.curves, **kwargs)
+
+
+ def copyKey(self, **kwargs):
+ '''
+
+ '''
+
+ if not 'includeUpperBound' in kwargs:
+ kwargs['includeUpperBound'] = False
+
+ if self.selected:
+ mc.copyKey(sl=True, **kwargs)
+ return
+
+ if not 'time' in kwargs:
+ kwargs['time'] = self.time
+
+ mc.copyKey(self.args, **kwargs)
+
+
+ def pasteKey(self, option='replaceCompletely', **kwargs):
+ '''
+
+ '''
+ mc.pasteKey(self.args, option=option, **kwargs)
+
+
+ def selectKey(self,**kwargs):
+ '''
+ Wrapper for maya's selectKey command.
+ '''
+
+ if not 'time' in kwargs:
+ kwargs['time'] = self.time
+ mc.selectKey(self.curves, **kwargs)
+
+
+ def moveKey(self, frames):
+ '''
+ A wrapper for keyframe -edit -timeChange
+ '''
+
+ if not frames:
+ return
+
+ self.keyframe(edit=True, relative=True, timeChange=frames)
+
+
+ def scaleKey(self, timePivot=0, **kwargs):
+ '''
+ Wrapper for maya's scaleKey command.
+ '''
+
+ if not 'time' in kwargs:
+ kwargs['time'] = self.time
+
+ if timePivot == 'current':
+ timePivot = self.currentTime
+
+ mc.scaleKey(self.curves, timePivot=timePivot, **kwargs)
+
+
+ def tangentType(self, **kwargs):
+ '''
+ Wrapper for maya's tangentType command.
+ '''
+ if not 'time' in kwargs:
+ kwargs['time'] = self.time
+ mc.tangentType(self.curves, **kwargs)
+
+
+ def keyTangent(self, **kwargs):
+ '''
+ Wrapper for maya's keyTangent command.
+ '''
+ if not 'time' in kwargs:
+ kwargs['time'] = self.time
+ mc.keyTangent(self.curves, **kwargs)
+
+
+ def findKeyframe(self, which='next', loop=False, roundFrame=False, **kwargs):
+ '''
+ This is similar to maya's findKeyframe, but operates on the keySelection and has options
+ for rounding and looping.
+ '''
+
+ if which not in ('next','previous','first','last'):
+ return
+
+ if not roundFrame:
+ if not loop or which == 'first' or which == 'last':
+ #if there's not special options, just use default maya command for speed
+ return mc.findKeyframe(self.args, which=which, **kwargs)
+
+ keyTimes = self.getSortedKeyTimes()
+
+ #if we don't find any, we're done
+ if not keyTimes:
+ return
+
+ tolerence = 0.0
+ if roundFrame:
+ tolerence = 0.5
+
+ if which == 'previous':
+ findTime = keyTimes[-1]
+ for x in reversed(keyTimes):
+ if self.currentTime - x > tolerence:
+ findTime=x
+ break
+ elif which == 'next':
+ findTime = keyTimes[0]
+ for x in keyTimes:
+ if x - self.currentTime > tolerence:
+ findTime=x
+ break
+ elif which == 'first':
+ findTime = keyTimes[0]
+ elif which == 'last':
+ findTime = keyTimes[-1]
+
+ if roundFrame:
+ #round to nearest frame, if that option is selected
+ findTime = round(findTime)
+
+ return findTime
+
+
+ def getSortedKeyTimes(self):
+ '''
+ Returns a list of the key times in order without duplicates.
+ '''
+
+ keyTimes = self.keyframe(query=True, timeChange=True)
+ if not keyTimes:
+ return
+ return sorted(list(set(keyTimes)))
+
+
+
+class MlUi(object):
+ '''
+ Window template for consistency
+ '''
+
+ def __init__(self, name, title, width=400, height=200, info='', menu=True, module=None):
+
+ self.name = name
+ self.title = title
+ self.width = width
+ self.height = height
+ self.info = info
+ self.menu = menu
+
+ self.module = module
+ if not module or module == '__main__':
+ self.module = self.name
+
+ #look for icon
+ self.icon = getIcon(name)
+
+
+ def __enter__(self):
+ self.buildWindow()
+ return self
+
+ def __exit__(self, *args):
+ self.finish()
+
+ def buildWindow(self):
+ '''
+ Initialize the UI
+ '''
+
+ if mc.window(self.name, exists=True):
+ mc.deleteUI(self.name)
+
+ mc.window(self.name, title='ml :: '+self.title, iconName=self.title, width=self.width, height=self.height, menuBar=self.menu)
+
+
+ if self.menu:
+ self.createMenu()
+
+ self.form = mc.formLayout()
+ self.column = mc.columnLayout(adj=True)
+
+
+ mc.rowLayout( numberOfColumns=2, columnWidth2=(34, self.width-34), adjustableColumn=2,
+ columnAlign2=('right','left'),
+ columnAttach=[(1, 'both', 0), (2, 'both', 8)] )
+
+ #if we can find an icon, use that, otherwise do the text version
+ if self.icon:
+ mc.iconTextStaticLabel(style='iconOnly', image1=self.icon)
+ else:
+ mc.text(label=' _ _ |\n| | | |')
+
+ if not self.menu:
+ mc.popupMenu(button=1)
+ mc.menuItem(label='Help', command=(_showHelpCommand(TOOL_URL+self.name+'/')))
+
+ mc.text(label=self.info)
+ mc.setParent('..')
+ mc.separator(height=8, style='single', horizontal=True)
+
+
+ def finish(self):
+ '''
+ Finalize the UI
+ '''
+
+ mc.setParent(self.form)
+
+ frame = mc.frameLayout(labelVisible=False)
+ mc.helpLine()
+
+ mc.formLayout( self.form, edit=True,
+ attachForm=((self.column, 'top', 0), (self.column, 'left', 0),
+ (self.column, 'right', 0), (frame, 'left', 0),
+ (frame, 'bottom', 0), (frame, 'right', 0)),
+ attachNone=((self.column, 'bottom'), (frame, 'top')) )
+
+ mc.showWindow(self.name)
+ mc.window(self.name, edit=True, width=self.width, height=self.height)
+
+
+ def createMenu(self, *args):
+ '''
+ Create the main menu for the UI
+ '''
+
+ #generate shelf label by removing ml_
+ shelfLabel = self.name.replace('ml_','')
+ module = self.module
+ if not module:
+ module = self.name
+
+ #if icon exists, use that
+ argString = ''
+ if not self.icon:
+ argString = ', label="'+shelfLabel+'"'
+
+ mc.menu(label='Tools')
+ mc.menuItem(label='Add to shelf',
+ command='import ml_utilities;ml_utilities.createShelfButton("import '+module+';'+module+'.ui()", name="'+self.name+'", description="Open the UI for '+self.name+'."'+argString+')')
+ if not self.icon:
+ mc.menuItem(label='Get Icon',
+ command=(_showHelpCommand(ICON_URL+self.name+'.png')))
+ mc.menuItem(label='Get More Tools!',
+ command=(_showHelpCommand(WEBSITE_URL+'/tools/')))
+ mc.setParent( '..', menu=True )
+
+ mc.menu(label='Help')
+ mc.menuItem(label='About', command=self.about)
+ mc.menuItem(label='Documentation', command=(_showHelpCommand(TOOL_URL+self.name+'/')))
+ mc.menuItem(label='Python Command Documentation', command=(_showHelpCommand(TOOL_URL+'#\%5B\%5B'+self.name+'\%20Python\%20Documentation\%5D\%5D')))
+ mc.menuItem(label='Submit a Bug or Request', command=(_showHelpCommand(WEBSITE_URL+'/about/')))
+
+ mc.setParent( '..', menu=True )
+
+
+ def about(self, *args):
+ '''
+ This pops up a window which shows the revision number of the current script.
+ '''
+
+ text='by Morgan Loomis\n\n'
+ try:
+ __import__(self.module)
+ module = sys.modules[self.module]
+ text = text+'Revision: '+str(module.__revision__)+'\n'
+ except Exception:
+ pass
+ try:
+ text = text+'ml_utilities Rev: '+str(__revision__)+'\n'
+ except Exception:
+ pass
+
+ mc.confirmDialog(title=self.name, message=text, button='Close')
+
+
+ def buttonWithPopup(self, label=None, command=None, annotation='', shelfLabel='', shelfIcon='render_useBackground', readUI_toArgs={}):
+ '''
+ Create a button and attach a popup menu to a control with options to create a shelf button or a hotkey.
+ The argCommand should return a kwargs dictionary that can be used as args for the main command.
+ '''
+
+ if self.icon:
+ shelfIcon = self.icon
+
+ if annotation and not annotation.endswith('.'):
+ annotation+='.'
+
+ button = mc.button(label=label, command=command, annotation=annotation+' Or right click for more options.')
+
+ mc.popupMenu()
+ self.shelfMenuItem(command=command, annotation=annotation, shelfLabel=shelfLabel, shelfIcon=shelfIcon)
+ self.hotkeyMenuItem(command=command, annotation=annotation)
+ return button
+
+
+ def shelfMenuItem(self, command=None, annotation='', shelfLabel='', shelfIcon='menuIconConstraints', menuLabel='Create Shelf Button'):
+ '''
+ This creates a menuItem that can be attached to a control to create a shelf menu with the given command
+ '''
+ pythonCommand = 'import '+self.name+';'+self.name+'.'+command.__name__+'()'
+
+ mc.menuItem(label=menuLabel,
+ command='import ml_utilities;ml_utilities.createShelfButton(\"'+pythonCommand+'\", \"'+shelfLabel+'\", \"'+self.name+'\", description=\"'+annotation+'\", image=\"'+shelfIcon+'\")',
+ enableCommandRepeat=True,
+ image=shelfIcon)
+
+
+ def hotkeyMenuItem(self, command=None, annotation='', menuLabel='Create Hotkey'):
+ '''
+ This creates a menuItem that can be attached to a control to create a hotkey with the given command
+ '''
+ melCommand = 'import '+self.name+';'+self.name+'.'+command.__name__+'()'
+ mc.menuItem(label=menuLabel,
+ command='import ml_utilities;ml_utilities.createHotkey(\"'+melCommand+'\", \"'+self.name+'\", description=\"'+annotation+'\")',
+ enableCommandRepeat=True,
+ image='commandButton')
+
+
+ def selectionField(self, label='', annotation='', channel=False, text=''):
+ '''
+ Create a field with a button that adds the selection to the field.
+ '''
+ field = mc.textFieldButtonGrp(label=label, text=text,
+ buttonLabel='Set Selected')
+ mc.textFieldButtonGrp(field, edit=True, buttonCommand=partial(self._populateSelectionField, channel, field))
+ return field
+
+
+ def _populateSelectionField(self, channel, field, *args):
+
+ selectedChannels = None
+ if channel:
+ selectedChannels = getSelectedChannels()
+ if not selectedChannels:
+ raise RuntimeError('Please select an attribute in the channelBox.')
+ if len(selectedChannels) > 1:
+ raise RuntimeError('Please select only one attribute.')
+
+ sel = mc.ls(sl=True)
+ if not sel:
+ raise RuntimeError('Please select a node.')
+ if len(sel) > 1:
+ raise RuntimeError('Please select only one node.')
+
+ selection = sel[0]
+ if selectedChannels:
+ selection = selection+'.'+selectedChannels[0]
+
+ mc.textFieldButtonGrp(field, edit=True, text=selection)
+
+
+ def selectionList(self, channel=False, **kwargs):
+ tsl = mc.textScrollList(**kwargs)
+ mc.button(label='Append Selected', command=partial(self._populateSelectionList, channel, tsl))
+ return tsl
+
+
+ def _populateSelectionList(self, channel, control, *args):
+
+ selectedChannels = None
+ if channel:
+ selectedChannels = getSelectedChannels()
+ if not selectedChannels:
+ raise RuntimeError('Please select an attribute in the channelBox.')
+ if len(selectedChannels) > 1:
+ raise RuntimeError('Please select only one attribute.')
+
+ sel = mc.ls(sl=True)
+ if not sel:
+ raise RuntimeError('Please select a node.')
+ if len(sel) > 1:
+ raise RuntimeError('Please select only one node.')
+
+ selection = sel[0]
+ if selectedChannels:
+ selection = selection+'.'+selectedChannels[0]
+
+ mc.textScrollList(control, edit=True, append=[selection])
+
+
+ class ButtonWithPopup():
+
+ def __init__(self, label=None, name=None, command=None, annotation='', shelfLabel='', shelfIcon='render_useBackground', readUI_toArgs={}, **kwargs):
+ '''
+ The fancy part of this object is the readUI_toArgs argument.
+ '''
+
+ self.uiArgDict = readUI_toArgs
+ self.name = name
+ self.command = command
+ self.kwargs = kwargs
+
+ self.annotation = annotation
+ self.shelfLabel = shelfLabel
+ self.shelfIcon = shelfIcon
+
+ if annotation and not annotation.endswith('.'):
+ annotation+='.'
+
+ button = mc.button(label=label, command=self.runCommand, annotation=annotation+' Or right click for more options.')
+
+ mc.popupMenu()
+ mc.menuItem(label='Create Shelf Button', command=self.createShelfButton, image=shelfIcon)
+
+ mc.menuItem(label='Create Hotkey',
+ command=self.createHotkey, image='commandButton')
+
+
+ def readUI(self):
+ '''
+ This reads the UI elements and turns them into arguments saved in a kwargs style member variable
+ '''
+
+ if self.uiArgDict:
+ #this is some fanciness to read the values of UI elements and generate or run the resulting command
+ #keys represent the argument names, the values are UI elements
+ for k in list(self.uiArgDict.keys()):
+
+ uiType = mc.objectTypeUI(self.uiArgDict[k])
+ value = None
+ if uiType == 'rowGroupLayout':
+ controls = mc.layout(self.uiArgDict[k], query=True, childArray=True)
+ if 'check1' in controls:
+ value = mc.checkBoxGrp(self.uiArgDict[k], query=True, value1=True)
+ elif 'radio1' in controls:
+ #this will be a 1 based index, we want to return formatted button name?
+ value = mc.radioButtonGrp(self.uiArgDict[k], query=True, select=True)-1
+ elif 'slider' in controls:
+ try:
+ value = mc.floatSliderGrp(self.uiArgDict[k], query=True, value=True)
+
+ except Exception:
+ pass
+ try:
+ value = mc.intSliderGrp(self.uiArgDict[k], query=True, value=True)
+
+ except Exception:
+ pass
+ elif 'field1' in controls:
+ value = mc.floatFieldGrp(self.uiArgDict[k], query=True, value1=True)
+ elif 'OptionMenu' in controls:
+ value = mc.optionMenuGrp(self.uiArgDict[k], query=True, select=True)
+ else:
+ OpenMaya.MGlobal.displayWarning('Cannot read '+uiType+' UI element: '+self.uiArgDict[k])
+ continue
+
+ self.kwargs[k] = value
+
+
+ def runCommand(self, *args):
+ '''
+ This compiles the kwargs and runs the command directly
+ '''
+ self.readUI()
+ self.command(**self.kwargs)
+
+
+ def stringCommand(self):
+ '''
+ This takes the command
+ '''
+
+ cmd = 'import '+self.name+'\n'+self.name+'.'+self.command.__name__+'('
+
+ comma = False
+ for k,v in list(self.kwargs.items()):
+ value = v
+ if isinstance(v, str):
+ value = "'"+value+"'"
+ else:
+ value = str(value)
+
+ if comma:
+ cmd+=', '
+ cmd = cmd+k+'='+value
+
+ comma = True
+
+ cmd+=')'
+
+ return cmd
+
+
+ def createShelfButton(self,*args):
+ '''
+ Builds the command and creates a shelf button out of it
+ '''
+ self.readUI()
+ pythonCommand = self.stringCommand()
+ createShelfButton(pythonCommand, self.shelfLabel, self.name, description=self.annotation, image=self.shelfIcon)
+
+
+ def createHotkey(self, annotation='', menuLabel='Create Hotkey'):
+ '''
+ Builds the command and prompts to create a hotkey.
+ '''
+
+ self.readUI()
+ pythonCommand = self.stringCommand()
+ createHotkey(pythonCommand, self.name, description=self.annotation)
+
+
+class SkipUndo():
+ '''
+ Skips adding the encapsulated commands to the undo queue, so that you
+ cannot undo them.
+ '''
+
+ def __enter__(self):
+ '''
+ Turn off undo
+ '''
+ mc.undoInfo(stateWithoutFlush=False)
+
+ def __exit__(self,*args):
+ '''
+ Turn on undo
+ '''
+ mc.undoInfo(stateWithoutFlush=True)
+
+
+class UndoChunk():
+ '''
+ In versions of maya before 2011, python doesn't always undo properly, so in
+ some cases we have to manage the undo queue ourselves.
+ '''
+
+ def __init__(self, force=False):
+ self.force = force
+
+ def __enter__(self):
+ '''open the undo chunk'''
+ if self.force or MAYA_VERSION < 2011:
+ self.force = True
+ mc.undoInfo(openChunk=True)
+
+ def __exit__(self, *args):
+ '''close the undo chunk'''
+ if self.force:
+ mc.undoInfo(closeChunk=True)
+
+
+class Vector:
+
+ def __init__(self, x=0, y=0, z=0):
+ '''
+ Initialize the vector with 3 values, or else
+ '''
+
+ if self._isCompatible(x):
+ x = x[0]
+ y = x[1]
+ z = x[2]
+ self.x = x
+ self.y = y
+ self.z = z
+
+ def __repr__(self):
+ return 'Vector({0:.2f}, {1:.2f}, {2:.2f})'.format(*self)
+
+ #iterator methods
+ def __iter__(self):
+ return iter((self.x, self.y, self.z))
+
+ def __getitem__(self, key):
+ return (self.x, self.y, self.z)[key]
+
+ def __setitem__(self, key, value):
+ [self.x, self.y, self.z][key] = value
+
+ def __len__(self):
+ return 3
+
+ def _isCompatible(self, other):
+ '''
+ Return true if the provided argument is a vector
+ '''
+ if isinstance(other,(Vector,list,tuple)) and len(other)==3:
+ return True
+ return False
+
+
+ def __add__(self, other):
+
+ if not self._isCompatible(other):
+ raise TypeError('Can only add to another vector of the same dimension.')
+
+ return Vector(*[a+b for a,b in zip(self,other)])
+
+
+ def __sub__(self, other):
+
+ if not self._isCompatible(other):
+ raise TypeError('Can only subtract another vector of the same dimension.')
+
+ return Vector(*[a-b for a,b in zip(self,other)])
+
+
+ def __mul__(self, other):
+
+ if self._isCompatible(other):
+ return Vector(*[a*b for a,b in zip(self,other)])
+ elif isinstance(other, (float,int)):
+ return Vector(*[x*float(other) for x in self])
+ else:
+ raise TypeError("Can't multiply {} with {}".format(self, other))
+
+
+ def __div__(self, other):
+ if isinstance(other, (float,int)):
+ return Vector(*[x/float(other) for x in self])
+ else:
+ raise TypeError("Can't divide {} by {}".format(self, other))
+
+
+ def magnitude(self):
+ return math.sqrt(sum([x**2 for x in self]))
+
+
+ def normalize(self):
+ d = self.magnitude()
+ if d:
+ self.x /= d
+ self.y /= d
+ self.z /= d
+ return self
+
+
+ def normalized(self):
+ d = self.magnitude()
+ if d:
+ return self/d
+ return self
+
+
+ def dot(self, other):
+ if not self._isCompatible(other):
+ raise TypeError('Can only perform dot product with another Vector object of equal dimension.')
+ return sum([a*b for a,b in zip(self,other)])
+
+
+ def cross(self, other):
+ if not self._isCompatible(other):
+ raise TypeError('Can only perform cross product with another Vector object of equal dimension.')
+ return Vector(self.y * other.z - self.z * other.y,
+ -self.x * other.z + self.z * other.x,
+ self.x * other.y - self.y * other.x)
+
+
+
+# ______________________
+# - -/__ Revision History __/- - - - - - - - - - - - - - - - - - - - - - - -
+#
+# Revision 1: : First publish
+#
+# Revision 2: 2011-05-04 : Fixed error in frameRange.
+#
+# Revision 3: 2012-05-31 : Adding Menu and Icon update to UI, adding KeyframeSelection object, and a few random utility functions.
+#
+# Revision 4: 2012-06-01 : Fixing bug with UI icons
+#
+# Revision 5: 2012-07-23 : Expanding and bug fixing Keyselection, added SkipUndo, minor bug fixes.
+#
+# Revision 6: 2012-07-23 : KeySelection bug fix
+#
+# Revision 7: 2012-08-07 : Minor bug with Keyselection, adding functions.
+#
+# Revision 8: 2012-11-06 : Backwards incompatable update to KeySelection, and several new functions.
+#
+# Revision 9: 2013-10-29 : Update to support more options for goToKeyframe, update to preserve isolateSelect.
+#
+# Revision 10: 2014-03-01 : adding category, updating contact.
+#
+# Revision 11: 2014-03-08 : Fixed keySelection bug with keyed channels.
+#
+# Revision 12: 2015-04-27 : updated for ml_puppet support
+#
+# Revision 13: 2015-05-13 : UI function updates
+#
+# Revision 14: 2015-05-16 : minor update to setAnimValue
+#
+# Revision 15: 2015-05-18 : Small bugfix in matchBake
+#
+# Revision 16: 2016-02-29 : Support for animCurveEditor and fixing some old hotkey bugs.
+#
+# Revision 17: 2016-03-23 : keySelection bug fix.
+#
+# Revision 18: 2016-05-05 : Update matchBake to support tangent weights.
+#
+# Revision 19: 2016-06-02 : temp patching hotkey issue with > 2015
+#
+# Revision 20: 2016-07-31 : Update to MlUi to support subclassing.
+#
+# Revision 21: 2016-07-31 : MlUi bug fixes.
+#
+# Revision 21: 2016-08-11 : windows support for icons
+#
+# Revision 22: 2016-10-01 : changing frameRange to return consistent results when returning timeline or selection.
+#
+# Revision 23: 2016-10-12 : Tangent bug fixes for 2016.5
+#
+# Revision 24: 2016-10-31 : Adding selection field to mlUI
+#
+# Revision 25: 2016-11-21 : silly icon path bug
+#
+# Revision 26: 2016-12-05 : Adding getSkinCluster
+#
+# Revision 27: 2016-12-10 : Adding Vector class to remove euclid dependency
+#
+# Revision 28: 2017-03-20 : bug fix and support for ml_puppet
+#
+# Revision 29: 2017-04-25 : matchBake support input frames
+#
+# Revision 30: 2017-06-13 : unify version test, isolate view update
+#
+# Revision 31: 2017-06-30 : getCamera support for stereo camera
+#
+# Revision 32: 2018-02-17 : Updating license to MIT.
+#
+# Revision 33: 2018-07-18 : getNamespace bug
+#
+# Revision 34: 2019-03-07 : github URL update
\ No newline at end of file
diff --git a/python/mmSolver/tools/mltools/tools.py b/python/mmSolver/tools/mltools/tools.py
new file mode 100644
index 000000000..78266e8fc
--- /dev/null
+++ b/python/mmSolver/tools/mltools/tools.py
@@ -0,0 +1,102 @@
+# Copyright (C) 2022 Kazuma Tonegawa.
+#
+# This file is part of mmSolver.
+#
+# mmSolver is free software: you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# mmSolver is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with mmSolver. If not, see .
+#
+"""
+This file contains functions to further extend the features of
+Morgan Loomis' tools.
+"""
+
+import maya.cmds as mc
+from maya import OpenMaya
+
+import ml_convertRotationOrder
+from constant import ROTATE_ORDERS
+
+def loadTipsForAll():
+ '''
+ This is a modified version of the loadTips() method from
+ ml_convertRotationOrder.py file, but this is supposed to
+ print the rotate order status for all selected objects
+ and print them in the Script Editor.
+ '''
+
+ selList = mc.ls(sl=True)
+
+ if not selList:
+ OpenMaya.MGlobal.displayWarning('Please select at least one object.')
+ return
+
+ for sel in selList:
+ ro = ROTATE_ORDERS[mc.getAttr(sel+'.rotateOrder')]
+
+ nodeName = mc.ls(sel, shortNames=True)[0]
+
+ infoText = 'This object is '
+ tol = ml_convertRotationOrder.gimbalTolerence(sel)
+ if tol < 0.1:
+ infoText += 'not currently gimballing'
+ else:
+ if tol < 0.5:
+ infoText += 'only '
+ infoText += str(int(tol*100))
+ infoText+='% gimballed'
+
+
+ #test all rotation orders and find the lowest value
+ rotOrderTests = ml_convertRotationOrder.testAllRotateOrdersForGimbal(sel)
+ lowest = sorted(rotOrderTests)[0]
+ #find the lower of the two worldspace options
+ lowestWS = 1
+ for each in rotOrderTests[2:4]:
+ if each < lowestWS:
+ lowestWS = each
+
+ #determine if it's a worldspace control
+ ws = ml_convertRotationOrder.isWorldSpaceControl(sel)
+ if ws:
+ infoText += ", and it looks like it's a worldspace control."
+ else:
+ infoText+='.'
+
+ resultingText = '{nodeName} | {ro} -- Current rotate order --'.format(
+ nodeName=nodeName,
+ ro=ro
+ )
+ for t, r in zip(rotOrderTests, ROTATE_ORDERS):
+ if r == ro:
+ continue
+
+ text = '(' + str(int(t*100)) + '%) Gimballed.'
+
+ if ws:
+ if r.endswith('y') and t == lowestWS: #lowest worldspace option is reccomended
+ text += '<-- [RECOMMENDED]'
+ elif lowest