Source: objects/BaseEntity.js

import { Utils } from '../common/Utils';
import { Object3D } from './Object3D';
import { ModelAnimationComponent } from '../components/ModelAnimationComponent';

const __ = {
	private: Symbol('private'),
}

const _internalAnimationComponentName = '_internalAnimation';

const registerComponentParam = { autoRegister: false, isResident: true };

/**
 * @class BaseEntity
 * The base entity object.
 * @memberof THING
 * @extends THING.Object3D
 */
class BaseEntity extends Object3D {

	/**
	 * The base entity object that load 3D model resource in scene.
	 * @param {Object} param The initial parameters.
	 */
	constructor(param) {
		super(param);

		this._animation = this._animation || null;
	}

	// #region Private

	_playDelayAnimation() {
		// Make sure object is alive
		if (this.destroyed) {
			return;
		}

		// Get the renderable node
		let node = this.bodyNode;

		// If entity has animation then complete the delayed commands of it
		if (node.hasAnimation()) {
			this.animation.runDelayedCommands();
		}
		// If entity do not have any animations, then use the dummy animation component
		else {
			if (this._animation) {
				this.removeComponent('animation');
			}

			this._animation = ModelAnimationComponent.cDummyAnimationInterface;
		}
	}

	// #endregion

	// #region Object3D overrides

	onAfterSetup(param) {
		if (param.extras && param.extras.animInfo) {
			const animInfo = param.extras.animInfo;

			this.playAnimation(animInfo);
		}
	}

	onLoadComplete(options) {
		super.onLoadComplete(options);

		this._playDelayAnimation();
	}

	unloadResource(recursive = true) {
		this.clearInternalAnimation();

		return super.unloadResource(recursive);
	}

	clearInternalAnimation() {
		if (this._animation) {
			this._animation.stopAllAnimations();
			this._animation = null;

			this.removeComponent(_internalAnimationComponentName);
		}
	}

	// #endregion

	// #region Internal components attributes and functions

	// #region Component - animation

	get animation() {
		if (!this._animation) {
			this._animation = new ModelAnimationComponent();
			this.addComponent(this._animation, _internalAnimationComponentName, registerComponentParam);
		}

		return this._animation;
	}

	/**
	 * Get all animations info.
	 * @type {Array<AnimationResult>}
	 * @example
	 * var UinoSpaceman = new THING.Entity({url:'.assets/models/UinoSpaceman/UinoSpaceman.gltf'});
     * UinoSpaceman.waitForComplete().then(() => {
     *   let animations = UinoSpaceman.animations;
	 *   let ret = animations[0].name == 'Walk';
	 * 	 // @expect(ret == true);
     * });
	 * @public
	 */
	get animations() { return this.animation.animations; }

	/**
	 * Get the animation names.
	 * @type {Array<String>}
	 * @example
	 * var UinoSpaceman = new THING.Entity({url:'.assets/models/UinoSpaceman/UinoSpaceman.gltf'});
     * UinoSpaceman.waitForComplete().then(() => {
     *   let animations = UinoSpaceman.animationNames;
	 *   let ret = animations[0] == 'Walk';
	 * 	 // @expect(ret == true);
     * });
     * @public
	 */
	get animationNames() { return this.animation.animationNames; }

	/**
	 * @typedef {Object} PlayAnimationArgs
	 * @property {String} name The animation name.
	 * @property {Number} times? The loop times.
	 * @property {LoopType} loopType? The loop type.
	 * @property {Number} [speed=1] The playing speed.
	 * @property {Boolean} [reverse=false] True indicates to play in reverse mode.
	 * @property {Function} [onComplete=null] The callback function to receive complete event.
	 * @public
	 */

	/**
	 * Play animation, would stop all other animations.
	 * @param {PlayAnimationArgs} param The parameters.
	 * @example
	 * var UinoSpaceman = new THING.Entity({url:'.assets/models/UinoSpaceman/UinoSpaceman.gltf'});
     * UinoSpaceman.waitForComplete().then(() => {
     *   UinoSpaceman.playAnimation({ name: 'Walk', loopType: "Repeat" });
	 *   let ret = UinoSpaceman.isAnimationPlaying('Walk');
	 * 	 // @expect(ret == true);
     * });
	 * @public
	 */
	playAnimation() { return this.animation.playAnimation.apply(this.animation, arguments); }

