From 6a399594c31a02693486b6ff35ffd6bc51c21c3b Mon Sep 17 00:00:00 2001
From: Kevin Lee <kevin@infinite-lee.com>
Date: Wed, 18 Apr 2018 18:50:09 -0700
Subject: [PATCH] Adding cursor-controller. Mostly feature complete with a few
 bugs. Still WIP.

---
 src/components/cursor-controller.js | 337 ++++++++++++++++++++++++++++
 src/components/event-repeater.js    |   2 +-
 src/components/super-cursor.js      | 168 --------------
 src/components/super-spawner.js     |   7 +-
 src/hub.html                        |  69 +++---
 src/hub.js                          |   3 +-
 src/input-mappings.js               |  44 ++--
 7 files changed, 402 insertions(+), 228 deletions(-)
 create mode 100644 src/components/cursor-controller.js
 delete mode 100644 src/components/super-cursor.js

diff --git a/src/components/cursor-controller.js b/src/components/cursor-controller.js
new file mode 100644
index 000000000..345ccb1fb
--- /dev/null
+++ b/src/components/cursor-controller.js
@@ -0,0 +1,337 @@
+const TARGET_TYPE_NONE = 1;
+const TARGET_TYPE_INTERACTABLE = 2;
+const TARGET_TYPE_UI = 4;
+const TARGET_TYPE_OTHER = 8;
+const TARGET_TYPE_INTERACTABLE_OR_UI = TARGET_TYPE_INTERACTABLE | TARGET_TYPE_UI;
+
+AFRAME.registerComponent("cursor-controller", {
+  dependencies: ["raycaster", "line"],
+  schema: {
+    cursor: { type: "selector" },
+    camera: { type: "selector" },
+    controller: { type: "selector" },
+    playerRig: { type: "selector" },
+    otherHand: { type: "string" },
+    hand: { default: "right" },
+    trackedControls: { type: "selectorAll", default: "[tracked-controls]" },
+    maxDistance: { default: 3 },
+    minDistance: { default: 0.5 },
+    cursorColorHovered: { default: "#FF0000" },
+    cursorColorUnhovered: { efault: "#FFFFFF" },
+    controllerEvent: { type: "string", default: "action_primary_down" },
+    controllerEndEvent: { type: "string", default: "action_primary_up" },
+    teleportEvent: { type: "string", default: "action_teleport_down" },
+    teleportEndEvent: { type: "string", default: "action_teleport_up" }
+  },
+
+  init: function() {
+    this.inVR = false;
+    this.isMobile = AFRAME.utils.device.isMobile();
+    this.trackedControls = [];
+    this.hasPointingDevice = false;
+    this.currentTargetType = TARGET_TYPE_NONE;
+    this.isGrabbing = false;
+    this.wasOtherHandGrabbing = false;
+    this.wasIntersecting = false;
+    this.currentDistance = this.data.maxDistance;
+    this.currentDistanceMod = 0;
+    this.origin = new THREE.Vector3();
+    this.direction = new THREE.Vector3();
+    this.point = new THREE.Vector3();
+    this.mousePos = new THREE.Vector2();
+    this.controllerQuaternion = new THREE.Quaternion();
+
+    this.data.cursor.setAttribute("material", { color: this.data.cursorColorUnhovered });
+
+    this.mouseDownListener = this._handleMouseDown.bind(this);
+    this.mouseMoveListener = this._handleMouseMove.bind(this);
+    this.mouseUpListener = this._handleMouseUp.bind(this);
+    this.wheelListener = this._handleWheel.bind(this);
+
+    this.enterVRListener = this._handleEnterVR.bind(this);
+    this.exitVRListener = this._handleExitVR.bind(this);
+
+    this.raycasterIntersectionListener = this._handleRaycasterIntersection.bind(this);
+    this.raycasterIntersectionClearedListener = this._handleRaycasterIntersectionCleared.bind(this);
+
+    this.controllerEventListener = this._handleControllerEvent.bind(this);
+    this.controllerEndEventListener = this._handleControllerEndEvent.bind(this);
+
+    this.modelLoadedListener = this._handleModelLoaded.bind(this);
+  },
+
+  update: function(oldData) {
+    if (this.data.controller !== oldData.controller) {
+      if (oldData.controller) {
+        oldData.controller.removeEventListener(this.data.controllerEvent, this.controllerEventListener);
+        oldData.controller.removeEventListener(this.data.controllerEndEvent, this.controllerEndEventListener);
+      }
+
+      this.data.controller.addEventListener(this.data.controllerEvent, this.controllerEventListener);
+      this.data.controller.addEventListener(this.data.controllerEndEvent, this.controllerEndEventListener);
+    }
+
+    if (oldData.otherHand && this.data.otherHand !== oldData.otherHand) {
+      this._handleModelLoaded();
+    }
+  },
+
+  play: function() {
+    document.addEventListener("mousedown", this.mouseDownListener);
+    document.addEventListener("mousemove", this.mouseMoveListener);
+    document.addEventListener("mouseup", this.mouseUpListener);
+    document.addEventListener("wheel", this.wheelListener);
+
+    window.addEventListener("enter-vr", this.enterVRListener);
+    window.addEventListener("exit-vr", this.exitVRListener);
+
+    this.el.addEventListener("raycaster-intersection", this.raycasterIntersectionListener);
+    this.el.addEventListener("raycaster-intersection-cleared", this.raycasterIntersectionClearedListener);
+
+    this.data.playerRig.addEventListener("model-loaded", this.modelLoadedListener);
+
+    //TODO: separate this into its own component? Or find an existing component that does this better.
+    this.checkForPointingDeviceInterval = setInterval(() => {
+      const controller = this._getController();
+      if (this.hasPointingDevice != !!controller) {
+        this.el.setAttribute("line", { visible: !!controller });
+      }
+      this.hasPointingDevice = !!controller;
+      if (controller && this.data.hand != controller.hand) {
+        this.el.setAttribute("cursor-controller", {
+          hand: controller.hand,
+          controller: `#player-${controller.hand}-controller`,
+          otherHand: `#${controller.hand}-super-hand`
+        });
+      }
+    }, 1000);
+  },
+
+  pause: function() {
+    document.removeEventListener("mousedown", this.mouseDownListener);
+    document.removeEventListener("mousemove", this.mouseMoveListener);
+    document.removeEventListener("mouseup", this.mouseUpListener);
+    document.removeEventListener("wheel", this.wheelListener);
+
+    window.removeEventListener("enter-vr", this.enterVRListener);
+    window.removeEventListener("exit-vr", this.exitVRListener);
+
+    this.el.removeEventListener("raycaster-intersection", this.raycasterIntersectionListener);
+    this.el.removeEventListener("raycaster-intersection-cleared", this.raycasterIntersectionClearedListener);
+
+    this.data.controller.removeEventListener(this.data.controllerEvent, this.controllerEventListener);
+    this.data.controller.removeEventListener(this.data.controllerEndEvent, this.controllerEndEventListener);
+
+    this.data.playerRig.removeEventListener("model-loaded", this.modelLoadedListener);
+
+    clearInterval(this.checkForPointingDeviceInterval);
+  },
+
+  tick: function() {
+    this.isGrabbing = this.data.cursor.components["super-hands"].state.has("grab-start");
+
+    if (this.otherHand) {
+      const state = this.otherHand.components["super-hands"].state;
+      const isOtherHandGrabbing = state.has("grab-start") || state.has("hover-start");
+      if (this.wasOtherHandGrabbing != isOtherHandGrabbing) {
+        this.data.cursor.setAttribute("visible", !isOtherHandGrabbing);
+      }
+      this.wasOtherHandGrabbing = isOtherHandGrabbing;
+    }
+
+    if (this.wasOtherHandGrabbing) return;
+
+    const camera = this.data.camera.components.camera.camera;
+    if (!this.inVR && !this.isMobile) {
+      //mouse
+      const raycaster = this.el.components.raycaster.raycaster;
+      raycaster.setFromCamera(this.mousePos, camera);
+      this.origin = raycaster.ray.origin;
+      this.direction = raycaster.ray.direction;
+    } else if ((this.inVR || this.isMobile) && !this.hasPointingDevice) {
+      //gaze
+      camera.getWorldPosition(this.origin);
+      camera.getWorldDirection(this.direction);
+    } else {
+      //3d
+      this.data.controller.object3D.getWorldPosition(this.origin);
+      this.data.controller.object3D.getWorldQuaternion(this.controllerQuaternion);
+      this.direction
+        .set(0, 0, -1)
+        .applyQuaternion(this.controllerQuaternion)
+        .normalize();
+    }
+
+    this.el.setAttribute("raycaster", { origin: this.origin, direction: this.direction });
+
+    let intersection = null;
+
+    if (!this.isGrabbing) {
+      const intersections = this.el.components.raycaster.intersections;
+      if (intersections.length > 0 && intersections[0].distance <= this.data.maxDistance) {
+        intersection = intersections[0];
+        this.data.cursor.object3D.position.copy(intersection.point);
+        this.currentDistance = intersections[0].distance;
+        this.currentDistanceMod = 0;
+      } else {
+        this.currentDistance = this.data.maxDistance;
+      }
+    }
+
+    if (this.isGrabbing || !intersection) {
+      const distance = Math.min(
+        Math.max(this.data.minDistance, this.currentDistance - this.currentDistanceMod),
+        this.data.maxDistance
+      );
+      this.currentDistanceMod = this.currentDistance - distance;
+      this.direction.multiplyScalar(distance);
+      this.point.addVectors(this.origin, this.direction);
+      this.data.cursor.object3D.position.copy(this.point);
+    }
+
+    if (intersection) {
+      if (this._isInteractableAllowed() && this._isClass("interactable", intersection.object.el)) {
+        this.currentTargetType = TARGET_TYPE_INTERACTABLE;
+      } else if (this._isClass("ui", intersection.object.el)) {
+        this.currentTargetType = TARGET_TYPE_UI;
+      }
+    } else {
+      this.currentTargetType = TARGET_TYPE_NONE;
+    }
+
+    const isTarget = this._isTargetOfType(TARGET_TYPE_INTERACTABLE_OR_UI);
+    if ((this.isGrabbing || isTarget) && !this.wasIntersecting) {
+      this.wasIntersecting = true;
+      this.data.cursor.setAttribute("material", { color: this.data.cursorColorHovered });
+    } else if (!this.isGrabbing && !isTarget && this.wasIntersecting) {
+      this.wasIntersecting = false;
+      this.data.cursor.setAttribute("material", { color: this.data.cursorColorUnhovered });
+    }
+
+    if (this.hasPointingDevice) {
+      this.el.setAttribute("line", { start: this.origin.clone(), end: this.data.cursor.object3D.position.clone() });
+    }
+  },
+
+  _isClass: function(className, el) {
+    return (
+      el.className === className ||
+      (el.parentNode && el.parentNode != el.sceneEl && this._isClass(className, el.parentNode))
+    );
+  },
+
+  _isInteractableAllowed: function() {
+    return !(this.inVR && this.hasPointingDevice) || this.isMobile;
+  },
+
+  _isTargetOfType: function(mask) {
+    return (this.currentTargetType & mask) === this.currentTargetType;
+  },
+
+  _getController: function() {
+    //TODO: prefer initial hand set in data.hand
+    for (let i = this.data.trackedControls.length - 1; i >= 0; i--) {
+      const trackedControlsComponent = this.data.trackedControls[i].components["tracked-controls"];
+      if (trackedControlsComponent && trackedControlsComponent.controller) {
+        return trackedControlsComponent.controller;
+      }
+    }
+    return null;
+  },
+
+  _startTeleport: function() {
+    this.data.controller.emit(this.data.teleportEvent, {});
+    this.el.setAttribute("line", { visible: false });
+    this.data.cursor.setAttribute("visible", false);
+  },
+
+  _endTeleport: function() {
+    this.data.controller.emit(this.data.teleportEndEvent, {});
+    this.el.setAttribute("line", { visible: true });
+    this.data.cursor.setAttribute("visible", true);
+  },
+
+  _handleMouseDown: function() {
+    if (this._isTargetOfType(TARGET_TYPE_INTERACTABLE)) {
+      const lookControls = this.data.camera.components["look-controls"];
+      if (lookControls) lookControls.pause();
+      this.data.cursor.emit(this.data.controllerEvent, {});
+    } else if (this.inVR && this.hasPointingDevice) {
+      this._startTeleport();
+    }
+  },
+
+  _handleMouseMove: function(e) {
+    this.mousePos.set(e.clientX / window.innerWidth * 2 - 1, -(e.clientY / window.innerHeight) * 2 + 1);
+  },
+
+  _handleMouseUp: function() {
+    const lookControls = this.data.camera.components["look-controls"];
+    if (lookControls) lookControls.play();
+    this.data.cursor.emit(this.data.controllerEndEvent, {});
+    if (this.inVR && this.hasPointingDevice) {
+      this._endTeleport();
+    }
+  },
+
+  _handleWheel: function(e) {
+    if (this.isGrabbing) {
+      switch (e.deltaMode) {
+        case e.DOM_DELTA_PIXEL:
+          this.currentDistanceMod += e.deltaY / 500;
+          break;
+        case e.DOM_DELTA_LINE:
+          this.currentDistanceMod += e.deltaY / 10;
+          break;
+        case e.DOM_DELTA_PAGE:
+          this.currentDistanceMod += e.deltaY / 2;
+          break;
+      }
+    }
+  },
+
+  _handleEnterVR: function() {
+    if (AFRAME.utils.device.checkHeadsetConnected()) {
+      this.inVR = true;
+    }
+  },
+
+  _handleExitVR: function() {
+    this.inVR = false;
+  },
+
+  _handleRaycasterIntersection: function(e) {
+    this.data.cursor.emit("raycaster-intersection", e.detail);
+  },
+
+  _handleRaycasterIntersectionCleared: function(e) {
+    this.data.cursor.emit("raycaster-intersection-cleared", e.detail);
+  },
+
+  _handleControllerEvent: function(e) {
+    switch (this.currentTargetType) {
+      case TARGET_TYPE_INTERACTABLE:
+        if (!this._isInteractableAllowed()) {
+          break;
+        }
+      case TARGET_TYPE_UI:
+        this.data.cursor.emit(this.data.controllerEvent, e.detail);
+        break;
+      default:
+        this._startTeleport();
+        break;
+    }
+  },
+
+  _handleControllerEndEvent: function(e) {
+    if (this.isGrabbing || this._isTargetOfType(TARGET_TYPE_UI)) {
+      this.data.cursor.emit(this.data.controllerEndEvent, e.detail);
+    } else {
+      this._endTeleport();
+    }
+  },
+
+  _handleModelLoaded: function() {
+    this.otherHand = this.data.playerRig.querySelector(this.data.otherHand);
+  }
+});
diff --git a/src/components/event-repeater.js b/src/components/event-repeater.js
index 64e287206..eed848d26 100644
--- a/src/components/event-repeater.js
+++ b/src/components/event-repeater.js
@@ -24,6 +24,6 @@ AFRAME.registerComponent("event-repeater", {
   },
 
   _handleEvent: function(event, e) {
-    this.el.emit(event, e.details);
+    this.el.emit(event, e.detail ? e.detail : {});
   }
 });
