Wednesday, November 8, 2017

naNudgeWeight

Hi,
I wrote this to help with cluster weighting using hot keys.
please use/modify at your own risk.

Happy Sculpting!
Nate

"""useful for adjusting weights on selected components via hot keys. currently works with clusters only.
with skinclusters it would be more complicated to subtract weights.
later i want to add support for clusters on cvs of curves or nurbssurfaces.

@author Nathaniel Anozie
Modify at your own risk
last updated: 11/08/2017 -- initial release
last updated: 11/07/2017 -- preparing for initial release
last updated: 11/05/2017 -- initial work

import sys
sys.path.append('/Users/Nathaniel/Documents/src/python/naDeformation')#put your script path here or add to python scripts maya looks for
import naNudgeWeight
reload(naNudgeWeight)

#this is needed to tell what is the cluster to be painted
naNudgeWeight.InitSimpleWindow()

#these two lines can be used as a hot key to add weight
nudge = naNudgeWeight.NudgeCluster()
nudge.nudge(0.1)

#this can be used as a hotkey to remove weight
nudge = naNudgeWeight.NudgeCluster()
nudge.nudge(-0.1)

#this can be used as a hotkey to smooth weight
nudge = naNudgeWeight.NudgeCluster()
nudge.smooth()

#please note when you’re done with the tool just run
naNudgeWeight.removeAllTagging() #so there is no naNudge attribute left on any clusters in scene
"""

import maya.cmds as cmds
import maya.mel as mel
from functools import partial

NA_ACTIVE_CLUSTER_ATTR = 'naNudge'


#for a tagging mechanism so our hot keys can work by finding active weight container
def removeTagging(clusterName):
    if not cmds.listAttr(clusterName,ud=True):
        return
    if NA_ACTIVE_CLUSTER_ATTR in cmds.listAttr(clusterName,ud=True):
        cmds.deleteAttr('%s.%s' %(clusterName,NA_ACTIVE_CLUSTER_ATTR) )
def removeAllTagging():
    for cl in findWeightContainers():
        removeTagging(cl)
def addTagging(clusterName):
    removeAllTagging()
    #only one weight container gets tag, should stay same with skin clusters
    cmds.addAttr(clusterName, ln= NA_ACTIVE_CLUSTER_ATTR, at="bool")
def getActiveWeightContainerInScene():
    """searches scene for a tagged object.  
    in this case we find first active weight container. should be just one
    """
    result = None
    for clusterName in findWeightContainers():
        if not cmds.listAttr(clusterName,ud=True):
            continue
        if NA_ACTIVE_CLUSTER_ATTR in cmds.listAttr(clusterName,ud=True):
            result = clusterName
            break
    return result
    
        
