diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp
index 7adde0e406..f3c41565f8 100644
--- a/interface/src/Application.cpp
+++ b/interface/src/Application.cpp
@@ -738,6 +738,7 @@ const float DEFAULT_HMD_TABLET_SCALE_PERCENT = 100.0f;
 const float DEFAULT_DESKTOP_TABLET_SCALE_PERCENT = 75.0f;
 const bool DEFAULT_DESKTOP_TABLET_BECOMES_TOOLBAR = true;
 const bool DEFAULT_HMD_TABLET_BECOMES_TOOLBAR = false;
+const bool DEFAULT_PREFER_STYLUS_OVER_LASER = false;
 const bool DEFAULT_PREFER_AVATAR_FINGER_OVER_STYLUS = false;
 const QString DEFAULT_CURSOR_NAME = "DEFAULT";
 
@@ -757,6 +758,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
     _desktopTabletScale("desktopTabletScale", DEFAULT_DESKTOP_TABLET_SCALE_PERCENT),
     _desktopTabletBecomesToolbarSetting("desktopTabletBecomesToolbar", DEFAULT_DESKTOP_TABLET_BECOMES_TOOLBAR),
     _hmdTabletBecomesToolbarSetting("hmdTabletBecomesToolbar", DEFAULT_HMD_TABLET_BECOMES_TOOLBAR),
+    _preferStylusOverLaserSetting("preferStylusOverLaser", DEFAULT_PREFER_STYLUS_OVER_LASER),
     _preferAvatarFingerOverStylusSetting("preferAvatarFingerOverStylus", DEFAULT_PREFER_AVATAR_FINGER_OVER_STYLUS),
     _constrainToolbarPosition("toolbar/constrainToolbarToCenterX", true),
     _preferredCursor("preferredCursor", DEFAULT_CURSOR_NAME),
@@ -2580,6 +2582,10 @@ void Application::setHmdTabletBecomesToolbarSetting(bool value) {
     updateSystemTabletMode();
 }
 
+void Application::setPreferStylusOverLaser(bool value) {
+    _preferStylusOverLaserSetting.set(value);
+}
+
 void Application::setPreferAvatarFingerOverStylus(bool value) {
     _preferAvatarFingerOverStylusSetting.set(value);
 }
diff --git a/interface/src/Application.h b/interface/src/Application.h
index 5267d50cf6..479ce919a3 100644
--- a/interface/src/Application.h
+++ b/interface/src/Application.h
@@ -207,7 +207,11 @@ public:
     void setDesktopTabletBecomesToolbarSetting(bool value);
     bool getHmdTabletBecomesToolbarSetting() { return _hmdTabletBecomesToolbarSetting.get(); }
     void setHmdTabletBecomesToolbarSetting(bool value);
-    bool getPreferAvatarFingerOverStylus() { return _preferAvatarFingerOverStylusSetting.get(); }
+    bool getPreferStylusOverLaser() { return _preferStylusOverLaserSetting.get(); }
+    void setPreferStylusOverLaser(bool value);
+    // FIXME: Remove setting completely or make available through JavaScript API?
+    //bool getPreferAvatarFingerOverStylus() { return _preferAvatarFingerOverStylusSetting.get(); }
+    bool getPreferAvatarFingerOverStylus() { return false; }
     void setPreferAvatarFingerOverStylus(bool value);
 
     float getSettingConstrainToolbarPosition() { return _constrainToolbarPosition.get(); }
@@ -551,6 +555,7 @@ private:
     Setting::Handle<float> _desktopTabletScale;
     Setting::Handle<bool> _desktopTabletBecomesToolbarSetting;
     Setting::Handle<bool> _hmdTabletBecomesToolbarSetting;
+    Setting::Handle<bool> _preferStylusOverLaserSetting;
     Setting::Handle<bool> _preferAvatarFingerOverStylusSetting;
     Setting::Handle<bool> _constrainToolbarPosition;
     Setting::Handle<QString> _preferredCursor;
diff --git a/interface/src/raypick/LaserPointer.cpp b/interface/src/raypick/LaserPointer.cpp
index 35f3a44227..a4fe516590 100644
--- a/interface/src/raypick/LaserPointer.cpp
+++ b/interface/src/raypick/LaserPointer.cpp
@@ -218,14 +218,40 @@ Pointer::PickedObject LaserPointer::getHoveredObject(const PickResultPointer& pi
     return PickedObject(rayPickResult->objectID, rayPickResult->type);
 }
 
