Source: components/TransformComponent.js

import { Utils } from '../common/Utils';
import { MathUtils } from '../math/MathUtils';
import { BaseComponent } from './BaseComponent';
import { AxisType } from '../const';

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

const cLookAtTargetEventTag = '__cLookAtTargetEventTag__';
const cKeepSizeEventTag = '__cKeepSizeEventTag__';
const cKeepSizeEventPriority = -10000000; // Make it update at last

let _defaultParams = {};
let _vector3 = MathUtils.createVec3();
let _position = MathUtils.createVec3();
let _mat4 = MathUtils.createMat4();
let _quat = MathUtils.createQuat();
let _originMat4 = MathUtils.createMat4();
let _axis = [0, 0, 0];
let _x_axis = [1, 0, 0];
let _y_axis = [0, 1, 0];
let _z_axis = [0, 0, 1];

// #region Private Functions

// Get the radian for looking at target.
function _getRadian(x, y) {
	return Math.atan2(-y, -x) + Math.PI / 2;
}

// Set world position only (keep all children world position).
function _setWorldPositionOnly(node, position) {
	// Keep all children world position after set node position
	let children = node.getChildren().filter(child => {
		if (child.getUserData('isDebugNode')) {
			return false;
		}

		return true;
	});

	let childPosition = [0, 0, 0];
	let positions = children.map(child => {
		return child.getWorldPosition(childPosition).slice(0);
	});

	node.setWorldPosition(position);

	// Start to resume children world position
	for (let i = 0; i < children.length; i++) {
		children[i].setWorldPosition(positions[i]);
	}
}

// #endregion

/**
 * @class TransformComponent
 * The transform component.
 * @memberof THING
 * @extends THING.BaseComponent
 * @public
 */
class TransformComponent extends BaseComponent {

	static mustCopyWithInstance = true;

	/**
	 * The transform component of object.
	 */
	constructor() {
		super();

		this[__.private] = {};
		let _private = this[__.private];

		_private.lookAtTargetInfo = null;

		_private.keepSizeInfo = null;
		_private.keepSizeDistance = null;

		_private.onChange = null;
	}

	// #region Private

	_lookAtPosition(position, up) {
		let _private = this[__.private];

		let info = _private.lookAtTargetInfo;

		let node = this.object.node;
		node.lookAt(position, up);

		this.onLookAt(position, up);

		// Notify outside
		let update = info.update;
		if (update) {
			update();
		}
	}

	_lookAtTarget() {
		let _private = this[__.private];

		let info = _private.lookAtTargetInfo;

		// Get the target
		let target = info.target;

		// Get the object
		let object = this.object;
		let node = object.node;

		// Get the look point
		let lookPoint;
		if (info.lookOnPlane) {
			lookPoint = MathUtils.getPositionOnPlane(object.position, target.position, target.forward);
		}
		else {
			lookPoint = target.position;
		}

		// Get the lock axis
		let lockAxis = info.lockAxis;
		if (lockAxis) {
			lookPoint = object.worldToLocal(lookPoint);

			node.getPosition(_position);

			if (lockAxis == AxisType.X) {
				let y = lookPoint[1] - _position[1];
				let z = lookPoint[2] - _position[2];
				let radian = _getRadian(y, z);
				MathUtils.getQuatFromAxisRadian(_x_axis, -radian, _quat);

				node.setQuaternion(_quat);
			}
			else if (lockAxis == AxisType.Y) {
				let x = lookPoint[0] - _position[0];
				let z = lookPoint[2] - _position[2];
				let radian = _getRadian(x, z);
				MathUtils.getQuatFromAxisRadian(_y_axis, -radian, _quat);

				node.setQuaternion(_quat);
			}
			else if (lockAxis == AxisType.Z) {
				let x = lookPoint[0] - _position[0];
				let y = lookPoint[1] - _position[1];
				let radian = _getRadian(x, y);
				MathUtils.getQuatFromAxisRadian(_z_axis, -radian, _quat);

				node.setQuaternion(_quat);
			}
		}
		else {
			let up = info.useTargetUpDirection ? target.up : info.up;
			this._lookAtPosition(lookPoint, up);
		}

		// Notify outside
		let update = info.update;
		if (update) {
			update();
		}
	}

