From 9bc670aa521f0911cb965bd839e322dd4eff7613 Mon Sep 17 00:00:00 2001
From: joni <johnfshaughnessy@gmail.com>
Date: Fri, 1 Jun 2018 18:41:31 -0700
Subject: [PATCH] Further refactor cursor-controller to remove interaction with
 physicalHand. Move configuration out of hub.js

---
 src/components/cursor-controller.js      | 111 +++++++++++------------
 src/components/input-configurator.js     |  97 ++++++++++++++++++++
 src/components/look-on-mobile.js         |   3 -
 src/hub.html                             |   8 +-
 src/hub.js                               | 105 +--------------------
 src/utils/gearvr-mouse-events-handler.js |  34 ++-----
 src/utils/mouse-events-handler.js        |  39 +++-----
 src/utils/primary-action-handler.js      |  45 +++++----
 src/utils/touch-events-handler.js        |  46 +++-------
 9 files changed, 220 insertions(+), 268 deletions(-)
 create mode 100644 src/components/input-configurator.js

diff --git a/src/components/cursor-controller.js b/src/components/cursor-controller.js
index 35b72d9b5..616fcf84f 100644
--- a/src/components/cursor-controller.js
+++ b/src/components/cursor-controller.js
@@ -18,12 +18,13 @@ AFRAME.registerComponent("cursor-controller", {
     physicalHandSelector: { type: "string" },
     handedness: { default: "right", oneOf: ["right", "left"] },
     maxDistance: { default: 3 },
-    minDistance: { default: 0.5 },
+    minDistance: { default: 0 },
     cursorColorHovered: { default: "#2F80ED" },
     cursorColorUnhovered: { default: "#FFFFFF" }
   },
 
   init: function() {
+    this.enabled = true;
     this.inVR = false;
     this.isMobile = AFRAME.utils.device.isMobile();
     this.hasPointingDevice = false;
@@ -31,6 +32,7 @@ AFRAME.registerComponent("cursor-controller", {
     this.currentDistance = this.data.maxDistance;
     this.currentDistanceMod = 0;
     this.mousePos = new THREE.Vector2();
+    this.useMousePos = true;
     this.controller = null;
     this.controllerQueue = [];
     this.wasCursorHovered = false;
@@ -91,48 +93,42 @@ AFRAME.registerComponent("cursor-controller", {
     this.el.sceneEl.removeEventListener("controllerdisconnected", this._handleControllerDisconnected);
   },
 
+  enable: function() {
+    this.enabled = true;
+  },
+
+  disable: function() {
+    this.enabled = false;
+    this.setCursorVisibility(false);
+  },
+
   tick: function() {
-    //handle physical hand
-    if (this.physicalHand) {
-      const state = this.physicalHand.components["super-hands"].state;
-      const isPhysicalHandGrabbing = state.has("grab-start") || state.has("hover-start");
-      if (this.wasPhysicalHandGrabbing != isPhysicalHandGrabbing) {
-        this.setCursorVisibility(!isPhysicalHandGrabbing);
-        this.currentTargetType = TARGET_TYPE_NONE;
-      }
-      this.wasPhysicalHandGrabbing = isPhysicalHandGrabbing;
-      if (isPhysicalHandGrabbing) {
-        return;
-      }
+    if (!this.enabled) {
+      return;
     }
 
-    //set raycaster origin/direction
-    const camera = this.data.camera.components.camera.camera;
-    if (!this.inVR) {
-      //mouse cursor mode
+    if (this.useMousePos) {
+      const camera = this.data.camera.components.camera.camera;
       const raycaster = this.el.components.raycaster.raycaster;
       raycaster.setFromCamera(this.mousePos, camera);
       this.origin.copy(raycaster.ray.origin);
       this.direction.copy(raycaster.ray.direction);
-    } else if ((this.inVR || this.isMobile) && !this.hasPointingDevice) {
-      //gaze cursor mode
-      camera.getWorldPosition(this.origin);
-      camera.getWorldDirection(this.direction);
-    } else if (this.controller != null) {
-      //3d cursor mode
-      this.controller.object3D.getWorldPosition(this.origin);
-      this.controller.object3D.getWorldQuaternion(this.controllerQuaternion);
-      this.direction
-        .set(0, 0, -1)
-        .applyQuaternion(this.controllerQuaternion)
-        .normalize();
+    } else {
+      this.rayObject.getWorldPosition(this.origin);
+      this.rayObject.getWorldDirection(this.direction);
     }
     this.el.setAttribute("raycaster", { origin: this.origin, direction: this.direction });
 
-    let intersection = null;
-
-    //update cursor position
-    if (!this._isGrabbing()) {
+    if (this._isGrabbing()) {
+      const distance = Math.min(
+        this.data.maxDistance,
+        Math.max(this.data.minDistance, this.currentDistance - this.currentDistanceMod)
+      );
+      this.direction.multiplyScalar(distance);
+      this.data.cursor.object3D.position.addVectors(this.origin, this.direction);
+    } else {
+      this.currentDistanceMod = 0;
+      let intersection = null;
       const intersections = this.el.components.raycaster.intersections;
       if (intersections.length > 0 && intersections[0].distance <= this.data.maxDistance) {
         intersection = intersections[0];
@@ -140,42 +136,28 @@ AFRAME.registerComponent("cursor-controller", {
         this.currentDistance = intersections[0].distance;
       } else {
         this.currentDistance = this.data.maxDistance;
+        this.direction.multiplyScalar(this.currentDistance);
+        this.data.cursor.object3D.position.addVectors(this.origin, this.direction);
       }
-      this.currentDistanceMod = 0;
-    }
-
-    if (this._isGrabbing() || !intersection) {
-      const max = Math.max(this.data.minDistance, this.currentDistance - this.currentDistanceMod);
-      const distance = Math.min(max, this.data.maxDistance);
-      this.currentDistanceMod = this.currentDistance - distance;
-      this.direction.multiplyScalar(distance);
-      this.data.cursor.object3D.position.addVectors(this.origin, this.direction);
-    }
 
-    //update currentTargetType
-    if (this._isGrabbing() && !intersection) {
-      this.currentTargetType = TARGET_TYPE_INTERACTABLE;
-    } else if (intersection) {
-      if (intersection.object.el.matches(".interactable, .interactable *")) {
+      if (!intersection) {
+        this.currentTargetType = TARGET_TYPE_NONE;
+      } else if (intersection.object.el.matches(".interactable, .interactable *")) {
         this.currentTargetType = TARGET_TYPE_INTERACTABLE;
       } else if (intersection.object.el.matches(".ui, .ui *")) {
         this.currentTargetType = TARGET_TYPE_UI;
       }
-    } else {
-      this.currentTargetType = TARGET_TYPE_NONE;
-    }
 
-    //update cursor material
-    const isTarget = this._isTargetOfType(TARGET_TYPE_INTERACTABLE_OR_UI);
-    if ((this._isGrabbing() || isTarget) && !this.wasCursorHovered) {
-      this.wasCursorHovered = true;
-      this.data.cursor.setAttribute("material", { color: this.data.cursorColorHovered });
-    } else if (!this._isGrabbing() && !isTarget && this.wasCursorHovered) {
-      this.wasCursorHovered = false;
-      this.data.cursor.setAttribute("material", { color: this.data.cursorColorUnhovered });
+      const isTarget = this._isTargetOfType(TARGET_TYPE_INTERACTABLE_OR_UI);
+      if (isTarget && !this.wasCursorHovered) {
+        this.wasCursorHovered = true;
+        this.data.cursor.setAttribute("material", { color: this.data.cursorColorHovered });
+      } else if (!isTarget && this.wasCursorHovered) {
+        this.wasCursorHovered = false;
+        this.data.cursor.setAttribute("material", { color: this.data.cursorColorUnhovered });
+      }
     }
 
-    //update line
     if (this.hasPointingDevice) {
       this.el.setAttribute("line", { start: this.origin.clone(), end: this.data.cursor.object3D.position.clone() });
     }
@@ -292,8 +274,17 @@ AFRAME.registerComponent("cursor-controller", {
       const hand = controllerData.handedness;
       this.el.setAttribute("cursor-controller", { physicalHandSelector: `#player-${hand}-controller` });
       this.controller = controllerData.controller;
+      this.rayObject = controllerData.controller.querySelector(`#player-${hand}-controller-reverse-z`).object3D;
+      this.useMousePos = false;
     } else {
       this.controller = null;
+      if (this.inVR) {
+        const camera = this.data.camera.components.camera.camera;
+        this.rayObject = camera;
+        this.useMousePos = false;
+      } else {
+        this.useMousePos = true;
+      }
     }
   }
 });
diff --git a/src/components/input-configurator.js b/src/components/input-configurator.js
new file mode 100644
index 000000000..e600dbdd5
--- /dev/null
+++ b/src/components/input-configurator.js
@@ -0,0 +1,97 @@
+import TouchEventsHandler from "../utils/touch-events-handler.js";
+import MouseEventsHandler from "../utils/mouse-events-handler.js";
+import GearVRMouseEventsHandler from "../utils/gearvr-mouse-events-handler.js";
+import PrimaryActionHandler from "../utils/primary-action-handler.js";
+
+AFRAME.registerComponent("input-configurator", {
+  init() {
+    this.inVR = this.el.sceneEl.is("vr-mode");
+    this.isMobile = AFRAME.utils.device.isMobile();
+    this.eventHandlers = [];
+    this.cursor = document.querySelector("#cursor-controller").components["cursor-controller"];
+    this.gazeTeleporter = document.querySelector("#gaze-teleport").components["teleport-controls"];
+    this.cameraController = document.querySelector("#player-camera").components["camera-controller"];
+
+    this.onEnterVR = this.onEnterVR.bind(this);
+    this.onExitVR = this.onExitVR.bind(this);
+    this.tearDown = this.tearDown.bind(this);
+    this.configureInput = this.configureInput.bind(this);
+    this.addLookOnMobile = this.addLookOnMobile.bind(this);
+
+    this.el.sceneEl.addEventListener("enter-vr", this.onEnterVR);
+    this.el.sceneEl.addEventListener("exit-vr", this.onExitVR);
+
+    this.tearDown();
+    this.configureInput();
+  },
+
+  onEnterVR() {
+    this.inVR = true;
+    this.tearDown();
+    this.configureInput();
+  },
+
+  onExitVR() {
+    this.inVR = false;
+    this.tearDown();
+    this.configureInput();
+  },
+
+  tearDown() {
+    this.eventHandlers.forEach(handler => handler.tearDown());
+    this.eventHandlers = [];
+    this.primaryActionHandler = null;
+    if (this.lookOnMobile) {
+      this.lookOnMobile.el.removeComponent("look-on-mobile");
+      this.lookOnMobile = null;
+    }
+    this.cameraController.pause();
+    this.cursorRequiresManagement = false;
+  },
+
+  addLookOnMobile() {
+    const onAdded = e => {
+      if (e.detail.name !== "look-on-mobile") return;
+      this.lookOnMobile = this.el.sceneEl.components["look-on-mobile"];
+      this.lookOnMobile.registerCameraController(this.cameraController);
+    };
+    this.el.sceneEl.addEventListener("componentinitialized", onAdded);
+    this.el.sceneEl.setAttribute("look-on-mobile", "");
+  },
+
+  configureInput() {
+    if (this.inVR) {
+      this.cursorRequiresManagement = true;
+      this.hovered = false;
+      this.primaryActionHandler = new PrimaryActionHandler(this.el.sceneEl, this.cursor);
+      this.eventHandlers.push(this.primaryActionHandler);
+      if (this.isMobile) {
+        this.eventHandlers.push(new GearVRMouseEventHandler(this.cursor, this.gazeTeleporter));
+      }
+    } else {
+      this.cameraController.play();
+      if (this.isMobile) {
+        this.eventHandlers.push(new TouchEventsHandler(this.cursor, this.cameraController, this.cursor.el));
+        this.addLookOnMobile();
+      } else {
+        this.eventHandlers.push(new MouseEventsHandler(this.cursor, this.cameraController));
+      }
+    }
+  },
+
+  tick() {
+    if (!this.cursorRequiresManagement) return;
+
+    if (this.cursor.physicalHand) {
+      const state = this.cursor.physicalHand.components["super-hands"].state;
+      if (!this.hovered && state.has("hover-start") && !this.primaryActionHandler.isCursorInteracting) {
+        this.cursor.disable();
+        this.hovered = true;
+      } else if (this.hovered === true && !state.has("hover-start") && !state.has("grab-start")) {
+        this.cursor.enable();
+        this.cursor.setCursorVisibility(!this.primaryActionHandler.isTeleporting);
+        this.hovered = false;
+      }
+    }
+  }
+});
diff --git a/src/components/look-on-mobile.js b/src/components/look-on-mobile.js
index e8047c92f..11e85b1d4 100644
--- a/src/components/look-on-mobile.js
+++ b/src/components/look-on-mobile.js
@@ -28,7 +28,6 @@ const average = a => {
 
 AFRAME.registerComponent("look-on-mobile", {
   schema: {
-    enabled: { default: false },
     horizontalLookSpeedRatio: { default: 0.4 }, // motion applied to camera / motion of polyfill object
     verticalLookSpeedRatio: { default: 0.4 } // motion applied to camera / motion of polyfill object
   },
@@ -64,9 +63,7 @@ AFRAME.registerComponent("look-on-mobile", {
   },
 
   tick() {
-    if (!this.data.enabled) return;
     const scene = this.el.sceneEl;
-    if (scene.is("vr-mode") && scene.checkHeadsetConnected()) return; // TODO: Why would this be ticking if we're in vr-mode?
     const hmdEuler = this.hmdEuler;
     const { horizontalLookSpeedRatio, verticalLookSpeedRatio } = this.data;
     this.polyfillControls.update();
diff --git a/src/hub.html b/src/hub.html
index 60ed2982c..d61dc3333 100644
--- a/src/hub.html
+++ b/src/hub.html
@@ -42,7 +42,7 @@
         personal-space-bubble="debug: false;"
         vr-mode-ui="enabled: false"
         pinch-to-move
-        look-on-mobile
+        input-configurator
     >
 
         <a-assets>
@@ -263,6 +263,7 @@
                 camera
                 position="0 1.6 0"
                 personal-space-bubble="radius: 0.4"
+                camera-controller
             >
                 <a-entity
                     id="gaze-teleport"
@@ -298,6 +299,7 @@
                 mixin="controller-super-hands"
                 controls-shape-offset
             >
+                <a-entity id="player-left-controller-reverse-z" rotation="0 180 0"></a-entity>
             </a-entity>
 
             <a-entity
@@ -318,7 +320,9 @@
                 body="type: static; shape: none;"
                 mixin="controller-super-hands"
                 controls-shape-offset
-            ></a-entity>
+            >
+                <a-entity id="player-right-controller-reverse-z" rotation="0 180 0"></a-entity>
+            </a-entity>
 
             <a-entity gltf-model-plus="inflate: true;"
                       class="model">
diff --git a/src/hub.js b/src/hub.js
index bf2470b1d..0dbd43739 100644
--- a/src/hub.js
+++ b/src/hub.js
@@ -65,6 +65,7 @@ import "./components/avatar-replay";
 import "./components/pinch-to-move";
 import "./components/look-on-mobile";
 import "./components/camera-controller";
+import "./components/input-configurator";
 
 import ReactDOM from "react-dom";
 import React from "react";
@@ -124,10 +125,6 @@ import registerTelemetry from "./telemetry";
 
 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";
-import GearVRMouseEventsHandler from "./utils/gearvr-mouse-events-handler.js";
-import PrimaryActionHandler from "./utils/primary-action-handler.js";
 
 function qsTruthy(param) {
   const val = qs[param];
@@ -239,106 +236,6 @@ const onReady = async () => {
 
     if (enterInVR) {
       scene.enterVR();
-      if (isMobile) {
-        // Set up GearVR event handling
-        // TODO: Only use this when using gearvr
-        window.APP.gearvrMouseEventsHandler = new GearVRMouseEventsHandler();
-        const teleportEl = document.querySelector("#gaze-teleport");
-        if (teleportEl && teleportEl.components && teleportEl.components["teleport-controls"]) {
-          const teleportControls = teleportEl.components["teleport-controls"];
-          window.APP.gearvrMouseEventsHandler.registerGazeTeleporter(teleportControls);
-        } else {
-          const registerTeleporter = e => {
-            if (e.detail.name !== "teleport-controls") return;
-            teleportEl.removeEventListener("componentinitialized", registerTeleporter);
-            const teleportControls = teleportEl.components["teleport-controls"];
-            window.APP.gearvrMouseEventsHandler.registerGazeTeleporter(teleportControls);
-          };
-          teleportEl.addEventListener("componentinitialized", registerTeleporter);
-        }
-
-        const cursorEl = document.querySelector("#cursor-controller");
-        if (cursorEl && cursorEl.components && cursorEl.components["cursor-controller"]) {
-          const cursor = cursorEl.components["cursor-controller"];
-          window.APP.gearvrMouseEventsHandler.registerCursor(cursor);
-        } else {
-          const registerCursor = e => {
-            if (e.detail.name !== "cursor-controller") return;
-            cursorEl.removeEventListener("componentinitialized", registerCursor);
-            const cursor = cursorEl.components["cursor-controller"];
-            window.APP.gearvrMouseEventsHandler.registerCursor(cursor);
-          };
-          cursorEl.addEventListener("componentinitialized", registerCursor);
-        }
-      }
-
-      // Set up event handling for anything emitting "action_primary_down/up" and "action_grab/release"
-      window.APP.primaryActionHandler = new PrimaryActionHandler(scene);
-
-      const cursorEl = document.querySelector("#cursor-controller");
-      if (cursorEl && cursorEl.components && cursorEl.components["cursor-controller"]) {
-        const cursor = cursorEl.components["cursor-controller"];
-        window.APP.primaryActionHandler.registerCursor(cursor);
-      } else {
-        const registerCursor = e => {
-          if (e.detail.name !== "cursor-controller") return;
-          cursorEl.removeEventListener("componentinitialized", registerCursor);
-          const cursor = cursorEl.components["cursor-controller"];
-          window.APP.primaryActionHandler.registerCursor(cursor);
-        };
-        cursorEl.addEventListener("componentinitialized", registerCursor);
-      }
-    } else {
-      if (isMobile) {
-        window.APP.touchEventsHandler = new TouchEventsHandler();
-      } else {
-        window.APP.mouseEventsHandler = new MouseEventsHandler();
-      }
-
-      const camera = document.querySelector("#player-camera");
-      const registerCameraController = e => {
-        if (e.detail.name !== "camera-controller") return;
-        camera.removeEventListener("componentinitialized", registerCameraController);
-
-        if (window.APP.touchEventsHandler) {
-          window.APP.touchEventsHandler.registerCameraController(camera.components["camera-controller"]);
-          scene.components["look-on-mobile"].registerCameraController(camera.components["camera-controller"]);
-          scene.setAttribute("look-on-mobile", "enabled", true);
-        }
-
-        if (window.APP.mouseEventsHandler) {
-          window.APP.mouseEventsHandler.registerCameraController(camera.components["camera-controller"]);
-          window.APP.mouseEventsHandler.setInverseMouseLook(qsTruthy("invertMouseLook"));
-        }
-      };
-      camera.addEventListener("componentinitialized", registerCameraController);
-      camera.setAttribute("camera-controller", "");
-
-      const cursorEl = document.querySelector("#cursor-controller");
-      if (cursorEl && cursorEl.components && cursorEl.components["cursor-controller"]) {
-        const cursor = cursorEl.components["cursor-controller"];
-        if (window.APP.touchEventsHandler) {
-          window.APP.touchEventsHandler.registerPinchEmitter(cursorEl);
-          window.APP.touchEventsHandler.registerCursor(cursor);
-        }
-        if (window.APP.mouseEventsHandler) {
-          window.APP.mouseEventsHandler.registerCursor(cursor);
-        }
-      } else {
-        const registerCursor = e => {
-          if (e.detail.name !== "cursor-controller") return;
-          cursorEl.removeEventListener("componentinitialized", registerCursor);
-          const cursor = cursorEl.components["cursor-controller"];
-          if (window.APP.touchEventsHandler) {
-            window.APP.touchEventsHandler.registerPinchEmitter(cursorEl);
-            window.APP.touchEventsHandler.registerCursor(cursor);
-          }
-          if (window.APP.mouseEventsHandler) {
-            window.APP.mouseEventsHandler.registerCursor(cursor);
-          }
-        };
-        cursorEl.addEventListener("componentinitialized", registerCursor);
-      }
     }
 
     AFRAME.registerInputActions(inGameActions, "default");
diff --git a/src/utils/gearvr-mouse-events-handler.js b/src/utils/gearvr-mouse-events-handler.js
index 7e614ab15..90ee3c853 100644
--- a/src/utils/gearvr-mouse-events-handler.js
+++ b/src/utils/gearvr-mouse-events-handler.js
@@ -1,34 +1,15 @@
 export default class GearVRMouseEventsHandler {
-  constructor() {
-    this.cursor = null;
-    this.gazeTeleporter = null;
+  constructor(cursor, gazeTeleporter) {
+    this.cursor = cursor;
+    this.gazeTeleporter = gazeTeleporter;
     this.isMouseDownHandledByCursor = false;
     this.isMouseDownHandledByGazeTeleporter = false;
 
-    this.registerCursor = this.registerCursor.bind(this);
-    this.registerGazeTeleporter = this.registerGazeTeleporter.bind(this);
-    this.isReady = this.isReady.bind(this);
     this.addEventListeners = this.addEventListeners.bind(this);
+    this.tearDown = this.tearDown.bind(this);
     this.onMouseDown = this.onMouseDown.bind(this);
     this.onMouseUp = this.onMouseUp.bind(this);
-  }
-
-  registerCursor(cursor) {
-    this.cursor = cursor;
-    if (this.isReady()) {
-      this.addEventListeners();
-    }
-  }
-
-  registerGazeTeleporter(gazeTeleporter) {
-    this.gazeTeleporter = gazeTeleporter;
-    if (this.isReady()) {
-      this.addEventListeners();
-    }
-  }
-
-  isReady() {
-    return this.cursor && this.gazeTeleporter;
+    this.addEventListeners();
   }
 
   addEventListeners() {
@@ -36,6 +17,11 @@ export default class GearVRMouseEventsHandler {
     document.addEventListener("mouseup", this.onMouseUp);
   }
 
+  tearDown() {
+    document.removeEventListener("mousedown", this.onMouseDown);
+    document.removeEventListener("mouseup", this.onMouseUp);
+  }
+
   onMouseDown() {
     this.isMouseDownHandledByCursor = this.cursor.startInteraction();
     if (this.isMouseDownHandledByCursor) {
diff --git a/src/utils/mouse-events-handler.js b/src/utils/mouse-events-handler.js
index 86ed25430..43c92f11e 100644
--- a/src/utils/mouse-events-handler.js
+++ b/src/utils/mouse-events-handler.js
@@ -3,47 +3,38 @@ const HORIZONTAL_LOOK_SPEED = 0.1;
 const VERTICAL_LOOK_SPEED = 0.06;
 
 export default class MouseEventsHandler {
-  constructor() {
-    this.cursor = null;
-    this.cameraController = null;
+  constructor(cursor, cameraController) {
+    this.cursor = cursor;
+    this.cameraController = cameraController;
     this.isLeftButtonDown = false;
     this.isLeftButtonHandledByCursor = false;
     this.isPointerLocked = false;
 
-    this.registerCursor = this.registerCursor.bind(this);
-    this.registerCameraController = this.registerCameraController.bind(this);
-    this.isReady = this.isReady.bind(this);
     this.addEventListeners = this.addEventListeners.bind(this);
     this.onMouseDown = this.onMouseDown.bind(this);
     this.onLeftButtonDown = this.onLeftButtonDown.bind(this);
     this.onRightButtonDown = this.onRightButtonDown.bind(this);
-
+    this.tearDown = this.tearDown.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);
-  }
-
-  setInverseMouseLook(invert) {
-    this.invertMouseLook = invert;
-  }
 
-  registerCursor(cursor) {
-    this.cursor = cursor;
-    if (this.isReady()) {
-      this.addEventListeners();
-    }
+    this.addEventListeners();
   }
 
-  registerCameraController(cameraController) {
-    this.cameraController = cameraController;
-    if (this.isReady()) {
-      this.addEventListeners();
-    }
+  tearDown() {
+    document.removeEventListener("mousedown", this.onMouseDown);
+    document.removeEventListener("mousemove", this.onMouseMove);
+    document.removeEventListener("mouseup", this.onMouseUp);
+    document.removeEventListener("wheel", this.onMouseWheel);
+    document.removeEventListener("contextmenu", e => {
+      e.preventDefault();
+    });
   }
 
-  isReady() {
-    return this.cursor && this.cameraController;
+  setInverseMouseLook(invert) {
+    this.invertMouseLook = invert;
   }
 
   addEventListeners() {
diff --git a/src/utils/primary-action-handler.js b/src/utils/primary-action-handler.js
index adb585e05..9a22c20ab 100644
--- a/src/utils/primary-action-handler.js
+++ b/src/utils/primary-action-handler.js
@@ -1,12 +1,12 @@
 export default class PrimaryActionHandler {
-  constructor(scene) {
+  constructor(scene, cursor) {
     this.scene = scene;
-    this.cursor = null;
+    this.cursor = cursor;
     this.isCursorInteracting = false;
+    this.isTeleporting = false;
 
-    this.registerCursor = this.registerCursor.bind(this);
-    this.isReady = this.isReady.bind(this);
     this.addEventListeners = this.addEventListeners.bind(this);
+    this.tearDown = this.tearDown.bind(this);
     this.onPrimaryDown = this.onPrimaryDown.bind(this);
     this.onPrimaryUp = this.onPrimaryUp.bind(this);
     this.onGrab = this.onGrab.bind(this);
@@ -14,17 +14,7 @@ export default class PrimaryActionHandler {
     this.onCardboardButtonDown = this.onCardboardButtonDown.bind(this);
     this.onCardboardButtonUp = this.onCardboardButtonUp.bind(this);
     this.onMoveDuck = this.onMoveDuck.bind(this);
-  }
-
-  registerCursor(cursor) {
-    this.cursor = cursor;
-    if (this.isReady()) {
-      this.addEventListeners();
-    }
-  }
-
-  isReady() {
-    return this.cursor;
+    this.addEventListeners();
   }
 
   addEventListeners() {
@@ -37,6 +27,16 @@ export default class PrimaryActionHandler {
     this.scene.addEventListener("move_duck", this.onMoveDuck);
   }
 
+  tearDown() {
+    this.scene.removeEventListener("action_primary_down", this.onPrimaryDown);
+    this.scene.removeEventListener("action_primary_up", this.onPrimaryUp);
+    this.scene.removeEventListener("action_grab", this.onGrab);
+    this.scene.removeEventListener("action_release", this.onRelease);
+    this.scene.removeEventListener("cardboardbuttondown", this.onCardboardButtonDown);
+    this.scene.removeEventListener("cardboardbuttonup", this.onCardboardButtonUp);
+    this.scene.removeEventListener("move_duck", this.onMoveDuck);
+  }
+
   onMoveDuck(e) {
     if (this.isCursorInteracting) {
       this.cursor.changeDistanceMod(-e.detail.axis[1] / 8);
@@ -85,22 +85,29 @@ export default class PrimaryActionHandler {
     this.cursor.setCursorVisibility(false);
     const button = e.target.components["teleport-controls"].data.button;
     e.target.emit(button + "down");
+    this.isTeleporting = true;
   }
 
   onPrimaryUp(e) {
-    if (this.isCursorInteracting && this.cursor.controller && this.cursor.controller === e.target) {
+    const isCursorHand = this.cursor.controller && this.cursor.controller === e.target;
+    if (this.isCursorInteracting && isCursorHand) {
       this.isCursorInteracting = false;
       this.cursor.endInteraction();
       return;
     }
 
-    if (e.target.components["super-hands"].state.has("grab-start")) {
+    const state = e.target.components["super-hands"].state;
+    if (state.has("grab-start")) {
       e.target.emit("hand_release");
+      return;
     }
 
-    this.cursor.setCursorVisibility(true);
+    if (isCursorHand) {
+      this.cursor.setCursorVisibility(!state.has("hover-start"));
+    }
     const button = e.target.components["teleport-controls"].data.button;
     e.target.emit(button + "up");
+    this.isTeleporting = false;
   }
 
   onCardboardButtonDown(e) {
@@ -114,6 +121,7 @@ export default class PrimaryActionHandler {
     const gazeTeleport = e.target.querySelector("#gaze-teleport");
     const button = gazeTeleport.components["teleport-controls"].data.button;
     gazeTeleport.emit(button + "down");
+    this.isTeleporting = true;
   }
 
   onCardboardButtonUp(e) {
@@ -128,5 +136,6 @@ export default class PrimaryActionHandler {
     const gazeTeleport = e.target.querySelector("#gaze-teleport");
     const button = gazeTeleport.components["teleport-controls"].data.button;
     gazeTeleport.emit(button + "up");
+    this.isTeleporting = false;
   }
 }
diff --git a/src/utils/touch-events-handler.js b/src/utils/touch-events-handler.js
index 0c08a41ac..91b9bc25b 100644
--- a/src/utils/touch-events-handler.js
+++ b/src/utils/touch-events-handler.js
@@ -3,11 +3,10 @@ const HORIZONTAL_LOOK_SPEED = 0.35;
 const VERTICAL_LOOK_SPEED = 0.18;
 
 export default class TouchEventsHandler {
-  constructor() {
-    this.cursor = null;
-    this.cameraController = null;
-
-    this.pinchEmitter = null;
+  constructor(cursor, cameraController, pinchEmitter) {
+    this.cursor = cursor;
+    this.cameraController = cameraController;
+    this.pinchEmitter = pinchEmitter;
     this.touches = [];
     this.touchReservedForCursor = null;
     this.touchesReservedForPinch = [];
@@ -16,10 +15,6 @@ export default class TouchEventsHandler {
     this.pinchTouchId1 = -1;
     this.pinchTouchId2 = -1;
 
-    this.registerCursor = this.registerCursor.bind(this);
-    this.registerCameraController = this.registerCameraController.bind(this);
-
-    this.isReady = this.isReady.bind(this);
     this.addEventListeners = this.addEventListeners.bind(this);
     this.handleTouchStart = this.handleTouchStart.bind(this);
     this.singleTouchStart = this.singleTouchStart.bind(this);
@@ -29,31 +24,9 @@ export default class TouchEventsHandler {
     this.singleTouchEnd = this.singleTouchEnd.bind(this);
     this.pinch = this.pinch.bind(this);
     this.look = this.look.bind(this);
-  }
-
-  registerCursor(cursor) {
-    this.cursor = cursor;
-    if (this.isReady()) {
-      this.addEventListeners();
-    }
-  }
+    this.tearDown = this.tearDown.bind(this);
 
-  registerCameraController(cameraController) {
-    this.cameraController = cameraController;
-    if (this.isReady()) {
-      this.addEventListeners();
-    }
-  }
-
-  registerPinchEmitter(pinchEmitter) {
-    this.pinchEmitter = pinchEmitter;
-    if (this.isReady()) {
-      this.addEventListeners();
-    }
-  }
-
-  isReady() {
-    return this.cursor && this.cameraController && this.pinchEmitter;
+    this.addEventListeners();
   }
 
   addEventListeners() {
@@ -63,6 +36,13 @@ export default class TouchEventsHandler {
     document.addEventListener("touchcancel", this.handleTouchEnd);
   }
 
+  tearDown() {
+    document.removeEventListener("touchstart", this.handleTouchStart);
+    document.removeEventListener("touchmove", this.handleTouchMove);
+    document.removeEventListener("touchend", this.handleTouchEnd);
+    document.removeEventListener("touchcancel", this.handleTouchEnd);
+  }
+
   handleTouchStart(e) {
     this.cursor.setCursorVisibility(false);
 
-- 
GitLab