# VRML node prototype class (SGbuilder) # Wed Oct 31 16:18:35 CET 2001 '''Prototype2 -- VRML 97 sceneGraph/Node/Script/ROUTE/IS implementations''' import copy, types # extern import strop as string # builtin from utils import typeclasses, err, namespace # XXX ## TODO: namespace must go class baseProto: def __vrmlStr__( self, **namedargs ): '''Generate a VRML 97-syntax string representing this Prototype **namedargs -- key:value passed arguments for the linearisation object see lineariser4.Lineariser ''' import lineariser4 lineariser = apply( lineariser4.Lineariser, (), namedargs ) return apply( lineariser.linear, ( self, ), namedargs ) toString = __vrmlStr__ # added stuff for linking support for target scenegraph def setTargetnode(self, node): self.__dict__['_targetnode'] = node def getTargetnode(self): try: return self.__dict__['_targetnode'] except: return None class Prototype(baseProto): ''' A VRML 97 Prototype object A Prototype is a callable object which produces Node instances the Node uses a pointer to its Prototype to provide much of the Node's standard functionality. Prototype's are often stored in a sceneGraph's protoTypes namespace, where you can access them as sceneGraph.protoTypes.nodeGI . They are also commonly found in Nodes' PROTO attributes. Attributes: __gi__ -- constant string "PROTO" nodeGI -- string gi The "generic identifier" of the node type, i.e. the name of the node fieldDictionary -- string name: (string name, string dataType, boolean exposed) defaultDictionary -- string name: object defaultValue Will be blank for EXTERNPROTO's and Script prototypes eventDictionary -- string name: (string name, string dataType, boolean eventOut) sceneGraph -- object sceneGraph MFNodeNames -- list of field name strings Allows for easy calculation of "children" nodes SFNodeNames -- list of field name strings Allows for easy calculation of "children" nodes ''' __gi__ = "PROTO" def __init__(self, gi, fieldDict=None, defaultDict=None, eventDict=None, sGraph=None): ''' gi -- string gi see attribute nodeGI fieldDict -- string name: (string name, string dataType, boolean exposed) see attribute fieldDictionary defaultDict -- string name: object defaultValue see attribute defaultDictionary eventDict -- string name: (string name, string dataType, boolean eventOut) see attribute eventDictionary sceneGraph -- object sceneGraph see attribute sceneGraph ''' self.nodeGI = checkName( gi ) self.fieldDictionary = {} self.defaultDictionary = {} self.eventDictionary = {} self.SFNodeNames = [] self.MFNodeNames = [] self.sceneGraph = sGraph # setup the fields/events for definition in (fieldDict or {}).values(): self.addField( definition, (defaultDict or {}).get( definition[0])) for definition in (eventDict or {}).values(): self.addEvent( definition ) def getSceneGraph( self ): ''' Retrieve the sceneGraph object (may be None object) see attribute sceneGraph''' return self.sceneGraph def setSceneGraph( self, sceneGraph ): ''' Set the sceneGraph object (may be None object) see attribute sceneGraph''' self.sceneGraph = sceneGraph def getChildren(self, includeSceneGraph=None, includeDefaults=1, *args, **namedargs): ''' Calculate the current children of the PROTO and return as a list of nodes if includeDefaults: include those default values which are node values if includeSceneGraph: include the sceneGraph object if it is not None see attribute MFNodeNames see attribute SFNodeNames see attribute sceneGraph ''' temp = [] if includeDefaults: for attrname in self.SFNodeNames: try: temp.append( self.defaultDictionary[attrname] ) except KeyError: # sceneGraph object is not copied... pass for attrname in self.MFNodeNames: try: temp[len(temp):] = self.defaultDictionary[attrname] except KeyError: pass if includeSceneGraph and self.sceneGraph: temp.append( self.getSceneGraph() ) return temp def addField (self, definition, default = None): ''' Add a single field definition to the Prototype definition -- (string name, string dataType, boolean exposed) default -- object defaultValue see attribute fieldDictionary see attribute defaultDictionary ''' if type (definition) == types.InstanceType: definition = definition.getDefinition() default = definition.getDefault () self.removeField( definition[0] ) self.fieldDictionary[definition [0]] = definition if default is not None: default = fieldcoercian.FieldCoercian()( default, definition[1] ) self.defaultDictionary [definition [0]] = default if definition[1] == 'SFNode': self.SFNodeNames.append(definition[0]) elif definition[1] == 'MFNode': self.MFNodeNames.append(definition[0]) def removeField (self, key): ''' Remove a single field from the Prototype key -- string fieldName The name of the field to remove ''' if self.fieldDictionary.has_key (key): del self.fieldDictionary [key] if self.defaultDictionary.has_key (key): del self.defaultDictionary [key] for attribute in (self.SFNodeNames, self.MFNodeNames): while key in attribute: attribute.remove(key) def addEvent(self, definition): ''' Add a single event definition to the Prototype definition -- (string name, string dataType, boolean eventOut) see attribute eventDictionary ''' if type (definition) == types.InstanceType: definition = definition.getDefinition() self.eventDictionary[definition [0]] = definition def removeEvent(self, key): ''' Remove a single event from the Prototype key -- string eventName The name of the event to remove ''' if self.eventDictionary.has_key (key): del self.eventDictionary [key] def getField( self, key ): '''Return a Field or Event object representing a given name key -- string name The name of the field or event to retrieve will attempt to match key, key[4:], and key [:-8] corresponding to key, set_key and key_changed see class Field see class Event ''' # print self.fieldDictionary, self.eventDictionary for tempkey in (key, key[4:], key[:-8]): if self.fieldDictionary.has_key( tempkey ): return Field( self.fieldDictionary[tempkey], self.defaultDictionary.get(tempkey) ) elif self.eventDictionary.has_key( tempkey ): return Event( self.eventDictionary[tempkey] ) raise AttributeError, key def getDefault( self, key ): '''Return the default value for the given field key -- string name The name of the field Will attempt to match key, key[4:], and key [:-8] corresponding to key, set_key and key_changed see attribute defaultDictionary ''' for key in (key, key[4:], key[:-8]): if self.defaultDictionary.has_key( key ): val = self.defaultDictionary[key] if type(val) in typeclasses.MutableTypes: val = copy.deepcopy( val ) return val elif self.fieldDictionary.has_key( key ): '''We have the field, but we don't have a default, we are likely an EXTERNPROTO''' return None raise AttributeError, key def setDefault (self, key, value): '''Set the default value for the given field key -- string name The name of the field to set value -- object defaultValue The default value, will be checked for type and coerced if necessary ''' field = self.getField (key) self.defaultDictionary [field.name]= field.coerce (value) def clone( self, children = 1, sceneGraph = 1 ): '''Return a copy of this Prototype children -- boolean if true, copy the children of the Prototype, otherwise include them sceneGraph -- boolean if true, copy the sceneGraph of the Prototype ''' if sceneGraph: sceneGraph = self.sceneGraph else: sceneGraph = None # defaults should always be copied before modification, but this is still dangerous... defaultDictionary = self.defaultDictionary.copy() if not children: for attrname in self.SFNodeNames+self.MFNodeNames: try: del defaultDictionary[attrname] except KeyError: # sceneGraph object is not copied... pass # now make a copy if self.__gi__ == "PROTO": newNode = self.__class__( self.nodeGI, self.fieldDictionary, defaultDictionary, self.eventDictionary, sceneGraph, ) else: newNode = self.__class__( self.nodeGI, self.url, self.fieldDictionary, self.eventDictionary, ) return newNode def __call__(self, *args, **namedargs): '''Create a new Node instance associated with this Prototype *args, **namedargs -- passed to the Node.__init__ see class Node ''' node = apply( Node, (self, )+args, namedargs ) return node def __repr__ ( self ): '''Create a simple Python representation''' return '''%s( %s )'''%( self.__class__.__name__, self.nodeGI ) class ExternalPrototype( Prototype ): '''Sub-class of Prototype The ExternalPrototype is a minor sub-classing of the Prototype it does not have any defaults, nor a sceneGraph Attributes: __gi__ -- constant string "EXTERNPROTO" url -- string list urls implementation source for the ExternalPrototype ''' __gi__ = "EXTERNPROTO" def __init__(self, gi, url=None, fieldDict=None, eventDict=None): ''' gi -- string gi see attribute nodeGI url -- string list url MFString-compatible list of url's for EXTERNPROTO fieldDict -- string name: (string name, string dataType, boolean exposed) see attribute fieldDictionary eventDict -- string name: (string name, string dataType, boolean eventOut) see attribute eventDictionary ''' if url is None: url = [] self.url = url Prototype.__init__( self, gi, fieldDict=fieldDict, eventDict=eventDict) from vrml import fieldcoercian # XXX class Field: ''' Representation of a Prototype Field The Field object is a simple wrapper to provide convenient access to field coercian and meta- information ''' def __init__( self, specification, default=None ): self.name, self.type, self.exposure = specification self.default = default def getDefinition (self): return self.name, self.type, self.exposure def getDefault (self): return self.default def coerce( self, value ): ''' Coerce value to the appropriate dataType for this Field ''' return fieldcoercian.FieldCoercian()( value,self.type, ) def __repr__( self ): if hasattr (self, "default"): return '%s( (%s,%s,%s), %s)'%( self.__class__.__name__, self.name, self.type, self.exposure, self.default) else: return '%s( (%s,%s,%s),)'%( self.__class__.__name__, self.name, self.type, self.exposure) def __str__( self ): if self.exposure: exposed = "exposedField" else: exposed = field if hasattr (self, "default"): default = ' ' + str( self.default) else: default = "" return '%s %s %s%s'%(exposed, self.type, self.name, default) class Event (Field): def __str__( self ): if self.exposure: exposed = "eventOut" else: exposed = "eventIn" return '%s %s %s'%(exposed, self.type, self.name) ### Translation strings for VRML node names... translationstring = '''][0123456789{}"'#,.\\ \000\001\002\003\004\005\006\007\010\011\012\013\014\015\016\017\020\021\022\023''' NAMEFIRSTCHARTRANSLATOR = string.maketrans( translationstring, '_'*len(translationstring) ) translationstring = '''][{}"'#,.\\ \000\001\002\003\004\005\006\007\010\011\012\013\014\015\016\017\020\021\022\023''' NAMERESTCHARTRANSLATOR = string.maketrans( translationstring, '_'*len(translationstring) ) del translationstring def checkName( name ): '''Convert arbitrary string to a valid VRML id''' if type(name) is types.StringType: if not name: return name return string.translate( name[:1], NAMEFIRSTCHARTRANSLATOR) + string.translate( name[1:], NAMERESTCHARTRANSLATOR) else: raise TypeError, "VRML Node Name must be a string, was a %s: %s"%(type(name), name) class Node(baseProto): ''' A VRML 97 Node object A Node object represents a VRML 97 node. Attributes of the Node can be set/retrieved with standard python setattr/getattr syntax. VRML 97 attributes may be passed to the constructor as named arguments. Attributes: __gi__ -- string PROTOname DEF -- string DEFName The DEF name of the node, will be coerced to be a valid identifier (with "" being considered valid) PROTO -- Prototype PROTO The node's Prototype object attributeDictionary -- string name: object value Dictionary in which VRML 97 attributes are stored ''' DEF = '' # the default name for all nodes (arbitrary) def __init__(self, PROTO, name='', attrDict=None, *args, **namedargs): '''Normally this method is only called indirectly via the Prototype() interface PROTO -- Prototype PROTO see attribute PROTO name -- string DEFName see attribute DEF attrDict -- string name: object value see attribute attributeDictionary **namedargs -- string name: object value added to attrDict to create attributeDictionary ''' self.__dict__["PROTO"] = PROTO self.DEF = name self.__dict__["attributeDictionary"] = {} ## print attrDict, namedargs for dict in (attrDict or {}), namedargs: if dict: for key, value in dict.items (): self.__setattr__( key, value, check=1 ) def __setattr__( self, key, value, check=1, raw=0 ): '''Set attribute on Node key -- string attributeName value -- object attributeValue check -- boolean check if false, put values for unrecognized keys into __dict__ otherwise, raise an AttributeError ''' if key == "DEF": self.__dict__["DEF"] = checkName( value ) return None elif key == "PROTO": self.__dict__["PROTO"] = value try: field = self.PROTO.getField( key ) if (hasattr( value, "__gi__") and value.__gi__ == "IS") or raw: self.attributeDictionary[ field.name] = value else: self.attributeDictionary[ field.name] = field.coerce( value ) except ValueError, x: raise ValueError( "Could not coerce value %s into value of VRML type %s for %s node %s's field %s"%( value, field.type, self.__gi__, self.DEF, key), x.args) except (AttributeError), x: if check: raise AttributeError("%s is not a known field for node %s"%(key, repr(self))) else: self.__dict__[key] = value def __getattr__( self, key, default = 1 ): ''' Retrieve an attribute when standard lookup fails key -- string attributeName default -- boolean default if true, return the default value if the node does not have local value otherwise, raise AttributeError ''' if key != "attributeDictionary": if self.__dict__.has_key( key): return self.__dict__[ key ] elif self.attributeDictionary.has_key( key): return self.attributeDictionary[key] if key != "PROTO": if key == "__gi__": return self.PROTO.nodeGI elif default: try: default = self.PROTO.getDefault( key ) if type( default ) in typeclasses.MutableTypes: # we need a copy, not the original default = copy.deepcopy( default ) self.__setattr__( key, default, check=0, raw=1 ) return default except AttributeError: pass raise AttributeError, key def __delattr__( self, key ): ''' Delete an attribute from the Node key -- string attributeName ''' if key != "attributeDictionary": if self.attributeDictionary.has_key( key): del self.attributeDictionary[key] elif self.__dict__.has_key( key): del self.__dict__[ key ] raise AttributeError, key def __repr__(self): ''' Create simple python representation ''' return '<%s(%s): %s>'%(self.__gi__, `self.DEF`, self.attributeDictionary.keys() ) def getChildrenNames( self, current = 1, *args, **namedargs ): ''' Get the (current) children of Node returns two lists: MFNode children, SFNode children current -- boolean currentOnly if true, only return current children otherwise, include all potential children ''' MFNODES, SFNODES = self.PROTO.MFNodeNames, self.PROTO.SFNodeNames mns, sns = [],[] for key in MFNODES: if current and self.attributeDictionary.has_key(key): mns.append(key) elif not current: mns.append(key) for key in SFNODES: if self.attributeDictionary.has_key(key): sns.append(key) elif not current: sns.append(key) return mns,sns def calculateChildren(self, *args, **namedargs): '''Calculate the current children of the Node as list of Nodes ''' MFNODES, SFNODES = self.getChildrenNames( ) temp = [] for key in MFNODES: try: temp.extend( self.__getattr__( key, default=0 ) ) except AttributeError: pass for key in SFNODES: try: temp.append( self.__getattr__(key, default = 0 ) ) except AttributeError: pass return temp def clone(self, newclass=None, name=None, children=None, attrDeepCopy=1, *args, **namedargs): '''Return a copy of this Node newclass -- object newClass or None optionally use a different Prototype as base name -- string DEFName or None or 1 if 1, copy from current elif None, set to "" else, set to passed value children -- boolean copyChildren if true, copy the children of this node otherwise, skip children attrDeepCopy -- boolean deepCopy if true, use deepcopy otherwise, use copy ''' if attrDeepCopy: cpy = copy.deepcopy else: cpy = copy.copy newattrs = self.attributeDictionary.copy() if not children: mnames,snames = self.getChildrenNames( ) for key in mnames+snames: try: del(newattrs[key]) except KeyError: pass for key, val in newattrs.items(): if type(val) in typeclasses.MutableTypes: newattrs[key] = cpy(val) # following is Node specific, won't work for sceneGraphs, scripts, etceteras if name == 1: # asked to copy the name name = self.DEF elif name is None: # asked to clear the name name = '' if not newclass: newclass = self.PROTO return newclass( name, newattrs ) def __cmp__( self, other, stop=None ): ''' Compare this node to another object/node other -- object otherNode stop -- boolean stopIfFailure if true, failure to find comparison causes match failure (i.e. considered unequal) ''' if hasattr( other, '__gi__') and other.__gi__ == self.__gi__: try: return cmp( self.DEF, other.DEF) or cmp( self.attributeDictionary, other.attributeDictionary ) except: if not stop: try: return other.__cmp__( self , 1) # 1 being stop... except: pass return -1 # could be one, doesn't really matter def Script( name="", attrDict=None, fieldDict=None, defaultDict=None, eventDict=None, **namedarguments): ''' Create a script node (and associated prototype) name -- string DEFName attrDict -- string name: object value see class Node.attributeDictionary fieldDict -- string name: (string name, string dataType, boolean exposure) see class Prototype.fieldDictionary defaultDict -- string name: object value see class Prototype.defaultDictionary eventDict -- string name: (string name, string dataType, boolean eventOut) ''' fieldDictionary = { 'directOutput':('directOutput', 'SFBool',0), 'url':('url',"MFString",0), 'mustEvaluate':('mustEvaluate', 'SFBool',0), } fieldDictionary.update( fieldDict or {}) defaultDictionary = { "directOutput":0, "url":[], "mustEvaluate":0, } defaultDictionary.update( defaultDict or {}) PROTO = Prototype( "Script", fieldDictionary, defaultDictionary , eventDict = eventDict, ) if attrDict is not None: attrDict.update( namedarguments ) else: attrDict = namedarguments return PROTO( name, attrDict ) class NullNode: '''NULL SFNode value There should only be a single NULL instance for any particular system. It should, for all intents and purposes just sit there inertly ''' __gi__ = 'NULL' DEF = '' __walker_is_temporary_item__ = 1 # hacky signal to walking engine not to reject this node as already processed def __repr__(self): return '' def __vrmlStr__(self,*args,**namedargs): return ' NULL ' toString = __vrmlStr__ def __nonzero__(self ): return 0 def __call__(self, *args, **namedargs): return self def __cmp__( self, other ): if hasattr( other, '__gi__') and other.__gi__ == self.__gi__: return 0 return -1 # could be one, doesn't really matter def clone( self ): return self NULL = NullNode() class fieldRef: '''IS Prototype field reference ''' __gi__ = 'IS' DEF = '' def __init__(self, declaredName): self.declaredName = declaredName def __repr__(self): return 'IS %s'%self.declaredName def __vrmlStr__(self,*args,**namedargs): return 'IS %s'%self.declaredName toString = __vrmlStr__ def __cmp__( self, other ): if hasattr( other, '__gi__') and other.__gi__ == self.__gi__: return cmp( self.declaredName, other.declaredName ) return -1 # could be one, doesn't really matter def clone( self ): return self.__class__( self.declaredName ) IS = fieldRef class ROUTE: ''' VRML 97 ROUTE object The ROUTE object keeps track of its source and destination nodes and attributes It generally lives in a sceneGraph's "routes" collection ''' __gi__ = 'ROUTE' def __init__( self, fromNode, fromField, toNode, toField ): if type(fromNode) is types.StringType: raise TypeError( "String value for ROUTE fromNode",fromNode) if type(toNode) is types.StringType: raise TypeError( "String value for ROUTE toNode",toNode) self.fromNode = fromNode self.fromField = fromField self.toNode = toNode self.toField = toField def __getitem__( self, index ): return (self.fromNode, self.fromField, self.toNode, self.toField)[index] def __setitem__( self, index, value ): attribute = ("fromNode","fromField","toNode", "toField")[index] setattr( self, attribute, value ) def __repr__( self ): return 'ROUTE %s.%s TO %s.%s'%( self.fromNode.DEF, self.fromField, self.toNode.DEF, self.toField ) def clone( self ): return self.__class__( self.fromNode, self.fromField, self.toNode, self.toField, ) class sceneGraph(baseProto): ''' A VRML 97 sceneGraph Attributes: __gi__ -- constant string "sceneGraph" DEF -- constant string "" children -- Node list List of the root children of the sceneGraph, nodes/scripts only routes -- ROUTE list List of the routes within the sceneGraph defNames -- string DEFName: Node node Mapping of DEF names to their respective nodes protoTypes -- Namespace prototypes Namespace (with chaining lookup) collection of prototypes getattr( sceneGraph.protoTypes, 'nodeGI' ) retrieves a prototype ''' __gi__ = 'sceneGraph' DEF = '' def __init__(self, root=None, protoTypes=None, routes=None, defNames=None, children=None, *args, **namedargs): ''' root -- sceneGraph root or Dictionary root or Module root or None Base object for root of protoType namespace hierarchy protoTypes -- string nodeGI: Prototype PROTO Dictionary of prototype definitions routes -- ROUTE list or (string sourcenode, string sourceeventOut, string destinationnode, string destinationeventOut) list List of route objects or tuples to be added to the sceneGraph see attribute routes defNames -- string DEFName: Node node see attribute defNames children -- Node list see attribute children ''' if children is None: self.children = [] else: self.children = children if routes is None: self.routes = [] # how will we efficiently handle routes? else: self.routes = routes if defNames == None: self.defNames = {} # maps 'defName':Node else: self.defNames = defNames if protoTypes is None: protoTypes = {} if root is None: from vrml import basenodes # XXX self.protoTypes = namespace.NameSpace( protoTypes, children = [namespace.NameSpace(basenodes)] ) else: # there is a root file, so need to use it as the children instead of basenodes... if hasattr( root, "protoTypes"): self.protoTypes = namespace.NameSpace( protoTypes, children = [root.protoTypes] ) else: self.protoTypes = namespace.NameSpace( protoTypes, children = [ namespace.NameSpace(root) ] ) def __getinitargs__( self ): # we only copy our explicit protos, our routes, our defNames, and our children # inherited protos will be pulled along by their nodes... return None, self.protoTypes._base, self.routes, self.defNames, self.children def __getstate__( self ): return {} def __setstate__( self, dict ): pass def __del__( self, id=id ): ''' Need to clean up the namespace's mutual references, this can be done without affecting the cascade by just eliminating the key/value pairs. The namespaces will no longer contain the prototypes, but they will still chain up to the higher-level namespaces, and the nodes will have those prototypes still in use. ''' ## print 'del sceneGraph', id(self ) try: ## import pdb ## pdb.set_trace() ## self.protoTypes.__dict__.clear() self.protoTypes._base.clear() del self.protoTypes.__namespace_cascade__[:] except: print 'unable to free references' def addRoute(self, routeTuple, getNewNodes=0): ''' Add a single route to the sceneGraph routeTuple -- ROUTE route or (string sourcenode, string sourceeventOut, string destinationnode, string destinationeventOut) getNewNodes -- boolean getNewNodes if true, look up sourcenode and destinationnode within the current defNames to determine source/destination nodes otherwise, just use current if available ''' # create and wire together the Routes here, # should just be a matter of pulling the events and passing the nodes... ## import pdb ## pdb.set_trace() if type( routeTuple) in ( types.TupleType, types.ListType): (fromNode, fromField, toNode, toField ) = routeTuple if type(fromNode) is types.StringType: # get the node instead of the string... if self.defNames.has_key( fromNode ): fromNode = self.defNames[fromNode] else: err.err( "ROUTE from an unknown node %s "%(routeTuple) ) return 0 if type(toNode) is types.StringType: # get the node instead of the string... if self.defNames.has_key( toNode ): toNode = self.defNames[toNode] else: err.err( "ROUTE to an unknown node %s "%(routeTuple) ) return 0 routeTuple = ROUTE( fromNode, fromField, toNode, toField) elif getNewNodes: # get the nodes with the same names... if self.defNames.has_key( routeTuple[0].DEF ): routeTuple[0] = self.defNames[routeTuple[0].DEF] else: err.err( "ROUTE from an unknown node %s "%(routeTuple) ) return 0 if self.defNames.has_key( routeTuple[2].DEF ): routeTuple[2] = self.defNames[routeTuple[2].DEF] else: err.err( "ROUTE to an unknown node %s "%(routeTuple) ) return 0 # should be a Route node now, append to our ROUTE list... self.routes.append(routeTuple) return 1 def regDefName(self, defName, object): ''' Register a DEF name for a particular object defName -- string DEFName object -- Node node ''' object.DEF = defName self.defNames[defName] = object def addProto(self, proto): '''Register a Prototype for this sceneGraph proto -- Prototype PROTO ''' setattr( self.protoTypes, proto.__gi__, proto ) #toString = __vrmlStr__ #__vrmlStr__ = toString ## def __setattr__( self, key, value ): ## if key == 'protoTypes' and type( value) is types.ListType: ## import pdb ## pdb.set_trace() ## raise TypeError( "Invalid type for protoTypes attribute of sceneGraph %s"%(`value`) ) ## else: ## self.__dict__[key] = value DEFAULTFIELDVALUES ={ "SFBool": 0, "SFString": "", "SFFloat": 0, "SFTime": 0, "SFVec3f": (0, 0,0), "SFVec2f": (0,0), "SFRotation": (0, 1,0, 0), "SFInt32": 0, "SFImage": (0,0,0), "SFColor": (0,0, 0), "SFNode": NULL, "MFString": [], "MFFloat": [], "MFTime": [], "MFVec3f": [], "MFVec2f": [], "MFRotation": [], "MFInt32": [], "MFColor": [], "MFNode": [], }