// Characters [].:/ are reserved for track binding syntax. const _RESERVED_CHARS_RE = '\\[\\]\\.:\\/'; const _reservedRe = new RegExp( '[' + _RESERVED_CHARS_RE + ']', 'g' ); // Attempts to allow node names from any language. ES5's `\w` regexp matches // only latin characters, and the unicode \p{L} is not yet supported. So // instead, we exclude reserved characters and match everything else. const _wordChar = '[^' + _RESERVED_CHARS_RE + ']'; const _wordCharOrDot = '[^' + _RESERVED_CHARS_RE.replace( '\\.', '' ) + ']'; // Parent directories, delimited by '/' or ':'. Currently unused, but must // be matched to parse the rest of the track name. const _directoryRe = /((?:WC+[\/:])*)/.source.replace( 'WC', _wordChar ); // Target node. May contain word characters (a-zA-Z0-9_) and '.' or '-'. const _nodeRe = /(WCOD+)?/.source.replace( 'WCOD', _wordCharOrDot ); // Object on target node, and accessor. May not contain reserved // characters. Accessor may contain any character except closing bracket. const _objectRe = /(?:\.(WC+)(?:\[(.+)\])?)?/.source.replace( 'WC', _wordChar ); // Property and accessor. May not contain reserved characters. Accessor may // contain any non-bracket characters. const _propertyRe = /\.(WC+)(?:\[(.+)\])?/.source.replace( 'WC', _wordChar ); const _trackRe = new RegExp( '' + '^' + _directoryRe + _nodeRe + _objectRe + _propertyRe + '$' ); const _supportedObjectNames = [ 'material', 'materials', 'bones' ]; class Composite { constructor( targetGroup, path, optionalParsedPath ) { const parsedPath = optionalParsedPath || PropertyBinding.parseTrackName( path ); this._targetGroup = targetGroup; this._bindings = targetGroup.subscribe_( path, parsedPath ); } getValue( array, offset ) { this.bind(); // bind all binding const firstValidIndex = this._targetGroup.nCachedObjects_, binding = this._bindings[ firstValidIndex ]; // and only call .getValue on the first if ( binding !== undefined ) binding.getValue( array, offset ); } setValue( array, offset ) { const bindings = this._bindings; for ( let i = this._targetGroup.nCachedObjects_, n = bindings.length; i !== n; ++ i ) { bindings[ i ].setValue( array, offset ); } } bind() { const bindings = this._bindings; for ( let i = this._targetGroup.nCachedObjects_, n = bindings.length; i !== n; ++ i ) { bindings[ i ].bind(); } } unbind() { const bindings = this._bindings; for ( let i = this._targetGroup.nCachedObjects_, n = bindings.length; i !== n; ++ i ) { bindings[ i ].unbind(); } } } // 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. class PropertyBinding { constructor( rootNode, path, parsedPath ) { this.path = path; this.parsedPath = parsedPath || PropertyBinding.parseTrackName( path ); this.node = PropertyBinding.findNode( rootNode, this.parsedPath.nodeName ) || rootNode; this.rootNode = rootNode; // initial state of these methods that calls 'bind' this.getValue = this._getValue_unbound; this.setValue = this._setValue_unbound; } static create( root, path, parsedPath ) { if ( ! ( root && root.isAnimationObjectGroup ) ) { return new PropertyBinding( root, path, parsedPath ); } else { return new PropertyBinding.Composite( root, path, parsedPath ); } } /** * Replaces spaces with underscores and removes unsupported characters from * node names, to ensure compatibility with parseTrackName(). * * @param {string} name Node name to be sanitized. * @return {string} */ static sanitizeNodeName( name ) { return name.replace( /\s/g, '_' ).replace( _reservedRe, '' ); } static parseTrackName( trackName ) { const matches = _trackRe.exec( trackName ); if ( ! matches ) { throw new Error( 'PropertyBinding: Cannot parse trackName: ' + trackName ); } const results = { // directoryName: matches[ 1 ], // (tschw) currently unused nodeName: matches[ 2 ], objectName: matches[ 3 ], objectIndex: matches[ 4 ], propertyName: matches[ 5 ], // required propertyIndex: matches[ 6 ] }; const lastDot = results.nodeName && results.nodeName.lastIndexOf( '.' ); if ( lastDot !== undefined && lastDot !== - 1 ) { const objectName = results.nodeName.substring( lastDot + 1 ); // Object names must be checked against an allowlist. Otherwise, there // is no way to parse 'foo.bar.baz': 'baz' must be a property, but // 'bar' could be the objectName, or part of a nodeName (which can // include '.' characters). if ( _supportedObjectNames.indexOf( objectName ) !== - 1 ) { results.nodeName = results.nodeName.substring( 0, lastDot ); results.objectName = objectName; } } if ( results.propertyName === null || results.propertyName.length === 0 ) { throw new Error( 'PropertyBinding: can not parse propertyName from trackName: ' + trackName ); } return results; } static findNode( root, nodeName ) { if ( ! nodeName || nodeName === '' || nodeName === '.' || nodeName === - 1 || nodeName === root.name || nodeName === root.uuid ) { return root; } // search into skeleton bones. if ( root.skeleton ) { const bone = root.skeleton.getBoneByName( nodeName ); if ( bone !== undefined ) { return bone; } } // search into node subtree. if ( root.children ) { const searchNodeSubtree = function ( children ) { for ( let i = 0; i < children.length; i ++ ) { const childNode = children[ i ]; if ( childNode.name === nodeName || childNode.uuid === nodeName ) { return childNode; } const result = searchNodeSubtree( childNode.children ); if ( result ) return result; } return null; }; const subTreeNode = searchNodeSubtree( root.children ); if ( subTreeNode ) { return subTreeNode; } } return null; } // these are used to "bind" a nonexistent property _getValue_unavailable() {} _setValue_unavailable() {} // Getters _getValue_direct( buffer, offset ) { buffer[ offset ] = this.node[ this.propertyName ]; } _getValue_array( buffer, offset ) { const source = this.resolvedProperty; for ( let i = 0, n = source.length; i !== n; ++ i ) { buffer[ offset ++ ] = source[ i ]; } } _getValue_arrayElement( buffer, offset ) { buffer[ offset ] = this.resolvedProperty[ this.propertyIndex ]; } _getValue_toArray( buffer, offset ) { this.resolvedProperty.toArray( buffer, offset ); } // Direct _setValue_direct( buffer, offset ) { this.targetObject[ this.propertyName ] = buffer[ offset ]; } _setValue_direct_setNeedsUpdate( buffer, offset ) { this.targetObject[ this.propertyName ] = buffer[ offset ]; this.targetObject.needsUpdate = true; } _setValue_direct_setMatrixWorldNeedsUpdate( buffer, offset ) { this.targetObject[ this.propertyName ] = buffer[ offset ]; this.targetObject.matrixWorldNeedsUpdate = true; } // EntireArray _setValue_array( buffer, offset ) { const dest = this.resolvedProperty; for ( let i = 0, n = dest.length; i !== n; ++ i ) { dest[ i ] = buffer[ offset ++ ]; } } _setValue_array_setNeedsUpdate( buffer, offset ) { const dest = this.resolvedProperty; for ( let i = 0, n = dest.length; i !== n; ++ i ) { dest[ i ] = buffer[ offset ++ ]; } this.targetObject.needsUpdate = true; } _setValue_array_setMatrixWorldNeedsUpdate( buffer, offset ) { const dest = this.resolvedProperty; for ( let i = 0, n = dest.length; i !== n; ++ i ) { dest[ i ] = buffer[ offset ++ ]; } this.targetObject.matrixWorldNeedsUpdate = true; } // ArrayElement _setValue_arrayElement( buffer, offset ) { this.resolvedProperty[ this.propertyIndex ] = buffer[ offset ]; } _setValue_arrayElement_setNeedsUpdate( buffer, offset ) { this.resolvedProperty[ this.propertyIndex ] = buffer[ offset ]; this.targetObject.needsUpdate = true; } _setValue_arrayElement_setMatrixWorldNeedsUpdate( buffer, offset ) { this.resolvedProperty[ this.propertyIndex ] = buffer[ offset ]; this.targetObject.matrixWorldNeedsUpdate = true; } // HasToFromArray _setValue_fromArray( buffer, offset ) { this.resolvedProperty.fromArray( buffer, offset ); } _setValue_fromArray_setNeedsUpdate( buffer, offset ) { this.resolvedProperty.fromArray( buffer, offset ); this.targetObject.needsUpdate = true; } _setValue_fromArray_setMatrixWorldNeedsUpdate( buffer, offset ) { this.resolvedProperty.fromArray( buffer, offset ); this.targetObject.matrixWorldNeedsUpdate = true; } _getValue_unbound( targetArray, offset ) { this.bind(); this.getValue( targetArray, offset ); } _setValue_unbound( sourceArray, offset ) { this.bind(); this.setValue( sourceArray, offset ); } // create getter / setter pair for a property in the scene graph bind() { let targetObject = this.node; const parsedPath = this.parsedPath; const objectName = parsedPath.objectName; const propertyName = parsedPath.propertyName; let propertyIndex = parsedPath.propertyIndex; if ( ! targetObject ) { targetObject = PropertyBinding.findNode( this.rootNode, parsedPath.nodeName ) || this.rootNode; this.node = targetObject; } // set fail state so we can just 'return' on error this.getValue = this._getValue_unavailable; this.setValue = this._setValue_unavailable; // ensure there is a value node if ( ! targetObject ) { console.error( 'THREE.PropertyBinding: Trying to update node for track: ' + this.path + ' but it wasn\'t found.' ); return; } if ( objectName ) { let 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 ) { console.error( 'THREE.PropertyBinding: Can not bind to material as node does not have a material.', this ); return; } if ( ! targetObject.material.materials ) { console.error( 'THREE.PropertyBinding: Can not bind to material.materials as node.material does not have a materials array.', this ); return; } targetObject = targetObject.material.materials; break; case 'bones': if ( ! targetObject.skeleton ) { console.error( 'THREE.PropertyBinding: 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 ( let i = 0; i < targetObject.length; i ++ ) { if ( targetObject[ i ].name === objectIndex ) { objectIndex = i; break; } } break; default: if ( targetObject[ objectName ] === undefined ) { console.error( 'THREE.PropertyBinding: Can not bind to objectName of node undefined.', this ); return; } targetObject = targetObject[ objectName ]; } if ( objectIndex !== undefined ) { if ( targetObject[ objectIndex ] === undefined ) { console.error( 'THREE.PropertyBinding: Trying to bind to objectIndex of objectName, but is undefined.', this, targetObject ); return; } targetObject = targetObject[ objectIndex ]; } } // resolve property const nodeProperty = targetObject[ propertyName ]; if ( nodeProperty === undefined ) { const nodeName = parsedPath.nodeName; console.error( 'THREE.PropertyBinding: Trying to update property for track: ' + nodeName + '.' + propertyName + ' but it wasn\'t found.', targetObject ); return; } // determine versioning scheme let versioning = this.Versioning.None; this.targetObject = targetObject; if ( targetObject.needsUpdate !== undefined ) { // material versioning = this.Versioning.NeedsUpdate; } else if ( targetObject.matrixWorldNeedsUpdate !== undefined ) { // node transform versioning = this.Versioning.MatrixWorldNeedsUpdate; } // determine how the property gets bound let 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. // support resolving morphTarget names into indices. if ( ! targetObject.geometry ) { console.error( 'THREE.PropertyBinding: Can not bind to morphTargetInfluences because node does not have a geometry.', this ); return; } if ( targetObject.geometry.isBufferGeometry ) { if ( ! targetObject.geometry.morphAttributes ) { console.error( 'THREE.PropertyBinding: Can not bind to morphTargetInfluences because node does not have a geometry.morphAttributes.', this ); return; } if ( targetObject.morphTargetDictionary[ propertyIndex ] !== undefined ) { propertyIndex = targetObject.morphTargetDictionary[ propertyIndex ]; } } else { console.error( 'THREE.PropertyBinding: Can not bind to morphTargetInfluences on THREE.Geometry. Use THREE.BufferGeometry instead.', this ); return; } } 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; } else if ( Array.isArray( nodeProperty ) ) { bindingType = this.BindingType.EntireArray; this.resolvedProperty = nodeProperty; } else { this.propertyName = propertyName; } // select getter / setter this.getValue = this.GetterByBindingType[ bindingType ]; this.setValue = this.SetterByBindingTypeAndVersioning[ bindingType ][ versioning ]; } unbind() { 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; } } PropertyBinding.Composite = Composite; PropertyBinding.prototype.BindingType = { Direct: 0, EntireArray: 1, ArrayElement: 2, HasFromToArray: 3 }; PropertyBinding.prototype.Versioning = { None: 0, NeedsUpdate: 1, MatrixWorldNeedsUpdate: 2 }; PropertyBinding.prototype.GetterByBindingType = [ PropertyBinding.prototype._getValue_direct, PropertyBinding.prototype._getValue_array, PropertyBinding.prototype._getValue_arrayElement, PropertyBinding.prototype._getValue_toArray, ]; PropertyBinding.prototype.SetterByBindingTypeAndVersioning = [ [ // Direct PropertyBinding.prototype._setValue_direct, PropertyBinding.prototype._setValue_direct_setNeedsUpdate, PropertyBinding.prototype._setValue_direct_setMatrixWorldNeedsUpdate, ], [ // EntireArray PropertyBinding.prototype._setValue_array, PropertyBinding.prototype._setValue_array_setNeedsUpdate, PropertyBinding.prototype._setValue_array_setMatrixWorldNeedsUpdate, ], [ // ArrayElement PropertyBinding.prototype._setValue_arrayElement, PropertyBinding.prototype._setValue_arrayElement_setNeedsUpdate, PropertyBinding.prototype._setValue_arrayElement_setMatrixWorldNeedsUpdate, ], [ // HasToFromArray PropertyBinding.prototype._setValue_fromArray, PropertyBinding.prototype._setValue_fromArray_setNeedsUpdate, PropertyBinding.prototype._setValue_fromArray_setMatrixWorldNeedsUpdate, ] ]; export { PropertyBinding };