	_updateKeepSize() {
		let _private = this[__.private];

		let object = this.object;

		let keepSizeInfo = _private.keepSizeInfo;

		let position = object.position;
		MathUtils.vec3.transformMat4(_position, position, keepSizeInfo.camera.inversedMatrixWorld);
		let length = Math.abs(_position[2]);
		if (length) {
			let scaleFactor = keepSizeInfo.scaleFactor;
			object.localScale = [length / scaleFactor[0], length / scaleFactor[1], length / scaleFactor[2]];
		}
	}

	_getDistanceToCameraInKeepSizeMode(camera) {
		let _private = this[__.private];

		let keepSizeDistance = _private.keepSizeDistance;
		if (keepSizeDistance) {
			return keepSizeDistance;
		}
		else {
			let distance = MathUtils.getDistance(this.object.position, camera.position);
			return distance;
		}
	}

	// #endregion

	// #region BaseComponent Interface

	onRemove() {
		let _private = this[__.private];

		_private.lookAtTargetInfo = null;
		_private.keepSizeInfo = null;

		_private.onChange = null;

		super.onRemove();
	}

	// #endregion

	// #region Overrides

	onNotifyChange() {
		let _private = this[__.private];

		if (_private.onChange) {
			_private.onChange();
		}
	}

	onLookAt(position, up) {

	}

	// #endregion

	localToSelf(position, ignoreScale = false, target = []) {
		let worldPosition = this.localToWorld(position, ignoreScale, target);

		return this.worldToSelf(worldPosition, ignoreScale, target);
	}

	selfToLocal(position, ignoreScale = false, target = []) {
		let worldPosition = this.selfToWorld(position, ignoreScale, target);

		return this.worldToLocal(worldPosition, ignoreScale, target);
	}

	worldToSelf(position, ignoreScale = false, target = []) {
		return this.object.node.worldToSelf(target, position, ignoreScale);
	}

	selfToWorld(position, ignoreScale = false, target = []) {
		return this.object.node.selfToWorld(target, position, ignoreScale);
	}

	localToWorld(position, ignoreScale = false, target = []) {
		let parent = this.object.parent;
		if (parent) {
			return parent.selfToWorld(position, ignoreScale, target);
		}
		else {
			return position;
		}
	}

	worldToLocal(position, ignoreScale = false, target = []) {
		let parent = this.object.parent;
		if (parent) {
			return parent.worldToSelf(position, ignoreScale, target);
		}
		else {
			return position;
		}
	}

	getLocalPosition(target = [0, 0, 0]) {
		return this.object.node.getPosition(target);
	}

	/**
	 * Set local position.
	 * @param {Array<Number>} position The local position.
	 * @private
	 */
	setLocalPosition(position) {
		// Update local position
		this.object.node.setPosition(position);

		// Notify transform changed
		this.onNotifyChange();
	}

	/**
	 * Get angles of the inertial space.
	 * @returns {Array<Number>}
	 * @private
	 */
	getLocalAngles() {
		let angles = [];
		this.object.node.getEuler(angles);

		return [angles[0], angles[1], angles[2]];
	}

	/**
	 * Set angles of the inertial space.
	 * @param {Array<Number>} angles The angles.
	 * @private
	 */
	setLocalAngles(angles) {
		// Update local quaternion
		this.object.node.setEuler(angles);

		// Notify transform changed
		this.onNotifyChange();
	}

	getLocalQuaternion(target = [0, 0, 0, 1]) {
		return this.object.node.getQuaternion(target);
	}

	/**
	 * Set quaternion of the inertial space.
	 * @param {Array<Number>} quat The angles.
	 * @private
	 */
	setLocalQuaternion(quat) {
		// Update local quaternion
		this.object.node.setQuaternion(quat);

		// Notify transform changed
		this.onNotifyChange();
	}

	getLocalScale(target = [0, 0, 0]) {
		return this.object.node.getScale(target);
	}

	/**
	 * Set scale of the self coordinate system.
	 * @param {Array<Number>} value The scale factor.
	 * @private
	 */
	setLocalScale(value) {
		// Update local scale
		let node = this.object.node;
		node.setScale(value);

		// Notify transform changed
		this.onNotifyChange();
	}

