Is it purple?

Or.. is it affected by a selected object?

I asked this question on Creative Crash and this reply was definately helpful in finding out how to deal with this; but don’t use affectsNet, it creates tons of nodes which contain info you could also come up with or print out. The golden tip was simply to look at the matrix and shape attributes of geometry and transform nodes.

So to do this we use the cmds.affects function, which tells what inputs of a node have influence (affect) the given output (it can do the other way around with the by flag, I don’t use that here however).

So whether a transform node is affected seems reasonably simple, the shape turns purple when one of it’s parents’ matrices is affected by a selected node.

>We use affects to find out what inputs affect the matrix attribute on a transform node
>Then we list the incoming connection of every parent
>We filter the incoming connections to only those that connect to the attributes returned by affects
>We check if one of these nodes is selected

But then it’s not that simple. The matrix attribute is affected by several attributes, these attributes are affected by more attributes, hence we need to iterate over all affecting attributes to see what affects them, until we had all the attributes and have a clear map of what nodes – directly or indirectly – affect the matrix attribute.

When none of the nodes are selected, we’re not there yet – what if a parent of an incomoing connection is selected? That is solved with the isParentSelected function posted here.

Now what if the node (and its parents) isn’t selected, maybe the output attributes that drive the inputs that affect the matrix attribute on the done we wish to know more about are affected by inputs which are connected as well. On second thought, look at this image instead of attempting to grasp that scentence.

affects

So we wish to know whether the rightmost node is affected, we map the matrix attribute and see that the red attributes affect eachother. Now the other node connected to it isn’t selected, but we must map the incoming connections’ affection to see that the blue attributes also affect eachother, because there are more affecting attributes we need to map inputs on the input nodes’ affected attributes as well, giving us the leftmost node which IS selected and therefore the rightmost node IS affected!

So the first trick is to get the true affects result by iterating of the the initial result until no more attributes affect the affects set.

def affectsAll( attr, type ):
    #these lists can in theory be precalculated constants
    attrs = cmds.affects(attr, t=type)
    if not attrs:
        return []
    i = 0
    while i < len(attrs):
        tmp = cmds.affects(attrs[i], t=type)
        if tmp:
            attrs.extend(tmp)
        attrs = list(set(attrs))
        i += 1
    return attrs

The next step is to find the full network of affected attributes by listing inputs, filtering by affected attributes, and iterating again as if we wanted to know whether that input node was affected. This can be done by going over all known nodes, starting with the given node, then appending all valid input nodes to the target list and repeating the iteration:

def affectedNet( inAttr, inNode ):
    nodes = [inNode]
    attributes = [[inAttr]]
    
    #iterate until affection found or entire network traversed
    i = 0
    while i < len(nodes):
        #find internel affection net
        attributes[i].extend( affectsAll(attributes[i][0], cmds.nodeType(nodes[i])) )
        
        #find nodes that are connected to plugs in the affected net
        inputs = cmds.listConnections(nodes[i], s=True, d=False, c=True, p=True)
        if inputs:
            for j in range(0,len(inputs),2):
                #attribute name in affectednet
                if inputs[j].rsplit('.',1)[-1] in attributes[i]:
                    #get node attribute pair
                    nodeattr = inputs[j+1].split('.',1)
                    nodeattr[0] = cmds.ls(nodeattr[0], l=True)[0]
                    if nodeattr[0] not in nodes:
                        #append new nodes
                        nodes.append(nodeattr[0])
                        attributes.append([nodeattr[1]])
                    else:
                        #append new plugs on known nodes
                        attributes[ nodes.index(nodeattr[0]) ].append( nodeattr[1] )
        
        #if no incoming node was selected, continue iterating
        i += 1
    return nodes, attributes

The next step is to provide input for these functions. If we wish to check whether a shape node is affected this is most cumbersome, as every shape node's out geometry attribute has a different name. So we assume the node to be a transfomr node with the attribute that determines affected color being 'matrix'. Then we check whether the object is a shape and change the attribute name before finally grabbing the affectedNet for that node/attr combination and checking whether any node in it, or one if that node's, parents is selected.

def isAffected(inPathStr):
    #assume node is a transform by default
    attrib = 'matrix'
    
    #get the output attribute if node is a shape
    if cmds.ls(inPathStr, type='shape'):
        #detect the attribute name to get the affectedNet for
        nodetype = cmds.nodeType( inPathStr )
        if nodetype == 'mesh':
            attrib = 'outMesh'
        elif nodetype == 'subdiv':
            attrib = 'outSubdiv'
        elif nodetype in ('nurbsCurve','nurbsSurface'):
            attrib = 'local'
        else:
            raise ValueError('Nodetype %s of node %s not supported in isAffected'%(nodetype, inPathStr))
    elif not cmds.ls(inPathStr, type='dagNode'):
        raise ValueError('Given node path %s is not a Dag node in isAffected'%inPathStr)

    
    for node in affectedNet(attrib, inPathStr)[0]:
        if isParentSelected(node):
            return True
    return False

