@openhps/core
Version:
Open Hybrid Positioning System - Core component
512 lines (464 loc) • 18.7 kB
JavaScript
// 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) {
console.warn('THREE.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) {
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;
case 'map':
if ('map' in targetObject) {
targetObject = targetObject.map;
break;
}
if (!targetObject.material) {
console.error('THREE.PropertyBinding: Can not bind to material as node does not have a material.', this);
return;
}
if (!targetObject.material.map) {
console.error('THREE.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) {
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.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) {
console.error('THREE.PropertyBinding: Can not bind to morphTargetInfluences because node does not have a geometry.', this);
return;
}
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];
}
}
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 };