Repository

js/Mobilizing/renderer/3D/three/scene/Transform.js

import { Raycaster, Object3D } from 'three';

import Mesh from '../shape/Mesh';
import Quaternion from '../types/Quaternion';
import Matrix4 from '../types/Matrix4';
import Vector3 from '../types/Vector3';
import Vector2 from '../types/Vector2';
import * as _Math from '../../../../core/util/Math';

export default class Transform {
    /**
    Transform class is meant to be aggregated to any object that needs to be transformed in space.
    {@link Mesh}, {@link Light} or {@link Camera} contains a transform instance.

    @class Transform
    @constructor
    @uses {Math}
    */
    constructor(obj) {

        this.mobObject = obj;//used in children array to avoid confusion between three and mob objects

        if (obj.getNativeObject()) {
            this.nativeObject = obj.getNativeObject();
            //add a custom prop to three meshes
            this.nativeObject.mobObject = obj;
        }
        else {
            console.error("No object was assigned to this Transform, aborting.")
            return;
        }

        this.children = [];
        this.recursiveChildren = [];
    }

    /**
    * @returns the Three.js native object used in this class
    */
    getNativeObject() {
        return this.nativeObject;
    }

    /**
    * Geneates a transform to point at a certain coordinates, makes the object "look at" this point.
    * @param {Vector} vector
    */
    lookAt(vector) {
        this.nativeObject.lookAt(vector);
    }

    /**
    Set the object rotation with a quarternion {x,y,z,w}.
    @param {Quaternion} quarternion The quaternion to apply
    */
    setLocalQuaternion(q) {
        if (q instanceof Quaternion) {
            this.nativeObject.quaternion.set(q.x, q.y, q.z, q.w);
        }
    }

    /**
    Get the object rotation as a quarternion.
    @return {Quaternion} quarternion
    */
    getLocalQuaternion() {
        const temp = new Quaternion(this.nativeObject.quaternion.x, this.nativeObject.quaternion.y, this.nativeObject.quaternion.z, this.nativeObject.quaternion.w);
        return temp;
    }

    setUpDirection(vector){
        this.nativeObject.up.set(vector.x, vector.y, vector.z);
    }

    /**
    * Get the 3 direction vectors of this transform
    * @return {Object} object with all 3 Vector3 as {upVector: upVector, forwardVector: forwardVector,leftVector: leftVector};
    */
    getDirections() {

        const quaternion = this.nativeObject.quaternion;

        const upVector = new Vector3();
        upVector.x = 2 * (quaternion.x * quaternion.y - quaternion.w * quaternion.z);
        upVector.y = 1 - 2 * (quaternion.x * quaternion.x + quaternion.z * quaternion.z);
        upVector.z = 2 * (quaternion.y * quaternion.z + quaternion.w * quaternion.x);

        const forwardVector = new Vector3();
        forwardVector.x = 2 * (quaternion.x * quaternion.z + quaternion.w * quaternion.y);
        forwardVector.y = 2 * (quaternion.y * quaternion.z - quaternion.w * quaternion.x);
        forwardVector.z = 1 - 2 * (quaternion.x * quaternion.x + quaternion.y * quaternion.y);

        const leftVector = new Vector3();
        leftVector.x = 1 - 2 * (quaternion.y * quaternion.y + quaternion.z * quaternion.z);
        leftVector.y = 2 * (quaternion.x * quaternion.y + quaternion.w * quaternion.z);
        leftVector.z = 2 * (quaternion.x * quaternion.z - quaternion.w * quaternion.y);

        const result = {
            upVector,
            forwardVector,
            leftVector
        };

        return result;
    }

    setLocalMatrix(m) {
        this.nativeObject.matrixAutoUpdate = false;

        const mat = new Matrix4().multiplyMatrices(this.nativeObject.matrix, m);
        this.nativeObject.applyMatrix(mat);
        this.nativeObject.updateMatrix();
    }