diff --git a/src/components/super-cursor.js b/src/components/super-cursor.js
deleted file mode 100644
index ee70e9606..000000000
--- a/src/components/super-cursor.js
+++ /dev/null
@@ -1,168 +0,0 @@
-AFRAME.registerComponent("super-cursor", {
-  dependencies: ["raycaster"],
-  schema: {
-    cursor: { type: "selector" },
-    camera: { type: "selector" },
-    maxDistance: { default: 3 },
-    minDistance: { default: 0.5 },
-    cursorColorHovered: { default: "#FF0000" },
-    cursorColorUnhovered: { efault: "#FFFFFF" }
-  },
-
-  init: function() {
-    this.isGrabbing = false;
-    this.isInteractable = false;
-    this.wasIntersecting = false;
-    this.currentDistance = this.data.maxDistance;
-    this.currentDistanceMod = 0;
-    this.enabled = true;
-    this.origin = new THREE.Vector3();
-    this.direction = new THREE.Vector3();
-    this.point = new THREE.Vector3();
-    this.mousePos = new THREE.Vector2();
-
-    this.data.cursor.setAttribute("material", { color: this.data.cursorColorUnhovered });
-
-    this.mouseDownListener = this._handleMouseDown.bind(this);
-    this.mouseMoveListener = this._handleMouseMove.bind(this);
-    this.mouseUpListener = this._handleMouseUp.bind(this);
-    this.wheelListener = this._handleWheel.bind(this);
-    this.enterVRListener = this._handleEnterVR.bind(this);
-    this.exitVRListener = this._handleExitVR.bind(this);
-  },
-
-  play: function() {
-    document.addEventListener("mousedown", this.mouseDownListener);
-    document.addEventListener("mousemove", this.mouseMoveListener);
-    document.addEventListener("mouseup", this.mouseUpListener);
-    document.addEventListener("wheel", this.wheelListener);
-    window.addEventListener("enter-vr", this.enterVRListener);
-    window.addEventListener("exit-vr", this.exitVRListener);
-
-    this._enable();
-  },
-
-  pause: function() {
-    document.removeEventListener("mousedown", this.mouseDownListener);
-    document.removeEventListener("mousemove", this.mouseMoveListener);
-    document.removeEventListener("mouseup", this.mouseUpListener);
-    document.removeEventListener("wheel", this.wheelListener);
-    window.removeEventListener("enter-vr", this.enterVRListener);
-    window.removeEventListener("exit-vr", this.exitVRListener);
-
-    this._disable();
-  },
-
-  tick: function() {
-    if (!this.enabled) {
-      return;
-    }
-
-    this.isGrabbing = this.data.cursor.components["super-hands"].state.has("grab-start");
-
-    const camera = this.data.camera.components.camera.camera;
-    const raycaster = this.el.components.raycaster.raycaster;
-    raycaster.setFromCamera(this.mousePos, camera);
-    this.origin = raycaster.ray.origin;
-    this.direction = raycaster.ray.direction;
-    this.el.setAttribute("raycaster", { origin: this.origin, direction: this.direction });
-
-    let intersection = null;
-
-    if (!this.isGrabbing) {
-      const intersections = this.el.components.raycaster.intersections;
-      if (intersections.length > 0 && intersections[0].distance <= this.data.maxDistance) {
-        intersection = intersections[0];
-        this.data.cursor.object3D.position.copy(intersection.point);
-        this.currentDistance = intersections[0].distance;
-        this.currentDistanceMod = 0;
-      } else {
-        this.currentDistance = this.data.maxDistance;
-      }
-    }
-
-    if (this.isGrabbing || !intersection) {
-      const distance = Math.min(
-        Math.max(this.data.minDistance, this.currentDistance - this.currentDistanceMod),
-        this.data.maxDistance
-      );
-      this.currentDistanceMod = this.currentDistance - distance;
-      this.direction.multiplyScalar(distance);
-      this.point.addVectors(this.origin, this.direction);
-      this.data.cursor.object3D.position.copy(this.point);
-    }
-
-    this.isInteractable = intersection && this._isInteractable(intersection.object.el);
-
-    if ((this.isGrabbing || this.isInteractable) && !this.wasIntersecting) {
-      this.wasIntersecting = true;
-      this.data.cursor.setAttribute("material", { color: this.data.cursorColorHovered });
-    } else if (!this.isGrabbing && !this.isInteractable && this.wasIntersecting) {
-      this.wasIntersecting = false;
-      this.data.cursor.setAttribute("material", { color: this.data.cursorColorUnhovered });
-    }
-  },
-
-  _isInteractable: function(el) {
-    return (
-      el.className === "interactable" ||
-      (el.parentNode && el.parentNode != el.sceneEl && this._isInteractable(el.parentNode))
-    );
-  },
-
-  _handleMouseDown: function() {
-    if (this.isInteractable) {
-      const lookControls = this.data.camera.components["look-controls"];
-      if (lookControls) lookControls.pause();
-    }
-    this.data.cursor.emit("action_grab", {});
-  },
-
-  _handleMouseMove: function(e) {
-    this.mousePos.set(e.clientX / window.innerWidth * 2 - 1, -(e.clientY / window.innerHeight) * 2 + 1);
-  },
-
-  _handleMouseUp: function() {
-    const lookControls = this.data.camera.components["look-controls"];
-    if (lookControls) lookControls.play();
-    this.data.cursor.emit("action_release", {});
-  },
-
-  _handleWheel: function(e) {
-    if (this.isGrabbing) {
-      switch (e.deltaMode) {
-        case e.DOM_DELTA_PIXEL:
-          this.currentDistanceMod += e.deltaY / 500;
-          break;
-        case e.DOM_DELTA_LINE:
-          this.currentDistanceMod += e.deltaY / 10;
-          break;
-        case e.DOM_DELTA_PAGE:
-          this.currentDistanceMod += e.deltaY / 2;
-          break;
-      }
-    }
-  },
-
-  _handleEnterVR: function() {
-    if (AFRAME.utils.device.checkHeadsetConnected() || AFRAME.utils.device.isMobile()) {
-      this._disable();
-    }
-  },
-
-  _handleExitVR: function() {
-    this._enable();
-  },
-
-  _enable: function() {
-    this.enabled = true;
-    this.data.cursor.setAttribute("visible", true);
-    this.el.setAttribute("raycaster", { enabled: true });
-  },
-
-  _disable: function() {
-    this.enabled = false;
-    this.data.cursor.setAttribute("visible", false);
-    this.el.setAttribute("raycaster", { enabled: false });
-  }
-});
diff --git a/src/components/super-spawner.js b/src/components/super-spawner.js
index bb6762a20..b67f60651 100644
--- a/src/components/super-spawner.js
+++ b/src/components/super-spawner.js
@@ -36,7 +36,6 @@ AFRAME.registerComponent("super-spawner", {
 
     const componentinInitializedListener = this._handleComponentInitialzed.bind(this, entity);
     const bodyLoadedListener = this._handleBodyLoaded.bind(this, entity);
-
     this.entities.set(entity, {
       hand: hand,
       componentInitialized: false,
@@ -68,8 +67,10 @@ AFRAME.registerComponent("super-spawner", {
   _emitEvents: function(entity) {
     const data = this.entities.get(entity);
     if (data.componentInitialized && data.bodyLoaded) {
-      data.hand.emit("action_grab", { targetEntity: entity });
-      entity.emit("grab-start", { hand: data.hand });
+      data.hand.emit("action_primary_down", { targetEntity: entity });
+      const eventData = { bubbles: true, cancelable: true, detail: { hand: data.hand, target: entity } };
+      const event = new CustomEvent("grab-start", eventData);
+      entity.dispatchEvent(event);
 
       entity.removeEventListener("componentinitialized", data.componentinInitializedListener);
       entity.removeEventListener("body-loaded", data.bodyLoadedListener);
diff --git a/src/hub.html b/src/hub.html
index 0f6df33ec..b69dec687 100644
--- a/src/hub.html
+++ b/src/hub.html
@@ -22,8 +22,6 @@
         physics
         mute-mic="eventSrc: a-scene; toggleEvents: action_mute"
         personal-space-bubble="debug: false;"
-
-        app-mode-input-mappings="modes: default, hud; actionSets: default, hud;"
         >
 
         <a-assets>
@@ -112,16 +110,17 @@
                     body="type: dynamic; shape: none; mass: 5;"
                     grabbable
                     stretchable="useWorldPosition: true;"
+                    hoverable
                 ></a-entity>
             </template>
 
             <a-mixin id="super-hands"
-                super-hands="colliderEvent: collisions; colliderEventProperty: els;
+                super-hands="
+                    colliderEvent: collisions; colliderEventProperty: els;
                     colliderEndEvent: collisions; colliderEndEventProperty: clearedEls;
-                    grabStartButtons: action_grab; grabEndButtons: action_release;
-                    stretchStartButtons: action_grab; stretchEndButtons: action_release;
-                    dragDropStartButtons: action_grab; dragDropEndButtons: action_release;
-                    "
+                    grabStartButtons: action_primary_down; grabEndButtons: action_primary_up;
+                    stretchStartButtons: action_primary_down; stretchEndButtons: action_primary_up;
+                    dragDropStartButtons: action_primary_down; dragDropEndButtons: action_primary_up;"
                 collision-filter="collisionForces: false"
                 physics-collider
             ></a-mixin>
@@ -137,22 +136,34 @@
             super-spawner="template: #interactable-template;" 
             position="2.9 1.2 0" 
             body="mass: 0; type: static; shape: box;"
+            hoverable
         ></a-entity>
 
         <a-entity
-            id="super-cursor"
-            super-cursor="cursor: #3d-cursor; camera: #player-camera;"
-            raycaster="objects: .collidable, .interactable; far: 10;"
-        >
-            <a-sphere
-                id="3d-cursor"
-                radius="0.02"
-                static-body="shape: sphere;"
-                mixin="super-hands"
-                segments-height="9"
-                segments-width="9"
-            ></a-sphere>  
-        </a-entity>
+            cursor-controller="
+                cursor: #cursor; 
+                camera: #player-camera; 
+                controller: #player-right-controller;
+                playerRig: #player-rig;
+                otherHand: #right-super-hand;"
+            raycaster="objects: .collidable, .interactable, .ui; far: 3;"
+            line="visible: false; color: white; opacity: 0.2;"
+        ></a-entity>
+
+        <a-sphere
+            id="cursor"
+            radius="0.02"
+            static-body="shape: sphere;"
+            collision-filter="collisionForces: false"
+            super-hands="
+                colliderEvent: raycaster-intersection; colliderEventProperty: els;
+                colliderEndEvent: raycaster-intersection-cleared; colliderEndEventProperty: clearedEls;
+                grabStartButtons: action_primary_down; grabEndButtons: action_primary_up;
+                stretchStartButtons: action_primary_down; stretchEndButtons: action_primary_up;
+                dragDropStartButtons: action_primary_down; dragDropEndButtons: action_primary_up;"
+            segments-height="9"
+            segments-width="9"
+        ></a-sphere> 
 
         <!-- Player Rig -->
         <a-entity
@@ -162,13 +173,11 @@
             wasd-to-analog2d
             character-controller="pivot: #player-camera"
             ik-root
-            app-mode-toggle-playing__character-controller="mode: hud; invert: true;"
-            app-mode-toggle-playing__wasd-to-analog2d="mode: hud; invert: true;"
             player-info
         >
-
             <a-entity
                 id="player-hud"
+                class="ui"
                 hud-controller="head: #player-camera;"
                 vr-mode-toggle-visibility
                 vr-mode-toggle-playing__hud-controller
@@ -199,7 +208,6 @@
                     teleportOrigin: #player-camera; 
                     button: action_teleport_; 
                     collisionEntities: [nav-mesh]"
-                app-mode-toggle-playing__teleport-controls="mode: hud; invert: true;"
                 haptic-feedback
             ></a-entity>
 
@@ -214,13 +222,6 @@
                     button: action_teleport_; 
                     collisionEntities: [nav-mesh]"
                 haptic-feedback
-                raycaster="objects:.hud; showLine: true; far: 2;"
-                cursor="fuse: false; downEvents: action_ui_select_down; upEvents: action_ui_select_up;"
-
-                app-mode-toggle-playing__teleport-controls="mode: hud; invert: true;"
-                app-mode-toggle-playing__raycaster="mode: hud;"
-                app-mode-toggle-playing__cursor="mode: hud;"
-                app-mode-toggle-attribute__line="mode: hud; property: visible;"
             ></a-entity>
 
             <a-entity gltf-model-plus="inflate: true;" class="model">
@@ -248,7 +249,8 @@
                 <template data-selector=".LeftHand">
                     <a-entity bone-visibility>
                         <a-entity
-                            event-repeater="events: action_grab, action_release; eventSource: #player-left-controller"
+                            id="left-super-hand"
+                            event-repeater="events: action_primary_down, action_primary_up; eventSource: #player-left-controller"
                             static-body="shape: sphere; sphereRadius: 0.02"
                             mixin="super-hands"
                             position="0 0.05 0"
@@ -259,7 +261,8 @@
                 <template data-selector=".RightHand">
                     <a-entity bone-visibility>
                         <a-entity
-                            event-repeater="events: action_grab, action_release; eventSource: #player-right-controller"
+                            id="right-super-hand"
+                            event-repeater="events: action_primary_down, action_primary_up; eventSource: #player-right-controller"
                             static-body="shape: sphere; sphereRadius: 0.02"
                             mixin="super-hands"
                             position="0 -0.05 0"
diff --git a/src/hub.js b/src/hub.js
index 9dc3247e3..5074e8e3d 100644
--- a/src/hub.js
+++ b/src/hub.js
@@ -76,9 +76,10 @@ import "super-hands";
 import "./components/super-networked-interactable";
 import "./components/networked-counter";
 import "./components/super-spawner";
-import "./components/super-cursor";
 import "./components/event-repeater";
 
+import "./components/cursor-controller";
+
 import "./components/nav-mesh-helper";
 
 import registerNetworkSchemas from "./network-schemas";
diff --git a/src/input-mappings.js b/src/input-mappings.js
index 9296fd71f..09160b0ec 100644
--- a/src/input-mappings.js
+++ b/src/input-mappings.js
@@ -40,18 +40,18 @@ const config = {
         "trackpad.pressedmove": { left: "move" },
         trackpad_dpad4_pressed_west_down: { right: "snap_rotate_left" },
         trackpad_dpad4_pressed_east_down: { right: "snap_rotate_right" },
-        trackpad_dpad4_pressed_center_down: { right: "action_teleport_down" },
-        trackpad_dpad4_pressed_north_down: { right: "action_teleport_down" },
-        trackpad_dpad4_pressed_south_down: { right: "action_teleport_down" },
-        trackpadup: { right: "action_teleport_up" },
+        trackpad_dpad4_pressed_center_down: { right: "action_primary_down" },
+        trackpad_dpad4_pressed_north_down: { right: "action_primary_down" },
+        trackpad_dpad4_pressed_south_down: { right: "action_primary_down" },
+        trackpadup: { right: "action_primary_up" },
         menudown: "thumb_down",
         menuup: "thumb_up",
-        gripdown: ["action_grab", "middle_ring_pinky_down", "index_down"],
-        gripup: ["action_release", "middle_ring_pinky_up", "index_up"],
+        gripdown: ["action_primary_down", "middle_ring_pinky_down", "index_down"],
+        gripup: ["action_primary_up", "middle_ring_pinky_up", "index_up"],
         trackpadtouchstart: "thumb_down",
         trackpadtouchend: "thumb_up",
-        triggerdown: ["action_grab", "index_down"],
-        triggerup: ["action_release", "index_up"]
+        triggerdown: ["action_primary_down", "index_down"],
+        triggerup: ["action_primary_up", "index_up"]
       },
       "oculus-touch-controls": {
         joystick_dpad4_west: {
@@ -60,8 +60,8 @@ const config = {
         joystick_dpad4_east: {
           right: "snap_rotate_right"
         },
-        gripdown: ["action_grab", "middle_ring_pinky_down"],
-        gripup: ["action_release", "middle_ring_pinky_up"],
+        gripdown: ["action_primary_down", "middle_ring_pinky_down"],
+        gripup: ["action_primary_up", "middle_ring_pinky_up"],
         abuttontouchstart: "thumb_down",
         abuttontouchend: "thumb_up",
         bbuttontouchstart: "thumb_down",
@@ -74,27 +74,27 @@ const config = {
         surfacetouchend: "thumb_up",
         thumbsticktouchstart: "thumb_down",
         thumbsticktouchend: "thumb_up",
-        triggerdown: "index_down",
-        triggerup: "index_up",
+        triggerdown: ["action_primary_down", "index_down"],
+        triggerup: ["action_primary_up", "index_up"],
         "axismove.reverseY": { left: "move" },
-        abuttondown: "action_teleport_down",
-        abuttonup: "action_teleport_up"
+        abuttondown: "action_primary_down",
+        abuttonup: "action_primary_up"
       },
       "daydream-controls": {
         trackpad_dpad4_pressed_west_down: "snap_rotate_left",
         trackpad_dpad4_pressed_east_down: "snap_rotate_right",
-        trackpad_dpad4_pressed_center_down: "action_teleport_down",
-        trackpad_dpad4_pressed_north_down: "action_teleport_down",
-        trackpad_dpad4_pressed_south_down: "action_teleport_down",
-        trackpadup: "action_teleport_up"
+        trackpad_dpad4_pressed_center_down: ["action_primary_down"],
+        trackpad_dpad4_pressed_north_down: ["action_primary_down"],
+        trackpad_dpad4_pressed_south_down: ["action_primary_down"],
+        trackpadup: ["action_primary_up"]
       },
       "gearvr-controls": {
         trackpad_dpad4_pressed_west_down: "snap_rotate_left",
         trackpad_dpad4_pressed_east_down: "snap_rotate_right",
-        trackpad_dpad4_pressed_center_down: "action_teleport_down",
-        trackpad_dpad4_pressed_north_down: "action_teleport_down",
-        trackpad_dpad4_pressed_south_down: "action_teleport_down",
-        trackpadup: "action_teleport_up"
+        trackpad_dpad4_pressed_center_down: ["action_primary_down"],
+        trackpad_dpad4_pressed_north_down: ["action_primary_down"],
+        trackpad_dpad4_pressed_south_down: ["action_primary_down"],
+        trackpadup: ["action_primary_up"]
       },
       keyboard: {
         m_press: "action_mute",
-- 
GitLab