class InitSimpleWindow(object):
    """
    this kind of maya based ui can be used for other examples
    
    inspired by capper's techart online forum posting on #acessing controls from external functions
    """
    WINDOW_NAME = 'naNudgeWindow'
    WINDOW_WIDTH = 250
    WINDOW_HEIGHT = 150
    SCROLL_LIST_HEIGHT = 100    
    
    def __init__(self):
        self.weightContainerScrollList = None
        self.initUI()
    
    #these are a bunch of defs specific to the ui widgets used in window
    def getActiveCluster(self):
        result = None
        clusterSelected = cmds.textScrollList(self.weightContainerScrollList, q=True,si=True)
        if clusterSelected:
            result = clusterSelected[0]
        return result
        
    def selectionChange(self, *args):
        #print 'selectionChange event'
        clusterName = self.getActiveCluster()
        if not clusterName:
            return
        cmds.select(cmds.listConnections(clusterName))
        
    def buttonPressedOK(self, *args):
        #print 'buttonPressedOK()>>>'
        clusterName = self.getActiveCluster()
        if not clusterName:
            print 'no weight container chosen'
            return
        
        print 'initializing weighting tool for cluster %s' %clusterName
        removeAllTagging()
        addTagging(clusterName)
        
        #clear selection
        cmds.select(clear=True)
        self.closeUI()
        
    def buttonPressedHelp(self, *args):
        msg = 'After Picking a Cluster from list and pressing ok.  Use something like this for hotkeys:\n\n'
        msg += 'alt + --> add weight:\n'
        msg += 'nudge = naNudgeWeight.NudgeCluster()\n'
        msg += 'nudge.nudge(0.1)\n\n'
        msg += 'alt - --> remove weight:\n'
        msg += 'nudge = naNudgeWeight.NudgeCluster()\n'
        msg += 'nudge.nudge(-0.1)\n\n'    
        msg += 'nudge = naNudgeWeight.NudgeCluster()\n'
        msg += 'alt s --> smooth weight:\n'
        msg += 'nudge.smooth()'  
    
        cmds.confirmDialog( title='help', message=msg )
        
    def closeUI(self):
        if cmds.window(self.WINDOW_NAME,exists=True):
            cmds.deleteUI(self.WINDOW_NAME)
        
    def initUI(self):
        """run this each time we want to work on a new or different cluster.
        it sets an attribute on active cluser.
        
        Inspired by:
        Mark Jackson's online ui examples
        """
        weightContainers = findWeightContainers()
        if not weightContainers:
            print 'Nothing to do, tool requires a supported weight container in scene.\
            supporting: clusters only'
            return
    

        #some error checking
        self.closeUI()
            
        cmds.window(self.WINDOW_NAME,title="naNudgeWeight v 1.0.0")#,
        cmds.columnLayout(w=self.WINDOW_WIDTH, adj=True)
        self.weightContainerScrollList = cmds.textScrollList(height = self.SCROLL_LIST_HEIGHT,\
                                                        ams=False,\
                                                        da=True,\
                                                        sc=self.selectionChange
                                                        )
        for wgtContainer in weightContainers:
            cmds.textScrollList( self.weightContainerScrollList, edit=True, append=wgtContainer)
        #cmds.setParent('..')
        cmds.button(label="OK",command= self.buttonPressedOK )
        cmds.button(label="help",c=self.buttonPressedHelp)
        #cmds.setParent('..')
        
        cmds.showWindow(self.WINDOW_NAME)
        cmds.window(self.WINDOW_NAME, edit=True, widthHeight=(self.WINDOW_WIDTH,self.WINDOW_HEIGHT))#w=self.WINDOW_WIDTH
        
        
class NudgeCluster(object):
    """
    tools for nudging weight on cluster
    Inspired by Brian Tindall
    """
    CLUSTER_SUPPORTED = ['cluster'] #later add skinCluster
    COMPONENT_SUPPORTED = ['mesh'] #later add nurbs curve cvs, nurbs surface cvs
    
    def __init__(self, cluster = None):
        """
        needed because there could be multiple clusters acting on selected
        components.  this will make sure we are acting on correct cluster.
        should be done on initialization.
        """
        self.cluster = cluster
        if not cluster:
            print 'if dont manually initialize a cluster it will go by possible tags'
            print 'looking for active cluster...'
            activeWeightContainer = getActiveWeightContainerInScene()
            if activeWeightContainer and cmds.objectType(activeWeightContainer) in self.CLUSTER_SUPPORTED:
                print 'found active cluster  %s' %activeWeightContainer
                self._setCluster(activeWeightContainer)

    def getActiveWeightContainer(self):
        """will be called on every hot key press. returns active weight container
        """
        result = None
        for cl in findWeightContainers():
            if NA_ACTIVE_CLUSTER_ATTR in cmds.listAttr(cl,ud=True):
                return cl
        return result
        
    def _setCluster(self,clusterName):
        self.cluster = clusterName
        
    def _getCluster(self):
        return self.cluster
    
    def _getWeightInfo(self):
        """ get current weight information on selected vertices. later could support nurbs curves/surfaces
        weightInfo = [{'cmp':'pPlane1.vtx[0]','wgt':0.95},{'cmp':'pPlane1.vtx[4]','wgt':0.85}]
        """
        result = []
        
        cluster = self._getCluster()
        if not cluster:
            return
        if not cmds.objectType(cluster) in self.CLUSTER_SUPPORTED:
            return
        
        curSel = cmds.ls(selection=True)
        selExpandedComponents = getExpandedComponents(curSel)
        
        ###only consider components a member of same cluster.
        clusterSetAll = [x for x in cmds.listConnections(cluster) if cmds.objectType(x) == 'objectSet']
        if not clusterSetAll:
            print 'cluster is probably not of cluster type'
            return
        clusterSet = clusterSetAll[0]
        clusterSetShort = cmds.sets( clusterSet, query=True)
        
        clusterExpandedComponents = getExpandedComponents(clusterSetShort)
        if not clusterExpandedComponents:
            return
        
        print 'clusterExpandedComponents',clusterExpandedComponents
        print 'selExpandedComponents', selExpandedComponents
        
        #only consider components a member of same cluster
        for comp in selExpandedComponents:
            if comp in clusterExpandedComponents:
                info = {}
                info['cmp'] =  comp
                info['wgt'] = getWeight(cluster,comp)
                result.append(info)
                
        return result
    
    
    def nudge(self, delta = 0.05):
        """
        add or subtract delta from selected components
        """
        
        cluster = self._getCluster()
        if not cluster:
            print 'could not find a cluster to nudge :('
            return
        if not cmds.objectType(cluster) in self.CLUSTER_SUPPORTED:
            return
        
        #in case selection included components not members of our cluster it will skip them
        weightInfo = self._getWeightInfo()
        #print 'weightInfo', weightInfo
        for arg in weightInfo:
            component = arg['cmp']
            currentWeight = arg['wgt']
            #make sure weight between 0 and 1
            newWeight = min( max( currentWeight + delta, 0 ), 1.0 ) 
            setWeight( cluster, [component], newWeight )
    
    def smooth(self):
        cluster = self._getCluster()
        if not cluster:
            print 'could not find a cluster to nudge :('
            return
        if not cmds.objectType(cluster) in self.CLUSTER_SUPPORTED:
            return
        
        smoothClusterWeight( cluster )

