# ***** BEGIN LICENSE BLOCK *****
# Version: MPL 1.1/GPL 2.0/LGPL 2.1
#
# The contents of this file are subject to the Mozilla Public License Version
# 1.1 (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
# http://www.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS IS" basis,
# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
# for the specific language governing rights and limitations under the
# License.
#
# The Original Code is the Python Computer Graphics Kit.
#
# The Initial Developer of the Original Code is Matthias Baas.
# Portions created by the Initial Developer are Copyright (C) 2004
# the Initial Developer. All Rights Reserved.
#
# Contributor(s):
#
# Alternatively, the contents of this file may be used under the terms of
# either the GNU General Public License Version 2 or later (the "GPL"), or
# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
# in which case the provisions of the GPL or the LGPL are applicable instead
# of those above. If you wish to allow use of your version of this file only
# under the terms of either the GPL or the LGPL, and not to allow others to
# use your version of this file under the terms of the MPL, indicate your
# decision by deleting the provisions above and replace them with the notice
# and other provisions required by the GPL or the LGPL. If you do not delete
# the provisions above, a recipient may use your version of this file under
# the terms of any one of the MPL, the GPL or the LGPL.
#
# ***** END LICENSE BLOCK *****
# $Id: mayaascii.py,v 1.10 2005/06/15 19:18:46 mbaas Exp $
import sys, types, simplecpp
# The keywords that may be used for the value True
_true_keywords = ["true", "on", "yes"]
# and for the value False
_false_keywords = ["false", "off", "no"]
# splitDAGPath
def splitDAGPath(path):
"""Split a Maya DAG path into its components.
The path is given as a string that may have the form
<namespace>:<path> where <path> is a sequence of strings
separated by '|'.
The return value is a 2-tuple (namespace, names) where namespace
is None if the path did not contain a ':'. names is a list of
individual path names.
Examples:
:time1 -> ('', ['time1'])
|foo|bar -> (None, ['', 'foo', 'bar'])
foo|bar -> (None, ['foo', 'bar'])
"""
if not isinstance(path, types.StringTypes):
raise ValueError, "string type expected as path argument, got %s"%type(path)
namespace = None
n = path.find(":")
if n!=-1:
namespace = path[:n]
path = path[n+1:]
return namespace, path.split("|")
# stripQuotes
def stripQuotes(s):
"""Remove surrounding quotes if there are any.
The function returns the string without surrounding quotes
(i.e. '"foo"' -> 'foo'). If there are no quotes the string
is returned unchanged.
"""
if s[0]=='"':
return s[1:-1]
else:
return s
# MAPreProcessor
class MAPreProcessor(simplecpp.PreProcessor):
"""Preprocess the source file and invoke a callback for each line.
"""
def __init__(self, linehandler):
simplecpp.PreProcessor.__init__(self)
self.linehandler = linehandler
def output(self, s):
# Ignore the preprocessor lines
if s[0:1]!="#":
continue_flag = self.linehandler(s)
if not continue_flag:
self.abort()
# PolyFace
class PolyFace:
"""Stores the data of a polyFace value.
PolyFace objects are returned by the getValue() method of the
Attribute class when the type was "polyFaces".
"""
def __init__(self):
# face (list of ints)
self.f = None
# holes (list of list of ints)
# There's a list if vertex ids for each hole
self.h = []
# face tex coords (list of list of ints)
self.mf = []
# hole tex coords (list of list of ints)
self.mh = []
# tex coords (list of list of 2-tuple (uvset, list of ints))
# There's a list for each loop (#f + #h).
# Each list contains a list of 2-tuples (as there can be more than
# one uvset per face)
self.mu = []
# face colors (list of list of ints)
# There's a list for each loop (#f + #h)
self.fc = []
def __str__(self):
return "<PolyFace %s #holes:%d>"%(self.f, len(self.h))
# hasValidTexCoords
def hasValidTexCoords(self):
loops = [self.f]+self.h
if len(self.mu)!=len(loops):
return False
for mus,loop in zip(self.mu, loops):
if len(mus)==0:
return False
if len(mus[0][1])!=len(loop):
return False
return True
# newLoop
def newLoop(self):
"""This is an internal method used during construction of a poly face.
The method has to be called after a new 'f' or 'h' attribute was
encountered. The method will then allocate a new empty list for the
texture coords and face colors. This list is then filled during
construction.
This ensures that the number of tex coords and color lists matches
the number of loops.
"""
self.mf.append([])
self.mh.append([])
self.mu.append([])
self.fc.append([])
# NurbsCurve
class NurbsCurve:
"""Stores the data of a nurbsCurve value.
NurbsCurve objects are returned by the getValue() method of the
Attribute class when the type was "nurbsCurve".
"""
def __init__(self):
# Degree
self.degree = 0
# Spans
self.spans = 0
# Form attribute (0=open, 1=closed, 2=periodic)
self.form = 0
# Is the curve rational?
self.isrational = False
# Dimension (2 or 3)
self.dimension = 0
# Knots
self.knots = []
# Control vertices
self.cvs = []
# def __str__(self):
# return "<NurbsCurve>"
# NurbsSurface
class NurbsSurface:
"""Stores the data of a nurbsSurface value.
NurbsSurface objects are returned by the getValue() method of the
Attribute class when the type was "nurbsSurface".
"""
def __init__(self):
# Degree in u and v
self.udegree = 0
self.vdegree = 0
# Form attribute in u and v (0=open, 1=closed, 2=periodic)
self.uform = 0
self.vform = 0
# Is the surface rational?
self.isrational = False
# Knots in u and v
self.uknots = []
self.vknots = []
# Control vertices
self.cvs = []
# def __str__(self):
# return "<NurbsSurface>"
# Attribute
class Attribute:
"""This class stores an attribute (i.e. the name and its value).
An Attribute object is initialized with the arguments that were passed
to the onSetAttr() callback of the reader class. The main purpose
of an Attribute object is to convert the value into an appropriate
Python value.
"""
def __init__(self, attr, vals, opts):
"""Constructor.
attr, vals and opts are the parameters of the onSetAttr() callback.
"""
self._attr = stripQuotes(attr)
self._vals = vals
self._opts = opts
def __str__(self):
val = str(self._vals)
if len(val)>10:
val = val[:10]+"..."
return "<Attribute %s %s %s>"%(self._attr, val, self._opts)
# getBaseName
def getBaseName(self):
"""Return the base name of the attribute.
This is the first part of the attribute name (and may actually
refer to another attribute).
".t" -> "t"
".ed[0:11]" -> "ed"
".uvst[0].uvsn" -> "uvst"
"""
a = self._attr.split(".")
b = a[1].split("[")
return b[0]
# getFullName
def getFullName(self):
return self._attr
# getValue
def getValue(self, type=None, n=None):
"""Return the converted value.
type is a string containing the required type of the value.
Valid types are "bool", "int", "float"
"short2", "short3", "long2", "long3",
"double2", "double3", "float2", "float3", "string", "int32Array",
"doubleArray", "polyFaces", "nurbsSurface", "nurbsCurve",
"double4", "float4" (for colors).
"""
# Check if the value type was specified in the setAttr call
valtype = self._opts.get("type", [None])[0]
if valtype==None and type==None:
if filter(lambda x: x in _true_keywords+_false_keywords, self._vals)!=[]:
valtype = "bool"
else:
valtype = "float"
# Use the real attribute type if no required type was specified
if type==None:
type = valtype
# Use the provided type if the attribute had no 'type' option
if valtype==None:
valtype = type
# No type information given?
if valtype==None:
raise ValueError, "No type information available for attribute '%s'"%self.getBaseName()
# Check if the type matches the required type...
if valtype!=type:
raise ValueError, "Attribute of type %s expected, got %s"%(type, valtype)
# Convert the values..
convertername = "convert"+type[0].upper()+type[1:]
f = getattr(self, convertername)
try:
vs = f()
except ValueError, e:
print >>sys.stderr, e
# Try a string conversion when no type was specified...
if type==None and convertername!="convertString":
vs = self.convertString()
else:
raise
if n==None:
return vs
if len(vs)!=n:
raise ValueError, "%s: %d values expected, got %d"%(self._attr, n, len(vs))
if n==1:
return vs[0]
else:
return vs
# The following convert methods have to convert the value list (self._vals)
# into the appropriate type. The return value is always a list of values.
def convertInt(self):
return map(lambda x: int(x), self._vals)
def convertFloat(self):
return map(lambda x: float(x), self._vals)
def convertBool(self):
res = []
for v in self._vals:
res.append(v in _true_keywords)
return res
def convertLong2(self):
res = []
vs = self._vals
for i in range(0,len(vs), 2):
res.append((int(vs[i]), int(vs[i+1])))
return res
convertShort2 = convertLong2
def convertLong3(self):
res = []
vs = self._vals
for i in range(0,len(vs), 3):
res.append((int(vs[i]), int(vs[i+1]), int(vs[i+2])))
return res
convertShort3 = convertLong3
def convertDouble2(self):
res = []
vs = self._vals
for i in range(0,len(vs), 2):
res.append((float(vs[i]), float(vs[i+1])))
return res
convertFloat2 = convertDouble2
def convertDouble3(self):
res = []
vs = self._vals
for i in range(0,len(vs), 3):
res.append((float(vs[i]), float(vs[i+1]), float(vs[i+2])))
return res
convertFloat3 = convertDouble3
def convertDouble4(self):
res = []
vs = self._vals
for i in range(0,len(vs), 4):
res.append((float(vs[i]), float(vs[i+1]), float(vs[i+2]), float(vs[i+3])))
return res
convertFloat4 = convertDouble4
def convertString(self):
return map(lambda x: str(x), self._vals)
def convertInt32Array(self):
n = int(self._vals[0])
return map(lambda x: int(x), self._vals[1:n+1])
def convertDoubleArray(self):
n = int(self._vals[0])
return map(lambda x: float(x), self._vals[1:n+1])
def convertPolyFaces(self):
res = []
vs = self._vals
i=0
pf = None
while i<len(vs):
c = vs[i]
i+=1
if c not in ["f", "h", "mf", "mh", "mu", "fc"]:
raise ValueError, "Unknown polyFace data: %s"%c
if c=="mu":
uvset = int(vs[i])
i+=1
n = int(vs[i])
ids = map(lambda x: int(x), vs[i+1:i+n+1])
i+=n+1
# Is that already a new polyFace? Then store the previous one
# and start a new face
if c=="f":
if pf!=None:
res.append(pf)
pf = PolyFace()
pf.f = ids
pf.newLoop()
elif c=="h":
pf.h.append(ids)
pf.newLoop()
elif c=="mu":
pf.mu[-1].append((uvset, ids))
else:
# Set the IDs into the corresponding list
lst = getattr(pf, c)
lst[-1] = ids
# Append the last face
if pf!=None:
res.append(pf)
return res
def convertNurbsCurve(self):
res = []
vs = self._vals
while vs!=[]:
nc = NurbsCurve()
nc.degree = int(vs[0])
nc.spans = int(vs[1])
nc.form = int(vs[2])
nc.isrational = vs[3] in _true_keywords
nc.dimension = int(vs[4])
# Get knots
n = int(vs[5])
nc.knots = map(lambda x: float(x), vs[6:n+6])
vs = vs[n+6:]
# Get CVs
n = int(vs[0])
dim = nc.dimension
if nc.isrational:
dim += 1
cvs = []
for i in range(n):
cvs.append( tuple(map(lambda x: float(x), vs[1+i*dim:1+(i+1)*dim])) )
nc.cvs = cvs
vs = vs[1+n*dim:]
res.append(nc)
return res
def convertNurbsSurface(self):
res = []
vs = self._vals
while vs!=[]:
ns = NurbsSurface()
ns.udegree = int(vs[0])
ns.vdegree = int(vs[1])
ns.uform = int(vs[2])
ns.vform = int(vs[3])
ns.isrational = vs[4] in _true_keywords
vs = vs[5:]
# Get u knots
n = int(vs[0])
ns.uknots = map(lambda x: float(x), vs[1:n+1])
vs = vs[n+1:]
# Get v knots
n = int(vs[0])
ns.vknots = map(lambda x: float(x), vs[1:n+1])
vs = vs[n+1:]
# Skip TRIM|NOTRIM
if vs[0] in ["TRIM", "NOTRIM"]:
vs = vs[1:]
# Get CVs
n = int(vs[0])
if ns.isrational:
dim = 4
else:
dim = 3
cvs = []
for i in range(n):
cvs.append( tuple(map(lambda x: float(x), vs[1+i*dim:1+(i+1)*dim])) )
ns.cvs = cvs
vs = vs[1+n*dim:]
res.append(ns)
return res
# setValue
def setValue(self, value):
pass
# MultiAttrStorage
class MultiAttrStorage:
"""This helper class serves as MEL style array.
You can assign values to arbitrary indices which will automatically
enlarge the array (filling missing values with None).
The slicing operation is different than Python as the stop index is
inclusive. The actual array is stored in the _array attribute.
Example:
a = MultiAttrStorage()
a[2] = 5 # -> [None, None, 5]
a[4:6] = [1,2,3] # -> [None, None, 5, None, 1, 2, 3]
Reading an arbitrary attribute from an object of this class will
automatically create this attribute which will in turn be of type
MultiAttrStorage. So this class can also be used for compound objects.
If you want to check such a compound for a particular attribute you
must not use hasattr() as this would silently create the attribute if
it didn't already exist and always return True. Instead you have to
do the check as follows:
if <attrname> in dir(compound):
...
"""
def __init__(self):
self._array = []
def __str__(self):
namedattrs = filter(lambda x: x[0:1]!="_", self.__dict__.keys())
# Not an array but a 'struct' with named attributes...
if namedattrs!=[] and self._array==[]:
a = []
for name in namedattrs:
a.append(".%s:%s"%(name, getattr(self, name)))
return ", ".join(a)
else:
return "["+", ".join(map(lambda x: str(x), self._array))+"]"
def __iter__(self):
return iter(self._array)
def __len__(self):
return len(self._array)
def __getattr__(self, name):
if name[:2]=="__":
raise AttributeError, name
ma = MultiAttrStorage()
setattr(self, name, ma)
return ma
def __getitem__(self, key):
if key>=len(self._array):
ma = MultiAttrStorage()
self.__setitem__(key, ma)
return ma
else:
return self._array[key]
def __setitem__(self, key, value):
al = len(self._array)
if type(key)==slice:
if key.stop>=al:
self._array += (key.stop-al+1)*[None]
if len(value)!=(key.stop-key.start+1):
raise ValueError, "%d values expected, got %d"%(key.stop-key.start+1, len(value))
self._array[key.start:key.stop+1] = value
# print key.start,key.stop
else:
if key<al:
self._array[key] = value
else:
self._array += (key-al)*[None] + [value]
# Node
class Node:
"""A generic Maya node class.
This is a helper class which may be used in a concrete implementation
of the MAReader class to represent Maya nodes.
This class does not implement the actual functionality of a
particular Maya node, it just tracks attribute changes and
connections which can later be retrieved once the entire file was
read. So this class can be used for all Maya nodes in a file.
"""
def __init__(self, nodetype, opts, parent=None):
"""Constructor.
nodetype and opts are the arguments of the onCreateNode()
callback of the MAReader class.
parent is the parent Node object or None.
"""
# Do some type checking...
if not isinstance(nodetype, types.StringTypes):
raise ValueError, "Argument 'nodetype' must be a string, got %s."%(type(nodetype))
if type(opts)!=dict:
raise ValueError, "Argument 'opts' must be a dict, got %s."%(type(opts))
if parent!=None and not isinstance(parent, Node):
raise ValueError, "Argument 'parent' must be a Node object or None, got %s."%(type(parent))
# A string containing the node type
self.nodetype = nodetype
# The options dictionary
self.opts = opts
# Parent Node object
self._parent = None
# Children Node objects
self._children = []
# Attribute values.
# Key: Attribute base name / Value: List of Attribute objects
self._setattr = {}
# If True, accessing attributes will automatically create a new
# attribute that contains a MultiAttrStorage object
self._create_attributes = False
# Key: Local attribute / Value: (nodename, attrname) of source
self.in_connections = {}
# Key: Local attribute / Value: List of (node, nodename, attrname) tuples
self.out_connections = {}
# Set the parent
self.setParent(parent)
def __str__(self):
return '<Node "%s" %s>'%(self.getFullName(), self.nodetype)
def __getattr__(self, name):
if self._create_attributes:
ma = MultiAttrStorage()
setattr(self, name, ma)
return ma
raise AttributeError, name
# getName
def getName(self):
"""Return the node name.
If no node name was specified during the creation of the object,
the dummy name 'MayaNode' is returned.
"""
return self.opts.get("name", ["MayaNode"])[0]
# getFullName
def getFullName(self):
"""Return the full node name.
"""
name = self.getName()
if self._parent==None:
return "|%s"%name
else:
return "%s|%s"%(self._parent.getFullName(), name)
# getParentName
def getParentName(self):
"""Return the parent's node name or None.
"""
if self._parent==None:
return None
else:
return self._parent.getFullName()
# getParent
def getParent(self):
"""Return the parent Node object or None.
"""
return self._parent
# setParent
def setParent(self, parent):
"""Reparent the Node object.
"""
# Remove self from the previous parent's children list...
if self._parent!=None:
self._parent._children.remove(self)
# Set the new parent...
self._parent = parent
if parent!=None:
parent._children.append(self)
# iterChildren
def iterChildren(self):
"""Return an iterator that yields all children Node objects.
"""
return iter(self._children)
# setAttr
def setAttr(self, attr, vals, opts):
"""Store the attribute value.
The arguments are the same than the arguments of the onSetAttr()
callback in the MAReader class.
The final Python value can be retrieved with the getAttrValue()
method.
"""
# Is this setattr call only used to "declare" the size of an array?
# then ignore the call
if vals==[] and "size" in opts:
return
a = Attribute(attr, vals, opts)
basename = a.getBaseName()
if basename in self._setattr:
self._setattr[basename].append(a)
else:
self._setattr[basename] = [a]
# addAttr
def addAttr(self, opts):
pass
# addInConnection
def addInConnection(self, localattr, nodename, attrname):
"""Add an 'in' connection.
'in' = node.attr is connected to localattr
nodename is the name of a Node object and attrname the full
attribute name.
"""
self.in_connections[localattr] = (nodename, attrname)
# addutConnection
def addOutConnection(self, localattr, node, nodename, attrname):
"""Add an 'out' connection.
'out' = localattr is connected to node.attr
node is a Node object, nodename the name of the node and attrname
the full attribute name.
"""
if not isinstance(node, Node):
raise ValueError, "Argument 'node' must be a Node instance."
if self.out_connections.has_key(localattr):
self.out_connections[localattr].append((node, nodename, attrname))
else:
self.out_connections[localattr] = [(node, nodename, attrname)]
# getAttrValue
def getAttrValue(self, lname, sname, type, n=1, default=None):
"""Get the Python value of an attribute.
lname is the long name, sname the short name. type is the
required type and n the required number of elements. type and
n may be None.
The return value is either a normal Python type (int, float, sequence)
or a MultiAttrStorage object in cases where the setAttr command
contained the index operator.
When no attribute with the given long or short name could be
found the provided default value is returned.
This method will only return a value other than the default
value when the \method{setAttr()} was called with the corresponding
attribute.
"""
# 'Execute' the stored setAttr commands for the long name and
# the short name. This will store the value as an attribute
# of self.
self._executeAttrs(lname, type, n)
self._executeAttrs(sname, type, n)
# Check if the long name is available, otherwise try the short name...
if hasattr(self, lname):
return getattr(self, lname)
return getattr(self, sname, default)
# getInNode
def getInNode(self, localattr_long, localattr_short):
"""Return the node and attribute that serves as input for localattr.
The return value is a 2-tuple (nodename, attrname) that specifies
the input connection for localattr. (None, None) is returned if there
is no connection.
"""
node, attr = self.in_connections.get(localattr_long, (None, None))
if node==None:
node, attr = self.in_connections.get(localattr_short, (None, None))
return node, attr
# getOutNodes
def getOutNodes(self, localattr_long, localattr_short):
"""Return the nodes and attributes that this attribute connects to.
The return value is a list of 3-tuples (node, nodename, attrname) that
specify the output connections for localattr.
"""
lst = self.out_connections.get(localattr_long, None)
if lst==None:
lst = self.out_connections.get(localattr_short, [])
return lst
# getOutAttr
def getOutAttr(self, localattr_long, localattr_short, dstnodetype):
"""Check if a local attribute is connected to a particular type of node.
Returns a tuple (node, attrname) where node is the Node object
of the destination node and attrname the name of the destination
attribute. If there is no connection with a node of type dstnodetype,
the method returns (None,None).
If the attribute is connected to more than one node with the
given type or to several attributes of the same node then only
the first connection encountered is returned.
"""
cs = self.getOutNodes(localattr_long, localattr_short)
# Search the connections for one that goes to a node with
# the specified type...
for node,nodename,attrname in cs:
if node.nodetype==dstnodetype:
return node, attrname
return None,None
# _executeAttrs
def _executeAttrs(self, basename, type, n):
"""Retrieve the Python values out of an attribute.
All stored attributes with the given basename will be 'executed'.
This means, their Python value will be obtained and stored
in the node under the same name than the original attribute.
type is the required type of the attribute (may be None) and
n the required number of elements (or None = Dynamic array).
"""
self._create_attributes = True
efftype = type
for attr in self._setattr.get(basename, []):
# Keep the type information once an attribute explicitly
# provided some info. Later attributes that don't specify
# the type (as can happen with the texture coordinates) can
# then be read properly.
if type==None:
valtype = attr._opts.get("type", [None])[0]
if valtype!=None:
efftype = valtype
v = attr.getValue(efftype, n)
# Unpack the value if the number of elements wasn't known and
# it turned out to be only one element (in this case, the below
# assignment will be of the form a[i]=v instead of a[i:j]=v)
if n==None and len(v)==1:
v = v[0]
if attr.getBaseName() in ["in", "from", "for", "if", "while",
"else", "elif", "exec"]:
cmd = "setattr(self, '%s', v)"%attr.getFullName()[1:]
else:
cmd = "self%s = v"%attr.getFullName()
if v!=[]:
exec cmd
self._create_attributes = False
######################################################################
# MAReader
class MAReader:
"""Low level MA (Maya ASCII) reader.
The MAReader class reads Maya ASCII files and calls handler
methods which have to be implemented in a derived class. The
content of the file is actually a subset of the Maya Embedded
Language (MEL) which is the scripting language implemented inside
Maya. The MAReader parses the file, breaks down the content of the
file in commands and their arguments and options (expressions are
not evaluated). Each MEL command will then trigger a callback
method that has to execute the command. These callback methods
have to be implemented in a derived class.
There are 12 MEL commands that can appear in a Maya ASCII file:
- file
- requires
- fileInfo
- currentUnit
- createNode
- setAttr
- addAttr
- connectAttr
- disconnectAttr
- parent
- select
- lockNode
Each command has a number of arguments and can also take
options. The callback methods receive the arguments as regular
arguments to the method and the options as an additional argument
opts which is a dictionary containing the options that were
specified in the file. The key is the long name of the option
(without leading dash) and the value is a list of strings
containing the option values. The number of values and how they
have to be interpreted depend on the actual option.
The callbacks may access a few instance variables that carry
further information:
- filename: The name of the ma file
- cmd_start_linenr: The line number where the current command began
- cmd_end_linenr: The line number where the current command ended
"""
def __init__(self):
# createNode options
self.createNode_name_dict = { "n":"name", "p":"parent", "s":"shared", "ss":"skipSelect" }
self.createNode_opt_def = { "name" : (1, None),
"parent" : (1, None),
"shared" : (0, None),
"skipSelect" : (0, None) }
# setAttr options
self.setAttr_name_dict = { "k":"keyable",
"l":"lock",
"cb":"channelBox",
"ca":"caching",
"s":"size",
"typ":"type",
"av":"alteredValue",
"c":"clamp" }
self.setAttr_opt_def = { "keyable" : (1, None),
"lock" : (1, None),
"channelBox" : (1, None),
"caching" : (1, None),
"size" : (1, None),
"type" : (1, None),
"alteredValue" : (0, None),
"clamp" : (0, None)}
# fileInfo options
self.fileInfo_name_dict = { "rm":"remove" }
self.fileInfo_opt_def = { "remove" : (1, None)}
# currentUnit options
self.currentUnit_name_dict = { "l":"linear",
"a":"angle",
"t":"time",
"f":"fullName",
"ua":"updateAnimation"}
self.currentUnit_opt_def = { "linear" : (1, None),
"angle" : (1, None),
"time" : (1, None),
"fullName" : (0, None),
"updateAnimation" : (1, None)}
# connectAttr options
self.connectAttr_name_dict = { "l":"lock",
"f":"force",
"na":"nextAvailable",
"rd":"referenceDest" }
self.connectAttr_opt_def = { "lock" : (1, None),
"force" : (0, None),
"nextAvailable" : (0, None),
"referenceDest" : (1, None) }
# disconnectAttr options
self.disconnectAttr_name_dict = { "na":"nextAvailable" }
self.disconnectAttr_opt_def = { "nextAvailable" : (0, None)}
# parent options
self.parent_name_dict = { "w":"world",
"r":"relative",
"a":"absolute",
"add":"addObject",
"rm":"removeObject",
"s":"shape",
"nc":"noConnections" }
self.parent_opt_def = { "world" : (0, None),
"relative" : (0, None),
"absolute" : (0, None),
"addObject" : (0, None),
"removeObject" : (0, None),
"shape" : (0, None),
"noConnections" : (0, None) }
# select options
self.select_name_dict = { "adn":"allDependencyNodes",
"ado":"allDagObjects",
"vis":"visible",
"hi":"hierarchy",
"af":"addFirst",
"r":"replace",
"d":"deselect",
"tgl":"toggle",
"cl":"clear",
"ne":"noExpand" }
self.select_opt_def = { "all" : (0, None),
"allDependencyNodes" : (0, None),
"allDagObjects" : (0, None),
"visible" : (0, None),
"hierarchy" : (0, None),
"add" : (0, None),
"addFirst" : (0, None),
"replace" : (0, None),
"deselect" : (0, None),
"toggle" : (0, None),
"clear" : (0, None),
"noExpand" : (0, None) }
# addAttr options
self.addAttr_name_dict = { "ln":"longName",
"sn":"shortName",
"bt":"binaryTag",
"at":"attributeType",
"dt":"dataType",
"dv":"defaultValue",
"m":"multi",
"im":"indexMatters",
"min":"minValue",
"hnv":"hasMinValue",
"max":"maxValue",
"hxv":"hasMaxValue",
"ci":"cachedInternally",
"is":"internalSet",
"p":"parent",
"nc":"numberOfChildren",
"uac":"usedAsColor",
"h":"hidden",
"r":"readable",
"w":"writable",
"s":"storable",
"k":"keyable",
"smn":"softMinValue",
"hsn":"hasSoftMinValue",
"smx":"softMaxValue",
"hsx":"hasSoftMaxValue",
"en":"enumName" }
self.addAttr_opt_def = { "longName" : (1, None),
"shortName" : (1, None),
"binaryTag" : (1, None),
"attributeType" : (1, None),
"dataType" : (1, None),
"defaultValue" : (1, None),
"multi" : (0, None),
"indexMatters" : (1, None),
"minValue" : (1, None),
"hasMinValue" : (1, None),
"maxValue" : (1, None),
"hasMaxValue" : (1, None),
"cachedInternally" : (1, None),
"internalSet" : (1, None),
"parent" : (1, None),
"numberOfChildren" : (1, None),
"usedAsColor" : (0, None),
"hidden" : (1, None),
"readable" : (1, None),
"writable" : (1, None),
"storable" : (1, None),
"keyable" : (1, None),
"softMinValue" : (1, None),
"hasSoftMinValue" : (1, None),
"softMaxValue" : (1, None),
"hasSoftMaxValue" : (1, None),
"enumName" : (1, None) }
# file options (incomplete)
self.file_name_dict = { "bls":"buildLoadSettings",
"c":"command",
"dns":"defaultNamespace",
"dr":"deferReference",
"f":"force",
"fr":"flushReference",
"gl":"groupLocator",
"gn":"groupName",
"gr":"groupReference",
"ir":"importReference",
"lck":"lockReference",
"lf":"lockFile",
"lrd":"loadReferenceDepth",
"lad":"loadAllDeferred",
"lar":"loadAllReferences",
"lnr":"loadNoReferences",
"lr":"loadReference",
"ls":"loadSettings",
"pr":"preserveReferences",
"new":"newFile",
"o":"open",
"op":"options",
"pmt":"prompt",
"r":"reference",
"ra":"renameAll",
"rdi":"referenceDepthInfo",
"rfn":"referenceNode",
"rpr":"renamingPrefix",
"shd":"sharedNodes",
"sns":"swapNamespace",
"srf":"sharedReferenceFile",
"str":"strict",
"ns":"namespace",
# The following flags are not documented in the Maya docs
"pm":"proxyManager",
"pt":"proxyTag",
"ap":"activeProxy" }
self.file_opt_def = { "buildLoadSettings" : (0, None),
"command" : (1, None),
"defaultNamespace" : (0, None),
"deferReference" : (1, None),
"force" : (0, None),
"flushReference" : (1, None),
"groupLocator" : (0, None),
"groupName" : (1, None),
"groupReference" : (0, None),
"importReference" : (0, None),
"lockReference" : (0, None),
"lockFile" : (1, None),
"loadReferenceDepth" : (1, None),
"loadAllDeferred" : (1, None),
"loadAllReferences" : (0, None),
"loadNoReferences" : (0, None),
"loadReference" : (1, None),
"loadSettings" : (1, None),
"preserveReferences" : (0, None),
"newFile" : (0, None),
"open" : (0, None),
"options" : (1, None),
"prompt" : (1, None),
"reference" : (0, None),
"renameAll" : (1, None),
"referenceDepthInfo" : (1, None),
"referenceNode" : (1, None),
"renamingPrefix" : (1, None),
"sharedNodes" : (1, None),
"swapNamespace" : (2, None),
"sharedReferenceFile" : (0, None),
"strict" : (1, None),
"namespace" : (1, None),
"proxyManager" : (1, None),
"proxyTag" : (1, None),
"activeProxy" : (0, None) }
# lockNode options
self.lockNode_name_dict = { "l":"lock",
"ic":"ignoreComponents" }
self.lockNode_opt_def = { "lock" : (1, None),
"ignoreComponents" : (0, None) }
# Provide linenr as an alias for cmd_start_linenr
@property
def linenr(self):
return self.cmd_start_linenr
def read(self, f):
"""Read a MA file and invoke the callbacks.
f is a file-like object or the name of a file.
"""
self.begin()
if isinstance(f, types.StringTypes):
self.filename = f
else:
self.filename = getattr(f, "name", "?")
# A flag that indicates if a new MEL command is about to begin
self.new_cmd = True
# The name of the current MEL command
self.cmd = None
# The arguments of the current MEL command
self.args = None
# This flag specifies whether reading the file should continue or not
self.continue_flag = True
# The line number where the current MEL command began
self.cmd_start_linenr = None
# The line number where the current MEL command ended
self.cmd_end_linenr = None
cpp = MAPreProcessor(self.lineHandler)
self.cpp = cpp
# Read the file and invoke the lineHandler for each line...
cpp(f)
# Execute the last command
self.processCommands(";")
self.end()
def lineHandler(self, s):
# self.linenr += 1
z = s.strip()
if z!="":
self.processCommands(z)
return self.continue_flag
def abort(self):
"""Stop reading the MA file.
This method can be called by a callback method to abort
reading the file.
"""
self.continue_flag = False
def begin(self):
"""Callback that is invoked before the file is read."""
pass
def end(self):
"""Callback that is invoked after the file was read."""
pass
def onFile(self, filename, opts):
"""Callback for the 'file' MEL command."""
pass
# print "file", filename, opts
def onRequires(self, product, version):
"""Callback for the 'requires' MEL command."""
pass
# print "requires",product, version
def onFileInfo(self, keyword, value, opts):
"""Callback for the 'fileInfo' MEL command."""
pass
# print "fileInfo",keyword, value
def onCurrentUnit(self, opts):
"""Callback for the 'currentUnit' MEL command."""
pass
# print "currentUnit",opts
def onCreateNode(self, nodetype, opts):
"""Callback for the 'createNode' MEL command."""
pass
# print "createNode", nodetype, opts
def onSetAttr(self, attr, vals, opts):
"""Callback for the 'setAttr' MEL command."""
pass
# print "setAttr %s = %s %s"%(attr, vals, opts)
def onConnectAttr(self, srcattr, dstattr, opts):
"""Callback for the 'connectAttr' MEL command."""
pass
# print "connectAttr %s %s %s"%(srcattr, dstattr, opts)
def onDisconnectAttr(self, srcattr, dstattr, opts):
"""Callback for the 'disconnectAttr' MEL command."""
pass
# print "disconnectAttr %s %s %s"%(srcattr, dstattr, opts)
def onAddAttr(self, opts):
"""Callback for the 'addAttr' MEL command."""
pass
# print "addAttr", opts
def onParent(self, objects, parent, opts):
"""Callback for the 'parent' MEL command."""
pass
# print "parent",objects,parent,opts
def onSelect(self, objects, opts):
"""Callback for the 'select' MEL command."""
pass
# print "select",objects,opts
def onLockNode(self, objects, opts):
"""Callback for the 'lockNode' MEL command.
objects is a list of objects (which may be empty).
"""
pass
# print "lockNode",objects,opts
# onCommand
def onCommand(self, cmd, args):
"""Generic command callback.
This callback invokes the "per command" callbacks.
cmd is the MEL command name and args is a list of strings
that are the arguments of the command. The arguments have
been split into their individual tokens. Quotes around
quoted tokens are still present.
Example:
The MEL command setAttr -k off ".v"; would be passed in
as onCommand('setAttr', ['-k', 'off', '"v"'])
"""
# print "**",cmd, args
# setAttr
if cmd=="setAttr":
args, opts = self.getOpt(args,
self.setAttr_opt_def,
self.setAttr_name_dict)
self.onSetAttr(args[0], args[1:], opts)
# createNode
elif cmd=="createNode":
args, opts = self.getOpt(args,
self.createNode_opt_def,
self.createNode_name_dict)
self.onCreateNode(args[0], opts)
# connectAttr
elif cmd=="connectAttr":
args, opts = self.getOpt(args,
self.connectAttr_opt_def,
self.connectAttr_name_dict)
self.onConnectAttr(args[0], args[1], opts)
# disconnectAttr
elif cmd=="disconnectAttr":
args, opts = self.getOpt(args,
self.disconnectAttr_opt_def,
self.disconnectAttr_name_dict)
self.onDisconnectAttr(args[0], args[1], opts)
# addAttr
elif cmd=="addAttr":
args, opts = self.getOpt(args,
self.addAttr_opt_def,
self.addAttr_name_dict)
self.onAddAttr(opts)
# parent
elif cmd=="parent":
args, opts = self.getOpt(args,
self.parent_opt_def,
self.parent_name_dict)
self.onParent(args[:-1], args[-1], opts)
# select
elif cmd=="select":
args, opts = self.getOpt(args,
self.select_opt_def,
self.select_name_dict)
self.onSelect(args, opts)
# fileInfo
elif cmd=="fileInfo":
args, opts = self.getOpt(args,
self.fileInfo_opt_def,
self.fileInfo_name_dict)
self.onFileInfo(args[0], args[1], opts)
# currentUnit
elif cmd=="currentUnit":
args, opts = self.getOpt(args,
self.currentUnit_opt_def,
self.currentUnit_name_dict)
self.onCurrentUnit(opts)
# requires
elif cmd=="requires":
args, opts = self.getOpt(args, {}, {})
self.onRequires(args[0], args[1])
# file
elif cmd=="file":
args, opts = self.getOpt(args,
self.file_opt_def,
self.file_name_dict)
self.onFile(args[0], opts)
# lockNode
elif cmd=="lockNode":
args, opts = self.getOpt(args,
self.lockNode_opt_def,
self.lockNode_name_dict)
self.onLockNode(args, opts)
# unknown
else:
print >>sys.stderr, "WARNING: %s, line %d: Unknown MEL command: '%s'"%(self.filename, self.cmd_start_linenr, cmd)
# getOpt
def getOpt(self, arglist, opt_def, name_dict):
"""Separate arguments from options and preprocess options.
arglist is a list of arguments (i.e. the 'command line').
opt_def specifies the available options and their respective
number of arguments. name_dict is a dictionary that is used
to convert short names into long names.
The return value is a 2-tuple (args, opts) where args is a list
of arguments and opts is a dictionary containing the options.
The key is the long name of the option (without leading dash)
and the value is a list of values. Any existing quotes around
a value is removed (only around the option values, not around
the args!).
"""
args = []
opts = {}
i=0
while i<len(arglist):
arg = arglist[i]
i += 1
try:
float(arg)
is_number = True
except:
is_number = False
# Option?
a = stripQuotes(arg)
if a[0:1]=="-" and not is_number:
# Convert short names into long names...
optname = name_dict.get(a[1:], a[1:])
# Check if the option is known
if optname not in opt_def:
raise SyntaxError, "Unknown option in line %d: %s"%(self.cmd_start_linenr, optname)
# Get the number of arguments
numargs, filter = opt_def[optname]
optvals = [stripQuotes(x) for x in arglist[i:i+numargs]]
# Did the same option already appear? So this is a multi-use flag.
# Then extend the current list with the new values
if optname in opts:
opts[optname].extend(optvals)
else:
opts[optname] = optvals
i += numargs
else:
args.append(arg)
return args, opts
# processCommands
def processCommands(self, s):
"""Process one or more commands.
s is a string that contains one line of MEL code (may be several
commands or only a partial command that is continued in the next
line). This method splits the arguments and calls onCommand()
for every command found.
"""
# Split the command into tokens...
a,n = self.splitCommand(s)
if a!=[]:
# Does a new command begin? then set the command name
# and the args, otherwise just append to the existing args
if self.new_cmd:
self.cmd = a[0]
self.args = a[1:]
# Store the line number where the command began
self.cmd_start_linenr = self.cpp.context.start_linenr
else:
self.args += a
if n==-1:
# The command isn't finished yet
if self.cmd!=None:
self.new_cmd = False
else:
# The command is finished, so execute it
if self.cmd!=None:
# Store the line number where the command ended
self.cmd_end_linenr = self.cpp.context.linenr
self.onCommand(self.cmd, self.args)
self.new_cmd = True
self.cmd = None
self.args = []
self.processCommands(s[n+1:])
# splitCommand
def splitCommand(self, s):
"""Split a command into its arguments.
This is an extended version of the string split() method. It
splits (using whitespace as separator) but takes quoted strings
and ';' into account.
Returns a list of strings and the position of the ';'
that terminated the first command (or -1).
The quotes around strings are kept.
'setAttr -k off ".v";' -> (['setAttr', '-k', 'off', '".v"'], 19)
"""
# Search for a quoted string
b,e = self.findString(s)
# Search for the first semicolon (which might be the true command
# separator or not)
n = s.find(";")
# Was a semicolon before the first string? Then the string belongs
# to a subsequent command, so ignore it for now
if n!=-1 and n<b:
b = e = None
# No string found?
if b==None:
if n==-1:
return s.split(), -1
else:
return s[:n].split(), n
else:
if e==None:
return s[:b].split() + [s[b:]+'"'], -1
else:
s2,n = self.splitCommand(s[e+1:])
if n!=-1:
n += e+1
return s[:b].split() + [s[b:e+1]] + s2, n
# findString
def findString(self, s):
"""Find the first string occurence.
The return value is a 2-tuple (begin, end) with the indices
of the opening and closing apostrophes (can also be None).
'foo' -> (None, None)
'a="foo"' -> (2, 6)
'a="foo' -> (2, None)
'a="foo \" spam"' -> (2,14)
"""
#'
offset = 0
while 1:
# Search the beginning of a string
n1 = s.find('"', offset)
if n1==-1:
return None,None
# Search the end of the string (ignore quoted apostrophes)...
start = n1+1
n2 = None
while 1:
n2 = s.find('"', start)
if n2==-1:
return n1,None
elif s[n2-1]!='\\':
return n1,n2
else:
start = n2+1
# DefaultMAReader
class DefaultMAReader(MAReader):
"""Default MA reader implementation.
This class creates Node objects, sets attributes and does the
connections so that after the file is read the entire dependency
graph is available.
A derived class only has to implement the end() callback and
process the graph as desired. All created Node objects are available
in the attribute self.nodelist.
"""
def read(self, f):
# A dict with imported Node objects
# Key: Node name (without path) / Value: Node object
# If the node name is not unique anymore, the value contains None.
self.nodes = {}
# A list with all Node objects (in the same order as they were
# encountered in the file)
self.nodelist = []
# The currently active Node object
# (changes with every createNode or select command)
self.currentnode = None
MAReader.read(self, f)
# onCreateNode
def onCreateNode(self, nodetype, opts):
"""Create a new node and make it current.
"""
# Remove all quotes...
nodetype = stripQuotes(nodetype)
# for name in opts:
# opts[name] = map(lambda x: stripQuotes(x), opts[name])
node = self.createNode(nodetype, opts)
self.currentnode = node
# onSelect
def onSelect(self, objects, opts):
"""Make another node current."""
# Remove all quotes...
objects = map(lambda x: stripQuotes(x), objects)
if opts!={"noExpand":[]}:
raise ValueError, "%s, %d: The select command contains unsupported options."%(self.filename, self.linenr)
if len(objects)==0:
raise ValueError, "%s, %d: The select command contains no object name."%(self.filename, self.linenr)
if len(objects)!=1:
raise ValueError, "%s, %d: The select command contains more than one object."%(self.filename, self.linenr)
self.currentnode = self.findNode(objects[0], create=True)
# onSetAttr
def onSetAttr(self, attr, vals, opts):
"""Set an attribute."""
if self.currentnode==None:
return
# Remove the quotes...
attr = stripQuotes(attr)
# for name in opts:
# opts[name] = map(lambda x: stripQuotes(x), opts[name])
if attr[0]!=".":
print >>sys.stderr, "mayaascii: Warning: DefaultMAReader.onSetAttr(): The attribute refers to a different object than the current object. This is not yet supported."
self.currentnode.setAttr(attr, vals, opts)
# onAddAttr
def onAddAttr(self, opts):
"""Add a dynamic attribute."""
if self.currentnode==None:
return
# Remove the quotes...
# for name in opts:
# opts[name] = map(lambda x: stripQuotes(x), opts[name])
self.currentnode.addAttr(opts)
# onConnectAttr
def onConnectAttr(self, srcattr, dstattr, opts):
"""Make a connection.
"""
# Remove the quotes...
srcattr = stripQuotes(srcattr)
dstattr = stripQuotes(dstattr)
# for name in opts:
# opts[name] = map(lambda x: stripQuotes(x), opts[name])
# Split into object name and attribute name
a = srcattr.split(".")
snode = a[0]
sattr = a[1]
b = dstattr.split(".")
dnode = b[0]
dattr = b[1]
sn = self.findNode(snode, create=True)
dn = self.findNode(dnode, create=True)
if sn!=None:
if dn==None:
print >>sys.stderr, 'WARNING: %s, %d: connectAttr "%s" "%s"'%(self.filename, self.linenr, srcattr, dstattr)
print >>sys.stderr, ' Node "%s" not found. The connection is ignored.'%dnode
else:
sn.addOutConnection(sattr, dn, dnode, dattr)
if dn!=None:
dn.addInConnection(dattr, snode, sattr)
# findNode
def findNode(self, path, create=False):
"""Return the Node object corresponding to a particular path.
path may also be None in which case None is returned.
If create is True, any missing nodes are automatically created.
(this method doesn't handle namespaces yet)
"""
if path==None:
return None
namespace,names = splitDAGPath(path)
# The current node (and eventually the result)
node = None
# Iterate over all names from 'top' to 'bottom'...
for name in names:
# An empty name? Then start from the beginning
if name=="":
node = None
else:
if node==None:
node = self.nodes.get(name)
if node==None:
if create:
node = self.createNode("<unknown>", {"name":[name]})
else:
raise KeyError, "Node %s not found (%s is missing)"%(path, name)
else:
for cn in node.iterChildren():
if name==cn.getName():
node = cn
break
else:
if create:
node = self.createNode("<unknown>", {"name":[name], "parent":[node.getFullName()]})
else:
raise KeyError, "Node %s not found (%s is missing)"%(path, name)
return node
# createNode
def createNode(self, nodetype, opts):
"""Create a new node and return it.
"""
parentname = opts.get("parent", [None])[0]
parent = self.findNode(parentname)
node = Node(nodetype, opts, parent=parent)
# The constant default name (if there was no name set in the file)
# will override a previous node without name. But this doesn't
# matter as the node cannot be addressed by name in the file anyway.
nodename = node.getName()
self.nodes[nodename] = node
self.nodelist.append(node)
return node
######################################################################
if __name__=="__main__":
class TestReader(DefaultMAReader):
def end(self):
for node in self.nodelist:
print '%-30s %-20s parent:%s'%('"'+node.getFullName()+'"', node.nodetype, node.getParentName())
for attrname in node._setattr:
try:
val = node.getAttrValue(attrname, attrname, None, None)
except:
val = "<error retrieving value, need more type information>"
val = str(val)
if len(val)>60:
val = val[:60]+"..."
print " %s = %s"%(attrname, val)
maname = sys.argv[1]
rd = TestReader()
rd.read(maname)
|