	/**
	 * Get local matrix.
	 * @returns {Array<Number>}
	 * @private
	 */
	getMatrix() {
		return this.object.node.getMatrix(_mat4);
	}

	/**
	 * Set local matrix.
	 * @param {Array<Number>} value The matrix value.
	 * @private
	 */
	setMatrix(value) {
		// Update matrix
		let node = this.object.node;
		node.setMatrix(value);

		// Notify transform changed
		this.onNotifyChange();
	}

	getWorldPosition(target = [0, 0, 0]) {
		return this.object.node.getWorldPosition(target);
	}

	/**
	 * Set world position.
	 * @param {Array<Number>} position The world position.
	 * @private
	 */
	setWorldPosition(position) {
		// Update world position
		this.object.node.setWorldPosition(position);

		// Notify transform changed
		this.onNotifyChange();
	}

	/**
	 * Get angles of the world space.
	 * @returns {Array<Number>}
	 * @private
	 */
	getWorldAngles() {
		let angles = [];
		this.object.node.getWorldEuler(angles);

		return [angles[0], angles[1], angles[2]];
	}

	/**
	 * Set angles of the world space.
	 * @param {Array<Number>} angles The angles.
	 * @private
	 */
	setWorldAngles(angles) {
		// Update world angels
		this.object.node.setWorldEuler(angles);

		// Notify transform changed
		this.onNotifyChange();
	}

	getWorldQuaternion(target = [0, 0, 0, 1]) {
		return this.object.node.getWorldQuaternion(target);
	}

	/**
	 * Set quaternion of the world space.
	 * @param {Array<Number>} quat The angles.
	 * @private
	 */
	setWorldQuaternion(quat) {
		// Update world quaternion
		this.object.node.setWorldQuaternion(quat);

		// Notify transform changed
		this.onNotifyChange();
	}

	getWorldScale(target = [0, 0, 0]) {
		return this.object.node.getWorldScale(target);
	}

	/**
	 * Set scale of the world coordinate system.
	 * @param {Array<Number>} value The scale factor.
	 * @private
	 */
	setWorldScale(value) {
		// Update world scale
		let node = this.object.node;
		node.setWorldScale(value);

		// Notify transform changed
		this.onNotifyChange();
	}

	/**
	 * Get matrix world.
	 * @param {Array<Number>} target The target to store value.
	 * @param {Boolean} [updateMatrix=false] True indicates update all parents matrix world.
	 * @returns {Array<Number>} The reference of target.
	 * @private
	 */
	getMatrixWorld(target = [], updateMatrix = false) {
		return this.object.node.getMatrixWorld(target, updateMatrix);
	}

	/**
	 * Set matrix world.
	 * @param {Array<Number>} value The matrix value.
	 * @param {Boolean} [updateMatrix=true] True indicates update all parents matrix world.
	 * @private
	 */
	setMatrixWorld(value, updateMatrix = true) {
		// Update matrix world
		let node = this.object.node;
		node.setMatrixWorld(value, updateMatrix);

		// Notify transform changed
		this.onNotifyChange();
	}

	updateWorldMatrix(updateParents, updateChildren) {
		this.object.node.updateWorldMatrix(updateParents, updateChildren);
	}

	updateMatrixWorld() {
		// parent
		this.object.node.updateMatrixWorld();
	}

	translateOnAxis(axis, distance) {
		MathUtils.vec3.normalize(_axis, axis);
		MathUtils.vec3.transformQuat(_vector3, _axis, this.getLocalQuaternion());
		MathUtils.vec3.scale(_vector3, _vector3, distance);

		let position = this.getLocalPosition();
		this.setLocalPosition(MathUtils.addVector(position, _vector3));
	}

	translateX(distance) {
		this.translateOnAxis(Utils.xAxis, distance);
	}

	translateY(distance) {
		this.translateOnAxis(Utils.yAxis, distance);
	}

	translateZ(distance) {
		this.translateOnAxis(Utils.zAxis, distance);
	}

	translate(offset) {
		this.translateX(offset[0]);
		this.translateY(offset[1]);
		this.translateZ(offset[2]);
	}