def setWeight(cluster, component, value):
    """
    set weight on provided components, and cluster to value.
    currently only supports cluster. later add skinCluster type.
    """
    curSel = cmds.ls(selection=True)
    
    if not cmds.objectType(cluster) in ['cluster']:
        return
    
    #later add in other support for skinCluster
    if cmds.objectType(cluster) == 'cluster':
        for comp in component:
            print 'setting weight cluster:%s, component:%s, value:%d' %(cluster,comp,value )
            cmds.select( comp, replace=True)
            cmds.percent( cluster, v = value )

    #restore selection
    cmds.select(curSel, replace=True)

def getWeight(cluster, component):
    """
    get weight. later add support for skinCluster type.
    maybe add support for list of components
    """
    result = None
    
    if not cmds.objectType(component) in ['mesh']:
        print 'cannot support component type %s' %(cmds.objectType(component))
        return
        
    if cmds.objectType(cluster) == 'cluster':
        result = cmds.percent(cluster,value=True,query=True)[0]
    
    return result


def smoothClusterWeight( clusterName ):
    """smooths weights on selected components for given cluster name
    
    inspired by
    Paul Molodowitch (online examples on smoothing cluster weights)
    """
    if not cmds.objectType(clusterName)=="cluster":
        return
        
    curSel = cmds.ls(selection=True)
    #not sure of another way to smooth cluster weights
    mel.eval('artSetToolAndSelectAttr( "artAttrCtx", ("cluster.%s.weights"));'%clusterName)
    mel.eval("artAttrInitPaintableAttr;")
    mel.eval( "artAttrPaintOperation artAttrCtx Smooth;" )
    mel.eval( "artAttrCtx -e -clear `currentCtx`;")
    
    #this bit restores our mode to object, without it we would be in paint weight mode
    mel.eval("selectMode -object;")
    mel.eval("setToolTo selectSuperContext;")
    cmds.select(clear=True)
    cmds.select(curSel,replace=True)


def findWeightContainers():
    return cmds.ls(type="cluster")

def getExpandedComponents( sel = [] ):
    """
    get expanded components supports cvs, vertices
    """
    result = []
    curSel = cmds.ls(selection=True)
    
    #need selection to expand
    cmds.select(sel,replace=True)
    
    components = cmds.filterExpand(sm = 31) #VERTS
    if not components:
        components = cmds.filterExpand(sm = 28) #CV
    
    if components:
        result = components
        
    #restore current selection
    cmds.select(curSel, replace=True)
    
    return result


Inspired by Brian Tindall's Art of moving points book