	/**
	 * Play animation in async mode, would stop all other animations.
	 * @param {PlayAnimationArgs} param The parameters.
	 * @returns {Promise<any>}
	 * @private
	 */
	playAnimationAsync() { return this.animation.playAnimationAsync.apply(this.animation, arguments); }

	/**
	 * Blends animation, would not stop all other animations.
	 * @param {PlayAnimationArgs} param The parameters.
	 * @public
	 */
	blendAnimation() { return this.animation.blendAnimation.apply(this.animation, arguments); }

	/**
	 * Blends animation in async mode, would not stop all other animations.
	 * @param {PlayAnimationArgs} param The parameters.
	 * @returns {Promise<any>}
	 * @private
	 */
	blendAnimationAsync() { return this.animation.blendAnimationAsync.apply(this.animation, arguments); }

	/**
	 * Check whether has animation by name.
	 * @param {String} name The animation name
	 * @returns {Boolean}
	 * @example
	 * var UinoSpaceman = new THING.Entity({url:'.assets/models/UinoSpaceman/UinoSpaceman.gltf'});
     * UinoSpaceman.waitForComplete().then(() => {
	 *   let ret = UinoSpaceman.hasAnimation('Walk');
	 * 	 // @expect(ret == true);
     * });
	 * @public
	 */
	hasAnimation() { return this.animation.hasAnimation.apply(this.animation, arguments); }

	/**
	 * Check whether animation is playing.
	 * @param {String} name The animation name
	 * @returns {Boolean}
	 * @example
	 * var UinoSpaceman = new THING.Entity({url:'.assets/models/UinoSpaceman/UinoSpaceman.gltf'});
     * UinoSpaceman.waitForComplete().then(() => {
     *   UinoSpaceman.playAnimation({ name: 'Walk', loopType: "Repeat" });
	 *   let ret = UinoSpaceman.isAnimationPlaying('Walk');
	 * 	 // @expect(ret == true);
     * });
	 * @public
	 */
	isAnimationPlaying() { return this.animation.isAnimationPlaying.apply(this.animation, arguments); }

	/**
	 * Pause animation(s).
	 * @param {String|Array<String>} name The animation name or name list.
	 * @example
	 * var UinoSpaceman = new THING.Entity({url:'.assets/models/UinoSpaceman/UinoSpaceman.gltf'});
     * UinoSpaceman.waitForComplete().then(() => {
     *   UinoSpaceman.playAnimation({ name: 'Walk', loopType: "Repeat" });
	 *   let ret1 = UinoSpaceman.isAnimationPlaying('Walk');
	 *   UinoSpaceman.pauseAnimation('Walk');
	 *   let ret2 = UinoSpaceman.getAnimationState() == 'Paused';
	 * 	 // @expect(ret1 == true && ret2 == true);
     * });
	 * @public
	 */
	pauseAnimation() { return this.animation.pauseAnimation.apply(this.animation, arguments); }

	/**
	 * Pause all animations.
	 * @example
	 * var UinoSpaceman = new THING.Entity({url:'.assets/models/UinoSpaceman/UinoSpaceman.gltf'});
     * UinoSpaceman.waitForComplete().then(() => {
     *   UinoSpaceman.playAnimation({ name: 'Walk', loopType: "Repeat" });
	 *   let ret1 = UinoSpaceman.isAnimationPlaying('Walk');
	 *   UinoSpaceman.pauseAllAnimations();
	 *   let ret2 = UinoSpaceman.getAnimationState() == 'Paused';
	 * 	 // @expect(ret1 == true && ret2 == true);
     * });
	 * @public
	 */
	pauseAllAnimations() { return this.animation.pauseAllAnimations.apply(this.animation, arguments); }

	/**
	 * Resume animation(s).
	 * @param {String|Array<String>} name The animation name or name list.
	 * @example
	 * var UinoSpaceman = new THING.Entity({url:'.assets/models/UinoSpaceman/UinoSpaceman.gltf'});
     * UinoSpaceman.waitForComplete().then(() => {
     *   UinoSpaceman.playAnimation({ name: 'Walk', loopType: "Repeat" });
	 *   let ret1 = UinoSpaceman.isAnimationPlaying('Walk');
	 *   UinoSpaceman.pauseAnimation('Walk');
	 *   let ret2 = UinoSpaceman.getAnimationState() == 'Paused';
	 * 	 UinoSpaceman.resumeAnimation('Walk');
	 *   let ret3 = UinoSpaceman.isAnimationPlaying('Walk');
	 * 	 // @expect(ret1 == true && ret2 == true && ret3 == true);
     * });
	 * @public
	 */
	resumeAnimation() { return this.animation.resumeAnimation.apply(this.animation, arguments); }

