Spaces:
Sleeping
Sleeping
| // 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 }; | |