starry / backend /libs /three /animation /PropertyBinding.js
k-l-lambda's picture
feat: add Python ML services (CPU mode) with model download
2b7aae2
// 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 };