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 }