    //not documented - use setRotation
    setLocalEulerAngles(arg1, arg2, arg3) {
        //x,y,z float
        if (arguments.length === 3) {
            this.nativeObject.rotation.x = _Math.degToRad(arg1);
            this.nativeObject.rotation.y = _Math.degToRad(arg2);
            this.nativeObject.rotation.z = _Math.degToRad(arg3);

        }
        else if (arguments.length === 2) {
            if (typeof arg1 === "number" && typeof arg2 === "number") {
                this.nativeObject.rotation.x = _Math.degToRad(arg1);
                this.nativeObject.rotation.y = _Math.degToRad(arg2);
            }

        }
        //direct vector or single float
        else if (arguments.length === 1) {
            if (arg1 instanceof Vector3) {
                this.nativeObject.rotation.x = _Math.degToRad(arg1.x);
                this.nativeObject.rotation.y = _Math.degToRad(arg1.y);
                this.nativeObject.rotation.z = _Math.degToRad(arg1.z);
            }
            if (arg1 instanceof Vector2) {
                this.nativeObject.rotation.x = _Math.degToRad(arg1.x);
                this.nativeObject.rotation.y = _Math.degToRad(arg1.y);
            }
            else if (typeof arg1 === "number") {
                this.nativeObject.rotation.z = _Math.degToRad(arg1);
            }
        }
    }

    //not documented - use getRotation
    getLocalEulerAngles() {
        return new Vector3(_Math.radToDeg(this.nativeObject.rotation.x) % 360, _Math.radToDeg(this.nativeObject.rotation.y) % 360, _Math.radToDeg(this.nativeObject.rotation.z) % 360);
    }

    /**
    * Defines the oreder of application for rotation axis.
    * Default is 'XYZ', others are'YZX', 'ZXY', 'XZY', 'YXZ' and 'ZYX'
    * @param {string} order one of 'XYZ', 'YZX', 'ZXY', 'XZY', 'YXZ' and 'ZYX'
    */
    setLocalRotationOrder(val) {
        this.nativeObject.rotation.order = val;
    }

    /**
    Set the euler angles rotation in degrees.
    Polymorphic : can take various agruments of various types. Possible arguments number is 1, 2 or 3.

    @param {float|Vector3|Vector2} number|Vector3|Vector2 Value for the new rotation of the transform. If a Vector3 is given, its x, y, z will be used for the rotation x, y, z. If a Vector2 is given, its x, y will be used for the rotation x and y, but z will be unchanged. If a number is given, it will be the position x.
    @param {float} Number Value for the new y rotation of the transform.
    @param {float} Number Value for the new z rotation of the transform.
    */
    setLocalRotation() {
        this.setLocalEulerAngles.apply(this, arguments);
    }

    /**
    * Sets this transform's x rotation
    * @param {Number} arg
    */
    setLocalRotationX(arg) {
        if (typeof arg === "number") {
            this.nativeObject.rotation.x = _Math.degToRad(arg);
        }
    }

    /**
    * Sets this transform's y rotation
    * @param {Number} arg
    */
    setLocalRotationY(arg) {
        if (typeof arg === "number") {
            this.nativeObject.rotation.y = _Math.degToRad(arg);
        }
    }

    /**
    * Sets this transform's z rotation
    * @param {Number} arg
    */
    setLocalRotationZ(arg) {
        if (typeof arg === "number") {
            this.nativeObject.rotation.z = _Math.degToRad(arg);
        }
    }

    /**
    * Get the current local rotation of this transform
    * @return {Vector3} localRotation vector
    */
    getLocalRotation() {
        return this.getLocalEulerAngles();
    }

    /**
    * Get the current x local rotation of this transform
    * @return {Number} localRotation x value
    */
    getLocalRotationX() {
        return this.getLocalEulerAngles().x;
    }

    /**
    * Get the current y local rotation of this transform
    * @return {Number} localRotation y value
    */
    getLocalRotationY() {
        return this.getLocalEulerAngles().y;
    }

    /**
    * Get the current z local rotation of this transform
    * @return {Number} localRotation z value
    */
    getLocalRotationZ() {
        return this.getLocalEulerAngles().z;
    }

