From fc86525863df9eb5a2a01c948fa08f30e6cf616d Mon Sep 17 00:00:00 2001
From: Olivier Prat <olivier@zvork.fr>
Date: Wed, 4 Apr 2018 12:00:06 +0200
Subject: [PATCH 01/25] Cleaned up a bit shadow map clear

---
 libraries/render-utils/src/RenderShadowTask.cpp | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/libraries/render-utils/src/RenderShadowTask.cpp b/libraries/render-utils/src/RenderShadowTask.cpp
index 69c5b3c689..faa5889307 100644
--- a/libraries/render-utils/src/RenderShadowTask.cpp
+++ b/libraries/render-utils/src/RenderShadowTask.cpp
@@ -149,9 +149,7 @@ void RenderShadowMap::run(const render::RenderContextPointer& renderContext, con
         batch.setStateScissorRect(viewport);
 
         batch.setFramebuffer(fbo);
-        batch.clearFramebuffer(
-            gpu::Framebuffer::BUFFER_COLOR0 | gpu::Framebuffer::BUFFER_DEPTH,
-            vec4(vec3(1.0, 1.0, 1.0), 0.0), 1.0, 0, true);
+        batch.clearDepthFramebuffer(1.0, false);
 
         glm::mat4 projMat;
         Transform viewMat;

From 573f399023eafd625212284f827ea3207f006f76 Mon Sep 17 00:00:00 2001
From: Olivier Prat <olivier@zvork.fr>
Date: Fri, 6 Apr 2018 14:45:16 +0200
Subject: [PATCH 02/25] Fixed incorrect shadow frustum far clip computation due
 to not taking into account shadow receivers

---
 .../render-utils/src/RenderShadowTask.cpp     | 27 ++++++++++--------
 libraries/render-utils/src/RenderShadowTask.h |  2 +-
 libraries/render/src/render/CullTask.cpp      | 28 +++++++++++++------
 libraries/render/src/render/CullTask.h        |  2 +-
 4 files changed, 38 insertions(+), 21 deletions(-)

diff --git a/libraries/render-utils/src/RenderShadowTask.cpp b/libraries/render-utils/src/RenderShadowTask.cpp
index faa5889307..fbb4bba263 100644
--- a/libraries/render-utils/src/RenderShadowTask.cpp
+++ b/libraries/render-utils/src/RenderShadowTask.cpp
@@ -230,12 +230,11 @@ void RenderShadowTask::build(JobModel& task, const render::Varying& input, rende
     const auto queryResolution = setupOutput.getN<RenderShadowSetup::Outputs>(2);
     // Fetch and cull the items from the scene
 
-    // Enable models to not cast shadows (otherwise, models will always cast shadows)
-    static const auto shadowCasterFilter = ItemFilter::Builder::visibleWorldItems().withTypeShape().withOpaque().withoutLayered().withTagBits(tagBits, tagMask).withShadowCaster();
+    static const auto shadowCasterReceiverFilter = ItemFilter::Builder::visibleWorldItems().withTypeShape().withOpaque().withoutLayered().withTagBits(tagBits, tagMask);
 
-    const auto fetchInput = FetchSpatialTree::Inputs(shadowCasterFilter, queryResolution).asVarying();
+    const auto fetchInput = FetchSpatialTree::Inputs(shadowCasterReceiverFilter, queryResolution).asVarying();
     const auto shadowSelection = task.addJob<FetchSpatialTree>("FetchShadowTree", fetchInput);
-    const auto selectionInputs = FetchSpatialSelection::Inputs(shadowSelection, shadowCasterFilter).asVarying();
+    const auto selectionInputs = FetchSpatialSelection::Inputs(shadowSelection, shadowCasterReceiverFilter).asVarying();
     const auto shadowItems = task.addJob<FetchSpatialSelection>("FetchShadowSelection", selectionInputs);
 
     // Cull objects that are not visible in camera view. Hopefully the cull functor only performs LOD culling, not
@@ -259,21 +258,22 @@ void RenderShadowTask::build(JobModel& task, const render::Varying& input, rende
         char jobName[64];
         sprintf(jobName, "ShadowCascadeSetup%d", i);
         const auto cascadeSetupOutput = task.addJob<RenderShadowCascadeSetup>(jobName, i, _cullFunctor, tagBits, tagMask);
-        const auto shadowFilter = cascadeSetupOutput.getN<RenderShadowCascadeSetup::Outputs>(0);
+        const auto shadowRenderFilter = cascadeSetupOutput.getN<RenderShadowCascadeSetup::Outputs>(0);
+        const auto shadowBoundsFilter = cascadeSetupOutput.getN<RenderShadowCascadeSetup::Outputs>(1);
         auto antiFrustum = render::Varying(ViewFrustumPointer());
-        cascadeFrustums[i] = cascadeSetupOutput.getN<RenderShadowCascadeSetup::Outputs>(1);
+        cascadeFrustums[i] = cascadeSetupOutput.getN<RenderShadowCascadeSetup::Outputs>(2);
         if (i > 1) {
             antiFrustum = cascadeFrustums[i - 2];
         }
 
         // CPU jobs: finer grained culling
-        const auto cullInputs = CullShapeBounds::Inputs(sortedShapes, shadowFilter, antiFrustum).asVarying();
+        const auto cullInputs = CullShapeBounds::Inputs(sortedShapes, shadowRenderFilter, shadowBoundsFilter, antiFrustum).asVarying();
         const auto culledShadowItemsAndBounds = task.addJob<CullShapeBounds>("CullShadowCascade", cullInputs, shadowCullFunctor, RenderDetails::SHADOW);
 
         // GPU jobs: Render to shadow map
         sprintf(jobName, "RenderShadowMap%d", i);
         task.addJob<RenderShadowMap>(jobName, culledShadowItemsAndBounds, shapePlumber, i);
-        task.addJob<RenderShadowCascadeTeardown>("ShadowCascadeTeardown", shadowFilter);
+        task.addJob<RenderShadowCascadeTeardown>("ShadowCascadeTeardown", shadowRenderFilter);
     }
 
     task.addJob<RenderShadowTeardown>("ShadowTeardown", setupOutput);
@@ -404,7 +404,11 @@ void RenderShadowCascadeSetup::run(const render::RenderContextPointer& renderCon
 
     const auto globalShadow = lightStage->getCurrentKeyShadow();
     if (globalShadow && _cascadeIndex<globalShadow->getCascadeCount()) {
-        output.edit0() = ItemFilter::Builder::visibleWorldItems().withTypeShape().withOpaque().withoutLayered().withTagBits(_tagBits, _tagMask).withShadowCaster();
+        auto baseFilter = ItemFilter::Builder::visibleWorldItems().withTypeShape().withOpaque().withoutLayered().withTagBits(_tagBits, _tagMask);
+        // Second item filter is to filter items to keep in shadow frustum computation (here we need to keep shadow receivers)
+        output.edit1() = baseFilter;
+        // First item filter is to filter items to render in shadow map (so only keep casters)
+        output.edit0() = baseFilter.withShadowCaster();
 
         // Set the keylight render args
         auto& cascade = globalShadow->getCascade(_cascadeIndex);
@@ -417,10 +421,11 @@ void RenderShadowCascadeSetup::run(const render::RenderContextPointer& renderCon
         texelSize *= minTexelCount;
         _cullFunctor._minSquareSize = texelSize * texelSize;
 
-        output.edit1() = cascadeFrustum;
+        output.edit2() = cascadeFrustum;
     } else {
         output.edit0() = ItemFilter::Builder::nothing();
-        output.edit1() = ViewFrustumPointer();
+        output.edit1() = ItemFilter::Builder::nothing();
+        output.edit2() = ViewFrustumPointer();
     }
 }
 
diff --git a/libraries/render-utils/src/RenderShadowTask.h b/libraries/render-utils/src/RenderShadowTask.h
index 98b70c0c9f..19ffcb4234 100644
--- a/libraries/render-utils/src/RenderShadowTask.h
+++ b/libraries/render-utils/src/RenderShadowTask.h
@@ -118,7 +118,7 @@ private:
 
 class RenderShadowCascadeSetup {
 public:
-    using Outputs = render::VaryingSet2<render::ItemFilter, ViewFrustumPointer>;
+    using Outputs = render::VaryingSet3<render::ItemFilter, render::ItemFilter, ViewFrustumPointer>;
     using JobModel = render::Job::ModelO<RenderShadowCascadeSetup, Outputs>;
 
     RenderShadowCascadeSetup(unsigned int cascadeIndex, RenderShadowTask::CullFunctor& cullFunctor, uint8_t tagBits = 0x00, uint8_t tagMask = 0x00) : 
diff --git a/libraries/render/src/render/CullTask.cpp b/libraries/render/src/render/CullTask.cpp
index f04427540a..b5819f114f 100644
--- a/libraries/render/src/render/CullTask.cpp
+++ b/libraries/render/src/render/CullTask.cpp
@@ -368,17 +368,19 @@ void CullShapeBounds::run(const RenderContextPointer& renderContext, const Input
     RenderArgs* args = renderContext->args;
 
     const auto& inShapes = inputs.get0();
-    const auto& filter = inputs.get1();
-    const auto& antiFrustum = inputs.get2();
+	const auto& cullFilter = inputs.get1();
+	const auto& boundsFilter = inputs.get2();
+	const auto& antiFrustum = inputs.get3();
     auto& outShapes = outputs.edit0();
     auto& outBounds = outputs.edit1();
 
     outShapes.clear();
     outBounds = AABox();
 
-    if (!filter.selectsNothing()) {
+    if (!cullFilter.selectsNothing() || !boundsFilter.selectsNothing()) {
         auto& details = args->_details.edit(_detailType);
         Test test(_cullFunctor, args, details, antiFrustum);
+		auto scene = args->_scene;
 
         for (auto& inItems : inShapes) {
             auto key = inItems.first;
@@ -393,16 +395,26 @@ void CullShapeBounds::run(const RenderContextPointer& renderContext, const Input
             if (antiFrustum == nullptr) {
                 for (auto& item : inItems.second) {
                     if (test.solidAngleTest(item.bound) && test.frustumTest(item.bound)) {
-                        outItems->second.emplace_back(item);
-                        outBounds += item.bound;
+						const auto shapeKey = scene->getItem(item.id).getKey();
+						if (cullFilter.test(shapeKey)) {
+							outItems->second.emplace_back(item);
+						}
+						if (boundsFilter.test(shapeKey)) {
+							outBounds += item.bound;
+						}
                     }
                 }
             } else {
                 for (auto& item : inItems.second) {
                     if (test.solidAngleTest(item.bound) && test.frustumTest(item.bound) && test.antiFrustumTest(item.bound)) {
-                        outItems->second.emplace_back(item);
-                        outBounds += item.bound;
-                    }
+						const auto shapeKey = scene->getItem(item.id).getKey();
+						if (cullFilter.test(shapeKey)) {
+							outItems->second.emplace_back(item);
+						}
+						if (boundsFilter.test(shapeKey)) {
+							outBounds += item.bound;
+						}
+					}
                 }
             }
             details._rendered += (int)outItems->second.size();
diff --git a/libraries/render/src/render/CullTask.h b/libraries/render/src/render/CullTask.h
index 3c5a30de89..47abe8a960 100644
--- a/libraries/render/src/render/CullTask.h
+++ b/libraries/render/src/render/CullTask.h
@@ -110,7 +110,7 @@ namespace render {
 
     class CullShapeBounds {
     public:
-        using Inputs = render::VaryingSet3<ShapeBounds, ItemFilter, ViewFrustumPointer>;
+        using Inputs = render::VaryingSet4<ShapeBounds, ItemFilter, ItemFilter, ViewFrustumPointer>;
         using Outputs = render::VaryingSet2<ShapeBounds, AABox>;
         using JobModel = Job::ModelIO<CullShapeBounds, Inputs, Outputs>;
 

From dd8eac8cb2d156513c49b5fb551ac492ee9eff23 Mon Sep 17 00:00:00 2001
From: David Rowe <david@ctrlaltstudio.com>
Date: Fri, 4 May 2018 16:14:42 +1200
Subject: [PATCH 03/25] Remove superfluous keyboardControl.js file and
 references

The raiseAndLowerKeyboard.js script is automatically injected into these pages, instead.
---
 scripts/system/html/entityList.html       |  1 -
 scripts/system/html/entityProperties.html |  1 -
 scripts/system/html/gridControls.html     |  1 -
 scripts/system/html/js/keyboardControl.js | 73 -----------------------
 4 files changed, 76 deletions(-)
 delete mode 100644 scripts/system/html/js/keyboardControl.js

diff --git a/scripts/system/html/entityList.html b/scripts/system/html/entityList.html
index d608ab63e5..7906a3c97f 100644
--- a/scripts/system/html/entityList.html
+++ b/scripts/system/html/entityList.html
@@ -14,7 +14,6 @@
         <script type="text/javascript" src="qrc:///qtwebchannel/qwebchannel.js"></script>
         <script type="text/javascript" src="js/eventBridgeLoader.js"></script>
         <script type="text/javascript" src="js/spinButtons.js"></script>
-        <script type="text/javascript" src="js/keyboardControl.js"></script>
         <script type="text/javascript" src="js/entityList.js"></script>
     </head>
     <body onload='loaded();'>
diff --git a/scripts/system/html/entityProperties.html b/scripts/system/html/entityProperties.html
index 8647dca035..8d63261f4c 100644
--- a/scripts/system/html/entityProperties.html
+++ b/scripts/system/html/entityProperties.html
@@ -20,7 +20,6 @@
     <script type="text/javascript" src="qrc:///qtwebchannel/qwebchannel.js"></script>
     <script type="text/javascript" src="js/eventBridgeLoader.js"></script>
     <script type="text/javascript" src="js/spinButtons.js"></script>
-    <script type="text/javascript" src="js/keyboardControl.js"></script>
     <script type="text/javascript" src="js/entityProperties.js"></script>
     <script src="js/jsoneditor.min.js"></script>
 </head>
diff --git a/scripts/system/html/gridControls.html b/scripts/system/html/gridControls.html
index c0bd87988d..cd646fed51 100644
--- a/scripts/system/html/gridControls.html
+++ b/scripts/system/html/gridControls.html
@@ -16,7 +16,6 @@
         <script type="text/javascript" src="qrc:///qtwebchannel/qwebchannel.js"></script>
         <script type="text/javascript" src="js/eventBridgeLoader.js"></script>
         <script type="text/javascript" src="js/spinButtons.js"></script>
-        <script type="text/javascript" src="js/keyboardControl.js"></script>
         <script type="text/javascript" src="js/gridControls.js"></script>
     </head>
     <body onload='loaded();'>
diff --git a/scripts/system/html/js/keyboardControl.js b/scripts/system/html/js/keyboardControl.js
deleted file mode 100644
index 7a8a314c62..0000000000
--- a/scripts/system/html/js/keyboardControl.js
+++ /dev/null
@@ -1,73 +0,0 @@
-//
-//  keyboardControl.js
-//
-//  Created by David Rowe on 28 Sep 2016.
-//  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
-//
-
-function setUpKeyboardControl() {
-
-    var lowerTimer = null;
-    var isRaised = false;
-    var KEYBOARD_HEIGHT = 200;
-
-    function raiseKeyboard() {
-        window.isKeyboardRaised = true;
-        window.isNumericKeyboard = this.type === "number";
-
-        if (lowerTimer !== null) {
-            clearTimeout(lowerTimer);
-            lowerTimer = null;
-        }
-
-        EventBridge.emitWebEvent("_RAISE_KEYBOARD" + (this.type === "number" ? "_NUMERIC" : ""));
-
-        if (!isRaised) {
-            var delta = this.getBoundingClientRect().bottom + 10 - (document.body.clientHeight - KEYBOARD_HEIGHT);
-            if (delta > 0) {
-                setTimeout(function () {
-                    document.body.scrollTop += delta;
-                }, 500);  // Allow time for keyboard to be raised in QML.
-            }
-        }
-
-        isRaised = true;
-    }
-
-    function doLowerKeyboard() {
-        window.isKeyboardRaised = false;
-        window.isNumericKeyboard = false;
-
-        EventBridge.emitWebEvent("_LOWER_KEYBOARD");
-        lowerTimer = null;
-        isRaised = false;
-    }
-
-    function lowerKeyboard() {
-        // Delay lowering keyboard a little in case immediately raise it again.
-        if (lowerTimer === null) {
-            lowerTimer = setTimeout(doLowerKeyboard, 20);
-        }
-    }
-
-    function documentBlur() {
-        // Action any pending Lower keyboard event immediately upon leaving document window so that they don't interfere with
-        // other Entities Editor tab.
-        if (lowerTimer !== null) {
-            clearTimeout(lowerTimer);
-            doLowerKeyboard();
-        }
-    }
-
-    var inputs = document.querySelectorAll("input[type=text], input[type=password], input[type=number], textarea");
-    for (var i = 0, length = inputs.length; i < length; i++) {
-        inputs[i].addEventListener("focus", raiseKeyboard);
-        inputs[i].addEventListener("blur", lowerKeyboard);
-    }
-
-    window.addEventListener("blur", documentBlur);
-}
-

From 03da2f09461b54a2b7fc8b119d8454212f62c8f5 Mon Sep 17 00:00:00 2001
From: David Rowe <david@ctrlaltstudio.com>
Date: Fri, 4 May 2018 20:32:53 +1200
Subject: [PATCH 04/25] Only scroll keyboard if necessary

---
 interface/resources/html/raiseAndLowerKeyboard.js | 15 +++++++--------
 1 file changed, 7 insertions(+), 8 deletions(-)

diff --git a/interface/resources/html/raiseAndLowerKeyboard.js b/interface/resources/html/raiseAndLowerKeyboard.js
index a0aa1eb7fe..b2688e3db8 100644
--- a/interface/resources/html/raiseAndLowerKeyboard.js
+++ b/interface/resources/html/raiseAndLowerKeyboard.js
@@ -44,15 +44,14 @@
     };
 
     function scheduleBringToView(timeout) {
-
-        var timer = setTimeout(function () {
-            clearTimeout(timer);
-
+        setTimeout(function () {
+            // If the element is not visible because the keyboard has been raised over the top of it, scroll it into view.
             var elementRect = document.activeElement.getBoundingClientRect();
-            var absoluteElementTop = elementRect.top + window.scrollY;
-            var middle = absoluteElementTop - (window.innerHeight / 2);
-
-            window.scrollTo(0, middle);
+            var VISUAL_MARGIN = 3
+            var delta = elementRect.y + elementRect.height + VISUAL_MARGIN - window.innerHeight;
+            if (delta > 0) {
+                window.scrollBy(0, delta);
+            }
         }, timeout);
     }
 

From 1be948ee2846b02e98e974568798ff275cf40cf3 Mon Sep 17 00:00:00 2001
From: David Rowe <david@ctrlaltstudio.com>
Date: Sat, 5 May 2018 12:03:18 +1200
Subject: [PATCH 05/25] Scroll element into view if it's been moved off the
 screen

---
 interface/resources/html/raiseAndLowerKeyboard.js | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/interface/resources/html/raiseAndLowerKeyboard.js b/interface/resources/html/raiseAndLowerKeyboard.js
index b2688e3db8..15df94334c 100644
--- a/interface/resources/html/raiseAndLowerKeyboard.js
+++ b/interface/resources/html/raiseAndLowerKeyboard.js
@@ -45,12 +45,15 @@
 
     function scheduleBringToView(timeout) {
         setTimeout(function () {
-            // If the element is not visible because the keyboard has been raised over the top of it, scroll it into view.
+            // If the element is not visible because the keyboard has been raised over the top of it, scroll it up into view.
+            // If the element is not visible because the keyboard raising has moved it off screen, scroll it down into view.
             var elementRect = document.activeElement.getBoundingClientRect();
             var VISUAL_MARGIN = 3
             var delta = elementRect.y + elementRect.height + VISUAL_MARGIN - window.innerHeight;
             if (delta > 0) {
                 window.scrollBy(0, delta);
+            } else if (elementRect.y < VISUAL_MARGIN) {
+                window.scrollBy(0, elementRect.y - VISUAL_MARGIN);
             }
         }, timeout);
     }

From 3c2bee757a521e918d195772a88dc8e3913f50a4 Mon Sep 17 00:00:00 2001
From: David Rowe <david@ctrlaltstudio.com>
Date: Sat, 5 May 2018 12:14:40 +1200
Subject: [PATCH 06/25] Remove calls to function that's been removed

---
 scripts/system/html/js/entityList.js       | 2 --
 scripts/system/html/js/entityProperties.js | 4 +---
 scripts/system/html/js/gridControls.js     | 2 --
 3 files changed, 1 insertion(+), 7 deletions(-)

diff --git a/scripts/system/html/js/entityList.js b/scripts/system/html/js/entityList.js
index 625aa26b00..88b3ccbf7c 100644
--- a/scripts/system/html/js/entityList.js
+++ b/scripts/system/html/js/entityList.js
@@ -444,8 +444,6 @@ function loaded() {
 
   augmentSpinButtons();
 
-  setUpKeyboardControl();
-
   // Disable right-click context menu which is not visible in the HMD and makes it seem like the app has locked
   document.addEventListener("contextmenu", function (event) {
       event.preventDefault();
diff --git a/scripts/system/html/js/entityProperties.js b/scripts/system/html/js/entityProperties.js
index 4b6329db44..2194b539ef 100644
--- a/scripts/system/html/js/entityProperties.js
+++ b/scripts/system/html/js/entityProperties.js
@@ -7,7 +7,7 @@
 //  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
 
 /* global alert, augmentSpinButtons, clearTimeout, console, document, Element, EventBridge, 
-    HifiEntityUI, JSONEditor, openEventBridge, setUpKeyboardControl, setTimeout, window, _ $ */
+    HifiEntityUI, JSONEditor, openEventBridge, setTimeout, window, _ $ */
 
 var PI = 3.14159265358979;
 var DEGREES_TO_RADIANS = PI / 180.0;
@@ -2157,8 +2157,6 @@ function loaded() {
 
     augmentSpinButtons();
 
-    setUpKeyboardControl();
-
     // Disable right-click context menu which is not visible in the HMD and makes it seem like the app has locked
     document.addEventListener("contextmenu", function(event) {
         event.preventDefault();
diff --git a/scripts/system/html/js/gridControls.js b/scripts/system/html/js/gridControls.js
index be4271788e..79a169400a 100644
--- a/scripts/system/html/js/gridControls.js
+++ b/scripts/system/html/js/gridControls.js
@@ -129,8 +129,6 @@ function loaded() {
 
         augmentSpinButtons();
 
-        setUpKeyboardControl();
-
         EventBridge.emitWebEvent(JSON.stringify({ type: 'init' }));
     });
     document.addEventListener("keydown", function (keyDown) {

From 5e6b83a6346bde1d00fc148c073d0b7687a01692 Mon Sep 17 00:00:00 2001
From: David Rowe <david@ctrlaltstudio.com>
Date: Sat, 5 May 2018 12:14:51 +1200
Subject: [PATCH 07/25] Lint

---
 .../resources/html/raiseAndLowerKeyboard.js     | 17 +++++++++++------
 1 file changed, 11 insertions(+), 6 deletions(-)

diff --git a/interface/resources/html/raiseAndLowerKeyboard.js b/interface/resources/html/raiseAndLowerKeyboard.js
index 15df94334c..f40c0d7376 100644
--- a/interface/resources/html/raiseAndLowerKeyboard.js
+++ b/interface/resources/html/raiseAndLowerKeyboard.js
@@ -7,6 +7,9 @@
 //
 //  Sends messages over the EventBridge when text input is required.
 //
+
+/* global document, window, console, setTimeout, setInterval, EventBridge */
+
 (function () {
     var POLL_FREQUENCY = 500; // ms
     var MAX_WARNINGS = 3;
@@ -37,18 +40,18 @@
             }
             return false;
         }
-    };
+    }
 
     function shouldSetNumeric() {
         return document.activeElement.type === "number";
-    };
+    }
 
     function scheduleBringToView(timeout) {
         setTimeout(function () {
             // If the element is not visible because the keyboard has been raised over the top of it, scroll it up into view.
             // If the element is not visible because the keyboard raising has moved it off screen, scroll it down into view.
             var elementRect = document.activeElement.getBoundingClientRect();
-            var VISUAL_MARGIN = 3
+            var VISUAL_MARGIN = 3;
             var delta = elementRect.y + elementRect.height + VISUAL_MARGIN - window.innerHeight;
             if (delta > 0) {
                 window.scrollBy(0, delta);
@@ -64,11 +67,13 @@
         var passwordField = shouldSetPasswordField();
 
         if (isWindowFocused &&
-            (keyboardRaised !== window.isKeyboardRaised || numericKeyboard !== window.isNumericKeyboard || passwordField !== window.isPasswordField)) {
+            (keyboardRaised !== window.isKeyboardRaised || numericKeyboard !== window.isNumericKeyboard
+                || passwordField !== window.isPasswordField)) {
 
             if (typeof EventBridge !== "undefined" && EventBridge !== null) {
                 EventBridge.emitWebEvent(
-                    keyboardRaised ? ("_RAISE_KEYBOARD" + (numericKeyboard ? "_NUMERIC" : "") + (passwordField ? "_PASSWORD" : "")) : "_LOWER_KEYBOARD"
+                    keyboardRaised ? ("_RAISE_KEYBOARD" + (numericKeyboard ? "_NUMERIC" : "")
+                        + (passwordField ? "_PASSWORD" : "")) : "_LOWER_KEYBOARD"
                 );
             } else {
                 if (numWarnings < MAX_WARNINGS) {
@@ -79,7 +84,7 @@
 
             if (!window.isKeyboardRaised) {
                 scheduleBringToView(250); // Allow time for keyboard to be raised in QML.
-                                          // 2DO: should it be rather done from 'client area height changed' event?
+                // 2DO: should it be rather done from 'client area height changed' event?
             }
 
             window.isKeyboardRaised = keyboardRaised;

From 30c5fa6fa167edd515440ff301c3ffc6fd7dbec0 Mon Sep 17 00:00:00 2001
From: Seth Alves <seth.alves@gmail.com>
Date: Sat, 5 May 2018 20:45:06 -0700
Subject: [PATCH 08/25] fix divide-by-zero asan warning in
 LODManager::autoAdjustLOD

---
 interface/src/LODManager.cpp | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/interface/src/LODManager.cpp b/interface/src/LODManager.cpp
index d06ba14bcf..da1f14c450 100644
--- a/interface/src/LODManager.cpp
+++ b/interface/src/LODManager.cpp
@@ -70,7 +70,7 @@ void LODManager::autoAdjustLOD(float realTimeDelta) {
     // Note: we MUST clamp the blend to 1.0 for stability
     float blend = (realTimeDelta < LOD_ADJUST_RUNNING_AVG_TIMESCALE) ? realTimeDelta / LOD_ADJUST_RUNNING_AVG_TIMESCALE : 1.0f;
     _avgRenderTime = (1.0f - blend) * _avgRenderTime + blend * maxRenderTime; // msec
-    if (!_automaticLODAdjust) {
+    if (!_automaticLODAdjust || _avgRenderTime == 0.0f) {
         // early exit
         return;
     }

From ee4b1af3aa4ff93eb1348fb5d8a924b49237a9e2 Mon Sep 17 00:00:00 2001
From: NissimHadar <nissim.hadar@gmail.com>
Date: Tue, 8 May 2018 16:51:58 -0700
Subject: [PATCH 09/25] Corrected missing images in test.md files

---
 tools/auto-tester/src/Test.cpp | 76 ----------------------------------
 tools/auto-tester/src/Test.h   |  4 --
 2 files changed, 80 deletions(-)

diff --git a/tools/auto-tester/src/Test.cpp b/tools/auto-tester/src/Test.cpp
index 99f9025fdd..bdf734897e 100644
--- a/tools/auto-tester/src/Test.cpp
+++ b/tools/auto-tester/src/Test.cpp
@@ -478,26 +478,6 @@ ExtractedText Test::getTestScriptLines(QString testFileName) {
     QString regexTestTitle(ws + functionPerformName + "\\(" + quotedString + "\\," + ws + ownPath + "\\," + ws + functionParameter + ws + "{" + ".*");
     QRegularExpression lineContainingTitle = QRegularExpression(regexTestTitle);
 
-    // Assert platform checks that test is running on the correct OS
-    const QString functionAssertPlatform(ws + "autoTester" + ws + "\\." + ws + "assertPlatform");
-    const QString regexAssertPlatform(ws + functionAssertPlatform + ws + "\\(" + ws + quotedString + ".*");
-    const QRegularExpression lineAssertPlatform = QRegularExpression(regexAssertPlatform);
-
-    // Assert display checks that test is running on the correct display
-    const QString functionAssertDisplay(ws + "autoTester" + ws + "\\." + ws + "assertDisplay");
-    const QString regexAssertDisplay(ws + functionAssertDisplay + ws + "\\(" + ws + quotedString + ".*");
-    const QRegularExpression lineAssertDisplay = QRegularExpression(regexAssertDisplay);
-
-    // Assert CPU checks that test is running on the correct type of CPU 
-    const QString functionAssertCPU(ws + "autoTester" + ws + "\\." + ws + "assertCPU");
-    const QString regexAssertCPU(ws + functionAssertCPU + ws + "\\(" + ws + quotedString + ".*");
-    const QRegularExpression lineAssertCPU = QRegularExpression(regexAssertCPU);
-
-    // Assert GPU checks that test is running on the correct type of GPU 
-    const QString functionAssertGPU(ws + "autoTester" + ws + "\\." + ws + "assertGPU");
-    const QString regexAssertGPU(ws + functionAssertGPU + ws + "\\(" + ws + quotedString + ".*");
-    const QRegularExpression lineAssertGPU = QRegularExpression(regexAssertGPU);
-
     // Each step is either of the following forms:
     //        autoTester.addStepSnapshot("Take snapshot"...
     //        autoTester.addStep("Clean up after test"...
@@ -514,18 +494,6 @@ ExtractedText Test::getTestScriptLines(QString testFileName) {
         if (lineContainingTitle.match(line).hasMatch()) {
             QStringList tokens = line.split('"');
             relevantTextFromTest.title = tokens[1];
-        } else if (lineAssertPlatform.match(line).hasMatch()) {
-            QStringList platforms = line.split('"');
-            relevantTextFromTest.platform = platforms[1];
-        } else if (lineAssertDisplay.match(line).hasMatch()) {
-            QStringList displays = line.split('"');
-            relevantTextFromTest.display = displays[1];
-        } else if (lineAssertCPU.match(line).hasMatch()) {
-            QStringList cpus = line.split('"');
-            relevantTextFromTest.cpu = cpus[1];
-        } else if (lineAssertGPU.match(line).hasMatch()) {
-            QStringList gpus = line.split('"');
-            relevantTextFromTest.gpu = gpus[1];
         } else if (lineStepSnapshot.match(line).hasMatch()) {
             QStringList tokens = line.split('"');
             QString nameOfStep = tokens[1];
@@ -628,50 +596,6 @@ void Test::createMDFile(QString testDirectory) {
     stream << "## Preconditions" << "\n";
     stream << "- In an empty region of a domain with editing rights." << "\n\n";
 
-    // Platform
-    QStringList  platforms = testScriptLines.platform.split(" ");;
-    stream << "## Platforms\n";
-    stream << "Run the test on each of the following platforms\n";
-    for (int i = 0; i < platforms.size(); ++i) {
-        // Note that the platforms parameter may include extra spaces, these appear as empty strings in the list
-        if (platforms[i] != QString()) {
-            stream << " - " << platforms[i] << "\n";
-        }
-    }
-
-    // Display
-    QStringList  displays = testScriptLines.display.split(" ");
-    stream << "## Displays\n";
-    stream << "Run the test on each of the following displays\n";
-    for (int i = 0; i < displays.size(); ++i) {
-        // Note that the displays parameter may include extra spaces, these appear as empty strings in the list
-        if (displays[i] != QString()) {
-            stream << " - " << displays[i] << "\n";
-        }
-    }
-
-    // CPU
-    QStringList  cpus = testScriptLines.cpu.split(" ");
-    stream << "## Processors\n";
-    stream << "Run the test on each of the following processors\n";
-    for (int i = 0; i < cpus.size(); ++i) {
-        // Note that the cpus parameter may include extra spaces, these appear as empty strings in the list
-        if (cpus[i] != QString()) {
-            stream << " - " << cpus[i] << "\n";
-        }
-    }
-
-    // GPU
-    QStringList  gpus = testScriptLines.gpu.split(" ");
-    stream << "## Graphics Cards\n";
-    stream << "Run the test on graphics cards from each of the following vendors\n";
-    for (int i = 0; i < gpus.size(); ++i) {
-        // Note that the gpus parameter may include extra spaces, these appear as empty strings in the list
-        if (gpus[i] != QString()) {
-            stream << " - " << gpus[i] << "\n";
-        }
-    }
-
     stream << "## Steps\n";
     stream << "Press space bar to advance step by step\n\n";
 
diff --git a/tools/auto-tester/src/Test.h b/tools/auto-tester/src/Test.h
index e69459fef2..975d5dfdaf 100644
--- a/tools/auto-tester/src/Test.h
+++ b/tools/auto-tester/src/Test.h
@@ -30,10 +30,6 @@ using StepList = std::vector<Step*>;
 class ExtractedText {
 public:
     QString title;
-    QString platform;
-    QString display;
-    QString cpu;
-    QString gpu;
     StepList stepList;
 };
 

From 32f2494416a64a210a2da668be9a87485018e029 Mon Sep 17 00:00:00 2001
From: NissimHadar <nissim.hadar@gmail.com>
Date: Wed, 9 May 2018 11:58:16 -0700
Subject: [PATCH 10/25] Stores previous user-selected folders.

---
 tools/auto-tester/src/Test.cpp | 128 +++++++++++++++++++++++----------
 tools/auto-tester/src/Test.h   |   7 +-
 2 files changed, 96 insertions(+), 39 deletions(-)

diff --git a/tools/auto-tester/src/Test.cpp b/tools/auto-tester/src/Test.cpp
index bdf734897e..1ab934cf63 100644
--- a/tools/auto-tester/src/Test.cpp
+++ b/tools/auto-tester/src/Test.cpp
@@ -176,33 +176,38 @@ void Test::appendTestResultsToFile(QString testResultsFolderPath, TestFailure te
 
 void Test::startTestsEvaluation() {
     // Get list of JPEG images in folder, sorted by name
-    pathToTestResultsDirectory = QFileDialog::getExistingDirectory(nullptr, "Please select folder containing the test images", ".", QFileDialog::ShowDirsOnly);
-    if (pathToTestResultsDirectory == "") {
+    QString previousSelection = snapshotDirectory;
+
+    snapshotDirectory = QFileDialog::getExistingDirectory(nullptr, "Please select folder containing the test images",
+                                                          previousSelection, QFileDialog::ShowDirsOnly);
+
+    // If user cancelled then restore previous selection and return
+    if (snapshotDirectory == "") {
+        snapshotDirectory = previousSelection;
         return;
     }
 
     // Quit if test results folder could not be created
-    if (!createTestResultsFolderPath(pathToTestResultsDirectory)) {
+    if (!createTestResultsFolderPath(snapshotDirectory)) {
         return;
     }
 
     // Before any processing - all images are converted to PNGs, as this is the format stored on GitHub
-    QStringList sortedSnapshotFilenames = createListOfAll_imagesInDirectory("jpg", pathToTestResultsDirectory);
+    QStringList sortedSnapshotFilenames = createListOfAll_imagesInDirectory("jpg", snapshotDirectory);
     foreach(QString filename, sortedSnapshotFilenames) {
         QStringList stringParts = filename.split(".");
-        copyJPGtoPNG(
-            pathToTestResultsDirectory + "/" + stringParts[0] + ".jpg", 
-            pathToTestResultsDirectory + "/" + stringParts[0] + ".png"
+        copyJPGtoPNG(snapshotDirectory + "/" + stringParts[0] + ".jpg", 
+            snapshotDirectory + "/" + stringParts[0] + ".png"
         );
 
-        QFile::remove(pathToTestResultsDirectory + "/" + stringParts[0] + ".jpg");
+        QFile::remove(snapshotDirectory + "/" + stringParts[0] + ".jpg");
     }
 
     // Create two lists.  The first is the test results,  the second is the expected images
     // The expected images are represented as a URL to enable download from GitHub
     // Images that are in the wrong format are ignored.
 
-    QStringList sortedTestResultsFilenames = createListOfAll_imagesInDirectory("png", pathToTestResultsDirectory);
+    QStringList sortedTestResultsFilenames = createListOfAll_imagesInDirectory("png", snapshotDirectory);
     QStringList expectedImagesURLs;
 
     resultImagesFullFilenames.clear();
@@ -210,7 +215,7 @@ void Test::startTestsEvaluation() {
     expectedImagesFullFilenames.clear();
 
     foreach(QString currentFilename, sortedTestResultsFilenames) {
-        QString fullCurrentFilename = pathToTestResultsDirectory + "/" + currentFilename;
+        QString fullCurrentFilename = snapshotDirectory + "/" + currentFilename;
         if (isInSnapshotFilenameFormat("png", currentFilename)) {
             resultImagesFullFilenames << fullCurrentFilename;
 
@@ -230,11 +235,11 @@ void Test::startTestsEvaluation() {
             QString expectedImageFilename = currentFilename.replace("/", "_").replace(".", "_EI.");
 
             expectedImagesFilenames << expectedImageFilename;
-            expectedImagesFullFilenames << pathToTestResultsDirectory + "/" + expectedImageFilename;
+            expectedImagesFullFilenames << snapshotDirectory + "/" + expectedImageFilename;
         }
     }
 
-    autoTester->downloadImages(expectedImagesURLs, pathToTestResultsDirectory, expectedImagesFilenames);
+    autoTester->downloadImages(expectedImagesURLs, snapshotDirectory, expectedImagesFilenames);
 }
 
 void Test::finishTestsEvaluation(bool interactiveMode, QProgressBar* progressBar) {
@@ -295,25 +300,39 @@ void Test::importTest(QTextStream& textStream, const QString& testPathname) {
 // This script will run all text.js scripts in every applicable sub-folder
 void Test::createRecursiveScript() {
     // Select folder to start recursing from
-    QString topLevelDirectory = QFileDialog::getExistingDirectory(nullptr, "Please select folder that will contain the top level test script", ".", QFileDialog::ShowDirsOnly);
-    if (topLevelDirectory == "") {
+    QString previousSelection = testDirectory;
+
+    testDirectory =
+        QFileDialog::getExistingDirectory(nullptr, "Please select folder that will contain the top level test script",
+                                          previousSelection, QFileDialog::ShowDirsOnly);
+
+    // If user cancelled then restore previous selection and return
+    if (testDirectory == "") {
+        testDirectory = previousSelection;
         return;
     }
 
-    createRecursiveScript(topLevelDirectory, true);
+    createRecursiveScript(testDirectory, true);
 }
 
 // This method creates a `testRecursive.js` script in every sub-folder.
 void Test::createAllRecursiveScripts() {
     // Select folder to start recursing from
-    QString topLevelDirectory = QFileDialog::getExistingDirectory(nullptr, "Please select the root folder for the recursive scripts", ".", QFileDialog::ShowDirsOnly);
-    if (topLevelDirectory == "") {
+    QString previousSelection = testDirectory;
+
+    testDirectory = QFileDialog::getExistingDirectory(nullptr, "Please select the root folder for the recursive scripts",
+                                                      previousSelection,
+                                                      QFileDialog::ShowDirsOnly);
+
+    // If user cancelled then restore previous selection and return
+    if (testDirectory == "") {
+        testDirectory = previousSelection;
         return;
     }
 
-    createRecursiveScript(topLevelDirectory, false);
+    createRecursiveScript(testDirectory, false);
 
-    QDirIterator it(topLevelDirectory.toStdString().c_str(), QDirIterator::Subdirectories);
+    QDirIterator it(testDirectory.toStdString().c_str(), QDirIterator::Subdirectories);
     while (it.hasNext()) {
         QString directory = it.next();
 
@@ -416,29 +435,42 @@ void Test::createRecursiveScript(QString topLevelDirectory, bool interactiveMode
 void Test::createTest() {
     // Rename files sequentially, as ExpectedResult_00000.jpeg, ExpectedResult_00001.jpg and so on
     // Any existing expected result images will be deleted
-    QString imageSourceDirectory = QFileDialog::getExistingDirectory(nullptr, "Please select folder containing the test images", ".", QFileDialog::ShowDirsOnly);
-    if (imageSourceDirectory == "") {
+    QString previousSelection = snapshotDirectory;
+
+    snapshotDirectory = QFileDialog::getExistingDirectory(nullptr, "Please select folder containing the test images",
+                                                          previousSelection,
+                                                          QFileDialog::ShowDirsOnly);
+
+    // If user cancelled then restore previous selection and return
+    if (snapshotDirectory == "") {
+        snapshotDirectory = previousSelection;
         return;
     }
 
-    QString imageDestinationDirectory = QFileDialog::getExistingDirectory(nullptr, "Please select folder to save the test images", ".", QFileDialog::ShowDirsOnly);
-    if (imageDestinationDirectory == "") {
+    previousSelection = testDirectory;
+
+    QString testDirectory = QFileDialog::getExistingDirectory(nullptr, "Please select folder to save the test images",
+                                                              previousSelection, QFileDialog::ShowDirsOnly);
+
+    // If user cancelled then restore previous selection and return
+    if (testDirectory == "") {
+        testDirectory = previousSelection;
         return;
     }
 
-    QStringList sortedImageFilenames = createListOfAll_imagesInDirectory("jpg", imageSourceDirectory);
+    QStringList sortedImageFilenames = createListOfAll_imagesInDirectory("jpg", snapshotDirectory);
 
     int i = 1; 
     const int maxImages = pow(10, NUM_DIGITS);
     foreach (QString currentFilename, sortedImageFilenames) {
-        QString fullCurrentFilename = imageSourceDirectory + "/" + currentFilename;
+        QString fullCurrentFilename = snapshotDirectory + "/" + currentFilename;
         if (isInSnapshotFilenameFormat("jpg", currentFilename)) {
             if (i >= maxImages) {
                 messageBox.critical(0, "Error", "More than " + QString::number(maxImages) + " images not supported");
                 exit(-1);
             }
             QString newFilename = "ExpectedImage_" + QString::number(i - 1).rightJustified(5, '0') + ".png";
-            QString fullNewFileName = imageDestinationDirectory + "/" + newFilename;
+            QString fullNewFileName = testDirectory + "/" + newFilename;
 
             try {
                 copyJPGtoPNG(fullCurrentFilename, fullNewFileName);
@@ -486,7 +518,7 @@ ExtractedText Test::getTestScriptLines(QString testFileName) {
     const QRegularExpression lineStepSnapshot = QRegularExpression(regexStepSnapshot);
 
     const QString functionAddStepName(ws + "autoTester" + ws + "\\." + ws + "addStep");
-    const QString regexStep(ws + functionAddStepName + ws + "\\(" + ws + quotedString + ws + "\\)" + ".*");
+    const QString regexStep(ws + functionAddStepName + ws + "\\(" + ws + quotedString + ".*");
     const QRegularExpression lineStep = QRegularExpression(regexStep);
 
     while (!line.isNull()) {
@@ -522,29 +554,43 @@ ExtractedText Test::getTestScriptLines(QString testFileName) {
 // The folder selected must contain a script named "test.js", the file produced is named "test.md"
 void Test::createMDFile() {
     // Folder selection
-    QString testDirectory = QFileDialog::getExistingDirectory(nullptr, "Please select folder containing the test", ".", QFileDialog::ShowDirsOnly);
+    QString previousSelection = testDirectory;
+
+    testDirectory = QFileDialog::getExistingDirectory(nullptr, "Please select folder containing the test", previousSelection,
+                                                      QFileDialog::ShowDirsOnly);
+
+    // If user cancelled then restore previous selection and return
     if (testDirectory == "") {
+        testDirectory = previousSelection;
         return;
     }
 
     createMDFile(testDirectory);
+
+    messageBox.information(0, "Success", "MD file has been created");
 }
 
 void Test::createAllMDFiles() {
     // Select folder to start recursing from
-    QString topLevelDirectory = QFileDialog::getExistingDirectory(nullptr, "Please select the root folder for the MD files", ".", QFileDialog::ShowDirsOnly);
-    if (topLevelDirectory == "") {
+    QString previousSelection = testDirectory;
+
+    testDirectory = QFileDialog::getExistingDirectory(nullptr, "Please select the root folder for the MD files",
+                                                      previousSelection, QFileDialog::ShowDirsOnly);
+
+    // If user cancelled then restore previous selection and return
+    if (testDirectory == "") {
+        testDirectory = previousSelection;
         return;
     }
 
     // First test if top-level folder has a test.js file
-    const QString testPathname{ topLevelDirectory + "/" + TEST_FILENAME };
+    const QString testPathname { testDirectory + "/" + TEST_FILENAME };
     QFileInfo fileInfo(testPathname);
     if (fileInfo.exists()) {
-        createMDFile(topLevelDirectory);
+        createMDFile(testDirectory);
     }
 
-    QDirIterator it(topLevelDirectory.toStdString().c_str(), QDirIterator::Subdirectories);
+    QDirIterator it(testDirectory.toStdString().c_str(), QDirIterator::Subdirectories);
     while (it.hasNext()) {
         QString directory = it.next();
 
@@ -613,13 +659,19 @@ void Test::createMDFile(QString testDirectory) {
 }
 
 void Test::createTestsOutline() {
-    QString testsRootDirectory = QFileDialog::getExistingDirectory(nullptr, "Please select the tests root folder", ".", QFileDialog::ShowDirsOnly);
-    if (testsRootDirectory == "") {
+    QString previousSelection = testDirectory;
+
+    testDirectory = QFileDialog::getExistingDirectory(nullptr, "Please select the tests root folder", previousSelection,
+                                                      QFileDialog::ShowDirsOnly);
+
+    // If user cancelled then restore previous selection and return
+    if (testDirectory == "") {
+        testDirectory = previousSelection;
         return;
     }
 
     const QString testsOutlineFilename { "testsOutline.md" };
-    QString mdFilename(testsRootDirectory + "/" + testsOutlineFilename);
+    QString mdFilename(testDirectory + "/" + testsOutlineFilename);
     QFile mdFile(mdFilename);
     if (!mdFile.open(QIODevice::WriteOnly)) {
         messageBox.critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "Failed to create file " + mdFilename);
@@ -633,10 +685,10 @@ void Test::createTestsOutline() {
     stream << "Directories with an appended (*) have an automatic test\n\n";
 
     // We need to know our current depth, as this isn't given by QDirIterator
-    int rootDepth { testsRootDirectory.count('/') };
+    int rootDepth { testDirectory.count('/') };
 
     // Each test is shown as the folder name linking to the matching GitHub URL, and the path to the associated test.md file
-    QDirIterator it(testsRootDirectory.toStdString().c_str(), QDirIterator::Subdirectories);
+    QDirIterator it(testDirectory.toStdString().c_str(), QDirIterator::Subdirectories);
     while (it.hasNext()) {
         QString directory = it.next();
 
diff --git a/tools/auto-tester/src/Test.h b/tools/auto-tester/src/Test.h
index 975d5dfdaf..896fe24fcc 100644
--- a/tools/auto-tester/src/Test.h
+++ b/tools/auto-tester/src/Test.h
@@ -91,7 +91,12 @@ private:
     const int NUM_DIGITS { 5 };
     const QString EXPECTED_IMAGE_PREFIX { "ExpectedImage_" };
 
-    QString pathToTestResultsDirectory;
+    // We have to directories to work with.
+    // The first is the directory containing the test we are working with
+    // The second contains the snapshots taken for test runs that need to be evaluated
+    QString testDirectory;
+    QString snapshotDirectory;
+
     QStringList expectedImagesFilenames;
     QStringList expectedImagesFullFilenames;
     QStringList resultImagesFullFilenames;

From f454dac709d5b709545a7e484f7c3eed30333594 Mon Sep 17 00:00:00 2001
From: Andrew Meadows <andrew@highfidelity.io>
Date: Wed, 9 May 2018 12:04:44 -0700
Subject: [PATCH 11/25] avoid div by zero when measuring acceleration

---
 libraries/physics/src/EntityMotionState.cpp | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/libraries/physics/src/EntityMotionState.cpp b/libraries/physics/src/EntityMotionState.cpp
index a801392b66..fbb4b69ce0 100644
--- a/libraries/physics/src/EntityMotionState.cpp
+++ b/libraries/physics/src/EntityMotionState.cpp
@@ -731,7 +731,9 @@ void EntityMotionState::measureBodyAcceleration() {
         // hence the equation for acceleration is: a = (v1 / (1 - D)^dt - v0) / dt
         glm::vec3 velocity = getBodyLinearVelocityGTSigma();
 
-        _measuredAcceleration = (velocity / powf(1.0f - _body->getLinearDamping(), dt) - _lastVelocity) * invDt;
+        const float MIN_DAMPING_FACTOR = 0.01f;
+        float dampingAttenuationFactor = 1.0f / glm::max(powf(1.0f - _body->getLinearDamping(), dt), MIN_DAMPING_FACTOR);
+        _measuredAcceleration = (velocity * dampingAttenuationFactor - _lastVelocity) * invDt;
         _lastVelocity = velocity;
         if (numSubsteps > PHYSICS_ENGINE_MAX_NUM_SUBSTEPS) {
             // we fall in here when _lastMeasureStep is old: the body has just become active

From b8d34f4b8fee9f754f7312f6351bc347db31ba85 Mon Sep 17 00:00:00 2001
From: Andrew Meadows <andrew@highfidelity.io>
Date: Wed, 9 May 2018 12:13:27 -0700
Subject: [PATCH 12/25] more correct variable name

---
 libraries/physics/src/EntityMotionState.cpp | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/libraries/physics/src/EntityMotionState.cpp b/libraries/physics/src/EntityMotionState.cpp
index fbb4b69ce0..e04055ec32 100644
--- a/libraries/physics/src/EntityMotionState.cpp
+++ b/libraries/physics/src/EntityMotionState.cpp
@@ -732,8 +732,8 @@ void EntityMotionState::measureBodyAcceleration() {
         glm::vec3 velocity = getBodyLinearVelocityGTSigma();
 
         const float MIN_DAMPING_FACTOR = 0.01f;
-        float dampingAttenuationFactor = 1.0f / glm::max(powf(1.0f - _body->getLinearDamping(), dt), MIN_DAMPING_FACTOR);
-        _measuredAcceleration = (velocity * dampingAttenuationFactor - _lastVelocity) * invDt;
+        float invDampingAttenuationFactor = 1.0f / glm::max(powf(1.0f - _body->getLinearDamping(), dt), MIN_DAMPING_FACTOR);
+        _measuredAcceleration = (velocity * invDampingAttenuationFactor - _lastVelocity) * invDt;
         _lastVelocity = velocity;
         if (numSubsteps > PHYSICS_ENGINE_MAX_NUM_SUBSTEPS) {
             // we fall in here when _lastMeasureStep is old: the body has just become active

From fab85c3f6d433940d7c503d3a103317447300593 Mon Sep 17 00:00:00 2001
From: David Rowe <david@ctrlaltstudio.com>
Date: Fri, 11 May 2018 09:52:48 +1200
Subject: [PATCH 13/25] Fix JSDoc function definitions specifying properties
 instead of params

---
 .../entities/src/EntityScriptingInterface.h   |  4 +-
 .../src/AssetScriptingInterface.h             | 58 +++++++++----------
 2 files changed, 31 insertions(+), 31 deletions(-)

diff --git a/libraries/entities/src/EntityScriptingInterface.h b/libraries/entities/src/EntityScriptingInterface.h
index 8adb5138f2..7e47d9e2d4 100644
--- a/libraries/entities/src/EntityScriptingInterface.h
+++ b/libraries/entities/src/EntityScriptingInterface.h
@@ -481,8 +481,8 @@ public slots:
     /**jsdoc
      * Gets the status of server entity script attached to an entity
      * @function Entities.getServerScriptStatus
-     * @property {Uuid} entityID - The ID of the entity to get the server entity script status for.
-     * @property {Entities~getServerScriptStatusCallback} callback - The function to call upon completion.
+     * @param {Uuid} entityID - The ID of the entity to get the server entity script status for.
+     * @param {Entities~getServerScriptStatusCallback} callback - The function to call upon completion.
      * @returns {boolean} <code>true</code> always.
      */
     /**jsdoc
diff --git a/libraries/script-engine/src/AssetScriptingInterface.h b/libraries/script-engine/src/AssetScriptingInterface.h
index eb9a628ae3..7f7a3a68b0 100644
--- a/libraries/script-engine/src/AssetScriptingInterface.h
+++ b/libraries/script-engine/src/AssetScriptingInterface.h
@@ -186,36 +186,36 @@ public:
 
     /**jsdoc
      * @function Assets.deleteAsset
-     * @property {} options
-     * @property {} scope
-     * @property {} [callback = ""]
+     * @param {} options
+     * @param {} scope
+     * @param {} [callback = ""]
      */
 
     Q_INVOKABLE void deleteAsset(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue());
     
     /**jsdoc
      * @function Assets.resolveAsset
-     * @property {} options
-     * @property {} scope
-     * @property {} [callback = ""]
+     * @param {} options
+     * @param {} scope
+     * @param {} [callback = ""]
      */
 
     Q_INVOKABLE void resolveAsset(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue());
     
     /**jsdoc
      * @function Assets.decompressData
-     * @property {} options
-     * @property {} scope
-     * @property {} [callback = ""]
+     * @param {} options
+     * @param {} scope
+     * @param {} [callback = ""]
      */
 
     Q_INVOKABLE void decompressData(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue());
     
     /**jsdoc
      * @function Assets.compressData
-     * @property {} options
-     * @property {} scope
-     * @property {} [callback = ""]
+     * @param {} options
+     * @param {} scope
+     * @param {} [callback = ""]
      */
 
     Q_INVOKABLE void compressData(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue());
@@ -229,7 +229,7 @@ public:
     
     /**jsdoc
      * @function Assets.canWriteCacheValue
-     * @property {string} url
+     * @param {string} url
      * @returns {boolean}
      */
 
@@ -237,8 +237,8 @@ public:
     
     /**jsdoc
      * @function Assets.getCacheStatus
-     * @property {} scope
-     * @property {} [callback=undefined]
+     * @param {} scope
+     * @param {} [callback=undefined]
      */
 
     Q_INVOKABLE void getCacheStatus(QScriptValue scope, QScriptValue callback = QScriptValue()) {
@@ -247,38 +247,38 @@ public:
 
     /**jsdoc
      * @function Assets.queryCacheMeta
-     * @property {} options
-     * @property {} scope
-     * @property {} [callback=undefined]
+     * @param {} options
+     * @param {} scope
+     * @param {} [callback=undefined]
      */
 
     Q_INVOKABLE void queryCacheMeta(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue());
     
     /**jsdoc
      * @function Assets.loadFromCache
-     * @property {} options
-     * @property {} scope
-     * @property {} [callback=undefined]
+     * @param {} options
+     * @param {} scope
+     * @param {} [callback=undefined]
      */
 
     Q_INVOKABLE void loadFromCache(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue());
     
     /**jsdoc
      * @function Assets.saveToCache
-     * @property {} options
-     * @property {} scope
-     * @property {} [callback=undefined]
+     * @param {} options
+     * @param {} scope
+     * @param {} [callback=undefined]
      */
 
     Q_INVOKABLE void saveToCache(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue());
     
     /**jsdoc
      * @function Assets.saveToCache
-     * @property {} url
-     * @property {} data
-     * @property {} metadata
-     * @property {} scope
-     * @property {} [callback=undefined]
+     * @param {} url
+     * @param {} data
+     * @param {} metadata
+     * @param {} scope
+     * @param {} [callback=undefined]
      */
 
     Q_INVOKABLE void saveToCache(const QUrl& url, const QByteArray& data, const QVariantMap& metadata,

From 50a53a5174bd3dc2a29131d0e316266c060a5d66 Mon Sep 17 00:00:00 2001
From: howard-stearns <howard@highfidelity.io>
Date: Thu, 10 May 2018 16:00:01 -0700
Subject: [PATCH 14/25] fix lastEdited vs pal

---
 scripts/system/pal.js | 10 +++++++---
 1 file changed, 7 insertions(+), 3 deletions(-)

diff --git a/scripts/system/pal.js b/scripts/system/pal.js
index 0a01007ee9..84328001e1 100644
--- a/scripts/system/pal.js
+++ b/scripts/system/pal.js
@@ -251,6 +251,7 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See
             });
         }
         break;
+    case 'refresh': // old name for refreshNearby
     case 'refreshNearby':
         data = {};
         ExtendedOverlay.some(function (overlay) { // capture the audio data
@@ -743,10 +744,13 @@ function receiveMessage(channel, messageString, senderID) {
     var message = JSON.parse(messageString);
     switch (message.method) {
     case 'select':
-        sendToQml(message); // Accepts objects, not just strings.
+	if (!onPalScreen) {
+	    tablet.loadQMLSource(PAL_QML_SOURCE);
+	    Script.setTimeout(function () { sendToQml(message); }, 1000);
+	} else {
+            sendToQml(message); // Accepts objects, not just strings.
+	}
         break;
-    default:
-        print('Unrecognized PAL message', messageString);
     }
 }
 

From bf26aec260fe661b2391d1a92157c80c5c29b6cc Mon Sep 17 00:00:00 2001
From: howard-stearns <howard@highfidelity.io>
Date: Thu, 10 May 2018 16:39:56 -0700
Subject: [PATCH 15/25] untabify

---
 scripts/system/pal.js | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/scripts/system/pal.js b/scripts/system/pal.js
index 84328001e1..c70c2729f5 100644
--- a/scripts/system/pal.js
+++ b/scripts/system/pal.js
@@ -744,12 +744,12 @@ function receiveMessage(channel, messageString, senderID) {
     var message = JSON.parse(messageString);
     switch (message.method) {
     case 'select':
-	if (!onPalScreen) {
-	    tablet.loadQMLSource(PAL_QML_SOURCE);
-	    Script.setTimeout(function () { sendToQml(message); }, 1000);
-	} else {
+        if (!onPalScreen) {
+            tablet.loadQMLSource(PAL_QML_SOURCE);
+            Script.setTimeout(function () { sendToQml(message); }, 1000);
+        } else {
             sendToQml(message); // Accepts objects, not just strings.
-	}
+        }
         break;
     }
 }

From 1364f4391d1ce0400a3ab1e16d1754f99f7793d4 Mon Sep 17 00:00:00 2001
From: NissimHadar <nissim.hadar@gmail.com>
Date: Thu, 10 May 2018 18:02:44 -0700
Subject: [PATCH 16/25] Can store JSON object.

---
 interface/src/Application.cpp                 | 29 ++++++++++++-------
 interface/src/Application.h                   |  3 +-
 .../src/scripting/TestScriptingInterface.cpp  | 27 +++++++++++++++++
 .../src/scripting/TestScriptingInterface.h    |  5 ++++
 4 files changed, 52 insertions(+), 12 deletions(-)

diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp
index 20efe73f08..89cbf0f6c0 100644
--- a/interface/src/Application.cpp
+++ b/interface/src/Application.cpp
@@ -734,9 +734,9 @@ extern InputPluginList getInputPlugins();
 extern void saveInputPluginSettings(const InputPluginList& plugins);
 
 // Parameters used for running tests from teh command line
-const QString TEST_SCRIPT_COMMAND { "--testScript" };
-const QString TEST_QUIT_WHEN_FINISHED_OPTION { "quitWhenFinished" };
-const QString TEST_SNAPSHOT_LOCATION_COMMAND { "--testSnapshotLocation" };
+const QString TEST_SCRIPT_COMMAND{ "--testScript" };
+const QString TEST_QUIT_WHEN_FINISHED_OPTION{ "quitWhenFinished" };
+const QString TEST_RESULTS_LOCATION_COMMAND{ "--testResultsLocation" };
 
 bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) {
     const char** constArgv = const_cast<const char**>(argv);
@@ -1014,22 +1014,25 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
 
                 // If the URL scheme is http(s) or ftp, then use as is, else - treat it as a local file
                 // This is done so as not break previous command line scripts
-                if (testScriptPath.left(URL_SCHEME_HTTP.length()) == URL_SCHEME_HTTP || testScriptPath.left(URL_SCHEME_FTP.length()) == URL_SCHEME_FTP) {
+                if (testScriptPath.left(URL_SCHEME_HTTP.length()) == URL_SCHEME_HTTP ||
+                    testScriptPath.left(URL_SCHEME_FTP.length()) == URL_SCHEME_FTP) {
+                    
                     setProperty(hifi::properties::TEST, QUrl::fromUserInput(testScriptPath));
                 } else if (QFileInfo(testScriptPath).exists()) {
                     setProperty(hifi::properties::TEST, QUrl::fromLocalFile(testScriptPath));
                 }
 
-				// quite when finished parameter must directly follow the test script
+                // quite when finished parameter must directly follow the test script
                 if ((i + 2) < args.size() && args.at(i + 2) == TEST_QUIT_WHEN_FINISHED_OPTION) {
                     quitWhenFinished = true;
                 }
-            } else if (args.at(i) == TEST_SNAPSHOT_LOCATION_COMMAND) {
+            } else if (args.at(i) == TEST_RESULTS_LOCATION_COMMAND) {
                 // Set test snapshot location only if it is a writeable directory
-                QString pathname(args.at(i + 1));
-                QFileInfo fileInfo(pathname);
+                QString path(args.at(i + 1));
+
+                QFileInfo fileInfo(path);
                 if (fileInfo.isDir() && fileInfo.isWritable()) {
-                    testSnapshotLocation = pathname;
+                    testResultsLocation = path;
                 }
             }
         }
@@ -7475,7 +7478,9 @@ void Application::loadAvatarBrowser() const {
 void Application::takeSnapshot(bool notify, bool includeAnimated, float aspectRatio, const QString& filename) {
     postLambdaEvent([notify, includeAnimated, aspectRatio, filename, this] {
         // Get a screenshot and save it
-        QString path = Snapshot::saveSnapshot(getActiveDisplayPlugin()->getScreenshot(aspectRatio), filename, testSnapshotLocation);
+        QString path =
+            Snapshot::saveSnapshot(getActiveDisplayPlugin()->getScreenshot(aspectRatio), filename, testResultsLocation);
+
         // If we're not doing an animated snapshot as well...
         if (!includeAnimated) {
             // Tell the dependency manager that the capture of the still snapshot has taken place.
@@ -7489,7 +7494,9 @@ void Application::takeSnapshot(bool notify, bool includeAnimated, float aspectRa
 
 void Application::takeSecondaryCameraSnapshot(const QString& filename) {
     postLambdaEvent([filename, this] {
-        QString snapshotPath = Snapshot::saveSnapshot(getActiveDisplayPlugin()->getSecondaryCameraScreenshot(), filename, testSnapshotLocation);
+        QString snapshotPath =
+            Snapshot::saveSnapshot(getActiveDisplayPlugin()->getSecondaryCameraScreenshot(), filename, testResultsLocation);
+
         emit DependencyManager::get<WindowScriptingInterface>()->stillSnapshotTaken(snapshotPath, true);
     });
 }
diff --git a/interface/src/Application.h b/interface/src/Application.h
index aa6469c592..3189b87775 100644
--- a/interface/src/Application.h
+++ b/interface/src/Application.h
@@ -418,6 +418,7 @@ public slots:
     void updateVerboseLogging();
     Q_INVOKABLE void openAndroidActivity(const QString& activityName);
 
+    QString getTestResultsLocation() { return testResultsLocation; };
 
 private slots:
     void onDesktopRootItemCreated(QQuickItem* qmlContext);
@@ -751,7 +752,7 @@ private:
     std::atomic<bool> _pendingIdleEvent { true };
     std::atomic<bool> _pendingRenderEvent { true };
 
-    QString testSnapshotLocation;
+    QString testResultsLocation;
     bool quitWhenFinished { false };
 };
 #endif // hifi_Application_h
diff --git a/interface/src/scripting/TestScriptingInterface.cpp b/interface/src/scripting/TestScriptingInterface.cpp
index 9e7c0e142e..15a024fa03 100644
--- a/interface/src/scripting/TestScriptingInterface.cpp
+++ b/interface/src/scripting/TestScriptingInterface.cpp
@@ -160,3 +160,30 @@ void TestScriptingInterface::clearCaches() {
 	qApp->reloadResourceCaches();
 }
 
+// Writes a JSON object from javascript to a file
+void TestScriptingInterface::saveObject(QVariant variant, const QString& filename) {
+    QString testResultsLocation = qApp->getTestResultsLocation();
+    if (testResultsLocation.isNull()) {
+        return;
+    }
+
+    QJsonDocument jsonDocument;
+    jsonDocument = QJsonDocument::fromVariant(variant);
+    if (jsonDocument.isNull()) {
+        return;
+    }
+
+    QByteArray jsonData = jsonDocument.toJson();
+
+    // Append trailing slash if needed
+    if (testResultsLocation.right(1) != "/") {
+        testResultsLocation += "/";
+    }
+
+    QString filepath = QDir::cleanPath(testResultsLocation + filename);
+    QFile file(filepath);
+
+    file.open(QFile::WriteOnly);
+    file.write(jsonData);
+    file.close();
+}
diff --git a/interface/src/scripting/TestScriptingInterface.h b/interface/src/scripting/TestScriptingInterface.h
index 687cb41689..4b469244d1 100644
--- a/interface/src/scripting/TestScriptingInterface.h
+++ b/interface/src/scripting/TestScriptingInterface.h
@@ -83,6 +83,11 @@ public slots:
     */
     void clearCaches();
 
+    /**jsdoc
+    * Save a JSON object to a file in the test results location
+    */
+    void saveObject(QVariant v, const QString& filename);
+
 private:
     bool waitForCondition(qint64 maxWaitMs, std::function<bool()> condition);
 };

From 898dec29cbb48dd59a0303aa52d04ba37b2e5473 Mon Sep 17 00:00:00 2001
From: Seth Alves <seth.alves@gmail.com>
Date: Fri, 11 May 2018 17:04:40 -0700
Subject: [PATCH 17/25] don't cache value of getControllerJointIndex at
 startup, because the script may run before the data is available.  Instead,
 refresh the data every few seconds

---
 .../controllerModules/equipEntity.js          | 12 ++---
 .../controllerModules/nearActionGrabEntity.js |  2 +-
 .../nearGrabHyperLinkEntity.js                |  8 +---
 .../controllerModules/nearParentGrabEntity.js | 13 ++----
 .../nearParentGrabOverlay.js                  |  3 +-
 .../controllers/controllerModules/teleport.js |  2 +-
 .../libraries/controllerDispatcherUtils.js    | 45 ++++++++++++-------
 7 files changed, 44 insertions(+), 41 deletions(-)

diff --git a/scripts/system/controllers/controllerModules/equipEntity.js b/scripts/system/controllers/controllerModules/equipEntity.js
index 1fce772ec8..093a8b57d8 100644
--- a/scripts/system/controllers/controllerModules/equipEntity.js
+++ b/scripts/system/controllers/controllerModules/equipEntity.js
@@ -6,11 +6,11 @@
 //  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
 
 
-/* global Script, Entities, MyAvatar, Controller, RIGHT_HAND, LEFT_HAND,
-   getControllerJointIndex, enableDispatcherModule, disableDispatcherModule,
+/* global Script, Entities, MyAvatar, Controller, RIGHT_HAND, LEFT_HAND, Camera,
+   getControllerJointIndex, enableDispatcherModule, disableDispatcherModule, entityIsFarGrabbedByOther,
    Messages, makeDispatcherModuleParameters, makeRunningValues, Settings, entityHasActions,
    Vec3, Overlays, flatten, Xform, getControllerWorldLocation, ensureDynamic, entityIsCloneable,
-   cloneEntity, DISPATCHER_PROPERTIES, TEAR_AWAY_DISTANCE, Uuid, unhighlightTargetEntity
+   cloneEntity, DISPATCHER_PROPERTIES, Uuid, unhighlightTargetEntity, isInEditMode
 */
 
 Script.include("/~/system/libraries/Xform.js");
@@ -781,7 +781,7 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa
             }
         }
     };
-    
+
     var clearGrabActions = function(entityID) {
         var actionIDs = Entities.getActionIDs(entityID);
         var myGrabTag = "grab-" + MyAvatar.sessionUUID;
@@ -794,7 +794,7 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa
             }
         }
     };
-    
+
     var onMousePress = function(event) {
         if (isInEditMode() || !event.isLeftButton) { // don't consider any left clicks on the entity while in edit
             return;
@@ -808,7 +808,7 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa
             if (hasEquipData && entityProperties.parentID === EMPTY_PARENT_ID && !entityIsFarGrabbedByOther(entityID)) {
                 entityProperties.id = entityID;
                 var rightHandPosition = MyAvatar.getJointPosition("RightHand");
-                var leftHandPosition = MyAvatar.getJointPosition("LeftHand");   
+                var leftHandPosition = MyAvatar.getJointPosition("LeftHand");
                 var distanceToRightHand = Vec3.distance(entityProperties.position, rightHandPosition);
                 var distanceToLeftHand = Vec3.distance(entityProperties.position, leftHandPosition);
                 var leftHandAvailable = leftEquipEntity.targetEntityID === null;
diff --git a/scripts/system/controllers/controllerModules/nearActionGrabEntity.js b/scripts/system/controllers/controllerModules/nearActionGrabEntity.js
index a4e439fe2f..274a4264cd 100644
--- a/scripts/system/controllers/controllerModules/nearActionGrabEntity.js
+++ b/scripts/system/controllers/controllerModules/nearActionGrabEntity.js
@@ -10,7 +10,7 @@
    propsArePhysical, Messages, HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, entityIsGrabbable,
    Quat, Vec3, MSECS_PER_SEC, getControllerWorldLocation, makeDispatcherModuleParameters, makeRunningValues,
    TRIGGER_OFF_VALUE, NEAR_GRAB_RADIUS, findGroupParent, entityIsCloneable, propsAreCloneDynamic, cloneEntity,
-   HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, BUMPER_ON_VALUE, unhighlightTargetEntity
+   HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, BUMPER_ON_VALUE, unhighlightTargetEntity, Uuid
 */
 
 Script.include("/~/system/libraries/controllerDispatcherUtils.js");
diff --git a/scripts/system/controllers/controllerModules/nearGrabHyperLinkEntity.js b/scripts/system/controllers/controllerModules/nearGrabHyperLinkEntity.js
index 59ce79cfd1..962ae89bb9 100644
--- a/scripts/system/controllers/controllerModules/nearGrabHyperLinkEntity.js
+++ b/scripts/system/controllers/controllerModules/nearGrabHyperLinkEntity.js
@@ -7,12 +7,8 @@
 //  Distributed under the Apache License, Version 2.0.
 //  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
 
-/* global Script, Entities, MyAvatar, Controller, RIGHT_HAND, LEFT_HAND,
-   getControllerJointIndex, getGrabbableData, enableDispatcherModule, disableDispatcherModule,
-   propsArePhysical, Messages, HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, entityIsGrabbable,
-   Quat, Vec3, MSECS_PER_SEC, getControllerWorldLocation, makeDispatcherModuleParameters, makeRunningValues,
-   TRIGGER_OFF_VALUE, NEAR_GRAB_RADIUS, findGroupParent, entityIsCloneable, propsAreCloneDynamic, cloneEntity,
-   HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, BUMPER_ON_VALUE, AddressManager
+/* global Script, MyAvatar, RIGHT_HAND, LEFT_HAND, enableDispatcherModule, disableDispatcherModule,
+   makeDispatcherModuleParameters, makeRunningValues, TRIGGER_OFF_VALUE, NEAR_GRAB_RADIUS, BUMPER_ON_VALUE, AddressManager
 */
 
 (function() {
diff --git a/scripts/system/controllers/controllerModules/nearParentGrabEntity.js b/scripts/system/controllers/controllerModules/nearParentGrabEntity.js
index d454d20a02..cf3a9cf14b 100644
--- a/scripts/system/controllers/controllerModules/nearParentGrabEntity.js
+++ b/scripts/system/controllers/controllerModules/nearParentGrabEntity.js
@@ -11,8 +11,7 @@
    TRIGGER_OFF_VALUE, makeDispatcherModuleParameters, entityIsGrabbable, makeRunningValues, NEAR_GRAB_RADIUS,
    findGroupParent, Vec3, cloneEntity, entityIsCloneable, propsAreCloneDynamic, HAPTIC_PULSE_STRENGTH,
    HAPTIC_PULSE_DURATION, BUMPER_ON_VALUE, findHandChildEntities, TEAR_AWAY_DISTANCE, MSECS_PER_SEC, TEAR_AWAY_CHECK_TIME,
-   TEAR_AWAY_COUNT, distanceBetweenPointAndEntityBoundingBox, print, Selection, DISPATCHER_HOVERING_LIST, Uuid,
-   highlightTargetEntity, unhighlightTargetEntity
+   TEAR_AWAY_COUNT, distanceBetweenPointAndEntityBoundingBox, print, Uuid, highlightTargetEntity, unhighlightTargetEntity
 */
 
 Script.include("/~/system/libraries/controllerDispatcherUtils.js");
@@ -43,11 +42,6 @@ Script.include("/~/system/libraries/cloneEntityUtils.js");
             [],
             100);
 
-
-        // XXX does handJointIndex change if the avatar changes?
-        this.handJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? "RightHand" : "LeftHand");
-        this.controllerJointIndex = getControllerJointIndex(this.hand);
-
         this.thisHandIsParent = function(props) {
             if (!props) {
                 return false;
@@ -62,8 +56,7 @@ Script.include("/~/system/libraries/cloneEntityUtils.js");
                 return true;
             }
 
-            var controllerJointIndex = this.controllerJointIndex;
-            if (props.parentJointIndex === controllerJointIndex) {
+            if (props.parentJointIndex === getControllerJointIndex(this.hand)) {
                 return true;
             }
 
@@ -102,7 +95,7 @@ Script.include("/~/system/libraries/cloneEntityUtils.js");
             // } else {
             //     handJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? "RightHand" : "LeftHand");
             // }
-            handJointIndex = this.controllerJointIndex;
+            handJointIndex = getControllerJointIndex(this.hand);
 
             var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID];
             Entities.callEntityMethod(targetProps.id, "startNearGrab", args);
diff --git a/scripts/system/controllers/controllerModules/nearParentGrabOverlay.js b/scripts/system/controllers/controllerModules/nearParentGrabOverlay.js
index 0f876816b3..368d5c483b 100644
--- a/scripts/system/controllers/controllerModules/nearParentGrabOverlay.js
+++ b/scripts/system/controllers/controllerModules/nearParentGrabOverlay.js
@@ -9,7 +9,7 @@
 /* global Script, MyAvatar, Controller, RIGHT_HAND, LEFT_HAND, getControllerJointIndex,
    enableDispatcherModule, disableDispatcherModule, Messages, HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION,
    makeDispatcherModuleParameters, Overlays, makeRunningValues, Vec3, resizeTablet, getTabletWidthFromSettings,
-   NEAR_GRAB_RADIUS
+   NEAR_GRAB_RADIUS, HMD, Uuid
 */
 
 Script.include("/~/system/libraries/controllerDispatcherUtils.js");
@@ -37,7 +37,6 @@ Script.include("/~/system/libraries/utils.js");
 
         // XXX does handJointIndex change if the avatar changes?
         this.handJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? "RightHand" : "LeftHand");
-        this.controllerJointIndex = getControllerJointIndex(this.hand);
 
         this.getOtherModule = function() {
             return (this.hand === RIGHT_HAND) ? leftNearParentingGrabOverlay : rightNearParentingGrabOverlay;
diff --git a/scripts/system/controllers/controllerModules/teleport.js b/scripts/system/controllers/controllerModules/teleport.js
index 560da57b20..3bf99ca26a 100644
--- a/scripts/system/controllers/controllerModules/teleport.js
+++ b/scripts/system/controllers/controllerModules/teleport.js
@@ -10,7 +10,7 @@
 
 /* jslint bitwise: true */
 
-/* global Script, Entities, MyAvatar, Controller, RIGHT_HAND, LEFT_HAND, getControllerJointIndex,
+/* global Script, Entities, MyAvatar, Controller, RIGHT_HAND, LEFT_HAND,
    enableDispatcherModule, disableDispatcherModule, Messages, makeDispatcherModuleParameters, makeRunningValues, Vec3,
    HMD, Uuid, AvatarList, Picks, Pointers, PickType
 */
diff --git a/scripts/system/libraries/controllerDispatcherUtils.js b/scripts/system/libraries/controllerDispatcherUtils.js
index 71dc5e4273..04ae01bad6 100644
--- a/scripts/system/libraries/controllerDispatcherUtils.js
+++ b/scripts/system/libraries/controllerDispatcherUtils.js
@@ -7,7 +7,7 @@
 
 
 /* global module, Camera, HMD, MyAvatar, controllerDispatcherPlugins:true, Quat, Vec3, Overlays, Xform,
-   Selection,
+   Selection, Uuid,
    MSECS_PER_SEC:true , LEFT_HAND:true, RIGHT_HAND:true, FORBIDDEN_GRAB_TYPES:true,
    HAPTIC_PULSE_STRENGTH:true, HAPTIC_PULSE_DURATION:true, ZERO_VEC:true, ONE_VEC:true,
    DEFAULT_REGISTRATION_POINT:true, INCHES_TO_METERS:true,
@@ -34,11 +34,12 @@
    getGrabbableData:true,
    entityIsGrabbable:true,
    entityIsDistanceGrabbable:true,
+   getControllerJointIndexCacheTime:true,
+   getControllerJointIndexCache:true,
    getControllerJointIndex:true,
    propsArePhysical:true,
    controllerDispatcherPluginsNeedSort:true,
    projectOntoXYPlane:true,
-   getChildrenProps:true,
    projectOntoEntityXYPlane:true,
    projectOntoOverlayXYPlane:true,
    makeLaserLockInfo:true,
@@ -53,6 +54,8 @@
    TEAR_AWAY_COUNT:true,
    TEAR_AWAY_CHECK_TIME:true,
    distanceBetweenPointAndEntityBoundingBox:true,
+   entityIsEquipped:true,
+   entityIsFarGrabbedByOther:true,
    highlightTargetEntity:true,
    clearHighlightedEntities:true,
    unhighlightTargetEntity:true
@@ -265,20 +268,32 @@ entityIsDistanceGrabbable = function(props) {
     return true;
 };
 
-getControllerJointIndex = function (hand) {
-    if (HMD.isHandControllerAvailable()) {
-        var controllerJointIndex = -1;
-        if (Camera.mode === "first person") {
-            controllerJointIndex = MyAvatar.getJointIndex(hand === RIGHT_HAND ?
-                "_CONTROLLER_RIGHTHAND" :
-                "_CONTROLLER_LEFTHAND");
-        } else if (Camera.mode === "third person") {
-            controllerJointIndex = MyAvatar.getJointIndex(hand === RIGHT_HAND ?
-                "_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND" :
-                "_CAMERA_RELATIVE_CONTROLLER_LEFTHAND");
-        }
+getControllerJointIndexCacheTime = [0, 0];
+getControllerJointIndexCache = [-1, -1];
 
-        return controllerJointIndex;
+getControllerJointIndex = function (hand) {
+    var GET_CONTROLLERJOINTINDEX_CACHE_REFRESH_TIME = 3000; // msecs
+
+    var now = Date.now();
+    if (now - getControllerJointIndexCacheTime[hand] > GET_CONTROLLERJOINTINDEX_CACHE_REFRESH_TIME) {
+        if (HMD.isHandControllerAvailable()) {
+            var controllerJointIndex = -1;
+            if (Camera.mode === "first person") {
+                controllerJointIndex = MyAvatar.getJointIndex(hand === RIGHT_HAND ?
+                                                              "_CONTROLLER_RIGHTHAND" :
+                                                              "_CONTROLLER_LEFTHAND");
+            } else if (Camera.mode === "third person") {
+                controllerJointIndex = MyAvatar.getJointIndex(hand === RIGHT_HAND ?
+                                                              "_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND" :
+                                                              "_CAMERA_RELATIVE_CONTROLLER_LEFTHAND");
+            }
+
+            getControllerJointIndexCacheTime[hand] = now;
+            getControllerJointIndexCache[hand] = controllerJointIndex;
+            return controllerJointIndex;
+        }
+    } else {
+        return getControllerJointIndexCache[hand];
     }
 
     return -1;

From a9a783588bae2dca4f048fd257253f4cca9dfe71 Mon Sep 17 00:00:00 2001
From: NissimHadar <nissim.hadar@gmail.com>
Date: Mon, 14 May 2018 09:25:17 -0700
Subject: [PATCH 18/25] Fixed typo.

---
 tools/auto-tester/src/Test.h | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/tools/auto-tester/src/Test.h b/tools/auto-tester/src/Test.h
index c7ce5f8893..0fb957d309 100644
--- a/tools/auto-tester/src/Test.h
+++ b/tools/auto-tester/src/Test.h
@@ -89,7 +89,7 @@ private:
     const int NUM_DIGITS { 5 };
     const QString EXPECTED_IMAGE_PREFIX { "ExpectedImage_" };
 
-    // We have to directories to work with.
+    // We have two directories to work with.
     // The first is the directory containing the test we are working with
     // The second contains the snapshots taken for test runs that need to be evaluated
     QString testDirectory;

From 81ebb681bb185ff5ec5430e981333425b79f7be8 Mon Sep 17 00:00:00 2001
From: Zach Fox <fox@highfidelity.io>
Date: Mon, 14 May 2018 09:35:34 -0700
Subject: [PATCH 19/25] Show invalidated items in My Purchases

---
 .../resources/qml/hifi/commerce/purchases/Purchases.qml   | 8 --------
 1 file changed, 8 deletions(-)

diff --git a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml
index d79b8d09fa..0b95f26f55 100644
--- a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml
+++ b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml
@@ -995,10 +995,6 @@ Rectangle {
 
         for (var i = 0; i < purchasesModel.count; i++) {
             if (purchasesModel.get(i).title.toLowerCase().indexOf(filterBar.text.toLowerCase()) !== -1) {
-                if (!purchasesModel.get(i).valid) {
-                    continue;
-                }
-
                 if (purchasesModel.get(i).status !== "confirmed" && !root.isShowingMyItems) {
                     tempPurchasesModel.insert(0, purchasesModel.get(i));
                 } else if ((root.isShowingMyItems && purchasesModel.get(i).edition_number === "0") ||
@@ -1055,10 +1051,6 @@ Rectangle {
             var currentId;
             for (var i = 0; i < tempPurchasesModel.count; i++) {
                 currentId = tempPurchasesModel.get(i).id;
-                
-                if (!purchasesModel.get(i).valid) {
-                    continue;
-                }
                 filteredPurchasesModel.append(tempPurchasesModel.get(i));
                 filteredPurchasesModel.setProperty(i, 'cardBackVisible', false);
                 filteredPurchasesModel.setProperty(i, 'isInstalled', ((root.installedApps).indexOf(currentId) > -1));

From 2a1c2ba7b1ce7fe3e583117f4d2a3c2f18f0f2d4 Mon Sep 17 00:00:00 2001
From: Zach Fox <fox@highfidelity.io>
Date: Mon, 14 May 2018 09:54:05 -0700
Subject: [PATCH 20/25] PurchasedItem logic

---
 .../resources/qml/hifi/commerce/purchases/PurchasedItem.qml  | 5 ++++-
 .../resources/qml/hifi/commerce/purchases/Purchases.qml      | 1 +
 2 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml b/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml
index 4db98091c1..17c42d1b08 100644
--- a/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml
+++ b/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml
@@ -49,6 +49,7 @@ Item {
     property string upgradeTitle;
     property bool updateAvailable: root.upgradeUrl !== "" && !root.isShowingMyItems;
     property bool isShowingMyItems;
+    property bool valid;
 
     property string originalStatusText;
     property string originalStatusColor;
@@ -239,6 +240,7 @@ Item {
                     width: 62;
 
                     onLoaded: {
+                        item.enabled = root.valid;
                         item.buttonGlyphText = hifi.glyphs.gift;
                         item.buttonText = "Gift";
                         item.buttonClicked = function() {
@@ -646,7 +648,8 @@ Item {
             height: 40;
             enabled: root.hasPermissionToRezThis &&
                 root.purchaseStatus !== "invalidated" &&
-                MyAvatar.skeletonModelURL !== root.itemHref;
+                MyAvatar.skeletonModelURL !== root.itemHref &&
+                root.valid;
 
             onHoveredChanged: {
                 if (hovered) {
diff --git a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml
index 0b95f26f55..8fe1ebe6c9 100644
--- a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml
+++ b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml
@@ -616,6 +616,7 @@ Rectangle {
                 upgradeTitle: model.upgrade_title;
                 itemType: model.itemType;
                 isShowingMyItems: root.isShowingMyItems;
+                valid: model.valid;
                 anchors.topMargin: 10;
                 anchors.bottomMargin: 10;
 

From ac78e145853a66e2dac0522144e623c7d7782c7d Mon Sep 17 00:00:00 2001
From: David Back <davidback@highfidelity.io>
Date: Mon, 14 May 2018 11:44:25 -0700
Subject: [PATCH 21/25] fix unequipping with capital U

---
 scripts/system/controllers/controllerModules/equipEntity.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/scripts/system/controllers/controllerModules/equipEntity.js b/scripts/system/controllers/controllerModules/equipEntity.js
index 1fce772ec8..dff1f45258 100644
--- a/scripts/system/controllers/controllerModules/equipEntity.js
+++ b/scripts/system/controllers/controllerModules/equipEntity.js
@@ -828,7 +828,7 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa
     };
     
     var onKeyPress = function(event) {
-        if (event.text === UNEQUIP_KEY) {
+        if (event.text.toLowerCase() === UNEQUIP_KEY) {
             if (rightEquipEntity.targetEntityID) {
                 rightEquipEntity.endEquipEntity();
             }

From a881d07e59f72e7de90f19478f011f0aa0c10fe3 Mon Sep 17 00:00:00 2001
From: Zach Fox <fox@highfidelity.io>
Date: Mon, 14 May 2018 13:12:58 -0700
Subject: [PATCH 22/25] MS15090: Fix gifting when running marketplaces.js
 separately

---
 scripts/system/marketplaces/marketplaces.js | 1 +
 1 file changed, 1 insertion(+)

diff --git a/scripts/system/marketplaces/marketplaces.js b/scripts/system/marketplaces/marketplaces.js
index c3edee264f..dc4d5aa844 100644
--- a/scripts/system/marketplaces/marketplaces.js
+++ b/scripts/system/marketplaces/marketplaces.js
@@ -19,6 +19,7 @@ var selectionDisplay = null; // for gridTool.js to ignore
 
     Script.include("/~/system/libraries/WebTablet.js");
     Script.include("/~/system/libraries/gridTool.js");
+    Script.include("/~/system/libraries/connectionUtils.js");
 
     var METAVERSE_SERVER_URL = Account.metaverseServerURL;
     var MARKETPLACE_URL = METAVERSE_SERVER_URL + "/marketplace";

From 83cae5460101cae5f8f5c3c76d3e76a24790ce3a Mon Sep 17 00:00:00 2001
From: Zach Fox <fox@highfidelity.io>
Date: Mon, 14 May 2018 13:34:20 -0700
Subject: [PATCH 23/25] Show 'invalidated' label for valid = false

---
 .../hifi/commerce/purchases/PurchasedItem.qml   | 17 ++++++++---------
 1 file changed, 8 insertions(+), 9 deletions(-)

diff --git a/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml b/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml
index 17c42d1b08..19b57354dc 100644
--- a/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml
+++ b/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml
@@ -465,7 +465,7 @@ Item {
 
         Item {
             id: statusContainer;
-            visible: root.purchaseStatus === "pending" || root.purchaseStatus === "invalidated" || root.numberSold > -1;
+            visible: root.purchaseStatus === "pending" || !root.valid || root.numberSold > -1;
             anchors.left: itemName.left;
             anchors.right: itemName.right;
             anchors.top: itemName.bottom;
@@ -482,7 +482,7 @@ Item {
                 text: {
                         if (root.purchaseStatus === "pending") {
                             "PENDING..."
-                        } else if (root.purchaseStatus === "invalidated") {
+                        } else if (!root.valid) {
                             "INVALIDATED"
                         } else if (root.numberSold > -1) {
                             ("Sales: " + root.numberSold + "/" + (root.limitedRun === -1 ? "\u221e" : root.limitedRun))
@@ -494,7 +494,7 @@ Item {
                 color: {
                         if (root.purchaseStatus === "pending") {
                             hifi.colors.blueAccent
-                        } else if (root.purchaseStatus === "invalidated") {
+                        } else if (!root.valid) {
                             hifi.colors.redAccent
                         } else {
                             hifi.colors.baseGray
@@ -508,7 +508,7 @@ Item {
                 text: {
                         if (root.purchaseStatus === "pending") {
                             hifi.glyphs.question
-                        } else if (root.purchaseStatus === "invalidated") {
+                        } else if (!root.valid) {
                             hifi.glyphs.question
                         } else {
                             ""
@@ -525,7 +525,7 @@ Item {
                 color: {
                         if (root.purchaseStatus === "pending") {
                             hifi.colors.blueAccent
-                        } else if (root.purchaseStatus === "invalidated") {
+                        } else if (!root.valid) {
                             hifi.colors.redAccent
                         } else {
                             hifi.colors.baseGray
@@ -540,7 +540,7 @@ Item {
                 onClicked: {
                     if (root.purchaseStatus === "pending") {
                         sendToPurchases({method: 'showPendingLightbox'});
-                    } else if (root.purchaseStatus === "invalidated") {
+                    } else if (!root.valid) {
                         sendToPurchases({method: 'showInvalidatedLightbox'});
                     }
                 }
@@ -548,7 +548,7 @@ Item {
                     if (root.purchaseStatus === "pending") {
                         statusText.color = hifi.colors.blueHighlight;
                         statusIcon.color = hifi.colors.blueHighlight;
-                    } else if (root.purchaseStatus === "invalidated") {
+                    } else if (!root.valid) {
                         statusText.color = hifi.colors.redAccent;
                         statusIcon.color = hifi.colors.redAccent;
                     }
@@ -557,7 +557,7 @@ Item {
                     if (root.purchaseStatus === "pending") {
                         statusText.color = hifi.colors.blueAccent;
                         statusIcon.color = hifi.colors.blueAccent;
-                    } else if (root.purchaseStatus === "invalidated") {
+                    } else if (!root.valid) {
                         statusText.color = hifi.colors.redHighlight;
                         statusIcon.color = hifi.colors.redHighlight;
                     }
@@ -647,7 +647,6 @@ Item {
             width: 160;
             height: 40;
             enabled: root.hasPermissionToRezThis &&
-                root.purchaseStatus !== "invalidated" &&
                 MyAvatar.skeletonModelURL !== root.itemHref &&
                 root.valid;
 

From ed726c9d77ae8e218cc51e99add5e79fbcabb1a9 Mon Sep 17 00:00:00 2001
From: "Anthony J. Thibault" <tony@highfidelity.io>
Date: Mon, 14 May 2018 13:51:13 -0700
Subject: [PATCH 24/25] OpenVR: Remove reference to HMD Standing Mode menu item

This menu item has not existed for over a year.
I was sometimes seeing a crash on startup when the display plugin
attempted to reference this menu item.
---
 interface/src/Menu.h                       | 1 -
 plugins/openvr/src/OpenVrDisplayPlugin.cpp | 3 ---
 2 files changed, 4 deletions(-)

diff --git a/interface/src/Menu.h b/interface/src/Menu.h
index 20375a71b2..be3dd705f7 100644
--- a/interface/src/Menu.h
+++ b/interface/src/Menu.h
@@ -193,7 +193,6 @@ namespace MenuOption {
     const QString ShowOtherLookAtVectors = "Show Other Eye Vectors";
     const QString EnableLookAtSnapping = "Enable LookAt Snapping";
     const QString ShowRealtimeEntityStats = "Show Realtime Entity Stats";
-    const QString StandingHMDSensorMode = "Standing HMD Sensor Mode";
     const QString SimulateEyeTracking = "Simulate";
     const QString SMIEyeTracking = "SMI Eye Tracking";
     const QString SparseTextureManagement = "Enable Sparse Texture Management";
diff --git a/plugins/openvr/src/OpenVrDisplayPlugin.cpp b/plugins/openvr/src/OpenVrDisplayPlugin.cpp
index 714cb91b3f..5a7417cb49 100644
--- a/plugins/openvr/src/OpenVrDisplayPlugin.cpp
+++ b/plugins/openvr/src/OpenVrDisplayPlugin.cpp
@@ -36,7 +36,6 @@
 
 Q_DECLARE_LOGGING_CATEGORY(displayplugins)
 
-const char* StandingHMDSensorMode { "Standing HMD Sensor Mode" }; // this probably shouldn't be hardcoded here
 const char* OpenVrThreadedSubmit { "OpenVR Threaded Submit" }; // this probably shouldn't be hardcoded here
 
 PoseData _nextRenderPoseData;
@@ -451,7 +450,6 @@ bool OpenVrDisplayPlugin::internalActivate() {
     qDebug() << "OpenVR Threaded submit enabled:  " << _threadedSubmit;
 
     _openVrDisplayActive = true;
-    _container->setIsOptionChecked(StandingHMDSensorMode, true);
     _system->GetRecommendedRenderTargetSize(&_renderTargetSize.x, &_renderTargetSize.y);
     // Recommended render target size is per-eye, so double the X size for 
     // left + right eyes
@@ -507,7 +505,6 @@ void OpenVrDisplayPlugin::internalDeactivate() {
     Parent::internalDeactivate();
 
     _openVrDisplayActive = false;
-    _container->setIsOptionChecked(StandingHMDSensorMode, false);
     if (_system) {
         // TODO: Invalidate poses. It's fine if someone else sets these shared values, but we're about to stop updating them, and
         // we don't want ViveControllerManager to consider old values to be valid.

From d4d2f3c9573f375a8af51886a8e468a0c9500386 Mon Sep 17 00:00:00 2001
From: NissimHadar <nissim.hadar@gmail.com>
Date: Mon, 14 May 2018 16:32:04 -0700
Subject: [PATCH 25/25] Moved ownership of test location into the Test
 Scripting class.

---
 interface/src/Application.cpp                      | 10 +++++-----
 interface/src/Application.h                        |  3 ---
 interface/src/scripting/TestScriptingInterface.cpp |  9 ++++-----
 interface/src/scripting/TestScriptingInterface.h   |  8 ++++++--
 4 files changed, 15 insertions(+), 15 deletions(-)

diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp
index 08550a81f0..61ed5acdd2 100644
--- a/interface/src/Application.cpp
+++ b/interface/src/Application.cpp
@@ -1033,7 +1033,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
 
                 QFileInfo fileInfo(path);
                 if (fileInfo.isDir() && fileInfo.isWritable()) {
-                    testResultsLocation = path;
+                    TestScriptingInterface::getInstance()->setTestResultsLocation(path);
                 }
             }
         }
@@ -7591,8 +7591,8 @@ void Application::loadAvatarBrowser() const {
 void Application::takeSnapshot(bool notify, bool includeAnimated, float aspectRatio, const QString& filename) {
     postLambdaEvent([notify, includeAnimated, aspectRatio, filename, this] {
         // Get a screenshot and save it
-        QString path =
-            Snapshot::saveSnapshot(getActiveDisplayPlugin()->getScreenshot(aspectRatio), filename, testResultsLocation);
+        QString path = Snapshot::saveSnapshot(getActiveDisplayPlugin()->getScreenshot(aspectRatio), filename,
+                                              TestScriptingInterface::getInstance()->getTestResultsLocation());
 
         // If we're not doing an animated snapshot as well...
         if (!includeAnimated) {
@@ -7607,8 +7607,8 @@ void Application::takeSnapshot(bool notify, bool includeAnimated, float aspectRa
 
 void Application::takeSecondaryCameraSnapshot(const QString& filename) {
     postLambdaEvent([filename, this] {
-        QString snapshotPath =
-            Snapshot::saveSnapshot(getActiveDisplayPlugin()->getSecondaryCameraScreenshot(), filename, testResultsLocation);
+        QString snapshotPath = Snapshot::saveSnapshot(getActiveDisplayPlugin()->getSecondaryCameraScreenshot(), filename,
+                                                      TestScriptingInterface::getInstance()->getTestResultsLocation());
 
         emit DependencyManager::get<WindowScriptingInterface>()->stillSnapshotTaken(snapshotPath, true);
     });
diff --git a/interface/src/Application.h b/interface/src/Application.h
index d61e986e55..17e28f0e6e 100644
--- a/interface/src/Application.h
+++ b/interface/src/Application.h
@@ -419,8 +419,6 @@ public slots:
     void updateVerboseLogging();
     Q_INVOKABLE void openAndroidActivity(const QString& activityName);
 
-    QString getTestResultsLocation() { return testResultsLocation; };
-
 private slots:
     void onDesktopRootItemCreated(QQuickItem* qmlContext);
     void onDesktopRootContextCreated(QQmlContext* qmlContext);
@@ -754,7 +752,6 @@ private:
     std::atomic<bool> _pendingIdleEvent { true };
     std::atomic<bool> _pendingRenderEvent { true };
 
-    QString testResultsLocation;
     bool quitWhenFinished { false };
 };
 #endif // hifi_Application_h
diff --git a/interface/src/scripting/TestScriptingInterface.cpp b/interface/src/scripting/TestScriptingInterface.cpp
index 15a024fa03..700994c517 100644
--- a/interface/src/scripting/TestScriptingInterface.cpp
+++ b/interface/src/scripting/TestScriptingInterface.cpp
@@ -162,8 +162,7 @@ void TestScriptingInterface::clearCaches() {
 
 // Writes a JSON object from javascript to a file
 void TestScriptingInterface::saveObject(QVariant variant, const QString& filename) {
-    QString testResultsLocation = qApp->getTestResultsLocation();
-    if (testResultsLocation.isNull()) {
+    if (_testResultsLocation.isNull()) {
         return;
     }
 
@@ -176,11 +175,11 @@ void TestScriptingInterface::saveObject(QVariant variant, const QString& filenam
     QByteArray jsonData = jsonDocument.toJson();
 
     // Append trailing slash if needed
-    if (testResultsLocation.right(1) != "/") {
-        testResultsLocation += "/";
+    if (_testResultsLocation.right(1) != "/") {
+        _testResultsLocation += "/";
     }
 
-    QString filepath = QDir::cleanPath(testResultsLocation + filename);
+    QString filepath = QDir::cleanPath(_testResultsLocation + filename);
     QFile file(filepath);
 
     file.open(QFile::WriteOnly);
diff --git a/interface/src/scripting/TestScriptingInterface.h b/interface/src/scripting/TestScriptingInterface.h
index 4b469244d1..5666417727 100644
--- a/interface/src/scripting/TestScriptingInterface.h
+++ b/interface/src/scripting/TestScriptingInterface.h
@@ -18,6 +18,10 @@ class QScriptValue;
 class TestScriptingInterface : public QObject {
     Q_OBJECT
 
+public:
+    void setTestResultsLocation(const QString path) { _testResultsLocation = path; }
+    const QString& getTestResultsLocation() { return _testResultsLocation;  };
+
 public slots:
     static TestScriptingInterface* getInstance();
 
@@ -46,7 +50,6 @@ public slots:
     */
     void waitIdle();
 
-
     bool waitForConnection(qint64 maxWaitMs = 10000);
 
     void wait(int milliseconds);
@@ -90,6 +93,7 @@ public slots:
 
 private:
     bool waitForCondition(qint64 maxWaitMs, std::function<bool()> condition);
+    QString _testResultsLocation;
 };
 
-#endif // hifi_TestScriptingInterface_h
+#endif  // hifi_TestScriptingInterface_h