BroTools Snippets #03 – Mirror FFD Box

Another small script that can be of any help for others. It mirrors FFD box shape from one FFD to another. Useful for, for example, cartony eye rigs. I think anyone who knows a little python in maya should be able to write this one, but for those who just start with Python in Maya or those who are just lazy, here it is.

Tried to comment the hell out of it.

Mirror ffd deformer shape. Useful for cartoon eyes.
by Michael Davydov

Currently only works with mirroring across X axis.

    1. Copy your ffdLattice and ffdBase objects. Change TranslateX to the opposite value. 
    This means that you basically need to multiply current TranslateX by (-1). 
    Basically just add a minus sign if it's positive value, and remove it if it's negative value.
    2. Change names in the script below, FROM and TO.
    3. Run the script.

import maya.cmds as cmds # As usual, import maya's python bindings
import time # This is Python's time module, just for fun

FROM = "ffd_eye_R_Lattice" # Change this to the FFD object name you need to mirror FROM
TO = "ffd2Lattice" # Change this to the FFD object name you need to mirror TO

# Now, let's get number of S, T and U divisions from the "FROM" lattice.
# I'm going to use dictionary here, to save some space and time.
divisions = {}
for ax in ['s','t','u']:
    divisions[ax+"Divisions"] = cmds.getAttr(FROM+".{0}Divisions".format(ax))

# Make sure that target have the same number. Here we have two options,
# we can either force change number of divisions for user, or we can
# warn him. I'll do both. Why not?

# For each division check if there is the same number.
matching = True
confirm = None
for division, value in divisions.iteritems():
    if cmds.getAttr(TO+"."+division) != value:
        matching = False
if not matching:
    # Call the confirm dialog, thank you maya for built-in command.
    confirm = cmds.confirmDialog( title='Confirm', message='Number of divisions do not match. Script will only work with matching FFD divisions. Change division number or target ffd to match the source?', button=['Yes','No'], defaultButton='Yes', cancelButton='No', dismissString='No' )

if confirm=="Yes" or matching:
    for division, value in divisions.iteritems():
        cmds.setAttr(TO+"."+division, value)

    # Iterate over all points
    for S in range (0,3):
        for T in range (0,3):
            for U in range (0,3):
                print "//", i,o,p
                # Get position of a point
                pos = cmds.xform('{0}.pt[{1}][{2}][{3}]'.format(FROM, S,T,U), q=1, ws=1, t=1)
                pos[0] = pos[0]*(-1) # Reverse x position value
                cmds.xform('{0}.pt[{1}][{2}][{3}]'.format(TO, 2-S,T,U), ws=1, t=pos) # Paste position to another object's opposite point
                # Make progress visible.
                cmds.refresh() # Refresh the viewport to see what's going on
                time.sleep(0.03) # Wait for a few milliseconds, otherwise the script would work instantly. Remove these two lines for production use.
    print "// User canceled"


Non-3D Python exercise: Instaload

Well, this is not related to 3D or CG, but it’s Python!

Was bored one day, and had to download a couple of images from instagram. is great, but it only allows you download images, and only one by one. So, I spent a few hours writing a little web-app to download multiple images and even videos from instagram. So, here it is:

Using it is simple. Just open required instagram photos or images on your pc, copy and paste links into the app, each link on a separate line, and click download button. You should then see download progress, and in the end you’ll get a download link for your zip archive with downloaded files. It will be there for an hour. Heroku has a limit of 300MB of disk space, so this app is not ready for heavy production use because of that limitation, if it somehow becomes popular, I’ll have to move it somewhere else, or change the logic to, for example, use some file sharing service to upload files instead…

Just deployed it on heroku, and I’ll be honest – deployment was the hardest part! I already had a lot of experience with Python, Flask and socketio programming, while writing my own personal home assistant. But my little AI friend is running on my local network, and I had no need to even try to run it on production server, flask’s built in test server works perfectly fine for this.

But with heroku I had to switch to gunicorn, and here comes the fun part – you have to use gunicorn 18.0, not 19.x, as, in fact, stated in the docs:¬† but it’s easy to miss.


BroTools Snippets #03 – PySide context menu for QLineEdit and other elements…

Well, some time ago I decided to go with PySide code for all my UIs instead of native maya.cmds functions for menu-building. I was attracted to the freedom in creating and styling of those menus, and the fact that Jeremy Ernst did a lot of menus with PySide. And another fact that knowing PySide I can not only write UIs for maya, but also for different standalone python tools. Which is cool. But if you just need a menu – go with cmds. Don’t bother with PySide.

Anyway, with that said, I still prefer to use PySide. And recently I was banging my head against the wall, trying to add a simple context menu to QLineEdit. To allow pasting some preset text into QLineEdit, for BroSelector tool.

And Finally, it worked!

Here is the full code related to it. I skip imports and window creation. Just the relevant stuff.


#Creating the actual QLineEdit. I use from QtGui import *, so I don't need to write QtGui.QLineEdit, just QLineEdit, mind that.
self.type = QLineEdit("transform")
# Adding context menu to line edit
# Creating action. Make as many as you like
self.actionHello = QAction(self)

# Creating Menu = QMenu(self)
# Adding action to menu. Add as many as you like

# First we need to change our element's Context Menu Policy to Custom.

# Now we catch basically the right-click event, the customContextMenuRequested event, and assing our own handler (function) for it.

#And here goes the handler function.
def contextMenuRequested(self, point):
    # the point variable (which you can call whatever you like actually) is passed to this function as first arg, so we can use it in the next line.

And thats it. YAY! Saving it here, so I won’t lose it, and in case it is useful to someone.