    /**
    Set the local position of the transform (and to the object attach to it).
    Polymorphic : can take various agruments of various types. Possible arguments number is 1, 2 or 3.

    @param {float|Vector3|Vector2} number|Vector3|Vector2 Value for the new position of the transform. If a Vector3 is given, its x, y, z will be used for the position x, y, z. If a Vector2 is given, its x, y will be used for the position x and y, but z will be unchanged. If a number is given, it will be the position x.
    @param {float} Number Value for the new y position of the transform.
    @param {float} Number Value for the new z position of the transform.
    */
    setLocalPosition(arg1, arg2, arg3) {
        //x,y,z float
        if (arguments.length === 3) {
            this.nativeObject.position.x = arg1;
            this.nativeObject.position.y = arg2;
            this.nativeObject.position.z = arg3;
        }
        else if (arguments.length === 2) {
            if (typeof arg1 === "number" && typeof arg2 === "number") {
                this.nativeObject.position.x = arg1;
                this.nativeObject.position.y = arg2;
            }
        }
        //direct vector
        else if (arguments.length === 1) {
            if (arg1 instanceof Vector3) {
                this.nativeObject.position.x = arg1.x;
                this.nativeObject.position.y = arg1.y;
                this.nativeObject.position.z = arg1.z;
            }
            else if (arg1 instanceof Vector2) {
                this.nativeObject.position.x = arg1.x;
                this.nativeObject.position.y = arg1.y;
            }
            else if (typeof arg1 === "number") {
                this.nativeObject.position.x = arg1;
                this.nativeObject.position.y = arg1;
                this.nativeObject.position.z = arg1;
            }
        }
    }

    /**
    * Sets this transform's x position
    * @param {Number} arg
    */
    setLocalPositionX(arg) {
        if (typeof arg === "number") {
            this.nativeObject.position.x = arg;
        }
    }

    /**
    * Sets this transform's y position
    * @param {Number} arg
    */
    setLocalPositionY(arg) {
        if (typeof arg === "number") {
            this.nativeObject.position.y = arg;
        }
    }

    /**
    * Sets this transform's z position
    * @param {Number} arg
    */
    setLocalPositionZ(arg) {
        if (typeof arg === "number") {
            this.nativeObject.position.z = arg;
        }
    }

    /**
    Gets the local position of this transform
    @return {Vector3} localPosition vector
    */
    getLocalPosition() {
        const tempVec = new Vector3(this.nativeObject.position.x, this.nativeObject.position.y, this.nativeObject.position.z);
        return tempVec;
    }

    /**
    Gets the local x position of this transform
    @return {Number} localPosition x
    */
    getLocalPositionX() {
        const tempVec = this.nativeObject.position.x;
        return tempVec;
    }

    /**
    Gets the local x position of this transform
    @return {Number} localPosition y
    */
    getLocalPositionY() {
        const tempVec = this.nativeObject.position.y;
        return tempVec;
    }

    /**
    Gets the local z position of this transform
    @return {Number} localPosition z
    */
    getLocalPositionZ() {
        const tempVec = this.nativeObject.position.z;
        return tempVec;
    }

    /**
    @returns {Vector3} world Position vector of the object
    */
    getWorldPosition() {
        const vector = new Vector3();
        vector.setFromMatrixPosition(this.nativeObject.matrixWorld);
        return vector;
    }

    /**
     * get the world matrix of the object
     * @returns world matrix of the object
     */
    getWorldMatrix() {
        return this.nativeObject.matrixWorld;
    }

    /**
     * get the world quaternion of the object
     * @returns world quaternion of the object
     */
    getWorldQuaternion() {
        const q = new Quaternion().setFromRotationMatrix(this.getWorldMatrix());
        return q;
    }

    /**
   @return {Matrix4} local matrix of the object
   */
    getLocalMatrix() {
        return this.nativeObject.matrix;
    }

