From c41d2df6e3f928a31d249d7c843bcfd7ce2c939d Mon Sep 17 00:00:00 2001
From: joni <johnfshaughnessy@gmail.com>
Date: Mon, 21 May 2018 17:31:53 -0700
Subject: [PATCH] Add MouseEventHandler with optional use of the Pointer Lock
 API

---
 src/components/cursor-controller.js        |  43 ++++----
 src/components/look-on-mobile.js           |   1 -
 src/components/virtual-gamepad-controls.js |  10 +-
 src/hub.js                                 |   8 +-
 src/utils/mouse-events-handler.js          | 115 +++++++++++++++++++++
 5 files changed, 148 insertions(+), 29 deletions(-)
 create mode 100644 src/utils/mouse-events-handler.js

diff --git a/src/components/cursor-controller.js b/src/components/cursor-controller.js
index 71f50fde5..da43d4f7d 100644
--- a/src/components/cursor-controller.js
+++ b/src/components/cursor-controller.js
@@ -49,10 +49,13 @@ AFRAME.registerComponent("cursor-controller", {
     this.handleTouchStart = this.handleTouchStart.bind(this);
     this.handleTouchMove = this.handleTouchMove.bind(this);
     this.handleTouchEnd = this.handleTouchEnd.bind(this);
-    this._handleMouseDown = this._handleMouseDown.bind(this);
-    this._handleMouseMove = this._handleMouseMove.bind(this);
-    this._handleMouseUp = this._handleMouseUp.bind(this);
-    this._handleWheel = this._handleWheel.bind(this);
+
+    window.APP.mouseEventsHandler.registerCursor(this);
+    this.handleMouseDown = this.handleMouseDown.bind(this);
+    this.handleMouseMove = this.handleMouseMove.bind(this);
+    this.handleMouseUp = this.handleMouseUp.bind(this);
+    this.handleMouseWheel = this.handleMouseWheel.bind(this);
+
     this._handleEnterVR = this._handleEnterVR.bind(this);
     this._handleExitVR = this._handleExitVR.bind(this);
     this._handlePrimaryDown = this._handlePrimaryDown.bind(this);
@@ -80,11 +83,6 @@ AFRAME.registerComponent("cursor-controller", {
   },
 
   play: function() {
-    document.addEventListener("mousedown", this._handleMouseDown);
-    document.addEventListener("mousemove", this._handleMouseMove);
-    document.addEventListener("mouseup", this._handleMouseUp);
-    document.addEventListener("wheel", this._handleWheel);
-
     window.addEventListener("enter-vr", this._handleEnterVR);
     window.addEventListener("exit-vr", this._handleExitVR);
 
@@ -101,11 +99,6 @@ AFRAME.registerComponent("cursor-controller", {
   },
 
   pause: function() {
-    document.removeEventListener("mousedown", this._handleMouseDown);
-    document.removeEventListener("mousemove", this._handleMouseMove);
-    document.removeEventListener("mouseup", this._handleMouseUp);
-    document.removeEventListener("wheel", this._handleWheel);
-
     window.removeEventListener("enter-vr", this._handleEnterVR);
     window.removeEventListener("exit-vr", this._handleExitVR);
 
@@ -226,11 +219,12 @@ AFRAME.registerComponent("cursor-controller", {
 
   _setLookControlsEnabled(enabled) {
     const lookControls = this.data.camera.components["look-controls"];
-    if (!lookControls) return;
-    if (enabled) {
-      lookControls.play();
-    } else {
-      lookControls.pause();
+    if (lookControls) {
+      if (enabled) {
+        lookControls.play();
+      } else {
+        lookControls.pause();
+      }
     }
   },
 
@@ -289,24 +283,27 @@ AFRAME.registerComponent("cursor-controller", {
     this._setCursorVisibility(false);
   },
 
-  _handleMouseDown: function() {
+  handleMouseDown: function() {
     if (this.isMobile && !this.inVR && !this.hasPointingDevice) return;
 
     if (this._isTargetOfType(TARGET_TYPE_INTERACTABLE_OR_UI)) {
       this._setLookControlsEnabled(false);
       this.data.cursor.emit("cursor-grab", {});
+      return true;
     } else if (this.inVR || this.isMobile) {
       this._startTeleport();
+      return;
     }
+    return false;
   },
 
-  _handleMouseMove: function(e) {
+  handleMouseMove: function(e) {
     if (this.isMobile && !this.inVR && !this.hasPointingDevice) return;
 
     this.mousePos.set(e.clientX / window.innerWidth * 2 - 1, -(e.clientY / window.innerHeight) * 2 + 1);
   },
 
-  _handleMouseUp: function() {
+  handleMouseUp: function() {
     if (this.isMobile && !this.inVR && !this.hasPointingDevice) return;
 
     this._setLookControlsEnabled(true);
@@ -314,7 +311,7 @@ AFRAME.registerComponent("cursor-controller", {
     this._endTeleport();
   },
 
-  _handleWheel: function(e) {
+  handleMouseWheel: function(e) {
     if (this._isGrabbing()) {
       switch (e.deltaMode) {
         case e.DOM_DELTA_PIXEL:
diff --git a/src/components/look-on-mobile.js b/src/components/look-on-mobile.js
index 53670eb8f..2287910ee 100644
--- a/src/components/look-on-mobile.js
+++ b/src/components/look-on-mobile.js
@@ -39,7 +39,6 @@ AFRAME.registerComponent("look-on-mobile", {
     this.hmdEuler = new THREE.Euler();
     this.prevX = this.hmdEuler.x;
     this.prevY = this.hmdEuler.y;
-    this.ticks = 0;
     this.pendingLookX = 0;
     this.onRotateX = this.onRotateX.bind(this);
     this.dXBuffer = [];
diff --git a/src/components/virtual-gamepad-controls.js b/src/components/virtual-gamepad-controls.js
index 44843532d..e90f7ac36 100644
--- a/src/components/virtual-gamepad-controls.js
+++ b/src/components/virtual-gamepad-controls.js
@@ -89,8 +89,9 @@ AFRAME.registerComponent("virtual-gamepad-controls", {
   onMoveJoystickChanged(event, joystick) {
     const angle = joystick.angle.radian;
     const force = joystick.force < 1 ? joystick.force : 1;
-    const x = Math.cos(angle) * force * 0.85;
-    const z = Math.sin(angle) * force * 0.85;
+    const moveStrength = 0.85;
+    const x = Math.cos(angle) * force * moveStrength;
+    const z = Math.sin(angle) * force * moveStrength;
     this.moving = true;
     this.moveEvent.axis[0] = x;
     this.moveEvent.axis[1] = z;
@@ -107,9 +108,10 @@ AFRAME.registerComponent("virtual-gamepad-controls", {
     // Set pitch and yaw angles on right stick move
     const angle = joystick.angle.radian;
     const force = joystick.force < 1 ? joystick.force : 1;
+    const turnStrength = 0.5;
     this.rotating = true;
-    this.rotateYEvent.value = Math.cos(angle) * force * 0.5;
-    this.rotateXEvent.value = Math.sin(angle) * force * 0.5;
+    this.rotateYEvent.value = Math.cos(angle) * force * turnStrength;
+    this.rotateXEvent.value = Math.sin(angle) * force * turnStrength;
   },
 
   onLookJoystickEnd() {
diff --git a/src/hub.js b/src/hub.js
index d38367bde..bbd203f7d 100644
--- a/src/hub.js
+++ b/src/hub.js
@@ -123,7 +123,9 @@ import { generateDefaultProfile, generateRandomName } from "./utils/identity.js"
 import { getAvailableVREntryTypes, VR_DEVICE_AVAILABILITY } from "./utils/vr-caps-detect.js";
 import ConcurrentLoadDetector from "./utils/concurrent-load-detector.js";
 import TouchEventsHandler from "./utils/touch-events-handler.js";
+import MouseEventsHandler from "./utils/mouse-events-handler.js";
 window.APP.touchEventsHandler = new TouchEventsHandler();
+window.APP.mouseEventsHandler = new MouseEventsHandler();
 
 function qsTruthy(param) {
   const val = qs[param];
@@ -242,10 +244,14 @@ const onReady = async () => {
     const camera = document.querySelector("#player-camera");
     const registerLookControls = e => {
       if (e.detail.name !== "look-controls") return;
-      window.APP.touchEventsHandler.registerLookControls(camera.components["look-controls"]);
       camera.removeEventListener("componentinitialized", registerLookControls);
+
+      window.APP.touchEventsHandler.registerLookControls(camera.components["look-controls"]);
       scene.components["look-on-mobile"].registerLookControls(camera.components["look-controls"]);
       scene.setAttribute("look-on-mobile", "enabled", true);
+
+      window.APP.mouseEventsHandler.registerLookControls(camera.components["look-controls"]);
+      window.APP.mouseEventsHandler.setInverseMouseLook(qsTruthy("invertMouseLook"));
     };
     camera.addEventListener("componentinitialized", registerLookControls);
     camera.setAttribute("look-controls", {
diff --git a/src/utils/mouse-events-handler.js b/src/utils/mouse-events-handler.js
new file mode 100644
index 000000000..bd55ccb9b
--- /dev/null
+++ b/src/utils/mouse-events-handler.js
@@ -0,0 +1,115 @@
+const HORIZONTAL_LOOK_SPEED = 0.0035;
+const VERTICAL_LOOK_SPEED = 0.0021;
+const PI_4 = Math.PI / 4;
+
+export default class MouseEventsHandler {
+  constructor() {
+    this.cursor = null;
+    this.lookControls = null;
+    this.isMouseDown = false;
+    this.isMouseDownHandledByCursor = false;
+    this.isPointerLocked = false;
+    this.dXBuffer = [];
+    this.dYBuffer = [];
+
+    this.registerCursor = this.registerCursor.bind(this);
+    this.registerLookControls = this.registerLookControls.bind(this);
+    this.isReady = this.isReady.bind(this);
+    this.addEventListeners = this.addEventListeners.bind(this);
+    this.onMouseDown = this.onMouseDown.bind(this);
+    this.onMouseMove = this.onMouseMove.bind(this);
+    this.onMouseUp = this.onMouseUp.bind(this);
+    this.onMouseWheel = this.onMouseWheel.bind(this);
+    this.look = this.look.bind(this);
+
+    document.addEventListener("contextmenu", function(e) {
+      e.preventDefault();
+    });
+  }
+
+  setInverseMouseLook(invert) {
+    this.invertMouseLook = invert;
+  }
+
+  registerCursor(cursor) {
+    this.cursor = cursor;
+    if (this.isReady()) {
+      this.addEventListeners();
+    }
+  }
+
+  registerLookControls(lookControls) {
+    this.lookControls = lookControls;
+    if (this.isReady()) {
+      this.addEventListeners();
+    }
+  }
+
+  isReady() {
+    return this.cursor && this.lookControls;
+  }
+
+  addEventListeners() {
+    document.addEventListener("mousedown", this.onMouseDown);
+    document.addEventListener("mousemove", this.onMouseMove);
+    document.addEventListener("mouseup", this.onMouseUp);
+    document.addEventListener("wheel", this.onMouseWheel);
+  }
+
+  onMouseDown(e) {
+    const isLeftButton = e.button === 0;
+    if (isLeftButton) {
+      this.isMouseDownHandledByCursor = this.cursor.handleMouseDown();
+      this.isMouseDown = true;
+    } else {
+      if (this.isPointerLocked) {
+        document.exitPointerLock();
+        this.isPointerLocked = false;
+      } else {
+        document.body.requestPointerLock();
+        this.isPointerLocked = true;
+      }
+    }
+  }
+
+  onMouseWheel(e) {
+    this.cursor.handleMouseWheel(e);
+  }
+
+  onMouseMove(e) {
+    const shouldLook = (this.isMouseDown && !this.isMouseDownHandledByCursor) || this.isPointerLocked;
+    if (shouldLook) {
+      this.look(e);
+    }
+
+    this.cursor.handleMouseMove(e);
+  }
+
+  look(e) {
+    const movementX = e.movementX;
+    const movementY = e.movementY;
+
+    const sign = this.invertMouseLook ? 1 : -1;
+    this.lookControls.yawObject.rotation.y += sign * movementX * HORIZONTAL_LOOK_SPEED;
+    this.lookControls.pitchObject.rotation.x += sign * movementY * VERTICAL_LOOK_SPEED;
+    this.lookControls.pitchObject.rotation.x = Math.max(
+      -PI_4,
+      Math.min(PI_4, this.lookControls.pitchObject.rotation.x)
+    );
+    this.lookControls.updateOrientation();
+  }
+
+  onMouseUp(e) {
+    const isLeftButton = e.button === 0;
+    if (isLeftButton) {
+      if (this.isMouseDownHandledByCursor) {
+        this.cursor.handleMouseUp();
+      }
+      this.isMouseDownHandledByCursor = false;
+      this.isMouseDown = false;
+      this.dXBuffer = [];
+      this.dYBuffer = [];
+    } else {
+    }
+  }
+}
-- 
GitLab