scripted/modules/JSONPatternCreator.py

scripted/modules/JSONPatternCreator.py
1 import sys
2 import json
3 import maya.cmds as cmds
4 import maya.api.OpenMaya as omAPI
5 from pyJsonAttrPatternInfo import PyJsonAttrPatternInfo as JsonKeys
6 from pyJsonAttrPatternInfo import jsonDebug as jsonDebug
7 
8 #======================================================================
9 def attributeTypeName(node, attr):
10  """
11  Find the name of an attribute's type. Some translation is required since
12  some construction types don't exactly match the class type as returned
13  by the attributeQuery command.
14 
15  Returns None if the type is either unknown or not one of the ones this
16  script currently supports.
17  """
18  attributeType = cmds.attributeQuery( attr, node=node, attributeType=True )
19 
20  if attributeType in JsonKeys.kNumericTypes:
21  return attributeType
22 
23  if attributeType in JsonKeys.kTypeMatrix:
24  return attributeType
25 
26  if attributeType == JsonKeys.kTypeEnum:
27  return attributeType
28 
29  if attributeType == JsonKeys.kTypeMessage:
30  return attributeType
31 
32  if attributeType == JsonKeys.kTypeString:
33  return attributeType
34 
35  return None
36 
37 #======================================================================
38 class JSONPatternCreator:
39  """
40  Utility class to build JSON pattern objects from a set of attributes.
41  The most common use is to create a set of patterns from a node and
42  print them out using:
43  import json
44  import JSONPatternCreator
45  patternCreator = JSONPatternCreator.JSONPatternCreator()
46  jsonNodePattern = patternCreator.nodeAsJSON( mayaNodeName )
47  json.dumps( jsonNodePattern, sort_keys=True, indent=4 )
48  """
49 
50  def __init__(self):
51  """
52  Initialize the pattern creator.
53  """
54  self.node = None
55  self.dgNode = None
56  self.nodeType = None
57 
58  #======================================================================
59  def numericAttributeAsJSON(self, attr):
60  """
61  Find the numeric-specific attribute parameters
62  attr = Attribute belonging to the pattern
63  RETURN = JSON object representing the numeric-attribute-specific parameters
64  It's best to merge these into the main object one by one, rather
65  than making it a sub-object.
66  """
67  pattern = {}
68 
69  # Set default value(s)
70  defaultValue = cmds.attributeQuery( attr, node=self.node, listDefault=True )
71  if defaultValue != None and len(defaultValue) > 0:
72  pattern[JsonKeys.kKeyDefault] = defaultValue[0]
73 
74  # Set min/max values
75  if cmds.attributeQuery( attr, node=self.node, minExists=True ):
76  value = cmds.attributeQuery( attr, node=self.node, min=True )
77  if value != None:
78  if len(value) == 1:
79  pattern[JsonKeys.kKeyMin] = value[0]
80  elif len(value) > 1:
81  pattern[JsonKeys.kKeyMin] = value
82 
83  if cmds.attributeQuery( attr, node=self.node, maxExists=True ):
84  value = cmds.attributeQuery( attr, node=self.node, max=True )
85  if value != None:
86  if len(value) == 1:
87  pattern[JsonKeys.kKeyMax] = value[0]
88  elif len(value) > 1:
89  pattern[JsonKeys.kKeyMax] = value
90 
91  if cmds.attributeQuery( attr, node=self.node, softMinExists=True ):
92  value = cmds.attributeQuery( attr, node=self.node, softMin=True )
93  if value != None:
94  pattern[JsonKeys.kKeySoftMin] = value[0]
95 
96  if cmds.attributeQuery( attr, node=self.node, softMaxExists=True ):
97  value = cmds.attributeQuery( attr, node=self.node, softMax=True )
98  if value != None:
99  pattern[JsonKeys.kKeySoftMax] = value[0]
100 
101  return pattern
102 
103  #======================================================================
104  def compoundAttributeAsJSON(self, attr):
105  """
106  Find the compound-specific attribute parameters
107  attr = Attribute belonging to the pattern
108  RETURN = JSON object representing the compound-attribute-specific parameters
109  It's best to merge these into the main object one by one, rather
110  than making it a sub-object.
111  """
112  pattern = {}
113  # TODO:
114  return pattern
115 
116  #======================================================================
117  def lightDataAttributeAsJSON(self, attr):
118  """
119  Find the lightData-specific attribute parameters
120  attr = Attribute belonging to the pattern
121  RETURN = JSON object representing the lightData-attribute-specific parameters
122  It's best to merge these into the main object one by one, rather
123  than making it a sub-object.
124  """
125  pattern = {}
126  # TODO:
127  return pattern
128 
129  #======================================================================
130  def stringAttributeAsJSON(self, attr):
131  """
132  Find the string-specific attribute parameters
133  attr = Attribute belonging to the pattern
134  RETURN = JSON object representing the string-attribute-specific parameters
135  It's best to merge these into the main object one by one, rather
136  than making it a sub-object.
137  """
138  pattern = {}
139  stringDefault = cmds.attributeQuery( attr, node=self.node, listDefault=True )
140  if stringDefault != None and len(stringDefault) > 0:
141  pattern[JsonKeys.kKeyDefault] = stringDefault[0]
142  return pattern
143 
144  #======================================================================
145  def matrixAttributeAsJSON(self, attr):
146  """
147  Find the matrix-specific attribute parameters
148  attr = Attribute belonging to the pattern
149  RETURN = JSON object representing the matrix-attribute-specific parameters
150  It's best to merge these into the main object one by one, rather
151  than making it a sub-object.
152  """
153  pattern = {}
154  matrixDefault = cmds.attributeQuery( attr, node=self.node, listDefault=True )
155  if matrixDefault != None and len(matrixDefault) > 0:
156  pattern[JsonKeys.kKeyDefault] = matrixDefault[0]
157  return pattern
158 
159  #======================================================================
160  def typedAttributeAsJSON(self, attr):
161  """
162  Find the typed-specific attribute parameters
163  attr = Attribute belonging to the pattern
164  RETURN = JSON object representing the typed-attribute-specific parameters
165  It's best to merge these into the main object one by one, rather
166  than making it a sub-object.
167  """
168  pattern = {}
169  # TODO:
170  return pattern
171 
172  #======================================================================
173  def enumAttributeAsJSON(self, attr):
174  """
175  Find the enum-specific attribute parameters
176  attr = Attribute belonging to the pattern
177  RETURN = JSON object representing the enum-attribute-specific parameters
178  It's best to merge these into the main object one by one, rather
179  than making it a sub-object.
180  """
181  pattern = {}
182  enumDefault = cmds.attributeQuery( attr, node=self.node, listDefault=True )
183  if enumDefault != None and len(enumDefault) > 0:
184  pattern[JsonKeys.kKeyDefault] = int(enumDefault[0])
185  enumList = cmds.attributeQuery( attr, node=self.node, listEnum=True)
186  if enumList != None and len(enumList) > 0:
187  pattern[JsonKeys.kKeyEnumNames] = enumList[0].split(':')
188  return pattern
189 
190  #======================================================================
191  def attributeAsJSON(self, attr):
192  """
193  Convert the attribute into its JSON attribute pattern equivalent.
194  attr = Attribute belonging to the pattern
195  RETURN = JSON object representing the attribute
196  """
197  jsonDebug( 'attributeAsJSON(%s)' % attr)
198  jsonAttr = {}
199  jsonAttr['name'] = attr
200 
201  shortName = cmds.attributeQuery( attr, node=self.node, shortName=True )
202  # No need to write it if they two names are the same
203  if shortName != attr: jsonAttr['shortName'] = shortName
204 
205  niceName = cmds.attributeQuery( attr, node=self.node, niceName=True )
206  jsonAttr['niceName'] = niceName
207 
208  attributeType = attributeTypeName(self.node, attr)
209  jsonAttr['attributeType'] = attributeType
210  jsonDebug( '... type %s' % attributeType )
211 
212  categories = cmds.attributeQuery( attr, node=self.node, categories=True )
213  if categories != None and len(categories) > 0:
214  jsonAttr['categories'] = categories
215 
216  # Some flags default to false, some default to true, some are
217  # code-dependent. Force setting of the ones who have changed
218  # from their defaults, or whose defaults are unknown.
219  #
220  # Keep the list alphabetical for convenience
221  flagList = []
222  if cmds.attributeQuery( attr, node=self.node, affectsAppearance=True ):
223  flagList.append( 'affectsAppearance' )
224  if cmds.attributeQuery( attr, node=self.node, affectsWorldspace=True ):
225  flagList.append( 'affectsWorldspace' )
226  if not cmds.attributeQuery( attr, node=self.node, cachedInternally=True ):
227  flagList.append( '!cached' )
228  if not cmds.attributeQuery( attr, node=self.node, writable=True ):
229  flagList.append( '!canConnectAsDst' )
230  isReadable = True
231  if not cmds.attributeQuery( attr, node=self.node, readable=True ):
232  isReadable = False
233  flagList.append( '!canConnectAsSrc' )
234  if cmds.attributeQuery( attr, node=self.node, multi=True ):
235  flagList.append( 'array' )
236  # You can't set the indexMatters flag unless the attribute is an
237  # unreadable array attribute. (It might be set otherwise, but just
238  # by accident and the API doesn't support setting it incorrectly.)
239  if cmds.attributeQuery( attr, node=self.node, indexMatters=True ) and not isReadable:
240  flagList.append( 'indexMatters' )
241  if cmds.attributeQuery( attr, node=self.node, channelBox=True ):
242  flagList.append( 'channelBox' )
243  if not cmds.attributeQuery( attr, node=self.node, connectable=True ):
244  flagList.append( '!connectable' )
245  if cmds.attributeQuery( attr, node=self.node, hidden=True ):
246  flagList.append( 'hidden' )
247  if cmds.attributeQuery( attr, node=self.node, indeterminant=True ):
248  flagList.append( 'indeterminant' )
249  if cmds.attributeQuery( attr, node=self.node, internalSet=True ):
250  flagList.append( 'internal' )
251  if cmds.attributeQuery( attr, node=self.node, keyable=True ):
252  flagList.append( 'keyable' )
253  else:
254  flagList.append( '!keyable' )
255  if cmds.attributeQuery( attr, node=self.node, renderSource=True ):
256  flagList.append( 'renderSource' )
257  if not cmds.attributeQuery( attr, node=self.node, storable=True ):
258  flagList.append( '!storable' )
259  if cmds.attributeQuery( attr, node=self.node, usedAsColor=True ):
260  flagList.append( 'usedAsColor' )
261  if cmds.attributeQuery( attr, node=self.node, usedAsFilename=True ):
262  flagList.append( 'usedAsFilename' )
263  if cmds.attributeQuery( attr, node=self.node, usesMultiBuilder=True ):
264  flagList.append( 'usesArrayDataBuilder' )
265  if cmds.attributeQuery( attr, node=self.node, worldspace=True ):
266  flagList.append( 'worldspace' )
267 
268  # Write out the flag collection
269  if len(flagList) > 0:
270  jsonAttr['flags'] = flagList
271 
272  # Get attribute-type specific JSON parameters
273  extraInfo = None
274  if attributeType == 'enum':
275  jsonDebug( '... decoding enum attribute parameters' )
276  extraInfo = self.enumAttributeAsJSON(attr )
277  elif attributeType in JsonKeys.kNumericTypes:
278  jsonDebug( '... decoding numeric attribute parameters' )
279  extraInfo = self.numericAttributeAsJSON(attr )
280  elif attributeType in JsonKeys.kTypeMatrix:
281  jsonDebug( '... decoding matrix attribute parameters' )
282  extraInfo = self.matrixAttributeAsJSON(attr )
283  elif attributeType == 'string':
284  jsonDebug( '... decoding string attribute parameters' )
285  extraInfo = self.stringAttributeAsJSON(attr )
286  elif attributeType == 'message':
287  jsonDebug( '... decoding message attribute parameters' )
288  elif attributeType == 'compound':
289  jsonDebug( '... decoding compound attribute parameters' )
290  extraInfo = self.compoundAttributeAsJSON(attr )
291  elif attributeType == 'lightData':
292  jsonDebug( '... decoding lightData attribute parameters' )
293  extraInfo = self.lightDataAttributeAsJSON(attr )
294  elif attributeType == 'typed':
295  jsonDebug( '... decoding typed attribute parameters' )
296  extraInfo = self.typedAttributeAsJSON(attr )
297 
298  if extraInfo != None:
299  for extraKey in extraInfo:
300  jsonAttr[extraKey] = extraInfo[extraKey]
301 
302  return jsonAttr
303 
304  #======================================================================
305  def attributeListAsJSON(self, patternName, attrs):
306  """
307  Convert the list of attributes into a JSON attribute pattern object.
308  Any attributes not supported will be written out as an (ignored) JSON property
309  "unsupportedAttributes".
310  patternName = Name of the new pattern
311  attrs = Attributes belonging to the pattern
312  RETURN = JSON object containing the pattern for the attribute list
313  """
314  # Firewall to ignore bad input
315  if patternName == None or len(patternName) == 0:
316  raise ValueError( 'Pattern name cannot be empty' )
317  if attrs == None or len(attrs) == 0:
318  return None
319 
320  unsupportedAttrs = []
321  supportedAttrs = []
322  pattern = {}
323  pattern["name"] = patternName
324 
325  for attr in attrs:
326  attrType = attributeTypeName( self.node, attr )
327  if attrType != None:
328  supportedAttrs.append( attr )
329  else:
330  unsupportedAttrs.append( '%s:%s' % (attr, cmds.attributeQuery( attr, node=self.node, attributeType=True )) )
331  unsupportedAttrs.sort()
332  supportedAttrs.sort()
333 
334  if len(unsupportedAttrs) > 0:
335  pattern["unsupportedAttributes"] = unsupportedAttrs
336 
337  attrPatternList = []
338  for attr in supportedAttrs:
339  attrPatternList.append( self.attributeAsJSON( attr ) )
340  pattern["attributes"] = attrPatternList
341 
342  return pattern
343 
344  #======================================================================
345  def nodeAsJSON(self, node):
346  """
347  node = Node based on which the pattern is to be created
348  RETURN = JSON object containing patterns for all node attributes
349 
350  Method to walk through the list of attributes in a node and return the
351  supported types out in a JSON attribute pattern format. The returned
352  object can then be written out to a file or processed directly as a
353  JSON attribute pattern.
354 
355  There will be up to three attribute patterns specified in the object,
356  one for each of static, dynamic, and extension attributes. Each of the
357  patterns is only created if there is at least one attribute of that type.
358 
359  The names of the patterns for a node named "NODE" will be:
360  dynamic_NODE
361  static_NODE
362  extension_NODE
363  You really don't need me to explain which is which do you?
364  """
365  self.node = node # Need this local for getting attribute info
366  self.dgNode = omAPI.MSelectionList().add(node).getDependNode(0)
367  jsonDebug( 'Getting node information from %s' % str(self.dgNode) )
368  patternList = []
369 
370  try:
371  dynamicAttributes = cmds.listAttr( node, userDefined=True )
372  extensionAttributes = cmds.listAttr( node, extension=True )
373  allAttributes = cmds.listAttr( node )
374  staticAttributes = [attr for attr in allAttributes if not attr in extensionAttributes and not attr in dynamicAttributes]
375  #----------------------------------------------------------------------
376  # Add the static attribute pattern to the string if there are any
377  newPattern = self.attributeListAsJSON( "static_%s" % node, staticAttributes )
378  if newPattern != None:
379  patternList.append( newPattern )
380 
381  #----------------------------------------------------------------------
382  # Add the dynamic attribute pattern to the string if there are any
383  newPattern = self.attributeListAsJSON( "dynamic_%s" % node, dynamicAttributes )
384  if newPattern != None:
385  patternList.append( newPattern )
386 
387  #----------------------------------------------------------------------
388  # Add the extension attribute pattern to the string if there are any
389  newPattern = self.attributeListAsJSON( "extension_%s" % node, extensionAttributes )
390  if newPattern != None:
391  patternList.append( newPattern )
392 
393  except Exception, e:
394  print 'ERR: Failed pattern creation on node %s (%s)' % (node, str(e))
395 
396  # The pattern should be all built up now
397  return patternList
398 
399  #======================================================================
400  def test(self):
401  """
402  Run an internal consistency test on the pattern creator to verify its
403  functions are operating correctly.
404  """
405  factoryIsLoaded = cmds.pluginInfo('pyJsonAttrPatternFactory.py', query=True, loaded=True)
406  if not factoryIsLoaded:
407  try:
408  cmds.loadPlugin('pyJsonAttrPatternFactory.py', quiet=True)
409  except:
410  # Repotest environment won't find it so skip it then
411  return False
412  factoryIsLoaded = cmds.pluginInfo('pyJsonAttrPatternFactory.py', query=True, loaded=True)
413  # If the environment isn't set up to find the plugin there's
414  # nothing we can do about it. It's not a failure of the test so
415  # don't report anything other than a warning that the test could
416  # not be run.
417  if not factoryIsLoaded:
418  print 'Warning: JSON attribute pattern factory could not be loaded, test aborted'
419  return False
420 
421  patterns = """
422  [
423  {
424  "name": "testPattern",
425  "attributes": [
426  {
427  "name" : "floatWithRanges",
428  "shortName" : "fwr",
429  "defaultValue" : 0.5,
430  "min" : -10.0,
431  "max" : 20.0,
432  "softMin" : 1.0,
433  "softMax" : 10.0,
434  "attributeType" : "float"
435  } ,
436  {
437  "name" : "float3WithRanges",
438  "shortName" : "ftwr",
439  "defaultValue" : [7.5, 7.6, 7.7],
440  "min" : [-17.0, -17.1, -17.2],
441  "max" : [27.0, 27.1, 27.2],
442  "attributeType" : "float3"
443  }
444  ]
445  }
446  ]
447  """
448  cmds.createAttrPatterns( patternType='json', patternDefinition=patterns )
449  cmds.file( force=True, new=True )
450  node = cmds.createNode( 'addMatrix' )
451  cmds.applyAttrPattern( node, patternName='testPattern' )
452 
453  jsonString = self.nodeAsJSON( node )
454  print json.dumps(jsonString, indent=4)
455