import { PropertyBinding } from './PropertyBinding.js'; import * as MathUtils 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 { constructor() { this.uuid = MathUtils.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; } }; } 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; } 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; } // remove & forget 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(); } } } AnimationObjectGroup.prototype.isAnimationObjectGroup = true; export { AnimationObjectGroup };