diff --git a/public/index.html b/public/index.html
index 55de75fee265c1f5cd022d42f134d4d12b12986b..6f23a9b0e66ddb637564801e81d092255a482545 100644
--- a/public/index.html
+++ b/public/index.html
@@ -24,53 +24,37 @@
       <script id="hand-template" type="text/html">
         <a-box class="hand" scale="0.2 0.1 0.3"></a-box>
       </script>
-
-      <script id="vive-rig" type="text/html">
-        <a-entity id="player-rig" networked wasd-controls>
-          <a-entity id="head" camera="userHeight: 1.6" look-controls networked="template:#head-template;showLocalTemplate:false;"></a-entity>
-
-          <a-entity id="left-hand" vive-controls="hand: left" teleport-controls="cameraRig: #player-rig" networked="template:#hand-template;showLocalTemplate:false;"></a-entity>
-          <a-entity id="right-hand" vive-controls="hand: right" teleport-controls="cameraRig: #player-rig" networked="template:#hand-template;showLocalTemplate:false;"></a-entity>
-        </a-entity>
-      </script>
-
-      <script id="oculus-rig" type="text/html">
-        <a-entity id="player-rig" networked wasd-controls>
-          <a-entity id="head" camera="userHeight: 1.6" look-controls networked="template:#head-template;showLocalTemplate:false;"></a-entity>
-
-          <a-entity oculus-touch-controls="left" teleport-controls="cameraRig:#player-rig;button:trigger;" networked="template:#hand-template;showLocalTemplate:false;"></a-entity>
-          <a-entity oculus-touch-controls="right" teleport-controls="cameraRig:#player-rig;button:trigger;" networked="template:#hand-template;showLocalTemplate:false;"></a-entity>
-        </a-entity>
-      </script>
-
-      <script id="daydream-rig" type="text/html">
-        <a-entity id="player-rig" networked wasd-controls>
-          <a-entity id="head" camera="userHeight: 1.6" look-controls networked="template:#head-template;showLocalTemplate:false;"></a-entity>
-
-          <a-entity daydream-controls networked="template:#hand-template;showLocalTemplate:true;"></a-entity>
-        </a-entity>
-      </script>
-
-      <script id="gearvr-rig" type="text/html">
-        <a-entity id="player-rig" networked wasd-controls>
-          <a-entity id="head" camera="userHeight: 1.6" look-controls networked="template:#head-template;showLocalTemplate:false;"></a-entity>
-
-          <a-entity gearvr-controls networked="template:#hand-template;showLocalTemplate:true;"></a-entity>
-        </a-entity>
-      </script>
-
-      <script id="dolly-rig" type="text/html">
-        <a-entity id="player-rig" networked wasd-controls>
-          <a-entity id="head" camera="userHeight: 1.6" look-controls networked="template:#head-template;showLocalTemplate:false;"></a-entity>
-        </a-entity>
-      </script>
     </a-assets>
 
-    <a-entity rig-selector="vive:#vive-rig;oculus:oculus-rig;daydream:#daydream-rig;desktop:#dolly-rig;mobile:dolly-rig;"></a-entity>
+    <a-entity id="player-rig" networked wasd-controls snap-rotation="leftEventSrc: #right-hand; rightEventSrc: #right-hand">
+        <a-entity
+            id="head"
+            camera="userHeight: 1.6"
+            look-controls
+            networked="template:#head-template;showLocalTemplate:false;"></a-entity>
+
+        <a-entity
+            id="left-hand"
+            vive-controls="hand: left"
+            oculus-touch-controls="left"
+            axis-dpad="centerZone: 1"
+            teleport-controls="cameraRig: #player-rig; button: dpadcenter"
+            networked="template:#hand-template;showLocalTemplate:false;"></a-entity>
+
+        <a-entity
+            id="right-hand"
+            vive-controls="hand: right"
+            oculus-touch-controls="right"
+            daydream-controls
+            gearvr-controls
+            axis-dpad
+            teleport-controls="cameraRig: #player-rig; button: dpadcenter"
+            networked="template:#hand-template;showLocalTemplate:false;"></a-entity>
+    </a-entity>
 
     <a-entity id="ground" position="0 0 0"
-        geometry="primitive: plane; width: 10000; height: 10000;" rotation="-90 0 0"
-        material="shader: flat; src: #grid; repeat: 10000 10000;"></a-entity>
+        geometry="primitive: plane; width: 100; height: 100;" rotation="-90 0 0"
+        material="shader: flat; src: #grid; repeat: 100 100;"></a-entity>
     <a-sky src="#sky" rotation="0 -90 0"></a-sky>
   </a-scene>
 </body>
