@openhps/core
Version:
Open Hybrid Positioning System - Core component
307 lines (272 loc) • 10.4 kB
JavaScript
import { PropertyBinding } from './PropertyBinding.js';
import { generateUUID } from '../math/MathUtils.js';
/**
* A group of objects that receives a shared animation state.
*
* Usage:
*
* - Add objects you would otherwise pass as 'root' to the
* constructor or the .clipAction method of AnimationMixer.
* - Instead pass this object as 'root'.
* - You can also add and remove objects later when the mixer is running.
*
* Note:
*
* - Objects of this class appear as one object to the mixer,
* so cache control of the individual objects must be done on the group.
*
* Limitation:
*
* - The animated properties must be compatible among the all objects in the group.
* - A single property can either be controlled through a target group or directly, but not both.
*/
class AnimationObjectGroup {
/**
* Constructs a new animation group.
*
* @param {...Object3D} arguments - An arbitrary number of 3D objects that share the same animation state.
*/
constructor() {
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isAnimationObjectGroup = true;
/**
* The UUID of the 3D object.
*
* @type {string}
* @readonly
*/
this.uuid = generateUUID();
// cached objects followed by the active ones
this._objects = Array.prototype.slice.call(arguments);
this.nCachedObjects_ = 0; // threshold
// note: read by PropertyBinding.Composite
const indices = {};
this._indicesByUUID = indices; // for bookkeeping
for (let i = 0, n = arguments.length; i !== n; ++i) {
indices[arguments[i].uuid] = i;
}
this._paths = []; // inside: string
this._parsedPaths = []; // inside: { we don't care, here }
this._bindings = []; // inside: Array< PropertyBinding >
this._bindingsIndicesByPath = {}; // inside: indices in these arrays
const scope = this;
this.stats = {
objects: {
get total() {
return scope._objects.length;
},
get inUse() {
return this.total - scope.nCachedObjects_;
}
},
get bindingsPerObject() {
return scope._bindings.length;
}
};
}
/**
* Adds an arbitrary number of objects to this animation group.
*
* @param {...Object3D} arguments - The 3D objects to add.
*/
add() {
const objects = this._objects,
indicesByUUID = this._indicesByUUID,
paths = this._paths,
parsedPaths = this._parsedPaths,
bindings = this._bindings,
nBindings = bindings.length;
let knownObject = undefined,
nObjects = objects.length,
nCachedObjects = this.nCachedObjects_;
for (let i = 0, n = arguments.length; i !== n; ++i) {
const object = arguments[i],
uuid = object.uuid;
let index = indicesByUUID[uuid];
if (index === undefined) {
// unknown object -> add it to the ACTIVE region
index = nObjects++;
indicesByUUID[uuid] = index;
objects.push(object);
// accounting is done, now do the same for all bindings
for (let j = 0, m = nBindings; j !== m; ++j) {
bindings[j].push(new PropertyBinding(object, paths[j], parsedPaths[j]));
}
} else if (index < nCachedObjects) {
knownObject = objects[index];
// move existing object to the ACTIVE region
const firstActiveIndex = --nCachedObjects,
lastCachedObject = objects[firstActiveIndex];
indicesByUUID[lastCachedObject.uuid] = index;
objects[index] = lastCachedObject;
indicesByUUID[uuid] = firstActiveIndex;
objects[firstActiveIndex] = object;
// accounting is done, now do the same for all bindings
for (let j = 0, m = nBindings; j !== m; ++j) {
const bindingsForPath = bindings[j],
lastCached = bindingsForPath[firstActiveIndex];
let binding = bindingsForPath[index];
bindingsForPath[index] = lastCached;
if (binding === undefined) {
// since we do not bother to create new bindings
// for objects that are cached, the binding may
// or may not exist
binding = new PropertyBinding(object, paths[j], parsedPaths[j]);
}
bindingsForPath[firstActiveIndex] = binding;
}
} else if (objects[index] !== knownObject) {
console.error('THREE.AnimationObjectGroup: Different objects with the same UUID ' + 'detected. Clean the caches or recreate your infrastructure when reloading scenes.');
} // else the object is already where we want it to be
} // for arguments
this.nCachedObjects_ = nCachedObjects;
}
/**
* Removes an arbitrary number of objects to this animation group
*
* @param {...Object3D} arguments - The 3D objects to remove.
*/
remove() {
const objects = this._objects,
indicesByUUID = this._indicesByUUID,
bindings = this._bindings,
nBindings = bindings.length;
let nCachedObjects = this.nCachedObjects_;
for (let i = 0, n = arguments.length; i !== n; ++i) {
const object = arguments[i],
uuid = object.uuid,
index = indicesByUUID[uuid];
if (index !== undefined && index >= nCachedObjects) {
// move existing object into the CACHED region
const lastCachedIndex = nCachedObjects++,
firstActiveObject = objects[lastCachedIndex];
indicesByUUID[firstActiveObject.uuid] = index;
objects[index] = firstActiveObject;
indicesByUUID[uuid] = lastCachedIndex;
objects[lastCachedIndex] = object;
// accounting is done, now do the same for all bindings
for (let j = 0, m = nBindings; j !== m; ++j) {
const bindingsForPath = bindings[j],
firstActive = bindingsForPath[lastCachedIndex],
binding = bindingsForPath[index];
bindingsForPath[index] = firstActive;
bindingsForPath[lastCachedIndex] = binding;
}
}
} // for arguments
this.nCachedObjects_ = nCachedObjects;
}
/**
* Deallocates all memory resources for the passed 3D objects of this animation group.
*
* @param {...Object3D} arguments - The 3D objects to uncache.
*/
uncache() {
const objects = this._objects,
indicesByUUID = this._indicesByUUID,
bindings = this._bindings,
nBindings = bindings.length;
let nCachedObjects = this.nCachedObjects_,
nObjects = objects.length;
for (let i = 0, n = arguments.length; i !== n; ++i) {
const object = arguments[i],
uuid = object.uuid,
index = indicesByUUID[uuid];
if (index !== undefined) {
delete indicesByUUID[uuid];
if (index < nCachedObjects) {
// object is cached, shrink the CACHED region
const firstActiveIndex = --nCachedObjects,
lastCachedObject = objects[firstActiveIndex],
lastIndex = --nObjects,
lastObject = objects[lastIndex];
// last cached object takes this object's place
indicesByUUID[lastCachedObject.uuid] = index;
objects[index] = lastCachedObject;
// last object goes to the activated slot and pop
indicesByUUID[lastObject.uuid] = firstActiveIndex;
objects[firstActiveIndex] = lastObject;
objects.pop();
// accounting is done, now do the same for all bindings
for (let j = 0, m = nBindings; j !== m; ++j) {
const bindingsForPath = bindings[j],
lastCached = bindingsForPath[firstActiveIndex],
last = bindingsForPath[lastIndex];
bindingsForPath[index] = lastCached;
bindingsForPath[firstActiveIndex] = last;
bindingsForPath.pop();
}
} else {
// object is active, just swap with the last and pop
const lastIndex = --nObjects,
lastObject = objects[lastIndex];
if (lastIndex > 0) {
indicesByUUID[lastObject.uuid] = index;
}
objects[index] = lastObject;
objects.pop();
// accounting is done, now do the same for all bindings
for (let j = 0, m = nBindings; j !== m; ++j) {
const bindingsForPath = bindings[j];
bindingsForPath[index] = bindingsForPath[lastIndex];
bindingsForPath.pop();
}
} // cached or active
} // if object is known
} // for arguments
this.nCachedObjects_ = nCachedObjects;
}
// Internal interface used by befriended PropertyBinding.Composite:
subscribe_(path, parsedPath) {
// returns an array of bindings for the given path that is changed
// according to the contained objects in the group
const indicesByPath = this._bindingsIndicesByPath;
let index = indicesByPath[path];
const bindings = this._bindings;
if (index !== undefined) return bindings[index];
const paths = this._paths,
parsedPaths = this._parsedPaths,
objects = this._objects,
nObjects = objects.length,
nCachedObjects = this.nCachedObjects_,
bindingsForPath = new Array(nObjects);
index = bindings.length;
indicesByPath[path] = index;
paths.push(path);
parsedPaths.push(parsedPath);
bindings.push(bindingsForPath);
for (let i = nCachedObjects, n = objects.length; i !== n; ++i) {
const object = objects[i];
bindingsForPath[i] = new PropertyBinding(object, path, parsedPath);
}
return bindingsForPath;
}
unsubscribe_(path) {
// tells the group to forget about a property path and no longer
// update the array previously obtained with 'subscribe_'
const indicesByPath = this._bindingsIndicesByPath,
index = indicesByPath[path];
if (index !== undefined) {
const paths = this._paths,
parsedPaths = this._parsedPaths,
bindings = this._bindings,
lastBindingsIndex = bindings.length - 1,
lastBindings = bindings[lastBindingsIndex],
lastBindingsPath = path[lastBindingsIndex];
indicesByPath[lastBindingsPath] = index;
bindings[index] = lastBindings;
bindings.pop();
parsedPaths[index] = parsedPaths[lastBindingsIndex];
parsedPaths.pop();
paths[index] = paths[lastBindingsIndex];
paths.pop();
}
}
}
export { AnimationObjectGroup };