Then the very last thing we need to do is to not check only the given node, but all it's parent nodes as well. Because if not the shape, then perhaps a parent is affected and the shape still needs to appear affected.

def isAffectedRecursively(inPathStr):
    obj = cmds.ls(inPathStr, l=True)
    if not obj:
        return False
    obj = obj[0]
    while obj and len(obj) > 1:
        if isAffected(obj):
            return True
        obj = obj.rsplit('|',1)[0]
    return False

By merging the affectedNet with the isAffected function I managed to get a 15% speed increase as this function is reasonably slow, but it cancels simply as soon as a found node is selected. What may be better is to cache all the affected networks once we need them (put them in a dict, key is the full dagpath string) and then use that. Just the merged code in case you disagree:

def isAffected( inNode ):
    nodes = [inNode]
    attributes = [['matrix']]
    
    
    #get the output attribute if node is a shape
    if cmds.ls(inNode, type='shape'):
        #detect the attribute name to get the affectedNet for
        nodetype = cmds.nodeType( inNode )
        if nodetype == 'mesh':
            attributes[0][0] = 'outMesh'
        elif nodetype == 'subdiv':
            attributes[0][0] = 'outSubdiv'
        elif nodetype in ('nurbsCurve','nurbsSurface'):
            attributes[0][0] = 'local'
        else:
            raise ValueError('Nodetype %s of node %s not supported in isAffected'%(nodetype, inNode))
    elif not cmds.ls(inNode, type='dagNode'):
        raise ValueError('Given node path %s is not a Dag node in isAffected'%inNode)


    #iterate until affection found or entire network traversed
    i = 0
    while i < len(nodes):
        #find internel affection net
        attributes[i].extend( affectsAll(attributes[i][0], cmds.nodeType(nodes[i])) )
        
        #find nodes that are connected to plugs in the affected net
        inputs = cmds.listConnections(nodes[i], s=True, d=False, c=True, p=True)
        if inputs:
            for j in range(0,len(inputs),2):
                #attribute name in affectednet
                if inputs[j].rsplit('.',1)[-1] in attributes[i]:
                    #get node attribute pair
                    nodeattr = inputs[j+1].split('.',1)
                    nodeattr[0] = cmds.ls(nodeattr[0], l=True)[0]
                    if nodeattr[0] not in nodes:
                        #bail as soon as node is affected
                        if isParentSelected(nodeattr[0]):
                            return True
                        #append new nodes
                        nodes.append(nodeattr[0])
                        attributes.append([nodeattr[1]])
                    else:
                        #append new plugs on known nodes
                        attributes[ nodes.index(nodeattr[0]) ].append( nodeattr[1] )
    
        #if no incoming node was selected, continue iterating
        i += 1
    return False

Remove the affectedNet and replace the isAffected function with the above and run this testing code to see the printed time drop as well as to see the function's cases functional (note I didn't use isAffectedRecursively here):

#test code
c = cmds.polyCube()[0]
s = cmds.polySphere()[0]
cmds.xform(s,t=[0,0,2])
cn = cmds.orientConstraint(c, s)
import time
t = time.time()
print 'direct input selection'
cmds.select(cn)
print isAffected( s )
print 
print 'direct input selection that does not drive an affecting attribute'
n = cmds.group(em=True)
cmds.connectAttr('%s.visibility'%n, '%s.visibility'%s)
print isAffected( s )
print 
print 'secondary input selection'
cmds.select(c)
print isAffected( s )
print 
print 'input parent selection'
cmds.group(c)
print isAffected( s )
print 
print 'irrelevant selection'
cmds.select(cmds.listRelatives(c,c=True,f=True))
print isAffected( s )
print 
print 'no selection'
cmds.select(cl=True)
print isAffected( s )
print 
print 'beware: the object affects itself because it contains the constraint'
cmds.select(s)
print isAffected( s )
print 
print 'as you can see it\'s shape does not'
cmds.select(cmds.listRelatives(s,c=True,f=True,type='shape'))
print isAffected( s )
print 
print 'and with the constraint deleted neither does the object any longer'
cmds.delete(cn)
cmds.select(s)
print isAffected( s )
print 
print time.time()-t

Leave a Reply

Your email address will not be published. Required fields are marked *