Repository

js/Mobilizing/renderer/3D/three/input/SpaceNavigator.js

/**
 * https://github.com/archilogic-com/aframe-space-navigator-controls/blob/master/src/aframe-space-navigator-controls.js
 */
/* eslint-disable no-unused-vars */
import Component from '../../../../core/Component';
import Gamepad from '../../../../input/Gamepad';
import Vector3 from "../types/Vector3";
import Euler from "../types/Euler";
import Quaternion from '../types/Quaternion';
import Node from "../shape/3D/primitive/Node";
import Time from "../../../../time/Time";
import * as math from "../../../../core/util/Math";

const MAX_DELTA = 200, // ms
    ROTATION_EPS = 0.000,
    DEFAULT_FOV = 60;

export default class SpaceNavigator extends Component {

    constructor({
        rotationSensitivity = 0.05,
        movementEasing = 3,
        movementAcceleration = 700,
        fovSensitivity = 0.01,
        fovEasing = 3,
        fovAcceleration = 5,
        invertScroll = false
    } = {}) {
        super(...arguments);

        this.rotationSensitivity = rotationSensitivity;
        this.movementEasing = movementEasing;
        this.movementAcceleration = movementAcceleration;
        this.fovSensitivity = fovSensitivity;
        this.fovEasing = fovEasing;
        this.fovAcceleration = fovAcceleration;
        this.invertScroll = invertScroll;

        this.data = {
            // Enable/disable features
            enabled: true,
            movementEnabled: true,
            lookEnabled: true,
            rollEnabled: true,
            invertPitch: false,
            fovEnabled: true,
            fovMin: 2,
            fovMax: 115,

            // Constants
            rotationSensitivity,
            movementEasing,
            movementAcceleration,
            fovSensitivity,
            fovEasing,
            fovAcceleration,
            invertScroll
        }

        this.connected = false;
    }

    setup() {

        if (!this._setupDone) {

            this.spaceNavigator = null;

            this.gamepad = new Gamepad();
            this.gamepad.setup();
            this.gamepad.on();
            this.context.addComponent(this.gamepad);

            //console.log(this.gamepad);
            this.gamepad.events.on("connected", (gamepad) => {

                const gamepadName = gamepad.id.toLowerCase();

                if (gamepadName.indexOf("spacenavigator") >= 0 ||
                    gamepadName.indexOf("space navigator") >= 0) {

                    this.spaceNavigator = gamepad;
                    console.log("found SpaceNavigator", this.spaceNavigator);
                    this.events.trigger("connected");
                    this.connected = true;
                }
            });

            this.gamepad.events.on("disconnected", () => {
                this.events.trigger("disconnected");
                this.spaceNavigator = null;
                this.connected = false;
            });

            // Movement
            this.position = new Vector3(0, 0, 0);
            this.movement = new Vector3(0, 0, 0);
            this.movementVelocity = new Vector3(0, 0, 0);
            this.movementDirection = new Vector3(0, 0, 0);

            // Rotation
            this.rotation = new Euler(0, 0, 0, 'YXZ');
            this.pitch = new Node();
            this.roll = new Node();
            this.yaw = new Node();
            this.yaw.transform.setLocalPositionY = 10;
            this.yaw.transform.addChild(this.pitch.transform);

            this.initialRotation = new Vector3();
            this.prevInitialRotation = new Vector3();
            this.prevFinalRotation = new Vector3();

            this.tCurrent = 0;
            this.tLastLocalActivity = 0;
            this.tLastExternalActivity = 0;

            // FOV
            this.fov = DEFAULT_FOV;
            this.fovVelocity = 0;

            //time delta
            this.time = new Time();
            this.time.setup();
            this.time.on();
            this.context.addComponent(this.time);

            super.setup();
        }
    }

    /*******************************************************************
   * Movement
   */
    updatePosition(dt) {

        const data = this.data;
        const acceleration = data.movementAcceleration;
        const easing = data.movementEasing;
        const velocity = this.movementVelocity;
        const spaceNavigator = this.spaceNavigator;

        // If data has changed or FPS is too low
        // we reset the velocity
        if (dt > MAX_DELTA) {
            velocity.x = 0;
            velocity.y = 0;
            velocity.z = 0;
            return;
        }

        velocity.z -= velocity.z * easing * dt / 1000;
        velocity.x -= velocity.x * easing * dt / 1000;
        velocity.y -= velocity.y * easing * dt / 1000;

        if (data.enabled && data.movementEnabled && spaceNavigator) {
            //  3dconnexion space navigator position axes
            //  "right handed coordinate system"
            //  0: - left / + right (pos: X axis pointing to the right)
            //  1: - backwards / + forward (pos: Z axis pointing forwards)
            //  2: - up / + down (pos: Y axis pointing down)

            const axes = spaceNavigator.axes;

            const xDelta = axes[0];
            const yDelta = axes[2];
            const zDelta = axes[1];

            velocity.x += xDelta * acceleration * dt / 1000;
            velocity.z += zDelta * acceleration * dt / 1000;
            velocity.y -= yDelta * acceleration * dt / 1000;
        }

        const movementVector = this.getMovementVector(dt);

        this.movement.copy(movementVector)
        this.position.add(movementVector)

        //this.position.add(velocity);

        /* if (el) {
            el.object3D.position.copy(this.position)
            el.setAttribute('position', {
                x: this.position.x,
                y: this.position.y,
                z: this.position.z
            });
        } */

        //console.log("this.position", this.position);
    }

