Get vertices of an edge-loop and sort them around their unit circle

Funny story: I actually got to use the code I wrote for you the other day at work!
That means I got to flesh out that code into something a bit more production ready. (Also, this geometry connectivity stuff is my absolute favorite task, so I maaaayyyy have gone a bit overboard :smiley: )

One annoying thing (as you found out) is the string parsing. I handle that with mayaSelRange
Then I get the neighbors with buildNeighborDict. No pickwalking required. Also, I have a very strong aversion to doing anything with selection during the execution of a script.
Then with a little thought, I figured out how to handle multiple loops and loops that don’t connect back to themselves. Thats what sortLoops does.

from maya import cmds

def mayaSelRange(vals):
    """Convert maya cmds.ls() component selection list into indices

    Arguments:
        vals (list): A list of components like what you get out of cmds.ls(sl=True)

    Returns:
        list: A list of integer indices
    """
    out = []
    for val in vals:
        nn = val.split('[')[1][:-1].split(':')
        nn = list(map(int, nn))
        out.extend(range(nn[0], nn[-1] + 1))
    return out


def buildNeighborDict(vertColl):
    """ Parse vertices into a dictionary of neighbors, limited to the original vertex set

    Arguments:
        vertColl (list):  A list of verts like what you get out of cmds.ls(sl=True)

    Returns:
        dict: A dictionary formatted like {vertIndex: [neighborVertIdx, ...], ...}
    """
    # Get the object name
    objName = vertColl[0].split('.')[0]
    verts = set(mayaSelRange(vertColl))
    neighborDict = {}
    for v in verts:
        vname = '{0}.vtx[{1}]'.format(objName, v)
        edges = cmds.polyListComponentConversion(vname, fromVertex=True, toEdge=True)
        neighbors = cmds.polyListComponentConversion(
            edges, fromEdge=True, toVertex=True
        )
        neighbors = set(mayaSelRange(neighbors))
        neighbors.remove(v)
        neighborDict[v] = list(neighbors & verts)
    return neighborDict


def sortLoops(neighborDict):
    """Sort vertex loop neighbors into individual loops

    Arguments:
        neighborDict (dict):  A dictionary formatted like {vertIndex: [neighborVertIdx, ...], ...}

    Returns:
        list of lists: A list of lists containing ordered vertex loops.
            Only if the loop is closed, the first and last element will be the same.
    """
    neighborDict = dict(neighborDict)  # work on a copy of the dict so I don't destroy the original
    loops = []

    # If it makes it more than 1000 times through this code, something is probably wrong
    # This way I don't get stuck in an infinite loop like I could with while(neighborDict)
    for _ in range(1000):
        if not neighborDict:
            break
        vertLoop = [neighborDict.keys()[0]]
        vertLoop.append(neighborDict[vertLoop[-1]][0])

        # Loop over this twice: Once forward, and once backward
        # This handles loops that don't connect back to themselves
        for i in range(2):
            vertLoop = vertLoop[::-1]
            while vertLoop[0] != vertLoop[-1]:
                nextNeighbors = neighborDict[vertLoop[-1]]
                if len(nextNeighbors) == 1:
                    break
                elif nextNeighbors[0] == vertLoop[-2]:
                    vertLoop.append(nextNeighbors[1])
                else:
                    vertLoop.append(nextNeighbors[0])

        # Remove vertices I've already seen from the dict
        # Don't remove the same vert twice if the first and last items are the same
        start = 0
        if vertLoop[0] == vertLoop[-1]:
            start = 1
        for v in vertLoop[start:]:
            del neighborDict[v]
        loops.append(vertLoop)
    else:
        raise RuntimeError("You made it through 1000 loops, and you still aren't done?  Something must be wrong")
    return loops
4 Likes