	rotateOnAxis(axis, angle) {
		axis = MathUtils.normalizeVector(axis, [0, 0, 0]);
		MathUtils.quat.setAxisAngle(_quat, axis, MathUtils.degToRad(angle));

		let localQuat = this.getLocalQuaternion();
		MathUtils.quat.multiply(localQuat, localQuat, _quat);
		this.setLocalQuaternion(localQuat);
	}

	rotateX(angle) {
		this.rotateOnAxis(Utils.xAxis, angle);
	}

	rotateY(angle) {
		this.rotateOnAxis(Utils.yAxis, angle);
	}

	rotateZ(angle) {
		this.rotateOnAxis(Utils.zAxis, angle);
	}

	/**
	 * Get the up direction of world space.
	 * @returns {Array<Number>}
	 * @private
	 */
	getUpDirection() {
		this.object.node.getMatrixWorld(_mat4);
		return MathUtils.normalizeVector([_mat4[4], _mat4[5], _mat4[6]]);
	}

	/**
	 * Get the forward direction in world space.
	 * @returns {Array<Number>}
	 * @private
	 */
	getForward(target = [0, 0, 0]) {
		return this.object.node.getForward(target);
	}

	/**
	 * Get the cross direction in world space.
	 * @returns {Array<Number>}
	 * @private
	 */
	getCross() {
		let up = this.getUpDirection();
		let forward = this.getForward();

		let target = [0, 0, 0];
		MathUtils.vec3.cross(target, up, forward);
		MathUtils.vec3.normalize(target, target);

		return target;
	}

	/**
	 * Get the direction to target position.
	 * @param {Array<Number>} target The target position.
	 * @returns {Array<Number>}
	 * @private
	 */
	getDirectionToTarget(target) {
		if (!target) {
			return null;
		}

		let direction = [0, 0, 0];
		MathUtils.vec3.sub(direction, target, this.object.position);
		MathUtils.vec3.normalize(direction, direction);

		return direction;
	}

	getSelfQuaternionFromTarget(target) {
		let targetPosition = this.worldToSelf(target);
		if (MathUtils.exactEqualsVector3([0, 0, 0], targetPosition)) {
			return [0, 0, 0, 1];
		}

		let mat = MathUtils.lookAt(targetPosition, [0, 0, 0], [0, 1, 0]);
		MathUtils.mat4.multiply(_mat4, this.matrix, mat);

		return MathUtils.getQuatFromMat4(_mat4);
	}

	getSelfAnglesFromTarget(target) {
		return MathUtils.getAnglesFromQuat(this.getSelfQuaternionFromTarget(target));
	}

	getWorldQuaternionFromTarget(target) {
		if (!target || !Utils.isArray(target)) {
			return [0, 0, 0, 1];
		}

		let object = this.object;
		let position = object.position;
		if (MathUtils.equalsVector3(target, position)) {
			return object.quaternion;
		}

		return MathUtils.getQuatFromTarget(position, target, [0, 1, 0]);
	}

	getWorldAnglesFromTarget(target) {
		return MathUtils.getAnglesFromQuat(this.getWorldQuaternionFromTarget(target));
	}

	getWorldPositionFromSelfAngles(value, distance) {
		let quat = MathUtils.getQuatFromAngles(value);
		MathUtils.vec3.transformQuat(_vector3, [0, 0, 1], quat);
		MathUtils.vec3.scale(_vector3, _vector3, distance);

		let position = MathUtils.vec3.add(_vector3, this.position, _vector3);
		return position;
	}

	getLocalMatrix(target, updateMatrix = false) {
		if (!target || !target.isObject3D) {
			return _originMat4;
		}

		var inversedMatrixWorld = MathUtils.mat4.invert([], target.getMatrixWorld([], updateMatrix));
		var selfMatrixWorld = this.getMatrixWorld([], updateMatrix);

		var localMatrix = MathUtils.mat4.multiply([], inversedMatrixWorld, selfMatrixWorld);
		return localMatrix;
	}