    /**
     * 
     * @param {*} dt 
     * @returns 
     * @private
     */
    getMovementVector(dt) {

        const euler = new Euler(0, 0, 0, 'YXZ');
        const rotation = new Vector3();
        const direction = this.movementDirection;
        const velocity = this.movementVelocity;

        //rotation.set(math.radToDeg(this.rotation.x), math.radToDeg(this.rotation.y), math.radToDeg(this.rotation.z));
        rotation.set(0, 0, 1);

        direction.copy(velocity)
        direction.multiplyScalar(dt / 1000)

        euler.set(math.degToRad(rotation.x), math.degToRad(rotation.y), math.degToRad(rotation.z));
        direction.applyEuler(euler);
        return direction;
    }

    /*******************************************************************
   * Rotation
   */

    updateRotation() {

        const rotationEps = 0.0001;
        const debounce = 500;

        const spaceNavigator = this.spaceNavigator;

        if (!this.data.lookEnabled || !spaceNavigator) {
            return;
        }

        this.tCurrent = Date.now();

        this.initialRotation.set(
            math.radToDeg(this.rotation.x),
            math.radToDeg(this.rotation.y),
            math.radToDeg(this.rotation.z),
        )

        // If initial rotation for this frame is different from last frame, and
        // doesn't match last spaceNavigator state, assume an external component is
        // active on this element.
        if (this.initialRotation.distanceToSquared(this.prevInitialRotation) > rotationEps
            && this.initialRotation.distanceToSquared(this.prevFinalRotation) > rotationEps) {
            this.prevInitialRotation.copy(this.initialRotation);
            this.tLastExternalActivity = this.tCurrent;
            return;
        }

        this.prevInitialRotation.copy(this.initialRotation);

        // If external controls have been active in last 500ms, wait.
        if (this.tCurrent - this.tLastExternalActivity < debounce) {
            return;
        }

        /*
         * 3dconnexion space navigator rotation axes
         *
         * "right handed coordinate system"
         * 3: - pitch down / + pitch up (rot: X axis clock wise)
         * 4: - roll right / + roll left (rot: Z axis clock wise)
         * 5: - yaw right / + yaw left (rot: Y axis clock wise)
         */

        const delta = new Vector3(
            spaceNavigator.axes[3],
            spaceNavigator.axes[5],
            spaceNavigator.axes[4]);

        if (delta.x < ROTATION_EPS && delta.x > -ROTATION_EPS) {
            delta.z = 0;
        }
        if (delta.y < ROTATION_EPS && delta.y > -ROTATION_EPS) {
            delta.y = 0;
        }
        if (delta.z < ROTATION_EPS && delta.z > -ROTATION_EPS) {
            delta.x = 0;
        }

        if (this.data.invertPitch) {
            delta.x *= -delta.x;
        }

        // If external controls have been active more recently than spaceNavigator,
        // and spaceNavigator hasn't moved, don't overwrite the existing rotation.
        if (this.tLastExternalActivity > this.tLastLocalActivity && !delta.lengthSq()) {
            return;
        }

        delta.multiplyScalar(this.data.rotationSensitivity);

        /* this.pitch.rotation.x += delta.x;
        this.yaw.rotation.y -= delta.y;
        this.roll.rotation.z += delta.z; */

        this.pitch.transform.setLocalRotationX(this.pitch.transform.getLocalRotationX() + delta.x);
        this.yaw.transform.setLocalRotationY(this.yaw.transform.getLocalRotationY() - delta.y);
        this.roll.transform.setLocalRotationZ(this.roll.transform.getLocalRotationZ() + delta.z);

        this.rotation.set(
            this.pitch.transform.getLocalRotationX(),
            this.yaw.transform.getLocalRotationY(),
            this.data.rollEnabled ? this.roll.transform.getLocalRotationZ() : 0
        );

        //console.log(this.rotation);

        /*  if (this.el) {
             this.el.setAttribute('rotation', {
                 x: this.pitch.rotation.x * RAD_TO_DEG,
                 y: this.yaw.rotation.y * RAD_TO_DEG,
                 z: this.data.rollEnabled ? this.roll.rotation.z * RAD_TO_DEG : 0
             })
             prevFinalRotation.copy(this.el.getAttribute('rotation'))
         } else { */
        this.prevFinalRotation.set(
            math.radToDeg(this.rotation.x),
            math.radToDeg(this.rotation.y),
            math.radToDeg(this.rotation.z)
        )
        //}

        this.tLastLocalActivity = this.tCurrent;
    }

    getPosition(scaleFactor) {
        if (this.spaceNavigator) {
            if (!scaleFactor) {
                scaleFactor = 1;
            }
            const pos = new Vector3().copy(this.position);
            pos.multiplyScalar(scaleFactor);

            return pos;
        }
        return null;
    }

    getRotation() {
        if (this.spaceNavigator) {
            return this.rotation;
        }
        return null;
    }

    getQuaternion() {
        if (this.spaceNavigator) {
            const quat = new Quaternion().setFromEuler(this.rotation);
            return quat;
        }
        return null;
    }

    update() {

        if (this.spaceNavigator) {

            const updatedSpaceNavigator = this.gamepad.getGamePadFromIndex(this.spaceNavigator.index);
            this.spaceNavigator = updatedSpaceNavigator;

            if (this.spaceNavigator.connected) {
                const dt = this.time.getDelta();
                this.updatePosition(dt);
                this.updateRotation(dt);
            }
        }
    }

}