diff --git a/src/components/axis-dpad.js b/src/components/axis-dpad.js
new file mode 100644
index 0000000000000000000000000000000000000000..90fb29176013384f42d3c2da02631a1f0a88c634
--- /dev/null
+++ b/src/components/axis-dpad.js
@@ -0,0 +1,81 @@
+/**
+ * @fileOverview
+ * Treats a pair of axes and a button as a dpad
+ * This is useful for Vive trackpad and Oculus Touch thumbstick
+ *
+ * @name axis-dpad.js
+ * @TODO allow use of thumbstick without press
+ * @TODO make axes configurable
+ */
+
+const angleToDirection = function(angle) {
+  angle = (angle * THREE.Math.RAD2DEG + 180 + 45) % 360;
+  if (angle > 0 && angle < 90) {
+    return "down";
+  } else if (angle >= 90 && angle < 180) {
+    return "left";
+  } else if (angle >= 180 && angle < 270) {
+    return "up";
+  } else {
+    return "right";
+  }
+};
+
+AFRAME.registerComponent("axis-dpad", {
+  schema: {
+    centerZone: { default: 0.5 },
+    moveEvents: { default: ["axismove"] },
+    downEvents: { default: ["trackpaddown", "thumbstickdown"] },
+    upEvents: { default: ["trackpadup", "thumbstickup"] }
+  },
+
+  init: function() {
+    this.onAxisMove = this.onAxisMove.bind(this);
+    this.onButtonPressed = this.onButtonPressed.bind(this);
+    this.lastPos = [0, 0];
+  },
+
+  play: function() {
+    const { moveEvents, downEvents, upEvents } = this.data;
+    moveEvents.forEach(moveEvent => {
+      this.el.addEventListener(moveEvent, this.onAxisMove);
+    });
+    downEvents.concat(upEvents).forEach(eventName => {
+      this.el.addEventListener(eventName, this.onButtonPressed);
+    });
+  },
+
+  pause: function() {
+    const { moveEvents, downEvents, upEvents } = this.data;
+    moveEvents.forEach(moveEvent => {
+      this.el.removeEventListener(moveEvent, this.onAxisMove);
+    });
+    downEvents.concat(upEvents).forEach(eventName => {
+      this.el.removeEventListener(eventName, this.onButtonPressed);
+    });
+  },
+
+  onAxisMove: function(e) {
+    this.lastPos = e.detail.axis;
+  },
+
+  onButtonPressed: function(e) {
+    const [x, y] = this.lastPos;
+    const { upEvents, centerZone } = this.data;
+    const state = upEvents.includes(e.type) ? "up" : "down";
+    const direction =
+      state === "up" && this.lastDirection // Always trigger the up event for the last down event
+        ? this.lastDirection
+        : x * x + y * y < centerZone * centerZone // If within center zone angle does not matter
+          ? "center"
+          : angleToDirection(Math.atan2(x, y));
+
+    this.el.emit(`dpad${direction}${state}`);
+
+    if (state === "down") {
+      this.lastDirection = direction;
+    } else {
+      delete this.lastDirection;
+    }
+  }
+});
diff --git a/src/components/snap-rotation.js b/src/components/snap-rotation.js
new file mode 100644
index 0000000000000000000000000000000000000000..5836a55965601d1aa05f700196c6ad95e75b2e28
--- /dev/null
+++ b/src/components/snap-rotation.js
@@ -0,0 +1,62 @@
+/**
+ * @fileOverview
+ * Rotate an entity in fixed increments based on events or keyboard input
+ * @name snap-rotation.js
+ * @TODO pull keyboard input out into a component that just emits events
+ * @TODO allow specifying multiple events and sources
+ */
+
+AFRAME.registerComponent("snap-rotation", {
+  schema: {
+    rotationAxis: { type: "vec3", default: { x: 0, y: 1, z: 0 } },
+    rotationDegres: { default: 45 },
+
+    leftKey: { default: "q" },
+    leftEvent: { default: "dpadleftdown" },
+    leftEventSrc: { type: "selector" },
+
+    rightKey: { default: "e" },
+    rightEvent: { default: "dpadrightdown" },
+    rightEventSrc: { type: "selector" }
+  },
+
+  init: function() {
+    this.onButtonPressed = this.onButtonPressed.bind(this);
+  },
+
+  play: function() {
+    const { leftEventSrc, leftEvent, rightEventSrc, rightEvent } = this.data;
+    window.addEventListener("keypress", this.onButtonPressed);
+    rightEventSrc &&
+      rightEventSrc.addEventListener(rightEvent, this.onButtonPressed);
+    leftEventSrc &&
+      leftEventSrc.addEventListener(leftEvent, this.onButtonPressed);
+  },
+
+  pause: function() {
+    const { leftEventSrc, leftEvent, rightEventSrc, rightEvent } = this.data;
+    window.removeEventListener("keypress", this.onButtonPRessed);
+    rightEventSrc &&
+      rightEventSrc.removeEventListener(rightEvent, this.onButtonPressed);
+    leftEventSrc &&
+      leftEventSrc.removeEventListener(leftEvent, this.onButtonPressed);
+  },
+
+  onButtonPressed: function(e) {
+    const {
+      rotationAxis,
+      rotationDegres,
+      leftKey,
+      leftEvent,
+      rightKey,
+      rightEvent
+    } = this.data;
+    const obj = this.el.object3D;
+
+    if (e.type === leftEvent || (leftKey && e.key === leftKey)) {
+      obj.rotateOnAxis(rotationAxis, rotationDegres * THREE.Math.DEG2RAD);
+    } else if (e.type === rightEvent || (rightKey && e.key === rightKey)) {
+      obj.rotateOnAxis(rotationAxis, -rotationDegres * THREE.Math.DEG2RAD);
+    }
+  }
+});
diff --git a/src/index.js b/src/index.js
index e379fd736749cf5cf54cff18465d301eeaba1106..85a49209aa9b82aa5ba6cbf2b2366d9e119367ba 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,3 +1,6 @@
 require("networked-aframe");
-require("./components/rig-selector");
-require('aframe-teleport-controls');
+require("aframe-teleport-controls");
+
+// require("./components/rig-selector");
+require("./components/axis-dpad");
+require("./components/snap-rotation");