	lookAt(target, param = _defaultParams) {
		let _private = this[__.private];

		// Start to look at target
		if (target) {
			if (!target.isBaseObject && !Utils.isArray(target)) {
				Utils.error(`Look at target failed, due to target is not a BaseObject or position`);
				return;
			}

			// Set the default values
			let defaultValues = {
				lookOnPlane: false
			};

			// Parse arguments
			let up = Utils.parseValue(param['up'], [0, 1, 0]);
			let lockAxis = param['lockAxis'];
			let always = Utils.parseValue(param['always'], false);
			let useTargetUpDirection = Utils.parseValue(param['useTargetUpDirection'], false);
			let lookOnPlane = Utils.parseValue(param['lookOnPlane'], defaultValues['lookOnPlane']);
			let update = param['update'];

			// Update info
			_private.lookAtTargetInfo = {
				up: up.slice(0),
				target,
				lockAxis,
				always,
				useTargetUpDirection,
				lookOnPlane,
				update,
				owner: this,
				object: this.object
			};

			// Look at target directly
			if (target.isBaseObject) {
				this._lookAtTarget();

				// Continue to look
				if (always) {
					this.object.on('update', () => {
						this._lookAtTarget();
					}, cLookAtTargetEventTag);
				}
			}
			// Look at position
			else {
				this._lookAtPosition(target, up);

				// Continue to look
				if (always) {
					this.object.on('update', () => {
						this._lookAtPosition(target, up);
					}, cLookAtTargetEventTag);
				}
			}
		}
		// Stop to look
		else {
			_private.lookAtTargetInfo = null;

			this.object.off('update', cLookAtTargetEventTag);
		}
	}

	/**
	 * Keep object's size in screen(auto adjust object's scale).
	 * @param {Boolean} value True indicates keep size.
	 * @param {THING.Camera} camera The camera.
	 * @private
	 */
	keepSize(value, camera) {
		let _private = this[__.private];

		let object = this.object;

		if (value) {
			if (!_private.keepSizeInfo) {
				let distanceToCamera = this._getDistanceToCameraInKeepSizeMode(camera);
				let initialLocalScale = object.localScale;

				_private.keepSizeInfo = {
					camera,
					initialLocalScale,
					scaleFactor: [distanceToCamera / initialLocalScale[0], distanceToCamera / initialLocalScale[1], distanceToCamera / initialLocalScale[2]],
					refresh: () => {
						let distanceToCamera = this._getDistanceToCameraInKeepSizeMode(camera);

						_private.keepSizeInfo.scaleFactor = [distanceToCamera / initialLocalScale[0], distanceToCamera / initialLocalScale[1], distanceToCamera / initialLocalScale[2]];

						this._updateKeepSize();
					}
				};

				object.on('update', () => {
					this._updateKeepSize();
				}, cKeepSizeEventTag, cKeepSizeEventPriority);

				this._updateKeepSize();
			}
		}
		else {
			// Clear previous keep size
			if (_private.keepSizeInfo) {
				object.localScale = _private.keepSizeInfo.initialLocalScale;

				_private.keepSizeInfo = null;

				object.off('update', cKeepSizeEventTag);
			}
		}
	}

	/**
	 * Get pivot from oriented box.
	 * @returns {Array<Number>}
	 * @private
	 */
	getPivot() {
		const basePivot = [0.5, 0.5, 0.5];

		let object = this.object;
		if (object.hasComponent('bounding')) {
			let orientedBox = this.object.getOBB(false);
			let center = orientedBox.center;
			let size = MathUtils.fixScaleFactor(orientedBox.size);

			let offset = this.worldToSelf(center);
			let offsetInScale = MathUtils.divideVector(offset, size);
			let pivot = MathUtils.subVector(basePivot, offsetInScale);

			return pivot;
		}
		else {
			// We must return new pivot here to prevent user modify it outside
			return [0.5, 0.5, 0.5];
		}
	}

	/**
	 * Set pivot from oriented box.
	 * @param {Array<Number>} value The pivot value.
	 * @param {Array<Number>} basePivot The base pivot value.
	 * @private
	 */
	setPivot(value, basePivot) {
		const object = this.object;

		let orientedBox = object.getOBB(false);
		let scale = MathUtils.scaleVector(orientedBox.halfSize, 2);

		let offsetPivot = MathUtils.subVector(value, basePivot);
		let selfPosition = MathUtils.scaleVector(offsetPivot, scale);
		let position = object.selfToWorld(MathUtils.negVector(selfPosition), true);

		this.setPivotWorldPosition(position);
	}

