├── .gitignore ├── __init__.py ├── README.md ├── managerUI.py ├── utils.py ├── manager.py └── functions.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | ''' 2 | A python package that creates a control shape manager for maya to load, save, copy, paste, mirror, etc. nurbs curves. 3 | ''' 4 | import managerUI 5 | reload(managerUI) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Demo Control shape manager for Maya 2 | *This repo is used for demonstration purposes, to be followed along with in this blog post* 3 | 4 | http://bindpose.com/creating-maya-control-shape-manager 5 | 6 | Scroll down for an example GIF. 7 | 8 | ### Usage 9 | 1. Copy the `controlShapeManager` folder to your maya scripts path - typically `C:/Users/username/maya/scripts`. 10 | 2. In `manager.py` change the `SHAPE_LIBRARY_PATH` to the path where you would like to store your control shapes. 11 | 3. In `managerUI.py` change `SHELF_NAME` to the name of the shelf where you want to create the example button for the manager. 12 | 4. Run the following code, which will create a button, containing a dropdown menu, on the specified shelf. 13 | ```python 14 | import controlShapeManager 15 | ``` 16 | 5. *(Optional)* To get the colours working you need to get the images from [here](https://www.dropbox.com/sh/osdatp13h01coz7/AAB9pCYP9uBZRaVRjYKqIk--a?dl=1). Then in `managerUI.py` change `ICON_PATH` to the path where you've saved them. 17 | 18 | ### Example 19 | ![](http://bindpose.com/wp-content/uploads/2017/05/2017-05-01_08-54-21.gif) 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /managerUI.py: -------------------------------------------------------------------------------- 1 | '''Creates a very simple UI, which is just a button on a specified shelf containing a popup with all the needed 2 | control shape functions.''' 3 | import maya.cmds as mc 4 | 5 | 6 | # Local import 7 | import functions 8 | reload(functions) 9 | 10 | SHELF_NAME = "Custom" 11 | ICON_PATH = "C:/PATH_TO_ICONS" 12 | 13 | if SHELF_NAME and mc.shelfLayout(SHELF_NAME, ex=1): 14 | children = mc.shelfLayout(SHELF_NAME, q=1, ca=1) or [] 15 | for each in children: 16 | try: 17 | label = mc.shelfButton(each, q=1, l=1) 18 | except: 19 | continue 20 | if label == "ctlShapeManager": 21 | mc.deleteUI(each) 22 | 23 | mc.setParent(SHELF_NAME) 24 | mc.shelfButton(l="ctlShapeManager", i="commandButton.png", width=37, height=37, iol="CTL") 25 | popup = mc.popupMenu(b=1) 26 | mc.menuItem(p=popup, l="Save to library", c=functions.saveCtlShapeToLib) 27 | 28 | sub = mc.menuItem(p=popup, l="Assign from library", subMenu=1) 29 | 30 | for each in functions.getAvailableControlShapes(): 31 | mc.menuItem(p=sub, l=each[0], c=each[1]) 32 | 33 | mc.menuItem(p=popup, l="Copy", c=functions.copyCtlShape) 34 | mc.menuItem(p=popup, l="Paste", c=functions.pasteCtlShape) 35 | 36 | sub = mc.menuItem(p=popup, l="Set colour", subMenu=1) 37 | 38 | for each in functions.getAvailableColours(): 39 | mc.menuItem(p=sub, l=each[0], c=each[1], i=ICON_PATH + each[2]) 40 | 41 | mc.menuItem(p=popup, l="Flip", c=functions.flipCtlShape) 42 | mc.menuItem(p=popup, l="Mirror", c=functions.mirrorCtlShapes) 43 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | '''Contains utility commands to help work and I/O nurbsCurve data.''' 2 | import os 3 | import json 4 | from maya import cmds as mc, OpenMaya as om 5 | 6 | 7 | def validatePath(path=None): 8 | '''Checks if the file already exists and provides a dialog to overwrite or not''' 9 | if os.path.isfile(path): 10 | confirm = mc.confirmDialog(title='Overwrite file?', 11 | message='The file ' + path + ' already exists.Do you want to overwrite it?', 12 | button=['Yes', 'No'], 13 | defaultButton='Yes', 14 | cancelButton='No', 15 | dismissString='No') 16 | if confirm == "No": 17 | mc.warning("The file " + path + " was not saved") 18 | return 0 19 | return 1 20 | 21 | 22 | def loadData(path=None): 23 | '''Loads raw JSON data from a file and returns it as a dict''' 24 | if os.path.isfile(path): 25 | f = open(path, "r") 26 | data = json.loads(f.read()) 27 | f.close() 28 | return data 29 | else: 30 | mc.error("The file " + path + " doesn't exist") 31 | 32 | 33 | def saveData(path=None, 34 | data=None): 35 | '''Saves a dictionary as JSON in a file''' 36 | if validatePath(path): 37 | f = open(path, "w") 38 | f.write(json.dumps(data, sort_keys=1, indent=4, separators=(",", ":"))) 39 | f.close() 40 | return 1 41 | return 0 42 | 43 | 44 | def getKnots(crvShape=None): 45 | mObj = om.MObject() 46 | sel = om.MSelectionList() 47 | sel.add(crvShape) 48 | sel.getDependNode(0, mObj) 49 | 50 | fnCurve = om.MFnNurbsCurve(mObj) 51 | tmpKnots = om.MDoubleArray() 52 | fnCurve.getKnots(tmpKnots) 53 | 54 | return [tmpKnots[i] for i in range(tmpKnots.length())] 55 | -------------------------------------------------------------------------------- /manager.py: -------------------------------------------------------------------------------- 1 | '''Contains the core low level functionality of the control shape manager. The functions here work directly 2 | with the data in the nurbs curves.''' 3 | import os 4 | import re 5 | 6 | from maya import cmds as mc 7 | 8 | # Local import 9 | import utils 10 | reload(utils) 11 | 12 | SHAPE_LIBRARY_PATH = "C:/PATH_TO_LIBRARY" 13 | 14 | 15 | def getShape(crv=None): 16 | '''Returns a dictionary containing all the necessery information for rebuilding the passed in crv.''' 17 | crvShapes = validateCurve(crv) 18 | 19 | crvShapeList = [] 20 | 21 | for crvShape in crvShapes: 22 | crvShapeDict = { 23 | "points": [], 24 | "knots": [], 25 | "form": mc.getAttr(crvShape + ".form"), 26 | "degree": mc.getAttr(crvShape + ".degree"), 27 | "colour": mc.getAttr(crvShape + ".overrideColor") 28 | } 29 | points = [] 30 | 31 | for i in range(mc.getAttr(crvShape + ".controlPoints", s=1)): 32 | points.append(mc.getAttr(crvShape + ".controlPoints[%i]" % i)[0]) 33 | 34 | crvShapeDict["points"] = points 35 | crvShapeDict["knots"] = utils.getKnots(crvShape) 36 | 37 | crvShapeList.append(crvShapeDict) 38 | 39 | return crvShapeList 40 | 41 | 42 | def setShape(crv, crvShapeList): 43 | '''Creates a new shape on the crv transform, using the properties in the crvShapeDict.''' 44 | crvShapes = validateCurve(crv) 45 | 46 | oldColour = mc.getAttr(crvShapes[0] + ".overrideColor") 47 | mc.delete(crvShapes) 48 | 49 | for i, crvShapeDict in enumerate(crvShapeList): 50 | tmpCrv = mc.curve(p=crvShapeDict["points"], k=crvShapeDict["knots"], d=crvShapeDict["degree"], per=bool(crvShapeDict["form"])) 51 | newShape = mc.listRelatives(tmpCrv, s=1)[0] 52 | mc.parent(newShape, crv, r=1, s=1) 53 | 54 | mc.delete(tmpCrv) 55 | newShape = mc.rename(newShape, crv + "Shape" + str(i + 1).zfill(2)) 56 | 57 | mc.setAttr(newShape + ".overrideEnabled", 1) 58 | 59 | if "colour" in crvShapeDict.keys(): 60 | setColour(newShape, crvShapeDict["colour"]) 61 | else: 62 | setColour(newShape, oldColour) 63 | 64 | 65 | def validateCurve(crv=None): 66 | '''Checks whether the transform we are working with is actually a curve and returns it's shapes''' 67 | if mc.nodeType(crv) == "transform" and mc.nodeType(mc.listRelatives(crv, c=1, s=1)[0]) == "nurbsCurve": 68 | crvShapes = mc.listRelatives(crv, c=1, s=1) 69 | elif mc.nodeType(crv) == "nurbsCurve": 70 | crvShapes = mc.listRelatives(mc.listRelatives(crv, p=1)[0], c=1, s=1) 71 | else: 72 | mc.error("The object " + crv + " passed to validateCurve() is not a curve") 73 | return crvShapes 74 | 75 | 76 | def loadFromLib(shape=None): 77 | '''Loads the shape data from the shape file in the SHAPE_LIBRARY_PATH directory''' 78 | path = os.path.join(SHAPE_LIBRARY_PATH, shape + ".json") 79 | data = utils.loadData(path) 80 | return data 81 | 82 | 83 | def saveToLib(crv=None, 84 | shapeName=None): 85 | '''Saves the shape data to a shape file in the SHAPE_LIBRARY_PATH directory''' 86 | crvShape = getShape(crv=crv) 87 | path = os.path.join(SHAPE_LIBRARY_PATH, re.sub("\s", "", shapeName) + ".json") 88 | for shapeDict in crvShape: 89 | shapeDict.pop("colour", None) 90 | utils.saveData(path, crvShape) 91 | 92 | 93 | def setColour(crv, colour): 94 | '''Sets the overrideColor of a curve''' 95 | if mc.nodeType(crv) == "transform": 96 | crvShapes = mc.listRelatives(crv) 97 | else: 98 | crvShapes = [crv] 99 | for crv in crvShapes: 100 | mc.setAttr(crv + ".overrideColor", colour) 101 | 102 | 103 | def getColour(crv): 104 | '''Returns the overrideColor of a curve''' 105 | if mc.nodeType(crv) == "transform": 106 | crv = mc.listRelatives(crv)[0] 107 | return mc.getAttr(crv + ".overrideColor") 108 | -------------------------------------------------------------------------------- /functions.py: -------------------------------------------------------------------------------- 1 | '''This module contains functions to provide a higher level of interaction with the commands in the manager.py file. 2 | The commands in this file are meant to be the ones used by the users, so they should be used in the UI.''' 3 | import functools 4 | import os 5 | 6 | import maya.cmds as mc 7 | 8 | # Local import 9 | import manager 10 | reload(manager) 11 | 12 | 13 | def getAvailableControlShapes(): 14 | '''Returns a list of the available control shapes in the specified library. Each element 15 | of the list is a tuple containing the label (name) of the controlShape and a reference 16 | to the command to assign that shape via functools.partial''' 17 | lib = manager.SHAPE_LIBRARY_PATH 18 | return [(x.split(".")[0], functools.partial(assignControlShape, x.split(".")[0])) for x in os.listdir(lib)] 19 | 20 | 21 | def getAvailableColours(): 22 | '''Returns a list of the available 32 colours for overrideColor in maya. Each element 23 | of the list is a tuple containig the label, reference to the command which assigns the 24 | colour and the name of an image to be used as an icon''' 25 | return [("index" + str(i).zfill(2), functools.partial(assignColour, i), "shapeColour" + str(i).zfill(2) + ".png") for i in range(32)] 26 | 27 | 28 | def assignColour(*args): 29 | '''Assigns args[0] as the overrideColor of the selected curves''' 30 | for each in mc.ls(sl=1, fl=1): 31 | manager.setColour(each, args[0]) 32 | 33 | 34 | def assignControlShape(*args): 35 | '''Assigns args[0] as the shape of the selected curves''' 36 | sel = mc.ls(sl=1, fl=1) 37 | for each in sel: 38 | manager.setShape(each, manager.loadFromLib(args[0])) 39 | mc.select(sel) 40 | 41 | 42 | def saveCtlShapeToLib(*args): 43 | '''Saves the selected shape in the defined control shape library''' 44 | result = mc.promptDialog(title="Save Control Shape to Library", 45 | m="Control Shape Name", 46 | button=["Save", "Cancel"], 47 | cancelButton="Cancel", 48 | dismissString="Cancel") 49 | if result == "Save": 50 | name = mc.promptDialog(q=1, t=1) 51 | manager.saveToLib(mc.ls(sl=1, fl=1)[0], name) 52 | rebuildUI() 53 | 54 | 55 | def mirrorCtlShapes(*args): 56 | '''Mirrors the selected control's shape to the other control on the other side''' 57 | sel = mc.ls(sl=1, fl=1) 58 | for ctl in sel: 59 | if ctl[0] not in ["L", "R"]: 60 | continue 61 | search = "R_" 62 | replace = "L_" 63 | if ctl[0] == "L": 64 | search = "L_" 65 | replace = "R_" 66 | shapes = manager.getShape(ctl) 67 | for shape in shapes: 68 | shape.pop("colour") 69 | manager.setShape(ctl.replace(search, replace), shapes) 70 | _flipCtlShape(ctl.replace(search, replace)) 71 | mc.select(sel) 72 | 73 | 74 | def copyCtlShape(*args): 75 | '''Copies the selected control's shape to a global variable for pasting''' 76 | global ctlShapeClipboard 77 | ctlShapeClipboard = manager.getShape(mc.ls(sl=1, fl=1)[0]) 78 | for ctlShape in ctlShapeClipboard: 79 | ctlShape.pop("colour") 80 | 81 | 82 | def pasteCtlShape(*args): 83 | '''Assigns the control's shape from the ctlShapeClipboard global variable 84 | to the selected controls''' 85 | sel = mc.ls(sl=1, fl=1) 86 | for each in sel: 87 | manager.setShape(each, ctlShapeClipboard) 88 | mc.select(sel) 89 | 90 | 91 | def flipCtlShape(*args): 92 | '''Flips the selected control shapes to the other side in all axis''' 93 | sel = mc.ls(sl=1, fl=1) 94 | for each in sel: 95 | _flipCtlShape(each) 96 | mc.select(sel) 97 | 98 | 99 | def flipCtlShapeX(*args): 100 | '''Flips the selected control shapes to the other side in X''' 101 | sel = mc.ls(sl=1, fl=1) 102 | for each in sel: 103 | _flipCtlShape(each, [-1, 1, 1]) 104 | mc.select(sel) 105 | 106 | 107 | def flipCtlShapeY(*args): 108 | '''Flips the selected control shapes to the other side in Y''' 109 | sel = mc.ls(sl=1, fl=1) 110 | for each in sel: 111 | _flipCtlShape(each, [1, -1, 1]) 112 | mc.select(sel) 113 | 114 | 115 | def flipCtlShapeZ(*args): 116 | '''Flips the selected control shapes to the other side in Z''' 117 | sel = mc.ls(sl=1, fl=1) 118 | for each in sel: 119 | _flipCtlShape(each, [1, 1, -1]) 120 | mc.select(sel) 121 | 122 | 123 | def _flipCtlShape(crv=None, axis=[-1, -1, -1]): 124 | '''Scales the points of the crv argument by the axis argument. This function is not meant to be 125 | called directly. Look at the flipCtlShape instead.''' 126 | shapes = manager.getShape(crv) 127 | newShapes = [] 128 | for shape in shapes: 129 | for i, each in enumerate(shape["points"]): 130 | shape["points"][i] = [each[0] * axis[0], each[1] * axis[1], each[2] * axis[2]] 131 | newShapes.append(shape) 132 | manager.setShape(crv, newShapes) 133 | mc.select(crv) 134 | 135 | 136 | def rebuildUI(*args): 137 | '''Rebuilds the UI defined in managerUI.py''' 138 | mc.evalDeferred(""" 139 | import controlShapeManager 140 | reload(controlShapeManager) 141 | """) 142 | --------------------------------------------------------------------------------