| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794 |
- import { warn, error } from '../utils.js';
- // 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 = /*@__PURE__*/ /((?:WC+[\/:])*)/.source.replace( 'WC', _wordChar );
- // Target node. May contain word characters (a-zA-Z0-9_) and '.' or '-'.
- const _nodeRe = /*@__PURE__*/ /(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 = /*@__PURE__*/ /(?:\.(WC+)(?:\[(.+)\])?)?/.source.replace( 'WC', _wordChar );
- // Property and accessor. May not contain reserved characters. Accessor may
- // contain any non-bracket characters.
- const _propertyRe = /*@__PURE__*/ /\.(WC+)(?:\[(.+)\])?/.source.replace( 'WC', _wordChar );
- const _trackRe = new RegExp( ''
- + '^'
- + _directoryRe
- + _nodeRe
- + _objectRe
- + _propertyRe
- + '$'
- );
- const _supportedObjectNames = [ 'material', 'materials', 'bones', 'map' ];
- 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.
- /**
- * This holds a reference to a real property in the scene graph; used internally.
- */
- class PropertyBinding {
- /**
- * Constructs a new property binding.
- *
- * @param {Object} rootNode - The root node.
- * @param {string} path - The path.
- * @param {?Object} [parsedPath] - The parsed path.
- */
- constructor( rootNode, path, parsedPath ) {
- /**
- * The object path to the animated property.
- *
- * @type {string}
- */
- this.path = path;
- /**
- * An object holding information about the path.
- *
- * @type {Object}
- */
- this.parsedPath = parsedPath || PropertyBinding.parseTrackName( path );
- /**
- * The object owns the animated property.
- *
- * @type {?Object}
- */
- this.node = PropertyBinding.findNode( rootNode, this.parsedPath.nodeName );
- /**
- * The root node.
- *
- * @type {Object3D|Skeleton}
- */
- this.rootNode = rootNode;
- // initial state of these methods that calls 'bind'
- this.getValue = this._getValue_unbound;
- this.setValue = this._setValue_unbound;
- }
- /**
- * Factory method for creating a property binding from the given parameters.
- *
- * @static
- * @param {Object} root - The root node.
- * @param {string} path - The path.
- * @param {?Object} [parsedPath] - The parsed path.
- * @return {PropertyBinding|Composite} The created property binding or composite.
- */
- 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} The sanitized node name.
- */
- static sanitizeNodeName( name ) {
- return name.replace( /\s/g, '_' ).replace( _reservedRe, '' );
- }
- /**
- * Parses the given track name (an object path to an animated property) and
- * returns an object with information about the path. Matches strings in the following forms:
- *
- * - 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
- *
- * @static
- * @param {string} trackName - The track name to parse.
- * @return {Object} The parsed track name as an object.
- */
- static parseTrackName( trackName ) {
- const matches = _trackRe.exec( trackName );
- if ( matches === null ) {
- 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;
- }
- /**
- * Searches for a node in the hierarchy of the given root object by the given
- * node name.
- *
- * @static
- * @param {Object} root - The root object.
- * @param {string|number} nodeName - The name of the node.
- * @return {?Object} The found node. Returns `null` if no object was found.
- */
- static findNode( root, nodeName ) {
- if ( nodeName === undefined || 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.targetObject[ 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 );
- }
- /**
- * Creates a getter / setter pair for the property tracked by this binding.
- */
- 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.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 ) {
- warn( 'PropertyBinding: No target node found for track: ' + this.path + '.' );
- 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 ) {
- error( 'PropertyBinding: Can not bind to material as node does not have a material.', this );
- return;
- }
- if ( ! targetObject.material.materials ) {
- error( '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 ) {
- error( '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;
- case 'map':
- if ( 'map' in targetObject ) {
- targetObject = targetObject.map;
- break;
- }
- if ( ! targetObject.material ) {
- error( 'PropertyBinding: Can not bind to material as node does not have a material.', this );
- return;
- }
- if ( ! targetObject.material.map ) {
- error( 'PropertyBinding: Can not bind to material.map as node.material does not have a map.', this );
- return;
- }
- targetObject = targetObject.material.map;
- break;
- default:
- if ( targetObject[ objectName ] === undefined ) {
- error( 'PropertyBinding: Can not bind to objectName of node undefined.', this );
- return;
- }
- targetObject = targetObject[ objectName ];
- }
- if ( objectIndex !== undefined ) {
- if ( targetObject[ objectIndex ] === undefined ) {
- error( '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;
- error( '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.isMaterial === true ) {
- versioning = this.Versioning.NeedsUpdate;
- } else if ( targetObject.isObject3D === true ) {
- 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 ) {
- error( 'PropertyBinding: Can not bind to morphTargetInfluences because node does not have a geometry.', this );
- return;
- }
- if ( ! targetObject.geometry.morphAttributes ) {
- error( '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 ];
- }
- }
- 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 ];
- }
- /**
- * Unbinds the property.
- */
- 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 };
|