	/**
	 * Resume all animations.
	 * @example
	 * var UinoSpaceman = new THING.Entity({url:'.assets/models/UinoSpaceman/UinoSpaceman.gltf'});
     * UinoSpaceman.waitForComplete().then(() => {
     *   UinoSpaceman.playAnimation({ name: 'Walk', loopType: "Repeat" });
	 *   let ret1 = UinoSpaceman.isAnimationPlaying('Walk');
	 *   UinoSpaceman.pauseAnimation('Walk');
	 *   let ret2 = UinoSpaceman.getAnimationState() == 'Paused';
	 * 	 UinoSpaceman.resumeAllAnimations();
	 *   let ret3 = UinoSpaceman.isAnimationPlaying('Walk');
	 * 	 // @expect(ret1 == true && ret2 == true && ret3 == true);
     * });
	 * @public
	 */
	resumeAllAnimations() { return this.animation.resumeAllAnimations.apply(this.animation, arguments); }

	/**
	 * Stop animation.
	 * @param {String} name The animation name.
	 * @example
	 * var UinoSpaceman = new THING.Entity({url:'.assets/models/UinoSpaceman/UinoSpaceman.gltf'});
     * UinoSpaceman.waitForComplete().then(() => {
     *   UinoSpaceman.playAnimation({ name: 'Walk', loopType: "Repeat" });
	 *   let ret1 = UinoSpaceman.isAnimationPlaying('Walk');
	 *   UinoSpaceman.stopAnimation('Walk');
	 *   let ret2 = UinoSpaceman.getAnimationState() == 'Stopped';
	 * 	 // @expect(ret1 == true && ret2 == true);
     * });
	 * @public
	 */
	stopAnimation() { return this.animation.stopAnimation.apply(this.animation, arguments); }

	/**
	 * Stop all animations.
	 * @example
	 * var UinoSpaceman = new THING.Entity({url:'.assets/models/UinoSpaceman/UinoSpaceman.gltf'});
     * UinoSpaceman.waitForComplete().then(() => {
     *   UinoSpaceman.playAnimation({ name: 'Walk', loopType: "Repeat" });
	 *   let ret1 = UinoSpaceman.isAnimationPlaying('Walk');
	 *   UinoSpaceman.stopAllAnimations();
	 *   let ret2 = UinoSpaceman.getAnimationState() == 'Stopped';
	 * 	 // @expect(ret1 == true && ret2 == true);
     * });
	 * @public
	 */
	stopAllAnimations() { return this.animation.stopAllAnimations.apply(this.animation, arguments); }

	/**
	 * @typedef {Object} AnimationResult
	 * @property {String} name The name.
	 * @property {Number} duration The duration in seconds.
	 * @property {Number} speed The playing speed.
	 * @property {PlayStateType} state The state type.
	 * @public
	 */

	/**
	 * Get animation info.
	 * @param {String} name The animation name.
	 * @returns {AnimationResult}
	 * @example
	 * var UinoSpaceman = new THING.Entity({url:'.assets/models/UinoSpaceman/UinoSpaceman.gltf'});
     * UinoSpaceman.waitForComplete().then(() => {
     *   let animation = UinoSpaceman.getAnimation('Walk');
	 *   let ret = animation.name == 'Walk';
	 * 	 // @expect(ret == true);
     * });
	 * @public
	 */
	getAnimation() { return this.animation.getAnimation.apply(this.animation, arguments); }

	/**
	 * Get animation state.
	 * @param {String} name The animation name.
	 * @returns {PlayStateType}
	 * @example
	 * var UinoSpaceman = new THING.Entity({url:'.assets/models/UinoSpaceman/UinoSpaceman.gltf'});
     * UinoSpaceman.waitForComplete().then(() => {
	 *   UinoSpaceman.playAnimation({ name: 'Walk', loopType: "Repeat" });
	 *   let state = UinoSpaceman.getAnimationState('Walk');
	 *   let ret = state == 'Playing';
	 * 	 // @expect(ret == true);
     * });
	 * @public
	 */
	getAnimationState() { return this.animation.getAnimationState.apply(this.animation, arguments); }