	/**
	 * Set pivot by world position.
	 * @param {Array<Number>} position The world position.
	 * @private
	 */
	setPivotWorldPosition(position) {
		// Get object
		let object = this.object;
		let body = object.body;

		// Pivot node must works with root node
		body.createRootNode();

		// Create pivot node and backup its position
		let pivotNode = body.createPivotNode();

		// Update object position but without changing children location
		_setWorldPositionOnly(pivotNode, position);

		// Notify transform changed
		this.onNotifyChange();
	}

	/**
	 * Clear pivot node.
	 * @private
	 */
	clearPivot() {
		let object = this.object;

		object.body.clearPivotNode();
	}

	bindSubNode(target, subNodeName) {
		if (!target) {
			return false;
		}

		this.unbindSubNode();

		// Get the sub node object
		let subNodeObject = target.body.getNodeByName(subNodeName);
		if (!subNodeObject) {
			return false;
		}

		// Prepare to bind sub node
		let subNode = subNodeObject.node;
		if (!subNode) {
			return false;
		}

		// Keep the world matrix when start to bind
		MathUtils.mat4.invert(_mat4, subNodeObject.matrixWorld);
		MathUtils.mat4.multiply(_mat4, _mat4, this.object.body.matrixWorld);

		// Bind sub node
		if (!this.object.bodyNode.bindSubNode(subNode, _mat4, true)) {
			return false;
		}

		return true;
	}

	/**
	 * Unbind sub node.
	 * @private
	 */
	unbindSubNode() {
		this.object.bodyNode.unbindSubNode();
	}

	// #region Accessor

	get lookAtTargetInfo() {
		let _private = this[__.private];

		return _private.lookAtTargetInfo;
	}

	get keepSizeInfo() {
		let _private = this[__.private];

		return _private.keepSizeInfo;
	}

	get localPosition() {
		return this.getLocalPosition();
	}
	set localPosition(value) {
		this.setLocalPosition(value);
	}

	get localAngles() {
		return this.getLocalAngles();
	}
	set localAngles(value) {
		this.setLocalAngles(value);
	}

	get localQuaternion() {
		return this.getLocalQuaternion();
	}
	set localQuaternion(value) {
		this.setLocalQuaternion(value);
	}

	get localScale() {
		return this.getLocalScale();
	}
	set localScale(value) {
		this.setLocalScale(value);
	}

	get position() {
		return this.getWorldPosition();
	}
	set position(value) {
		this.setWorldPosition(value);
	}

	get angles() {
		return this.getWorldAngles();
	}
	set angles(value) {
		this.setWorldAngles(value);
	}

	get quaternion() {
		return this.getWorldQuaternion();
	}
	set quaternion(value) {
		this.setWorldQuaternion(value);
	}

	get scale() {
		return this.getWorldScale();
	}
	set scale(value) {
		this.setWorldScale(value);
	}

	get matrix() {
		return this.getMatrix();
	}
	set matrix(value) {
		this.setMatrix(value);
	}

	get matrixWorld() {
		return this.getMatrixWorld();
	}
	set matrixWorld(value) {
		this.setMatrixWorld(value);
	}

	get inversedMatrix() {
		return MathUtils.mat4.invert(_mat4, this.matrix);
	}

	get inversedMatrixWorld() {
		return MathUtils.mat4.invert(_mat4, this.matrixWorld);
	}

	get up() {
		return this.getUpDirection();
	}

	get forward() {
		return this.getForward();
	}

	get cross() {
		return this.getCross();
	}

	/**
	 * Get/Set the distance of keep size mode.
	 * @type {Number}
	 * @example
	 * let component = new THING.TransformComponent();
	 * component.keepSizeDistance = 10;
	 * // @expect(component.keepSizeDistance == 10);
	 * @public
	 */
	get keepSizeDistance() {
		let _private = this[__.private];

		return _private.keepSizeDistance;
	}
	set keepSizeDistance(value) {
		let _private = this[__.private];

		_private.keepSizeDistance = value;

		// Refresh keep size
		let keepSizeInfo = _private.keepSizeInfo;
		if (keepSizeInfo) {
			keepSizeInfo.refresh();
		}
	}

	get onChange() {
		let _private = this[__.private];

		return _private.onChange;
	}
	set onChange(value) {
		let _private = this[__.private];

		_private.onChange = value;
	}

	// #endregion

}

export { TransformComponent }