    /**
    Set the scale of the transform.
    Polymorphic : can take various agruments of various types. Possible arguments number is 1, 2 or 3.

    @param {float|Vector3|Vector2} number|Vector3|Vector2 Value for the new scale. If a Vector3 is given, its x, y, z will be used for the scale x, y, z. If a Vector2 is given, its x, y will be used for the scale x and y, but z will be 1. If a number is given, it will be the scale x.
    @param {float} Number Value for the new y scale.
    @param {float} Number Value for the new z scale.
    */
    setLocalScale(arg1, arg2, arg3) {
        if (arguments.length === 3) {
            this.nativeObject.scale.x = arg1;
            this.nativeObject.scale.y = arg2;
            this.nativeObject.scale.z = arg3;
        }
        else if (arguments.length === 2) {
            if (typeof arg1 === "number" && typeof arg2 === "number") {
                this.nativeObject.scale.x = arg1;
                this.nativeObject.scale.y = arg2;
            }
        }
        else if (arguments.length === 1) {
            if (arg1 instanceof Vector3) {
                this.nativeObject.scale.x = arg1.x;
                this.nativeObject.scale.y = arg1.y;
                this.nativeObject.scale.z = arg1.z;
            }
            else if (arg1 instanceof Vector2) {
                this.nativeObject.scale.x = arg1.x;
                this.nativeObject.scale.y = arg1.y;
            }
            else if (typeof arg1 === "number") {
                this.nativeObject.scale.x = arg1;
                this.nativeObject.scale.y = arg1;
                this.nativeObject.scale.z = arg1;
            }
        }
    }

    /**
    * Sets this transform's x scale
    * @param {Number} arg
    */
    setLocalScaleX(arg) {
        if (typeof arg === "number") {
            this.nativeObject.scale.x = arg;
        }
    }

    /**
    * Sets this transform's y scale
    * @param {Number} arg
    */
    setLocalScaleY(arg) {
        if (typeof arg === "number") {
            this.nativeObject.scale.y = arg;
        }
    }

    /**
    * Sets this transform's z scale
    * @param {Number} arg
    */
    setLocalScaleZ(arg) {
        if (typeof arg === "number") {
            this.nativeObject.scale.z = arg;
        }
    }

    /**
    @return {Vector3} localScale vector
    */
    getLocalScale() {
        const temp = new Vector3(this.nativeObject.scale.x, this.nativeObject.scale.y, this.nativeObject.scale.z);
        return temp;
    }

    /**
    @return {Number} localScale x
    */
    getLocalScaleX() {
        const temp = this.nativeObject.scale.x;
        return temp;
    }

    /**
    @return {Number} localScale y
    */
    getLocalScaleY() {
        const temp = this.nativeObject.scale.y;
        return temp;
    }

    /**
    @return {Number} localScale z
    */
    getLocalScaleZ() {
        const temp = this.nativeObject.scale.z;
        return temp;
    }

    /**
    * Perform a RayCast picking
    *
    * @param {Camera} cam The camera to use as a ray caster.
    * @param {number} x The x coordinate in screen space for the ray casting
    * @param {number} y The y coordinate in screen space for the ray casting
    * @return {Object} if picking success, gives an object as {point:{x,y,z} in **world coordinates**, uv:{u,v}, distance:dist}, null otherwise.
    */
    pick(cam, x, y, recursive) {

        //FIXME : will bug with non fullscreen configuration!!!
        //IDEA : use the context referenced in the cam to grab the canvas!
        let nx = 0;
        let ny = 0;

        if (cam.context) {
            const canvas = cam.context.getCanvasSize();
            nx = (x / canvas.width) * 2 - 1;
            ny = - (y / canvas.height) * 2 + 1;
        }
        else {
            nx = (x / window.innerWidth) * 2 - 1;
            ny = - (y / window.innerHeight) * 2 + 1;
        }
        // create a Ray with origin at the mouse position
        //and direction into the scene (camera direction)
        const raycaster = new Raycaster(); // create once

        const vector = new Vector3(nx, ny, 1);
        const camera = cam.transform.nativeObject;

        raycaster.setFromCamera(vector, camera);

        //const intersects = raycaster.intersectObjects([this.nativeObject]);
        const intersects = raycaster.intersectObject(this.nativeObject, recursive);

        /* if (intersects && intersects.length > 0) {
            console.log("intersects", intersects);
        } */

        //TODO switch to array return
        // if there is one (or more) intersections
        if (intersects.length > 0) {

            if (recursive) {

                const result = [];

                for (let i = 0; i < intersects.length; i++) {
                    const temp = {};
                    temp.point = intersects[i].point;
                    temp.uv = intersects[i].uv;
                    temp.distance = intersects[i].distance;
                    temp.mobObject = intersects[i].object.mobObject;
                    result.push(temp);
                }
                return result;
            }

            if (!recursive) {
                const temp = {};
                temp.point = intersects[0].point;
                temp.uv = intersects[0].uv;
                temp.distance = intersects[0].distance;
                temp.mobObject = intersects[0].object.mobObject;
                return temp;
            }
        }

        // there are no intersections
        return null;
    }

