// 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.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); } // 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 };