	/**
	 * Get playing animations.
	 * @returns {Array<AnimationResult>}
	 * @example
	 * var UinoSpaceman = new THING.Entity({url:'.assets/models/UinoSpaceman/UinoSpaceman.gltf'});
     * UinoSpaceman.waitForComplete().then(() => {
	 *   UinoSpaceman.playAnimation({ name: 'Walk', loopType: "Repeat" });
	 *   let animations = UinoSpaceman.getPlayingAnimations();
	 *   let ret = animations[0].state == 'Playing';
	 * 	 // @expect(ret == true);
     * });
	 * @public
	 */
	getPlayingAnimations() { return this.animation.getPlayingAnimations.apply(this.animation, arguments); }

	/**
	 * Get animation direction type.
	 * @param {String} name The animation name.
	 * @returns {AnimationDirectionType}
	 * @public
	 */
	getAnimationDirectionType() { return this.animation.getAnimationDirectionType.apply(this.animation, arguments); }

	/**
	 * Set animation direction type.
	 * @param {String} name The animation name.
	 * @param {AnimationDirectionType} value The direction type.
	 * @returns {Boolean}
	 * @public
	 */
	setAnimationDirectionType() { return this.animation.setAnimationDirectionType.apply(this.animation, arguments); }

	/**
	 * Get animation speed.
	 * @param {String} name The animation name.
	 * @returns {Number}
	 * @example
	 * var UinoSpaceman = new THING.Entity({url:'.assets/models/UinoSpaceman/UinoSpaceman.gltf'});
     * UinoSpaceman.waitForComplete().then(() => {
	 *   UinoSpaceman.playAnimation({ name: 'Walk', loopType: "Repeat", speed: 10 });
	 *   let speed = UinoSpaceman.getAnimationSpeed('Walk');
	 *   let ret = speed == 10;
	 * 	 // @expect(ret == true);
     * });
	 * @public
	 */
	getAnimationSpeed() { return this.animation.getAnimationSpeed.apply(this.animation, arguments); }

	/**
	 * Set animation speed.
	 * @param {String} name The animation name.
	 * @param {Number} value The speed.
	 * @returns {Boolean}
	 * @example
	 * var UinoSpaceman = new THING.Entity({url:'.assets/models/UinoSpaceman/UinoSpaceman.gltf'});
     * UinoSpaceman.waitForComplete().then(() => {
	 *   UinoSpaceman.playAnimation({ name: 'Walk', loopType: "Repeat", speed: 10 });
	 * 	 UinoSpaceman.setAnimationSpeed('Walk', 20)
	 *   let speed = UinoSpaceman.getAnimationSpeed('Walk');
	 *   let ret = speed == 20;
	 * 	 // @expect(ret == true);
     * });
	 * @public
	 */
	setAnimationSpeed() { return this.animation.setAnimationSpeed.apply(this.animation, arguments); }

	// #endregion

	/**
	 * Promote node as child object.
	 * @param {String} name The node name.
	 * @param {THING.Object3D} parent The parent object, if it's null then indicates use current object as parent.
	 * @returns {THING.BaseEntity}
	 * @example
	 * var UinoSpaceman = new THING.Entity({url:'.assets/models/UinoSpaceman/UinoSpaceman.gltf'});
     * UinoSpaceman.waitForComplete().then(() => {
	 *   let obj = UinoSpaceman.promoteNode('pasted__head');
	 * 	 let ret1 = obj instanceof THING.BaseEntity;
	 *   let ret2 = obj.name == 'pasted__head';
	 * 	 // @expect(ret1 == true && ret2 == true);
     * });
	 */
	promoteNode(name, parent) {
		if (!name) {
			return null;
		}

		// We can not promote node when it's in instanced drawing mode
		if (this.isInstancedDrawing) {
			this.makeInstancedDrawing(false);
		}

		// Create root node to let sub node attach it
		this.body.createRootNode();

		// Get the parent
		parent = parent || this;

		// Remove some components, due to we can not work it together
		this.removeComponent('animation');

		// Promote node in body object
		let node = this.body.promoteNode(name, parent.node);
		if (!node) {
			Utils.error(`Promote '${name}' node failed, due to node is not exist`);
			return null;
		}

		// Create object
		let object = new BaseEntity({
			name,
			renderableNode: node,
			parent,
		});

		return object;
	}

	/**
	 * Check whether it's BaseEntity type or inherit from it.
	 * @type {Boolean}
	 * @example
	 * var UinoSpaceman = new THING.Entity({url:'.assets/models/UinoSpaceman/UinoSpaceman.gltf'});
	 * let ret = UinoSpaceman.isBaseEntity;
	 * // @expect(ret == true);
	 */
	get isBaseEntity() {
		return true;
	}

}

export { BaseEntity }