    /**
    * Return a Vector2 giving the screen coordinates of the object
    * @param {Context} context the current Mobilizing context
    * @param {Camera} camera the Camera to use for the projection
    * @return {Vector2} the screen coordinates of the object
    */
    getScreenCoordinates(context, camera) {
        if (camera.type === "perspective") {
            const vector = new Vector3();

            // TODO: need to update this when resize window
            const widthHalf = 0.5 * context.getCanvasSize().width * camera.viewport.width;
            const heightHalf = 0.5 * context.getCanvasSize().height * camera.viewport.height;

            this.nativeObject.updateMatrixWorld();
            vector.setFromMatrixPosition(this.nativeObject.matrixWorld);
            vector.project(camera.camera);

            vector.x = (vector.x * widthHalf) + widthHalf;
            vector.y = -(vector.y * heightHalf) + heightHalf;

            return {
                x: vector.x,
                y: vector.y
            }
        }

        return null;
    }

    /**
    * Adds a child to this transform (argument must be a transform too)
    * @param {Transform Mesh} child
    */
    addChild(child) {
        if (child instanceof Transform) {
            child.parent = this.mobObject;
            this.children.push(child.mobObject);
            this.nativeObject.add(child.nativeObject);
        }
        else if (child instanceof Mesh) {
            child.transform.parent = this.mobObject;
            this.children.push(child.transform.mobObject);
            this.nativeObject.add(child.transform.nativeObject);
        }
        else if (child instanceof Object3D){
            console.log("Three object3D added");

        }
    }

    /**
    * Adds a children array to this transform (argument must be a transform too)
    * @param {Array} child
    */
    addChildren(array) {
        array.forEach((child) => {
            this.addChild(child);
        });
    }

    /**
    * Removes a child from the children chain
    * @param {Transform} child
    */
    removeChild(child) {
        this.children.splice(this.children.indexOf(child), 1);
        this.nativeObject.remove(child.nativeObject);
    }

    /**
    * Gets an array containning all the children objects of this transform
    * @return {Array}
    */
    getChildren(recursive) {
        if (!recursive) {
            return this.children;
        }
        this.getRecurseChildren(this);
        return this.recursiveChildren;
    }

    getRecurseChildren(transform) {
        if (transform.children.length > 0) {
            transform.children.forEach((child) => {
                this.recursiveChildren.push(child);
                if (child.transform.getChildren().length > 0) {
                    transform.getRecurseChildren(child.transform);
                }
            });
        }
    }

    /**
    * Gets one of all the children objects of this transform
    *
    * @param {Number} index Index of the child to get
    * @return {Object}
    */
    getChild(index) {
        return this.children[index];
    }

    getParent() {
        return this.parent;
    }

    /**
    * Sets the render order of this object. The sortObjects of the renderer should be true for this property to have any effect.
    @param {Number} the render order index
    */
    setRenderOrder(val) {
        this.nativeObject.renderOrder = val;
    }

    /**
     * Set this object visibility
     * @param {Boolean} val 
     */
    setVisible(val) {
        this.nativeObject = val;
    }

    /**
    * @returns the visibility of this object
    */
    getVisible() {
        return this.nativeObject.visible;
    }
}