Browse Source

Protect PropertyBinding against the closure circular reference bug, and "externalize" Composite from prototype

Tristan VALCKE 8 years ago
parent
commit
01f62dc632
1 changed files with 271 additions and 272 deletions
  1. 271 272
      src/animation/PropertyBinding.js

+ 271 - 272
src/animation/PropertyBinding.js

@@ -8,268 +8,226 @@
  * @author tschw
  */
 
-function PropertyBinding( rootNode, path, parsedPath ) {
-
-	this.path = path;
-	this.parsedPath = parsedPath ||
-			PropertyBinding.parseTrackName( path );
+function Composite ( targetGroup, path, optionalParsedPath ) {
 
-	this.node = PropertyBinding.findNode(
-			rootNode, this.parsedPath.nodeName ) || rootNode;
+	var parsedPath = optionalParsedPath || PropertyBinding.parseTrackName( path );
 
-	this.rootNode = rootNode;
+	this._targetGroup = targetGroup;
+	this._bindings = targetGroup.subscribe_( path, parsedPath );
 
 }
 
-PropertyBinding.prototype = {
+Object.assign( Composite.prototype, {
 
-	constructor: PropertyBinding,
+	constructor: Composite,
 
-	getValue: function getValue_unbound( targetArray, offset ) {
-
-		this.bind();
-		this.getValue( targetArray, offset );
-
-		// Note: This class uses a State pattern on a per-method basis:
-		// 'bind' sets 'this.getValue' / 'setValue' and shadows the
-		// prototype version of these methods with one that represents
-		// the bound state. When the property is not found, the methods
-		// become no-ops.
+	getValue: function( array, offset ) {
 
-	},
+		this.bind(); // bind all binding
 
-	setValue: function getValue_unbound( sourceArray, offset ) {
+		var firstValidIndex = this._targetGroup.nCachedObjects_,
+			binding = this._bindings[ firstValidIndex ];
 
-		this.bind();
-		this.setValue( sourceArray, offset );
+		// and only call .getValue on the first
+		if ( binding !== undefined ) binding.getValue( array, offset );
 
 	},
 
-	// create getter / setter pair for a property in the scene graph
-	bind: function() {
-
-		var targetObject = this.node,
-			parsedPath = this.parsedPath,
-
-			objectName = parsedPath.objectName,
-			propertyName = parsedPath.propertyName,
-			propertyIndex = parsedPath.propertyIndex;
-
-		if ( ! targetObject ) {
-
-			targetObject = PropertyBinding.findNode(
-					this.rootNode, parsedPath.nodeName ) || this.rootNode;
-
-			this.node = targetObject;
-
-		}
+	setValue: function( array, offset ) {
 
-		// set fail state so we can just 'return' on error
-		this.getValue = this._getValue_unavailable;
-		this.setValue = this._setValue_unavailable;
+		var bindings = this._bindings;
 
- 		// ensure there is a value node
-		if ( ! targetObject ) {
+		for ( var i = this._targetGroup.nCachedObjects_,
+				  n = bindings.length; i !== n; ++ i ) {
 
-			console.error( "  trying to update node for track: " + this.path + " but it wasn't found." );
-			return;
+			bindings[ i ].setValue( array, offset );
 
 		}
 
-		if ( objectName ) {
-
-			var objectIndex = parsedPath.objectIndex;
-
-			// special cases were we need to reach deeper into the hierarchy to get the face materials....
-			switch ( objectName ) {
-
-				case 'materials':
+	},
 
-					if ( ! targetObject.material ) {
+	bind: function() {
 
-						console.error( '  can not bind to material as node does not have a material', this );
-						return;
+		var bindings = this._bindings;
 
-					}
+		for ( var i = this._targetGroup.nCachedObjects_,
+				  n = bindings.length; i !== n; ++ i ) {
 
-					if ( ! targetObject.material.materials ) {
+			bindings[ i ].bind();
 
-						console.error( '  can not bind to material.materials as node.material does not have a materials array', this );
-						return;
+		}
 
-					}
+	},
 
-					targetObject = targetObject.material.materials;
+	unbind: function() {
 
-					break;
+		var bindings = this._bindings;
 
-				case 'bones':
+		for ( var i = this._targetGroup.nCachedObjects_,
+				  n = bindings.length; i !== n; ++ i ) {
 
-					if ( ! targetObject.skeleton ) {
+			bindings[ i ].unbind();
 
-						console.error( '  can not bind to bones as node does not have a skeleton', this );
-						return;
+		}
 
-					}
+	}
 
-					// potential future optimization: skip this if propertyIndex is already an integer
-					// and convert the integer string to a true integer.
+} );
 
-					targetObject = targetObject.skeleton.bones;
 
-					// support resolving morphTarget names into indices.
-					for ( var i = 0; i < targetObject.length; i ++ ) {
+function PropertyBinding( rootNode, path, parsedPath ) {
 
-						if ( targetObject[ i ].name === objectIndex ) {
+	this.path = path;
+	this.parsedPath = parsedPath || PropertyBinding.parseTrackName( path );
 
-							objectIndex = i;
-							break;
+	this.node = PropertyBinding.findNode( rootNode, this.parsedPath.nodeName ) || rootNode;
 
-						}
+	this.rootNode = rootNode;
 
-					}
+}
 
-					break;
+Object.assign( PropertyBinding, {
 
-				default:
+	Composite: Composite,
 
-					if ( targetObject[ objectName ] === undefined ) {
+	create: function( root, path, parsedPath ) {
 
-						console.error( '  can not bind to objectName of node, undefined', this );
-						return;
+		if ( ! ( root && root.isAnimationObjectGroup ) ) {
 
-					}
+			return new PropertyBinding( root, path, parsedPath );
 
-					targetObject = targetObject[ objectName ];
+		} else {
 
-			}
+			return new PropertyBinding.Composite( root, path, parsedPath );
 
+		}
 
-			if ( objectIndex !== undefined ) {
+	},
 
-				if ( targetObject[ objectIndex ] === undefined ) {
+	parseTrackName: function( trackName ) {
 
-					console.error( "  trying to bind to objectIndex of objectName, but is undefined:", this, targetObject );
-					return;
+		// matches strings in the form of:
+		//    nodeName.property
+		//    nodeName.property[accessor]
+		//    nodeName.material.property[accessor]
+		//    uuid.property[accessor]
+		//    uuid.objectName[objectIndex].propertyName[propertyIndex]
+		//    parentName/nodeName.property
+		//    parentName/parentName/nodeName.property[index]
+		//    .bone[Armature.DEF_cog].position
+		//    scene:helium_balloon_model:helium_balloon_model.position
+		// created and tested via https://regex101.com/#javascript
 
-				}
+		var re = /^((?:[\w-]+[\/:])*)([\w-]+)?(?:\.([\w-]+)(?:\[(.+)\])?)?\.([\w-]+)(?:\[(.+)\])?$/;
+		var matches = re.exec( trackName );
 
-				targetObject = targetObject[ objectIndex ];
+		if ( ! matches ) {
 
-			}
+			throw new Error( "cannot parse trackName at all: " + trackName );
 
 		}
 
-		// resolve property
-		var nodeProperty = targetObject[ propertyName ];
-
-		if ( nodeProperty === undefined ) {
+		var results = {
+			// directoryName: matches[ 1 ], // (tschw) currently unused
+			nodeName: matches[ 2 ], 	// allowed to be null, specified root node.
+			objectName: matches[ 3 ],
+			objectIndex: matches[ 4 ],
+			propertyName: matches[ 5 ],
+			propertyIndex: matches[ 6 ]	// allowed to be null, specifies that the whole property is set.
+		};
 
-			var nodeName = parsedPath.nodeName;
+		if ( results.propertyName === null || results.propertyName.length === 0 ) {
 
-			console.error( "  trying to update property for track: " + nodeName +
-					'.' + propertyName + " but it wasn't found.", targetObject );
-			return;
+			throw new Error( "can not parse propertyName from trackName: " + trackName );
 
 		}
 
-		// determine versioning scheme
-		var versioning = this.Versioning.None;
+		return results;
 
-		if ( targetObject.needsUpdate !== undefined ) { // material
+	},
 
-			versioning = this.Versioning.NeedsUpdate;
-			this.targetObject = targetObject;
+	findNode: function( root, nodeName ) {
 
-		} else if ( targetObject.matrixWorldNeedsUpdate !== undefined ) { // node transform
+		if ( ! nodeName || nodeName === "" || nodeName === "root" || nodeName === "." || nodeName === -1 || nodeName === root.name || nodeName === root.uuid ) {
 
-			versioning = this.Versioning.MatrixWorldNeedsUpdate;
-			this.targetObject = targetObject;
+			return root;
 
 		}
 
-		// determine how the property gets bound
-		var bindingType = this.BindingType.Direct;
+		// search into skeleton bones.
+		if ( root.skeleton ) {
 
-		if ( propertyIndex !== undefined ) {
-			// access a sub element of the property array (only primitives are supported right now)
+			var searchSkeleton = function( skeleton ) {
 
-			if ( propertyName === "morphTargetInfluences" ) {
-				// potential optimization, skip this if propertyIndex is already an integer, and convert the integer string to a true integer.
-
-				// support resolving morphTarget names into indices.
-				if ( ! targetObject.geometry ) {
-
-					console.error( '  can not bind to morphTargetInfluences becasuse node does not have a geometry', this );
-					return;
+				for( var i = 0; i < skeleton.bones.length; i ++ ) {
 
-				}
+					var bone = skeleton.bones[ i ];
 
-				if ( ! targetObject.geometry.morphTargets ) {
+					if ( bone.name === nodeName ) {
 
-					console.error( '  can not bind to morphTargetInfluences becasuse node does not have a geometry.morphTargets', this );
-					return;
+						return bone;
 
+					}
 				}
 
-				for ( var i = 0; i < this.node.geometry.morphTargets.length; i ++ ) {
+				return null;
 
-					if ( targetObject.geometry.morphTargets[ i ].name === propertyIndex ) {
+			};
 
-						propertyIndex = i;
-						break;
+			var bone = searchSkeleton( root.skeleton );
 
-					}
+			if ( bone ) {
 
-				}
+				return bone;
 
 			}
+		}
 
-			bindingType = this.BindingType.ArrayElement;
+		// search into node subtree.
+		if ( root.children ) {
 
-			this.resolvedProperty = nodeProperty;
-			this.propertyIndex = propertyIndex;
+			var searchNodeSubtree = function( children ) {
 
-		} else if ( nodeProperty.fromArray !== undefined && nodeProperty.toArray !== undefined ) {
-			// must use copy for Object3D.Euler/Quaternion
+				for( var i = 0; i < children.length; i ++ ) {
 
-			bindingType = this.BindingType.HasFromToArray;
+					var childNode = children[ i ];
 
-			this.resolvedProperty = nodeProperty;
+					if ( childNode.name === nodeName || childNode.uuid === nodeName ) {
 
-		} else if ( nodeProperty.length !== undefined ) {
+						return childNode;
 
-			bindingType = this.BindingType.EntireArray;
+					}
 
-			this.resolvedProperty = nodeProperty;
+					var result = searchNodeSubtree( childNode.children );
 
-		} else {
+					if ( result ) return result;
 
-			this.propertyName = propertyName;
+				}
 
-		}
+				return null;
 
-		// select getter / setter
-		this.getValue = this.GetterByBindingType[ bindingType ];
-		this.setValue = this.SetterByBindingTypeAndVersioning[ bindingType ][ versioning ];
+			};
 
-	},
+			var subTreeNode = searchNodeSubtree( root.children );
 
-	unbind: function() {
+			if ( subTreeNode ) {
 
-		this.node = null;
+				return subTreeNode;
 
-		// back to the prototype version of getValue / setValue
-		// note: avoiding to mutate the shape of 'this' via 'delete'
-		this.getValue = this._getValue_unbound;
-		this.setValue = this._setValue_unbound;
+			}
+
+		}
+
+		return null;
 
 	}
 
-};
+} );
 
 Object.assign( PropertyBinding.prototype, { // prototype, continued
 
+	constructor: PropertyBinding,
+
 	// these are used to "bind" a nonexistent property
 	_getValue_unavailable: function() {},
 	_setValue_unavailable: function() {},
@@ -444,209 +402,250 @@ Object.assign( PropertyBinding.prototype, { // prototype, continued
 
 		]
 
-	]
+	],
 
-} );
+	getValue: function getValue_unbound( targetArray, offset ) {
 
-PropertyBinding.Composite =
-		function( targetGroup, path, optionalParsedPath ) {
+		this.bind();
+		this.getValue( targetArray, offset );
 
-	var parsedPath = optionalParsedPath ||
-			PropertyBinding.parseTrackName( path );
+		// Note: This class uses a State pattern on a per-method basis:
+		// 'bind' sets 'this.getValue' / 'setValue' and shadows the
+		// prototype version of these methods with one that represents
+		// the bound state. When the property is not found, the methods
+		// become no-ops.
 
-	this._targetGroup = targetGroup;
-	this._bindings = targetGroup.subscribe_( path, parsedPath );
+	},
 
-};
+	setValue: function getValue_unbound( sourceArray, offset ) {
 
-PropertyBinding.Composite.prototype = {
+		this.bind();
+		this.setValue( sourceArray, offset );
 
-	constructor: PropertyBinding.Composite,
+	},
 
-	getValue: function( array, offset ) {
+	// create getter / setter pair for a property in the scene graph
+	bind: function() {
 
-		this.bind(); // bind all binding
+		var targetObject = this.node,
+			parsedPath = this.parsedPath,
 
-		var firstValidIndex = this._targetGroup.nCachedObjects_,
-			binding = this._bindings[ firstValidIndex ];
+			objectName = parsedPath.objectName,
+			propertyName = parsedPath.propertyName,
+			propertyIndex = parsedPath.propertyIndex;
 
-		// and only call .getValue on the first
-		if ( binding !== undefined ) binding.getValue( array, offset );
+		if ( ! targetObject ) {
 
-	},
+			targetObject = PropertyBinding.findNode(
+					this.rootNode, parsedPath.nodeName ) || this.rootNode;
 
-	setValue: function( array, offset ) {
+			this.node = targetObject;
 
-		var bindings = this._bindings;
+		}
 
-		for ( var i = this._targetGroup.nCachedObjects_,
-				n = bindings.length; i !== n; ++ i ) {
+		// set fail state so we can just 'return' on error
+		this.getValue = this._getValue_unavailable;
+		this.setValue = this._setValue_unavailable;
 
-			bindings[ i ].setValue( array, offset );
+		// ensure there is a value node
+		if ( ! targetObject ) {
 
-		}
+			console.error( "  trying to update node for track: " + this.path + " but it wasn't found." );
+			return;
 
-	},
+		}
 
-	bind: function() {
+		if ( objectName ) {
 
-		var bindings = this._bindings;
+			var objectIndex = parsedPath.objectIndex;
 
-		for ( var i = this._targetGroup.nCachedObjects_,
-				n = bindings.length; i !== n; ++ i ) {
+			// special cases were we need to reach deeper into the hierarchy to get the face materials....
+			switch ( objectName ) {
 
-			bindings[ i ].bind();
+				case 'materials':
 
-		}
+					if ( ! targetObject.material ) {
 
-	},
+						console.error( '  can not bind to material as node does not have a material', this );
+						return;
 
-	unbind: function() {
+					}
 
-		var bindings = this._bindings;
+					if ( ! targetObject.material.materials ) {
 
-		for ( var i = this._targetGroup.nCachedObjects_,
-				n = bindings.length; i !== n; ++ i ) {
+						console.error( '  can not bind to material.materials as node.material does not have a materials array', this );
+						return;
 
-			bindings[ i ].unbind();
+					}
 
-		}
+					targetObject = targetObject.material.materials;
 
-	}
+					break;
 
-};
+				case 'bones':
 
-PropertyBinding.create = function( root, path, parsedPath ) {
+					if ( ! targetObject.skeleton ) {
 
-	if ( ! ( root && root.isAnimationObjectGroup ) ) {
+						console.error( '  can not bind to bones as node does not have a skeleton', this );
+						return;
 
-		return new PropertyBinding( root, path, parsedPath );
+					}
 
-	} else {
+					// potential future optimization: skip this if propertyIndex is already an integer
+					// and convert the integer string to a true integer.
 
-		return new PropertyBinding.Composite( root, path, parsedPath );
+					targetObject = targetObject.skeleton.bones;
 
-	}
+					// support resolving morphTarget names into indices.
+					for ( var i = 0; i < targetObject.length; i ++ ) {
 
-};
+						if ( targetObject[ i ].name === objectIndex ) {
 
-PropertyBinding.parseTrackName = function( trackName ) {
+							objectIndex = i;
+							break;
 
-	// matches strings in the form of:
-	//    nodeName.property
-	//    nodeName.property[accessor]
-	//    nodeName.material.property[accessor]
-	//    uuid.property[accessor]
-	//    uuid.objectName[objectIndex].propertyName[propertyIndex]
-	//    parentName/nodeName.property
-	//    parentName/parentName/nodeName.property[index]
-	//    .bone[Armature.DEF_cog].position
-	//    scene:helium_balloon_model:helium_balloon_model.position
-	// created and tested via https://regex101.com/#javascript
+						}
 
-	var re = /^((?:[\w-]+[\/:])*)([\w-]+)?(?:\.([\w-]+)(?:\[(.+)\])?)?\.([\w-]+)(?:\[(.+)\])?$/;
-	var matches = re.exec( trackName );
+					}
 
-	if ( ! matches ) {
+					break;
 
-		throw new Error( "cannot parse trackName at all: " + trackName );
+				default:
 
-	}
+					if ( targetObject[ objectName ] === undefined ) {
 
-	var results = {
-		// directoryName: matches[ 1 ], // (tschw) currently unused
-		nodeName: matches[ 2 ], 	// allowed to be null, specified root node.
-		objectName: matches[ 3 ],
-		objectIndex: matches[ 4 ],
-		propertyName: matches[ 5 ],
-		propertyIndex: matches[ 6 ]	// allowed to be null, specifies that the whole property is set.
-	};
+						console.error( '  can not bind to objectName of node, undefined', this );
+						return;
 
-	if ( results.propertyName === null || results.propertyName.length === 0 ) {
+					}
 
-		throw new Error( "can not parse propertyName from trackName: " + trackName );
+					targetObject = targetObject[ objectName ];
 
-	}
+			}
 
-	return results;
 
-};
+			if ( objectIndex !== undefined ) {
 
-PropertyBinding.findNode = function( root, nodeName ) {
+				if ( targetObject[ objectIndex ] === undefined ) {
 
-	if ( ! nodeName || nodeName === "" || nodeName === "root" || nodeName === "." || nodeName === -1 || nodeName === root.name || nodeName === root.uuid ) {
+					console.error( "  trying to bind to objectIndex of objectName, but is undefined:", this, targetObject );
+					return;
 
-		return root;
+				}
 
-	}
+				targetObject = targetObject[ objectIndex ];
 
-	// search into skeleton bones.
-	if ( root.skeleton ) {
+			}
 
-		var searchSkeleton = function( skeleton ) {
+		}
 
-			for( var i = 0; i < skeleton.bones.length; i ++ ) {
+		// resolve property
+		var nodeProperty = targetObject[ propertyName ];
 
-				var bone = skeleton.bones[ i ];
+		if ( nodeProperty === undefined ) {
 
-				if ( bone.name === nodeName ) {
+			var nodeName = parsedPath.nodeName;
 
-					return bone;
+			console.error( "  trying to update property for track: " + nodeName +
+				'.' + propertyName + " but it wasn't found.", targetObject );
+			return;
 
-				}
-			}
+		}
 
-			return null;
+		// determine versioning scheme
+		var versioning = this.Versioning.None;
 
-		};
+		if ( targetObject.needsUpdate !== undefined ) { // material
 
-		var bone = searchSkeleton( root.skeleton );
+			versioning = this.Versioning.NeedsUpdate;
+			this.targetObject = targetObject;
 
-		if ( bone ) {
+		} else if ( targetObject.matrixWorldNeedsUpdate !== undefined ) { // node transform
 
-			return bone;
+			versioning = this.Versioning.MatrixWorldNeedsUpdate;
+			this.targetObject = targetObject;
 
 		}
-	}
 
-	// search into node subtree.
-	if ( root.children ) {
+		// determine how the property gets bound
+		var bindingType = this.BindingType.Direct;
+
+		if ( propertyIndex !== undefined ) {
+			// access a sub element of the property array (only primitives are supported right now)
+
+			if ( propertyName === "morphTargetInfluences" ) {
+				// potential optimization, skip this if propertyIndex is already an integer, and convert the integer string to a true integer.
 
-		var searchNodeSubtree = function( children ) {
+				// support resolving morphTarget names into indices.
+				if ( ! targetObject.geometry ) {
 
-			for( var i = 0; i < children.length; i ++ ) {
+					console.error( '  can not bind to morphTargetInfluences becasuse node does not have a geometry', this );
+					return;
 
-				var childNode = children[ i ];
+				}
 
-				if ( childNode.name === nodeName || childNode.uuid === nodeName ) {
+				if ( ! targetObject.geometry.morphTargets ) {
 
-					return childNode;
+					console.error( '  can not bind to morphTargetInfluences becasuse node does not have a geometry.morphTargets', this );
+					return;
 
 				}
 
-				var result = searchNodeSubtree( childNode.children );
+				for ( var i = 0; i < this.node.geometry.morphTargets.length; i ++ ) {
+
+					if ( targetObject.geometry.morphTargets[ i ].name === propertyIndex ) {
+
+						propertyIndex = i;
+						break;
+
+					}
 
-				if ( result ) return result;
+				}
 
 			}
 
-			return null;
+			bindingType = this.BindingType.ArrayElement;
 
-		};
+			this.resolvedProperty = nodeProperty;
+			this.propertyIndex = propertyIndex;
+
+		} else if ( nodeProperty.fromArray !== undefined && nodeProperty.toArray !== undefined ) {
+			// must use copy for Object3D.Euler/Quaternion
+
+			bindingType = this.BindingType.HasFromToArray;
+
+			this.resolvedProperty = nodeProperty;
 
-		var subTreeNode = searchNodeSubtree( root.children );
+		} else if ( nodeProperty.length !== undefined ) {
+
+			bindingType = this.BindingType.EntireArray;
 
-		if ( subTreeNode ) {
+			this.resolvedProperty = nodeProperty;
+
+		} else {
 
-			return subTreeNode;
+			this.propertyName = propertyName;
 
 		}
 
-	}
+		// select getter / setter
+		this.getValue = this.GetterByBindingType[ bindingType ];
+		this.setValue = this.SetterByBindingTypeAndVersioning[ bindingType ][ versioning ];
+
+	},
+
+	unbind: function() {
+
+		this.node = null;
+
+		// back to the prototype version of getValue / setValue
+		// note: avoiding to mutate the shape of 'this' via 'delete'
+		this.getValue = this._getValue_unbound;
+		this.setValue = this._setValue_unbound;
 
-	return null;
+	}
 
-};
+} );
 
 
 export { PropertyBinding };