diff --git a/interface/resources/controllers/vive.json b/interface/resources/controllers/vive.json
index 1f71658946..37f5b45b1e 100644
--- a/interface/resources/controllers/vive.json
+++ b/interface/resources/controllers/vive.json
@@ -1,15 +1,15 @@
 {
     "name": "Vive to Standard",
     "channels": [
-        { "from": "Vive.LY",  "when": "Vive.LS", "filters": "invert", "to": "Standard.LY" },
-        { "from": "Vive.LX",  "when": "Vive.LS", "to": "Standard.LX" },
+        { "from": "Vive.LY",  "when": "Vive.LS", "filters": ["invert" ,{ "type": "deadZone", "min": 0.6 }], "to": "Standard.LY" },
+        { "from": "Vive.LX",  "when": "Vive.LS", "filters": [{ "type": "deadZone", "min": 0.6 }], "to": "Standard.LX" },
 
         { "from": "Vive.LT", "to": "Standard.LT" },
         { "from": "Vive.LB", "to": "Standard.LB" },
         { "from": "Vive.LS", "to": "Standard.LS" },
 
-        { "from": "Vive.RY",  "when": "Vive.RS", "filters": "invert", "to": "Standard.RY" },
-        { "from": "Vive.RX",  "when": "Vive.RS", "to": "Standard.RX" },
+        { "from": "Vive.RY",  "when": "Vive.RS", "filters": ["invert", { "type": "deadZone", "min": 0.6 }], "to": "Standard.RY" },
+        { "from": "Vive.RX",  "when": "Vive.RS", "filters": [{ "type": "deadZone", "min": 0.6 }], "to": "Standard.RX" },
 
         { "from": "Vive.RT", "to": "Standard.RT" },
         { "from": "Vive.RB", "to": "Standard.RB" },
diff --git a/libraries/controllers/src/controllers/impl/filters/DeadZoneFilter.cpp b/libraries/controllers/src/controllers/impl/filters/DeadZoneFilter.cpp
index 809308eeab..f07ef25976 100644
--- a/libraries/controllers/src/controllers/impl/filters/DeadZoneFilter.cpp
+++ b/libraries/controllers/src/controllers/impl/filters/DeadZoneFilter.cpp
@@ -13,11 +13,12 @@
 
 using namespace controller;
 float DeadZoneFilter::apply(float value) const {
-    float scale = 1.0f / (1.0f - _min);
-    if (std::abs(value) < _min) {
+    float scale = ((value < 0.0f) ? -1.0f : 1.0f) / (1.0f - _min);
+    float magnitude = std::abs(value);
+    if (magnitude < _min) {
         return 0.0f;
     }
-    return (value - _min) * scale;
+    return (magnitude - _min) * scale;
 }
 
 bool DeadZoneFilter::parseParameters(const QJsonValue& parameters) {
diff --git a/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp b/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp
index 4648fc8957..f9d527de8f 100644
--- a/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp
+++ b/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp
@@ -16,6 +16,8 @@
 #include <QtWidgets/QApplication>
 #include <QtWidgets/QDesktopWidget>
 #include <glm/gtc/type_ptr.hpp>
+#include <QtGui/QWindow>
+#include <QQuickWindow>
 
 #include <ui/Menu.h>
 #include <NumericalConstants.h>
@@ -256,7 +258,7 @@ glm::vec2 CompositorHelper::getReticlePosition() const {
         QMutexLocker locker(&_reticleLock);
         return _reticlePositionInHMD;
     }
-    return toGlm(QCursor::pos());
+    return toGlm(_renderingWidget->mapFromGlobal(QCursor::pos()));
 }
 
 bool CompositorHelper::getReticleOverDesktop() const {
@@ -322,17 +324,8 @@ void CompositorHelper::setReticlePosition(const glm::vec2& position, bool sendFa
             sendFakeMouseEvent();
         }
     } else {
-        // NOTE: This is some debugging code we will leave in while debugging various reticle movement strategies,
-        // remove it after we're done
-        const float REASONABLE_CHANGE = 50.0f;
-        glm::vec2 oldPos = toGlm(QCursor::pos());
-        auto distance = glm::distance(oldPos, position);
-        if (distance > REASONABLE_CHANGE) {
-            qDebug() << "Contrller::ScriptingInterface ---- UNREASONABLE CHANGE! distance:" <<
-                distance << " oldPos:" << oldPos.x << "," << oldPos.y << " newPos:" << position.x << "," << position.y;
-        }
-
-        QCursor::setPos(position.x, position.y);
+        const QPoint point(position.x, position.y);
+        QCursor::setPos(_renderingWidget->mapToGlobal(point));
     }
 }
 
diff --git a/scripts/defaultScripts.js b/scripts/defaultScripts.js
index 9b11fa73cc..2a050d183e 100644
--- a/scripts/defaultScripts.js
+++ b/scripts/defaultScripts.js
@@ -17,7 +17,7 @@ Script.load("system/edit.js");
 Script.load("system/selectAudioDevice.js");
 Script.load("system/notifications.js");
 Script.load("system/controllers/handControllerGrab.js");
+Script.load("system/controllers/handControllerPointer.js");
 Script.load("system/controllers/squeezeHands.js");
 Script.load("system/controllers/grab.js");
 Script.load("system/dialTone.js");
-Script.load("system/depthReticle.js");
\ No newline at end of file
diff --git a/scripts/system/controllers/handControllerPointer.js b/scripts/system/controllers/handControllerPointer.js
new file mode 100644
index 0000000000..6e9fe17077
--- /dev/null
+++ b/scripts/system/controllers/handControllerPointer.js
@@ -0,0 +1,456 @@
+"use strict";
+/*jslint vars: true, plusplus: true*/
+/*globals Script, Overlays, Controller, Reticle, HMD, Camera, Entities, MyAvatar, Settings, Menu, ScriptDiscoveryService, Window, Vec3, Quat, print */
+
+//
+//  handControllerPointer.js
+//  examples/controllers
+//
+//  Created by Howard Stearns on 2016/04/22
+//  Copyright 2016 High Fidelity, Inc.
+//
+//  Distributed under the Apache License, Version 2.0.
+//  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
+//
+
+// Control the "mouse" using hand controller. (HMD and desktop.)
+// For now:
+// Hydra thumb button 3 is left-mouse, button 4 is right-mouse.
+// A click in the center of the vive thumb pad is left mouse. Vive menu button is context menu (right mouse).
+// First-person only.
+// Starts right handed, but switches to whichever is free: Whichever hand was NOT most recently squeezed.
+//   (For now, the thumb buttons on both controllers are always on.)
+// When over a HUD element, the reticle is shown where the active hand controller beam intersects the HUD.
+// Otherwise, the active hand controller shows a red ball where a click will act.
+//
+// Bugs:
+// On Windows, the upper left corner of Interface must be in the upper left corner of the screen, and the title bar must be 50px high. (System bug.)
+// While hardware mouse move switches to mouse move, hardware mouse click (without amove) does not.
+
+
+// UTILITIES -------------
+//
+
+// Utility to make it easier to setup and disconnect cleanly.
+function setupHandler(event, handler) {
+    event.connect(handler);
+    Script.scriptEnding.connect(function () {
+        event.disconnect(handler);
+    });
+}
+// If some capability is not available until expiration milliseconds after the last update.
+function TimeLock(expiration) {
+    var last = 0;
+    this.update = function (optionalNow) {
+        last = optionalNow || Date.now();
+    };
+    this.expired = function (optionalNow) {
+        return ((optionalNow || Date.now()) - last) > expiration;
+    };
+}
+var handControllerLockOut = new TimeLock(2000);
+
+// Calls onFunction() or offFunction() when swtich(on), but only if it is to a new value.
+function LatchedToggle(onFunction, offFunction, state) {
+    this.getState = function () {
+        return state;
+    };
+    this.setState = function (on) {
+        if (state === on) {
+            return;
+        }
+        state = on;
+        if (on) {
+            onFunction();
+        } else {
+            offFunction();
+        }
+    };
+}
+
+// VERTICAL FIELD OF VIEW ---------
+//
+// Cache the verticalFieldOfView setting and update it every so often.
+var verticalFieldOfView, DEFAULT_VERTICAL_FIELD_OF_VIEW = 45; // degrees
+function updateFieldOfView() {
+    verticalFieldOfView = Settings.getValue('fieldOfView') || DEFAULT_VERTICAL_FIELD_OF_VIEW;
+}
+
+// SHIMS ----------
+//
+var weMovedReticle = false;
+function ignoreMouseActivity() {
+    // If we're paused, or if change in cursor position is from this script, not the hardware mouse.
+    if (!Reticle.allowMouseCapture) {
+        return true;
+    }
+    // Only we know if we moved it, which is why this script has to replace depthReticle.js
+    if (!weMovedReticle) {
+        return false;
+    }
+    weMovedReticle = false;
+    return true;
+}
+var setReticlePosition = function (point2d) {
+    weMovedReticle = true;
+    Reticle.setPosition(point2d);
+};
+
+// Generalizations of utilities that work with system and overlay elements.
+function findRayIntersection(pickRay) {
+    // Check 3D overlays and entities. Argument is an object with origin and direction.
+    var result = Overlays.findRayIntersection(pickRay);
+    if (!result.intersects) {
+        result = Entities.findRayIntersection(pickRay, true);
+    }
+    return result;
+}
+function isPointingAtOverlay(optionalHudPosition2d) {
+    return Reticle.pointingAtSystemOverlay || Overlays.getOverlayAtPoint(optionalHudPosition2d || Reticle.position);
+}
+
+// Generalized HUD utilities, with or without HMD:
+// These two "vars" are for documentation. Do not change their values!
+var SPHERICAL_HUD_DISTANCE = 1; // meters.
+var PLANAR_PERPENDICULAR_HUD_DISTANCE = SPHERICAL_HUD_DISTANCE;
+function calculateRayUICollisionPoint(position, direction) {
+    // Answer the 3D intersection of the HUD by the given ray, or falsey if no intersection.
+    if (HMD.active) {
+        return HMD.calculateRayUICollisionPoint(position, direction);
+    }
+    // interect HUD plane, 1m in front of camera, using formula:
+    //   scale = hudNormal dot (hudPoint - position) / hudNormal dot direction
+    //   intersection = postion + scale*direction
+    var hudNormal = Quat.getFront(Camera.getOrientation());
+    var hudPoint = Vec3.sum(Camera.getPosition(), hudNormal); // must also scale if PLANAR_PERPENDICULAR_HUD_DISTANCE!=1
+    var denominator = Vec3.dot(hudNormal, direction);
+    if (denominator === 0) {
+        return null;
+    } // parallel to plane
+    var numerator = Vec3.dot(hudNormal, Vec3.subtract(hudPoint, position));
+    var scale = numerator / denominator;
+    return Vec3.sum(position, Vec3.multiply(scale, direction));
+}
+var DEGREES_TO_HALF_RADIANS = Math.PI / 360;
+function overlayFromWorldPoint(point) {
+    // Answer the 2d pixel-space location in the HUD that covers the given 3D point.
+    // REQUIRES: that the 3d point be on the hud surface!
+    // Note that this is based on the Camera, and doesn't know anything about any
+    // ray that may or may not have been used to compute the point. E.g., the
+    // overlay point is NOT the intersection of some non-camera ray with the HUD.
+    if (HMD.active) {
+        return HMD.overlayFromWorldPoint(point);
+    }
+    var cameraToPoint = Vec3.subtract(point, Camera.getPosition());
+    var cameraX = Vec3.dot(cameraToPoint, Quat.getRight(Camera.getOrientation()));
+    var cameraY = Vec3.dot(cameraToPoint, Quat.getUp(Camera.getOrientation()));
+    var size = Controller.getViewportDimensions();
+    var hudHeight = 2 * Math.tan(verticalFieldOfView * DEGREES_TO_HALF_RADIANS); // must adjust if PLANAR_PERPENDICULAR_HUD_DISTANCE!=1
+    var hudWidth = hudHeight * size.x / size.y;
+    var horizontalFraction = (cameraX / hudWidth + 0.5);
+    var verticalFraction = 1 - (cameraY / hudHeight + 0.5);
+    var horizontalPixels = size.x * horizontalFraction;
+    var verticalPixels = size.y * verticalFraction;
+    return { x: horizontalPixels, y: verticalPixels };
+}
+
+// MOUSE ACTIVITY --------
+//
+var isSeeking = false;
+var averageMouseVelocity = 0, lastIntegration = 0, lastMouse;
+var WEIGHTING = 1 / 20; // simple moving average over last 20 samples
+var ONE_MINUS_WEIGHTING = 1 - WEIGHTING;
+var AVERAGE_MOUSE_VELOCITY_FOR_SEEK_TO = 20;
+function isShakingMouse() { // True if the person is waving the mouse around trying to find it.
+    var now = Date.now(), mouse = Reticle.position, isShaking = false;
+    if (lastIntegration && (lastIntegration !== now)) {
+        var velocity = Vec3.length(Vec3.subtract(mouse, lastMouse)) / (now - lastIntegration);
+        averageMouseVelocity = (ONE_MINUS_WEIGHTING * averageMouseVelocity) + (WEIGHTING * velocity);
+        if (averageMouseVelocity > AVERAGE_MOUSE_VELOCITY_FOR_SEEK_TO) {
+            isShaking = true;
+        }
+    }
+    lastIntegration = now;
+    lastMouse = mouse;
+    return isShaking;
+}
+var NON_LINEAR_DIVISOR = 2;
+var MINIMUM_SEEK_DISTANCE = 0.01;
+function updateSeeking() {
+    if (!Reticle.visible || isShakingMouse()) {
+        isSeeking = true;
+    } // e.g., if we're about to turn it on with first movement.
+    if (!isSeeking) {
+        return;
+    }
+    averageMouseVelocity = lastIntegration = 0;
+    var lookAt2D = HMD.getHUDLookAtPosition2D();
+    if (!lookAt2D) {
+        print('Cannot seek without lookAt position');
+        return;
+    } // E.g., if parallel to location in HUD
+    var copy = Reticle.position;
+    function updateDimension(axis) {
+        var distanceBetween = lookAt2D[axis] - Reticle.position[axis];
+        var move = distanceBetween / NON_LINEAR_DIVISOR;
+        if (Math.abs(move) < MINIMUM_SEEK_DISTANCE) {
+            return false;
+        }
+        copy[axis] += move;
+        return true;
+    }
+    var okX = !updateDimension('x'), okY = !updateDimension('y'); // Evaluate both. Don't short-circuit.
+    if (okX && okY) {
+        isSeeking = false;
+    } else {
+        Reticle.setPosition(copy); // Not setReticlePosition
+    }
+}
+
+var mouseCursorActivity = new TimeLock(5000);
+var APPARENT_MAXIMUM_DEPTH = 100.0; // this is a depth at which things all seem sufficiently distant
+function updateMouseActivity(isClick) {
+    if (ignoreMouseActivity()) {
+        return;
+    }
+    var now = Date.now();
+    mouseCursorActivity.update(now);
+    if (isClick) {
+        return;
+    } // Bug: mouse clicks should keep going. Just not hand controller clicks
+    handControllerLockOut.update(now);
+    Reticle.visible = true;
+}
+function expireMouseCursor(now) {
+    if (!isPointingAtOverlay() && mouseCursorActivity.expired(now)) {
+        Reticle.visible = false;
+    }
+}
+function onMouseMove() {
+    // Display cursor at correct depth (as in depthReticle.js), and updateMouseActivity.
+    if (ignoreMouseActivity()) {
+        return;
+    }
+
+    if (HMD.active) { // set depth
+        updateSeeking();
+        if (isPointingAtOverlay()) {
+            Reticle.setDepth(SPHERICAL_HUD_DISTANCE); // NOT CORRECT IF WE SWITCH TO OFFSET SPHERE!
+        } else {
+            var result = findRayIntersection(Camera.computePickRay(Reticle.position.x, Reticle.position.y));
+            var depth = result.intersects ? result.distance : APPARENT_MAXIMUM_DEPTH;
+            Reticle.setDepth(depth);
+        }
+    }
+    updateMouseActivity(); // After the above, just in case the depth movement is awkward when becoming visible.
+}
+function onMouseClick() {
+    updateMouseActivity(true);
+}
+setupHandler(Controller.mouseMoveEvent, onMouseMove);
+setupHandler(Controller.mousePressEvent, onMouseClick);
+setupHandler(Controller.mouseDoublePressEvent, onMouseClick);
+
+// CONTROLLER MAPPING ---------
+//
+
+var activeHand = Controller.Standard.RightHand;
+function toggleHand() {
+    if (activeHand === Controller.Standard.RightHand) {
+        activeHand = Controller.Standard.LeftHand;
+    } else {
+        activeHand = Controller.Standard.RightHand;
+    }
+}
+
+// Create clickMappings as needed, on demand.
+var clickMappings = {}, clickMapping, clickMapToggle;
+var hardware; // undefined
+function checkHardware() {
+    var newHardware = Controller.Hardware.Hydra ? 'Hydra' : (Controller.Hardware.Vive ? 'Vive' : null); // not undefined
+    if (hardware === newHardware) {
+        return;
+    }
+    print('Setting mapping for new controller hardware:', newHardware);
+    if (clickMapToggle) {
+        clickMapToggle.setState(false);
+    }
+    hardware = newHardware;
+    if (clickMappings[hardware]) {
+        clickMapping = clickMappings[hardware];
+    } else {
+        clickMapping = Controller.newMapping(Script.resolvePath('') + '-click-' + hardware);
+        Script.scriptEnding.connect(clickMapping.disable);
+        function mapToAction(button, action) {
+            clickMapping.from(Controller.Hardware[hardware][button]).peek().to(Controller.Actions[action]);
+        }
+        function makeHandToggle(button, hand, optionalWhen) {
+            var whenThunk = optionalWhen || function () {
+                return true;
+            };
+            function maybeToggle() {
+                if (activeHand !== Controller.Standard[hand]) {
+                    toggleHand();
+                }
+
+            }
+            clickMapping.from(Controller.Hardware[hardware][button]).peek().when(whenThunk).to(maybeToggle);
+        }
+        function makeViveWhen(click, x, y) {
+            var viveClick = Controller.Hardware.Vive[click],
+                viveX = Controller.Standard[x],  // Standard after filtering by mapping
+                viveY = Controller.Standard[y];
+            return function () {
+                var clickValue = Controller.getValue(viveClick);
+                var xValue = Controller.getValue(viveX);
+                var yValue = Controller.getValue(viveY);
+                return clickValue && !xValue && !yValue;
+            };
+        }
+        switch (hardware) {
+            case 'Hydra':
+                makeHandToggle('R3', 'RightHand');
+                makeHandToggle('L3', 'LeftHand');
+
+                mapToAction('R3', 'ReticleClick');
+                mapToAction('L3', 'ReticleClick');
+                mapToAction('R4', 'ContextMenu');
+                mapToAction('L4', 'ContextMenu');
+                break;
+            case 'Vive':
+                // When touchpad click is NOT treated as movement, treat as left click
+                makeHandToggle('RS', 'RightHand', makeViveWhen('RS', 'RX', 'RY'));
+                makeHandToggle('LS', 'LeftHand', makeViveWhen('LS', 'LX', 'LY'));
+                clickMapping.from(Controller.Hardware.Vive.RS).when(makeViveWhen('RS', 'RX', 'RY')).to(Controller.Actions.ReticleClick);
+                clickMapping.from(Controller.Hardware.Vive.LS).when(makeViveWhen('LS', 'LX', 'LY')).to(Controller.Actions.ReticleClick);
+                mapToAction('RightApplicationMenu', 'ContextMenu');
+                mapToAction('LeftApplicationMenu', 'ContextMenu');
+                break;
+        }
+        clickMappings[hardware] = clickMapping;
+    }
+    clickMapToggle = new LatchedToggle(clickMapping.enable, clickMapping.disable);
+    clickMapToggle.setState(true);
+}
+checkHardware();
+
+// VISUAL AID -----------
+// Same properties as handControllerGrab search sphere
+var BALL_SIZE = 0.011;
+var BALL_ALPHA = 0.5;
+var fakeProjectionBall = Overlays.addOverlay("sphere", {
+    size: 5 * BALL_SIZE,
+    color: {red: 255, green: 10, blue: 10},
+    ignoreRayIntersection: true,
+    alpha: BALL_ALPHA,
+    visible: false,
+    solid: true,
+    drawInFront: true // Even when burried inside of something, show it.
+});
+var overlays = [fakeProjectionBall]; // If we want to try showing multiple balls and lasers.
+Script.scriptEnding.connect(function () {
+    overlays.forEach(Overlays.deleteOverlay);
+});
+var visualizationIsShowing = false; // Not whether it desired, but simply whether it is. Just an optimziation.
+function turnOffVisualization(optionalEnableClicks) { // because we're showing cursor on HUD
+    if (!optionalEnableClicks) {
+        expireMouseCursor();
+    }
+    if (!visualizationIsShowing) {
+        return;
+    }
+    visualizationIsShowing = false;
+    overlays.forEach(function (overlay) {
+        Overlays.editOverlay(overlay, {visible: false});
+    });
+}
+var MAX_RAY_SCALE = 32000; // Anything large. It's a scale, not a distance.
+function updateVisualization(controllerPosition, controllerDirection, hudPosition3d, hudPosition2d) {
+    // Show an indication of where the cursor will appear when crossing a HUD element,
+    // and where in-world clicking will occur.
+    //
+    // There are a number of ways we could do this, but for now, it's a blue sphere that rolls along
+    // the HUD surface, and a red sphere that rolls along the 3d objects that will receive the click.
+    // We'll leave it to other scripts (like handControllerGrab) to show a search beam when desired.
+
+    function intersection3d(position, direction) {
+        // Answer in-world intersection (entity or 3d overlay), or way-out point
+        var pickRay = {origin: position, direction: direction};
+        var result = findRayIntersection(pickRay);
+        return result.intersects ? result.intersection : Vec3.sum(position, Vec3.multiply(MAX_RAY_SCALE, direction));
+    }
+
+    visualizationIsShowing = true;
+    // We'd rather in-world interactions be done at the termination of the hand beam
+    // -- intersection3d(controllerPosition, controllerDirection). Maybe have handControllerGrab
+    // direclty manipulate both entity and 3d overlay objects.
+    // For now, though, we present a false projection of the cursor onto whatever is below it. This is
+    // different from the hand beam termination because the false projection is from the camera, while
+    // the hand beam termination is from the hand.
+    var eye = Camera.getPosition();
+    var falseProjection = intersection3d(eye, Vec3.subtract(hudPosition3d, eye));
+    Overlays.editOverlay(fakeProjectionBall, {visible: true, position: falseProjection});
+    Reticle.visible = false;
+
+    return visualizationIsShowing; // In case we change caller to act conditionally.
+}
+
+// MAIN OPERATIONS -----------
+//
+function update() {
+    var now = Date.now();
+    if (!handControllerLockOut.expired(now)) {
+        return turnOffVisualization();
+    } // Let them use mouse it in peace.
+    if (!Menu.isOptionChecked("First Person")) {
+        return turnOffVisualization();
+    }  // What to do? menus can be behind hand!
+    var controllerPose = Controller.getPoseValue(activeHand);
+    // Vive is effectively invalid when not in HMD
+    if (!controllerPose.valid || ((hardware === 'Vive') && !HMD.active)) {
+        return turnOffVisualization();
+    } // Controller is cradled.
+    var controllerPosition = Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, controllerPose.translation),
+                                      MyAvatar.position);
+    // This gets point direction right, but if you want general quaternion it would be more complicated:
+    var controllerDirection = Quat.getUp(Quat.multiply(MyAvatar.orientation, controllerPose.rotation));
+
+    var hudPoint3d = calculateRayUICollisionPoint(controllerPosition, controllerDirection);
+    if (!hudPoint3d) {
+        print('Controller is parallel to HUD');
+        return turnOffVisualization();
+    }
+    var hudPoint2d = overlayFromWorldPoint(hudPoint3d);
+
+    // We don't know yet if we'll want to make the cursor visble, but we need to move it to see if
+    // it's pointing at a QML tool (aka system overlay).
+    setReticlePosition(hudPoint2d);
+    // If there's a HUD element at the (newly moved) reticle, just make it visible and bail.
+    if (isPointingAtOverlay(hudPoint2d)) {
+        if (HMD.active) {  // Doesn't hurt anything without the guard, but consider it documentation.
+            Reticle.depth = SPHERICAL_HUD_DISTANCE; // NOT CORRECT IF WE SWITCH TO OFFSET SPHERE!
+        }
+        Reticle.visible = true;
+        return turnOffVisualization(true);
+    }
+    // We are not pointing at a HUD element (but it could be a 3d overlay).
+    updateVisualization(controllerPosition, controllerDirection, hudPoint3d, hudPoint2d);
+}
+
+var UPDATE_INTERVAL = 20; // milliseconds. Script.update is too frequent.
+var updater = Script.setInterval(update, UPDATE_INTERVAL);
+Script.scriptEnding.connect(function () {
+    Script.clearInterval(updater);
+});
+
+// Check periodically for changes to setup.
+var SETTINGS_CHANGE_RECHECK_INTERVAL = 10 * 1000; // milliseconds
+function checkSettings() {
+    updateFieldOfView();
+    checkHardware();
+}
+checkSettings();
+var settingsChecker = Script.setInterval(checkSettings, SETTINGS_CHANGE_RECHECK_INTERVAL);
+Script.scriptEnding.connect(function () {
+    Script.clearInterval(settingsChecker);
+});
diff --git a/scripts/system/depthReticle.js b/scripts/system/depthReticle.js
deleted file mode 100644
index 10d604f707..0000000000
--- a/scripts/system/depthReticle.js
+++ /dev/null
@@ -1,185 +0,0 @@
-//  depthReticle.js
-//  examples
-//
-//  Created by Brad Hefta-Gaub on 2/23/16.
-//  Copyright 2016 High Fidelity, Inc.
-//
-//  When used in HMD, this script will make the reticle depth track to any clickable item in view.
-//  This script also handles auto-hiding the reticle after inactivity, as well as having the reticle
-//  seek the look at position upon waking up.
-//
-//  Distributed under the Apache License, Version 2.0.
-//  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
-//
-
-var APPARENT_2D_OVERLAY_DEPTH = 1.0;
-var APPARENT_MAXIMUM_DEPTH = 100.0; // this is a depth at which things all seem sufficiently distant
-var lastDepthCheckTime = Date.now();
-var desiredDepth = APPARENT_2D_OVERLAY_DEPTH;
-var TIME_BETWEEN_DEPTH_CHECKS = 100;
-var MINIMUM_DEPTH_ADJUST = 0.01;
-var NON_LINEAR_DIVISOR = 2;
-var MINIMUM_SEEK_DISTANCE = 0.01;
-
-var lastMouseMoveOrClick = Date.now();
-var lastMouseX = Reticle.position.x;
-var lastMouseY = Reticle.position.y;
-var HIDE_STATIC_MOUSE_AFTER = 3000; // 3 seconds
-var shouldSeekToLookAt = false;
-var fastMouseMoves = 0;
-var averageMouseVelocity = 0;
-var WEIGHTING = 1/20; // simple moving average over last 20 samples
-var ONE_MINUS_WEIGHTING = 1 - WEIGHTING;
-var AVERAGE_MOUSE_VELOCITY_FOR_SEEK_TO = 50;
-
-function showReticleOnMouseClick() {
-    Reticle.visible = true;
-    lastMouseMoveOrClick = Date.now(); // move or click
-}
-
-Controller.mousePressEvent.connect(showReticleOnMouseClick);
-Controller.mouseDoublePressEvent.connect(showReticleOnMouseClick);
-
-Controller.mouseMoveEvent.connect(function(mouseEvent) {
-    var now = Date.now();
-
-    // if the reticle is hidden, and we're not in away mode...
-    if (!Reticle.visible && Reticle.allowMouseCapture) {
-        Reticle.visible = true;
-        if (HMD.active) {
-            shouldSeekToLookAt = true;
-        }
-    } else {
-        // even if the reticle is visible, if we're in HMD mode, and the person is moving their mouse quickly (shaking it)
-        // then they are probably looking for it, and we should move into seekToLookAt mode
-        if (HMD.active && !shouldSeekToLookAt && Reticle.allowMouseCapture) {
-            var dx = Reticle.position.x - lastMouseX;
-            var dy = Reticle.position.y - lastMouseY;
-            var dt = Math.max(1, (now - lastMouseMoveOrClick)); // mSecs since last mouse move
-            var mouseMoveDistance = Math.sqrt((dx*dx) + (dy*dy));
-            var mouseVelocity = mouseMoveDistance / dt;
-            averageMouseVelocity = (ONE_MINUS_WEIGHTING * averageMouseVelocity) + (WEIGHTING * mouseVelocity);
-            if (averageMouseVelocity > AVERAGE_MOUSE_VELOCITY_FOR_SEEK_TO) {
-                shouldSeekToLookAt = true;
-            }
-        }
-    }
-    lastMouseMoveOrClick = now;
-    lastMouseX = mouseEvent.x;
-    lastMouseY = mouseEvent.y;
-});
-
-function seekToLookAt() {
-    // if we're currently seeking the lookAt move the mouse toward the lookat
-    if (shouldSeekToLookAt) {
-        averageMouseVelocity = 0; // reset this, these never count for movement...
-        var lookAt2D = HMD.getHUDLookAtPosition2D();
-        var currentReticlePosition = Reticle.position;
-        var distanceBetweenX = lookAt2D.x - Reticle.position.x;
-        var distanceBetweenY = lookAt2D.y - Reticle.position.y;
-        var moveX = distanceBetweenX / NON_LINEAR_DIVISOR;
-        var moveY = distanceBetweenY / NON_LINEAR_DIVISOR;
-        var newPosition = { x: Reticle.position.x + moveX, y: Reticle.position.y + moveY };
-        var closeEnoughX = false;
-        var closeEnoughY = false;
-        if (moveX < MINIMUM_SEEK_DISTANCE) {
-            newPosition.x = lookAt2D.x;
-            closeEnoughX = true;
-        }
-        if (moveY < MINIMUM_SEEK_DISTANCE) {
-            newPosition.y = lookAt2D.y;
-            closeEnoughY = true;
-        }
-        Reticle.position = newPosition;
-        if (closeEnoughX && closeEnoughY) {
-            shouldSeekToLookAt = false;
-        }
-    }
-}
-
-function autoHideReticle() {
-    var now = Date.now();
-
-    // sometimes we don't actually get mouse move messages (for example, if the focus has been set
-    // to an overlay or web page 'overlay') in but the mouse can still be moving, and we don't want
-    // to autohide in these cases, so we will take this opportunity to also check if the reticle
-    // position has changed.
-    if (lastMouseX != Reticle.position.x || lastMouseY != Reticle.position.y) {
-        lastMouseMoveOrClick = now;
-        lastMouseX = Reticle.position.x;
-        lastMouseY = Reticle.position.y;
-    }
-
-    // if we haven't moved in a long period of time, and we're not pointing at some
-    // system overlay (like a window), then hide the reticle
-    if (Reticle.visible && !Reticle.pointingAtSystemOverlay) {
-        var timeSinceLastMouseMove = now - lastMouseMoveOrClick;
-        if (timeSinceLastMouseMove > HIDE_STATIC_MOUSE_AFTER) {
-            Reticle.visible = false;
-        }
-    }
-}
-
-function checkReticleDepth() {
-    var now = Date.now();
-    var timeSinceLastDepthCheck = now - lastDepthCheckTime;
-    if (timeSinceLastDepthCheck > TIME_BETWEEN_DEPTH_CHECKS && Reticle.visible) {
-        var newDesiredDepth = desiredDepth;
-        lastDepthCheckTime = now;
-        var reticlePosition = Reticle.position;
-
-        // first check the 2D Overlays
-        if (Reticle.pointingAtSystemOverlay || Overlays.getOverlayAtPoint(reticlePosition)) {
-            newDesiredDepth = APPARENT_2D_OVERLAY_DEPTH;
-        } else {
-            var pickRay = Camera.computePickRay(reticlePosition.x, reticlePosition.y);
-
-            // Then check the 3D overlays
-            var result = Overlays.findRayIntersection(pickRay);
-
-            if (!result.intersects) {
-                // finally check the entities
-                result = Entities.findRayIntersection(pickRay, true);
-            }
-
-            // If either the overlays or entities intersect, then set the reticle depth to 
-            // the distance of intersection
-            if (result.intersects) {
-                newDesiredDepth = result.distance;
-            } else {
-                // if nothing intersects... set the depth to some sufficiently large depth
-                newDesiredDepth = APPARENT_MAXIMUM_DEPTH;
-            }
-        }
-
-        // If the desired depth has changed, reset our fade start time
-        if (desiredDepth != newDesiredDepth) {
-            desiredDepth = newDesiredDepth;
-        }
-    }
-
-}
-
-function moveToDesiredDepth() {
-    // move the reticle toward the desired depth
-    if (desiredDepth != Reticle.depth) {
-
-        // cut distance between desiredDepth and current depth in half until we're close enough
-        var distanceToAdjustThisCycle = (desiredDepth - Reticle.depth) / NON_LINEAR_DIVISOR;
-        if (Math.abs(distanceToAdjustThisCycle) < MINIMUM_DEPTH_ADJUST) {
-            newDepth = desiredDepth;
-        } else {
-            newDepth = Reticle.depth + distanceToAdjustThisCycle;
-        }
-        Reticle.setDepth(newDepth);
-    }
-}
-
-Script.update.connect(function(deltaTime) {
-    autoHideReticle(); // auto hide reticle for desktop or HMD mode
-    if (HMD.active) {
-        seekToLookAt(); // handle moving the reticle toward the look at
-        checkReticleDepth(); // make sure reticle is at correct depth
-        moveToDesiredDepth(); // move the fade the reticle to the desired depth
-    }
-});