-Pointer::Buttons LaserPointer::getPressedButtons() {
+Pointer::Buttons LaserPointer::getPressedButtons(const PickResultPointer& pickResult) {
     std::unordered_set<std::string> toReturn;
-    for (const PointerTrigger& trigger : _triggers) {
-        // TODO: right now, LaserPointers don't support axes, only on/off buttons
-        if (trigger.getEndpoint()->peek() >= 1.0f) {
-            toReturn.insert(trigger.getButton());
+    auto rayPickResult = std::static_pointer_cast<const RayPickResult>(pickResult);
+
+    if (rayPickResult) {
+        for (const PointerTrigger& trigger : _triggers) {
+            std::string button = trigger.getButton();
+            TriggerState& state = _states[button];
+            // TODO: right now, LaserPointers don't support axes, only on/off buttons
+            if (trigger.getEndpoint()->peek() >= 1.0f) {
+                toReturn.insert(button);
+
+                if (_previousButtons.find(button) == _previousButtons.end()) {
+                    // start triggering for buttons that were just pressed
+                    state.triggeredObject = PickedObject(rayPickResult->objectID, rayPickResult->type);
+                    state.intersection = rayPickResult->intersection;
+                    state.triggerPos2D = findPos2D(state.triggeredObject, rayPickResult->intersection);
+                    state.triggerStartTime = usecTimestampNow();
+                    state.surfaceNormal = rayPickResult->surfaceNormal;
+                    state.deadspotExpired = false;
+                    state.wasTriggering = true;
+                    state.triggering = true;
+                    _latestState = state;
+                }
+            } else {
+                // stop triggering for buttons that aren't pressed
+                state.wasTriggering = state.triggering;
+                state.triggering = false;
+                _latestState = state;
+            }
         }
+        _previousButtons = toReturn;
     }
+
     return toReturn;
 }
 
@@ -303,7 +329,7 @@ RenderState LaserPointer::buildRenderState(const QVariantMap& propMap) {
     return RenderState(startID, pathID, endID);
 }
 
-PointerEvent LaserPointer::buildPointerEvent(const PickedObject& target, const PickResultPointer& pickResult, bool hover) const {
+PointerEvent LaserPointer::buildPointerEvent(const PickedObject& target, const PickResultPointer& pickResult, const std::string& button, bool hover) {
     QUuid pickedID;
     glm::vec3 intersection, surfaceNormal, direction, origin;
     auto rayPickResult = std::static_pointer_cast<RayPickResult>(pickResult);
@@ -316,20 +342,48 @@ PointerEvent LaserPointer::buildPointerEvent(const PickedObject& target, const P
         pickedID = rayPickResult->objectID;
     }
 
-    glm::vec2 pos2D;
     if (pickedID != target.objectID) {
-        if (target.type == ENTITY) {
-            intersection = RayPick::intersectRayWithEntityXYPlane(target.objectID, origin, direction);
-        } else if (target.type == OVERLAY) {
-            intersection = RayPick::intersectRayWithOverlayXYPlane(target.objectID, origin, direction);
-        }
+        intersection = findIntersection(target, origin, direction);
     }
-    if (target.type == ENTITY) {
-        pos2D = RayPick::projectOntoEntityXYPlane(target.objectID, intersection);
-    } else if (target.type == OVERLAY) {
-        pos2D = RayPick::projectOntoOverlayXYPlane(target.objectID, intersection);
-    } else if (target.type == HUD) {
-        pos2D = DependencyManager::get<PickManager>()->calculatePos2DFromHUD(intersection);
+    glm::vec2 pos2D = findPos2D(target, intersection);
+
+    // If we just started triggering and we haven't moved too much, don't update intersection and pos2D
+    TriggerState& state = hover ? _latestState : _states[button];
+    float sensorToWorldScale = DependencyManager::get<AvatarManager>()->getMyAvatar()->getSensorToWorldScale();
+    float deadspotSquared = TOUCH_PRESS_TO_MOVE_DEADSPOT_SQUARED * sensorToWorldScale * sensorToWorldScale;
+    bool withinDeadspot = usecTimestampNow() - state.triggerStartTime < POINTER_MOVE_DELAY && glm::distance2(pos2D, state.triggerPos2D) < deadspotSquared;
+    if ((state.triggering || state.wasTriggering) && !state.deadspotExpired && withinDeadspot) {
+        pos2D = state.triggerPos2D;
+        intersection = state.intersection;
+        surfaceNormal = state.surfaceNormal;
     }
+    if (!withinDeadspot) {
+        state.deadspotExpired = true;
+    }
+
     return PointerEvent(pos2D, intersection, surfaceNormal, direction);
+}
+
+glm::vec3 LaserPointer::findIntersection(const PickedObject& pickedObject, const glm::vec3& origin, const glm::vec3& direction) {
+    switch (pickedObject.type) {
+    case ENTITY:
+        return RayPick::intersectRayWithEntityXYPlane(pickedObject.objectID, origin, direction);
+    case OVERLAY:
+        return RayPick::intersectRayWithOverlayXYPlane(pickedObject.objectID, origin, direction);
+    default:
+        return glm::vec3(NAN);
+    }
+}
+
+glm::vec2 LaserPointer::findPos2D(const PickedObject& pickedObject, const glm::vec3& origin) {
+    switch (pickedObject.type) {
+    case ENTITY:
+        return RayPick::projectOntoEntityXYPlane(pickedObject.objectID, origin);
+    case OVERLAY:
+        return RayPick::projectOntoOverlayXYPlane(pickedObject.objectID, origin);
+    case HUD:
+        return DependencyManager::get<PickManager>()->calculatePos2DFromHUD(origin);
+    default:
+        return glm::vec2(NAN);
+    }
 }
\ No newline at end of file
diff --git a/interface/src/raypick/LaserPointer.h b/interface/src/raypick/LaserPointer.h
index 5cc83749bd..efc5c02729 100644
--- a/interface/src/raypick/LaserPointer.h
+++ b/interface/src/raypick/LaserPointer.h
@@ -80,10 +80,10 @@ public:
     static RenderState buildRenderState(const QVariantMap& propMap);
 
 protected:
-    PointerEvent buildPointerEvent(const PickedObject& target, const PickResultPointer& pickResult, bool hover = true) const override;
+    PointerEvent buildPointerEvent(const PickedObject& target, const PickResultPointer& pickResult, const std::string& button = "", bool hover = true) override;
 
     PickedObject getHoveredObject(const PickResultPointer& pickResult) override;
-    Pointer::Buttons getPressedButtons() override;
+    Pointer::Buttons getPressedButtons(const PickResultPointer& pickResult) override;
 
     bool shouldHover(const PickResultPointer& pickResult) override { return _currentRenderState != ""; }
     bool shouldTrigger(const PickResultPointer& pickResult) override { return _currentRenderState != ""; }
@@ -105,6 +105,23 @@ private:
     void updateRenderState(const RenderState& renderState, const IntersectionType type, float distance, const QUuid& objectID, const PickRay& pickRay, bool defaultState);
     void disableRenderState(const RenderState& renderState);
 
+    struct TriggerState {
+        PickedObject triggeredObject;
+        glm::vec3 intersection { NAN };
+        glm::vec3 surfaceNormal { NAN };
+        glm::vec2 triggerPos2D { NAN };
+        quint64 triggerStartTime { 0 };
+        bool deadspotExpired { true };
+        bool triggering { false };
+        bool wasTriggering { false };
+    };
+
+    Pointer::Buttons _previousButtons;
+    std::unordered_map<std::string, TriggerState> _states;
+    TriggerState _latestState;
+    static glm::vec3 findIntersection(const PickedObject& pickedObject, const glm::vec3& origin, const glm::vec3& direction);
+    static glm::vec2 findPos2D(const PickedObject& pickedObject, const glm::vec3& origin);
+
 };
 
 #endif // hifi_LaserPointer_h
diff --git a/interface/src/raypick/RayPick.cpp b/interface/src/raypick/RayPick.cpp
index 501e8d1e42..4a39d5df59 100644
--- a/interface/src/raypick/RayPick.cpp
+++ b/interface/src/raypick/RayPick.cpp
@@ -92,4 +92,4 @@ glm::vec2 RayPick::projectOntoOverlayXYPlane(const QUuid& overlayID, const glm::
 glm::vec2 RayPick::projectOntoEntityXYPlane(const QUuid& entityID, const glm::vec3& worldPos, bool unNormalized) {
     auto props = DependencyManager::get<EntityScriptingInterface>()->getEntityProperties(entityID);
     return projectOntoXYPlane(worldPos, props.getPosition(), props.getRotation(), props.getDimensions(), props.getRegistrationPoint(), unNormalized);
-}
+}
\ No newline at end of file
diff --git a/interface/src/raypick/StylusPointer.cpp b/interface/src/raypick/StylusPointer.cpp
index 5afbc2058a..fad6fe47ae 100644
--- a/interface/src/raypick/StylusPointer.cpp
+++ b/interface/src/raypick/StylusPointer.cpp
@@ -28,10 +28,6 @@ static const float TABLET_MAX_TOUCH_DISTANCE = 0.005f;
 static const float HOVER_HYSTERESIS = 0.01f;
 static const float TOUCH_HYSTERESIS = 0.001f;
 
-static const float STYLUS_MOVE_DELAY = 0.33f * USECS_PER_SECOND;
-static const float TOUCH_PRESS_TO_MOVE_DEADSPOT = 0.0481f;
-static const float TOUCH_PRESS_TO_MOVE_DEADSPOT_SQUARED = TOUCH_PRESS_TO_MOVE_DEADSPOT * TOUCH_PRESS_TO_MOVE_DEADSPOT;
-
 StylusPointer::StylusPointer(const QVariant& props, const OverlayID& stylusOverlay, bool hover, bool enabled) :
     Pointer(DependencyManager::get<PickScriptingInterface>()->createStylusPick(props), enabled, hover),
     _stylusOverlay(stylusOverlay)
@@ -112,37 +108,37 @@ bool StylusPointer::shouldHover(const PickResultPointer& pickResult) {
 
 bool StylusPointer::shouldTrigger(const PickResultPointer& pickResult) {
     auto stylusPickResult = std::static_pointer_cast<const StylusPickResult>(pickResult);
+    bool wasTriggering = false;
     if (_renderState == EVENTS_ON && stylusPickResult) {
         auto sensorScaleFactor = DependencyManager::get<AvatarManager>()->getMyAvatar()->getSensorToWorldScale();
         float distance = stylusPickResult->distance;
 
         // If we're triggering on an object, recalculate the distance instead of using the pickResult
         glm::vec3 origin = vec3FromVariant(stylusPickResult->pickVariant["position"]);
-        glm::vec3 direction = -_state.surfaceNormal;
-        if (!_state.triggeredObject.objectID.isNull() && stylusPickResult->objectID != _state.triggeredObject.objectID) {
+        glm::vec3 direction = _state.triggering ? -_state.surfaceNormal : -stylusPickResult->surfaceNormal;
+        if ((_state.triggering || _state.wasTriggering) && stylusPickResult->objectID != _state.triggeredObject.objectID) {
             distance = glm::dot(findIntersection(_state.triggeredObject, origin, direction) - origin, direction);
         }
 
         float hysteresis = _state.triggering ? TOUCH_HYSTERESIS * sensorScaleFactor : 0.0f;
         if (isWithinBounds(distance, TABLET_MIN_TOUCH_DISTANCE * sensorScaleFactor,
-                           TABLET_MAX_TOUCH_DISTANCE * sensorScaleFactor, hysteresis)) {
-            if (_state.triggeredObject.objectID.isNull()) {
+            TABLET_MAX_TOUCH_DISTANCE * sensorScaleFactor, hysteresis)) {
+            _state.wasTriggering = _state.triggering;
+            if (!_state.triggering) {
                 _state.triggeredObject = PickedObject(stylusPickResult->objectID, stylusPickResult->type);
-                _state.intersection = findIntersection(_state.triggeredObject, origin, direction);
+                _state.intersection = stylusPickResult->intersection;
                 _state.triggerPos2D = findPos2D(_state.triggeredObject, origin);
                 _state.triggerStartTime = usecTimestampNow();
                 _state.surfaceNormal = stylusPickResult->surfaceNormal;
+                _state.deadspotExpired = false;
                 _state.triggering = true;
             }
             return true;
         }
+        wasTriggering = _state.triggering;
     }
 
-    _state.triggeredObject = PickedObject();
-    _state.intersection = glm::vec3(NAN);
-    _state.triggerPos2D = glm::vec2(NAN);
-    _state.triggerStartTime = 0;
-    _state.surfaceNormal = glm::vec3(NAN);
+    _state.wasTriggering = wasTriggering;
     _state.triggering = false;
     return false;
 }
@@ -155,13 +151,13 @@ Pointer::PickedObject StylusPointer::getHoveredObject(const PickResultPointer& p
     return PickedObject(stylusPickResult->objectID, stylusPickResult->type);
 }
 
-Pointer::Buttons StylusPointer::getPressedButtons() {
+Pointer::Buttons StylusPointer::getPressedButtons(const PickResultPointer& pickResult) {
     // TODO: custom buttons for styluses
     Pointer::Buttons toReturn({ "Primary", "Focus" });
     return toReturn;
 }
 
-PointerEvent StylusPointer::buildPointerEvent(const PickedObject& target, const PickResultPointer& pickResult, bool hover) const {
+PointerEvent StylusPointer::buildPointerEvent(const PickedObject& target, const PickResultPointer& pickResult, const std::string& button, bool hover) {
     QUuid pickedID;
     glm::vec2 pos2D;
     glm::vec3 intersection, surfaceNormal, direction, origin;
@@ -177,18 +173,22 @@ PointerEvent StylusPointer::buildPointerEvent(const PickedObject& target, const
     }
 
     // If we just started triggering and we haven't moved too much, don't update intersection and pos2D
-    if (!_state.triggeredObject.objectID.isNull() && usecTimestampNow() - _state.triggerStartTime < STYLUS_MOVE_DELAY &&
-            glm::distance2(pos2D, _state.triggerPos2D) < TOUCH_PRESS_TO_MOVE_DEADSPOT_SQUARED) {
+    float sensorToWorldScale = DependencyManager::get<AvatarManager>()->getMyAvatar()->getSensorToWorldScale();
+    float deadspotSquared = TOUCH_PRESS_TO_MOVE_DEADSPOT_SQUARED * sensorToWorldScale * sensorToWorldScale;
+    bool withinDeadspot = usecTimestampNow() - _state.triggerStartTime < POINTER_MOVE_DELAY && glm::distance2(pos2D, _state.triggerPos2D) < deadspotSquared;
+    if ((_state.triggering || _state.wasTriggering) && !_state.deadspotExpired && withinDeadspot) {
         pos2D = _state.triggerPos2D;
         intersection = _state.intersection;
     } else if (pickedID != target.objectID) {
         intersection = findIntersection(target, origin, direction);
     }
+    if (!withinDeadspot) {
+        _state.deadspotExpired = true;
+    }
 
     return PointerEvent(pos2D, intersection, surfaceNormal, direction);
 }
 
-
 bool StylusPointer::isWithinBounds(float distance, float min, float max, float hysteresis) {
     return (distance == glm::clamp(distance, min - hysteresis, max + hysteresis));
 }
diff --git a/interface/src/raypick/StylusPointer.h b/interface/src/raypick/StylusPointer.h
index 9c69915108..950b03b7c9 100644
--- a/interface/src/raypick/StylusPointer.h
+++ b/interface/src/raypick/StylusPointer.h
@@ -37,11 +37,11 @@ public:
 
 protected:
     PickedObject getHoveredObject(const PickResultPointer& pickResult) override;
-    Buttons getPressedButtons() override;
+    Buttons getPressedButtons(const PickResultPointer& pickResult) override;
     bool shouldHover(const PickResultPointer& pickResult) override;
     bool shouldTrigger(const PickResultPointer& pickResult) override;
 
-    PointerEvent buildPointerEvent(const PickedObject& target, const PickResultPointer& pickResult, bool hover = true) const override;
+    PointerEvent buildPointerEvent(const PickedObject& target, const PickResultPointer& pickResult, const std::string& button = "", bool hover = true) override;
 
 private:
     void show(const StylusTip& tip);
@@ -53,7 +53,9 @@ private:
         glm::vec2 triggerPos2D { NAN };
         glm::vec3 surfaceNormal { NAN };
         quint64 triggerStartTime { 0 };
+        bool deadspotExpired { true };
         bool triggering { false };
+        bool wasTriggering { false };
 
         bool hovering { false };
     };
diff --git a/interface/src/ui/PreferencesDialog.cpp b/interface/src/ui/PreferencesDialog.cpp
index 023d3e6e17..1a09af07ab 100644
--- a/interface/src/ui/PreferencesDialog.cpp
+++ b/interface/src/ui/PreferencesDialog.cpp
@@ -90,11 +90,19 @@ void setupPreferences() {
         preference->setMax(500);
         preferences->addPreference(preference);
     }
+    {
+        auto getter = []()->bool { return qApp->getPreferStylusOverLaser(); };
+        auto setter = [](bool value) { qApp->setPreferStylusOverLaser(value); };
+        preferences->addPreference(new CheckPreference(UI_CATEGORY, "Prefer Stylus Over Laser", getter, setter));
+    }
+    // FIXME: Remove setting completely or make available through JavaScript API?
+    /*
     {
         auto getter = []()->bool { return qApp->getPreferAvatarFingerOverStylus(); };
         auto setter = [](bool value) { qApp->setPreferAvatarFingerOverStylus(value); };
         preferences->addPreference(new CheckPreference(UI_CATEGORY, "Prefer Avatar Finger Over Stylus", getter, setter));
     }
+    */
     {
         static const QString RETICLE_ICON_NAME = { Cursor::Manager::getIconName(Cursor::Icon::RETICLE) };
         auto getter = []()->bool { return qApp->getPreferredCursor() == RETICLE_ICON_NAME; };
diff --git a/libraries/pointers/src/Pointer.cpp b/libraries/pointers/src/Pointer.cpp
index fcebb2a23f..5307e17355 100644
--- a/libraries/pointers/src/Pointer.cpp
+++ b/libraries/pointers/src/Pointer.cpp
@@ -11,6 +11,12 @@
 #include "PickManager.h"
 #include "PointerManager.h"
 
+#include "NumericalConstants.h"
+
+const float Pointer::POINTER_MOVE_DELAY = 0.33f * USECS_PER_SECOND;
+const float TOUCH_PRESS_TO_MOVE_DEADSPOT = 0.0481f;
+const float Pointer::TOUCH_PRESS_TO_MOVE_DEADSPOT_SQUARED = TOUCH_PRESS_TO_MOVE_DEADSPOT * TOUCH_PRESS_TO_MOVE_DEADSPOT;
+
 Pointer::~Pointer() {
     DependencyManager::get<PickManager>()->removePick(_pickUID);
 }
@@ -77,7 +83,7 @@ void Pointer::generatePointerEvents(unsigned int pointerID, const PickResultPoin
     Buttons newButtons;
     Buttons sameButtons;
     if (_enabled && shouldTrigger(pickResult)) {
-        buttons = getPressedButtons();
+        buttons = getPressedButtons(pickResult);
         for (const std::string& button : buttons) {
             if (_prevButtons.find(button) == _prevButtons.end()) {
                 newButtons.insert(button);
@@ -175,17 +181,6 @@ void Pointer::generatePointerEvents(unsigned int pointerID, const PickResultPoin
         }
     }
 
-    // send hoverEnd events if we disable the pointer or disable hovering
-    if (_hover && ((!_enabled && _prevEnabled) || (!doHover && _prevDoHover))) {
-        if (_prevHoveredObject.type == ENTITY) {
-            emit pointerManager->hoverEndEntity(_prevHoveredObject.objectID, hoveredEvent);
-        } else if (_prevHoveredObject.type == OVERLAY) {
-            emit pointerManager->hoverEndOverlay(_prevHoveredObject.objectID, hoveredEvent);
-        } else if (_prevHoveredObject.type == HUD) {
-            emit pointerManager->hoverEndHUD(hoveredEvent);
-        }
-    }
-
     // Trigger begin
     const std::string SHOULD_FOCUS_BUTTON = "Focus";
     for (const std::string& button : newButtons) {
@@ -204,7 +199,7 @@ void Pointer::generatePointerEvents(unsigned int pointerID, const PickResultPoin
 
     // Trigger continue
     for (const std::string& button : sameButtons) {
-        PointerEvent triggeredEvent = buildPointerEvent(_triggeredObjects[button], pickResult, false);
+        PointerEvent triggeredEvent = buildPointerEvent(_triggeredObjects[button], pickResult, button, false);
         triggeredEvent.setID(pointerID);
         triggeredEvent.setType(PointerEvent::Move);
         triggeredEvent.setButton(chooseButton(button));
@@ -219,7 +214,7 @@ void Pointer::generatePointerEvents(unsigned int pointerID, const PickResultPoin
 
     // Trigger end
     for (const std::string& button : _prevButtons) {
-        PointerEvent triggeredEvent = buildPointerEvent(_triggeredObjects[button], pickResult, false);
+        PointerEvent triggeredEvent = buildPointerEvent(_triggeredObjects[button], pickResult, button, false);
         triggeredEvent.setID(pointerID);
         triggeredEvent.setType(PointerEvent::Release);
         triggeredEvent.setButton(chooseButton(button));
@@ -233,6 +228,17 @@ void Pointer::generatePointerEvents(unsigned int pointerID, const PickResultPoin
         _triggeredObjects.erase(button);
     }
 
+    // if we disable the pointer or disable hovering, send hoverEnd events after triggerEnd
+    if (_hover && ((!_enabled && _prevEnabled) || (!doHover && _prevDoHover))) {
+        if (_prevHoveredObject.type == ENTITY) {
+            emit pointerManager->hoverEndEntity(_prevHoveredObject.objectID, hoveredEvent);
+        } else if (_prevHoveredObject.type == OVERLAY) {
+            emit pointerManager->hoverEndOverlay(_prevHoveredObject.objectID, hoveredEvent);
+        } else if (_prevHoveredObject.type == HUD) {
+            emit pointerManager->hoverEndHUD(hoveredEvent);
+        }
+    }
+
     _prevHoveredObject = hoveredObject;
     _prevButtons = buttons;
     _prevEnabled = _enabled;
diff --git a/libraries/pointers/src/Pointer.h b/libraries/pointers/src/Pointer.h
index 5ee55e7aeb..3197c80cad 100644
--- a/libraries/pointers/src/Pointer.h
+++ b/libraries/pointers/src/Pointer.h
@@ -82,14 +82,17 @@ protected:
     bool _enabled;
     bool _hover;
 
-    virtual PointerEvent buildPointerEvent(const PickedObject& target, const PickResultPointer& pickResult, bool hover = true) const = 0;
+    virtual PointerEvent buildPointerEvent(const PickedObject& target, const PickResultPointer& pickResult, const std::string& button = "", bool hover = true) = 0;
 
     virtual PickedObject getHoveredObject(const PickResultPointer& pickResult) = 0;
-    virtual Buttons getPressedButtons() = 0;
+    virtual Buttons getPressedButtons(const PickResultPointer& pickResult) = 0;
 
     virtual bool shouldHover(const PickResultPointer& pickResult) { return true; }
     virtual bool shouldTrigger(const PickResultPointer& pickResult) { return true; }
 
+    static const float POINTER_MOVE_DELAY;
+    static const float TOUCH_PRESS_TO_MOVE_DEADSPOT_SQUARED;
+
 private:
     PickedObject _prevHoveredObject;
     Buttons _prevButtons;
diff --git a/scripts/system/controllers/controllerDispatcher.js b/scripts/system/controllers/controllerDispatcher.js
index 51f927f224..16f1d086b7 100644
--- a/scripts/system/controllers/controllerDispatcher.js
+++ b/scripts/system/controllers/controllerDispatcher.js
@@ -145,11 +145,11 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js");
             return deltaTime;
         };
 
-        this.setIgnoreTablet = function() {
+        this.setIgnorePointerItems = function() {
             if (HMD.tabletID !== this.tabletID) {
                 this.tabletID = HMD.tabletID;
-                Pointers.setIgnoreItems(_this.leftPointer, _this.blacklist.concat([HMD.tabletID]));
-                Pointers.setIgnoreItems(_this.rightPointer, _this.blacklist.concat([HMD.tabletID]));
+                Pointers.setIgnoreItems(_this.leftPointer, _this.blacklist);
+                Pointers.setIgnoreItems(_this.rightPointer, _this.blacklist);
             }
         };
 
@@ -168,7 +168,7 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js");
             }
             var sensorScaleFactor = MyAvatar.sensorToWorldScale;
             var deltaTime = _this.updateTimings();
-            _this.setIgnoreTablet();
+            _this.setIgnorePointerItems();
 
             if (controllerDispatcherPluginsNeedSort) {
                 _this.orderedPluginNames = [];
@@ -182,16 +182,6 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js");
                         controllerDispatcherPlugins[b].parameters.priority;
                 });
 
-                var output = "controllerDispatcher -- new plugin order: ";
-                for (var k = 0; k < _this.orderedPluginNames.length; k++) {
-                    var dbgPluginName = _this.orderedPluginNames[k];
-                    var priority = controllerDispatcherPlugins[dbgPluginName].parameters.priority;
-                    output += dbgPluginName + ":" + priority;
-                    if (k + 1 < _this.orderedPluginNames.length) {
-                        output += ", ";
-                    }
-                }
-
                 controllerDispatcherPluginsNeedSort = false;
             }
 
@@ -388,8 +378,8 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js");
         };
 
         this.setBlacklist = function() {
-            RayPick.setIgnoreItems(_this.leftControllerRayPick, this.blacklist.concat(HMD.tabletID));
-            RayPick.setIgnoreItems(_this.rightControllerRayPick, this.blacklist.concat(HMD.tabletID));
+            RayPick.setIgnoreItems(_this.leftControllerRayPick, this.blacklist);
+            RayPick.setIgnoreItems(_this.rightControllerRayPick, this.blacklist);
         };
 
         var MAPPING_NAME = "com.highfidelity.controllerDispatcher";
diff --git a/scripts/system/controllers/controllerModules/farActionGrabEntity.js b/scripts/system/controllers/controllerModules/farActionGrabEntity.js
index 0d421bbaec..5e12252bc3 100644
--- a/scripts/system/controllers/controllerModules/farActionGrabEntity.js
+++ b/scripts/system/controllers/controllerModules/farActionGrabEntity.js
@@ -14,7 +14,7 @@
    PICK_MAX_DISTANCE, COLORS_GRAB_SEARCHING_HALF_SQUEEZE, COLORS_GRAB_SEARCHING_FULL_SQUEEZE, COLORS_GRAB_DISTANCE_HOLD,
    DEFAULT_SEARCH_SPHERE_DISTANCE, TRIGGER_OFF_VALUE, TRIGGER_ON_VALUE, ZERO_VEC, ensureDynamic,
    getControllerWorldLocation, projectOntoEntityXYPlane, ContextOverlay, HMD, Reticle, Overlays, isPointingAtUI
-   Picks, makeLaserLockInfo Xform
+   Picks, makeLaserLockInfo Xform, makeLaserParams
 */
 
 Script.include("/~/system/libraries/controllerDispatcherUtils.js");
@@ -119,7 +119,7 @@ Script.include("/~/system/libraries/Xform.js");
             this.hand === RIGHT_HAND ? ["rightHand"] : ["leftHand"],
             [],
             100,
-            this.hand);
+            makeLaserParams(this.hand, false));
 
 
         this.handToController = function() {
diff --git a/scripts/system/controllers/controllerModules/farTrigger.js b/scripts/system/controllers/controllerModules/farTrigger.js
index 24f336d581..70e9ceff16 100644
--- a/scripts/system/controllers/controllerModules/farTrigger.js
+++ b/scripts/system/controllers/controllerModules/farTrigger.js
@@ -9,7 +9,7 @@
 /* global Script, Controller, LaserPointers, RayPick, RIGHT_HAND, LEFT_HAND, MyAvatar, getGrabPointSphereOffset,
    makeRunningValues, Entities, enableDispatcherModule, disableDispatcherModule, makeDispatcherModuleParameters,
    PICK_MAX_DISTANCE, COLORS_GRAB_SEARCHING_HALF_SQUEEZE, COLORS_GRAB_SEARCHING_FULL_SQUEEZE, COLORS_GRAB_DISTANCE_HOLD,
-   DEFAULT_SEARCH_SPHERE_DISTANCE, getGrabbableData
+   DEFAULT_SEARCH_SPHERE_DISTANCE, getGrabbableData, makeLaserParams
 */
 
 Script.include("/~/system/libraries/controllerDispatcherUtils.js");
@@ -34,7 +34,7 @@ Script.include("/~/system/libraries/controllers.js");
             this.hand === RIGHT_HAND ? ["rightHand"] : ["leftHand"],
             [],
             100,
-            this.hand);
+            makeLaserParams(this.hand, false));
 
         this.getTargetProps = function (controllerData) {
             // nearbyEntityProperties is already sorted by length from controller
diff --git a/scripts/system/controllers/controllerModules/hudOverlayPointer.js b/scripts/system/controllers/controllerModules/hudOverlayPointer.js
index a1faacbdd6..a7a186b07a 100644
--- a/scripts/system/controllers/controllerModules/hudOverlayPointer.js
+++ b/scripts/system/controllers/controllerModules/hudOverlayPointer.js
@@ -16,7 +16,8 @@
    makeDispatcherModuleParameters, MSECS_PER_SEC, HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION,
    PICK_MAX_DISTANCE, COLORS_GRAB_SEARCHING_HALF_SQUEEZE, COLORS_GRAB_SEARCHING_FULL_SQUEEZE, COLORS_GRAB_DISTANCE_HOLD,
    DEFAULT_SEARCH_SPHERE_DISTANCE, TRIGGER_OFF_VALUE, TRIGGER_ON_VALUE, ZERO_VEC, ensureDynamic,
-   getControllerWorldLocation, projectOntoEntityXYPlane, ContextOverlay, HMD, Reticle, Overlays, isPointingAtUI
+   getControllerWorldLocation, projectOntoEntityXYPlane, ContextOverlay, HMD, Reticle, Overlays, isPointingAtUI,
+   makeLaserParams
 
 */
 (function() {
@@ -36,7 +37,7 @@
             this.hand === RIGHT_HAND ? ["rightHand"] : ["leftHand"],
             [],
             100,
-            (this.hand + HUD_LASER_OFFSET));
+            makeLaserParams((this.hand + HUD_LASER_OFFSET), false));
 
         this.getOtherHandController = function() {
             return (this.hand === RIGHT_HAND) ? Controller.Standard.LeftHand : Controller.Standard.RightHand;
diff --git a/scripts/system/controllers/controllerModules/inEditMode.js b/scripts/system/controllers/controllerModules/inEditMode.js
index f79b2db1e2..763258573d 100644
--- a/scripts/system/controllers/controllerModules/inEditMode.js
+++ b/scripts/system/controllers/controllerModules/inEditMode.js
@@ -10,7 +10,7 @@
 /* global Script, Controller, RIGHT_HAND, LEFT_HAND, enableDispatcherModule, disableDispatcherModule, makeRunningValues,
    Messages, makeDispatcherModuleParameters, HMD, getGrabPointSphereOffset, COLORS_GRAB_SEARCHING_HALF_SQUEEZE,
    COLORS_GRAB_SEARCHING_FULL_SQUEEZE, COLORS_GRAB_DISTANCE_HOLD, DEFAULT_SEARCH_SPHERE_DISTANCE, TRIGGER_ON_VALUE,
-   getEnabledModuleByName, PICK_MAX_DISTANCE, isInEditMode, LaserPointers, RayPick, Picks
+   getEnabledModuleByName, PICK_MAX_DISTANCE, isInEditMode, LaserPointers, RayPick, Picks, makeLaserParams
 */
 
 Script.include("/~/system/libraries/controllerDispatcherUtils.js");
@@ -28,7 +28,7 @@ Script.include("/~/system/libraries/utils.js");
             this.hand === RIGHT_HAND ? ["rightHand", "rightHandEquip", "rightHandTrigger"] : ["leftHand", "leftHandEquip", "leftHandTrigger"],
             [],
             100,
-            this.hand);
+            makeLaserParams(this.hand, false));
 
         this.nearTablet = function(overlays) {
             for (var i = 0; i < overlays.length; i++) {
@@ -44,10 +44,7 @@ Script.include("/~/system/libraries/utils.js");
         };
 
         this.pointingAtTablet = function(objectID) {
-            if (objectID === HMD.tabletScreenID || objectID === HMD.homeButtonID) {
-                return true;
-            }
-            return false;
+            return objectID === HMD.tabletScreenID || objectID === HMD.homeButtonID;
         };
 
         this.sendPickData = function(controllerData) {
@@ -99,8 +96,8 @@ Script.include("/~/system/libraries/utils.js");
             var overlayLaser = getEnabledModuleByName(this.hand === RIGHT_HAND ? "RightWebSurfaceLaserInput" : "LeftWebSurfaceLaserInput");
             if (overlayLaser) {
                 var overlayLaserReady = overlayLaser.isReady(controllerData);
-
-                if (overlayLaserReady.active && this.pointingAtTablet(overlayLaser.target)) {
+                var target = controllerData.rayPicks[this.hand].objectID;
+                if (overlayLaserReady.active && this.pointingAtTablet(target)) {
                     return this.exitModule();
                 }
             }
diff --git a/scripts/system/controllers/controllerModules/inVREditMode.js b/scripts/system/controllers/controllerModules/inVREditMode.js
index e3035b26f2..38eca65dd3 100644
--- a/scripts/system/controllers/controllerModules/inVREditMode.js
+++ b/scripts/system/controllers/controllerModules/inVREditMode.js
@@ -9,7 +9,7 @@
 //  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
 
 /* global Script, MyAvatar, RIGHT_HAND, LEFT_HAND, enableDispatcherModule, disableDispatcherModule,
-   makeDispatcherModuleParameters, makeRunningValues, getEnabledModuleByName
+   makeDispatcherModuleParameters, makeRunningValues, getEnabledModuleByName, makeLaserParams
 */
 
 Script.include("/~/system/libraries/controllerDispatcherUtils.js");
@@ -19,15 +19,21 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js");
     function InVREditMode(hand) {
         this.hand = hand;
         this.disableModules = false;
+        var NO_HAND_LASER = -1; // Invalid hand parameter so that default laser is not displayed.
         this.parameters = makeDispatcherModuleParameters(
             200, // Not too high otherwise the tablet laser doesn't work.
             this.hand === RIGHT_HAND
                 ? ["rightHand", "rightHandEquip", "rightHandTrigger"]
                 : ["leftHand", "leftHandEquip", "leftHandTrigger"],
             [],
-            100
+            100,
+            makeLaserParams(NO_HAND_LASER, false)
         );
 
+        this.pointingAtTablet = function (objectID) {
+            return objectID === HMD.tabletScreenID || objectID === HMD.homeButtonID;
+        };
+
         this.isReady = function (controllerData) {
             if (this.disableModules) {
                 return makeRunningValues(true, [], []);
@@ -42,7 +48,6 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js");
             }
 
             // Tablet stylus.
-            // Includes the tablet laser.
             var tabletStylusInput = getEnabledModuleByName(this.hand === RIGHT_HAND
                 ? "RightTabletStylusInput"
                 : "LeftTabletStylusInput");
@@ -53,6 +58,18 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js");
                 }
             }
 
+            // Tablet surface.
+            var overlayLaser = getEnabledModuleByName(this.hand === RIGHT_HAND
+                ? "RightWebSurfaceLaserInput"
+                : "LeftWebSurfaceLaserInput");
+            if (overlayLaser) {
+                var overlayLaserReady = overlayLaser.isReady(controllerData);
+                var target = controllerData.rayPicks[this.hand].objectID;
+                if (overlayLaserReady.active && this.pointingAtTablet(target)) {
+                    return makeRunningValues(false, [], []);
+                }
+            }
+
             // Tablet grabbing.
             var nearOverlay = getEnabledModuleByName(this.hand === RIGHT_HAND
                 ? "RightNearParentingGrabOverlay"
diff --git a/scripts/system/controllers/controllerModules/stylusInput.js b/scripts/system/controllers/controllerModules/stylusInput.js
index d9fa0f76a9..aa65135289 100644
--- a/scripts/system/controllers/controllerModules/stylusInput.js
+++ b/scripts/system/controllers/controllerModules/stylusInput.js
@@ -142,7 +142,10 @@ Script.include("/~/system/libraries/controllers.js");
         };
 
         this.isReady = function (controllerData) {
-            if (this.processStylus(controllerData)) {
+            var PREFER_STYLUS_OVER_LASER = "preferStylusOverLaser";
+            var isUsingStylus = Settings.getValue(PREFER_STYLUS_OVER_LASER, false);
+
+            if (isUsingStylus && this.processStylus(controllerData)) {
                 Pointers.enablePointer(this.pointer);
                 return makeRunningValues(true, [], []);
             } else {
diff --git a/scripts/system/controllers/controllerModules/webSurfaceLaserInput.js b/scripts/system/controllers/controllerModules/webSurfaceLaserInput.js
index bf90124cab..3d9d7979d5 100644
--- a/scripts/system/controllers/controllerModules/webSurfaceLaserInput.js
+++ b/scripts/system/controllers/controllerModules/webSurfaceLaserInput.js
@@ -9,7 +9,7 @@
    makeRunningValues, Messages, Quat, Vec3, makeDispatcherModuleParameters, Overlays, ZERO_VEC, HMD,
    INCHES_TO_METERS, DEFAULT_REGISTRATION_POINT, getGrabPointSphereOffset, COLORS_GRAB_SEARCHING_HALF_SQUEEZE,
    COLORS_GRAB_SEARCHING_FULL_SQUEEZE, COLORS_GRAB_DISTANCE_HOLD, DEFAULT_SEARCH_SPHERE_DISTANCE, TRIGGER_ON_VALUE,
-   TRIGGER_OFF_VALUE, getEnabledModuleByName, PICK_MAX_DISTANCE, LaserPointers, RayPick, ContextOverlay, Picks
+   TRIGGER_OFF_VALUE, getEnabledModuleByName, PICK_MAX_DISTANCE, LaserPointers, RayPick, ContextOverlay, Picks, makeLaserParams
 */
 
 Script.include("/~/system/libraries/controllerDispatcherUtils.js");
@@ -18,6 +18,7 @@ Script.include("/~/system/libraries/controllers.js");
 (function() {
     function WebSurfaceLaserInput(hand) {
         this.hand = hand;
+        this.otherHand = this.hand === RIGHT_HAND ? LEFT_HAND : RIGHT_HAND;
         this.running = false;
 
         this.parameters = makeDispatcherModuleParameters(
@@ -25,7 +26,7 @@ Script.include("/~/system/libraries/controllers.js");
             this.hand === RIGHT_HAND ? ["rightHand"] : ["leftHand"],
             [],
             100,
-            this.hand);
+            makeLaserParams(hand, true));
 
         this.grabModuleWantsNearbyOverlay = function(controllerData) {
             if (controllerData.triggerValues[this.hand] > TRIGGER_ON_VALUE) {
@@ -65,7 +66,8 @@ Script.include("/~/system/libraries/controllers.js");
         };
 
         this.deleteContextOverlay = function() {
-            var farGrabModule = getEnabledModuleByName(this.hand === RIGHT_HAND ? "RightFarActionGrabEntity" : "LeftFarActionGrabEntity");
+            var farGrabModule = getEnabledModuleByName(this.hand === RIGHT_HAND
+                ? "RightFarActionGrabEntity" : "LeftFarActionGrabEntity");
             if (farGrabModule) {
                 var entityWithContextOverlay = farGrabModule.entityWithContextOverlay;
 
@@ -76,25 +78,50 @@ Script.include("/~/system/libraries/controllers.js");
             }
         };
 
-        this.isReady = function (controllerData) {
+        this.updateAllwaysOn = function() {
+            var PREFER_STYLUS_OVER_LASER = "preferStylusOverLaser";
+            this.parameters.handLaser.allwaysOn = !Settings.getValue(PREFER_STYLUS_OVER_LASER, false);
+        };
+
+        this.getDominantHand = function() {
+            return MyAvatar.getDominantHand() === "right" ? 1 : 0;
+        };
+
+        this.dominantHandOverride = false;
+
+        this.isReady = function(controllerData) {
             var otherModuleRunning = this.getOtherModule().running;
-            if ((this.isPointingAtOverlay(controllerData) || this.isPointingAtWebEntity(controllerData)) &&
-                !otherModuleRunning) {
-                if (controllerData.triggerValues[this.hand] > TRIGGER_OFF_VALUE) {
+            otherModuleRunning = otherModuleRunning && this.getDominantHand() !== this.hand; // Auto-swap to dominant hand.
+            var isTriggerPressed = controllerData.triggerValues[this.hand] > TRIGGER_OFF_VALUE
+                && controllerData.triggerValues[this.otherHand] <= TRIGGER_OFF_VALUE;
+            if ((!otherModuleRunning || isTriggerPressed)
+                    && (this.isPointingAtOverlay(controllerData) || this.isPointingAtWebEntity(controllerData))) {
+                this.updateAllwaysOn();
+                if (isTriggerPressed) {
+                    this.dominantHandOverride = true; // Override dominant hand.
+                    this.getOtherModule().dominantHandOverride = false;
+                }
+                if (this.parameters.handLaser.allwaysOn || isTriggerPressed) {
                     return makeRunningValues(true, [], []);
                 }
             }
             return makeRunningValues(false, [], []);
         };
 
-        this.run = function (controllerData, deltaTime) {
+        this.run = function(controllerData, deltaTime) {
+            var otherModuleRunning = this.getOtherModule().running;
+            otherModuleRunning = otherModuleRunning && this.getDominantHand() !== this.hand; // Auto-swap to dominant hand.
+            otherModuleRunning = otherModuleRunning || this.getOtherModule().dominantHandOverride; // Override dominant hand.
             var grabModuleNeedsToRun = this.grabModuleWantsNearbyOverlay(controllerData);
-            if (controllerData.triggerValues[this.hand] > TRIGGER_OFF_VALUE && !grabModuleNeedsToRun) {
+            if (!otherModuleRunning && !grabModuleNeedsToRun && (controllerData.triggerValues[this.hand] > TRIGGER_OFF_VALUE
+                || this.parameters.handLaser.allwaysOn
+                    && (this.isPointingAtOverlay(controllerData) || this.isPointingAtWebEntity(controllerData)))) {
                 this.running = true;
                 return makeRunningValues(true, [], []);
             }
             this.deleteContextOverlay();
             this.running = false;
+            this.dominantHandOverride = false;
             return makeRunningValues(false, [], []);
         };
     }
diff --git a/scripts/system/libraries/WebTablet.js b/scripts/system/libraries/WebTablet.js
index 4217ec503e..05b4963280 100644
--- a/scripts/system/libraries/WebTablet.js
+++ b/scripts/system/libraries/WebTablet.js
@@ -118,7 +118,8 @@ WebTablet = function (url, width, dpi, hand, clientOnly, location, visible) {
         Overlays.deleteOverlay(this.webOverlayID);
     }
 
-    var WEB_ENTITY_Z_OFFSET = (tabletDepth / 2.0) / sensorScaleFactor;
+    var RAYPICK_OFFSET = 0.0001; // Sufficient for raypick to reliably intersect tablet screen before tablet model.
+    var WEB_ENTITY_Z_OFFSET = (tabletDepth / 2.0) / sensorScaleFactor + RAYPICK_OFFSET;
     var WEB_ENTITY_Y_OFFSET = 0.004;
     var screenWidth = 0.82 * tabletWidth;
     var screenHeight = 0.81 * tabletHeight;
diff --git a/scripts/system/libraries/controllerDispatcherUtils.js b/scripts/system/libraries/controllerDispatcherUtils.js
index e0971201e6..915cfc05fb 100644
--- a/scripts/system/libraries/controllerDispatcherUtils.js
+++ b/scripts/system/libraries/controllerDispatcherUtils.js
@@ -45,6 +45,7 @@
    BUMPER_ON_VALUE:true,
    getEntityParents:true,
    findHandChildEntities:true,
+   makeLaserParams:true,
    TEAR_AWAY_DISTANCE:true,
    TEAR_AWAY_COUNT:true,
    TEAR_AWAY_CHECK_TIME:true,
@@ -134,6 +135,17 @@ makeLaserLockInfo = function(targetID, isOverlay, hand, offset) {
     };
 };
 
+makeLaserParams = function(hand, allwaysOn) {
+    if (allwaysOn === undefined) {
+        allwaysOn = false;
+    }
+
+    return {
+        hand: hand,
+        allwaysOn: allwaysOn
+    };
+};
+
 makeRunningValues = function (active, targets, requiredDataForRun, laserLockInfo) {
     return {
         active: active,
diff --git a/scripts/system/libraries/pointersUtils.js b/scripts/system/libraries/pointersUtils.js
index 896ef094b8..2af563f8d4 100644
--- a/scripts/system/libraries/pointersUtils.js
+++ b/scripts/system/libraries/pointersUtils.js
@@ -95,6 +95,7 @@ Pointer = function(hudLayer, pickType, pointerData) {
     this.pointerID = null;
     this.visible = false;
     this.locked = false;
+    this.allwaysOn = false;
     this.hand = pointerData.hand;
     delete pointerData.hand;
 
@@ -150,7 +151,7 @@ Pointer = function(hudLayer, pickType, pointerData) {
                 mode = "hold";
             } else if (triggerClicks[this.hand]) {
                 mode = "full";
-            } else if (triggerValues[this.hand] > TRIGGER_ON_VALUE) {
+            } else if (triggerValues[this.hand] > TRIGGER_ON_VALUE || this.allwaysOn) {
                 mode = "half";
             }
         }
@@ -172,19 +173,23 @@ PointerManager = function() {
         return pointer.pointerID;
     };
 
-    this.makePointerVisible = function(index) {
+    this.makePointerVisible = function(laserParams) {
+        var index = laserParams.hand;
         if (index < this.pointers.length && index >= 0) {
             this.pointers[index].makeVisible();
+            this.pointers[index].allwaysOn = laserParams.allwaysOn;
         }
     };
 
-    this.makePointerInvisible = function(index) {
+    this.makePointerInvisible = function(laserParams) {
+        var index = laserParams.hand;
         if (index < this.pointers.length && index >= 0) {
             this.pointers[index].makeInvisible();
         }
     };
 
-    this.lockPointerEnd = function(index, lockData) {
+    this.lockPointerEnd = function(laserParams, lockData) {
+        var index = laserParams.hand;
         if (index < this.pointers.length && index >= 0) {
             this.pointers[index].lockEnd(lockData);
         }
diff --git a/scripts/system/libraries/utils.js b/scripts/system/libraries/utils.js
index 4a1fcdf301..a6e2751a83 100644
--- a/scripts/system/libraries/utils.js
+++ b/scripts/system/libraries/utils.js
@@ -400,7 +400,8 @@ resizeTablet = function (width, newParentJointIndex, sensorToWorldScaleOverride)
     });
 
     // update webOverlay
-    var WEB_ENTITY_Z_OFFSET = (tabletDepth / 2.0) * sensorScaleOffsetOverride;
+    var RAYPICK_OFFSET = 0.0001; // Sufficient for raypick to reliably intersect tablet screen before tablet model.
+    var WEB_ENTITY_Z_OFFSET = (tabletDepth / 2.0) * sensorScaleOffsetOverride + RAYPICK_OFFSET;
     var WEB_ENTITY_Y_OFFSET = 0.004 * sensorScaleFactor * sensorScaleOffsetOverride;
     var screenWidth = 0.82 * tabletWidth;
     var screenHeight = 0.81 * tabletHeight;
@@ -417,11 +418,13 @@ resizeTablet = function (width, newParentJointIndex, sensorToWorldScaleOverride)
     var homeButtonDim = 4.0 * tabletScaleFactor / 3.0;
     Overlays.editOverlay(HMD.homeButtonID, {
         localPosition: { x: 0, y: -HOME_BUTTON_Y_OFFSET, z: -WEB_ENTITY_Z_OFFSET },
+        localRotation: Quat.angleAxis(180, Vec3.UNIT_Y),
         dimensions: { x: homeButtonDim, y: homeButtonDim, z: homeButtonDim }
     });
 
     Overlays.editOverlay(HMD.homeButtonHighlightID, {
         localPosition: { x: 0, y: -HOME_BUTTON_Y_OFFSET, z: -WEB_ENTITY_Z_OFFSET },
+        localRotation: Quat.angleAxis(180, Vec3.UNIT_Y),
         dimensions: { x: homeButtonDim, y: homeButtonDim, z: homeButtonDim }
     });
 };