├── .gitignore ├── README.rst ├── bin └── tractor-spool.py ├── build_deb.sh ├── deploy_deb.sh ├── setup.py ├── src ├── lib │ ├── __init__.py │ └── tractor │ │ ├── __init__.py │ │ ├── api │ │ ├── __init__.py │ │ ├── render.py │ │ ├── serialize.py │ │ └── tasktree.py │ │ ├── ordereddict.py │ │ └── plugin │ │ ├── __init__.py │ │ ├── maya.py │ │ ├── nuke.py │ │ └── shake.py └── ui │ ├── maya │ ├── tractorUI.txt │ └── tractorUI │ │ └── scripts │ │ ├── pyQtSubmitMaya.py │ │ ├── pySubmitMaya.py │ │ ├── userSetup.py │ │ └── utils.py │ └── nuke │ └── tractorUI │ ├── icons │ ├── icon-large.png │ └── icon-small.png │ ├── menu.py │ └── python │ ├── pyQtSubmitDialog.ui │ ├── pyQtSubmitNuke.py │ └── simpleSubmitNuke.py └── test ├── exampleScript.py ├── testSpool.py └── testTaskTree.py /.gitignore: -------------------------------------------------------------------------------- 1 | # git-ls-files --others --exclude-from=.git/info/exclude 2 | # Lines that start with '#' are comments. 3 | # For a project mostly in C, the following would be a good set of 4 | # exclude patterns (uncomment them if you want to use them): 5 | # *.[oa] 6 | # *~ 7 | *.pyc 8 | build/ 9 | dist/ 10 | sdist/ 11 | *egg/ 12 | *.deb 13 | #build_deb.sh 14 | *.config 15 | *.creator 16 | *.creator.user 17 | *.files 18 | *.includes 19 | *.swp 20 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | This library represents a work in progress wrapper for tractor/alfred job submission scripts. 2 | 3 | The current version works as a command line only callable script, located in /bin or as a python module tractor.api 4 | which can be imported and used to create a simple submission to tractor. 5 | 6 | *Applogies* for the state of the tests and broken UI code. These could be taken out of the repo but since they are not installed with the lib I thought them 7 | harmless to leave there for reference. 8 | 9 | Command Line Use 10 | ------------------------------ 11 | 12 | The library comes with a rather unimaginativly titled script called *tractor-spool.py*. This conflicts badly with the name 13 | which Pixar uses for their submission script, but at the time my mind went blank and now artists are using it at BB and the name 14 | as stuck. 15 | 16 | Useage of the cli is fairly easy, although I have to admit that if you decide to run it with all the options you'd like to that it can become a little long winded. 17 | An example for the non-faint of heart:: 18 | 19 | tractor-spool.py maya -j '251_gl_060:LS_tran' \ 20 | --imagename='lighting/v005///1024x778/251_gl_060_lighting---v005' \ 21 | --project=/mnt/muxfs/extratime/251/251_gl_060/maya \ 22 | -r 6-34 -c 1 -t 8 \ 23 | --layers=MasterBeautyLayer,SpecularLayer,BackgroundLayer \ 24 | /mnt/muxfs/extratime/251/251_gl_060/maya/scenes/251_gl_060_lighting/251_gl_060_lighting_v003-06_LS_trans_arms.ma 25 | 26 | Virtualenv 27 | ~~~~~~~~~~~~ 28 | 29 | The cli has been written making use of virtualenv to protect it from any changes which can and do often occur. If you don't want to use virtualenv then simply comment out the lines 30 | which activate it and you'll be sorted. 31 | 32 | Unit Tests 33 | ~~~~~~~~~~~~ 34 | 35 | Provided are a few barebones unit tests for ensuring that the library is still behaving correctly. I've been using python-nose to run the tests. 36 | 37 | TaskTree Module 38 | ------------------------ 39 | 40 | This example should demonstrate the general syntax:: 41 | 42 | from tractor.api import Job, Task, RemoteCmd, Serializer 43 | 44 | myjob = Job('Simple Job Script') 45 | 46 | task1 = Task( '1', 'Task number 1' ) 47 | cmd1 = task1.addRemoteCmd(service='Default') 48 | cmd1.addExecutable('echo') 49 | cmd1.addOption('hello') 50 | 51 | myjob.addTask( task1 ) 52 | 53 | task2 = myjob.addTask( '2' ) 54 | cmd2 = RemoteCmd('echo hello') 55 | task2.addRemoteCmd( cmd2 ) 56 | 57 | serializer = Serializer( myjob ) 58 | serializer.spool( 'stdout' ) 59 | 60 | TaskTree Node Types 61 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~ 62 | 63 | Job 64 | This is the root node for an task tree. It can hold any number of task nodes. 65 | 66 | Task 67 | Individual task nodes. Each task can hold any number of subtasks or commands. 68 | 69 | Command/RemoteCommand 70 | The remote command is a subclass of command, providing additional service information for remote processing. 71 | 72 | Serializer 73 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~ 74 | 75 | Rather than overload the print methods on each of the task tree nodes a serializer instead takes the tasktree when spool'd 76 | iterated over the tree converting the attributes on each of the nodes into a job script. The spool method can either print 77 | out the resulting script to a file on disk, stdout or to disk and then call the pixar tractor-spool.py script to submit the job 78 | to the tractor engine. 79 | 80 | The rationale for creating the serializer this way has been to allow room to change the serialization option to JSON or plain 81 | python pickles easily without the need to make changes to the main task tree code. 82 | 83 | Maya/Nuke/Shake Tasktree Convience Classes 84 | -------------------------------------------------------------------- 85 | 86 | Common task types such as those used to create a Nuke render, or Mental Ray for Maya as encapsulated into helper classes which can imported from tractor.api. 87 | These helpers allow for two dictionaries to be passed as constructor arguments. The first argument contains options for the job as a whole, such as the job name and number of 88 | frames to render. The command args contains options which are specific to the renderer, such as a string containing a list of node names to render from a nuke script. 89 | 90 | Example:: 91 | 92 | from tractor.api import Nuke 93 | 94 | jobargs = {'title':'A simple render'} 95 | cmdargs = {'file':'/path/to/my/scene/file.nk'} 96 | 97 | renderer = Nuke( jobargs, cmdargs ) 98 | renderer.buildTaskTree() 99 | renderer.spool( 'tractor', startpaused=options.paused ) 100 | 101 | GUIs. They don't exist. (Yet). 102 | ------------------------------------------ 103 | 104 | On the todo list is the creation of stable ui's for maya/nuke to make submission easier. Presently they are however low on the list since by forcing artists t use the commandline 105 | we've been able to make them think more about what they submit to the farm, rather than throwing dross onto it. -------------------------------------------------------------------------------- /bin/tractor-spool.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | Python wrapper around tractor.glue library. Permits the command line launching of 5 | renders onto a tractor render farm. 6 | 7 | example usages: 8 | 9 | tractor-spool.py nuke -s 1 -e 20 -b 5 -t 8 /path/to/scene/file.nk 10 | 11 | tractor-spool.py -i 12 | Tractor Spool interactive session. Enter the command you wish to execute: 13 | > nuke 14 | 15 | Generic Supported Options: 16 | 17 | -j --job, job title default = 'Generic Job' 18 | -r --range frame range default '1' 19 | -c --chunk, chunk size default = 5 20 | -p --preview, preview render frames first default = False 21 | 22 | Maya Supported Options: 23 | 24 | --imagename name of the image to render 25 | --project, path to the maya project 26 | -t --threads, threads to use default = 8 27 | -l --layers, layers to render 28 | 29 | Nuke Supported Options: 30 | 31 | -q --quiet, quiet mode default = True 32 | --no-quiet, don't be quiet 33 | -t --threads, threads to use default = 4 34 | -n --nodes, nodes to render 35 | -f --fullsize, force fullsize default = True 36 | 37 | """ 38 | 39 | #virtual environment setup -- If you don't use virtualenv then you'll probably want to comment this bit out. 40 | activate_this = '/opt/baseblack/python2.6/bin/activate_this.py' 41 | execfile(activate_this, dict(__file__=activate_this)) 42 | 43 | import sys 44 | import os 45 | import getpass 46 | import time 47 | import optparse 48 | 49 | from tractor.api import Nuke, Maya, Shake 50 | from tractor.api import Serializer 51 | 52 | __version__ = '1.0.0' 53 | 54 | def buildArgumentDict( options, filepath ): 55 | """Returns two dictionaries, jobargs and command. These are the arguments to the tractor.glue.render derived objects.""" 56 | jobargs = {} 57 | cmdargs = {} 58 | 59 | jobargs['title'] = options.title if options.title else filepath 60 | if options.imagename: jobargs['imagename'] = options.imagename 61 | if options.chunksize: jobargs['chunksize'] = options.chunksize 62 | 63 | jobargs['range'] = options.range 64 | jobargs['preview'] = options.preview 65 | 66 | jobargs['user'] = getpass.getuser() 67 | jobargs['timestamp'] = int( time.time() ) 68 | 69 | if options.projectpath: jobargs['projectpath'] = options.projectpath 70 | if options.threads: cmdargs['threads'] = options.threads 71 | if options.layers: jobargs['layers'] = options.layers 72 | 73 | cmdargs['quiet'] = options.quiet 74 | cmdargs['fullsize'] = options.fullsize 75 | 76 | if options.threads: cmdargs['threads'] = options.threads 77 | if options.nodes: cmdargs['nodes'] = options.nodes 78 | 79 | if options.proxyscale: cmdargs['proxyscale'] = options.proxyscale 80 | 81 | if options.doprogress: 82 | jobargs['progress'] = 'tkrProgress' 83 | 84 | cmdargs['file'] = filepath 85 | 86 | return ( jobargs, cmdargs ) 87 | 88 | # Option fun, or hell it's sometimes a little hard to tell. 89 | # The options which are supported here are the same as those which are currently exposed through tractor.glue 90 | # As further renderers and options are supported in tractor.glue they should be added to here to give a one stop 91 | # shop for command line rendering. 92 | 93 | parser = optparse.OptionParser( version=__version__, usage="%prog [options] [nuke/maya/shake] /path/to/my/scene/file" ) 94 | parser.disable_interspersed_args() 95 | 96 | parser.add_option("--paused", dest="paused", action="store_true", default=False, help="Send the job to tractor in a paused state. Usefull for queuing jobs." ) 97 | 98 | parser.add_option("-j","--job", dest="title", type="string", default='', help="Title to name the job by in tractor." ) 99 | parser.add_option("-r","--range", dest="range", type="string", default='1', help="Frame range to render. In the form 1-10x2. [Default '1']" ) 100 | parser.add_option("-c","--chunk", dest="chunksize", type="int", default=0, help="Size for each chunk for frames. For slow renders reduce this value. [Default maya:5 nuke:10]" ) 101 | parser.add_option("-t","--threads", dest="threads", type="int", default=0, help="Number of threads to render with." ) 102 | parser.add_option("--preview", action="store_true", dest="preview", default=False, help="perform a preview render of a few frames from the sequence. [Default False]" ) 103 | parser.add_option("--progress", action="store_true", dest="doprogress", default=False, help="Name of the progress monitor script to attach to the process. [Default False]" ) 104 | 105 | mayaOptGroup = optparse.OptionGroup( parser, 'Maya Supported Options' ) 106 | mayaOptGroup.add_option("-p","--project", dest="projectpath", type="string", default='', help="Path to the maya project file for this scene." ) 107 | mayaOptGroup.add_option("-l","--layers", dest="layers", type="string", default='', help="Names of the layers to render. Written as a comma separated list layer1,layer2,layer3 " ) 108 | mayaOptGroup.add_option("--imagename", dest="imagename", type="string", default='default', help="Name to render the image as. [Default 'default']" ) 109 | 110 | nukeOptGroup = optparse.OptionGroup( parser, 'Nuke Supported Options' ) 111 | nukeOptGroup.add_option("-q","--quiet", action="store_true", dest="quiet", default=False, help="Be quiet and do not provide extra output. [Default False]" ) 112 | nukeOptGroup.add_option("--no-quiet", action="store_false", dest="quiet", help="Be loud and give lots of message output. [Default True]" ) 113 | nukeOptGroup.add_option("-f","--fullsize", action="store_true", dest="fullsize", help="Force the render to be fullsize, ignore proxies. [Default True]" ) 114 | nukeOptGroup.add_option("-n","--nodes", dest="nodes", type="string", default='', help="Names of nodes to render." ) 115 | 116 | shakeOptGroup = optparse.OptionGroup( parser, 'Shake Supported Options' ) 117 | parser.add_option("--proxyscale", dest="proxyscale", type="int", default=0, help="Proxy scale to render at. [Default 1]" ) 118 | 119 | parser.add_option_group( mayaOptGroup ) 120 | parser.add_option_group( nukeOptGroup ) 121 | 122 | options, args = parser.parse_args() 123 | 124 | if len(args) < 2: 125 | print "Incorrect arguments. Please provide the renderer you wish to use and path to the scene file." 126 | print parser.usage 127 | sys.exit(1) 128 | 129 | renderer = args[0] 130 | filepath = args[1] 131 | 132 | jobargs, cmdargs = buildArgumentDict( options, filepath ) 133 | 134 | # I'd really prefer to write this so that an arbitary renderer value can be added without having to update this script. 135 | # Unfortunatly that may not be easily possible without writing confusing code. 136 | 137 | if renderer == 'maya': 138 | renderObject = MRfMaya( jobargs, cmdargs ) 139 | if renderer == 'nuke': 140 | renderObject = Nuke( jobargs, cmdargs ) 141 | if renderer == 'shake': 142 | renderObject = Shake( jobargs, cmdargs ) 143 | 144 | renderObject.buildTaskTree() 145 | 146 | renderObject.spool( 'tractor', startpaused=options.paused ) 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | -------------------------------------------------------------------------------- /build_deb.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # The tractor-api needs to be available to 3 processes. 4 | # 5 | # 1) Cmdline access via python scripts on in the interactive interpreter 6 | # 2) Within Maya 7 | # 3) Within Nuke 8 | # 9 | # This script creates 3 debs to accomodate these requirements. Packaging the 10 | # api into the appropriate structure for each application. 11 | # 12 | #Name #Version 13 | # tractor-api3.0 3.0.0-baseblack-r1234 14 | # nuke6.2-tractor-api3.0 3.0.0-baseblack-r1234 15 | # maya2011-tractor-api3.0 3.0.0-baseblack-r1234 16 | 17 | 18 | ### CLEAN UP ### 19 | # 20 | # 21 | 22 | if [ "$1" == "clean" ]; then 23 | echo Cleaning... 24 | rm *.deb 25 | rm $PWD/dist -r 26 | exit #quit the script 27 | fi 28 | 29 | 30 | ### BASIC SETUP ### 31 | # 32 | # 33 | 34 | if [ ! -n "$PLUGIN_NAME" ]; then PLUGIN_NAME="tractor-api"; fi 35 | if [ ! -n "$PLUGIN_VERSION" ]; then PLUGIN_VERSION=4.2.0; fi 36 | if [ ! -n "$PLUGIN_SHORT_VERSION" ]; then PLUGIN_SHORT_VERSION=`echo $PLUGIN_VERSION | sed 's/\.[0-9]$//'`; fi 37 | if [ ! -n "$BUILD_NUMBER" ]; then BUILD_NUMBER=1; fi 38 | if [ ! -n "$MAYA_VERSION" ]; then MAYA_VERSION=2011; fi 39 | if [ ! -n "$NUKE_VERSION" ]; then NUKE_VERSION=6.3; fi 40 | 41 | RELEASE_VERSION="$PLUGIN_VERSION-baseblack-r$BUILD_NUMBER" 42 | DESCRIPTION="Provides access to the tractor-api" 43 | 44 | mkdir -p $PWD/dist/release 45 | mkdir -p $PWD/dist/maya 46 | mkdir -p $PWD/dist/nuke 47 | 48 | 49 | ### MAYA PLUGIN ### 50 | # Built for a specific version of maya, a module is created which is loaded using a .mod file 51 | # 52 | 53 | mkdir -p $PWD/dist/maya/opt/baseblack/autodesk/maya/$MAYA_VERSION/modules 54 | mkdir -p $PWD/dist/maya/opt/baseblack/autodesk/maya/$MAYA_VERSION/plugins/$PLUGIN_NAME/$PLUGIN_SHORT_VERSION/scripts 55 | cp -r src/lib/* $PWD/dist/maya/opt/baseblack/autodesk/maya/$MAYA_VERSION/plugins/$PLUGIN_NAME/$PLUGIN_SHORT_VERSION/scripts/ 56 | 57 | # creates module for maya to load. 58 | echo "+ $PLUGIN_NAME $PLUGIN_SHORT_VERSION /opt/baseblack/autodesk/maya/$MAYA_VERSION/plugins/$PLUGIN_NAME/$PLUGIN_SHORT_VERSION" \ 59 | > $PWD/dist/maya/opt/baseblack/autodesk/maya/$MAYA_VERSION/modules/$PLUGIN_NAME-$PLUGIN_SHORT_VERSION.mod 60 | 61 | REPLACES="maya${MAYA_VERSION}-${PLUGIN_NAME}${PLUGIN_SHORT_VERSION} (<< ${RELEASE_VERSION})" 62 | 63 | fpm -n maya${MAYA_VERSION}-${PLUGIN_NAME}${PLUGIN_SHORT_VERSION} \ 64 | -v $RELEASE_VERSION \ 65 | -t deb \ 66 | -s dir \ 67 | -C dist/maya \ 68 | --description "$DESCRIPTION" \ 69 | --url "http://tech.baseblack.com/" \ 70 | --replaces "$REPLACES" \ 71 | --maintainer "$DEBEMAIL" 72 | 73 | 74 | ### NUKE PLUGIN ### 75 | # 76 | # 77 | 78 | mkdir -p $PWD/dist/nuke/opt/baseblack/foundry/nuke/$NUKE_VERSION/plugins 79 | mkdir -p $PWD/dist/nuke/opt/baseblack/foundry/nuke/$NUKE_VERSION/plugins/$PLUGIN_NAME/$PLUGIN_SHORT_VERSION/python 80 | 81 | cp -r src/lib/* $PWD/dist/nuke/opt/baseblack/foundry/nuke/$NUKE_VERSION/plugins/$PLUGIN_NAME/$PLUGIN_SHORT_VERSION/python 82 | 83 | REPLACES="nuke${NUKE_VERSION}-${PLUGIN_NAME}${PLUGIN_SHORT_VERSION} (<< ${RELEASE_VERSION})" 84 | 85 | fpm -n nuke${NUKE_VERSION}-${PLUGIN_NAME}${PLUGIN_SHORT_VERSION} \ 86 | -v $RELEASE_VERSION \ 87 | -t deb \ 88 | -s dir \ 89 | -C dist/nuke \ 90 | --description "$DESCRIPTION" \ 91 | --url "http://tech.baseblack.com/" \ 92 | --replaces "$REPLACES" \ 93 | --maintainer "$DEBEMAIL" 94 | 95 | 96 | ### SHARED LIBRARY ### 97 | # 98 | # 99 | 100 | mkdir -p $PWD/dist/release/opt/baseblack/python2.6/lib/python2.6/site-packages 101 | cp -r src/lib/tractor $PWD/dist/release/opt/baseblack/python2.6/lib/python2.6/site-packages/ 102 | 103 | sed -i "s/VERSION/${PLUGIN_VERSION}/g" $PWD/dist/release/opt/baseblack/python2.6/lib/python2.6/site-packages/tractor/__init__.py 104 | sed -i "s/muxfs/shows/g" $PWD/dist/release/opt/baseblack/python2.6/lib/python2.6/site-packages/tractor/__init__.py 105 | 106 | REPLACES="${PLUGIN_NAME}${PLUGIN_SHORT_VERSION} (<< ${RELEASE_VERSION})" 107 | 108 | fpm -n ${PLUGIN_NAME}${PLUGIN_SHORT_VERSION} \ 109 | -v $RELEASE_VERSION \ 110 | -t deb \ 111 | -s dir \ 112 | -C dist/release \ 113 | --description "$DESCRIPTION" \ 114 | --url "http://tech.baseblack.com/" \ 115 | --replaces "$REPLACES" \ 116 | --maintainer "$DEBEMAIL" 117 | 118 | ### done ### 119 | -------------------------------------------------------------------------------- /deploy_deb.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Pushes a debian package into the apt repostory 4 | # 5 | 6 | if [ $# -ne "2" ]; then 7 | if [ "$COMPONENT" == "" ]; then 8 | echo "COMPONENT:" 9 | read COMPONENT 10 | else 11 | echo "component -> $COMPONENT" 12 | fi 13 | 14 | if [ "$DEB_FILE" == "" ]; then 15 | echo "DEB_FILE:" 16 | read DEB_FILE 17 | else 18 | echo "package file -> $DEB_FILE" 19 | fi 20 | else 21 | COMPONENT=$1 22 | DEB_FILE=$2 23 | fi 24 | 25 | reprepro -V --basedir /home/tech/repositories/apt/baseblack --component $COMPONENT includedeb ubuntu-lucid $DEB_FILE -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from distutils.core import setup 4 | setup(name='tractor-api', 5 | version='3.0', 6 | description='Tractor script making api in python', 7 | author='Andrew Bunday', 8 | author_email='andrew.bunday@baseblack.com', 9 | maintainer_email='requests@baseblack.com', 10 | packages=['tractor', 'tractor.api'], 11 | package_dir={'tractor':'src/lib/tractor'}, 12 | ) -------------------------------------------------------------------------------- /src/lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baseblack/Tractor-API/a59e5615e5d7ad1c1b2fb0c76366f01421501b06/src/lib/__init__.py -------------------------------------------------------------------------------- /src/lib/tractor/__init__.py: -------------------------------------------------------------------------------- 1 | __version__="VERSION" 2 | 3 | SPOOL_DIRECTORY="/mnt/shows/users/spool/tkr" -------------------------------------------------------------------------------- /src/lib/tractor/api/__init__.py: -------------------------------------------------------------------------------- 1 | from tasktree import Job, Task, RemoteCmd, Cmd 2 | from serialize import Serializer 3 | 4 | from tractor.plugin.maya import MRfMaya 5 | from tractor.plugin.nuke import Nuke 6 | from tractor.plugin.shake import Shake 7 | 8 | __all__ = [ 9 | 'MRfMaya', 10 | 'Nuke', 11 | 'Shake', 12 | 'Job', 13 | 'Task', 14 | 'RemoteCmd', 15 | 'Cmd', 16 | 'Serializer', 17 | ] -------------------------------------------------------------------------------- /src/lib/tractor/api/render.py: -------------------------------------------------------------------------------- 1 | import re 2 | import sys 3 | 4 | from tractor.ordereddict import OrderedDict 5 | from tractor.api.tasktree import Task 6 | from tractor.api.serialize import Serializer 7 | 8 | class RenderJob( object ): 9 | """Sets up the expected minimum dictionary of arguments expected for an empty job. """ 10 | 11 | def __init__( self, jobargs ): 12 | 13 | self.job = dict() 14 | 15 | # default values @optionally have these read from a config.... 16 | self.job['title'] = 'Generic Job' 17 | self.job['imagename'] = 'default' 18 | self.job['user'] = 'unknown.soldier' 19 | self.job['range'] = '1' 20 | self.job['chunksize'] = 5 21 | self.job['timestamp'] = '' 22 | self.job['weight'] = '2' 23 | self.job['check'] = False 24 | self.job['quicktime'] = False 25 | self.job['preview'] = False 26 | self.job['progress'] = '' 27 | 28 | if 'title' in jobargs: self.job['title'] = jobargs[ 'title' ] 29 | if 'imagename' in jobargs: self.job['imagename'] = jobargs[ 'imagename' ] 30 | if 'user' in jobargs: self.job['user'] = jobargs[ 'user' ] 31 | if 'range' in jobargs: self.job['range'] = jobargs[ 'range' ] 32 | if 'chunksize' in jobargs: self.job['chunksize'] = jobargs[ 'chunksize' ] 33 | if 'timestamp' in jobargs: self.job['timestamp'] = jobargs[ 'timestamp' ] 34 | if 'weight' in jobargs: self.job['weight'] = jobargs[ 'weight' ] 35 | if 'check' in jobargs: self.job['check'] = jobargs[ 'check' ] 36 | if 'quicktime' in jobargs: self.job['quicktime'] = jobargs[ 'quicktime' ] 37 | if 'preview' in jobargs: self.job['preview'] = jobargs[ 'preview' ] 38 | if 'progress' in jobargs: self.job['progress'] = jobargs[ 'progress' ] 39 | 40 | class RenderCommand( object ): 41 | """Sets up the expected minimum dictionary of arguments expected for an empty command. """ 42 | 43 | def __init__( self, cmdargs ): 44 | 45 | self.command = dict() 46 | self.command['file'] = cmdargs[ 'file' ] if 'file' in cmdargs else None 47 | 48 | # 49 | # This is the main class in this module. This class should only be used to construct plugins 50 | # for the api for applications such as nuke, maya, prman etc. Added functionallity which can be reused. 51 | # 52 | class Render( RenderJob, RenderCommand ): 53 | 54 | def __init__( self, jobargs, cmdargs ): 55 | 56 | RenderJob.__init__( self, jobargs ) 57 | RenderCommand.__init__( self, cmdargs ) 58 | 59 | self.splitFrameRange() 60 | 61 | def splitFrameRange( self ): 62 | 63 | self.job['framerange'] = [] 64 | 65 | for each_range in self.job['range'].split(',') : 66 | 67 | this = OrderedDict() 68 | 69 | args = re.findall( "([0-9]+)(-)?([0-9]+)?(x)?([0-9]+)?" , each_range )[0] 70 | 71 | if len( args[0] ): this['first'] = int( args[0] ); 72 | if len( args[1] ) and len( args[2] ): this['last'] = int( args[2] ); 73 | if len( args[3] ) and len( args[4] ): this['step'] = int( args[4] ); 74 | 75 | self.job['framerange'].append( this ) 76 | 77 | def previewTask( self ): 78 | 79 | previewtask = Task('Preview__') 80 | return previewtask 81 | 82 | def mainTask( self ): 83 | 84 | maintask = Task('Main__') 85 | 86 | for thisrange in self.job['framerange']: # ['1-10x2','11-20',25] 87 | if 'last' in thisrange: 88 | rangelength = thisrange['last'] - thisrange['first'] + 1 #+1 for inclusive range 89 | 90 | if rangelength > self.job['chunksize'] and self.job['chunksize'] != 0: 91 | 92 | #number of frames is greater than a single chunk. need to split 93 | chunkCount = int( rangelength / self.job['chunksize'] ) 94 | chunkRemainder = rangelength % self.job['chunksize'] 95 | 96 | for chunk in range( chunkCount ): #frame range for the chunk (firstframe, lastframe, step) 97 | chunkrange = dict() 98 | chunkrange['first'] = thisrange['first'] + ( chunk * self.job['chunksize'] ) 99 | chunkrange['last'] = chunkrange['first'] + ( self.job['chunksize'] - 1 ) # inclusive of the first frame 100 | 101 | if 'step' in thisrange: 102 | chunkrange['step'] = thisrange['step'] 103 | maintask.addTask( self.multiFrameTask( chunkrange ) ) # this is not strictly correct. If len(chunkrange)==1 then a single frame should be used. 104 | 105 | if chunkRemainder : 106 | if chunkRemainder > 1: 107 | remainder_range = {} 108 | remainder_range['first'] = thisrange['first'] + ( self.job['chunksize'] * chunkCount ) 109 | remainder_range['last'] = thisrange['last'] 110 | if 'step' in thisrange: 111 | remainder_range['step'] = thisrange['step'] 112 | maintask.addTask( self.multiFrameTask( remainder_range ) ) 113 | 114 | else: 115 | maintask.addTask( self.singleFrameTask( thisrange['last'] ) ) 116 | 117 | else: 118 | maintask.addTask( self.multiFrameTask( thisrange ) ) #no chunking is required for the range 119 | 120 | else: 121 | maintask.addTask( self.singleFrameTask( thisrange['first'] ) ) #single frame task 122 | 123 | return maintask 124 | 125 | def spool( self, destination, startpaused=False ): 126 | 127 | #try: 128 | jobscript = Serializer( self.JobObj ) 129 | jobscript.serialize() 130 | jobscript.spool( destination, startpaused ) #where destination is either 'stdout', 'disk', or 'tractor' 131 | #except: 132 | # print "Error during spooling. Phew, could have segfaulted there." 133 | -------------------------------------------------------------------------------- /src/lib/tractor/api/serialize.py: -------------------------------------------------------------------------------- 1 | import time 2 | import subprocess 3 | import os 4 | 5 | from tractor.api.tasktree import TaskTree 6 | from tractor.api.tasktree import Job 7 | from tractor.api.tasktree import Task 8 | from tractor.api.tasktree import RemoteCmd 9 | from tractor.api.tasktree import Cmd 10 | 11 | # define for the location of the .tkr spool files. Stored in ../__init__.py 12 | from tractor import SPOOL_DIRECTORY 13 | 14 | class SerialError( Exception ): 15 | pass 16 | 17 | class SerializingError( SerialError ): 18 | """This exeception is raised when a non tractor oject is passed to 19 | the dump() method.""" 20 | pass 21 | 22 | class UnserializingError( SerialError ): 23 | """There was an error in unserializing a tractor script into a tractor 24 | object. Could be a bunch of reasons really.""" 25 | pass 26 | 27 | class SerializerBase( object ): 28 | """The base class for the serializer handles the iteration of the job tree and writing of the serialized 29 | tree to disk. It handles calling the submission script provided by the render manager""" 30 | 31 | def __init__(self, obj): 32 | if isinstance(obj, TaskTree): 33 | self.tree = obj 34 | 35 | self.now = int( time.time() ) # <-- should alos get this from the tree 36 | self.spooldir = os.path.join( SPOOL_DIRECTORY, self.tree.user ) 37 | self.spoolfile = "%s/%s.tkr" % (self.spooldir, self.now) 38 | 39 | super( SerializerBase, self).__init__() 40 | 41 | def serialize( self ): 42 | if hasattr( self, 'tree' ): 43 | self.jobscript = self.writeNode( self.tree) 44 | 45 | def writeNode( self, node ): 46 | 47 | if isinstance( node, Job ): 48 | output = self.writeJob( node ) 49 | elif isinstance( node, Task ): 50 | output = self.writeTask( node ) 51 | 52 | return output 53 | 54 | def writeJob( self, job ): 55 | 56 | output = self.jobtitle( job ) 57 | 58 | if job.after : output += self.subtasks( job ) 59 | if job.globalvars : output += self.init( job ) 60 | if job.tasks : output += self.subtasks( job ) 61 | if job.atleast > 0 : output += self.subtasks( job ) 62 | if job.atmost > 0 : output += self.subtasks( job ) 63 | if job.tags : output += self.tags( job ) 64 | if job.service : output += self.service( job ) 65 | 66 | return output 67 | 68 | def writeTask( self, task ): 69 | 70 | output = self.tasktitle( task ) 71 | 72 | if task.serialsubtasks : output += self.serialsubtasks( task ) 73 | if task.tasks : output += self.subtasks( task ) 74 | if task.commands : output += self.commands( task ) 75 | 76 | return output 77 | 78 | def writeCmds( self, task ): 79 | #hand over the task these commands belong to as it may hold some information needed by each command 80 | 81 | for command in task.commands: 82 | options = "".join([ '%s %s ' % (key, value) for key, value in command.flags.items() ]) 83 | 84 | if command.remote: 85 | output = self.writeRemoteCmd( command, options ) 86 | else: 87 | output = self.writeLocalCmd( command, options ) 88 | 89 | if command.tags: 90 | self.tags( command ) 91 | 92 | return output 93 | 94 | def spool( self, destination=None, startpaused=True, user=None ): 95 | """The destination can be any one of 'stdout'/'tractor'/'disk'. To launch the render but not have it 96 | start immediately set startpaused to True. To launch the job to run as another user set user to the 97 | username of the person to run as. 98 | """ 99 | 100 | if not hasattr(self, 'jobscript'): 101 | self.serialize() 102 | 103 | if destination is 'disk' or destination is 'tractor': 104 | if not os.path.exists( self.spooldir ): 105 | os.makedirs( self.spooldir ) 106 | 107 | p = open( self.spoolfile, "w") 108 | p.write( self.jobscript ) 109 | p.flush() 110 | p.close() 111 | 112 | if destination is 'tractor': 113 | argumentlist = ["/opt/pixar/tractor-blade/default/tractor-spool.py"] 114 | if startpaused: 115 | argumentlist.append( "--paused" ) 116 | if user: 117 | argumentlist.append( "--user=%s" % user ) 118 | 119 | argumentlist.append( self.spoolfile ) 120 | try: 121 | retcode = subprocess.call( argumentlist ) 122 | except Exception, e: 123 | print "Error Calling tractor-spool.py. Please refer to your Jersey Cow for udder help" 124 | print e 125 | 126 | if destination is 'stdout' or not destination : 127 | print self.jobscript 128 | 129 | class Serializer( SerializerBase ): 130 | """Tractor based serialization class. When spool() is called on a job object the 131 | tree of tasks is walked down, serializing all nodes in the tree into their alf script 132 | equivalents. 133 | """ 134 | 135 | def __init__( self, obj ): 136 | self.jobSerializableAttrs = [ 'globalvars', 'tags', 'tasks' ] 137 | self.taskSerializableAttrs = {} 138 | self.cmdSerializableAttrs = {} 139 | 140 | super(Serializer, self).__init__( obj ) 141 | 142 | 143 | ###################################### 144 | # 145 | # Serialization methods. 146 | # 147 | ###################################### 148 | 149 | def atleast( self, node ): 150 | """returns the minimum number of procs for the node:\n\n\t "-atleast %%d" """ 151 | return " -atleast %d" % node.atleast 152 | 153 | def atmost( self, node ): 154 | """eturns the maximum number of procs for the node:\n\n\t "-atmost %%d" """ 155 | return " -atmost %d" % node.atmost 156 | 157 | def chaser( self, node ): 158 | """returns the command to display ouput once a task has completed:\n\n\t "-chaser {%%s} """ 159 | return " -chaser {%s}" % node.chaser 160 | 161 | def commands( self, node ): 162 | """returns a string containing commands for a task:\n\n\t "-cmds { command/s }" """ 163 | return " -cmds { %s \n\t}" % self.writeCmds( node ) 164 | 165 | def init( self, node ): 166 | """Sets global job variables using the Assign keyworkd:\n\n\t "-init{ Assign A {B} }" """ 167 | output + " -init { " 168 | for var in node.globalvars: 169 | output += "\nAssign %s {%s}" % ( var, node.globalvars[var] ) 170 | output += "\n}" 171 | 172 | def jobtitle( self, node ): 173 | """returns a string containing the start of an alf script:\n\n\t "Job -title {example job}" """ 174 | return "\nJob -title { %s } " % node.title 175 | 176 | def preview( self, node ): 177 | """returns the command to display output during processing:\n\n\t "-preview {%%s} """ 178 | return " -preview {%s}" % node.preview 179 | 180 | def serialsubtasks( self, node ): 181 | """returns a flag for if the children of this node should be executed in serial:\n\n\t "-serialsubtasks 1" """ 182 | return " -serialsubtasks 1" 183 | 184 | def service( self, node ): 185 | """returns the list of services required for the task/job:\n\n\t "-service { servicename,servicename }" """ 186 | return " -service {%s}" % node.service 187 | 188 | def subtasks( self, node ): 189 | """returns a string containing all of the subtasks for the current node:\n\n\t "-subtasks{ (N) }" """ 190 | output = " -subtasks {\n" 191 | 192 | for each in node.tasks: 193 | output += self.writeNode( node.tasks[each] ) 194 | output += "\n}" 195 | 196 | return output 197 | 198 | def tags( self, node ): 199 | """returns tags for a task or job node:\n\n\t "-tags { tag/s }" """ 200 | return " -tags {%s}" % ",".join( node.tags ) 201 | 202 | def tasktitle( self, node ): 203 | """returns a string containing the definition of a task:\n\n\t "Task {name } -id {1234}" """ 204 | return "\nTask { %s } -id { %s } " % ( node.label, node.name ) 205 | 206 | def writeLocalCmd( self, command, options ): 207 | """writes out a local command:\n\n\t "Cmd { executable options[] }" """ 208 | 209 | if command.shell: 210 | return "\n\t\tCmd { %s %s %s }" % ( command.shell, command.executable, options) 211 | else: 212 | return "\n\t\tCmd { %s %s }" % ( command.executable, options) 213 | 214 | def writeRemoteCmd( self, command, options ): 215 | """writes out a remote command:\n\n\t "RemoteCmd { executable options[] }" """ 216 | 217 | if command.shell: 218 | return "\n\t\tRemoteCmd { %s {%s %s} } -service {%s}" % ( command.shell, command.executable, options, command.service) 219 | else: 220 | return "\n\t\tRemoteCmd { %s %s } -service {%s}" % ( command.executable, options, command.service) 221 | 222 | 223 | -------------------------------------------------------------------------------- /src/lib/tractor/api/tasktree.py: -------------------------------------------------------------------------------- 1 | import time 2 | import getpass 3 | import re 4 | 5 | from tractor.ordereddict import OrderedDict 6 | 7 | class TaskTree( object ): 8 | """Base class for tractor based job scripts. This class should not be called directly.""" 9 | # tasks are stored within a dict within each Task. There is therefore no global 10 | # list of all of the nodes in the tree. It would be possible to overload the OrderedDict 11 | # to create a global name variable to keep track of all insertions from any instance 12 | # of the ODict. 13 | 14 | def __init__(self): 15 | pass 16 | 17 | def __getattr__( self, attr ): 18 | try: 19 | return self.tasks.__getitem__( attr ) 20 | except KeyError, inst: 21 | #print "Warning: %s is not a known task id or methodname" % inst 22 | raise AttributeError 23 | 24 | def addTask( self, task ): 25 | """A task can be created outside of the job tree and added afterwards or 26 | it can be created by passing a valid name""" 27 | 28 | # Since either a simple string or a Task object may have been provided 29 | # as an argument the returned task is indexed in the task dictionary by 30 | # a key whose setting is dependant on the type of the argument. 31 | 32 | if isinstance( task, Task ): 33 | # Check to see if another task already exists within the job tree 34 | # with the same name, referenced as task.name. To do this we 35 | # match against the list of keys looking for matching names less 36 | # the "_NodeNNN" that is added. A list of matches is returned 37 | # of which we only want the last one. 38 | 39 | # This method is fairly obviously fraught with peril once/if a task 40 | # graph becomes too large. If performance becomes too bad then 41 | # it may be easier to switch to the simpler, if two nodes are named 42 | # the same then they second will be preservered, overwriting the 43 | # first. 44 | 45 | ## Simple Version 46 | # if isinstance( taskname, Task ): 47 | # self.tasks[taskname.name] = taskname 48 | # taskkey = taskname.name 49 | # else: 50 | # self.tasks[taskname] = Task( taskname ) 51 | # taskkey = taskname 52 | # return self.tasks[taskkey] 53 | 54 | regex = re.compile( '(%s_Node\d+$)' % task.name ).search 55 | result = [ match.group(1) for l in self.tasks.globalNodeList for match in [ regex(l) ] if match ] 56 | 57 | if result: 58 | result.sort() 59 | name, id = result[-1].split('_Node') 60 | 61 | task.name = '%s_Node%d' % ( name, int(id)+1 ) 62 | else: 63 | task.name = '%s_Node1' % task.name # rename the current task's name 64 | 65 | self.tasks[ task.name ] = task 66 | key = task.name 67 | 68 | else: 69 | # Create a new task with impunity, appending "_Node" to the 70 | # end of the taskname. In this case 'task' is simply a string stating 71 | # the name of the task to create. 72 | 73 | # The taskname is the unique key for the node in the task tree 74 | 75 | regex = re.compile( '(%s_Node\d+$)' % task ).search # search to see if the task name string provided matches a node 76 | result = [ m.group(1) for l in self.tasks.keys() for m in [ regex(l) ] if m] # it shouldn't be possible to match more that one, but it can happen so a list is built 77 | 78 | if result: 79 | result.sort() 80 | name, id = result[-1].split('_Node') 81 | 82 | task= '%s_Node%d' % ( name, int(id)+1 ) # if the taskname we've been given exists then we create a new node with an incremented indice. 83 | else: 84 | task = '%s_Node1' % task 85 | 86 | self.tasks[ task ] = Task( task ) 87 | key = task 88 | 89 | self.tasks.globalNodeList.append( key ) 90 | return self.tasks[ key ] 91 | 92 | def printme( self ): 93 | for task in self.tasks: 94 | print task, self.tasks[task].commands 95 | if self.tasks[task].commands: 96 | print "\t", self.tasks[task].commands[0].flags 97 | self.tasks[task].printme() 98 | 99 | 100 | class Job( TaskTree ): 101 | """Simplest way to think of this object is as the start of a job creation script. Global 102 | parameters are set here which can effect all the children blah. 103 | """ 104 | 105 | def __init__( self, *args, **kwargs ): 106 | self.after = "" # {month day hour:minute} or {hour:minute} 107 | self.atleast = 0 108 | self.atmost = 0 109 | self.globalvars = {} # used in -init{} 110 | self.tags = [] 111 | self.serialsubtasks = False 112 | self.service = None 113 | self.tasks = OrderedDict() 114 | self.title = "" 115 | self.user = getpass.getuser() 116 | 117 | if 'jobname' in kwargs: 118 | self.title = jobname 119 | elif len(args) == 1 : 120 | self.title = args[0] 121 | 122 | def assign( self, varname, value_string ): 123 | self.globalvars[varname] = value_string 124 | 125 | 126 | class Task( TaskTree ): 127 | # Task names are internally suffixed with "_NodeNNN". As such no 128 | # task should be named this way intentionally. 129 | 130 | def __init__( self, taskname, label="" ): 131 | self.name = taskname 132 | self.label = label if label else taskname # -title{} 133 | 134 | self.cleanup = [] 135 | self.chaser = "" 136 | self.commands = [] # -cmds{} 137 | self.preview = "" 138 | self.service = None 139 | self.serialsubtasks = False 140 | self.tasks = OrderedDict() # -subtasks{} 141 | 142 | @property 143 | def lastCmd( self ): 144 | try: 145 | return self.commands[-1] 146 | except: 147 | return None 148 | 149 | def addCmd( self, *args, **kwargs ): 150 | self.commands.append( Cmd() ) 151 | 152 | if 'cmd' in kwargs: 153 | self.lastCmd.executable = cmd 154 | 155 | return self.lastCmd 156 | 157 | def addRemoteCmd( self, *args, **kwargs ): 158 | self.commands.append( RemoteCmd() ) 159 | 160 | if 'cmd' in kwargs: 161 | self.lastCmd.executable = kwargs['cmd'] 162 | 163 | if 'service' in kwargs: 164 | self.lastCmd.service = kwargs['service'] 165 | 166 | return self.lastCmd 167 | 168 | def addChaser( self, executable, file ): 169 | pass 170 | 171 | def addPreview( self, executable, file ): 172 | pass 173 | 174 | 175 | class Cmd( object ): 176 | 177 | def __init__( self, *args, **kwargs ): 178 | """Simple execution command node. The executable can be passed 179 | a string containing the name of the program and any flags. Or the 180 | class can be subclassed for a specific program type.""" 181 | 182 | self.atleast = 0 183 | self.executable = "" 184 | self.environ = [] 185 | self.expand = False 186 | self.flags = OrderedDict() 187 | self.grab = [] 188 | self.id = "" 189 | self.ifcond = "" 190 | self.metrics = "" 191 | self.refersto = "" 192 | self.remote = False 193 | self.retryrc = [] 194 | self.samehost = False 195 | self.shell = "" 196 | self.tags = [] 197 | 198 | for attr in self.__dict__: 199 | if attr in kwargs: 200 | if type( kwargs[attr] ) == type( self.__dict__[attr] ): 201 | self.__dict__[attr] = kwargs[attr] 202 | 203 | def addShell( self, shell ): 204 | self.shell = shell 205 | 206 | def addExecutable( self, executable ): 207 | self.executable = executable 208 | 209 | def addOption( self, flag, option='' ): 210 | if option: 211 | self.flags[flag] = str(option) 212 | else: 213 | self.flags[flag] = "" 214 | 215 | def addPipe( self, executable, options='' ): 216 | if options: 217 | self.flags['|'] = '' 218 | self.flags[executable] = options 219 | else: 220 | self.flags['|'] = executable 221 | 222 | class RemoteCmd( Cmd ): 223 | def __init__( self, *args, **kwargs ): 224 | Cmd.__init__(self, args, kwargs) 225 | self.remote = True 226 | 227 | class Iterate( object ): 228 | def __init__(self): 229 | pass 230 | 231 | -------------------------------------------------------------------------------- /src/lib/tractor/ordereddict.py: -------------------------------------------------------------------------------- 1 | __all__ = ['OrderedDict'] 2 | # For bootstrapping reasons, the collection ABCs are defined in _abcoll.py. 3 | # They should however be considered an integral part of collections.py. 4 | from _abcoll import * 5 | import _abcoll 6 | __all__ += _abcoll.__all__ 7 | 8 | from operator import itemgetter as _itemgetter, eq as _eq 9 | from itertools import repeat as _repeat, chain as _chain, starmap as _starmap, \ 10 | ifilter as _ifilter, imap as _imap 11 | try: 12 | from thread import get_ident 13 | except ImportError: 14 | from dummy_thread import get_ident 15 | 16 | def _recursive_repr(user_function): 17 | 'Decorator to make a repr function return "..." for a recursive call' 18 | repr_running = set() 19 | 20 | def wrapper(self): 21 | key = id(self), get_ident() 22 | if key in repr_running: 23 | return '...' 24 | repr_running.add(key) 25 | try: 26 | result = user_function(self) 27 | finally: 28 | repr_running.discard(key) 29 | return result 30 | 31 | # Can't use functools.wraps() here because of bootstrap issues 32 | wrapper.__module__ = getattr(user_function, '__module__') 33 | wrapper.__doc__ = getattr(user_function, '__doc__') 34 | wrapper.__name__ = getattr(user_function, '__name__') 35 | return wrapper 36 | 37 | ################################################################################ 38 | ### OrderedDict 39 | ################################################################################ 40 | 41 | class OrderedDict(dict, MutableMapping): 42 | 'Dictionary that remembers insertion order' 43 | # An inherited dict maps keys to values. 44 | # The inherited dict provides __getitem__, __len__, __contains__, and get. 45 | # The remaining methods are order-aware. 46 | # Big-O running times for all methods are the same as for regular dictionaries. 47 | 48 | # The internal self.__map dictionary maps keys to links in a doubly linked list. 49 | # The circular doubly linked list starts and ends with a sentinel element. 50 | # The sentinel element never gets deleted (this simplifies the algorithm). 51 | # Each link is stored as a list of length three: [PREV, NEXT, KEY]. 52 | 53 | globalNodeList = [] 54 | 55 | def __init__(self, *args, **kwds): 56 | '''Initialize an ordered dictionary. Signature is the same as for 57 | regular dictionaries, but keyword arguments are not recommended 58 | because their insertion order is arbitrary. 59 | 60 | ''' 61 | if len(args) > 1: 62 | raise TypeError('expected at most 1 arguments, got %d' % len(args)) 63 | try: 64 | self.__root 65 | except AttributeError: 66 | self.__root = root = [None, None, None] # sentinel node 67 | PREV = 0 68 | NEXT = 1 69 | root[PREV] = root[NEXT] = root 70 | self.__map = {} 71 | self.update(*args, **kwds) 72 | 73 | def __setitem__(self, key, value, PREV=0, NEXT=1, dict_setitem=dict.__setitem__): 74 | 'od.__setitem__(i, y) <==> od[i]=y' 75 | # Setting a new item creates a new link which goes at the end of the linked 76 | # list, and the inherited dictionary is updated with the new key/value pair. 77 | if key not in self: 78 | root = self.__root 79 | last = root[PREV] 80 | last[NEXT] = root[PREV] = self.__map[key] = [last, root, key] 81 | dict_setitem(self, key, value) 82 | 83 | def __delitem__(self, key, PREV=0, NEXT=1, dict_delitem=dict.__delitem__): 84 | 'od.__delitem__(y) <==> del od[y]' 85 | # Deleting an existing item uses self.__map to find the link which is 86 | # then removed by updating the links in the predecessor and successor nodes. 87 | dict_delitem(self, key) 88 | link = self.__map.pop(key) 89 | link_prev = link[PREV] 90 | link_next = link[NEXT] 91 | link_prev[NEXT] = link_next 92 | link_next[PREV] = link_prev 93 | 94 | def __iter__(self, NEXT=1, KEY=2): 95 | 'od.__iter__() <==> iter(od)' 96 | # Traverse the linked list in order. 97 | root = self.__root 98 | curr = root[NEXT] 99 | while curr is not root: 100 | yield curr[KEY] 101 | curr = curr[NEXT] 102 | 103 | def __reversed__(self, PREV=0, KEY=2): 104 | 'od.__reversed__() <==> reversed(od)' 105 | # Traverse the linked list in reverse order. 106 | root = self.__root 107 | curr = root[PREV] 108 | while curr is not root: 109 | yield curr[KEY] 110 | curr = curr[PREV] 111 | 112 | def __reduce__(self): 113 | 'Return state information for pickling' 114 | items = [[k, self[k]] for k in self] 115 | tmp = self.__map, self.__root 116 | del self.__map, self.__root 117 | inst_dict = vars(self).copy() 118 | self.__map, self.__root = tmp 119 | if inst_dict: 120 | return (self.__class__, (items,), inst_dict) 121 | return self.__class__, (items,) 122 | 123 | def clear(self): 124 | 'od.clear() -> None. Remove all items from od.' 125 | try: 126 | for node in self.__map.itervalues(): 127 | del node[:] 128 | self.__root[:] = [self.__root, self.__root, None] 129 | self.__map.clear() 130 | except AttributeError: 131 | pass 132 | dict.clear(self) 133 | 134 | setdefault = MutableMapping.setdefault 135 | update = MutableMapping.update 136 | pop = MutableMapping.pop 137 | keys = MutableMapping.keys 138 | values = MutableMapping.values 139 | items = MutableMapping.items 140 | iterkeys = MutableMapping.iterkeys 141 | itervalues = MutableMapping.itervalues 142 | iteritems = MutableMapping.iteritems 143 | __ne__ = MutableMapping.__ne__ 144 | 145 | def viewkeys(self): 146 | "od.viewkeys() -> a set-like object providing a view on od's keys" 147 | return KeysView(self) 148 | 149 | def viewvalues(self): 150 | "od.viewvalues() -> an object providing a view on od's values" 151 | return ValuesView(self) 152 | 153 | def viewitems(self): 154 | "od.viewitems() -> a set-like object providing a view on od's items" 155 | return ItemsView(self) 156 | 157 | def popitem(self, last=True): 158 | '''od.popitem() -> (k, v), return and remove a (key, value) pair. 159 | Pairs are returned in LIFO order if last is true or FIFO order if false. 160 | 161 | ''' 162 | if not self: 163 | raise KeyError('dictionary is empty') 164 | key = next(reversed(self) if last else iter(self)) 165 | value = self.pop(key) 166 | return key, value 167 | 168 | @_recursive_repr 169 | def __repr__(self): 170 | 'od.__repr__() <==> repr(od)' 171 | if not self: 172 | return '%s()' % (self.__class__.__name__,) 173 | return '%s(%r)' % (self.__class__.__name__, self.items()) 174 | 175 | def copy(self): 176 | 'od.copy() -> a shallow copy of od' 177 | return self.__class__(self) 178 | 179 | @classmethod 180 | def fromkeys(cls, iterable, value=None): 181 | '''OD.fromkeys(S[, v]) -> New ordered dictionary with keys from S 182 | and values equal to v (which defaults to None). 183 | 184 | ''' 185 | d = cls() 186 | for key in iterable: 187 | d[key] = value 188 | return d 189 | 190 | def __eq__(self, other): 191 | '''od.__eq__(y) <==> od==y. Comparison to another OD is order-sensitive 192 | while comparison to a regular mapping is order-insensitive. 193 | 194 | ''' 195 | if isinstance(other, OrderedDict): 196 | return len(self)==len(other) and \ 197 | all(_imap(_eq, self.iteritems(), other.iteritems())) 198 | return dict.__eq__(self, other) 199 | 200 | -------------------------------------------------------------------------------- /src/lib/tractor/plugin/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baseblack/Tractor-API/a59e5615e5d7ad1c1b2fb0c76366f01421501b06/src/lib/tractor/plugin/__init__.py -------------------------------------------------------------------------------- /src/lib/tractor/plugin/maya.py: -------------------------------------------------------------------------------- 1 | from tractor.api.tasktree import Job, Task 2 | from tractor.api.render import Render 3 | 4 | class ArnoldfMaya( Render ): 5 | pass 6 | 7 | class RManfMaya( Render ): 8 | pass 9 | 10 | class MRfMaya( Render ): 11 | """Simple command line render of a maya scene. Breaks the render up in (n) number of chunks 12 | based on n = number of frames / chunk size.""" 13 | 14 | #init needs to set threads and layers 15 | 16 | def __init__( self, jobargs, cmdargs ): 17 | 18 | super( MRfMaya, self ).__init__( jobargs, cmdargs ) 19 | 20 | self.job['title'] = 'MRfMaya Job' 21 | self.job['projectpath'] = None 22 | self.job['layers'] = None 23 | self.command['threads'] = 8 24 | 25 | if 'title' in jobargs: self.job['title'] = jobargs['title'] 26 | if 'projectpath' in jobargs: self.job['projectpath'] = jobargs['projectpath'] 27 | if 'threads' in cmdargs: self.command['threads'] = cmdargs['threads'] 28 | if 'layers' in jobargs: self.job['layers'] = jobargs['layers'] 29 | 30 | def buildTaskTree( self ): 31 | 32 | self.JobObj = Job( self.job['title'] ) 33 | 34 | maintask = self.mainTask() 35 | 36 | self.JobObj.addTask( maintask ) 37 | 38 | def multiFrameTask( self, f_range ): 39 | name = 'Main_MultiFr__%s_%s' % ( f_range['first'],f_range['last'] ) #Main will be replaced with LAYERNAME in layer based renders 40 | label = 'Maya : Render : %s-%s' %( f_range['first'],f_range['last'] ) 41 | 42 | task = Task( name, label ) 43 | cmd = task.addRemoteCmd() 44 | 45 | #cmd.addShell( '/bin/bash -c' ) 46 | 47 | cmd.addExecutable( 'maya-render' ) 48 | cmd.service = 'MRfMRender' 49 | cmd.tags = ['maya'] 50 | 51 | cmd.addOption( '-r', "mr" ) 52 | cmd.addOption( '-v', 5 ) 53 | cmd.addOption( '-im', self.job['imagename'] ) 54 | 55 | if self.job['projectpath']: 56 | cmd.addOption( '-rd', ('%s/images') % self.job['projectpath'] ) 57 | 58 | if 'threads' in self.command: 59 | cmd.addOption( '-rt', self.command['threads'] ) 60 | 61 | cmd.addOption( '-s' , "%s" % f_range['first'] ) 62 | cmd.addOption( '-e' , "%s" % f_range['last'] ) 63 | 64 | if 'step' in f_range: 65 | cmd.addOption( '-b' , "%s" % f_range['step'] ) 66 | 67 | if self.job['layers']: 68 | cmd.addOption( '-rl', self.job['layers'] ) 69 | 70 | cmd.addOption( self.command['file'] ) 71 | 72 | if self.job['progress']: 73 | cmd.addPipe( self.job['progress'] ) 74 | 75 | return task 76 | 77 | 78 | def singleFrameTask( self, frame ): 79 | """ 80 | Single frame render task. Takes a single frame as argument and adds other options from self.cmdargs 81 | Options are written to the cmdline in the order they are added to the cmd obj. 82 | """ 83 | name = 'Main_IndivFr__%s' % frame 84 | label = 'Maya : Render : %s :: %s' % ( frame, name ) 85 | task = Task( name, label ) 86 | 87 | cmd = task.addRemoteCmd() 88 | 89 | #cmd.addShell( '/bin/bash -c' ) 90 | 91 | cmd.addExecutable( 'maya-render' ) 92 | cmd.service = 'MRfMRender' 93 | cmd.tags = ['maya'] 94 | 95 | cmd.addOption( '-r', "mr" ) 96 | cmd.addOption( '-v', 5 ) 97 | cmd.addOption( '-im', self.job['imagename'] ) 98 | 99 | if self.job['projectpath']: 100 | cmd.addOption( '-rd', ('%s/images') % self.job['projectpath'] ) 101 | 102 | if 'threads' in self.command: 103 | cmd.addOption( '-rt', self.command['threads'] ) 104 | 105 | cmd.addOption( '-s' , frame ) 106 | cmd.addOption( '-e' , frame ) 107 | 108 | if self.job['layers']: 109 | cmd.addOption( '-rl', self.job['layers'] ) 110 | 111 | cmd.addOption( self.command['file'] ) 112 | 113 | if self.job['progress']: 114 | cmd.addPipe( self.job['progress'] ) 115 | 116 | return task -------------------------------------------------------------------------------- /src/lib/tractor/plugin/nuke.py: -------------------------------------------------------------------------------- 1 | from tractor.api.tasktree import Job, Task 2 | from tractor.api.render import Render 3 | 4 | class Nuke( Render ): 5 | """Simple command line render of a nuke script. Breaks the script up in (n) number of chunks 6 | based on n = number of frames / chunk size.""" 7 | 8 | def __init__( self, jobargs, cmdargs ): 9 | 10 | super( Nuke, self ).__init__( jobargs, cmdargs ) 11 | 12 | self.job['title'] = self.command['file'] 13 | self.job['chunksize'] = 10 14 | self.command['quiet'] = False 15 | self.command['threads'] = 4 16 | self.command['nodes'] = '' 17 | self.command['fullsize'] = True 18 | 19 | if 'title' in jobargs: self.job['title'] = jobargs['title'] 20 | if 'chunksize' in jobargs: self.job['chunksize'] = jobargs['chunksize'] 21 | if 'quiet' in cmdargs: self.command['quiet'] = cmdargs['quiet'] 22 | if 'threads' in cmdargs: self.command['threads'] = cmdargs['threads'] 23 | if 'nodes' in cmdargs: self.command['nodes'] = cmdargs['nodes'] 24 | if 'fullsize' in cmdargs: self.command['fullsize'] = cmdargs['fullsize'] 25 | 26 | def buildTaskTree( self ): 27 | 28 | self.JobObj = Job( self.job['title'] ) 29 | 30 | qttask = None 31 | maintask = self.mainTask() 32 | 33 | if self.job['quicktime']: 34 | qttask = quicktimeTask(); 35 | qttask.addTask( maintask ) 36 | 37 | else: 38 | self.JobObj.addTask( maintask ) 39 | 40 | 41 | def multiFrameTask( self, f_range ): 42 | name = 'Main_MultiFr__%s_%s' % ( f_range['first'],f_range['last'] ) 43 | label = 'Nuke : Render : %s-%s :: %s' %( f_range['first'],f_range['last'], name ) 44 | task = Task( name, label ) 45 | 46 | cmd = task.addRemoteCmd() 47 | 48 | cmd.addShell( '/bin/bash -c' ) 49 | 50 | cmd.addExecutable( 'nuke' ) 51 | cmd.tags = ['nuke'] 52 | cmd.service = 'NukeRender' 53 | 54 | if 'quiet' in self.command: 55 | if self.command['quiet'] == True: 56 | cmd.addOption( '-q' ) 57 | 58 | if self.command['fullsize'] == True: 59 | cmd.addOption( '-f' ) 60 | 61 | if 'threads' in self.command: 62 | cmd.addOption( '-m', self.command['threads'] ) 63 | 64 | if 'step' in f_range: 65 | cmd.addOption( '-F' , "%s-%sx%s" % ( f_range['first'],f_range['last'],f_range['step'] ) ) 66 | else: 67 | cmd.addOption( '-F' , "%s-%s" % ( f_range['first'],f_range['last'] ) ) 68 | 69 | cmd.addOption( '-x' ) 70 | 71 | cmd.addOption( self.command['file'] ) 72 | 73 | if self.job['progress']: 74 | cmd.addPipe( self.job['progress'] ) 75 | 76 | return task 77 | 78 | 79 | def singleFrameTask( self, frame ): 80 | """ 81 | Single frame render task. Takes a single frame as argument and adds other options from self.cmdargs 82 | Options are written to the cmdline in the order they are added to the cmd obj. 83 | """ 84 | name = 'Main_IndivFr__%s' % frame 85 | label = 'Nuke : Render : %s :: %s' % ( frame, name ) 86 | task = Task( name, label ) 87 | 88 | cmd = task.addRemoteCmd() 89 | cmd.addExecutable( 'nuke' ) 90 | cmd.tags = ['nuke'] 91 | cmd.service = 'NukeRender' 92 | cmd.addOption( '-x' ) 93 | 94 | if 'quiet' in self.command: 95 | if self.command['quiet'] == True: 96 | cmd.addOption( '-q' ) 97 | 98 | if self.command['fullsize'] == True: 99 | cmd.addOption( '-f' ) 100 | 101 | if 'threads' in self.command: 102 | cmd.addOption( '-m', self.command['threads'] ) 103 | 104 | cmd.addOption( '-F' , frame ) 105 | cmd.addOption( self.command['file'] ) 106 | 107 | if self.job['progress']: 108 | cmd.addPipe( self.job['progress'] ) 109 | 110 | return task -------------------------------------------------------------------------------- /src/lib/tractor/plugin/shake.py: -------------------------------------------------------------------------------- 1 | from tractor.api.tasktree import Job, Task 2 | from tractor.api.render import Render 3 | 4 | class Shake( Render ): 5 | """Simple command line render of a maya scene. Breaks the render up in (n) number of chunks 6 | based on n = number of frames / chunk size.""" 7 | 8 | #init needs to set threads and layers 9 | 10 | def __init__( self, jobargs, cmdargs ): 11 | 12 | super( Shake, self ).__init__( jobargs, cmdargs ) 13 | 14 | self.job['title'] = 'Shake Job' 15 | self.command['proxyscale'] = 1 16 | 17 | if 'proxyscale' in cmdargs: self.command['proxyscale'] = cmdargs['proxyscale'] 18 | if 'title' in jobargs: self.job['title'] = jobargs['title'] 19 | 20 | def buildTaskTree( self ): 21 | 22 | self.JobObj = Job( self.job['title'] ) 23 | 24 | maintask = self.mainTask() 25 | 26 | self.JobObj.addTask( maintask ) 27 | 28 | def multiFrameTask( self, f_range ): 29 | name = 'Main_MultiFr__%s_%s' % ( f_range['first'],f_range['last'] ) 30 | label = 'Shake : Render : %s-%s :: %s' %( f_range['first'],f_range['last'], name ) 31 | 32 | task = Task( name, label ) 33 | cmd = task.addRemoteCmd() 34 | 35 | cmd.addShell( '/bin/bash -c' ) 36 | 37 | cmd.addExecutable( 'shake' ) 38 | cmd.service = 'ShakeRender' 39 | cmd.tags = ['shake'] 40 | 41 | cmd.addOption( '-exec' ) 42 | 43 | cmd.addOption( self.command['file'] ) 44 | 45 | cmd.addOption( '-vv' ) 46 | 47 | if self.command['proxyscale']: 48 | cmd.addOption( '-proxyscale', self.command['proxyscale'] ) 49 | 50 | if 'step' in f_range: 51 | cmd.addOption( '-t' , "%s-%sx%s" % ( f_range['first'],f_range['last'],f_range['step'] ) ) 52 | else: 53 | cmd.addOption( '-t' , "%s-%s" % ( f_range['first'],f_range['last'] ) ) 54 | 55 | if self.job['progress']: 56 | cmd.addPipe( self.job['progress'] ) 57 | 58 | return task 59 | 60 | def singleFrameTask( self, frame ): 61 | """ 62 | Single frame render task. Takes a single frame as argument and adds other options from self.cmdargs 63 | Options are written to the cmdline in the order they are added to the cmd obj. 64 | """ 65 | name = 'Main_IndivFr__%s' % frame 66 | label = 'Shake : Render : %s :: %s' % ( frame, name ) 67 | task = Task( name, label ) 68 | 69 | cmd = task.addRemoteCmd() 70 | 71 | cmd.addShell( '/bin/bash -c' ) 72 | 73 | cmd.addExecutable( 'shake' ) 74 | cmd.service = 'ShakeRender' 75 | cmd.tags = ['shake'] 76 | 77 | cmd.addOption( '-exec' ) 78 | 79 | cmd.addOption( self.command['file'] ) 80 | 81 | cmd.addOption( '-vv' ) 82 | 83 | if self.command['proxyscale']: 84 | cmd.addOption( '-proxyscale', self.command['proxyscale'] ) 85 | 86 | cmd.addOption( '-t' , frame ) 87 | 88 | if self.job['progress']: 89 | cmd.addPipe( self.job['progress'] ) 90 | 91 | return task -------------------------------------------------------------------------------- /src/ui/maya/tractorUI.txt: -------------------------------------------------------------------------------- 1 | + tractorUI 1.0 /home/andrew.bunday/Dropbox/home/renderLaunchers/tractor-glue/ui/maya/tractorUI 2 | 3 | -------------------------------------------------------------------------------- /src/ui/maya/tractorUI/scripts/pyQtSubmitMaya.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baseblack/Tractor-API/a59e5615e5d7ad1c1b2fb0c76366f01421501b06/src/ui/maya/tractorUI/scripts/pyQtSubmitMaya.py -------------------------------------------------------------------------------- /src/ui/maya/tractorUI/scripts/pySubmitMaya.py: -------------------------------------------------------------------------------- 1 | import maya.cmds as cmds 2 | import maya.mel as mel 3 | 4 | import os 5 | import getpass 6 | import time 7 | 8 | import tractor.glue.render as tractor 9 | 10 | class pyRenderDialog(): 11 | def __init__( self ): 12 | 13 | class ui(): 14 | def __init__(self): 15 | pass 16 | 17 | self.ui = ui() 18 | self.buildWindow() 19 | self.setupControls() 20 | self.sceneInfo = getSceneInfo() 21 | 22 | 23 | def buildWindow( self ): 24 | self.window = cmds.window( title="Submit to Farm", iconName='Render', widthHeight=(387, 450) ) 25 | 26 | cmds.columnLayout(rowSpacing=20, columnOffset=['both',20] ) 27 | 28 | cmds.setParent("..") 29 | 30 | cmds.rowColumnLayout( numberOfColumns=2, columnAttach=(1, 'right', 0), columnWidth=[(1, 100), (2, 250)] ) 31 | 32 | 33 | # render globals 34 | cmds.text( label = 'Job Name' ) 35 | self.ui.jobname = cmds.textField() 36 | 37 | cmds.text( label = 'Render Name' ) 38 | self.ui.rendername = cmds.textField() 39 | 40 | cmds.text( label = 'Frame Range' ) 41 | self.ui.framerange = cmds.textField() 42 | 43 | cmds.text( label = 'Chunk Size' ) 44 | self.ui.chunksize = cmds.intSliderGrp( field=True, maxValue=100, step=15,fieldStep=15,sliderStep=15 ) 45 | 46 | cmds.text( label = 'Threads' ) 47 | self.ui.threads = cmds.intSliderGrp( field=True, minValue=1, maxValue=16, fieldStep=2, value=8 ) 48 | 49 | cmds.text( label = '' ) 50 | self.ui.globals = cmds.button( label='Open Render Settings') 51 | 52 | # select the layers to render 53 | cmds.text( label = 'Render Layers' ) 54 | self.ui.renderlayers = cmds.textScrollList( numberOfRows=8, allowMultiSelection=True, append=['default'], selectItem='default', showIndexedItem=1 ) 55 | 56 | cmds.text( label = '' ) 57 | self.ui.refresh = cmds.button( label='refresh' ) 58 | 59 | 60 | # general options for the render manager 61 | cmds.text( label = 'Options' ) 62 | self.ui.options = cmds.checkBoxGrp( numberOfCheckBoxes=2, labelArray2=['Check Outputs', 'Generate Quicktime'] ) 63 | cmds.text( label = 'Process' ) 64 | self.ui.process = cmds.radioButtonGrp( labelArray4=['Light', 'Medium', 'Heavy', 'Sumo'], select=2, numberOfRadioButtons=4, columnWidth4=[60,60,60,60] ) 65 | 66 | cmds.setParent("..") 67 | 68 | 69 | # render button 70 | cmds.columnLayout( columnOffset=['both',20] ) 71 | self.ui.render = cmds.button( label='Render', width=330, height=50 ) 72 | 73 | 74 | def setupControls( self ): 75 | 76 | cmds.textField( self.ui.rendername, edit=True, enterCommand=('cmds.setFocus(\"' + self.ui.framerange + '\")') ) 77 | cmds.textField( self.ui.framerange, edit=True, enterCommand=('cmds.setFocus(\"' + self.ui.chunksize + '\")') ) 78 | 79 | cmds.button( self.ui.refresh, edit=True, command=self.refreshLayers ) 80 | cmds.button( self.ui.globals, edit=True, command=self.launchGlobalsWindow ) 81 | cmds.button( self.ui.render, edit=True, command=self.render ) 82 | 83 | if cmds.getAttr('defaultRenderGlobals.imageFilePrefix'): 84 | self.rendername = cmds.getAttr('defaultRenderGlobals.imageFilePrefix') 85 | 86 | if cmds.getAttr('defaultRenderGlobals.animation'): 87 | first = cmds.getAttr('defaultRenderGlobals.startFrame') 88 | last = cmds.getAttr('defaultRenderGlobals.endFrame') 89 | step = cmds.getAttr('defaultRenderGlobals.byFrameStep') 90 | self.framerange = "%d-%dx%d" % ( first, last, step) 91 | else: 92 | first = cmds.getAttr('defaultRenderGlobals.startFrame') 93 | self.framerange = "%d" % first 94 | 95 | if hasattr(self,'rendername'): 96 | cmds.textField( self.ui.rendername, edit=True, text=self.rendername ) 97 | else: 98 | cmds.textField( self.ui.rendername, edit=True, text="[not set; using scene name]" ) 99 | if hasattr(self,'framerange'): 100 | cmds.textField( self.ui.framerange, edit=True, text=self.framerange ) 101 | 102 | def launchGlobalsWindow( self, clicked ): 103 | mel.eval('unifiedRenderGlobalsWindow;') 104 | 105 | def refreshLayers( self, clicked ): 106 | 107 | print "refreshing..." 108 | 109 | cmds.textScrollList( self.ui.renderlayers, edit=True, removeAll=True ) 110 | renderableLayers = getRenderableLayers(self.sceneInfo) 111 | 112 | print renderableLayers 113 | if renderableLayers: 114 | for layer in renderableLayers: 115 | cmds.textScrollList( self.ui.renderlayers, edit=True, append=layer ) 116 | 117 | def show( self ): 118 | cmds.showWindow( self.window ) 119 | self.refreshLayers(True) 120 | 121 | def render( self, clicked ): 122 | self.jobname = cmds.textField( self.ui.jobname, query=True, text=True ) 123 | self.rendername = cmds.textField( self.ui.rendername, query=True, text=True ) 124 | self.framerange = cmds.textField( self.ui.framerange, query=True, text=True ) 125 | self.chunksize = cmds.intSliderGrp( self.ui.chunksize, query=True, value=True ) 126 | self.threads = cmds.intSliderGrp( self.ui.threads, query=True, value=True ) 127 | self.checkoutput = cmds.checkBoxGrp( self.ui.options, query=True, value1=True ) 128 | self.quicktime = cmds.checkBoxGrp( self.ui.options, query=True, value2=True ) 129 | self.weight = cmds.radioButtonGrp( self.ui.process, query=True, select=True ) 130 | self.projectpath = cmds.workspace(q=True,fn=True) 131 | 132 | self.username = getpass.getuser() 133 | self.timestamp = int( time.time() ) 134 | 135 | if self.rendername.find( "[not set; using scene name]" ) >= 0: 136 | if not cmds.file( query=True, sceneName=True ): 137 | self.rendername = "MayaRender-%s" % self.username 138 | else: 139 | scenepath = cmds.file( query=True, sceneName=True ) 140 | self.rendername = scenepath.split("/")[-1].split(".")[0] 141 | 142 | 143 | try: 144 | spooldir = "/mnt/muxfs/users/%s/spool" % self.username # should be built from a template in cfg 145 | if not os.path.exists( spooldir ): 146 | os.makedirs( spooldir ) 147 | except: 148 | print "Warning. Attempt to create spool directory %s failed" % spooldir 149 | 150 | filename = "maya-%s-%s.ma" % ( self.username, self.timestamp ) 151 | filepath = os.path.join( spooldir, filename ) 152 | 153 | try: 154 | cmds.file( rename=filepath ) 155 | cmds.file( save=True, type='mayaAscii' ) 156 | except: 157 | cmds.confirmDialog( title='Error', message='Error, unable to save scene to %s'%filepath) 158 | finally: 159 | cmds.file( rename=self.sceneInfo['SceneName'] ) 160 | 161 | maya_args = { "type":"Maya", "threads":self.threads, "file":filepath, "layers":self.selectedLayers() } 162 | render_args = { "range":self.framerange, "chunksize":self.chunksize, "timestamp":self.timestamp, "name":self.rendername, "user":self.username, 163 | "quicktime":self.quicktime, "weight":self.weight , "check":self.checkoutput, "title":self.jobname, "projectpath":self.projectpath } 164 | 165 | print render_args 166 | print maya_args 167 | 168 | launchRender( render_args, maya_args ) 169 | 170 | def selectedLayers( self ): 171 | return cmds.textScrollList( self.ui.renderlayers, query=True, selectItem=True ) 172 | 173 | def startRenderDialog(): 174 | 175 | dialog = pyRenderDialog() 176 | dialog.show() 177 | 178 | def launchRender( jobargs, cmdargs ): 179 | mayaRenderObj = tractor.Render( jobargs, cmdargs ) 180 | mayaRenderObj.build() 181 | mayaRenderObj.spool() 182 | 183 | def getSceneInfo(): 184 | 185 | result = {} 186 | result['MayaVersion'] = mel.eval('getApplicationVersionAsFloat') 187 | result['SceneName'] = cmds.file( q=True, location=True ) 188 | result['DatabaseDir'] = cmds.workspace( q=True, rd=True ) 189 | return result 190 | 191 | def getRenderableLayers( sceneinfo ): 192 | allLayers = cmds.listConnections( "renderLayerManager", t="renderLayer") 193 | renderableLayers = [] 194 | 195 | if len(allLayers) > 1: 196 | for layer in allLayers: 197 | if cmds.getAttr( '%s.renderable'%layer ): 198 | renderableLayers.append( layer ) 199 | 200 | return renderableLayers 201 | 202 | def menu(): 203 | gMainWindow = mel.eval ('global string $gMainWindow; string $temp=$gMainWindow;' ) 204 | 205 | cmds.menu( "Baseblack", label="Baseblack", parent=gMainWindow ) 206 | cmds.menuItem( subMenu=True, label='Tractor' ) 207 | cmds.menuItem( label="Render", command="import pySubmitMaya\npySubmitMaya.startRenderDialog()" ) 208 | 209 | 210 | 211 | 212 | 213 | -------------------------------------------------------------------------------- /src/ui/maya/tractorUI/scripts/userSetup.py: -------------------------------------------------------------------------------- 1 | import pySubmitMaya 2 | import maya.utils as utils 3 | 4 | utils.executeDeferred( 'pySubmitMaya.menu()' ) -------------------------------------------------------------------------------- /src/ui/maya/tractorUI/scripts/utils.py: -------------------------------------------------------------------------------- 1 | import maya.cmds as cmds 2 | import maya.mel 3 | import maya.utils as mu 4 | 5 | class MayaLayer: 6 | def __init__(self): 7 | self.name="" 8 | self.camera="" 9 | self.renderer="" 10 | self.requiredPlugins="" 11 | self.IsActive=False 12 | self.seqStart=1 13 | self.seqEnd=100 14 | self.seqStep=1 15 | self.seqFileOffset=0 16 | self.imageWidth=100 17 | self.imageHeight=100 18 | self.imageFileName="" 19 | self.imageFramePadding=1 20 | self.imageDir="" 21 | self.imageExtension="" 22 | self.imagePreNumberLetter="" 23 | self.ImageSingleOutputFile=False 24 | self.channelName="" 25 | self.maxChannels=0 26 | self.channelFileName=[] 27 | self.channelExtension=[] 28 | self.tempModifyExtension=False 29 | self.tempModifyByframe=1 30 | self.tempModifyStart=1 31 | self.tempImageFormat=1 32 | self.tempImfKeyPlugin="" 33 | self.tempImageExtension="unknown" 34 | self.tempImageFilePrefix="unknown" 35 | self.tempExtensionPadding=1 36 | self.tempVersionTag="" 37 | self.tempIsGI=False 38 | self.tempIsGI2=False 39 | self.tempGIFileName="" 40 | self.tempCamNames = [] 41 | self.tempCamRenderable = [] 42 | return 43 | 44 | 45 | def GetSceneFps(self): 46 | FpsName= cmds.currentUnit(query=True, time=True) 47 | if (FpsName=="game"): 48 | return 15.0 49 | elif (FpsName=="film"): 50 | return 24.0 51 | elif (FpsName=="pal"): 52 | return 25.0 53 | elif (FpsName=="ntsc"): 54 | return 30.0 55 | elif (FpsName=="show"): 56 | return 48.0 57 | elif (FpsName=="palf"): 58 | return 50.0 59 | elif (FpsName=="ntscf"): 60 | return 60.0 61 | elif (FpsName=="millisec"): 62 | return 1000.0 63 | elif (FpsName=="sec"): 64 | return 1.0 65 | elif (FpsName=="min"): 66 | return (1.0/60.0) 67 | elif (FpsName=="hour"): 68 | return (1.0/60.0/60.0) 69 | elif (FpsName=="2fps"): 70 | return 2.0 71 | elif (FpsName=="3fps"): 72 | return 3.0 73 | elif (FpsName=="4fps"): 74 | return 4.0 75 | elif (FpsName=="5fps"): 76 | return 5.0 77 | elif (FpsName=="6fps"): 78 | return 6.0 79 | elif (FpsName=="8fps"): 80 | return 8.0 81 | elif (FpsName=="10fps"): 82 | return 10.0 83 | elif (FpsName=="12fps"): 84 | return 12.0 85 | elif (FpsName=="16fps"): 86 | return 16.0 87 | elif (FpsName=="20fps"): 88 | return 20.0 89 | elif (FpsName=="40fps"): 90 | return 40.0 91 | elif (FpsName=="75fps"): 92 | return 75.0 93 | elif (FpsName=="80fps"): 94 | return 80.0 95 | elif (FpsName=="100fps"): 96 | return 100.0 97 | elif (FpsName=="120fps"): 98 | return 120.0 99 | elif (FpsName=="125fps"): 100 | return 125.0 101 | elif (FpsName=="150fps"): 102 | return 150.0 103 | elif (FpsName=="200fps"): 104 | return 200.0 105 | elif (FpsName=="240fps"): 106 | return 240.0 107 | elif (FpsName=="250fps"): 108 | return 250.0 109 | elif (FpsName=="300fps"): 110 | return 300.0 111 | elif (FpsName=="375fps"): 112 | return 375.0 113 | elif (FpsName=="400fps"): 114 | return 400.0 115 | elif (FpsName=="500fps"): 116 | return 500.0 117 | elif (FpsName=="600fps"): 118 | return 600.0 119 | elif (FpsName=="750fps"): 120 | return 750.0 121 | elif (FpsName=="1200fps"): 122 | return 1200.0 123 | elif (FpsName=="1500fps"): 124 | return 1500.0 125 | elif (FpsName=="2000fps"): 126 | return 2000.0 127 | elif (FpsName=="3000fps"): 128 | return 3000.0 129 | elif (FpsName=="6000fps"): 130 | return 6000.0 131 | else: 132 | return 25.0 133 | 134 | def CalcImageExtension(self): 135 | if (self.renderer=="renderMan"): 136 | rmanImages = maya.mel.eval('rman getPrefAsArray ImageFormatQuantizationTable;') 137 | for img in range(1, len(rmanImages)-1): 138 | if (rmanImages[img]==self.tempImfKeyPlugin): 139 | self.tempImageExtension= rmanImages[img-1] 140 | if (self.tempImageExtension.find("(")>=0): 141 | self.tempImageExtension=self.tempImageExtension[self.tempImageExtension.find("(")+1:] 142 | if (self.tempImageExtension.find(")")>=0): 143 | self.tempImageExtension=self.tempImageExtension[:self.tempImageExtension.find("(")] 144 | return 145 | self.tempImageExtension=".unknown" 146 | return 147 | 148 | #MRay, Maya Software: 149 | if (self.tempImageFormat== 60): 150 | self.tempImageExtension="swf" 151 | elif (self.tempImageFormat== 61): 152 | self.tempImageExtension="ai" 153 | elif (self.tempImageFormat== 62): 154 | self.tempImageExtension="svg" 155 | elif (self.tempImageFormat== 63): 156 | self.tempImageExtension="swft" 157 | elif (self.tempImageFormat== 50): 158 | mayaimfPlugInExt = maya.mel.eval('$rrTempimfPlugInExt = $imfPlugInExt;') 159 | mayaimfPlugInKey = maya.mel.eval('$rrTempimfPlugInKey = $imfPlugInKey;') 160 | for i in range(0, len(mayaimfPlugInKey)): 161 | if (mayaimfPlugInKey[i]==self.tempImfKeyPlugin): 162 | self.tempImageExtension=mayaimfPlugInKey[i] 163 | elif (self.tempImageFormat== 51): 164 | self.tempImageExtension=self.tempImfKeyPlugin 165 | else: 166 | try: 167 | mayaImgExt = maya.mel.eval('$rrTempimgExt = $imgExt;') 168 | if (len(mayaImgExt)==0): 169 | maya.mel.eval('createImageFormats()') 170 | mayaImgExt = maya.mel.eval('$rrTempimgExt2 = $imgExt;') 171 | except: 172 | maya.mel.eval('createImageFormats()') 173 | mayaImgExt = maya.mel.eval('$rrTempimgExt2 = $imgExt;') 174 | self.tempImageExtension = mayaImgExt[self.tempImageFormat] 175 | if (self.renderer=="mentalRay"): 176 | if (self.tempImageExtension=="sgi"): 177 | self.tempImageExtension="rgb" 178 | if (self.tempImageExtension=="tifu"): 179 | self.tempImageExtension="tif" 180 | if (self.tempImageExtension=="qntntsc"): 181 | self.tempImageExtension="yuv" 182 | if (self.tempImageExtension=="qntpal"): 183 | self.tempImageExtension="yuv" 184 | if (self.tempImageExtension=="jpeg"): 185 | self.tempImageExtension="jpg" 186 | 187 | 188 | # gather all information from a layer 189 | def getLayerSettings(self,Layer,DatabaseDir,SceneName,MayaVersion,isLayerRendering): 190 | self.tempVersionTag = cmds.getAttr('defaultRenderGlobals.renderVersion') 191 | if ((self.tempVersionTag==None) or (len(self.tempVersionTag)==0)): 192 | self.tempVersionTag="" 193 | CurrentLayer=cmds.editRenderLayerGlobals( query=True, currentRenderLayer=True ) 194 | LayerOverrides=cmds.listConnections( Layer+".adjustments", p=True, c=True) 195 | LayerOverridesMaster=cmds.listConnections( "defaultRenderLayer.adjustments", p=True, c=True) 196 | 197 | self.renderer= cmds.getAttr('defaultRenderGlobals.currentRenderer') 198 | if (CurrentLayer!=Layer): 199 | if ( (not (LayerOverridesMaster==None) ) and (len(LayerOverridesMaster)>1)): 200 | for o in range(0, len(LayerOverridesMaster) /2 ): 201 | OWhat=LayerOverridesMaster[o*2+1] 202 | LayerOverridesMaster[o*2]=LayerOverridesMaster[o*2].replace(".plug",".value") 203 | OValue=cmds.getAttr(LayerOverridesMaster[o*2]) 204 | if (OWhat=="defaultRenderGlobals.currentRenderer"): 205 | self.renderer= OValue 206 | if ( (not (LayerOverrides==None) ) and (len(LayerOverrides)>1)): 207 | for o in range(0, len(LayerOverrides) /2 ): 208 | OWhat=LayerOverrides[o*2+1] 209 | LayerOverrides[o*2]=LayerOverrides[o*2].replace(".plug",".value") 210 | OValue=cmds.getAttr(LayerOverrides[o*2]) 211 | if (OWhat=="defaultRenderGlobals.currentRenderer"): 212 | self.renderer= OValue 213 | if (self.renderer!="mayaSoftware"): 214 | self.requiredPlugins=self.renderer+";" 215 | 216 | #VRAY only: 217 | if (self.renderer=="vray"): 218 | vrayVersion=cmds.pluginInfo( 'vrayformaya', query=True, version=True ) 219 | isVRaySP=(vrayVersion.find("SP")>=0) 220 | self.imageWidth= int(cmds.getAttr('vraySettings.width')) 221 | self.imageHeight= int(cmds.getAttr('vraySettings.height')) 222 | self.imageFramePadding=cmds.getAttr('vraySettings.fileNamePadding') 223 | self.imageFileName=cmds.getAttr('vraySettings.fileNamePrefix') 224 | self.imageExtension=cmds.getAttr('vraySettings.imageFormatStr') 225 | self.imagePreNumberLetter=cmds.getAttr('vraySettings.fileNameRenderElementSeparator') 226 | self.camera=cmds.getAttr('vraySettings.batchCamera') 227 | if (isVRaySP): 228 | print("VRay SP1") 229 | isAnimation= cmds.getAttr('defaultRenderGlobals.animation') 230 | self.seqStart= int(cmds.getAttr('defaultRenderGlobals.startFrame')) 231 | self.seqEnd= int(cmds.getAttr('defaultRenderGlobals.endFrame')) 232 | self.seqStep= int(cmds.getAttr('defaultRenderGlobals.byFrameStep')) 233 | else: 234 | print("VRay") 235 | isAnimation= cmds.getAttr('vraySettings.animation') 236 | self.seqStart= int(cmds.getAttr('vraySettings.startFrame')) 237 | self.seqEnd= int(cmds.getAttr('vraySettings.endFrame')) 238 | self.seqStep= int(cmds.getAttr('vraySettings.frameStep')) 239 | self.tempIsGI= cmds.getAttr('vraySettings.giOn') 240 | self.tempIsGI2= ( (cmds.getAttr('vraySettings.imap_mode')==6) or (cmds.getAttr('vraySettings.imap_mode')==1)) 241 | self.tempGIFileName=cmds.getAttr('vraySettings.imap_autoSaveFile') 242 | 243 | self.ImageSingleOutputFile=False 244 | if (isAnimation!=1): 245 | self.ImageSingleOutputFile=True 246 | print "Still frames not allowed!\n Please use a sequence with one frame.\n Layer: "+Layer+"\n" 247 | return False 248 | if ((self.imageFileName==None) or (len(self.imageFileName)==0)): 249 | self.imageFileName=SceneName 250 | self.imageFileName=self.imageFileName.replace("\\","/") 251 | if (self.imageFileName.find("/")>=0): 252 | splitted=self.imageFileName.split("/") 253 | self.imageFileName=splitted[len(splitted)-1] 254 | if (self.imageFileName.find(".")>=0): 255 | splitted=self.imageFileName.split(".") 256 | self.imageFileName=splitted[0] 257 | 258 | if (CurrentLayer!=Layer): 259 | if ( (not (LayerOverridesMaster==None) ) and (len(LayerOverridesMaster)>1)): 260 | for o in range(0, len(LayerOverridesMaster) /2 ): 261 | OWhat=LayerOverridesMaster[o*2+1] 262 | LayerOverridesMaster[o*2]=LayerOverridesMaster[o*2].replace(".plug",".value") 263 | OValue=cmds.getAttr(LayerOverridesMaster[o*2]) 264 | 265 | if (OWhat=="vraySettings.width"): 266 | self.imageWidth= int(OValue) 267 | elif (OWhat=="vraySettings.height"): 268 | self.imageHeight= int(OValue) 269 | elif (isVRaySP and (OWhat=="defaultRenderGlobals.startFrame")): 270 | self.seqStart= int(OValue) 271 | elif (isVRaySP and (OWhat=="defaultRenderGlobals.endFrame")): 272 | self.seqEnd= int(OValue) 273 | elif (isVRaySP and (OWhat=="defaultRenderGlobals.byFrameStep")): 274 | self.seqStep= int(OValue) 275 | elif (OWhat=="vraySettings.startFrame"): 276 | self.seqStart= int(OValue) 277 | elif (OWhat=="vraySettings.endFrame"): 278 | self.seqEnd= int(OValue) 279 | elif (OWhat=="vraySettings.frameStep"): 280 | self.seqStep= int(OValue) 281 | elif (OWhat=="vraySettings.fileNamePadding"): 282 | self.imageFramePadding= int(OValue) 283 | elif (OWhat=="vraySettings.fileNamePrefix"): 284 | self.imageFileName=OValue 285 | elif (OWhat=="vraySettings.imageFormatStr"): 286 | self.imageExtension=OValue 287 | elif (OWhat=="vraySettings.fileNameRenderElementSeparator"): 288 | self.imagePreNumberLetter=OValue 289 | elif (OWhat=="vraySettings.batchCamera"): 290 | self.camera=OValue 291 | elif (OWhat=="vraySettings.giOn"): 292 | self.tempIsGI=OValue 293 | elif (OWhat=="vraySettings.imap_mode"): 294 | self.tempIsGI2=((int(OValue)==6) or (int(OValue)==1)) 295 | elif (OWhat=="vraySettings.imap_autoSaveFile"): 296 | self.tempGIFileName=OValue 297 | if ( (not (LayerOverrides==None) ) and (len(LayerOverrides)>1)): 298 | for o in range(0, len(LayerOverrides) /2 ): 299 | OWhat=LayerOverrides[o*2+1] 300 | LayerOverrides[o*2]=LayerOverrides[o*2].replace(".plug",".value") 301 | OValue=cmds.getAttr(LayerOverrides[o*2]) 302 | 303 | if (OWhat=="vraySettings.width"): 304 | self.imageWidth= int(OValue) 305 | elif (OWhat=="vraySettings.height"): 306 | self.imageHeight= int(OValue) 307 | elif (isVRaySP and (OWhat=="defaultRenderGlobals.startFrame")): 308 | self.seqStart= int(OValue) 309 | elif (isVRaySP and (OWhat=="defaultRenderGlobals.endFrame")): 310 | self.seqEnd= int(OValue) 311 | elif (isVRaySP and (OWhat=="defaultRenderGlobals.byFrameStep")): 312 | self.seqStep= int(OValue) 313 | elif (OWhat=="vraySettings.startFrame"): 314 | self.seqStart= int(OValue) 315 | elif (OWhat=="vraySettings.endFrame"): 316 | self.seqEnd= int(OValue) 317 | elif (OWhat=="vraySettings.frameStep"): 318 | self.seqStep= int(OValue) 319 | elif (OWhat=="vraySettings.fileNamePadding"): 320 | self.imageFramePadding= int(OValue) 321 | elif (OWhat=="vraySettings.fileNamePrefix"): 322 | self.imageFileName=OValue 323 | elif (OWhat=="vraySettings.imageFormatStr"): 324 | self.imageExtension=OValue 325 | elif (OWhat=="vraySettings.fileNameRenderElementSeparator"): 326 | self.imagePreNumberLetter=OValue 327 | elif (OWhat=="vraySettings.batchCamera"): 328 | self.camera=OValue 329 | elif (OWhat=="vraySettings.giOn"): 330 | self.tempIsGI=OValue 331 | elif (OWhat=="vraySettings.imap_mode"): 332 | self.tempIsGI2=((int(OValue)==6) or (int(OValue)==1)) 333 | elif (OWhat=="vraySettings.imap_autoSaveFile"): 334 | self.tempGIFileName=OValue 335 | 336 | if ((self.imageExtension==None) or (len(self.imageExtension)==0)): 337 | self.imageExtension="png" 338 | self.imageExtension= "."+ self.imageExtension 339 | if ((self.imageExtension==".exr (multichannel)")): 340 | self.imageExtension=".exr" 341 | self.imagePreNumberLetter="" 342 | if ((self.camera==None) or (len(self.camera)==0)): 343 | self.camera="" 344 | if ((self.tempIsGI) and (self.tempIsGI2)): 345 | self.imageFileName=self.tempGIFileName 346 | self.imageExtension="" 347 | self.imagePreNumberLetter="" 348 | self.renderer="vray_prepass" 349 | if (self.imageFileName.find(".vrmap")>=0): 350 | splitted=self.imageFileName.split(".vrmap") 351 | self.imageFileName=splitted[0] 352 | self.imageExtension=".vrmap" 353 | 354 | 355 | self.imageFileName= self.imageFileName + self.imagePreNumberLetter 356 | self.imageFileName=self.imageFileName.replace("%/l","/") 357 | self.imageFileName=self.imageFileName.replace("%l","") 358 | self.imageFileName=self.imageFileName.replace("","") 359 | self.imageFileName=self.imageFileName.replace("","") 360 | self.imageFileName=self.imageFileName.replace("%/c","/") 361 | self.imageFileName=self.imageFileName.replace("%c","") 362 | self.imageFileName=self.imageFileName.replace("%/s","/") 363 | self.imageFileName=self.imageFileName.replace("%s","") 364 | self.imageFileName=self.imageFileName.replace("","") 365 | self.imageFileName=self.imageFileName.replace("","") 366 | self.imageFileName=self.imageFileName.replace("%e",self.tempImageExtension) 367 | self.imageFileName=self.imageFileName.replace("",self.tempImageExtension) 368 | self.imageFileName=self.imageFileName.replace("",self.tempVersionTag) 369 | if (isLayerRendering and (self.imageFileName.find("")<0)): 370 | self.imageFileName="/"+self.imageFileName 371 | 372 | 373 | if (self.renderer=="vray_prepass"): 374 | self.ImageDir="" 375 | else: 376 | self.ImageDir= cmds.workspace(fre="images") 377 | isRelative=True 378 | if (len(self.ImageDir)>1): 379 | self.ImageDir=self.ImageDir.replace("\\","/") 380 | if (self.ImageDir[0]=="/"): 381 | isRelative=False 382 | if (self.ImageDir[1]==":"): 383 | isRelative=False 384 | if (isRelative): 385 | self.ImageDir=DatabaseDir+self.ImageDir 386 | self.ImageDir+="/" 387 | 388 | return True 389 | #"VRay Only" returned 390 | 391 | 392 | #MentalRay, Maya software, hardware renderer, Renderman: 393 | attrNameImfkey="defaultRenderGlobals.imfPluginKey"; 394 | if (self.renderer=="renderMan"): 395 | attrNameImfkey="rmanFinalOutputGlobals0.rman__riopt__Display_type"; 396 | 397 | self.imageWidth= int(cmds.getAttr('defaultResolution.width')) 398 | self.imageHeight= int(cmds.getAttr('defaultResolution.height')) 399 | self.seqStart= int(cmds.getAttr('defaultRenderGlobals.startFrame')) 400 | self.seqEnd= int(cmds.getAttr('defaultRenderGlobals.endFrame')) 401 | self.seqStep= int(cmds.getAttr('defaultRenderGlobals.byFrameStep')) 402 | self.tempModifyExtension=(cmds.getAttr('defaultRenderGlobals.modifyExtension')==True) 403 | self.tempModifyStart=int(cmds.getAttr('defaultRenderGlobals.startExtension')) 404 | self.tempModifyByframe=int(cmds.getAttr('defaultRenderGlobals.byExtension')) 405 | self.tempImageFormat=int(cmds.getAttr('defaultRenderGlobals.imageFormat')) 406 | self.tempImfKeyPlugin=cmds.getAttr(attrNameImfkey) 407 | self.tempImageFilePrefix=cmds.getAttr('defaultRenderGlobals.imageFilePrefix') 408 | self.tempExtensionPadding=cmds.getAttr('defaultRenderGlobals.extensionPadding') 409 | isAnimation= cmds.getAttr('defaultRenderGlobals.animation') 410 | if (isAnimation!=1): 411 | self.ImageSingleOutputFile=True 412 | return False 413 | 414 | cameraList=cmds.ls(ca=True) 415 | for cam in cameraList: 416 | self.tempCamNames.append(cam); 417 | if (cmds.getAttr(cam+'.renderable')): 418 | self.tempCamRenderable.append(True) 419 | else: 420 | self.tempCamRenderable.append(False) 421 | self.ImageSingleOutputFile=False 422 | if (CurrentLayer!=Layer): 423 | if ( (not (LayerOverridesMaster==None) ) and (len(LayerOverridesMaster)>1)): 424 | for o in range(0, len(LayerOverridesMaster) /2 ): 425 | OWhat=LayerOverridesMaster[o*2+1] 426 | LayerOverridesMaster[o*2]=LayerOverridesMaster[o*2].replace(".plug",".value") 427 | OValue=cmds.getAttr(LayerOverridesMaster[o*2]) 428 | 429 | for c in range(0, len(self.tempCamNames)): 430 | if (self.tempCamNames[c]+'.renderable'== OWhat): 431 | if (OValue): 432 | self.tempCamRenderable[c]=True 433 | else: 434 | self.tempCamRenderable[c]=False 435 | if (OWhat=="defaultResolution.width"): 436 | self.imageWidth= int(OValue) 437 | elif (OWhat=="defaultResolution.height"): 438 | self.imageHeight= int(OValue) 439 | elif (OWhat=="defaultRenderGlobals.startFrame"): 440 | self.seqStart= int(OValue* self.GetSceneFps() +0.001 ) 441 | elif (OWhat=="defaultRenderGlobals.endFrame"): 442 | self.seqEnd= int(OValue* self.GetSceneFps() +0.001 ) 443 | elif (OWhat=="defaultRenderGlobals.byFrameStep"): 444 | self.seqStep= int(OValue* self.GetSceneFps() +0.001) 445 | elif (OWhat=="defaultRenderGlobals.modifyExtension"): 446 | self.tempModifyExtension=(OValue==True) 447 | elif (OWhat=="defaultRenderGlobals.startExtension"): 448 | self.tempModifyStart=int(OValue) 449 | elif (OWhat=="defaultRenderGlobals.byExtension"): 450 | self.tempModifyByframe=int(OValue) 451 | elif (OWhat=="defaultRenderGlobals.imageFormat"): 452 | self.tempImageFormat=int(OValue) 453 | elif (OWhat==attrNameImfkey): 454 | self.tempImfKeyPlugin=OValue 455 | elif (OWhat=="defaultRenderGlobals.imageFilePrefix"): 456 | self.tempImageFilePrefix=OValue 457 | elif (OWhat=="defaultRenderGlobals.extensionPadding"): 458 | self.tempExtensionPadding=OValue 459 | if ( (not (LayerOverrides==None) ) and (len(LayerOverrides)>1)): 460 | for o in range(0, len(LayerOverrides) /2 ): 461 | OWhat=LayerOverrides[o*2+1] 462 | LayerOverrides[o*2]=LayerOverrides[o*2].replace(".plug",".value") 463 | OValue=cmds.getAttr(LayerOverrides[o*2]) 464 | 465 | for c in range(0, len(self.tempCamNames)): 466 | if (self.tempCamNames[c]+'.renderable'== OWhat): 467 | if (OValue): 468 | self.tempCamRenderable[c]=True 469 | else: 470 | self.tempCamRenderable[c]=False 471 | if (OWhat=="defaultResolution.width"): 472 | self.imageWidth= int(OValue) 473 | elif (OWhat=="defaultResolution.height"): 474 | self.imageHeight= int(OValue) 475 | elif (OWhat=="defaultRenderGlobals.startFrame"): 476 | self.seqStart= int(OValue* self.GetSceneFps() +0.001) 477 | elif (OWhat=="defaultRenderGlobals.endFrame"): 478 | self.seqEnd= int(OValue* self.GetSceneFps() +0.001) 479 | elif (OWhat=="defaultRenderGlobals.byFrameStep"): 480 | self.seqStep= int(OValue* self.GetSceneFps() +0.001) 481 | elif (OWhat=="defaultRenderGlobals.modifyExtension"): 482 | self.tempModifyExtension=(OValue==True) 483 | elif (OWhat=="defaultRenderGlobals.startExtension"): 484 | self.tempModifyStart=int(OValue) 485 | elif (OWhat=="defaultRenderGlobals.byExtension"): 486 | self.tempModifyByframe=int(OValue) 487 | elif (OWhat=="defaultRenderGlobals.imageFormat"): 488 | self.tempImageFormat=int(OValue) 489 | elif (OWhat==attrNameImfkey): 490 | self.tempImfKeyPlugin=OValue 491 | elif (OWhat=="defaultRenderGlobals.imageFilePrefix"): 492 | self.tempImageFilePrefix=OValue 493 | elif (OWhat=="defaultRenderGlobals.extensionPadding"): 494 | self.tempExtensionPadding=OValue 495 | 496 | nbRenderableCams=0 497 | for c in range(0, len(self.tempCamRenderable)): 498 | if (self.tempCamRenderable[c]): 499 | nbRenderableCams=nbRenderableCams+1 500 | transformNode = cmds.listRelatives(self.tempCamNames[c],parent=True) 501 | transformNode=transformNode[0] 502 | self.camera=transformNode 503 | 504 | self.CalcImageExtension() 505 | 506 | if ( self.tempModifyExtension): 507 | self.seqFileOffset=self.tempModifyStart 508 | if (self.tempModifyByframe!="1"): 509 | rrWriteLog("No 'By Frame' renumbering allowed!\n Layer: "+Layer+"\n") 510 | return False 511 | else: 512 | self.seqFileOffset=0 513 | if (not self.getImageOut(DatabaseDir,MayaVersion,SceneName)): 514 | return False 515 | 516 | if (nbRenderableCams>1): 517 | for c in range(0, len(self.tempCamRenderable)): 518 | if (self.tempCamRenderable[c]): 519 | nbRenderableCams=nbRenderableCams+1 520 | transformNode = cmds.listRelatives(self.tempCamNames[c],parent=True) 521 | transformNode=transformNode[0] 522 | if (self.camera!=transformNode): 523 | self.channelFileName.append(self.imageFileName.replace('',transformNode)) 524 | self.channelExtension.append(self.imageExtension) 525 | self.maxChannels +=1 526 | self.camera=self.camera + " ClearAtSubmit" 527 | 528 | if (self.renderer == 'mentalRay') and (MayaVersion>=2009.0): 529 | self.addChannelsToLayer() 530 | 531 | return True 532 | 533 | 534 | 535 | #add MRay Render Passes to layer (for information only) 536 | def addChannelsToLayer(self): 537 | name = self.name 538 | if self.name == 'masterLayer': 539 | name = 'defaultRenderLayer' 540 | passes = cmds.listConnections(name+'.renderPass') 541 | if passes == None or len(passes) == 0: 542 | return 543 | for p in passes: 544 | if ((cmds.nodeType(p)=="renderPass") and (cmds.getAttr(p+'.renderable') == 1)): 545 | self.channelFileName.append(self.imageFileName.replace('',p)) 546 | self.channelExtension.append(self.imageExtension) 547 | self.maxChannels +=1 548 | 549 | 550 | #calculate image name from layer/render settings for Maya 2009+: 551 | def getImageOut2009(self): 552 | maya.mel.eval('renderSettings -fin -lyr "'+self.name+'";') # workaround for batch mode to load command 553 | RenderOut=cmds.renderSettings(ign=True,lyr=self.name) 554 | RenderOut=RenderOut[0] 555 | RenderOut=RenderOut.replace("\\","/") 556 | FNsplitter="" 557 | if (RenderOut.find("%0n")>=0): 558 | FNsplitter="%0n" 559 | self.imageFramePadding=1 560 | if (RenderOut.find("%1n")>=0): 561 | FNsplitter="%1n" 562 | self.imageFramePadding=1 563 | if (RenderOut.find("%2n")>=0): 564 | FNsplitter="%2n" 565 | self.imageFramePadding=2 566 | if (RenderOut.find("%3n")>=0): 567 | FNsplitter="%3n" 568 | self.imageFramePadding=3 569 | if (RenderOut.find("%4n")>=0): 570 | FNsplitter="%4n" 571 | self.imageFramePadding=4 572 | if (RenderOut.find("%5n")>=0): 573 | FNsplitter="%5n" 574 | self.imageFramePadding=5 575 | if (RenderOut.find("%6n")>=0): 576 | FNsplitter="%6n" 577 | self.imageFramePadding=6 578 | if (RenderOut.find("%7n")>=0): 579 | FNsplitter="%7n" 580 | self.imageFramePadding=7 581 | if (RenderOut.find("%8n")>=0): 582 | FNsplitter="%8n" 583 | self.imageFramePadding=8 584 | if (RenderOut.find("%9n")>=0): 585 | FNsplitter="%9n" 586 | self.imageFramePadding=9 587 | 588 | if (len(FNsplitter)>0): 589 | Splitted=RenderOut.split(FNsplitter,1) 590 | self.imageFileName=Splitted[0] 591 | self.imageExtension=Splitted[1] 592 | if ((self.renderer=="renderMan") and (self.imagePreNumberLetter=="_")): 593 | if (self.name=="masterLayer"): 594 | self.imageFileName+="__" 595 | else: 596 | self.imageFileName+="_" 597 | else: 598 | self.imageFileName=RenderOut 599 | self.imageExtension="" 600 | 601 | self.imageFileName=self.imageFileName.replace("%/l","/") 602 | self.imageFileName=self.imageFileName.replace("%l","") 603 | self.imageFileName=self.imageFileName.replace("","") 604 | self.imageFileName=self.imageFileName.replace("%/c","/") 605 | self.imageFileName=self.imageFileName.replace("%c","") 606 | self.imageFileName=self.imageFileName.replace("%/s","/") 607 | self.imageFileName=self.imageFileName.replace("%s","") 608 | self.imageFileName=self.imageFileName.replace("","") 609 | self.imageFileName=self.imageFileName.replace("","") 610 | self.imageFileName=self.imageFileName.replace("",self.tempVersionTag) 611 | 612 | if ((self.name=="masterLayer") and (self.renderer=="renderMan")): 613 | self.imageFileName=self.imageFileName.replace("","") 614 | self.imageFileName=self.imageFileName.replace("//","/") 615 | 616 | if (self.imageFileName.find("")>=0): 617 | if (self.renderer!="mentalRay"): 618 | self.imageFileName=self.imageFileName.replace("","") 619 | else: 620 | self.channelName="MasterBeauty" 621 | self.imageFileName=self.imageFileName.replace("%e",self.tempImageExtension) 622 | self.imageExtension=self.imageExtension.replace("%e",self.tempImageExtension) 623 | self.imageFileName=self.imageFileName.replace("",self.tempImageExtension) 624 | self.imageExtension=self.imageExtension.replace("",self.tempImageExtension) 625 | 626 | 627 | #calculate image name from layer/render settings for Maya 8.5: 628 | def getImageOut85(self,SceneName): 629 | ImageNoExtension= cmds.getAttr('defaultRenderGlobals.outFormatControl') 630 | ImageperiodInExt= cmds.getAttr('defaultRenderGlobals.periodInExt') 631 | ImageputFrameBeforeExt= cmds.getAttr('defaultRenderGlobals.putFrameBeforeExt') 632 | self.imageFramePadding=self.tempExtensionPadding 633 | self.imageExtension="" 634 | if ((self.tempImageFilePrefix==None) or (len(self.tempImageFilePrefix)==0)): 635 | self.tempImageFilePrefix=SceneName 636 | self.tempImageFilePrefix=self.tempImageFilePrefix.replace("\\","/") 637 | if (self.tempImageFilePrefix.find("/")>=0): 638 | splitted=self.tempImageFilePrefix.split("/") 639 | self.tempImageFilePrefix=splitted[len(splitted)-1] 640 | if (self.tempImageFilePrefix.find(".")>=0): 641 | splitted=self.tempImageFilePrefix.split(".") 642 | self.tempImageFilePrefix=splitted[0] 643 | if (ImageperiodInExt==0): 644 | self.imagePreNumberLetter="" 645 | elif (ImageperiodInExt==1): 646 | self.imagePreNumberLetter="." 647 | elif (ImageperiodInExt==2): 648 | self.imagePreNumberLetter="_" 649 | 650 | if (not self.ImageSingleOutputFile): 651 | if (ImageNoExtension): 652 | self.imageFileName=self.tempImageFilePrefix 653 | else: 654 | self.imageFileName=self.tempImageFilePrefix+"."+self.tempImageExtension 655 | elif (ImageNoExtension): 656 | self.imageFileName=self.tempImageFilePrefix+"." 657 | elif (ImageperiodInExt==0): 658 | self.imageFileName=self.tempImageFilePrefix 659 | self.imageExtension="."+self.tempImageExtension 660 | elif (ImageperiodInExt==2): 661 | self.imageFileName=self.tempImageFilePrefix+"_" 662 | self.imageExtension="."+self.tempImageExtension 663 | elif (ImageputFrameBeforeExt): 664 | self.imageFileName=self.tempImageFilePrefix+"." 665 | self.imageExtension="."+self.tempImageExtension 666 | else: 667 | self.imageFileName=self.tempImageFilePrefix+"." 668 | self.imageExtension="."+self.tempImageExtension 669 | rrWriteLog("'name.ext.#' not supported!\n Layer: "+Layer+"\n") 670 | return False 671 | 672 | self.imageFileName=self.imageFileName.replace("",self.tempVersionTag) 673 | RenderLayer=cmds.listConnections( "renderLayerManager", t="renderLayer") 674 | 675 | return True 676 | 677 | 678 | #prepare image name from layer/render settings: 679 | def getImageOut(self,DatabaseDir,MayaVersion,SceneName): 680 | ImageperiodInExt= cmds.getAttr('defaultRenderGlobals.periodInExt') 681 | if (ImageperiodInExt==0): 682 | self.imagePreNumberLetter="" 683 | elif (ImageperiodInExt==1): 684 | self.imagePreNumberLetter="." 685 | elif (ImageperiodInExt==2): 686 | self.imagePreNumberLetter="_" 687 | 688 | self.ImageDir= cmds.workspace(fre="images") 689 | isRelative=True 690 | if (len(self.ImageDir)>1): 691 | self.ImageDir=self.ImageDir.replace("\\","/") 692 | if (self.ImageDir[0]=="/"): 693 | isRelative=False 694 | if (self.ImageDir[1]==":"): 695 | isRelative=False 696 | if (isRelative): 697 | self.ImageDir=DatabaseDir+self.ImageDir 698 | self.ImageDir+="/" 699 | self.imageFramePadding=0 700 | if (MayaVersion>=2009.0): 701 | self.getImageOut2009() 702 | return True 703 | else: 704 | return self.getImageOut85(SceneName) 705 | 706 | 707 | 708 | class SceneInfo: 709 | def __init__(self): 710 | self.MayaVersion="" 711 | self.SceneName="" 712 | self.DatabaseDir="" 713 | 714 | def getSceneInfo(self): 715 | self.DatabaseDir=cmds.workspace( q=True, rd=True ) 716 | self.SceneName=cmds.file( q=True, location=True ) 717 | self.MayaVersion=maya.mel.eval('getApplicationVersionAsFloat') 718 | 719 | 720 | def getAllLayers( sceneInfo ): 721 | """mercillisuly ripped from royalrender""" 722 | 723 | renderLayers = [] #returnable 724 | 725 | allLayers = cmds.listConnections( "renderLayerManager", t="renderLayer") 726 | print allLayers 727 | 728 | layerBasedRendering = ( len( allLayers ) > 1 ) 729 | 730 | #masterlayer information 731 | masterLayer = utils.MayaLayer() 732 | masterLayer.name = "masterlayer" 733 | masterLayer.isActive = (cmds.getAttr('defaultRenderLayer.renderable')==True) 734 | 735 | if not masterLayer.getLayerSettings( "defaultRenderLayer", sceneInfo.DatabaseDir, sceneInfo.SceneName, sceneInfo.MayaVersion, layerBasedRendering ) : 736 | print "master blah" 737 | return 738 | 739 | renderLayers.append( masterLayer ) 740 | 741 | if len(renderLayers) > 1: 742 | for layer in renderLayers: 743 | if layer != "defaultRenderLayer": 744 | Layer = utils.MayaLayer() 745 | Layer.name = layer 746 | Layer.isActive = cmds.getAttr( '%s.renderable'%layer ) 747 | if not Layer.getLayerSettings( layer, sceneInfo.DatabaseDir, sceneInfo.SceneName, sceneInfo.MayaVersion, layerBasedRendering ) : 748 | print "layer foo" 749 | return 750 | 751 | renderLayers.append(Layer) 752 | 753 | if masterLayer.imageFileName.find("") == -1: 754 | masterLayer.name = "" 755 | 756 | return renderLayers 757 | -------------------------------------------------------------------------------- /src/ui/nuke/tractorUI/icons/icon-large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baseblack/Tractor-API/a59e5615e5d7ad1c1b2fb0c76366f01421501b06/src/ui/nuke/tractorUI/icons/icon-large.png -------------------------------------------------------------------------------- /src/ui/nuke/tractorUI/icons/icon-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baseblack/Tractor-API/a59e5615e5d7ad1c1b2fb0c76366f01421501b06/src/ui/nuke/tractorUI/icons/icon-small.png -------------------------------------------------------------------------------- /src/ui/nuke/tractorUI/menu.py: -------------------------------------------------------------------------------- 1 | import nuke 2 | import simpleSubmitNuke 3 | import pyQtSubmitNuke 4 | 5 | 6 | menubar = nuke.menu("Nuke") 7 | m = menubar.addMenu("Baseblack") 8 | 9 | m.addCommand("Tractor/Simple", "dialog=simpleSubmitNuke.submit_gui(); dialog.show()", icon="BB_icon.png") 10 | m.addCommand("Tractor/Complete", "pyQtSubmitNuke.startQtRenderDialog()", icon="BB_icon.png") 11 | 12 | toolbar = nuke.toolbar("Nodes") 13 | bb = toolbar.addMenu("Baseblack", "BB_icon.png") 14 | 15 | bb.addCommand("Tractor/Simple", "dialog=simpleSubmitNuke.submit_gui(); dialog.show()", icon="BB_icon.png") 16 | bb.addCommand("Tractor/Complete", "pyQtSubmitNuke.startQtRenderDialog()", icon="BB_icon.png") 17 | 18 | -------------------------------------------------------------------------------- /src/ui/nuke/tractorUI/python/pyQtSubmitDialog.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Baseblack 4 | submissionDialog 5 | 6 | 7 | 8 | 0 9 | 0 10 | 387 11 | 576 12 | 13 | 14 | 15 | Qt::DefaultContextMenu 16 | 17 | 18 | Submit to Farm 19 | 20 | 21 | 22 | ../icons/icon-small.png../icons/icon-small.png 23 | 24 | 25 | false 26 | 27 | 28 | false 29 | 30 | 31 | 32 | 33 | 3 34 | 528 35 | 381 36 | 46 37 | 38 | 39 | 40 | Render 41 | 42 | 43 | 44 | 45 | 46 | 3 47 | 138 48 | 381 49 | 221 50 | 51 | 52 | 53 | 54 | 3 55 | 56 | 57 | 58 | 59 | 60 | 0 61 | 125 62 | 63 | 64 | 65 | 66 | 16777215 67 | 300 68 | 69 | 70 | 71 | 72 | 73 | 74 | Qt::ScrollBarAlwaysOff 75 | 76 | 77 | true 78 | 79 | 80 | QAbstractItemView::MultiSelection 81 | 82 | 83 | true 84 | 85 | 86 | 87 | Scene 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | Select Write Nodes to Render 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 150 106 | 16777215 107 | 108 | 109 | 110 | Refresh 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 3 122 | 4 123 | 381 124 | 133 125 | 126 | 127 | 128 | 129 | 3 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 92 138 | 0 139 | 140 | 141 | 142 | Qt::RightToLeft 143 | 144 | 145 | Render Name 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | false 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 92 168 | 0 169 | 170 | 171 | 172 | Qt::RightToLeft 173 | 174 | 175 | Frame Range 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | false 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 92 198 | 0 199 | 200 | 201 | 202 | Qt::RightToLeft 203 | 204 | 205 | Chunk Size 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | None 214 | 215 | 216 | 217 | 218 | 100 219 | 220 | 221 | 222 | 223 | 50 224 | 225 | 226 | 227 | 228 | 25 229 | 230 | 231 | 232 | 233 | 15 234 | 235 | 236 | 237 | 238 | 10 239 | 240 | 241 | 242 | 243 | 5 244 | 245 | 246 | 247 | 248 | 2 249 | 250 | 251 | 252 | 253 | 1 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | Qt::Horizontal 262 | 263 | 264 | 265 | 40 266 | 20 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 92 280 | 0 281 | 282 | 283 | 284 | Qt::RightToLeft 285 | 286 | 287 | Threads 288 | 289 | 290 | 291 | 292 | 293 | 294 | true 295 | 296 | 297 | 4 298 | 299 | 300 | 301 | 302 | 303 | 304 | Qt::Horizontal 305 | 306 | 307 | 308 | 40 309 | 20 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 3 322 | 368 323 | 381 324 | 131 325 | 326 | 327 | 328 | Options 329 | 330 | 331 | 332 | 333 | 0 334 | 20 335 | 381 336 | 111 337 | 338 | 339 | 340 | 341 | 342 | 343 | Force Fullsize 344 | 345 | 346 | true 347 | 348 | 349 | 350 | 351 | 352 | 353 | Quiet Mode 354 | 355 | 356 | 357 | 358 | 359 | 360 | Generate Quicktime 361 | 362 | 363 | false 364 | 365 | 366 | 367 | 368 | 369 | 370 | Check Outputs 371 | 372 | 373 | false 374 | 375 | 376 | 377 | 378 | 379 | 380 | Light Process 381 | 382 | 383 | 384 | 385 | 386 | 387 | Normal Process 388 | 389 | 390 | true 391 | 392 | 393 | 394 | 395 | 396 | 397 | Heavy Process 398 | 399 | 400 | 401 | 402 | 403 | 404 | Sumo Process 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 3 415 | 508 416 | 381 417 | 16 418 | 419 | 420 | 421 | Qt::Horizontal 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 10 431 | 432 | 433 | 10 434 | 435 | 436 | false 437 | 438 | 439 | false 440 | 441 | 442 | true 443 | 444 | 445 | 446 | -------------------------------------------------------------------------------- /src/ui/nuke/tractorUI/python/pyQtSubmitNuke.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import getpass 4 | import time 5 | 6 | import nuke 7 | from nukescripts import pyQtAppUtils, utils 8 | import tractor.glue.render as tractor 9 | 10 | def initPyQtSubmitDialog( pyQtApp, appArgv=['pyQtSubmitDialog'] ): 11 | from PyQt4 import QtCore, QtGui, uic 12 | 13 | class pyQtSubmitDialog(object): 14 | def __init__(self): 15 | # Set up the user interface from Designer. 16 | 17 | self.ui = uic.loadUi( self.uipath() ) 18 | self.ui.connect(self.ui.renderButton, QtCore.SIGNAL("clicked()"), self.render) 19 | self.ui.connect(self.ui.refreshButton, QtCore.SIGNAL("clicked()"), self.refresh) 20 | nuke.addOnCreate(self.onCreateCallback) 21 | nuke.addOnDestroy(pyQtThreadCleanupCallback, nodeClass='Root') 22 | 23 | self.setdefaults() 24 | 25 | self.ui.show() 26 | 27 | def uipath( self ): 28 | module = sys.modules[self.__module__] 29 | modulepath = os.path.realpath( module.__file__ ) 30 | moduledir = os.path.dirname( modulepath ) 31 | 32 | relativeUIpath = "./pyQtSubmitDialog.ui" 33 | return os.path.join( moduledir, relativeUIpath ) 34 | 35 | def setdefaults(self): 36 | self.ui.frameRange.setText( "1-100" ) 37 | 38 | self.ui.renderName.setText("Default Render") 39 | self.ui.chunkComboBox.setCurrentIndex( 0 ) 40 | self.ui.threadSpinBox.setValue( 4 ) 41 | self.refresh() 42 | 43 | def onCreateCallback(self): 44 | n = nuke.thisNode() 45 | if n.Class() == "Write": 46 | n.setName(n.name()) 47 | self.addItem(n) 48 | 49 | def refresh( self ): 50 | self.ui.treeWidget.clear() 51 | nodes = nuke.allNodes("Write") 52 | for n in nodes: 53 | n.setName(n.name()) 54 | self.addItem(n) 55 | 56 | def addItem(self, n): 57 | item = QtGui.QTreeWidgetItem() 58 | item.setText(0, n['name'].value()) 59 | self.ui.treeWidget.addTopLevelItem(item) 60 | 61 | def selectedWriteNodes( self ): 62 | 63 | selectedNodes = QtCore.QStringList() 64 | 65 | nodeCount = self.ui.treeWidget.topLevelItemCount() 66 | for i in range( nodeCount ): 67 | item = self.ui.treeWidget.topLevelItem(i) 68 | if self.ui.treeWidget.isItemSelected( item ): 69 | selectedNodes.append( str( item.text(0) ) ) 70 | 71 | if len( selectedNodes ) == 0: 72 | nuke.message("You must select a write node. It's onerous I know but live with it.") 73 | 74 | return selectedNodes.join(",") 75 | 76 | def render(self): 77 | render_name = self.ui.renderName.text() 78 | frame_range = self.ui.frameRange.text() 79 | chunk_size = self.ui.chunkComboBox.currentText() 80 | force_fullsize = self.ui.fullsizeCheckbox.isChecked() 81 | quiet_mode = self.ui.quietCheckbox.isChecked() 82 | thread_count = self.ui.threadSpinBox.value() 83 | 84 | if chunk_size == "None": chunk_size = 0; 85 | 86 | self.username = getpass.getuser() 87 | self.timestamp = int( time.time() ) 88 | 89 | #username = "steve" 90 | #timestamp = "123445677" 91 | 92 | if render_name is None: 93 | render_name = "Nuke Render for %s" % self.username 94 | 95 | try: 96 | spooldir = "/mnt/muxfs/users/%s/spool" % self.username # should be built from a template in cfg 97 | exists = utils.executeInMainThreadWithResult( os.path.exists, ( spooldir, ) ) 98 | if not exists: 99 | utils.executeInMainThreadWithResult( os.makedirs, ( spooldir, ) ) 100 | except: 101 | print "Warning. Attempt to create spool directory failed" 102 | 103 | filename = "nuke-%s-%s.nk" % ( self.username, self.timestamp ) 104 | filepath = os.path.join( spooldir, filename ) 105 | 106 | try: 107 | utils.executeInMainThreadWithResult( nuke.scriptSave, (filepath,) ) 108 | print filepath 109 | except: 110 | nuke.message("ERR-001 Unable to save spool file") 111 | 112 | nuke_args = { "type":"Nuke", "threads":thread_count, "fullsize":force_fullsize, "quiet":quiet_mode, "file":filepath, "nodes":self.selectedWriteNodes() } 113 | render_args = { "range":frame_range, "chunksize":chunk_size, "timestamp":self.timestamp, "name":render_name, "user":self.username,} 114 | 115 | print render_args 116 | print nuke_args 117 | 118 | launchRender( render_args, nuke_args ) 119 | 120 | self.ui.close() 121 | 122 | app = pyQtApp.getApplication(appArgv) 123 | dialog = pyQtSubmitDialog() 124 | app.exec_() 125 | 126 | def launchRender( jobargs, cmdargs ): 127 | tr = tractor.Render( jobargs, cmdargs ) 128 | tr.build() 129 | tr.spool() 130 | 131 | def startQtRenderDialog(): 132 | pyQtApp = pyQtAppUtils.pyQtAppHelper(start = True) 133 | pyQtApp.run(initPyQtSubmitDialog, (pyQtApp,)) 134 | return pyQtApp 135 | 136 | def pyQtThreadCleanupCallback(): 137 | import threading 138 | from nukescripts import pyQtAppUtils 139 | 140 | for t in threading.enumerate(): 141 | if(t.getName() == "Nuke_PyQt_Thread"): 142 | pyQtApp = pyQtAppUtils.pyQtAppHelper() 143 | pyQtApp.stop() 144 | -------------------------------------------------------------------------------- /src/ui/nuke/tractorUI/python/simpleSubmitNuke.py: -------------------------------------------------------------------------------- 1 | # stupidly simple render submission panel 2 | 3 | import nuke 4 | import getpass 5 | import time 6 | import tractor.glue.render as tractor 7 | 8 | class submit_gui( object ): 9 | 10 | __renderName = None 11 | __frameRange = "1-100" 12 | __writePulldown = "input global custom" 13 | __threadPulldown = "1 2 3 4 5 6 7 8" 14 | __chunkPulldown = "None 50 20 10 5 2 1" 15 | __writeNodePulldown = "" 16 | __writeNodePath = None 17 | 18 | __proxyCheckbox = False 19 | __quietCheckbox = False 20 | __splitWriteNodesCheckbox = False 21 | 22 | 23 | def __init__( self, *argv, **kwargs ): 24 | self.writeNodes = {} 25 | 26 | allWriteNodes = [n for n in nuke.allNodes() if n.Class() in ('Write')] 27 | 28 | print allWriteNodes 29 | for node in allWriteNodes: 30 | nodename = node.fullName() 31 | nodefilepath = node.knob("file").value() 32 | self.writeNodes[nodename] = nodefilepath 33 | 34 | self.__writeNodePulldown = " ".join( self.writeNodes.keys() ) 35 | if self.writeNodes : 36 | self.__writeNodePath = self.writeNodes.values()[0] 37 | 38 | self.__drawPanel() 39 | 40 | def __drawPanel( self ): 41 | 42 | self.panel = nuke.Panel("Render") 43 | 44 | #self.panel.addEnumerationPulldown("Frame Range", self.__writePulldown) 45 | self.panel.addSingleLineInput( "Render Name", self.__renderName ) 46 | self.panel.addSingleLineInput( "Frame Range", self.__frameRange ) 47 | 48 | #self.panel.addEnumerationPulldown( "Write Node", self.__writeNodePulldown ) 49 | #self.panel.addSingleLineInput( "Output Path", self.__writeNodePath ) 50 | 51 | 52 | self.panel.addEnumerationPulldown("Num Threads", self.__threadPulldown) 53 | self.panel.addEnumerationPulldown("Frames per Chunk", self.__chunkPulldown) 54 | 55 | self.panel.addBooleanCheckBox("Force Fullsize", self.__proxyCheckbox) 56 | self.panel.addBooleanCheckBox("Quiet Mode", self.__quietCheckbox) 57 | 58 | self.panel.addButton( "Cancel" ) 59 | self.panel.addButton( "OK" ) 60 | 61 | #self.panel.setWidth(600) 62 | 63 | def cancel( self ): 64 | pass 65 | 66 | def show( self ): 67 | result = self.panel.show() 68 | 69 | if result == 0: 70 | self.cancel() 71 | elif result ==1: 72 | self.render() 73 | 74 | def showModal( self ): 75 | self.panel.showModalDialog() 76 | 77 | def render( self ): 78 | 79 | frame_range = self.panel.value("Frame Range") 80 | thread_count = self.panel.value("Num Threads") 81 | chunk_size = self.panel.value("Frames per Chunk") 82 | force_fullsize = self.panel.value("Force Fullsize") 83 | quiet_mode = self.panel.value("Quiet Mode") 84 | render_name = self.panel.value("Render Name") 85 | 86 | if chunk_size == "None": 87 | chunk_size = 0 88 | 89 | username = getpass.getuser() 90 | timestamp = int( time.time() ) 91 | 92 | if render_name is None: 93 | render_name = "Nuke Render for %s" % username 94 | 95 | filename = "/tmp/nuke-%s-%s-%s.nk" % ( frame_range, username, timestamp ) 96 | 97 | try: 98 | nuke.scriptSave( filename ) 99 | print filename 100 | except: 101 | nuke.message("ERR-001 Unable to save spool file") 102 | 103 | nuke_args = { "type":"Nuke", "threads":thread_count, "fullsize":force_fullsize, "quiet":quiet_mode, "file":filename, "nodes":self.writeNodes} 104 | render_args = { "range":frame_range, "chunksize":chunk_size, "timestamp":timestamp, "name":render_name, "user":username,} 105 | 106 | print render_args 107 | print nuke_args 108 | 109 | rr = tractor.Render( render_args, nuke_args ) 110 | rr.build() 111 | rr.spool() 112 | 113 | -------------------------------------------------------------------------------- /test/exampleScript.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Example script demonstrating building a simple tree of tasks which 4 | # can be executed of iterated over. 5 | # 6 | 7 | from tractor.api.tasktree import Job, Task, RemoteCmd 8 | import tractor.api.serialize as serialize 9 | 10 | 11 | myjob = Job('Simple Job Script') 12 | 13 | task1 = Task( '1', 'Task number 1' ) 14 | cmd1 = task1.addRemoteCmd(service='Default') 15 | cmd1.addExecutable('echo') 16 | cmd1.addOption('hello') 17 | 18 | myjob.addTask( task1 ) 19 | 20 | task2 = myjob.addTask( '2' ) 21 | cmd2 = RemoteCmd('echo hello') 22 | task2.addRemoteCmd( cmd2 ) 23 | 24 | serializer = tractor.TractorSerializer( myjob ) 25 | serializer.spool( 'stdout' ) 26 | 27 | """ 28 | job = { type':'job', 'label':'Simple Job Script', 29 | 'tasks': { 30 | 'task1': { 31 | 'type':'task', 32 | 'cmds': [ 33 | 'cmd1':{ 34 | 'type':'RemoteCmd', 35 | 'executable':'echo', 36 | 'options':[ 37 | ('hello',), 38 | ] 39 | } 40 | 'cmd2':{ 41 | 'type':'RemoteCmd', 42 | 'executable':'echo', 43 | 'options':[ 44 | ('hello',), 45 | ] 46 | } 47 | ] 48 | }, 49 | 'task2': { 50 | 'type':'task', 51 | 'cmds': [ 52 | 'cmd1':{ 53 | 'type':'RemoteCmd', 54 | 'executable':'echo', 55 | 'options':[ 56 | ('hello',), 57 | ] 58 | } 59 | 'cmd2':{ 60 | 'type':'RemoteCmd', 61 | 'executable':'echo', 62 | 'options':[ 63 | ('hello',), 64 | ] 65 | } 66 | ] 67 | }, 68 | }, 69 | } 70 | 71 | serializer = tractor.api.serialize.AlfSerializer() 72 | serializer = tractor.api.serialize.PySerializer() 73 | serializer = tractor.api.serialize.JSONSerializer() 74 | serializer = tractor.api.serialize.Pickle() 75 | 76 | """ 77 | 78 | 79 | -------------------------------------------------------------------------------- /test/testSpool.py: -------------------------------------------------------------------------------- 1 | from tractor.api import Nuke 2 | from tractor.api import MRfMaya 3 | from tractor.api import Shake 4 | 5 | def NukeSpool( arg ): 6 | cmd_args = { "threads":4, "fullsize":False, "quiet":True, "file":"/tmp/mynukefile.nk"} 7 | job_args = { "title":"Nuke Tractor.Glue Test", "range":"1-30x2,40-53,60", "chunksize":5, "timestamp":"jan31st", "name":"simplerender", "user":"andrew.bunday"} 8 | 9 | job = Nuke( job_args, cmd_args ) 10 | 11 | job.buildTaskTree() 12 | job.spool( arg, startpaused=True ) 13 | 14 | def MayaSpool( arg ): 15 | cmd_args = { "file":"/tmp/mynukefile.ma"} 16 | job_args = { "title":"Maya Tractor.Glue Test" , "range":"1-30x2,40-53,60", "chunksize":5, "timestamp":"jan31st", "name":"simplerender", "user":"andrew.bunday", 'projectpath':'/tmp/stupid/images'} 17 | 18 | job = MRfMaya( job_args, cmd_args ) 19 | 20 | job.buildTaskTree() 21 | job.spool( arg, startpaused=True ) 22 | 23 | 24 | def ShakeSpool( arg ): 25 | cmd_args = { "file":"/tmp/myshakefile.shk"} 26 | job_args = { "title":"Shake Tractor.Glue Test", "range":"1-30x2,40-53,60", "chunksize":5, "timestamp":"jan31st", "name":"simplerender", "user":"andrew.bunday", 'projectpath':'/tmp/stupid/images'} 27 | 28 | job = Shake( job_args, cmd_args ) 29 | 30 | job.buildTaskTree() 31 | job.spool( arg ) 32 | 33 | def test_NukeSpoolStdOut( ): 34 | NukeSpool( 'stdout' ) 35 | 36 | def xtest_NukeSpoolTractor( ): 37 | NukeSpool( 'tractor' ) 38 | 39 | def test_MayaSpoolStdOut( ): 40 | MayaSpool( 'stdout' ) 41 | 42 | def xtest_MayaSpoolTractor( ): 43 | MayaSpool( 'tractor' ) 44 | 45 | def test_ShakeSpoolStdOut( ): 46 | ShakeSpool( 'stdout' ) 47 | 48 | def xtest_ShakeSpoolTractor( ): 49 | ShakeSpool( 'tractor' ) 50 | 51 | -------------------------------------------------------------------------------- /test/testTaskTree.py: -------------------------------------------------------------------------------- 1 | from tractor.api.tasktree import Job, Task 2 | 3 | 4 | def test_taskTree(): 5 | 6 | job = Job('my simple job') 7 | 8 | preflight = job.addTask( 'preflight' ) 9 | preflight.serialsubtasks = True 10 | 11 | preview = preflight.addTask( 'preview' ) 12 | 13 | task = preview.addTask( 'frame_1' ) # three frames to render from our sequence in the preview 14 | task.addCmd( "maya -s 1 -e 1" ) 15 | 16 | task = preview.addTask( 'frame_15' ) 17 | task.addCmd( "maya -s 15 -e 15" ) 18 | 19 | task = preview.addTask( 'frame_30' ) 20 | task.addCmd( "maya -s 30 -e 30" ) 21 | 22 | main = preflight.addTask( 'main' ) # and then the main body of the job broken into 3 chunks 23 | task = main.addTask( 'frame_1-10' ) 24 | task.addCmd( "maya -s 1 -e 10" ) # this is the simple way to define a command to execute 25 | task = main.addTask( 'frame_11-20' ) 26 | task.addCmd( "maya -s 11 -e 20" ) 27 | 28 | task = main.addTask( 'frame_21_30' ) 29 | task.addCmd() 30 | cmd = task.lastCmd 31 | cmd.addExecutable("maya-render") # and this one is a little more convoluted 32 | cmd.addOption( "-r", "mr" ) 33 | cmd.addOption( "-rt", 4 ) 34 | cmd.addOption( "-s", 21 ) 35 | cmd.addOption( "-e", 30 ) 36 | cmd.addOption( "-im", job.title ) 37 | cmd.addOption( "/tmp/maya-1-100-andrew.bunday-1296468388.ma" ) 38 | 39 | print "job tasks: ", job.tasks 40 | print "preflight tasks: ", job.preflight_Node1.tasks 41 | print "preview tasks: ", job.preflight_Node1.preview_Node1.tasks 42 | print "preview frame1 tasks: ", job.preflight_Node1.preview_Node1.frame_1_Node1.tasks 43 | print "preview frame1 cmds: ", job.preflight_Node1.preview_Node1.frame_1_Node1.commands 44 | print "" 45 | print "main cmd frame 21-30 :", job.preflight_Node1.main_Node1.frame_21_30_Node1.lastCmd.executable 46 | print "main cmd frame 21-30 :", job.preflight_Node1.main_Node1.frame_21_30_Node1.lastCmd.flags 47 | 48 | 49 | 50 | --------------------------------------------------------------------------------